/ Mods / 171浏览

【Payday2】收获日2 Mod制作教程: CF连杀图标和连杀语音Mod

文章目录

本文将介绍收获日2的功能性脚本mod制作流程,以本人制作的CF连杀图标和语音mod为例。Mod游戏预览点这里。Mod下载地址:https://modworkshop.net/mod/39464

项目仓库:https://github.com/Eysent/pd2-kill-streak

演示视频:https://www.bilibili.com/video/BV1DK411f7W6/

mod前置库安装和前置mod安装

SuperBlt下载地址:https://superblt.znix.xyz/

SuperBLT是Payday 2游戏的一个插件框架,它允许玩家安装和运行各种自定义MOD来增强游戏体验。它提供了更好的稳定性和性能,并且支持更多的功能和MOD。在上述网址的安装步骤里有详细说明:把下载的dll文件放到游戏根目录下,运行游戏会弹出下载对话框。完成后就出现了mods文件夹,里边就是存放各种mod文件的位置。

几乎所有的SuperBLT提供的Lua函数都有注释,在像VS Code这样的编辑器中工作时会显示提示。关于BLT功能的总体概述,请参见vanilla BLT的文档

由SuperBLT添加的功能的文档可以在网站导航菜单中找到。

除了SuperBlt,此mod还依赖于HopLib。这个库包含了很多实用的函数,方便我们mod功能的实现。下载后,解压到\PAYDAY 2\mods文件夹下。

如下所示:

另外,对于需要写脚本的mod来说,我推荐你下载一份源码来进行参考,这将会有利于你的开发工作。仓库地址:https://github.com/mwSora/payday-2-luajit

关于调试和问题定位:如果游戏崩溃了,你可以在C:\Users\你的用户名\AppData\Local\PAYDAY 2下找到crash.txt文件,里边有你崩溃的原因说明。

如果没有崩溃,但是没有你预期的效果,你可以在PAYDAY 2\mods\logs查看日志文件,如果需要还可以mod中使用blt.log来打印信息。

创建mod文件

\PAYDAY 2\mods文件夹下新建一个Kill streak 文件夹,这就是我们mod文件存放的位置。在其中新建一个assets文件夹,里边将存放我们用到的资源,如图片,语音等等。另外新建一个mod.txt,这个文件用于说明我们mod的名称、作者、用到的库文件等。

assets 文件夹

在此文件夹中包含了mod所需的美术资源或者声音资源等。文件夹的结构如下所示,其中assets.xml文件中描述了资源存放的位置。

killicons文件夹中包含了连杀图标文件,格式后缀是.texture。可以用其他软件编辑.dds 文件最后再改成.texture后缀。

这个文件包含了所需的连杀图标,所以mod的实现思路很简单,在对应的击杀数时播放对应的语音和展示对应的图标。Mod所需的资源可以直接从我的项目中复制。(这里可以把图标安排的更紧凑以节省空间,但我不会,所以就只能这样了)

在确定好资源文件后,我们在kill streak 文件夹中新建一个supermod.xml, 用来告诉游戏加载我们的资源。

<?xml version="1.0"?>
<mod>
    <:include src="assets/assets.xml"/>
</mod>

编辑mod.txt如下,里边包含了mod的基本信息:

{
    "name" : "KillStreak",
    "author" : "Eysent",
    "blt_version" : 2.3,
    "hooks" : [
    ]
}

此时打开游戏,就可以看到新建的mod了,只不过还没添加关键代码,是一个空mod。

实现基础图片显示

新建两个文本文档,分别重命名为KillStreakPanel.luaPlayerManager.lua

下面我们先对核心功能代码文件进行说明。在KillStreakPanel.lua中包含对连杀图标的切换控制、语音的播放等内容。

编辑KillStreakPanel.lua文件,添加如下代码

KillStreakPanel = KillStreakPanel or class() 

