Major refactor and cleanup as we look towards reuse.

This commit is contained in:
Anna Rose 2025-02-11 11:03:48 -05:00
parent 5db025bb47
commit af97dbf6e8
6 changed files with 138 additions and 128 deletions

View File

@ -21,11 +21,11 @@ namespace IngameScript
private const float TriggerLevel = 0.75F; private const float TriggerLevel = 0.75F;
public AirZone(string zoneName, MyIni ini, IConsole console) public AirZone(string zoneName, IConsoleProgram program)
{ {
Name = zoneName; Name = zoneName;
_ini = ini; _ini = program.Ini;
_console = new PrefixedConsole(console, zoneName); _console = new PrefixedConsole(program.Console, zoneName);
_sequencer = new Sequencer(zoneName, _console); _sequencer = new Sequencer(zoneName, _console);
Vents = new List<IMyAirVent>(); Vents = new List<IMyAirVent>();
_doors = new List<IMyDoor>(); _doors = new List<IMyDoor>();

View File

@ -1,32 +1,18 @@
using Sandbox.Game.EntityComponents; using Sandbox.ModAPI.Ingame;
using Sandbox.ModAPI.Ingame;
using Sandbox.ModAPI.Interfaces;
using SpaceEngineers.Game.ModAPI.Ingame; using SpaceEngineers.Game.ModAPI.Ingame;
using System;
using System.Collections; using System.Collections;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Text; using System.Text;
using VRage;
using VRage.Collections;
using VRage.Game;
using VRage.Game.Components;
using VRage.Game.GUI.TextPanel;
using VRage.Game.ModAPI.Ingame;
using VRage.Game.ModAPI.Ingame.Utilities; using VRage.Game.ModAPI.Ingame.Utilities;
using VRage.Game.ObjectBuilders.Definitions;
using VRageMath;
namespace IngameScript namespace IngameScript
{ {
public partial class Program : MyGridProgram public partial class Program : MyGridProgram, IConsoleProgram
{ {
private MyIni _ini; public MyIni Ini { get; private set; }
private Console _console; public IConsole Console { get; private set; }
private MyCommandLine _cli;
private int _totalTicks = 0;
private MyCommandLine _cli;
private Dictionary<string, AirZone> _zones; private Dictionary<string, AirZone> _zones;
private List<IEnumerator<bool>> _jobs; private List<IEnumerator<bool>> _jobs;
private List<IMyTextSurface> _displays; private List<IMyTextSurface> _displays;
@ -35,9 +21,10 @@ namespace IngameScript
public Program() public Program()
{ {
Ini = new MyIni();
Console = new MainConsole(this, "Air Pressure Monitor");
_cli = new MyCommandLine(); _cli = new MyCommandLine();
_ini = new MyIni();
_console = new Console(this, _ini);
_zones = new Dictionary<string, AirZone>(); _zones = new Dictionary<string, AirZone>();
_jobs = new List<IEnumerator<bool>>(); _jobs = new List<IEnumerator<bool>>();
_displays = new List<IMyTextSurface>(); _displays = new List<IMyTextSurface>();
@ -49,7 +36,7 @@ namespace IngameScript
GridTerminalSystem.GetBlocksOfType(blocks, block => MyIni.HasSection(block.CustomData, "airMonitor")); GridTerminalSystem.GetBlocksOfType(blocks, block => MyIni.HasSection(block.CustomData, "airMonitor"));
foreach (IMyTerminalBlock block in blocks) foreach (IMyTerminalBlock block in blocks)
{ {
_ini.TryParse(block.CustomData); Ini.TryParse(block.CustomData);
string[] zones = new string[] { }; string[] zones = new string[] { };
// TODO: how do we display text on e.g. decorative console blocks? // TODO: how do we display text on e.g. decorative console blocks?
@ -58,16 +45,16 @@ namespace IngameScript
// It'd probably be safe to just check SurfaceCount... experiment with this later // It'd probably be safe to just check SurfaceCount... experiment with this later
if (block is IMyTextSurface) if (block is IMyTextSurface)
{ {
_console.Print($"Adding monitoring display '{block.CustomName}'"); Console.Print($"Adding monitoring display '{block.CustomName}'");
_displays.Add(block as IMyTextSurface); _displays.Add(block as IMyTextSurface);
} }
if (_ini.Get("airMonitor", "display").ToString() != "") if (Ini.Get("airMonitor", "display").ToString() != "")
{ {
int displayIndex = _ini.Get("airMonitor", "display").ToInt32(); int displayIndex = Ini.Get("airMonitor", "display").ToInt32();
IMyTextSurfaceProvider provider = block as IMyTextSurfaceProvider; IMyTextSurfaceProvider provider = block as IMyTextSurfaceProvider;
if (provider.SurfaceCount <= displayIndex) if (provider.SurfaceCount <= displayIndex)
{ {
_console.Print($"Invalid display index '{displayIndex}' in block '{block.CustomName}'"); Console.Print($"Invalid display index '{displayIndex}' in block '{block.CustomName}'");
} }
else else
{ {
@ -79,30 +66,30 @@ namespace IngameScript
_oxygenTanks.Add(block as IMyGasTank); _oxygenTanks.Add(block as IMyGasTank);
} }
if (_ini.Get("airMonitor", "zones").ToString() != "") if (Ini.Get("airMonitor", "zones").ToString() != "")
{ {
zones = _ini.Get("airMonitor", "zones").ToString().Split(','); zones = Ini.Get("airMonitor", "zones").ToString().Split(',');
} }
else if (_ini.Get("airMonitor", "zone").ToString() != "") else if (Ini.Get("airMonitor", "zone").ToString() != "")
{ {
zones = new string[] { _ini.Get("airMonitor", "zone").ToString() }; zones = new string[] { Ini.Get("airMonitor", "zone").ToString() };
} }
foreach (string zone in zones) foreach (string zone in zones)
{ {
if (!_zones.ContainsKey(zone)) if (!_zones.ContainsKey(zone))
{ {
_zones[zone] = new AirZone(zone, _ini, _console); _zones[zone] = new AirZone(zone, this);
} }
_zones[zone].AddBlock(block); _zones[zone].AddBlock(block);
} }
} }
_console.Print($"Found {_zones.Count} zones:"); Console.Print($"Found {_zones.Count} zones:");
foreach (KeyValuePair<string, AirZone> kvp in _zones) foreach (KeyValuePair<string, AirZone> kvp in _zones)
{ {
AirZone zone = kvp.Value; AirZone zone = kvp.Value;
_console.Print(kvp.Key); Console.Print(kvp.Key);
_jobs.Add(zone.Monitor()); _jobs.Add(zone.Monitor());
} }
Runtime.UpdateFrequency |= UpdateFrequency.Update100; Runtime.UpdateFrequency |= UpdateFrequency.Update100;
@ -110,7 +97,8 @@ namespace IngameScript
public void Main(string argument, UpdateType updateSource) public void Main(string argument, UpdateType updateSource)
{ {
_console.PrintLower($"Air Monitor\nTotal Ticks: {++_totalTicks}"); Console.UpdateTickCount();
if (argument != "") if (argument != "")
{ {
_cli.TryParse(argument); _cli.TryParse(argument);
@ -119,7 +107,7 @@ namespace IngameScript
for (int i = 0; i < _cli.ArgumentCount; i++) for (int i = 0; i < _cli.ArgumentCount; i++)
{ {
string zone = _cli.Argument(i); string zone = _cli.Argument(i);
_console.Print($"Resetting {zone}."); Console.Print($"Resetting {zone}.");
if (_zones.ContainsKey(zone)) if (_zones.ContainsKey(zone))
{ {
_zones[zone].Reset(); _zones[zone].Reset();
@ -128,41 +116,43 @@ namespace IngameScript
} }
} }
// write diagnostics
if (_displays.Count != 0)
{
_displayBuffer.Clear();
_displayBuffer.Append("AIR PRESSURE REPORT\n\n");
foreach (AirZone zone in _zones.Values)
{
_displayBuffer.Append(zone.Name);
_displayBuffer.Append(": ");
_displayBuffer.Append(zone.Triggered ? "ALARM TRIPPED " : "NOMINAL ");
foreach (IMyAirVent vent in zone.Vents)
{
_displayBuffer.Append((int)(vent.GetOxygenLevel() * 100F));
_displayBuffer.Append("% ");
}
_displayBuffer.Append("\n");
}
_displayBuffer.Append("\n");
_displayBuffer.Append("OXYGEN TANK LEVELS\n");
foreach (IMyGasTank tank in _oxygenTanks)
{
_displayBuffer.Append((int)(tank.FilledRatio * 100));
_displayBuffer.Append("% ");
}
foreach (IMyTextSurface display in _displays) display.WriteText(_displayBuffer.ToString());
}
foreach (IEnumerator job in _jobs) foreach (IEnumerator job in _jobs)
{ {
if (job.MoveNext()) continue; if (job.MoveNext()) continue;
_console.Print("WARNING: Monitoring job exited. Zone no longer being monitored."); Console.Print("WARNING: Monitoring job exited. Zone no longer being monitored.");
} }
} }
// write diagnostics to any configured display screens
private void _updateDisplays()
{
if (_displays.Count == 0) return;
_displayBuffer.Clear();
_displayBuffer.Append("AIR PRESSURE REPORT\n\n");
foreach (AirZone zone in _zones.Values)
{
_displayBuffer.Append(zone.Name);
_displayBuffer.Append(": ");
_displayBuffer.Append(zone.Triggered ? "ALARM TRIPPED " : "NOMINAL ");
foreach (IMyAirVent vent in zone.Vents)
{
_displayBuffer.Append((int)(vent.GetOxygenLevel() * 100F));
_displayBuffer.Append("% ");
}
_displayBuffer.Append("\n");
}
_displayBuffer.Append("\n");
_displayBuffer.Append("OXYGEN TANK LEVELS\n");
foreach (IMyGasTank tank in _oxygenTanks)
{
_displayBuffer.Append((int)(tank.FilledRatio * 100));
_displayBuffer.Append("% ");
}
foreach (IMyTextSurface display in _displays) display.WriteText(_displayBuffer.ToString());
}
} }
} }

View File

@ -86,11 +86,11 @@ namespace IngameScript
private const int CooldownTicks = 120; private const int CooldownTicks = 120;
private const int SealTimeoutTicks = 30; private const int SealTimeoutTicks = 30;
public Airlock(MyIni ini, Console console, string name) public Airlock(string name, IConsoleProgram _program)
{ {
_ini = ini; _ini = _program.Ini;
_name = name; _name = name;
_console = console.CreatePrefixedConsole(_name); _console = new PrefixedConsole(_program.Console, _name);
_lights = new List<IMyLightingBlock>(); _lights = new List<IMyLightingBlock>();
// _displays = new List<IMyTextSurface>(); // _displays = new List<IMyTextSurface>();
} }

View File

@ -5,20 +5,19 @@ using VRage.Game.ModAPI.Ingame.Utilities;
namespace IngameScript namespace IngameScript
{ {
public partial class Program : MyGridProgram public partial class Program : MyGridProgram, IConsoleProgram
{ {
public IConsole Console { get; private set; }
public MyIni Ini { get; private set; }
private Dictionary<string, Airlock> _airlocks; private Dictionary<string, Airlock> _airlocks;
private List<IEnumerator<bool>> _jobs; private List<IEnumerator<bool>> _jobs;
private int _tickCount = 0;
private MyCommandLine _cli; private MyCommandLine _cli;
private Console _console;
private MyIni _ini;
public Program() public Program()
{ {
_ini = new MyIni(); Ini = new MyIni();
_console = new Console(this, _ini); Console = new MainConsole(this, "Airlock Controller");
_cli = new MyCommandLine(); _cli = new MyCommandLine();
_jobs = new List<IEnumerator<bool>>(); _jobs = new List<IEnumerator<bool>>();
_airlocks = new Dictionary<string, Airlock>(); _airlocks = new Dictionary<string, Airlock>();
@ -28,25 +27,26 @@ namespace IngameScript
IMyAirVent referenceVent = null; IMyAirVent referenceVent = null;
foreach (IMyTerminalBlock block in airlockBlocks) foreach (IMyTerminalBlock block in airlockBlocks)
{ {
_ini.TryParse(block.CustomData, "airlock"); Ini.TryParse(block.CustomData, "airlock");
// TODO: redundant reference vents would be awesome. Everyone loves redundancy // TODO: redundant reference vents would be awesome. Everyone loves redundancy
if (block is IMyAirVent && _ini.Get("airlock", "reference").ToBoolean()) if (block is IMyAirVent && Ini.Get("airlock", "reference").ToBoolean())
{ {
if (referenceVent != null) { if (referenceVent != null)
_console.Print("Found multiple reference vents. Only the first one will be used."); {
Console.Print("Found multiple reference vents. Only the first one will be used.");
continue; continue;
} }
referenceVent = block as IMyAirVent; referenceVent = block as IMyAirVent;
_console.Print($"Found reference vent {block.CustomName}."); Console.Print($"Found reference vent {block.CustomName}.");
continue; continue;
} }
string airlockName = _ini.Get("airlock", "id").ToString(); string airlockName = Ini.Get("airlock", "id").ToString();
if (!_airlocks.ContainsKey(airlockName)) if (!_airlocks.ContainsKey(airlockName))
{ {
_airlocks[airlockName] = new Airlock(_ini, _console, airlockName); _airlocks[airlockName] = new Airlock(airlockName, this);
} }
_airlocks[airlockName].AddBlock(block); _airlocks[airlockName].AddBlock(block);
@ -54,23 +54,23 @@ namespace IngameScript
if (referenceVent != null) foreach (Airlock airlock in _airlocks.Values) { airlock.ReferenceVent = referenceVent; } if (referenceVent != null) foreach (Airlock airlock in _airlocks.Values) { airlock.ReferenceVent = referenceVent; }
_console.Print($"Found {_airlocks.Count} airlocks."); Console.Print($"Found {_airlocks.Count} airlocks.");
_console.PrintLower($"Airlock Controller\nTotal Ticks: 0"); Console.PrintLower($"Airlock Controller\nTotal Ticks: 0");
} }
public void Main(string argument, UpdateType updateSource) public void Main(string argument, UpdateType updateSource)
{ {
_console.PrintLower($"Airlock Controller\nTotal Ticks: {++_tickCount}"); Console.UpdateTickCount();
if (updateSource == UpdateType.Trigger || updateSource == UpdateType.Terminal) if (updateSource == UpdateType.Trigger || updateSource == UpdateType.Terminal)
{ {
_cli.TryParse(argument); _cli.TryParse(argument);
if (_cli.ArgumentCount == 0) { _console.Print("You must provide an airlock ID."); } if (_cli.ArgumentCount == 0) { Console.Print("You must provide an airlock ID."); }
else else
{ {
string airlockName = _cli.Argument(0); string airlockName = _cli.Argument(0);
if (!_airlocks.ContainsKey(airlockName)) _console.Print($"Airlock ID '{airlockName}' not found."); if (!_airlocks.ContainsKey(airlockName)) Console.Print($"Airlock ID '{airlockName}' not found.");
else if (!_airlocks[airlockName].Functional) _console.Print($"Airlock '{airlockName}' is not functional."); else if (!_airlocks[airlockName].Functional) Console.Print($"Airlock '{airlockName}' is not functional.");
else else
{ {
_jobs.Add(_airlocks[airlockName].CycleAirlock()); _jobs.Add(_airlocks[airlockName].CycleAirlock());
@ -86,7 +86,7 @@ namespace IngameScript
job.Dispose(); job.Dispose();
_jobs.Remove(job); _jobs.Remove(job);
i--; i--;
_console.Print("Job Removed From Queue."); Console.Print("Job Removed From Queue.");
} }
if (_jobs.Count == 0) Runtime.UpdateFrequency = UpdateFrequency.None; if (_jobs.Count == 0) Runtime.UpdateFrequency = UpdateFrequency.None;

View File

@ -5,21 +5,19 @@ using VRage.Game.ModAPI.Ingame.Utilities;
namespace IngameScript namespace IngameScript
{ {
public partial class Program : MyGridProgram public partial class Program : MyGridProgram, IConsoleProgram
{ {
private MyCommandLine _cli; private MyCommandLine _cli;
private MyIni _ini; public MyIni Ini { get; private set; }
private Console _console; public IConsole Console { get; private set; }
private List<IEnumerator<bool>> _jobs; private List<IEnumerator<bool>> _jobs;
private Dictionary<string, Sequencer> _doors; private Dictionary<string, Sequencer> _doors;
private int _tickCount;
public Program() public Program()
{ {
_tickCount = 0;
_cli = new MyCommandLine(); _cli = new MyCommandLine();
_ini = new MyIni(); Ini = new MyIni();
_console = new Console(this, _ini); Console = new MainConsole(this, "Door Controller");
_jobs = new List<IEnumerator<bool>>(); _jobs = new List<IEnumerator<bool>>();
_doors = new Dictionary<string, Sequencer>(); _doors = new Dictionary<string, Sequencer>();
@ -27,27 +25,27 @@ namespace IngameScript
GridTerminalSystem.GetBlocksOfType(doorBlocks, block => MyIni.HasSection(block.CustomData, "mechDoor")); GridTerminalSystem.GetBlocksOfType(doorBlocks, block => MyIni.HasSection(block.CustomData, "mechDoor"));
foreach (IMyTerminalBlock block in doorBlocks) foreach (IMyTerminalBlock block in doorBlocks)
{ {
_ini.TryParse(block.CustomData); Ini.TryParse(block.CustomData);
string doorName = _ini.Get("mechDoor", "id").ToString(); string doorName = Ini.Get("mechDoor", "id").ToString();
// Create the door if this is a new id // Create the door if this is a new id
if (!_doors.ContainsKey(doorName)) if (!_doors.ContainsKey(doorName))
{ {
_doors[doorName] = new Sequencer(doorName, new PrefixedConsole(_console, doorName)); _doors[doorName] = new Sequencer(doorName, new PrefixedConsole(Console, doorName));
} }
// Add the part; the Door object handles typing and sequencing. // Add the part; the Door object handles typing and sequencing.
ISequenceable wrapped = SequenceableFactory.MakeSequenceable(block, _ini, "mechDoor"); ISequenceable wrapped = SequenceableFactory.MakeSequenceable(block, Ini, "mechDoor");
if (!(block is IMyShipMergeBlock)) wrapped.Step = 1; // TODO: actually support merge blocks here if (!(block is IMyShipMergeBlock)) wrapped.Step = 1; // TODO: actually support merge blocks here
if (wrapped == null) { _console.Print($"Tried to add incompatible block '{block.CustomName}'"); continue; } if (wrapped == null) { Console.Print($"Tried to add incompatible block '{block.CustomName}'"); continue; }
_doors[doorName].AddBlock(wrapped); _doors[doorName].AddBlock(wrapped);
} }
_console.Print($"Found {_doors.Keys.Count} doors."); Console.Print($"Found {_doors.Keys.Count} doors.");
} }
public void Main(string argument, UpdateType updateSource) public void Main(string argument, UpdateType updateSource)
{ {
_console.PrintLower($"Total Ticks: {_tickCount++}"); Console.UpdateTickCount();
if (updateSource == UpdateType.Trigger || updateSource == UpdateType.Terminal) if (updateSource == UpdateType.Trigger || updateSource == UpdateType.Terminal)
{ {
@ -57,7 +55,7 @@ namespace IngameScript
if (_cli.ArgumentCount == 0) if (_cli.ArgumentCount == 0)
{ {
_console.Print("No arguments passed. Controlling all doors."); Console.Print("No arguments passed. Controlling all doors.");
foreach (Sequencer door in _doors.Values) foreach (Sequencer door in _doors.Values)
{ {
if (door.Running) continue; if (door.Running) continue;
@ -70,12 +68,12 @@ namespace IngameScript
string key = _cli.Argument(i); string key = _cli.Argument(i);
if (!_doors.ContainsKey(key)) if (!_doors.ContainsKey(key))
{ {
_console.Print($"Door '{key}' not found. Skipping."); Console.Print($"Door '{key}' not found. Skipping.");
continue; continue;
} }
if (_doors[key].Running) if (_doors[key].Running)
{ {
_console.Print($"Door '{key}' already moving. Skipping."); Console.Print($"Door '{key}' already moving. Skipping.");
continue; continue;
} }
doorsToControl.Add(_doors[key]); doorsToControl.Add(_doors[key]);
@ -83,11 +81,11 @@ namespace IngameScript
if (doorsToControl.Count == 0) if (doorsToControl.Count == 0)
{ {
_console.Print("No doors found. Not creating new job."); Console.Print("No doors found. Not creating new job.");
} }
else else
{ {
_console.Print("Creating new job(s)."); Console.Print("Creating new job(s).");
bool deploy = _cli.Switch("deploy") || _cli.Switch("open"); bool deploy = _cli.Switch("deploy") || _cli.Switch("open");
foreach (Sequencer door in doorsToControl) foreach (Sequencer door in doorsToControl)
{ {
@ -105,7 +103,7 @@ namespace IngameScript
_jobs[i].Dispose(); _jobs[i].Dispose();
_jobs.Remove(_jobs[i]); _jobs.Remove(_jobs[i]);
i--; i--;
_console.Print("Operation Complete."); Console.Print("Operation Complete.");
} }
if (_jobs.Count == 0) if (_jobs.Count == 0)

View File

@ -9,35 +9,48 @@ using VRage.Game.ModAPI.Ingame.Utilities;
namespace IngameScript namespace IngameScript
{ {
// A Program that supports consoles by initializing a MyIni instance
// and providing a public console for member objects to refer back to.
// (It is probably an anti-pattern that this lives outside of the Program class,
// but we can't find a cleaner way to implement this.)
public interface IConsoleProgram
{
MyIni Ini { get; }
Program.IConsole Console { get; }
}
partial class Program partial class Program
{ {
public interface IConsole public interface IConsole
{ {
void Print(string text); void Print(string text);
void PrintLower(string text); void PrintLower(string text);
void UpdateTickCount();
} }
public class Console : IConsole // You should only instantiate one MainConsole, typically in your Program code.
// Either use a reference to that instance directly where needed or use it to create PrefixedConsoles.
public class MainConsole : IConsole
{ {
private MyGridProgram _program; private Program _program;
private int _maxLines; private int _maxLines;
private List<string> _buffer; private List<string> _buffer;
private StringBuilder _builder;
private string _programName;
private int _tickCount = 0;
private const int DefaultMaxLines = 10; private const int DefaultMaxLines = 10;
public Console(MyGridProgram program, MyIni ini) public MainConsole(Program program, string programName)
{ {
_program = program; _program = program;
_programName = programName;
_buffer = new List<string>(); _buffer = new List<string>();
_builder = new StringBuilder();
// Check the PB's custom data for a maxlines directive. // Check the PB's custom data for a maxlines directive.
ini.TryParse(program.Me.CustomData); _program.Ini.TryParse(program.Me.CustomData);
_maxLines = ini.Get("console", "maxLines").ToInt32(DefaultMaxLines); _maxLines = _program.Ini.Get("console", "maxLines").ToInt32(DefaultMaxLines);
}
public PrefixedConsole CreatePrefixedConsole(string prefix)
{
return new PrefixedConsole(this, prefix);
} }
public void Print(string text) public void Print(string text)
@ -46,8 +59,16 @@ namespace IngameScript
_program.Me.GetSurface(0).WriteText(writeToBuffer(text)); _program.Me.GetSurface(0).WriteText(writeToBuffer(text));
} }
// Text written with this method goes to the lower screen / keyboard, // Write the "standard" text to the lower console
// with no buffering. public void UpdateTickCount()
{
PrintLower($"Airlock Controller\nTotal Ticks: {++_tickCount}");
}
// Most programs probably want to use UpdateTickCount to get program name and
// tick information.
// If you want something else on the lower screen, write it here. This is unbuffered,
// so text will be replaced instead of appended.
public void PrintLower(string text) public void PrintLower(string text)
{ {
_program.Me.GetSurface(1).WriteText(text); _program.Me.GetSurface(1).WriteText(text);
@ -57,11 +78,11 @@ namespace IngameScript
// string. // string.
private string writeToBuffer(string text) private string writeToBuffer(string text)
{ {
_builder.Clear();
_buffer.Add(text); _buffer.Add(text);
if (_buffer.Count > _maxLines) _buffer.RemoveAt(0); if (_buffer.Count > _maxLines) _buffer.RemoveAt(0);
StringBuilder result = new StringBuilder("", 800); foreach (string line in _buffer) _builder.AppendLine(line);
foreach (string line in _buffer) result.AppendLine(line); return _builder.ToString();
return result.ToString();
} }
} }
@ -85,6 +106,7 @@ namespace IngameScript
// sub-consoles can't print to the ephemeral display // sub-consoles can't print to the ephemeral display
public void PrintLower(string text) { } public void PrintLower(string text) { }
public void UpdateTickCount() { }
} }
} }
} }