饥荒mod开发——覆盖大法
没有一个功能丰富的mod能避免覆盖源码方法。
1. 前言
如果只用官方提供的方法和函数要实现一个功能丰富的mod基本是不可能的,覆盖原来的方法是不可避免的,但是覆盖也有讲究,覆盖的好兼容性就好,代码简洁,看的舒适,覆盖的不好,代码凌乱,和其他mod兼容性差,需要经常维护,甚至可能越维护自己都不想看了,本博客介绍饥荒各部分功能覆盖方法。
在讲覆盖的各种案例之前,这里先介绍一个函数,这是记录在饥荒Mod工具函数中的一个函数,也是我覆盖经常用的函数。
简单介绍一下下面的函数,这个参数的功能是包装一个对象的方法,你可以在方法之前执行一些操作,决定是否是否执行原方法,或者修改原方法传入的参数,在原方法执行之后执行一些操作,决定最后的返回值。
---函数装饰器,增强原有函数的时候可以使用
---@param beforeFn function|nil 先于fn执行,参数为fn参数,返回三个值:新返回值表、是否跳过旧函数执行,旧函数执行参数(要求是表,会用unpack解开)
---@param afterFn function|nil 晚于fn执行,第一个参数为前面执行后的返回值表,后续为fn的参数,返回值作为最终返回值(要求是表或nil,会用unpack解开)
---@param isUseBeforeReturn boolean|nil 在没有afterFn却有beforeFn的时候,是否采用beforeFn的返回值作为最终返回值,默认以原函数的返回值作为最终返回值
function FN.FnDecorator(obj, key, beforeFn, afterFn, isUseBeforeReturn)
assert(type(obj) == "table")
assert(beforeFn == nil or type(beforeFn) == "function", "beforeFn must be nil or a function")
assert(afterFn == nil or type(afterFn) == "function", "afterFn must be nil or a function")
local oldVal = obj[key]
obj[key] = function(...)
local retTab, isSkipOld, newParam, r
if beforeFn then
retTab, isSkipOld, newParam = beforeFn(...)
end
if type(oldVal) == "function" and not isSkipOld then
if newParam ~= nil then
r = { oldVal(unpack(newParam)) }
else
r = { oldVal(...) }
end
if not isUseBeforeReturn then
retTab = r
end
end
if afterFn then
retTab = afterFn(retTab, ...)
end
if retTab == nil then
return nil
end
return unpack(retTab)
end
end
代码前缀
我的习惯是新创建的预制体和文件都加上mod的前缀,这里假设我的mod名是MethodDecorator,下面关于预制体或其他变量的命名统一采用前缀"md_"。
2. 覆盖预制体prefab
首先为什么要覆盖原来的预制体?最大的理由是你想在原来的对象上增加新功能做成一个新的对象,或者是你不希望别人的mod影响到你的预制体。比如你的mod是雇佣单位,但是一些mod会修改生物的逻辑,你雇佣的单位可能就会攻击玩家。
这是覆盖猪人示例代码
-- prefabs/md_pigman.lua
local oldFn = Prefabs["pigman"].fn
local function fn(...)
local inst = oldFn(...)
-- 你自己的初始化部分
return inst
end
return Prefab("md_pigman", fn)
3. 覆盖大脑brain
覆盖brain的理由一般是希望在原来的节点中追加自己的节点,修改生物的逻辑。
大脑逻辑节点是在OnStart方法中定义的,如果只是自己写还好,但是考虑到其他mod对brain的修改,在brain众多节点中指定位置插入自己节点就比较困难了,一般分两种情况,一种是覆盖整个文件,另一种是在首尾插入节点。
3.1 覆盖整个brain文件
覆盖整个文件时,你可能会考虑把源码文件拷贝到自己mod相应的目录下就行了,我之前也是这样做的,但是很快就有玩家反馈没效果,仔细研究后发现仅仅是因为别人的mod使用了AddClassPostConstruct,只是用了这个函数程序就不会使用我的文件了,而是重新读取源码文件,最后我的解决办法是同样使用这个方法来覆盖整个文件。
-- pigbrainpost.lua
-- ...源码全拷贝
local function OnStart(self)
-- ...源码全拷贝
end
AddClassPostConstruct("brains/pigbrain", function(self)
self.OnStart = OnStart
end)
3.2 在首尾插入自己的节点
该方法只适用在首尾添加节点。
比如我有一个mod是雇佣大霜鲨,我需要再brain中给大霜鲨添加Follow来让大霜鲨随时跟随玩家。
-- brain.lua
local function GetLeader(inst)
return inst.components.follower.leader
end
AddBrainPostInit("sharkboibrain", function(self)
table.insert(self.bt.root.children, 1, Follow(self.inst, GetLeader, 1, 8, 16, true))
end)
需要注意的是,如果你写雇佣原版生物的mod,最好拷贝一份原本生物预制体,做一个新的预制体,brain和stategraph也一起拷贝。
4. 覆盖stategraph
4.1 将Stategraph独立出来
我覆盖stategraph的唯一理由是不想被生物被其他mod影响,不过不同于覆盖prefab和brain,官方提供的AddXXX()mod方法对stategraph的初始化竟然都在StateGraph的构造器里?对象在创建时就被mod污染了。我根本没有阻止的途径。

