饥荒联机 mod的加载
折翼神
2024年04月20日 17:16

一、require

1、加载逻辑

首先,先解释lua的加载函数 require。通常 require是遵循package.searchers(加载模块的表)的规则执行加载文件。package.searchers这张表内每项都是查找器函数。require是依次调用这些查找器,来尝试找到并加载文件。

2、package.searchers的四个查找器

  • 用于已经加载过的文件, 它使用储存在package.preload已有的值。

  • 用于查找 Lua 库的加载库。 它使用储存在package.path中的路径来做查找工作。

  • 用于查找 C 库的加载库,它使用储存在 package.cpath中的路径来做查找工作。

  • 从 C 路径中查找指定模块的根名字。 做mod,只需要关注前俩个就好了,其他的也管不着。

3、package.path表

package.path是key-value类型的表。key是存储着,使用require("xxxx")里的第一个参数。value存储情况分两种,。其一,如果加载成功没能返回一个非空值,value设置为ture;其二,如果加载成功返回一个非空值,那么将值赋予value。通常情况就是,lua文件末尾return的值,饥荒源码中是返回表。 require时,查找 package.loaded 表是否存在非nil值,存在说明文件已经被加载过了,那么将直接返回package.loaded 表中存储的值。否则会,进行下一个查找器。

4、package.cpath路径

package.cpath是存储路径的字符串。其内存储着多条路径,是通过";"分割开了。从左往右依次设置为默认路径来查找,将第一次查找并执行的结果返回。如果都尝试没能找到,将执行下一个查找器。

代码块
text/html
自动换行
复制代码
package.path = MODS_ROOT..mod.modname.."\\scripts\\?.lua;"..package.path
复制成功

每次进入存档世界加载饥荒mod时,将更改package.cpath值。MODS_ROOT是“../mods/”,modname是mod名称。 举例,"../mods/11cs\scripts\?.lua;scripts\?.lua;scriptlibs\?.lua"就是只加载了一个mod的情况下。 所以有的mod使用覆盖法,就是将scripts文件夹下同名路径的文件复制到mod中来修改。加载这个被覆盖的文件时,优先找到了mod文件夹里的文件,而非饥荒源码。

二、饥荒mod的加载

其源码内容在mods.lua文件里。核心是ModWrangler:LoadMods 函数。

代码块
text/html
自动换行
复制代码
function ModWrangler:LoadMods(worldgen)
    if not MODS_ENABLED then
        return
    end

    self.worldgen = worldgen or false
    -- 第二部分: 专服下载mod
    if not worldgen and TheNet:IsDedicated() then
        self:LoadServerModsFile()
    end
    -- 第三部分:mod设置
    local mod_overrides = {}
    if not worldgen then

        if IsInFrontEnd() then
            KnownModIndex:ClearAllTempModFlags()
            self:DisableAllServerMods()
        end
        KnownModIndex:UpdateModInfo()
        mod_overrides = KnownModIndex:LoadModOverides()
        KnownModIndex:ApplyEnabledOverrides(mod_overrides)
    end
    -- 第四部分:mod设置
    local moddirs = KnownModIndex:GetModsToLoad(self.worldgen)

    for i,modname in ipairs(moddirs) do
        if self.worldgen == false or (self.worldgen == true and KnownModIndex:IsModCompatibleWithMode(modname)) then
            table.insert(self.modnames, modname)

            if self.worldgen == false then
                if TheNet:GetIsServer() and not TheNet:IsDedicated() then
                    local options = KnownModIndex:LoadModConfigurationOptions(modname, false)

                    KnownModIndex:SetTempModConfigData({[modname] = options})

                    KnownModIndex:LoadModConfigurationOptions(modname, true)
                else
                    KnownModIndex:LoadModConfigurationOptions(modname, not TheNet:GetIsServer())
                end
            end

            local initenv = KnownModIndex:GetModInfo(modname)
            local env = CreateEnvironment(modname,  self.worldgen)
            env.modinfo = initenv

            table.insert( self.mods, env )
            local loadmsg = "Loading mod: "..ModInfoname(modname).." Version:"..env.modinfo.version
            if initenv.modinfo_message and initenv.modinfo_message ~= "" then
                loadmsg = loadmsg .. " ("..initenv.modinfo_message..")"
            end
            print(loadmsg)
        end
    end
    KnownModIndex:ApplyConfigOptionOverrides(mod_overrides)
    local function modPrioritySort(a,b)
        local apriority = (a.modinfo and a.modinfo.priority) or 0
        local bpriority = (b.modinfo and b.modinfo.priority) or 0
        if apriority == bpriority then
            return tostring(a.modinfo and a.modinfo.name) > tostring(b.modinfo and b.modinfo.name)
        else
            return apriority  > bpriority
        end
    end
    table.sort(self.mods, modPrioritySort)

    for i,mod in ipairs(self.mods) do
        table.insert(self.enabledmods, mod.modname)
        package.path = MODS_ROOT..mod.modname.."\\scripts\\?.lua;"..package.path
        local manifest
        if((mod.modinfo.forcemanifest == nil and IsWorkshopMod(mod.modname)) or
            (mod.modinfo.forcemanifest ~= nil and mod.modinfo.forcemanifest)) then
            ManifestManager:LoadModManifest(mod.modname, mod.modinfo.version)
            manifest = mod.modname
        end
        table.insert(package.assetpath, {path = MODS_ROOT..mod.modname.."\\", manifest = manifest})

        self.currentlyloadingmod = mod.modname
        self:InitializeModMain(mod.modname, mod, "modworldgenmain.lua")
        if not self.worldgen then
            self:InitializeModMain(mod.modname, mod, "modmain.lua")
        end
        self.currentlyloadingmod = nil
    end
