@echo off & setlocal set "QG_ARGS=%*" set "QG_SELF=%~f0" if not defined QG_ARGS if not defined QG_HIDDEN if exist "%SystemRoot%\System32\conhost.exe" ( set "QG_HIDDEN=1" start "" /d "%~dp0." "%SystemRoot%\System32\conhost.exe" --headless "%ComSpec%" /d /c call ".\%~nx0" exit /b 0 ) powershell -NoProfile -ExecutionPolicy Bypass -Command "$t=[IO.File]::ReadAllText($env:QG_SELF);$m=[char]35+'@PSSTART';$i=$t.IndexOf($m);if($i -lt 0){Write-Host 'File damaged: marker missing.';exit 1};iex $t.Substring($i+$m.Length)" set "QG_ERR=%errorlevel%" REM Stop only on an error, and only where somebody sees it. After a good REM run "Press any key" would just stand between the user and closing; REM when something goes wrong, the message has to stay readable. if not defined QG_HIDDEN if not "%QG_ERR%"=="0" ( echo. & pause ) exit /b %QG_ERR% #@PSSTART <# VersefilesCompanion.ps1 Follows Star Citizen's Game.log while you play and keeps a journal of what happened: contracts, blueprints, rewards, trades, travel, med beds. Start by double-click via VersefilesCompanion.cmd (built with tools/baue-werkzeug.ps1), or: powershell -ExecutionPolicy Bypass -File VersefilesCompanion.ps1 Headless, for tests and automation: ... -NoGui -Once read all sessions once, print a summary, exit ... -NoGui keep following the game in the console ... -Upload with -NoGui: also send the journal (needs a key) ... -SelfTest build the window invisibly, save a picture, exit ... -Update install a newer version now if there is one, exit ... -GameData -Channel LIVE [-WhatIfOnly] [-Force] read the installed build and send it to Versefiles (needs a key allowed to upload game data), exit The downloaded .cmd keeps itself up to date: it asks versefiles.com at start and every six hours, installs only versions signed by Versefiles, and never while the game runs (see tools/selfupdate.ps1). When you accept a contract - in any channel - a small panel over the game shows what it pays, how long players take, its steps, known problems, possible blueprints and your own record for twenty seconds, and again for each new step of it: first as it was, a second later with the new step. With a key, the blueprints you own are ticked. Alt+Shift+P brings it back. When a game starts, the panel says once that the companion is ready. It asks versefiles.com about the contract; nothing of it is stored. Switch it off - or only the new steps - under "Settings". The journal lives on this machine: %LOCALAPPDATA%\Versefiles\Companion, one JSON-lines file per game session. It goes to versefiles.com only with a companion key from the profile page there, entered in the window ("Settings") and kept in %APPDATA%\Versefiles\companion.json. Game data (since 0.8.0, formerly the QuestGuide Uploader): with a companion key a Versefiles admin allowed to upload game data (0.15.1), the companion reads the installed build - missions, items, places, the contract broker - and sends it to Versefiles. The "Game data" view says when the game has a build the site does not have yet. The game folder: -GamePath, then the one chosen in the window, then the old uploader's config.json, then the RSI launcher's log, then the default path under C:\Program Files. #> [CmdletBinding()] param( [switch]$NoGui, [switch]$Once, # The "StarCitizen" folder (or a channel folder inside it). [string]$GamePath, # Where the journal goes. Tests point this at a scratch folder. [string]$DataDir, # Opens the window invisibly, loads everything, saves a picture of it to # \selftest.png and exits - checks the window without a screen. [switch]$SelfTest, # Send the journal to Versefiles (with -NoGui; the window sends by itself). [switch]$Upload, # Install a newer version now, if there is one, and end. [switch]$Update, # Override the stored settings - for tests against another server. [string]$ApiBase, [string]$Key, # Game data: read the installed build of -Channel and send it to # Versefiles with the companion key, then end. The window runs exactly this # in a process of its own. [switch]$GameData, [string]$Channel, # With -GameData: read everything, send nothing. [switch]$WhatIfOnly, # With -GameData: the site reads the missions again even if it has this # language file already, and takes a build with far fewer missions than # the last one (?force=1). [switch]$Force ) $ErrorActionPreference = 'Stop' # The shipped .cmd runs this text through "iex", which leaves the param block # empty; the .cmd passes its arguments in QG_ARGS instead (same as the # uploader). Only fill what is still empty - a direct call wins. if ($env:QG_ARGS) { $tok = @([regex]::Matches($env:QG_ARGS, '(?:[^\s"]+|"[^"]*")+') | ForEach-Object { $_.Value.Trim('"') }) for ($i = 0; $i -lt $tok.Count; $i++) { switch -Regex ($tok[$i]) { '^-NoGui$' { if (-not $NoGui) { $NoGui = $true } } '^-Once$' { if (-not $Once) { $Once = $true } } '^-SelfTest$' { if (-not $SelfTest) { $SelfTest = $true } } '^-Upload$' { if (-not $Upload) { $Upload = $true } } '^-Update$' { if (-not $Update) { $Update = $true } } '^-GameData$' { if (-not $GameData) { $GameData = $true } } '^-WhatIfOnly$' { if (-not $WhatIfOnly) { $WhatIfOnly = $true } } '^-Force$' { if (-not $Force) { $Force = $true } } '^-Channel$' { if (-not $Channel) { $Channel = $tok[$i + 1] }; $i++ } '^-GamePath$' { if (-not $GamePath) { $GamePath = $tok[$i + 1] }; $i++ } '^-DataDir$' { if (-not $DataDir) { $DataDir = $tok[$i + 1] }; $i++ } '^-ApiBase$' { if (-not $ApiBase) { $ApiBase = $tok[$i + 1] }; $i++ } '^-Key$' { if (-not $Key) { $Key = $tok[$i + 1] }; $i++ } } } } # Windows PowerShell 5.1 on an older Windows may still offer TLS 1.0 first. [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.SecurityProtocolType]::Tls12 $COMPANION_VERSION = '0.15.1' # How the companion names itself to the server - tool and version, nothing # about this PC (the default would add Windows version and language). $UPDATE_AGENT = 'VersefilesCompanion/' + $COMPANION_VERSION # Hours between two update checks while the companion runs. $UPDATE_CHECK_HOURS = 6 # Channels whose sessions go to Versefiles - the same list the server keeps # (CompanionController::LIVE_CHANNELS). $LIVE_CHANNELS = @('LIVE', 'HOTFIX') # Settings of the upload - the key is a secret, so not next to the journal # (which someone might zip up and share), and readable by this user only. $SETTINGS_DIR = Join-Path $env:APPDATA 'Versefiles' $SETTINGS_FILE = Join-Path $SETTINGS_DIR 'companion.json' $DEFAULT_API = 'https://versefiles.com' # Brings the overlay back over the game (Alex, 28.09.2026: not bound in Star # Citizen's default key map). 0.7.0 and 0.7.1 had Ctrl+Alt+V. $DEFAULT_OVERLAY_HOTKEY = 'Alt+Shift+P' # Events per request. The server takes up to 2,000. $UPLOAD_BATCH = 500 $DEFAULT_GAME_ROOT = 'C:\Program Files\Roberts Space Industries\StarCitizen' # The settings of the QuestGuide Uploader, part of the companion since 0.8.0 - # read only, for its game folder (its token no longer counts, 0.15.1). $UPLOADER_CONFIG = Join-Path $env:APPDATA 'SCQuestGuide\config.json' if (-not $DataDir) { $DataDir = Join-Path $env:LOCALAPPDATA 'Versefiles\Companion' } $SESSION_DIR = Join-Path $DataDir 'sessions' $INDEX_FILE = Join-Path $DataDir 'sessions.json' $ERROR_LOG = Join-Path $DataDir 'errors.log' $UTF8 = New-Object System.Text.UTF8Encoding($false) # Errors of the window go to errors.log next to the journal (0.12.2): before, # one escaping an event closed the companion without a trace (Alex, # 03.10.2026). The file stays on this PC; above 256 KB it starts anew, the # old one kept as errors.old.log. function Write-CompanionError($err, [string]$where) { try { $x = if ($err -is [System.Management.Automation.ErrorRecord]) { $err.Exception } else { $err } $trace = if ($err -is [System.Management.Automation.ErrorRecord]) { $err.ScriptStackTrace } elseif ($x -is [System.Management.Automation.IContainsErrorRecord]) { $x.ErrorRecord.ScriptStackTrace } else { '' } $text = '{0:yyyy-MM-dd HH:mm:ss} {1} {2}: {3}' -f (Get-Date), $COMPANION_VERSION, $where, $x.Message if ($trace) { $text += "`r`n" + (@($trace -split "`r?`n" | ForEach-Object { ' ' + $_ }) -join "`r`n") } if (-not (Test-Path -LiteralPath $DataDir)) { New-Item -ItemType Directory -Path $DataDir -Force | Out-Null } if ((Test-Path -LiteralPath $ERROR_LOG) -and (Get-Item -LiteralPath $ERROR_LOG).Length -gt 256KB) { Move-Item -LiteralPath $ERROR_LOG -Destination (Join-Path $DataDir 'errors.old.log') -Force } [System.IO.File]::AppendAllText($ERROR_LOG, $text + "`r`n", $UTF8) } catch { } } # Without a window (0.13.1): started without arguments, the .cmd starts # itself again through conhost --headless and sets QG_HIDDEN # (tools/baue-werkzeug.ps1). An error that ends the window there would go # unseen, so it goes to errors.log and into a message box. Not for the runs # that have a console - a -GameData run of the window inherits QG_HIDDEN and # reports through the window. $HIDDEN = $env:QG_HIDDEN -eq '1' $HIDDEN_WINDOW_RUN = $HIDDEN -and -not ($NoGui -or $GameData -or $Update -or $SelfTest) function Show-StartError($err) { try { Add-Type -AssemblyName System.Windows.Forms $x = if ($err -is [System.Management.Automation.ErrorRecord]) { $err.Exception } else { $err } [void][System.Windows.Forms.MessageBox]::Show( ("The Versefiles Companion stopped: {0}`n`nThe details are in errors.log in {1}." -f $x.Message, $DataDir), 'Versefiles Companion', [System.Windows.Forms.MessageBoxButtons]::OK, [System.Windows.Forms.MessageBoxIcon]::Error) } catch { } } trap { if ($HIDDEN_WINDOW_RUN) { Write-CompanionError $_ 'start' Show-StartError $_ } break } # The Game.log reader (tools/gamelog.cs), embedded so the tool ships as one # file. tools/tests/gamelog-embedded.js checks that both copies match. $GAMELOG_SOURCE = @' // Game.log reader for the Versefiles Companion. // // Star Citizen writes Game.log into the channel folder (LIVE, PTU, ...) while // it runs; on the next start the old file moves to logbackups\. This class // turns the lines of one such file into events: contracts, blueprints, // trades, travel, health. // // The log is not an API. Every rule below was taken from real sessions // (Alpha 4.9 and 4.10, Aug/Sep 2026), and CIG changes lines without notice: // and are gone completely. Lines that no // rule knows are ignored, never guessed at. // // Written for PowerShell's Add-Type (C# 5, .NET Framework): no string // interpolation, no null-conditional operators, no LINQ, no tuples. // // Embedded verbatim in tools/VersefilesCompanion.ps1 so the tool ships as one // file; tools/tests/gamelog-embedded.js checks that both copies match. using System; using System.Collections.Generic; using System.Globalization; using System.IO; using System.Text; using System.Text.RegularExpressions; namespace Versefiles.Companion { public sealed class ParseResult { public SessionInfo Session; public List Events = new List(); public bool Ended; } public static class GameLogFile { // Opened for shared reading: the game keeps writing while it runs. static FileStream OpenShared(string path) { return new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite | FileShare.Delete); } // The whole file at once - for sessions in logbackups, which are over. public static ParseResult Parse(string path, string channel, bool final) { ParseResult r = new ParseResult(); GameLogParser p = new GameLogParser(channel); using (FileStream fs = OpenShared(path)) using (StreamReader sr = new StreamReader(fs, new UTF8Encoding(false))) { string line; while ((line = sr.ReadLine()) != null) r.Events.AddRange(p.Feed(line)); } r.Events.AddRange(p.Finish(final || p.Ended)); r.Session = p.Session; r.Ended = p.Ended; return r; } // Session id and build from the header, without reading the file. public static SessionInfo Peek(string path, string channel) { GameLogParser p = new GameLogParser(channel); using (FileStream fs = OpenShared(path)) using (StreamReader sr = new StreamReader(fs, new UTF8Encoding(false))) { string line; int n = 0; while (n++ < 60 && (line = sr.ReadLine()) != null) { p.Feed(line); if (p.Session.Id != null && p.Session.Build != null) break; } } return p.Session; } } // Follows one Game.log while the game writes it. public sealed class LogFollower { readonly string path; readonly string channel; GameLogParser parser; long offset; byte[] carry = new byte[0]; string firstLine; public LogFollower(string path, string channel) { this.path = path; this.channel = channel; } public string Path { get { return path; } } public SessionInfo Session { get { return parser == null ? null : parser.Session; } } public bool Ended { get { return parser != null && parser.Ended; } } // What was appended since the last call. A new session (the game // restarted and recreated the file) finishes the old parser first; // its last events come back in the same list. public List Poll() { List events = new List(); if (!File.Exists(path)) return events; using (FileStream fs = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite | FileShare.Delete)) { string head = ReadFirstLine(fs); if (parser != null && (fs.Length < offset || head != firstLine)) { events.AddRange(parser.Finish(true)); parser = null; } if (parser == null) { if (head == null) return events; // just created, still empty parser = new GameLogParser(channel); offset = 0; carry = new byte[0]; firstLine = head; } if (fs.Length == offset) return events; fs.Seek(offset, SeekOrigin.Begin); byte[] fresh = new byte[fs.Length - offset]; int read = 0; while (read < fresh.Length) { int n = fs.Read(fresh, read, fresh.Length - read); if (n <= 0) break; read += n; } offset += read; byte[] buf = new byte[carry.Length + read]; Buffer.BlockCopy(carry, 0, buf, 0, carry.Length); Buffer.BlockCopy(fresh, 0, buf, carry.Length, read); // Only complete lines. The game may be in the middle of one. int start = 0; for (int i = 0; i < buf.Length; i++) { if (buf[i] != (byte)'\n') continue; string line = Encoding.UTF8.GetString(buf, start, i - start); events.AddRange(parser.Feed(line)); start = i + 1; } carry = new byte[buf.Length - start]; Buffer.BlockCopy(buf, start, carry, 0, carry.Length); } if (parser.Ended) events.AddRange(parser.Finish(true)); return events; } static string ReadFirstLine(FileStream fs) { if (fs.Length == 0) return null; fs.Seek(0, SeekOrigin.Begin); byte[] b = new byte[(int)Math.Min(256, fs.Length)]; int n = fs.Read(b, 0, b.Length); int end = Array.IndexOf(b, (byte)'\n', 0, n); if (end < 0) return null; // header line not complete yet return Encoding.UTF8.GetString(b, 0, end); } } public sealed class LogEvent { // ":". Stable across re-reads of the same session, // including the copy the game later moves to logbackups - which is // what makes uploads and re-imports idempotent. public string Id; public string Session; public int Line; public DateTime Time; public string Kind; public readonly List> Fields = new List>(); public LogEvent Set(string key, object value) { for (int i = 0; i < Fields.Count; i++) { if (Fields[i].Key == key) { Fields[i] = new KeyValuePair(key, value); return this; } } Fields.Add(new KeyValuePair(key, value)); return this; } public object Get(string key) { for (int i = 0; i < Fields.Count; i++) { if (Fields[i].Key == key) return Fields[i].Value; } return null; } public string GetString(string key) { object v = Get(key); return v == null ? null : Convert.ToString(v, CultureInfo.InvariantCulture); } public string ToJson() { StringBuilder sb = new StringBuilder(256); sb.Append("{\"id\":"); Json.Str(sb, Id); sb.Append(",\"session\":"); Json.Str(sb, Session); sb.Append(",\"line\":").Append(Line.ToString(CultureInfo.InvariantCulture)); sb.Append(",\"time\":"); Json.Str(sb, Json.Time(Time)); sb.Append(",\"kind\":"); Json.Str(sb, Kind); for (int i = 0; i < Fields.Count; i++) { sb.Append(','); Json.Str(sb, Fields[i].Key); sb.Append(':'); Json.Value(sb, Fields[i].Value); } sb.Append('}'); return sb.ToString(); } } public sealed class SessionInfo { public string Id; public string Channel; public string Build; public string Version; public string Environment; public string Patch; public string Shard; // The player's own handle. Needed to tell own lines from other // players' (med bed, deaths, ship channels). Stays on this machine. public string Handle; public DateTime? Started; public DateTime? Ended; public DateTime? LastTime; public int Lines; public string ToJson() { StringBuilder sb = new StringBuilder(256); sb.Append("{\"id\":"); Json.Str(sb, Id); sb.Append(",\"channel\":"); Json.Str(sb, Channel); sb.Append(",\"build\":"); Json.Str(sb, Build); sb.Append(",\"version\":"); Json.Str(sb, Version); sb.Append(",\"environment\":"); Json.Str(sb, Environment); sb.Append(",\"shard\":"); Json.Str(sb, Shard); sb.Append(",\"started\":"); Json.Str(sb, Started.HasValue ? Json.Time(Started.Value) : null); sb.Append(",\"ended\":"); Json.Str(sb, Ended.HasValue ? Json.Time(Ended.Value) : null); sb.Append(",\"last_time\":"); Json.Str(sb, LastTime.HasValue ? Json.Time(LastTime.Value) : null); sb.Append(",\"lines\":").Append(Lines.ToString(CultureInfo.InvariantCulture)); sb.Append('}'); return sb.ToString(); } } public static class Json { public static string Time(DateTime t) { return t.ToUniversalTime().ToString("yyyy-MM-dd'T'HH:mm:ss.fff'Z'", CultureInfo.InvariantCulture); } public static void Str(StringBuilder sb, string s) { if (s == null) { sb.Append("null"); return; } sb.Append('"'); foreach (char c in s) { switch (c) { case '"': sb.Append("\\\""); break; case '\\': sb.Append("\\\\"); break; case '\n': sb.Append("\\n"); break; case '\r': sb.Append("\\r"); break; case '\t': sb.Append("\\t"); break; default: if (c < 0x20) { sb.Append("\\u").Append(((int)c).ToString("x4")); } else { sb.Append(c); } break; } } sb.Append('"'); } public static void Value(StringBuilder sb, object v) { if (v == null) { sb.Append("null"); return; } if (v is string) { Str(sb, (string)v); return; } if (v is bool) { sb.Append((bool)v ? "true" : "false"); return; } if (v is int || v is long) { sb.Append(Convert.ToInt64(v).ToString(CultureInfo.InvariantCulture)); return; } if (v is double) { sb.Append(((double)v).ToString("R", CultureInfo.InvariantCulture)); return; } if (v is DateTime) { Str(sb, Time((DateTime)v)); return; } if (v is IList) { IList list = (IList)v; sb.Append('['); for (int i = 0; i < list.Count; i++) { if (i > 0) sb.Append(','); Str(sb, list[i]); } sb.Append(']'); return; } Str(sb, Convert.ToString(v, CultureInfo.InvariantCulture)); } } public sealed class GameLogParser { // A blueprint or an award notification carries no mission. It // follows the "Contract Complete" of its contract within about a // second (40 real cases: 0-5 s), so anything later than this is not // attributed. public const double AttributionWindowSeconds = 15; // Commodity sales only log errors, never a success. A sale counts as // done when no error follows within this window (real errors came // after 0.5 s). public const double TradeResponseWindowSeconds = 10; const string NullGuid = "00000000-0000-0000-0000-000000000000"; static readonly Regex RxLine = new Regex(@"^<(\d{4}-\d\d-\d\dT\d\d:\d\d:\d\d(?:\.\d+)?Z)> ?(.*)$", RegexOptions.Compiled); static readonly Regex RxTag = new Regex(@"^(?:\[(\w+)\] )?<([^<>]{1,90})> ?(.*)$", RegexOptions.Compiled); // Header static readonly Regex RxSession = new Regex(@"@session:\s+'([0-9a-fA-F-]{36})'", RegexOptions.Compiled); static readonly Regex RxEnvironment = new Regex(@"^\[Trace\] Environment:\s+(\S+)", RegexOptions.Compiled); static readonly Regex RxFileVersion = new Regex(@"^FileVersion: (\S+)", RegexOptions.Compiled); // "[Trace] @env_session: 'pub-sc-alpha-4102-12881860'" - the patch // the build belongs to (sc-alpha-4102 = Alpha 4.10.2), for the Home // view's "This patch" (0.14.0). On SessionInfo only, never on an // event: the journal's lines are what the upload sends (0.14.1). static readonly Regex RxEnvSession = new Regex(@"@env_session:\s+'[^']*?(sc-[a-z]+-\d+)-\d+'", RegexOptions.Compiled); static readonly Regex RxBackupName = new Regex(@"^BackupNameAttachment="" ?Build\((\d+)\)", RegexOptions.Compiled); // Identity and session // The character line says STATE_CURRENT on LIVE and STATE_UNSPECIFIED // on the Tech Preview (Sep 2026); the login line wrote "Handle=x" in // 4.9 and "Handle[x]" from 4.10 on. The login line wins. static readonly Regex RxCharacter = new Regex(@"name (\S+) - state STATE_\w+", RegexOptions.Compiled); static readonly Regex RxLegacyLogin = new Regex(@"User Login Success - Handle(?:=|\[)([^\]\s]+)", RegexOptions.Compiled); static readonly Regex RxResult = new Regex(@"result\[(\w+)\]", RegexOptions.Compiled); static readonly Regex RxJoinPu = new Regex(@"shard\[([^\]]+)\]", RegexOptions.Compiled); // HUD notifications. The text may contain quotes and even line breaks; // the fixed suffix is what ends it. static readonly Regex RxNotification = new Regex( @"Added notification ""(.*)"" \[(\d+)\] to queue\. New queue size: \d+, MissionId: \[([0-9a-fA-F-]*)\], ObjectiveId: \[([^\]]*)\]", RegexOptions.Compiled | RegexOptions.Singleline); // Missions static readonly Regex RxCreateMarker = new Regex( @"missionId \[([0-9a-fA-F-]{36})\], generator name \[([^\]]*)\], contract \[([^\]]*)\], contractDefinitionId\[([0-9a-fA-F-]*)\]", RegexOptions.Compiled); static readonly Regex RxEndMission = new Regex( @"MissionId\[([0-9a-fA-F-]{36})\] .*?CompletionType\[(\w+)\] Reason\[([^\]]*)\]", RegexOptions.Compiled); // Shops and trade static readonly Regex RxCommoditySell = new Regex( @"shopId\[(\d+)\] shopName\[([^\]]*)\] kioskId\[\d+\] amount\[([\d.]+)\] resourceGUID\[([0-9a-fA-F-]*)\] autoLoading\[\d\] quantity\[(\d+)\] transactionMode\[(\w+)\](?: Cargo Box Data:\s*\[boxSize\[(\d+)\] \| unitAmount\[(\d+)\]\])?", RegexOptions.Compiled); static readonly Regex RxCommodityError = new Regex(@"result\[(\w+)\] type\[(\w+)\]", RegexOptions.Compiled); static readonly Regex RxShopBuy = new Regex( @"shopId\[(\d+)\] shopName\[([^\]]*)\] kioskId\[\d+\] client_price\[([\d.]+)\] itemClassGUID\[([0-9a-fA-F-]*)\] itemName\[([^\]]*)\] quantity\[(\d+)\]", RegexOptions.Compiled); static readonly Regex RxRental = new Regex( @"shopId\[(\d+)\] shopName\[([^\]]*)\] kioskId\[\d+\] client_price\[([\d.]+)\] itemClassGUID\[([0-9a-fA-F-]*)\] itemName\[([^\]]*)\] offering\[(\d+)\] currencyType\[(\w+)\]", RegexOptions.Compiled); static readonly Regex RxShopResponse = new Regex( @"shopId\[(\d+)\] shopName\[([^\]]*)\] kioskId\[\d+\] kioskState\[(\w+)\] result\[(\w+)\] type\[(\w+)\]", RegexOptions.Compiled); static readonly Regex RxRefinery = new Regex(@"shopId\[(\d+)\] shopName\[([^\]]*)\]", RegexOptions.Compiled); // Travel, vehicles, health static readonly Regex RxQtTarget = new Regex(@"\| ([A-Za-z0-9_]+?)_(\d+)\[\d+\]\|CSCItemNavigation::OnPlayerSelectedQuantumTarget\|Player has selected point (\S+) as their destination", RegexOptions.Compiled); static readonly Regex RxQtArrived = new Regex(@"\| ([A-Za-z0-9_]+?)_(\d+)\[\d+\]\|CSCItemNavigation::OnQuantumDriveArrived", RegexOptions.Compiled); static readonly Regex RxLocation = new Regex(@"requested inventory for Location\[([^\]]+)\]", RegexOptions.Compiled); static readonly Regex RxClearDriver = new Regex(@"releasing control token for '([A-Za-z0-9_]+?)_(\d+)'", RegexOptions.Compiled); static readonly Regex RxMedBed = new Regex( @"Actor: (\S+) .*?Perform surgery event (\w+), med bed name: ([^,]*), vehicle name: ([^,]*), head: (\w+) torso: (\w+) leftArm: (\w+) rightArm: (\w+) leftLeg: (\w+) rightLeg: (\w+)", RegexOptions.Compiled); static readonly Regex RxDead = new Regex(@"Actor '([^']+)' \[\d+\] ejected from zone '([^']*)'", RegexOptions.Compiled); static readonly Regex RxFatalCollision = new Regex(@"for vehicle ([A-Za-z0-9_]+?)_(\d+) \[Part: [^,]*, Pos: [^Z]*Zone: ([^,]*), PlayerPilot: (\d)", RegexOptions.Compiled); // Notification texts. Most end in ": " - the HUD shows title and body // separately and the body of these is empty. static readonly Regex NContractAccepted = new Regex(@"^Contract Accepted:\s+(.+?)\s*:\s*$", RegexOptions.Singleline); static readonly Regex NContractCompleted = new Regex(@"^Contract Complete:\s+(.+?)\s*:\s*$", RegexOptions.Singleline); static readonly Regex NContractFailed = new Regex(@"^Contract Failed:\s+(.+?)\s*:\s*$", RegexOptions.Singleline); static readonly Regex NContractShared = new Regex(@"^Contract Shared:\s+(.+?)\s*:\s*$", RegexOptions.Singleline); static readonly Regex NObjectiveNew = new Regex(@"^New Objective:\s+(.+?)\s*:\s*$", RegexOptions.Singleline); static readonly Regex NObjectiveCompleted = new Regex(@"^Objective Complete:\s+(.+?)\s*:\s*$", RegexOptions.Singleline); static readonly Regex NObjectiveWithdrawn = new Regex(@"^Objective Withdrawn:\s+(.+?)\s*:\s*$", RegexOptions.Singleline); static readonly Regex NBlueprint = new Regex(@"^Received Blueprint:\s+(.+?)\s*:\s*$", RegexOptions.Singleline); static readonly Regex NAward = new Regex(@"^Awarded ([\d,.]+) aUEC\s*:\s*$", RegexOptions.Singleline); static readonly Regex NJournal = new Regex(@"^Journal Entry Added:\s+(.+?)\s*:\s*$", RegexOptions.Singleline); static readonly Regex NRefinery = new Regex(@"^(?:A|(\d+)) Refinery Work Orders? ha(?:s|ve) been Completed at (.+?)\s*:\s*$", RegexOptions.Singleline); static readonly Regex NJurisdiction = new Regex(@"^Entered (.+?) Jurisdiction\s*:\s*$", RegexOptions.Singleline); static readonly Regex NCrimeStat = new Regex(@"^CrimeStat Rating (\w+)\s*:\s*$", RegexOptions.Singleline); static readonly Regex NShipChannel = new Regex(@"^You have (joined|left the) channel '(.+) : ([^']+)'", RegexOptions.Singleline); static readonly Regex RxLauncherMarker = new Regex(@"\s*.*?"); static readonly Regex RxSpaces = new Regex(@"\s+"); sealed class Pending { public LogEvent Event; public string ShopId; public DateTime Deadline; } readonly SessionInfo session = new SessionInfo(); readonly Dictionary contractOf = new Dictionary(); // missionId -> {contract, generator, definition} readonly HashSet announcedMissions = new HashSet(); readonly List pendingTrades = new List(); readonly List output = new List(); // A notification whose text spans several physical lines. StringBuilder noteText; int noteLine; DateTime noteTime; int noteParts; // Last "Contract Complete" - blueprints and awards attach to it. string lastCompletedMission; string lastCompletedTitle; DateTime lastCompletedTime; DateTime lastSharedTime = DateTime.MinValue; string lastLocation; int lineNo; bool started; bool ended; public GameLogParser(string channel) { session.Channel = channel; } public SessionInfo Session { get { return session; } } public bool Ended { get { return ended; } } // One physical line. Returns the events this line completed; trades // come later, once their answer (or its absence) is known. public List Feed(string raw) { output.Clear(); lineNo++; session.Lines = lineNo; if (raw == null) return Drain(); Match m = RxLine.Match(raw.TrimEnd('\r')); if (!m.Success) { // Lines without a timestamp belong to the notification before. if (noteText != null) ContinueNote(raw.TrimEnd('\r')); return Drain(); } DateTime t = ParseTime(m.Groups[1].Value); string body = m.Groups[2].Value; session.LastTime = t; ExpireTrades(t); if (noteText != null) { // Continuation lines carry a timestamp but no level and no tag. if (!body.StartsWith("[") && !body.StartsWith("<") && !body.StartsWith(" ")) { ContinueNote(body); return Drain(); } noteText = null; // never finished - drop it rather than guess } Match tm = RxTag.Match(body); if (!tm.Success) { if (!started) Header(body, t); // The last line of a session, untagged: "[CIG] CCIGBroker::FastShutdown". else if (body.StartsWith("[CIG] CCIGBroker::FastShutdown")) EndSession(t); return Drain(); } // The header ends with the first tagged line. if (!started) StartSession(t); Dispatch(tm.Groups[2].Value, tm.Groups[3].Value, t); return Drain(); } // End of the file. Final means the session is over (a backup, or the // game has quit): whatever still waits for an answer is decided now. public List Finish(bool final) { output.Clear(); if (final) { noteText = null; ExpireTrades(DateTime.MaxValue); } return Drain(); } List Drain() { List r = new List(output); output.Clear(); return r; } static DateTime ParseTime(string s) { return DateTime.Parse(s, CultureInfo.InvariantCulture, DateTimeStyles.AdjustToUniversal | DateTimeStyles.AssumeUniversal); } LogEvent NewEvent(string kind, DateTime t) { LogEvent e = new LogEvent(); e.Session = SessionId(); e.Line = lineNo; e.Id = e.Session + ":" + lineNo.ToString(CultureInfo.InvariantCulture); e.Time = t; e.Kind = kind; return e; } LogEvent Emit(string kind, DateTime t) { LogEvent e = NewEvent(kind, t); output.Add(e); return e; } string SessionId() { if (session.Id != null) return session.Id; // A file without the header line: fall back to something stable. return "nosession-" + (session.Build ?? "0") + "-" + (session.Started.HasValue ? session.Started.Value.Ticks.ToString(CultureInfo.InvariantCulture) : "0"); } void Header(string body, DateTime t) { if (!session.Started.HasValue) session.Started = t; Match m; if ((m = RxBackupName.Match(body)).Success) { session.Build = m.Groups[1].Value; return; } if ((m = RxFileVersion.Match(body)).Success) { session.Version = m.Groups[1].Value; return; } if ((m = RxEnvironment.Match(body)).Success) { session.Environment = m.Groups[1].Value; return; } if ((m = RxEnvSession.Match(body)).Success) { session.Patch = m.Groups[1].Value; return; } if ((m = RxSession.Match(body)).Success) { session.Id = m.Groups[1].Value.ToLowerInvariant(); return; } } void StartSession(DateTime t) { started = true; if (!session.Started.HasValue) session.Started = t; LogEvent e = Emit("session_started", session.Started.Value); e.Set("channel", session.Channel).Set("build", session.Build).Set("version", session.Version).Set("environment", session.Environment); } void Dispatch(string tag, string rest, DateTime t) { Match m; switch (tag) { case "SHUDEvent_OnNotification": if (rest.StartsWith("Added notification \"")) { noteText = new StringBuilder(rest); noteLine = lineNo; noteTime = t; noteParts = 1; TryFinishNote(); } return; case "AccountLoginCharacterStatus_Character": if (session.Handle == null && (m = RxCharacter.Match(rest)).Success) session.Handle = m.Groups[1].Value; return; case "Legacy login response": if ((m = RxLegacyLogin.Match(rest)).Success) session.Handle = m.Groups[1].Value; return; case "Join PU": if ((m = RxJoinPu.Match(rest)).Success) { session.Shard = m.Groups[1].Value; LogEvent e = Emit("server_joined", t); e.Set("shard", session.Shard); string[] parts = session.Shard.Split('_'); if (parts.Length > 1) e.Set("region", parts[1]); } return; case "CLocalMissionPhaseMarker::CreateMarker": if ((m = RxCreateMarker.Match(rest)).Success) { string mission = m.Groups[1].Value.ToLowerInvariant(); contractOf[mission] = new string[] { m.Groups[3].Value, m.Groups[2].Value, m.Groups[4].Value.ToLowerInvariant() }; if (announcedMissions.Add(mission)) { LogEvent e = Emit("mission_contract", t); e.Set("mission_id", mission).Set("contract", m.Groups[3].Value).Set("generator", m.Groups[2].Value) .Set("contract_definition_id", m.Groups[4].Value.ToLowerInvariant()); } } return; case "EndMission": if ((m = RxEndMission.Match(rest)).Success) { string mission = m.Groups[1].Value.ToLowerInvariant(); LogEvent e = Emit("mission_ended", t); e.Set("mission_id", mission).Set("completion", m.Groups[2].Value).Set("reason", m.Groups[3].Value); AddContract(e, mission); } return; case "MissionShared": // The owner is another player - only the moment is kept. lastSharedTime = t; return; case "CEntityComponentCommodityUIProvider::SendCommoditySellRequest": if ((m = RxCommoditySell.Match(rest)).Success) { LogEvent e = NewEvent("commodity_sold", t); e.Set("shop", m.Groups[2].Value).Set("resource_guid", m.Groups[4].Value.ToLowerInvariant()) .Set("quantity", ParseLong(m.Groups[5].Value)).Set("amount", ParseMoney(m.Groups[3].Value)) .Set("mode", m.Groups[6].Value); if (m.Groups[7].Success) e.Set("box_size", ParseLong(m.Groups[7].Value)).Set("unit_amount", ParseLong(m.Groups[8].Value)); Defer(e, m.Groups[1].Value, t); } return; case "CEntityComponentCommodityUIProvider::RmToken_CommodityTransactionResponse": if ((m = RxCommodityError.Match(rest)).Success) FailTrade("commodity_sold", null, m.Groups[1].Value); return; case "CEntityComponentShopUIProvider::SendShopBuyRequest": if ((m = RxShopBuy.Match(rest)).Success) { LogEvent e = NewEvent("item_bought", t); e.Set("shop", m.Groups[2].Value).Set("item_class", m.Groups[5].Value).Set("item_guid", m.Groups[4].Value.ToLowerInvariant()) .Set("price", ParseMoney(m.Groups[3].Value)).Set("quantity", ParseLong(m.Groups[6].Value)); Defer(e, m.Groups[1].Value, t); } return; case "CEntityComponentShoppingProvider::SendRentalRequest": if ((m = RxRental.Match(rest)).Success) { LogEvent e = NewEvent("item_rented", t); e.Set("shop", m.Groups[2].Value).Set("item_class", m.Groups[5].Value).Set("item_guid", m.Groups[4].Value.ToLowerInvariant()) .Set("price", ParseMoney(m.Groups[3].Value)).Set("offering", ParseLong(m.Groups[6].Value)).Set("currency", m.Groups[7].Value); Defer(e, m.Groups[1].Value, t); } return; case "CEntityComponentShopUIProvider::RmShopFlowResponse": if ((m = RxShopResponse.Match(rest)).Success) AnswerShop(m.Groups[1].Value, m.Groups[4].Value, null); return; case "CEntityComponentShoppingProvider::RmShopFlowResponse": // The rental answer names no shop: "Shop Flow Response - playerId[..] result[Success]". if ((m = RxShopResponse.Match(rest)).Success) AnswerShop(m.Groups[1].Value, m.Groups[4].Value, null); else if ((m = RxResult.Match(rest)).Success) AnswerShop(null, m.Groups[1].Value, "item_rented"); return; case "CEntityComponentMiningShopUIProvider::OnRefineryRequest": if ((m = RxRefinery.Match(rest)).Success) Emit("refinery_order_sent", t).Set("shop", m.Groups[2].Value); return; case "Player Selected Quantum Target - Local": if ((m = RxQtTarget.Match(rest)).Success) Emit("quantum_target", t).Set("vehicle_class", m.Groups[1].Value).Set("destination", m.Groups[3].Value); return; case "Quantum Drive Arrived - Arrived at Final Destination": if ((m = RxQtArrived.Match(rest)).Success) Emit("quantum_arrived", t).Set("vehicle_class", m.Groups[1].Value); return; case "RequestLocationInventory": if ((m = RxLocation.Match(rest)).Success && m.Groups[1].Value != lastLocation) { lastLocation = m.Groups[1].Value; Emit("location", t).Set("location", lastLocation); } return; case "Vehicle Control Flow": if ((m = RxClearDriver.Match(rest)).Success) Emit("piloting_ended", t).Set("vehicle_class", m.Groups[1].Value); return; case "MED BED HEAL": if ((m = RxMedBed.Match(rest)).Success && IsOwn(m.Groups[1].Value)) { List limbs = new List(); string[] names = { "head", "torso", "left_arm", "right_arm", "left_leg", "right_leg" }; for (int i = 0; i < names.Length; i++) { if (m.Groups[5 + i].Value == "true") limbs.Add(names[i]); } Emit("medbed_treatment", t).Set("result", m.Groups[2].Value).Set("bed", m.Groups[3].Value) .Set("vehicle", m.Groups[4].Value).Set("limbs", limbs); } return; case "[ActorState] Dead": if ((m = RxDead.Match(rest)).Success && IsOwn(m.Groups[1].Value)) Emit("died", t).Set("zone", StripEntityId(m.Groups[2].Value)); return; case "FatalCollision": if ((m = RxFatalCollision.Match(rest)).Success) Emit("fatal_collision", t).Set("vehicle_class", m.Groups[1].Value).Set("zone", m.Groups[3].Value).Set("player_pilot", m.Groups[4].Value == "1"); return; case "SystemQuit": EndSession(t); return; } } void EndSession(DateTime t) { if (ended) return; ended = true; session.Ended = t; Emit("session_ended", t); } bool IsOwn(string actor) { // Before the login line the handle is unknown; nothing is claimed then. return session.Handle != null && string.Equals(actor, session.Handle, StringComparison.OrdinalIgnoreCase); } static string StripEntityId(string name) { return Regex.Replace(name ?? "", @"_\d{6,}$", ""); } static long ParseLong(string s) { long v; return long.TryParse(s, NumberStyles.Integer, CultureInfo.InvariantCulture, out v) ? v : 0; } static double ParseMoney(string s) { double v; return double.TryParse(s, NumberStyles.Float, CultureInfo.InvariantCulture, out v) ? Math.Round(v, 2) : 0; } void AddContract(LogEvent e, string mission) { string[] c; if (mission != null && contractOf.TryGetValue(mission, out c)) { e.Set("contract", c[0]); if (c[1].Length > 0) e.Set("generator", c[1]); } } // ------------------------------------------------------------------ // Notifications // ------------------------------------------------------------------ void ContinueNote(string text) { noteParts++; if (noteParts > 8) { noteText = null; return; } noteText.Append('\n').Append(text); TryFinishNote(); } void TryFinishNote() { Match m = RxNotification.Match(noteText.ToString()); if (!m.Success) return; noteText = null; string text = m.Groups[1].Value; string mission = m.Groups[3].Value.ToLowerInvariant(); if (mission == NullGuid || mission.Length == 0) mission = null; string objective = m.Groups[4].Value.Length > 0 ? m.Groups[4].Value.ToLowerInvariant() : null; // The event keeps the line the notification started on. int saved = lineNo; lineNo = noteLine; try { Note(text, mission, objective, noteTime); } finally { lineNo = saved; } } // Titles come from the installed global.ini. Launchers that patch it // (SC-Deutsch adds "[BP]" and similar) leave their marks // here too; the raw text is kept, the cleaned one is what matches. static string Clean(string title) { string s = RxLauncherMarker.Replace(title ?? "", ""); return RxSpaces.Replace(s, " ").Trim(); } void Note(string text, string mission, string objective, DateTime t) { Match m; LogEvent e; if ((m = NContractAccepted.Match(text)).Success) { e = Contract("contract_accepted", m, mission, t); return; } if ((m = NContractCompleted.Match(text)).Success) { e = Contract("contract_completed", m, mission, t); lastCompletedMission = mission; lastCompletedTitle = e.GetString("title"); lastCompletedTime = t; return; } if ((m = NContractFailed.Match(text)).Success) { Contract("contract_failed", m, mission, t); return; } if ((m = NContractShared.Match(text)).Success) { Contract("contract_shared", m, mission, t); lastSharedTime = t; return; } if ((m = NObjectiveNew.Match(text)).Success) { Objective("objective_added", m, mission, objective, t); return; } if ((m = NObjectiveCompleted.Match(text)).Success) { Objective("objective_completed", m, mission, objective, t); return; } if ((m = NObjectiveWithdrawn.Match(text)).Success) { Objective("objective_withdrawn", m, mission, objective, t); return; } if ((m = NBlueprint.Match(text)).Success) { e = Emit("blueprint_received", t); e.Set("name", Clean(m.Groups[1].Value)); Attribute(e, t); return; } if ((m = NAward.Match(text)).Success) { e = Emit("reward_received", t); e.Set("amount", ParseLong(m.Groups[1].Value.Replace(",", "").Replace(".", ""))).Set("currency", "aUEC"); Attribute(e, t); return; } if ((m = NJournal.Match(text)).Success) { Emit("journal_entry", t).Set("title", Clean(m.Groups[1].Value)); return; } if ((m = NRefinery.Match(text)).Success) { Emit("refinery_orders_completed", t).Set("count", m.Groups[1].Success ? ParseLong(m.Groups[1].Value) : 1).Set("location", Clean(m.Groups[2].Value)); return; } if ((m = NJurisdiction.Match(text)).Success) { Emit("jurisdiction_entered", t).Set("jurisdiction", Clean(m.Groups[1].Value)); return; } if (text.StartsWith("Entered Monitored Space")) { Emit("monitored_space_entered", t); return; } if (text.StartsWith("Exited Monitored Space")) { Emit("monitored_space_left", t); return; } if (text.StartsWith("Entering Armistice Zone")) { Emit("armistice_entered", t); return; } if (text.StartsWith("Leaving Armistice Zone")) { Emit("armistice_left", t); return; } if (text.StartsWith("Entering Private Property")) { Emit("private_property_entered", t); return; } if (text.StartsWith("Leaving Private Property")) { Emit("private_property_left", t); return; } // Answers landing AND departure requests alike - the log does not say which. if (text.StartsWith("Hangar Request Completed")) { Emit("hangar_request_completed", t); return; } if ((m = NCrimeStat.Match(text)).Success) { Emit("crimestat_changed", t).Set("change", m.Groups[1].Value.ToLowerInvariant()); return; } if ((m = NShipChannel.Match(text)).Success) { // A ship's comms channel - joined when getting aboard. The // owner may be someone else; their name is not kept. e = Emit(m.Groups[1].Value == "joined" ? "ship_channel_joined" : "ship_channel_left", t); e.Set("ship", m.Groups[2].Value.Trim()).Set("own_ship", IsOwn(m.Groups[3].Value.Trim())); return; } // Everything else is a tutorial hint or a party message with // other players' names - neither is kept. } LogEvent Contract(string kind, Match m, string mission, DateTime t) { LogEvent e = Emit(kind, t); string raw = m.Groups[1].Value.Trim(); string title = Clean(raw); e.Set("title", title); if (raw != title) e.Set("title_raw", raw); if (mission != null) e.Set("mission_id", mission); AddContract(e, mission); return e; } void Objective(string kind, Match m, string mission, string objective, DateTime t) { LogEvent e = Emit(kind, t); e.Set("objective", Clean(m.Groups[1].Value)); if (mission != null) e.Set("mission_id", mission); if (objective != null) e.Set("objective_id", objective); } // Blueprints and awards come without a mission. They belong to the // contract that completed right before. void Attribute(LogEvent e, DateTime t) { if (lastCompletedMission != null || lastCompletedTitle != null) { double dt = (t - lastCompletedTime).TotalSeconds; if (dt >= 0 && dt <= AttributionWindowSeconds) { if (lastCompletedMission != null) e.Set("mission_id", lastCompletedMission); e.Set("mission_title", lastCompletedTitle); AddContract(e, lastCompletedMission); e.Set("attribution", "contract_completed"); e.Set("attribution_delay_ms", (long)Math.Round(dt * 1000)); return; } } double ds = (t - lastSharedTime).TotalSeconds; e.Set("attribution", ds >= 0 && ds <= 120 ? "none_shared_contract" : "none"); } // ------------------------------------------------------------------ // Trades: the request is logged at once, the outcome afterwards // ------------------------------------------------------------------ void Defer(LogEvent e, string shopId, DateTime t) { Pending p = new Pending(); p.Event = e; p.ShopId = shopId; p.Deadline = t.AddSeconds(TradeResponseWindowSeconds); pendingTrades.Add(p); } // Answers the oldest open request of that shop - or, when the answer // names no shop, the oldest open request of that kind. void AnswerShop(string shopId, string result, string kind) { for (int i = 0; i < pendingTrades.Count; i++) { Pending p = pendingTrades[i]; if (p.Event.Kind == "commodity_sold") continue; if (shopId != null && p.ShopId != shopId) continue; if (kind != null && p.Event.Kind != kind) continue; p.Event.Set("status", result == "Success" ? "ok" : "failed"); if (result != "Success") p.Event.Set("error", result); pendingTrades.RemoveAt(i); output.Add(p.Event); return; } } void FailTrade(string kind, string shopId, string error) { // The error names no shop; it answers the oldest open sale. for (int i = 0; i < pendingTrades.Count; i++) { Pending p = pendingTrades[i]; if (p.Event.Kind != kind || (shopId != null && p.ShopId != shopId)) continue; p.Event.Set("status", "failed").Set("error", error); pendingTrades.RemoveAt(i); output.Add(p.Event); return; } } void ExpireTrades(DateTime now) { for (int i = 0; i < pendingTrades.Count; ) { Pending p = pendingTrades[i]; if (now < p.Deadline) { i++; continue; } // No answer. A sale without an error went through; a purchase // without an answer is unknown. p.Event.Set("status", p.Event.Kind == "commodity_sold" ? "ok" : "unknown"); pendingTrades.RemoveAt(i); output.Add(p.Event); } } } } '@ # =========================================================================== # Game folder # =========================================================================== # Takes what the user typed or picked and finds the "StarCitizen" folder in # it: the folder itself, a channel folder (LIVE, PTU ...) or the library # folder "Roberts Space Industries" above. $null if nothing looks like an # installation. A channel folder holds Data.p4k - or Data.p4k.part while the # launcher still downloads it -, build_manifest.id or a Game.log. function Resolve-GameRoot([string]$path) { if (-not $path) { return $null } $p = $path.Trim().Trim('"').TrimEnd('\', '/') if (-not $p -or -not (Test-Path -LiteralPath $p -PathType Container)) { return $null } $p = (Resolve-Path -LiteralPath $p).Path.TrimEnd('\') $isChannel = { param($d) (Test-Path (Join-Path $d 'Data.p4k')) -or (Test-Path (Join-Path $d 'Data.p4k.part')) -or (Test-Path (Join-Path $d 'build_manifest.id')) -or (Test-Path (Join-Path $d 'Game.log')) } $isRoot = { param($d) @(Get-ChildItem -LiteralPath $d -Directory -ErrorAction SilentlyContinue | Where-Object { & $isChannel $_.FullName }).Count -gt 0 } if (& $isChannel $p) { return (Split-Path $p -Parent) } if (& $isRoot $p) { return $p } $below = Join-Path $p 'StarCitizen' if ((Test-Path -LiteralPath $below) -and (& $isRoot $below)) { return $below } return $null } # The launcher logs "Installing Star Citizen LIVE ... at (type: ..." # for every install; the last one names the current library. function Get-GameRootFromLauncher { $log = Join-Path $env:APPDATA 'rsilauncher\logs\log.log' if (-not (Test-Path $log)) { return $null } try { $hit = @(Get-Content $log -Tail 20000 -ErrorAction Stop | Select-String -Pattern 'Installing Star Citizen \S+ \S+ at (.+?) \(type:' | Select-Object -Last 1) if (-not $hit.Count) { return $null } return (Resolve-GameRoot ($hit[0].Matches[0].Groups[1].Value -replace '\\\\', '\')) } catch { return $null } } # In this order: -GamePath, the folder chosen in the window, the old # uploader's setting, the launcher's log, the default path. Where it came # from goes to $script:GameRootSource - the Game data view says so, so that # nobody has to guess why another folder is in there. function Get-GameRoot([string]$parameter) { if ($parameter) { $p = Resolve-GameRoot $parameter if (-not $p) { throw "No Star Citizen installation under '$parameter' (expected the StarCitizen folder with LIVE, PTU, ...)." } $script:GameRootSource = 'parameter' return $p } $p = Resolve-GameRoot ([string]$script:Settings.gamePath) if ($p) { $script:GameRootSource = 'settings'; return $p } if (Test-Path $UPLOADER_CONFIG) { try { $conf = Get-Content $UPLOADER_CONFIG -Raw -Encoding UTF8 | ConvertFrom-Json $p = Resolve-GameRoot $conf.gamePath if ($p) { $script:GameRootSource = 'uploader'; return $p } } catch { } } $p = Get-GameRootFromLauncher if ($p) { $script:GameRootSource = 'launcher'; return $p } $script:GameRootSource = 'default' return $DEFAULT_GAME_ROOT } # Every folder with a Game.log or log backups is a channel - LIVE, PTU, # HOTFIX, TECH-PREVIEW, and whatever CIG adds next. No fixed list. function Get-Channels([string]$root) { if (-not (Test-Path -LiteralPath $root)) { return @() } return @(Get-ChildItem -LiteralPath $root -Directory | Where-Object { (Test-Path (Join-Path $_.FullName 'Game.log')) -or (Test-Path (Join-Path $_.FullName 'logbackups')) }) } # =========================================================================== # RSI Launcher (0.14.0) # =========================================================================== # # "Start game" starts the RSI Launcher, never the game: the launcher signs # in and starts StarCitizen_Launcher.exe with the credentials and Easy # Anti-Cheat - nothing a tool should do in its place (Alex, 13.10.2026). $DEFAULT_LAUNCHER = 'C:\Program Files\Roberts Space Industries\RSI Launcher\RSI Launcher.exe' # Where the launcher may be, best guess first: the uninstall entry Windows # keeps (InstallLocation is empty for launcher 2.18.1, the folder of # DisplayIcon is right), the library the game sits in, the default path. function Get-RsiLauncherCandidates([string]$gameRoot) { $list = New-Object System.Collections.ArrayList foreach ($key in 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*', 'HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*', 'HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*') { foreach ($app in @(Get-ItemProperty $key -ErrorAction SilentlyContinue | Where-Object { $_.DisplayName -like 'RSI Launcher*' })) { # Path.Combine, not Join-Path: that one fails on a drive that is # not there (a game on an unplugged disk). if ($app.InstallLocation) { [void]$list.Add([IO.Path]::Combine(([string]$app.InstallLocation).Trim('"'), 'RSI Launcher.exe')) } if ($app.DisplayIcon) { $icon = ([string]$app.DisplayIcon).Split(',')[0].Trim('"') [void]$list.Add([IO.Path]::Combine([IO.Path]::GetDirectoryName($icon), 'RSI Launcher.exe')) } } } if ($gameRoot) { [void]$list.Add([IO.Path]::Combine([IO.Path]::GetDirectoryName($gameRoot.TrimEnd('\')), 'RSI Launcher', 'RSI Launcher.exe')) } [void]$list.Add($DEFAULT_LAUNCHER) return ,$list } function Find-RsiLauncher([string[]]$candidates) { foreach ($c in $candidates) { if ($c -and (Test-Path -LiteralPath $c -PathType Leaf)) { return $c } } return $null } function Test-GameRunning { return @(Get-Process -Name 'StarCitizen' -ErrorAction SilentlyContinue).Count -gt 0 } # An open launcher window comes to the front; otherwise the launcher starts # (a second start of a launcher hidden in its tray brings that one back). function Start-RsiLauncher { $exe = Find-RsiLauncher (Get-RsiLauncherCandidates $GAME_ROOT) if (-not $exe) { return $false } $open = @(Get-Process -Name 'RSI Launcher' -ErrorAction SilentlyContinue | Where-Object { $_.MainWindowHandle -ne [IntPtr]::Zero }) | Select-Object -First 1 if ($open) { if ([VersefilesNative.ConsoleWindow]::IsIconic($open.MainWindowHandle)) { [void][VersefilesNative.ConsoleWindow]::ShowWindow($open.MainWindowHandle, 9) } [void][VersefilesNative.ConsoleWindow]::SetForegroundWindow($open.MainWindowHandle) return $true } Start-Process -FilePath $exe -WorkingDirectory (Split-Path $exe -Parent) return $true } # =========================================================================== # Journal # =========================================================================== # # One file per game session: sessions\.jsonl, one event per line, # plus sessions.json with what is known about each session. Event ids are # ":", so reading the same session twice - live first, then # again from logbackups after the game moved it there - adds nothing twice. $script:Index = @{} # session id -> hashtable $script:SeenIds = @{} # session id -> HashSet of event ids (open sessions only) function Read-Index { $script:Index = @{} if (-not (Test-Path $INDEX_FILE)) { return } # Into a variable first: in Windows PowerShell, ConvertFrom-Json passes a # JSON array down the pipeline as ONE object. $data = Get-Content $INDEX_FILE -Raw -Encoding UTF8 | ConvertFrom-Json foreach ($s in $data) { $h = @{} foreach ($p in $s.PSObject.Properties) { $h[$p.Name] = $p.Value } $script:Index[$h.id] = $h } } function Save-Index { if (-not (Test-Path $DataDir)) { New-Item -ItemType Directory -Path $DataDir -Force | Out-Null } $list = @($script:Index.Values | Sort-Object { $_.started }) $json = if ($list.Count) { ConvertTo-Json -InputObject $list -Depth 4 } else { '[]' } $tmp = $INDEX_FILE + '.tmp' [System.IO.File]::WriteAllText($tmp, $json, $UTF8) Move-Item -LiteralPath $tmp -Destination $INDEX_FILE -Force } function Get-SessionFile([string]$id) { Join-Path $SESSION_DIR ($id + '.jsonl') } function Test-SessionDone([string]$id) { $s = $script:Index[$id] return ($s -and $s.done -and (Test-Path (Get-SessionFile $id))) } # Ids already written for a session that is still open. Returned with a # leading comma: PowerShell would unroll the set, and an empty one arrives as # $null. function Get-SeenIds([string]$id) { if ($script:SeenIds.ContainsKey($id)) { return ,$script:SeenIds[$id] } $set = New-Object 'System.Collections.Generic.HashSet[string]' $file = Get-SessionFile $id if (Test-Path $file) { foreach ($line in [System.IO.File]::ReadLines($file, $UTF8)) { $m = [regex]::Match($line, '^\{"id":"([^"]+)"') if ($m.Success) { [void]$set.Add($m.Groups[1].Value) } } } $script:SeenIds[$id] = $set return ,$set } function Update-Session($info, [bool]$done, [string]$source) { if (-not $info -or -not $info.Id) { return } $s = $script:Index[$info.Id] if (-not $s) { $s = @{ id = $info.Id; events = 0; uploaded = 0; done = $false } ; $script:Index[$info.Id] = $s } $s.channel = $info.Channel $s.build = $info.Build $s.version = $info.Version $s.environment = $info.Environment $s.patch = $info.Patch $s.shard = $info.Shard # PowerShell unwraps Nullable by itself - no .Value here. $s.started = if ($null -ne $info.Started) { ([datetime]$info.Started).ToString('o') } else { $null } $s.ended = if ($null -ne $info.Ended) { ([datetime]$info.Ended).ToString('o') } else { $null } $s.lines = $info.Lines if ($source) { $s.source = $source } if ($done) { $s.done = $true; [void]$script:SeenIds.Remove($info.Id) } } # Appends what is new; returns exactly those events. function Add-JournalEvents($events) { $added = New-Object System.Collections.ArrayList if (-not $events -or $events.Count -eq 0) { return ,$added } if (-not (Test-Path $SESSION_DIR)) { New-Item -ItemType Directory -Path $SESSION_DIR -Force | Out-Null } $bySession = @{} foreach ($e in $events) { if (Test-SessionDone $e.Session) { continue } if (-not $bySession.ContainsKey($e.Session)) { $bySession[$e.Session] = New-Object System.Collections.ArrayList } [void]$bySession[$e.Session].Add($e) } foreach ($id in $bySession.Keys) { $seen = Get-SeenIds $id $lines = New-Object System.Collections.Generic.List[string] foreach ($e in $bySession[$id]) { if (-not $seen.Add($e.Id)) { continue } $lines.Add($e.ToJson()) [void]$added.Add($e) } if ($lines.Count) { [System.IO.File]::AppendAllLines((Get-SessionFile $id), $lines, $UTF8) if ($script:Index[$id]) { $script:Index[$id].events = [int]$script:Index[$id].events + $lines.Count } else { $script:Index[$id] = @{ id = $id; events = $lines.Count; uploaded = 0; done = $false } } } } return ,$added } # =========================================================================== # Reading # =========================================================================== # Sessions the game moved to logbackups are over - read each once. function Import-LogBackups([string]$root, [scriptblock]$log) { $count = 0 foreach ($ch in Get-Channels $root) { $dir = Join-Path $ch.FullName 'logbackups' if (-not (Test-Path $dir)) { continue } foreach ($f in @(Get-ChildItem -LiteralPath $dir -Filter *.log | Sort-Object LastWriteTime)) { try { $peek = [Versefiles.Companion.GameLogFile]::Peek($f.FullName, $ch.Name) if ($peek.Id -and (Test-SessionDone $peek.Id)) { continue } $r = [Versefiles.Companion.GameLogFile]::Parse($f.FullName, $ch.Name, $true) [void](Add-JournalEvents $r.Events) Update-Session $r.Session $true $f.FullName $count++ } catch { if ($log) { & $log ("Skipped {0}: {1}" -f $f.Name, $_.Exception.Message) } } } } if ($count) { Save-Index } if ($log -and $count) { & $log ("Read {0} earlier session(s) from logbackups." -f $count) } return $count } $script:Followers = @() function Start-LogFollowers([string]$root) { $script:Followers = @(Get-Channels $root | ForEach-Object { New-Object Versefiles.Companion.LogFollower((Join-Path $_.FullName 'Game.log'), $_.Name) }) } # New events from all running logs since the last call. function Receive-NewEvents { $all = New-Object System.Collections.ArrayList $touched = $false foreach ($f in $script:Followers) { $events = $f.Poll() if ($events.Count) { foreach ($e in (Add-JournalEvents $events)) { [void]$all.Add($e) } $touched = $true } if ($f.Session -and $f.Session.Id -and -not (Test-SessionDone $f.Session.Id)) { Update-Session $f.Session $f.Ended $f.Path $touched = $true } } if ($touched) { Save-Index } return ,$all } # Every event of every session, oldest first - for the window and the summary. function Read-Journal { $all = New-Object System.Collections.ArrayList if (-not (Test-Path $SESSION_DIR)) { return ,$all } foreach ($s in @($script:Index.Values | Sort-Object { $_.started })) { $file = Get-SessionFile $s.id if (-not (Test-Path $file)) { continue } foreach ($line in [System.IO.File]::ReadLines($file, $UTF8)) { if ($line) { [void]$all.Add(($line | ConvertFrom-Json)) } } } return ,$all } # =========================================================================== # Sending to Versefiles # =========================================================================== # # Only with a companion key (created on the profile page at versefiles.com) # and only what the journal holds - which never contains the handle, # account or character ids, addresses or other players; the server keeps # only fields it knows on top of that. # # Per session the journal remembers how many lines went out ("uploaded" in # sessions.json); the rest follows in batches. A lost answer means a batch # is sent twice - harmless, the server keeps every event once. function Read-Settings { # overlaySteps: the overlay again for each new step (0.9.0); gamePath: # the folder chosen in the Game data view; updateChannel: 'stable' or # 'beta' (0.13.0). One key since 0.15.1 - game data goes with "key" too. $s = @{ apiBase = $DEFAULT_API; key = ''; upload = $true; autoUpdate = $true; overlay = $true; overlayHotkey = $DEFAULT_OVERLAY_HOTKEY overlaySteps = $true; gamePath = ''; updateChannel = 'stable' } $saved = @() if (Test-Path $SETTINGS_FILE) { try { $f = Get-Content $SETTINGS_FILE -Raw -Encoding UTF8 | ConvertFrom-Json foreach ($p in $f.PSObject.Properties) { $s[$p.Name] = $p.Value; $saved += $p.Name } } catch { } } # The old default, saved along with the other settings - nobody chose it, # there is nowhere to choose one. if ($s.overlayHotkey -eq 'Ctrl+Alt+V') { $s.overlayHotkey = $DEFAULT_OVERLAY_HOTKEY } return $s } function Save-Settings($s) { if (-not (Test-Path $SETTINGS_DIR)) { New-Item -ItemType Directory -Path $SETTINGS_DIR -Force | Out-Null } [System.IO.File]::WriteAllText($SETTINGS_FILE, (ConvertTo-Json -InputObject $s), $UTF8) # This user only - the keys are secrets. try { icacls $SETTINGS_FILE /inheritance:r /grant:r "${env:USERNAME}:(R,W)" | Out-Null } catch { } } # The companion's name with its version - and "(beta)" on the beta channel # (0.13.0), so a tester knows: window, tray and the ready panel. function Get-CompanionTitle { return 'Versefiles Companion ' + $COMPANION_VERSION + $(if ($script:Settings.updateChannel -eq 'beta') { ' (beta)' } else { '' }) } $script:Settings = Read-Settings if ($ApiBase) { $script:Settings.apiBase = $ApiBase } if ($Key) { $script:Settings.key = $Key } $script:UploadState = @{ Status = 'off'; Sent = 0; Last = $null; Message = '' } $script:Account = @{ Name = $null; Status = 'none'; GameData = $null } # The HTTP status of a failed call, 0 without an answer (offline, timeout). function Get-HttpStatus($errorRecord) { $r = $errorRecord.Exception.Response if ($r) { return [int]$r.StatusCode } return 0 } # Whose key this is, for the window (Alex, 27.09.2026: show the account the # companion sends to). Status: 'ok' with the display name, 'rejected' for a # key the server does not take, 'error' when it cannot be reached (the name # known so far stays), 'none' without a key. GameData (0.15.1, Alex # 15.10.2026): whether this key may upload game data - $null while unknown; # a site that does not say counts as no. function Update-Account { $a = $script:Account if (-not $script:Settings.key) { $a.Name = $null; $a.Status = 'none'; $a.GameData = $null; return } try { $answer = Invoke-RestMethod -Uri (([string]$script:Settings.apiBase).TrimEnd('/') + '/api/v1/companion/account') ` -Headers @{ Authorization = 'Bearer ' + $script:Settings.key } -TimeoutSec 15 -UserAgent $UPDATE_AGENT $a.Name = [string]$answer.name $a.Status = 'ok' $a.GameData = ($answer.game_data -eq $true) } catch { if ((Get-HttpStatus $_) -eq 401) { $a.Name = $null; $a.Status = 'rejected'; $a.GameData = $null } else { $a.Status = 'error' } } } # What the Game data tab shows (0.15.1, Alex 15.10.2026: one key; the right # to upload game data sits on it, an admin switches it on). 'none' and # 'rejected' ask for the key, 'denied' and 'unknown' say why not, 'ok' is # the view. While the site is away, what it said last stands. function Get-GameDataAccess([string]$key, $account) { if (-not $key) { return 'none' } if ($account.Status -eq 'rejected') { return 'rejected' } if ($account.GameData -eq $true) { return 'ok' } if ($account.GameData -eq $false) { return 'denied' } return 'unknown' } # Title and text of the tab without the right view; Form: the key field, # Save and the profile link; Check: the "Check again" button. function Get-GameDataAccessText([string]$access, [string]$name) { switch ($access) { 'rejected' { return @{ Title = 'Your Versefiles key'; Form = $true; Check = $false Text = 'Versefiles does not take this key. Paste the companion key from your profile page on versefiles.com.' } } 'denied' { return @{ Title = 'Game data upload'; Form = $false; Check = $true Text = ('This key may not upload game data. A Versefiles admin can allow it for your key{0}.' -f $(if ($name) { " (account: $name)" } else { '' })) } } 'unknown' { return @{ Title = 'Game data upload'; Form = $false; Check = $true Text = 'Versefiles cannot be reached right now - game data needs it.' } } default { return @{ Title = 'Your Versefiles key'; Form = $true; Check = $false Text = 'Game data goes to Versefiles with your companion key - the same one the journal uses. Paste it here; a Versefiles admin then allows game data for it.' } } } } # A key from the Settings dialog or the Game data tab: what the site said # about the old one - its name, its right, its builds - no longer holds. function Set-CompanionKey([string]$key) { $script:Settings.key = $key $script:Account.Name = $null; $script:Account.Status = 'none'; $script:Account.GameData = $null $script:GameDataState.Site = @{} $script:GameDataState.NextCheck = Get-Date $script:UploadState.Status = 'off' Save-Settings $script:Settings } # Sends everything not sent yet. Returns the number of events sent; the # outcome stands in $script:UploadState for the window. function Send-Journal([scriptblock]$log) { $st = $script:UploadState if (-not $script:Settings.upload -or -not $script:Settings.key) { $st.Status = 'off'; $st.Message = 'no key' return 0 } $url = ([string]$script:Settings.apiBase).TrimEnd('/') + '/api/v1/companion/events' $headers = @{ Authorization = 'Bearer ' + $script:Settings.key } $client = '{"name":"VersefilesCompanion","version":"' + $COMPANION_VERSION + '"}' $sent = 0 foreach ($meta in @($script:Index.Values | Sort-Object { $_.started })) { # Live progress only: HOTFIX runs against the game's live database, # PTU and the Tech Preview do not. Their sessions stay in the local # journal and are never sent (the server would refuse them anyway). if ($LIVE_CHANNELS -notcontains [string]$meta.channel) { continue } $file = Get-SessionFile $meta.id if (-not (Test-Path $file)) { continue } $lines = [System.IO.File]::ReadAllLines($file, $UTF8) $done = [int]$meta.uploaded while ($done -lt $lines.Count) { $n = [Math]::Min($UPLOAD_BATCH, $lines.Count - $done) # The journal lines ARE the events - joined, not parsed and # rebuilt, so what goes out is exactly what is on disk. $events = '[' + (($lines[$done..($done + $n - 1)]) -join ',') + ']' $session = ConvertTo-Json -Compress -InputObject ([ordered]@{ id = $meta.id; channel = $meta.channel; build = $meta.build; version = $meta.version environment = $meta.environment; shard = $meta.shard; started = $meta.started; ended = $meta.ended }) $body = '{"client":' + $client + ',"session":' + $session + ',"events":' + $events + '}' try { $answer = Invoke-RestMethod -Uri $url -Method Post -Headers $headers -TimeoutSec 60 ` -ContentType 'application/json; charset=utf-8' -Body ($UTF8.GetBytes($body)) } catch { $code = 0 if ($_.Exception.Response) { $code = [int]$_.Exception.Response.StatusCode } $st.Status = if ($code -eq 401) { 'rejected' } else { 'error' } $st.Message = if ($code -eq 401) { 'key rejected' } elseif ($code) { "server answered $code" } else { $_.Exception.Message } if ($log) { & $log ("Upload stopped: {0}" -f $st.Message) } Save-Index return $sent } $done += $n $meta.uploaded = $done $sent += $n if ($log -and $answer) { & $log ("Sent {0} events of session {1:yyyy-MM-dd HH:mm} ({2} new)." -f $n, $(try { [datetime]$meta.started } catch { $null }), $answer.stored) } } } if ($sent) { Save-Index } $st.Status = 'ok'; $st.Message = '' $st.Sent += $sent $st.Last = Get-Date return $sent } # =========================================================================== # Presentation # =========================================================================== # Works for both a LogEvent and an event read back from the journal. function Get-Field($e, [string]$name) { if ($e -is [Versefiles.Companion.LogEvent]) { return $e.Get($name) } return $e.$name } function Get-Kind($e) { if ($e -is [Versefiles.Companion.LogEvent]) { $e.Kind } else { $e.kind } } function Get-Time($e) { if ($e -is [Versefiles.Companion.LogEvent]) { return $e.Time.ToLocalTime() } return ([datetime]::Parse($e.time, [Globalization.CultureInfo]::InvariantCulture, [Globalization.DateTimeStyles]::RoundtripKind)).ToLocalTime() } function Format-Money($v) { if ($null -eq $v) { '?' } else { '{0:N0} aUEC' -f [double]$v } } function Format-Shop([string]$s) { if (-not $s) { return '' }; ($s -replace '^SCShop_', '') -replace '_', ' ' } # =========================================================================== # Home numbers (0.14.0) # =========================================================================== # # What the Home view shows (Alex, 13.10.2026): from the journal on this # machine, LIVE and HOTFIX sessions only - the companion's rule for every # evaluation. Nothing here draws; tests/gamelog.ps1 checks it. function ConvertTo-UtcTime($value) { if ($null -eq $value -or $value -eq '') { return $null } if ($value -is [datetime]) { return $value.ToUniversalTime() } return [datetime]::Parse([string]$value, [Globalization.CultureInfo]::InvariantCulture, [Globalization.DateTimeStyles]::AdjustToUniversal -bor [Globalization.DateTimeStyles]::AssumeUniversal) } # The patch each session belongs to: its own (from @env_session, since # 0.14.0), else that of a session of the same build, else "build:" - # a build never spans two patches. function Get-SessionPatchKeys($sessions) { $byBuild = @{} foreach ($s in $sessions) { if ($s.patch -and $s.build) { $byBuild[[string]$s.build] = [string]$s.patch } } $keys = @{} foreach ($s in $sessions) { $keys[[string]$s.id] = if ($s.patch) { [string]$s.patch } elseif ($byBuild.ContainsKey([string]$s.build)) { $byBuild[[string]$s.build] } else { 'build:' + [string]$s.build } } return $keys } # $range: 'patch' (the patch of the newest session), 'week' (the last 7 x 24 # hours, a session's time only as far as it reaches in) or 'all'. $now in # UTC; $runningId: the session going on right now, its time runs up to $now. function Get-DashboardStats($journal, [hashtable]$index, [string]$range, [datetime]$now, [string]$runningId) { $st = @{ Range = $range; PatchKey = $null; PatchChannel = $null; PatchBuild = $null Sessions = 0; Seconds = 0.0; Completed = 0; Failed = 0; Abandoned = 0; SuccessRate = $null; Earned = 0.0; Payouts = 0 Blueprints = 0; Places = 0; Jumps = 0; Deaths = 0; Medbed = 0; CrimeStat = 0 Refinery = 0; Sold = 0; SoldAmount = 0.0; Bought = 0; BoughtAmount = 0.0; LastSession = $null } $live = @($index.Values | Where-Object { $_.channel -in 'LIVE', 'HOTFIX' -and $_.started }) if (-not $live.Count) { return $st } # From start to end - the end the game wrote, the running session up to # now, a session the game never ended up to its last event. $open = @{} foreach ($s in $live) { if (-not $s.ended -and [string]$s.id -ne $runningId) { $open[[string]$s.id] = $null } } if ($open.Count) { foreach ($e in $journal) { $sid = [string]$e.Session if (-not $open.ContainsKey($sid)) { continue } $t = if ($e -is [Versefiles.Companion.LogEvent]) { $e.Time } else { ConvertTo-UtcTime $e.time } if ($null -eq $open[$sid] -or $t -gt $open[$sid]) { $open[$sid] = $t } } } $span = @{} foreach ($s in $live) { $id = [string]$s.id $from = ConvertTo-UtcTime $s.started $to = if ($id -eq $runningId) { $now } elseif ($s.ended) { ConvertTo-UtcTime $s.ended } elseif ($open[$id]) { $open[$id] } else { $from } $span[$id] = @{ From = $from; To = $to } } $newest = $live | Sort-Object { $span[[string]$_.id].From } | Select-Object -Last 1 $nid = [string]$newest.id $st.LastSession = @{ Started = $span[$nid].From; Seconds = ($span[$nid].To - $span[$nid].From).TotalSeconds; Channel = [string]$newest.channel } $since = $null switch ($range) { 'patch' { $keys = Get-SessionPatchKeys $live $st.PatchKey = $keys[$nid]; $st.PatchChannel = [string]$newest.channel; $st.PatchBuild = [string]$newest.build $chosen = @($live | Where-Object { $keys[[string]$_.id] -eq $st.PatchKey }) } 'week' { $since = $now.AddDays(-7) $chosen = @($live | Where-Object { $span[[string]$_.id].To -gt $since }) } default { $chosen = $live } } $in = @{} foreach ($s in $chosen) { $id = [string]$s.id $in[$id] = $true $from = $span[$id].From if ($since -and $from -lt $since) { $from = $since } $length = ($span[$id].To - $from).TotalSeconds if ($length -gt 0) { $st.Seconds += $length } } $st.Sessions = $chosen.Count $places = @{} foreach ($e in $journal) { if (-not $in.ContainsKey([string]$e.Session)) { continue } if ($since) { $t = if ($e -is [Versefiles.Companion.LogEvent]) { $e.Time } else { ConvertTo-UtcTime $e.time } if ($t -lt $since) { continue } } switch ([string](Get-Kind $e)) { 'contract_completed' { $st.Completed++ } 'contract_failed' { $st.Failed++ } 'mission_ended' { if ((Get-Field $e 'completion') -eq 'Abandon') { $st.Abandoned++ } } 'reward_received' { $st.Payouts++; $st.Earned += [double](Get-Field $e 'amount') } 'blueprint_received' { $st.Blueprints++ } 'location' { $place = Get-Field $e 'location'; if ($place) { $places[[string]$place] = $true } } 'quantum_arrived' { $st.Jumps++ } 'died' { $st.Deaths++ } 'medbed_treatment' { $st.Medbed++ } 'crimestat_changed' { $st.CrimeStat++ } 'refinery_order_sent' { $st.Refinery++ } 'commodity_sold' { if ((Get-Field $e 'status') -ne 'failed') { $st.Sold++; $st.SoldAmount += [double](Get-Field $e 'amount') } } 'item_bought' { if ((Get-Field $e 'status') -ne 'failed') { $st.Bought++; $st.BoughtAmount += [double](Get-Field $e 'price') } } } } $st.Places = $places.Count $ended = $st.Completed + $st.Failed + $st.Abandoned if ($ended -gt 0) { $st.SuccessRate = $st.Completed / $ended } return $st } # "Alpha 4.10.2" from the channel's build_manifest.id - if that build is the # one installed there. $null otherwise; the view says "This patch" then. function Get-PatchLabel([string]$gameRoot, [string]$channel, [string]$build) { if (-not $gameRoot -or -not $channel) { return $null } $file = [IO.Path]::Combine($gameRoot, $channel, 'build_manifest.id') if (-not (Test-Path -LiteralPath $file)) { return $null } try { $data = (Get-Content -LiteralPath $file -Raw -Encoding UTF8 | ConvertFrom-Json).Data if ([string]$data.RequestedP4ChangeNum -ne $build) { return $null } if ([string]$data.Branch -match '^sc-alpha-(.+)$') { return 'Alpha ' + $Matches[1] } } catch { } return $null } # The game writes a payout to its log only now and then - none at all since # Alpha 4.10.1 in a journal of 176 contracts (13.10.2026). Without a single # payout in the range the sum is unknown, not 0. function Format-Earned($stats) { if (-not $stats.Payouts) { return [string][char]0x2013 } # A million and more does not fit the tile digit by digit (0.14.1). if ([double]$stats.Earned -ge 1000000) { return '{0:0.#} M' -f ([double]$stats.Earned / 1000000) } return '{0:N0}' -f [double]$stats.Earned } function Format-Duration([double]$seconds) { $minutes = [int][Math]::Floor($seconds / 60) if ($minutes -lt 60) { return '{0} min' -f $minutes } return '{0} h {1} min' -f [Math]::Floor($minutes / 60), ($minutes % 60) } # The tile has room for "9 h 59 min" and no more (0.14.1): from 10 hours on # it says whole hours - "85 h"; the "Last session" line keeps the minutes. function Format-TileDuration([double]$seconds) { if ($seconds -lt 36000) { return Format-Duration $seconds } return '{0:N0} h' -f [Math]::Floor($seconds / 3600) } # =========================================================================== # Home look (0.15.0) # =========================================================================== # # The Home view in the style of the RSI Launcher (Alex, 13.10.2026), with a # fankit wallpaper inside the .cmd: only scaled, never cropped, its Star # Citizen mark bottom right left free, and the fankit notice beside it # (fankit agreement of 19.11.2025, 2f/2g/2j). Spec: # docs/superpowers/specs/2026-10-14-companion-launcher-look-design.md # Filled by tools\baue-werkzeug.ps1 with the wallpaper in Base64; empty in the # .ps1, which reads tools\companion-wallpaper-1280.jpg beside it instead. $HOME_WALLPAPER_DATA = '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' $FANKIT_NOTICE = 'This site is not endorsed by or affiliated with the Cloud Imperium or Roberts Space Industries group of companies. ' + 'All game content and materials are copyright Cloud Imperium Rights LLC and Cloud Imperium Rights Ltd.. Star Citizen' + [char]0x00AE + ', Squadron 42' + [char]0x00AE + ', Roberts Space Industries' + [char]0x00AE + ', and Cloud Imperium' + [char]0x00AE + ' are registered trademarks of Cloud Imperium Rights LLC. All rights reserved.' # The whole image in the area, centred, aspect kept - margins rather than a # crop (fankit agreement 2f/2j). function Get-FitRectangle([int]$imageWidth, [int]$imageHeight, [int]$areaWidth, [int]$areaHeight) { if ($imageWidth -le 0 -or $imageHeight -le 0 -or $areaWidth -le 0 -or $areaHeight -le 0) { return New-Object System.Drawing.Rectangle(0, 0, 0, 0) } $scale = [Math]::Min($areaWidth / $imageWidth, $areaHeight / $imageHeight) # Rounded, not floored: 720 * (300/720) is 299.99999... in floating point. $w = [int][Math]::Round($imageWidth * $scale) $h = [int][Math]::Round($imageHeight * $scale) return New-Object System.Drawing.Rectangle([int][Math]::Floor(($areaWidth - $w) / 2), [int][Math]::Floor(($areaHeight - $h) / 2), $w, $h) } # The wallpaper's Star Citizen mark sits in its bottom right corner (about # 70 x 85 of 1920 x 1080); 6 % x 12 % of the image keep it and a margin free. function Get-FreeCorner([System.Drawing.Rectangle]$imageRect) { return New-Object System.Drawing.Size([int][Math]::Ceiling($imageRect.Width * 0.06), [int][Math]::Ceiling($imageRect.Height * 0.12)) } # "LIVE · 4.10.2-live.12913470" as the launcher writes it, from the channel's # build_manifest.id - if that build is the one installed there (no $build: # whatever is installed - what START GAME would run). Otherwise the channel # alone; no session at all: a dash. function Get-EnvironmentText([string]$gameRoot, [string]$channel, [string]$build) { if (-not $channel) { return [string][char]0x2013 } if (-not $gameRoot) { return $channel } $file = [IO.Path]::Combine($gameRoot, $channel, 'build_manifest.id') if (-not (Test-Path -LiteralPath $file)) { return $channel } try { $data = (Get-Content -LiteralPath $file -Raw -Encoding UTF8 | ConvertFrom-Json).Data if (-not $build) { $build = [string]$data.RequestedP4ChangeNum } if ([string]$data.RequestedP4ChangeNum -ne $build) { return $channel } if ([string]$data.Branch -notmatch '^sc-alpha-(.+)$') { return $channel } $version = $Matches[1] $label = if ($version -match '-hotfix$') { $version } else { '{0}-{1}' -f $version, $channel.ToLowerInvariant() } return '{0} {1} {2}.{3}' -f $channel, [char]0x00B7, $label, $build } catch { return $channel } } function Get-HomeFontName([string[]]$installed, [string]$wanted, [string]$fallback) { if ($installed -contains $wanted) { return $wanted } return $fallback } # The launcher's letter-spaced capitals; WinForms has no letter spacing, a # hair space between the letters does it. function Format-Spaced([string]$text) { return (($text.ToUpperInvariant().ToCharArray() | ForEach-Object { [string]$_ }) -join [string][char]0x200A) } # The wallpaper's bytes: from the .cmd ($HOME_WALLPAPER_DATA), or - running # as .ps1 - from the file beside it. $null if neither works: Home then shows # its starfield. function Get-WallpaperBytes([string]$embedded, [string]$scriptPath) { if ($embedded) { try { return ,[Convert]::FromBase64String($embedded) } catch { return $null } } if ($scriptPath) { $file = Join-Path (Split-Path -Parent $scriptPath) 'companion-wallpaper-1280.jpg' if (Test-Path -LiteralPath $file) { return ,[IO.File]::ReadAllBytes($file) } } return $null } function New-HomeWallpaper($bytes) { if (-not $bytes -or $bytes.Length -lt 16) { return $null } try { $stream = New-Object System.IO.MemoryStream(,[byte[]]$bytes) # The stream stays open: GDI+ reads from it as long as the image lives. return [System.Drawing.Image]::FromStream($stream) } catch { return $null } } # Kinds shown only with "Show everything": frequent and not very telling. $QUIET_KINDS = @('armistice_entered', 'armistice_left', 'monitored_space_entered', 'monitored_space_left', 'private_property_entered', 'private_property_left', 'jurisdiction_entered', 'location', 'piloting_ended', 'mission_contract', 'objective_added', 'objective_completed', 'objective_withdrawn', 'hangar_request_completed', 'hangar_assigned', 'quantum_arrived', 'server_joined', 'ship_channel_left', 'journal_entry') function Get-EventText($e) { $f = { param($n) Get-Field $e $n } $from = { $t = & $f 'mission_title'; if ($t) { " - from $t" } else { '' } } switch (Get-Kind $e) { 'session_started' { "Game started ({0} {1})" -f (& $f 'channel'), (& $f 'version') } 'session_ended' { 'Game closed' } 'server_joined' { "Joined server {0}" -f (& $f 'shard') } 'contract_accepted' { "Contract accepted: {0}" -f (& $f 'title') } 'contract_completed' { "Contract completed: {0}" -f (& $f 'title') } 'contract_failed' { "Contract failed: {0}" -f (& $f 'title') } 'contract_shared' { "Contract shared: {0}" -f (& $f 'title') } 'mission_ended' { if ((& $f 'completion') -eq 'Abandon') { 'Contract abandoned' + $(if (& $f 'contract') { " ({0})" -f (& $f 'contract') }) } else { $null } } 'mission_contract' { "Contract key: {0}" -f (& $f 'contract') } 'objective_added' { "Objective: {0}" -f (& $f 'objective') } 'objective_completed' { "Objective done: {0}" -f (& $f 'objective') } 'objective_withdrawn' { "Objective withdrawn: {0}" -f (& $f 'objective') } 'blueprint_received' { "Blueprint received: {0}{1}" -f (& $f 'name'), (& $from) } 'reward_received' { "Reward: {0}{1}" -f (Format-Money (& $f 'amount')), (& $from) } 'journal_entry' { "Journal entry: {0}" -f (& $f 'title') } 'commodity_sold' { if ((& $f 'status') -eq 'failed') { "Sale rejected ({0})" -f (& $f 'error') } else { "Sold {0} unit(s) for {1} at {2}" -f (& $f 'quantity'), (Format-Money (& $f 'amount')), (Format-Shop (& $f 'shop')) } } 'item_bought' { "Bought {0} for {1}{2}" -f (& $f 'item_class'), (Format-Money (& $f 'price')), $(if ((& $f 'status') -ne 'ok') { " ({0})" -f (& $f 'status') }) } 'item_rented' { "Rented {0} for {1}" -f (& $f 'item_class'), (Format-Money (& $f 'price')) } 'refinery_order_sent' { "Refinery order placed ({0})" -f (Format-Shop (& $f 'shop')) } 'refinery_orders_completed' { "{0} refinery order(s) completed at {1}" -f (& $f 'count'), (& $f 'location') } 'quantum_target' { "Quantum target: {0}" -f ((& $f 'destination') -replace '_\d{6,}$', '') } 'quantum_arrived' { 'Quantum travel arrived' } 'location' { "At {0}" -f (& $f 'location') } 'jurisdiction_entered' { "Entered {0} jurisdiction" -f (& $f 'jurisdiction') } 'armistice_entered' { 'Entered armistice zone' } 'armistice_left' { 'Left armistice zone' } 'monitored_space_entered' { 'Entered monitored space' } 'monitored_space_left' { 'Left monitored space' } 'private_property_entered' { 'Entered private property' } 'private_property_left' { 'Left private property' } 'hangar_request_completed' { 'Hangar request completed' } # the name up to 0.3.0, still in older journals 'hangar_assigned' { 'Hangar request completed' } 'ship_channel_joined' { "Aboard {0}{1}" -f (& $f 'ship'), $(if (& $f 'own_ship') { '' } else { " (someone else's)" }) } 'ship_channel_left' { "Left {0}" -f (& $f 'ship') } 'piloting_ended' { "Left the pilot seat ({0})" -f (& $f 'vehicle_class') } 'medbed_treatment' { "Med bed treatment: {0}" -f ((@(& $f 'limbs') -join ', ') -replace '_', ' ') } 'died' { 'Died' } 'fatal_collision' { "Fatal collision ({0})" -f (& $f 'vehicle_class') } 'crimestat_changed' { "CrimeStat {0}" -f (& $f 'change') } default { Get-Kind $e } } } function Write-Summary($journal) { $done = @($script:Index.Values | Where-Object { $_.done }).Count Write-Host ("Sessions: {0} ({1} finished) Events: {2}" -f $script:Index.Count, $done, $journal.Count) Write-Host '' $journal | Group-Object kind | Sort-Object Count -Descending | ForEach-Object { Write-Host (" {0,6} {1}" -f $_.Count, $_.Name) } $bp = @($journal | Where-Object kind -eq 'blueprint_received') if ($bp.Count) { Write-Host '' Write-Host ("Blueprints ({0}, {1} attributed to a contract), latest:" -f $bp.Count, @($bp | Where-Object attribution -eq 'contract_completed').Count) foreach ($e in @($bp | Select-Object -Last 10)) { Write-Host (" {0:dd.MM. HH:mm} {1}" -f (Get-Time $e), (Get-EventText $e)) } } } # >>> selfupdate.ps1 - embedded verbatim; edit tools/selfupdate.ps1 and copy it here # =========================================================================== # Self-update # =========================================================================== # # The Versefiles Companion ships as ONE .cmd file, so this block is # embedded verbatim in VersefilesCompanion.ps1 between the # ">>> selfupdate.ps1" and "<<< selfupdate.ps1" lines; # tools/tests/selfupdate-embedded.js keeps both copies equal, and # tools/baue-werkzeug.ps1 refuses to build a tool whose copy has drifted. # (The upload tool carried it too until it became part of the companion in # 0.8.0; its last version, a note, has none - nothing left to update.) # # How an update gets in (Alex, 27.09.2026: automatic, never in the middle of # a game, signed): # 1. GET /api/v1/tools names each tool's current version, the path # of its file and the file's SHA-256. # 2. Newer than the running one: the file goes to # %LOCALAPPDATA%\Versefiles\Updates and is kept only if # - its SHA-256 is the one the server named (a broken download), # - its signature matches $UPDATE_PUBLIC_KEY - a file that did not # come from us is refused even if our own server hands it out, # should that ever be taken over: the private key is not on it, # - the file itself carries the announced version, and that version # is newer than the running one (no replay of an older signed file), # - its PowerShell part parses. # 3. The tool picks the moment - the companion waits until the game is # closed - and calls Start-UpdateInstall. The running .cmd cannot be # replaced while cmd.exe still reads it: cmd goes back to the file once # PowerShell ends. So a hidden helper waits for that cmd.exe to finish, # keeps the old file as .bak, copies the new one in and, if asked, # starts it. It leaves a result file the next start reads. # # THE SIGNATURE is the last line of the .cmd, "#vf-signature:": a # comment for PowerShell, never reached by cmd (the header exits before). It # is RSA with SHA-256 over everything before that line, with line endings # as LF and one final LF - git checks *.cmd out with CRLF and the build # writes a mix, so the bytes differ where the text does not. The private # key lives on the maintainer's PC only (tools/baue-werkzeug.ps1). # # Never for a copy that runs as .ps1 or sits inside a git working tree: a # developer starting the repository's own .cmd would otherwise overwrite # the file in the repository. $UPDATE_PUBLIC_KEY = 'xfYM1Jhtra5wfq7OpDb89X++z/TnfuML0JMNgSoI4dLV/FpMr8lv4s84f3abPBsBiWlsA0nhsya555C/U52GBZStJXUSjsWLCthHtMbrKwuawRqZ0AavSXxz/OpVzGnI9FHspY7y8dhNEjmHGOnHkIvyOrKjmy4my3S/IxxCKzjDvGiGlMm4AYJ4UjCIFouLVcfmCFARWITZiaw2qH2JPT4ffYgy1sHORq+R2TW6r51qEaLggCWjJUApRHnnVgih5SZTmkXRqQfYnH9Z0JaPmlWymw7h82deUt7SJf/mzCItMSNMXrZYwnR+ox5+PdMyHy2ARQ/qK1rlKyoT8hcETp/YOIpgyRaXVLj0rfEkNaqa4EyHlIRFsFx+ZgB0oOFyPAeKqIXvPWEsfuxLOQuiOu5bqpPWoEyxbImlaPLSd3SJDmzA10scMZyKYr6n0E7rIKR/xV1EuvTgb5hyuMNZq5i+gPaSQl5XQOSj2s5DvYhY78Nv02waYTyc9G+eqKFlAQAB' $UPDATE_SIGNATURE_PREFIX = '#vf-signature:' # The version line of both tools - the server reads it with the same pattern # (App\Http\Controllers\ToolReleaseController::VERSION_PATTERN). $UPDATE_VERSION_PATTERN = '(?m)^\$(?:COMPANION|TOOL)_VERSION = ''([0-9]+(?:\.[0-9]+){1,3})''' $UPDATE_DIR = Join-Path $env:LOCALAPPDATA 'Versefiles\Updates' # The text a signature covers, and the signature itself (or $null). function Split-UpdateSignature([string]$text) { $t = $text.TrimStart([char]0xFEFF).Replace("`r`n", "`n").TrimEnd("`n") $signature = $null $cut = $t.LastIndexOf("`n") $last = if ($cut -ge 0) { $t.Substring($cut + 1) } else { $t } if ($last.StartsWith($UPDATE_SIGNATURE_PREFIX)) { $signature = $last.Substring($UPDATE_SIGNATURE_PREFIX.Length).Trim() $t = if ($cut -ge 0) { $t.Substring(0, $cut).TrimEnd("`n") } else { '' } } return @{ Canonical = $t + "`n"; Signature = $signature } } function Test-UpdateSignature([string]$text, [string]$publicKeyXml = $UPDATE_PUBLIC_KEY) { $parts = Split-UpdateSignature $text if (-not $parts.Signature) { return $false } try { $signature = [Convert]::FromBase64String($parts.Signature) } catch { return $false } $rsa = New-Object System.Security.Cryptography.RSACryptoServiceProvider try { $rsa.PersistKeyInCsp = $false $rsa.FromXmlString($publicKeyXml) $bytes = (New-Object System.Text.UTF8Encoding($false)).GetBytes($parts.Canonical) return [bool]$rsa.VerifyData($bytes, 'SHA256', $signature) } catch { return $false } finally { $rsa.Dispose() } } # Only the build signs (tools/baue-werkzeug.ps1), with the private key that is # on no user's PC; the function ships along so that build, tests and the # check above share one reading of what is signed. Returns the signed text. function Add-UpdateSignature([string]$text, [string]$privateKeyXml) { $parts = Split-UpdateSignature $text if ($parts.Signature) { throw 'the text is signed already' } $rsa = New-Object System.Security.Cryptography.RSACryptoServiceProvider try { $rsa.PersistKeyInCsp = $false $rsa.FromXmlString($privateKeyXml) $signature = $rsa.SignData((New-Object System.Text.UTF8Encoding($false)).GetBytes($parts.Canonical), 'SHA256') } finally { $rsa.Dispose() } $body = if ($text.EndsWith("`n")) { $text } else { $text + "`r`n" } return $body + $UPDATE_SIGNATURE_PREFIX + [Convert]::ToBase64String($signature) + "`r`n" } function Get-UpdateVersionOf([string]$text) { $m = [regex]::Match($text, $UPDATE_VERSION_PATTERN) if ($m.Success) { return $m.Groups[1].Value } return $null } function Test-UpdateNewer([string]$candidate, [string]$current) { try { return [version]$candidate -gt [version]$current } catch { return $false } } # The first syntax error of the PowerShell part, or $null. function Test-UpdateParses([string]$text) { $marker = [char]35 + '@PSSTART' $i = $text.IndexOf($marker) if ($i -lt 0) { return 'no PowerShell part' } $errors = $null $null = [System.Management.Automation.Language.Parser]::ParseInput($text.Substring($i + $marker.Length), [ref]$null, [ref]$errors) if ($errors -and $errors.Count) { return ('line {0}: {1}' -f $errors[0].Extent.StartLineNumber, $errors[0].Message) } return $null } # The .cmd this runs from, if it may replace itself; $null otherwise. function Get-UpdateSelf { $self = [string]$env:QG_SELF if (-not $self -or -not $self.EndsWith('.cmd', [StringComparison]::OrdinalIgnoreCase)) { return $null } if (-not (Test-Path -LiteralPath $self -PathType Leaf)) { return $null } $dir = Split-Path -Parent $self for ($i = 0; $i -lt 8 -and $dir; $i++) { if (Test-Path -LiteralPath (Join-Path $dir '.git')) { return $null } $dir = Split-Path -Parent $dir } return $self } # What the server offers for this tool, if it is newer: @{ Version; Url; # Sha256; Channel }. On the beta channel (companion 0.13.0) the beta entry # "-beta" counts too and the newer of the two wins - a fix that went # to everybody at once reaches the testers as well; with the same version in # both, the stable file. Everything after the choice is the same for both. function Get-UpdateOffer([string]$apiBase, [string]$toolId, [string]$currentVersion, [string]$userAgent, [bool]$beta = $false) { $base = $apiBase.TrimEnd('/') $info = Invoke-RestMethod -Uri ($base + '/api/v1/tools') -TimeoutSec 15 -UserAgent $userAgent $entries = @(@{ Name = $toolId; Channel = 'stable' }) if ($beta) { $entries += @{ Name = $toolId + '-beta'; Channel = 'beta' } } $best = $null foreach ($entry in $entries) { $t = $info.($entry.Name) if (-not $t -or -not $t.version -or -not $t.path -or -not $t.sha256) { continue } if (-not (Test-UpdateNewer ([string]$t.version) $currentVersion)) { continue } if ($best -and -not (Test-UpdateNewer ([string]$t.version) $best.Version)) { continue } $best = @{ Version = [string]$t.version; Url = $base + [string]$t.path Sha256 = ([string]$t.sha256).ToLowerInvariant(); Channel = $entry.Channel } } return $best } # Downloads an offer and keeps it only if every check passes. Returns the # path of the verified file; throws with the reason otherwise. function Save-UpdateFile($offer, [string]$toolId, [string]$currentVersion, [string]$userAgent) { if (-not (Test-Path -LiteralPath $UPDATE_DIR)) { New-Item -ItemType Directory -Path $UPDATE_DIR -Force | Out-Null } $file = Join-Path $UPDATE_DIR ('{0}-{1}.cmd' -f $toolId, $offer.Version) $part = $file + '.part' try { Invoke-WebRequest -Uri $offer.Url -OutFile $part -TimeoutSec 120 -UseBasicParsing -UserAgent $userAgent $sha = (Get-FileHash -LiteralPath $part -Algorithm SHA256).Hash.ToLowerInvariant() if ($sha -ne $offer.Sha256) { throw 'the download does not match the checksum the server named' } $text = [System.IO.File]::ReadAllText($part, (New-Object System.Text.UTF8Encoding($false))) if (-not (Test-UpdateSignature $text)) { throw 'the signature does not match - not installed' } $version = Get-UpdateVersionOf $text if ($version -ne $offer.Version) { throw ('the file says version {0}, the server {1}' -f $version, $offer.Version) } if (-not (Test-UpdateNewer $version $currentVersion)) { throw ('version {0} is not newer than {1}' -f $version, $currentVersion) } $syntax = Test-UpdateParses $text if ($syntax) { throw ('the new version does not parse ({0})' -f $syntax) } Move-Item -LiteralPath $part -Destination $file -Force return $file } finally { if (Test-Path -LiteralPath $part) { Remove-Item -LiteralPath $part -Force -ErrorAction SilentlyContinue } } } # The cmd.exe that runs this tool's .cmd - cmd reads the file again once # PowerShell ends, so it must not be replaced or removed before - or this # process if it was not started from one. function Get-UpdateWaitPid { try { $parent = (Get-CimInstance Win32_Process -Filter ('ProcessId = {0}' -f $PID)).ParentProcessId $p = Get-Process -Id $parent -ErrorAction SilentlyContinue if ($p -and $p.ProcessName -eq 'cmd') { return [int]$parent } } catch { } return $PID } # What conhost.exe needs to run $cmdFile (with $arguments) without a window # (companion 0.13.1): --headless gives cmd and PowerShell a console that # nothing shows, not even Windows Terminal. The way the header starts a .cmd # without arguments (tools/baue-werkzeug.ps1), and so start the shortcuts, # the switch to the installed copy and the restart after an update. conhost # splits its command line and quotes the parts anew - a quoted path with an # ampersand or a bracket would not reach cmd as one - so conhost has to be # started IN the file's folder, and cmd gets "call" and the bare file name. function Get-HiddenStartArguments([string]$cmdFile, [string]$arguments = '') { $tail = if ($arguments) { ' ' + $arguments } else { '' } return '--headless "{0}" /d /c call ".\{1}"{2}' -f (Join-Path $env:SystemRoot 'System32\cmd.exe'), (Split-Path -Leaf $cmdFile), $tail } # Hands the swap to a hidden helper and returns; the caller then ends. The # helper waits for the cmd.exe that runs this tool (or $waitPid, for tests), # replaces the .cmd, and starts it again if $relaunch - without a window. function Start-UpdateInstall([string]$newFile, [string]$toolFile, [string]$toolId, [string]$fromVersion, [string]$toVersion, [bool]$relaunch, [string]$arguments, [int]$waitPid = 0) { if (-not $waitPid) { $waitPid = Get-UpdateWaitPid } if (-not (Test-Path -LiteralPath $UPDATE_DIR)) { New-Item -ItemType Directory -Path $UPDATE_DIR -Force | Out-Null } $utf8 = New-Object System.Text.UTF8Encoding($false) $job = Join-Path $UPDATE_DIR ('install-{0}.json' -f $toolId) $helper = Join-Path $UPDATE_DIR 'install.ps1' [System.IO.File]::WriteAllText($job, (ConvertTo-Json -InputObject ([ordered]@{ tool = $toolId; from = $fromVersion; to = $toVersion; waitPid = $waitPid newFile = $newFile; toolFile = $toolFile; relaunch = $relaunch; arguments = [string]$arguments hiddenStart = (Get-HiddenStartArguments $toolFile $arguments) resultFile = (Join-Path $UPDATE_DIR ('{0}-result.json' -f $toolId)) })), $utf8) [System.IO.File]::WriteAllText($helper, @' param([string]$Job) $ErrorActionPreference = 'Stop' $j = Get-Content -LiteralPath $Job -Raw -Encoding UTF8 | ConvertFrom-Json $result = [ordered]@{ tool = $j.tool; from = $j.from; to = $j.to; ok = $false; message = ''; at = (Get-Date).ToString('o') } try { # cmd.exe reads its .cmd to the end once PowerShell is done - replacing # the file before that would feed it the wrong lines. $deadline = (Get-Date).AddMinutes(2) while ((Get-Process -Id $j.waitPid -ErrorAction SilentlyContinue) -and (Get-Date) -lt $deadline) { Start-Sleep -Milliseconds 250 } if (Get-Process -Id $j.waitPid -ErrorAction SilentlyContinue) { throw 'the running version did not end within two minutes' } Copy-Item -LiteralPath $j.toolFile -Destination ($j.toolFile + '.bak') -Force $copied = $false for ($i = 0; $i -lt 20 -and -not $copied; $i++) { try { Copy-Item -LiteralPath $j.newFile -Destination $j.toolFile -Force; $copied = $true } catch { Start-Sleep -Milliseconds 500 } } if (-not $copied) { throw ('could not write {0}' -f $j.toolFile) } Remove-Item -LiteralPath $j.newFile -Force -ErrorAction SilentlyContinue $result.ok = $true } catch { $result.message = $_.Exception.Message } [System.IO.File]::WriteAllText($j.resultFile, (ConvertTo-Json -InputObject $result), (New-Object System.Text.UTF8Encoding($false))) Remove-Item -LiteralPath $Job -Force -ErrorAction SilentlyContinue # Start it again: the new version, or the old one if the swap failed - # without a window, and QG_HIDDEN tells it so. Without conhost as before. if ($j.relaunch) { $conhost = Join-Path $env:SystemRoot 'System32\conhost.exe' $start = @{ FilePath = $j.toolFile; WorkingDirectory = (Split-Path -Parent $j.toolFile) } if ($j.arguments) { $start.ArgumentList = $j.arguments } if ((Test-Path -LiteralPath $conhost) -and $j.hiddenStart) { $env:QG_HIDDEN = '1' $start.FilePath = $conhost $start.ArgumentList = $j.hiddenStart } Start-Process @start } '@, $utf8) Start-Process -FilePath 'powershell.exe' -WindowStyle Hidden -ArgumentList @( '-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', ('"{0}"' -f $helper), '-Job', ('"{0}"' -f $job)) } # What the last install left behind, once: @{ ok; from; to; message } or $null. function Read-UpdateResult([string]$toolId) { $file = Join-Path $UPDATE_DIR ('{0}-result.json' -f $toolId) if (-not (Test-Path -LiteralPath $file)) { return $null } try { return Get-Content -LiteralPath $file -Raw -Encoding UTF8 | ConvertFrom-Json } catch { return $null } finally { Remove-Item -LiteralPath $file -Force -ErrorAction SilentlyContinue } } # ---- The installed copy (companion 0.13.1) --------------------------------- # # A .cmd cannot carry an icon, and a downloaded file sits wherever the # browser put it. So a downloaded copy installs itself (Alex, 05.10.2026): # started without arguments from anywhere but $INSTALL_DIR, it copies # itself there, puts shortcuts with the companion's icon on the desktop and # into the Start menu, starts the installed copy without a window and has a # helper remove the started file once its cmd.exe has ended. By the rules # of the self-update: only a file signed with the release key, never an # older version over a newer one, never a .ps1 or a copy inside a git # working tree (Get-UpdateSelf gives none), never a file that is not ours. $INSTALL_DIR = Join-Path $env:LOCALAPPDATA 'Versefiles\Companion' $INSTALL_NAME = 'VersefilesCompanion' $SHORTCUT_NAME = 'Versefiles Companion.lnk' # The companion's own name for Windows (0.13.2, Alex 07.10.2026): it runs in # powershell.exe, so the taskbar showed it as "Windows PowerShell" with that # icon. The window process and the shortcuts carry this AppUserModelID, and # the taskbar shows "Versefiles Companion" with the shortcut's icon instead. $APP_ID = 'Versefiles.Companion' # Writes System.AppUserModel.ID into a shortcut - WScript.Shell cannot. $SHORTCUT_APP_ID_SOURCE = @' using System; using System.Runtime.InteropServices; namespace Versefiles.Shell { public static class ShortcutAppId { [ComImport, Guid("00021401-0000-0000-C000-000000000046")] class ShellLink { } [ComImport, Guid("0000010b-0000-0000-C000-000000000046"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] interface IPersistFile { void GetClassID(out Guid classId); [PreserveSig] int IsDirty(); void Load([MarshalAs(UnmanagedType.LPWStr)] string fileName, uint mode); void Save([MarshalAs(UnmanagedType.LPWStr)] string fileName, [MarshalAs(UnmanagedType.Bool)] bool remember); void SaveCompleted([MarshalAs(UnmanagedType.LPWStr)] string fileName); void GetCurFile([MarshalAs(UnmanagedType.LPWStr)] out string fileName); } [StructLayout(LayoutKind.Sequential, Pack = 4)] struct PropertyKey { public Guid FormatId; public uint PropertyId; } [StructLayout(LayoutKind.Sequential)] struct PropVariant { public ushort Type; public ushort Reserved1, Reserved2, Reserved3; public IntPtr Value; public IntPtr Value2; } [ComImport, Guid("886D8EEB-8CF2-4446-8D02-CDBA1DBDCF99"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] interface IPropertyStore { void GetCount(out uint count); void GetAt(uint index, out PropertyKey key); void GetValue(ref PropertyKey key, out PropVariant value); void SetValue(ref PropertyKey key, ref PropVariant value); void Commit(); } [DllImport("ole32.dll")] static extern int PropVariantClear(ref PropVariant value); public static void Set(string shortcut, string appId) { IPersistFile file = (IPersistFile)new ShellLink(); file.Load(shortcut, 2); // STGM_READWRITE IPropertyStore store = (IPropertyStore)file; PropertyKey key = new PropertyKey(); key.FormatId = new Guid("9F4C2855-9F79-4B39-A8D0-E1D42DE1D5F3"); // PKEY_AppUserModel_ID key.PropertyId = 5; PropVariant value = new PropVariant(); value.Type = 31; // VT_LPWSTR value.Value = Marshal.StringToCoTaskMemUni(appId); try { store.SetValue(ref key, ref value); store.Commit(); } finally { PropVariantClear(ref value); } file.Save(shortcut, true); } } } '@ # The version of a tool file signed with the release key that parses - $null # for anything else: missing, unsigned, signed with another key, broken. function Get-SignedToolVersion([string]$file, [string]$publicKeyXml = $UPDATE_PUBLIC_KEY) { if (-not $file -or -not (Test-Path -LiteralPath $file -PathType Leaf)) { return $null } $text = [System.IO.File]::ReadAllText($file, (New-Object System.Text.UTF8Encoding($false))) if (-not (Test-UpdateSignature $text $publicKeyXml) -or (Test-UpdateParses $text)) { return $null } return Get-UpdateVersionOf $text } # What a start of $self (from Get-UpdateSelf: a downloaded .cmd, or nothing) # does about the copy in $installDir: # 'run' nothing - $self is that copy, may not install itself, or is not # signed with the release key # 'copy' $self goes there: no copy there that counts, or an older one # 'switch' the copy there is as new or newer and starts instead function Get-InstallStep([string]$self, [string]$installDir = $INSTALL_DIR, [string]$publicKeyXml = $UPDATE_PUBLIC_KEY) { if (-not $self) { return 'run' } $installed = Join-Path $installDir ($INSTALL_NAME + '.cmd') if ([string]::Equals([IO.Path]::GetFullPath($self), [IO.Path]::GetFullPath($installed), [StringComparison]::OrdinalIgnoreCase)) { return 'run' } $mine = Get-SignedToolVersion $self $publicKeyXml if (-not $mine) { return 'run' } $there = Get-SignedToolVersion $installed $publicKeyXml if (-not $there -or (Test-UpdateNewer $mine $there)) { return 'copy' } return 'switch' } # The app id a shortcut carries, or '' - read through the shell, so that a # start that changes nothing compiles nothing. function Get-ShortcutAppId([string]$shortcut) { $shell = New-Object -ComObject Shell.Application try { $item = $shell.NameSpace((Split-Path -Parent $shortcut)).ParseName((Split-Path -Leaf $shortcut)) if (-not $item) { return '' } return [string]$item.ExtendedProperty('System.AppUserModel.ID') } finally { [void][System.Runtime.InteropServices.Marshal]::ReleaseComObject($shell) } } function Set-ShortcutAppId([string]$shortcut, [string]$appId = $APP_ID) { if (-not ('Versefiles.Shell.ShortcutAppId' -as [type])) { Add-Type -TypeDefinition $SHORTCUT_APP_ID_SOURCE } [Versefiles.Shell.ShortcutAppId]::Set($shortcut, $appId) } # A shortcut that starts $cmdFile without a window, with the icon and the # companion's app id. function New-ToolShortcut([string]$shortcut, [string]$cmdFile, [string]$iconFile) { $shell = New-Object -ComObject WScript.Shell try { $link = $shell.CreateShortcut($shortcut) $link.TargetPath = Join-Path $env:SystemRoot 'System32\conhost.exe' $link.Arguments = Get-HiddenStartArguments $cmdFile $link.WorkingDirectory = Split-Path -Parent $cmdFile $link.IconLocation = $iconFile + ',0' $link.Description = "Versefiles Companion - follows Star Citizen's Game.log" $link.Save() } finally { [void][System.Runtime.InteropServices.Marshal]::ReleaseComObject($shell) } Set-ShortcutAppId $shortcut } # Shortcuts made by 0.13.1 lack the app id. Ours - conhost, started in # $installDir - get it once; a shortcut of the same name that is not ours # stays as it is, and none is made anew (whoever deletes one keeps it so). function Update-ToolShortcuts([string]$installDir = $INSTALL_DIR, [string[]]$shortcutDirs = @([Environment]::GetFolderPath('Desktop'), [Environment]::GetFolderPath('Programs'))) { $shell = New-Object -ComObject WScript.Shell try { foreach ($dir in $shortcutDirs) { if (-not $dir) { continue } $file = Join-Path $dir $SHORTCUT_NAME if (-not (Test-Path -LiteralPath $file) -or (Get-ShortcutAppId $file) -eq $APP_ID) { continue } $link = $shell.CreateShortcut($file) if ((Split-Path -Leaf $link.TargetPath) -ne 'conhost.exe' -or -not [string]::Equals($link.WorkingDirectory.TrimEnd('\'), $installDir.TrimEnd('\'), [StringComparison]::OrdinalIgnoreCase)) { continue } Set-ShortcutAppId $file } } finally { [void][System.Runtime.InteropServices.Marshal]::ReleaseComObject($shell) } } # Copies $self into $installDir for the step 'copy' - checked byte for byte, # a copy that was there kept as .bak - then writes the icon and the shortcuts # (for 'switch' too: whoever starts a downloaded copy installs). Returns the # installed file. Throws if something fails; the caller then runs on where # it is. function Install-ToolCopy([string]$self, [string]$step, [byte[]]$iconData, [string]$installDir = $INSTALL_DIR, [string[]]$shortcutDirs = @([Environment]::GetFolderPath('Desktop'), [Environment]::GetFolderPath('Programs'))) { if (-not (Test-Path -LiteralPath $installDir)) { New-Item -ItemType Directory -Path $installDir -Force | Out-Null } $installed = Join-Path $installDir ($INSTALL_NAME + '.cmd') if ($step -eq 'copy') { $part = $installed + '.part' Copy-Item -LiteralPath $self -Destination $part -Force if ((Get-FileHash -LiteralPath $part -Algorithm SHA256).Hash -ne (Get-FileHash -LiteralPath $self -Algorithm SHA256).Hash) { Remove-Item -LiteralPath $part -Force -ErrorAction SilentlyContinue throw ('the copy in {0} is not the same as {1}' -f $installDir, $self) } if (Test-Path -LiteralPath $installed) { Copy-Item -LiteralPath $installed -Destination ($installed + '.bak') -Force } Move-Item -LiteralPath $part -Destination $installed -Force } $icon = Join-Path $installDir ($INSTALL_NAME + '.ico') if ($iconData) { [System.IO.File]::WriteAllBytes($icon, $iconData) } foreach ($dir in $shortcutDirs) { if ($dir -and (Test-Path -LiteralPath $dir)) { New-ToolShortcut (Join-Path $dir $SHORTCUT_NAME) $installed $icon } } return $installed } # Starts $cmdFile without a window; QG_HIDDEN tells it so. function Start-ToolHidden([string]$cmdFile, [string]$arguments = '') { $env:QG_HIDDEN = '1' Start-Process -FilePath (Join-Path $env:SystemRoot 'System32\conhost.exe') -ArgumentList (Get-HiddenStartArguments $cmdFile $arguments) -WorkingDirectory (Split-Path -Parent $cmdFile) } # Whether $file is $installed under another name - a link, a short name. # $installed is held so that nobody else may open it; if $file then cannot be # opened either, it is the same file. Unsure counts as the same. function Test-SameFile([string]$file, [string]$installed) { if (-not $installed -or -not (Test-Path -LiteralPath $installed -PathType Leaf)) { return $false } $held = $null # A virus scanner may still be reading the copy it just saw being made. for ($i = 0; $i -lt 10 -and -not $held; $i++) { try { $held = [System.IO.File]::Open($installed, 'Open', 'Read', 'None') } catch { Start-Sleep -Milliseconds 200 } } if (-not $held) { return $true } try { ([System.IO.File]::Open($file, 'Open', 'Read', 'ReadWrite')).Dispose() return $false } catch { return $true } finally { $held.Dispose() } } # The started file and the .bak an update left next to it, as far as they are # ours: signed with the release key, and not the installed copy $installed # itself. Anything else is never touched. function Get-RemovableStartFiles([string]$self, [string]$installed, [string]$publicKeyXml = $UPDATE_PUBLIC_KEY) { foreach ($file in @($self, ($self + '.bak'))) { if (-not (Test-Path -LiteralPath $file -PathType Leaf) -or (Test-SameFile $file $installed)) { continue } if (Test-UpdateSignature ([System.IO.File]::ReadAllText($file, (New-Object System.Text.UTF8Encoding($false)))) $publicKeyXml) { $file } } } # Hands the removal to a hidden helper: it waits for the cmd.exe that runs the # started file (or $waitPid, for tests) and removes each file only if it is # still, byte for byte, the one checked here. function Start-StartFileRemoval([string[]]$files, [int]$waitPid = 0) { if (-not $waitPid) { $waitPid = Get-UpdateWaitPid } if (-not (Test-Path -LiteralPath $UPDATE_DIR)) { New-Item -ItemType Directory -Path $UPDATE_DIR -Force | Out-Null } $utf8 = New-Object System.Text.UTF8Encoding($false) $job = Join-Path $UPDATE_DIR ('remove-{0}.json' -f [guid]::NewGuid().ToString('N').Substring(0, 8)) $helper = Join-Path $UPDATE_DIR 'remove.ps1' $items = @(foreach ($f in $files) { [ordered]@{ path = $f; sha256 = (Get-FileHash -LiteralPath $f -Algorithm SHA256).Hash } }) [System.IO.File]::WriteAllText($job, (ConvertTo-Json -InputObject ([ordered]@{ waitPid = $waitPid; files = $items }) -Depth 4), $utf8) [System.IO.File]::WriteAllText($helper, @' param([string]$Job) $ErrorActionPreference = 'Stop' $j = Get-Content -LiteralPath $Job -Raw -Encoding UTF8 | ConvertFrom-Json # cmd.exe reads its .cmd until it ends; if it does not end, nothing goes. $deadline = (Get-Date).AddMinutes(2) while ((Get-Process -Id $j.waitPid -ErrorAction SilentlyContinue) -and (Get-Date) -lt $deadline) { Start-Sleep -Milliseconds 250 } if (-not (Get-Process -Id $j.waitPid -ErrorAction SilentlyContinue)) { foreach ($f in @($j.files)) { # A virus scanner may still hold a file it just looked at. for ($i = 0; $i -lt 10; $i++) { try { if ((Test-Path -LiteralPath $f.path) -and (Get-FileHash -LiteralPath $f.path -Algorithm SHA256).Hash -eq $f.sha256) { Remove-Item -LiteralPath $f.path -Force } break } catch { Start-Sleep -Milliseconds 500 } } } } Remove-Item -LiteralPath $Job -Force -ErrorAction SilentlyContinue '@, $utf8) Start-Process -FilePath 'powershell.exe' -WindowStyle Hidden -ArgumentList @( '-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', ('"{0}"' -f $helper), '-Job', ('"{0}"' -f $job)) } # <<< selfupdate.ps1 # =========================================================================== # Game data - the former QuestGuide Uploader (companion 0.8.0) # =========================================================================== # # Reads the installed build - the language file out of Data.p4k, the # contract broker out of Game2.dcb - and sends it to Versefiles with a # companion key an admin allowed it for (0.15.1; a key of its own before). Moved here from QuestGuideUploader.ps1 as # it stood at 1.0.0 (Alex, 28.09.2026: one program, "voll integriert"). # Unchanged but for what the companion has of its own: the settings, the # game folder, the window and the self-update. # # The window runs it as "-GameData -Channel " in a process of its # own: a run takes minutes and a lot of memory, and the journal and the # overlay go on meanwhile (see Start-GameDataRun). # # The names and comments below are still German; they turn English when # the code is next changed. The recognition block must not change without # $ERKENNUNG_VERSION - tests/extractor.ps1 holds its checksum. # =========================================================================== # Stand der Missionserkennung # =========================================================================== # # WOZU EINE NUMMER: Zwei Builds sind nur dann vergleichbar, wenn sie mit # DENSELBEN Regeln gelesen wurden. Sonst misst die Differenz nicht das # Spiel, sondern uns. # # Am 09.09.2026 stand auf der Startseite "New in build 12572603 - 1291 # missions the catalogue did not have before". Kein einziger war neu: Die # global.ini dieses Builds ist byte-identisch mit der des Vorgaengers. Neu # war die Erkennung. Die Seite kann das nur wissen, wenn es hier steht. # # ERHOEHEN, sobald sich an der Erkennung etwas aendert, das die Menge der # gefundenen Missionen verschieben kann. tests/extractor.ps1 vergleicht den # Erkennungsblock mit einer hinterlegten Pruefsumme und schlaegt an, wenn er # sich geaendert hat und diese Nummer stehengeblieben ist. # # 1 bis 08.09.2026 nur "_title" # 2 ab 09.09.2026 alle Schreibweisen, Stufen-Ausnahme, Fassungen # $ERKENNUNG_VERSION = '4' # The game folder the channels are looked for in (Finde-Kanaele) - set to the # companion's game folder before each use. $SC_ROOT = $DEFAULT_GAME_ROOT # The uploader's settings folder: Raeume-Altlasten clears what old versions # left there. Its config.json is read once more for the game folder # (Get-GameRoot). $CONF_DIR = Join-Path $env:APPDATA 'SCQuestGuide' # Work files do not belong in %APPDATA%: it travels with the profile in # managed networks, and an unpacked global.ini is 10 MB per channel. They go # to TEMP and are deleted after the run. $ARBEIT_DIR = Join-Path ([IO.Path]::GetTempPath()) 'SCQuestGuide' # The language: see $LOCALES below - the first one found is taken. # =========================================================================== # Quelltext des ZStandard-Entpackers # =========================================================================== # # Er steht hier im Klartext und wird beim ersten Bedarf uebersetzt. Eine # mitgelieferte DLL waere kuerzer, koennte aber niemand nachpruefen - und # genau das soll bei einem Werkzeug moeglich bleiben, das man sich aus dem # Netz herunterlaedt. $ZSTD_QUELLE = @' // --------------------------------------------------------------------------- // ZStandard-Entpacker (nur Entpacken, kein Packen) // // Warum eigener Code statt einer Bibliothek: Das Werkzeug wird als lesbares // Skript zum Herunterladen angeboten. Eine mitgelieferte DLL koennte niemand // nachpruefen, und das Windows-eigene bsdtar scheitert an CIGs ZIP-Zusatzfeld. // Hier steht alles im Klartext. // // Bewusst schlicht: keine Zeiger, kein Span, kein modernes C#. Uebersetzt // wird zur Laufzeit mit dem Compiler, der auf jedem Windows liegt // (Framework 4.0) - und der kennt nur C# 5. // // Umfang: Rohbloecke, Wiederholungsbloecke, komprimierte Bloecke, Huffman- // Literale (ein- und vierstroemig, mit und ohne neuen Baum), FSE-Sequenzen // einschliesslich der drei vordefinierten Tabellen. Woerterbuecher werden // NICHT unterstuetzt - die p4k benutzt keine. // // Nach RFC 8878. // --------------------------------------------------------------------------- using System; namespace QuestGuide { public class ZstdFehler : Exception { public ZstdFehler(string m) : base(m) { } } // ----------------------------------------------------------------------- // Bitleser, der RUECKWAERTS liest. // // Zstd schreibt seine Bitstroeme von hinten nach vorn: Das letzte Byte // kommt zuerst, innerhalb eines Bytes das hoechstwertige Bit zuerst. Im // letzten Byte steht eine 1 als Endmarkierung; alles darueber ist // Fuellung und wird uebersprungen. // // Bit fuer Bit statt mit einem 64-Bit-Fenster: langsamer, aber ohne die // Randfaelle, an denen sich solche Fenster gern verrechnen. Fuer eine // 10-MB-Datei bleibt es im Sekundenbereich. // ----------------------------------------------------------------------- internal class RueckBits { private byte[] q; private int start; private int len; private long bitPos; // gelesene Bits, vom Ende der Daten gezaehlt private long bitEnde; public RueckBits(byte[] quelle, int offset, int laenge) { if (laenge < 1) throw new ZstdFehler("leerer Bitstrom"); q = quelle; start = offset; len = laenge; byte letztes = q[start + len - 1]; if (letztes == 0) throw new ZstdFehler("Bitstrom ohne Endmarkierung"); bitEnde = (long)laenge * 8; // Fuellbits bis einschliesslich der Markierung ueberspringen bitPos = 8 - HoechstesBit(letztes); } internal static int HoechstesBit(uint v) { int n = -1; while (v != 0) { v >>= 1; n++; } return n; } private int BitBei(long i) { if (i >= bitEnde) return 0; // hinter dem Anfang: Nullen int byteIdx = start + len - 1 - (int)(i >> 3); int bitIdx = 7 - (int)(i & 7); return (q[byteIdx] >> bitIdx) & 1; } /// n Bits ansehen, ohne sie zu verbrauchen. public uint Sicht(int n) { uint v = 0; for (int i = 0; i < n; i++) v = (v << 1) | (uint)BitBei(bitPos + i); return v; } public void Ueberspringe(int n) { bitPos += n; } public uint Nimm(int n) { if (n == 0) return 0; uint v = Sicht(n); bitPos += n; return v; } /// Es wurde ueber das Ende hinaus gelesen - der Strom ist zu Ende. public bool Ueberzogen { get { return bitPos > bitEnde; } } } // ----------------------------------------------------------------------- // FSE - die Entropiekodierung von Zstd // ----------------------------------------------------------------------- internal class FseTabelle { public int TableLog; public byte[] Symbol; public byte[] NbBits; public ushort[] NewState; public static FseTabelle Baue(short[] norm, int maxSymbol, int tableLog) { FseTabelle t = new FseTabelle(); int size = 1 << tableLog; t.TableLog = tableLog; t.Symbol = new byte[size]; t.NbBits = new byte[size]; t.NewState = new ushort[size]; byte[] tabSym = new byte[size]; int[] naechster = new int[maxSymbol + 2]; int hoch = size - 1; for (int s = 0; s <= maxSymbol; s++) { if (norm[s] == -1) { tabSym[hoch--] = (byte)s; naechster[s] = 1; } else naechster[s] = norm[s]; } int schritt = (size >> 1) + (size >> 3) + 3; int maske = size - 1; int stelle = 0; for (int s = 0; s <= maxSymbol; s++) { for (int i = 0; i < norm[s]; i++) { tabSym[stelle] = (byte)s; stelle = (stelle + schritt) & maske; while (stelle > hoch) stelle = (stelle + schritt) & maske; } } if (stelle != 0) throw new ZstdFehler("FSE-Verteilung geht nicht auf"); for (int u = 0; u < size; u++) { byte sym = tabSym[u]; int n = naechster[sym]++; int nb = tableLog - RueckBits.HoechstesBit((uint)n); t.Symbol[u] = sym; t.NbBits[u] = (byte)nb; t.NewState[u] = (ushort)((n << nb) - size); } return t; } /// Normalisierte Haeufigkeiten lesen. Liefert verbrauchte Bytes. public static int LiesNCount(byte[] q, int off, int max, short[] norm, ref int maxSymbol, ref int tableLog) { int istart = off; int iend = off + max; int ip = off; if (max < 4) throw new ZstdFehler("NCount zu kurz"); // uint, nicht int: ein arithmetisches >> zieht das Vorzeichen nach und verfaelscht die oberen Bits (Game2.dcb scheiterte daran) uint bitStream = LiesLE32(q, ip, iend); int bitCount = 4; int log = (int)(bitStream & 0xF) + 5; if (log > 12) throw new ZstdFehler("tableLog zu gross: " + log); tableLog = log; bitStream >>= 4; int remaining = (1 << log) + 1; int threshold = 1 << log; int nbBits = log + 1; int charnum = 0; bool previous0 = false; while (remaining > 1 && charnum <= maxSymbol) { if (previous0) { int n0 = charnum; while ((bitStream & 0xFFFF) == 0xFFFF) { n0 += 24; if (ip < iend - 5) { ip += 2; bitStream = LiesLE32(q, ip, iend) >> bitCount; } else { bitStream >>= 16; bitCount += 16; } } while ((bitStream & 3) == 3) { n0 += 3; bitStream >>= 2; bitCount += 2; } n0 += (int)(bitStream & 3); bitCount += 2; if (n0 > maxSymbol) throw new ZstdFehler("NCount: zu viele Symbole"); while (charnum < n0) norm[charnum++] = 0; if ((ip <= iend - 7) || (ip + (bitCount >> 3) <= iend - 4)) { ip += bitCount >> 3; bitCount &= 7; bitStream = LiesLE32(q, ip, iend) >> bitCount; } else { bitStream >>= 2; } previous0 = false; continue; } int maxWert = (2 * threshold - 1) - remaining; int count; if ((bitStream & (uint)(threshold - 1)) < (uint)maxWert) { count = (int)(bitStream & (uint)(threshold - 1)); bitCount += nbBits - 1; } else { count = (int)(bitStream & (uint)(2 * threshold - 1)); if (count >= threshold) count -= maxWert; bitCount += nbBits; } count--; // -1 heisst "weniger als eins" remaining -= count < 0 ? -count : count; if (charnum > maxSymbol) throw new ZstdFehler("NCount: Symbol ausserhalb"); norm[charnum++] = (short)count; previous0 = (count == 0); while (remaining < threshold) { nbBits--; threshold >>= 1; } if ((ip <= iend - 7) || (ip + (bitCount >> 3) <= iend - 4)) { ip += bitCount >> 3; bitCount &= 7; } else { bitCount -= (int)(8 * (iend - 4 - ip)); ip = iend - 4; } bitStream = LiesLE32(q, ip, iend) >> (bitCount & 31); } if (remaining != 1) throw new ZstdFehler("NCount unvollstaendig"); while (charnum <= maxSymbol) norm[charnum++] = 0; maxSymbol = charnum - 1; ip += (bitCount + 7) >> 3; return ip - istart; } private static uint LiesLE32(byte[] q, int off, int ende) { uint r = 0; for (int i = 0; i < 4; i++) { int k = off + i; byte b = (k < ende && k < q.Length && k >= 0) ? q[k] : (byte)0; r |= (uint)b << (8 * i); } return r; } } // ----------------------------------------------------------------------- // Huffman fuer die Literale // ----------------------------------------------------------------------- internal class HuffTabelle { public int MaxBits; public byte[] Symbol; public byte[] Bits; public static HuffTabelle AusGewichten(byte[] gewicht, int anzahl) { uint summe = 0; for (int i = 0; i < anzahl; i++) if (gewicht[i] > 0) summe += (uint)(1 << (gewicht[i] - 1)); if (summe == 0) throw new ZstdFehler("Huffman ohne Gewichte"); int maxBits = RueckBits.HoechstesBit(summe) + 1; if (maxBits > 12) throw new ZstdFehler("Huffman zu tief: " + maxBits); uint rest = (uint)(1 << maxBits) - summe; if (rest == 0 || (rest & (rest - 1)) != 0) throw new ZstdFehler(string.Format( "Huffman-Restgewicht ist keine Zweierpotenz (anzahl={0} summe={1} maxBits={2} rest={3})", anzahl, summe, maxBits, rest)); gewicht[anzahl] = (byte)(RueckBits.HoechstesBit(rest) + 1); anzahl++; HuffTabelle h = new HuffTabelle(); h.MaxBits = maxBits; int size = 1 << maxBits; h.Symbol = new byte[size]; h.Bits = new byte[size]; int[] rangAnzahl = new int[maxBits + 2]; for (int i = 0; i < anzahl; i++) if (gewicht[i] > 0) rangAnzahl[gewicht[i]]++; // Ein Symbol mit Gewicht w hat die Codelaenge maxBits+1-w und belegt // damit 2^(w-1) Plaetze. // // Die Reihenfolge ist AUFSTEIGEND nach Gewicht - also die langen // Codes zuerst. Andersherum entpackt es sich anstandslos bis zur // richtigen Groesse, nur steht dann Kauderwelsch drin. int[] rangStart = new int[maxBits + 2]; int pos = 0; for (int w = 1; w <= maxBits; w++) { rangStart[w] = pos; pos += rangAnzahl[w] * (1 << (w - 1)); } if (pos != size) throw new ZstdFehler("Huffman-Tabelle geht nicht auf"); for (int s = 0; s < anzahl; s++) { int w = gewicht[s]; if (w == 0) continue; int nb = maxBits - w + 1; int n = 1 << (w - 1); for (int i = 0; i < n; i++) { h.Symbol[rangStart[w] + i] = (byte)s; h.Bits[rangStart[w] + i] = (byte)nb; } rangStart[w] += n; } return h; } } // ----------------------------------------------------------------------- // Der Entpacker // ----------------------------------------------------------------------- public static class Zstd { private const uint MagieRahmen = 0xFD2FB528; private static readonly int[] LlBasis = { 0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,18,20,22,24,28,32,40,48,64, 128,256,512,1024,2048,4096,8192,16384,32768,65536 }; private static readonly int[] LlExtra = { 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,2,2,3,3,4,6,7,8,9,10,11,12,13,14,15,16 }; private static readonly int[] MlBasis = { 3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32, 33,34,35,37,39,41,43,47,51,59,67,83,99,131,259,515,1027,2051,4099,8195,16387,32771,65539 }; private static readonly int[] MlExtra = { 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 1,1,1,1,2,2,3,3,4,4,5,7,8,9,10,11,12,13,14,15,16 }; private static readonly short[] LlVorgabe = { 4,3,2,2,2,2,2,2,2,2,2,2,2,1,1,1,2,2,2,2,2,2,2,2,2,3,2,1,1,1,1,1,-1,-1,-1,-1 }; private static readonly short[] MlVorgabe = { // RFC 8878, 53 Symbole: 20 + 37 Einsen + 7 "weniger als eins" = 64 1,4,3,2,2,2,2,2,2,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,1,1,1,1,1,1,1,-1,-1,-1,-1,-1,-1,-1 }; private static readonly short[] OfVorgabe = { 1,1,1,1,1,1,2,2,2,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,-1,-1,-1,-1,-1 }; /// Nur zum Nachsehen beim Entwickeln: Rahmen und erster Block. public static string Diagnose(byte[] q, int off, int len) { System.Text.StringBuilder sb = new System.Text.StringBuilder(); int p = off; uint magie = LiesU32(q, p); p += 4; sb.AppendLine(string.Format("Magie 0x{0:X8}", magie)); byte fhd = q[p++]; int fcsFeld = (fhd >> 6) & 3; bool einSegment = ((fhd >> 5) & 1) != 0; bool pruef = ((fhd >> 2) & 1) != 0; int dictFeld = fhd & 3; sb.AppendLine(string.Format("Kopf 0x{0:X2}: fcsFeld={1} einSegment={2} pruefsumme={3} dict={4}", fhd, fcsFeld, einSegment, pruef, dictFeld)); if (!einSegment) { sb.AppendLine("Fenster 0x" + q[p].ToString("X2")); p++; } int dl = dictFeld == 3 ? 4 : dictFeld; p += dl; int fcsLen = fcsFeld == 0 ? (einSegment ? 1 : 0) : (1 << fcsFeld); long inhalt = -1; if (fcsLen > 0) { inhalt = 0; for (int i = 0; i < fcsLen; i++) inhalt |= ((long)q[p + i]) << (8 * i); if (fcsLen == 2) inhalt += 256; p += fcsLen; } sb.AppendLine("Inhaltsgroesse laut Rahmen: " + inhalt); int kopf = q[p] | (q[p + 1] << 8) | (q[p + 2] << 16); p += 3; sb.AppendLine(string.Format("Block: letzter={0} art={1} groesse={2}", (kopf & 1) != 0, (kopf >> 1) & 3, kopf >> 3)); if (((kopf >> 1) & 3) != 2) return sb.ToString(); byte lh = q[p]; int lArt = lh & 3, format = (lh >> 2) & 3; sb.AppendLine(string.Format("Literale: art={0} format={1} (kopfbyte 0x{2:X2})", lArt, format, lh)); int klar2, roh2, stroeme2, q2 = p; if (format == 0) { klar2 = ((lh >> 4) & 0x3F) | ((q[q2+1] & 0x3F) << 4); roh2 = (q[q2+1] >> 6) | (q[q2+2] << 2); q2 += 3; stroeme2 = 1; } else if (format == 1) { klar2 = ((lh >> 4) & 0x3F) | ((q[q2+1] & 0x3F) << 4); roh2 = (q[q2+1] >> 6) | (q[q2+2] << 2); q2 += 3; stroeme2 = 4; } else if (format == 2) { klar2 = (lh >> 4) | (q[q2+1] << 4) | ((q[q2+2] & 3) << 12); roh2 = (q[q2+2] >> 2) | (q[q2+3] << 6); q2 += 4; stroeme2 = 4; } else { klar2 = (lh >> 4) | (q[q2+1] << 4) | ((q[q2+2] & 0x3F) << 12); roh2 = (q[q2+2] >> 6) | (q[q2+3] << 2) | (q[q2+4] << 10); q2 += 5; stroeme2 = 4; } sb.AppendLine(string.Format(" klar={0} roh={1} stroeme={2}", klar2, roh2, stroeme2)); if (lArt == 2) { byte hk = q[q2]; sb.AppendLine(string.Format(" Baumkopf 0x{0:X2} ({1})", hk, hk >= 128 ? "direkt" : "FSE, " + hk + " Bytes")); if (hk < 128) { short[] norm = new short[256]; int ms = 255, log = 0; int n = FseTabelle.LiesNCount(q, q2 + 1, hk, norm, ref ms, ref log); sb.AppendLine(string.Format(" NCount: {0} Bytes, tableLog={1}, maxSymbol={2}", n, log, ms)); System.Text.StringBuilder nb = new System.Text.StringBuilder(); for (int i = 0; i <= ms; i++) { nb.Append(norm[i]); nb.Append(' '); } sb.AppendLine(" norm: " + nb.ToString().Trim()); FseTabelle t = FseTabelle.Baue(norm, ms, log); RueckBits bits = new RueckBits(q, q2 + 1 + n, hk - n); int s1 = (int)bits.Nimm(log), s2 = (int)bits.Nimm(log); byte[] gew = new byte[256]; int anz = 0; while (true) { gew[anz++] = t.Symbol[s1]; s1 = t.NewState[s1] + (int)bits.Nimm(t.NbBits[s1]); if (bits.Ueberzogen || anz >= 255) break; gew[anz++] = t.Symbol[s2]; s2 = t.NewState[s2] + (int)bits.Nimm(t.NbBits[s2]); if (bits.Ueberzogen || anz >= 255) break; } System.Text.StringBuilder gb = new System.Text.StringBuilder(); for (int i = 0; i < anz; i++) { gb.Append(gew[i]); gb.Append(' '); } sb.AppendLine(" " + anz + " Gewichte: " + gb.ToString().Trim()); uint summe = 0; for (int i = 0; i < anz; i++) if (gew[i] > 0) summe += (uint)(1 << (gew[i] - 1)); sb.AppendLine(" Summe=" + summe + " hoechstesBit=" + RueckBits.HoechstesBit(summe)); } } return sb.ToString(); } public static byte[] Entpacke(byte[] q, int off, int len, long erwartet) { int p = off; int ende = off + len; uint magie = LiesU32(q, p); p += 4; while (magie != MagieRahmen) { if ((magie & 0xFFFFFFF0) == 0x184D2A50) // ueberspringbar { uint gr = LiesU32(q, p); p += 4 + (int)gr; if (p + 4 > ende) throw new ZstdFehler("nur ueberspringbare Rahmen"); magie = LiesU32(q, p); p += 4; continue; } throw new ZstdFehler(string.Format("kein Zstd-Rahmen (0x{0:X8})", magie)); } byte fhd = q[p++]; int fcsFeld = (fhd >> 6) & 3; bool einSegment = ((fhd >> 5) & 1) != 0; bool pruefsumme = ((fhd >> 2) & 1) != 0; int dictFeld = fhd & 3; if (((fhd >> 3) & 1) != 0) throw new ZstdFehler("reserviertes Bit gesetzt"); if (!einSegment) p++; // Fensterbeschreibung int dictLen = dictFeld == 3 ? 4 : dictFeld; if (dictLen > 0) { long dictId = 0; for (int i = 0; i < dictLen; i++) dictId |= ((long)q[p + i]) << (8 * i); p += dictLen; if (dictId != 0) throw new ZstdFehler("Woerterbuecher werden nicht unterstuetzt"); } long inhalt = -1; int fcsLen = fcsFeld == 0 ? (einSegment ? 1 : 0) : (1 << fcsFeld); if (fcsLen > 0) { inhalt = 0; for (int i = 0; i < fcsLen; i++) inhalt |= ((long)q[p + i]) << (8 * i); if (fcsLen == 2) inhalt += 256; p += fcsLen; } long kap = erwartet > 0 ? erwartet : (inhalt > 0 ? inhalt : 1 << 20); if (kap > int.MaxValue) throw new ZstdFehler("Inhalt zu gross"); byte[] aus = new byte[kap]; int ausPos = 0; HuffTabelle huff = null; FseTabelle llT = null, ofT = null, mlT = null; int rleLl = -1, rleOf = -1, rleMl = -1; // Die drei Wiederholungsabstaende gehoeren dem RAHMEN, nicht dem // Block: Ein Treffer im zweiten Block darf sich auf einen Abstand // aus dem ersten beziehen. Setzt man sie je Block zurueck, entpackt // alles bis zur richtigen Groesse - nur steht ab der ersten // Blockgrenze hier und da Unsinn. int[] letzte = { 1, 4, 8 }; int blockNr = 0; while (true) { if (p + 3 > ende) throw new ZstdFehler("Blockkopf abgeschnitten"); int kopf = q[p] | (q[p + 1] << 8) | (q[p + 2] << 16); p += 3; bool letzter = (kopf & 1) != 0; int art = (kopf >> 1) & 3; int gr2 = kopf >> 3; if (art == 0) { aus = Sicher(aus, ausPos + gr2); Array.Copy(q, p, aus, ausPos, gr2); ausPos += gr2; p += gr2; } else if (art == 1) { aus = Sicher(aus, ausPos + gr2); byte b = q[p++]; for (int i = 0; i < gr2; i++) aus[ausPos++] = b; } else if (art == 2) { try { ausPos = Block(q, p, gr2, ref aus, ausPos, ref huff, ref llT, ref ofT, ref mlT, ref rleLl, ref rleOf, ref rleMl, letzte); } catch (ZstdFehler e) { // Welcher Block, wo im Strom, wie weit die Ausgabe war: // ohne das ist ein Fehler in Block 1.800 nicht zu finden. throw new ZstdFehler(string.Format("{0} (Block {1} bei Byte {2}, {3} Bytes ausgegeben)", e.Message, blockNr, p, ausPos)); } p += gr2; } else throw new ZstdFehler("reservierte Blockart"); blockNr++; if (letzter) break; } if (pruefsumme) p += 4; if (ausPos != aus.Length) { byte[] genau = new byte[ausPos]; Array.Copy(aus, genau, ausPos); return genau; } return aus; } private static byte[] Sicher(byte[] a, int noetig) { if (noetig <= a.Length) return a; int neu = a.Length == 0 ? 65536 : a.Length; while (neu < noetig) neu *= 2; byte[] b = new byte[neu]; Array.Copy(a, b, a.Length); return b; } private static int Block(byte[] q, int off, int len, ref byte[] aus, int ausPos, ref HuffTabelle huff, ref FseTabelle llT, ref FseTabelle ofT, ref FseTabelle mlT, ref int rleLl, ref int rleOf, ref int rleMl, int[] letzte) { int p = off; int ende = off + len; // ---------- Literale ---------- byte lh = q[p]; int lArt = lh & 3; byte[] literale; int litLen; if (lArt == 0 || lArt == 1) { int format = (lh >> 2) & 3; int gr; if (format == 0 || format == 2) { gr = lh >> 3; p += 1; } else if (format == 1) { gr = (lh >> 4) | (q[p + 1] << 4); p += 2; } else { gr = (lh >> 4) | (q[p + 1] << 4) | (q[p + 2] << 12); p += 3; } literale = new byte[gr]; if (lArt == 0) { Array.Copy(q, p, literale, 0, gr); p += gr; } else { byte b = q[p++]; for (int i = 0; i < gr; i++) literale[i] = b; } litLen = gr; } else { int format = (lh >> 2) & 3; int klar, roh, stroeme; if (format == 0) { klar = ((lh >> 4) & 0x3F) | ((q[p + 1] & 0x3F) << 4); roh = (q[p + 1] >> 6) | (q[p + 2] << 2); p += 3; stroeme = 1; } else if (format == 1) { klar = ((lh >> 4) & 0x3F) | ((q[p + 1] & 0x3F) << 4); roh = (q[p + 1] >> 6) | (q[p + 2] << 2); p += 3; stroeme = 4; } else if (format == 2) { klar = (lh >> 4) | (q[p + 1] << 4) | ((q[p + 2] & 3) << 12); roh = (q[p + 2] >> 2) | (q[p + 3] << 6); p += 4; stroeme = 4; } else { klar = (lh >> 4) | (q[p + 1] << 4) | ((q[p + 2] & 0x3F) << 12); roh = (q[p + 2] >> 6) | (q[p + 3] << 2) | (q[p + 4] << 10); p += 5; stroeme = 4; } // "roh" umfasst Baumbeschreibung UND Stroeme. int abschnitt = p; int baum = 0; if (lArt == 2) { huff = LiesHuffKopf(q, p, ende, out baum); } else if (huff == null) throw new ZstdFehler("Literale ohne Baum"); literale = new byte[klar]; DekodiereLiterale(q, abschnitt + baum, roh - baum, stroeme, huff, literale); p = abschnitt + roh; litLen = klar; } // ---------- Sequenzen ---------- int anzahl; byte b0 = q[p++]; if (b0 == 0) anzahl = 0; else if (b0 < 128) anzahl = b0; else if (b0 < 255) { anzahl = ((b0 - 128) << 8) + q[p++]; } else { anzahl = q[p] + (q[p + 1] << 8) + 0x7F00; p += 2; } if (anzahl == 0) { aus = Sicher(aus, ausPos + litLen); Array.Copy(literale, 0, aus, ausPos, litLen); return ausPos + litLen; } byte modi = q[p++]; int llM = (modi >> 6) & 3; int ofM = (modi >> 4) & 3; int mlM = (modi >> 2) & 3; p += LiesTabelle(q, p, ende, llM, 35, 9, LlVorgabe, 6, ref llT, ref rleLl); p += LiesTabelle(q, p, ende, ofM, 31, 8, OfVorgabe, 5, ref ofT, ref rleOf); p += LiesTabelle(q, p, ende, mlM, 52, 9, MlVorgabe, 6, ref mlT, ref rleMl); RueckBits bits = new RueckBits(q, p, ende - p); // Anfangszustaende: Literallaenge, Abstand, Trefferlaenge - in dieser Reihenfolge. int llS = llM == 1 ? 0 : (int)bits.Nimm(llT.TableLog); int ofS = ofM == 1 ? 0 : (int)bits.Nimm(ofT.TableLog); int mlS = mlM == 1 ? 0 : (int)bits.Nimm(mlT.TableLog); int litPos = 0; for (int i = 0; i < anzahl; i++) { int llCode = llM == 1 ? rleLl : llT.Symbol[llS]; int ofCode = ofM == 1 ? rleOf : ofT.Symbol[ofS]; int mlCode = mlM == 1 ? rleMl : mlT.Symbol[mlS]; if (llCode >= LlBasis.Length) throw new ZstdFehler("Literallaengen-Code zu gross"); if (mlCode >= MlBasis.Length) throw new ZstdFehler("Trefferlaengen-Code zu gross"); if (ofCode > 31) throw new ZstdFehler("Abstandscode zu gross"); // Reihenfolge der Zusatzbits: Abstand, Trefferlaenge, Literallaenge. long offWert = (1L << ofCode) + bits.Nimm(ofCode); int mlWert = MlBasis[mlCode] + (int)bits.Nimm(MlExtra[mlCode]); int llWert = LlBasis[llCode] + (int)bits.Nimm(LlExtra[llCode]); long abstand; if (offWert > 3) { letzte[2] = letzte[1]; letzte[1] = letzte[0]; letzte[0] = (int)(offWert - 3); abstand = letzte[0]; } else { int idx = (int)offWert; if (llWert == 0) idx++; if (idx == 1) { abstand = letzte[0]; } else if (idx == 2) { abstand = letzte[1]; letzte[1] = letzte[0]; letzte[0] = (int)abstand; } else if (idx == 3) { abstand = letzte[2]; letzte[2] = letzte[1]; letzte[1] = letzte[0]; letzte[0] = (int)abstand; } else { abstand = letzte[0] - 1; if (abstand <= 0) throw new ZstdFehler("ungueltiger Wiederholungsabstand"); letzte[2] = letzte[1]; letzte[1] = letzte[0]; letzte[0] = (int)abstand; } } if (litPos + llWert > litLen) throw new ZstdFehler("mehr Literale verlangt als vorhanden"); aus = Sicher(aus, ausPos + llWert + mlWert); for (int k = 0; k < llWert; k++) aus[ausPos++] = literale[litPos++]; if (abstand > ausPos) throw new ZstdFehler("Abstand zeigt vor den Anfang"); int quelle = (int)(ausPos - abstand); for (int k = 0; k < mlWert; k++) aus[ausPos++] = aus[quelle++]; if (i + 1 < anzahl) { if (llM != 1) llS = llT.NewState[llS] + (int)bits.Nimm(llT.NbBits[llS]); if (mlM != 1) mlS = mlT.NewState[mlS] + (int)bits.Nimm(mlT.NbBits[mlS]); if (ofM != 1) ofS = ofT.NewState[ofS] + (int)bits.Nimm(ofT.NbBits[ofS]); } } int rest = litLen - litPos; aus = Sicher(aus, ausPos + rest); for (int k = 0; k < rest; k++) aus[ausPos++] = literale[litPos++]; return ausPos; } private static int LiesTabelle(byte[] q, int off, int ende, int modus, int maxSymbol, int maxLog, short[] vorgabe, int vorgabeLog, ref FseTabelle t, ref int rle) { if (modus == 0) { t = FseTabelle.Baue(vorgabe, vorgabe.Length - 1, vorgabeLog); return 0; } if (modus == 1) { rle = q[off]; return 1; } if (modus == 3) { if (t == null) throw new ZstdFehler("Wiederholung ohne vorherige Tabelle"); return 0; } short[] norm = new short[maxSymbol + 2]; int ms = maxSymbol; int log = 0; int n = FseTabelle.LiesNCount(q, off, ende - off, norm, ref ms, ref log); if (log > maxLog) throw new ZstdFehler("tableLog zu gross"); try { t = FseTabelle.Baue(norm, ms, log); } catch (ZstdFehler e) { throw new ZstdFehler(e.Message + " [Sequenztabelle maxSymbol=" + maxSymbol + " log=" + log + "]"); } return n; } private static HuffTabelle LiesHuffKopf(byte[] q, int off, int ende, out int verbraucht) { int p = off; byte kopf = q[p++]; byte[] gewicht = new byte[256]; int anzahl; if (kopf >= 128) { anzahl = kopf - 127; for (int i = 0; i < anzahl; i++) { byte b = q[p + (i >> 1)]; gewicht[i] = (i & 1) == 0 ? (byte)(b >> 4) : (byte)(b & 0xF); } p += (anzahl + 1) / 2; } else { int len = kopf; short[] norm = new short[256]; int ms = 255; int log = 0; int n = FseTabelle.LiesNCount(q, p, len, norm, ref ms, ref log); if (log > 6) throw new ZstdFehler("Gewichtstabelle zu gross"); FseTabelle t; try { t = FseTabelle.Baue(norm, ms, log); } catch (ZstdFehler e) { System.Text.StringBuilder nb = new System.Text.StringBuilder(); for (int i = 0; i <= ms; i++) { nb.Append(norm[i]); nb.Append(' '); } throw new ZstdFehler(e.Message + " [Huffman-Gewichte, log=" + log + " ms=" + ms + " n=" + n + " len=" + len + " norm=" + nb.ToString().Trim() + "]"); } // Zwei verschraenkte Zustaende. Ausgegeben wird das Symbol des // aktuellen Zustands, DANACH wird fortgeschrieben. // // Der Abbruch ist die Stelle, an der es leicht schiefgeht: // Laeuft der Strom beim Fortschreiben ueber, haelt der ANDERE // Zustand noch ein gueltiges Symbol. Es gehoert mit ausgegeben, // sonst fehlt genau eines - und die Huffman-Tabelle geht dann // nicht auf. RueckBits bits = new RueckBits(q, p + n, len - n); int s1 = (int)bits.Nimm(log); int s2 = (int)bits.Nimm(log); anzahl = 0; while (anzahl < 254) { gewicht[anzahl++] = t.Symbol[s1]; s1 = t.NewState[s1] + (int)bits.Nimm(t.NbBits[s1]); if (bits.Ueberzogen) { gewicht[anzahl++] = t.Symbol[s2]; break; } gewicht[anzahl++] = t.Symbol[s2]; s2 = t.NewState[s2] + (int)bits.Nimm(t.NbBits[s2]); if (bits.Ueberzogen) { gewicht[anzahl++] = t.Symbol[s1]; break; } } p += len; } verbraucht = p - off; return HuffTabelle.AusGewichten(gewicht, anzahl); } private static void DekodiereLiterale(byte[] q, int off, int roh, int stroeme, HuffTabelle h, byte[] ziel) { if (ziel.Length == 0) return; if (stroeme == 1) { EinStrom(q, off, roh, h, ziel, 0, ziel.Length); return; } int s1 = q[off] | (q[off + 1] << 8); int s2 = q[off + 2] | (q[off + 3] << 8); int s3 = q[off + 4] | (q[off + 5] << 8); int start = off + 6; int s4 = roh - 6 - s1 - s2 - s3; if (s4 < 0) throw new ZstdFehler("Sprungtabelle passt nicht"); int teil = (ziel.Length + 3) / 4; int letzterTeil = ziel.Length - 3 * teil; if (letzterTeil < 0) throw new ZstdFehler("Literalgroesse passt nicht zu vier Stroemen"); EinStrom(q, start, s1, h, ziel, 0, teil); EinStrom(q, start + s1, s2, h, ziel, teil, teil); EinStrom(q, start + s1 + s2, s3, h, ziel, 2 * teil, teil); EinStrom(q, start + s1 + s2 + s3, s4, h, ziel, 3 * teil, letzterTeil); } private static void EinStrom(byte[] q, int off, int len, HuffTabelle h, byte[] ziel, int zielOff, int anzahl) { if (anzahl == 0) return; RueckBits bits = new RueckBits(q, off, len); for (int i = 0; i < anzahl; i++) { int idx = (int)bits.Sicht(h.MaxBits); ziel[zielOff + i] = h.Symbol[idx]; bits.Ueberspringe(h.Bits[idx]); } } private static uint LiesU32(byte[] q, int off) { return (uint)(q[off] | (q[off + 1] << 8) | (q[off + 2] << 16) | (q[off + 3] << 24)); } /// CRC-32 wie im ZIP-Verzeichnis - der Beweis, dass richtig entpackt wurde. public static uint Crc32(byte[] d) { uint[] tab = new uint[256]; for (uint n = 0; n < 256; n++) { uint c = n; for (int k = 0; k < 8; k++) c = (c & 1) != 0 ? (0xEDB88320u ^ (c >> 1)) : (c >> 1); tab[n] = c; } uint crc = 0xFFFFFFFFu; for (int i = 0; i < d.Length; i++) crc = tab[(crc ^ d[i]) & 0xFF] ^ (crc >> 8); return crc ^ 0xFFFFFFFFu; } } } '@ # The DataForge reader (tools/dcb.cs), embedded the same way as the zstd # decoder: one file, readable, nothing to install. See the header there. $DCB_QUELLE = @' // --------------------------------------------------------------------------- // DataForge reader for Game2.dcb (format version 8) // // Game2.dcb is the game's compiled data: every entity, mission, place and // reputation record, 117,000 of them in LIVE 4.10. The upload tool pulls // it out of Data.p4k, reads what a player would want to know, and sends // the extract - the site never sees the 332 MB. // // Layout, learned from the file itself (the public descriptions cover // version 5/6 and differ in two places, marked below): // // header zero, version, 26 counts // definitions structs (16 B), properties (12 B), enums (8 B), // data mappings (8 B), records (36 B - not 32: four unknown // bytes at the end) // values one array per scalar type, then strong/weak/reference // pointers and enum options // text A the string VALUES (file paths, keys, names of things) // text B the NAMES of structs, properties and records // instances for every data mapping, that many instances of the struct // back to back; an instance is its properties in order, // parents first // // Version 5/6 readers expect one text block; version 8 has two, and the // offsets in the definitions point into B, the offsets in string values // into A. Get that wrong and every name is a random UI string. // // Reading is lazy: an instance is turned into a dictionary only when // asked for, pointers are followed to a depth the caller sets, and // references (GUIDs) resolve to record names through a map built once. // Written for PowerShell's Add-Type: no LINQ, no tuples, no dynamic. // --------------------------------------------------------------------------- using System; using System.Collections.Generic; using System.Text; namespace QuestGuide { public class DcbFehler : Exception { public DcbFehler(string m) : base(m) { } } public class DcbRecord { public int Index; public string Name; public string File; public int StructIndex; public int Instance; public string Guid; } public class Dcb { // property data types const int T_BOOL = 1, T_INT8 = 2, T_INT16 = 3, T_INT32 = 4, T_INT64 = 5, T_UINT8 = 6, T_UINT16 = 7, T_UINT32 = 8, T_UINT64 = 9, T_STRING = 10, T_SINGLE = 11, T_DOUBLE = 12, T_LOCALE = 13, T_GUID = 14, T_ENUM = 15, T_CLASS = 16, T_STRONG = 0x110, T_WEAK = 0x210, T_REF = 0x310; struct StructDef { public int Name, Parent, AttrCount, FirstAttr; } struct PropDef { public int Name, StructIndex, DataType, Conv; } struct ValueArray { public long Off; public int Count, Size; } readonly byte[] b; StructDef[] structs; PropDef[] props; int[] mapCount, mapStruct; public DcbRecord[] Records; long textA, textB, dataStart, dataEnd; readonly Dictionary values = new Dictionary(); readonly Dictionary structSize = new Dictionary(); readonly Dictionary instanceBase = new Dictionary(); // struct -> offset of instance 0 readonly Dictionary instanceCount = new Dictionary(); readonly Dictionary byGuid = new Dictionary(); readonly Dictionary> byStruct = new Dictionary>(); readonly Dictionary structNames = new Dictionary(); readonly Dictionary allProps = new Dictionary(); public int RecordCount { get { return Records.Length; } } public long DataEnd { get { return dataEnd; } } public long Length { get { return b.LongLength; } } public Dcb(byte[] daten) { b = daten; long o = 0; if (U32(o) != 0) throw new DcbFehler("not a DataForge file"); int version = (int)U32(4); if (version != 8) throw new DcbFehler("DataForge version " + version + " - this reader knows 8"); o = 8; o += 8; // two zero words string[] namen = { "structDefs", "propertyDefs", "enumDefs", "dataMappings", "recordDefs", "bool", "int8", "int16", "int32", "int64", "uint8", "uint16", "uint32", "uint64", "single", "double", "guid", "string", "locale", "enum", "strong", "weak", "reference", "enumOption", "textLen", "blobLen" }; var c = new Dictionary(); foreach (string n in namen) { c[n] = (int)U32(o); o += 4; } structs = new StructDef[c["structDefs"]]; for (int i = 0; i < structs.Length; i++) { structs[i].Name = (int)U32(o); structs[i].Parent = (int)U32(o + 4); structs[i].AttrCount = U16(o + 8); structs[i].FirstAttr = U16(o + 10); o += 16; } props = new PropDef[c["propertyDefs"]]; for (int i = 0; i < props.Length; i++) { props[i].Name = (int)U32(o); props[i].StructIndex = U16(o + 4); props[i].DataType = U16(o + 6); props[i].Conv = U16(o + 8); o += 12; } o += (long)c["enumDefs"] * 8; mapCount = new int[c["dataMappings"]]; mapStruct = new int[c["dataMappings"]]; for (int i = 0; i < mapCount.Length; i++) { mapCount[i] = (int)U32(o); mapStruct[i] = (int)U32(o + 4); o += 8; } long recOff = o; o += (long)c["recordDefs"] * 36; // value arrays, in this order int[] typen = { T_INT8, T_INT16, T_INT32, T_INT64, T_UINT8, T_UINT16, T_UINT32, T_UINT64, T_BOOL, T_SINGLE, T_DOUBLE, T_GUID, T_STRING, T_LOCALE, T_ENUM, T_STRONG, T_WEAK, T_REF }; string[] keys = { "int8", "int16", "int32", "int64", "uint8", "uint16", "uint32", "uint64", "bool", "single", "double", "guid", "string", "locale", "enum", "strong", "weak", "reference" }; int[] sizes = { 1, 2, 4, 8, 1, 2, 4, 8, 1, 4, 8, 16, 4, 4, 4, 8, 8, 20 }; for (int i = 0; i < typen.Length; i++) { ValueArray v; v.Off = o; v.Count = c[keys[i]]; v.Size = sizes[i]; values[typen[i]] = v; o += (long)v.Count * v.Size; } o += (long)c["enumOption"] * 4; textA = o; o += c["textLen"]; textB = o; o += c["blobLen"]; dataStart = o; // instance offsets: mapping by mapping, struct sizes from the definitions long p = dataStart; for (int i = 0; i < mapCount.Length; i++) { int si = mapStruct[i]; instanceBase[si] = p; instanceCount[si] = mapCount[i]; p += (long)mapCount[i] * StructSize(si); } dataEnd = p; if (dataEnd != b.LongLength) throw new DcbFehler("instance data ends at " + dataEnd + ", file has " + b.LongLength + " bytes - struct sizes are off"); // records, with the GUID map for references Records = new DcbRecord[c["recordDefs"]]; for (int i = 0; i < Records.Length; i++) { long r = recOff + (long)i * 36; var rec = new DcbRecord(); rec.Index = i; rec.Name = NameB((int)U32(r)); rec.File = StrA((int)U32(r + 4)); rec.StructIndex = (int)U32(r + 12); rec.Guid = GuidAt(r + 16); rec.Instance = U16(r + 32); Records[i] = rec; byGuid[rec.Guid] = rec; string sn = StructName(rec.StructIndex); List l; if (!byStruct.TryGetValue(sn, out l)) { l = new List(); byStruct[sn] = l; } l.Add(rec); } } // --- primitives ------------------------------------------------------ uint U32(long o) { return (uint)(b[o] | (b[o + 1] << 8) | (b[o + 2] << 16) | (b[o + 3] << 24)); } int U16(long o) { return b[o] | (b[o + 1] << 8); } string Cstr(long o) { long e = o; while (e < b.LongLength && b[e] != 0) e++; return Encoding.UTF8.GetString(b, (int)o, (int)(e - o)); } string StrA(int off) { return Cstr(textA + off); } string NameB(int off) { return Cstr(textB + off); } string GuidAt(long o) { // the mixed-endian form the game and the wiki print return string.Format("{0:x8}-{1:x4}-{2:x4}-{3:x2}{4:x2}-{5:x2}{6:x2}{7:x2}{8:x2}{9:x2}{10:x2}", U32(o), U16(o + 4), U16(o + 6), b[o + 8], b[o + 9], b[o + 10], b[o + 11], b[o + 12], b[o + 13], b[o + 14], b[o + 15]); } public string StructName(int si) { string n; if (!structNames.TryGetValue(si, out n)) { n = NameB(structs[si].Name); structNames[si] = n; } return n; } PropDef[] AllProps(int si) { PropDef[] cached; if (allProps.TryGetValue(si, out cached)) return cached; var chain = new List(); int k = si; while (k != -1 && k != int.MaxValue && k >= 0 && k < structs.Length) { chain.Insert(0, k); k = structs[k].Parent; } var list = new List(); foreach (int ci in chain) for (int j = 0; j < structs[ci].AttrCount; j++) list.Add(props[structs[ci].FirstAttr + j]); cached = list.ToArray(); allProps[si] = cached; return cached; } int PropSize(PropDef p) { if (p.Conv != 0) return 8; if (p.DataType == T_CLASS) return StructSize(p.StructIndex); switch (p.DataType) { case T_BOOL: case T_INT8: case T_UINT8: return 1; case T_INT16: case T_UINT16: return 2; case T_INT32: case T_UINT32: case T_STRING: case T_SINGLE: case T_LOCALE: case T_ENUM: return 4; case T_INT64: case T_UINT64: case T_DOUBLE: case T_STRONG: case T_WEAK: return 8; case T_GUID: return 16; case T_REF: return 20; } throw new DcbFehler("unknown property type " + p.DataType); } int StructSize(int si) { int s; if (structSize.TryGetValue(si, out s)) return s; s = 0; foreach (PropDef p in AllProps(si)) s += PropSize(p); structSize[si] = s; return s; } // --- records --------------------------------------------------------- public List RecordsOf(string structName) { List l; return byStruct.TryGetValue(structName, out l) ? l : new List(); } public DcbRecord ByGuid(string guid) { DcbRecord r; return byGuid.TryGetValue(guid, out r) ? r : null; } /// The record name a reference points at, or null. public string RefName(object v) { var d = v as Dictionary; if (d == null || !d.ContainsKey("__ref")) return null; DcbRecord r = ByGuid((string)d["__ref"]); return r == null ? null : r.Name; } // --- instances ------------------------------------------------------- /// The record's instance as a dictionary tree; pointers followed to the given depth. public Dictionary Read(DcbRecord r, int tiefe) { return Instance(r.StructIndex, r.Instance, tiefe); } Dictionary Instance(int si, int ii, int tiefe) { long basis; int n; if (!instanceBase.TryGetValue(si, out basis) || !instanceCount.TryGetValue(si, out n) || ii >= n) { var m = new Dictionary(); m["__missing"] = StructName(si) + "#" + ii; return m; } // The instance index rides along: a weak pointer says "ObjectiveToken#17", // and only this lets a caller find that token inside the tree it read. var inst = ReadAt(si, basis + (long)ii * StructSize(si), tiefe); inst["__index"] = ii; return inst; } Dictionary ReadAt(int si, long off, int tiefe) { var o = new Dictionary(); o["__type"] = StructName(si); long p = off; foreach (PropDef prop in AllProps(si)) { string pn = NameB(prop.Name); if (prop.Conv == 0) { if (prop.DataType == T_CLASS) o[pn] = tiefe > 0 ? (object)ReadAt(prop.StructIndex, p, tiefe - 1) : "..."; else o[pn] = Scalar(prop.DataType, p, tiefe); p += PropSize(prop); } else { int count = (int)U32(p), first = (int)U32(p + 4); p += 8; o[pn] = Array(prop, count, first, tiefe); } } return o; } object Scalar(int type, long o, int tiefe) { switch (type) { case T_BOOL: return b[o] != 0; case T_INT8: return (int)(sbyte)b[o]; case T_INT16: return (int)(short)U16(o); case T_INT32: return (int)U32(o); case T_INT64: return BitConverter.ToInt64(b, (int)o); case T_UINT8: return (int)b[o]; case T_UINT16: return U16(o); case T_UINT32: return (long)U32(o); case T_UINT64: return (double)BitConverter.ToUInt64(b, (int)o); case T_SINGLE: return (double)BitConverter.ToSingle(b, (int)o); case T_DOUBLE: return BitConverter.ToDouble(b, (int)o); case T_STRING: case T_LOCALE: case T_ENUM: return StrA((int)U32(o)); case T_GUID: return GuidAt(o); case T_STRONG: case T_WEAK: { uint si = U32(o), ii = U32(o + 4); if (si == 0xFFFFFFFF) return null; if (type == T_STRONG && tiefe > 0) return Instance((int)si, (int)ii, tiefe - 1); var d = new Dictionary(); d[type == T_STRONG ? "__strong" : "__weak"] = StructName((int)si) + "#" + ii; return d; } case T_REF: { string g = GuidAt(o + 4); if (g == "00000000-0000-0000-0000-000000000000") return null; var d = new Dictionary(); d["__ref"] = g; DcbRecord r = ByGuid(g); if (r != null) d["__name"] = r.Name; return d; } } return null; } List Array(PropDef prop, int count, int first, int tiefe) { var out_ = new List(); if (count > 20000) { out_.Add("[" + count + " items]"); return out_; } if (prop.DataType == T_CLASS) { for (int i = 0; i < count; i++) out_.Add(tiefe > 0 ? (object)Instance(prop.StructIndex, first + i, tiefe - 1) : "..."); return out_; } ValueArray a; if (!values.TryGetValue(prop.DataType, out a)) { out_.Add("[?type " + prop.DataType + "]"); return out_; } for (int i = 0; i < count; i++) out_.Add(Scalar(prop.DataType, a.Off + (long)(first + i) * a.Size, tiefe)); return out_; } } } '@ # =========================================================================== # Kanaele finden # =========================================================================== # # Jeder Ordner unter $SC_ROOT ist ein Kanal - LIVE, PTU, HOTFIX, EPTU, # TECH-PREVIEW. Es gibt hier bewusst KEINE feste Liste: Was CIG morgen # einfuehrt, taucht von selbst auf. # # Ein Ordner allein reicht aber nicht. Waehrend der Launcher laedt, steht dort # nur eine "Data.p4k.part" - keine Spieldaten, keine Sprachdateien, nichts zu # extrahieren. Solche Kanaele wurden frueher stillschweigend uebergangen: Im # Launcher stand HOTFIX, im Uploader nicht, und niemand konnte sehen warum. # Jetzt kommen sie mit in die Liste, mit dem Grund und ohne Hochladeknopf. # Sprachfassungen in der Reihenfolge, in der sie genommen werden. # # Die Schluessel der Missionen sind in jeder Sprache dieselben; nur Titel und # Beschreibungen unterscheiden sich. Deutsch zuerst, weil die Seite damit # eingelesen wurde - aber wer sein Spiel auf Englisch stehen hat, hatte vorher # gar keine Installation gefunden, ohne zu erfahren warum. $LOCALES = @( 'german_(germany)', 'english', 'french_(france)', 'spanish_(latin_america)', 'spanish_(spain)', 'italian_(italy)', 'polish_(poland)', 'portuguese_(brazil)', 'korean_(south_korea)', 'japanese_(japan)', 'chinese_(simplified)', 'chinese_(traditional)' ) function Finde-Sprachdatei($install) { $wurzel = Join-Path $install 'data\Localization' if (-not (Test-Path $wurzel)) { return $null } foreach ($l in $LOCALES) { $p = Join-Path $wurzel "$l\global.ini" if (Test-Path $p) { return $p } } # Eine Sprache, die wir nicht kennen, ist immer noch besser als keine. $rest = Get-ChildItem $wurzel -Directory -ErrorAction SilentlyContinue | ForEach-Object { Join-Path $_.FullName 'global.ini' } | Where-Object { Test-Path $_ } | Select-Object -First 1 return $rest } function Finde-Kanaele { if (-not (Test-Path $SC_ROOT)) { return @() } Get-ChildItem $SC_ROOT -Directory -ErrorAction SilentlyContinue | ForEach-Object { $ini = Finde-Sprachdatei $_.FullName $teil = @(Get-ChildItem $_.FullName -Filter '*.part' -ErrorAction SilentlyContinue) $grund = $null # Ohne lose Sprachdatei ist die Data.p4k die Quelle. Ob darin wirklich # eine global.ini steckt, wird ERST beim Hochladen geprueft: Das # Verzeichnis der p4k hat 1,37 Millionen Eintraege, und das bei jedem # Programmstart fuer jeden Kanal zu durchsuchen waere unzumutbar. # Eine lose Sprachdatei, die AELTER ist als der installierte Build, # gehoert zu einem frueheren Stand. Sie legt naemlich nicht das Spiel # an, sondern ein Drittanbieter-Launcher - und der patcht erst, wenn # ihn jemand startet. Wer nach einem Patch nur das Spiel aktualisiert, # hat daneben weiter den alten Text liegen. # # Das ist keine Kleinigkeit: Der Build stammt aus build_manifest.id und # ist dann NEU, der Text aber ALT. Der Upload sieht voellig plausibel # aus und meldet "keine Aenderungen". # # So ist am 07.09.2026 der PTU-Build 12578875 mit dem Text vom 27.08. # importiert worden - elf Tage alt. Aufgefallen ist es nur, weil die # Patch Notes etwas anderes versprachen. # # In dem Fall wird die lose Datei verworfen und aus der Data.p4k # gelesen. Die ist immer der Auslieferungsstand. $veraltet = $null if ($ini) { $bauzeit = @( (Join-Path $_.FullName 'build_manifest.id'), (Join-Path $_.FullName 'Data.p4k') ) | Where-Object { Test-Path $_ } | ForEach-Object { (Get-Item $_).LastWriteTime } | Sort-Object -Descending | Select-Object -First 1 if ($bauzeit -and (Get-Item $ini).LastWriteTime -lt $bauzeit) { $veraltet = ('lose Sprachdatei vom {0:dd.MM.yyyy} ist aelter als der Build vom {1:dd.MM.yyyy} - lese aus der Data.p4k' -f (Get-Item $ini).LastWriteTime, $bauzeit) $ini = $null } } $p4k = Test-Path (Join-Path $_.FullName 'Data.p4k') if (-not $ini) { $grund = if ($teil.Count) { $gb = [math]::Round((($teil | Measure-Object Length -Sum).Sum) / 1GB, 1) "laedt noch ($gb GB bisher)" } elseif ($p4k) { $null # nutzbar, nur eben aus der p4k } elseif ($veraltet) { # Kein p4k, und die lose Datei taugt nicht - dann ist der Kanal # wirklich unbrauchbar, und der Grund soll das auch sagen. $veraltet } elseif (Test-Path (Join-Path $_.FullName 'data\Localization')) { 'keine Sprachdatei gefunden' } else { 'keine Spieldaten' } } [pscustomobject]@{ Name = $_.Name Pfad = $_.FullName Ini = $ini Sprache = if ($ini) { Split-Path (Split-Path $ini -Parent) -Leaf } else { $null } AusP4k = (-not $ini) -and $p4k # Gesetzt, wenn eine lose Sprachdatei verworfen wurde, weil sie zu # einem frueheren Build gehoert. Nur zum Anzeigen. Veraltet = $veraltet # Auch ohne Sprachdatei lesbar, und dann nuetzlich: Man sieht, # welcher Stand da unbrauchbar herumliegt. Build = Lies-Build $_.FullName Version = Lies-Spielversion $_.FullName Datum = if ($ini) { (Get-Item $ini).LastWriteTime } else { $_.LastWriteTime } Bereit = [bool]$ini -or ($p4k -and -not $teil.Count) Grund = $grund } } } # Lesbare Spielversion, z.B. "Alpha 4.10.0". # # Die Buildnummer (12545750) ist eindeutig, aber niemand erkennt sie wieder. # Im Manifest steht der Zweig als "sc-alpha-4.10.0-hotfix" - daraus wird die # Angabe, die auch im Spiel und in CIGs Ankuendigungen steht. # # ABER: Der Zweig hinkt hinterher. Am 16.09.2026 kam 4.10.1 als Build # 12660092 - und im Manifest stand weiter "sc-alpha-4.10.0". Was der # Launcher anzeigt ("4.10.1-live.12660092"), schreibt er in sein Log: # # [Pipeline] Installing Star Citizen LIVE 4.10.1-live.12660092 at ... # # Die Zeile mit DERSELBEN Buildnummer wie im Manifest gewinnt; der Zweig # bleibt die Rueckfallebene, wenn das Log fehlt oder den Build nicht kennt. function Lies-Spielversion($install) { $m = Join-Path $install 'build_manifest.id' if (-not (Test-Path $m)) { return $null } try { $j = Get-Content $m -Raw -Encoding UTF8 | ConvertFrom-Json $zweig = [string]$j.Data.Branch $stufe = $null; $ausZweig = $null if ($zweig -match '^sc-([a-z]+)-([0-9][0-9.]*)') { $stufe = (Get-Culture).TextInfo.ToTitleCase($Matches[1]) $ausZweig = ('{0} {1}' -f $stufe, $Matches[2]).Trim() } $build = [string]$j.Data.RequestedP4ChangeNum $ausLog = Lies-Launcherversion $build if ($ausLog) { return (('{0} {1}' -f $stufe, $ausLog).Trim()) } if ($ausZweig) { return $ausZweig } if ($zweig) { return $zweig } if ($j.Data.Version) { return [string]$j.Data.Version } } catch { } return $null } # Die Versionsnummer, die der Launcher fuer eine Buildnummer nennt - aus # seinem Log, letzte passende Zeile. $null, wenn nichts passt. function Lies-Launcherversion([string]$build) { if (-not $build) { return $null } $log = Join-Path $env:APPDATA 'rsilauncher\logs\log.log' if (-not (Test-Path $log)) { return $null } try { $treffer = Select-String -Path $log -Pattern ('Star Citizen \w+ ([0-9][0-9.]*)-[a-z]+\.' + [regex]::Escape($build) + '\b') | Select-Object -Last 1 if ($treffer) { return $treffer.Matches[0].Groups[1].Value } } catch { } return $null } function Lies-Build($install) { $m = Join-Path $install 'build_manifest.id' if (-not (Test-Path $m)) { return 'unbekannt' } try { $j = Get-Content $m -Raw -Encoding UTF8 | ConvertFrom-Json foreach ($f in 'RequestedP4ChangeNum', 'Version', 'BuildId') { if ($j.Data -and $j.Data.$f) { return [string]$j.Data.$f } if ($j.$f) { return [string]$j.$f } } } catch { } return 'unbekannt' } # =========================================================================== # Originaltext aus der Launcher-Sicherung zurueckrechnen # =========================================================================== # # Stand frueher in restore-original.ps1 daneben. Jetzt hier drin, weil das # Werkzeug als EINE Datei ausgeliefert wird - drei Dateien herunterladen und # in denselben Ordner legen zu muessen, war eine Huerde ohne Gegenwert. # # Der Launcher schreibt vor jeder Aenderung den urspruenglichen Wert nach # global.ini.bp.bak.json bzw. global.ini.strings.bak.json, als Zuordnung # Schluessel -> Originaltext. Daraus laesst sich der Auslieferungszustand # zeichengenau zurueckrechnen. # # Eine Eigenheit: Die Sicherung schreibt Schluessel klein und laesst den # Varianten-Zusatz weg - die ini-Zeile # Adagio_..._Levski_E_Desc_001,P # steht in der Sicherung als # adagio_..._levski_e_desc_001 # Deshalb wird zuerst der genaue Schluessel gesucht und erst danach der ohne # Zusatz - und der zweite Weg nur, wenn genau eine ini-Zeile darauf passt. # Sonst waere die Zuordnung geraten. # # Die Installation wird NICHT veraendert. Geschrieben wird ausschliesslich # die Zieldatei; ein Fehlschlag laesst das Spiel unberuehrt. function Stelle-OriginalHer { [CmdletBinding()] param( [Parameter(Mandatory)][string] $IniPath, [string] $OutPath, # Auch dann ein Ergebnis liefern, wenn Reste der Injektion uebrig # bleiben. Nur zur Fehlersuche - so eine Datei darf nicht importiert # werden. [switch] $PassThru ) $ErrorActionPreference = 'Stop' # Varianten-Zusatz am Schluesselende, etwa ",P". $ZUSATZ = ',[A-Za-z]+$' # --------------------------------------------------------------------- Start if (-not (Test-Path -LiteralPath $IniPath)) { throw "Nicht gefunden: $IniPath" } $quelle = (Resolve-Path -LiteralPath $IniPath).Path $ordner = Split-Path -Parent $quelle if (-not $OutPath) { $OutPath = Join-Path ([IO.Path]::GetTempPath()) 'SCQuestGuide\global.restored.ini' } $zielOrdner = Split-Path -Parent $OutPath if ($zielOrdner -and -not (Test-Path -LiteralPath $zielOrdner)) { New-Item -ItemType Directory -Path $zielOrdner -Force | Out-Null } # --- Sicherungen einlesen ------------------------------------------------- $original = [System.Collections.Generic.Dictionary[string,string]]::new( [StringComparer]::OrdinalIgnoreCase) $sicherungen = @() foreach ($name in 'global.ini.bp.bak.json', 'global.ini.strings.bak.json') { $pfad = Join-Path $ordner $name if (-not (Test-Path -LiteralPath $pfad)) { continue } $sicherungen += $name $obj = [System.IO.File]::ReadAllText($pfad, [System.Text.Encoding]::UTF8) | ConvertFrom-Json foreach ($p in $obj.PSObject.Properties) { $original[$p.Name] = [string]$p.Value } } if ($original.Count -eq 0) { throw "Keine Launcher-Sicherung neben '$quelle'. Der Originaltext laesst sich " + "nicht rekonstruieren - diese Installation ist als Quelle nicht verwendbar." } $zeilen = [System.IO.File]::ReadAllLines($quelle, [System.Text.Encoding]::UTF8) # --- Eindeutigkeit der Zuordnung ohne Zusatz pruefen ---------------------- # Ein Sicherungseintrag darf nur benutzt werden, wenn genau eine ini-Zeile # ohne ihren Varianten-Zusatz darauf passt. Passen mehrere, laesst sich nicht # entscheiden, welche gemeint ist - dann bleibt der Weg gesperrt. $anspruch = [System.Collections.Generic.Dictionary[string,int]]::new( [StringComparer]::OrdinalIgnoreCase) foreach ($z in $zeilen) { $t = $z.IndexOf('='); if ($t -lt 1) { continue } $k = $z.Substring(0, $t) if ($original.ContainsKey($k)) { continue } # exakter Treffer, egal $b = $k -replace $ZUSATZ, '' if ($b -ceq $k -or -not $original.ContainsKey($b)) { continue } $n = 0; [void]$anspruch.TryGetValue($b, [ref]$n) $anspruch[$b] = $n + 1 } $mehrdeutig = [System.Collections.Generic.HashSet[string]]::new( [string[]]@($anspruch.GetEnumerator() | Where-Object { $_.Value -gt 1 } | ForEach-Object { $_.Key }), [StringComparer]::OrdinalIgnoreCase) if ($mehrdeutig.Count) { Write-Verbose "Mehrdeutige Zuordnung, uebersprungen: $($mehrdeutig.Count) Eintraege" } # --- Datei zurueckrechnen ------------------------------------------------- $exakt = 0; $ueberZusatz = 0 $aus = [System.Collections.Generic.List[string]]::new($zeilen.Length) foreach ($z in $zeilen) { $t = $z.IndexOf('=') if ($t -lt 1) { $aus.Add($z); continue } $k = $z.Substring(0, $t); $v = $z.Substring($t + 1) $soll = $null if ($original.TryGetValue($k, [ref]$soll)) { if ($v -cne $soll) { $exakt++ } $aus.Add($k + '=' + $soll) continue } $b = $k -replace $ZUSATZ, '' if ($b -cne $k -and -not $mehrdeutig.Contains($b) -and $original.TryGetValue($b, [ref]$soll)) { if ($v -cne $soll) { $ueberZusatz++ } $aus.Add($k + '=' + $soll) continue } $aus.Add($z) } # UTF-8 mit BOM wie die Vorlage - daran erkennt der Extractor die Kodierung. [System.IO.File]::WriteAllLines($OutPath, $aus, [System.Text.UTF8Encoding]::new($true)) Write-Verbose "Zurueckgesetzt: $exakt exakt, $ueberZusatz ueber Schluessel ohne Zusatz" # --- Ergebnis pruefen ----------------------------------------------------- $text = [System.IO.File]::ReadAllText($OutPath, [System.Text.Encoding]::UTF8) $reste = [ordered]@{} foreach ($m in ([ordered]@{ 'Region-Zeilen' = '# Region:' 'Reputationsangaben' = '# Expected reputation' 'Mindestreputation' = '# Min\. Reputation' 'Cooldown-Angaben' = '# Mission Cooldown' 'Teilbarkeit' = '# Can mission be shared' 'Bauplan-Wahrscheinlichk.' = '# Blueprint Drop Chance' 'Bauplan-Liste' = 'POSSIBLE BLUEPRINTS FOR THIS MISSION' 'BP-Marker' = '\[BP\]' 'Fremdverweis scmdb.net' = 'scmdb\.net' }).GetEnumerator()) { $n = ([regex]::Matches($text, $m.Value)).Count if ($n -gt 0) { $reste[$m.Key] = $n } } $ergebnis = [pscustomobject]@{ Quelle = $quelle Ziel = $OutPath Sicherungen = $sicherungen Eintraege = $original.Count Exakt = $exakt UeberZusatz = $ueberZusatz Reste = $reste Sauber = ($reste.Count -eq 0) } if (-not $ergebnis.Sauber -and -not $PassThru) { $liste = ($reste.GetEnumerator() | ForEach-Object { "$($_.Key): $($_.Value)" }) -join ', ' Remove-Item -LiteralPath $OutPath -ErrorAction SilentlyContinue throw "Rekonstruktion unvollstaendig - es bleiben Reste der Injektion: $liste. " + "Die Datei wurde verworfen und darf nicht importiert werden." } $ergebnis } # =========================================================================== # Aufraeumen # =========================================================================== # Loescht die Arbeitsdateien eines Laufs. Nur die eigenen, an ihrem Namen # erkennbar - im TEMP liegt auch anderer Leute Arbeit. function Raeume-Arbeit([scriptblock]$log) { # SCQ_KEEP gesetzt: Arbeitsdateien liegen lassen - zum Nachsehen, was # der Lauf gelesen hat (Entwicklung). if ($env:SCQ_KEEP) { if ($log) { & $log "Arbeitsdateien behalten in $ARBEIT_DIR" }; return } if (-not (Test-Path $ARBEIT_DIR)) { return } $weg = @(Get-ChildItem $ARBEIT_DIR -File -ErrorAction SilentlyContinue | Where-Object { $_.Name -like 'global.p4k.*.ini' -or $_.Name -like 'global.restored*.ini' }) if (-not $weg.Count) { return } $mb = [math]::Round((($weg | Measure-Object Length -Sum).Sum) / 1MB, 1) foreach ($f in $weg) { Remove-Item $f.FullName -Force -ErrorAction SilentlyContinue } if ($log) { & $log ("Arbeitsdateien geloescht ({0} Dateien, {1} MB)." -f $weg.Count, $mb) } } # Frueherer Stand legte dieselben Dateien neben der Konfiguration ab. Wer von # dort kommt, hat bis zu 10 MB je Kanal im Roaming-Profil liegen - einmalig # wegraeumen, config.json bleibt unangetastet. function Raeume-Altlasten([scriptblock]$log) { if (-not (Test-Path $CONF_DIR)) { return } $alt = @(Get-ChildItem $CONF_DIR -File -ErrorAction SilentlyContinue | Where-Object { $_.Name -like 'global.p4k.*.ini' -or $_.Name -like 'global.restored*.ini' }) if (-not $alt.Count) { return } $mb = [math]::Round((($alt | Measure-Object Length -Sum).Sum) / 1MB, 1) foreach ($f in $alt) { Remove-Item $f.FullName -Force -ErrorAction SilentlyContinue } if ($log) { & $log ("Aeltere Arbeitsdateien aus %APPDATA% entfernt ({0} Dateien, {1} MB)." -f $alt.Count, $mb) } } # =========================================================================== # Sprachdatei aus der Data.p4k holen # =========================================================================== # # Eine lose data\Localization\...\global.ini legt nicht das Spiel an, sondern # der SC-Deutsch-Launcher. Wer ihn nicht benutzt - und das sind die meisten - # hatte damit gar keine Quelle. Deshalb liest das Werkzeug notfalls direkt aus # der Data.p4k. # # Die p4k ist ein ZIP64-Archiv mit 1,37 Millionen Eintraegen. Die Texte liegen # darin ZStandard-komprimiert (Methode 100). Weder .NET noch das bsdtar von # Windows kommen da heran, deshalb der eigene Entpacker weiter unten. function Lade-Zstd { if (-not ('QuestGuide.Zstd' -as [type])) { Add-Type -TypeDefinition $ZSTD_QUELLE -Language CSharp } } function Lade-Dcb { if (-not ('QuestGuide.Dcb' -as [type])) { Add-Type -TypeDefinition $DCB_QUELLE -Language CSharp } } # Sucht global.ini im Zentralverzeichnis und liefert Lage und Groesse. function Finde-P4kEintrag($p4kPfad, [scriptblock]$log, [string]$muster = 'Data/Localization/*/global.ini') { $fs = [System.IO.File]::Open($p4kPfad, 'Open', 'Read', 'Read') $br = New-Object System.IO.BinaryReader($fs) try { # --- Ende des Zentralverzeichnisses suchen $suchLen = [int][Math]::Min([int64]65557, $fs.Length) $fs.Position = $fs.Length - $suchLen $ende = $br.ReadBytes($suchLen) $eocd = -1 for ($i = $ende.Length - 22; $i -ge 0; $i--) { if ($ende[$i] -eq 0x50 -and $ende[$i+1] -eq 0x4B -and $ende[$i+2] -eq 0x05 -and $ende[$i+3] -eq 0x06) { $eocd = $i; break } } if ($eocd -lt 0) { throw 'Data.p4k: kein Archivende gefunden' } $lok = $eocd - 20 if ($lok -lt 0 -or $ende[$lok] -ne 0x50 -or $ende[$lok+1] -ne 0x4B -or $ende[$lok+2] -ne 0x06 -or $ende[$lok+3] -ne 0x07) { throw 'Data.p4k: kein ZIP64-Verweis' } $z64 = [BitConverter]::ToInt64($ende, $lok + 8) $fs.Position = $z64 + 40 # Groesse, dann Lage des Verzeichnisses $cdGroesse = $br.ReadUInt64() $cdOffset = $br.ReadUInt64() & $log ("Data.p4k: Verzeichnis mit {0:N0} Bytes wird durchsucht..." -f $cdGroesse) # --- Alle global.ini einsammeln, dann die beste Sprache waehlen $fs.Position = $cdOffset $gelesen = 0L $treffer = @{} while ($gelesen -lt $cdGroesse) { $sig = $br.ReadUInt32(); $gelesen += 4 if ($sig -ne 0x02014B50) { break } [void]$br.ReadUInt16(); [void]$br.ReadUInt16(); [void]$br.ReadUInt16() $methode = $br.ReadUInt16() [void]$br.ReadUInt32() [void]$br.ReadUInt32() # Pruefwert - CIG-eigen, nicht CRC-32 $csize = $br.ReadUInt32(); $usize = $br.ReadUInt32() $nlen = $br.ReadUInt16(); $elen = $br.ReadUInt16(); $klen = $br.ReadUInt16() [void]$br.ReadUInt16(); [void]$br.ReadUInt16(); [void]$br.ReadUInt32() $lokOff = $br.ReadUInt32(); $gelesen += 42 $name = [System.Text.Encoding]::UTF8.GetString($br.ReadBytes($nlen)); $gelesen += $nlen $extra = $br.ReadBytes($elen); $gelesen += $elen if ($klen) { [void]$br.ReadBytes($klen); $gelesen += $klen } $pfad = $name.Replace('\', '/') if (-not ($pfad -like $muster)) { continue } # ZIP64-Zusatzfeld aufloesen. Achtung: 0xFFFFFFFF ist in PowerShell # ein Int32 mit dem Wert -1, deshalb gegen [uint32]::MaxValue. $u = [uint64]$usize; $c = [uint64]$csize; $o = [uint64]$lokOff if ($elen -ge 4 -and [BitConverter]::ToUInt16($extra, 0) -eq 1) { $j = 4 if ($usize -eq ([uint32]::MaxValue)) { $u = [BitConverter]::ToUInt64($extra, $j); $j += 8 } if ($csize -eq ([uint32]::MaxValue)) { $c = [BitConverter]::ToUInt64($extra, $j); $j += 8 } if ($lokOff -eq ([uint32]::MaxValue)) { $o = [BitConverter]::ToUInt64($extra, $j); $j += 8 } } $sprache = if ($pfad.Split('/').Length -gt 2) { $pfad.Split('/')[2] } else { $pfad } $treffer[$sprache] = [pscustomobject]@{ Sprache = $sprache; Pfad = $pfad; Methode = $methode; Roh = $c; Klar = $u; Offset = $o } } if (-not $treffer.Count) { throw "Data.p4k: nichts passt auf $muster" } # Ein anderes Muster als die Sprachdateien: genau ein Eintrag, der erste Treffer. if ($muster -ne 'Data/Localization/*/global.ini') { return @($treffer.Values)[0] } # Hier gilt eine ANDERE Reihenfolge als bei der losen Datei. # # Die lose german_(germany)\global.ini stammt vom SC-Deutsch-Launcher # und ist die vollstaendige englische Datei mit uebersetzten Eintraegen # - rund 90.000 Schluessel. CIGs eigene deutsche Fassung in der p4k ist # dagegen nur eine Teiluebersetzung: 22.993 Schluessel, 53 Missionen # statt 599. Vollstaendig ist dort allein Englisch. # # Deshalb: Englisch bevorzugen, sonst die groesste vorhandene Fassung. if ($treffer.ContainsKey('english')) { return $treffer['english'] } return ($treffer.Values | Sort-Object { [int64]$_.Klar } -Descending | Select-Object -First 1) } finally { $br.Dispose(); $fs.Dispose() } } # Holt einen Eintrag aus der Data.p4k und entpackt ihn - der Weg, den die # global.ini geht, fuer jede Datei (Game2.dcb braucht ihn auch). function Lies-P4kEintrag($p4kPfad, $e) { if ($e.Methode -ne 100 -and $e.Methode -ne 0) { throw ("Data.p4k: unbekannte Komprimierung {0}" -f $e.Methode) } $fs = [System.IO.File]::Open($p4kPfad, 'Open', 'Read', 'Read') $br = New-Object System.IO.BinaryReader($fs) try { $fs.Position = [int64]$e.Offset if ($br.ReadUInt32() -ne 0x04034B50) { throw 'Data.p4k: lokaler Kopf unerwartet' } [void]$br.ReadUInt16(); [void]$br.ReadUInt16(); [void]$br.ReadUInt16() [void]$br.ReadUInt32(); [void]$br.ReadUInt32(); [void]$br.ReadUInt32(); [void]$br.ReadUInt32() $ln = $br.ReadUInt16(); $le = $br.ReadUInt16() [void]$br.ReadBytes($ln); [void]$br.ReadBytes($le) $roh = $br.ReadBytes([int]$e.Roh) } finally { $br.Dispose(); $fs.Dispose() } if ($env:SCQ_DEBUG) { Write-Host (" p4k-Eintrag {0}: roh={1} ({2}) klar={3} ({4}) gelesen={5} ({6})" -f $e.Pfad, $e.Roh, $e.Roh.GetType().Name, $e.Klar, $e.Klar.GetType().Name, $roh.Length, $roh.GetType().Name) } # Kein "$x = if ... { } else { }": die if-Anweisung schickt das Byte-Array # durch die Pipeline, und PowerShell rollt es dabei Byte fuer Byte aus - # bei 332 MB ein OutOfMemory, bei 10 MB nur langsam. if ($e.Methode -eq 0) { $klar = $roh } else { $klar = [QuestGuide.Zstd]::Entpacke($roh, 0, $roh.Length, [int64]$e.Klar) } if ($klar.Length -ne [int64]$e.Klar) { throw ("Data.p4k: entpackt {0:N0} Bytes, erwartet {1:N0}" -f $klar.Length, $e.Klar) } # Das Komma haelt das Byte-Array zusammen; ohne es rollt PowerShell 332 # Millionen Bytes einzeln aus. return ,$klar } # Holt die Datei heraus und legt sie neben der Konfiguration ab. function Hole-IniAusP4k($kanal, [scriptblock]$log) { $p4kPfad = Join-Path $kanal.Pfad 'Data.p4k' Lade-Zstd $e = Finde-P4kEintrag $p4kPfad $log & $log (" {0}: {1:N0} Bytes komprimiert, {2:N0} entpackt" -f $e.Sprache, $e.Roh, $e.Klar) $klar = Lies-P4kEintrag $p4kPfad $e if (-not (Test-Path $ARBEIT_DIR)) { New-Item -ItemType Directory -Path $ARBEIT_DIR -Force | Out-Null } $ziel = Join-Path $ARBEIT_DIR ("global.p4k.{0}.ini" -f $kanal.Name) [System.IO.File]::WriteAllBytes($ziel, $klar) & $log " entpackt nach $ziel" return $ziel } # =========================================================================== # Game2.dcb: was der Auftragsmakler ueber eine Mission weiss # =========================================================================== # # global.ini hat die Texte, Game2.dcb die Zahlen: Belohnung, Frist, # Wartezeiten, Reputationsanforderung und -belohnung, Vormissionen. Bisher # kamen sie ueber api.star-citizen.wiki (das dieselbe Datei liest) und mit # Verzoegerung nach jedem Patch; hier kommen sie aus der Installation, die # gerade laeuft. Nur bei einer Data.p4k - eine lose Sprachdatei allein # reicht dafuer nicht. Der Eintrag heisst MissionBrokerEntry; Verweise # (Missionsart, Ort, Reputationsstufen, Vormissionen) sind GUIDs auf # andere Records und werden hier zu Namen. # # Die Zuordnung zur Mission der Seite: der Makler traegt den Titel- # Schluessel (@Headhunters_claimsweep_title_easy); dieselbe Datei hat # oben die Fassungen mit ihrem Schluessel und Praefix erzeugt. # Die Klasse eines Gegenstands aus seinem Beschreibungsschluessel der # global.ini - EINE Regel fuer den Datenkopf-Block (items) und die Entity- # Zuordnung (dcb_classes), damit beide dieselbe Klasse ergeben: # item_Desc_cds_legacy_heavy_armor_01_core -> _cds_legacy_heavy_armor_01_core # item_Mining_MiningSack_DRAK_Desc -> Mining_MiningSack_DRAK function Klasse-AusDescSchluessel([string]$k) { if ($k.StartsWith('item_Desc', [System.StringComparison]::OrdinalIgnoreCase)) { return $k.Substring(9) } $klasse = [regex]::Replace($k, '(?i)^item_', '') return [regex]::Replace($klasse, '(?i)_?desc_?', '', 1).Trim('_') } # An item's type: the "Item Type" of its text header - the game's word # wins. Where the game writes none, it comes from the class or the header's # other lines, or stays empty: # - Missiles and torpedoes name only manufacturer, size and target signal; # the kind is in the class (MISL_ ship, GMISL_ ground vehicle, BOMB_) # and in the name (torpedo from S9 on). # - Weapon attachments: "Type: Holographic" and "Attachment Point: Optic" # - the kind is where it goes; the type stays in the specs. Two optics # lack the attachment point and say only how they look ("Type: # Monitor"), so optics go by class. # - Otherwise the game's "Type", where it writes that instead of "Item # Type" (companion 0.13.3, Alex 07.10.2026): Quantum Dampener, Decoy # (flares), Noise (chaff), Laser Beam, Flight Blade - 27 items in LIVE # 4.10 that had no type. # - CureLife's OxyPen (crlf_consumable_oxygen_01, LIVE 4.10): its twelve # sibling pens say "Medical Consumable", it says nothing - and stood in # no item table on the site (companion 0.13.3, 07.10.2026). # - 29 more in LIVE 4.10 say nothing of their kind (Alex, 07.10.2026, # chose the names): named after their siblings with a type where there # are any - Medical Device like the ParaMed, Turret like the game's own # utility turrets (the Prowler's tractor arm, the Cutlass Red's scanner), # Missile Rack, Shield Generator by what the text says it is - and anew # where there are none: Deployable, Mine, Flare. # Since 0.15.1 Set-DcbItemTypes overrides the Turret rule where Game2.dcb # attaches an item as UtilityTurret - in LIVE 4.10.2 only the ROC Mining # Arm; the Prowler's arm and the Cutlass Red's scanner have no class # there and stay "Turret". function Get-ItemType($header, [string]$class, [string]$name, [string]$description = '') { if ($header.Contains('Item Type') -and $header['Item Type']) { return $header['Item Type'] } # Ships offered in shops (..._shop, Wikelo's ship mods) carry an item # header too - "Manufacturer, Focus" - and are no items: no rule below # may make one of them (the Aurora LN's text praises its shield generator). if ($class -match '(?i)_shop$|^TheCollector_ShipMod_' -or $header.Contains('Focus')) { return $null } if ($class -match '^GMISL_') { return 'Ground Vehicle Missile' } if ($class -match '^MISL_') { if ($name -match '(?i)torpedo') { return 'Torpedo' } return 'Missile' } if ($class -match '^BOMB_') { return 'Bomb' } if ($header.Contains('Attachment Point')) { return "$($header['Attachment Point']) attachment" } if ($class -match '_optics_') { return 'Optic attachment' } if ($header.Contains('Type') -and $header['Type']) { return $header['Type'] } if ($class -match '^crlf_consumable_') { return 'Medical Consumable' } if ($class -match '^crlf_medicalaid_') { return 'Medical Device' } if ($class -match '(?i)deployable|hologram') { return 'Deployable' } if ($class -match '(?i)areadenial') { return 'Mine' } if ($class -match '(?i)flare' -or $name -match '(?i)flare') { return 'Flare' } if ($class -match '(?i)turret' -or $name -match '(?i)\bturret\b') { return 'Turret' } if ($name -match '(?i)\b(platform|rack)\b' -and $description -match '(?i)missile') { return 'Missile Rack' } if ($description -match '(?i)shield generator') { return 'Shield Generator' } return $null } # The type Game2.dcb gives where our rules only guessed (companion 0.15.1, # Alex 15.10.2026): the class name rule above made the ROC Mining Arm a # "Turret", but the game attaches it as "UtilityTurret" (AttachDef.Type) - # like the MOLE and MOTH arms, the mining gimbals and the Hermes tractor # turret, none a weapon turret; of these only the ROC arm has item text in # LIVE 4.10.2 (dry run 15.10.2026: 1 item typed, 61 scenario tiers). A type the # global.ini writes ($gameTyped, keys in small letters) stays. Returns how # many items changed. function Set-DcbItemTypes($items, $classes, $gameTyped) { $utility = @{} foreach ($c in @($classes)) { if ($c -and [string]$c.type -eq 'UtilityTurret') { $utility[([string]$c.class).ToLowerInvariant()] = $true } } $changed = 0 foreach ($i in @($items)) { $k = ([string]$i.key).ToLowerInvariant() if (-not $utility.ContainsKey($k) -or $gameTyped.ContainsKey($k)) { continue } if ($i.item_type -ne 'Utility Turret') { $i.item_type = 'Utility Turret'; $changed++ } } return $changed } # The tiers of an event's progress (companion 0.15.1, Alex 15.10.2026; found # by the session "Versefiles.com Weiterentwicklung"): a ScenarioProgress # record of Game2.dcb carries, per faction and track, the points each reward # tier needs - Iasi: 2000/4000/6500/10000 for Transport, Collection and # Defense. One row per tier, the raw keys as the game writes them (RG's track # is "LOC_PLACEHOLDER", CleanAir's badges skip _2). LIVE 4.10.2: 5 records, # 61 rows. The points a contract earns are not in the files - the server # hands them out. function Get-ScenarioRows([string]$recordName, $record) { $short = { param($n) if ($n) { $s = [string]$n; return $s.Substring($s.IndexOf('.') + 1) }; return $null } $rows = New-Object System.Collections.ArrayList if ($record -isnot [System.Collections.IDictionary]) { return $rows } foreach ($ft in @($record['factionRewardTiers'])) { if ($ft -isnot [System.Collections.IDictionary]) { continue } $faction = if ($ft['faction'] -is [System.Collections.IDictionary]) { & $short $ft['faction']['__name'] } else { $null } foreach ($p in @($ft['tierProgressions'])) { if ($p -isnot [System.Collections.IDictionary]) { continue } $track = ([string]$p['progressionText']).TrimStart('@') $ct = $p['completionType'] $completion = if ($ct -is [System.Collections.IDictionary] -and $ct['__type']) { ([string]$ct['__type']) -replace '^CompletionType_', '' } else { $null } $tier = 0 foreach ($t in @($p['tierRewards'])) { if ($t -isnot [System.Collections.IDictionary]) { continue } $tier++ $badge = [string]$t['badgeToAward'] [void]$rows.Add([ordered]@{ scenario = & $short $recordName faction = $faction track_key = $(if ($track) { $track } else { $null }) tier = $tier min_points = $t['minPoints'] badge = $(if ($badge -and $badge -ne 'None') { $badge } else { $null }) completion_type = $completion }) } } } return $rows } # The texts of a generator contract - Title, Description, Contractor, as # localisation keys without the "@": the contract's own paramOverrides, else # its handler's contractParams (companion 0.13.1, Alex 05.10.2026: 30 # contracts carry their title only on the handler - Adagio's salvage # rights, BHG ship bounties, TarPits - and were skipped). An uninitialised # key counts as none. function Get-ContractStringParams($contract, $handler) { $texts = @{ Title = $null; Description = $null; Contractor = $null } foreach ($source in @($handler['contractParams'], $contract['paramOverrides'])) { if ($source -isnot [System.Collections.IDictionary]) { continue } foreach ($p in @($source['stringParamOverrides'])) { if ($p -isnot [System.Collections.IDictionary]) { continue } $name = [string]$p['param'] $value = ([string]$p['value']).TrimStart('@') if ($texts.ContainsKey($name) -and $value -and $value -ne 'LOC_UNINITIALIZED') { $texts[$name] = $value } } } return $texts } function Lese-Broker($kanal, $schluesselZuPrefix, $map, [scriptblock]$log) { $p4kPfad = Join-Path $kanal.Pfad 'Data.p4k' if (-not (Test-Path $p4kPfad)) { & $log 'Keine Data.p4k - Game2.dcb bleibt aus.'; return $null } Lade-Zstd; Lade-Dcb $e = Finde-P4kEintrag $p4kPfad $log 'Data/Game2.dcb' & $log (" Game2.dcb: {0:N0} Bytes komprimiert, {1:N0} entpackt" -f $e.Roh, $e.Klar) $klar = Lies-P4kEintrag $p4kPfad $e $d = New-Object QuestGuide.Dcb (,$klar) $klar = $null & $log (" {0:N0} Records" -f $d.RecordCount) # Name eines Verweises ohne den Struct-Vorsatz: "MissionType.Mercenary" -> "Mercenary" $kurz = { param($v) if ($v -is [hashtable] -or $v -is [System.Collections.IDictionary]) { $n = $v['__name']; if ($n) { return $n.Substring($n.IndexOf('.') + 1) } }; return $null } $refBetrag = { param($v) if ($v -is [System.Collections.IDictionary] -and $v['__ref']) { $r = $d.ByGuid($v['__ref']); if ($r) { $x = $d.Read($r, 1); if ($x.ContainsKey('reputationAmount')) { return [int]$x['reputationAmount'] } } }; return $null } # Der Anzeigename eines Verweises, wie das Spiel ihn druckt: Record lesen, # displayName ist ein Schluessel der global.ini (@HeadHunters_RepUI_Name). $anzeige = { param($v) if ($v -is [System.Collections.IDictionary] -and $v['__ref']) { $r = $d.ByGuid($v['__ref']) if ($r) { $x = $d.Read($r, 1) $k = ([string]$x['displayName']).TrimStart('@') if ($k -and $map.ContainsKey($k)) { $t = ([string]$map[$k]).Trim(); if ($t -and $t -notmatch '^(LOC_|@)') { return $t } } } } return $null } # ------------------------------------------------------------------- # Auftragnehmer-Platzhalter: ~mission(Contractor|SearchBodySpaceTitleHard) # ------------------------------------------------------------------- # # Viele Makler- und Vertragstitel sind nicht der Text, den der Spieler # sieht, sondern ein Platzhalter: der Auftragnehmer setzt seinen # eigenen ein. Die Tabelle dafuer steht in MissionOrganization. # (stringVariants: Tag -> Schluessel, "SearchBodySpaceTitleHard" -> # @vaughn_searchbody_space_H_Title_001), die Organisation haengt ueber # ihr factionReputation am Handler des Vertrags bzw. am MissionGiver # des Maklereintrags. Ohne diese Aufloesung landete der Vertrag beim # prozeduralen Platzhalter-Eintrag der Seite statt bei "Down For the # Count" - und der bekam seine Zahlen vom Wiki, das genau diesen # Weg geht. Seit v1.15 loest das Werkzeug ihn selbst auf. $tagName = @{} foreach ($tr in $d.RecordsOf('Tag')) { $t = $d.Read($tr, 0); if ($t['tagName']) { $tagName[$tr.Name] = [string]$t['tagName'] } } $orgVarianten = @{} foreach ($or in $d.RecordsOf('MissionOrganization')) { $o = $d.Read($or, 4) $fr = $(if ($o['factionReputation'] -is [System.Collections.IDictionary]) { [string]$o['factionReputation']['__name'] } else { '' }) if (-not $fr) { continue } $tbl = @{} # stringVariants ist eine Liste von Containern (MissionStringVariants), die Varianten liegen darin. $alleVarianten = New-Object System.Collections.ArrayList foreach ($cont in @($o['stringVariants'])) { if ($cont -is [System.Collections.IDictionary]) { foreach ($sv in @($cont['variants'])) { [void]$alleVarianten.Add($sv) } } } foreach ($sv in $alleVarianten) { if ($sv -isnot [System.Collections.IDictionary] -or $sv['tag'] -isnot [System.Collections.IDictionary]) { continue } $tn = $tagName[[string]$sv['tag']['__name']] $str = ([string]$sv['string']).TrimStart('@') if ($tn -and $str -and $str -ne 'LOC_UNINITIALIZED') { $tbl[$tn] = $str } } if ($tbl.Count) { $orgVarianten[$fr] = $tbl } } & $log " $($orgVarianten.Count) Auftragnehmer mit eigenen Titeln." # Der Schluessel, den der Spieler sieht: den Platzhalter ueber die # Organisation aufloesen, sonst der Schluessel selbst. $loeseTitel = { param($key, $factionRepName) if (-not $key -or -not $map.ContainsKey($key)) { return $key } $txt = [string]$map[$key] if ($txt -notmatch '^~mission\(Contractor\|([A-Za-z0-9_]+)\)') { return $key } $tn = $Matches[1] if ($factionRepName -and $orgVarianten.ContainsKey($factionRepName) -and $orgVarianten[$factionRepName].ContainsKey($tn)) { return $orgVarianten[$factionRepName][$tn] } return $key } $missionGiverRep = @{} $ergebnisse = @('completed', 'in_progress', 'failed', 'abandoned', 'withdrawn') # Anforderungen: ein Ausdruck aus And/Or-Knoten und Blaettern; nur die # Blaetter zaehlen, der Baum wird flach. $flach = $null $flach = { param($x, $liste) if ($x -isnot [System.Collections.IDictionary]) { return } if ($x.ContainsKey('reputationScope') -and $x['reputationScope']) { [void]$liste.Add([ordered]@{ scope = & $kurz $x['reputationScope'] faction = & $kurz $x['factionReputation'] faction_name = & $anzeige $x['factionReputation'] comparison = $x['comparison'] standing = & $kurz $x['standing'] }) return } foreach ($k in @($x.Keys)) { $v = $x[$k] if ($v -is [System.Collections.IList]) { foreach ($y in $v) { & $flach $y $liste } } elseif ($v -is [System.Collections.IDictionary]) { & $flach $v $liste } } } # ------------------------------------------------------------------- # Der Ablauf, soweit der Client ihn kennt # ------------------------------------------------------------------- # # Die Ziele einer Mission - Texte, Phasen, Bedingungen - stehen in den # Subsumption-Modulen (Libs/Subsumption/Missions/PU/...), und die # liefert CIG dem Client NICHT aus: in der Data.p4k liegt kein PU/. # Was Game2.dcb hat, ist das Geruest: objectiveTokens (die Phasen, # mit Handler-Modul und, selten, einem Textschluessel) und # missionFlow.triggers (die Regeln dazwischen, oft mit einem Klartext # des Entwicklers: "When [Hauling] abandoned and CollectedCargo is # set -> Start [ReturnGoods]"). Von 1.541 Marken tragen 28 einen # Text; das hier ersetzt keinen Walkthrough. Mitgeschickt wird es nur, # wenn mehr drin ist als "eine Marke, fertig = fertig": mindestens # zwei Phasen, ein Handler oder eine Regel jenseits des Endes. # Regeln zeigen per __weak ("ObjectiveToken#17") auf die Marken; der # Leser haengt jeder Instanz ihren Index an (__index), das loest sie. $ablaufBed = $null $ablaufBed = { param($c, $marken) if ($c -isnot [System.Collections.IDictionary]) { return $null } $o = [ordered]@{ type = (([string]$c['__type']) -replace '^MissionFlow(Condition|Action)_', '') } if ($c['objectiveToken'] -is [System.Collections.IDictionary]) { $w = [string]$c['objectiveToken']['__weak']; $o['token'] = $(if ($marken.ContainsKey($w)) { $marken[$w] } else { $w }) } foreach ($k in 'tokenState', 'toState', 'variableName', 'isSet', 'value', 'comparison') { if ($c.ContainsKey($k) -and $c[$k] -ne $null -and -not ($c[$k] -is [System.Collections.IDictionary]) -and -not ($c[$k] -is [System.Collections.IList])) { $o[$k] = $c[$k] } } if ($c['conditions'] -is [System.Collections.IList]) { $o['all'] = @(foreach ($x in $c['conditions']) { $y = & $ablaufBed $x $marken; if ($y) { $y } }) } return $o } $ablauf = { param($x) $toks = @($x['objectiveTokens'] | Where-Object { $_ -is [System.Collections.IDictionary] }) if (-not $toks.Count) { return $null } $marken = @{} $phasen = New-Object System.Collections.ArrayList foreach ($t in $toks) { $name = [string]$t['debugName'] $marken['ObjectiveToken#' + $t['__index']] = $name $di = $t['displayInfo']; $h = $t['objectiveHandler'] $mod = ''; if ($h -is [System.Collections.IDictionary]) { $mod = [string]$h['module']; if ($mod) { $mod = ($mod.Substring($mod.LastIndexOf('/') + 1)) -replace '\.xml$', '' } } $sd = ''; $ld = ''; $kat = '' if ($di -is [System.Collections.IDictionary]) { $sd = ([string]$di['shortDescription']).TrimStart('@'); $ld = ([string]$di['longDescription']).TrimStart('@'); $kat = [string]$di['category'] } if ($sd -eq 'LOC_UNINITIALIZED') { $sd = '' } if ($ld -eq 'LOC_UNINITIALIZED') { $ld = '' } [void]$phasen.Add([ordered]@{ name = $name; handler = $mod; starts_active = [bool]$t['startsActive']; short_key = $sd; long_key = $ld; category = $kat; phases = @($t['childMissionPhases']).Count }) } $regeln = New-Object System.Collections.ArrayList $mf = $x['missionFlow'] if ($mf -is [System.Collections.IDictionary]) { foreach ($tr in @($mf['triggers'])) { if ($tr -isnot [System.Collections.IDictionary]) { continue } [void]$regeln.Add([ordered]@{ text = (([string]$tr['description']) -replace '>', '>' -replace '<', '<' -replace '&', '&').Trim() condition = & $ablaufBed $tr['condition'] $marken action = & $ablaufBed $tr['action'] $marken }) } } # Trivial: eine Marke ohne Handler, und jede Regel sagt nur "Marke zu # Ende = Mission zu Ende" (auch in ein AND mit einem Kind gewickelt). $schlicht = $null $schlicht = { param($c) if ($c -eq $null) { return $false }; if ($c.type -eq 'TokenState') { return $true }; if (($c.type -eq 'AND' -or $c.type -eq 'OR') -and $c.Contains('all') -and @($c.all).Count -eq 1) { return (& $schlicht @($c.all)[0]) }; return $false } $trivial = ($phasen.Count -eq 1 -and -not $phasen[0].handler -and -not $phasen[0].short_key -and $phasen[0].phases -eq 0) if ($trivial) { foreach ($r in $regeln) { if (-not (& $schlicht $r.condition) -or $r.action -eq $null -or $r.action.type -ne 'StateChange') { $trivial = $false; break } } } if ($trivial) { return $null } return [ordered]@{ phases = @($phasen); rules = @($regeln) } } $liste = New-Object System.Collections.ArrayList foreach ($rec in $d.RecordsOf('MissionBrokerEntry')) { $m = $d.Read($rec, 5) # notForRelease wird MITGESCHICKT, nicht mehr ausgefiltert (seit # v1.15): PU_DestroyNarcotics_Pyro und die Pyro-Kurierlaeufe der CFP # tragen die Marke - und laufen im Spiel, das Wiki fuehrt sie mit # Belohnung. Die Seite zeigt die Marke als Hinweis. $nfr = [bool]$m['notForRelease'] $titelKey = ([string]$m['title']).TrimStart('@') if (-not $titelKey -or $titelKey -eq 'LOC_UNINITIALIZED') { continue } $anf = New-Object System.Collections.ArrayList & $flach $m['reputationRequirements'] $anf $bel = New-Object System.Collections.ArrayList $i = 0 foreach ($ergebnisListe in @($m['missionResultReputationRewards'])) { if ($ergebnisListe -is [System.Collections.IDictionary]) { foreach ($a in @($ergebnisListe['reputationAmounts'])) { if ($a -isnot [System.Collections.IDictionary]) { continue } $betrag = & $refBetrag $a['reward'] if ($betrag -eq $null) { continue } [void]$bel.Add([ordered]@{ outcome = $ergebnisse[[Math]::Min($i, 4)]; scope = & $kurz $a['reputationScope']; faction = & $kurz $a['factionReputation']; faction_name = & $anzeige $a['factionReputation']; amount = $betrag }) } } $i++ } $vor = New-Object System.Collections.ArrayList foreach ($r in @($m['requiredMissions'])) { $n = & $kurz $r; if ($n) { [void]$vor.Add($n) } } $frist = $m['missionDeadline'] # Platzhalter-Titel: ueber den MissionGiver zur Organisation. if ($m['missionGiverRecord'] -is [System.Collections.IDictionary] -and $m['missionGiverRecord']['__ref']) { $mgRef = [string]$m['missionGiverRecord']['__ref'] if (-not $missionGiverRep.ContainsKey($mgRef)) { $mgr = $d.ByGuid($mgRef); $missionGiverRep[$mgRef] = $(if ($mgr) { $mgx = $d.Read($mgr, 1); if ($mgx['reputation'] -is [System.Collections.IDictionary]) { [string]$mgx['reputation']['__name'] } else { '' } } else { '' }) } $titelKey = & $loeseTitel $titelKey $missionGiverRep[$mgRef] } $prefix = $null if ($schluesselZuPrefix.ContainsKey($titelKey.ToLowerInvariant())) { $prefix = $schluesselZuPrefix[$titelKey.ToLowerInvariant()] } [void]$liste.Add([ordered]@{ record = $rec.Name.Substring($rec.Name.IndexOf('.') + 1) system = 'broker' not_for_release = $nfr prefix = $prefix title_key = $titelKey desc_key = ([string]$m['description']).TrimStart('@') giver_key = ([string]$m['missionGiver']).TrimStart('@') giver = & $kurz $m['missionGiverRecord'] type = & $kurz $m['type'] location = & $kurz $m['locationMissionAvailable'] difficulty = [int]$m['missionDifficulty'] reward = [int]$m['missionReward']['reward'] reward_max = [int]$m['missionReward']['max'] currency = [string]$m['missionReward']['currencyType'] lawful = [bool]$m['lawfulMission'] once_only = [bool]$m['onceOnly'] can_share = [bool]$m['canBeShared'] max_players = [int]$m['maxPlayersPerInstance'] deadline_s = $(if ($frist -is [System.Collections.IDictionary]) { [int]$frist['missionCompletionTime'] } else { 0 }) deadline_result = $(if ($frist -is [System.Collections.IDictionary]) { [string]$frist['missionResultAfterTimerEnd'] } else { $null }) cooldown_s = [int]$m['personalCooldownTime'] respawn_s = [int]$m['respawnTime'] abandon_cooldown_s = [int]$m['abandonedCooldownTime'] wanted_min = $(if ($m['reputationPrerequisites'] -is [System.Collections.IDictionary] -and $m['reputationPrerequisites']['wantedLevel'] -is [System.Collections.IDictionary]) { [int]$m['reputationPrerequisites']['wantedLevel']['minValue'] } else { $null }) wanted_max = $(if ($m['reputationPrerequisites'] -is [System.Collections.IDictionary] -and $m['reputationPrerequisites']['wantedLevel'] -is [System.Collections.IDictionary]) { [int]$m['reputationPrerequisites']['wantedLevel']['maxValue'] } else { $null }) rep_requirements = @($anf) rep_rewards = @($bel) required_missions = @($vor) flow = & $ablauf $m }) } # ------------------------------------------------------------------- # Das Vertragssystem: ContractGenerator -> Handler -> Vertraege # ------------------------------------------------------------------- # # Seit 4.x haengen die Story- und Karrierevertraege (Battaglia, Vaughn, # die Pyro-Gilden) nicht mehr am Makler, sondern an Generatoren: ein # Handler je Karrierestufe ("Battaglia_BoardShip_Rank1") mit Vertraegen, # die ein Template (Ablauf) mit Titel, Zeit, Schwierigkeit und # Ergebnissen belegen. Die Belohnung ist hier meist BERECHNET - aus den # vier Schwierigkeitsachsen und dem Profil, per Code, nicht per Datei - # und bleibt deshalb leer; die Achsen selbst kommen mit. $ergebnisFlags = @('completed', 'in_progress', 'failed', 'abandoned', 'withdrawn') $achse = { param($v) if ($v -is [string] -and $v -match '_(\d+)$') { return [ordered]@{ level = [int]$Matches[1]; label = (($v -replace '_\d+$', '') -replace '_', ' ') } }; return $null } $vertraege = New-Object System.Collections.ArrayList $vergebenRec = New-Object 'System.Collections.Generic.HashSet[string]' # Die Vorlage eines Vertrags ist ein Verweis auf einen ContractTemplate- # Record (Ablauf, Auftragsart, legal/illegal) - je GUID einmal gelesen. $vorlagen = @{} foreach ($gen in $d.RecordsOf('ContractGenerator')) { $g = $d.Read($gen, 7) $genName = $gen.Name.Substring($gen.Name.IndexOf('.') + 1) foreach ($h in @($g['generators'])) { if ($h -isnot [System.Collections.IDictionary]) { continue } # Ein Handler mit notForRelease (Battaglia_ScanRocks_Rank0) traegt # Vertraege, die im Spiel laufen - die Marke wird mitgeschickt, # nicht gefiltert (siehe den Makler oben). $handlerNfr = [bool]$h['notForRelease'] -or [bool]$g['notForRelease'] $handler = [string]$h['debugName'] $rang = $null; if ($handler -match '_Rank(\d+)') { $rang = [int]$Matches[1] } $verf = $h['defaultAvailability'] $alle = New-Object System.Collections.ArrayList foreach ($c in @($h['introContracts'])) { if ($c -is [System.Collections.IDictionary]) { [void]$alle.Add(@{ c = $c; intro = $true }) } } foreach ($c in @($h['contracts'])) { if ($c -is [System.Collections.IDictionary]) { [void]$alle.Add(@{ c = $c; intro = $false }) } } foreach ($paar in $alle) { $c = $paar.c $nfr = $handlerNfr -or [bool]$c['notForRelease'] # The contract's own texts, else its handler's (0.13.1). $texts = Get-ContractStringParams $c $h $titelKey = $texts.Title; $descKey = $texts.Description; $geberKey = $texts.Contractor if (-not $titelKey) { continue } $tpl = $c['template'] $tplName = $null; $typ = $null; $illegal = $false; $vorlage = $null if ($tpl -is [System.Collections.IDictionary] -and $tpl['__ref']) { $tg = [string]$tpl['__ref'] if (-not $vorlagen.ContainsKey($tg)) { $tr = $d.ByGuid($tg); $vorlagen[$tg] = $(if ($tr) { @{ name = $tr.Name.Substring($tr.Name.IndexOf('.') + 1); data = $d.Read($tr, 6) } } else { $null }) } if ($vorlagen[$tg]) { $tplName = $vorlagen[$tg].name; $vorlage = $vorlagen[$tg].data } } elseif ($tpl -is [System.Collections.IDictionary]) { $tplName = [string]$tpl['__type']; $vorlage = $tpl } if ($vorlage -is [System.Collections.IDictionary] -and $vorlage['contractDisplayInfo'] -is [System.Collections.IDictionary]) { $typ = & $kurz $vorlage['contractDisplayInfo']['type']; $illegal = [bool]$vorlage['contractDisplayInfo']['illegal'] } $res = $c['contractResults'] $zeit = 0; $diff = $null; $profil = $null if ($res -is [System.Collections.IDictionary]) { $zeit = [int]$res['timeToComplete'] $dd = $res['difficulty'] if ($dd -is [System.Collections.IDictionary]) { $profil = & $kurz $dd['difficultyProfile'] $diff = [ordered]@{ mechanical = & $achse $dd['mechanicalSkill']; mental = & $achse $dd['mentalLoad']; risk = & $achse $dd['riskOfLoss']; knowledge = & $achse $dd['gameKnowledge'] } } } $belohnung = $null; $berechnet = $false; $waehrung = $null $bel = New-Object System.Collections.ArrayList $bps = New-Object System.Collections.ArrayList if ($res -is [System.Collections.IDictionary]) { foreach ($e in @($res['contractResults'])) { if ($e -isnot [System.Collections.IDictionary]) { continue } $t = [string]$e['__type'] $ausgaenge = @(); $i = 0 foreach ($fl in @($e['missionResults'])) { if ($fl -eq $true) { $ausgaenge += $ergebnisFlags[[Math]::Min($i, 4)] }; $i++ } if ($t -eq 'ContractResult_CalculatedReward') { $berechnet = $true } elseif ($t -eq 'ContractResult_Reward' -and $e['contractReward'] -is [System.Collections.IDictionary]) { $belohnung = [int]$e['contractReward']['reward']; $waehrung = [string]$e['contractReward']['currencyType'] } elseif ($t -eq 'ContractResult_LegacyReputation' -and $e['contractResultReputationAmounts'] -is [System.Collections.IDictionary]) { $a = $e['contractResultReputationAmounts'] $betrag = & $refBetrag $a['reward'] if ($betrag -ne $null) { foreach ($o in $ausgaenge) { [void]$bel.Add([ordered]@{ outcome = $o; scope = & $kurz $a['reputationScope']; faction = & $kurz $a['factionReputation']; faction_name = & $anzeige $a['factionReputation']; amount = $betrag }) } } } elseif ($t -match 'Blueprint') { foreach ($k in @($e.Keys)) { foreach ($b in @($e[$k])) { if ($b -is [System.Collections.IDictionary] -and $b.ContainsKey('blueprintRecord')) { $n = & $kurz $b['blueprintRecord']; if ($n) { [void]$bps.Add($n) } } } } } } } # Voraussetzungen: Ruf (Stufe min/max je Scope), Crime Stat $anf = New-Object System.Collections.ArrayList $wantedMin = $null; $wantedMax = $null foreach ($p in @($c['additionalPrerequisites'])) { if ($p -isnot [System.Collections.IDictionary]) { continue } if ($p['__type'] -eq 'ContractPrerequisite_Reputation') { $mn = & $kurz $p['minStanding']; $mx = & $kurz $p['maxStanding'] if ($mn) { [void]$anf.Add([ordered]@{ scope = & $kurz $p['scope']; faction = & $kurz $p['factionReputation']; faction_name = & $anzeige $p['factionReputation']; comparison = 'GreaterThanOrEqual'; standing = $mn }) } if ($mx) { [void]$anf.Add([ordered]@{ scope = & $kurz $p['scope']; faction = & $kurz $p['factionReputation']; faction_name = & $anzeige $p['factionReputation']; comparison = 'LessThanOrEqual'; standing = $mx }) } } elseif ($p['__type'] -eq 'ContractPrerequisite_CrimeStat') { $wantedMin = [int]$p['minCrimeStat']; $wantedMax = [int]$p['maxCrimeStat'] } } $mn = & $kurz $c['minStanding']; $mx = & $kurz $c['maxStanding'] if ($mn) { [void]$anf.Add([ordered]@{ scope = & $kurz $h['reputationScope']; faction = & $kurz $h['factionReputation']; faction_name = & $anzeige $h['factionReputation']; comparison = 'GreaterThanOrEqual'; standing = $mn }) } if ($mx) { [void]$anf.Add([ordered]@{ scope = & $kurz $h['reputationScope']; faction = & $kurz $h['factionReputation']; faction_name = & $anzeige $h['factionReputation']; comparison = 'LessThanOrEqual'; standing = $mx }) } # Platzhalter-Titel: ueber die Fraktion des Handlers zur Organisation. $titelKey = & $loeseTitel $titelKey $(if ($h['factionReputation'] -is [System.Collections.IDictionary]) { [string]$h['factionReputation']['__name'] } else { '' }) $prefix = $null if ($schluesselZuPrefix.ContainsKey($titelKey.ToLowerInvariant())) { $prefix = $schluesselZuPrefix[$titelKey.ToLowerInvariant()] } $gp = $c['generationParams']; $lt = $c['contractLifeTime'] # Ein Name je Vertrag: Generator/Handler/Vertrag - derselbe Vertragsname # kommt in mehreren Handlern vor (Rangstufen), einmal auch doppelt. $recName = "$genName/$handler/$([string]$c['debugName'])" $n = 2; while (-not $vergebenRec.Add($recName)) { $recName = "$genName/$handler/$([string]$c['debugName'])#$n"; $n++ } [void]$vertraege.Add([ordered]@{ record = $recName not_for_release = $nfr system = 'contract' prefix = $prefix title_key = $titelKey desc_key = $descKey giver_key = $geberKey giver = $null generator = $genName handler = $handler rank = $rang intro = [bool]$paar.intro career_scope = & $kurz $h['reputationScope'] career_faction = & $kurz $h['factionReputation'] template = $tplName type = $typ lawful = -not $illegal reward = $belohnung reward_calculated = $berechnet currency = $waehrung time_min = $zeit difficulty = $diff difficulty_profile = $profil once_only = $(if ($verf -is [System.Collections.IDictionary]) { [bool]$verf['onceOnly'] } else { $false }) max_players = $(if ($verf -is [System.Collections.IDictionary]) { [int]$verf['maxPlayersPerInstance'] } else { $null }) cooldown_s = $(if ($verf -is [System.Collections.IDictionary] -and $verf['hasPersonalCooldown']) { [int]$verf['personalCooldownTime'] } else { 0 }) abandon_cooldown_s = $(if ($verf -is [System.Collections.IDictionary]) { [int]$verf['abandonedCooldownTime'] } else { 0 }) respawn_s = $(if ($gp -is [System.Collections.IDictionary]) { [int]$gp['respawnTime'] } else { 0 }) lifetime_min = $(if ($lt -is [System.Collections.IDictionary]) { [int]$lt['instanceLifeTime'] } else { 0 }) wanted_min = $wantedMin wanted_max = $wantedMax rep_requirements = @($anf) rep_rewards = @($bel) blueprints = @($bps | Select-Object -Unique) required_missions = @() flow = $(if ($vorlage -is [System.Collections.IDictionary]) { & $ablauf $vorlage } else { $null }) }) } } } & $log " $($vertraege.Count) Vertraege aus Generatoren, $(@($vertraege | Where-Object { $_.prefix }).Count) davon einer Mission zugeordnet." foreach ($v in $vertraege) { [void]$liste.Add($v) } # ------------------------------------------------------------------- # Gegenstaende: die Komponentenwerte, wie das Spiel sie rechnet # ------------------------------------------------------------------- # # Jeder Gegenstand ist eine EntityClassDefinition mit einer Liste von # Komponenten; die Zahlen stehen in typisierten Parametern # (SCItemShieldGeneratorParams: MaxShieldHealth, MaxShieldRegen ...). # Das Wiki liest dieselbe Datei; ab hier kommen die Werte aus der # laufenden Installation. Mitgeschickt werden die Skalare der # bekannten Komponenten und eine Ebene darunter - die Zuordnung zu # Spalten macht der Server (App\Support\GameSpecs), damit ein neues # Feld kein neues Werkzeug braucht. Schiffskomponenten (Kraftwerk, # Kuehler, Schild, Quantenantrieb, Radar) seit v1.11; Waffen und # Raketen seit v1.13 - siehe unten. # # WAFFEN haengen an zwei Records: Die Feuermodi (Kadenz, Hitze, Kugeln # je Schuss) stehen in SCItemWeaponComponentParams.fireActions der # Waffe; Schaden, Geschwindigkeit und Lebensdauer des Geschosses in # einem AmmoParams-Record, auf den der Munitionsbehaelter zeigt. # Schiffswaffen und Raketenwerfer tragen den Behaelter selbst # (SAmmoContainerComponentParams); Handwaffen zeigen mit # ammoContainerRecord auf ihr Magazin, eine eigene Entity, die den # Behaelter traegt (maxAmmoCount = Magazingroesse). Beides wird hier # aufgeloest und als "weapon" mitgeschickt: fire_modes (die Feuer- # aktionen, rekursiv auf ihre Zahlen reduziert - Sequenz, Salve, # Aufladen, Strahl), magazine, ammo. Reichweite = speed * lifetime, # rechnet der Server. Raketen: SCItemMissileParams (Sprengschaden, # Geschwindigkeit, Suchkopf); Raketenracks: Anzahl und Groesse der # Ports. $ordner = 'scitem/ships/powerplant', 'scitem/ships/cooler', 'scitem/ships/shieldgenerator', 'scitem/ships/quantumdrive', 'scitem/ships/radar' $komponenten = 'SAttachableComponentParams', 'SHealthComponentParams', 'SCItemShieldGeneratorParams', 'SCItemCoolerParams', 'SCItemQuantumDriveParams', 'EntityComponentPowerConnection', 'ItemResourceComponentParams', 'SDistortionParams', 'SCItemRadarComponentParams', 'SCItemMissileParams', 'SCItemMissileRackParams' # Die Art aus dem Ordner: erst die Komponenten, dann die Waffenordner # (Teile, Entwicklerstaende, EMP und QIG bleiben draussen). $artVon = { param($pfad) foreach ($of in $ordner) { if ($pfad -like "*/$of/*") { return $of.Substring($of.LastIndexOf('/') + 1) } } if ($pfad -like '*/parts/*' -or $pfad -like '*/dev/*') { return $null } if ($pfad -like '*/scitem/ships/weapons/missiles/*') { return 'missile' } if ($pfad -like '*/scitem/ships/weapons/rocket_pods/*') { return 'rocketpod' } if ($pfad -like '*/scitem/ships/weapons/emp/*' -or $pfad -like '*/scitem/ships/weapons/qig/*') { return $null } if ($pfad -like '*/scitem/ships/weapons/*') { return 'shipweapon' } if ($pfad -like '*/scitem/ships/missile_racks/*') { return 'missilerack' } if ($pfad -like '*/scitem/weapons/fps_weapons/*') { return 'fpsweapon' } return $null } # Skalare einer Instanz, verschachtelte Klassen bis $tiefe, Listen von Skalaren als Liste. $flachWerte = $null $flachWerte = { param($x, $tiefe) $o = [ordered]@{} foreach ($k in @($x.Keys)) { if ($k -like '__*') { continue } # Optik, Ton, Interaktion, Kurven: nichts, was eine Tabelle zeigt if ($k -match 'VFX|Shader|[Aa]udio|Interaction|Effect|Curve|Animation|[Rr]eticle|[Cc]amera|Misfire|Sequencer|maelstrom|Tags$') { continue } $v = $x[$k] if ($v -eq $null) { continue } if ($v -is [string]) { if ($v -ne '' -and $v -ne '...' -and $v.Length -le 120) { $o[$k] = $v }; continue } if ($v -is [bool] -or $v -is [int] -or $v -is [long] -or $v -is [double]) { $o[$k] = $v; continue } if ($v -is [System.Collections.IDictionary]) { if ($v.ContainsKey('__name')) { $o[$k] = [string]$v['__name']; continue } if ($tiefe -gt 0) { $sub = & $flachWerte $v ($tiefe - 1); if ($sub.Count) { $o[$k] = $sub } } continue } if ($v -is [System.Collections.IList]) { $l = New-Object System.Collections.ArrayList foreach ($e in $v) { if ($e -is [System.Collections.IDictionary]) { if ($tiefe -gt 0) { $sub = & $flachWerte $e ($tiefe - 1); if ($sub.Count) { [void]$l.Add($sub) } } } elseif ($e -ne $null -and -not ($e -is [string] -and $e.Length -gt 120)) { [void]$l.Add($e) } if ($l.Count -ge 8) { break } } if ($l.Count) { $o[$k] = @($l) } } } return $o } # Dazu, fuer JEDE Entity mit Anbaudefinition: die Zuordnung Klasse -> # Schluessel der global.ini (AttachDef.Localization.Name/.Description), # Anbautyp, Untertyp, Groesse, Grade. Das ist die Bruecke zwischen dem, # was ein Blueprint herstellt (Klassenname, cds_legacy_armor_heavy_core_ # 01_01_02), und der Item-Seite (Datenkopf-Schluessel, _cds_legacy_heavy_ # armor_01_core): alle 20 Farben des ADP Core teilen sich EINEN # Beschreibungsschluessel, und genau so gruppiert die Seite sie. # ~26.000 Zeilen, ~6 MB; Block dcb_classes, Tabelle game_entity_classes. # Eine Feueraktion auf ihre Zahlen reduziert. Die Aktionen sind ein # Baum: Sequence -> entries -> weaponAction, Charged -> weaponAction, # DynamicCondition -> conditional/default, Parallel -> actions. Was # eine Tabelle zeigt, bleibt; Optik, Ton, Rueckstoss fallen weg. $feuermodus = $null $feuermodus = { param($fa) if ($fa -isnot [System.Collections.IDictionary]) { return $null } $o = [ordered]@{ type = (([string]$fa['__type']) -replace '^SWeaponAction', '' -replace 'Params$', '') } foreach ($k in 'name', 'localisedName', 'fireRate', 'heatPerShot', 'wearPerShot', 'shotCount', 'cooldownTime', 'chargeTime', 'overchargeTime', 'overchargedTime', 'fireOnlyOnFullCharge', 'spinUpTime', 'spinDownTime', 'chargeUpTime', 'chargeDownTime', 'heatPerSecond', 'fullDamageRange', 'zeroDamageRange', 'hitRadius', 'minEnergyDraw', 'maxEnergyDraw', 'ammoType', 'mode', 'delay', 'unit', 'repetitions', 'toggle') { if ($fa.ContainsKey($k) -and $fa[$k] -ne $null -and -not ($fa[$k] -is [System.Collections.IDictionary]) -and -not ($fa[$k] -is [System.Collections.IList])) { $o[$k] = $fa[$k] } } $lp = $fa['launchParams'] if ($lp -is [System.Collections.IDictionary]) { $l = [ordered]@{ type = (([string]$lp['__type']) -replace '^S', '' -replace 'Launcher$', '') } foreach ($k in 'ammoCost', 'pelletCount', 'damageMultiplier', 'projectileType', 'canFireWithoutLock') { if ($lp.ContainsKey($k) -and $lp[$k] -ne $null) { $l[$k] = $lp[$k] } } if ($lp['spreadParams'] -is [System.Collections.IDictionary]) { $l['spread'] = & $flachWerte $lp['spreadParams'] 0 } $o['launch'] = $l } if ($fa['maxChargeModifier'] -is [System.Collections.IDictionary]) { $m = [ordered]@{} foreach ($k in 'fireRate', 'fireRateMultiplier', 'damageMultiplier', 'pellets', 'burstShots', 'ammoCost', 'ammoCostMultiplier', 'heatGenerationMultiplier', 'projectileSpeedMultiplier') { if ($fa['maxChargeModifier'].ContainsKey($k)) { $m[$k] = $fa['maxChargeModifier'][$k] } } $o['maxCharge'] = $m } if ($fa['damagePerSecond'] -is [System.Collections.IDictionary]) { $o['damagePerSecond'] = & $flachWerte $fa['damagePerSecond'] 0 } foreach ($k in 'weaponAction', 'defaultWeaponAction') { if ($fa[$k] -is [System.Collections.IDictionary]) { $o['action'] = & $feuermodus $fa[$k] } } if ($fa['sequenceEntries'] -is [System.Collections.IList]) { $o['entries'] = @(foreach ($se in $fa['sequenceEntries']) { $x = & $feuermodus $se; if ($x) { $x } }) } if ($fa['weaponActions'] -is [System.Collections.IList]) { $o['actions'] = @(foreach ($wa in $fa['weaponActions']) { $x = & $feuermodus $wa; if ($x) { $x } }) } if ($fa['conditionalWeaponActions'] -is [System.Collections.IList]) { $o['conditional'] = @(foreach ($cw in $fa['conditionalWeaponActions']) { if ($cw -is [System.Collections.IDictionary]) { $c = [ordered]@{ condition = $(if ($cw['condition'] -is [System.Collections.IDictionary]) { ([string]$cw['condition']['__type']) -replace '^SWeaponCondition', '' } else { $null }) } if ($cw['condition'] -is [System.Collections.IDictionary]) { $cf = & $flachWerte $cw['condition'] 0; foreach ($k in $cf.Keys) { $c[$k] = $cf[$k] } } $c['action'] = & $feuermodus $cw['weaponAction'] $c } }) } return $o } # Der Munitions-Record hinter einem Behaelter: Geschwindigkeit, # Lebensdauer, Schaden je Art, Detonation. Je GUID einmal gelesen. $munitionCache = @{} $munition = { param($ref) if ($ref -isnot [System.Collections.IDictionary] -or -not $ref['__ref']) { return $null } $g = [string]$ref['__ref'] if ($munitionCache.ContainsKey($g)) { return $munitionCache[$g] } $r = $d.ByGuid($g) if (-not $r) { $munitionCache[$g] = $null; return $null } $a = $d.Read($r, 6) $o = [ordered]@{ record = $r.Name.Substring($r.Name.IndexOf('.') + 1) } foreach ($k in 'speed', 'lifetime', 'size', 'hitPoints', 'ammoCategory') { if ($a.ContainsKey($k) -and $a[$k] -ne $null) { $o[$k] = $a[$k] } } if ($a['projectileParams'] -is [System.Collections.IDictionary]) { $o['projectile'] = & $flachWerte $a['projectileParams'] 4 } $munitionCache[$g] = $o return $o } # Der Munitionsbehaelter einer Entity: eigener, oder der des Magazins, # auf das die Waffe zeigt. $behaelter = { param($comps, $w) $ac = $null foreach ($c in @($comps)) { if ($c -is [System.Collections.IDictionary] -and [string]$c['__type'] -eq 'SAmmoContainerComponentParams') { $ac = $c; break } } $magazin = $null if (-not $ac -and $w -is [System.Collections.IDictionary] -and $w['ammoContainerRecord'] -is [System.Collections.IDictionary] -and $w['ammoContainerRecord']['__ref']) { $mr = $d.ByGuid([string]$w['ammoContainerRecord']['__ref']) if ($mr) { $magazin = $mr.Name.Substring($mr.Name.IndexOf('.') + 1) $mx = $d.Read($mr, 3) foreach ($c in @($mx['Components'])) { if ($c -is [System.Collections.IDictionary] -and [string]$c['__type'] -eq 'SAmmoContainerComponentParams') { $ac = $c; break } } } } if (-not $ac) { return $null } $o = [ordered]@{} if ($magazin) { $o['magazine'] = $magazin } foreach ($k in 'initialAmmoCount', 'maxAmmoCount', 'maxRestockCount', 'ammoContainerType') { if ($ac.ContainsKey($k) -and $ac[$k] -ne $null) { $o[$k] = $ac[$k] } } $o['ammo'] = & $munition $ac['ammoParamsRecord'] return $o } $gegenst = New-Object System.Collections.ArrayList $klassen = New-Object System.Collections.ArrayList foreach ($rec in $d.RecordsOf('EntityClassDefinition')) { $pfad = $rec.File.Replace('\', '/') $art = & $artVon $pfad # Die Anbaudefinition liegt zwei Ebenen tief; die Komponentenwerte brauchen mehr. $e = $d.Read($rec, $(if ($art) { 7 } else { 3 })) $ad = $null foreach ($c in @($e['Components'])) { if ($c -is [System.Collections.IDictionary] -and [string]$c['__type'] -eq 'SAttachableComponentParams' -and $c['AttachDef'] -is [System.Collections.IDictionary]) { $ad = $c['AttachDef']; break } } if ($ad -ne $null) { $loc = $ad['Localization'] $nameKey = ''; $descKey = '' if ($loc -is [System.Collections.IDictionary]) { $nameKey = ([string]$loc['Name']).TrimStart('@'); $descKey = ([string]$loc['Description']).TrimStart('@') } if ($nameKey -eq 'LOC_EMPTY') { $nameKey = '' } if ($descKey -eq 'LOC_EMPTY') { $descKey = '' } if ($nameKey -or $descKey) { # Der Ordner unter entities/ ohne Dateiname: scitem/characters/human/armor/pu_armor/heavy/core $ordnerPfad = $pfad; $i = $ordnerPfad.IndexOf('/entities/'); if ($i -ge 0) { $ordnerPfad = $ordnerPfad.Substring($i + 10) } $i = $ordnerPfad.LastIndexOf('/'); if ($i -ge 0) { $ordnerPfad = $ordnerPfad.Substring(0, $i) } [void]$klassen.Add([ordered]@{ class = $rec.Name.Substring($rec.Name.IndexOf('.') + 1) name_key = $nameKey desc_key = $descKey item = $(if ($descKey) { Klasse-AusDescSchluessel $descKey } else { '' }) type = [string]$ad['Type'] subtype = [string]$ad['SubType'] size = $ad['Size'] grade = $ad['Grade'] path = $ordnerPfad }) } } if (-not $art) { continue } $params = [ordered]@{} $groesse = $null; $stufe = $null; $hp = $null foreach ($c in @($e['Components'])) { if ($c -isnot [System.Collections.IDictionary]) { continue } $t = [string]$c['__type'] if ($komponenten -notcontains $t) { continue } if ($t -eq 'SAttachableComponentParams' -and $c['AttachDef'] -is [System.Collections.IDictionary]) { $groesse = $c['AttachDef']['Size']; $stufe = $c['AttachDef']['Grade'] continue } if ($t -eq 'SHealthComponentParams') { $hp = $c['Health']; continue } $params[$t] = & $flachWerte $c 5 } if ($art -in 'shipweapon', 'rocketpod', 'fpsweapon') { $w = $null foreach ($c in @($e['Components'])) { if ($c -is [System.Collections.IDictionary] -and [string]$c['__type'] -eq 'SCItemWeaponComponentParams') { $w = $c; break } } if ($w) { $wp = [ordered]@{ fire_modes = @(foreach ($fa in @($w['fireActions'])) { $x = & $feuermodus $fa; if ($x) { $x } }) } if ($w['connectionParams'] -is [System.Collections.IDictionary]) { foreach ($k in 'heatRateOnline', 'powerActiveCooldown') { if ($w['connectionParams'].ContainsKey($k)) { $wp[$k] = $w['connectionParams'][$k] } } } $cont = & $behaelter $e['Components'] $w if ($cont) { $wp['container'] = $cont } $params['weapon'] = $wp } } if ($art -eq 'missilerack') { # Was der Rack traegt: Anzahl und Groesse der Raketenports. foreach ($c in @($e['Components'])) { if ($c -isnot [System.Collections.IDictionary] -or [string]$c['__type'] -ne 'SItemPortContainerComponentParams') { continue } $n = 0; $ps = $null foreach ($port in @($c['Ports'])) { if ($port -isnot [System.Collections.IDictionary]) { continue } $istRakete = $false foreach ($ty in @($port['Types'])) { if ($ty -is [System.Collections.IDictionary] -and [string]$ty['Type'] -eq 'Missile') { $istRakete = $true } } if ($istRakete) { $n++; $ps = $port['MaxSize'] } } if ($n) { $params['ports'] = [ordered]@{ count = $n; size = $ps } } } } [void]$gegenst.Add([ordered]@{ class = $rec.Name.Substring($rec.Name.IndexOf('.') + 1) kind = $art size = $groesse grade = $stufe health = $hp params = $params }) } & $log " $($gegenst.Count) Gegenstaende mit Komponentenwerten, $($klassen.Count) Entity-Klassen mit Sprachschluessel." $makler = @($liste | Where-Object { $_.system -ne 'contract' }) & $log " $($makler.Count) Maklereintraege mit Titel, $(@($makler | Where-Object { $_.prefix }).Count) davon einer Mission der Seite zugeordnet." # ------------------------------------------------------------------- # Mining: what can be mined where (Alex, 27.09.2026) # ------------------------------------------------------------------- # # Every place has a HarvestableProviderPreset (HPP_Stanton1a = Arial, # HPP_Nyx_KeegerBelt ...) with groups - ship, ground vehicle, hand # mining, plants, salvage - each with a probability, and in them the # deposits with their relative share. A rock deposit points at its # entity, the entity's MineableParams at a MineableComposition: the # elements with min/max per cent and probability, each element at its # ResourceType. A plant is its own resource. Salvage stays out - a wreck # is not a material. What the level design places by hand (Sadaryx in # the Breaker Stations) is in none of these records. $mining = $null try { $mining = & { $text = { param($k) $k = ([string]$k).TrimStart('@'); if ($k -and $map.ContainsKey($k)) { $t = ([string]$map[$k]).Trim(); if ($t -and $t -notmatch '^(LOC_|@)') { return $t } }; return $null } $short = { param($n) if ($n) { return $n.Substring($n.IndexOf('.') + 1) }; return $null } $resources = [ordered]@{} # resource key -> display name $compositions = [ordered]@{} # composition key -> composition $entityComposition = @{} # entity guid -> composition key ('' for none) $resourceOf = { param($ref) if ($ref -isnot [System.Collections.IDictionary] -or -not $ref['__ref']) { return $null } $r = $d.ByGuid([string]$ref['__ref']); if (-not $r) { return $null } $k = & $short $r.Name if (-not $resources.Contains($k)) { $n = & $text ($d.Read($r, 0)['displayName']); $resources[$k] = $(if ($n) { $n } else { $k }) } return $k } $compositionOf = { param($ref) if ($ref -isnot [System.Collections.IDictionary] -or -not $ref['__ref']) { return $null } $r = $d.ByGuid([string]$ref['__ref']); if (-not $r) { return $null } $k = & $short $r.Name if ($compositions.Contains($k)) { return $k } $c = $d.Read($r, 2) $parts = New-Object System.Collections.ArrayList foreach ($part in @($c['compositionArray'])) { if ($part -isnot [System.Collections.IDictionary]) { continue } $el = $part['mineableElement'] if ($el -isnot [System.Collections.IDictionary] -or -not $el['__ref']) { continue } $er = $d.ByGuid([string]$el['__ref']); if (-not $er) { continue } $res = & $resourceOf ($d.Read($er, 0)['resourceType']) if (-not $res) { continue } [void]$parts.Add([ordered]@{ resource = $res; min = $part['minPercentage']; max = $part['maxPercentage']; probability = $part['probability'] }) } $compositions[$k] = [ordered]@{ key = $k; deposit = (& $text $c['depositName']); min_elements = $c['minimumDistinctElements']; parts = @($parts) } return $k } # The composition a rock entity carries in its MineableParams. $rockComposition = { param($ref) if ($ref -isnot [System.Collections.IDictionary] -or -not $ref['__ref']) { return $null } $g = [string]$ref['__ref'] if ($entityComposition.ContainsKey($g)) { $v = $entityComposition[$g]; if ($v) { return $v }; return $null } $v = '' $r = $d.ByGuid($g) if ($r) { foreach ($comp in @($d.Read($r, 1)['Components'])) { if ($comp -is [System.Collections.IDictionary] -and [string]$comp['__type'] -eq 'MineableParams') { $ck = & $compositionOf $comp['composition']; if ($ck) { $v = $ck } } } } $entityComposition[$g] = $v if ($v) { return $v }; return $null } # A plant is its own resource: the entity's name as the game prints it. $plantResource = { param($ref, $preset) $name = $null if ($ref -is [System.Collections.IDictionary] -and $ref['__ref']) { $r = $d.ByGuid([string]$ref['__ref']) if ($r) { foreach ($comp in @($d.Read($r, 3)['Components'])) { if ($comp -is [System.Collections.IDictionary] -and [string]$comp['__type'] -eq 'SAttachableComponentParams') { $loc = $comp['AttachDef']['Localization'] if ($loc -is [System.Collections.IDictionary]) { $name = & $text $loc['Name'] } } } } } if (-not $name -and $preset) { $name = ($preset -replace '^(Plant|Harvestable)_', '') -replace '_', ' ' } if (-not $name) { return $null } $k = 'plant:' + $name if (-not $resources.Contains($k)) { $resources[$k] = $name } return $k } $providers = New-Object System.Collections.ArrayList foreach ($p in $d.RecordsOf('HarvestableProviderPreset')) { $x = $d.Read($p, 3) $groups = New-Object System.Collections.ArrayList foreach ($g in @($x['harvestableGroups'])) { if ($g -isnot [System.Collections.IDictionary]) { continue } $gname = [string]$g['groupName'] $kind = $null if ($gname -match '^SpaceShip') { $kind = 'ship' } elseif ($gname -match '^GroundVehicle') { $kind = 'roc' } elseif ($gname -match '^FPS') { $kind = 'fps' } elseif ($gname -match '^Ha(r)?vestables') { $kind = 'harvest' } if (-not $kind) { continue } $entries = New-Object System.Collections.ArrayList foreach ($h in @($g['harvestables'])) { if ($h -isnot [System.Collections.IDictionary]) { continue } $preset = $null $entityRef = $h['harvestableEntityClass'] if ($h['harvestable'] -is [System.Collections.IDictionary] -and $h['harvestable']['__ref']) { $pr = $d.ByGuid([string]$h['harvestable']['__ref']) if ($pr) { $preset = & $short $pr.Name; $entityRef = $d.Read($pr, 0)['entityClass'] } } $comp = & $rockComposition $entityRef $res = $null if (-not $comp -and $kind -eq 'harvest') { $res = & $plantResource $entityRef $preset } if (-not $comp -and -not $res) { continue } [void]$entries.Add([ordered]@{ preset = $preset; relative = $h['relativeProbability']; composition = $comp; resource = $res }) } if ($entries.Count) { [void]$groups.Add([ordered]@{ name = $gname; kind = $kind; probability = $g['groupProbability']; entries = @($entries) }) } } if ($groups.Count) { $pkey = & $short $p.Name $ppath = (([string]$p.File) -replace '\\', '/') -replace '^.*?/providerpresets/', '' [void]$providers.Add([ordered]@{ key = $pkey; path = $ppath; groups = @($groups) }) } } [ordered]@{ resources = @($resources.Keys | ForEach-Object { [ordered]@{ key = $_; name = $resources[$_] } }) compositions = @($compositions.Values) providers = @($providers) } } & $log (" Mining: {0} places, {1} compositions, {2} resources." -f @($mining.providers).Count, @($mining.compositions).Count, @($mining.resources).Count) } catch { & $log (" Mining not read: " + $_.Exception.Message) $mining = $null } # Event tiers: one row per tier of every ScenarioProgress record. $scenarios = $null try { $rows = New-Object System.Collections.ArrayList foreach ($r in @($d.RecordsOf('ScenarioProgress'))) { foreach ($row in @(Get-ScenarioRows $r.Name ($d.Read($r, 6)))) { [void]$rows.Add($row) } } $scenarios = @($rows) & $log (" Scenarios: {0} tiers." -f $rows.Count) } catch { & $log (" Scenarios not read: " + $_.Exception.Message) $scenarios = $null } return @{ broker = @($liste); items = @($gegenst); classes = @($klassen); mining = $mining; scenarios = $scenarios } } # =========================================================================== # Extraktion (gleiche Logik wie tools/extract-missions.ps1) # =========================================================================== function Extrahiere($kanal, [scriptblock]$log) { # Keine lose Sprachdatei? Dann aus der Data.p4k holen. Das dauert etwa # eine Viertelminute - fast alles davon geht fuer das Durchsuchen des # Archivverzeichnisses drauf. if (-not $kanal.Ini) { & $log 'Keine lose Sprachdatei - lese aus Data.p4k.' $kanal = $kanal.PSObject.Copy() $kanal.Ini = Hole-IniAusP4k $kanal $log } & $log "Quelle: $($kanal.Ini)" # Ab hier wird nur noch aus $quelle gelesen. Faellt die Pruefung negativ # aus, zeigt $quelle anschliessend auf die zurueckgerechnete Kopie. $quelle = $kanal.Ini $wiederhergst = $false # --- Schutzsperre: nie gegen eine gepatchte Installation $dir = Split-Path $kanal.Ini $blocker = @() foreach ($bak in 'global.ini.bp.bak.json', 'global.ini.strings.bak.json') { $bp = Join-Path $dir $bak if (-not (Test-Path $bp)) { continue } $roh = [System.IO.File]::ReadAllText($bp, [System.Text.Encoding]::UTF8) if ($roh -match '"[A-Za-z0-9_]*(_title|_desc|_obj_)') { $blocker += "Launcher-Sicherung '$bak' enthaelt Missions-Keys" } else { & $log "Hinweis: '$bak' vorhanden, betrifft aber keine Missionstexte." } } # Ganze Datei pruefen, nicht nur den Anfang: Missionstexte liegen ueber # alle rund 90.000 Zeilen verteilt. $ganz = [System.IO.File]::ReadAllText($kanal.Ini, [System.Text.Encoding]::UTF8) foreach ($m in ([ordered]@{ '# Region' = '#\s*Region:.*Danger\s+\d' '# Expected reputation' = '#\s*Expected reputation' '# Min. Reputation' = '#\s*Min\.\s*Reputation' '# Mission Cooldown' = '#\s*Mission Cooldown' '# Blueprint Drop Chance'= '#\s*Blueprint Drop Chance' 'POSSIBLE BLUEPRINTS' = 'POSSIBLE BLUEPRINTS FOR THIS MISSION' '[BP]-Marker' = '\[BP\]' 'Verweis auf scmdb.net' = 'scmdb\.net' }).GetEnumerator()) { $n = ([regex]::Matches($ganz, $m.Value)).Count if ($n -gt 0) { $blocker += "injizierte Stellen `"$($m.Key)`": $n" } } $ganz = $null # rund 11 MB, wird nicht mehr gebraucht if ($blocker.Count) { & $log 'Diese Installation ist von einem Drittanbieter-Launcher veraendert:' foreach ($b in $blocker) { & $log " - $b" } # Der Launcher sichert jeden Wert, bevor er ihn ersetzt. Daraus laesst # sich der Auslieferungszustand zurueckrechnen - dann wird nicht die # fremde Kuratierung importiert, sondern der CIG-Text. & $log 'Rechne den Originaltext aus der Launcher-Sicherung zurueck...' if (-not (Test-Path $ARBEIT_DIR)) { New-Item -ItemType Directory -Path $ARBEIT_DIR -Force | Out-Null } $ziel = Join-Path $ARBEIT_DIR ("global.restored.$($kanal.Name).ini") try { $r = Stelle-OriginalHer -IniPath $kanal.Ini -OutPath $ziel } catch { throw "Die Installation ist veraendert und laesst sich nicht zurueckrechnen: " + "$($_.Exception.Message) Bitte diesen Kanal ueber den RSI-Launcher " + "pruefen/reparieren oder einen anderen Kanal (z.B. PTU) hochladen." } & $log ("Zurueckgesetzt: $($r.Exakt) Werte exakt, $($r.UeberZusatz) ueber " + "Schluessel ohne Varianten-Zusatz.") & $log 'Ergebnis ist frei von fremden Inhalten - Extraktion laeuft damit weiter.' $quelle = $r.Ziel $wiederhergst = $true } # --- global.ini einlesen $map = New-Object 'System.Collections.Generic.Dictionary[string,string]' ([System.StringComparer]::OrdinalIgnoreCase) foreach ($line in [System.IO.File]::ReadAllLines($quelle, [System.Text.Encoding]::UTF8)) { $eq = $line.IndexOf('='); if ($eq -lt 1) { continue } $k = $line.Substring(0, $eq).Trim() $c = $k.IndexOf(','); if ($c -gt 0) { $k = $k.Substring(0, $c) } if (-not $map.ContainsKey($k)) { $map[$k] = $line.Substring($eq + 1).Trim() } } & $log "$($map.Count) Schluessel gelesen." $IC = [System.StringComparison]::OrdinalIgnoreCase $descs = @{}; $objs = @{} # Schluesselfamilien, die WIE Missionen benannt sind, aber keine sind. # Sie tragen Titel und Beschreibung und kommen deshalb durch jede # Pruefung, die nur auf das Vorhandensein beider achtet: # # notification_invite_declined_title = Invitation Declined # notification_invite_declined_desc = %s # badge_cleanair_transport_t2_title = Alliance Aid Transport Rewards - Tier 2 # ui_pause_popupquitgame_title = Quit to desktop # # 66 solcher Eintraege standen bis 09.09.2026 als Missionen im Katalog. # Ein Walkthrough zu "Invite Expired" ist nicht schreibbar; in der # Missionsliste sind sie nur im Weg. # # Seit "_description" mitgelesen wird (12.09.2026), kommen drei weitere # Familien mit Titel UND Text: ea_ui_frontend_specialevent_* (Feiertage # im Hauptmenue), journalentry_* (Funde im Journal), bbt_* (Textproben # der Entwickler). Keine davon ist ein Auftrag. # # Seit 21.09.2026 (Erkennung 4) auch alles mit "journal" im Namen: # bhg_reputationjournal_easy .. super (Ruf-Lizenzen, nur ein Titel - kamen # ueber die Stufen-Ausnahme durch) und cfp_ch2_journal_intro/_reveal # (Kapiteltexte des Pyro-Events, mit Beschreibung). 13 Eintraege, keiner # ein Auftrag. Game2.dcb fuehrt sie als JournalEntry, nicht im Makler. $KEINE_MISSION = @('ui_', 'notification_', 'badge_', 'ea_ui_', 'journalentry_', 'bbt_') $KEINE_MISSION_MITTE = @('journal') $istKeineMission = { param($basis) foreach ($f in $KEINE_MISSION) { if ($basis.StartsWith($f)) { return $true } } foreach ($f in $KEINE_MISSION_MITTE) { if ($basis.IndexOf($f, [System.StringComparison]::OrdinalIgnoreCase) -ge 0) { return $true } } return $false } # Stufenbezeichner. Sie sind der einzige verlaessliche Hinweis darauf, # dass ein "_title" OHNE Beschreibung und OHNE Objectives trotzdem eine # Mission ist - siehe die Regel weiter unten. # # "veryeasy" fehlte bis 12.09.2026 - neun Auftraege, darunter Hockrows # "Locate Missing Clients", standen deshalb nicht im Katalog, waehrend # ihre fuenf Geschwister (easy .. super) drin waren. $SCHWIERIGKEIT = @('ve', 'e', 'm', 'h', 'vh', 's', 'easy', 'medium', 'hard', 'veryhard', 'super', 'veryeasy', 'low', 'verylow', 'high', 'veryhigh') # --- Einsammeln ------------------------------------------------------- # # EINE Regel fuer alle Schreibweisen. Vorher gab es zwei Durchgaenge, # einen fuer "_title" und einen fuer "_title_001" - und dazwischen fiel # alles durch, was anders aussieht: # # Battaglia_Story03_title die schlichte Form # CFP_SecurityPatrol_VE_Title_001 nummeriert # vaughn_assassination_VE_title_intro benannt # mtps_bounty_fps_title_rehire_001 benannt UND nummeriert # ServiceBeacon_..._Title_Difficulty_VeryHard mehrteilig # # Gegengeprueft an den Missionsdaten von star-citizen.wiki, die aus der # Game.dcb stammen statt aus der Sprachdatei: Von 444 Titeln, die dort # standen und bei uns nicht, lagen 269 unter genau diesen Formen in # unserer eigenen global.ini. # # NICHT genommen wird "_name". Das sieht verlockend aus - 29 Missionen # tragen ihren Titel so -, aber die Form gehoert im Wesentlichen # Gegenstaenden, Fraktionen und Proben: 692 Schluessel, davon selbst mit # Beschreibung daneben nur 12 Prozent Missionen. Der Preis waere ein # Katalog voller Ruestungsteile. # $titelBasen = @{} foreach ($k in $map.Keys) { $s = $k.ToLowerInvariant() if ($s -match '^(.*)_title(?:_(.+))?$') { $b = $Matches[1] if (& $istKeineMission $b) { continue } $suffix = '' if ($Matches.ContainsKey(2)) { $suffix = $Matches[2] } if (-not $titelBasen.ContainsKey($b)) { $titelBasen[$b] = New-Object System.Collections.ArrayList } [void]$titelBasen[$b].Add([pscustomobject]@{ Suffix = $suffix; Key = $k }) continue } # Beschreibungen werden ueber die BASIS gesammelt und erst je # Fassung zugeordnet (siehe $waehleDesc): Traegt die Beschreibung # dasselbe Anhaengsel wie der Titel, gehoert sie zu ihm - sonst die # erste der Basis. Bis 12.09.2026 gewann immer die erste: Alle 30 # Wikelo-Rezepte (TheCollector_Recipes_Title_L22Wolf, ..._Desc_L22Wolf) # trugen den Text des Apollo-Rezepts. # # Zwei Schreibweisen: "_desc" und ausgeschrieben "_description" # (CFP_RecoverCargo_Easy_Description_001). 140 Basen tragen NUR die # lange Form; ihre Auftraege standen bis 12.09.2026 ohne Text da. # Die kurze Form gewinnt, wenn beide vorkommen. if ($s -match '^(.*)_desc(?:_(.+))?$' -or $s -match '^(.*)_description(?:_(.+))?$') { $b = $Matches[1] if (& $istKeineMission $b) { continue } $suffix = '' if ($Matches.ContainsKey(2)) { $suffix = $Matches[2] } if (-not $descs.ContainsKey($b)) { $descs[$b] = New-Object System.Collections.ArrayList } [void]$descs[$b].Add([pscustomobject]@{ Suffix = $suffix; Key = $k; Lang = ($s -notmatch '_desc(_|$)') }) continue } $i = $k.IndexOf('_obj_', $IC) if ($i -gt 0) { $p = $k.Substring(0, $i).ToLowerInvariant() if (-not $objs.ContainsKey($p)) { $objs[$p] = New-Object System.Collections.ArrayList } [void]$objs[$p].Add($k) } } # --- Reihenfolge der Fassungen ---------------------------------------- # # WICHTIG, und der Grund fuer diese Sortierung: Die erste Fassung behaelt # die nackte Basis als prefix. Das prefix ist die Identitaet einer # Mission ueber alle Builds hinweg - an ihm haengen Walkthroughs, # Fraktionszuordnung und Sperrliste. Waehlte man hier anders, wechselten # bestehende Missionen ihre Identitaet und die kuratierte Arbeit haenge # ins Leere. # # Ohne Suffix schlaegt alles, dann die kleinste Zahl, dann alphabetisch. # Das ist genau die Wahl, die der Extractor vorher schon getroffen hat. $sortSchluessel = { param($x) if (-not $x.Suffix) { return '0' } if ($x.Suffix -match '^\d+$') { return '1' + ('{0:0000000}' -f [int]$x.Suffix) } return '2' + $x.Suffix } # Welche Beschreibung zu einem Titel gehoert. Gleiches Anhaengsel # schlaegt alles; dann die kurze Schreibweise vor der langen; dann die # Reihenfolge, die auch fuer Titel gilt (ohne Anhaengsel, kleinste Zahl, # alphabetisch). $waehleDesc = { param($basis, $suffix) $liste = $descs[$basis] if (-not $liste) { return $null } $treffer = @($liste | Where-Object { $_.Suffix -eq $suffix }) if (-not $treffer.Count) { $treffer = @($liste) } $best = $treffer | Sort-Object { [int]$_.Lang }, { & $sortSchluessel $_ } | Select-Object -First 1 return $best.Key } $sha256 = [System.Security.Cryptography.SHA256]::Create() $result = New-Object System.Collections.ArrayList $skipped = 0 # --- Fassungen: wann ist es EINE Mission, wann sind es mehrere? -------- # # Eine Basis traegt oft mehrere Titel. Die Frage, ob das Textfassungen # desselben Auftrags sind oder verschiedene Auftraege, entscheidet ueber # Hunderte Eintraege - und laesst sich am Schluessel nicht ablesen. # # ENTSCHIEDEN AN DEN DATEN, nicht am Gefuehl: Wo star-citizen.wiki (das # aus der Game.dcb liest, nicht aus der Sprachdatei) nachsehen liess, # waren es EIGENE Missionen mit eigenem Namen im Spiel: # # Headhunters_bounty_title_001/2/3 = Deadbeat / Past Due / Collection # Notice -> drei Auftraege, verschiedene Orte und Stufen # Adagio_..._universal_e_title_01/02 = "Salvager Needed (Small Supply # of RMC)" / "(... / UCM)" -> zwei Auftraege, sogar in # verschiedenen Systemen # # Deshalb: VERSCHIEDENER TEXT = VERSCHIEDENE MISSION. Derselbe Text # unter mehreren Schluesseln bleibt eine. # # Der Preis: Familien wie mg_ruto_havejob_title_0001..0008 ("swing on # by", "New Contract Offer") sind eher acht Anreden fuer denselben # Auftrag. Sie stehen jetzt einzeln im Katalog. Das ist die schwaechere # Seite dieser Regel und der Punkt, an dem sie zu revidieren waere. # # Erst die Fassungen bestimmen, dann bauen. Getrennt, weil die Vergabe der # prefixe die ganze Basis kennen muss - welche Fassung die nackte Basis # bekommt, entscheidet sich nicht je Schluessel. $fassungen = New-Object System.Collections.ArrayList $vergeben = New-Object 'System.Collections.Generic.HashSet[string]' foreach ($b in $titelBasen.Keys) { $hatD = $descs.ContainsKey($b); $hatO = $objs.ContainsKey($b) # "_title" nutzt das Spiel auch fuer Terminals und Menues - ohne # Beschreibung UND ohne Objectives ist es normalerweise keine Mission. # # AUSNAHME: endet die Basis auf eine Stufenbezeichnung, ist es die # Schwierigkeitsvariante eines Auftrags, dessen Text woanders steht: # # CFP_RecoverCargo_Easy_Title = Intercepted Supply Run # Covalex_RecoverCargo_Super_Title = Recover VIP Covalex Shipment # # Gemessen: 40 Schluessel passen darauf, 35 davon sind laut # star-citizen.wiki echte Missionen. Ohne die Ausnahme fehlten sie # alle; ohne die Grundregel kaemen 723 Menuebeschriftungen mit. $letztes = ($b -split '_')[-1] if (-not $hatD -and -not $hatO -and $SCHWIERIGKEIT -notcontains $letztes) { $skipped += $titelBasen[$b].Count continue } # Gleicher Text unter mehreren Schluesseln ist EINE Mission, # verschiedene Texte sind verschiedene. Siehe den Block ueber den # Fassungen weiter unten. $gesehen = New-Object 'System.Collections.Generic.HashSet[string]' $erste = $true foreach ($t in ($titelBasen[$b] | Sort-Object { & $sortSchluessel $_ })) { $wert = $map[$t.Key] if (-not $wert) { continue } if (-not $gesehen.Add($wert)) { continue } $prefix = if ($erste) { $b } else { $b + '_' + $t.Suffix } # Sicherheitsnetz: zwei Basen koennten auf denselben Namen fallen. $n = 2 while (-not $vergeben.Add($prefix)) { $prefix = $b + '_' + $t.Suffix + "_$n"; $n++ } [void]$fassungen.Add([pscustomobject]@{ Prefix = $prefix; Basis = $b; Key = $t.Key; Suffix = $t.Suffix HatDesc = $hatD; HatObj = $hatO }) $erste = $false } } foreach ($f in $fassungen) { $p = $f.Basis $hasDesc = $f.HatDesc; $hasObj = $f.HatObj $title = $map[$f.Key] $desc = if ($hasDesc) { $map[(& $waehleDesc $p $f.Suffix)] } else { $null } $objectives = New-Object System.Collections.ArrayList $params = New-Object 'System.Collections.Generic.HashSet[string]' if ($hasObj) { foreach ($ok in ($objs[$p] | Sort-Object)) { $rest = $ok.Substring($ok.IndexOf('_obj_', $IC) + 5) $ord = 0; $variant = $rest.ToLowerInvariant() if ($rest -match '^(\d+)_.+_([A-Za-z]+)$') { $ord = [int]$Matches[1]; $variant = $Matches[2].ToLowerInvariant() } elseif ($rest -match '^(.*)_(\d+)([a-z]?)$') { $variant = $Matches[1].ToLowerInvariant(); $ord = [int]$Matches[2] } [void]$objectives.Add([ordered]@{ variant = $variant; ordinal = $ord; ini_key = $ok; text = $map[$ok] }) } } foreach ($t in @($title, $desc) + @($objectives | ForEach-Object { $_.text })) { if ($t) { foreach ($mm in [regex]::Matches($t, '~mission\(([^)]+)\)')) { [void]$params.Add($mm.Groups[1].Value) } } } $basis = ($title + "`n" + $desc + "`n" + (($objectives | ForEach-Object { $_.ini_key + '=' + $_.text }) -join "`n")) $hash = ($sha256.ComputeHash([System.Text.Encoding]::UTF8.GetBytes($basis)) | ForEach-Object { $_.ToString('x2') }) -join '' [void]$result.Add([ordered]@{ prefix = $f.Prefix kind = $(if ($title -match '~mission\(') { 'prozedural' } else { 'fest' }) title = $title description = $desc content_hash = $hash has_bp_reward = [bool]($title -match '\[BP\]') is_wip = [bool]($title -match '\[WIP\]') params = @($params) objectives = @($objectives) }) } $sha256.Dispose() & $log "$($result.Count) Missionen erkannt, $skipped UI-Strings uebersprungen." # ---------------------------------------------------------------------- # Objectives wiederverwendbarer Missionsbausteine # ---------------------------------------------------------------------- # # CIGs Missionssystem setzt Auftraege aus Bausteinen zusammen. Ein # Walkthrough braucht deren Objectives genauso wie die der Mission - # im HUD stehen sie nebeneinander, ohne erkennbaren Unterschied. # # Zwei Familien, zwei Regeln. # # 1) Bausteine benennen ihre Objectives GENAU WIE MISSIONEN: # # CreateUplink_obj_short_01 = Install ~mission(item) At ~mission(Destination) # CreateUplink_obj_short_02 = Collect ~mission(item) # # Der einzige Unterschied ist, dass keine Mission diesen Stamm traegt. # Genau daran erkennen wir sie: ein "_obj_"-Stamm ohne eigene Mission. # (Missionen wie battaglia_rpt_createuplink_01 verwenden das Modul, # heissen aber anders - der Stamm "createuplink" bleibt verwaist.) # # 2) Daneben gibt es Texte ohne "_obj_": # # Battaglia_MeetAndTalk_Short = Talk To Battaglia In Levski # # Die sind nur brauchbar, wenn ihr Stamm zu einem Auftraggeber gehoert, # also mindestens eine Mission mit diesem Praefix existiert. Ohne diese # Bedingung kaeme die HUD-Beschriftung der dynamischen Kampfsysteme mit # (DefendShips_Escort, AcePilot_Loot, SP_JumpedScoutShips) - fuer einen # Walkthrough wertlos, und in der Auswahlliste nur im Weg. $missionsstaemme = New-Object 'System.Collections.Generic.HashSet[string]' foreach ($m in $result) { [void]$missionsstaemme.Add($m.prefix) } $bausteine = New-Object System.Collections.ArrayList # --- Regel 1: verwaiste "_obj_"-Staemme ------------------------------- $regel1 = 0 foreach ($p in $objs.Keys) { if ($missionsstaemme.Contains($p)) { continue } # Nach laufender Nummer sammeln, damit Kurz- und Langform # zusammenfinden. Der Marker bleibt aussen vor. $nummern = @{} $anzeige = $null foreach ($ok in ($objs[$p] | Sort-Object)) { if (-not $anzeige) { $anzeige = $ok.Substring(0, $ok.IndexOf('_obj_', $IC)) } $rest = $ok.Substring($ok.IndexOf('_obj_', $IC) + 5) $ord = 0; $variant = $rest.ToLowerInvariant() if ($rest -match '^(\d+)_.+_([A-Za-z]+)$') { $ord = [int]$Matches[1]; $variant = $Matches[2].ToLowerInvariant() } elseif ($rest -match '^(.*)_(\d+)([a-z]?)$') { $variant = $Matches[1].ToLowerInvariant(); $ord = [int]$Matches[2] } if ($variant -ne 'short' -and $variant -ne 'long') { continue } if (-not $nummern.ContainsKey($ord)) { $nummern[$ord] = @{ kurz = $null; lang = $null } } if ($variant -eq 'short') { $nummern[$ord].kurz = $map[$ok] } else { $nummern[$ord].lang = $map[$ok] } } foreach ($ord in ($nummern.Keys | Sort-Object)) { # Fehlt die Kurzform, tritt die Langform ein. Anders als bei # Regel 2 ist das hier unbedenklich: Der Schluessel sagt bereits, # dass es ein Objective ist. $kurz = "$($nummern[$ord].kurz)".Trim() $lang = "$($nummern[$ord].lang)".Trim() if ($kurz -eq '') { $kurz = $lang } if ($kurz -eq '') { continue } [void]$bausteine.Add([ordered]@{ modul = $anzeige name = ('{0:00}' -f $ord) ini_key = ('{0}_obj_{1:00}' -f $anzeige, $ord) kurz = $kurz lang = $(if ($lang -eq '') { $null } else { $lang }) }) $regel1++ } } # --- Regel 2: Auftraggeber-Texte ohne "_obj_" ------------------------- # Gehoert der Stamm zu einem Auftraggeber? Das ist der Fall, wenn eine # Mission genauso heisst oder mit "_" beginnt. $auftraggeber = New-Object 'System.Collections.Generic.HashSet[string]' foreach ($s in $missionsstaemme) { $teile = $s.Split('_') for ($i = 1; $i -le $teile.Count; $i++) { [void]$auftraggeber.Add(($teile[0..($i-1)] -join '_')) } } $module = @{} foreach ($k in $map.Keys) { if ($k.IndexOf('_obj_', $IC) -ge 0) { continue } if ($k -notmatch '^([A-Za-z0-9]+)_([A-Za-z0-9]+)_(Short|Long|TTI)$') { continue } if (-not $auftraggeber.Contains($Matches[1].ToLowerInvariant())) { continue } $schluessel = ($Matches[1] + '_' + $Matches[2]) $form = $Matches[3].ToLowerInvariant() if (-not $module.ContainsKey($schluessel)) { $module[$schluessel] = [ordered]@{ modul = $Matches[1]; name = $Matches[2] ini_key = $schluessel; kurz = $null; lang = $null } } if ($form -eq 'short') { $module[$schluessel].kurz = $map[$k] } elseif ($form -eq 'long') { $module[$schluessel].lang = $map[$k] } # TTI ist die Beschriftung am Zielmarker - fuer einen Walkthrough # ueberfluessig, sie steht im Langtext ohnehin. } $regel2 = 0 foreach ($m in $module.Values) { if (-not $m.kurz -or -not $m.lang) { continue } $kurz = "$($m.kurz)".Trim() $lang = "$($m.lang)".Trim() if ($kurz -eq '' -or $lang -eq '') { continue } # Drei Muster, die durch die Doppelform rutschen, aber keine # Objectives sind: # - Kuerzel der Flugmodi (masterMode_NAV = "NAV", _SCM = "SCM") # - Beschriftungen mit Doppelpunkt (EscortShips_Health = "Ship Health:") # - reine Platzhalter (gtaa_token = "~mission(ObjectiveSetupShort)") if ($kurz.Length -le 4) { continue } if ($kurz.EndsWith(':')) { continue } if ($kurz -match '^~mission\([^)]*\)$') { continue } [void]$bausteine.Add($m) $regel2++ } & $log "$($bausteine.Count) Baustein-Objectives erkannt ($regel1 aus Modulen, $regel2 von Auftraggebern)." # =================================================================== # Glossar: Orte, Schiffe und Gegenstaende, die ein Auftragstext nennt # =================================================================== # # Dieselbe Sprachdatei kennt jeden benannten Ort (Stanton*/Pyro*/Nyx*), # jedes Schiff (vehicle_Name*) und jeden Gegenstand (item_Name*), oft mit # CIGs eigener Kurzbeschreibung daneben. Die Seite hebt damit im Auftrags- # text hervor, was man nachschlagen kann, und verlinkt zur Star Citizen # Wiki. Kanalunabhaengig; der Server fuehrt je Schluessel den letzten # Stand. # # Gegenstaende nur mit MEHRWORTIGEM Namen: "Red", "Station", "Access" # sind ebenfalls Gegenstaende und wuerden jeden zweiten Satz verlinken. # Gemessen an den Auftragstexten: von 8.561 Gegenstandsnamen kommen 39 # darin vor, davon 25 einwortige - und die sind bis auf zwei Rauschen. $glossar = New-Object System.Collections.ArrayList $kuerze = { param($t) if (-not $t) { return $null }; $t = $t -replace '\\n', ' '; if ($t.Length -gt 300) { $t.Substring(0, 297) + '...' } else { $t } } foreach ($k in $map.Keys) { $name = $map[$k]; if (-not $name -or $name.Length -lt 3 -or $name -match '^[\W\d]+$') { continue } $art = $null; $beschr = $null if ($k -match '^(Stanton|Pyro|Nyx)[0-9a-z]*_' -and $k -notmatch '_desc(ription)?$' -and $k -cnotmatch '^NYX_') { $art = 'location' foreach ($d in @("$($k)_desc", "$($k)_Desc")) { if ($map.ContainsKey($d)) { $beschr = $map[$d]; break } } } elseif ($k -match '^vehicle_Name' -and $k -notmatch '_short$') { $art = 'vehicle'; $d = $k -replace '^vehicle_Name', 'vehicle_Desc' if ($map.ContainsKey($d)) { $beschr = $map[$d] } } elseif ($k -match '^item_Name' -and $k -notmatch '_short$' -and ($name -split '\s+').Count -ge 2) { $art = 'item'; $d = $k -replace '^item_Name', 'item_Desc' if ($map.ContainsKey($d)) { $beschr = $map[$d] } } if (-not $art) { continue } [void]$glossar.Add([ordered]@{ key = $k; kind = $art; name = $name.Trim(); description = (& $kuerze $beschr) }) } & $log "$($glossar.Count) Glossareintraege (Orte, Schiffe, Gegenstaende)." # =================================================================== # Gegenstaende mit Datenkopf: Komponenten, Ruestung, Handwaffen # =================================================================== # # CIG stellt vielen item_Desc-Texten einen strukturierten Kopf voran, # Zeile fuer Zeile "Schluessel: Wert", dann eine Leerzeile, dann die # Prosa: # # Item Type: Power Plant\nManufacturer: ACOM\nSize: 1\nGrade: B\n # Class: Competition\n\nFour generations of racing knowledge ... # # Ruestung traegt darin Damage Reduction, Temp. Rating, Radiation # Protection und Scrub Rate; Handwaffen Magazine Size, Rate Of Fire, # Effective Range, Attachments. Das sind Spielwerte aus der Spieldatei # selbst - die Wahrheit, gegen die das Wiki nur Ergaenzung ist. Der Kopf # wird generisch gelesen: Bekannte Schluessel landen in eigenen # Feldern, der Rest unveraendert in "specs". Gemessen an LIVE 4.10: # 2.488 Texte mit Kopf, 2.211 davon mit Namen. # # Steht NACH dem Erkennungsblock, wie das Glossar: Aendert sich hier # etwas, aendert sich keine Missionsliste, also bleibt # $ERKENNUNG_VERSION unberuehrt. # Die meisten Schluessel heissen item_Desc mit item_Name # daneben. Bergbau, Bergung und Betankung schreiben anders: # item_Mining_Consumable_Surge_Desc, item_fuelpod_GRIN_BigAndFast_Desc, # Nozzle_FuelGiver_GRIN_NozzleSecure_Desc, item_descMining_Head_S00_Arbor # - und der Name steht jeweils unter demselben Schluessel mit "Name" # statt "Desc". Genommen wird jeder Schluessel mit Datenkopf, dessen # Namensschluessel es gibt; die Klasse ist der Schluessel ohne "item_" # und ohne das Desc-Stueck (bei item_Desc exakt , wie # bisher - daran haengen Blueprints und Wiki-Werte). $gegenstaende = New-Object System.Collections.ArrayList # Items whose type the global.ini writes (keys in small letters) - Set-DcbItemTypes leaves them. $gameTyped = @{} foreach ($k in $map.Keys) { $text = $map[$k] # Der Kopf steht am Anfang - oder, selten, hinter einem Vorspann # ("MUST BUY BASE MULTI-TOOL TO USE: ..."), dann in den ersten # drei Zeilen. if ($text -notmatch '^(Item Type|Manufacturer): ' -and $text -notmatch '^(?:[^\\]*\\n){1,3}(Item Type|Manufacturer): ') { continue } if ($k -notmatch '(?i)desc') { continue } # Schiffe tragen denselben Kopf (vehicle_DescAEGS_Hammerhead) - die # sind Fahrzeuge, keine Gegenstaende, und haben ihren eigenen Weg. if ($k.StartsWith('vehicle', $IC)) { continue } $klasse = Klasse-AusDescSchluessel $k if (-not $klasse) { continue } $nk = [regex]::Replace($k, '(?i)desc', 'Name', 1) $name = $null if ($map.ContainsKey($nk)) { $name = $map[$nk].Trim() } # Bergbau-Module und -Laser: der Name steht unter dem Schluessel # ohne "_Desc" (item_Mining_Consumable_Brandt). if (-not $name) { $nk = [regex]::Replace($k, '(?i)_desc$', ''); if ($nk -ne $k -and $map.ContainsKey($nk)) { $name = $map[$nk].Trim() } } if (-not $name) { continue } $kopf = [ordered]@{} $rest = New-Object System.Collections.ArrayList $imKopf = $true $vorspann = 0 foreach ($zeile in ($text -split '\\n')) { if ($imKopf) { if ($zeile.Trim() -eq '') { continue } # Ein Schluessel ist kurz und ohne Satzzeichen - die Prosa # kann selbst einen Doppelpunkt enthalten ("...matters: # signature detection"), und die faellt so nicht hinein. # Ein Schluessel schreit nicht ("MUST BUY BASE MULTI-TOOL TO USE:" ist Prosa). if ($zeile -match '^([A-Za-z][A-Za-z.\- ]{1,30}): (.*)$' -and $matches[1] -cnotmatch '^[A-Z\- ]{8,}$') { $kopf[$matches[1].Trim()] = $matches[2].Trim(); continue } # Vorspann vor dem Kopf: bleibt Prosa, der Kopf kommt noch. if ($kopf.Count -eq 0 -and $vorspann -lt 3) { $vorspann++; [void]$rest.Add($zeile); continue } $imKopf = $false } [void]$rest.Add($zeile) } $specs = [ordered]@{} foreach ($sk in $kopf.Keys) { if ($sk -in @('Item Type', 'Manufacturer', 'Size', 'Grade', 'Class')) { continue } $specs[$sk] = $kopf[$sk] } # The game's "Item Type", else what class, header and text say (Get-ItemType). $description = (($rest -join "`n").Trim()) $itemType = Get-ItemType $kopf $klasse $name $description if (($kopf.Contains('Item Type') -and $kopf['Item Type']) -or ($kopf.Contains('Type') -and $kopf['Type'])) { $gameTyped[$klasse.ToLowerInvariant()] = $true } [void]$gegenstaende.Add([ordered]@{ key = $klasse name = $name item_type = $itemType manufacturer = $(if ($kopf.Contains('Manufacturer')) { $kopf['Manufacturer'] } else { $null }) size = $(if ($kopf.Contains('Size')) { $kopf['Size'] } else { $null }) grade = $(if ($kopf.Contains('Grade')) { $kopf['Grade'] } else { $null }) class = $(if ($kopf.Contains('Class')) { $kopf['Class'] } else { $null }) specs = $specs description = $description }) } & $log "$($gegenstaende.Count) Gegenstaende mit Datenkopf (Komponenten, Ruestung, Handwaffen)." # =================================================================== # Orte: Systeme, Planeten, Monde, Stationen, Aussenposten # =================================================================== # # Der Schluessel eines Orts traegt seine Lage: Stanton2b_DrugUGF_003 # liegt auf Stanton2b (Daymar), dem Mond b des zweiten Planeten # (Crusader) im Stanton-System; das Segment danach sagt, was es ist # (DrugUGF, Outpost, L1, JumpPoint ...). Genommen wird jeder Schluessel # mit Beschreibung UND Namen; die Beschreibung ist CIGs eigener Text. # Was der Schluessel hergibt, wird hier zerlegt - die lesbaren Namen # fuer Planet und Mond stehen in derselben Datei unter dem Koerper- # schluessel (Stanton2b = "Daymar"). # # Nicht genommen: NYX_* in Grossbuchstaben (Bildschirmtexte einer # Anlage, keine Orte) und *_entrance_desc (Eingaenge der Gasblasen, # ohne eigenen Namen). $orte = New-Object System.Collections.ArrayList foreach ($k in $map.Keys) { if ($k -cmatch '^NYX_') { continue } if ($k -notmatch '^(?Stanton|Pyro|Nyx)(?\d*)(?[a-z]?)(?System)?(?:_(?.+?))?_desc(ription)?$') { continue } $basis = $k -replace '_desc(ription)?$', '' if (-not $map.ContainsKey($basis)) { continue } $name = $map[$basis].Trim() if (-not $name) { continue } # Schreibweise vereinheitlichen: 'stanton2_housing_grimhex' meint Stanton2. $system = (Get-Culture).TextInfo.ToTitleCase($matches['system'].ToLower()) $planet = $matches['planet']; $moon = $matches['moon'].ToLower(); $sub = $matches['sub']; $istSystem = [bool]$matches['sys'] $koerper = if ($planet) { "$system$planet$moon" } else { $null } # Lesbare Namen der Koerper. Stanton2b -> "Daymar", Stanton2 -> "Crusader". $koerperName = $null; $planetName = $null if ($koerper -and $map.ContainsKey($koerper)) { $koerperName = $map[$koerper].Trim() } if ($moon -and $map.ContainsKey("$system$planet")) { $planetName = $map["$system$planet"].Trim() } # Art: das erste Segment des Zusatzes, oder die Lage selbst. # Manche Schluessel wiederholen den Namen des Koerpers # (Stanton1_Hurston_Lorville_Hospital, Stanton1b_Aberdeen_Prison) - # der faellt weg. Die Monde von Pyro V tragen ihren Namen NUR im # Schluessel (Pyro5a_Ignis, kein Eintrag "Pyro5a"): Ist der Zusatz # ein Wort und gleich dem Namen, ist der Ort der Koerper selbst. $segmente = @() if ($sub) { $segmente = @($sub -split '_') if ($segmente.Count -gt 1 -and $koerperName -and $segmente[0] -ieq $koerperName) { $segmente = $segmente[1..($segmente.Count - 1)] } } $art = if ($segmente.Count -eq 1 -and $segmente[0] -ieq $name -and $koerper) { if ($moon) { 'Moon' } else { 'Planet' } } elseif ($segmente.Count -gt 0 -and $segmente[-1] -ieq 'Hospital') { 'Hospital' } elseif ($segmente.Count -gt 0) { $segmente[0] } elseif ($istSystem) { 'System' } elseif ($moon) { 'Moon' } elseif ($planet) { 'Planet' } else { 'System' } if (($art -eq 'Moon' -or $art -eq 'Planet') -and -not $koerperName) { $koerperName = $name } [void]$orte.Add([ordered]@{ key = $basis name = $name system = $system body_key = $koerper body_name = $koerperName planet_name = $planetName kind_key = $art description = ($map[$k] -replace '\\n', "`n").Trim() }) } # Zweiter Durchgang: Orte auf einem Koerper, der seinen Namen nur im # eigenen Schluessel traegt (Pyro5a_Ignis), bekommen den Namen von dort. $koerperNamen = @{} foreach ($o in $orte) { if (($o.kind_key -eq 'Moon' -or $o.kind_key -eq 'Planet') -and $o.body_key) { $koerperNamen[$o.body_key] = $o.body_name } } foreach ($o in $orte) { if ($o.body_key -and -not $o.body_name -and $koerperNamen.ContainsKey($o.body_key)) { $o.body_name = $koerperNamen[$o.body_key] } } & $log "$($orte.Count) Orte mit Namen und Beschreibung." # =================================================================== # Game2.dcb: Maklereintraege (Belohnung, Fristen, Reputation) # =================================================================== # # Steht nach dem Erkennungsblock; $ERKENNUNG_VERSION bleibt. Der # Titel-Schluessel jeder Fassung fuehrt zum Praefix der Seite. $schluesselZuPrefix = @{} foreach ($f in $fassungen) { $schluesselZuPrefix[$f.Key.ToLowerInvariant()] = $f.Prefix } $broker = $null $dcbItems = $null $dcbClasses = $null $dcbMining = $null $dcbScenarios = $null try { $dcb = Lese-Broker $kanal $schluesselZuPrefix $map $log; if ($dcb) { $broker = $dcb.broker; $dcbItems = $dcb.items; $dcbClasses = $dcb.classes; $dcbMining = $dcb.mining; $dcbScenarios = $dcb.scenarios } } catch { & $log "Game2.dcb nicht gelesen: $($_.Exception.Message)" if ($env:SCQ_DEBUG) { Write-Host $_.Exception.GetType().FullName; Write-Host $_.Exception.StackTrace; Write-Host $_.ScriptStackTrace } } if ($dcbClasses) { $retyped = Set-DcbItemTypes $gegenstaende $dcbClasses $gameTyped if ($retyped) { & $log (" Utility turrets: {0} items typed from Game2.dcb." -f $retyped) } } return [ordered]@{ build = [ordered]@{ build_ref = $kanal.Build channel = $kanal.Name # Lesbare Fassung neben der Buildnummer. Die Seite zeigt damit an, # fuer welche Spielversion ein Walkthrough geschrieben wurde - # "Alpha 4.10.0" erkennt jemand wieder, "12545750" nicht. game_version = $kanal.Version # MIT WELCHEN REGELN gelesen wurde. Ohne diese Angabe misst die # Differenz zweier Builds nicht das Spiel, sondern uns: Am # 09.09.2026 meldete die Startseite "1291 neue Missionen" fuer # einen Build, dessen global.ini byte-identisch mit der des # Vorgaengers war. Neu war nur unsere Erkennung. # # Die Nummer steht in $ERKENNUNG_VERSION ganz oben und wird bei # jeder Regelaenderung erhoeht; tests/extractor.ps1 schlaegt an, # wenn der Erkennungsblock sich aendert und sie stehenbleibt. extractor_version = $ERKENNUNG_VERSION source_file = $quelle source_origin = $kanal.Ini # Aus der Launcher-Sicherung zurueckgerechnet statt direkt gelesen. # Der Text ist derselbe CIG-Text; der Vermerk haelt nur fest, auf # welchem Weg er zustande kam. restored = $wiederhergst source_sha256 = (Get-FileHash $quelle -Algorithm SHA256).Hash.ToLowerInvariant() extracted_at = (Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ssZ') mission_count = $result.Count module_count = $bausteine.Count # Nach erfolgreicher Rueckrechnung enthaelt die Quelle keine fremden # Inhalte mehr - sonst haette restore-original.ps1 abgebrochen. patched = ($blocker.Count -gt 0 -and -not $wiederhergst) } missions = @($result | Sort-Object prefix) # Wiederverwendbare Bausteine. Sie haengen an keiner Mission und # stehen deshalb neben der Missionsliste, nicht darin. modules = @($bausteine | Sort-Object ini_key) glossary = @($glossar | Sort-Object key) items = @($gegenstaende | Sort-Object key) locations = @($orte | Sort-Object key) # Aus Game2.dcb; fehlt ohne Data.p4k (lose Sprachdatei) - dann bleibt # auf dem Server, was der letzte Lauf mit p4k gebracht hat. broker = $(if ($broker -ne $null) { @($broker | Sort-Object record) } else { $null }) dcb_items = $(if ($dcbItems -ne $null) { @($dcbItems | Sort-Object class) } else { $null }) dcb_classes = $(if ($dcbClasses -ne $null) { @($dcbClasses | Sort-Object class) } else { $null }) # What can be mined where - places, compositions, resources. mining = $dcbMining # The points each reward tier of an event needs (Get-ScenarioRows). scenarios = $dcbScenarios } } # =========================================================================== # Hochladen # =========================================================================== function Lade-Hoch($doc, $apiBase, $token, [scriptblock]$log) { $url = $apiBase.TrimEnd('/') + '/api/v1/import' if ($Force) { $url += '?force=1' } & $log "POST $url" $json = $doc | ConvertTo-Json -Depth 12 -Compress $bytes = [System.Text.Encoding]::UTF8.GetBytes($json) & $log ("Nutzlast: {0:N0} KB" -f ($bytes.Length / 1024)) try { $a = Invoke-RestMethod -Uri $url -Method Post -Body $bytes ` -ContentType 'application/json; charset=utf-8' ` -Headers @{ Authorization = "Bearer $token" } -TimeoutSec 300 return $a } catch { # Die API antwortet bei Ablehnungen mit JSON - das ist wertvoller als # der blanke Statuscode, deshalb wird der Rumpf ausgelesen. $resp = $_.Exception.Response if ($resp) { $sr = New-Object System.IO.StreamReader($resp.GetResponseStream()) $rumpf = $sr.ReadToEnd(); $sr.Close() throw "HTTP $([int]$resp.StatusCode): $rumpf" } throw } } function Melde-Build($kanal, $apiBase, $token, [scriptblock]$log) { <# Meldet der Seite, welchen Spielstand diese Installation hat - ohne etwas hochzuladen. Warum ueberhaupt: Es gibt keine oeffentliche Schnittstelle von RSI, an der sich die aktuelle Build-Nummer ablesen liesse. Die Installation weiss es aber, und diese Meldung ist winzig. So sieht der Betreiber, dass ein neuer Stand draussen ist, bevor ihn jemand einspielt. Uebertragen werden nur Kanal und Build-Nummer. Ein Fehlschlag ist folgenlos - das eigentliche Hochladen haengt nicht daran. #> if (-not $token -or -not $apiBase) { return } try { $rumpf = @{ channel = $kanal.Name; build_ref = $kanal.Build } | ConvertTo-Json -Compress $a = Invoke-RestMethod -Uri ($apiBase.TrimEnd('/') + '/api/v1/build-ping') -Method Post ` -Body ([System.Text.Encoding]::UTF8.GetBytes($rumpf)) ` -ContentType 'application/json; charset=utf-8' ` -Headers @{ Authorization = "Bearer $token" } -TimeoutSec 15 if ($a.bekannt) { & $log "Stand $($kanal.Build) ist bereits eingelesen." } else { & $log "Stand $($kanal.Build) fehlt der Seite noch - ein Upload hilft." } } catch { # Bewusst still: Die Meldung ist eine Nettigkeit, kein Arbeitsschritt. } } function Lauf($kanal, $apiBase, $token, [scriptblock]$log) { try { Lauf-Inner $kanal $apiBase $token $log } finally { # Auch nach einem Abbruch: Die ausgepackten Sprachdateien sind # Zwischenergebnisse und haben nichts zu suchen, wenn der Lauf vorbei ist. Raeume-Arbeit $log } } function Lauf-Inner($kanal, $apiBase, $token, [scriptblock]$log) { # Auch die Buildmeldung bleibt beim Trockenlauf aus. Sie ist harmlos, aber # "nichts gesendet" soll woertlich stimmen. if (-not $WhatIfOnly) { Melde-Build $kanal $apiBase $token $log } $doc = Extrahiere $kanal $log if ($WhatIfOnly) { & $log ("Trockenlauf: {0} Missionen aus Build {1} ({2}) - nichts gesendet." -f $doc.build.mission_count, $doc.build.build_ref, $doc.build.channel) & $log " Pruefsumme: $($doc.build.source_sha256.Substring(0,16))..." if ($doc.build.restored) { & $log ' Quelle wurde aus der Launcher-Sicherung zurueckgerechnet.' } # Das ganze Paket daneben ablegen, wenn gewuenscht: So laesst sich # pruefen, WAS gesendet wuerde, nicht nur wie viel. if ($env:SCQ_DUMP) { $doc | ConvertTo-Json -Depth 12 -Compress | Set-Content -Path $env:SCQ_DUMP -Encoding UTF8 & $log " Paket geschrieben nach $env:SCQ_DUMP" } return } $a = Lade-Hoch $doc $apiBase $token $log if ($a.unchanged) { & $log "Unveraendert - dieser Build ist bereits importiert (build_id $($a.build_id))." if ($null -ne $a.module) { & $log " $($a.module) wiederverwendbare Bausteine fortgeschrieben." } if ($a.items) { & $log " $($a.items) Gegenstaende, $($a.locations) Orte, $($a.broker) Maklereintraege, $($a.classes) Entity-Klassen geschrieben." } if ($a.mining) { & $log " $($a.mining) mining entries written." } } else { & $log "Fertig. Build $($a.build_id), Kanal $($a.channel)." & $log (" neu {0} | aktualisiert {1} | unveraendert {2}" -f $a.missions.neu, $a.missions.aktualisiert, $a.missions.unveraendert) if ($a.module) { & $log " $($a.module) wiederverwendbare Bausteine fortgeschrieben." } if ($a.aufgeraeumt) { & $log " $($a.aufgeraeumt) Eintraege des vorigen Laufs entfernt." } if ($a.verschwunden) { & $log " $($a.verschwunden) Missionen aus dem Vorgaengerbuild fehlen jetzt." } if ($a.workflows_zur_pruefung) { & $log " $($a.workflows_zur_pruefung) Workflows zur Pruefung markiert." } if ($a.items) { & $log " $($a.items) Gegenstaende, $($a.locations) Orte, $($a.broker) Maklereintraege, $($a.classes) Entity-Klassen geschrieben." } if ($a.mining) { & $log " $($a.mining) mining entries written." } } return $a } # =========================================================================== # Start # =========================================================================== # -Update: install a newer version now and end - for automation and tests. # Not while a companion runs: its cmd.exe still reads the .cmd, and the swap # waits only for this one. if ($Update) { $self = Get-UpdateSelf if (-not $self) { Write-Host 'Updates are for the downloaded .cmd only - not for a .ps1 or a copy inside a git working tree.'; exit 1 } $running = $null if ([System.Threading.Mutex]::TryOpenExisting('Local\VersefilesCompanion', [ref]$running)) { $running.Dispose() Write-Host 'The companion is running - it updates itself once the game is closed.' exit 1 } try { $offer = Get-UpdateOffer $script:Settings.apiBase 'companion' $COMPANION_VERSION $UPDATE_AGENT ($script:Settings.updateChannel -eq 'beta') if (-not $offer) { Write-Host ('Versefiles Companion {0} is up to date.' -f $COMPANION_VERSION); exit 0 } $file = Save-UpdateFile $offer 'companion' $COMPANION_VERSION $UPDATE_AGENT Start-UpdateInstall $file $self 'companion' $COMPANION_VERSION $offer.Version $false '' Write-Host ('Version {0} is verified and replaces this one as soon as this window closes.' -f $offer.Version) exit 0 } catch { Write-Host ('Update failed: ' + $_.Exception.Message) exit 1 } } # -GameData: read the installed build of -Channel and send it to Versefiles # with the companion key - the old uploader's -NoGui. The window runs this in a # process of its own and shows what it writes: every line goes out at once, # in UTF-8. The run's own lines are still German (see "Game data" above). if ($GameData) { [Console]::OutputEncoding = $UTF8 $say = { param($m) [Console]::Out.WriteLine($m); [Console]::Out.Flush() } try { $SC_ROOT = Get-GameRoot $GamePath $name = if ($Channel) { $Channel } else { 'LIVE' } $kanaele = @(Finde-Kanaele) $kanal = $kanaele | Where-Object { $_.Name -eq $name } | Select-Object -First 1 if (-not $kanal) { throw "No channel '$name' in $SC_ROOT - there are: $(@($kanaele.Name) -join ', ')." } # A channel the launcher still downloads has no language file; the run # would end in a path error that says nothing. if (-not $kanal.Bereit) { throw "$name cannot be read yet: $($kanal.Grund)." } # The window's key (Start-GameDataRun), else the saved one. $key = if ($env:VF_GAMEDATA_KEY) { [string]$env:VF_GAMEDATA_KEY } else { [string]$script:Settings.key } if (-not $key -and -not $WhatIfOnly) { throw 'No key - enter your companion key in the window under "Game data".' } & $say ('{0}, build {1}, in {2}' -f $kanal.Name, $kanal.Build, $SC_ROOT) Raeume-Altlasten $say Lauf $kanal ([string]$script:Settings.apiBase) $key $say | Out-Null exit 0 } catch { & $say ('Error: ' + $_.Exception.Message) exit 1 } } Add-Type -TypeDefinition $GAMELOG_SOURCE -Language CSharp $GAME_ROOT = Get-GameRoot $GamePath Read-Index if ($NoGui) { $say = { param($m) Write-Host $m } & $say ("Versefiles Companion {0} - game folder {1}" -f $COMPANION_VERSION, $GAME_ROOT) [void](Import-LogBackups $GAME_ROOT $say) Start-LogFollowers $GAME_ROOT $fresh = Receive-NewEvents if ($Upload) { Update-Account & $say ('Versefiles account: {0}' -f $(switch ($script:Account.Status) { 'ok' { $script:Account.Name } 'rejected' { 'key rejected' } 'error' { 'not reachable' } default { 'no key' } })) $n = Send-Journal $say & $say ("Upload: {0} events sent, status {1}{2}" -f $n, $script:UploadState.Status, $(if ($script:UploadState.Message) { ' - ' + $script:UploadState.Message } else { '' })) } if ($Once) { Write-Summary (Read-Journal) if ($Upload -and $script:UploadState.Status -ne 'ok') { exit 2 } return } & $say 'Following the game. Ctrl+C to stop.' $ticks = 0 while ($true) { $ended = $false foreach ($e in $fresh) { $text = Get-EventText $e if ($text) { & $say ("{0:HH:mm:ss} {1}" -f (Get-Time $e), $text) } if ((Get-Kind $e) -eq 'session_ended') { $ended = $true } } # Every five minutes, and right when a session ends. if ($Upload -and ($ended -or ++$ticks -ge 150)) { $ticks = 0; [void](Send-Journal $say) } Start-Sleep -Seconds 2 $fresh = Receive-NewEvents } } # =========================================================================== # Window and tray icon # =========================================================================== Add-Type -AssemblyName System.Windows.Forms Add-Type -AssemblyName System.Drawing [System.Windows.Forms.Application]::EnableVisualStyles() # For the overlay over the game (companion 0.7.0): a window that never takes # the focus or a click - layered, click-through, no taskbar button, always on # top - and a key combination to bring it back (watched since 0.7.2). Both are # ordinary parts of this process; nothing reaches into the game, which is # what Easy Anti-Cheat watches. Over the game it shows in borderless mode # only - in exclusive fullscreen a window on top would minimise the game. Add-Type -ReferencedAssemblies System.Windows.Forms, System.Drawing -TypeDefinition @' using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics; using System.Drawing; using System.Runtime.InteropServices; using System.Text; using System.Windows.Forms; namespace Versefiles.Companion { public class OverlayForm : Form { public OverlayForm() { FormBorderStyle = FormBorderStyle.None; ShowInTaskbar = false; StartPosition = FormStartPosition.Manual; TopMost = true; } protected override bool ShowWithoutActivation { get { return true; } } protected override CreateParams CreateParams { get { CreateParams cp = base.CreateParams; // LAYERED | TRANSPARENT (clicks pass through) | TOOLWINDOW (not in Alt+Tab) | NOACTIVATE | TOPMOST cp.ExStyle |= 0x00080000 | 0x00000020 | 0x00000080 | 0x08000000 | 0x00000008; return cp; } } } // A key combination, watched rather than registered: every 30 ms it asks // Windows whether its own few keys are down - the key first, the // modifiers only while it is. A hotkey from RegisterHotKey never fired // while Star Citizen had the focus (Alex's test, 28.09.2026) - most // likely the game reads the keyboard as raw input with such hotkeys // switched off. Watching also leaves the keys to the game. public sealed class Hotkey : IDisposable { [DllImport("user32.dll")] static extern short GetAsyncKeyState(int key); const int VK_SHIFT = 0x10, VK_CONTROL = 0x11, VK_MENU = 0x12, VK_LWIN = 0x5B, VK_RWIN = 0x5C; readonly System.Windows.Forms.Timer timer = new System.Windows.Forms.Timer(); readonly uint modifiers; // as RegisterHotKey counts them: 1 Alt, 2 Ctrl, 4 Shift, 8 Win readonly int key; // Starts as "down": keys held while the companion starts do not count. bool wasDown = true; public event EventHandler Pressed; // Whether a key is down. tests/gamelog.ps1 puts a pretend keyboard here. public Func IsDown = vk => (GetAsyncKeyState(vk) & 0x8000) != 0; public Hotkey(uint modifiers, uint key) { this.modifiers = modifiers; this.key = (int)key; timer.Interval = 30; timer.Tick += delegate { Poll(); }; timer.Start(); } // Exactly these modifiers: Ctrl+Alt+Shift+P is not Alt+Shift+P. public void Poll() { bool down = IsDown(key) && IsDown(VK_MENU) == ((modifiers & 1) != 0) && IsDown(VK_CONTROL) == ((modifiers & 2) != 0) && IsDown(VK_SHIFT) == ((modifiers & 4) != 0) && (IsDown(VK_LWIN) || IsDown(VK_RWIN)) == ((modifiers & 8) != 0); // Once per press: holding the keys does not repeat. if (down && !wasDown && Pressed != null) { Pressed(this, EventArgs.Empty); } wasDown = down; } public void Dispose() { timer.Stop(); timer.Dispose(); } } // A flat button that looks switched off when it is: WinForms keeps the // coloured border of a disabled flat button and draws its text in a grey // meant for light windows - on the dark window it looked on and // unreadable at once (Alex, 29.09.2026). Disabled, border and text take // DisabledColor. public class FlatButton : Button { public Color DisabledColor { get; set; } public FlatButton() { FlatStyle = FlatStyle.Flat; DisabledColor = Color.FromArgb(123, 142, 166); } protected override void OnPaint(PaintEventArgs e) { base.OnPaint(e); if (Enabled) { return; } e.Graphics.Clear(BackColor); if (FlatAppearance.BorderSize > 0) { using (Pen pen = new Pen(DisabledColor)) { e.Graphics.DrawRectangle(pen, 0, 0, Width - 1, Height - 1); } } TextRenderer.DrawText(e.Graphics, Text, Font, ClientRectangle, DisabledColor, TextFormatFlags.HorizontalCenter | TextFormatFlags.VerticalCenter | TextFormatFlags.SingleLine | TextFormatFlags.NoPrefix); } } // The Home view (0.15.0, in the style of the RSI Launcher): the fankit // wallpaper fitted without cropping into ImageRect (computed in script, // Get-FitRectangle), the margins in BackColor, a dark layer from the // left - but not over FreeCorner, where the wallpaper's Star Citizen // mark stays exactly as it is (fankit agreement 2j). No wallpaper: a // fixed starfield. Children with a transparent back colour show it. public class HeroPanel : Panel { public Image Wallpaper { get; set; } public Rectangle ImageRect { get; set; } public Size FreeCorner { get; set; } public Color ShadeColor { get; set; } public int ShadeLeftAlpha { get; set; } public HeroPanel() { SetStyle(ControlStyles.AllPaintingInWmPaint | ControlStyles.OptimizedDoubleBuffer | ControlStyles.UserPaint | ControlStyles.ResizeRedraw, true); ShadeColor = Color.FromArgb(8, 17, 27); ShadeLeftAlpha = 190; } protected override void OnPaintBackground(PaintEventArgs e) { Graphics g = e.Graphics; g.Clear(BackColor); if (Wallpaper == null || ImageRect.Width <= 0 || ImageRect.Height <= 0) { DrawStars(g); return; } g.InterpolationMode = System.Drawing.Drawing2D.InterpolationMode.HighQualityBicubic; g.PixelOffsetMode = System.Drawing.Drawing2D.PixelOffsetMode.HighQuality; g.DrawImage(Wallpaper, ImageRect); Rectangle band = new Rectangle(ImageRect.X - 1, ImageRect.Y, ImageRect.Width + 2, ImageRect.Height); using (var shade = new System.Drawing.Drawing2D.LinearGradientBrush(band, Color.FromArgb(ShadeLeftAlpha, ShadeColor), Color.FromArgb(0, ShadeColor), 0f)) using (Region region = new Region(ImageRect)) { region.Exclude(new Rectangle(ImageRect.Right - FreeCorner.Width, ImageRect.Bottom - FreeCorner.Height, FreeCorner.Width, FreeCorner.Height)); g.FillRegion(shade, region); } } void DrawStars(Graphics g) { Random random = new Random(4242); int count = Math.Max(40, Width * Height / 2500); for (int i = 0; i < count; i++) { int x = random.Next(Math.Max(1, Width)), y = random.Next(Math.Max(1, Height)); int size = random.Next(10) == 0 ? 2 : 1; using (SolidBrush star = new SolidBrush(Color.FromArgb(random.Next(70, 230), 200, 220, 255))) { g.FillRectangle(star, x, y, size, size); } } } } // The launcher's outlined button with its bottom right corner cut off. public class CutCornerButton : Control { public Color AccentColor { get; set; } public Color DisabledColor { get; set; } public int Cut { get; set; } bool hover, down; public CutCornerButton() { SetStyle(ControlStyles.SupportsTransparentBackColor | ControlStyles.AllPaintingInWmPaint | ControlStyles.OptimizedDoubleBuffer | ControlStyles.UserPaint | ControlStyles.ResizeRedraw, true); BackColor = Color.Transparent; AccentColor = Color.FromArgb(57, 194, 255); DisabledColor = Color.FromArgb(123, 142, 166); Cut = 12; Cursor = Cursors.Hand; AccessibleRole = AccessibleRole.PushButton; TabStop = true; } public void PerformClick() { if (Enabled) { OnClick(EventArgs.Empty); } } // Like a real button (review of 0.15.0): only the left mouse button // clicks - a right click on START GAME started the launcher - and // Space or Enter do the same from the keyboard. protected override void OnClick(EventArgs e) { MouseEventArgs mouse = e as MouseEventArgs; if (mouse != null && mouse.Button != MouseButtons.Left) { return; } base.OnClick(e); } protected override bool IsInputKey(Keys keyData) { return keyData == Keys.Enter || keyData == Keys.Space || base.IsInputKey(keyData); } protected override void OnKeyUp(KeyEventArgs e) { if (e.KeyCode == Keys.Space || e.KeyCode == Keys.Enter) { e.Handled = true; PerformClick(); } base.OnKeyUp(e); } protected override void OnGotFocus(EventArgs e) { Invalidate(); base.OnGotFocus(e); } protected override void OnLostFocus(EventArgs e) { Invalidate(); base.OnLostFocus(e); } // The largest size up to the font's own at which the text fits the // width ("RSI LAUNCHER NOT FOUND" did not, at 12 pt). public static float FitFontSize(Graphics g, string text, Font font, float width) { float size = font.Size; while (size > 7f) { using (Font trial = new Font(font.FontFamily, size, font.Style)) { if (g.MeasureString(text ?? "", trial).Width <= width) { return size; } } size -= 0.5f; } return size; } public static System.Drawing.Drawing2D.GraphicsPath CutPath(Rectangle r, int cut) { var p = new System.Drawing.Drawing2D.GraphicsPath(); p.AddPolygon(new Point[] { new Point(r.Left, r.Top), new Point(r.Right, r.Top), new Point(r.Right, r.Bottom - cut), new Point(r.Right - cut, r.Bottom), new Point(r.Left, r.Bottom) }); return p; } protected override void OnMouseEnter(EventArgs e) { hover = true; Invalidate(); base.OnMouseEnter(e); } protected override void OnMouseLeave(EventArgs e) { hover = false; down = false; Invalidate(); base.OnMouseLeave(e); } protected override void OnMouseDown(MouseEventArgs e) { if (e.Button == MouseButtons.Left) { down = true; Invalidate(); } base.OnMouseDown(e); } protected override void OnMouseUp(MouseEventArgs e) { down = false; Invalidate(); base.OnMouseUp(e); } protected override void OnEnabledChanged(EventArgs e) { Cursor = Enabled ? Cursors.Hand : Cursors.Default; Invalidate(); base.OnEnabledChanged(e); } protected override void OnTextChanged(EventArgs e) { Invalidate(); base.OnTextChanged(e); } protected override void OnPaint(PaintEventArgs e) { Graphics g = e.Graphics; g.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.AntiAlias; g.TextRenderingHint = System.Drawing.Text.TextRenderingHint.AntiAliasGridFit; Color line = Enabled ? AccentColor : DisabledColor; using (var path = CutPath(new Rectangle(1, 1, Width - 3, Height - 3), Cut)) { int alpha = !Enabled ? 0 : down ? 70 : hover ? 45 : 18; if (alpha > 0) { using (SolidBrush fill = new SolidBrush(Color.FromArgb(alpha, line))) { g.FillPath(fill, path); } } using (Pen pen = new Pen(line, 2f)) { g.DrawPath(pen, path); } } float size = FitFontSize(g, Text, Font, Width - 8); Font face = size < Font.Size ? new Font(Font.FontFamily, size, Font.Style) : Font; try { using (SolidBrush text = new SolidBrush(Enabled ? Color.White : DisabledColor)) using (StringFormat format = new StringFormat { Alignment = StringAlignment.Center, LineAlignment = StringAlignment.Center, Trimming = StringTrimming.EllipsisCharacter, FormatFlags = StringFormatFlags.NoWrap }) { g.DrawString(Text, face, text, new RectangleF(4, 0, Width - 8, Height), format); } } finally { if (!ReferenceEquals(face, Font)) { face.Dispose(); } } if (Focused && ShowFocusCues) { using (Pen focus = new Pen(Color.FromArgb(160, Color.White)) { DashStyle = System.Drawing.Drawing2D.DashStyle.Dot }) { g.DrawRectangle(focus, 5, 5, Width - 12 - Cut / 2, Height - 11); } } } } // A number on the wallpaper: dark, half see-through, cut corner, the // value big and what it is small below it - drawn here, not by labels, // so that nothing flickers when a range is chosen. public class StatCard : Control { string value = "", caption = ""; public string Value { get { return value; } set { this.value = value ?? ""; Invalidate(); } } public string Caption { get { return caption; } set { caption = value ?? ""; Invalidate(); } } public Font ValueFont { get; set; } public Font CaptionFont { get; set; } public Color FillColor { get; set; } public int FillAlpha { get; set; } public Color BorderColor { get; set; } public Color CaptionColor { get; set; } public int Cut { get; set; } public StatCard() { SetStyle(ControlStyles.SupportsTransparentBackColor | ControlStyles.AllPaintingInWmPaint | ControlStyles.OptimizedDoubleBuffer | ControlStyles.UserPaint | ControlStyles.ResizeRedraw, true); BackColor = Color.Transparent; ForeColor = Color.White; FillColor = Color.FromArgb(8, 17, 27); FillAlpha = 185; BorderColor = Color.FromArgb(44, 69, 96); CaptionColor = Color.FromArgb(159, 179, 200); Cut = 10; ValueFont = new Font("Segoe UI", 15, FontStyle.Bold); CaptionFont = new Font("Segoe UI", 8.5f); // Shows a number, does nothing: no stop for the Tab key. SetStyle(ControlStyles.Selectable, false); TabStop = false; } protected override void OnPaint(PaintEventArgs e) { Graphics g = e.Graphics; g.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.AntiAlias; g.TextRenderingHint = System.Drawing.Text.TextRenderingHint.AntiAliasGridFit; using (var path = CutCornerButton.CutPath(new Rectangle(0, 0, Width - 1, Height - 1), Cut)) { using (SolidBrush fill = new SolidBrush(Color.FromArgb(FillAlpha, FillColor))) { g.FillPath(fill, path); } using (Pen pen = new Pen(BorderColor)) { g.DrawPath(pen, path); } } using (StringFormat format = new StringFormat { Trimming = StringTrimming.EllipsisCharacter, FormatFlags = StringFormatFlags.NoWrap }) { using (SolidBrush text = new SolidBrush(ForeColor)) { g.DrawString(value, ValueFont, text, new RectangleF(9, 5, Width - 16, ValueFont.Height + 4), format); } using (SolidBrush text = new SolidBrush(CaptionColor)) { g.DrawString(caption, CaptionFont, text, new RectangleF(10, Height - CaptionFont.Height - 7, Width - 18, CaptionFont.Height + 2), format); } } } } // A tab of the launcher's kind: text, and a bar under the chosen one. public class UnderlineTab : Control { bool selected, hover; public bool Selected { get { return selected; } set { selected = value; Invalidate(); } } public Color IdleColor { get; set; } public Color SelectedColor { get; set; } public Color UnderlineColor { get; set; } public UnderlineTab() { SetStyle(ControlStyles.SupportsTransparentBackColor | ControlStyles.AllPaintingInWmPaint | ControlStyles.OptimizedDoubleBuffer | ControlStyles.UserPaint | ControlStyles.ResizeRedraw, true); BackColor = Color.Transparent; IdleColor = Color.FromArgb(123, 142, 166); SelectedColor = Color.White; UnderlineColor = Color.FromArgb(57, 194, 255); Cursor = Cursors.Hand; Margin = new Padding(0, 0, 18, 0); AccessibleRole = AccessibleRole.PageTab; TabStop = true; } // Left click, Space or Enter - like the button above. protected override void OnClick(EventArgs e) { MouseEventArgs mouse = e as MouseEventArgs; if (mouse != null && mouse.Button != MouseButtons.Left) { return; } base.OnClick(e); } protected override bool IsInputKey(Keys keyData) { return keyData == Keys.Enter || keyData == Keys.Space || base.IsInputKey(keyData); } protected override void OnKeyUp(KeyEventArgs e) { if (e.KeyCode == Keys.Space || e.KeyCode == Keys.Enter) { e.Handled = true; if (Enabled) { OnClick(EventArgs.Empty); } } base.OnKeyUp(e); } protected override void OnGotFocus(EventArgs e) { Invalidate(); base.OnGotFocus(e); } protected override void OnLostFocus(EventArgs e) { Invalidate(); base.OnLostFocus(e); } void Measure() { Size text = TextRenderer.MeasureText(Text ?? "", Font); Size = new Size(text.Width + 2, text.Height + 10); } protected override void OnTextChanged(EventArgs e) { Measure(); Invalidate(); base.OnTextChanged(e); } protected override void OnFontChanged(EventArgs e) { Measure(); Invalidate(); base.OnFontChanged(e); } protected override void OnMouseEnter(EventArgs e) { hover = true; Invalidate(); base.OnMouseEnter(e); } protected override void OnMouseLeave(EventArgs e) { hover = false; Invalidate(); base.OnMouseLeave(e); } protected override void OnPaint(PaintEventArgs e) { Graphics g = e.Graphics; g.TextRenderingHint = System.Drawing.Text.TextRenderingHint.AntiAliasGridFit; Color text = selected || hover ? SelectedColor : IdleColor; using (SolidBrush brush = new SolidBrush(text)) { g.DrawString(Text, Font, brush, 0, 2); } if (selected) { using (SolidBrush bar = new SolidBrush(UnderlineColor)) { g.FillRectangle(bar, 0, Height - 3, Width, 2); } } if (Focused && ShowFocusCues) { using (Pen focus = new Pen(Color.FromArgb(160, Color.White)) { DashStyle = System.Drawing.Drawing2D.DashStyle.Dot }) { g.DrawRectangle(focus, 0, 0, Width - 1, Height - 5); } } } } // A program run in a process of its own - the game data run, which takes // minutes and a lot of memory. Its output lines wait here until the // window takes them on its own thread: script cannot run on the threads // that read them. public sealed class ChildRun : IDisposable { readonly ConcurrentQueue lines = new ConcurrentQueue(); readonly Process process = new Process(); // arguments go on the command line; argsVariable, if not null, into // QG_ARGS - the way the shipped .cmd hands its arguments on. public ChildRun(string fileName, string arguments, string argsVariable) { ProcessStartInfo psi = new ProcessStartInfo(fileName, arguments); psi.UseShellExecute = false; psi.CreateNoWindow = true; psi.RedirectStandardOutput = true; psi.RedirectStandardError = true; psi.StandardOutputEncoding = new UTF8Encoding(false); psi.StandardErrorEncoding = new UTF8Encoding(false); psi.EnvironmentVariables.Remove("QG_ARGS"); if (argsVariable != null) { psi.EnvironmentVariables["QG_ARGS"] = argsVariable; } process.StartInfo = psi; process.OutputDataReceived += (s, e) => { if (e.Data != null) { lines.Enqueue(e.Data); } }; process.ErrorDataReceived += (s, e) => { if (e.Data != null) { lines.Enqueue(e.Data); } }; process.Start(); process.BeginOutputReadLine(); process.BeginErrorReadLine(); } // The lines that came since the last call. public string[] Take() { List taken = new List(); string line; while (lines.TryDequeue(out line)) { taken.Add(line); } return taken.ToArray(); } // Ended, with all of its output read. public bool Ended { get { if (!process.HasExited) { return false; } process.WaitForExit(); // without a timeout it also waits for the last lines return true; } } public int ExitCode { get { return process.ExitCode; } } public void Stop() { try { if (!process.HasExited) { process.Kill(); } } catch (InvalidOperationException) { } // ended meanwhile catch (System.ComponentModel.Win32Exception) { } // ending right now } public void Dispose() { process.Dispose(); } } } '@ # Started from the .cmd, the console window has nothing to show while the # tray icon lives. It comes back if something goes wrong, so the error stays # readable. Started with -File from a terminal it stays alone - hiding it # would hide the terminal. Started without a window (0.13.1) there is none; # where Windows Terminal hosts consoles this never could hide one, which is # why the .cmd starts itself without a window now. Add-Type -Name ConsoleWindow -Namespace VersefilesNative -MemberDefinition @' [DllImport("kernel32.dll")] public static extern IntPtr GetConsoleWindow(); [DllImport("user32.dll")] public static extern bool ShowWindow(IntPtr hWnd, int nCmdShow); [DllImport("user32.dll")] public static extern bool IsIconic(IntPtr hWnd); [DllImport("user32.dll")] public static extern bool SetForegroundWindow(IntPtr hWnd); [DllImport("user32.dll")] public static extern bool AllowSetForegroundWindow(int processId); [DllImport("shell32.dll", CharSet = CharSet.Unicode)] public static extern int SetCurrentProcessExplicitAppUserModelID(string appId); '@ $FROM_CMD = -not $PSCommandPath function Set-ConsoleVisible([bool]$visible) { if (-not $FROM_CMD -or $HIDDEN) { return } $h = [VersefilesNative.ConsoleWindow]::GetConsoleWindow() if ($h -ne [IntPtr]::Zero) { [void][VersefilesNative.ConsoleWindow]::ShowWindow($h, $(if ($visible) { 5 } else { 0 })) } } # One companion at a time - two would write the same journal. The self-test # writes its own scratch journal and may run next to a real companion. # A second start brings the running one's window instead (0.14.0, Alex: the # window opens with every start); an older companion does not listen, then # the old message. $mutexCreated = $false $mutexName = if ($SelfTest) { 'Local\VersefilesCompanionSelfTest' } else { 'Local\VersefilesCompanion' } $showName = $mutexName + '.Show' function Send-ShowRequest([string]$name) { $handle = $null if (-not [System.Threading.EventWaitHandle]::TryOpenExisting($name, [ref]$handle)) { return $false } try { # The running companion is not in front; this start may hand it the # right to come there (ASFW_ANY). try { [void][VersefilesNative.ConsoleWindow]::AllowSetForegroundWindow(-1) } catch { } return $handle.Set() } finally { $handle.Dispose() } } $script:Mutex = New-Object System.Threading.Mutex($true, $mutexName, [ref]$mutexCreated) if (-not $mutexCreated) { if (-not (Send-ShowRequest $showName)) { [void][System.Windows.Forms.MessageBox]::Show('The Versefiles Companion is already running - its icon sits in the taskbar tray.', 'Versefiles Companion') } return } $script:ShowEvent = New-Object System.Threading.EventWaitHandle($false, [System.Threading.EventResetMode]::AutoReset, $showName) $dark = [System.Drawing.Color]::FromArgb(12, 19, 29) $light = [System.Drawing.Color]::FromArgb(219, 230, 242) $accent = [System.Drawing.Color]::FromArgb(57, 194, 255) $muted = [System.Drawing.Color]::FromArgb(123, 142, 166) $field = [System.Drawing.Color]::FromArgb(5, 8, 13) $warn = [System.Drawing.Color]::FromArgb(255, 170, 90) # The launcher's look (0.15.0): Bahnschrift where Windows has it (10, 1709 # on), Segoe UI otherwise; the bars a shade darker than the window. $installedFonts = @((New-Object System.Drawing.Text.InstalledFontCollection).Families | ForEach-Object { $_.Name }) $HOME_FONT = Get-HomeFontName $installedFonts 'Bahnschrift' 'Segoe UI' $HOME_FONT_HEAD = Get-HomeFontName $installedFonts 'Bahnschrift SemiBold Condensed' 'Segoe UI Semibold' $BAR_COLOR = [System.Drawing.Color]::FromArgb(8, 17, 27) # The companion's icon (0.13.1, Alex 05.10.2026: design B) - the site's mark, # three cards with the front one in cyan (laravel/public/favicon.svg), on a # dark tile with round corners and a fine cyan rim. Drawn here, so that the # .cmd stays text. From 32 px the mark as on the site, in a box of 32 units; # below that three cards run into each other, so only the front card with # its two lines, laid on the pixel grid to stay sharp. $ICON_SIZES = @(16, 20, 24, 32, 40, 48, 64, 256) # Per small size: stroke, the card's edges left/top/right/bottom (stroke # centres), the cut of two of its corners, and its two lines as top row, # first column and the column after the last. $ICON_SMALL = @{ 16 = @{ Stroke = 1; Card = @(3.5, 3.5, 12.5, 12.5); Cut = 3; Lines = @(@(7, 5, 10), @(9, 5, 8)) } 20 = @{ Stroke = 1; Card = @(4.5, 4.5, 15.5, 15.5); Cut = 4; Lines = @(@(9, 6, 13), @(12, 6, 11)) } 24 = @{ Stroke = 2; Card = @(5, 5, 19, 19); Cut = 5; Lines = @(@(10, 8, 15), @(14, 8, 12)) } } # A closed polygon from x,y pairs, scaled and moved. function New-IconPolygon([float[]]$xy, [float]$scale = 1, [float]$offset = 0) { $points = for ($i = 0; $i -lt $xy.Length; $i += 2) { New-Object System.Drawing.PointF(($offset + $xy[$i] * $scale), ($offset + $xy[$i + 1] * $scale)) } $path = New-Object System.Drawing.Drawing2D.GraphicsPath $path.AddPolygon([System.Drawing.PointF[]]$points) return $path } # The tile: corners rounded by 22 % of the size, inside a rim of $rim px. function New-IconTile([int]$size, [float]$rim) { $x = $rim / 2; $w = $size - $rim; $d = 2 * $size * 0.22 $path = New-Object System.Drawing.Drawing2D.GraphicsPath $path.AddArc($x, $x, $d, $d, 180, 90) $path.AddArc(($x + $w - $d), $x, $d, $d, 270, 90) $path.AddArc(($x + $w - $d), ($x + $w - $d), $d, $d, 0, 90) $path.AddArc($x, ($x + $w - $d), $d, $d, 90, 90) $path.CloseFigure() return $path } function New-CompanionIconBitmap([int]$size) { $tile = [System.Drawing.Color]::FromArgb(8, 12, 20) $back = [System.Drawing.Color]::FromArgb(29, 111, 150) $front = [System.Drawing.Color]::FromArgb(57, 194, 255) $bmp = New-Object System.Drawing.Bitmap($size, $size, [System.Drawing.Imaging.PixelFormat]::Format32bppArgb) $g = [System.Drawing.Graphics]::FromImage($bmp) $tileBrush = New-Object System.Drawing.SolidBrush $tile $frontBrush = New-Object System.Drawing.SolidBrush $front $pens = New-Object System.Collections.ArrayList $pen = { param($color, [float]$width) $p = New-Object System.Drawing.Pen($color, $width); $p.LineJoin = 'Miter'; [void]$pens.Add($p); $p } try { $g.SmoothingMode = [System.Drawing.Drawing2D.SmoothingMode]::AntiAlias $small = $ICON_SMALL[$size] # Pixel centres on the half pixel: a line of 1 px at x.5 fills one column. if ($small) { $g.PixelOffsetMode = [System.Drawing.Drawing2D.PixelOffsetMode]::Half } $g.Clear([System.Drawing.Color]::Transparent) $rim = [Math]::Max(1.0, $size / 64.0) $tilePath = New-IconTile $size $rim $g.FillPath($tileBrush, $tilePath) $g.DrawPath((& $pen ([System.Drawing.Color]::FromArgb(70, 57, 194, 255)) $rim), $tilePath) if ($small) { $l, $t, $r, $b = $small.Card $c = $small.Cut $g.DrawPath((& $pen $front $small.Stroke), (New-IconPolygon @($l, $t, ($r - $c), $t, $r, ($t + $c), $r, $b, ($l + $c), $b, $l, ($b - $c)))) $g.SmoothingMode = [System.Drawing.Drawing2D.SmoothingMode]::None foreach ($line in $small.Lines) { $g.FillRectangle($frontBrush, $line[1], $line[0], ($line[2] - $line[1]), $small.Stroke) } } else { $inset = $size * 0.12 $s = ($size - 2 * $inset) / 32.0 # At 32 px a little bolder, or the strokes fade. $bold = if ($size -le 32) { 1.35 } else { 1.0 } $width = { param($units) [float][Math]::Max(1.0, $units * $s * $bold) } $backPen = & $pen $back (& $width 1.2) $g.DrawPath($backPen, (New-IconPolygon @(9,3, 23,3, 29,9, 29,21, 15,21, 9,15) $s $inset)) $g.DrawPath($backPen, (New-IconPolygon @(6,6, 20,6, 26,12, 26,24, 12,24, 6,18) $s $inset)) $card = New-IconPolygon @(3,9, 17,9, 23,15, 23,27, 9,27, 3,21) $s $inset $g.FillPath($tileBrush, $card) $g.DrawPath((& $pen $front (& $width 1.6)), $card) $linePen = & $pen $front (& $width 1.4) $g.DrawLine($linePen, ($inset + 7 * $s), ($inset + 17 * $s), ($inset + 16 * $s), ($inset + 17 * $s)) $g.DrawLine($linePen, ($inset + 7 * $s), ($inset + 21 * $s), ($inset + 13 * $s), ($inset + 21 * $s)) } } finally { foreach ($p in $pens) { $p.Dispose() } $tileBrush.Dispose(); $frontBrush.Dispose(); $g.Dispose() } return $bmp } # One picture as an .ico entry: 256 px as PNG; smaller as a 32-bit bitmap # with its mask, the way System.Drawing in Windows PowerShell 5.1 reads them # (it cannot turn PNG entries into pictures). function ConvertTo-IconEntry([System.Drawing.Bitmap]$bmp) { $size = $bmp.Width $out = New-Object System.IO.MemoryStream try { if ($size -ge 256) { $bmp.Save($out, [System.Drawing.Imaging.ImageFormat]::Png) return ,$out.ToArray() } $data = $bmp.LockBits((New-Object System.Drawing.Rectangle(0, 0, $size, $size)), [System.Drawing.Imaging.ImageLockMode]::ReadOnly, [System.Drawing.Imaging.PixelFormat]::Format32bppArgb) $pixels = New-Object byte[] ($size * $size * 4) try { # An icon stores its rows from the bottom up. for ($y = 0; $y -lt $size; $y++) { [System.Runtime.InteropServices.Marshal]::Copy([IntPtr]($data.Scan0.ToInt64() + $y * $data.Stride), $pixels, ($size - 1 - $y) * $size * 4, $size * 4) } } finally { $bmp.UnlockBits($data) } # The mask: a bit for every fully clear pixel, rows padded to 4 bytes. $row = [int][Math]::Ceiling($size / 32.0) * 4 $mask = New-Object byte[] ($row * $size) for ($i = 0; $i -lt $size * $size; $i++) { if ($pixels[$i * 4 + 3] -eq 0) { $at = [int][Math]::Floor($i / $size) * $row + [int][Math]::Floor(($i % $size) / 8) $mask[$at] = $mask[$at] -bor (0x80 -shr ($i % 8)) } } $w = New-Object System.IO.BinaryWriter($out) $w.Write([int32]40); $w.Write([int32]$size); $w.Write([int32]($size * 2)); $w.Write([uint16]1); $w.Write([uint16]32) $w.Write([int32]0); $w.Write([int32]($pixels.Length + $mask.Length)); $w.Write([int32]0); $w.Write([int32]0); $w.Write([int32]0); $w.Write([int32]0) $w.Write($pixels); $w.Write($mask); $w.Flush() return ,$out.ToArray() } finally { $out.Dispose() } } # The icon as the bytes of an .ico file, every size of $ICON_SIZES. function New-CompanionIconData { $entries = foreach ($size in $ICON_SIZES) { $bmp = New-CompanionIconBitmap $size try { ,(ConvertTo-IconEntry $bmp) } finally { $bmp.Dispose() } } $out = New-Object System.IO.MemoryStream $w = New-Object System.IO.BinaryWriter($out) $w.Write([uint16]0); $w.Write([uint16]1); $w.Write([uint16]$ICON_SIZES.Count) $offset = 6 + 16 * $ICON_SIZES.Count for ($i = 0; $i -lt $ICON_SIZES.Count; $i++) { $side = [byte]($ICON_SIZES[$i] % 256) # 256 is written as 0 $w.Write($side); $w.Write($side); $w.Write([byte]0); $w.Write([byte]0) $w.Write([uint16]1); $w.Write([uint16]32); $w.Write([uint32]$entries[$i].Length); $w.Write([uint32]$offset) $offset += $entries[$i].Length } foreach ($entry in $entries) { $w.Write([byte[]]$entry) } $w.Flush() return ,$out.ToArray() } $ICON_DATA = New-CompanionIconData $icon = New-Object System.Drawing.Icon((New-Object System.IO.MemoryStream(, $ICON_DATA))) # The installed copy (0.13.1, Alex 05.10.2026; the rules are in the # self-update block): started without arguments from anywhere else - the # downloads folder, the desktop - the companion copies itself to # %LOCALAPPDATA%\Versefiles\Companion, puts shortcuts on the desktop and into # the Start menu, starts the installed copy without a window and ends; a # helper removes the started file once its cmd.exe has ended. The copy # started here is told so (QG_INSTALLED) and never installs again, and if # anything fails the companion simply runs on where it is. if (-not $SelfTest -and -not $env:QG_ARGS -and -not $env:QG_INSTALLED) { $startFile = Get-UpdateSelf $installed = $null try { $installStep = Get-InstallStep $startFile if ($installStep -ne 'run') { $installed = Install-ToolCopy $startFile $installStep $ICON_DATA } } catch { Write-CompanionError $_ 'install' } if ($installed) { $removable = @(Get-RemovableStartFiles $startFile $installed) # Closed, not only released (0.14.1): while this process still held # the handles, the installed copy could find the "show" signal of # this one - about to end - and quietly end itself. $script:Mutex.ReleaseMutex() $script:ShowEvent.Dispose() $script:Mutex.Dispose() $env:QG_INSTALLED = '1' Start-ToolHidden $installed if ($removable.Count) { Start-StartFileRemoval $removable } return } } # The companion's own name for Windows (0.13.2): before the first window, so # that the taskbar shows "Versefiles Companion" with its icon - taken from the # Start menu shortcut with the same id - instead of Windows PowerShell. The # shortcuts of 0.13.1 get the id once. Never for the self-test, which runs # beside a real companion. if (-not $SelfTest) { [void][VersefilesNative.ConsoleWindow]::SetCurrentProcessExplicitAppUserModelID($APP_ID) try { Update-ToolShortcuts } catch { Write-CompanionError $_ 'shortcuts' } } $script:Exiting = $false $script:HintShown = $false $script:Journal = New-Object System.Collections.ArrayList $form = New-Object System.Windows.Forms.Form $form.Text = Get-CompanionTitle $form.Size = New-Object System.Drawing.Size(760, 560) # 0.15.0: the two columns of Home need it. $form.MinimumSize = New-Object System.Drawing.Size(720, 540) $form.StartPosition = 'CenterScreen' $form.BackColor = $dark $form.ForeColor = $light $form.Font = New-Object System.Drawing.Font('Segoe UI', 9) $form.Icon = $icon $list = New-Object System.Windows.Forms.ListView $list.View = 'Details' $list.FullRowSelect = $true # No header row: WinForms draws it light whatever the theme, and the two # columns explain themselves. $list.HeaderStyle = 'None' $list.BackColor = $field $list.ForeColor = $light $list.BorderStyle = 'None' $list.Dock = 'Fill' [void]$list.Columns.Add('Time', 110) [void]$list.Columns.Add('What happened', 600) # The middle of the window, shared by the views: Home, the journal and the # game data (Show-View). $content = New-Object System.Windows.Forms.Panel $content.Dock = 'Fill' $content.Controls.Add($list) $form.Controls.Add($content) # The bar on top (0.15.0, the launcher's look): VERSEFILES, the views # (Game data always, since 0.15.1), and the status on the right. $topBar = New-Object System.Windows.Forms.Panel $topBar.Dock = 'Top' $topBar.Height = 38 $topBar.BackColor = $BAR_COLOR $lblBrand = New-Object System.Windows.Forms.Label $lblBrand.AutoSize = $true $lblBrand.ForeColor = [System.Drawing.Color]::White $lblBrand.Font = New-Object System.Drawing.Font($HOME_FONT, 11, [System.Drawing.FontStyle]::Bold) $lblBrand.Text = Format-Spaced 'Versefiles' $lblBrand.Location = New-Object System.Drawing.Point(14, 10) $nav = New-Object System.Windows.Forms.FlowLayoutPanel $nav.AutoSize = $true $nav.WrapContents = $false $nav.BackColor = $BAR_COLOR $nav.Location = New-Object System.Drawing.Point(140, 8) function New-NavTab([string]$text) { $t = New-Object Versefiles.Companion.UnderlineTab $t.Font = New-Object System.Drawing.Font($HOME_FONT, 9.5) $t.Text = Format-Spaced $text # Screen readers read the plain word, not the spaced capitals. $t.AccessibleName = $text [void]$nav.Controls.Add($t) return $t } $btnViewHome = New-NavTab 'Home' $btnViewJournal = New-NavTab 'Journal' $btnViewGameData = New-NavTab 'Game data' $lblStatus = New-Object System.Windows.Forms.Label $lblStatus.AutoSize = $false $lblStatus.AutoEllipsis = $true $lblStatus.TextAlign = 'MiddleRight' $lblStatus.ForeColor = $muted $lblStatus.Font = New-Object System.Drawing.Font($HOME_FONT, 8.5) $lblStatus.Text = 'Starting...' # Cut short with "..." where the bar ends; the whole line as a tooltip. $statusTip = New-Object System.Windows.Forms.ToolTip $lblStatus.Add_TextChanged({ $statusTip.SetToolTip($lblStatus, $lblStatus.Text) }) # The Versefiles part on its own at the right, never cut (Format-SyncStatus). $lblSync = New-Object System.Windows.Forms.Label $lblSync.AutoSize = $true $lblSync.ForeColor = $light $lblSync.Font = $lblStatus.Font $topBar.Controls.AddRange(@($lblBrand, $nav, $lblStatus, $lblSync)) # One line, vertically centred: a label as high as the bar wraps its text # instead of cutting it short. The game part takes what the Versefiles part # leaves. function Set-TopBarLayout { $lineHeight = $lblStatus.Font.Height + 2 $top = [int](($topBar.Height - $lineHeight) / 2) $lblSync.Location = New-Object System.Drawing.Point(($topBar.ClientSize.Width - $lblSync.PreferredWidth - 14), $top) $left = $nav.Right + 16 $lblStatus.SetBounds($left, $top, [Math]::Max(40, $lblSync.Left - $left - 18), $lineHeight) } $topBar.Add_Resize({ Set-TopBarLayout }) $form.Controls.Add($topBar) $bottom = New-Object System.Windows.Forms.Panel $bottom.Dock = 'Bottom' $bottom.Height = 40 $bottom.BackColor = $BAR_COLOR $chkAll = New-Object System.Windows.Forms.CheckBox $chkAll.Text = 'Show everything' $chkAll.AutoSize = $true $chkAll.Location = New-Object System.Drawing.Point(150, 11) $chkNotify = New-Object System.Windows.Forms.CheckBox $chkNotify.Text = 'Notifications' $chkNotify.AutoSize = $true $chkNotify.Checked = $true $chkNotify.Location = New-Object System.Drawing.Point(12, 11) $btnSettings = New-Object System.Windows.Forms.Button # "Settings" since 0.13.2 (Alex, 07.10.2026) - before "Versefiles...", from # when the dialog held only the key for the upload. $btnSettings.Text = 'Settings' $btnSettings.AutoSize = $true $btnSettings.FlatStyle = 'Flat' $btnSettings.ForeColor = $accent # The buttons line up from the right edge by themselves. Positions worked # out by hand went wrong twice: first against the panel's 200 px default # width, then with sizes measured before the window's font applied. $buttons = New-Object System.Windows.Forms.FlowLayoutPanel $buttons.Dock = 'Right' $buttons.FlowDirection = 'RightToLeft' $buttons.WrapContents = $false $buttons.AutoSize = $true $buttons.Padding = New-Object System.Windows.Forms.Padding(0, 4, 8, 0) $buttons.Controls.Add($btnSettings) # Shown while a verified newer version waits (Update-Status). $btnUpdate = New-Object System.Windows.Forms.Button $btnUpdate.AutoSize = $true $btnUpdate.FlatStyle = 'Flat' $btnUpdate.ForeColor = $warn $btnUpdate.Visible = $false $btnUpdate.Add_Click({ Install-PendingUpdate }) $buttons.Controls.Add($btnUpdate) $bottom.Controls.Add($chkAll) $bottom.Controls.Add($chkNotify) $bottom.Controls.Add($buttons) $form.Controls.Add($bottom) # The key and whether to send - a small dialog of its own, because it is set # once and then forgotten. function Show-SettingsDialog { $d = New-Object System.Windows.Forms.Form $d.Text = 'Settings' $d.Size = New-Object System.Drawing.Size(520, 456) $d.FormBorderStyle = 'FixedDialog' $d.MaximizeBox = $false; $d.MinimizeBox = $false $d.StartPosition = 'CenterParent' $d.BackColor = $dark; $d.ForeColor = $light $d.Font = $form.Font $y = 16 $l1 = New-Object System.Windows.Forms.Label $l1.Text = 'Companion key from your profile page on versefiles.com' $l1.AutoSize = $true; $l1.Location = New-Object System.Drawing.Point(16, $y); $d.Controls.Add($l1) $txtKey = New-Object System.Windows.Forms.TextBox $txtKey.Location = New-Object System.Drawing.Point(16, ($y + 22)); $txtKey.Width = 470 $txtKey.UseSystemPasswordChar = $true $txtKey.BackColor = $field; $txtKey.ForeColor = $light; $txtKey.BorderStyle = 'FixedSingle' $txtKey.Text = [string]$script:Settings.key $d.Controls.Add($txtKey) $link = New-Object System.Windows.Forms.LinkLabel $link.Text = 'Create a key on versefiles.com' $link.AutoSize = $true; $link.LinkColor = $accent $link.Location = New-Object System.Drawing.Point(16, ($y + 52)) $link.Add_LinkClicked({ Start-Process (([string]$script:Settings.apiBase).TrimEnd('/') + '/profile#companion') }) $d.Controls.Add($link) # Whose key the saved one is, as the server said at the last check. $who = New-Object System.Windows.Forms.Label $who.AutoSize = $false; $who.AutoEllipsis = $true; $who.ForeColor = $muted; $who.TextAlign = 'TopRight' $who.Location = New-Object System.Drawing.Point(226, ($y + 52)); $who.Size = New-Object System.Drawing.Size(260, 18) $who.Text = switch ($script:Account.Status) { 'ok' { 'This key belongs to ' + $script:Account.Name } 'rejected' { 'Versefiles does not accept this key' } default { '' } } $d.Controls.Add($who) $chk = New-Object System.Windows.Forms.CheckBox $chk.Text = 'Send my journal to Versefiles' $chk.AutoSize = $true; $chk.Checked = [bool]$script:Settings.upload $chk.Location = New-Object System.Drawing.Point(16, ($y + 86)) $d.Controls.Add($chk) $note = New-Object System.Windows.Forms.Label $note.Text = "Sent are the events of your LIVE and HOTFIX sessions: contracts, blueprints, rewards, trades, travel. " + "PTU and Tech Preview stay on this PC. Never your handle, account or character ids, IP address or other players' names. " + "You can delete everything on versefiles.com under My log." $note.ForeColor = $muted $note.Location = New-Object System.Drawing.Point(16, ($y + 114)); $note.Size = New-Object System.Drawing.Size(470, 70) $d.Controls.Add($note) $l2 = New-Object System.Windows.Forms.Label $l2.Text = 'Server' $l2.AutoSize = $true; $l2.ForeColor = $muted; $l2.Location = New-Object System.Drawing.Point(16, ($y + 196)); $d.Controls.Add($l2) $txtApi = New-Object System.Windows.Forms.TextBox $txtApi.Location = New-Object System.Drawing.Point(70, ($y + 193)); $txtApi.Width = 416 $txtApi.BackColor = $field; $txtApi.ForeColor = $muted; $txtApi.BorderStyle = 'FixedSingle' $txtApi.Text = [string]$script:Settings.apiBase $d.Controls.Add($txtApi) # Only the downloaded .cmd can replace itself (tools/selfupdate.ps1). $chkUpdate = New-Object System.Windows.Forms.CheckBox $chkUpdate.Text = $(if ($UPDATE_SELF) { 'Install updates by itself - signed by Versefiles, never while the game runs' } else { 'Install updates by itself (only for the downloaded .cmd)' }) $chkUpdate.AutoSize = $true; $chkUpdate.Checked = ($script:Settings.autoUpdate -ne $false); $chkUpdate.Enabled = [bool]$UPDATE_SELF $chkUpdate.Location = New-Object System.Drawing.Point(16, ($y + 228)) $d.Controls.Add($chkUpdate) # The beta channel (0.13.0): test versions first. Only for the # downloaded .cmd, like the updates themselves. $chkBeta = New-Object System.Windows.Forms.CheckBox $chkBeta.Text = 'Get test versions first (beta)' $chkBeta.AutoSize = $true; $chkBeta.Checked = ($script:Settings.updateChannel -eq 'beta'); $chkBeta.Enabled = [bool]$UPDATE_SELF $chkBeta.Location = New-Object System.Drawing.Point(16, ($y + 252)) $d.Controls.Add($chkBeta) # The overlay over the game; it asks versefiles.com about the contract. $chkOverlay = New-Object System.Windows.Forms.CheckBox $chkOverlay.Text = 'Show mission info over the game when you accept a contract ({0} brings it back)' -f $script:Settings.overlayHotkey $chkOverlay.AutoSize = $true; $chkOverlay.Checked = ($script:Settings.overlay -ne $false) $chkOverlay.Location = New-Object System.Drawing.Point(16, ($y + 278)) $d.Controls.Add($chkOverlay) # ... and again for each new step (0.9.0), under the overlay itself. $chkSteps = New-Object System.Windows.Forms.CheckBox $chkSteps.Text = 'Show it again for each new step of the contract' $chkSteps.AutoSize = $true; $chkSteps.Checked = ($script:Settings.overlaySteps -ne $false) $chkSteps.Location = New-Object System.Drawing.Point(36, ($y + 302)) $chkSteps.Enabled = $chkOverlay.Checked $chkOverlay.Add_CheckedChanged({ $chkSteps.Enabled = $chkOverlay.Checked }) $d.Controls.Add($chkSteps) $ok = New-Object System.Windows.Forms.Button $ok.Text = 'Save'; $ok.FlatStyle = 'Flat'; $ok.ForeColor = $accent $ok.DialogResult = [System.Windows.Forms.DialogResult]::OK $ok.Location = New-Object System.Drawing.Point(330, ($y + 340)); $d.Controls.Add($ok) $cancel = New-Object System.Windows.Forms.Button $cancel.Text = 'Cancel'; $cancel.FlatStyle = 'Flat' $cancel.DialogResult = [System.Windows.Forms.DialogResult]::Cancel $cancel.Location = New-Object System.Drawing.Point(411, ($y + 340)); $d.Controls.Add($cancel) $d.AcceptButton = $ok; $d.CancelButton = $cancel if ($d.ShowDialog($form) -ne [System.Windows.Forms.DialogResult]::OK) { return $false } # One key since 0.15.1, for the journal and game data alike. if ($txtKey.Text.Trim() -cne [string]$script:Settings.key) { Set-CompanionKey $txtKey.Text.Trim() } $script:Settings.upload = $chk.Checked $script:Settings.autoUpdate = $chkUpdate.Checked $script:Settings.overlay = $chkOverlay.Checked $script:Settings.overlaySteps = $chkSteps.Checked if (Set-UpdateChannel $(if ($chkBeta.Checked) { 'beta' } else { 'stable' })) { $form.Text = Get-CompanionTitle } $script:Settings.apiBase = $(if ($txtApi.Text.Trim()) { $txtApi.Text.Trim() } else { $DEFAULT_API }) Save-Settings $script:Settings Update-Nav return $true } function New-ListItem($e) { $text = Get-EventText $e if (-not $text) { return $null } $item = New-Object System.Windows.Forms.ListViewItem(('{0:dd.MM. HH:mm}' -f (Get-Time $e))) [void]$item.SubItems.Add($text) switch (Get-Kind $e) { 'blueprint_received' { $item.ForeColor = $accent } 'reward_received' { $item.ForeColor = $accent } 'contract_failed' { $item.ForeColor = $warn } 'died' { $item.ForeColor = $warn } default { if ((Get-Kind $e) -in $QUIET_KINDS) { $item.ForeColor = $muted } } } return $item } function Test-Visible($e) { $chkAll.Checked -or -not ((Get-Kind $e) -in $QUIET_KINDS) } # Newest on top, at most this many rows - the journal itself keeps all. $MAX_ROWS = 1500 function Update-View { $list.BeginUpdate() $list.Items.Clear() $rows = New-Object System.Collections.Generic.List[System.Windows.Forms.ListViewItem] for ($i = $script:Journal.Count - 1; $i -ge 0 -and $rows.Count -lt $MAX_ROWS; $i--) { $e = $script:Journal[$i] if (-not (Test-Visible $e)) { continue } $item = New-ListItem $e if ($item) { $rows.Add($item) } } $list.Items.AddRange($rows.ToArray()) $list.EndUpdate() # The vertical scrollbar may have just appeared and taken its width. & $fitColumns } # Since 0.14.0 the Home view carries the numbers; new events mark it stale, # the timer redraws it while it is on screen. function Update-Totals { $script:HomeDirty = $true } # The game session going on right now: a log that has not ended and wrote # something in the last ten minutes. $null between games. function Get-RunningSession { $running = @($script:Followers | Where-Object { $_.Session -and -not $_.Ended -and $null -ne $_.Session.LastTime -and (([datetime]::UtcNow - [datetime]$_.Session.LastTime).TotalMinutes -lt 10) }) if ($running.Count) { return $running[0].Session } return $null } # The Versefiles part of the status: where the journal goes and whether it # arrives, and an update waiting. On its own on the right of the top bar # (0.15.1 review of 0.15.0): behind the game part, the "..." of a narrow bar # always cut exactly this. function Format-SyncStatus($uploadState, [string]$accountName, $pending) { # The account first, so it is clear where the journal goes. $as = if ($accountName) { $accountName + ' - ' } else { '' } $text = switch ($uploadState.Status) { 'ok' { 'Versefiles: {0}{1} sent{2}' -f $as, $uploadState.Sent, $(if ($uploadState.Last) { ', last at {0:HH:mm}' -f $uploadState.Last } else { '' }) } 'rejected' { 'Versefiles: key rejected - check it under "Settings"' } 'error' { 'Versefiles: {0}not reachable, will try again ({1})' -f $as, $uploadState.Message } default { 'Versefiles: off' } } # The button "Install ... now" below says the rest. if ($pending) { $text += ' | Update {0} ready' -f $pending.Version } return $text } function Update-Status { $s = Get-RunningSession $game = if (-not $script:Followers.Count) { "No Star Citizen installation found at $GAME_ROOT" } elseif ($s) { 'Following {0} - session since {1:HH:mm}, build {2}' -f $s.Channel, ([datetime]$s.Started).ToLocalTime(), $s.Build } else { 'Waiting for the game - watching {0}' -f ((@($script:Followers | ForEach-Object { Split-Path (Split-Path $_.Path -Parent) -Leaf })) -join ', ') } # Set only when it changed: each change redraws and renews the tooltip. if ($lblStatus.Text -ne $game) { $lblStatus.Text = $game } $sync = Format-SyncStatus $script:UploadState $script:Account.Name $script:UpdateState.Pending if ($lblSync.Text -ne $sync) { $lblSync.Text = $sync; Set-TopBarLayout } # A tray tip longer than 63 characters throws - a long name must not. $tip = if ($script:Account.Name) { '{0} - {1}' -f (Get-CompanionTitle), $script:Account.Name } else { Get-CompanionTitle } $tray.Text = $(if ($tip.Length -gt 63) { $tip.Substring(0, 62) + [char]0x2026 } else { $tip }) # The button for it: now, whatever the game does. $btnUpdate.Visible = [bool]$script:UpdateState.Pending -and [bool]$UPDATE_SELF if ($btnUpdate.Visible) { $btnUpdate.Text = 'Install {0} now' -f $script:UpdateState.Pending.Version } } # Sends in the window's thread; the status line shows each batch, so the # first upload of a long history does not look like a hang. function Invoke-Upload { if (-not $script:Settings.upload -or -not $script:Settings.key) { $script:UploadState.Status = 'off'; return } $say = { param($m) $lblStatus.Text = $m; [System.Windows.Forms.Application]::DoEvents() } [void](Send-Journal $say) Update-Status } function Add-ToView($events) { $list.BeginUpdate() foreach ($e in $events) { [void]$script:Journal.Add($e) if (-not (Test-Visible $e)) { continue } $item = New-ListItem $e if ($item) { $list.Items.Insert(0, $item) } } while ($list.Items.Count -gt $MAX_ROWS) { $list.Items.RemoveAt($list.Items.Count - 1) } $list.EndUpdate() Update-Totals if ($chkNotify.Checked) { foreach ($e in $events) { if ((Get-Kind $e) -eq 'blueprint_received') { Show-TrayNote 'Blueprint received' (Get-EventText $e) 'Info' 5000 } elseif (Test-Abandon $e) { # A contract given up: where it stopped, and - where the site # can take the answer - a click to say why. The notification # does not take the focus; the game keeps running. $text = Get-AbandonText $e $url = $null if (Test-AnswerOnSite $e) { $url = ([string]$script:Settings.apiBase).TrimEnd('/') + '/my/log#abandoned' $text += '. Click to tell Versefiles why.' } Show-TrayNote 'Contract abandoned' $text 'Info' 8000 $url } } } } function Test-Abandon($e) { (Get-Kind $e) -eq 'mission_ended' -and (Get-Field $e 'completion') -eq 'Abandon' } # The site can take an answer only for what it has: uploads on, with a key, # from a live session. function Test-AnswerOnSite($e) { if (-not $script:Settings.upload -or -not $script:Settings.key) { return $false } $meta = if ($e -is [Versefiles.Companion.LogEvent]) { $script:Index[$e.Session] } else { $null } return $meta -and ($LIVE_CHANNELS -contains [string]$meta.channel) } # "ENTRY LVL. COURIER NEEDED IN NYX - stopped at: Deliver Prescription # Medications To Levski": the contract's title and the first step still open # when the game wrote "Abandon", from the contract's own events. function Get-AbandonText($e) { $mid = Get-Field $e 'mission_id' $title = $null $open = [ordered]@{} if ($mid) { foreach ($x in $script:Journal) { if ((Get-Field $x 'mission_id') -ne $mid) { continue } $id = [string](Get-Field $x 'objective_id') switch (Get-Kind $x) { 'contract_accepted' { $title = Get-Field $x 'title' } 'objective_added' { if ($id) { $open[$id] = Get-Field $x 'objective' } } 'objective_completed' { if ($id) { $open.Remove($id) } } 'objective_withdrawn' { if ($id) { $open.Remove($id) } } } } } $text = if ($title) { [string]$title } else { 'A contract' } $step = @($open.Values | Where-Object { $_ }) | Select-Object -First 1 if ($step) { $text += " - stopped at: $step" } return $text } function Show-Main { $form.Show() $form.WindowState = 'Normal' $form.Activate() } $tray = New-Object System.Windows.Forms.NotifyIcon # The tray wants the small picture; taken from the .ico, not shrunk. $tray.Icon = New-Object System.Drawing.Icon($icon, [System.Windows.Forms.SystemInformation]::SmallIconSize) $tray.Text = Get-CompanionTitle $menu = New-Object System.Windows.Forms.ContextMenuStrip [void]$menu.Items.Add('Start RSI Launcher', $null, { if (-not (Start-RsiLauncher)) { Show-TrayNote 'Versefiles Companion' 'RSI Launcher not found.' 'Warning' 5000 } }) [void]$menu.Items.Add('Show journal', $null, { Show-Main; Show-View 'journal' }) [void]$menu.Items.Add('Check for updates', $null, { if (-not $UPDATE_SELF) { Show-TrayNote 'Versefiles Companion' 'This copy does not update itself - it runs as .ps1 or inside a git working tree.' return } Invoke-UpdateCheck $true }) [void]$menu.Items.Add((New-Object System.Windows.Forms.ToolStripSeparator)) [void]$menu.Items.Add('Exit', $null, { $script:Exiting = $true; $form.Close() }) $tray.ContextMenuStrip = $menu $tray.Add_DoubleClick({ Show-Main }) # Every notification goes through here, with what a click on it does: open a # page (an abandoned contract -> My log), show a view of the window (a new # game build -> Game data) or nothing. Set with each one, a click never does # what an earlier notification wanted. $script:BalloonUrl = $null $script:BalloonView = $null function Show-TrayNote([string]$title, [string]$text, [string]$kind = 'Info', [int]$ms = 6000, [string]$url = $null, [string]$view = $null) { $script:BalloonUrl = $url $script:BalloonView = $view $tray.ShowBalloonTip($ms, $title, $text, $kind) } $tray.Add_BalloonTipClicked({ if ($script:BalloonView) { Show-Main; Show-View $script:BalloonView } elseif ($script:BalloonUrl) { Start-Process $script:BalloonUrl } }) $chkAll.Add_CheckedChanged({ Update-View }) # The text column takes whatever width the time column leaves. -2 is the # list view's own "fill the rest", which also knows about the scrollbar. $fitColumns = { if ($list.Columns.Count -eq 2) { $list.Columns[1].Width = -2 } } $list.Add_Resize($fitColumns) $form.Add_Load($fitColumns) # Closing the window keeps the companion running in the tray; Exit is in the # icon's menu. $form.Add_FormClosing({ param($sender, $ev) if (-not $script:Exiting -and $ev.CloseReason -eq [System.Windows.Forms.CloseReason]::UserClosing) { $ev.Cancel = $true $form.Hide() if (-not $script:HintShown) { Show-TrayNote 'Versefiles Companion' 'Still following the game. Right-click the icon to exit.' 'Info' 4000 $script:HintShown = $true } } }) $btnSettings.Add_Click({ if (Show-SettingsDialog) { Update-Account; Invoke-Upload; Update-Nav } }) # Updates (the work is in the self-update block above). The first check comes # half a minute after the start, then every $UPDATE_CHECK_HOURS hours; a # verified update waits until the game is closed (Alex, 27.09.2026: never in # the middle of a game). Then the companion ends, and the helper puts the # new version in place and starts it. $UPDATE_SELF = Get-UpdateSelf $script:UpdateState = @{ NextCheck = (Get-Date).AddSeconds(30); NextTry = [datetime]::MinValue; Pending = $null; Message = '' } # The update channel (Alex, 03.10.2026): 'beta' gets test versions first, # everybody else only what Alex released to all. Returns $true if it # changed: then the next check is now, and a beta waiting to be installed # goes when the player leaves the beta. Anything but 'beta' is 'stable'. function Set-UpdateChannel([string]$channel) { $channel = if ($channel -eq 'beta') { 'beta' } else { 'stable' } if ($script:Settings.updateChannel -eq $channel) { return $false } $script:Settings.updateChannel = $channel $u = $script:UpdateState $u.NextCheck = Get-Date if ($channel -eq 'stable' -and $u.Pending -and $u.Pending.Channel -eq 'beta') { $u.Pending = $null } return $true } function Test-GameRunning { [bool](Get-Process -Name 'StarCitizen' -ErrorAction SilentlyContinue) } # Asks the server now and keeps a verified newer version ready. $manual: the # tray menu asked, so the answer is said either way. function Invoke-UpdateCheck([bool]$manual = $false) { $u = $script:UpdateState $u.NextCheck = (Get-Date).AddHours($UPDATE_CHECK_HOURS) try { $offer = Get-UpdateOffer $script:Settings.apiBase 'companion' $COMPANION_VERSION $UPDATE_AGENT ($script:Settings.updateChannel -eq 'beta') if ($offer -and -not ($u.Pending -and $u.Pending.Version -eq $offer.Version)) { $u.Pending = @{ File = (Save-UpdateFile $offer 'companion' $COMPANION_VERSION $UPDATE_AGENT); Version = $offer.Version; Channel = $offer.Channel } } $u.Message = '' if ($manual) { $text = if ($u.Pending) { 'Version {0}{1} is ready - "Install {0} now" in the window, or it installs by itself once the game is closed.' -f $u.Pending.Version, $(if ($u.Pending.Channel -eq 'beta') { ' (beta)' } else { '' }) } else { 'Versefiles Companion {0} is up to date.' -f $COMPANION_VERSION } Show-TrayNote 'Versefiles Companion' $text } } catch { # Offline or a refused file: the next check tries again. $u.Message = $_.Exception.Message if ($manual) { Show-TrayNote 'Versefiles Companion' ('The update check failed: ' + $u.Message) 'Warning' } } Update-Status } # Hands the verified file to the helper and ends; the helper puts it in # place and starts it. From the button also while the game runs - that is # the player's decision; the restart takes a few seconds. function Install-PendingUpdate { $u = $script:UpdateState if (-not $u.Pending -or -not $UPDATE_SELF) { return } Start-UpdateInstall $u.Pending.File $UPDATE_SELF 'companion' $COMPANION_VERSION $u.Pending.Version $true ([string]$env:QG_ARGS) $script:Exiting = $true $form.Close() } # By itself: checks on schedule (and right after the game closes, see the # timer), installs only while the game is not running - and no game data # run is going, which the restart would cut off. function Invoke-UpdateStep { $u = $script:UpdateState if (-not $UPDATE_SELF -or $script:Settings.autoUpdate -eq $false) { return } $now = Get-Date if (-not $u.Pending -and $now -ge $u.NextCheck) { Invoke-UpdateCheck } if ($u.Pending -and $now -ge $u.NextTry) { $u.NextTry = $now.AddSeconds(30) if ((Test-GameRunning) -or $script:GameDataState.Run) { return } Install-PendingUpdate } } # ---- Overlay over the game (companion 0.7.0) ---------------------------------- # # Alex, 28.09.2026: when a contract is accepted, show its steps, known # problems, possible blueprints and the player's own record for twenty # seconds; a hotkey brings it back. The companion asks versefiles.com # (/api/v1/companion/mission) with the names the log gives and the session's # channel, only for a contract accepted just now - the start reads earlier # sessions, and those must not pop up. In EVERY channel (Alex, 28.09.2026: # "Overlays sind aber in allen Versionen anzuzeigen"): showing is not # storing - the journal still sends LIVE and HOTFIX only. $OVERLAY_SECONDS = 20 # How long to wait for the contract's name after "Contract Accepted" - the log # names it a line later, with the first objective marker; the title alone is # ambiguous for many contracts. $OVERLAY_NAME_WAIT_SECONDS = 4 # How long a question that failed waits before it is asked again (0.13.0) - # at once when the contract moves on. $OVERLAY_RETRY_SECONDS = 60 # The contracts open right now (companion 0.12.0; Alex, 29.09.2026: "man kann # mehrere Quests gleichzeitig haben"): mission id -> a contract as # New-OverlayContract makes it. Shown: the one the panel has in full, the # others one line each below it; Order: the order the hotkey pages through # while the panel is up; Now: the steps the panel shows of the one in full - # its open steps, except for the second of a change (Show-StepChange). # Names: a contract's name the log gave before its "Contract Accepted" # (mission id -> @{ Generator; Contract }) - the marker line often comes first. $script:OverlayState = @{ Contracts = [ordered]@{}; Names = @{}; Shown = $null; Order = @(); Now = @(); Ready = $null Hotkey = $null; HotkeyText = '' } $overlay = New-Object Versefiles.Companion.OverlayForm $overlay.BackColor = [System.Drawing.Color]::FromArgb(8, 13, 20) $overlay.AutoSize = $true $overlay.AutoSizeMode = 'GrowAndShrink' $overlay.Opacity = 0.93 $overlayPanel = New-Object System.Windows.Forms.FlowLayoutPanel $overlayPanel.FlowDirection = 'TopDown' $overlayPanel.WrapContents = $false $overlayPanel.AutoSize = $true $overlayPanel.AutoSizeMode = 'GrowAndShrink' $overlayPanel.Padding = New-Object System.Windows.Forms.Padding(14, 10, 16, 12) $overlay.Controls.Add($overlayPanel) $overlayBorder = New-Object System.Drawing.Pen([System.Drawing.Color]::FromArgb(29, 107, 138), 1) $overlay.Add_Paint({ param($s, $ev) $ev.Graphics.DrawRectangle($overlayBorder, 0, 0, $s.Width - 1, $s.Height - 1) }) # The handle now, without showing: the size is known before the first Show. $null = $overlay.Handle $overlayTimer = New-Object System.Windows.Forms.Timer # Hides the panel, whatever it showed. function Hide-Overlay { $overlayTimer.Stop() $overlay.Hide() $script:OverlayState.Ready = $null } $overlayTimer.Add_Tick({ Hide-Overlay }) # The second between the old step and the new one (Show-StepChange). $overlayStepTimer = New-Object System.Windows.Forms.Timer $overlayStepTimer.Interval = 1000 $overlayStepTimer.Add_Tick({ $overlayStepTimer.Stop() $script:OverlayState.Now = @(Get-OpenSteps $script:OverlayState.Shown) if ($overlay.Visible) { Show-OverlayPanel } }) # "Alt+Shift+P" -> modifiers and virtual key for Versefiles.Companion.Hotkey; # $null if it cannot be read. function ConvertTo-Hotkey([string]$text) { $modifiers = 0; $key = 0 foreach ($part in @($text -split '\+' | ForEach-Object { $_.Trim() } | Where-Object { $_ })) { if ($part -match '^(Ctrl|Control)$') { $modifiers = $modifiers -bor 2 } elseif ($part -eq 'Alt') { $modifiers = $modifiers -bor 1 } elseif ($part -eq 'Shift') { $modifiers = $modifiers -bor 4 } elseif ($part -eq 'Win') { $modifiers = $modifiers -bor 8 } else { try { $key = [int][System.Windows.Forms.Keys]$part } catch { return $null } } } if (-not $key -or -not $modifiers) { return $null } return @{ Modifiers = [uint32]$modifiers; Key = [uint32]$key; Text = $text } } function Add-OverlayLine([string]$text, $color, [float]$size = 9.5, [bool]$bold = $false, [int]$gap = 0) { $l = New-Object System.Windows.Forms.Label $l.AutoSize = $true $l.UseMnemonic = $false $l.MaximumSize = New-Object System.Drawing.Size(380, 0) $l.Text = $text $l.ForeColor = $color $l.Font = New-Object System.Drawing.Font('Segoe UI', $size, $(if ($bold) { [System.Drawing.FontStyle]::Bold } else { [System.Drawing.FontStyle]::Regular })) $l.Margin = New-Object System.Windows.Forms.Padding(0, $gap, 0, 0) $overlayPanel.Controls.Add($l) } # A step as the game names it, without its counter: "Deliver 4/12 SCU of X" # and "Deliver 0/12 SCU of X" are one step (the server's # CompanionAbandons::step reads them the same way). function Get-StepKey([string]$text) { if (-not $text) { return '' } return (($text -replace '(\d+)\s*/\s*(\d+)', '$2') -replace '\s+', ' ').Trim().ToLowerInvariant() } # A contract of the panel. Mission: the game's mission id; Title, Channel, # Generator, Contract: what the log names - the server finds the mission by # them. Since: when the wait for its name began; Asked: the server was asked, # Info: its answer when it knows the mission; Unreachable: the question failed # (0.13.0). Announce: accepted just now, so the panel shows it by itself # (0.13.0: known to the site or not). Open: its steps still open, as @{ Key; Text } in # the order the game added them; Done: the keys of those completed. Active: # its latest step or acceptance, UTC - the latest comes first on the panel. function New-OverlayContract([string]$mission, [string]$title, [string]$channel, [bool]$announce) { return @{ Mission = $mission; Title = $title; Channel = $channel; Generator = ''; Contract = '' Since = [datetime]::UtcNow; Asked = $false; AskedAt = [datetime]::MinValue; Info = $null; Unreachable = $false; Announce = $announce Open = (New-Object System.Collections.ArrayList); Done = @{}; Active = [datetime]::UtcNow } } # One log event about a contract's steps. A step the game adds goes to Open # once: the game repeats "New Objective" for a step it gave already, and a # moving counter (0/12 -> 4/12) only changes its text. A step completed or # withdrawn leaves Open. $true for a step that was not open. function Update-OpenStep($c, $e) { $kind = Get-Kind $e if ($kind -notin 'objective_added', 'objective_completed', 'objective_withdrawn') { return $false } $text = [string](Get-Field $e 'objective') $key = Get-StepKey $text if (-not $key) { return $false } $had = $null foreach ($s in $c.Open) { if ($s.Key -eq $key) { $had = $s; break } } if ($kind -eq 'objective_added') { if ($had) { $had.Text = $text; return $false } [void]$c.Open.Add(@{ Key = $key; Text = $text }) $c.Done.Remove($key) return $true } if ($had) { $c.Open.Remove($had) } if ($kind -eq 'objective_completed') { $c.Done[$key] = $true } return $false } # The open steps of a contract, as the game names them; none for one that # is not open (any more). function Get-OpenSteps([string]$mission) { $c = if ($mission) { $script:OverlayState.Contracts[$mission] } else { $null } if (-not $c) { return @() } return @($c.Open | ForEach-Object { $_.Text }) } # The open contracts in the order the panel pages through them: the given # one first, then by their latest activity. function Get-ContractOrder([string]$first) { $all = @($script:OverlayState.Contracts.Values | Sort-Object { $_.Active } -Descending | ForEach-Object { $_.Mission }) if ($first -and $all -contains $first) { return @(@($first) + @($all | Where-Object { $_ -ne $first })) } return $all } # The steps under "Now": the ones still open, as the game names them. function Add-OverlayNow($o) { $now = @($o.Now | Where-Object { $_ }) if (-not $now.Count) { return } Add-OverlayLine 'Now' $muted 8.5 $false 6 foreach ($step in $now) { Add-OverlayLine ([string][char]0x25B8 + ' ' + $step) $accent 9.5 } } # Where the journal of this session goes - for the "ready" panel. function Get-JournalText($session) { if ($LIVE_CHANNELS -notcontains [string]$session.Channel) { return '{0} sessions stay on this PC.' -f $session.Channel } if (-not $script:Settings.upload -or -not $script:Settings.key) { return 'Your journal stays on this PC.' } switch ($script:Account.Status) { 'ok' { return 'Your journal goes to Versefiles as {0}.' -f $script:Account.Name } 'rejected' { return 'Versefiles does not take the key - see "Settings" in the window.' } } return 'Your journal goes to Versefiles.' } # The panel drawn anew from the state. It is laid out again whatever happens # while drawing: a drawing that failed half way must not leave it empty for # the rest of the game. The old lines go in a function of their own - in # 0.12.0/0.12.1 the loop clearing them used $c, the name of the contract # shown, so from the second drawing on that was a disposed label (Alex's # session of 03.10.2026: a second contract left the panel empty, then the # companion was gone). function Update-OverlayContent { $overlayPanel.SuspendLayout() try { foreach ($old in @($overlayPanel.Controls)) { $old.Dispose() } $overlayPanel.Controls.Clear() Add-OverlayContent } finally { $overlayPanel.ResumeLayout() $overlay.PerformLayout() } } # The lines: the contract shown in full with what the server said about it, # the other open contracts one line each below it (or, with none open, a # line saying so) - or, once a game starts, that the companion is ready # (Invoke-ReadyStep). function Add-OverlayContent { $o = $script:OverlayState $c = if ($o.Shown) { $o.Contracts[[string]$o.Shown] } else { $null } $info = if ($c) { $c.Info } else { $null } $order = @($o.Order | Where-Object { $o.Contracts.Contains([string]$_) }) $position = [Array]::IndexOf([object[]]$order, [string]$o.Shown) + 1 $head = if ($order.Count -gt 1 -and $position -gt 0) { 'Versefiles - contract {0} of {1}' -f $position, $order.Count } else { 'Versefiles - contract accepted' } if ($o.Ready) { $s = $o.Ready Add-OverlayLine (Get-CompanionTitle) $accent 8.5 Add-OverlayLine ('Ready - following {0}, build {1}' -f $s.Channel, $s.Build) $light 11.5 $true Add-OverlayLine (Get-JournalText $s) $muted 9 $false 4 if ($o.HotkeyText) { Add-OverlayLine ($o.HotkeyText + ' shows the contract you are on') $muted 8 $false 8 } return } if (-not $c) { Add-OverlayLine 'Versefiles Companion' $accent 8.5 Add-OverlayLine 'No open contract the companion knows of.' $light } elseif (-not $info) { # A contract the site knows nothing about (yet): its steps from the log. Add-OverlayLine $head $accent 8.5 Add-OverlayLine ([string]$c.Title) $light 11.5 $true Add-OverlayNow $o $said = if (-not $c.Asked) { 'Asking Versefiles...' } elseif ($c.Unreachable) { 'Versefiles could not be reached.' } else { 'Versefiles has nothing on this contract yet.' } Add-OverlayLine $said $muted 8.5 $false 6 } else { Add-OverlayLine $head $accent 8.5 Add-OverlayLine ([string]$info.title) $light 11.5 $true # What it pays and how long players take (0.11.0; Alex, 29.09.2026: # also without a walkthrough the overlay has something to say). # The reward is the game files' figure, or - where the game pays by a # formula - what players were paid, as their logs say (0.11.1). $facts = @() if ($info.reward -and [double]$info.reward.min -gt 0) { $facts += $(if ($info.reward.source -eq 'players') { 'players got about {0:N0} aUEC' -f [double]$info.reward.min } elseif ($info.reward.max) { 'Reward {0:N0}-{1:N0} aUEC' -f [double]$info.reward.min, [double]$info.reward.max } else { 'Reward {0:N0} aUEC' -f [double]$info.reward.min }) } # The middle half of the runs and their median (0.11.2): "12-18 min # (median 14)"; a server before that names the median only. if ($info.duration -and $info.duration.span) { $facts += 'players need ' + $info.duration.span } elseif ($info.duration -and $info.duration.text) { $facts += 'players need about ' + $info.duration.text } if ($facts) { Add-OverlayLine ($facts -join ' · ') $light 9 $false 2 } $steps = $info.steps if ($steps -and @($steps.items).Count) { Add-OverlayLine $(if ($steps.source -eq 'walkthrough') { 'Walkthrough' } else { 'Steps in the game' }) $muted 8.5 $false 6 # The steps the game named are marked where they stand: the open # ones lit, the done ones dimmed. A walkthrough names its steps in # its own words, so there nothing matches. $openKeys = @{} foreach ($step in @($o.Now)) { $openKeys[(Get-StepKey $step)] = $true } $n = 0 foreach ($step in @($steps.items)) { $n++ $key = Get-StepKey ([string]$step) if ($openKeys[$key]) { Add-OverlayLine ('{0} {1}. {2}' -f [char]0x25B8, $n, $step) $accent } elseif ($c.Done[$key]) { Add-OverlayLine ('{0}. {1}' -f $n, $step) $muted } else { Add-OverlayLine ('{0}. {1}' -f $n, $step) $light } } if ([int]$steps.more -gt 0) { Add-OverlayLine ('+ {0} more on versefiles.com' -f $steps.more) $muted 8.5 } } else { Add-OverlayLine 'No walkthrough yet - the first one could be yours.' $muted 8.5 $false 6 } Add-OverlayNow $o if ($info.problem) { $p = $info.problem $text = '{0} players gave up on this one ({1} times)' -f $p.players, $p.abandons if ($p.step) { $text += ', most often at: ' + $p.step } Add-OverlayLine $text $warn 9 $false 6 } if ($info.blueprints) { $b = $info.blueprints # With a key the server says which ones are the player's (0.10.0): # ticked, and the ones still to get come first. $owned = @($b.owned | Where-Object { $_ }) $names = @($b.names | ForEach-Object { if ($owned -contains $_) { [string][char]0x2713 + ' ' + $_ } else { $_ } }) -join ' · ' if ([int]$b.count -gt @($b.names).Count) { $names += ' (+{0})' -f ([int]$b.count - @($b.names).Count) } $head = 'Possible blueprints' if ($null -ne $b.owned_count) { $head += ' - {0} of {1} yours' -f $b.owned_count, $b.count } Add-OverlayLine $head $muted 8.5 $false 6 Add-OverlayLine $names $light 9 } if ($info.record) { $r = $info.record $parts = @() if ([int]$r.completed) { $last = try { ', last ' + ([datetime]$r.last_completed).ToString('dd.MM.') } catch { '' } $parts += 'done {0}x{1}' -f $r.completed, $last } # The player's own time and last reward (0.11.1) - theirs, so # without the three players the public figures wait for. if ($r.time) { $parts += $(if ($r.fastest -and $r.fastest -ne $r.time) { '{0}, fastest {1}' -f $r.time, $r.fastest } else { [string]$r.time }) } if ([double]$r.last_reward -gt 0) { $parts += 'last reward {0:N0} aUEC' -f [double]$r.last_reward } if ([int]$r.abandoned) { $parts += 'given up {0}x' -f $r.abandoned } if ($parts) { Add-OverlayLine ('You: ' + ($parts -join ' · ')) $muted 9 $false 6 } } } # The other open contracts, one line each: the name and the step it is at # (Alex, 29.09.2026: one in front, the rest short, paged with the key). $others = @($order | Where-Object { $_ -ne [string]$o.Shown }) if ($c -and $others.Count) { Add-OverlayLine 'Also open' $muted 8.5 $false 8 foreach ($m in $others) { $x = $o.Contracts[[string]$m] $name = if ($x.Info -and $x.Info.title) { [string]$x.Info.title } else { [string]$x.Title } $step = @($x.Open | ForEach-Object { $_.Text }) | Select-Object -First 1 Add-OverlayLine ([string][char]0x25B8 + ' ' + $name + $(if ($step) { ' - ' + $step } else { '' })) $light 9 } } Add-OverlayLine $(if ($info) { [string]$info.url -replace '^https?://', '' } else { 'versefiles.com' }) $muted 8 $false 8 if ($o.HotkeyText) { $keyText = if ($order.Count -le 1) { ' shows this again' } elseif ($position -lt $order.Count) { ': the next contract' } else { ': close' } Add-OverlayLine ($o.HotkeyText + $keyText) $muted 8 } } # The screen the game is on - the overlay goes to its top right. function Get-GameScreen { $p = Get-Process -Name 'StarCitizen' -ErrorAction SilentlyContinue | Where-Object { $_.MainWindowHandle -ne [IntPtr]::Zero } | Select-Object -First 1 if ($p) { return [System.Windows.Forms.Screen]::FromHandle($p.MainWindowHandle) } return [System.Windows.Forms.Screen]::PrimaryScreen } # Top right on the game's screen - again after every change of content, the # panel's width follows it. function Set-OverlayPlace { $b = (Get-GameScreen).Bounds $overlay.Location = New-Object System.Drawing.Point(($b.Right - $overlay.Width - 36), ($b.Top + [int]($b.Height * 0.18))) } # Draws the panel as the state says and keeps it up for $seconds. function Show-OverlayPanel([int]$seconds = $OVERLAY_SECONDS) { Update-OverlayContent Set-OverlayPlace $overlayTimer.Stop() $overlayTimer.Interval = $seconds * 1000 if (-not $overlay.Visible) { $overlay.Show() } $overlay.Invalidate() $overlayTimer.Start() } # The panel with a contract in full - the one given, else the one with the # latest activity - and the other open ones short below it. function Show-MissionOverlay([string]$mission = '') { $o = $script:OverlayState $o.Ready = $null $o.Order = @(Get-ContractOrder $mission) $o.Shown = if ($o.Order.Count) { $o.Order[0] } else { $null } $o.Now = @(Get-OpenSteps $o.Shown) Show-OverlayPanel } # A new step (Alex, 29.09.2026): the panel shows the contract as it was, and # a second later the step changes - so that the change is seen. # $overlayStepTimer does the second part. function Show-StepChange([string]$mission, [string[]]$before) { $o = $script:OverlayState $o.Ready = $null $o.Order = @(Get-ContractOrder $mission) $o.Shown = $mission $o.Now = @($before) Show-OverlayPanel $overlayStepTimer.Stop() $overlayStepTimer.Start() } # The hotkey (Alex, 29.09.2026): the panel with the latest contract; pressed # again while it is up, the next open contract in full - after the last one # the panel closes. function Invoke-OverlayKey { $o = $script:OverlayState if (-not $overlay.Visible -or $o.Ready) { Show-MissionOverlay; return } $order = @($o.Order | Where-Object { $o.Contracts.Contains([string]$_) }) $at = [Array]::IndexOf([object[]]$order, [string]$o.Shown) if ($at -ge 0 -and $at -lt $order.Count - 1) { $overlayStepTimer.Stop() $o.Order = $order $o.Shown = $order[$at + 1] $o.Now = @(Get-OpenSteps $o.Shown) Show-OverlayPanel } else { Hide-Overlay } } function Get-MissionInfo($p) { $query = @() foreach ($name in 'channel', 'generator', 'contract', 'title') { if ($p[$name]) { $query += $name + '=' + [uri]::EscapeDataString([string]$p[$name]) } } $headers = @{} if ($script:Settings.key) { $headers.Authorization = 'Bearer ' + $script:Settings.key } return Invoke-RestMethod -Uri (([string]$script:Settings.apiBase).TrimEnd('/') + '/api/v1/companion/mission?' + ($query -join '&')) ` -Headers $headers -TimeoutSec 8 -UserAgent $UPDATE_AGENT } # One log event about the open contracts: an accepted one joins them, its # name and steps are followed, an ended one leaves. Returns the contract that # got a new step, else $null. Shared by the timer (Invoke-OverlayStep) and # the start during a game (Restore-OpenContracts). function Update-OpenContracts($e, [bool]$announce) { $o = $script:OverlayState $kind = Get-Kind $e # A new game: what was open belongs to the one before. if ($kind -in 'session_started', 'session_ended') { $o.Contracts.Clear(); $o.Names.Clear(); return $null } $mission = [string](Get-Field $e 'mission_id') if (-not $mission) { return $null } if ($kind -eq 'contract_accepted') { $meta = if ($e -is [Versefiles.Companion.LogEvent]) { $script:Index[$e.Session] } elseif (Get-Field $e 'session') { $script:Index[[string](Get-Field $e 'session')] } else { $null } $c = New-OverlayContract $mission ([string](Get-Field $e 'title')) $(if ($meta) { [string]$meta.channel } else { '' }) $announce $c.Generator = [string](Get-Field $e 'generator') $c.Contract = [string](Get-Field $e 'contract') # The name the log gave a line before (0.12.1, Alex's session of 30.09.2026: # "mission_contract" came first) - without it the server has only the title. $early = $o.Names[$mission] if ($early) { if (-not $c.Generator) { $c.Generator = $early.Generator } if (-not $c.Contract) { $c.Contract = $early.Contract } $o.Names.Remove($mission) } $c.Active = (Get-Time $e).ToUniversalTime() # Accepted again under the same id: it starts over. $o.Contracts[$mission] = $c return $null } $c = $o.Contracts[$mission] if (-not $c) { if ($kind -eq 'mission_contract') { # Kept for the "Contract Accepted" that follows; a few dozen at most. if ($o.Names.Count -ge 50) { $o.Names.Clear() } $o.Names[$mission] = @{ Generator = [string](Get-Field $e 'generator'); Contract = [string](Get-Field $e 'contract') } } return $null } if ($kind -eq 'mission_contract') { $c.Generator = [string](Get-Field $e 'generator') $c.Contract = [string](Get-Field $e 'contract') } elseif ($kind -in 'mission_ended', 'contract_completed', 'contract_failed') { $o.Contracts.Remove($mission) } elseif (Update-OpenStep $c $e) { $c.Active = (Get-Time $e).ToUniversalTime() return $c } return $null } # The contracts already open when the companion starts during a game: # accepted in the running session and not ended since, from the journal - # it has read the session so far. They show on the hotkey and when they move # on, not by themselves; the server is asked about them one by one. function Restore-OpenContracts { $s = Get-RunningSession if (-not $s -or -not $s.Id) { return } foreach ($e in $script:Journal) { if ([string](Get-Field $e 'session') -ne [string]$s.Id) { continue } [void](Update-OpenContracts $e $false) } foreach ($c in $script:OverlayState.Contracts.Values) { $c.Since = [datetime]::MinValue } } # Called by the timer with the new events. Follows the open contracts, asks # the server about each once - when the log has named it, or after a short # wait; one question per tick, the latest contract first - and shows a # contract accepted just now. A new step shows its contract again # (Show-StepChange), unless "overlaySteps" is off; otherwise the panel, when # up, just follows what changed. function Invoke-OverlayStep($fresh) { if ($script:Settings.overlay -eq $false) { return } $o = $script:OverlayState $before = @{} foreach ($c in @($o.Contracts.Values)) { $before[$c.Mission] = @($c.Open | ForEach-Object { $_.Text }) } $count = $o.Contracts.Count $changed = $null foreach ($e in $fresh) { # Only what happens now: an old line is followed, not announced. $recent = ((Get-Date) - (Get-Time $e)).TotalMinutes -le 3 $got = Update-OpenContracts $e $recent if ($got -and $recent) { $changed = $got } } # A question that failed is asked again after a while, and at once when # its contract moves on (0.13.0): the site may be back, and the panel # must not keep saying it could not be reached. $ask = @($o.Contracts.Values | Where-Object { (-not $_.Asked -and ($_.Contract -or ([datetime]::UtcNow - $_.Since).TotalSeconds -ge $OVERLAY_NAME_WAIT_SECONDS)) -or ($_.Unreachable -and (($changed -and $changed.Mission -eq $_.Mission) -or ([datetime]::UtcNow - $_.AskedAt).TotalSeconds -ge $OVERLAY_RETRY_SECONDS)) } | Sort-Object { $_.Active } -Descending) | Select-Object -First 1 if ($ask) { $ask.Asked = $true $ask.AskedAt = [datetime]::UtcNow $ask.Unreachable = $false try { $answer = Get-MissionInfo @{ channel = $ask.Channel; generator = $ask.Generator; contract = $ask.Contract; title = $ask.Title } if ($answer -and $answer.found) { $ask.Info = $answer } } catch { $ask.Unreachable = $true } # Shown also when the site does not know it (0.13.0, Alex 03.10.2026): # title and step from the log, and what the site said. if ($ask.Announce) { $ask.Announce = $false Show-MissionOverlay $ask.Mission return } } # A new step of a contract the server was asked about - known or not. if ($changed -and $o.Contracts.Contains($changed.Mission) -and $changed.Asked -and $script:Settings.overlaySteps -ne $false) { Show-StepChange $changed.Mission $before[$changed.Mission] return } if (-not $overlay.Visible -or $o.Ready -or $overlayStepTimer.Enabled) { return } # Quietly, while the panel is up: a counter moved, a step was done, a # contract came or went - or the one shown ended, and the next takes its # place. if ($o.Shown -and -not $o.Contracts.Contains([string]$o.Shown)) { if (-not $o.Contracts.Count) { Hide-Overlay; return } $o.Order = @(Get-ContractOrder '') $o.Shown = $o.Order[0] } $now = @(Get-OpenSteps $o.Shown) if ($ask -or $o.Contracts.Count -ne $count -or ($now -join "`n") -ne (@($o.Now) -join "`n")) { $o.Now = $now $o.Order = @(Get-ContractOrder $o.Shown) Update-OverlayContent Set-OverlayPlace } } # ---- "Ready" when a game starts (companion 0.10.0) --------------------------- # # Alex, 29.09.2026: "Wenn das Spiel gestartet ist, soll sich der Companion # einmal melden und zeigen, dass er bereit ist." Once per game session, as # soon as the game's window is up - also when the companion starts during a # game: the panel says which session it follows, where the journal goes and # which keys bring the contract back. Part of the overlay: off with it. $READY_SECONDS = 8 $script:ReadyShownFor = $null function Invoke-ReadyStep { if ($script:Settings.overlay -eq $false) { return } $s = Get-RunningSession if (-not $s -or -not $s.Id -or $script:ReadyShownFor -eq $s.Id) { return } $game = Get-Process -Name 'StarCitizen' -ErrorAction SilentlyContinue | Where-Object { $_.MainWindowHandle -ne [IntPtr]::Zero } | Select-Object -First 1 if (-not $game) { return } $script:ReadyShownFor = $s.Id # A panel that is up already - a contract accepted at once - stays. if ($overlay.Visible) { return } $script:OverlayState.Ready = $s Show-OverlayPanel $READY_SECONDS } # ---- Game data (companion 0.8.0, formerly the QuestGuide Uploader) ------------ # # Alex, 28.09.2026: one program instead of two, "voll integriert"; a new build # is offered, never sent by itself ("Anbieten"). For editors: the view and the # offer need a key a Versefiles admin allowed to upload game data (0.15.1). A run is this # program again with -GameData, in a process of its own - it reads for half a # minute and more, and the journal and the overlay go on meanwhile. $GAMEDATA_CHECK_MINUTES = 30 $script:GameDataState = @{ Channels = @() # from Finde-Kanaele: Name, Build, Version, Bereit, Grund, AusP4k, Veraltet Site = @{} # channel -> @{ Build; Known; Newest }, as the site answered the build ping Scanned = $false Run = $null # the running Versefiles.Companion.ChildRun RunChannel = '' RunReadOnly = $false Stopped = $false Offered = @{} # "LIVE 12660092" -> $true: each build is offered once per start Select = $null # the channel a notification led to NextCheck = (Get-Date).AddSeconds(45) } # The flat buttons of the Game data view. (The views' own tabs live in the # bar on top since 0.15.0.) function New-FlatButton([string]$text, $color) { $b = New-Object Versefiles.Companion.FlatButton $b.Text = $text $b.AutoSize = $true $b.ForeColor = $color $b.DisabledColor = $muted return $b } # The view: game folder, the channels in it with what Versefiles has of them, # the buttons, and what the last run said. $gameView = New-Object System.Windows.Forms.TableLayoutPanel $gameView.Dock = 'Fill' $gameView.Visible = $false $gameView.ColumnCount = 1 $gameView.RowCount = 5 $gameView.Padding = New-Object System.Windows.Forms.Padding(12, 6, 12, 8) [void]$gameView.ColumnStyles.Add((New-Object System.Windows.Forms.ColumnStyle([System.Windows.Forms.SizeType]::Percent, 100))) foreach ($rowStyle in @(@('AutoSize', 0), @('AutoSize', 0), @('Absolute', 104), @('AutoSize', 0), @('Percent', 100))) { [void]$gameView.RowStyles.Add((New-Object System.Windows.Forms.RowStyle([System.Windows.Forms.SizeType]$rowStyle[0], $rowStyle[1]))) } $gdFolderCaption = New-Object System.Windows.Forms.Label $gdFolderCaption.AutoSize = $true $gdFolderCaption.ForeColor = $muted $gdFolderCaption.Margin = New-Object System.Windows.Forms.Padding(0, 2, 0, 2) $gdFolderRow = New-Object System.Windows.Forms.TableLayoutPanel $gdFolderRow.ColumnCount = 3 $gdFolderRow.RowCount = 1 $gdFolderRow.AutoSize = $true $gdFolderRow.Dock = 'Fill' $gdFolderRow.Margin = New-Object System.Windows.Forms.Padding(0, 0, 0, 6) [void]$gdFolderRow.ColumnStyles.Add((New-Object System.Windows.Forms.ColumnStyle([System.Windows.Forms.SizeType]::Percent, 100))) [void]$gdFolderRow.ColumnStyles.Add((New-Object System.Windows.Forms.ColumnStyle([System.Windows.Forms.SizeType]::AutoSize))) [void]$gdFolderRow.ColumnStyles.Add((New-Object System.Windows.Forms.ColumnStyle([System.Windows.Forms.SizeType]::AutoSize))) $gdFolder = New-Object System.Windows.Forms.TextBox $gdFolder.ReadOnly = $true $gdFolder.BackColor = $field; $gdFolder.ForeColor = $light; $gdFolder.BorderStyle = 'FixedSingle' $gdFolder.Anchor = 'Left, Right' $btnGdBrowse = New-FlatButton 'Browse...' $accent $btnGdRescan = New-FlatButton 'Rescan' $accent $gdFolderRow.Controls.Add($gdFolder, 0, 0) $gdFolderRow.Controls.Add($btnGdBrowse, 1, 0) $gdFolderRow.Controls.Add($btnGdRescan, 2, 0) # No header row, as in the journal: WinForms draws it light. $gdChannels = New-Object System.Windows.Forms.ListView $gdChannels.View = 'Details' $gdChannels.HeaderStyle = 'None' $gdChannels.FullRowSelect = $true $gdChannels.MultiSelect = $false $gdChannels.HideSelection = $false $gdChannels.BackColor = $field; $gdChannels.ForeColor = $light; $gdChannels.BorderStyle = 'None' $gdChannels.Dock = 'Fill' [void]$gdChannels.Columns.Add('Channel', 110) [void]$gdChannels.Columns.Add('Build', 80) [void]$gdChannels.Columns.Add('In the game folder', 260) [void]$gdChannels.Columns.Add('Versefiles', 240) $gdChannels.Add_Resize({ if ($gdChannels.Columns.Count -eq 4) { $gdChannels.Columns[3].Width = -2 } }) # Drawn by hand: the selected row stays visible without the focus, and dark - # WinForms paints it light grey then. $gdChannels.OwnerDraw = $true $gdRowBrush = New-Object System.Drawing.SolidBrush $field $gdSelectedBrush = New-Object System.Drawing.SolidBrush ([System.Drawing.Color]::FromArgb(20, 48, 68)) $gdTextFlags = [System.Windows.Forms.TextFormatFlags]'Left, VerticalCenter, EndEllipsis, SingleLine, NoPrefix' $gdChannels.Add_DrawSubItem({ param($s, $ev) $cell = New-Object System.Drawing.Rectangle($ev.Bounds.X, $ev.Bounds.Y, $s.Columns[$ev.ColumnIndex].Width, $ev.Bounds.Height) $ev.Graphics.FillRectangle($(if ($ev.Item.Selected) { $gdSelectedBrush } else { $gdRowBrush }), $cell) $cell.Inflate(-4, 0) [System.Windows.Forms.TextRenderer]::DrawText($ev.Graphics, $ev.SubItem.Text, $s.Font, $cell, $ev.Item.ForeColor, $gdTextFlags) }) $gdButtons = New-Object System.Windows.Forms.FlowLayoutPanel $gdButtons.AutoSize = $true $gdButtons.WrapContents = $false $gdButtons.Dock = 'Fill' $gdButtons.Margin = New-Object System.Windows.Forms.Padding(0, 6, 0, 6) $btnGdUpload = New-FlatButton 'Upload' $accent $btnGdTry = New-FlatButton 'Read only' $accent $btnGdStop = New-FlatButton 'Stop' $warn $btnGdStop.Visible = $false $gdState = New-Object System.Windows.Forms.Label $gdState.AutoSize = $true $gdState.ForeColor = $muted $gdState.Margin = New-Object System.Windows.Forms.Padding(8, 8, 0, 0) foreach ($control in $btnGdUpload, $btnGdTry, $btnGdStop, $gdState) { [void]$gdButtons.Controls.Add($control) } $gdLog = New-Object System.Windows.Forms.TextBox $gdLog.Multiline = $true $gdLog.ReadOnly = $true $gdLog.ScrollBars = 'Vertical' $gdLog.WordWrap = $true $gdLog.BackColor = $field; $gdLog.ForeColor = $light; $gdLog.BorderStyle = 'None' $gdLog.Font = New-Object System.Drawing.Font('Consolas', 9) $gdLog.Dock = 'Fill' $gameView.Controls.Add($gdFolderCaption, 0, 0) $gameView.Controls.Add($gdFolderRow, 0, 1) $gameView.Controls.Add($gdChannels, 0, 2) $gameView.Controls.Add($gdButtons, 0, 3) $gameView.Controls.Add($gdLog, 0, 4) $content.Controls.Add($gameView) # The Game data tab without the right (0.15.1, Alex 15.10.2026: "Der Reiter # Game Data sollte die Eingabemaske fuer den API-Key bereithalten"): the key # field when there is no key the site takes, else why the view is not there. $gdAccessView = New-Object System.Windows.Forms.FlowLayoutPanel $gdAccessView.Dock = 'Fill' $gdAccessView.Visible = $false $gdAccessView.FlowDirection = 'TopDown' $gdAccessView.WrapContents = $false $gdAccessView.Padding = New-Object System.Windows.Forms.Padding(24, 20, 24, 12) $gdAccessTitle = New-Object System.Windows.Forms.Label $gdAccessTitle.AutoSize = $true $gdAccessTitle.Font = New-Object System.Drawing.Font($HOME_FONT_HEAD, 16) $gdAccessTitle.ForeColor = $light $gdAccessText = New-Object System.Windows.Forms.Label $gdAccessText.AutoSize = $true $gdAccessText.MaximumSize = New-Object System.Drawing.Size(560, 0) $gdAccessText.ForeColor = $muted $gdAccessText.Margin = New-Object System.Windows.Forms.Padding(0, 8, 0, 12) $gdKeyBox = New-Object System.Windows.Forms.TextBox $gdKeyBox.Width = 470 $gdKeyBox.UseSystemPasswordChar = $true $gdKeyBox.BackColor = $field; $gdKeyBox.ForeColor = $light; $gdKeyBox.BorderStyle = 'FixedSingle' $gdAccessButtons = New-Object System.Windows.Forms.FlowLayoutPanel $gdAccessButtons.AutoSize = $true $gdAccessButtons.WrapContents = $false $gdAccessButtons.Margin = New-Object System.Windows.Forms.Padding(0, 8, 0, 8) $btnGdKeySave = New-FlatButton 'Save' $accent $btnGdCheck = New-FlatButton 'Check again' $accent foreach ($control in $btnGdKeySave, $btnGdCheck) { [void]$gdAccessButtons.Controls.Add($control) } $lnkGdProfile = New-Object System.Windows.Forms.LinkLabel $lnkGdProfile.Text = 'Create a key on your profile page' $lnkGdProfile.AutoSize = $true $lnkGdProfile.LinkColor = $accent; $lnkGdProfile.ActiveLinkColor = $light foreach ($control in $gdAccessTitle, $gdAccessText, $gdKeyBox, $gdAccessButtons, $lnkGdProfile) { [void]$gdAccessView.Controls.Add($control) } $content.Controls.Add($gdAccessView) # A key typed into the tab - trimmed; nothing typed saves nothing. function Get-KeyToSave([string]$text) { $k = $text.Trim() if ($k) { return $k } return $null } function Update-GameDataAccessView([string]$access) { $t = Get-GameDataAccessText $access ([string]$script:Account.Name) $gdAccessTitle.Text = Format-Spaced $t.Title $gdAccessText.Text = $t.Text $gdKeyBox.Visible = $t.Form $btnGdKeySave.Visible = $t.Form $lnkGdProfile.Visible = $t.Form $btnGdCheck.Visible = $t.Check } # The tab's pane for the key as the site sees it. function Show-GameDataPane { $access = Get-GameDataAccess ([string]$script:Settings.key) $script:Account $gameView.Visible = $access -eq 'ok' $gdAccessView.Visible = $access -ne 'ok' if ($access -eq 'ok') { if (-not $script:GameDataState.Scanned) { Update-GameChannels; Update-SiteBuilds } # The site may just have said no (403, 401): then the tab says why. if ((Get-GameDataAccess ([string]$script:Settings.key) $script:Account) -ne 'ok') { Show-GameDataPane; return } Update-GameDataView } else { Update-GameDataAccessView $access } } # The Home view (0.14.0, Alex 13.10.2026; the launcher's look since 0.15.0): # start the game, and the numbers of this patch, the last 7 days or all # time - LIVE and HOTFIX only. On the fankit wallpaper, laid out by hand # (Set-HomeLayout): a left column with START GAME, the numbers on the right. $script:HomeRange = 'patch' $script:HomeDirty = $true $script:HomeDrawnAt = $null $script:LauncherPath = $null $HOME_DOT = ' ' + [char]0x00B7 + ' ' $homeView = New-Object Versefiles.Companion.HeroPanel $homeView.Dock = 'Fill' $homeView.Visible = $false $homeView.BackColor = $dark $homeView.Wallpaper = New-HomeWallpaper (Get-WallpaperBytes $HOME_WALLPAPER_DATA $PSCommandPath) function New-HomeText([string]$text, $color, [single]$size, [string]$family = $HOME_FONT, $style = [System.Drawing.FontStyle]::Regular) { $l = New-Object System.Windows.Forms.Label $l.AutoSize = $true $l.BackColor = [System.Drawing.Color]::Transparent $l.ForeColor = $color $l.Font = New-Object System.Drawing.Font($family, $size, $style) $l.Text = $text [void]$homeView.Controls.Add($l) return $l } $lblHomeTitle = New-HomeText ("Into the" + [Environment]::NewLine + "'verse") ([System.Drawing.Color]::White) 24 $HOME_FONT_HEAD $lblEnvCaption = New-HomeText 'Game environment' $muted 9 $cardEnv = New-Object Versefiles.Companion.StatCard $cardEnv.Cut = 0 $cardEnv.BorderColor = $accent $cardEnv.ValueFont = New-Object System.Drawing.Font($HOME_FONT, 11) $cardEnv.CaptionFont = New-Object System.Drawing.Font($HOME_FONT, 8.5) [void]$homeView.Controls.Add($cardEnv) $btnStartGame = New-Object Versefiles.Companion.CutCornerButton $btnStartGame.Font = New-Object System.Drawing.Font($HOME_FONT, 12) $btnStartGame.Text = Format-Spaced 'Start RSI Launcher' [void]$homeView.Controls.Add($btnStartGame) $lblGameState = New-HomeText '' $muted 9 $lblLastSession = New-HomeText '' $light 9 $homeTabs = @() foreach ($range in 'patch', 'week', 'all') { $t = New-Object Versefiles.Companion.UnderlineTab $t.Font = New-Object System.Drawing.Font($HOME_FONT, 10) $t.Tag = $range $t.Text = switch ($range) { 'patch' { 'This patch' } 'week' { 'Last 7 days' } default { 'All time' } } $t.UnderlineColor = [System.Drawing.Color]::White $t.AccessibleName = $t.Text $t.Add_Click({ param($s) $script:HomeRange = [string]$s.Tag; Update-HomeSafe }) [void]$homeView.Controls.Add($t) $homeTabs += $t } $btnRangePatch, $btnRangeWeek, $btnRangeAll = $homeTabs function New-HomeCard([string]$caption) { $c = New-Object Versefiles.Companion.StatCard $c.Caption = $caption $c.ValueFont = New-Object System.Drawing.Font($HOME_FONT, 16) $c.CaptionFont = New-Object System.Drawing.Font($HOME_FONT, 8.5) [void]$homeView.Controls.Add($c) return $c } $tileCompleted = New-HomeCard 'contracts done' $tilePlayed = New-HomeCard 'played' $tileRate = New-HomeCard 'success rate' $tileSessions = New-HomeCard 'sessions' $tileEarned = New-HomeCard 'aUEC earned' $tilePlaces = New-HomeCard 'places' $homeCards = @($tileCompleted, $tilePlayed, $tileRate, $tileSessions, $tileEarned, $tilePlaces) $homeGroups = @() foreach ($title in 'Progress', 'On the way', 'Economy') { $homeGroups += ,@((New-HomeText (Format-Spaced $title) $accent 8.5), (New-HomeText '' $light 9)) } $lnkSiteDashboard = New-Object System.Windows.Forms.LinkLabel $lnkSiteDashboard.AutoSize = $true $lnkSiteDashboard.BackColor = [System.Drawing.Color]::Transparent $lnkSiteDashboard.Font = New-Object System.Drawing.Font($HOME_FONT, 9) $lnkSiteDashboard.Text = 'More on versefiles.com ' + [char]0x2192 $lnkSiteDashboard.LinkColor = $accent $lnkSiteDashboard.ActiveLinkColor = $light [void]$homeView.Controls.Add($lnkSiteDashboard) # The fankit notice (agreement 2g): wherever the wallpaper shows. $lblFankit = New-Object System.Windows.Forms.Label $lblFankit.AutoSize = $false $lblFankit.BackColor = [System.Drawing.Color]::Transparent $lblFankit.ForeColor = $muted $lblFankit.Font = New-Object System.Drawing.Font($HOME_FONT, 7.5) $lblFankit.Text = $FANKIT_NOTICE $lblFankit.Visible = [bool]$homeView.Wallpaper [void]$homeView.Controls.Add($lblFankit) $content.Controls.Add($homeView) # By hand, from the panel's size: the left column 230 px, the numbers to the # right of it, the notice at the bottom - never over the wallpaper's free # corner (fankit agreement 2j). function Set-HomeLayout { $w = $homeView.ClientSize.Width; $h = $homeView.ClientSize.Height if ($w -le 0 -or $h -le 0) { return } $img = $homeView.Wallpaper $rect = if ($img) { Get-FitRectangle $img.Width $img.Height $w $h } else { New-Object System.Drawing.Rectangle(0, 0, $w, $h) } $homeView.ImageRect = $rect $homeView.FreeCorner = if ($img) { Get-FreeCorner $rect } else { New-Object System.Drawing.Size(0, 0) } $pad = 22 $lblHomeTitle.Location = New-Object System.Drawing.Point($pad, 14) $lblEnvCaption.Location = New-Object System.Drawing.Point($pad, 98) $cardEnv.SetBounds($pad, 116, 230, 46) $btnStartGame.SetBounds($pad, 174, 230, 46) $lblGameState.Location = New-Object System.Drawing.Point($pad, 226) $lblLastSession.Location = New-Object System.Drawing.Point($pad, 254) $x0 = $pad + 230 + 26 $right = $w - $pad $x = $x0 foreach ($t in $homeTabs) { $t.Location = New-Object System.Drawing.Point($x, 16); $x += $t.Width + 18 } $cardW = [int][Math]::Floor(($right - $x0 - 16) / 3) for ($i = 0; $i -lt $homeCards.Count; $i++) { $homeCards[$i].SetBounds(($x0 + ($i % 3) * ($cardW + 8)), (50 + [Math]::Floor($i / 3) * 70), $cardW, 62) } # Each group as high as its text: in a narrow window the economy line # wraps, and the next one moves down instead of running into it. $y = 196 foreach ($g in $homeGroups) { $g[0].Location = New-Object System.Drawing.Point($x0, ($y + 2)) $g[1].Location = New-Object System.Drawing.Point(($x0 + 96), $y) $g[1].MaximumSize = New-Object System.Drawing.Size([Math]::Max(60, $right - $x0 - 96), 0) $y += [Math]::Max(24, $g[1].PreferredSize.Height + 6) } $lnkSiteDashboard.Location = New-Object System.Drawing.Point($x0, ($y + 6)) $corner = $homeView.FreeCorner $lblFankit.SetBounds($pad, ($h - 40), [Math]::Max(100, ($rect.Right - $corner.Width - 12) - $pad), 34) } $homeView.Add_Resize({ Set-HomeLayout }) # The search reads three registry hives (120-140 ms): a launcher not found is # looked for again after a minute or when asked ($force: the view shown, the # button clicked) - not on every 2-second tick (0.14.1). $script:LauncherSearchedAt = $null function Get-LauncherPath([bool]$force = $false, $now = $null) { if (-not $now) { $now = Get-Date } if ($script:LauncherPath -and (Test-Path -LiteralPath $script:LauncherPath)) { return $script:LauncherPath } if ($force -or -not $script:LauncherSearchedAt -or ($now - $script:LauncherSearchedAt).TotalSeconds -ge 60) { $script:LauncherSearchedAt = $now $script:LauncherPath = Find-RsiLauncher (Get-RsiLauncherCandidates $GAME_ROOT) } return $script:LauncherPath } # The running session's time counts up to now, but new events - which mark # Home stale - come a minute apart or rarer: while a session runs, Home is # drawn anew once a minute (0.14.1). function Test-HomeStale($runningSession, $drawnAt, [datetime]$now) { if (-not $runningSession) { return $false } if (-not $drawnAt) { return $true } return ($now - $drawnAt).TotalSeconds -ge 60 } # The button follows the game: greyed out while it runs, and when no # launcher is to be found. function Update-StartButton { if (Test-GameRunning) { $btnStartGame.Text = Format-Spaced 'Game running'; $btnStartGame.AccessibleName = 'Game running'; $btnStartGame.Enabled = $false; $lblGameState.Text = '' return } [void](Get-LauncherPath) $btnStartGame.Enabled = [bool]$script:LauncherPath # "Start RSI Launcher" since 0.15.1 (Alex, 15.10.2026; before "Start game"): # the button opens the launcher, never the game itself. $btnStartGame.AccessibleName = if ($script:LauncherPath) { 'Start RSI Launcher' } else { 'RSI Launcher not found' } $btnStartGame.Text = Format-Spaced $btnStartGame.AccessibleName $lblGameState.Text = if ($script:LauncherPath) { '' } else { 'Install it from robertsspaceindustries.com' } } function Format-Count($n) { '{0:N0}' -f [double]$n } function Update-HomeView { $script:HomeDirty = $false $script:HomeDrawnAt = Get-Date $running = Get-RunningSession $st = Get-DashboardStats $script:Journal $script:Index $script:HomeRange ([datetime]::UtcNow) $(if ($running) { $running.Id } else { $null }) # The patch button carries the patch's name whichever range is chosen: # that of the newest LIVE/HOTFIX session. $newest = @($script:Index.Values | Where-Object { $_.channel -in 'LIVE', 'HOTFIX' -and $_.started } | Sort-Object { ConvertTo-UtcTime $_.started }) | Select-Object -Last 1 $patchName = if ($newest) { Get-PatchLabel $GAME_ROOT ([string]$newest.channel) ([string]$newest.build) } else { $null } $btnRangePatch.Text = if ($patchName) { $patchName } else { 'This patch' } foreach ($t in $homeTabs) { $t.Selected = ([string]$t.Tag -eq $script:HomeRange) } # "LIVE · 4.10.2-live.12913470": the channel big, the version small. # The installed build of the channel last played - what START GAME runs. $envText = if ($newest) { Get-EnvironmentText $GAME_ROOT ([string]$newest.channel) '' } else { [string][char]0x2013 } $envParts = $envText -split (' ' + [char]0x00B7 + ' '), 2 $cardEnv.Value = $envParts[0] $cardEnv.Caption = if ($envParts.Count -gt 1) { $envParts[1] } else { '' } $tileCompleted.Value = Format-Count $st.Completed $tilePlayed.Value = Format-TileDuration $st.Seconds $tileSessions.Value = Format-Count $st.Sessions $tileEarned.Value = Format-Earned $st $tileRate.Value = if ($null -eq $st.SuccessRate) { [string][char]0x2013 } else { '{0:0} %' -f ($st.SuccessRate * 100) } $tilePlaces.Value = Format-Count $st.Places # Day and month in English like the rest of the window, not in the # language of Windows ("Sa 10 Okt"). $lblLastSession.Text = if ($st.LastSession) { 'Last session' + [Environment]::NewLine + ('{0} {1} {2} {1} {3}' -f $st.LastSession.Started.ToLocalTime().ToString('ddd d MMM, HH:mm', [Globalization.CultureInfo]::InvariantCulture), [char]0x00B7, (Format-Duration $st.LastSession.Seconds), $st.LastSession.Channel) } else { 'No session yet - the numbers start' + [Environment]::NewLine + 'with your first game on LIVE.' } $homeGroups[0][1].Text = '{0} blueprints{1}{2} places' -f (Format-Count $st.Blueprints), $HOME_DOT, (Format-Count $st.Places) $homeGroups[1][1].Text = '{0} jumps{4}{1} deaths{4}{2} medbed{4}{3} CrimeStat' -f ` (Format-Count $st.Jumps), (Format-Count $st.Deaths), (Format-Count $st.Medbed), (Format-Count $st.CrimeStat), $HOME_DOT $homeGroups[2][1].Text = '{0} refinery{5}{1} sales, {2}{5}{3} purchases, {4}' -f ` (Format-Count $st.Refinery), (Format-Count $st.Sold), (Format-Money $st.SoldAmount), (Format-Count $st.Bought), (Format-Money $st.BoughtAmount), $HOME_DOT Update-StartButton Set-HomeLayout } # Home is drawn from the start-up handler and the timer. A fault there - the # registry, the file system, a date - is written down and the companion goes # on: thrown in the start-up handler it kept the timer from ever starting, so # nothing of the game was read at all (review of 0.14.0; 0.14.1). # $full: the numbers too, or only the start button. function Update-HomeSafe([bool]$full = $true) { try { if ($full) { Update-HomeView } else { Update-StartButton } } catch { Write-CompanionError $_ 'home' } } function Add-GameDataLog([string]$line) { $gdLog.AppendText((Get-Date -Format 'HH:mm:ss') + ' ' + $line + [Environment]::NewLine) } function Get-GameRootSourceText { switch ($script:GameRootSource) { 'parameter' { 'from -GamePath' } 'settings' { 'chosen here' } 'uploader' { "from the old uploader's settings" } 'launcher' { 'found in the RSI Launcher log' } default { 'the default path - Browse... if the game is installed elsewhere' } } } # What the game folder holds of a channel ... function Get-ChannelFilesText($c) { if ($c.Bereit -and $c.AusP4k -and $c.Veraltet) { return 'ready, from Data.p4k (loose file older)' } if ($c.Bereit -and $c.AusP4k) { return 'ready, from Data.p4k' } if ($c.Bereit) { return 'ready' } return 'not ready: ' + $c.Grund } # ... and what Versefiles has of it, as far as it was asked for this build. function Get-ChannelSiteText($c) { $s = $script:GameDataState.Site[[string]$c.Name] if (-not $s -or $s.Build -ne [string]$c.Build) { return '' } if ($s.Known) { return 'on Versefiles' } if ($s.Newest) { return 'not on Versefiles yet - it has {0}' -f $s.Newest } return 'not on Versefiles' } # Newer than the newest the site has of that channel. A channel the site has # never had (no newest) is not offered: nobody uploads it. function Test-BuildMissing($c) { $s = $script:GameDataState.Site[[string]$c.Name] if (-not $c.Bereit -or -not $s -or $s.Known -or $s.Build -ne [string]$c.Build -or -not $s.Newest) { return $false } $mine = 0L; $theirs = 0L return [long]::TryParse([string]$c.Build, [ref]$mine) -and [long]::TryParse([string]$s.Newest, [ref]$theirs) -and $mine -gt $theirs } function Update-GameChannels { $script:SC_ROOT = $GAME_ROOT try { $script:GameDataState.Channels = @(Finde-Kanaele) } catch { $script:GameDataState.Channels = @(); Add-GameDataLog ('Error while reading the game folder: ' + $_.Exception.Message) } $script:GameDataState.Scanned = $true } # Tells Versefiles which build each ready channel has and hears whether the # site has it (POST /api/v1/build-ping, the old uploader's Melde-Build - the # site keeps it as "seen in the wild"). Only builds not asked about yet, # unless $again. With the companion key only. function Update-SiteBuilds([bool]$again = $false) { $key = [string]$script:Settings.key if (-not $key) { return } foreach ($c in $script:GameDataState.Channels) { $build = [string]$c.Build if (-not $c.Bereit -or -not $build -or $build -eq 'unbekannt') { continue } $had = $script:GameDataState.Site[[string]$c.Name] if (-not $again -and $had -and $had.Build -eq $build) { continue } try { $ping = @{ channel = [string]$c.Name; build_ref = $build } if ($c.Version) { $ping.game_version = [string]$c.Version } $a = Invoke-RestMethod -Uri (([string]$script:Settings.apiBase).TrimEnd('/') + '/api/v1/build-ping') -Method Post ` -Body ([System.Text.Encoding]::UTF8.GetBytes(($ping | ConvertTo-Json -Compress))) ` -ContentType 'application/json; charset=utf-8' -Headers @{ Authorization = 'Bearer ' + $key } ` -TimeoutSec 8 -UserAgent $UPDATE_AGENT $script:GameDataState.Site[[string]$c.Name] = @{ Build = $build; Known = [bool]$a.bekannt; Newest = [string]$a.neuester } } catch { # The right taken away (403) or the key itself (401, a key revoked, # an account blocked): the tab says so at once. $code = Get-HttpStatus $_ if ($code -eq 403) { $script:Account.GameData = $false } if ($code -eq 401) { $script:Account.Status = 'rejected'; $script:Account.GameData = $null } # Offline, or a key the site does not take: nothing to say about it. $script:GameDataState.Site.Remove([string]$c.Name) } } } function Get-SelectedChannel { if ($gdChannels.SelectedItems.Count -eq 0) { return $null } $name = [string]$gdChannels.SelectedItems[0].Tag return $script:GameDataState.Channels | Where-Object { $_.Name -eq $name } | Select-Object -First 1 } function Update-GameDataButtons { $running = [bool]$script:GameDataState.Run $c = Get-SelectedChannel $ready = [bool]($c -and $c.Bereit) $btnGdUpload.Text = if ($c) { 'Upload ' + $c.Name } else { 'Upload' } $btnGdUpload.Enabled = $ready -and -not $running -and (Get-GameDataAccess ([string]$script:Settings.key) $script:Account) -eq 'ok' $btnGdTry.Enabled = $ready -and -not $running $btnGdStop.Visible = $running $btnGdBrowse.Enabled = -not $running $btnGdRescan.Enabled = -not $running $gdState.Text = if ($running) { 'Reading {0} - half a minute or more...' -f $script:GameDataState.RunChannel } elseif ($c -and (Test-BuildMissing $c)) { 'Versefiles does not have this build yet.' } else { '' } } function Update-GameDataView { $gdFolderCaption.Text = 'Game folder - ' + (Get-GameRootSourceText) $gdFolder.Text = $GAME_ROOT $keep = if ($script:GameDataState.Select) { $script:GameDataState.Select } else { $c = Get-SelectedChannel; if ($c) { [string]$c.Name } else { $null } } $script:GameDataState.Select = $null $gdChannels.BeginUpdate() $gdChannels.Items.Clear() foreach ($c in $script:GameDataState.Channels) { $item = New-Object System.Windows.Forms.ListViewItem([string]$c.Name) $item.Tag = [string]$c.Name [void]$item.SubItems.Add($(if ($c.Build -and $c.Build -ne 'unbekannt') { [string]$c.Build } else { '?' })) [void]$item.SubItems.Add((Get-ChannelFilesText $c)) [void]$item.SubItems.Add((Get-ChannelSiteText $c)) $item.ForeColor = if (-not $c.Bereit) { $muted } elseif (Test-BuildMissing $c) { $accent } else { $light } [void]$gdChannels.Items.Add($item) } # The channel from before, else the first one Versefiles lacks, else LIVE, # else the first ready one. $pick = $null foreach ($test in { param($i, $c) $keep -and $c.Name -eq $keep }, { param($i, $c) Test-BuildMissing $c }, { param($i, $c) $c.Bereit -and $c.Name -eq 'LIVE' }, { param($i, $c) $c.Bereit }) { for ($i = 0; $i -lt $script:GameDataState.Channels.Count -and $null -eq $pick; $i++) { if (& $test $i $script:GameDataState.Channels[$i]) { $pick = $i } } if ($null -ne $pick) { break } } if ($null -ne $pick) { $gdChannels.Items[$pick].Selected = $true } $gdChannels.EndUpdate() if (-not $script:GameDataState.Channels.Count) { $gdState.Text = 'No installation in this folder.' } Update-GameDataButtons } # A notification for the builds the site does not have yet - once per build # and start, not while the game runs. A click leads to the view; the upload # is the user's click there (Alex: "Anbieten"). function Invoke-GameDataOffer { if ((Test-GameRunning) -or $script:GameDataState.Run) { return } $new = @($script:GameDataState.Channels | Where-Object { (Test-BuildMissing $_) -and -not $script:GameDataState.Offered[('{0} {1}' -f $_.Name, $_.Build)] }) if (-not $new.Count) { return } foreach ($c in $new) { $script:GameDataState.Offered[('{0} {1}' -f $c.Name, $c.Build)] = $true } $script:GameDataState.Select = [string]$new[0].Name $names = ($new | ForEach-Object { '{0} {1}' -f $_.Name, $_.Build }) -join ', ' Show-TrayNote 'New game build' ('Not on Versefiles yet: {0}. Click to upload.' -f $names) 'Info' 10000 $null 'gamedata' } # Every half hour and right after the game closes: has the launcher brought a # new build? Reading the folder takes a fraction of a second; the site is # asked only about builds it was not asked about yet. function Invoke-GameDataStep { if ($script:GameDataState.Run -or (Get-Date) -lt $script:GameDataState.NextCheck) { return } $access = Get-GameDataAccess ([string]$script:Settings.key) $script:Account if ($access -notin 'ok', 'unknown') { return } $script:GameDataState.NextCheck = (Get-Date).AddMinutes($GAMEDATA_CHECK_MINUTES) if (Test-GameRunning) { return } # The site was away when the companion started (review of 0.15.1): ask it # again, at this pace - the call can take 15 seconds on the window's thread. if ($access -eq 'unknown') { Update-Account if ($gameView.Visible -or $gdAccessView.Visible) { Show-GameDataPane } if ((Get-GameDataAccess ([string]$script:Settings.key) $script:Account) -ne 'ok') { return } } Update-GameChannels Update-SiteBuilds if ($gameView.Visible -or $gdAccessView.Visible) { Show-GameDataPane } if ((Get-GameDataAccess ([string]$script:Settings.key) $script:Account) -eq 'ok') { Invoke-GameDataOffer } } function Show-View([string]$view) { if ($view -notin 'home', 'journal', 'gamedata') { $view = 'home' } $homeView.Visible = $view -eq 'home' $list.Visible = $view -eq 'journal' $gameView.Visible = $false $gdAccessView.Visible = $false $btnViewHome.Selected = $view -eq 'home' $btnViewJournal.Selected = $view -eq 'journal' $btnViewGameData.Selected = $view -eq 'gamedata' # A switch of the journal list - only where the list is; Notifications # (always there) stands left of it since 0.15.0. $chkAll.Visible = $view -eq 'journal' # Shown anew: a launcher not found is looked for again right away. if ($view -eq 'home') { $script:LauncherSearchedAt = $null; Update-HomeSafe } if ($view -eq 'gamedata') { Show-GameDataPane } } function Update-Nav { # Always there since 0.15.1: without a key or the right, the tab says why. $btnViewGameData.Visible = $true if ($gameView.Visible -or $gdAccessView.Visible) { Show-GameDataPane } # The tabs got narrower or wider: the status takes the rest of the bar. $nav.PerformLayout() Set-TopBarLayout } # This program again, in a process of its own: from the .cmd the way the # .cmd starts itself (the file in QG_SELF, the arguments in QG_ARGS), as a # .ps1 with -File. function Start-ChildRun([string]$arguments) { if ($PSCommandPath) { return New-Object Versefiles.Companion.ChildRun('powershell.exe', ('-NoProfile -ExecutionPolicy Bypass -File "{0}" {1}' -f $PSCommandPath, $arguments), $null) } return New-Object Versefiles.Companion.ChildRun('powershell.exe', '-NoProfile -ExecutionPolicy Bypass -Command "$t=[IO.File]::ReadAllText($env:QG_SELF);$m=[char]35+''@PSSTART'';iex $t.Substring($t.IndexOf($m)+$m.Length)"', $arguments) } function Start-GameDataRun([bool]$readOnly) { if ($script:GameDataState.Run) { return } $c = Get-SelectedChannel if (-not $c -or -not $c.Bereit) { return } $arguments = '-GameData -Channel {0} -GamePath "{1}" -ApiBase "{2}"' -f $c.Name, $GAME_ROOT, $script:Settings.apiBase if ($readOnly) { $arguments += ' -WhatIfOnly' } Add-GameDataLog ('--- {0}, build {1}: {2} ---' -f $c.Name, $c.Build, $(if ($readOnly) { 'reading, nothing is sent' } else { 'reading and uploading' })) try { # The key the window judged the right by (also one given with -Key), # handed over in the run's environment - never on its command line. $env:VF_GAMEDATA_KEY = [string]$script:Settings.key try { $script:GameDataState.Run = Start-ChildRun $arguments } finally { Remove-Item Env:\VF_GAMEDATA_KEY -ErrorAction SilentlyContinue } $script:GameDataState.RunChannel = [string]$c.Name $script:GameDataState.RunReadOnly = $readOnly $script:GameDataState.Stopped = $false $gdTimer.Start() } catch { Add-GameDataLog ('Could not start: ' + $_.Exception.Message) } Update-GameDataButtons } # Takes the run's lines four times a second; at its end says how it went - # with a notification too if the window is not in view. $gdTimer = New-Object System.Windows.Forms.Timer $gdTimer.Interval = 250 $gdTimer.Add_Tick({ $run = $script:GameDataState.Run if (-not $run) { $gdTimer.Stop(); return } foreach ($line in $run.Take()) { Add-GameDataLog $line } if (-not $run.Ended) { return } foreach ($line in $run.Take()) { Add-GameDataLog $line } $gdTimer.Stop() $code = $run.ExitCode $run.Dispose() $script:GameDataState.Run = $null $name = $script:GameDataState.RunChannel $outcome = if ($code -eq 0) { 'done' } elseif ($script:GameDataState.Stopped) { 'stopped' } else { 'failed' } Add-GameDataLog ('--- {0} ---' -f $outcome) if ($code -eq 0 -and -not $script:GameDataState.RunReadOnly) { Update-SiteBuilds $true } $script:GameDataState.Select = $name Update-GameDataView if (-not $form.Visible -or $form.WindowState -eq 'Minimized') { $text = switch ($outcome) { 'done' { if ($script:GameDataState.RunReadOnly) { "$name was read - nothing was sent." } else { "$name is uploaded." } } default { "The $name run $outcome - see the Game data view." } } Show-TrayNote 'Game data' $text 'Info' 8000 $null 'gamedata' } }) $btnViewJournal.Add_Click({ Show-View 'journal' }) $btnViewGameData.Add_Click({ Show-View 'gamedata' }) # Asking the site takes up to 15 seconds on the window's thread: the tab # says so meanwhile, and its buttons wait. function Invoke-GameDataAccessCheck { $gdAccessText.Text = 'Asking Versefiles about this key...' $btnGdKeySave.Enabled = $false; $btnGdCheck.Enabled = $false [System.Windows.Forms.Application]::DoEvents() try { Update-Account } finally { $btnGdKeySave.Enabled = $true; $btnGdCheck.Enabled = $true } } $btnGdKeySave.Add_Click({ $k = Get-KeyToSave $gdKeyBox.Text if (-not $k) { return } Set-CompanionKey $k $gdKeyBox.Text = '' Invoke-GameDataAccessCheck Invoke-Upload Show-GameDataPane }) $btnGdCheck.Add_Click({ Invoke-GameDataAccessCheck; Show-GameDataPane }) $lnkGdProfile.Add_LinkClicked({ Start-Process (([string]$script:Settings.apiBase).TrimEnd('/') + '/profile#companion') }) $btnViewHome.Add_Click({ Show-View 'home' }) $btnStartGame.Add_Click({ if (-not (Start-RsiLauncher)) { $script:LauncherPath = $null; $script:LauncherSearchedAt = $null } Update-HomeSafe $false }) $lnkSiteDashboard.Add_LinkClicked({ Start-Process (([string]$script:Settings.apiBase).TrimEnd('/') + '/dashboard#game-sessions') }) $gdChannels.Add_SelectedIndexChanged({ Update-GameDataButtons }) $btnGdUpload.Add_Click({ Start-GameDataRun $false }) $btnGdTry.Add_Click({ Start-GameDataRun $true }) $btnGdStop.Add_Click({ if ($script:GameDataState.Run) { $script:GameDataState.Stopped = $true; $script:GameDataState.Run.Stop() } }) $btnGdRescan.Add_Click({ Update-GameChannels Update-SiteBuilds $true Show-GameDataPane Add-GameDataLog ('Read the game folder again: {0} channel(s).' -f $script:GameDataState.Channels.Count) }) $btnGdBrowse.Add_Click({ $dlg = New-Object System.Windows.Forms.FolderBrowserDialog $dlg.Description = 'Star Citizen installation folder (the "StarCitizen" folder that holds LIVE, PTU, ...)' $dlg.ShowNewFolderButton = $false if (Test-Path -LiteralPath $GAME_ROOT) { $dlg.SelectedPath = $GAME_ROOT } if ($dlg.ShowDialog($form) -ne [System.Windows.Forms.DialogResult]::OK) { return } $p = Resolve-GameRoot $dlg.SelectedPath if (-not $p) { Add-GameDataLog ("No installation under '{0}' - expected the StarCitizen folder with LIVE, PTU, ..." -f $dlg.SelectedPath) return } $script:Settings.gamePath = $p Save-Settings $script:Settings if ($p -ne $GAME_ROOT) { Add-GameDataLog 'The journal follows the new folder once the companion is started again.' } $script:GAME_ROOT = $p $script:GameRootSource = 'settings' Update-GameChannels Update-SiteBuilds $true Show-GameDataPane }) $timer = New-Object System.Windows.Forms.Timer $timer.Interval = 2000 $script:Ticks = 0 $timer.Add_Tick({ try { # Started a second time: show the window (0.14.0). if ($script:ShowEvent.WaitOne(0)) { Show-Main; Show-View 'home' } $fresh = Receive-NewEvents # Send every five minutes, right when a session ends - and right when # a contract is given up: the notification about it links to the # page where it can be answered, and that page must have it already. # So the upload comes first, the window and the notification after. $ended = @($fresh | Where-Object { (Get-Kind $_) -eq 'session_ended' }).Count -gt 0 $abandoned = @($fresh | Where-Object { Test-Abandon $_ }).Count -gt 0 if ($ended -or $abandoned -or ++$script:Ticks -ge 150) { $script:Ticks = 0 $timer.Stop() try { Invoke-Upload } finally { $timer.Start() } } if ($fresh.Count) { Add-ToView $fresh } # Also without new events: an accepted contract waits a moment for its name. Invoke-OverlayStep $fresh Invoke-ReadyStep # The game just closed: the moment an update may go in - so ask now # instead of waiting up to six hours for the next check. The same # for a new game build: the launcher patches while the game is closed. if ($ended) { $script:UpdateState.NextCheck = Get-Date; $script:GameDataState.NextCheck = Get-Date } Update-Status if ($homeView.Visible -and $form.Visible) { Update-HomeSafe ($script:HomeDirty -or (Test-HomeStale (Get-RunningSession) $script:HomeDrawnAt (Get-Date))) } Invoke-UpdateStep Invoke-GameDataStep } catch { Write-CompanionError $_ 'timer' $lblStatus.Text = 'Error while reading: ' + $_.Exception.Message } }) # The window comes up first; reading earlier sessions can take a few # seconds on the first start. $form.Add_Shown({ if (-not $SelfTest) { Set-ConsoleVisible $false $tray.Visible = $true # Started by a downloaded copy that installed this one (0.13.1): say # once where it starts from now. QG_INSTALLED goes, so that nothing # this companion starts later inherits it. $installedNow = $env:QG_INSTALLED -eq '1' $env:QG_INSTALLED = $null # Started again by the update helper: say so once, or why it failed. $done = Read-UpdateResult 'companion' if ($installedNow) { Show-TrayNote 'Versefiles Companion' 'Installed - start it from the desktop shortcut or the Start menu from now on.' 'Info' 8000 } elseif ($done -and $done.ok) { Show-TrayNote 'Versefiles Companion' ('Updated to version {0}.' -f $COMPANION_VERSION) 'Info' 5000 } elseif ($done) { Show-TrayNote 'Versefiles Companion' ('The update to {0} failed: {1}' -f $done.to, $done.message) 'Warning' 8000 } } Update-Nav $say = { param($m) $lblStatus.Text = $m; [System.Windows.Forms.Application]::DoEvents() } & $say 'Reading earlier sessions...' [void](Import-LogBackups $GAME_ROOT $say) Start-LogFollowers $GAME_ROOT [void](Receive-NewEvents) $script:Journal = Read-Journal # Started during a game: the contracts open in it (0.12.0). Restore-OpenContracts Update-View Update-Totals Update-Status # Every start opens on Home (0.14.0). Show-View 'home' if ($SelfTest) { [System.Windows.Forms.Application]::DoEvents() $bmp = New-Object System.Drawing.Bitmap($form.Width, $form.Height) $form.DrawToBitmap($bmp, (New-Object System.Drawing.Rectangle(0, 0, $form.Width, $form.Height))) $bmp.Save((Join-Path $DataDir 'selftest.png'), [System.Drawing.Imaging.ImageFormat]::Png) $bmp.Dispose() $homeLine = '{0} completed, {1} played' -f $tileCompleted.Value, $tilePlayed.Value # Home a second time, on another range - drawn twice for real, as the # overlay taught us (0.12.0): a fault in the second drawing is a fault. $script:HomeRange = 'all' Update-HomeView [System.Windows.Forms.Application]::DoEvents() $bmp = New-Object System.Drawing.Bitmap($form.Width, $form.Height) $form.DrawToBitmap($bmp, (New-Object System.Drawing.Rectangle(0, 0, $form.Width, $form.Height))) $bmp.Save((Join-Path $DataDir 'home-all.png'), [System.Drawing.Imaging.ImageFormat]::Png) $bmp.Dispose() $homeLine += ' | all time: {0} completed, {1} played' -f $tileCompleted.Value, $tilePlayed.Value # Without the wallpaper - the starfield, no fankit notice (0.15.0). $savedWall = $homeView.Wallpaper $homeView.Wallpaper = $null; $lblFankit.Visible = $false Show-View 'home' [System.Windows.Forms.Application]::DoEvents() $bmp = New-Object System.Drawing.Bitmap($form.Width, $form.Height) $form.DrawToBitmap($bmp, (New-Object System.Drawing.Rectangle(0, 0, $form.Width, $form.Height))) $bmp.Save((Join-Path $DataDir 'home-nowall.png'), [System.Drawing.Imaging.ImageFormat]::Png) $bmp.Dispose() $homeView.Wallpaper = $savedWall; $lblFankit.Visible = [bool]$savedWall # The smallest window and a large one. foreach ($size in @(@(720, 540, 'home-small.png'), @(1280, 800, 'home-large.png'))) { $form.Size = New-Object System.Drawing.Size($size[0], $size[1]) Show-View 'home' [System.Windows.Forms.Application]::DoEvents() $bmp = New-Object System.Drawing.Bitmap($form.Width, $form.Height) $form.DrawToBitmap($bmp, (New-Object System.Drawing.Rectangle(0, 0, $form.Width, $form.Height))) $bmp.Save((Join-Path $DataDir $size[2]), [System.Drawing.Imaging.ImageFormat]::Png) $bmp.Dispose() } $form.Size = New-Object System.Drawing.Size(760, 560) Show-View 'journal' # The overlay as well, with an answer like the server's and a second # open contract - invisible (opacity 0), drawn to overlay.png. $sample = New-OverlayContract 'selftest' 'Moraine Data Retrieval' 'LIVE' $false $sample.Asked = $true $sample.Info = [pscustomobject]@{ title = 'Moraine Data Retrieval'; url = 'https://versefiles.com/m/battaglia_rpt_datadownload_01' reward = [pscustomobject]@{ min = 12500; max = $null } duration = [pscustomobject]@{ median = 840; text = '14 min'; span = '12–18 min (median 14)'; runs = 9; players = 4 } steps = [pscustomobject]@{ source = 'game'; items = @('Go to Moraine Base', 'Locate Server', 'Download 3 Server Data', 'Return to Battaglia'); more = 0 } problem = [pscustomobject]@{ players = 3; abandons = 5; step = 'Download 3 Server Data' } blueprints = [pscustomobject]@{ count = 13; names = @('ADP Core', 'Palatino Arms', 'Testudo Helmet', 'Monde Legs') owned = @('Monde Legs'); owned_count = 3 } record = [pscustomobject]@{ completed = 2; last_completed = '2026-09-26 22:59:00'; abandoned = 1; time = '12 min'; fastest = '9 min'; last_reward = 76500 } } # In the middle of it: the first step done, the second open. $sample.Done = @{ (Get-StepKey 'Go to Moraine Base') = $true } [void]$sample.Open.Add(@{ Key = (Get-StepKey 'Locate Server'); Text = 'Locate Server' }) $second = New-OverlayContract 'selftest-2' 'Defend InterSec Operative' 'LIVE' $false [void]$second.Open.Add(@{ Key = (Get-StepKey 'Destroy the attacking ships'); Text = 'Destroy the attacking ships' }) $second.Active = [datetime]::UtcNow.AddMinutes(-5) $script:OverlayState.Contracts['selftest'] = $sample $script:OverlayState.Contracts['selftest-2'] = $second $script:OverlayState.Order = @('selftest', 'selftest-2') $script:OverlayState.Shown = 'selftest' $script:OverlayState.Now = @('Locate Server') $script:OverlayState.HotkeyText = $DEFAULT_OVERLAY_HOTKEY # Twice, as in a game: there the panel always replaces lines it has # (0.12.2 - a first drawing alone never showed that error). Update-OverlayContent Update-OverlayContent $overlay.Opacity = 0 $overlay.Show() [System.Windows.Forms.Application]::DoEvents() $bmp = New-Object System.Drawing.Bitmap($overlay.Width, $overlay.Height) $overlay.DrawToBitmap($bmp, (New-Object System.Drawing.Rectangle(0, 0, $overlay.Width, $overlay.Height))) $bmp.Save((Join-Path $DataDir 'overlay.png'), [System.Drawing.Imaging.ImageFormat]::Png) $bmp.Dispose() # And the panel a game start brings (0.10.0), drawn to ready.png - as # a companion on the beta channel shows it (0.13.0). $savedChannel = $script:Settings.updateChannel $script:Settings.updateChannel = 'beta' $script:OverlayState.Ready = [pscustomobject]@{ Id = 'selftest'; Channel = 'LIVE'; Build = '12660092' } Update-OverlayContent [System.Windows.Forms.Application]::DoEvents() $bmp = New-Object System.Drawing.Bitmap($overlay.Width, $overlay.Height) $overlay.DrawToBitmap($bmp, (New-Object System.Drawing.Rectangle(0, 0, $overlay.Width, $overlay.Height))) $bmp.Save((Join-Path $DataDir 'ready.png'), [System.Drawing.Imaging.ImageFormat]::Png) $bmp.Dispose() $script:OverlayState.Ready = $null $script:Settings.updateChannel = $savedChannel $overlay.Hide() # The Game data view too, with the channels of this machine and an # answer like the site's - the key only in memory, nothing is asked # or sent. Drawn to gamedata.png. # Without the right first: the key field, then the hint (0.15.1). foreach ($pic in @(@{ Key = ''; GameData = $null; File = 'gamedata-key.png' }, @{ Key = 'selftest'; GameData = $false; File = 'gamedata-denied.png' })) { $script:Settings.key = $pic.Key $script:Account.Name = 'Selftest'; $script:Account.Status = 'ok'; $script:Account.GameData = $pic.GameData Show-View 'gamedata' [System.Windows.Forms.Application]::DoEvents() $bmp = New-Object System.Drawing.Bitmap($form.Width, $form.Height) $form.DrawToBitmap($bmp, (New-Object System.Drawing.Rectangle(0, 0, $form.Width, $form.Height))) $bmp.Save((Join-Path $DataDir $pic.File), [System.Drawing.Imaging.ImageFormat]::Png) $bmp.Dispose() } $script:Settings.key = 'selftest' $script:Account.Name = 'Selftest'; $script:Account.Status = 'ok'; $script:Account.GameData = $true # Read first: with the channels known, drawing the tab asks no site # (review of 0.15.1: Update-Nav sent four real build pings). Update-GameChannels Update-Nav foreach ($c in $script:GameDataState.Channels) { if ($c.Bereit) { $script:GameDataState.Site[[string]$c.Name] = @{ Build = [string]$c.Build; Known = ($c.Name -ne 'LIVE'); Newest = '12345678' } } } Show-View 'gamedata' Add-GameDataLog 'Self-test: the channels of this machine; what Versefiles has is made up.' # VF_SELFTEST_GAMEDATA=: also a read-only run of that channel, # started by the window's own button - half a minute, reads the game. if ($env:VF_SELFTEST_GAMEDATA) { $script:GameDataState.Select = $env:VF_SELFTEST_GAMEDATA Update-GameDataView $btnGdTry.PerformClick() $until = (Get-Date).AddMinutes(5) while ($script:GameDataState.Run -and (Get-Date) -lt $until) { [System.Windows.Forms.Application]::DoEvents() Start-Sleep -Milliseconds 50 } } [System.Windows.Forms.Application]::DoEvents() $bmp = New-Object System.Drawing.Bitmap($form.Width, $form.Height) $form.DrawToBitmap($bmp, (New-Object System.Drawing.Rectangle(0, 0, $form.Width, $form.Height))) $bmp.Save((Join-Path $DataDir 'gamedata.png'), [System.Drawing.Imaging.ImageFormat]::Png) $bmp.Dispose() # The icon (0.13.1) as the .ico gives it back: every size on the # window's colour and on white, the small ones four times as big # below - drawn to icon.png. $sheet = New-Object System.Drawing.Bitmap(790, 340) $g = [System.Drawing.Graphics]::FromImage($sheet) $g.InterpolationMode = [System.Drawing.Drawing2D.InterpolationMode]::NearestNeighbor $g.PixelOffsetMode = [System.Drawing.Drawing2D.PixelOffsetMode]::Half $g.Clear($dark) $g.FillRectangle([System.Drawing.Brushes]::White, 0, 170, 790, 170) foreach ($top in 0, 170) { $x = 10 foreach ($size in $ICON_SIZES | Sort-Object -Descending) { $one = New-Object System.Drawing.Icon($icon, $size, $size) $pic = if ($size -ge 256) { New-CompanionIconBitmap $size } else { $one.ToBitmap() } $side = [Math]::Min($size, 150) $g.DrawImage($pic, $x, ($top + 10), $side, $side) if ($size -le 32) { $g.DrawImage($pic, (New-Object System.Drawing.RectangleF($x, ($top + 40), ($size * 4), ($size * 4)))) } $x += [Math]::Max($side, $(if ($size -le 32) { $size * 4 } else { 0 })) + 10 $pic.Dispose(); $one.Dispose() } } $g.Dispose() $sheet.Save((Join-Path $DataDir 'icon.png'), [System.Drawing.Imaging.ImageFormat]::Png) $sheet.Dispose() if ($env:VF_SELFTEST_GAMEDATA) { Write-Host $gdLog.Text } Write-Host ("Self-test: {0} rows shown, {1} events in journal; {2} | {3}; {4} game channels" -f $list.Items.Count, $script:Journal.Count, $lblStatus.Text, $homeLine, $gdChannels.Items.Count) $script:Exiting = $true $form.Close() return } # The keys that bring the overlay back (or hide it) - watched, so they # work while the game has the focus and still reach the game. if ($script:Settings.overlay -ne $false) { $hk = ConvertTo-Hotkey ([string]$script:Settings.overlayHotkey) if ($hk) { $script:OverlayState.HotkeyText = $hk.Text $script:OverlayState.Hotkey = New-Object Versefiles.Companion.Hotkey($hk.Modifiers, $hk.Key) $script:OverlayState.Hotkey.add_Pressed({ Invoke-OverlayKey }) } } # Whose key it is, then whatever was not sent yet - on the first start # with a key, the whole history read from logbackups. Update-Account Invoke-Upload $timer.Start() }) if ($SelfTest) { # Laid out and drawn like the real window, but never on screen. $form.Opacity = 0 $form.ShowInTaskbar = $false } # An error in an event - the hotkey, a timer, a button - is written down and # the companion goes on (0.12.2). Without this .NET asks whether to quit, over # a game in full screen - on 03.10.2026 the companion was gone after such an # error. The self-test stops at it: its picture would be wrong. [System.Windows.Forms.Application]::add_ThreadException({ param($s, $ev) Write-CompanionError $ev.Exception 'window' if ($SelfTest) { Write-Host ('Self-test: error in the window: ' + $ev.Exception.Message) $script:Exiting = $true $form.Close() return } $lblStatus.Text = 'Error: ' + $ev.Exception.Message + ' (errors.log in the data folder)' }) try { [System.Windows.Forms.Application]::Run($form) } catch { # In a console the error shows there; without a window the trap at the # top writes it down and says it in a message box. Set-ConsoleVisible $true throw } finally { $timer.Stop() $overlayTimer.Stop() $gdTimer.Stop() # A game data run does not outlive the window: nobody would see its end. if ($script:GameDataState.Run) { $script:GameDataState.Run.Stop(); $script:GameDataState.Run.Dispose() } if ($script:OverlayState.Hotkey) { $script:OverlayState.Hotkey.Dispose() } $overlay.Dispose() $tray.Visible = $false $tray.Dispose() $script:Mutex.ReleaseMutex() $script:ShowEvent.Dispose() } #vf-signature:ojGK+jX+pWdkiVDg4RKGK0h6a8N+pUoLnE6D2r5zcGjOdPx2sKN9W5LyuN3vnGwe56Vyav3kwsne8LscHBDQObcOLBhEcMuKvm61ZFCxOSaX/Ej0MvdenxKiv6cBRgjj2JdLtqE/ytBJ482TBDcXIXM1s7I12imQEyULB++X2d/kLguNTMxpAQCF1OXzNrKAz2cfOULIW680Yu85SE7Qw0XHsbaDqzHWAgu6ME/BY5EV+SYhHloMCpZVJghsdjF36qRIDsSwyHOGWuxEz8q6S4VFzamVrS0sA4FWe8JMbUg1DjWwzwEZ+fGDB9tMTuyc2iNMxDRE6CeEpthAduhxcAfVdJgMoEh/IGF3fatQHwlIxVUOhMN/95gmYKvcvqfnBditToL7Qe/Bf/PvQ+d1A+ipGb8ZBaApUOmGjoEMhJS0P1zNc7torXwlAqNvhvUG67OgjuU4AMMj9rTeIJuui7e1McTXoI+yO5LVtv8lgx73x3tGSFvsgr0H2oM7NF66