警告:本版本为尚未完成的开发版,许多功能不完善且未经验证。请在使用前对你的地图做好备份。
relert.js
是由Heli开发的系列JavaScript
库,它允许地图制作者使用原生JavaScript
直接对游戏《红色警戒2》的地图文件(.map
等)进行操作,从而更方便的编写脚本处理地图。
FinalAlert的“工具脚本”功能使用了自带的类似汇编语言的低级脚本语言,它过于底层的语法不能清晰地表述逻辑,代码编写困难且不可读,其运行效率也是相当低。
而有了relert.js
,你就可以解放JavaScript
作为一门高级语言完全的编程能力,用更少的代码,以更高的运行效率,来实现更复杂的逻辑。
最初版本的relert.js
运行在node.js
环境下,虽然我自己使用它感到十分满意,但大部分地图制作者并没有node.js
的运行环境。
为了实现对大多数人的“开箱即用”,我决定将relert.js
的运行环境移入浏览器——浏览器嘛,没有人的电脑里面没有这个。
这就促成了relert.js-browser
这一分支项目。
当然,除了运行环境的移植以外,我还希望通过一些新技术的使用,进一步简化逻辑,让使用者在调用接口的时候可以得到更加符合直觉的结果。为此我也将代码进行了大规模的重构,希望这些重构是值得的。
relert.js-browser
主要通过网页加载的方式运行。
需要浏览器支持ECMAScript6标准和一些比较新的特性,具体版本为:
- Chrome 71 版本及以上
- Edge 79 版本及以上
- Firefox 65 版本及以上
- Safari 12.1 版本及以上
- Opera 58 版本及以上
- 各种双核浏览器请使用“极速模式”
- 不支持IE所有版本,及各种IE内核的套壳浏览器
一句话概述:除IE以外主流浏览器的最新版本均可使用。
当然除了relert.js-browser
框架本身的运行需求以外,用户编写出来的的脚本也有自己的运行需求——如果你使用了更加激进的新特性来编写脚本,那么使用的范围就会更窄。
不过,鉴于relert.js-browser
是一件有效的生产力工具,没有必要牺牲开发的便捷度而去追求对远古浏览器的兼容性,还是请诸位mapper把自己的浏览器升级到最新版为宜。
除了浏览器之外,你还可以通过node.js
加载relert.js
,以纯命令行的方式执行。
如同一个网站一样,你可以使用index.html
作为relert.js-browser
的入口,它会为你提供一个浏览器窗口,其中包含一个简单易用的文本编辑器界面。
但你也可以在自己编写网页的环境中运行,或者在node.js
环境中使用。在文档的后续章节会提供相关的参考。
“快速上手”部分的教程都以默认的index.html
作为规范。
relert.js-browser
的用户界面总体分为左右两栏,而右侧有上下两个区域,它们的功能如此划分:
- 左栏:地图快照列表。
relert.js-browser
用“快照”这一概念来管理地图在各个时刻的状态。你可以上传一张地图,分别执行多个脚本,生成多个“快照”,并随意在这些“快照”之间切换。 - 右栏上半部分:脚本编辑区。这里有一个简易的多标签文本编辑器界面,可以在这里查看并编写需要在地图上执行的
JavaScript
脚本。 - 右栏下半部分:调试信息区。脚本执行时输出的调试信息、警告信息、报错信息都会显示在这个区域。
下面,我们试着编写并运行一些脚本。
在右栏上半部分的脚本编辑区键入以下代码:
relert.log('helloworld');
然后点击脚本编辑区上方的“运行”按钮。
你可以看到,在右栏下半部分的调试信息区出现了类似这样的内容:
[2021.08.16 20:30:17] - helloworld
前面方括号内的内容是程序执行时的时间,而在后面,我们可以看到,字符串“helloworld”被成功输出了。It works!
本文档不会讲述JavaScript
语言的语法,只会描述relert.js
提供的接口——毕竟relert.js
只是一个JavaScript
的库而已,它并不是一个脚本引擎。也就是说,只要是你的浏览器支持的JavaScript
语法,都可以在relert.js-browser
之中运行!
JavaScript
语言本身的教程很容易搜索到。或者照着本文档末尾的“常用逻辑示例”照葫芦画瓢,也可以很快写出能用的代码。
relert.log(info: String)
这个JavaScript
函数用于在调试信息区输出内容,这个函数不需要在某一张地图上就可以执行。
如果想要使用脚本具体操作地图上的物件,我们需要先加载一张地图。
加载地图的按钮在整个网页的左上角,左栏地图快照列表的最上端。按下它,会弹出一个选择文件的对话框。
选择你自己的地图文件以后,你会发现地图快照列表里面多了一项,它展示了你刚才打开地图的名称、打开时间,下面还用斜体字展示了“打开文件”四个字——这就是一个“快照”。(注:为了接下来的示例脚本顺利运行,选中的地图上应该有平民建筑物)
快照代表了一张地图在某时某刻的状态,以下三种操作会生成新的快照:
-
打开地图文件。
-
在一个快照上执行脚本。“执行脚本”的过程其实就是:脚本在当前快照的状态上进行操作,操作的结果会存入下一个状态,也就是新的快照。
-
刚刚打开
relert.js-browser
时,从缓存中恢复上一次关闭之前的最后一个快照。
接下来我们尝试通过执行脚本的方式生成快照。在右栏上半部分的脚本编辑区键入以下代码:
relert.Structure.forEach((structure) => {
if (structure.House == 'Neutral House') {
structure.Strength = relert.randomStrength(0.15, 0.24);
relert.log(`${structure.Type}, ${structure.Strength}`);
}
});
然后点击脚本编辑区上方的“运行”按钮。
这次在右栏下半部分的调试信息区出现了更多的内容,而且我们看到,左侧边栏的地图快照列表中新增了一个快照——它就是这段脚本执行以后地图的状态。而在这个快照的上方,地图刚打开时的原始状态仍然保留在快照列表中。
你可以通过快照列表里面的“载入”按钮随时切换哪一个快照为当前快照(在列表中使用彩色显示)。脚本的执行都是在当前快照上执行。
“保存”按钮允许你把任何一个快照以文件的形式下载。
“删除”按钮允许你删除某个快照。注意,删除快照的操作是不可恢复的。
我们回到上面的那段测试代码。它具体做了什么呢?
作为relert.js
这个库的入口,我们提供了relert
这一全局对象作为所有函数的挂载点。也就是说,我们调用relert.js
的所有接口时,都以relert.XXXX
的格式调用。
relert.Structure
提供了“建筑(Structure)”这一数据代理。数据代理这一概念在后面文档中会详细的讲,但在这里,你可以暂时理解为:我们通过relert.Structure
这一接口,来更加方便直观的操作建筑对象。
有了relert.Structure
以后,我们可以通过relert.Structure.forEach
函数这一遍历器来遍历所有的建筑。
relert.Structure.forEach((structure) => {
// 相当于从relert.Structure中,取出每一个structure,进行这里面的操作
// 中间写的代码都是针对structure的操作
// structure只是一个变量名,你可以任意的自定义,比如用i都行
});
中间部分的代码就是对单个Structure
的子数据代理进行的操作了。之所以叫“子数据代理”,是因为数据代理Structure
下面还有另外一层数据代理,可以让你十分舒服的对单个的某一座建筑进行操作,比如,形如JavaScript
的赋值语句XXXX = XXXX
的形式:
// 如果这个structure的House属性(所属方属性)等于'Neutral House',即单人任务中的平民作战方
if (structure.House == 'Neutral House') {
// 那么,设置这个structure的生命值,即Strength属性,为15%~24%之间的随机值
// relert.randomStrength这一函数也由relert.js库提供
structure.Strength = relert.randomStrength(0.15, 0.24);
// 输出这个建筑的类型,以及它的当前生命值
relert.log(`${structure.Type}, ${structure.Strength}`);
}
完整的拼起来就是:
// 相当于从relert.Structure中,取出每一个i,进行这里面的操作
relert.Structure.forEach((i) => {
// 如果这个建筑i的House(所属方)属性等于'Neutral House'
if (i.House == 'Neutral House') {
// 那么,设置这个建筑i的Strength(生命值)属性为15%~24%之间的随机值
i.Strength = relert.randomStrength(0.15, 0.24);
// 输出这个建筑i的类型,以及它的当前生命值
relert.log(`${i.Type}, ${i.Strength}`);
}
});
以上脚本中,每一条代码都带有强烈的语义性,这是接口的优化带来的正面效果。希望这样的代码能读起来能像流程图一样清晰明确,并帮助你构建更加复杂的逻辑。
“快速上手”部分到此为止。想要完成更多的功能,其实你需要的只是在这篇文档中,找到相应的接口,然后用JavaScript
的方式合理的调用它们。
祝Scripting愉快!
由于relert.js
和relert.js-browser
暴露的接口几乎完全一致,下文如不特殊说明,均使用relert.js
统一指代。
relert.js
会将整张地图的INI
结构,转化为JavaScript Object
类型的数据对象,并通过relert.INI
这一属性对外暴露。
INI
文件格式是[Section] key = value
的两层结构,转成JavaScript Object
以后也是一个两层的结构,如同这样:
{
Section: {
key: 'value',
},
}
我们来举一个稍微复杂点的例子。
比如说一段INI
的结构是这样的:
[SectionA]
keyA1 = valueA1
keyA2 = valueA2
[SectionB]
keyB1 = valueB1
keyB2 = valueB2
把它转化成JavaScript Object
就是:
{
SectionA : {
keyA1 : 'valueA1',
keyA2 : 'valueA2',
},
SectionB : {
keyB1 : 'valueB1',
keyB2 : 'valueB2',
},
}
在这个例子中,我们在relert.js
中想访问SectionA
中的keyA1
属性,可以这样写:
relert.INI['SectionA']['keyA1']
对这个属性进行直接的取值或者赋值都是可以的。
这就是对INI
的直接操作。
(注意:relert.INI['SectionA']
不一定存在,有时候你的代码需要考虑它不存在、 即读取出来的值是undefined
的情况,以避免程序出错)
如果想要同时对多个属性进行操作,可以考虑使用我们在relert.Toolbox
模块为Object
引入的原型方法:Object.prototype.assign
。
下面展示一个实用性的例子:
// 本段程序作用:把列表中的平民单位的属性,都改为“可被自动攻击、生命值50点”
// 需要修改属性的平民单位列表
let civList = ['CIV1', 'CIV2', 'CIV3', 'CIVA', 'CIVB', 'CIVC'];
for (let i in civList) {
// 判断对应的平民单位字段是否存在
if (!relert.INI[civList[i]]) {
// 如果不存在就新建一个空的
relert.INI[civList[i]] = {};
}
// 使用assign()方法将后面的对象合并入前面的relert.INI[civList[i]]
relert.INI[civList[i]].assign({
// 这样利用合并机制,可以同时修改多个属性
Insignificant: 'yes',
Strength: 50,
});
}
relert.INI
提供了最基本的底层接口。其实它已经允许我们直接修改地图相关的任何底层数据(毕竟红色警戒2的地图本质上就是一个INI
文件),但是这种修改方式只对修改内置rules
显得比较友好,进行其它地图相关的操作就显得非常繁琐了——当然,relert.js
一定会有让你满意的办法!这就要等后面章节慢慢介绍了。
除了直接操作INI以外,你还可以通过relert.js
抽象出的数据代理接口,以一种人类可读的方式,对一些在游戏中有明确意义的属性进行操作。
为什么称之为“数据代理”呢?因为它只是对relert.INI
的操作进行转化的中间层,并没有在relert.INI
以外的地方存储额外的数据——这意味着,你对数据代理进行任何操作后,其背后的实际数据,relert.INI
中的对应字段,也会进行实时的更新。
数据代理的具体实现使用了JavaScript
的Proxy
对象:Proxy
对象允许我们接管对一个对象所有的操作,包括对它属性的赋值等基础操作。比如,我们想要修改某座建筑物的生命值,通过数据代理,只需要做以下操作:
- 通过在建筑列表的数据代理
relert.Structure
之中,(通过索引或者遍历器),找到这个具体建筑的数据代理a
。 - 直接把
a.Strength
赋值为你需要的值。
这样就完成了“修改生命值”的过程。其余的操作,包括解析建筑数据的编码解码、导入INI等操作,relert.js
提供的数据代理都会在内部帮你完成。
接下来,本文档将按照大类,具体罗列relert.js
中提供的所有数据代理。
物体Object
描述这样一类对象:它们被放在地图上的某个位置。即,每一个物体Item
,都有明确的位置坐标(X, Y)
,对应属性Item.X
和Item.Y
。
由于它们都具有X
和Y
属性,因此它们都可以被当成“坐标”,传入需要输入坐标的函数接口。
属于物体Object
的对象有以下几类:
INI中的注册位置 | relert.js中的访问接口 | 描述 |
---|---|---|
Structure |
relert.Structure |
建筑物 |
Infantry |
relert.Infantry |
步兵 |
Units |
relert.Unit |
车辆单位 |
Aircraft |
relert.Aircraft |
飞行器 |
Terrain |
relert.Terrain |
地形对象 |
Smudge |
relert.Smudge |
污染 |
Waypoint |
relert.Waypoint |
路径点 |
各作战方注册表下 | relert.BaseNode |
基地节点 |
CellTags |
relert.CellTag |
单元标记 |
注意 :覆盖物Overlay
在地图中的存储方式有点特殊——它是以Base64编码的二维数据进行存储的。因此在relert.js
中,并没有把它归类于物体Object
,而是归类于后面介绍的MapData
类型。
下面章节将会介绍一些适用于所有Object
“物体”数据代理的公共操作。
虽然它们在地图中的存储形式不尽相同,但是relert.js
尽量把它们包装成了相同的接口。接下来的部分都使用建筑Structure
来举例。
几乎所有的物体都是以一个列表来存储的,因此均可以以数据代理作为入口,按照注册号进行访问,得到表示单个物体的子代理:
relert.Structure[0] //表示地图上的0号建筑
relert.Structure[1] //表示地图上的1号建筑
你也可以通过count
或者length
属性,来获得一类物体的总数量:
relert.Structure.count //地图上建筑的总数量
relert.Structure.length //同上一条
对于一个Object
类型数据代理,relert.js
提供了两种接口来方便的遍历它:forEach
函数和for ... of
循环。
// forEach遍历接口
relert.Structure.forEach((item, index) => {
//item为Structure之中一个元素的数据代理
//index为该元素的注册号(也可以不使用index)
});
// for ... of遍历接口
for (item of relert.Structure) {
//item为Structure之中一个元素的数据代理
}
虽然通过count
或length
获得所有注册号,然后通过for
循环按照注册号进行访问是可行的,但还是推荐使用上文的两种接口——它们内部对删除做了特殊处理,允许方便的边遍历边删除。
// 遍历的同时进行删除
relert.Structure.forEach((item) => {
if (...) { //如果满足了一定的条件
item.delete(); //此时删除正在被访问的item,遍历器仍然保证以后可以访问到每一个元素
}
});
当我们需要新增一个物体的时候,只需要调用它相应代理下的add
接口:
// 添加一个新建筑
relert.Structure.add({
X: 12, //要增加的新建筑的属性,详见各个子代理的参数列表
Y: 34,
...//未指定的参数会被设置为默认值,详见各个子代理的参数列表
});
add
接口会返回新增物体的子代理。
通过注册号或者遍历器获得子代理以后,相当于直接对某一个物体的属性进行操作了。
可以把物体的属性当作对象属性,直接进行赋值和取值操作:
// 赋值和取值操作
relert.Structure.forEach((item) => { //此时item就表示了单独的某座建筑
relert.log(item.Type); //直接输出item.Type的值
item.Strength = 255; //直接给item.Strength赋值255
});
这些数据操作都会即时的反馈在INI
的变化中。
关于物体的属性,详见各个子代理的参数列表。
同时修改多个属性建议使用assign
接口:
// assign接口同时设置多个属性的值
relert.Structure.forEach((item) => { //此时item就表示了单独的某座建筑
item.assign({ //使用子代理的assign接口
Strength: 255,
AIRepair: 1, //同时设置多个属性的值
});
});
除了在遍历器中通过item.delete()
删除特定的某个物体,主代理还提供了批量删除接口delete
。该接口提供了两种使用方式,分别为输入一个对象和输入一个函数。
输入对象,则是遍历其中的每一个物体,其属性与对象完全匹配时才执行删除:
// delete接口的第一种使用方式:输入一个对象
relert.Structure.delete({
House: 'Americans House',
Strength: 255,
}); //从Structure中,删除所有House属性为Americans House且Strength为255的物体。
//有多个属性必须完全匹配才会删除
输入函数,则是把每一个物体输入函数,返回true
的时候会执行删除。
// delete接口的第二种使用方式:输入一个函数
relert.Structure.delete((item, index) => { //和forEach遍历器接口相同
if ((item.House == 'Americans House') && (item.Strength == '255')) {
return true; //函数返回值为true,该对象就会被删除
} else {
return false; //函数返回值为false就会删除
}
});
对于一个未知类型的物体,我们可以通过读取它的$category
属性,来确定它属于哪个数据代理:
// 比如,structure是一个建筑子代理
structure.$category == 'Structure';
// 再比如,waypoint是一个路径点子代理
waypoint.$category == 'Waypoint';
属性 | 描述 | 默认值 |
---|---|---|
House |
所属方 | 'Neutral House' |
Type |
注册名 | 'GAPOWR' |
Strength |
生命值 | '255' |
X |
x 坐标 |
'0' |
Y |
y 坐标 |
'0' |
Facing |
面向 | '0' |
Tag |
关联标签 | 'none' |
Sellable |
可变卖(无用属性) | '0' |
Rebuild |
重建(无用属性) | '0' |
Enabled |
启用 | '1' |
UpgradesCount |
加载物数量 | '0' |
SpotLight |
聚光灯 | '0' |
Upgrade1 |
加载物1 | 'none' |
Upgrade2 |
加载物2 | 'none' |
Upgrade3 |
加载物3 | 'none' |
AIRepair |
AI修复 | '1' |
ShowName |
显示名称 | '0' |
属性 | 描述 | 默认值 |
---|---|---|
Type |
注册名 | 'TREE01' |
X |
x 坐标 |
'0' |
Y |
y 坐标 |
'0' |
注意:同一个位置上只有一个地形对象。这意味着你在修改地形对象的X
或者Y
属性的时候,可能会覆盖掉原有位置上的地形对象。
属性 | 描述 | 默认值 |
---|---|---|
Type |
注册名 | 'CRATER01' |
X |
x 坐标 |
'0' |
Y |
y 坐标 |
'0' |
Ignore |
是否被忽略(填非0的值都会被忽略) | '0' |
(施工中)
(施工中)
地表MapData
描述这样一类对象:它由覆盖整张地图的Base64
编码+lzo
压缩数据组成。在使用之前,需要先对整个区段进行解码解压缩,才能得到可直接操作的数据。
解码/解压缩是一个费时费力的操作。relert.js
在设计时为了避免重复做无用功,亦或在不需要解码/解压缩的时候浪费计算资源,采用了“需要时调用接口进行手动解码”的方式。
这里介绍一些MapData
数据代理所共有的接口。为了演示方便,接下来的操作都以地形MapPack
为例。
MapData
型的数据代理本身被看做一个函数:
// 在函数被调用时进行解包,需要访问MapData类型代理的操作全都在这个函数内进行
relert.MapPack((data) => {
data({X: 12, Y: 12}) // 获取坐标(12, 12)处的MapPack对象数据
});
(施工中)
(施工中)
逻辑Logic
描述这样一类对象:
(施工中)
(施工中)
(施工中)
(施工中)
(施工中)
(施工中)
注册表Register
描述这样一类对象:由一系列INI
结构的Section
组成,但是存在一个Section
,其它的Section
名字必须注册在它下面。
选择器对象Picker
提供了对代理的另一个访问接口。
(施工中)
“静态模块”指没有自己独立入口的模块,它们在被导入以后,会在全局对象relert
上直接附加一些属性或者方法。
静态模块都由relert.Static
类继承而来。
环境变量模块relert.Static.Environment
导出了几个非常基本的纯静态属性:
-
relert.isNode: Boolean
:当前脚本是否在node.js
下运行。 -
relert.isBrowser: Boolean
:当前脚本是否在浏览器下运行。 -
relert.version: String
:当前relert.js
的版本号。
需要时直接使用即可,没什么好说的。
JavaScript
是一种单线程的语言,用户调用relert.js
执行JavaScript
代码的时候,只能把用户编写的脚本注入到主线程中执行。在执行的过程中,主线程(含用户界面)会被阻塞,浏览器窗口会进入一种“假死”状态,直到脚本执行结束。
但这就引发了一个重大的安全隐患:万一用户编写的脚本中含有死循环、无限递归等导致脚本不能执行结束的问题,整个浏览器窗口就无法再使用了。用户可能会收到浏览器的报错:“喔唷,崩溃了!”(Chrome的提示如此)然后被迫关闭整个页面,从而丢失工作区中的所有未保存的内容,至少绝大部分内容都不可恢复。
而由于JavaScript
的单线程特性,我们无法方便的在代码段的外部,对代码本身的执行状况进行监听——所有操作都在主线程之中排队进行,就连用于监听的函数自己也会被堵死在JavaScript
事件队列中。
为了在这种情况下仍然能保护主线程,我开发了relert.Static.Tick
模块来解决这一问题。由于它采用了“注入脚本内部,不断唤起自身”的方式对用户脚本的运行时间进行监测,因此我称其为“打点计时器”。
在relert.js-browser
的环境下,一般来说,不用你做任何事情,relert.Static.Tick
模块就已经在保护你的主线程了。
在一张地图上尝试以下脚本:
// 死循环,不断的在地图上添加单位
while (true) {
relert.Unit.add({
Type: 'MTNK',
X: 40,
Y: 40,
});
}
运行数秒之后,CPU狂转,你会观察到主界面恢复了响应,并输出了类似如下报错信息:
Time Limit Exceeded: [3000.100000023842ms > 3000ms]
The script process has already been killed.
Please check if there is an INFINITE LOOP in your script,
or, manually increase the time limit with method <relert.tickTimeOut(number[in millseconds])>.
于是,我们的脚本在3000毫秒内没有执行完毕,就成功的抛出了一个异常并终止了进程。
relert.Static.Tick
默认的超时时间为3000毫秒即3秒。
不像效率低下的fscript
,对于relert.js
来说,这些时间已经足以应付大多数的状况。但如果你要处理的数据量真的很多,你可能需要自行更改这个阈值,让relert.Static.Tick
容忍更长的运行时间,说不定再运行几秒就能出结果了呢。
为此,relert.Static.Tick
提供了一个挂载在全局对象relert
的接口:
relert.tickTimeOut(timeOut: Number): Number;
接收数值类型输入,并返回一个数值,其含义均代表当前relert.Static.Tick
模块的全局等待时间,单位为毫秒。
设置等待时间:
relert.tickTimeOut(5000); //将等待时间设为5000毫秒,即5秒
获取并打印当前的等待时间:
relert.log(relert.tickTimeOut()); //输出当前的等待时间
你可能会注意到,relert.Static.Tick
模块无法对一些没有调用relert
相关接口的耗时工作进行监听,比如说你尝试在relert.js
中写一个空的死循环:
//一个死循环
while (true) { }
或者不那么明显的,在一个复杂的计算过程中:
//一个有错误的验证角谷猜想的程序
let collatz = function(start) {
let step = 0;
let number = start;
while (number != 7) { //这里不小心把终止条件1写成了7,导致部分条件下程序无法停止
if ((number % 2) == 0) {
number = parseInt(number / 2);
} else {
number = number * 3 + 1;
}
step ++;
}
return step;
}
// 调用函数以后,就会进入死循环
relert.log(`Steps: ${collatz(180352746940718527, 0)}`);
这段代码仍然会引起整个浏览器的崩溃,relert.Static.Tick
模块你干什么吃的?
这要从relert.Static.Tick
模块的原理讲起。
该模块中导出了一个全局函数relert.tick()
。每当relert.tick()
被调用,都相当于脚本向监视者relert.Static.Tick
主动报告状态,此时该模块就可以暂时接管主线程,快速的做时间计算、异常处理等操作。而relert.Static.Tick
模块在自身被加载时,就将自身的relert.tick()
函数注入到INI访问的底层接口中——也就是说,你每次使用relert.js
提供的其它模块的接口时,relert.tick()
函数都会自动插入执行。(不必担心这个操作耗费大量时间,relert.tick()
被设计为可以快速多次调用,消耗性能极低。)
所以,当一个耗时操作之中完全没有使用relert.js
提供的数据代理接口时,relert.Static.Tick
模块就监视不到它了。在我们编写操作地图对象的脚本时,这样的耗时操作真的少见……但偶尔真的需要监视这样一个纯计算函数的时候,就需要我们手动去调用了,比如这样:
//一个死循环2.0
while (true) {
relert.tick();
}
或者这样:
//一个有错误的验证角谷猜想的程序 2.0
let collatz = function(start) {
let step = 0;
let number = start;
while (number != 7) { //这里不小心把终止条件1写成了7,导致部分条件下程序无法停止
if ((number % 2) == 0) {
number = parseInt(number / 2);
} else {
number = number * 3 + 1;
}
step ++;
relert.tick(); //把 relert.tick() 放到最底层的循环之内,进行打点操作
}
return step;
}
// 调用函数以后,relert.Tick模块会正确的监视脚本超时并抛出异常
relert.log(`Steps: ${collatz(180352746940718527, 0)}`);
相比之前完全自动的“打点计时器”,这样的操作被我称为“手动打点”。
除了尽职尽责的保护你的主界面不被卡死以外,relert.Static.Tick
模块还提供了简明易用的自定义监听功能:它可以监听任何一个函数执行了多久,也可以对任何一个函数开启超时保护,抛出异常,并允许脚本的其他部分捕获异常进行处理。
relert.Static.Tick
模块提供了这样的一个静态函数入口:
relert.tickProcess(process: Function, [processId: Any, timeOut: Number]);
process
:必需,被监听的函数;processId
:监听任务的唯一ID,用于区分不同的监听起点与等待时间。具体是什么值无所谓,只要唯一且对应即可。缺省值为全局监听IDSymbol
。timeOut
:监听任务的等待时间,单位为毫秒。缺省值为全局等待时间(即relert.tickTimeOut()
的返回值)。
而我们的“打点”操作relert.tick()
函数除了可以无参数调用以表示全局打点以外,它也可以有自己的参数和返回值:
relert.tick([processId: Any]): number;
processId
:监听任务的唯一ID,与relert.tickProcess()
的参数对应。缺省值为全局监听ID。- 返回值:当前监听任务已经执行的时间。如果在指定ID的监听任务之外被调用,则返回0。
**自定义监听必须使用自定义打点。**也就是说,relert.tickProcess()
所监听的函数之中,必须有对应ID的relert.tick( ID )
打点。
下面的范例中,使用字符串'proc'
作为了监听任务的唯一ID,等待时间100毫秒,超时则抛出异常:
relert.tickProcess(() => {
while (true) {
relert.tick('proc');
}
}, 'proc', 100);
当然最适合做唯一ID的还是Symbol
类型:
let procId = Symbol();
relert.tickProcess(() => {
while (true) {
relert.tick(procId);
}
}, procId, 100);
可以读取relert.tick()
的返回值,获取“这个被监听的函数已经执行了多久”:
let procId2 = Symbol();
relert.tickProcess(() => {
while (true) {
relert.log(relert.tick(procId2));
}
}, procId2, 100);
除了relert.tickProcess()
方法来监听一个函数以外,我们还提供了另一种方式:把两个函数relert.tickStart()
和relert.tickEnd()
分别放到需要监听代码段的开头和末尾:
relert.tickStart([processId: Any, timeOut: Number]);
relert.tickEnd([processId: Any]);
比如这样:
let procId3 = Symbol();
relert.tickStart(procId3, 100);
while (true) {
relert.log(relert.tick(procId3));
}
relert.tickEnd(procId3);
自定义监听功能抛出的异常是可以被try..catch
语句捕获的,像下面这样:
let procId4 = Symbol();
relert.tickStart(procId4, 100);
try {
while (true) {
relert.log(relert.tick(procId4));
}
} catch(e) {
//捕获异常并进行处理
} finally {
//结束处理
relert.tickEnd(procId4);
}
这就使得我们可以在程序的局部,给一些可能会无法终止的步骤(比如说尝试寻找某个数学问题的可行解)加一层保险:如果它运行超过一定时间,就判定它无法成功,从而避免程序整体崩溃,方便进行后续的处理。
node.js
环境下的relert.Static.Tick
模块的行为和在浏览器中行为稍有不同。
在node.js
下。relert.Static.Tick
不会自动启动全局的监听,需要手动执行relert.tickStart()
来开启监听。毕竟node.js
下不存在崩掉整个工作区的问题,即便是出现了死循环也可以通过Ctrl+C
组合键来打断。另外,默认关闭也方便了在node.js
的交互式执行界面进行操作——如果监听开着,那么你每敲入一行代码,都会产生超时异常!
调试输出是一个最基本的功能。relert.Static.Log
模块在node.js
环境和浏览器环境下均可使用,但它在node.js
环境下和自带的console
对象差别不大。
relert.Static.Log
模块提供的接口有如下几个:
relert.log(...info: Any);
输出调试信息。没什么好说的,就是在调试区显示一段文字。
如果要输出多个变量的值,可以使用逗号分隔,也使用模板字符串(即类似`${var}`的格式)。
relert.warn(...warning: Any);
输出警告调试信息。以黄色为主色调显示,其余同上。
relert.error(...error: Any);
输出错误信息,以红色为主色调显示。注意这里并没有真的引发异常使程序终止。
relert.cls();
清空输出区域。在浏览器环境下就是编辑器下方的调试区,在node.js
环境下就是整个console
窗口。
relert.Static.Log
模块在浏览器中运行时,其内部做了缓存操作。这使得一瞬间连续输出几十万条信息,也不会让浏览器当场崩溃(虽然这仍然是很不好的习惯,过多的调试信息可能会让浏览器变卡)。
使用relert.log
代替console.log
的一个另好处是,脚本在浏览器端的行为和在node.js
端的行为被统一了起来,使得我们更容易写出能在两个环境下通用的脚本。
中文文本编码问题一直是远古软件的一大积弊,FinalAlert
也是一样。基于旧地图编辑器FinalAlert
制作的地图,一律以GB2312
格式进行中文文本编码;而JavaScript
的字符串只支持UTF-8
编码;一些新开发的地图编辑器如RelertSharp
则采用纯UTF-8
格式……如此种种不统一的问题造成了中文读写时常出现乱码,为此我开发了relert.Static.GB2312
模块。
relert.Static.GB2312
模块基于iconv-lite
库的改写、打包与再封装,为relert.js
在浏览器环境和node.js
环境提供了基础的编码解码支持。正是在这个模块的加持下,relert.js
在UTF-8
和GB2312
模式下均可工作,并且可以把文件在这两种格式之间任意转换。
relert.Static.GB2312
模块已经与relert.Static.FileSys
模块做了深度的整合,也就是说你不用做任何操作,只是正常的读写文件,relert.Static.GB2312
库就会自动帮你完成编码与解码。
该模块提供了几种不同格式的注册号生成函数,可以自动生成地图中没有使用过的注册号。
该模块提供了一个函数relert.init()
。一旦你在一个空的relert
对象上执行这个函数,它就会被写入必要的数据,从而使其成为一张合法的空白地图。
当你在node.js
环境中生成了一个新的relert
对象而不打算打开文件的时候,或者是在浏览器环境中打开了一个空白文件(真·空白文件)的时候,可能会需要用它来初始化地图。
relert.init()
只会尝试添加属性,它不会覆盖任何已经设置的属性。
relert.Static.Toolbox
“工具箱”是一个独立的纯静态模块。
该模块一旦加载,就会将一些可能被高频率使用的纯静态函数挂载在relert
全局对象下,或者添加到Object
等原型上。适当使用这些函数可以让代码更加方便可读。
下面分类对这些函数进行介绍:
在relert.js
中的“坐标”类型的数据有pos
和coord
两种格式:
pos
格式:使用一个对象表示坐标,对象的X
和Y
属性代表x
、y
坐标值。任何一个“物体”的子代理都是合法的pos
格式坐标,因为它们都有X
和Y
属性。coord
格式:使用一个4~6位由数码组成的字符串表示坐标,后3位表示x
坐标(缺位用0补齐),后3位之前表示y
坐标。(地图内部结构中多使用这种坐标,而且这种坐标表示便于使用1维结构进行编码)
虽然relert.js
提供的大部分接口都使用了pos
格式的坐标,但总有不得不使用coord
格式的坐标的时候。
relert.static.Toolbox
提供了在两种坐标格式之间转换的函数:
relert.posToCoord(pos: Object): String;
把pos
格式转化为coord
格式坐标。
relert.coordToPos(coord: String): Object;
把coord
格式转化为pos
格式坐标。
另外还可以通过下面的方法判断是否是合法的pos
坐标:
relert.isPos(pos: Object): Boolean;
如果输入合法的pos
坐标,会返回true
,否则返回false
。注意这个函数只判断X
、Y
属性,不会判断这个坐标是否在地图合法区域内。
relert.mapWidth(): Integer;
返回地图的宽度。
relert.mapHeight(): Integer;
返回地图的高度。
relert.posInnerMap(pos: Object): Boolean;
接受一个pos
坐标对象(一个带有X
和Y
属性的对象,代表它的X坐标和Y坐标),返回它的坐标是否在地图内。
可以将relert.js
提供的,数据代理层的“物体”对象直接传入,因为它们都有X
和Y
属性。
也可以输入形如{X: 12, Y: 34}
这样的对象,直接指定坐标。
relert.posInnerCircle(pos: Object, center: Object, r: Number): Boolean;
接受一个pos
坐标对象(一个带有X
和Y
属性的对象,代表它的X坐标和Y坐标),返回它的坐标是否在圆心为pos
坐标center
、半径为r
的圆内。
relert.randomBetween(a: Integer, b: Integer): Integer;
接受2个整数a
和b
,返回介于a
和b
之间的随机整数。
relert.randomFacing(): Integer;
返回随机朝向数值。朝向数值有0, 32, 64, 96, 128, 160, 192, 224
八个,分别对应“右上, 右, 右下, 下, 左下, 左, 左上, 上”。
relert.randomStrength(a: Float, b: Float): Integer;
接受两个0~1之间的实数a
和b
(代表生命值百分比的上下限,但是此函数不会检查输入),返回随机的生命值数字(255
为满生命值的整数)。
relert.randomPosInnerMap(): Object {X: Integer, Y: Integer}
返回地图内的随机坐标。(暂未实装)
返回值为一个pos
坐标对象。
relert.randomPosOnLine(pos1: Object, pos2: Object): Object {X: Integer, Y: Integer}
返回pos1
和pos2
两个坐标之间连线上的随机一格坐标。(暂未实装)
pos1
和pos2
两个对象为pos
坐标。
可以将relert.js
提供的,数据代理层的“物体”对象直接传入,因为它们都有X
和Y
属性,都是合法的pos
坐标。
也可以输入形如{X: 12, Y: 34}
这样的对象,直接指定坐标。
返回值为一个pos
坐标对象。
relert.randomPosInnerCircle(center: Object, r: Number): Object {X: Integer, Y: Integer}
返回以pos
坐标center
为圆心、半径r
的圆范围内随机一格的坐标。(暂未实装)
返回值为一个pos
坐标对象。
返回结果保证一定落在地图内。如果给的条件不足以产生落在地图内的随机坐标,会抛出异常。
relert.randomSelect(list: Array): Any;
接受一个数组list
,返回数组中的随机一项。
为了使用方便,一些常用函数除了挂载在relert
上以外,还直接写进了原型方法。这些原型方法在下面列出:
Object.prototype.assign(obj: Object): Object;
对于对象obj1
,obj1.assign(obj2)
将obj2
的属性合并到obj1
中(obj1
本身会发生改变),并返回改变过的obj1
。
(施工中)
(施工中)
(施工中)
relert.js
最早是一个基于node.js
的脚本库。在移植到浏览器端以后,在合适的兼容处理下,它仍然能够无缝的在node.js
环境中使用。
在浏览器环境中,浏览器已经自动在环境中生成了一个relert
对象。但在node.js
环境中我们无法预先指定运行环境上下文,故需要自己在脚本中引入relert
对象。
首先通过require
语句,从relert.js
中获取用于创建relert
对象工厂函数,然后再调用这个函数:
const relertCreater = require('./script/relert.js'); //应为relert.js存储的位置
const relert = relertCreater();
这样我们就获取了一个和浏览器端几乎完全一致的relert
对象,可以使用它上面的各种方法。
以上两条语句也可以合并为一条(注意require
语句末尾多了一个空括号):
const relert = require('./script/relert.js')();
当然,你也可以给relert
全局对象换个名字,接口也会发生相应的改变。下文如不特殊说明,仍默认全局对象的命名仍然是relert
。
在浏览器环境中,我们执行脚本的直接作用对象是快照;而在node.js
环境下,没有这个东西。
但是鉴于node.js
有对文件系统的完全访问权限,我们写在node.js
中的脚本,可以直接读写本地文件——这不比快照更好用?
首先,这带来的好处就是,relert.js
在node.js
端所有和“文件名”有关的接口,其“文件名”都可以指定为“文件路径”,即直接指向本地的某个文件。
其次,在node.js
端还多了一个可以直接使用的文件读取接口:
relert.load(filename: String);
指定一个文件,将其读取并加载入relert
实例。
有读就有写,我们原有的relert.save
接口仍然能正常的发挥作用。不带任何参数就是存到原有打开的文件,但甚至能存到任意位置的本地文件:
relert.save([filename: String, [content: Buffer]]);
在node.js
环境中,通常我们需要自己手动完成读写文件的操作——脚本开始初始化relert
示例以后手动读取文件,脚本执行结束后手动保存文件。
但是有一个例外,就是命令行执行的时候带上需要读取的文件名:
node [scriptFileName] [mapFileName]
这样scriptFileName
的脚本执行的时候,其中的relert
实例都会默认加载mapFileName
表示的文件。你也可以通过relert.args
来读取命令行参数。
本章讨论如何使用relert.js
写出在浏览器和node.js
中都可以正常发挥作用的地图脚本。
由于在relert
实例初始化之前,没有relert.Environment
模块可用,我们不能在最开头直接使用relert.isNode
来判断脚本是在哪种环境中执行的。但由于浏览器环境中会预先实例化一个relert
对象,我们可以通过判断relert
对象的存在性来辨别运行环境:
// 程序开头判断入口
if (typeof relert == 'undefined') {
relert = require('./script/relert.js')(); //在node.js环境下,需要自己实例化relert对象
}
避免使用relert.load
接口,而是使用命令行参数来读取文件。
在node.js
环境下,需要自己手动保存文件:
// 程序结尾保存文件
if (relert.isNode) { //此时已经有relert实例,可以使用isNode来判断了
relert.save();
}
在正确的实例化relert
对象以后,使用relert.isNode
和relert.isBrowser
主动判断脚本的运行环境,来做出差异化的处理。
使用relert.log
而非console.log
确保对两个运行环境的兼容。
在node.js
环境中还有一个好处,就是你可以通过工厂函数,创建多个relert
实例。这在浏览器环境中是做不到的。
const relert1 = require('./script/relert.js')();
const relert2 = require('./script/relert.js')();
此时relert1
和relert2
就是两个不同的relert
实例。可以读取不同的文件,也可以在二者之间传递数据。你甚至可以做出“把一张地图拆成两张”或者“把两张地图合并为一张”等操作,具体如何使用就要发挥想象力了。
本章讨论relert.js-browser
在浏览器端的定制开发问题——比如说,我想要丢弃原有的index.html
,重新开发一个网页客户端,需要做什么呢?
(施工中)
relert.js-browser
版本0.1
,2021年9月
开发及贡献者:
- heli-lab
relert.js
遵循ES6
标准,其代码风格也以ES6
推荐的代码风格为基础。- 大括号不换行,且空1个空格。
- 标识符命名基本使用大驼峰(PascalCase)和小驼峰(camelCase)两种命名规则:
Proxy
对象,即各级数据代理,使用大驼峰规则命名。例:relert.BaseNode
。INI
属性遵循原版rules
的拼写风格,也使用大驼峰规则命名。例:structure.UpgradesCount
。- 内部常量使用全大写字母+下划线分隔命名。例:
ENCODING
。 - 对于内部标识符,在其标识符前加双下划线“
__
”。例:__RelertObject
。 - 其余标识符使用小驼峰规则命名。
// relert.js范例A-1:平民单位INI导入
// * 用途:将所有平民步兵全都设置为不随机走动、受主动攻击、血量50点
// * 运行环境:浏览器
// 需要修改属性的平民单位列表
let civList = ['CIV1', 'CIV2', 'CIV3', 'CIVA', 'CIVB', 'CIVC'];
for (let i in civList) {
// 判断对应的平民单位字段是否存在
if (!relert.INI[civList[i]]) {
// 如果不存在就新建一个空的
relert.INI[civList[i]] = {};
}
relert.INI[civList[i]].assign({
Insignificant: 'yes',
Strength: 50,
});
}
// relert.js范例B-1:平民建筑生命值调整
// * 用途:将地图上特定作战方(平民方)的特定建筑物,生命值在一个范围内随机调整
// * 运行环境:浏览器
let ignoreList = ['CAARMY01', 'CAARMY02', 'CAARMY03', 'CAARMY04', 'CATS01', 'CAEURO05', 'CAWASH18',
'CASANF15', 'CAFRMB', 'CAWT01', 'CAMSC01', 'CAMSC02', 'CAMSC03', 'CAMSC04', 'CAMSC05', 'CAMSC11',
'CAPARK01', 'CAPARK02', 'CAPARK03', 'CAPARK04', 'CAPARK05', 'CAPARK06', 'CAMISC04', 'CAPARS07',
'CAMSC06', 'CAURB01', 'CAURB02', 'CABARR01', 'CABARR02', 'CASIN03E', 'CASIN03S', 'CAMISC03',
'CAMISC05', 'CAMISC11', 'CASTRT05', 'INBLULMP', 'INREDLMP', 'INGALITE', 'INGRNLMP', 'INYELWLAMP',
'INORANLAMP', 'INPURPLAMP', 'CAOILD']; //定义一个ignoreList“忽略列表”,表示不想被此脚本处理的建筑物类型列表
relert.Structure.forEach((item) => { //对于从Structure中取出每一个item
if ((item.House == 'Neutral House') && (!ignoreList.includes(item.Type))) { //如果其所属为Neutral House,且类型不在ignoreList内
item.assign({
Strength: relert.randomStrength(0.15, 0.25), //设置其生命值在15%~25%之间
AIRepair: 0, //设置其AI修复属性为0
});
}
});
// relert.js范例B-2:均匀分布树木类型
// * 用途:地图上已有的树木位置不变,但类型重新安排,使其尽可能保证平均分布,重复的树木不挨的太近
// * 运行环境:浏览器
//计算量可能很大,多给点时间吧
relert.tickTimeOut(10000);
//希望出现的树木类型
let treeType = ['TREE05', 'TREE06', 'TREE07', 'TREE08', 'TREE10', 'TREE11', 'TREE12', 'TREE14', 'TREE15'];
//判断Terrain是否应该被替换的条件
let isTree = (item) => {
return (item.Type.substring(0, 4)) == 'TREE';
}
//树木分布权重图
//每放置一棵树,在其周围都会产生一圈递减的权重
let heatMap = {};
//在权重图上放置树木
let heatPlace = (item) => {
let r = 11; //每棵树木的影响半径/格
for (let i = -r; i <= r; i++) {
for (let j = -r; j <= r; j++) {
let distance = Math.hypot(i, j);
let coord = relert.posToCoord({
X: parseInt(item.X) + i,
Y: parseInt(item.Y) + j,
});
if ((distance > r) || (distance < Number.EPSILON)) {
continue;
}
if (!heatMap[coord]) {
heatMap[coord] = {};
}
if (!heatMap[coord][item.Type]) {
heatMap[coord][item.Type] = 0;
}
heatMap[coord][item.Type] += Math.exp(-distance);
}
}
}
//获取某个点位权重最低的树木
let getWeakHeat = (pos) => {
let coord = relert.posToCoord(pos);
if (!heatMap[coord]) {
return relert.randomSelect(treeType);
}
let minHeat = 9999;
let minHeatType = '';
for (let i in treeType) {
if (!heatMap[coord][treeType[i]]) {
return treeType[i];
} else {
if (heatMap[coord][treeType[i]] < minHeat) {
minHeatType = treeType[i];
minHeat = heatMap[coord][treeType[i]];
}
}
}
return minHeatType;
}
//遍历树木并重新排布
relert.Terrain.forEach((item) => {
if (isTree(item)) {
item.Type = getWeakHeat(item);
heatPlace(item);
}
});
// relert.js范例C-1:随机生成污染
// * 用途:在地图上随机生成污染
// * 运行环境:浏览器
let density = 0.01; //随机生成污染的密度(个/格)
let smudgeList = []; //污染种类
//此示例未完成
// relert.js范例D-1:删除地图外物体
// * 用途:批量删除地图边界区域以外的物体
// * 运行环境:浏览器
relert.Structure.delete((item) => {
return !relert.posInnerMap(item);
});
relert.Infantry.delete((item) => {
return !relert.posInnerMap(item);
});
relert.Unit.delete((item) => {
return !relert.posInnerMap(item);
});
relert.Aircraft.delete((item) => {
return !relert.posInnerMap(item);
});
relert.Terrain.delete((item) => {
return !relert.posInnerMap(item);
});
relert.Smudge.delete((item) => {
return !relert.posInnerMap(item);
});
(施工中)
(施工中)