0%

只要你等待的足够久,你想要的功能就会从开源社区自己长出来。 —— tg大佬·但忘了·是谁。
跳转:messagepack_csharp_unpacker.py

起因

在20-21年我尝试写了一个《河洛群侠传》的存档修改器,但因为当时水平太差,连格式都分析不出来,最后只能将河洛存读档的代码复制出来直接用,最后也只写出一个解包器,修改器不了了之。

时隔多年重新搜索,竟然发现有大佬写出了转换工具,而且最后更新时间在一个月前
令人感慨,果然只要等待的时间够久,就一定有人能将你的想法实现(笑
于是拿大佬的代码改改,写(zu)出(zhuang)来(chu)了给河洛群侠传用的存档转换器。

存档格式

河洛群侠传有两种存档格式:

  • 纯文本格式的存档,header为”TitleSave”,是早期使用的存档格式
  • 使用MessagePack-CSharp打包的存档,header为”HELUO_1_0”或”HELUO_1_1”,是新版本的存档格式
    第一种格式是明文存储,修改起来很方便,网络上也已经有了很成熟的修改器
    但并没有第二种格式的修改器,通常的解决办法是修改System.String Heluo.GameConfig::SaveDataVersion为”TitleSave”,使新版游戏以旧版格式存档,以便修改。这种方法需要反编译Assembly-CSharp.dll,难度不大。

新版本存档转换为旧版本格式

我希望有不破坏游戏代码本身的方法,所以在寻求这种方式。

格式

旧版本格式的存档格式非常简单

1
2
3
4
5
6
7
第一行: TitleSave,[difficulty]
第二行:playedTime
第三行:playedDays
第四行:trackedQuestId,modName,modId
第五行:ScreenShotSize.x,ScreenShotSize.y
第六行:一个base64编码的图片
第七行:GameData

新版本格式则是
“HELUO_1_1” + HELUO_1_1 + GameData
其中后两个数据是序列化后的自定义类,而GameData中就包含角色属性、物品等数据。只要将它反序列化,就能修改了。
序列化的方法为

1
2
LZ4MessagePackSerializer.Serialize<Header_HELUO_1_1>(memoryStream2, header_HELUO_1_2, HeluoResolver.Instance);                
LZ4MessagePackSerializer.Serialize<GameData>(memoryStream2, data, HeluoResolver.Instance);

原来卡住的地方

我希望用python的msgpack和lz4包对游戏存档进行解码,再转换为旧版本格式。
但MessagePack-CSharp的LZ4MessagePackSerializer算法看不懂,再加上逆向不熟练,代码追踪时总是跟丢,多次尝试后还是放弃了。最后解决的办法是将从游戏剥离出的存读档代码和1.x版本的MessagePack-CSharp一起编译成了dll。

messagepack_csharp_unpacker.py

在多年之后,终于看到了LZ4MessagePackSerializer.Deserialize的python实现,泪目
大佬的代码:https://gist.github.com/t-wy/778123fedd15513e4626ad27f07cb690

看完代码之后才发现原来结构这么简单(汗

于是改了一下,河群的格式转换就成了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
import sys
import shutil
import json
from datetime import datetime
from msgpack import Unpacker
from lz4.block import decompress as lz4_decompress
def decode_savedata(data):
save_loader = Unpacker(None, max_buffer_size=0,strict_map_key=False)
save_loader.feed(data)
for _ in range(2):
ext_code, data = save_loader.unpack()
assert ext_code == 99

unpacker = Unpacker(None, max_buffer_size=0,strict_map_key=False)
unpacker.feed(data)
original_size = unpacker.unpack()
start_pos = unpacker.tell()
print("original_size",original_size,"start_pos",start_pos)
data1 = data[start_pos:]
raw = lz4_decompress(data1, uncompressed_size=original_size)
assert len(raw) == original_size

unpacker = Unpacker(None, max_buffer_size=0,strict_map_key=False)
unpacker.feed(raw)
d = unpacker.unpack()
if 'screenShotData' in d:
d['screenShotData'] = ''
yield d

def time_format(playedTime):
millisec = playedTime % 10000000
ss = int(playedTime / 10000000 % 60)
mm = int(playedTime / 600000000 % 60)
hh = int(playedTime / 36000000000)
return f"{hh:02}:{mm:02}:{ss:02}.{millisec:07}"

def generate_titlesave(header,data) -> bytes:
s = ''
s += f"TitleSave,{header['difficulty']}\n"
s += time_format(header['playedTime']) + '\n'
s += str(header['playedDays']) + '\n'
s += f"{header['trackedQuestId']},{header['modName']},{header['modId']}\n"
s += f"{int(data['ScreenShotSize'][0])}, {int(data['ScreenShotSize'][1])}, 0\n"
s += data['ScreenShot'] + "\n"
data['ScreenShot'] = ''
# 特殊的格式转换
data['TotalTime'] = time_format(data['TotalTime'])
data['GameWorldTime'] = datetime.fromtimestamp((data['GameWorldTime'] - 621355968000000000) / 10000000).strftime("%Y-%m-%dT%H:%M:%S") # https://github.com/EduBic/CSharp-Ticks-Date-Time-In-Javascript
data['PlayerPostioion'] = [f"{round(i,5):.5f}" for i in data['PlayerPostioion']]
data['PlayerDirection'] = round(data['PlayerDirection'],6)
data['RelativeCameraPosition'] = [f"{round(i,5):.5f}" for i in data['RelativeCameraPosition']]
data['TaggedPosition'] = [f"{round(i,5):.5f}" for i in data['TaggedPosition']]
def recursive_modify(d: dict):
for k in d:
v = d[k]
if k in ['position', 'forward'] and isinstance(v,list):
d[k] = [f"{round(j,5):.5f}" for j in d[k]]
elif isinstance(v,dict):
recursive_modify(d[k])
elif isinstance(v,list):
for it in v:
if isinstance(it,dict):
recursive_modify(it)
recursive_modify(data)
data = json.dumps(data,separators=(",",":"),ensure_ascii=False) # https://stackoverflow.com/a/18337754
s += data
s = s.encode("utf8")
return s

if __name__ == '__main__':
if len(sys.argv) < 2:
print("usage: python heluo_save_converter.py savefile [output_name]")
exit()
elif len(sys.argv) == 2:
save1 = save2 = sys.argv[1]
else:
save1, save2, *_ = sys.argv[1:]
with open(save1,"rb") as f:
f.read(9)
binary = f.read()
if save1 == save2:
shutil.copy(save1,save1+".bak")
header,gamedata = list(decode_savedata(binary))
save = generate_titlesave(header,gamedata)
with open(save2,"wb") as f:
f.write(save)

说点别的,没有逻辑

没想到三年前没做成的事情会在这个时候重新跳出来。回想一下三年前,才发现这几年过去了,自己竟然一点长进也没有(叹气
有人说过,和平庸的自己和解,是绝大多数人必须经历的事。对我来说,虽然很早就意识到了,但直到现在也不愿承认。或许今后我还是会一直挣扎吧。
从以前,到现在这个已不再青春的年纪,直到本就不强的能力越来越衰弱,最终被这个世界抛弃的未来,或许和自己闹别扭,想要在一地废品中找到属于自己独有的、能拿来吹牛逼的优势这件事终究还是会一直缠着我吧。
什么眼高手低的goushi人生。

仙肴圣餐 (Ambrosia アンブロシア)

开发商: しもばしら工房
链接: https://store.steampowered.com/app/1078350/_/, https://www.dlsite.com/maniax/work/=/product_id/RJ237060.html

魔法少女天穹法妮雅 (Magical Girl Celesphonia 魔法戦士セレスフォニア)

开发商: しもばしら工房
链接: https://store.steampowered.com/app/1786790/Magical_Girl_Celesphonia/

祈愿诗篇 (Pray Game)

开发商:U-ROOM
链接: https://store.steampowered.com/app/1551530/Pray_Game/
零h经验通关,这就是故事模式给我的自信

本文转载于blog.casualthink.com,原作者ztmzzz,原标题为“破解sandboxie-plus”

前提准备

  1. 拥有可用于驱动签名的证书或者Winodws安装自签名驱动
  2. 安装Visual Studio 2019,勾选使用C++的桌面开发
  3. 安装Windows SDK,对应WDK版本
  4. 安装Windows WDK, Windows 10, version 2004 https://go.microsoft.com/fwlink/?linkid=2128854
  5. 安装QT5和VS的QT插件(可选,编译界面用)(QT6应该也行)
  6. 在VS中安装带有Spectre缓解的C++ MFC
  7. GitHub上下载最新的安装包和对应的源代码
  8. 使用安装包安装

编译

  1. 用VS打开Sandboxie\SandboxDrv.sln
  2. 选择verify.c
  3. 修改KphVerifySignature函数,在函数开头直接return 0;
  4. 选择Release,x64,编译出SbieDrv.sys

驱动签名

在VS的Developer Command Prompt中执行
signtool sign /v /a /f 证书.pfx /p "证书密码" SbieDrv.sys

覆盖驱动

重命名原始的SbieDrv.sys为其他名字,替换为修改过的SbieDrv.sys,然后重启既可

输入捐赠证书

例如:

1
2
3
4
5
6
7
8
[NAME: 123  
LEVEL: CONTRIBUTOR
DATE: 01.01.2200
SIGNATURE: 1111](name: 123 LEVEL: CONTRIBUTOR DATE: 01.01.2200 SIGNATURE: 1111)```
NAME: 123
LEVEL: CONTRIBUTOR
DATE: 01.01.2200
SIGNATURE: 1111

  1. 从nsp中dump Romfs
  2. 13机兵的digital artwork dump出来的Romfs是三个cpk文件,用CPK Browser 或者quickbms+cpk.bms 把cpk解包
  3. 解包后的文件(.ftx, .mbs, .fsb) 中,猜测ftx后缀为texture文件。用xentax论坛用户pasta提供的python脚本 可以解开ftx文件
  4. 使用Rawtex即可将拆分的ftx(即nvt文件)转换为dds和png。但论坛所说的参数有误,不应是BC7,而是 Offset 100 + BC3(bc3_unom) + Switch swizzle + 自动识别宽高大小(或4096x4096)。如果转换后的texture有条纹状坏块,可能要重新尝试其他Offset值。
  5. 转换后的texture包含数个图片,最后使用PS、GIMP将不需要的图crop掉即可。
    解包出来的美图



    参考

一些作者会喜欢在自家的软件里加上使用统计功能(usage analytics),以便于收集用户使用习惯等,用于改进软件。虽说统计模块确实能通过统计数据帮助作者做出决策,而且很多软件作者都在这么做(比如微软),但笔者从心里抗拒这种方式,所以看到“遥测”、“用户体验改善计划”等字眼就会想办法关掉这类功能。如果只是单纯的用户找bug的离线统计,并且默认离线存储那倒还好,但事实上绝大多数软件都会默认设置定期把统计结果发回自己的服务器,甚至有的没法关闭这个功能。好在如果软件开源,我们可以直接修改代码,把统计功能拔了。
相比于大公司的大项目(如vscode),个人作者的软件所的使用统计功能则有很多种样子。本文所述的项目则是使用electron-google-analytics,通过Google Analytics来收集统计数据。而对于这种项目,我们可以通过构造一个无任何联网行为的Analytics类,并将源码中导入electron-google-analytics的地方修改为导入我们构造好的Analytics类来达成屏蔽统计功能的效果。

  • 第一步,我们先根据electron-google-analytics的代码构造一个空的Analytics类:
    fake-ga.js

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    // Fake Google-Analytics component
    class Analytics {
    constructor(trackingID, {
    userAgent = '',
    debug = false,
    version = 1
    } = {}) {
    this.foo={};
    }

    set(key, value) {

    }

    pageview(hostname, url, title, clientID, sessDuration) {
    }

    event(evCategory, evAction, { evLabel, evValue, clientID } = {}) {
    }

    screen(appName, appVer, appID, appInstallerID, screenName, clientID) {

    }

    transaction(trnID, {
    trnAffil, trnRev, trnShip, trnTax, currCode
    } = {}, clientID) {

    }

    social(socialAction, socialNetwork, socialTarget, clientID) {

    }

    exception(exDesc, exFatal, clientID) {

    }

    refund(trnID, evCategory = 'Ecommerce', evAction = 'Refund', nonInteraction = 1, { prdID, prdQty } = {}, clientID) {

    }

    purchase(hostname, url, title, transactionID, {
    trnAffil, trnRev, trnTax, trnShip, trnCoupon,
    prdID, prdName, prdCtg, prdBrand, prdVar, prdPos
    } = {}, clientID) {

    }

    checkout(hostname, url, title, checkoutStep, checkoutOpt, {
    prdID, prdName, prdCtg, prdBrand, prdVar, prdPrice, prdQty
    } = {}, clientID) {

    }

    checkoutOpt(evCategory, evAction, checkoutStep, checkoutOpt, clientID) {

    }

    promoImp(hostname, url, title, {
    promoID, promoName, promoCrt, promoPos
    } = {}, clientID) {

    }

    promoCk(evCategory, evAction, {
    evLabel, promoID, promoName, promoCrt, promoPos
    } = {}, clientID) {

    }

    item(trnID, itemName, {
    itemPrice, itemQty, itemSku, itemVariation, currCode
    } = {}, clientID) {

    }

    timingTrk(timingCtg, timingVar, timingTime, {
    timingLbl, dns, pageDownTime, redirTime, tcpConnTime, serverResTime
    } = {}, clientID) {

    }

    send(hitType, params, clientID) {

    }
    }

    export default Analytics;
  • 第二步,我们将源码中导入electron-google-analytics的地方改为导入我们构造好的组件:

    1
    2
    //import Analytics from "electron-google-analytics";
    import Analytics from "./fake-ga.js";
  • 第三步,测试修改

    1
    2
    yarn
    yarn test
  • 如果软件不再连接谷歌服务器(1e100.net),则可以编译后放心使用了

Example: https://github.com/bigbenzh/koodo-reader

感谢你看到这里❤

作者:[saw2008](https://www.pixiv.net/en/users/11084628) 作者:[SLASH](https://www.pixiv.net/en/users/11135560) 作者:[おか](https://www.pixiv.net/en/users/18688569)

python 条件表达式(ternary operator):

1
a if b else c

python 列表生成式(List Comprehension):

1
[x for x in range(100) if y]

同时使用条件表达式与列表生成式:

1
[a if {condition of x} else c for x in range(100) (if y) ]

(if y)不是条件表达式中的语法,而是列表生成式的语法(filter)
例子:

1
[1 if x%2==1 else 0 for x in range(10)]

结果:[0, 1, 0, 1, 0, 1, 0, 1, 0, 1]
例子2:

1
[1 if x%2==1 else 0 for x in range(10) if x%3==0]

结果:
[0, 1, 0, 1]

source:
https://stackoverflow.com/questions/17321138/one-line-list-comprehension-if-else-variants#comment50313650_17321170
https://stackoverflow.com/questions/2951701/is-it-possible-to-use-else-in-a-list-comprehension

(转移博客时源代码丢失,本文根据备份的网页还原,感谢turndown项目
Ender Lilies的美术确实顶,特别是暂停界面的白毛萝莉特写,所以想把它弄出来做桌面背景。但今天用UEViewer拆包ender lilies时发现居然会出错,报错内容为assertion failed: !isUnicode。本人水平有限,不知道为何导致这个问题,也不知道怎么修复。但事实上注释掉源代码中的assert,是可以成功导出图片的。至少是部分。

(注:如果你只是为了找暂停界面的莉莉图片,不需要这么麻烦。她在Game/_Zenith/UI/Textures/Characters里,可以直接导出)

定位错误位置:
UEViewer/Unreal/UnrealPackage/UnPackage4.cpp SerializeFNameSerializedView 中
注释掉第671行:

1
//assert(!isUnicode);  

同时注释掉 LoadNameTableIoStore 中的第695行:

1
//assert(Data == EndPosition);  

重新编译就可以导出大部分图片(大概)

可爱,可惜是半透明的TAT

(转移博客时源代码丢失,本文根据备份的网页还原,感谢turndown项目

来自asd11132@psnine

游戏主界面

亚马逊PC壁纸

ost封面的小千寻

原版

注:修改后缀、文件名可以获得不同规格的图片。1903x1903是分辨率,”bb”是”Image Resize Style”,经测试可以是”b”(back?)或”f”(front?),具体用法还不了解。在Image Resize Style是bb的情况下,返回图片的分辨率存在最大值(此处为1903x1621),应该是封面原图的大小。将”bb”删除,效果跟保留”bb”一致。把拓展名换成png、webp也能获得不同格式的图片。注意使用不同的参数获得的图片虽然分辨率可能相同,但存在不同的exif值。提取itunes专辑封面原图的网站:Apple Music Artwork Finder,作者旺代

来自官方推特页

来自nga

五百里色图,PS4主题

日版早期购买特典,现在(2021年)可以在淘宝搜”十三机兵+首发版”或者”十三机兵+特典”买到
nga老哥发的高清图 链接1 链接2
可能的原图来源https://ngabbs.com/read.php?tid=21045195 (但图床链接无了,附件大小不对)

PS4主题

日版预购特典,现在买不到了
来自nga老哥https://ngabbs.com/read.php?tid=22313997 楼主的图用ps提取视频帧拼成,二楼则是楼主的图+waifu2x

来自youtube

预约特典主题展示https://www.youtube.com/watch?v=vq7WcCJTvic
目测是上面那个nga老哥用的视频源。可能可以用来提取另一张图

在官网上有对calibre统计信息的说明。每次启动calibre,就会向服务器发送一次统计数据,其中包含OS,版本及一个在安装时就生成的UUID。

UUID的生成代码在src/calibre/utils 下的exim.py和src/calibre/srv下的opds.py中,其中opds.py应该是calibre-server的代码。同时src/calibre/utils/config_base.py 中也有和UUID相关的代码:

1
2
3
4
5
# 550-553行
prefs = ConfigProxy(create_global_prefs())
if prefs['installation_uuid'] is None:
import uuid
prefs['installation_uuid'] = unicode_type(uuid.uuid4())

而每次启动calibre时向服务器发送统计信息的代码在src/calibre/gui2/update.py中:

1
2
3
4
5
6
7
42-47行
headers={
'CALIBRE-VERSION':__version__,
'CALIBRE-OS': ('win' if iswindows else 'osx' if ismacos else 'oth'),
'CALIBRE-INSTALL-UUID': prefs['installation_uuid'],
'CALIBRE-ICON-THEME': icon_theme_name,
}

事实上没有这个headers也能正常获取版本号_(:з)∠)_

最后在获取新闻(feeds)时也会发送UUID(src/calibre/web/feeds/recipes):

1
2
recipe_source = bz2.decompress(get_https_resource_securely(
'https://code.calibre-ebook.com/recipe-compressed/'+urn, headers={'CALIBRE-INSTALL-UUID':prefs['installation_uuid']}))

尚不清楚删除这个headers会不会影响feeds的获取

感谢你看到这里❤

https://bw.bilibili.com/2020/index.html
目标:2233娘的魔女立绘

f12,network,看到加载图片的脚本为bundle.css

在图上右键——审查元素,到达名叫content wrapper的class
发现装2233娘图的框在content-bg-bw-layer1里,对应的class为content-bg-bw

进入bundle.css,查找content-bg-bw,找到图片的链接,分别在content-bg-bw.layer-22和content-bg-bw.layer-33处
22和33各有两张



顺便一提,背景的2233娘在/html/body/div[1]/div/div[1]/video处,class name是video-cover-video
https://activity.hdslb.com/blackboard/static/20201028/b8f2b74d0482aed61472c7065dc1ed56/ictGGduDW.mp4


#鹿乃 #花寄女子寮 #kano
作者:哈利贝西
https://t.bilibili.com/414848034975429243
https://twitter.com/HarryBeace/status/1285949904016166912


#洛天依
作者:T爹
https://www.pixiv.net/artworks/91226205


#洛天依 #2019生日会
作者:T爹
https://t.bilibili.com/275131730925598823
这张p站和画师推都没有


#花寄女子寮
作者:へちま(@hechima10040)
https://twitter.com/hechima10040/status/1361320052373393411
https://hechima10040.fanbox.cc/posts/1922197


https://twitter.com/hechima10040/status/1302569925458845699

https://twitter.com/nonomiyanonono/status/1302622568155807745


https://twitter.com/i/web/status/1302216734993076226 (无了)
https://danbooru.donmai.us/posts/4089640

本文中的部分代码是对《河洛群侠传》反编译的结果。这些代码的版权属河洛工作室所有

正版玩家体验不到,亏大了

在用dnSpy分析河洛群侠传的代码时意外地发现了这游戏居然有一串检测steam_api.dll文件完整性的代码,如果发现steam_api.dll不是原版,就把主角传送到拱石村附近的龙口洞窟中(这个地方只能用卡点脱逃跑出来),并把卡点脱逃关掉,让玩家卡死在里面。

这段检测出现在Heluo.Platform.SteamPlatform中,代码为

1
2
3
4
5
6
if (SteamManager.HelloWorld)
{
gameData.PlayerPostioion = new Vector3(48.48f, 155.22f, -1202.68f);
gameData.TimeFreezed = true;
GameConfig.EscapeFlag = "NoPainNoGain";
}

追踪到SteamManager,可以看到跟原版Steamworks.NET的SteamManager相比,它多了一个名叫HelloWorld的方法。它会返回一个叫m_bHelloWorld的bool变量,而设置m_bHelloWorld值的代码在Awake方法中:

1
2
3
4
if (!DllCheck.Test())
{
this.m_bHelloWorld = true;
}

继续追踪DllCheck,发现这是魔改的SteamWorks自带的DllCheck。原版的这段代码上面的注释“Returns false only if the steam_api.dll is found and it’s the wrong size or version number.”表明它会检测steam_api.dll是否存在,如果存在则检测文件大小和版本号是否正确。而魔改后的代码有一些变动(我的分析可能有错,望指出):

  • 如果找不到steam_api.dll就返回false(原版是返回true)
  • 多出了一段类似CRC的完整性检验(可能是自己写的),操作为读入steam_api.dll文件并用一个预设的变量和第一个byte异或,将结果再和第二个byte异或……直到和所有的byte执行异或计算,最后将变量和一个预设的值进行对比,如果不同就返回false。

好的,我懂了,这就作个死
(嚣张.jpg)


还原dll文件之后就能正常读档了

背景

这《河洛群侠传》,玩到一半才发现漆笑儿和开明王城图只能二选一,被这个选项恶心到了,所以我决定自己把这个道具加进我的存档里。在一顿搜索后发现了阿B上的一篇文章,于是按照文章的方法导出并修改了明文的存档。但这种方法要修改dll文件,于是我转而寻找其他更加简单的方法(但事实证明,我找的方法一个比一个麻烦_(:з)∠)_)。先是mod,但因为单个存档不能同时适应两个mod,所以最后变成了直接修改非明文存档这样的结果。

研究

用dnSpy反编译了游戏的存读档过程,发现存档文件里包含三部分数据:

  • 文件头HELUO_1_0
  • 一个包含mod名称,mod ID,游戏难度,存档时的截图等数据的struct,名字叫Header_HELUO_1_0
  • 包含其他数据的struct,名字叫GameData

其中第二和第三部分是压缩过的,所以如果要修改这两个部分必须先解压缩数据。
背包里的道具数据在第三部分,所以我的目标是对第三部分进行修改。

同时发现,对存档数据的编码、加解压使用的是MessagePack-Csharp库。

走的弯路,和用过的方法

噢,原来是MessagePack+LZ4啊,好的,我已经完全搞懂了。

由于对MessagePack一窍不通+没有发现读取存档的代码里对存档文件进行分段的操作,导致我花了好长时间才搞清楚过程。同时又因为对C#不了解,于是打算用python来写解包工具,又遇上无数的坑。
首先是LZ4MessagePackSerializer.Deserialize和LZ4MessagePackSerializer.Serialize都需要在调用时都必须要把数据的struct当作类型参数传进去,但gamedata结构有点复杂,不像Header_HELUO_1_0那样可以反编译后直接拿出来放进我的代码里,所以我决定退而求其次,手动把gamedata先分离出来,解压,然后用python的msgpack来deserialize。(万幸msgpack可以直接decode)
然后是MessagePack-Csharp的LZ4MessagePackerSerializer在压缩时,似乎并不是直接压缩,而是压缩之前还有一些重要操作。这些操作出现在调用LZ4压缩的ToLZ4BinaryCore方法和LZ4解压的DeserializeCore方法中。而我既读不懂这段代码,也找不到用python的msgpack和lz4代替的方法, 所以干脆直接照抄,把这一整段代码截下来放进我的代码里调用。

完成

最后我的补丁程序变成了python + C# dll,C# dll里包含导出存档的gamedata、将gamedata合并回去、LZ4压缩和解压缩等方法,而python则使用pythonnet库调用dll,和使用msgpack库来encode、decode导出来的gamedata。解码后的gamedata跟明文存档中一样是json格式,所以只要在inventory中添加一项:

1
{"ItemId":"it801069","Count":1,"Durability":0,"MaxDurability":0,"Weight":0,"EffectId":[],"IsNew":true,"Stolen":false,"Level":0,"Hurt":{},"HurtDifference":0.0,"ForgeMaterials":{},"QualityTitle":"","QuenchHoleCount":0,"QuenchEffect":[],"ReforgeType":-1,"Id":null}

就大功告成了。至此本菜鸟终于成功的修改了新版本的存档文件,可喜可贺。

相关文件
dll的源码
python源码
编译好的文件

(7.17 update: 原来的版本居然会影响存档……虽然不会坏档,但打完补丁之后就变成了10倍大小,还再也不能用相同的办法解包了……换成新版了。)

注:
dll中包含部分MessagePack的代码 https://github.com/neuecc/MessagePack-CSharp
MessagePack-CSharp基于MIT开源
部分代码通过反编译《河洛群侠传》获得

感想

做完之后回头看才发现原来这件事情其实很简单(╥_╥)但怎么弄了这么久 (╥_╥)
果然入门用python一时爽,泛型之类复杂一点的来一点就不会了……我这就去学C#
另外,感谢你看到这里❤如果思路或者代码让你血压拉满

那,对不起嘛

转载请注明出处。
本文来自澄空学园,原链接https://bbs.sumisora.net/read.php?tid=11042351


Ever tried. Ever failed.
No matter. Try again.
Fail again. Fail better.

一直想写一篇面向新手的 Galgame 汉化教程,但一直苦于没有时间。现在终于考完了试,申请也告一段落,这才抽了点时间来写这篇文。还有,工口你认真学,今后杏爱会的程序就看你的了。

本教程假设你已经满足以下条件:

  • 玩过 Galgame,了解一般 Galgame 资源存储的方式
  • 会一门编程语言,VB6 和 JavaScript 等前端语言除外
  • 能看懂用其它编程语言写出的源代码,此处特指用 C++
  • 能看懂汇编,51 单片机的汇编语言就行
  • 懂数据结构
  • 知道什么是字符编码
  • 英语水平还说得过去
  • 懂一点儿日语

本教程中使用的名词:

  • 封包:指游戏的资源文件在游戏安装目录下的保存形式
  • 脚本:含有二进制控制符和纯文本的文件
  • 文本:从脚本中提取,人类能够读懂的文本文件

首先来看看破解都要干啥:

  • 解开游戏的封包文件
  • 从提取出的脚本中提取出文本
  • 从提取出的脚本中提取出图片
  • 把翻译好的文本写回到脚本里
  • 把新的图片和脚本放回到封包里
  • 修改游戏主程序,使之能正常读取中文

好,让我们开始吧。

1. 准备

我们以方糖社的《花色ヘプタグラム》为例进行实战。不过在开始之前,你需要准备几种工具:

  • 一款支持多语言编码的文本编辑器,例如 Notepad2
  • 一款十六进制编辑器,最好也能支持多语言编码,例如 010Editor
  • 一款反汇编工具,例如 OllyDbg
  • 你常用的 IDE,比如 Visual Studio

工具的用法在此不再赘述,请自行谷歌。
安装游戏后打开游戏的主目录,你会发现这些文件(已略去部分无用文件):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
2012/09/26  16:33  643 autoload.arc
2012/09/26 16:33 1,817,109 data01000.arc
2012/09/26 16:33 876,414,725 data02000.arc
2012/09/26 16:33 654,502,744 data02001.arc
2012/09/26 16:33 1,179,169,979 data02010.arc
2012/09/26 16:33 12,594,853 data02050.arc
2012/09/26 16:33 299,065,396 data02100.arc
2012/09/26 16:33 21,874,416 data02950.arc
2012/09/26 16:33 14,331,692 data03000.arc
2012/09/26 16:33 922,747,352 data04000.arc
2012/09/26 16:33 75,902,855 data05000.arc
2012/09/26 16:33 860,160 Heptagram.exe
2012/09/26 16:33 34,148,877 sysgrp.arc
2012/09/26 16:33 290,498 sysprg.arc
2012/09/26 16:33 3,702,778 syssnd.arc
2012/09/26 16:33 2,825 system.arc

很明显,Heptagram.exe 是游戏的主程序,*.arc是游戏的封包文件。接下来,我们开始正式的流程。

2. 破解

2.1. 运行破解

双击 Heptagram.exe 来测试一下,发现没有出现游戏主窗口;而用 AppLocate 工具加载后却能够正常运行。这说明开发商在游戏中进行了某种检查,以防止游戏在日本日外的国家运行(原因大家都知道,而且还有家中二公司把这点做到了极致)。

好,让我们想一想,如何得到当前操作系统的区域信息?

透露一下,BGI 引擎使用 GetSystemDefaultLangID 来确定当前操作系统的区域。
运行 OllyDbg,载入 Heptagram.exe,如图。

按下 CTRL + N,在打开的 API 列表中找到 GetSystemDefaultLangID,右键选择“在每个参考上设置断点”。如图。

双击 Breakpoints 窗口中的项,来到 GetSystemDefaultLangID 的调用位置:

1
2
3
4
5
6
7
8
00455CE0  /$  FF15 D0E04A00	call    dword ptr [<&KERNEL32.GetSystemD>;  [GetSystemDefaultLangID  
00455CE6 |. 8B5424 04 mov edx, dword ptr [esp+4]
00455CEA |. 25 FF030000 and eax, 3FF
00455CEF |. 33C9 xor ecx, ecx
00455CF1 |. 3BC2 cmp eax, edx
00455CF3 |. 0F94C1 sete cl
00455CF6 |. 8BC1 mov eax, ecx
00455CF8 \. C3 retn

在简体中文环境下, GetSystemDefaultLangID 返回值是 0x804,而在日语环境下,返回值是 0x411。那么怎么改?很简单,直接把 0x411 写入返回值的寄存器就行了。这样,无论是什么语言的系统,都会被认为是日语系统。
双击 00455CE0 这一行,把汇编改为 mov eax, 411。改好后的这部分代码如下所示:

1
2
3
4
5
6
7
8
9
00455CE0 B8 11040000		mov	eax,  411  
00455CE5 90 nop ;修改后的代码比原先的要短,所以剩余的部分会用 nop 补齐
00455CE6 |. 8B5424 04 mov edx, dword ptr [esp+4]
00455CEA |. 25 FF030000 and eax, 3FF
00455CEF |. 33C9 xor ecx, ecx
00455CF1 |. 3BC2 cmp eax, edx
00455CF3 |. 0F94C1 sete cl
00455CF6 |. 8BC1 mov eax, ecx
00455CF8 \. C3 retn

改好之后,在 CPU 窗口右键,复制到可执行文件 -> 所有修改 -> 全部复制,再在弹出的窗口中右键,保存文件,命名为 Heptagram_NoCheck.exe,完成后退出 OllyDbg。

接下来找到刚保存的 Heptagram_NoCheck.exe,双击运行,游戏界面是不是出来了?

2.2. 游戏脚本

2.2.1. 解包

这一步可以说是整个汉化过程中最简单的了(当然,这是建立在前辈们的辛苦分析上的):用老毛子出品的 AnimED 即可(不推荐使用 Crass)。

从 BGI 汉化经验来看,脚本文件一般都保存在前几个封包中(本游戏的脚本就在 data01000.arc 中)。如果你找不到,你可以把封包逐一拖到 ExtractData 窗口中观察。单个脚本的大小一般不会超过 200KB。

使用 AnimED 解开 data01000.arc ,拿到脚本(同样,略去了部分文件):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

2013/01/13 16:31 64,740 a01
2013/01/13 16:31 72,838 a02
2013/01/13 16:31 76,598 a03
2013/01/13 16:31 6,433 b06_3
2013/01/13 16:31 47,365 b07_1
2013/01/13 16:31 71,012 b07_2
2013/01/13 16:31 92,156 c06_2
2013/01/13 16:31 77,374 d09
2013/01/13 16:31 79,352 d10_1
2013/01/13 16:31 1,712 main
2013/01/13 16:31 3,150 s_ci_02
2013/01/13 16:31 110,513 Yuzuriha
2013/01/13 16:31 128,763 _01
2013/01/13 16:31 93,819 _02
2013/01/13 16:31 154,264 _03
2013/01/13 16:31 100,339 _07
2013/01/13 16:31 268,696 _08
2013/01/13 16:31 28,475 _10_2
打开准备好的 Notepad2,载入其中的 _01 文件,然后按下 F8,选择 Japanese (Shift-JIS)。你会发现,这个文件就是游戏的第一章:

2.2.2. 二进制脚本分析

在打开的 _01 文件中,你可以看到不可读的二进制代码和可读的文本混在一起。接下来,打开你的十六进制编辑器,载入_01 文件。

如图所示,BGI 脚本分为四部分:

  • 头部 Magic:用于校验该文件的类型
  • 脚本头部:定义了该脚本所需的全局变量
  • 指令部分:控制游戏的流程
  • 文本列表:我们最感兴趣的东西

在游戏执行中,引擎不断地读取指令部分中的指令,形成了游戏的时间线。 可以看出,0x00000003 之后的四字节(DWORD)和下面文本列表中字符串 _01.txt (起始地址为 0x18D1C)有关:

1
0x18D1C  =  0x1C  (MAGIC 长度)  +  0x34  (头部长度)  +  0x18CCC  (0x00000003  之后的四字节)

如果你不放心,可以再找几句话计算看看。
那么,思路就确定下来了:
逐个读取所有的 0x00000003,把之后的四字节作为地址,修正后用来查找目标处的字符串。也就是

1
String  =  GetStringAt(GetUnsignedIntAt(AddressOfAny(0x03)  +  4)  +  LengthOf(magic)  +  LengthOf(header))

接下来,用你最擅长的编程语言写一个提取文本的工具吧。

2.3. 图片提取与转化

BGI 引擎的图片有两种格式:标准 BMP/PNG 和去掉文件头的 BMP。前者用于 CG 和立绘,后者用于系统图片(sysgrp.arc中的图片文件)。
注意:部分图片的像素排序是反转的,保存时也要保持反转排序(在 PS 的保存窗口中勾选翻转行序
注意2:如果图片含有 Alpha 通道,那么 Alpha 通道也要修!

2.3.1. 标准 BMP/PNG 文件

这些文件很好判断。对于 BMP 文件,文件头部总是 BM 两个字母;而对于 PNG 格式,文件头总是 {0x89, 0x50, 0x4E, 0x47}(写成中文的话就是 塒NG)。给解出来的文件添加相应后缀,用 Photoshop 打开即可。

2.3.2. 系统图片

让我们再次祭出十六进制编辑器。

如图,文件的钱六个字节分别是图片宽度,图片高度和色深。可以看出,sys_title_006 图片是一个 1280 * 720 的 32 位图片。
再次使用你最拿手的编程语言,写一个小程序来修复 BMP 文件头吧:请阅读 BeginBuildBMP 函数的代码

至此,资源破解结束。

3. 资源回封

3.1. 游戏脚本

脚本的回封很简单,照着提取工具写一个对应的逆运算就行了。注意,新文件中的汉字必须是 GBK 编码。

3.2. 图片

3.2.1. 标准图片

去掉后缀就行。

3.2.2. 系统图片

刚刚我们添加了 BMP Header,现在我们需要一个去掉 BMP Header 的工具:请阅读 BeginBuildResource 函数的代码

4. 测试运行

上面,我

  • “汉化”了一张 CG (ev_warn02 来自 data02010.arc):

  • 汉化了一张 系统图 (sys_title_006 来自 sysgrp.arc):

  • 汉化了 _01 脚本的前几句话:

把 ev_warn02 的后缀去掉,把 sys_title_006 的文件头去掉,把文本回写到 _01 中,生成新文件再重新命名为 _01。
完成后,把三个文件重命名为原本的名字,复制到游戏目录下。双击游戏,运行。


图片很正常,能用。可是脚本就没这么方便了:

出现了乱码。
这就不得不说字符编码和边界检查了。点击这里这里,仔细阅读肠姐姐的文章。
BGI 引擎中存在多处校验边界的代码,具体可以通过 OllyDbg 查找(CTRL+L)cmp al, 0A0 来定位(《花色ヘプタグラム》中只有两处)。
其它需要修改的地方:

  • CreateFontA 的 fdwCharSet 参数:改为 86 (其他游戏可能会有 CreateFontIndirectA 函数,后者要麻烦一些)
  • MultiByteToWideChar 的 Charset 参数:改为 3A8
  • cmp eax, 0EF40 ; 改为 0FE40
  • cmp ebx, 0EF40 ; 改为 0FE40
  • cmp ebx, 8140 ; 8140 是 Shift-JIS 编码中的全角空格,所以此处应改为 0A1A1 (GBK 的全角空格)

修改后保存主程序,再次运行,一切正常。

5. 后续工作

5.1. 字体

用十六进制编辑器打开刚刚运行成功的主程序,把所有MS 明朝(俵俽 柧挬),MS ゴシック(俵俽 僑僔僢僋),MS Mincho,MS Gothic 改为黑体,或 SimHei,别忘了把空余的字节全填上 0x00。

5.2. 窗口标题

从 system.arc 中得到 ipl._bp,用十六进制编辑器修改字符串。

5.3. (按需)人名乱码,下一句提示符乱码,方括号乱码

从 sysprg.arc 中提取相应的 ._bp 文件,用十六进制编辑器修改。下面的是完成品,补丁质量:

5.4. 补丁的注意事项

既然 BGI 不用封包就能读取汉化后的文件,那么在发补丁时怎么办?打补丁之后,游戏目录不得一下子多出几百个文件么?孩子,你需要 MoleBox

6. 接下来?

如果你觉得自己需要再次学习基本知识,请把这里所有回复大于 2 的主题帖读完。
如果你觉得自己能够挑战更高难度的破解,请点这里
如果你有任何疑问,请留言。