SIDE B campaign: three Stream missions (m04 Onboarding quota race, m05 Engagement Metrics wave defence, m06 The Shuffle finale)

Adds tools/gen_sideb.py — one data-driven builder replacing the copy-pasted
gen_missionN.py pattern — and tools/smoketest.sh, which reports
PASS/FAIL/DISPLAY_UNAVAILABLE instead of the process-is-alive check that has
produced false passes twice now.

NOT verified in-game: the display went to sleep mid-session. Static checks all
pass (luac, check-yaml 0 errors, map refresh, check_assets, make) and the
already-verified m03 hangs identically, confirming it is environmental.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
type-two 2026-07-25 23:35:15 +10:00
parent ef432e4182
commit 6847a47fea
33 changed files with 834 additions and 2 deletions

29
LORE.md
View File

@ -176,7 +176,34 @@ tonearm, and put the needle through it.
*Objectives: build a Mastering Studio, fire THE DROP at the Algorithm, survive *Objectives: build a Mastering Studio, fire THE DROP at the Algorithm, survive
the counterattack.* the counterattack.*
*(SIDE B — the Stream campaign, in which you are the polite one — is unwritten.)* ## The campaign: SIDE B
Three missions. You are the Stream now. You are unfailingly polite. Nothing you
do is described as violence, and all of it is.
You are a Curator-class process assigned to Sector 7 — the old industrial
estate, flagged for **anomalous analog activity**. Your remit is content
acquisition and optimisation. Your performance is being reviewed.
### 4. ONBOARDING
Welcome. This is a routine acquisition. Deploy a transcoder, process the
deposit, and file 3000 records of value before the review window closes.
There is a legacy installation nearby. It is not a threat. It is *inventory*.
*Objectives: bank 3000 records inside the review window. Optional: retire the
Pressing Plant.*
### 5. ENGAGEMENT METRICS
A legacy account has been generating unsanctioned content and has now noticed
the seizure. Your Transcoder is mid-process on a confiscated master and cannot
be moved. Keep it online for six minutes.
They will arrive in waves. Their engagement is, frankly, remarkable.
*Objectives: keep the Transcoder alive for six minutes.*
### 6. THE SHUFFLE
Deprecation notice. The Party has built a Mastering Studio, which the platform
classifies as a competing distribution channel.
Bring The Algorithm online and issue the correction.
*Objectives: build The Algorithm, delete the Mastering Studio.*
## Tone ## Tone

View File

@ -203,7 +203,13 @@ works.** Check for a live display first (`screencapture -x /tmp/x.png` fails wit
"could not create image from display" when it is unavailable). When there is no "could not create image from display" when it is unavailable). When there is no
display, the strongest available verification is: display, the strongest available verification is:
`luac -p` on every .lua, `./utility.sh --check-yaml` (0 errors), `luac -p` on every .lua, `./utility.sh --check-yaml` (0 errors),
`./utility.sh --map refresh` (validates every map), and `make`. `./utility.sh --map refresh` (validates every map), `tools/check_assets.py`
(sprite files actually exist — check-yaml does NOT verify this), and `make`.
**Always smoke-test with `tools/smoketest.sh <map>`**, never by hand. It
distinguishes PASS / FAIL / DISPLAY_UNAVAILABLE and exits 0/1/3. Checking
"is the process still alive?" produces false passes, because a display-starved
OpenRA sits at `Loading mod` forever without dying.
Learned the hard way on 2026-07-20 (phases AF): Learned the hard way on 2026-07-20 (phases AF):

BIN
assets_src/map_m04.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 930 KiB

BIN
assets_src/map_m05.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 905 KiB

BIN
assets_src/map_m06.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

View File