function KillStreakPanel:init(hud) -- 初始化方法
    self._full_hud = hud
    self.kills = 0         -- 当前的连杀数
    self._headshot = false -- 是否是爆头击杀

    -- 图像绘制的面板定义
    self._kill_panel = self._full_hud:panel({
        name = "kill_panel", 
        alpha = 0,   -- 不透明度为0,就是看不见 不然一开始就会有图标显示
        layer = 100,
    })

    -- 设定哪一个图标,初始化选择第一个图标; _kill_icon是绘制在_kill_panel上的
    self._kill_icon = self._kill_panel:bitmap({
        vertical = "center",    -- 居中
        align = "center",
        name = "kill_icon",
        texture = "guis/textures/killstreak/killicons/streak",  -- 资源位置
        texture_rect = {0,0,450,450},   -- 选择文件中的显示区域方框大小及位置,这里框住了第一个图标
        blend_mode      = "normal",
        alpha = 1,        -- 透明度
        w = 450 ,        -- 显示的长宽高,可以缩放或放大
        h = 450 ,
        layer = 4
    })
    self:Set_kill_icon()  -- 根据连杀数更改设定显示的图标
    self:MakeFine()       -- 调整图标位置

end

下面添加Set_kill_icon()MakeFine() 函数,在文件中添加以下代码:

function KillStreakPanel:Set_kill_icon()
    local index = self.kills        -- 判断用哪个图标的变量
    if self.kills == 1 and self._headshot then -- 第一个击杀并且是爆头
        if math.random() > 0.7 then     -- 没办法根据子弹命中位置选用哪个爆头图标,选用概率判断
            index = 8                   -- 有0.3的概率选用黄金爆头
        else
            index = 9                   -- 0.7的概率选用白金爆头
        end

    elseif self.kills >7 then       -- 连杀图标最多只有这么多
        index = 7
    end

    local x = 0+450*(index-1)       -- 根据index判断方框的位置

    self._kill_icon:set_texture_rect(x, 0, 450, 450) -- 根据方框更改贴图
end

function KillStreakPanel:MakeFine()  -- 在_kill_panel居中显示
    self._kill_panel:set_size(self._kill_icon:w() + 2, self._kill_icon:h() + 2)
    self._kill_icon:set_center(self._kill_panel:w() / 2, self._kill_panel:h() / 2)
    self._kill_panel:set_center(self._full_hud:center_x(), self._full_hud:center_y() )    
end

好了现在我们需要告诉脚本我们什么时候统计并更新击杀数,以及进行初始化工作。编辑PlayerManager.lua文件并添加如下代码:

dofile(ModPath .. "/KillStreakPanel.lua") -- 执行KillStreakPanel.lua

--             Hook文件名          函数名          自定义名称
Hooks:PostHook(PlayerManager, "on_killshot", "KillStreakHook1", function()
    KillStreakPanel.kills = KillStreakPanel.kills + 1
    KillStreakPanel:Set_kill_icon()  -- 更换图标
    KillStreakPanel._kill_panel:set_alpha(1)  -- 不透明度为1,这样才看得见
end)
Hooks:PostHook(PlayerManager, "on_lethal_headshot_dealt", "KillStreakHook2", function()
    KillStreakPanel._headshot = true
end)

-- 在hudmaneger的init_finalize函数执行后调用该代码,完成初始化
Hooks:PostHook(HUDManager,"init_finalize","killstreak_createhud",function(self,...)
    KillStreakPanel = KillStreakPanel:new(managers.gui_data:create_fullscreen_workspace():panel())
end)

另外,我们用到了Hook,那就需要在mod.txt文件中添加我们用到的Hook。

{
    "name" : "KillStreak",
    "author" : "Eysent",
    "blt_version" : 2.3,
    "hooks" : [
        {"hook_id" : "lib/managers/menumanager", "script_path" : "PlayerManager.lua"},
        {"hook_id" : "lib/managers/playermanager", "script_path" : "PlayerManager.lua"}
        {"hook_id" : "lib/managers/hudmanager", "script_path" : "PlayerManager.lua"}
    ]
}

Hook你可以理解为在系统对应的函数调用后调用你自己的代码,也就是决定你的代码什么时候工作。具体的函数列表可以参考第一节末尾提到的源码。比如打开我们下载好的源码,在\payday-2-luajit\pd2-lua\lib\managers目录下可以看到很多文件,其中就包含了我们在中调用的Hooklib/managers/hudmanager等。打开可以查看其包含哪些代码:

第189行就是我们在PlayerManager.lua中初始化KillStreakPanel的函数。

好了,言归正传,现在我们打开游戏就可以看到击杀图标和它的切换了。


好,现在我们以看到图标会随着击杀数的改变而改变了。但是新的问题来了,我们没有设置连杀数的清零机制,图标也不会消失,图标的大小和位置都需要调整。这些问题我们将在后文中解决。

增加图片的消失动画

