Skip to content

Instantly share code, notes, and snippets.

@zett42
Last active May 5, 2026 17:25
Show Gist options
  • Select an option

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

Select an option

Save zett42/52c04b4315d37d00b0a62456166a882d to your computer and use it in GitHub Desktop.
<#!
.SYNOPSIS
Patches a SCORE.BO file for the DOS PC game Bolo by Dongleware to unlock training mode.
.DESCRIPTION
Updates one SCORE.BO record with a caller-supplied name and score, then recomputes the 16-bit checksum expected by the DOS executable.
To unlock training mode, set the score to >= 50000.
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 ScoreFile
Path to the SCORE.BO file to patch.
Defaults to this sub path relative to the script location, which assumes the script is put into the root of a DOSBox installation of Bolo:
"\DATA\HD\DOS\PREF\BOLO\SCORE.BO"
.PARAMETER Name
ASCII player name to write into the selected score record. The name must fit in 17 characters.
.PARAMETER Score
Score value to encode as packed BCD in the selected record.
.PARAMETER RecordIndex
Zero-based score record index to overwrite. Valid values are 0 through 5.
.EXAMPLE
.\Unlock-BoloTraining.ps1 -ScoreFile .\SCORE.BO -Name FREEPLAY -Score 50000 -RecordIndex 0
#>
param(
[Parameter()]
[string] $ScoreFile = (Join-Path $PSScriptRoot 'DATA\HD\DOS\PREF\BOLO\SCORE.BO'),
[string] $Name = 'FREEPLAY',
[int] $Score = 50000,
[int] $RecordIndex = 0
)
Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'
<# ---------------------------------------------------------------------------------------------------
.SYNOPSIS
Runs the script workflow.
.DESCRIPTION
Validates the caller input, loads SCORE.BO, updates one score record, recalculates the
checksum, writes the patched file back to disk, and returns a summary object.
#>
function Start-Main {
# Validate the caller input before touching the score file.
Assert-ValidInputs -ScoreFile $ScoreFile -Name $Name -Score $Score -RecordIndex $RecordIndex
$bytes = Read-ScoreFileBytes -ScoreFile $ScoreFile
Set-ScoreRecord -Data $bytes -Name $Name -Score $Score -RecordIndex $RecordIndex
$checksum = Set-ScoreChecksum -Data $bytes
$backupPath = Backup-ScoreFile -ScoreFile $ScoreFile
[System.IO.File]::WriteAllBytes($ScoreFile, $bytes)
[pscustomobject]@{
ScoreFile = $ScoreFile
BackupFile = $backupPath
Name = $Name
Score = $Score
RecordIndex = $RecordIndex
Checksum = ('0x{0:X4}' -f $checksum)
}
}
<# ---------------------------------------------------------------------------------------------------
.SYNOPSIS
Validates caller input for the patch operation.
.DESCRIPTION
Checks the score file path, record index, score range, and encoded name length before the
script reads or mutates SCORE.BO.
.PARAMETER ScoreFile
Path to the SCORE.BO file.
.PARAMETER Name
ASCII player name to write into the score record.
.PARAMETER Score
Score value to encode into the score record.
.PARAMETER RecordIndex
Zero-based score record index to overwrite.
#>
function Assert-ValidInputs {
param(
[string] $ScoreFile,
[string] $Name,
[int] $Score,
[int] $RecordIndex
)
# SCORE.BO always contains exactly six fixed-size records.
if ($RecordIndex -lt 0 -or $RecordIndex -gt 5) {
throw 'RecordIndex must be between 0 and 5.'
}
if ($Score -lt 0 -or $Score -gt 99999999) {
throw 'Score must be between 0 and 99999999.'
}
if (-not (Test-Path -LiteralPath $ScoreFile)) {
throw "Score file not found: $ScoreFile"
}
$nameBytes = [Text.Encoding]::ASCII.GetBytes($Name)
if ($nameBytes.Length -gt 17) {
throw 'Name must be at most 17 ASCII characters.'
}
}
<# ---------------------------------------------------------------------------------------------------
.SYNOPSIS
Reads SCORE.BO from disk.
.DESCRIPTION
Loads the file as raw bytes and verifies that it matches the expected six-record SCORE.BO
layout of 134 bytes.
.PARAMETER ScoreFile
Path to the SCORE.BO file.
#>
function Read-ScoreFileBytes {
param([string] $ScoreFile)
# SCORE.BO is always six 22-byte records plus a 2-byte checksum.
$bytes = [System.IO.File]::ReadAllBytes($ScoreFile)
if ($bytes.Length -ne 134) {
throw "Unexpected SCORE.BO size: $($bytes.Length) bytes"
}
# Use comma-operator to avoid unrolling the byte array into individual bytes in the caller's scope.
, $bytes
}
<# ---------------------------------------------------------------------------------------------------
.SYNOPSIS
Creates a backup copy of SCORE.BO before patching.
.DESCRIPTION
Copies the original score file to a sibling backup path using the first available name in
the sequence .bak, .bak1, .bak2, and so on.
.PARAMETER ScoreFile
Path to the SCORE.BO file that will be backed up.
#>
function Backup-ScoreFile {
param([string] $ScoreFile)
$index = 0
while ($true) {
$suffix = if ($index -eq 0) { '.bak' } else { ".bak$index" }
$backupPath = "$ScoreFile$suffix"
if (-not (Test-Path -LiteralPath $backupPath)) {
Copy-Item -LiteralPath $ScoreFile -Destination $backupPath
return $backupPath
}
$index++
}
}
<# ---------------------------------------------------------------------------------------------------
.SYNOPSIS
Writes one score record into the in-memory SCORE.BO buffer.
.DESCRIPTION
Replaces the selected record's name and packed-BCD score fields while preserving the rest
of the file contents for later checksum recalculation.
.PARAMETER Data
Mutable SCORE.BO byte buffer.
.PARAMETER Name
ASCII player name to write into the selected record.
.PARAMETER Score
Score value to encode as packed BCD.
.PARAMETER RecordIndex
Zero-based record index to overwrite.
#>
function Set-ScoreRecord {
param(
[byte[]] $Data,
[string] $Name,
[int] $Score,
[int] $RecordIndex
)
$offset = $RecordIndex * 22
$nameBytes = [Text.Encoding]::ASCII.GetBytes($Name)
$scoreBytes = ConvertTo-BoloBcdBytes -Value $Score
# Reset the whole 18-byte name field to dashes before writing the new name.
for ($i = 0; $i -lt 18; $i++) {
$Data[$offset + $i] = 0x2D
}
# Write the name bytes into the record, leaving the remaining bytes as dashes if the name is shorter than 18 characters.
for ($i = 0; $i -lt $nameBytes.Length; $i++) {
$Data[$offset + $i] = $nameBytes[$i]
}
# The in-game name parser stops at the first NUL byte.
$Data[$offset + $nameBytes.Length] = 0x00
for ($i = 0; $i -lt 4; $i++) {
$Data[$offset + 18 + $i] = $scoreBytes[$i]
}
}
<# ---------------------------------------------------------------------------------------------------
.SYNOPSIS
Encodes a score as Bolo packed BCD bytes.
.DESCRIPTION
Converts an integer score into the four-byte packed-BCD representation used by SCORE.BO.
.PARAMETER Value
Score value to encode.
#>
function ConvertTo-BoloBcdBytes {
param([int] $Value)
# Scores are stored as four packed-BCD bytes in little-endian pair order.
$digits = $Value.ToString('D8')
for ($i = 6; $i -ge 0; $i -= 2) {
$lowDigit = [int] $digits[$i] - 48
$highDigit = [int] $digits[$i + 1] - 48
$packedByte = $lowDigit -bor (($highDigit -band 0x0F) -shl 4)
[byte] $packedByte
}
}
<# ---------------------------------------------------------------------------------------------------
.SYNOPSIS
Recomputes and writes the SCORE.BO checksum.
.DESCRIPTION
Calculates the 16-bit checksum used by the game and stores it in the trailing checksum field.
.PARAMETER Data
Mutable SCORE.BO byte buffer.
#>
function Set-ScoreChecksum {
param([byte[]] $Data)
$checksum = Get-BoloChecksum -Data $Data
[BitConverter]::GetBytes($checksum).CopyTo($Data, 132)
$checksum
}
<# ---------------------------------------------------------------------------------------------------
.SYNOPSIS
Calculates the SCORE.BO checksum.
.DESCRIPTION
Implements the checksum routine reverse-engineered from the DOS executable: start from the
seed value 0x007B, add each record's visible name bytes until the first NUL, then add the
two 16-bit score words for all six records, folding the result to 16 bits.
.PARAMETER Data
SCORE.BO byte buffer to checksum.
#>
function Get-BoloChecksum {
param([byte[]] $Data)
# The DOS executable seeds the checksum with 0x007B and then folds each
# record's visible name bytes plus its two score words into a 16-bit sum.
$sum = 0x7B
for ($record = 0; $record -lt 6; $record++) {
$offset = $record * 22
for ($i = 0; $i -lt 18; $i++) {
$value = $Data[$offset + $i]
if ($value -eq 0) {
break
}
$sum = ($sum + $value) -band 0xFFFF
}
$scoreLow = [BitConverter]::ToUInt16($Data, $offset + 18)
$scoreHigh = [BitConverter]::ToUInt16($Data, $offset + 20)
$sum = ($sum + $scoreLow + $scoreHigh) -band 0xFFFF
}
[uint16]$sum
}
# Invoke the main entry point and return its result to the caller.
Start-Main
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment