state("maxpayne") { int onLoadScreen : 0x4A6400, 0x80, 0xB4; int viewingComic : "e2mfc.dll", 0x651DC; int inCutscene : 0x4B5080; int level : 0x4B1370; int lastLevelComplete : 0x4B408C, 0x154; int tutorialStatus : 0x4B3E7C; float playerX : 0x4B08C0; float playerY : 0x4B08C4; float playerZ : 0x4B08C8; float nymGameTime : 0x4B709C, 0x770; } startup { vars.EPSILON = 0.0003; vars.START_POSITION_RANGE = 0.05; vars.NYM_SAVE_LOAD_PENALTY = 5; //5 second penalty per save loaded for NYM runs vars.LEVELS_START_POS = new Dictionary<int, Tuple<double, double, double>>() { {651, new Tuple<double, double, double>(-1.27900, -0.51500, -13.25000)}, //P1C0 {1608, new Tuple<double, double, double>(-4.15398, -0.01500, 2.79962)}, //P1C1 {1260, new Tuple<double, double, double>(-8.13992, -1.81498, 11.34619)}, //P1C2 {2094, new Tuple<double, double, double>(5.72900, -1.51565, -0.26233)}, //P1C3 {1656, new Tuple<double, double, double>(-0.12500, 3.85000, 1.65400)}, //P1C4 {1338, new Tuple<double, double, double>(-34.00000, -11.01500, -12.02900)}, //P1C5 {1254, new Tuple<double, double, double>(8.75107, -2.98375, -19.39504)}, //P1C6 {1344, new Tuple<double, double, double>(-13.5, 4.01400, -21.75000)}, //P1C7 {1347, new Tuple<double, double, double>(3.84185, -1.51500, 4.26432)}, //P1C8 {234, new Tuple<double, double, double>(-4.00000, 1.46600, -11.75000)}, //P1C9 {417, new Tuple<double, double, double>(-1.00000, -0.01500, -0.27900)}, //P2C0 {1179, new Tuple<double, double, double>(1.75007, -2.01500, 6.40532)}, //P2C1 {1086, new Tuple<double, double, double>(-5.37386, -6.01500, -8.34099)}, //P2C2 {897, new Tuple<double, double, double>(7.74934, -4.01500, -15.27830)}, //P2C3 {1581, new Tuple<double, double, double>(-4.52900, -1.01500, -12.00000)}, //P2C4 {1620, new Tuple<double, double, double>(-3.3415, 3.11000, -0.37500)}, //P2C5 {714, new Tuple<double, double, double>(-0.58702, -12.01500, 0.25898)}, //P3C0 {1146, new Tuple<double, double, double>(18.44053, 8.27849, 22.06493)}, //P3C1 {1920, new Tuple<double, double, double>(2.40400, -0.01500, -6.375)}, //P3C2 {2781, new Tuple<double, double, double>(0.75, -0.01500, 0.77900)}, //P3C3 {1209, new Tuple<double, double, double>(-17.49977, -2.51500, 27.77921)}, //P3C4 {1401, new Tuple<double, double, double>(13.74994, -1.48600, 8.68750)}, //P3C5 {1014, new Tuple<double, double, double>(-13.40400, -1.01500, -1.7500)}, //P3C6 {1110, new Tuple<double, double, double>(3.62406, -6.37906, -0.00000)}, //P3C7 {1374, new Tuple<double, double, double>(7.52900, -31.01500, -1.00000)}, //P3C8 {531, new Tuple<double, double, double>(-0.17405, -5.51500, 14.68526)}, //Tutorial {402, new Tuple<double, double, double>(3.34600, -1.51500, 3.7500)} //Secret Finale }; vars.LEVEL_NUMS = new int[27] {651, 1608, 1260, 2094, 1656, 1338, 1254, 1344, 1347, 234, 417, 1179, 1086, 897, 1581, 1620, 714, 1146, 1920, 2781, 1209, 1401, 1014, 1110, 1374, 531, 402}; settings.Add("nymRunMode", false, "NYM Run Mode"); settings.SetToolTip("nymRunMode", "Times the current run using the in-game New York Minute timer."); settings.Add("nymSaveLoadPenalty", true, "Apply Loading Penalty", "nymRunMode"); settings.SetToolTip("nymSaveLoadPenalty", "On by default, as the loading penalty is applied to all runs on speedrun.com. A 5-second penalty is applied each time a save is loaded by the runner. But you can turn it off to match the raw in-game New York Minute timer for testing."); settings.Add("ilRunMode", false, "IL Run Mode"); settings.SetToolTip("ilRunMode", "Starts the timer at the start of any level. Stops the timer when the level has been completed (autostop is not supported for Secret Finale)."); } init { vars.playerInStartPosition = false; vars.resetValid = false; vars.nextLevelIndex = 1; vars.autoEndDone = false; vars.totalTimePenalty = 0; vars.tutorialStatusIncrementedCount = 0; //necessary to ensure that the timer will start correctly if the runner starts Livesplit while already loaded into a level if (current.level > 0) { // defines the coordinate range of Max's starting position for the current level // (to deal with float inaccuracies and slight differences in position due to inputs on the loading screen affecting Max's starting position) vars.startPositionX = new Tuple<double, double>(vars.LEVELS_START_POS[current.level].Item1 - vars.START_POSITION_RANGE, vars.LEVELS_START_POS[current.level].Item1 + vars.START_POSITION_RANGE); vars.startPositionY = new Tuple<double, double>(vars.LEVELS_START_POS[current.level].Item2 - vars.START_POSITION_RANGE, vars.LEVELS_START_POS[current.level].Item2 + vars.START_POSITION_RANGE); vars.startPositionZ = new Tuple<double, double>(vars.LEVELS_START_POS[current.level].Item3 - vars.START_POSITION_RANGE, vars.LEVELS_START_POS[current.level].Item3 + vars.START_POSITION_RANGE); } else { vars.startPositionX = new Tuple<double, double>(0, 0); vars.startPositionY = new Tuple<double, double>(0, 0); vars.startPositionZ = new Tuple<double, double>(0, 0); } } update { // almost every time you kill an enemy in the tutorial, the so-called tutorial status will increment... // we can use this to tell when the tutorial is complete, since killing enemies is the main and final goal // note: interacting with the enemy dispenser and painkillers in the subway entrance also increments the tutorial status if (current.level == vars.LEVEL_NUMS[25] && current.tutorialStatus == old.tutorialStatus + 1) { vars.tutorialStatusIncrementedCount += 1; } if (current.level > 0 && current.level != old.level) { // defines the coordinate range of Max's starting position for the current level // (to deal with float inaccuracies and slight differences in position due to inputs on the loading screen affecting Max's starting position) vars.startPositionX = new Tuple<double, double>(vars.LEVELS_START_POS[current.level].Item1 - vars.START_POSITION_RANGE, vars.LEVELS_START_POS[current.level].Item1 + vars.START_POSITION_RANGE); vars.startPositionY = new Tuple<double, double>(vars.LEVELS_START_POS[current.level].Item2 - vars.START_POSITION_RANGE, vars.LEVELS_START_POS[current.level].Item2 + vars.START_POSITION_RANGE); vars.startPositionZ = new Tuple<double, double>(vars.LEVELS_START_POS[current.level].Item3 - vars.START_POSITION_RANGE, vars.LEVELS_START_POS[current.level].Item3 + vars.START_POSITION_RANGE); } vars.playerInStartPosition = current.level > 0 && (settings["ilRunMode"] || current.level == vars.LEVEL_NUMS[0]) && current.onLoadScreen == 0 && current.viewingComic == 0 && current.inCutscene == 0 && current.playerX >= vars.startPositionX.Item1 && current.playerX <= vars.startPositionX.Item2 && current.playerY >= vars.startPositionY.Item1 && current.playerY <= vars.startPositionY.Item2 && current.playerZ >= vars.startPositionZ.Item1 && current.playerZ <= vars.startPositionZ.Item2; if (!vars.playerInStartPosition) { vars.resetValid = true; } return true; } start { if (vars.playerInStartPosition) { return true; } } onStart { vars.autoEndDone = false; vars.tutorialStatusIncrementedCount = 0; } reset { if (vars.resetValid && vars.playerInStartPosition) { return true; } } onReset { vars.resetValid = false; vars.autoEndDone = false; vars.tutorialStatusIncrementedCount = 0; vars.nextLevelIndex = 1; vars.totalTimePenalty = 0; } split { //see if the conditions are met to split for an IL run, there is a special case in this for the Tutorial bool shouldSplit = !vars.autoEndDone && settings["ilRunMode"] && (current.level == vars.LEVEL_NUMS[25] && vars.tutorialStatusIncrementedCount == 8 || current.level > 0 && current.level != old.level); //determine if the timer should split during a full game run if (!shouldSplit) { shouldSplit = !vars.autoEndDone && !settings["ilRunMode"] && current.level > 0 && current.level != old.level && current.level == vars.LEVEL_NUMS[vars.nextLevelIndex]; if (shouldSplit) { vars.nextLevelIndex++; } } //special case to autosplit once the final cutscene in P3C8 has started if (!shouldSplit && !vars.autoEndDone) { shouldSplit = current.level == vars.LEVEL_NUMS[24] && current.inCutscene == 1 && current.lastLevelComplete == 0; vars.autoEndDone = shouldSplit; } if (shouldSplit) { return true; } } gameTime { if (settings["nymRunMode"]) { if (current.level > 0 && current.nymGameTime > vars.EPSILON) { if (settings["nymSaveLoadPenalty"] && current.nymGameTime < old.nymGameTime - vars.EPSILON) { vars.totalTimePenalty += vars.NYM_SAVE_LOAD_PENALTY; } //just take the time from the game, as it keeps the entire time of the run //doing it this way allows loading quick saves or autosaves to correctly adjust the timer return TimeSpan.FromSeconds(current.nymGameTime + vars.totalTimePenalty); } } } isLoading { if (settings["nymRunMode"]) { return true; } else { return current.onLoadScreen > 0 && current.viewingComic == 0; } }