cardsurvival吧 关注:5,823贴子:48,252

关于游戏的修改,好像很少看到有人分享

只看楼主收藏回复

首先说下,这个游戏没啥难度,没事不建议修改。
但我是个爱折腾的人,喜欢对游戏代码动手动脚,大家引以为戒。
这是一个从设计上来说,结构非常简单的游戏,简单到我玩了一遍后感觉没啥可修改的,游戏基于Unity引擎开发,估计也可以使用MelonLoader啥的加载mod,但我实在懒得去试,有兴趣的可以试验下,或许可以扩展些游戏乐趣。
看了大概好几个小时的代码,总结下这个游戏实际上就是两组状态数据的交互,一组数据叫卡牌状态数据,比如在卡牌上显示的耐久、使用次数、燃料啥的;另一组类似公共状态数据,比如种群数量、角色的那一坨状态和技能、环境影响啥。然后,没了。是不是觉得没啥修改的。
接下来说下这两组数据的修改,实际上就是两个函数,卡牌的状态数据修改在InGameCardBase的ModifyDurability函数,通过这个函数的修改,你可以修改卡牌状态数据,比如耐久、使用次数、燃料消耗的快慢、液体的使用次数(也可以无限使用)等等;公共状态数据修改在GameManager的ChangeStat函数,可以修改种群变化、角色状态(包括负重、技能经验等)、环境影响啥的。
接下来如果大家感兴趣,看我如何一步一步的控制【毁掉】这个游戏,工具和具体操作我会一楼一楼的码字
不喜欢修改的直接忽略此贴,我只是图个乐


IP属地:广东1楼2023-01-08 14:08回复
    修改的第一步,就是了解这个游戏的文件结构
    游戏根目录
    Card Survival - Tropical Island_Data 文件夹,游戏的资源以及逻辑
    -----Managed 文件夹,里面的是游戏引用的DLL,一般会打包进去很多.net的和Unity的,修改的话也就是其中一个,其他的都不管
    ----------Assembly-CSharp.dll,游戏的基本逻辑都在里面了,我们修改也是这个文件
    ----------其他DLL,不理会了
    -----Plugins 文件夹 如果做Mod的话可以放这里,或者用单独的Mod文件夹,我这次要修改DLL,不用管他
    -----Resources 文件夹 同Plugins
    -----StreamingAssets 文件夹,包含本地化啥的,我们要使用本地化的文件来查找对应的编码
    ----------Localization 文件夹,本地化
    ---------------SimpCn.csv 文件,用文本编辑工具打开,可以用中文找到对应的编码,也就是第一列
    -----一大堆的资源文件,可以用特殊的工具打开,但是没必要
    MonoBleedingEdge 文件夹,Mono生成,不深究,也就不展开了
    Card Survival - Tropical Island.exe 游戏执行文件
    UnityCrashHandler64.exe 引擎必要文件
    UnityPlayer.dll 引擎必要文件
    游戏存档【Steam】:
    默认位置:%USERPROFILE%\AppData\LocalLow\WinterSpring Games\Card Survival - Tropical Island
    Unity 文件夹,不用管
    Options.json 文件,设置对应的存储文件
    Player.log 文件,游戏日志,如果我们要修改的话,可能会作为输出
    SaveData.json 文件,游戏存档,结构贴吧内有说明,后面我们修改也要检查存档来对应
    SaveData.json.backup 文件,当天的备份
    steam_autocloud.vdf 文件,我开的云存储,不知道不开的话有没有
    修改第二步,那就是准备工具,工具只有一个:DNSpy,主要用来反编译C Sharp语言生成的DLL,方便我们阅读和修改,这个工具网上随便搜索就有,Github地址我就不放了


    IP属地:广东2楼2023-01-08 14:48
    回复
      这个游戏有两个难题,一个是绿皮程序员的代码确实门槛高,一众萌新改派看都看不懂
      而是游戏弄清楚资源获取和极端情况的应对方法后,这游戏就变成了休闲养生游戏,真不懂要改什么


      IP属地:广西来自Android客户端4楼2023-01-08 15:03
      回复
        介绍完工具和结构,我们就开始折腾了
        打开DNSpy,选择文件->打开,找到游戏根目录\Card Survival - Tropical Island_Data\Managed\Assembly-CSharp.dll,选择打开。
        在程序集资源管理器中,依次展开Assembly-CSharp->Assembly-CSharp.dll->{}-,就得到了如下图所示

        向下拉,找到InGameCardBase,展开后找到ModifyDurability((DurabilitiesTypes, float, bool)函数,点击后可以在右侧查看该函数的代码,如下图所示:

        下一楼层我们粗略的分析下这个函数的代码,然后再说怎么修改


        IP属地:广东5楼2023-01-08 15:06
        回复
          首先是函数的定义
          ModifyDurability(DurabilitiesTypes _Type, float _Amt, bool _Feedback)
          三个参数分别对应的含义:
          _Type:你要修改的状态,一共有九种Spoilage/Usage/Fuel/Progress/Liquid/Special1/Special2/Special3/Special4,其中Spoilage是自然损耗,包括食物掉耐久啥的都是这里;Usage是使用,比如工具、还有食物你吃了一口……,但是液体不算在里面;Fuel是燃料,比如营火的燃料、熔炉啥的;Liquid是液体,单独拉出来,相当于Usage,可能是作者有其他想法;其他的不重要,不介绍了。
          _Amt:变更的数据,可以为正数,代表增长,也可以为负数,代表减少
          _Feedback:不要动就是了,不影响
          涉及到修改函数的部分逻辑
          我从没见过如此简单粗暴的游戏开发代码,直接找到(大概也就七八行的样子)
          switch (_Type),大括号里case DurabilitiesTypes针对每一种状态简单粗暴的加减……,怪不得卡牌一多就加载慢哈
          要修改最简单的办法就是在开始就对参数_Amt下手了,比如我要无限净水,那么就在水这张卡牌上的Liquid状态上设置Amt为负数时变成0就可以了,这样子水就不会减少,只会增加,放心,不会超过100%的哈,是不是超级简单。
          原始代码如下:
          case DurabilitiesTypes.Liquid:
          if (!this.IsLiquid)
          {
          yield break;
          }
          num = this.CurrentLiquidQuantity;
          this.CurrentLiquidQuantity += _Amt;
          num2 = this.CurrentLiquidQuantity;
          durabilityStat = null;
          this.CurrentLiquidQuantity = ((this.CurrentMaxLiquidQuantity > 0f) ? Mathf.Clamp(this.CurrentLiquidQuantity, 0f, this.CurrentMaxLiquidQuantity) : Mathf.Min(this.CurrentLiquidQuantity, 0f));
          amt = this.CurrentLiquidQuantity - num;
          this.WeightHasChanged();
          break;
          修改后:
          case DurabilitiesTypes.Liquid:
          if (!this.IsLiquid)
          {
          yield break;
          }
          if (this.CardModel.CardName.LocalizationKey == "LQ_Water_CardName") //判断当前卡牌,怎么来的下面说
          {
          _Amt *= ((_Amt > 0f) ? 1f : 0f);//_Amt大于0,乘以1f;_Amt小于0,乘以0f
          }
          num = this.CurrentLiquidQuantity;
          this.CurrentLiquidQuantity += _Amt;
          num2 = this.CurrentLiquidQuantity;
          durabilityStat = null;
          this.CurrentLiquidQuantity = ((this.CurrentMaxLiquidQuantity > 0f) ? Mathf.Clamp(this.CurrentLiquidQuantity, 0f, this.CurrentMaxLiquidQuantity) : Mathf.Min(this.CurrentLiquidQuantity, 0f));
          amt = this.CurrentLiquidQuantity - num;
          this.WeightHasChanged();
          break;
          其中this.CardModel.CardName.LocalizationKey是指当前卡牌的编码
          其中LQ_Water_CardName是水的编码
          编码查看在【游戏根目录\Card Survival - Tropical Island_Data\StreamingAssets\Localization\SimpCn.csv】,用文本工具查找水,会找到这一行:
          LQ_Water_CardName,Water,水
          第一列是编码,第二列是英文,第三列中文
          至于为什么不在switch外面修改参数_Amt,那是因为同一张卡牌可以有多个状态,修改前还要判断一下要修改的状态,没必要,这些switch已经在做了
          当然你这样是无法保存的,只是演示修改的思路
          具体修改介绍完下一个函数后,再写怎么用IL任意蹂躏别人的dll


          IP属地:广东6楼2023-01-08 15:57
          回复
            在程序集资源管理器中,向上拉不远找到GameManager类,展开后找到 ChangeStat(StatModifier, StatModification, string, string, int, object, Transform, bool)函数,本来我看到这个函数的定义
            ChangeStat(StatModifier _Stat, StatModification _Modification, string _From, string _Action, int _ActionTick, object _Source, Transform _FeedbackPos = null, bool _WaitForAnimation = true)
            这么多的参数,应该很好修改吧,结果这些参数只有_Stat和_Action可堪一用,其他的参数,哎,不知道为什么要传递进来,只说下这两个参数含义吧:
            参数_Stat:要修改的状态,这个就非常多了,大概要上百个吧,具体可以查看存档里面的AllStats,里面有的都能修改
            参数_Action:每一个操作都会有一个_Action字符串,具体的我没找到统计,反正不重要。重要的是只有操作才会有_Action,其他方式引起变化时_Action为null。比如阳光强度等来自于环境,这时候_Action就为null。还有一个特殊情况,就是创建的时候_Action为null,所以_Action内容不重要,重要的是它是否为null。
            涉及到修改函数的部分逻辑
            继续简单粗暴,在函数开始大概七八行左右,有这么一句:
            float num = Mathf.Approximately(_Stat.ValueModifier.x, _Stat.ValueModifier.y) ? _Stat.ValueModifier.x : UnityEngine.Random.Range(_Stat.ValueModifier.x, _Stat.ValueModifier.y);
            这里的num就是状态变化的值,只要这句话下面加一个判断:
            if (_Stat.Stat.GameName.LocalizationKey == "Skill_Woodworking_GameName") //判断当前修改的状态为木工技能,怎么来的看上一楼
            {
            num *= ((num > 0f) ? 2.5f : 0f);//num大于0,乘以2.5f,就是木工技能获取2.5倍的经验;num 小于0,乘以0f
            }
            其中_Stat.Stat.GameName.LocalizationKey就是当前要修改的状态编码,道理和前面一样
            这样,你的木工2.5倍经验就成功了,但是会遇到你角色初始化也是2.5倍的,如果初始化不想2.5倍,那就需要判断_Action不为null就可以了
            同样无法直接保存,只是演示修改思路,要修改只能通过IL的方式,下一楼层演示怎么用IL方式来修改


            IP属地:广东7楼2023-01-08 16:25
            回复
              使用DNSpy修改Dll的时候有几种方式,第一种是简单粗暴的在代码页面直接点击右键,选择编辑类或者编辑方法,如下图所示:

              之后进入代码编辑页面,直接修改代码,如下图所示,修改后点击编译。

              然后选择文件->保存模块,弹出的对话框保存即可.
              但是如果你编辑的时候dll引用不完全或者一些特殊情况下,就会用另一种常见的修改方式,那就是IL指令修改方式,下一楼介绍IL修改


              IP属地:广东8楼2023-01-08 16:42
              回复
                你打的这些,其实就很能说明为啥没人分享修改了


                IP属地:广东9楼2023-01-08 16:52
                回复
                  IL指令是一种类似汇编的低级语言,所以不存在什么dll引用,只要粗暴的盘它,就可以修改任意没有加壳dll了
                  同样在DNSpy中,在代码页面中右键,选择编辑IL指令,就会进入IL编辑模式,如下图所示

                  进入IL编辑模式,如下图

                  如上图所示,有四列分别是序号、偏移、操作码、操作符
                  序号:类似于行号,我们要选择某一行的时候单机行号即可,在行号上面右键就可以插入新的行,把自己的内容写进去就可以了
                  偏移:每个函数都是从0000开始计算偏移地址,每个指令的长度不一样,所以每行会相对函数其实有一个偏移量,当时使用跳转的时候,就要指向偏移,不能指向行号
                  操作码:参考微软文档,搜索opcodes,所有的指令基本都在里面了
                  操作符:就是要操作的内容,当对栈内容进行操作时是没有操作符,只有操作码的
                  听起来是不是有点坑了,但是IL修改真的是游戏修改党常备的技能了,哈哈,下一楼来做个简单的IL修改


                  IP属地:广东10楼2023-01-08 16:56
                  回复
                    前面介绍的两个函数涉及到判断,不适合做修改IL的例子,这里就用另外一个修改吧
                    比如,科技速度吧,现在的科技速度很烦人,动不动就好几天,我们给它提速
                    科技研究的速度控制在GameManager类的ProgressCurrentResearch函数,找到下面这句
                    blueprintResearchTimes[currentResearch] = num + 1;
                    把他改成:
                    blueprintResearchTimes[currentResearch] = num + 25;
                    这样子研究速度就变成二十五倍了,然后用第一种方式的话,会出现如下图提示,无法保存

                    这个时候取消吧,该是IL登场了
                    在代码中选择blueprintResearchTimes[currentResearch] = num + 1;这一行,一定要选中哈,不然你就挨行找吧
                    然后右键,选择编辑IL指令,然后这一行的IL指令就高亮了,如下图所示

                    其中第50行
                    50008Dldc.i4.1
                    ldc.i4.1就是1,这是编码中的短码,其中int类型0-8分别是ldc.i4.0……ldc.i4.8,再大就要使用ldc.i4,然后操作符填数字,比如我就稍微修改一下,25倍太变态了,20倍吧

                    点击确定,代码中已经显示
                    blueprintResearchTimes[currentResearch] = num + 25;
                    然后在DNSpy中选择文件->保存模块,弹出对话框选择确定
                    进入游戏,喝口水的功夫都能研究出皮裤了,哈哈
                    这是一个IL编辑的演示,和前面两个函数没有关系,下一楼接着说前面两个函数的修改


                    IP属地:广东11楼2023-01-08 17:21
                    收起回复
                      左转mod制作群,


                      IP属地:山东来自Android客户端13楼2023-01-08 18:55
                      回复
                        吃饭归来,接着聊两个函数的修改
                        两个函数的修改涉及到上百种状态编码和那么多的卡的编码,这些都是字符串,显然写在代码里有点累,所有就先创建一个xml来定义这些修改,xml结构如下:
                        <?xml version="1.0" encoding="utf-8"?>
                        <root debug = "true"><!--debug表示是否需要打印修改时相关的状态或者卡牌信息,玩的时候要设置为false,我这是要测试配置文件,所以设置为true,一旦设置为true,可以在存档目录的Player.log里面看到修改相关的记录,非常多,会让日志爆炸-->
                        ----<patchers name = "Stats"/> <!--要修改的状态信息-->
                        ----<patchers name = "FuelCards"/><!--要修改燃料的卡牌信息-->
                        ----<patchers name = "LiquidCards"/> <!--要修改液体的卡牌信息-->
                        ----<patchers name = "SpoilageCards"/><!--要修改损耗\腐烂\自然消耗的卡牌信息-->
                        ----<patchers name = "UsageCards"/><!--要修改使用耐久的卡牌信息-->
                        </root>
                        加入具体内容,比如我要改攀爬技能100倍,爬一下就满级哈,那么就改成如下
                        <?xml version="1.0" encoding="utf-8"?>
                        <root debug = "true">
                        ----<patchers name = "Stats">
                        --------<patcher name="Skill_Climbing_GameName" action = "true" negative = "0" positive = "100"/><!--测试用猎人啥的开局就有攀爬50,所以要设置action为true,这样只有在游戏中做了攀爬的动作,才会触发100倍-->
                        ----</patchers>
                        </root>
                        比如我再加一个卡瓦汤和淡水管饱,无限续杯
                        <?xml version="1.0" encoding="utf-8"?>
                        <root debug = "true">
                        ----<patchers name = "Stats">
                        --------<patcher name="Skill_Climbing_GameName" action = "true" negative = "0" positive = "100"/>
                        ----</patchers>
                        ----<patchers name = "LiquidCards"/> <!--卡瓦汤和净水都是液体,所以要在这里改,这里的action不重要-->
                        --------<patcher name="LQ_Water_CardName" action = "false" negative = "0" positive = "1"/>
                        --------<patcher name="LQ_Kava_CardName" action = "false" negative = "0" positive = "1"/>
                        ----</patchers>
                        ----<patchers name = "SpoilageCards"/><!--卡瓦汤还有变质的数值,所以再自然损耗里也要加上卡瓦汤,这里的action不重要,随意设置true和false-->
                        --------<patcher name="LQ_Kava_CardName" action = "false" negative = "0" positive = "1"/>
                        ----</patchers>
                        </root>
                        好了,就是这么简单粗暴的配置方式
                        我们把这个xml文件命名为Patch.xml
                        放在存档路径:%USERPROFILE%\AppData\LocalLow\WinterSpring Games\Card Survival - Tropical Island
                        接下来定义怎么使用这个配置,排版废了


                        IP属地:广东15楼2023-01-08 22:21
                        回复
                          接下来我们回到DNSpy中,在Assembly-CSharp->Assembly-CSharp.dll->{}-上面点击右键,选择添加类,如下图所示

                          在弹出框内输入类的定义如下:
                          using System;
                          //这个类用来与xml映射,方便解析xm'l和后期对数据的调用
                          public class Patcher
                          {
                          // 对应着root/patchers/patcher的name
                          public string Name { get; set; }
                          // 对应着root/patchers/patcher的action
                          public bool Action { get; set; }
                          // 对应着root/patchers/patcher的negative
                          public float Negative { get; set; }
                          // 对应着root/patchers/patcher的positive
                          public float Positive { get; set; }
                          //创建Patcher实例,方便添加
                          public static Patcher Create(string name, bool action, float negative, float positive)
                          {
                          return new Patcher
                          {
                          Name = name,
                          Action = action,
                          Negative = negative,
                          Positive = positive
                          };
                          }
                          }
                          点击编译
                          然后在DNSpy菜单中选择文件-保存模块,弹出的框中选择确定
                          接下里就是逻辑处理了


                          IP属地:广东16楼2023-01-08 22:44
                          回复
                            逻辑类的定义首先要上一个类一样,选择添加类
                            在弹出框内输入类的定义如下:
                            using System;
                            using System.Collections.Generic;
                            using System.IO;
                            using System.Xml;
                            using UnityEngine;
                            // 自定义逻辑类
                            public class Udf
                            {
                            // 静态构造函数,用来加载xml的配置到变量
                            static Udf()
                            {
                            Udf.InitPatchers();
                            }
                            // 拼接xml文件所在位置
                            private static string ConfigFile()
                            {
                            return Path.Combine(new string[]
                            {
                            Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
                            "AppData",
                            "LocalLow",
                            "WinterSpring Games",
                            "Card Survival - Tropical Island",
                            "Patch.xml"
                            });
                            }
                            // 加载xml文件信息到变量
                            private static void InitPatchers()
                            {
                            XmlDocument xmlDocument = new XmlDocument();
                            xmlDocument.Load(Udf.ConfigFile());
                            XmlElement documentElement = xmlDocument.DocumentElement;
                            Udf.debug = bool.Parse(documentElement.GetAttribute("debug"));
                            foreach (object obj in documentElement.ChildNodes)
                            {
                            XmlElement element_patchers = (XmlElement)obj;
                            Dictionary<string, Patcher> dictionary = new Dictionary<string, Patcher>();
                            foreach (object obj2 in element_patchers.ChildNodes)
                            {
                            XmlElement element_patcher = (XmlElement)obj2;
                            dictionary.Add(element_patcher.GetAttribute("name"), Patcher.Create(element_patcher.GetAttribute("name"), bool.Parse(element_patcher.GetAttribute("action")), float.Parse(element_patcher.GetAttribute("negative")), float.Parse(element_patcher.GetAttribute("positive"))));
                            }
                            string patcher_type = element_patchers.GetAttribute("name");
                            if (!(patcher_type == "Stats"))
                            {
                            if (!(patcher_type == "FuelCards"))
                            {
                            if (!(patcher_type == "LiquidCards"))
                            {
                            if (!(patcher_type == "SpoilageCards"))
                            {
                            if (patcher_type == "UsageCards")
                            {
                            Udf.patch_usage_cards = dictionary;
                            }
                            }
                            else
                            {
                            Udf.patch_spoilage_cards = dictionary;
                            }
                            }
                            else
                            {
                            Udf.patch_liquid_cards = dictionary;
                            }
                            }
                            else
                            {
                            Udf.patch_fuel_cards = dictionary;
                            }
                            }
                            else
                            {
                            Udf.patch_stats = dictionary;
                            }
                            }
                            }
                            // 处理逻辑,非常简单
                            private static float Patch(Dictionary<string, Patcher> patchers, string key, bool action, float num)
                            {
                            if (num == 0f)
                            {
                            return num;
                            }
                            if (patchers == null)
                            {
                            return num;
                            }
                            if (!patchers.ContainsKey(key))
                            {
                            return num;
                            }
                            if (action && patchers[key].Action)
                            {
                            return num;
                            }
                            return num * ((num > 0f) ? patchers[key].Positive : patchers[key].Negative);
                            }
                            // 入口函数:GameManager的ChangeStat函数中调用
                            public static float PatchStat(StatModifier _Stat, string _Action, float num)
                            {
                            if (Udf.debug)
                            {
                            Debug.Log(string.Format("PatchStat::{0}::{1}::{2}::{3}", new object[]
                            {
                            _Stat.Stat.GameName.LocalizationKey,
                            _Stat.Stat.GameName,
                            _Action,
                            num
                            }));
                            }
                            bool flag = _Action == null;
                            return Udf.Patch(Udf.patch_stats, _Stat.Stat.GameName.LocalizationKey, flag, num);
                            }
                            // 入口函数:InGameCardBase的ModifyDurability函数中case DurabilitiesTypes.Liquid: 中调用
                            public static float PatchLiquidCards(string cardName, float _Amt)
                            {
                            if (Udf.debug)
                            {
                            Debug.Log(string.Format("PatchLiquidCards::{0}::{1}", cardName, _Amt));
                            }
                            return Udf.Patch(Udf.patch_liquid_cards, cardName, false, _Amt);
                            }
                            // 入口函数:InGameCardBase的ModifyDurability函数中case DurabilitiesTypes.Spoilage: 中调用
                            public static float PatchSpoilageCards(string cardName, float _Amt)
                            {
                            if (Udf.debug)
                            {
                            Debug.Log(string.Format("PatchSpoilageCards::{0}::{1}", cardName, _Amt));
                            }
                            return Udf.Patch(Udf.patch_spoilage_cards, cardName, false, _Amt);
                            }
                            // 入口函数:InGameCardBase的ModifyDurability函数中case DurabilitiesTypes.Usage: 中调用
                            public static float PatchUsageCards(string cardName, float _Amt)
                            {
                            if (Udf.debug)
                            {
                            Debug.Log(string.Format("PatchUsageCards::{0}::{1}", cardName, _Amt));
                            }
                            return Udf.Patch(Udf.patch_usage_cards, cardName, false, _Amt);
                            }
                            // 入口函数:InGameCardBase的ModifyDurability函数中case DurabilitiesTypes.Fuel: 中调用
                            public static float PatchFuelCards(string cardName, float _Amt)
                            {
                            if (Udf.debug)
                            {
                            Debug.Log(string.Format("PatchFuelCards::{0}::{1}", cardName, _Amt));
                            }
                            return Udf.Patch(Udf.patch_fuel_cards, cardName, false, _Amt);
                            }
                            // 变量:是否输出debug信息
                            private static bool debug;
                            // 变量:用来存储从xml中定义的状态修改信息,未定义就是null
                            private static Dictionary<string, Patcher> patch_stats;
                            // 变量:用来存储从xml中定义的卡牌燃料修改信息,未定义就是null
                            private static Dictionary<string, Patcher> patch_fuel_cards;
                            // 变量:用来存储从xml中定义的卡牌液体修改信息,未定义就是null
                            private static Dictionary<string, Patcher> patch_liquid_cards;
                            // 变量:用来存储从xml中定义的卡牌损耗修改信息,未定义就是null
                            private static Dictionary<string, Patcher> patch_spoilage_cards;
                            // T变量:用来存储从xml中定义的卡牌耐久修改信息,未定义就是null
                            private static Dictionary<string, Patcher> patch_usage_cards;
                            }
                            很长,但都是些垃圾代码


                            IP属地:广东17楼2023-01-08 23:18
                            回复
                              能看懂代码结构,其实你自己就可以做这么一款了,无非就是需要罗列堆叠很多很多东西
                              现在的大众接受修改的复杂度很低:
                              基本仅限于:叮的一下
                              所以你研究的这些,估计也就常做游戏的程序会感兴趣


                              IP属地:吉林来自iPhone客户端18楼2023-01-08 23:59
                              回复