覆盖Stategraph只能采用整个文件拷贝的方式了,修改文件名,并且在修改的地方做好标记,方便以后维护(很重要)。
4.2 追加内容
官方已经提供了AddXXX()方法来给玩家追加内容,但是有时候无法满足自己的需要,这时候就需要覆盖。
local Utils = require("md_utils/utils")
AddStategraphPostInit("pig", function(sg)
-- 覆盖locomote的EventHandler
FnDecorator(sg.events["locomote"], "fn", function(inst)
if true then --一些判断
-- 执行自己的操作
return nil, true
end
end)
-- 覆盖state中attack的onenter函数
FnDecorator(sg.states["attack"], "onenter", nil, function(retTab, inst)
if true then --一些判断
-- 执行自己的操作
return nil, true
end
end)
-- 覆盖state中attack的animover的事件回调
FnDecorator(sg.states["attack"].events["animover"], "fn", function(inst)
if true then --一些判断
-- 执行自己的操作
return nil, true
end
end)
-- 覆盖attack的第 13 帧的timeline
local attackTimeline = sg.states["attack"].timeline
FnDecorator(attackTimeline[Utils.GetStateTimelineIndex(attackTimeline, 13 * FRAMES)], "fn", function(inst)
if true then --一些判断
-- 追加内容
end
end)
end)
5. 覆盖Map
有时候你需要修改TheWorld.Map的判定,比如一些儿在虚空生成一片场景的mod都不得不覆盖他们,除此之外不建议覆盖这种比较底层的方法。
比如小房子mod的逻辑就是在虚空生成房间,默认情况下玩家可以在虚空行走,但是不能放置东西,物品掉地上也会直接落水处理,这时候就必须覆盖Map相关的判断方法。
local function CheckPointBefore(self, x, y, z)
--地图的最大值一般为900,玩家普通情况下不可能超过这个值,大于950一律认为在小房间
if z >= 950 then
return { true }, true
end
end
-- 根据components/deployable.lua判断需要覆盖的方法
Utils.FnDecorator(Map, "IsAboveGroundAtPoint", CheckPointBefore)
Utils.FnDecorator(Map, "IsPassableAtPoint", CheckPointBefore)
Utils.FnDecorator(Map, "IsVisualGroundAtPoint", CheckPointBefore)
Utils.FnDecorator(Map, "CanPlantAtPoint", CheckPointBefore) --允许房间里种植,不知道算不算超模
Utils.FnDecorator(Map, "GetTileCenterPoint", GetTileCenterPointBefore)
覆盖了这一堆判断方法,你才能在虚空生成小房子里放置建筑、物品不会落水、生物不会落水、退出重新不会出生在绚丽之门、虚空可种植等等。
6. 覆盖组件方法
只能说这需求再常见不过了。这里只随便写几个
6.1 真实伤害
真实伤害最简单的逻辑就是攻击时将真实伤害赋值到目标身上,目标扣除血量时修改扣除的血量参数。
local function OnAttackOther(inst, data)
if data and data.target then
data.target.md_realDamage = 50 --真实伤害50
end
end
inst:ListenForEvent("onattackother", OnAttackOther)
AddClassPostConstruct("components/health", function(self)
FnDecorator(self, "DoDelta", function(self, amount, ...)
local realDamage = self.inst.md_realDamage
if realDamage then
self.inst.md_realDamage = nil --清除标记
return nil, false, { self, realDamage, ... } --真实伤害作为新的伤害计算
end
end)
end)
修改health的DoDelta方法的参数可能还不算真实伤害,因为health的DoDelta里还有一个伤害吸收倍率externalabsorbmodifiers的计算,一般是不用考虑这个的,但如果不希望其他mod通过这个倍率影响你的真实伤害的话,可以在if中手动调用SetVal、推送healthdelta事件、调用ondelta,并且跳过原方法的执行,不过mod之间的勾心斗角也只是在层层包装的函数中比较谁的函数在最外层谁的在里层。
6.2 指定位置不落鸟
AddComponentPostInit("birdspawner", function(self)
-- 虚空不会生成鸟,z值大于950就不执行生成鸟的函数
Utils.FnDecorator(self, "GetSpawnPoint", function(self, pt)
return nil, pt.z >= 950
end)
end)
6.3 使用耐久4倍消耗
local function UseBefore(self, num)
num = num or 1
if num > 0 then
return nil, false, { self, num * 4 }
end
end
AddComponentPostInit("finiteuses", function(self)
Utils.FnDecorator(self, "Use", UseBefore)
end)
7. UI的覆盖
UI相关的方法都是本地执行,当游戏原本展示的数据不足以满足玩家时,就会有mod通过各种方式让玩家方便地获取到他们想要的数据。
下面的代码就是在物品的名字下面追加内容,当玩家鼠标放到对象身上时展示额外的数据
local Utils = require("md_utils/utils")
AddClassPostConstruct("widgets/hoverer", function(self)
--self.text是和名字一样的蓝色字体,self.secondarytext是白字,但是只有按Alt查看时才显示
Utils.FnDecorator(self.text, "SetString", function(text, str)
local target = TheInput:GetWorldEntityUnderMouse()
if not target then return end
local s = ""
if true then --根据一些条件判断是否追加
s = "\n" .. "玩家当前等级:100"
end
if s ~= "" then
str = str .. s
return nil, false, { text, str }
end
end)
end)
8. action的覆盖
覆盖原版ACTION的fn函数或者str函数
比如在工具函数中有一个函数AllowTraderGiveAll,允许玩家对于trader的对象可以整组投喂,就是覆盖了ACTIONS.GIVE的fn,在执行前自己判断,自己执行。
---允许玩家通过trader给予道具允许每次给予一组,在onaccept中不要忘了可能是一组道具
---@param checkFn function 校验函数,如果为true则表示可以给予一组
function FN.AllowTraderGiveAll(GLOBAL, checkFn)
assert(checkFn)
Utils.FnDecorator(GLOBAL.ACTIONS.GIVE, "fn", function(act)
if act.target ~= nil
and not (act.target:HasTag("playbill_lecturn") and act.invobject.components.playbill)
and not (act.target.components.ghostlyelixirable ~= nil and act.invobject.components.ghostlyelixir ~= nil)
and act.target.components.trader ~= nil then
local able, reason = act.target.components.trader:AbleToAccept(act.invobject, act.doer)
if not able then
return { false, reason }, true
end
act.target.components.trader:AcceptGift(act.doer, act.invobject,
checkFn(act) and GetStackSize(act.invobject) or nil)
return { true }, true
end
end)
end
9. 制作栏相关
动态展示原型机里可制作的物品
AddClassPostConstruct("components/builder_replica", function(self)
Utils.FnDecorator(self, "CanLearn", function(self, recipename)
if self.classified then
local p = self.classified.current_prototyper:value()
if true then --根据原型机和配方名判断是否显示该商品
return { true }, true
end
end
end)
end)
修改制作栏制作的按钮名
```lua
AddClassPostConstruct("widgets/redux/craftingmenu_details", function(self)
Utils.FnDecorator(self, "UpdateBuildButton", nil, function(retTab, self)
local button = self.build_button_root.button
local recipe = self.data.recipe
local blueprintName = TRIBE_BLUEPRINTS[recipe.name]
if blueprintName and not TheInput:ControllerAttached() then
if true then --判断条件
button:SetText("新的按钮名")
end
end
end)
end)
浙公网安备 33010602011771号