Staredit Network > Forums > Modding Assistance > Topic: StarCraft 64 Co-Op Mod - Enemy AI won't launch scripted attacks
StarCraft 64 Co-Op Mod - Enemy AI won't launch scripted attacks
Aug 26 2026, 2:13 am
By: tokatta007  

Aug 26 2026, 2:13 am tokatta007 Post #1



Hi all,

I've built a 2-player split-screen shared-control co-op hack of StarCraft 64 that runs across all 60 campaign missions (I released it as V1 on romhacking.net + GitHub). It works well, but there's one bug I've spent about two days chasing and can't crack, and I'm hoping someone with deeper SC64 knowledge might be able to help out.

The bug: On script-driven survival missions (clearest example: Terran mission 3, "Desperate Alliance" / 00C.chk), the enemy computer doesn't launch proactive scripted attack waves in co-op like it does in the vanilla game. It builds units, the defense buildings fire, and units occasionally defend when directly attacked, but the zerg ai never sends scripted waves of enemies at the player. In vanilla single-player, the same mission's enemy attacks reliably (~1–3 min in). Missions where the enemy uses autonomous/melee-style AI (T1, T2, T5) attack perfectly fine in co-op. Only the trigger/script-driven survival mission type fails.

What I've verified (live, in the PJ64 debugger, vanilla vs co-op on the same mission):
- Enemy is a valid computer player, correct owner (05) / race, on the same slot (4) in both. The AI did not shift to a different slot in co-op.
- The AI script interpreter (0x800939F0 I think), the command dispatcher (0x800196F8 I think), and the AI-script setup action (0x80073740 I think) all run in co-op, same as vanilla.
- Alliance/hostility is fine. The enemy attacks the human players on the missions that work, so it does consider the human an enemy. And occasionally the enemy will attack the units I send at it on this T3 mission.
- I traced the 2-player flag (0x800AFEFC): 137 read sites, 16 that run during the mission. I force-patched all 16 to take the 1-player path simultaneously, but sadly no change to the enemy. So the bug is NOT a 2p-flag branch.

- The mod's functional changes are: P2 shares P1's slot, a stamp of type=2 into player-3's slot (0x800B0108) for independent P2 control, viewport, and a campaign-load enable at 0x800229B4 that also bumps the player count to 2.
- The clearest data difference is the enemy's per-player AI struct (0x800F0E70 + slot*0x500): in co-op its unit-tracking list has ~10 records vs ~4 in vanilla, but I traced the code that adds them (0x8004AB20 / 0x8004B208) and it registers records keyed by unit type, so I believe that's a symptom of the busier co-op game, not the cause.

My working theory: The no-attack behavior is emergent from the 2-player context. Some timing/ordering difference in the scripted-wave path that doesn't reduce to a single flag or value I can find. But I'd love to be wrong, because that means it's fixable. Currently I'm stumped.

Questions:
1. Does anyone know how SC64's trigger engine handles AI Script/scripted attack activation, and whether the player count or the 2-player launch path changes when or whether that action fires? That's the piece I can't see cleanly.
2. Any docs on the runtime trigger/AI-script data layout in RAM (the trigger data lives around 0x800C0000) would help a lot.

I can share the full list of addresses, my debugger captures, RAM dumps (vanilla vs co-op on Desperate Alliance), and the BPS patch, or whatever's useful. Happy to do any tests someone wants to suggest. Thanks for reading, and thanks for keeping Starcraft alive <3

My mod: https://github.com/Tokatta007/Starcraft-64-Co-Op-Mod

Post has been edited 3 time(s), last time on Aug 26 2026, 2:34 am by tokatta007.



None.

Aug 26 2026, 3:54 am Heinermann Post #2

memes memes memes memes memes memes memes memes memes memes memes memes memes memes memes memes memes memes memes memes memes memes memes memes memes

Starcraft 64 uses almost the same code as the PC ver, it is a true port as far as I could tell. I think maybe the patch turned a UMS (campaign) into a Team game. I think we did this as a hack on PC a long time ago too, but I don't remember what issues it caused if any.

Is the slot that P2 is using not overlapping with other in-game slots? Are they both marked as human-controlled in the same force? If one is marked as something else (i.e. computer/inactive) it might cause a problem. Of course, that's only if SC64 is handling it the same way it would on PC.

Quote
2. Any docs on the runtime trigger/AI-script data layout in RAM (the trigger data lives around 0x800C0000) would help a lot.
It depends on the ROM, I think? The addresses provided don't match the ROM I annotated. I don't know that triggers would impact it, and since Team UMS is not a valid game mode on PC unless hacked in I don't know how the AI interacts with it. I think it's strictly an AIScript issue or slot configuration issue, unless that mission specifically has a trigger that forces an attack wave.

I have only ported some symbols to SC64 to use as a reference for reverse engineering the PC ver. In the reverse engineering project, AI is by far the least understood and least labelled part of the source right now.

I plan to create a thread announcing the project Soon™ with crowdsourcing tasks for non-coders too, but I'd be happy to share stuff early. Let me know if you want source access & I can upload the IDA dbs separately. The IDB I labelled is for `StarCraft 64 (E) [f1] (NTSC).z64`, and the best labelled idb I have is 1.17 followed by MacOS's 1.16.1. If you don't have/use IDA I can dump the symbols into any format you want.

Some structs in SC64 are different i.e. in https://www.decomp.me/scratch/nolS6 (see Context tab) TRIGGERNODE has a pointer to the TRIGGER struct instead of a copy, to reduce memory usage on the N64. You can grab all the enums and related structs from that context tab.

After looking at Terran3 it does run Send All Units on Strategic Suicide Missions after 2 minutes, and runs Terran 3 - Zerg Town at the start.

I've got this for the send suicide opcode lol so not much info
Code
   case AIOPC_SEND_SUICIDE:
     {
       BYTE bSuicideType = aiscript_get_byte(pScp);
       aiscript_attack_clear(pScp->owner, TRUE);
       //sub_4B11B0(pScp->owner, bSuicideType);
       aiscript_quick_attack(pScp->owner, 5);  // TODO def
     }
     break;





Aug 26 2026, 5:27 am tokatta007 Post #3



Thank you for the quick and detailed reply! This is incredibly helpful.

"Send All Units on Strategic Suicide Missions after 2 minutes" lines up with what I see. In co-op that attack never comes. The enemy runs "Zerg Town" fine (it builds, defends locally), but the suicide wave never fires. Given the opcode you pasted:

aiscript_attack_clear(pScp->owner, TRUE);
aiscript_quick_attack(pScp->owner, 5);

My suspicion is that pScp->owner isn't resolving to the Zerg in the co-op configuration, so quick_attack is pointed at the wrong (or an empty) owner and does nothing. That would explain why every stage I could observe "runs" but no wave appears. The script executes, it's just commanding the wrong player. Does that hold up against how the AI script owner actually gets assigned? And is pScp->owner set once when the script is attached (Run AI Script), or resolved live each time an opcode runs?

So my mod is a shared-control split-screen hack that effectively puts the campaign into Team Melee / Archon mode: both players share one force, one base, one army, and Player 2 is a second cursor, selection, and viewport rather than a separate player with its own units. The core changes are that P2 shares P1's slot for control (patch at 0x8003DF34), P2 gets its own selection context (0x8003DF48), and there's a campaign-load enable at 0x800229B4 that also sets the second_player_active flag (0x800AFEFC) and bumps the active player count to 2. The engine's master player table at 0x800AFF54 (stride 0x24, +0 control where 02 = human occupied, +1 race, +2 local player #) is where the co-op controller config ends up, so if that's what determines the AI script's owner, that's where I'd expect the mismatch to originate.

Your reply is resonating with me since I already hit the same "co-op is classified as multiplayer from launch" behavior in a completely different system. Mid-game Campaign saves and mission progression doesn't save in co-op, and when I traced it, the cause was that second_player_active makes the engine treat the session as multiplayer, so the campaign mission-complete event chain never fires and it ends through a multiplayer results path instead. If that same launch-mode classification also changes how AI script ownership or the force setup resolves, that would explain it.

For reference, Terran 3's OWNR is 07 06 00 00 05 (slot 0 neutral, slot 1 you/human, slots 2 and 3 empty, slot 4 Zerg/computer). Missions where the enemy uses autonomous/melee-style AI attack perfectly fine in co-op. Only the trigger-driven survival type, the ones that rely on that suicide script, fail. That pattern seems consistent with a script-owner or force-resolution problem rather than anything unit or map specific. Or maybe the issue is my mod doesn't like ai script changes?