在前文我们可以看到mod已经实现了,图片的展示功能以及根据击杀数切换图片显示的功能。但是仍然缺失图片的消失动画和击杀数一段时间后清零的机制。本小节将解决这两个问题。图片的位置调整在后文将通过菜单设置选项来完善。

首先来着手处理图片的渐变消失效果,在KillStreakPanel.lua中添加一个函数,它的功能就是添加渐变效果:

function KillStreakPanel:Show_fading(rect)
    -- fadetime 是开始渐变消失之前的持续时间
    local fadetime = 3
    local wait_t = fadetime

    local t = 0.5 -- 从完全不透明到完全消失的时间

    -- 展示图层
    self._kill_panel:set_alpha(1)
    -- 等待anim_t秒
    while wait_t > 0 do
        wait_t = wait_t - coroutine.yield()
    end

    -- fade animation
    while t > 0 do
        t = t - coroutine.yield()
        local n = math.sin((t / 2) * 350)       
        self._kill_panel:set_alpha(math.lerp(0, 1, n))
    end

    self.kills = 0
    self._kill_panel:set_alpha(0)
    self._kill_panel:set_x(self._full_hud:center_x())
end

在此函数中,击杀图标出现的fadetime秒内不会有渐变消失的效果,在此期间可以正常切换击杀图标。之后的0.5s内击杀图标才会慢慢消失,并且会把击杀数清零。这样击杀图标消失后可以再次触发从0杀开始的图片切换循环。现在我们有了渐变的函数,还需要一个调用渐变动画的函数。再新写一个函数如下:

function KillStreakPanel:SetKills()
    self:Set_kill_icon()

    self._headshot = false  -- 已经有击杀了,就不能再显示爆头图标了
    self:MakeFine()

    self._kill_panel:stop()
    self._kill_panel:animate(callback(self, self, "Show_fading"))
end

另外再修改PlayerManager.lua中的Hook调用函数KillStreakHook1

Hooks:PostHook(PlayerManager, "on_killshot", "KillStreakHook1", function()
    KillStreakPanel.kills = KillStreakPanel.kills + 1
    KillStreakPanel:SetKills()
end)

好,保存,运行游戏来看看效果吧!

嗯,效果不错,但是好像有点不得劲。图片突然出现,感觉缺少一点打击感。那就再添加一个瞬间变大再复原,并来附加一个短暂的渐变。

添加如下函数:

function KillStreakPanel:Show_brust(panel)
    local t = 0
    local w = self._kill_icon:w()

    while t < 0.1 do
        t = t + coroutine.yield()
        local n = math.sin((t / 2) * 350)  
        local t_size = math.lerp( w, 2*w, n)

        -- 放大icon并居中,不然会以左上角为原点放大
        self._kill_icon:set_size(t_size,t_size)
        self._kill_icon:set_center(self._kill_panel:w() / 2, self._kill_panel:h() / 2)

        self._kill_panel:set_alpha(math.lerp(0, 1, n))
    end

    self._kill_panel:set_alpha(1)-- 保证完全显示
    self:Reset_icon() -- 回复原来大小并居中
end

function KillStreakPanel:Reset_icon()
    self._kill_icon:set_size(450 ,450 )
    self:MakeFine()
end

完成上述函数后,还需要调用新加的动画效果,修改SetKills()如下:

function KillStreakPanel:SetKills()
    self:Set_kill_icon()

    self._headshot = false
    self:MakeFine()

    self._kill_panel:stop()

    self._kill_panel:animate(callback(self, self, "Show_brust"))
    self._kill_panel:animate(callback(self, self, "Show_fading"))
end

好,现在基本的击杀图标显示功能就实现了,接下来就是调整位置和大小了。

添加菜单选项

我们当然可以直接在代码里修改图片的位置和大小,但这样可能不能适应其他用户的使用情况,比如安装了其他HUD mod,可能会存在遮挡的情况。因此,我们需要添加一个游戏内的选项菜单来自定义这些选项,也方便后续控制新添加的变量。

在Mod目录下新建一个Menu.lua:

_G.KillStreak = _G.KillStreak or {}
KillStreak.ModPath = ModPath
KillStreak.SavePath = SavePath .. "KillStreak.txt"  -- 设置保存位置
KillStreak.LocPath = ModPath .. "loc/"              -- 本地化保存位置 方便翻译成多国语言
KillStreak.Opt = {} 
KillStreak.OptMenuId = "KillStreakOptions"