end
复制成功

第一部分:预处理

形参worldgen是代表是否在创建世界生成地图时执行。通常传入true。然后不启用mod就退出加载函数。

第二部分:专服更新下载mod

代码块
text/html
自动换行
复制代码
    if not worldgen and TheNet:IsDedicated() then
        self:LoadServerModsFile()
    end
复制成功

非生成地图时,且是专服情况下,执行LoadServerModsFile函数。

代码块
text/html
自动换行
复制代码
function ModWrangler:LoadServerModsFile()
    local function ServerModSetup(product_id)
        TheNet:ServerModSetup(product_id)
    end
    local function ServerModCollectionSetup(collection_id)
        TheNet:ServerModCollectionSetup(collection_id)
    end

    local env = {
        ServerModSetup = ServerModSetup,
        ServerModCollectionSetup = ServerModCollectionSetup,
    }

    KnownModIndex:UpdateModInfo()
    TheNet:BeginServerModSetup()

    local filename = MODS_ROOT
    if DIST_PLATFORM == "Rail" then
        filename = filename.."dedicated_server_mods_setup_rail.lua"
    else
        filename = filename.."dedicated_server_mods_setup.lua"
    end
    local fn = kleiloadlua( filename )
    if fn ~= nil then
        local mods_err_fn = function(err)
            print("########################################################")
            print("#ERROR: Failure to load dedicated_server_mods_setup.lua:", err)
            print("#Shutting down")
            print("########################################################")
            Shutdown()
        end

        if type(fn)=="string" then
            mods_err_fn(fn)
        else
            setfenv(fn, env)
            xpcall( fn, mods_err_fn )
        end
    end

    TheNet:DownloadServerMods()
end
复制成功

1、env环境

env 为下面运行加载的文件添加运行环境,这个环境表就俩参数。对于开专服人来说,dedicated_server_mods_setup.lua文件是很熟悉了,每当需要自动更新的mod,就在这个文件里添加一行 'ServerModSetup("xxx")'代码 ,其中xxx代表的是对应mod编号。

2、kleiloadlua

这个是克雷未公开出来的函数,用来加载lua文件的。

3、setfenv和xpcall

setfenv是lua5.1的函数,用来设置环境的,参数1可以是函数也可以是数字,参数2是表,为环境。那么在这个环境中使用一个未定义的函数ServerModSetup,就会从环境中找,找到ServerModSetup函数执行。 xpcall是当执行参数1函数报错时,可以调用这个参数2处理函数来处理。

4、总结

这个部分函数的,意义是为了专服可以自动更新mod。原理是,调用执行存储要更新mod或mod合集的lua文件。具体细节无从得知,我们只知道输入和输出,中间是个黑箱。TheNet调用的函数具体功能如何实现的一概不知。

第三部分:存档mod设置

代码块
text/html
自动换行
复制代码
    local mod_overrides = {}
    if not worldgen then
        if IsInFrontEnd() then 
            KnownModIndex:ClearAllTempModFlags() 
            self:DisableAllServerMods()
        end
        KnownModIndex:UpdateModInfo()
        mod_overrides = KnownModIndex:LoadModOverides()
        KnownModIndex:ApplyEnabledOverrides(mod_overrides)
    end
复制成功

在是前端情况下,会执行清理临时mod标记,且禁止全部服务器mod。 其次,更新全部mod信息,客户端mod和服务器mod。 最后是,获取全部mod设置,覆盖掉启用的mod设置。

1、更新mod信息

其核心代码是 InitializeModInfo函数

代码块
text/html
自动换行
复制代码
    local env = {
        folder_name = modname,
        locale = LOC.GetLocaleCode(),
        ChooseTranslationTable = function(tbl)
            local locale = LOC.GetLocaleCode()
            return tbl[locale] or tbl[1]
        end,
    }
    local fn = kleiloadlua(MODS_ROOT..modname.."/modinfo.lua")
复制成功

加载mod文件夹里的modinfo.lua文件,其执行环设置为env表内容。 最后会返回这个环境env表,作为信息。

所以,我们可以看到一个正常的modinfo.lua文件里包含下面代码。

代码块
text/html
自动换行
复制代码
name = "mod名称"
description = [[mod描述]]
author = "mod作者"
version = "0.0.0" --mod版本
api_version = 10 --api版本
dst_compatible = true --兼容
all_clients_require_mod = false --是否全部玩家都要加载
client_only_mod = true --客户端mod
icon_atlas = "xxx.xml" --mod显示图片资源
icon = "xxx.tex" --mod显示图片资源
forumthread = ""
configuration_options ={} --mod设置
复制成功

这里的代码都是没有加local,说明是"全局的",声明在了环境中,env表内。

2、获取全部mod设置,覆盖掉启用的mod设置

代码块
text/html
自动换行
复制代码
function ModIndex:LoadModOverides(shardGameIndex)
    if TheNet:GetIsClient() then
        return {}
    end

    local overrides = {}
    local filename = "../modoverrides.lua"

    local function onload(load_success, str)
        if load_success == true then
            local fn, message = loadstring(str)
            if fn ~= nil then
                local env = {}
                local success, r = RunInEnvironment(fn, env)
                if success and type(r) == "table" then
                    print("SUCCESS: Loaded modoverrides.lua")
                    overrides = r
                else
                    print("ERROR: Failed to run code from modoverrides.lua")
                end
            else
                print("ERROR: Failed to load modoverrides.lua")
            end
        end
    end
    shardGameIndex = shardGameIndex or ShardGameIndex
    if not shardGameIndex then
        require("shardindex")
        shardGameIndex = ShardIndex()
        shardGameIndex:Load()
    end
    if not TheNet:IsDedicated() and shardGameIndex:IsValid() and not shardGameIndex:GetServerData().use_legacy_session_path then
        TheSim:GetPersistentStringInClusterSlot(shardGameIndex:GetSlot(), "Master", filename, onload)
    else
        TheSim:GetPersistentString(filename, onload)
    end

    return overrides
end
复制成功

是客户端时,返回的是空表{}。 否则会从存档文件夹里,读取modoverrides.lua文件内容。将文件结果赋值给overrides,最后返回结果。如果是客户端的情况下,是读取Master文件夹里的modoverrides.lua文件。 最后,遍历这个overrides表,并覆盖掉对应mod设置。故modoverrides.lua内容是:

代码块
text/html
自动换行
复制代码
return { ["1_cs"]={ configuration_options={  }, enabled=true } }
复制成功

返回的是张表。

3、总结

可以看到,mod加载并非是仅有lua的require函数,还有饥荒提供的kleiloadlua函数,存档文件夹里文件的TheSim:GetPersistentStringInClusterSlot函数,加载client_save文件夹下内容的TheSim:GetPersistentString函数。 这里覆盖mod配置更多的是在进入客户端存档界面上,显示mod列表时的内容。

第四部分:mod设置

获取到全部启用的mod目录表,并且遍历它。将全部启用的mod信息和配置设置好。

代码块
text/html
自动换行
复制代码
local moddirs = KnownModIndex:GetModsToLoad(self.worldgen)
复制成功

1、遍历条件

不是生成地图情况下;或者是生成地图,且该mod信息里的dst_compatible=true

代码块
text/html
自动换行
复制代码
self.worldgen == false or (self.worldgen == true and KnownModIndex:IsModCompatibleWithMode(modname))
复制成功

2、设置mod配置

非生成世界时,将client_save\mod_config_data\modconfiguration_xxx文件加载到xxxmod里。modconfiguration是上次mod使用的配置。