Two things that would help me a ton:

1. In your annotated source, how is the owner determined for a running AI script? Is there a spot where the Team Melee / multiplayer player config could cause a script to be owned by, or its attack directed at, the wrong player?

2. Yes please to the source, I'd be really grateful <3 I don't actually have IDA, so the reverse-engineered code and the struct and enum definitions are the part I'm missing, more than the IDB itself. A symbol dump or the source in any plain format would be perfect, if it's not too much trouble. Also, I'm on StarCraft 64 (U), so I know your (E)[f1] addresses won't line up directly, but the structs and script logic are exactly what I need.

Really appreciate you taking the time on this. The reverse-engineering project sounds great too, and I'm happy to take a look at it whenever you launch it. Exciting stuff!



None.

Aug 26 2026, 8:36 am Heinermann Post #4

memes memes memes memes memes memes memes memes memes memes memes memes memes memes memes memes memes memes memes memes memes memes memes memes memes

Quote
Does that hold up against how the AI script owner actually gets assigned? And is pScp->owner set once when the script is attached (Run AI Script), or resolved live each time an opcode runs?
pScp->owner will be the player owner of the script that was run - that would be the executing trigger player. It's correct for the area town so it should be correct for suicide mission.

Another point on that mission, P2 is the main player, P1 (red) is the neutral player that shows up in the in-game cutscene at the end of the mission. So for co-op in that mission you want them to be P2 and P3 I think. The enemy is P5 orange and probably wouldn't change. If the computer player sees your team captain as non-enemy then it might not attack. There's a mapping of team captains in GAMEDATA.

Quote
1. In your annotated source, how is the owner determined for a running AI script? Is there a spot where the Team Melee / multiplayer player config could cause a script to be owned by, or its attack directed at, the wrong player?
It just passes from trigger owner directly to the script when it's created.

I invited you to the repo but the related AI code is probably not reverse engineered yet.




Aug 26 2026, 4:45 pm tokatta007 Post #5



Thanks again! The source access let me actually verify things instead of guessing, and I think it ruled out the alliance theory.

Using the OFFSET_ASSERT for sgEnemyTbl (0xE544 in the GAMEDATA struct) I found the table in a live co-op Desperate Alliance dump by anchoring on the force names. Then I pulled three time-stamped RAM dumps at 1, 2, and 5 minutes in and read the Zerg's row each time.

The result was sgEnemyTbl[Zerg][human] = 0 (AS_AGGRESSIVE) at all three times, and the entire table is byte-for-byte identical across the whole mission. So the Zerg correctly sees the human as an enemy the entire time, including well past the 2-minute suicide window, and nothing (no PLAYER_ALLY opcode or trigger) flips it mid-mission. IsEnemyUnit(Zerg, human) should be returning TRUE throughout.

So the alliance/captain path isn't it after all and the failure must be downstream in the attack machinery. AIOPC_SEND_SUICIDE calls aiscript_quick_attack(owner, 5), which just sets sgBanker[owner].unk_228 (the attack timer). Then sub_483940 reads unk_228 and, when it expires, returns TRUE to actually form and send the wave, and that former is in the AI code you mentioned isn't reverse-engineered yet.

That's where I'm stuck, so I have two questions:

1. In a session with two human players in Team Melee, does the AI banker/town actually get serviced for a computer player? I'm wondering if the "classified as multiplayer from launch" behavior means the computer AI's per-frame update (the thing that reads unk_228 and forms the attack) doesn't run the same way for the Zerg. So the script sets the timer, but nothing services it, so the wave never forms while local defense still works.

2. Is any of that path — sgBanker, aiscript_quick_attack, sub_483940, or the attack-former, mappable to the N64 (U) ROM yet, or is there a way to locate sgBanker in an N64 RAM dump? Your symbols are PC addresses so I can't map them directly, and the struct signature is too generic to find sgBanker reliably by scanning. If I could read the Zerg's unk_228 across those same three dumps I could tell whether the timer is being set and ignored instead of never set at all.