function KillStreak:Init()
    dofile(self.ModPath .. "/KillStreakPanel.lua")

    self:Load()

    self.Opt.fadetime = self.Opt.fadetime or 3    -- 后边的数字是默认设置
    self.Opt.pos = self.Opt.pos or 142
    self.Opt.scale = self.Opt.scale or 0.35
    self.Opt.alpha = self.Opt.alpha or 1.0

    HopLib:load_localization(KillStreak.LocPath, managers.localization)

    self.Panel = KillStreakPanel:new(managers.gui_data:create_fullscreen_workspace():panel())

    self.InitDone = true
end

function KillStreak:Save()
    local file = io.open(self.SavePath, "w+")
    if file then
        file:write(json.encode(self.Opt))
        file:close()
    end
end

function KillStreak:Load()
    local file = io.open(self.SavePath, "r")
    if file then
        self.Opt = json.decode(file:read("*all"))
        file:close()
    end
end

function MenuCallbackHandler:KillStreak_value(item)
    KillStreak.Opt[item._parameters.name:gsub("KillStreak_", "")] = item:value()
    KillStreak:Save()
    KillStreak.Panel:Reset_icon()
end

Hooks:Add("MenuManagerPopulateCustomMenus", "KillStreakOptions", function(self, nodes)
    if not KillStreak.InitDone then
        KillStreak:Init()
    end
    -- 添加各种选项
    MenuHelper:NewMenu(KillStreak.OptMenuId)
    MenuHelper:AddSlider({
        id = "KillStreak_fadetime",
        title = "KillStreak_fadetime_title",
        callback = "KillStreak_value",
        menu_id = KillStreak.OptMenuId,
        max = 60,
        min = 0.5,
        step = 0.1,
        show_value = true,
        value = KillStreak.Opt.fadetime,
    })      
    MenuHelper:AddSlider({
        id = "KillStreak_pos",
        title = "KillStreak_pos_title",
        callback = "KillStreak_value",
        menu_id = KillStreak.OptMenuId,
        max = 300,
        min = 16,
        step = 1,
        show_value = true,
        value = KillStreak.Opt.pos,
    })
    MenuHelper:AddSlider({
        id = "KillStreak_scale",
        title = "KillStreak_scale_title",
        callback = "KillStreak_value",
        menu_id = KillStreak.OptMenuId,
        max = 2.0,
        min = 0.1,
        step = 0.1,
        show_value = true,
        value = KillStreak.Opt.scale,
    })  
    MenuHelper:AddSlider({
        id = "KillStreak_alpha",
        title = "KillStreak_alpha_title",
        callback = "KillStreak_value",
        menu_id = KillStreak.OptMenuId,
        max = 1.0,
        min = 0.0,
        step = 0.1,
        show_value = true,
        value = KillStreak.Opt.alpha,
    })

    nodes[KillStreak.OptMenuId] = MenuHelper:BuildMenu(KillStreak.OptMenuId)
    MenuHelper:AddMenuItem(nodes.lua_mod_options_menu or nodes.blt_options, KillStreak.OptMenuId, "KillStreak_options_title", "KillStreak_options_desc")
end)

在mod目录中新建一个loc文件夹,里边放翻译文件,这里我们先创建简体中文的文件schinese.txt

{
    "KillStreak_options_title" : "连杀图标&语音选项",
    "KillStreak_options_desc" : "设定参数",
    "KillStreak_fadetime_title" : "连杀持续时间",
    "KillStreak_pos_title" : "连杀图标y轴偏移",
    "KillStreak_scale_title" : "连杀图标缩放因子",
    "KillStreak_alpha_title" : "图标透明度",
}

为了方便,这里统一把KillStreakPanel的实例化放在这里,所以对应的PlayerManager.lua也要修改,让它的代码更简洁。

Hooks:PostHook(PlayerManager, "on_killshot", "KillStreakHook1", function()
    KillStreak.Panel.kills = KillStreak.Panel.kills + 1
    KillStreak.Panel:SetKills()
end)
Hooks:PostHook(PlayerManager, "on_lethal_headshot_dealt", "KillStreakHook2", function()
    KillStreak.Panel._headshot = true
end)

因为更改了Hook相关代码,这里更新下mod.txt

{
    "name" : "KillStreak",
    "author" : "Eysent",
    "blt_version" : 2.3,
    "hooks" : [
        {"hook_id" : "lib/managers/playermanager", "script_path" : "PlayerManager.lua"},
        {"hook_id" : "lib/managers/menumanager", "script_path" : "Menu.lua"},
        {"hook_id" : "lib/states/ingameplayerbase", "script_path" : "Menu.lua"},
    ]
}

最后,我们把相关参数应用到以下几个函数中:

function KillStreakPanel:init(hud)
    self._full_hud = hud
    self.kills = 0
    self._headshot = false

    self._kill_panel = self._full_hud:panel({
        name = "kill_panel", 
        alpha = 0,
        layer = 100,
    })

    self._kill_icon = self._kill_panel:bitmap({
        vertical = "center",
        align = "center",
        name = "kill_icon",
        texture = "guis/textures/killstreak/killicons/streak",
        texture_rect = {0,0,450,450},
        blend_mode      = "normal",
        alpha = KillStreak.Opt.alpha,   -- 自定义参数
        w = 450 * KillStreak.Opt.scale,
        h = 450 * KillStreak.Opt.scale,
        layer = 4
    })
    self:Set_kill_icon()
    self:MakeFine()
end
function KillStreakPanel:MakeFine()
    self._kill_panel:set_size(self._kill_icon:w() + 2, self._kill_icon:h() + 2)
    self._kill_icon:set_center(self._kill_panel:w() / 2, self._kill_panel:h() / 2)
    self._kill_panel:set_center(self._full_hud:center_x(), self._full_hud:center_y() + KillStreak.Opt.pos)    -- Y轴位置
end
function KillStreakPanel:Show_fading(rect)
    -- fadetime 是开始渐变消失之前的持续时间
    local fadetime = KillStreak.Opt.fadetime -- 持续时间
    local wait_t = fadetime

    local t = 0.5 -- 从完全不透明到完全消失的时间

    -- 展示图层
    self._kill_panel:set_alpha(1)
    -- 等待anim_t秒

    while wait_t > 0 do
        wait_t = wait_t - coroutine.yield()
    end

    -- fade animation
    while t > 0 do
        t = t - coroutine.yield()
        local n = math.sin((t / 2) * 350)       
        self._kill_panel:set_alpha(math.lerp(0, 1, n))
    end

    self.kills = 0
    self._kill_panel:set_alpha(0)
    self._kill_panel:set_x(self._full_hud:center_x())
end
function KillStreakPanel:Reset_icon()
    self._kill_icon:set_size(450*KillStreak.Opt.scale ,450*KillStreak.Opt.scale) -- 大小
    self:MakeFine()
end

击杀图标的部分告一段落,下边就是最后一步,添加击杀语音。

添加语音提示

首先,从网上准备语音素材,我找到了部分CF角色的语音包,大概有这几个:"猎狐者", "零", "瞳", "毛妹"(不知道是谁,听起来有俄罗斯口音),"夏日之兰","奥摩","飞虎队"。不知道语音跟人对不对的上,毕竟我退坑很久了,大部分角色语音都没听过。每个角色都包含以下语音,通过名字很清楚了解到对应什么语音。如果你要自定义的话,替换同名文件就可以了。

关于语音的菜单部分,即Menu.lua,这里就不贴出来了,和前文大同小异,只是多几个而已。在KillStreakPanel.lua中添加的函数如下:

function KillStreakPanel:play_kill_sound()
    blt.xaudio.setup()
    local table_index = self.kills
    if self.kills ==1 and not self._headshot then
        return 
    elseif self.kills > 8 then 
        table_index = 8
    end
    local filename = self:get_sound_filename(table_index)
    local buffer = XAudio.Buffer:new(filename)

    table.insert(self._sound_sources,XAudio.Source:new(buffer))
end

function KillStreakPanel:update_sound_sources()
    for i, src in ipairs(self._sound_sources) do
        if src then
            if not src:is_closed() then
                blt.xaudio.setup()
                src:set_volume(KillStreak.Opt.volume/100)
                if managers.player:player_unit() then
                    src:set_position(managers.player:player_unit():position())
                end
            else
                src = nil
            end
        end
    end
end

function KillStreakPanel:play_voices(voice_type)
    if KillStreak.Opt.enable_sound then
        blt.xaudio.setup()

        local filename = self:get_sound_filename(voice_type)
        local buffer = XAudio.Buffer:new(filename)
        table.insert(self._sound_sources,XAudio.Source:new(buffer))
    end
end

function KillStreakPanel:get_sound_filename(voice_type)
    local filename = KillStreak.ModPath .. "assets/sounds/"
    --filename = filename .. "women1"
    filename = filename .. KillStreak.voices[KillStreak.Opt.voice_source].src
    filename = filename .. "/"
    filename = filename .. self._ogg_table[voice_type]
    return filename
end

修改部分函数:

function KillStreakPanel:init(hud)
    self._full_hud = hud
    self.kills = 0
    self._headshot = false
    self._ogg_table = {
        --[0] = "Knifekill.ogg"
        [1] = "Headshot.ogg",
        [2] = "MultiKill_2.ogg",
        [3] = "MultiKill_3.ogg",
        [4] = "MultiKill_4.ogg",
        [5] = "MultiKill_5.ogg",
        [6] = "MultiKill_6.ogg",
        [7] = "MultiKill_7.ogg",
        [8] = "MultiKill_8.ogg",
        [9] = "GrenadeKill.ogg",
        [10] = "Round_Start.ogg",
        [11] = "Fireinthehole_Grenade.ogg",
    }
    self._sound_sources = {}
   ... ... 
   ... ... 

end

function KillStreakPanel:SetKills()
    self:Set_kill_icon()
    if KillStreak.Opt.enable_sound then
        self:play_kill_sound()
    end

    self._headshot = false
    self:MakeFine()

    self._kill_panel:stop()

    self._kill_panel:animate(callback(self, self, "Show_brust"))
    self._kill_panel:animate(callback(self, self, "Show_fading"))
end

最后在PlayerManager.lua 添加扔手雷等其他语音函数并修改mod.txt

Hooks:PostHook(PlayerManager, "on_throw_grenade", "killsteak_play_grenade_voice", function()
    KillStreak.Panel:play_voices(11)
end)

Hooks:PostHook(HUDManager, "update", "killstreak_update_sound", function (self, ...)
    KillStreak.Panel:update_sound_sources(...)
end)

-- use voice manager to make character speak one setence at same time. see https://superblt.znix.xyz/doc/xaudio/
Hooks:PostHook(IngameMaskOffState, "at_exit", "killsteak_play_start_voice", function (self, ...)
    KillStreak.Panel:play_voices(10)
end)
{
    "name" : "KillStreak",
    "author" : "Eysent",
    "description" : "Add Crossfire kill streak icons and voices.",
    "blt_version" : 2.3,
    "hooks" : [
        {"hook_id" : "lib/managers/playermanager", "script_path" : "PlayerManager.lua"},
        {"hook_id" : "lib/managers/menumanager", "script_path" : "Menu.lua"},
        {"hook_id" : "lib/states/ingameplayerbase", "script_path" : "Menu.lua"},
        {"hook_id" : "lib/managers/hudmanager", "script_path" : "PlayerManager.lua"}
    ]
}

大功告成!希望你也可以自定义出自己的MOD,如有问题或者反馈可以留言或者联系我。

视频演示地址:https://www.bilibili.com/video/BV1DK411f7W6/

Eysent
【神界原罪2】超详细Mod实例教程 03 : 故事编辑器以及Osiris脚本
【神界原罪2】超详细Mod实例教程 03 : 故事编辑器以及Osiris脚本
【神界原罪2】修复官方Mod工具The Divinity Engine 2 数据编辑器部分技能伤害来源失效BUG
【神界原罪2】修复官方Mod工具The Divinity Engine 2 数据编辑器部分技能伤害来源失效BUG
【神界原罪2】超详细MOD实例教程 02:技能常用属性详细讲解及示例
【神界原罪2】超详细MOD实例教程 02:技能常用属性详细讲解及示例
【神界原罪2】超详细Mod实例教程 01:工具基本介绍和修改技能实例
【神界原罪2】超详细Mod实例教程 01:工具基本介绍和修改技能实例
【骑马与砍杀2:霸主】 dnSpy修改游戏数据,改变俘虏的招募速度
【骑马与砍杀2:霸主】 dnSpy修改游戏数据,改变俘虏的招募速度
【维多利亚3】游戏数据修改,模组教程——修改单一建筑最大建筑力
【维多利亚3】游戏数据修改,模组教程——修改单一建筑最大建筑力

0

  1. This post has no comment yet

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注