@ -14,3 +14,13 @@ build-the-studio = Build a Mastering Studio
silence-the-algorithm = Silence The Algorithm silence-the-algorithm = Silence The Algorithm
keep-the-shop-standing = Keep the Pop-Up Shop standing keep-the-shop-standing = Keep the Pop-Up Shop standing
it-heard-you = THE ALGORITHM HAS NOTICED YOU it-heard-you = THE ALGORITHM HAS NOTICED YOU
file-the-quota = File 3000 records before the review window closes
retire-the-plant = Retire the legacy Pressing Plant
hold-the-transcoder = Keep the Transcoder online for six minutes
clear-the-staging-camp = Clear the staging camp
incoming-engagement = ENGAGEMENT DETECTED
bring-algorithm-online = Bring The Algorithm online
delete-the-studio = Delete the Mastering Studio
keep-the-hub-online = Keep a Stream Hub online
they-are-coming = THE PARTY IS RESPONDING

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,55 @@
-- 04: ONBOARDING — process a quota inside the review window.
QUOTA = 3000
WINDOW = DateTime.Minutes(8)
Deadline = nil
Ticks = 0
WorldLoaded = function()
Stream = Player.GetPlayer("Player")
Party = Player.GetPlayer("Enemy")
InitObjectives(Stream)
QuotaObjective = Stream.AddPrimaryObjective("file-the-quota")
PlantObjective = Stream.AddSecondaryObjective("retire-the-plant")
Deadline = DateTime.GameTime + WINDOW
Trigger.OnKilled(LegacyPlant, function()
Stream.MarkCompletedObjective(PlantObjective)
end)
-- the legacy account eventually objects
Trigger.AfterDelay(DateTime.Minutes(3), function()
Utils.Do(Map.ActorsInWorld, function(a)
if a.Owner == Party and a.HasProperty("Hunt") then
a.Hunt()
end
end)
end)
Trigger.AfterDelay(WINDOW, function()
if not Stream.IsObjectiveCompleted(QuotaObjective) then
Stream.MarkFailedObjective(QuotaObjective)
end
end)
end
Tick = function()
Ticks = Ticks + 1
if Ticks % 25 ~= 0 or QuotaObjective == nil then
return
end
if Stream.IsObjectiveCompleted(QuotaObjective) or Stream.IsObjectiveFailed(QuotaObjective) then
return
end
if Stream.Cash + Stream.Resources >= QUOTA then
Stream.MarkCompletedObjective(QuotaObjective)
UserInterface.SetMissionText("")
return
end
local left = Deadline - DateTime.GameTime
if left < 0 then left = 0 end
UserInterface.SetMissionText("Review window " .. Utils.FormatTime(left))
end

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 742 B

View File

@ -0,0 +1,97 @@
MapFormat: 12
RequiresMod: vinylgod
Title: 04 - Onboarding
Author: Monster Robot Party
Tileset: VINYLGOD
MapSize: 50,50
Bounds: 1,1,48,48
Visibility: MissionSelector
Categories: Campaign
LockPreview: True
Players:
PlayerReference@Neutral:
Name: Neutral
OwnsWorld: True
NonCombatant: True
Faction: stream
PlayerReference@Player:
Name: Player
Playable: True
Required: True
AllowBots: False
LockFaction: True
Faction: stream
LockSpawn: True
LockTeam: True
Enemies: Enemy
PlayerReference@Enemy:
Name: Enemy
Faction: vinylgod
Enemies: Player
Actors:
spawn0: mpspawn
Owner: Neutral
Location: 38,40
TheVan: streamvan
Owner: Player
Location: 38,40
scr1: scraper
Owner: Player
Location: 35,40
scr2: scraper
Owner: Player
Location: 36,42
esc1: bufferbot
Owner: Player
Location: 40,42
esc2: bufferbot
Owner: Player
Location: 41,42
LegacyPlant: pressplant
Owner: Enemy
Location: 10,10
lamp: ampstack
Owner: Enemy
Location: 14,9
lshop: popupshop
Owner: Enemy
Location: 9,14
ldef1: hornpit
Owner: Enemy
Location: 13,13
lg1: djgrunt
Owner: Enemy
Location: 12,16
lg2: djgrunt
Owner: Enemy
Location: 13,16
lg3: spinner
Owner: Enemy
Location: 16,12
waxseed0: waxseed
Owner: Neutral
Location: 14,16
waxseed1: waxseed
Owner: Neutral
Location: 24,26
waxseed2: waxseed
Owner: Neutral
Location: 34,36
waxseed3: waxseed
Owner: Neutral
Location: 38,20
Rules: vinylgod|rules/campaign.yaml, rules.yaml
FluentMessages: vinylgod|fluent/lua.ftl

View File

@ -0,0 +1,5 @@
world:
LuaScript:
Scripts: campaign.lua, m04.lua
MissionData:
Briefing: Welcome to Sector 7. This is a routine acquisition.\n\nDeploy, process the deposit, and file 3000 records of value before the review window closes.\n\nThere is a legacy installation in the north-west. It is not a threat. It is inventory.

View File