Either way, really appreciate the help! Ruling out the alliance table cleanly was only possible because of your source. Happy to share the dumps or the exact offsets if useful.



None.

Aug 26 2026, 5:55 pm Heinermann Post #6

memes memes memes memes memes memes memes memes memes memes memes memes memes memes memes memes memes memes memes memes memes memes memes memes memes

At this stage I would see if the problem is reproducible on PC. You could take a RAM dump of sgBanker on the PC ver and reverse the bits of each field to find it.

Can you give the exact steps you performed to make the mission co-op? I could repro it and get the dump with my setup easily.




Aug 26 2026, 6:24 pm NimoStar Post #7



This is really, really cool and I appreciate the work.

There's some stuff that will make me never use it tho:
* I'm not a console peasant ;)
* I don't have anyone to play it with (sad)



None.

Aug 26 2026, 10:26 pm tokatta007 Post #8



So the mod only 16 changes/56 bytes, so here's the complete list. The addresses are N64, so they won't map to PC directly, but hopefully they tell you exactly what state to recreate.

The game-logic changes (the ones that matter for the repro):
- 0x800229B4: at campaign load, force the "second player active" flag to 1 (the original code read the flag and branched; the patch just writes 1 to it). This is the master switch that makes a campaign mission run with 2 players.
- 0x800D8104: this hijacks a load-time copy routine to stamp 0x0101 into the game template/options block at load. I verified in a live RAM dump that it lands at 0x800D118C (vanilla 0x0000 -> co-op 0x0101). I couldn't pin down which specific Options field that maps to on N64. This is the one I'm most curious about on PC. Does an equivalent write flip IsScenarioGame() to false and route alliance_init down the team path?
- 0x8003DF34 and 0x8003DF48: P2 shares P1's control slot, and P2 gets its own selection context (shared-control, one army).
- 0x8007A454: P2's viewport for split-screen.

The rest are cosmetic and irrelevant to the bug. They are menu text (renaming the modes to "Story Co-Op" / "Team Melee"), an unlock-all-missions table, a couple of menu indices, a save-game-menu tweak (the vanilla save screen freezes so I block scrolling into it), and the ROM header region byte plus its recalculated CRC.

So the state to reproduce on PC would be the campaign mission Terran 3 Desperate Alliance running with the second-player flag forced on and that template write applied. Effectively a 2-human shared-control session on a campaign map, with the map's computer enemy (Zerg) untouched.

And I think I ruled out the alliance table on the N64. I pulled sgEnemyTbl (found it via the force-name strings, so the s-global base is nailed down) in three time-stamped dumps at 1, 2, and 5 minutes, and sgEnemyTbl[Zerg][human] = AS_AGGRESSIVE at all three, never flipping. So IsEnemyUnit is returning TRUE the whole time; the wave still never forms. That's what points me downstream to the banker / quick_attack servicing.

So the two things a PC repro could nail: (1) does that template write actually flip IsScenarioGame() and send alliance_init down the team path, and (2) does sgBanker[Zerg].unk_228 get set by the suicide script and then never consumed (or never set at all). Is the computer AI's attack former even being serviced in this 2-human configuration?

Thanks again btw. I'm happy to send the three N64 dumps or exact offsets if any of it saves you a step.



None.

Yesterday, 3:01 am Heinermann Post #9

memes memes memes memes memes memes memes memes memes memes memes memes memes memes memes memes memes memes memes memes memes memes memes memes memes

P1 is a reserved neutral player in that mission, you mean P2 and P3 slots or which slot does the second co-op player occupy?




Yesterday, 3:37 am tokatta007 Post #10



It's a little weird because of how the mod pulls off co-op.

The human Player 1 Terran in Desperate Alliance is slot 1 (zero based). Slot 0 is that reserved neutral Terran, and slot 4 is the Zerg computer. Everything else is empty.

So the second player doesn't get its own slot at all. The mod does shared control, archon-style co-op. Controller 2 (Player 2) just binds to slot 1 alongside Controller 1 (Player 1), so both players are driving the same army. Both co-op players are sitting in slot 1.

Which means from the engine's point of view the player table looks exactly like single-player with one human on slot 1, one computer on slot 4, the neutral in slot 0, and nothing else. I checked a RAM dump and the resolved player types are the same as vanilla 1 Player. The only things that actually say "two players" are the second_player_active flag and the input/viewport patches.

So I don't think you need a real second human in a slot, just single-player Desperate Alliance with that second_player_active flag forced on (plus the template write). If the enemy still won't send its wave when the slot layout looks single-player, then whatever that flag touches is what's killing it.

Also, I spent a while in the StarSource AI code while waiting and it kept pointing me at the banker. The enemy resolves to C_COMPUTER fine, so IsComputer doesn't look like the issue, and it feels like either the banker never gets enabled or the attack never gets assembled. I'll spare you the function-by-function trace I did through the banker unless you'd like me to elaborate.



None.

Yesterday, 5:52 am Heinermann Post #11

memes memes memes memes memes memes memes memes memes memes memes memes memes memes memes memes memes memes memes memes memes memes memes memes memes

It seemed to run the script fine in team mode on PC.

Banker slot for computer player at start of game (after a couple seconds):

flags = 0x26
max_force = 600
field_22F = 1
defensebuild_gg[0] = 0x26 (Ling)
defensebuild_ag[0] = 0x27 (Hydra)
defensebuild_ga[0] = 0x2C (Muta)
defensebuild_aa[0] = 0x2C (Muta)
defenseuse_gg[0] = 0x27 (Hydra)
defenseuse_gg[1] = 0x26 (Ling)
defenseuse_ag[0] = 0x27 (Hydra)
defenseuse_ga[0] = 0x2C (Muta)
defenseuse_aa[0] = 0x2C (Muta)


After suicide mission script runs:
flags = 0x527
field_21E = 0x61
field_223 = 9
field_228 = -144
field_22C = 0x17

Not sure what else to check, I'm at a loss sorry.




Today, 5:39 am tokatta007 Post #12



Hey! Just closing the loop on this. Your decomp work and that banker dump were a biggg part of cracking it. Turned out not to be the AI logic at all.

The banker was a red herring (your dump is what let me rule it out). The real cause was on the mod's side. To enable some co-op/menu features, the hack disables a couple of internal list allocators by zeroing a branch. One of those "allocator" edits was shared with the enemy AI's attack-group assembly. The fix was a one-byte revert of that edit (restoring the branch that keeps the shared allocator alive). Enemy attack waves now fire in co-op just like single-player, and all the co-op/menu features still work. I'll be putting out a V2 of the mod on my github and romhacking.net shortly.

Really appreciate you offering to dig in and sharing the dump! Being able to confirm
the banker side was healthy on the PC side is what pointed me back at the mod's own changes instead of chasing the AI scripts. Thanks again! <3



None.

Options
  Back to forum
Please log in to reply to this topic or to report it.
Members in this topic: None.
[2026-8-27. : 2:37 am]
tokatta007 -- I love my everdrive. Was really cool to test the coop mod on there. Just wish I can polish this last bug! >:(
[2026-8-27. : 2:26 am]
Symmetry -- I really need to get an everdrive. and a TV that doesn't suck for my n64
[2026-8-27. : 1:15 am]
tokatta007 -- Thank you Symmetry. It was about time someone worked on SC64 :)
[2026-8-26. : 2:34 am]
Symmetry -- Power to ya tokatta
[2026-8-26. : 2:34 am]
Symmetry -- Jeez someone's modding SC64? I've never even hear of anyone trying
[2026-8-26. : 2:27 am]
Symmetry -- if you're looking to upload a map put it on scmscx and link it here. the dldb is... inconsistent
[2026-8-25. : 12:51 pm]
Zakerozi -- Lel... This guy is probably a Zoomer :D :D :D
[2026-8-25. : 9:57 am]
NudeRaider -- dydwk474
dydwk474 shouted: HOW DO YOU EVEN UPLOAD FILE HERE THATS BULLCOWAP THE WEBSITE DUMB
mostly magic, but also doing a :wob:-raindance and sacrificing your firstborn. If that sounds like too much effort, just use www.scmscx.com
[2026-8-25. : 9:55 am]
NudeRaider -- Vrael, did you rename your account?
[2026-8-25. : 7:30 am]
dydwk474 -- HOW DO YOU EVEN UPLOAD FILE HERE THATS BULLCOWAP THE WEBSITE DUMB
Please log in to shout.


Members Online: Roy