Skip to content

Instantly share code, notes, and snippets.

@DorentuZ
Last active February 12, 2022 23:12
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save DorentuZ/288fe12fc10c6ba9e87f7593657e926f to your computer and use it in GitHub Desktop.
Save DorentuZ/288fe12fc10c6ba9e87f7593657e926f to your computer and use it in GitHub Desktop.
/*
* 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