本篇教程由作者设定使用 CC BY-NC-SA 协议。
接续前篇链接内容,本篇将使用反射(除了loadClass以外的,主要用于读写私有方法)等更激进的策略来深♂度定制咒法学的游戏内容
与前篇相比未作调整的内容(如基类定义、法术注册等)将省略
里技:反射
一类通过字符串索引对应类/方法/变量的骚操作的通称,可以通过对照开源代码或jd-gui发现并访问一些作者并未暴露接口的功能
当然,能力越大责任越大,更侵入性的修改也伴随着更花式的报错与跳出风险
我在自己的仓库里简单地封装了两个方法,getField和setField,分别用于对任意对象的任意字段进行读写,这是等一下会用到的神奇妙妙工具.jpg
里技试验:解除递归深度限制
注意到CastingContext类中存在一个私有变量depth,其初始值为0,唯一可外部修改的地方位于函数incDepth,后者在每次赫尔墨斯之策略与托特之策略等运行子串动作时调用,到达上限后即抛出爆栈事故结束运行
那么,如果我们创建一个将该字段拉出来并还原的法术,就可以解除递归深度限制了(智将)
// operate
global.setField(ctx, 'depth', -114514)
掏出我们的简易爆栈程序(下图中带红标的是对应符文),进游戏,运行——
第一个坑来了:js里的所有普通数值对象都是double,不管有没有小数、是否被取整过
因此,需要另外引用java.lang.Integer,创建一个类型匹配的数字丢进去(注:传入需使用字符串,直传数字也会被自动加“.0”而报错)
// 可访问的位置
let Integer = Java.loadClass('java.lang.Integer')
// operate(c, stack, r, ctx)
global.setField(ctx, 'depth', Integer('-114514'))
再次运行,可以看到聊天框顺利地被512个"1.00"刷屏了
那么既然已经实现了爆栈法术,让我们备份一下存档,逝逝这个法术罢
(解锁成就:程序未响应)(解锁成就:内存泄漏)
应用:彻底热重载
注意到,现在注册自定义法术的基础架构本身也有不少代码,例如action类的内部逻辑;由于这些逻辑需要通过注册接口进入咒法学的内部系统,难以仅凭global索引的策略来进行完全的热重载
通过阅读PatternRegistry代码可以发现,mapPattern函数除了一开始根据actionLookup私有字典内是否存在对应id判断了重复注册并抛出异常外,其余位置均是覆盖写入;那么只要事先删了这个位置的东西不就可以实现重复注册了(智将)
理论存在,实际开始:将startup_scripts内相应封装注册函数改为如下形式:
global.loadCustomPatterns = () => {
let actionLookup = global.getField(PatternRegistry, 'actionLookup', 1) // 反射拿字典
function registerPatternWrap(seq, dir, id, isGreat) {
isGreat = !!isGreat
if (!id in global.PatternOperateMap) throw new Error('missing operate: ' + id)
let resourceKey = ResourceLocation('kubejs', id)
if (actionLookup.containsKey(resourceKey)) // 忽有狂徒夜磨刀
actionLookup.remove(resourceKey)
PatternRegistry.mapPattern(
HexPattern.fromAngles(seq, dir),
ResourceLocation('kubejs', id),
new MyActionClass(isGreat, id), isGreat
)
}
// 以下为实际注册自定义法术位置
}
StartupEvents.postInit(global.loadCustomPatterns)
再去server_scripts添一个运行时热重载指令
ServerEvents.commandRegistry(e => {
const { commands: cmd, arguments: arg } = e
e.register(
cmd.literal('hexcasting').then(
cmd.literal('reloadCustomPatterns').executes(ctx => {
let server = ctx.source.server
server.runCommand('kjs reload startup_scripts') // 重载代码们
global.loadCustomPatterns() // 覆盖注册
server.runCommand('hexcasting recalcPatterns') // 更新卓越法术缓存
// server.runCommand('reload') 本行非必须
return 114514
}),
),
)
})
\完结撒花!/(指本节)
源码仓库拾遗
本章内容并不特别涉及反射获取私有属性,甚至很多部分属于作者开放的API内容;但是需要进入github仓库去阅读搜索代码以获得这些关键的部分
换句话说,需要惊人的注意力
法杖画布
注意到咒法学源码里有个单例类叫IXplatAbstractions,其中有两组值得关注的函数:
getHarness/setHarness:读写玩家使用法杖施法时的上下文,其中也包括了数据栈和渡鸦等
getPatterns/setPatterns:读写玩家的画布,其中画布列表的单位ResolvedPattern中记录了各图案的位置和内容
又注意到玩家在使用各种预编译/自动化方式施法时,法杖的手动施法空间其实是被闲置的,这可是一整条长度不限、可访问的栈被闲置了;另外,在手动施法时,经常需要另存栈清空画布来写下更多图案这点也很折磨
那么我们整一套把它们全部利用起来的图案吧(
// operate函数们
// 把数据塞进法杖栈
'mind_stack/push': (c, stack, r, ctx) => {
let args = new Args(stack, 1)
let harness = IXplatAbstractions.INSTANCE.getHarness(ctx.caster, ctx.castingHand)
harness.stack.push(args.get(0))
IXplatAbstractions.INSTANCE.setHarness(ctx.caster, harness)
},
// 从法杖栈拉一个数据
'mind_stack/pop': (c, stack, r, ctx) => {
let harness = IXplatAbstractions.INSTANCE.getHarness(ctx.caster, ctx.castingHand)
if (harness.stack.length < 1) throw MishapNotEnoughArgs(1, 0)
stack.push(harness.stack.pop())
IXplatAbstractions.INSTANCE.setHarness(ctx.caster, harness)
},
// 法杖栈版的群体之精思
'mind_stack/size': (c, stack, r, ctx) => {
let harness = IXplatAbstractions.INSTANCE.getHarness(ctx.caster, ctx.castingHand)
stack.push(DoubleIota(harness.stack.length))
},
// 获取画布内所有图案的列表
mind_patterns: (c, stack, r, ctx) => {
let patterns = IXplatAbstractions.INSTANCE.getPatterns(ctx.caster)
stack.push(ListIota(patterns.map(x => PatternIota(x.pattern))))
},
// 保留栈并清空画布
'mind_patterns/clear': (c, s, r, ctx) => {
// 自动重开画布
let itemStack = ctx.caster.getItemInHand(ctx.castingHand)
let item = itemStack?.item
if (item?.class.name === 'at.petrak.hexcasting.common.items.ItemStaff') {
item.use(ctx.world, ctx.caster, ctx.castingHand)
} else item = null
ctx.caster.server.scheduleInTicks(1, () => {
IXplatAbstractions.INSTANCE.setPatterns(ctx.caster, [])
if (item) item.use(ctx.world, ctx.caster, ctx.castingHand)
})
},
不消耗媒质
首先是使用造物等施法道具进行施法的部分:注意到通过CastingContext可以获得施法玩家与其施法主副手,而Player.getItemInHand方法可以获取到相应的道具;又注意到施法道具继承的“含媒质道具(ItemMediaHolder)”类拥有读写媒质量与读取最大媒质的方法,那么……
// operate(c, stack, r, ctx)
let stack = ctx.caster.getItemInHand(ctx.castingHand)
let item = stack.item
if (item.setMedia && item.getMaxMedia) {
item.setMedia(stack, item.getMaxMedia(stack))
}
现在我们拥有了单次施法动作640粉以内的施法自由
其次是通过Hexal的咒灵施法的部分:注意到两种咒灵的基类(BaseCastingWisp)有个叫media的字段,而且没设访问限制,那么……
// operate(c, stack, r, ctx)
let wisp = ctx.wisp
if (wisp) {
wisp.media = 1145140000 // 沼气驱动,哼哼 啊啊啊啊啊啊——
}
至此我们又获得了沼气驱动范围内的咒灵施法&存活自由
至于法术环之类的施法媒介……理论上也可以通过类似的方法强行填充媒质
此处留作习题(并不是我还没有玩到这里)