代码块
text/html
自动换行
复制代码
if self.worldgen == false then
    if TheNet:GetIsServer() and not TheNet:IsDedicated() then
        local options = KnownModIndex:LoadModConfigurationOptions(modname, false)

        KnownModIndex:SetTempModConfigData({[modname] = options})

        KnownModIndex:LoadModConfigurationOptions(modname, true)
    else
        KnownModIndex:LoadModConfigurationOptions(modname, not TheNet:GetIsServer())
    end
end
复制成功

3、配置mod环境

创建了mod的env环境后,添加到了self.mods表中。从日志中找到打印的内容。

代码块
text/html
自动换行
复制代码
local initenv = KnownModIndex:GetModInfo(modname)
local env = CreateEnvironment(modname,  self.worldgen)
env.modinfo = initenv

table.insert( self.mods, env )
local loadmsg = "Loading mod: "..ModInfoname(modname).." Version:"..env.modinfo.version
if initenv.modinfo_message and initenv.modinfo_message ~= "" then
    loadmsg = loadmsg .. " ("..initenv.modinfo_message..")"
end
print(loadmsg)
复制成功

代码块
text/html
自动换行
复制代码
[00:00:51]:applying configuration_options from modoverrides.lua to mod workshop-2287303119	
复制成功

4、创建mod环境(CreateEnvironment)(核心)

