Skip to content

Instantly share code, notes, and snippets.

@zett42
Created May 8, 2026 10:04
Show Gist options
  • Select an option

  • Save zett42/b2aa577b74399a4e0cc038b58725c0bd to your computer and use it in GitHub Desktop.

Select an option

Save zett42/b2aa577b74399a4e0cc038b58725c0bd to your computer and use it in GitHub Desktop.
<#!
.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