故事起因如下:
- 发现了新的自动化游戏,好耶!
- 里面有不得不玩的战斗部分,坏耶。
- 掏出CheatEngine对人物血量进行一个锁定!
- 然后出战斗场景忘记关了,改写了其他数据,导致存档爆炸了。
上一次手动存档还是好几小时前的。好在CE只改写了4字节数据,修复存档怎么想也比重来一次快。但是这崭新的游戏根本没资料可以参考,想要拿到存档反序列化逻辑就不得不对游戏本身动手了。
望闻问切
先从读档报错开始下手。
问题表现为读取存档时出现如下报错:
Unhandled exception. System.Collections.Generic.KeyNotFoundException: The given key '343' was not present in the dictionary.
at System.Collections.Generic.Dictionary`2.get_Item(TKey key)
at iFActionScript.GDItemMin.getData()
at iFActionScript.GDItemMin.getEquip()
at iFActionScript.GActor.get_NG()
at iFActionScript.GActor.get_hpMax()
at iFActionScript.STitle.loadSelect(GSaveInfoMin data, Boolean ns)
at iFActionScript.WFile.loadGame(GSaveInfoMin data, Boolean ns)
at iFActionScript.WFile.minWinClose()
at iFActionScript.WBase.dispose()
at iFActionScript.WFileSelect.dispose()
at iFActionScript.WFileSelect.update()
at iFActionScript.WBase.update()
at iFActionScript.WFile.update()
at iFActionScript.STitle.update()
at iFActionGame2.Game.OnUpdateFrame(FrameEventArgs e)
at OpenTK.Windowing.Desktop.GameWindow.DispatchUpdateFrame()
at OpenTK.Windowing.Desktop.GameWindow.Run()
at iFActionGame2.Program.Main(String[] args)
正好手边的CE还没关。看一眼修改的数据,也是343。
这不是巧了吗?和报错里的数字居然一样欸!
看一眼调用栈,应该是退出战斗场景之后,某个倒霉的装备数据栏被CE改了。保存的时候没有检查,但是加载存档的时候计算装备加成发现根本没这号装备,遂崩。
速通Bug
拿binwalk搜索int32值343,共找到一处。打出来看一眼:
❯ printf "%x\n" 343
157
❯ binwalk -R "\x57\x01" save_1680854935_0.sav
DECIMAL HEXADECIMAL DESCRIPTION
--------------------------------------------------------------------------------
111 0x6F Raw signature (\x57\x01)
❯ binwalk -W save_1680854935_0.sav --offset=0x6f --length=0x20
OFFSET save_1680854935_0.sav
--------------------------------------------------------------------------------
0x0000006F 57 01 00 00 01 00 00 00 01 00 00 00 CB 32 00 00 |W............2..|
0x0000007F 01 00 00 00 01 00 00 00 D6 03 00 00 01 00 00 00 |................|
不难看出,这里似乎是12字节一个单位。既然后面那组数据没改过,那就把57 01改成CB 32。
改完,加载,没有崩溃。除了角色的服装槽装了一把剑之外,一切都很正常。
全文完。
无事生非
“诶诶你别急着全文完。我逆向呢?我拆包呢?”
得嘞,我不装了。修存档是假,搞事情是真。每天拿CE来回搜数值多累啊,这不得给自己减负一下?
既然要给病人拆胳膊卸腿了,那得先看看全貌。回忆一下刚才的调用栈,顶层函数签名是iFActionGame2.Program.Main(String[] args)。扫一眼游戏目录,就是你小子叫iFActionGame.dll,身边还配一个叫iFActionGame.deps.json的小弟?看你面相就像是团伙头目,就拿你开刀。
dnSpy加载iFActionGame.dll,入口点映入眼帘。可是刚才调用栈里那一堆叫iFActionScript的方法怎么也找不着。先不急,看一眼入口。
iFActionGame2.Program.Main
// iFActionGame2.Program
// Token: 0x060002A4 RID: 676 RVA: 0x00016338 File Offset: 0x00014538
private static void Main(string[] args)
{
AppDomain.CurrentDomain.UnhandledException += Program.CurrentDomain_UnhandledException;
bool flag = args == null || args.Length == 0;
if (flag)
{
args = new string[]
{
"iFActionEditor",
"E:\\unfamiliar\\path\\",
"1280",
"720",
"2"
};
}
IVal.DEBUG = false;
IVal.MaxWidth = 1920;
IVal.MaxHeight = 1080;
IVal.WindowZoomOldType = 1;
IVal.WindowZoomType = 1;
bool debug = IVal.DEBUG;
if (debug)
{
bool flag2 = args.Length > 2;
if (flag2)
{
IVal.GWidth = int.Parse(args[2]);
IVal.GHeight = int.Parse(args[3]);
}
else
{
IVal.GWidth = 1280;
IVal.GHeight = 720;
}
bool flag3 = args.Length > 4;
if (flag3)
{
IVal.Language = int.Parse(args[4]);
}
else
{
IVal.Language = 1;
}
bool flag4 = IVal.Language == 1;
if (flag4)
{
IVal.Text = new GLanguageEN().GetLanguage();
}
else
{
bool flag5 = IVal.Language == 2;
if (flag5)
{
IVal.Text = new GLanguageCH().GetLanguage();
}
else
{
IVal.Text = new GLanguageEN().GetLanguage();
}
}
IVal.TempWinWidth = IVal.GWidth;
IVal.TempWinHeight = IVal.GHeight;
bool flag6 = args.Length != 0 && args[0] == "iFActionEditor";
if (flag6)
{
IVal.BasePath = args[1].Replace("?", " ");
Program.<Main>g__loadSet|0_1();
Program.<Main>g__loadMod|0_0(IVal.BasePath);
NativeWindowSettings nativeWindowSettings = new NativeWindowSettings
{
Size = new Vector2i(IVal.GWidth, IVal.GHeight),
Title = "iFActionGame"
};
nativeWindowSettings.AutoLoadBindings = true;
using (Game game = new Game(new GameWindowSettings
{
RenderFrequency = 60.0,
UpdateFrequency = 60.0
}, nativeWindowSettings))
{
IVal.mainGame = game;
bool flag7 = File.Exists(IVal.BasePath + "icon.png");
if (flag7)
{
Bitmap bitmap = IBitmap.ABitmap("icon.png");
game.Icon = new WindowIcon(new OpenTK.Windowing.Common.Input.Image[]
{
new OpenTK.Windowing.Common.Input.Image(bitmap.Width, bitmap.Height, IBitmap.bmpToRGBA(bitmap))
});
bitmap.Dispose();
}
IVal.mainGame.WindowBorder = WindowBorder.Resizable;
game.Run();
}
}
}
else
{
IVal.BasePath = AppDomain.CurrentDomain.BaseDirectory;
bool flag8 = File.Exists(IVal.BasePath + "iFCon");
if (flag8)
{
IVal.Pack = new DPack("iFCon");
}
IVal.Text = new GLanguageEN().GetLanguage();
IRWFile irwfile = new IRWFile("Game.ifaction");
string a = irwfile.ReadMs(8);
bool flag9 = a == "IFACTION";
if (flag9)
{
Program.<Main>g__loadSet|0_1();
Program.<Main>g__loadMod|0_0(IVal.BasePath);
string title = irwfile.ReadString();
irwfile.ReadString();
irwfile.ReadInt();
irwfile.ReadInt();
IVal.GWidth = irwfile.ReadInt();
IVal.GHeight = irwfile.ReadInt();
NativeWindowSettings nativeWindowSettings2 = new NativeWindowSettings
{
Size = new Vector2i(IVal.GWidth, IVal.GHeight),
Title = title
};
using (Game game2 = new Game(new GameWindowSettings
{
RenderFrequency = 60.0,
UpdateFrequency = 60.0
}, nativeWindowSettings2))
{
IVal.mainGame = game2;
bool flag10 = File.Exists(IVal.BasePath + "icon.png");
if (flag10)
{
Bitmap bitmap2 = IBitmap.ABitmap("icon.png");
game2.Icon = new WindowIcon(new OpenTK.Windowing.Common.Input.Image[]
{
new OpenTK.Windowing.Common.Input.Image(bitmap2.Width, bitmap2.Height, IBitmap.bmpToRGBA(bitmap2))
});
bitmap2.Dispose();
}
IVal.mainGame.WindowBorder = WindowBorder.Resizable;
game2.VSync = VSyncMode.On;
game2.Run();
}
}
else
{
Console.WriteLine(IVal.Text["Msg2"]);
Process.GetCurrentProcess().Kill();
}
}
}
虽然没有看到iFActionScript的踪影,但是看到了一些关于资源的可疑痕迹。稍后资源解包再来看这个iFCon。
IVal.BasePath = AppDomain.CurrentDomain.BaseDirectory;
bool flag8 = File.Exists(IVal.BasePath + "iFCon");
if (flag8)
{
IVal.Pack = new DPack("iFCon");
}
入口点没有,那就再往里跟。OpenTK两层都是框架,直接不看。iFActionGame2.Game.OnUpdateFrame()作为进入iFActionScript前的最后一层调用,就从这里下手了。
// iFActionGame2.Game
// Token: 0x06000015 RID: 21
protected override void OnUpdateFrame(FrameEventArgs e)
{
if (IVal.BackgroundRun || this.isFocused)
{
if (this.scriptLoadOver && IVal.Assembly != null)
{
this.scriptLoadOver = false;
Type type = IVal.Assembly.GetType("iFActionScript.iFActionGameStart");
if (type != null)
{
type.GetMethod("GameRun").Invoke(null, null);
}
if (!ISteam.IsSteamDeck())
{
IVal.OldCulture = Game.getCultureType();
Game.changeLanguageMode("en-US");
}
IVal.Scene.init();
}
IAudio.updateSound();
if (IVal.Scene != null)
{
IVal.Scene.update();
}
IInput.update();
IInput.up = false;
}
}
看来找对了,但是只有一半。这里只有入口,但是没看到加载。看此处反射调用方式,大概可以确定游戏的核心逻辑是动态加载的。
对看起来就很可疑的scriptLoadOver分析一下发生修改的地方。除了本方法还找到了一个iFActionGame2.Game.loadScript(),看名字就知道找对了,跟进去。
一进去直接给博主看傻了。地方是找对了,可是现场拼字符串……不会是存的明文源代码吧?
// iFActionGame2.Game
// Token: 0x06000010 RID: 16
private void loadScript()
{
string text = "";
text += "using System;\r\n";
text += "using System.Collections.Generic;\r\n";
text += "using System.Text;\r\n";
text += "using System.Drawing;\r\n";
text += "using Newtonsoft.Json;\r\n";
text += "using iFActionGame2;\r\n";
text += "using iFActionGame2.js;\r\n";
text += "using System.IO;\r\n";
text += "using System.Runtime.Serialization.Formatters.Binary;\r\n";
text += "using System.Linq;\r\n";
text += "using System.Threading;\r\n";
text += "namespace iFActionScript {\r\n";
if (!IVal.DEBUG)
{
text += File.ReadAllText(IVal.BasePath + "script.ifsharp", Encoding.UTF8);
}
else
{
text += Encoding.UTF8.GetString(IVal.Pack.getFile("Graphics\\Scene\\item_5.png"));
}
text += "}";
IVal.Assembly = new ICompiler().Compile(text.ToString(), "iFActionGameScript", new Assembly[]
{
Assembly.Load(new AssemblyName("Newtonsoft.Json"))
});
this.scriptLoadOver = true;
}
非DEBUG模式下读取用的是IVal.Pack.getFile。如果不从外部读取,那么要不然通过反射更改,要不然把改完的封回资源包内。
考虑到该游戏近期的更新频率,把资源封回包里的话每次更新都要改。还是直接硬patch把源代码写出来,然后改成默认从外部加载比较好。
// iFActionGame2.Game
// Token: 0x06000010 RID: 16
private void loadScript()
{
string text = "";
text += "using System;\r\n";
text += "using System.Collections.Generic;\r\n";
text += "using System.Text;\r\n";
text += "using System.Drawing;\r\n";
text += "using Newtonsoft.Json;\r\n";
text += "using iFActionGame2;\r\n";
text += "using iFActionGame2.js;\r\n";
text += "using System.IO;\r\n";
text += "using System.Runtime.Serialization.Formatters.Binary;\r\n";
text += "using System.Linq;\r\n";
text += "using System.Threading;\r\n";
text += "namespace iFActionScript {\r\n";
if (!File.Exists("script.ifsharp"))
{
FileStream fileStream = new FileStream("script.ifsharp", FileMode.Create);
StreamWriter streamWriter = new StreamWriter(fileStream);
streamWriter.Write(Encoding.UTF8.GetString(IVal.Pack.getFile("Graphics\\Scene\\item_5.png")));
streamWriter.Flush();
streamWriter.Close();
fileStream.Close();
}
text += File.ReadAllText(IVal.BasePath + "script.ifsharp", Encoding.UTF8);
text += "}";
IVal.Assembly = new ICompiler().Compile(text.ToString(), "iFActionGameScript", new Assembly[]
{
Assembly.Load(new AssemblyName("Newtonsoft.Json"))
});
this.scriptLoadOver = true;
}
打开dump出来的文件一看,是全套带注释的源代码。你说制作组不重视安全吧,他把代码藏在一个看起来就像是图像的文件里。你说他重视安全吧,不混淆不编译……这算是一种新式JIT吗?
且不说别的,光里面保留的注释就能提供很多的信息,给我后续的魔改省下了不少读代码的时间。
顺带一提,又是一个在代码里硬编码NPC和事件数据的。游戏开发者似乎都挺爱这么干的?
原汁原味
首先为了方便调试,把游戏主程序的PE头改成WindowsCUI,可以直接看日志。
将近十万行单文件CSharp源码,快给我头都看大了。通过查阅源码,发现游戏包含一个可以对接核心脚本引擎LScript的调试窗口。将RV.DEBUG设为true,用F12即可呼出。
if(RV.DEBUG && IInput.isKeyDown(123)){
RF.ShowWin(new WConsole());
}
游戏内的界面确实有一些函数提示,但是远不够用。同时,物品id、NPCid的定义是未知的。
对LScript提取方法摘要如下,这就是这个控制台内全部可用的函数。大部分函数可以通过函数名和注释理解用途,剩下的需要看一看实现来确认。
很长的LScript摘要
public class LScript{
public static void test(){}
public static void testArgs(string s1,string s2){}
public static void setValue(string id,string value){}
public static void playCacheSE(string file,string volume){}
public static bool tesif(){}
public static void stopIM(){}
/// <summary>
/// 管理资源点
/// </summary>
public static void manageOre(){}
/// <summary>
/// 添加资源点
/// </summary>
/// <param name="oreType">资源类型 0木材 1铁矿 2铜矿 3石头 4煤 5稻田 6农场 7砂石</param>
public static void addSelfOre(string oreType){}
/// <summary>
/// 获得当前矿场等级
/// </summary>
/// <returns></returns>
public static int getOreStaff(){}
/// <summary>
/// 获取当前矿场升级钱数
/// </summary>
/// <returns></returns>
public static int getOreLevelUpMoney(){}
/// <summary>
/// 升级矿场
/// </summary>
public static void OreLevelUp(){}
/// <summary>
/// 矿场是否已被占领
/// </summary>
/// <returns></returns>
public static bool isSelfOre(){}
public static void showHelpMoney(){}
public static void hideHelpMoney(){}
/// <summary>
/// 设置当前时间(小时)
/// </summary>
/// <param name="hear">时</param>
public static void setTime(string hear){}
/// <summary>
/// 增加分钟(实际游戏时间)
/// </summary>
/// <param name="minutes">分钟</param>
public static void addTimeMinute(string minutes){}
/// <summary>
/// 重新执行工坊StoryDo
/// </summary>
public static void reStoryDo(){}
/// <summary>
/// 设置亮度
/// </summary>
/// <param name="opacity">不透明度</param>
public static void setLight(string opacity){}
public static void fadeLight(string endL,string frame){}
public static void toMapSelect(){}
/// <summary>
/// 移动地图(只能在地图场景SMain中使用)
/// </summary>
/// <param name="mapId">地图ID</param>
/// <param name="x">x(非格子)</param>
/// <param name="y">y(非格子)</param>
/// <param name="dir">朝向 0 下 1 左 2 右3 上</param>
public static void moveMap(string mapId,string x,string y,string dir){}
/// <summary>
/// 睡觉
/// </summary>
public static void sleep(){}
/// <summary>
/// 移动至工厂
/// </summary>
/// <param name="mapId">地图ID</param>
/// <param name="x">x坐标 非格子</param>
/// <param name="y">y坐标 非格子</param>
public static void toFactory(string mapId,string x,string y){}
/// <summary>
/// 从工坊移动至一般地图
/// </summary>
/// <param name="mapId">地图id</param>
/// <param name="x">x坐标 非格子</param>
/// <param name="y">y坐标 非格子</param>
/// <param name="dir">朝向</param>
public static void toNMap(string mapId,string x,string y,string dir){}
/// <summary>
/// 设置角色类型
/// </summary>
/// <param name="types">0 复原 1现代</param>
public static void setActorType(string types){}
/// <summary>
/// 开启NPC对话
/// </summary>
/// <param name="ids"></param>
public static void onNpcTalk(string ids){}
/// <summary>
/// 关闭NPC对话
/// </summary>
public static void offNpcTalk(){}
/// <summary>
/// 添加配方
/// </summary>
/// <param name="ids"></param>
public static void addFormula(string ids){}
/// <summary>
/// 添加建筑
/// </summary>
/// <param name="ids"></param>
public static void addBuild(string ids){}
/// <summary>
/// 添加一个驿站点
/// </summary>
/// <param name="ids"></param>
public static void addStage(string ids){}
public static void addHelp(string ids,string show){}
public static void addAllHelp(){}
public static void addExp(string type,string exp){}
public static void addGfExp(string type,string exp){}
public static void setTalentLevel(string type,string leve){}
public static void reTalent(){}
public static void makeMana(){}
/// <summary>
/// 将大地图不可建造地图转为可建造
/// </summary>
/// <param name="id"></param>
public static void openBigMapBuild(string id){}
/// <summary>
/// 显示需要点击的Tips
/// </summary>
/// <param name="msg">Tips内显示的文字</param>
public static void showTipsClick(string msg){}
/// <summary>
/// 显示Tips
/// </summary>
/// <param name="msg">Tips内的文字</param>
/// <param name="time">显示时间,如果显示时间为0则不消失</param>
public static void showTips(string msg,string time){}
public static void showLeftTips(string msg,string type){}
public static void showLeftTips2(string msg,string pic){}
public static void showBigTips(string title,string icon,string msg){}
public static void showBigTips2(string title,string icon,string msg){}
public static void showTimeTips(string msg,string time){}
public static void showFatherLetter(){}
public static void showBigTipsForNode(string ids){}
public static void HelpVisible(string v){}
/// <summary>
/// 使摄像机注释某个点
/// </summary>
/// <param name="xs">注视的x坐标</param>
/// <param name="ys">注视的y坐标</param>
/// <param name="fs">所需帧数</param>
public static void moveCamera(string xs,string ys,string fs){}
/// <summary>
/// 将摄像机移动回主角的位置
/// </summary>
/// <param name="fs">所需帧数</param>
public static void moveReCamera(string fs){}
/// <summary>
/// 设置主角的位置
/// </summary>
/// <param name="x">主角x坐标</param>
/// <param name="y">主角y坐标</param>
public static void setActorXY(string x, string y,string dir){}
/// <summary>
/// 设置角色可见
/// </summary>
/// <param name="v"></param>
public static void setActorVisible(string v){}
/// <summary>
/// 刷新地图所有触发器开启条件
/// </summary>
public static void updateAllTrigger(){}
/// <summary>
/// 切换时间线
/// </summary>
/// <param name="fo">0为古代 1为现代</param>
public static void changeTimes(string fo){}
/// <summary>
/// 自然层内容闪烁
/// </summary>
/// <param name="bx">查询格子X坐标</param>
/// <param name="by">查询格子y坐标</param>
/// <param name="rgb">闪烁RGB R|G|B</param>
/// <param name="f1">淡入时间</param>
/// <param name="f2">淡出时间</param>
public static void flashMapAuto(string bx,string by,string rgb,string f1,string f2){}
/// <summary>
/// 触发器闪烁
/// </summary>
/// <param name="id">触发器ID</param>
/// <param name="rgb">闪烁RGB R|G|B</param>
/// <param name="f1">进入时间</param>
/// <param name="f2">淡出时间</param>
public static void flashTrigger(string id,string rgb,string f1,string f2){}
public static void canvasFlashEx(string rgb,string f1,string f2){}
/// <summary>
/// 显示迷你气泡对话框
/// </summary>
/// <param name="key">对应Key键</param>
/// <param name="msg">对话内容</param>
/// <param name="id">显示触发器ID,-10为角色,-20为当前触发器</param>
public static void talkMin(string key,string msg,string id){}
/// <summary>
/// 关闭指定迷你气泡对话框
/// </summary>
/// <param name="key">对应key键</param>
public static void removeTalk(string key){}
/// <summary>
/// 关闭全部迷你对话框
/// </summary>
public static void removeAllTalk(){}
/// <summary>
/// 开始剧情模式
/// </summary>
public static void startStory(){}
/// <summary>
/// 结束剧情模式
/// </summary>
public static void endStory(){}
/// <summary>
/// 将某个故事所用的通用触发器加入故事进行完毕队列
///<param name="id">触发器id</param>
/// </summary>
public static void addStory(string id){}
#region 角色动作相关
/// <summary>
/// 人物震动
/// </summary>
/// <param name="id">人物id -10 为角色本身</param>
/// <param name="pow">震动力度</param>
/// <param name="speed">震动速度</param>
/// <param name="time">震动时间</param>
public static void shakeActor(string id,string pow,string speed,string time){}
/// <summary>
/// NPC震动
/// </summary>
/// <param name="id"></param>
/// <param name="pow"></param>
/// <param name="speed"></param>
/// <param name="time"></param>
public static void shakeNpc(string id,string pow,string speed,string time){}
/// <summary>
/// 角色坐下
/// </summary>
/// <param name="tid">作为校准坐标的触发器ID</param>
/// <param name="actorID">角色ID,角色本身ID为-10</param>
/// <param name="dir">0 下 1左 2右 3下</param>
public static void sitDownActor(string tid,string actorID,string dir){}
// /// <summary>
// /// 坐到指定坐标
// /// </summary>
// /// <param name="tid"></param>
// /// <param name="actorID"></param>
// /// <param name="dir"></param>
// public static void sitDownActorXY(string x,string y,string actorID,string dir){}
/// <summary>
/// 角色坐下
/// </summary>
/// <param name="x">目标x坐标</param>
/// <param name="y">目标y坐标</param>
/// <param name="actorID">角色ID,角色本身ID为-10</param>
/// <param name="dir">0下 1左 2右 3下</param>
public static void sitDownActorPoint(string x,string y ,string actorID,string dir){}
public static void sitDownActorPoint2(string x,string y ,string actorID,string dir,string actionIndex = "2"){}
public static void setShadowVisible(string nid,string v){}
#endregion
/// <summary>
/// 增加NPC好感度
/// </summary>
/// <param name="sid">npc id</param>
/// <param name="snum">增减值</param>
public static void addNpcFavor(string sid,string snum){}
/// <summary>
/// 增加NPC好感度-变量版本
/// </summary>
/// <param name="sid">npc id所保存的变量id</param>
/// <param name="snum">增减值</param>
public static void addNpcFavorValue(string vid,string snum){}
/// <summary>
/// 判断NPC好感度是否达到某一个级别
/// </summary>
/// <param name="sid">npc id</param>
/// <param name="level">需要判断的等级</param>
public static bool checkNpcFavor(string sid,string level){}
public static int addTask(string ids){}
public static void subTask(string ids){}
public static void subTaskMust(string ids,string valueID){}
public static void onSubTaskMustEnd(int type){}
//是否有某个任务
public static bool lssHaveTask(string id){}
public static void addMoney(string num){}
public static void addItem(string ids,string nums){}
//减少某物品
public static void subItem(string ids,string nums){}
//减少某物品(变量数)
public static void subItemValue(string ids,string vid){}
public static void completeTask(string ids,string isCheck){}
public static bool getTaskStatus(){}
public static bool isCompletedTask(string ids){}
public static void showShop(string ids){}
public static void showShop2(string ids,string type){}
public static void showNode(string type){}
public static void showNodeEnd(int end,int tp){}
public static void showMinEnd(int end,GDMainNode node){}
public static void showScience(){}
public static void showStage(string ids){}
public static void showWell(string ids){}
public static void showTianGong(){}
public static void showTalent(string types){}
public static void showKitchen(){}
/// <summary>
/// 将天工阁的已典藏数量赋值给valueID
/// </summary>
/// <param name="valueId"></param>
public static void getTianGongCount(string valueId){}
/// <summary>
/// 判定NPC是否在这个位置
/// </summary>
/// <param name="id">npc id</param>
/// <param name="mapId">地图id</param>
/// <param name="x">地图x坐标 格子</param>
/// <param name="y">地图y坐标 格子</param>
/// <param name="dir">朝向 下左右上</param>
/// <returns></returns>
public static bool NPCIsInlocation(string id,string mapId,string x,string y,string dir){}
/// <summary>
/// 检测指定位置是否有NPC
/// </summary>
/// <param name="x">像素 地图X</param>
/// <param name="y">像素 地图y</param>
/// <param name="w">格子 宽度</param>
/// <param name="h">格子 高度</param>
/// <returns></returns>
public static bool locationHaveNPC(string x,string y,string w,string h){}
/// <summary>
/// 首次打开通讯器
/// </summary>
public static void templeOpen(){}
/// <summary>
/// 是否用工具
/// </summary>
/// <returns></returns>
public static bool haveTools(){}
/// <summary>
/// 通讯器是否打开
/// </summary>
/// <returns></returns>
public static bool templeIsOpen(){}
public static bool temleIsComplete(){}
/// <summary>
/// 背包强化次数是否大于某个值
/// </summary>
/// <param name="level">是否大于的等级</param>
/// <returns></returns>
public static bool bagLevelOn(string level){}
public static void bagLevelUp(){}
//增加背包格子数,8格每次
public static void addBagNum(){}
public static void toolLevelAnim(){}
/// <summary>
/// 工具等级是否大于某个值
/// </summary>
/// <param name="level">工具等级</param>
/// <returns></returns>
public static bool toolLevelOn(string level){}
/// <summary>
/// 工具升级
/// </summary>
public static void toolLevelUp(){}
/// <summary>
/// 房屋等级是否大于某个值
/// </summary>
/// <param name="level">要判断的值</param>
/// <returns></returns>
public static bool homeLevelOn(string level){}
/// <summary>
/// 房屋升级
/// </summary>
public static void homeLevelUp(){}
public static bool homeLeveling(){}
public static void changeTalkSound(string file){}
public static void sendLetter(string index){}
public static void showLetter(){}
/// <summary>
/// 判定天机庙某级是否修复完毕
/// </summary>
/// <param name="level">T级别</param>
/// <returns></returns>
public static bool isCheckNodeAll(string level){}
/// <summary>
/// 判定天机庙某个ID是否修复完成
/// </summary>
/// <param name="id"></param>
/// <returns></returns>
public static bool isCheckNodeMin(string id){}
#region 大地图相关
/// <summary>
/// 前往大地图
/// </summary>
public static void toBigMap(){}
/// <summary>
/// 前往大地图
/// </summary>
/// <param name="x">格子坐标</param>
/// <param name="y">格子坐标</param>
/// <param name="eventId"></param>
public static void toBigMap2(string x,string y,string eventId){}
/// <summary>
/// 从战场回家,只能用于战场返回
/// </summary>
public static void goHome(){}
/// <summary>
/// 设置当前战斗ID
/// </summary>
/// <param name="id"></param>
public static void setNowBattle(string id){}
public static bool isBattle(){}
/// <summary>
/// 大地图阴影开启
/// </summary>
/// <param name="nx">原点x坐标</param>
/// <param name="ny">原点y坐标</param>
/// <param name="level">开启范围(递归)</param>
/// <param name="value">0 黑的 1灰的 2亮的 5不熄灭的</param>
public static void openBlock(string nx,string ny,string level,string value){}
/// <summary>
/// 设置大地图位置
/// </summary>
/// <param name="x">x位置</param>
/// <param name="y">y位置</param>
public static void setBigMapXY(string x,string y){}
/// <summary>
/// 设置大地图镜头移动
/// </summary>
/// <param name="ex">目标x</param>
/// <param name="ey">目标y</param>
/// <param name="time">移动时间(帧)</param>
public static void setBigMapView(string ex,string ey,string time){}
/// <summary>
/// 设置镜头归位
/// </summary>
/// <param name="time">时间</param>
public static void setBigMapReView(string time){}
/// <summary>
/// 锁定为角色镜头
/// </summary>
public static void setBigMapLockView(){}
/// <summary>
/// 大地图建筑闪烁
/// </summary>
/// <param name="id">对应ID</param>
/// <param name="f1">闪烁进入时间</param>
/// <param name="f2">闪烁淡出时间</param>
public static void bigMapBildFlash(string id,string f1,string f2){}
/// <summary>
/// 让大地图某个建筑完成建造
/// </summary>
/// <param name="id"></param>
public static void bigMapBuildOpen(string id){}
public static void backToMap(string id,string x,string y,string dir){}
public static void addActorMaXHp(string add){}
/// <summary>
/// 增加字符串(陆蝉衣的山洞)
/// </summary>
/// <param name="str">添加的字符串</param>
public static void addStrForValue(string str){}
/// <summary>
/// 判断是否每个条件都触发(陆蝉衣的山洞条件)
/// </summary>
/// <param name="str">待检查的字符串</param>
public static bool isAllValueFinish(){}
/// <summary>
/// 触发器查找切换为2楼模式
/// </summary>
public static void end2ndModel(){}
/// <summary>
/// 设置次日改变变量
/// </summary>
/// <param name="key"></param>
/// <param name="value"></param>
public static void setNextValue(string key,string value){}
/// <summary>
/// 采集资源
/// </summary>
/// <param name="index">采集的ID号</param>
public static void collectRes(string index){}
/// <summary>
/// 习武
/// </summary>
/// <param name="index">要练习的武功种类</param>
public static void studyGongfu(string index){}
/// <summary>
/// 缩放摄像机
/// </summary>
/// <param name="x">横向缩放比例</param>
/// <param name="y">纵向缩放比例</param>
public static void zoomCamera(string zx,string zy,string time){}
/// <summary>
/// 显示悬赏任务窗体
/// </summary>
/// <param name="type">0 制作悬赏__1 战斗悬赏</param>
public static void showTaskList(string type){}
public static void reZJGTask(){}
public static void showStoryBar(){}
public static void hideStoryBar(){}
/// <summary>
/// 获得选中位置装备的id到指定变量,如果未装备,则值为-1
/// </summary>
/// <param name="pos"></param>0头 1武器 2衣服 3饰品1 4鞋 5饰品2
/// <param name="valueId"></param>
public static void getEquip(string pos,string valueId){}
/// <summary>
/// 获得选中位置装备的武学搭配指定变量,如果未装备则值为-1
/// </summary>
/// <param name="pos">0外功 1内功 2轻功 3硬功</param>
/// <param name="valueId"></param>
public static void getGongFu(string pos,string valueId){}
/// <summary>
/// 获得某项武学能力数值到指定变量
/// </summary>
/// <param name="pos">0外功 1内功 2轻功 3硬功</param>
/// <param name="valueId"></param>
public static void getGongFuValue(string pos,string valueId){}
/// <summary>
/// 获得功法
/// </summary>
/// <param name="gongfuId">功法ID</param>
public static void addGongfu(string gongfuId){}
/// <summary>
/// 藏剑阁悬赏榜等级是否小于目标等级
/// </summary>
/// <param name="level">目标等级</param>
/// <returns></returns>
public static bool cjLevel(string level){}
/// <summary>
/// 获得某个物品身上所拥有个数到某变量
/// </summary>
/// <param name="id">物品id</param>
/// <param name="value">变量</param>
/// <returns></returns>
public static void getItemNum(string id , string valueId){}
/// <summary>
/// 将今日的幸运程度,赋值给指定变量
/// </summary>
/// <param name="ids"></param>
/// <returns></returns>
public static void getLuckType(string ids){}
public static void randNPCForGift(){}
public static bool canGetGift(){}
public static void addRemberGift(string ids,string itemIds){}
public static void openChest(string ids){}
public static bool isOpenChest(string ids){}
public static bool isBattleOver(){}
public static bool haveEnemys(){}
public static bool isAllEnemyDie(){}
public static bool canPlayMusic(){}
public static void reMusic(){}
/// <summary>
/// 所以触发器或角色的动作帧
/// </summary>
/// <param name="tid">触发器ID,为-10时选择角色</param>
/// <param name="index"></param>
public static void lockAction(string tid, string index){}
/// <summary>
/// 解除动作锁定
/// </summary>
/// <param name="tid">触发器ID,为-10时选择角色</param>
public static void unlockAction(string tid){}
public static void mustOverBatlle(){}
//花朝节投壶游戏结束后流程
public static void touHuCompetition(){}
public static void ZhongQiuItem(){}
/// <summary>
/// 投壶小游戏设定
/// </summary>
/// <param name="num">有几次机会</param>
/// <param name="isTest">是否是练习模式</param>
/// <param name="haveWind">是否有风</param>
/// <param name="autoStory">自动进入剧情模式</param>
/// <param name="scoreValueId">分数绑定的变量ID 不需要时填0</param>
/// <param name="storyValueId">剧情变量绑定的ID 不需要时填0</param>
/// <param name="storyValueNum">剧情变量要付的值</param>
public static void touHuSet(string num,string isTest,string haveWind,string autoStory,string scoreValueId,string storyValueId,string storyValueNum){}
/// <summary>
/// 划龙舟小游戏
/// </summary>
/// <param name="level">游戏难度 0,1,2,5(练习)</param>
/// <param name="autoStory">自动进入剧情模式</param>
/// <param name="scoreValueId">名次绑定变量ID 不需要时填0</param>
/// <param name="storyValueId">剧情变量绑定的ID 不需要时填0</param>
/// <param name="storyValueNum">剧情变量要付的值</param>
/// <param name="partnerID">伴侣ID</param>
public static void longZhouSet(string level,string autoStory,string scoreValueId,string storyValueId,string storyValueNum,string partnerID){}
public static void baHeSet(string type,string face,string scoreValueId){}
public static void showSubtitle(string type){}
/// <summary>
/// 判断玩家是否结婚
/// </summary>
/// <returns></returns>
public static bool isMarry(){}
/// <summary>
/// 与指定NPC结婚
/// </summary>
/// <param name="id">npcID</param>
public static void MarryForNpc(string id){}
public static void DivorceForNpc(){}
/// <summary>
/// 开启结婚技能
/// </summary>
/// <param name="value">0-没有任何增益 10-加钱增益 20-耐力恢复 30-练功经验减少 40-制造数量增加 50-加原材料 60-增加悟性</param>
public static void MarrySkill(string value){}
/// <summary>
/// 设置角色着装
/// </summary>
/// <param name="dressID">着装id</param>
public static void SetDress(string dressID){}
/// <summary>
/// 复原着装
/// </summary>
/// <param name="type">1-服装 2-裤子 3-发型</param>
public static void ReDress(string type){}
/// <summary>
/// 设置着装部位是否可见
/// </summary>
/// <param name="type">1-服装 2-裤子 3-发型</param>
/// <param name="visible">是否可见</param>
public static void SetDressVisible(string type,string visible){}
public static void NpcInit(){}
public static void openAchievement(string ids){}
public static void sendAllLetter(){}
public static void gamePin(string isTest,string speed){}
public static void npcFollow(string ids){}
public static void checkNPCGift(string ids){}
#endregion
#region 门的开关营业判定
//江城行会
public static bool jjchDoor(){}
//铁匠铺
public static bool tjpDoor(){}
//济安堂
public static bool jatDoor(){}
//裁缝铺
public static bool cfpDoor(){}
//裁缝铺二楼
public static bool cfp2Door(){}
//江月楼
public static bool jylDoor(){}
//映雪书馆
public static bool yxsgDoor(){}
//天工阁
public static bool tggDoor(){}
//通玄棋社
public static bool txqsDoor(){}
//徐府正房
public static bool xfzfDoor(){}
//徐长庭房
public static bool xctDoor(){}
//徐长空房
public static bool xckDoor(){}
//徐府仓库
public static bool xfckDoor(){}
//鲁家
public static bool ljDoor(){}
//孙道一家门
public static bool sdyjmDoor(){}
//孙道一家窗
//赫连常山家
public static bool hlcsjDoor(){}
//徐长空租住地(好感大于6开启)
public static bool xckzzdDoor(){}
//空铺子
//萧府
//正殿-左偏殿-右偏殿
public static bool xfzdDoor(){}
//寒山房
public static bool hsfDoor(){}
//柴府
//柴府正房与左右偏房
public static bool cfzfDoor(){}
//柴府祠堂
public static bool cfctDoor(){}
//纵剑阁
//正房
public static bool zjgzfDoor(){}
//藏剑楼
public static bool zjgcjlDoor(){}
//赫连常山小摊
public static bool hlcsStall(){}
//野利旺珠的织布机
public static bool yslzZbj(){}
//斧镐任务完成
public static void getLssAxeTask(){}
//测试用函数升级斧镐
public static void addToolLevelTest(string le){}
public static bool isDEBUG(){}
public static void rssyr(){}
public static void fullNpcFavor(){}
public static void setNpcFavor(string id,string f){}
//获得人物性别
public static bool isMan(){}
/// <summary>
/// 判断是否认识某NPC
/// </summary>
/// <param name="id">npc ID</param>
public static bool checkIsRenshi(string id){}
/// <summary>
/// 强制认识某NPC
/// </summary>
/// <param name="id">npc ID</param>
public static void mustRenshi(string id){}
//徐长空支线任务提交时间判定
public static bool isTimeRightXU(){}
//判断金钱是否足够支付
public static bool haveMoneyEnough(string nmoney){}
//判断时间是否在某点某分前或某点某分后
/// </summary>
/// <param name="id">type</param> 0判断在某点某分之前,1判断在某点某分之后
public static bool isBeforeTime(string hour,string min,string type){}
//获得当前学习的菜谱价格
public static void getMenuPrice(){}
//学习菜谱
public static void studyMenu(){}
//是否是某一月某个季节
//1春 2夏 3秋 4冬
public static bool isSpecialSeason(string season){}
//是否是某一日
//day 日子
public static bool isSpecialDay(string day){}
//显示与隐藏层级最高的黑屏
/// </summary>
/// <param name="type">type</param> 1 显示 0 消失
/// <param name="time">time</param> 帧数
public static void mustMask(string type , string time){}
//模糊地图
public static void blurMap(string id, string value, string frame){}
#endregion
}
但是,注释内并没有给出ID定义,那么就向LScript加入一个函数用于生成ID文档。
public static void generateDebugInfo(){
FileStream fs = new FileStream("id_table.txt",FileMode.Create);
StreamWriter sw = new StreamWriter(fs);
sw.WriteLine("Version:"+RV.ver);
foreach(var kv in RV.SelfSet.setItem){
sw.WriteLine("道具 ID:"+kv.Value.id+" 名称:"+kv.Value.name);
}
foreach(var kv in RV.SelfSet.setFormula){
sw.WriteLine("配方 ID:"+kv.Value.id+" 名称:"+kv.Value.name);
}
foreach(var kv in RV.SelfSet.setBuild){
sw.WriteLine("建筑 ID:"+kv.Value.id+" 名称:"+kv.Value.name);
}
foreach(var kv in RV.SelfSet.setGongfu){
sw.WriteLine("功夫 ID:"+kv.Value.id+" 名称:"+kv.Value.name);
}
for(int i = 0;i<RV.NPCList.Count;i++){
sw.WriteLine("NPC ID:"+RV.NPCList[i].id+" 名称:"+RV.NPCList[i].name);
}
sw.Flush();
sw.Close();
fs.Close();
}
从控制台手动触发一次,即可得到包含全部ID的参考文件。
很长的id_table.txt
Version:0.7.2.0408.1
道具 ID:1 名称:原木
道具 ID:2 名称:木材
道具 ID:4 名称:粮食
道具 ID:5 名称:石头
道具 ID:6 名称:沙子
道具 ID:10 名称:石材
道具 ID:11 名称:煤矿石
道具 ID:12 名称:铜矿石
道具 ID:13 名称:铁矿石
道具 ID:14 名称:木纤维
道具 ID:15 名称:线
道具 ID:16 名称:布
道具 ID:17 名称:玻璃
道具 ID:18 名称:提纯硅
道具 ID:50 名称:木板
道具 ID:51 名称:木棒
道具 ID:52 名称:加强木板
道具 ID:53 名称:木质框架
道具 ID:54 名称:铁质核心
道具 ID:55 名称:石砖
道具 ID:56 名称:强力石砖
道具 ID:57 名称:初级工具
道具 ID:58 名称:纸
道具 ID:59 名称:油纸
道具 ID:60 名称:铁锭
道具 ID:61 名称:铁板
道具 ID:62 名称:铁棒
道具 ID:63 名称:螺丝
道具 ID:64 名称:铁质框架
道具 ID:65 名称:加固铁板
道具 ID:66 名称:铁质工具
道具 ID:67 名称:钢制核心
道具 ID:68 名称:铜锭
道具 ID:69 名称:铜板
道具 ID:70 名称:铜线
道具 ID:71 名称:C级能量块
道具 ID:72 名称:B级能量块
道具 ID:73 名称:A级能量块
道具 ID:74 名称:钢锭
道具 ID:75 名称:钢管
道具 ID:76 名称:钢板
道具 ID:77 名称:工业结构体
道具 ID:78 名称:钢筋混凝土
道具 ID:79 名称:电动机
道具 ID:81 名称:机械核心
道具 ID:82 名称:酒
道具 ID:83 名称:油
道具 ID:84 名称:酒精
道具 ID:90 名称:电枢
道具 ID:91 名称:集电环
道具 ID:92 名称:芯片
道具 ID:93 名称:控制器
道具 ID:94 名称:高级工具
道具 ID:95 名称:塑料
道具 ID:96 名称:橡胶
道具 ID:97 名称:电路板
道具 ID:98 名称:大型计算机
道具 ID:120 名称:燃料碳棒
道具 ID:121 名称:燃油能量块
道具 ID:122 名称:高性能地板
道具 ID:200 名称:桶装石油
道具 ID:700 名称:红色晶能
道具 ID:701 名称:绿色晶能
道具 ID:702 名称:蓝色晶能
道具 ID:703 名称:紫色晶能
道具 ID:704 名称:黄色晶能
道具 ID:705 名称:黑色晶能
道具 ID:800 名称:抗生素
道具 ID:900 名称:蛛丝
道具 ID:901 名称:蜘蛛腿
道具 ID:902 名称:蝙蝠牙
道具 ID:903 名称:蝙蝠翅膀
道具 ID:904 名称:狼牙
道具 ID:905 名称:兽皮
道具 ID:906 名称:野猪牙
道具 ID:907 名称:坚硬甲壳
道具 ID:908 名称:尖锐毒针
道具 ID:909 名称:锋利蝎钳
道具 ID:910 名称:幻棱角
道具 ID:911 名称:樊莽皮革
道具 ID:912 名称:幻源
道具 ID:950 名称:精致皮革
道具 ID:951 名称:精致丝线
道具 ID:980 名称:天机石
道具 ID:981 名称:防御磁盾
道具 ID:982 名称:次元靴
道具 ID:983 名称:天工戒指
道具 ID:1000 名称:逢春叶
道具 ID:1001 名称:翠羽草
道具 ID:1002 名称:野琴花
道具 ID:1003 名称:茴参
道具 ID:1004 名称:提灯胡兰
道具 ID:1005 名称:赤菘子
道具 ID:2000 名称:禽蛋
道具 ID:2001 名称:肉
道具 ID:2005 名称:酱油
道具 ID:2006 名称:糖
道具 ID:2010 名称:青菜
道具 ID:2012 名称:水果
道具 ID:2015 名称:河鲜
道具 ID:2016 名称:鱼
道具 ID:2017 名称:垃圾
道具 ID:3000 名称:豆腐
道具 ID:3001 名称:黄金鸡
道具 ID:3003 名称:青团
道具 ID:3004 名称:水煮蛋
道具 ID:3005 名称:炙炊饼
道具 ID:3006 名称:烤鱼
道具 ID:3007 名称:素包子
道具 ID:3009 名称:玉井鱼饭
道具 ID:3010 名称:青菜豆腐煲
道具 ID:3016 名称:醉蟹
道具 ID:3017 名称:糖藕
道具 ID:3018 名称:野琴酒
道具 ID:3019 名称:粽子
道具 ID:3021 名称:东坡肉
道具 ID:3023 名称:祝东风
道具 ID:3024 名称:金汤花胶鸡
道具 ID:3027 名称:雪霞羹
道具 ID:3030 名称:肉夹馍
道具 ID:3031 名称:水龙白鱼
道具 ID:3035 名称:荔枝虾球
道具 ID:3038 名称:蟹黄豆腐
道具 ID:3039 名称:鲜花饼
道具 ID:3041 名称:山煮羊
道具 ID:3043 名称:月饼
道具 ID:3048 名称:五珍烩
道具 ID:4000 名称:止血散
道具 ID:4005 名称:金疮药
道具 ID:4010 名称:补脉散
道具 ID:4015 名称:百花生脉灵
道具 ID:4020 名称:赤龙血茶
道具 ID:4050 名称:茴参丸
道具 ID:4055 名称:调元膏
道具 ID:4060 名称:通络丸
道具 ID:4065 名称:敛气灵膏
道具 ID:4070 名称:凝神玉露
道具 ID:4090 名称:回心丸
道具 ID:4095 名称:玄玉系魂丹
道具 ID:4110 名称:外功经验丹
道具 ID:4120 名称:内功经验丹
道具 ID:4130 名称:轻功经验丹
道具 ID:4140 名称:硬功经验丹
道具 ID:4210 名称:强功火蝉
道具 ID:4220 名称:逐桑灵芝
道具 ID:4230 名称:轻身琥珀
道具 ID:4240 名称:灵龟硬甲
道具 ID:4310 名称:赤血珊瑚
道具 ID:4320 名称:元觉舍利
道具 ID:6001 名称:帐篷
道具 ID:6100 名称:古玩珍宝
道具 ID:6101 名称:不谢花
道具 ID:6200 名称:定情玉佩
道具 ID:6201 名称:同心锁
道具 ID:7002 名称:香皂
道具 ID:7008 名称:铜镜
道具 ID:7012 名称:油纸伞
道具 ID:7016 名称:医药箱
道具 ID:7017 名称:手表
道具 ID:7024 名称:烤箱
道具 ID:9000 名称:铁剑
道具 ID:9001 名称:裁云剑
道具 ID:9002 名称:行猎轻剑
道具 ID:9003 名称:冰花小剑
道具 ID:9004 名称:夜雨剑
道具 ID:9005 名称:大荒飞雪
道具 ID:9006 名称:斩川大剑
道具 ID:9007 名称:遁空剑
道具 ID:9008 名称:携玉龙
道具 ID:9009 名称:逐桑古剑
道具 ID:10000 名称:麻布方巾
道具 ID:10001 名称:锦缎帽
道具 ID:10002 名称:戴月冠
道具 ID:10003 名称:飞鹰帽
道具 ID:10004 名称:长夜笠
道具 ID:10008 名称:九霄御龙冠
道具 ID:11000 名称:布衫
道具 ID:11002 名称:魅狐裘
道具 ID:11003 名称:镂月服
道具 ID:11004 名称:御霜披风
道具 ID:11007 名称:百战金甲
道具 ID:11008 名称:天机仙袍
道具 ID:12000 名称:布鞋
道具 ID:12002 名称:逐鹿鞋
道具 ID:12004 名称:狐行履
道具 ID:12005 名称:踏月靴
道具 ID:12006 名称:极武战靴
道具 ID:12008 名称:灵鹫踏
道具 ID:13000 名称:金丝荷包
道具 ID:13001 名称:银扳指
道具 ID:13003 名称:金玉扳指
道具 ID:13006 名称:塞上羊脂
道具 ID:13007 名称:添香扣
道具 ID:13008 名称:净念佛珠
道具 ID:13009 名称:灵纹双蝠扣
道具 ID:13012 名称:惊画扇
道具 ID:13013 名称:碎梦扳指
道具 ID:13015 名称:梦枕石
道具 ID:15000 名称:止血散
道具 ID:15001 名称:金疮药
道具 ID:15002 名称:赤龙血茶
道具 ID:15003 名称:茴参丸
道具 ID:15004 名称:调元膏
道具 ID:15005 名称:通络丸
道具 ID:15006 名称:回心丸
道具 ID:15007 名称:玄玉系魂丹
道具 ID:15100 名称:裁云剑
道具 ID:15101 名称:冰花小剑
道具 ID:15102 名称:斩川大剑
道具 ID:15103 名称:携玉龙
道具 ID:15200 名称:飞鹰帽
道具 ID:15201 名称:长夜笠
道具 ID:15202 名称:御霜披风
道具 ID:15203 名称:百战金甲
道具 ID:15204 名称:狐行履
道具 ID:15206 名称:灵纹双蝠扣
道具 ID:15207 名称:碎梦扳指
道具 ID:15300 名称:豆腐菜谱
道具 ID:15301 名称:青团菜谱
道具 ID:15302 名称:粽子菜谱
道具 ID:15303 名称:月饼菜谱
道具 ID:15304 名称:五珍烩菜谱
道具 ID:15305 名称:荔枝虾球
道具 ID:15306 名称:祝东风酒方
道具 ID:15307 名称:素包子菜谱
道具 ID:16000 名称:六尘印
道具 ID:16010 名称:困龙真气
道具 ID:16020 名称:幻花身法
道具 ID:16030 名称:寒山剑诀
道具 ID:16040 名称:鹧鸪纵
道具 ID:16050 名称:止观心经
道具 ID:17000 名称:钱袋子
道具 ID:17001 名称:红包
道具 ID:20001 名称:极武宗信物
道具 ID:20002 名称:天机秘钥碎片
道具 ID:20003 名称:天机秘钥
道具 ID:21000 名称:江城地图
道具 ID:99999 名称:无
配方 ID:1 名称:木材生产
配方 ID:2 名称:木板生产
配方 ID:3 名称:加强木板生产
配方 ID:4 名称:木质框架生产
配方 ID:5 名称:铁质核心生产
配方 ID:6 名称:木棒生产
配方 ID:9 名称:石材生产
配方 ID:10 名称:铁锭生产
配方 ID:11 名称:铜锭生产
配方 ID:12 名称:石砖生产
配方 ID:20 名称:铁板生产
配方 ID:21 名称:铁棒生产
配方 ID:22 名称:螺丝生产
配方 ID:23 名称:加固铁板生产
配方 ID:24 名称:铁质框架生产
配方 ID:25 名称:铁质工具生产
配方 ID:26 名称:钢质核心生产
配方 ID:30 名称:铜板生产
配方 ID:31 名称:铜线生产
配方 ID:40 名称:硅提纯
配方 ID:41 名称:玻璃生产
配方 ID:50 名称:木纤维生产
配方 ID:51 名称:线生产
配方 ID:52 名称:布生产
配方 ID:53 名称:纸生产
配方 ID:54 名称:油纸生产
配方 ID:60 名称:初级工具生产
配方 ID:70 名称:粮食酒酿造
配方 ID:71 名称:粮食榨油
配方 ID:72 名称:酒精提纯
配方 ID:80 名称:钢熔铸
配方 ID:81 名称:钢管生产
配方 ID:82 名称:钢板生产
配方 ID:83 名称:钢筋混凝土生产
配方 ID:90 名称:电枢生产
配方 ID:91 名称:集电环生产
配方 ID:95 名称:燃料碳棒生产
配方 ID:96 名称:强力石砖
配方 ID:97 名称:精致皮革生产
配方 ID:98 名称:精致丝线生产
配方 ID:100 名称:红色晶能生产
配方 ID:101 名称:绿色晶能生产
配方 ID:102 名称:蓝色晶能生产
配方 ID:103 名称:紫色晶能生产
配方 ID:104 名称:黄色晶能生产
配方 ID:105 名称:黑色晶能生产_close
配方 ID:110 名称:芯片生产
配方 ID:111 名称:控制器生产
配方 ID:112 名称:电动机生产
配方 ID:113 名称:高级工具生产
配方 ID:120 名称:帐篷制造
配方 ID:121 名称:天机石制造
配方 ID:122 名称:防御磁盾
配方 ID:150 名称:C级能量块生产
配方 ID:151 名称:B级能量块生产
配方 ID:160 名称:工业结构体生产
配方 ID:161 名称:塑料生产
配方 ID:162 名称:橡胶生产
配方 ID:163 名称:电路板生产
配方 ID:164 名称:大型计算机生产
配方 ID:190 名称:燃油能量块生产
配方 ID:191 名称:高性能地板生产
配方 ID:200 名称:抗生素生产
配方 ID:502 名称:香皂生产
配方 ID:508 名称:铜镜生产
配方 ID:512 名称:油纸伞生产
配方 ID:516 名称:医药箱生产
配方 ID:517 名称:手表生产
配方 ID:524 名称:烤箱生产
配方 ID:4000 名称:止血散制作
配方 ID:4005 名称:金疮药制作
配方 ID:4010 名称:补脉散制作
配方 ID:4015 名称:百花生脉灵制作
配方 ID:4020 名称:赤龙血茶制作
配方 ID:4025 名称:茴参丸制作
配方 ID:4030 名称:调元膏制作
配方 ID:4035 名称:通络丸制作
配方 ID:4040 名称:敛气灵膏制作
配方 ID:4045 名称:凝神玉露制作
配方 ID:4050 名称:回心丸制作
配方 ID:4055 名称:玄玉系魂丹制作
配方 ID:4100 名称:裁云剑铸造
配方 ID:4101 名称:冰花小剑铸造
配方 ID:4102 名称:斩川大剑铸造
配方 ID:4103 名称:携玉龙铸造
配方 ID:4200 名称:飞鹰帽制作
配方 ID:4201 名称:长夜笠制作
配方 ID:4202 名称:御霜披风制作
配方 ID:4203 名称:百战金甲制作
配方 ID:4204 名称:狐行履制作
配方 ID:4205 名称:极武战靴制作
配方 ID:4206 名称:灵纹双蝠扣制作
配方 ID:4207 名称:碎梦扳指制作
配方 ID:4208 名称:梦枕石制作
配方 ID:4308 名称:豆腐制作
配方 ID:4315 名称:水煮蛋制作
配方 ID:4320 名称:炙炊饼制作
配方 ID:4325 名称:素包子制作
配方 ID:4330 名称:烤鱼制作
配方 ID:4335 名称:黄金鸡制作
配方 ID:4340 名称:玉井鱼饭制作
配方 ID:4345 名称:醉蟹制作
配方 ID:4350 名称:野琴酒制作
配方 ID:4355 名称:青菜豆腐煲制作
配方 ID:4360 名称:糖藕制作
配方 ID:4365 名称:肉夹馍制作
配方 ID:4370 名称:东坡肉制作
配方 ID:4375 名称:金汤花胶鸡制作
配方 ID:4380 名称:雪霞羹制作
配方 ID:4385 名称:粽子制作
配方 ID:4390 名称:青团制作
配方 ID:4395 名称:月饼制作
配方 ID:4400 名称:祝东风制作
配方 ID:4405 名称:水龙白鱼制作
配方 ID:4410 名称:鲜花饼制作
配方 ID:4415 名称:蟹黄豆腐制作
配方 ID:4420 名称:荔枝虾球制作
配方 ID:4425 名称:山煮羊制作
配方 ID:4430 名称:五珍烩制作
建筑 ID:1 名称:木箱
建筑 ID:2 名称:储物箱
建筑 ID:3 名称:自动出货箱
建筑 ID:4 名称:自动进货箱
建筑 ID:5 名称:出货箱
建筑 ID:6 名称:大型进货箱
建筑 ID:20 名称:1级传送带
建筑 ID:21 名称:2级传送带
建筑 ID:22 名称:3级传送带
建筑 ID:23 名称:4级传送带
建筑 ID:30 名称:1级地下传送带
建筑 ID:31 名称:2级地下传送带
建筑 ID:32 名称:3级地下传送带
建筑 ID:33 名称:4级地下传送带
建筑 ID:40 名称:分流器
建筑 ID:41 名称:合并器
建筑 ID:42 名称:智能分流器
建筑 ID:50 名称:制造机
建筑 ID:51 名称:组装机
建筑 ID:52 名称:合成机
建筑 ID:54 名称:熔炉
建筑 ID:55 名称:冶炼器
建筑 ID:56 名称:熔铸机
建筑 ID:60 名称:核心充能器
建筑 ID:61 名称:晶体置换机
建筑 ID:62 名称:粮食处理器
建筑 ID:63 名称:采集物收集箱
建筑 ID:100 名称:制作台
建筑 ID:101 名称:生产间
建筑 ID:110 名称:原油精炼厂
建筑 ID:130 名称:迷你卫星
建筑 ID:199 名称:小型火力发电机
建筑 ID:200 名称:风力发电机_close
建筑 ID:201 名称:火力发电机
建筑 ID:202 名称:太阳能发电板
建筑 ID:203 名称:燃油发电机
建筑 ID:220 名称:储电器
建筑 ID:250 名称:电线
建筑 ID:260 名称:跨区送电站
建筑 ID:261 名称:跨区收电站
建筑 ID:300 名称:抽水机
建筑 ID:301 名称:原油钻井
建筑 ID:320 名称:水管
建筑 ID:400 名称:石墙
建筑 ID:410 名称:告示牌
建筑 ID:430 名称:木质短门
建筑 ID:450 名称:传送板
建筑 ID:451 名称:跨区传送板
建筑 ID:452 名称:跨区送货站
建筑 ID:453 名称:跨区收货站
建筑 ID:459 名称:地板拆除
建筑 ID:460 名称:石砖地板
建筑 ID:461 名称:高速地板
建筑 ID:462 名称:高性能地面
建筑 ID:480 名称:小型地灯
建筑 ID:481 名称:路灯
建筑 ID:482 名称:远光灯塔
建筑 ID:483 名称:长空灯
功夫 ID:1 名称:沉舟心法
功夫 ID:2 名称:三星照财功
功夫 ID:3 名称:危桥心法
功夫 ID:4 名称:鲲鹏劲
功夫 ID:5 名称:玄霜劲
功夫 ID:6 名称:枯井功
功夫 ID:7 名称:幽昙功
功夫 ID:8 名称:止观心经
功夫 ID:9 名称:野夫真经
功夫 ID:10 名称:四海战典
功夫 ID:11 名称:困龙真气
功夫 ID:12 名称:大梵天功
功夫 ID:13 名称:冰蚀功法
功夫 ID:14 名称:白帝神功
功夫 ID:100 名称:破竹剑法
功夫 ID:101 名称:游鱼刃法
功夫 ID:102 名称:孤村剑法
功夫 ID:103 名称:玩命百式
功夫 ID:104 名称:残冬剑法
功夫 ID:105 名称:破阵五十弦
功夫 ID:106 名称:窃星剑舞
功夫 ID:107 名称:六尘印
功夫 ID:108 名称:脱兔十式
功夫 ID:109 名称:烽烟剑诀
功夫 ID:110 名称:无灭印法
功夫 ID:111 名称:寒山剑诀
功夫 ID:112 名称:燎天九绝
功夫 ID:200 名称:云行术
功夫 ID:201 名称:掩月步
功夫 ID:202 名称:踏剑飞身
功夫 ID:203 名称:灵猫隐
功夫 ID:204 名称:穿雨步法
功夫 ID:205 名称:鹧鸪纵
功夫 ID:206 名称:幻花身法
功夫 ID:207 名称:御风无迹
功夫 ID:300 名称:甲胄诀
功夫 ID:301 名称:泥衣功
功夫 ID:302 名称:铁臂诀
功夫 ID:303 名称:金屏功
功夫 ID:304 名称:倒悬金钟
功夫 ID:305 名称:龟甲诀
功夫 ID:306 名称:大目连气罩
功夫 ID:307 名称:擎天壁障
NPC ID:9999 名称:陈艺天
NPC ID:9998 名称:贺玥萌
NPC ID:7 名称:柴霏霏
NPC ID:21 名称:徐长庭
NPC ID:16 名称:陆蝉衣
NPC ID:20 名称:徐长空
NPC ID:2 名称:阿昙
NPC ID:9 名称:赫连常山
NPC ID:12 名称:柳依依
NPC ID:13 名称:李望星
NPC ID:14 名称:礼嫣
NPC ID:24 名称:萧钰
NPC ID:1 名称:徐富春
NPC ID:10 名称:林山石
NPC ID:23 名称:俞关山
NPC ID:4 名称:曹云秀
NPC ID:17 名称:鲁威
NPC ID:3 名称:曹云惠
NPC ID:15 名称:鲁冰花
NPC ID:22 名称:一元
NPC ID:11 名称:柳三娘
NPC ID:18 名称:陆英
NPC ID:19 名称:孙道一
NPC ID:5 名称:柴家祖母
NPC ID:8 名称:寒山
NPC ID:25 名称:郑德
NPC ID:26 名称:野利旺珠
NPC ID:27 名称:红弦
NPC ID:28 名称:阿拓
NPC ID:6 名称:柴世豪
NPC ID:29 名称:海山
NPC ID:30 名称:武大林
NPC ID:31 名称:白鸟
NPC ID:32 名称:夏为旗
NPC ID:33 名称:金日天
NPC ID:34 名称:赵洛阳
NPC ID:35 名称:于磨
NPC ID:36 名称:家丁柴刀
NPC ID:37 名称:家丁柴房
NPC ID:38 名称:家丁柴火
NPC ID:39 名称:家丁柴鸡
NPC ID:100 名称:阿卓
虽然NPC是乱序的,不过无所谓。反正真要用的时候会直接搜索。
至此,我们已经获得了控制几乎全部游戏内容的能力。不论是无敌还是伤害倍率,都可以直接改源代码实现。虽然更加普适的做法是对着代码写个CheatTable,但既然是自用,还是省点事吧。
他山之石
接下来进行资源拆包。经过之前的源代码审查,可以确定iFCon就是资源文件。直接找到DPack的初始化部分。摘录文件解析及读取代码如下:
// iFActionGame2.data.DPack
// Token: 0x06000357 RID: 855 RVA: 0x0001AB48 File Offset: 0x00018D48
public DPack(string path)
{
IRWFile irwfile = new IRWFile(path);
string a = irwfile.ReadMs(6);
bool flag = a == "iFFile";
if (flag)
{
this.filebyteSize = irwfile.ReadInt();
this.fileList = new DPack.DFileList(irwfile);
this.pack = irwfile.ReadByteToEnd();
this.read = new IRWFile(this.pack);
}
}
// iFActionGame2.data.DPack
// Token: 0x06000359 RID: 857 RVA: 0x0001ABDC File Offset: 0x00018DDC
public byte[] getFile(string path)
{
bool flag = this.fileList.list.ContainsKey(path);
byte[] result;
if (flag)
{
DPack.DFileList.DFile dfile = this.fileList.list[path];
this.read.pos = dfile.point;
result = this.read.ReadByte(dfile.length);
}
else
{
result = null;
}
return result;
}
// iFActionGame2.data.DPack.DFileList
// Token: 0x06000395 RID: 917 RVA: 0x0001B5CC File Offset: 0x000197CC
public DFileList(IRWFile read)
{
this.size = read.ReadInt();
this.list = new Dictionary<string, DPack.DFileList.DFile>();
for (int i = 0; i < this.size; i++)
{
DPack.DFileList.DFile dfile = new DPack.DFileList.DFile(read);
this.list.Add(dfile.name, dfile);
}
}
// iFActionGame2.data.DPack.DFileList.DFile
// Token: 0x06000398 RID: 920 RVA: 0x0001B66E File Offset: 0x0001986E
public DFile(IRWFile read)
{
this.point = read.ReadInt();
this.length = read.ReadInt();
this.name = read.ReadString();
}
// iFActionGame2.IRWFile
// Token: 0x060001E7 RID: 487 RVA: 0x00010CBC File Offset: 0x0000EEBC
public int ReadInt()
{
return BitConverter.ToInt32(this.Read(4), 0);
}
// iFActionGame2.IRWFile
// Token: 0x060001E9 RID: 489 RVA: 0x00010CFC File Offset: 0x0000EEFC
public string ReadString()
{
int length = this.ReadInt();
return Encoding.UTF8.GetString(this.Read(length));
}
从这里我们可以得知iFCon文件的反序列化逻辑。这里博主使用010Editor解析文件,编写解析模板如下:
//------------------------------------------------
//--- 010 Editor v12.0.1 Binary Template
typedef struct DString{
int length;
char str[length];
};
typedef struct DFile{
int point;
int length;
DString name<read=Str("%s", this.str)>;
local uint64 ret = FTell();
FSeek(parentof(this).end+point);
uchar data[length];
FSeek(ret);
};
typedef struct DFileList{
int size;
local uint64 ret = FTell();
local int i;
local int _slen;
for(i=0;i<size;i++){
_slen = ReadInt(FTell()+8);
//Printf("%d : %d\n",FTell()+8,_slen);
FSkip(12+_slen);
}
local uint64 end = FTell();
//Printf("end @ %d\n",end);
if (parentof(this).payload_offset == end){
Printf("Endpoint check pass...");
}
FSeek(ret);
DFile list[size]<optimize=false, read=Str("%s", this.name.str)>;
};
typedef struct DPack{
char Sanity[6];
int filebyteSize;
local int payload_offset=FTell()+filebyteSize;
DFileList fileList;
};
DPack pack;
因为一条文件存储记录是分为头部的索引和负载部分的二进制数据,在空间上割裂为两部分,因此使用了FSeek等方式乱序解析。本模板的语法文档可以在此处找到。
虽然DPack.filebyteSize似乎标记了二进制负载的基础偏移,但是原实现中并未通过本数据定位负载,而是以读取完所有头部记录后的偏移作为基础。因此仅使用本数据作校验用,在确定偏移时按照原版实现。
可以看到,模板解析出了全部的文件记录。随便选一个PNG文件,选取负载段,可以看到合法的PNG文件头。
右键导出这一部分,使用图像浏览工具打开,可以看到完整的素材。这证明解析部分的逻辑是正确的。
尾声
其实博主还尝试了其他魔改方式,例如通过dsound.dll注入自定义库,然后通过反射修改行为等。但是没写完就咕了,因此就不在本文详述了。
大概读了一下游戏代码,博主确实不是很理解为什么要在代码中内联数据。这种写法用来做原型是很快,但是后期要修改或者添加新内容就是加倍的折磨。这九万多行代码里面大部分都是重复的内容,完全可以有更好的实现方式。
掐指一算,游戏外捣鼓的时间都快赶上游戏时长了,该回去继续搭生产线了。
真·全文完。
请问“通过 dsound.dll 注入自定义库\”
有一个没有加密的c#自研引擎游戏,我想用imgui.net写个菜单加载到游戏中,能实现吗?
实现肯定是能实现,毕竟最差情况下你可以直接写个新游戏替换掉原来的。但是如果希望比较轻松愉快地实现,那建议不要试图创建任何原来不存在的东西,特别是涉及到UI的方面。
这方面建议直接去用BepInEx,社区相当成熟。听起来你的需求像是写一个mod,BepInEx有许多成功案例可供参考。你也可以看看其他游戏中有没有人尝试做过类似的事情。
——就我的观察来说,一般是游戏里有什么就用什么。
看了一下BepInEx,.NET的版本只有
BepInEx-NET.Framework-net40-win-x86-6.0.0-pre.2.zip
BepInEx-NET.Framework-net452-win-x86-6.0.0-pre.2.zip
没有找到x64的。
游戏使用了System.Windows.Forms.dll,我尝试用这个类在dnspy里造一个界面,感觉很麻烦,有没有办法在ide里造一个Forms界面,再引用游戏dll写好mod功能注入到游戏中?
可以问 一下在 新版本的loadScript 里面多了个 XORDecrypt 我外部实现了一下他
但是却解码出来了这种东西请问是我实现错了还是他新版本混淆了?
展开查看
然后iFActionGame2.data.DPack 这个我并没有找到
是他改名了还是这个被移除了?
望解答
其实这个游戏在成文几天后就没再碰过了。如果代码和资源都改了,那可能是制作组意识到这个问题了
也可能是制作组自搜发现了这篇文章。我暂且猜测如下:代码部分应该是某个版本开始混淆了。截止我拆的版本还是完全的明文源代码,这个看起来就像是混淆的片段在我拆的时候还是没有的。
DPack的模式应该是他们引擎设计的资源包模式,不太可能因为被拆包就大改,可以试试模版还能不能解析。
但是具体的函数名是有可能修改的,这个在没看新版的情况下我没法确定。考虑一下找找不能混淆的方法名或者干脆动态调试?
如果过一段有时间的话我去看看新版本,或者你看看能不能下到旧版本复现?旧版本应该能提供很多有用的信息。
DPack 我刚刚找到了了倒是 那他们资源包里面有代码吗 我之前直接vsc打开发现里面有大量的可读的字符串 并且包含了像调用方法一样的东西 但是只有一半我不太敢确认比如这种removeTalk(e) 然后前后就都是不可读的内容了
我逆那个版本,代码是存在资源包里的。
IVal.Pack.getFile("Graphics\\Scene\\item_5.png")这个获取的其实不是图片,是代码。参见loadScript函数。剩下的不可读内容你可以试着拿hex编辑器看看,应该是其他的二进制资源。
我刚刚下载了旧版本 确实和文中是一样的 然后发现新版本的iFCon 小了不少 不知道他做了什么 倒是
对了可以问个问题吗?他那个资源文件 如果按照 他开头的列表读取的话后面还会剩下一堆数据这种该怎么解决?(新版本的)
大概分析了下他貌似吧游戏逻辑切成两个部分了 一个单独文件一个直接塞后面 离谱 但是后面那个没有找到读取方法 emmm
看起来是.ifres 文件但是不知道这东西应该怎么解析 (看起来剧情主要是写里面的 有点怪 他居然不在开头的索引里面
新版本的话我确实不了解,最近暂时没空闲再去摸摸它了。如果你能找到DPack类的话看看改了没。
不过居然在后面加了一段没索引的数据,我开始好奇制作组怎么改的了。