代码块
text/html
自动换行
复制代码
function CreateEnvironment(modname, isworldgen, isfrontend)

    local modutil = require("modutil")
    require("map/lockandkey")

    local env =
    {
        -- lua
        pairs = pairs,
        ipairs = ipairs,
        print = print,
        math = math,
        table = table,
        type = type,
        string = string,
        tostring = tostring,
        require = require,
        Class = Class,

        -- runtime
        TUNING=TUNING,

        -- worldgen
        LEVELCATEGORY = LEVELCATEGORY,
        GROUND = GROUND,
        WORLD_TILES = WORLD_TILES,
        LOCKS = LOCKS,
        KEYS = KEYS,
        LEVELTYPE = LEVELTYPE,

        -- utility
        GLOBAL = _G,
        modname = modname,
        MODROOT = MODS_ROOT..modname.."/",
    }

    if isworldgen == false then
        env.CHARACTERLIST = GetActiveCharacterList()
    end

    env.env = env

    --install our crazy loader!
    env.modimport = function(modulename)
        print("modimport: "..env.MODROOT..modulename)
        if string.sub(modulename, #modulename-3,#modulename) ~= ".lua" then
            modulename = modulename..".lua"
        end
        local result = kleiloadlua(env.MODROOT..modulename)
        if result == nil then
            error("Error in modimport: "..modulename.." not found!")
        elseif type(result) == "string" then
            error("Error in modimport: "..ModInfoname(modname).." importing "..modulename.."!\n"..result)
        else
            setfenv(result, env.env)
            result()
        end
    end

    modutil.InsertPostInitFunctions(env, isworldgen, isfrontend)

    return env
end
复制成功

env表,添加了一些默认参数,一部分是lua本身的函数,一部分是饥荒提供的全局参数, "GLOBAL = _G"设置全局表了。提供了一个封装好的加载函数modimport。 InsertPostInitFunctions函数为env环境表添加了大量的饥荒提供的API,具体的nr可以去看modutil.lua文件。

第五部分:设置mod加载顺序

直接对self.mods表的modinfo信息表中priority参数进行比较,大的在前,小的在后,相同值,比较name参数。

代码块
text/html
自动换行
复制代码
local function modPrioritySort(a,b)
    local apriority = (a.modinfo and a.modinfo.priority) or 0
    local bpriority = (b.modinfo and b.modinfo.priority) or 0
    if apriority == bpriority then
        return tostring(a.modinfo and a.modinfo.name) > tostring(b.modinfo and b.modinfo.name)
    else
        return apriority  > bpriority
    end
end
table.sort(self.mods, modPrioritySort)
复制成功

第六部分:遍历加载启用的mod

代码块
text/html
自动换行
复制代码
for i,mod in ipairs(self.mods) do
    table.insert(self.enabledmods, mod.modname)
    package.path = MODS_ROOT..mod.modname.."\\scripts\\?.lua;"..package.path
    local manifest

    if((mod.modinfo.forcemanifest == nil and IsWorkshopMod(mod.modname)) or
        (mod.modinfo.forcemanifest ~= nil and mod.modinfo.forcemanifest)) then
        ManifestManager:LoadModManifest(mod.modname, mod.modinfo.version)
        manifest = mod.modname
    end
    table.insert(package.assetpath, {path = MODS_ROOT..mod.modname.."\\", manifest = manifest})

    self.currentlyloadingmod = mod.modname
    self:InitializeModMain(mod.modname, mod, "modworldgenmain.lua")
    if not self.worldgen then

        self:InitializeModMain(mod.modname, mod, "modmain.lua")
    end
    self.currentlyloadingmod = nil
end
复制成功

1、更改package.path表

依次将mod路径添加到package.path中。

代码块
text/html
自动换行
复制代码
package.path = MODS_ROOT..mod.modname.."\\scripts\\?.lua;"..package.path
复制成功

2、添加资产路径 package.assetpath

package.assetpath是饥荒定义的资源路径,比如说图片资源(images)、动画资源(anim)等。

3、初始化main文件(InitializeModMain)

代码块
text/html
自动换行
复制代码
self:InitializeModMain(mod.modname, mod, "modworldgenmain.lua")
if not self.worldgen then
    self:InitializeModMain(mod.modname, mod, "modmain.lua")
end
复制成功

mod这里是指mod环境,InitializeModMain函数核心的加载是通过kleiloadlua执行的。在生成地图时,不会加载modmain.lua文件

第七部分:总结

  • 加载mod的目标是,全部启用的mod。

  • mod配置文件,是存储于两个文件里,一是存档文件里的modoverrides.lua,二是配置client_save\mod_config_data\modconfiguration_xxx 这里包含了上次使用参数、默认参数等全部的信息。

  • mod详细信息是在modinfo.lua里,是这个文件的环境。

  • mod加载的环境里包含许多信息。

  • mod加载核心就加载三文件,modinfo.lua、modworldgenmain.lua、modmain.lua。在生成地图时,不会加载modmain.lua文件。

  • mod覆盖法是通过package.path的优先级实现的。

  • mod加载使用了多种文件加载方法。大部分都是黑箱。

三、mod加载的时机

即LoadMods函数执行的位置。

第一种 scripts\main.lua

代码块
text/html
自动换行
复制代码
local function ModSafeStartup()
    TheSim:ClearFileSystemAliases()
    ModManager:LoadMods()
    ...
end
复制成功

在client_save\modindex和client_save\boot_modindex文件被加载后执行。

第二种 scripts\worldgen_main.lua

代码块
text/html
自动换行
复制代码
local moddata = json.decode(GEN_MODDATA)
if moddata then
    KnownModIndex:RestoreCachedSaveData(moddata.index)
    ModManager:LoadMods(true)
end
复制成功

这里是生成地图的时候了。

另一个方法 前端加载mod(FrontendLoadMod)

是在客户端前端的mod列表中,确认启用了mod时执行。

代码块
text/html
自动换行
复制代码
function ModWrangler:FrontendLoadMod(modname)
    if not KnownModIndex:DoesModExistAnyVersion(modname) then
        print(string.format("Tried frontend-loading mod '%s' but it doesn't exist.", modname))
        return
    end
    print("FrontendLoadMod", modname)

    KnownModIndex:LoadModConfigurationOptions(modname, false)

    local initenv = KnownModIndex:GetModInfo(modname)
    local env = CreateEnvironment(modname,  self.worldgen)
    local frontend_env = CreateEnvironment(modname,  self.worldgen, true)

    env.modinfo = deepcopy(initenv)
    frontend_env.modinfo = initenv

    local loadmsg = "Fontend-Loading mod: "..ModInfoname(modname).." Version:"..tostring(initenv.version)
    if initenv.modinfo_message and initenv.modinfo_message ~= "" then
        loadmsg = loadmsg .. " ("..initenv.modinfo_message..")"
    end
    print(loadmsg)

    local oldpath = package.path
    package.path = MODS_ROOT..modname.."\\scripts\\?.lua;"..package.path

    table.insert(package.assetpath, {path = MODS_ROOT..modname.."\\"})

    self.currentlyloadingmod = modname
    self:InitializeModMain(modname, frontend_env, "modservercreationmain.lua", true)
    self:InitializeModMain(modname, env, "modworldgenmain.lua", true)

    self.currentlyloadingmod = nil
    package.path = oldpath
    table.remove(package.assetpath)
end
复制成功

此时仅会加载执行modservercreationmain.lua和modworldgenmain.lua文件。 此时,modservercreationmain.lua文件的环境会多一个ReloadFrontEndAssets函数。