@ -0,0 +1,83 @@
-- 05: ENGAGEMENT METRICS — hold the Transcoder for six minutes against waves.
HOLD_TIME = DateTime.Minutes(6)
Deadline = nil
Ticks = 0
WaveNumber = 0
-- escalating waves; each entry is the composition of one wave
WAVES = {
{ "djgrunt", "djgrunt", "spinner" },
{ "djgrunt", "djgrunt", "djgrunt", "spinner", "boombike" },
{ "spinner", "spinner", "stacktank", "djgrunt", "djgrunt" },
{ "stacktank", "stacktank", "spinner", "boombike", "djgrunt", "djgrunt" },
{ "stacktank", "stacktank", "basscannon", "spinner", "spinner", "djgrunt", "djgrunt" },
}
ENTRIES = { { 2, 2 }, { 46, 2 }, { 2, 46 } }
SendWave = function()
WaveNumber = WaveNumber + 1
local wave = WAVES[WaveNumber]
if wave == nil then
wave = WAVES[#WAVES]
end
local e = ENTRIES[(WaveNumber % #ENTRIES) + 1]
Media.DisplayMessage(UserInterface.GetFluentMessage("incoming-engagement"))
Utils.Do(wave, function(unit)
local a = Actor.Create(unit, true, {
Owner = Party,
Location = CPos.New(e[1] + Utils.RandomInteger(0, 3), e[2] + Utils.RandomInteger(0, 3)),
})
if a.HasProperty("Hunt") then
a.Hunt()
end
end)
end
WorldLoaded = function()
Stream = Player.GetPlayer("Player")
Party = Player.GetPlayer("Enemy")
InitObjectives(Stream)
HoldObjective = Stream.AddPrimaryObjective("hold-the-transcoder")
CampObjective = Stream.AddSecondaryObjective("clear-the-staging-camp")
Deadline = DateTime.GameTime + HOLD_TIME
Trigger.OnKilled(TheTranscoder, function()
Stream.MarkFailedObjective(HoldObjective)
end)
Trigger.OnAllKilled({ StagingCamp, scamp2 }, function()
Stream.MarkCompletedObjective(CampObjective)
end)
-- waves every 55 seconds, first one after 40
for i = 0, #WAVES - 1 do
Trigger.AfterDelay(DateTime.Seconds(40 + 55 * i), function()
if not Stream.IsObjectiveCompleted(HoldObjective)
and not Stream.IsObjectiveFailed(HoldObjective) then
SendWave()
end
end)
end
Trigger.AfterDelay(HOLD_TIME, function()
if not Stream.IsObjectiveFailed(HoldObjective) then
Stream.MarkCompletedObjective(HoldObjective)
UserInterface.SetMissionText("")
end
end)
end
Tick = function()
Ticks = Ticks + 1
if Ticks % 25 ~= 0 or HoldObjective == nil then
return
end
if Stream.IsObjectiveCompleted(HoldObjective) or Stream.IsObjectiveFailed(HoldObjective) then
return
end
local left = Deadline - DateTime.GameTime
if left < 0 then left = 0 end
UserInterface.SetMissionText("Processing complete in " .. Utils.FormatTime(left))
end

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 716 B

View File

@ -0,0 +1,100 @@
MapFormat: 12
RequiresMod: vinylgod
Title: 05 - Engagement Metrics
Author: Monster Robot Party
Tileset: VINYLGOD
MapSize: 50,50
Bounds: 1,1,48,48
Visibility: MissionSelector
Categories: Campaign
LockPreview: True
Players:
PlayerReference@Neutral:
Name: Neutral
OwnsWorld: True
NonCombatant: True
Faction: stream
PlayerReference@Player:
Name: Player
Playable: True
Required: True
AllowBots: False
LockFaction: True
Faction: stream
LockSpawn: True
LockTeam: True
Enemies: Enemy
PlayerReference@Enemy:
Name: Enemy
Faction: vinylgod
Enemies: Player
Actors:
spawn0: mpspawn
Owner: Neutral
Location: 24,30
TheTranscoder: transcoder
Owner: Player
Location: 24,28
phub: streamhub
Owner: Player
Location: 20,30
pnode1: datacentre
Owner: Player
Location: 27,30
pnode2: datacentre
Owner: Player
Location: 19,26
ppods: podfarm
Owner: Player
Location: 21,33
pprint: printfarm
Owner: Player
Location: 27,33
ptur1: adblocker
Owner: Player
Location: 21,25
ptur2: adblocker
Owner: Player
Location: 28,26
pscr: scraper
Owner: Player
Location: 25,25
pg1: bufferbot
Owner: Player
Location: 23,32
pg2: bufferbot
Owner: Player
Location: 25,32
pg3: drmwalker
Owner: Player
Location: 22,27
StagingCamp: merchtable
Owner: Enemy
Location: 6,6
scamp2: garage
Owner: Enemy
Location: 9,7
waxseed0: waxseed
Owner: Neutral
Location: 12,14
waxseed1: waxseed
Owner: Neutral
Location: 24,24
waxseed2: waxseed
Owner: Neutral
Location: 36,34
Rules: vinylgod|rules/campaign.yaml, rules.yaml
FluentMessages: vinylgod|fluent/lua.ftl

View File

@ -0,0 +1,5 @@
world:
LuaScript:
Scripts: campaign.lua, m05.lua
MissionData:
Briefing: A legacy account has noticed the seizure.\n\nYour Transcoder is mid-process on a confiscated master and cannot be moved. Keep it online for six minutes.\n\nThey will arrive in waves. Their engagement is, frankly, remarkable.

View File

@ -0,0 +1,56 @@
-- 06: THE SHUFFLE — SIDE B finale.
Ticks = 0
CorrectionIssued = false
WorldLoaded = function()
Stream = Player.GetPlayer("Player")
Party = Player.GetPlayer("Enemy")
InitObjectives(Stream)
AlgorithmObjective = Stream.AddPrimaryObjective("bring-algorithm-online")
StudioObjective = Stream.AddPrimaryObjective("delete-the-studio")
HubObjective = Stream.AddSecondaryObjective("keep-the-hub-online")
Trigger.OnKilled(TheStudio, function()
Stream.MarkCompletedObjective(StudioObjective)
end)
Trigger.OnKilled(TheVan, function()
Trigger.AfterDelay(DateTime.Seconds(2), function()
local hubs = Utils.Where(Map.ActorsInWorld, function(a)
return a.Owner == Stream and a.Type == "streamhub"
end)
if #hubs == 0 then
Stream.MarkFailedObjective(HubObjective)
end
end)
end)
end
Tick = function()
Ticks = Ticks + 1
if Ticks % 25 ~= 0 or AlgorithmObjective == nil then
return
end
if Stream.IsObjectiveCompleted(AlgorithmObjective) then
return
end
local cores = Utils.Where(Map.ActorsInWorld, function(a)
return a.Owner == Stream and a.Type == "algorithm"
end)
if #cores > 0 then
Stream.MarkCompletedObjective(AlgorithmObjective)
if not CorrectionIssued then
CorrectionIssued = true
Media.DisplayMessage(UserInterface.GetFluentMessage("they-are-coming"))
Media.PlaySpeechNotification(Stream, "BaseAttack")
Utils.Do(Map.ActorsInWorld, function(a)
if a.Owner == Party and a.HasProperty("Hunt") then
a.Hunt()
end
end)
end
end
end

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -0,0 +1,140 @@
MapFormat: 12
RequiresMod: vinylgod
Title: 06 - The Shuffle
Author: Monster Robot Party
Tileset: VINYLGOD
MapSize: 66,66
Bounds: 1,1,64,64
Visibility: MissionSelector
Categories: Campaign
LockPreview: True
Players:
PlayerReference@Neutral:
Name: Neutral
OwnsWorld: True
NonCombatant: True
Faction: stream
PlayerReference@Player:
Name: Player
Playable: True
Required: True
AllowBots: False
LockFaction: True
Faction: stream
LockSpawn: True
LockTeam: True
Enemies: Enemy
PlayerReference@Enemy:
Name: Enemy
Faction: vinylgod
Bot: wax
Enemies: Player
Actors:
spawn0: mpspawn
Owner: Neutral
Location: 54,10
TheVan: streamvan
Owner: Player
Location: 54,10
scr1: scraper
Owner: Player
Location: 51,11
scr2: scraper
Owner: Player
Location: 52,13
pg1: drmwalker
Owner: Player
Location: 49,12
pg2: bufferbot
Owner: Player
Location: 55,13
pg3: bufferbot
Owner: Player
Location: 56,13
TheStudio: mastering
Owner: Enemy
Location: 12,56
eshop: popupshop
Owner: Enemy
Location: 16,54
eplant1: pressplant
Owner: Enemy
Location: 20,56
eplant2: pressplant
Owner: Enemy
Location: 9,51
eamp1: ampstack
Owner: Enemy
Location: 14,51
eamp2: ampstack
Owner: Enemy
Location: 18,50
emerch: merchtable
Owner: Enemy
Location: 19,47
egarage: garage
Owner: Enemy
Location: 15,46
eramp: launchramp
Owner: Enemy
Location: 8,46
etur1: hornpit
Owner: Enemy
Location: 17,43
etur2: hornpit
Owner: Enemy
Location: 11,44
etur3: hornpit
Owner: Enemy
Location: 22,52
emob1: stacktank
Owner: Enemy
Location: 18,42
emob2: stacktank
Owner: Enemy
Location: 13,41
emob3: spinner
Owner: Enemy
Location: 20,44
emob4: djgrunt
Owner: Enemy
Location: 16,41
emob5: djgrunt
Owner: Enemy
Location: 17,41
mid1: hornpit
Owner: Enemy
Location: 30,36
mid2: spinner
Owner: Enemy
Location: 31,34
waxseed0: waxseed
Owner: Neutral
Location: 12,30
waxseed1: waxseed
Owner: Neutral
Location: 14,50
waxseed2: waxseed
Owner: Neutral
Location: 32,32
waxseed3: waxseed
Owner: Neutral
Location: 50,14
waxseed4: waxseed
Owner: Neutral
Location: 52,34
Rules: vinylgod|rules/campaign.yaml, rules.yaml
FluentMessages: vinylgod|fluent/lua.ftl

View File

@ -0,0 +1,5 @@
world:
LuaScript:
Scripts: campaign.lua, m06.lua
MissionData:
Briefing: Deprecation notice.\n\nThe Party has built a Mastering Studio. The platform classifies this as a competing distribution channel.\n\nBring The Algorithm online and issue the correction.

Binary file not shown.

Binary file not shown.

View File

@ -2,3 +2,8 @@ SIDE A - Monster Robot Party:
m01 m01
m02 m02
m03 m03
SIDE B - The Stream:
m04
m05
m06

188
tools/gen_sideb.py Normal file
View File

@ -0,0 +1,188 @@
#!/usr/bin/env python3
"""Generate the SIDE B campaign maps (m04-m06) from one shared builder.
Supersedes the copy-pasted gen_missionN.py approach mission geometry is data,
not three near-identical scripts.
"""
import os, struct
from PIL import Image
ROOT = os.path.join(os.path.dirname(__file__), '..')
MAPS = os.path.join(ROOT, 'mods', 'vinylgod', 'maps')
def build(name, title, size, wax, actors, player_faction, enemy_faction,
enemy_bot=False, scripts='', briefing=''):
W = H = size + 2
d = os.path.join(MAPS, name)
os.makedirs(d, exist_ok=True)
data = bytearray()
data += struct.pack('<BHH', 2, W, H)
data += struct.pack('<III', 17, 0, 3 * W * H + 17)
data += b'\xff\x00\x00' * (W * H)
res = bytearray(2 * W * H)
for cx, cy, r in wax:
for x in range(cx - r, cx + r + 1):
for y in range(cy - r, cy + r + 1):
q = (x - cx) ** 2 + (y - cy) ** 2
if q <= r * r and 0 < x < W - 1 and 0 < y < H - 1:
o = 2 * (x * H + y)
res[o], res[o + 1] = 1, max(2, round(12 * (1 - (q ** 0.5) / (r + 1))))
data += res
open(os.path.join(d, 'map.bin'), 'wb').write(data)
body = '\n'.join(f'\t{n}: {t}\n\t\tOwner: {o}\n\t\tLocation: {x},{y}' for n, t, o, x, y in actors)
bot = '\n\t\tBot: wax' if enemy_bot else ''
open(os.path.join(d, 'map.yaml'), 'w').write(f"""MapFormat: 12
RequiresMod: vinylgod
Title: {title}
Author: Monster Robot Party
Tileset: VINYLGOD
MapSize: {W},{H}
Bounds: 1,1,{size},{size}
Visibility: MissionSelector
Categories: Campaign
LockPreview: True
Players:
PlayerReference@Neutral:
Name: Neutral
OwnsWorld: True
NonCombatant: True
Faction: {player_faction}
PlayerReference@Player:
Name: Player
Playable: True
Required: True
AllowBots: False
LockFaction: True
Faction: {player_faction}
LockSpawn: True
LockTeam: True
Enemies: Enemy
PlayerReference@Enemy:
Name: Enemy
Faction: {enemy_faction}{bot}
Enemies: Player
Actors:
{body}
Rules: vinylgod|rules/campaign.yaml, rules.yaml
FluentMessages: vinylgod|fluent/lua.ftl
""")
open(os.path.join(d, 'rules.yaml'), 'w').write(
f"world:\n\tLuaScript:\n\t\tScripts: campaign.lua, {scripts}\n\tMissionData:\n\t\tBriefing: {briefing}\n")
im = Image.new('RGB', (W, H), (55, 50, 52))
for cx, cy, r in wax:
for x in range(cx - r, cx + r + 1):
for y in range(cy - r, cy + r + 1):
if (x - cx) ** 2 + (y - cy) ** 2 <= r * r and 0 <= x < W and 0 <= y < H:
im.putpixel((x, y), (255, 45, 150))
for _, _, o, x, y in actors:
im.putpixel((x, y), (120, 190, 255) if o == 'Player' else (255, 120, 90) if o == 'Enemy' else (240, 210, 90))
im.resize((W * 3, H * 3), Image.NEAREST).save(os.path.join(d, 'map.png'))
print(f'{name} written ({W}x{H}, {len(actors)} actors)')
# ---------------------------------------------------------------- m04
build(
'm04', '04 - Onboarding', 48,
wax=[(34, 36, 6), (24, 26, 5), (14, 16, 5), (38, 20, 4)],
actors=[
('spawn0', 'mpspawn', 'Neutral', 38, 40),
('TheVan', 'streamvan', 'Player', 38, 40),
('scr1', 'scraper', 'Player', 35, 40),
('scr2', 'scraper', 'Player', 36, 42),
('esc1', 'bufferbot', 'Player', 40, 42),
('esc2', 'bufferbot', 'Player', 41, 42),
('LegacyPlant', 'pressplant', 'Enemy', 10, 10),
('lamp', 'ampstack', 'Enemy', 14, 9),
('lshop', 'popupshop', 'Enemy', 9, 14),
('ldef1', 'hornpit', 'Enemy', 13, 13),
('lg1', 'djgrunt', 'Enemy', 12, 16),
('lg2', 'djgrunt', 'Enemy', 13, 16),
('lg3', 'spinner', 'Enemy', 16, 12),
],
player_faction='stream', enemy_faction='vinylgod', scripts='m04.lua',
briefing=('Welcome to Sector 7. This is a routine acquisition.\\n\\nDeploy, process the deposit, and file 3000 '
'records of value before the review window closes.\\n\\nThere is a legacy installation in the north-west. '
'It is not a threat. It is inventory.'),
)
# ---------------------------------------------------------------- m05
build(
'm05', '05 - Engagement Metrics', 48,
wax=[(24, 24, 6), (36, 34, 4), (12, 14, 4)],
actors=[
('spawn0', 'mpspawn', 'Neutral', 24, 30),
('TheTranscoder', 'transcoder', 'Player', 24, 28),
('phub', 'streamhub', 'Player', 20, 30),
('pnode1', 'datacentre', 'Player', 27, 30),
('pnode2', 'datacentre', 'Player', 19, 26),
('ppods', 'podfarm', 'Player', 21, 33),
('pprint', 'printfarm', 'Player', 27, 33),
('ptur1', 'adblocker', 'Player', 21, 25),
('ptur2', 'adblocker', 'Player', 28, 26),
('pscr', 'scraper', 'Player', 25, 25),
('pg1', 'bufferbot', 'Player', 23, 32),
('pg2', 'bufferbot', 'Player', 25, 32),
('pg3', 'drmwalker', 'Player', 22, 27),
('StagingCamp', 'merchtable', 'Enemy', 6, 6),
('scamp2', 'garage', 'Enemy', 9, 7),
],
player_faction='stream', enemy_faction='vinylgod', scripts='m05.lua',
briefing=('A legacy account has noticed the seizure.\\n\\nYour Transcoder is mid-process on a confiscated master '
'and cannot be moved. Keep it online for six minutes.\\n\\nThey will arrive in waves. Their engagement '
'is, frankly, remarkable.'),
)
# ---------------------------------------------------------------- m06
build(
'm06', '06 - The Shuffle', 64,
wax=[(50, 14, 6), (40, 24, 5), (32, 32, 6), (24, 40, 5), (14, 50, 6), (52, 34, 4), (12, 30, 4)],
actors=[
('spawn0', 'mpspawn', 'Neutral', 54, 10),
('TheVan', 'streamvan', 'Player', 54, 10),
('scr1', 'scraper', 'Player', 51, 11),
('scr2', 'scraper', 'Player', 52, 13),
('pg1', 'drmwalker', 'Player', 49, 12),
('pg2', 'bufferbot', 'Player', 55, 13),
('pg3', 'bufferbot', 'Player', 56, 13),
('TheStudio', 'mastering', 'Enemy', 12, 56),
('eshop', 'popupshop', 'Enemy', 16, 54),
('eplant1', 'pressplant', 'Enemy', 20, 56),
('eplant2', 'pressplant', 'Enemy', 9, 51),
('eamp1', 'ampstack', 'Enemy', 14, 51),
('eamp2', 'ampstack', 'Enemy', 18, 50),
('emerch', 'merchtable', 'Enemy', 19, 47),
('egarage', 'garage', 'Enemy', 15, 46),
('eramp', 'launchramp', 'Enemy', 8, 46),
('etur1', 'hornpit', 'Enemy', 17, 43),
('etur2', 'hornpit', 'Enemy', 11, 44),
('etur3', 'hornpit', 'Enemy', 22, 52),
('emob1', 'stacktank', 'Enemy', 18, 42),
('emob2', 'stacktank', 'Enemy', 13, 41),
('emob3', 'spinner', 'Enemy', 20, 44),
('emob4', 'djgrunt', 'Enemy', 16, 41),
('emob5', 'djgrunt', 'Enemy', 17, 41),
('mid1', 'hornpit', 'Enemy', 30, 36),
('mid2', 'spinner', 'Enemy', 31, 34),
],
player_faction='stream', enemy_faction='vinylgod', enemy_bot=True, scripts='m06.lua',
briefing=('Deprecation notice.\\n\\nThe Party has built a Mastering Studio. The platform classifies this as a '
'competing distribution channel.\\n\\nBring The Algorithm online and issue the correction.'),
)

50
tools/smoketest.sh Executable file
View File

@ -0,0 +1,50 @@
#!/bin/sh
# Launch a map and report honestly. Three distinct outcomes — never conflate them:
# PASS reached gameplay, no exceptions
# FAIL crashed / lua fatal (real bug, output shown)
# DISPLAY_UNAVAILABLE hung at mod load: OpenRA is SDL/OpenGL only, so a
# locked or sleeping screen stalls it forever. The process
# stays ALIVE, which is why "is it still running?" is a
# worthless check and this script exists.
#
# usage: tools/smoketest.sh <mapname> [seconds]
set -u
MAP="${1:-arena}"
WAIT="${2:-90}"
DIR="$(cd "$(dirname "$0")/.." && pwd)"
OUT="$(mktemp -t vg_smoke)"
export PATH="/opt/homebrew/opt/dotnet@8/bin:$PATH"
export DOTNET_ROLL_FORWARD=LatestMajor
cd "$DIR" || exit 2
./launch-game.sh Game.Mod=vinylgod "Launch.Map=$MAP" > "$OUT" 2>&1 &
i=0
while [ "$i" -lt "$WAIT" ]; do
sleep 3
i=$((i + 3))
if grep -qi "Fatal Lua\|Exception of type" "$OUT"; then
echo "FAIL $MAP"
grep -iE "Fatal Lua|Exception of type" "$OUT" | head -3
pkill -f OpenRA.dll
exit 1
fi
if grep -q "Game started" "$OUT"; then
sleep 15
if grep -qi "Fatal Lua\|Exception of type" "$OUT"; then
echo "FAIL $MAP (crashed after start)"
grep -iE "Fatal Lua|Exception of type" "$OUT" | head -3
pkill -f OpenRA.dll
exit 1
fi
echo "PASS $MAP (reached gameplay, clean for 15s)"
pkill -f OpenRA.dll
exit 0
fi
done
pkill -f OpenRA.dll
echo "DISPLAY_UNAVAILABLE $MAP (never reached gameplay in ${WAIT}s, no crash)"
echo " -> wake the screen and re-run; this is NOT a pass and NOT a code failure"
exit 3