chore: initial commit

This commit is contained in:
Anna 2022-03-27 22:40:52 -04:00
commit a5ccd5e691
Signed by: anna
GPG Key ID: 0B391D8F06FCD9E0
22 changed files with 2322 additions and 0 deletions

365
.gitignore vendored Normal file
View File

@ -0,0 +1,365 @@
## Ignore Visual Studio temporary files, build results, and
## files generated by popular Visual Studio add-ons.
##
## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
# Packaging
pack/
# User-specific files
*.rsuser
*.suo
*.user
*.userosscache
*.sln.docstates
# User-specific files (MonoDevelop/Xamarin Studio)
*.userprefs
# Mono auto generated files
mono_crash.*
# Build results
[Dd]ebug/
[Dd]ebugPublic/
[Rr]elease/
[Rr]eleases/
x64/
x86/
[Ww][Ii][Nn]32/
[Aa][Rr][Mm]/
[Aa][Rr][Mm]64/
bld/
[Bb]in/
[Oo]bj/
[Ll]og/
[Ll]ogs/
# Visual Studio 2015/2017 cache/options directory
.vs/
# Uncomment if you have tasks that create the project's static files in wwwroot
#wwwroot/
# Visual Studio 2017 auto generated files
Generated\ Files/
# MSTest test Results
[Tt]est[Rr]esult*/
[Bb]uild[Ll]og.*
# NUnit
*.VisualState.xml
TestResult.xml
nunit-*.xml
# Build Results of an ATL Project
[Dd]ebugPS/
[Rr]eleasePS/
dlldata.c
# Benchmark Results
BenchmarkDotNet.Artifacts/
# .NET Core
project.lock.json
project.fragment.lock.json
artifacts/
# ASP.NET Scaffolding
ScaffoldingReadMe.txt
# StyleCop
StyleCopReport.xml
# Files built by Visual Studio
*_i.c
*_p.c
*_h.h
*.ilk
*.meta
*.obj
*.iobj
*.pch
*.pdb
*.ipdb
*.pgc
*.pgd
*.rsp
*.sbr
*.tlb
*.tli
*.tlh
*.tmp
*.tmp_proj
*_wpftmp.csproj
*.log
*.vspscc
*.vssscc
.builds
*.pidb
*.svclog
*.scc
# Chutzpah Test files
_Chutzpah*
# Visual C++ cache files
ipch/
*.aps
*.ncb
*.opendb
*.opensdf
*.sdf
*.cachefile
*.VC.db
*.VC.VC.opendb
# Visual Studio profiler
*.psess
*.vsp
*.vspx
*.sap
# Visual Studio Trace Files
*.e2e
# TFS 2012 Local Workspace
$tf/
# Guidance Automation Toolkit
*.gpState
# ReSharper is a .NET coding add-in
_ReSharper*/
*.[Rr]e[Ss]harper
*.DotSettings.user
# TeamCity is a build add-in
_TeamCity*
# DotCover is a Code Coverage Tool
*.dotCover
# AxoCover is a Code Coverage Tool
.axoCover/*
!.axoCover/settings.json
# Coverlet is a free, cross platform Code Coverage Tool
coverage*.json
coverage*.xml
coverage*.info
# Visual Studio code coverage results
*.coverage
*.coveragexml
# NCrunch
_NCrunch_*
.*crunch*.local.xml
nCrunchTemp_*
# MightyMoose
*.mm.*
AutoTest.Net/
# Web workbench (sass)
.sass-cache/
# Installshield output folder
[Ee]xpress/
# DocProject is a documentation generator add-in
DocProject/buildhelp/
DocProject/Help/*.HxT
DocProject/Help/*.HxC
DocProject/Help/*.hhc
DocProject/Help/*.hhk
DocProject/Help/*.hhp
DocProject/Help/Html2
DocProject/Help/html
# Click-Once directory
publish/
# Publish Web Output
*.[Pp]ublish.xml
*.azurePubxml
# Note: Comment the next line if you want to checkin your web deploy settings,
# but database connection strings (with potential passwords) will be unencrypted
*.pubxml
*.publishproj
# Microsoft Azure Web App publish settings. Comment the next line if you want to
# checkin your Azure Web App publish settings, but sensitive information contained
# in these scripts will be unencrypted
PublishScripts/
# NuGet Packages
*.nupkg
# NuGet Symbol Packages
*.snupkg
# The packages folder can be ignored because of Package Restore
**/[Pp]ackages/*
# except build/, which is used as an MSBuild target.
!**/[Pp]ackages/build/
# Uncomment if necessary however generally it will be regenerated when needed
#!**/[Pp]ackages/repositories.config
# NuGet v3's project.json files produces more ignorable files
*.nuget.props
*.nuget.targets
# Microsoft Azure Build Output
csx/
*.build.csdef
# Microsoft Azure Emulator
ecf/
rcf/
# Windows Store app package directories and files
AppPackages/
BundleArtifacts/
Package.StoreAssociation.xml
_pkginfo.txt
*.appx
*.appxbundle
*.appxupload
# Visual Studio cache files
# files ending in .cache can be ignored
*.[Cc]ache
# but keep track of directories ending in .cache
!?*.[Cc]ache/
# Others
ClientBin/
~$*
*~
*.dbmdl
*.dbproj.schemaview
*.jfm
*.pfx
*.publishsettings
orleans.codegen.cs
# Including strong name files can present a security risk
# (https://github.com/github/gitignore/pull/2483#issue-259490424)
#*.snk
# Since there are multiple workflows, uncomment next line to ignore bower_components
# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
#bower_components/
# RIA/Silverlight projects
Generated_Code/
# Backup & report files from converting an old project file
# to a newer Visual Studio version. Backup files are not needed,
# because we have git ;-)
_UpgradeReport_Files/
Backup*/
UpgradeLog*.XML
UpgradeLog*.htm
ServiceFabricBackup/
*.rptproj.bak
# SQL Server files
*.mdf
*.ldf
*.ndf
# Business Intelligence projects
*.rdl.data
*.bim.layout
*.bim_*.settings
*.rptproj.rsuser
*- [Bb]ackup.rdl
*- [Bb]ackup ([0-9]).rdl
*- [Bb]ackup ([0-9][0-9]).rdl
# Microsoft Fakes
FakesAssemblies/
# GhostDoc plugin setting file
*.GhostDoc.xml
# Node.js Tools for Visual Studio
.ntvs_analysis.dat
node_modules/
# Visual Studio 6 build log
*.plg
# Visual Studio 6 workspace options file
*.opt
# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
*.vbw
# Visual Studio LightSwitch build output
**/*.HTMLClient/GeneratedArtifacts
**/*.DesktopClient/GeneratedArtifacts
**/*.DesktopClient/ModelManifest.xml
**/*.Server/GeneratedArtifacts
**/*.Server/ModelManifest.xml
_Pvt_Extensions
# Paket dependency manager
.paket/paket.exe
paket-files/
# FAKE - F# Make
.fake/
# CodeRush personal settings
.cr/personal
# Python Tools for Visual Studio (PTVS)
__pycache__/
*.pyc
# Cake - Uncomment if you are using it
# tools/**
# !tools/packages.config
# Tabs Studio
*.tss
# Telerik's JustMock configuration file
*.jmconfig
# BizTalk build output
*.btp.cs
*.btm.cs
*.odx.cs
*.xsd.cs
# OpenCover UI analysis results
OpenCover/
# Azure Stream Analytics local run output
ASALocalRun/
# MSBuild Binary and Structured Log
*.binlog
# NVidia Nsight GPU debugger configuration file
*.nvuser
# MFractors (Xamarin productivity tool) working folder
.mfractor/
# Local History for Visual Studio
.localhistory/
# BeatPulse healthcheck temp database
healthchecksdb
# Backup folder for Package Reference Convert tool in Visual Studio 2017
MigrationBackup/
# Ionide (cross platform F# VS Code tools) working folder
.ionide/
# Fody - auto-generated XML schema
FodyWeavers.xsd

16
LiveSplit.TZA.sln Executable file
View File

@ -0,0 +1,16 @@

Microsoft Visual Studio Solution File, Format Version 12.00
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LiveSplit.TZA", "LiveSplit.TZA\LiveSplit.TZA.csproj", "{2AD5AFB0-A544-462C-9A42-4254898C44A6}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{2AD5AFB0-A544-462C-9A42-4254898C44A6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{2AD5AFB0-A544-462C-9A42-4254898C44A6}.Debug|Any CPU.Build.0 = Debug|Any CPU
{2AD5AFB0-A544-462C-9A42-4254898C44A6}.Release|Any CPU.ActiveCfg = Release|Any CPU
{2AD5AFB0-A544-462C-9A42-4254898C44A6}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
EndGlobal

113
LiveSplit.TZA/GameMemory.cs Executable file
View File

@ -0,0 +1,113 @@
using System.Diagnostics;
using System.Runtime.InteropServices;
using LiveSplit.ComponentUtil;
using LiveSplit.TZA.Splits;
namespace LiveSplit.TZA;
public class GameMemory : MemoryWatcherList {
private Process Game { get; }
internal MemoryWatcher<uint> IsLoaded { get; }
internal MemoryWatcher<ulong> ConfigMenu { get; }
internal MemoryWatcher<uint> Stage { get; }
internal MemoryWatcher<ushort> Location { get; }
internal MemoryWatcher<uint> BossHp { get; }
internal MemoryWatcher<UndyingInfo> UndyingInfo { get; }
internal StringWatcher Cutscene { get; }
internal Dictionary<Character, MemoryWatcher<byte>> Levels { get; } = new();
public GameMemory(Process game) {
this.Game = game;
var scanner = new SignatureScanner(game, game.MainModuleWow64Safe().BaseAddress, game.MainModuleWow64Safe().ModuleMemorySize);
var configPtrIns = scanner.Scan(new SigScanTarget(3, "48 89 1D ?? ?? ?? ?? 89 BB ?? ?? ?? ?? 66 89 BB"));
this.ConfigMenu = new MemoryWatcher<ulong>(new DeepPointer(configPtrIns + 4 + game.ReadValue<int>(configPtrIns))) {
FailAction = MemoryWatcher.ReadFailAction.SetZeroOrNull,
};
var dataPtrIns = scanner.Scan(new SigScanTarget(3, "48 8D 0D ?? ?? ?? ?? 41 B8 ?? ?? ?? ?? E8 ?? ?? ?? ?? 33 C9 E8"));
if (!new DeepPointer(dataPtrIns + 4 + game.ReadValue<int>(dataPtrIns)).DerefOffsets(this.Game, out var dataPtr)) {
dataPtr = IntPtr.Zero;
}
this.Stage = new MemoryWatcher<uint>(dataPtr + 0x200) {
FailAction = MemoryWatcher.ReadFailAction.SetZeroOrNull,
};
this.Location = new MemoryWatcher<ushort>(dataPtr - 0x90) {
FailAction = MemoryWatcher.ReadFailAction.SetZeroOrNull,
};
var bossHpIns = scanner.Scan(new SigScanTarget(3, "48 8D 1D ?? ?? ?? ?? 8B F5 48 89 29 8B FD 4C 8B"));
if (!new DeepPointer(bossHpIns + 4 + game.ReadValue<int>(bossHpIns)).DerefOffsets(this.Game, out var bossHpPtr)) {
bossHpPtr = IntPtr.Zero;
}
this.BossHp = new MemoryWatcher<uint>(bossHpPtr - 4) {
FailAction = MemoryWatcher.ReadFailAction.SetZeroOrNull,
};
var partyIns = scanner.Scan(new SigScanTarget(15, "F2 0F 10 05 ?? ?? ?? ?? F2 0F 11 41 ?? 8B 05 ?? ?? ?? ?? 89 41 28 C3"));
if (!new DeepPointer(partyIns + 4 + game.ReadValue<int>(partyIns)).DerefOffsets(this.Game, out var partyPtr)) {
partyPtr = IntPtr.Zero;
}
partyPtr += 4 + 8;
foreach (var character in (Character[]) Enum.GetValues(typeof(Character))) {
this.Levels[character] = new MemoryWatcher<byte>(partyPtr + 0x1C8 * (int) character + 0x1C2) {
FailAction = MemoryWatcher.ReadFailAction.SetZeroOrNull,
};
}
var cutsceneNameIns = scanner.Scan(new SigScanTarget(3, "48 8B 05 ?? ?? ?? ?? 48 3B 05 ?? ?? ?? ?? 74 11 C6 00 00 48 8B 05 ?? ?? ?? ?? 48 89 05 ?? ?? ?? ?? 48 63 0D"));
if (!new DeepPointer(cutsceneNameIns + 4 + game.ReadValue<int>(cutsceneNameIns)).DerefOffsets(this.Game, out var cutsceneNamePtr)) {
cutsceneNamePtr = IntPtr.Zero;
}
this.Cutscene = new StringWatcher(new DeepPointer(cutsceneNamePtr, 0), ReadStringType.UTF8, 32) {
FailAction = MemoryWatcher.ReadFailAction.SetZeroOrNull,
};
var isLoadedIns = scanner.Scan(new SigScanTarget(3, "4C 8D 3D ?? ?? ?? ?? 48 89 0D ?? ?? ?? ?? 48 85 C0 74 31 8B 48 0C E8"));
if (!new DeepPointer(isLoadedIns + 4 + game.ReadValue<int>(isLoadedIns)).DerefOffsets(this.Game, out var isLoadedPtr)) {
isLoadedPtr = IntPtr.Zero;
}
this.IsLoaded = new MemoryWatcher<uint>(isLoadedPtr) {
FailAction = MemoryWatcher.ReadFailAction.SetZeroOrNull,
};
var undyingInfoIns = scanner.Scan(new SigScanTarget(17, "33 C9 B8 ?? ?? ?? ?? 66 89 05 ?? ?? ?? ?? 48 89 0D ?? ?? ?? ?? 89 0D ?? ?? ?? ?? 66 89 0D"));
this.UndyingInfo = new MemoryWatcher<UndyingInfo>(new DeepPointer(undyingInfoIns + 4 + game.ReadValue<int>(undyingInfoIns), 0)) {
FailAction = MemoryWatcher.ReadFailAction.SetZeroOrNull,
};
}
public bool Update() {
if (this.Game.HasExited) {
return false;
}
this.IsLoaded.Update(this.Game);
this.ConfigMenu.Update(this.Game);
this.Stage.Update(this.Game);
this.Location.Update(this.Game);
this.BossHp.Update(this.Game);
this.UndyingInfo.Update(this.Game);
foreach (var level in this.Levels.Values) {
level.Update(this.Game);
}
this.Cutscene.Update(this.Game);
return true;
}
}
[StructLayout(LayoutKind.Explicit, Pack = 1)]
internal struct UndyingInfo {
[FieldOffset(0x48)]
internal readonly uint Hp;
}

View File

@ -0,0 +1,33 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net461</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<LangVersion>preview</LangVersion>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>
<ItemGroup>
<Using Remove="System.Net.Http" />
</ItemGroup>
<ItemGroup>
<Reference Include="LiveSplit.Core">
<HintPath>..\..\LiveSplit\LiveSplit\bin\Release\LiveSplit.Core.dll</HintPath>
<Private>false</Private>
</Reference>
<Reference Include="System.Windows.Forms" />
<Reference Include="UpdateManager">
<HintPath>..\..\LiveSplit\LiveSplit\bin\Release\UpdateManager.dll</HintPath>
<Private>false</Private>
</Reference>
</ItemGroup>
<ItemGroup>
<Compile Update="TzaControl.cs">
<SubType>UserControl</SubType>
</Compile>
</ItemGroup>
</Project>

View File

@ -0,0 +1,4 @@
using LiveSplit.TZA;
using LiveSplit.UI.Components;
[assembly: ComponentFactory(typeof(TzaFactory))]

259
LiveSplit.TZA/SplitLogic.cs Executable file
View File

@ -0,0 +1,259 @@
using LiveSplit.Model;
using LiveSplit.TZA.Splits;
namespace LiveSplit.TZA;
internal class SplitLogic {
internal static readonly List<SplitInfo> Splits = new() {
new SplitInfo(
SplitId.StartGame,
new GameStart()
),
new SplitInfo(
SplitId.AirCutterRemora,
new KillBoss(16, 275, 1_000)
),
new SplitInfo(
SplitId.Reks,
new Cutscene("naf_b0401", CutsceneAction.Enter),
new StageChange(25)
),
new SplitInfo(
SplitId.Rats,
new Cutscene("grm_g0100", CutsceneAction.Enter),
new Cutscene("grm_g0100", CutsceneAction.Exit)
),
new SplitInfo(
SplitId.RogueTomato,
new StageChange(78)
),
new SplitInfo(
SplitId.Sunstone,
new EnterLocationWithStage(306, 160)
),
new SplitInfo(
SplitId.Dustia,
new LevelUp(Character.Vaan, 13)
),
new SplitInfo(
SplitId.Flans,
new StageChange(265)
),
new SplitInfo(
SplitId.Firemane,
new KillBoss(265, 329),
new Cutscene("grm_d0102", CutsceneAction.Exit)
),
new SplitInfo(
SplitId.MimicQueen,
new KillBoss(340, 69),
new Cutscene("mic_c0101", CutsceneAction.Exit)
),
new SplitInfo(
SplitId.LhusuMines,
new EnterLocationWithStage(806, 680)
),
new SplitInfo(
SplitId.Judges,
new StageChange(966)
),
new SplitInfo(
SplitId.JudgeGhis,
new KillBoss(1000, 552, 1_030),
new Cutscene("rsn_a0102", CutsceneAction.Exit)
),
new SplitInfo(
SplitId.Garuda,
new KillBoss(1352, 403),
new Cutscene("rwg_a0181", CutsceneAction.Exit)
),
new SplitInfo(
SplitId.DemonWall2,
new KillBoss(1364, 407),
new Cutscene("rwg_b0202", CutsceneAction.Exit)
),
new SplitInfo(
SplitId.Belias,
new KillBoss(1376, 415),
new Cutscene("rwg_d0101", CutsceneAction.Exit)
),
new SplitInfo(
SplitId.Vossler,
new KillBoss(1460, 872),
new Cutscene("rsn_z0201", CutsceneAction.Exit)
),
new SplitInfo(
SplitId.EnterEruytVillage,
new EnterLocation(768)
),
new SplitInfo(
SplitId.LeaveEruytVillage,
new EnterLocationWithStage(751, 1790)
),
new SplitInfo(
SplitId.OsmonePlain,
new EnterLocation(712)
),
new SplitInfo(
SplitId.Jellies,
new EnterLocation(718)
),
new SplitInfo(
SplitId.Tiamat,
new KillBoss(1820, 722),
new Cutscene("hne_b0101", CutsceneAction.Exit)
),
new SplitInfo(
SplitId.EnterStilshrine,
new EnterLocation(597)
),
new SplitInfo(
SplitId.Vinuskar,
new KillBoss(2057, 603),
new Cutscene("mrm_c0101", CutsceneAction.Exit)
),
new SplitInfo(
SplitId.Mateus,
new KillBoss(2100, 612),
new Cutscene("mrm_e0101", CutsceneAction.Exit)
),
new SplitInfo(
SplitId.JudgeBergan,
new KillBoss(2290, 737),
new Cutscene("bul_a0201", CutsceneAction.Exit)
),
new SplitInfo(
SplitId.Salikawood,
new EnterLocation(483)
),
new SplitInfo(
SplitId.PhonCoastTchitaUplands,
new EnterLocation(187)
),
new SplitInfo(
SplitId.Mandragoras,
new Cutscene("rui_d0101", CutsceneAction.Exit)
),
new SplitInfo(
SplitId.Ahriman,
new KillBoss(new uint[] { 3150, 3200 }, 198),
new Cutscene("rui_c0101", CutsceneAction.Exit)
),
new SplitInfo(
SplitId.Archades,
new EnterLocation(1008)
),
new SplitInfo(
SplitId.Cid1,
new KillBoss(3440, 286, 18_247),
new Cutscene("dor_b0101", CutsceneAction.Exit)
),
new SplitInfo(
SplitId.Rafflesia,
new KillBoss(4160, 545),
new Cutscene("mfr_b0101", CutsceneAction.Exit)
),
new SplitInfo(
SplitId.Daedalus,
new KillBoss(4295, 687),
new Cutscene("gil_a0201", CutsceneAction.Exit)
),
new SplitInfo(
SplitId.Tyrant,
new KillBoss(4320, 695),
new Cutscene("gil_c0201", CutsceneAction.Exit)
),
new SplitInfo(
SplitId.Shemhazai,
new KillBoss(4370, 698),
new Cutscene("gil_d0101", CutsceneAction.Exit)
),
new SplitInfo(
SplitId.Hydro,
new KillBoss(5200, 522),
new Cutscene("rwf_c0101", CutsceneAction.Exit)
),
new SplitInfo(
SplitId.Pandaemonium,
new KillBoss(5303, 1113),
new Cutscene("rbl_c0101", CutsceneAction.Exit)
),
new SplitInfo(
SplitId.Slyt,
new KillBoss(5320, 1121),
new Cutscene("rbl_f0101", CutsceneAction.Exit)
),
new SplitInfo(
SplitId.Fenrir,
new KillBoss(5320, 1132),
new Cutscene("rbl_j0101", CutsceneAction.Exit)
),
new SplitInfo(
SplitId.Hashmal,
new KillBoss(5320, 670),
new Cutscene("dgl_h0101", CutsceneAction.Exit)
),
new SplitInfo(
SplitId.Cid2,
new Cutscene("dgl_g0103", CutsceneAction.Exit)
// new KillBoss(0, 0, 20_523) - too many boss HP bars in same stage and area
),
new SplitInfo(
SplitId.Gabranth,
new KillBoss(6600, 779),
new Cutscene("bhm_b0101", CutsceneAction.Exit)
),
new SplitInfo(
SplitId.Vayne,
new KillBoss(6750, 782),
new Cutscene("bhm_c0101", CutsceneAction.Exit)
),
new SplitInfo(
SplitId.VayneNovus,
new KillBoss(new uint[] { 6850, 6900 }, 782)
),
new SplitInfo(
SplitId.TheUndying,
new TheUndying()
),
};
private HashSet<uint> Completed { get; } = new();
internal LogicResult Tick(TzaControl control, TimerPhase phase, GameMemory memory) {
// never do anything if the timer is ended
if (phase == TimerPhase.Ended) {
return LogicResult.None;
}
foreach (var info in Splits) {
if (this.Completed.Contains(info.Id)) {
continue;
}
if (!control.EnabledSplits.TryGetValue(info.Id, out var idx)) {
continue;
}
if (idx >= info.Splits.Length) {
continue;
}
var result = info.Splits[idx].Calculate(phase, memory);
if (result == LogicResult.None) {
continue;
}
this.Completed.Add(info.Id);
return result;
}
return LogicResult.None;
}
}
internal enum LogicResult {
None,
Start,
Split,
}

View File

@ -0,0 +1,42 @@
using LiveSplit.Model;
namespace LiveSplit.TZA.Splits;
internal class Cutscene : ISplit {
private string Name { get; }
private CutsceneAction Action { get; }
public Cutscene(string name, CutsceneAction action) {
this.Name = name;
this.Action = action;
}
public LogicResult Calculate(TimerPhase phase, GameMemory memory) {
if (phase != TimerPhase.Running) {
return LogicResult.None;
}
if (!memory.Cutscene.Changed) {
return LogicResult.None;
}
switch (this.Action) {
case CutsceneAction.Enter when memory.Cutscene.Current != this.Name:
case CutsceneAction.Exit when memory.Cutscene.Old != this.Name:
return LogicResult.None;
default:
return LogicResult.Split;
}
}
public string GetHumanName() => this.Action switch {
CutsceneAction.Enter => "After cutscene starts",
CutsceneAction.Exit => "After cutscene ends",
_ => "Unknown cutscene action - please report this",
};
}
internal enum CutsceneAction {
Enter,
Exit,
}

View File

@ -0,0 +1,26 @@
using LiveSplit.Model;
using LiveSplit.TZA.Util;
namespace LiveSplit.TZA.Splits;
internal class EnterLocation : ISplit {
private ushort Location { get; }
internal EnterLocation(ushort location) {
this.Location = location;
}
public LogicResult Calculate(TimerPhase phase, GameMemory memory) {
if (phase != TimerPhase.Running) {
return LogicResult.None;
}
if (!memory.Location.Changed || memory.Location.Current != this.Location) {
return LogicResult.None;
}
return LogicResult.Split;
}
public string GetHumanName() => $"After entering {LocationNames.Get(this.Location)}";
}

View File

@ -0,0 +1,32 @@
using LiveSplit.Model;
using LiveSplit.TZA.Util;
namespace LiveSplit.TZA.Splits;
internal class EnterLocationWithStage : ISplit {
private ushort Location { get; }
private uint Stage { get; }
internal EnterLocationWithStage(ushort location, uint stage) {
this.Location = location;
this.Stage = stage;
}
public LogicResult Calculate(TimerPhase phase, GameMemory memory) {
if (phase != TimerPhase.Running) {
return LogicResult.None;
}
if (memory.Stage.Current != this.Stage) {
return LogicResult.None;
}
if (!memory.Location.Changed || memory.Location.Current != this.Location) {
return LogicResult.None;
}
return LogicResult.Split;
}
public string GetHumanName() => $"After entering {LocationNames.Get(this.Location)} during game stage {this.Stage}";
}

View File

@ -0,0 +1,30 @@
using LiveSplit.Model;
namespace LiveSplit.TZA.Splits;
internal class GameStart : ISplit {
public LogicResult Calculate(TimerPhase phase, GameMemory memory) {
if (phase != TimerPhase.NotRunning) {
return LogicResult.None;
}
if (memory.Stage.Current != 0) {
return LogicResult.None;
}
if (memory.Location.Current != 12) {
return LogicResult.None;
}
if (memory.ConfigMenu.Current != 0 || !memory.ConfigMenu.Changed) {
return LogicResult.None;
}
// this is extremely finicky
// going from the new game screen to ANY screen activates this
return LogicResult.Start;
}
public string GetHumanName() => "After selecting New Game";
}

9
LiveSplit.TZA/Splits/ISplit.cs Executable file
View File

@ -0,0 +1,9 @@
using LiveSplit.Model;
namespace LiveSplit.TZA.Splits;
internal interface ISplit {
LogicResult Calculate(TimerPhase phase, GameMemory memory);
string GetHumanName();
}

View File

@ -0,0 +1,44 @@
using LiveSplit.Model;
using LiveSplit.TZA.Util;
namespace LiveSplit.TZA.Splits;
internal class KillBoss : ISplit {
private uint[] Stage { get; }
private ushort Location { get; }
private uint HpRemaining { get; }
internal KillBoss(uint stage, ushort location, uint hpRemaining = 0) {
this.Stage = new[] { stage };
this.Location = location;
this.HpRemaining = hpRemaining;
}
internal KillBoss(uint[] stage, ushort location, uint hpRemaining = 0) {
this.Stage = stage;
this.Location = location;
this.HpRemaining = hpRemaining;
}
public LogicResult Calculate(TimerPhase phase, GameMemory memory) {
if (phase != TimerPhase.Running) {
return LogicResult.None;
}
if (!this.Stage.Contains(memory.Stage.Current)) {
return LogicResult.None;
}
if (memory.Location.Current != this.Location) {
return LogicResult.None;
}
if (!memory.BossHp.Changed || memory.BossHp.Current > this.HpRemaining) {
return LogicResult.None;
}
return LogicResult.Split;
}
public string GetHumanName() => $"When boss in {LocationNames.Get(this.Location)} has {this.HpRemaining:N0} HP remaining";
}

37
LiveSplit.TZA/Splits/LevelUp.cs Executable file
View File

@ -0,0 +1,37 @@
using LiveSplit.Model;
namespace LiveSplit.TZA.Splits;
internal class LevelUp : ISplit {
private Character Character { get; }
private byte Level { get; }
public LevelUp(Character character, byte level) {
this.Character = character;
this.Level = level;
}
public LogicResult Calculate(TimerPhase phase, GameMemory memory) {
if (phase != TimerPhase.Running) {
return LogicResult.None;
}
var watcher = memory.Levels[this.Character];
if (!watcher.Changed || watcher.Old != this.Level - 1 || watcher.Current != this.Level) {
return LogicResult.None;
}
return LogicResult.Split;
}
public string GetHumanName() => $"When {this.Character} reaches level {this.Level}";
}
internal enum Character {
Vaan = 0,
Ashe = 1,
Fran = 2,
Balthier = 3,
Basch = 4,
Penelo = 5,
}

117
LiveSplit.TZA/Splits/SplitInfo.cs Executable file
View File

@ -0,0 +1,117 @@
namespace LiveSplit.TZA.Splits;
internal class SplitInfo {
internal uint Id { get; }
internal string Name { get; }
internal ISplit[] Splits { get; }
internal SplitInfo(SplitId id, params ISplit[] splits) {
this.Id = (uint) id;
this.Name = id.Name();
this.Splits = splits;
}
}
// NOTE: reordering is a breaking change
internal enum SplitId {
StartGame = 1,
Reks,
Rats,
RogueTomato,
Dustia,
Firemane,
MimicQueen,
JudgeGhis,
Garuda,
Belias,
Jellies,
Tiamat,
EnterStilshrine,
Vinuskar,
Mateus,
JudgeBergan,
Mandragoras,
Ahriman,
Cid1,
Rafflesia,
Daedalus,
Tyrant,
Shemhazai,
Hydro,
Pandaemonium,
Slyt,
Fenrir,
Hashmal,
Cid2,
Gabranth,
TheUndying,
// added later
Judges,
DemonWall2,
Vossler,
Flans,
Sunstone,
AirCutterRemora,
Vayne,
VayneNovus,
LhusuMines,
LeaveEruytVillage,
OsmonePlain,
Salikawood,
PhonCoastTchitaUplands,
Archades,
EnterEruytVillage,
}
internal static class SplitIdExt {
internal static string Name(this SplitId id) => id switch {
SplitId.StartGame => "Start Game",
SplitId.Reks => "Reks",
SplitId.Rats => "Rats",
SplitId.RogueTomato => "Rogue Tomato",
SplitId.Dustia => "Dustia",
SplitId.Firemane => "Firemane",
SplitId.MimicQueen => "Mimic Queen",
SplitId.JudgeGhis => "Judge Ghis",
SplitId.Garuda => "Garuda",
SplitId.Belias => "Belias",
SplitId.Jellies => "Jellies",
SplitId.Tiamat => "Tiamat",
SplitId.EnterStilshrine => "Enter Stilshrine",
SplitId.Vinuskar => "Vinuskar",
SplitId.Mateus => "Mateus",
SplitId.JudgeBergan => "Judge Bergan",
SplitId.Mandragoras => "Mandragoras",
SplitId.Ahriman => "Ahriman",
SplitId.Cid1 => "Cid 1",
SplitId.Rafflesia => "Rafflesia",
SplitId.Daedalus => "Daedalus",
SplitId.Tyrant => "Tyrant",
SplitId.Shemhazai => "Shemhazai",
SplitId.Hydro => "Hydro",
SplitId.Pandaemonium => "Pandaemonium",
SplitId.Slyt => "Slyt",
SplitId.Fenrir => "Fenrir",
SplitId.Hashmal => "Hashmal",
SplitId.Cid2 => "Cid 2",
SplitId.Gabranth => "Gabranth",
SplitId.TheUndying => "The Undying",
SplitId.Judges => "Judges",
SplitId.DemonWall2 => "Demon Wall 2",
SplitId.Vossler => "Vossler",
SplitId.Flans => "Flans",
SplitId.AirCutterRemora => "Air Cutter Remora",
SplitId.Sunstone => "Sunstone",
SplitId.Vayne => "Vayne",
SplitId.VayneNovus => "Vayne Novus",
SplitId.LhusuMines => "Lhusu Mines",
SplitId.LeaveEruytVillage => "Leave Eruyt Village",
SplitId.OsmonePlain => "Osmone Plain",
SplitId.Salikawood => "Salikawood",
SplitId.PhonCoastTchitaUplands => "Phon Coast & Tchita Uplands",
SplitId.Archades => "Archades",
SplitId.EnterEruytVillage => "Enter Eruyt Village",
_ => throw new ArgumentOutOfRangeException(nameof(id), id, null),
};
}

View File

@ -0,0 +1,26 @@
using LiveSplit.Model;
namespace LiveSplit.TZA.Splits;
internal class StageChange : ISplit {
private uint Stage { get; }
internal StageChange(uint stage) {
this.Stage = stage;
}
public LogicResult Calculate(TimerPhase phase, GameMemory memory) {
if (phase != TimerPhase.Running) {
return LogicResult.None;
}
// previous stage being 0 means either new game, trial, or loading save
if (!memory.Stage.Changed || memory.Stage.Old == 0 || memory.Stage.Current != this.Stage) {
return LogicResult.None;
}
return LogicResult.Split;
}
public string GetHumanName() => $"When the game stage advances to {this.Stage}";
}

View File

@ -0,0 +1,27 @@
using LiveSplit.Model;
namespace LiveSplit.TZA.Splits;
internal class TheUndying : ISplit {
public LogicResult Calculate(TimerPhase phase, GameMemory memory) {
if (phase != TimerPhase.Running) {
return LogicResult.None;
}
if (memory.Stage.Current != 7100) {
return LogicResult.None;
}
if (memory.Location.Current != 785) {
return LogicResult.None;
}
if (!memory.UndyingInfo.Changed || memory.UndyingInfo.Current.Hp != 0) {
return LogicResult.None;
}
return LogicResult.Split;
}
public string GetHumanName() => "On last hit on The Undying";
}

106
LiveSplit.TZA/TzaComponent.cs Executable file
View File

@ -0,0 +1,106 @@
using System.Diagnostics;
using System.Timers;
using System.Windows.Forms;
using System.Xml;
using LiveSplit.Model;
using LiveSplit.UI;
using LiveSplit.UI.Components;
using SystemTimer = System.Timers.Timer;
namespace LiveSplit.TZA;
public class TzaComponent : LogicComponent {
public override string ComponentName => "FFXII: The Zodiac Age";
internal TimerModel Timer { get; }
internal GameMemory? GameMemory { get; set; }
private TzaControl Control { get; }
private SystemTimer UpdateTimer { get; }
private SplitLogic Logic { get; set; }
public TzaComponent(LiveSplitState state) {
this.Timer = new TimerModel { CurrentState = state };
this.Control = new TzaControl();
this.Logic = new SplitLogic();
this.UpdateTimer = new SystemTimer(16f);
this.UpdateTimer.Elapsed += this.UpdateTimeUpdate;
this.UpdateTimer.Start();
this.Timer.CurrentState.OnStart += this.OnStart;
this.Timer.CurrentState.OnReset += this.OnReset;
}
public override void Dispose() {
this.Timer.CurrentState.OnReset -= this.OnReset;
this.Timer.CurrentState.OnStart += this.OnStart;
this.UpdateTimer.Stop();
this.UpdateTimer.Dispose();
}
private void OnStart(object sender, EventArgs e) {
this.Timer.InitializeGameTime();
}
private void OnReset(object sender, TimerPhase value) {
this.Logic = new SplitLogic();
}
private void UpdateTimeUpdate(object? sender, ElapsedEventArgs? e) {
// if (this.Timer.CurrentState.Run.GameName != "FFXII: The Zodiac Age") {
// return;
// }
if (this.GameMemory == null) {
var game = Process.GetProcessesByName("FFXII_TZA").FirstOrDefault();
if (game != null) {
this.GameMemory = new GameMemory(game);
}
}
if (this.GameMemory != null) {
if (!this.GameMemory.Update()) {
this.GameMemory = null;
}
}
if (this.GameMemory == null) {
this.Timer.CurrentState.IsGameTimePaused = false;
return;
}
if (this.Timer.CurrentState.CurrentPhase is not (TimerPhase.NotRunning or TimerPhase.Ended) && this.GameMemory.Location.Current is 12 or 13) {
// if timer is started and at main menu or credits, never count as paused
this.Timer.CurrentState.IsGameTimePaused = false;
} else {
this.Timer.CurrentState.IsGameTimePaused = (this.GameMemory.IsLoaded.Current & 0x100) != 0;
}
switch (this.Logic.Tick(this.Control, this.Timer.CurrentState.CurrentPhase, this.GameMemory)) {
case LogicResult.Start:
this.Timer.Start();
break;
case LogicResult.Split:
this.Timer.Split();
break;
case LogicResult.None:
default:
// do nothing
break;
}
}
public override Control GetSettingsControl(LayoutMode mode) {
return this.Control;
}
public override XmlNode GetSettings(XmlDocument document) {
return this.Control.GetSettings(document);
}
public override void SetSettings(XmlNode settings) {
this.Control.SetSettings(settings);
}
public override void Update(IInvalidator invalidator, LiveSplitState state, float width, float height, LayoutMode mode) {
}
}

78
LiveSplit.TZA/TzaControl.Designer.cs generated Executable file
View File

@ -0,0 +1,78 @@
using System.ComponentModel;
namespace LiveSplit.TZA;
partial class TzaControl {
/// <summary>
/// Required designer variable.
/// </summary>
private IContainer components = null;
/// <summary>
/// Clean up any resources being used.
/// </summary>
/// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
protected override void Dispose(bool disposing) {
if (disposing && (components != null)) {
components.Dispose();
}
base.Dispose(disposing);
}
#region Component Designer generated code
/// <summary>
/// Required method for Designer support - do not modify
/// the contents of this method with the code editor.
/// </summary>
private void InitializeComponent() {
this.components = new System.ComponentModel.Container();
this.splitsPanel = new System.Windows.Forms.TreeView();
this.splitsGroupBox = new System.Windows.Forms.GroupBox();
this.toolTip = new System.Windows.Forms.ToolTip(this.components);
this.splitsGroupBox.SuspendLayout();
this.SuspendLayout();
//
// splitsPanel
//
this.splitsPanel.CheckBoxes = true;
this.splitsPanel.Dock = System.Windows.Forms.DockStyle.Fill;
this.splitsPanel.Location = new System.Drawing.Point(3, 16);
this.splitsPanel.Name = "splitsPanel";
this.splitsPanel.Size = new System.Drawing.Size(283, 318);
this.splitsPanel.TabIndex = 1;
//
// splitsGroupBox
//
this.splitsGroupBox.AutoSize = true;
this.splitsGroupBox.Controls.Add(this.splitsPanel);
this.splitsGroupBox.Dock = System.Windows.Forms.DockStyle.Fill;
this.splitsGroupBox.Location = new System.Drawing.Point(10, 10);
this.splitsGroupBox.Name = "splitsGroupBox";
this.splitsGroupBox.Size = new System.Drawing.Size(289, 337);
this.splitsGroupBox.TabIndex = 1;
this.splitsGroupBox.TabStop = false;
this.splitsGroupBox.Text = "Splits";
//
// TzaControl
//
this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F);
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
this.Controls.Add(this.splitsGroupBox);
this.Name = "TzaControl";
this.Padding = new System.Windows.Forms.Padding(10);
this.Size = new System.Drawing.Size(309, 357);
this.splitsGroupBox.ResumeLayout(false);
this.ResumeLayout(false);
this.PerformLayout();
}
private System.Windows.Forms.ToolTip toolTip;
private System.Windows.Forms.GroupBox splitsGroupBox;
private System.Windows.Forms.TreeView splitsPanel;
#endregion
}

117
LiveSplit.TZA/TzaControl.cs Executable file
View File

@ -0,0 +1,117 @@
using System.Windows.Forms;
using System.Xml;
using LiveSplit.TZA.Splits;
namespace LiveSplit.TZA;
public partial class TzaControl : UserControl {
internal Dictionary<uint, int> EnabledSplits { get; } = new();
private Dictionary<uint, TreeNode> CheckBoxes { get; } = new();
private bool _updating;
public TzaControl() {
this.InitializeComponent();
this.SuspendLayout();
this.Dock = DockStyle.Fill;
this.splitsPanel.AfterCheck += (_, args) => {
if (this._updating) {
return;
}
var isChecked = args.Node.Checked;
if (args.Node.Tag is SplitInfo info) {
// this is a parent node
if (isChecked) {
this.EnabledSplits[info.Id] = 0;
} else {
this.EnabledSplits.Remove(info.Id);
}
} else if (args.Node.Tag is int idx && args.Node.Parent.Tag is SplitInfo parentInfo) {
// this is a child node
if (isChecked) {
this.EnabledSplits[parentInfo.Id] = idx;
} else {
this.EnabledSplits.Remove(parentInfo.Id);
}
}
this.UpdateCheckboxState();
};
foreach (var info in SplitLogic.Splits) {
var node = new TreeNode($"({info.Splits.Length}) {info.Name}") {
Tag = info,
Checked = this.EnabledSplits.ContainsKey(info.Id),
};
for (var i = 0; i < info.Splits.Length; i++) {
var split = info.Splits[i];
node.Nodes.Add(new TreeNode(split.GetHumanName()) {
Tag = i,
Checked = this.EnabledSplits.TryGetValue(info.Id, out var idx) && idx == i,
});
}
this.CheckBoxes[info.Id] = node;
this.splitsPanel.Nodes.Add(node);
}
this.ResumeLayout(false);
this.PerformLayout();
}
internal XmlNode GetSettings(XmlDocument doc) {
var node = doc.CreateElement("Settings");
var enabledSplits = doc.CreateElement("EnabledSplits");
foreach (var entry in this.EnabledSplits) {
var elem = doc.CreateElement("Split");
elem.InnerText = (entry.Key * 1_000 + entry.Value).ToString();
enabledSplits.AppendChild(elem);
}
node.AppendChild(enabledSplits);
return node;
}
internal void SetSettings(XmlNode node) {
if (node is not XmlElement settings) {
return;
}
this.EnabledSplits.Clear();
if (settings["EnabledSplits"] is { } enabledSplits) {
foreach (var split in enabledSplits.ChildNodes) {
if (split is not XmlElement splitElem) {
continue;
}
if (uint.TryParse(splitElem.InnerText.Trim(), out var id)) {
var idx = id % 1_000;
this.EnabledSplits[id / 1_000] = (int) idx;
}
}
}
this.UpdateCheckboxState();
}
private void UpdateCheckboxState() {
this._updating = true;
foreach (var entry in this.CheckBoxes) {
entry.Value.Checked = this.EnabledSplits.ContainsKey(entry.Key);
foreach (var node in entry.Value.Nodes) {
if (node is TreeNode child) {
child.Checked = this.EnabledSplits.TryGetValue(entry.Key, out var idx) && idx == (int) child.Tag;
}
}
}
this._updating = false;
}
}

123
LiveSplit.TZA/TzaControl.resx Executable file
View File

@ -0,0 +1,123 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<metadata name="toolTip.TrayLocation" type="System.Drawing.Point, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
<value>17, 17</value>
</metadata>
</root>

18
LiveSplit.TZA/TzaFactory.cs Executable file
View File

@ -0,0 +1,18 @@
using LiveSplit.Model;
using LiveSplit.UI.Components;
namespace LiveSplit.TZA;
public class TzaFactory : IComponentFactory {
public string ComponentName => "FFXII: The Zodiac Age";
public string Description => "Autosplitter for FFXII: The Zodiac Age";
public ComponentCategory Category => ComponentCategory.Control;
public string UpdateName => this.ComponentName;
public string? XMLURL => null;
public string? UpdateURL => null;
public Version Version => new(1, 0);
public IComponent Create(LiveSplitState state) {
return new TzaComponent(state);
}
}

View File

@ -0,0 +1,700 @@
namespace LiveSplit.TZA.Util;
internal static class LocationNames {
private static readonly Dictionary<ushort, string> Names = new() {
[12] = "Main Menu",
[13] = "End Credits",
[48] = "Stockade",
[49] = "Arena",
[51] = "Oubliette",
[53] = "The Lightworks",
[54] = "Great Eastern Passage",
[55] = "Op Sector 36",
[56] = "Special Op Sector 3",
[57] = "Op Sector 37",
[58] = "Op Sector 29",
[61] = "Great Central Passage",
[62] = "The Zeviah Subterrane",
[66] = "North-South Junction",
[69] = "Terminus No. 4",
[70] = "Terminus No. 4 Adjunct",
[71] = "Terminus No. 7",
[124] = "Observation Parlour",
[125] = "Sky Saloon",
[126] = "Air Deck",
[129] = "South Bank Village",
[130] = "North Bank Village",
[132] = "The Yoma",
[133] = "Broken Sands",
[137] = "Uazcuff Hills",
[138] = "Sundered Earth",
[139] = "The Highlands",
[140] = "Fields of Eternity",
[141] = "The Shaded Path",
[142] = "The Chosen Path",
[144] = "The Skytrail",
[145] = "Realm of the Elder Dream",
[146] = "The Lost Way",
[147] = "Garden of Life's Circle",
[148] = "Oliphzak Rise",
[149] = "The Nameless Spring",
[151] = "The Omen-Spur",
[152] = "Trunkwall Road",
[153] = "Diverging Way",
[154] = "Sun-dappled Path",
[155] = "Garden of Decay",
[156] = "Path of Hours",
[157] = "Quietened Trace",
[158] = "Grand Bower",
[162] = "Corridor of Ages",
[163] = "Piebald Path",
[167] = "Greencrag",
[168] = "The Muted Scarp",
[169] = "Vale of Lingering Sorrow",
[170] = "Hope's Reach",
[171] = "Echoes of the Past",
[175] = "The Slumbermead",
[176] = "Succor Midst Sorrow",
[177] = "The Fog Mutters",
[178] = "Overlooking Eternity",
[179] = "Lifeless Strand",
[180] = "Field of the Fallen Lord",
[184] = "Falls of Time",
[185] = "Mirror of the Soul",
[186] = "The Acolyte's Burden",
[187] = "Doubt Abandoned",
[188] = "Skybent Chamber",
[192] = "Destiny's March",
[193] = "Temptation Eluded",
[194] = "Chamber of the Chosen",
[198] = "Hall of Shadowlight",
[200] = "Hall of Lambent Darkness",
[202] = "Hall of the Wroth God",
[209] = "Southern Skirts",
[210] = "Summit Path",
[215] = "Rays of Ashen Light",
[216] = "Empyrean Way",
[217] = "Skyreach Ridge",
[218] = "Trail of Sky-flung Stone",
[219] = "Northern Skirts",
[220] = "Halny Crossing",
[223] = "Empyrean Seat",
[227] = "The Stepping",
[228] = "Yardang Labyrinth",
[229] = "Sand-swept Naze",
[230] = "Banks of the Nebra",
[231] = "Passage Entrance",
[232] = "Murmuring Defile",
[233] = "Outpost",
[236] = "Throne Road",
[237] = "Warrior's Wash",
[238] = "Gizas North Bank",
[239] = "Toam Hills",
[243] = "Nomad Village",
[247] = "Starfall Field",
[248] = "Crystal Glade",
[249] = "Gizas South Bank",
[253] = "Throne Road",
[254] = "Warrior's Wash",
[255] = "Gizas North Bank",
[256] = "Toam Hills",
[260] = "Nomad Village",
[264] = "Starfall Field",
[265] = "Crystal Glade",
[266] = "Gizas South Bank",
[270] = "Tracks of the Beast",
[274] = "Aerial Gardens",
[275] = "Inner Ward",
[279] = "Lower Apartments",
[280] = "Upper Apartments",
[282] = "The Highhall",
[286] = "Energy Transitarium",
[289] = "North End",
[290] = "Muthru Bazaar",
[291] = "East End",
[292] = "Southern Plaza",
[295] = "Amal's Weaponry",
[296] = "Panamis's Protectives",
[297] = "Migelo's Sundries",
[298] = "Yugri's Magicks",
[299] = "Batahn's Technicks",
[300] = "Yamoora's Gambits",
[301] = "Samalzalam Manor",
[302] = "The Clan Hall",
[304] = "The Sandsea",
[305] = "Eastgate",
[306] = "Southgate",
[307] = "Westgate",
[311] = "Central Spur Stairs",
[312] = "East Waterway Control",
[313] = "North Spur Sluiceway",
[314] = "East Spur Stairs",
[315] = "Northern Sluiceway",
[316] = "East Waterway Control",
[318] = "No. 11 Channel",
[319] = "East Sluice Control",
[320] = "West Sluice Control",
[321] = "No. 10 Channel",
[322] = "Central Waterway Control",
[326] = "Southern Sluiceway",
[329] = "Overflow Cloaca",
[332] = "A Vikaari Bhrum",
[333] = "Trahk Pis Praa",
[334] = "Sthaana Pisces",
[335] = "Dha Vikaari Jula",
[336] = "Trahk Jilaam Praa'dii",
[337] = "Sthaana Aries",
[341] = "Crystal Core",
[342] = "No. 1 Cloaca",
[345] = "Galtea Downs",
[346] = "Corridor of Sand",
[347] = "Shimmering Horizons",
[348] = "The Midfault",
[349] = "Windtrace Dunes",
[350] = "The Western Divide",
[354] = "Wyrm's Nest",
[357] = "Shaft Entry",
[358] = "Oltam Span",
[359] = "Transitway 1",
[360] = "Transitway 2",
[365] = "Shunia Twinspan",
[366] = "Site 2",
[367] = "Site 3",
[369] = "Tasche Span",
[372] = "Site 9",
[374] = "Site 11",
[376] = "Site 7",
[379] = "Platform 1 - East Tanks",
[380] = "Platform 1 - Refinery",
[381] = "East Junction",
[382] = "Primary Tank Complex",
[383] = "Central Junction",
[384] = "Platform 1 - South Tanks",
[385] = "Platform 2 - Refinery",
[386] = "Yensa Border Tunnel",
[387] = "South Tank Approach",
[390] = "The Urutan-Yensa Sea",
[391] = "Withering Shores",
[392] = "Augur Hill",
[393] = "Yellow Sands",
[394] = "The Sandscale Bank",
[398] = "Demesne of the Sandqueen",
[399] = "Trail of Fading Warmth",
[400] = "Simoon Bluff",
[403] = "Valley of the Dead",
[406] = "Hall of the Destroyer",
[407] = "Hall of the Sentinel",
[410] = "Royal Passage",
[411] = "Southfall Passage",
[412] = "Northfall Passage",
[415] = "Cloister of Flame",
[418] = "Chamber of First Light",
[421] = "Hall of Effulgent Light",
[422] = "Cloister of Distant Song",
[426] = "Cloister of the Highborn",
[430] = "Hall of the Ivory Covenant",
[431] = "Hall of Slumbering Might",
[438] = "The Crucible",
[441] = "Cloister of Solace",
[444] = "Cloister of Reason",
[447] = "Paths of Chained Light",
[448] = "The Needlebrake",
[449] = "Whisperleaf Way",
[450] = "The Parting Glade",
[453] = "The Rustling Chapel",
[456] = "Dell of the Dreamer",
[459] = "The Branchway",
[460] = "The Greenswathe",
[463] = "Fading Vale",
[464] = "Head of the Silverflow",
[465] = "Freezing Gorge",
[468] = "Frozen Brook",
[469] = "Icebound Flow",
[470] = "Karydine Glacier",
[473] = "Path of the Firstfall",
[474] = "Spine of the Icewyrm",
[475] = "Silverflow's End",
[478] = "The Reseta Strand",
[479] = "Pora-Pora Sands",
[480] = "The Mauleia Strand",
[481] = "Cape Uahuk",
[482] = "Cape Tialan",
[483] = "Kaukula Pass",
[484] = "The Hakawea Shore",
[488] = "Hunters' Camp",
[491] = "Caima Hills",
[492] = "The Vaddu Strand",
[493] = "Limatra Hills",
[494] = "Rava's Pass",
[497] = "Old Elanise Road",
[498] = "Crossfield",
[499] = "The Terraced Bank",
[500] = "Journey's Rest",
[503] = "North Liavell Hills",
[504] = "South Liavell Hills",
[505] = "Feddik River",
[506] = "The Northsward",
[509] = "Footfalls of the Past",
[511] = "Echoes from Time's Garden",
[517] = "City of Other Days",
[518] = "Path of Hidden Blessing",
[522] = "They Who Thirst Not",
[525] = "Field of Fallen Wings",
[526] = "The Switchback",
[527] = "Haulo Green",
[530] = "Dagan Flats",
[531] = "Field of Light Winds",
[532] = "The Greensnake",
[533] = "Sunlit Path",
[536] = "The Shred",
[539] = "Walk of Flitting Rifts",
[540] = "Walk of Stolen Truths",
[541] = "Walk of Dancing Shadow",
[542] = "Antiquity's End",
[545] = "Redolent Glade",
[548] = "White Magick's Embrace",
[549] = "Ice Field of Clearsight",
[550] = "The Edge of Reason",
[552] = "Port Launch",
[555] = "Port Section",
[556] = "Large Freight Stores",
[557] = "Starboard Section",
[558] = "Sub-control Room",
[561] = "Airship Berth Access",
[564] = "Central Brig Access",
[567] = "Cellar Stores",
[568] = "Cellars",
[569] = "Lower Halls",
[570] = "Secret Passage",
[571] = "Treasure Room No. 8",
[572] = "The Garden Stairs",
[576] = "Invitation to Heresy",
[577] = "Sandfalls",
[578] = "Hourglass Basin",
[581] = "The Undershore",
[582] = "Halls of Ardent Darkness",
[585] = "The Balamka Fault",
[586] = "Drybeam Cavern",
[587] = "Darkened Wharf",
[588] = "Canopy of Clay",
[590] = "Athroza Quicksands",
[593] = "Walk of Sky",
[594] = "Walk of Mind",
[597] = "Ward of Measure",
[598] = "Cold Distance",
[599] = "Walk of Prescience",
[600] = "Walk of Reason",
[603] = "Ward of Steel",
[606] = "Ward of Velitation",
[607] = "Walk of Torn Illusion",
[608] = "Walk of Revelation",
[609] = "Ward of the Sword-King",
[612] = "Hall of Worth",
[615] = "Vault of the Champion",
[618] = "Throne of Veiled Gods",
[621] = "A Prama Vikaari",
[622] = "Kabonii Jilaam Pratii'vaa",
[623] = "Kabonii Jilaam Avaa",
[624] = "Dha Vikaari Bhrum",
[625] = "Sthaana Scorpio",
[626] = "A Vikaari Dhebon",
[630] = "West Barbican",
[631] = "Jajim Bazaar",
[632] = "West Ward",
[635] = "Grand Arcade",
[636] = "Highgarden Terrace",
[640] = "Molberry",
[641] = "Trant",
[642] = "Charlotte's Magickery",
[643] = "Bulward's Technicks",
[666] = "Womb of the Sun-cryst",
[668] = "Womb of the Sun-cryst",
[670] = "Heaven's Challenge",
[682] = "Hell's Challenge",
[686] = "Gate of Earth",
[687] = "Gate of Water",
[690] = "The Trimahla Water-Steps",
[691] = "The Aadha Water-Steps",
[694] = "The Haalmikah Water-Steps",
[695] = "Gate of Fire",
[698] = "Gate of Wind",
[701] = "North Sprawl",
[702] = "South Sprawl",
[703] = "Dalan's House",
[704] = "Residence",
[709] = "The Black Watch",
[710] = "The Confiscatory",
[712] = "North Entrance",
[713] = "Pithead Junction A",
[714] = "Phase 1 Shaft",
[715] = "Phase 1 Dig",
[716] = "Crossover A",
[717] = "Pithead Junction B",
[718] = "Staging Shaft",
[719] = "Crossover B",
[722] = "Ore Separation",
[725] = "Phase 2 Dig",
[726] = "Crossover C",
[727] = "Pithead Junction C",
[728] = "Phase 2 Shaft",
[730] = "Special Charter Shaft",
[733] = "Special Charter Dig",
[736] = "Hall of the Light",
[737] = "Hall of the Light",
[738] = "Temple Grounds",
[741] = "Temple Approach",
[742] = "Sand-strewn Pass",
[745] = "Nilbasse",
[746] = "Rienna",
[747] = "Vint's Armaments",
[748] = "Granch's Requisites",
[749] = "Lebleu's Gambits",
[751] = "Banks of the Sogoht",
[752] = "Lull of the Land",
[753] = "The Elderknoll",
[756] = "Tsenoble",
[762] = "Alley of Muted Sighs",
[763] = "Alley of Low Whispers",
[766] = "Fane of the Path",
[767] = "The Spiritwood",
[768] = "Road of Verdant Praise",
[774] = "Periphery",
[775] = "Catwalk",
[776] = "Antechamber",
[777] = "Antechamber",
[779] = "Central Lift",
[782] = "Central Shaft",
[785] = "Cannon Superstructure",
[788] = "Aerodrome (Rabanastre)",
[791] = "Aerodrome (Bhujerba)",
[794] = "Aerodrome (Archades)",
[797] = "Aerodrome (Balfonheim Port)",
[800] = "Aerodrome (Nalbina Town)",
[803] = "Travica Way",
[804] = "Cloudborne Row",
[805] = "Miners' End",
[806] = "Lhusu Square",
[809] = "Khus Skygrounds",
[810] = "Kaff Terrace",
[813] = "Targe's Arms",
[814] = "Rithil's Protectives",
[816] = "Mait's Magicks",
[817] = "Clio's Technicks",
[818] = "Bashketi's Gambits",
[819] = "The Staras Residence",
[820] = "The Cloudborne",
[823] = "Sea Breeze Lane",
[824] = "Gallerina Marketplace",
[825] = "Quayside Court",
[826] = "Saccio Lane",
[827] = "Chivany Breakwater",
[828] = "Canal Lane",
[833] = "Beruny's Armaments",
[834] = "Odo's Technicks",
[835] = "Port Villa",
[836] = "The Whitecap",
[837] = "Port Villa",
[838] = "No. 11 Channel",
[839] = "No. 11 Channel",
[840] = "East Sluice Control",
[841] = "Southern Sluiceway",
[842] = "West Sluice Control",
[843] = "No. 10 Channel",
[844] = "No. 10 Channel",
[845] = "No. 3 Cloaca Spur",
[846] = "No. 3 Cloaca Spur",
[847] = "No. 1 Cloaca",
[848] = "No. 4 Cloaca Spur",
[849] = "No. 4 Cloaca Spur",
[850] = "Central Waterway Control",
[854] = "C.D.B.",
[856] = "Dalan's Marker",
[871] = "Bridge",
[872] = "Battle Launch",
[876] = "Brig No. 1",
[888] = "Crystal Peak",
[891] = "East-West Bypass",
[892] = "The Zeviah Span",
[893] = "West Annex",
[894] = "Terminus No. 7 Adjunct",
[895] = "Special Op Sector 5",
[898] = "Colosseum",
[901] = "Lasche Span",
[902] = "Site 5",
[903] = "Site 6 South",
[904] = "Site 6 North",
[905] = "Staging Area",
[908] = "Living Chasm",
[916] = "Siti Bhrusuna",
[926] = "A Vikaari Kabonii",
[927] = "Sthaana Cancer",
[928] = "Bhrum Pis Avaa",
[929] = "Bhrum Pis Pratii",
[930] = "Dha Vikaari Trahk",
[933] = "Site 2",
[934] = "Shunia Twinspan",
[935] = "Transitway 1",
[936] = "Oltam Span",
[939] = "Dha Vikaari Kabonii",
[940] = "A Vikaari Kanbhru Ra",
[941] = "Dhebon Jilaam Praa'dii",
[942] = "Dhebon Jilaam Pratii'dii",
[943] = "Sthaana Sagittarius",
[944] = "A Vikaari Sirhru Pratii",
[945] = "A Vikaari Sirhru Praa",
[946] = "Dhebon Jilaam Avaapratii",
[947] = "A Vikaari Sirhru Si",
[949] = "Dhebon Jilaam Avaa",
[952] = "Dha Vikaari Dhebon Praa",
[953] = "Dha Vikaari Dhebon Pratii",
[954] = "Sirhru Phullam Praa",
[955] = "Sthaana Leo",
[956] = "Sirhru Phullam Praa'vaa",
[957] = "Sirhru Phullam Pratii'vaa",
[958] = "Sthaana Gemini",
[959] = "Sirhru Phullam Udiipratii",
[960] = "Sirhru Jilaam Praa'dii",
[961] = "Sirhru Jilaam Pratii'dii",
[962] = "Sirhru Jilaam Praa",
[963] = "Sirhru Jilaam Pratii",
[964] = "Sirhru Jilaam Praa'vaa",
[965] = "Sirhru Jilaam Pratii'vaa",
[966] = "Sirhru Pis Praa",
[967] = "Sirhru Pis Pratii",
[968] = "Sirhru Pis Avaa",
[969] = "Sirhru Jilaam Avaapratii",
[970] = "Sirhru Jilaam Avaapraa",
[971] = "A Vikaari Uldobi",
[972] = "A Vikaari Uldobi Si",
[973] = "Dha Vikaari Dhebon Si",
[979] = "Dha Vikaari Sirhru",
[980] = "Sthaana Virgo",
[981] = "Uldobi Jilaam Praa'dii",
[982] = "Uldobi Jilaam Pratii",
[983] = "Uldobi Jilaam Praa",
[984] = "Uldobi Phullam Pratii'dii",
[985] = "Sthaana Capricorn",
[986] = "Dha Vikaari Sirhru Si",
[987] = "Uldobi Phullam Udiipraa",
[988] = "Uldobi Phullam Pratii'vaa",
[989] = "Uldobi Phullam Praa'vaa",
[990] = "Uldobi Phullam Pratii",
[991] = "Sthaana Taurus",
[992] = "Sthaana Libra",
[993] = "Uldobi Jilaam Praa'vaa",
[994] = "Uldobi Jilaam Pratii'vaa",
[995] = "Uldobi Jilaam Avaa",
[996] = "A Vikaari Kanbhru",
[1002] = "Dha Vikaari Uldobi",
[1003] = "Kanbhru Pis",
[1004] = "Dha Vikaari Dhebon Ra",
[1005] = "Sthaana Aquarius",
[1008] = "66th Floor",
[1009] = "Rm 6613 West",
[1010] = "Rm 6613 East",
[1011] = "Rm 6612 West",
[1013] = "Rm 6611 West",
[1014] = "Rm 6611 East",
[1015] = "Rm 6602 West",
[1016] = "Rm 6601 West",
[1017] = "Rm 6602 East",
[1018] = "Rm 6601 East",
[1019] = "67th Floor",
[1020] = "Rm 6711 West",
[1021] = "Rm 6711 East",
[1023] = "Rm 6703 West",
[1024] = "Rm 6704 East",
[1025] = "Rm 6703 East",
[1026] = "Rm 6702 West",
[1027] = "Rm 6701 West",
[1028] = "Rm 6702 East",
[1030] = "68th Floor",
[1031] = "69th Floor",
[1036] = "66th Floor",
[1037] = "Rm 6613 West",
[1038] = "Rm 6613 East",
[1039] = "Rm 6612 West",
[1041] = "Rm 6611 West",
[1042] = "Rm 6611 East",
[1043] = "Rm 6602 West",
[1044] = "Rm 6601 West",
[1045] = "Rm 6602 East",
[1046] = "Rm 6601 East",
[1047] = "70th Floor",
[1048] = "Rm 7002 West",
[1049] = "Rm 7001 West",
[1050] = "Rm 7002 East",
[1054] = "67th Floor",
[1055] = "Rm 6711 West",
[1056] = "Rm 6711 East",
[1058] = "Rm 6703 West",
[1059] = "Rm 6704 East",
[1060] = "Rm 6703 East",
[1061] = "Rm 6702 West",
[1062] = "Rm 6701 West",
[1063] = "Rm 6702 East",
[1067] = "68th Floor",
[1068] = "Rm 6814 West",
[1069] = "Rm 6814 East",
[1070] = "Rm 6813 West",
[1071] = "Rm 6813 East",
[1072] = "Rm 6812 West",
[1073] = "Rm 6812 East",
[1074] = "Rm 6811 West",
[1075] = "Rm 6811 East",
[1076] = "Rm 6804 West",
[1077] = "Rm 6803 West",
[1078] = "Rm 6804 East",
[1079] = "Rm 6803 East",
[1080] = "Rm 6802 West",
[1081] = "Rm 6801 West",
[1082] = "Rm 6802 East",
[1083] = "Rm 6801 East",
[1088] = "Rm 6912 East",
[1089] = "Rm 6911 West",
[1091] = "Rm 6904 West",
[1094] = "Rm 6903 East",
[1095] = "Rm 6902 West",
[1096] = "Rm 6901 West",
[1098] = "Rm 6901 East",
[1101] = "The Wellspring",
[1102] = "Horizon's Break",
[1103] = "The Reach",
[1104] = "Reach of the Damned",
[1105] = "Reach of the Occult",
[1111] = "Wellspring Labyrinth",
[1113] = "Dunes of Profaning Wind",
[1114] = "Blackrock Vault",
[1115] = "Wellspring Ravel - 1st Flight",
[1116] = "Wellspring Ravel - 2nd Flight",
[1118] = "Wellspring Ravel - 3rd Flight",
[1119] = "Wellspring Ravel - 4th Flight",
[1121] = "Marsh of Profaning Wind",
[1122] = "Horizon's Cusp",
[1123] = "Penumbra - Interior",
[1124] = "Penumbra - North",
[1125] = "Penumbra - South",
[1126] = "Umbra - Interior",
[1127] = "Umbra - North",
[1128] = "Umbra - South",
[1129] = "Abyssal - Interior",
[1130] = "Abyssal - North",
[1131] = "Abyssal - South",
[1132] = "Cleft of Profaning Wind",
[1133] = "The Bounds of Truth",
[1134] = "Station of Banishment",
[1136] = "Station of Suffering",
[1138] = "Station of Ascension",
[1140] = "Spire Ravel - 1st Flight",
[1141] = "Spire Ravel - 2nd Flight",
[1143] = "Empyrean Ravel",
[1145] = "Overflow Cloaca",
[1150] = "Air Deck",
[1151] = "Babbling Vale",
[1153] = "Withering Shores",
[1155] = "Stage 1",
[1156] = "Stage 2",
[1157] = "Stage 3",
[1158] = "Stage 4",
[1159] = "Stage 5",
[1163] = "Stage 6",
[1164] = "Stage 7",
[1165] = "Stage 8",
[1166] = "Stage 9",
[1167] = "Stage 10",
[1171] = "Stage 11",
[1172] = "Stage 12",
[1173] = "Stage 13",
[1174] = "Stage 14",
[1175] = "Stage 15",
[1179] = "Stage 16",
[1180] = "Stage 17",
[1181] = "Stage 18",
[1182] = "Stage 19",
[1183] = "Stage 20",
[1187] = "Stage 21",
[1188] = "Stage 22",
[1189] = "Stage 23",
[1190] = "Stage 24",
[1191] = "Stage 25",
[1195] = "Stage 26",
[1196] = "Stage 27",
[1197] = "Stage 28",
[1198] = "Stage 29",
[1199] = "Stage 30",
[1203] = "Stage 31",
[1204] = "Stage 32",
[1205] = "Stage 33",
[1206] = "Stage 34",
[1207] = "Stage 35",
[1211] = "Stage 36",
[1212] = "Stage 37",
[1213] = "Stage 38",
[1214] = "Stage 39",
[1215] = "Stage 40",
[1219] = "Stage 41",
[1220] = "Stage 42",
[1221] = "Stage 43",
[1222] = "Stage 44",
[1223] = "Stage 45",
[1227] = "Stage 46",
[1228] = "Stage 47",
[1229] = "Stage 48",
[1230] = "Stage 49",
[1231] = "Stage 50",
[1235] = "Stage 51",
[1236] = "Stage 52",
[1237] = "Stage 53",
[1238] = "Stage 54",
[1239] = "Stage 55",
[1243] = "Stage 56",
[1244] = "Stage 57",
[1245] = "Stage 58",
[1246] = "Stage 59",
[1247] = "Stage 60",
[1251] = "Stage 61",
[1252] = "Stage 62",
[1253] = "Stage 63",
[1254] = "Stage 64",
[1255] = "Stage 65",
[1259] = "Stage 66",
[1260] = "Stage 67",
[1261] = "Stage 68",
[1262] = "Stage 69",
[1263] = "Stage 70",
[1267] = "Stage 71",
[1268] = "Stage 72",
[1269] = "Stage 73",
[1270] = "Stage 74",
[1271] = "Stage 75",
[1275] = "Stage 76",
[1276] = "Stage 77",
[1277] = "Stage 78",
[1278] = "Stage 79",
[1279] = "Stage 80",
[1283] = "Stage 81",
[1284] = "Stage 82",
[1285] = "Stage 83",
[1286] = "Stage 84",
[1287] = "Stage 85",
[1291] = "Stage 86",
[1292] = "Stage 87",
[1293] = "Stage 88",
[1294] = "Stage 89",
[1295] = "Stage 90",
[1299] = "Stage 91",
[1300] = "Stage 92",
[1301] = "Stage 93",
[1302] = "Stage 94",
[1303] = "Stage 95",
[1307] = "Stage 96",
[1308] = "Stage 97",
[1309] = "Stage 98",
[1310] = "Stage 99",
[1311] = "Stage 100",
};
internal static string Get(ushort location) {
return Names.TryGetValue(location, out var name) ? name : "???";
}
}