Created
May 8, 2026 10:04
-
-
Save zett42/b2aa577b74399a4e0cc038b58725c0bd to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <#! | |
| .SYNOPSIS | |
| Decrypts original level files for the DOS PC game Bolo by Dongleware. | |
| .DESCRIPTION | |
| Writes each decrypted level beside the input as <name>.decrypted.txt. | |
| By default, processes LEVEL* files with no extension in this script's folder. | |
| Decoded bytes are interpreted as DOS code page 850 text and written as UTF-8 without BOM. | |
| DISCLAIMER: This script is an unofficial community utility and is not provided, endorsed, or supported by Dongleware. | |
| It is provided for educational purposes only. | |
| .PARAMETER Folder | |
| Folder containing the level files to decrypt. | |
| Defaults to the script location. | |
| .PARAMETER Pattern | |
| Filename pattern used to select level files. | |
| Defaults to "LEVEL*.", which means LEVEL* files with no extension. | |
| .EXAMPLE | |
| .\ConvertFrom-EncryptedBoloLevel.ps1 | |
| .EXAMPLE | |
| .\ConvertFrom-EncryptedBoloLevel.ps1 -Folder .\levels -Pattern "LEVEL*." | |
| #> | |
| #Requires -Version 7.0 | |
| param( | |
| [string] $Folder = $PSScriptRoot, | |
| [string] $Pattern = 'LEVEL*.' | |
| ) | |
| Set-StrictMode -Version 3.0 | |
| $ErrorActionPreference = 'Stop' | |
| # ----------------------------------------------------------------------------- | |
| function Invoke-Main { | |
| <# | |
| .SYNOPSIS | |
| Runs level decryption for the selected input files. | |
| .DESCRIPTION | |
| Builds the decoder context, resolves the target files, and writes one | |
| decrypted text output file for each matching input file. | |
| #> | |
| [System.Text.Encoding]::RegisterProvider([System.Text.CodePagesEncodingProvider]::Instance) | |
| $keyText = 'DYFAHTRGHJHZKWLPEIWEQDSFWGPUWIOIOFRJGGIQWHSERHJUUITMFBOLPGDNBSKAXLAVMNFASGTDHJ' | |
| $keyBytes = [System.Text.Encoding]::ASCII.GetBytes($keyText) | |
| $context = @{ | |
| KeyBytes = $keyBytes | |
| LegacyTextEncoding = [System.Text.Encoding]::GetEncoding(850) | |
| OutputTextEncoding = [System.Text.UTF8Encoding]::new($false) | |
| } | |
| $inputFiles = Get-BoloInputFiles $Folder $Pattern | |
| foreach ($inputFile in $inputFiles) { | |
| Convert-BoloLevelFile $inputFile $context | |
| } | |
| } | |
| # ----------------------------------------------------------------------------- | |
| function Get-BoloInputFiles { | |
| <# | |
| .SYNOPSIS | |
| Finds level files selected by folder and filename pattern. | |
| .DESCRIPTION | |
| Resolves the requested folder and applies the filename filter. | |
| .PARAMETER Folder | |
| Folder containing the level files. | |
| .PARAMETER Pattern | |
| Filename pattern. The default LEVEL*. means LEVEL* files with no extension. | |
| #> | |
| param( | |
| [string] $Folder, | |
| [string] $Pattern | |
| ) | |
| $resolvedFolder = (Resolve-Path -LiteralPath $Folder).Path | |
| $inputFiles = Get-ChildItem -LiteralPath $resolvedFolder -File -Filter $Pattern | Sort-Object Name | |
| return $inputFiles | |
| } | |
| # ----------------------------------------------------------------------------- | |
| function Convert-BoloLevelFile { | |
| <# | |
| .SYNOPSIS | |
| Decrypts one level file and writes its .decrypted.txt output. | |
| .DESCRIPTION | |
| Reads the input as bytes, decodes each line with the Bolo line key, and | |
| writes UTF-8 text. | |
| .PARAMETER InputFile | |
| FileInfo for the source level file. | |
| .PARAMETER Context | |
| Shared decoder data and text encodings. | |
| #> | |
| param( | |
| [System.IO.FileInfo] $InputFile, | |
| [hashtable] $Context | |
| ) | |
| $inputBytes = [System.IO.File]::ReadAllBytes($InputFile.FullName) | |
| $lines = Split-ByteLines $inputBytes | |
| $decodedLines = [System.Collections.Generic.List[byte[]]]::new() | |
| foreach ($line in $lines) { | |
| $decodedLines.Add((ConvertFrom-BoloLineBytes $line $Context.KeyBytes)) | |
| } | |
| $outputBytes = Join-ByteLines $decodedLines | |
| $outputPath = Join-Path $InputFile.DirectoryName ($InputFile.Name + '.decrypted.txt') | |
| $outputText = $Context.LegacyTextEncoding.GetString($outputBytes) | |
| [System.IO.File]::WriteAllText($outputPath, $outputText, $Context.OutputTextEncoding) | |
| Write-Host "Wrote $outputPath" | |
| } | |
| # ----------------------------------------------------------------------------- | |
| function ConvertFrom-BoloLineBytes { | |
| <# | |
| .SYNOPSIS | |
| Decodes one Bolo level line. | |
| .DESCRIPTION | |
| Removes the two leading padding bytes and subtracts the Bolo key from each | |
| remaining byte, wrapping the key for long lines. | |
| .PARAMETER LineBytes | |
| Line bytes to decode. | |
| .PARAMETER KeyBytes | |
| ASCII bytes for the Bolo key. | |
| #> | |
| param( | |
| [byte[]] $LineBytes, | |
| [byte[]] $KeyBytes | |
| ) | |
| if ($LineBytes.Count -le 2) { | |
| return ,[byte[]]::new(0) | |
| } | |
| $result = [byte[]]::new($LineBytes.Count - 2) | |
| $keyIndex = 2 | |
| for ($index = 0; $index -lt $result.Count; $index += 1) { | |
| $result[$index] = [byte](($LineBytes[$index + 2] - $KeyBytes[$keyIndex]) -band 0xFF) | |
| $keyIndex += 1 | |
| if ($keyIndex -ge $KeyBytes.Count) { | |
| $keyIndex = 0 | |
| } | |
| } | |
| return ,$result | |
| } | |
| # ----------------------------------------------------------------------------- | |
| function Split-ByteLines { | |
| <# | |
| .SYNOPSIS | |
| Splits file bytes into line byte arrays. | |
| .DESCRIPTION | |
| Recognizes CRLF, CR, and LF line endings while excluding the newline bytes | |
| from the returned line arrays. | |
| .PARAMETER Bytes | |
| Raw file bytes. | |
| #> | |
| param([byte[]] $Bytes) | |
| $lines = [System.Collections.Generic.List[byte[]]]::new() | |
| $lineStart = 0 | |
| $index = 0 | |
| while ($index -lt $Bytes.Count) { | |
| if (($Bytes[$index] -ne 10) -and ($Bytes[$index] -ne 13)) { | |
| $index += 1 | |
| continue | |
| } | |
| $lineLength = $index - $lineStart | |
| $line = [byte[]]::new($lineLength) | |
| if ($lineLength -gt 0) { | |
| [Array]::Copy($Bytes, $lineStart, $line, 0, $lineLength) | |
| } | |
| $lines.Add($line) | |
| if (($Bytes[$index] -eq 13) -and (($index + 1) -lt $Bytes.Count) -and ($Bytes[$index + 1] -eq 10)) { | |
| $index += 2 | |
| } | |
| else { | |
| $index += 1 | |
| } | |
| $lineStart = $index | |
| } | |
| if ($lineStart -lt $Bytes.Count) { | |
| $lineLength = $Bytes.Count - $lineStart | |
| $line = [byte[]]::new($lineLength) | |
| [Array]::Copy($Bytes, $lineStart, $line, 0, $lineLength) | |
| $lines.Add($line) | |
| } | |
| return ,$lines | |
| } | |
| # ----------------------------------------------------------------------------- | |
| function Join-ByteLines { | |
| <# | |
| .SYNOPSIS | |
| Joins line byte arrays with CRLF separators. | |
| .DESCRIPTION | |
| Recreates the decrypted level text byte stream using Windows CRLF line | |
| endings to match the expected fixture format. | |
| .PARAMETER Lines | |
| Line byte arrays to join. | |
| #> | |
| param([System.Collections.Generic.List[byte[]]] $Lines) | |
| $output = [System.Collections.Generic.List[byte]]::new() | |
| for ($lineIndex = 0; $lineIndex -lt $Lines.Count; $lineIndex += 1) { | |
| if ($lineIndex -gt 0) { | |
| $output.Add(13) | |
| $output.Add(10) | |
| } | |
| $output.AddRange($Lines[$lineIndex]) | |
| } | |
| return ,$output.ToArray() | |
| } | |
| # ----------------------------------------------------------------------------- | |
| Invoke-Main |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment