Skip to content

Instantly share code, notes, and snippets.

@psyouloveme
Last active March 1, 2024 21:57
Show Gist options
  • Save psyouloveme/1a349b5d6e8fdb2a48cdfd71d7307d60 to your computer and use it in GitHub Desktop.
Save psyouloveme/1a349b5d6e8fdb2a48cdfd71d7307d60 to your computer and use it in GitHub Desktop.
[WIP] Auto-splitter for Shenmue 2 PC
state("Shenmue2")
{
/***
AutoSplitter Script for Shenmue 2 v1.00 (Steam)
by psyouloveme, with some info from original by Odyssic.
This AutoSplitter uses 5 metrics to check for game progress:
1. Map ID (area)
2. Loading Screen Area Name Text
3. Dialog ID
4. Cutscene Length
5. Money on-hand
With these metrics, you can figure out the conditions for most events in the game.
Where this is lacking is when moving to or from a "sub-area" like a pawnshop or
Collect Antiques. It's possible to read the loading screen text when entering,
but no Map ID change occurs. When exiting, there is not loading screen, so
there's nothing to latch on there. This can be fixed by finding the sub-area entry
addresses, but I haven't found them yet.
Here's a Livesplit file with splits that should line up with the autosplitter:
https://drive.google.com/file/d/1Mfd0oBMVtAHigFwkdwQt5TRgBG6h4VPh
Here's the listing of the splits (at least the ones I expect to happen, fingers crossed):
Disc 1 Splits
1. Cutscene where ryo gets his bag stolen
2. Enter Green Market Qr from Queens St
3. Enter S. Carmain Qr from Green Market Quarter
4. Leave Yan Tin Apts to S. Carmain Qr.
5. Enter Man Mo Temple from Scarlet Hills
6. Item get sound when getting DAN from Barber
7. Get notebook ping from Martial Arts School
8. Cutscene after chopping rock at the mall
9. Item get sound when getting JIE from the Martial Arts School
10. Cutscene after punching the tree
11. Cutscene after Yan Tin Apts fight
12. Enter Man Mo Temple from Scarlet Hills
13. Cutscene after pressing button to interact will wall
14. Enter pawnshop
15. Enter pawnshop
16. Cutscene with Xiuying at Da Yuan Apts
Disc 2 Splits
1. Enter Xiuying's Room (disc change done)
2. Enter Man Mo Temple from Yard (skip work)
3. Cutscene in Library finding Wulinshu
4. Cutscene after catching leaves
5. Notebook ding at Collect Antiques
6. Enter Xiuying's Room (after learning chawan sign)
7. Enter Man Mo Temple from Yard (skip work)
8. Cutscene after fight at the diner
9. Cutscene after CQTE with Yuan
10. Enter Wise Man's Qr from Scarlet Hills (barrier skip)
11. Enter Fortune's Pier from Worker's Pier
12. Money > 500
13. Cutscene after fight at Warehouse No 8
14. Cutscene meeting Cool Z at Beverly Hills Wharf
15. Notebook ding after fight with Heaven's grunts at Beverly Hills Wharf
16. Cutscene after Ren CQTE
Disc 3 Splits
1. Enter Kowloon (disc change done)
2. Enter 1,000 White Qr from Great View Bldg.
3. Enter Yellow Head Bldg F1 from Underground
4. Enter Yellow Head Bldg F5 from Yellow Head Bldg F2
5. Enter Big Ox Bldg from Yellow Head F5
6. Cutscene after Black Suit QTE
7. Cutscene after Dou Niu CQTEs
Disc 4 Splits
1. Enter Guilin (disc change done)
2. Enter Yingshuie after cutscenes saving Shenhua
3. Enter Crag from Deep Green Way (qtes done)
4. Cutscene when reaching the cave (walk done)
5. Cutscene when leaving the next morning (cave done)
6. Enter Big Tree Forest area after River CQTE 1
7. Enter Forest after River CQTE 2
8. Cutscene with the Spider Tree
9. Cutscene at the 5 Color Spring
10. Enter Rocky Area
11. Cutscene after Rocky Area CQTE
12. Enter Shenhua's House area
13. Cutscene when reaching Shenhua's House
14. Enter Cloud Bird Trail (done at shenhua's house)
15. Enter Stone Pit (done at cloud bird)
16. Cutscene at the bottom of the slope at stone pit (made it past shenhua)
17. Final input (check for cutscene and input)
***/
// 0 if in menu, 1 otherwise
// Found by Odyssic
int menu : 0x8205E18;
// Was final input pressed?
// Found by Odyssic
int finalInput : 0x3AB92A4;
// Current money
// Found by Odyssic
int money : 0x81BF058;
// The current SCENE (disc number)
// Should be 1-4 always when in game, idk about otherwise
int scene : 0x8205E14;
// The current area ID
// 4 characters, alphanumeric (i'm pretty sure)
// e.g. S. Carmain Qr. is "WK00"
string4 area : 0x8205E10;
// The current area entry
// 1 byte, indicates which entry to an area you came in from
// referred to by where you exit from.
int entry : 0x8205E0C;
// Area Name (from the loading screen)
// e.g. Worker's Pier
string512 areaName : 0x3BEEC70;
// Sub Area Name (from the loading screen)
// e.g. Wan Chai
string512 subAreaName : 0x3BEF070;
// Dialog ID string. Variable width.
// Assume the rightmost characters could be garbage
string24 dialog: 0x3D84B38;
// Cutscene Location (can, rarely, be different)
// See Area above
string4 csArea : 0x44A2A08;
// Cutscene Scene (unlikely to ever be different)
int csScene : 0x44A2A0C;
// Cutscene Type??? idk what this is
int csType : 0x44A2A10;
// Cutscene length - this is unique enough when paired with an area.
// to narrow further you could use an entry or something too
int csLength: 0x44a2a18;
}
startup {
settings.Add("debug", false, "Enable Debug Logging");
settings.CurrentDefaultParent = "debug";
settings.Add("debug_splits", false, "Log Split Reasons");
settings.Add("debug_area", false, "Log Area IDs");
settings.Add("debug_location", false, "Log Area Names");
settings.Add("debug_cutscene", false, "Log Cuscene Info");
settings.Add("debug_dialog", false, "Log Dialog Filenames");
settings.Add("debug_menu", false, "Log 'Menu' Value");
settings.Add("debug_money", false, "Log Money Changes");
}
init {
// Internal var to track if we've exited green market for the first time
vars.leftGreenMarket = false;
// Internal var to track if we've found the mysterious paper
vars.foundPaper = false;
// Started Dou Niu
vars.douNiu = false;
Func<string, bool> onSplit = (string splitName) => {
if (settings["debug_splits"]) {
if (String.IsNullOrEmpty(splitName)) {
print("Shenmue2ASL: Split: @ " + timer.CurrentTime);
} else {
print("Shenmue2ASL: Split: '" + splitName + "' @ " + timer.CurrentTime);
}
}
return true;
};
vars.split = onSplit;
// cleaned version of the area name
current.areaNameFormatted = "";
// cleaned version of the sub area name
current.subAreaNameFormatted = "";
}
start {
if (old.menu == 0 && current.menu == 1) {
return true;
}
}
split {
// Main Location Diffs
bool entryChanged = old.entry != current.entry;
bool sceneChanged = old.scene != current.scene;
bool areaChanged = old.area != current.area;
bool locationChanged = entryChanged || sceneChanged || areaChanged;
// Cutscene Diffs
bool csAreaChanged = old.csArea != current.csArea;
bool csSceneChanged = old.csScene != current.csScene;
bool csTypeChanged = old.csType != current.csType;
bool csLengthChanged = old.csLength != current.csLength;
bool cutsceneChanged = csTypeChanged || csLengthChanged || (csAreaChanged && !areaChanged) || (csSceneChanged && !sceneChanged);
// Money Diff
bool moneyChanged = old.money != current.money;
// Dialog formatting & diffs
if (current.dialog == null) {
current.dialog = String.Empty;
} else {
current.dialog = System.Text.RegularExpressions.Regex.Replace(current.dialog, @"[^A-Z0-9]", string.Empty);
}
bool dialogChanged = old.dialog != current.dialog;
// Loading formatiting & Screen Diffs
if (!String.IsNullOrEmpty(current.areaName)){
current.areaNameFormatted = current.areaName.Trim(); // Overwrite the area name with the trimmed string
}
if (!String.IsNullOrEmpty(current.subAreaName)){
current.subAreaNameFormatted = current.subAreaName.Trim(); // Overwrite the sub area name with the trimmed string
}
bool areaNameChanged = old.areaNameFormatted != current.areaNameFormatted;
bool subAreaNameChanged = old.subAreaNameFormatted != current.subAreaNameFormatted;
// Debug Logging
if (settings["debug"]) {
if (settings["debug_dialog"] && dialogChanged) {
print("Shenmue2ASL: Dialog: " + current.dialog);
}
if (settings["debug_area"] && (areaNameChanged || subAreaNameChanged)) {
string a = "Area: Moved from '" + old.areaNameFormatted + "'";
a += String.IsNullOrEmpty(old.subAreaNameFormatted) ? "" : "' ('" + old.subAreaNameFormatted + "')";
a += " to '" + current.areaNameFormatted + "'";
a += String.IsNullOrEmpty(current.subAreaNameFormatted) ? "" : " ('" + current.subAreaNameFormatted + "')";
print("Shenmue2ASL: " + a);
}
if (settings["debug_cutscene"] && cutsceneChanged) {
print("Shenmue2ASL: Cutscene: "
+ "Area: " + current.csArea
+ " | Scene: 0x" + current.csScene.ToString("x2") + " (" + current.csScene + ")"
+ " | ?????: 0x" + current.csType.ToString("x2") + " (" + current.csType + ")"
+ " | Length: 0x" + current.csLength.ToString("x4") + " (" + current.csLength + ")");
}
if (settings["debug_location"] && locationChanged) {
print("Shenmue2ASL: Area: From: Scene: 0x" + old.scene.ToString("x2")
+ " | Map: " + old.area
+ " | Entry: 0x" + old.entry.ToString("x2"));
print("Shenmue2ASL: Area: To: Scene: 0x" + current.scene.ToString("x2")
+ " | Map: " + current.area
+ " | Entry: 0x" + current.entry.ToString("x2"));
}
if (settings["debug_menu"] && old.menu != current.menu) {
print("Shenmue2ASL: Menu: Changed from 0x" + old.menu.ToString("x4") + " to 0x" + current.menu.ToString("x4"));
}
if (settings["debug_money"] && moneyChanged) {
print("Shenmue2ASL: Money: Changed from " + old.money + " to " + current.money);
}
}
/*
TODO:
big or small misfires when reloading saves
find a better way to start timer
*/
/* Disc 1 */
if (current.scene == 1) {
/*
Disc 1 Splits
1. Cutscene where ryo gets his bag stolen
2. Enter Green Market Qr from Queens St
3. Enter S. Carmain Qr from Green Market Quarter
4. Leave Yan Tin Apts to S. Carmain Qr.
5. Enter Man Mo Temple from Scarlet Hills
6. Item get sound when getting DAN from Barber
7. Get notebook ping from Martial Arts School
8. Cutscene after chopping rock at the mall
9. Item get sound when getting JIE from the Martial Arts School
10. Cutscene after punching the tree
11. Cutscene after Yan Tin Apts fight
12. Enter Man Mo Temple from Scarlet Hills
13. Cutscene after pressing button to interact will wall
14. Enter pawnshop
15. Enter pawnshop
16. Cutscene with Xiuying at Da Yuan Apts
*/
/* Worker's Pier */
if (current.area == "AR02" && csLengthChanged && current.csLength == 0x03e0) return vars.split("Bag steal cutscene"); // Worker's Pier Bag Steal Cutscene
/* Green Market Qr */
if (current.area == "WS00") {
if (areaChanged) {
if (current.entry == 0x01) return vars.split("Left Queen's St."); // Exit Queen's St.
}
if (dialogChanged) {
if (old.dialog.StartsWith("F2130B032") && current.dialog.StartsWith("JINGURU03")) return vars.split("Talked to Zhoushan"); // Talked to Zhoushan
if (current.dialog.StartsWith("JINGURU01")) return vars.split("Got YI"); // From Golden Qr, item get sound on Yi
}
}
/* Golden Shopping Mall */
if (current.area == "WESM"){
if (csLengthChanged && current.csLength == 0x0104) return vars.split("Rock chopped"); // rock chopped
}
/* S Carmain Qr */
if (current.area == "WK00") {
if (areaChanged) {
// From Green Market Qr. the first time
// also take this entrance after the martial arts school
if (!vars.leftGreenMarket && current.entry == 0x01) {
vars.leftGreenMarket = true;
return vars.split("Left Green Market");
}
if (current.entry == 0x61) return vars.split("Left Yan Tin Apts."); // From Yan Tin Apts.
}
if (csLengthChanged) {
if (current.csLength == 0x06d6) return vars.split("Punched the tree"); // Tree punch done
if (current.csLength == 0x09c4) return vars.split("Yan Tin fight done"); // Yan Tin Fight done
}
}
/* Lucky Charm Qr */
if (current.area == "WN00") {
if (dialogChanged) {
if (current.dialog.StartsWith("JINGURU01")) return vars.split("Got DAN"); // Item get sound effect for Dan
}
}
/* Man Mo Temple */
if (current.area == "WB01") {
if (areaChanged) return vars.split("Entered Man Mo Temple"); // first time entering, after glitching in
if (csLengthChanged) {
if (current.csLength == 0x0260) return vars.split("Wiped the wall"); // Wall Wipe cutscene
}
}
/* Wise Man's Qr */
if (current.area == "WT00") {
if (areaNameChanged && current.entry != 0x04 && current.areaNameFormatted.Contains("Pawnshop")) return vars.split("Entered a pawnshop"); // Enter a pawnshop (but not after leaving lucky charm qr)
}
/* Da Yuan Apartments */
if (current.area == "WTA0" && areaChanged && current.entry == 0x01) return vars.split("Entered Da Yuan Apts."); // Exit Wise Man's Qr.
}
// Disc 2
else if (current.scene == 2) {
/*
Disc 2 Splits
1. Enter Xiuying's Room (disc change done)
2. Enter Man Mo Temple from Yard (skip work)
3. Cutscene in Library finding Wulinshu
4. Cutscene after catching leaves
5. Notebook ding at Collect Antiques
6. Enter Xiuying's Room (after learning chawan sign)
7. Enter Man Mo Temple from Yard (skip work)
8. Cutscene after fight at the diner
9. Cutscene after CQTE with Yuan
10. Enter Wise Man's Qr from Scarlet Hills (barrier skip)
11. Enter Fortune's Pier from Worker's Pier
12. Money > 500
13. Cutscene after fight at Warehouse No 8
14. Cutscene meeting Cool Z at Beverly Hills Wharf
15. Notebook ding after fight with Heaven's grunts at Beverly Hills Wharf
16. Cutscene after Ren CQTE
*/
if (current.menu == 1 && old.money < 510 && current.money >= 510) {
return vars.split("Money >= $510");
}
/* Da Yuan Apartments */
if (current.area == "WTA0") {
if (sceneChanged) return vars.split("Scene changed to 2"); // From disc 1
if (areaChanged) {
if (current.entry == 0x28) return vars.split("Entered Da Yuan Apts"); // From chawan sign
}
}
/* Man Mo Temple */
if (current.area == "WB01" && areaChanged) return vars.split("Entered Man Mo Temple"); // From Yard
/* Scarlet Hills */
if (current.area == "WB00") {
if (csLengthChanged) {
if (current.csLength == 0x0212){
vars.foundPaper = true;
return vars.split("Found Wulinshu"); // Found Wulinshu
}
if (current.csLength == 0x0640) return vars.split("Caught leaves"); // Leaves Caught
}
}
/* Wise Man's Qr */
if (current.area == "WT00") {
if ((current.entry == 0x02) || (current.entry == 0x03)) {
if (areaChanged && old.entry == 0x21) return vars.split("Barrier skipped"); // From Scarlet Hills Barrier Skip Done
if (vars.foundPaper && dialogChanged && old.dialog.StartsWith("H2090A009") && current.dialog.StartsWith("JINGURU03")){
vars.foundPaper = false;
return vars.split("Info from Collect Antiques"); // Talked to the guy at collect antiques
}
}
}
/* Dou Jiang Diner */
if (current.area == "TOFB" && csLengthChanged && current.csLength == 0x267) return vars.split("Dou Jiang Diner fight done"); // After fight cutscene
/* Yuan QTE */
if (current.area == "YTQB" && csLengthChanged && current.csLength == 0x0758) return vars.split("Yuan CQTE done"); // After Yuan QTE
/* Fortune's pier */
if (current.area == "AK00") {
if (areaChanged && current.entry == 0x01 && old.area == "AR02") return vars.split("Entered worker's pier"); // Enter from Worker's Pier
// if (csLengthChanged && current.csLength == 0x016B) return vars.split("Start Warehouse No. 8 fight"); // Start warehouse No. 8 fight
}
/* Worker's pier - unwinnable fight */
if (current.area == "ABLB" && areaChanged && current.entry == 0x00) return vars.split("Warehouse No. 8 fight done"); // From Warehouse No. 8 Fight
/* Beverly Hills Wharf */
if (current.area == "AB00") {
if (csLengthChanged) {
if (current.csLength == 0x0578) return vars.split("Cool Z cutscene"); // Cool J/Z Cutscene
}
if (dialogChanged) {
if (old.dialog.StartsWith("0052B018") && current.dialog.StartsWith("JINGURU03")){
return vars.split("Learned Ren's location"); // Learned where ren is
}
}
}
/* Lucky Plaza */
if (current.area == "CB00" && csLengthChanged && current.csLength == 0x0088) return vars.split("Ren CQTE done"); // Post-Ren QTE Cutscene
}
else if (current.scene == 0x03) {
/*
Disc 3 Splits
1. Enter Kowloon (disc change done)
2. Enter 1,000 White Qr from Great View Bldg.
3. Enter Yellow Head Bldg F1 from Underground
4. Enter Yellow Head Bldg F5 from Yellow Head Bldg F2
5. Enter Big Ox Bldg from Yellow Head F5
6. Cutscene after Black Suit QTE
7. Cutscene after Dou Niu CQTEs
*/
/* Kowloon */
if (sceneChanged) return vars.split("Scene changed to 3"); // From Disc 2
/* Thousand White Qr */
if (current.area == "Q100" && areaChanged && current.entry == 0x07) return vars.split("Left Great View Bldg."); // From Great View Bldg.
/* Yellow Head Building (F1) */
if (current.area == "QF01" && areaChanged && old.area == "QUG0") return vars.split("Entered Yellow Head Bldg."); // From Underground
/* Yellow Head Building (F5) */
if (current.area == "QF00" && areaChanged && old.area == "QF02") return vars.split("Entered Yellow Head floor 5"); // From Floor 4 Stairs
/* Big Ox (F40) */
if (current.area == "QF39" && areaChanged) return vars.split("Entered Big Ox Bldg."); // From Floor 39 Stairs
/* Bix Ox Roof */
if (current.area == "QFRR") {
if (csLengthChanged) {
if (current.csLength == 0x15c2 && !vars.douNiu) {
vars.douNiu = true;
return vars.split("Black suit QTE done"); // Pre Dou-Niu Cutscene
}
if (current.csLength == 0x02bc) return vars.split("Dou Niu CQTES done"); // Post Dou-Niu Cutscene
}
}
}
else if (current.scene == 0x04) {
/*
Disc 4 Splits
1. Enter Guilin (disc change done)
2. Enter Yingshuie after cutscenes saving Shenhua
3. Enter Crag from Deep Green Way (qtes done)
4. Cutscene when reaching the cave (walk done)
5. Cutscene when leaving the next morning (cave done)
6. Enter Big Tree Forest area after River CQTE 1
7. Enter Forest after River CQTE 2
8. Cutscene with the Spider Tree
9. Cutscene at the 5 Color Spring
10. Enter Rocky Area
11. Cutscene after Rocky Area CQTE
12. Enter Shenhua's House area
13. Cutscene when reaching Shenhua's House
14. Enter Cloud Bird Trail (done at shenhua's house)
15. Enter Stone Pit (done at cloud bird)
16. Cutscene at the bottom of the slope at stone pit (made it past shenhua)
17. Final input (check for cutscene)
*/
/* Guilin */
if (sceneChanged) return vars.split("Scene changed to 4"); // From Disc 3
/* Yingshuihe */
if (areaChanged && current.area == "KRM3") return vars.split("Entered Yingshuie with Shenhua"); // Met shenhua
/* Crag */
if (current.area == "KRC1") {
if (areaChanged && old.area == "KWQA") return vars.split("Left Deep Green Way"); // From Deep Green Way
if (csLengthChanged) {
if (current.csLength == 0x0c4e) return vars.split("Reach the cave"); // Reach the Cave
if (current.csLength == 0x0758) return vars.split("Morning after cave"); // The Next Day
}
}
/* Forest */
if (current.area == "KMZ2") {
if (areaChanged && old.area == "KRK2") return vars.split("Left River CQTE 1"); // finished river qte 1
}
/* Forest */
if (current.area == "KMZ3") {
if (areaChanged && old.area == "KRK4") return vars.split("Left River CQTE 2"); // finished river qte 2
if (csLengthChanged) {
if (current.csLength == 0x05dc) return vars.split("Spider tree cutscene"); // spider tree
if (current.csLength == 0x0767) return vars.split("Five colors spring cutscene"); // five colors
}
}
/* Rocky Area */
if (current.area == "KRK6") {
if (areaChanged) return vars.split("Entered Rocky Area"); // entered rocky area
if (csLengthChanged && current.csLength == 0x01f4) return vars.split("Rocky Area CQTE cutscene"); // finished qte
}
/* Shenhua's House */
if (current.area == "KSH1") {
if (areaChanged) return vars.split("Entered Shenhua's House area"); // entered area
if (csLengthChanged && current.csLength == 0x53c) return vars.split("Shenhuas's House cutscene"); // entered house
}
/* Cloud Bird Trail */
if (current.area == "KWW4") {
if (areaChanged) return vars.split("Entered Cloud Bird Trail"); // entered area
}
/* Stone Pit */
if (current.area == "KES1") {
if (areaChanged) return vars.split("Entered Stone Pit"); // entered area
if (csLengthChanged && current.csLength == 0x048d) return vars.split("First Stone Pit cutscene"); // got past shenhua
/* Final Input */
if (current.entry == 0x35 && current.csLength == 0x0889 && old.finalInput == 1 && current.finalInput == 0) return vars.split("Final input");
}
}
return false;
}
reset {
// if (current.gameLaunch == 1) {
// return vars.split(null);
// }
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment