本篇教程由作者设定使用 CC BY-NC-SA 协议。

接续前篇链接内容,本篇将使用反射(除了loadClass以外的,主要用于读写私有方法)等更激进的策略来深♂度定制咒法学的游戏内容

与前篇相比未作调整的内容(如基类定义、法术注册等)将省略

里技:反射

一类通过字符串索引对应类/方法/变量的操作的通称,可以通过对照开源代码或jd-gui发现并访问一些作者并未暴露接口的功能

当然,能力越大责任越大,更侵入性的修改也伴随着更花式的报错与跳出风险

我在自己的仓库里简单地封装了两个方法,getField和setField,分别用于对任意对象的任意字段进行读写,这是等一下会用到的神奇妙妙工具.jpg

里技试验:解除递归深度限制

注意到CastingContext类中存在一个私有变量depth,其初始值为0,唯一可外部修改的地方位于函数incDepth,后者在每次赫尔墨斯之策略托特之策略等运行子串动作时调用,到达上限后即抛出爆栈事故结束运行

那么,如果我们创建一个将该字段拉出来并还原的法术,就可以解除递归深度限制了(智将)

// operate
global.setField(ctx, 'depth', -114514)

掏出我们的简易爆栈程序(下图中带红标的是对应符文),进游戏,运行——

使用KubeJS添加自定义法术(里篇)-第1张图片

第一个坑来了: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"刷屏了

使用KubeJS添加自定义法术(里篇)-第2张图片


那么既然已经实现了爆栈法术,让我们备份一下存档,逝逝这个法术罢

使用KubeJS添加自定义法术(里篇)-第3张图片

(解锁成就:程序未响应)(解锁成就:内存泄漏)


应用:彻底热重载

注意到,现在注册自定义法术的基础架构本身也有不少代码,例如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 // 沼气驱动,哼哼 啊啊啊啊啊啊——
        }

至此我们又获得了沼气驱动范围内的咒灵施法&存活自由


至于法术环之类的施法媒介……理论上也可以通过类似的方法强行填充媒质

此处留作习题(并不是我还没有玩到这里)