-
-
Save DorentuZ/288fe12fc10c6ba9e87f7593657e926f to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
* HOW TO USE: | |
* This autosplitter requires an event server to fire events over a named pipe. This is provided by the pdthhook (as of version 23) used | |
* by the DAHM4PD mod for PAYDAY: The Heist. It's included from version 1.9.10 onwards. | |
* | |
* This event server has to be enabled manually. This is done by creating a file named 'pdthhook.ini' in the root folder of the game with | |
* the following contents: | |
* | |
* [events] | |
* events = 1 | |
* | |
* Besides having a working event service, there's a few other things to consider. This splitter can be used for both individual level | |
* and multi-level runs. To keep an accurate history of your runs, there's a few limitations: objectives and heists can be skipped, but | |
* they have to be played in the order defined for the run. | |
* | |
* For individual level runs, this splitter looks for the splits file having a certain name. If the name matches the format, all splits | |
* will be for a single heist. The naming format is '<a>.<b>.<c>.<level id>' (without the .lss extension). As an example, a solo glitchless | |
* run on first world bank would possibly be named as 'solo.il.glitchless.bank'. | |
* | |
* Once a single file is loaded with such a setup, any next levels that are loaded will automatically have their splits file loaded or | |
* created. If a run does not have any splits, they will be automatically generated based on the name part in the filename and saved to | |
* a file with the same name. If the previous bank example was used, then Heat Street would be saved as 'solo.il.glitchless.heat_street'. | |
* | |
* For multi level splits all you have to do is NOT name it like for the invidual level runs. If the run does not have any splits, then | |
* this script will automatically append all heists and objectives in the order they appear in the (vanilla) game. To generate splits | |
* in a specific order or a subset of the available heists, then the first split has to be named as '=>level0,level1,level2'. That is, | |
* starting with => and followed by heist ids, separated by commas. | |
* | |
* You can find the heist ids by scrolling down a bit. You can freely change the texts they appear as, but the ids have to remain unchanged | |
* in order to function. Objectives are always done in the order they're defined. You can comment them out (or remove them) if you're not | |
* interested in splitting on a certain objective. | |
* | |
* | |
* LIMITATIONS: | |
* - The script has to be active before a level is loaded. | |
* - Splits cannot be reordered and/or removed UNLESS it's done in both this script and in the splits files. | |
* - The game time is reset every time an event with a time is received. Small lags in the game or the communication with this script can | |
* cause discrepancies between the internal timer and the value that was received, resulting in time skips (in either direction). | |
* | |
* | |
* TODO: | |
* - More settings/customization, etc. | |
* - Test it better to ensure it works with edge cases. | |
* - Re-evaluate the custom relative (level) time comparison. Is it actually usefuL? | |
* - Clean up and optimize the code a bit. | |
* - ?? | |
* | |
*/ | |
state("payday_win32_release", "1.21.0") {} | |
state("FakePdthEventServer") {} // Obviously for testing this without launching the game. | |
state("NamedPipesTestServer2") {} // Obviously for testing this without launching the game. | |
startup | |
{ | |
// ============================================================= | |
// Variables | |
// ============================================================= | |
vars.pipeName = "DAHM4PDTH"; | |
vars.debugHeader = "[PD:TH AutoSplitter] "; | |
vars.RelativeSplitTimeComparisonName = "Relative Split Time"; | |
refreshRate = 50; | |
// these just make sure the dictionary has entries | |
vars.level = null; | |
vars.totalGameTime = TimeSpan.Zero; | |
// simple debug function | |
vars.DebugOutput = ((Action<string>) ((string s) => { print(vars.debugHeader + s); })); | |
// ============================================================= | |
// Splits | |
// ============================================================= | |
// helper function to create tuples | |
Func<string, string, Tuple<string, string>> T = (string key, string text) => | |
{ | |
return new Tuple<string, string>(key, text); | |
}; | |
// levels: id - text | |
vars.levels = null; | |
vars.availableLevels = new List<Tuple<string, string>>(new Tuple<string, string>[] | |
{ | |
T("bank", "First World Bank"), | |
T("heat_street", "Heat Street"), | |
T("apartment", "Panic Room"), | |
T("bridge", "Green Bridge"), | |
T("diamond_heist", "Diamond Heist"), | |
T("slaughter_house", "Slaughterhouse"), | |
T("suburbia", "Counterfeit"), | |
T("secret_stash", "Undercover"), | |
T("hospital", "No Mercy") | |
}); | |
// objectives: id - text | |
vars.objectives = new Dictionary<string, Tuple<string, string>[]> | |
{ | |
{ "bank", new Tuple<string, string>[] { | |
T("bank_get_into_bank", "Enter the bank"), // Enter the bank (Lobby.) | |
T("bank_get_key", "Find the bank manager"), // Find the bank manager. | |
T("bank_get_drill", "Get the drill & thermite"), // Get the drill & thermite (Server Room.) | |
T("bank_apply_drill", "Start the drill"), // Start the drill (Drill Area.) | |
//T("bank_security_footage", "Erase the security footage"), // Erase the Security Footage (Management.) --#! | |
T("bank_get_into_vault", "Drill the gates"), // Drill the gates (Drill Area.) | |
T("bank_thermite", "Poor out the thermite"), // Poor out the thermite to melt the floor (above the vault). | |
T("bank_thermite_wait", "Melt the floor"), // Melt the floor (above the Vault) | |
T("bank_get_empty_vault", "Empty the vault"), // Empty the Vault (Vault) | |
T("bank_trough_lobby", "Get through the lobby"), // Get through the Lobby. | |
T("bank_blow_wall", "Blow a hole in the wall"), // Blow a hole in the wall (Management.) | |
T("bank_escape", "Escape") // Escape (Basement.) | |
} }, | |
{ "heat_street", new Tuple<string, string>[] { | |
T("str_catch", "Chase down Matt"), // Chase Down Matt! (Foreign Ave.) | |
T("str_meet", "Meet Bruce"), // Meet Bruce (Scarlet ST.) | |
T("str_reach", "Continue chasing Matt"), // Continue Chasing Matt (Major Ave.) | |
T("str_reach2", "Reach the crash site"), // Reach the Crash Site (Easy ST.) | |
T("str_extract", "Light the van on fire"), // Light the Van on Fire! (Inkwell Ind.) | |
T("str_parking", "Force Matt to the parking lot"), // Force Matt to the Parking Lot (Jake's Parking.) | |
T("str_escort", "Force Matt up the hill"), // Force Matt up the Hill (Armitate Ave.) | |
T("str_clear", "Clear the zone off cops"), // Clear the Zone of Cops (Armitate Ave.) | |
//T("str_fail", "Move back down"), // Move back down to the Helicopter (Armitage Ave.) --# | |
T("str_exit", "Force Matt to the heli") // Force Matt to the Heli and rally there (Armitage Ave.) | |
} }, | |
{ "apartment", new Tuple<string, string>[] { | |
T("ap_intro", "Entrance"), // Entrance (Back alley.) | |
T("ap_start", "Drug deal"), // Drug deal (Lobby.) | |
T("ap_door", "Locate the panic room"), // Locate the Panic Room (Floor 3.) | |
//T("ap_key", "Get Chavez' key"), // Find Chavez and get his key. --# | |
T("ap_panic", "Get into the panic room"), // Get into the Panic Room (Floor 3). | |
T("ap_detach", "Detach the panic room"), // Detach the panic room (Floor 2 and 3.) | |
T("ap_detach2", "Keep the saws going"), // Keep the saws going (Floor 2 and 3.) | |
T("ap_wait", "Wait for the explosives"), // Wait for the explosives (Roof.) | |
T("ap_snipes", "Take out the snipers"), // Take out the snipers (Roof.) | |
T("ap_c4", "Rig the c4 charges"), // Rig the C4 charges. | |
T("ap_c42", "Move away from the c4"), // Move away from the C4 (Floor 2.) | |
T("ap_wait2", "Wait for the heli to return"), // Wait for the helicopter to return to the roof (Roof.) | |
T("ap_roof", "Secure the roof"), // Secure the roof (Roof.) | |
T("ap_winch", "Attach the magnet"), // Attach the magnet (Floor 4.) | |
T("ap_roof2", "Defend the heli"), // Defend the Helicopter (Roof.) | |
T("ap_escape", "Escape") // Escape (Basement.) | |
} }, | |
{ "bridge", new Tuple<string, string>[] { | |
T("bridge_locate_convoy", "Locate the prison convoy"), // Locate the prison convoy (Convoy.) | |
T("bridge_get_hostage", "Find the prisoner"), // Find the Chinese prisoner (Convoy.) | |
T("bridge_escort", "Escort the prisoner"), // Escort the prisoner (Scaffolding.) | |
T("bridge_identify", "Send up the balloon"), // Send up the Balloon (Scaffolding.) | |
T("bridge_heli_clear", "Defend the scaffolding"), // Defend (Scaffolding.) | |
T("bridge_jump", "Escape") // Make the escape (Bridge pillar.) | |
} }, | |
{ "diamond_heist", new Tuple<string, string>[] { | |
T("diamond_sneak", "Access the alarm boxes"), // Sneak and access the alarm boxes. | |
//T("diamond_sec_doors", "Hack the alarm boxes"), // Plants tablets on the remaining alarm boxes. --# | |
T("diamond_codes", "Enter the codes"), // Enter the codes. | |
T("diamond_escort1", "Get the cfo"), // Change of plan, get the CFO. --# | |
T("diamond_wait", "Wait for Bain"), // Wait for Bain. --# | |
T("diamond_escort2", "Get Ralph"), // Get Mr. Garnet's son Ralph. --# | |
T("diamond_timelock", "Wait for the time lock"), // Wait for the time lock. --# | |
T("diamond_loot", "Steal the diamonds"), // Steal the diamonds. | |
//T("diamond_vault_drill", "?"), // --#! | |
T("diamond_escape", "Escape") // Escape! | |
} }, | |
{ "slaughter_house", new Tuple<string, string>[] { | |
T("slaughterhouse_prepare_ambush", "Prepare for the ambush"), // Prepare for the ambush. | |
T("slaughterhouse_attack_convoy", "Hit the convoy"), // Hit the convoy! | |
T("slaughterhouse_locate_van", "Get to the armored truck"), // Get to the armored truck! | |
T("slaughterhouse_drill", "Drill the safe"), // Drill the safe | |
T("slaughterhouse_take", "Take the gold"), // Take the gold! | |
T("slaughterhouse_hide", "Hide the gold"), // Hide the Gold. | |
T("slaughterhouse_gasoline", "Get the gas"), // Get the Gasoline! | |
T("slaughterhouse_smoke", "Start the smokescreenn"), // Start the smokescreen! | |
T("slaughterhouse_prepare_gas", "Prepare a trap"), // Prepare a trap! | |
T("slaughterhouse_load_gold", "Lift the gold out of here"), // Lift the gold out of here! | |
T("slaughterhouse_load_gas", "Set the trap in place"), // Set the trap in place! | |
T("slaughterhouse_escape", "Ëscape") // Make the escape! | |
} }, | |
{ "suburbia", new Tuple<string, string>[] { | |
T("xsub_mission1", "Talk to the owner"), // TALK TO THE OWNER. | |
T("xsub_mission2", "Hack the code locks"), // HACK THE CODE LOCKS. | |
T("xsub_mission9", "Get inside the shelter"), // GET INSIDE THE SHELTER. | |
T("xsub_mission12", "Investigate the safe"), // INVESTIGATE THE SAFE. | |
//T("xsub_mission3", "Defuse the c4"), // IT'S A TRAP! --#! | |
//T("xsub_mission13", "It's a trap"), // IT'S A TRAP! --# / maybe for failure? idk. | |
T("xsub_mission4", "Start the drill"), // DRILL A HOLE AND CONNECT THE HOSE TO THE SAFE. | |
T("xsub_mission10", "Let the drill finish"), // LET THE DRILL FINISH. | |
T("xsub_mission11", "Connect the hose"), // CONNECT THE HOSE TO THE SAFE. | |
T("xsub_mission5", "Get the water running"), // GET THE WATER RUNNING. | |
T("xsub_mission8", "Keep the water running"), // MAKE SURE THE WATER'S FLOATING | |
T("xsub_mission7", "Blow the safe"), // ATTACH THE C4 TO THE SAFE. | |
T("xsub_mission6", "Take the plates"), // TAKE THE PLATES AND GET OUT! | |
T("xsub_escape", "Escape") // ESCAPE! | |
} }, | |
{ "secret_stash", new Tuple<string, string>[] { | |
T("uc_roof", "Preparations"), // PREPARATIONS. | |
T("uc_deal", "Deal going down"), // DEAL GOING DOWN. | |
T("uc_check", "Check the limo"), // CHECK THE LIMO. --# | |
//T("uc_crane", "Lift the limo"), // LIMO LIFT. -- not available --#! | |
T("uc_shoot_limo", "Free the limo"), // FREE THE LIMO. --# | |
T("uc_saw_limo", "Saw open the limo"), // SAW OPEN THE LIMO. | |
T("uc_escort", "Escort taxman"), // TAKE TAXMAN TO THE TRANSFER ROOM. | |
T("uc_fasten", "Set up the server"), // GET THE SERVER, PLUG IT IN. | |
T("uc_interogation", "Get the codes (1/3)"), // CODES NOW! -- 7 | |
T("uc_transaction", "IRS hack (1/3)"), // IRS HACK. -- 8 | |
//T("uc_power", "Keep the power (1/3)"), // KEEP THE POWER. | |
T("uc_interogation", "Get the codes (2/3)"), // CODES NOW! -- 9 | |
T("uc_transaction", "IRS hack (2/3)"), // IRS HACK. -- 10 | |
//T("uc_power", "Keep the power (2/3)"), // KEEP THE POWER. | |
T("uc_interogation", "Get the codes (3/3)"), // CODES NOW! -- 11 | |
T("uc_transaction", "IRS hack (3/3)"), // IRS HACK. -- 12 | |
//T("uc_power", "Keep the power (3/3)"), // KEEP THE POWER. | |
T("uc_escape2", "Escape") // ESCAPE! | |
} }, | |
{ "hospital", new Tuple<string, string>[] { | |
T("hospital_cameras", "Take out the cameras"), // TAKE OUT THE CAMERAS. | |
T("hospital_hostage", "Keep the hostages down"), // KEEP THE HOSTAGES DOWN. --# | |
T("hospital_rapport", "Find the correct patient file"), // FIND THE CORRECT PATIENT FILE. --# | |
T("hospital_atrapp", "Set up the fake sentry guns"), // SET UP FAKE SENTRY GUNS. -- | |
T("hospital_disguise", "Play doctor"), // PLAY DOCTOR. --# | |
//T("hospital_rounds", "Listen to the doctor"), // FIND OUT WHERE THE CONTAMINATED PATIENT IS. --# | |
T("hospital_ICU", "Saw into the icu"), // SAW OPEN THE DOORS INTO THE ICU. --# | |
T("hospital_getToPatient", "Find the patient"), // PICK THE RIGHT DOOR TO OPEN. | |
T("hospital_drawblood", "Get a validated sample"), // DRAW BLOOD FROM THE PATIENT. | |
T("hospital_callelevator", "Call the elevator"), // CALL THE ELEVATOR. | |
T("hospital_waitElevator", "Wait for the elevator"), // WAIT FOR THE ELEVATOR. | |
T("hospital_cooler", "Put two validated samples into the cooler"), // PUUT TWO VALIDATED SAMPLES INTO THE COOLER. | |
T("hospital_enterelevator", "Take the elevator"), // TAKE THE ELEVATOR TO THE ROOF. | |
T("hospital_hatch", "Open the hatch"), // OPEN THE HATCH. | |
T("hospital_escape", "Escape") // ESCAPE! | |
} } | |
}; | |
// override standard split behaviour | |
var objectiveHistory = new List<Tuple<string, string>>(); | |
vars.splitLogic = new Dictionary<string, Func<string, int, string, TimerModel, bool>> | |
{ | |
{ | |
"secret_stash", (string objectiveID, int index, string action, TimerModel timerModel) => | |
{ | |
vars.DebugOutput(string.Format("Split logic: '{0}' {1}.", objectiveID, action)); | |
if (action == "heist") | |
{ | |
if (objectiveID == "<begin>") | |
objectiveHistory.Clear(); | |
return true; | |
} | |
// keep track of the event | |
objectiveHistory.Add(new Tuple<string, string>(objectiveID, action)); | |
// handle the whole transaction part as it's special: most are added and removed multiple times | |
if (objectiveHistory.Count == 1) | |
return true; | |
if (action == "removed") | |
{ | |
if (objectiveID == "uc_interogation") | |
{ | |
// split on uc_interogation as they're removed instead of completed | |
timerModel.Split(); | |
return false; | |
} | |
if (objectiveID == "uc_transaction") | |
{ | |
// manually split transaction objectives based on the previous objective to ignore their removal on power outages | |
Tuple<string, string> prevAction = objectiveHistory[objectiveHistory.Count - 2]; | |
if (prevAction.Item1 != "uc_transaction") | |
{ | |
timerModel.Split(); | |
} | |
return false; | |
} | |
} | |
else if (action == "activated") | |
{ | |
if (objectiveID == "uc_transaction") | |
{ | |
return false; | |
} | |
} | |
return true; | |
} | |
} | |
}; | |
// ============================================================= | |
// Definitions for events and thread | |
// ============================================================= | |
var eventsLock = new object(); | |
var events = new Queue<object>(); | |
vars.eventsLock = eventsLock; | |
vars.events = events; | |
vars.handlers = null; | |
vars.thread = null; | |
vars.cts = null; | |
ThreadStart ThreadWorker = async delegate() | |
{ | |
Action<string> DebugOutput = vars.DebugOutput; | |
CancellationToken token = vars.cts.Token; | |
DebugOutput("Pipe listener service started."); | |
while (!token.IsCancellationRequested) | |
{ | |
// Try connecting to the pipe until it is cancelled | |
using (var client = new System.IO.Pipes.NamedPipeClientStream(vars.pipeName)) | |
{ | |
DebugOutput("Attempting to (re)connect the pipe listener..."); | |
try | |
{ | |
await client.ConnectAsync(token); | |
if (!client.IsConnected) break; // cancelled? | |
DebugOutput("Client is connected to the server!"); | |
using (System.IO.StreamReader reader = new System.IO.StreamReader(client)) | |
{ | |
while (!reader.EndOfStream) | |
{ | |
// Read line. There's a limited buffer, so it can't take too long. | |
string response = await reader.ReadLineAsync(); | |
if (token.IsCancellationRequested) break; | |
// Do some housekeeping. | |
response = response.TrimStart('\0'); | |
if (response.Length == 0) continue; | |
// Some debug info to inspect *all* received data. | |
DebugOutput(string.Format("Response from server: '{0}'.", string.Join(string.Empty, | |
response.Select(c => (32 <= c && c <= 126) | |
? c.ToString() | |
: string.Format("{{{0}}}", (int)c)) | |
.ToArray() | |
))); | |
try | |
{ | |
// Read pipe data, parse it as JSON and keep it if the message has a name. | |
dynamic data = LiveSplit.Web.JSON.FromString(response); | |
if (data.type != null) | |
{ | |
lock (eventsLock) | |
{ | |
if (!vars.handlers.ContainsKey((string)data.type)) | |
{ | |
DebugOutput(string.Format("Ignoring {0} event.", data.type)); | |
continue; | |
} | |
events.Enqueue(data); | |
DebugOutput(string.Format("Event {0} added. Events in queue: {1}.", data.type, events.Count)); | |
} | |
} | |
} | |
catch (Exception e) | |
{ | |
DebugOutput("Failed to parse response: " + e.Message); | |
} | |
} | |
} | |
} | |
catch (OperationCanceledException) {} // ignore | |
catch (Exception e) | |
{ | |
DebugOutput("Cannot connect to pipe: " + e.Message); | |
} | |
} | |
} | |
DebugOutput("Pipe listener service stopped."); | |
}; | |
Action<Action<string>, Action<string>> StopThread = (InfoOutput, ExceptionOutput) => | |
{ | |
try | |
{ | |
if (vars.thread != null) | |
{ | |
InfoOutput("Stopping pipe listener service thread..."); | |
try | |
{ | |
vars.cts.Cancel(); | |
} | |
catch (AggregateException) {} | |
vars.thread.Join(); | |
vars.cts.Dispose(); | |
} | |
} | |
catch (Exception e) | |
{ | |
ExceptionOutput("Failed to stop pipe listener service: " + e.Message); | |
} | |
finally | |
{ | |
vars.thread = null; | |
vars.cts = null; | |
} | |
}; | |
vars.ThreadWorker = ThreadWorker; | |
vars.StopThread = StopThread; | |
// ============================================================= | |
// Settings | |
// ============================================================= | |
//settings.Add(string id, bool default_value, string description = null, string parent = null) | |
//settings.SetToolTip(string id, string tooltip); | |
//settings.Add("debug", false, "Enable debug logging"); | |
settings.Add("splits.autoload", true, "Automatically load level splits."); | |
settings.Add("splits.autocreate", true, "Create splits that don't exist yet.", "splits.autoload"); | |
settings.Add("splits.autoload.confirm", true, "Ask for confirmation when a segment has been improved.", "splits.autoload"); | |
// ============================================================= | |
// Load old runs | |
// ============================================================= | |
Func<string, IRun> ImportRun = (string filePath) => | |
{ | |
Action<string> DebugOutput = text => print(vars.debugHeader + text); // forced output | |
if (File.Exists(filePath)) | |
{ | |
using (var stream = File.OpenRead(filePath)) | |
{ | |
var runFactory = new LiveSplit.Model.RunFactories.StandardFormatsRunFactory(); | |
var comparisonGeneratorsFactory = new LiveSplit.Model.Comparisons.StandardComparisonGeneratorsFactory(); | |
runFactory.Stream = stream; | |
runFactory.FilePath = filePath; | |
var imported = runFactory.Create(comparisonGeneratorsFactory); | |
DebugOutput("Run has " + imported.Count + " splits."); | |
DebugOutput(imported.CustomComparisons.Aggregate((a, b) => a + ", " + b)); | |
return imported; | |
} | |
} | |
else | |
{ | |
DebugOutput(string.Format("Not loading previous runs: {0} does not exist.", filePath)); // force output | |
} | |
return null; | |
}; | |
Action<IRun, string> ExportRun = (IRun run, string filePath) => | |
{ | |
var saver = new LiveSplit.Model.RunSavers.XMLRunSaver(); | |
using (var stream = File.OpenWrite(filePath)) | |
{ | |
saver.Save(run, stream); | |
} | |
}; | |
vars.ImportRun = ImportRun; | |
vars.ExportRun = ExportRun; | |
{ | |
IRun run = timer.Run; | |
string dirName = System.IO.Path.GetDirectoryName(run.FilePath); | |
string fileName = System.IO.Path.GetFileNameWithoutExtension(run.FilePath); | |
string ext = System.IO.Path.GetExtension(run.FilePath); | |
print(string.Format("========================================== FilePath: {0}\\<{1}>{2}", dirName, fileName, ext)); | |
//IRun imported = vars.ImportRun(timer.Run, null); | |
//vars.ExportRun(imported, @"C:\\test.lss"); | |
} | |
// print(string.Format("Active splitters: {0} ({1}).", string.Join(";", timer.Settings.ActiveAutoSplitters), timer.Settings.ActiveAutoSplitters.Count)); | |
// var activeSplitters = timer.Settings.ActiveAutoSplitters; | |
// if (!activeSplitters.Contains("PD:TH AutoSplitter")) | |
// { | |
// print("IT'S NOT ACTIVE!"); | |
// activeSplitters.Add("PD:TH AutoSplitter"); | |
// } | |
// else | |
// { | |
// print("IT'S ALREADY ACTIVE!"); | |
// } | |
// timerModel.OnReset += (s, e) => { print("==================== RERERERERERESET ============================= "); }; | |
} | |
shutdown | |
{ | |
// Make sure the thread is stopped when this script is reloaded. | |
var DebugOutput = (Action<string>) vars.DebugOutput; | |
vars.StopThread(DebugOutput, DebugOutput); | |
} | |
init | |
{ | |
// NOTE: cannot access all variables in the startup function, so most are defined in this scope instead... | |
TimerModel m_timerModel = new TimerModel { CurrentState = timer }; | |
string m_splitsFilePath = timer.Run.FilePath; | |
List<Tuple<string, string>> m_levels = vars.levels; | |
List<Tuple<string, string>> m_availableLevels = vars.availableLevels; | |
Dictionary<string, Tuple<string, string>[]> m_objectives = vars.objectives; | |
int m_currentObjectiveIndex = 0; | |
// ============================================================= | |
// Helper functions | |
// ============================================================= | |
// log function -- uses settings, so has to be here | |
// Action<string> DebugOutput = (text) => | |
// { | |
// if (settings["debug"]) | |
// { | |
// print(vars.debugHeader + text); | |
// } | |
// }; | |
var DebugOutput = (Action<string>) vars.DebugOutput; | |
// ============================================================= | |
Func<DialogResult> WarnAboutResetting = () => | |
{ | |
LiveSplitState CurrentState = timer; | |
var warnUser = false; | |
for (var index = 0; index < CurrentState.Run.Count; index++) | |
{ | |
if (LiveSplitStateHelper.CheckBestSegment(CurrentState, index, CurrentState.CurrentTimingMethod)) | |
{ | |
warnUser = true; | |
break; | |
} | |
} | |
if (!warnUser && (CurrentState.Run.Last().SplitTime[CurrentState.CurrentTimingMethod] != null && CurrentState.Run.Last().PersonalBestSplitTime[CurrentState.CurrentTimingMethod] == null) || CurrentState.Run.Last().SplitTime[CurrentState.CurrentTimingMethod] < CurrentState.Run.Last().PersonalBestSplitTime[CurrentState.CurrentTimingMethod]) | |
warnUser = true; | |
if (warnUser) | |
{ | |
return MessageBox.Show("You have beaten some of your best times.\r\nDo you want to update them?", "Update Times?", MessageBoxButtons.YesNoCancel, MessageBoxIcon.Question); | |
} | |
return DialogResult.Yes; | |
}; | |
Action<IRun> SetupSegmentsForLevels = (IRun run) => | |
{ | |
run.Clear(); | |
if (m_levels != null && m_levels.Count > 0) | |
{ | |
foreach (var h in m_levels) | |
{ | |
string hID = h.Item1; | |
string hText = h.Item2; | |
DebugOutput(string.Format("Setting up segments for level '{0}...", hID)); | |
if (m_objectives.ContainsKey(hID)) | |
{ | |
foreach (var o in m_objectives[hID]) | |
{ | |
string oID = o.Item1; | |
string oText = o.Item2; | |
run.Add(new Segment("-" + oText)); | |
} | |
run.Add(new Segment(hText)); | |
} | |
} | |
} | |
else | |
{ | |
DebugOutput("There's no levels to setup segments for..."); | |
} | |
// ensure there's always at least one segment | |
if (run.Count == 0) | |
run.Add(new Segment("")); | |
}; | |
Func<IRun, bool> FillEmptySplits = (IRun run) => | |
{ | |
if (run != null) | |
{ | |
if (run.Count == 0 || (run.Count == 1 && System.Text.RegularExpressions.Regex.IsMatch(run[0].Name, @"^\s*$"))) | |
{ | |
DebugOutput("No splits detected. Generating them..."); | |
// create segments for this level | |
SetupSegmentsForLevels(run); | |
return true; | |
} | |
else | |
{ | |
DebugOutput(string.Format("Run has ({0}) valid splits. First split: '{1}'.", run.Count, run[0].Name)); | |
} | |
} | |
return false; | |
}; | |
Func<IRun, List<Tuple<string, string>>> ExtractLevelsFromMetadata = (IRun run) => | |
{ | |
List<Tuple<string, string>> levels = null; | |
IDictionary<string, string> variableValueNames = run.Metadata.VariableValueNames; | |
DebugOutput("Has levels variable: " + variableValueNames.ContainsKey("heists")); | |
string levelsOrder; | |
if (variableValueNames.TryGetValue("heists", out levelsOrder)) | |
{ | |
levels = new List<Tuple<string, string>>(); | |
DebugOutput("Loading heists from metadata..."); | |
foreach (var name in levelsOrder.Split(';').ToList()) | |
{ | |
if (m_objectives.ContainsKey(name)) | |
{ | |
DebugOutput(string.Format("Adding level '{0}' to the list...", name)); | |
try | |
{ | |
levels.Add(m_availableLevels.First(t => t.Item1 == name)); | |
} | |
catch (InvalidOperationException) {} | |
} | |
else | |
{ | |
DebugOutput(string.Format("Ignoring invalid level '{0}'...", name)); | |
} | |
} | |
} | |
return levels; | |
}; | |
Func<bool> AutoResetRun = () => | |
{ | |
if (settings.ResetEnabled) | |
{ | |
m_currentObjectiveIndex = 0; | |
if (settings["splits.autoload.confirm"]) | |
{ | |
var result = WarnAboutResetting(); | |
if (result == DialogResult.Yes) | |
m_timerModel.Reset(); | |
else if (result == DialogResult.No) | |
m_timerModel.Reset(false); | |
else // cancel | |
return false; | |
} | |
else | |
{ | |
// force save | |
m_timerModel.Reset(); | |
} | |
return true; | |
} | |
return false; | |
}; | |
Func<string, IRun> LoadSplitsForLevel = (string levelID) => | |
{ | |
m_currentObjectiveIndex = 0; | |
if (!settings["splits.autoload"]) | |
{ | |
DebugOutput("Auto loading splits is disabled."); | |
return null; | |
} | |
IRun run = timer.Run; | |
if (run.FilePath == null) | |
{ | |
DebugOutput("Run is not saved to a file."); | |
return null; | |
} | |
// match individual level file based on current splits file, e.g. pdth.il.anyp.bank.lss | |
var regex = new System.Text.RegularExpressions.Regex(@"^(?<name>.+)\.(?<category>.+)\.(?<variant>.+)\.(?<level>.+)$"); | |
var match = regex.Match(System.IO.Path.GetFileNameWithoutExtension(run.FilePath)); | |
if (match.Success) | |
{ | |
if (levelID == null || levelID == "init" || (m_availableLevels != null && !m_availableLevels.Exists(item => item.Item1 == levelID))) | |
{ | |
DebugOutput("No splits to load: invalid level."); | |
return null; | |
} | |
bool splitsAreLoaded = (match.Groups["level"].Value == levelID); | |
if (!splitsAreLoaded || m_levels == null || m_levels.Count == 0) | |
{ | |
// setup internal splits for level | |
{ | |
int index = m_availableLevels.FindIndex(item => item.Item1 == levelID); | |
if (index >= 0) | |
{ | |
DebugOutput(string.Format("Level is at index {0}.", index)); | |
// Set active heists list. | |
m_levels = m_availableLevels.GetRange(index, 1); | |
} | |
} | |
} | |
if (!splitsAreLoaded) | |
{ | |
// show dialog | |
AutoResetRun(); | |
string newName = string.Format("{0}.{1}.{2}.{3}", | |
match.Groups["name"], match.Groups["category"], match.Groups["variant"], levelID | |
); | |
string ext = System.IO.Path.GetExtension(run.FilePath); | |
string dirName = System.IO.Path.GetDirectoryName(run.FilePath); | |
string newPath = string.Format(@"{0}\{1}{2}", dirName, newName, ext); | |
if (File.Exists(newPath)) | |
{ | |
DebugOutput(string.Format("Loading existing splits file '{0}'.", newPath)); | |
// load it | |
IRun newRun = vars.ImportRun(newPath); | |
FillEmptySplits(newRun); | |
return newRun; | |
} | |
else if (settings["splits.autocreate"]) | |
{ | |
DebugOutput(string.Format("Creating new splits file '{0}'.", newPath)); | |
// create it + load it | |
IRun newRun = (IRun) timer.Run.Clone(); | |
newRun.FilePath = newPath; | |
newRun.AttemptHistory.Clear(); | |
newRun.AttemptCount = 0; | |
// create segments for this level | |
SetupSegmentsForLevels(newRun); | |
// and finally save the run | |
vars.ExportRun(newRun, newPath); | |
return newRun; | |
} | |
} | |
else | |
{ | |
DebugOutput(string.Format("Splits file '{0}' is already loaded.", run.FilePath)); | |
if (FillEmptySplits(run)) | |
{ | |
DebugOutput(string.Format("{0} splits have been added to the run.", run.Count)); | |
timer.CallRunManuallyModified(); | |
} | |
} | |
} | |
else | |
{ | |
// determine levels + order | |
if (run.Count == 0 || (run.Count == 1 && System.Text.RegularExpressions.Regex.IsMatch(run[0].Name, @"^\s*$"))) | |
{ | |
if (m_levels == null) | |
{ | |
// empty list, copy all in the same order | |
DebugOutput("No splits found. Assuming all levels in the standard order."); | |
m_levels = m_availableLevels.GetRange(0, m_availableLevels.Count); | |
} | |
else | |
{ | |
DebugOutput("No splits found, but the heists variable is already defined. Using that."); | |
return null; | |
} | |
} | |
else if (run.Count == 1 && run[0].Name.StartsWith("=>")) | |
{ | |
// selective include in the defined order (.e.g '=>bridge,apartment,bank,heat_street,diamond_heist,slaughter_house') | |
m_levels = new List<Tuple<string, string>>(); | |
DebugOutput("Found a single run starting with '=>', assuming this defines levels..."); | |
foreach (var name in System.Text.RegularExpressions.Regex.Split(run[0].Name.Substring(2), @"\s*[;,+]\s*").ToList()) | |
{ | |
if (m_objectives.ContainsKey(name)) | |
{ | |
DebugOutput(string.Format("Adding level '{0}' to the list...", name)); | |
try | |
{ | |
m_levels.Add(m_availableLevels.First(t => t.Item1 == name)); | |
} | |
catch (InvalidOperationException) {} | |
} | |
else | |
{ | |
DebugOutput(string.Format("Ignoring invalid level '{0}'...", name)); | |
} | |
} | |
// clear split to process it as an empty list next | |
run[0].Name = ""; | |
} | |
else | |
{ | |
// No hint from the splits, try metadata. | |
DebugOutput("Does not match pattern for individual levels."); | |
if (m_levels == null) | |
{ | |
m_levels = ExtractLevelsFromMetadata(run); | |
if (m_levels == null) | |
{ | |
// import directly from the file, deals with vars being removed | |
m_levels = ExtractLevelsFromMetadata(vars.ImportRun(run.FilePath)); | |
if (m_levels == null) | |
{ | |
DebugOutput("No result from the metadata either."); | |
return null; | |
} | |
} | |
} | |
} | |
// update variable with our local (typed) var | |
{ | |
string levelsOrder = m_levels.Aggregate("", (a, b) => a + ";" + b.Item1); | |
IDictionary<string, string> variableValueNames = run.Metadata.VariableValueNames; | |
DebugOutput("Updating levels variable(s): " + levelsOrder); | |
variableValueNames["heists"] = levelsOrder; | |
vars.levels = m_levels; | |
} | |
// try load segments | |
if (FillEmptySplits(run)) | |
{ | |
DebugOutput(string.Format("{0} splits have been added to the run.", run.Count > 1 ? run.Count : run[0].Name == "" ? 0 : 1)); | |
timer.CallRunManuallyModified(); | |
} | |
// append relative times | |
IList<string> comparisons = run.CustomComparisons; | |
string relativeOffsetComparisonName = vars.RelativeSplitTimeComparisonName; | |
if (!comparisons.Contains(relativeOffsetComparisonName)) | |
{ | |
DebugOutput(string.Format("Appending custom comparison {0}.", relativeOffsetComparisonName)); | |
comparisons.Add(relativeOffsetComparisonName); | |
} | |
} | |
return null; | |
}; | |
Func<string, string, int> GetObjectiveIndex = (string levelID, string objectiveID) => | |
{ | |
if (m_levels == null) | |
{ | |
DebugOutput("No levels defined."); | |
return -1; | |
} | |
DebugOutput(string.Format("Finding index for objective '{0}' in level '{1}'...", objectiveID, levelID)); | |
DebugOutput("Levels = " + string.Join(";", m_levels)); | |
int offset = 0; | |
bool validLevel = false; | |
foreach (var h in m_levels) | |
{ | |
string hID = h.Item1; | |
if (hID == levelID) | |
{ | |
validLevel = true; | |
break; | |
} | |
offset += m_objectives[hID].Length + 1; // objectives + the level split | |
} | |
// Ensure validity of both the objective and the level. | |
if (!validLevel || !m_objectives.ContainsKey(levelID)) | |
{ | |
DebugOutput(string.Format("There's no objectives for level '{0}'", levelID)); | |
return -1; | |
} | |
// First objective for the level | |
if (objectiveID == "<begin>") | |
{ | |
DebugOutput(string.Format("First objective for level '{0}' has index {1}.", levelID, offset)); | |
return offset; | |
} | |
// The level segment itself | |
if (objectiveID == "<end>") | |
{ | |
DebugOutput(string.Format("Last segment for level '{0}' has index {1}.", levelID, offset + m_objectives[levelID].Length)); | |
return offset + m_objectives[levelID].Length; | |
} | |
// An actual objective | |
bool hasObjective = false; | |
var objectives = m_objectives[levelID]; | |
// forward check | |
for (int i = m_currentObjectiveIndex; i < objectives.Length; ++i) | |
{ | |
var o = objectives[i]; | |
if (o.Item1 == objectiveID) | |
{ | |
DebugOutput(string.Format("Index for '{0}' is {1}.", objectiveID, i)); | |
m_currentObjectiveIndex = i; | |
return offset + i; | |
} | |
} | |
// backward check | |
for (int i = m_currentObjectiveIndex; i >= 0; --i) | |
{ | |
var o = objectives[i]; | |
if (o.Item1 == objectiveID) | |
{ | |
DebugOutput(string.Format("Index for '{0}' is {1}.", objectiveID, i)); | |
m_currentObjectiveIndex = i; | |
return offset + i; | |
} | |
} | |
return -1; | |
}; | |
Action ResetCustomSplits = () => | |
{ | |
vars.totalGameTime = TimeSpan.Zero; | |
current.activeObjectives.Clear(); | |
// reset custom comparisons | |
IRun run = timer.Run; | |
string relativeOffsetComparisonName = vars.RelativeSplitTimeComparisonName; | |
foreach (var segment in run) | |
{ | |
if (segment.Comparisons.ContainsKey(relativeOffsetComparisonName)) | |
segment.Comparisons[relativeOffsetComparisonName] = default(Time); | |
} | |
}; | |
Action<string, string> StartLevel = (string levelID, string difficulty) => | |
{ | |
vars.level = levelID; | |
current.level = levelID; | |
current.difficulty = difficulty; | |
if (levelID == "init") | |
{ | |
// load splits for multilevel runs | |
LoadSplitsForLevel(levelID); | |
} | |
// process start event | |
Func<string, int, string, TimerModel, bool> splitLogic = null; | |
if (vars.splitLogic.TryGetValue(current.level, out splitLogic)) | |
{ | |
splitLogic("<begin>", -1, "heist", m_timerModel); | |
} | |
// auto reset if the first level is played again | |
if (m_levels != null && m_levels.Count > 0) | |
{ | |
if (current.isPlaying | |
|| timer.CurrentPhase == TimerPhase.Ended | |
|| current.levelStartTime.RealTime != null | |
|| vars.totalGameTime.CompareTo(TimeSpan.Zero) > 0) | |
{ | |
var h = m_levels.First(); | |
if (h.Item1 == levelID) | |
{ | |
AutoResetRun(); | |
} | |
} | |
if (settings.StartEnabled && (current.isPlaying && !current.isPaused && !current.isLoading)) | |
{ | |
m_timerModel.Start(); | |
} | |
} | |
}; | |
Action<decimal> UpdateGameTime = (decimal syncedTime) => | |
{ | |
// only update the time when the timer is active to avoid messing with old runs | |
if ((timer.CurrentPhase & (TimerPhase.Running | TimerPhase.Paused)) > 0) | |
{ | |
TimeSpan syncedGameTime = TimeSpan.FromSeconds(decimal.ToDouble(syncedTime)); | |
timer.SetGameTime(vars.totalGameTime + syncedGameTime); | |
DebugOutput(string.Format("Current game time is {0} (total time: {1}).", timer.CurrentTime.GameTime, vars.totalGameTime)); | |
// only add to the total time to the end of the level (when not playing) | |
if (!current.isPlaying && !current.isLoading) | |
{ | |
DebugOutput("Adding to total game time: " + syncedGameTime); | |
vars.totalGameTime += syncedGameTime; | |
} | |
} | |
else | |
{ | |
DebugOutput("Ignoring time sync: Timer is not active."); | |
} | |
}; | |
Func<string, string, bool> OnObjectiveAction = (string objectiveID, string action) => | |
{ | |
if (!settings.SplitEnabled) | |
return false; | |
int index = GetObjectiveIndex(current.level, objectiveID); | |
Func<string, int, string, TimerModel, bool> splitLogic = null; | |
if (vars.splitLogic.TryGetValue(current.level, out splitLogic)) | |
{ | |
if (!splitLogic(objectiveID, index, action, m_timerModel)) | |
{ | |
// don't process the standard stuff | |
return false; | |
} | |
} | |
if (index >= 0) | |
{ | |
DebugOutput(string.Format("Split Segment At Index: {0}, {1}, {2}.", index, objectiveID, action)); | |
// find offset to current split | |
int nSegmentsToSkip = index - timer.CurrentSplitIndex; | |
// match current split when a single objective is activated | |
if (action == "activated") | |
{ | |
if (current.activeObjectives.Count == 1) | |
{ | |
for (int i = 0; i < nSegmentsToSkip; ++i) | |
{ | |
m_timerModel.SkipSplit(); | |
} | |
} | |
return false; | |
} | |
else if (action == "completed") | |
{ | |
// TODO: segment is in the past, move it up | |
DebugOutput(string.Format("Objective '{0}' has been skipped before. Move it...?", objectiveID)); | |
} | |
// split correct segment (forward only for now) | |
for (int i = 0; i < nSegmentsToSkip; ++i) | |
{ | |
m_timerModel.SkipSplit(); | |
} | |
if (action == "removed") | |
{ | |
m_timerModel.SkipSplit(); | |
return true; | |
} | |
m_timerModel.Split(); | |
// update relative time | |
IRun run = timer.Run; | |
ISegment segment = run[index]; | |
string relativeOffsetComparisonName = vars.RelativeSplitTimeComparisonName; | |
IList<string> comparisons = run.CustomComparisons; | |
if (comparisons.Contains(relativeOffsetComparisonName)) | |
{ | |
Time cTime = timer.CurrentTime; | |
segment.Comparisons[relativeOffsetComparisonName] = new Time( | |
current.levelStartTime.RealTime - cTime.RealTime, | |
current.levelStartTime.GameTime - cTime.GameTime | |
); | |
DebugOutput(string.Format("Relative split time is {0}", segment.Comparisons[relativeOffsetComparisonName])); | |
} | |
else | |
{ | |
DebugOutput("Has no relative split time. Available comparisions: " + string.Join(";", segment.Comparisons)); | |
DebugOutput(timer.Run.CustomComparisons.Aggregate((a, b) => a + ", " + b)); | |
} | |
return true; | |
} | |
return false; | |
}; | |
// ============================================================= | |
// Event handlers (game events) | |
// ============================================================= | |
var handlers = new Dictionary<string, Action<object>> | |
{ | |
{ | |
"OnLoadStarted", (dynamic data) => | |
{ | |
current.isLoading = true; | |
current.isPlaying = false; | |
DebugOutput("Started loading."); | |
// (pre)load splits | |
if (data.level != null) | |
{ | |
string levelID = (string) data.level; | |
if (m_levels == null || (m_levels.Count == 1 && m_levels.First().Item1 != levelID)) | |
{ | |
DebugOutput(string.Format("Loading splits for level '{0}'...", levelID)); | |
IRun run = LoadSplitsForLevel(levelID); | |
if (run == null) | |
{ | |
// run was not modified | |
run = timer.Run; | |
if (run.Count == 0 || (run.Count == 1 && run[0].Name == "")) | |
{ | |
// TODO: use dummy splits? | |
} | |
else if (m_levels != null && m_levels.Count > 1) | |
{ | |
// ML: skip to first split of the level | |
timer.CurrentSplitIndex = GetObjectiveIndex((string) data.level, "<begin>"); | |
timer.CallRunManuallyModified(); | |
} | |
} | |
else | |
{ | |
timer.Run = run; | |
timer.CallRunManuallyModified(); | |
} | |
} | |
} | |
} | |
}, | |
{ | |
"OnLoadFinished", (dynamic data) => | |
{ | |
current.isLoading = false; | |
current.isPlaying = false; | |
if (data.level != null) | |
current.level = (string) data.level; | |
DebugOutput("Finished loading."); | |
} | |
}, | |
{ | |
"OnGamePaused", (dynamic data) => | |
{ | |
// pause the timer | |
current.isPaused = true; | |
DebugOutput("Game paused."); | |
if (current.isPlaying) | |
{ | |
if (timer.CurrentPhase != TimerPhase.Paused) | |
{ | |
m_timerModel.Pause(); | |
} | |
else | |
{ | |
DebugOutput("No action: Already paused."); | |
} | |
} | |
else | |
{ | |
DebugOutput("No action: Not playing."); | |
} | |
} | |
}, | |
{ | |
"OnGameResumed", (dynamic data) => | |
{ | |
// unpause the timer | |
current.isPaused = false; | |
DebugOutput("Game resumed."); | |
if (current.isPlaying) | |
{ | |
if (timer.CurrentPhase == TimerPhase.Paused) | |
{ | |
m_timerModel.Pause(); // undoes the pause... | |
} | |
else | |
{ | |
DebugOutput("No action: Not paused."); | |
} | |
} | |
else | |
{ | |
DebugOutput("No action: Not playing."); | |
} | |
} | |
}, | |
{ | |
"OnIntroStarted", (dynamic data) => | |
{ | |
if (data.level == null) | |
{ | |
throw new FormatException("Missing 'level'."); | |
} | |
current.isLoading = false; | |
current.isPlaying = false; | |
// set vars for the current level | |
StartLevel(data.level, data.difficulty); | |
DebugOutput(string.Format("Level intro started: {0} @ {1}", current.level, current.difficulty)); | |
} | |
}, | |
{ | |
"OnHeistStarted", (dynamic data) => | |
{ | |
if (data.level == null) | |
{ | |
throw new FormatException("Missing 'level'."); | |
} | |
if (data.level == current.level && current.isPlaying) | |
{ | |
// ignoring | |
DebugOutput("No action: Already started this level."); | |
goto EndOfFunction; // canot use return in action lambdas... | |
} | |
// if (data.difficulty == null) | |
// { | |
// throw new FormatException("Missing 'difficulty'."); | |
// } | |
// housekeeping | |
current.isLoading = false; | |
current.isPlaying = true; | |
current.isPaused = false; | |
current.levelStartTime = timer.CurrentTime; | |
// set vars for the current level | |
StartLevel(data.level, data.difficulty); | |
DebugOutput(string.Format("Level started: {0} @ {1}", current.level, current.difficulty)); | |
EndOfFunction: | |
{ | |
// empty | |
} | |
} | |
}, | |
{ | |
"OnHeistFinished", (dynamic data) => | |
{ | |
if (data.level == null) | |
{ | |
throw new FormatException("Missing 'level'."); | |
} | |
if (data.level != current.level) | |
{ | |
throw new InvalidOperationException("Unexpected level."); | |
} | |
current.isPlaying = false; | |
// sync timer | |
if (data.time != null && data.time is decimal) | |
{ | |
UpdateGameTime(data.time); | |
} | |
Time cTime = timer.CurrentTime; | |
DebugOutput("Game time is " + cTime.GameTime); | |
{ | |
if (data.win != null && ((bool) data.win)) | |
{ | |
// win: Split on the last segment. | |
OnObjectiveAction("<end>", "heist"); | |
} | |
else | |
{ | |
// failure: Move back to the beginning. | |
timer.CurrentSplitIndex = GetObjectiveIndex(current.level, "<begin>"); | |
timer.CallRunManuallyModified(); | |
} | |
} | |
DebugOutput(string.Format("Level ended: {0} @ {1} in {2} seconds", current.level, current.difficulty, data.time ?? -1)); | |
} | |
}, | |
{ | |
"OnObjectiveActivated", (dynamic data) => | |
{ | |
if (data.level == null) | |
{ | |
throw new FormatException("Missing 'level'."); | |
} | |
if (data.id == null || !(data.id is string)) | |
{ | |
throw new FormatException("Missing 'id'."); | |
} | |
if (data.level != current.level) | |
{ | |
throw new InvalidOperationException("Wrong level"); | |
} | |
// sync timer | |
if (data.time != null && data.time is decimal) | |
{ | |
UpdateGameTime(data.time); | |
} | |
string objectiveID = (string)data.id; | |
current.activeObjectives.Add(objectiveID); | |
OnObjectiveAction(objectiveID, "activated"); | |
DebugOutput(string.Format("Objective '{0}' activated.", objectiveID)); | |
} | |
}, | |
{ | |
"OnObjectiveCompleted", (dynamic data) => | |
{ | |
if (data.level == null) | |
{ | |
throw new FormatException("Missing 'level'."); | |
} | |
if (data.id == null || !(data.id is string)) | |
{ | |
throw new FormatException("Missing 'id'."); | |
} | |
if (data.level != current.level || !m_objectives.ContainsKey(current.level)) | |
{ | |
throw new InvalidOperationException("Bad level"); | |
} | |
// sync timer | |
if (data.time != null && data.time is decimal) | |
{ | |
UpdateGameTime(data.time); | |
} | |
string objectiveID = (string)data.id; | |
current.activeObjectives.Remove(objectiveID); | |
// find index that corresponds to level+objective and if no active objective, skip to level end split | |
OnObjectiveAction(objectiveID, "completed"); | |
DebugOutput(string.Format("Objective {0} completed.", objectiveID)); | |
EndOfFunction: | |
{ | |
// empty | |
} | |
} | |
}, | |
{ | |
"OnObjectiveRemoved", (dynamic data) => | |
{ | |
if (data.level == null) | |
{ | |
throw new FormatException("Missing 'level'."); | |
} | |
if (data.id == null || !(data.id is string)) | |
{ | |
throw new FormatException("Missing 'id'."); | |
} | |
if (data.level != current.level) | |
{ | |
throw new InvalidOperationException("Wrong level"); | |
} | |
// sync timer | |
if (data.time != null && data.time is decimal) | |
{ | |
UpdateGameTime(data.time); | |
} | |
string objectiveID = (string)data.id; | |
current.activeObjectives.Remove(objectiveID); | |
// find index that corresponds to level+objective and if no active objective, skip to level end split | |
OnObjectiveAction(objectiveID, "removed"); | |
DebugOutput(string.Format("Objective '{0}' removed.", objectiveID)); | |
EndOfFunction: | |
{ | |
// empty | |
} | |
} | |
} | |
}; | |
// ============================================================= | |
// Event handlers (model events) | |
// ============================================================= | |
LiveSplit.Model.Input.EventHandlerT<LiveSplit.Model.TimerPhase> ModelOnReset = (s, e) => | |
{ | |
print("============================= MODEL ON RESET ============================="); | |
ResetCustomSplits(); | |
}; | |
timer.OnReset += ModelOnReset; | |
vars.ModelOnReset = ModelOnReset; | |
EventHandler ModelOnPause = (s, e) => | |
{ | |
print("============================= MODEL ON PAUSE ============================="); | |
current.levelPauseTime = DateTime.Now; | |
}; | |
timer.OnPause += ModelOnPause; | |
vars.ModelOnPause = ModelOnPause; | |
EventHandler ModelOnResume = (s, e) => | |
{ | |
print("============================= MODEL ON RESUME ============================="); | |
//if (current.levelPauseTime.HasValue) | |
//{ | |
//current.levelPausedTime.Add(DateTime.Now - current.levelPauseTime.Value); | |
//current.levelPauseTime.Value = null; | |
//} | |
}; | |
timer.OnResume += ModelOnResume; | |
vars.ModelOnResume = ModelOnResume; | |
EventHandler ModelOnModified = (s, e) => | |
{ | |
print("============================= MODEL MODIFIED ============================="); | |
if (timer.Run.FilePath != m_splitsFilePath) | |
{ | |
print("============================= NEW RUN LOADED?! ============================="); | |
m_splitsFilePath = timer.Run.FilePath; | |
} | |
}; | |
timer.RunManuallyModified += ModelOnModified; | |
vars.ModelOnModified = ModelOnModified; | |
// ============================================================= | |
// Setup more vars | |
// ============================================================= | |
vars.handlers = handlers; | |
current.isLoading = true; | |
current.isPlaying = false; | |
current.isPaused = false; | |
current.levelStartTime = new Time();//default(Time); | |
current.activeObjectives = new HashSet<string>(); | |
// ============================================================= | |
// Set up segments | |
// ============================================================= | |
{ | |
current.level = vars.level != null ? vars.level : "init"; | |
IRun newRun = LoadSplitsForLevel(current.level); | |
if (newRun != null) | |
timer.Run = newRun; | |
} | |
// ============================================================= | |
// Start pipe listener thread | |
// ============================================================= | |
vars.cts = new CancellationTokenSource(); | |
vars.thread = new System.Threading.Thread(vars.ThreadWorker); | |
vars.thread.Start(); | |
vars.cts.CancelAfter(36000000); // cancel after 10hrs just in case | |
//vars.cts.CancelAfter(180000); | |
//vars.cts.CancelAfter(1800000); | |
print("Setup finished."); | |
StartLevel(current.level, ""); // load without a difficulty | |
} | |
exit | |
{ | |
// ============================================================= | |
// Stop pipe listener thread | |
// ============================================================= | |
var DebugOutput = (Action<string>) vars.DebugOutput; | |
vars.StopThread(DebugOutput, DebugOutput); | |
// ============================================================= | |
// Remove state event handlers | |
// ============================================================= | |
timer.OnReset -= vars.ModelOnReset; | |
timer.OnPause -= vars.ModelOnPause; | |
timer.OnResume -= vars.ModelOnResume; | |
timer.RunManuallyModified -= vars.ModelOnModified; | |
} | |
update | |
{ | |
//Action<string> DebugOutput = (vars.DebugOutput != null) ? ((Action<string>) vars.DebugOutput) : (s) => {}; | |
var DebugOutput = (Action<string>) vars.DebugOutput; | |
for (;;) | |
{ | |
dynamic data; | |
lock (vars.eventsLock) | |
{ | |
if (vars.events.Count == 0) | |
break; | |
data = vars.events.Dequeue(); | |
DebugOutput("Processing data: " + data); | |
} | |
Action<object> handler; | |
string eventType = data.type; | |
if (vars.handlers.TryGetValue(eventType, out handler)) | |
{ | |
DebugOutput(string.Format("Calling the '{0}' event handler...", eventType)); | |
try | |
{ | |
handler(data); | |
} | |
catch (FormatException e) | |
{ | |
DebugOutput(string.Format("Malformed event {0}: {1}.", eventType, e.Message)); | |
} | |
catch (InvalidOperationException e) | |
{ | |
DebugOutput(string.Format("Invalid event {0}: {1}.", eventType, e.Message)); | |
} | |
} | |
else | |
{ | |
DebugOutput(string.Format("No handler for event {0}.", eventType)); | |
} | |
} | |
} | |
start | |
{ | |
return false;//current.isPlaying && !current.isPaused && !current.isLoading; | |
} | |
split | |
{ | |
// It's an autosplitter, indicate support for this | |
return false; | |
} | |
reset | |
{ | |
return false; | |
} | |
isLoading | |
{ | |
return current.isLoading || !current.isPlaying || current.isPaused; | |
} | |
gameTime | |
{ | |
// Indicate support for game times | |
return timer.CurrentTime.GameTime; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment