state("b1-Win64-Shipping"){} state("b1-WinGDK-Shipping"){} startup { string componentDir = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Components"); Assembly.Load(File.ReadAllBytes(Path.Combine(componentDir, "asl-help"))).CreateInstance("Basic"); vars.Helper.Settings.CreateFromXml(Path.Combine(componentDir, "BMWukong.Settings.xml")); vars.Helper.GameName = "Black Myth: Wukong (2024)"; } init { IntPtr gEngine = vars.Helper.ScanRel(3, "48 89 05 ???????? 48 85 c9 74 ?? e8 ???????? 48 8d 4d"); IntPtr Loading = vars.Helper.ScanRel(3, "48 89 35 ???????? 48 89 74 24 ?? e8 ???????? 48 8b 4c 24 ?? 8d 7e"); vars.Helper["Level"] = vars.Helper.MakeString(gEngine, 0x910, 0x24); vars.Helper["Level"].FailAction = MemoryWatcher.ReadFailAction.SetZeroOrNull; vars.Helper["localPlayer"] = vars.Helper.Make(gEngine, 0xDB8, 0x38, 0x0, 0x30); vars.Helper["localPlayer"].FailAction = MemoryWatcher.ReadFailAction.SetZeroOrNull; vars.Helper["isLoading"] = vars.Helper.Make(Loading, 0x240); vars.completedSplits = new HashSet(); vars.unlockedBosses = new HashSet(); vars.deadBosses = new HashSet(); vars.pendingBossSplits = new Queue(); vars.savePath = null; vars.lastSaveWrite = 0L; vars.nextSaveSearch = 0L; vars.nextSaveLog = 0L; vars.gameExePath = ""; vars.saveReady = false; // Minimal Protobuf reader. The game stores the archive payload in field 2 // of the outer ArchiveFile message and XORs it with this 8-byte key. vars.ReadVarint = (Func) ((buf, ptr) => { int value = 0; int shift = 0; while (true) { byte b = buf[ptr[0]++]; value |= (b & 0x7F) << shift; if ((b & 0x80) == 0) return value; shift += 7; } }); vars.GetMessageField = (Func) ((buf, targetField) => { if (buf == null) return null; int[] pos = new int[] { 0 }; while (pos[0] < buf.Length) { int key = ((Func)vars.ReadVarint)(buf, pos); int field = key >> 3; int wire = key & 7; if (wire == 2) { int len = ((Func)vars.ReadVarint)(buf, pos); if (field == targetField) { byte[] value = new byte[len]; Array.Copy(buf, pos[0], value, 0, len); return value; } pos[0] += len; } else if (wire == 0) { ((Func)vars.ReadVarint)(buf, pos); } else if (wire == 1) { pos[0] += 8; } else if (wire == 5) { pos[0] += 4; } else { return null; } } return null; }); vars.ExtractPayload = (Func) ((bytes) => { if (bytes == null) return null; int[] pos = new int[] { 0 }; while (pos[0] < bytes.Length) { int key = ((Func)vars.ReadVarint)(bytes, pos); int field = key >> 3; int wire = key & 7; if (wire == 2) { int len = ((Func)vars.ReadVarint)(bytes, pos); if (field == 2) { byte[] payload = new byte[len]; Array.Copy(bytes, pos[0], payload, 0, len); byte[] keyBytes = new byte[] { 0x7B, 0x5C, 0xDA, 0x91, 0x3E, 0xFC, 0xDA, 0x37 }; for (int i = 0; i < payload.Length; i++) { payload[i] ^= keyBytes[i % keyBytes.Length]; } return payload; } pos[0] += len; } else if (wire == 0) { ((Func)vars.ReadVarint)(bytes, pos); } else if (wire == 1) { pos[0] += 8; } else if (wire == 5) { pos[0] += 4; } else { return null; } } return null; }); // Path: FUStBEDArchivesData(1) -> RoleData(1) -> RoleDataCS(8) // -> RoleCollection / repeated MonsterCollectionList(1) // -> MonsterCollection{ Id(1), PortraitStatus(4) } vars.GetUnlockedBosses = (Func>) ((data) => { HashSet result = new HashSet(); if (data == null) return result; byte[] roleData = ((Func)vars.GetMessageField)(data, 1); if (roleData == null) return result; byte[] roleCs = ((Func)vars.GetMessageField)(roleData, 1); if (roleCs == null) return result; byte[] collection = ((Func)vars.GetMessageField)(roleCs, 8); if (collection == null) return result; int[] pos = new int[] { 0 }; while (pos[0] < collection.Length) { int key = ((Func)vars.ReadVarint)(collection, pos); int field = key >> 3; int wire = key & 7; if (wire == 2) { int len = ((Func)vars.ReadVarint)(collection, pos); if (field == 1) { byte[] monster = new byte[len]; Array.Copy(collection, pos[0], monster, 0, len); int monsterId = 0; bool portraitUnlocked = false; int[] monsterPos = new int[] { 0 }; while (monsterPos[0] < monster.Length) { int monsterKey = ((Func)vars.ReadVarint)(monster, monsterPos); int monsterField = monsterKey >> 3; int monsterWire = monsterKey & 7; if (monsterWire == 0) { int value = ((Func)vars.ReadVarint)(monster, monsterPos); if (monsterField == 1) monsterId = value; } else if (monsterWire == 2) { int valueLen = ((Func)vars.ReadVarint)(monster, monsterPos); if (monsterField == 4 && valueLen > 0) portraitUnlocked = true; monsterPos[0] += valueLen; } else if (monsterWire == 1) { monsterPos[0] += 8; } else if (monsterWire == 5) { monsterPos[0] += 4; } else { break; } } if (monsterId != 0 && portraitUnlocked) result.Add(monsterId); } else { pos[0] += len; } } else if (wire == 0) { ((Func)vars.ReadVarint)(collection, pos); } else if (wire == 1) { pos[0] += 8; } else if (wire == 5) { pos[0] += 4; } else { break; } } return result; }); // Path: FUStBEDArchivesData(2) -> LevelArchiveData(1) // -> LevelBaseData(3) -> DeadUnitData{ Uid(1), ResetType(2) } vars.GetDeadBosses = (Func>) ((data) => { HashSet result = new HashSet(); if (data == null) return result; byte[] levelData = ((Func)vars.GetMessageField)(data, 2); if (levelData == null) return result; int[] p = new int[] { 0 }; while (p[0] < levelData.Length) { int key = ((Func)vars.ReadVarint)(levelData, p); int field = key >> 3; int wire = key & 7; if (wire == 2) { int len = ((Func)vars.ReadVarint)(levelData, p); if (field == 1) { byte[] levelBase = new byte[len]; Array.Copy(levelData, p[0], levelBase, 0, len); int[] q = new int[] { 0 }; while (q[0] < levelBase.Length) { int subKey = ((Func)vars.ReadVarint)(levelBase, q); int subField = subKey >> 3; int subWire = subKey & 7; if (subWire == 2) { int subLen = ((Func)vars.ReadVarint)(levelBase, q); if (subField == 3) { byte[] deadUnit = new byte[subLen]; Array.Copy(levelBase, q[0], deadUnit, 0, subLen); string uid = null; int resetType = 0; int[] r = new int[] { 0 }; while (r[0] < deadUnit.Length) { int deadKey = ((Func)vars.ReadVarint)(deadUnit, r); int deadField = deadKey >> 3; int deadWire = deadKey & 7; if (deadWire == 0) { int deadValue = ((Func)vars.ReadVarint)(deadUnit, r); if (deadField == 2) resetType = deadValue; } else if (deadWire == 2) { int deadLen = ((Func)vars.ReadVarint)(deadUnit, r); if (deadField == 1) uid = System.Text.Encoding.UTF8.GetString(deadUnit, r[0], deadLen); r[0] += deadLen; } else if (deadWire == 1) { r[0] += 8; } else if (deadWire == 5) { r[0] += 4; } else { break; } } if (!string.IsNullOrEmpty(uid) && (resetType == 1 || resetType == 2) && uid.StartsWith("UGuid.")) { result.Add(uid); } } else { q[0] += subLen; } } else if (subWire == 0) { ((Func)vars.ReadVarint)(levelBase, q); } else if (subWire == 1) { q[0] += 8; } else if (subWire == 5) { q[0] += 4; } else { break; } } } else { p[0] += len; } } else if (wire == 0) { ((Func)vars.ReadVarint)(levelData, p); } else if (wire == 1) { p[0] += 8; } else if (wire == 5) { p[0] += 4; } else { break; } } return result; }); vars.Log = (Action) ((message) => { try { string logDirectory = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Logs"); Directory.CreateDirectory(logDirectory); File.AppendAllText(Path.Combine(logDirectory, "BlackMythWukong.log"), DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff") + " " + message + Environment.NewLine); } catch { } }); ((Action)vars.Log)("init: BlackMythWukong save splitter loaded"); vars.FindSavePath = (Func) ((gameExePath) => { try { System.Collections.Generic.List roots = new System.Collections.Generic.List(); string localRoot = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "b1", "Saved", "SaveGames"); if (Directory.Exists(localRoot)) roots.Add(localRoot); if (!string.IsNullOrEmpty(gameExePath)) { string exeDir = Path.GetDirectoryName(gameExePath); if (!string.IsNullOrEmpty(exeDir)) { string savedDir = Path.GetFullPath(Path.Combine(exeDir, "..", "..", "Saved")); string gameRoot = Path.Combine(savedDir, "SaveGames"); if (Directory.Exists(gameRoot)) roots.Add(gameRoot); } } string[] fallbackRoots = new string[] { @"E:\SteamLibrary\steamapps\common\BlackMythWukong\b1\Saved\SaveGames", @"C:\Program Files (x86)\Steam\steamapps\common\BlackMythWukong\b1\Saved\SaveGames" }; foreach (string fallbackRoot in fallbackRoots) { if (Directory.Exists(fallbackRoot)) roots.Add(fallbackRoot); } string best = null; DateTime bestTime = DateTime.MinValue; foreach (string root in roots) { string[] files = Directory.GetFiles(root, "ArchiveSaveFile.*.sav", SearchOption.AllDirectories); foreach (string file in files) { DateTime fileTime = File.GetLastWriteTimeUtc(file); if (best == null || fileTime > bestTime) { best = file; bestTime = fileTime; } } } return best; } catch (Exception ex) { ((Action)vars.Log)("FindSavePath error: " + ex.Message); return null; } }); } update { vars.Helper.Update(); vars.Helper.MapPointers(); if (!string.IsNullOrEmpty(vars.savePath) && !File.Exists(vars.savePath)) { // game is rewriting the save momentarily: keep the path, just retry soon vars.nextSaveSearch = 0L; vars.lastSaveWrite = 0L; } if (DateTime.UtcNow.Ticks >= ((long)vars.nextSaveSearch)) { string gameExePath = ""; try { if (game != null) gameExePath = game.MainModule.FileName; } catch { } if (!string.IsNullOrEmpty(gameExePath)) vars.gameExePath = gameExePath; string detectedSavePath = ((Func)vars.FindSavePath)(gameExePath); vars.nextSaveSearch = DateTime.UtcNow.AddSeconds(10).Ticks; if (!string.IsNullOrEmpty(detectedSavePath) && detectedSavePath != vars.savePath) { ((Action)vars.Log)("save path: " + detectedSavePath); vars.savePath = detectedSavePath; vars.saveReady = false; vars.lastSaveWrite = 0L; vars.unlockedBosses = new HashSet(); vars.deadBosses = new HashSet(); ((Queue)vars.pendingBossSplits).Clear(); } else if (string.IsNullOrEmpty(detectedSavePath) && DateTime.UtcNow.Ticks >= ((long)vars.nextSaveLog)) { ((Action)vars.Log)("save path not found. gameExePath=" + gameExePath); vars.nextSaveLog = DateTime.UtcNow.AddMinutes(1).Ticks; } } if (!string.IsNullOrEmpty(vars.savePath)) { try { long writeTicks = File.GetLastWriteTimeUtc(vars.savePath).Ticks; if (writeTicks != ((long)vars.lastSaveWrite)) { byte[] saveBytes = File.ReadAllBytes(vars.savePath); byte[] payload = ((Func)vars.ExtractPayload)(saveBytes); HashSet unlocked = ((Func>)vars.GetUnlockedBosses)(payload); HashSet deadBosses = ((Func>)vars.GetDeadBosses)(payload); if (unlocked != null && deadBosses != null) { if (!((bool)vars.saveReady)) { vars.unlockedBosses = unlocked; vars.deadBosses = deadBosses; vars.saveReady = true; } else { HashSet oldUnlocked = (HashSet)vars.unlockedBosses; foreach (int bossId in unlocked) { if (!oldUnlocked.Contains(bossId)) { string settingId = "Boss_" + bossId; bool enabled = settings.ContainsKey(settingId) && settings[settingId]; ((Action)vars.Log)("new portrait unlock: " + bossId + " enabled=" + enabled); if (enabled) { ((Queue)vars.pendingBossSplits).Enqueue(settingId); } } } HashSet oldDeadBosses = (HashSet)vars.deadBosses; foreach (string uid in deadBosses) { if (!oldDeadBosses.Contains(uid)) { string settingId = "Dead_" + uid; bool enabled = settings.ContainsKey(settingId) && settings[settingId]; ((Action)vars.Log)("new dead-unit: " + uid + " setting=" + settingId + " enabled=" + enabled); if (enabled && vars.completedSplits.Add(settingId)) { ((Queue)vars.pendingBossSplits).Enqueue(settingId); } } } vars.unlockedBosses = unlocked; vars.deadBosses = deadBosses; } } vars.lastSaveWrite = writeTicks; } } catch (System.IO.FileNotFoundException) { } catch (System.IO.DirectoryNotFoundException) { } catch (Exception ex) { ((Action)vars.Log)("update error: " + ex.Message); } } } onStart { // Keep the timer at 0.00 and treat the current save state as the baseline. timer.IsGameTimePaused = true; vars.completedSplits.Clear(); vars.pendingBossSplits = new Queue(); vars.unlockedBosses = new HashSet(); vars.deadBosses = new HashSet(); vars.saveReady = false; if (!string.IsNullOrEmpty(vars.savePath)) { try { byte[] saveBytes = File.ReadAllBytes(vars.savePath); byte[] payload = ((Func)vars.ExtractPayload)(saveBytes); HashSet currentUnlocked = ((Func>)vars.GetUnlockedBosses)(payload); HashSet currentDead = ((Func>)vars.GetDeadBosses)(payload); vars.unlockedBosses = currentUnlocked; vars.deadBosses = currentDead; vars.saveReady = true; ((Action)vars.Log)("onStart baseline portraits=" + currentUnlocked.Count + " deadUnits=" + currentDead.Count); } catch (Exception ex) { ((Action)vars.Log)("onStart error: " + ex.Message); } } } start { return (current.Level == "HFS01/HFS01_PersistentLevel" || current.Level == "MGD/MGD_PersistentLevel") && current.isLoading == 0 && old.isLoading != 0; } split { Queue pending = (Queue)vars.pendingBossSplits; while (pending.Count > 0) { string bossSetting = pending.Dequeue(); if (settings.ContainsKey(bossSetting) && settings[bossSetting]) { ((Action)vars.Log)("boss split: " + bossSetting); return true; } } // All boss triggers are now config-driven through the pending queue. return false; } isLoading { return current.isLoading != 0 || current.localPlayer == null || current.Level == "Startup/Startup_V2_P"; } exit { // Pauses the timer if the game crashes. timer.IsGameTimePaused = true; }