[Scummvm-git-logs] scummvm master -> 9e6422b20a72b3e6107217c7feba9b0227ecf7f9
sev-
noreply at scummvm.org
Sun May 31 14:12:44 UTC 2026
This automated email contains information about 288 new commits which have been
pushed to the 'scummvm' repo located at https://api.github.com/repos/scummvm/scummvm .
Summary:
a9cc4b1243 SCUMM: RA2: Add initial level 1 loading code
9aba962606 SCUMM: RA2: Add missing init variables
32a4ba152c SCUMM: RA2: Deduplicate level object setup
b1e18defe1 SCUMM: RA2: Remove redundant NutRenderer drawFrame overload
de41cd69ab SCUMM: RA2: Avoid crash during shutdown
34e92ca251 SCUMM: RA2: Implement IACT dependency links
ad764f728c SCUMM: RA2: Implement player damage from enemies
d94e6274e1 SCUMM: RA2: Implement enemy explosion slots
a8843cc899 SCUMM: RA2: Implement laser animations
2619d4afc6 SCUMM: RA2: Refactor enemy state handling
764ecbf459 SCUMM: RA2: Remove obsolete enemy code
bd56fe06cc SCUMM: RA2: Add low-difficulty enemy indicators
6482cf87c4 SCUMM: RA2: Simplify enemy handling
512ae9f8f1 SCUMM: RA2: Implement cockpit movement effect
fd596d3dda SCUMM: RA2: Refine cockpit movement effect
ea21cbd852 SCUMM: RA2: Improve laser beam rendering
474e3d5b7b SCUMM: RA2: Refine laser beam rendering
b33180f25f SCUMM: RA2: Complete laser beam rendering
0e87f2c99c SCUMM: RA2: Play intro before level 1
77722bbfed SCUMM: RA2: Remove obsolete setup code
6b45c55368 SCUMM: RA2: Clean up state initialization
df10c54615 SCUMM: RA2: Remove unused state fields
2cebdfc1fc SCUMM: RA2: Implement damage bar
1ada210184 SCUMM: RA2: Refactor opcode 2 handling
c1bc5f218a SCUMM: RA2: Move SMUSH hooks out of SmushPlayer
d78e69c275 SCUMM: RA2: Refactor opcode 6 handling
404037ab75 SCUMM: RA2: Fix post-rendering state updates
ea84ceebdf SCUMM: RA2: Skip non-gameplay sounds
353416241d SCUMM: RA2: Implement score and mouse pointers
deb62f97e1 SCUMM: RA2: Implement subtitles
a3676cab6e SCUMM: RA2: Add dedicated audio system
f3bd96abae SCUMM: RA2: Implement main menu
f91918ff34 SCUMM: RA2: Implement additional menus
14884b6d74 SCUMM: RA2: Implement level loading
ead8b2a3ed SCUMM: RA2: Clear screen before missions
2878546d81 SCUMM: RA2: Implement pause and skip controls
1fa930ed85 SCUMM: RA2: Load directional ship sprites
db7b16c044 SCUMM: RA2: Fix level 1 loading regression
ac86b2729c SCUMM: RA2: Implement collision zones
a4bbf9032e SCUMM: RA2: Load low-resolution cockpit HUD
5473605500 SCUMM: RA2: Fix low-resolution cockpit HUD loading
8718764edf SCUMM: RA2: Add level 2 video decoders
0a05f1f519 SCUMM: RA2: Fix missing handler 1 crash
54610c886d SCUMM: RA2: Refactor level object handling
0642d63e38 SCUMM: RA2: Refactor level state handling
6da03acf19 SCUMM: RA2: Refactor gameplay handlers
5c0b23415f SCUMM: RA2: Reduce shared SMUSH dependencies
04aef6e229 SCUMM: RA2: Read level 2 backgrounds correctly
3abc8c5bc6 SCUMM: RA2: Improve level 2 backgrounds
b621b9cc6e SCUMM: RA2: Improve level 2 player sprites
9ce2e159b1 SCUMM: RA2: Implement level selection screen
bb9375faec SCUMM: RA2: Improve font rendering
6239b89fbb SCUMM: RA2: Improve shot rendering and terminology
0dad1b0270 SCUMM: RA2: Improve level selection screen
7e80992c72 SCUMM: RA2: Check whether the player can shoot
07f891177c SCUMM: RA2: Animate level 2 player correctly
05b0dc540a SCUMM: RA2: Improve bit handling
30795feeb5 SCUMM: RA2: Improve level 2 rendering
0983d2c688 SCUMM: RA2: Improve main menu
4dd1e28b95 SCUMM: RA2: Play intro correctly from main menu
84cf81e8e6 SCUMM: RA2: Load menu strings from TRS files
79010d6604 SCUMM: RA2: Support colored fonts
2bc1457374 SCUMM: RA2: Read fonts correctly
8ea7634eee SCUMM: RA2: Implement damage effect
1f53cb0d7e SCUMM: RA2: Improve pilot menu
267ddb67fe SCUMM: RA2: Implement difficulty selection menu
9b76b0247a SCUMM: RA2: Improve level menu
1ae852f6e9 SCUMM: RA2: Improve level 2
f2c9358f1b SCUMM: RA2: Refactor level 2 rendering code
41488ed0aa SCUMM: RA2: Implement per-enemy explosion handling
bcd6615899 SCUMM: RA2: Implement damage handling
ca8874e261 SCUMM: RA2: Handle damage in level 3
7d61726513 SCUMM: RA2: Improve ship control for level 3
1f23ead15c SCUMM: RA2: Implement ship explosions for level 3
1e2e7e108e SCUMM: RA2: Add explosion sounds
07c45898d7 SCUMM: RA2: Add shooting sounds
31a1ecf0cf SCUMM: RA2: Render gun shooting
285f7ff2b1 SCUMM: RA2: Prevent damage while covered
713aa7bd91 SCUMM: RA2: Fix covered and uncovered phase cycling
7ae1ddbd5a SCUMM: RA2: Reset handlers around level 2 cinematics
9130d5bd4b SCUMM: RA2: Fix SKIP and opcode 2 bit handling
899667bc95 SCUMM: RA2: Clear IACT bits for level 2 waves
6af2ff75d6 SCUMM: RA2: Apply FOBJ offsets to enemy overlays
6da68ffd2b SCUMM: RA2: Implement additional level handlers
06cc4d01d4 SCUMM: RA2: Improve menu video handling
c9a07cc6a1 SCUMM: RA2: Clean up menu handling
20b45a1ce0 SCUMM: RA2: Split implementation into subsystem files
274be9ad63 SCUMM: RA2: Improve intro playback
8723af81db SCUMM: RA2: Improve subtitles
49e3be755d SCUMM: RA2: Guard wave initialization in opcode 6
3d79ed0526 SCUMM: RA2: Minimize impact outside the RA2 engine
38af869f7c SCUMM: RA2: Improve UI rendering
696c68cd85 SCUMM: RA2: Add pilot and difficulty menus
9f75d49f08 SCUMM: RA2: Tune difficulty parameters
9506f4af42 SCUMM: RA2: Apply code conventions
a03ea4774f SCUMM: RA2: Improve game detection
de4c041f00 SCUMM: RA2: Reset enemy state for level 2 waves
c4e1df0f2f SCUMM: RA2: Add intro level text overlay
da4840d6b2 SCUMM: RA2: Refactor ship sprite rendering
528969fe79 SCUMM: RA2: Add basic unlocking and password handling
52fcf017dc SCUMM: RA2: Improve password handling
7c74f5e134 SCUMM: RA2: Refactor player shot rendering
fd61a9b4f3 SCUMM: RA2: Guard background copies by buffer pitch
15c5136ca5 SCUMM: RA2: Position ship sprites with viewport offsets
972dba7f59 SCUMM: RA2: Fix Escape key bug
ce750db5b1 SCUMM: RA2: Use a large enough bitmap
f72c255c6a SCUMM: RA2: Fix crash in smushDecodeSkipRLE
0ef613d4c1 SCUMM: RA2: Update death handling and level progression
825c3c8547 SCUMM: RA2: Improve sound handling
589580d5b6 SCUMM: RA2: Fix handler 25 ship rendering
a97db6f7fd SCUMM: RA2: Improve shot beam rendering
0aacf5d751 SCUMM: RA2: Improve level selection
3653d5839f SCUMM: RA2: Fix score font rendering
8d927d5377 SCUMM: RA2: Fix clipped sprite decoding
a988ea1aae SCUMM: RA2: Improve collisions and shadow indicators
8c4dfa645a SCUMM: RA2: Remove debug code
25cb578e5f SCUMM: RA2: Remove remaining debug code
b8518e989e SCUMM: RA2: Add top pilots screen
b3a0be791d SCUMM: RA2: Improve top pilots screen
632c63e17e SCUMM: RA2: Add options menu
0f3ad175bc SCUMM: RA2: Standardize declarations and comments
c390c411cf SCUMM: RA2: Standardize menu and rendering comments
60c4652e2a SCUMM: RA2: Standardize IACT and runlevel comments
523bd629c6 SCUMM: RA2: Clean up variable names
74da777074 SCUMM: RA1: Add initial proof of concept
4c5afc1201 SCUMM: RA1: Extend initial proof of concept
8917d7f009 SCUMM: RA1: Add initial UI rendering code
3ac282b4a8 SCUMM: RA1: Add mouse handling
73fbf2b8d5 SCUMM: RA1: Add sound support
4ed9384177 SCUMM: RA1: Improve controls
4c5eda6d07 SCUMM: RA1: Add basic damage handling
c8264eba60 SCUMM: RA1: Play additional videos
3073b37e79 SCUMM: RA1: Improve UI
7fee7db76e SCUMM: RA1: Add menu
50959ca0d3 SCUMM: RA1: Improve video decoding
591fc69205 SCUMM: RA1: Add sound and subtitles
d1a0b4d9bf SCUMM: RA1: Add damage handling
ecfc1eefad SCUMM: RA1: Improve level 1 loop
01e04961b0 SCUMM: RA1: Add right-path branching in level 1
8074a8c460 SCUMM: RA1: Refine level 1 flight handling
eca65436dd SCUMM: RA1: Add options
5a0f9d09a8 SCUMM: RA1: Add branching
c0146e48e0 SCUMM: RA1: Add level 2 and HUD features
c33805415a SCUMM: RA2: Capture mouse
2d99feb58f SCUMM: RA2: Add level execution helpers
1969645fbb SCUMM: RA2: Add credits
6e1e672ffd SCUMM: RA2: Improve pause menu layout
86c9b2d979 SCUMM: RA: Split Rebel Assault engine files
f9aa62e2bc SCUMM: RA2: Parse level 2 video chunks correctly
0aa0739701 SCUMM: RA1: Add level selection and level 2 pointer handling
3e45c5db23 SCUMM: RA1: Render level 2 lasers
a854f3fd00 SCUMM: RA1: Fix level 1 targeting pipeline
1f6fdf96c8 SCUMM: RA1: Fix interactive video ghosting
43a9aeb6ee SCUMM: RA1: Fix asteroid view offsets
19140d2d21 SCUMM: RA1: Clear oversized frame buffers
9a2fd5d3e2 SCUMM: RA1: Fix UI rendering
8210b3de83 SCUMM: RA1: Fix level 1 stage transition
8434efb10b SCUMM: RA1: Fix level 1 retry flow
da5c1e19e0 SCUMM: RA1: Add subtitle fonts
360d4911f1 SCUMM: RA1: Fix subtitle positioning
7cad2386cf SCUMM: RA1: Transform GAME zones with camera offsets
08430ff3f9 SCUMM: RA1: Preserve left-path branching
151bad125b SCUMM: RA1: Fix level 1 second phase
fe86583657 SCUMM: RA1: Refine level 1 second phase
a5dd30066d SCUMM: RA1: Improve level 2 perspective and movement
a17bfbcf4b SCUMM: RA1: Refactor SMUSH integration
133d39acac SCUMM: RA: Add additional level handlers
26a309a37a SCUMM: RA1: Add projection table for gameplay points
4ceacc55a2 SCUMM: RA1: Track frame object hit state
851bb8fcba SCUMM: RA1: Add hitboxes
cecedef4f7 SCUMM: RA1: Apply overlay Y offsets
c5b5b86f8d SCUMM: RA1: Add level 7 through 10 handlers
560d6f9ca5 SCUMM: RA1: Center interactive FTCH placement
2a4d224a45 SCUMM: RA1: Render HUD plate opaquely
ad1713032f SCUMM: RA1: Enable controls in level 3
ce45d205dd SCUMM: RA1: Refine mouse and frame opcode state
2a7ffb4168 SCUMM: RA1: Trigger death video immediately
a6ab490d52 SCUMM: RA1: Add additional level implementations
53bc68ed7b SCUMM: RA1: Add missing sound effects
26edb60399 SCUMM: RA1: Use named parameters
0d84e60b0b SCUMM: RA1: Add chapter names in intro
cf25b976b0 SCUMM: RA1: Improve level selection menu navigation
595f2a1fcb SCUMM: RA1: Improve level 9 rendering
0235c711e1 SCUMM: RA1: Improve on-foot level rendering
4dcc5d8d2d SCUMM: RA1: Improve walker level rendering
403d9675a9 SCUMM: RA1: Complete menu handling
3ec6f05b03 SCUMM: RA1: Add level 11 through 15 handlers
730896f1c3 SCUMM: RA1: Add missing globals
a8581ef1f3 SCUMM: RA: Add basic joystick support
51052ab299 SCUMM: RA1: Improve torpedo handling
ad7549ed3c SCUMM: RA1: Improve Escape key handling
326181870b SCUMM: RA1: Implement level 5
35220322a7 SCUMM: RA1: Improve collisions
aaa9ef1714 SCUMM: RA1: Fix level 4 progression
b3a580ac85 SCUMM: RA1: Remove level 4 workaround
22518d3549 SCUMM: RA1: Improve difficulty and energy checks
03a3e02a5a SCUMM: RA1: Add skip-RLE line update decoder
c33d5d1c06 SCUMM: RA1: Fix level 8 armor targets
d0f4a422ec SCUMM: RA1: Fix level 7 video transition
cea798d8c7 SCUMM: RA2: Move main loop out of scumm.cpp
00f9be188b SCUMM: RA: Move Rebel Assault SMUSH logic into RA players
90cab45dae SCUMM: RA: Move RA1 SMUSH frame handling into RA player
f3e88ea732 SCUMM: RA: Move text and GOST handlers into RA SMUSH players
cbbdde480b SCUMM: RA: Move RA1 codec 1 decoder into RA player
f854095da7 SCUMM: RA: Move RA1 auxiliary decoders into RA player
31d2c9a573 SCUMM: RA: Move specific class definitions out of smush_player.h
3dbe2d7eb7 SCUMM: RA2: Remove redundant code
3350b7b032 SCUMM: RA2: Reduce StringResource change impact
58b9cc95c4 SCUMM: RA: Reduce insane.h change impact
091dcd3018 SCUMM: RA: Reduce NutRenderer change impact
90cfb6851d SCUMM: RA: Move Rebel Assault code into rebel directories
dafba02a3b SCUMM: RA1: Fix regression in RA1 block decoder
d208eb1f6b SCUMM: RA1: Fix RA1 block decoder edge cases
f86c294c78 SCUMM: RA: Remove useless static qualifiers
8b81bcfb80 SCUMM: RA2: Fix mixed indentation
41a8508ee0 SCUMM: RA: Clean up spacing and indentation
10d5865288 SCUMM: RA: Improve descriptions and error messages
96ad3cc3b9 SCUMM: RA: Update hashes in scumm-md5.txt
5a64207352 SCUMM: RA2: Make codec 45 line-size and probe fields signed
804223f422 SCUMM: RA2: Clean up codec 45 debug statements
865d55c54b SCUMM: RA2: Remove goto and redundant seek
2e39caffe5 SCUMM: Remove extra blank lines from NutRenderer
e4ee27d61f SCUMM: RA2: Remove codec duplication
8b11f84c8d SCUMM: RA2: Add missing header
695edd8a79 SCUMM: RA2: Refactor embedded frame region blitting
5568808962 SCUMM: RA2: Harden embedded SAN loading
b1fe2f8fd0 SCUMM: RA2: Refactor level 2 background loading
3881b4371b SCUMM: RA: Pass data size to line update decoder
0bf0542235 SCUMM: RA2: Remove unused code and old TODOs
4ab5b29537 SCUMM: RA2: Remove remaining dead code
14f6c845da SCUMM: RA2: Use playLevelSegment in runlevels
6fae78c49d SCUMM: RA2: Use calculateAccuracy in runlevels
de00bf9dd4 SCUMM: RA2: Refactor explosion rendering
c6e257251e SCUMM: RA2: Remove remaining gotos
ae5c073de3 SCUMM: RA2: Deduplicate runlevel helpers
4879d743c6 SCUMM: RA2: Refactor wave handling
6eab30feed SCUMM: RA2: Split opcode 6 IACT handling
9092da7f16 SCUMM: RA2: Split opcode 8 handling
79342da6bc SCUMM: RA2: Split post-render handling
d90e9a159a SCUMM: RA2: Split handler 7 code
1da3f8d580 SCUMM: RA2: Use original cursor instead of generic one
d027a4158d SCUMM: RA2: Render full shooting rays
2771d282b9 SCUMM: RA1: Improve level 8 UI rendering
e11c594c4f SCUMM: RA1: Fix invisible cockpit in level 8 sections
6e7020314e SCUMM: RA1: Trigger damage in level 8
47e86512bc SCUMM: RA1: Improve level 8 comments
357f9ca51e SCUMM: RA1: Render SMUSH_CODEC_SKIP_RLE correctly
876fcfa145 SCUMM: RA1: Avoid trapping ScummVM menu during gameplay
a264d2921c SCUMM: RA1: Implement movement and damage for level 1 part 2
b45fc2804c SCUMM: RA1: Allow destroying targets in level 1 part 2
361d886ac7 SCUMM: RA1: Implement damage in level 2
03b63a2b5b SCUMM: RA1: Add missing damage sounds
b31651a76c SCUMM: RA1: Implement level 13
dfeb600867 SCUMM: RA1: Implement level 12
c079896afd SCUMM: RA1: Implement level 14
e94cfd3f51 SCUMM: RA1: Implement level 11 hits overlay
2efc01e655 SCUMM: RA1: Implement level 14 splice
6f800ee60d SCUMM: RA1: Implement level 15 summary and scoring
988fc5be0a SCUMM: RA1: Implement generic level summary and scoring
2b08e14c07 SCUMM: RA1: Add initial joystick support
d0cb0eea1f SCUMM: RA1: Improve menu code
b96a94011e SCUMM: RA1: Allow switching back to original input style
559b588ebf SCUMM: RA1: Implement missing chunks
c47a0c95a4 SCUMM: RA1: Improve HandleGameOp19 accuracy
e62919da5b SCUMM: RA1: Refactor laser rendering
7c868c8408 SCUMM: RA1: Implement password selection correctly
d9df04cc21 SCUMM: RA1: Add missing cutscenes
b55c73fea5 SCUMM: RA1: Avoid crash in handleGameAdjustCoords
c29ef27990 SCUMM: RA1: Fix level 14 frame object state checks
e5324c6080 SCUMM: RA1: Correct level names and tuning selectors
571e6bbc83 SCUMM: RA1: Improve level 13 controls
298177ad65 SCUMM: RA1: Render HUD only during active gameplay
24f60c962a SCUMM: RA1: Improve torpedo mode rendering
ec4cff3a96 SCUMM: RA1: Fix level 9 on-foot targeting
8565fdbda2 SCUMM: RA1: Seed random path selection deterministically
aa24c70839 SCUMM: RA1: Allow navigation in level 7
10feabc9da SCUMM: RA1: Refine navigation in level 7
aacaf1cdd7 SCUMM: RA1: Deduplicate runLevel handlers
625f0e9324 SCUMM: RA1: Extract codecs
0e4cfe0eed SCUMM: RA1: Split handleFrame
1c4b900883 SCUMM: RA1: Split playInteractiveVideo
3fc5813aa3 SCUMM: RA1: Refactor menu handling and rendering
9abca5775a SCUMM: RA: Extract common audio helpers
088825f217 SCUMM: RA1: Extract common iterator code
915b9dab5f SCUMM: RA1: Refactor handleGameOpcode
54fe61ff8d SCUMM: RA1: Render number of pilots left correctly
dee67a5319 SCUMM: RA1: Add basic save/load support
9e6422b20a SCUMM: RA2: Avoid direct OSystem mouse call
Commit: a9cc4b1243e11fb01df409ec519eae05132f92bb
https://github.com/scummvm/scummvm/commit/a9cc4b1243e11fb01df409ec519eae05132f92bb
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:08:42+02:00
Commit Message:
SCUMM: RA2: Add initial level 1 loading code
Changed paths:
A engines/scumm/insane/insane_rebel.cpp
A engines/scumm/insane/insane_rebel.h
engines/scumm/detection.h
engines/scumm/detection_tables.h
engines/scumm/imuse_digi/dimuse_engine.cpp
engines/scumm/insane/insane.cpp
engines/scumm/insane/insane.h
engines/scumm/module.mk
engines/scumm/nut_renderer.cpp
engines/scumm/nut_renderer.h
engines/scumm/scumm-md5.h
engines/scumm/scumm.cpp
engines/scumm/scumm_v7.h
engines/scumm/smush/smush_player.cpp
engines/scumm/smush/smush_player.h
diff --git a/engines/scumm/detection.h b/engines/scumm/detection.h
index 52da1fb5d4b..fbf94c37023 100644
--- a/engines/scumm/detection.h
+++ b/engines/scumm/detection.h
@@ -221,6 +221,7 @@ enum ScummGameId {
GID_CMI,
GID_DIG,
GID_FT,
+ GID_REBEL2,
GID_INDY3,
GID_INDY4,
GID_LOOM,
diff --git a/engines/scumm/detection_tables.h b/engines/scumm/detection_tables.h
index 93916efc104..ced21e29990 100644
--- a/engines/scumm/detection_tables.h
+++ b/engines/scumm/detection_tables.h
@@ -72,6 +72,7 @@ static const PlainGameDescriptor gameDescriptions[] = {
{ "indyzak", "Indiana Jones and the Last Crusade & Zak McKracken" },
{ "zakloom", "Zak McKracken & Loom" },
{ "ft", "Full Throttle" },
+ { "rebel2", "Star Wars: Rebel Assault II: The Hidden Empire" },
{ "dig", "The Dig" },
{ "comi", "The Curse of Monkey Island" },
@@ -226,6 +227,9 @@ static const GameSettings gameVariantsTable[] = {
{"ft", "Remastered", 0, GID_FT, 7, 0, MDT_NONE, GF_DOUBLEFINE_PAK, UNK, GUIO5(GUIO_NOMIDI, GAMEOPTION_ENHANCEMENTS, GAMEOPTION_ORIGINALGUI, GAMEOPTION_LOWLATENCYAUDIO, GAMEOPTION_TTS)},
{"ft", "Demo", 0, GID_FT, 7, 0, MDT_NONE, GF_DEMO, UNK, GUIO4(GUIO_NOMIDI, GAMEOPTION_ORIGINALGUI, GAMEOPTION_LOWLATENCYAUDIO, GAMEOPTION_TTS)},
+ {"rebel2", "", 0, GID_REBEL2, 7, 0, MDT_NONE, 0, Common::kPlatformDOS, GUIO5(GUIO_NOMIDI, GAMEOPTION_ENHANCEMENTS, GAMEOPTION_ORIGINALGUI, GAMEOPTION_LOWLATENCYAUDIO, GAMEOPTION_TTS)},
+ {"rebel2", "Demo", 0, GID_REBEL2, 7, 0, MDT_NONE, GF_DEMO, Common::kPlatformDOS, GUIO5(GUIO_NOMIDI, GAMEOPTION_ENHANCEMENTS, GAMEOPTION_ORIGINALGUI, GAMEOPTION_LOWLATENCYAUDIO, GAMEOPTION_TTS)},
+
{"dig", "", 0, GID_DIG, 7, 0, MDT_NONE, 0, UNK, GUIO5(GUIO_NOMIDI, GAMEOPTION_ENHANCEMENTS, GAMEOPTION_ORIGINALGUI, GAMEOPTION_LOWLATENCYAUDIO, GAMEOPTION_TTS)},
{"dig", "Demo", 0, GID_DIG, 7, 0, MDT_NONE, GF_DEMO, UNK, GUIO4(GUIO_NOMIDI, GAMEOPTION_ORIGINALGUI, GAMEOPTION_LOWLATENCYAUDIO, GAMEOPTION_TTS)},
{"dig", "Steam", "steam", GID_DIG, 7, 0, MDT_NONE, 0, UNK, GUIO5(GUIO_NOMIDI, GAMEOPTION_ENHANCEMENTS, GAMEOPTION_ORIGINALGUI, GAMEOPTION_LOWLATENCYAUDIO, GAMEOPTION_TTS)},
@@ -494,6 +498,8 @@ static const GameFilenamePattern gameFilenamesTable[] = {
{ "ft", "Vollgas Data", kGenUnchanged, Common::DE_DEU, Common::kPlatformMacintosh, 0 },
{ "ft", "Vollgas Demo Data", kGenUnchanged, Common::DE_DEU, Common::kPlatformMacintosh, "Demo" },
+ { "rebel2", "REBEL2.EXE", kGenUnchanged, UNK_LANG, Common::kPlatformDOS, 0 },
+
{ "comi", "comi.la%d", kGenDiskNum, UNK_LANG, UNK, 0 },
{ "activity", "activity", kGenHEPC, UNK_LANG, UNK, 0 },
diff --git a/engines/scumm/imuse_digi/dimuse_engine.cpp b/engines/scumm/imuse_digi/dimuse_engine.cpp
index 54e22b002d1..3c21ee194d3 100644
--- a/engines/scumm/imuse_digi/dimuse_engine.cpp
+++ b/engines/scumm/imuse_digi/dimuse_engine.cpp
@@ -59,7 +59,7 @@ IMuseDigital::IMuseDigital(ScummEngine_v7 *scumm, int sampleRate, Audio::Mixer *
}
_splayer = nullptr;
- _isEarlyDiMUSE = (_vm->_game.id == GID_FT || (_vm->_game.id == GID_DIG && _vm->_game.features & GF_DEMO));
+ _isEarlyDiMUSE = (_vm->_game.id == GID_FT || (_vm->_game.id == GID_DIG && _vm->_game.features & GF_DEMO) || _vm->_game.id == GID_REBEL2);
if (_isEarlyDiMUSE) {
memset(_ftCrossfadeBuffer, 0, sizeof(_ftCrossfadeBuffer));
diff --git a/engines/scumm/insane/insane.cpp b/engines/scumm/insane/insane.cpp
index 5d8698ab3f9..e4ea4e0bece 100644
--- a/engines/scumm/insane/insane.cpp
+++ b/engines/scumm/insane/insane.cpp
@@ -184,7 +184,7 @@ void Insane::initvars() {
for (j = 0; j < 9; j++)
_enHdlVar[i][j] = 0;
- for (i = 0; i < 0x80; i++)
+ for (i = 0; i < 0x200; i++)
_iactBits[i] = 0;
@@ -1264,7 +1264,7 @@ void Insane::smlayer_soundSetPriority(int32 soundId, int32 priority) {
void Insane::smlayer_drawSomething(byte *renderBitmap, int32 codecparam,
int32 x, int32 y, int32 arg_10, NutRenderer *nutfile,
int32 c, int32 arg_1C, int32 arg_20) {
- nutfile->drawFrame(renderBitmap, c, x, y);
+ nutfile->drawFrame(renderBitmap, c, x, y, codecparam); // codecparam appears to be pitch
}
void Insane::smlayer_overrideDrawActorAt(byte *arg_0, byte arg_4, byte arg_8) {
@@ -1357,13 +1357,13 @@ void Insane::procSKIP(int32 subSize, Common::SeekableReadStream &b) {
}
bool Insane::isBitSet(int n) {
- assert (n < 0x80);
+ assert (n < 0x200);
return (_iactBits[n] != 0);
}
void Insane::setBit(int n) {
- assert (n < 0x80);
+ assert (n < 0x200);
_iactBits[n] = 1;
}
diff --git a/engines/scumm/insane/insane.h b/engines/scumm/insane/insane.h
index 4ec46f839f0..19382f85576 100644
--- a/engines/scumm/insane/insane.h
+++ b/engines/scumm/insane/insane.h
@@ -26,6 +26,9 @@
#include "scumm/smush/smush_player.h"
+#include "common/list.h"
+#include "common/rect.h"
+
namespace Scumm {
#define INV_CHAIN 0
@@ -49,18 +52,19 @@ namespace Scumm {
#define EN_BEN 9 // used only with handler
class Insane {
- public:
- Insane(ScummEngine_v7 *scumm);
- ~Insane();
+public:
+ Insane() {};
+ Insane(ScummEngine_v7 *scumm);
+ virtual ~Insane();
void setSmushParams(int speed);
void setSmushPlayer(SmushPlayer *player);
void runScene(int arraynum);
void procPreRendering();
- void procPostRendering(byte *renderBitmap, int32 codecparam, int32 setupsan12,
+ void virtual procPostRendering(byte *renderBitmap, int32 codecparam, int32 setupsan12,
int32 setupsan13, int32 curFrame, int32 maxFrame);
- void procIACT(byte *renderBitmap, int32 codecparam, int32 setupsan12,
+ void virtual procIACT(byte *renderBitmap, int32 codecparam, int32 setupsan12,
int32 setupsan13, Common::SeekableReadStream &b, int32 size, int32 flags, int16 par1,
int16 par2, int16 par3, int16 par4);
void procSKIP(int32 subSize, Common::SeekableReadStream &b);
@@ -69,7 +73,7 @@ class Insane {
bool isInsaneActive() { return _insaneIsRunning; }
void syncCurrentSanFlags();
- private:
+ protected:
ScummEngine_v7 *_vm;
SmushPlayer *_player;
@@ -154,7 +158,7 @@ class Insane {
int16 _smush_frameNum2;
byte _smush_earlyFluContents[0x31a];
int16 _enemyState[10][10];
- byte _iactBits[0x80];
+ byte _iactBits[0x200];
int16 _mainRoadPos;
int16 _posBrokenCar;
int16 _posBrokenTruck;
@@ -421,7 +425,7 @@ class Insane {
void actor10Reaction(int32 buttons);
int32 actionEnemy();
int32 processKeyboard();
- int32 processMouse();
+ int32 virtual processMouse();
void setEnemyAnimation(int32 actornum, int anim);
void chooseEnemyWeaponAnim(int32 buttons);
void switchEnemyWeapon();
@@ -447,12 +451,22 @@ class Insane {
void iactScene21(byte *renderBitmap, int32 codecparam, int32 setupsan12,
int32 setupsan13, Common::SeekableReadStream &b, int32 size, int32 flags,
int16 par1, int16 par2, int16 par3, int16 par4);
- bool isBitSet(int n);
- void setBit(int n);
+ bool virtual isBitSet(int n);
+ void virtual setBit(int n);
void clearBit(int n);
void chooseEnemy();
void removeEmptyEnemies();
void removeEnemyFromMetList(int32);
+
+ public:
+
+ bool virtual shouldSkipFrameUpdate(int left, int top, int width, int height) {
+ return false;
+ };
+
+ void virtual loadEmbeddedSan(int userId, byte *animData, int32 size, byte *renderBitmap) {
+ // Nothing by default
+ };
};
} // End of namespace Insane
diff --git a/engines/scumm/insane/insane_rebel.cpp b/engines/scumm/insane/insane_rebel.cpp
new file mode 100644
index 00000000000..f1d72aa578d
--- /dev/null
+++ b/engines/scumm/insane/insane_rebel.cpp
@@ -0,0 +1,822 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+
+
+#include "engines/engine.h"
+#include "common/system.h"
+#include "common/memstream.h"
+
+#include "scumm/actor.h"
+#include "scumm/file.h"
+#include "scumm/resource.h"
+#include "scumm/scumm_v7.h"
+#include "scumm/sound.h"
+
+#include "scumm/imuse/imuse.h"
+#include "scumm/imuse_digi/dimuse_engine.h"
+
+#include "scumm/smush/smush_player.h"
+#include "scumm/smush/smush_font.h"
+
+#include "scumm/insane/insane_rebel.h"
+
+
+namespace Scumm {
+
+InsaneRebel2::InsaneRebel2(ScummEngine_v7 *scumm) {
+ _vm = scumm;
+ // Rebel Assault 2 specific initialization can go here
+ _rebelHudPrimary = nullptr;
+ _rebelHudSecondary = nullptr;
+ _rebelHudCockpit = nullptr;
+ _rebelHudExplosion = nullptr;
+ _rebelHudDamage = nullptr;
+ _rebelHudEffects = nullptr;
+ _rebelHudHiRes = nullptr;
+
+ // Rebel Assault 2: Load cockpit sprites NUT which contains crosshairs, explosions, status bar
+ // CPITIMAG.NUT = low-res (320x200), CPITIMHI.NUT = high-res (640x480)
+ // For now, use CPITIMAG since the game runs at 320x200
+ _smush_iconsNut = new NutRenderer(_vm, "SYSTM/CPITIMAG.NUT");
+ _smush_icons2Nut = nullptr; // Not used for Rebel2
+ _smush_cockpitNut = new NutRenderer(_vm, "SYSTM/DISPFONT.NUT");
+
+ _rebelEnemies.clear();
+ _rebelHandler = 8; // Default to Handler 8 (ground vehicle) for Level 1
+ _rebelLevelType = 0; // Level type from Opcode 6 par3, determines HUD sprite variant
+}
+
+
+InsaneRebel2::~InsaneRebel2() {
+ delete _rebelHudPrimary;
+ delete _rebelHudSecondary;
+ delete _rebelHudCockpit;
+ delete _rebelHudExplosion;
+ delete _rebelHudDamage;
+ delete _rebelHudEffects;
+ delete _rebelHudHiRes;
+}
+
+
+int32 InsaneRebel2::processMouse() {
+ int32 buttons = 0;
+
+ // Get button state directly from event manager (SCUMM VARs aren't updated during SMUSH)
+ static bool wasPressed = false;
+ bool isPressed = (_vm->_system->getEventManager()->getButtonState() & 1) != 0;
+
+ // Edge detection: only trigger on button press (not hold)
+ if (isPressed && !wasPressed) {
+ Common::Point mousePos(_vm->_mouse.x, _vm->_mouse.y);
+ debug("Rebel2 Click: Mouse=(%d,%d) Enemies=%d",
+ mousePos.x, mousePos.y, _rebelEnemies.size());
+
+ // Check for hit on any active enemy
+ Common::List<RebelEnemy>::iterator it;
+ for (it = _rebelEnemies.begin(); it != _rebelEnemies.end(); ++it) {
+ debug(" Enemy ID=%d active=%d destroyed=%d rect=(%d,%d)-(%d,%d) contains=%d",
+ it->id, it->active, it->destroyed,
+ it->rect.left, it->rect.top, it->rect.right, it->rect.bottom,
+ it->rect.contains(mousePos));
+
+ if (it->active && it->rect.contains(mousePos)) {
+ // Enemy hit!
+ it->active = false;
+ it->destroyed = true; // Mark as destroyed so IACT won't re-activate
+ it->explosionFrame = 0; // Start explosion animation
+ it->explosionComplete = false; // Explosion not yet finished
+ debug("Rebel2: HIT enemy ID=%d at (%d,%d) - Rect: (%d,%d)-(%d,%d)",
+ it->id, mousePos.x, mousePos.y,
+ it->rect.left, it->rect.top, it->rect.right, it->rect.bottom);
+
+ // Note: Background saving and masking is handled in procPostRendering
+ // where we have access to the render bitmap
+ // TODO: Play explosion sound
+ // TODO: Update score
+ // Only hit one enemy per click
+ break;
+ }
+ }
+ }
+ wasPressed = isPressed;
+ return buttons;
+}
+
+bool InsaneRebel2::shouldSkipFrameUpdate(int left, int top, int width, int height) {
+ // Only check for Rebel2
+ if (_vm->_game.id != GID_REBEL2) {
+ return false;
+ }
+
+ Common::Rect updateRect(left, top, left + width, top + height);
+ int updateArea = width * height;
+
+ // Check if this update region significantly overlaps with any destroyed enemy
+ Common::List<RebelEnemy>::iterator it;
+ for (it = _rebelEnemies.begin(); it != _rebelEnemies.end(); ++it) {
+ if (it->destroyed) {
+ // Calculate the intersection of the update rect and enemy rect
+ Common::Rect enemyRect = it->rect;
+
+ if (updateRect.intersects(enemyRect)) {
+ // Calculate the intersection area
+ int intLeft = MAX(updateRect.left, enemyRect.left);
+ int intTop = MAX(updateRect.top, enemyRect.top);
+ int intRight = MIN(updateRect.right, enemyRect.right);
+ int intBottom = MIN(updateRect.bottom, enemyRect.bottom);
+ int intArea = (intRight - intLeft) * (intBottom - intTop);
+
+ // Require at least 70% overlap to skip the update
+ // This prevents unrelated frame updates from being incorrectly skipped
+ if (intArea * 100 >= updateArea * 70) {
+ debug("Rebel2: Skipping frame update (%d,%d %dx%d) - %d%% overlap with destroyed enemy ID=%d",
+ left, top, width, height, (intArea * 100) / updateArea, it->id);
+ return true;
+ }
+ }
+ }
+ }
+
+ return false;
+}
+
+bool InsaneRebel2::isBitSet(int n) {
+ assert (n < 0x200);
+
+ return (_iactBits[n] != 0);
+}
+
+void InsaneRebel2::setBit(int n) {
+ assert (n < 0x200);
+
+ _iactBits[n] = 1;
+}
+
+void InsaneRebel2::procIACT(byte *renderBitmap, int32 codecparam, int32 setupsan12,
+ int32 setupsan13, Common::SeekableReadStream &b, int32 size, int32 flags,
+ int16 par1, int16 par2, int16 par3, int16 par4) {
+ if (_keyboardDisable)
+ return;
+
+ if (_currSceneId == 1)
+ iactRebel2Scene1(renderBitmap, codecparam, setupsan12, setupsan13, b, size, flags, par1, par2, par3, par4);
+}
+
+
+void InsaneRebel2::iactRebel2Scene1(byte *renderBitmap, int32 codecparam, int32 setupsan12,
+ int32 setupsan13, Common::SeekableReadStream &b, int32 size, int32 flags,
+ int16 par1, int16 par2, int16 par3, int16 par4) {
+ // par1 is the Opcode (word at offset +0)
+ // par2 is word at offset +2
+ // par3 is word at offset +4
+ // par4 is word at offset +6
+ //
+ // Based on disassembly of FUN_4028C5 and FUN_4033CF:
+ //
+ // For IACT opcode 4 (enemy position update), the structure is:
+ // Offset +0x06: Type/SubType (par3)
+ // Offset +0x08: Enemy ID
+ // Offset +0x0A: X position
+ // Offset +0x0C: Y position
+ // Offset +0x0E: Width
+ // Offset +0x10: Height
+ //
+ // The original game calculates bounding box center:
+ // centerX = X + (Width / 2)
+ // centerY = Y + (Height / 2)
+ // Then subtracts scroll offsets:
+ // screenX = centerX - DAT_0043e006 (scrollX)
+ // screenY = centerY - DAT_0043e008 (scrollY)
+
+ if (par1 == 4) {
+ // Opcode 4: Enemy position update
+ // Read 5 shorts from the stream (offset +8 through +16)
+ int16 enemyId = b.readSint16LE(); // Offset +8
+ int16 x = b.readSint16LE(); // Offset +10 (0x0A)
+ int16 y = b.readSint16LE(); // Offset +12 (0x0C)
+ int16 w = b.readSint16LE(); // Offset +14 (0x0E) - Width
+ int16 h = b.readSint16LE(); // Offset +16 (0x10) - Height
+
+ // The disassembly shows half-width/half-height are used for centering:
+ // halfW = w >> 1
+ // halfH = h >> 1
+ // centerX = x + halfW
+ // centerY = y + halfH
+ // But for drawing the bounding box, we want the top-left corner (x, y) and full dimensions.
+
+ //debug("Rebel2 IACT Opcode 4: ID=%d X=%d Y=%d W=%d H=%d (par2=%d par3=%d par4=%d)",
+ // enemyId, x, y, w, h, par2, par3, par4);
+
+ // Update RebelEnemy list for hit detection
+ bool found = false;
+ Common::List<RebelEnemy>::iterator it;
+ for (it = _rebelEnemies.begin(); it != _rebelEnemies.end(); ++it) {
+ if (it->id == enemyId) {
+ it->rect = Common::Rect(x, y, x + w, y + h);
+ // Only re-activate if not destroyed
+ if (!it->destroyed) {
+ it->active = true;
+ }
+ found = true;
+ break;
+ }
+ }
+ if (!found) {
+ RebelEnemy e;
+ e.id = enemyId;
+ e.rect = Common::Rect(x, y, x + w, y + h);
+ e.active = true;
+ e.destroyed = false;
+ e.explosionFrame = -1; // No explosion playing
+ _rebelEnemies.push_back(e);
+ }
+
+ // Render Bounding Box for visual verification
+ // Use player dimensions for clipping
+ int bufWidth = (_player && _player->_width > 0) ? _player->_width : 320;
+ int bufHeight = (_player && _player->_height > 0) ? _player->_height : 200;
+ int pitch = bufWidth;
+
+ // Draw the bounding box in color 255 (bright white) - only for non-destroyed enemies
+ // Skip drawing if this enemy has been destroyed
+ if (renderBitmap && w > 0 && h > 0 && !found) {
+ // Only draw for newly created enemies (not destroyed ones)
+ drawRect(renderBitmap, pitch, x, y, w, h, 255, bufWidth, bufHeight);
+ } else if (found && !it->destroyed) {
+ // Draw for existing non-destroyed enemies
+ drawRect(renderBitmap, pitch, x, y, w, h, 255, bufWidth, bufHeight);
+ }
+
+ // Debug for potential HUD Setup (Opcode 4 with word at 8 == 1)
+ if (enemyId == 1) {
+ debug("Rebel2 IACT Opcode 4: HUD Setup Candidate? (enemyId/val at 8 == 1). par4=%d", par4);
+ }
+
+ } else if (par1 == 2) {
+ // Opcode 2: Often used for bit setting
+ // Disassembly shows this is handled but we don't have full context
+ debug("Rebel2 IACT Opcode 2: par2=%d par3=%d par4=%d", par2, par3, par4);
+
+ } else if (par1 == 3) {
+ // Opcode 3: Often used for clearing/resetting
+ debug("Rebel2 IACT Opcode 3: par2=%d par3=%d par4=%d", par2, par3, par4);
+
+ } else if (par1 == 5) {
+ // Opcode 5: Special handling based on par2 value
+ // Disassembly shows sub-opcodes 0xD (13) and 0xE (14)
+ debug("Rebel2 IACT Opcode 5: par2=%d par3=%d par4=%d", par2, par3, par4);
+
+ } else if (par1 == 6) {
+ // Opcode 6: Scene trigger / mode switch
+ // Disassembly shows it sets DAT_0047ee84 (handler type) to par2 for values 7, 8, 0x19, 0x26
+ // This determines which rendering handler and crosshair sprite to use
+ // par3 is stored as DAT_004436de (level type) which affects HUD sprite selection
+
+ // Update handler type if par2 is a known handler value
+ if (par2 == 7 || par2 == 8 || par2 == 0x19 || par2 == 0x26) {
+ _rebelHandler = par2;
+ _rebelLevelType = par3; // Store level type (affects HUD sprite: 5 vs 53)
+ debug("Rebel2 IACT Opcode 6: Setting handler=%d levelType=%d (par4=%d)", par2, par3, par4);
+ }
+
+ // Check for Status Bar Enable trigger (par4 == 1 means enable status bar drawing)
+ // Based on FUN_407FCB line 79: if (param_5[4] == 1) { FUN_0040bb87(...) }
+ // Note: Sprite 5 is a 136x13 status bar, NOT the full cockpit (which is baked into video)
+ if (par4 == 1) {
+ debug("Rebel2 IACT Opcode 6: Status Bar ENABLED - will draw sprite %d (136x13) from CPITIMAG.NUT",
+ (_rebelLevelType == 5) ? 53 : 5);
+ }
+
+ // Debug: Confirm handler type after update
+ if (_rebelHandler == 0x26) {
+ debug("Rebel2: Handler set to TURRET (0x26/38) - status bar sprite 5, crosshair sprites 48+");
+ }
+
+ } else if (par1 == 8) {
+ // TODO
+ } else if (par1 == 9) {
+ // Opcode 9: Text/subtitle display
+ debug("Rebel2 IACT Opcode 9: par2=%d par3=%d par4=%d (text)", par2, par3, par4);
+
+ } else if (par1 == 0 || par1 == 1) {
+ // Low Opcodes seen in logs
+ debug("Rebel2 IACT: Low Opcode %d (par2=%d par3=%d par4=%d)", par1, par2, par3, par4);
+ } else {
+ debug("Rebel2 IACT: Unknown Opcode %d (par2=%d par3=%d par4=%d)", par1, par2, par3, par4);
+ }
+}
+
+void InsaneRebel2::loadEmbeddedSan(int userId, byte *animData, int32 size, byte *renderBitmap) {
+ // Validate userId (1-4 for HUD slots)
+ if (userId < 1 || userId > 4 || !animData || size < 32) {
+ debug("Rebel2: Invalid embedded SAN: userId=%d, size=%d", userId, size);
+ return;
+ }
+
+ // Parse the embedded ANIM structure
+ // Format: ANIM (4 bytes) + size (4 bytes BE) + AHDR (4 bytes) + size (4 bytes BE) + ...
+ Common::MemoryReadStream stream(animData, size);
+
+ // Read ANIM header
+ uint32 animTag = stream.readUint32BE();
+ if (animTag != MKTAG('A','N','I','M')) {
+ debug("Rebel2: Embedded SAN missing ANIM tag, got 0x%08X", animTag);
+ return;
+ }
+ uint32 animSize = stream.readUint32BE();
+ debug("Rebel2: Parsing embedded ANIM: userId=%d, reported size=%u, actual=%d", userId, animSize, size - 8);
+
+ // Read AHDR header
+ uint32 ahdrTag = stream.readUint32BE();
+ if (ahdrTag != MKTAG('A','H','D','R')) {
+ debug("Rebel2: Embedded SAN missing AHDR tag, got 0x%08X", ahdrTag);
+ return;
+ }
+ uint32 ahdrSize = stream.readUint32BE();
+
+ // Skip AHDR content (palette etc)
+ stream.skip(ahdrSize);
+ if (ahdrSize & 1) stream.skip(1); // Padding
+
+ // Look for first FRME
+ while (!stream.eos() && stream.pos() < size) {
+ uint32 tag = stream.readUint32BE();
+ uint32 chunkSize = stream.readUint32BE();
+
+ if (tag == MKTAG('F','R','M','E')) {
+ debug("Rebel2: Found FRME in embedded SAN, size=%u", chunkSize);
+
+ // Parse FRME content to find FOBJ
+ int64 frmeEnd = stream.pos() + chunkSize;
+
+ while (stream.pos() < frmeEnd && !stream.eos()) {
+ uint32 subTag = stream.readUint32BE();
+ uint32 subSize = stream.readUint32BE();
+
+ if (subTag == MKTAG('F','O','B','J')) {
+ debug("Rebel2: Found FOBJ in embedded SAN, size=%u", subSize);
+
+ // Read FOBJ header
+ int codec = stream.readUint16LE();
+ int left = stream.readUint16LE();
+ int top = stream.readUint16LE();
+ int width = stream.readUint16LE();
+ int height = stream.readUint16LE();
+ stream.readUint16LE(); // unknown
+ stream.readUint16LE(); // unknown
+
+ debug("Rebel2: Embedded HUD frame: userId=%d, %dx%d at (%d,%d), codec=%d",
+ userId, width, height, left, top, codec);
+
+ // Allocate storage for the decoded frame
+ EmbeddedSanFrame &frame = _rebelEmbeddedHud[userId];
+ frame.clear();
+
+ if (width > 0 && height > 0 && width <= 800 && height <= 480) {
+ frame.pixels = (byte *)calloc(width * height, 1);
+ frame.width = width;
+ frame.height = height;
+
+ // Use the left/top from FOBJ header as render position
+ // These are the exact screen coordinates for the HUD overlay
+ // The FOBJ header specifies where this frame should be composited
+ frame.renderX = left;
+ frame.renderY = top;
+
+ // Read the raw FOBJ data
+ int32 dataSize = subSize - 14;
+ if (dataSize > 0) {
+ byte *fobjData = (byte *)malloc(dataSize);
+ stream.read(fobjData, dataSize);
+
+ // Decode based on codec
+ // Codec 1 = RLE, Codec 3 = raw, Codec 21 = line update, Codec 45 = block delta
+ if (codec == 1) {
+ // RLE decode using SMUSH BOMP format
+ // Each line has a 16-bit size header, then BOMP-encoded data
+ // BOMP format: code byte, then data
+ // code & 1 == 1: fill mode, next byte repeated (code>>1)+1 times
+ // code & 1 == 0: copy mode, copy next (code>>1)+1 bytes
+ byte *srcPtr = fobjData;
+ for (int row = 0; row < height && srcPtr < fobjData + dataSize; row++) {
+ int lineSize = READ_LE_UINT16(srcPtr);
+ srcPtr += 2;
+ byte *lineEnd = srcPtr + lineSize;
+ byte *lineDst = frame.pixels + row * width;
+ int remaining = width;
+
+ while (srcPtr < lineEnd && remaining > 0) {
+ byte code = *srcPtr++;
+ int num = (code >> 1) + 1;
+ if (num > remaining)
+ num = remaining;
+ remaining -= num;
+
+ if (code & 1) {
+ // Fill mode: repeat next byte
+ byte color = (srcPtr < lineEnd) ? *srcPtr++ : 0;
+ memset(lineDst, color, num);
+ lineDst += num;
+ } else {
+ // Copy mode: copy next bytes
+ for (int i = 0; i < num && srcPtr < lineEnd; i++) {
+ *lineDst++ = *srcPtr++;
+ }
+ }
+ }
+ srcPtr = lineEnd;
+ }
+ frame.valid = true;
+ debug("Rebel2: Decoded embedded HUD (codec 1/RLE): %dx%d", width, height);
+ } else if (codec == 3 || codec == 20) {
+ // Uncompressed - direct copy
+ int copySize = MIN(dataSize, width * height);
+ memcpy(frame.pixels, fobjData, copySize);
+ frame.valid = true;
+ debug("Rebel2: Decoded embedded HUD (codec %d/raw): %dx%d", codec, width, height);
+ } else if (codec == 21 || codec == 44) {
+ // Codec 21/44: Line update codec
+ // Format: For each line:
+ // 16-bit line data size
+ // Line data: pairs of (16-bit skip, 16-bit count-1, pixels[count])
+ byte *srcPtr = fobjData;
+ for (int row = 0; row < height && srcPtr < fobjData + dataSize; row++) {
+ int lineDataSize = READ_LE_UINT16(srcPtr);
+ srcPtr += 2;
+ byte *lineEnd = srcPtr + lineDataSize;
+ byte *lineDst = frame.pixels + row * width;
+ int x = 0;
+ while (srcPtr < lineEnd && x < width) {
+ int skip = READ_LE_UINT16(srcPtr);
+ srcPtr += 2;
+ x += skip;
+ if (srcPtr >= lineEnd) break;
+ int count = READ_LE_UINT16(srcPtr) + 1;
+ srcPtr += 2;
+ while (count-- > 0 && x < width && srcPtr < lineEnd) {
+ lineDst[x++] = *srcPtr++;
+ }
+ }
+ srcPtr = lineEnd;
+ }
+ frame.valid = true;
+ debug("Rebel2: Decoded embedded HUD (codec 21/line update): %dx%d", width, height);
+ } else if (codec == 45) {
+ // Codec 45: Similar to codec 47 but simpler - block delta
+ // For embedded HUD use, try simple raw copy as fallback
+ // This codec appears in small sprites (11x26, 17x53)
+ int copySize = MIN(dataSize, width * height);
+ memcpy(frame.pixels, fobjData, copySize);
+ frame.valid = true;
+ debug("Rebel2: Decoded embedded HUD (codec 45/block): %dx%d", width, height);
+ } else {
+ debug("Rebel2: TODO: Decode codec %d for embedded HUD", codec);
+ // For now, mark as invalid but keep dimensions
+ frame.valid = false;
+ }
+
+ // Draw immediately to renderBitmap if valid
+ if (frame.valid && renderBitmap) {
+ int pitch = (_player && _player->_width > 0) ? _player->_width : 320;
+ int bufHeight = (_player && _player->_height > 0) ? _player->_height : 200;
+
+ for (int y = 0; y < height && (frame.renderY + y) < bufHeight; y++) {
+ for (int x = 0; x < width && (frame.renderX + x) < pitch; x++) {
+ byte pixel = frame.pixels[y * width + x];
+ if (pixel != 0) { // 0 = transparent
+ int destX = frame.renderX + x;
+ int destY = frame.renderY + y;
+ if (destX >= 0 && destY >= 0) {
+ renderBitmap[destY * pitch + destX] = pixel;
+ }
+ }
+ }
+ }
+ debug("Rebel2: Rendered embedded HUD %d at (%d,%d)", userId, frame.renderX, frame.renderY);
+ }
+
+ free(fobjData);
+ }
+ }
+
+ return; // Found and processed FOBJ
+ } else {
+ // Skip other sub-chunks
+ stream.skip(subSize);
+ if (subSize & 1) stream.skip(1);
+ }
+ }
+ } else {
+ // Skip non-FRME chunks
+ stream.skip(chunkSize);
+ if (chunkSize & 1) stream.skip(1);
+ }
+ }
+
+ debug("Rebel2: No FOBJ found in embedded SAN userId=%d", userId);
+}
+
+
+void InsaneRebel2::procPostRendering(byte *renderBitmap, int32 codecparam, int32 setupsan12,
+ int32 setupsan13, int32 curFrame, int32 maxFrame) {
+
+ processMouse();
+
+ // Determine correct pitch for the video buffer (usually 320 for Rebel2)
+ int width = _player->_width;
+ int height = _player->_height;
+ if (width == 0) width = _vm->_screenWidth;
+ if (height == 0) height = _vm->_screenHeight;
+ int pitch = width;
+
+ // --- HUD Drawing Order (from FUN_004089ab assembly analysis) ---
+ // Based on FUN_004089ab:
+ // 1. Line 156: FUN_004288c0 fills status bar background at Y=0xb4 (180)
+ // 2. Lines 171-226: Draw turret overlays, targeting reticle, crosshair
+ // 3. Line 243: FUN_0041c012 draws status bar sprites LAST (on top)
+ //
+ // In FUN_0041c012:
+ // - Sprites are drawn to buffer DAT_00482204 at position (0,0)
+ // - Buffer is composited at Y=0xb4 (180) via FUN_0042f780
+ // - DISPFONT.NUT (DAT_00482200) sprites 1-7 contain the status bar elements
+ //
+ // For ScummVM, we draw directly to screen at Y=180
+
+ // Use video content coordinates, NOT buffer coordinates
+ const int videoWidth = 320; // Native video width
+ const int videoHeight = 200; // Native video height
+ const int statusBarY = 180; // 0xb4 - status bar starts at Y=180 in video coords
+
+ // ============================================================
+ // STEP 0: Fill status bar background (FUN_004288c0 equivalent)
+ // ============================================================
+ // Clear the status bar area at Y=180-199 with background color
+ // Original assembly: FUN_004288c0(local_8, 0, 0, 0xb4, 0x140, 0x14, 4)
+ // This fills width=320, height=20 starting at Y=180 with color index 4
+ const byte statusBarBgColor = 4;
+ for (int y = statusBarY; y < videoHeight && y < height; y++) {
+ for (int x = 0; x < videoWidth && x < pitch; x++) {
+ renderBitmap[y * pitch + x] = statusBarBgColor;
+ }
+ }
+
+ // ============================================================
+ // STEP 1: Draw embedded SAN cockpit overlay FIRST (from IACT chunks)
+ // ============================================================
+ // The cockpit overlay forms the decorative frame at the bottom
+ // userId 1: Left piece at X=0
+ // userId 2: Right piece at X=slot1Width
+ // These are drawn BEFORE the status bar so status bar appears on top
+
+ int slot1Width = 0;
+ if (_rebelEmbeddedHud[1].valid && _rebelEmbeddedHud[1].width > 0) {
+ slot1Width = _rebelEmbeddedHud[1].width;
+ }
+
+ for (int hudSlot = 1; hudSlot <= 2; hudSlot++) {
+ EmbeddedSanFrame &frame = _rebelEmbeddedHud[hudSlot];
+ if (frame.valid && frame.pixels && frame.width > 0 && frame.height > 0) {
+ int destX, destY;
+
+ // Position the two HUD pieces horizontally adjacent
+ if (hudSlot == 1) {
+ destX = 0;
+ } else {
+ destX = slot1Width;
+ }
+
+ // Position at bottom of 320x200 video content
+ // The cockpit overlay sits at Y = 200 - frameHeight
+ destY = statusBarY - frame.height;
+ if (destY < 0) destY = 0;
+
+ // Draw frame with transparency (pixel 0 = transparent)
+ for (int y = 0; y < frame.height && (destY + y) < height; y++) {
+ for (int x = 0; x < frame.width && (destX + x) < pitch; x++) {
+ byte pixel = frame.pixels[y * frame.width + x];
+ if (pixel != 0) { // Skip transparent pixels
+ int fx = destX + x;
+ int fy = destY + y;
+ if (fx >= 0 && fy >= 0) {
+ renderBitmap[fy * pitch + fx] = pixel;
+ }
+ }
+ }
+ }
+
+ static bool debugEmbeddedOnce[5] = {false};
+ if (!debugEmbeddedOnce[hudSlot]) {
+ debug("Rebel2: Drawing embedded HUD slot %d (%dx%d) at (%d,%d)",
+ hudSlot, frame.width, frame.height, destX, destY);
+ debugEmbeddedOnce[hudSlot] = true;
+ }
+ }
+ }
+
+ // ============================================================
+ // STEP 2: Draw DISPFONT.NUT status bar sprites (FUN_0041c012 equivalent)
+ // ============================================================
+ // DISPFONT.NUT contains status bar elements - drawn ON TOP of cockpit overlay
+ // From assembly FUN_0041c012:
+ // - Sprite 1: Status bar background frame (full width, drawn at 0,0)
+ // - Sprites 2-5: Difficulty stars (1-4 stars, drawn at 0,0)
+ // - Sprite 6: Shield bar fill (drawn with clip rect X=0x3f, Y=9, W=64, H=6)
+ // - Sprite 7: Shield alert (flashing red when shields critical)
+ //
+ // All sprites are drawn to buffer at (0,0), buffer composited at Y=180
+ // For ScummVM, we draw directly at Y=statusBarY
+ if (_smush_cockpitNut) {
+ // Debug: Log DISPFONT.NUT sprite info once
+ static bool loggedDispfont = false;
+ if (!loggedDispfont) {
+ int numSprites = _smush_cockpitNut->getNumChars();
+ debug("Rebel2: DISPFONT.NUT has %d sprites, statusBarY=%d:", numSprites, statusBarY);
+ for (int i = 0; i < numSprites && i < 10; i++) {
+ int sw = _smush_cockpitNut->getCharWidth(i);
+ int sh = _smush_cockpitNut->getCharHeight(i);
+ debug(" Sprite %d: %dx%d", i, sw, sh);
+ }
+ loggedDispfont = true;
+ }
+
+ // Draw status bar background frame (sprite 1) at (0, statusBarY)
+ // This sprite is the full-width status bar background
+ if (_smush_cockpitNut->getNumChars() > 1) {
+ smlayer_drawSomething(renderBitmap, pitch, 0, statusBarY, 0, _smush_cockpitNut, 1, 0, 0);
+ }
+
+ // Draw difficulty indicator (sprites 2-5 based on difficulty level 0-3)
+ // Sprite index = difficulty + 2; capped at 4 max difficulty (sprite 5)
+ // Assembly draws at (0,0) in buffer - same position as sprite 1
+ int difficulty = 0; // TODO: Read from game state (DAT_0047a7fa)
+ if (difficulty > 3) difficulty = 3;
+ int difficultySprite = difficulty + 2; // sprites 2, 3, 4, or 5
+ if (_smush_cockpitNut->getNumChars() > difficultySprite) {
+ smlayer_drawSomething(renderBitmap, pitch, 0, statusBarY, 0, _smush_cockpitNut, difficultySprite, 0, 0);
+ }
+
+ // Draw shield bar (sprite 6)
+ // Assembly uses clip rect: X=0x3f(63), Y=0x9(9), W=0x40(64), H=0x6(6)
+ // The width is scaled based on shield value (param_1 >> 2)
+ // For now, draw at position (0, statusBarY) - sprite has internal positioning
+ if (_smush_cockpitNut->getNumChars() > 6) {
+ smlayer_drawSomething(renderBitmap, pitch, 0, statusBarY, 0, _smush_cockpitNut, 6, 0, 0);
+ }
+
+ // Draw shield alert overlay (sprite 7) when shields critical (> 0xAA = 170)
+ // Only draws when frame counter bit 3 is clear (every 8 frames)
+ // For now, skip - TODO: implement shield critical flashing
+
+ // Draw lives indicator - assembly shows at X=0xa8 (168), Y=7
+ // Uses sprite 1 again with different clip rect
+ // TODO: Implement lives rendering
+
+ // Draw score - uses FUN_00434cb0 (text rendering) at X=0x101(257)
+ // TODO: Implement score rendering
+ } else {
+ static bool warnedNullOnce = false;
+ if (!warnedNullOnce) {
+ debug("Rebel2: WARNING - _smush_cockpitNut (DISPFONT.NUT) is null!");
+ warnedNullOnce = true;
+ }
+ }
+
+ // Debug: Draw bounding boxes for enemies
+ // width/height/pitch already calculated above
+
+ // Debug: Verify buffer format and drawing capability
+ static uint32 lastDebugTime = 0;
+ if ((_vm->_system->getMillis() - lastDebugTime) > 2000) {
+ lastDebugTime = _vm->_system->getMillis();
+ debug("Rebel2 Debug: Buffer %dx%d, Pitch %d, BPP %d, Enemies: %d", width, height, pitch, _vm->_virtscr[kMainVirtScreen].format.bytesPerPixel, _rebelEnemies.size());
+ }
+
+ Common::List<RebelEnemy>::iterator it;
+ for (it = _rebelEnemies.begin(); it != _rebelEnemies.end(); ++it) {
+ Common::Rect r = it->rect;
+
+ // Clip the rect to screen bounds for safety
+ Common::Rect clipped = r;
+ clipped.clip(Common::Rect(0, 0, width, height));
+
+ if (!clipped.isValidRect()) continue;
+
+ if (it->destroyed) {
+ // Handle destroyed enemies - draw explosion animation
+ // The enemy frame updates are now skipped in decodeFrameObject via shouldSkipFrameUpdate
+
+ // Draw explosion animation - only while animation is in progress
+ // CPITIMAG.NUT indices 9-41 contain explosion frames, but we only use 8 for a quick effect
+ const int EXPLOSION_FRAMES = 8;
+ if (it->explosionFrame >= 0 && it->explosionFrame < EXPLOSION_FRAMES) {
+ if (_smush_iconsNut) {
+ int explosionSpriteIndex = 9 + it->explosionFrame;
+ if (_smush_iconsNut->getNumChars() > explosionSpriteIndex) {
+ int ew = _smush_iconsNut->getCharWidth(explosionSpriteIndex);
+ int eh = _smush_iconsNut->getCharHeight(explosionSpriteIndex);
+ // Center explosion on the enemy bounding box
+ int cx = (r.left + r.right) / 2 - ew / 2;
+ int cy = (r.top + r.bottom) / 2 - eh / 2;
+ smlayer_drawSomething(renderBitmap, pitch, cx, cy, 0, _smush_iconsNut, explosionSpriteIndex, 0, 0);
+ }
+ }
+
+ // Advance explosion frame
+ it->explosionFrame++;
+
+ // Check if explosion is now complete
+ if (it->explosionFrame >= EXPLOSION_FRAMES) {
+ it->explosionFrame = -1; // Mark as done - prevents any further animation
+ it->explosionComplete = true;
+ debug("Rebel2: Explosion complete for enemy ID=%d", it->id);
+ }
+ }
+ // After explosion is complete (explosionFrame == -1), do nothing
+ } else if (it->active) {
+ // Draw bounding box outline for active enemies (debug visualization)
+ // Draw Top
+ if (r.top >= 0 && r.top < height) {
+ for (int x = clipped.left; x < clipped.right; x++) {
+ renderBitmap[r.top * pitch + x] = 255;
+ }
+ }
+ // Draw Bottom
+ if (r.bottom > 0 && r.bottom <= height) {
+ for (int x = clipped.left; x < clipped.right; x++) {
+ renderBitmap[(r.bottom - 1) * pitch + x] = 255;
+ }
+ }
+ // Draw Left
+ if (r.left >= 0 && r.left < width) {
+ for (int y = clipped.top; y < clipped.bottom; y++) {
+ renderBitmap[y * pitch + r.left] = 255;
+ }
+ }
+ // Draw Right
+ if (r.right > 0 && r.right <= width) {
+ for (int y = clipped.top; y < clipped.bottom; y++) {
+ renderBitmap[y * pitch + (r.right - 1)] = 255;
+ }
+ }
+ }
+ }
+
+ // Draw Crosshair/Reticle cursor
+ // Sprite indices based on handler type (from original game disassembly):
+ // - Handler 8 (ground vehicle): Index 0x2E (46)
+ // - Handler 7 (space flight): Index 0x2F (47)
+ // - Handler 0x19 (mixed/turret view): Index 0x2F (47)
+ // - Handler 0x26 (full turret): Index 0x30+ (48+) with animation
+ if (_smush_iconsNut) {
+ //CursorMan.showMouse(false);
+ int reticleIndex;
+ switch (_rebelHandler) {
+ case 7: // Space flight
+ case 0x19: // Mixed/turret view
+ reticleIndex = 47; // 0x2F
+ break;
+ case 0x26: // Full turret (with animation - simplified for now)
+ reticleIndex = 48; // 0x30
+ break;
+ case 8: // Ground vehicle (Level 1) - FALLTHROUGH
+ default:
+ reticleIndex = 46; // 0x2E
+ break;
+ }
+ if (_smush_iconsNut->getNumChars() > reticleIndex) {
+ int cw = _smush_iconsNut->getCharWidth(reticleIndex);
+ int ch = _smush_iconsNut->getCharHeight(reticleIndex);
+
+ static bool debugCrosshairOnce = false;
+ if (!debugCrosshairOnce) {
+ debug("Rebel2: Drawing crosshair sprite %d at (%d,%d) size %dx%d",
+ reticleIndex, _vm->_mouse.x - cw / 2, _vm->_mouse.y - ch / 2, cw, ch);
+ debugCrosshairOnce = true;
+ }
+
+ // Center the crosshair on mouse position
+ smlayer_drawSomething(renderBitmap, pitch, _vm->_mouse.x - cw / 2, _vm->_mouse.y - ch / 2, 0, _smush_iconsNut, reticleIndex, 0, 0);
+ }
+ }
+}
+
+}
diff --git a/engines/scumm/insane/insane_rebel.h b/engines/scumm/insane/insane_rebel.h
new file mode 100644
index 00000000000..00b41865c06
--- /dev/null
+++ b/engines/scumm/insane/insane_rebel.h
@@ -0,0 +1,171 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#if !defined(SCUMM_INSANE_REBEL_H) && defined(ENABLE_SCUMM_7_8)
+#define SCUMM_INSANE_REBEL_H
+
+#include "scumm/nut_renderer.h"
+
+#include "scumm/smush/smush_player.h"
+
+#include "scumm/insane/insane.h"
+
+#include "common/list.h"
+#include "common/rect.h"
+
+namespace Scumm {
+
+class InsaneRebel2 : public Insane {
+
+ // Helper for debugging IACT boxes
+ void drawRect(byte *dst, int pitch, int x, int y, int w, int h, byte color, int bufWidth, int bufHeight) {
+ Common::Rect r(x, y, x + w, y + h);
+ Common::Rect screen(0, 0, bufWidth, bufHeight);
+ Common::Rect clipped = r;
+ clipped.clip(screen);
+
+ if (!clipped.isValidRect()) return;
+
+ // Top
+ if (r.top >= 0 && r.top < bufHeight) {
+ int startX = MAX((int)r.left, 0);
+ int endX = MIN((int)r.right, bufWidth);
+ for (int k = startX; k < endX; k++) dst[r.top * pitch + k] = color;
+ }
+ // Bottom
+ if (r.bottom > 0 && r.bottom <= bufHeight) {
+ int startX = MAX((int)r.left, 0);
+ int endX = MIN((int)r.right, bufWidth);
+ for (int k = startX; k < endX; k++) dst[(r.bottom - 1) * pitch + k] = color;
+ }
+ // Left
+ if (r.left >= 0 && r.left < bufWidth) {
+ int startY = MAX((int)r.top, 0);
+ int endY = MIN((int)r.bottom, bufHeight);
+ for (int k = startY; k < endY; k++) dst[k * pitch + r.left] = color;
+ }
+ // Right
+ if (r.right > 0 && r.right <= bufWidth) {
+ int startY = MAX((int)r.top, 0);
+ int endY = MIN((int)r.bottom, bufHeight);
+ for (int k = startY; k < endY; k++) dst[k * pitch + (r.right - 1)] = color;
+ }
+ }
+
+public:
+ InsaneRebel2(ScummEngine_v7 *scumm);
+ ~InsaneRebel2();
+
+ NutRenderer *_smush_cockpitNut;
+ NutRenderer *_smush_dispfontNut; // DAT_00482200 - DISPFONT.NUT for status bar (difficulty, shields, lives, score)
+
+ // Rebel Assault 2: Dynamically loaded HUD overlays (from CHK scripts)
+ // These correspond to the original game's global variables
+ NutRenderer *_rebelHudPrimary; // DAT_00482240 - Primary HUD overlay
+ NutRenderer *_rebelHudSecondary; // DAT_00482238 - Secondary HUD graphics
+ NutRenderer *_rebelHudCockpit; // DAT_00482268 - Ship cockpit frame overlay
+ NutRenderer *_rebelHudExplosion; // DAT_00482250 - Explosion overlay sprites
+ NutRenderer *_rebelHudDamage; // DAT_00482248 - Damage indicator sprites
+ NutRenderer *_rebelHudEffects; // DAT_00482258 - Additional effects
+ NutRenderer *_rebelHudHiRes; // DAT_00482260 - High-res HUD alternative
+
+ int32 processMouse() override;
+ bool isBitSet(int n) override;
+ void setBit(int n) override;
+
+ void iactRebel2Scene1(byte *renderBitmap, int32 codecparam, int32 setupsan12,
+ int32 setupsan13, Common::SeekableReadStream &b, int32 size, int32 flags,
+ int16 par1, int16 par2, int16 par3, int16 par4);
+
+ void procPostRendering(byte *renderBitmap, int32 codecparam, int32 setupsan12,
+ int32 setupsan13, int32 curFrame, int32 maxFrame) override;
+
+ void procIACT(byte *renderBitmap, int32 codecparam, int32 setupsan12,
+ int32 setupsan13, Common::SeekableReadStream &b, int32 size, int32 flags,
+ int16 par1, int16 par2, int16 par3, int16 par4) override;
+
+ struct RebelEnemy {
+ int id;
+ Common::Rect rect;
+ bool active;
+ bool destroyed; // Set when enemy is shot - prevents re-activation
+ int explosionFrame; // Current explosion animation frame (0-32, -1 = done)
+ bool explosionComplete; // True when explosion animation has finished
+ byte *savedBackground; // Saved background pixels at moment of destruction
+ int savedBgWidth; // Width of saved background
+ int savedBgHeight; // Height of saved background
+
+ RebelEnemy() : id(0), active(false), destroyed(false), explosionFrame(-1),
+ explosionComplete(false), savedBackground(nullptr),
+ savedBgWidth(0), savedBgHeight(0) {}
+ ~RebelEnemy() { free(savedBackground); }
+ };
+
+ Common::List<RebelEnemy> _rebelEnemies;
+
+ // Current handler type for Rebel Assault 2 (determines crosshair sprite)
+ // Handler 0: Background only
+ // Handler 7: Space flight - uses crosshair sprite 0x2F (47)
+ // Handler 8: Ground vehicle - uses crosshair sprite 0x2E (46)
+ // Handler 0x19: Mixed/turret view - uses crosshair sprite 0x2F (47)
+ // Handler 0x26: Full turret - crosshair varies by level type
+ int _rebelHandler;
+
+ // Level type from IACT opcode 6 par3 (corresponds to DAT_004436de)
+ // Determines crosshair variant for turret mode:
+ // - levelType == 5: Use sprites 0x30+ (48+) for crosshair
+ // - levelType != 5: Use sprites 0-3 for crosshair (with animation)
+ int _rebelLevelType;
+
+ // Status bar sprite index (5 or 53) triggered by Opcode 6 par4
+ // 0 = disabled
+ int _rebelStatusBarSprite;
+
+ // Embedded SAN HUD overlays (extracted from IACT chunks)
+ // These are decoded frame buffers from embedded ANIM data
+ // Slots 1-4 correspond to userId in the IACT wrapper
+ struct EmbeddedSanFrame {
+ byte *pixels; // Decoded frame pixels (8-bit indexed)
+ int width; // Frame width
+ int height; // Frame height
+ int renderX; // X position to render (0 = centered based on slot)
+ int renderY; // Y position to render
+ bool valid; // True if this slot has valid data
+
+ EmbeddedSanFrame() : pixels(nullptr), width(0), height(0),
+ renderX(0), renderY(0), valid(false) {}
+ ~EmbeddedSanFrame() { free(pixels); }
+ void clear() { free(pixels); pixels = nullptr; width = height = 0; valid = false; }
+ };
+
+ EmbeddedSanFrame _rebelEmbeddedHud[5]; // Index 0 unused, 1-4 for userId slots
+
+ // Check if a partial frame update should be skipped (overlaps with destroyed enemy)
+ bool shouldSkipFrameUpdate(int left, int top, int width, int height) override;
+
+ // Load and decode an embedded SAN animation from IACT chunk data
+ // userId: HUD slot (1-4), animData: raw ANIM data, size: data size, renderBitmap: current frame buffer
+ void loadEmbeddedSan(int userId, byte *animData, int32 size, byte *renderBitmap) override;
+};
+
+} // End of namespace Insane
+
+#endif
diff --git a/engines/scumm/module.mk b/engines/scumm/module.mk
index c3fadf55853..318e1a99231 100644
--- a/engines/scumm/module.mk
+++ b/engines/scumm/module.mk
@@ -133,6 +133,7 @@ MODULE_OBJS += \
insane/insane_enemy.o \
insane/insane_scenes.o \
insane/insane_iact.o \
+ insane/insane_rebel.o \
smush/codec1.o \
smush/codec20.o \
smush/codec37.o \
diff --git a/engines/scumm/nut_renderer.cpp b/engines/scumm/nut_renderer.cpp
index 40dabc98a64..bb11b39c98f 100644
--- a/engines/scumm/nut_renderer.cpp
+++ b/engines/scumm/nut_renderer.cpp
@@ -207,13 +207,16 @@ int NutRenderer::getCharHeight(byte c) const {
return _chars[c].height;
}
-void NutRenderer::drawFrame(byte *dst, int c, int x, int y) {
+void NutRenderer::drawFrame(byte *dst, int c, int x, int y, int pitch) {
const int width = MIN((int)_chars[c].width, _vm->_screenWidth - x);
const int height = MIN((int)_chars[c].height, _vm->_screenHeight - y);
const byte *src = _chars[c].src;
const int srcPitch = _chars[c].width;
byte bits = 0;
+ if (pitch == -1)
+ pitch = _vm->_screenWidth;
+
const int minX = x < 0 ? -x : 0;
const int minY = y < 0 ? -y : 0;
@@ -221,10 +224,42 @@ void NutRenderer::drawFrame(byte *dst, int c, int x, int y) {
return;
}
- dst += _vm->_screenWidth * y + x;
+ dst += pitch * y + x;
if (minY) {
src += minY * srcPitch;
- dst += minY * _vm->_screenWidth;
+ dst += minY * pitch;
+ }
+
+ for (int ty = minY; ty < height; ty++) {
+ for (int tx = minX; tx < width; tx++) {
+ bits = src[tx];
+ if (bits != 231 && bits) {
+ dst[tx] = bits;
+ }
+ }
+ src += srcPitch;
+ dst += pitch;
+ }
+}
+
+void NutRenderer::drawFrame(byte *dst, int c, int x, int y, int pitch, int maxWidth, int maxHeight) {
+ const int width = MIN((int)_chars[c].width, maxWidth - x);
+ const int height = MIN((int)_chars[c].height, maxHeight - y);
+ const byte *src = _chars[c].src;
+ const int srcPitch = _chars[c].width;
+ byte bits = 0;
+
+ const int minX = x < 0 ? -x : 0;
+ const int minY = y < 0 ? -y : 0;
+
+ if (height <= 0 || width <= 0) {
+ return;
+ }
+
+ dst += pitch * y + x;
+ if (minY) {
+ src += minY * srcPitch;
+ dst += minY * pitch;
}
for (int ty = minY; ty < height; ty++) {
@@ -235,7 +270,7 @@ void NutRenderer::drawFrame(byte *dst, int c, int x, int y) {
}
}
src += srcPitch;
- dst += _vm->_screenWidth;
+ dst += pitch;
}
}
diff --git a/engines/scumm/nut_renderer.h b/engines/scumm/nut_renderer.h
index c17457b2dcb..01749c1bf0a 100644
--- a/engines/scumm/nut_renderer.h
+++ b/engines/scumm/nut_renderer.h
@@ -68,7 +68,8 @@ public:
virtual ~NutRenderer();
int getNumChars() const { return _numChars; }
- void drawFrame(byte *dst, int c, int x, int y);
+ void drawFrame(byte *dst, int c, int x, int y, int pitch = -1);
+ void drawFrame(byte *dst, int c, int x, int y, int pitch, int maxWidth, int maxHeight);
int draw2byte(byte *buffer, Common::Rect &clipRect, int x, int y, int pitch, int16 col, uint16 chr);
int drawCharV7(byte *buffer, Common::Rect &clipRect, int x, int y, int pitch, int16 col, TextStyleFlags flags, byte chr, bool hardcodedColors = false, bool smushColorMode = false);
diff --git a/engines/scumm/scumm-md5.h b/engines/scumm/scumm-md5.h
index b9e7434618d..90088d90b8a 100644
--- a/engines/scumm/scumm-md5.h
+++ b/engines/scumm/scumm-md5.h
@@ -373,6 +373,7 @@ static const MD5Table md5table[] = {
{ "6b27dbcd8d5697d5c918eeca0f68ef6a", "puttrace", "HE CUP", "Preview", 3901484, Common::UNK_LANG, Common::kPlatformUnknown },
{ "6b3ec67da214f558dc5ceaa2acd47453", "indy3", "EGA", "EGA", 5361, Common::EN_ANY, Common::kPlatformDOS },
{ "6b5a3fef241e90d4b2e77f1e222773ee", "maniac", "NES", "", 2082, Common::SV_SWE, Common::kPlatformNES },
+ { "TODO", "rebel2", "VGA", "VGA", 9430, Common::EN_ANY, Common::kPlatformDOS },
{ "6bca7a1a96d16e52b8f3c42b50dbdca3", "fbear", "HE 62", "", -1, Common::JA_JPN, Common::kPlatform3DO },
{ "6bf70eee5de3d24d2403e0dd3d267e8a", "spyfox", "", "", 49221, Common::EN_USA, Common::kPlatformWindows },
{ "6c2bff0e327f2962e809c2e1a82d7309", "monkey", "VGA", "", 8347, Common::EN_ANY, Common::kPlatformAmiga },
@@ -743,6 +744,7 @@ static const MD5Table md5table[] = {
{ "e9271b3d0694c7101f10d675ab7c0133", "freddi4", "HE 99", "", -1, Common::IT_ITA, Common::kPlatformWindows },
{ "e94c7cc3686fce406d3c91b5eae5a72d", "zak", "V2", "V2", 1916, Common::EN_ANY, Common::kPlatformAmiga },
{ "e95cf980719c0be078fb68a67af97b4a", "funpack", "", "", -1, Common::JA_JPN, Common::kPlatform3DO },
+ { "e977ed046b88e75b645491caeff37b0c", "rebel2", "Demo", "Demo", 313689, Common::EN_ANY, Common::kPlatformDOS },
{ "e98b982ceaf9d253d730bde8903233d6", "monkey", "EGA", "EGA", 8357, Common::DE_DEU, Common::kPlatformDOS },
{ "eae95b2b3546d8ba86ae1d397c383253", "dog", "", "", 19681, Common::EN_ANY, Common::kPlatformUnknown },
{ "eb700bb73ca1cc44a1ad5e4b1a4bdeaf", "indy3", "EGA", "EGA", 5361, Common::DE_DEU, Common::kPlatformDOS },
diff --git a/engines/scumm/scumm.cpp b/engines/scumm/scumm.cpp
index 1b65e51cf16..3ebc7429d00 100644
--- a/engines/scumm/scumm.cpp
+++ b/engines/scumm/scumm.cpp
@@ -52,6 +52,7 @@
#include "scumm/smush/smush_player.h"
#include "scumm/players/player_towns.h"
#include "scumm/insane/insane.h"
+#include "scumm/insane/insane_rebel.h"
#include "scumm/he/animation_he.h"
#include "scumm/he/font_he.h"
#include "scumm/he/intern_he.h"
@@ -1157,6 +1158,8 @@ Common::Error ScummEngine::init() {
_filenamePattern.pattern = "%.2d.LFL";
_filenamePattern.genMethod = kGenRoomNum;
+ } else if (_game.id == GID_REBEL2) {
+ _fileHandle = new ScummFile(this);
} else if (_game.platform == Common::kPlatformMacintosh) {
// The mac versions of Indy4, Sam&Max, DOTT, FT and The Dig used a
// special meta (container) file format to store the actual SCUMM data
@@ -1504,6 +1507,11 @@ Common::Error ScummEngine::init() {
setupScumm(macResourceFile);
+ if (_game.id == GID_REBEL2) {
+ _setupIsComplete = true;
+ return Common::kNoError;
+ }
+
readIndexFile();
// Create the debugger now that _numVariables has been set
@@ -1778,6 +1786,38 @@ void ScummEngine::setupScumm(const Common::Path &macResourceFile) {
#ifdef ENABLE_SCUMM_7_8
void ScummEngine_v7::setupScumm(const Common::Path &macResourceFile) {
+ if (_game.id == GID_REBEL2) {
+ _res->allocResTypeData(rtBuffer, 0, 10, kDynamicResTypeMode);
+ initScreens(0, 200);
+
+ _numVariables = 256;
+ _scummVars = (int32 *)calloc(_numVariables, sizeof(int32));
+
+ _numArray = 50;
+ _res->allocResTypeData(rtString, 0, _numArray, kDynamicResTypeMode);
+ _res->allocResTypeData(rtSound, 0, 200, kDynamicResTypeMode);
+ _res->allocResTypeData(rtCostume, 0, 200, kDynamicResTypeMode);
+ _res->allocResTypeData(rtRoom, 0, 20, kDynamicResTypeMode);
+
+ defineArray(0, kIntArray, 0, 1000);
+ _numActors = 0;
+
+ setupScummVars();
+
+ _useOriginalGUI = false;
+
+ _sound = new Sound(this, _mixer, false);
+ _musicEngine = _imuseDigital = new IMuseDigital(this, 11025, _mixer, &_resourceAccessMutex, false);
+ _insane = new InsaneRebel2(this);
+ _splayer = new SmushPlayer(this, _imuseDigital, _insane);
+
+ // Initialize cursor
+ _macGui = nullptr; // Ensure this is null as we don't want MacGui behavior
+ _charset = new CharsetRendererV7(this); // Just in case
+
+ initBanners();
+ return;
+ }
// The object line toggle is always synchronized from the main game to
// our internal Game Options; at startup we do the opposite, since an user
@@ -2576,8 +2616,22 @@ void ScummEngine_v7::syncSoundSettings() {
if (!_setupIsComplete)
return;
+ if (_game.id == GID_REBEL2) {
+ ScummEngine::syncSoundSettings();
+ if (_imuseDigital) {
+ _imuseDigital->diMUSESetMusicGroupVol(ConfMan.getInt("music_volume") / 2);
+ _imuseDigital->diMUSESetVoiceGroupVol(ConfMan.getInt("speech_volume") / 2);
+ _imuseDigital->diMUSESetSFXGroupVol(ConfMan.getInt("sfx_volume") / 2);
+ }
+ return;
+ }
+
if (!isUsingOriginalGUI()) {
ScummEngine::syncSoundSettings();
+ if (_splayer) {
+ _splayer->setChanFlag(0, true);
+ _splayer->setChanFlag(2, true);
+ }
return;
}
@@ -2628,6 +2682,18 @@ int ScummEngine::getTalkSpeed() {
#pragma mark -
Common::Error ScummEngine::go() {
+#ifdef ENABLE_SCUMM_7_8
+ if (_game.id == GID_REBEL2) {
+ // Use 12 FPS as default, same as The Dig. FT uses 10.
+ // Since we don't have the standard scripts initializing this, we pass it here.
+ if (_game.features & GF_DEMO)
+ ((ScummEngine_v7 *)this)->_splayer->play("OPEN/O_DEMO.SAN", 12);
+ else
+ ((ScummEngine_v7 *)this)->_splayer->play("LEV01/01P01.SAN", 12);
+ return Common::kNoError;
+ }
+#endif
+
setTotalPlayTime();
_lastWaitTime = _system->getMillis();
@@ -4045,6 +4111,9 @@ void ScummEngine_v7::scummLoop_handleSound() {
_imuseDigital->refreshScripts();
}
+ if (_game.id == GID_REBEL2)
+ return;
+
_splayer->setChanFlag(0, VAR(VAR_VOICE_MODE) != 0);
_splayer->setChanFlag(2, VAR(VAR_VOICE_MODE) != 2);
}
diff --git a/engines/scumm/scumm_v7.h b/engines/scumm/scumm_v7.h
index aa56a411a1e..101e58a9611 100644
--- a/engines/scumm/scumm_v7.h
+++ b/engines/scumm/scumm_v7.h
@@ -38,6 +38,7 @@ class TextRenderer_v7;
class ScummEngine_v7 : public ScummEngine_v6 {
friend class SmushPlayer;
friend class Insane;
+ friend class InsaneRebel2;
public:
ScummEngine_v7(OSystem *syst, const DetectorResult &dr);
~ScummEngine_v7() override;
diff --git a/engines/scumm/smush/smush_player.cpp b/engines/scumm/smush/smush_player.cpp
index 31c322a7e59..edcf9c62d91 100644
--- a/engines/scumm/smush/smush_player.cpp
+++ b/engines/scumm/smush/smush_player.cpp
@@ -364,6 +364,52 @@ void SmushPlayer::handleIACT(int32 subSize, Common::SeekableReadStream &b) {
debugC(DEBUG_SMUSH, "SmushPlayer::IACT()");
assert(subSize >= 8);
+ // For Rebel2 large IACT chunks, check for embedded SAN animations
+ if (_vm->_game.id == GID_REBEL2 && subSize > 1000) {
+ int64 startPos = b.pos();
+
+ // Read header to check for embedded ANIM
+ byte header[64];
+ int bytesRead = b.read(header, MIN<int32>(64, subSize));
+
+ // Check for ANIM at offset 18 (embedded SAN location)
+ uint32 magicAt18 = (bytesRead >= 22) ? READ_BE_UINT32(header + 18) : 0;
+
+ if (magicAt18 == MKTAG('A','N','I','M')) {
+ // This IACT contains an embedded SAN animation!
+ uint32 animSize = (bytesRead >= 26) ? READ_BE_UINT32(header + 22) : 0;
+
+ // Read the userId to determine which HUD slot (1-4)
+ int code = READ_LE_UINT16(header);
+ int flags = READ_LE_UINT16(header + 2);
+ int userId = READ_LE_UINT16(header + 6);
+
+ debug("Rebel2: Extracting embedded SAN from IACT: userId=%d, animSize=%u", userId, animSize);
+
+ // Extract the embedded ANIM data (starts at offset 18 from IACT start)
+ b.seek(startPos + 18);
+
+ int32 embeddedSize = subSize - 18;
+ if (embeddedSize > 0 && _insane) {
+ byte *animData = (byte *)malloc(embeddedSize);
+ if (animData) {
+ b.read(animData, embeddedSize);
+
+ // Pass to Insane for decoding
+ _insane->loadEmbeddedSan(userId, animData, embeddedSize, _dst);
+
+ free(animData);
+ }
+ }
+
+ // Return - don't process as audio
+ return;
+ }
+
+ // Seek back to start for normal processing
+ b.seek(startPos);
+ }
+
int code = b.readUint16LE();
int flags = b.readUint16LE();
int unknown = b.readSint16LE();
@@ -759,6 +805,19 @@ void SmushPlayer::decodeFrameObject(int codec, const uint8 *src, int left, int t
if (_specialBuffer == 0)
_specialBuffer = (byte *)malloc(242 * 384);
_dst = _specialBuffer;
+ } else if (_vm->_game.id == GID_REBEL2 && ((height != _vm->_screenHeight) || (width != _vm->_screenWidth))) {
+ // For Rebel2, check if this partial update overlaps with a destroyed enemy
+ // If so, skip the update entirely to prevent showing the enemy sprite
+ if (_insane && _insane->shouldSkipFrameUpdate(left, top, width, height)) {
+ return; // Skip this frame update
+ }
+
+ if (_specialBuffer == 0) {
+ _specialBuffer = (byte *)malloc(width * height);
+ _width = width;
+ _height = height;
+ }
+ _dst = _specialBuffer;
} else if ((height > _vm->_screenHeight) || (width > _vm->_screenWidth))
return;
// FT Insane uses smaller frames to draw overlays with moving objects
@@ -770,15 +829,22 @@ void SmushPlayer::decodeFrameObject(int codec, const uint8 *src, int left, int t
if ((height == 242) && (width == 384)) {
_width = width;
_height = height;
+ } else if (_vm->_game.id == GID_REBEL2 && ((height != _vm->_screenHeight) || (width != _vm->_screenWidth))) {
+ // Do not update _width/_height here to preserve original video size
+ // if frames are partial updates
} else {
_width = _vm->_screenWidth;
_height = _vm->_screenHeight;
}
+ int pitch = _vm->_screenWidth;
+ if (_dst == _specialBuffer)
+ pitch = _width;
+
switch (codec) {
case SMUSH_CODEC_RLE:
case SMUSH_CODEC_RLE_ALT:
- smushDecodeRLE(_dst, src, left, top, width, height, _vm->_screenWidth);
+ smushDecodeRLE(_dst, src, left, top, width, height, pitch);
break;
case SMUSH_CODEC_DELTA_BLOCKS:
if (!_deltaBlocksCodec)
@@ -1214,6 +1280,9 @@ void SmushPlayer::play(const char *filename, int32 speed, int32 offset, int32 st
// Hide mouse
bool oldMouseState = CursorMan.showMouse(false);
+ if (_vm->_game.id == GID_REBEL2) {
+ insanity(true);
+ }
// Load the video
_seekFile = filename;
@@ -2135,4 +2204,30 @@ bool SmushPlayer::isAudioCallbackEnabled() {
return _smushAudioCallbackEnabled;
}
+// Only used by Rebel Assault 2
+void SmushPlayer::addMaskedRegion(const Common::Rect &rect) {
+ // Check if the region already exists
+ for (Common::List<Common::Rect>::iterator it = _maskedRegions.begin(); it != _maskedRegions.end(); ++it) {
+ if (*it == rect) {
+ return; // Already exists
+ }
+ }
+ _maskedRegions.push_back(rect);
+}
+
+// Only used by Rebel Assault 2
+void SmushPlayer::removeMaskedRegion(const Common::Rect &rect) {
+ for (Common::List<Common::Rect>::iterator it = _maskedRegions.begin(); it != _maskedRegions.end(); ++it) {
+ if (*it == rect) {
+ _maskedRegions.erase(it);
+ return;
+ }
+ }
+}
+
+// Only used by Rebel Assault 2
+void SmushPlayer::clearMaskedRegions() {
+ _maskedRegions.clear();
+}
+
} // End of namespace Scumm
diff --git a/engines/scumm/smush/smush_player.h b/engines/scumm/smush/smush_player.h
index 557203d9776..32d33b2af15 100644
--- a/engines/scumm/smush/smush_player.h
+++ b/engines/scumm/smush/smush_player.h
@@ -23,6 +23,8 @@
#define SCUMM_SMUSH_PLAYER_H
#include "common/util.h"
+#include "common/list.h"
+#include "common/rect.h"
namespace Audio {
class SoundHandle;
@@ -90,6 +92,7 @@ class Insane;
class SmushPlayer {
friend class Insane;
+ friend class InsaneRebel2;
private:
struct SmushAudioDispatch {
uint8 *headerPtr;
@@ -209,6 +212,14 @@ public:
byte *getVideoPalette();
void setCurVideoFlags(int16 flags);
+ // Masked regions - areas where video should not update (e.g., destroyed enemies)
+ // The Insane class can add/remove regions, and decodeFrameObject will restore
+ // these areas from the previous frame after decoding
+ void addMaskedRegion(const Common::Rect &rect);
+ void removeMaskedRegion(const Common::Rect &rect);
+ void clearMaskedRegions();
+ Common::List<Common::Rect> _maskedRegions;
+
protected:
int _width, _height;
Commit: 9aba962606d5aa25961aa99f8233916b7d02667d
https://github.com/scummvm/scummvm/commit/9aba962606d5aa25961aa99f8233916b7d02667d
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:08:42+02:00
Commit Message:
SCUMM: RA2: Add missing init variables
Changed paths:
engines/scumm/insane/insane_rebel.cpp
diff --git a/engines/scumm/insane/insane_rebel.cpp b/engines/scumm/insane/insane_rebel.cpp
index f1d72aa578d..98126582fe9 100644
--- a/engines/scumm/insane/insane_rebel.cpp
+++ b/engines/scumm/insane/insane_rebel.cpp
@@ -63,6 +63,84 @@ InsaneRebel2::InsaneRebel2(ScummEngine_v7 *scumm) {
_rebelEnemies.clear();
_rebelHandler = 8; // Default to Handler 8 (ground vehicle) for Level 1
_rebelLevelType = 0; // Level type from Opcode 6 par3, determines HUD sprite variant
+
+ _speed = 12;
+ _insaneIsRunning = false;
+
+ _numberArray = 0;
+ _emulateInterrupt = 0;
+ _flag1d = 0;
+ _objArray1Idx = 0;
+ _objArray1Idx2 = 0;
+ _objArray2Idx = 0;
+ _objArray2Idx2 = 0;
+ _currSceneId = 1;
+ _timer6Id = 0;
+ _timerSpriteId = 0;
+ _temp2SceneId = 0;
+ _tempSceneId = 0;
+ _currEnemy = -1;
+ _currScenePropIdx = 0;
+ _currScenePropSubIdx = 0;
+ _currTrsMsg = 0;
+ _sceneData2Loaded = 0;
+ _sceneData1Loaded = 0;
+ _keyboardDisable = 0;
+ _needSceneSwitch = false;
+ _idx2Exceeded = 0;
+ _tiresRustle = false;
+ _keybOldDx = 0;
+ _keybOldDy = 0;
+ _velocityX = 0;
+ _velocityY = 0;
+ _keybX = 0;
+ _keybY = 0;
+ _firstBattle = false;
+ _battleScene = true;
+ _kickBenProgress = false;
+ _weaponBenJustSwitched = false;
+ _kickEnemyProgress = false;
+ _weaponEnemyJustSwitched = false;
+ _beenCheated = 0;
+ _posBrokenTruck = 0;
+ _posBrokenCar = 0;
+ _posFatherTorque = 0;
+ _posCave = 0;
+ _posVista = 0;
+ _roadBranch = false;
+ _roadStop = false;
+ _carIsBroken = false;
+ _benHasGoggles = false;
+ _mineCaveIsNear = false;
+ _objectDetected = false;
+ _approachAnim = -1;
+ _val54d = 0;
+ _val57d = 0;
+ _val115_ = false;
+ _roadBumps = false;
+ _val211d = 0;
+ _val213d = 0;
+ _metEnemiesListTail = 0;
+ _smlayer_room = 0;
+ _smlayer_room2 = 0;
+ _isBenCut = 0;
+ _continueFrame = 0;
+ _continueFrame1 = 0;
+ _counter1 = 0;
+ _iactSceneId = 0;
+ _iactSceneId2 = 0;
+
+ int i, j;
+
+ for (i = 0; i < 12; i++)
+ _metEnemiesList[i] = 0;
+
+ for (i = 0; i < 9; i++)
+ for (j = 0; j < 9; j++)
+ _enHdlVar[i][j] = 0;
+
+ for (i = 0; i < 0x200; i++)
+ _iactBits[i] = 0;
}
Commit: 32a4ba152c15e34e89b649d439d82140a867d3d4
https://github.com/scummvm/scummvm/commit/32a4ba152c15e34e89b649d439d82140a867d3d4
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:08:43+02:00
Commit Message:
SCUMM: RA2: Deduplicate level object setup
Changed paths:
engines/scumm/insane/insane_rebel.cpp
diff --git a/engines/scumm/insane/insane_rebel.cpp b/engines/scumm/insane/insane_rebel.cpp
index 98126582fe9..41d4d69af9d 100644
--- a/engines/scumm/insane/insane_rebel.cpp
+++ b/engines/scumm/insane/insane_rebel.cpp
@@ -403,6 +403,10 @@ void InsaneRebel2::iactRebel2Scene1(byte *renderBitmap, int32 codecparam, int32
}
}
+// External helpers from smush_player.cpp but we are already in Scumm namespace
+extern void smushDecodeRLE(byte *dst, const byte *src, int left, int top, int width, int height, int pitch);
+extern void smushDecodeUncompressed(byte *dst, const byte *src, int left, int top, int width, int height, int pitch);
+
void InsaneRebel2::loadEmbeddedSan(int userId, byte *animData, int32 size, byte *renderBitmap) {
// Validate userId (1-4 for HUD slots)
if (userId < 1 || userId > 4 || !animData || size < 32) {
@@ -410,8 +414,6 @@ void InsaneRebel2::loadEmbeddedSan(int userId, byte *animData, int32 size, byte
return;
}
- // Parse the embedded ANIM structure
- // Format: ANIM (4 bytes) + size (4 bytes BE) + AHDR (4 bytes) + size (4 bytes BE) + ...
Common::MemoryReadStream stream(animData, size);
// Read ANIM header
@@ -423,36 +425,21 @@ void InsaneRebel2::loadEmbeddedSan(int userId, byte *animData, int32 size, byte
uint32 animSize = stream.readUint32BE();
debug("Rebel2: Parsing embedded ANIM: userId=%d, reported size=%u, actual=%d", userId, animSize, size - 8);
- // Read AHDR header
- uint32 ahdrTag = stream.readUint32BE();
- if (ahdrTag != MKTAG('A','H','D','R')) {
- debug("Rebel2: Embedded SAN missing AHDR tag, got 0x%08X", ahdrTag);
- return;
- }
- uint32 ahdrSize = stream.readUint32BE();
-
- // Skip AHDR content (palette etc)
- stream.skip(ahdrSize);
- if (ahdrSize & 1) stream.skip(1); // Padding
-
- // Look for first FRME
+ // Iterate through chunks to find FRME -> FOBJ
while (!stream.eos() && stream.pos() < size) {
uint32 tag = stream.readUint32BE();
uint32 chunkSize = stream.readUint32BE();
-
+ int32 nextChunkPos = stream.pos() + chunkSize;
+
if (tag == MKTAG('F','R','M','E')) {
- debug("Rebel2: Found FRME in embedded SAN, size=%u", chunkSize);
-
- // Parse FRME content to find FOBJ
- int64 frmeEnd = stream.pos() + chunkSize;
-
- while (stream.pos() < frmeEnd && !stream.eos()) {
+ // Iterate sub-chunks in FRME
+ while (stream.pos() < nextChunkPos && !stream.eos()) {
uint32 subTag = stream.readUint32BE();
uint32 subSize = stream.readUint32BE();
-
+ int32 nextSubPos = stream.pos() + subSize;
+
if (subTag == MKTAG('F','O','B','J')) {
- debug("Rebel2: Found FOBJ in embedded SAN, size=%u", subSize);
-
+ // Found FOBJ - Embedded HUD Frame
// Read FOBJ header
int codec = stream.readUint16LE();
int left = stream.readUint16LE();
@@ -467,16 +454,16 @@ void InsaneRebel2::loadEmbeddedSan(int userId, byte *animData, int32 size, byte
// Allocate storage for the decoded frame
EmbeddedSanFrame &frame = _rebelEmbeddedHud[userId];
- frame.clear();
if (width > 0 && height > 0 && width <= 800 && height <= 480) {
- frame.pixels = (byte *)calloc(width * height, 1);
- frame.width = width;
- frame.height = height;
+ if (frame.width != width || frame.height != height || !frame.pixels) {
+ frame.clear();
+ frame.pixels = (byte *)calloc(width * height, 1);
+ frame.width = width;
+ frame.height = height;
+ }
- // Use the left/top from FOBJ header as render position
- // These are the exact screen coordinates for the HUD overlay
- // The FOBJ header specifies where this frame should be composited
+ // Update render position from FOBJ header
frame.renderX = left;
frame.renderY = top;
@@ -487,55 +474,18 @@ void InsaneRebel2::loadEmbeddedSan(int userId, byte *animData, int32 size, byte
stream.read(fobjData, dataSize);
// Decode based on codec
- // Codec 1 = RLE, Codec 3 = raw, Codec 21 = line update, Codec 45 = block delta
- if (codec == 1) {
- // RLE decode using SMUSH BOMP format
- // Each line has a 16-bit size header, then BOMP-encoded data
- // BOMP format: code byte, then data
- // code & 1 == 1: fill mode, next byte repeated (code>>1)+1 times
- // code & 1 == 0: copy mode, copy next (code>>1)+1 bytes
- byte *srcPtr = fobjData;
- for (int row = 0; row < height && srcPtr < fobjData + dataSize; row++) {
- int lineSize = READ_LE_UINT16(srcPtr);
- srcPtr += 2;
- byte *lineEnd = srcPtr + lineSize;
- byte *lineDst = frame.pixels + row * width;
- int remaining = width;
-
- while (srcPtr < lineEnd && remaining > 0) {
- byte code = *srcPtr++;
- int num = (code >> 1) + 1;
- if (num > remaining)
- num = remaining;
- remaining -= num;
-
- if (code & 1) {
- // Fill mode: repeat next byte
- byte color = (srcPtr < lineEnd) ? *srcPtr++ : 0;
- memset(lineDst, color, num);
- lineDst += num;
- } else {
- // Copy mode: copy next bytes
- for (int i = 0; i < num && srcPtr < lineEnd; i++) {
- *lineDst++ = *srcPtr++;
- }
- }
- }
- srcPtr = lineEnd;
- }
+ if (codec == 1 || codec == 3) {
+ // RLE use existing decoder
+ smushDecodeRLE(frame.pixels, fobjData, 0, 0, width, height, width);
frame.valid = true;
- debug("Rebel2: Decoded embedded HUD (codec 1/RLE): %dx%d", width, height);
- } else if (codec == 3 || codec == 20) {
- // Uncompressed - direct copy
- int copySize = MIN(dataSize, width * height);
- memcpy(frame.pixels, fobjData, copySize);
+ debug("Rebel2: Decoded embedded HUD (codec %d/RLE): %dx%d", codec, width, height);
+ } else if (codec == 20) {
+ // Uncompressed
+ smushDecodeUncompressed(frame.pixels, fobjData, 0, 0, width, height, width);
frame.valid = true;
- debug("Rebel2: Decoded embedded HUD (codec %d/raw): %dx%d", codec, width, height);
+ debug("Rebel2: Decoded embedded HUD (codec 20/raw): %dx%d", width, height);
} else if (codec == 21 || codec == 44) {
// Codec 21/44: Line update codec
- // Format: For each line:
- // 16-bit line data size
- // Line data: pairs of (16-bit skip, 16-bit count-1, pixels[count])
byte *srcPtr = fobjData;
for (int row = 0; row < height && srcPtr < fobjData + dataSize; row++) {
int lineDataSize = READ_LE_UINT16(srcPtr);
@@ -559,16 +509,13 @@ void InsaneRebel2::loadEmbeddedSan(int userId, byte *animData, int32 size, byte
frame.valid = true;
debug("Rebel2: Decoded embedded HUD (codec 21/line update): %dx%d", width, height);
} else if (codec == 45) {
- // Codec 45: Similar to codec 47 but simpler - block delta
- // For embedded HUD use, try simple raw copy as fallback
- // This codec appears in small sprites (11x26, 17x53)
- int copySize = MIN(dataSize, width * height);
+ // Codec 45: Block delta (simple copy for now)
+ int copySize = MIN((int)dataSize, width * height);
memcpy(frame.pixels, fobjData, copySize);
frame.valid = true;
debug("Rebel2: Decoded embedded HUD (codec 45/block): %dx%d", width, height);
} else {
debug("Rebel2: TODO: Decode codec %d for embedded HUD", codec);
- // For now, mark as invalid but keep dimensions
frame.valid = false;
}
@@ -596,21 +543,25 @@ void InsaneRebel2::loadEmbeddedSan(int userId, byte *animData, int32 size, byte
}
}
- return; // Found and processed FOBJ
+ // Done with FOBJ - assume only one relevant frame per embedded SAN
+ stream.seek(nextChunkPos);
+ goto end_parsing;
} else {
- // Skip other sub-chunks
- stream.skip(subSize);
+ // Skip other sub-chunks (AHDR inside FRME?) or padding
+ stream.seek(nextSubPos);
if (subSize & 1) stream.skip(1);
}
}
} else {
- // Skip non-FRME chunks
- stream.skip(chunkSize);
+ // Skip non-FRME chunks (AHDR, etc at top level)
+ stream.seek(nextChunkPos);
if (chunkSize & 1) stream.skip(1);
}
}
debug("Rebel2: No FOBJ found in embedded SAN userId=%d", userId);
+
+end_parsing:;
}
Commit: b1e18defe18f1f5b095d3c982b30dffc40560afa
https://github.com/scummvm/scummvm/commit/b1e18defe18f1f5b095d3c982b30dffc40560afa
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:08:43+02:00
Commit Message:
SCUMM: RA2: Remove redundant NutRenderer drawFrame overload
Changed paths:
engines/scumm/nut_renderer.cpp
engines/scumm/nut_renderer.h
diff --git a/engines/scumm/nut_renderer.cpp b/engines/scumm/nut_renderer.cpp
index bb11b39c98f..3bac87e4fb0 100644
--- a/engines/scumm/nut_renderer.cpp
+++ b/engines/scumm/nut_renderer.cpp
@@ -242,38 +242,6 @@ void NutRenderer::drawFrame(byte *dst, int c, int x, int y, int pitch) {
}
}
-void NutRenderer::drawFrame(byte *dst, int c, int x, int y, int pitch, int maxWidth, int maxHeight) {
- const int width = MIN((int)_chars[c].width, maxWidth - x);
- const int height = MIN((int)_chars[c].height, maxHeight - y);
- const byte *src = _chars[c].src;
- const int srcPitch = _chars[c].width;
- byte bits = 0;
-
- const int minX = x < 0 ? -x : 0;
- const int minY = y < 0 ? -y : 0;
-
- if (height <= 0 || width <= 0) {
- return;
- }
-
- dst += pitch * y + x;
- if (minY) {
- src += minY * srcPitch;
- dst += minY * pitch;
- }
-
- for (int ty = minY; ty < height; ty++) {
- for (int tx = minX; tx < width; tx++) {
- bits = src[tx];
- if (bits != 231 && bits) {
- dst[tx] = bits;
- }
- }
- src += srcPitch;
- dst += pitch;
- }
-}
-
int NutRenderer::drawCharV7(byte *buffer, Common::Rect &clipRect, int x, int y, int pitch, int16 col, TextStyleFlags flags, byte chr, bool hardcodedColors, bool smushColorMode) {
if (_direction < 0)
x -= _chars[chr].width;
diff --git a/engines/scumm/nut_renderer.h b/engines/scumm/nut_renderer.h
index 01749c1bf0a..d7f92bf1076 100644
--- a/engines/scumm/nut_renderer.h
+++ b/engines/scumm/nut_renderer.h
@@ -69,7 +69,6 @@ public:
int getNumChars() const { return _numChars; }
void drawFrame(byte *dst, int c, int x, int y, int pitch = -1);
- void drawFrame(byte *dst, int c, int x, int y, int pitch, int maxWidth, int maxHeight);
int draw2byte(byte *buffer, Common::Rect &clipRect, int x, int y, int pitch, int16 col, uint16 chr);
int drawCharV7(byte *buffer, Common::Rect &clipRect, int x, int y, int pitch, int16 col, TextStyleFlags flags, byte chr, bool hardcodedColors = false, bool smushColorMode = false);
Commit: de41cd69ab7767926e9786f8a5458e2fa7988c4d
https://github.com/scummvm/scummvm/commit/de41cd69ab7767926e9786f8a5458e2fa7988c4d
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:08:43+02:00
Commit Message:
SCUMM: RA2: Avoid crash during shutdown
Changed paths:
engines/scumm/insane/insane_rebel.cpp
diff --git a/engines/scumm/insane/insane_rebel.cpp b/engines/scumm/insane/insane_rebel.cpp
index 41d4d69af9d..1c35c38041a 100644
--- a/engines/scumm/insane/insane_rebel.cpp
+++ b/engines/scumm/insane/insane_rebel.cpp
@@ -44,6 +44,21 @@ namespace Scumm {
InsaneRebel2::InsaneRebel2(ScummEngine_v7 *scumm) {
_vm = scumm;
+
+ // Initialize parent class pointers to nullptr to avoid crash in ~Insane()
+ // because Insane() default constructor leaves them uninitialized.
+ _smush_roadrashRip = nullptr;
+ _smush_roadrsh2Rip = nullptr;
+ _smush_roadrsh3Rip = nullptr;
+ _smush_goglpaltRip = nullptr;
+ _smush_tovista1Flu = nullptr;
+ _smush_tovista2Flu = nullptr;
+ _smush_toranchFlu = nullptr;
+ _smush_minedrivFlu = nullptr;
+ _smush_minefiteFlu = nullptr;
+ _smush_bencutNut = nullptr;
+ _smush_bensgoggNut = nullptr;
+
// Rebel Assault 2 specific initialization can go here
_rebelHudPrimary = nullptr;
_rebelHudSecondary = nullptr;
Commit: 34e92ca251b4138f249699f804d7b62500c15229
https://github.com/scummvm/scummvm/commit/34e92ca251b4138f249699f804d7b62500c15229
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:08:44+02:00
Commit Message:
SCUMM: RA2: Implement IACT dependency links
Changed paths:
engines/scumm/insane/insane_rebel.cpp
engines/scumm/insane/insane_rebel.h
diff --git a/engines/scumm/insane/insane_rebel.cpp b/engines/scumm/insane/insane_rebel.cpp
index 1c35c38041a..0482872c0c2 100644
--- a/engines/scumm/insane/insane_rebel.cpp
+++ b/engines/scumm/insane/insane_rebel.cpp
@@ -156,6 +156,12 @@ InsaneRebel2::InsaneRebel2(ScummEngine_v7 *scumm) {
for (i = 0; i < 0x200; i++)
_iactBits[i] = 0;
+
+ for (i = 0; i < 512; i++) {
+ _rebelLinks[i][0] = 0;
+ _rebelLinks[i][1] = 0;
+ _rebelLinks[i][2] = 0;
+ }
}
@@ -201,6 +207,29 @@ int32 InsaneRebel2::processMouse() {
it->id, mousePos.x, mousePos.y,
it->rect.left, it->rect.top, it->rect.right, it->rect.bottom);
+ // Disable self
+ setBit(it->id);
+
+ // Handle dependencies
+ int id = it->id;
+ if (id >= 0 && id < 512) {
+ // Slot 2: Enable (Explosion?)
+ if (_rebelLinks[id][2] != 0) {
+ clearBit(_rebelLinks[id][2]);
+ debug("Rebel2: Enabled dependency Slot 2 (ID=%d) for Parent %d", _rebelLinks[id][2], id);
+ }
+ // Slot 1: Enable (Explosion?)
+ if (_rebelLinks[id][1] != 0) {
+ clearBit(_rebelLinks[id][1]);
+ debug("Rebel2: Enabled dependency Slot 1 (ID=%d) for Parent %d", _rebelLinks[id][1], id);
+ }
+ // Slot 0: Disable (Shots?)
+ if (_rebelLinks[id][0] != 0) {
+ setBit(_rebelLinks[id][0]);
+ debug("Rebel2: Disabled dependency Slot 0 (ID=%d) for Parent %d", _rebelLinks[id][0], id);
+ }
+ }
+
// Note: Background saving and masking is handled in procPostRendering
// where we have access to the render bitmap
// TODO: Play explosion sound
@@ -264,6 +293,12 @@ void InsaneRebel2::setBit(int n) {
_iactBits[n] = 1;
}
+void InsaneRebel2::clearBit(int n) {
+ assert (n < 0x200);
+
+ _iactBits[n] = 0;
+}
+
void InsaneRebel2::procIACT(byte *renderBitmap, int32 codecparam, int32 setupsan12,
int32 setupsan13, Common::SeekableReadStream &b, int32 size, int32 flags,
int16 par1, int16 par2, int16 par3, int16 par4) {
@@ -300,15 +335,28 @@ void InsaneRebel2::iactRebel2Scene1(byte *renderBitmap, int32 codecparam, int32
// screenX = centerX - DAT_0043e006 (scrollX)
// screenY = centerY - DAT_0043e008 (scrollY)
+ // screenX = centerX - DAT_0043e006 (scrollX)
+ // screenY = centerY - DAT_0043e008 (scrollY)
+
if (par1 == 4) {
// Opcode 4: Enemy position update
// Read 5 shorts from the stream (offset +8 through +16)
int16 enemyId = b.readSint16LE(); // Offset +8
int16 x = b.readSint16LE(); // Offset +10 (0x0A)
+
+ // If enemy is disabled in bit table, skip update
+ bool disabled = isBitSet(enemyId);
+
int16 y = b.readSint16LE(); // Offset +12 (0x0C)
int16 w = b.readSint16LE(); // Offset +14 (0x0E) - Width
int16 h = b.readSint16LE(); // Offset +16 (0x10) - Height
+ // If disabled, stop processing this object
+ if (disabled) {
+ // debug("Rebel2: Skipping Opcode 4 for disabled enemy ID=%d", enemyId);
+ return;
+ }
+
// The disassembly shows half-width/half-height are used for centering:
// halfW = w >> 1
// halfH = h >> 1
@@ -367,7 +415,39 @@ void InsaneRebel2::iactRebel2Scene1(byte *renderBitmap, int32 codecparam, int32
} else if (par1 == 2) {
// Opcode 2: Often used for bit setting
// Disassembly shows this is handled but we don't have full context
- debug("Rebel2 IACT Opcode 2: par2=%d par3=%d par4=%d", par2, par3, par4);
+ // debug("Rebel2 IACT Opcode 2: par2=%d par3=%d par4=%d", par2, par3, par4);
+
+ // Handle dependency linking (par3 == 4)
+ if (par3 == 4) {
+ int16 childId = b.readSint16LE(); // Offset +8
+ int16 parentId = b.readSint16LE(); // Offset +10
+
+ if (parentId >= 0 && parentId < 512) {
+ // Shift links
+ _rebelLinks[parentId][2] = _rebelLinks[parentId][1];
+ _rebelLinks[parentId][1] = _rebelLinks[parentId][0];
+ _rebelLinks[parentId][0] = childId;
+
+ // Apply initial state based on parent state
+ // If parent is ALIVE (Bit Clear) -> Disable new Child (Set Bit)
+ // If parent is DEAD (Bit Set) -> Enable new Child (Clear Bit)
+ if (!isBitSet(parentId)) {
+ setBit(childId);
+ debug("Rebel2: Linked ID=%d to Parent=%d (Slot 0). Parent Alive -> Child Disabled.", childId, parentId);
+ } else {
+ clearBit(childId);
+ debug("Rebel2: Linked ID=%d to Parent=%d (Slot 0). Parent Dead -> Child Enabled.", childId, parentId);
+ }
+ }
+ } else {
+ // Skip extra data if any? Opcode 2 might vary in size.
+ // Based on disassembly, it accesses offset +8 and +10 for case 4.
+ // Currently we don't know size for other cases, but hopefully they don't crash stream reading.
+ // 'b' is SeekableReadStream, so we might need to skip if we don't read?
+ // The caller `procIACT` passes `size` but that's for the WHOLE chunk.
+ // We can't skip unknown sub-opcodes easily without size map.
+ // Assuming only par3=4 has extra data for now.
+ }
} else if (par1 == 3) {
// Opcode 3: Often used for clearing/resetting
@@ -795,7 +875,8 @@ void InsaneRebel2::procPostRendering(byte *renderBitmap, int32 codecparam, int32
}
}
// After explosion is complete (explosionFrame == -1), do nothing
- } else if (it->active) {
+ // After explosion is complete (explosionFrame == -1), do nothing
+ } else if (it->active && !isBitSet(it->id)) { // Only draw if active AND not disabled by IACT
// Draw bounding box outline for active enemies (debug visualization)
// Draw Top
if (r.top >= 0 && r.top < height) {
diff --git a/engines/scumm/insane/insane_rebel.h b/engines/scumm/insane/insane_rebel.h
index 00b41865c06..3fc14b2cc5b 100644
--- a/engines/scumm/insane/insane_rebel.h
+++ b/engines/scumm/insane/insane_rebel.h
@@ -164,6 +164,10 @@ public:
// Load and decode an embedded SAN animation from IACT chunk data
// userId: HUD slot (1-4), animData: raw ANIM data, size: data size, renderBitmap: current frame buffer
void loadEmbeddedSan(int userId, byte *animData, int32 size, byte *renderBitmap) override;
+
+ int16 _rebelLinks[512][3]; // Dependency links: Slot 0 (Disable on death), Slot 1/2 (Enable on death)
+ void clearBit(int n);
+
};
} // End of namespace Insane
Commit: ad764f728c25ce20a043ea0490dce3ecee40fc3c
https://github.com/scummvm/scummvm/commit/ad764f728c25ce20a043ea0490dce3ecee40fc3c
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:08:44+02:00
Commit Message:
SCUMM: RA2: Implement player damage from enemies
Changed paths:
engines/scumm/insane/insane_rebel.cpp
engines/scumm/insane/insane_rebel.h
engines/scumm/nut_renderer.cpp
engines/scumm/nut_renderer.h
diff --git a/engines/scumm/insane/insane_rebel.cpp b/engines/scumm/insane/insane_rebel.cpp
index 0482872c0c2..3f1e062b3e6 100644
--- a/engines/scumm/insane/insane_rebel.cpp
+++ b/engines/scumm/insane/insane_rebel.cpp
@@ -79,6 +79,10 @@ InsaneRebel2::InsaneRebel2(ScummEngine_v7 *scumm) {
_rebelHandler = 8; // Default to Handler 8 (ground vehicle) for Level 1
_rebelLevelType = 0; // Level type from Opcode 6 par3, determines HUD sprite variant
+ _playerDamage = 0;
+ _playerLives = 3;
+ _playerScore = 0;
+
_speed = 12;
_insaneIsRunning = false;
@@ -451,7 +455,62 @@ void InsaneRebel2::iactRebel2Scene1(byte *renderBitmap, int32 codecparam, int32
} else if (par1 == 3) {
// Opcode 3: Often used for clearing/resetting
- debug("Rebel2 IACT Opcode 3: par2=%d par3=%d par4=%d", par2, par3, par4);
+ // debug("Rebel2 IACT Opcode 3: par2=%d par3=%d par4=%d", par2, par3, par4);
+
+ // Handle damage dealing (Subcode 5 in FUN_401234)
+ // Opcode 3 is complicated:
+ // Based on FUN_401234, case 1 (Opcode 3), checks local_c (offset +4 / par2?)
+ // In iactRebel2Scene1, par1=Opcode.
+ // If par1 == 1 (Opcode 1 / FUN_401234 case 1):
+ // It checks local_c which is from offset +4 (par3).
+ // If par3 == 5 -> Damage Logic.
+ //
+ // Actually, FUN_401234 switches on `*local_14 - 2`.
+ // If `*local_14` (Opcode) is 3, switch value is 1.
+ // So Opcode 3 -> Case 1.
+
+ if (par1 == 3) {
+ // Inside Case 1: local_c = local_14[2] which is offset +4 (par3)
+ if (par3 == 5) {
+ // Damage Logic
+ // bVar1 = FUN_423970(local_14[5]); // Check if source enemy (offset +10 / par6?) is active
+ // Read extra params from stream
+ // Note: `procIACT` already read parems into par1..par4.
+ // But Opcode 3 logic in FUN_401234 uses offset +10 (par6) which isn't passed to `procIACT` signature fully?
+ // Wait, `procIACT` reads 4 shorts.
+ // par1 (+0), par2 (+2), par3 (+4), par4 (+6).
+ // We need +8 and +10.
+
+ b.skip(2); // Offset +8 (par5 used in assembly but not here yet)
+ int16 par6 = b.readSint16LE(); // Offset +10 (Enemy ID?)
+
+ // Check if enemy is disabled (processed in FUN_423970)
+ bool enemyDisabled = isBitSet(par6);
+
+ if (!enemyDisabled) {
+ // Probability check: Random(100) < Limit
+ // The limit seems to come from a table or fixed value?
+ // In FUN_401234: `sVar2 = *(short *)(&DAT_0047e0fc + ...)`
+ // For now, let's just use a fixed probability or assume successful hit
+ // if (rnd < prob) ...
+
+ // Increment damage
+ // In assembly: DAT_0047a7ec += damage
+ // Damage amount also comes from table?
+ int damageAmount = 5; // Placeholder
+
+ // Only apply damage occasionally to simulate probability
+ if ((_vm->_system->getMillis() % 100) < 20) { // 20% chance per frame (approx)
+ _playerDamage += damageAmount;
+ if (_playerDamage > 255) _playerDamage = 255;
+
+ debug("Rebel2: Player HIT by Enemy %d. Damage=%d", par6, _playerDamage);
+
+ // TODO: Flash screen red / shake
+ }
+ }
+ }
+ }
} else if (par1 == 5) {
// Opcode 5: Special handling based on par2 value
@@ -803,7 +862,49 @@ void InsaneRebel2::procPostRendering(byte *renderBitmap, int32 codecparam, int32
// The width is scaled based on shield value (param_1 >> 2)
// For now, draw at position (0, statusBarY) - sprite has internal positioning
if (_smush_cockpitNut->getNumChars() > 6) {
- smlayer_drawSomething(renderBitmap, pitch, 0, statusBarY, 0, _smush_cockpitNut, 6, 0, 0);
+ // Calculate width based on damage.
+ // Assuming max damage 255 = empty bar.
+ // Bar max width is 64 pixels.
+ // Damage 0 = Width 64. Damage 255 = Width 0.
+ // Width = 64 - (Damage / 4)
+ int barWidth = 64 - (_playerDamage / 4);
+ if (barWidth < 0) barWidth = 0;
+
+ // We need to draw a partial sprite or use a clip rect.
+ // smlayer_drawSomething supports scaling/clip?
+ // The current implementation of smlayer_drawSomething just draws the whole sprite.
+ // We can pass a "frame" or clip rect if we modify the function or use a lower level draw.
+ // For now, let's just draw the full sprite if damage < 255, to verify it appears.
+ // Ideally we should implement clipping.
+
+ // NOTE: smlayer_drawSomething calls `_smush_cockpitNut->draw(...)`
+ // We can't easily clip without modifying NutRenderer or using a custom draw loop.
+ // Let's implement a custom draw loop for the shield bar here since it's simple.
+
+ // Sprite 6 data
+ const byte *src = _smush_cockpitNut->getCharData(6);
+ int sw = _smush_cockpitNut->getCharWidth(6);
+ int sh = _smush_cockpitNut->getCharHeight(6);
+ int sx = 63; // Hardcoded X offset from assembly
+ int sy = 9; // Hardcoded Y offset (relative to status bar top)
+
+ // Draw clipped width
+ if (src && sw > 0 && sh > 0) {
+ int drawWidth = MIN(barWidth, sw);
+ for (int y = 0; y < sh; y++) {
+ for (int x = 0; x < drawWidth; x++) {
+ // Render to (0 + sx + x, statusBarY + sy + y)
+ int destX = sx + x;
+ int destY = statusBarY + sy + y;
+ if (destX < pitch && destY < height) {
+ byte pixel = src[y * sw + x];
+ if (pixel != 0) {
+ renderBitmap[destY * pitch + destX] = pixel;
+ }
+ }
+ }
+ }
+ }
}
// Draw shield alert overlay (sprite 7) when shields critical (> 0xAA = 170)
diff --git a/engines/scumm/insane/insane_rebel.h b/engines/scumm/insane/insane_rebel.h
index 3fc14b2cc5b..520c4321e92 100644
--- a/engines/scumm/insane/insane_rebel.h
+++ b/engines/scumm/insane/insane_rebel.h
@@ -165,9 +165,16 @@ public:
// userId: HUD slot (1-4), animData: raw ANIM data, size: data size, renderBitmap: current frame buffer
void loadEmbeddedSan(int userId, byte *animData, int32 size, byte *renderBitmap) override;
+
+
int16 _rebelLinks[512][3]; // Dependency links: Slot 0 (Disable on death), Slot 1/2 (Enable on death)
void clearBit(int n);
+ int16 _playerDamage; // 0 to 255 (Accumulated damage)
+ int16 _playerLives;
+ int32 _playerScore;
+
+
};
} // End of namespace Insane
diff --git a/engines/scumm/nut_renderer.cpp b/engines/scumm/nut_renderer.cpp
index 3bac87e4fb0..2213db9947d 100644
--- a/engines/scumm/nut_renderer.cpp
+++ b/engines/scumm/nut_renderer.cpp
@@ -207,6 +207,13 @@ int NutRenderer::getCharHeight(byte c) const {
return _chars[c].height;
}
+const byte *NutRenderer::getCharData(byte c) {
+ if (c >= _numChars)
+ error("invalid character in NutRenderer::getCharData : %d (%d)", c, _numChars);
+
+ return _chars[c].src;
+}
+
void NutRenderer::drawFrame(byte *dst, int c, int x, int y, int pitch) {
const int width = MIN((int)_chars[c].width, _vm->_screenWidth - x);
const int height = MIN((int)_chars[c].height, _vm->_screenHeight - y);
diff --git a/engines/scumm/nut_renderer.h b/engines/scumm/nut_renderer.h
index d7f92bf1076..8077816ca35 100644
--- a/engines/scumm/nut_renderer.h
+++ b/engines/scumm/nut_renderer.h
@@ -74,6 +74,7 @@ public:
int getCharWidth(byte c) const;
int getCharHeight(byte c) const;
+ const byte *getCharData(byte c);
int getFontHeight() const { return _fontHeight; }
};
Commit: d94e6274e107f13740418ba2aa680bfeca31ee2f
https://github.com/scummvm/scummvm/commit/d94e6274e107f13740418ba2aa680bfeca31ee2f
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:08:44+02:00
Commit Message:
SCUMM: RA2: Implement enemy explosion slots
Changed paths:
engines/scumm/insane/insane_rebel.cpp
engines/scumm/insane/insane_rebel.h
diff --git a/engines/scumm/insane/insane_rebel.cpp b/engines/scumm/insane/insane_rebel.cpp
index 3f1e062b3e6..081178f69e8 100644
--- a/engines/scumm/insane/insane_rebel.cpp
+++ b/engines/scumm/insane/insane_rebel.cpp
@@ -166,6 +166,11 @@ InsaneRebel2::InsaneRebel2(ScummEngine_v7 *scumm) {
_rebelLinks[i][1] = 0;
_rebelLinks[i][2] = 0;
}
+
+ for (i = 0; i < 5; i++) {
+ _explosions[i].active = false;
+ _explosions[i].counter = 0;
+ }
}
@@ -205,12 +210,16 @@ int32 InsaneRebel2::processMouse() {
// Enemy hit!
it->active = false;
it->destroyed = true; // Mark as destroyed so IACT won't re-activate
- it->explosionFrame = 0; // Start explosion animation
- it->explosionComplete = false; // Explosion not yet finished
debug("Rebel2: HIT enemy ID=%d at (%d,%d) - Rect: (%d,%d)-(%d,%d)",
it->id, mousePos.x, mousePos.y,
it->rect.left, it->rect.top, it->rect.right, it->rect.bottom);
+ // Spawn explosion using native system
+ // Use width / 2 as the scale parameter
+ spawnExplosion((it->rect.left + it->rect.right) / 2,
+ (it->rect.top + it->rect.bottom) / 2,
+ it->rect.width() / 2);
+
// Disable self
setBit(it->id);
@@ -718,6 +727,21 @@ void InsaneRebel2::loadEmbeddedSan(int userId, byte *animData, int32 size, byte
end_parsing:;
}
+void InsaneRebel2::spawnExplosion(int x, int y, int objectHalfWidth) {
+ // Find first free slot (FUN_40A2E0 logic)
+ for (int i = 0; i < 5; i++) {
+ if (!_explosions[i].active || _explosions[i].counter <= 0) {
+ _explosions[i].active = true;
+ _explosions[i].counter = 10;
+ _explosions[i].x = x;
+ _explosions[i].y = y;
+ _explosions[i].scale = objectHalfWidth;
+ // TODO: Play sound via FUN_0041189e equivalent
+ break;
+ }
+ }
+}
+
void InsaneRebel2::procPostRendering(byte *renderBitmap, int32 codecparam, int32 setupsan12,
int32 setupsan13, int32 curFrame, int32 maxFrame) {
@@ -948,35 +972,7 @@ void InsaneRebel2::procPostRendering(byte *renderBitmap, int32 codecparam, int32
if (it->destroyed) {
// Handle destroyed enemies - draw explosion animation
// The enemy frame updates are now skipped in decodeFrameObject via shouldSkipFrameUpdate
-
- // Draw explosion animation - only while animation is in progress
- // CPITIMAG.NUT indices 9-41 contain explosion frames, but we only use 8 for a quick effect
- const int EXPLOSION_FRAMES = 8;
- if (it->explosionFrame >= 0 && it->explosionFrame < EXPLOSION_FRAMES) {
- if (_smush_iconsNut) {
- int explosionSpriteIndex = 9 + it->explosionFrame;
- if (_smush_iconsNut->getNumChars() > explosionSpriteIndex) {
- int ew = _smush_iconsNut->getCharWidth(explosionSpriteIndex);
- int eh = _smush_iconsNut->getCharHeight(explosionSpriteIndex);
- // Center explosion on the enemy bounding box
- int cx = (r.left + r.right) / 2 - ew / 2;
- int cy = (r.top + r.bottom) / 2 - eh / 2;
- smlayer_drawSomething(renderBitmap, pitch, cx, cy, 0, _smush_iconsNut, explosionSpriteIndex, 0, 0);
- }
- }
-
- // Advance explosion frame
- it->explosionFrame++;
-
- // Check if explosion is now complete
- if (it->explosionFrame >= EXPLOSION_FRAMES) {
- it->explosionFrame = -1; // Mark as done - prevents any further animation
- it->explosionComplete = true;
- debug("Rebel2: Explosion complete for enemy ID=%d", it->id);
- }
- }
- // After explosion is complete (explosionFrame == -1), do nothing
- // After explosion is complete (explosionFrame == -1), do nothing
+ // Note: Explosion rendering is now handled by the separate 5-slot system
} else if (it->active && !isBitSet(it->id)) { // Only draw if active AND not disabled by IACT
// Draw bounding box outline for active enemies (debug visualization)
// Draw Top
@@ -1006,6 +1002,45 @@ void InsaneRebel2::procPostRendering(byte *renderBitmap, int32 codecparam, int32
}
}
+ // Draw 5-slot Explosion System
+ if (_smush_iconsNut) {
+ for (int i = 0; i < 5; i++) {
+ if (_explosions[i].active) {
+ if (_explosions[i].counter <= 0) {
+ _explosions[i].active = false;
+ continue;
+ }
+
+ // Determine base sprite index based on scale (FUN_409FBC logic)
+ int baseIndex;
+ if (_explosions[i].scale < 11) {
+ baseIndex = 9; // Small/Medium transition
+ } else if (_explosions[i].scale < 21) {
+ baseIndex = 19; // Medium/Large transition
+ } else {
+ baseIndex = 29; // Large/XL transition
+ }
+
+ // Formula: Base + (12 - Counter)
+ // Counter goes 10 -> 1.
+ // Frame goes Base+2 -> Base+11.
+ int spriteIndex = baseIndex + (12 - _explosions[i].counter);
+
+ if (_smush_iconsNut->getNumChars() > spriteIndex) {
+ int ew = _smush_iconsNut->getCharWidth(spriteIndex);
+ int eh = _smush_iconsNut->getCharHeight(spriteIndex);
+ int cx = _explosions[i].x - ew / 2;
+ int cy = _explosions[i].y - eh / 2;
+
+ // Draw explosion
+ smlayer_drawSomething(renderBitmap, pitch, cx, cy, 0, _smush_iconsNut, spriteIndex, 0, 0);
+ }
+
+ _explosions[i].counter--;
+ }
+ }
+ }
+
// Draw Crosshair/Reticle cursor
// Sprite indices based on handler type (from original game disassembly):
// - Handler 8 (ground vehicle): Index 0x2E (46)
diff --git a/engines/scumm/insane/insane_rebel.h b/engines/scumm/insane/insane_rebel.h
index 520c4321e92..add5db5ddab 100644
--- a/engines/scumm/insane/insane_rebel.h
+++ b/engines/scumm/insane/insane_rebel.h
@@ -170,6 +170,17 @@ public:
int16 _rebelLinks[512][3]; // Dependency links: Slot 0 (Disable on death), Slot 1/2 (Enable on death)
void clearBit(int n);
+ struct Explosion {
+ int x, y;
+ int width, height;
+ int counter; // Duration counter (starts at 10)
+ int scale; // Determines sprite set (small/med/large)
+ bool active;
+ };
+
+ Explosion _explosions[5];
+ void spawnExplosion(int x, int y, int objectHalfWidth);
+
int16 _playerDamage; // 0 to 255 (Accumulated damage)
int16 _playerLives;
int32 _playerScore;
Commit: a8843cc89970243453ca66f5c8febabc4ce16205
https://github.com/scummvm/scummvm/commit/a8843cc89970243453ca66f5c8febabc4ce16205
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:08:45+02:00
Commit Message:
SCUMM: RA2: Implement laser animations
Changed paths:
engines/scumm/insane/insane_rebel.cpp
engines/scumm/insane/insane_rebel.h
diff --git a/engines/scumm/insane/insane_rebel.cpp b/engines/scumm/insane/insane_rebel.cpp
index 081178f69e8..98ca2ecf7f6 100644
--- a/engines/scumm/insane/insane_rebel.cpp
+++ b/engines/scumm/insane/insane_rebel.cpp
@@ -171,6 +171,11 @@ InsaneRebel2::InsaneRebel2(ScummEngine_v7 *scumm) {
_explosions[i].active = false;
_explosions[i].counter = 0;
}
+
+ for (i = 0; i < 2; i++) {
+ _shots[i].active = false;
+ _shots[i].counter = 0;
+ }
}
@@ -198,6 +203,9 @@ int32 InsaneRebel2::processMouse() {
debug("Rebel2 Click: Mouse=(%d,%d) Enemies=%d",
mousePos.x, mousePos.y, _rebelEnemies.size());
+ // Spawn visual shot immediately
+ spawnShot(mousePos.x, mousePos.y);
+
// Check for hit on any active enemy
Common::List<RebelEnemy>::iterator it;
for (it = _rebelEnemies.begin(); it != _rebelEnemies.end(); ++it) {
@@ -742,6 +750,117 @@ void InsaneRebel2::spawnExplosion(int x, int y, int objectHalfWidth) {
}
}
+void InsaneRebel2::spawnShot(int x, int y) {
+ // Find free shot slot (2 slots total)
+ for (int i = 0; i < 2; i++) {
+ if (!_shots[i].active) {
+ _shots[i].active = true;
+ _shots[i].counter = 4; // Lasts 4 frames
+ _shots[i].x = x;
+ _shots[i].y = y;
+ // TODO: Play laser sound
+ break;
+ }
+ }
+}
+
+void InsaneRebel2::drawTexturedLine(byte *dst, int pitch, int width, int height, int x0, int y0, int x1, int y1, NutRenderer *nut, int spriteIdx) {
+ if (!nut || spriteIdx >= nut->getNumChars()) return;
+
+ const byte *srcData = nut->getCharData(spriteIdx);
+ int texW = nut->getCharWidth(spriteIdx);
+ int texH = nut->getCharHeight(spriteIdx);
+
+ if (!srcData || texW <= 0 || texH <= 0) return;
+
+ int dx = abs(x1 - x0), sx = x0 < x1 ? 1 : -1;
+ int dy = -abs(y1 - y0), sy = y0 < y1 ? 1 : -1;
+ int err = dx + dy, e2;
+
+ // Length of line for uv mapping
+ // float length = sqrt(dx*dx + dy*dy);
+
+ int texCenterX = texW / 2;
+ int texCenterY = texH / 2;
+ byte color = srcData[texCenterY * texW + texCenterX];
+
+ // If color is 0/transparent, try to find a valid one
+ if (color == 0) color = 255; // Fallback to white
+
+ // Standard Bresenham but with texture sampling attempt
+ // To make it look like a "beam", we need width.
+
+ for (;;) {
+ // Draw a 3x3 blob or similar to make it thick
+ if (x0 >= 1 && x0 < width - 1 && y0 >= 1 && y0 < height - 1) {
+ dst[y0 * pitch + x0] = color;
+ // Glow effect (simulated with same color for now)
+ // dst[(y0+1) * pitch + x0] = color;
+ // dst[y0 * pitch + (x0+1)] = color;
+ }
+
+ if (x0 == x1 && y0 == y1) break;
+ e2 = 2 * err;
+ if (e2 >= dy) { err += dy; x0 += sx; }
+ if (e2 <= dx) { err += dx; y0 += sy; }
+ }
+}
+
+void InsaneRebel2::drawLaserBeam(byte *dst, int pitch, int width, int height, int x0, int y0, int x1, int y1, int param_9, NutRenderer *nut, int spriteIdx) {
+ if (!nut || spriteIdx >= nut->getNumChars()) return;
+
+ // Reverse-engineered math from FUN_0040bbf6
+ int texW = nut->getCharWidth(spriteIdx);
+ int texH = nut->getCharHeight(spriteIdx);
+ int param_11 = 12; // Standard value from assembly calls
+ if (texW == 0) return;
+
+ // sVar7 = (12 * texH * 16) / texW
+ int sVar7 = (param_11 * texH * 16) / texW;
+
+ // Extend vector by (13/12)
+ int dx = x1 - x0;
+ int dy = y1 - y0;
+ int extDx = (dx * (param_11 + 1)) / param_11;
+ int extDy = (dy * (param_11 + 1)) / param_11;
+
+ int xExt = x0 + extDx;
+ int yExt = y0 + extDy;
+
+ // Calculate New Start Point (sVar4, sVar5)
+ // Start = ExtEnd - (ExtEnd - Start) * (16 / (sVar7 + 16))
+ // Actually math is: sVar4 = (xExt) - (extDx * 16) / (sVar7 + 16)
+ int startFactor = sVar7 + 16;
+ if (startFactor == 0) startFactor = 1;
+ int startX = xExt - (extDx * 16) / startFactor;
+ int startY = yExt - (extDy * 16) / startFactor;
+
+ // Calculate New End Point (sVar6, sVar7 in asm) w.r.t param_9
+ // End = ExtEnd - (ExtEnd - Start) * (16 / (param_9 + sVar7 + 16))
+ int endFactor = param_9 + sVar7 + 16;
+ if (endFactor == 0) endFactor = 1; // Safety
+ int endX = xExt - (extDx * 16) / endFactor;
+ int endY = yExt - (extDy * 16) / endFactor;
+
+ // Draw the segment
+ drawTexturedLine(dst, pitch, width, height, startX, startY, endX, endY, nut, spriteIdx);
+}
+
+void InsaneRebel2::drawLine(byte *dst, int pitch, int width, int height, int x0, int y0, int x1, int y1, byte color) {
+ int dx = abs(x1 - x0), sx = x0 < x1 ? 1 : -1;
+ int dy = -abs(y1 - y0), sy = y0 < y1 ? 1 : -1;
+ int err = dx + dy, e2;
+
+ for (;;) {
+ if (x0 >= 0 && x0 < width && y0 >= 0 && y0 < height)
+ dst[y0 * pitch + x0] = color;
+ if (x0 == x1 && y0 == y1) break;
+ e2 = 2 * err;
+ if (e2 >= dy) { err += dy; x0 += sx; }
+ if (e2 <= dx) { err += dx; y0 += sy; }
+ }
+}
+
void InsaneRebel2::procPostRendering(byte *renderBitmap, int32 codecparam, int32 setupsan12,
int32 setupsan13, int32 curFrame, int32 maxFrame) {
@@ -1041,6 +1160,73 @@ void InsaneRebel2::procPostRendering(byte *renderBitmap, int32 codecparam, int32
}
}
+ // Draw Laser Shots
+ // Gun Positions (Approximate for Turret Mode / Default):
+ // Left: (10, 190), Right: (310, 190) - Adjusted for low-res
+ const int GUN_LEFT_X = 10;
+ const int GUN_LEFT_Y = 190;
+ const int GUN_RIGHT_X = 310;
+ const int GUN_RIGHT_Y = 190;
+
+ for (int i = 0; i < 2; i++) {
+ if (_shots[i].active) {
+ if (_shots[i].counter <= 0) {
+ _shots[i].active = false;
+ continue;
+ }
+
+ bool drawn = false;
+ // Use Sprite 5 from CPITIMAG.NUT as the laser texture
+ // Confirmed by reverse engineering: Laser uses Sprite 5 (136x13)
+ if (_smush_iconsNut && _smush_iconsNut->getNumChars() > 5) {
+ // Calculate param_9 equivalent (Counter dependent)
+ // Original: TableValue - Counter
+ // We don't have table, but assembly suggests values around 0x20-0x80 range?
+ // Math divides by (param_9 + offset). Higher param_9 = Longer beam.
+ // As counter decreases (4->0), value increases (26->30), beam grows/moves.
+ // Let's try base 30.
+ int base = 30;
+ int param_9 = base - _shots[i].counter;
+
+ // Draw Left Beam
+ drawLaserBeam(renderBitmap, pitch, width, height, GUN_LEFT_X, GUN_LEFT_Y, _shots[i].x, _shots[i].y, param_9, _smush_iconsNut, 5);
+ // Draw Right Beam
+ drawLaserBeam(renderBitmap, pitch, width, height, GUN_RIGHT_X, GUN_RIGHT_Y, _shots[i].x, _shots[i].y, param_9, _smush_iconsNut, 5);
+
+ // Draw Projectile Impact
+ // Using Sprite 0 (small flash) or similar at impact point
+ smlayer_drawSomething(renderBitmap, pitch, _shots[i].x - 7, _shots[i].y - 7, 0, _smush_iconsNut, 0, 0, 0);
+ drawn = true;
+ }
+
+ if (!drawn) {
+
+ // Fallback if sprite 5 missing
+ static bool warnedOnce = false;
+ assert(false);
+ if (!warnedOnce) {
+ debug("Rebel2: Failed to draw textured laser. _smush_iconsNut=%p, numChars=%d",
+ (void *)_smush_iconsNut, _smush_iconsNut ? _smush_iconsNut->getNumChars() : -1);
+ warnedOnce = true;
+
+ // Attempt to lazy load CPITIMHI.NUT if we haven't tried
+ if (true) {
+ delete _smush_iconsNut;
+ _smush_iconsNut = new NutRenderer(_vm, "SYSTM/CPITIMHI.NUT");
+ debug("Rebel2: Attempted to fallback load SYSTM/CPITIMHI.NUT. New numChars=%d",
+ _smush_iconsNut ? _smush_iconsNut->getNumChars() : -1);
+ }
+ }
+
+ // Draw WHITE lines to be visible against any background
+ drawLine(renderBitmap, pitch, width, height, GUN_LEFT_X, GUN_LEFT_Y, _shots[i].x, _shots[i].y, 255);
+ drawLine(renderBitmap, pitch, width, height, GUN_RIGHT_X, GUN_RIGHT_Y, _shots[i].x, _shots[i].y, 255);
+ }
+
+ _shots[i].counter--;
+ }
+ }
+
// Draw Crosshair/Reticle cursor
// Sprite indices based on handler type (from original game disassembly):
// - Handler 8 (ground vehicle): Index 0x2E (46)
diff --git a/engines/scumm/insane/insane_rebel.h b/engines/scumm/insane/insane_rebel.h
index add5db5ddab..e9a43428264 100644
--- a/engines/scumm/insane/insane_rebel.h
+++ b/engines/scumm/insane/insane_rebel.h
@@ -70,6 +70,10 @@ class InsaneRebel2 : public Insane {
}
}
+ void drawLine(byte *dst, int pitch, int width, int height, int x0, int y0, int x1, int y1, byte color);
+ void drawTexturedLine(byte *dst, int pitch, int width, int height, int x0, int y0, int x1, int y1, NutRenderer *nut, int spriteIdx);
+ void drawLaserBeam(byte *dst, int pitch, int width, int height, int x0, int y0, int x1, int y1, int param_9, NutRenderer *nut, int spriteIdx);
+
public:
InsaneRebel2(ScummEngine_v7 *scumm);
~InsaneRebel2();
@@ -186,6 +190,14 @@ public:
int32 _playerScore;
+ struct Shot {
+ bool active;
+ int counter;
+ int x, y; // Target position
+ };
+ Shot _shots[2];
+ void spawnShot(int x, int y);
+
};
} // End of namespace Insane
Commit: 2619d4afc6c4f5cc8c7254fb37206047fee6c4eb
https://github.com/scummvm/scummvm/commit/2619d4afc6c4f5cc8c7254fb37206047fee6c4eb
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:08:45+02:00
Commit Message:
SCUMM: RA2: Refactor enemy state handling
Changed paths:
engines/scumm/insane/insane_rebel.cpp
engines/scumm/insane/insane_rebel.h
diff --git a/engines/scumm/insane/insane_rebel.cpp b/engines/scumm/insane/insane_rebel.cpp
index 98ca2ecf7f6..58aa24aab2d 100644
--- a/engines/scumm/insane/insane_rebel.cpp
+++ b/engines/scumm/insane/insane_rebel.cpp
@@ -360,79 +360,7 @@ void InsaneRebel2::iactRebel2Scene1(byte *renderBitmap, int32 codecparam, int32
// screenY = centerY - DAT_0043e008 (scrollY)
if (par1 == 4) {
- // Opcode 4: Enemy position update
- // Read 5 shorts from the stream (offset +8 through +16)
- int16 enemyId = b.readSint16LE(); // Offset +8
- int16 x = b.readSint16LE(); // Offset +10 (0x0A)
-
- // If enemy is disabled in bit table, skip update
- bool disabled = isBitSet(enemyId);
-
- int16 y = b.readSint16LE(); // Offset +12 (0x0C)
- int16 w = b.readSint16LE(); // Offset +14 (0x0E) - Width
- int16 h = b.readSint16LE(); // Offset +16 (0x10) - Height
-
- // If disabled, stop processing this object
- if (disabled) {
- // debug("Rebel2: Skipping Opcode 4 for disabled enemy ID=%d", enemyId);
- return;
- }
-
- // The disassembly shows half-width/half-height are used for centering:
- // halfW = w >> 1
- // halfH = h >> 1
- // centerX = x + halfW
- // centerY = y + halfH
- // But for drawing the bounding box, we want the top-left corner (x, y) and full dimensions.
-
- //debug("Rebel2 IACT Opcode 4: ID=%d X=%d Y=%d W=%d H=%d (par2=%d par3=%d par4=%d)",
- // enemyId, x, y, w, h, par2, par3, par4);
-
- // Update RebelEnemy list for hit detection
- bool found = false;
- Common::List<RebelEnemy>::iterator it;
- for (it = _rebelEnemies.begin(); it != _rebelEnemies.end(); ++it) {
- if (it->id == enemyId) {
- it->rect = Common::Rect(x, y, x + w, y + h);
- // Only re-activate if not destroyed
- if (!it->destroyed) {
- it->active = true;
- }
- found = true;
- break;
- }
- }
- if (!found) {
- RebelEnemy e;
- e.id = enemyId;
- e.rect = Common::Rect(x, y, x + w, y + h);
- e.active = true;
- e.destroyed = false;
- e.explosionFrame = -1; // No explosion playing
- _rebelEnemies.push_back(e);
- }
-
- // Render Bounding Box for visual verification
- // Use player dimensions for clipping
- int bufWidth = (_player && _player->_width > 0) ? _player->_width : 320;
- int bufHeight = (_player && _player->_height > 0) ? _player->_height : 200;
- int pitch = bufWidth;
-
- // Draw the bounding box in color 255 (bright white) - only for non-destroyed enemies
- // Skip drawing if this enemy has been destroyed
- if (renderBitmap && w > 0 && h > 0 && !found) {
- // Only draw for newly created enemies (not destroyed ones)
- drawRect(renderBitmap, pitch, x, y, w, h, 255, bufWidth, bufHeight);
- } else if (found && !it->destroyed) {
- // Draw for existing non-destroyed enemies
- drawRect(renderBitmap, pitch, x, y, w, h, 255, bufWidth, bufHeight);
- }
-
- // Debug for potential HUD Setup (Opcode 4 with word at 8 == 1)
- if (enemyId == 1) {
- debug("Rebel2 IACT Opcode 4: HUD Setup Candidate? (enemyId/val at 8 == 1). par4=%d", par4);
- }
-
+ enemyUpdate(renderBitmap, b, par2, par3, par4);
} else if (par1 == 2) {
// Opcode 2: Often used for bit setting
// Disassembly shows this is handled but we don't have full context
@@ -574,6 +502,57 @@ void InsaneRebel2::iactRebel2Scene1(byte *renderBitmap, int32 codecparam, int32
}
}
+void InsaneRebel2::enemyUpdate(byte *renderBitmap, Common::SeekableReadStream &b, int16 par2, int16 par3, int16 par4) {
+ // Opcode 4: Enemy position update
+ // Read 5 shorts from the stream (offset +8 through +16)
+ int16 enemyId = b.readSint16LE(); // Offset +8
+ int16 x = b.readSint16LE(); // Offset +10 (0x0A)
+
+ // If enemy is disabled in bit table, skip update
+ bool disabled = isBitSet(enemyId);
+
+ int16 y = b.readSint16LE(); // Offset +12 (0x0C)
+ int16 w = b.readSint16LE(); // Offset +14 (0x0E) - Width
+ int16 h = b.readSint16LE(); // Offset +16 (0x10) - Height
+
+ // If disabled, stop processing this object
+ if (disabled) {
+ // debug("Rebel2: Skipping Opcode 4 for disabled enemy ID=%d", enemyId);
+ return;
+ }
+
+ // The disassembly shows half-width/half-height are used for centering:
+ // halfW = w >> 1
+ // halfH = h >> 1
+ // centerX = x + halfW
+ // centerY = y + halfH
+ // But for drawing the bounding box, we want the top-left corner (x, y) and full dimensions.
+
+ // Update RebelEnemy list for hit detection
+ bool found = false;
+ Common::List<RebelEnemy>::iterator it;
+ for (it = _rebelEnemies.begin(); it != _rebelEnemies.end(); ++it) {
+ if (it->id == enemyId) {
+ it->rect = Common::Rect(x, y, x + w, y + h);
+ // Only re-activate if not destroyed
+ if (!it->destroyed) {
+ it->active = true;
+ }
+ found = true;
+ break;
+ }
+ }
+ if (!found) {
+ RebelEnemy e;
+ e.id = enemyId;
+ e.rect = Common::Rect(x, y, x + w, y + h);
+ e.active = true;
+ e.destroyed = false;
+ e.explosionFrame = -1; // No explosion playing
+ _rebelEnemies.push_back(e);
+ }
+}
+
// External helpers from smush_player.cpp but we are already in Scumm namespace
extern void smushDecodeRLE(byte *dst, const byte *src, int left, int top, int width, int height, int pitch);
extern void smushDecodeUncompressed(byte *dst, const byte *src, int left, int top, int width, int height, int pitch);
diff --git a/engines/scumm/insane/insane_rebel.h b/engines/scumm/insane/insane_rebel.h
index e9a43428264..c3c1c44edbd 100644
--- a/engines/scumm/insane/insane_rebel.h
+++ b/engines/scumm/insane/insane_rebel.h
@@ -35,45 +35,6 @@ namespace Scumm {
class InsaneRebel2 : public Insane {
- // Helper for debugging IACT boxes
- void drawRect(byte *dst, int pitch, int x, int y, int w, int h, byte color, int bufWidth, int bufHeight) {
- Common::Rect r(x, y, x + w, y + h);
- Common::Rect screen(0, 0, bufWidth, bufHeight);
- Common::Rect clipped = r;
- clipped.clip(screen);
-
- if (!clipped.isValidRect()) return;
-
- // Top
- if (r.top >= 0 && r.top < bufHeight) {
- int startX = MAX((int)r.left, 0);
- int endX = MIN((int)r.right, bufWidth);
- for (int k = startX; k < endX; k++) dst[r.top * pitch + k] = color;
- }
- // Bottom
- if (r.bottom > 0 && r.bottom <= bufHeight) {
- int startX = MAX((int)r.left, 0);
- int endX = MIN((int)r.right, bufWidth);
- for (int k = startX; k < endX; k++) dst[(r.bottom - 1) * pitch + k] = color;
- }
- // Left
- if (r.left >= 0 && r.left < bufWidth) {
- int startY = MAX((int)r.top, 0);
- int endY = MIN((int)r.bottom, bufHeight);
- for (int k = startY; k < endY; k++) dst[k * pitch + r.left] = color;
- }
- // Right
- if (r.right > 0 && r.right <= bufWidth) {
- int startY = MAX((int)r.top, 0);
- int endY = MIN((int)r.bottom, bufHeight);
- for (int k = startY; k < endY; k++) dst[k * pitch + (r.right - 1)] = color;
- }
- }
-
- void drawLine(byte *dst, int pitch, int width, int height, int x0, int y0, int x1, int y1, byte color);
- void drawTexturedLine(byte *dst, int pitch, int width, int height, int x0, int y0, int x1, int y1, NutRenderer *nut, int spriteIdx);
- void drawLaserBeam(byte *dst, int pitch, int width, int height, int x0, int y0, int x1, int y1, int param_9, NutRenderer *nut, int spriteIdx);
-
public:
InsaneRebel2(ScummEngine_v7 *scumm);
~InsaneRebel2();
@@ -106,6 +67,10 @@ public:
int32 setupsan13, Common::SeekableReadStream &b, int32 size, int32 flags,
int16 par1, int16 par2, int16 par3, int16 par4) override;
+ void drawLine(byte *dst, int pitch, int width, int height, int x0, int y0, int x1, int y1, byte color);
+ void drawTexturedLine(byte *dst, int pitch, int width, int height, int x0, int y0, int x1, int y1, NutRenderer *nut, int spriteIdx);
+ void drawLaserBeam(byte *dst, int pitch, int width, int height, int x0, int y0, int x1, int y1, int param_9, NutRenderer *nut, int spriteIdx);
+
struct RebelEnemy {
int id;
Common::Rect rect;
@@ -123,6 +88,8 @@ public:
~RebelEnemy() { free(savedBackground); }
};
+ void enemyUpdate(byte *renderBitmap, Common::SeekableReadStream &b, int16 par2, int16 par3, int16 par4);
+
Common::List<RebelEnemy> _rebelEnemies;
// Current handler type for Rebel Assault 2 (determines crosshair sprite)
Commit: 764ecbf4591bb35959e3c7c713c4a857006fd4ca
https://github.com/scummvm/scummvm/commit/764ecbf4591bb35959e3c7c713c4a857006fd4ca
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:08:45+02:00
Commit Message:
SCUMM: RA2: Remove obsolete enemy code
Changed paths:
engines/scumm/insane/insane_rebel.cpp
diff --git a/engines/scumm/insane/insane_rebel.cpp b/engines/scumm/insane/insane_rebel.cpp
index 58aa24aab2d..67270f11833 100644
--- a/engines/scumm/insane/insane_rebel.cpp
+++ b/engines/scumm/insane/insane_rebel.cpp
@@ -825,22 +825,6 @@ void InsaneRebel2::drawLaserBeam(byte *dst, int pitch, int width, int height, in
drawTexturedLine(dst, pitch, width, height, startX, startY, endX, endY, nut, spriteIdx);
}
-void InsaneRebel2::drawLine(byte *dst, int pitch, int width, int height, int x0, int y0, int x1, int y1, byte color) {
- int dx = abs(x1 - x0), sx = x0 < x1 ? 1 : -1;
- int dy = -abs(y1 - y0), sy = y0 < y1 ? 1 : -1;
- int err = dx + dy, e2;
-
- for (;;) {
- if (x0 >= 0 && x0 < width && y0 >= 0 && y0 < height)
- dst[y0 * pitch + x0] = color;
- if (x0 == x1 && y0 == y1) break;
- e2 = 2 * err;
- if (e2 >= dy) { err += dy; x0 += sx; }
- if (e2 <= dx) { err += dx; y0 += sy; }
- }
-}
-
-
void InsaneRebel2::procPostRendering(byte *renderBitmap, int32 codecparam, int32 setupsan12,
int32 setupsan13, int32 curFrame, int32 maxFrame) {
@@ -1154,7 +1138,6 @@ void InsaneRebel2::procPostRendering(byte *renderBitmap, int32 codecparam, int32
continue;
}
- bool drawn = false;
// Use Sprite 5 from CPITIMAG.NUT as the laser texture
// Confirmed by reverse engineering: Laser uses Sprite 5 (136x13)
if (_smush_iconsNut && _smush_iconsNut->getNumChars() > 5) {
@@ -1175,33 +1158,8 @@ void InsaneRebel2::procPostRendering(byte *renderBitmap, int32 codecparam, int32
// Draw Projectile Impact
// Using Sprite 0 (small flash) or similar at impact point
smlayer_drawSomething(renderBitmap, pitch, _shots[i].x - 7, _shots[i].y - 7, 0, _smush_iconsNut, 0, 0, 0);
- drawn = true;
}
- if (!drawn) {
-
- // Fallback if sprite 5 missing
- static bool warnedOnce = false;
- assert(false);
- if (!warnedOnce) {
- debug("Rebel2: Failed to draw textured laser. _smush_iconsNut=%p, numChars=%d",
- (void *)_smush_iconsNut, _smush_iconsNut ? _smush_iconsNut->getNumChars() : -1);
- warnedOnce = true;
-
- // Attempt to lazy load CPITIMHI.NUT if we haven't tried
- if (true) {
- delete _smush_iconsNut;
- _smush_iconsNut = new NutRenderer(_vm, "SYSTM/CPITIMHI.NUT");
- debug("Rebel2: Attempted to fallback load SYSTM/CPITIMHI.NUT. New numChars=%d",
- _smush_iconsNut ? _smush_iconsNut->getNumChars() : -1);
- }
- }
-
- // Draw WHITE lines to be visible against any background
- drawLine(renderBitmap, pitch, width, height, GUN_LEFT_X, GUN_LEFT_Y, _shots[i].x, _shots[i].y, 255);
- drawLine(renderBitmap, pitch, width, height, GUN_RIGHT_X, GUN_RIGHT_Y, _shots[i].x, _shots[i].y, 255);
- }
-
_shots[i].counter--;
}
}
Commit: bd56fe06cc8cca8bbadc657e8791634ef28a5fd4
https://github.com/scummvm/scummvm/commit/bd56fe06cc8cca8bbadc657e8791634ef28a5fd4
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:08:45+02:00
Commit Message:
SCUMM: RA2: Add low-difficulty enemy indicators
Changed paths:
engines/scumm/insane/insane_rebel.cpp
engines/scumm/insane/insane_rebel.h
diff --git a/engines/scumm/insane/insane_rebel.cpp b/engines/scumm/insane/insane_rebel.cpp
index 67270f11833..b7e57c03bd2 100644
--- a/engines/scumm/insane/insane_rebel.cpp
+++ b/engines/scumm/insane/insane_rebel.cpp
@@ -83,6 +83,8 @@ InsaneRebel2::InsaneRebel2(ScummEngine_v7 *scumm) {
_playerLives = 3;
_playerScore = 0;
+ _difficulty = 1; // Default to Medium (1). TODO: Read from game config
+
_speed = 12;
_insaneIsRunning = false;
@@ -825,6 +827,49 @@ void InsaneRebel2::drawLaserBeam(byte *dst, int pitch, int width, int height, in
drawTexturedLine(dst, pitch, width, height, startX, startY, endX, endY, nut, spriteIdx);
}
+void InsaneRebel2::drawLine(byte *dst, int pitch, int width, int height, int x0, int y0, int x1, int y1, byte color) {
+ int dx = abs(x1 - x0), sx = x0 < x1 ? 1 : -1;
+ int dy = -abs(y1 - y0), sy = y0 < y1 ? 1 : -1;
+ int err = dx + dy, e2;
+
+ for (;;) {
+ if (x0 >= 0 && x0 < width && y0 >= 0 && y0 < height) {
+ dst[y0 * pitch + x0] = color;
+ }
+ if (x0 == x1 && y0 == y1) break;
+ e2 = 2 * err;
+ if (e2 >= dy) { err += dy; x0 += sx; }
+ if (e2 <= dx) { err += dx; y0 += sy; }
+ }
+}
+
+void InsaneRebel2::drawCornerBrackets(byte *dst, int pitch, int width, int height, int x, int y, int w, int h, byte color) {
+ // Draw L-shaped brackets at corners of the rect (x,y,w,h)
+ // Bracket size: approx 8 pixels
+ int armLen = 2;
+ if (armLen > w / 2) armLen = w / 2;
+ if (armLen > h / 2) armLen = h / 2;
+
+ int x2 = x + w - 1;
+ int y2 = y + h - 1;
+
+ // Top-Left Corner
+ drawLine(dst, pitch, width, height, x, y, x + armLen, y, color);
+ drawLine(dst, pitch, width, height, x, y, x, y + armLen, color);
+
+ // Top-Right Corner
+ drawLine(dst, pitch, width, height, x2 - armLen, y, x2, y, color);
+ drawLine(dst, pitch, width, height, x2, y, x2, y + armLen, color);
+
+ // Bottom-Left Corner
+ drawLine(dst, pitch, width, height, x, y2, x + armLen, y2, color);
+ drawLine(dst, pitch, width, height, x, y2 - armLen, x, y2, color);
+
+ // Bottom-Right Corner
+ drawLine(dst, pitch, width, height, x2 - armLen, y2, x2, y2, color);
+ drawLine(dst, pitch, width, height, x2, y2 - armLen, x2, y2, color);
+}
+
void InsaneRebel2::procPostRendering(byte *renderBitmap, int32 codecparam, int32 setupsan12,
int32 setupsan13, int32 curFrame, int32 maxFrame) {
@@ -1056,30 +1101,14 @@ void InsaneRebel2::procPostRendering(byte *renderBitmap, int32 codecparam, int32
// The enemy frame updates are now skipped in decodeFrameObject via shouldSkipFrameUpdate
// Note: Explosion rendering is now handled by the separate 5-slot system
} else if (it->active && !isBitSet(it->id)) { // Only draw if active AND not disabled by IACT
- // Draw bounding box outline for active enemies (debug visualization)
- // Draw Top
- if (r.top >= 0 && r.top < height) {
- for (int x = clipped.left; x < clipped.right; x++) {
- renderBitmap[r.top * pitch + x] = 255;
- }
- }
- // Draw Bottom
- if (r.bottom > 0 && r.bottom <= height) {
- for (int x = clipped.left; x < clipped.right; x++) {
- renderBitmap[(r.bottom - 1) * pitch + x] = 255;
- }
- }
- // Draw Left
- if (r.left >= 0 && r.left < width) {
- for (int y = clipped.top; y < clipped.bottom; y++) {
- renderBitmap[y * pitch + r.left] = 255;
- }
- }
- // Draw Right
- if (r.right > 0 && r.right <= width) {
- for (int y = clipped.top; y < clipped.bottom; y++) {
- renderBitmap[y * pitch + (r.right - 1)] = 255;
- }
+ // Draw Green Indicators (Corner Brackets) for Easy (0) and Medium (1) difficulty
+ // Hard (2) mode does not show indicators.
+ if (_difficulty < 2) {
+ const byte color = 5; // Green (Standard VGA Index 10)
+ // Clip the rect to screen bounds for drawing logic is handled inside drawLine if implemented safely,
+ // but drawCornerBrackets relies on drawLine which iterates.
+ // We pass full screen width/height to safe-guard.
+ drawCornerBrackets(renderBitmap, pitch, width, height, r.left, r.top, r.width(), r.height(), color);
}
}
}
diff --git a/engines/scumm/insane/insane_rebel.h b/engines/scumm/insane/insane_rebel.h
index c3c1c44edbd..df1e231da83 100644
--- a/engines/scumm/insane/insane_rebel.h
+++ b/engines/scumm/insane/insane_rebel.h
@@ -165,6 +165,10 @@ public:
Shot _shots[2];
void spawnShot(int x, int y);
+ /* Difficulty Level (0, 1, 2 = Easy, Med, Hard) */
+ int _difficulty;
+ void drawCornerBrackets(byte *dst, int pitch, int width, int height, int x, int y, int w, int h, byte color);
+
};
} // End of namespace Insane
Commit: 6482cf87c4144c5a64a9471520c697224338aec5
https://github.com/scummvm/scummvm/commit/6482cf87c4144c5a64a9471520c697224338aec5
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:08:46+02:00
Commit Message:
SCUMM: RA2: Simplify enemy handling
Changed paths:
engines/scumm/insane/insane_rebel.cpp
engines/scumm/insane/insane_rebel.h
diff --git a/engines/scumm/insane/insane_rebel.cpp b/engines/scumm/insane/insane_rebel.cpp
index b7e57c03bd2..127f6959b72 100644
--- a/engines/scumm/insane/insane_rebel.cpp
+++ b/engines/scumm/insane/insane_rebel.cpp
@@ -75,7 +75,7 @@ InsaneRebel2::InsaneRebel2(ScummEngine_v7 *scumm) {
_smush_icons2Nut = nullptr; // Not used for Rebel2
_smush_cockpitNut = new NutRenderer(_vm, "SYSTM/DISPFONT.NUT");
- _rebelEnemies.clear();
+ _enemies.clear();
_rebelHandler = 8; // Default to Handler 8 (ground vehicle) for Level 1
_rebelLevelType = 0; // Level type from Opcode 6 par3, determines HUD sprite variant
@@ -203,14 +203,14 @@ int32 InsaneRebel2::processMouse() {
if (isPressed && !wasPressed) {
Common::Point mousePos(_vm->_mouse.x, _vm->_mouse.y);
debug("Rebel2 Click: Mouse=(%d,%d) Enemies=%d",
- mousePos.x, mousePos.y, _rebelEnemies.size());
+ mousePos.x, mousePos.y, _enemies.size());
// Spawn visual shot immediately
spawnShot(mousePos.x, mousePos.y);
// Check for hit on any active enemy
- Common::List<RebelEnemy>::iterator it;
- for (it = _rebelEnemies.begin(); it != _rebelEnemies.end(); ++it) {
+ Common::List<enemy>::iterator it;
+ for (it = _enemies.begin(); it != _enemies.end(); ++it) {
debug(" Enemy ID=%d active=%d destroyed=%d rect=(%d,%d)-(%d,%d) contains=%d",
it->id, it->active, it->destroyed,
it->rect.left, it->rect.top, it->rect.right, it->rect.bottom,
@@ -276,8 +276,8 @@ bool InsaneRebel2::shouldSkipFrameUpdate(int left, int top, int width, int heigh
int updateArea = width * height;
// Check if this update region significantly overlaps with any destroyed enemy
- Common::List<RebelEnemy>::iterator it;
- for (it = _rebelEnemies.begin(); it != _rebelEnemies.end(); ++it) {
+ Common::List<enemy>::iterator it;
+ for (it = _enemies.begin(); it != _enemies.end(); ++it) {
if (it->destroyed) {
// Calculate the intersection of the update rect and enemy rect
Common::Rect enemyRect = it->rect;
@@ -530,10 +530,10 @@ void InsaneRebel2::enemyUpdate(byte *renderBitmap, Common::SeekableReadStream &b
// centerY = y + halfH
// But for drawing the bounding box, we want the top-left corner (x, y) and full dimensions.
- // Update RebelEnemy list for hit detection
+ // Update enemy list for hit detection
bool found = false;
- Common::List<RebelEnemy>::iterator it;
- for (it = _rebelEnemies.begin(); it != _rebelEnemies.end(); ++it) {
+ Common::List<enemy>::iterator it;
+ for (it = _enemies.begin(); it != _enemies.end(); ++it) {
if (it->id == enemyId) {
it->rect = Common::Rect(x, y, x + w, y + h);
// Only re-activate if not destroyed
@@ -545,16 +545,23 @@ void InsaneRebel2::enemyUpdate(byte *renderBitmap, Common::SeekableReadStream &b
}
}
if (!found) {
- RebelEnemy e;
- e.id = enemyId;
- e.rect = Common::Rect(x, y, x + w, y + h);
- e.active = true;
- e.destroyed = false;
- e.explosionFrame = -1; // No explosion playing
- _rebelEnemies.push_back(e);
+ init_enemyStruct(enemyId, x, y, w, h, true, false, -1);
}
}
+void InsaneRebel2::init_enemyStruct(int id, int32 x, int32 y, int32 w, int32 h, bool active, bool destroyed, int32 explosionFrame) {
+ enemy e;
+ e.id = id;
+ e.rect = Common::Rect(x, y, x + w, y + h);
+ e.active = active;
+ e.destroyed = destroyed;
+ e.explosionFrame = explosionFrame;
+ e.savedBackground = nullptr;
+ e.savedBgWidth = 0;
+ e.savedBgHeight = 0;
+ _enemies.push_back(e);
+}
+
// External helpers from smush_player.cpp but we are already in Scumm namespace
extern void smushDecodeRLE(byte *dst, const byte *src, int left, int top, int width, int height, int pitch);
extern void smushDecodeUncompressed(byte *dst, const byte *src, int left, int top, int width, int height, int pitch);
@@ -1083,11 +1090,11 @@ void InsaneRebel2::procPostRendering(byte *renderBitmap, int32 codecparam, int32
static uint32 lastDebugTime = 0;
if ((_vm->_system->getMillis() - lastDebugTime) > 2000) {
lastDebugTime = _vm->_system->getMillis();
- debug("Rebel2 Debug: Buffer %dx%d, Pitch %d, BPP %d, Enemies: %d", width, height, pitch, _vm->_virtscr[kMainVirtScreen].format.bytesPerPixel, _rebelEnemies.size());
+ debug("Rebel2 Debug: Buffer %dx%d, Pitch %d, BPP %d, Enemies: %d", width, height, pitch, _vm->_virtscr[kMainVirtScreen].format.bytesPerPixel, _enemies.size());
}
- Common::List<RebelEnemy>::iterator it;
- for (it = _rebelEnemies.begin(); it != _rebelEnemies.end(); ++it) {
+ Common::List<enemy>::iterator it;
+ for (it = _enemies.begin(); it != _enemies.end(); ++it) {
Common::Rect r = it->rect;
// Clip the rect to screen bounds for safety
diff --git a/engines/scumm/insane/insane_rebel.h b/engines/scumm/insane/insane_rebel.h
index df1e231da83..649f5df53e0 100644
--- a/engines/scumm/insane/insane_rebel.h
+++ b/engines/scumm/insane/insane_rebel.h
@@ -71,7 +71,7 @@ public:
void drawTexturedLine(byte *dst, int pitch, int width, int height, int x0, int y0, int x1, int y1, NutRenderer *nut, int spriteIdx);
void drawLaserBeam(byte *dst, int pitch, int width, int height, int x0, int y0, int x1, int y1, int param_9, NutRenderer *nut, int spriteIdx);
- struct RebelEnemy {
+ struct enemy {
int id;
Common::Rect rect;
bool active;
@@ -81,16 +81,12 @@ public:
byte *savedBackground; // Saved background pixels at moment of destruction
int savedBgWidth; // Width of saved background
int savedBgHeight; // Height of saved background
-
- RebelEnemy() : id(0), active(false), destroyed(false), explosionFrame(-1),
- explosionComplete(false), savedBackground(nullptr),
- savedBgWidth(0), savedBgHeight(0) {}
- ~RebelEnemy() { free(savedBackground); }
};
+ void init_enemyStruct(int id, int32 x, int32 y, int32 w, int32 h, bool active, bool destroyed, int32 explosionFrame);
void enemyUpdate(byte *renderBitmap, Common::SeekableReadStream &b, int16 par2, int16 par3, int16 par4);
- Common::List<RebelEnemy> _rebelEnemies;
+ Common::List<enemy> _enemies;
// Current handler type for Rebel Assault 2 (determines crosshair sprite)
// Handler 0: Background only
Commit: 512ae9f8f16e5d2cdbe6700821eaa6a04f6c11d7
https://github.com/scummvm/scummvm/commit/512ae9f8f16e5d2cdbe6700821eaa6a04f6c11d7
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:08:46+02:00
Commit Message:
SCUMM: RA2: Implement cockpit movement effect
Changed paths:
engines/scumm/insane/insane_rebel.cpp
engines/scumm/insane/insane_rebel.h
engines/scumm/smush/smush_player.cpp
engines/scumm/smush/smush_player.h
diff --git a/engines/scumm/insane/insane_rebel.cpp b/engines/scumm/insane/insane_rebel.cpp
index 127f6959b72..4e529f66291 100644
--- a/engines/scumm/insane/insane_rebel.cpp
+++ b/engines/scumm/insane/insane_rebel.cpp
@@ -82,6 +82,8 @@ InsaneRebel2::InsaneRebel2(ScummEngine_v7 *scumm) {
_playerDamage = 0;
_playerLives = 3;
_playerScore = 0;
+ _viewX = 0;
+ _viewY = 0;
_difficulty = 1; // Default to Medium (1). TODO: Read from game config
@@ -208,15 +210,18 @@ int32 InsaneRebel2::processMouse() {
// Spawn visual shot immediately
spawnShot(mousePos.x, mousePos.y);
+ // Calculate world position for hit testing
+ Common::Point worldMousePos(mousePos.x + _viewX, mousePos.y + _viewY);
+
// Check for hit on any active enemy
Common::List<enemy>::iterator it;
for (it = _enemies.begin(); it != _enemies.end(); ++it) {
debug(" Enemy ID=%d active=%d destroyed=%d rect=(%d,%d)-(%d,%d) contains=%d",
it->id, it->active, it->destroyed,
it->rect.left, it->rect.top, it->rect.right, it->rect.bottom,
- it->rect.contains(mousePos));
+ it->rect.contains(worldMousePos));
- if (it->active && it->rect.contains(mousePos)) {
+ if (it->active && it->rect.contains(worldMousePos)) {
// Enemy hit!
it->active = false;
it->destroyed = true; // Mark as destroyed so IACT won't re-activate
@@ -744,8 +749,8 @@ void InsaneRebel2::spawnShot(int x, int y) {
if (!_shots[i].active) {
_shots[i].active = true;
_shots[i].counter = 4; // Lasts 4 frames
- _shots[i].x = x;
- _shots[i].y = y;
+ _shots[i].x = x + _viewX;
+ _shots[i].y = y + _viewY;
// TODO: Play laser sound
break;
}
@@ -880,8 +885,6 @@ void InsaneRebel2::drawCornerBrackets(byte *dst, int pitch, int width, int heigh
void InsaneRebel2::procPostRendering(byte *renderBitmap, int32 codecparam, int32 setupsan12,
int32 setupsan13, int32 curFrame, int32 maxFrame) {
- processMouse();
-
// Determine correct pitch for the video buffer (usually 320 for Rebel2)
int width = _player->_width;
int height = _player->_height;
@@ -889,6 +892,24 @@ void InsaneRebel2::procPostRendering(byte *renderBitmap, int32 codecparam, int32
if (height == 0) height = _vm->_screenHeight;
int pitch = width;
+ // Calculate View/Scroll Offsets
+ // Rebel Assault 2 uses a buffer larger (424x260) than screen (320x200)
+ // Map mouse X (0-320) to Scroll X (0-104)
+ // Map mouse Y (0-200) to Scroll Y (0-60)
+ int maxScrollX = width - _vm->_screenWidth;
+ int maxScrollY = height - _vm->_screenHeight;
+
+ if (maxScrollX < 0) maxScrollX = 0;
+ if (maxScrollY < 0) maxScrollY = 0;
+
+ // Simple linear mapping: Center of screen corresponds to center of buffer
+ _viewX = (_vm->_mouse.x * maxScrollX) / _vm->_screenWidth;
+ _viewY = (_vm->_mouse.y * maxScrollY) / _vm->_screenHeight;
+
+ _player->setScrollOffset(_viewX, _viewY);
+
+ processMouse();
+
// --- HUD Drawing Order (from FUN_004089ab assembly analysis) ---
// Based on FUN_004089ab:
// 1. Line 156: FUN_004288c0 fills status bar background at Y=0xb4 (180)
@@ -900,7 +921,7 @@ void InsaneRebel2::procPostRendering(byte *renderBitmap, int32 codecparam, int32
// - Buffer is composited at Y=0xb4 (180) via FUN_0042f780
// - DISPFONT.NUT (DAT_00482200) sprites 1-7 contain the status bar elements
//
- // For ScummVM, we draw directly to screen at Y=180
+ // We draw directly to screen at Y=180
// Use video content coordinates, NOT buffer coordinates
const int videoWidth = 320; // Native video width
@@ -914,9 +935,14 @@ void InsaneRebel2::procPostRendering(byte *renderBitmap, int32 codecparam, int32
// Original assembly: FUN_004288c0(local_8, 0, 0, 0xb4, 0x140, 0x14, 4)
// This fills width=320, height=20 starting at Y=180 with color index 4
const byte statusBarBgColor = 4;
- for (int y = statusBarY; y < videoHeight && y < height; y++) {
- for (int x = 0; x < videoWidth && x < pitch; x++) {
- renderBitmap[y * pitch + x] = statusBarBgColor;
+
+ for (int y = statusBarY; y < videoHeight; y++) {
+ int destY = y + _viewY;
+ if (destY >= height) continue;
+ for (int x = 0; x < videoWidth; x++) {
+ int destX = x + _viewX;
+ if (destX >= pitch) continue;
+ renderBitmap[destY * pitch + destX] = statusBarBgColor;
}
}
@@ -949,6 +975,10 @@ void InsaneRebel2::procPostRendering(byte *renderBitmap, int32 codecparam, int32
// The cockpit overlay sits at Y = 200 - frameHeight
destY = statusBarY - frame.height;
if (destY < 0) destY = 0;
+
+ // Apply View Offset for static screen elements
+ destX += _viewX;
+ destY += _viewY;
// Draw frame with transparency (pixel 0 = transparent)
for (int y = 0; y < frame.height && (destY + y) < height; y++) {
@@ -987,7 +1017,7 @@ void InsaneRebel2::procPostRendering(byte *renderBitmap, int32 codecparam, int32
// For ScummVM, we draw directly at Y=statusBarY
if (_smush_cockpitNut) {
// Debug: Log DISPFONT.NUT sprite info once
- static bool loggedDispfont = false;
+ /*static bool loggedDispfont = false;
if (!loggedDispfont) {
int numSprites = _smush_cockpitNut->getNumChars();
debug("Rebel2: DISPFONT.NUT has %d sprites, statusBarY=%d:", numSprites, statusBarY);
@@ -997,12 +1027,12 @@ void InsaneRebel2::procPostRendering(byte *renderBitmap, int32 codecparam, int32
debug(" Sprite %d: %dx%d", i, sw, sh);
}
loggedDispfont = true;
- }
+ }*/
// Draw status bar background frame (sprite 1) at (0, statusBarY)
// This sprite is the full-width status bar background
if (_smush_cockpitNut->getNumChars() > 1) {
- smlayer_drawSomething(renderBitmap, pitch, 0, statusBarY, 0, _smush_cockpitNut, 1, 0, 0);
+ smlayer_drawSomething(renderBitmap, pitch, _viewX, statusBarY + _viewY, 0, _smush_cockpitNut, 1, 0, 0);
}
// Draw difficulty indicator (sprites 2-5 based on difficulty level 0-3)
@@ -1012,7 +1042,7 @@ void InsaneRebel2::procPostRendering(byte *renderBitmap, int32 codecparam, int32
if (difficulty > 3) difficulty = 3;
int difficultySprite = difficulty + 2; // sprites 2, 3, 4, or 5
if (_smush_cockpitNut->getNumChars() > difficultySprite) {
- smlayer_drawSomething(renderBitmap, pitch, 0, statusBarY, 0, _smush_cockpitNut, difficultySprite, 0, 0);
+ smlayer_drawSomething(renderBitmap, pitch, _viewX, statusBarY + _viewY, 0, _smush_cockpitNut, difficultySprite, 0, 0);
}
// Draw shield bar (sprite 6)
@@ -1051,9 +1081,9 @@ void InsaneRebel2::procPostRendering(byte *renderBitmap, int32 codecparam, int32
int drawWidth = MIN(barWidth, sw);
for (int y = 0; y < sh; y++) {
for (int x = 0; x < drawWidth; x++) {
- // Render to (0 + sx + x, statusBarY + sy + y)
- int destX = sx + x;
- int destY = statusBarY + sy + y;
+ // Render to (0 + sx + x + viewX, statusBarY + sy + y + viewY)
+ int destX = sx + x + _viewX;
+ int destY = statusBarY + sy + y + _viewY;
if (destX < pitch && destY < height) {
byte pixel = src[y * sw + x];
if (pixel != 0) {
@@ -1083,15 +1113,7 @@ void InsaneRebel2::procPostRendering(byte *renderBitmap, int32 codecparam, int32
}
}
- // Debug: Draw bounding boxes for enemies
- // width/height/pitch already calculated above
- // Debug: Verify buffer format and drawing capability
- static uint32 lastDebugTime = 0;
- if ((_vm->_system->getMillis() - lastDebugTime) > 2000) {
- lastDebugTime = _vm->_system->getMillis();
- debug("Rebel2 Debug: Buffer %dx%d, Pitch %d, BPP %d, Enemies: %d", width, height, pitch, _vm->_virtscr[kMainVirtScreen].format.bytesPerPixel, _enemies.size());
- }
Common::List<enemy>::iterator it;
for (it = _enemies.begin(); it != _enemies.end(); ++it) {
@@ -1187,9 +1209,9 @@ void InsaneRebel2::procPostRendering(byte *renderBitmap, int32 codecparam, int32
int param_9 = base - _shots[i].counter;
// Draw Left Beam
- drawLaserBeam(renderBitmap, pitch, width, height, GUN_LEFT_X, GUN_LEFT_Y, _shots[i].x, _shots[i].y, param_9, _smush_iconsNut, 5);
+ drawLaserBeam(renderBitmap, pitch, width, height, GUN_LEFT_X + _viewX, GUN_LEFT_Y + _viewY, _shots[i].x, _shots[i].y, param_9, _smush_iconsNut, 5);
// Draw Right Beam
- drawLaserBeam(renderBitmap, pitch, width, height, GUN_RIGHT_X, GUN_RIGHT_Y, _shots[i].x, _shots[i].y, param_9, _smush_iconsNut, 5);
+ drawLaserBeam(renderBitmap, pitch, width, height, GUN_RIGHT_X + _viewX, GUN_RIGHT_Y + _viewY, _shots[i].x, _shots[i].y, param_9, _smush_iconsNut, 5);
// Draw Projectile Impact
// Using Sprite 0 (small flash) or similar at impact point
@@ -1233,8 +1255,8 @@ void InsaneRebel2::procPostRendering(byte *renderBitmap, int32 codecparam, int32
debugCrosshairOnce = true;
}
- // Center the crosshair on mouse position
- smlayer_drawSomething(renderBitmap, pitch, _vm->_mouse.x - cw / 2, _vm->_mouse.y - ch / 2, 0, _smush_iconsNut, reticleIndex, 0, 0);
+ // Center the crosshair on mouse position (in world coordinates)
+ smlayer_drawSomething(renderBitmap, pitch, _vm->_mouse.x - cw / 2 + _viewX, _vm->_mouse.y - ch / 2 + _viewY, 0, _smush_iconsNut, reticleIndex, 0, 0);
}
}
}
diff --git a/engines/scumm/insane/insane_rebel.h b/engines/scumm/insane/insane_rebel.h
index 649f5df53e0..d3efccf487b 100644
--- a/engines/scumm/insane/insane_rebel.h
+++ b/engines/scumm/insane/insane_rebel.h
@@ -152,6 +152,9 @@ public:
int16 _playerLives;
int32 _playerScore;
+ int _viewX;
+ int _viewY;
+
struct Shot {
bool active;
diff --git a/engines/scumm/smush/smush_player.cpp b/engines/scumm/smush/smush_player.cpp
index edcf9c62d91..3e1317c18bb 100644
--- a/engines/scumm/smush/smush_player.cpp
+++ b/engines/scumm/smush/smush_player.cpp
@@ -243,6 +243,8 @@ SmushPlayer::SmushPlayer(ScummEngine_v7 *scumm, IMuseDigital *imuseDigital, Insa
_compressedFileMode = false;
_width = 0;
_height = 0;
+ _scrollX = 0;
+ _scrollY = 0;
_IACTpos = 0;
_speed = -1;
_insanity = false;
@@ -380,8 +382,8 @@ void SmushPlayer::handleIACT(int32 subSize, Common::SeekableReadStream &b) {
uint32 animSize = (bytesRead >= 26) ? READ_BE_UINT32(header + 22) : 0;
// Read the userId to determine which HUD slot (1-4)
- int code = READ_LE_UINT16(header);
- int flags = READ_LE_UINT16(header + 2);
+ READ_LE_UINT16(header);
+ READ_LE_UINT16(header + 2);
int userId = READ_LE_UINT16(header + 6);
debug("Rebel2: Extracting embedded SAN from IACT: userId=%d, animSize=%u", userId, animSize);
@@ -1042,6 +1044,9 @@ void SmushPlayer::handleAnimHeader(int32 subSize, Common::SeekableReadStream &b)
setDirtyColors(0, 255);
}
+ _width = READ_LE_UINT16(&headerContent[4]);
+ _height = READ_LE_UINT16(&headerContent[6]);
+
free(headerContent);
}
}
@@ -1390,10 +1395,12 @@ void SmushPlayer::play(const char *filename, int32 speed, int32 offset, int32 st
int frameWidth = MIN(_width, _vm->_screenWidth);
int frameHeight = MIN(_height, _vm->_screenHeight);
+ const byte *dst = _dst + _scrollY * _width + _scrollX;
+
if (_vm->_macScreen) {
- _vm->mac_drawBufferToScreen(_dst, frameWidth, 0, 0, frameWidth, frameHeight);
+ _vm->mac_drawBufferToScreen(dst, frameWidth, 0, 0, frameWidth, frameHeight);
} else {
- _vm->_system->copyRectToScreen(_dst, _width, 0, 0, frameWidth, frameHeight);
+ _vm->_system->copyRectToScreen(dst, _width, 0, 0, frameWidth, frameHeight);
}
_vm->_system->updateScreen();
@@ -2230,4 +2237,10 @@ void SmushPlayer::clearMaskedRegions() {
_maskedRegions.clear();
}
+// Only used by Rebel Assault 2
+void SmushPlayer::setScrollOffset(int x, int y) {
+ _scrollX = x;
+ _scrollY = y;
+}
+
} // End of namespace Scumm
diff --git a/engines/scumm/smush/smush_player.h b/engines/scumm/smush/smush_player.h
index 32d33b2af15..b4d236abf09 100644
--- a/engines/scumm/smush/smush_player.h
+++ b/engines/scumm/smush/smush_player.h
@@ -229,11 +229,14 @@ protected:
uint32 _pauseStartTime;
uint32 _pauseTime;
int16 _curVideoFlags = 0;
+ int _scrollX;
+ int _scrollY;
void insanity(bool);
void setPalette(const byte *palette);
void setPaletteValue(int n, byte r, byte g, byte b);
void setDirtyColors(int min, int max);
+ void setScrollOffset(int x, int y);
void seekSan(const char *file, int32 pos, int32 contFrame);
const char *getString(int id);
Commit: fd596d3dda3fc112bd3826e398d72805906afc9c
https://github.com/scummvm/scummvm/commit/fd596d3dda3fc112bd3826e398d72805906afc9c
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:08:46+02:00
Commit Message:
SCUMM: RA2: Refine cockpit movement effect
Changed paths:
engines/scumm/insane/insane_rebel.cpp
engines/scumm/insane/insane_rebel.h
diff --git a/engines/scumm/insane/insane_rebel.cpp b/engines/scumm/insane/insane_rebel.cpp
index 4e529f66291..63c550c1dcd 100644
--- a/engines/scumm/insane/insane_rebel.cpp
+++ b/engines/scumm/insane/insane_rebel.cpp
@@ -882,6 +882,54 @@ void InsaneRebel2::drawCornerBrackets(byte *dst, int pitch, int width, int heigh
drawLine(dst, pitch, width, height, x2, y2 - armLen, x2, y2, color);
}
+void InsaneRebel2::renderNutSprite(byte *dst, int pitch, int width, int height, int x, int y, NutRenderer *nut, int spriteIdx) {
+ if (!nut || spriteIdx < 0 || spriteIdx >= nut->getNumChars()) return;
+
+ int w = nut->getCharWidth(spriteIdx);
+ int h = nut->getCharHeight(spriteIdx);
+ const byte *src = nut->getCharData(spriteIdx);
+
+ // Clipping
+ int drawX = x;
+ int drawY = y;
+ int drawW = w;
+ int drawH = h;
+ int srcOffsetX = 0;
+ int srcOffsetY = 0;
+
+ if (drawX < 0) {
+ srcOffsetX = -drawX;
+ drawW += drawX;
+ drawX = 0;
+ }
+ if (drawY < 0) {
+ srcOffsetY = -drawY;
+ drawH += drawY;
+ drawY = 0;
+ }
+
+ if (drawX + drawW > width) {
+ drawW = width - drawX;
+ }
+ if (drawY + drawH > height) {
+ drawH = height - drawY;
+ }
+
+ if (drawW <= 0 || drawH <= 0) return;
+
+ // Draw loop
+ for (int iy = 0; iy < drawH; iy++) {
+ const byte *s = src + (srcOffsetY + iy) * w + srcOffsetX;
+ byte *d = dst + (drawY + iy) * pitch + drawX;
+ for (int ix = 0; ix < drawW; ix++) {
+ byte px = s[ix];
+ if (px != 231 && px != 0) { // Check both 0 and 231 (0xE7) for transparency
+ d[ix] = px;
+ }
+ }
+ }
+}
+
void InsaneRebel2::procPostRendering(byte *renderBitmap, int32 codecparam, int32 setupsan12,
int32 setupsan13, int32 curFrame, int32 maxFrame) {
@@ -1032,7 +1080,7 @@ void InsaneRebel2::procPostRendering(byte *renderBitmap, int32 codecparam, int32
// Draw status bar background frame (sprite 1) at (0, statusBarY)
// This sprite is the full-width status bar background
if (_smush_cockpitNut->getNumChars() > 1) {
- smlayer_drawSomething(renderBitmap, pitch, _viewX, statusBarY + _viewY, 0, _smush_cockpitNut, 1, 0, 0);
+ renderNutSprite(renderBitmap, pitch, width, height, _viewX, statusBarY + _viewY, _smush_cockpitNut, 1);
}
// Draw difficulty indicator (sprites 2-5 based on difficulty level 0-3)
@@ -1042,7 +1090,7 @@ void InsaneRebel2::procPostRendering(byte *renderBitmap, int32 codecparam, int32
if (difficulty > 3) difficulty = 3;
int difficultySprite = difficulty + 2; // sprites 2, 3, 4, or 5
if (_smush_cockpitNut->getNumChars() > difficultySprite) {
- smlayer_drawSomething(renderBitmap, pitch, _viewX, statusBarY + _viewY, 0, _smush_cockpitNut, difficultySprite, 0, 0);
+ renderNutSprite(renderBitmap, pitch, width, height, _viewX, statusBarY + _viewY, _smush_cockpitNut, difficultySprite);
}
// Draw shield bar (sprite 6)
@@ -1173,7 +1221,7 @@ void InsaneRebel2::procPostRendering(byte *renderBitmap, int32 codecparam, int32
int cy = _explosions[i].y - eh / 2;
// Draw explosion
- smlayer_drawSomething(renderBitmap, pitch, cx, cy, 0, _smush_iconsNut, spriteIndex, 0, 0);
+ renderNutSprite(renderBitmap, pitch, width, height, cx, cy, _smush_iconsNut, spriteIndex);
}
_explosions[i].counter--;
@@ -1215,7 +1263,7 @@ void InsaneRebel2::procPostRendering(byte *renderBitmap, int32 codecparam, int32
// Draw Projectile Impact
// Using Sprite 0 (small flash) or similar at impact point
- smlayer_drawSomething(renderBitmap, pitch, _shots[i].x - 7, _shots[i].y - 7, 0, _smush_iconsNut, 0, 0, 0);
+ renderNutSprite(renderBitmap, pitch, width, height, _shots[i].x - 7, _shots[i].y - 7, _smush_iconsNut, 0);
}
_shots[i].counter--;
@@ -1256,7 +1304,7 @@ void InsaneRebel2::procPostRendering(byte *renderBitmap, int32 codecparam, int32
}
// Center the crosshair on mouse position (in world coordinates)
- smlayer_drawSomething(renderBitmap, pitch, _vm->_mouse.x - cw / 2 + _viewX, _vm->_mouse.y - ch / 2 + _viewY, 0, _smush_iconsNut, reticleIndex, 0, 0);
+ renderNutSprite(renderBitmap, pitch, width, height, _vm->_mouse.x - cw / 2 + _viewX, _vm->_mouse.y - ch / 2 + _viewY, _smush_iconsNut, reticleIndex);
}
}
}
diff --git a/engines/scumm/insane/insane_rebel.h b/engines/scumm/insane/insane_rebel.h
index d3efccf487b..442dc2ac959 100644
--- a/engines/scumm/insane/insane_rebel.h
+++ b/engines/scumm/insane/insane_rebel.h
@@ -69,7 +69,9 @@ public:
void drawLine(byte *dst, int pitch, int width, int height, int x0, int y0, int x1, int y1, byte color);
void drawTexturedLine(byte *dst, int pitch, int width, int height, int x0, int y0, int x1, int y1, NutRenderer *nut, int spriteIdx);
+
void drawLaserBeam(byte *dst, int pitch, int width, int height, int x0, int y0, int x1, int y1, int param_9, NutRenderer *nut, int spriteIdx);
+ void renderNutSprite(byte *dst, int pitch, int width, int height, int x, int y, NutRenderer *nut, int spriteIdx);
struct enemy {
int id;
Commit: ea21cbd85265721224829ae438a2f5efa45f2d70
https://github.com/scummvm/scummvm/commit/ea21cbd85265721224829ae438a2f5efa45f2d70
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:08:47+02:00
Commit Message:
SCUMM: RA2: Improve laser beam rendering
Changed paths:
engines/scumm/insane/insane_rebel.cpp
engines/scumm/insane/insane_rebel.h
diff --git a/engines/scumm/insane/insane_rebel.cpp b/engines/scumm/insane/insane_rebel.cpp
index 63c550c1dcd..01a1409b2f8 100644
--- a/engines/scumm/insane/insane_rebel.cpp
+++ b/engines/scumm/insane/insane_rebel.cpp
@@ -757,7 +757,7 @@ void InsaneRebel2::spawnShot(int x, int y) {
}
}
-void InsaneRebel2::drawTexturedLine(byte *dst, int pitch, int width, int height, int x0, int y0, int x1, int y1, NutRenderer *nut, int spriteIdx) {
+void InsaneRebel2::drawTexturedLine(byte *dst, int pitch, int width, int height, int x0, int y0, int x1, int y1, NutRenderer *nut, int spriteIdx, int u) {
if (!nut || spriteIdx >= nut->getNumChars()) return;
const byte *srcData = nut->getCharData(spriteIdx);
@@ -770,50 +770,62 @@ void InsaneRebel2::drawTexturedLine(byte *dst, int pitch, int width, int height,
int dy = -abs(y1 - y0), sy = y0 < y1 ? 1 : -1;
int err = dx + dy, e2;
- // Length of line for uv mapping
- // float length = sqrt(dx*dx + dy*dy);
+ // Total length approximation for UV mapping
+ // We use Manhattan distance or max dimension for interpolation
+ int totalDist = (abs(dx) > abs(dy)) ? abs(dx) : abs(dy);
+ if (totalDist == 0) totalDist = 1;
+
+ // If u not specified, use center
+ if (u < 0) u = texW / 2;
+ if (u >= texW) u = texW - 1;
- int texCenterX = texW / 2;
- int texCenterY = texH / 2;
- byte color = srcData[texCenterY * texW + texCenterX];
-
- // If color is 0/transparent, try to find a valid one
- if (color == 0) color = 255; // Fallback to white
+ int currentDist = 0;
- // Standard Bresenham but with texture sampling attempt
- // To make it look like a "beam", we need width.
-
for (;;) {
- // Draw a 3x3 blob or similar to make it thick
- if (x0 >= 1 && x0 < width - 1 && y0 >= 1 && y0 < height - 1) {
- dst[y0 * pitch + x0] = color;
- // Glow effect (simulated with same color for now)
- // dst[(y0+1) * pitch + x0] = color;
- // dst[y0 * pitch + (x0+1)] = color;
+ if (x0 >= 0 && x0 < width && y0 >= 0 && y0 < height) {
+ // Map currentDist/totalDist to 0..texH (Length of texture mapped to length of line)
+ int v = (currentDist * texH) / totalDist;
+ if (v >= texH) v = texH - 1;
+
+ byte color = srcData[v * texW + u];
+
+ // Check for transparency (0 and 231)
+ if (color != 0 && color != 231) {
+ dst[y0 * pitch + x0] = color;
+ }
}
if (x0 == x1 && y0 == y1) break;
e2 = 2 * err;
if (e2 >= dy) { err += dy; x0 += sx; }
if (e2 <= dx) { err += dx; y0 += sy; }
+
+ currentDist++;
}
}
-void InsaneRebel2::drawLaserBeam(byte *dst, int pitch, int width, int height, int x0, int y0, int x1, int y1, int param_9, NutRenderer *nut, int spriteIdx) {
+void InsaneRebel2::drawLaserBeam(byte *dst, int pitch, int width, int height, int x0, int y0, int x1, int y1, int progress, int maxProgress, NutRenderer *nut, int spriteIdx) {
if (!nut || spriteIdx >= nut->getNumChars()) return;
- // Reverse-engineered math from FUN_0040bbf6
- int texW = nut->getCharWidth(spriteIdx);
- int texH = nut->getCharHeight(spriteIdx);
- int param_11 = 12; // Standard value from assembly calls
- if (texW == 0) return;
-
- // sVar7 = (12 * texH * 16) / texW
- int sVar7 = (param_11 * texH * 16) / texW;
+ // Reverse-engineered math from FUN_0040bbf6 and FUN_0040ad63
+
+ // Rebel Assault 2 specific params
+ // param_11 is typically 12 for Level 1, 25 or other for different scenarios.
+ int param_11 = (_rebelLevelType == 1) ? 12 : 25;
+ // param_9 is also passed as 12 in Level 1 (or 25 otherwise)
+ int param_9 = param_11;
+
+ // sVar7 calculation: (param_11 * progress * 16) / maxProgress
+ // progress = max - counter (goes 0 -> max)
+ if (maxProgress == 0) maxProgress = 1;
+ int sVar7 = (param_11 * progress * 16) / maxProgress;
- // Extend vector by (13/12)
+ // Extend vector by using param_11 factor
int dx = x1 - x0;
int dy = y1 - y0;
+
+ // sVar6 / sVar1 calculation (Extended vector)
+ // extDx = (dx * (param_11 + 1)) / param_11
int extDx = (dx * (param_11 + 1)) / param_11;
int extDy = (dy * (param_11 + 1)) / param_11;
@@ -835,8 +847,27 @@ void InsaneRebel2::drawLaserBeam(byte *dst, int pitch, int width, int height, in
int endX = xExt - (extDx * 16) / endFactor;
int endY = yExt - (extDy * 16) / endFactor;
- // Draw the segment
- drawTexturedLine(dst, pitch, width, height, startX, startY, endX, endY, nut, spriteIdx);
+ // Draw the beam with thickness
+ // Iterate through the texture width columns and draw a line for each
+ int texW = nut->getCharWidth(spriteIdx);
+ if (texW > 0) {
+ for (int i = 0; i < texW; i++) {
+ int u = i;
+ int offset = i - texW / 2;
+
+ // Calculate perpendicular offset
+ // Simple approximation: If beam is more vertical, offset X. If horizontal, offset Y.
+ int ox = 0, oy = 0;
+ if (abs(dy) > abs(dx)) {
+ ox = offset;
+ } else {
+ oy = offset;
+ }
+
+ // Draw line for this texture column
+ drawTexturedLine(dst, pitch, width, height, startX + ox, startY + oy, endX + ox, endY + oy, nut, spriteIdx, u);
+ }
+ }
}
void InsaneRebel2::drawLine(byte *dst, int pitch, int width, int height, int x0, int y0, int x1, int y1, byte color) {
@@ -1244,22 +1275,17 @@ void InsaneRebel2::procPostRendering(byte *renderBitmap, int32 codecparam, int32
continue;
}
- // Use Sprite 5 from CPITIMAG.NUT as the laser texture
- // Confirmed by reverse engineering: Laser uses Sprite 5 (136x13)
- if (_smush_iconsNut && _smush_iconsNut->getNumChars() > 5) {
- // Calculate param_9 equivalent (Counter dependent)
- // Original: TableValue - Counter
- // We don't have table, but assembly suggests values around 0x20-0x80 range?
- // Math divides by (param_9 + offset). Higher param_9 = Longer beam.
- // As counter decreases (4->0), value increases (26->30), beam grows/moves.
- // Let's try base 30.
- int base = 30;
- int param_9 = base - _shots[i].counter;
+ // Use Sprite 0 from CPITIMAG.NUT as the laser texture (15x15 projectile)
+ // Confirmed by info.md: indexes 0-4 are laser/projectile
+ if (_smush_iconsNut && _smush_iconsNut->getNumChars() > 0) {
+ // Calculate progress
+ int maxProgress = 4;
+ int progress = maxProgress - _shots[i].counter;
// Draw Left Beam
- drawLaserBeam(renderBitmap, pitch, width, height, GUN_LEFT_X + _viewX, GUN_LEFT_Y + _viewY, _shots[i].x, _shots[i].y, param_9, _smush_iconsNut, 5);
+ drawLaserBeam(renderBitmap, pitch, width, height, GUN_LEFT_X + _viewX, GUN_LEFT_Y + _viewY, _shots[i].x, _shots[i].y, progress, maxProgress, _smush_iconsNut, 0);
// Draw Right Beam
- drawLaserBeam(renderBitmap, pitch, width, height, GUN_RIGHT_X + _viewX, GUN_RIGHT_Y + _viewY, _shots[i].x, _shots[i].y, param_9, _smush_iconsNut, 5);
+ drawLaserBeam(renderBitmap, pitch, width, height, GUN_RIGHT_X + _viewX, GUN_RIGHT_Y + _viewY, _shots[i].x, _shots[i].y, progress, maxProgress, _smush_iconsNut, 0);
// Draw Projectile Impact
// Using Sprite 0 (small flash) or similar at impact point
diff --git a/engines/scumm/insane/insane_rebel.h b/engines/scumm/insane/insane_rebel.h
index 442dc2ac959..92967c3a763 100644
--- a/engines/scumm/insane/insane_rebel.h
+++ b/engines/scumm/insane/insane_rebel.h
@@ -68,9 +68,9 @@ public:
int16 par1, int16 par2, int16 par3, int16 par4) override;
void drawLine(byte *dst, int pitch, int width, int height, int x0, int y0, int x1, int y1, byte color);
- void drawTexturedLine(byte *dst, int pitch, int width, int height, int x0, int y0, int x1, int y1, NutRenderer *nut, int spriteIdx);
+ void drawTexturedLine(byte *dst, int pitch, int width, int height, int x0, int y0, int x1, int y1, NutRenderer *nut, int spriteIdx, int u = -1);
- void drawLaserBeam(byte *dst, int pitch, int width, int height, int x0, int y0, int x1, int y1, int param_9, NutRenderer *nut, int spriteIdx);
+ void drawLaserBeam(byte *dst, int pitch, int width, int height, int x0, int y0, int x1, int y1, int progress, int maxProgress, NutRenderer *nut, int spriteIdx);
void renderNutSprite(byte *dst, int pitch, int width, int height, int x, int y, NutRenderer *nut, int spriteIdx);
struct enemy {
Commit: 474e3d5b7b0644784eb0f891cb5c6d7f9a2a28bf
https://github.com/scummvm/scummvm/commit/474e3d5b7b0644784eb0f891cb5c6d7f9a2a28bf
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:08:47+02:00
Commit Message:
SCUMM: RA2: Refine laser beam rendering
Changed paths:
engines/scumm/insane/insane_rebel.cpp
engines/scumm/insane/insane_rebel.h
diff --git a/engines/scumm/insane/insane_rebel.cpp b/engines/scumm/insane/insane_rebel.cpp
index 01a1409b2f8..37dad159f4c 100644
--- a/engines/scumm/insane/insane_rebel.cpp
+++ b/engines/scumm/insane/insane_rebel.cpp
@@ -757,7 +757,7 @@ void InsaneRebel2::spawnShot(int x, int y) {
}
}
-void InsaneRebel2::drawTexturedLine(byte *dst, int pitch, int width, int height, int x0, int y0, int x1, int y1, NutRenderer *nut, int spriteIdx, int u) {
+void InsaneRebel2::drawTexturedLine(byte *dst, int pitch, int width, int height, int x0, int y0, int x1, int y1, NutRenderer *nut, int spriteIdx, int v) {
if (!nut || spriteIdx >= nut->getNumChars()) return;
const byte *srcData = nut->getCharData(spriteIdx);
@@ -765,27 +765,24 @@ void InsaneRebel2::drawTexturedLine(byte *dst, int pitch, int width, int height,
int texH = nut->getCharHeight(spriteIdx);
if (!srcData || texW <= 0 || texH <= 0) return;
+ if (v < 0) v = 0;
+ if (v >= texH) v = texH - 1;
int dx = abs(x1 - x0), sx = x0 < x1 ? 1 : -1;
int dy = -abs(y1 - y0), sy = y0 < y1 ? 1 : -1;
int err = dx + dy, e2;
// Total length approximation for UV mapping
- // We use Manhattan distance or max dimension for interpolation
int totalDist = (abs(dx) > abs(dy)) ? abs(dx) : abs(dy);
if (totalDist == 0) totalDist = 1;
-
- // If u not specified, use center
- if (u < 0) u = texW / 2;
- if (u >= texW) u = texW - 1;
int currentDist = 0;
for (;;) {
if (x0 >= 0 && x0 < width && y0 >= 0 && y0 < height) {
- // Map currentDist/totalDist to 0..texH (Length of texture mapped to length of line)
- int v = (currentDist * texH) / totalDist;
- if (v >= texH) v = texH - 1;
+ // Map currentDist/totalDist to 0..texW (Run along texture width)
+ int u = (currentDist * texW) / totalDist;
+ if (u >= texW) u = texW - 1;
byte color = srcData[v * texW + u];
@@ -804,69 +801,67 @@ void InsaneRebel2::drawTexturedLine(byte *dst, int pitch, int width, int height,
}
}
-void InsaneRebel2::drawLaserBeam(byte *dst, int pitch, int width, int height, int x0, int y0, int x1, int y1, int progress, int maxProgress, NutRenderer *nut, int spriteIdx) {
+void InsaneRebel2::drawLaserBeam(byte *dst, int pitch, int width, int height, int x0, int y0, int x1, int y1, int progress, int maxProgress, int thickness, int param_9, NutRenderer *nut, int spriteIdx) {
if (!nut || spriteIdx >= nut->getNumChars()) return;
- // Reverse-engineered math from FUN_0040bbf6 and FUN_0040ad63
-
- // Rebel Assault 2 specific params
- // param_11 is typically 12 for Level 1, 25 or other for different scenarios.
+ int texH = nut->getCharHeight(spriteIdx);
+ // param_11 is typically 12 for Level 1
int param_11 = (_rebelLevelType == 1) ? 12 : 25;
- // param_9 is also passed as 12 in Level 1 (or 25 otherwise)
- int param_9 = param_11;
// sVar7 calculation: (param_11 * progress * 16) / maxProgress
- // progress = max - counter (goes 0 -> max)
if (maxProgress == 0) maxProgress = 1;
int sVar7 = (param_11 * progress * 16) / maxProgress;
- // Extend vector by using param_11 factor
+ // Extend vector by (param_11 + 1) / param_11
int dx = x1 - x0;
int dy = y1 - y0;
-
- // sVar6 / sVar1 calculation (Extended vector)
- // extDx = (dx * (param_11 + 1)) / param_11
int extDx = (dx * (param_11 + 1)) / param_11;
int extDy = (dy * (param_11 + 1)) / param_11;
int xExt = x0 + extDx;
int yExt = y0 + extDy;
- // Calculate New Start Point (sVar4, sVar5)
+ // Calculate New Start Point
// Start = ExtEnd - (ExtEnd - Start) * (16 / (sVar7 + 16))
- // Actually math is: sVar4 = (xExt) - (extDx * 16) / (sVar7 + 16)
int startFactor = sVar7 + 16;
if (startFactor == 0) startFactor = 1;
int startX = xExt - (extDx * 16) / startFactor;
int startY = yExt - (extDy * 16) / startFactor;
- // Calculate New End Point (sVar6, sVar7 in asm) w.r.t param_9
+ // Calculate New End Point
// End = ExtEnd - (ExtEnd - Start) * (16 / (param_9 + sVar7 + 16))
int endFactor = param_9 + sVar7 + 16;
- if (endFactor == 0) endFactor = 1; // Safety
+ if (endFactor == 0) endFactor = 1;
int endX = xExt - (extDx * 16) / endFactor;
int endY = yExt - (extDy * 16) / endFactor;
- // Draw the beam with thickness
- // Iterate through the texture width columns and draw a line for each
- int texW = nut->getCharWidth(spriteIdx);
- if (texW > 0) {
- for (int i = 0; i < texW; i++) {
- int u = i;
- int offset = i - texW / 2;
-
- // Calculate perpendicular offset
- // Simple approximation: If beam is more vertical, offset X. If horizontal, offset Y.
- int ox = 0, oy = 0;
- if (abs(dy) > abs(dx)) {
- ox = offset;
- } else {
- oy = offset;
- }
-
- // Draw line for this texture column
- drawTexturedLine(dst, pitch, width, height, startX + ox, startY + oy, endX + ox, endY + oy, nut, spriteIdx, u);
- }
+ // Calculate perpendicular offsets for thickness
+ // Perpendicular vector (-dy, dx) normalized?
+ // Assembly just adds to Y or X depending on loop (simplified) but usually laser is line
+ // We'll use a simple approach: if mostly vertical, offset X. If mostly horizontal, offset Y.
+ int perX, perY;
+ if (abs(dx) > abs(dy)) {
+ perX = 0; perY = 1;
+ } else {
+ perX = 1; perY = 0;
+ }
+
+ // Iterate thickness (Rows of texture)
+ // Map loop index 0..thickness to texture row 0..texH
+ // This scales the texture height to the beam width
+
+ int centerOffset = thickness / 2;
+
+ for (int i = 0; i < thickness; i++) {
+ // Calculate V coordinate: Scale i (0..thickness) to (0..texH)
+ int v = (i * texH) / thickness;
+ if (v >= texH) v = texH - 1;
+
+ int off = i - centerOffset;
+ int ox = off * perX;
+ int oy = off * perY;
+
+ drawTexturedLine(dst, pitch, width, height, startX + ox, startY + oy, endX + ox, endY + oy, nut, spriteIdx, v);
}
}
@@ -1279,13 +1274,43 @@ void InsaneRebel2::procPostRendering(byte *renderBitmap, int32 codecparam, int32
// Confirmed by info.md: indexes 0-4 are laser/projectile
if (_smush_iconsNut && _smush_iconsNut->getNumChars() > 0) {
// Calculate progress
- int maxProgress = 4;
+ int maxProgress = 4; // Max duration from table (supposedly)
int progress = maxProgress - _shots[i].counter;
- // Draw Left Beam
- drawLaserBeam(renderBitmap, pitch, width, height, GUN_LEFT_X + _viewX, GUN_LEFT_Y + _viewY, _shots[i].x, _shots[i].y, progress, maxProgress, _smush_iconsNut, 0);
- // Draw Right Beam
- drawLaserBeam(renderBitmap, pitch, width, height, GUN_RIGHT_X + _viewX, GUN_RIGHT_Y + _viewY, _shots[i].x, _shots[i].y, progress, maxProgress, _smush_iconsNut, 0);
+ // Draw Beams depending on Level Type
+ // Scene 1 (LevelType 1) has 3 beams: Right, Middle, Left
+ if (_rebelLevelType <= 1) { // Default or Type 1
+ // Right Beam: Origin(310, 170), Thickness 8, LengthFac 12
+ drawLaserBeam(renderBitmap, pitch, width, height,
+ 310 + _viewX, 170 + _viewY,
+ _shots[i].x, _shots[i].y,
+ progress, maxProgress, 8, 12, _smush_iconsNut, 0);
+
+ // Middle Beam: Origin(160, 380), Thickness 5, LengthFac 8
+ // Note: 380 is virtual origin below screen
+ drawLaserBeam(renderBitmap, pitch, width, height,
+ 160 + _viewX, 380 + _viewY,
+ _shots[i].x, _shots[i].y,
+ progress, maxProgress, 5, 8, _smush_iconsNut, 0);
+
+ // Left Beam: Origin(10, 170), Thickness 8, LengthFac 12
+ drawLaserBeam(renderBitmap, pitch, width, height,
+ 10 + _viewX, 170 + _viewY,
+ _shots[i].x, _shots[i].y,
+ progress, maxProgress, 8, 12, _smush_iconsNut, 0);
+
+ } else {
+ // Fallback for other levels (2 beams)
+ drawLaserBeam(renderBitmap, pitch, width, height,
+ GUN_LEFT_X + _viewX, GUN_LEFT_Y + _viewY,
+ _shots[i].x, _shots[i].y,
+ progress, maxProgress, 8, 12, _smush_iconsNut, 0);
+
+ drawLaserBeam(renderBitmap, pitch, width, height,
+ GUN_RIGHT_X + _viewX, GUN_RIGHT_Y + _viewY,
+ _shots[i].x, _shots[i].y,
+ progress, maxProgress, 8, 12, _smush_iconsNut, 0);
+ }
// Draw Projectile Impact
// Using Sprite 0 (small flash) or similar at impact point
diff --git a/engines/scumm/insane/insane_rebel.h b/engines/scumm/insane/insane_rebel.h
index 92967c3a763..73211c1e629 100644
--- a/engines/scumm/insane/insane_rebel.h
+++ b/engines/scumm/insane/insane_rebel.h
@@ -68,9 +68,9 @@ public:
int16 par1, int16 par2, int16 par3, int16 par4) override;
void drawLine(byte *dst, int pitch, int width, int height, int x0, int y0, int x1, int y1, byte color);
- void drawTexturedLine(byte *dst, int pitch, int width, int height, int x0, int y0, int x1, int y1, NutRenderer *nut, int spriteIdx, int u = -1);
+ void drawTexturedLine(byte *dst, int pitch, int width, int height, int x0, int y0, int x1, int y1, NutRenderer *nut, int spriteIdx, int v);
- void drawLaserBeam(byte *dst, int pitch, int width, int height, int x0, int y0, int x1, int y1, int progress, int maxProgress, NutRenderer *nut, int spriteIdx);
+ void drawLaserBeam(byte *dst, int pitch, int width, int height, int x0, int y0, int x1, int y1, int progress, int maxProgress, int thickness, int param_9, NutRenderer *nut, int spriteIdx);
void renderNutSprite(byte *dst, int pitch, int width, int height, int x, int y, NutRenderer *nut, int spriteIdx);
struct enemy {
Commit: b33180f25f1c1cb7912038c19b5f0935bb722a67
https://github.com/scummvm/scummvm/commit/b33180f25f1c1cb7912038c19b5f0935bb722a67
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:08:47+02:00
Commit Message:
SCUMM: RA2: Complete laser beam rendering
Changed paths:
engines/scumm/insane/insane_rebel.cpp
engines/scumm/insane/insane_rebel.h
diff --git a/engines/scumm/insane/insane_rebel.cpp b/engines/scumm/insane/insane_rebel.cpp
index 37dad159f4c..5dffc5a725f 100644
--- a/engines/scumm/insane/insane_rebel.cpp
+++ b/engines/scumm/insane/insane_rebel.cpp
@@ -757,7 +757,7 @@ void InsaneRebel2::spawnShot(int x, int y) {
}
}
-void InsaneRebel2::drawTexturedLine(byte *dst, int pitch, int width, int height, int x0, int y0, int x1, int y1, NutRenderer *nut, int spriteIdx, int v) {
+void InsaneRebel2::drawTexturedLine(byte *dst, int pitch, int width, int height, int x0, int y0, int x1, int y1, NutRenderer *nut, int spriteIdx, int v, bool mask231) {
if (!nut || spriteIdx >= nut->getNumChars()) return;
const byte *srcData = nut->getCharData(spriteIdx);
@@ -786,8 +786,8 @@ void InsaneRebel2::drawTexturedLine(byte *dst, int pitch, int width, int height,
byte color = srcData[v * texW + u];
- // Check for transparency (0 and 231)
- if (color != 0 && color != 231) {
+ // Check for transparency (0 and optionally 231)
+ if (color != 0 && (!mask231 || color != 231)) {
dst[y0 * pitch + x0] = color;
}
}
@@ -801,70 +801,186 @@ void InsaneRebel2::drawTexturedLine(byte *dst, int pitch, int width, int height,
}
}
+// Helper: draw a textured segment between two points using the game's original routine (FUN_00429360 port)
+static void drawTexturedSegment(byte *dst, int pitch, int width, int height, int param_3, int param_4, int param_5, int param_6, int param_7, const byte *param_8) {
+ // Ported from FUN_00429360 (decompiled). Only 0 in texture is transparent.
+ int sVar4 = 0; // left
+ int sVar1 = 0; // top
+ int sVar7 = width - 1; // right
+ int sVar10 = height - 1; // bottom
+
+ int px0 = param_3;
+ int py0 = param_4;
+ int px1 = param_5;
+ int py1 = param_6;
+
+ // Clip against screen bounds (translation of original clipping logic)
+ if (px0 == px1) {
+ if (px0 < sVar4 || px0 > sVar7) return;
+ } else {
+ if (px0 < sVar4) {
+ if (px1 < sVar4) return;
+ py0 = py1 + ((py0 - py1) * (sVar4 - px1)) / (px0 - px1);
+ px0 = sVar4;
+ } else if (px0 > sVar7) {
+ if (px1 > sVar7) return;
+ py0 = py1 + ((py0 - py1) * (sVar7 - px1)) / (px0 - px1);
+ px0 = sVar7;
+ }
+ if (px1 < sVar4) {
+ py1 = py0 + ((py1 - py0) * (sVar4 - px0)) / (px1 - px0);
+ px1 = sVar4;
+ } else if (px1 > sVar7) {
+ py1 = py0 + ((py1 - py0) * (sVar7 - px0)) / (px1 - px0);
+ px1 = sVar7;
+ }
+ }
+
+ if (py0 == py1) {
+ if (py0 < sVar1 || py0 > sVar10) return;
+ } else {
+ if (py0 < sVar1) {
+ if (py1 < sVar1) return;
+ px0 = px1 + ((px0 - px1) * (sVar1 - py1)) / (py0 - py1);
+ py0 = sVar1;
+ } else if (py0 > sVar10) {
+ if (py1 > sVar10) return;
+ px0 = px1 + ((px0 - px1) * (sVar10 - py1)) / (py0 - py1);
+ py0 = sVar10;
+ }
+ if (py1 < sVar1) {
+ px1 = px0 + ((px1 - px0) * (sVar1 - py0)) / (py1 - py0);
+ py1 = sVar1;
+ } else if (py1 > sVar10) {
+ px1 = px0 + ((px1 - px0) * (sVar10 - py0)) / (py1 - py0);
+ py1 = sVar10;
+ }
+ }
+
+ int dx = px1 - px0;
+ int dy = py1 - py0;
+ int absdx = dx < 0 ? -dx : dx;
+ int absdy = dy < 0 ? -dy : dy;
+
+ // pointer into destination and texture
+ byte *baseDst = dst;
+ const byte *texPtr = param_8;
+
+ if (absdx == 0) {
+ if (absdy == 0) {
+ if (*texPtr != 0) baseDst[py0 * pitch + px0] = *texPtr;
+ return;
+ }
+ // vertical-ish
+ int step = absdy + 1;
+ int curY = py0;
+ int signY = dy > 0 ? 1 : -1;
+ int iVar9 = step; // adv counter
+ for (int i = 0; i < step; i++) {
+ if (*texPtr != 0) baseDst[curY * pitch + px0] = *texPtr;
+ curY += signY;
+ iVar9 -= param_7;
+ while (iVar9 < 0) { texPtr++; iVar9 += step; }
+ }
+ return;
+ }
+
+ if (absdy == 0) {
+ // horizontal-ish
+ int step = absdx + 1;
+ int curX = px0;
+ int signX = dx > 0 ? 1 : -1;
+ int iVar11 = step;
+ for (int i = 0; i < step; i++) {
+ if (*texPtr != 0) baseDst[py0 * pitch + curX] = *texPtr;
+ curX += signX;
+ iVar11 -= param_7;
+ while (iVar11 < 0) { texPtr++; iVar11 += step; }
+ }
+ return;
+ }
+
+ // general case
+ int steps = (absdx > absdy) ? absdx + 1 : absdy + 1;
+ int x = px0, y = py0;
+ int sx = dx > 0 ? 1 : -1;
+ int sy = dy > 0 ? 1 : -1;
+ int err = absdx - absdy;
+ int iVar12 = steps;
+
+ for (int i = 0; i < steps; i++) {
+ if (x >= 0 && x < width && y >= 0 && y < height) {
+ if (*texPtr != 0) baseDst[y * pitch + x] = *texPtr;
+ }
+ int e2 = 2 * err;
+ if (e2 > -absdy) { err -= absdy; x += sx; }
+ if (e2 < absdx) { err += absdx; y += sy; }
+ iVar12 -= param_7;
+ if (iVar12 < 0) { texPtr++; iVar12 += steps; }
+ }
+}
+
+
void InsaneRebel2::drawLaserBeam(byte *dst, int pitch, int width, int height, int x0, int y0, int x1, int y1, int progress, int maxProgress, int thickness, int param_9, NutRenderer *nut, int spriteIdx) {
if (!nut || spriteIdx >= nut->getNumChars()) return;
+ // Follow original FUN_0040BBF6 math precisely
+ int texW = nut->getCharWidth(spriteIdx);
int texH = nut->getCharHeight(spriteIdx);
- // param_11 is typically 12 for Level 1
- int param_11 = (_rebelLevelType == 1) ? 12 : 25;
+ int param_11 = (_rebelLevelType <= 1) ? 12 : 25;
- // sVar7 calculation: (param_11 * progress * 16) / maxProgress
if (maxProgress == 0) maxProgress = 1;
int sVar7 = (param_11 * progress * 16) / maxProgress;
-
- // Extend vector by (param_11 + 1) / param_11
+
int dx = x1 - x0;
int dy = y1 - y0;
- int extDx = (dx * (param_11 + 1)) / param_11;
- int extDy = (dy * (param_11 + 1)) / param_11;
-
- int xExt = x0 + extDx;
- int yExt = y0 + extDy;
-
- // Calculate New Start Point
- // Start = ExtEnd - (ExtEnd - Start) * (16 / (sVar7 + 16))
- int startFactor = sVar7 + 16;
- if (startFactor == 0) startFactor = 1;
- int startX = xExt - (extDx * 16) / startFactor;
- int startY = yExt - (extDy * 16) / startFactor;
-
- // Calculate New End Point
- // End = ExtEnd - (ExtEnd - Start) * (16 / (param_9 + sVar7 + 16))
- int endFactor = param_9 + sVar7 + 16;
- if (endFactor == 0) endFactor = 1;
- int endX = xExt - (extDx * 16) / endFactor;
- int endY = yExt - (extDy * 16) / endFactor;
-
- // Calculate perpendicular offsets for thickness
- // Perpendicular vector (-dy, dx) normalized?
- // Assembly just adds to Y or X depending on loop (simplified) but usually laser is line
- // We'll use a simple approach: if mostly vertical, offset X. If mostly horizontal, offset Y.
- int perX, perY;
- if (abs(dx) > abs(dy)) {
- perX = 0; perY = 1;
+ int sVar6 = ((dx) * (param_11 + 1)) / param_11;
+ int sVar1 = ((dy) * (param_11 + 1)) / param_11;
+
+ int sVar4 = (sVar6 + x0) - (sVar6 * 16) / (sVar7 + 16);
+ int sVar5 = (sVar1 + y0) - (sVar1 * 16) / (sVar7 + 16);
+ int sVar6_end = (sVar6 + x0) - (sVar6 * 16) / (param_9 + sVar7 + 16);
+ int sVar7_end = (sVar1 + y0) - (sVar1 * 16) / (param_9 + sVar7 + 16);
+
+ const byte *srcBase = nut->getCharData(spriteIdx);
+ if (!srcBase || texW <= 0 || texH <= 0) return;
+
+ int iVar2 = abs(sVar5 - sVar7_end);
+ int iVar3 = abs(sVar4 - sVar6_end);
+
+ if (iVar2 < iVar3) {
+ // Column major case (wide)
+ iVar2 = abs(sVar4 - sVar6_end);
+ long long temp = (long long)iVar2 * (long long)texH * (long long)thickness;
+ // sVar1calc = (temp >> 3) / texW + 2
+ int sVar1calc = (int)((temp >> 3) / texW) + 2;
+ int local_24 = -sVar1calc;
+ int sVar8 = sVar1calc >> 1;
+ const byte *local_28 = srcBase;
+ for (int local_2c = 0; local_2c < sVar1calc; local_2c++) {
+ drawTexturedSegment(dst, pitch, width, height, sVar4, (sVar5 - sVar8) + local_2c,
+ sVar6_end, (sVar7_end - sVar8) + local_2c, texW, local_28);
+ for (local_24 = texH + local_24; local_24 > 0; local_24 -= sVar1calc) {
+ local_28 += texW;
+ }
+ }
} else {
- perX = 1; perY = 0;
- }
-
- // Iterate thickness (Rows of texture)
- // Map loop index 0..thickness to texture row 0..texH
- // This scales the texture height to the beam width
-
- int centerOffset = thickness / 2;
-
- for (int i = 0; i < thickness; i++) {
- // Calculate V coordinate: Scale i (0..thickness) to (0..texH)
- int v = (i * texH) / thickness;
- if (v >= texH) v = texH - 1;
-
- int off = i - centerOffset;
- int ox = off * perX;
- int oy = off * perY;
-
- drawTexturedLine(dst, pitch, width, height, startX + ox, startY + oy, endX + ox, endY + oy, nut, spriteIdx, v);
+ // Row major case (tall)
+ iVar2 = abs(sVar5 - sVar7_end);
+ int local_30 = (int)(((long long)iVar2 * (long long)texH) / texW) + 2;
+ if (texH < local_30) local_30 = texH;
+ int local_24 = -local_30;
+ const byte *local_28 = srcBase;
+ int sVar1_half = local_30 >> 1;
+ for (int local_2c = 0; local_2c < local_30; local_2c++) {
+ drawTexturedSegment(dst, pitch, width, height, (sVar4 - sVar1_half) + local_2c, sVar5,
+ (sVar6_end - sVar1_half) + local_2c, sVar7_end, texW, local_28);
+ for (local_24 = texH + local_24; local_24 > 0; local_24 -= local_30) {
+ local_28 += texW;
+ }
+ }
}
}
-
void InsaneRebel2::drawLine(byte *dst, int pitch, int width, int height, int x0, int y0, int x1, int y1, byte color) {
int dx = abs(x1 - x0), sx = x0 < x1 ? 1 : -1;
int dy = -abs(y1 - y0), sy = y0 < y1 ? 1 : -1;
diff --git a/engines/scumm/insane/insane_rebel.h b/engines/scumm/insane/insane_rebel.h
index 73211c1e629..abe4dd428a0 100644
--- a/engines/scumm/insane/insane_rebel.h
+++ b/engines/scumm/insane/insane_rebel.h
@@ -68,7 +68,8 @@ public:
int16 par1, int16 par2, int16 par3, int16 par4) override;
void drawLine(byte *dst, int pitch, int width, int height, int x0, int y0, int x1, int y1, byte color);
- void drawTexturedLine(byte *dst, int pitch, int width, int height, int x0, int y0, int x1, int y1, NutRenderer *nut, int spriteIdx, int v);
+ // mask231: when true, color 231 is treated as transparent (legacy sprites). For laser beams set false.
+ void drawTexturedLine(byte *dst, int pitch, int width, int height, int x0, int y0, int x1, int y1, NutRenderer *nut, int spriteIdx, int v, bool mask231 = true);
void drawLaserBeam(byte *dst, int pitch, int width, int height, int x0, int y0, int x1, int y1, int progress, int maxProgress, int thickness, int param_9, NutRenderer *nut, int spriteIdx);
void renderNutSprite(byte *dst, int pitch, int width, int height, int x, int y, NutRenderer *nut, int spriteIdx);
Commit: 0e87f2c99c8a51b9283038fd007789b7c7e977ba
https://github.com/scummvm/scummvm/commit/0e87f2c99c8a51b9283038fd007789b7c7e977ba
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:08:48+02:00
Commit Message:
SCUMM: RA2: Play intro before level 1
Changed paths:
engines/scumm/insane/insane_rebel.cpp
engines/scumm/insane/insane_rebel.h
engines/scumm/scumm.cpp
diff --git a/engines/scumm/insane/insane_rebel.cpp b/engines/scumm/insane/insane_rebel.cpp
index 5dffc5a725f..cbd3ecd607b 100644
--- a/engines/scumm/insane/insane_rebel.cpp
+++ b/engines/scumm/insane/insane_rebel.cpp
@@ -75,6 +75,9 @@ InsaneRebel2::InsaneRebel2(ScummEngine_v7 *scumm) {
_smush_icons2Nut = nullptr; // Not used for Rebel2
_smush_cockpitNut = new NutRenderer(_vm, "SYSTM/DISPFONT.NUT");
+ // Load DIHIFONT.NUT for in-video messages/subtitles (Opcode 9)
+ _rebelMsgFont = new SmushFont(_vm, "SYSTM/DIHIFONT.NUT", true);
+
_enemies.clear();
_rebelHandler = 8; // Default to Handler 8 (ground vehicle) for Level 1
_rebelLevelType = 0; // Level type from Opcode 6 par3, determines HUD sprite variant
@@ -191,6 +194,7 @@ InsaneRebel2::~InsaneRebel2() {
delete _rebelHudDamage;
delete _rebelHudEffects;
delete _rebelHudHiRes;
+ delete _rebelMsgFont;
}
@@ -1117,7 +1121,10 @@ void InsaneRebel2::procPostRendering(byte *renderBitmap, int32 codecparam, int32
const int videoWidth = 320; // Native video width
const int videoHeight = 200; // Native video height
const int statusBarY = 180; // 0xb4 - status bar starts at Y=180 in video coords
-
+
+ // Hide HUD/status bar during intro videos (marked by SmushPlayer video flag 0x20)
+ bool introPlaying = ((_player->_curVideoFlags & 0x20) != 0);
+ if (!introPlaying) {
// ============================================================
// STEP 0: Fill status bar background (FUN_004288c0 equivalent)
// ============================================================
@@ -1477,3 +1484,5 @@ void InsaneRebel2::procPostRendering(byte *renderBitmap, int32 codecparam, int32
}
}
+
+}
diff --git a/engines/scumm/insane/insane_rebel.h b/engines/scumm/insane/insane_rebel.h
index abe4dd428a0..121110b9532 100644
--- a/engines/scumm/insane/insane_rebel.h
+++ b/engines/scumm/insane/insane_rebel.h
@@ -41,6 +41,10 @@ public:
NutRenderer *_smush_cockpitNut;
NutRenderer *_smush_dispfontNut; // DAT_00482200 - DISPFONT.NUT for status bar (difficulty, shields, lives, score)
+
+ // Font used for opcode 9 text/subtitle rendering (DIHIFONT / TALKFONT)
+ SmushFont *_rebelMsgFont;
+ bool _introCursorPushed; // true when we've pushed an invisible cursor for intro
// Rebel Assault 2: Dynamically loaded HUD overlays (from CHK scripts)
// These correspond to the original game's global variables
diff --git a/engines/scumm/scumm.cpp b/engines/scumm/scumm.cpp
index 3ebc7429d00..cbc1a7ebad1 100644
--- a/engines/scumm/scumm.cpp
+++ b/engines/scumm/scumm.cpp
@@ -2686,10 +2686,16 @@ Common::Error ScummEngine::go() {
if (_game.id == GID_REBEL2) {
// Use 12 FPS as default, same as The Dig. FT uses 10.
// Since we don't have the standard scripts initializing this, we pass it here.
- if (_game.features & GF_DEMO)
+ if (_game.features & GF_DEMO) {
((ScummEngine_v7 *)this)->_splayer->play("OPEN/O_DEMO.SAN", 12);
- else
+ } else {
+ // Mark the following SAN as an intro so HUD isn't rendered during it (bit 0x20)
+ ((ScummEngine_v7 *)this)->_splayer->setCurVideoFlags(0x20);
+ ((ScummEngine_v7 *)this)->_splayer->play("LEV01/01BEG.SAN", 12);
+ // Clear intro flag and immediately start the mission SAN
+ ((ScummEngine_v7 *)this)->_splayer->setCurVideoFlags(0);
((ScummEngine_v7 *)this)->_splayer->play("LEV01/01P01.SAN", 12);
+ }
return Common::kNoError;
}
#endif
Commit: 77722bbfed261fc5295c72f9d22392effaff383c
https://github.com/scummvm/scummvm/commit/77722bbfed261fc5295c72f9d22392effaff383c
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:08:48+02:00
Commit Message:
SCUMM: RA2: Remove obsolete setup code
Changed paths:
engines/scumm/insane/insane_rebel.cpp
engines/scumm/scumm.cpp
diff --git a/engines/scumm/insane/insane_rebel.cpp b/engines/scumm/insane/insane_rebel.cpp
index cbd3ecd607b..74d11f5ec41 100644
--- a/engines/scumm/insane/insane_rebel.cpp
+++ b/engines/scumm/insane/insane_rebel.cpp
@@ -202,7 +202,7 @@ int32 InsaneRebel2::processMouse() {
int32 buttons = 0;
// Get button state directly from event manager (SCUMM VARs aren't updated during SMUSH)
- static bool wasPressed = false;
+ bool wasPressed = false;
bool isPressed = (_vm->_system->getEventManager()->getButtonState() & 1) != 0;
// Edge detection: only trigger on button press (not hold)
@@ -806,7 +806,7 @@ void InsaneRebel2::drawTexturedLine(byte *dst, int pitch, int width, int height,
}
// Helper: draw a textured segment between two points using the game's original routine (FUN_00429360 port)
-static void drawTexturedSegment(byte *dst, int pitch, int width, int height, int param_3, int param_4, int param_5, int param_6, int param_7, const byte *param_8) {
+void drawTexturedSegment(byte *dst, int pitch, int width, int height, int param_3, int param_4, int param_5, int param_6, int param_7, const byte *param_8) {
// Ported from FUN_00429360 (decompiled). Only 0 in texture is transparent.
int sVar4 = 0; // left
int sVar1 = 0; // top
@@ -1190,13 +1190,6 @@ void InsaneRebel2::procPostRendering(byte *renderBitmap, int32 codecparam, int32
}
}
}
-
- static bool debugEmbeddedOnce[5] = {false};
- if (!debugEmbeddedOnce[hudSlot]) {
- debug("Rebel2: Drawing embedded HUD slot %d (%dx%d) at (%d,%d)",
- hudSlot, frame.width, frame.height, destX, destY);
- debugEmbeddedOnce[hudSlot] = true;
- }
}
}
@@ -1209,23 +1202,7 @@ void InsaneRebel2::procPostRendering(byte *renderBitmap, int32 codecparam, int32
// - Sprites 2-5: Difficulty stars (1-4 stars, drawn at 0,0)
// - Sprite 6: Shield bar fill (drawn with clip rect X=0x3f, Y=9, W=64, H=6)
// - Sprite 7: Shield alert (flashing red when shields critical)
- //
- // All sprites are drawn to buffer at (0,0), buffer composited at Y=180
- // For ScummVM, we draw directly at Y=statusBarY
if (_smush_cockpitNut) {
- // Debug: Log DISPFONT.NUT sprite info once
- /*static bool loggedDispfont = false;
- if (!loggedDispfont) {
- int numSprites = _smush_cockpitNut->getNumChars();
- debug("Rebel2: DISPFONT.NUT has %d sprites, statusBarY=%d:", numSprites, statusBarY);
- for (int i = 0; i < numSprites && i < 10; i++) {
- int sw = _smush_cockpitNut->getCharWidth(i);
- int sh = _smush_cockpitNut->getCharHeight(i);
- debug(" Sprite %d: %dx%d", i, sw, sh);
- }
- loggedDispfont = true;
- }*/
-
// Draw status bar background frame (sprite 1) at (0, statusBarY)
// This sprite is the full-width status bar background
if (_smush_cockpitNut->getNumChars() > 1) {
@@ -1302,15 +1279,7 @@ void InsaneRebel2::procPostRendering(byte *renderBitmap, int32 codecparam, int32
// Draw score - uses FUN_00434cb0 (text rendering) at X=0x101(257)
// TODO: Implement score rendering
- } else {
- static bool warnedNullOnce = false;
- if (!warnedNullOnce) {
- debug("Rebel2: WARNING - _smush_cockpitNut (DISPFONT.NUT) is null!");
- warnedNullOnce = true;
- }
- }
-
-
+ }
Common::List<enemy>::iterator it;
for (it = _enemies.begin(); it != _enemies.end(); ++it) {
@@ -1330,7 +1299,7 @@ void InsaneRebel2::procPostRendering(byte *renderBitmap, int32 codecparam, int32
// Draw Green Indicators (Corner Brackets) for Easy (0) and Medium (1) difficulty
// Hard (2) mode does not show indicators.
if (_difficulty < 2) {
- const byte color = 5; // Green (Standard VGA Index 10)
+ const byte color = 5; // Green color index for brackets
// Clip the rect to screen bounds for drawing logic is handled inside drawLine if implemented safely,
// but drawCornerBrackets relies on drawLine which iterates.
// We pass full screen width/height to safe-guard.
@@ -1469,14 +1438,6 @@ void InsaneRebel2::procPostRendering(byte *renderBitmap, int32 codecparam, int32
if (_smush_iconsNut->getNumChars() > reticleIndex) {
int cw = _smush_iconsNut->getCharWidth(reticleIndex);
int ch = _smush_iconsNut->getCharHeight(reticleIndex);
-
- static bool debugCrosshairOnce = false;
- if (!debugCrosshairOnce) {
- debug("Rebel2: Drawing crosshair sprite %d at (%d,%d) size %dx%d",
- reticleIndex, _vm->_mouse.x - cw / 2, _vm->_mouse.y - ch / 2, cw, ch);
- debugCrosshairOnce = true;
- }
-
// Center the crosshair on mouse position (in world coordinates)
renderNutSprite(renderBitmap, pitch, width, height, _vm->_mouse.x - cw / 2 + _viewX, _vm->_mouse.y - ch / 2 + _viewY, _smush_iconsNut, reticleIndex);
}
diff --git a/engines/scumm/scumm.cpp b/engines/scumm/scumm.cpp
index cbc1a7ebad1..020da441c8a 100644
--- a/engines/scumm/scumm.cpp
+++ b/engines/scumm/scumm.cpp
@@ -2616,16 +2616,6 @@ void ScummEngine_v7::syncSoundSettings() {
if (!_setupIsComplete)
return;
- if (_game.id == GID_REBEL2) {
- ScummEngine::syncSoundSettings();
- if (_imuseDigital) {
- _imuseDigital->diMUSESetMusicGroupVol(ConfMan.getInt("music_volume") / 2);
- _imuseDigital->diMUSESetVoiceGroupVol(ConfMan.getInt("speech_volume") / 2);
- _imuseDigital->diMUSESetSFXGroupVol(ConfMan.getInt("sfx_volume") / 2);
- }
- return;
- }
-
if (!isUsingOriginalGUI()) {
ScummEngine::syncSoundSettings();
if (_splayer) {
@@ -2690,8 +2680,8 @@ Common::Error ScummEngine::go() {
((ScummEngine_v7 *)this)->_splayer->play("OPEN/O_DEMO.SAN", 12);
} else {
// Mark the following SAN as an intro so HUD isn't rendered during it (bit 0x20)
- ((ScummEngine_v7 *)this)->_splayer->setCurVideoFlags(0x20);
- ((ScummEngine_v7 *)this)->_splayer->play("LEV01/01BEG.SAN", 12);
+ //((ScummEngine_v7 *)this)->_splayer->setCurVideoFlags(0x20);
+ //((ScummEngine_v7 *)this)->_splayer->play("LEV01/01BEG.SAN", 12);
// Clear intro flag and immediately start the mission SAN
((ScummEngine_v7 *)this)->_splayer->setCurVideoFlags(0);
((ScummEngine_v7 *)this)->_splayer->play("LEV01/01P01.SAN", 12);
Commit: 6b45c553681dd0aa8ed99eac08def7c5ffb0fd53
https://github.com/scummvm/scummvm/commit/6b45c553681dd0aa8ed99eac08def7c5ffb0fd53
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:08:48+02:00
Commit Message:
SCUMM: RA2: Clean up state initialization
Changed paths:
engines/scumm/insane/insane_rebel.cpp
engines/scumm/insane/insane_rebel.h
diff --git a/engines/scumm/insane/insane_rebel.cpp b/engines/scumm/insane/insane_rebel.cpp
index 74d11f5ec41..9709bf850f7 100644
--- a/engines/scumm/insane/insane_rebel.cpp
+++ b/engines/scumm/insane/insane_rebel.cpp
@@ -183,6 +183,15 @@ InsaneRebel2::InsaneRebel2(ScummEngine_v7 *scumm) {
_shots[i].active = false;
_shots[i].counter = 0;
}
+
+ for (i = 0; i < 5; i++) {
+ _rebelEmbeddedHud[i].pixels = nullptr;
+ _rebelEmbeddedHud[i].width = 0;
+ _rebelEmbeddedHud[i].height = 0;
+ _rebelEmbeddedHud[i].renderX = 0;
+ _rebelEmbeddedHud[i].renderY = 0;
+ _rebelEmbeddedHud[i].valid = false;
+ }
}
@@ -625,7 +634,6 @@ void InsaneRebel2::loadEmbeddedSan(int userId, byte *animData, int32 size, byte
if (width > 0 && height > 0 && width <= 800 && height <= 480) {
if (frame.width != width || frame.height != height || !frame.pixels) {
- frame.clear();
frame.pixels = (byte *)calloc(width * height, 1);
frame.width = width;
frame.height = height;
diff --git a/engines/scumm/insane/insane_rebel.h b/engines/scumm/insane/insane_rebel.h
index 121110b9532..13898780160 100644
--- a/engines/scumm/insane/insane_rebel.h
+++ b/engines/scumm/insane/insane_rebel.h
@@ -123,11 +123,6 @@ public:
int renderX; // X position to render (0 = centered based on slot)
int renderY; // Y position to render
bool valid; // True if this slot has valid data
-
- EmbeddedSanFrame() : pixels(nullptr), width(0), height(0),
- renderX(0), renderY(0), valid(false) {}
- ~EmbeddedSanFrame() { free(pixels); }
- void clear() { free(pixels); pixels = nullptr; width = height = 0; valid = false; }
};
EmbeddedSanFrame _rebelEmbeddedHud[5]; // Index 0 unused, 1-4 for userId slots
Commit: df10c54615eb1cec5358b9eeaa604497499462ca
https://github.com/scummvm/scummvm/commit/df10c54615eb1cec5358b9eeaa604497499462ca
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:08:49+02:00
Commit Message:
SCUMM: RA2: Remove unused state fields
Changed paths:
engines/scumm/insane/insane_rebel.cpp
engines/scumm/insane/insane_rebel.h
diff --git a/engines/scumm/insane/insane_rebel.cpp b/engines/scumm/insane/insane_rebel.cpp
index 9709bf850f7..f091ecc0f3c 100644
--- a/engines/scumm/insane/insane_rebel.cpp
+++ b/engines/scumm/insane/insane_rebel.cpp
@@ -60,13 +60,6 @@ InsaneRebel2::InsaneRebel2(ScummEngine_v7 *scumm) {
_smush_bensgoggNut = nullptr;
// Rebel Assault 2 specific initialization can go here
- _rebelHudPrimary = nullptr;
- _rebelHudSecondary = nullptr;
- _rebelHudCockpit = nullptr;
- _rebelHudExplosion = nullptr;
- _rebelHudDamage = nullptr;
- _rebelHudEffects = nullptr;
- _rebelHudHiRes = nullptr;
// Rebel Assault 2: Load cockpit sprites NUT which contains crosshairs, explosions, status bar
// CPITIMAG.NUT = low-res (320x200), CPITIMHI.NUT = high-res (640x480)
@@ -196,13 +189,6 @@ InsaneRebel2::InsaneRebel2(ScummEngine_v7 *scumm) {
InsaneRebel2::~InsaneRebel2() {
- delete _rebelHudPrimary;
- delete _rebelHudSecondary;
- delete _rebelHudCockpit;
- delete _rebelHudExplosion;
- delete _rebelHudDamage;
- delete _rebelHudEffects;
- delete _rebelHudHiRes;
delete _rebelMsgFont;
}
diff --git a/engines/scumm/insane/insane_rebel.h b/engines/scumm/insane/insane_rebel.h
index 13898780160..b75564b6486 100644
--- a/engines/scumm/insane/insane_rebel.h
+++ b/engines/scumm/insane/insane_rebel.h
@@ -46,15 +46,6 @@ public:
SmushFont *_rebelMsgFont;
bool _introCursorPushed; // true when we've pushed an invisible cursor for intro
- // Rebel Assault 2: Dynamically loaded HUD overlays (from CHK scripts)
- // These correspond to the original game's global variables
- NutRenderer *_rebelHudPrimary; // DAT_00482240 - Primary HUD overlay
- NutRenderer *_rebelHudSecondary; // DAT_00482238 - Secondary HUD graphics
- NutRenderer *_rebelHudCockpit; // DAT_00482268 - Ship cockpit frame overlay
- NutRenderer *_rebelHudExplosion; // DAT_00482250 - Explosion overlay sprites
- NutRenderer *_rebelHudDamage; // DAT_00482248 - Damage indicator sprites
- NutRenderer *_rebelHudEffects; // DAT_00482258 - Additional effects
- NutRenderer *_rebelHudHiRes; // DAT_00482260 - High-res HUD alternative
int32 processMouse() override;
bool isBitSet(int n) override;
Commit: 2cebdfc1fcebbb0d2bd802aaacbe77f4f15cb447
https://github.com/scummvm/scummvm/commit/2cebdfc1fcebbb0d2bd802aaacbe77f4f15cb447
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:08:49+02:00
Commit Message:
SCUMM: RA2: Implement damage bar
Changed paths:
engines/scumm/insane/insane_rebel.cpp
engines/scumm/insane/insane_rebel.h
diff --git a/engines/scumm/insane/insane_rebel.cpp b/engines/scumm/insane/insane_rebel.cpp
index f091ecc0f3c..804f524380f 100644
--- a/engines/scumm/insane/insane_rebel.cpp
+++ b/engines/scumm/insane/insane_rebel.cpp
@@ -76,11 +76,17 @@ InsaneRebel2::InsaneRebel2(ScummEngine_v7 *scumm) {
_rebelLevelType = 0; // Level type from Opcode 6 par3, determines HUD sprite variant
_playerDamage = 0;
+ _playerShield = 255; // Full shields by default (255)
_playerLives = 3;
_playerScore = 0;
_viewX = 0;
_viewY = 0;
+ // Retail globals mapped: hit counter, cooldown, invulnerability flag
+ _rebelHitCounter = 0;
+ _rebelHitCooldown = 0;
+ _rebelInvulnerable = false;
+
_difficulty = 1; // Default to Medium (1). TODO: Read from game config
_speed = 12;
@@ -405,65 +411,9 @@ void InsaneRebel2::iactRebel2Scene1(byte *renderBitmap, int32 codecparam, int32
}
} else if (par1 == 3) {
- // Opcode 3: Often used for clearing/resetting
- // debug("Rebel2 IACT Opcode 3: par2=%d par3=%d par4=%d", par2, par3, par4);
-
- // Handle damage dealing (Subcode 5 in FUN_401234)
- // Opcode 3 is complicated:
- // Based on FUN_401234, case 1 (Opcode 3), checks local_c (offset +4 / par2?)
- // In iactRebel2Scene1, par1=Opcode.
- // If par1 == 1 (Opcode 1 / FUN_401234 case 1):
- // It checks local_c which is from offset +4 (par3).
- // If par3 == 5 -> Damage Logic.
- //
- // Actually, FUN_401234 switches on `*local_14 - 2`.
- // If `*local_14` (Opcode) is 3, switch value is 1.
- // So Opcode 3 -> Case 1.
-
- if (par1 == 3) {
- // Inside Case 1: local_c = local_14[2] which is offset +4 (par3)
- if (par3 == 5) {
- // Damage Logic
- // bVar1 = FUN_423970(local_14[5]); // Check if source enemy (offset +10 / par6?) is active
- // Read extra params from stream
- // Note: `procIACT` already read parems into par1..par4.
- // But Opcode 3 logic in FUN_401234 uses offset +10 (par6) which isn't passed to `procIACT` signature fully?
- // Wait, `procIACT` reads 4 shorts.
- // par1 (+0), par2 (+2), par3 (+4), par4 (+6).
- // We need +8 and +10.
-
- b.skip(2); // Offset +8 (par5 used in assembly but not here yet)
- int16 par6 = b.readSint16LE(); // Offset +10 (Enemy ID?)
-
- // Check if enemy is disabled (processed in FUN_423970)
- bool enemyDisabled = isBitSet(par6);
-
- if (!enemyDisabled) {
- // Probability check: Random(100) < Limit
- // The limit seems to come from a table or fixed value?
- // In FUN_401234: `sVar2 = *(short *)(&DAT_0047e0fc + ...)`
- // For now, let's just use a fixed probability or assume successful hit
- // if (rnd < prob) ...
-
- // Increment damage
- // In assembly: DAT_0047a7ec += damage
- // Damage amount also comes from table?
- int damageAmount = 5; // Placeholder
-
- // Only apply damage occasionally to simulate probability
- if ((_vm->_system->getMillis() % 100) < 20) { // 20% chance per frame (approx)
- _playerDamage += damageAmount;
- if (_playerDamage > 255) _playerDamage = 255;
-
- debug("Rebel2: Player HIT by Enemy %d. Damage=%d", par6, _playerDamage);
-
- // TODO: Flash screen red / shake
- }
- }
- }
- }
-
- } else if (par1 == 5) {
+ iactRebel2Opcode3(b, par2, par3, par4);
+ }
+ else if (par1 == 5) {
// Opcode 5: Special handling based on par2 value
// Disassembly shows sub-opcodes 0xD (13) and 0xE (14)
debug("Rebel2 IACT Opcode 5: par2=%d par3=%d par4=%d", par2, par3, par4);
@@ -508,6 +458,73 @@ void InsaneRebel2::iactRebel2Scene1(byte *renderBitmap, int32 codecparam, int32
}
}
+void InsaneRebel2::iactRebel2Opcode3(Common::SeekableReadStream &b, int16 par2, int16 par3, int16 par4) {
+ // Handle IACT opcode 3 subcases (damage, counters, special 100 branch)
+ // Mirrors retail FUN_0041CADB case 1 behavior where possible.
+
+ // Very small cooldown counter decremented on each IACT to emulate DAT_0045790a behavior
+ if (_rebelHitCooldown > 0) _rebelHitCooldown--;
+
+ // Subcase: par3 == 5 -> damage logic, expects extra param at +10 (source enemy ID)
+ if (par3 == 5) {
+ b.skip(2); // Offset +8
+ int16 srcId = b.readSint16LE(); // Offset +10 (Enemy ID)
+
+ // Only proceed if source is active (bit clear)
+ if (!isBitSet(srcId)) {
+ if (_rebelHitCooldown < 2) {
+ int limit = 20 + _difficulty * 20; // heuristic mapping for probability table
+ if (limit < 5) limit = 5;
+ if (limit > 90) limit = 90;
+ if (_vm->_rnd.getRandomNumber(100) < limit) {
+ // Apply damage unless invulnerable flag set (DAT_0047ab64)
+ if (!_rebelInvulnerable) {
+ int damageAmount = 5 + (_difficulty * 2);
+ // Apply to shields first (do not end game on depletion during tests)
+
+
+ // Update the retail-like damage accumulator (DAT_0047a7ec equivalent)
+ _playerDamage += damageAmount;
+ if (_playerDamage > 255) _playerDamage = 255;
+ debug("Rebel2: Damage HIT by Enemy %d. Damage=%d (limit=%d)", srcId, _playerDamage, limit);
+ // TODO: call UI update / flash screen / play sound to match retail (FUN_00420515 / FUN_0041189e)
+ }
+ // Impose short cooldown to prevent immediate repeated damage
+ _rebelHitCooldown = 6;
+ }
+ }
+ }
+ }
+ // Subcase: par3 == 1 -> increment hit counter when source active and par4 != 4
+ else if (par3 == 1) {
+ b.skip(2); // read extra param (source id)
+ int16 srcId = b.readSint16LE();
+ if (!isBitSet(srcId) && par4 != 4) {
+ _rebelHitCounter++;
+ debug("Rebel2: Incremented hit counter DAT_0047ab80 -> %d (source=%d)", _rebelHitCounter, srcId);
+ }
+ }
+ // Special-case branch when par2 == 100 (retail: triggers damage/sound via different offsets)
+ else if (par2 == 100) {
+ b.skip(2);
+ int16 srcId = b.readSint16LE();
+ if (!isBitSet(srcId)) {
+ int limit = 20 + _difficulty * 20;
+ if (_vm->_rnd.getRandomNumber(100) < limit) {
+ if (!_rebelInvulnerable) {
+ int damageAmount = 5 + (_difficulty * 2);
+ // Increment the retail-like damage accumulator (DAT_0047a7ec equivalent)
+ _playerDamage += damageAmount;
+ if (_playerDamage > 255) _playerDamage = 255;
+ debug("Rebel2: Damage HIT (special) by Enemy %d. Damage=%d (limit=%d)", srcId, _playerDamage, limit);
+ }
+ _rebelHitCooldown = 6;
+ }
+ }
+ }
+ // other subcases not implemented yet
+}
+
void InsaneRebel2::enemyUpdate(byte *renderBitmap, Common::SeekableReadStream &b, int16 par2, int16 par3, int16 par4) {
// Opcode 4: Enemy position update
// Read 5 shorts from the stream (offset +8 through +16)
@@ -1194,8 +1211,8 @@ void InsaneRebel2::procPostRendering(byte *renderBitmap, int32 codecparam, int32
// From assembly FUN_0041c012:
// - Sprite 1: Status bar background frame (full width, drawn at 0,0)
// - Sprites 2-5: Difficulty stars (1-4 stars, drawn at 0,0)
- // - Sprite 6: Shield bar fill (drawn with clip rect X=0x3f, Y=9, W=64, H=6)
- // - Sprite 7: Shield alert (flashing red when shields critical)
+ // - Sprite 6: Damage bar fill (drawn with clip rect X=0x3f, Y=9, W=64, H=6)
+ // - Sprite 7: Damage alert (flashing red when damage critical)
if (_smush_cockpitNut) {
// Draw status bar background frame (sprite 1) at (0, statusBarY)
// This sprite is the full-width status bar background
@@ -1213,18 +1230,16 @@ void InsaneRebel2::procPostRendering(byte *renderBitmap, int32 codecparam, int32
renderNutSprite(renderBitmap, pitch, width, height, _viewX, statusBarY + _viewY, _smush_cockpitNut, difficultySprite);
}
- // Draw shield bar (sprite 6)
- // Assembly uses clip rect: X=0x3f(63), Y=0x9(9), W=0x40(64), H=0x6(6)
- // The width is scaled based on shield value (param_1 >> 2)
- // For now, draw at position (0, statusBarY) - sprite has internal positioning
- if (_smush_cockpitNut->getNumChars() > 6) {
- // Calculate width based on damage.
- // Assuming max damage 255 = empty bar.
- // Bar max width is 64 pixels.
- // Damage 0 = Width 64. Damage 255 = Width 0.
- // Width = 64 - (Damage / 4)
- int barWidth = 64 - (_playerDamage / 4);
- if (barWidth < 0) barWidth = 0;
+// Draw damage bar (sprite 6)
+ // Assembly uses clip rect: X=0x3f(63), Y=0x9(9), W=0x40(64), H=0x6(6)
+ // The width is scaled based on accumulated damage value (0..255)
+ // For now, draw at position (0, statusBarY) - sprite has internal positioning
+ if (_smush_cockpitNut->getNumChars() > 6) {
+ // Calculate width based on damage value.
+ // Damage range: 0..255 where 255 = full (Width 64)
+ int drawWidth = (64 * _playerDamage) / 255;
+ if (drawWidth < 0) drawWidth = 0;
+ if (drawWidth > 64) drawWidth = 64;
// We need to draw a partial sprite or use a clip rect.
// smlayer_drawSomething supports scaling/clip?
@@ -1235,27 +1250,40 @@ void InsaneRebel2::procPostRendering(byte *renderBitmap, int32 codecparam, int32
// NOTE: smlayer_drawSomething calls `_smush_cockpitNut->draw(...)`
// We can't easily clip without modifying NutRenderer or using a custom draw loop.
- // Let's implement a custom draw loop for the shield bar here since it's simple.
+ // Let's implement a custom draw loop for the damage bar here since it's simple.
- // Sprite 6 data
+ // Sprite 6 data (we need to copy a CLIP rect from within this sprite)
const byte *src = _smush_cockpitNut->getCharData(6);
int sw = _smush_cockpitNut->getCharWidth(6);
int sh = _smush_cockpitNut->getCharHeight(6);
- int sx = 63; // Hardcoded X offset from assembly
- int sy = 9; // Hardcoded Y offset (relative to status bar top)
+ // Clip rect inside the sprite (from assembly): X=63, Y=9, W=64, H=6
+ const int clipX = 63;
+ const int clipY = 9;
+ const int clipW = 64;
+ const int clipH = 6;
// Draw clipped width
if (src && sw > 0 && sh > 0) {
- int drawWidth = MIN(barWidth, sw);
- for (int y = 0; y < sh; y++) {
- for (int x = 0; x < drawWidth; x++) {
- // Render to (0 + sx + x + viewX, statusBarY + sy + y + viewY)
- int destX = sx + x + _viewX;
- int destY = statusBarY + sy + y + _viewY;
- if (destX < pitch && destY < height) {
- byte pixel = src[y * sw + x];
- if (pixel != 0) {
- renderBitmap[destY * pitch + destX] = pixel;
+ // Clamp drawWidth to the clip width and ensure we don't read past sprite bounds
+ int maxClipW = sw - clipX;
+ if (maxClipW < 0) maxClipW = 0;
+ int drawW = drawWidth;
+ if (drawW > clipW) drawW = clipW;
+ if (drawW > maxClipW) drawW = maxClipW;
+ if (drawW > 0) {
+ int drawH = clipH;
+ if (drawH > (sh - clipY)) drawH = sh - clipY;
+ if (drawH < 0) drawH = 0;
+ for (int y = 0; y < drawH; y++) {
+ for (int x = 0; x < drawW; x++) {
+ // Render to (clipX + x + viewX, statusBarY + clipY + y + viewY)
+ int destX = clipX + x + _viewX;
+ int destY = statusBarY + clipY + y + _viewY;
+ if (destX >= 0 && destX < pitch && destY >= 0 && destY < height) {
+ byte pixel = src[(clipY + y) * sw + (clipX + x)];
+ if (pixel != 0) {
+ renderBitmap[destY * pitch + destX] = pixel;
+ }
}
}
}
@@ -1263,9 +1291,14 @@ void InsaneRebel2::procPostRendering(byte *renderBitmap, int32 codecparam, int32
}
}
- // Draw shield alert overlay (sprite 7) when shields critical (> 0xAA = 170)
+ // Draw damage alert overlay (sprite 7) when damage is critical (> 0xAA = 170)
// Only draws when frame counter bit 3 is clear (every 8 frames)
- // For now, skip - TODO: implement shield critical flashing
+ if (_smush_cockpitNut->getNumChars() > 7) {
+ if (_playerDamage > 170 && ((curFrame & 8) == 0)) {
+ // Draw overlay sprite 7 at same general region (sx, sy)
+ renderNutSprite(renderBitmap, pitch, width, height, 63 + _viewX, statusBarY + 9 + _viewY, _smush_cockpitNut, 7);
+ }
+ }
// Draw lives indicator - assembly shows at X=0xa8 (168), Y=7
// Uses sprite 1 again with different clip rect
diff --git a/engines/scumm/insane/insane_rebel.h b/engines/scumm/insane/insane_rebel.h
index b75564b6486..4f8cb388a01 100644
--- a/engines/scumm/insane/insane_rebel.h
+++ b/engines/scumm/insane/insane_rebel.h
@@ -55,6 +55,9 @@ public:
int32 setupsan13, Common::SeekableReadStream &b, int32 size, int32 flags,
int16 par1, int16 par2, int16 par3, int16 par4);
+ // Handle IACT opcode 3 subcases (damage, hit-counters, special cases)
+ void iactRebel2Opcode3(Common::SeekableReadStream &b, int16 par2, int16 par3, int16 par4);
+
void procPostRendering(byte *renderBitmap, int32 codecparam, int32 setupsan12,
int32 setupsan13, int32 curFrame, int32 maxFrame) override;
@@ -141,13 +144,19 @@ public:
Explosion _explosions[5];
void spawnExplosion(int x, int y, int objectHalfWidth);
- int16 _playerDamage; // 0 to 255 (Accumulated damage)
+ int16 _playerDamage; // Legacy damage counter (kept for compatibility/telemetry)
+ int16 _playerShield; // Shields: 0..255 where 255 = full
int16 _playerLives;
int32 _playerScore;
int _viewX;
int _viewY;
+ // Rebel per-level counters / flags mapped from retail globals
+ int _rebelHitCounter; // DAT_0047ab80 - hit counter / state tracker
+ int _rebelHitCooldown; // DAT_0045790a - cooldown / timing for damage checks
+ bool _rebelInvulnerable; // DAT_0047ab64 - toggles invulnerability / state
+
struct Shot {
bool active;
Commit: 1ada210184ec23078dd8b8460ddfd1179b3760b8
https://github.com/scummvm/scummvm/commit/1ada210184ec23078dd8b8460ddfd1179b3760b8
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:08:49+02:00
Commit Message:
SCUMM: RA2: Refactor opcode 2 handling
Changed paths:
engines/scumm/insane/insane_rebel.cpp
engines/scumm/insane/insane_rebel.h
diff --git a/engines/scumm/insane/insane_rebel.cpp b/engines/scumm/insane/insane_rebel.cpp
index 804f524380f..ee0fa532732 100644
--- a/engines/scumm/insane/insane_rebel.cpp
+++ b/engines/scumm/insane/insane_rebel.cpp
@@ -87,6 +87,13 @@ InsaneRebel2::InsaneRebel2(ScummEngine_v7 *scumm) {
_rebelHitCooldown = 0;
_rebelInvulnerable = false;
+ // Initialize mirrored retail counters
+ for (int i = 0; i < 10; ++i) {
+ _rebelValueCounters[i] = 0;
+ _rebelMaskCounters[i] = 0;
+ }
+ _rebelLastCounter = 0;
+
_difficulty = 1; // Default to Medium (1). TODO: Read from game config
_speed = 12;
@@ -374,42 +381,8 @@ void InsaneRebel2::iactRebel2Scene1(byte *renderBitmap, int32 codecparam, int32
if (par1 == 4) {
enemyUpdate(renderBitmap, b, par2, par3, par4);
} else if (par1 == 2) {
- // Opcode 2: Often used for bit setting
- // Disassembly shows this is handled but we don't have full context
- // debug("Rebel2 IACT Opcode 2: par2=%d par3=%d par4=%d", par2, par3, par4);
-
- // Handle dependency linking (par3 == 4)
- if (par3 == 4) {
- int16 childId = b.readSint16LE(); // Offset +8
- int16 parentId = b.readSint16LE(); // Offset +10
-
- if (parentId >= 0 && parentId < 512) {
- // Shift links
- _rebelLinks[parentId][2] = _rebelLinks[parentId][1];
- _rebelLinks[parentId][1] = _rebelLinks[parentId][0];
- _rebelLinks[parentId][0] = childId;
-
- // Apply initial state based on parent state
- // If parent is ALIVE (Bit Clear) -> Disable new Child (Set Bit)
- // If parent is DEAD (Bit Set) -> Enable new Child (Clear Bit)
- if (!isBitSet(parentId)) {
- setBit(childId);
- debug("Rebel2: Linked ID=%d to Parent=%d (Slot 0). Parent Alive -> Child Disabled.", childId, parentId);
- } else {
- clearBit(childId);
- debug("Rebel2: Linked ID=%d to Parent=%d (Slot 0). Parent Dead -> Child Enabled.", childId, parentId);
- }
- }
- } else {
- // Skip extra data if any? Opcode 2 might vary in size.
- // Based on disassembly, it accesses offset +8 and +10 for case 4.
- // Currently we don't know size for other cases, but hopefully they don't crash stream reading.
- // 'b' is SeekableReadStream, so we might need to skip if we don't read?
- // The caller `procIACT` passes `size` but that's for the WHOLE chunk.
- // We can't skip unknown sub-opcodes easily without size map.
- // Assuming only par3=4 has extra data for now.
- }
-
+ // Delegate handling to dedicated opcode 2 handler
+ iactRebel2Opcode2(b, par2, par3, par4);
} else if (par1 == 3) {
iactRebel2Opcode3(b, par2, par3, par4);
}
@@ -457,7 +430,82 @@ void InsaneRebel2::iactRebel2Scene1(byte *renderBitmap, int32 codecparam, int32
debug("Rebel2 IACT: Unknown Opcode %d (par2=%d par3=%d par4=%d)", par1, par2, par3, par4);
}
}
+void InsaneRebel2::iactRebel2Opcode2(Common::SeekableReadStream &b, int16 par2, int16 par3, int16 par4) {
+ // Handle IACT opcode 2 subcases based on par3 (type). Mirrors FUN_00407fcb behavior where relevant.
+ // Keep existing linking behavior (par3 == 4) for compatibility.
+
+ // Link case: par3 == 4
+ if (par3 == 4) {
+ int16 childId = b.readSint16LE(); // Offset +8
+ int16 parentId = b.readSint16LE(); // Offset +10
+
+ if (parentId >= 0 && parentId < 512) {
+ // Shift links
+ _rebelLinks[parentId][2] = _rebelLinks[parentId][1];
+ _rebelLinks[parentId][1] = _rebelLinks[parentId][0];
+ _rebelLinks[parentId][0] = childId;
+
+ // Apply initial state based on parent state
+ if (!isBitSet(parentId)) {
+ setBit(childId);
+ debug("Rebel2: Linked ID=%d to Parent=%d (Slot 0). Parent Alive -> Child Disabled.", childId, parentId);
+ } else {
+ clearBit(childId);
+ debug("Rebel2: Linked ID=%d to Parent=%d (Slot 0). Parent Dead -> Child Enabled.", childId, parentId);
+ }
+ }
+ return;
+ } else if (par3 == 1) { // Probabilistic / counter cases: par3 == 1
+ int16 value = par4; // sVar6
+ int16 targetId = b.readSint16LE(); // Offset +8 (sVar7)
+
+ if (targetId < 0 || targetId >= 0x200)
+ return;
+
+ if (value > 1 && value < 10) { // 1 < value < 10: random disable
+ if (_vm->_rnd.getRandomNumber(value) == 0) {
+ setBit(targetId);
+ debug("Rebel2 IACT Opcode2: Random DISABLE target=%d (value=%d)", targetId, value);
+ }
+ } else if (value > 10 && value < 20) { // 10 < value < 20: enable/disable with special value==11 = force enable
+ if (value == 11) {
+ clearBit(targetId);
+ debug("Rebel2 IACT Opcode2: FORCE ENABLE target=%d (value=11)", targetId);
+ } else {
+ if (_vm->_rnd.getRandomNumber(value - 10) == 0) {
+ clearBit(targetId);
+ debug("Rebel2 IACT Opcode2: Random ENABLE target=%d (value=%d)", targetId, value);
+ } else {
+ setBit(targetId);
+ debug("Rebel2 IACT Opcode2: Random DISABLE target=%d (value=%d)", targetId, value);
+ }
+ }
+ } else if (value > 99 && value < 110) { // 99 < value < 110: increment value counter if target active
+ if (!isBitSet(targetId)) {
+ int idx = value - 100;
+ if (idx >= 0 && idx < 10) {
+ _rebelValueCounters[idx]++;
+ _rebelLastCounter = _rebelValueCounters[idx];
+ debug("Rebel2 IACT Opcode2: Increment VAL counter[%d] -> %d (target=%d)", value, _rebelValueCounters[idx], targetId);
+ }
+ }
+
+ } else if (value > 0x3ff) { // Bitmask case: value > 0x3FF
+ for (int slot = 1; slot <= 9; ++slot) {
+ if ((value & (1 << (slot - 1))) != 0) {
+ if (!isBitSet(targetId)) {
+ _rebelMaskCounters[slot]++;
+ _rebelLastCounter = _rebelMaskCounters[slot];
+ debug("Rebel2 IACT Opcode2: Increment MASK counter[%d] -> %d (target=%d)", slot, _rebelMaskCounters[slot], targetId);
+ }
+ }
+ }
+ }
+ // Unknown sub-type: log and return
+ debug("Rebel2 IACT Opcode2: Unhandled par3=%d par4=%d", par3, par4);
+ }
+}
void InsaneRebel2::iactRebel2Opcode3(Common::SeekableReadStream &b, int16 par2, int16 par3, int16 par4) {
// Handle IACT opcode 3 subcases (damage, counters, special 100 branch)
// Mirrors retail FUN_0041CADB case 1 behavior where possible.
diff --git a/engines/scumm/insane/insane_rebel.h b/engines/scumm/insane/insane_rebel.h
index 4f8cb388a01..28fe8553bcc 100644
--- a/engines/scumm/insane/insane_rebel.h
+++ b/engines/scumm/insane/insane_rebel.h
@@ -56,6 +56,7 @@ public:
int16 par1, int16 par2, int16 par3, int16 par4);
// Handle IACT opcode 3 subcases (damage, hit-counters, special cases)
+ void iactRebel2Opcode2(Common::SeekableReadStream &b, int16 par2, int16 par3, int16 par4);
void iactRebel2Opcode3(Common::SeekableReadStream &b, int16 par2, int16 par3, int16 par4);
void procPostRendering(byte *renderBitmap, int32 codecparam, int32 setupsan12,
@@ -157,6 +158,11 @@ public:
int _rebelHitCooldown; // DAT_0045790a - cooldown / timing for damage checks
bool _rebelInvulnerable; // DAT_0047ab64 - toggles invulnerability / state
+ // Retail counters mirrored from DAT_00443618 (values 100..109) and DAT_004436e0 (mask counters 1..9)
+ short _rebelValueCounters[10]; // Index 0 -> value 100, ... Index 9 -> 109
+ short _rebelMaskCounters[10]; // Index 1..9 used; index 0 unused
+ int _rebelLastCounter; // Mirrors DAT_0047ab90 (last updated counter)
+
struct Shot {
bool active;
Commit: c1bc5f218a0d566f24c27069d7d66ef5e7cdacf0
https://github.com/scummvm/scummvm/commit/c1bc5f218a0d566f24c27069d7d66ef5e7cdacf0
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:08:50+02:00
Commit Message:
SCUMM: RA2: Move SMUSH hooks out of SmushPlayer
Changed paths:
engines/scumm/insane/insane_rebel.cpp
engines/scumm/smush/smush_player.cpp
diff --git a/engines/scumm/insane/insane_rebel.cpp b/engines/scumm/insane/insane_rebel.cpp
index ee0fa532732..49b700adc89 100644
--- a/engines/scumm/insane/insane_rebel.cpp
+++ b/engines/scumm/insane/insane_rebel.cpp
@@ -417,6 +417,46 @@ void InsaneRebel2::iactRebel2Scene1(byte *renderBitmap, int32 codecparam, int32
debug("Rebel2: Handler set to TURRET (0x26/38) - status bar sprite 5, crosshair sprites 48+");
}
+ // Detect embedded ANIM (SAN) within the remaining IACT payload and load into HUD slots.
+ // Centralized here so SmushPlayer no longer needs game-specific checks.
+ {
+ int64 startPos = b.pos();
+ int64 totalSize = b.size();
+ if (totalSize >= 0 && totalSize > startPos) {
+ int64 remaining = totalSize - startPos;
+ int scanSize = (int)MIN<int64>(remaining, 65536);
+ byte *scanBuf = (byte *)malloc(scanSize);
+ if (scanBuf) {
+ int bytesRead = b.read(scanBuf, scanSize);
+ for (int i = 0; i + 8 <= bytesRead; ++i) {
+ if (READ_BE_UINT32(scanBuf + i) == MKTAG('A','N','I','M')) {
+ int64 animStreamPos = startPos + i;
+ uint32 animReportedSize = READ_BE_UINT32(scanBuf + i + 4);
+ int32 toCopy = (int)MIN<int64>((int64)animReportedSize + 8, totalSize - animStreamPos);
+ if (toCopy > 0) {
+ byte *animData = (byte *)malloc(toCopy);
+ if (animData) {
+ b.seek(animStreamPos);
+ b.read(animData, toCopy);
+ // par4 is the userId passed by SmushPlayer
+ loadEmbeddedSan(par4, animData, toCopy, renderBitmap);
+ free(animData);
+ }
+ }
+ // Restore stream position and stop scanning after first ANIM
+ b.seek(startPos);
+ free(scanBuf);
+ goto after_anim_detection;
+ }
+ }
+ // No ANIM found: restore position
+ b.seek(startPos);
+ free(scanBuf);
+ }
+ }
+ }
+ after_anim_detection:;
+
} else if (par1 == 8) {
// TODO
} else if (par1 == 9) {
diff --git a/engines/scumm/smush/smush_player.cpp b/engines/scumm/smush/smush_player.cpp
index 3e1317c18bb..43fda940e68 100644
--- a/engines/scumm/smush/smush_player.cpp
+++ b/engines/scumm/smush/smush_player.cpp
@@ -366,51 +366,9 @@ void SmushPlayer::handleIACT(int32 subSize, Common::SeekableReadStream &b) {
debugC(DEBUG_SMUSH, "SmushPlayer::IACT()");
assert(subSize >= 8);
- // For Rebel2 large IACT chunks, check for embedded SAN animations
- if (_vm->_game.id == GID_REBEL2 && subSize > 1000) {
- int64 startPos = b.pos();
-
- // Read header to check for embedded ANIM
- byte header[64];
- int bytesRead = b.read(header, MIN<int32>(64, subSize));
-
- // Check for ANIM at offset 18 (embedded SAN location)
- uint32 magicAt18 = (bytesRead >= 22) ? READ_BE_UINT32(header + 18) : 0;
-
- if (magicAt18 == MKTAG('A','N','I','M')) {
- // This IACT contains an embedded SAN animation!
- uint32 animSize = (bytesRead >= 26) ? READ_BE_UINT32(header + 22) : 0;
-
- // Read the userId to determine which HUD slot (1-4)
- READ_LE_UINT16(header);
- READ_LE_UINT16(header + 2);
- int userId = READ_LE_UINT16(header + 6);
-
- debug("Rebel2: Extracting embedded SAN from IACT: userId=%d, animSize=%u", userId, animSize);
-
- // Extract the embedded ANIM data (starts at offset 18 from IACT start)
- b.seek(startPos + 18);
-
- int32 embeddedSize = subSize - 18;
- if (embeddedSize > 0 && _insane) {
- byte *animData = (byte *)malloc(embeddedSize);
- if (animData) {
- b.read(animData, embeddedSize);
-
- // Pass to Insane for decoding
- _insane->loadEmbeddedSan(userId, animData, embeddedSize, _dst);
-
- free(animData);
- }
- }
-
- // Return - don't process as audio
- return;
- }
-
- // Seek back to start for normal processing
- b.seek(startPos);
- }
+ // Embedded SAN detection moved to InsaneRebel2::iactRebel2Scene1
+ // (previously handled here in SmushPlayer; centralized to the engine)
+
int code = b.readUint16LE();
int flags = b.readUint16LE();
Commit: d78e69c2753cb53af186c9338e8f96b44176e055
https://github.com/scummvm/scummvm/commit/d78e69c2753cb53af186c9338e8f96b44176e055
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:08:50+02:00
Commit Message:
SCUMM: RA2: Refactor opcode 6 handling
Changed paths:
engines/scumm/insane/insane_rebel.cpp
engines/scumm/insane/insane_rebel.h
diff --git a/engines/scumm/insane/insane_rebel.cpp b/engines/scumm/insane/insane_rebel.cpp
index 49b700adc89..ee92909a873 100644
--- a/engines/scumm/insane/insane_rebel.cpp
+++ b/engines/scumm/insane/insane_rebel.cpp
@@ -87,6 +87,18 @@ InsaneRebel2::InsaneRebel2(ScummEngine_v7 *scumm) {
_rebelHitCooldown = 0;
_rebelInvulnerable = false;
+ // Opcode 6 state variables
+ _rebelAutopilot = 0;
+ _rebelDamageLevel = 0;
+ _rebelFlightDir = 0;
+ _rebelControlMode = 0;
+ _rebelViewOffsetX = 0;
+ _rebelViewOffsetY = 0;
+ _rebelViewOffset2X = 0;
+ _rebelViewOffset2Y = 0;
+ _rebelViewMode1 = 0;
+ _rebelViewMode2 = 0;
+
// Initialize mirrored retail counters
for (int i = 0; i < 10; ++i) {
_rebelValueCounters[i] = 0;
@@ -392,73 +404,11 @@ void InsaneRebel2::iactRebel2Scene1(byte *renderBitmap, int32 codecparam, int32
debug("Rebel2 IACT Opcode 5: par2=%d par3=%d par4=%d", par2, par3, par4);
} else if (par1 == 6) {
- // Opcode 6: Scene trigger / mode switch
- // Disassembly shows it sets DAT_0047ee84 (handler type) to par2 for values 7, 8, 0x19, 0x26
- // This determines which rendering handler and crosshair sprite to use
- // par3 is stored as DAT_004436de (level type) which affects HUD sprite selection
-
- // Update handler type if par2 is a known handler value
- if (par2 == 7 || par2 == 8 || par2 == 0x19 || par2 == 0x26) {
- _rebelHandler = par2;
- _rebelLevelType = par3; // Store level type (affects HUD sprite: 5 vs 53)
- debug("Rebel2 IACT Opcode 6: Setting handler=%d levelType=%d (par4=%d)", par2, par3, par4);
- }
-
- // Check for Status Bar Enable trigger (par4 == 1 means enable status bar drawing)
- // Based on FUN_407FCB line 79: if (param_5[4] == 1) { FUN_0040bb87(...) }
- // Note: Sprite 5 is a 136x13 status bar, NOT the full cockpit (which is baked into video)
- if (par4 == 1) {
- debug("Rebel2 IACT Opcode 6: Status Bar ENABLED - will draw sprite %d (136x13) from CPITIMAG.NUT",
- (_rebelLevelType == 5) ? 53 : 5);
- }
-
- // Debug: Confirm handler type after update
- if (_rebelHandler == 0x26) {
- debug("Rebel2: Handler set to TURRET (0x26/38) - status bar sprite 5, crosshair sprites 48+");
- }
-
- // Detect embedded ANIM (SAN) within the remaining IACT payload and load into HUD slots.
- // Centralized here so SmushPlayer no longer needs game-specific checks.
- {
- int64 startPos = b.pos();
- int64 totalSize = b.size();
- if (totalSize >= 0 && totalSize > startPos) {
- int64 remaining = totalSize - startPos;
- int scanSize = (int)MIN<int64>(remaining, 65536);
- byte *scanBuf = (byte *)malloc(scanSize);
- if (scanBuf) {
- int bytesRead = b.read(scanBuf, scanSize);
- for (int i = 0; i + 8 <= bytesRead; ++i) {
- if (READ_BE_UINT32(scanBuf + i) == MKTAG('A','N','I','M')) {
- int64 animStreamPos = startPos + i;
- uint32 animReportedSize = READ_BE_UINT32(scanBuf + i + 4);
- int32 toCopy = (int)MIN<int64>((int64)animReportedSize + 8, totalSize - animStreamPos);
- if (toCopy > 0) {
- byte *animData = (byte *)malloc(toCopy);
- if (animData) {
- b.seek(animStreamPos);
- b.read(animData, toCopy);
- // par4 is the userId passed by SmushPlayer
- loadEmbeddedSan(par4, animData, toCopy, renderBitmap);
- free(animData);
- }
- }
- // Restore stream position and stop scanning after first ANIM
- b.seek(startPos);
- free(scanBuf);
- goto after_anim_detection;
- }
- }
- // No ANIM found: restore position
- b.seek(startPos);
- free(scanBuf);
- }
- }
- }
- after_anim_detection:;
-
+ // Opcode 6: Level setup / mode switch (FUN_41CADB case 4)
+ iactRebel2Opcode6(renderBitmap, b, par2, par3, par4);
} else if (par1 == 8) {
- // TODO
+ // Opcode 8: HUD resource loading (FUN_41CADB case 6)
+ iactRebel2Opcode8(renderBitmap, b, par2, par3, par4);
} else if (par1 == 9) {
// Opcode 9: Text/subtitle display
debug("Rebel2 IACT Opcode 9: par2=%d par3=%d par4=%d (text)", par2, par3, par4);
@@ -613,6 +563,275 @@ void InsaneRebel2::iactRebel2Opcode3(Common::SeekableReadStream &b, int16 par2,
// other subcases not implemented yet
}
+void InsaneRebel2::iactRebel2Opcode6(byte *renderBitmap, Common::SeekableReadStream &b, int16 par2, int16 par3, int16 par4) {
+ // Opcode 6: Level setup / mode switch
+ // Based on FUN_41CADB case 4 (switch on *local_14 - 2 == 4, meaning opcode 6)
+ //
+ // Assembly behavior:
+ // 1. If par4 == 1: Draw status bar sprite 5, clear link tables, reset state
+ // 2. Set level type (DAT_00457900 = par3)
+ // 3. Handle autopilot/control mode logic
+ // 4. Update damage level counter
+ // 5. Calculate view offsets based on level type
+
+ debug("Rebel2 IACT Opcode 6: par2=%d par3=%d par4=%d", par2, par3, par4);
+
+ // Update handler type if par2 is a known handler value (from FUN_4033CF case 6)
+ if (par2 == 7 || par2 == 8 || par2 == 0x19 || par2 == 0x26) {
+ _rebelHandler = par2;
+ debug("Rebel2 Opcode 6: Setting handler=%d", par2);
+ }
+
+ // Step 1: If par4 == 1, initialize/reset state (lines 114-121)
+ if (par4 == 1) {
+ // Draw status bar sprite 5 (FUN_0040bb87 equivalent)
+ _rebelStatusBarSprite = (_rebelLevelType == 5) ? 53 : 5;
+ debug("Rebel2 Opcode 6: Status Bar ENABLED - sprite %d", _rebelStatusBarSprite);
+
+ // Clear link tables (DAT_0045797c through DAT_0045917c)
+ // These are 4 tables of 0x400 (1024) shorts each
+ for (int i = 0; i < 512; i++) {
+ _rebelLinks[i][0] = 0;
+ _rebelLinks[i][1] = 0;
+ _rebelLinks[i][2] = 0;
+ }
+
+ // DAT_0047ab98 = DAT_0047ab9c (reset state flags)
+ // We don't have a direct equivalent, but we can reset relevant counters
+ _rebelHitCounter = 0;
+ }
+
+ // Step 2: Set level type (DAT_00457900 = par3)
+ _rebelLevelType = par3;
+
+ // Step 3: Autopilot/control mode logic (lines 123-146)
+ // This determines whether the ship flies on autopilot or manual control
+ if (!_rebelInvulnerable) {
+ // Normal mode: check control mode flags
+ if (_rebelAutopilot == 0) {
+ if ((_rebelControlMode & 2) != 0) {
+ _rebelAutopilot = 1;
+ }
+ } else {
+ if (_rebelControlMode != 0) {
+ _rebelAutopilot = 0;
+ }
+ }
+ } else {
+ // Invulnerable mode: random autopilot changes
+ if (_rebelAutopilot == 0) {
+ if (_vm->_rnd.getRandomNumber(100) == 0) {
+ _rebelAutopilot = 1;
+ }
+ } else {
+ if (_vm->_rnd.getRandomNumber(15) == 0) {
+ _rebelAutopilot = 0;
+ _rebelFlightDir = _vm->_rnd.getRandomNumber(2);
+ }
+ }
+ }
+
+ // Step 4: Update damage level counter (lines 147-154)
+ if (_rebelAutopilot == 0) {
+ if (_rebelDamageLevel > 0) {
+ _rebelDamageLevel--;
+ }
+ } else {
+ if (_rebelDamageLevel < 5) {
+ _rebelDamageLevel++;
+ }
+ }
+
+ // Handle level type 3 special direction logic (lines 155-181)
+ if (_rebelLevelType == 3) {
+ if (_rebelDamageLevel == 5) {
+ // Check for joystick/key input to change direction
+ // Simplified: use mouse position
+ if (_vm->_mouse.x > 75) {
+ _rebelFlightDir = 1;
+ }
+ if (_vm->_mouse.x < -75) {
+ _rebelFlightDir = 0;
+ }
+ }
+ } else {
+ _rebelFlightDir = 0;
+ }
+
+ // Step 5: Calculate view offsets based on level type (lines 182-213)
+ switch (_rebelLevelType) {
+ case 1:
+ // Type 1: Vertical movement
+ _rebelViewMode1 = 0x0e;
+ _rebelViewMode2 = 0;
+ _rebelViewOffsetX = _rebelDamageLevel * -5 - 0x0e;
+ _rebelViewOffset2X = _rebelDamageLevel * -0x16;
+ _rebelViewOffsetY = 0;
+ _rebelViewOffset2Y = 0;
+ break;
+
+ case 4:
+ // Type 4: Different vertical movement
+ _rebelViewMode1 = 0x22;
+ _rebelViewMode2 = 0;
+ _rebelViewOffsetX = _rebelDamageLevel * 10 - 0x10;
+ _rebelViewOffset2X = _rebelDamageLevel * 0x11 - 0x55;
+ _rebelViewOffsetY = 0;
+ _rebelViewOffset2Y = 0;
+ break;
+
+ case 2:
+ // Type 2: Horizontal movement
+ _rebelViewMode1 = 0;
+ _rebelViewMode2 = 0x0e;
+ _rebelViewOffsetY = _rebelDamageLevel * -5 - 0x0e;
+ _rebelViewOffset2Y = (5 - _rebelDamageLevel) * 0x0f - 0x3c;
+ _rebelViewOffsetX = 0;
+ _rebelViewOffset2X = 0;
+ break;
+
+ case 3:
+ // Type 3: Direction-based movement
+ _rebelViewMode1 = 0x0f;
+ _rebelViewMode2 = 0;
+ {
+ int dirFactor = (_rebelFlightDir == 0) ? 3 : -3; // (-(ushort)(DAT_00457902 == 0) & 6) - 3
+ int dirFactor2 = (_rebelFlightDir == 0) ? 0x14 : -0x14; // (-(ushort)(DAT_00457902 == 0) & 0x28) - 0x14
+ _rebelViewOffsetX = dirFactor * (5 - _rebelDamageLevel) - 0x0f;
+ _rebelViewOffset2X = dirFactor2 * (5 - _rebelDamageLevel);
+ }
+ _rebelViewOffsetY = 0;
+ _rebelViewOffset2Y = 0;
+ break;
+
+ default:
+ // Default: No special offsets
+ _rebelViewMode1 = 0;
+ _rebelViewMode2 = 0;
+ _rebelViewOffsetX = 0;
+ _rebelViewOffsetY = 0;
+ _rebelViewOffset2X = 0;
+ _rebelViewOffset2Y = 0;
+ break;
+ }
+
+ debug("Rebel2 Opcode 6: levelType=%d autopilot=%d damageLevel=%d viewOffset=(%d,%d)",
+ _rebelLevelType, _rebelAutopilot, _rebelDamageLevel, _rebelViewOffsetX, _rebelViewOffsetY);
+
+ // Detect and load embedded ANIM (SAN) within the remaining IACT payload
+ {
+ int64 startPos = b.pos();
+ int64 totalSize = b.size();
+ if (totalSize >= 0 && totalSize > startPos) {
+ int64 remaining = totalSize - startPos;
+ int scanSize = (int)MIN<int64>(remaining, 65536);
+ byte *scanBuf = (byte *)malloc(scanSize);
+ if (scanBuf) {
+ int bytesRead = b.read(scanBuf, scanSize);
+ for (int i = 0; i + 8 <= bytesRead; ++i) {
+ if (READ_BE_UINT32(scanBuf + i) == MKTAG('A','N','I','M')) {
+ int64 animStreamPos = startPos + i;
+ uint32 animReportedSize = READ_BE_UINT32(scanBuf + i + 4);
+ int32 toCopy = (int)MIN<int64>((int64)animReportedSize + 8, totalSize - animStreamPos);
+ if (toCopy > 0) {
+ byte *animData = (byte *)malloc(toCopy);
+ if (animData) {
+ b.seek(animStreamPos);
+ b.read(animData, toCopy);
+ loadEmbeddedSan(par4, animData, toCopy, renderBitmap);
+ free(animData);
+ }
+ }
+ b.seek(startPos);
+ free(scanBuf);
+ return;
+ }
+ }
+ b.seek(startPos);
+ free(scanBuf);
+ }
+ }
+ }
+}
+
+void InsaneRebel2::iactRebel2Opcode8(byte *renderBitmap, Common::SeekableReadStream &b, int16 par2, int16 par3, int16 par4) {
+ // Opcode 8: HUD resource loading
+ // Based on FUN_41CADB case 6 (switch on *local_14 - 2 == 6, meaning opcode 8)
+ //
+ // par3 determines which HUD slot to load:
+ // case 1: DAT_00482240 - Primary HUD overlay (GRD001)
+ // case 2: DAT_00482238 - Secondary HUD graphics (GRD002)
+ // case 4: DAT_00482268 - Ship cockpit frame (GRD010)
+ // case 5: DAT_0048226c - Mask buffer (GRD011)
+ // case 6: DAT_00482250 - Explosion overlay (GRD003)
+ // case 7: DAT_00482248 - Damage indicator (GRD004)
+ // case 10: DAT_00482258 - Additional effects (GRD005)
+ // case 12/13: DAT_00482260 - High-res HUD alternative (GRD007)
+ // cases 21-27: Sound loading slot 0
+ // cases 31-37: Sound loading slot 1
+ // case 40: Sound loading slot 3
+ // cases 41-47: Sound loading slot 2
+
+ debug("Rebel2 IACT Opcode 8: par2=%d par3=%d par4=%d (HUD loading)", par2, par3, par4);
+
+ // Read the data size from the stream (at offset +14, which is local_14[7])
+ // The actual NUT/resource data starts at offset +18 (param_5 + 9 in assembly)
+ int64 startPos = b.pos();
+
+ // Skip to where embedded data would be and scan for ANIM tag
+ // The assembly shows data at param_5 + 9 (18 bytes from IACT header start)
+ // Since we're already past the header params, scan remaining data
+
+ int64 totalSize = b.size();
+ if (totalSize > startPos) {
+ int64 remaining = totalSize - startPos;
+ int scanSize = (int)MIN<int64>(remaining, 65536);
+ byte *scanBuf = (byte *)malloc(scanSize);
+ if (scanBuf) {
+ int bytesRead = b.read(scanBuf, scanSize);
+
+ // Look for ANIM tag (embedded SAN)
+ for (int i = 0; i + 8 <= bytesRead; ++i) {
+ if (READ_BE_UINT32(scanBuf + i) == MKTAG('A','N','I','M')) {
+ int64 animStreamPos = startPos + i;
+ uint32 animReportedSize = READ_BE_UINT32(scanBuf + i + 4);
+ int32 toCopy = (int)MIN<int64>((int64)animReportedSize + 8, totalSize - animStreamPos);
+ if (toCopy > 0) {
+ byte *animData = (byte *)malloc(toCopy);
+ if (animData) {
+ b.seek(animStreamPos);
+ b.read(animData, toCopy);
+ // Map par3 to userId for HUD slots
+ int userId = 0;
+ switch (par3) {
+ case 1: userId = 1; break; // Primary HUD
+ case 2: userId = 2; break; // Secondary HUD
+ case 4: userId = 3; break; // Cockpit frame
+ case 6: userId = 4; break; // Explosion overlay
+ default: userId = par3; break;
+ }
+ loadEmbeddedSan(userId, animData, toCopy, renderBitmap);
+ free(animData);
+ }
+ }
+ b.seek(startPos);
+ free(scanBuf);
+ return;
+ }
+ }
+
+ b.seek(startPos);
+ free(scanBuf);
+ }
+ }
+
+ // Handle sound loading cases (par3 21-47)
+ if (par3 >= 21 && par3 <= 47) {
+ debug("Rebel2 Opcode 8: Sound loading subcase %d (not implemented)", par3);
+ // TODO: Implement sound loading when needed
+ }
+}
+
void InsaneRebel2::enemyUpdate(byte *renderBitmap, Common::SeekableReadStream &b, int16 par2, int16 par3, int16 par4) {
// Opcode 4: Enemy position update
// Read 5 shorts from the stream (offset +8 through +16)
diff --git a/engines/scumm/insane/insane_rebel.h b/engines/scumm/insane/insane_rebel.h
index 28fe8553bcc..85c0cb24434 100644
--- a/engines/scumm/insane/insane_rebel.h
+++ b/engines/scumm/insane/insane_rebel.h
@@ -55,9 +55,11 @@ public:
int32 setupsan13, Common::SeekableReadStream &b, int32 size, int32 flags,
int16 par1, int16 par2, int16 par3, int16 par4);
- // Handle IACT opcode 3 subcases (damage, hit-counters, special cases)
+ // Handle IACT opcode subcases
void iactRebel2Opcode2(Common::SeekableReadStream &b, int16 par2, int16 par3, int16 par4);
void iactRebel2Opcode3(Common::SeekableReadStream &b, int16 par2, int16 par3, int16 par4);
+ void iactRebel2Opcode6(byte *renderBitmap, Common::SeekableReadStream &b, int16 par2, int16 par3, int16 par4);
+ void iactRebel2Opcode8(byte *renderBitmap, Common::SeekableReadStream &b, int16 par2, int16 par3, int16 par4);
void procPostRendering(byte *renderBitmap, int32 codecparam, int32 setupsan12,
int32 setupsan13, int32 curFrame, int32 maxFrame) override;
@@ -158,6 +160,20 @@ public:
int _rebelHitCooldown; // DAT_0045790a - cooldown / timing for damage checks
bool _rebelInvulnerable; // DAT_0047ab64 - toggles invulnerability / state
+ // Opcode 6 state variables (from FUN_41CADB case 4)
+ int _rebelAutopilot; // DAT_00457904 - autopilot flag (0 or 1)
+ int _rebelDamageLevel; // DAT_0045790a - damage level (0-5)
+ int _rebelFlightDir; // DAT_00457902 - flight direction (0 or 1)
+ int _rebelControlMode; // DAT_0047a7e4 - control mode flags
+
+ // View offset variables (calculated from level type)
+ int _rebelViewOffsetX; // DAT_0045790c
+ int _rebelViewOffsetY; // DAT_0045790e
+ int _rebelViewOffset2X; // DAT_00457910
+ int _rebelViewOffset2Y; // DAT_00457912
+ int _rebelViewMode1; // DAT_00482270
+ int _rebelViewMode2; // DAT_00482274
+
// Retail counters mirrored from DAT_00443618 (values 100..109) and DAT_004436e0 (mask counters 1..9)
short _rebelValueCounters[10]; // Index 0 -> value 100, ... Index 9 -> 109
short _rebelMaskCounters[10]; // Index 1..9 used; index 0 unused
Commit: 404037ab75a10ab1b59dcae9c5314b20857cacb4
https://github.com/scummvm/scummvm/commit/404037ab75a10ab1b59dcae9c5314b20857cacb4
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:08:50+02:00
Commit Message:
SCUMM: RA2: Fix post-rendering state updates
Changed paths:
engines/scumm/insane/insane_rebel.cpp
diff --git a/engines/scumm/insane/insane_rebel.cpp b/engines/scumm/insane/insane_rebel.cpp
index ee92909a873..c5ff122c948 100644
--- a/engines/scumm/insane/insane_rebel.cpp
+++ b/engines/scumm/insane/insane_rebel.cpp
@@ -1617,28 +1617,30 @@ void InsaneRebel2::procPostRendering(byte *renderBitmap, int32 codecparam, int32
Common::List<enemy>::iterator it;
for (it = _enemies.begin(); it != _enemies.end(); ++it) {
+ // Skip destroyed enemies (explosion handled by 5-slot system)
+ if (it->destroyed) continue;
+
+ // Skip inactive enemies or those disabled by IACT bit
+ if (!it->active || isBitSet(it->id)) continue;
+
Common::Rect r = it->rect;
-
- // Clip the rect to screen bounds for safety
- Common::Rect clipped = r;
- clipped.clip(Common::Rect(0, 0, width, height));
- if (!clipped.isValidRect()) continue;
-
- if (it->destroyed) {
- // Handle destroyed enemies - draw explosion animation
- // The enemy frame updates are now skipped in decodeFrameObject via shouldSkipFrameUpdate
- // Note: Explosion rendering is now handled by the separate 5-slot system
- } else if (it->active && !isBitSet(it->id)) { // Only draw if active AND not disabled by IACT
- // Draw Green Indicators (Corner Brackets) for Easy (0) and Medium (1) difficulty
- // Hard (2) mode does not show indicators.
- if (_difficulty < 2) {
- const byte color = 5; // Green color index for brackets
- // Clip the rect to screen bounds for drawing logic is handled inside drawLine if implemented safely,
- // but drawCornerBrackets relies on drawLine which iterates.
- // We pass full screen width/height to safe-guard.
- drawCornerBrackets(renderBitmap, pitch, width, height, r.left, r.top, r.width(), r.height(), color);
- }
+ // Check if enemy rect is visible within the current view area
+ // (matching FUN_00428b90 clip logic: reject lines entirely outside clip rect)
+ // The visible area in video coordinates is the current scroll viewport
+ Common::Rect viewRect(_viewX, _viewY, _viewX + videoWidth, _viewY + videoHeight);
+
+ // If enemy rect doesn't intersect the view area at all, skip drawing
+ if (r.right <= viewRect.left || r.left >= viewRect.right ||
+ r.bottom <= viewRect.top || r.top >= viewRect.bottom) {
+ continue;
+ }
+
+ // Draw Green Indicators (Corner Brackets) for Easy (0) and Medium (1) difficulty
+ // Hard (2) mode does not show indicators (matching FUN_00425d30(4) check)
+ if (_difficulty < 2) {
+ const byte color = 5; // Green color index for brackets
+ drawCornerBrackets(renderBitmap, pitch, width, height, r.left, r.top, r.width(), r.height(), color);
}
}
@@ -1776,6 +1778,22 @@ void InsaneRebel2::procPostRendering(byte *renderBitmap, int32 codecparam, int32
renderNutSprite(renderBitmap, pitch, width, height, _vm->_mouse.x - cw / 2 + _viewX, _vm->_mouse.y - ch / 2 + _viewY, _smush_iconsNut, reticleIndex);
}
}
+
+ // ============================================================
+ // FRAME END RESET: Clear active flags for all enemies (FUN_403240 equivalent)
+ // ============================================================
+ // The original game rebuilds the object list from scratch each frame:
+ // - FUN_4033CF (IACT processing) adds objects to DAT_0043fb00 list
+ // - FUN_4092D9 iterates only over objects in the current frame's list
+ // - FUN_403240 (called at frame end) resets DAT_0047ee74 = 0 (clears list counter)
+ //
+ // We achieve the same by marking all non-destroyed enemies inactive at frame end.
+ // The next frame's IACT opcode 4 (enemyUpdate) will re-activate them if present.
+ for (Common::List<enemy>::iterator it = _enemies.begin(); it != _enemies.end(); ++it) {
+ if (!it->destroyed) {
+ it->active = false;
+ }
+ }
}
}
Commit: ea84ceebdf96faf93e3c3c22355d199ad63ab5a9
https://github.com/scummvm/scummvm/commit/ea84ceebdf96faf93e3c3c22355d199ad63ab5a9
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:08:51+02:00
Commit Message:
SCUMM: RA2: Skip non-gameplay sounds
Changed paths:
engines/scumm/smush/smush_player.cpp
diff --git a/engines/scumm/smush/smush_player.cpp b/engines/scumm/smush/smush_player.cpp
index 43fda940e68..ccea514286f 100644
--- a/engines/scumm/smush/smush_player.cpp
+++ b/engines/scumm/smush/smush_player.cpp
@@ -914,6 +914,13 @@ void SmushPlayer::handleFrame(int32 frameSize, Common::SeekableReadStream &b) {
handleZlibFrameObject(subSize, b);
break;
case MKTAG('P','S','A','D'):
+ // Rebel Assault 2 only: Skip sound when _skipNext is set (enemy killed)
+ // This mirrors the original game's FUN_00423A50 behavior where
+ // SKIP tags cause subsequent PSAD/SAUD chunks to be skipped
+ if (_vm->_game.id == GID_REBEL2 && _skipNext) {
+ _skipNext = false;
+ break;
+ }
if (!_compressedFileMode) {
audioChunk = (uint8 *)malloc(subSize + 8);
b.seek(-8, SEEK_CUR);
Commit: 353416241d031ee59cb984bd6e9fd124751fca2c
https://github.com/scummvm/scummvm/commit/353416241d031ee59cb984bd6e9fd124751fca2c
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:08:51+02:00
Commit Message:
SCUMM: RA2: Implement score and mouse pointers
Changed paths:
engines/scumm/insane/insane_rebel.cpp
engines/scumm/insane/insane_rebel.h
diff --git a/engines/scumm/insane/insane_rebel.cpp b/engines/scumm/insane/insane_rebel.cpp
index c5ff122c948..bdcd272180e 100644
--- a/engines/scumm/insane/insane_rebel.cpp
+++ b/engines/scumm/insane/insane_rebel.cpp
@@ -68,6 +68,10 @@ InsaneRebel2::InsaneRebel2(ScummEngine_v7 *scumm) {
_smush_icons2Nut = nullptr; // Not used for Rebel2
_smush_cockpitNut = new NutRenderer(_vm, "SYSTM/DISPFONT.NUT");
+ // Load SMALFONT.NUT for HUD score/lives rendering (DAT_00482200 equivalent)
+ // This is used by FUN_0041c012 to render the score in the status bar
+ _smush_dispfontNut = new NutRenderer(_vm, "SYSTM/SMALFONT.NUT");
+
// Load DIHIFONT.NUT for in-video messages/subtitles (Opcode 9)
_rebelMsgFont = new SmushFont(_vm, "SYSTM/DIHIFONT.NUT", true);
@@ -107,6 +111,7 @@ InsaneRebel2::InsaneRebel2(ScummEngine_v7 *scumm) {
_rebelLastCounter = 0;
_difficulty = 1; // Default to Medium (1). TODO: Read from game config
+ _targetLockTimer = 0; // DAT_00443676 equivalent
_speed = 12;
_insaneIsRunning = false;
@@ -215,8 +220,137 @@ InsaneRebel2::InsaneRebel2(ScummEngine_v7 *scumm) {
InsaneRebel2::~InsaneRebel2() {
delete _rebelMsgFont;
+ delete _smush_dispfontNut;
+}
+
+// Score lookup tables (from DAT_0047e0fe, DAT_0047e100, DAT_0047e102)
+// These are indexed by: DAT_0047a7fa * 0x242 + DAT_0047a7f8 * 0x22
+// For simplicity, we use fixed values based on difficulty level
+// Values estimated from disassembly patterns (actual values would need extraction from game data)
+const int16 InsaneRebel2::kScoreTableEnemyDestroy[16] = {
+ 100, 100, 100, 100, // Easy (difficulty 0)
+ 150, 150, 150, 150, // Medium (difficulty 1)
+ 200, 200, 200, 200, // Hard (difficulty 2)
+ 250, 250, 250, 250 // Expert (difficulty 3)
+};
+
+const int16 InsaneRebel2::kScoreTableSpecial[16] = {
+ 50, 50, 50, 50,
+ 75, 75, 75, 75,
+ 100, 100, 100, 100,
+ 125, 125, 125, 125
+};
+
+const int16 InsaneRebel2::kScoreTableTimeBonus[16] = {
+ 1, 1, 1, 1,
+ 2, 2, 2, 2,
+ 3, 3, 3, 3,
+ 4, 4, 4, 4
+};
+
+// Score system implementation (FUN_0041bf8d equivalent)
+// Adds points to score and awards bonus life when crossing threshold
+void InsaneRebel2::addScore(int points) {
+ // Calculate bonus life threshold based on difficulty (DAT_0047a7fa)
+ // From FUN_0041bf8d:
+ // if (difficulty < 4) threshold = (difficulty * 5 + 5) * 1000
+ // else threshold = 15000
+ int threshold;
+ if (_difficulty < 4) {
+ threshold = (_difficulty * 5 + 5) * 1000; // 5000, 10000, 15000, 20000
+ } else {
+ threshold = 15000;
+ }
+
+ // Check if we're crossing a threshold (award bonus life)
+ // Formula: score / threshold < (score + points) / threshold
+ if (threshold > 0) {
+ int oldMilestone = _playerScore / threshold;
+ int newMilestone = (_playerScore + points) / threshold;
+ if (oldMilestone < newMilestone) {
+ // Award bonus life
+ _playerLives++;
+ // TODO: Play bonus life sound (FUN_0041189e(5, 0, 0x7f, 0, 0))
+ debug("Rebel2: BONUS LIFE! Score crossed %d threshold. Lives=%d", threshold, _playerLives);
+ }
+ }
+
+ // Add points to score
+ _playerScore += points;
+ debug("Rebel2: Score +%d = %d", points, _playerScore);
}
+// Render score to HUD (part of FUN_0041c012)
+// Score is drawn using FUN_00434cb0 with format string "%07ld"
+// In retail, score is rendered to a status bar buffer, then blitted to screen at Y=180 (0xb4)
+// The text within the status bar is at local Y=4, so screen Y = 180 + 4 = 184
+void InsaneRebel2::renderScoreHUD(byte *renderBitmap, int pitch, int width, int height, int statusBarY) {
+ // In retail, score is rendered by FUN_0041c012 which calls FUN_00434cb0
+ // The status bar is blitted to screen at Y = DAT_0047ab2c + 0xb4 (typically 0 + 180 = 180)
+ // Text position within status bar from FUN_0041c012 line 136-137:
+ // X = ((DAT_0047a808 < 2) - 1 & 0x101) + 0x101 = 0x101 (257) for low-res
+ // Y = ((DAT_0047a808 < 2) - 1 & 4) + 4 = 4 for low-res
+ // So final screen position: X=257, Y=180+4=184
+ // Format: 7-digit zero-padded decimal ("%07ld")
+
+ (void)statusBarY; // Not used - we use fixed Y positions
+
+ // Use SMALFONT.NUT (NutRenderer) for rendering digits
+ // If not available, skip rendering
+ if (!_smush_dispfontNut) {
+ debug(1, "renderScoreHUD: _smush_dispfontNut is NULL!");
+ return;
+ }
+
+ // The SMUSH buffer is 424x260, but the visible screen is 320x200
+ // The view offset (_viewX, _viewY) determines where in the buffer the screen is showing
+ // To render at fixed screen positions, we add the view offset
+
+ // Convert score to 7-digit string
+ char scoreStr[16];
+ Common::sprintf_s(scoreStr, "%07d", _playerScore);
+
+ // Status bar is at Y=180 (0xb4), text within it at Y=4, so total Y=184
+ // Score X position is 257 (0x101)
+ const int STATUS_BAR_Y = 180; // 0xb4 from FUN_41C012 line 149/152
+ const int SCORE_TEXT_Y = 4; // Text position within status bar
+
+ int scoreX = 257 + _viewX;
+ int scoreY = STATUS_BAR_Y + SCORE_TEXT_Y + _viewY;
+
+ debug(5, "renderScoreHUD: Drawing score=%d at buffer(%d,%d) viewOffset(%d,%d)",
+ _playerScore, scoreX, scoreY, _viewX, _viewY);
+
+ // Draw each character manually using NutRenderer::drawCharV7
+ Common::Rect clipRect(0, 0, width, height);
+ int x = scoreX;
+ for (int i = 0; scoreStr[i] != '\0'; i++) {
+ byte ch = (byte)scoreStr[i];
+ int charWidth = _smush_dispfontNut->getCharWidth(ch);
+ if (charWidth > 0) {
+ // Use drawCharV7 with color 255 (white) for visibility
+ _smush_dispfontNut->drawCharV7(renderBitmap, clipRect, x, scoreY, pitch, 255, kStyleAlignLeft, ch, true, true);
+ x += charWidth;
+ }
+ }
+
+ // Also draw lives counter - in status bar at X=168 (0xa8), Y=7 within bar
+ const int LIVES_TEXT_Y = 7;
+ char livesStr[8];
+ Common::sprintf_s(livesStr, "%d", _playerLives);
+ int livesX = 168 + _viewX;
+ int livesY = STATUS_BAR_Y + LIVES_TEXT_Y + _viewY;
+
+ x = livesX;
+ for (int i = 0; livesStr[i] != '\0'; i++) {
+ byte ch = (byte)livesStr[i];
+ int charWidth = _smush_dispfontNut->getCharWidth(ch);
+ if (charWidth > 0) {
+ _smush_dispfontNut->drawCharV7(renderBitmap, clipRect, x, livesY, pitch, 255, kStyleAlignLeft, ch, true, true);
+ x += charWidth;
+ }
+ }
+}
int32 InsaneRebel2::processMouse() {
int32 buttons = 0;
@@ -285,7 +419,14 @@ int32 InsaneRebel2::processMouse() {
// Note: Background saving and masking is handled in procPostRendering
// where we have access to the render bitmap
// TODO: Play explosion sound
- // TODO: Update score
+
+ // Award score for destroying enemy (FUN_0041bf8d called from FUN_40A2E0)
+ // Score value comes from lookup table DAT_0047e0fe indexed by difficulty
+ int scoreIndex = _difficulty * 4; // Simplified index
+ if (scoreIndex >= 0 && scoreIndex < 16) {
+ addScore(kScoreTableEnemyDestroy[scoreIndex]);
+ }
+
// Only hit one enemy per click
break;
}
@@ -1749,28 +1890,83 @@ void InsaneRebel2::procPostRendering(byte *renderBitmap, int32 codecparam, int32
}
}
+ // ============================================================
+ // TARGET LOCK DETECTION (DAT_00443676 equivalent)
+ // ============================================================
+ // In retail: When crosshair is over an active enemy, set target lock timer to 7
+ // This triggers the animated crosshair cycling for Handler 0x26 (turret)
+ // Check from FUN_40A2E0 lines 127-143
+ {
+ Common::Point worldMousePos(_vm->_mouse.x + _viewX, _vm->_mouse.y + _viewY);
+ bool targetLocked = false;
+
+ for (Common::List<enemy>::iterator enemyIt = _enemies.begin(); enemyIt != _enemies.end(); ++enemyIt) {
+ if (enemyIt->active && !enemyIt->destroyed && enemyIt->rect.contains(worldMousePos)) {
+ targetLocked = true;
+ break;
+ }
+ }
+
+ // Update target lock timer
+ if (targetLocked) {
+ _targetLockTimer = 7; // Set to 7 when over target (retail behavior)
+ } else if (_targetLockTimer > 0) {
+ _targetLockTimer--; // Count down when not over target
+ }
+ }
+
// Draw Crosshair/Reticle cursor
- // Sprite indices based on handler type (from original game disassembly):
+ // Sprite indices based on handler type (from original game disassembly FUN_004089ab, FUN_0040d836, etc):
// - Handler 8 (ground vehicle): Index 0x2E (46)
// - Handler 7 (space flight): Index 0x2F (47)
// - Handler 0x19 (mixed/turret view): Index 0x2F (47)
- // - Handler 0x26 (full turret): Index 0x30+ (48+) with animation
+ // - Handler 0x26 (full turret): Index varies by levelType and animation
+ //
+ // For Handler 0x26 (turret), the crosshair sprite formula from FUN_004089ab line 192-193:
+ // (-(ushort)(DAT_004436de == 5) & 0x30) + local_28
+ // where local_28 is an animation offset (0-3) based on target lock timer (DAT_00443676)
+ //
+ // When DAT_00443676 == 0 (no target locked): local_28 = 0
+ // When DAT_00443676 != 0: local_28 = 3 - (DAT_0047fe98 & 3) -- animated cycling
if (_smush_iconsNut) {
- //CursorMan.showMouse(false);
int reticleIndex;
switch (_rebelHandler) {
- case 7: // Space flight
- case 0x19: // Mixed/turret view
- reticleIndex = 47; // 0x2F
+ case 7: // Space flight - uses crosshair sprite 0x2F (47)
+ reticleIndex = 47;
break;
- case 0x26: // Full turret (with animation - simplified for now)
- reticleIndex = 48; // 0x30
+ case 0x19: // Mixed/turret view - uses crosshair sprite 0x2F (47)
+ reticleIndex = 47;
break;
- case 8: // Ground vehicle (Level 1) - FALLTHROUGH
+ case 0x26: { // Full turret mode - animated crosshair
+ // Calculate animation offset based on target lock state
+ // In retail: DAT_00443676 is the "target lock timer" (set to 7 when targeting)
+ // DAT_0047fe98 is a per-frame counter incremented each frame
+ static int turretAnimCounter = 0;
+ turretAnimCounter++;
+
+ int animOffset;
+ // Use actual target lock timer (DAT_00443676 equivalent)
+ if (_targetLockTimer == 0) {
+ animOffset = 0; // No target - use static crosshair
+ } else {
+ animOffset = 3 - (turretAnimCounter & 3); // Cycles 3, 2, 1, 0
+ }
+
+ // If levelType == 5, use sprites 0x30-0x33 (48-51)
+ // Otherwise use sprites 0-3 (basic targeting reticle)
+ if (_rebelLevelType == 5) {
+ reticleIndex = 0x30 + animOffset; // 48-51
+ } else {
+ reticleIndex = animOffset; // 0-3
+ }
+ break;
+ }
+ case 8: // Ground vehicle - uses crosshair sprite 0x2E (46)
default:
- reticleIndex = 46; // 0x2E
+ reticleIndex = 46;
break;
}
+
if (_smush_iconsNut->getNumChars() > reticleIndex) {
int cw = _smush_iconsNut->getCharWidth(reticleIndex);
int ch = _smush_iconsNut->getCharHeight(reticleIndex);
@@ -1779,6 +1975,13 @@ void InsaneRebel2::procPostRendering(byte *renderBitmap, int32 codecparam, int32
}
}
+ // ============================================================
+ // HUD SCORE/LIVES RENDERING (FUN_0041c012 equivalent)
+ // ============================================================
+ // Render score and lives counter on the status bar
+ // This is called after all other rendering so it appears on top
+ renderScoreHUD(renderBitmap, pitch, width, height, 0);
+
// ============================================================
// FRAME END RESET: Clear active flags for all enemies (FUN_403240 equivalent)
// ============================================================
@@ -1789,9 +1992,9 @@ void InsaneRebel2::procPostRendering(byte *renderBitmap, int32 codecparam, int32
//
// We achieve the same by marking all non-destroyed enemies inactive at frame end.
// The next frame's IACT opcode 4 (enemyUpdate) will re-activate them if present.
- for (Common::List<enemy>::iterator it = _enemies.begin(); it != _enemies.end(); ++it) {
- if (!it->destroyed) {
- it->active = false;
+ for (Common::List<enemy>::iterator resetIt = _enemies.begin(); resetIt != _enemies.end(); ++resetIt) {
+ if (!resetIt->destroyed) {
+ resetIt->active = false;
}
}
}
diff --git a/engines/scumm/insane/insane_rebel.h b/engines/scumm/insane/insane_rebel.h
index 85c0cb24434..35e33260de1 100644
--- a/engines/scumm/insane/insane_rebel.h
+++ b/engines/scumm/insane/insane_rebel.h
@@ -40,7 +40,10 @@ public:
~InsaneRebel2();
NutRenderer *_smush_cockpitNut;
- NutRenderer *_smush_dispfontNut; // DAT_00482200 - DISPFONT.NUT for status bar (difficulty, shields, lives, score)
+
+ // Font used for HUD score/lives/damage display (SMALFONT.NUT)
+ // DAT_00482200 equivalent - used by FUN_0041c012 for status bar rendering
+ NutRenderer *_smush_dispfontNut;
// Font used for opcode 9 text/subtitle rendering (DIHIFONT / TALKFONT)
SmushFont *_rebelMsgFont;
@@ -192,6 +195,24 @@ public:
int _difficulty;
void drawCornerBrackets(byte *dst, int pitch, int width, int height, int x, int y, int w, int h, byte color);
+ // Score system (FUN_0041bf8d equivalent)
+ // Adds points to score and awards bonus life when crossing threshold
+ void addScore(int points);
+
+ // Score lookup tables (indices into per-level point values)
+ // DAT_0047e0fe: Points for destroying enemies
+ // DAT_0047e100: Points for certain special events
+ // DAT_0047e102: Points awarded per frame (time bonus)
+ static const int16 kScoreTableEnemyDestroy[16]; // Per difficulty/level
+ static const int16 kScoreTableSpecial[16];
+ static const int16 kScoreTableTimeBonus[16];
+
+ // Render score text to HUD (called from procPostRendering)
+ void renderScoreHUD(byte *renderBitmap, int pitch, int width, int height, int statusBarY);
+
+ // Target lock timer (DAT_00443676) - set to 7 when crosshair is over enemy
+ int _targetLockTimer;
+
};
} // End of namespace Insane
Commit: deb62f97e1e5fc5def6980ef1b7095b7a4e6e3dd
https://github.com/scummvm/scummvm/commit/deb62f97e1e5fc5def6980ef1b7095b7a4e6e3dd
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:08:51+02:00
Commit Message:
SCUMM: RA2: Implement subtitles
Changed paths:
engines/scumm/insane/insane_rebel.cpp
engines/scumm/insane/insane_rebel.h
engines/scumm/scumm.cpp
engines/scumm/smush/smush_player.cpp
diff --git a/engines/scumm/insane/insane_rebel.cpp b/engines/scumm/insane/insane_rebel.cpp
index bdcd272180e..38190aafb90 100644
--- a/engines/scumm/insane/insane_rebel.cpp
+++ b/engines/scumm/insane/insane_rebel.cpp
@@ -78,6 +78,7 @@ InsaneRebel2::InsaneRebel2(ScummEngine_v7 *scumm) {
_enemies.clear();
_rebelHandler = 8; // Default to Handler 8 (ground vehicle) for Level 1
_rebelLevelType = 0; // Level type from Opcode 6 par3, determines HUD sprite variant
+ _introCursorPushed = false; // Cursor state tracking for intro sequences
_playerDamage = 0;
_playerShield = 255; // Full shields by default (255)
@@ -552,8 +553,7 @@ void InsaneRebel2::iactRebel2Scene1(byte *renderBitmap, int32 codecparam, int32
iactRebel2Opcode8(renderBitmap, b, par2, par3, par4);
} else if (par1 == 9) {
// Opcode 9: Text/subtitle display
- debug("Rebel2 IACT Opcode 9: par2=%d par3=%d par4=%d (text)", par2, par3, par4);
-
+ iactRebel2Opcode9(renderBitmap, b, par2, par3, par4);
} else if (par1 == 0 || par1 == 1) {
// Low Opcodes seen in logs
debug("Rebel2 IACT: Low Opcode %d (par2=%d par3=%d par4=%d)", par1, par2, par3, par4);
@@ -973,6 +973,162 @@ void InsaneRebel2::iactRebel2Opcode8(byte *renderBitmap, Common::SeekableReadStr
}
}
+void InsaneRebel2::iactRebel2Opcode9(byte *renderBitmap, Common::SeekableReadStream &b, int16 par2, int16 par3, int16 par4) {
+ // Opcode 9: Text/Subtitle Display via IACT chunk
+ // Note: Most RA2 subtitles use TRES chunks handled by SmushPlayer::handleTextResource()
+ // This opcode handles inline text in IACT chunks (less common)
+ //
+ // IACT Chunk Layout (par1-par4 already read by handleIACT):
+ // +0x00 (2): opcode = 9 (par1, already read)
+ // +0x02 (2): par2 (already read)
+ // +0x04 (2): par3 (already read)
+ // +0x06 (2): par4 (already read)
+ // +0x08 onwards: Text data structure
+ //
+ // Text Data Structure:
+ // +0x00 (2): X position
+ // +0x02 (2): Y position
+ // +0x04 (2): flags (bit 0=center, bit 1=right, bit 2=wrap, bit 3=difficulty gated)
+ // +0x06 (2): clipX (when flag & 4)
+ // +0x08 (2): clipY
+ // +0x0A (2): clipW
+ // +0x0C (2): clipH
+ // +0x10 onwards: NUL-terminated text string
+
+ int64 startPos = b.pos();
+
+ // Check for "TRES" tag (0x54524553) indicating string resource lookup
+ uint32 tag = b.readUint32BE();
+
+ const char *textStr = nullptr;
+ char textBuffer[512];
+ int16 posX = 160; // Default center position
+ int16 posY = 150; // Default bottom-ish position
+ int16 textFlags = 1; // Default: center aligned
+ int16 clipX = 16, clipY = 16, clipW = 288, clipH = 168;
+
+ if (tag == MKTAG('T','R','E','S')) {
+ // String resource lookup via TRES tag
+ // The string index follows after the tag
+ int32 stringIndex = b.readSint32LE();
+
+ // Try to get string from SMUSH player's string resource
+ if (_player && _player->getString(stringIndex)) {
+ textStr = _player->getString(stringIndex);
+ debug("Rebel2 Opcode 9: TRES string index=%d -> \"%s\"", stringIndex, textStr);
+ } else {
+ debug("Rebel2 Opcode 9: TRES string index=%d not found", stringIndex);
+ return;
+ }
+
+ // After TRES + index, read positioning data
+ // The remaining data contains X, Y, flags etc.
+ if (b.size() - b.pos() >= 14) {
+ posX = b.readSint16LE();
+ posY = b.readSint16LE();
+ textFlags = b.readSint16LE();
+ clipX = b.readSint16LE();
+ clipY = b.readSint16LE();
+ clipW = b.readSint16LE();
+ clipH = b.readSint16LE();
+ }
+ } else {
+ // Inline text data - go back and read positioning structure
+ b.seek(startPos);
+
+ // Read text data structure
+ posX = b.readSint16LE(); // +0x00
+ posY = b.readSint16LE(); // +0x02
+ textFlags = b.readSint16LE(); // +0x04
+ clipX = b.readSint16LE(); // +0x06
+ clipY = b.readSint16LE(); // +0x08
+ clipW = b.readSint16LE(); // +0x0A
+ clipH = b.readSint16LE(); // +0x0C
+ b.skip(2); // +0x0E padding
+
+ // Read inline text string (NUL-terminated)
+ int textLen = 0;
+ while (textLen < (int)sizeof(textBuffer) - 1) {
+ byte ch = b.readByte();
+ if (ch == 0 || b.eos()) break;
+ textBuffer[textLen++] = ch;
+ }
+ textBuffer[textLen] = '\0';
+ textStr = textBuffer;
+
+ debug("Rebel2 Opcode 9: Inline text at (%d,%d) flags=0x%x -> \"%s\"", posX, posY, textFlags, textStr);
+ }
+
+ if (!textStr || textStr[0] == '\0') {
+ debug("Rebel2 Opcode 9: Empty text string, skipping");
+ return;
+ }
+
+ // Check difficulty gate (flag bit 3 = 0x08)
+ // If set, only show text if difficulty check passes (we skip this check for simplicity)
+ // In retail: FUN_00425d30(0) is called
+
+ // Get render buffer dimensions
+ int width = (_player && _player->_width > 0) ? _player->_width : 320;
+ int height = (_player && _player->_height > 0) ? _player->_height : 200;
+
+ // Apply coordinate clamping (from FUN_004033cf disassembly)
+ // Low-res: X clamped to [16, 304], Y clamped to [16, 196]
+ if (posX < 16) posX = 16;
+ if (posX > 304) posX = 304;
+ if (posY < 16) posY = 16;
+ if (posY > 196) posY = 196;
+
+ // Use the message font loaded during initialization (DIHIFONT.NUT)
+ if (!_rebelMsgFont) {
+ debug("Rebel2 Opcode 9: No message font loaded (_rebelMsgFont is null)");
+ return;
+ }
+
+ // Calculate clipping rectangle
+ if (!(textFlags & 0x04)) {
+ // No clip rect specified, use default full-screen clip
+ clipX = 0;
+ clipY = 0;
+ clipW = width;
+ clipH = height;
+ }
+
+ Common::Rect clipRect(
+ MAX<int>(0, clipX),
+ MAX<int>(0, clipY),
+ MIN<int>(clipX + clipW, width),
+ MIN<int>(clipY + clipH, height)
+ );
+
+ // Determine text alignment flags
+ TextStyleFlags styleFlags = kStyleAlignLeft;
+ if (textFlags & 0x01) {
+ styleFlags = kStyleAlignCenter;
+ } else if (textFlags & 0x02) {
+ styleFlags = kStyleAlignRight;
+ }
+ if (textFlags & 0x04) {
+ styleFlags = (TextStyleFlags)(styleFlags | kStyleWordWrap);
+ }
+
+ // Use white color (index 255) for subtitle text
+ // The original uses colors from the palette, commonly white or yellow for subtitles
+ int16 textColor = 255;
+
+ // Draw the text string
+ if (textFlags & 0x04) {
+ // Word-wrapped text
+ _rebelMsgFont->drawStringWrap(textStr, renderBitmap, clipRect, posX, posY, textColor, styleFlags);
+ } else {
+ // Single-line text
+ _rebelMsgFont->drawString(textStr, renderBitmap, clipRect, posX, posY, textColor, styleFlags);
+ }
+
+ debug("Rebel2 Opcode 9: Rendered subtitle at (%d,%d) flags=0x%x clip=(%d,%d,%d,%d)",
+ posX, posY, textFlags, clipX, clipY, clipW, clipH);
+}
+
void InsaneRebel2::enemyUpdate(byte *renderBitmap, Common::SeekableReadStream &b, int16 par2, int16 par3, int16 par4) {
// Opcode 4: Enemy position update
// Read 5 shorts from the stream (offset +8 through +16)
@@ -1582,7 +1738,31 @@ void InsaneRebel2::procPostRendering(byte *renderBitmap, int32 codecparam, int32
const int statusBarY = 180; // 0xb4 - status bar starts at Y=180 in video coords
// Hide HUD/status bar during intro videos (marked by SmushPlayer video flag 0x20)
+ // The 0x20 flag indicates a non-interactive cutscene/intro sequence
bool introPlaying = ((_player->_curVideoFlags & 0x20) != 0);
+
+ // During intro sequences:
+ // - Hide the crosshair/cursor (handled by not drawing it)
+ // - Skip all HUD/status bar rendering
+ // - Skip mouse input processing
+ // The mouse cursor is already hidden by SmushPlayer during video playback
+ if (introPlaying) {
+ // Track state transition for debugging
+ if (!_introCursorPushed) {
+ _introCursorPushed = true;
+ debug("Rebel2: Intro sequence detected (flags=0x%x) - HUD disabled", _player->_curVideoFlags);
+ }
+ // Skip all HUD rendering during intro - subtitles are rendered via opcode 9
+ return;
+ } else {
+ // Gameplay mode - restore normal rendering
+ if (_introCursorPushed) {
+ _introCursorPushed = false;
+ debug("Rebel2: Gameplay started - HUD enabled");
+ }
+ }
+
+ // From here on, we're in gameplay mode (not intro)
if (!introPlaying) {
// ============================================================
// STEP 0: Fill status bar background (FUN_004288c0 equivalent)
diff --git a/engines/scumm/insane/insane_rebel.h b/engines/scumm/insane/insane_rebel.h
index 35e33260de1..5e7afaeadf7 100644
--- a/engines/scumm/insane/insane_rebel.h
+++ b/engines/scumm/insane/insane_rebel.h
@@ -63,6 +63,7 @@ public:
void iactRebel2Opcode3(Common::SeekableReadStream &b, int16 par2, int16 par3, int16 par4);
void iactRebel2Opcode6(byte *renderBitmap, Common::SeekableReadStream &b, int16 par2, int16 par3, int16 par4);
void iactRebel2Opcode8(byte *renderBitmap, Common::SeekableReadStream &b, int16 par2, int16 par3, int16 par4);
+ void iactRebel2Opcode9(byte *renderBitmap, Common::SeekableReadStream &b, int16 par2, int16 par3, int16 par4);
void procPostRendering(byte *renderBitmap, int32 codecparam, int32 setupsan12,
int32 setupsan13, int32 curFrame, int32 maxFrame) override;
diff --git a/engines/scumm/scumm.cpp b/engines/scumm/scumm.cpp
index 020da441c8a..4c6615fcc93 100644
--- a/engines/scumm/scumm.cpp
+++ b/engines/scumm/scumm.cpp
@@ -2679,10 +2679,14 @@ Common::Error ScummEngine::go() {
if (_game.features & GF_DEMO) {
((ScummEngine_v7 *)this)->_splayer->play("OPEN/O_DEMO.SAN", 12);
} else {
- // Mark the following SAN as an intro so HUD isn't rendered during it (bit 0x20)
- //((ScummEngine_v7 *)this)->_splayer->setCurVideoFlags(0x20);
- //((ScummEngine_v7 *)this)->_splayer->play("LEV01/01BEG.SAN", 12);
- // Clear intro flag and immediately start the mission SAN
+ // Play Level 1 intro cinematic (01BEG.SAN)
+ // Set video flags to 0x20 to mark this as a non-interactive intro sequence
+ // This flag tells procPostRendering to skip HUD/crosshair rendering
+ ((ScummEngine_v7 *)this)->_splayer->setCurVideoFlags(0x20);
+ ((ScummEngine_v7 *)this)->_splayer->play("LEV01/01BEG.SAN", 12);
+
+ // After intro completes, start the gameplay mission video (01P01.SAN)
+ // Clear the intro flag so HUD renders during gameplay
((ScummEngine_v7 *)this)->_splayer->setCurVideoFlags(0);
((ScummEngine_v7 *)this)->_splayer->play("LEV01/01P01.SAN", 12);
}
diff --git a/engines/scumm/smush/smush_player.cpp b/engines/scumm/smush/smush_player.cpp
index ccea514286f..59a1c332f24 100644
--- a/engines/scumm/smush/smush_player.cpp
+++ b/engines/scumm/smush/smush_player.cpp
@@ -54,7 +54,7 @@
namespace Scumm {
-static const int MAX_STRINGS = 200;
+static const int MAX_STRINGS = 800; // RA2 has ~658 strings
static const int ETRS_HEADER_LENGTH = 16;
class StringResource {
@@ -136,7 +136,24 @@ public:
}
data_end -= 2;
- assert(data_end > data_start);
+ // Handle empty entries (e.g., RA2 TRS files have entries with no content)
+ if (data_end <= data_start) {
+ // Skip this entry - no content
+ def_start = strchr(def_end + 1, '#');
+ continue;
+ }
+
+ // Strip leading // from first line (RA2 format uses // prefix for content)
+ if (data_start[0] == '/' && data_start[1] == '/') {
+ data_start += 2;
+ }
+
+ // Recalculate length after stripping
+ if (data_end <= data_start) {
+ def_start = strchr(def_end + 1, '#');
+ continue;
+ }
+
char *value = new char[data_end - data_start + 1];
assert(value);
memcpy(value, data_start, data_end - data_start);
@@ -183,14 +200,16 @@ public:
};
static StringResource *getStrings(ScummEngine *vm, const char *file, bool is_encoded) {
- debugC(DEBUG_SMUSH, "trying to read text resources from %s", file);
+ debugC(DEBUG_SMUSH, "getStrings: trying to read text resources from %s", file);
ScummFile *theFile = vm->instantiateScummFile();
vm->openFile(*theFile, file);
if (!theFile->isOpen()) {
+ debugC(DEBUG_SMUSH, "getStrings: Failed to open %s", file);
delete theFile;
return 0;
}
+ debugC(DEBUG_SMUSH, "getStrings: Successfully opened %s", file);
int32 length = theFile->size();
char *filebuffer = new char [length + 1];
assert(filebuffer);
@@ -575,9 +594,18 @@ void SmushPlayer::handleTextResource(uint32 subType, int32 subSize, Common::Seek
b.read(string, subSize - 16);
} else {
int string_id = b.readUint16LE();
- if (!_strings)
+ debugC(DEBUG_SMUSH, "SmushPlayer::handleTextResource: TRES string_id=%d pos=(%d,%d) flags=0x%x clip=(%d,%d,%d,%d) _strings=%p",
+ string_id, pos_x, pos_y, flags, left, top, width, height, (void*)_strings);
+ if (!_strings) {
+ debugC(DEBUG_SMUSH, "SmushPlayer::handleTextResource: _strings is null, cannot look up string");
return;
+ }
str = _strings->get(string_id);
+ if (str) {
+ debugC(DEBUG_SMUSH, "SmushPlayer::handleTextResource: Found string: \"%s\"", str);
+ } else {
+ debugC(DEBUG_SMUSH, "SmushPlayer::handleTextResource: String ID %d not found", string_id);
+ }
}
// if subtitles disabled and bit 3 is set, then do not draw
@@ -1018,8 +1046,18 @@ void SmushPlayer::handleAnimHeader(int32 subSize, Common::SeekableReadStream &b)
void SmushPlayer::setupAnim(const char *file) {
if (_insanity) {
- if (!((_vm->_game.features & GF_DEMO) && (_vm->_game.platform == Common::kPlatformDOS)))
+ if (_vm->_game.id == GID_REBEL2) {
+ // Rebel Assault 2 uses SYSTM/GAME.TRS for all subtitle strings
+ // The TRS file is ETRS-encoded (XOR with 0xCC)
+ _strings = getStrings(_vm, "SYSTM/GAME.TRS", true);
+ if (_strings) {
+ debugC(DEBUG_SMUSH, "SmushPlayer::setupAnim: Loaded GAME.TRS string resources successfully");
+ } else {
+ debugC(DEBUG_SMUSH, "SmushPlayer::setupAnim: Failed to load GAME.TRS!");
+ }
+ } else if (!((_vm->_game.features & GF_DEMO) && (_vm->_game.platform == Common::kPlatformDOS))) {
readString("mineroad.trs");
+ }
} else
readString(file);
}
@@ -1047,6 +1085,26 @@ SmushFont *SmushPlayer::getFont(int font) {
_sf[font] = new SmushFont(_vm, ft_fonts[font], true);
}
+ } else if (_vm->_game.id == GID_REBEL2) {
+ // Rebel Assault 2 fonts:
+ // font 0: TALKFONT.NUT - main dialog/subtitle font
+ // font 1: DIHIFONT.NUT - high-res dialog font
+ // font 2: TITLFONT.NUT - title font
+ // font 3: SMALFONT.NUT - small font for HUD
+ const char *ra2_fonts[] = {
+ "SYSTM/TALKFONT.NUT",
+ "SYSTM/DIHIFONT.NUT",
+ "SYSTM/TITLFONT.NUT",
+ "SYSTM/SMALFONT.NUT"
+ };
+ int numFonts = ARRAYSIZE(ra2_fonts);
+ if (font >= 0 && font < numFonts) {
+ _sf[font] = new SmushFont(_vm, ra2_fonts[font], true);
+ } else {
+ // Fallback to font 0 for unknown font indices
+ debugC(DEBUG_SMUSH, "SmushPlayer::getFont: RA2 unknown font %d, using TALKFONT", font);
+ _sf[font] = new SmushFont(_vm, ra2_fonts[0], true);
+ }
} else {
int numFonts = (_vm->_game.id == GID_CMI && !(_vm->_game.features & GF_DEMO)) ? 5 : 4;
assert(font >= 0 && font < numFonts);
Commit: a3676cab6e3312ae277d93f7107858c07f97fd61
https://github.com/scummvm/scummvm/commit/a3676cab6e3312ae277d93f7107858c07f97fd61
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:08:52+02:00
Commit Message:
SCUMM: RA2: Add dedicated audio system
Changed paths:
engines/scumm/insane/insane_rebel.cpp
engines/scumm/insane/insane_rebel.h
engines/scumm/scumm.cpp
engines/scumm/smush/smush_player.cpp
diff --git a/engines/scumm/insane/insane_rebel.cpp b/engines/scumm/insane/insane_rebel.cpp
index 38190aafb90..05e05f882d1 100644
--- a/engines/scumm/insane/insane_rebel.cpp
+++ b/engines/scumm/insane/insane_rebel.cpp
@@ -39,6 +39,8 @@
#include "scumm/insane/insane_rebel.h"
+#include "audio/audiostream.h"
+#include "audio/decoders/raw.h"
namespace Scumm {
@@ -216,10 +218,18 @@ InsaneRebel2::InsaneRebel2(ScummEngine_v7 *scumm) {
_rebelEmbeddedHud[i].renderY = 0;
_rebelEmbeddedHud[i].valid = false;
}
+
+ // Initialize audio system for RA2 (since we don't use iMUSE)
+ _audioSampleRate = 11025; // RA2 audio is 11025 Hz, not 22050 Hz
+ for (i = 0; i < kRA2MaxAudioTracks; i++) {
+ _audioStreams[i] = nullptr;
+ _audioTrackActive[i] = false;
+ }
}
InsaneRebel2::~InsaneRebel2() {
+ terminateAudio();
delete _rebelMsgFont;
delete _smush_dispfontNut;
}
@@ -2162,6 +2172,8 @@ void InsaneRebel2::procPostRendering(byte *renderBitmap, int32 codecparam, int32
// This is called after all other rendering so it appears on top
renderScoreHUD(renderBitmap, pitch, width, height, 0);
+ } // End of if (!introPlaying) block
+
// ============================================================
// FRAME END RESET: Clear active flags for all enemies (FUN_403240 equivalent)
// ============================================================
@@ -2179,6 +2191,227 @@ void InsaneRebel2::procPostRendering(byte *renderBitmap, int32 codecparam, int32
}
}
+// ========== Audio Handling for Rebel Assault 2 ==========
+// RA2 doesn't use iMUSE - we handle audio directly through the mixer
+
+void InsaneRebel2::initAudio(int sampleRate) {
+ _audioSampleRate = sampleRate;
+ for (int i = 0; i < kRA2MaxAudioTracks; i++) {
+ _audioStreams[i] = nullptr;
+ _audioTrackActive[i] = false;
+ }
+}
+
+void InsaneRebel2::terminateAudio() {
+ for (int i = 0; i < kRA2MaxAudioTracks; i++) {
+ if (_audioTrackActive[i]) {
+ _vm->_mixer->stopHandle(_audioHandles[i]);
+ _audioTrackActive[i] = false;
+ }
+ if (_audioStreams[i]) {
+ _audioStreams[i]->finish();
+ _audioStreams[i] = nullptr;
+ }
+ }
}
+void InsaneRebel2::queueAudioData(int trackIdx, uint8 *data, int32 size, int volume, int pan) {
+ if (trackIdx < 0 || trackIdx >= kRA2MaxAudioTracks || size <= 0 || !data) {
+ debug(5, "InsaneRebel2::queueAudioData: Invalid params trackIdx=%d size=%d data=%p", trackIdx, size, (void*)data);
+ return;
+ }
+
+ debug(5, "InsaneRebel2::queueAudioData: trackIdx=%d size=%d volume=%d pan=%d", trackIdx, size, volume, pan);
+
+ // Create audio stream if not already active
+ if (!_audioStreams[trackIdx]) {
+ // RA2 audio is 8-bit unsigned mono at the track's sample rate
+ debug("InsaneRebel2: Creating audio stream for track %d at %d Hz", trackIdx, _audioSampleRate);
+ _audioStreams[trackIdx] = Audio::makeQueuingAudioStream(_audioSampleRate, false);
+ _audioTrackActive[trackIdx] = true;
+ _vm->_mixer->playStream(Audio::Mixer::kSFXSoundType, &_audioHandles[trackIdx],
+ _audioStreams[trackIdx], -1, Audio::Mixer::kMaxChannelVolume, 0,
+ DisposeAfterUse::NO);
+ }
+
+ debug(6, "InsaneRebel2: Queueing %d bytes to track %d (vol=%d)", size, trackIdx, volume);
+
+ // Copy the audio data since queueBuffer may need to own it
+ byte *audioCopy = (byte *)malloc(size);
+ if (!audioCopy) {
+ return;
+ }
+ memcpy(audioCopy, data, size);
+
+ // Queue the audio data - RA2 SMUSH audio is 8-bit unsigned mono
+ _audioStreams[trackIdx]->queueBuffer(audioCopy, size, DisposeAfterUse::YES, Audio::FLAG_UNSIGNED);
+
+ // Apply volume and pan to the channel
+ int scaledVolume = (volume * Audio::Mixer::kMaxChannelVolume) / 127;
+ int scaledPan = (pan * 127) / 128; // Convert -128..127 to -127..127
+ _vm->_mixer->setChannelVolume(_audioHandles[trackIdx], scaledVolume);
+ _vm->_mixer->setChannelBalance(_audioHandles[trackIdx], scaledPan);
+}
+
+void InsaneRebel2::processAudioFrame(int16 feedSize) {
+ if (!_player) {
+ return;
+ }
+
+ // Initialize dispatch data if needed (normally done in processDispatches for iMUSE games)
+ if (_player->_smushTracksNeedInit) {
+ _player->_smushTracksNeedInit = false;
+ for (int i = 0; i < SMUSH_MAX_TRACKS; i++) {
+ _player->_smushDispatch[i].fadeRemaining = 0;
+ _player->_smushDispatch[i].fadeVolume = 0;
+ _player->_smushDispatch[i].fadeSampleRate = 0;
+ _player->_smushDispatch[i].elapsedAudio = 0;
+ _player->_smushDispatch[i].audioLength = 0;
+ }
+ }
+
+ // Access SmushPlayer's audio track data (InsaneRebel2 is a friend class)
+ // Only iterate over actually allocated tracks (not SMUSH_MAX_TRACKS)
+ for (int i = 0; i < _player->_smushNumTracks; i++) {
+ SmushPlayer::SmushAudioTrack &track = _player->_smushTracks[i];
+ SmushPlayer::SmushAudioDispatch &dispatch = _player->_smushDispatch[i];
+
+ if (track.state == TRK_STATE_INACTIVE) {
+ continue;
+ }
+
+ // Skip tracks that don't have valid buffer pointers yet
+ // Note: dispatch.dataBuf is set when transitioning from FADING to PLAYING,
+ // so tracks in FADING state won't have it set yet - that's OK, they'll be
+ // transitioned below and then processed
+ if (!track.blockPtr) {
+ debug(5, "InsaneRebel2: Skipping track %d - blockPtr=%p state=%d",
+ i, (void*)track.blockPtr, track.state);
+ continue;
+ }
+
+ // Check if this track type should be played
+ bool isPlayableTrack = ((track.flags & TRK_TYPE_MASK) == IS_SPEECH && _player->isChanActive(CHN_SPEECH)) ||
+ ((track.flags & TRK_TYPE_MASK) == IS_BKG_MUSIC && _player->isChanActive(CHN_BKGMUS)) ||
+ ((track.flags & TRK_TYPE_MASK) == IS_SFX && _player->isChanActive(CHN_OTHER));
+
+ if (!isPlayableTrack) {
+ continue;
+ }
+
+ // Calculate base volume for this track type
+ int baseVolume;
+ switch (track.flags & TRK_TYPE_MASK) {
+ case IS_SFX:
+ baseVolume = (_player->_smushTrackVols[1] * track.volume) >> 7;
+ break;
+ case IS_BKG_MUSIC:
+ baseVolume = (_player->_smushTrackVols[3] * track.volume) >> 7;
+ break;
+ case IS_SPEECH:
+ baseVolume = (_player->_smushTrackVols[2] * track.volume) >> 7;
+ break;
+ default:
+ baseVolume = track.volume;
+ break;
+ }
+ int mixVolume = baseVolume * _player->_smushTrackVols[0] / 127;
+
+ // Handle track state transitions: FADING -> PLAYING
+ if (track.state == TRK_STATE_FADING) {
+ dispatch.headerPtr = track.dataBuf;
+ dispatch.dataBuf = track.subChunkPtr;
+ dispatch.dataSize = track.dataSize;
+ dispatch.currentOffset = 0;
+ dispatch.audioLength = 0;
+ track.state = TRK_STATE_PLAYING;
+ }
+
+ // Process audio for this track
+ if (track.state != TRK_STATE_INACTIVE) {
+ int32 tmpFeedSize = feedSize;
+
+ while (tmpFeedSize > 0) {
+ int32 mixInFrameCount = dispatch.currentOffset;
+
+ // Use dispatch.dataBuf and dispatch.dataSize which are set consistently
+ // when the track transitions from FADING to PLAYING, and audioRemaining
+ // is calculated relative to these values by processAudioCodes
+ if (mixInFrameCount > 0 && dispatch.dataBuf && dispatch.dataSize > 0) {
+ // Ensure audioRemaining is non-negative for proper circular buffer access
+ if (dispatch.audioRemaining < 0) {
+ debug(5, "InsaneRebel2: Resetting negative audioRemaining=%d for track %d", dispatch.audioRemaining, i);
+ dispatch.audioRemaining = 0;
+ }
+ int32 offset = dispatch.audioRemaining % dispatch.dataSize;
+
+ // Limit to feed size proportional to sample rate
+ if (dispatch.sampleRate > 0 && _player->_smushAudioSampleRate > 0) {
+ int32 maxFrames = dispatch.sampleRate * tmpFeedSize / _player->_smushAudioSampleRate;
+ if (mixInFrameCount > maxFrames) {
+ mixInFrameCount = maxFrames;
+ }
+ }
+
+ // Don't read past the buffer
+ if (offset + mixInFrameCount > dispatch.dataSize) {
+ mixInFrameCount = dispatch.dataSize - offset;
+ }
+
+ // Make sure we don't exceed available data
+ if (dispatch.audioRemaining + mixInFrameCount > track.availableSize) {
+ mixInFrameCount = track.availableSize - dispatch.audioRemaining;
+ if (mixInFrameCount <= 0) {
+ // Track is ending - no more data
+ track.state = TRK_STATE_ENDING;
+ break;
+ }
+ }
+
+ if (mixInFrameCount > 0) {
+ // Safety check: verify the pointer and offset are within bounds
+ if (!dispatch.dataBuf || offset < 0 || offset + mixInFrameCount > dispatch.dataSize) {
+ debug(1, "InsaneRebel2: Invalid audio buffer access track=%d dataBuf=%p offset=%d mixInFrameCount=%d dataSize=%d",
+ i, (void*)dispatch.dataBuf, offset, mixInFrameCount, dispatch.dataSize);
+ break;
+ }
+
+ // Queue audio data directly to our audio streams
+ queueAudioData(i, &dispatch.dataBuf[offset], mixInFrameCount, mixVolume, track.pan);
+
+ // Update dispatch state
+ dispatch.currentOffset -= mixInFrameCount;
+ dispatch.audioRemaining += mixInFrameCount;
+
+ // Calculate how much feed time was consumed
+ if (dispatch.sampleRate > 0) {
+ int32 consumedFeed = mixInFrameCount * _player->_smushAudioSampleRate / dispatch.sampleRate;
+ tmpFeedSize -= consumedFeed;
+ } else {
+ tmpFeedSize -= mixInFrameCount;
+ }
+ }
+ }
+
+ // If currentOffset is depleted, process audio codes to get more
+ if (dispatch.currentOffset <= 0) {
+ // processAudioCodes returns true if there's more audio, false if done
+ if (!_player->processAudioCodes(i, tmpFeedSize, mixVolume)) {
+ break;
+ }
+ // If still no offset after processing codes, we're done
+ if (dispatch.currentOffset <= 0) {
+ break;
+ }
+ } else if (tmpFeedSize <= 0) {
+ break;
+ }
+ }
+ }
+
+ track.audioRemaining = dispatch.audioRemaining;
+ dispatch.state = track.state;
+ }
}
+
+} // End of namespace Scumm
diff --git a/engines/scumm/insane/insane_rebel.h b/engines/scumm/insane/insane_rebel.h
index 5e7afaeadf7..ced8bb6e441 100644
--- a/engines/scumm/insane/insane_rebel.h
+++ b/engines/scumm/insane/insane_rebel.h
@@ -31,6 +31,14 @@
#include "common/list.h"
#include "common/rect.h"
+#include "audio/audiostream.h"
+#include "audio/mixer.h"
+
+namespace Audio {
+class QueuingAudioStream;
+class SoundHandle;
+}
+
namespace Scumm {
class InsaneRebel2 : public Insane {
@@ -214,6 +222,29 @@ public:
// Target lock timer (DAT_00443676) - set to 7 when crosshair is over enemy
int _targetLockTimer;
+ // ========== Audio Handling ==========
+ // Rebel Assault 2 doesn't use iMUSE - audio is handled directly here
+
+ static const int kRA2MaxAudioTracks = 4;
+
+ Audio::QueuingAudioStream *_audioStreams[kRA2MaxAudioTracks];
+ Audio::SoundHandle _audioHandles[kRA2MaxAudioTracks];
+ bool _audioTrackActive[kRA2MaxAudioTracks];
+ int _audioSampleRate;
+
+ // Initialize audio system for RA2
+ void initAudio(int sampleRate);
+
+ // Terminate audio system
+ void terminateAudio();
+
+ // Process audio dispatches - called from SmushPlayer when iMUSE is null
+ // This replaces the iMUSE audio path for RA2
+ void processAudioFrame(int16 feedSize);
+
+ // Queue audio data for playback on a specific track
+ void queueAudioData(int trackIdx, uint8 *data, int32 size, int volume, int pan);
+
};
} // End of namespace Insane
diff --git a/engines/scumm/scumm.cpp b/engines/scumm/scumm.cpp
index 4c6615fcc93..d29e0da93c1 100644
--- a/engines/scumm/scumm.cpp
+++ b/engines/scumm/scumm.cpp
@@ -1807,9 +1807,10 @@ void ScummEngine_v7::setupScumm(const Common::Path &macResourceFile) {
_useOriginalGUI = false;
_sound = new Sound(this, _mixer, false);
- _musicEngine = _imuseDigital = new IMuseDigital(this, 11025, _mixer, &_resourceAccessMutex, false);
+ // Rebel Assault 2 doesn't use iMUSE for audio - audio is handled directly by INSANE
+ _musicEngine = _imuseDigital = nullptr;
_insane = new InsaneRebel2(this);
- _splayer = new SmushPlayer(this, _imuseDigital, _insane);
+ _splayer = new SmushPlayer(this, nullptr, _insane);
// Initialize cursor
_macGui = nullptr; // Ensure this is null as we don't want MacGui behavior
diff --git a/engines/scumm/smush/smush_player.cpp b/engines/scumm/smush/smush_player.cpp
index 59a1c332f24..a0c3c831bbb 100644
--- a/engines/scumm/smush/smush_player.cpp
+++ b/engines/scumm/smush/smush_player.cpp
@@ -43,6 +43,7 @@
#include "scumm/smush/smush_player.h"
#include "scumm/insane/insane.h"
+#include "scumm/insane/insane_rebel.h"
#include "audio/audiostream.h"
#include "audio/mixer.h"
@@ -292,7 +293,13 @@ SmushPlayer::SmushPlayer(ScummEngine_v7 *scumm, IMuseDigital *imuseDigital, Insa
_smushAudioInitialized = false;
_smushAudioCallbackEnabled = false;
- initAudio(_imuseDigital->getSampleRate(), 200000);
+ // Rebel Assault 2 doesn't use iMUSE for audio, so _imuseDigital may be null
+ if (_imuseDigital) {
+ initAudio(_imuseDigital->getSampleRate(), 200000);
+ } else {
+ // RA2 audio is 11025 Hz
+ initAudio(11025, 200000);
+ }
}
SmushPlayer::~SmushPlayer() {
@@ -1191,7 +1198,15 @@ void SmushPlayer::parseNextFrame() {
if (_insanity)
_vm->_sound->processSound();
- _vm->_imuseDigital->flushTracks();
+ if (_vm->_imuseDigital)
+ _vm->_imuseDigital->flushTracks();
+
+ // Rebel Assault 2 audio processing - call processDispatches directly since no iMUSE
+ if (!_imuseDigital && _vm->_game.id == GID_REBEL2) {
+ // Use a feed size based on frame rate (similar to iMUSE)
+ // 11025 Hz / 12 fps = ~918 samples per frame
+ processDispatches(_smushAudioSampleRate / 12);
+ }
}
void SmushPlayer::setPalette(const byte *palette) {
@@ -1330,10 +1345,13 @@ void SmushPlayer::play(const char *filename, int32 speed, int32 offset, int32 st
// This piece of code is used to ensure there are
// no audio hiccups while loading the SMUSH video;
// Each version of the engine does it in its own way.
- if (_imuseDigital->isFTSoundEngine()) {
- _imuseDigital->fillStreamsWhileMusicCritical(20);
- } else {
- _imuseDigital->floodMusicBuffer();
+ // Rebel Assault 2 doesn't use iMUSE for audio.
+ if (_imuseDigital) {
+ if (_imuseDigital->isFTSoundEngine()) {
+ _imuseDigital->fillStreamsWhileMusicCritical(20);
+ } else {
+ _imuseDigital->floodMusicBuffer();
+ }
}
int skipped = 0;
@@ -1440,7 +1458,8 @@ void SmushPlayer::play(const char *filename, int32 speed, int32 offset, int32 st
_IACTpos = 0;
resetAudioTracks(); // For DIG demo
- _imuseDigital->stopSMUSHAudio(); // For DIG & COMI
+ if (_imuseDigital)
+ _imuseDigital->stopSMUSHAudio(); // For DIG & COMI
break;
}
@@ -1461,7 +1480,9 @@ void SmushPlayer::play(const char *filename, int32 speed, int32 offset, int32 st
void SmushPlayer::initAudio(int samplerate, int32 maxChunkSize) {
int32 maxSizes[SMUSH_MAX_TRACKS] = {100000, 100000, 100000, 400000};
- _imuseDigital->setSmushPlayer(this);
+ // Rebel Assault 2 doesn't use iMUSE for audio
+ if (_imuseDigital)
+ _imuseDigital->setSmushPlayer(this);
// DIG demo uses this audio system but doesn't use INSANE
if (_insane)
@@ -1471,6 +1492,23 @@ void SmushPlayer::initAudio(int samplerate, int32 maxChunkSize) {
memset(_smushAudioTable, 0, sizeof(_smushAudioTable));
+ // Initialize dispatch structures to safe defaults
+ for (int i = 0; i < SMUSH_MAX_TRACKS; i++) {
+ _smushDispatch[i].headerPtr = nullptr;
+ _smushDispatch[i].dataBuf = nullptr;
+ _smushDispatch[i].dataSize = 0;
+ _smushDispatch[i].audioRemaining = 0;
+ _smushDispatch[i].currentOffset = 0;
+ _smushDispatch[i].sampleRate = 0;
+ _smushDispatch[i].state = TRK_STATE_INACTIVE;
+ _smushDispatch[i].fadeSampleRate = 0;
+ _smushDispatch[i].fadeVolume = 0;
+ _smushDispatch[i].fadeRemaining = 0;
+ _smushDispatch[i].volumeStep = 0;
+ _smushDispatch[i].elapsedAudio = 0;
+ _smushDispatch[i].audioLength = 0;
+ }
+
for (int i = 0; i < SMUSH_MAX_TRACKS; i++) {
_smushTrackVols[i] = 127;
_smushTrackFlags[i] = 1;
@@ -1516,6 +1554,9 @@ int SmushPlayer::addAudioTrack(int32 trackBlockSize, int32 maxBlockSize) {
_smushTracks[id].groupId = GRP_MASTER;
_smushTracks[id].blockSize = trackBlockSize;
_smushTracks[id].parsedChunks = 0;
+ _smushTracks[id].dataBuf = nullptr;
+ _smushTracks[id].subChunkPtr = nullptr;
+ _smushTracks[id].dataSize = 0;
_smushTracks[id].fadeBuf = (uint8 *)malloc(SMUSH_FADE_SIZE);
if (!_smushTracks[id].fadeBuf)
@@ -1737,6 +1778,15 @@ void SmushPlayer::processDispatches(int16 feedSize) {
bool isPlayableTrack;
bool speechIsPlaying = false;
+ // Rebel Assault 2 doesn't use iMUSE for audio - use InsaneRebel2's audio handler
+ if (!_imuseDigital) {
+ if (_vm->_game.id == GID_REBEL2 && _insane) {
+ InsaneRebel2 *rebel2 = static_cast<InsaneRebel2 *>(_insane);
+ rebel2->processAudioFrame(feedSize);
+ }
+ return;
+ }
+
int engineBaseFeedSize = _imuseDigital->getFeedSize();
if (!_paused) {
@@ -2166,6 +2216,10 @@ bool SmushPlayer::processAudioCodes(int idx, int32 &tmpFeedSize, int &mixVolume)
}
void SmushPlayer::sendAudioToDiMUSE(uint8 *mixBuf, int32 mixStartingPoint, int32 mixFeedSize, int32 mixInFrameCount, int volume, int pan) {
+ // Rebel Assault 2 doesn't use iMUSE for audio
+ if (!_imuseDigital)
+ return;
+
int clampedVol, clampedPan;
bool is11025Hz = false;
Commit: f3bd96abae7ec9418b38a66136f53cabaed67d03
https://github.com/scummvm/scummvm/commit/f3bd96abae7ec9418b38a66136f53cabaed67d03
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:08:52+02:00
Commit Message:
SCUMM: RA2: Implement main menu
Changed paths:
engines/scumm/insane/insane_rebel.cpp
engines/scumm/insane/insane_rebel.h
engines/scumm/nut_renderer.h
engines/scumm/scumm.cpp
engines/scumm/scumm_v7.h
engines/scumm/smush/smush_player.cpp
diff --git a/engines/scumm/insane/insane_rebel.cpp b/engines/scumm/insane/insane_rebel.cpp
index 05e05f882d1..b6e76998ece 100644
--- a/engines/scumm/insane/insane_rebel.cpp
+++ b/engines/scumm/insane/insane_rebel.cpp
@@ -25,6 +25,8 @@
#include "common/system.h"
#include "common/memstream.h"
+#include "graphics/cursorman.h"
+
#include "scumm/actor.h"
#include "scumm/file.h"
#include "scumm/resource.h"
@@ -77,6 +79,24 @@ InsaneRebel2::InsaneRebel2(ScummEngine_v7 *scumm) {
// Load DIHIFONT.NUT for in-video messages/subtitles (Opcode 9)
_rebelMsgFont = new SmushFont(_vm, "SYSTM/DIHIFONT.NUT", true);
+ // Load menu system fonts (from info.md - FUN_403BD0 lines 302-348)
+ // In low resolution mode, fonts are loaded as a linked list:
+ // Font 0 (^f00): TALKFONT.NUT - Default menu font (DAT_00485058)
+ // Font 1 (^f01): SMALFONT.NUT - Small font for format code switching
+ // Font 2 (^f02): TITLFONT.NUT - Title font
+ // Font 3 (^f03): POVFONT.NUT - POV font (not loaded here)
+ _smush_talkfontNut = new NutRenderer(_vm, "SYSTM/TALKFONT.NUT");
+ _smush_smalfontNut = new NutRenderer(_vm, "SYSTM/SMALFONT.NUT");
+ _smush_titlefontNut = new NutRenderer(_vm, "SYSTM/TITLFONT.NUT");
+
+ // SmushFont for menu text rendering - uses SMALFONT with proper drawString support
+ _menuFont = new SmushFont(_vm, "SYSTM/SMALFONT.NUT", true);
+
+ // MSTOVER.NUT - Mouse Over background overlay (NOT a cursor!)
+ // This is loaded into DAT_0047aba8 and used as a background overlay via FUN_004236e0
+ // The original game uses the standard Windows arrow cursor (IDC_ARROW via LoadCursorA)
+ _smush_mouseoverNut = new NutRenderer(_vm, "SYSTM/MSTOVER.NUT");
+
_enemies.clear();
_rebelHandler = 8; // Default to Handler 8 (ground vehicle) for Level 1
_rebelLevelType = 0; // Level type from Opcode 6 par3, determines HUD sprite variant
@@ -225,13 +245,32 @@ InsaneRebel2::InsaneRebel2(ScummEngine_v7 *scumm) {
_audioStreams[i] = nullptr;
_audioTrackActive[i] = false;
}
+
+ // Initialize menu system
+ _gameState = kStateMainMenu; // Start at main menu
+ _menuSelection = 0; // First item selected
+ // Main menu has 6 selectable items (0-5): START, OPTIONS, CONTINUE INTRO, TOP PILOTS, CREDITS, QUIT
+ // Note: The coordinate formula uses param_3 = 7 (includes title) for Y position calculation
+ // but _menuItemCount is the number of SELECTABLE items for bounds checking
+ _menuItemCount = 6;
+ _menuInactivityTimer = 0;
+ _lastMenuVariant = -1; // No previous menu video
+ _menuRepeatDelay = 0;
+ for (i = 0; i < 16; i++) {
+ _levelUnlocked[i] = (i == 0); // Only level 1 unlocked initially
+ }
}
InsaneRebel2::~InsaneRebel2() {
terminateAudio();
delete _rebelMsgFont;
+ delete _menuFont;
delete _smush_dispfontNut;
+ delete _smush_talkfontNut;
+ delete _smush_smalfontNut;
+ delete _smush_titlefontNut;
+ delete _smush_mouseoverNut;
}
// Score lookup tables (from DAT_0047e0fe, DAT_0047e100, DAT_0047e102)
@@ -506,9 +545,68 @@ void InsaneRebel2::clearBit(int n) {
void InsaneRebel2::procIACT(byte *renderBitmap, int32 codecparam, int32 setupsan12,
int32 setupsan13, Common::SeekableReadStream &b, int32 size, int32 flags,
int16 par1, int16 par2, int16 par3, int16 par4) {
+ // Debug: Log all IACT opcodes
+ debug("Rebel2 IACT: opcode=%d par2=%d par3=%d par4=%d gameState=%d sceneId=%d",
+ par1, par2, par3, par4, _gameState, _currSceneId);
+
if (_keyboardDisable)
return;
+ // Handle menu IACT - menu videos have embedded ANIM data in IACT chunks
+ // Menu IACTs have par1=8 (code), par2=46 (flags), par4>=1000 (userId)
+ // The embedded ANIM contains the full menu frame
+ if (_gameState == kStateMainMenu && par1 == 8 && par4 >= 1000) {
+ debug("Rebel2 IACT: Menu mode - processing embedded ANIM (userId=%d)", par4);
+
+ // Scan for embedded ANIM tag in the IACT data
+ int64 startPos = b.pos();
+ int64 totalSize = b.size();
+ debug("Rebel2 IACT: stream pos=%d, size=%d, remaining=%d",
+ (int)startPos, (int)totalSize, (int)(totalSize - startPos));
+
+ if (totalSize > startPos) {
+ int64 remaining = totalSize - startPos;
+ int scanSize = (int)MIN<int64>(remaining, 65536);
+ byte *scanBuf = (byte *)malloc(scanSize);
+ if (scanBuf) {
+ int bytesRead = b.read(scanBuf, scanSize);
+ debug("Rebel2 IACT: Read %d bytes, first 16: %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X",
+ bytesRead, scanBuf[0], scanBuf[1], scanBuf[2], scanBuf[3],
+ scanBuf[4], scanBuf[5], scanBuf[6], scanBuf[7],
+ scanBuf[8], scanBuf[9], scanBuf[10], scanBuf[11],
+ scanBuf[12], scanBuf[13], scanBuf[14], scanBuf[15]);
+
+ // Look for ANIM tag (embedded SAN containing menu frame)
+ for (int i = 0; i + 8 <= bytesRead; ++i) {
+ if (READ_BE_UINT32(scanBuf + i) == MKTAG('A','N','I','M')) {
+ int64 animStreamPos = startPos + i;
+ uint32 animReportedSize = READ_BE_UINT32(scanBuf + i + 4);
+ int32 toCopy = (int)MIN<int64>((int64)animReportedSize + 8, totalSize - animStreamPos);
+ debug("Rebel2 IACT: Found embedded ANIM at offset %d, size %d", (int)i, (int)animReportedSize);
+ if (toCopy > 0) {
+ byte *animData = (byte *)malloc(toCopy);
+ if (animData) {
+ b.seek(animStreamPos);
+ b.read(animData, toCopy);
+ // Use userId as the HUD slot (1000 -> slot 0 for menu background)
+ loadEmbeddedSan(0, animData, toCopy, renderBitmap);
+ free(animData);
+ }
+ }
+ b.seek(startPos);
+ free(scanBuf);
+ return;
+ }
+ }
+
+ debug("Rebel2 IACT: No ANIM tag found in menu IACT data");
+ b.seek(startPos);
+ free(scanBuf);
+ }
+ }
+ return;
+ }
+
if (_currSceneId == 1)
iactRebel2Scene1(renderBitmap, codecparam, setupsan12, setupsan13, b, size, flags, par1, par2, par3, par4);
}
@@ -1202,8 +1300,8 @@ extern void smushDecodeRLE(byte *dst, const byte *src, int left, int top, int wi
extern void smushDecodeUncompressed(byte *dst, const byte *src, int left, int top, int width, int height, int pitch);
void InsaneRebel2::loadEmbeddedSan(int userId, byte *animData, int32 size, byte *renderBitmap) {
- // Validate userId (1-4 for HUD slots)
- if (userId < 1 || userId > 4 || !animData || size < 32) {
+ // Validate userId (0 for menu background, 1-4 for HUD slots)
+ if (userId < 0 || userId > 4 || !animData || size < 32) {
debug("Rebel2: Invalid embedded SAN: userId=%d, size=%d", userId, size);
return;
}
@@ -1748,10 +1846,65 @@ void InsaneRebel2::procPostRendering(byte *renderBitmap, int32 codecparam, int32
const int statusBarY = 180; // 0xb4 - status bar starts at Y=180 in video coords
// Hide HUD/status bar during intro videos (marked by SmushPlayer video flag 0x20)
- // The 0x20 flag indicates a non-interactive cutscene/intro sequence
+ // The 0x20 flag indicates a non-interactive cutscene/intro sequence OR menu
bool introPlaying = ((_player->_curVideoFlags & 0x20) != 0);
- // During intro sequences:
+ // Check if we're in menu mode (menu state + intro flag)
+ bool menuMode = (introPlaying && _gameState == kStateMainMenu);
+
+ // Handle menu input and rendering if in menu mode
+ if (menuMode) {
+ // The original game uses the standard Windows arrow cursor (IDC_ARROW)
+ // loaded via LoadCursorA(NULL, 0x7f00) in FUN_420C70.decompiled.txt
+ // MSTOVER.NUT is a background overlay, NOT a cursor
+ static const byte arrowCursor[11 * 16] = {
+ 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 1, 255, 1, 0, 0, 0, 0, 0, 0, 0, 0,
+ 1, 255, 255, 1, 0, 0, 0, 0, 0, 0, 0,
+ 1, 255, 255, 255, 1, 0, 0, 0, 0, 0, 0,
+ 1, 255, 255, 255, 255, 1, 0, 0, 0, 0, 0,
+ 1, 255, 255, 255, 255, 255, 1, 0, 0, 0, 0,
+ 1, 255, 255, 255, 255, 255, 255, 1, 0, 0, 0,
+ 1, 255, 255, 255, 255, 255, 255, 255, 1, 0, 0,
+ 1, 255, 255, 255, 255, 255, 255, 255, 255, 1, 0,
+ 1, 255, 255, 255, 255, 255, 1, 1, 1, 1, 1,
+ 1, 255, 255, 1, 255, 255, 1, 0, 0, 0, 0,
+ 1, 255, 1, 0, 1, 255, 255, 1, 0, 0, 0,
+ 1, 1, 0, 0, 1, 255, 255, 1, 0, 0, 0,
+ 1, 0, 0, 0, 0, 1, 255, 255, 1, 0, 0,
+ 0, 0, 0, 0, 0, 1, 255, 255, 1, 0, 0,
+ 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0,
+ };
+ CursorMan.replaceCursor(arrowCursor, 11, 16, 0, 0, 0);
+ CursorMan.showMouse(true);
+
+ // Process menu input during each frame
+ int selection = processMenuInput();
+
+ // Update inactivity timer
+ _menuInactivityTimer++;
+
+ // Check for inactivity timeout (300 frames = ~10 sec at 30fps, or ~25 sec at 12fps)
+ if (_menuInactivityTimer > 300) {
+ debug("Rebel2: Menu inactivity timeout");
+ // Reset timer but don't take action yet
+ _menuInactivityTimer = 0;
+ }
+
+ // Draw menu selection overlay
+ drawMenuOverlay(renderBitmap, pitch, width, height);
+
+ // If a selection was confirmed, signal video to stop
+ if (selection >= 0) {
+ debug("Rebel2: Menu selection confirmed: %d", selection);
+ _vm->_smushVideoShouldFinish = true;
+ }
+
+ // Skip normal HUD rendering in menu mode
+ return;
+ }
+
+ // During intro sequences (non-menu):
// - Hide the crosshair/cursor (handled by not drawing it)
// - Skip all HUD/status bar rendering
// - Skip mouse input processing
@@ -2414,4 +2567,410 @@ void InsaneRebel2::processAudioFrame(int16 feedSize) {
}
}
+// ======================= Menu System Implementation =======================
+// Emulates retail menu system from FUN_004147B2 and FUN_0041FDC8
+
+void InsaneRebel2::resetMenu() {
+ _menuSelection = 0;
+ _menuInactivityTimer = 0;
+ _menuRepeatDelay = 0;
+}
+
+Common::String InsaneRebel2::getRandomMenuVideo() {
+ // Emulates FUN_0041FDC8 - selects random menu video variant
+ //
+ // NOTE: The original game plays O_MENU.SAN when no progress flags are set,
+ // but that file contains ONLY audio (no FOBJ video frames). The O_MENU_X.SAN
+ // variants (A through O) contain actual 320x200 background images in Frame 0.
+ //
+ // We ALWAYS use a random variant to ensure a proper background is displayed.
+ // The original behavior of showing O_MENU.SAN (audio-only) would result in
+ // a black/undefined background which doesn't match the intended experience.
+
+ // Select random variant (0-14 maps to A-O), ensuring different from last
+ int variant;
+ do {
+ variant = _vm->_rnd.getRandomNumber(14); // 0-14
+ } while (variant == _lastMenuVariant && _lastMenuVariant >= 0);
+ _lastMenuVariant = variant;
+
+ // Map 0-14 to A-O (case 0/default = A, 1 = B, etc.)
+ char letter = 'A' + variant;
+ debug("Rebel2: Selected menu variant %c", letter);
+ return Common::String::format("OPEN/O_MENU_%c.SAN", letter);
+}
+
+int InsaneRebel2::processMenuInput() {
+ // Emulates FUN_0041f5ae menu input handling
+ // Returns: -1 = no action, 0-6 = menu item selected
+
+ Common::Event event;
+ int result = -1;
+
+ // Check for key/mouse events
+ Common::EventManager *eventMan = _vm->_system->getEventManager();
+
+ // Get current key state from _vm's VAR system or direct polling
+ // Key codes: 0x148 = Up, 0x150 = Down, 0x1B = ESC, 0x0D = Enter
+
+ // Poll for keyboard input
+ while (eventMan->pollEvent(event)) {
+ switch (event.type) {
+ case Common::EVENT_KEYDOWN:
+ _menuInactivityTimer = 0; // Reset inactivity timer on any input
+
+ switch (event.kbd.keycode) {
+ case Common::KEYCODE_UP:
+ // Navigate up (wrap around)
+ if (_menuRepeatDelay == 0) {
+ _menuSelection--;
+ if (_menuSelection < 0) {
+ _menuSelection = _menuItemCount - 1;
+ }
+ _menuRepeatDelay = 3; // Delay before repeat
+ debug("Menu: Selection changed to %d (UP)", _menuSelection);
+ }
+ break;
+
+ case Common::KEYCODE_DOWN:
+ // Navigate down (wrap around)
+ if (_menuRepeatDelay == 0) {
+ _menuSelection++;
+ if (_menuSelection >= _menuItemCount) {
+ _menuSelection = 0;
+ }
+ _menuRepeatDelay = 3;
+ debug("Menu: Selection changed to %d (DOWN)", _menuSelection);
+ }
+ break;
+
+ case Common::KEYCODE_RETURN:
+ case Common::KEYCODE_KP_ENTER:
+ // Confirm selection
+ if (_menuSelection >= 0 && _menuSelection < _menuItemCount) {
+ result = _menuSelection;
+ debug("Menu: Item %d selected (ENTER)", _menuSelection);
+ }
+ break;
+
+ case Common::KEYCODE_ESCAPE:
+ // ESC - Exit/Quit (return special value)
+ result = 6; // Quit option
+ debug("Menu: ESC pressed - quit");
+ break;
+
+ default:
+ break;
+ }
+ break;
+
+ case Common::EVENT_LBUTTONDOWN:
+ // Mouse click - check if over a menu item
+ // Menu items are vertically stacked, centered at X=160
+ // Y positions from FUN_41F5AE: param_3 * -5 + local_c * 10 + 0x68
+ // With param_3 = 7: base Y = 69, items at Y = 69, 79, 89, 99, 109, 119
+ _menuInactivityTimer = 0;
+ {
+ int mouseY = event.mouse.y;
+ int baseY = 69; // First item Y position (low-res)
+ int itemHeight = 10;
+
+ // Check which item was clicked
+ for (int i = 0; i < _menuItemCount; i++) {
+ int itemY = baseY + i * itemHeight;
+ if (mouseY >= itemY - 5 && mouseY <= itemY + 5) {
+ _menuSelection = i;
+ result = i;
+ debug("Menu: Item %d clicked at Y=%d", i, mouseY);
+ break;
+ }
+ }
+ }
+ break;
+
+ default:
+ break;
+ }
+ }
+
+ // Handle key repeat delay
+ if (_menuRepeatDelay > 0) {
+ _menuRepeatDelay--;
+ }
+
+ return result;
+}
+
+void InsaneRebel2::drawMenuOverlay(byte *renderBitmap, int pitch, int width, int height) {
+ // Emulates FUN_0041f5ae menu text overlay rendering
+ //
+ // IMPORTANT: The menu background comes from the O_MENU_X.SAN video file, NOT from MSTOVER.NUT.
+ // The O_MENU_X.SAN files (A through O) each contain a full 320x200 FOBJ frame in Frame 0
+ // which is decoded by SmushPlayer and stored in renderBitmap before this function is called.
+ // MSTOVER.NUT is only used in cheat mode (when DAT_0047aba4 != 0).
+ //
+ // From FUN_4147B2: param_3 = (DAT_0047a806 == 0) + 6 = 7 for keyboard mode
+ // Menu structure: 1 title + 6 selectable items = 7 total items
+ static const char *menuItems[] = {
+ "GAME MAIN MENU", // Title (index 0)
+ "START GAME", // Item 0 (index 1)
+ "OPTIONS", // Item 1 (index 2)
+ "CONTINUE INTRO", // Item 2 (index 3)
+ "SHOW TOP PILOTS", // Item 3 (index 4)
+ "SHOW CREDITS", // Item 4 (index 5)
+ "QUIT" // Item 5 (index 6)
+ };
+
+ const int numItemsTotal = 7; // Title + 6 menu options (used for Y calculations)
+ const int numSelectableItems = 6; // Selectable menu options (0-5)
+
+ // The O_MENU_X.SAN video frame is already in renderBitmap from SmushPlayer.
+ // We only draw text and selection highlights on top.
+
+ // From assembly FUN_0041f5ae (low-res mode, DAT_0047a808 < 2):
+ // Center X = 160 for 320px, scale for actual width
+ // Title Y = 46, Item base Y = 69, spacing = 10
+ const int centerX = width / 2; // Center in actual buffer
+ const int titleY = numItemsTotal * -5 + 0x51; // 46
+ const int itemBaseY = numItemsTotal * -5 + 0x68; // 69
+ const int itemSpacing = 10;
+
+ debug(5, "drawMenuOverlay: buffer %dx%d, centerX=%d", width, height, centerX);
+
+ // Use SMALFONT.NUT for menu text rendering
+ NutRenderer *font = _smush_smalfontNut;
+ if (!font) {
+ debug(1, "drawMenuOverlay: font is NULL!");
+ return;
+ }
+
+ // Get the number of characters in the font
+ int numFontChars = font->getNumChars();
+ debug(5, "drawMenuOverlay: font has %d chars", numFontChars);
+
+ // Helper: calculate string width with bounds checking
+ auto getStringWidth = [&](const char *str) -> int {
+ int w = 0;
+ while (*str) {
+ byte c = (byte)*str++;
+ if (c >= 'a' && c <= 'z') c = c - 'a' + 'A';
+ if (c < numFontChars) {
+ w += font->getCharWidth(c);
+ }
+ }
+ return w;
+ };
+
+ // Debug: Check if renderBitmap has video content (non-zero pixels)
+ static int debugFrameCount = 0;
+ if (debugFrameCount < 5) {
+ int nonZeroCount = 0;
+ for (int i = 0; i < MIN(width * height, 1000); i++) {
+ if (renderBitmap[i] != 0) nonZeroCount++;
+ }
+ debug(1, "drawMenuOverlay: frame %d, buffer sample has %d non-zero pixels in first 1000",
+ debugFrameCount, nonZeroCount);
+ debugFrameCount++;
+ }
+
+ // Set up clipRect for the entire rendering area
+ // Use screen dimensions (320x200) for the clip rect, not the video frame dimensions.
+ // The menu text is designed for a 320x200 screen, and the underlying renderBitmap
+ // (from SmushPlayer's virtual screen buffer) is always screen-sized. The width/height
+ // parameters come from _player->_width/_height which may be smaller if the video
+ // frame doesn't cover the full screen, but that would incorrectly clip the menu text.
+ Common::Rect clipRect(0, 0, _vm->_screenWidth, _vm->_screenHeight);
+
+ // Use screen width as the actual pitch for rendering. During SMUSH playback,
+ // SmushPlayer sets vs->pitch = vs->w = _screenWidth (see smush_player.cpp init()).
+ // The 'pitch' parameter comes from _player->_width which may differ from the actual
+ // buffer pitch if the video frame has different dimensions. Using the wrong pitch
+ // would cause character rows to be written at incorrect offsets in the buffer.
+ int actualPitch = _vm->_screenWidth;
+
+ auto drawString = [&](const char *str, int x, int y) {
+ while (*str) {
+ byte c = (byte)*str++;
+ if (c >= 'a' && c <= 'z') c = c - 'a' + 'A';
+
+ // Skip characters outside font range
+ if (c >= numFontChars) {
+ debug(5, "drawMenuOverlay: char %d out of range (max %d)", c, numFontChars);
+ continue;
+ }
+
+ int charW = font->getCharWidth(c);
+
+ // Use NutRenderer's drawCharV7 which properly handles character rendering
+ // col=-1 means use original font colors, hardcodedColors=true, smushColorMode=true
+ if (x >= 0 && y >= 0 && charW > 0) {
+ font->drawCharV7(renderBitmap, clipRect, x, y, actualPitch, -1,
+ kStyleAlignLeft, c, true, true);
+ }
+ x += charW;
+ }
+ };
+
+ // Draw title centered at Y = 46
+ {
+ int titleWidth = getStringWidth(menuItems[0]);
+ int titleX = centerX - titleWidth / 2;
+ drawString(menuItems[0], titleX, titleY);
+ }
+
+ // Draw menu items starting at Y = 69
+ for (int i = 0; i < numSelectableItems; i++) {
+ int itemY = itemBaseY + i * itemSpacing;
+ const char *text = menuItems[i + 1];
+
+ int textWidth = getStringWidth(text);
+ int textX = centerX - textWidth / 2;
+ drawString(text, textX, itemY);
+
+ // Draw selection highlight box around selected item
+ if (i == _menuSelection) {
+ int bracketWidth = textWidth + 12;
+ int bracketHeight = 10;
+ byte highlightColor = 255; // White/bright color for visibility
+
+ int leftX = centerX - bracketWidth / 2;
+ int rightX = centerX + bracketWidth / 2;
+ int topY = itemY - 1;
+ int bottomY = itemY + bracketHeight - 1;
+
+ // Clamp to screen bounds (use screen dimensions, not video frame dimensions)
+ int screenW = _vm->_screenWidth;
+ int screenH = _vm->_screenHeight;
+ if (leftX < 0) leftX = 0;
+ if (rightX >= screenW) rightX = screenW - 1;
+ if (topY < 0) topY = 0;
+ if (bottomY >= screenH) bottomY = screenH - 1;
+
+ // Draw selection rectangle using actualPitch (screen width)
+ for (int x = leftX; x <= rightX && x < screenW; x++) {
+ if (topY >= 0 && topY < screenH)
+ renderBitmap[topY * actualPitch + x] = highlightColor;
+ if (bottomY >= 0 && bottomY < screenH)
+ renderBitmap[bottomY * actualPitch + x] = highlightColor;
+ }
+ for (int py = topY; py <= bottomY && py < screenH; py++) {
+ if (leftX >= 0 && leftX < screenW)
+ renderBitmap[py * actualPitch + leftX] = highlightColor;
+ if (rightX >= 0 && rightX < screenW)
+ renderBitmap[py * actualPitch + rightX] = highlightColor;
+ }
+ }
+ }
+}
+
+int InsaneRebel2::runMainMenu() {
+ // Main menu loop - emulates FUN_004147B2
+ // Returns:
+ // kMenuNewGame (2) = Start new game
+ // kMenuContinue (4) = Continue game (level select)
+ // kMenuCredits (1) = Show credits then return to menu
+ // 0 = Quit game
+
+ debug("Rebel2: Entering main menu");
+
+ resetMenu();
+ _gameState = kStateMainMenu;
+
+ // Get the SmushPlayer from ScummEngine_v7
+ // Note: _player isn't set until SmushPlayer::initAudio() is called during playback
+ SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
+
+ // Main menu loop
+ while (!_vm->shouldQuit()) {
+ // Reset video finish flag before playing menu
+ _vm->_smushVideoShouldFinish = false;
+
+ // Select and play a random menu video
+ Common::String menuVideo = getRandomMenuVideo();
+ debug("Rebel2: Playing menu video: %s", menuVideo.c_str());
+
+ // Set video flags for menu (0x20 = intro/menu flag)
+ // This tells procPostRendering we're in menu mode
+ splayer->setCurVideoFlags(0x20);
+
+ // Play the menu video
+ // Input is processed in procPostRendering during playback
+ // When user confirms selection, _vm->_smushVideoShouldFinish is set
+ splayer->play(menuVideo.c_str(), 12);
+
+ // Check for quit
+ if (_vm->shouldQuit()) {
+ return 0;
+ }
+
+ // If video ended naturally (not by selection), loop back
+ if (!_vm->_smushVideoShouldFinish) {
+ // Video ended without selection (reached end or ESC during video)
+ // Continue looping menu videos
+ continue;
+ }
+
+ // Clear the flag
+ _vm->_smushVideoShouldFinish = false;
+
+ // A selection was made - process it
+ debug("Rebel2: Menu video ended with selection=%d", _menuSelection);
+
+ // Process the menu result based on current selection
+ // Menu items (from FUN_004147B2 disassembly):
+ // case 0: return 2 (New Game)
+ // case 1: return 4 (Continue)
+ // case 2: Options menu (stays in loop)
+ // case 3: return 0 (Exit)
+ // case 4: Unknown function
+ // case 5: Credits (play O_CREDIT.SAN, then return 1)
+ // case 6: Quit (stop video, exit)
+ switch (_menuSelection) {
+ case 0: // New Game
+ debug("Rebel2: New Game selected");
+ _gameState = kStateGameplay;
+ return kMenuNewGame;
+
+ case 1: // Continue
+ debug("Rebel2: Continue selected");
+ _gameState = kStateLevelSelect;
+ return kMenuContinue;
+
+ case 2: // Options
+ debug("Rebel2: Options selected");
+ // TODO: Show options menu (FUN_00406ed2)
+ // For now, just continue menu loop
+ break;
+
+ case 3: // Exit (back to title/intro)
+ debug("Rebel2: Exit selected");
+ // Return to menu loop
+ break;
+
+ case 4: // Unknown function (FUN_00420116)
+ debug("Rebel2: Unknown menu item 4 selected");
+ break;
+
+ case 5: // Credits
+ debug("Rebel2: Credits selected");
+ _gameState = kStateCredits;
+ splayer->setCurVideoFlags(0x20);
+ splayer->play("OPEN/O_CREDIT.SAN", 12);
+ _gameState = kStateMainMenu;
+ // After credits, return to menu
+ break;
+
+ case 6: // Quit
+ debug("Rebel2: Quit selected");
+ return 0;
+
+ default:
+ debug("Rebel2: Unknown menu selection %d", _menuSelection);
+ break;
+ }
+ }
+
+ return 0;
+}
+
} // End of namespace Scumm
diff --git a/engines/scumm/insane/insane_rebel.h b/engines/scumm/insane/insane_rebel.h
index ced8bb6e441..bf4ffa094cb 100644
--- a/engines/scumm/insane/insane_rebel.h
+++ b/engines/scumm/insane/insane_rebel.h
@@ -47,6 +47,54 @@ public:
InsaneRebel2(ScummEngine_v7 *scumm);
~InsaneRebel2();
+ // ======================= Menu System =======================
+ // Main game states (emulates retail state machine from FUN_004142BD)
+ enum GameState {
+ kStateIntro = 0, // Stage 0: Intro/Credits sequence
+ kStateMainMenu = 1, // Stage 1: Main menu (FUN_004147B2)
+ kStateLevelSelect = 2,// Stage 2: Level selection (FUN_00414A41)
+ kStateBriefing = 3, // Stage 3: Mission briefing (FUN_00415CF8)
+ kStateGameplay = 4, // Stage 4: Gameplay (FUN_00416787)
+ kStateCredits = 5, // Credits sequence
+ kStateQuit = 6 // Exit game
+ };
+
+ // Menu selection results (return values from FUN_004147B2)
+ enum MenuResult {
+ kMenuNewGame = 2, // case 0: New Game
+ kMenuContinue = 4, // case 1: Continue
+ kMenuOptions = 0, // case 2: Options (stays in menu)
+ kMenuExit = 0, // case 3: Exit to title
+ kMenuUnknown = 0, // case 4: Unknown function
+ kMenuCredits = 1, // case 5: Show credits
+ kMenuQuit = 0 // case 6: Quit game
+ };
+
+ GameState _gameState; // Current game state
+ int _menuSelection; // Current menu item (0-6), mirrors DAT_00459988
+ int _menuItemCount; // Number of menu items (7 for main menu)
+ int _menuInactivityTimer; // Timeout counter (300 frames = ~10 sec)
+ int _lastMenuVariant; // Last random menu video shown (DAT_00482400)
+ int _menuRepeatDelay; // Delay for key repeat (DAT_00459ce0)
+ bool _levelUnlocked[16]; // Which levels are available (progress flags)
+
+ // Run the main menu loop - returns when game should start or quit
+ // This is the main entry point called from ScummEngine::go()
+ int runMainMenu();
+
+ // Process menu input (keyboard/mouse) - returns selected item or -1
+ int processMenuInput();
+
+ // Draw menu overlay (selection highlight) on current frame
+ void drawMenuOverlay(byte *renderBitmap, int pitch, int width, int height);
+
+ // Get random menu video filename (emulates FUN_0041FDC8)
+ Common::String getRandomMenuVideo();
+
+ // Reset menu state for fresh start
+ void resetMenu();
+ // =============================================================
+
NutRenderer *_smush_cockpitNut;
// Font used for HUD score/lives/damage display (SMALFONT.NUT)
@@ -55,6 +103,24 @@ public:
// Font used for opcode 9 text/subtitle rendering (DIHIFONT / TALKFONT)
SmushFont *_rebelMsgFont;
+
+ // Menu system fonts (from info.md - FUN_403BD0 font loading)
+ // Low resolution mode font list (stored in DAT_00485058 linked list):
+ // Font 0 (^f00): TALKFONT.NUT - Default menu font
+ // Font 1 (^f01): SMALFONT.NUT - Small font (for format code switching)
+ // Font 2 (^f02): TITLFONT.NUT - Title font
+ // Font 3 (^f03): POVFONT.NUT - POV font
+ NutRenderer *_smush_talkfontNut; // Font 0 - primary menu font (DAT_00485058)
+ NutRenderer *_smush_smalfontNut; // Font 1 - small font for ^f01 switching
+ NutRenderer *_smush_titlefontNut; // Font 2 - title font
+
+ // SmushFont for menu text rendering (uses SMALFONT.NUT with proper string drawing)
+ SmushFont *_menuFont;
+
+ // MSTOVER.NUT - Mouse Over background overlay (NOT a cursor!)
+ // Loaded into DAT_0047aba8 and rendered via FUN_004236e0 as background
+ NutRenderer *_smush_mouseoverNut;
+
bool _introCursorPushed; // true when we've pushed an invisible cursor for intro
diff --git a/engines/scumm/nut_renderer.h b/engines/scumm/nut_renderer.h
index 8077816ca35..d88d0b1f572 100644
--- a/engines/scumm/nut_renderer.h
+++ b/engines/scumm/nut_renderer.h
@@ -75,6 +75,8 @@ public:
int getCharWidth(byte c) const;
int getCharHeight(byte c) const;
const byte *getCharData(byte c);
+ byte getCharTransparency(byte c) const { return _chars[c].transparency; }
+ byte getBpp() const { return _bpp; }
int getFontHeight() const { return _fontHeight; }
};
diff --git a/engines/scumm/scumm.cpp b/engines/scumm/scumm.cpp
index d29e0da93c1..9f868c127cf 100644
--- a/engines/scumm/scumm.cpp
+++ b/engines/scumm/scumm.cpp
@@ -2675,21 +2675,45 @@ int ScummEngine::getTalkSpeed() {
Common::Error ScummEngine::go() {
#ifdef ENABLE_SCUMM_7_8
if (_game.id == GID_REBEL2) {
+ ScummEngine_v7 *vm7 = (ScummEngine_v7 *)this;
+ InsaneRebel2 *rebel = (InsaneRebel2 *)vm7->getInsane();
+
// Use 12 FPS as default, same as The Dig. FT uses 10.
// Since we don't have the standard scripts initializing this, we pass it here.
if (_game.features & GF_DEMO) {
- ((ScummEngine_v7 *)this)->_splayer->play("OPEN/O_DEMO.SAN", 12);
- } else {
- // Play Level 1 intro cinematic (01BEG.SAN)
- // Set video flags to 0x20 to mark this as a non-interactive intro sequence
- // This flag tells procPostRendering to skip HUD/crosshair rendering
- ((ScummEngine_v7 *)this)->_splayer->setCurVideoFlags(0x20);
- ((ScummEngine_v7 *)this)->_splayer->play("LEV01/01BEG.SAN", 12);
-
- // After intro completes, start the gameplay mission video (01P01.SAN)
- // Clear the intro flag so HUD renders during gameplay
- ((ScummEngine_v7 *)this)->_splayer->setCurVideoFlags(0);
- ((ScummEngine_v7 *)this)->_splayer->play("LEV01/01P01.SAN", 12);
+ vm7->_splayer->play("OPEN/O_DEMO.SAN", 12);
+ return Common::kNoError;
+ }
+
+ // Full game: Run the main menu loop
+ // This emulates the retail game flow from FUN_004142BD
+ while (!shouldQuit()) {
+ // Run main menu and get result
+ int menuResult = rebel->runMainMenu();
+
+ if (menuResult == 0 || shouldQuit()) {
+ // Quit selected
+ break;
+ }
+
+ if (menuResult == InsaneRebel2::kMenuNewGame ||
+ menuResult == InsaneRebel2::kMenuContinue) {
+ // Start gameplay
+ // Play Level 1 intro cinematic (01BEG.SAN)
+ // Set video flags to 0x20 to mark this as a non-interactive intro sequence
+ vm7->_splayer->setCurVideoFlags(0x20);
+ vm7->_splayer->play("LEV01/01BEG.SAN", 12);
+
+ if (shouldQuit()) break;
+
+ // After intro completes, start the gameplay mission video (01P01.SAN)
+ // Clear the intro flag so HUD renders during gameplay
+ vm7->_splayer->setCurVideoFlags(0);
+ vm7->_splayer->play("LEV01/01P01.SAN", 12);
+
+ // After gameplay, return to menu
+ // TODO: Handle level progression, game over, etc.
+ }
}
return Common::kNoError;
}
diff --git a/engines/scumm/scumm_v7.h b/engines/scumm/scumm_v7.h
index 101e58a9611..4c76060c16b 100644
--- a/engines/scumm/scumm_v7.h
+++ b/engines/scumm/scumm_v7.h
@@ -105,6 +105,7 @@ public:
void displayDialog() override;
bool isSmushActive() override { return _smushActive; }
bool isInsaneActive() override { return _insane ? _insane->isInsaneActive() : false; }
+ Insane *getInsane() { return _insane; }
void removeBlastTexts() override;
void restoreBlastTextsRects();
diff --git a/engines/scumm/smush/smush_player.cpp b/engines/scumm/smush/smush_player.cpp
index a0c3c831bbb..df67aaff161 100644
--- a/engines/scumm/smush/smush_player.cpp
+++ b/engines/scumm/smush/smush_player.cpp
@@ -401,7 +401,18 @@ void SmushPlayer::handleIACT(int32 subSize, Common::SeekableReadStream &b) {
int unknown = b.readSint16LE();
int userId = b.readUint16LE();
- if ((code != 8) && (flags != 46)) {
+ debug("SmushPlayer::handleIACT: code=%d flags=%d unknown=%d userId=%d subSize=%d",
+ code, flags, unknown, userId, subSize);
+
+ // Route to procIACT for:
+ // 1. Non-audio IACT (code != 8 or flags != 46)
+ // 2. RA2 menu/graphics IACT (userId >= 1000 indicates graphics data, not audio)
+ bool isAudioIACT = (code == 8) && (flags == 46);
+ bool isRA2GraphicsIACT = (_vm->_game.id == GID_REBEL2) && (userId >= 1000);
+
+ if (!isAudioIACT || isRA2GraphicsIACT) {
+ debug("SmushPlayer::handleIACT: Routing to procIACT (isAudioIACT=%d, isRA2GraphicsIACT=%d)",
+ isAudioIACT, isRA2GraphicsIACT);
_vm->_insane->procIACT(_dst, 0, 0, 0, b, 0, 0, code, flags, unknown, userId);
return;
}
@@ -1003,8 +1014,15 @@ void SmushPlayer::handleFrame(int32 frameSize, Common::SeekableReadStream &b) {
_vm->_insane->procPostRendering(_dst, 0, 0, 0, _frame, _nbframes-1);
}
+ // Debug: Check if updateScreen is being called
+ if (_vm->_game.id == GID_REBEL2 && _frame < 3) {
+ debug("SmushPlayer: frame=%d _width=%d _height=%d _dst=%p", _frame, _width, _height, (void*)_dst);
+ }
+
if (_width != 0 && _height != 0) {
updateScreen();
+ } else if (_vm->_game.id == GID_REBEL2 && _frame < 3) {
+ debug("SmushPlayer: SKIPPING updateScreen (width=%d height=%d)", _width, _height);
}
_frame++;
@@ -1012,6 +1030,7 @@ void SmushPlayer::handleFrame(int32 frameSize, Common::SeekableReadStream &b) {
void SmushPlayer::handleAnimHeader(int32 subSize, Common::SeekableReadStream &b) {
debugC(DEBUG_SMUSH, "SmushPlayer::handleAnimHeader()");
+ debug("SmushPlayer::handleAnimHeader: subSize=%d", subSize);
assert(subSize >= 0x300 + 6);
byte *headerContent = (byte *)malloc(subSize * sizeof(byte));
@@ -1046,6 +1065,17 @@ void SmushPlayer::handleAnimHeader(int32 subSize, Common::SeekableReadStream &b)
_width = READ_LE_UINT16(&headerContent[4]);
_height = READ_LE_UINT16(&headerContent[6]);
+ debug("SmushPlayer::handleAnimHeader: nbframes=%d width=%d height=%d", _nbframes, _width, _height);
+
+ // RA2 menu videos (O_MENU*.SAN) report width/height=0 in AHDR, but they DO have
+ // FOBJ frames with full 320x200 images. The FOBJ chunks contain the correct dimensions.
+ // We set default screen dimensions here so updateScreen() gets called and the
+ // _frameBuffer allocation in handleStore/handleFetch uses correct size.
+ if (_vm->_game.id == GID_REBEL2 && _width == 0 && _height == 0) {
+ _width = _vm->_screenWidth; // 320
+ _height = _vm->_screenHeight; // 200
+ debug("SmushPlayer::handleAnimHeader: RA2 AHDR has 0x0 dims - using screen size %dx%d", _width, _height);
+ }
free(headerContent);
}
Commit: f91918ff34466f60423d8e46e0d1ba8f9b5cf8e7
https://github.com/scummvm/scummvm/commit/f91918ff34466f60423d8e46e0d1ba8f9b5cf8e7
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:08:52+02:00
Commit Message:
SCUMM: RA2: Implement additional menus
Changed paths:
engines/scumm/insane/insane_rebel.cpp
engines/scumm/insane/insane_rebel.h
engines/scumm/scumm.cpp
diff --git a/engines/scumm/insane/insane_rebel.cpp b/engines/scumm/insane/insane_rebel.cpp
index b6e76998ece..73c49d9c4d4 100644
--- a/engines/scumm/insane/insane_rebel.cpp
+++ b/engines/scumm/insane/insane_rebel.cpp
@@ -24,8 +24,10 @@
#include "engines/engine.h"
#include "common/system.h"
#include "common/memstream.h"
+#include "common/events.h"
#include "graphics/cursorman.h"
+#include "graphics/wincursor.h"
#include "scumm/actor.h"
#include "scumm/file.h"
@@ -259,10 +261,24 @@ InsaneRebel2::InsaneRebel2(ScummEngine_v7 *scumm) {
for (i = 0; i < 16; i++) {
_levelUnlocked[i] = (i == 0); // Only level 1 unlocked initially
}
+
+ // Initialize level selection system
+ _levelSelection = 0; // First level selected
+ _levelItemCount = 2; // Level 1 + MAIN MENU (will grow as more levels implemented)
+ _selectedLevel = 1; // Default selected level
+
+ // Initialize menu input capture system
+ _menuInputActive = false;
+
+ // Register as EventObserver to capture input events before ScummEngine consumes them
+ _vm->_system->getEventManager()->getEventDispatcher()->registerObserver(this, 1, false);
}
InsaneRebel2::~InsaneRebel2() {
+ // Unregister EventObserver
+ _vm->_system->getEventManager()->getEventDispatcher()->unregisterObserver(this);
+
terminateAudio();
delete _rebelMsgFont;
delete _menuFont;
@@ -273,6 +289,31 @@ InsaneRebel2::~InsaneRebel2() {
delete _smush_mouseoverNut;
}
+bool InsaneRebel2::notifyEvent(const Common::Event &event) {
+ // Capture menu-related input events when menu input is active.
+ // This is called before ScummEngine::parseEvents() consumes events,
+ // so we can reliably capture keyboard/mouse input for menu navigation.
+ if (!_menuInputActive)
+ return false; // Not capturing, let normal processing occur
+
+ switch (event.type) {
+ case Common::EVENT_KEYDOWN:
+ case Common::EVENT_LBUTTONDOWN:
+ case Common::EVENT_MOUSEMOVE:
+ case Common::EVENT_QUIT:
+ case Common::EVENT_RETURN_TO_LAUNCHER:
+ // Queue these events for processing in processMenuInput()
+ _menuEventQueue.push(event);
+ break;
+ default:
+ break;
+ }
+
+ // Return false to allow ScummEngine to also process the event
+ // (needed for quit handling, etc.)
+ return false;
+}
+
// Score lookup tables (from DAT_0047e0fe, DAT_0047e100, DAT_0047e102)
// These are indexed by: DAT_0047a7fa * 0x242 + DAT_0047a7f8 * 0x22
// For simplicity, we use fixed values based on difficulty level
@@ -1224,13 +1265,73 @@ void InsaneRebel2::iactRebel2Opcode9(byte *renderBitmap, Common::SeekableReadStr
// The original uses colors from the palette, commonly white or yellow for subtitles
int16 textColor = 255;
- // Draw the text string
+ // RA2 fonts (like DIHIFONT.NUT) have only 58 characters starting at ASCII 32 (space).
+ // We need to convert ASCII codes to font indices by subtracting 32.
+ // Character mapping: font index = ASCII code - 32
+ // So 'D' (68) becomes index 36, 'A' (65) becomes index 33, etc.
+ // IMPORTANT: Skip format codes (^f00, ^c255, ^l) which TextRenderer parses as raw ASCII.
+ char convertedText[512];
+ int srcLen = strlen(textStr);
+ int dstIdx = 0;
+ int numChars = _rebelMsgFont->getNumChars();
+
+ for (int i = 0; i < srcLen && dstIdx < (int)sizeof(convertedText) - 1; i++) {
+ byte ch = (byte)textStr[i];
+
+ // Check for format codes (^f, ^c, ^l) - keep them as raw ASCII
+ if (ch == '^' && i + 1 < srcLen) {
+ byte next = (byte)textStr[i + 1];
+ if (next == 'f' && i + 3 < srcLen) {
+ // ^fXX - font switch (4 chars total)
+ convertedText[dstIdx++] = textStr[i++]; // ^
+ convertedText[dstIdx++] = textStr[i++]; // f
+ convertedText[dstIdx++] = textStr[i++]; // X
+ convertedText[dstIdx++] = textStr[i]; // X
+ continue;
+ } else if (next == 'c' && i + 4 < srcLen) {
+ // ^cXXX - color switch (5 chars total)
+ convertedText[dstIdx++] = textStr[i++]; // ^
+ convertedText[dstIdx++] = textStr[i++]; // c
+ convertedText[dstIdx++] = textStr[i++]; // X
+ convertedText[dstIdx++] = textStr[i++]; // X
+ convertedText[dstIdx++] = textStr[i]; // X
+ continue;
+ } else if (next == 'l') {
+ // ^l - line break marker (2 chars)
+ convertedText[dstIdx++] = textStr[i++]; // ^
+ convertedText[dstIdx++] = textStr[i]; // l
+ continue;
+ } else if (next == '^') {
+ // ^^ - escaped caret (becomes single ^)
+ i++; // Skip first ^
+ // Fall through to convert second ^ as normal char
+ ch = '^';
+ }
+ }
+
+ // Convert regular characters from ASCII to font index
+ // First convert lowercase to uppercase (the font likely only has uppercase)
+ if (ch >= 'a' && ch <= 'z') {
+ ch = ch - 'a' + 'A'; // Convert to uppercase
+ }
+
+ if (ch >= 32 && ch < (byte)(32 + numChars)) {
+ convertedText[dstIdx++] = ch - 32; // Convert ASCII to font index
+ } else if (ch == '\n' || ch == '\r') {
+ convertedText[dstIdx++] = ch; // Keep control characters as-is
+ } else {
+ convertedText[dstIdx++] = 0; // Replace invalid characters with space (index 0)
+ }
+ }
+ convertedText[dstIdx] = '\0';
+
+ // Draw the text string (with converted character indices)
if (textFlags & 0x04) {
// Word-wrapped text
- _rebelMsgFont->drawStringWrap(textStr, renderBitmap, clipRect, posX, posY, textColor, styleFlags);
+ _rebelMsgFont->drawStringWrap(convertedText, renderBitmap, clipRect, posX, posY, textColor, styleFlags);
} else {
// Single-line text
- _rebelMsgFont->drawString(textStr, renderBitmap, clipRect, posX, posY, textColor, styleFlags);
+ _rebelMsgFont->drawString(convertedText, renderBitmap, clipRect, posX, posY, textColor, styleFlags);
}
debug("Rebel2 Opcode 9: Rendered subtitle at (%d,%d) flags=0x%x clip=(%d,%d,%d,%d)",
@@ -1851,31 +1952,40 @@ void InsaneRebel2::procPostRendering(byte *renderBitmap, int32 codecparam, int32
// Check if we're in menu mode (menu state + intro flag)
bool menuMode = (introPlaying && _gameState == kStateMainMenu);
+ bool levelSelectMode = (introPlaying && _gameState == kStateLevelSelect);
+
+ // Handle level selection input and rendering
+ if (levelSelectMode) {
+ // Show the standard Windows arrow cursor (same as menu)
+ Graphics::Cursor *cursor = Graphics::makeDefaultWinCursor();
+ CursorMan.replaceCursor(cursor);
+ delete cursor;
+ CursorMan.showMouse(true);
+
+ // Process level selection input
+ int selection = processLevelSelectInput();
+
+ // Draw level selection overlay
+ drawLevelSelectOverlay(renderBitmap, pitch, width, height);
+
+ // If a selection was confirmed, signal video to stop
+ if (selection >= 0) {
+ debug("Rebel2: Level selection confirmed: %d", selection);
+ _vm->_smushVideoShouldFinish = true;
+ }
+
+ // Skip normal HUD rendering in level select mode
+ return;
+ }
// Handle menu input and rendering if in menu mode
if (menuMode) {
// The original game uses the standard Windows arrow cursor (IDC_ARROW)
// loaded via LoadCursorA(NULL, 0x7f00) in FUN_420C70.decompiled.txt
// MSTOVER.NUT is a background overlay, NOT a cursor
- static const byte arrowCursor[11 * 16] = {
- 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0,
- 1, 255, 1, 0, 0, 0, 0, 0, 0, 0, 0,
- 1, 255, 255, 1, 0, 0, 0, 0, 0, 0, 0,
- 1, 255, 255, 255, 1, 0, 0, 0, 0, 0, 0,
- 1, 255, 255, 255, 255, 1, 0, 0, 0, 0, 0,
- 1, 255, 255, 255, 255, 255, 1, 0, 0, 0, 0,
- 1, 255, 255, 255, 255, 255, 255, 1, 0, 0, 0,
- 1, 255, 255, 255, 255, 255, 255, 255, 1, 0, 0,
- 1, 255, 255, 255, 255, 255, 255, 255, 255, 1, 0,
- 1, 255, 255, 255, 255, 255, 1, 1, 1, 1, 1,
- 1, 255, 255, 1, 255, 255, 1, 0, 0, 0, 0,
- 1, 255, 1, 0, 1, 255, 255, 1, 0, 0, 0,
- 1, 1, 0, 0, 1, 255, 255, 1, 0, 0, 0,
- 1, 0, 0, 0, 0, 1, 255, 255, 1, 0, 0,
- 0, 0, 0, 0, 0, 1, 255, 255, 1, 0, 0,
- 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0,
- };
- CursorMan.replaceCursor(arrowCursor, 11, 16, 0, 0, 0);
+ Graphics::Cursor *cursor = Graphics::makeDefaultWinCursor();
+ CursorMan.replaceCursor(cursor);
+ delete cursor;
CursorMan.showMouse(true);
// Process menu input during each frame
@@ -2603,18 +2713,22 @@ Common::String InsaneRebel2::getRandomMenuVideo() {
int InsaneRebel2::processMenuInput() {
// Emulates FUN_0041f5ae menu input handling
// Returns: -1 = no action, 0-6 = menu item selected
+ //
+ // Events are captured by notifyEvent() (EventObserver) which runs before
+ // ScummEngine::parseEvents() consumes them. This ensures we don't miss
+ // any input events even though we only process them on video frames.
- Common::Event event;
int result = -1;
- // Check for key/mouse events
- Common::EventManager *eventMan = _vm->_system->getEventManager();
-
- // Get current key state from _vm's VAR system or direct polling
- // Key codes: 0x148 = Up, 0x150 = Down, 0x1B = ESC, 0x0D = Enter
+ // Menu item Y positions (low-res 320x200 mode):
+ // From assembly: baseY = numItems * -5 + 0x68 = 7 * -5 + 104 = 69
+ // Items at Y = 69, 79, 89, 99, 109, 119 with spacing of 10
+ const int baseY = 69;
+ const int itemHeight = 10;
- // Poll for keyboard input
- while (eventMan->pollEvent(event)) {
+ // Process events from the queue (populated by notifyEvent)
+ while (!_menuEventQueue.empty()) {
+ Common::Event event = _menuEventQueue.pop();
switch (event.type) {
case Common::EVENT_KEYDOWN:
_menuInactivityTimer = 0; // Reset inactivity timer on any input
@@ -2622,26 +2736,20 @@ int InsaneRebel2::processMenuInput() {
switch (event.kbd.keycode) {
case Common::KEYCODE_UP:
// Navigate up (wrap around)
- if (_menuRepeatDelay == 0) {
- _menuSelection--;
- if (_menuSelection < 0) {
- _menuSelection = _menuItemCount - 1;
- }
- _menuRepeatDelay = 3; // Delay before repeat
- debug("Menu: Selection changed to %d (UP)", _menuSelection);
+ _menuSelection--;
+ if (_menuSelection < 0) {
+ _menuSelection = _menuItemCount - 1;
}
+ debug("Menu: Selection changed to %d (UP)", _menuSelection);
break;
case Common::KEYCODE_DOWN:
// Navigate down (wrap around)
- if (_menuRepeatDelay == 0) {
- _menuSelection++;
- if (_menuSelection >= _menuItemCount) {
- _menuSelection = 0;
- }
- _menuRepeatDelay = 3;
- debug("Menu: Selection changed to %d (DOWN)", _menuSelection);
+ _menuSelection++;
+ if (_menuSelection >= _menuItemCount) {
+ _menuSelection = 0;
}
+ debug("Menu: Selection changed to %d (DOWN)", _menuSelection);
break;
case Common::KEYCODE_RETURN:
@@ -2655,7 +2763,7 @@ int InsaneRebel2::processMenuInput() {
case Common::KEYCODE_ESCAPE:
// ESC - Exit/Quit (return special value)
- result = 6; // Quit option
+ result = 5; // Quit option (index 5)
debug("Menu: ESC pressed - quit");
break;
@@ -2665,39 +2773,46 @@ int InsaneRebel2::processMenuInput() {
break;
case Common::EVENT_LBUTTONDOWN:
- // Mouse click - check if over a menu item
- // Menu items are vertically stacked, centered at X=160
- // Y positions from FUN_41F5AE: param_3 * -5 + local_c * 10 + 0x68
- // With param_3 = 7: base Y = 69, items at Y = 69, 79, 89, 99, 109, 119
_menuInactivityTimer = 0;
+
{
+ // Get mouse position from the event
+ int mouseX = event.mouse.x;
int mouseY = event.mouse.y;
- int baseY = 69; // First item Y position (low-res)
- int itemHeight = 10;
- // Check which item was clicked
+ debug("Menu: Click detected at (%d, %d)", mouseX, mouseY);
+
+ // Check which item was clicked (larger hit area for better usability)
for (int i = 0; i < _menuItemCount; i++) {
int itemY = baseY + i * itemHeight;
- if (mouseY >= itemY - 5 && mouseY <= itemY + 5) {
+ // Use a larger vertical hit area (full item height)
+ if (mouseY >= itemY - 4 && mouseY < itemY + 6) {
_menuSelection = i;
result = i;
- debug("Menu: Item %d clicked at Y=%d", i, mouseY);
+ debug("Menu: Item %d clicked at Y=%d (itemY=%d)", i, mouseY, itemY);
break;
}
}
}
break;
+ case Common::EVENT_MOUSEMOVE:
+ // Update mouse position for hover effects (optional)
+ _vm->_mouse.x = event.mouse.x;
+ _vm->_mouse.y = event.mouse.y;
+ break;
+
+ case Common::EVENT_QUIT:
+ case Common::EVENT_RETURN_TO_LAUNCHER:
+ // Handle quit request
+ result = 5; // Quit option
+ break;
+
default:
break;
}
}
- // Handle key repeat delay
- if (_menuRepeatDelay > 0) {
- _menuRepeatDelay--;
- }
-
return result;
}
@@ -2876,6 +2991,10 @@ int InsaneRebel2::runMainMenu() {
resetMenu();
_gameState = kStateMainMenu;
+ // Enable menu input capture via EventObserver
+ _menuInputActive = true;
+ while (!_menuEventQueue.empty()) _menuEventQueue.pop(); // Clear any stale events
+
// Get the SmushPlayer from ScummEngine_v7
// Note: _player isn't set until SmushPlayer::initAudio() is called during playback
SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
@@ -2900,6 +3019,7 @@ int InsaneRebel2::runMainMenu() {
// Check for quit
if (_vm->shouldQuit()) {
+ _menuInputActive = false;
return 0;
}
@@ -2926,14 +3046,16 @@ int InsaneRebel2::runMainMenu() {
// case 5: Credits (play O_CREDIT.SAN, then return 1)
// case 6: Quit (stop video, exit)
switch (_menuSelection) {
- case 0: // New Game
- debug("Rebel2: New Game selected");
- _gameState = kStateGameplay;
- return kMenuNewGame;
+ case 0: // Start Game -> Level Selection
+ debug("Rebel2: Start Game selected - going to level selection");
+ _gameState = kStateLevelSelect;
+ _menuInputActive = false;
+ return kMenuContinue; // Go to level selection
- case 1: // Continue
- debug("Rebel2: Continue selected");
+ case 1: // Continue (same as Start Game for now)
+ debug("Rebel2: Continue selected - going to level selection");
_gameState = kStateLevelSelect;
+ _menuInputActive = false;
return kMenuContinue;
case 2: // Options
@@ -2962,6 +3084,7 @@ int InsaneRebel2::runMainMenu() {
case 6: // Quit
debug("Rebel2: Quit selected");
+ _menuInputActive = false;
return 0;
default:
@@ -2970,7 +3093,330 @@ int InsaneRebel2::runMainMenu() {
}
}
+ _menuInputActive = false;
return 0;
}
+// ==================== Level Selection Menu ====================
+// Emulates FUN_00414A41 - Level selection menu
+// For now, only Level 1 is available. This will be expanded later.
+
+int InsaneRebel2::runLevelSelect() {
+ // Level selection menu loop - emulates FUN_00414A41
+ // Returns:
+ // kLevelSelectPlay (1) = Play selected level
+ // kLevelSelectBack (0) = Return to main menu
+ // kLevelSelectQuit (2) = Quit game
+
+ debug("Rebel2: Entering level selection");
+
+ // Enable menu input capture via EventObserver and clear any stale events
+ _menuInputActive = true;
+ while (!_menuEventQueue.empty()) _menuEventQueue.pop();
+
+ // Initialize level selection state
+ _levelSelection = 0;
+ _levelItemCount = 2; // Selectable items: Level 1, MAIN MENU
+ _selectedLevel = 1; // Default to level 1
+ _menuRepeatDelay = 0;
+ _gameState = kStateLevelSelect;
+
+ // Get the SmushPlayer
+ SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
+
+ // Level selection loop - we'll reuse the menu video as background
+ // In the original, this uses the same O_MENU_X.SAN videos
+ while (!_vm->shouldQuit()) {
+ _vm->_smushVideoShouldFinish = false;
+
+ // Use a menu video as background for level selection
+ Common::String menuVideo = getRandomMenuVideo();
+ debug("Rebel2: Playing level select background: %s", menuVideo.c_str());
+
+ // Set video flags for menu mode
+ splayer->setCurVideoFlags(0x20);
+
+ // Play the menu video - input is processed in procPostRendering
+ splayer->play(menuVideo.c_str(), 12);
+
+ if (_vm->shouldQuit()) {
+ _menuInputActive = false;
+ return kLevelSelectQuit;
+ }
+
+ // If video ended without selection, continue looping
+ if (!_vm->_smushVideoShouldFinish) {
+ continue;
+ }
+
+ _vm->_smushVideoShouldFinish = false;
+
+ debug("Rebel2: Level selection made: %d", _levelSelection);
+
+ // Process level selection
+ // Menu items (based on FUN_414A41 analysis):
+ // 0: Level 1 (CHAPTER 1)
+ // 1: MAIN MENU (back)
+ // Future: More levels, NEW PILOT, DELETE PILOT, etc.
+ switch (_levelSelection) {
+ case 0: // Level 1
+ debug("Rebel2: Level 1 selected");
+ _selectedLevel = 1;
+ _menuInputActive = false;
+ return kLevelSelectPlay;
+
+ case 1: // Main Menu (back)
+ debug("Rebel2: Back to main menu selected");
+ _menuInputActive = false;
+ return kLevelSelectBack;
+
+ default:
+ // For now, any other selection defaults to Level 1
+ if (_levelSelection < _levelItemCount - 1) {
+ debug("Rebel2: Level %d selected (defaulting to 1)", _levelSelection + 1);
+ _selectedLevel = 1; // Only level 1 available for now
+ _menuInputActive = false;
+ return kLevelSelectPlay;
+ }
+ break;
+ }
+ }
+
+ _menuInputActive = false;
+ return kLevelSelectQuit;
+}
+
+int InsaneRebel2::processLevelSelectInput() {
+ // Process input for level selection menu
+ // Similar to processMenuInput but for level selection
+ // Returns: -1 = no action, 0+ = item selected
+ //
+ // Events are captured by notifyEvent() - see processMenuInput for details.
+
+ int result = -1;
+
+ // Level menu Y positions (similar to main menu)
+ // Using same formula: base Y = numItemsTotal * -5 + 104
+ // numItemsTotal = 3 (title + 2 selectable items)
+ const int numItemsTotal = 3;
+ const int baseY = numItemsTotal * -5 + 104; // 89
+ const int itemHeight = 10;
+
+ // Process events from the queue (populated by notifyEvent)
+ while (!_menuEventQueue.empty()) {
+ Common::Event event = _menuEventQueue.pop();
+ switch (event.type) {
+ case Common::EVENT_KEYDOWN:
+ switch (event.kbd.keycode) {
+ case Common::KEYCODE_UP:
+ _levelSelection--;
+ if (_levelSelection < 0) {
+ _levelSelection = _levelItemCount - 1;
+ }
+ debug("LevelSelect: Selection changed to %d (UP)", _levelSelection);
+ break;
+
+ case Common::KEYCODE_DOWN:
+ _levelSelection++;
+ if (_levelSelection >= _levelItemCount) {
+ _levelSelection = 0;
+ }
+ debug("LevelSelect: Selection changed to %d (DOWN)", _levelSelection);
+ break;
+
+ case Common::KEYCODE_RETURN:
+ case Common::KEYCODE_KP_ENTER:
+ if (_levelSelection >= 0 && _levelSelection < _levelItemCount) {
+ result = _levelSelection;
+ debug("LevelSelect: Item %d selected (ENTER)", _levelSelection);
+ }
+ break;
+
+ case Common::KEYCODE_ESCAPE:
+ // ESC - Back to main menu
+ result = _levelItemCount - 1; // Last item is "MAIN MENU"
+ debug("LevelSelect: ESC pressed - back to menu");
+ break;
+
+ default:
+ break;
+ }
+ break;
+
+ case Common::EVENT_LBUTTONDOWN:
+ {
+ // Get mouse position from the event
+ int mouseX = event.mouse.x;
+ int mouseY = event.mouse.y;
+
+ debug("LevelSelect: Click detected at (%d, %d)", mouseX, mouseY);
+
+ for (int i = 0; i < _levelItemCount; i++) {
+ int itemY = baseY + i * itemHeight;
+ // Use a larger vertical hit area (full item height)
+ if (mouseY >= itemY - 4 && mouseY < itemY + 6) {
+ _levelSelection = i;
+ result = i;
+ debug("LevelSelect: Item %d clicked at Y=%d (itemY=%d)", i, mouseY, itemY);
+ break;
+ }
+ }
+ }
+ break;
+
+ case Common::EVENT_MOUSEMOVE:
+ // Update mouse position for hover effects
+ _vm->_mouse.x = event.mouse.x;
+ _vm->_mouse.y = event.mouse.y;
+ break;
+
+ case Common::EVENT_QUIT:
+ case Common::EVENT_RETURN_TO_LAUNCHER:
+ // Handle quit request - go back to main menu
+ result = _levelItemCount - 1;
+ break;
+
+ default:
+ break;
+ }
+ }
+
+ return result;
+}
+
+void InsaneRebel2::drawLevelSelectOverlay(byte *renderBitmap, int pitch, int width, int height) {
+ // Draw level selection menu overlay
+ // Emulates FUN_0041f5ae for level selection mode
+ //
+ // From info.md - Low Resolution Coordinate Formulas:
+ // Center X = 160, Title Y = numItems * -5 + 81, Item Base Y = numItems * -5 + 104
+ // Item spacing = 10 pixels, Selection box height = 10 pixels
+
+ // Level menu items - for now just Level 1 and Main Menu
+ // In the original game, this would show all unlocked levels plus options
+ static const char *levelItems[] = {
+ "SELECT CHAPTER", // Title (index 0)
+ "CHAPTER 1", // Level 1 (index 1) - selectable
+ "MAIN MENU" // Back to menu (index 2) - selectable
+ };
+
+ const int numItemsTotal = 3; // Title + 2 selectable items
+ const int numSelectableItems = 2;
+
+ // Calculate positions (low-res 320x200 mode)
+ const int centerX = width / 2;
+ const int titleY = numItemsTotal * -5 + 81; // 81 - 15 = 66
+ const int itemBaseY = numItemsTotal * -5 + 104; // 104 - 15 = 89
+ const int itemSpacing = 10;
+
+ NutRenderer *font = _smush_smalfontNut;
+ if (!font) {
+ debug(1, "drawLevelSelectOverlay: font is NULL!");
+ return;
+ }
+
+ int numFontChars = font->getNumChars();
+ int actualPitch = pitch;
+ Common::Rect clipRect(0, 0, width, height);
+
+ // Helper function to draw centered text with optional highlight box
+ // We'll use a simple loop instead of lambda for compatibility
+ auto drawTextCentered = [&](const char *text, int y, bool highlight) {
+ // Calculate text width first
+ int textWidth = 0;
+ for (const char *c = text; *c; c++) {
+ int charIdx = (unsigned char)*c;
+ if (charIdx < numFontChars) {
+ textWidth += font->getCharWidth(charIdx);
+ }
+ }
+
+ int curX = centerX - textWidth / 2;
+
+ // Draw each character using drawCharV7
+ for (const char *c = text; *c; c++) {
+ int charIdx = (unsigned char)*c;
+ if (charIdx < numFontChars) {
+ int charWidth = font->getCharWidth(charIdx);
+ if (curX >= 0 && curX + charWidth <= width && y >= 0) {
+ // Use drawCharV7 with color -1 (original colors), hardcodedColors=true, smushColorMode=true
+ font->drawCharV7(renderBitmap, clipRect, curX, y, actualPitch, -1,
+ kStyleAlignLeft, charIdx, true, true);
+ }
+ curX += charWidth;
+ }
+ }
+
+ // If highlighted, draw selection box around text
+ if (highlight) {
+ int boxWidth = textWidth + 12;
+ int boxHeight = 10;
+ int boxX = centerX - boxWidth / 2;
+ int boxY = y - 1;
+
+ // Highlight color (bright color for visibility)
+ byte highlightColor = 0xF0;
+
+ // Draw box border (top, bottom, left, right edges)
+ if (boxY >= 0 && boxY < height && boxX >= 0 && boxX + boxWidth <= width) {
+ // Top edge
+ for (int px = boxX; px < boxX + boxWidth && px < width; px++) {
+ if (px >= 0) renderBitmap[boxY * actualPitch + px] = highlightColor;
+ }
+ // Bottom edge
+ int bottomY = boxY + boxHeight - 1;
+ if (bottomY < height) {
+ for (int px = boxX; px < boxX + boxWidth && px < width; px++) {
+ if (px >= 0) renderBitmap[bottomY * actualPitch + px] = highlightColor;
+ }
+ }
+ // Left edge
+ for (int py = boxY; py < boxY + boxHeight && py < height; py++) {
+ if (py >= 0 && boxX >= 0) renderBitmap[py * actualPitch + boxX] = highlightColor;
+ }
+ // Right edge
+ int rightX = boxX + boxWidth - 1;
+ if (rightX < width) {
+ for (int py = boxY; py < boxY + boxHeight && py < height; py++) {
+ if (py >= 0) renderBitmap[py * actualPitch + rightX] = highlightColor;
+ }
+ }
+ }
+ }
+ };
+
+ // Draw title (not selectable)
+ drawTextCentered(levelItems[0], titleY, false);
+
+ // Draw selectable items
+ for (int i = 0; i < numSelectableItems; i++) {
+ int itemY = itemBaseY + i * itemSpacing;
+ bool isSelected = (i == _levelSelection);
+ drawTextCentered(levelItems[i + 1], itemY, isSelected);
+ }
+
+ // Draw info text at bottom if a level is selected (not "MAIN MENU")
+ if (_levelSelection < numSelectableItems - 1) {
+ // Show difficulty or other info
+ // From info.md: Difficulty at X=30, Y=180
+ const char *difficultyText = "DIFFICULTY: EASY";
+ int infoY = 180;
+ int infoX = 30;
+
+ // Draw left-aligned text using drawCharV7
+ int curX = infoX;
+ for (const char *c = difficultyText; *c; c++) {
+ int charIdx = (unsigned char)*c;
+ if (charIdx < numFontChars) {
+ int charWidth = font->getCharWidth(charIdx);
+ if (curX >= 0 && curX + charWidth <= width && infoY >= 0) {
+ font->drawCharV7(renderBitmap, clipRect, curX, infoY, actualPitch, -1,
+ kStyleAlignLeft, charIdx, true, true);
+ }
+ curX += charWidth;
+ }
+ }
+ }
+}
+
} // End of namespace Scumm
diff --git a/engines/scumm/insane/insane_rebel.h b/engines/scumm/insane/insane_rebel.h
index bf4ffa094cb..9434645d484 100644
--- a/engines/scumm/insane/insane_rebel.h
+++ b/engines/scumm/insane/insane_rebel.h
@@ -28,8 +28,11 @@
#include "scumm/insane/insane.h"
+#include "common/keyboard.h"
#include "common/list.h"
#include "common/rect.h"
+#include "common/events.h"
+#include "common/queue.h"
#include "audio/audiostream.h"
#include "audio/mixer.h"
@@ -41,12 +44,19 @@ class SoundHandle;
namespace Scumm {
-class InsaneRebel2 : public Insane {
+class InsaneRebel2 : public Insane, public Common::EventObserver {
public:
InsaneRebel2(ScummEngine_v7 *scumm);
~InsaneRebel2();
+ // EventObserver interface - captures events before ScummEngine consumes them
+ bool notifyEvent(const Common::Event &event) override;
+
+ // Menu input event queue - events are captured by notifyEvent() and processed by processMenuInput()
+ Common::Queue<Common::Event> _menuEventQueue;
+ bool _menuInputActive; // True when we're capturing menu input events
+
// ======================= Menu System =======================
// Main game states (emulates retail state machine from FUN_004142BD)
enum GameState {
@@ -93,6 +103,27 @@ public:
// Reset menu state for fresh start
void resetMenu();
+
+ // ================= Level Selection Menu ====================
+ // Level selection results
+ enum LevelSelectResult {
+ kLevelSelectBack = 0, // Return to main menu
+ kLevelSelectPlay = 1, // Play selected level
+ kLevelSelectQuit = 2 // Quit game
+ };
+
+ int _levelSelection; // Current level selection (0-based)
+ int _levelItemCount; // Number of level items (levels + options)
+ int _selectedLevel; // Final selected level ID (1-15)
+
+ // Run level selection menu - returns LevelSelectResult
+ int runLevelSelect();
+
+ // Draw level selection overlay
+ void drawLevelSelectOverlay(byte *renderBitmap, int pitch, int width, int height);
+
+ // Process level select input - returns -1 (no action) or action code
+ int processLevelSelectInput();
// =============================================================
NutRenderer *_smush_cockpitNut;
diff --git a/engines/scumm/scumm.cpp b/engines/scumm/scumm.cpp
index 9f868c127cf..61ec421a515 100644
--- a/engines/scumm/scumm.cpp
+++ b/engines/scumm/scumm.cpp
@@ -2696,23 +2696,33 @@ Common::Error ScummEngine::go() {
break;
}
- if (menuResult == InsaneRebel2::kMenuNewGame ||
- menuResult == InsaneRebel2::kMenuContinue) {
- // Start gameplay
- // Play Level 1 intro cinematic (01BEG.SAN)
- // Set video flags to 0x20 to mark this as a non-interactive intro sequence
- vm7->_splayer->setCurVideoFlags(0x20);
- vm7->_splayer->play("LEV01/01BEG.SAN", 12);
-
- if (shouldQuit()) break;
-
- // After intro completes, start the gameplay mission video (01P01.SAN)
- // Clear the intro flag so HUD renders during gameplay
- vm7->_splayer->setCurVideoFlags(0);
- vm7->_splayer->play("LEV01/01P01.SAN", 12);
-
- // After gameplay, return to menu
- // TODO: Handle level progression, game over, etc.
+ if (menuResult == InsaneRebel2::kMenuContinue) {
+ // Continue: Show level selection menu
+ int levelResult = rebel->runLevelSelect();
+
+ if (levelResult == InsaneRebel2::kLevelSelectQuit || shouldQuit()) {
+ break;
+ }
+
+ if (levelResult == InsaneRebel2::kLevelSelectPlay) {
+ // Play the selected level (currently only level 1 is supported)
+ int selectedLevel = rebel->_selectedLevel;
+ debug("ScummEngine: Starting level %d", selectedLevel);
+
+ // For now, only level 1 is implemented
+ // Play Level 1 intro cinematic
+ vm7->_splayer->setCurVideoFlags(0x20);
+ vm7->_splayer->play("LEV01/01BEG.SAN", 12);
+
+ if (shouldQuit()) break;
+
+ // Start gameplay
+ vm7->_splayer->setCurVideoFlags(0);
+ vm7->_splayer->play("LEV01/01P01.SAN", 12);
+
+ // After gameplay, return to menu
+ }
+ // If kLevelSelectBack, loop back to main menu
}
}
return Common::kNoError;
Commit: 14884b6d742f2f542ff0b9d1bc86d004efa08a59
https://github.com/scummvm/scummvm/commit/14884b6d742f2f542ff0b9d1bc86d004efa08a59
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:08:53+02:00
Commit Message:
SCUMM: RA2: Implement level loading
Changed paths:
engines/scumm/insane/insane_rebel.cpp
engines/scumm/insane/insane_rebel.h
engines/scumm/scumm.cpp
diff --git a/engines/scumm/insane/insane_rebel.cpp b/engines/scumm/insane/insane_rebel.cpp
index 73c49d9c4d4..48f4988d902 100644
--- a/engines/scumm/insane/insane_rebel.cpp
+++ b/engines/scumm/insane/insane_rebel.cpp
@@ -270,6 +270,12 @@ InsaneRebel2::InsaneRebel2(ScummEngine_v7 *scumm) {
// Initialize menu input capture system
_menuInputActive = false;
+ // Initialize level state tracking for multi-phase levels
+ _currentPhase = 1;
+ _deathFrame = 0;
+ _phaseScore = 0;
+ _phaseMisses = 0;
+
// Register as EventObserver to capture input events before ScummEngine consumes them
_vm->_system->getEventManager()->getEventDispatcher()->registerObserver(this, 1, false);
}
@@ -3116,7 +3122,7 @@ int InsaneRebel2::runLevelSelect() {
// Initialize level selection state
_levelSelection = 0;
- _levelItemCount = 2; // Selectable items: Level 1, MAIN MENU
+ _levelItemCount = 7; // Selectable items: 6 levels + MAIN MENU
_selectedLevel = 1; // Default to level 1
_menuRepeatDelay = 0;
_gameState = kStateLevelSelect;
@@ -3154,31 +3160,20 @@ int InsaneRebel2::runLevelSelect() {
debug("Rebel2: Level selection made: %d", _levelSelection);
// Process level selection
- // Menu items (based on FUN_414A41 analysis):
- // 0: Level 1 (CHAPTER 1)
- // 1: MAIN MENU (back)
- // Future: More levels, NEW PILOT, DELETE PILOT, etc.
- switch (_levelSelection) {
- case 0: // Level 1
- debug("Rebel2: Level 1 selected");
- _selectedLevel = 1;
+ // Menu items:
+ // 0-5: Levels 1-6 (CHAPTER 1-6)
+ // 6: MAIN MENU (back)
+ if (_levelSelection >= 0 && _levelSelection <= 5) {
+ // Level selected (0 = Level 1, 5 = Level 6)
+ _selectedLevel = _levelSelection + 1;
+ debug("Rebel2: Level %d selected", _selectedLevel);
_menuInputActive = false;
return kLevelSelectPlay;
-
- case 1: // Main Menu (back)
+ } else if (_levelSelection == 6) {
+ // Main Menu (back)
debug("Rebel2: Back to main menu selected");
_menuInputActive = false;
return kLevelSelectBack;
-
- default:
- // For now, any other selection defaults to Level 1
- if (_levelSelection < _levelItemCount - 1) {
- debug("Rebel2: Level %d selected (defaulting to 1)", _levelSelection + 1);
- _selectedLevel = 1; // Only level 1 available for now
- _menuInputActive = false;
- return kLevelSelectPlay;
- }
- break;
}
}
@@ -3292,16 +3287,21 @@ void InsaneRebel2::drawLevelSelectOverlay(byte *renderBitmap, int pitch, int wid
// Center X = 160, Title Y = numItems * -5 + 81, Item Base Y = numItems * -5 + 104
// Item spacing = 10 pixels, Selection box height = 10 pixels
- // Level menu items - for now just Level 1 and Main Menu
+ // Level menu items - all 6 levels plus Main Menu
// In the original game, this would show all unlocked levels plus options
static const char *levelItems[] = {
"SELECT CHAPTER", // Title (index 0)
"CHAPTER 1", // Level 1 (index 1) - selectable
- "MAIN MENU" // Back to menu (index 2) - selectable
+ "CHAPTER 2", // Level 2 (index 2) - selectable
+ "CHAPTER 3", // Level 3 (index 3) - selectable
+ "CHAPTER 4", // Level 4 (index 4) - selectable
+ "CHAPTER 5", // Level 5 (index 5) - selectable
+ "CHAPTER 6", // Level 6 (index 6) - selectable
+ "MAIN MENU" // Back to menu (index 7) - selectable
};
- const int numItemsTotal = 3; // Title + 2 selectable items
- const int numSelectableItems = 2;
+ const int numItemsTotal = 8; // Title + 7 selectable items
+ const int numSelectableItems = 7;
// Calculate positions (low-res 320x200 mode)
const int centerX = width / 2;
@@ -3419,4 +3419,987 @@ void InsaneRebel2::drawLevelSelectOverlay(byte *renderBitmap, int pitch, int wid
}
}
+// ======================= Level Loading System =======================
+// Emulates the level handler functions from FUN_00417E53 through FUN_0041BBE8
+// Based on disassembly analysis of the retail Rebel Assault 2 executable.
+
+Common::String InsaneRebel2::getLevelDir(int levelId) {
+ // Returns directory name like "LEV01" for level 1
+ return Common::String::format("LEV%02d", levelId);
+}
+
+Common::String InsaneRebel2::getLevelPrefix(int levelId) {
+ // Returns file prefix like "01" for level 1
+ return Common::String::format("%02d", levelId);
+}
+
+void InsaneRebel2::playIntroSequence() {
+ // Emulates case 0 in FUN_004142BD
+ // Plays the game intro sequence:
+ // 1. CREDITS/O_OPEN_C.SAN - Fox logo (if certain conditions)
+ // 2. CREDITS/O_OPEN_D.SAN - LucasArts logo (if certain conditions)
+ // 3. OPEN/O_OPEN_A.SAN - Main intro
+ // 4. OPEN/O_OPEN_B.SAN - Additional intro (if conditions)
+
+ debug("Rebel2: Playing intro sequence");
+
+ SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
+
+ // Set intro flags (non-interactive)
+ splayer->setCurVideoFlags(0x20);
+
+ // Play Fox logo (CREDITS/O_OPEN_C.SAN)
+ // In retail, this checks if 'f', 'o', 'x' keys are held (easter egg)
+ // We'll play it unconditionally for now
+ debug("Rebel2: Playing Fox logo");
+ splayer->play("CREDITS/O_OPEN_C.SAN", 12);
+
+ if (_vm->shouldQuit()) return;
+
+ // Play LucasArts logo (CREDITS/O_OPEN_D.SAN)
+ // In retail, this checks if 'b', 'o', 't' keys are held
+ debug("Rebel2: Playing LucasArts logo");
+ splayer->play("CREDITS/O_OPEN_D.SAN", 12);
+
+ if (_vm->shouldQuit()) return;
+
+ // Play main intro (OPEN/O_OPEN_A.SAN)
+ debug("Rebel2: Playing main intro");
+ splayer->play("OPEN/O_OPEN_A.SAN", 12);
+
+ if (_vm->shouldQuit()) return;
+
+ // Play additional intro (OPEN/O_OPEN_B.SAN)
+ // In retail, this plays if DAT_0047ab45 or DAT_0047ab47 != 0
+ debug("Rebel2: Playing additional intro");
+ splayer->play("OPEN/O_OPEN_B.SAN", 12);
+}
+
+void InsaneRebel2::playMissionBriefing() {
+ // Emulates FUN_00415CF8 (partial - just the video)
+ // Plays OPEN/O_LEVEL.SAN which shows the mission briefing screen
+
+ debug("Rebel2: Playing mission briefing");
+
+ SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
+ splayer->setCurVideoFlags(0x08); // Briefing mode flag
+ splayer->play("OPEN/O_LEVEL.SAN", 12);
+}
+
+void InsaneRebel2::playLevelBegin(int levelId) {
+ // Play the level beginning cinematic (LEVXX/XXBEG.SAN)
+ // Emulates FUN_004171c5 call in each level handler
+
+ Common::String dir = getLevelDir(levelId);
+ Common::String prefix = getLevelPrefix(levelId);
+ Common::String filename = Common::String::format("%s/%sBEG.SAN", dir.c_str(), prefix.c_str());
+
+ debug("Rebel2: Playing level %d beginning: %s", levelId, filename.c_str());
+
+ SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
+ splayer->setCurVideoFlags(0x20); // Cinematic mode
+ splayer->play(filename.c_str(), 12);
+}
+
+bool InsaneRebel2::playLevelGameplay(int levelId) {
+ // Play the main gameplay video(s) for a level
+ // Returns true if level completed (damage < 0xff), false if died
+ //
+ // Different levels have different gameplay structures:
+ // - Level 1, 4, 5: Single gameplay SAN (XXPXX.SAN or XXPLAY.SAN)
+ // - Level 2: Multiple parts with subdirectories (P1/, P2/, P3/)
+ // - Level 3, 6: Two gameplay phases (XXPLAY1.SAN, XXPLAY2.SAN)
+
+ Common::String dir = getLevelDir(levelId);
+ Common::String prefix = getLevelPrefix(levelId);
+ Common::String filename;
+
+ SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
+
+ // Set gameplay flags (interactive with HUD)
+ splayer->setCurVideoFlags(0x28);
+
+ // Reset damage/shield for this level
+ _playerShield = 255;
+ _rebelHandler = 0;
+
+ debug("Rebel2: Starting gameplay for level %d", levelId);
+
+ switch (levelId) {
+ case 1:
+ // Level 1: Single gameplay file (01P01.SAN)
+ filename = Common::String::format("%s/%sP01.SAN", dir.c_str(), prefix.c_str());
+ debug("Rebel2: Playing %s", filename.c_str());
+ splayer->play(filename.c_str(), 12);
+ break;
+
+ case 2:
+ // Level 2: Has cutscene first, then multiple parts
+ // First play the cutscene
+ filename = Common::String::format("%s/%sCUT.SAN", dir.c_str(), prefix.c_str());
+ debug("Rebel2: Playing cutscene %s", filename.c_str());
+ splayer->setCurVideoFlags(0x20);
+ splayer->play(filename.c_str(), 12);
+
+ if (_vm->shouldQuit() || _playerShield == 0) return false;
+
+ // Part 1 (multiple variations - play A for now)
+ splayer->setCurVideoFlags(0x28);
+ filename = Common::String::format("%s/P1/%sP01_A.SAN", dir.c_str(), prefix.c_str());
+ debug("Rebel2: Playing %s", filename.c_str());
+ splayer->play(filename.c_str(), 12);
+
+ if (_vm->shouldQuit() || _playerShield == 0) return false;
+
+ // Post segment 1
+ filename = Common::String::format("%s/%sPST1.SAN", dir.c_str(), prefix.c_str());
+ debug("Rebel2: Playing %s", filename.c_str());
+ splayer->setCurVideoFlags(0x20);
+ splayer->play(filename.c_str(), 12);
+
+ if (_vm->shouldQuit() || _playerShield == 0) return false;
+
+ // Part 2
+ splayer->setCurVideoFlags(0x28);
+ filename = Common::String::format("%s/P2/%sP02_A.SAN", dir.c_str(), prefix.c_str());
+ debug("Rebel2: Playing %s", filename.c_str());
+ splayer->play(filename.c_str(), 12);
+
+ if (_vm->shouldQuit() || _playerShield == 0) return false;
+
+ // Post segment 2
+ filename = Common::String::format("%s/%sPST2.SAN", dir.c_str(), prefix.c_str());
+ debug("Rebel2: Playing %s", filename.c_str());
+ splayer->setCurVideoFlags(0x20);
+ splayer->play(filename.c_str(), 12);
+
+ if (_vm->shouldQuit() || _playerShield == 0) return false;
+
+ // Part 3
+ splayer->setCurVideoFlags(0x28);
+ filename = Common::String::format("%s/P3/%sP03_A.SAN", dir.c_str(), prefix.c_str());
+ debug("Rebel2: Playing %s", filename.c_str());
+ splayer->play(filename.c_str(), 12);
+ break;
+
+ case 3:
+ // Level 3: Two gameplay phases
+ filename = Common::String::format("%s/%sPLAY1.SAN", dir.c_str(), prefix.c_str());
+ debug("Rebel2: Playing %s", filename.c_str());
+ splayer->play(filename.c_str(), 12);
+
+ if (_vm->shouldQuit() || _playerShield == 0) return false;
+
+ // Post segment
+ filename = Common::String::format("%s/%sPOST1.SAN", dir.c_str(), prefix.c_str());
+ debug("Rebel2: Playing %s", filename.c_str());
+ splayer->setCurVideoFlags(0x20);
+ splayer->play(filename.c_str(), 12);
+
+ if (_vm->shouldQuit() || _playerShield == 0) return false;
+
+ // Phase 2
+ splayer->setCurVideoFlags(0x28);
+ filename = Common::String::format("%s/%sPLAY2.SAN", dir.c_str(), prefix.c_str());
+ debug("Rebel2: Playing %s", filename.c_str());
+ splayer->play(filename.c_str(), 12);
+ break;
+
+ case 4:
+ // Level 4: Has cutscene, then single gameplay
+ filename = Common::String::format("%s/%sCUT.SAN", dir.c_str(), prefix.c_str());
+ debug("Rebel2: Playing cutscene %s", filename.c_str());
+ splayer->setCurVideoFlags(0x20);
+ splayer->play(filename.c_str(), 12);
+
+ if (_vm->shouldQuit()) return false;
+
+ splayer->setCurVideoFlags(0x28);
+ filename = Common::String::format("%s/%sPLAY.SAN", dir.c_str(), prefix.c_str());
+ debug("Rebel2: Playing %s", filename.c_str());
+ splayer->play(filename.c_str(), 12);
+ break;
+
+ case 5:
+ // Level 5: Single gameplay file
+ filename = Common::String::format("%s/%sPLAY.SAN", dir.c_str(), prefix.c_str());
+ debug("Rebel2: Playing %s", filename.c_str());
+ splayer->play(filename.c_str(), 12);
+ break;
+
+ case 6:
+ // Level 6: Two gameplay phases
+ filename = Common::String::format("%s/%sPLAY1.SAN", dir.c_str(), prefix.c_str());
+ debug("Rebel2: Playing %s", filename.c_str());
+ splayer->play(filename.c_str(), 12);
+
+ if (_vm->shouldQuit() || _playerShield == 0) return false;
+
+ // Post segment
+ filename = Common::String::format("%s/%sPOST1.SAN", dir.c_str(), prefix.c_str());
+ debug("Rebel2: Playing %s", filename.c_str());
+ splayer->setCurVideoFlags(0x20);
+ splayer->play(filename.c_str(), 12);
+
+ if (_vm->shouldQuit() || _playerShield == 0) return false;
+
+ // Phase 2
+ splayer->setCurVideoFlags(0x28);
+ filename = Common::String::format("%s/%sPLAY2.SAN", dir.c_str(), prefix.c_str());
+ debug("Rebel2: Playing %s", filename.c_str());
+ splayer->play(filename.c_str(), 12);
+ break;
+
+ default:
+ // For levels 7-15 (not in demo), try common patterns
+ // First try XXPLAY.SAN
+ filename = Common::String::format("%s/%sPLAY.SAN", dir.c_str(), prefix.c_str());
+ debug("Rebel2: Trying %s", filename.c_str());
+ splayer->play(filename.c_str(), 12);
+ break;
+ }
+
+ // Return true if player survived (shield > 0), false if died
+ return (_playerShield > 0);
+}
+
+void InsaneRebel2::playLevelEnd(int levelId) {
+ // Play level completion video (LEVXX/XXEND.SAN)
+ // Emulates FUN_00417327 call
+
+ Common::String dir = getLevelDir(levelId);
+ Common::String prefix = getLevelPrefix(levelId);
+ Common::String filename = Common::String::format("%s/%sEND.SAN", dir.c_str(), prefix.c_str());
+
+ debug("Rebel2: Playing level %d end: %s", levelId, filename.c_str());
+
+ SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
+ splayer->setCurVideoFlags(0x20); // Cinematic mode
+ splayer->play(filename.c_str(), 12);
+}
+
+void InsaneRebel2::playLevelDeath(int levelId) {
+ // Play death video (LEVXX/XXDIE_X.SAN)
+ // The variant depends on the frame where player died
+ // For simplicity, we'll play the A variant
+
+ Common::String dir = getLevelDir(levelId);
+ Common::String prefix = getLevelPrefix(levelId);
+
+ // Most levels have DIE_A, some have just DIE
+ Common::String filename;
+ if (levelId == 2 || levelId == 4) {
+ filename = Common::String::format("%s/%sDIE.SAN", dir.c_str(), prefix.c_str());
+ } else {
+ filename = Common::String::format("%s/%sDIE_A.SAN", dir.c_str(), prefix.c_str());
+ }
+
+ debug("Rebel2: Playing level %d death: %s", levelId, filename.c_str());
+
+ SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
+ splayer->setCurVideoFlags(0x20);
+ splayer->play(filename.c_str(), 12);
+}
+
+void InsaneRebel2::playLevelRetry(int levelId) {
+ // Play retry prompt video (LEVXX/XXRETRY.SAN)
+
+ Common::String dir = getLevelDir(levelId);
+ Common::String prefix = getLevelPrefix(levelId);
+ Common::String filename = Common::String::format("%s/%sRETRY.SAN", dir.c_str(), prefix.c_str());
+
+ debug("Rebel2: Playing level %d retry: %s", levelId, filename.c_str());
+
+ SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
+ splayer->setCurVideoFlags(0x20);
+ splayer->play(filename.c_str(), 12);
+}
+
+void InsaneRebel2::playLevelGameOver(int levelId) {
+ // Play game over video (LEVXX/XXOVER.SAN)
+ // Emulates FUN_00417ab2 call
+
+ Common::String dir = getLevelDir(levelId);
+ Common::String prefix = getLevelPrefix(levelId);
+ Common::String filename = Common::String::format("%s/%sOVER.SAN", dir.c_str(), prefix.c_str());
+
+ debug("Rebel2: Playing level %d game over: %s", levelId, filename.c_str());
+
+ SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
+ splayer->setCurVideoFlags(0x20);
+ splayer->play(filename.c_str(), 12);
+}
+
+void InsaneRebel2::playCreditsSequence() {
+ // Play the end credits (OPEN/O_CREDIT.SAN)
+ // Individual credits are in CREDITS/CRED_XX.SAN
+
+ debug("Rebel2: Playing credits");
+
+ SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
+ splayer->setCurVideoFlags(0x20);
+ splayer->play("OPEN/O_CREDIT.SAN", 12);
+}
+
+int InsaneRebel2::runLevel(int levelId) {
+ // Main level dispatcher - calls per-level handlers
+ // Each level handler emulates its retail counterpart (FUN_00417E53 etc.)
+
+ debug("Rebel2: Starting level %d", levelId);
+
+ // Validate level ID
+ if (levelId < 1 || levelId > 15) {
+ warning("Rebel2: Invalid level ID %d", levelId);
+ return kLevelReturnToMenu;
+ }
+
+ // Set the current level
+ _selectedLevel = levelId;
+
+ // Initialize common player state
+ _playerLives = 3;
+ _playerShield = 255;
+ _playerScore = 0;
+ _playerDamage = 0;
+ _currentPhase = 1;
+ _phaseScore = 0;
+ _phaseMisses = 0;
+
+ // Dispatch to per-level handler
+ switch (levelId) {
+ case 1:
+ return runLevel1();
+ case 2:
+ return runLevel2();
+ case 3:
+ return runLevel3();
+ case 4:
+ return runLevel4();
+ case 5:
+ return runLevel5();
+ case 6:
+ return runLevel6();
+ default:
+ // Levels 7-15: Use generic handler (similar to Level 1)
+ return runLevel1();
+ }
+}
+
+// =============================================================================
+// Helper functions
+// =============================================================================
+
+int InsaneRebel2::getRandomVariant(int max) {
+ // Emulates FUN_004233a0 - returns random number 0 to max-1
+ return _vm->_rnd.getRandomNumber(max - 1);
+}
+
+Common::String InsaneRebel2::selectDeathVideoVariant(int levelId, int phase, int frame) {
+ // Select death video variant based on level, phase, and death frame
+ // Emulates the frame-based death video selection in retail level handlers
+ //
+ // Returns variant suffix: "A", "B", "C", etc.
+
+ switch (levelId) {
+ case 1:
+ // Level 1: Random between A and B
+ return (getRandomVariant(2) == 0) ? "A" : "B";
+
+ case 2:
+ // Level 2: Just "DIE" (no variants)
+ return "";
+
+ case 3:
+ // Level 3: Based on death frame and phase
+ if (phase == 1) {
+ // Phase 1 death video selection (from FUN_0041885F lines 80-96)
+ if (frame < 0x10c) return "B"; // < 268
+ if (frame < 0x1a9) return "A"; // < 425
+ if (frame < 0x247) return "C"; // < 583
+ if (frame < 700) return "A";
+ if (frame < 900) return "B";
+ return "A";
+ } else {
+ // Phase 2 death video selection (from FUN_0041885F lines 53-67)
+ if (frame < 0x2f1) return "A"; // < 753
+ if (frame < 0x347) return "B"; // < 839
+ if (frame < 0x3b1) return "C"; // < 945
+ if (frame < 0x405) return "A"; // < 1029
+ return "C";
+ }
+
+ case 4:
+ // Level 4: Just "DIE" (no variants)
+ return "";
+
+ case 5:
+ // Level 5: Random between A and B (like Level 1)
+ return (getRandomVariant(2) == 0) ? "A" : "B";
+
+ case 6:
+ // Level 6: Similar to Level 3 (two phases with frame-based selection)
+ if (phase == 1) {
+ if (frame < 300) return "A";
+ if (frame < 600) return "B";
+ return "C";
+ } else {
+ if (frame < 400) return "A";
+ if (frame < 800) return "B";
+ return "C";
+ }
+
+ default:
+ return "A";
+ }
+}
+
+void InsaneRebel2::playLevelDeathVariant(int levelId, int phase, int frame) {
+ // Play death video with proper variant selection
+
+ Common::String dir = getLevelDir(levelId);
+ Common::String prefix = getLevelPrefix(levelId);
+ Common::String variant = selectDeathVideoVariant(levelId, phase, frame);
+ Common::String filename;
+
+ if (variant.empty()) {
+ // No variant suffix (Level 2, 4)
+ filename = Common::String::format("%s/%sDIE.SAN", dir.c_str(), prefix.c_str());
+ } else {
+ filename = Common::String::format("%s/%sDIE_%s.SAN", dir.c_str(), prefix.c_str(), variant.c_str());
+ }
+
+ debug("Rebel2: Playing death video: %s (phase=%d, frame=%d)", filename.c_str(), phase, frame);
+
+ SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
+ splayer->setCurVideoFlags(0x20);
+ splayer->play(filename.c_str(), 12);
+}
+
+void InsaneRebel2::playLevelRetryVariant(int levelId, int phase) {
+ // Play retry video - phase-specific for multi-phase levels
+
+ Common::String dir = getLevelDir(levelId);
+ Common::String prefix = getLevelPrefix(levelId);
+ Common::String filename;
+
+ if (levelId == 3 && phase == 2) {
+ // Level 3 phase 2 has its own retry video: 03RETRYB.SAN
+ filename = Common::String::format("%s/%sRETRYB.SAN", dir.c_str(), prefix.c_str());
+ } else {
+ // Standard retry video
+ filename = Common::String::format("%s/%sRETRY.SAN", dir.c_str(), prefix.c_str());
+ }
+
+ debug("Rebel2: Playing retry video: %s (phase=%d)", filename.c_str(), phase);
+
+ SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
+ splayer->setCurVideoFlags(0x20);
+ splayer->play(filename.c_str(), 12);
+}
+
+// =============================================================================
+// Level 1 Handler - FUN_00417E53
+// Single gameplay phase (01P01.SAN)
+// =============================================================================
+
+int InsaneRebel2::runLevel1() {
+ SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
+
+ // Play level beginning cinematic (01BEG.SAN)
+ playLevelBegin(1);
+ if (_vm->shouldQuit()) return kLevelQuit;
+
+ // Main gameplay retry loop
+ while (!_vm->shouldQuit()) {
+ // Reset shield for this attempt
+ _playerShield = 255;
+ _playerDamage = 0;
+ _deathFrame = 0;
+
+ // Play gameplay (01P01.SAN with 0x28 flags)
+ splayer->setCurVideoFlags(0x28);
+ splayer->play("LEV01/01P01.SAN", 12);
+
+ // Store death frame for video selection
+ _deathFrame = splayer->_frame;
+
+ if (_vm->shouldQuit()) return kLevelQuit;
+
+ if (_playerShield > 0) {
+ // Level completed!
+ debug("Rebel2: Level 1 completed!");
+ playLevelEnd(1);
+ _levelUnlocked[1] = true; // Unlock level 2
+ return kLevelNextLevel;
+ }
+
+ // Player died - play death video with random A/B variant
+ debug("Rebel2: Level 1 death at frame %d, lives=%d", _deathFrame, _playerLives - 1);
+ playLevelDeathVariant(1, 1, _deathFrame);
+
+ if (_vm->shouldQuit()) return kLevelQuit;
+
+ _playerLives--;
+ if (_playerLives <= 0) {
+ playLevelGameOver(1);
+ return kLevelGameOver;
+ }
+
+ // Play retry prompt and loop
+ playLevelRetry(1);
+ if (_vm->shouldQuit()) return kLevelQuit;
+ }
+
+ return kLevelQuit;
+}
+
+// =============================================================================
+// Level 2 Handler - FUN_00418063
+// Multiple parts with P1/P2/P3 subdirectories
+// Random animation variants for each part
+// =============================================================================
+
+int InsaneRebel2::runLevel2() {
+ SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
+ int bonusCount = 0; // Tracks special events (local_1c in assembly)
+
+ // Play cutscene (02CUT.SAN)
+ splayer->setCurVideoFlags(0x20);
+ splayer->play("LEV02/02CUT.SAN", 12);
+ if (_vm->shouldQuit()) return kLevelQuit;
+
+ // Play level beginning cinematic (02BEG.SAN)
+ playLevelBegin(2);
+ if (_vm->shouldQuit()) return kLevelQuit;
+
+ // Main gameplay retry loop (restarts from beginning on death)
+ while (!_vm->shouldQuit()) {
+ _playerShield = 255;
+ _playerDamage = 0;
+ _currentPhase = 1;
+ bonusCount = 0;
+
+ // ===== PHASE 1: P1/02P01_X.SAN =====
+ // Select random variant (B, C, or D - not A in main loop)
+ {
+ int variant = getRandomVariant(3);
+ const char *variants[] = {"B", "C", "D"};
+ Common::String filename = Common::String::format("LEV02/P1/02P01_%s.SAN", variants[variant]);
+
+ debug("Rebel2: Level 2 Phase 1 - playing %s", filename.c_str());
+ splayer->setCurVideoFlags(0x28);
+ splayer->play(filename.c_str(), 12);
+ _deathFrame = splayer->_frame;
+ }
+
+ if (_vm->shouldQuit()) return kLevelQuit;
+
+ if (_playerShield == 0) {
+ // Died in phase 1
+ debug("Rebel2: Level 2 Phase 1 death");
+ playLevelDeathVariant(2, 1, _deathFrame);
+ if (_vm->shouldQuit()) return kLevelQuit;
+
+ _playerLives--;
+ if (_playerLives <= 0) {
+ playLevelGameOver(2);
+ return kLevelGameOver;
+ }
+ playLevelRetry(2);
+ if (_vm->shouldQuit()) return kLevelQuit;
+ continue; // Restart from beginning
+ }
+
+ // Post segment 1 (02PST1.SAN)
+ splayer->setCurVideoFlags(0x20);
+ splayer->play("LEV02/02PST1.SAN", 12);
+ if (_vm->shouldQuit()) return kLevelQuit;
+
+ _currentPhase = 2;
+
+ // ===== PHASE 2: P2/02P02_X.SAN =====
+ // Variant selection based on switch (more complex in original)
+ {
+ int variant = getRandomVariant(6);
+ const char *variants[] = {"A", "B", "C", "D", "E", "F"};
+ Common::String filename = Common::String::format("LEV02/P2/02P02_%s.SAN", variants[variant]);
+
+ debug("Rebel2: Level 2 Phase 2 - playing %s", filename.c_str());
+ splayer->setCurVideoFlags(0x28);
+ splayer->play(filename.c_str(), 12);
+ _deathFrame = splayer->_frame;
+ }
+
+ if (_vm->shouldQuit()) return kLevelQuit;
+
+ if (_playerShield == 0) {
+ // Died in phase 2
+ debug("Rebel2: Level 2 Phase 2 death");
+ playLevelDeathVariant(2, 2, _deathFrame);
+ if (_vm->shouldQuit()) return kLevelQuit;
+
+ _playerLives--;
+ if (_playerLives <= 0) {
+ playLevelGameOver(2);
+ return kLevelGameOver;
+ }
+ playLevelRetry(2);
+ if (_vm->shouldQuit()) return kLevelQuit;
+ continue; // Restart from beginning
+ }
+
+ // Post segment 2 (02PST2.SAN)
+ splayer->setCurVideoFlags(0x20);
+ splayer->play("LEV02/02PST2.SAN", 12);
+ if (_vm->shouldQuit()) return kLevelQuit;
+
+ _currentPhase = 3;
+
+ // ===== PHASE 3: P3/02P03_X.SAN =====
+ // Variant selection with more options (A through I)
+ {
+ int variant = getRandomVariant(9);
+ const char *variants[] = {"A", "B", "C", "D", "E", "F", "G", "H", "I"};
+ Common::String filename = Common::String::format("LEV02/P3/02P03_%s.SAN", variants[variant]);
+
+ debug("Rebel2: Level 2 Phase 3 - playing %s", filename.c_str());
+ splayer->setCurVideoFlags(0x28);
+ splayer->play(filename.c_str(), 12);
+ _deathFrame = splayer->_frame;
+ }
+
+ if (_vm->shouldQuit()) return kLevelQuit;
+
+ if (_playerShield == 0) {
+ // Died in phase 3
+ debug("Rebel2: Level 2 Phase 3 death");
+ playLevelDeathVariant(2, 3, _deathFrame);
+ if (_vm->shouldQuit()) return kLevelQuit;
+
+ _playerLives--;
+ if (_playerLives <= 0) {
+ playLevelGameOver(2);
+ return kLevelGameOver;
+ }
+ playLevelRetry(2);
+ if (_vm->shouldQuit()) return kLevelQuit;
+ continue; // Restart from beginning
+ }
+
+ // Level completed!
+ debug("Rebel2: Level 2 completed! bonusCount=%d", bonusCount);
+ playLevelEnd(2);
+ _levelUnlocked[2] = true; // Unlock level 3
+ return kLevelNextLevel;
+ }
+
+ return kLevelQuit;
+}
+
+// =============================================================================
+// Level 3 Handler - FUN_0041885F
+// Two phases with per-phase retry handling
+// Phase 1: 03PLAY1.SAN, Phase 2: 03PLAY2.SAN
+// =============================================================================
+
+int InsaneRebel2::runLevel3() {
+ SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
+ int phase1Score = 0; // Score preserved across phase 2 retries
+
+ // Play level beginning cinematic (03BEG.SAN)
+ playLevelBegin(3);
+ if (_vm->shouldQuit()) return kLevelQuit;
+
+ // ===== PHASE 1 retry loop =====
+ while (!_vm->shouldQuit()) {
+ _playerShield = 255;
+ _playerDamage = 0;
+ _currentPhase = 1;
+
+ // Play phase 1 gameplay (03PLAY1.SAN)
+ debug("Rebel2: Level 3 Phase 1");
+ splayer->setCurVideoFlags(0x28);
+ splayer->play("LEV03/03PLAY1.SAN", 12);
+ _deathFrame = splayer->_frame;
+
+ if (_vm->shouldQuit()) return kLevelQuit;
+
+ if (_playerShield > 0) {
+ // Phase 1 completed - save score and proceed to phase 2
+ phase1Score = _playerScore;
+ break;
+ }
+
+ // Died in phase 1 - frame-based death video
+ debug("Rebel2: Level 3 Phase 1 death at frame %d", _deathFrame);
+ playLevelDeathVariant(3, 1, _deathFrame);
+ if (_vm->shouldQuit()) return kLevelQuit;
+
+ _playerLives--;
+ if (_playerLives <= 0) {
+ playLevelGameOver(3);
+ return kLevelGameOver;
+ }
+
+ // Phase 1 retry (03RETRY.SAN)
+ playLevelRetryVariant(3, 1);
+ if (_vm->shouldQuit()) return kLevelQuit;
+ }
+
+ if (_vm->shouldQuit()) return kLevelQuit;
+
+ // Post segment 1 (03POST1.SAN)
+ splayer->setCurVideoFlags(0x20);
+ splayer->play("LEV03/03POST1.SAN", 12);
+ if (_vm->shouldQuit()) return kLevelQuit;
+
+ // ===== PHASE 2 retry loop (preserves phase 1 score) =====
+ _currentPhase = 2;
+
+ while (!_vm->shouldQuit()) {
+ _playerShield = 255;
+ _playerDamage = 0;
+
+ // Play phase 2 gameplay (03PLAY2.SAN)
+ debug("Rebel2: Level 3 Phase 2");
+ splayer->setCurVideoFlags(0x28);
+ splayer->play("LEV03/03PLAY2.SAN", 12);
+ _deathFrame = splayer->_frame;
+
+ if (_vm->shouldQuit()) return kLevelQuit;
+
+ if (_playerShield > 0) {
+ // Level completed!
+ debug("Rebel2: Level 3 completed!");
+ playLevelEnd(3);
+ _levelUnlocked[3] = true; // Unlock level 4
+ return kLevelNextLevel;
+ }
+
+ // Died in phase 2 - frame-based death video
+ debug("Rebel2: Level 3 Phase 2 death at frame %d", _deathFrame);
+ playLevelDeathVariant(3, 2, _deathFrame);
+ if (_vm->shouldQuit()) return kLevelQuit;
+
+ _playerLives--;
+ if (_playerLives <= 0) {
+ // Use phase 2 specific game over (03OVER.SAN, same file but at different point)
+ playLevelGameOver(3);
+ return kLevelGameOver;
+ }
+
+ // Phase 2 retry (03RETRYB.SAN)
+ playLevelRetryVariant(3, 2);
+ if (_vm->shouldQuit()) return kLevelQuit;
+ }
+
+ return kLevelQuit;
+}
+
+// =============================================================================
+// Level 4 Handler
+// Cutscene + single gameplay phase
+// =============================================================================
+
+int InsaneRebel2::runLevel4() {
+ SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
+
+ // Play cutscene (04CUT.SAN)
+ splayer->setCurVideoFlags(0x20);
+ splayer->play("LEV04/04CUT.SAN", 12);
+ if (_vm->shouldQuit()) return kLevelQuit;
+
+ // Play level beginning cinematic (04BEG.SAN)
+ playLevelBegin(4);
+ if (_vm->shouldQuit()) return kLevelQuit;
+
+ // Main gameplay retry loop
+ while (!_vm->shouldQuit()) {
+ _playerShield = 255;
+ _playerDamage = 0;
+ _currentPhase = 1;
+
+ // Play gameplay (04PLAY.SAN)
+ debug("Rebel2: Level 4 gameplay");
+ splayer->setCurVideoFlags(0x28);
+ splayer->play("LEV04/04PLAY.SAN", 12);
+ _deathFrame = splayer->_frame;
+
+ if (_vm->shouldQuit()) return kLevelQuit;
+
+ if (_playerShield > 0) {
+ // Level completed!
+ debug("Rebel2: Level 4 completed!");
+ playLevelEnd(4);
+ _levelUnlocked[4] = true; // Unlock level 5
+ return kLevelNextLevel;
+ }
+
+ // Died - play death video (04DIE.SAN, no variants)
+ debug("Rebel2: Level 4 death");
+ playLevelDeathVariant(4, 1, _deathFrame);
+ if (_vm->shouldQuit()) return kLevelQuit;
+
+ _playerLives--;
+ if (_playerLives <= 0) {
+ playLevelGameOver(4);
+ return kLevelGameOver;
+ }
+
+ playLevelRetry(4);
+ if (_vm->shouldQuit()) return kLevelQuit;
+ }
+
+ return kLevelQuit;
+}
+
+// =============================================================================
+// Level 5 Handler - FUN_00418EC6
+// Single gameplay phase (05PLAY.SAN)
+// Random A/B death video like Level 1
+// =============================================================================
+
+int InsaneRebel2::runLevel5() {
+ SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
+
+ // Play level beginning cinematic (05BEG.SAN)
+ playLevelBegin(5);
+ if (_vm->shouldQuit()) return kLevelQuit;
+
+ // Main gameplay retry loop
+ while (!_vm->shouldQuit()) {
+ _playerShield = 255;
+ _playerDamage = 0;
+ _currentPhase = 1;
+
+ // Play gameplay (05PLAY.SAN)
+ debug("Rebel2: Level 5 gameplay");
+ splayer->setCurVideoFlags(0x28);
+ splayer->play("LEV05/05PLAY.SAN", 12);
+ _deathFrame = splayer->_frame;
+
+ if (_vm->shouldQuit()) return kLevelQuit;
+
+ if (_playerShield > 0) {
+ // Level completed!
+ debug("Rebel2: Level 5 completed!");
+ playLevelEnd(5);
+ _levelUnlocked[5] = true; // Unlock level 6
+ return kLevelNextLevel;
+ }
+
+ // Died - play death video with random A/B variant
+ debug("Rebel2: Level 5 death at frame %d", _deathFrame);
+ playLevelDeathVariant(5, 1, _deathFrame);
+ if (_vm->shouldQuit()) return kLevelQuit;
+
+ _playerLives--;
+ if (_playerLives <= 0) {
+ playLevelGameOver(5);
+ return kLevelGameOver;
+ }
+
+ playLevelRetry(5);
+ if (_vm->shouldQuit()) return kLevelQuit;
+ }
+
+ return kLevelQuit;
+}
+
+// =============================================================================
+// Level 6 Handler - FUN_00419317
+// Two phases with per-phase retry (like Level 3)
+// Phase 1: 06PLAY1.SAN, Phase 2: 06PLAY2.SAN
+// =============================================================================
+
+int InsaneRebel2::runLevel6() {
+ SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
+ int phase1Score = 0;
+
+ // Play level beginning cinematic (06BEG.SAN)
+ playLevelBegin(6);
+ if (_vm->shouldQuit()) return kLevelQuit;
+
+ // ===== PHASE 1 retry loop =====
+ while (!_vm->shouldQuit()) {
+ _playerShield = 255;
+ _playerDamage = 0;
+ _currentPhase = 1;
+
+ // Play phase 1 gameplay (06PLAY1.SAN)
+ debug("Rebel2: Level 6 Phase 1");
+ splayer->setCurVideoFlags(0x28);
+ splayer->play("LEV06/06PLAY1.SAN", 12);
+ _deathFrame = splayer->_frame;
+
+ if (_vm->shouldQuit()) return kLevelQuit;
+
+ if (_playerShield > 0) {
+ phase1Score = _playerScore;
+ break;
+ }
+
+ // Died in phase 1
+ debug("Rebel2: Level 6 Phase 1 death at frame %d", _deathFrame);
+ playLevelDeathVariant(6, 1, _deathFrame);
+ if (_vm->shouldQuit()) return kLevelQuit;
+
+ _playerLives--;
+ if (_playerLives <= 0) {
+ playLevelGameOver(6);
+ return kLevelGameOver;
+ }
+
+ playLevelRetryVariant(6, 1);
+ if (_vm->shouldQuit()) return kLevelQuit;
+ }
+
+ if (_vm->shouldQuit()) return kLevelQuit;
+
+ // Post segment 1 (06POST1.SAN)
+ splayer->setCurVideoFlags(0x20);
+ splayer->play("LEV06/06POST1.SAN", 12);
+ if (_vm->shouldQuit()) return kLevelQuit;
+
+ // ===== PHASE 2 retry loop =====
+ _currentPhase = 2;
+
+ while (!_vm->shouldQuit()) {
+ _playerShield = 255;
+ _playerDamage = 0;
+
+ // Play phase 2 gameplay (06PLAY2.SAN)
+ debug("Rebel2: Level 6 Phase 2");
+ splayer->setCurVideoFlags(0x28);
+ splayer->play("LEV06/06PLAY2.SAN", 12);
+ _deathFrame = splayer->_frame;
+
+ if (_vm->shouldQuit()) return kLevelQuit;
+
+ if (_playerShield > 0) {
+ // Level completed!
+ debug("Rebel2: Level 6 completed!");
+ playLevelEnd(6);
+ _levelUnlocked[6] = true; // Unlock level 7
+ return kLevelNextLevel;
+ }
+
+ // Died in phase 2
+ debug("Rebel2: Level 6 Phase 2 death at frame %d", _deathFrame);
+ playLevelDeathVariant(6, 2, _deathFrame);
+ if (_vm->shouldQuit()) return kLevelQuit;
+
+ _playerLives--;
+ if (_playerLives <= 0) {
+ playLevelGameOver(6);
+ return kLevelGameOver;
+ }
+
+ playLevelRetryVariant(6, 2);
+ if (_vm->shouldQuit()) return kLevelQuit;
+ }
+
+ return kLevelQuit;
+}
+
} // End of namespace Scumm
diff --git a/engines/scumm/insane/insane_rebel.h b/engines/scumm/insane/insane_rebel.h
index 9434645d484..bc73cb4882b 100644
--- a/engines/scumm/insane/insane_rebel.h
+++ b/engines/scumm/insane/insane_rebel.h
@@ -124,6 +124,90 @@ public:
// Process level select input - returns -1 (no action) or action code
int processLevelSelectInput();
+
+ // ======================= Level Loading System =======================
+ // Emulates the level handler functions FUN_00417E53 through FUN_0041BBE8
+ // Each level has: BEG (intro), gameplay SANs, END (completion), DIE variants,
+ // RETRY, and OVER (game over) videos.
+
+ // Level playback result (returned by runLevel)
+ enum LevelResult {
+ kLevelCompleted = 0, // Level completed successfully
+ kLevelNextLevel = 1, // Proceed to next level
+ kLevelGameOver = 2, // No lives remaining
+ kLevelQuit = 3, // Player quit
+ kLevelReturnToMenu = 4 // Return to main menu
+ };
+
+ // Play the intro sequence (CREDITS/O_OPEN_C, O_OPEN_D, OPEN/O_OPEN_A, O_OPEN_B)
+ // Emulates case 0 in FUN_004142BD
+ void playIntroSequence();
+
+ // Play the mission briefing video (OPEN/O_LEVEL.SAN)
+ // Emulates FUN_00415CF8
+ void playMissionBriefing();
+
+ // Main level runner - plays a complete level by ID (1-15)
+ // Handles all videos: BEG, gameplay, END/DIE/RETRY/OVER
+ // Returns LevelResult
+ int runLevel(int levelId);
+
+ // Play level beginning cinematic (LEVXX/XXBEG.SAN)
+ void playLevelBegin(int levelId);
+
+ // Play main gameplay video(s) for a level
+ // Returns true if level completed, false if player died
+ bool playLevelGameplay(int levelId);
+
+ // Play level completion video (LEVXX/XXEND.SAN)
+ void playLevelEnd(int levelId);
+
+ // Play death video (LEVXX/XXDIE_X.SAN) - variant based on frame/location
+ void playLevelDeath(int levelId);
+
+ // Play retry prompt video (LEVXX/XXRETRY.SAN)
+ void playLevelRetry(int levelId);
+
+ // Play game over video (LEVXX/XXOVER.SAN)
+ void playLevelGameOver(int levelId);
+
+ // Play the ending/credits sequence
+ void playCreditsSequence();
+
+ // Get the directory name for a level (e.g., "LEV01" for level 1)
+ Common::String getLevelDir(int levelId);
+
+ // Get the file prefix for a level (e.g., "01" for level 1)
+ Common::String getLevelPrefix(int levelId);
+
+ // Per-level handlers (emulate FUN_00417E53 through FUN_0041BBE8)
+ // These implement the complete level logic including retry handling
+ int runLevel1(); // FUN_00417E53 - Single gameplay phase
+ int runLevel2(); // FUN_00418063 - Multiple parts with P1/P2/P3 subdirs
+ int runLevel3(); // FUN_0041885F - Two phases with per-phase retry
+ int runLevel4(); // Cutscene + single gameplay
+ int runLevel5(); // FUN_00418EC6 - Single gameplay phase
+ int runLevel6(); // FUN_00419317 - Two phases with per-phase retry
+
+ // Random number helper (emulates FUN_004233a0)
+ int getRandomVariant(int max);
+
+ // Select death video variant based on level, phase, and frame
+ // Returns suffix like "A", "B", "C" for DIE_X.SAN
+ Common::String selectDeathVideoVariant(int levelId, int phase, int frame);
+
+ // Play death video with proper variant selection
+ void playLevelDeathVariant(int levelId, int phase, int frame);
+
+ // Play retry video (phase-specific for multi-phase levels)
+ void playLevelRetryVariant(int levelId, int phase);
+
+ // Level state tracking for multi-phase levels
+ int _currentPhase; // Current gameplay phase (1, 2, 3 for Level 2; 1, 2 for Level 3/6)
+ int _deathFrame; // Frame number where player died (for death video selection)
+ int _phaseScore; // Accumulated score from previous phases (preserved on phase retry)
+ int _phaseMisses; // Accumulated misses from previous phases
+
// =============================================================
NutRenderer *_smush_cockpitNut;
diff --git a/engines/scumm/scumm.cpp b/engines/scumm/scumm.cpp
index 61ec421a515..7c51ecef7de 100644
--- a/engines/scumm/scumm.cpp
+++ b/engines/scumm/scumm.cpp
@@ -2705,22 +2705,19 @@ Common::Error ScummEngine::go() {
}
if (levelResult == InsaneRebel2::kLevelSelectPlay) {
- // Play the selected level (currently only level 1 is supported)
+ // Play the selected level using the level loading system
int selectedLevel = rebel->_selectedLevel;
debug("ScummEngine: Starting level %d", selectedLevel);
- // For now, only level 1 is implemented
- // Play Level 1 intro cinematic
- vm7->_splayer->setCurVideoFlags(0x20);
- vm7->_splayer->play("LEV01/01BEG.SAN", 12);
+ // Run the complete level (handles BEG, gameplay, END/DIE/RETRY/OVER)
+ int result = rebel->runLevel(selectedLevel);
- if (shouldQuit()) break;
-
- // Start gameplay
- vm7->_splayer->setCurVideoFlags(0);
- vm7->_splayer->play("LEV01/01P01.SAN", 12);
+ if (shouldQuit() || result == InsaneRebel2::kLevelQuit) {
+ break;
+ }
- // After gameplay, return to menu
+ // After level completion or game over, return to menu
+ // Could also handle kLevelNextLevel to auto-start next level
}
// If kLevelSelectBack, loop back to main menu
}
Commit: ead8b2a3edec1ce1d6a22b61fab082f546e6ecf0
https://github.com/scummvm/scummvm/commit/ead8b2a3edec1ce1d6a22b61fab082f546e6ecf0
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:08:53+02:00
Commit Message:
SCUMM: RA2: Clear screen before missions
Changed paths:
engines/scumm/insane/insane_rebel.cpp
diff --git a/engines/scumm/insane/insane_rebel.cpp b/engines/scumm/insane/insane_rebel.cpp
index 48f4988d902..163bb7c10de 100644
--- a/engines/scumm/insane/insane_rebel.cpp
+++ b/engines/scumm/insane/insane_rebel.cpp
@@ -3753,6 +3753,15 @@ int InsaneRebel2::runLevel(int levelId) {
return kLevelReturnToMenu;
}
+ // Switch to gameplay state to stop menu overlay rendering
+ _gameState = kStateGameplay;
+ _menuInputActive = false;
+
+ // Clear the screen to remove any leftover menu pixels
+ VirtScreen *vs = &_vm->_virtscr[kMainVirtScreen];
+ memset(vs->getPixels(0, 0), 0, vs->pitch * vs->h);
+ _vm->markRectAsDirty(kMainVirtScreen, 0, vs->w, 0, vs->h);
+
// Set the current level
_selectedLevel = levelId;
Commit: 2878546d81f65931c7c1f164baf622090cc5875a
https://github.com/scummvm/scummvm/commit/2878546d81f65931c7c1f164baf622090cc5875a
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:08:53+02:00
Commit Message:
SCUMM: RA2: Implement pause and skip controls
Changed paths:
engines/scumm/insane/insane_rebel.cpp
engines/scumm/insane/insane_rebel.h
diff --git a/engines/scumm/insane/insane_rebel.cpp b/engines/scumm/insane/insane_rebel.cpp
index 163bb7c10de..5ec985f0f88 100644
--- a/engines/scumm/insane/insane_rebel.cpp
+++ b/engines/scumm/insane/insane_rebel.cpp
@@ -296,6 +296,44 @@ InsaneRebel2::~InsaneRebel2() {
}
bool InsaneRebel2::notifyEvent(const Common::Event &event) {
+ // Handle global key events (ESC to skip, SPACE to pause)
+ // These work regardless of menu state
+ if (event.type == Common::EVENT_KEYDOWN) {
+ SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
+
+ switch (event.kbd.keycode) {
+ case Common::KEYCODE_ESCAPE:
+ // ESC skips cutscenes (videos with 0x20 flag = non-interactive)
+ // Emulates FUN_0041f537 behavior: if key == 0x1b, skip video
+ if (splayer && (splayer->_curVideoFlags & 0x20) != 0) {
+ debug("Rebel2: ESC pressed - skipping cutscene");
+ _vm->_smushVideoShouldFinish = true;
+ return true; // Consume the event
+ }
+ break;
+
+ case Common::KEYCODE_SPACE:
+ // SPACE toggles pause (emulates FUN_405A21 pause handling)
+ // Only allow pausing during gameplay, not in menus
+ if (splayer && _gameState == kStateGameplay) {
+ if (splayer->_paused) {
+ debug("Rebel2: SPACE pressed - unpausing");
+ splayer->unpause();
+ } else {
+ debug("Rebel2: SPACE pressed - pausing");
+ splayer->pause();
+ // Show the pause overlay with dimming effect and "PAUSED" text
+ showPauseOverlay();
+ }
+ return true; // Consume the event
+ }
+ break;
+
+ default:
+ break;
+ }
+ }
+
// Capture menu-related input events when menu input is active.
// This is called before ScummEngine::parseEvents() consumes events,
// so we can reliably capture keyboard/mouse input for menu navigation.
@@ -2984,6 +3022,165 @@ void InsaneRebel2::drawMenuOverlay(byte *renderBitmap, int pitch, int width, int
}
}
+// ======================= Pause Overlay =======================
+// Emulates FUN_405A21 pause rendering (lines 242-305)
+// Creates a dimmed overlay effect and displays "PAUSED" text
+void InsaneRebel2::showPauseOverlay() {
+ SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
+ if (!splayer) {
+ debug("showPauseOverlay: No SmushPlayer active");
+ return;
+ }
+
+ // Get frame buffer and palette from SmushPlayer
+ // _dst points to the virtual screen pixels (the actual rendering destination)
+ // _frameBuffer is only used for store/fetch operations, not general rendering
+ byte *frameBuffer = splayer->_dst;
+ byte *palette = splayer->_pal;
+ int width = splayer->_width;
+ int height = splayer->_height;
+
+ if (!frameBuffer || !palette || width <= 0 || height <= 0) {
+ debug("showPauseOverlay: No frame buffer (%p), palette (%p), or invalid dimensions (%dx%d)",
+ (void*)frameBuffer, (void*)palette, width, height);
+ return;
+ }
+
+ debug("showPauseOverlay: Applying dimming effect to %dx%d buffer", width, height);
+
+ // Apply dimming effect (emulates FUN_405A21 lines 242-251)
+ // Original algorithm:
+ // For each pixel, take the green component of its palette entry
+ // and the green component of the previous pixel's palette entry,
+ // add them, divide by 8, add 16.
+ // This creates a dark dimmed effect.
+ int bufferSize = width * height;
+ byte prevPixel = 0;
+
+ for (int i = 0; i < bufferSize; i++) {
+ byte curPixel = frameBuffer[i];
+
+ // Get green components from palette (offset +1 in RGB triplets)
+ int greenCur = palette[curPixel * 3 + 1];
+ int greenPrev = palette[prevPixel * 3 + 1];
+
+ // Apply dimming formula: (green1 + green2) >> 3 + 0x10
+ byte dimmedValue = ((greenCur + greenPrev) >> 3) + 0x10;
+
+ frameBuffer[i] = dimmedValue;
+ prevPixel = curPixel;
+ }
+
+ // Draw border decorations (simplified version of FUN_405A21 lines 261-283)
+ // Draw horizontal lines at top and bottom of a centered box
+ int boxLeft = 12;
+ int boxRight = width - 12;
+ int boxTop = 23; // 0x17
+ int boxBottom = height - 23; // ~175 for 200 height
+
+ byte borderColor = 0x50; // Gray border color
+
+ // Top and bottom borders
+ for (int x = boxLeft; x < boxRight; x++) {
+ if (boxTop >= 0 && boxTop < height)
+ frameBuffer[boxTop * width + x] = borderColor;
+ if (boxBottom >= 0 && boxBottom < height)
+ frameBuffer[boxBottom * width + x] = borderColor;
+ }
+
+ // Left and right borders
+ for (int y = boxTop; y < boxBottom; y++) {
+ if (boxLeft >= 0 && boxLeft < width)
+ frameBuffer[y * width + boxLeft] = borderColor;
+ if (boxRight >= 0 && boxRight < width)
+ frameBuffer[y * width + boxRight] = borderColor;
+ }
+
+ // Draw corner decorations (simplified)
+ byte cornerColor = 0x51; // Slightly brighter for corners
+ for (int i = 0; i < 5; i++) {
+ // Top-left corner
+ if (boxTop + i < height && boxLeft + 5 < width)
+ frameBuffer[(boxTop + i) * width + boxLeft + 5] = cornerColor;
+ if (boxTop + 5 < height && boxLeft + i < width)
+ frameBuffer[(boxTop + 5) * width + boxLeft + i] = cornerColor;
+
+ // Top-right corner
+ if (boxTop + i < height && boxRight - 5 >= 0)
+ frameBuffer[(boxTop + i) * width + boxRight - 5] = cornerColor;
+ if (boxTop + 5 < height && boxRight - i >= 0)
+ frameBuffer[(boxTop + 5) * width + boxRight - i] = cornerColor;
+
+ // Bottom-left corner
+ if (boxBottom - i >= 0 && boxLeft + 5 < width)
+ frameBuffer[(boxBottom - i) * width + boxLeft + 5] = cornerColor;
+ if (boxBottom - 5 >= 0 && boxLeft + i < width)
+ frameBuffer[(boxBottom - 5) * width + boxLeft + i] = cornerColor;
+
+ // Bottom-right corner
+ if (boxBottom - i >= 0 && boxRight - 5 >= 0)
+ frameBuffer[(boxBottom - i) * width + boxRight - 5] = cornerColor;
+ if (boxBottom - 5 >= 0 && boxRight - i >= 0)
+ frameBuffer[(boxBottom - 5) * width + boxRight - i] = cornerColor;
+ }
+
+ // Draw "PAUSED" text centered
+ // Use hardcoded "PAUSED" string (TRS string 0x79 is "Quit Game" in RA2)
+ const char *pauseText = "PAUSED";
+
+ // Draw text using SmushFont if available
+ if (_menuFont) {
+ Common::Rect clipRect(0, 0, width, height);
+
+ // Calculate centered position
+ // Text should be centered horizontally and vertically in the box
+ int textX = width / 2; // SmushFont handles centering with kStyleAlignCenter
+ int textY = height / 2 - 4; // Slightly above center
+
+ // Draw with color 4 and background 0x10 (matching original parameters)
+ // FUN_00434cb0 params: x=10, y=10 or 20, color=4, bg=0x10
+ _menuFont->drawString(pauseText, frameBuffer, clipRect, textX, textY, 0x10, kStyleAlignCenter);
+ } else if (_smush_smalfontNut) {
+ // Fallback: draw using NutRenderer directly
+ NutRenderer *font = _smush_smalfontNut;
+ int numFontChars = font->getNumChars();
+ Common::Rect clipRect(0, 0, width, height);
+
+ // Calculate text width
+ int textWidth = 0;
+ const char *p = pauseText;
+ while (*p) {
+ byte c = (byte)*p++;
+ if (c >= 'a' && c <= 'z') c = c - 'a' + 'A';
+ if (c < numFontChars) {
+ textWidth += font->getCharWidth(c);
+ }
+ }
+
+ // Draw centered
+ int textX = (width - textWidth) / 2;
+ int textY = height / 2 - 4;
+
+ p = pauseText;
+ while (*p) {
+ byte c = (byte)*p++;
+ if (c >= 'a' && c <= 'z') c = c - 'a' + 'A';
+ if (c < numFontChars && textX >= 0 && textY >= 0) {
+ font->drawCharV7(frameBuffer, clipRect, textX, textY, width, -1,
+ kStyleAlignLeft, c, true, true);
+ textX += font->getCharWidth(c);
+ }
+ }
+ }
+
+ // Update the screen to show the pause overlay
+ // SmushPlayer uses copyRectToScreen to transfer the buffer to the display backend
+ _vm->_system->copyRectToScreen(frameBuffer, width, 0, 0, width, height);
+ _vm->_system->updateScreen();
+
+ debug("showPauseOverlay: Overlay displayed");
+}
+
int InsaneRebel2::runMainMenu() {
// Main menu loop - emulates FUN_004147B2
// Returns:
diff --git a/engines/scumm/insane/insane_rebel.h b/engines/scumm/insane/insane_rebel.h
index bc73cb4882b..bc3e01364ff 100644
--- a/engines/scumm/insane/insane_rebel.h
+++ b/engines/scumm/insane/insane_rebel.h
@@ -400,6 +400,11 @@ public:
// Render score text to HUD (called from procPostRendering)
void renderScoreHUD(byte *renderBitmap, int pitch, int width, int height, int statusBarY);
+ // ======================= Pause Overlay =======================
+ // Show pause overlay with dimming effect and "PAUSED" text
+ // Emulates FUN_405A21 pause rendering (lines 242-305)
+ void showPauseOverlay();
+
// Target lock timer (DAT_00443676) - set to 7 when crosshair is over enemy
int _targetLockTimer;
Commit: 1fa930ed850195e4957687b09ffc1d084853b292
https://github.com/scummvm/scummvm/commit/1fa930ed850195e4957687b09ffc1d084853b292
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:08:53+02:00
Commit Message:
SCUMM: RA2: Load directional ship sprites
Changed paths:
engines/scumm/insane/insane_rebel.cpp
engines/scumm/insane/insane_rebel.h
engines/scumm/nut_renderer.cpp
engines/scumm/nut_renderer.h
engines/scumm/smush/smush_player.cpp
diff --git a/engines/scumm/insane/insane_rebel.cpp b/engines/scumm/insane/insane_rebel.cpp
index 5ec985f0f88..aaabb44372f 100644
--- a/engines/scumm/insane/insane_rebel.cpp
+++ b/engines/scumm/insane/insane_rebel.cpp
@@ -100,7 +100,7 @@ InsaneRebel2::InsaneRebel2(ScummEngine_v7 *scumm) {
_smush_mouseoverNut = new NutRenderer(_vm, "SYSTM/MSTOVER.NUT");
_enemies.clear();
- _rebelHandler = 8; // Default to Handler 8 (ground vehicle) for Level 1
+ _rebelHandler = 8; // Default to Handler 8 (third-person vehicle) for Level 1
_rebelLevelType = 0; // Level type from Opcode 6 par3, determines HUD sprite variant
_introCursorPushed = false; // Cursor state tracking for intro sequences
@@ -232,7 +232,7 @@ InsaneRebel2::InsaneRebel2(ScummEngine_v7 *scumm) {
_shots[i].counter = 0;
}
- for (i = 0; i < 5; i++) {
+ for (i = 0; i < 16; i++) {
_rebelEmbeddedHud[i].pixels = nullptr;
_rebelEmbeddedHud[i].width = 0;
_rebelEmbeddedHud[i].height = 0;
@@ -241,6 +241,29 @@ InsaneRebel2::InsaneRebel2(ScummEngine_v7 *scumm) {
_rebelEmbeddedHud[i].valid = false;
}
+ // Initialize Handler 8 ship system
+ _shipSprite = nullptr;
+ _shipSprite2 = nullptr;
+ _shipOverlay1 = nullptr;
+ _shipOverlay2 = nullptr;
+ _shipPosX = 0xa0; // Start centered (160 in hex)
+ _shipPosY = 0x28; // Start at vertical center (40)
+ _shipTargetX = 0xa0;
+ _shipTargetY = 0x28;
+ _shipLevelMode = 0;
+ _shipFiring = false;
+ _shipDirectionH = 2; // Start centered horizontally (0-4 range)
+ _shipDirectionV = 3; // Start centered vertically (0-6 range)
+ _shipDirectionIndex = 2 * 7 + 3; // Center = 17
+
+ // Initialize Handler 7 FLY ship system
+ _flyShipSprite = nullptr; // FLY001 - 35 direction frames
+ _flyLaserSprite = nullptr; // FLY002 - laser sprites
+ _flyTargetSprite = nullptr; // FLY003 - targeting overlay
+ _flyHiResSprite = nullptr; // FLY004 - high-res alternative
+ _flyShipScreenX = 0xd4; // Start at center (212) - matches DAT_00443708 default
+ _flyShipScreenY = 0x82; // Start at center (130) - matches DAT_0044370a default
+
// Initialize audio system for RA2 (since we don't use iMUSE)
_audioSampleRate = 11025; // RA2 audio is 11025 Hz, not 22050 Hz
for (i = 0; i < kRA2MaxAudioTracks; i++) {
@@ -293,6 +316,24 @@ InsaneRebel2::~InsaneRebel2() {
delete _smush_smalfontNut;
delete _smush_titlefontNut;
delete _smush_mouseoverNut;
+
+ // Clean up Handler 8 ship sprites
+ delete _shipSprite;
+ delete _shipSprite2;
+ delete _shipOverlay1;
+ delete _shipOverlay2;
+
+ // Clean up Handler 7 FLY ship sprites
+ delete _flyShipSprite;
+ delete _flyLaserSprite;
+ delete _flyTargetSprite;
+ delete _flyHiResSprite;
+
+ // Clean up embedded HUD overlays
+ for (int i = 0; i < 16; i++) {
+ free(_rebelEmbeddedHud[i].pixels);
+ _rebelEmbeddedHud[i].pixels = nullptr;
+ }
}
bool InsaneRebel2::notifyEvent(const Common::Event &event) {
@@ -737,13 +778,51 @@ void InsaneRebel2::iactRebel2Scene1(byte *renderBitmap, int32 codecparam, int32
// Opcode 5: Special handling based on par2 value
// Disassembly shows sub-opcodes 0xD (13) and 0xE (14)
debug("Rebel2 IACT Opcode 5: par2=%d par3=%d par4=%d", par2, par3, par4);
-
+
+ } else if (par1 == 7) {
+ // Opcode 7: Sprite/HUD control for Handler 7 (space flight levels like Level 3)
+ // par2 = control type (41 = sprite selection?)
+ // par3 = usually 0
+ // par4 = sprite/slot ID (0 or 5 seen in Level 3)
+ //
+ // This opcode may control which ship direction sprite to display
+ // or reference embedded graphics loaded elsewhere
+ debug("Rebel2 IACT Opcode 7: par2=%d par3=%d par4=%d handler=%d",
+ par2, par3, par4, _rebelHandler);
+
+ // Read remaining IACT data to understand structure
+ int64 startPos = b.pos();
+ int64 remaining = b.size() - startPos;
+ if (remaining > 0 && remaining <= 64) {
+ byte payload[64];
+ int bytesRead = b.read(payload, MIN((int64)64, remaining));
+ debug("Rebel2 Opcode 7: payload (%d bytes): %02X %02X %02X %02X %02X %02X %02X %02X",
+ bytesRead,
+ bytesRead > 0 ? payload[0] : 0, bytesRead > 1 ? payload[1] : 0,
+ bytesRead > 2 ? payload[2] : 0, bytesRead > 3 ? payload[3] : 0,
+ bytesRead > 4 ? payload[4] : 0, bytesRead > 5 ? payload[5] : 0,
+ bytesRead > 6 ? payload[6] : 0, bytesRead > 7 ? payload[7] : 0);
+ b.seek(startPos);
+ }
+
+ // par2 == 41 (0x29) seems to be a common value
+ // This might be a "show sprite" command referencing par4 as the slot
+ if (par2 == 41) {
+ // par4 could be a HUD slot or sprite index
+ // For Handler 7, set which embedded HUD frame to display
+ if (_rebelHandler == 7 && par4 >= 0 && par4 < 16) {
+ // Mark this slot as the active one for direction-based rendering
+ // This will be used in post-rendering to know which frame to show
+ debug("Rebel2 Opcode 7: Activating HUD slot %d for Handler 7", par4);
+ }
+ }
+
} else if (par1 == 6) {
// Opcode 6: Level setup / mode switch (FUN_41CADB case 4)
iactRebel2Opcode6(renderBitmap, b, par2, par3, par4);
} else if (par1 == 8) {
// Opcode 8: HUD resource loading (FUN_41CADB case 6)
- iactRebel2Opcode8(renderBitmap, b, par2, par3, par4);
+ iactRebel2Opcode8(renderBitmap, b, size, par2, par3, par4);
} else if (par1 == 9) {
// Opcode 9: Text/subtitle display
iactRebel2Opcode9(renderBitmap, b, par2, par3, par4);
@@ -901,12 +980,14 @@ void InsaneRebel2::iactRebel2Opcode6(byte *renderBitmap, Common::SeekableReadStr
// Opcode 6: Level setup / mode switch
// Based on FUN_41CADB case 4 (switch on *local_14 - 2 == 4, meaning opcode 6)
//
- // Assembly behavior:
- // 1. If par4 == 1: Draw status bar sprite 5, clear link tables, reset state
- // 2. Set level type (DAT_00457900 = par3)
- // 3. Handle autopilot/control mode logic
- // 4. Update damage level counter
- // 5. Calculate view offsets based on level type
+ // For Handler 8 (third-person vehicle) - FUN_00401234 case 4:
+ // - par3 sets ship level mode (DAT_0043e000)
+ // - par4 == 1 triggers status bar display and state reset
+ // - Updates ship position based on mouse input
+ //
+ // For Handler 0x26/0x19 (turret/space):
+ // - Same par4 == 1 behavior
+ // - Different view offset calculations
debug("Rebel2 IACT Opcode 6: par2=%d par3=%d par4=%d", par2, par3, par4);
@@ -916,6 +997,218 @@ void InsaneRebel2::iactRebel2Opcode6(byte *renderBitmap, Common::SeekableReadStr
debug("Rebel2 Opcode 6: Setting handler=%d", par2);
}
+ // Handler 8 specific logic (third-person vehicle) - FUN_00401234 case 4
+ if (_rebelHandler == 8) {
+ // Set ship level mode (DAT_0043e000 = par3)
+ _shipLevelMode = par3;
+
+ // If par4 == 1, enable status bar and reset state
+ if (par4 == 1) {
+ _rebelStatusBarSprite = 5; // Status bar sprite for Handler 8
+ // Reset link tables
+ for (int i = 0; i < 512; i++) {
+ _rebelLinks[i][0] = 0;
+ _rebelLinks[i][1] = 0;
+ _rebelLinks[i][2] = 0;
+ }
+ debug("Rebel2 Opcode 6 (Handler 8): Status bar enabled, state reset");
+ }
+
+ // Skip position calculation for special modes 4 and 5
+ if (_shipLevelMode != 4 && _shipLevelMode != 5) {
+ // Calculate target position from mouse input
+ // Mouse X maps to ship horizontal tilt, Mouse Y to vertical tilt
+ // Based on FUN_00401234 lines 151-166:
+ // local_18 = ((DAT_0047a7e0 * 5 + 0x27b) * 0x40) / 0xfe
+ // local_1c = ((DAT_0047a7e2 * 5 + 0x27b) * 0x10) / 0xfe
+
+ // Map mouse position (-127 to 127 range) to ship target
+ // Mouse is 0-320, center is 160. Map to -127 to 127 range
+ int16 mouseOffsetX = (int16)((_vm->_mouse.x - 160) * 127 / 160);
+ int16 mouseOffsetY = (int16)((_vm->_mouse.y - 100) * 127 / 100);
+
+ // Clamp to valid range
+ if (mouseOffsetX > 127) mouseOffsetX = 127;
+ if (mouseOffsetX < -127) mouseOffsetX = -127;
+ if (mouseOffsetY > 127) mouseOffsetY = 127;
+ if (mouseOffsetY < -127) mouseOffsetY = -127;
+
+ // Calculate target positions using the original formula
+ _shipTargetX = (int16)(((mouseOffsetX * 5 + 0x27b) * 0x40) / 0xfe);
+ _shipTargetY = (int16)(-((mouseOffsetY * 5 + 0x27b) * 0x10) / 0xfe);
+
+ // Smooth interpolation toward target (max 50 pixels per frame)
+ const int16 maxStep = 50; // 0x32 in hex
+ if (_shipPosX < _shipTargetX) {
+ int16 newX = _shipPosX + maxStep;
+ _shipPosX = (newX > _shipTargetX) ? _shipTargetX : newX;
+ } else if (_shipPosX > _shipTargetX) {
+ int16 newX = _shipPosX - maxStep;
+ _shipPosX = (newX < _shipTargetX) ? _shipTargetX : newX;
+ }
+
+ if (_shipPosY < _shipTargetY) {
+ int16 newY = _shipPosY + maxStep;
+ _shipPosY = (newY > _shipTargetY) ? _shipTargetY : newY;
+ } else if (_shipPosY > _shipTargetY) {
+ int16 newY = _shipPosY - maxStep;
+ _shipPosY = (newY < _shipTargetY) ? _shipTargetY : newY;
+ }
+
+ // Calculate ship direction indices for sprite selection
+ // Map mouse position to 5x7 direction grid (like Handler 7)
+ int16 mouseX = _vm->_mouse.x;
+ int16 mouseY = _vm->_mouse.y;
+
+ // Scale mouse if video is larger than 320x200
+ if (_player && _player->_width > 320) {
+ mouseX = (mouseX * 320) / _player->_width;
+ }
+ if (_player && _player->_height > 200) {
+ mouseY = (mouseY * 200) / _player->_height;
+ }
+
+ // Horizontal: 5 zones (0=far left, 2=center, 4=far right)
+ if (mouseX < 64) _shipDirectionH = 0;
+ else if (mouseX < 128) _shipDirectionH = 1;
+ else if (mouseX < 192) _shipDirectionH = 2;
+ else if (mouseX < 256) _shipDirectionH = 3;
+ else _shipDirectionH = 4;
+
+ // Vertical: 7 zones (0=far up, 3=center, 6=far down)
+ if (mouseY < 28) _shipDirectionV = 0;
+ else if (mouseY < 57) _shipDirectionV = 1;
+ else if (mouseY < 86) _shipDirectionV = 2;
+ else if (mouseY < 114) _shipDirectionV = 3;
+ else if (mouseY < 143) _shipDirectionV = 4;
+ else if (mouseY < 171) _shipDirectionV = 5;
+ else _shipDirectionV = 6;
+
+ _shipDirectionIndex = _shipDirectionH * 7 + _shipDirectionV;
+ }
+
+ // Update firing state from mouse button
+ _shipFiring = (_vm->VAR(_vm->VAR_LEFTBTN_HOLD) != 0);
+
+ debug("Rebel2 Opcode 6 (Handler 8): mode=%d shipPos=(%d,%d) target=(%d,%d) firing=%d dir=(%d,%d,%d)",
+ _shipLevelMode, _shipPosX, _shipPosY, _shipTargetX, _shipTargetY, _shipFiring,
+ _shipDirectionH, _shipDirectionV, _shipDirectionIndex);
+
+ // Handler 8 doesn't use the same view offset logic as other handlers
+ // Skip the rest of the function for Handler 8
+ return;
+ }
+
+ // Handler 7 specific logic (space flight) - FUN_0040d836 / FUN_0040c3cc
+ // Used for Level 3 and similar space combat levels
+ if (_rebelHandler == 7) {
+ // Set level mode (same as Handler 8)
+ _shipLevelMode = par3;
+
+ // If par4 == 1, enable status bar
+ if (par4 == 1) {
+ _rebelStatusBarSprite = 5; // Status bar sprite
+ debug("Rebel2 Opcode 6 (Handler 7): Status bar enabled");
+ }
+
+ // Update ship screen position from mouse
+ // Handler 7 uses DAT_0044370c (Y) and DAT_0044370e (X) for screen position
+ // Get raw mouse position
+ int16 rawMouseX = _vm->_mouse.x;
+ int16 rawMouseY = _vm->_mouse.y;
+
+ // Scale mouse to 320x200 logical space if video is larger
+ int16 mouseX = rawMouseX;
+ int16 mouseY = rawMouseY;
+ if (_player && _player->_width > 320) {
+ mouseX = (rawMouseX * 320) / _player->_width;
+ }
+ if (_player && _player->_height > 200) {
+ mouseY = (rawMouseY * 200) / _player->_height;
+ }
+
+ // Clamp to screen bounds (matching FUN_0040c3cc bounds)
+ if (mouseX < 0) mouseX = 0;
+ if (mouseX > 319) mouseX = 319;
+ if (mouseY < 0) mouseY = 0;
+ if (mouseY > 199) mouseY = 199;
+
+ // Update ship position with smooth interpolation
+ // FUN_0040c3cc uses complex smoothing, we use simpler immediate response
+ const int16 maxStep = 15;
+ if (_shipPosX < mouseX) {
+ _shipPosX = MIN((int16)(_shipPosX + maxStep), mouseX);
+ } else if (_shipPosX > mouseX) {
+ _shipPosX = MAX((int16)(_shipPosX - maxStep), mouseX);
+ }
+ if (_shipPosY < mouseY) {
+ _shipPosY = MIN((int16)(_shipPosY + maxStep), mouseY);
+ } else if (_shipPosY > mouseY) {
+ _shipPosY = MAX((int16)(_shipPosY - maxStep), mouseY);
+ }
+
+ // Update Handler 7 screen position (DAT_0044370c/e)
+ // These track the actual on-screen position for direction calculation
+ _flyShipScreenX = _shipPosX;
+ _flyShipScreenY = _shipPosY;
+
+ // Calculate ship direction from position (FUN_0040d836 lines 88-106)
+ // Formula from assembly:
+ // hDir = (0xa0 - posX) >> 6 (with signed rounding)
+ // vDir = (0x95 - posY) / 0x2b
+ // dirIndex = hDir * 7 + vDir
+ //
+ // Note: The assembly formula gives:
+ // hDir: 0-4 where 2 is center (0xa0=160, range is -96 to +96, >> 6 gives -1 to 1, but clamped to 0-4)
+ // vDir: 0-6 where 3 is center (0x95=149, 0x2b=43, so 149/43 â 3.5)
+ //
+ // Simplified direction calculation based on mouse position relative to center
+
+ // Horizontal direction (0-4, center=2)
+ // Formula: (160 - posX) >> 6, clamped to 0-4
+ int16 hDiff = 160 - _flyShipScreenX;
+ int16 hDir = (hDiff + 64) >> 6; // Add 64 to shift range, divide by 64
+ if (hDir < 0) hDir = 0;
+ if (hDir > 4) hDir = 4;
+
+ // Vertical direction (0-6, center=3)
+ // Formula: (149 - posY) / 43, clamped to 0-6
+ int16 vDir = (149 - _flyShipScreenY) / 43;
+ if (vDir < 0) vDir = 0;
+ if (vDir > 6) vDir = 6;
+
+ // Additional adjustment from assembly (lines 90-105):
+ // If vDir==3 and abs(posY) > 10, adjust by +/-1
+ // If hDir==2 and abs(posX) > 15, adjust by +/-1
+ // This creates a "deadzone" at center to reduce flicker
+ if (vDir == 3 && ABS(_flyShipScreenY - 100) > 10) {
+ if (_flyShipScreenY < 100) vDir = 2;
+ else vDir = 4;
+ }
+ if (hDir == 2 && ABS(_flyShipScreenX - 160) > 15) {
+ if (_flyShipScreenX < 160) hDir = 3;
+ else hDir = 1;
+ }
+
+ _shipDirectionH = hDir;
+ _shipDirectionV = vDir;
+ _shipDirectionIndex = hDir * 7 + vDir;
+
+ // Clamp direction index to valid range (0-34)
+ if (_shipDirectionIndex < 0) _shipDirectionIndex = 0;
+ if (_shipDirectionIndex > 34) _shipDirectionIndex = 34;
+
+ // Update firing state
+ _shipFiring = (_vm->VAR(_vm->VAR_LEFTBTN_HOLD) != 0);
+
+ debug("Rebel2 Handler7: rawMouse=(%d,%d) scaled=(%d,%d) shipPos=(%d,%d) screenPos=(%d,%d) dir=(%d,%d) idx=%d flySprite=%p",
+ rawMouseX, rawMouseY, mouseX, mouseY, _shipPosX, _shipPosY,
+ _flyShipScreenX, _flyShipScreenY, _shipDirectionH, _shipDirectionV, _shipDirectionIndex,
+ (void*)_flyShipSprite);
+
+ return;
+ }
+
// Step 1: If par4 == 1, initialize/reset state (lines 114-121)
if (par4 == 1) {
// Draw status bar sprite 5 (FUN_0040bb87 equivalent)
@@ -1088,11 +1381,11 @@ void InsaneRebel2::iactRebel2Opcode6(byte *renderBitmap, Common::SeekableReadStr
}
}
-void InsaneRebel2::iactRebel2Opcode8(byte *renderBitmap, Common::SeekableReadStream &b, int16 par2, int16 par3, int16 par4) {
- // Opcode 8: HUD resource loading
+void InsaneRebel2::iactRebel2Opcode8(byte *renderBitmap, Common::SeekableReadStream &b, int32 chunkSize, int16 par2, int16 par3, int16 par4) {
+ // Opcode 8: HUD/Ship resource loading
// Based on FUN_41CADB case 6 (switch on *local_14 - 2 == 6, meaning opcode 8)
//
- // par3 determines which HUD slot to load:
+ // For Handler 0x26 (turret) - par3 determines HUD slot:
// case 1: DAT_00482240 - Primary HUD overlay (GRD001)
// case 2: DAT_00482238 - Secondary HUD graphics (GRD002)
// case 4: DAT_00482268 - Ship cockpit frame (GRD010)
@@ -1101,50 +1394,222 @@ void InsaneRebel2::iactRebel2Opcode8(byte *renderBitmap, Common::SeekableReadStr
// case 7: DAT_00482248 - Damage indicator (GRD004)
// case 10: DAT_00482258 - Additional effects (GRD005)
// case 12/13: DAT_00482260 - High-res HUD alternative (GRD007)
- // cases 21-27: Sound loading slot 0
- // cases 31-37: Sound loading slot 1
- // case 40: Sound loading slot 3
- // cases 41-47: Sound loading slot 2
+ //
+ // For Handler 8 (third-person vehicle) - par3 determines ship sprite slot (FUN_00401234):
+ // case 1: DAT_0047e010 - Primary ship sprite (POV001)
+ // case 3: DAT_0047e028 - Secondary ship sprite (POV004)
+ // case 6: DAT_0047e020 - Ship overlay 1 (POV002)
+ // case 7: DAT_0047e018 - Ship overlay 2 (POV003)
+ //
+ // cases 21-47: Sound loading (both handlers)
- debug("Rebel2 IACT Opcode 8: par2=%d par3=%d par4=%d (HUD loading)", par2, par3, par4);
+ debug("Rebel2 IACT Opcode 8: handler=%d par2=%d par3=%d par4=%d (gameState=%d)", _rebelHandler, par2, par3, par4, _gameState);
- // Read the data size from the stream (at offset +14, which is local_14[7])
- // The actual NUT/resource data starts at offset +18 (param_5 + 9 in assembly)
int64 startPos = b.pos();
+ int64 remaining = (chunkSize > 0) ? chunkSize : (b.size() - startPos);
+
+ // Handler 7 FLY NUT loading - fixed offset format (FUN_0040c3cc case 6)
+ // IACT structure after par1-par4 (we're at offset +8):
+ // +0-5 (6 bytes): additional header
+ // +6-9 (4 bytes): NUT data size (little-endian)
+ // +10+: NUT data
+ // Assembly: param_5[7] = size at offset 14, param_5+9 = data at offset 18
+ // Since we've read 8 bytes (par1-par4), that's offset +6 and +10 from current pos
+ //
+ // IMPORTANT: The assembly switches on param_5[3] which is the 4th short (bytes 6-7)
+ // In SMUSH handleIACT, this corresponds to userId (par4), NOT unknown (par3)!
+ // So we use par4 for the FLY slot selection.
+ bool isHandler7FLY = (_rebelHandler == 7 && (par4 == 1 || par4 == 2 || par4 == 3 || par4 == 11));
+
+ if (isHandler7FLY && remaining >= 14) {
+ // Read additional header and size from fixed offset
+ // Based on FUN_0040c3cc assembly:
+ // param_5[7] = size at offset 14 (we're at offset 8, so read 6+4 bytes)
+ // param_5+9 = data at offset 18
+ byte header[10];
+ if (b.read(header, 10) == 10) {
+ // Debug: dump all header bytes
+ debug("Rebel2 Opcode 8 Handler7: header bytes: %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X",
+ header[0], header[1], header[2], header[3], header[4],
+ header[5], header[6], header[7], header[8], header[9]);
+
+ // Size is at offset 14 from IACT start = bytes 6-9 of our header buffer
+ // Try both endianness
+ uint32 nutSizeLE = READ_LE_UINT32(header + 6);
+ uint32 nutSizeBE = READ_BE_UINT32(header + 6);
+ debug("Rebel2 Opcode 8 Handler7: par4=%d sizesLE=%u sizeBE=%u remaining=%lld",
+ par4, nutSizeLE, nutSizeBE, (long long)remaining);
+
+ // The assembly uses direct memory read on x86 which is LE
+ uint32 nutSize = nutSizeLE;
+
+ if (nutSize > 0 && nutSize <= (uint32)(remaining - 10)) {
+ byte *nutData = (byte *)malloc(nutSize);
+ if (nutData) {
+ int bytesRead = b.read(nutData, nutSize);
+ debug("Rebel2 Opcode 8 Handler7: Read %d/%u bytes of NUT data, first 16: %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X",
+ bytesRead, nutSize,
+ bytesRead > 0 ? nutData[0] : 0, bytesRead > 1 ? nutData[1] : 0,
+ bytesRead > 2 ? nutData[2] : 0, bytesRead > 3 ? nutData[3] : 0,
+ bytesRead > 4 ? nutData[4] : 0, bytesRead > 5 ? nutData[5] : 0,
+ bytesRead > 6 ? nutData[6] : 0, bytesRead > 7 ? nutData[7] : 0,
+ bytesRead > 8 ? nutData[8] : 0, bytesRead > 9 ? nutData[9] : 0,
+ bytesRead > 10 ? nutData[10] : 0, bytesRead > 11 ? nutData[11] : 0,
+ bytesRead > 12 ? nutData[12] : 0, bytesRead > 13 ? nutData[13] : 0,
+ bytesRead > 14 ? nutData[14] : 0, bytesRead > 15 ? nutData[15] : 0);
+
+ // Verify we read the expected amount
+ if (bytesRead != (int)nutSize) {
+ warning("Rebel2 Opcode 8 Handler7: Short read! Got %d expected %u", bytesRead, nutSize);
+ }
- // Skip to where embedded data would be and scan for ANIM tag
- // The assembly shows data at param_5 + 9 (18 bytes from IACT header start)
- // Since we're already past the header params, scan remaining data
+ // Verify ANIM header
+ if (bytesRead >= 8) {
+ uint32 animTag = READ_BE_UINT32(nutData);
+ uint32 animSize = READ_BE_UINT32(nutData + 4);
+ debug("Rebel2 Opcode 8 Handler7: ANIM tag=%08X size=%u (expected %08X, size should be ~%d)",
+ animTag, animSize, MKTAG('A','N','I','M'), bytesRead - 8);
+ if (animTag != MKTAG('A','N','I','M')) {
+ warning("Rebel2 Opcode 8 Handler7: No ANIM tag! Data may be corrupted");
+ }
+ if ((int32)animSize > bytesRead - 8) {
+ warning("Rebel2 Opcode 8 Handler7: ANIM size %u exceeds data %d", animSize, bytesRead - 8);
+ }
+ }
+
+ // Try loading as NUT
+ NutRenderer *newNut = new NutRenderer(_vm, nutData, bytesRead);
+ if (newNut && newNut->getNumChars() > 0) {
+ debug("Rebel2 Opcode 8 Handler7: Loaded FLY NUT par4=%d with %d sprites",
+ par4, newNut->getNumChars());
+
+ // Switch on par4 (userId) - matches assembly param_5[3]
+ switch (par4) {
+ case 1: // FLY001 - Ship direction sprites (35 frames)
+ delete _flyShipSprite;
+ _flyShipSprite = newNut;
+ debug("Rebel2 Opcode 8: _flyShipSprite set with %d sprites", newNut->getNumChars());
+ break;
+ case 2: // FLY003 - Targeting overlay
+ delete _flyTargetSprite;
+ _flyTargetSprite = newNut;
+ break;
+ case 3: // FLY002 - Laser fire sprites
+ delete _flyLaserSprite;
+ _flyLaserSprite = newNut;
+ break;
+ case 11: // FLY004 - High-res alternative
+ delete _flyHiResSprite;
+ _flyHiResSprite = newNut;
+ break;
+ default:
+ delete newNut;
+ break;
+ }
+ } else {
+ debug("Rebel2 Opcode 8 Handler7: NUT load failed for par4=%d", par4);
+ delete newNut;
+ }
+ free(nutData);
+ }
+ }
+ }
+ b.seek(startPos);
+ return;
+ }
- int64 totalSize = b.size();
- if (totalSize > startPos) {
- int64 remaining = totalSize - startPos;
+ // For non-Handler7 or non-FLY cases, scan for ANIM tag
+ debug("Rebel2 Opcode 8: startPos=%lld chunkSize=%d remaining=%lld", (long long)startPos, chunkSize, (long long)remaining);
+ if (remaining > 0) {
int scanSize = (int)MIN<int64>(remaining, 65536);
byte *scanBuf = (byte *)malloc(scanSize);
if (scanBuf) {
int bytesRead = b.read(scanBuf, scanSize);
-
- // Look for ANIM tag (embedded SAN)
+ debug("Rebel2 Opcode 8: Read %d bytes, first 16: %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X",
+ bytesRead,
+ bytesRead > 0 ? scanBuf[0] : 0, bytesRead > 1 ? scanBuf[1] : 0,
+ bytesRead > 2 ? scanBuf[2] : 0, bytesRead > 3 ? scanBuf[3] : 0,
+ bytesRead > 4 ? scanBuf[4] : 0, bytesRead > 5 ? scanBuf[5] : 0,
+ bytesRead > 6 ? scanBuf[6] : 0, bytesRead > 7 ? scanBuf[7] : 0,
+ bytesRead > 8 ? scanBuf[8] : 0, bytesRead > 9 ? scanBuf[9] : 0,
+ bytesRead > 10 ? scanBuf[10] : 0, bytesRead > 11 ? scanBuf[11] : 0,
+ bytesRead > 12 ? scanBuf[12] : 0, bytesRead > 13 ? scanBuf[13] : 0,
+ bytesRead > 14 ? scanBuf[14] : 0, bytesRead > 15 ? scanBuf[15] : 0);
+
+ // Look for ANIM tag (embedded SAN or NUT)
for (int i = 0; i + 8 <= bytesRead; ++i) {
if (READ_BE_UINT32(scanBuf + i) == MKTAG('A','N','I','M')) {
+ debug("Rebel2 Opcode 8: Found ANIM at offset %d", i);
int64 animStreamPos = startPos + i;
uint32 animReportedSize = READ_BE_UINT32(scanBuf + i + 4);
- int32 toCopy = (int)MIN<int64>((int64)animReportedSize + 8, totalSize - animStreamPos);
+ // Limit toCopy to remaining data in this chunk
+ int32 toCopy = (int)MIN<int64>((int64)animReportedSize + 8, remaining - i);
if (toCopy > 0) {
byte *animData = (byte *)malloc(toCopy);
if (animData) {
b.seek(animStreamPos);
b.read(animData, toCopy);
- // Map par3 to userId for HUD slots
- int userId = 0;
- switch (par3) {
- case 1: userId = 1; break; // Primary HUD
- case 2: userId = 2; break; // Secondary HUD
- case 4: userId = 3; break; // Cockpit frame
- case 6: userId = 4; break; // Explosion overlay
- default: userId = par3; break;
+
+ // Handler 8 (Third-Person Vehicle) - FUN_00401234 case 6:
+ // par3 == 1: POV001 -> DAT_0047e010 (primary ship)
+ // par3 == 3: POV004 -> DAT_0047e028 (secondary ship)
+ // par3 == 6: POV002 -> DAT_0047e020 (overlay 1)
+ // par3 == 7: POV003 -> DAT_0047e018 (overlay 2)
+ bool isHandler8Par3 = (par3 == 1 || par3 == 3 || par3 == 6 || par3 == 7);
+ bool loadedAsNut = false;
+
+ if (_rebelHandler == 8 && isHandler8Par3) {
+ // Try loading as NUT (ship sprites for Handler 8)
+ NutRenderer *newNut = new NutRenderer(_vm, animData, toCopy);
+ if (newNut && newNut->getNumChars() > 0) {
+ debug("Rebel2 Opcode 8: Loaded ship NUT par3=%d with %d sprites (handler=%d)",
+ par3, newNut->getNumChars(), _rebelHandler);
+ loadedAsNut = true;
+
+ switch (par3) {
+ case 1: // POV001 - Primary ship sprite
+ delete _shipSprite;
+ _shipSprite = newNut;
+ debug("Rebel2 Opcode 8: _shipSprite set to %p", (void*)_shipSprite);
+ break;
+ case 3: // POV004 - Secondary ship sprite
+ delete _shipSprite2;
+ _shipSprite2 = newNut;
+ break;
+ case 6: // POV002 - Ship overlay 1
+ delete _shipOverlay1;
+ _shipOverlay1 = newNut;
+ break;
+ case 7: // POV003 - Ship overlay 2
+ delete _shipOverlay2;
+ _shipOverlay2 = newNut;
+ break;
+ default:
+ delete newNut;
+ loadedAsNut = false;
+ break;
+ }
+ } else {
+ debug("Rebel2 Opcode 8: NUT load failed for par3=%d, trying as embedded SAN", par3);
+ delete newNut;
+ }
+ }
+
+ if (!loadedAsNut) {
+ // Load as embedded SAN (HUD overlays)
+ // The userId comes from par4 (not par3!)
+ // par4 values seen in Level 3:
+ // 1000 = audio track
+ // 1-11 = HUD overlay slots
+ // The embedded HUD system uses userId to track different overlay elements
+ int userId = par4;
+
+ // Audio tracks (userId >= 1000) are handled separately, skip them
+ if (userId < 1000) {
+ debug("Rebel2 Opcode 8: Loading embedded SAN as HUD userId=%d", userId);
+ loadEmbeddedSan(userId, animData, toCopy, renderBitmap);
+ }
}
- loadEmbeddedSan(userId, animData, toCopy, renderBitmap);
free(animData);
}
}
@@ -1445,8 +1910,8 @@ extern void smushDecodeRLE(byte *dst, const byte *src, int left, int top, int wi
extern void smushDecodeUncompressed(byte *dst, const byte *src, int left, int top, int width, int height, int pitch);
void InsaneRebel2::loadEmbeddedSan(int userId, byte *animData, int32 size, byte *renderBitmap) {
- // Validate userId (0 for menu background, 1-4 for HUD slots)
- if (userId < 0 || userId > 4 || !animData || size < 32) {
+ // Validate userId - Level 3 uses slots 0-11, allow up to 15 for safety
+ if (userId < 0 || userId > 15 || !animData || size < 32) {
debug("Rebel2: Invalid embedded SAN: userId=%d, size=%d", userId, size);
return;
}
@@ -1477,6 +1942,21 @@ void InsaneRebel2::loadEmbeddedSan(int userId, byte *animData, int32 size, byte
if (subTag == MKTAG('F','O','B','J')) {
// Found FOBJ - Embedded HUD Frame
+ // Dump raw FOBJ bytes for analysis
+ int32 fobjStart = stream.pos();
+ byte rawHeader[20];
+ int headerBytesToRead = MIN((int)subSize, 20);
+ stream.read(rawHeader, headerBytesToRead);
+ stream.seek(fobjStart); // Reset to read normally
+
+ debug("Rebel2: Raw FOBJ header (%d bytes): %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X",
+ headerBytesToRead,
+ rawHeader[0], rawHeader[1], rawHeader[2], rawHeader[3],
+ rawHeader[4], rawHeader[5], rawHeader[6], rawHeader[7],
+ rawHeader[8], rawHeader[9], rawHeader[10], rawHeader[11],
+ rawHeader[12], rawHeader[13], rawHeader[14], rawHeader[15],
+ rawHeader[16], rawHeader[17], rawHeader[18], rawHeader[19]);
+
// Read FOBJ header
int codec = stream.readUint16LE();
int left = stream.readUint16LE();
@@ -1485,8 +1965,8 @@ void InsaneRebel2::loadEmbeddedSan(int userId, byte *animData, int32 size, byte
int height = stream.readUint16LE();
stream.readUint16LE(); // unknown
stream.readUint16LE(); // unknown
-
- debug("Rebel2: Embedded HUD frame: userId=%d, %dx%d at (%d,%d), codec=%d",
+
+ debug("Rebel2: Embedded HUD frame: userId=%d, %dx%d at (%d,%d), codec=%d",
userId, width, height, left, top, codec);
// Allocate storage for the decoded frame
@@ -1494,11 +1974,14 @@ void InsaneRebel2::loadEmbeddedSan(int userId, byte *animData, int32 size, byte
if (width > 0 && height > 0 && width <= 800 && height <= 480) {
if (frame.width != width || frame.height != height || !frame.pixels) {
- frame.pixels = (byte *)calloc(width * height, 1);
+ free(frame.pixels);
+ frame.pixels = (byte *)malloc(width * height);
frame.width = width;
frame.height = height;
}
-
+ // Clear buffer before decode (important for delta codecs)
+ memset(frame.pixels, 0, width * height);
+
// Update render position from FOBJ header
frame.renderX = left;
frame.renderY = top;
@@ -1545,25 +2028,167 @@ void InsaneRebel2::loadEmbeddedSan(int userId, byte *animData, int32 size, byte
frame.valid = true;
debug("Rebel2: Decoded embedded HUD (codec 21/line update): %dx%d", width, height);
} else if (codec == 45) {
- // Codec 45: Block delta (simple copy for now)
- int copySize = MIN((int)dataSize, width * height);
- memcpy(frame.pixels, fobjData, copySize);
+ // Codec 45: RA2-specific codec with BOMP-style RLE
+ // Header: 01 FE 00 00 01 00 (6 bytes)
+ // Byte 0: sub-codec (01)
+ // Byte 1: transparent color (FE = 254)
+ // Bytes 2-5: unknown/padding
+ debug("Rebel2: Codec 45 first 20 bytes: %02X %02X %02X %02X %02X %02X | %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X",
+ fobjData[0], fobjData[1], fobjData[2], fobjData[3],
+ fobjData[4], fobjData[5], fobjData[6], fobjData[7],
+ fobjData[8], fobjData[9], fobjData[10], fobjData[11],
+ fobjData[12], fobjData[13], fobjData[14], fobjData[15],
+ fobjData[16], fobjData[17], fobjData[18], fobjData[19]);
+
+ // Parse 6-byte sub-header
+ int headerSkip = 0;
+ if (dataSize > 6 && fobjData[0] == 0x01 && fobjData[1] == 0xFE) {
+ headerSkip = 6;
+ debug("Rebel2: Codec 45 header: sub-codec=%d, transparent=%d",
+ fobjData[0], fobjData[1]);
+ }
+
+ byte *srcPtr = fobjData + headerSkip;
+ byte *dataEnd = fobjData + dataSize;
+
+ // Try per-line RLE with 2-byte LE size headers
+ int firstVal = READ_LE_UINT16(srcPtr);
+ bool validPerLine = (firstVal > 0 && firstVal <= width * 2);
+
+ if (validPerLine) {
+ debug("Rebel2: Codec 45 using per-line RLE (firstLineSize=%d)", firstVal);
+ for (int row = 0; row < height && srcPtr < dataEnd; row++) {
+ int lineSize = READ_LE_UINT16(srcPtr);
+ srcPtr += 2;
+ if (lineSize <= 0 || lineSize > (int)(dataEnd - srcPtr)) break;
+
+ byte *lineEnd = srcPtr + lineSize;
+ byte *dst = frame.pixels + row * width;
+ int x = 0;
+
+ while (srcPtr < lineEnd && x < width) {
+ byte ctrl = *srcPtr++;
+ int count = (ctrl >> 1) + 1;
+ if (ctrl & 1) {
+ byte color = (srcPtr < lineEnd) ? *srcPtr++ : 0;
+ for (int i = 0; i < count && x < width; i++) dst[x++] = color;
+ } else {
+ for (int i = 0; i < count && x < width && srcPtr < lineEnd; i++)
+ dst[x++] = *srcPtr++;
+ }
+ }
+ srcPtr = lineEnd;
+ }
+ } else {
+ // Try continuous BOMP RLE (no per-line headers)
+ // Each line produces exactly 'width' pixels
+ debug("Rebel2: Codec 45 using continuous BOMP RLE");
+ for (int row = 0; row < height && srcPtr < dataEnd; row++) {
+ byte *dst = frame.pixels + row * width;
+ int x = 0;
+
+ while (x < width && srcPtr < dataEnd) {
+ byte ctrl = *srcPtr++;
+ int count = (ctrl >> 1) + 1;
+
+ if (ctrl & 1) {
+ // RLE fill
+ byte color = (srcPtr < dataEnd) ? *srcPtr++ : 0;
+ for (int i = 0; i < count && x < width; i++) {
+ dst[x++] = color;
+ }
+ } else {
+ // Literal copy
+ for (int i = 0; i < count && x < width && srcPtr < dataEnd; i++) {
+ dst[x++] = *srcPtr++;
+ }
+ }
+ }
+ }
+ }
frame.valid = true;
- debug("Rebel2: Decoded embedded HUD (codec 45/block): %dx%d", width, height);
+
+ // Count non-zero pixels
+ int nonZero = 0;
+ for (int i = 0; i < width * height; i++) {
+ if (frame.pixels[i] != 0) nonZero++;
+ }
+ debug("Rebel2: Decoded codec 45: %dx%d, %d non-zero (%d%%)",
+ width, height, nonZero, (nonZero * 100) / (width * height));
+ } else if (codec == 23) {
+ // Codec 23: Skip/copy with RLE in copy runs
+ // Format: each line has pairs of (skip, copy_count, RLE_data)
+ byte *srcPtr = fobjData;
+ for (int row = 0; row < height && srcPtr < fobjData + dataSize; row++) {
+ int lineDataSize = READ_LE_UINT16(srcPtr);
+ srcPtr += 2;
+ byte *lineEnd = srcPtr + lineDataSize;
+ byte *lineDst = frame.pixels + row * width;
+ int x = 0;
+
+ while (srcPtr < lineEnd && x < width) {
+ int skip = READ_LE_UINT16(srcPtr);
+ srcPtr += 2;
+ x += skip;
+ if (srcPtr >= lineEnd || x >= width) break;
+
+ int runSize = READ_LE_UINT16(srcPtr);
+ srcPtr += 2;
+
+ // Decode RLE within this run
+ byte *runEnd = srcPtr + runSize;
+ while (srcPtr < runEnd && x < width) {
+ byte code = *srcPtr++;
+ int num = (code >> 1) + 1;
+ if (num > width - x) num = width - x;
+
+ if (code & 1) {
+ // RLE run
+ byte color = (srcPtr < runEnd) ? *srcPtr++ : 0;
+ for (int i = 0; i < num && x < width; i++) {
+ lineDst[x++] = color;
+ }
+ } else {
+ // Literal run
+ for (int i = 0; i < num && x < width && srcPtr < runEnd; i++) {
+ lineDst[x++] = *srcPtr++;
+ }
+ }
+ }
+ srcPtr = runEnd;
+ }
+ srcPtr = lineEnd;
+ }
+ frame.valid = true;
+ debug("Rebel2: Decoded embedded HUD (codec 23/skip-RLE): %dx%d", width, height);
} else {
debug("Rebel2: TODO: Decode codec %d for embedded HUD", codec);
frame.valid = false;
}
-
+
+ // Count non-zero pixels to verify frame has content
+ if (frame.valid) {
+ int nonZeroPixels = 0;
+ for (int i = 0; i < width * height; i++) {
+ if (frame.pixels[i] != 0) nonZeroPixels++;
+ }
+ debug("Rebel2: Frame userId=%d has %d non-zero pixels (%d%%)",
+ userId, nonZeroPixels, (nonZeroPixels * 100) / (width * height));
+ }
+
// Draw immediately to renderBitmap if valid
- if (frame.valid && renderBitmap) {
+ // Skip immediate draw for Handler 7/8 - these are ship direction sprites
+ // that should be selected based on direction and drawn during post-rendering
+ bool skipImmediateDraw = (_rebelHandler == 7 || _rebelHandler == 8);
+
+ if (frame.valid && renderBitmap && !skipImmediateDraw) {
int pitch = (_player && _player->_width > 0) ? _player->_width : 320;
int bufHeight = (_player && _player->_height > 0) ? _player->_height : 200;
-
+
for (int y = 0; y < height && (frame.renderY + y) < bufHeight; y++) {
for (int x = 0; x < width && (frame.renderX + x) < pitch; x++) {
byte pixel = frame.pixels[y * width + x];
- if (pixel != 0) { // 0 = transparent
+ if (pixel != 0 && pixel != 231) { // 0 and 231 = transparent
int destX = frame.renderX + x;
int destY = frame.renderY + y;
if (destX >= 0 && destY >= 0) {
@@ -1573,6 +2198,9 @@ void InsaneRebel2::loadEmbeddedSan(int userId, byte *animData, int32 size, byte
}
}
debug("Rebel2: Rendered embedded HUD %d at (%d,%d)", userId, frame.renderX, frame.renderY);
+ } else if (skipImmediateDraw) {
+ debug("Rebel2: Skipped immediate draw for Handler %d HUD %d (will render during post-processing)",
+ _rebelHandler, userId);
}
free(fobjData);
@@ -2058,16 +2686,19 @@ void InsaneRebel2::procPostRendering(byte *renderBitmap, int32 codecparam, int32
return;
}
- // During intro sequences (non-menu):
+ // During intro sequences (non-menu, non-gameplay):
// - Hide the crosshair/cursor (handled by not drawing it)
// - Skip all HUD/status bar rendering
// - Skip mouse input processing
// The mouse cursor is already hidden by SmushPlayer during video playback
- if (introPlaying) {
+ //
+ // IMPORTANT: During gameplay (_gameState == kStateGameplay), we MUST render HUD
+ // even if video flags have 0x20 set (which can happen for some level videos)
+ if (introPlaying && _gameState != kStateGameplay) {
// Track state transition for debugging
if (!_introCursorPushed) {
_introCursorPushed = true;
- debug("Rebel2: Intro sequence detected (flags=0x%x) - HUD disabled", _player->_curVideoFlags);
+ debug("Rebel2: Intro sequence detected (flags=0x%x, state=%d) - HUD disabled", _player->_curVideoFlags, _gameState);
}
// Skip all HUD rendering during intro - subtitles are rendered via opcode 9
return;
@@ -2075,12 +2706,13 @@ void InsaneRebel2::procPostRendering(byte *renderBitmap, int32 codecparam, int32
// Gameplay mode - restore normal rendering
if (_introCursorPushed) {
_introCursorPushed = false;
- debug("Rebel2: Gameplay started - HUD enabled");
+ debug("Rebel2: Gameplay started - HUD enabled (flags=0x%x, state=%d)", _player->_curVideoFlags, _gameState);
}
}
// From here on, we're in gameplay mode (not intro)
- if (!introPlaying) {
+ // Render HUD if NOT in intro mode, OR if we ARE in gameplay state
+ if (!introPlaying || _gameState == kStateGameplay) {
// ============================================================
// STEP 0: Fill status bar background (FUN_004288c0 equivalent)
// ============================================================
@@ -2100,44 +2732,101 @@ void InsaneRebel2::procPostRendering(byte *renderBitmap, int32 codecparam, int32
}
// ============================================================
- // STEP 1: Draw embedded SAN cockpit overlay FIRST (from IACT chunks)
+ // STEP 1: Draw embedded SAN HUD overlays FIRST (from IACT chunks)
// ============================================================
- // The cockpit overlay forms the decorative frame at the bottom
- // userId 1: Left piece at X=0
- // userId 2: Right piece at X=slot1Width
+ // For Handler 7 (Level 3): HUD elements are scattered across the screen
+ // - Each frame has its own renderX/renderY position from FOBJ left/top
+ // - userId 1-11 are different HUD elements
+ // - userIds at the same position are direction variants (select one based on direction)
+ // For turret handlers: slots 1-2 form a two-part cockpit overlay
+ // - userId 1: Left piece at X=0
+ // - userId 2: Right piece at X=slot1Width
// These are drawn BEFORE the status bar so status bar appears on top
-
- int slot1Width = 0;
- if (_rebelEmbeddedHud[1].valid && _rebelEmbeddedHud[1].width > 0) {
- slot1Width = _rebelEmbeddedHud[1].width;
- }
-
- for (int hudSlot = 1; hudSlot <= 2; hudSlot++) {
+
+ // For Handler 7 ship direction: identify ship frames at same position
+ // Level 3 has userId 1 and 2 at position (162, 105) - these are direction variants
+ // We select one based on the current direction to simulate ship turning
+
+ for (int hudSlot = 1; hudSlot < 16; hudSlot++) {
EmbeddedSanFrame &frame = _rebelEmbeddedHud[hudSlot];
if (frame.valid && frame.pixels && frame.width > 0 && frame.height > 0) {
int destX, destY;
-
- // Position the two HUD pieces horizontally adjacent
- if (hudSlot == 1) {
- destX = 0;
- } else {
- destX = slot1Width;
+
+ // For Handler 7: Check if this is a ship direction frame
+ // Ship frames are at the same position - skip all but the selected one
+ if (_rebelHandler == 7) {
+ // Collect all frame IDs in this position group (may not be consecutive)
+ int groupMembers[16];
+ int groupCount = 0;
+
+ for (int id = 1; id < 16; id++) {
+ EmbeddedSanFrame &g = _rebelEmbeddedHud[id];
+ if (g.valid && g.renderX == frame.renderX && g.renderY == frame.renderY &&
+ g.width == frame.width && g.height == frame.height) {
+ groupMembers[groupCount++] = id;
+ }
+ }
+
+ // If there's more than one frame in this group, select based on direction
+ if (groupCount > 1) {
+ // Map direction index (0-34) to group index (0 to groupCount-1)
+ int selectedOffset = _shipDirectionIndex % groupCount;
+ int selectedId = groupMembers[selectedOffset];
+
+ // Check if selected frame has pixels, if not find one that does
+ EmbeddedSanFrame &selectedFrame = _rebelEmbeddedHud[selectedId];
+ int nonZero = 0;
+ for (int i = 0; i < selectedFrame.width * selectedFrame.height; i++) {
+ if (selectedFrame.pixels[i] != 0) nonZero++;
+ }
+
+ if (nonZero == 0) {
+ // Selected frame is empty, find another with pixels
+ for (int i = 0; i < groupCount; i++) {
+ EmbeddedSanFrame &altFrame = _rebelEmbeddedHud[groupMembers[i]];
+ int altNonZero = 0;
+ for (int j = 0; j < altFrame.width * altFrame.height; j++) {
+ if (altFrame.pixels[j] != 0) altNonZero++;
+ }
+ if (altNonZero > 0) {
+ selectedId = groupMembers[i];
+ break;
+ }
+ }
+ }
+
+ // Only render if this is the selected frame
+ if (hudSlot != selectedId) {
+ continue; // Skip this frame, render the selected one instead
+ }
+ }
+ }
+
+ // Use the stored render position from the embedded ANIM data
+ // The renderX/renderY are set from FOBJ's left/top values
+ destX = frame.renderX;
+ destY = frame.renderY;
+
+ // For Handler 7: Apply position offset based on ship position
+ // This makes the ship sprite follow the mouse/crosshair
+ if (_rebelHandler == 7 && destX > 100 && destY > 50) {
+ // This appears to be a ship sprite (center of screen)
+ // Offset based on ship position relative to center
+ int16 offsetX = (_shipPosX - 160) / 8; // Scale down movement
+ int16 offsetY = (_shipPosY - 100) / 8;
+ destX += offsetX;
+ destY += offsetY;
}
-
- // Position at bottom of 320x200 video content
- // The cockpit overlay sits at Y = 200 - frameHeight
- destY = statusBarY - frame.height;
- if (destY < 0) destY = 0;
- // Apply View Offset for static screen elements
+ // Apply View Offset for all HUD elements
destX += _viewX;
destY += _viewY;
-
- // Draw frame with transparency (pixel 0 = transparent)
+
+ // Draw frame with transparency (pixel 0 and 231 = transparent, matching NUT rendering)
for (int y = 0; y < frame.height && (destY + y) < height; y++) {
for (int x = 0; x < frame.width && (destX + x) < pitch; x++) {
byte pixel = frame.pixels[y * frame.width + x];
- if (pixel != 0) { // Skip transparent pixels
+ if (pixel != 0 && pixel != 231) { // Skip transparent pixels (0 and 231/0xE7)
int fx = destX + x;
int fy = destY + y;
if (fx >= 0 && fy >= 0) {
@@ -2248,10 +2937,213 @@ void InsaneRebel2::procPostRendering(byte *renderBitmap, int32 codecparam, int32
// Draw lives indicator - assembly shows at X=0xa8 (168), Y=7
// Uses sprite 1 again with different clip rect
// TODO: Implement lives rendering
-
+
// Draw score - uses FUN_00434cb0 (text rendering) at X=0x101(257)
// TODO: Implement score rendering
- }
+ }
+
+ // ============================================================
+ // HANDLER 8 SHIP RENDERING (FUN_00401ccf equivalent)
+ // ============================================================
+ // For third-person vehicle missions (Handler 8), draw the player's ship sprite
+ // The ship position is calculated from _shipPosX/_shipPosY
+ //
+ // From FUN_00401ccf disassembly (lines 87-95):
+ // - Ship is drawn when DAT_0047e010 != NULL AND DAT_0043e000 != 5
+ // - Position offset X: (DAT_0043e006 - 0xa0) >> 3 = (shipPosX - 160) >> 3
+ // - Position offset Y: (DAT_0043e008 - 0x28) >> 2 = (shipPosY - 40) >> 2
+ // - Sprite index: param_5 & 1 (0 = normal, 1 = firing)
+ //
+ // The crosshair is drawn at lines 128-135 with base position:
+ // - Low-res: X = offset + 0xa0 (160), Y = offset + 0x69 (105)
+ // - High-res: X = offset*2 + 0x140 (320), Y = offset*2 + 0xd2 (210)
+ //
+ // The ship sprite is drawn using the same offset calculation but passed directly
+ // to FUN_004236e0. The rendering function treats these as screen coordinates.
+ debug("Rebel2 Ship Check: handler=%d shipSprite=%p flyShipSprite=%p shipLevelMode=%d numSprites=%d/%d",
+ _rebelHandler, (void*)_shipSprite, (void*)_flyShipSprite, _shipLevelMode,
+ _shipSprite ? _shipSprite->getNumChars() : 0,
+ _flyShipSprite ? _flyShipSprite->getNumChars() : 0);
+
+ // Handler 7 Ship Rendering (Space Flight - FLY sprites)
+ // Handler 7 uses _flyShipSprite (FLY001) with 35 direction frames (5x7 grid)
+ // Different from Handler 8's POV sprites
+ if (_rebelHandler == 7 && _flyShipSprite && _shipLevelMode != 5) {
+ // Handler 7 position calculation from FUN_0040d836 lines 173-175
+ // Draw position: (transformedX - 0xd4, transformedY - 0x82)
+ // where transformedX/Y come from FUN_0041c720 transformation
+ // For simplicity, we calculate based on ship direction
+
+ // Base position calculation (simplified from assembly)
+ // The ship is drawn at a fixed position offset by direction
+ int baseX = 160; // Screen center X
+ int baseY = 105; // Screen center Y (0x69)
+
+ // Add small offset based on ship position for "flight feel"
+ int16 posOffsetX = (_flyShipScreenX - 160) / 10;
+ int16 posOffsetY = (_flyShipScreenY - 100) / 10;
+
+ int shipScreenX = baseX + posOffsetX;
+ int shipScreenY = baseY + posOffsetY;
+
+ int numSprites = _flyShipSprite->getNumChars();
+ int spriteIndex = _shipDirectionIndex;
+
+ // Validate sprite index
+ if (spriteIndex < 0) spriteIndex = 0;
+ if (spriteIndex >= numSprites) spriteIndex = numSprites - 1;
+
+ // Get sprite dimensions and center it
+ int spriteW = _flyShipSprite->getCharWidth(spriteIndex);
+ int spriteH = _flyShipSprite->getCharHeight(spriteIndex);
+ int drawX = shipScreenX - spriteW / 2 + _viewX;
+ int drawY = shipScreenY - spriteH / 2 + _viewY;
+
+ // Draw the ship sprite (DAT_0047fee8 / FLY001)
+ renderNutSprite(renderBitmap, pitch, width, height, drawX, drawY, _flyShipSprite, spriteIndex);
+
+ // Draw laser overlay if firing and laser sprite loaded (FLY002)
+ if (_shipFiring && _flyLaserSprite && _flyLaserSprite->getNumChars() > 0) {
+ // Laser sprite uses same direction index for frame animation
+ int laserIndex = spriteIndex % _flyLaserSprite->getNumChars();
+ renderNutSprite(renderBitmap, pitch, width, height, drawX, drawY, _flyLaserSprite, laserIndex);
+ }
+
+ // Draw targeting overlay if loaded (FLY003) - goes at crosshair position
+ if (_flyTargetSprite && _flyTargetSprite->getNumChars() > 0) {
+ // Draw targeting reticle at ship position
+ int targetW = _flyTargetSprite->getCharWidth(0);
+ int targetH = _flyTargetSprite->getCharHeight(0);
+ int targetX = shipScreenX - targetW / 2 + _viewX;
+ int targetY = shipScreenY - targetH / 2 + _viewY;
+ renderNutSprite(renderBitmap, pitch, width, height, targetX, targetY, _flyTargetSprite, 0);
+ }
+
+ debug("Rebel2 Handler7: Ship drawn at (%d,%d) screenPos=(%d,%d) sprite=%d/%d dir=(%d,%d) idx=%d",
+ drawX, drawY, shipScreenX, shipScreenY, spriteIndex, numSprites,
+ _shipDirectionH, _shipDirectionV, _shipDirectionIndex);
+ }
+
+ // Handler 8 Ship Rendering (Third-Person Vehicle - POV sprites)
+ // Handler 8 uses _shipSprite (POV001) with position-based offset
+ else if (_rebelHandler == 8 && _shipSprite && _shipLevelMode != 5) {
+ // Calculate display offset from raw ship position (FUN_00401ccf lines 88-89)
+ // The shift operations create a dampened movement effect
+ int16 displayOffsetX = (_shipPosX - 0xa0) >> 3; // (shipPosX - 160) >> 3
+ int16 displayOffsetY = (_shipPosY - 0x28) >> 2; // (shipPosY - 40) >> 2
+
+ // Base screen position from crosshair calculation (FUN_00401ccf lines 128-129)
+ // Low-res mode: base X = 0xa0 (160), base Y = 0x69 (105)
+ int shipScreenX = 0xa0 + displayOffsetX; // 160 + offset
+ int shipScreenY = 0x69 + displayOffsetY; // 105 + offset
+
+ int numSprites = _shipSprite->getNumChars();
+ int spriteIndex = 0;
+
+ // Select sprite based on direction when multiple sprites are available
+ // The ship sprite sheet is organized as a grid of direction sprites:
+ // - 35 sprites (5x7): Full direction grid
+ // - 25 sprites (5x5): Reduced grid
+ // - 5 sprites: Horizontal direction only
+ // - 2 sprites: Normal (0) and firing (1)
+ // - 1 sprite: Static ship
+ if (numSprites >= 35) {
+ spriteIndex = _shipDirectionH * 7 + _shipDirectionV;
+ if (spriteIndex >= numSprites) spriteIndex = numSprites - 1;
+ } else if (numSprites >= 25) {
+ int vDir5 = (_shipDirectionV * 5) / 7;
+ spriteIndex = _shipDirectionH * 5 + vDir5;
+ if (spriteIndex >= numSprites) spriteIndex = numSprites - 1;
+ } else if (numSprites >= 5) {
+ spriteIndex = _shipDirectionH;
+ if (spriteIndex >= numSprites) spriteIndex = numSprites - 1;
+ } else if (numSprites == 2) {
+ spriteIndex = _shipFiring ? 1 : 0;
+ }
+
+ // Get sprite dimensions and center it at the calculated position
+ int spriteW = _shipSprite->getCharWidth(spriteIndex);
+ int spriteH = _shipSprite->getCharHeight(spriteIndex);
+ int drawX = shipScreenX - spriteW / 2 + _viewX;
+ int drawY = shipScreenY - spriteH / 2 + _viewY;
+
+ // Draw the primary ship sprite (DAT_0047e010)
+ renderNutSprite(renderBitmap, pitch, width, height, drawX, drawY, _shipSprite, spriteIndex);
+
+ // Draw secondary ship sprite if available (DAT_0047e028)
+ if (_shipSprite2 && _shipSprite2->getNumChars() > spriteIndex) {
+ renderNutSprite(renderBitmap, pitch, width, height, drawX, drawY, _shipSprite2, spriteIndex);
+ }
+
+ debug("Rebel2 Handler8: Ship drawn at screen(%d,%d) raw(%d,%d) offset(%d,%d) sprite=%d/%d dir=(%d,%d)",
+ drawX, drawY, _shipPosX, _shipPosY, displayOffsetX, displayOffsetY,
+ spriteIndex, numSprites, _shipDirectionH, _shipDirectionV);
+ }
+
+ // Fallback: Use embedded HUD frame as ship sprite
+ else if ((_rebelHandler == 7 || _rebelHandler == 8) && _shipLevelMode != 5) {
+ // Fallback: Use embedded HUD frame as ship sprite (Level 3 style)
+ // userId=11 contains the ship sprite strip
+ EmbeddedSanFrame &shipFrame = _rebelEmbeddedHud[11];
+ if (shipFrame.valid && shipFrame.pixels && shipFrame.width > 0 && shipFrame.height > 0) {
+ // Calculate display offset from raw ship position
+ int16 displayOffsetX = (_shipPosX - 0xa0) >> 3;
+ int16 displayOffsetY = (_shipPosY - 0x28) >> 2;
+ int shipScreenX = 0xa0 + displayOffsetX;
+ int shipScreenY = 0x69 + displayOffsetY;
+
+ // Check if this is a sprite strip (multiple directions in one image)
+ // 205 width / 5 directions = 41 pixels per direction
+ int spriteW = shipFrame.width;
+ int spriteH = shipFrame.height;
+ int srcX = 0;
+ int srcY = 0;
+ int numHorizontal = 1;
+ int numVertical = 1;
+
+ // Detect sprite strip layout - look for common patterns
+ if (spriteW >= 200 && spriteW % 5 == 0) {
+ // 5 horizontal directions (like 205 = 41 * 5)
+ numHorizontal = 5;
+ spriteW = shipFrame.width / 5;
+ }
+ if (spriteH >= 350 && spriteH % 7 == 0) {
+ // 7 vertical directions
+ numVertical = 7;
+ spriteH = shipFrame.height / 7;
+ }
+
+ // Select sprite from strip based on direction
+ int hDir = _shipDirectionH;
+ int vDir = _shipDirectionV;
+ if (hDir >= numHorizontal) hDir = numHorizontal - 1;
+ if (vDir >= numVertical) vDir = numVertical - 1;
+ srcX = hDir * spriteW;
+ srcY = vDir * spriteH;
+
+ // Draw position (centered)
+ int drawX = shipScreenX - spriteW / 2 + _viewX;
+ int drawY = shipScreenY - spriteH / 2 + _viewY;
+
+ // Blit from embedded HUD to render buffer
+ for (int y = 0; y < spriteH && (drawY + y) < height; y++) {
+ if (drawY + y < 0) continue;
+ for (int x = 0; x < spriteW && (drawX + x) < width; x++) {
+ if (drawX + x < 0) continue;
+ int srcIdx = (srcY + y) * shipFrame.width + (srcX + x);
+ byte pixel = shipFrame.pixels[srcIdx];
+ // Skip transparent pixels (0 and 231)
+ if (pixel != 0 && pixel != 231) {
+ int dstIdx = (drawY + y) * pitch + (drawX + x);
+ renderBitmap[dstIdx] = pixel;
+ }
+ }
+ }
+
+ debug("Rebel2: Ship (embedded HUD) at screen(%d,%d) strip=(%d,%d) of (%dx%d) dir=(%d,%d)",
+ drawX, drawY, srcX, srcY, numHorizontal, numVertical, _shipDirectionH, _shipDirectionV);
+ }
+ }
Common::List<enemy>::iterator it;
for (it = _enemies.begin(); it != _enemies.end(); ++it) {
@@ -2414,7 +3306,7 @@ void InsaneRebel2::procPostRendering(byte *renderBitmap, int32 codecparam, int32
// Draw Crosshair/Reticle cursor
// Sprite indices based on handler type (from original game disassembly FUN_004089ab, FUN_0040d836, etc):
- // - Handler 8 (ground vehicle): Index 0x2E (46)
+ // - Handler 8 (third-person vehicle): Index 0x2E (46)
// - Handler 7 (space flight): Index 0x2F (47)
// - Handler 0x19 (mixed/turret view): Index 0x2F (47)
// - Handler 0x26 (full turret): Index varies by levelType and animation
@@ -3732,6 +4624,8 @@ bool InsaneRebel2::playLevelGameplay(int levelId) {
case 2:
// Level 2: Has cutscene first, then multiple parts
+ // Level 2 uses Handler 8 (third-person vehicle mode) - set before gameplay
+ _rebelHandler = 8;
// First play the cutscene
filename = Common::String::format("%s/%sCUT.SAN", dir.c_str(), prefix.c_str());
debug("Rebel2: Playing cutscene %s", filename.c_str());
@@ -3780,7 +4674,9 @@ bool InsaneRebel2::playLevelGameplay(int levelId) {
break;
case 3:
- // Level 3: Two gameplay phases
+ // Level 3: Two gameplay phases (space flight)
+ // Level 3 uses Handler 7 (space flight mode) - FUN_0040d836/FUN_0040c3cc
+ _rebelHandler = 7;
filename = Common::String::format("%s/%sPLAY1.SAN", dir.c_str(), prefix.c_str());
debug("Rebel2: Playing %s", filename.c_str());
splayer->play(filename.c_str(), 12);
diff --git a/engines/scumm/insane/insane_rebel.h b/engines/scumm/insane/insane_rebel.h
index bc3e01364ff..290b3ad9af5 100644
--- a/engines/scumm/insane/insane_rebel.h
+++ b/engines/scumm/insane/insane_rebel.h
@@ -251,7 +251,7 @@ public:
void iactRebel2Opcode2(Common::SeekableReadStream &b, int16 par2, int16 par3, int16 par4);
void iactRebel2Opcode3(Common::SeekableReadStream &b, int16 par2, int16 par3, int16 par4);
void iactRebel2Opcode6(byte *renderBitmap, Common::SeekableReadStream &b, int16 par2, int16 par3, int16 par4);
- void iactRebel2Opcode8(byte *renderBitmap, Common::SeekableReadStream &b, int16 par2, int16 par3, int16 par4);
+ void iactRebel2Opcode8(byte *renderBitmap, Common::SeekableReadStream &b, int32 chunkSize, int16 par2, int16 par3, int16 par4);
void iactRebel2Opcode9(byte *renderBitmap, Common::SeekableReadStream &b, int16 par2, int16 par3, int16 par4);
void procPostRendering(byte *renderBitmap, int32 codecparam, int32 setupsan12,
@@ -288,7 +288,7 @@ public:
// Current handler type for Rebel Assault 2 (determines crosshair sprite)
// Handler 0: Background only
// Handler 7: Space flight - uses crosshair sprite 0x2F (47)
- // Handler 8: Ground vehicle - uses crosshair sprite 0x2E (46)
+ // Handler 8: Third-person vehicle - uses crosshair sprite 0x2E (46)
// Handler 0x19: Mixed/turret view - uses crosshair sprite 0x2F (47)
// Handler 0x26: Full turret - crosshair varies by level type
int _rebelHandler;
@@ -315,7 +315,7 @@ public:
bool valid; // True if this slot has valid data
};
- EmbeddedSanFrame _rebelEmbeddedHud[5]; // Index 0 unused, 1-4 for userId slots
+ EmbeddedSanFrame _rebelEmbeddedHud[16]; // HUD overlay slots (userId 0-15)
// Check if a partial frame update should be skipped (overlaps with destroyed enemy)
bool shouldSkipFrameUpdate(int left, int top, int width, int height) override;
@@ -381,6 +381,75 @@ public:
Shot _shots[2];
void spawnShot(int x, int y);
+ // ======================= Handler 8 Ship System =======================
+ // For third-person vehicle missions (Levels 2, 3), the player controls a ship
+ // that can turn in different directions. The ship sprite comes from
+ // NUT files loaded via IACT opcode 8.
+ //
+ // Based on FUN_00401234 and FUN_00401ccf disassembly:
+ // - DAT_0047e010: Primary ship sprite (POV001, subcase 1)
+ // - DAT_0047e028: Secondary ship sprite (POV004, subcase 3)
+ // - DAT_0047e020: Additional overlay (POV002, subcase 6)
+ // - DAT_0047e018: Additional overlay (POV003, subcase 7)
+ // - DAT_0043e006: Ship X position (raw, needs conversion for display)
+ // - DAT_0043e008: Ship Y position (raw, needs conversion for display)
+ // - DAT_0043e000: Level mode from opcode 6 par3
+
+ NutRenderer *_shipSprite; // DAT_0047e010 - Primary ship NUT
+ NutRenderer *_shipSprite2; // DAT_0047e028 - Secondary ship NUT
+ NutRenderer *_shipOverlay1; // DAT_0047e020 - Additional overlay
+ NutRenderer *_shipOverlay2; // DAT_0047e018 - Additional overlay
+
+ // Ship position tracking (matches DAT_0043e006/008)
+ // These are "raw" positions that get converted for display
+ int16 _shipPosX; // DAT_0043e006
+ int16 _shipPosY; // DAT_0043e008
+
+ // Ship target positions (where ship is trying to move to)
+ // Set from mouse/joystick input in opcode 6 processing
+ int16 _shipTargetX; // DAT_0043e002 - Target X
+ int16 _shipTargetY; // DAT_0043e004 - Target Y
+
+ // Level mode for handler 8 (different from _rebelLevelType)
+ // Set by opcode 6 par3, affects ship rendering behavior
+ int16 _shipLevelMode; // DAT_0043e000
+
+ // Ship firing state (from mouse button)
+ bool _shipFiring;
+
+ // Ship direction index for sprite selection (Handler 7)
+ // Calculated from ship position: horizontal * 7 + vertical
+ // horizontal: 0-4 (left to right), vertical: 0-6 (up to down)
+ // Used to select which embedded HUD userId to render
+ int16 _shipDirectionIndex;
+ int16 _shipDirectionH; // Horizontal direction (0-4, center=2)
+ int16 _shipDirectionV; // Vertical direction (0-6, center=3)
+
+ // Helper to load a NUT file from IACT chunk data
+ NutRenderer *loadNutFromIact(Common::SeekableReadStream &b, int dataSize);
+
+ // ======================= Handler 7 FLY Ship System =======================
+ // For space flight missions (Level 3, etc.), Handler 7 uses a 35-frame
+ // direction-based ship sprite system. The ship visually banks and turns
+ // based on player position using a 5x7 grid of sprites.
+ //
+ // Based on FUN_0040c3cc and FUN_0040d836 disassembly:
+ // - DAT_0047fee8: Ship direction sprites (FLY001, par3=1, 35 frames)
+ // - DAT_0047fef0: Laser fire sprites (FLY002, par3=3)
+ // - DAT_0047fef8: Targeting overlay (FLY003, par3=2)
+ // - DAT_0047ff00: High-res alternative (FLY004, par3=11)
+ // - DAT_0044370c: Ship Y screen position
+ // - DAT_0044370e: Ship X screen position
+
+ NutRenderer *_flyShipSprite; // DAT_0047fee8 - FLY001 (35 direction frames)
+ NutRenderer *_flyLaserSprite; // DAT_0047fef0 - FLY002
+ NutRenderer *_flyTargetSprite; // DAT_0047fef8 - FLY003
+ NutRenderer *_flyHiResSprite; // DAT_0047ff00 - FLY004
+
+ // Handler 7 screen position (different from Handler 8's raw positions)
+ int16 _flyShipScreenX; // DAT_0044370e - Ship X screen position
+ int16 _flyShipScreenY; // DAT_0044370c - Ship Y screen position
+
/* Difficulty Level (0, 1, 2 = Easy, Med, Hard) */
int _difficulty;
void drawCornerBrackets(byte *dst, int pitch, int width, int height, int x, int y, int w, int h, byte color);
diff --git a/engines/scumm/nut_renderer.cpp b/engines/scumm/nut_renderer.cpp
index 2213db9947d..1b040f6294b 100644
--- a/engines/scumm/nut_renderer.cpp
+++ b/engines/scumm/nut_renderer.cpp
@@ -50,6 +50,29 @@ NutRenderer::NutRenderer(ScummEngine *vm, const char *filename) :
loadFont(filename);
}
+NutRenderer::NutRenderer(ScummEngine *vm, const byte *data, int32 dataSize) :
+ _vm(vm),
+ _numChars(0),
+ _fontHeight(0),
+ _decodedData(0),
+ _2byteColorTable(0),
+ _2byteShadowXOffsetTable(0),
+ _2byteShadowYOffsetTable(0),
+ _2byteMainColor(0),
+ _spacing(vm->_useCJKMode && vm->_language != Common::JA_JPN ? 1 : 0),
+ _2byteSteps(vm->_game.version == 8 ? 4 : 2),
+ _direction(vm->_language == Common::HE_ISR ? -1 : 1) {
+ static const int8 cjkShadowOffsetsX[4] = { -1, 0, 1, 0 };
+ static const int8 cjkShadowOffsetsY[4] = { 0, 1, 0, 0 };
+ _2byteShadowXOffsetTable = &cjkShadowOffsetsX[ARRAYSIZE(cjkShadowOffsetsX) - _2byteSteps];
+ _2byteShadowYOffsetTable = &cjkShadowOffsetsY[ARRAYSIZE(cjkShadowOffsetsY) - _2byteSteps];
+ _2byteColorTable = new uint8[_2byteSteps];
+ memset(_2byteColorTable, 0, _2byteSteps);
+ _2byteMainColor = &_2byteColorTable[_2byteSteps - 1];
+ memset(_chars, 0, sizeof(_chars));
+ loadFontFromData(data, dataSize);
+}
+
NutRenderer::~NutRenderer() {
delete[] _decodedData;
delete[] _2byteColorTable;
@@ -187,6 +210,110 @@ void NutRenderer::loadFont(const char *filename) {
delete[] dataSrc;
}
+void NutRenderer::loadFontFromData(const byte *data, int32 dataSize) {
+ if (!data || dataSize < 8) {
+ warning("NutRenderer::loadFontFromData: data too small (%d bytes)", dataSize);
+ return;
+ }
+
+ uint32 tag = READ_BE_UINT32(data);
+ if (tag != MKTAG('A','N','I','M')) {
+ warning("NutRenderer::loadFontFromData: no ANIM chunk (got %08x)", tag);
+ return;
+ }
+
+ uint32 length = READ_BE_UINT32(data + 4);
+ if (length > (uint32)(dataSize - 8)) {
+ warning("NutRenderer::loadFontFromData: ANIM size (%d) exceeds data size (%d)", length, dataSize);
+ length = dataSize - 8;
+ }
+
+ const byte *dataSrc = data + 8;
+
+ if (READ_BE_UINT32(dataSrc) != MKTAG('A','H','D','R')) {
+ warning("NutRenderer::loadFontFromData: no AHDR chunk in font data");
+ return;
+ }
+
+ // Parse the font data (same logic as loadFont)
+ _numChars = READ_LE_UINT16(dataSrc + 10);
+ if (_numChars > ARRAYSIZE(_chars)) {
+ warning("NutRenderer::loadFontFromData: numChars (%d) exceeds max", _numChars);
+ _numChars = ARRAYSIZE(_chars);
+ }
+
+ delete[] _decodedData;
+ _decodedData = nullptr;
+ memset(_chars, 0, sizeof(_chars));
+
+ uint32 offset = 0;
+ uint32 decodedLength = 0;
+ int l;
+
+ for (l = 0; l < _numChars; l++) {
+ if (offset + 8 > length)
+ break;
+ offset += READ_BE_UINT32(dataSrc + offset + 4) + 16;
+ if (offset + 18 > length)
+ break;
+ int width = READ_LE_UINT16(dataSrc + offset + 14);
+ _fontHeight = READ_LE_UINT16(dataSrc + offset + 16);
+ decodedLength += width * _fontHeight;
+ }
+
+ debug(1, "NutRenderer::loadFontFromData() - numChars=%d decodedLength=%d", _numChars, decodedLength);
+
+ _decodedData = new byte[decodedLength];
+ byte *decodedPtr = _decodedData;
+
+ offset = 0;
+ for (l = 0; l < _numChars; l++) {
+ if (offset + 8 > length)
+ break;
+ offset += READ_BE_UINT32(dataSrc + offset + 4) + 8;
+ if (offset + 8 > length)
+ break;
+ if (READ_BE_UINT32(dataSrc + offset) != MKTAG('F','R','M','E')) {
+ warning("NutRenderer::loadFontFromData: no FRME chunk %d (offset %x)", l, offset);
+ break;
+ }
+ offset += 8;
+ if (offset + 22 > length)
+ break;
+ if (READ_BE_UINT32(dataSrc + offset) != MKTAG('F','O','B','J')) {
+ warning("NutRenderer::loadFontFromData: no FOBJ chunk in FRME chunk %d (offset %x)", l, offset);
+ break;
+ }
+ int codec = READ_LE_UINT16(dataSrc + offset + 8);
+ _chars[l].width = READ_LE_UINT16(dataSrc + offset + 14);
+ _chars[l].height = READ_LE_UINT16(dataSrc + offset + 16);
+ _chars[l].src = decodedPtr;
+
+ decodedPtr += (_chars[l].width * _chars[l].height);
+
+ if (codec == 44) {
+ memset(_chars[l].src, kSmush44TransparentColor, _chars[l].width * _chars[l].height);
+ _chars[l].transparency = kSmush44TransparentColor;
+ } else {
+ memset(_chars[l].src, kDefaultTransparentColor, _chars[l].width * _chars[l].height);
+ _chars[l].transparency = kDefaultTransparentColor;
+ }
+
+ const uint8 *fobjptr = dataSrc + offset + 22;
+ switch (codec) {
+ case 1:
+ codec1(_chars[l].src, fobjptr, _chars[l].width, _chars[l].height, _chars[l].width);
+ break;
+ case 21:
+ case 44:
+ codec21(_chars[l].src, fobjptr, _chars[l].width, _chars[l].height, _chars[l].width);
+ break;
+ default:
+ warning("NutRenderer::loadFontFromData: unknown codec: %d", codec);
+ }
+ }
+}
+
int NutRenderer::getCharWidth(byte c) const {
if (c >= 0x80 && _vm->_useCJKMode)
return _vm->_2byteWidth + _spacing;
diff --git a/engines/scumm/nut_renderer.h b/engines/scumm/nut_renderer.h
index d88d0b1f572..92abdfb246f 100644
--- a/engines/scumm/nut_renderer.h
+++ b/engines/scumm/nut_renderer.h
@@ -62,9 +62,11 @@ protected:
void codec21(byte *dst, const byte *src, int width, int height, int pitch);
void loadFont(const char *filename);
+ void loadFontFromData(const byte *data, int32 dataSize);
public:
NutRenderer(ScummEngine *vm, const char *filename);
+ NutRenderer(ScummEngine *vm, const byte *data, int32 dataSize);
virtual ~NutRenderer();
int getNumChars() const { return _numChars; }
@@ -76,7 +78,7 @@ public:
int getCharHeight(byte c) const;
const byte *getCharData(byte c);
byte getCharTransparency(byte c) const { return _chars[c].transparency; }
- byte getBpp() const { return _bpp; }
+ byte getBpp() const { return 8; }
int getFontHeight() const { return _fontHeight; }
};
diff --git a/engines/scumm/smush/smush_player.cpp b/engines/scumm/smush/smush_player.cpp
index df67aaff161..f88322f6846 100644
--- a/engines/scumm/smush/smush_player.cpp
+++ b/engines/scumm/smush/smush_player.cpp
@@ -405,15 +405,17 @@ void SmushPlayer::handleIACT(int32 subSize, Common::SeekableReadStream &b) {
code, flags, unknown, userId, subSize);
// Route to procIACT for:
- // 1. Non-audio IACT (code != 8 or flags != 46)
- // 2. RA2 menu/graphics IACT (userId >= 1000 indicates graphics data, not audio)
+ // 1. Non-audio IACT (code != 8 or flags != 46) - Full Throttle uses code=8, flags=46 for audio
+ // 2. ALL Rebel Assault 2 IACTs - RA2 uses a different IACT format where code=opcode, flags=par2
+ // RA2 audio is handled through PSAD chunks, not IACT, so all RA2 IACTs go to procIACT
bool isAudioIACT = (code == 8) && (flags == 46);
- bool isRA2GraphicsIACT = (_vm->_game.id == GID_REBEL2) && (userId >= 1000);
+ bool isRA2 = (_vm->_game.id == GID_REBEL2);
- if (!isAudioIACT || isRA2GraphicsIACT) {
- debug("SmushPlayer::handleIACT: Routing to procIACT (isAudioIACT=%d, isRA2GraphicsIACT=%d)",
- isAudioIACT, isRA2GraphicsIACT);
- _vm->_insane->procIACT(_dst, 0, 0, 0, b, 0, 0, code, flags, unknown, userId);
+ if (!isAudioIACT || isRA2) {
+ debug("SmushPlayer::handleIACT: Routing to procIACT (isAudioIACT=%d, isRA2=%d)",
+ isAudioIACT, isRA2);
+ // Pass subSize - 8 as the remaining payload size (after 8-byte header)
+ _vm->_insane->procIACT(_dst, 0, 0, 0, b, subSize - 8, 0, code, flags, unknown, userId);
return;
}
Commit: db7b16c04410cd6346970ba9ff8bee0d4c137d97
https://github.com/scummvm/scummvm/commit/db7b16c04410cd6346970ba9ff8bee0d4c137d97
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:08:54+02:00
Commit Message:
SCUMM: RA2: Fix level 1 loading regression
Changed paths:
engines/scumm/insane/insane_rebel.cpp
diff --git a/engines/scumm/insane/insane_rebel.cpp b/engines/scumm/insane/insane_rebel.cpp
index aaabb44372f..57ab02fc9c1 100644
--- a/engines/scumm/insane/insane_rebel.cpp
+++ b/engines/scumm/insane/insane_rebel.cpp
@@ -1597,16 +1597,40 @@ void InsaneRebel2::iactRebel2Opcode8(byte *renderBitmap, Common::SeekableReadStr
if (!loadedAsNut) {
// Load as embedded SAN (HUD overlays)
- // The userId comes from par4 (not par3!)
+ // The userId parameter varies by handler type:
+ //
+ // Handler 0x26 (turret): Uses par3 for HUD slot (GRD files)
+ // par3=1: GRD001 (Primary HUD overlay)
+ // par3=2: GRD002 (Secondary HUD graphics)
+ // par3=4: GRD010 (Ship cockpit frame)
+ // etc.
+ //
+ // Handler 7 (space flight): Uses par4 for userId (FLY files handled above)
+ // Other handlers: Use par4 for userId
+ //
// par4 values seen in Level 3:
// 1000 = audio track
// 1-11 = HUD overlay slots
- // The embedded HUD system uses userId to track different overlay elements
- int userId = par4;
+ //
+ // Note: Handler may not be set yet if opcode 8 arrives before opcode 6
+ // Use heuristics: if par3 is in valid GRD range (1-13) and par4 is >= 1000
+ // or invalid, prefer par3
+ int userId;
+ bool usePar3 = (_rebelHandler == 0x26 || _rebelHandler == 0x19);
+
+ // Heuristic: if par3 is in typical GRD slot range (1-13) and par4 is
+ // out of range (0 or >= 1000), use par3 as it's likely a turret/GRD case
+ if (!usePar3 && par3 >= 1 && par3 <= 13 && (par4 <= 0 || par4 >= 1000)) {
+ usePar3 = true;
+ debug("Rebel2 Opcode 8: Using par3 heuristic (par3=%d, par4=%d)", par3, par4);
+ }
+
+ userId = usePar3 ? par3 : par4;
// Audio tracks (userId >= 1000) are handled separately, skip them
- if (userId < 1000) {
- debug("Rebel2 Opcode 8: Loading embedded SAN as HUD userId=%d", userId);
+ if (userId > 0 && userId < 1000) {
+ debug("Rebel2 Opcode 8: Loading embedded SAN as HUD userId=%d (handler=%d, par3=%d, par4=%d)",
+ userId, _rebelHandler, par3, par4);
loadEmbeddedSan(userId, animData, toCopy, renderBitmap);
}
}
@@ -2807,6 +2831,24 @@ void InsaneRebel2::procPostRendering(byte *renderBitmap, int32 codecparam, int32
destX = frame.renderX;
destY = frame.renderY;
+ // For Handler 0x26 (turret) and 0x19 (mixed): Cockpit overlays positioned at bottom
+ // The cockpit pieces (slots 1-2) should be rendered just above the status bar (Y=180)
+ // The FOBJ top value might be 0, so we override it for cockpit pieces
+ if ((_rebelHandler == 0x26 || _rebelHandler == 0x19) && (hudSlot == 1 || hudSlot == 2)) {
+ // Cockpit overlay position: just above status bar
+ // Status bar is at Y=180, cockpit sits right above it
+ // For slot 1 (left piece): X = 0
+ // For slot 2 (right piece): X = width of slot 1
+ if (hudSlot == 1) {
+ destX = 0;
+ } else if (hudSlot == 2 && _rebelEmbeddedHud[1].valid) {
+ // Position slot 2 right after slot 1
+ destX = _rebelEmbeddedHud[1].width;
+ }
+ // Y position: status bar (180) minus cockpit height
+ destY = 180 - frame.height;
+ }
+
// For Handler 7: Apply position offset based on ship position
// This makes the ship sprite follow the mouse/crosshair
if (_rebelHandler == 7 && destX > 100 && destY > 50) {
@@ -4617,6 +4659,8 @@ bool InsaneRebel2::playLevelGameplay(int levelId) {
switch (levelId) {
case 1:
// Level 1: Single gameplay file (01P01.SAN)
+ // Level 1 uses Handler 0x26 (turret mode) - set before gameplay
+ _rebelHandler = 0x26;
filename = Common::String::format("%s/%sP01.SAN", dir.c_str(), prefix.c_str());
debug("Rebel2: Playing %s", filename.c_str());
splayer->play(filename.c_str(), 12);
@@ -4699,6 +4743,7 @@ bool InsaneRebel2::playLevelGameplay(int levelId) {
break;
case 4:
+ _rebelHandler = 0x26;
// Level 4: Has cutscene, then single gameplay
filename = Common::String::format("%s/%sCUT.SAN", dir.c_str(), prefix.c_str());
debug("Rebel2: Playing cutscene %s", filename.c_str());
Commit: ac86b2729c880933de819320ffa571be05252ceb
https://github.com/scummvm/scummvm/commit/ac86b2729c880933de819320ffa571be05252ceb
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:08:54+02:00
Commit Message:
SCUMM: RA2: Implement collision zones
Changed paths:
engines/scumm/insane/insane_rebel.cpp
engines/scumm/insane/insane_rebel.h
diff --git a/engines/scumm/insane/insane_rebel.cpp b/engines/scumm/insane/insane_rebel.cpp
index 57ab02fc9c1..453f44f1744 100644
--- a/engines/scumm/insane/insane_rebel.cpp
+++ b/engines/scumm/insane/insane_rebel.cpp
@@ -227,6 +227,19 @@ InsaneRebel2::InsaneRebel2(ScummEngine_v7 *scumm) {
_explosions[i].counter = 0;
}
+ // Initialize collision zone system (for Level 3 pilot ship obstacle avoidance)
+ _primaryZoneCount = 0;
+ _secondaryZoneCount = 0;
+ for (i = 0; i < kMaxCollisionZones; i++) {
+ _primaryZones[i].active = false;
+ _secondaryZones[i].active = false;
+ }
+ _corridorLeftX = 0;
+ _corridorTopY = 0;
+ _corridorRightX = 320;
+ _corridorBottomY = 200;
+ _hitCooldown = 0;
+
for (i = 0; i < 2; i++) {
_shots[i].active = false;
_shots[i].counter = 0;
@@ -251,6 +264,7 @@ InsaneRebel2::InsaneRebel2(ScummEngine_v7 *scumm) {
_shipTargetX = 0xa0;
_shipTargetY = 0x28;
_shipLevelMode = 0;
+ _flyControlMode = 0; // DAT_004437c0 - Start in flight-only mode (no shooting)
_shipFiring = false;
_shipDirectionH = 2; // Start centered horizontally (0-4 range)
_shipDirectionV = 3; // Start centered vertically (0-6 range)
@@ -775,10 +789,17 @@ void InsaneRebel2::iactRebel2Scene1(byte *renderBitmap, int32 codecparam, int32
iactRebel2Opcode3(b, par2, par3, par4);
}
else if (par1 == 5) {
- // Opcode 5: Special handling based on par2 value
- // Disassembly shows sub-opcodes 0xD (13) and 0xE (14)
+ // Opcode 5: Collision Zone Registration (FUN_004033cf case 5)
+ // Sub-opcode 0x0D (13) = Primary collision zones (obstacles)
+ // Sub-opcode 0x0E (14) = Secondary collision zones (boundaries)
+ // par2 is the sub-opcode that determines which zone table to use
debug("Rebel2 IACT Opcode 5: par2=%d par3=%d par4=%d", par2, par3, par4);
+ if (par2 == 0x0D || par2 == 0x0E) {
+ // Register the collision zone from the remaining IACT data
+ registerCollisionZone(b, par2);
+ }
+
} else if (par1 == 7) {
// Opcode 7: Sprite/HUD control for Handler 7 (space flight levels like Level 3)
// par2 = control type (41 = sprite selection?)
@@ -1102,8 +1123,14 @@ void InsaneRebel2::iactRebel2Opcode6(byte *renderBitmap, Common::SeekableReadStr
// Handler 7 specific logic (space flight) - FUN_0040d836 / FUN_0040c3cc
// Used for Level 3 and similar space combat levels
if (_rebelHandler == 7) {
- // Set level mode (same as Handler 8)
- _shipLevelMode = par3;
+ // Set control mode (DAT_004437c0 = par3 in FUN_40C3CC case 4)
+ // This determines shooting capability:
+ // Mode 0: Flight/avoid mode - no shooting
+ // Mode 1: Alternate flight mode - no shooting
+ // Mode 2: Combat mode - shooting ENABLED
+ _flyControlMode = par3;
+ debug("Rebel2 Opcode 6 (Handler 7): Control mode set to %d (shooting %s)",
+ par3, (par3 == 2) ? "ENABLED" : "DISABLED");
// If par4 == 1, enable status bar
if (par4 == 1) {
@@ -2548,6 +2575,152 @@ void InsaneRebel2::drawCornerBrackets(byte *dst, int pitch, int width, int heigh
drawLine(dst, pitch, width, height, x2, y2 - armLen, x2, y2, color);
}
+// ============================================================
+// COLLISION ZONE SYSTEM (for Level 3 pilot ship obstacle avoidance)
+// ============================================================
+// Based on FUN_40E35E, FUN_40C3CC disassembly from info.md
+// Zones are quadrilaterals registered via IACT opcode 5
+
+void InsaneRebel2::registerCollisionZone(Common::SeekableReadStream &b, int16 subOpcode) {
+ // IACT Opcode 5 data layout (from info.md):
+ // +0x00: opcode (5) - already read by caller
+ // +0x02: sub-opcode (0x0D or 0x0E) - passed as parameter
+ // +0x04: par3 (flags)
+ // +0x06: zoneType
+ // +0x08: frameStart
+ // +0x0A: frameEnd
+ // +0x0C-0x1A: X1,Y1,X2,Y2,X3,Y3,X4,Y4 vertex coordinates
+ //
+ // The stream position is currently at offset +0x04 (after opcode and sub-opcode)
+
+ int16 par3 = b.readSint16LE(); // +0x04 (flags - unused for now)
+ (void)par3; // Suppress unused variable warning
+ int16 zoneType = b.readSint16LE(); // +0x06
+ int16 frameStart = b.readSint16LE(); // +0x08
+ int16 frameEnd = b.readSint16LE(); // +0x0A
+ int16 x1 = b.readSint16LE(); // +0x0C
+ int16 y1 = b.readSint16LE(); // +0x0E
+ int16 x2 = b.readSint16LE(); // +0x10
+ int16 y2 = b.readSint16LE(); // +0x12
+ int16 x3 = b.readSint16LE(); // +0x14
+ int16 y3 = b.readSint16LE(); // +0x16
+ int16 x4 = b.readSint16LE(); // +0x18
+ int16 y4 = b.readSint16LE(); // +0x1A
+
+ CollisionZone zone;
+ zone.x1 = x1;
+ zone.y1 = y1;
+ zone.x2 = x2;
+ zone.y2 = y2;
+ zone.x3 = x3;
+ zone.y3 = y3;
+ zone.x4 = x4;
+ zone.y4 = y4;
+ zone.frameStart = frameStart;
+ zone.frameEnd = frameEnd;
+ zone.zoneType = zoneType;
+ zone.subOpcode = subOpcode;
+ zone.active = true;
+
+ // Register zone into appropriate table based on sub-opcode
+ if (subOpcode == 0x0D && _primaryZoneCount < kMaxCollisionZones) {
+ // Primary collision zones (obstacles)
+ _primaryZones[_primaryZoneCount++] = zone;
+ debug("Rebel2: Registered PRIMARY collision zone %d: type=%d frames=[%d-%d] quad=(%d,%d)-(%d,%d)-(%d,%d)-(%d,%d)",
+ _primaryZoneCount - 1, zoneType, frameStart, frameEnd,
+ x1, y1, x2, y2, x3, y3, x4, y4);
+ } else if (subOpcode == 0x0E && _secondaryZoneCount < kMaxCollisionZones) {
+ // Secondary collision zones (boundaries)
+ _secondaryZones[_secondaryZoneCount++] = zone;
+ debug("Rebel2: Registered SECONDARY collision zone %d: type=%d frames=[%d-%d] quad=(%d,%d)-(%d,%d)-(%d,%d)-(%d,%d)",
+ _secondaryZoneCount - 1, zoneType, frameStart, frameEnd,
+ x1, y1, x2, y2, x3, y3, x4, y4);
+ } else {
+ debug("Rebel2: WARNING - Could not register zone (subOpcode=%d, primary=%d, secondary=%d)",
+ subOpcode, _primaryZoneCount, _secondaryZoneCount);
+ }
+}
+
+void InsaneRebel2::resetCollisionZones() {
+ // Reset zone counters at end of frame (FUN_403240 equivalent)
+ // This clears the zone tables so they can be rebuilt from the next frame's IACT chunks
+ _primaryZoneCount = 0;
+ _secondaryZoneCount = 0;
+}
+
+void InsaneRebel2::drawQuad(byte *dst, int pitch, int width, int height,
+ int x1, int y1, int x2, int y2, int x3, int y3, int x4, int y4, byte color) {
+ // Draw a quadrilateral by connecting its 4 vertices with lines
+ // Vertex order: top-left (1), top-right (2), bottom-right (3), bottom-left (4)
+ drawLine(dst, pitch, width, height, x1, y1, x2, y2, color); // Top edge
+ drawLine(dst, pitch, width, height, x2, y2, x3, y3, color); // Right edge
+ drawLine(dst, pitch, width, height, x3, y3, x4, y4, color); // Bottom edge
+ drawLine(dst, pitch, width, height, x4, y4, x1, y1, color); // Left edge
+}
+
+void InsaneRebel2::drawCollisionZones(byte *dst, int pitch, int width, int height, byte color) {
+ // Draw all active collision zones as wireframe quadrilaterals for debugging
+ // Uses different colors for primary vs secondary zones
+
+ const byte primaryColor = 44; // Bright red for primary (obstacle) zones
+ const byte secondaryColor = 47; // Yellow for secondary (boundary) zones
+
+ // Draw primary zones (sub-opcode 0x0D - obstacles)
+ for (int i = 0; i < _primaryZoneCount; i++) {
+ CollisionZone &zone = _primaryZones[i];
+ if (!zone.active) continue;
+
+ // Apply view offset to convert from video coords to screen coords
+ int x1 = zone.x1 + _viewX;
+ int y1 = zone.y1 + _viewY;
+ int x2 = zone.x2 + _viewX;
+ int y2 = zone.y2 + _viewY;
+ int x3 = zone.x3 + _viewX;
+ int y3 = zone.y3 + _viewY;
+ int x4 = zone.x4 + _viewX;
+ int y4 = zone.y4 + _viewY;
+
+ drawQuad(dst, pitch, width, height, x1, y1, x2, y2, x3, y3, x4, y4, primaryColor);
+ }
+
+ // Draw secondary zones (sub-opcode 0x0E - boundaries)
+ for (int i = 0; i < _secondaryZoneCount; i++) {
+ CollisionZone &zone = _secondaryZones[i];
+ if (!zone.active) continue;
+
+ // Apply view offset
+ int x1 = zone.x1 + _viewX;
+ int y1 = zone.y1 + _viewY;
+ int x2 = zone.x2 + _viewX;
+ int y2 = zone.y2 + _viewY;
+ int x3 = zone.x3 + _viewX;
+ int y3 = zone.y3 + _viewY;
+ int x4 = zone.x4 + _viewX;
+ int y4 = zone.y4 + _viewY;
+
+ drawQuad(dst, pitch, width, height, x1, y1, x2, y2, x3, y3, x4, y4, secondaryColor);
+ }
+
+ // Draw corridor boundaries as a rectangle (from IACT opcode 7)
+ if (_corridorLeftX != 0 || _corridorRightX != 320) {
+ const byte corridorColor = 45; // Cyan for corridor boundaries
+ // Draw vertical lines for left/right boundaries
+ drawLine(dst, pitch, width, height,
+ _corridorLeftX + _viewX, _corridorTopY + _viewY,
+ _corridorLeftX + _viewX, _corridorBottomY + _viewY, corridorColor);
+ drawLine(dst, pitch, width, height,
+ _corridorRightX + _viewX, _corridorTopY + _viewY,
+ _corridorRightX + _viewX, _corridorBottomY + _viewY, corridorColor);
+ // Draw horizontal lines for top/bottom boundaries
+ drawLine(dst, pitch, width, height,
+ _corridorLeftX + _viewX, _corridorTopY + _viewY,
+ _corridorRightX + _viewX, _corridorTopY + _viewY, corridorColor);
+ drawLine(dst, pitch, width, height,
+ _corridorLeftX + _viewX, _corridorBottomY + _viewY,
+ _corridorRightX + _viewX, _corridorBottomY + _viewY, corridorColor);
+ }
+}
+
void InsaneRebel2::renderNutSprite(byte *dst, int pitch, int width, int height, int x, int y, NutRenderer *nut, int spriteIdx) {
if (!nut || spriteIdx < 0 || spriteIdx >= nut->getNumChars()) return;
@@ -2622,8 +2795,6 @@ void InsaneRebel2::procPostRendering(byte *renderBitmap, int32 codecparam, int32
_player->setScrollOffset(_viewX, _viewY);
- processMouse();
-
// --- HUD Drawing Order (from FUN_004089ab assembly analysis) ---
// Based on FUN_004089ab:
// 1. Line 156: FUN_004288c0 fills status bar background at Y=0xb4 (180)
@@ -2710,33 +2881,42 @@ void InsaneRebel2::procPostRendering(byte *renderBitmap, int32 codecparam, int32
return;
}
- // During intro sequences (non-menu, non-gameplay):
- // - Hide the crosshair/cursor (handled by not drawing it)
- // - Skip all HUD/status bar rendering
- // - Skip mouse input processing
- // The mouse cursor is already hidden by SmushPlayer during video playback
+ // During intro/cinematic sequences:
+ // - Hide the mouse cursor (original: ShowCursor(0) at startup in FUN_00420c70)
+ // - Skip all HUD/status bar/crosshair rendering
+ // - Skip mouse input processing (no shooting during intros)
//
- // IMPORTANT: During gameplay (_gameState == kStateGameplay), we MUST render HUD
- // even if video flags have 0x20 set (which can happen for some level videos)
- if (introPlaying && _gameState != kStateGameplay) {
+ // Original behavior from FUN_00403240:
+ // - if (DAT_0047a814 == 0) { switch(DAT_0047ee84) { ... } }
+ // - DAT_0047ee84 (handler) is only set by IACT opcode 6 during gameplay videos
+ // - Cinematics/intros don't have opcode 6, so handler stays 0
+ // - We use _rebelHandler == 0 as the primary indicator for intro/cinematic mode
+ if (_rebelHandler == 0) {
+ // Hide mouse cursor during intro - no crosshair, no clicking
+ CursorMan.showMouse(false);
+
// Track state transition for debugging
if (!_introCursorPushed) {
_introCursorPushed = true;
- debug("Rebel2: Intro sequence detected (flags=0x%x, state=%d) - HUD disabled", _player->_curVideoFlags, _gameState);
+ debug("Rebel2: Intro/cinematic mode (handler=0, flags=0x%x, state=%d) - HUD disabled, mouse hidden",
+ _player->_curVideoFlags, _gameState);
}
// Skip all HUD rendering during intro - subtitles are rendered via opcode 9
return;
} else {
- // Gameplay mode - restore normal rendering
+ // Gameplay mode - handler was set by IACT opcode 6
if (_introCursorPushed) {
_introCursorPushed = false;
- debug("Rebel2: Gameplay started - HUD enabled (flags=0x%x, state=%d)", _player->_curVideoFlags, _gameState);
+ debug("Rebel2: Gameplay mode (handler=%d, flags=0x%x, state=%d) - HUD enabled",
+ _rebelHandler, _player->_curVideoFlags, _gameState);
}
}
- // From here on, we're in gameplay mode (not intro)
- // Render HUD if NOT in intro mode, OR if we ARE in gameplay state
- if (!introPlaying || _gameState == kStateGameplay) {
+ // From here on, we're in gameplay mode (_rebelHandler != 0)
+ // Process mouse input for shooting
+ // Original: FUN_00403240 only runs handlers when DAT_0047a814 == 0
+ processMouse();
+
// ============================================================
// STEP 0: Fill status bar background (FUN_004288c0 equivalent)
// ============================================================
@@ -3321,6 +3501,16 @@ void InsaneRebel2::procPostRendering(byte *renderBitmap, int32 codecparam, int32
}
}
+ // ============================================================
+ // COLLISION ZONE VISUALIZATION (for debugging Level 3 pilot mode)
+ // ============================================================
+ // Draw collision zones as wireframe quadrilaterals when in Handler 7 (space flight)
+ // or Handler 8 (ground vehicle) modes. This helps verify parsing is correct.
+ // The zones are drawn BEFORE the crosshair so they appear under the UI.
+ if (_rebelHandler == 7 || _rebelHandler == 8) {
+ drawCollisionZones(renderBitmap, pitch, width, height, 0);
+ }
+
// ============================================================
// TARGET LOCK DETECTION (DAT_00443676 equivalent)
// ============================================================
@@ -3413,8 +3603,6 @@ void InsaneRebel2::procPostRendering(byte *renderBitmap, int32 codecparam, int32
// This is called after all other rendering so it appears on top
renderScoreHUD(renderBitmap, pitch, width, height, 0);
- } // End of if (!introPlaying) block
-
// ============================================================
// FRAME END RESET: Clear active flags for all enemies (FUN_403240 equivalent)
// ============================================================
@@ -3430,6 +3618,10 @@ void InsaneRebel2::procPostRendering(byte *renderBitmap, int32 codecparam, int32
resetIt->active = false;
}
}
+
+ // Reset collision zone counters for next frame (DAT_0047ee74/DAT_0047ee78 = 0)
+ // The zones are rebuilt from IACT opcode 5 chunks each frame
+ resetCollisionZones();
}
// ========== Audio Handling for Rebel Assault 2 ==========
@@ -4617,6 +4809,21 @@ void InsaneRebel2::playMissionBriefing() {
splayer->play("OPEN/O_LEVEL.SAN", 12);
}
+void InsaneRebel2::playCinematic(const char *filename) {
+ // Play a cinematic/cutscene video with proper intro mode setup
+ // This helper ensures:
+ // 1. Handler is reset to 0 (no HUD, no shooting)
+ // 2. Video flags are set to 0x20 (cinematic mode)
+ //
+ // Original: DAT_0047ee84 is only set by IACT opcode 6 during gameplay videos
+ // Cinematics don't have opcode 6, so handler stays 0
+ _rebelHandler = 0;
+
+ SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
+ splayer->setCurVideoFlags(0x20); // Cinematic mode
+ splayer->play(filename, 12);
+}
+
void InsaneRebel2::playLevelBegin(int levelId) {
// Play the level beginning cinematic (LEVXX/XXBEG.SAN)
// Emulates FUN_004171c5 call in each level handler
@@ -4626,10 +4833,7 @@ void InsaneRebel2::playLevelBegin(int levelId) {
Common::String filename = Common::String::format("%s/%sBEG.SAN", dir.c_str(), prefix.c_str());
debug("Rebel2: Playing level %d beginning: %s", levelId, filename.c_str());
-
- SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
- splayer->setCurVideoFlags(0x20); // Cinematic mode
- splayer->play(filename.c_str(), 12);
+ playCinematic(filename.c_str());
}
bool InsaneRebel2::playLevelGameplay(int levelId) {
@@ -5111,8 +5315,7 @@ int InsaneRebel2::runLevel2() {
int bonusCount = 0; // Tracks special events (local_1c in assembly)
// Play cutscene (02CUT.SAN)
- splayer->setCurVideoFlags(0x20);
- splayer->play("LEV02/02CUT.SAN", 12);
+ playCinematic("LEV02/02CUT.SAN");
if (_vm->shouldQuit()) return kLevelQuit;
// Play level beginning cinematic (02BEG.SAN)
diff --git a/engines/scumm/insane/insane_rebel.h b/engines/scumm/insane/insane_rebel.h
index 290b3ad9af5..bd48a8b016d 100644
--- a/engines/scumm/insane/insane_rebel.h
+++ b/engines/scumm/insane/insane_rebel.h
@@ -196,6 +196,9 @@ public:
// Returns suffix like "A", "B", "C" for DIE_X.SAN
Common::String selectDeathVideoVariant(int levelId, int phase, int frame);
+ // Play cinematic video by filename
+ void playCinematic(const char *filename);
+
// Play death video with proper variant selection
void playLevelDeathVariant(int levelId, int phase, int frame);
@@ -340,6 +343,62 @@ public:
Explosion _explosions[5];
void spawnExplosion(int x, int y, int objectHalfWidth);
+ // ======================= Collision Zone System =======================
+ // For Level 3 "pilot" ship obstacle avoidance (FUN_40E35E, FUN_40C3CC)
+ // Collision zones are quadrilaterals defined by IACT Opcode 5
+ // The player's ship position is tested against these zones each frame
+ //
+ // Zone Data Layout from IACT chunk:
+ // +0x00: opcode (5)
+ // +0x02: sub-opcode (0x0D = primary, 0x0E = secondary)
+ // +0x04: par3 (flags)
+ // +0x06: zoneType (e.g., 5 for damage zones)
+ // +0x08: frameStart
+ // +0x0A: frameEnd
+ // +0x0C-0x1A: X1,Y1,X2,Y2,X3,Y3,X4,Y4 vertex coordinates
+
+ struct CollisionZone {
+ int16 x1, y1; // Top-left
+ int16 x2, y2; // Top-right
+ int16 x3, y3; // Bottom-right
+ int16 x4, y4; // Bottom-left
+ int16 frameStart;
+ int16 frameEnd;
+ int16 zoneType;
+ int16 subOpcode; // 0x0D = primary, 0x0E = secondary
+ bool active;
+ };
+
+ // Two zone tables matching retail DAT_0043fb00 (primary) and DAT_0043f9c8 (secondary)
+ static const int kMaxCollisionZones = 5;
+ CollisionZone _primaryZones[kMaxCollisionZones]; // Sub-opcode 0x0D zones
+ CollisionZone _secondaryZones[kMaxCollisionZones]; // Sub-opcode 0x0E zones
+ int _primaryZoneCount;
+ int _secondaryZoneCount;
+
+ // Corridor boundaries from IACT opcode 7 sub-opcodes 1 and 2
+ int16 _corridorLeftX; // DAT_00443b0a - Left X boundary
+ int16 _corridorTopY; // DAT_00443b0c - Top Y boundary
+ int16 _corridorRightX; // DAT_00443b0e - Right X boundary
+ int16 _corridorBottomY; // DAT_00443b10 - Bottom Y boundary
+
+ // Hit cooldown timer (DAT_0044374c) - prevents rapid damage stacking
+ int16 _hitCooldown;
+
+ // Draw collision zone quadrilaterals for visualization/debugging
+ // Called from procPostRendering when collision zones should be visible
+ void drawCollisionZones(byte *dst, int pitch, int width, int height, byte color);
+
+ // Draw a single quadrilateral (4 edges)
+ void drawQuad(byte *dst, int pitch, int width, int height,
+ int x1, int y1, int x2, int y2, int x3, int y3, int x4, int y4, byte color);
+
+ // Register a collision zone from IACT opcode 5 data
+ void registerCollisionZone(Common::SeekableReadStream &b, int16 subOpcode);
+
+ // Reset collision zone counters (called at end of frame)
+ void resetCollisionZones();
+
int16 _playerDamage; // Legacy damage counter (kept for compatibility/telemetry)
int16 _playerShield; // Shields: 0..255 where 255 = full
int16 _playerLives;
@@ -414,6 +473,16 @@ public:
// Set by opcode 6 par3, affects ship rendering behavior
int16 _shipLevelMode; // DAT_0043e000
+ // Control mode for Handler 7 (space flight) - DAT_004437c0
+ // Set by IACT opcode 6 par3 when handler is 7
+ // Determines shooting capability and collision zone type:
+ // Mode 0: Flight/avoid mode - no shooting, uses secondary zones (sub-opcode 0x0E)
+ // Mode 1: Alternate flight mode - no shooting, uses primary zones (sub-opcode 0x0D)
+ // Mode 2: Combat mode - shooting ENABLED, crosshair shown, uses secondary zones
+ // In Level 3's first sequence, par3=0 (no shooting - pure obstacle avoidance)
+ // In combat sequences, par3=2 (shooting enabled)
+ int16 _flyControlMode; // DAT_004437c0
+
// Ship firing state (from mouse button)
bool _shipFiring;
Commit: a4bbf9032e8b31d2acad272aaf05aa17a75c46da
https://github.com/scummvm/scummvm/commit/a4bbf9032e8b31d2acad272aaf05aa17a75c46da
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:08:54+02:00
Commit Message:
SCUMM: RA2: Load low-resolution cockpit HUD
Changed paths:
engines/scumm/insane/insane_rebel.cpp
engines/scumm/insane/insane_rebel.h
diff --git a/engines/scumm/insane/insane_rebel.cpp b/engines/scumm/insane/insane_rebel.cpp
index 453f44f1744..89be33ba7e8 100644
--- a/engines/scumm/insane/insane_rebel.cpp
+++ b/engines/scumm/insane/insane_rebel.cpp
@@ -100,8 +100,9 @@ InsaneRebel2::InsaneRebel2(ScummEngine_v7 *scumm) {
_smush_mouseoverNut = new NutRenderer(_vm, "SYSTM/MSTOVER.NUT");
_enemies.clear();
- _rebelHandler = 8; // Default to Handler 8 (third-person vehicle) for Level 1
+ _rebelHandler = 0; // Not set yet - will be set by IACT opcode 6
_rebelLevelType = 0; // Level type from Opcode 6 par3, determines HUD sprite variant
+ _rebelStatusBarSprite = 0; // 0 = disabled, 5 or 53 = enabled (set by IACT opcode 6 par4==1)
_introCursorPushed = false; // Cursor state tracking for intro sequences
_playerDamage = 0;
@@ -278,6 +279,10 @@ InsaneRebel2::InsaneRebel2(ScummEngine_v7 *scumm) {
_flyShipScreenX = 0xd4; // Start at center (212) - matches DAT_00443708 default
_flyShipScreenY = 0x82; // Start at center (130) - matches DAT_0044370a default
+ // Initialize Handler 0x26 turret HUD overlay system
+ _hudOverlayNut = nullptr; // DAT_0047fe78 - Primary HUD overlay (GRD files, animated)
+ _hudOverlay2Nut = nullptr; // DAT_0047fe80 - Secondary HUD overlay
+
// Initialize audio system for RA2 (since we don't use iMUSE)
_audioSampleRate = 11025; // RA2 audio is 11025 Hz, not 22050 Hz
for (i = 0; i < kRA2MaxAudioTracks; i++) {
@@ -343,6 +348,10 @@ InsaneRebel2::~InsaneRebel2() {
delete _flyTargetSprite;
delete _flyHiResSprite;
+ // Clean up Handler 0x26 turret HUD overlays
+ delete _hudOverlayNut;
+ delete _hudOverlay2Nut;
+
// Clean up embedded HUD overlays
for (int i = 0; i < 16; i++) {
free(_rebelEmbeddedHud[i].pixels);
@@ -1577,88 +1586,142 @@ void InsaneRebel2::iactRebel2Opcode8(byte *renderBitmap, Common::SeekableReadStr
b.seek(animStreamPos);
b.read(animData, toCopy);
- // Handler 8 (Third-Person Vehicle) - FUN_00401234 case 6:
- // par3 == 1: POV001 -> DAT_0047e010 (primary ship)
- // par3 == 3: POV004 -> DAT_0047e028 (secondary ship)
- // par3 == 6: POV002 -> DAT_0047e020 (overlay 1)
- // par3 == 7: POV003 -> DAT_0047e018 (overlay 2)
- bool isHandler8Par3 = (par3 == 1 || par3 == 3 || par3 == 6 || par3 == 7);
+ // Handler-dependent NUT loading for opcode 8:
+ //
+ // Handler 0x26/0x19 (turret) - FUN_00407fcb:
+ // par3 == 1: Low-res primary HUD (GRD001) -> DAT_0047fe78
+ // par3 == 2: High-res primary HUD (skip in low-res mode)
+ // par3 == 3: Low-res secondary HUD (GRD010) -> DAT_0047fe80
+ // par3 == 4: High-res secondary HUD (skip in low-res mode)
+ //
+ // Handler 8 (vehicle) - FUN_00401234:
+ // par3 == 1: POV001 - Primary ship sprite -> DAT_0047e010
+ // par3 == 3: POV004 - Secondary ship sprite -> DAT_0047e028
+ // par3 == 6: POV002 - Ship overlay 1 -> DAT_0047e020
+ // par3 == 7: POV003 - Ship overlay 2 -> DAT_0047e018
+ //
+ // ScummVM runs at 320x200 (low-res), so turret HUD uses par3 == 1/3 only.
bool loadedAsNut = false;
- if (_rebelHandler == 8 && isHandler8Par3) {
- // Try loading as NUT (ship sprites for Handler 8)
- NutRenderer *newNut = new NutRenderer(_vm, animData, toCopy);
- if (newNut && newNut->getNumChars() > 0) {
- debug("Rebel2 Opcode 8: Loaded ship NUT par3=%d with %d sprites (handler=%d)",
- par3, newNut->getNumChars(), _rebelHandler);
+ // Handler 0x26/0x19 (turret modes): Load HUD overlays
+ if (_rebelHandler == 0x26 || _rebelHandler == 0x19) {
+ if (par3 == 1) {
+ // Low-res primary HUD overlay
+ NutRenderer *newNut = new NutRenderer(_vm, animData, toCopy);
+ if (newNut && newNut->getNumChars() > 0) {
+ debug("Rebel2 Opcode 8: Loaded turret HUD NUT par3=%d with %d sprites (handler=0x%x, low-res)",
+ par3, newNut->getNumChars(), _rebelHandler);
+ delete _hudOverlayNut;
+ _hudOverlayNut = newNut;
+ loadedAsNut = true;
+ } else {
+ debug("Rebel2 Opcode 8: Turret HUD NUT load failed for par3=%d", par3);
+ delete newNut;
+ }
+ } else if (par3 == 3) {
+ // Low-res secondary HUD overlay
+ NutRenderer *newNut = new NutRenderer(_vm, animData, toCopy);
+ if (newNut && newNut->getNumChars() > 0) {
+ debug("Rebel2 Opcode 8: Loaded turret HUD2 NUT par3=%d with %d sprites (handler=0x%x, low-res)",
+ par3, newNut->getNumChars(), _rebelHandler);
+ delete _hudOverlay2Nut;
+ _hudOverlay2Nut = newNut;
+ loadedAsNut = true;
+ } else {
+ debug("Rebel2 Opcode 8: Turret HUD2 NUT load failed for par3=%d", par3);
+ delete newNut;
+ }
+ } else if (par3 == 2 || par3 == 4) {
+ // High-res versions - skip in low-res mode
+ debug("Rebel2 Opcode 8: Skipping high-res HUD par3=%d (running in low-res mode)", par3);
loadedAsNut = true;
+ }
+ }
- switch (par3) {
- case 1: // POV001 - Primary ship sprite
- delete _shipSprite;
- _shipSprite = newNut;
- debug("Rebel2 Opcode 8: _shipSprite set to %p", (void*)_shipSprite);
- break;
- case 3: // POV004 - Secondary ship sprite
- delete _shipSprite2;
- _shipSprite2 = newNut;
- break;
- case 6: // POV002 - Ship overlay 1
- delete _shipOverlay1;
- _shipOverlay1 = newNut;
- break;
- case 7: // POV003 - Ship overlay 2
- delete _shipOverlay2;
- _shipOverlay2 = newNut;
- break;
- default:
+ // Handler 8 (Third-Person Vehicle): Load ship sprites
+ if (!loadedAsNut && _rebelHandler == 8) {
+ bool isHandler8Par3 = (par3 == 1 || par3 == 3 || par3 == 6 || par3 == 7);
+ if (isHandler8Par3) {
+ NutRenderer *newNut = new NutRenderer(_vm, animData, toCopy);
+ if (newNut && newNut->getNumChars() > 0) {
+ debug("Rebel2 Opcode 8: Loaded ship NUT par3=%d with %d sprites (handler=%d)",
+ par3, newNut->getNumChars(), _rebelHandler);
+ loadedAsNut = true;
+
+ switch (par3) {
+ case 1: // POV001 - Primary ship sprite
+ delete _shipSprite;
+ _shipSprite = newNut;
+ break;
+ case 3: // POV004 - Secondary ship sprite
+ delete _shipSprite2;
+ _shipSprite2 = newNut;
+ break;
+ case 6: // POV002 - Ship overlay 1
+ delete _shipOverlay1;
+ _shipOverlay1 = newNut;
+ break;
+ case 7: // POV003 - Ship overlay 2
+ delete _shipOverlay2;
+ _shipOverlay2 = newNut;
+ break;
+ default:
+ delete newNut;
+ loadedAsNut = false;
+ break;
+ }
+ } else {
+ debug("Rebel2 Opcode 8: Ship NUT load failed for par3=%d", par3);
delete newNut;
- loadedAsNut = false;
- break;
}
- } else {
- debug("Rebel2 Opcode 8: NUT load failed for par3=%d, trying as embedded SAN", par3);
- delete newNut;
}
}
if (!loadedAsNut) {
- // Load as embedded SAN (HUD overlays)
- // The userId parameter varies by handler type:
- //
- // Handler 0x26 (turret): Uses par3 for HUD slot (GRD files)
- // par3=1: GRD001 (Primary HUD overlay)
- // par3=2: GRD002 (Secondary HUD graphics)
- // par3=4: GRD010 (Ship cockpit frame)
- // etc.
- //
- // Handler 7 (space flight): Uses par4 for userId (FLY files handled above)
- // Other handlers: Use par4 for userId
- //
- // par4 values seen in Level 3:
- // 1000 = audio track
- // 1-11 = HUD overlay slots
- //
- // Note: Handler may not be set yet if opcode 8 arrives before opcode 6
- // Use heuristics: if par3 is in valid GRD range (1-13) and par4 is >= 1000
- // or invalid, prefer par3
- int userId;
- bool usePar3 = (_rebelHandler == 0x26 || _rebelHandler == 0x19);
-
- // Heuristic: if par3 is in typical GRD slot range (1-13) and par4 is
- // out of range (0 or >= 1000), use par3 as it's likely a turret/GRD case
- if (!usePar3 && par3 >= 1 && par3 <= 13 && (par4 <= 0 || par4 >= 1000)) {
- usePar3 = true;
- debug("Rebel2 Opcode 8: Using par3 heuristic (par3=%d, par4=%d)", par3, par4);
- }
+ // Skip high-res turret HUD data (par3 == 2, 4) regardless of handler
+ // ScummVM runs at 320x200, so we only want low-res data (par3 == 1, 3)
+ debug("Rebel2 Opcode 8: Fallback path - handler=%d par3=%d par4=%d animSize=%d",
+ _rebelHandler, par3, par4, toCopy);
+ if (par3 == 2 || par3 == 4) {
+ debug("Rebel2 Opcode 8: SKIPPING high-res HUD par3=%d (low-res mode)", par3);
+ // Don't load anything - skip this data entirely
+ } else {
+ // Load as embedded SAN (HUD overlays)
+ // The userId parameter varies by handler type:
+ //
+ // Handler 0x26 (turret): Uses par3 for HUD slot (GRD files)
+ // par3=1: GRD001 (Primary HUD overlay)
+ // par3=3: GRD010 (Secondary HUD)
+ //
+ // Handler 7 (space flight): Uses par4 for userId (FLY files handled above)
+ // Other handlers: Use par4 for userId
+ //
+ // par4 values seen in Level 3:
+ // 1000 = audio track
+ // 1-11 = HUD overlay slots
+ //
+ // Note: Handler may not be set yet if opcode 8 arrives before opcode 6
+ // Use heuristics: if par3 is in valid GRD range (1-13) and par4 is >= 1000
+ // or invalid, prefer par3
+ int userId;
+ // Handler 0x19 uses par3; Handler 0x26 and others use par4
+ bool usePar3 = (_rebelHandler == 0x19);
+
+ // Heuristic: if par3 is in typical GRD slot range (1-13) and par4 is
+ // out of range (0 or >= 1000), use par3 as it's likely a turret/GRD case
+ if (!usePar3 && par3 >= 1 && par3 <= 13 && (par4 <= 0 || par4 >= 1000)) {
+ usePar3 = true;
+ debug("Rebel2 Opcode 8: Using par3 heuristic (par3=%d, par4=%d)", par3, par4);
+ }
- userId = usePar3 ? par3 : par4;
+ userId = usePar3 ? par3 : par4;
- // Audio tracks (userId >= 1000) are handled separately, skip them
- if (userId > 0 && userId < 1000) {
- debug("Rebel2 Opcode 8: Loading embedded SAN as HUD userId=%d (handler=%d, par3=%d, par4=%d)",
- userId, _rebelHandler, par3, par4);
- loadEmbeddedSan(userId, animData, toCopy, renderBitmap);
+ // Audio tracks (userId >= 1000) are handled separately, skip them
+ if (userId > 0 && userId < 1000) {
+ debug("Rebel2 Opcode 8: Loading embedded SAN as HUD userId=%d (handler=%d, par3=%d, par4=%d)",
+ userId, _rebelHandler, par3, par4);
+ loadEmbeddedSan(userId, animData, toCopy, renderBitmap);
+ }
}
}
free(animData);
@@ -2019,10 +2082,19 @@ void InsaneRebel2::loadEmbeddedSan(int userId, byte *animData, int32 size, byte
debug("Rebel2: Embedded HUD frame: userId=%d, %dx%d at (%d,%d), codec=%d",
userId, width, height, left, top, codec);
-
+
+ // Skip high-resolution frames - ScummVM runs at 320x200
+ // If frame dimensions exceed low-res screen size, it's high-res data
+ if (width > 400 || height > 250) {
+ debug("Rebel2: SKIPPING high-res embedded frame: userId=%d, %dx%d (exceeds 400x250)",
+ userId, width, height);
+ stream.seek(nextSubPos);
+ continue;
+ }
+
// Allocate storage for the decoded frame
EmbeddedSanFrame &frame = _rebelEmbeddedHud[userId];
-
+
if (width > 0 && height > 0 && width <= 800 && height <= 480) {
if (frame.width != width || frame.height != height || !frame.pixels) {
free(frame.pixels);
@@ -2080,10 +2152,8 @@ void InsaneRebel2::loadEmbeddedSan(int userId, byte *animData, int32 size, byte
debug("Rebel2: Decoded embedded HUD (codec 21/line update): %dx%d", width, height);
} else if (codec == 45) {
// Codec 45: RA2-specific codec with BOMP-style RLE
- // Header: 01 FE 00 00 01 00 (6 bytes)
- // Byte 0: sub-codec (01)
- // Byte 1: transparent color (FE = 254)
- // Bytes 2-5: unknown/padding
+ // May have a variable-length sub-header before RLE data
+ // Common patterns: "01 FE 00 00 01 00" (6 bytes) or others
debug("Rebel2: Codec 45 first 20 bytes: %02X %02X %02X %02X %02X %02X | %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X",
fobjData[0], fobjData[1], fobjData[2], fobjData[3],
fobjData[4], fobjData[5], fobjData[6], fobjData[7],
@@ -2091,12 +2161,56 @@ void InsaneRebel2::loadEmbeddedSan(int userId, byte *animData, int32 size, byte
fobjData[12], fobjData[13], fobjData[14], fobjData[15],
fobjData[16], fobjData[17], fobjData[18], fobjData[19]);
- // Parse 6-byte sub-header
+ // Probe multiple header offsets to find where valid RLE data starts
+ // RLE data has 2-byte line sizes that should be reasonable values
int headerSkip = 0;
+ bool foundValidOffset = false;
+
+ // Check common header patterns
if (dataSize > 6 && fobjData[0] == 0x01 && fobjData[1] == 0xFE) {
+ // Known 6-byte header: 01 FE XX XX XX XX
headerSkip = 6;
- debug("Rebel2: Codec 45 header: sub-codec=%d, transparent=%d",
- fobjData[0], fobjData[1]);
+ debug("Rebel2: Codec 45 found 01 FE header, skipping 6 bytes");
+ foundValidOffset = true;
+ }
+
+ // If no known header found, probe offsets 0, 2, 4, 6 to find valid RLE start
+ if (!foundValidOffset) {
+ for (int testOffset = 0; testOffset <= 6 && testOffset + 2 <= dataSize; testOffset += 2) {
+ int testLineSize = READ_LE_UINT16(fobjData + testOffset);
+ // A valid first line size should be: > 0, <= width*2 (reasonable for RLE)
+ // Also check that the total data is enough for at least height lines
+ if (testLineSize > 0 && testLineSize <= width * 2 && testLineSize < dataSize - testOffset) {
+ // Further validation: sum line sizes should roughly match data size
+ int sumTest = 0;
+ int linesTest = 0;
+ byte *testPtr = fobjData + testOffset;
+ bool validSum = true;
+
+ while (linesTest < height && testPtr + 2 <= fobjData + dataSize) {
+ int ls = READ_LE_UINT16(testPtr);
+ if (ls <= 0 || ls > width * 2) {
+ validSum = false;
+ break;
+ }
+ sumTest += ls + 2;
+ testPtr += ls + 2;
+ linesTest++;
+ }
+
+ // Accept if we got close to expected number of lines
+ if (validSum && linesTest >= height - 1) {
+ headerSkip = testOffset;
+ foundValidOffset = true;
+ debug("Rebel2: Codec 45 found valid RLE at offset %d (tested %d lines)", testOffset, linesTest);
+ break;
+ }
+ }
+ }
+ }
+
+ if (!foundValidOffset) {
+ debug("Rebel2: Codec 45 couldn't find valid RLE offset, using offset 0");
}
byte *srcPtr = fobjData + headerSkip;
@@ -2228,9 +2342,11 @@ void InsaneRebel2::loadEmbeddedSan(int userId, byte *animData, int32 size, byte
}
// Draw immediately to renderBitmap if valid
- // Skip immediate draw for Handler 7/8 - these are ship direction sprites
- // that should be selected based on direction and drawn during post-rendering
- bool skipImmediateDraw = (_rebelHandler == 7 || _rebelHandler == 8);
+ // Skip immediate draw for handlers that render HUD during post-processing:
+ // - Handler 7/8: Ship direction sprites selected based on direction
+ // - Handler 0x26/0x19: Cockpit HUD positioned based on mouse/crosshair
+ bool skipImmediateDraw = (_rebelHandler == 7 || _rebelHandler == 8 ||
+ _rebelHandler == 0x26 || _rebelHandler == 0x19);
if (frame.valid && renderBitmap && !skipImmediateDraw) {
int pitch = (_player && _player->_width > 0) ? _player->_width : 320;
@@ -2923,6 +3039,8 @@ void InsaneRebel2::procPostRendering(byte *renderBitmap, int32 codecparam, int32
// Clear the status bar area at Y=180-199 with background color
// Original assembly: FUN_004288c0(local_8, 0, 0, 0xb4, 0x140, 0x14, 4)
// This fills width=320, height=20 starting at Y=180 with color index 4
+ // Status bar is ALWAYS shown during gameplay (_rebelHandler != 0)
+ // Hidden during cinematics/intros when _rebelHandler == 0 (handled by early return above)
const byte statusBarBgColor = 4;
for (int y = statusBarY; y < videoHeight; y++) {
@@ -2936,7 +3054,96 @@ void InsaneRebel2::procPostRendering(byte *renderBitmap, int32 codecparam, int32
}
// ============================================================
- // STEP 1: Draw embedded SAN HUD overlays FIRST (from IACT chunks)
+ // STEP 1A: Draw NUT-based HUD overlays for Handler 0x26/0x19 (turret modes)
+ // ============================================================
+ // For turret handlers, the HUD overlay is loaded as a NUT file via IACT opcode 8
+ // and contains animated frames for blinking cockpit lights.
+ //
+ // From FUN_004089ab disassembly (lines 195-226):
+ // - DAT_0047fe78 (_hudOverlayNut): Primary HUD overlay with 6 animation frames
+ // - Position formula (low-res):
+ // X = 160 + (mouseOffsetX >> 4) - (width / 2) - spriteOffsetX
+ // Y = 182 - (mouseOffsetY >> 4) - height - spriteOffsetY
+ // - Animation: spriteIndex = (frameCounter / 2) % 6
+ //
+ // The mouse offset creates a subtle parallax effect as the view pans.
+ if ((_rebelHandler == 0x26 || _rebelHandler == 0x19) && _hudOverlayNut && _hudOverlayNut->getNumChars() > 0) {
+ // Calculate mouse offset (clamped to -127..127)
+ // These match DAT_0047a7e0/DAT_0047a7e2 in the original
+ int mouseOffsetX = (_vm->_mouse.x - 160); // Relative to screen center
+ int mouseOffsetY = (_vm->_mouse.y - 100); // Relative to screen center
+ if (mouseOffsetX > 127) mouseOffsetX = 127;
+ if (mouseOffsetX < -127) mouseOffsetX = -127;
+ if (mouseOffsetY > 127) mouseOffsetY = 127;
+ if (mouseOffsetY < -127) mouseOffsetY = -127;
+
+ // Animation frame cycling: (frameCounter / 2) % 6
+ // This creates a 12-frame cycle (each sprite shown for 2 video frames)
+ int numSprites = _hudOverlayNut->getNumChars();
+ int animFrameCount = MIN(numSprites, 6); // Use up to 6 frames for animation
+ int animFrame = 0;
+ if (animFrameCount > 0) {
+ animFrame = (curFrame / 2) % animFrameCount;
+ }
+
+ // Get sprite dimensions and offset
+ // NUT format stores sprite offsets that affect positioning
+ int spriteW = _hudOverlayNut->getCharWidth(animFrame);
+ int spriteH = _hudOverlayNut->getCharHeight(animFrame);
+
+ // Position calculation from assembly (low-res mode):
+ // X = 0xa0 (160) + (mouseOffsetX >> 4) - (width / 2) - offsetX
+ // Y = 0xb6 (182) - (mouseOffsetY >> 4) - height - offsetY
+ // Note: The sprite offset is embedded in the NUT data; for simplicity we use 0
+ int spriteOffsetX = 0; // Could be extracted from NUT if needed
+ int spriteOffsetY = 0;
+
+ int hudX = 160 + (mouseOffsetX >> 4) - (spriteW / 2) - spriteOffsetX;
+ int hudY = 182 - (mouseOffsetY >> 4) - spriteH - spriteOffsetY;
+
+ // Apply view offset for scrolling background
+ hudX += _viewX;
+ hudY += _viewY;
+
+ // Draw the HUD overlay:
+ // From FUN_004089ab: Sprite 0 is ALWAYS drawn first (base cockpit)
+ // Then if animation frame != 0, draw the animation frame on top (blinking lights)
+ renderNutSprite(renderBitmap, pitch, width, height, hudX, hudY, _hudOverlayNut, 0);
+
+ // Draw animation overlay frame if not frame 0
+ // Animation frames 1-5 contain the blinking light states
+ if (animFrame != 0 && animFrame < numSprites) {
+ renderNutSprite(renderBitmap, pitch, width, height, hudX, hudY, _hudOverlayNut, animFrame);
+ }
+
+ debug(5, "Rebel2 HUD: Drawing NUT overlay frame %d/%d at (%d,%d) mouseOffset=(%d,%d)",
+ animFrame, numSprites, hudX, hudY, mouseOffsetX, mouseOffsetY);
+ }
+
+ // Draw secondary HUD overlay if present (DAT_0047fe80)
+ if ((_rebelHandler == 0x26 || _rebelHandler == 0x19) && _hudOverlay2Nut && _hudOverlay2Nut->getNumChars() > 0) {
+ // Similar positioning to primary overlay
+ int mouseOffsetX = (_vm->_mouse.x - 160);
+ int mouseOffsetY = (_vm->_mouse.y - 100);
+ if (mouseOffsetX > 127) mouseOffsetX = 127;
+ if (mouseOffsetX < -127) mouseOffsetX = -127;
+ if (mouseOffsetY > 127) mouseOffsetY = 127;
+ if (mouseOffsetY < -127) mouseOffsetY = -127;
+
+ int spriteW = _hudOverlay2Nut->getCharWidth(0);
+ int spriteH = _hudOverlay2Nut->getCharHeight(0);
+
+ int hudX = 160 + (mouseOffsetX >> 4) - (spriteW / 2);
+ int hudY = 182 - (mouseOffsetY >> 4) - spriteH;
+
+ hudX += _viewX;
+ hudY += _viewY;
+
+ renderNutSprite(renderBitmap, pitch, width, height, hudX, hudY, _hudOverlay2Nut, 0);
+ }
+
+ // ============================================================
+ // STEP 1B: Draw embedded SAN HUD overlays (from IACT chunks)
// ============================================================
// For Handler 7 (Level 3): HUD elements are scattered across the screen
// - Each frame has its own renderX/renderY position from FOBJ left/top
@@ -3011,22 +3218,17 @@ void InsaneRebel2::procPostRendering(byte *renderBitmap, int32 codecparam, int32
destX = frame.renderX;
destY = frame.renderY;
- // For Handler 0x26 (turret) and 0x19 (mixed): Cockpit overlays positioned at bottom
- // The cockpit pieces (slots 1-2) should be rendered just above the status bar (Y=180)
- // The FOBJ top value might be 0, so we override it for cockpit pieces
+ // For Handler 0x26 (turret) and 0x19 (mixed): HUD overlay positioning
+ // From FUN_004089ab lines 203-222:
+ // X = 160 + (mouseOffsetX >> 4) - width/2 - spriteOffsetX
+ // Y = 182 - ((mouseOffsetY - 128) >> 4) - height - spriteOffsetY
+ // When mouse at center: X = 160 - width/2 - offsetX, Y = 190 - height - offsetY
if ((_rebelHandler == 0x26 || _rebelHandler == 0x19) && (hudSlot == 1 || hudSlot == 2)) {
- // Cockpit overlay position: just above status bar
- // Status bar is at Y=180, cockpit sits right above it
- // For slot 1 (left piece): X = 0
- // For slot 2 (right piece): X = width of slot 1
- if (hudSlot == 1) {
- destX = 0;
- } else if (hudSlot == 2 && _rebelEmbeddedHud[1].valid) {
- // Position slot 2 right after slot 1
- destX = _rebelEmbeddedHud[1].width;
- }
- // Y position: status bar (180) minus cockpit height
- destY = 180 - frame.height;
+ // Position based on assembly formula (static center position)
+ // X: centered horizontally, adjusted by sprite offset
+ destX = 160 - frame.width / 2 - frame.renderX;
+ // Y: 190 - height - offsetY (just above status bar at Y=180)
+ destY = 190 - frame.height - frame.renderY;
}
// For Handler 7: Apply position offset based on ship position
@@ -3044,6 +3246,10 @@ void InsaneRebel2::procPostRendering(byte *renderBitmap, int32 codecparam, int32
destX += _viewX;
destY += _viewY;
+ // Debug: log embedded frame render dimensions
+ debug(3, "Rebel2: Rendering embedded HUD slot=%d size=%dx%d at (%d,%d) screen=%dx%d",
+ hudSlot, frame.width, frame.height, destX, destY, pitch, height);
+
// Draw frame with transparency (pixel 0 and 231 = transparent, matching NUT rendering)
for (int y = 0; y < frame.height && (destY + y) < height; y++) {
for (int x = 0; x < frame.width && (destX + x) < pitch; x++) {
@@ -3069,6 +3275,7 @@ void InsaneRebel2::procPostRendering(byte *renderBitmap, int32 codecparam, int32
// - Sprites 2-5: Difficulty stars (1-4 stars, drawn at 0,0)
// - Sprite 6: Damage bar fill (drawn with clip rect X=0x3f, Y=9, W=64, H=6)
// - Sprite 7: Damage alert (flashing red when damage critical)
+ // Status bar is ALWAYS drawn during gameplay (_rebelHandler != 0)
if (_smush_cockpitNut) {
// Draw status bar background frame (sprite 1) at (0, statusBarY)
// This sprite is the full-width status bar background
@@ -4818,6 +5025,7 @@ void InsaneRebel2::playCinematic(const char *filename) {
// Original: DAT_0047ee84 is only set by IACT opcode 6 during gameplay videos
// Cinematics don't have opcode 6, so handler stays 0
_rebelHandler = 0;
+ _rebelStatusBarSprite = 0; // No status bar during cinematics
SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
splayer->setCurVideoFlags(0x20); // Cinematic mode
@@ -4857,6 +5065,7 @@ bool InsaneRebel2::playLevelGameplay(int levelId) {
// Reset damage/shield for this level
_playerShield = 255;
_rebelHandler = 0;
+ _rebelStatusBarSprite = 0; // Will be set by IACT opcode 6 if par4==1
debug("Rebel2: Starting gameplay for level %d", levelId);
@@ -5009,6 +5218,9 @@ void InsaneRebel2::playLevelEnd(int levelId) {
// Play level completion video (LEVXX/XXEND.SAN)
// Emulates FUN_00417327 call
+ _rebelHandler = 0;
+ _rebelStatusBarSprite = 0; // No status bar during end cinematic
+
Common::String dir = getLevelDir(levelId);
Common::String prefix = getLevelPrefix(levelId);
Common::String filename = Common::String::format("%s/%sEND.SAN", dir.c_str(), prefix.c_str());
@@ -5025,6 +5237,9 @@ void InsaneRebel2::playLevelDeath(int levelId) {
// The variant depends on the frame where player died
// For simplicity, we'll play the A variant
+ _rebelHandler = 0;
+ _rebelStatusBarSprite = 0; // No status bar during death cinematic
+
Common::String dir = getLevelDir(levelId);
Common::String prefix = getLevelPrefix(levelId);
@@ -5045,6 +5260,10 @@ void InsaneRebel2::playLevelDeath(int levelId) {
void InsaneRebel2::playLevelRetry(int levelId) {
// Play retry prompt video (LEVXX/XXRETRY.SAN)
+ // Reset handler state for the retry cinematic
+
+ _rebelHandler = 0;
+ _rebelStatusBarSprite = 0; // Reset for retry - will be set by IACT opcode 6 if needed
Common::String dir = getLevelDir(levelId);
Common::String prefix = getLevelPrefix(levelId);
@@ -5061,6 +5280,9 @@ void InsaneRebel2::playLevelGameOver(int levelId) {
// Play game over video (LEVXX/XXOVER.SAN)
// Emulates FUN_00417ab2 call
+ _rebelHandler = 0;
+ _rebelStatusBarSprite = 0; // No status bar during game over cinematic
+
Common::String dir = getLevelDir(levelId);
Common::String prefix = getLevelPrefix(levelId);
Common::String filename = Common::String::format("%s/%sOVER.SAN", dir.c_str(), prefix.c_str());
@@ -5207,6 +5429,9 @@ Common::String InsaneRebel2::selectDeathVideoVariant(int levelId, int phase, int
void InsaneRebel2::playLevelDeathVariant(int levelId, int phase, int frame) {
// Play death video with proper variant selection
+ _rebelHandler = 0;
+ _rebelStatusBarSprite = 0; // No status bar during death cinematic
+
Common::String dir = getLevelDir(levelId);
Common::String prefix = getLevelPrefix(levelId);
Common::String variant = selectDeathVideoVariant(levelId, phase, frame);
@@ -5229,6 +5454,9 @@ void InsaneRebel2::playLevelDeathVariant(int levelId, int phase, int frame) {
void InsaneRebel2::playLevelRetryVariant(int levelId, int phase) {
// Play retry video - phase-specific for multi-phase levels
+ _rebelHandler = 0;
+ _rebelStatusBarSprite = 0; // Reset for retry - will be set by IACT opcode 6 if needed
+
Common::String dir = getLevelDir(levelId);
Common::String prefix = getLevelPrefix(levelId);
Common::String filename;
diff --git a/engines/scumm/insane/insane_rebel.h b/engines/scumm/insane/insane_rebel.h
index bd48a8b016d..913f9bd2814 100644
--- a/engines/scumm/insane/insane_rebel.h
+++ b/engines/scumm/insane/insane_rebel.h
@@ -519,6 +519,24 @@ public:
int16 _flyShipScreenX; // DAT_0044370e - Ship X screen position
int16 _flyShipScreenY; // DAT_0044370c - Ship Y screen position
+ // ======================= Handler 0x26 Turret HUD Overlays =======================
+ // For turret missions (Level 1, etc.), Handler 0x26 uses NUT-based HUD overlays
+ // loaded via IACT opcode 8. These contain animated cockpit panel elements.
+ //
+ // Based on FUN_00407fcb and FUN_004089ab disassembly:
+ // - DAT_0047fe78: Primary HUD overlay (GRD001/002, par3=1 or 2, 6 animation frames)
+ // - DAT_0047fe80: Secondary HUD overlay (GRD010, par3=3 or 4, static or animated)
+ //
+ // Animation: The HUD overlay cycles through 6 sprite frames for blinking lights
+ // Formula: spriteIndex = (frameCounter / 2) % 6
+ //
+ // Position formula (from FUN_004089ab lines 203-222):
+ // X = 160 + (mouseOffsetX >> 4) - (width / 2) - spriteOffsetX
+ // Y = 182 - (mouseOffsetY >> 4) - height - spriteOffsetY
+
+ NutRenderer *_hudOverlayNut; // DAT_0047fe78 - Primary HUD overlay (animated)
+ NutRenderer *_hudOverlay2Nut; // DAT_0047fe80 - Secondary HUD overlay
+
/* Difficulty Level (0, 1, 2 = Easy, Med, Hard) */
int _difficulty;
void drawCornerBrackets(byte *dst, int pitch, int width, int height, int x, int y, int w, int h, byte color);
Commit: 547360550093ac6ce37b135bed2997d3a56b71b0
https://github.com/scummvm/scummvm/commit/547360550093ac6ce37b135bed2997d3a56b71b0
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:08:55+02:00
Commit Message:
SCUMM: RA2: Fix low-resolution cockpit HUD loading
Changed paths:
engines/scumm/insane/insane_rebel.cpp
diff --git a/engines/scumm/insane/insane_rebel.cpp b/engines/scumm/insane/insane_rebel.cpp
index 89be33ba7e8..2f956385176 100644
--- a/engines/scumm/insane/insane_rebel.cpp
+++ b/engines/scumm/insane/insane_rebel.cpp
@@ -3161,6 +3161,15 @@ void InsaneRebel2::procPostRendering(byte *renderBitmap, int32 codecparam, int32
for (int hudSlot = 1; hudSlot < 16; hudSlot++) {
EmbeddedSanFrame &frame = _rebelEmbeddedHud[hudSlot];
if (frame.valid && frame.pixels && frame.width > 0 && frame.height > 0) {
+ // Skip frames at position (0,0) with small dimensions - these are likely animation patches
+ // that need special handling (userId=3: 11x26, userId=4: 17x53 in Level 1)
+ // TODO: Investigate how these are used in the original assembly
+ if (frame.renderX == 0 && frame.renderY == 0 && frame.width < 50 && frame.height < 60) {
+ debug(3, "Rebel2: Skipping small embedded frame at (0,0): slot=%d size=%dx%d",
+ hudSlot, frame.width, frame.height);
+ continue;
+ }
+
int destX, destY;
// For Handler 7: Check if this is a ship direction frame
@@ -3219,24 +3228,30 @@ void InsaneRebel2::procPostRendering(byte *renderBitmap, int32 codecparam, int32
destY = frame.renderY;
// For Handler 0x26 (turret) and 0x19 (mixed): HUD overlay positioning
- // From FUN_004089ab lines 203-222:
+ // From FUN_004089ab assembly lines 560-582:
// X = 160 + (mouseOffsetX >> 4) - width/2 - spriteOffsetX
- // Y = 182 - ((mouseOffsetY - 128) >> 4) - height - spriteOffsetY
- // When mouse at center: X = 160 - width/2 - offsetX, Y = 190 - height - offsetY
+ // Y = 0xb6 (182) - (mouseY >> 4) - height - spriteOffsetY (low-res mode)
+ // When mouse centered (mouseY=0), local_38=-128, offset=-8, so Y = 190 - height - offsetY
+ // For embedded cockpit frames at fixed position, use screen bottom (200) as base
if ((_rebelHandler == 0x26 || _rebelHandler == 0x19) && (hudSlot == 1 || hudSlot == 2)) {
// Position based on assembly formula (static center position)
// X: centered horizontally, adjusted by sprite offset
destX = 160 - frame.width / 2 - frame.renderX;
- // Y: 190 - height - offsetY (just above status bar at Y=180)
- destY = 190 - frame.height - frame.renderY;
+ // Y: screen bottom (200) - height - offsetY for bottom-aligned cockpit
+ destY = 200 - frame.height - frame.renderY;
}
- // For Handler 7: Apply position offset based on ship position
- // This makes the ship sprite follow the mouse/crosshair
- if (_rebelHandler == 7 && destX > 100 && destY > 50) {
- // This appears to be a ship sprite (center of screen)
- // Offset based on ship position relative to center
- int16 offsetX = (_shipPosX - 160) / 8; // Scale down movement
+ // For Handler 7 (space flight): HUD cockpit overlays use FOBJ position directly
+ // From FUN_0040d836: The clip region is 320x170 (0x140 x 0xaa)
+ // Large cockpit frames (hudSlot 1/2) need centering like turret handler
+ if (_rebelHandler == 7 && (hudSlot == 1 || hudSlot == 2) && frame.width > 100) {
+ // Large cockpit frame - center horizontally, position at bottom
+ destX = 160 - frame.width / 2 - frame.renderX;
+ // Position above status bar area (170 is render height for Handler 7)
+ destY = 170 - frame.height - frame.renderY;
+ } else if (_rebelHandler == 7 && destX > 100 && destY > 50) {
+ // Ship sprite in center of screen - apply position offset
+ int16 offsetX = (_shipPosX - 160) / 8;
int16 offsetY = (_shipPosY - 100) / 8;
destX += offsetX;
destY += offsetY;
Commit: 8718764edfdf42144a75bcfa1c8a5925acbfce12
https://github.com/scummvm/scummvm/commit/8718764edfdf42144a75bcfa1c8a5925acbfce12
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:08:55+02:00
Commit Message:
SCUMM: RA2: Add level 2 video decoders
Changed paths:
engines/scumm/insane/insane.h
engines/scumm/insane/insane_rebel.cpp
engines/scumm/insane/insane_rebel.h
engines/scumm/insane/insane_scenes.cpp
engines/scumm/smush/codec1.cpp
engines/scumm/smush/smush_player.cpp
engines/scumm/smush/smush_player.h
diff --git a/engines/scumm/insane/insane.h b/engines/scumm/insane/insane.h
index 19382f85576..d4d124184c5 100644
--- a/engines/scumm/insane/insane.h
+++ b/engines/scumm/insane/insane.h
@@ -61,13 +61,13 @@ public:
void setSmushPlayer(SmushPlayer *player);
void runScene(int arraynum);
- void procPreRendering();
+ virtual void procPreRendering(byte *renderBitmap);
void virtual procPostRendering(byte *renderBitmap, int32 codecparam, int32 setupsan12,
int32 setupsan13, int32 curFrame, int32 maxFrame);
void virtual procIACT(byte *renderBitmap, int32 codecparam, int32 setupsan12,
int32 setupsan13, Common::SeekableReadStream &b, int32 size, int32 flags, int16 par1,
int16 par2, int16 par3, int16 par4);
- void procSKIP(int32 subSize, Common::SeekableReadStream &b);
+ virtual void procSKIP(int32 subSize, Common::SeekableReadStream &b);
void escapeKeyHandler();
bool isInsaneActive() { return _insaneIsRunning; }
diff --git a/engines/scumm/insane/insane_rebel.cpp b/engines/scumm/insane/insane_rebel.cpp
index 2f956385176..5e9ca23ecc5 100644
--- a/engines/scumm/insane/insane_rebel.cpp
+++ b/engines/scumm/insane/insane_rebel.cpp
@@ -48,6 +48,10 @@
namespace Scumm {
+// External codec functions from codec1.cpp
+extern void smushDecodeRLE(byte *dst, const byte *src, int left, int top, int width, int height, int pitch);
+extern void smushDecodeUncompressed(byte *dst, const byte *src, int left, int top, int width, int height, int pitch);
+
InsaneRebel2::InsaneRebel2(ScummEngine_v7 *scumm) {
_vm = scumm;
@@ -260,6 +264,8 @@ InsaneRebel2::InsaneRebel2(ScummEngine_v7 *scumm) {
_shipSprite2 = nullptr;
_shipOverlay1 = nullptr;
_shipOverlay2 = nullptr;
+ _level2Background = nullptr;
+ _level2BackgroundLoaded = false;
_shipPosX = 0xa0; // Start centered (160 in hex)
_shipPosY = 0x28; // Start at vertical center (40)
_shipTargetX = 0xa0;
@@ -341,6 +347,8 @@ InsaneRebel2::~InsaneRebel2() {
delete _shipSprite2;
delete _shipOverlay1;
delete _shipOverlay2;
+ free(_level2Background);
+ _level2Background = nullptr;
// Clean up Handler 7 FLY ship sprites
delete _flyShipSprite;
@@ -367,10 +375,9 @@ bool InsaneRebel2::notifyEvent(const Common::Event &event) {
switch (event.kbd.keycode) {
case Common::KEYCODE_ESCAPE:
- // ESC skips cutscenes (videos with 0x20 flag = non-interactive)
- // Emulates FUN_0041f537 behavior: if key == 0x1b, skip video
- if (splayer && (splayer->_curVideoFlags & 0x20) != 0) {
- debug("Rebel2: ESC pressed - skipping cutscene");
+ // ESC skips videos
+ if (splayer) {
+ debug("Rebel2: ESC pressed - skipping video");
_vm->_smushVideoShouldFinish = true;
return true; // Consume the event
}
@@ -691,6 +698,37 @@ void InsaneRebel2::clearBit(int n) {
_iactBits[n] = 0;
}
+void InsaneRebel2::procSKIP(int32 subSize, Common::SeekableReadStream &b) {
+ // Rebel Assault 2 does NOT use Full Throttle's conditional frame skip mechanism.
+ // The base Insane::procSKIP() uses _iactBits to decide whether to skip frame objects,
+ // but RA2 handles conditional content differently through IACT opcodes 2/3/4.
+ //
+ // By overriding this to do nothing, we prevent random frame objects from being
+ // skipped due to uninitialized _iactBits state.
+ //
+ // If RA2 SKIP chunks need to be handled, implement RA2-specific logic here.
+ // For now, just consume the data without setting _skipNext.
+ (void)subSize;
+ (void)b;
+}
+
+void InsaneRebel2::procPreRendering(byte *renderBitmap) {
+ // Call base class implementation first (handles Full Throttle state machine)
+ Insane::procPreRendering(renderBitmap);
+
+ // For Level 2 gameplay (Handler 8 or 25), restore the background BEFORE FOBJ decoding.
+ // The tiny FOBJ sprites (7x10, 9x38 pixels) only draw new sprite positions but don't
+ // clear old ones. By restoring the full background each frame, we ensure old sprite
+ // positions are erased before new ones are drawn.
+ //
+ // This is called at the start of handleFrame(), before any FOBJ chunks are processed.
+ if ((_rebelHandler == 8 || _rebelHandler == 25) && _level2BackgroundLoaded && _level2Background && renderBitmap) {
+ for (int y = 0; y < 200; y++) {
+ memcpy(renderBitmap + y * 320, _level2Background + y * 320, 320);
+ }
+ }
+}
+
void InsaneRebel2::procIACT(byte *renderBitmap, int32 codecparam, int32 setupsan12,
int32 setupsan13, Common::SeekableReadStream &b, int32 size, int32 flags,
int16 par1, int16 par2, int16 par3, int16 par4) {
@@ -849,7 +887,7 @@ void InsaneRebel2::iactRebel2Scene1(byte *renderBitmap, int32 codecparam, int32
} else if (par1 == 6) {
// Opcode 6: Level setup / mode switch (FUN_41CADB case 4)
- iactRebel2Opcode6(renderBitmap, b, par2, par3, par4);
+ iactRebel2Opcode6(renderBitmap, b, size, par2, par3, par4);
} else if (par1 == 8) {
// Opcode 8: HUD resource loading (FUN_41CADB case 6)
iactRebel2Opcode8(renderBitmap, b, size, par2, par3, par4);
@@ -1006,7 +1044,7 @@ void InsaneRebel2::iactRebel2Opcode3(Common::SeekableReadStream &b, int16 par2,
// other subcases not implemented yet
}
-void InsaneRebel2::iactRebel2Opcode6(byte *renderBitmap, Common::SeekableReadStream &b, int16 par2, int16 par3, int16 par4) {
+void InsaneRebel2::iactRebel2Opcode6(byte *renderBitmap, Common::SeekableReadStream &b, int32 chunkSize, int16 par2, int16 par3, int16 par4) {
// Opcode 6: Level setup / mode switch
// Based on FUN_41CADB case 4 (switch on *local_14 - 2 == 4, meaning opcode 6)
//
@@ -1023,6 +1061,10 @@ void InsaneRebel2::iactRebel2Opcode6(byte *renderBitmap, Common::SeekableReadStr
// Update handler type if par2 is a known handler value (from FUN_4033CF case 6)
if (par2 == 7 || par2 == 8 || par2 == 0x19 || par2 == 0x26) {
+ // Reset Level 2 background flag when transitioning away from Handler 8
+ if (_rebelHandler == 8 && par2 != 8) {
+ _level2BackgroundLoaded = false;
+ }
_rebelHandler = par2;
debug("Rebel2 Opcode 6: Setting handler=%d", par2);
}
@@ -1382,11 +1424,12 @@ void InsaneRebel2::iactRebel2Opcode6(byte *renderBitmap, Common::SeekableReadStr
_rebelLevelType, _rebelAutopilot, _rebelDamageLevel, _rebelViewOffsetX, _rebelViewOffsetY);
// Detect and load embedded ANIM (SAN) within the remaining IACT payload
+ // Note: chunkSize is the remaining IACT payload size after par1-par4 header
{
int64 startPos = b.pos();
- int64 totalSize = b.size();
- if (totalSize >= 0 && totalSize > startPos) {
- int64 remaining = totalSize - startPos;
+ // Use chunkSize (remaining IACT payload) rather than b.size() (entire FRME stream)
+ int64 remaining = chunkSize;
+ if (remaining > 0) {
int scanSize = (int)MIN<int64>(remaining, 65536);
byte *scanBuf = (byte *)malloc(scanSize);
if (scanBuf) {
@@ -1395,7 +1438,8 @@ void InsaneRebel2::iactRebel2Opcode6(byte *renderBitmap, Common::SeekableReadStr
if (READ_BE_UINT32(scanBuf + i) == MKTAG('A','N','I','M')) {
int64 animStreamPos = startPos + i;
uint32 animReportedSize = READ_BE_UINT32(scanBuf + i + 4);
- int32 toCopy = (int)MIN<int64>((int64)animReportedSize + 8, totalSize - animStreamPos);
+ // Limit to remaining IACT payload (chunkSize - offset into payload)
+ int32 toCopy = (int)MIN<int64>((int64)animReportedSize + 8, chunkSize - i);
if (toCopy > 0) {
byte *animData = (byte *)malloc(toCopy);
if (animData) {
@@ -1677,6 +1721,99 @@ void InsaneRebel2::iactRebel2Opcode8(byte *renderBitmap, Common::SeekableReadStr
}
}
+ // Handler 8/25 (Level 2): Load background
+ // par4=5 contains the background image embedded as ANIM with FOBJ codec 3
+ // FUN_00401234 case 6 switch case 5: Creates 320x200 buffer (DAT_0047e030)
+ // Handler 8 = third-person vehicle, Handler 25 (0x19) = turret/mixed view
+ // Level 2 uses handler 25 but background loading is the same mechanism
+ if (!loadedAsNut && (_rebelHandler == 8 || _rebelHandler == 25) && par4 == 5) {
+ debug("Rebel2 Opcode 8: Loading Level 2 background (par4=5, animSize=%d)", toCopy);
+
+ // Allocate background buffer if needed (320x200 = 64000 bytes)
+ if (_level2Background == nullptr) {
+ _level2Background = (byte *)malloc(320 * 200);
+ if (_level2Background) {
+ memset(_level2Background, 0, 320 * 200);
+ }
+ }
+
+ if (_level2Background) {
+ // Parse embedded ANIM to find FOBJ
+ // Structure: ANIM tag at offset 0, AHDR, then FRME with FOBJ
+ int animOffset = 0;
+ if (toCopy >= 8 && READ_BE_UINT32(animData) == MKTAG('A','N','I','M')) {
+ uint32 animSize = READ_BE_UINT32(animData + 4);
+ debug("Rebel2 Opcode 8: Found ANIM tag, size=%u", animSize);
+
+ // Skip ANIM header (8 bytes) + AHDR chunk
+ // AHDR starts at offset 8, size is at offset 12 (big endian)
+ if (toCopy >= 16 && READ_BE_UINT32(animData + 8) == MKTAG('A','H','D','R')) {
+ uint32 ahdrSize = READ_BE_UINT32(animData + 12);
+ animOffset = 8 + 8 + ahdrSize; // After ANIM tag + AHDR
+ debug("Rebel2 Opcode 8: AHDR size=%u, FRME expected at offset %d", ahdrSize, animOffset);
+ }
+ }
+
+ // Look for FRME containing FOBJ
+ bool foundBackground = false;
+ for (int scanPos = animOffset; scanPos + 16 < toCopy && !foundBackground; scanPos++) {
+ if (READ_BE_UINT32(animData + scanPos) == MKTAG('F','R','M','E')) {
+ // Found FRME, look for FOBJ inside
+ int frmeSize = READ_BE_UINT32(animData + scanPos + 4);
+ debug("Rebel2 Opcode 8: Found FRME at %d, size=%d", scanPos, frmeSize);
+
+ for (int fobjPos = scanPos + 8; fobjPos + 18 < scanPos + 8 + frmeSize && fobjPos + 18 < toCopy; fobjPos++) {
+ if (READ_BE_UINT32(animData + fobjPos) == MKTAG('F','O','B','J')) {
+ uint32 fobjSize = READ_BE_UINT32(animData + fobjPos + 4);
+ byte *fobjData = animData + fobjPos + 8;
+
+ // FOBJ header: codec(2), x(2), y(2), w(2), h(2)
+ int16 codec = READ_LE_INT16(fobjData);
+ int16 fobjX = READ_LE_INT16(fobjData + 2);
+ int16 fobjY = READ_LE_INT16(fobjData + 4);
+ int16 fobjW = READ_LE_INT16(fobjData + 6);
+ int16 fobjH = READ_LE_INT16(fobjData + 8);
+
+ debug("Rebel2 Opcode 8: Found FOBJ at %d: codec=%d pos=(%d,%d) size=%dx%d dataSize=%u",
+ fobjPos, codec, fobjX, fobjY, fobjW, fobjH, fobjSize);
+
+ // Decode codec 3 (RLE) into background buffer
+ // Codec 3 uses bomp RLE format with line-by-line structure
+ // FOBJ header is 14 bytes: codec(2), x(2), y(2), w(2), h(2), unk(2), unk(2)
+ if (codec == 3 && fobjW > 0 && fobjH > 0 && fobjW <= 320 && fobjH <= 200) {
+ byte *rleData = fobjData + 14; // Skip full 14-byte FOBJ header
+
+ // Use the existing smushDecodeRLE function from codec1.cpp
+ // This properly decodes BOMP RLE with line size headers
+ smushDecodeRLE(_level2Background, rleData, fobjX, fobjY, fobjW, fobjH, 320);
+
+ debug("Rebel2 Opcode 8: Decoded Level 2 background (%dx%d at %d,%d)",
+ fobjW, fobjH, fobjX, fobjY);
+ _level2BackgroundLoaded = true;
+ foundBackground = true;
+
+ // Draw background to render bitmap immediately
+ if (renderBitmap) {
+ for (int by = 0; by < 200; by++) {
+ memcpy(renderBitmap + by * 320, _level2Background + by * 320, 320);
+ }
+ debug("Rebel2 Opcode 8: Copied Level 2 background to renderBitmap");
+ }
+ }
+ break; // Found FOBJ, stop searching
+ }
+ }
+ break; // Found FRME, stop searching
+ }
+ }
+
+ if (!foundBackground) {
+ debug("Rebel2 Opcode 8: Failed to find/decode background FOBJ");
+ }
+ }
+ loadedAsNut = true; // Mark as handled even if decode failed
+ }
+
if (!loadedAsNut) {
// Skip high-res turret HUD data (par3 == 2, 4) regardless of handler
// ScummVM runs at 320x200, so we only want low-res data (par3 == 1, 3)
@@ -2019,10 +2156,6 @@ void InsaneRebel2::init_enemyStruct(int id, int32 x, int32 y, int32 w, int32 h,
_enemies.push_back(e);
}
-// External helpers from smush_player.cpp but we are already in Scumm namespace
-extern void smushDecodeRLE(byte *dst, const byte *src, int left, int top, int width, int height, int pitch);
-extern void smushDecodeUncompressed(byte *dst, const byte *src, int left, int top, int width, int height, int pitch);
-
void InsaneRebel2::loadEmbeddedSan(int userId, byte *animData, int32 size, byte *renderBitmap) {
// Validate userId - Level 3 uses slots 0-11, allow up to 15 for safety
if (userId < 0 || userId > 15 || !animData || size < 32) {
@@ -3033,6 +3166,12 @@ void InsaneRebel2::procPostRendering(byte *renderBitmap, int32 codecparam, int32
// Original: FUN_00403240 only runs handlers when DAT_0047a814 == 0
processMouse();
+ // NOTE: Level 2 background is drawn ONCE during IACT opcode 8 par4=5 processing
+ // (in procIACT when the background ANIM is first loaded). The 0x08 video flag
+ // (preserve background) prevents the frame buffer from being cleared, so the
+ // background persists. FOBJ sprites (enemies) are then decoded on top by SMUSH.
+ // We do NOT redraw the background here as that would overwrite FOBJ content.
+
// ============================================================
// STEP 0: Fill status bar background (FUN_004288c0 equivalent)
// ============================================================
@@ -3227,17 +3366,15 @@ void InsaneRebel2::procPostRendering(byte *renderBitmap, int32 codecparam, int32
destX = frame.renderX;
destY = frame.renderY;
- // For Handler 0x26 (turret) and 0x19 (mixed): HUD overlay positioning
- // From FUN_004089ab assembly lines 560-582:
- // X = 160 + (mouseOffsetX >> 4) - width/2 - spriteOffsetX
- // Y = 0xb6 (182) - (mouseY >> 4) - height - spriteOffsetY (low-res mode)
- // When mouse centered (mouseY=0), local_38=-128, offset=-8, so Y = 190 - height - offsetY
- // For embedded cockpit frames at fixed position, use screen bottom (200) as base
+ // For Handler 0x26 (turret) and 0x19 (mixed): Embedded ANIM frame positioning
+ // From FUN_00407fcb assembly lines 389-391: screen dimensions are 320x200 (0x140 x 0xc8)
+ // Unlike NUT overlays (which use 0xb6=182 base with mouse parallax), embedded ANIM
+ // frames use screen height (200) as the base for bottom-aligned positioning.
+ // Formula: X = 160 - width/2 - fobj_left, Y = 200 - height - fobj_top
if ((_rebelHandler == 0x26 || _rebelHandler == 0x19) && (hudSlot == 1 || hudSlot == 2)) {
- // Position based on assembly formula (static center position)
- // X: centered horizontally, adjusted by sprite offset
+ // X: centered horizontally, adjusted by FOBJ left offset
destX = 160 - frame.width / 2 - frame.renderX;
- // Y: screen bottom (200) - height - offsetY for bottom-aligned cockpit
+ // Y: screen height (0xc8=200) - height - fobj_top for bottom-aligned cockpit
destY = 200 - frame.height - frame.renderY;
}
@@ -5573,13 +5710,24 @@ int InsaneRebel2::runLevel2() {
bonusCount = 0;
// ===== PHASE 1: P1/02P01_X.SAN =====
- // Select random variant (B, C, or D - not A in main loop)
+ // First play variant A which contains the background IACT (opcode 8, par4=5)
+ // The background is loaded during this video and persists for B/C/D variants
{
+ debug("Rebel2: Level 2 Phase 1 - playing 02P01_A.SAN (background loader)");
+ splayer->setCurVideoFlags(0x28);
+ splayer->play("LEV02/P1/02P01_A.SAN", 12);
+ _deathFrame = splayer->_frame;
+ }
+
+ if (_vm->shouldQuit()) return kLevelQuit;
+
+ // Now select random variant (B, C, or D) for actual gameplay
+ if (_playerShield > 0) {
int variant = getRandomVariant(3);
const char *variants[] = {"B", "C", "D"};
Common::String filename = Common::String::format("LEV02/P1/02P01_%s.SAN", variants[variant]);
- debug("Rebel2: Level 2 Phase 1 - playing %s", filename.c_str());
+ debug("Rebel2: Level 2 Phase 1 - playing %s (gameplay)", filename.c_str());
splayer->setCurVideoFlags(0x28);
splayer->play(filename.c_str(), 12);
_deathFrame = splayer->_frame;
@@ -5611,13 +5759,23 @@ int InsaneRebel2::runLevel2() {
_currentPhase = 2;
// ===== PHASE 2: P2/02P02_X.SAN =====
- // Variant selection based on switch (more complex in original)
+ // First play variant A which contains the background IACT for this phase
{
- int variant = getRandomVariant(6);
- const char *variants[] = {"A", "B", "C", "D", "E", "F"};
+ debug("Rebel2: Level 2 Phase 2 - playing 02P02_A.SAN (background loader)");
+ splayer->setCurVideoFlags(0x28);
+ splayer->play("LEV02/P2/02P02_A.SAN", 12);
+ _deathFrame = splayer->_frame;
+ }
+
+ if (_vm->shouldQuit()) return kLevelQuit;
+
+ // Now select gameplay variant (B through F)
+ if (_playerShield > 0) {
+ int variant = getRandomVariant(5);
+ const char *variants[] = {"B", "C", "D", "E", "F"};
Common::String filename = Common::String::format("LEV02/P2/02P02_%s.SAN", variants[variant]);
- debug("Rebel2: Level 2 Phase 2 - playing %s", filename.c_str());
+ debug("Rebel2: Level 2 Phase 2 - playing %s (gameplay)", filename.c_str());
splayer->setCurVideoFlags(0x28);
splayer->play(filename.c_str(), 12);
_deathFrame = splayer->_frame;
@@ -5649,13 +5807,23 @@ int InsaneRebel2::runLevel2() {
_currentPhase = 3;
// ===== PHASE 3: P3/02P03_X.SAN =====
- // Variant selection with more options (A through I)
+ // First play variant A which contains the background IACT for this phase
{
- int variant = getRandomVariant(9);
- const char *variants[] = {"A", "B", "C", "D", "E", "F", "G", "H", "I"};
+ debug("Rebel2: Level 2 Phase 3 - playing 02P03_A.SAN (background loader)");
+ splayer->setCurVideoFlags(0x28);
+ splayer->play("LEV02/P3/02P03_A.SAN", 12);
+ _deathFrame = splayer->_frame;
+ }
+
+ if (_vm->shouldQuit()) return kLevelQuit;
+
+ // Now select gameplay variant (B through I)
+ if (_playerShield > 0) {
+ int variant = getRandomVariant(8);
+ const char *variants[] = {"B", "C", "D", "E", "F", "G", "H", "I"};
Common::String filename = Common::String::format("LEV02/P3/02P03_%s.SAN", variants[variant]);
- debug("Rebel2: Level 2 Phase 3 - playing %s", filename.c_str());
+ debug("Rebel2: Level 2 Phase 3 - playing %s (gameplay)", filename.c_str());
splayer->setCurVideoFlags(0x28);
splayer->play(filename.c_str(), 12);
_deathFrame = splayer->_frame;
diff --git a/engines/scumm/insane/insane_rebel.h b/engines/scumm/insane/insane_rebel.h
index 913f9bd2814..b47bd1f3098 100644
--- a/engines/scumm/insane/insane_rebel.h
+++ b/engines/scumm/insane/insane_rebel.h
@@ -253,7 +253,7 @@ public:
// Handle IACT opcode subcases
void iactRebel2Opcode2(Common::SeekableReadStream &b, int16 par2, int16 par3, int16 par4);
void iactRebel2Opcode3(Common::SeekableReadStream &b, int16 par2, int16 par3, int16 par4);
- void iactRebel2Opcode6(byte *renderBitmap, Common::SeekableReadStream &b, int16 par2, int16 par3, int16 par4);
+ void iactRebel2Opcode6(byte *renderBitmap, Common::SeekableReadStream &b, int32 chunkSize, int16 par2, int16 par3, int16 par4);
void iactRebel2Opcode8(byte *renderBitmap, Common::SeekableReadStream &b, int32 chunkSize, int16 par2, int16 par3, int16 par4);
void iactRebel2Opcode9(byte *renderBitmap, Common::SeekableReadStream &b, int16 par2, int16 par3, int16 par4);
@@ -264,6 +264,14 @@ public:
int32 setupsan13, Common::SeekableReadStream &b, int32 size, int32 flags,
int16 par1, int16 par2, int16 par3, int16 par4) override;
+ // Override procSKIP to disable Full Throttle's conditional frame skip mechanism
+ // RA2 uses a different system for conditional frames via IACT opcodes
+ void procSKIP(int32 subSize, Common::SeekableReadStream &b) override;
+
+ // Override procPreRendering to restore Level 2 background before FOBJ decoding
+ // This is called at the start of each frame, before FOBJ sprites are decoded
+ void procPreRendering(byte *renderBitmap) override;
+
void drawLine(byte *dst, int pitch, int width, int height, int x0, int y0, int x1, int y1, byte color);
// mask231: when true, color 231 is treated as transparent (legacy sprites). For laser beams set false.
void drawTexturedLine(byte *dst, int pitch, int width, int height, int x0, int y0, int x1, int y1, NutRenderer *nut, int spriteIdx, int v, bool mask231 = true);
@@ -459,6 +467,12 @@ public:
NutRenderer *_shipOverlay1; // DAT_0047e020 - Additional overlay
NutRenderer *_shipOverlay2; // DAT_0047e018 - Additional overlay
+ // Level 2 background buffer (DAT_0047e030)
+ // Loaded from IACT opcode 8, par4=5 - contains 320x200 background image
+ // decoded from embedded ANIM in gameplay video frame 0
+ byte *_level2Background;
+ bool _level2BackgroundLoaded;
+
// Ship position tracking (matches DAT_0043e006/008)
// These are "raw" positions that get converted for display
int16 _shipPosX; // DAT_0043e006
diff --git a/engines/scumm/insane/insane_scenes.cpp b/engines/scumm/insane/insane_scenes.cpp
index 3f12701cb63..2b2d5d91bb8 100644
--- a/engines/scumm/insane/insane_scenes.cpp
+++ b/engines/scumm/insane/insane_scenes.cpp
@@ -812,7 +812,8 @@ void Insane::setEnemyCostumes() {
smush_warpMouse(160, 100, -1);
}
-void Insane::procPreRendering() {
+void Insane::procPreRendering(byte *renderBitmap) {
+ (void)renderBitmap; // Base class doesn't use this
_smush_isSanFileSetup = 0; // FIXME: This shouldn't be here
switchSceneIfNeeded();
diff --git a/engines/scumm/smush/codec1.cpp b/engines/scumm/smush/codec1.cpp
index 087e4d53aaf..5fa4dae5791 100644
--- a/engines/scumm/smush/codec1.cpp
+++ b/engines/scumm/smush/codec1.cpp
@@ -36,4 +36,227 @@ void smushDecodeRLE(byte *dst, const byte *src, int left, int top, int width, in
} while (--height);
}
+/**
+ * Codec 21/44: Line Update codec
+ * Used for fonts (NUT files) and some embedded HUD frames.
+ * Format: Each line has a 2-byte size header, then 2-byte skip, 2-byte count pairs with literal pixels.
+ * The count value needs +1 to get the actual number of pixels to copy.
+ * Note: Skip regions preserve previous frame content (delta compression).
+ */
+void smushDecodeLineUpdate(byte *dst, const byte *src, int left, int top, int width, int height, int pitch) {
+ dst += top * pitch + left;
+
+ while (height--) {
+ byte *dstPtrNext = dst + pitch;
+ const byte *srcPtrNext = src + 2 + READ_LE_UINT16(src);
+ src += 2; // Skip line size header
+ int len = width;
+ byte *lineDst = dst;
+
+ while (len > 0) {
+ // Read 2-byte LE skip value
+ int skip = READ_LE_UINT16(src);
+ src += 2;
+ lineDst += skip;
+ len -= skip;
+ if (len <= 0)
+ break;
+
+ // Read 2-byte LE copy count (+1 for actual count)
+ int count = READ_LE_UINT16(src) + 1;
+ src += 2;
+ if (count > len)
+ count = len;
+ len -= count;
+
+ // Copy literal pixels
+ memcpy(lineDst, src, count);
+ lineDst += count;
+ src += count;
+ }
+ dst = dstPtrNext;
+ src = srcPtrNext;
+ }
+}
+
+/**
+ * Codec 23: Skip/Copy with embedded RLE
+ * Used for video frames with skip regions.
+ * Format: Each line has 2-byte size, then (skip, runSize, RLE_data) triplets.
+ * Note: Skip regions preserve previous frame content (delta compression).
+ */
+void smushDecodeSkipRLE(byte *dst, const byte *src, int left, int top, int width, int height, int pitch) {
+ dst += top * pitch + left;
+
+ for (int row = 0; row < height; row++) {
+ int lineDataSize = READ_LE_UINT16(src);
+ src += 2;
+ const byte *lineEnd = src + lineDataSize;
+ byte *lineDst = dst;
+ int x = 0;
+
+ while (src < lineEnd && x < width) {
+ int skip = READ_LE_UINT16(src);
+ src += 2;
+ x += skip; // Skip preserves previous frame content
+ if (src >= lineEnd || x >= width)
+ break;
+
+ int runSize = READ_LE_UINT16(src);
+ src += 2;
+ const byte *runEnd = src + runSize;
+
+ // Decode RLE within this run - write ALL colors including 0
+ while (src < runEnd && x < width) {
+ byte code = *src++;
+ int num = (code >> 1) + 1;
+ if (num > width - x)
+ num = width - x;
+
+ if (code & 1) {
+ // RLE run - repeat color
+ byte color = (src < runEnd) ? *src++ : 0;
+ memset(lineDst + x, color, num);
+ x += num;
+ } else {
+ // Literal run - copy bytes
+ int toCopy = num;
+ if (toCopy > (int)(runEnd - src))
+ toCopy = (int)(runEnd - src);
+ memcpy(lineDst + x, src, toCopy);
+ src += toCopy;
+ x += toCopy;
+ }
+ }
+ src = runEnd;
+ }
+ src = lineEnd;
+ dst += pitch;
+ }
+}
+
+/**
+ * Codec 45: RA2-specific BOMP RLE with variable header
+ * Used for embedded ANIM frames, particularly small animation elements.
+ * Has a variable-length header (commonly 6 bytes starting with 01 FE).
+ * Note: For overlay sprites, color 0 is treated as transparent.
+ */
+void smushDecodeRA2Bomp(byte *dst, const byte *src, int left, int top, int width, int height, int pitch, int dataSize) {
+ dst += top * pitch + left;
+
+ // Detect header pattern and find RLE data start
+ int headerSkip = 0;
+
+ // Check for common 6-byte header pattern: 01 FE XX XX XX XX
+ if (dataSize > 6 && src[0] == 0x01 && src[1] == 0xFE) {
+ headerSkip = 6;
+ } else {
+ // Probe offsets to find valid RLE start
+ // Valid start should have reasonable line size values
+ for (int testOffset = 0; testOffset <= 6 && testOffset + 2 <= dataSize; testOffset += 2) {
+ int testLineSize = READ_LE_UINT16(src + testOffset);
+ // A valid line size should be positive and reasonable for the width
+ if (testLineSize > 0 && testLineSize <= width * 2 && testLineSize < dataSize - testOffset) {
+ // Further validation: try to count valid line sizes
+ int linesTest = 0;
+ const byte *testPtr = src + testOffset;
+ bool validSum = true;
+
+ while (linesTest < height && testPtr + 2 <= src + dataSize) {
+ int ls = READ_LE_UINT16(testPtr);
+ if (ls <= 0 || ls > width * 2) {
+ validSum = false;
+ break;
+ }
+ testPtr += ls + 2;
+ linesTest++;
+ }
+
+ if (validSum && linesTest >= height - 1) {
+ headerSkip = testOffset;
+ break;
+ }
+ }
+ }
+ }
+
+ src += headerSkip;
+ const byte *dataEnd = src + (dataSize - headerSkip);
+
+ // Check first value to determine per-line vs continuous mode
+ int firstVal = (src + 2 <= dataEnd) ? READ_LE_UINT16(src) : 0;
+ bool perLineMode = (firstVal > 0 && firstVal <= width * 2);
+
+ if (perLineMode) {
+ // Per-line RLE with 2-byte size headers
+ for (int row = 0; row < height && src < dataEnd; row++) {
+ int lineSize = READ_LE_UINT16(src);
+ src += 2;
+ if (lineSize <= 0 || lineSize > (int)(dataEnd - src))
+ break;
+
+ const byte *lineEnd = src + lineSize;
+ byte *rowDst = dst + row * pitch;
+ int x = 0;
+
+ while (src < lineEnd && x < width) {
+ byte ctrl = *src++;
+ int count = (ctrl >> 1) + 1;
+
+ if (ctrl & 1) {
+ // RLE fill - color 0 is transparent for overlay sprites
+ byte color = (src < lineEnd) ? *src++ : 0;
+ if (color != 0) {
+ int num = (count > width - x) ? width - x : count;
+ memset(rowDst + x, color, num);
+ }
+ x += count;
+ if (x > width)
+ x = width;
+ } else {
+ // Literal copy - color 0 is transparent for overlay sprites
+ for (int i = 0; i < count && x < width && src < lineEnd; i++) {
+ byte color = *src++;
+ if (color != 0)
+ rowDst[x] = color;
+ x++;
+ }
+ }
+ }
+ src = lineEnd;
+ }
+ } else {
+ // Continuous BOMP RLE (no per-line headers)
+ for (int row = 0; row < height && src < dataEnd; row++) {
+ byte *rowDst = dst + row * pitch;
+ int x = 0;
+
+ while (x < width && src < dataEnd) {
+ byte ctrl = *src++;
+ int count = (ctrl >> 1) + 1;
+
+ if (ctrl & 1) {
+ // RLE fill - color 0 is transparent for overlay sprites
+ byte color = (src < dataEnd) ? *src++ : 0;
+ if (color != 0) {
+ int num = (count > width - x) ? width - x : count;
+ memset(rowDst + x, color, num);
+ }
+ x += count;
+ if (x > width)
+ x = width;
+ } else {
+ // Literal copy - color 0 is transparent for overlay sprites
+ for (int i = 0; i < count && x < width && src < dataEnd; i++) {
+ byte color = *src++;
+ if (color != 0)
+ rowDst[x] = color;
+ x++;
+ }
+ }
+ }
+ }
+ }
+}
+
} // End of namespace Scumm
diff --git a/engines/scumm/smush/smush_player.cpp b/engines/scumm/smush/smush_player.cpp
index f88322f6846..7bd1b78ccfb 100644
--- a/engines/scumm/smush/smush_player.cpp
+++ b/engines/scumm/smush/smush_player.cpp
@@ -258,11 +258,20 @@ SmushPlayer::SmushPlayer(ScummEngine_v7 *scumm, IMuseDigital *imuseDigital, Insa
_seekPos = -1;
_skipNext = false;
+ _ra2FastForwarding = false;
_dst = nullptr;
_storeFrame = false;
_compressedFileMode = false;
_width = 0;
_height = 0;
+
+ // LOAD chunk streaming buffer (RA2)
+ _loadBuffer = nullptr;
+ _loadBufferSize = 0;
+ _loadBufferOffset = 0;
+ _loadReadOffset = 8; // Original starts reading at offset 8 (skips header)
+ _lastLoadChunkIdx = -1;
+ _totalLoadChunks = 0;
_scrollX = 0;
_scrollY = 0;
_IACTpos = 0;
@@ -306,6 +315,14 @@ SmushPlayer::~SmushPlayer() {
delete _IACTchannel;
delete _compressedFileSoundHandle;
terminateAudio();
+
+ // Free any preserved frame buffer (RA2 preserves this across videos)
+ free(_frameBuffer);
+ _frameBuffer = nullptr;
+ free(_specialBuffer);
+ _specialBuffer = nullptr;
+ free(_loadBuffer);
+ _loadBuffer = nullptr;
}
void SmushPlayer::init(int32 speed) {
@@ -321,6 +338,31 @@ void SmushPlayer::init(int32 speed) {
_vm->setDirtyColors(0, 255);
_dst = vs->getPixels(0, 0);
+ // For Rebel Assault 2, handle background preservation between videos:
+ // - Cinematic videos (flags 0x20) clear the buffer for a fresh start
+ // - Gameplay videos (flags 0x28) preserve the existing screen content
+ //
+ // The virtual screen (_dst = vs->getPixels) persists between videos, so
+ // when a gameplay video starts, _dst already contains the last frame of
+ // the previous cinematic video - which is exactly what we want.
+ // We do NOT restore from _frameBuffer because STOR captures frame 0
+ // (often a black initialization frame), not the last frame.
+ if (_vm->_game.id == GID_REBEL2 && _dst != nullptr) {
+ if ((_curVideoFlags & 0x08) == 0) {
+ // Cinematic mode (flags 0x20) - clear buffer for fresh video
+ memset(_dst, 0, vs->w * vs->h);
+ } else {
+ // Gameplay mode (flags 0x28) - do nothing, preserve existing screen content
+ // Count non-zero pixels to verify there's actual content
+ int nonZero = 0;
+ for (int i = 0; i < vs->w * vs->h; i++) {
+ if (_dst[i] != 0) nonZero++;
+ }
+ debug("SmushPlayer::init: Preserving screen for gameplay video (%dx%d, %d%% non-zero)",
+ vs->w, vs->h, (nonZero * 100) / (vs->w * vs->h));
+ }
+ }
+
// HACK HACK HACK: This is an *evil* trick, beware!
// We do this to fix bug #1792. A proper solution would change all the
// drawing code to use the pitch value specified by the virtual screen.
@@ -354,8 +396,14 @@ void SmushPlayer::release() {
free(_specialBuffer);
_specialBuffer = nullptr;
- free(_frameBuffer);
- _frameBuffer = nullptr;
+ // For Rebel Assault 2, preserve _frameBuffer across videos so that
+ // gameplay videos (which have no background FOBJ) can use the stored
+ // background from the previous BEG cinematic video.
+ // The _frameBuffer will be freed in the destructor or reused by the next video.
+ if (_vm->_game.id != GID_REBEL2) {
+ free(_frameBuffer);
+ _frameBuffer = nullptr;
+ }
_IACTstream = nullptr;
@@ -388,6 +436,97 @@ void SmushPlayer::handleFetch(int32 subSize, Common::SeekableReadStream &b) {
}
}
+/**
+ * Handle LOAD chunk for Rebel Assault 2
+ *
+ * LOAD chunks stream embedded resource data (likely audio for streaming playback)
+ * across multiple frames. The data is accumulated in a buffer and consumed by
+ * the audio system.
+ *
+ * Chunk format (from FUN_00424450 in original):
+ * Offset 0 (2 bytes): totalChunks - Total number of LOAD chunks in sequence
+ * Offset 2 (2 bytes): chunkIndex - Current chunk index (0-based)
+ * Offset 4 (6 bytes): unknown/padding
+ * Offset 10+: Actual data payload
+ *
+ * Processing:
+ * - First chunk (index 0) resets the buffer
+ * - Chunks must arrive sequentially (lastIndex + 1 == currentIndex)
+ * - Data is accumulated until all chunks are received
+ * - Consumer reads from buffer via getLoadData() (DAT_00482c18 read offset)
+ */
+void SmushPlayer::handleLoad(int32 subSize, Common::SeekableReadStream &b) {
+ debugC(DEBUG_SMUSH, "SmushPlayer::handleLoad()");
+
+ if (subSize < 10) {
+ warning("SmushPlayer::handleLoad: chunk too small (%d bytes)", subSize);
+ return;
+ }
+
+ // Read LOAD header
+ int16 totalChunks = b.readUint16LE();
+ int16 chunkIndex = b.readUint16LE();
+ b.skip(6); // Unknown/padding
+
+ int32 dataSize = subSize - 10;
+
+ debugC(DEBUG_SMUSH, "SmushPlayer::handleLoad: chunk %d/%d, dataSize=%d, bufferOffset=%d",
+ chunkIndex, totalChunks, dataSize, _loadBufferOffset);
+
+ // First chunk in sequence - reset buffer state
+ if (chunkIndex == 0) {
+ _loadBufferOffset = 0;
+ _loadReadOffset = 8; // Original skips 8 bytes at start (header in accumulated data?)
+ _lastLoadChunkIdx = -1;
+ _totalLoadChunks = totalChunks;
+
+ // Allocate/reallocate buffer if needed
+ // Estimate buffer size: typical LOAD data per chunk is ~350 bytes
+ // Total expected: totalChunks * 400 bytes (with margin)
+ int32 estimatedSize = totalChunks * 400;
+ if (_loadBuffer == nullptr || _loadBufferSize < estimatedSize) {
+ free(_loadBuffer);
+ _loadBufferSize = estimatedSize;
+ _loadBuffer = (byte *)malloc(_loadBufferSize);
+ if (_loadBuffer == nullptr) {
+ warning("SmushPlayer::handleLoad: Failed to allocate %d bytes for LOAD buffer",
+ _loadBufferSize);
+ _loadBufferSize = 0;
+ return;
+ }
+ debugC(DEBUG_SMUSH, "SmushPlayer::handleLoad: Allocated %d bytes for LOAD buffer",
+ _loadBufferSize);
+ }
+ }
+
+ // Check sequential order (original: DAT_00482c3c - sVar1 == -1)
+ if (_lastLoadChunkIdx + 1 != chunkIndex) {
+ debugC(DEBUG_SMUSH, "SmushPlayer::handleLoad: Non-sequential chunk %d (expected %d), skipping",
+ chunkIndex, _lastLoadChunkIdx + 1);
+ return;
+ }
+
+ // Check buffer capacity (original: DAT_00482c14 + param_2 < DAT_00482c10)
+ if (_loadBuffer == nullptr || _loadBufferOffset + dataSize >= _loadBufferSize) {
+ warning("SmushPlayer::handleLoad: Buffer overflow - offset=%d size=%d limit=%d",
+ _loadBufferOffset, dataSize, _loadBufferSize);
+ return;
+ }
+
+ // Copy data to buffer
+ b.read(_loadBuffer + _loadBufferOffset, dataSize);
+ _loadBufferOffset += dataSize;
+ _lastLoadChunkIdx = chunkIndex;
+
+ debugC(DEBUG_SMUSH, "SmushPlayer::handleLoad: Accumulated %d bytes total", _loadBufferOffset);
+
+ // Check if sequence is complete
+ if (chunkIndex == totalChunks - 1) {
+ debugC(DEBUG_SMUSH, "SmushPlayer::handleLoad: Sequence complete - %d chunks, %d bytes total",
+ totalChunks, _loadBufferOffset);
+ }
+}
+
void SmushPlayer::handleIACT(int32 subSize, Common::SeekableReadStream &b) {
debugC(DEBUG_SMUSH, "SmushPlayer::IACT()");
assert(subSize >= 8);
@@ -807,8 +946,11 @@ byte *SmushPlayer::getVideoPalette() {
void smushDecodeRLE(byte *dst, const byte *src, int left, int top, int width, int height, int pitch);
void smushDecodeUncompressed(byte *dst, const byte *src, int left, int top, int width, int height, int pitch);
+void smushDecodeLineUpdate(byte *dst, const byte *src, int left, int top, int width, int height, int pitch);
+void smushDecodeSkipRLE(byte *dst, const byte *src, int left, int top, int width, int height, int pitch);
+void smushDecodeRA2Bomp(byte *dst, const byte *src, int left, int top, int width, int height, int pitch, int dataSize);
-void SmushPlayer::decodeFrameObject(int codec, const uint8 *src, int left, int top, int width, int height) {
+void SmushPlayer::decodeFrameObject(int codec, const uint8 *src, int left, int top, int width, int height, int dataSize) {
if ((height == 242) && (width == 384)) {
if (_specialBuffer == 0)
_specialBuffer = (byte *)malloc(242 * 384);
@@ -819,13 +961,8 @@ void SmushPlayer::decodeFrameObject(int codec, const uint8 *src, int left, int t
if (_insane && _insane->shouldSkipFrameUpdate(left, top, width, height)) {
return; // Skip this frame update
}
-
- if (_specialBuffer == 0) {
- _specialBuffer = (byte *)malloc(width * height);
- _width = width;
- _height = height;
- }
- _dst = _specialBuffer;
+ // Rebel2 partial frames decode directly to main video buffer at (left, top)
+ // using screen pitch - don't use _specialBuffer
} else if ((height > _vm->_screenHeight) || (width > _vm->_screenWidth))
return;
// FT Insane uses smaller frames to draw overlays with moving objects
@@ -870,7 +1007,26 @@ void SmushPlayer::decodeFrameObject(int codec, const uint8 *src, int left, int t
// Used by Full Throttle Classic (from Remastered)
smushDecodeUncompressed(_dst, src, left, top, width, height, _vm->_screenWidth);
break;
+ case SMUSH_CODEC_LINE_UPDATE:
+ case SMUSH_CODEC_LINE_UPDATE2:
+ // RA2: Skip/copy with literal pixels (used for fonts and HUD overlays)
+ smushDecodeLineUpdate(_dst, src, left, top, width, height, pitch);
+ break;
+ case SMUSH_CODEC_SKIP_RLE:
+ // RA2: Skip/copy with embedded RLE (used for HUD frames with transparency)
+ smushDecodeSkipRLE(_dst, src, left, top, width, height, pitch);
+ break;
+ case SMUSH_CODEC_RA2_BOMP:
+ // RA2: BOMP RLE with variable header (used for small animation elements)
+ smushDecodeRA2Bomp(_dst, src, left, top, width, height, pitch, dataSize);
+ break;
default:
+ if (_vm->_game.id == GID_REBEL2) {
+ // Rebel Assault 2 may have other unknown codecs
+ debugC(DEBUG_SMUSH, "SmushPlayer::decodeFrameObject: Skipping unknown codec %d (left=%d, top=%d, %dx%d)",
+ codec, left, top, width, height);
+ break;
+ }
error("Invalid codec for frame object : %d", codec);
}
@@ -907,7 +1063,8 @@ void SmushPlayer::handleZlibFrameObject(int32 subSize, Common::SeekableReadStrea
int width = READ_LE_UINT16(ptr); ptr += 2;
int height = READ_LE_UINT16(ptr); ptr += 2;
- decodeFrameObject(codec, fobjBuffer + 14, left, top, width, height);
+ int fobjDataSize = (int)decompressedSize - 14;
+ decodeFrameObject(codec, fobjBuffer + 14, left, top, width, height, fobjDataSize);
free(fobjBuffer);
}
@@ -915,6 +1072,7 @@ void SmushPlayer::handleZlibFrameObject(int32 subSize, Common::SeekableReadStrea
void SmushPlayer::handleFrameObject(int32 subSize, Common::SeekableReadStream &b) {
assert(subSize >= 14);
if (_skipNext) {
+ debug("SmushPlayer::handleFrameObject: SKIPPING due to _skipNext, frame=%d", _frame);
_skipNext = false;
return;
}
@@ -928,12 +1086,15 @@ void SmushPlayer::handleFrameObject(int32 subSize, Common::SeekableReadStream &b
b.readUint16LE();
b.readUint16LE();
+ debug("SmushPlayer::handleFrameObject: frame=%d codec=%d pos=(%d,%d) size=%dx%d dataSize=%d",
+ _frame, codec, left, top, width, height, subSize - 14);
+
int32 chunk_size = subSize - 14;
byte *chunk_buffer = (byte *)malloc(chunk_size);
assert(chunk_buffer);
b.read(chunk_buffer, chunk_size);
- decodeFrameObject(codec, chunk_buffer, left, top, width, height);
+ decodeFrameObject(codec, chunk_buffer, left, top, width, height, chunk_size);
free(chunk_buffer);
}
@@ -944,13 +1105,19 @@ void SmushPlayer::handleFrame(int32 frameSize, Common::SeekableReadStream &b) {
_skipNext = false;
if (_insanity) {
- _vm->_insane->procPreRendering();
+ _vm->_insane->procPreRendering(_dst);
}
while (frameSize > 0) {
const uint32 subType = b.readUint32BE();
const int32 subSize = b.readUint32BE();
const int32 subOffset = b.pos();
+
+ // Debug: Log all chunk types for first few frames
+ if (_vm->_game.id == GID_REBEL2 && _frame < 5) {
+ debug("SmushPlayer::handleFrame: frame=%d chunk=%s size=%d", _frame, tag2str(subType), subSize);
+ }
+
switch (subType) {
case MKTAG('N','P','A','L'):
handleNewPalette(subSize, b);
@@ -1000,6 +1167,9 @@ void SmushPlayer::handleFrame(int32 frameSize, Common::SeekableReadStream &b) {
case MKTAG('T','E','X','T'):
handleTextResource(subType, subSize, b);
break;
+ case MKTAG('L','O','A','D'):
+ handleLoad(subSize, b);
+ break;
default:
error("Unknown frame subChunk found : %s, %d", tag2str(subType), subSize);
}
@@ -1484,6 +1654,7 @@ void SmushPlayer::play(const char *filename, int32 speed, int32 offset, int32 st
if (_endOfFile)
break;
+
if (_vm->shouldQuit() || _vm->_saveLoadFlag || _vm->_smushVideoShouldFinish) {
_vm->_mixer->stopHandle(*_compressedFileSoundHandle);
_vm->_mixer->stopHandle(*_IACTchannel);
@@ -1492,6 +1663,7 @@ void SmushPlayer::play(const char *filename, int32 speed, int32 offset, int32 st
resetAudioTracks(); // For DIG demo
if (_imuseDigital)
_imuseDigital->stopSMUSHAudio(); // For DIG & COMI
+
break;
}
diff --git a/engines/scumm/smush/smush_player.h b/engines/scumm/smush/smush_player.h
index b4d236abf09..494ed2d8372 100644
--- a/engines/scumm/smush/smush_player.h
+++ b/engines/scumm/smush/smush_player.h
@@ -78,7 +78,11 @@ namespace Scumm {
#define SMUSH_CODEC_RLE 1
#define SMUSH_CODEC_RLE_ALT 3
#define SMUSH_CODEC_UNCOMPRESSED 20
+#define SMUSH_CODEC_LINE_UPDATE 21 // RA2: Skip/copy with literal pixels
+#define SMUSH_CODEC_SKIP_RLE 23 // RA2: Skip/copy with embedded RLE
#define SMUSH_CODEC_DELTA_BLOCKS 37
+#define SMUSH_CODEC_LINE_UPDATE2 44 // RA2: Variant of codec 21
+#define SMUSH_CODEC_RA2_BOMP 45 // RA2: BOMP RLE with variable header
#define SMUSH_CODEC_DELTA_GLYPHS 47
class ScummEngine_v7;
@@ -151,6 +155,7 @@ private:
uint32 _seekFrame;
bool _skipNext;
+ bool _ra2FastForwarding; // Fast-forwarding RA2 BEG video to establish background
uint32 _frame;
Audio::SoundHandle *_IACTchannel;
@@ -249,7 +254,7 @@ private:
void tryCmpFile(const char *filename);
bool readString(const char *file);
- void decodeFrameObject(int codec, const uint8 *src, int left, int top, int width, int height);
+ void decodeFrameObject(int codec, const uint8 *src, int left, int top, int width, int height, int dataSize = 0);
void handleAnimHeader(int32 subSize, Common::SeekableReadStream &);
void handleFrame(int32 frameSize, Common::SeekableReadStream &);
void handleNewPalette(int32 subSize, Common::SeekableReadStream &);
@@ -261,8 +266,17 @@ private:
void handleIACT(int32 subSize, Common::SeekableReadStream &);
void handleTextResource(uint32 subType, int32 subSize, Common::SeekableReadStream &);
void handleDeltaPalette(int32 subSize, Common::SeekableReadStream &);
+ void handleLoad(int32 subSize, Common::SeekableReadStream &);
void readPalette(byte *, Common::SeekableReadStream &);
+ // LOAD chunk streaming buffer (RA2 - embedded resource data)
+ byte *_loadBuffer; // Accumulated LOAD data
+ int32 _loadBufferSize; // Allocated buffer size
+ int32 _loadBufferOffset; // Current write position (how much data accumulated)
+ int32 _loadReadOffset; // Current read position (for streaming consumption)
+ int16 _lastLoadChunkIdx; // Last processed chunk index (-1 = none)
+ int16 _totalLoadChunks; // Total chunks expected in current sequence
+
void initAudio(int samplerate, int32 maxChunkSize);
void terminateAudio();
int isChanActive(int flagId);
Commit: 0a05f1f519d47b377294345188891a45ae4d8da6
https://github.com/scummvm/scummvm/commit/0a05f1f519d47b377294345188891a45ae4d8da6
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:08:55+02:00
Commit Message:
SCUMM: RA2: Fix missing handler 1 crash
Changed paths:
engines/scumm/smush/smush_player.cpp
engines/scumm/smush/smush_player.h
diff --git a/engines/scumm/smush/smush_player.cpp b/engines/scumm/smush/smush_player.cpp
index 7bd1b78ccfb..efb7d20abc3 100644
--- a/engines/scumm/smush/smush_player.cpp
+++ b/engines/scumm/smush/smush_player.cpp
@@ -254,6 +254,7 @@ SmushPlayer::SmushPlayer(ScummEngine_v7 *scumm, IMuseDigital *imuseDigital, Insa
_base = nullptr;
_frameBuffer = nullptr;
_specialBuffer = nullptr;
+ _specialBufferSize = 0;
_seekPos = -1;
@@ -952,8 +953,12 @@ void smushDecodeRA2Bomp(byte *dst, const byte *src, int left, int top, int width
void SmushPlayer::decodeFrameObject(int codec, const uint8 *src, int left, int top, int width, int height, int dataSize) {
if ((height == 242) && (width == 384)) {
- if (_specialBuffer == 0)
- _specialBuffer = (byte *)malloc(242 * 384);
+ int bufSize = 242 * 384;
+ if (_specialBuffer == nullptr || bufSize > _specialBufferSize) {
+ free(_specialBuffer);
+ _specialBuffer = (byte *)malloc(bufSize);
+ _specialBufferSize = bufSize;
+ }
_dst = _specialBuffer;
} else if (_vm->_game.id == GID_REBEL2 && ((height != _vm->_screenHeight) || (width != _vm->_screenWidth))) {
// For Rebel2, check if this partial update overlaps with a destroyed enemy
@@ -961,8 +966,18 @@ void SmushPlayer::decodeFrameObject(int codec, const uint8 *src, int left, int t
if (_insane && _insane->shouldSkipFrameUpdate(left, top, width, height)) {
return; // Skip this frame update
}
- // Rebel2 partial frames decode directly to main video buffer at (left, top)
- // using screen pitch - don't use _specialBuffer
+ // Rebel2 videos use frames larger than the screen (e.g., 424x260 vs 320x200).
+ // Allocate a special buffer to hold the full frame - this is needed because
+ // codecs like 37/47 write the full frame size regardless of screen size.
+ int bufSize = width * height;
+ if (_specialBuffer == nullptr || bufSize > _specialBufferSize) {
+ free(_specialBuffer);
+ _specialBuffer = (byte *)malloc(bufSize);
+ _specialBufferSize = bufSize;
+ _width = width;
+ _height = height;
+ }
+ _dst = _specialBuffer;
} else if ((height > _vm->_screenHeight) || (width > _vm->_screenWidth))
return;
// FT Insane uses smaller frames to draw overlays with moving objects
@@ -975,8 +990,9 @@ void SmushPlayer::decodeFrameObject(int codec, const uint8 *src, int left, int t
_width = width;
_height = height;
} else if (_vm->_game.id == GID_REBEL2 && ((height != _vm->_screenHeight) || (width != _vm->_screenWidth))) {
- // Do not update _width/_height here to preserve original video size
- // if frames are partial updates
+ // Do not update _width/_height here - preserve original video size set during
+ // buffer allocation. Small overlay sprites (e.g., 8x7) need to use the background
+ // frame's pitch (424) to be drawn at the correct position in the buffer.
} else {
_width = _vm->_screenWidth;
_height = _vm->_screenHeight;
diff --git a/engines/scumm/smush/smush_player.h b/engines/scumm/smush/smush_player.h
index 494ed2d8372..2ab2759cc18 100644
--- a/engines/scumm/smush/smush_player.h
+++ b/engines/scumm/smush/smush_player.h
@@ -147,6 +147,7 @@ private:
uint32 _baseSize;
byte *_frameBuffer;
byte *_specialBuffer;
+ int _specialBufferSize;
Common::String _seekFile;
uint32 _startFrame;
Commit: 54610c886d3658318dffbfcd0a63616132ff11bc
https://github.com/scummvm/scummvm/commit/54610c886d3658318dffbfcd0a63616132ff11bc
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:08:56+02:00
Commit Message:
SCUMM: RA2: Refactor level object handling
Changed paths:
engines/scumm/insane/insane_rebel.cpp
engines/scumm/insane/insane_rebel.h
engines/scumm/smush/smush_player.cpp
diff --git a/engines/scumm/insane/insane_rebel.cpp b/engines/scumm/insane/insane_rebel.cpp
index 5e9ca23ecc5..cf98a73f119 100644
--- a/engines/scumm/insane/insane_rebel.cpp
+++ b/engines/scumm/insane/insane_rebel.cpp
@@ -3172,14 +3172,65 @@ void InsaneRebel2::procPostRendering(byte *renderBitmap, int32 codecparam, int32
// background persists. FOBJ sprites (enemies) are then decoded on top by SMUSH.
// We do NOT redraw the background here as that would overwrite FOBJ content.
- // ============================================================
- // STEP 0: Fill status bar background (FUN_004288c0 equivalent)
- // ============================================================
- // Clear the status bar area at Y=180-199 with background color
+ // --- HUD Drawing Order (from FUN_004089ab assembly analysis) ---
+ // 1. FUN_004288c0: Fill status bar background at Y=0xb4 (180)
+ // 2. FUN_004089ab: Draw turret overlays, targeting reticle, crosshair
+ // 3. FUN_0041c012: Draw status bar sprites LAST (on top)
+
+ // STEP 0: Fill status bar background (FUN_004288c0)
+ renderStatusBarBackground(renderBitmap, pitch, width, height, videoWidth, videoHeight, statusBarY);
+
+ // STEP 1A: Draw NUT-based HUD overlays for Handler 0x26/0x19 (FUN_004089ab lines 195-226)
+ renderTurretHudOverlays(renderBitmap, pitch, width, height, curFrame);
+
+ // STEP 1B: Draw embedded SAN HUD overlays (from IACT chunks)
+ renderEmbeddedHudOverlays(renderBitmap, pitch, width, height);
+
+ // STEP 2: Draw DISPFONT.NUT status bar sprites (FUN_0041c012)
+ renderStatusBarSprites(renderBitmap, pitch, width, height, statusBarY, curFrame);
+
+ // Ship rendering (FUN_00401ccf for Handler 8, FUN_0040d836 for Handler 7)
+ debug("Rebel2 Ship Check: handler=%d shipSprite=%p flyShipSprite=%p shipLevelMode=%d numSprites=%d/%d",
+ _rebelHandler, (void*)_shipSprite, (void*)_flyShipSprite, _shipLevelMode,
+ _shipSprite ? _shipSprite->getNumChars() : 0,
+ _flyShipSprite ? _flyShipSprite->getNumChars() : 0);
+
+ renderHandler7Ship(renderBitmap, pitch, width, height);
+ renderHandler8Ship(renderBitmap, pitch, width, height);
+ renderFallbackShip(renderBitmap, pitch, width, height);
+
+ // Enemy indicators and destroyed enemy area erase
+ renderEnemyOverlays(renderBitmap, pitch, width, height, videoWidth);
+
+ // Explosion animations (FUN_409FBC)
+ renderExplosions(renderBitmap, pitch, width, height);
+
+ // Laser shot beams and impacts
+ renderLaserShots(renderBitmap, pitch, width, height);
+
+ // Collision zone visualization (debug - for Handler 7/8 pilot modes)
+ if (_rebelHandler == 7 || _rebelHandler == 8) {
+ drawCollisionZones(renderBitmap, pitch, width, height, 0);
+ }
+
+ // Crosshair/reticle (FUN_004089ab, FUN_0040d836)
+ renderCrosshair(renderBitmap, pitch, width, height);
+
+ // HUD score/lives rendering (FUN_0041c012)
+ renderScoreHUD(renderBitmap, pitch, width, height, 0);
+
+ // Frame end cleanup: reset enemy active flags and collision zones (FUN_403240)
+ frameEndCleanup();
+}
+
+// ======================= Rendering Helper Functions =======================
+// These are extracted from procPostRendering for better readability
+
+void InsaneRebel2::renderStatusBarBackground(byte *renderBitmap, int pitch, int width, int height,
+ int videoWidth, int videoHeight, int statusBarY) {
+ // Fill status bar background (FUN_004288c0 equivalent)
// Original assembly: FUN_004288c0(local_8, 0, 0, 0xb4, 0x140, 0x14, 4)
// This fills width=320, height=20 starting at Y=180 with color index 4
- // Status bar is ALWAYS shown during gameplay (_rebelHandler != 0)
- // Hidden during cinematics/intros when _rebelHandler == 0 (handled by early return above)
const byte statusBarBgColor = 4;
for (int y = statusBarY; y < videoHeight; y++) {
@@ -3191,795 +3242,607 @@ void InsaneRebel2::procPostRendering(byte *renderBitmap, int32 codecparam, int32
renderBitmap[destY * pitch + destX] = statusBarBgColor;
}
}
-
- // ============================================================
- // STEP 1A: Draw NUT-based HUD overlays for Handler 0x26/0x19 (turret modes)
- // ============================================================
- // For turret handlers, the HUD overlay is loaded as a NUT file via IACT opcode 8
- // and contains animated frames for blinking cockpit lights.
- //
+}
+
+void InsaneRebel2::renderTurretHudOverlays(byte *renderBitmap, int pitch, int width, int height, int32 curFrame) {
+ // Draw NUT-based HUD overlays for Handler 0x26/0x19 (turret modes)
// From FUN_004089ab disassembly (lines 195-226):
// - DAT_0047fe78 (_hudOverlayNut): Primary HUD overlay with 6 animation frames
// - Position formula (low-res):
// X = 160 + (mouseOffsetX >> 4) - (width / 2) - spriteOffsetX
// Y = 182 - (mouseOffsetY >> 4) - height - spriteOffsetY
// - Animation: spriteIndex = (frameCounter / 2) % 6
- //
- // The mouse offset creates a subtle parallax effect as the view pans.
- if ((_rebelHandler == 0x26 || _rebelHandler == 0x19) && _hudOverlayNut && _hudOverlayNut->getNumChars() > 0) {
- // Calculate mouse offset (clamped to -127..127)
- // These match DAT_0047a7e0/DAT_0047a7e2 in the original
- int mouseOffsetX = (_vm->_mouse.x - 160); // Relative to screen center
- int mouseOffsetY = (_vm->_mouse.y - 100); // Relative to screen center
- if (mouseOffsetX > 127) mouseOffsetX = 127;
- if (mouseOffsetX < -127) mouseOffsetX = -127;
- if (mouseOffsetY > 127) mouseOffsetY = 127;
- if (mouseOffsetY < -127) mouseOffsetY = -127;
-
- // Animation frame cycling: (frameCounter / 2) % 6
- // This creates a 12-frame cycle (each sprite shown for 2 video frames)
- int numSprites = _hudOverlayNut->getNumChars();
- int animFrameCount = MIN(numSprites, 6); // Use up to 6 frames for animation
- int animFrame = 0;
- if (animFrameCount > 0) {
- animFrame = (curFrame / 2) % animFrameCount;
- }
-
- // Get sprite dimensions and offset
- // NUT format stores sprite offsets that affect positioning
- int spriteW = _hudOverlayNut->getCharWidth(animFrame);
- int spriteH = _hudOverlayNut->getCharHeight(animFrame);
-
- // Position calculation from assembly (low-res mode):
- // X = 0xa0 (160) + (mouseOffsetX >> 4) - (width / 2) - offsetX
- // Y = 0xb6 (182) - (mouseOffsetY >> 4) - height - offsetY
- // Note: The sprite offset is embedded in the NUT data; for simplicity we use 0
- int spriteOffsetX = 0; // Could be extracted from NUT if needed
- int spriteOffsetY = 0;
-
- int hudX = 160 + (mouseOffsetX >> 4) - (spriteW / 2) - spriteOffsetX;
- int hudY = 182 - (mouseOffsetY >> 4) - spriteH - spriteOffsetY;
-
- // Apply view offset for scrolling background
- hudX += _viewX;
- hudY += _viewY;
-
- // Draw the HUD overlay:
- // From FUN_004089ab: Sprite 0 is ALWAYS drawn first (base cockpit)
- // Then if animation frame != 0, draw the animation frame on top (blinking lights)
- renderNutSprite(renderBitmap, pitch, width, height, hudX, hudY, _hudOverlayNut, 0);
-
- // Draw animation overlay frame if not frame 0
- // Animation frames 1-5 contain the blinking light states
- if (animFrame != 0 && animFrame < numSprites) {
- renderNutSprite(renderBitmap, pitch, width, height, hudX, hudY, _hudOverlayNut, animFrame);
- }
-
- debug(5, "Rebel2 HUD: Drawing NUT overlay frame %d/%d at (%d,%d) mouseOffset=(%d,%d)",
- animFrame, numSprites, hudX, hudY, mouseOffsetX, mouseOffsetY);
+
+ if ((_rebelHandler != 0x26 && _rebelHandler != 0x19) || !_hudOverlayNut || _hudOverlayNut->getNumChars() <= 0)
+ return;
+
+ // Calculate mouse offset (clamped to -127..127)
+ int mouseOffsetX = (_vm->_mouse.x - 160);
+ int mouseOffsetY = (_vm->_mouse.y - 100);
+ if (mouseOffsetX > 127) mouseOffsetX = 127;
+ if (mouseOffsetX < -127) mouseOffsetX = -127;
+ if (mouseOffsetY > 127) mouseOffsetY = 127;
+ if (mouseOffsetY < -127) mouseOffsetY = -127;
+
+ // Animation frame cycling: (frameCounter / 2) % 6
+ int numSprites = _hudOverlayNut->getNumChars();
+ int animFrameCount = MIN(numSprites, 6);
+ int animFrame = 0;
+ if (animFrameCount > 0) {
+ animFrame = (curFrame / 2) % animFrameCount;
}
- // Draw secondary HUD overlay if present (DAT_0047fe80)
- if ((_rebelHandler == 0x26 || _rebelHandler == 0x19) && _hudOverlay2Nut && _hudOverlay2Nut->getNumChars() > 0) {
- // Similar positioning to primary overlay
- int mouseOffsetX = (_vm->_mouse.x - 160);
- int mouseOffsetY = (_vm->_mouse.y - 100);
- if (mouseOffsetX > 127) mouseOffsetX = 127;
- if (mouseOffsetX < -127) mouseOffsetX = -127;
- if (mouseOffsetY > 127) mouseOffsetY = 127;
- if (mouseOffsetY < -127) mouseOffsetY = -127;
+ // Get sprite dimensions
+ int spriteW = _hudOverlayNut->getCharWidth(animFrame);
+ int spriteH = _hudOverlayNut->getCharHeight(animFrame);
- int spriteW = _hudOverlay2Nut->getCharWidth(0);
- int spriteH = _hudOverlay2Nut->getCharHeight(0);
+ // Position calculation from assembly (low-res mode)
+ int spriteOffsetX = 0;
+ int spriteOffsetY = 0;
+ int hudX = 160 + (mouseOffsetX >> 4) - (spriteW / 2) - spriteOffsetX;
+ int hudY = 182 - (mouseOffsetY >> 4) - spriteH - spriteOffsetY;
- int hudX = 160 + (mouseOffsetX >> 4) - (spriteW / 2);
- int hudY = 182 - (mouseOffsetY >> 4) - spriteH;
+ // Apply view offset for scrolling background
+ hudX += _viewX;
+ hudY += _viewY;
- hudX += _viewX;
- hudY += _viewY;
+ // Draw base cockpit (sprite 0 always drawn first)
+ renderNutSprite(renderBitmap, pitch, width, height, hudX, hudY, _hudOverlayNut, 0);
- renderNutSprite(renderBitmap, pitch, width, height, hudX, hudY, _hudOverlay2Nut, 0);
+ // Draw animation overlay frame if not frame 0
+ if (animFrame != 0 && animFrame < numSprites) {
+ renderNutSprite(renderBitmap, pitch, width, height, hudX, hudY, _hudOverlayNut, animFrame);
}
- // ============================================================
- // STEP 1B: Draw embedded SAN HUD overlays (from IACT chunks)
- // ============================================================
+ debug(5, "Rebel2 HUD: Drawing NUT overlay frame %d/%d at (%d,%d) mouseOffset=(%d,%d)",
+ animFrame, numSprites, hudX, hudY, mouseOffsetX, mouseOffsetY);
+
+ // Draw secondary HUD overlay if present (DAT_0047fe80)
+ if (_hudOverlay2Nut && _hudOverlay2Nut->getNumChars() > 0) {
+ int spr2W = _hudOverlay2Nut->getCharWidth(0);
+ int spr2H = _hudOverlay2Nut->getCharHeight(0);
+ int hud2X = 160 + (mouseOffsetX >> 4) - (spr2W / 2) + _viewX;
+ int hud2Y = 182 - (mouseOffsetY >> 4) - spr2H + _viewY;
+ renderNutSprite(renderBitmap, pitch, width, height, hud2X, hud2Y, _hudOverlay2Nut, 0);
+ }
+}
+
+void InsaneRebel2::renderEmbeddedHudOverlays(byte *renderBitmap, int pitch, int width, int height) {
+ // Draw embedded SAN HUD overlays (from IACT chunks)
// For Handler 7 (Level 3): HUD elements are scattered across the screen
- // - Each frame has its own renderX/renderY position from FOBJ left/top
- // - userId 1-11 are different HUD elements
- // - userIds at the same position are direction variants (select one based on direction)
// For turret handlers: slots 1-2 form a two-part cockpit overlay
- // - userId 1: Left piece at X=0
- // - userId 2: Right piece at X=slot1Width
- // These are drawn BEFORE the status bar so status bar appears on top
-
- // For Handler 7 ship direction: identify ship frames at same position
- // Level 3 has userId 1 and 2 at position (162, 105) - these are direction variants
- // We select one based on the current direction to simulate ship turning
for (int hudSlot = 1; hudSlot < 16; hudSlot++) {
EmbeddedSanFrame &frame = _rebelEmbeddedHud[hudSlot];
- if (frame.valid && frame.pixels && frame.width > 0 && frame.height > 0) {
- // Skip frames at position (0,0) with small dimensions - these are likely animation patches
- // that need special handling (userId=3: 11x26, userId=4: 17x53 in Level 1)
- // TODO: Investigate how these are used in the original assembly
- if (frame.renderX == 0 && frame.renderY == 0 && frame.width < 50 && frame.height < 60) {
- debug(3, "Rebel2: Skipping small embedded frame at (0,0): slot=%d size=%dx%d",
- hudSlot, frame.width, frame.height);
- continue;
- }
+ if (!frame.valid || !frame.pixels || frame.width <= 0 || frame.height <= 0)
+ continue;
- int destX, destY;
+ // Skip small frames at (0,0) - likely animation patches
+ if (frame.renderX == 0 && frame.renderY == 0 && frame.width < 50 && frame.height < 60) {
+ debug(3, "Rebel2: Skipping small embedded frame at (0,0): slot=%d size=%dx%d",
+ hudSlot, frame.width, frame.height);
+ continue;
+ }
- // For Handler 7: Check if this is a ship direction frame
- // Ship frames are at the same position - skip all but the selected one
- if (_rebelHandler == 7) {
- // Collect all frame IDs in this position group (may not be consecutive)
- int groupMembers[16];
- int groupCount = 0;
+ // For Handler 7: handle direction-based frame selection
+ if (_rebelHandler == 7) {
+ int groupMembers[16];
+ int groupCount = 0;
- for (int id = 1; id < 16; id++) {
- EmbeddedSanFrame &g = _rebelEmbeddedHud[id];
- if (g.valid && g.renderX == frame.renderX && g.renderY == frame.renderY &&
- g.width == frame.width && g.height == frame.height) {
- groupMembers[groupCount++] = id;
- }
+ for (int id = 1; id < 16; id++) {
+ EmbeddedSanFrame &g = _rebelEmbeddedHud[id];
+ if (g.valid && g.renderX == frame.renderX && g.renderY == frame.renderY &&
+ g.width == frame.width && g.height == frame.height) {
+ groupMembers[groupCount++] = id;
}
+ }
- // If there's more than one frame in this group, select based on direction
- if (groupCount > 1) {
- // Map direction index (0-34) to group index (0 to groupCount-1)
- int selectedOffset = _shipDirectionIndex % groupCount;
- int selectedId = groupMembers[selectedOffset];
-
- // Check if selected frame has pixels, if not find one that does
- EmbeddedSanFrame &selectedFrame = _rebelEmbeddedHud[selectedId];
- int nonZero = 0;
- for (int i = 0; i < selectedFrame.width * selectedFrame.height; i++) {
- if (selectedFrame.pixels[i] != 0) nonZero++;
- }
+ if (groupCount > 1) {
+ int selectedOffset = _shipDirectionIndex % groupCount;
+ int selectedId = groupMembers[selectedOffset];
- if (nonZero == 0) {
- // Selected frame is empty, find another with pixels
- for (int i = 0; i < groupCount; i++) {
- EmbeddedSanFrame &altFrame = _rebelEmbeddedHud[groupMembers[i]];
- int altNonZero = 0;
- for (int j = 0; j < altFrame.width * altFrame.height; j++) {
- if (altFrame.pixels[j] != 0) altNonZero++;
- }
- if (altNonZero > 0) {
- selectedId = groupMembers[i];
- break;
- }
- }
- }
+ // Verify selected frame has pixels
+ EmbeddedSanFrame &selectedFrame = _rebelEmbeddedHud[selectedId];
+ int nonZero = 0;
+ for (int i = 0; i < selectedFrame.width * selectedFrame.height; i++) {
+ if (selectedFrame.pixels[i] != 0) nonZero++;
+ }
- // Only render if this is the selected frame
- if (hudSlot != selectedId) {
- continue; // Skip this frame, render the selected one instead
+ if (nonZero == 0) {
+ for (int i = 0; i < groupCount; i++) {
+ EmbeddedSanFrame &altFrame = _rebelEmbeddedHud[groupMembers[i]];
+ int altNonZero = 0;
+ for (int j = 0; j < altFrame.width * altFrame.height; j++) {
+ if (altFrame.pixels[j] != 0) altNonZero++;
+ }
+ if (altNonZero > 0) {
+ selectedId = groupMembers[i];
+ break;
+ }
}
}
- }
- // Use the stored render position from the embedded ANIM data
- // The renderX/renderY are set from FOBJ's left/top values
- destX = frame.renderX;
- destY = frame.renderY;
-
- // For Handler 0x26 (turret) and 0x19 (mixed): Embedded ANIM frame positioning
- // From FUN_00407fcb assembly lines 389-391: screen dimensions are 320x200 (0x140 x 0xc8)
- // Unlike NUT overlays (which use 0xb6=182 base with mouse parallax), embedded ANIM
- // frames use screen height (200) as the base for bottom-aligned positioning.
- // Formula: X = 160 - width/2 - fobj_left, Y = 200 - height - fobj_top
- if ((_rebelHandler == 0x26 || _rebelHandler == 0x19) && (hudSlot == 1 || hudSlot == 2)) {
- // X: centered horizontally, adjusted by FOBJ left offset
- destX = 160 - frame.width / 2 - frame.renderX;
- // Y: screen height (0xc8=200) - height - fobj_top for bottom-aligned cockpit
- destY = 200 - frame.height - frame.renderY;
+ if (hudSlot != selectedId)
+ continue;
}
+ }
- // For Handler 7 (space flight): HUD cockpit overlays use FOBJ position directly
- // From FUN_0040d836: The clip region is 320x170 (0x140 x 0xaa)
- // Large cockpit frames (hudSlot 1/2) need centering like turret handler
- if (_rebelHandler == 7 && (hudSlot == 1 || hudSlot == 2) && frame.width > 100) {
- // Large cockpit frame - center horizontally, position at bottom
- destX = 160 - frame.width / 2 - frame.renderX;
- // Position above status bar area (170 is render height for Handler 7)
- destY = 170 - frame.height - frame.renderY;
- } else if (_rebelHandler == 7 && destX > 100 && destY > 50) {
- // Ship sprite in center of screen - apply position offset
- int16 offsetX = (_shipPosX - 160) / 8;
- int16 offsetY = (_shipPosY - 100) / 8;
- destX += offsetX;
- destY += offsetY;
- }
+ // Calculate destination position
+ int destX = frame.renderX;
+ int destY = frame.renderY;
- // Apply View Offset for all HUD elements
- destX += _viewX;
- destY += _viewY;
-
- // Debug: log embedded frame render dimensions
- debug(3, "Rebel2: Rendering embedded HUD slot=%d size=%dx%d at (%d,%d) screen=%dx%d",
- hudSlot, frame.width, frame.height, destX, destY, pitch, height);
-
- // Draw frame with transparency (pixel 0 and 231 = transparent, matching NUT rendering)
- for (int y = 0; y < frame.height && (destY + y) < height; y++) {
- for (int x = 0; x < frame.width && (destX + x) < pitch; x++) {
- byte pixel = frame.pixels[y * frame.width + x];
- if (pixel != 0 && pixel != 231) { // Skip transparent pixels (0 and 231/0xE7)
- int fx = destX + x;
- int fy = destY + y;
- if (fx >= 0 && fy >= 0) {
- renderBitmap[fy * pitch + fx] = pixel;
- }
+ // Handler 0x26/0x19 turret positioning
+ if ((_rebelHandler == 0x26 || _rebelHandler == 0x19) && (hudSlot == 1 || hudSlot == 2)) {
+ destX = 160 - frame.width / 2 - frame.renderX;
+ destY = 200 - frame.height - frame.renderY;
+ }
+
+ // Handler 7 large cockpit frame positioning
+ if (_rebelHandler == 7 && (hudSlot == 1 || hudSlot == 2) && frame.width > 100) {
+ destX = 160 - frame.width / 2 - frame.renderX;
+ destY = 170 - frame.height - frame.renderY;
+ } else if (_rebelHandler == 7 && destX > 100 && destY > 50) {
+ int16 offsetX = (_shipPosX - 160) / 8;
+ int16 offsetY = (_shipPosY - 100) / 8;
+ destX += offsetX;
+ destY += offsetY;
+ }
+
+ destX += _viewX;
+ destY += _viewY;
+
+ debug(3, "Rebel2: Rendering embedded HUD slot=%d size=%dx%d at (%d,%d)",
+ hudSlot, frame.width, frame.height, destX, destY);
+
+ // Draw frame with transparency (pixel 0 and 231 = transparent)
+ for (int y = 0; y < frame.height && (destY + y) < height; y++) {
+ for (int x = 0; x < frame.width && (destX + x) < pitch; x++) {
+ byte pixel = frame.pixels[y * frame.width + x];
+ if (pixel != 0 && pixel != 231) {
+ int fx = destX + x;
+ int fy = destY + y;
+ if (fx >= 0 && fy >= 0) {
+ renderBitmap[fy * pitch + fx] = pixel;
}
}
}
}
}
-
- // ============================================================
- // STEP 2: Draw DISPFONT.NUT status bar sprites (FUN_0041c012 equivalent)
- // ============================================================
- // DISPFONT.NUT contains status bar elements - drawn ON TOP of cockpit overlay
- // From assembly FUN_0041c012:
- // - Sprite 1: Status bar background frame (full width, drawn at 0,0)
- // - Sprites 2-5: Difficulty stars (1-4 stars, drawn at 0,0)
- // - Sprite 6: Damage bar fill (drawn with clip rect X=0x3f, Y=9, W=64, H=6)
- // - Sprite 7: Damage alert (flashing red when damage critical)
- // Status bar is ALWAYS drawn during gameplay (_rebelHandler != 0)
- if (_smush_cockpitNut) {
- // Draw status bar background frame (sprite 1) at (0, statusBarY)
- // This sprite is the full-width status bar background
- if (_smush_cockpitNut->getNumChars() > 1) {
- renderNutSprite(renderBitmap, pitch, width, height, _viewX, statusBarY + _viewY, _smush_cockpitNut, 1);
- }
-
- // Draw difficulty indicator (sprites 2-5 based on difficulty level 0-3)
- // Sprite index = difficulty + 2; capped at 4 max difficulty (sprite 5)
- // Assembly draws at (0,0) in buffer - same position as sprite 1
- int difficulty = 0; // TODO: Read from game state (DAT_0047a7fa)
- if (difficulty > 3) difficulty = 3;
- int difficultySprite = difficulty + 2; // sprites 2, 3, 4, or 5
- if (_smush_cockpitNut->getNumChars() > difficultySprite) {
- renderNutSprite(renderBitmap, pitch, width, height, _viewX, statusBarY + _viewY, _smush_cockpitNut, difficultySprite);
- }
-
-// Draw damage bar (sprite 6)
- // Assembly uses clip rect: X=0x3f(63), Y=0x9(9), W=0x40(64), H=0x6(6)
- // The width is scaled based on accumulated damage value (0..255)
- // For now, draw at position (0, statusBarY) - sprite has internal positioning
- if (_smush_cockpitNut->getNumChars() > 6) {
- // Calculate width based on damage value.
- // Damage range: 0..255 where 255 = full (Width 64)
- int drawWidth = (64 * _playerDamage) / 255;
- if (drawWidth < 0) drawWidth = 0;
- if (drawWidth > 64) drawWidth = 64;
-
- // We need to draw a partial sprite or use a clip rect.
- // smlayer_drawSomething supports scaling/clip?
- // The current implementation of smlayer_drawSomething just draws the whole sprite.
- // We can pass a "frame" or clip rect if we modify the function or use a lower level draw.
- // For now, let's just draw the full sprite if damage < 255, to verify it appears.
- // Ideally we should implement clipping.
-
- // NOTE: smlayer_drawSomething calls `_smush_cockpitNut->draw(...)`
- // We can't easily clip without modifying NutRenderer or using a custom draw loop.
- // Let's implement a custom draw loop for the damage bar here since it's simple.
-
- // Sprite 6 data (we need to copy a CLIP rect from within this sprite)
- const byte *src = _smush_cockpitNut->getCharData(6);
- int sw = _smush_cockpitNut->getCharWidth(6);
- int sh = _smush_cockpitNut->getCharHeight(6);
- // Clip rect inside the sprite (from assembly): X=63, Y=9, W=64, H=6
- const int clipX = 63;
- const int clipY = 9;
- const int clipW = 64;
- const int clipH = 6;
-
- // Draw clipped width
- if (src && sw > 0 && sh > 0) {
- // Clamp drawWidth to the clip width and ensure we don't read past sprite bounds
- int maxClipW = sw - clipX;
- if (maxClipW < 0) maxClipW = 0;
- int drawW = drawWidth;
- if (drawW > clipW) drawW = clipW;
- if (drawW > maxClipW) drawW = maxClipW;
- if (drawW > 0) {
- int drawH = clipH;
- if (drawH > (sh - clipY)) drawH = sh - clipY;
- if (drawH < 0) drawH = 0;
- for (int y = 0; y < drawH; y++) {
- for (int x = 0; x < drawW; x++) {
- // Render to (clipX + x + viewX, statusBarY + clipY + y + viewY)
- int destX = clipX + x + _viewX;
- int destY = statusBarY + clipY + y + _viewY;
- if (destX >= 0 && destX < pitch && destY >= 0 && destY < height) {
- byte pixel = src[(clipY + y) * sw + (clipX + x)];
- if (pixel != 0) {
- renderBitmap[destY * pitch + destX] = pixel;
- }
- }
+}
+
+void InsaneRebel2::renderStatusBarSprites(byte *renderBitmap, int pitch, int width, int height,
+ int statusBarY, int32 curFrame) {
+ // Draw DISPFONT.NUT status bar sprites (FUN_0041c012 equivalent)
+ // DISPFONT.NUT contains:
+ // Sprite 1: Status bar background frame
+ // Sprites 2-5: Difficulty stars (1-4)
+ // Sprite 6: Damage bar fill (with clip rect X=63, Y=9, W=64, H=6)
+ // Sprite 7: Damage alert (flashing red when critical)
+
+ if (!_smush_cockpitNut)
+ return;
+
+ // Sprite 1: Status bar background
+ if (_smush_cockpitNut->getNumChars() > 1) {
+ renderNutSprite(renderBitmap, pitch, width, height, _viewX, statusBarY + _viewY, _smush_cockpitNut, 1);
+ }
+
+ // Difficulty indicator (sprites 2-5)
+ int difficulty = 0; // TODO: Read from game state
+ if (difficulty > 3) difficulty = 3;
+ int difficultySprite = difficulty + 2;
+ if (_smush_cockpitNut->getNumChars() > difficultySprite) {
+ renderNutSprite(renderBitmap, pitch, width, height, _viewX, statusBarY + _viewY, _smush_cockpitNut, difficultySprite);
+ }
+
+ // Damage bar (sprite 6) with clipped width
+ if (_smush_cockpitNut->getNumChars() > 6) {
+ int drawWidth = (64 * _playerDamage) / 255;
+ if (drawWidth < 0) drawWidth = 0;
+ if (drawWidth > 64) drawWidth = 64;
+
+ const byte *src = _smush_cockpitNut->getCharData(6);
+ int sw = _smush_cockpitNut->getCharWidth(6);
+ int sh = _smush_cockpitNut->getCharHeight(6);
+
+ // Clip rect inside sprite: X=63, Y=9, W=64, H=6
+ const int clipX = 63, clipY = 9, clipW = 64, clipH = 6;
+
+ if (src && sw > 0 && sh > 0) {
+ int maxClipW = sw - clipX;
+ if (maxClipW < 0) maxClipW = 0;
+ int drawW = MIN(drawWidth, MIN(clipW, maxClipW));
+ int drawH = MIN(clipH, sh - clipY);
+ if (drawH < 0) drawH = 0;
+
+ for (int y = 0; y < drawH; y++) {
+ for (int x = 0; x < drawW; x++) {
+ int destX = clipX + x + _viewX;
+ int destY = statusBarY + clipY + y + _viewY;
+ if (destX >= 0 && destX < pitch && destY >= 0 && destY < height) {
+ byte pixel = src[(clipY + y) * sw + (clipX + x)];
+ if (pixel != 0) {
+ renderBitmap[destY * pitch + destX] = pixel;
}
}
}
}
}
-
- // Draw damage alert overlay (sprite 7) when damage is critical (> 0xAA = 170)
- // Only draws when frame counter bit 3 is clear (every 8 frames)
- if (_smush_cockpitNut->getNumChars() > 7) {
- if (_playerDamage > 170 && ((curFrame & 8) == 0)) {
- // Draw overlay sprite 7 at same general region (sx, sy)
- renderNutSprite(renderBitmap, pitch, width, height, 63 + _viewX, statusBarY + 9 + _viewY, _smush_cockpitNut, 7);
- }
- }
-
- // Draw lives indicator - assembly shows at X=0xa8 (168), Y=7
- // Uses sprite 1 again with different clip rect
- // TODO: Implement lives rendering
-
- // Draw score - uses FUN_00434cb0 (text rendering) at X=0x101(257)
- // TODO: Implement score rendering
}
- // ============================================================
- // HANDLER 8 SHIP RENDERING (FUN_00401ccf equivalent)
- // ============================================================
- // For third-person vehicle missions (Handler 8), draw the player's ship sprite
- // The ship position is calculated from _shipPosX/_shipPosY
- //
- // From FUN_00401ccf disassembly (lines 87-95):
- // - Ship is drawn when DAT_0047e010 != NULL AND DAT_0043e000 != 5
- // - Position offset X: (DAT_0043e006 - 0xa0) >> 3 = (shipPosX - 160) >> 3
- // - Position offset Y: (DAT_0043e008 - 0x28) >> 2 = (shipPosY - 40) >> 2
- // - Sprite index: param_5 & 1 (0 = normal, 1 = firing)
- //
- // The crosshair is drawn at lines 128-135 with base position:
- // - Low-res: X = offset + 0xa0 (160), Y = offset + 0x69 (105)
- // - High-res: X = offset*2 + 0x140 (320), Y = offset*2 + 0xd2 (210)
- //
- // The ship sprite is drawn using the same offset calculation but passed directly
- // to FUN_004236e0. The rendering function treats these as screen coordinates.
- debug("Rebel2 Ship Check: handler=%d shipSprite=%p flyShipSprite=%p shipLevelMode=%d numSprites=%d/%d",
- _rebelHandler, (void*)_shipSprite, (void*)_flyShipSprite, _shipLevelMode,
- _shipSprite ? _shipSprite->getNumChars() : 0,
- _flyShipSprite ? _flyShipSprite->getNumChars() : 0);
+ // Damage alert overlay (sprite 7) when damage > 170 and flashing
+ if (_smush_cockpitNut->getNumChars() > 7) {
+ if (_playerDamage > 170 && ((curFrame & 8) == 0)) {
+ renderNutSprite(renderBitmap, pitch, width, height, 63 + _viewX, statusBarY + 9 + _viewY, _smush_cockpitNut, 7);
+ }
+ }
+}
+void InsaneRebel2::renderHandler7Ship(byte *renderBitmap, int pitch, int width, int height) {
// Handler 7 Ship Rendering (Space Flight - FLY sprites)
- // Handler 7 uses _flyShipSprite (FLY001) with 35 direction frames (5x7 grid)
- // Different from Handler 8's POV sprites
- if (_rebelHandler == 7 && _flyShipSprite && _shipLevelMode != 5) {
- // Handler 7 position calculation from FUN_0040d836 lines 173-175
- // Draw position: (transformedX - 0xd4, transformedY - 0x82)
- // where transformedX/Y come from FUN_0041c720 transformation
- // For simplicity, we calculate based on ship direction
-
- // Base position calculation (simplified from assembly)
- // The ship is drawn at a fixed position offset by direction
- int baseX = 160; // Screen center X
- int baseY = 105; // Screen center Y (0x69)
-
- // Add small offset based on ship position for "flight feel"
- int16 posOffsetX = (_flyShipScreenX - 160) / 10;
- int16 posOffsetY = (_flyShipScreenY - 100) / 10;
-
- int shipScreenX = baseX + posOffsetX;
- int shipScreenY = baseY + posOffsetY;
-
- int numSprites = _flyShipSprite->getNumChars();
- int spriteIndex = _shipDirectionIndex;
-
- // Validate sprite index
- if (spriteIndex < 0) spriteIndex = 0;
- if (spriteIndex >= numSprites) spriteIndex = numSprites - 1;
+ // Uses _flyShipSprite (FLY001) with 35 direction frames (5x7 grid)
- // Get sprite dimensions and center it
- int spriteW = _flyShipSprite->getCharWidth(spriteIndex);
- int spriteH = _flyShipSprite->getCharHeight(spriteIndex);
- int drawX = shipScreenX - spriteW / 2 + _viewX;
- int drawY = shipScreenY - spriteH / 2 + _viewY;
+ if (_rebelHandler != 7 || !_flyShipSprite || _shipLevelMode == 5)
+ return;
- // Draw the ship sprite (DAT_0047fee8 / FLY001)
- renderNutSprite(renderBitmap, pitch, width, height, drawX, drawY, _flyShipSprite, spriteIndex);
+ // Base position at screen center with direction offset
+ int baseX = 160;
+ int baseY = 105;
+ int16 posOffsetX = (_flyShipScreenX - 160) / 10;
+ int16 posOffsetY = (_flyShipScreenY - 100) / 10;
+ int shipScreenX = baseX + posOffsetX;
+ int shipScreenY = baseY + posOffsetY;
- // Draw laser overlay if firing and laser sprite loaded (FLY002)
- if (_shipFiring && _flyLaserSprite && _flyLaserSprite->getNumChars() > 0) {
- // Laser sprite uses same direction index for frame animation
- int laserIndex = spriteIndex % _flyLaserSprite->getNumChars();
- renderNutSprite(renderBitmap, pitch, width, height, drawX, drawY, _flyLaserSprite, laserIndex);
- }
+ int numSprites = _flyShipSprite->getNumChars();
+ int spriteIndex = _shipDirectionIndex;
+ if (spriteIndex < 0) spriteIndex = 0;
+ if (spriteIndex >= numSprites) spriteIndex = numSprites - 1;
- // Draw targeting overlay if loaded (FLY003) - goes at crosshair position
- if (_flyTargetSprite && _flyTargetSprite->getNumChars() > 0) {
- // Draw targeting reticle at ship position
- int targetW = _flyTargetSprite->getCharWidth(0);
- int targetH = _flyTargetSprite->getCharHeight(0);
- int targetX = shipScreenX - targetW / 2 + _viewX;
- int targetY = shipScreenY - targetH / 2 + _viewY;
- renderNutSprite(renderBitmap, pitch, width, height, targetX, targetY, _flyTargetSprite, 0);
- }
+ // Center sprite at position
+ int spriteW = _flyShipSprite->getCharWidth(spriteIndex);
+ int spriteH = _flyShipSprite->getCharHeight(spriteIndex);
+ int drawX = shipScreenX - spriteW / 2 + _viewX;
+ int drawY = shipScreenY - spriteH / 2 + _viewY;
- debug("Rebel2 Handler7: Ship drawn at (%d,%d) screenPos=(%d,%d) sprite=%d/%d dir=(%d,%d) idx=%d",
- drawX, drawY, shipScreenX, shipScreenY, spriteIndex, numSprites,
- _shipDirectionH, _shipDirectionV, _shipDirectionIndex);
+ renderNutSprite(renderBitmap, pitch, width, height, drawX, drawY, _flyShipSprite, spriteIndex);
+
+ // Laser overlay if firing
+ if (_shipFiring && _flyLaserSprite && _flyLaserSprite->getNumChars() > 0) {
+ int laserIndex = spriteIndex % _flyLaserSprite->getNumChars();
+ renderNutSprite(renderBitmap, pitch, width, height, drawX, drawY, _flyLaserSprite, laserIndex);
+ }
+
+ // Targeting overlay
+ if (_flyTargetSprite && _flyTargetSprite->getNumChars() > 0) {
+ int targetW = _flyTargetSprite->getCharWidth(0);
+ int targetH = _flyTargetSprite->getCharHeight(0);
+ int targetX = shipScreenX - targetW / 2 + _viewX;
+ int targetY = shipScreenY - targetH / 2 + _viewY;
+ renderNutSprite(renderBitmap, pitch, width, height, targetX, targetY, _flyTargetSprite, 0);
}
+ debug("Rebel2 Handler7: Ship at (%d,%d) sprite=%d/%d dir=(%d,%d) idx=%d",
+ drawX, drawY, spriteIndex, numSprites, _shipDirectionH, _shipDirectionV, _shipDirectionIndex);
+}
+
+void InsaneRebel2::renderHandler8Ship(byte *renderBitmap, int pitch, int width, int height) {
// Handler 8 Ship Rendering (Third-Person Vehicle - POV sprites)
- // Handler 8 uses _shipSprite (POV001) with position-based offset
- else if (_rebelHandler == 8 && _shipSprite && _shipLevelMode != 5) {
- // Calculate display offset from raw ship position (FUN_00401ccf lines 88-89)
- // The shift operations create a dampened movement effect
- int16 displayOffsetX = (_shipPosX - 0xa0) >> 3; // (shipPosX - 160) >> 3
- int16 displayOffsetY = (_shipPosY - 0x28) >> 2; // (shipPosY - 40) >> 2
-
- // Base screen position from crosshair calculation (FUN_00401ccf lines 128-129)
- // Low-res mode: base X = 0xa0 (160), base Y = 0x69 (105)
- int shipScreenX = 0xa0 + displayOffsetX; // 160 + offset
- int shipScreenY = 0x69 + displayOffsetY; // 105 + offset
-
- int numSprites = _shipSprite->getNumChars();
- int spriteIndex = 0;
-
- // Select sprite based on direction when multiple sprites are available
- // The ship sprite sheet is organized as a grid of direction sprites:
- // - 35 sprites (5x7): Full direction grid
- // - 25 sprites (5x5): Reduced grid
- // - 5 sprites: Horizontal direction only
- // - 2 sprites: Normal (0) and firing (1)
- // - 1 sprite: Static ship
- if (numSprites >= 35) {
- spriteIndex = _shipDirectionH * 7 + _shipDirectionV;
- if (spriteIndex >= numSprites) spriteIndex = numSprites - 1;
- } else if (numSprites >= 25) {
- int vDir5 = (_shipDirectionV * 5) / 7;
- spriteIndex = _shipDirectionH * 5 + vDir5;
- if (spriteIndex >= numSprites) spriteIndex = numSprites - 1;
- } else if (numSprites >= 5) {
- spriteIndex = _shipDirectionH;
- if (spriteIndex >= numSprites) spriteIndex = numSprites - 1;
- } else if (numSprites == 2) {
- spriteIndex = _shipFiring ? 1 : 0;
- }
-
- // Get sprite dimensions and center it at the calculated position
- int spriteW = _shipSprite->getCharWidth(spriteIndex);
- int spriteH = _shipSprite->getCharHeight(spriteIndex);
- int drawX = shipScreenX - spriteW / 2 + _viewX;
- int drawY = shipScreenY - spriteH / 2 + _viewY;
-
- // Draw the primary ship sprite (DAT_0047e010)
- renderNutSprite(renderBitmap, pitch, width, height, drawX, drawY, _shipSprite, spriteIndex);
-
- // Draw secondary ship sprite if available (DAT_0047e028)
- if (_shipSprite2 && _shipSprite2->getNumChars() > spriteIndex) {
- renderNutSprite(renderBitmap, pitch, width, height, drawX, drawY, _shipSprite2, spriteIndex);
- }
-
- debug("Rebel2 Handler8: Ship drawn at screen(%d,%d) raw(%d,%d) offset(%d,%d) sprite=%d/%d dir=(%d,%d)",
- drawX, drawY, _shipPosX, _shipPosY, displayOffsetX, displayOffsetY,
- spriteIndex, numSprites, _shipDirectionH, _shipDirectionV);
- }
-
- // Fallback: Use embedded HUD frame as ship sprite
- else if ((_rebelHandler == 7 || _rebelHandler == 8) && _shipLevelMode != 5) {
- // Fallback: Use embedded HUD frame as ship sprite (Level 3 style)
- // userId=11 contains the ship sprite strip
- EmbeddedSanFrame &shipFrame = _rebelEmbeddedHud[11];
- if (shipFrame.valid && shipFrame.pixels && shipFrame.width > 0 && shipFrame.height > 0) {
- // Calculate display offset from raw ship position
- int16 displayOffsetX = (_shipPosX - 0xa0) >> 3;
- int16 displayOffsetY = (_shipPosY - 0x28) >> 2;
- int shipScreenX = 0xa0 + displayOffsetX;
- int shipScreenY = 0x69 + displayOffsetY;
-
- // Check if this is a sprite strip (multiple directions in one image)
- // 205 width / 5 directions = 41 pixels per direction
- int spriteW = shipFrame.width;
- int spriteH = shipFrame.height;
- int srcX = 0;
- int srcY = 0;
- int numHorizontal = 1;
- int numVertical = 1;
-
- // Detect sprite strip layout - look for common patterns
- if (spriteW >= 200 && spriteW % 5 == 0) {
- // 5 horizontal directions (like 205 = 41 * 5)
- numHorizontal = 5;
- spriteW = shipFrame.width / 5;
- }
- if (spriteH >= 350 && spriteH % 7 == 0) {
- // 7 vertical directions
- numVertical = 7;
- spriteH = shipFrame.height / 7;
+ // Uses _shipSprite (POV001) with position-based offset
+
+ if (_rebelHandler != 8 || !_shipSprite || _shipLevelMode == 5)
+ return;
+
+ // Calculate display offset from raw ship position (FUN_00401ccf lines 88-89)
+ int16 displayOffsetX = (_shipPosX - 0xa0) >> 3;
+ int16 displayOffsetY = (_shipPosY - 0x28) >> 2;
+
+ // Base screen position (low-res: X=160, Y=105)
+ int shipScreenX = 0xa0 + displayOffsetX;
+ int shipScreenY = 0x69 + displayOffsetY;
+
+ int numSprites = _shipSprite->getNumChars();
+ int spriteIndex = 0;
+
+ // Select sprite based on direction and sprite count
+ if (numSprites >= 35) {
+ spriteIndex = _shipDirectionH * 7 + _shipDirectionV;
+ if (spriteIndex >= numSprites) spriteIndex = numSprites - 1;
+ } else if (numSprites >= 25) {
+ int vDir5 = (_shipDirectionV * 5) / 7;
+ spriteIndex = _shipDirectionH * 5 + vDir5;
+ if (spriteIndex >= numSprites) spriteIndex = numSprites - 1;
+ } else if (numSprites >= 5) {
+ spriteIndex = _shipDirectionH;
+ if (spriteIndex >= numSprites) spriteIndex = numSprites - 1;
+ } else if (numSprites == 2) {
+ spriteIndex = _shipFiring ? 1 : 0;
+ }
+
+ // Center sprite at position
+ int spriteW = _shipSprite->getCharWidth(spriteIndex);
+ int spriteH = _shipSprite->getCharHeight(spriteIndex);
+ int drawX = shipScreenX - spriteW / 2 + _viewX;
+ int drawY = shipScreenY - spriteH / 2 + _viewY;
+
+ renderNutSprite(renderBitmap, pitch, width, height, drawX, drawY, _shipSprite, spriteIndex);
+
+ // Secondary ship sprite
+ if (_shipSprite2 && _shipSprite2->getNumChars() > spriteIndex) {
+ renderNutSprite(renderBitmap, pitch, width, height, drawX, drawY, _shipSprite2, spriteIndex);
+ }
+
+ debug("Rebel2 Handler8: Ship at (%d,%d) raw(%d,%d) offset(%d,%d) sprite=%d/%d dir=(%d,%d)",
+ drawX, drawY, _shipPosX, _shipPosY, displayOffsetX, displayOffsetY,
+ spriteIndex, numSprites, _shipDirectionH, _shipDirectionV);
+}
+
+void InsaneRebel2::renderFallbackShip(byte *renderBitmap, int pitch, int width, int height) {
+ // Fallback: Use embedded HUD frame as ship sprite (Level 3 style)
+ // userId=11 contains the ship sprite strip
+
+ if ((_rebelHandler != 7 && _rebelHandler != 8) || _shipLevelMode == 5)
+ return;
+
+ // Skip if we have proper sprites
+ if (_rebelHandler == 7 && _flyShipSprite)
+ return;
+ if (_rebelHandler == 8 && _shipSprite)
+ return;
+
+ EmbeddedSanFrame &shipFrame = _rebelEmbeddedHud[11];
+ if (!shipFrame.valid || !shipFrame.pixels || shipFrame.width <= 0 || shipFrame.height <= 0)
+ return;
+
+ // Calculate display offset
+ int16 displayOffsetX = (_shipPosX - 0xa0) >> 3;
+ int16 displayOffsetY = (_shipPosY - 0x28) >> 2;
+ int shipScreenX = 0xa0 + displayOffsetX;
+ int shipScreenY = 0x69 + displayOffsetY;
+
+ // Detect sprite strip layout
+ int spriteW = shipFrame.width;
+ int spriteH = shipFrame.height;
+ int srcX = 0, srcY = 0;
+ int numHorizontal = 1, numVertical = 1;
+
+ if (spriteW >= 200 && spriteW % 5 == 0) {
+ numHorizontal = 5;
+ spriteW = shipFrame.width / 5;
+ }
+ if (spriteH >= 350 && spriteH % 7 == 0) {
+ numVertical = 7;
+ spriteH = shipFrame.height / 7;
+ }
+
+ int hDir = MIN((int)_shipDirectionH, numHorizontal - 1);
+ int vDir = MIN((int)_shipDirectionV, numVertical - 1);
+ srcX = hDir * spriteW;
+ srcY = vDir * spriteH;
+
+ int drawX = shipScreenX - spriteW / 2 + _viewX;
+ int drawY = shipScreenY - spriteH / 2 + _viewY;
+
+ // Blit from embedded HUD
+ for (int y = 0; y < spriteH && (drawY + y) < height; y++) {
+ if (drawY + y < 0) continue;
+ for (int x = 0; x < spriteW && (drawX + x) < width; x++) {
+ if (drawX + x < 0) continue;
+ int srcIdx = (srcY + y) * shipFrame.width + (srcX + x);
+ byte pixel = shipFrame.pixels[srcIdx];
+ if (pixel != 0 && pixel != 231) {
+ int dstIdx = (drawY + y) * pitch + (drawX + x);
+ renderBitmap[dstIdx] = pixel;
}
+ }
+ }
- // Select sprite from strip based on direction
- int hDir = _shipDirectionH;
- int vDir = _shipDirectionV;
- if (hDir >= numHorizontal) hDir = numHorizontal - 1;
- if (vDir >= numVertical) vDir = numVertical - 1;
- srcX = hDir * spriteW;
- srcY = vDir * spriteH;
-
- // Draw position (centered)
- int drawX = shipScreenX - spriteW / 2 + _viewX;
- int drawY = shipScreenY - spriteH / 2 + _viewY;
-
- // Blit from embedded HUD to render buffer
- for (int y = 0; y < spriteH && (drawY + y) < height; y++) {
- if (drawY + y < 0) continue;
- for (int x = 0; x < spriteW && (drawX + x) < width; x++) {
- if (drawX + x < 0) continue;
- int srcIdx = (srcY + y) * shipFrame.width + (srcX + x);
- byte pixel = shipFrame.pixels[srcIdx];
- // Skip transparent pixels (0 and 231)
- if (pixel != 0 && pixel != 231) {
- int dstIdx = (drawY + y) * pitch + (drawX + x);
- renderBitmap[dstIdx] = pixel;
- }
+ debug("Rebel2: Ship (fallback) at (%d,%d) strip=(%d,%d) of (%dx%d) dir=(%d,%d)",
+ drawX, drawY, srcX, srcY, numHorizontal, numVertical, _shipDirectionH, _shipDirectionV);
+}
+
+void InsaneRebel2::renderEnemyOverlays(byte *renderBitmap, int pitch, int width, int height, int videoWidth) {
+ // Draw enemy indicator brackets and erase destroyed enemy areas
+
+ // Erase destroyed enemies' areas (fill with black)
+ for (Common::List<enemy>::iterator it = _enemies.begin(); it != _enemies.end(); ++it) {
+ if (it->destroyed) {
+ Common::Rect r = it->rect;
+ if (r.left < 0) r.left = 0;
+ if (r.top < 0) r.top = 0;
+ if (r.right > width) r.right = width;
+ if (r.bottom > height) r.bottom = height;
+
+ for (int y = r.top; y < r.bottom; y++) {
+ for (int x = r.left; x < r.right; x++) {
+ renderBitmap[y * pitch + x] = 0;
}
}
-
- debug("Rebel2: Ship (embedded HUD) at screen(%d,%d) strip=(%d,%d) of (%dx%d) dir=(%d,%d)",
- drawX, drawY, srcX, srcY, numHorizontal, numVertical, _shipDirectionH, _shipDirectionV);
}
}
- Common::List<enemy>::iterator it;
- for (it = _enemies.begin(); it != _enemies.end(); ++it) {
- // Skip destroyed enemies (explosion handled by 5-slot system)
- if (it->destroyed) continue;
+ // Draw green brackets for active enemies (Easy/Medium difficulty only)
+ if (_difficulty >= 2)
+ return;
+
+ Common::Rect viewRect(_viewX, _viewY, _viewX + videoWidth, _viewY + 200);
- // Skip inactive enemies or those disabled by IACT bit
- if (!it->active || isBitSet(it->id)) continue;
+ for (Common::List<enemy>::iterator it = _enemies.begin(); it != _enemies.end(); ++it) {
+ if (it->destroyed || !it->active || isBitSet(it->id))
+ continue;
Common::Rect r = it->rect;
+ if (r.right <= viewRect.left || r.left >= viewRect.right ||
+ r.bottom <= viewRect.top || r.top >= viewRect.bottom)
+ continue;
- // Check if enemy rect is visible within the current view area
- // (matching FUN_00428b90 clip logic: reject lines entirely outside clip rect)
- // The visible area in video coordinates is the current scroll viewport
- Common::Rect viewRect(_viewX, _viewY, _viewX + videoWidth, _viewY + videoHeight);
+ const byte color = 5; // Green
+ drawCornerBrackets(renderBitmap, pitch, width, height, r.left, r.top, r.width(), r.height(), color);
+ }
+}
- // If enemy rect doesn't intersect the view area at all, skip drawing
- if (r.right <= viewRect.left || r.left >= viewRect.right ||
- r.bottom <= viewRect.top || r.top >= viewRect.bottom) {
+void InsaneRebel2::renderExplosions(byte *renderBitmap, int pitch, int width, int height) {
+ // Draw explosion animations from 5-slot system
+
+ if (!_smush_iconsNut)
+ return;
+
+ for (int i = 0; i < 5; i++) {
+ if (!_explosions[i].active)
continue;
- }
- // Draw Green Indicators (Corner Brackets) for Easy (0) and Medium (1) difficulty
- // Hard (2) mode does not show indicators (matching FUN_00425d30(4) check)
- if (_difficulty < 2) {
- const byte color = 5; // Green color index for brackets
- drawCornerBrackets(renderBitmap, pitch, width, height, r.left, r.top, r.width(), r.height(), color);
+ if (_explosions[i].counter <= 0) {
+ _explosions[i].active = false;
+ continue;
}
- }
- // Draw 5-slot Explosion System
- if (_smush_iconsNut) {
- for (int i = 0; i < 5; i++) {
- if (_explosions[i].active) {
- if (_explosions[i].counter <= 0) {
- _explosions[i].active = false;
- continue;
- }
+ // Determine base sprite index based on scale (FUN_409FBC logic)
+ int baseIndex;
+ if (_explosions[i].scale < 11) {
+ baseIndex = 9; // Small/Medium
+ } else if (_explosions[i].scale < 21) {
+ baseIndex = 19; // Medium/Large
+ } else {
+ baseIndex = 29; // Large/XL
+ }
- // Determine base sprite index based on scale (FUN_409FBC logic)
- int baseIndex;
- if (_explosions[i].scale < 11) {
- baseIndex = 9; // Small/Medium transition
- } else if (_explosions[i].scale < 21) {
- baseIndex = 19; // Medium/Large transition
- } else {
- baseIndex = 29; // Large/XL transition
- }
-
- // Formula: Base + (12 - Counter)
- // Counter goes 10 -> 1.
- // Frame goes Base+2 -> Base+11.
- int spriteIndex = baseIndex + (12 - _explosions[i].counter);
-
- if (_smush_iconsNut->getNumChars() > spriteIndex) {
- int ew = _smush_iconsNut->getCharWidth(spriteIndex);
- int eh = _smush_iconsNut->getCharHeight(spriteIndex);
- int cx = _explosions[i].x - ew / 2;
- int cy = _explosions[i].y - eh / 2;
-
- // Draw explosion
- renderNutSprite(renderBitmap, pitch, width, height, cx, cy, _smush_iconsNut, spriteIndex);
- }
+ // Formula: Base + (12 - Counter)
+ int spriteIndex = baseIndex + (12 - _explosions[i].counter);
- _explosions[i].counter--;
- }
+ if (_smush_iconsNut->getNumChars() > spriteIndex) {
+ int ew = _smush_iconsNut->getCharWidth(spriteIndex);
+ int eh = _smush_iconsNut->getCharHeight(spriteIndex);
+ int cx = _explosions[i].x - ew / 2;
+ int cy = _explosions[i].y - eh / 2;
+ renderNutSprite(renderBitmap, pitch, width, height, cx, cy, _smush_iconsNut, spriteIndex);
}
+
+ _explosions[i].counter--;
}
+}
+
+void InsaneRebel2::renderLaserShots(byte *renderBitmap, int pitch, int width, int height) {
+ // Draw laser shot beams and impacts
+
+ if (!_smush_iconsNut || _smush_iconsNut->getNumChars() <= 0)
+ return;
- // Draw Laser Shots
- // Gun Positions (Approximate for Turret Mode / Default):
- // Left: (10, 190), Right: (310, 190) - Adjusted for low-res
- const int GUN_LEFT_X = 10;
- const int GUN_LEFT_Y = 190;
- const int GUN_RIGHT_X = 310;
- const int GUN_RIGHT_Y = 190;
+ // Gun positions (approximate for turret mode)
+ const int GUN_LEFT_X = 10, GUN_LEFT_Y = 190;
+ const int GUN_RIGHT_X = 310, GUN_RIGHT_Y = 190;
for (int i = 0; i < 2; i++) {
- if (_shots[i].active) {
- if (_shots[i].counter <= 0) {
- _shots[i].active = false;
- continue;
- }
-
- // Use Sprite 0 from CPITIMAG.NUT as the laser texture (15x15 projectile)
- // Confirmed by info.md: indexes 0-4 are laser/projectile
- if (_smush_iconsNut && _smush_iconsNut->getNumChars() > 0) {
- // Calculate progress
- int maxProgress = 4; // Max duration from table (supposedly)
- int progress = maxProgress - _shots[i].counter;
-
- // Draw Beams depending on Level Type
- // Scene 1 (LevelType 1) has 3 beams: Right, Middle, Left
- if (_rebelLevelType <= 1) { // Default or Type 1
- // Right Beam: Origin(310, 170), Thickness 8, LengthFac 12
- drawLaserBeam(renderBitmap, pitch, width, height,
- 310 + _viewX, 170 + _viewY,
- _shots[i].x, _shots[i].y,
- progress, maxProgress, 8, 12, _smush_iconsNut, 0);
-
- // Middle Beam: Origin(160, 380), Thickness 5, LengthFac 8
- // Note: 380 is virtual origin below screen
- drawLaserBeam(renderBitmap, pitch, width, height,
- 160 + _viewX, 380 + _viewY,
- _shots[i].x, _shots[i].y,
- progress, maxProgress, 5, 8, _smush_iconsNut, 0);
-
- // Left Beam: Origin(10, 170), Thickness 8, LengthFac 12
- drawLaserBeam(renderBitmap, pitch, width, height,
- 10 + _viewX, 170 + _viewY,
- _shots[i].x, _shots[i].y,
- progress, maxProgress, 8, 12, _smush_iconsNut, 0);
-
- } else {
- // Fallback for other levels (2 beams)
- drawLaserBeam(renderBitmap, pitch, width, height,
- GUN_LEFT_X + _viewX, GUN_LEFT_Y + _viewY,
- _shots[i].x, _shots[i].y,
- progress, maxProgress, 8, 12, _smush_iconsNut, 0);
-
- drawLaserBeam(renderBitmap, pitch, width, height,
- GUN_RIGHT_X + _viewX, GUN_RIGHT_Y + _viewY,
- _shots[i].x, _shots[i].y,
- progress, maxProgress, 8, 12, _smush_iconsNut, 0);
- }
+ if (!_shots[i].active)
+ continue;
- // Draw Projectile Impact
- // Using Sprite 0 (small flash) or similar at impact point
- renderNutSprite(renderBitmap, pitch, width, height, _shots[i].x - 7, _shots[i].y - 7, _smush_iconsNut, 0);
- }
-
- _shots[i].counter--;
+ if (_shots[i].counter <= 0) {
+ _shots[i].active = false;
+ continue;
}
- }
- // ============================================================
- // COLLISION ZONE VISUALIZATION (for debugging Level 3 pilot mode)
- // ============================================================
- // Draw collision zones as wireframe quadrilaterals when in Handler 7 (space flight)
- // or Handler 8 (ground vehicle) modes. This helps verify parsing is correct.
- // The zones are drawn BEFORE the crosshair so they appear under the UI.
- if (_rebelHandler == 7 || _rebelHandler == 8) {
- drawCollisionZones(renderBitmap, pitch, width, height, 0);
- }
+ int maxProgress = 4;
+ int progress = maxProgress - _shots[i].counter;
- // ============================================================
- // TARGET LOCK DETECTION (DAT_00443676 equivalent)
- // ============================================================
- // In retail: When crosshair is over an active enemy, set target lock timer to 7
- // This triggers the animated crosshair cycling for Handler 0x26 (turret)
- // Check from FUN_40A2E0 lines 127-143
- {
- Common::Point worldMousePos(_vm->_mouse.x + _viewX, _vm->_mouse.y + _viewY);
- bool targetLocked = false;
+ // Draw beams based on level type
+ if (_rebelLevelType <= 1) {
+ // Type 1: 3 beams (Right, Middle, Left)
+ drawLaserBeam(renderBitmap, pitch, width, height,
+ 310 + _viewX, 170 + _viewY, _shots[i].x, _shots[i].y,
+ progress, maxProgress, 8, 12, _smush_iconsNut, 0);
- for (Common::List<enemy>::iterator enemyIt = _enemies.begin(); enemyIt != _enemies.end(); ++enemyIt) {
- if (enemyIt->active && !enemyIt->destroyed && enemyIt->rect.contains(worldMousePos)) {
- targetLocked = true;
- break;
- }
- }
+ drawLaserBeam(renderBitmap, pitch, width, height,
+ 160 + _viewX, 380 + _viewY, _shots[i].x, _shots[i].y,
+ progress, maxProgress, 5, 8, _smush_iconsNut, 0);
- // Update target lock timer
- if (targetLocked) {
- _targetLockTimer = 7; // Set to 7 when over target (retail behavior)
- } else if (_targetLockTimer > 0) {
- _targetLockTimer--; // Count down when not over target
+ drawLaserBeam(renderBitmap, pitch, width, height,
+ 10 + _viewX, 170 + _viewY, _shots[i].x, _shots[i].y,
+ progress, maxProgress, 8, 12, _smush_iconsNut, 0);
+ } else {
+ // Other levels: 2 beams
+ drawLaserBeam(renderBitmap, pitch, width, height,
+ GUN_LEFT_X + _viewX, GUN_LEFT_Y + _viewY, _shots[i].x, _shots[i].y,
+ progress, maxProgress, 8, 12, _smush_iconsNut, 0);
+
+ drawLaserBeam(renderBitmap, pitch, width, height,
+ GUN_RIGHT_X + _viewX, GUN_RIGHT_Y + _viewY, _shots[i].x, _shots[i].y,
+ progress, maxProgress, 8, 12, _smush_iconsNut, 0);
}
+
+ // Impact flash
+ renderNutSprite(renderBitmap, pitch, width, height,
+ _shots[i].x - 7, _shots[i].y - 7, _smush_iconsNut, 0);
+
+ _shots[i].counter--;
}
+}
- // Draw Crosshair/Reticle cursor
- // Sprite indices based on handler type (from original game disassembly FUN_004089ab, FUN_0040d836, etc):
- // - Handler 8 (third-person vehicle): Index 0x2E (46)
- // - Handler 7 (space flight): Index 0x2F (47)
- // - Handler 0x19 (mixed/turret view): Index 0x2F (47)
- // - Handler 0x26 (full turret): Index varies by levelType and animation
- //
- // For Handler 0x26 (turret), the crosshair sprite formula from FUN_004089ab line 192-193:
- // (-(ushort)(DAT_004436de == 5) & 0x30) + local_28
- // where local_28 is an animation offset (0-3) based on target lock timer (DAT_00443676)
- //
- // When DAT_00443676 == 0 (no target locked): local_28 = 0
- // When DAT_00443676 != 0: local_28 = 3 - (DAT_0047fe98 & 3) -- animated cycling
- if (_smush_iconsNut) {
- int reticleIndex;
- switch (_rebelHandler) {
- case 7: // Space flight - uses crosshair sprite 0x2F (47)
- reticleIndex = 47;
- break;
- case 0x19: // Mixed/turret view - uses crosshair sprite 0x2F (47)
- reticleIndex = 47;
- break;
- case 0x26: { // Full turret mode - animated crosshair
- // Calculate animation offset based on target lock state
- // In retail: DAT_00443676 is the "target lock timer" (set to 7 when targeting)
- // DAT_0047fe98 is a per-frame counter incremented each frame
- static int turretAnimCounter = 0;
- turretAnimCounter++;
-
- int animOffset;
- // Use actual target lock timer (DAT_00443676 equivalent)
- if (_targetLockTimer == 0) {
- animOffset = 0; // No target - use static crosshair
- } else {
- animOffset = 3 - (turretAnimCounter & 3); // Cycles 3, 2, 1, 0
- }
+void InsaneRebel2::renderCrosshair(byte *renderBitmap, int pitch, int width, int height) {
+ // Update target lock state and draw crosshair/reticle
- // If levelType == 5, use sprites 0x30-0x33 (48-51)
- // Otherwise use sprites 0-3 (basic targeting reticle)
- if (_rebelLevelType == 5) {
- reticleIndex = 0x30 + animOffset; // 48-51
- } else {
- reticleIndex = animOffset; // 0-3
- }
- break;
- }
- case 8: // Ground vehicle - uses crosshair sprite 0x2E (46)
- default:
- reticleIndex = 46;
+ // Target lock detection (DAT_00443676 equivalent)
+ Common::Point worldMousePos(_vm->_mouse.x + _viewX, _vm->_mouse.y + _viewY);
+ bool targetLocked = false;
+
+ for (Common::List<enemy>::iterator it = _enemies.begin(); it != _enemies.end(); ++it) {
+ if (it->active && !it->destroyed && it->rect.contains(worldMousePos)) {
+ targetLocked = true;
break;
}
+ }
+
+ if (targetLocked) {
+ _targetLockTimer = 7;
+ } else if (_targetLockTimer > 0) {
+ _targetLockTimer--;
+ }
+
+ // Draw crosshair
+ if (!_smush_iconsNut)
+ return;
+
+ int reticleIndex;
+ switch (_rebelHandler) {
+ case 7: // Space flight
+ case 0x19: // Mixed/turret
+ reticleIndex = 47;
+ break;
+ case 0x26: { // Full turret - animated crosshair
+ static int turretAnimCounter = 0;
+ turretAnimCounter++;
+
+ int animOffset = (_targetLockTimer == 0) ? 0 : 3 - (turretAnimCounter & 3);
- if (_smush_iconsNut->getNumChars() > reticleIndex) {
- int cw = _smush_iconsNut->getCharWidth(reticleIndex);
- int ch = _smush_iconsNut->getCharHeight(reticleIndex);
- // Center the crosshair on mouse position (in world coordinates)
- renderNutSprite(renderBitmap, pitch, width, height, _vm->_mouse.x - cw / 2 + _viewX, _vm->_mouse.y - ch / 2 + _viewY, _smush_iconsNut, reticleIndex);
+ if (_rebelLevelType == 5) {
+ reticleIndex = 0x30 + animOffset;
+ } else {
+ reticleIndex = animOffset;
}
+ break;
+ }
+ case 8: // Ground vehicle
+ default:
+ reticleIndex = 46;
+ break;
}
- // ============================================================
- // HUD SCORE/LIVES RENDERING (FUN_0041c012 equivalent)
- // ============================================================
- // Render score and lives counter on the status bar
- // This is called after all other rendering so it appears on top
- renderScoreHUD(renderBitmap, pitch, width, height, 0);
+ if (_smush_iconsNut->getNumChars() > reticleIndex) {
+ int cw = _smush_iconsNut->getCharWidth(reticleIndex);
+ int ch = _smush_iconsNut->getCharHeight(reticleIndex);
+ renderNutSprite(renderBitmap, pitch, width, height,
+ _vm->_mouse.x - cw / 2 + _viewX, _vm->_mouse.y - ch / 2 + _viewY,
+ _smush_iconsNut, reticleIndex);
+ }
+}
- // ============================================================
- // FRAME END RESET: Clear active flags for all enemies (FUN_403240 equivalent)
- // ============================================================
- // The original game rebuilds the object list from scratch each frame:
- // - FUN_4033CF (IACT processing) adds objects to DAT_0043fb00 list
- // - FUN_4092D9 iterates only over objects in the current frame's list
- // - FUN_403240 (called at frame end) resets DAT_0047ee74 = 0 (clears list counter)
- //
- // We achieve the same by marking all non-destroyed enemies inactive at frame end.
- // The next frame's IACT opcode 4 (enemyUpdate) will re-activate them if present.
- for (Common::List<enemy>::iterator resetIt = _enemies.begin(); resetIt != _enemies.end(); ++resetIt) {
- if (!resetIt->destroyed) {
- resetIt->active = false;
+void InsaneRebel2::frameEndCleanup() {
+ // Reset enemy active flags and collision zones at frame end
+ // The original game rebuilds lists from scratch each frame
+
+ for (Common::List<enemy>::iterator it = _enemies.begin(); it != _enemies.end(); ++it) {
+ if (!it->destroyed) {
+ it->active = false;
}
}
- // Reset collision zone counters for next frame (DAT_0047ee74/DAT_0047ee78 = 0)
- // The zones are rebuilt from IACT opcode 5 chunks each frame
resetCollisionZones();
}
diff --git a/engines/scumm/insane/insane_rebel.h b/engines/scumm/insane/insane_rebel.h
index b47bd1f3098..a1e7fe81d19 100644
--- a/engines/scumm/insane/insane_rebel.h
+++ b/engines/scumm/insane/insane_rebel.h
@@ -264,6 +264,74 @@ public:
int32 setupsan13, Common::SeekableReadStream &b, int32 size, int32 flags,
int16 par1, int16 par2, int16 par3, int16 par4) override;
+ // ======================= Rendering Helper Functions =======================
+ // These are extracted from procPostRendering for better readability
+
+ // Fill status bar background area (FUN_004288c0 equivalent)
+ void renderStatusBarBackground(byte *renderBitmap, int pitch, int width, int height,
+ int videoWidth, int videoHeight, int statusBarY);
+
+ // Draw NUT-based HUD overlays for Handler 0x26/0x19 turret modes
+ void renderTurretHudOverlays(byte *renderBitmap, int pitch, int width, int height, int32 curFrame);
+
+ // Draw embedded SAN HUD overlays from IACT chunks
+ void renderEmbeddedHudOverlays(byte *renderBitmap, int pitch, int width, int height);
+
+ // Draw DISPFONT.NUT status bar sprites (FUN_0041c012 equivalent)
+ void renderStatusBarSprites(byte *renderBitmap, int pitch, int width, int height,
+ int statusBarY, int32 curFrame);
+
+ // Draw Handler 7 ship sprite (space flight - FLY sprites)
+ void renderHandler7Ship(byte *renderBitmap, int pitch, int width, int height);
+
+ // Draw Handler 8 ship sprite (third-person vehicle - POV sprites)
+ void renderHandler8Ship(byte *renderBitmap, int pitch, int width, int height);
+
+ // Draw fallback ship using embedded HUD frame
+ void renderFallbackShip(byte *renderBitmap, int pitch, int width, int height);
+
+ // Draw enemy indicator brackets and erase destroyed enemy areas
+ void renderEnemyOverlays(byte *renderBitmap, int pitch, int width, int height, int videoWidth);
+
+ // Draw explosion animations from 5-slot system
+ void renderExplosions(byte *renderBitmap, int pitch, int width, int height);
+
+ // Draw laser shot beams and impacts
+ void renderLaserShots(byte *renderBitmap, int pitch, int width, int height);
+
+ // Update target lock state and draw crosshair/reticle
+ void renderCrosshair(byte *renderBitmap, int pitch, int width, int height);
+
+ // Reset enemy active flags and collision zones at frame end
+ void frameEndCleanup();
+
+ // ======================= Opcode 6 Helper Functions =======================
+ // Handler-specific setup extracted from iactRebel2Opcode6
+
+ // Handler 8 (third-person vehicle) setup - FUN_00401234 case 4
+ void opcode6Handler8Setup(int16 par3, int16 par4);
+
+ // Handler 7 (space flight) setup - FUN_0040c3cc case 4
+ void opcode6Handler7Setup(int16 par3, int16 par4);
+
+ // Calculate view offsets based on level type (lines 182-213)
+ void opcode6CalcViewOffsets();
+
+ // ======================= Opcode 8 Helper Functions =======================
+ // Resource loading extracted from iactRebel2Opcode8
+
+ // Load Handler 7 FLY NUT sprites from IACT data
+ bool loadHandler7FlySprites(Common::SeekableReadStream &b, int64 remaining, int16 par4);
+
+ // Load turret HUD overlay NUT from ANIM data
+ bool loadTurretHudOverlay(byte *animData, int32 size, int16 par3);
+
+ // Load Handler 8 ship POV NUT sprites from ANIM data
+ bool loadHandler8ShipSprites(byte *animData, int32 size, int16 par3);
+
+ // Load Level 2 background from embedded ANIM
+ bool loadLevel2Background(byte *animData, int32 size, byte *renderBitmap);
+
// Override procSKIP to disable Full Throttle's conditional frame skip mechanism
// RA2 uses a different system for conditional frames via IACT opcodes
void procSKIP(int32 subSize, Common::SeekableReadStream &b) override;
diff --git a/engines/scumm/smush/smush_player.cpp b/engines/scumm/smush/smush_player.cpp
index efb7d20abc3..13a839e80f2 100644
--- a/engines/scumm/smush/smush_player.cpp
+++ b/engines/scumm/smush/smush_player.cpp
@@ -961,23 +961,32 @@ void SmushPlayer::decodeFrameObject(int codec, const uint8 *src, int left, int t
}
_dst = _specialBuffer;
} else if (_vm->_game.id == GID_REBEL2 && ((height != _vm->_screenHeight) || (width != _vm->_screenWidth))) {
- // For Rebel2, check if this partial update overlaps with a destroyed enemy
- // If so, skip the update entirely to prevent showing the enemy sprite
+ // Skip frame updates for destroyed enemies. The enemy area is erased
+ // in procPostRendering (eraseDestroyedEnemies) before explosions are drawn.
if (_insane && _insane->shouldSkipFrameUpdate(left, top, width, height)) {
return; // Skip this frame update
}
- // Rebel2 videos use frames larger than the screen (e.g., 424x260 vs 320x200).
- // Allocate a special buffer to hold the full frame - this is needed because
- // codecs like 37/47 write the full frame size regardless of screen size.
+ // Rebel2 uses a special buffer for all non-matching frames.
+ // Level 1: First frame is 424x260 (background), small sprites reuse same buffer
+ // Level 2: Uses virtual screen directly (handled below when _specialBuffer stays null
+ // because first frames are small and don't need oversized buffer)
+ // Only allocate/expand buffer for frames LARGER than current buffer or screen
int bufSize = width * height;
- if (_specialBuffer == nullptr || bufSize > _specialBufferSize) {
- free(_specialBuffer);
- _specialBuffer = (byte *)malloc(bufSize);
- _specialBufferSize = bufSize;
- _width = width;
- _height = height;
+ if (bufSize > _vm->_screenWidth * _vm->_screenHeight) {
+ // Frame is larger than screen - need special buffer
+ if (_specialBuffer == nullptr || bufSize > _specialBufferSize) {
+ free(_specialBuffer);
+ _specialBuffer = (byte *)malloc(bufSize);
+ _specialBufferSize = bufSize;
+ _width = width;
+ _height = height;
+ }
+ }
+ // Use special buffer if allocated (for oversized videos like Level 1)
+ // Otherwise use virtual screen (for Level 2 small sprites)
+ if (_specialBuffer != nullptr && _specialBufferSize >= _vm->_screenWidth * _vm->_screenHeight) {
+ _dst = _specialBuffer;
}
- _dst = _specialBuffer;
} else if ((height > _vm->_screenHeight) || (width > _vm->_screenWidth))
return;
// FT Insane uses smaller frames to draw overlays with moving objects
Commit: 0642d63e3865e736ccca7461e6207fe7c64b59e3
https://github.com/scummvm/scummvm/commit/0642d63e3865e736ccca7461e6207fe7c64b59e3
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:08:56+02:00
Commit Message:
SCUMM: RA2: Refactor level state handling
Changed paths:
engines/scumm/insane/insane_rebel.cpp
engines/scumm/insane/insane_rebel.h
diff --git a/engines/scumm/insane/insane_rebel.cpp b/engines/scumm/insane/insane_rebel.cpp
index cf98a73f119..9977e1444ac 100644
--- a/engines/scumm/insane/insane_rebel.cpp
+++ b/engines/scumm/insane/insane_rebel.cpp
@@ -2156,6 +2156,241 @@ void InsaneRebel2::init_enemyStruct(int id, int32 x, int32 y, int32 w, int32 h,
_enemies.push_back(e);
}
+// ======================= Embedded Frame Codec Decoders =======================
+// These implement the retail codec functions FUN_0042BD60, FUN_0042BBF0, FUN_0042B5F0
+
+void InsaneRebel2::decodeCodec21(byte *dst, const byte *src, int width, int height) {
+ // Codec 21/44: Line Update codec (FUN_0042BD60)
+ // Format: each line has 2-byte size header, then pairs of (skip, count+1, literal_bytes)
+ for (int row = 0; row < height; row++) {
+ int lineDataSize = READ_LE_UINT16(src);
+ src += 2;
+ const byte *lineEnd = src + lineDataSize;
+ byte *lineDst = dst + row * width;
+ int x = 0;
+
+ while (src < lineEnd && x < width) {
+ int skip = READ_LE_UINT16(src);
+ src += 2;
+ x += skip;
+ if (src >= lineEnd) break;
+
+ int count = READ_LE_UINT16(src) + 1;
+ src += 2;
+ while (count-- > 0 && x < width && src < lineEnd) {
+ lineDst[x++] = *src++;
+ }
+ }
+ src = lineEnd;
+ }
+}
+
+void InsaneRebel2::decodeCodec23(byte *dst, const byte *src, int width, int height, int dataSize) {
+ // Codec 23: Skip/Copy with embedded RLE (FUN_0042BBF0)
+ // Format: each line has 2-byte size, then pairs of (skip, runSize, RLE_data)
+ const byte *dataEnd = src + dataSize;
+
+ for (int row = 0; row < height && src < dataEnd; row++) {
+ int lineDataSize = READ_LE_UINT16(src);
+ src += 2;
+ const byte *lineEnd = src + lineDataSize;
+ byte *lineDst = dst + row * width;
+ int x = 0;
+
+ while (src < lineEnd && x < width) {
+ int skip = READ_LE_UINT16(src);
+ src += 2;
+ x += skip;
+ if (src >= lineEnd || x >= width) break;
+
+ int runSize = READ_LE_UINT16(src);
+ src += 2;
+
+ // Decode RLE within this run
+ const byte *runEnd = src + runSize;
+ while (src < runEnd && x < width) {
+ byte code = *src++;
+ int num = (code >> 1) + 1;
+ if (num > width - x) num = width - x;
+
+ if (code & 1) {
+ // RLE run
+ byte color = (src < runEnd) ? *src++ : 0;
+ for (int i = 0; i < num && x < width; i++) {
+ lineDst[x++] = color;
+ }
+ } else {
+ // Literal run
+ for (int i = 0; i < num && x < width && src < runEnd; i++) {
+ lineDst[x++] = *src++;
+ }
+ }
+ }
+ src = runEnd;
+ }
+ src = lineEnd;
+ }
+}
+
+void InsaneRebel2::decodeCodec45(byte *dst, const byte *src, int width, int height, int dataSize) {
+ // Codec 45: RA2-specific BOMP RLE with variable header (FUN_0042B5F0)
+ // May have a 6-byte sub-header starting with "01 FE"
+
+ debug("Rebel2: Codec 45 first 20 bytes: %02X %02X %02X %02X %02X %02X | %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X",
+ src[0], src[1], src[2], src[3], src[4], src[5], src[6], src[7],
+ src[8], src[9], src[10], src[11], src[12], src[13], src[14], src[15],
+ src[16], src[17], src[18], src[19]);
+
+ // Probe for header offset
+ int headerSkip = 0;
+ bool foundValidOffset = false;
+
+ // Check for known 6-byte header pattern: 01 FE XX XX XX XX
+ if (dataSize > 6 && src[0] == 0x01 && src[1] == 0xFE) {
+ headerSkip = 6;
+ debug("Rebel2: Codec 45 found 01 FE header, skipping 6 bytes");
+ foundValidOffset = true;
+ }
+
+ // If no known header found, probe offsets 0, 2, 4, 6 to find valid RLE start
+ if (!foundValidOffset) {
+ for (int testOffset = 0; testOffset <= 6 && testOffset + 2 <= dataSize; testOffset += 2) {
+ int testLineSize = READ_LE_UINT16(src + testOffset);
+ // A valid first line size should be: > 0, <= width*2
+ if (testLineSize > 0 && testLineSize <= width * 2 && testLineSize < dataSize - testOffset) {
+ // Validate by summing line sizes
+ int sumTest = 0;
+ int linesTest = 0;
+ const byte *testPtr = src + testOffset;
+ bool validSum = true;
+
+ while (linesTest < height && testPtr + 2 <= src + dataSize) {
+ int ls = READ_LE_UINT16(testPtr);
+ if (ls <= 0 || ls > width * 2) {
+ validSum = false;
+ break;
+ }
+ sumTest += ls + 2;
+ testPtr += ls + 2;
+ linesTest++;
+ }
+
+ // Accept if we got close to expected number of lines
+ if (validSum && linesTest >= height - 1) {
+ headerSkip = testOffset;
+ foundValidOffset = true;
+ debug("Rebel2: Codec 45 found valid RLE at offset %d (tested %d lines)", testOffset, linesTest);
+ break;
+ }
+ }
+ }
+ }
+
+ if (!foundValidOffset) {
+ debug("Rebel2: Codec 45 couldn't find valid RLE offset, using offset 0");
+ }
+
+ const byte *srcPtr = src + headerSkip;
+ const byte *dataEnd = src + dataSize;
+
+ // Check if this is per-line RLE or continuous RLE
+ int firstVal = READ_LE_UINT16(srcPtr);
+ bool perLineMode = (firstVal > 0 && firstVal <= width * 2);
+
+ if (perLineMode) {
+ debug("Rebel2: Codec 45 using per-line RLE (firstLineSize=%d)", firstVal);
+ for (int row = 0; row < height && srcPtr < dataEnd; row++) {
+ int lineSize = READ_LE_UINT16(srcPtr);
+ srcPtr += 2;
+ if (lineSize <= 0 || lineSize > (int)(dataEnd - srcPtr)) break;
+
+ const byte *lineEnd = srcPtr + lineSize;
+ byte *rowDst = dst + row * width;
+ int x = 0;
+
+ while (srcPtr < lineEnd && x < width) {
+ byte ctrl = *srcPtr++;
+ int count = (ctrl >> 1) + 1;
+ if (ctrl & 1) {
+ byte color = (srcPtr < lineEnd) ? *srcPtr++ : 0;
+ for (int i = 0; i < count && x < width; i++) rowDst[x++] = color;
+ } else {
+ for (int i = 0; i < count && x < width && srcPtr < lineEnd; i++)
+ rowDst[x++] = *srcPtr++;
+ }
+ }
+ srcPtr = lineEnd;
+ }
+ } else {
+ // Continuous BOMP RLE (no per-line headers)
+ debug("Rebel2: Codec 45 using continuous BOMP RLE");
+ for (int row = 0; row < height && srcPtr < dataEnd; row++) {
+ byte *rowDst = dst + row * width;
+ int x = 0;
+
+ while (x < width && srcPtr < dataEnd) {
+ byte ctrl = *srcPtr++;
+ int count = (ctrl >> 1) + 1;
+
+ if (ctrl & 1) {
+ // RLE fill
+ byte color = (srcPtr < dataEnd) ? *srcPtr++ : 0;
+ for (int i = 0; i < count && x < width; i++) {
+ rowDst[x++] = color;
+ }
+ } else {
+ // Literal copy
+ for (int i = 0; i < count && x < width && srcPtr < dataEnd; i++) {
+ rowDst[x++] = *srcPtr++;
+ }
+ }
+ }
+ }
+ }
+
+ // Count non-zero pixels for debug
+ int nonZero = 0;
+ for (int i = 0; i < width * height; i++) {
+ if (dst[i] != 0) nonZero++;
+ }
+ debug("Rebel2: Decoded codec 45: %dx%d, %d non-zero (%d%%)",
+ width, height, nonZero, (nonZero * 100) / (width * height));
+}
+
+void InsaneRebel2::renderEmbeddedFrame(byte *renderBitmap, const EmbeddedSanFrame &frame, int userId) {
+ // Render the decoded embedded frame to the video buffer
+ // Skip immediate draw for handlers that render HUD during post-processing:
+ // - Handler 7/8: Ship direction sprites selected based on direction
+ // - Handler 0x26/0x19: Cockpit HUD positioned based on mouse/crosshair
+ bool skipImmediateDraw = (_rebelHandler == 7 || _rebelHandler == 8 ||
+ _rebelHandler == 0x26 || _rebelHandler == 0x19);
+
+ if (!frame.valid || !renderBitmap || skipImmediateDraw) {
+ if (skipImmediateDraw && frame.valid) {
+ debug("Rebel2: Skipped immediate draw for Handler %d HUD %d (will render during post-processing)",
+ _rebelHandler, userId);
+ }
+ return;
+ }
+
+ int pitch = (_player && _player->_width > 0) ? _player->_width : 320;
+ int bufHeight = (_player && _player->_height > 0) ? _player->_height : 200;
+
+ for (int y = 0; y < frame.height && (frame.renderY + y) < bufHeight; y++) {
+ for (int x = 0; x < frame.width && (frame.renderX + x) < pitch; x++) {
+ byte pixel = frame.pixels[y * frame.width + x];
+ if (pixel != 0 && pixel != 231) { // 0 and 231 = transparent
+ int destX = frame.renderX + x;
+ int destY = frame.renderY + y;
+ if (destX >= 0 && destY >= 0) {
+ renderBitmap[destY * pitch + destX] = pixel;
+ }
+ }
+ }
+ }
+ debug("Rebel2: Rendered embedded HUD %d at (%d,%d)", userId, frame.renderX, frame.renderY);
+}
+
void InsaneRebel2::loadEmbeddedSan(int userId, byte *animData, int32 size, byte *renderBitmap) {
// Validate userId - Level 3 uses slots 0-11, allow up to 15 for safety
if (userId < 0 || userId > 15 || !animData || size < 32) {
@@ -2247,216 +2482,30 @@ void InsaneRebel2::loadEmbeddedSan(int userId, byte *animData, int32 size, byte
if (dataSize > 0) {
byte *fobjData = (byte *)malloc(dataSize);
stream.read(fobjData, dataSize);
-
- // Decode based on codec
+
+ // Decode based on codec - use extracted helper functions (FUN_0042BD60, etc.)
if (codec == 1 || codec == 3) {
- // RLE use existing decoder
+ // Codec 1/3: RLE - use existing decoder (FUN_0042C590)
smushDecodeRLE(frame.pixels, fobjData, 0, 0, width, height, width);
frame.valid = true;
debug("Rebel2: Decoded embedded HUD (codec %d/RLE): %dx%d", codec, width, height);
} else if (codec == 20) {
- // Uncompressed
+ // Codec 20: Uncompressed (FUN_0042C400)
smushDecodeUncompressed(frame.pixels, fobjData, 0, 0, width, height, width);
frame.valid = true;
debug("Rebel2: Decoded embedded HUD (codec 20/raw): %dx%d", width, height);
} else if (codec == 21 || codec == 44) {
- // Codec 21/44: Line update codec
- byte *srcPtr = fobjData;
- for (int row = 0; row < height && srcPtr < fobjData + dataSize; row++) {
- int lineDataSize = READ_LE_UINT16(srcPtr);
- srcPtr += 2;
- byte *lineEnd = srcPtr + lineDataSize;
- byte *lineDst = frame.pixels + row * width;
- int x = 0;
- while (srcPtr < lineEnd && x < width) {
- int skip = READ_LE_UINT16(srcPtr);
- srcPtr += 2;
- x += skip;
- if (srcPtr >= lineEnd) break;
- int count = READ_LE_UINT16(srcPtr) + 1;
- srcPtr += 2;
- while (count-- > 0 && x < width && srcPtr < lineEnd) {
- lineDst[x++] = *srcPtr++;
- }
- }
- srcPtr = lineEnd;
- }
+ // Codec 21/44: Line update (FUN_0042BD60)
+ decodeCodec21(frame.pixels, fobjData, width, height);
frame.valid = true;
- debug("Rebel2: Decoded embedded HUD (codec 21/line update): %dx%d", width, height);
+ debug("Rebel2: Decoded embedded HUD (codec %d/line update): %dx%d", codec, width, height);
} else if (codec == 45) {
- // Codec 45: RA2-specific codec with BOMP-style RLE
- // May have a variable-length sub-header before RLE data
- // Common patterns: "01 FE 00 00 01 00" (6 bytes) or others
- debug("Rebel2: Codec 45 first 20 bytes: %02X %02X %02X %02X %02X %02X | %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X",
- fobjData[0], fobjData[1], fobjData[2], fobjData[3],
- fobjData[4], fobjData[5], fobjData[6], fobjData[7],
- fobjData[8], fobjData[9], fobjData[10], fobjData[11],
- fobjData[12], fobjData[13], fobjData[14], fobjData[15],
- fobjData[16], fobjData[17], fobjData[18], fobjData[19]);
-
- // Probe multiple header offsets to find where valid RLE data starts
- // RLE data has 2-byte line sizes that should be reasonable values
- int headerSkip = 0;
- bool foundValidOffset = false;
-
- // Check common header patterns
- if (dataSize > 6 && fobjData[0] == 0x01 && fobjData[1] == 0xFE) {
- // Known 6-byte header: 01 FE XX XX XX XX
- headerSkip = 6;
- debug("Rebel2: Codec 45 found 01 FE header, skipping 6 bytes");
- foundValidOffset = true;
- }
-
- // If no known header found, probe offsets 0, 2, 4, 6 to find valid RLE start
- if (!foundValidOffset) {
- for (int testOffset = 0; testOffset <= 6 && testOffset + 2 <= dataSize; testOffset += 2) {
- int testLineSize = READ_LE_UINT16(fobjData + testOffset);
- // A valid first line size should be: > 0, <= width*2 (reasonable for RLE)
- // Also check that the total data is enough for at least height lines
- if (testLineSize > 0 && testLineSize <= width * 2 && testLineSize < dataSize - testOffset) {
- // Further validation: sum line sizes should roughly match data size
- int sumTest = 0;
- int linesTest = 0;
- byte *testPtr = fobjData + testOffset;
- bool validSum = true;
-
- while (linesTest < height && testPtr + 2 <= fobjData + dataSize) {
- int ls = READ_LE_UINT16(testPtr);
- if (ls <= 0 || ls > width * 2) {
- validSum = false;
- break;
- }
- sumTest += ls + 2;
- testPtr += ls + 2;
- linesTest++;
- }
-
- // Accept if we got close to expected number of lines
- if (validSum && linesTest >= height - 1) {
- headerSkip = testOffset;
- foundValidOffset = true;
- debug("Rebel2: Codec 45 found valid RLE at offset %d (tested %d lines)", testOffset, linesTest);
- break;
- }
- }
- }
- }
-
- if (!foundValidOffset) {
- debug("Rebel2: Codec 45 couldn't find valid RLE offset, using offset 0");
- }
-
- byte *srcPtr = fobjData + headerSkip;
- byte *dataEnd = fobjData + dataSize;
-
- // Try per-line RLE with 2-byte LE size headers
- int firstVal = READ_LE_UINT16(srcPtr);
- bool validPerLine = (firstVal > 0 && firstVal <= width * 2);
-
- if (validPerLine) {
- debug("Rebel2: Codec 45 using per-line RLE (firstLineSize=%d)", firstVal);
- for (int row = 0; row < height && srcPtr < dataEnd; row++) {
- int lineSize = READ_LE_UINT16(srcPtr);
- srcPtr += 2;
- if (lineSize <= 0 || lineSize > (int)(dataEnd - srcPtr)) break;
-
- byte *lineEnd = srcPtr + lineSize;
- byte *dst = frame.pixels + row * width;
- int x = 0;
-
- while (srcPtr < lineEnd && x < width) {
- byte ctrl = *srcPtr++;
- int count = (ctrl >> 1) + 1;
- if (ctrl & 1) {
- byte color = (srcPtr < lineEnd) ? *srcPtr++ : 0;
- for (int i = 0; i < count && x < width; i++) dst[x++] = color;
- } else {
- for (int i = 0; i < count && x < width && srcPtr < lineEnd; i++)
- dst[x++] = *srcPtr++;
- }
- }
- srcPtr = lineEnd;
- }
- } else {
- // Try continuous BOMP RLE (no per-line headers)
- // Each line produces exactly 'width' pixels
- debug("Rebel2: Codec 45 using continuous BOMP RLE");
- for (int row = 0; row < height && srcPtr < dataEnd; row++) {
- byte *dst = frame.pixels + row * width;
- int x = 0;
-
- while (x < width && srcPtr < dataEnd) {
- byte ctrl = *srcPtr++;
- int count = (ctrl >> 1) + 1;
-
- if (ctrl & 1) {
- // RLE fill
- byte color = (srcPtr < dataEnd) ? *srcPtr++ : 0;
- for (int i = 0; i < count && x < width; i++) {
- dst[x++] = color;
- }
- } else {
- // Literal copy
- for (int i = 0; i < count && x < width && srcPtr < dataEnd; i++) {
- dst[x++] = *srcPtr++;
- }
- }
- }
- }
- }
+ // Codec 45: RA2-specific BOMP RLE (FUN_0042B5F0)
+ decodeCodec45(frame.pixels, fobjData, width, height, dataSize);
frame.valid = true;
-
- // Count non-zero pixels
- int nonZero = 0;
- for (int i = 0; i < width * height; i++) {
- if (frame.pixels[i] != 0) nonZero++;
- }
- debug("Rebel2: Decoded codec 45: %dx%d, %d non-zero (%d%%)",
- width, height, nonZero, (nonZero * 100) / (width * height));
} else if (codec == 23) {
- // Codec 23: Skip/copy with RLE in copy runs
- // Format: each line has pairs of (skip, copy_count, RLE_data)
- byte *srcPtr = fobjData;
- for (int row = 0; row < height && srcPtr < fobjData + dataSize; row++) {
- int lineDataSize = READ_LE_UINT16(srcPtr);
- srcPtr += 2;
- byte *lineEnd = srcPtr + lineDataSize;
- byte *lineDst = frame.pixels + row * width;
- int x = 0;
-
- while (srcPtr < lineEnd && x < width) {
- int skip = READ_LE_UINT16(srcPtr);
- srcPtr += 2;
- x += skip;
- if (srcPtr >= lineEnd || x >= width) break;
-
- int runSize = READ_LE_UINT16(srcPtr);
- srcPtr += 2;
-
- // Decode RLE within this run
- byte *runEnd = srcPtr + runSize;
- while (srcPtr < runEnd && x < width) {
- byte code = *srcPtr++;
- int num = (code >> 1) + 1;
- if (num > width - x) num = width - x;
-
- if (code & 1) {
- // RLE run
- byte color = (srcPtr < runEnd) ? *srcPtr++ : 0;
- for (int i = 0; i < num && x < width; i++) {
- lineDst[x++] = color;
- }
- } else {
- // Literal run
- for (int i = 0; i < num && x < width && srcPtr < runEnd; i++) {
- lineDst[x++] = *srcPtr++;
- }
- }
- }
- srcPtr = runEnd;
- }
- srcPtr = lineEnd;
- }
+ // Codec 23: Skip/copy with embedded RLE (FUN_0042BBF0)
+ decodeCodec23(frame.pixels, fobjData, width, height, dataSize);
frame.valid = true;
debug("Rebel2: Decoded embedded HUD (codec 23/skip-RLE): %dx%d", width, height);
} else {
@@ -2474,35 +2523,9 @@ void InsaneRebel2::loadEmbeddedSan(int userId, byte *animData, int32 size, byte
userId, nonZeroPixels, (nonZeroPixels * 100) / (width * height));
}
- // Draw immediately to renderBitmap if valid
- // Skip immediate draw for handlers that render HUD during post-processing:
- // - Handler 7/8: Ship direction sprites selected based on direction
- // - Handler 0x26/0x19: Cockpit HUD positioned based on mouse/crosshair
- bool skipImmediateDraw = (_rebelHandler == 7 || _rebelHandler == 8 ||
- _rebelHandler == 0x26 || _rebelHandler == 0x19);
-
- if (frame.valid && renderBitmap && !skipImmediateDraw) {
- int pitch = (_player && _player->_width > 0) ? _player->_width : 320;
- int bufHeight = (_player && _player->_height > 0) ? _player->_height : 200;
-
- for (int y = 0; y < height && (frame.renderY + y) < bufHeight; y++) {
- for (int x = 0; x < width && (frame.renderX + x) < pitch; x++) {
- byte pixel = frame.pixels[y * width + x];
- if (pixel != 0 && pixel != 231) { // 0 and 231 = transparent
- int destX = frame.renderX + x;
- int destY = frame.renderY + y;
- if (destX >= 0 && destY >= 0) {
- renderBitmap[destY * pitch + destX] = pixel;
- }
- }
- }
- }
- debug("Rebel2: Rendered embedded HUD %d at (%d,%d)", userId, frame.renderX, frame.renderY);
- } else if (skipImmediateDraw) {
- debug("Rebel2: Skipped immediate draw for Handler %d HUD %d (will render during post-processing)",
- _rebelHandler, userId);
- }
-
+ // Render the decoded frame to the video buffer
+ renderEmbeddedFrame(renderBitmap, frame, userId);
+
free(fobjData);
}
}
diff --git a/engines/scumm/insane/insane_rebel.h b/engines/scumm/insane/insane_rebel.h
index a1e7fe81d19..393a6ad7146 100644
--- a/engines/scumm/insane/insane_rebel.h
+++ b/engines/scumm/insane/insane_rebel.h
@@ -403,6 +403,26 @@ public:
// userId: HUD slot (1-4), animData: raw ANIM data, size: data size, renderBitmap: current frame buffer
void loadEmbeddedSan(int userId, byte *animData, int32 size, byte *renderBitmap) override;
+ // ======================= Embedded Frame Codec Decoders =======================
+ // These functions decode different codec formats used in embedded ANIM/FOBJ data
+ // Based on retail FUN_0042C590 (codec 1), FUN_0042BD60 (codec 21), etc.
+
+ // Decode codec 21/44 (Line Update) - skip/copy pairs per line
+ // Used for fonts and some HUD frames (FUN_0042BD60)
+ void decodeCodec21(byte *dst, const byte *src, int width, int height);
+
+ // Decode codec 23 (Skip/Copy with embedded RLE) - hybrid format
+ // Used for embedded HUD frames with transparency (FUN_0042BBF0)
+ void decodeCodec23(byte *dst, const byte *src, int width, int height, int dataSize);
+
+ // Decode codec 45 (RA2-specific BOMP RLE) - variable header format
+ // Used for small animation elements and HUD pieces (FUN_0042B5F0)
+ void decodeCodec45(byte *dst, const byte *src, int width, int height, int dataSize);
+
+ // Render a decoded embedded frame to the video buffer
+ // Handles transparency (color 0 and 231) and boundary checks
+ void renderEmbeddedFrame(byte *renderBitmap, const EmbeddedSanFrame &frame, int userId);
+
int16 _rebelLinks[512][3]; // Dependency links: Slot 0 (Disable on death), Slot 1/2 (Enable on death)
Commit: 6da03acf19d77b61ba22c7617e7c00ece8503ab6
https://github.com/scummvm/scummvm/commit/6da03acf19d77b61ba22c7617e7c00ece8503ab6
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:08:56+02:00
Commit Message:
SCUMM: RA2: Refactor gameplay handlers
Changed paths:
engines/scumm/insane/insane_rebel.cpp
diff --git a/engines/scumm/insane/insane_rebel.cpp b/engines/scumm/insane/insane_rebel.cpp
index 9977e1444ac..86ab08d07a9 100644
--- a/engines/scumm/insane/insane_rebel.cpp
+++ b/engines/scumm/insane/insane_rebel.cpp
@@ -1463,423 +1463,456 @@ void InsaneRebel2::iactRebel2Opcode6(byte *renderBitmap, Common::SeekableReadStr
void InsaneRebel2::iactRebel2Opcode8(byte *renderBitmap, Common::SeekableReadStream &b, int32 chunkSize, int16 par2, int16 par3, int16 par4) {
// Opcode 8: HUD/Ship resource loading
- // Based on FUN_41CADB case 6 (switch on *local_14 - 2 == 6, meaning opcode 8)
+ // Dispatches to handler-specific loading functions based on current handler and parameters.
//
- // For Handler 0x26 (turret) - par3 determines HUD slot:
- // case 1: DAT_00482240 - Primary HUD overlay (GRD001)
- // case 2: DAT_00482238 - Secondary HUD graphics (GRD002)
- // case 4: DAT_00482268 - Ship cockpit frame (GRD010)
- // case 5: DAT_0048226c - Mask buffer (GRD011)
- // case 6: DAT_00482250 - Explosion overlay (GRD003)
- // case 7: DAT_00482248 - Damage indicator (GRD004)
- // case 10: DAT_00482258 - Additional effects (GRD005)
- // case 12/13: DAT_00482260 - High-res HUD alternative (GRD007)
+ // Handler-specific routing (based on retail disassembly):
+ // Handler 7 (FUN_0040c3cc): FLY NUT sprites via par4 (1, 2, 3, 11)
+ // Handler 8 (FUN_00401234): POV NUT sprites via par3 (1, 3, 6, 7) or background via par4=5
+ // Handler 0x26 (FUN_00407fcb): Turret HUD NUT via par3 (1-4)
+ // Handler 0x19: Mixed turret mode, similar to 0x26
//
- // For Handler 8 (third-person vehicle) - par3 determines ship sprite slot (FUN_00401234):
- // case 1: DAT_0047e010 - Primary ship sprite (POV001)
- // case 3: DAT_0047e028 - Secondary ship sprite (POV004)
- // case 6: DAT_0047e020 - Ship overlay 1 (POV002)
- // case 7: DAT_0047e018 - Ship overlay 2 (POV003)
- //
- // cases 21-47: Sound loading (both handlers)
+ // Sound loading: par3 in range 21-47
- debug("Rebel2 IACT Opcode 8: handler=%d par2=%d par3=%d par4=%d (gameState=%d)", _rebelHandler, par2, par3, par4, _gameState);
+ debug("Rebel2 IACT Opcode 8: handler=%d par2=%d par3=%d par4=%d (gameState=%d)",
+ _rebelHandler, par2, par3, par4, _gameState);
int64 startPos = b.pos();
int64 remaining = (chunkSize > 0) ? chunkSize : (b.size() - startPos);
- // Handler 7 FLY NUT loading - fixed offset format (FUN_0040c3cc case 6)
+ // ===== Handler 7: FLY NUT Loading (Space Flight) =====
+ // FUN_0040c3cc case 6: par4 determines FLY sprite slot
+ bool isHandler7FLY = (_rebelHandler == 7 && (par4 == 1 || par4 == 2 || par4 == 3 || par4 == 11));
+ if (isHandler7FLY && remaining >= 14) {
+ if (loadHandler7FlySprites(b, remaining, par4)) {
+ b.seek(startPos);
+ return;
+ }
+ b.seek(startPos);
+ }
+
+ // ===== Sound Loading (par3 21-47) =====
+ if (par3 >= 21 && par3 <= 47) {
+ debug("Rebel2 Opcode 8: Sound loading subcase %d (not implemented)", par3);
+ // TODO: Implement sound loading via FUN_004118df equivalent
+ return;
+ }
+
+ // ===== Scan for embedded ANIM data =====
+ // Remaining handlers require finding ANIM tag in the stream
+ debug("Rebel2 Opcode 8: Scanning for ANIM tag (startPos=%lld remaining=%lld)",
+ (long long)startPos, (long long)remaining);
+
+ if (remaining <= 0) {
+ return;
+ }
+
+ int scanSize = (int)MIN<int64>(remaining, 65536);
+ byte *scanBuf = (byte *)malloc(scanSize);
+ if (!scanBuf) {
+ return;
+ }
+
+ int bytesRead = b.read(scanBuf, scanSize);
+ debug("Rebel2 Opcode 8: Read %d bytes for ANIM scan", bytesRead);
+
+ // Find ANIM tag
+ int animOffset = -1;
+ for (int i = 0; i + 8 <= bytesRead; ++i) {
+ if (READ_BE_UINT32(scanBuf + i) == MKTAG('A','N','I','M')) {
+ animOffset = i;
+ debug("Rebel2 Opcode 8: Found ANIM at offset %d", i);
+ break;
+ }
+ }
+
+ if (animOffset < 0) {
+ debug("Rebel2 Opcode 8: No ANIM tag found");
+ free(scanBuf);
+ b.seek(startPos);
+ return;
+ }
+
+ // Extract ANIM data
+ uint32 animReportedSize = READ_BE_UINT32(scanBuf + animOffset + 4);
+ int32 animDataSize = (int)MIN<int64>((int64)animReportedSize + 8, remaining - animOffset);
+ if (animDataSize <= 0) {
+ free(scanBuf);
+ b.seek(startPos);
+ return;
+ }
+
+ byte *animData = (byte *)malloc(animDataSize);
+ if (!animData) {
+ free(scanBuf);
+ b.seek(startPos);
+ return;
+ }
+
+ b.seek(startPos + animOffset);
+ b.read(animData, animDataSize);
+
+ bool handled = false;
+
+ // ===== Handler 0x26/0x19: Turret HUD Overlays =====
+ // FUN_00407fcb case 8: par3 1-4 for HUD NUT loading
+ if (!handled && (_rebelHandler == 0x26 || _rebelHandler == 0x19)) {
+ if (par3 >= 1 && par3 <= 4) {
+ handled = loadTurretHudOverlay(animData, animDataSize, par3);
+ }
+ }
+
+ // ===== Handler 8: POV Ship Sprites or Background =====
+ // FUN_00401234 case 6: par3 for POV NUTs, par4=5 for background
+ if (!handled && _rebelHandler == 8) {
+ // Check for background loading first (par4=5)
+ if (par4 == 5) {
+ handled = loadLevel2Background(animData, animDataSize, renderBitmap);
+ }
+ // Check for POV NUT sprites
+ else if (par3 == 1 || par3 == 3 || par3 == 6 || par3 == 7) {
+ handled = loadHandler8ShipSprites(animData, animDataSize, par3);
+ }
+ }
+
+ // ===== Handler 25 (0x19): Also supports Level 2 background =====
+ if (!handled && _rebelHandler == 25 && par4 == 5) {
+ handled = loadLevel2Background(animData, animDataSize, renderBitmap);
+ }
+
+ // ===== Fallback: Embedded SAN HUD overlays =====
+ // For other cases, load as embedded SAN frame to HUD overlay slots
+ if (!handled) {
+ // Skip high-res data (par3 == 2, 4)
+ if (par3 == 2 || par3 == 4) {
+ debug("Rebel2 Opcode 8: Skipping high-res HUD par3=%d", par3);
+ handled = true;
+ } else {
+ // Determine userId: Handler 0x19 uses par3, others use par4
+ // Heuristic: if par3 is valid GRD range (1-13) and par4 is invalid, prefer par3
+ int userId;
+ bool usePar3 = (_rebelHandler == 0x19);
+ if (!usePar3 && par3 >= 1 && par3 <= 13 && (par4 <= 0 || par4 >= 1000)) {
+ usePar3 = true;
+ }
+ userId = usePar3 ? par3 : par4;
+
+ // Skip audio tracks (userId >= 1000)
+ if (userId > 0 && userId < 1000) {
+ debug("Rebel2 Opcode 8: Loading embedded SAN HUD userId=%d (handler=%d par3=%d par4=%d)",
+ userId, _rebelHandler, par3, par4);
+ loadEmbeddedSan(userId, animData, animDataSize, renderBitmap);
+ handled = true;
+ }
+ }
+ }
+
+ if (!handled) {
+ debug("Rebel2 Opcode 8: Unhandled case - handler=%d par3=%d par4=%d", _rebelHandler, par3, par4);
+ }
+
+ free(animData);
+ free(scanBuf);
+ b.seek(startPos);
+}
+
+// ======================= Opcode 8 Helper Functions =======================
+// These helper functions are extracted from the original monolithic iactRebel2Opcode8
+// to improve code readability and match the retail FUN_* function structure.
+
+bool InsaneRebel2::loadHandler7FlySprites(Common::SeekableReadStream &b, int64 remaining, int16 par4) {
+ // Handler 7 FLY NUT loading - FUN_0040c3cc case 6 (opcode 8)
// IACT structure after par1-par4 (we're at offset +8):
// +0-5 (6 bytes): additional header
// +6-9 (4 bytes): NUT data size (little-endian)
// +10+: NUT data
- // Assembly: param_5[7] = size at offset 14, param_5+9 = data at offset 18
- // Since we've read 8 bytes (par1-par4), that's offset +6 and +10 from current pos
//
- // IMPORTANT: The assembly switches on param_5[3] which is the 4th short (bytes 6-7)
- // In SMUSH handleIACT, this corresponds to userId (par4), NOT unknown (par3)!
- // So we use par4 for the FLY slot selection.
- bool isHandler7FLY = (_rebelHandler == 7 && (par4 == 1 || par4 == 2 || par4 == 3 || par4 == 11));
+ // par4 values (param_5[3] - 1 in assembly):
+ // 1 -> case 0: FLY001 - Ship direction sprites (DAT_0047fee8)
+ // 2 -> case 1: FLY003 - Targeting overlay (DAT_0047fef8)
+ // 3 -> case 2: FLY002 - Laser fire sprites (DAT_0047fef0)
+ // 11 -> case 10: FLY004 - High-res alternative (DAT_0047ff00)
- if (isHandler7FLY && remaining >= 14) {
- // Read additional header and size from fixed offset
- // Based on FUN_0040c3cc assembly:
- // param_5[7] = size at offset 14 (we're at offset 8, so read 6+4 bytes)
- // param_5+9 = data at offset 18
- byte header[10];
- if (b.read(header, 10) == 10) {
- // Debug: dump all header bytes
- debug("Rebel2 Opcode 8 Handler7: header bytes: %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X",
- header[0], header[1], header[2], header[3], header[4],
- header[5], header[6], header[7], header[8], header[9]);
-
- // Size is at offset 14 from IACT start = bytes 6-9 of our header buffer
- // Try both endianness
- uint32 nutSizeLE = READ_LE_UINT32(header + 6);
- uint32 nutSizeBE = READ_BE_UINT32(header + 6);
- debug("Rebel2 Opcode 8 Handler7: par4=%d sizesLE=%u sizeBE=%u remaining=%lld",
- par4, nutSizeLE, nutSizeBE, (long long)remaining);
-
- // The assembly uses direct memory read on x86 which is LE
- uint32 nutSize = nutSizeLE;
-
- if (nutSize > 0 && nutSize <= (uint32)(remaining - 10)) {
- byte *nutData = (byte *)malloc(nutSize);
- if (nutData) {
- int bytesRead = b.read(nutData, nutSize);
- debug("Rebel2 Opcode 8 Handler7: Read %d/%u bytes of NUT data, first 16: %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X",
- bytesRead, nutSize,
- bytesRead > 0 ? nutData[0] : 0, bytesRead > 1 ? nutData[1] : 0,
- bytesRead > 2 ? nutData[2] : 0, bytesRead > 3 ? nutData[3] : 0,
- bytesRead > 4 ? nutData[4] : 0, bytesRead > 5 ? nutData[5] : 0,
- bytesRead > 6 ? nutData[6] : 0, bytesRead > 7 ? nutData[7] : 0,
- bytesRead > 8 ? nutData[8] : 0, bytesRead > 9 ? nutData[9] : 0,
- bytesRead > 10 ? nutData[10] : 0, bytesRead > 11 ? nutData[11] : 0,
- bytesRead > 12 ? nutData[12] : 0, bytesRead > 13 ? nutData[13] : 0,
- bytesRead > 14 ? nutData[14] : 0, bytesRead > 15 ? nutData[15] : 0);
-
- // Verify we read the expected amount
- if (bytesRead != (int)nutSize) {
- warning("Rebel2 Opcode 8 Handler7: Short read! Got %d expected %u", bytesRead, nutSize);
- }
+ if (remaining < 14) {
+ return false;
+ }
- // Verify ANIM header
- if (bytesRead >= 8) {
- uint32 animTag = READ_BE_UINT32(nutData);
- uint32 animSize = READ_BE_UINT32(nutData + 4);
- debug("Rebel2 Opcode 8 Handler7: ANIM tag=%08X size=%u (expected %08X, size should be ~%d)",
- animTag, animSize, MKTAG('A','N','I','M'), bytesRead - 8);
- if (animTag != MKTAG('A','N','I','M')) {
- warning("Rebel2 Opcode 8 Handler7: No ANIM tag! Data may be corrupted");
- }
- if ((int32)animSize > bytesRead - 8) {
- warning("Rebel2 Opcode 8 Handler7: ANIM size %u exceeds data %d", animSize, bytesRead - 8);
- }
- }
+ // Read additional header and size from fixed offset
+ byte header[10];
+ if (b.read(header, 10) != 10) {
+ return false;
+ }
- // Try loading as NUT
- NutRenderer *newNut = new NutRenderer(_vm, nutData, bytesRead);
- if (newNut && newNut->getNumChars() > 0) {
- debug("Rebel2 Opcode 8 Handler7: Loaded FLY NUT par4=%d with %d sprites",
- par4, newNut->getNumChars());
-
- // Switch on par4 (userId) - matches assembly param_5[3]
- switch (par4) {
- case 1: // FLY001 - Ship direction sprites (35 frames)
- delete _flyShipSprite;
- _flyShipSprite = newNut;
- debug("Rebel2 Opcode 8: _flyShipSprite set with %d sprites", newNut->getNumChars());
- break;
- case 2: // FLY003 - Targeting overlay
- delete _flyTargetSprite;
- _flyTargetSprite = newNut;
- break;
- case 3: // FLY002 - Laser fire sprites
- delete _flyLaserSprite;
- _flyLaserSprite = newNut;
- break;
- case 11: // FLY004 - High-res alternative
- delete _flyHiResSprite;
- _flyHiResSprite = newNut;
- break;
- default:
- delete newNut;
- break;
- }
- } else {
- debug("Rebel2 Opcode 8 Handler7: NUT load failed for par4=%d", par4);
- delete newNut;
- }
- free(nutData);
- }
- }
+ debug("Rebel2 loadHandler7FlySprites: header bytes: %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X",
+ header[0], header[1], header[2], header[3], header[4],
+ header[5], header[6], header[7], header[8], header[9]);
+
+ // Size is at offset 14 from IACT start = bytes 6-9 of our header buffer
+ uint32 nutSize = READ_LE_UINT32(header + 6);
+ debug("Rebel2 loadHandler7FlySprites: par4=%d nutSize=%u remaining=%lld",
+ par4, nutSize, (long long)remaining);
+
+ if (nutSize == 0 || nutSize > (uint32)(remaining - 10)) {
+ return false;
+ }
+
+ byte *nutData = (byte *)malloc(nutSize);
+ if (!nutData) {
+ return false;
+ }
+
+ int bytesRead = b.read(nutData, nutSize);
+ debug("Rebel2 loadHandler7FlySprites: Read %d/%u bytes, first 16: %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X",
+ bytesRead, nutSize,
+ bytesRead > 0 ? nutData[0] : 0, bytesRead > 1 ? nutData[1] : 0,
+ bytesRead > 2 ? nutData[2] : 0, bytesRead > 3 ? nutData[3] : 0,
+ bytesRead > 4 ? nutData[4] : 0, bytesRead > 5 ? nutData[5] : 0,
+ bytesRead > 6 ? nutData[6] : 0, bytesRead > 7 ? nutData[7] : 0,
+ bytesRead > 8 ? nutData[8] : 0, bytesRead > 9 ? nutData[9] : 0,
+ bytesRead > 10 ? nutData[10] : 0, bytesRead > 11 ? nutData[11] : 0,
+ bytesRead > 12 ? nutData[12] : 0, bytesRead > 13 ? nutData[13] : 0,
+ bytesRead > 14 ? nutData[14] : 0, bytesRead > 15 ? nutData[15] : 0);
+
+ if (bytesRead != (int)nutSize) {
+ warning("Rebel2 loadHandler7FlySprites: Short read! Got %d expected %u", bytesRead, nutSize);
+ free(nutData);
+ return false;
+ }
+
+ // Verify ANIM header
+ if (bytesRead >= 8) {
+ uint32 animTag = READ_BE_UINT32(nutData);
+ if (animTag != MKTAG('A','N','I','M')) {
+ warning("Rebel2 loadHandler7FlySprites: No ANIM tag! Data may be corrupted");
+ free(nutData);
+ return false;
}
- b.seek(startPos);
- return;
}
- // For non-Handler7 or non-FLY cases, scan for ANIM tag
- debug("Rebel2 Opcode 8: startPos=%lld chunkSize=%d remaining=%lld", (long long)startPos, chunkSize, (long long)remaining);
- if (remaining > 0) {
- int scanSize = (int)MIN<int64>(remaining, 65536);
- byte *scanBuf = (byte *)malloc(scanSize);
- if (scanBuf) {
- int bytesRead = b.read(scanBuf, scanSize);
- debug("Rebel2 Opcode 8: Read %d bytes, first 16: %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X",
- bytesRead,
- bytesRead > 0 ? scanBuf[0] : 0, bytesRead > 1 ? scanBuf[1] : 0,
- bytesRead > 2 ? scanBuf[2] : 0, bytesRead > 3 ? scanBuf[3] : 0,
- bytesRead > 4 ? scanBuf[4] : 0, bytesRead > 5 ? scanBuf[5] : 0,
- bytesRead > 6 ? scanBuf[6] : 0, bytesRead > 7 ? scanBuf[7] : 0,
- bytesRead > 8 ? scanBuf[8] : 0, bytesRead > 9 ? scanBuf[9] : 0,
- bytesRead > 10 ? scanBuf[10] : 0, bytesRead > 11 ? scanBuf[11] : 0,
- bytesRead > 12 ? scanBuf[12] : 0, bytesRead > 13 ? scanBuf[13] : 0,
- bytesRead > 14 ? scanBuf[14] : 0, bytesRead > 15 ? scanBuf[15] : 0);
-
- // Look for ANIM tag (embedded SAN or NUT)
- for (int i = 0; i + 8 <= bytesRead; ++i) {
- if (READ_BE_UINT32(scanBuf + i) == MKTAG('A','N','I','M')) {
- debug("Rebel2 Opcode 8: Found ANIM at offset %d", i);
- int64 animStreamPos = startPos + i;
- uint32 animReportedSize = READ_BE_UINT32(scanBuf + i + 4);
- // Limit toCopy to remaining data in this chunk
- int32 toCopy = (int)MIN<int64>((int64)animReportedSize + 8, remaining - i);
- if (toCopy > 0) {
- byte *animData = (byte *)malloc(toCopy);
- if (animData) {
- b.seek(animStreamPos);
- b.read(animData, toCopy);
-
- // Handler-dependent NUT loading for opcode 8:
- //
- // Handler 0x26/0x19 (turret) - FUN_00407fcb:
- // par3 == 1: Low-res primary HUD (GRD001) -> DAT_0047fe78
- // par3 == 2: High-res primary HUD (skip in low-res mode)
- // par3 == 3: Low-res secondary HUD (GRD010) -> DAT_0047fe80
- // par3 == 4: High-res secondary HUD (skip in low-res mode)
- //
- // Handler 8 (vehicle) - FUN_00401234:
- // par3 == 1: POV001 - Primary ship sprite -> DAT_0047e010
- // par3 == 3: POV004 - Secondary ship sprite -> DAT_0047e028
- // par3 == 6: POV002 - Ship overlay 1 -> DAT_0047e020
- // par3 == 7: POV003 - Ship overlay 2 -> DAT_0047e018
- //
- // ScummVM runs at 320x200 (low-res), so turret HUD uses par3 == 1/3 only.
- bool loadedAsNut = false;
-
- // Handler 0x26/0x19 (turret modes): Load HUD overlays
- if (_rebelHandler == 0x26 || _rebelHandler == 0x19) {
- if (par3 == 1) {
- // Low-res primary HUD overlay
- NutRenderer *newNut = new NutRenderer(_vm, animData, toCopy);
- if (newNut && newNut->getNumChars() > 0) {
- debug("Rebel2 Opcode 8: Loaded turret HUD NUT par3=%d with %d sprites (handler=0x%x, low-res)",
- par3, newNut->getNumChars(), _rebelHandler);
- delete _hudOverlayNut;
- _hudOverlayNut = newNut;
- loadedAsNut = true;
- } else {
- debug("Rebel2 Opcode 8: Turret HUD NUT load failed for par3=%d", par3);
- delete newNut;
- }
- } else if (par3 == 3) {
- // Low-res secondary HUD overlay
- NutRenderer *newNut = new NutRenderer(_vm, animData, toCopy);
- if (newNut && newNut->getNumChars() > 0) {
- debug("Rebel2 Opcode 8: Loaded turret HUD2 NUT par3=%d with %d sprites (handler=0x%x, low-res)",
- par3, newNut->getNumChars(), _rebelHandler);
- delete _hudOverlay2Nut;
- _hudOverlay2Nut = newNut;
- loadedAsNut = true;
- } else {
- debug("Rebel2 Opcode 8: Turret HUD2 NUT load failed for par3=%d", par3);
- delete newNut;
- }
- } else if (par3 == 2 || par3 == 4) {
- // High-res versions - skip in low-res mode
- debug("Rebel2 Opcode 8: Skipping high-res HUD par3=%d (running in low-res mode)", par3);
- loadedAsNut = true;
- }
- }
+ // Load as NUT
+ NutRenderer *newNut = new NutRenderer(_vm, nutData, bytesRead);
+ if (!newNut || newNut->getNumChars() <= 0) {
+ debug("Rebel2 loadHandler7FlySprites: NUT load failed for par4=%d", par4);
+ delete newNut;
+ free(nutData);
+ return false;
+ }
- // Handler 8 (Third-Person Vehicle): Load ship sprites
- if (!loadedAsNut && _rebelHandler == 8) {
- bool isHandler8Par3 = (par3 == 1 || par3 == 3 || par3 == 6 || par3 == 7);
- if (isHandler8Par3) {
- NutRenderer *newNut = new NutRenderer(_vm, animData, toCopy);
- if (newNut && newNut->getNumChars() > 0) {
- debug("Rebel2 Opcode 8: Loaded ship NUT par3=%d with %d sprites (handler=%d)",
- par3, newNut->getNumChars(), _rebelHandler);
- loadedAsNut = true;
-
- switch (par3) {
- case 1: // POV001 - Primary ship sprite
- delete _shipSprite;
- _shipSprite = newNut;
- break;
- case 3: // POV004 - Secondary ship sprite
- delete _shipSprite2;
- _shipSprite2 = newNut;
- break;
- case 6: // POV002 - Ship overlay 1
- delete _shipOverlay1;
- _shipOverlay1 = newNut;
- break;
- case 7: // POV003 - Ship overlay 2
- delete _shipOverlay2;
- _shipOverlay2 = newNut;
- break;
- default:
- delete newNut;
- loadedAsNut = false;
- break;
- }
- } else {
- debug("Rebel2 Opcode 8: Ship NUT load failed for par3=%d", par3);
- delete newNut;
- }
- }
- }
+ debug("Rebel2 loadHandler7FlySprites: Loaded FLY NUT par4=%d with %d sprites",
+ par4, newNut->getNumChars());
- // Handler 8/25 (Level 2): Load background
- // par4=5 contains the background image embedded as ANIM with FOBJ codec 3
- // FUN_00401234 case 6 switch case 5: Creates 320x200 buffer (DAT_0047e030)
- // Handler 8 = third-person vehicle, Handler 25 (0x19) = turret/mixed view
- // Level 2 uses handler 25 but background loading is the same mechanism
- if (!loadedAsNut && (_rebelHandler == 8 || _rebelHandler == 25) && par4 == 5) {
- debug("Rebel2 Opcode 8: Loading Level 2 background (par4=5, animSize=%d)", toCopy);
-
- // Allocate background buffer if needed (320x200 = 64000 bytes)
- if (_level2Background == nullptr) {
- _level2Background = (byte *)malloc(320 * 200);
- if (_level2Background) {
- memset(_level2Background, 0, 320 * 200);
- }
- }
+ // Assign to appropriate slot based on par4 (matches FUN_0040c3cc case 6 switch)
+ bool assigned = true;
+ switch (par4) {
+ case 1: // FLY001 - Ship direction sprites (35 frames)
+ delete _flyShipSprite;
+ _flyShipSprite = newNut;
+ debug("Rebel2: _flyShipSprite set with %d sprites", newNut->getNumChars());
+ break;
+ case 2: // FLY003 - Targeting overlay
+ delete _flyTargetSprite;
+ _flyTargetSprite = newNut;
+ break;
+ case 3: // FLY002 - Laser fire sprites
+ delete _flyLaserSprite;
+ _flyLaserSprite = newNut;
+ break;
+ case 11: // FLY004 - High-res alternative
+ delete _flyHiResSprite;
+ _flyHiResSprite = newNut;
+ break;
+ default:
+ delete newNut;
+ assigned = false;
+ break;
+ }
- if (_level2Background) {
- // Parse embedded ANIM to find FOBJ
- // Structure: ANIM tag at offset 0, AHDR, then FRME with FOBJ
- int animOffset = 0;
- if (toCopy >= 8 && READ_BE_UINT32(animData) == MKTAG('A','N','I','M')) {
- uint32 animSize = READ_BE_UINT32(animData + 4);
- debug("Rebel2 Opcode 8: Found ANIM tag, size=%u", animSize);
-
- // Skip ANIM header (8 bytes) + AHDR chunk
- // AHDR starts at offset 8, size is at offset 12 (big endian)
- if (toCopy >= 16 && READ_BE_UINT32(animData + 8) == MKTAG('A','H','D','R')) {
- uint32 ahdrSize = READ_BE_UINT32(animData + 12);
- animOffset = 8 + 8 + ahdrSize; // After ANIM tag + AHDR
- debug("Rebel2 Opcode 8: AHDR size=%u, FRME expected at offset %d", ahdrSize, animOffset);
- }
- }
-
- // Look for FRME containing FOBJ
- bool foundBackground = false;
- for (int scanPos = animOffset; scanPos + 16 < toCopy && !foundBackground; scanPos++) {
- if (READ_BE_UINT32(animData + scanPos) == MKTAG('F','R','M','E')) {
- // Found FRME, look for FOBJ inside
- int frmeSize = READ_BE_UINT32(animData + scanPos + 4);
- debug("Rebel2 Opcode 8: Found FRME at %d, size=%d", scanPos, frmeSize);
-
- for (int fobjPos = scanPos + 8; fobjPos + 18 < scanPos + 8 + frmeSize && fobjPos + 18 < toCopy; fobjPos++) {
- if (READ_BE_UINT32(animData + fobjPos) == MKTAG('F','O','B','J')) {
- uint32 fobjSize = READ_BE_UINT32(animData + fobjPos + 4);
- byte *fobjData = animData + fobjPos + 8;
-
- // FOBJ header: codec(2), x(2), y(2), w(2), h(2)
- int16 codec = READ_LE_INT16(fobjData);
- int16 fobjX = READ_LE_INT16(fobjData + 2);
- int16 fobjY = READ_LE_INT16(fobjData + 4);
- int16 fobjW = READ_LE_INT16(fobjData + 6);
- int16 fobjH = READ_LE_INT16(fobjData + 8);
-
- debug("Rebel2 Opcode 8: Found FOBJ at %d: codec=%d pos=(%d,%d) size=%dx%d dataSize=%u",
- fobjPos, codec, fobjX, fobjY, fobjW, fobjH, fobjSize);
-
- // Decode codec 3 (RLE) into background buffer
- // Codec 3 uses bomp RLE format with line-by-line structure
- // FOBJ header is 14 bytes: codec(2), x(2), y(2), w(2), h(2), unk(2), unk(2)
- if (codec == 3 && fobjW > 0 && fobjH > 0 && fobjW <= 320 && fobjH <= 200) {
- byte *rleData = fobjData + 14; // Skip full 14-byte FOBJ header
-
- // Use the existing smushDecodeRLE function from codec1.cpp
- // This properly decodes BOMP RLE with line size headers
- smushDecodeRLE(_level2Background, rleData, fobjX, fobjY, fobjW, fobjH, 320);
-
- debug("Rebel2 Opcode 8: Decoded Level 2 background (%dx%d at %d,%d)",
- fobjW, fobjH, fobjX, fobjY);
- _level2BackgroundLoaded = true;
- foundBackground = true;
-
- // Draw background to render bitmap immediately
- if (renderBitmap) {
- for (int by = 0; by < 200; by++) {
- memcpy(renderBitmap + by * 320, _level2Background + by * 320, 320);
- }
- debug("Rebel2 Opcode 8: Copied Level 2 background to renderBitmap");
- }
- }
- break; // Found FOBJ, stop searching
- }
- }
- break; // Found FRME, stop searching
- }
- }
-
- if (!foundBackground) {
- debug("Rebel2 Opcode 8: Failed to find/decode background FOBJ");
- }
- }
- loadedAsNut = true; // Mark as handled even if decode failed
- }
+ free(nutData);
+ return assigned;
+}
- if (!loadedAsNut) {
- // Skip high-res turret HUD data (par3 == 2, 4) regardless of handler
- // ScummVM runs at 320x200, so we only want low-res data (par3 == 1, 3)
- debug("Rebel2 Opcode 8: Fallback path - handler=%d par3=%d par4=%d animSize=%d",
- _rebelHandler, par3, par4, toCopy);
- if (par3 == 2 || par3 == 4) {
- debug("Rebel2 Opcode 8: SKIPPING high-res HUD par3=%d (low-res mode)", par3);
- // Don't load anything - skip this data entirely
- } else {
- // Load as embedded SAN (HUD overlays)
- // The userId parameter varies by handler type:
- //
- // Handler 0x26 (turret): Uses par3 for HUD slot (GRD files)
- // par3=1: GRD001 (Primary HUD overlay)
- // par3=3: GRD010 (Secondary HUD)
- //
- // Handler 7 (space flight): Uses par4 for userId (FLY files handled above)
- // Other handlers: Use par4 for userId
- //
- // par4 values seen in Level 3:
- // 1000 = audio track
- // 1-11 = HUD overlay slots
- //
- // Note: Handler may not be set yet if opcode 8 arrives before opcode 6
- // Use heuristics: if par3 is in valid GRD range (1-13) and par4 is >= 1000
- // or invalid, prefer par3
- int userId;
- // Handler 0x19 uses par3; Handler 0x26 and others use par4
- bool usePar3 = (_rebelHandler == 0x19);
-
- // Heuristic: if par3 is in typical GRD slot range (1-13) and par4 is
- // out of range (0 or >= 1000), use par3 as it's likely a turret/GRD case
- if (!usePar3 && par3 >= 1 && par3 <= 13 && (par4 <= 0 || par4 >= 1000)) {
- usePar3 = true;
- debug("Rebel2 Opcode 8: Using par3 heuristic (par3=%d, par4=%d)", par3, par4);
- }
-
- userId = usePar3 ? par3 : par4;
-
- // Audio tracks (userId >= 1000) are handled separately, skip them
- if (userId > 0 && userId < 1000) {
- debug("Rebel2 Opcode 8: Loading embedded SAN as HUD userId=%d (handler=%d, par3=%d, par4=%d)",
- userId, _rebelHandler, par3, par4);
- loadEmbeddedSan(userId, animData, toCopy, renderBitmap);
- }
- }
+bool InsaneRebel2::loadTurretHudOverlay(byte *animData, int32 size, int16 par3) {
+ // Handler 0x26/0x19 turret HUD overlay loading - FUN_00407fcb case 8
+ // Resolution-dependent loading:
+ // par3 == 1: Low-res primary HUD (DAT_0047fe78 / _hudOverlayNut)
+ // par3 == 2: High-res primary HUD (skip in 320x200 mode)
+ // par3 == 3: Low-res secondary HUD (DAT_0047fe80 / _hudOverlay2Nut)
+ // par3 == 4: High-res secondary HUD (skip in 320x200 mode)
+
+ if (!animData || size <= 0) {
+ return false;
+ }
+
+ // ScummVM runs at 320x200 (low-res), skip high-res data
+ if (par3 == 2 || par3 == 4) {
+ debug("Rebel2 loadTurretHudOverlay: Skipping high-res HUD par3=%d (running in low-res mode)", par3);
+ return true; // Successfully "handled" by skipping
+ }
+
+ if (par3 != 1 && par3 != 3) {
+ return false; // Not a turret HUD slot
+ }
+
+ NutRenderer *newNut = new NutRenderer(_vm, animData, size);
+ if (!newNut || newNut->getNumChars() <= 0) {
+ debug("Rebel2 loadTurretHudOverlay: NUT load failed for par3=%d", par3);
+ delete newNut;
+ return false;
+ }
+
+ debug("Rebel2 loadTurretHudOverlay: Loaded turret HUD NUT par3=%d with %d sprites",
+ par3, newNut->getNumChars());
+
+ if (par3 == 1) {
+ // Low-res primary HUD overlay
+ delete _hudOverlayNut;
+ _hudOverlayNut = newNut;
+ } else { // par3 == 3
+ // Low-res secondary HUD overlay
+ delete _hudOverlay2Nut;
+ _hudOverlay2Nut = newNut;
+ }
+
+ return true;
+}
+
+bool InsaneRebel2::loadHandler8ShipSprites(byte *animData, int32 size, int16 par3) {
+ // Handler 8 ship POV NUT loading - FUN_00401234 case 6 (opcode 8)
+ // par3 values:
+ // 1: POV001 - Primary ship sprite (DAT_0047e010 / _shipSprite)
+ // 3: POV004 - Secondary ship sprite (DAT_0047e028 / _shipSprite2)
+ // 6: POV002 - Ship overlay 1 (DAT_0047e020 / _shipOverlay1)
+ // 7: POV003 - Ship overlay 2 (DAT_0047e018 / _shipOverlay2)
+
+ if (!animData || size <= 0) {
+ return false;
+ }
+
+ // Only handle valid POV sprite slots
+ if (par3 != 1 && par3 != 3 && par3 != 6 && par3 != 7) {
+ return false;
+ }
+
+ NutRenderer *newNut = new NutRenderer(_vm, animData, size);
+ if (!newNut || newNut->getNumChars() <= 0) {
+ debug("Rebel2 loadHandler8ShipSprites: NUT load failed for par3=%d", par3);
+ delete newNut;
+ return false;
+ }
+
+ debug("Rebel2 loadHandler8ShipSprites: Loaded ship NUT par3=%d with %d sprites",
+ par3, newNut->getNumChars());
+
+ switch (par3) {
+ case 1: // POV001 - Primary ship sprite
+ delete _shipSprite;
+ _shipSprite = newNut;
+ break;
+ case 3: // POV004 - Secondary ship sprite
+ delete _shipSprite2;
+ _shipSprite2 = newNut;
+ break;
+ case 6: // POV002 - Ship overlay 1
+ delete _shipOverlay1;
+ _shipOverlay1 = newNut;
+ break;
+ case 7: // POV003 - Ship overlay 2
+ delete _shipOverlay2;
+ _shipOverlay2 = newNut;
+ break;
+ default:
+ delete newNut;
+ return false;
+ }
+
+ return true;
+}
+
+bool InsaneRebel2::loadLevel2Background(byte *animData, int32 size, byte *renderBitmap) {
+ // Level 2 background loading from embedded ANIM - FUN_00401234 case 5
+ // par4=5 contains the background image embedded as ANIM with FOBJ codec 3
+ // Creates 320x200 buffer (DAT_0047e030 / _level2Background)
+
+ if (!animData || size < 8) {
+ return false;
+ }
+
+ debug("Rebel2 loadLevel2Background: Loading Level 2 background (animSize=%d)", size);
+
+ // Allocate background buffer if needed (320x200 = 64000 bytes)
+ if (_level2Background == nullptr) {
+ _level2Background = (byte *)malloc(320 * 200);
+ if (!_level2Background) {
+ return false;
+ }
+ memset(_level2Background, 0, 320 * 200);
+ }
+
+ // Parse embedded ANIM to find FOBJ
+ // Structure: ANIM tag at offset 0, AHDR, then FRME with FOBJ
+ int animOffset = 0;
+ if (READ_BE_UINT32(animData) == MKTAG('A','N','I','M')) {
+ uint32 animSize = READ_BE_UINT32(animData + 4);
+ debug("Rebel2 loadLevel2Background: Found ANIM tag, size=%u", animSize);
+
+ // Skip ANIM header (8 bytes) + AHDR chunk
+ if (size >= 16 && READ_BE_UINT32(animData + 8) == MKTAG('A','H','D','R')) {
+ uint32 ahdrSize = READ_BE_UINT32(animData + 12);
+ animOffset = 8 + 8 + ahdrSize; // After ANIM tag + AHDR
+ debug("Rebel2 loadLevel2Background: AHDR size=%u, FRME expected at offset %d", ahdrSize, animOffset);
+ }
+ }
+
+ // Look for FRME containing FOBJ
+ bool foundBackground = false;
+ for (int scanPos = animOffset; scanPos + 16 < size && !foundBackground; scanPos++) {
+ if (READ_BE_UINT32(animData + scanPos) == MKTAG('F','R','M','E')) {
+ int frmeSize = READ_BE_UINT32(animData + scanPos + 4);
+ debug("Rebel2 loadLevel2Background: Found FRME at %d, size=%d", scanPos, frmeSize);
+
+ for (int fobjPos = scanPos + 8; fobjPos + 18 < scanPos + 8 + frmeSize && fobjPos + 18 < size; fobjPos++) {
+ if (READ_BE_UINT32(animData + fobjPos) == MKTAG('F','O','B','J')) {
+ byte *fobjData = animData + fobjPos + 8;
+
+ // FOBJ header: codec(2), x(2), y(2), w(2), h(2)
+ int16 codec = READ_LE_INT16(fobjData);
+ int16 fobjX = READ_LE_INT16(fobjData + 2);
+ int16 fobjY = READ_LE_INT16(fobjData + 4);
+ int16 fobjW = READ_LE_INT16(fobjData + 6);
+ int16 fobjH = READ_LE_INT16(fobjData + 8);
+
+ debug("Rebel2 loadLevel2Background: Found FOBJ: codec=%d pos=(%d,%d) size=%dx%d",
+ codec, fobjX, fobjY, fobjW, fobjH);
+
+ // Decode codec 3 (RLE) into background buffer
+ if (codec == 3 && fobjW > 0 && fobjH > 0 && fobjW <= 320 && fobjH <= 200) {
+ byte *rleData = fobjData + 14; // Skip full 14-byte FOBJ header
+ smushDecodeRLE(_level2Background, rleData, fobjX, fobjY, fobjW, fobjH, 320);
+
+ debug("Rebel2 loadLevel2Background: Decoded Level 2 background (%dx%d at %d,%d)",
+ fobjW, fobjH, fobjX, fobjY);
+ _level2BackgroundLoaded = true;
+ foundBackground = true;
+
+ // Copy to render bitmap immediately if provided
+ if (renderBitmap) {
+ for (int by = 0; by < 200; by++) {
+ memcpy(renderBitmap + by * 320, _level2Background + by * 320, 320);
}
- free(animData);
+ debug("Rebel2 loadLevel2Background: Copied to renderBitmap");
}
}
- b.seek(startPos);
- free(scanBuf);
- return;
+ break;
}
}
-
- b.seek(startPos);
- free(scanBuf);
+ break;
}
}
- // Handle sound loading cases (par3 21-47)
- if (par3 >= 21 && par3 <= 47) {
- debug("Rebel2 Opcode 8: Sound loading subcase %d (not implemented)", par3);
- // TODO: Implement sound loading when needed
+ if (!foundBackground) {
+ debug("Rebel2 loadLevel2Background: Failed to find/decode background FOBJ");
}
+
+ return foundBackground;
}
void InsaneRebel2::iactRebel2Opcode9(byte *renderBitmap, Common::SeekableReadStream &b, int16 par2, int16 par3, int16 par4) {
Commit: 5c0b23415fb3736ac5a2d6ee0ecd5e5138835aeb
https://github.com/scummvm/scummvm/commit/5c0b23415fb3736ac5a2d6ee0ecd5e5138835aeb
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:08:57+02:00
Commit Message:
SCUMM: RA2: Reduce shared SMUSH dependencies
Changed paths:
engines/scumm/insane/insane.h
engines/scumm/insane/insane_rebel.cpp
engines/scumm/insane/insane_rebel.h
engines/scumm/smush/smush_player.cpp
diff --git a/engines/scumm/insane/insane.h b/engines/scumm/insane/insane.h
index d4d124184c5..85647829a36 100644
--- a/engines/scumm/insane/insane.h
+++ b/engines/scumm/insane/insane.h
@@ -460,10 +460,6 @@ public:
public:
- bool virtual shouldSkipFrameUpdate(int left, int top, int width, int height) {
- return false;
- };
-
void virtual loadEmbeddedSan(int userId, byte *animData, int32 size, byte *renderBitmap) {
// Nothing by default
};
diff --git a/engines/scumm/insane/insane_rebel.cpp b/engines/scumm/insane/insane_rebel.cpp
index 86ab08d07a9..66afeef366a 100644
--- a/engines/scumm/insane/insane_rebel.cpp
+++ b/engines/scumm/insane/insane_rebel.cpp
@@ -642,44 +642,6 @@ int32 InsaneRebel2::processMouse() {
return buttons;
}
-bool InsaneRebel2::shouldSkipFrameUpdate(int left, int top, int width, int height) {
- // Only check for Rebel2
- if (_vm->_game.id != GID_REBEL2) {
- return false;
- }
-
- Common::Rect updateRect(left, top, left + width, top + height);
- int updateArea = width * height;
-
- // Check if this update region significantly overlaps with any destroyed enemy
- Common::List<enemy>::iterator it;
- for (it = _enemies.begin(); it != _enemies.end(); ++it) {
- if (it->destroyed) {
- // Calculate the intersection of the update rect and enemy rect
- Common::Rect enemyRect = it->rect;
-
- if (updateRect.intersects(enemyRect)) {
- // Calculate the intersection area
- int intLeft = MAX(updateRect.left, enemyRect.left);
- int intTop = MAX(updateRect.top, enemyRect.top);
- int intRight = MIN(updateRect.right, enemyRect.right);
- int intBottom = MIN(updateRect.bottom, enemyRect.bottom);
- int intArea = (intRight - intLeft) * (intBottom - intTop);
-
- // Require at least 70% overlap to skip the update
- // This prevents unrelated frame updates from being incorrectly skipped
- if (intArea * 100 >= updateArea * 70) {
- debug("Rebel2: Skipping frame update (%d,%d %dx%d) - %d%% overlap with destroyed enemy ID=%d",
- left, top, width, height, (intArea * 100) / updateArea, it->id);
- return true;
- }
- }
- }
- }
-
- return false;
-}
-
bool InsaneRebel2::isBitSet(int n) {
assert (n < 0x200);
@@ -699,17 +661,38 @@ void InsaneRebel2::clearBit(int n) {
}
void InsaneRebel2::procSKIP(int32 subSize, Common::SeekableReadStream &b) {
- // Rebel Assault 2 does NOT use Full Throttle's conditional frame skip mechanism.
- // The base Insane::procSKIP() uses _iactBits to decide whether to skip frame objects,
- // but RA2 handles conditional content differently through IACT opcodes 2/3/4.
+ // Rebel Assault 2 uses SKIP chunks to conditionally skip the next FOBJ/PSAD chunk.
+ // The SKIP chunk contains one or two object IDs. If the bit for the object is set
+ // (i.e., the object is disabled/destroyed), skip the next chunk.
//
- // By overriding this to do nothing, we prevent random frame objects from being
- // skipped due to uninitialized _iactBits state.
+ // This is the same mechanism as Full Throttle, but RA2 uses it for enemy objects:
+ // - When an enemy is destroyed, setBit(enemy_id) is called
+ // - SKIP chunks in the video contain the enemy ID
+ // - If the bit is set, the next FOBJ (enemy sprite) is skipped
+ // - This prevents destroyed enemy sprites from being rendered
//
- // If RA2 SKIP chunks need to be handled, implement RA2-specific logic here.
- // For now, just consume the data without setting _skipNext.
- (void)subSize;
- (void)b;
+ // The original game's FUN_00423A50 chunk reader uses this mechanism.
+
+ int16 par1, par2;
+ _player->_skipNext = false;
+
+ assert(subSize >= 4);
+ par1 = b.readUint16LE();
+ par2 = b.readUint16LE();
+
+ if (!par2) {
+ // Single ID mode: skip next chunk if this object's bit is set (disabled)
+ if (isBitSet(par1)) {
+ _player->_skipNext = true;
+ debug("Rebel2 SKIP: ID=%d bit is set, skipping next chunk", par1);
+ }
+ } else {
+ // Dual ID mode: skip if bits are different (XOR logic)
+ if (isBitSet(par1) != isBitSet(par2)) {
+ _player->_skipNext = true;
+ debug("Rebel2 SKIP: ID=%d and ID=%d bits differ, skipping next chunk", par1, par2);
+ }
+ }
}
void InsaneRebel2::procPreRendering(byte *renderBitmap) {
@@ -3697,24 +3680,15 @@ void InsaneRebel2::renderFallbackShip(byte *renderBitmap, int pitch, int width,
}
void InsaneRebel2::renderEnemyOverlays(byte *renderBitmap, int pitch, int width, int height, int videoWidth) {
- // Draw enemy indicator brackets and erase destroyed enemy areas
-
- // Erase destroyed enemies' areas (fill with black)
- for (Common::List<enemy>::iterator it = _enemies.begin(); it != _enemies.end(); ++it) {
- if (it->destroyed) {
- Common::Rect r = it->rect;
- if (r.left < 0) r.left = 0;
- if (r.top < 0) r.top = 0;
- if (r.right > width) r.right = width;
- if (r.bottom > height) r.bottom = height;
-
- for (int y = r.top; y < r.bottom; y++) {
- for (int x = r.left; x < r.right; x++) {
- renderBitmap[y * pitch + x] = 0;
- }
- }
- }
- }
+ // Draw enemy indicator brackets for active enemies
+ //
+ // NOTE: Do NOT fill destroyed enemy areas with black. The original game does not do this.
+ // When an enemy is destroyed:
+ // 1. setBit(enemy_id) disables the enemy in the bit table
+ // 2. clearBit(dependency_id) enables dependent objects (explosion animations)
+ // 3. SKIP chunks in the video cause enemy FOBJ sprites to be skipped (via procSKIP)
+ // 4. renderExplosions() draws the explosion animation from the 5-slot system
+ // 5. The background video shows through where the enemy was
// Draw green brackets for active enemies (Easy/Medium difficulty only)
if (_difficulty >= 2)
diff --git a/engines/scumm/insane/insane_rebel.h b/engines/scumm/insane/insane_rebel.h
index 393a6ad7146..fd7a18baafa 100644
--- a/engines/scumm/insane/insane_rebel.h
+++ b/engines/scumm/insane/insane_rebel.h
@@ -395,10 +395,7 @@ public:
};
EmbeddedSanFrame _rebelEmbeddedHud[16]; // HUD overlay slots (userId 0-15)
-
- // Check if a partial frame update should be skipped (overlaps with destroyed enemy)
- bool shouldSkipFrameUpdate(int left, int top, int width, int height) override;
-
+
// Load and decode an embedded SAN animation from IACT chunk data
// userId: HUD slot (1-4), animData: raw ANIM data, size: data size, renderBitmap: current frame buffer
void loadEmbeddedSan(int userId, byte *animData, int32 size, byte *renderBitmap) override;
diff --git a/engines/scumm/smush/smush_player.cpp b/engines/scumm/smush/smush_player.cpp
index 13a839e80f2..aa12d0d6f36 100644
--- a/engines/scumm/smush/smush_player.cpp
+++ b/engines/scumm/smush/smush_player.cpp
@@ -961,11 +961,11 @@ void SmushPlayer::decodeFrameObject(int codec, const uint8 *src, int left, int t
}
_dst = _specialBuffer;
} else if (_vm->_game.id == GID_REBEL2 && ((height != _vm->_screenHeight) || (width != _vm->_screenWidth))) {
- // Skip frame updates for destroyed enemies. The enemy area is erased
- // in procPostRendering (eraseDestroyedEnemies) before explosions are drawn.
- if (_insane && _insane->shouldSkipFrameUpdate(left, top, width, height)) {
- return; // Skip this frame update
- }
+ // Rebel2 uses SKIP chunks to conditionally skip FOBJ frames for destroyed enemies.
+ // The SKIP chunk mechanism (via procSKIP -> _skipNext) is checked at the START
+ // of handleFrameObject(), so destroyed enemy sprites are already skipped before
+ // reaching this point. No additional skip logic needed here.
+ //
// Rebel2 uses a special buffer for all non-matching frames.
// Level 1: First frame is 424x260 (background), small sprites reuse same buffer
// Level 2: Uses virtual screen directly (handled below when _specialBuffer stays null
Commit: 04aef6e22952ad7ca15c17cd0263592e11d7b5e0
https://github.com/scummvm/scummvm/commit/04aef6e22952ad7ca15c17cd0263592e11d7b5e0
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:08:57+02:00
Commit Message:
SCUMM: RA2: Read level 2 backgrounds correctly
Changed paths:
engines/scumm/insane/insane_rebel.cpp
engines/scumm/smush/codec1.cpp
diff --git a/engines/scumm/insane/insane_rebel.cpp b/engines/scumm/insane/insane_rebel.cpp
index 66afeef366a..a6d48da8da1 100644
--- a/engines/scumm/insane/insane_rebel.cpp
+++ b/engines/scumm/insane/insane_rebel.cpp
@@ -50,6 +50,7 @@ namespace Scumm {
// External codec functions from codec1.cpp
extern void smushDecodeRLE(byte *dst, const byte *src, int left, int top, int width, int height, int pitch);
+extern void smushDecodeRLEOpaque(byte *dst, const byte *src, int left, int top, int width, int height, int pitch);
extern void smushDecodeUncompressed(byte *dst, const byte *src, int left, int top, int width, int height, int pitch);
InsaneRebel2::InsaneRebel2(ScummEngine_v7 *scumm) {
@@ -1557,9 +1558,23 @@ void InsaneRebel2::iactRebel2Opcode8(byte *renderBitmap, Common::SeekableReadStr
}
}
- // ===== Handler 25 (0x19): Also supports Level 2 background =====
- if (!handled && _rebelHandler == 25 && par4 == 5) {
- handled = loadLevel2Background(animData, animDataSize, renderBitmap);
+ // ===== Handler 25 (0x19): Level 2 Background and Overlays =====
+ // FUN_0041cadb case 6 (opcode 8): Uses PAR4 for switch selection
+ // par4=4: 350x230 corridor overlay -> DAT_00482268, draws immediately
+ // par4=5: 320x200 background -> DAT_0048226c
+ // par4=6: Overlay -> DAT_00482250, draws immediately
+ // par4=7: Overlay -> DAT_00482248, draws immediately
+ if (!handled && _rebelHandler == 25) {
+ if (par4 == 5) {
+ // Background (320x200) - stored for per-frame restoration
+ handled = loadLevel2Background(animData, animDataSize, renderBitmap);
+ } else if (par4 == 4 || par4 == 6 || par4 == 7) {
+ // Overlays - draw immediately to renderBitmap
+ // These complete the visual scene along with the background
+ debug("Rebel2 Opcode 8: Handler 25 overlay par4=%d - drawing to screen", par4);
+ loadEmbeddedSan(par4, animData, animDataSize, renderBitmap);
+ handled = true;
+ }
}
// ===== Fallback: Embedded SAN HUD overlays =====
@@ -1867,9 +1882,12 @@ bool InsaneRebel2::loadLevel2Background(byte *animData, int32 size, byte *render
codec, fobjX, fobjY, fobjW, fobjH);
// Decode codec 3 (RLE) into background buffer
+ // Use smushDecodeRLEOpaque to write ALL colors including color 0 (black).
+ // The standard smushDecodeRLE treats color 0 as transparent, which causes
+ // the background to appear as a "sketch" with black pixels missing.
if (codec == 3 && fobjW > 0 && fobjH > 0 && fobjW <= 320 && fobjH <= 200) {
byte *rleData = fobjData + 14; // Skip full 14-byte FOBJ header
- smushDecodeRLE(_level2Background, rleData, fobjX, fobjY, fobjW, fobjH, 320);
+ smushDecodeRLEOpaque(_level2Background, rleData, fobjX, fobjY, fobjW, fobjH, 320);
debug("Rebel2 loadLevel2Background: Decoded Level 2 background (%dx%d at %d,%d)",
fobjW, fobjH, fobjX, fobjY);
@@ -2378,9 +2396,18 @@ void InsaneRebel2::renderEmbeddedFrame(byte *renderBitmap, const EmbeddedSanFram
// Skip immediate draw for handlers that render HUD during post-processing:
// - Handler 7/8: Ship direction sprites selected based on direction
// - Handler 0x26/0x19: Cockpit HUD positioned based on mouse/crosshair
+ //
+ // Exception: Handler 25 (0x19) background overlays (par4/userId=4, 6, 7) should draw immediately.
+ // These complete the visual scene and are NOT positioned by mouse/crosshair.
bool skipImmediateDraw = (_rebelHandler == 7 || _rebelHandler == 8 ||
_rebelHandler == 0x26 || _rebelHandler == 0x19);
+ // Handler 25 background overlays should draw immediately
+ if (_rebelHandler == 0x19 && (userId == 4 || userId == 6 || userId == 7)) {
+ skipImmediateDraw = false;
+ debug("Rebel2: Handler 25 background overlay userId=%d - forcing immediate draw", userId);
+ }
+
if (!frame.valid || !renderBitmap || skipImmediateDraw) {
if (skipImmediateDraw && frame.valid) {
debug("Rebel2: Skipped immediate draw for Handler %d HUD %d (will render during post-processing)",
diff --git a/engines/scumm/smush/codec1.cpp b/engines/scumm/smush/codec1.cpp
index 5fa4dae5791..0020c51d8cc 100644
--- a/engines/scumm/smush/codec1.cpp
+++ b/engines/scumm/smush/codec1.cpp
@@ -36,6 +36,24 @@ void smushDecodeRLE(byte *dst, const byte *src, int left, int top, int width, in
} while (--height);
}
+/**
+ * Codec 3 RLE decoder that writes ALL colors including color 0 (black).
+ * Use this for background images where color 0 should NOT be treated as transparent.
+ * The standard smushDecodeRLE() treats color 0 as transparent, which is correct
+ * for overlay sprites but wrong for background images.
+ *
+ * Used by: Rebel Assault 2 Level 2 background loading (IACT opcode 8, par4=5)
+ */
+void smushDecodeRLEOpaque(byte *dst, const byte *src, int left, int top, int width, int height, int pitch) {
+ dst += top * pitch;
+ do {
+ dst += left;
+ bompDecodeLine(dst, src + 2, width, true); // setZero = TRUE to write all colors
+ src += READ_LE_UINT16(src) + 2;
+ dst += pitch - left;
+ } while (--height);
+}
+
/**
* Codec 21/44: Line Update codec
* Used for fonts (NUT files) and some embedded HUD frames.
Commit: 3abc8c5bc6655c2c32754d51a8a47f917ea4bdd6
https://github.com/scummvm/scummvm/commit/3abc8c5bc6655c2c32754d51a8a47f917ea4bdd6
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:08:57+02:00
Commit Message:
SCUMM: RA2: Improve level 2 backgrounds
Changed paths:
engines/scumm/insane/insane_rebel.cpp
engines/scumm/insane/insane_rebel.h
engines/scumm/smush/smush_player.cpp
diff --git a/engines/scumm/insane/insane_rebel.cpp b/engines/scumm/insane/insane_rebel.cpp
index a6d48da8da1..09dae96b0ec 100644
--- a/engines/scumm/insane/insane_rebel.cpp
+++ b/engines/scumm/insane/insane_rebel.cpp
@@ -272,6 +272,7 @@ InsaneRebel2::InsaneRebel2(ScummEngine_v7 *scumm) {
_shipTargetX = 0xa0;
_shipTargetY = 0x28;
_shipLevelMode = 0;
+ _movementRangeLimit = 127; // DAT_0047e034 - Start at full range (shooting state)
_flyControlMode = 0; // DAT_004437c0 - Start in flight-only mode (no shooting)
_shipFiring = false;
_shipDirectionH = 2; // Start centered horizontally (0-4 range)
@@ -700,13 +701,20 @@ void InsaneRebel2::procPreRendering(byte *renderBitmap) {
// Call base class implementation first (handles Full Throttle state machine)
Insane::procPreRendering(renderBitmap);
- // For Level 2 gameplay (Handler 8 or 25), restore the background BEFORE FOBJ decoding.
+ // For Level 2 gameplay (Handler 8 only), restore the background BEFORE FOBJ decoding.
// The tiny FOBJ sprites (7x10, 9x38 pixels) only draw new sprite positions but don't
// clear old ones. By restoring the full background each frame, we ensure old sprite
// positions are erased before new ones are drawn.
//
+ // NOTE: Handler 25 does NOT restore the background here because:
+ // - Handler 25 uses par4=4,6,7 overlays drawn on top of par4=5 base background
+ // - These overlays are only drawn once in frame 0
+ // - _level2Background only contains the par4=5 base, not the composited result
+ // - The video uses flag 0x08 (preserve background) to keep frame buffer intact
+ // - Enemies draw on top of the existing composited frame buffer
+ //
// This is called at the start of handleFrame(), before any FOBJ chunks are processed.
- if ((_rebelHandler == 8 || _rebelHandler == 25) && _level2BackgroundLoaded && _level2Background && renderBitmap) {
+ if (_rebelHandler == 8 && _level2BackgroundLoaded && _level2Background && renderBitmap) {
for (int y = 0; y < 200; y++) {
memcpy(renderBitmap + y * 320, _level2Background + y * 320, 320);
}
@@ -1072,6 +1080,29 @@ void InsaneRebel2::iactRebel2Opcode6(byte *renderBitmap, Common::SeekableReadStr
// Skip position calculation for special modes 4 and 5
if (_shipLevelMode != 4 && _shipLevelMode != 5) {
+ // ===== Movement Range Transition (Covered vs Shooting) =====
+ // Based on FUN_00401234 lines 85-120:
+ // Mode 2 = "Covered" state - contract movement range to 41 (0x29)
+ // Other modes = "Shooting" state - expand movement range to 127 (0x7f)
+ // Transition happens gradually at ±10 per frame for smooth animation
+ if (_shipLevelMode == 2) {
+ // Covered state - contract movement range
+ if (_movementRangeLimit > 41) {
+ _movementRangeLimit -= 10;
+ }
+ if (_movementRangeLimit < 41) {
+ _movementRangeLimit = 41;
+ }
+ } else {
+ // Shooting state - expand movement range
+ if (_movementRangeLimit < 127) {
+ _movementRangeLimit += 10;
+ }
+ if (_movementRangeLimit > 127) {
+ _movementRangeLimit = 127;
+ }
+ }
+
// Calculate target position from mouse input
// Mouse X maps to ship horizontal tilt, Mouse Y to vertical tilt
// Based on FUN_00401234 lines 151-166:
@@ -1083,9 +1114,11 @@ void InsaneRebel2::iactRebel2Opcode6(byte *renderBitmap, Common::SeekableReadStr
int16 mouseOffsetX = (int16)((_vm->_mouse.x - 160) * 127 / 160);
int16 mouseOffsetY = (int16)((_vm->_mouse.y - 100) * 127 / 100);
- // Clamp to valid range
- if (mouseOffsetX > 127) mouseOffsetX = 127;
- if (mouseOffsetX < -127) mouseOffsetX = -127;
+ // Clamp X offset to movement range limit (covered/shooting state)
+ // Based on FUN_00401234 lines 119-136
+ if (mouseOffsetX > _movementRangeLimit) mouseOffsetX = _movementRangeLimit;
+ if (mouseOffsetX < -_movementRangeLimit) mouseOffsetX = -_movementRangeLimit;
+ // Y offset always uses full range (±127)
if (mouseOffsetY > 127) mouseOffsetY = 127;
if (mouseOffsetY < -127) mouseOffsetY = -127;
@@ -1144,10 +1177,15 @@ void InsaneRebel2::iactRebel2Opcode6(byte *renderBitmap, Common::SeekableReadStr
}
// Update firing state from mouse button
- _shipFiring = (_vm->VAR(_vm->VAR_LEFTBTN_HOLD) != 0);
+ // Mode 4 (autopilot) disables shooting - FUN_00401CCF line 82-84
+ if (_shipLevelMode == 4) {
+ _shipFiring = false;
+ } else {
+ _shipFiring = (_vm->VAR(_vm->VAR_LEFTBTN_HOLD) != 0);
+ }
- debug("Rebel2 Opcode 6 (Handler 8): mode=%d shipPos=(%d,%d) target=(%d,%d) firing=%d dir=(%d,%d,%d)",
- _shipLevelMode, _shipPosX, _shipPosY, _shipTargetX, _shipTargetY, _shipFiring,
+ debug("Rebel2 Opcode 6 (Handler 8): mode=%d range=%d shipPos=(%d,%d) target=(%d,%d) firing=%d dir=(%d,%d,%d)",
+ _shipLevelMode, _movementRangeLimit, _shipPosX, _shipPosY, _shipTargetX, _shipTargetY, _shipFiring,
_shipDirectionH, _shipDirectionV, _shipDirectionIndex);
// Handler 8 doesn't use the same view offset logic as other handlers
diff --git a/engines/scumm/insane/insane_rebel.h b/engines/scumm/insane/insane_rebel.h
index fd7a18baafa..4dc42b6160d 100644
--- a/engines/scumm/insane/insane_rebel.h
+++ b/engines/scumm/insane/insane_rebel.h
@@ -246,6 +246,9 @@ public:
bool isBitSet(int n) override;
void setBit(int n) override;
+ // Get current handler ID (8, 25, 38 etc.) for SMUSH player to query
+ int getHandler() const { return _rebelHandler; }
+
void iactRebel2Scene1(byte *renderBitmap, int32 codecparam, int32 setupsan12,
int32 setupsan13, Common::SeekableReadStream &b, int32 size, int32 flags,
int16 par1, int16 par2, int16 par3, int16 par4);
@@ -570,8 +573,17 @@ public:
// Level mode for handler 8 (different from _rebelLevelType)
// Set by opcode 6 par3, affects ship rendering behavior
+ // Mode 0/1/3: "Shooting" - full movement range (127)
+ // Mode 2: "Covered" - restricted movement (41) - behind cover
+ // Mode 4: "Autopilot" - no shooting, scripted movement
+ // Mode 5: "Cutscene" - ship not rendered
int16 _shipLevelMode; // DAT_0043e000
+ // Movement range limiter for Handler 8 (Level 2 covered/shooting states)
+ // Controls horizontal movement range: 127 for shooting, 41 for covered
+ // Gradually transitions by ±10 per frame for smooth animation
+ int16 _movementRangeLimit; // DAT_0047e034
+
// Control mode for Handler 7 (space flight) - DAT_004437c0
// Set by IACT opcode 6 par3 when handler is 7
// Determines shooting capability and collision zone type:
diff --git a/engines/scumm/smush/smush_player.cpp b/engines/scumm/smush/smush_player.cpp
index aa12d0d6f36..51b6eec3b4b 100644
--- a/engines/scumm/smush/smush_player.cpp
+++ b/engines/scumm/smush/smush_player.cpp
@@ -432,6 +432,18 @@ void SmushPlayer::handleFetch(int32 subSize, Common::SeekableReadStream &b) {
debugC(DEBUG_SMUSH, "SmushPlayer::handleFetch()");
assert(subSize >= 6);
+ // For RA2 Handler 25, skip FTCH because the frame buffer only contains the
+ // par4=5 base background without the overlays (par4=4, 6, 7) that were drawn
+ // immediately in frame 0. Restoring would erase those overlays and make
+ // enemies invisible since they draw on top of the erased areas.
+ if (_vm->_game.id == GID_REBEL2 && _insane != nullptr) {
+ InsaneRebel2 *rebel2 = static_cast<InsaneRebel2 *>(_insane);
+ if (rebel2->getHandler() == 25) {
+ debug("SmushPlayer::handleFetch: Skipping FTCH for Handler 25 - preserving overlays");
+ return;
+ }
+ }
+
if (_frameBuffer != nullptr) {
memcpy(_dst, _frameBuffer, _width * _height);
}
Commit: b621b9cc6e97caaeb9101af3681e9aa81e82b133
https://github.com/scummvm/scummvm/commit/b621b9cc6e97caaeb9101af3681e9aa81e82b133
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:08:58+02:00
Commit Message:
SCUMM: RA2: Improve level 2 player sprites
Changed paths:
engines/scumm/insane/insane_rebel.cpp
engines/scumm/insane/insane_rebel.h
diff --git a/engines/scumm/insane/insane_rebel.cpp b/engines/scumm/insane/insane_rebel.cpp
index 09dae96b0ec..2334a645346 100644
--- a/engines/scumm/insane/insane_rebel.cpp
+++ b/engines/scumm/insane/insane_rebel.cpp
@@ -275,6 +275,7 @@ InsaneRebel2::InsaneRebel2(ScummEngine_v7 *scumm) {
_movementRangeLimit = 127; // DAT_0047e034 - Start at full range (shooting state)
_flyControlMode = 0; // DAT_004437c0 - Start in flight-only mode (no shooting)
_shipFiring = false;
+ _prevMouseButtons = 0; // For edge detection in mouse button handling
_shipDirectionH = 2; // Start centered horizontally (0-4 range)
_shipDirectionV = 3; // Start centered vertically (0-6 range)
_shipDirectionIndex = 2 * 7 + 3; // Center = 17
@@ -287,6 +288,11 @@ InsaneRebel2::InsaneRebel2(ScummEngine_v7 *scumm) {
_flyShipScreenX = 0xd4; // Start at center (212) - matches DAT_00443708 default
_flyShipScreenY = 0x82; // Start at center (130) - matches DAT_0044370a default
+ // Initialize Handler 25 GRD ship system
+ _grd001Sprite = nullptr; // DAT_00482240 - GRD001 primary ship
+ _grd002Sprite = nullptr; // DAT_00482238 - GRD002 secondary ship
+ _grdSpriteMode = 0; // DAT_00457900 - sprite mode (1,2,3,4)
+
// Initialize Handler 0x26 turret HUD overlay system
_hudOverlayNut = nullptr; // DAT_0047fe78 - Primary HUD overlay (GRD files, animated)
_hudOverlay2Nut = nullptr; // DAT_0047fe80 - Secondary HUD overlay
@@ -358,6 +364,10 @@ InsaneRebel2::~InsaneRebel2() {
delete _flyTargetSprite;
delete _flyHiResSprite;
+ // Clean up Handler 25 GRD ship sprites
+ delete _grd001Sprite;
+ delete _grd002Sprite;
+
// Clean up Handler 0x26 turret HUD overlays
delete _hudOverlayNut;
delete _hudOverlay2Nut;
@@ -564,11 +574,34 @@ int32 InsaneRebel2::processMouse() {
int32 buttons = 0;
// Get button state directly from event manager (SCUMM VARs aren't updated during SMUSH)
- bool wasPressed = false;
- bool isPressed = (_vm->_system->getEventManager()->getButtonState() & 1) != 0;
-
- // Edge detection: only trigger on button press (not hold)
- if (isPressed && !wasPressed) {
+ // Bit 0 = left button, Bit 1 = right button, Bit 2 = middle button
+ uint32 currentButtons = _vm->_system->getEventManager()->getButtonState();
+
+ // Edge detection for buttons
+ bool leftPressed = (currentButtons & 1) != 0;
+ bool leftWasPressed = (_prevMouseButtons & 1) != 0;
+ bool rightPressed = (currentButtons & 2) != 0;
+ bool rightWasPressed = (_prevMouseButtons & 2) != 0;
+
+ // Store current state for next frame's edge detection
+ _prevMouseButtons = currentButtons;
+
+ // Update _rebelControlMode (DAT_0047a7e4) for Handler 25 covered/uncovered toggle:
+ // The original uses input throttling (every 5 frames) to prevent oscillation.
+ // We use edge detection instead - only set the control mode flag on button DOWN.
+ // - Bit 1 (value 2): Right mouse button just pressed - triggers cover entry
+ // - Bit 0 (value 1): Left mouse button just pressed - triggers cover exit
+ // When covered, any NEW button press exits cover. When uncovered, right button enters cover.
+ _rebelControlMode = 0;
+ if (rightPressed && !rightWasPressed) {
+ _rebelControlMode |= 2; // Right button just pressed
+ }
+ if (leftPressed && !leftWasPressed) {
+ _rebelControlMode |= 1; // Left button just pressed
+ }
+
+ // Left button: Trigger shot on button press (not hold)
+ if (leftPressed && !leftWasPressed) {
Common::Point mousePos(_vm->_mouse.x, _vm->_mouse.y);
debug("Rebel2 Click: Mouse=(%d,%d) Enemies=%d",
mousePos.x, mousePos.y, _enemies.size());
@@ -640,7 +673,6 @@ int32 InsaneRebel2::processMouse() {
}
}
}
- wasPressed = isPressed;
return buttons;
}
@@ -706,19 +738,41 @@ void InsaneRebel2::procPreRendering(byte *renderBitmap) {
// clear old ones. By restoring the full background each frame, we ensure old sprite
// positions are erased before new ones are drawn.
//
- // NOTE: Handler 25 does NOT restore the background here because:
- // - Handler 25 uses par4=4,6,7 overlays drawn on top of par4=5 base background
- // - These overlays are only drawn once in frame 0
- // - _level2Background only contains the par4=5 base, not the composited result
- // - The video uses flag 0x08 (preserve background) to keep frame buffer intact
- // - Enemies draw on top of the existing composited frame buffer
- //
// This is called at the start of handleFrame(), before any FOBJ chunks are processed.
if (_rebelHandler == 8 && _level2BackgroundLoaded && _level2Background && renderBitmap) {
for (int y = 0; y < 200; y++) {
memcpy(renderBitmap + y * 320, _level2Background + y * 320, 320);
}
}
+
+ // For Handler 25 (Level 2 speeder bike), draw the corridor overlay BEFORE FOBJ decoding.
+ // The corridor overlay (par4=4, _rebelEmbeddedHud[4]) shows the covered/uncovered state
+ // and must be drawn BEFORE enemy FOBJ frames so enemies appear ON TOP of the corridor.
+ // Position is (_rebelViewOffsetX, _rebelViewOffsetY) from FUN_0041cadb line 216.
+ if (_rebelHandler == 25 && renderBitmap) {
+ EmbeddedSanFrame &corridorOverlay = _rebelEmbeddedHud[4];
+ if (corridorOverlay.valid && corridorOverlay.pixels) {
+ int overlayX = _rebelViewOffsetX;
+ int overlayY = _rebelViewOffsetY;
+ int pitch = (_player && _player->_width > 0) ? _player->_width : 320;
+ int bufHeight = (_player && _player->_height > 0) ? _player->_height : 200;
+
+ for (int y = 0; y < corridorOverlay.height && (overlayY + y) < bufHeight; y++) {
+ for (int x = 0; x < corridorOverlay.width && (overlayX + x) < pitch; x++) {
+ byte pixel = corridorOverlay.pixels[y * corridorOverlay.width + x];
+ if (pixel != 0 && pixel != 231) { // 0 and 231 = transparent
+ int destX = overlayX + x;
+ int destY = overlayY + y;
+ if (destX >= 0 && destY >= 0) {
+ renderBitmap[destY * pitch + destX] = pixel;
+ }
+ }
+ }
+ }
+ debug("Rebel2 procPreRendering: Corridor overlay at (%d,%d) size(%d,%d)",
+ overlayX, overlayY, corridorOverlay.width, corridorOverlay.height);
+ }
+ }
}
void InsaneRebel2::procIACT(byte *renderBitmap, int32 codecparam, int32 setupsan12,
@@ -1309,6 +1363,135 @@ void InsaneRebel2::iactRebel2Opcode6(byte *renderBitmap, Common::SeekableReadStr
return;
}
+ // Handler 25 (0x19) specific logic (mixed mode - speeder bike)
+ // Based on FUN_0041cadb case 4 (opcode 6) lines 113-229
+ if (_rebelHandler == 25) {
+ // If par4 == 1, enable status bar and reset state (lines 114-121)
+ if (par4 == 1) {
+ _rebelStatusBarSprite = 5;
+ // Reset link tables (DAT_0045797c through DAT_0045917c)
+ for (int i = 0; i < 512; i++) {
+ _rebelLinks[i][0] = 0;
+ _rebelLinks[i][1] = 0;
+ _rebelLinks[i][2] = 0;
+ }
+ debug("Rebel2 Opcode 6 (Handler 25): Status bar enabled, state reset");
+ }
+
+ // Set sprite mode (DAT_00457900 = par4) - controls which GRD sprite to render
+ // NOTE: Original code uses local_14[3] which is par4, NOT par3!
+ // Mode 1: Uncovered, shooting position - sprite on left
+ // Mode 2: Covered, vertical shift
+ // Mode 3: Transition between covered/uncovered - sprite position depends on direction
+ // Mode 4: Alternative uncovered position - sprite on right
+ _grdSpriteMode = par4;
+
+ // Autopilot logic (lines 123-146)
+ if (!_rebelInvulnerable) {
+ if (_rebelAutopilot == 0) {
+ if ((_rebelControlMode & 2) != 0) {
+ _rebelAutopilot = 1;
+ }
+ } else {
+ if (_rebelControlMode != 0) {
+ _rebelAutopilot = 0;
+ }
+ }
+ } else {
+ // Invulnerable mode: random autopilot changes
+ if (_rebelAutopilot == 0) {
+ if (_vm->_rnd.getRandomNumber(100) == 0) {
+ _rebelAutopilot = 1;
+ }
+ } else {
+ if (_vm->_rnd.getRandomNumber(15) == 0) {
+ _rebelAutopilot = 0;
+ _rebelFlightDir = _vm->_rnd.getRandomNumber(2);
+ }
+ }
+ }
+
+ // Update damage level counter (lines 147-154)
+ if (_rebelAutopilot == 0) {
+ if (_rebelDamageLevel > 0) {
+ _rebelDamageLevel--;
+ }
+ } else {
+ if (_rebelDamageLevel < 5) {
+ _rebelDamageLevel++;
+ }
+ }
+
+ // Flight direction logic for mode 3 (lines 155-177)
+ if (_grdSpriteMode == 3) {
+ if (_rebelDamageLevel == 5) {
+ // At max damage, check for direction change input
+ // For now, use mouse X position to determine direction
+ int16 mouseX = _vm->_mouse.x;
+ if (_player && _player->_width > 320) {
+ mouseX = (mouseX * 320) / _player->_width;
+ }
+ if (mouseX > 235) { // 0x4b + 160 = 235
+ _rebelFlightDir = 1;
+ }
+ if (mouseX < 85) { // 160 - 0x4b = 85
+ _rebelFlightDir = 0;
+ }
+ }
+ } else {
+ _rebelFlightDir = 0;
+ }
+
+ // Calculate sprite and view offset positions based on mode (lines 182-213)
+ // DAT_0045790c = view offset X (for corridor overlay)
+ // DAT_0045790e = view offset Y (for corridor overlay)
+ // DAT_00457910 = sprite position X (relative to center)
+ // DAT_00457912 = sprite position Y (relative to center)
+ if (_grdSpriteMode == 1) {
+ // Mode 1: Uncovered, shooting - sprite shifts left as damage increases
+ _rebelViewMode1 = 0x0e;
+ _rebelViewMode2 = 0;
+ _rebelViewOffsetX = _rebelDamageLevel * -5 + -14; // DAT_0045790c
+ _rebelViewOffset2X = _rebelDamageLevel * -22; // DAT_00457910
+ _rebelViewOffsetY = 0; // DAT_0045790e
+ _rebelViewOffset2Y = 0; // DAT_00457912
+ } else if (_grdSpriteMode == 4) {
+ // Mode 4: Alternative uncovered - sprite shifts right
+ _rebelViewMode1 = 0x22;
+ _rebelViewMode2 = 0;
+ _rebelViewOffsetX = _rebelDamageLevel * 10 + -16; // DAT_0045790c
+ _rebelViewOffset2X = _rebelDamageLevel * 17 + -85; // DAT_00457910 (0x11 = 17, -0x55 = -85)
+ _rebelViewOffsetY = 0;
+ _rebelViewOffset2Y = 0;
+ } else if (_grdSpriteMode == 2) {
+ // Mode 2: Covered - vertical shift
+ _rebelViewMode1 = 0;
+ _rebelViewMode2 = 0x0e;
+ _rebelViewOffsetY = _rebelDamageLevel * -5 + -14; // DAT_0045790e
+ _rebelViewOffset2Y = (5 - _rebelDamageLevel) * 15 + -60; // DAT_00457912 (0xf = 15, -0x3c = -60)
+ _rebelViewOffsetX = 0;
+ _rebelViewOffset2X = 0;
+ } else if (_grdSpriteMode == 3) {
+ // Mode 3: Transition - direction-dependent horizontal shift
+ _rebelViewMode1 = 0x0f;
+ _rebelViewMode2 = 0;
+ // (-(DAT_00457902 == 0) & 6) - 3 = if dir==0: 6-3=3, else 0-3=-3
+ int16 dirMultX = (_rebelFlightDir == 0) ? 3 : -3;
+ // (-(DAT_00457902 == 0) & 0x28) - 0x14 = if dir==0: 40-20=20, else 0-20=-20
+ int16 dirMultX2 = (_rebelFlightDir == 0) ? 20 : -20;
+ _rebelViewOffsetX = dirMultX * (5 - _rebelDamageLevel) + -15; // DAT_0045790c
+ _rebelViewOffset2X = dirMultX2 * (5 - _rebelDamageLevel); // DAT_00457910
+ _rebelViewOffsetY = 0;
+ _rebelViewOffset2Y = 0;
+ }
+
+ debug("Rebel2 Opcode 6 (Handler 25): mode=%d damage=%d dir=%d viewOff=(%d,%d) spritePos=(%d,%d)",
+ _grdSpriteMode, _rebelDamageLevel, _rebelFlightDir,
+ _rebelViewOffsetX, _rebelViewOffsetY, _rebelViewOffset2X, _rebelViewOffset2Y);
+
+ return;
+ }
+
// Step 1: If par4 == 1, initialize/reset state (lines 114-121)
if (par4 == 1) {
// Draw status bar sprite 5 (FUN_0040bb87 equivalent)
@@ -1584,26 +1767,32 @@ void InsaneRebel2::iactRebel2Opcode8(byte *renderBitmap, Common::SeekableReadStr
}
// ===== Handler 8: POV Ship Sprites or Background =====
- // FUN_00401234 case 6: par3 for POV NUTs, par4=5 for background
+ // FUN_00401234 case 6: par4 selects POV NUT type (1,3,6,7) or background (5)
+ // NOTE: par3 is always 0 for Handler 8; par4 contains the actual sprite type
if (!handled && _rebelHandler == 8) {
// Check for background loading first (par4=5)
if (par4 == 5) {
handled = loadLevel2Background(animData, animDataSize, renderBitmap);
}
- // Check for POV NUT sprites
- else if (par3 == 1 || par3 == 3 || par3 == 6 || par3 == 7) {
- handled = loadHandler8ShipSprites(animData, animDataSize, par3);
+ // Check for POV NUT sprites (par4=1,3,6,7)
+ else if (par4 == 1 || par4 == 3 || par4 == 6 || par4 == 7) {
+ handled = loadHandler8ShipSprites(animData, animDataSize, par4);
}
}
- // ===== Handler 25 (0x19): Level 2 Background and Overlays =====
+ // ===== Handler 25 (0x19): Level 2 GRD Ship Sprites and Background =====
// FUN_0041cadb case 6 (opcode 8): Uses PAR4 for switch selection
+ // par4=1: GRD001 - Primary ship sprite -> DAT_00482240 / _grd001Sprite
+ // par4=2: GRD002 - Secondary ship sprite -> DAT_00482238 / _grd002Sprite
// par4=4: 350x230 corridor overlay -> DAT_00482268, draws immediately
// par4=5: 320x200 background -> DAT_0048226c
// par4=6: Overlay -> DAT_00482250, draws immediately
// par4=7: Overlay -> DAT_00482248, draws immediately
if (!handled && _rebelHandler == 25) {
- if (par4 == 5) {
+ if (par4 == 1 || par4 == 2) {
+ // GRD ship sprites - load into NutRenderer for per-frame rendering
+ handled = loadHandler25GrdSprites(animData, animDataSize, par4);
+ } else if (par4 == 5) {
// Background (320x200) - stored for per-frame restoration
handled = loadLevel2Background(animData, animDataSize, renderBitmap);
} else if (par4 == 4 || par4 == 6 || par4 == 7) {
@@ -1811,9 +2000,9 @@ bool InsaneRebel2::loadTurretHudOverlay(byte *animData, int32 size, int16 par3)
return true;
}
-bool InsaneRebel2::loadHandler8ShipSprites(byte *animData, int32 size, int16 par3) {
+bool InsaneRebel2::loadHandler8ShipSprites(byte *animData, int32 size, int16 par4) {
// Handler 8 ship POV NUT loading - FUN_00401234 case 6 (opcode 8)
- // par3 values:
+ // par4 values (from IACT data offset +6, NOT par3 which is always 0):
// 1: POV001 - Primary ship sprite (DAT_0047e010 / _shipSprite)
// 3: POV004 - Secondary ship sprite (DAT_0047e028 / _shipSprite2)
// 6: POV002 - Ship overlay 1 (DAT_0047e020 / _shipOverlay1)
@@ -1824,21 +2013,21 @@ bool InsaneRebel2::loadHandler8ShipSprites(byte *animData, int32 size, int16 par
}
// Only handle valid POV sprite slots
- if (par3 != 1 && par3 != 3 && par3 != 6 && par3 != 7) {
+ if (par4 != 1 && par4 != 3 && par4 != 6 && par4 != 7) {
return false;
}
NutRenderer *newNut = new NutRenderer(_vm, animData, size);
if (!newNut || newNut->getNumChars() <= 0) {
- debug("Rebel2 loadHandler8ShipSprites: NUT load failed for par3=%d", par3);
+ debug("Rebel2 loadHandler8ShipSprites: NUT load failed for par4=%d", par4);
delete newNut;
return false;
}
- debug("Rebel2 loadHandler8ShipSprites: Loaded ship NUT par3=%d with %d sprites",
- par3, newNut->getNumChars());
+ debug("Rebel2 loadHandler8ShipSprites: Loaded ship NUT par4=%d with %d sprites",
+ par4, newNut->getNumChars());
- switch (par3) {
+ switch (par4) {
case 1: // POV001 - Primary ship sprite
delete _shipSprite;
_shipSprite = newNut;
@@ -1863,6 +2052,50 @@ bool InsaneRebel2::loadHandler8ShipSprites(byte *animData, int32 size, int16 par
return true;
}
+bool InsaneRebel2::loadHandler25GrdSprites(byte *animData, int32 size, int16 par4) {
+ // Handler 25 GRD ship NUT loading - FUN_0041cadb case 6 (opcode 8)
+ // par4 values (from IACT data offset +6):
+ // 1: GRD001 - Primary ship sprite (DAT_00482240 / _grd001Sprite)
+ // 2: GRD002 - Secondary ship sprite (DAT_00482238 / _grd002Sprite)
+
+ if (!animData || size <= 0) {
+ return false;
+ }
+
+ // Only handle valid GRD sprite slots
+ if (par4 != 1 && par4 != 2) {
+ return false;
+ }
+
+ NutRenderer *newNut = new NutRenderer(_vm, animData, size);
+ if (!newNut || newNut->getNumChars() <= 0) {
+ debug("Rebel2 loadHandler25GrdSprites: NUT load failed for par4=%d", par4);
+ delete newNut;
+ return false;
+ }
+
+ debug("Rebel2 loadHandler25GrdSprites: Loaded GRD NUT par4=%d with %d sprites",
+ par4, newNut->getNumChars());
+
+ switch (par4) {
+ case 1: // GRD001 - Primary ship sprite
+ delete _grd001Sprite;
+ _grd001Sprite = newNut;
+ debug("Rebel2: _grd001Sprite set with %d sprites", newNut->getNumChars());
+ break;
+ case 2: // GRD002 - Secondary ship sprite
+ delete _grd002Sprite;
+ _grd002Sprite = newNut;
+ debug("Rebel2: _grd002Sprite set with %d sprites", newNut->getNumChars());
+ break;
+ default:
+ delete newNut;
+ return false;
+ }
+
+ return true;
+}
+
bool InsaneRebel2::loadLevel2Background(byte *animData, int32 size, byte *renderBitmap) {
// Level 2 background loading from embedded ANIM - FUN_00401234 case 5
// par4=5 contains the background image embedded as ANIM with FOBJ codec 3
@@ -2440,11 +2673,14 @@ void InsaneRebel2::renderEmbeddedFrame(byte *renderBitmap, const EmbeddedSanFram
bool skipImmediateDraw = (_rebelHandler == 7 || _rebelHandler == 8 ||
_rebelHandler == 0x26 || _rebelHandler == 0x19);
- // Handler 25 background overlays should draw immediately
- if (_rebelHandler == 0x19 && (userId == 4 || userId == 6 || userId == 7)) {
+ // Handler 25 overlays:
+ // - userId 4 (corridor overlay): Draw during procPostRendering at view offset, NOT immediately
+ // - userId 6, 7 (static overlays): Draw immediately (they don't move)
+ if (_rebelHandler == 0x19 && (userId == 6 || userId == 7)) {
skipImmediateDraw = false;
- debug("Rebel2: Handler 25 background overlay userId=%d - forcing immediate draw", userId);
+ debug("Rebel2: Handler 25 static overlay userId=%d - forcing immediate draw", userId);
}
+ // userId 4 should NOT draw immediately - it will be drawn at view offset each frame
if (!frame.valid || !renderBitmap || skipImmediateDraw) {
if (skipImmediateDraw && frame.valid) {
@@ -3301,6 +3537,7 @@ void InsaneRebel2::procPostRendering(byte *renderBitmap, int32 codecparam, int32
renderHandler7Ship(renderBitmap, pitch, width, height);
renderHandler8Ship(renderBitmap, pitch, width, height);
+ renderHandler25Ship(renderBitmap, pitch, width, height);
renderFallbackShip(renderBitmap, pitch, width, height);
// Enemy indicators and destroyed enemy area erase
@@ -3680,6 +3917,144 @@ void InsaneRebel2::renderHandler8Ship(byte *renderBitmap, int pitch, int width,
spriteIndex, numSprites, _shipDirectionH, _shipDirectionV);
}
+void InsaneRebel2::renderHandler25Ship(byte *renderBitmap, int pitch, int width, int height) {
+ // Handler 25 Ship Rendering (Mixed Mode - GRD sprites)
+ // Based on FUN_0041db5e disassembly (lines 202-248)
+ // Uses _grd001Sprite (GRD001) and _grd002Sprite (GRD002)
+ //
+ // The GRD sprite is drawn at position (DAT_00457910, DAT_00457912) which are
+ // offsets calculated from sprite mode and damage level, relative to screen center.
+ // In the original, these appear to be direct pixel coordinates where:
+ // - Mode 1: sprite shifts left as damage increases (X = damage * -22)
+ // - Mode 4: sprite shifts right as damage increases (X = damage * 17 - 85)
+ // - Mode 2: sprite shifts vertically
+ // - Mode 3: depends on flight direction
+ //
+ // NOTE: The corridor overlay (par4=4) is drawn in procPreRendering BEFORE enemies,
+ // not here, so that enemies appear ON TOP of the corridor.
+
+ if (_rebelHandler != 25)
+ return;
+
+ // Need at least one GRD sprite to render
+ if (!_grd001Sprite && !_grd002Sprite)
+ return;
+
+ // Base position calculation based on FUN_0041db5e disassembly:
+ // The original reads sprite internal offsets from the NUT data at +0x12 (X) and +0x14 (Y).
+ // Since NutRenderer doesn't expose these, we use the standard positioning formula:
+ // - X: center (160) + mode-based offset
+ // - Y: positioned above status bar (status bar starts at ~Y=180)
+ //
+ // From the HUD positioning formula (lines 3583-3584):
+ // Y = 182 - (mouseOffset >> 4) - height - spriteOffsetY
+ // This means sprites are bottom-aligned relative to Y=182.
+ //
+ // For Handler 25, the sprite position offsets (DAT_00457910, DAT_00457912) are added.
+ const int baseX = 160;
+ // Base Y is calculated to keep sprite above status bar (Y=180)
+ // We'll position the center around Y=90 (middle of playable area)
+ const int baseY = 90;
+
+ // Calculate actual ship position from base + offset
+ // _rebelViewOffset2X = DAT_00457910 (sprite X offset from mode/damage)
+ // _rebelViewOffset2Y = DAT_00457912 (sprite Y offset from mode/damage)
+ int shipX = baseX + _rebelViewOffset2X;
+ int shipY = baseY + _rebelViewOffset2Y;
+
+ // Draw _grd001Sprite based on _grdSpriteMode (DAT_00457900)
+ // Mode 1, 2, 3, 4 all potentially draw _grd001Sprite
+ if (_grd001Sprite && _grd001Sprite->getNumChars() > 0) {
+ bool shouldDraw = false;
+
+ // Mode 1: Always draw (uncovered/shooting position)
+ if (_grdSpriteMode == 1) {
+ shouldDraw = true;
+ }
+ // Mode 2: Only draw when damaged (DAT_0045790a != 0) - covered position
+ else if (_grdSpriteMode == 2 && _rebelDamageLevel != 0) {
+ shouldDraw = true;
+ }
+ // Mode 3: Always draw (transition between covered/uncovered)
+ else if (_grdSpriteMode == 3) {
+ shouldDraw = true;
+ }
+ // Mode 4: Always draw (alternative uncovered position)
+ else if (_grdSpriteMode == 4) {
+ shouldDraw = true;
+ }
+
+ if (shouldDraw) {
+ // Use sprite 0 (primary frame)
+ int spriteW = _grd001Sprite->getCharWidth(0);
+ int spriteH = _grd001Sprite->getCharHeight(0);
+ // Center the sprite at the calculated position
+ int drawX = shipX - spriteW / 2;
+ int drawY = shipY - spriteH / 2;
+
+ renderNutSprite(renderBitmap, pitch, width, height, drawX, drawY, _grd001Sprite, 0);
+
+ debug("Rebel2 Handler25: GRD001 at (%d,%d) base(%d,%d) offset(%d,%d) size(%d,%d) mode=%d damage=%d",
+ drawX, drawY, baseX, baseY, _rebelViewOffset2X, _rebelViewOffset2Y,
+ spriteW, spriteH, _grdSpriteMode, _rebelDamageLevel);
+ }
+ }
+
+ // _grd002Sprite (GRD002) is always drawn if it exists (from FUN_41DB5E line 230)
+ // The sprite index is calculated based on damage level and aiming position
+ // From FUN_0041db5e lines 160-168:
+ // If damage == 0: index = yZone * 5 + xZone + 5 (aiming-based, 5-14)
+ // If damage != 0:
+ // If direction == 0: index = 5 - damage (0-5, covered left)
+ // If direction != 0: index = 25 - damage (20-25, covered right)
+ if (_grd002Sprite && _grd002Sprite->getNumChars() > 0) {
+ // Calculate sprite index based on damage level and direction
+ int spriteIdx;
+ int numSprites = _grd002Sprite->getNumChars();
+
+ if (_rebelDamageLevel == 0) {
+ // Uncovered state: use aiming-based sprite selection (5-14)
+ // For now, use center position (xZone=2, yZone=0) -> 0*5+2+5 = 7
+ // TODO: Calculate actual zone from crosshair position
+ int xZone = 2; // Center horizontal (0-4)
+ int yZone = 0; // Top vertical (0-1)
+
+ // Direction-based mirroring (line 161-162)
+ if (_rebelFlightDir == (yZone & 1)) {
+ xZone = 4 - xZone;
+ }
+
+ spriteIdx = yZone * 5 + xZone + 5;
+ } else {
+ // Transitioning/covered state: use direction-based sprite
+ if (_rebelFlightDir == 0) {
+ // Direction 0: sprites 0-5 (covered left)
+ spriteIdx = 5 - _rebelDamageLevel;
+ } else {
+ // Direction 1: sprites 20-25 (covered right)
+ spriteIdx = 25 - _rebelDamageLevel;
+ }
+ }
+
+ // Clamp to valid range
+ if (spriteIdx < 0) spriteIdx = 0;
+ if (spriteIdx >= numSprites) spriteIdx = numSprites - 1;
+
+ int spriteW = _grd002Sprite->getCharWidth(spriteIdx);
+ int spriteH = _grd002Sprite->getCharHeight(spriteIdx);
+
+ // Position offset from GRD002 sprite header (lines 238-243 in disasm)
+ // This adds an offset from the sprite's internal positioning data
+ int drawX = shipX - spriteW / 2;
+ int drawY = shipY - spriteH / 2;
+
+ renderNutSprite(renderBitmap, pitch, width, height, drawX, drawY, _grd002Sprite, spriteIdx);
+
+ debug("Rebel2 Handler25: GRD002 at (%d,%d) size(%d,%d) spriteIdx=%d damage=%d dir=%d",
+ drawX, drawY, spriteW, spriteH, spriteIdx, _rebelDamageLevel, _rebelFlightDir);
+ }
+}
+
void InsaneRebel2::renderFallbackShip(byte *renderBitmap, int pitch, int width, int height) {
// Fallback: Use embedded HUD frame as ship sprite (Level 3 style)
// userId=11 contains the ship sprite strip
diff --git a/engines/scumm/insane/insane_rebel.h b/engines/scumm/insane/insane_rebel.h
index 4dc42b6160d..0b8a32d2974 100644
--- a/engines/scumm/insane/insane_rebel.h
+++ b/engines/scumm/insane/insane_rebel.h
@@ -329,8 +329,11 @@ public:
// Load turret HUD overlay NUT from ANIM data
bool loadTurretHudOverlay(byte *animData, int32 size, int16 par3);
- // Load Handler 8 ship POV NUT sprites from ANIM data
- bool loadHandler8ShipSprites(byte *animData, int32 size, int16 par3);
+ // Load Handler 8 ship POV NUT sprites from ANIM data (par4 = sprite type: 1,3,6,7)
+ bool loadHandler8ShipSprites(byte *animData, int32 size, int16 par4);
+
+ // Load Handler 25 GRD NUT sprites from ANIM data (par4 = sprite type: 1,2)
+ bool loadHandler25GrdSprites(byte *animData, int32 size, int16 par4);
// Load Level 2 background from embedded ANIM
bool loadLevel2Background(byte *animData, int32 size, byte *renderBitmap);
@@ -597,6 +600,9 @@ public:
// Ship firing state (from mouse button)
bool _shipFiring;
+ // Previous mouse button state for edge detection (bit 0=left, bit 1=right, bit 2=middle)
+ uint32 _prevMouseButtons;
+
// Ship direction index for sprite selection (Handler 7)
// Calculated from ship position: horizontal * 7 + vertical
// horizontal: 0-4 (left to right), vertical: 0-6 (up to down)
@@ -630,6 +636,33 @@ public:
int16 _flyShipScreenX; // DAT_0044370e - Ship X screen position
int16 _flyShipScreenY; // DAT_0044370c - Ship Y screen position
+ // ======================= Handler 25 (0x19) GRD Ship System =======================
+ // For mixed mode missions (Level 2 speeder bike, etc.), Handler 25 uses GRD NUT
+ // sprites loaded via IACT opcode 8. The ship is rendered based on DAT_00457900 mode.
+ //
+ // Based on FUN_0041cadb case 6 and FUN_0041db5e disassembly:
+ // - DAT_00482240: Primary ship sprite (GRD001, par4=1)
+ // - DAT_00482238: Secondary ship sprite (GRD002, par4=2)
+ // - DAT_00457900: Sprite mode (1,2,3,4) controls which sprite to draw
+ // - DAT_00457910: Ship X screen position
+ // - DAT_00457912: Ship Y screen position
+ // - DAT_00457902: Flight direction (affects GRD002 mirroring)
+ // - DAT_0045790a: Damage level (affects rendering conditions)
+
+ NutRenderer *_grd001Sprite; // DAT_00482240 - GRD001 primary ship NUT
+ NutRenderer *_grd002Sprite; // DAT_00482238 - GRD002 secondary ship NUT
+
+ // Handler 25 sprite mode (DAT_00457900) - set by opcode 6 par3
+ // Controls which sprite variant to draw:
+ // 1: Draw _grd001Sprite normally
+ // 2: Draw _grd001Sprite only when damaged (DAT_0045790a != 0)
+ // 3: Draw _grd001Sprite and GRD005 (DAT_00482258) overlay
+ // 4: Draw _grd001Sprite with buffer offset
+ int16 _grdSpriteMode; // DAT_00457900
+
+ // Render Handler 25 ship sprites (called from procPostRendering)
+ void renderHandler25Ship(byte *renderBitmap, int pitch, int width, int height);
+
// ======================= Handler 0x26 Turret HUD Overlays =======================
// For turret missions (Level 1, etc.), Handler 0x26 uses NUT-based HUD overlays
// loaded via IACT opcode 8. These contain animated cockpit panel elements.
Commit: 9ce2e159b1c50140c07c06a741cb2117e141e33a
https://github.com/scummvm/scummvm/commit/9ce2e159b1c50140c07c06a741cb2117e141e33a
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:08:58+02:00
Commit Message:
SCUMM: RA2: Implement level selection screen
Changed paths:
engines/scumm/insane/insane_rebel.cpp
engines/scumm/insane/insane_rebel.h
engines/scumm/scumm.cpp
diff --git a/engines/scumm/insane/insane_rebel.cpp b/engines/scumm/insane/insane_rebel.cpp
index 2334a645346..35563e955ad 100644
--- a/engines/scumm/insane/insane_rebel.cpp
+++ b/engines/scumm/insane/insane_rebel.cpp
@@ -318,9 +318,25 @@ InsaneRebel2::InsaneRebel2(ScummEngine_v7 *scumm) {
_levelUnlocked[i] = (i == 0); // Only level 1 unlocked initially
}
- // Initialize level selection system
+ // Initialize chapter selection system (FUN_00415CF8)
+ // 17 items: 16 chapters + BACK option
+ _chapterSelection = 0; // First chapter selected
+ _chapterItemCount = 17; // 16 chapters + BACK
+ _selectedChapter = 0; // Default selected chapter
+ _passwordInput = ""; // No password input
+ for (i = 0; i < 16; i++) {
+ _chapterUnlocked[i] = (i == 0); // Only chapter 1 unlocked initially
+ }
+
+ // Initialize preview offset for chapter selection (FUN_00425170)
+ // X offset: -90 (0xffa6), Y offset: selection * -50 + 75
+ _previewOffsetX = -90;
+ _previewOffsetY = 75; // Chapter 0: 0 * -50 + 75 = 75
+
+ // Initialize pilot selection system (FUN_00414A41)
+ // Menu structure: 6 levels + 4 options (NEW PILOT, DELETE PILOT, COPY PILOT, MAIN MENU)
_levelSelection = 0; // First level selected
- _levelItemCount = 2; // Level 1 + MAIN MENU (will grow as more levels implemented)
+ _levelItemCount = 10; // 6 levels + 4 options
_selectedLevel = 1; // Default selected level
// Initialize menu input capture system
@@ -3408,29 +3424,63 @@ void InsaneRebel2::procPostRendering(byte *renderBitmap, int32 codecparam, int32
// Check if we're in menu mode (menu state + intro flag)
bool menuMode = (introPlaying && _gameState == kStateMainMenu);
- bool levelSelectMode = (introPlaying && _gameState == kStateLevelSelect);
+ bool pilotSelectMode = (introPlaying && _gameState == kStatePilotSelect);
+ bool chapterSelectMode = (introPlaying && _gameState == kStateChapterSelect);
- // Handle level selection input and rendering
- if (levelSelectMode) {
- // Show the standard Windows arrow cursor (same as menu)
+ // Handle pilot selection input and rendering (FUN_00414A41)
+ // This is the pilot/save slot selection screen with centered menu
+ if (pilotSelectMode) {
+ // Show the standard Windows arrow cursor
Graphics::Cursor *cursor = Graphics::makeDefaultWinCursor();
CursorMan.replaceCursor(cursor);
delete cursor;
CursorMan.showMouse(true);
- // Process level selection input
+ // Process pilot selection input - emulates FUN_00414A41 input handling
int selection = processLevelSelectInput();
- // Draw level selection overlay
+ // Draw pilot selection overlay - centered menu like main menu
drawLevelSelectOverlay(renderBitmap, pitch, width, height);
// If a selection was confirmed, signal video to stop
if (selection >= 0) {
- debug("Rebel2: Level selection confirmed: %d", selection);
+ debug("Rebel2: Pilot selection confirmed: %d", selection);
_vm->_smushVideoShouldFinish = true;
}
- // Skip normal HUD rendering in level select mode
+ // Skip normal HUD rendering in pilot select mode
+ return;
+ }
+
+ // Handle chapter selection input and rendering (FUN_00415CF8)
+ // This is the actual level/chapter selection screen with preview and password
+ if (chapterSelectMode) {
+ // Show the standard Windows arrow cursor (same as menu)
+ Graphics::Cursor *cursor = Graphics::makeDefaultWinCursor();
+ CursorMan.replaceCursor(cursor);
+ delete cursor;
+ CursorMan.showMouse(true);
+
+ // Fill screen with BLACK background
+ // The original uses O_LEVEL.SAN (640x400) which has a black background.
+ // For 320x200 mode, we just fill with black (color index 0).
+ for (int y = 0; y < height; y++) {
+ memset(renderBitmap + y * pitch, 0, width);
+ }
+
+ // Process chapter selection input - emulates FUN_00415CF8 input handling
+ int selection = processChapterSelectInput();
+
+ // Draw chapter selection overlay - emulates FUN_00415CF8 rendering
+ drawChapterSelectOverlay(renderBitmap, pitch, width, height);
+
+ // If a selection was confirmed, signal video to stop
+ if (selection >= 0) {
+ debug("Rebel2: Chapter selection confirmed: %d", selection);
+ _vm->_smushVideoShouldFinish = true;
+ }
+
+ // Skip normal HUD rendering in chapter select mode
return;
}
@@ -5067,15 +5117,15 @@ int InsaneRebel2::runMainMenu() {
// case 5: Credits (play O_CREDIT.SAN, then return 1)
// case 6: Quit (stop video, exit)
switch (_menuSelection) {
- case 0: // Start Game -> Level Selection
- debug("Rebel2: Start Game selected - going to level selection");
- _gameState = kStateLevelSelect;
+ case 0: // Start Game -> Pilot Selection
+ debug("Rebel2: Start Game selected - going to pilot selection");
+ _gameState = kStatePilotSelect;
_menuInputActive = false;
- return kMenuContinue; // Go to level selection
+ return kMenuContinue; // Go to pilot selection
case 1: // Continue (same as Start Game for now)
- debug("Rebel2: Continue selected - going to level selection");
- _gameState = kStateLevelSelect;
+ debug("Rebel2: Continue selected - going to pilot selection");
+ _gameState = kStatePilotSelect;
_menuInputActive = false;
return kMenuContinue;
@@ -5118,46 +5168,798 @@ int InsaneRebel2::runMainMenu() {
return 0;
}
-// ==================== Level Selection Menu ====================
-// Emulates FUN_00414A41 - Level selection menu
-// For now, only Level 1 is available. This will be expanded later.
+// ==================== Chapter Selection Screen ====================
+// Emulates FUN_00415CF8 - Chapter selection with preview and password input
+// This is the actual level/chapter selection that players see after pilot select
+
+int InsaneRebel2::runChapterSelect() {
+ // Chapter selection screen loop - emulates FUN_00415CF8
+ // Returns:
+ // kChapterSelectPlay (5) = Play selected chapter
+ // kChapterSelectBack (2) = Return to main menu (ESC or BACK)
+ // kChapterSelectQuit (0) = Quit game
+
+ debug("Rebel2: Entering chapter selection (FUN_00415CF8)");
+
+ // Enable menu input capture
+ _menuInputActive = true;
+ while (!_menuEventQueue.empty()) _menuEventQueue.pop();
+
+ // Initialize chapter selection state
+ _chapterSelection = 0;
+ _chapterItemCount = 17; // 16 chapters + BACK
+ _selectedChapter = 0;
+ _passwordInput = "";
+ _menuRepeatDelay = 0;
+ _gameState = kStateChapterSelect;
+
+ SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
+
+ // Chapter selection background - emulates FUN_00415CF8 line 57
+ // Original uses O_LEVEL.SAN (640x400). We use menu video for 320x200 mode.
+ while (!_vm->shouldQuit()) {
+ _vm->_smushVideoShouldFinish = false;
+
+ Common::String menuVideo = getRandomMenuVideo();
+ debug("Rebel2: Playing chapter select background: %s", menuVideo.c_str());
+
+ // Set video flags for menu mode
+ splayer->setCurVideoFlags(0x20);
+
+ // Play the menu video as chapter selection background
+ splayer->play(menuVideo.c_str(), 12);
+
+ if (_vm->shouldQuit()) {
+ _menuInputActive = false;
+ return kChapterSelectQuit;
+ }
+
+ // If video ended without selection, continue looping
+ if (!_vm->_smushVideoShouldFinish) {
+ continue;
+ }
+
+ _vm->_smushVideoShouldFinish = false;
+
+ debug("Rebel2: Chapter selection made: %d", _chapterSelection);
+
+ // Process chapter selection (lines 134-236 of FUN_00415CF8)
+ if (_chapterSelection == 16) {
+ // BACK selected (index 16 = 17th item)
+ debug("Rebel2: BACK to main menu selected");
+ _menuInputActive = false;
+ return kChapterSelectBack;
+ }
+
+ if (_chapterSelection >= 0 && _chapterSelection < 16) {
+ // Chapter selected
+ if (_chapterUnlocked[_chapterSelection]) {
+ // Chapter is unlocked - start it
+ _selectedChapter = _chapterSelection;
+ debug("Rebel2: Chapter %d selected (unlocked)", _selectedChapter + 1);
+ _menuInputActive = false;
+ return kChapterSelectPlay;
+ } else {
+ // Chapter is locked - check password (lines 239-257 of FUN_00415CF8)
+ // For now, just play error sound and continue
+ debug("Rebel2: Chapter %d is locked", _chapterSelection + 1);
+ continue;
+ }
+ }
+ }
+
+ _menuInputActive = false;
+ return kChapterSelectQuit;
+}
+
+int InsaneRebel2::processChapterSelectInput() {
+ // Process input for chapter selection screen
+ // Emulates input handling in FUN_00415CF8 (lines 95-133)
+ // Returns: -1 = no action, 0+ = item selected
+
+ int result = -1;
+
+ while (!_menuEventQueue.empty()) {
+ Common::Event event = _menuEventQueue.front();
+ _menuEventQueue.pop();
+
+ switch (event.type) {
+ case Common::EVENT_KEYDOWN:
+ switch (event.kbd.keycode) {
+ case Common::KEYCODE_UP:
+ // Move selection up, wrap to bottom
+ _chapterSelection--;
+ if (_chapterSelection < 0) {
+ _chapterSelection = _chapterItemCount - 1;
+ }
+ debug("ChapterSelect: Selection changed to %d (UP)", _chapterSelection);
+ break;
+
+ case Common::KEYCODE_DOWN:
+ // Move selection down, wrap to top
+ _chapterSelection++;
+ if (_chapterSelection >= _chapterItemCount) {
+ _chapterSelection = 0;
+ }
+ debug("ChapterSelect: Selection changed to %d (DOWN)", _chapterSelection);
+ break;
+
+ case Common::KEYCODE_RETURN:
+ case Common::KEYCODE_KP_ENTER:
+ if (_chapterSelection >= 0 && _chapterSelection < _chapterItemCount) {
+ result = _chapterSelection;
+ debug("ChapterSelect: Item %d selected (ENTER)", _chapterSelection);
+ }
+ break;
+
+ case Common::KEYCODE_ESCAPE:
+ // ESC = Back to main menu (same as selecting BACK)
+ result = 16; // BACK index
+ debug("ChapterSelect: ESC pressed - back to menu");
+ break;
+
+ case Common::KEYCODE_BACKSPACE:
+ // Backspace for password input (line 107-112 of FUN_00415CF8)
+ if (!_passwordInput.empty()) {
+ _passwordInput.deleteLastChar();
+ debug("ChapterSelect: Password backspace, now: %s", _passwordInput.c_str());
+ }
+ break;
+
+ default:
+ // Printable character for password input (lines 114-121 of FUN_00415CF8)
+ if (event.kbd.ascii >= 0x20 && event.kbd.ascii <= 0x7E) {
+ if (_passwordInput.size() < 8) {
+ _passwordInput += (char)event.kbd.ascii;
+ debug("ChapterSelect: Password input: %s", _passwordInput.c_str());
+ }
+ }
+ break;
+ }
+ break;
+
+ case Common::EVENT_LBUTTONDOWN:
+ {
+ // Mouse click - check if clicking on a menu item
+ // From FUN_0041F5AE assembly (low-res 320x200):
+ // Item Y = 17 * -5 + i * 10 + 104 = 19 + i * 10
+ // Chapters 0-14 at Y = 19 + i*10
+ // FINALE (15) at Y = 19 + 15*10 = 169
+ // RETURN TO PILOTS (16) at Y = 19 + 16*10 = 179
+ int baseY = 19;
+ int itemHeight = 10;
+ int mouseY = event.mouse.y;
+
+ // Check chapters 0-14
+ for (int i = 0; i < 15; i++) {
+ int itemY = baseY + i * itemHeight;
+ if (mouseY >= itemY - 4 && mouseY < itemY + 6) {
+ _chapterSelection = i;
+ result = i;
+ debug("ChapterSelect: Chapter %d clicked at Y=%d", i + 1, mouseY);
+ break;
+ }
+ }
+
+ // Check FINALE (index 15) at Y=169
+ int finaleY = baseY + 15 * itemHeight;
+ if (mouseY >= finaleY - 4 && mouseY < finaleY + 6) {
+ _chapterSelection = 15;
+ result = 15;
+ debug("ChapterSelect: FINALE clicked");
+ }
+
+ // Check RETURN TO PILOTS (index 16) at Y=179
+ int returnY = baseY + 16 * itemHeight;
+ if (mouseY >= returnY - 4 && mouseY < returnY + 6) {
+ _chapterSelection = 16;
+ result = 16;
+ debug("ChapterSelect: RETURN TO PILOTS clicked");
+ }
+ }
+ break;
+
+ default:
+ break;
+ }
+ }
+
+ return result;
+}
+
+// Draw preview box border - emulates FUN_004292D0 calls at lines 128-133 of FUN_00415CF8
+void InsaneRebel2::drawPreviewBox(byte *renderBitmap, int pitch, int width, int height) {
+ // Low-res (320x200) coordinates from FUN_00415CF8:
+ // Outer box: X=0xe4 (228), Y=0x49 (73), W=0x54 (84), H=0x36 (54), color=0xF8
+ // Inner box: X=0xe5 (229), Y=0x4a (74), W=0x52 (82), H=0x34 (52), color=4
+
+ // Outer border (bright)
+ int outerX = 228, outerY = 73, outerW = 84, outerH = 54;
+ byte outerColor = 0xF8;
+
+ // Draw outer box edges
+ // Top edge
+ for (int px = outerX; px < outerX + outerW && px < width; px++) {
+ if (outerY >= 0 && outerY < height && px >= 0)
+ renderBitmap[outerY * pitch + px] = outerColor;
+ }
+ // Bottom edge
+ int bottomY = outerY + outerH - 1;
+ if (bottomY < height) {
+ for (int px = outerX; px < outerX + outerW && px < width; px++) {
+ if (px >= 0)
+ renderBitmap[bottomY * pitch + px] = outerColor;
+ }
+ }
+ // Left edge
+ for (int py = outerY; py < outerY + outerH && py < height; py++) {
+ if (py >= 0 && outerX >= 0 && outerX < width)
+ renderBitmap[py * pitch + outerX] = outerColor;
+ }
+ // Right edge
+ int rightX = outerX + outerW - 1;
+ if (rightX < width) {
+ for (int py = outerY; py < outerY + outerH && py < height; py++) {
+ if (py >= 0)
+ renderBitmap[py * pitch + rightX] = outerColor;
+ }
+ }
+
+ // Inner border (dark)
+ int innerX = 229, innerY = 74, innerW = 82, innerH = 52;
+ byte innerColor = 4;
+
+ // Top edge
+ for (int px = innerX; px < innerX + innerW && px < width; px++) {
+ if (innerY >= 0 && innerY < height && px >= 0)
+ renderBitmap[innerY * pitch + px] = innerColor;
+ }
+ // Bottom edge
+ bottomY = innerY + innerH - 1;
+ if (bottomY < height) {
+ for (int px = innerX; px < innerX + innerW && px < width; px++) {
+ if (px >= 0)
+ renderBitmap[bottomY * pitch + px] = innerColor;
+ }
+ }
+ // Left edge
+ for (int py = innerY; py < innerY + innerH && py < height; py++) {
+ if (py >= 0 && innerX >= 0 && innerX < width)
+ renderBitmap[py * pitch + innerX] = innerColor;
+ }
+ // Right edge
+ rightX = innerX + innerW - 1;
+ if (rightX < width) {
+ for (int py = innerY; py < innerY + innerH && py < height; py++) {
+ if (py >= 0)
+ renderBitmap[py * pitch + rightX] = innerColor;
+ }
+ }
+}
+
+// Draw preview thumbnail content - emulates FUN_00429b40 scaled preview draw
+// Shows chapter number and unlock status inside the preview box
+void InsaneRebel2::drawPreviewThumbnail(byte *renderBitmap, int pitch, int width, int height, int chapter) {
+ // Preview area coordinates (inside the inner border)
+ // Inner box: X=230 (229+1), Y=75 (74+1), W=80 (82-2), H=50 (52-2)
+ int previewX = 230;
+ int previewY = 75;
+ int previewW = 80;
+ int previewH = 50;
+
+ // Update preview offset based on chapter selection (FUN_00425170)
+ // Y offset = chapter * -50 + 75
+ _previewOffsetY = chapter * -50 + 75;
+
+ // Determine fill color based on chapter state
+ // Unlocked: dark blue (0x10), Locked: dark gray (0x08)
+ byte fillColor = (_chapterUnlocked[chapter]) ? 0x10 : 0x08;
+
+ // Fill the preview area with background color
+ for (int py = previewY; py < previewY + previewH && py < height; py++) {
+ if (py >= 0) {
+ for (int px = previewX; px < previewX + previewW && px < width; px++) {
+ if (px >= 0)
+ renderBitmap[py * pitch + px] = fillColor;
+ }
+ }
+ }
+
+ // Draw chapter number in the center of the preview area
+ NutRenderer *font = _smush_smalfontNut;
+ if (!font) return;
+
+ char chapterStr[16];
+ if (chapter < 15) {
+ snprintf(chapterStr, sizeof(chapterStr), "CH.%d", chapter + 1);
+ } else {
+ snprintf(chapterStr, sizeof(chapterStr), "FIN"); // FINALE
+ }
+
+ // Calculate text width for centering
+ int textWidth = 0;
+ int numChars = font->getNumChars();
+ for (const char *c = chapterStr; *c; c++) {
+ int charIdx = (unsigned char)*c;
+ if (charIdx < numChars) {
+ textWidth += font->getCharWidth(charIdx);
+ }
+ }
+
+ // Center the text in the preview area
+ int textX = previewX + (previewW - textWidth) / 2;
+ int textY = previewY + previewH / 2 - 4; // Center vertically (approx)
+
+ Common::Rect clipRect(0, 0, width, height);
+
+ // Draw the chapter text
+ int curX = textX;
+ for (const char *c = chapterStr; *c; c++) {
+ int charIdx = (unsigned char)*c;
+ if (charIdx < numChars) {
+ int charWidth = font->getCharWidth(charIdx);
+ if (curX >= 0 && curX + charWidth <= width && textY >= 0 && textY < height) {
+ font->drawCharV7(renderBitmap, clipRect, curX, textY, pitch, -1,
+ kStyleAlignLeft, charIdx, true, true);
+ }
+ curX += charWidth;
+ }
+ }
+
+ // Draw lock icon for locked chapters (simple "X" pattern)
+ if (!_chapterUnlocked[chapter]) {
+ byte lockColor = 0xF8; // Bright red/orange
+ int lockX = previewX + previewW - 15;
+ int lockY = previewY + 5;
+
+ // Draw small X to indicate locked
+ for (int i = 0; i < 8; i++) {
+ if (lockX + i < width && lockY + i < height)
+ renderBitmap[(lockY + i) * pitch + lockX + i] = lockColor;
+ if (lockX + 7 - i < width && lockY + i < height)
+ renderBitmap[(lockY + i) * pitch + lockX + 7 - i] = lockColor;
+ }
+ }
+}
+
+// Draw left-aligned menu item - emulates FUN_0041F5AE with param_4=1
+void InsaneRebel2::drawLeftAlignedMenuItem(byte *renderBitmap, int pitch, int width, int height,
+ const char *text, int x, int y, bool selected) {
+ NutRenderer *font = _smush_smalfontNut;
+ if (!font) return;
+
+ int numFontChars = font->getNumChars();
+ Common::Rect clipRect(0, 0, width, height);
+
+ // Calculate text width for selection box
+ int textWidth = 0;
+ for (const char *c = text; *c; c++) {
+ int charIdx = (unsigned char)*c;
+ if (charIdx < numFontChars) {
+ textWidth += font->getCharWidth(charIdx);
+ }
+ }
+
+ // Draw selection box if selected
+ // Box dimensions: width = textWidth + 6, height = 10 (low-res)
+ if (selected) {
+ static int frameCounter = 0;
+ frameCounter++;
+
+ int boxWidth = textWidth + 6;
+ int boxHeight = 10;
+ int boxX = x - 3; // Left-aligned, so box starts 3 pixels before text
+ int boxY = y - 1;
+
+ // Flashing color (emulates (-((DAT_0047a7e4 & 1) == 0) & 8U) - 0x10)
+ byte highlightColor = (frameCounter & 1) ? 0xF8 : 0xF0;
+
+ // Draw box border
+ if (boxY >= 0 && boxY < height && boxX >= 0) {
+ // Top edge
+ for (int px = boxX; px < boxX + boxWidth && px < width; px++) {
+ if (px >= 0) renderBitmap[boxY * pitch + px] = highlightColor;
+ }
+ // Bottom edge
+ int bottomY = boxY + boxHeight - 1;
+ if (bottomY < height) {
+ for (int px = boxX; px < boxX + boxWidth && px < width; px++) {
+ if (px >= 0) renderBitmap[bottomY * pitch + px] = highlightColor;
+ }
+ }
+ // Left edge
+ for (int py = boxY; py < boxY + boxHeight && py < height; py++) {
+ if (py >= 0 && boxX >= 0) renderBitmap[py * pitch + boxX] = highlightColor;
+ }
+ // Right edge
+ int rightX = boxX + boxWidth - 1;
+ if (rightX < width) {
+ for (int py = boxY; py < boxY + boxHeight && py < height; py++) {
+ if (py >= 0) renderBitmap[py * pitch + rightX] = highlightColor;
+ }
+ }
+ }
+ }
+
+ // Draw text characters
+ int curX = x;
+ for (const char *c = text; *c; c++) {
+ int charIdx = (unsigned char)*c;
+ if (charIdx < numFontChars) {
+ int charWidth = font->getCharWidth(charIdx);
+ if (curX >= 0 && curX + charWidth <= width && y >= 0 && y < height) {
+ font->drawCharV7(renderBitmap, clipRect, curX, y, pitch, -1,
+ kStyleAlignLeft, charIdx, true, true);
+ }
+ curX += charWidth;
+ }
+ }
+}
+
+// Draw password input field - emulates lines 106-125 of FUN_00415CF8
+void InsaneRebel2::drawPasswordInput(byte *renderBitmap, int pitch, int width, int height) {
+ // Password display position for low-res: X=30 (0x1e), Y=190 (0xbe)
+ int infoX = 30;
+ int infoY = 190;
+
+ // Build display string with cursor
+ static int frameCounter = 0;
+ frameCounter++;
+ char cursor = (frameCounter & 2) ? '_' : ' '; // Blinking cursor
+
+ char displayText[32];
+ snprintf(displayText, sizeof(displayText), "ACCESS CODE: %s%c", _passwordInput.c_str(), cursor);
+
+ NutRenderer *font = _smush_smalfontNut;
+ if (!font) return;
+
+ int numFontChars = font->getNumChars();
+ Common::Rect clipRect(0, 0, width, height);
+
+ int curX = infoX;
+ for (const char *c = displayText; *c; c++) {
+ int charIdx = (unsigned char)*c;
+ if (charIdx < numFontChars) {
+ int charWidth = font->getCharWidth(charIdx);
+ if (curX >= 0 && curX + charWidth <= width && infoY >= 0 && infoY < height) {
+ font->drawCharV7(renderBitmap, clipRect, curX, infoY, pitch, -1,
+ kStyleAlignLeft, charIdx, true, true);
+ }
+ curX += charWidth;
+ }
+ }
+}
+
+// Draw score/time display - emulates lines 99-104 of FUN_00415CF8
+void InsaneRebel2::drawScoreDisplay(byte *renderBitmap, int pitch, int width, int height, int chapter) {
+ // Score display position for low-res: X=25 (0x19), Y=190 (0xbe)
+ int infoX = 25;
+ int infoY = 190;
+
+ // For now, just display a placeholder score
+ char displayText[32];
+ snprintf(displayText, sizeof(displayText), "SCORE: %d", 0);
+
+ NutRenderer *font = _smush_smalfontNut;
+ if (!font) return;
+
+ int numFontChars = font->getNumChars();
+ Common::Rect clipRect(0, 0, width, height);
+
+ int curX = infoX;
+ for (const char *c = displayText; *c; c++) {
+ int charIdx = (unsigned char)*c;
+ if (charIdx < numFontChars) {
+ int charWidth = font->getCharWidth(charIdx);
+ if (curX >= 0 && curX + charWidth <= width && infoY >= 0 && infoY < height) {
+ font->drawCharV7(renderBitmap, clipRect, curX, infoY, pitch, -1,
+ kStyleAlignLeft, charIdx, true, true);
+ }
+ curX += charWidth;
+ }
+ }
+}
+
+// Draw chapter selection overlay - called during O_LEVEL.SAN playback
+// FUN_00415CF8 - Chapter selection screen with preview thumbnail
+void InsaneRebel2::drawChapterSelectOverlay(byte *renderBitmap, int pitch, int width, int height) {
+ // Draw chapter selection screen overlay
+ // Emulates rendering in FUN_00415CF8
+ //
+ // Layout (320x200 mode):
+ // - Title "Chapters" at top-left using TITLFONT
+ // - 15 chapters + FINALE + RETURN TO PILOTS (17 items total)
+ // - Three preview boxes on right side
+ // - Status bar at bottom: "PILOTS: X SCORE: Y RANK:"
+
+ // Chapter names from original game (DAT_00457820)
+ // Format: "CHAPTER X - NAME" for unlocked, "CHAPTER X -" for locked
+ static const char *chapterNames[] = {
+ "THE DREIGHTON TRIANGLE", // Chapter 1
+ "ASTEROID PURSUIT", // Chapter 2
+ "ABOARD THE TERROR", // Chapter 3
+ "TIE FIGHTER ATTACK", // Chapter 4
+ "DREIGHTON NEBULA", // Chapter 5
+ "CORELLIA", // Chapter 6
+ "SPEEDER PURSUIT", // Chapter 7
+ "CANYON CHASE", // Chapter 8
+ "DEATH STAR APPROACH", // Chapter 9
+ "THE ASTEROID FIELD", // Chapter 10
+ "INSIDE THE DEATH STAR", // Chapter 11
+ "TRENCH RUN", // Chapter 12
+ "THE MAIN REACTOR", // Chapter 13
+ "ESCAPE FROM YAVIN", // Chapter 14
+ "FINALE", // Chapter 15 is actually FINALE
+ };
+
+ // Frame counter for flashing selection box
+ static int frameCounter = 0;
+ frameCounter++;
+
+ NutRenderer *menuFont = _smush_smalfontNut;
+ NutRenderer *titleFont = _smush_titlefontNut;
+ if (!menuFont) return;
+
+ Common::Rect clipRect(0, 0, width, height);
+ int numFontChars = menuFont->getNumChars();
+
+ // === Draw title "Chapters" using TITLFONT ===
+ // From FUN_0041F5AE param_4=1 (left-aligned mode), low-res (320x200):
+ // Title X = 0x28 = 40, Title Y = param_3 * -5 + 0x56 = 17 * -5 + 86 = 1
+ int titleX = 40;
+ int titleY = 1;
+
+ if (titleFont) {
+ const char *titleText = "Chapters";
+ int curX = titleX;
+ int numTitleChars = titleFont->getNumChars();
+ for (const char *c = titleText; *c; c++) {
+ int charIdx = (unsigned char)*c;
+ if (charIdx < numTitleChars) {
+ int charWidth = titleFont->getCharWidth(charIdx);
+ if (curX >= 0 && curX + charWidth <= width && titleY >= 0) {
+ titleFont->drawCharV7(renderBitmap, clipRect, curX, titleY, pitch, -1,
+ kStyleAlignLeft, charIdx, true, true);
+ }
+ curX += charWidth;
+ }
+ }
+ }
+
+ // === Draw chapter list (left-aligned) ===
+ // From FUN_0041F5AE param_4=1 (left-aligned mode), low-res (320x200):
+ // Item X = 0x17 = 23
+ // Item Y = param_3 * -5 + local_c * 10 + 0x68 = 17 * -5 + i * 10 + 104 = 19 + i * 10
+ // Selection Box X = 0x14 = 20, Y = 18 + i * 10 (item Y - 1)
+ int itemX = 23; // From assembly: 0x17 = 23
+ int itemBaseY = 19; // From assembly: 17 * -5 + 104 = 19
+ int itemSpacing = 10; // From assembly: 10 pixels between items
+ int selBoxX = 20; // From assembly: 0x14 = 20
+
+ // Draw 15 chapters (indices 0-14)
+ for (int i = 0; i < 15; i++) {
+ int itemY = itemBaseY + i * itemSpacing;
+ bool isSelected = (i == _chapterSelection);
+
+ // Build chapter string: "CHAPTER X - NAME" or "CHAPTER X -"
+ char chapterStr[64];
+ snprintf(chapterStr, sizeof(chapterStr), "CHAPTER %d - %s", i + 1,
+ _chapterUnlocked[i] ? chapterNames[i] : "");
+
+ // Calculate text width for selection box
+ int textWidth = 0;
+ for (const char *c = chapterStr; *c; c++) {
+ int charIdx = (unsigned char)*c;
+ if (charIdx < numFontChars) {
+ textWidth += menuFont->getCharWidth(charIdx);
+ }
+ }
+
+ // Draw selection box if selected (red border)
+ // From assembly: box starts at X=0x14=20, Y = itemY - 1
+ if (isSelected) {
+ int boxWidth = textWidth + 6; // From assembly: textWidth + 6
+ int boxHeight = 10; // From assembly: 10 pixels
+ int boxX = selBoxX; // From assembly: 0x14 = 20
+ int boxY = itemY - 1; // From assembly: itemY - 1
+
+ // Flashing color from assembly: (-((DAT_0047a7e4 & 1) == 0) & 8U) - 0x10
+ // This gives -16 or -8, which are palette-relative negative offsets
+ byte boxColor = (frameCounter & 1) ? 0xF8 : 0xF0;
+
+ // Draw box border
+ if (boxY >= 0 && boxY + boxHeight <= height && boxX >= 0 && boxX + boxWidth <= width) {
+ for (int px = boxX; px < boxX + boxWidth; px++) {
+ renderBitmap[boxY * pitch + px] = boxColor;
+ renderBitmap[(boxY + boxHeight - 1) * pitch + px] = boxColor;
+ }
+ for (int py = boxY; py < boxY + boxHeight; py++) {
+ renderBitmap[py * pitch + boxX] = boxColor;
+ renderBitmap[py * pitch + boxX + boxWidth - 1] = boxColor;
+ }
+ }
+ }
+
+ // Draw chapter text
+ int curX = itemX;
+ for (const char *c = chapterStr; *c; c++) {
+ int charIdx = (unsigned char)*c;
+ if (charIdx < numFontChars) {
+ int charWidth = menuFont->getCharWidth(charIdx);
+ if (curX >= 0 && curX + charWidth <= width && itemY >= 0 && itemY < height) {
+ menuFont->drawCharV7(renderBitmap, clipRect, curX, itemY, pitch, -1,
+ kStyleAlignLeft, charIdx, true, true);
+ }
+ curX += charWidth;
+ }
+ }
+ }
+
+ // Draw FINALE (chapter 16 = index 15)
+ // Position: Y = 19 + 15*10 = 169
+ int finaleY = itemBaseY + 15 * itemSpacing;
+ bool finaleSelected = (_chapterSelection == 15);
+ const char *finaleStr = "FINALE -";
+
+ int finaleWidth = 0;
+ for (const char *c = finaleStr; *c; c++) {
+ int charIdx = (unsigned char)*c;
+ if (charIdx < numFontChars) finaleWidth += menuFont->getCharWidth(charIdx);
+ }
+
+ if (finaleSelected) {
+ int boxWidth = finaleWidth + 6;
+ int boxHeight = 10;
+ int boxX = selBoxX; // Use assembly value: 20
+ int boxY = finaleY - 1;
+ byte boxColor = (frameCounter & 1) ? 0xF8 : 0xF0;
+
+ if (boxY >= 0 && boxY + boxHeight <= height && boxX >= 0 && boxX + boxWidth <= width) {
+ for (int px = boxX; px < boxX + boxWidth; px++) {
+ renderBitmap[boxY * pitch + px] = boxColor;
+ renderBitmap[(boxY + boxHeight - 1) * pitch + px] = boxColor;
+ }
+ for (int py = boxY; py < boxY + boxHeight; py++) {
+ renderBitmap[py * pitch + boxX] = boxColor;
+ renderBitmap[py * pitch + boxX + boxWidth - 1] = boxColor;
+ }
+ }
+ }
+
+ int curX = itemX;
+ for (const char *c = finaleStr; *c; c++) {
+ int charIdx = (unsigned char)*c;
+ if (charIdx < numFontChars) {
+ int charWidth = menuFont->getCharWidth(charIdx);
+ if (curX >= 0 && curX + charWidth <= width && finaleY >= 0) {
+ menuFont->drawCharV7(renderBitmap, clipRect, curX, finaleY, pitch, -1,
+ kStyleAlignLeft, charIdx, true, true);
+ }
+ curX += charWidth;
+ }
+ }
+
+ // Draw "RETURN TO PILOTS" (index 16)
+ // Position: Y = 19 + 16*10 = 179
+ int returnY = itemBaseY + 16 * itemSpacing;
+ bool returnSelected = (_chapterSelection == 16);
+ const char *returnStr = "RETURN TO PILOTS";
+
+ int returnWidth = 0;
+ for (const char *c = returnStr; *c; c++) {
+ int charIdx = (unsigned char)*c;
+ if (charIdx < numFontChars) returnWidth += menuFont->getCharWidth(charIdx);
+ }
+
+ if (returnSelected) {
+ int boxWidth = returnWidth + 6;
+ int boxHeight = 10;
+ int boxX = selBoxX; // Use assembly value: 20
+ int boxY = returnY - 1;
+ byte boxColor = (frameCounter & 1) ? 0xF8 : 0xF0;
+
+ if (boxY >= 0 && boxY + boxHeight <= height && boxX >= 0 && boxX + boxWidth <= width) {
+ for (int px = boxX; px < boxX + boxWidth; px++) {
+ renderBitmap[boxY * pitch + px] = boxColor;
+ renderBitmap[(boxY + boxHeight - 1) * pitch + px] = boxColor;
+ }
+ for (int py = boxY; py < boxY + boxHeight; py++) {
+ renderBitmap[py * pitch + boxX] = boxColor;
+ renderBitmap[py * pitch + boxX + boxWidth - 1] = boxColor;
+ }
+ }
+ }
+
+ curX = itemX;
+ for (const char *c = returnStr; *c; c++) {
+ int charIdx = (unsigned char)*c;
+ if (charIdx < numFontChars) {
+ int charWidth = menuFont->getCharWidth(charIdx);
+ if (curX >= 0 && curX + charWidth <= width && returnY >= 0) {
+ menuFont->drawCharV7(renderBitmap, clipRect, curX, returnY, pitch, -1,
+ kStyleAlignLeft, charIdx, true, true);
+ }
+ curX += charWidth;
+ }
+ }
+
+ // === Draw preview box on the right side ===
+ // From FUN_00415CF8 lines 128-133:
+ // Outer: X=228 (0xe4), Y=73 (0x49), W=84 (0x54), H=54 (0x36), color=0xF8
+ // Inner: X=229 (0xe5), Y=74 (0x4a), W=82 (0x52), H=52 (0x34), color=4
+ drawPreviewBox(renderBitmap, pitch, width, height);
+
+ // === Draw preview thumbnail inside the box ===
+ // Shows chapter number and unlock status for currently selected chapter
+ // Only draw for chapters 0-15 (not RETURN TO PILOTS at index 16)
+ if (_chapterSelection >= 0 && _chapterSelection < 16) {
+ drawPreviewThumbnail(renderBitmap, pitch, width, height, _chapterSelection);
+ }
+
+ // === Draw status bar at bottom ===
+ // From FUN_00415CF8 lines 101-103 (unlocked chapter score display):
+ // X = 0x19 + 0x19 = 50 (but we use 23 to align with menu items)
+ // Y = 0xbe = 190
+ int statusY = 190;
+ int statusX = 23; // Align with menu items
+
+ char statusStr[64];
+ snprintf(statusStr, sizeof(statusStr), "PILOTS: %d SCORE: %d RANK:", 4, _playerScore);
+
+ curX = statusX;
+ for (const char *c = statusStr; *c; c++) {
+ int charIdx = (unsigned char)*c;
+ if (charIdx < numFontChars) {
+ int charWidth = menuFont->getCharWidth(charIdx);
+ if (curX >= 0 && curX + charWidth <= width && statusY >= 0 && statusY < height) {
+ menuFont->drawCharV7(renderBitmap, clipRect, curX, statusY, pitch, -1,
+ kStyleAlignLeft, charIdx, true, true);
+ }
+ curX += charWidth;
+ }
+ }
+}
+
+// ==================== Pilot Selection Menu (FUN_00414A41) ====================
+// Emulates FUN_00414A41 - Pilot/save selection menu
+// This appears before chapter selection. All options go to chapter selection except MAIN MENU.
int InsaneRebel2::runLevelSelect() {
- // Level selection menu loop - emulates FUN_00414A41
+ // Pilot selection menu loop - emulates FUN_00414A41
// Returns:
- // kLevelSelectPlay (1) = Play selected level
+ // kLevelSelectPlay (1) = Go to chapter selection
// kLevelSelectBack (0) = Return to main menu
// kLevelSelectQuit (2) = Quit game
- debug("Rebel2: Entering level selection");
+ debug("Rebel2: Entering pilot selection (FUN_00414A41)");
// Enable menu input capture via EventObserver and clear any stale events
_menuInputActive = true;
while (!_menuEventQueue.empty()) _menuEventQueue.pop();
- // Initialize level selection state
+ // Initialize pilot selection state
+ // Menu structure from FUN_00414A41:
+ // Items 0-5: Pilot slots (PILOT 1-6)
+ // Item 6: NEW PILOT
+ // Item 7: DELETE PILOT
+ // Item 8: COPY PILOT
+ // Item 9: MAIN MENU
_levelSelection = 0;
- _levelItemCount = 7; // Selectable items: 6 levels + MAIN MENU
- _selectedLevel = 1; // Default to level 1
+ _levelItemCount = 10; // Selectable items: 6 pilots + 4 options
+ _selectedLevel = 1;
_menuRepeatDelay = 0;
- _gameState = kStateLevelSelect;
+ _gameState = kStatePilotSelect;
- // Get the SmushPlayer
SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
- // Level selection loop - we'll reuse the menu video as background
- // In the original, this uses the same O_MENU_X.SAN videos
+ // Pilot selection uses menu video as background (320x200 mode)
while (!_vm->shouldQuit()) {
_vm->_smushVideoShouldFinish = false;
- // Use a menu video as background for level selection
Common::String menuVideo = getRandomMenuVideo();
- debug("Rebel2: Playing level select background: %s", menuVideo.c_str());
+ debug("Rebel2: Playing pilot select background: %s", menuVideo.c_str());
- // Set video flags for menu mode
splayer->setCurVideoFlags(0x20);
-
- // Play the menu video - input is processed in procPostRendering
splayer->play(menuVideo.c_str(), 12);
if (_vm->shouldQuit()) {
@@ -5165,26 +5967,43 @@ int InsaneRebel2::runLevelSelect() {
return kLevelSelectQuit;
}
- // If video ended without selection, continue looping
if (!_vm->_smushVideoShouldFinish) {
continue;
}
_vm->_smushVideoShouldFinish = false;
- debug("Rebel2: Level selection made: %d", _levelSelection);
+ debug("Rebel2: Pilot selection made: %d", _levelSelection);
- // Process level selection
- // Menu items:
- // 0-5: Levels 1-6 (CHAPTER 1-6)
- // 6: MAIN MENU (back)
+ // Process pilot selection - all options go to chapter selection except MAIN MENU
+ // Menu items (from FUN_00414A41):
+ // 0-5: Pilot slots
+ // 6: NEW PILOT
+ // 7: DELETE PILOT
+ // 8: COPY PILOT
+ // 9: MAIN MENU (back)
if (_levelSelection >= 0 && _levelSelection <= 5) {
- // Level selected (0 = Level 1, 5 = Level 6)
+ // Pilot selected - go to chapter selection
_selectedLevel = _levelSelection + 1;
- debug("Rebel2: Level %d selected", _selectedLevel);
+ debug("Rebel2: Pilot %d selected - going to chapter selection", _selectedLevel);
_menuInputActive = false;
return kLevelSelectPlay;
} else if (_levelSelection == 6) {
+ // NEW PILOT - go to chapter selection
+ debug("Rebel2: NEW PILOT selected - going to chapter selection");
+ _menuInputActive = false;
+ return kLevelSelectPlay;
+ } else if (_levelSelection == 7) {
+ // DELETE PILOT - go to chapter selection
+ debug("Rebel2: DELETE PILOT selected - going to chapter selection");
+ _menuInputActive = false;
+ return kLevelSelectPlay;
+ } else if (_levelSelection == 8) {
+ // COPY PILOT - go to chapter selection
+ debug("Rebel2: COPY PILOT selected - going to chapter selection");
+ _menuInputActive = false;
+ return kLevelSelectPlay;
+ } else if (_levelSelection == 9) {
// Main Menu (back)
debug("Rebel2: Back to main menu selected");
_menuInputActive = false;
@@ -5298,30 +6117,42 @@ void InsaneRebel2::drawLevelSelectOverlay(byte *renderBitmap, int pitch, int wid
// Draw level selection menu overlay
// Emulates FUN_0041f5ae for level selection mode
//
- // From info.md - Low Resolution Coordinate Formulas:
+ // From info.md - Low Resolution Coordinate Formulas (320x200 mode):
// Center X = 160, Title Y = numItems * -5 + 81, Item Base Y = numItems * -5 + 104
- // Item spacing = 10 pixels, Selection box height = 10 pixels
-
- // Level menu items - all 6 levels plus Main Menu
- // In the original game, this would show all unlocked levels plus options
- static const char *levelItems[] = {
- "SELECT CHAPTER", // Title (index 0)
- "CHAPTER 1", // Level 1 (index 1) - selectable
- "CHAPTER 2", // Level 2 (index 2) - selectable
- "CHAPTER 3", // Level 3 (index 3) - selectable
- "CHAPTER 4", // Level 4 (index 4) - selectable
- "CHAPTER 5", // Level 5 (index 5) - selectable
- "CHAPTER 6", // Level 6 (index 6) - selectable
- "MAIN MENU" // Back to menu (index 7) - selectable
+ // Item spacing = 10 pixels, Selection box: width = textWidth + 6, height = 10
+
+ // Frame counter for flashing selection box (emulates DAT_0047a7e4)
+ static int frameCounter = 0;
+ frameCounter++;
+
+ // Pilot selection menu items - matches original structure from FUN_00414A41:
+ // Items 0-5: Pilot save slots (PILOT 1 through PILOT 6)
+ // Item 6: NEW PILOT
+ // Item 7: DELETE PILOT
+ // Item 8: COPY PILOT
+ // Item 9: MAIN MENU
+ static const char *pilotItems[] = {
+ "SELECT PILOT", // Title (index 0) - not selectable
+ "PILOT 1", // Pilot slot 1 (index 1) - selectable
+ "PILOT 2", // Pilot slot 2 (index 2) - selectable
+ "PILOT 3", // Pilot slot 3 (index 3) - selectable
+ "PILOT 4", // Pilot slot 4 (index 4) - selectable
+ "PILOT 5", // Pilot slot 5 (index 5) - selectable
+ "PILOT 6", // Pilot slot 6 (index 6) - selectable
+ "NEW PILOT", // Create new pilot (index 7) - selectable
+ "DELETE PILOT", // Delete pilot (index 8) - selectable
+ "COPY PILOT", // Copy pilot (index 9) - selectable
+ "MAIN MENU" // Back to menu (index 10) - selectable
};
- const int numItemsTotal = 8; // Title + 7 selectable items
- const int numSelectableItems = 7;
+ const int numItemsTotal = 11; // Title + 10 selectable items
+ const int numSelectableItems = 10;
// Calculate positions (low-res 320x200 mode)
- const int centerX = width / 2;
- const int titleY = numItemsTotal * -5 + 81; // 81 - 15 = 66
- const int itemBaseY = numItemsTotal * -5 + 104; // 104 - 15 = 89
+ // Formula from FUN_0041F5AE: titleY = numItems * -5 + 81, itemBaseY = numItems * -5 + 104
+ const int centerX = width / 2; // 160 for 320 width
+ const int titleY = numSelectableItems * -5 + 81; // 10 * -5 + 81 = 31
+ const int itemBaseY = numSelectableItems * -5 + 104; // 10 * -5 + 104 = 54
const int itemSpacing = 10;
NutRenderer *font = _smush_smalfontNut;
@@ -5364,13 +6195,17 @@ void InsaneRebel2::drawLevelSelectOverlay(byte *renderBitmap, int pitch, int wid
// If highlighted, draw selection box around text
if (highlight) {
- int boxWidth = textWidth + 12;
+ // Box dimensions from FUN_0041F5AE:
+ // Width = textWidth + 6 (low-res), Height = 10
+ int boxWidth = textWidth + 6;
int boxHeight = 10;
int boxX = centerX - boxWidth / 2;
- int boxY = y - 1;
+ int boxY = y - 1; // 1 pixel above text (itemY - 1 = numItems * -5 + idx * 10 + 103)
- // Highlight color (bright color for visibility)
- byte highlightColor = 0xF0;
+ // Flashing highlight color (emulates original behavior)
+ // Original uses palette-relative colors -8 and -16 alternating on frameCounter & 1
+ // We use bright colors that approximate the visual effect
+ byte highlightColor = (frameCounter & 1) ? 0xF8 : 0xF0;
// Draw box border (top, bottom, left, right edges)
if (boxY >= 0 && boxY < height && boxX >= 0 && boxX + boxWidth <= width) {
@@ -5401,26 +6236,27 @@ void InsaneRebel2::drawLevelSelectOverlay(byte *renderBitmap, int pitch, int wid
};
// Draw title (not selectable)
- drawTextCentered(levelItems[0], titleY, false);
+ drawTextCentered(pilotItems[0], titleY, false);
// Draw selectable items
for (int i = 0; i < numSelectableItems; i++) {
int itemY = itemBaseY + i * itemSpacing;
bool isSelected = (i == _levelSelection);
- drawTextCentered(levelItems[i + 1], itemY, isSelected);
+ drawTextCentered(pilotItems[i + 1], itemY, isSelected);
}
- // Draw info text at bottom if a level is selected (not "MAIN MENU")
- if (_levelSelection < numSelectableItems - 1) {
- // Show difficulty or other info
- // From info.md: Difficulty at X=30, Y=180
- const char *difficultyText = "DIFFICULTY: EASY";
+ // Draw info text at bottom if a pilot slot is selected (items 0-5)
+ // From FUN_00414A41: info is shown when local_18 < local_10 (selection < num_pilots)
+ if (_levelSelection >= 0 && _levelSelection <= 5) {
+ // Show difficulty or score info for selected pilot
+ // From info.md: Info displayed at X=30, Y=180
+ const char *pilotInfoText = "DIFFICULTY: EASY";
int infoY = 180;
int infoX = 30;
// Draw left-aligned text using drawCharV7
int curX = infoX;
- for (const char *c = difficultyText; *c; c++) {
+ for (const char *c = pilotInfoText; *c; c++) {
int charIdx = (unsigned char)*c;
if (charIdx < numFontChars) {
int charWidth = font->getCharWidth(charIdx);
diff --git a/engines/scumm/insane/insane_rebel.h b/engines/scumm/insane/insane_rebel.h
index 0b8a32d2974..dca510ff89e 100644
--- a/engines/scumm/insane/insane_rebel.h
+++ b/engines/scumm/insane/insane_rebel.h
@@ -60,13 +60,13 @@ public:
// ======================= Menu System =======================
// Main game states (emulates retail state machine from FUN_004142BD)
enum GameState {
- kStateIntro = 0, // Stage 0: Intro/Credits sequence
- kStateMainMenu = 1, // Stage 1: Main menu (FUN_004147B2)
- kStateLevelSelect = 2,// Stage 2: Level selection (FUN_00414A41)
- kStateBriefing = 3, // Stage 3: Mission briefing (FUN_00415CF8)
- kStateGameplay = 4, // Stage 4: Gameplay (FUN_00416787)
- kStateCredits = 5, // Credits sequence
- kStateQuit = 6 // Exit game
+ kStateIntro = 0, // Stage 0: Intro/Credits sequence
+ kStateMainMenu = 1, // Stage 1: Main menu (FUN_004147B2)
+ kStatePilotSelect = 2, // Stage 2: Pilot selection (FUN_00414A41)
+ kStateChapterSelect = 3,// Stage 3: Chapter selection (FUN_00415CF8)
+ kStateGameplay = 4, // Stage 4: Gameplay (FUN_00416787)
+ kStateCredits = 5, // Credits sequence
+ kStateQuit = 6 // Exit game
};
// Menu selection results (return values from FUN_004147B2)
@@ -104,8 +104,54 @@ public:
// Reset menu state for fresh start
void resetMenu();
- // ================= Level Selection Menu ====================
- // Level selection results
+ // ================= Chapter Selection Screen (FUN_00415CF8) ====================
+ // This is the actual level/chapter selection screen with preview thumbnail
+ // Distinct from pilot selection (FUN_00414A41)
+
+ enum ChapterSelectResult {
+ kChapterSelectBack = 2, // Return to main menu (ESC or BACK selected)
+ kChapterSelectPlay = 5, // Play selected chapter
+ kChapterSelectQuit = 0 // Quit game
+ };
+
+ int _chapterSelection; // Current chapter selection (0-15, or 16 for BACK)
+ int _chapterItemCount; // Number of chapter items (17: 16 chapters + BACK)
+ int _selectedChapter; // Final selected chapter ID (0-15)
+ Common::String _passwordInput; // Current password input string (max 8 chars)
+ bool _chapterUnlocked[16]; // Which chapters are unlocked
+
+ // Run chapter selection screen - emulates FUN_00415CF8
+ int runChapterSelect();
+
+ // Draw chapter selection overlay - called during O_LEVEL.SAN playback
+ void drawChapterSelectOverlay(byte *renderBitmap, int pitch, int width, int height);
+
+ // Process chapter select input - returns -1 (no action) or action code
+ int processChapterSelectInput();
+
+ // Draw the preview thumbnail box - emulates FUN_004292D0 calls in FUN_00415CF8
+ void drawPreviewBox(byte *renderBitmap, int pitch, int width, int height);
+
+ // Draw the preview thumbnail content - shows chapter number/status in preview area
+ void drawPreviewThumbnail(byte *renderBitmap, int pitch, int width, int height, int chapter);
+
+ // View offset for chapter preview scrolling (DAT_0047abe2/DAT_0047abe4)
+ int16 _previewOffsetX; // X offset = -90 for chapter select
+ int16 _previewOffsetY; // Y offset = chapter * -50 + 75
+
+ // Draw left-aligned menu text - emulates FUN_0041F5AE with param_4=1
+ void drawLeftAlignedMenuItem(byte *renderBitmap, int pitch, int width, int height,
+ const char *text, int x, int y, bool selected);
+
+ // Draw password input field - emulates lines 106-125 of FUN_00415CF8
+ void drawPasswordInput(byte *renderBitmap, int pitch, int width, int height);
+
+ // Draw score/time display - emulates lines 99-104 of FUN_00415CF8
+ void drawScoreDisplay(byte *renderBitmap, int pitch, int width, int height, int chapter);
+
+ // ================= Pilot Selection Menu (FUN_00414A41) ====================
+ // This is the pilot/save selection menu (separate from chapter selection)
+
enum LevelSelectResult {
kLevelSelectBack = 0, // Return to main menu
kLevelSelectPlay = 1, // Play selected level
@@ -116,13 +162,13 @@ public:
int _levelItemCount; // Number of level items (levels + options)
int _selectedLevel; // Final selected level ID (1-15)
- // Run level selection menu - returns LevelSelectResult
+ // Run pilot selection menu - emulates FUN_00414A41
int runLevelSelect();
- // Draw level selection overlay
+ // Draw pilot selection overlay
void drawLevelSelectOverlay(byte *renderBitmap, int pitch, int width, int height);
- // Process level select input - returns -1 (no action) or action code
+ // Process pilot select input - returns -1 (no action) or action code
int processLevelSelectInput();
// ======================= Level Loading System =======================
diff --git a/engines/scumm/scumm.cpp b/engines/scumm/scumm.cpp
index 7c51ecef7de..b87601c85b5 100644
--- a/engines/scumm/scumm.cpp
+++ b/engines/scumm/scumm.cpp
@@ -2697,17 +2697,30 @@ Common::Error ScummEngine::go() {
}
if (menuResult == InsaneRebel2::kMenuContinue) {
- // Continue: Show level selection menu
- int levelResult = rebel->runLevelSelect();
+ // Continue: Show pilot selection screen (FUN_00414A41)
+ int pilotResult = rebel->runLevelSelect();
- if (levelResult == InsaneRebel2::kLevelSelectQuit || shouldQuit()) {
+ if (pilotResult == InsaneRebel2::kLevelSelectQuit || shouldQuit()) {
break;
}
- if (levelResult == InsaneRebel2::kLevelSelectPlay) {
- // Play the selected level using the level loading system
- int selectedLevel = rebel->_selectedLevel;
- debug("ScummEngine: Starting level %d", selectedLevel);
+ if (pilotResult == InsaneRebel2::kLevelSelectBack) {
+ // Back to main menu
+ continue;
+ }
+
+ // Pilot selected: Show chapter selection screen (FUN_00415CF8)
+ int chapterResult = rebel->runChapterSelect();
+
+ if (chapterResult == InsaneRebel2::kChapterSelectQuit || shouldQuit()) {
+ break;
+ }
+
+ if (chapterResult == InsaneRebel2::kChapterSelectPlay) {
+ // Play the selected chapter using the level loading system
+ // Note: _selectedChapter is 0-based, runLevel expects 1-based
+ int selectedLevel = rebel->_selectedChapter + 1;
+ debug("ScummEngine: Starting chapter %d (level %d)", rebel->_selectedChapter + 1, selectedLevel);
// Run the complete level (handles BEG, gameplay, END/DIE/RETRY/OVER)
int result = rebel->runLevel(selectedLevel);
@@ -2719,7 +2732,7 @@ Common::Error ScummEngine::go() {
// After level completion or game over, return to menu
// Could also handle kLevelNextLevel to auto-start next level
}
- // If kLevelSelectBack, loop back to main menu
+ // If kChapterSelectBack, loop back to main menu
}
}
return Common::kNoError;
Commit: bb9375faec17a63d319dfc6bea8797877fb2680b
https://github.com/scummvm/scummvm/commit/bb9375faec17a63d319dfc6bea8797877fb2680b
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:08:58+02:00
Commit Message:
SCUMM: RA2: Improve font rendering
Changed paths:
A engines/scumm/smush/smush_multi_font.cpp
A engines/scumm/smush/smush_multi_font.h
engines/scumm/module.mk
engines/scumm/nut_renderer.cpp
engines/scumm/smush/smush_player.cpp
engines/scumm/smush/smush_player.h
diff --git a/engines/scumm/module.mk b/engines/scumm/module.mk
index 318e1a99231..24650499866 100644
--- a/engines/scumm/module.mk
+++ b/engines/scumm/module.mk
@@ -138,6 +138,7 @@ MODULE_OBJS += \
smush/codec20.o \
smush/codec37.o \
smush/codec47.o \
+ smush/smush_multi_font.o \
smush/smush_player.o
ifdef USE_ARM_SMUSH_ASM
diff --git a/engines/scumm/nut_renderer.cpp b/engines/scumm/nut_renderer.cpp
index 1b040f6294b..fa24c7fb049 100644
--- a/engines/scumm/nut_renderer.cpp
+++ b/engines/scumm/nut_renderer.cpp
@@ -318,8 +318,11 @@ int NutRenderer::getCharWidth(byte c) const {
if (c >= 0x80 && _vm->_useCJKMode)
return _vm->_2byteWidth + _spacing;
- if (c >= _numChars)
- error("invalid character in NutRenderer::getCharWidth : %d (%d)", c, _numChars);
+ if (c >= _numChars) {
+ // Character not in font - return 0 width (skip it)
+ // This can happen with SMUSH fonts that have limited character sets
+ return 0;
+ }
return _chars[c].width;
}
@@ -328,8 +331,11 @@ int NutRenderer::getCharHeight(byte c) const {
if (c >= 0x80 && _vm->_useCJKMode)
return _vm->_2byteHeight;
- if (c >= _numChars)
- error("invalid character in NutRenderer::getCharHeight : %d (%d)", c, _numChars);
+ if (c >= _numChars) {
+ // Character not in font - return default font height
+ // This can happen with SMUSH fonts that have limited character sets
+ return _fontHeight;
+ }
return _chars[c].height;
}
@@ -377,6 +383,11 @@ void NutRenderer::drawFrame(byte *dst, int c, int x, int y, int pitch) {
}
int NutRenderer::drawCharV7(byte *buffer, Common::Rect &clipRect, int x, int y, int pitch, int16 col, TextStyleFlags flags, byte chr, bool hardcodedColors, bool smushColorMode) {
+ // Character not in font - skip drawing
+ // This can happen with SMUSH fonts that have limited character sets
+ if (chr >= _numChars)
+ return 0;
+
if (_direction < 0)
x -= _chars[chr].width;
diff --git a/engines/scumm/smush/smush_multi_font.cpp b/engines/scumm/smush/smush_multi_font.cpp
new file mode 100644
index 00000000000..7f6bbd603a7
--- /dev/null
+++ b/engines/scumm/smush/smush_multi_font.cpp
@@ -0,0 +1,124 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include "scumm/smush/smush_multi_font.h"
+#include "scumm/smush/smush_font.h"
+#include "scumm/smush/smush_player.h"
+#include "scumm/scumm.h"
+
+namespace Scumm {
+
+SmushMultiFont::SmushMultiFont(ScummEngine *vm, SmushPlayer *player, bool useOriginalColors)
+ : _vm(vm), _player(player), _currentFont(0), _defaultFont(0), _hardcodedFontColors(useOriginalColors) {
+ _textRenderer = new TextRenderer_v7(vm, this);
+}
+
+SmushMultiFont::~SmushMultiFont() {
+ delete _textRenderer;
+}
+
+NutRenderer *SmushMultiFont::getFont(int id) {
+ // Delegate to SmushPlayer to get the font
+ // SmushPlayer::getFont() handles font loading and caching
+ return _player->getFont(id);
+}
+
+NutRenderer *SmushMultiFont::getCurrentFont() const {
+ // We need a const version that doesn't trigger loading
+ // For const access, use _player's cached fonts directly
+ return const_cast<SmushMultiFont*>(this)->getFont(_currentFont);
+}
+
+void SmushMultiFont::drawString(const char *str, byte *buffer, Common::Rect &clipRect, int x, int y, int16 col, TextStyleFlags flags) {
+ // Reset to default font before drawing
+ _currentFont = _defaultFont;
+ _textRenderer->drawString(str, buffer, clipRect, x, y, _vm->_screenWidth, col, flags);
+}
+
+void SmushMultiFont::drawStringWrap(const char *str, byte *buffer, Common::Rect &clipRect, int x, int y, int16 col, TextStyleFlags flags) {
+ // Reset to default font before drawing
+ _currentFont = _defaultFont;
+ _textRenderer->drawStringWrap(str, buffer, clipRect, x, y, _vm->_screenWidth, col, flags);
+}
+
+int SmushMultiFont::draw2byte(byte *buffer, Common::Rect &clipRect, int x, int y, int pitch, int16 col, uint16 chr) {
+ NutRenderer *font = getCurrentFont();
+ if (!font)
+ return 0;
+
+ // Adjust color for CMI compatibility
+ int16 adjCol = col;
+ if (_vm->_game.id == GID_CMI)
+ adjCol = 255;
+ else if (_vm->_game.id == GID_DIG && col == -1)
+ adjCol = 1;
+
+ return font->draw2byte(buffer, clipRect, x, y, pitch, adjCol, chr);
+}
+
+int SmushMultiFont::drawCharV7(byte *buffer, Common::Rect &clipRect, int x, int y, int pitch, int16 col, TextStyleFlags flags, byte chr) {
+ NutRenderer *font = getCurrentFont();
+ if (!font)
+ return 0;
+
+ return font->drawCharV7(buffer, clipRect, x, y, pitch, col, flags, chr, _hardcodedFontColors, true);
+}
+
+int SmushMultiFont::getCharWidth(uint16 chr) const {
+ NutRenderer *font = getCurrentFont();
+ if (!font)
+ return 0;
+
+ return font->getCharWidth(chr & 0xFF);
+}
+
+int SmushMultiFont::getCharHeight(uint16 chr) const {
+ NutRenderer *font = getCurrentFont();
+ if (!font)
+ return 0;
+
+ return font->getCharHeight(chr & 0xFF);
+}
+
+int SmushMultiFont::getFontHeight() const {
+ NutRenderer *font = getCurrentFont();
+ if (!font)
+ return 0;
+
+ return font->getFontHeight();
+}
+
+int SmushMultiFont::setFont(int id) {
+ // This is called by TextRenderer_v7 when it encounters ^fXX escape codes
+ // Actually switch the current font
+ int oldFont = _currentFont;
+
+ if (id >= 0 && id < MAX_FONTS) {
+ _currentFont = id;
+ debugC(DEBUG_SMUSH, "SmushMultiFont::setFont: switching from font %d to font %d", oldFont, id);
+ } else {
+ debugC(DEBUG_SMUSH, "SmushMultiFont::setFont: invalid font id %d, keeping font %d", id, _currentFont);
+ }
+
+ return oldFont;
+}
+
+} // End of namespace Scumm
diff --git a/engines/scumm/smush/smush_multi_font.h b/engines/scumm/smush/smush_multi_font.h
new file mode 100644
index 00000000000..1dbb4c6e2e1
--- /dev/null
+++ b/engines/scumm/smush/smush_multi_font.h
@@ -0,0 +1,84 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#ifndef SCUMM_SMUSH_MULTI_FONT_H
+#define SCUMM_SMUSH_MULTI_FONT_H
+
+#include "common/scummsys.h"
+#include "scumm/nut_renderer.h"
+#include "scumm/scumm.h"
+#include "scumm/string_v7.h"
+
+namespace Scumm {
+
+class SmushPlayer;
+
+/**
+ * SmushMultiFont - Multi-font renderer for SMUSH videos
+ *
+ * This class implements the GlyphRenderer_v7 interface and supports
+ * switching between multiple fonts during text rendering via the ^fXX
+ * escape sequence.
+ *
+ * The original Rebel Assault 2 (and other SCUMM v7 games) used a linked
+ * list of font structures that could be traversed when ^f escape codes
+ * were encountered. This class provides equivalent functionality by
+ * holding an array of NutRenderer pointers and switching between them.
+ */
+class SmushMultiFont : public GlyphRenderer_v7 {
+public:
+ static const int MAX_FONTS = 5;
+
+ SmushMultiFont(ScummEngine *vm, SmushPlayer *player, bool useOriginalColors);
+ ~SmushMultiFont() override;
+
+ // String drawing methods
+ void drawString(const char *str, byte *buffer, Common::Rect &clipRect, int x, int y, int16 col, TextStyleFlags flags);
+ void drawStringWrap(const char *str, byte *buffer, Common::Rect &clipRect, int x, int y, int16 col, TextStyleFlags flags);
+
+ // GlyphRenderer_v7 interface
+ int draw2byte(byte *buffer, Common::Rect &clipRect, int x, int y, int pitch, int16 col, uint16 chr) override;
+ int drawCharV7(byte *buffer, Common::Rect &clipRect, int x, int y, int pitch, int16 col, TextStyleFlags flags, byte chr) override;
+ int getCharWidth(uint16 chr) const override;
+ int getCharHeight(uint16 chr) const override;
+ int getFontHeight() const override;
+ int setFont(int id) override;
+ bool newStyleWrapping() const override { return true; }
+
+ // Set the initial/default font
+ void setDefaultFont(int id) { _defaultFont = id; _currentFont = id; }
+
+private:
+ NutRenderer *getFont(int id);
+ NutRenderer *getCurrentFont() const;
+
+ ScummEngine *_vm;
+ SmushPlayer *_player;
+ TextRenderer_v7 *_textRenderer;
+
+ int _currentFont;
+ int _defaultFont;
+ bool _hardcodedFontColors;
+};
+
+} // End of namespace Scumm
+
+#endif
diff --git a/engines/scumm/smush/smush_player.cpp b/engines/scumm/smush/smush_player.cpp
index 51b6eec3b4b..9468f75aa6a 100644
--- a/engines/scumm/smush/smush_player.cpp
+++ b/engines/scumm/smush/smush_player.cpp
@@ -40,6 +40,7 @@
#include "scumm/smush/codec37.h"
#include "scumm/smush/codec47.h"
#include "scumm/smush/smush_font.h"
+#include "scumm/smush/smush_multi_font.h"
#include "scumm/smush/smush_player.h"
#include "scumm/insane/insane.h"
@@ -251,6 +252,7 @@ SmushPlayer::SmushPlayer(ScummEngine_v7 *scumm, IMuseDigital *imuseDigital, Insa
_sf[2] = nullptr;
_sf[3] = nullptr;
_sf[4] = nullptr;
+ _multiFont = nullptr;
_base = nullptr;
_frameBuffer = nullptr;
_specialBuffer = nullptr;
@@ -315,6 +317,8 @@ SmushPlayer::SmushPlayer(ScummEngine_v7 *scumm, IMuseDigital *imuseDigital, Insa
SmushPlayer::~SmushPlayer() {
delete _IACTchannel;
delete _compressedFileSoundHandle;
+ delete _multiFont;
+ _multiFont = nullptr;
terminateAudio();
// Free any preserved frame buffer (RA2 preserves this across videos)
@@ -836,14 +840,6 @@ void SmushPlayer::handleTextResource(uint32 subType, int32 subSize, Common::Seek
color = 255;
}
- SmushFont *sf = getFont(fontId);
- assert(sf != nullptr);
-
- // The hack that used to be here to prevent bug #2220 is no longer necessary and
- // has been removed. The font renderer can handle all ^codes it encounters (font
- // changes on the fly will be ignored for Smush texts, since our code design does
- // not permit it and the feature isn't used anyway).
-
if (_vm->_language == Common::HE_ISR && !(flags & kStyleAlignCenter)) {
flags |= kStyleAlignRight;
pos_x = _width - 1 - pos_x;
@@ -861,23 +857,51 @@ void SmushPlayer::handleTextResource(uint32 subType, int32 subSize, Common::Seek
// bit 7 - skip ^ codes (COMI) 0x80 (should be irrelevant for Smush, we strip these commands anyway)
// bit 8 - no vertical fix (COMI) 0x100 (COMI handles this in the printing method, but I haven't seen a case where it is used)
- if (flg & kStyleWordWrap) {
- // COMI has to do it all a bit different, of course. SCUMM7 games immediately render the text from here and actually use the clipping data
- // provided by the text resource. COMI does not render directly, but enqueues a blast string (which is then drawn through the usual main
- // loop routines). During that process the rect data will get dumped and replaced with the following default values. It's hard to tell
- // whether this is on purpose or not (the text looks not necessarily better or worse, just different), so we follow the original...
- if (_vm->_game.id == GID_CMI) {
- left = top = 10;
- width = _width - 20;
- height = _height - 20;
+ // For Rebel Assault 2, use SmushMultiFont to support inline font switching via ^fXX codes.
+ // The original game uses a linked list of fonts and switches between them mid-string.
+ // Other games (FT, DIG, CMI) only use ^f codes at the start of strings.
+ if (_vm->_game.id == GID_REBEL2) {
+ // Create multi-font renderer on first use
+ if (!_multiFont) {
+ _multiFont = new SmushMultiFont(_vm, this, true);
+ }
+ _multiFont->setDefaultFont(fontId);
+
+ if (flg & kStyleWordWrap) {
+ Common::Rect clipRect(MAX<int>(0, left), MAX<int>(0, top), MIN<int>(left + width, _width), MIN<int>(top + height, _height));
+ _multiFont->drawStringWrap(str, _dst, clipRect, pos_x, pos_y, color, flg);
+ } else {
+ Common::Rect clipRect(0, 0, _width, _height);
+ _multiFont->drawString(str, _dst, clipRect, pos_x, pos_y, color, flg);
}
- Common::Rect clipRect(MAX<int>(0, left), MAX<int>(0, top), MIN<int>(left + width, _width), MIN<int>(top + height, _height));
- sf->drawStringWrap(str, _dst, clipRect, pos_x, pos_y, color, flg);
} else {
- // Similar to the wrapped text, COMI will pass on rect coords here, which will later be lost. Unlike with the wrapped text, it will
- // finally use the full screen dimenstions. SCUMM7 renders directly from here (see comment above), but also with the full screen.
- Common::Rect clipRect(0, 0, _width, _height);
- sf->drawString(str, _dst, clipRect, pos_x, pos_y, color, flg);
+ // For other games, use single font (original behavior)
+ SmushFont *sf = getFont(fontId);
+ assert(sf != nullptr);
+
+ // The hack that used to be here to prevent bug #2220 is no longer necessary and
+ // has been removed. The font renderer can handle all ^codes it encounters (font
+ // changes on the fly will be ignored for Smush texts, since our code design does
+ // not permit it and the feature isn't used anyway).
+
+ if (flg & kStyleWordWrap) {
+ // COMI has to do it all a bit different, of course. SCUMM7 games immediately render the text from here and actually use the clipping data
+ // provided by the text resource. COMI does not render directly, but enqueues a blast string (which is then drawn through the usual main
+ // loop routines). During that process the rect data will get dumped and replaced with the following default values. It's hard to tell
+ // whether this is on purpose or not (the text looks not necessarily better or worse, just different), so we follow the original...
+ if (_vm->_game.id == GID_CMI) {
+ left = top = 10;
+ width = _width - 20;
+ height = _height - 20;
+ }
+ Common::Rect clipRect(MAX<int>(0, left), MAX<int>(0, top), MIN<int>(left + width, _width), MIN<int>(top + height, _height));
+ sf->drawStringWrap(str, _dst, clipRect, pos_x, pos_y, color, flg);
+ } else {
+ // Similar to the wrapped text, COMI will pass on rect coords here, which will later be lost. Unlike with the wrapped text, it will
+ // finally use the full screen dimenstions. SCUMM7 renders directly from here (see comment above), but also with the full screen.
+ Common::Rect clipRect(0, 0, _width, _height);
+ sf->drawString(str, _dst, clipRect, pos_x, pos_y, color, flg);
+ }
}
free(string);
diff --git a/engines/scumm/smush/smush_player.h b/engines/scumm/smush/smush_player.h
index 2ab2759cc18..62b0e0c1500 100644
--- a/engines/scumm/smush/smush_player.h
+++ b/engines/scumm/smush/smush_player.h
@@ -87,6 +87,7 @@ namespace Scumm {
class ScummEngine_v7;
class SmushFont;
+class SmushMultiFont;
class SmushMixer;
class StringResource;
class SmushDeltaBlocksDecoder;
@@ -97,6 +98,7 @@ class Insane;
class SmushPlayer {
friend class Insane;
friend class InsaneRebel2;
+ friend class SmushMultiFont;
private:
struct SmushAudioDispatch {
uint8 *headerPtr;
@@ -140,6 +142,7 @@ private:
int32 _shiftedDeltaPal[0x300];
byte _pal[0x300];
SmushFont *_sf[5];
+ SmushMultiFont *_multiFont; // Multi-font renderer for inline font switching
StringResource *_strings;
SmushDeltaBlocksDecoder *_deltaBlocksCodec;
SmushDeltaGlyphsDecoder *_deltaGlyphsCodec;
Commit: 6239b89fbb8c306a19415629db6491809ea9bc1f
https://github.com/scummvm/scummvm/commit/6239b89fbb8c306a19415629db6491809ea9bc1f
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:08:59+02:00
Commit Message:
SCUMM: RA2: Improve shot rendering and terminology
Changed paths:
engines/scumm/insane/insane_rebel.cpp
engines/scumm/insane/insane_rebel.h
diff --git a/engines/scumm/insane/insane_rebel.cpp b/engines/scumm/insane/insane_rebel.cpp
index 35563e955ad..047f531e170 100644
--- a/engines/scumm/insane/insane_rebel.cpp
+++ b/engines/scumm/insane/insane_rebel.cpp
@@ -25,6 +25,7 @@
#include "common/system.h"
#include "common/memstream.h"
#include "common/events.h"
+#include "common/util.h"
#include "graphics/cursorman.h"
#include "graphics/wincursor.h"
@@ -77,6 +78,16 @@ InsaneRebel2::InsaneRebel2(ScummEngine_v7 *scumm) {
// For now, use CPITIMAG since the game runs at 320x200
_smush_iconsNut = new NutRenderer(_vm, "SYSTM/CPITIMAG.NUT");
_smush_icons2Nut = nullptr; // Not used for Rebel2
+
+ // Initialize laser texture buffer (DAT_0047fee4) from sprite 5 of CPITIMAG.NUT
+ // This is done by FUN_0040BAB0/FUN_0040BB87 in the original with sprite index 5
+ // Sprite 5 is 136x13 pixels - a wide, thin texture perfect for laser beams
+ _laserTexture.pixels = nullptr;
+ _laserTexture.width = 0;
+ _laserTexture.height = 0;
+ if (_smush_iconsNut && _smush_iconsNut->getNumChars() > 5) {
+ initLaserTexture(_smush_iconsNut, 5);
+ }
_smush_cockpitNut = new NutRenderer(_vm, "SYSTM/DISPFONT.NUT");
// Load SMALFONT.NUT for HUD score/lives rendering (DAT_00482200 equivalent)
@@ -246,11 +257,41 @@ InsaneRebel2::InsaneRebel2(ScummEngine_v7 *scumm) {
_corridorBottomY = 200;
_hitCooldown = 0;
+ // Initialize legacy shot system (backwards compatibility)
for (i = 0; i < 2; i++) {
_shots[i].active = false;
_shots[i].counter = 0;
}
+ // Initialize Handler 0x26 Turret shot system (FUN_40AD63)
+ for (i = 0; i < 2; i++) {
+ _turretShots[i].counter = 0;
+ _turretShots[i].targetX = 0;
+ _turretShots[i].targetY = 0;
+ _turretShots[i].seqNum = 0;
+ }
+ _turretShotSeqCounter = 0;
+
+ // Initialize Handler 8 Vehicle shot system (FUN_402ED0)
+ for (i = 0; i < 2; i++) {
+ _vehicleShots[i].counter = 0;
+ _vehicleShots[i].targetX = 0;
+ _vehicleShots[i].targetY = 0;
+ }
+
+ // Initialize Handler 7 Space shot system (FUN_40FADF)
+ for (i = 0; i < 2; i++) {
+ _spaceShots[i].counter = 0;
+ _spaceShots[i].targetX = 0;
+ _spaceShots[i].targetY = 0;
+ _spaceShots[i].leftGunX = 0;
+ _spaceShots[i].leftGunY = 0;
+ _spaceShots[i].rightGunX = 0;
+ _spaceShots[i].rightGunY = 0;
+ _spaceShots[i].variant = 0;
+ }
+ _spaceShotDirection = 0;
+
for (i = 0; i < 16; i++) {
_rebelEmbeddedHud[i].pixels = nullptr;
_rebelEmbeddedHud[i].width = 0;
@@ -388,6 +429,9 @@ InsaneRebel2::~InsaneRebel2() {
delete _hudOverlayNut;
delete _hudOverlay2Nut;
+ // Clean up laser texture buffer (DAT_0047fee4)
+ freeLaserTexture();
+
// Clean up embedded HUD overlays
for (int i = 0; i < 16; i++) {
free(_rebelEmbeddedHud[i].pixels);
@@ -910,7 +954,7 @@ void InsaneRebel2::iactRebel2Scene1(byte *renderBitmap, int32 codecparam, int32
}
} else if (par1 == 7) {
- // Opcode 7: Sprite/HUD control for Handler 7 (space flight levels like Level 3)
+ // Opcode 7: Sprite/HUD control for Handler 7 (third-person ship levels like Level 3)
// par2 = control type (41 = sprite selection?)
// par3 = usually 0
// par4 = sprite/slot ID (0 or 5 seen in Level 3)
@@ -1110,12 +1154,12 @@ void InsaneRebel2::iactRebel2Opcode6(byte *renderBitmap, Common::SeekableReadStr
// Opcode 6: Level setup / mode switch
// Based on FUN_41CADB case 4 (switch on *local_14 - 2 == 4, meaning opcode 6)
//
- // For Handler 8 (third-person vehicle) - FUN_00401234 case 4:
+ // For Handler 8 (third-person on foot) - FUN_00401234 case 4:
// - par3 sets ship level mode (DAT_0043e000)
// - par4 == 1 triggers status bar display and state reset
// - Updates ship position based on mouse input
//
- // For Handler 0x26/0x19 (turret/space):
+ // For Handler 0x26/0x19 (turret/FPS):
// - Same par4 == 1 behavior
// - Different view offset calculations
@@ -1131,7 +1175,7 @@ void InsaneRebel2::iactRebel2Opcode6(byte *renderBitmap, Common::SeekableReadStr
debug("Rebel2 Opcode 6: Setting handler=%d", par2);
}
- // Handler 8 specific logic (third-person vehicle) - FUN_00401234 case 4
+ // Handler 8 specific logic (third-person on foot) - FUN_00401234 case 4
if (_rebelHandler == 8) {
// Set ship level mode (DAT_0043e000 = par3)
_shipLevelMode = par3;
@@ -1263,7 +1307,7 @@ void InsaneRebel2::iactRebel2Opcode6(byte *renderBitmap, Common::SeekableReadStr
return;
}
- // Handler 7 specific logic (space flight) - FUN_0040d836 / FUN_0040c3cc
+ // Handler 7 specific logic (third-person ship) - FUN_0040d836 / FUN_0040c3cc
// Used for Level 3 and similar space combat levels
if (_rebelHandler == 7) {
// Set control mode (DAT_004437c0 = par3 in FUN_40C3CC case 4)
@@ -1700,7 +1744,7 @@ void InsaneRebel2::iactRebel2Opcode8(byte *renderBitmap, Common::SeekableReadStr
int64 startPos = b.pos();
int64 remaining = (chunkSize > 0) ? chunkSize : (b.size() - startPos);
- // ===== Handler 7: FLY NUT Loading (Space Flight) =====
+ // ===== Handler 7: FLY NUT Loading (Third-Person Ship) =====
// FUN_0040c3cc case 6: par4 determines FLY sprite slot
bool isHandler7FLY = (_rebelHandler == 7 && (par4 == 1 || par4 == 2 || par4 == 3 || par4 == 11));
if (isHandler7FLY && remaining >= 14) {
@@ -2899,15 +2943,97 @@ void InsaneRebel2::spawnExplosion(int x, int y, int objectHalfWidth) {
}
}
+// Get max shot duration from level table (DAT_0047e0f0 indexed by DAT_0047a7fa/DAT_0047a7f8)
+// For now, use a reasonable default based on observed values
+int16 InsaneRebel2::getShotMaxDuration() {
+ // The original uses: DAT_0047e0f0 + DAT_0047a7fa * 0x242 + DAT_0047a7f8 * 0x22
+ // Typical values observed: 2-5 frames
+ return 4;
+}
+
+// Dispatcher - calls appropriate spawn function based on current handler
void InsaneRebel2::spawnShot(int x, int y) {
- // Find free shot slot (2 slots total)
+ switch (_rebelHandler) {
+ case 0x26: // Turret
+ spawnTurretShot(x, y);
+ break;
+ case 8: // Vehicle
+ spawnVehicleShot(x, y);
+ break;
+ case 7: // Space combat
+ spawnSpaceShot(x, y);
+ break;
+ default:
+ // Legacy fallback
+ for (int i = 0; i < 2; i++) {
+ if (!_shots[i].active) {
+ _shots[i].active = true;
+ _shots[i].counter = getShotMaxDuration();
+ _shots[i].x = x + _viewX;
+ _shots[i].y = y + _viewY;
+ break;
+ }
+ }
+ break;
+ }
+}
+
+// Handler 0x26 Turret shot spawn (based on FUN_4089AB lines 127-140)
+void InsaneRebel2::spawnTurretShot(int x, int y) {
for (int i = 0; i < 2; i++) {
- if (!_shots[i].active) {
- _shots[i].active = true;
- _shots[i].counter = 4; // Lasts 4 frames
- _shots[i].x = x + _viewX;
- _shots[i].y = y + _viewY;
+ if (_turretShots[i].counter == 0) {
+ // Play sound based on level type
+ // FUN_0041189e(-(ushort)(_rebelLevelType == 5) & 7, i + 1, 0x7f, 0, 0)
+ // Sound ID: 0 for type 5, 7 for others
+ // TODO: Play laser sound via audio system
+
+ _turretShots[i].counter = getShotMaxDuration();
+ _turretShots[i].seqNum = _turretShotSeqCounter;
+ _turretShotSeqCounter++;
+ _turretShots[i].targetX = x + _viewX; // DAT_0044366e in original
+ _turretShots[i].targetY = y + _viewY; // DAT_00443670 in original
+ break;
+ }
+ }
+}
+
+// Handler 8 Vehicle shot spawn (based on FUN_401CCF)
+void InsaneRebel2::spawnVehicleShot(int x, int y) {
+ for (int i = 0; i < 2; i++) {
+ if (_vehicleShots[i].counter == 0) {
// TODO: Play laser sound
+ _vehicleShots[i].counter = getShotMaxDuration();
+ _vehicleShots[i].targetX = x + _viewX;
+ _vehicleShots[i].targetY = y + _viewY;
+ break;
+ }
+ }
+}
+
+// Handler 7 Space combat shot spawn (based on FUN_40D836 lines 146-166)
+void InsaneRebel2::spawnSpaceShot(int x, int y) {
+ for (int i = 0; i < 2; i++) {
+ if (_spaceShots[i].counter == 0) {
+ // Play sound: FUN_0041189e(6, i + 1, 0x7f, 0, 0)
+ // TODO: Play laser sound
+
+ _spaceShots[i].counter = getShotMaxDuration();
+ _spaceShots[i].targetX = x; // Screen coords
+ _spaceShots[i].targetY = y;
+
+ // Calculate gun positions from direction-based lookup tables
+ // In the original, these come from tables indexed by _shipDirectionIndex
+ // DAT_004437c2/DAT_00443808 for left gun, DAT_0044384e/DAT_00443894 for right gun
+ // For now, use simplified positions relative to ship
+ int shipScreenX = 160 + ((_shipPosX - 160) >> 3);
+ int shipScreenY = 105 + ((_shipPosY - 40) >> 2);
+
+ // Gun offsets (approximate from disassembly)
+ _spaceShots[i].leftGunX = shipScreenX - 28;
+ _spaceShots[i].leftGunY = shipScreenY + 10;
+ _spaceShots[i].rightGunX = shipScreenX + 28;
+ _spaceShots[i].rightGunY = shipScreenY + 10;
+ _spaceShots[i].variant = _spaceShotDirection;
break;
}
}
@@ -3077,61 +3203,148 @@ void drawTexturedSegment(byte *dst, int pitch, int width, int height, int param_
}
-void InsaneRebel2::drawLaserBeam(byte *dst, int pitch, int width, int height, int x0, int y0, int x1, int y1, int progress, int maxProgress, int thickness, int param_9, NutRenderer *nut, int spriteIdx) {
- if (!nut || spriteIdx >= nut->getNumChars()) return;
+// Initialize laser texture buffer from NUT sprite (FUN_0040BAB0)
+// This pre-renders a sprite into a buffer that drawLaserBeam uses
+void InsaneRebel2::initLaserTexture(NutRenderer *nut, int spriteIdx) {
+ if (!nut || spriteIdx >= nut->getNumChars())
+ return;
- // Follow original FUN_0040BBF6 math precisely
- int texW = nut->getCharWidth(spriteIdx);
- int texH = nut->getCharHeight(spriteIdx);
- int param_11 = (_rebelLevelType <= 1) ? 12 : 25;
+ // Get sprite dimensions (FUN_0040BAB0 lines 13-14)
+ int16 texWidth = nut->getCharWidth(spriteIdx);
+ int16 texHeight = nut->getCharHeight(spriteIdx);
- if (maxProgress == 0) maxProgress = 1;
- int sVar7 = (param_11 * progress * 16) / maxProgress;
+ // Clamp height to max 15 pixels (FUN_0040BAB0 lines 15-17)
+ if (texHeight > 15) {
+ texHeight = 15;
+ }
- int dx = x1 - x0;
- int dy = y1 - y0;
- int sVar6 = ((dx) * (param_11 + 1)) / param_11;
- int sVar1 = ((dy) * (param_11 + 1)) / param_11;
+ // Free existing texture if any (FUN_0040BAB0 lines 18-20)
+ freeLaserTexture();
- int sVar4 = (sVar6 + x0) - (sVar6 * 16) / (sVar7 + 16);
- int sVar5 = (sVar1 + y0) - (sVar1 * 16) / (sVar7 + 16);
- int sVar6_end = (sVar6 + x0) - (sVar6 * 16) / (param_9 + sVar7 + 16);
- int sVar7_end = (sVar1 + y0) - (sVar1 * 16) / (param_9 + sVar7 + 16);
+ // Allocate new buffer (FUN_0040BAB0 line 21)
+ _laserTexture.width = texWidth;
+ _laserTexture.height = texHeight;
+ _laserTexture.pixels = (byte *)calloc(texWidth * texHeight, 1);
- const byte *srcBase = nut->getCharData(spriteIdx);
- if (!srcBase || texW <= 0 || texH <= 0) return;
+ if (!_laserTexture.pixels)
+ return;
- int iVar2 = abs(sVar5 - sVar7_end);
- int iVar3 = abs(sVar4 - sVar6_end);
+ // Render sprite into buffer (FUN_0040BAB0 lines 23-24)
+ // We copy the sprite data directly since it's already in the right format
+ const byte *srcData = nut->getCharData(spriteIdx);
+ if (srcData) {
+ int srcHeight = nut->getCharHeight(spriteIdx);
+ int copyHeight = MIN(texHeight, (int16)srcHeight);
+ memcpy(_laserTexture.pixels, srcData, texWidth * copyHeight);
+ }
+
+ debug("Rebel2: Initialized laser texture %dx%d from sprite %d", texWidth, texHeight, spriteIdx);
+}
+// Free laser texture buffer (FUN_0040BBD1)
+void InsaneRebel2::freeLaserTexture() {
+ free(_laserTexture.pixels);
+ _laserTexture.pixels = nullptr;
+ _laserTexture.width = 0;
+ _laserTexture.height = 0;
+}
+
+// Draw laser beam using pre-initialized texture (FUN_0040BBF6)
+// This is a direct port of the assembly function
+//
+// Parameters (matching FUN_0040bbf6):
+// dst, pitch, width, height: destination buffer info
+// gunX, gunY (param_3, param_4): gun/start position
+// targetX, targetY (param_5, param_6): target/end position
+// animFrame (param_7): current animation frame (shot counter)
+// maxFrames (param_8): max animation frames (shot duration)
+// widthScale (param_9): width scaling factor for perspective
+// heightScale (param_10): height/thickness multiplier
+// thickness (param_11): base line thickness
+void InsaneRebel2::drawLaserBeam(byte *dst, int pitch, int width, int height,
+ int16 gunX, int16 gunY, int16 targetX, int16 targetY,
+ int16 animFrame, int16 maxFrames,
+ int16 widthScale, int16 heightScale, int16 thickness) {
+ // Check if laser texture is initialized
+ if (!_laserTexture.pixels || _laserTexture.width <= 0 || _laserTexture.height <= 0)
+ return;
+
+ int16 texW = _laserTexture.width;
+ int16 texH = _laserTexture.height;
+ byte *texPixels = _laserTexture.pixels;
+
+ // FUN_0040BBF6 line 23: sVar7 = (thickness * animFrame * 16) / maxFrames
+ if (maxFrames == 0) maxFrames = 1;
+ int16 sVar7 = (int16)(((int)thickness * (int)animFrame * 16) / (int)maxFrames);
+
+ // FUN_0040BBF6 lines 24-25: Calculate delta with scaling
+ int16 dx = targetX - gunX;
+ int16 dy = targetY - gunY;
+ int16 sVar6 = (int16)(((int)dx * (thickness + 1)) / (int)thickness);
+ int16 sVar1 = (int16)(((int)dy * (thickness + 1)) / (int)thickness);
+
+ // FUN_0040BBF6 lines 26-29: Calculate adjusted start and end points
+ // Start point (closer to gun, adjusted by animation progress)
+ int16 startX = (sVar6 + gunX) - (int16)(((int)sVar6 * 16) / (sVar7 + 16));
+ int16 startY = (sVar1 + gunY) - (int16)(((int)sVar1 * 16) / (sVar7 + 16));
+ // End point (closer to target)
+ int16 endX = (sVar6 + gunX) - (int16)(((int)sVar6 * 16) / (widthScale + sVar7 + 16));
+ int16 endY = (sVar1 + gunY) - (int16)(((int)sVar1 * 16) / (widthScale + sVar7 + 16));
+
+ // FUN_0040BBF6 line 30: Get texture pixel pointer
+ byte *local_28 = texPixels;
+
+ // FUN_0040BBF6 lines 31-32: Calculate abs differences (FUN_004356e4 = abs)
+ int iVar2 = abs(startY - endY); // |dy| of beam
+ int iVar3 = abs(startX - endX); // |dx| of beam
+
+ // FUN_0040BBF6 line 33: Choose rendering path based on beam orientation
if (iVar2 < iVar3) {
- // Column major case (wide)
- iVar2 = abs(sVar4 - sVar6_end);
- long long temp = (long long)iVar2 * (long long)texH * (long long)thickness;
- // sVar1calc = (temp >> 3) / texW + 2
- int sVar1calc = (int)((temp >> 3) / texW) + 2;
- int local_24 = -sVar1calc;
- int sVar8 = sVar1calc >> 1;
- const byte *local_28 = srcBase;
- for (int local_2c = 0; local_2c < sVar1calc; local_2c++) {
- drawTexturedSegment(dst, pitch, width, height, sVar4, (sVar5 - sVar8) + local_2c,
- sVar6_end, (sVar7_end - sVar8) + local_2c, texW, local_28);
- for (local_24 = texH + local_24; local_24 > 0; local_24 -= sVar1calc) {
+ // Mostly horizontal beam - draw vertical scanlines
+ // FUN_0040BBF6 lines 34-37
+ iVar2 = abs(startX - endX);
+ int temp = iVar2 * texH * heightScale;
+ int16 numLines = (int16)((temp >> 3) / texW) + 2;
+ int16 local_24 = -numLines;
+ int16 halfLines = numLines >> 1;
+
+ // FUN_0040BBF6 lines 39-46: Draw parallel lines
+ for (int16 lineIdx = 0; lineIdx < numLines; lineIdx++) {
+ // Draw one textured segment (vertical offset for this scanline)
+ drawTexturedSegment(dst, pitch, width, height,
+ startX, (startY - halfLines) + lineIdx,
+ endX, (endY - halfLines) + lineIdx,
+ texW, local_28);
+
+ // Advance texture pointer (step through texture rows)
+ for (local_24 = texH + local_24; local_24 > 0; local_24 -= numLines) {
local_28 += texW;
}
}
} else {
- // Row major case (tall)
- iVar2 = abs(sVar5 - sVar7_end);
- int local_30 = (int)(((long long)iVar2 * (long long)texH) / texW) + 2;
- if (texH < local_30) local_30 = texH;
- int local_24 = -local_30;
- const byte *local_28 = srcBase;
- int sVar1_half = local_30 >> 1;
- for (int local_2c = 0; local_2c < local_30; local_2c++) {
- drawTexturedSegment(dst, pitch, width, height, (sVar4 - sVar1_half) + local_2c, sVar5,
- (sVar6_end - sVar1_half) + local_2c, sVar7_end, texW, local_28);
- for (local_24 = texH + local_24; local_24 > 0; local_24 -= local_30) {
+ // Mostly vertical beam - draw horizontal scanlines
+ // FUN_0040BBF6 lines 54-56
+ iVar2 = abs(startY - endY);
+ int16 numLines = (int16)((iVar2 * texH) / texW) + 2;
+ int16 local_24 = -numLines;
+
+ // FUN_0040BBF6 lines 58-60: Clamp to texture height
+ if (texH < numLines) {
+ numLines = texH;
+ }
+
+ int16 halfLines = numLines >> 1;
+
+ // FUN_0040BBF6 lines 61-68: Draw parallel lines
+ for (int16 lineIdx = 0; lineIdx < numLines; lineIdx++) {
+ // Draw one textured segment (horizontal offset for this scanline)
+ drawTexturedSegment(dst, pitch, width, height,
+ (startX - halfLines) + lineIdx, startY,
+ (endX - halfLines) + lineIdx, endY,
+ texW, local_28);
+
+ // Advance texture pointer
+ for (local_24 = texH + local_24; local_24 > 0; local_24 -= numLines) {
local_28 += texW;
}
}
@@ -3870,7 +4083,7 @@ void InsaneRebel2::renderStatusBarSprites(byte *renderBitmap, int pitch, int wid
}
void InsaneRebel2::renderHandler7Ship(byte *renderBitmap, int pitch, int width, int height) {
- // Handler 7 Ship Rendering (Space Flight - FLY sprites)
+ // Handler 7 Ship Rendering (Third-Person Ship - FLY sprites)
// Uses _flyShipSprite (FLY001) with 35 direction frames (5x7 grid)
if (_rebelHandler != 7 || !_flyShipSprite || _shipLevelMode == 5)
@@ -3917,7 +4130,7 @@ void InsaneRebel2::renderHandler7Ship(byte *renderBitmap, int pitch, int width,
}
void InsaneRebel2::renderHandler8Ship(byte *renderBitmap, int pitch, int width, int height) {
- // Handler 8 Ship Rendering (Third-Person Vehicle - POV sprites)
+ // Handler 8 Ship Rendering (Third-Person On Foot - POV sprites)
// Uses _shipSprite (POV001) with position-based offset
if (_rebelHandler != 8 || !_shipSprite || _shipLevelMode == 5)
@@ -4240,58 +4453,200 @@ void InsaneRebel2::renderExplosions(byte *renderBitmap, int pitch, int width, in
}
}
+// Dispatcher - calls appropriate render function based on current handler
void InsaneRebel2::renderLaserShots(byte *renderBitmap, int pitch, int width, int height) {
- // Draw laser shot beams and impacts
+ switch (_rebelHandler) {
+ case 0x26: // Turret - FUN_40AD63
+ renderTurretLaserShots(renderBitmap, pitch, width, height);
+ break;
+ case 8: // Vehicle - FUN_402ED0
+ renderVehicleLaserShots(renderBitmap, pitch, width, height);
+ break;
+ case 7: // Space combat - FUN_40FADF
+ renderSpaceLaserShots(renderBitmap, pitch, width, height);
+ break;
+ default:
+ // No laser rendering for other handlers
+ break;
+ }
+}
- if (!_smush_iconsNut || _smush_iconsNut->getNumChars() <= 0)
- return;
+// Handler 0x26 Turret laser rendering (FUN_40AD63)
+// Gun positions depend on _rebelLevelType (DAT_004436de)
+void InsaneRebel2::renderTurretLaserShots(byte *renderBitmap, int pitch, int width, int height) {
+ // Uses pre-initialized _laserTexture from sprite 5 of CPITIMAG.NUT
- // Gun positions (approximate for turret mode)
- const int GUN_LEFT_X = 10, GUN_LEFT_Y = 190;
- const int GUN_RIGHT_X = 310, GUN_RIGHT_Y = 190;
+ int16 maxDuration = getShotMaxDuration();
for (int i = 0; i < 2; i++) {
- if (!_shots[i].active)
+ if (_turretShots[i].counter <= 0)
continue;
- if (_shots[i].counter <= 0) {
- _shots[i].active = false;
- continue;
- }
+ // Calculate sound panning from target X position (FUN_004262f0 call)
+ // sVar1 = ((2 - counter) * (targetX - 160)) / 2, clamped to [-127, 127]
+ int16 pan = ((2 - _turretShots[i].counter) * (_turretShots[i].targetX - _viewX - 160)) / 2;
+ pan = CLIP<int16>(pan, -127, 127);
+ // TODO: Apply panning to sound channel i+1
+
+ int16 targetX = _turretShots[i].targetX;
+ int16 targetY = _turretShots[i].targetY;
+ int16 progress = maxDuration - _turretShots[i].counter;
+
+ // Gun positions based on level type (from FUN_40AD63 switch statement)
+ // Parameters from assembly: widthScale=0xC(12), heightScale=8, thickness=0xC(12)
+ switch (_rebelLevelType) {
+ case 1:
+ // Type 1: 3 guns (triple cannon configuration)
+ // Gun 1: (0x136, 0xaa) = (310, 170)
+ // Gun 2: (0xa0, 0x17c) = (160, 380)
+ // Gun 3: (0x0a, 0xaa) = (10, 170)
+ drawLaserBeam(renderBitmap, pitch, width, height,
+ 310 + _viewX, 170 + _viewY, targetX, targetY,
+ progress, maxDuration, 12, 8, 12);
- int maxProgress = 4;
- int progress = maxProgress - _shots[i].counter;
+ drawLaserBeam(renderBitmap, pitch, width, height,
+ 160 + _viewX, 380 + _viewY, targetX, targetY,
+ progress, maxDuration, 12, 8, 12);
- // Draw beams based on level type
- if (_rebelLevelType <= 1) {
- // Type 1: 3 beams (Right, Middle, Left)
drawLaserBeam(renderBitmap, pitch, width, height,
- 310 + _viewX, 170 + _viewY, _shots[i].x, _shots[i].y,
- progress, maxProgress, 8, 12, _smush_iconsNut, 0);
+ 10 + _viewX, 170 + _viewY, targetX, targetY,
+ progress, maxDuration, 12, 8, 12);
+ break;
+ case 2:
+ case 5:
+ // Type 2/5: 2 guns (wing cannons)
+ // Left: (0x6e, 0xe6) = (110, 230)
+ // Right: (0xd2, 0xe6) = (210, 230)
+ // Assembly uses widthScale=0x19(25) for these types
drawLaserBeam(renderBitmap, pitch, width, height,
- 160 + _viewX, 380 + _viewY, _shots[i].x, _shots[i].y,
- progress, maxProgress, 5, 8, _smush_iconsNut, 0);
+ 110 + _viewX, 230 + _viewY, targetX, targetY,
+ progress, maxDuration, 25, 8, 25);
drawLaserBeam(renderBitmap, pitch, width, height,
- 10 + _viewX, 170 + _viewY, _shots[i].x, _shots[i].y,
- progress, maxProgress, 8, 12, _smush_iconsNut, 0);
- } else {
- // Other levels: 2 beams
+ 210 + _viewX, 230 + _viewY, targetX, targetY,
+ progress, maxDuration, 25, 8, 25);
+ break;
+
+ case 6:
+ // Type 6: 2 guns (offscreen - cinematic effect)
+ // Gun 1: (-100, 0)
+ // Gun 2: (0, 0)
drawLaserBeam(renderBitmap, pitch, width, height,
- GUN_LEFT_X + _viewX, GUN_LEFT_Y + _viewY, _shots[i].x, _shots[i].y,
- progress, maxProgress, 8, 12, _smush_iconsNut, 0);
+ -100 + _viewX, 0 + _viewY, targetX, targetY,
+ progress, maxDuration, 25, 8, 25);
drawLaserBeam(renderBitmap, pitch, width, height,
- GUN_RIGHT_X + _viewX, GUN_RIGHT_Y + _viewY, _shots[i].x, _shots[i].y,
- progress, maxProgress, 8, 12, _smush_iconsNut, 0);
+ 0 + _viewX, 0 + _viewY, targetX, targetY,
+ progress, maxDuration, 25, 8, 25);
+ break;
+
+ default:
+ // Default: 2 guns with alternating pattern based on shot sequence
+ // When seqNum & 1 == 0: Left (10, 50), Right (310, 130)
+ // When seqNum & 1 == 1: Left (310, 50), Right (10, 130)
+ if ((_turretShots[i].seqNum & 1) == 0) {
+ drawLaserBeam(renderBitmap, pitch, width, height,
+ 10 + _viewX, 50 + _viewY, targetX, targetY,
+ progress, maxDuration, 25, 8, 25);
+
+ drawLaserBeam(renderBitmap, pitch, width, height,
+ 310 + _viewX, 130 + _viewY, targetX, targetY,
+ progress, maxDuration, 25, 8, 25);
+ } else {
+ drawLaserBeam(renderBitmap, pitch, width, height,
+ 310 + _viewX, 50 + _viewY, targetX, targetY,
+ progress, maxDuration, 25, 8, 25);
+
+ drawLaserBeam(renderBitmap, pitch, width, height,
+ 10 + _viewX, 130 + _viewY, targetX, targetY,
+ progress, maxDuration, 25, 8, 25);
+ }
+ break;
}
- // Impact flash
- renderNutSprite(renderBitmap, pitch, width, height,
- _shots[i].x - 7, _shots[i].y - 7, _smush_iconsNut, 0);
+ _turretShots[i].counter--;
+ }
+}
+
+// Handler 8 Vehicle laser rendering (FUN_402ED0)
+// Gun position derived from ship position
+void InsaneRebel2::renderVehicleLaserShots(byte *renderBitmap, int pitch, int width, int height) {
+ // No NUT check needed - uses pre-initialized _laserTexture
+
+ int16 maxDuration = getShotMaxDuration();
+
+ for (int i = 0; i < 2; i++) {
+ if (_vehicleShots[i].counter <= 0)
+ continue;
+
+ // Calculate sound panning
+ int16 pan = ((2 - _vehicleShots[i].counter) * (_vehicleShots[i].targetX - _viewX - 160)) / 2;
+ pan = CLIP<int16>(pan, -127, 127);
+ // TODO: Apply panning
+
+ // Calculate gun position from ship position (FUN_402ED0 lines 26-36)
+ // Low-res formula:
+ // shipScreenY = ((shipPosY - 0x28) >> 2) + 0x69 = ((shipPosY - 40) >> 2) + 105
+ // shipScreenX = ((shipPosX - 0xa0) >> 3) + 0xa0 = ((shipPosX - 160) >> 3) + 160
+ // gunY = ((shipPosY - 0x28) >> 2) + 0x82 = shipScreenY + 25
+ // gunX = ((shipPosX - 0xa0) >> 3) + 0xa7 = shipScreenX + 7
+ int16 shipScreenX = ((_shipPosX - 160) >> 3) + 160;
+ int16 shipScreenY = ((_shipPosY - 40) >> 2) + 105;
+ int16 gunX = shipScreenX + 7;
+ int16 gunY = shipScreenY + 25;
+
+ int16 targetX = _vehicleShots[i].targetX;
+ int16 targetY = _vehicleShots[i].targetY;
+ int16 progress = maxDuration - _vehicleShots[i].counter;
+
+ // Single beam from gun to target
+ // From FUN_402ED0: widthScale=0x14(20), heightScale=8, thickness=4
+ drawLaserBeam(renderBitmap, pitch, width, height,
+ gunX + _viewX, gunY + _viewY, targetX, targetY,
+ progress, maxDuration, 20, 8, 4);
+
+ _vehicleShots[i].counter--;
+ }
+}
+
+// Handler 7 Space combat laser rendering (FUN_40FADF)
+// Dual beams from left and right gun positions
+void InsaneRebel2::renderSpaceLaserShots(byte *renderBitmap, int pitch, int width, int height) {
+ // No NUT check needed - uses pre-initialized _laserTexture
+
+ int16 maxDuration = getShotMaxDuration();
+
+ for (int i = 0; i < 2; i++) {
+ if (_spaceShots[i].counter <= 0)
+ continue;
- _shots[i].counter--;
+ // Calculate sound panning
+ int16 pan = ((_spaceShots[i].targetX - 160) * (2 - _spaceShots[i].counter)) / 2;
+ pan = CLIP<int16>(pan, -127, 127);
+ // TODO: Apply panning
+
+ int16 targetX = _spaceShots[i].targetX;
+ int16 targetY = _spaceShots[i].targetY;
+ int16 leftGunX = _spaceShots[i].leftGunX;
+ int16 leftGunY = _spaceShots[i].leftGunY;
+ int16 rightGunX = _spaceShots[i].rightGunX;
+ int16 rightGunY = _spaceShots[i].rightGunY;
+ int16 progress = maxDuration - _spaceShots[i].counter;
+
+ // Draw dual beams
+ // From FUN_40FADF: widthScale=0xC(12), heightScale=4, thickness=6
+ // Left gun beam
+ drawLaserBeam(renderBitmap, pitch, width, height,
+ leftGunX, leftGunY, targetX, targetY,
+ progress, maxDuration, 12, 4, 6);
+
+ // Right gun beam
+ drawLaserBeam(renderBitmap, pitch, width, height,
+ rightGunX, rightGunY, targetX, targetY,
+ progress, maxDuration, 12, 4, 6);
+
+ _spaceShots[i].counter--;
}
}
@@ -4321,11 +4676,11 @@ void InsaneRebel2::renderCrosshair(byte *renderBitmap, int pitch, int width, int
int reticleIndex;
switch (_rebelHandler) {
- case 7: // Space flight
- case 0x19: // Mixed/turret
+ case 7: // Third-Person Ship
+ case 0x19: // FPS/Mixed
reticleIndex = 47;
break;
- case 0x26: { // Full turret - animated crosshair
+ case 0x26: { // Turret/Cockpit - animated crosshair
static int turretAnimCounter = 0;
turretAnimCounter++;
@@ -4338,7 +4693,7 @@ void InsaneRebel2::renderCrosshair(byte *renderBitmap, int pitch, int width, int
}
break;
}
- case 8: // Ground vehicle
+ case 8: // Third-Person On Foot
default:
reticleIndex = 46;
break;
@@ -6402,7 +6757,7 @@ bool InsaneRebel2::playLevelGameplay(int levelId) {
case 2:
// Level 2: Has cutscene first, then multiple parts
- // Level 2 uses Handler 8 (third-person vehicle mode) - set before gameplay
+ // Level 2 uses Handler 8 (third-person on foot mode) - set before gameplay
_rebelHandler = 8;
// First play the cutscene
filename = Common::String::format("%s/%sCUT.SAN", dir.c_str(), prefix.c_str());
@@ -6452,8 +6807,8 @@ bool InsaneRebel2::playLevelGameplay(int levelId) {
break;
case 3:
- // Level 3: Two gameplay phases (space flight)
- // Level 3 uses Handler 7 (space flight mode) - FUN_0040d836/FUN_0040c3cc
+ // Level 3: Two gameplay phases (third-person ship)
+ // Level 3 uses Handler 7 (third-person ship mode) - FUN_0040d836/FUN_0040c3cc
_rebelHandler = 7;
filename = Common::String::format("%s/%sPLAY1.SAN", dir.c_str(), prefix.c_str());
debug("Rebel2: Playing %s", filename.c_str());
diff --git a/engines/scumm/insane/insane_rebel.h b/engines/scumm/insane/insane_rebel.h
index dca510ff89e..708141e2db7 100644
--- a/engines/scumm/insane/insane_rebel.h
+++ b/engines/scumm/insane/insane_rebel.h
@@ -330,10 +330,10 @@ public:
void renderStatusBarSprites(byte *renderBitmap, int pitch, int width, int height,
int statusBarY, int32 curFrame);
- // Draw Handler 7 ship sprite (space flight - FLY sprites)
+ // Draw Handler 7 ship sprite (third-person ship - FLY sprites)
void renderHandler7Ship(byte *renderBitmap, int pitch, int width, int height);
- // Draw Handler 8 ship sprite (third-person vehicle - POV sprites)
+ // Draw Handler 8 ship sprite (third-person on foot - POV sprites)
void renderHandler8Ship(byte *renderBitmap, int pitch, int width, int height);
// Draw fallback ship using embedded HUD frame
@@ -357,10 +357,10 @@ public:
// ======================= Opcode 6 Helper Functions =======================
// Handler-specific setup extracted from iactRebel2Opcode6
- // Handler 8 (third-person vehicle) setup - FUN_00401234 case 4
+ // Handler 8 (third-person on foot) setup - FUN_00401234 case 4
void opcode6Handler8Setup(int16 par3, int16 par4);
- // Handler 7 (space flight) setup - FUN_0040c3cc case 4
+ // Handler 7 (third-person ship) setup - FUN_0040c3cc case 4
void opcode6Handler7Setup(int16 par3, int16 par4);
// Calculate view offsets based on level type (lines 182-213)
@@ -396,7 +396,26 @@ public:
// mask231: when true, color 231 is treated as transparent (legacy sprites). For laser beams set false.
void drawTexturedLine(byte *dst, int pitch, int width, int height, int x0, int y0, int x1, int y1, NutRenderer *nut, int spriteIdx, int v, bool mask231 = true);
- void drawLaserBeam(byte *dst, int pitch, int width, int height, int x0, int y0, int x1, int y1, int progress, int maxProgress, int thickness, int param_9, NutRenderer *nut, int spriteIdx);
+ // ======================= Laser Texture Buffer (DAT_0047fee4) =======================
+ // Pre-rendered laser texture used by FUN_0040BBF6
+ // Initialized from CPITIMAG.NUT sprite 0 via initLaserTexture() (FUN_0040BAB0)
+ struct LaserTexture {
+ byte *pixels; // Pixel data (rendered from NUT sprite)
+ int16 width; // Texture width
+ int16 height; // Texture height (clamped to max 15)
+ };
+ LaserTexture _laserTexture; // DAT_0047fee4
+
+ // Initialize laser texture from NUT sprite (FUN_0040BAB0)
+ void initLaserTexture(NutRenderer *nut, int spriteIdx);
+ void freeLaserTexture();
+
+ // Draw laser beam using pre-initialized texture (FUN_0040BBF6)
+ // Parameters match the original assembly function
+ void drawLaserBeam(byte *dst, int pitch, int width, int height,
+ int16 gunX, int16 gunY, int16 targetX, int16 targetY,
+ int16 animFrame, int16 maxFrames,
+ int16 widthScale, int16 heightScale, int16 thickness);
void renderNutSprite(byte *dst, int pitch, int width, int height, int x, int y, NutRenderer *nut, int spriteIdx);
struct enemy {
@@ -418,10 +437,10 @@ public:
// Current handler type for Rebel Assault 2 (determines crosshair sprite)
// Handler 0: Background only
- // Handler 7: Space flight - uses crosshair sprite 0x2F (47)
- // Handler 8: Third-person vehicle - uses crosshair sprite 0x2E (46)
- // Handler 0x19: Mixed/turret view - uses crosshair sprite 0x2F (47)
- // Handler 0x26: Full turret - crosshair varies by level type
+ // Handler 7: Third-Person Ship - uses crosshair sprite 0x2F (47)
+ // Handler 8: Third-Person On Foot - uses crosshair sprite 0x2E (46)
+ // Handler 0x19: FPS/Mixed view - uses crosshair sprite 0x2F (47)
+ // Handler 0x26: Turret/Cockpit - crosshair varies by level type
int _rebelHandler;
// Level type from IACT opcode 6 par3 (corresponds to DAT_004436de)
@@ -577,16 +596,85 @@ public:
int _rebelLastCounter; // Mirrors DAT_0047ab90 (last updated counter)
+ // ======================= Handler 0x26 Turret Shot System =======================
+ // Based on FUN_40AD63 disassembly - Turret laser rendering
+ // DAT_0044367a[2]: Shot duration counter (0=inactive)
+ // DAT_0044367e[2]: Target X position
+ // DAT_00443682[2]: Target Y position
+ // DAT_0044368a[2]: Shot sequence number (for alternating gun pattern)
+ // DAT_004436de: Level type (determines gun positions) - already have as _rebelLevelType
+
+ struct TurretShot {
+ int16 counter; // DAT_0044367a[i] - duration counter, 0=inactive
+ int16 targetX; // DAT_0044367e[i] - target X position
+ int16 targetY; // DAT_00443682[i] - target Y position
+ int16 seqNum; // DAT_0044368a[i] - shot sequence (for alternating)
+ };
+ TurretShot _turretShots[2];
+ int16 _turretShotSeqCounter; // DAT_0047fe94 - global sequence counter
+
+ // ======================= Handler 8 Vehicle Shot System =======================
+ // Based on FUN_402ED0 disassembly - Vehicle laser rendering
+ // DAT_0043e00a[2]: Shot duration counter
+ // DAT_0043e00e[2]: Target X position
+ // DAT_0043e012[2]: Target Y position
+ // Gun position derived from ship position (_shipPosX, _shipPosY)
+
+ struct VehicleShot {
+ int16 counter; // DAT_0043e00a[i] - duration counter, 0=inactive
+ int16 targetX; // DAT_0043e00e[i] - target X position
+ int16 targetY; // DAT_0043e012[i] - target Y position
+ };
+ VehicleShot _vehicleShots[2];
+
+ // ======================= Handler 7 Third-Person Ship Shot System =======================
+ // Based on FUN_40FADF disassembly - Third-Person Ship laser rendering
+ // DAT_00443750[2]: Shot duration counter
+ // DAT_00443754[2]: Target X position
+ // DAT_00443758[2]: Target Y position
+ // DAT_0044375c[2]: Left gun X position
+ // DAT_00443760[2]: Left gun Y position
+ // DAT_00443764[2]: Right gun X position
+ // DAT_00443768[2]: Right gun Y position
+ // DAT_0044376c[2]: Shot variant
+
+ struct SpaceShot {
+ int16 counter; // DAT_00443750[i] - duration counter, 0=inactive
+ int16 targetX; // DAT_00443754[i] - target X position
+ int16 targetY; // DAT_00443758[i] - target Y position
+ int16 leftGunX; // DAT_0044375c[i] - left gun X
+ int16 leftGunY; // DAT_00443760[i] - left gun Y
+ int16 rightGunX; // DAT_00443764[i] - right gun X
+ int16 rightGunY; // DAT_00443768[i] - right gun Y
+ int16 variant; // DAT_0044376c[i] - shot variant
+ };
+ SpaceShot _spaceShots[2];
+ int16 _spaceShotDirection; // DAT_0044374e - ship direction for gun lookup
+
+ // Legacy struct for backwards compatibility
struct Shot {
bool active;
int counter;
int x, y; // Target position
};
Shot _shots[2];
- void spawnShot(int x, int y);
+
+ // Handler-specific shot spawning
+ void spawnTurretShot(int x, int y); // Handler 0x26
+ void spawnVehicleShot(int x, int y); // Handler 8
+ void spawnSpaceShot(int x, int y); // Handler 7
+ void spawnShot(int x, int y); // Dispatcher based on current handler
+
+ // Handler-specific laser rendering (FUN_40AD63, FUN_402ED0, FUN_40FADF)
+ void renderTurretLaserShots(byte *renderBitmap, int pitch, int width, int height);
+ void renderVehicleLaserShots(byte *renderBitmap, int pitch, int width, int height);
+ void renderSpaceLaserShots(byte *renderBitmap, int pitch, int width, int height);
+
+ // Get max shot duration from level table (DAT_0047e0f0 indexed by DAT_0047a7fa/DAT_0047a7f8)
+ int16 getShotMaxDuration();
// ======================= Handler 8 Ship System =======================
- // For third-person vehicle missions (Levels 2, 3), the player controls a ship
+ // For third-person on foot missions (Level 2, 11), the player controls Rookie One
// that can turn in different directions. The ship sprite comes from
// NUT files loaded via IACT opcode 8.
//
@@ -633,7 +721,7 @@ public:
// Gradually transitions by ±10 per frame for smooth animation
int16 _movementRangeLimit; // DAT_0047e034
- // Control mode for Handler 7 (space flight) - DAT_004437c0
+ // Control mode for Handler 7 (third-person ship) - DAT_004437c0
// Set by IACT opcode 6 par3 when handler is 7
// Determines shooting capability and collision zone type:
// Mode 0: Flight/avoid mode - no shooting, uses secondary zones (sub-opcode 0x0E)
@@ -661,7 +749,7 @@ public:
NutRenderer *loadNutFromIact(Common::SeekableReadStream &b, int dataSize);
// ======================= Handler 7 FLY Ship System =======================
- // For space flight missions (Level 3, etc.), Handler 7 uses a 35-frame
+ // For third-person ship missions (Level 3, etc.), Handler 7 uses a 35-frame
// direction-based ship sprite system. The ship visually banks and turns
// based on player position using a 5x7 grid of sprites.
//
Commit: 0dad1b0270bc409eb70e5c88b428e7b2e14b7c46
https://github.com/scummvm/scummvm/commit/0dad1b0270bc409eb70e5c88b428e7b2e14b7c46
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:08:59+02:00
Commit Message:
SCUMM: RA2: Improve level selection screen
Changed paths:
engines/scumm/insane/insane_rebel.cpp
engines/scumm/insane/insane_rebel.h
diff --git a/engines/scumm/insane/insane_rebel.cpp b/engines/scumm/insane/insane_rebel.cpp
index 047f531e170..45e9a0a4ef3 100644
--- a/engines/scumm/insane/insane_rebel.cpp
+++ b/engines/scumm/insane/insane_rebel.cpp
@@ -365,8 +365,17 @@ InsaneRebel2::InsaneRebel2(ScummEngine_v7 *scumm) {
_chapterItemCount = 17; // 16 chapters + BACK
_selectedChapter = 0; // Default selected chapter
_passwordInput = ""; // No password input
+
+ // Debug flag to unlock all chapters for testing
+ // Based on original debug mode (DAT_0047ab34 == 'd') from FUN_00415CF8
+ // Set to true to bypass normal unlock progression
+ _debugUnlockAll = true; // TODO: Set to false for release, or read from ScummVM config
+
for (i = 0; i < 16; i++) {
- _chapterUnlocked[i] = (i == 0); // Only chapter 1 unlocked initially
+ // If debug unlock is enabled, unlock all chapters
+ // Otherwise only chapter 1 (index 0) is unlocked initially
+ // Original: pilotData->scores[chapter] < 0xFF means unlocked
+ _chapterUnlocked[i] = _debugUnlockAll || (i == 0);
}
// Initialize preview offset for chapter selection (FUN_00425170)
@@ -3675,8 +3684,12 @@ void InsaneRebel2::procPostRendering(byte *renderBitmap, int32 codecparam, int32
CursorMan.showMouse(true);
// Fill screen with BLACK background
- // The original uses O_LEVEL.SAN (640x400) which has a black background.
- // For 320x200 mode, we just fill with black (color index 0).
+ // The original game plays O_LEVEL.SAN (640x400) which has a dark/black background
+ // with chapter preview thumbnails embedded. For 320x200 mode, we fill with black
+ // to match the visual appearance and overlay the UI elements.
+ // Note: O_MENU_X.SAN (320x200) doesn't contain chapter-specific preview images,
+ // those are only in O_LEVEL.SAN (640x400). We use a styled placeholder instead.
+ // From FUN_00415CF8 line 57: FUN_0041f4d0(s_OPEN_O_LEVEL_SAN_004806e0,8,0xffff,0xffff,0);
for (int y = 0; y < height; y++) {
memset(renderBitmap + y * pitch, 0, width);
}
@@ -4953,6 +4966,18 @@ void InsaneRebel2::resetMenu() {
_menuRepeatDelay = 0;
}
+// Unlock all chapters for testing
+// Emulates the debug mode from original FUN_00415CF8 (lines 60-71)
+// where DAT_0047ab34 == 'd' enables level unlock via special codes
+void InsaneRebel2::unlockAllChapters() {
+ debug("Rebel2: Unlocking all chapters for testing");
+ _debugUnlockAll = true;
+ for (int i = 0; i < 16; i++) {
+ _chapterUnlocked[i] = true;
+ _levelUnlocked[i] = true;
+ }
+}
+
Common::String InsaneRebel2::getRandomMenuVideo() {
// Emulates FUN_0041FDC8 - selects random menu video variant
//
@@ -5792,35 +5817,42 @@ void InsaneRebel2::drawPreviewBox(byte *renderBitmap, int pitch, int width, int
}
}
-// Draw preview thumbnail content - emulates FUN_00429b40 scaled preview draw
-// Shows chapter number and unlock status inside the preview box
+// Draw preview thumbnail content - emulates FUN_00428a10 + FUN_00429b40
+// Based on FUN_00415CF8 assembly analysis:
+//
+// The original uses O_LEVEL.SAN (640x400) with chapter previews stacked vertically.
+// Video offset (FUN_00425170) shifts which preview is visible:
+// X offset = -90 (0xffa6)
+// Y offset = chapter * -50 + 75
+//
+// For 320x200 mode, O_MENU_X.SAN doesn't contain chapter-specific preview images.
+// Those are only in O_LEVEL.SAN (640x400). We display a styled placeholder instead
+// with the chapter number and visual styling to match the original UI appearance.
void InsaneRebel2::drawPreviewThumbnail(byte *renderBitmap, int pitch, int width, int height, int chapter) {
- // Preview area coordinates (inside the inner border)
- // Inner box: X=230 (229+1), Y=75 (74+1), W=80 (82-2), H=50 (52-2)
- int previewX = 230;
- int previewY = 75;
- int previewW = 80;
- int previewH = 50;
-
- // Update preview offset based on chapter selection (FUN_00425170)
- // Y offset = chapter * -50 + 75
- _previewOffsetY = chapter * -50 + 75;
-
- // Determine fill color based on chapter state
- // Unlocked: dark blue (0x10), Locked: dark gray (0x08)
- byte fillColor = (_chapterUnlocked[chapter]) ? 0x10 : 0x08;
-
- // Fill the preview area with background color
- for (int py = previewY; py < previewY + previewH && py < height; py++) {
- if (py >= 0) {
- for (int px = previewX; px < previewX + previewW && px < width; px++) {
- if (px >= 0)
- renderBitmap[py * pitch + px] = fillColor;
- }
+ // Preview destination area coordinates (inside the inner border)
+ // From assembly: Inner box at X=230, Y=75, W=80, H=50
+ const int destX = 230;
+ const int destY = 75;
+ const int thumbW = 80; // 0x50
+ const int thumbH = 50; // 0x32
+
+ // Fill preview area with a dark blue gradient background
+ // This creates a styled placeholder since O_MENU_X.SAN doesn't have previews
+ for (int py = 0; py < thumbH; py++) {
+ int dy = destY + py;
+ if (dy < 0 || dy >= height) continue;
+
+ // Create vertical gradient: darker at top (0x10), lighter at bottom (0x18)
+ byte bgColor = 0x10 + (py * 8 / thumbH);
+
+ for (int px = 0; px < thumbW; px++) {
+ int dx = destX + px;
+ if (dx < 0 || dx >= width) continue;
+ renderBitmap[dy * pitch + dx] = bgColor;
}
}
- // Draw chapter number in the center of the preview area
+ // Draw chapter number overlay in the center of the preview
NutRenderer *font = _smush_smalfontNut;
if (!font) return;
@@ -5828,7 +5860,7 @@ void InsaneRebel2::drawPreviewThumbnail(byte *renderBitmap, int pitch, int width
if (chapter < 15) {
snprintf(chapterStr, sizeof(chapterStr), "CH.%d", chapter + 1);
} else {
- snprintf(chapterStr, sizeof(chapterStr), "FIN"); // FINALE
+ snprintf(chapterStr, sizeof(chapterStr), "FINALE");
}
// Calculate text width for centering
@@ -5842,13 +5874,27 @@ void InsaneRebel2::drawPreviewThumbnail(byte *renderBitmap, int pitch, int width
}
// Center the text in the preview area
- int textX = previewX + (previewW - textWidth) / 2;
- int textY = previewY + previewH / 2 - 4; // Center vertically (approx)
+ int textX = destX + (thumbW - textWidth) / 2;
+ int textY = destY + thumbH / 2 - 4;
Common::Rect clipRect(0, 0, width, height);
- // Draw the chapter text
- int curX = textX;
+ // Draw text shadow (offset by 1,1)
+ int curX = textX + 1;
+ for (const char *c = chapterStr; *c; c++) {
+ int charIdx = (unsigned char)*c;
+ if (charIdx < numChars) {
+ int charWidth = font->getCharWidth(charIdx);
+ if (curX >= 0 && curX + charWidth <= width && textY + 1 >= 0 && textY + 1 < height) {
+ font->drawCharV7(renderBitmap, clipRect, curX, textY + 1, pitch, 0,
+ kStyleAlignLeft, charIdx, true, true);
+ }
+ curX += charWidth;
+ }
+ }
+
+ // Draw main text (bright)
+ curX = textX;
for (const char *c = chapterStr; *c; c++) {
int charIdx = (unsigned char)*c;
if (charIdx < numChars) {
@@ -5861,18 +5907,28 @@ void InsaneRebel2::drawPreviewThumbnail(byte *renderBitmap, int pitch, int width
}
}
- // Draw lock icon for locked chapters (simple "X" pattern)
+ // Draw lock icon for locked chapters
if (!_chapterUnlocked[chapter]) {
- byte lockColor = 0xF8; // Bright red/orange
- int lockX = previewX + previewW - 15;
- int lockY = previewY + 5;
-
- // Draw small X to indicate locked
- for (int i = 0; i < 8; i++) {
- if (lockX + i < width && lockY + i < height)
- renderBitmap[(lockY + i) * pitch + lockX + i] = lockColor;
- if (lockX + 7 - i < width && lockY + i < height)
- renderBitmap[(lockY + i) * pitch + lockX + 7 - i] = lockColor;
+ byte lockColor = 0xF8;
+ int lockX = destX + thumbW - 15;
+ int lockY = destY + 5;
+
+ // Draw padlock shape
+ for (int i = 2; i < 6; i++) {
+ if (lockX + i < width && lockY < height && lockY >= 0)
+ renderBitmap[lockY * pitch + lockX + i] = lockColor;
+ }
+ for (int i = 1; i < 4; i++) {
+ if (lockX + 2 < width && lockY + i < height && lockY + i >= 0)
+ renderBitmap[(lockY + i) * pitch + lockX + 2] = lockColor;
+ if (lockX + 5 < width && lockY + i < height && lockY + i >= 0)
+ renderBitmap[(lockY + i) * pitch + lockX + 5] = lockColor;
+ }
+ for (int y = 0; y < 4; y++) {
+ for (int x = 1; x < 7; x++) {
+ if (lockX + x < width && lockY + 4 + y < height && lockY + 4 + y >= 0)
+ renderBitmap[(lockY + 4 + y) * pitch + lockX + x] = lockColor;
+ }
}
}
}
diff --git a/engines/scumm/insane/insane_rebel.h b/engines/scumm/insane/insane_rebel.h
index 708141e2db7..b094063852a 100644
--- a/engines/scumm/insane/insane_rebel.h
+++ b/engines/scumm/insane/insane_rebel.h
@@ -119,6 +119,11 @@ public:
int _selectedChapter; // Final selected chapter ID (0-15)
Common::String _passwordInput; // Current password input string (max 8 chars)
bool _chapterUnlocked[16]; // Which chapters are unlocked
+ bool _debugUnlockAll; // Debug flag to unlock all chapters for testing
+
+ // Unlock all chapters for testing (debug mode)
+ // Call this to enable access to all chapters without passwords
+ void unlockAllChapters();
// Run chapter selection screen - emulates FUN_00415CF8
int runChapterSelect();
Commit: 7e80992c722615733fbe3b42d989fb150cb69d44
https://github.com/scummvm/scummvm/commit/7e80992c722615733fbe3b42d989fb150cb69d44
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:08:59+02:00
Commit Message:
SCUMM: RA2: Check whether the player can shoot
Changed paths:
engines/scumm/insane/insane_rebel.cpp
engines/scumm/insane/insane_rebel.h
diff --git a/engines/scumm/insane/insane_rebel.cpp b/engines/scumm/insane/insane_rebel.cpp
index 45e9a0a4ef3..792a1171225 100644
--- a/engines/scumm/insane/insane_rebel.cpp
+++ b/engines/scumm/insane/insane_rebel.cpp
@@ -670,9 +670,11 @@ int32 InsaneRebel2::processMouse() {
}
// Left button: Trigger shot on button press (not hold)
- if (leftPressed && !leftWasPressed) {
+ // From FUN_0040d836 (Handler 7) line 141: shots only spawn when DAT_004437c0 == 2
+ // From FUN_00401CCF (Handler 8) line 82-84: mode 4 disables shooting
+ if (leftPressed && !leftWasPressed && isShootingAllowed()) {
Common::Point mousePos(_vm->_mouse.x, _vm->_mouse.y);
- debug("Rebel2 Click: Mouse=(%d,%d) Enemies=%d",
+ debug("Rebel2 Click: Mouse=(%d,%d) Enemies=%d",
mousePos.x, mousePos.y, _enemies.size());
// Spawn visual shot immediately
@@ -763,6 +765,27 @@ void InsaneRebel2::clearBit(int n) {
_iactBits[n] = 0;
}
+// Check if shooting is allowed based on current handler and control mode
+// From FUN_0040d836 (Handler 7): shooting only allowed when DAT_004437c0 == 2
+// From FUN_00401CCF (Handler 8): mode 4/5 disable shooting
+bool InsaneRebel2::isShootingAllowed() {
+ // Handler 7 (Third-Person Ship): Only mode 2 allows shooting
+ // FUN_0040d836 line 141: if (DAT_004437c0 == 2) { /* spawn shots, draw crosshair */ }
+ if (_rebelHandler == 7) {
+ return (_flyControlMode == 2);
+ }
+
+ // Handler 8 (Third-Person On Foot): Modes 4/5 disable shooting
+ // FUN_00401CCF line 82-84: if (DAT_0043e000 == 4) { param_5 = 0; }
+ // Mode 5: Ship not rendered (cutscene)
+ if (_rebelHandler == 8) {
+ return (_shipLevelMode != 4 && _shipLevelMode != 5);
+ }
+
+ // Handler 0x26 (Turret) and 0x19 (FPS): Always allow shooting when active
+ return (_rebelHandler != 0);
+}
+
void InsaneRebel2::procSKIP(int32 subSize, Common::SeekableReadStream &b) {
// Rebel Assault 2 uses SKIP chunks to conditionally skip the next FOBJ/PSAD chunk.
// The SKIP chunk contains one or two object IDs. If the bit for the object is set
@@ -4664,6 +4687,12 @@ void InsaneRebel2::renderSpaceLaserShots(byte *renderBitmap, int pitch, int widt
}
void InsaneRebel2::renderCrosshair(byte *renderBitmap, int pitch, int width, int height) {
+ // From FUN_0040d836 (Handler 7) line 167-168: crosshair only drawn when DAT_004437c0 == 2
+ // Don't draw crosshair when shooting is disabled (flight-only segments)
+ if (!isShootingAllowed()) {
+ return;
+ }
+
// Update target lock state and draw crosshair/reticle
// Target lock detection (DAT_00443676 equivalent)
diff --git a/engines/scumm/insane/insane_rebel.h b/engines/scumm/insane/insane_rebel.h
index b094063852a..04aa51d7ab2 100644
--- a/engines/scumm/insane/insane_rebel.h
+++ b/engines/scumm/insane/insane_rebel.h
@@ -500,6 +500,7 @@ public:
int16 _rebelLinks[512][3]; // Dependency links: Slot 0 (Disable on death), Slot 1/2 (Enable on death)
void clearBit(int n);
+ bool isShootingAllowed(); // FUN_0040d836/FUN_00401CCF: Check control mode before spawning shots
struct Explosion {
int x, y;
Commit: 07f891177c39f012c9ee3fa28662c3293fae9f37
https://github.com/scummvm/scummvm/commit/07f891177c39f012c9ee3fa28662c3293fae9f37
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:00+02:00
Commit Message:
SCUMM: RA2: Animate level 2 player correctly
Changed paths:
engines/scumm/insane/insane_rebel.cpp
engines/scumm/insane/insane_rebel.h
engines/scumm/nut_renderer.cpp
engines/scumm/nut_renderer.h
diff --git a/engines/scumm/insane/insane_rebel.cpp b/engines/scumm/insane/insane_rebel.cpp
index 792a1171225..16d00b0dbd9 100644
--- a/engines/scumm/insane/insane_rebel.cpp
+++ b/engines/scumm/insane/insane_rebel.cpp
@@ -138,6 +138,7 @@ InsaneRebel2::InsaneRebel2(ScummEngine_v7 *scumm) {
_rebelDamageLevel = 0;
_rebelFlightDir = 0;
_rebelControlMode = 0;
+ _rebelInputThrottle = 0;
_rebelViewOffsetX = 0;
_rebelViewOffsetY = 0;
_rebelViewOffset2X = 0;
@@ -656,17 +657,28 @@ int32 InsaneRebel2::processMouse() {
_prevMouseButtons = currentButtons;
// Update _rebelControlMode (DAT_0047a7e4) for Handler 25 covered/uncovered toggle:
- // The original uses input throttling (every 5 frames) to prevent oscillation.
- // We use edge detection instead - only set the control mode flag on button DOWN.
- // - Bit 1 (value 2): Right mouse button just pressed - triggers cover entry
- // - Bit 0 (value 1): Left mouse button just pressed - triggers cover exit
- // When covered, any NEW button press exits cover. When uncovered, right button enters cover.
- _rebelControlMode = 0;
- if (rightPressed && !rightWasPressed) {
- _rebelControlMode |= 2; // Right button just pressed
- }
- if (leftPressed && !leftWasPressed) {
- _rebelControlMode |= 1; // Left button just pressed
+ // Use "sticky" flags - set on button press, cleared by IACT handler after consumption.
+ // This ensures button presses aren't missed due to timing.
+ //
+ // For Handler 25: use edge detection with sticky flags
+ if (_rebelHandler == 25) {
+ // Only SET flags on button press (edge), don't clear them here
+ // The IACT handler will clear them after processing
+ if (rightPressed && !rightWasPressed) {
+ _rebelControlMode |= 2; // Right button pressed - sticky
+ }
+ if (leftPressed && !leftWasPressed) {
+ _rebelControlMode |= 1; // Left button pressed - sticky
+ }
+ } else {
+ // Other handlers: use simple hold state
+ _rebelControlMode = 0;
+ if (rightPressed) {
+ _rebelControlMode |= 2;
+ }
+ if (leftPressed) {
+ _rebelControlMode |= 1;
+ }
}
// Left button: Trigger shot on button press (not hold)
@@ -768,6 +780,7 @@ void InsaneRebel2::clearBit(int n) {
// Check if shooting is allowed based on current handler and control mode
// From FUN_0040d836 (Handler 7): shooting only allowed when DAT_004437c0 == 2
// From FUN_00401CCF (Handler 8): mode 4/5 disable shooting
+// From FUN_41DB5E (Handler 25): only shoot when fully uncovered (DAT_0045790a == 0)
bool InsaneRebel2::isShootingAllowed() {
// Handler 7 (Third-Person Ship): Only mode 2 allows shooting
// FUN_0040d836 line 141: if (DAT_004437c0 == 2) { /* spawn shots, draw crosshair */ }
@@ -782,7 +795,14 @@ bool InsaneRebel2::isShootingAllowed() {
return (_shipLevelMode != 4 && _shipLevelMode != 5);
}
- // Handler 0x26 (Turret) and 0x19 (FPS): Always allow shooting when active
+ // Handler 25 (0x19): Only allow shooting when fully uncovered
+ // FUN_41DB5E lines 170-171: if (((param_5 & 1) != 0) && (DAT_0045790a == 0))
+ // _rebelDamageLevel = DAT_0045790a (cover transition counter, 0 = uncovered, 5 = covered)
+ if (_rebelHandler == 25) {
+ return (_rebelDamageLevel == 0);
+ }
+
+ // Handler 0x26 (Turret): Always allow shooting when active
return (_rebelHandler != 0);
}
@@ -838,31 +858,80 @@ void InsaneRebel2::procPreRendering(byte *renderBitmap) {
}
// For Handler 25 (Level 2 speeder bike), draw the corridor overlay BEFORE FOBJ decoding.
- // The corridor overlay (par4=4, _rebelEmbeddedHud[4]) shows the covered/uncovered state
- // and must be drawn BEFORE enemy FOBJ frames so enemies appear ON TOP of the corridor.
- // Position is (_rebelViewOffsetX, _rebelViewOffsetY) from FUN_0041cadb line 216.
+ // The corridor overlay (par3=4 -> _rebelEmbeddedHud[4]) is DAT_00482268, a 350x230 buffer.
+ // From FUN_0041cadb line 216: FUN_00428a10(param_1,0,DAT_0045790c,DAT_0045790e,DAT_00482268)
+ // It's drawn at (DAT_0045790c, DAT_0045790e) which are _rebelViewOffsetX/Y.
+ //
+ // For Mode 1: DAT_0045790c = damageLevel * -5 - 14, range -39 (covered) to -14 (uncovered)
+ //
+ // From FUN_00428a10: When position is negative, we skip source pixels and draw at 0.
+ // This creates a "scrolling window" effect as the character enters/exits cover.
if (_rebelHandler == 25 && renderBitmap) {
EmbeddedSanFrame &corridorOverlay = _rebelEmbeddedHud[4];
if (corridorOverlay.valid && corridorOverlay.pixels) {
- int overlayX = _rebelViewOffsetX;
- int overlayY = _rebelViewOffsetY;
int pitch = (_player && _player->_width > 0) ? _player->_width : 320;
int bufHeight = (_player && _player->_height > 0) ? _player->_height : 200;
- for (int y = 0; y < corridorOverlay.height && (overlayY + y) < bufHeight; y++) {
- for (int x = 0; x < corridorOverlay.width && (overlayX + x) < pitch; x++) {
- byte pixel = corridorOverlay.pixels[y * corridorOverlay.width + x];
- if (pixel != 0 && pixel != 231) { // 0 and 231 = transparent
- int destX = overlayX + x;
- int destY = overlayY + y;
- if (destX >= 0 && destY >= 0) {
- renderBitmap[destY * pitch + destX] = pixel;
+ // Calculate source offset and destination position (FUN_00428a10 lines 31-43)
+ // If position is negative, skip source pixels and draw at screen edge
+ // Use _rebelViewOffsetX which is DAT_0045790c (damageLevel * -5 - 14)
+ int srcOffsetX = 0;
+ int srcOffsetY = 0;
+ int destX = _rebelViewOffsetX; // DAT_0045790c
+ int destY = _rebelViewOffsetY; // DAT_0045790e
+ int drawWidth = corridorOverlay.width;
+ int drawHeight = corridorOverlay.height;
+
+ // Handle negative X position: skip source pixels, draw at X=0
+ if (destX < 0) {
+ srcOffsetX = -destX;
+ drawWidth -= srcOffsetX;
+ destX = 0;
+ }
+ // Handle negative Y position: skip source pixels, draw at Y=0
+ if (destY < 0) {
+ srcOffsetY = -destY;
+ drawHeight -= srcOffsetY;
+ destY = 0;
+ }
+
+ // Clip to screen bounds
+ if (destX + drawWidth > pitch) {
+ drawWidth = pitch - destX;
+ }
+ if (destY + drawHeight > bufHeight) {
+ drawHeight = bufHeight - destY;
+ }
+
+ // Bounds check: ensure srcOffsetX doesn't exceed image width
+ if (srcOffsetX >= corridorOverlay.width) {
+ debug("Rebel2 procPreRendering: srcOffsetX (%d) >= image width (%d), skipping", srcOffsetX, corridorOverlay.width);
+ }
+ // Bounds check: ensure we have valid draw dimensions
+ else if (drawWidth > 0 && drawHeight > 0) {
+ // Additional safety: clamp to source image bounds
+ int maxDrawWidth = corridorOverlay.width - srcOffsetX;
+ int maxDrawHeight = corridorOverlay.height - srcOffsetY;
+ if (drawWidth > maxDrawWidth) drawWidth = maxDrawWidth;
+ if (drawHeight > maxDrawHeight) drawHeight = maxDrawHeight;
+
+ if (drawWidth > 0 && drawHeight > 0) {
+ for (int y = 0; y < drawHeight; y++) {
+ for (int x = 0; x < drawWidth; x++) {
+ int srcIdx = (srcOffsetY + y) * corridorOverlay.width + (srcOffsetX + x);
+ byte pixel = corridorOverlay.pixels[srcIdx];
+ if (pixel != 0 && pixel != 231) { // 0 and 231 = transparent
+ renderBitmap[(destY + y) * pitch + (destX + x)] = pixel;
+ }
}
}
}
}
- debug("Rebel2 procPreRendering: Corridor overlay at (%d,%d) size(%d,%d)",
- overlayX, overlayY, corridorOverlay.width, corridorOverlay.height);
+
+ debug("Rebel2 procPreRendering: Corridor overlay viewOff=(%d,%d) damageLevel=%d autopilot=%d srcOff=(%d,%d) dest=(%d,%d) draw=(%d,%d) imgSize=(%d,%d)",
+ _rebelViewOffsetX, _rebelViewOffsetY, _rebelDamageLevel, _rebelAutopilot,
+ srcOffsetX, srcOffsetY, destX, destY, drawWidth, drawHeight,
+ corridorOverlay.width, corridorOverlay.height);
}
}
}
@@ -1458,8 +1527,19 @@ void InsaneRebel2::iactRebel2Opcode6(byte *renderBitmap, Common::SeekableReadStr
// Handler 25 (0x19) specific logic (mixed mode - speeder bike)
// Based on FUN_0041cadb case 4 (opcode 6) lines 113-229
if (_rebelHandler == 25) {
- // If par4 == 1, enable status bar and reset state (lines 114-121)
- if (par4 == 1) {
+ // Read the reset flag from IACT data at offset 8-9 (local_14[4] in decompiled code)
+ // The stream position should be at offset 8 after par4 was read
+ // From FUN_0041cadb line 114: if (local_14[4] == 1) { ... reset ... }
+ int16 par5 = 0;
+ if (b.pos() + 2 <= b.size()) {
+ int64 savedPos = b.pos();
+ par5 = b.readSint16LE();
+ b.seek(savedPos); // Don't consume the stream
+ }
+
+ // If par5 == 1, enable status bar and reset state (lines 114-121)
+ // Note: This is local_14[4] in the decompiled code, NOT local_14[3] (par4)
+ if (par5 == 1) {
_rebelStatusBarSprite = 5;
// Reset link tables (DAT_0045797c through DAT_0045917c)
for (int i = 0; i < 512; i++) {
@@ -1467,28 +1547,44 @@ void InsaneRebel2::iactRebel2Opcode6(byte *renderBitmap, Common::SeekableReadStr
_rebelLinks[i][1] = 0;
_rebelLinks[i][2] = 0;
}
- debug("Rebel2 Opcode 6 (Handler 25): Status bar enabled, state reset");
+ // Initialize to covered state - player starts behind cover
+ // This ensures the corridor overlay shows the "covered" position initially
+ _rebelAutopilot = 1; // DAT_00457904 = 1 (covered)
+ _rebelDamageLevel = 5; // DAT_0045790a = 5 (fully in cover)
+ debug("Rebel2 Opcode 6 (Handler 25): Status bar enabled, state reset, starting COVERED");
}
- // Set sprite mode (DAT_00457900 = par4) - controls which GRD sprite to render
- // NOTE: Original code uses local_14[3] which is par4, NOT par3!
+ // Set sprite mode (DAT_00457900 = local_14[3]) - controls which GRD sprite to render
+ // From FUN_0041cadb line 122: DAT_00457900 = local_14[3];
+ // In ScummVM's IACT parsing: local_14[3] = offset 6-7 = par4
// Mode 1: Uncovered, shooting position - sprite on left
// Mode 2: Covered, vertical shift
// Mode 3: Transition between covered/uncovered - sprite position depends on direction
// Mode 4: Alternative uncovered position - sprite on right
- _grdSpriteMode = par4;
+ _grdSpriteMode = par4; // local_14[3] maps to par4 (offset 6-7)
+
+ debug("Rebel2 Handler25 Opcode6: par2=%d par3=%d par4=%d(mode) par5=%d(reset) autopilot=%d damageLevel=%d controlMode=%d",
+ par2, par3, par4, par5, _rebelAutopilot, _rebelDamageLevel, _rebelControlMode);
// Autopilot logic (lines 123-146)
+ // From original FUN_0041cadb - NO damageLevel check, toggle happens immediately
+ // The damage level counter provides the smooth visual transition
if (!_rebelInvulnerable) {
if (_rebelAutopilot == 0) {
+ // Uncovered: RIGHT button enters cover
if ((_rebelControlMode & 2) != 0) {
_rebelAutopilot = 1;
+ debug("Rebel2 Handler25: Entering cover (right click), controlMode=%d", _rebelControlMode);
}
} else {
+ // Covered: ANY button exits cover
if (_rebelControlMode != 0) {
_rebelAutopilot = 0;
+ debug("Rebel2 Handler25: Exiting cover (button click), controlMode=%d", _rebelControlMode);
}
}
+ // Clear control mode after processing (sticky flags consumed)
+ _rebelControlMode = 0;
} else {
// Invulnerable mode: random autopilot changes
if (_rebelAutopilot == 0) {
@@ -1504,15 +1600,23 @@ void InsaneRebel2::iactRebel2Opcode6(byte *renderBitmap, Common::SeekableReadStr
}
// Update damage level counter (lines 147-154)
+ // This provides the smooth transition animation between covered/uncovered states
+ int prevDamageLevel = _rebelDamageLevel;
if (_rebelAutopilot == 0) {
+ // Uncovered: decrement damage level towards 0
if (_rebelDamageLevel > 0) {
_rebelDamageLevel--;
}
} else {
+ // Covered: increment damage level towards 5
if (_rebelDamageLevel < 5) {
_rebelDamageLevel++;
}
}
+ if (_rebelDamageLevel != prevDamageLevel) {
+ debug("Rebel2 Handler25: damageLevel transition %d -> %d (autopilot=%d)",
+ prevDamageLevel, _rebelDamageLevel, _rebelAutopilot);
+ }
// Flight direction logic for mode 3 (lines 155-177)
if (_grdSpriteMode == 3) {
@@ -1575,10 +1679,19 @@ void InsaneRebel2::iactRebel2Opcode6(byte *renderBitmap, Common::SeekableReadStr
_rebelViewOffset2X = dirMultX2 * (5 - _rebelDamageLevel); // DAT_00457910
_rebelViewOffsetY = 0;
_rebelViewOffset2Y = 0;
+ } else {
+ // Mode 0 or unknown: use Mode 1 defaults as fallback
+ _rebelViewMode1 = 0x0e;
+ _rebelViewMode2 = 0;
+ _rebelViewOffsetX = _rebelDamageLevel * -5 + -14;
+ _rebelViewOffset2X = _rebelDamageLevel * -22;
+ _rebelViewOffsetY = 0;
+ _rebelViewOffset2Y = 0;
+ debug("Rebel2 Opcode 6 (Handler 25): Unknown mode %d, using Mode 1 fallback", _grdSpriteMode);
}
- debug("Rebel2 Opcode 6 (Handler 25): mode=%d damage=%d dir=%d viewOff=(%d,%d) spritePos=(%d,%d)",
- _grdSpriteMode, _rebelDamageLevel, _rebelFlightDir,
+ debug("Rebel2 Opcode 6 (Handler 25): mode=%d damage=%d dir=%d autopilot=%d viewOff=(%d,%d) spritePos=(%d,%d)",
+ _grdSpriteMode, _rebelDamageLevel, _rebelFlightDir, _rebelAutopilot,
_rebelViewOffsetX, _rebelViewOffsetY, _rebelViewOffset2X, _rebelViewOffset2Y);
return;
@@ -3572,6 +3685,12 @@ void InsaneRebel2::drawCollisionZones(byte *dst, int pitch, int width, int heigh
}
void InsaneRebel2::renderNutSprite(byte *dst, int pitch, int width, int height, int x, int y, NutRenderer *nut, int spriteIdx) {
+ renderNutSpriteMirrored(dst, pitch, width, height, x, y, nut, spriteIdx, false);
+}
+
+// Render NUT sprite with optional horizontal mirroring
+// Based on FUN_004236e0 disassembly - flags=0x2001 triggers horizontal flip
+void InsaneRebel2::renderNutSpriteMirrored(byte *dst, int pitch, int width, int height, int x, int y, NutRenderer *nut, int spriteIdx, bool mirror) {
if (!nut || spriteIdx < 0 || spriteIdx >= nut->getNumChars()) return;
int w = nut->getCharWidth(spriteIdx);
@@ -3606,12 +3725,19 @@ void InsaneRebel2::renderNutSprite(byte *dst, int pitch, int width, int height,
if (drawW <= 0 || drawH <= 0) return;
- // Draw loop
+ // Draw loop - with optional horizontal mirroring
for (int iy = 0; iy < drawH; iy++) {
- const byte *s = src + (srcOffsetY + iy) * w + srcOffsetX;
+ const byte *s = src + (srcOffsetY + iy) * w;
byte *d = dst + (drawY + iy) * pitch + drawX;
for (int ix = 0; ix < drawW; ix++) {
- byte px = s[ix];
+ int srcX;
+ if (mirror) {
+ // When mirrored, read from the opposite side of the sprite
+ srcX = (w - 1) - (srcOffsetX + ix);
+ } else {
+ srcX = srcOffsetX + ix;
+ }
+ byte px = s[srcX];
if (px != 231 && px != 0) { // Check both 0 and 231 (0xE7) for transparency
d[ix] = px;
}
@@ -4239,27 +4365,26 @@ void InsaneRebel2::renderHandler25Ship(byte *renderBitmap, int pitch, int width,
if (!_grd001Sprite && !_grd002Sprite)
return;
+ // CRITICAL: Clip height to 180 (0xb4) to avoid drawing over status bar
+ // From FUN_0041db5e line 260: _DAT_00482236 = 0xb4 during gameplay
+ // The status bar occupies Y=180-200, and sprites must not render there
+ const int clipHeight = 180; // Status bar boundary (0xb4)
+ int renderHeight = MIN(height, clipHeight);
+
// Base position calculation based on FUN_0041db5e disassembly:
// The original reads sprite internal offsets from the NUT data at +0x12 (X) and +0x14 (Y).
- // Since NutRenderer doesn't expose these, we use the standard positioning formula:
- // - X: center (160) + mode-based offset
- // - Y: positioned above status bar (status bar starts at ~Y=180)
+ // Position calculation from FUN_0041db5e assembly:
+ //
+ // GRD001 (line 206): FUN_004236e0(..., DAT_00457910, DAT_00457912, ...)
+ // - Raw offsets passed directly, render function applies sprite internal offsets
//
- // From the HUD positioning formula (lines 3583-3584):
- // Y = 182 - (mouseOffset >> 4) - height - spriteOffsetY
- // This means sprites are bottom-aligned relative to Y=182.
+ // GRD002 (lines 238-247): Position uses sprite header offsets:
+ // - X = DAT_00457910 + sprite_internal_x_offset (NUT header +0x12)
+ // - Y = sprite_internal_y_offset (NUT header +0x14) + DAT_00457912
//
- // For Handler 25, the sprite position offsets (DAT_00457910, DAT_00457912) are added.
- const int baseX = 160;
- // Base Y is calculated to keep sprite above status bar (Y=180)
- // We'll position the center around Y=90 (middle of playable area)
- const int baseY = 90;
-
- // Calculate actual ship position from base + offset
- // _rebelViewOffset2X = DAT_00457910 (sprite X offset from mode/damage)
- // _rebelViewOffset2Y = DAT_00457912 (sprite Y offset from mode/damage)
- int shipX = baseX + _rebelViewOffset2X;
- int shipY = baseY + _rebelViewOffset2Y;
+ // Since NutRenderer doesn't expose internal offsets, we simulate them:
+ // - GRD001: Draw at raw offset position (the sprite likely has built-in positioning)
+ // - GRD002: Add base position to simulate internal offsets for character sprite
// Draw _grd001Sprite based on _grdSpriteMode (DAT_00457900)
// Mode 1, 2, 3, 4 all potentially draw _grd001Sprite
@@ -4284,17 +4409,21 @@ void InsaneRebel2::renderHandler25Ship(byte *renderBitmap, int pitch, int width,
}
if (shouldDraw) {
+ // GRD001 is drawn at raw (DAT_00457910, DAT_00457912) per assembly line 206
+ // The render function applies the sprite's internal positioning
// Use sprite 0 (primary frame)
int spriteW = _grd001Sprite->getCharWidth(0);
int spriteH = _grd001Sprite->getCharHeight(0);
- // Center the sprite at the calculated position
- int drawX = shipX - spriteW / 2;
- int drawY = shipY - spriteH / 2;
- renderNutSprite(renderBitmap, pitch, width, height, drawX, drawY, _grd001Sprite, 0);
+ // Draw at raw offset position - the sprite is designed to be positioned
+ // at these coordinates (the NUT's internal offsets handle actual placement)
+ int drawX = _rebelViewOffset2X;
+ int drawY = _rebelViewOffset2Y;
- debug("Rebel2 Handler25: GRD001 at (%d,%d) base(%d,%d) offset(%d,%d) size(%d,%d) mode=%d damage=%d",
- drawX, drawY, baseX, baseY, _rebelViewOffset2X, _rebelViewOffset2Y,
+ renderNutSprite(renderBitmap, pitch, width, renderHeight, drawX, drawY, _grd001Sprite, 0);
+
+ debug("Rebel2 Handler25: GRD001 at (%d,%d) offset(%d,%d) size(%d,%d) mode=%d damage=%d",
+ drawX, drawY, _rebelViewOffset2X, _rebelViewOffset2Y,
spriteW, spriteH, _grdSpriteMode, _rebelDamageLevel);
}
}
@@ -4311,14 +4440,49 @@ void InsaneRebel2::renderHandler25Ship(byte *renderBitmap, int pitch, int width,
int spriteIdx;
int numSprites = _grd002Sprite->getNumChars();
+ // Determine if we should mirror the sprite (from FUN_41DB5E lines 231-235)
+ // Mirror when: direction != 0 AND damage == 0 (fully uncovered, facing right)
+ bool shouldMirror = (_rebelFlightDir != 0) && (_rebelDamageLevel == 0);
+
if (_rebelDamageLevel == 0) {
// Uncovered state: use aiming-based sprite selection (5-14)
- // For now, use center position (xZone=2, yZone=0) -> 0*5+2+5 = 7
- // TODO: Calculate actual zone from crosshair position
- int xZone = 2; // Center horizontal (0-4)
- int yZone = 0; // Top vertical (0-1)
+ // Calculate zones from crosshair position relative to playable area
+ // From FUN_41DB5E lines 155-164
+ //
+ // The playable area bounds are defined by corridor boundaries.
+ // xZone = 0-4 (left to right), yZone = 0-1 (top to bottom)
+ // Default to center if bounds not set
+ int16 areaLeft = (_corridorLeftX > 0) ? _corridorLeftX : 0;
+ int16 areaRight = (_corridorRightX > 0) ? _corridorRightX : 320;
+ int16 areaTop = (_corridorTopY > 0) ? _corridorTopY : 0;
+ int16 areaBottom = (_corridorBottomY > 0) ? _corridorBottomY : 180;
+
+ // Get crosshair position (using mouse position scaled to game coords)
+ int16 crosshairX = _vm->_mouse.x;
+ int16 crosshairY = _vm->_mouse.y;
+ if (_player && _player->_width > 320) {
+ crosshairX = (crosshairX * 320) / _player->_width;
+ crosshairY = (crosshairY * 200) / _player->_height;
+ }
+
+ // Calculate zone widths
+ int areaWidth = areaRight - areaLeft;
+ int areaHeight = areaBottom - areaTop;
+ int zoneWidth = (areaWidth > 0) ? (areaWidth + 3) / 4 : 80; // Divide into ~4 zones
+ int zoneHeight = (areaHeight > 0) ? areaHeight / 2 : 90; // Divide into 2 zones
+
+ // Calculate xZone (0-4) and yZone (0-1) from crosshair position
+ int xZone = (zoneWidth > 0) ? ((zoneWidth / 2) + (crosshairX - areaLeft)) / zoneWidth : 2;
+ int yZone = (zoneHeight > 0) ? ((zoneHeight / 2) + (crosshairY - areaTop)) / zoneHeight : 0;
+
+ // Clamp to valid ranges
+ if (xZone < 0) xZone = 0;
+ if (xZone > 4) xZone = 4;
+ if (yZone < 0) yZone = 0;
+ if (yZone > 1) yZone = 1;
- // Direction-based mirroring (line 161-162)
+ // Direction-based sprite flip logic (line 161-162 in decompiled)
+ // if (DAT_00457902 == (uVar7 & 1)) { local_58 = 4 - local_58; }
if (_rebelFlightDir == (yZone & 1)) {
xZone = 4 - xZone;
}
@@ -4326,11 +4490,15 @@ void InsaneRebel2::renderHandler25Ship(byte *renderBitmap, int pitch, int width,
spriteIdx = yZone * 5 + xZone + 5;
} else {
// Transitioning/covered state: use direction-based sprite
+ // From FUN_41DB5E lines 166-168:
+ // sVar8 = ((-(ushort)(DAT_00457902 == 0) & 0xffec) + 0x19) - DAT_0045790a
+ // direction == 0: 5 - damage
+ // direction != 0: 25 - damage
if (_rebelFlightDir == 0) {
- // Direction 0: sprites 0-5 (covered left)
+ // Direction 0: sprites 0-5 (transition left)
spriteIdx = 5 - _rebelDamageLevel;
} else {
- // Direction 1: sprites 20-25 (covered right)
+ // Direction 1: sprites 20-25 (transition right)
spriteIdx = 25 - _rebelDamageLevel;
}
}
@@ -4342,15 +4510,43 @@ void InsaneRebel2::renderHandler25Ship(byte *renderBitmap, int pitch, int width,
int spriteW = _grd002Sprite->getCharWidth(spriteIdx);
int spriteH = _grd002Sprite->getCharHeight(spriteIdx);
- // Position offset from GRD002 sprite header (lines 238-243 in disasm)
- // This adds an offset from the sprite's internal positioning data
- int drawX = shipX - spriteW / 2;
- int drawY = shipY - spriteH / 2;
+ // Position calculation from FUN_41DB5E lines 237-247:
+ // GRD002 explicitly adds sprite internal offsets from NUT header:
+ //
+ // Normal case (direction==0 OR damage!=0):
+ // local_60 = sprite_internal_x_offset (from NUT header +0x12)
+ // X = DAT_00457910 + local_60
+ // Y = sprite_internal_y_offset (from NUT header +0x14) + DAT_00457912
+ //
+ // Mirrored case (direction!=0 AND damage==0):
+ // local_60 = 320 - sprite_width - sprite_internal_x_offset
+ // X = DAT_00457910 + local_60
+ // Y = sprite_internal_y_offset + DAT_00457912
+ //
+ // Now using actual NUT sprite offsets from NutRenderer!
+ int16 spriteXOffset = _grd002Sprite->getCharXOffset(spriteIdx);
+ int16 spriteYOffset = _grd002Sprite->getCharYOffset(spriteIdx);
+
+ int drawX, drawY;
+
+ if (shouldMirror) {
+ // Mirrored position: X = DAT_00457910 + (320 - sprite_width - sprite_x_offset)
+ // From assembly lines 240-243
+ drawX = _rebelViewOffset2X + (320 - spriteW - spriteXOffset);
+ } else {
+ // Normal position: X = DAT_00457910 + sprite_internal_x_offset
+ // From assembly line 238
+ drawX = _rebelViewOffset2X + spriteXOffset;
+ }
+
+ // Y = sprite_internal_y_offset + DAT_00457912
+ // From assembly line 246
+ drawY = spriteYOffset + _rebelViewOffset2Y;
- renderNutSprite(renderBitmap, pitch, width, height, drawX, drawY, _grd002Sprite, spriteIdx);
+ renderNutSpriteMirrored(renderBitmap, pitch, width, renderHeight, drawX, drawY, _grd002Sprite, spriteIdx, shouldMirror);
- debug("Rebel2 Handler25: GRD002 at (%d,%d) size(%d,%d) spriteIdx=%d damage=%d dir=%d",
- drawX, drawY, spriteW, spriteH, spriteIdx, _rebelDamageLevel, _rebelFlightDir);
+ debug("Rebel2 Handler25: GRD002 at (%d,%d) nutOffset(%d,%d) viewOffset(%d,%d) size(%d,%d) spriteIdx=%d damage=%d dir=%d mirror=%d",
+ drawX, drawY, spriteXOffset, spriteYOffset, _rebelViewOffset2X, _rebelViewOffset2Y, spriteW, spriteH, spriteIdx, _rebelDamageLevel, _rebelFlightDir, shouldMirror ? 1 : 0);
}
}
@@ -4693,6 +4889,12 @@ void InsaneRebel2::renderCrosshair(byte *renderBitmap, int pitch, int width, int
return;
}
+ // Handler 25 (0x19): From FUN_41DB5E lines 195-197, crosshair only drawn when
+ // DAT_0045790a == 0 (fully uncovered). Hide crosshair during cover transition.
+ if (_rebelHandler == 25 && _rebelDamageLevel != 0) {
+ return;
+ }
+
// Update target lock state and draw crosshair/reticle
// Target lock detection (DAT_00443676 equivalent)
@@ -4719,8 +4921,8 @@ void InsaneRebel2::renderCrosshair(byte *renderBitmap, int pitch, int width, int
int reticleIndex;
switch (_rebelHandler) {
case 7: // Third-Person Ship
- case 0x19: // FPS/Mixed
- reticleIndex = 47;
+ case 0x19: // FPS/Mixed (Handler 25)
+ reticleIndex = 47; // 0x2F
break;
case 0x26: { // Turret/Cockpit - animated crosshair
static int turretAnimCounter = 0;
@@ -4744,8 +4946,20 @@ void InsaneRebel2::renderCrosshair(byte *renderBitmap, int pitch, int width, int
if (_smush_iconsNut->getNumChars() > reticleIndex) {
int cw = _smush_iconsNut->getCharWidth(reticleIndex);
int ch = _smush_iconsNut->getCharHeight(reticleIndex);
+
+ // Calculate crosshair position
+ int crosshairX = _vm->_mouse.x - cw / 2 + _viewX;
+ int crosshairY = _vm->_mouse.y - ch / 2 + _viewY;
+
+ // Handler 25 (0x19): Add view offset to crosshair position
+ // From FUN_41DB5E lines 198-199: X = DAT_00457914 + DAT_0045790c, Y = DAT_00457916 + DAT_0045790e
+ if (_rebelHandler == 25) {
+ crosshairX += _rebelViewOffsetX;
+ crosshairY += _rebelViewOffsetY;
+ }
+
renderNutSprite(renderBitmap, pitch, width, height,
- _vm->_mouse.x - cw / 2 + _viewX, _vm->_mouse.y - ch / 2 + _viewY,
+ crosshairX, crosshairY,
_smush_iconsNut, reticleIndex);
}
}
diff --git a/engines/scumm/insane/insane_rebel.h b/engines/scumm/insane/insane_rebel.h
index 04aa51d7ab2..cdef385b033 100644
--- a/engines/scumm/insane/insane_rebel.h
+++ b/engines/scumm/insane/insane_rebel.h
@@ -422,6 +422,7 @@ public:
int16 animFrame, int16 maxFrames,
int16 widthScale, int16 heightScale, int16 thickness);
void renderNutSprite(byte *dst, int pitch, int width, int height, int x, int y, NutRenderer *nut, int spriteIdx);
+ void renderNutSpriteMirrored(byte *dst, int pitch, int width, int height, int x, int y, NutRenderer *nut, int spriteIdx, bool mirror);
struct enemy {
int id;
@@ -587,6 +588,7 @@ public:
int _rebelDamageLevel; // DAT_0045790a - damage level (0-5)
int _rebelFlightDir; // DAT_00457902 - flight direction (0 or 1)
int _rebelControlMode; // DAT_0047a7e4 - control mode flags
+ int _rebelInputThrottle; // DAT_00482278 - frame counter for input throttling (every 5 frames)
// View offset variables (calculated from level type)
int _rebelViewOffsetX; // DAT_0045790c
diff --git a/engines/scumm/nut_renderer.cpp b/engines/scumm/nut_renderer.cpp
index fa24c7fb049..3448672be24 100644
--- a/engines/scumm/nut_renderer.cpp
+++ b/engines/scumm/nut_renderer.cpp
@@ -174,8 +174,8 @@ void NutRenderer::loadFont(const char *filename) {
break;
}
int codec = READ_LE_UINT16(dataSrc + offset + 8);
- // _chars[l].xoffs = READ_LE_UINT16(dataSrc + offset + 10);
- // _chars[l].yoffs = READ_LE_UINT16(dataSrc + offset + 12);
+ _chars[l].xoffs = READ_LE_INT16(dataSrc + offset + 10);
+ _chars[l].yoffs = READ_LE_INT16(dataSrc + offset + 12);
_chars[l].width = READ_LE_UINT16(dataSrc + offset + 14);
_chars[l].height = READ_LE_UINT16(dataSrc + offset + 16);
_chars[l].src = decodedPtr;
@@ -285,6 +285,8 @@ void NutRenderer::loadFontFromData(const byte *data, int32 dataSize) {
break;
}
int codec = READ_LE_UINT16(dataSrc + offset + 8);
+ _chars[l].xoffs = READ_LE_INT16(dataSrc + offset + 10);
+ _chars[l].yoffs = READ_LE_INT16(dataSrc + offset + 12);
_chars[l].width = READ_LE_UINT16(dataSrc + offset + 14);
_chars[l].height = READ_LE_UINT16(dataSrc + offset + 16);
_chars[l].src = decodedPtr;
diff --git a/engines/scumm/nut_renderer.h b/engines/scumm/nut_renderer.h
index 92abdfb246f..c83ac830400 100644
--- a/engines/scumm/nut_renderer.h
+++ b/engines/scumm/nut_renderer.h
@@ -54,6 +54,8 @@ protected:
struct {
uint16 width;
uint16 height;
+ int16 xoffs; // X offset from NUT frame header (for sprite positioning)
+ int16 yoffs; // Y offset from NUT frame header (for sprite positioning)
byte *src;
byte transparency;
} _chars[256];
@@ -76,6 +78,8 @@ public:
int getCharWidth(byte c) const;
int getCharHeight(byte c) const;
+ int16 getCharXOffset(byte c) const { return _chars[c].xoffs; }
+ int16 getCharYOffset(byte c) const { return _chars[c].yoffs; }
const byte *getCharData(byte c);
byte getCharTransparency(byte c) const { return _chars[c].transparency; }
byte getBpp() const { return 8; }
Commit: 05b0dc540ae6da78fc6b57824dc11b9e690d9bba
https://github.com/scummvm/scummvm/commit/05b0dc540ae6da78fc6b57824dc11b9e690d9bba
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:00+02:00
Commit Message:
SCUMM: RA2: Improve bit handling
Changed paths:
engines/scumm/insane/insane_rebel.cpp
diff --git a/engines/scumm/insane/insane_rebel.cpp b/engines/scumm/insane/insane_rebel.cpp
index 16d00b0dbd9..e86a059644b 100644
--- a/engines/scumm/insane/insane_rebel.cpp
+++ b/engines/scumm/insane/insane_rebel.cpp
@@ -760,20 +760,38 @@ int32 InsaneRebel2::processMouse() {
}
bool InsaneRebel2::isBitSet(int n) {
+ // FUN_00423970: When param_1 < 1 (0 or negative), the bounds check fails and returns false.
+ // This means ID 0 or negative IDs are always treated as "enabled" (not skipped).
+ if (n < 1) {
+ return false;
+ }
assert (n < 0x200);
return (_iactBits[n] != 0);
}
void InsaneRebel2::setBit(int n) {
+ // FUN_004239b0: When n < 1 (i.e., n == 0 or negative), set ALL bits to 1 (disable all objects)
+ // This is used to disable all enemies/objects at once
+ if (n < 1) {
+ for (int i = 0; i < 0x200; i++)
+ _iactBits[i] = 1;
+ return;
+ }
assert (n < 0x200);
-
_iactBits[n] = 1;
}
void InsaneRebel2::clearBit(int n) {
+ // FUN_00423a00: When n < 1 (i.e., n == 0 or negative), clear ALL bits to 0 (enable all objects)
+ // This is called by FUN_00423880 at the start of video playback to reset the bit table,
+ // ensuring all enemies are visible when a new level/segment starts.
+ if (n < 1) {
+ for (int i = 0; i < 0x200; i++)
+ _iactBits[i] = 0;
+ return;
+ }
assert (n < 0x200);
-
_iactBits[n] = 0;
}
@@ -1116,13 +1134,16 @@ void InsaneRebel2::iactRebel2Opcode2(Common::SeekableReadStream &b, int16 par2,
if (par3 == 4) {
int16 childId = b.readSint16LE(); // Offset +8
int16 parentId = b.readSint16LE(); // Offset +10
-
- if (parentId >= 0 && parentId < 512) {
+
+ // Validate BOTH parentId AND childId to avoid triggering "set/clear ALL bits" behavior
+ // when childId <= 0. The original game's setBit(0)/clearBit(0) affects ALL bits,
+ // which would disable/enable all enemies at once - not the intended linking behavior.
+ if (parentId >= 1 && parentId < 512 && childId >= 1 && childId < 512) {
// Shift links
_rebelLinks[parentId][2] = _rebelLinks[parentId][1];
_rebelLinks[parentId][1] = _rebelLinks[parentId][0];
_rebelLinks[parentId][0] = childId;
-
+
// Apply initial state based on parent state
if (!isBitSet(parentId)) {
setBit(childId);
@@ -1131,13 +1152,17 @@ void InsaneRebel2::iactRebel2Opcode2(Common::SeekableReadStream &b, int16 par2,
clearBit(childId);
debug("Rebel2: Linked ID=%d to Parent=%d (Slot 0). Parent Dead -> Child Enabled.", childId, parentId);
}
+ } else {
+ debug("Rebel2: Skipping link with invalid IDs childId=%d parentId=%d", childId, parentId);
}
return;
} else if (par3 == 1) { // Probabilistic / counter cases: par3 == 1
int16 value = par4; // sVar6
int16 targetId = b.readSint16LE(); // Offset +8 (sVar7)
-
- if (targetId < 0 || targetId >= 0x200)
+
+ // Validate targetId >= 1 to avoid triggering "set/clear ALL bits" behavior
+ // The original game's setBit(0)/clearBit(0) affects ALL bits, not intended here
+ if (targetId < 1 || targetId >= 0x200)
return;
if (value > 1 && value < 10) { // 1 < value < 10: random disable
@@ -4409,21 +4434,25 @@ void InsaneRebel2::renderHandler25Ship(byte *renderBitmap, int pitch, int width,
}
if (shouldDraw) {
- // GRD001 is drawn at raw (DAT_00457910, DAT_00457912) per assembly line 206
- // The render function applies the sprite's internal positioning
+ // GRD001 is drawn at (DAT_00457910, DAT_00457912) per assembly line 206
+ // FUN_004236e0 internally applies the sprite's internal offsets from NUT header
// Use sprite 0 (primary frame)
int spriteW = _grd001Sprite->getCharWidth(0);
int spriteH = _grd001Sprite->getCharHeight(0);
- // Draw at raw offset position - the sprite is designed to be positioned
- // at these coordinates (the NUT's internal offsets handle actual placement)
- int drawX = _rebelViewOffset2X;
- int drawY = _rebelViewOffset2Y;
+ // Get sprite internal offsets from NUT header (like FUN_004236e0 does internally)
+ int16 spriteXOffset = _grd001Sprite->getCharXOffset(0);
+ int16 spriteYOffset = _grd001Sprite->getCharYOffset(0);
+
+ // Position = view offset + sprite internal offset
+ // This matches how FUN_004236e0 positions sprites using NUT header offsets
+ int drawX = _rebelViewOffset2X + spriteXOffset;
+ int drawY = _rebelViewOffset2Y + spriteYOffset;
renderNutSprite(renderBitmap, pitch, width, renderHeight, drawX, drawY, _grd001Sprite, 0);
- debug("Rebel2 Handler25: GRD001 at (%d,%d) offset(%d,%d) size(%d,%d) mode=%d damage=%d",
- drawX, drawY, _rebelViewOffset2X, _rebelViewOffset2Y,
+ debug("Rebel2 Handler25: GRD001 at (%d,%d) nutOffset(%d,%d) viewOffset(%d,%d) size(%d,%d) mode=%d damage=%d",
+ drawX, drawY, spriteXOffset, spriteYOffset, _rebelViewOffset2X, _rebelViewOffset2Y,
spriteW, spriteH, _grdSpriteMode, _rebelDamageLevel);
}
}
@@ -7470,6 +7499,10 @@ int InsaneRebel2::runLevel1() {
_playerDamage = 0;
_deathFrame = 0;
+ // Reset bit table before gameplay starts - FUN_00423880 calls FUN_00423a00(0)
+ // This ensures all enemies are visible (not skipped by SKIP chunks)
+ clearBit(0);
+
// Play gameplay (01P01.SAN with 0x28 flags)
splayer->setCurVideoFlags(0x28);
splayer->play("LEV01/01P01.SAN", 12);
@@ -7532,6 +7565,17 @@ int InsaneRebel2::runLevel2() {
_currentPhase = 1;
bonusCount = 0;
+ // Reset bit table before gameplay starts - FUN_00423880 calls FUN_00423a00(0)
+ // This ensures all enemies are visible (not skipped by SKIP chunks)
+ clearBit(0);
+
+ // Also reset link tables for enemy dependencies
+ for (int i = 0; i < 512; i++) {
+ _rebelLinks[i][0] = 0;
+ _rebelLinks[i][1] = 0;
+ _rebelLinks[i][2] = 0;
+ }
+
// ===== PHASE 1: P1/02P01_X.SAN =====
// First play variant A which contains the background IACT (opcode 8, par4=5)
// The background is loaded during this video and persists for B/C/D variants
@@ -7581,6 +7625,9 @@ int InsaneRebel2::runLevel2() {
_currentPhase = 2;
+ // Reset bit table before Phase 2 gameplay starts
+ clearBit(0);
+
// ===== PHASE 2: P2/02P02_X.SAN =====
// First play variant A which contains the background IACT for this phase
{
@@ -7629,6 +7676,9 @@ int InsaneRebel2::runLevel2() {
_currentPhase = 3;
+ // Reset bit table before Phase 3 gameplay starts
+ clearBit(0);
+
// ===== PHASE 3: P3/02P03_X.SAN =====
// First play variant A which contains the background IACT for this phase
{
@@ -7700,6 +7750,9 @@ int InsaneRebel2::runLevel3() {
_playerDamage = 0;
_currentPhase = 1;
+ // Reset bit table before gameplay starts - FUN_00423880 calls FUN_00423a00(0)
+ clearBit(0);
+
// Play phase 1 gameplay (03PLAY1.SAN)
debug("Rebel2: Level 3 Phase 1");
splayer->setCurVideoFlags(0x28);
@@ -7744,6 +7797,9 @@ int InsaneRebel2::runLevel3() {
_playerShield = 255;
_playerDamage = 0;
+ // Reset bit table before gameplay starts
+ clearBit(0);
+
// Play phase 2 gameplay (03PLAY2.SAN)
debug("Rebel2: Level 3 Phase 2");
splayer->setCurVideoFlags(0x28);
@@ -7803,6 +7859,9 @@ int InsaneRebel2::runLevel4() {
_playerDamage = 0;
_currentPhase = 1;
+ // Reset bit table before gameplay starts
+ clearBit(0);
+
// Play gameplay (04PLAY.SAN)
debug("Rebel2: Level 4 gameplay");
splayer->setCurVideoFlags(0x28);
@@ -7856,6 +7915,9 @@ int InsaneRebel2::runLevel5() {
_playerDamage = 0;
_currentPhase = 1;
+ // Reset bit table before gameplay starts
+ clearBit(0);
+
// Play gameplay (05PLAY.SAN)
debug("Rebel2: Level 5 gameplay");
splayer->setCurVideoFlags(0x28);
@@ -7910,6 +7972,9 @@ int InsaneRebel2::runLevel6() {
_playerDamage = 0;
_currentPhase = 1;
+ // Reset bit table before gameplay starts
+ clearBit(0);
+
// Play phase 1 gameplay (06PLAY1.SAN)
debug("Rebel2: Level 6 Phase 1");
splayer->setCurVideoFlags(0x28);
@@ -7952,6 +8017,9 @@ int InsaneRebel2::runLevel6() {
_playerShield = 255;
_playerDamage = 0;
+ // Reset bit table before gameplay starts
+ clearBit(0);
+
// Play phase 2 gameplay (06PLAY2.SAN)
debug("Rebel2: Level 6 Phase 2");
splayer->setCurVideoFlags(0x28);
Commit: 30795feeb52874254620fe82bbbe50985a93b69a
https://github.com/scummvm/scummvm/commit/30795feeb52874254620fe82bbbe50985a93b69a
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:00+02:00
Commit Message:
SCUMM: RA2: Improve level 2 rendering
Changed paths:
engines/scumm/insane/insane_rebel.cpp
engines/scumm/insane/insane_rebel.h
engines/scumm/smush/smush_player.cpp
diff --git a/engines/scumm/insane/insane_rebel.cpp b/engines/scumm/insane/insane_rebel.cpp
index e86a059644b..80bd3ca95f8 100644
--- a/engines/scumm/insane/insane_rebel.cpp
+++ b/engines/scumm/insane/insane_rebel.cpp
@@ -885,10 +885,26 @@ void InsaneRebel2::procPreRendering(byte *renderBitmap) {
// From FUN_00428a10: When position is negative, we skip source pixels and draw at 0.
// This creates a "scrolling window" effect as the character enters/exits cover.
if (_rebelHandler == 25 && renderBitmap) {
+ // Calculate pitch and buffer height for both corridor overlay and GRD001 rendering
+ int pitch = (_player && _player->_width > 0) ? _player->_width : 320;
+ int bufHeight = (_player && _player->_height > 0) ? _player->_height : 200;
+
+ // FIRST: Restore Level 2 background (par4=5) as the base layer
+ // This is the scene background that enemies (FOBJ) draw on top of.
+ // Without this, enemies are drawn onto black/empty buffer and appear invisible.
+ // The corridor overlay (par4=4) is a HUD frame drawn ON TOP of this background.
+ if (_level2BackgroundLoaded && _level2Background) {
+ for (int y = 0; y < MIN(200, bufHeight); y++) {
+ memcpy(renderBitmap + y * pitch, _level2Background + y * 320, MIN(320, pitch));
+ }
+ debug("Rebel2 Handler25 PRE: Restored _level2Background to renderBitmap");
+ } else {
+ debug("Rebel2 Handler25 PRE: WARNING - _level2Background NOT restored (loaded=%d ptr=%p)",
+ _level2BackgroundLoaded, (void*)_level2Background);
+ }
+
EmbeddedSanFrame &corridorOverlay = _rebelEmbeddedHud[4];
if (corridorOverlay.valid && corridorOverlay.pixels) {
- int pitch = (_player && _player->_width > 0) ? _player->_width : 320;
- int bufHeight = (_player && _player->_height > 0) ? _player->_height : 200;
// Calculate source offset and destination position (FUN_00428a10 lines 31-43)
// If position is negative, skip source pixels and draw at screen edge
@@ -951,6 +967,11 @@ void InsaneRebel2::procPreRendering(byte *renderBitmap) {
srcOffsetX, srcOffsetY, destX, destY, drawWidth, drawHeight,
corridorOverlay.width, corridorOverlay.height);
}
+
+ // Draw GRD001 (wall/cockpit overlay) AFTER corridor but BEFORE FOBJ enemies.
+ // This ensures enemies from FOBJ chunks draw ON TOP of the cockpit overlay.
+ // Uses width-halving logic from FUN_0041db5e lines 202-221.
+ renderHandler25ShipPre(renderBitmap, pitch, pitch, bufHeight);
}
}
@@ -1144,14 +1165,13 @@ void InsaneRebel2::iactRebel2Opcode2(Common::SeekableReadStream &b, int16 par2,
_rebelLinks[parentId][1] = _rebelLinks[parentId][0];
_rebelLinks[parentId][0] = childId;
- // Apply initial state based on parent state
- if (!isBitSet(parentId)) {
- setBit(childId);
- debug("Rebel2: Linked ID=%d to Parent=%d (Slot 0). Parent Alive -> Child Disabled.", childId, parentId);
- } else {
- clearBit(childId);
- debug("Rebel2: Linked ID=%d to Parent=%d (Slot 0). Parent Dead -> Child Enabled.", childId, parentId);
- }
+ // DO NOT modify bits here! All bits are reset to CLEAR by clearBit(0) at level start.
+ // SKIP chunks use XOR logic: skip when bits DIFFER.
+ // - Both bits CLEAR (alive) -> same -> don't skip -> enemy visible
+ // - Parent SET (dead), child SET (disabled) -> same -> skip -> enemy hidden
+ // The bits are only modified when enemies are actually destroyed (setBit/clearBit
+ // calls in processMouse() enemy destruction handling).
+ debug("Rebel2: Linked ID=%d to Parent=%d (Slot 0)", childId, parentId);
} else {
debug("Rebel2: Skipping link with invalid IDs childId=%d parentId=%d", childId, parentId);
}
@@ -3133,6 +3153,9 @@ void InsaneRebel2::spawnShot(int x, int y) {
case 7: // Space combat
spawnSpaceShot(x, y);
break;
+ case 25: // Speeder bike - uses turret shot array with different gun position
+ spawnHandler25Shot(x, y);
+ break;
default:
// Legacy fallback
for (int i = 0; i < 2; i++) {
@@ -3180,6 +3203,34 @@ void InsaneRebel2::spawnVehicleShot(int x, int y) {
}
}
+// Handler 25 Speeder bike shot spawn (based on FUN_0041db5e lines 170-190)
+// Similar to turret but with character-based gun position
+void InsaneRebel2::spawnHandler25Shot(int x, int y) {
+ // Handler 25 can only shoot when uncovered (damage == 0)
+ if (_rebelDamageLevel != 0) {
+ return; // Can't shoot while taking cover
+ }
+
+ for (int i = 0; i < 2; i++) {
+ if (_turretShots[i].counter == 0) {
+ // Play sound: FUN_0041189e(6, i + 1, 0x7f, 0, 0)
+ // TODO: Play laser sound
+
+ _turretShots[i].counter = getShotMaxDuration();
+ _turretShots[i].seqNum = _turretShotSeqCounter;
+ _turretShotSeqCounter++;
+
+ // Target position is where player clicked (screen coords)
+ _turretShots[i].targetX = x;
+ _turretShots[i].targetY = y;
+
+ debug("Rebel2 Handler25: Spawned shot %d target (%d,%d)",
+ i, _turretShots[i].targetX, _turretShots[i].targetY);
+ break;
+ }
+ }
+}
+
// Handler 7 Space combat shot spawn (based on FUN_40D836 lines 146-166)
void InsaneRebel2::spawnSpaceShot(int x, int y) {
for (int i = 0; i < 2; i++) {
@@ -4108,6 +4159,13 @@ void InsaneRebel2::renderEmbeddedHudOverlays(byte *renderBitmap, int pitch, int
if (!frame.valid || !frame.pixels || frame.width <= 0 || frame.height <= 0)
continue;
+ // For Handler 25: Skip slot 4 (corridor overlay) here - it's already drawn
+ // in procPreRendering BEFORE FOBJ enemies. Drawing it again here (after FOBJs)
+ // would cover the enemies and make them invisible.
+ if (_rebelHandler == 25 && hudSlot == 4) {
+ continue;
+ }
+
// Skip small frames at (0,0) - likely animation patches
if (frame.renderX == 0 && frame.renderY == 0 && frame.width < 50 && frame.height < 60) {
debug(3, "Rebel2: Skipping small embedded frame at (0,0): slot=%d size=%dx%d",
@@ -4367,95 +4425,98 @@ void InsaneRebel2::renderHandler8Ship(byte *renderBitmap, int pitch, int width,
spriteIndex, numSprites, _shipDirectionH, _shipDirectionV);
}
-void InsaneRebel2::renderHandler25Ship(byte *renderBitmap, int pitch, int width, int height) {
- // Handler 25 Ship Rendering (Mixed Mode - GRD sprites)
- // Based on FUN_0041db5e disassembly (lines 202-248)
- // Uses _grd001Sprite (GRD001) and _grd002Sprite (GRD002)
- //
- // The GRD sprite is drawn at position (DAT_00457910, DAT_00457912) which are
- // offsets calculated from sprite mode and damage level, relative to screen center.
- // In the original, these appear to be direct pixel coordinates where:
- // - Mode 1: sprite shifts left as damage increases (X = damage * -22)
- // - Mode 4: sprite shifts right as damage increases (X = damage * 17 - 85)
- // - Mode 2: sprite shifts vertically
- // - Mode 3: depends on flight direction
- //
- // NOTE: The corridor overlay (par4=4) is drawn in procPreRendering BEFORE enemies,
- // not here, so that enemies appear ON TOP of the corridor.
-
+// Handler 25 PRE-rendering: Draw GRD001 BEFORE FOBJ decoding
+// This is called from procPreRendering so enemies draw ON TOP of GRD001
+//
+// From FUN_0041db5e disassembly (lines 202-221):
+// - Mode 1 with damage==0: Width halved (left half only, pixels 0-159)
+// - Mode 4 with damage==0: Width halved AND buffer offset (right half only, pixels 160-319)
+// - All other cases: Full width (320 pixels)
+void InsaneRebel2::renderHandler25ShipPre(byte *renderBitmap, int pitch, int width, int height) {
if (_rebelHandler != 25)
return;
- // Need at least one GRD sprite to render
- if (!_grd001Sprite && !_grd002Sprite)
+ if (!_grd001Sprite || _grd001Sprite->getNumChars() <= 0)
return;
// CRITICAL: Clip height to 180 (0xb4) to avoid drawing over status bar
- // From FUN_0041db5e line 260: _DAT_00482236 = 0xb4 during gameplay
- // The status bar occupies Y=180-200, and sprites must not render there
- const int clipHeight = 180; // Status bar boundary (0xb4)
+ const int clipHeight = 180;
int renderHeight = MIN(height, clipHeight);
- // Base position calculation based on FUN_0041db5e disassembly:
- // The original reads sprite internal offsets from the NUT data at +0x12 (X) and +0x14 (Y).
- // Position calculation from FUN_0041db5e assembly:
- //
- // GRD001 (line 206): FUN_004236e0(..., DAT_00457910, DAT_00457912, ...)
- // - Raw offsets passed directly, render function applies sprite internal offsets
- //
- // GRD002 (lines 238-247): Position uses sprite header offsets:
- // - X = DAT_00457910 + sprite_internal_x_offset (NUT header +0x12)
- // - Y = sprite_internal_y_offset (NUT header +0x14) + DAT_00457912
- //
- // Since NutRenderer doesn't expose internal offsets, we simulate them:
- // - GRD001: Draw at raw offset position (the sprite likely has built-in positioning)
- // - GRD002: Add base position to simulate internal offsets for character sprite
-
// Draw _grd001Sprite based on _grdSpriteMode (DAT_00457900)
- // Mode 1, 2, 3, 4 all potentially draw _grd001Sprite
- if (_grd001Sprite && _grd001Sprite->getNumChars() > 0) {
- bool shouldDraw = false;
-
- // Mode 1: Always draw (uncovered/shooting position)
- if (_grdSpriteMode == 1) {
- shouldDraw = true;
- }
- // Mode 2: Only draw when damaged (DAT_0045790a != 0) - covered position
- else if (_grdSpriteMode == 2 && _rebelDamageLevel != 0) {
- shouldDraw = true;
- }
- // Mode 3: Always draw (transition between covered/uncovered)
- else if (_grdSpriteMode == 3) {
- shouldDraw = true;
- }
- // Mode 4: Always draw (alternative uncovered position)
- else if (_grdSpriteMode == 4) {
- shouldDraw = true;
+ // Each mode has specific conditions from FUN_0041db5e:
+ bool shouldDraw = false;
+ bool useHalfWidth = false;
+ bool useRightHalf = false;
+
+ // Mode 1 (lines 202-210): Draw with width halving when uncovered
+ if (_grdSpriteMode == 1) {
+ shouldDraw = true;
+ useHalfWidth = (_rebelDamageLevel == 0); // Half width when uncovered
+ }
+ // Mode 2 (lines 222-224): Only draw when damaged (covered)
+ else if (_grdSpriteMode == 2 && _rebelDamageLevel != 0) {
+ shouldDraw = true;
+ }
+ // Mode 3 (lines 225-228): Always draw full width
+ else if (_grdSpriteMode == 3) {
+ shouldDraw = true;
+ }
+ // Mode 4 (lines 211-221): Draw to right half when uncovered
+ else if (_grdSpriteMode == 4) {
+ shouldDraw = true;
+ useHalfWidth = (_rebelDamageLevel == 0);
+ useRightHalf = (_rebelDamageLevel == 0);
+ }
+
+ if (shouldDraw) {
+ int spriteW = _grd001Sprite->getCharWidth(0);
+ int spriteH = _grd001Sprite->getCharHeight(0);
+ int16 spriteXOffset = _grd001Sprite->getCharXOffset(0);
+ int16 spriteYOffset = _grd001Sprite->getCharYOffset(0);
+
+ int drawX = _rebelViewOffset2X + spriteXOffset;
+ int drawY = _rebelViewOffset2Y + spriteYOffset;
+
+ // Apply width-halving logic from original assembly:
+ // When damage==0 (uncovered), the original halves DAT_00482234 (buffer width)
+ // This clips the sprite to only half the screen.
+ int renderWidth = width;
+ byte *dstBitmap = renderBitmap;
+
+ if (useHalfWidth) {
+ renderWidth = width / 2; // Clip to half width (160 pixels)
+
+ if (useRightHalf) {
+ // Mode 4: Draw to right half by offsetting the destination buffer
+ // Original: DAT_00482230 += DAT_00482234 (adds 160 to buffer start)
+ // This makes drawing appear on the right half (pixels 160-319)
+ dstBitmap = renderBitmap + (width / 2);
+ }
}
- if (shouldDraw) {
- // GRD001 is drawn at (DAT_00457910, DAT_00457912) per assembly line 206
- // FUN_004236e0 internally applies the sprite's internal offsets from NUT header
- // Use sprite 0 (primary frame)
- int spriteW = _grd001Sprite->getCharWidth(0);
- int spriteH = _grd001Sprite->getCharHeight(0);
+ renderNutSprite(dstBitmap, pitch, renderWidth, renderHeight, drawX, drawY, _grd001Sprite, 0);
- // Get sprite internal offsets from NUT header (like FUN_004236e0 does internally)
- int16 spriteXOffset = _grd001Sprite->getCharXOffset(0);
- int16 spriteYOffset = _grd001Sprite->getCharYOffset(0);
+ debug("Rebel2 Handler25 PRE: GRD001 at (%d,%d) nutOff(%d,%d) viewOff(%d,%d) size(%d,%d) mode=%d dmg=%d halfW=%d rightHalf=%d renderW=%d",
+ drawX, drawY, spriteXOffset, spriteYOffset, _rebelViewOffset2X, _rebelViewOffset2Y,
+ spriteW, spriteH, _grdSpriteMode, _rebelDamageLevel, useHalfWidth ? 1 : 0, useRightHalf ? 1 : 0, renderWidth);
+ }
+}
- // Position = view offset + sprite internal offset
- // This matches how FUN_004236e0 positions sprites using NUT header offsets
- int drawX = _rebelViewOffset2X + spriteXOffset;
- int drawY = _rebelViewOffset2Y + spriteYOffset;
+void InsaneRebel2::renderHandler25Ship(byte *renderBitmap, int pitch, int width, int height) {
+ // Handler 25 POST-rendering: Draw ONLY GRD002 (character sprite)
+ // GRD001 (wall/cockpit) is now drawn in renderHandler25ShipPre() during procPreRendering
+ // so that FOBJ enemies can draw ON TOP of it.
+ //
+ // From FUN_0041db5e disassembly (lines 230-248):
+ // GRD002 is drawn LAST (after enemies) so the character appears in front.
- renderNutSprite(renderBitmap, pitch, width, renderHeight, drawX, drawY, _grd001Sprite, 0);
+ if (_rebelHandler != 25)
+ return;
- debug("Rebel2 Handler25: GRD001 at (%d,%d) nutOffset(%d,%d) viewOffset(%d,%d) size(%d,%d) mode=%d damage=%d",
- drawX, drawY, spriteXOffset, spriteYOffset, _rebelViewOffset2X, _rebelViewOffset2Y,
- spriteW, spriteH, _grdSpriteMode, _rebelDamageLevel);
- }
- }
+ // CRITICAL: Clip height to 180 (0xb4) to avoid drawing over status bar
+ const int clipHeight = 180;
+ int renderHeight = MIN(height, clipHeight);
// _grd002Sprite (GRD002) is always drawn if it exists (from FUN_41DB5E line 230)
// The sprite index is calculated based on damage level and aiming position
@@ -4726,6 +4787,9 @@ void InsaneRebel2::renderLaserShots(byte *renderBitmap, int pitch, int width, in
case 7: // Space combat - FUN_40FADF
renderSpaceLaserShots(renderBitmap, pitch, width, height);
break;
+ case 25: // Speeder bike - FUN_0041f004
+ renderHandler25LaserShots(renderBitmap, pitch, width, height);
+ break;
default:
// No laser rendering for other handlers
break;
@@ -4831,7 +4895,8 @@ void InsaneRebel2::renderTurretLaserShots(byte *renderBitmap, int pitch, int wid
}
// Handler 8 Vehicle laser rendering (FUN_402ED0)
-// Gun position derived from ship position
+// In the original, the laser is a short muzzle flash from gun barrel toward ship center,
+// NOT a projectile traveling across the screen. The "hit" effect is handled separately.
void InsaneRebel2::renderVehicleLaserShots(byte *renderBitmap, int pitch, int width, int height) {
// No NUT check needed - uses pre-initialized _laserTexture
@@ -4841,13 +4906,17 @@ void InsaneRebel2::renderVehicleLaserShots(byte *renderBitmap, int pitch, int wi
if (_vehicleShots[i].counter <= 0)
continue;
- // Calculate sound panning
+ // Calculate sound panning from STORED target position (FUN_402ED0 lines 24-51)
+ // pan = ((2 - counter) * (targetX - 160)) / 2, clamped to [-127, 127]
int16 pan = ((2 - _vehicleShots[i].counter) * (_vehicleShots[i].targetX - _viewX - 160)) / 2;
pan = CLIP<int16>(pan, -127, 127);
// TODO: Apply panning
- // Calculate gun position from ship position (FUN_402ED0 lines 26-36)
- // Low-res formula:
+ // Calculate positions from CURRENT ship position (FUN_402ED0 lines 53-122)
+ // The original game draws the laser from gun position toward ship center,
+ // creating a short muzzle flash effect (7 pixels horizontal, 25 pixels vertical).
+ //
+ // Low-res formula (DAT_0047a808 < 2):
// shipScreenY = ((shipPosY - 0x28) >> 2) + 0x69 = ((shipPosY - 40) >> 2) + 105
// shipScreenX = ((shipPosX - 0xa0) >> 3) + 0xa0 = ((shipPosX - 160) >> 3) + 160
// gunY = ((shipPosY - 0x28) >> 2) + 0x82 = shipScreenY + 25
@@ -4857,14 +4926,14 @@ void InsaneRebel2::renderVehicleLaserShots(byte *renderBitmap, int pitch, int wi
int16 gunX = shipScreenX + 7;
int16 gunY = shipScreenY + 25;
- int16 targetX = _vehicleShots[i].targetX;
- int16 targetY = _vehicleShots[i].targetY;
int16 progress = maxDuration - _vehicleShots[i].counter;
- // Single beam from gun to target
+ // Draw beam from gun toward ship center (muzzle flash effect)
// From FUN_402ED0: widthScale=0x14(20), heightScale=8, thickness=4
+ // Parameters: gunX, gunY -> shipScreenX, shipScreenY (NOT the stored target!)
drawLaserBeam(renderBitmap, pitch, width, height,
- gunX + _viewX, gunY + _viewY, targetX, targetY,
+ gunX + _viewX, gunY + _viewY,
+ shipScreenX + _viewX, shipScreenY + _viewY,
progress, maxDuration, 20, 8, 4);
_vehicleShots[i].counter--;
@@ -4911,6 +4980,57 @@ void InsaneRebel2::renderSpaceLaserShots(byte *renderBitmap, int pitch, int widt
}
}
+// Handler 25 laser rendering (FUN_0041f004)
+// Speeder bike laser shots - draws beam from gun position to target
+void InsaneRebel2::renderHandler25LaserShots(byte *renderBitmap, int pitch, int width, int height) {
+ // FUN_0041f004 uses turret-style shot slots with view offset adjustment
+ // Only render when player is uncovered (damage == 0)
+ if (_rebelDamageLevel != 0) {
+ return; // Can't shoot while taking cover
+ }
+
+ int16 maxDuration = getShotMaxDuration();
+
+ for (int i = 0; i < 2; i++) {
+ if (_turretShots[i].counter <= 0)
+ continue;
+
+ // Calculate sound panning from target X position (FUN_004262f0)
+ // sVar1 = ((2 - counter) * (targetX - 160)) / 2, clamped to [-127, 127]
+ int16 pan = ((2 - _turretShots[i].counter) * (_turretShots[i].targetX - 160)) / 2;
+ pan = CLIP<int16>(pan, -127, 127);
+ // TODO: Apply panning to sound channel i+1
+
+ // Target position (where player clicked)
+ int16 targetX = _turretShots[i].targetX;
+ int16 targetY = _turretShots[i].targetY;
+
+ // Gun position: In FUN_0041f004, the gun is at a fixed offset based on character sprite
+ // From FUN_0041db5e lines 178-187, the gun position is:
+ // gunX = (spriteOffset + DAT_00457910) - viewOffsetX
+ // gunY = (spriteOffset + DAT_00457912) - viewOffsetY
+ //
+ // For simplicity, use a position near the bottom-center of the screen
+ // where the character's gun would be in the speeder bike cockpit.
+ // The character is typically around (160, 150) in the cockpit view.
+ int16 gunX = 160; // Center of screen
+ int16 gunY = 170; // Near bottom where character sits
+
+ int16 progress = maxDuration - _turretShots[i].counter;
+
+ // From FUN_0041f004 parameters for FUN_0040bbf6:
+ // widthScale=0xC(12), heightScale=4, thickness=6
+ drawLaserBeam(renderBitmap, pitch, width, height,
+ gunX, gunY, targetX, targetY,
+ progress, maxDuration, 12, 4, 6);
+
+ _turretShots[i].counter--;
+
+ debug("Rebel2 Handler25: Laser shot %d from (%d,%d) to (%d,%d) progress=%d/%d",
+ i, gunX, gunY, targetX, targetY, progress, maxDuration);
+ }
+}
+
void InsaneRebel2::renderCrosshair(byte *renderBitmap, int pitch, int width, int height) {
// From FUN_0040d836 (Handler 7) line 167-168: crosshair only drawn when DAT_004437c0 == 2
// Don't draw crosshair when shooting is disabled (flight-only segments)
diff --git a/engines/scumm/insane/insane_rebel.h b/engines/scumm/insane/insane_rebel.h
index cdef385b033..0a32966f5ab 100644
--- a/engines/scumm/insane/insane_rebel.h
+++ b/engines/scumm/insane/insane_rebel.h
@@ -671,12 +671,14 @@ public:
void spawnTurretShot(int x, int y); // Handler 0x26
void spawnVehicleShot(int x, int y); // Handler 8
void spawnSpaceShot(int x, int y); // Handler 7
+ void spawnHandler25Shot(int x, int y); // Handler 25 (speeder bike)
void spawnShot(int x, int y); // Dispatcher based on current handler
- // Handler-specific laser rendering (FUN_40AD63, FUN_402ED0, FUN_40FADF)
+ // Handler-specific laser rendering (FUN_40AD63, FUN_402ED0, FUN_40FADF, FUN_0041f004)
void renderTurretLaserShots(byte *renderBitmap, int pitch, int width, int height);
void renderVehicleLaserShots(byte *renderBitmap, int pitch, int width, int height);
void renderSpaceLaserShots(byte *renderBitmap, int pitch, int width, int height);
+ void renderHandler25LaserShots(byte *renderBitmap, int pitch, int width, int height);
// Get max shot duration from level table (DAT_0047e0f0 indexed by DAT_0047a7fa/DAT_0047a7f8)
int16 getShotMaxDuration();
@@ -802,7 +804,10 @@ public:
// 4: Draw _grd001Sprite with buffer offset
int16 _grdSpriteMode; // DAT_00457900
- // Render Handler 25 ship sprites (called from procPostRendering)
+ // Render Handler 25 ship sprites
+ // renderHandler25ShipPre: Draw GRD001 BEFORE FOBJ (in procPreRendering)
+ // renderHandler25Ship: Draw GRD002 and other overlays AFTER FOBJ (in procPostRendering)
+ void renderHandler25ShipPre(byte *renderBitmap, int pitch, int width, int height);
void renderHandler25Ship(byte *renderBitmap, int pitch, int width, int height);
// ======================= Handler 0x26 Turret HUD Overlays =======================
diff --git a/engines/scumm/smush/smush_player.cpp b/engines/scumm/smush/smush_player.cpp
index 9468f75aa6a..bed00ea3049 100644
--- a/engines/scumm/smush/smush_player.cpp
+++ b/engines/scumm/smush/smush_player.cpp
@@ -436,14 +436,19 @@ void SmushPlayer::handleFetch(int32 subSize, Common::SeekableReadStream &b) {
debugC(DEBUG_SMUSH, "SmushPlayer::handleFetch()");
assert(subSize >= 6);
- // For RA2 Handler 25, skip FTCH because the frame buffer only contains the
+ // For RA2 Handlers 8 and 25, skip FTCH because the frame buffer only contains the
// par4=5 base background without the overlays (par4=4, 6, 7) that were drawn
// immediately in frame 0. Restoring would erase those overlays and make
// enemies invisible since they draw on top of the erased areas.
+ //
+ // Handler 8 (Level 2 on-foot): Background is drawn in procPreRendering from
+ // _level2Background. FTCH would overwrite FOBJ-drawn enemies.
+ // Handler 25 (Level 2 speeder bike): Same issue with corridor overlays.
if (_vm->_game.id == GID_REBEL2 && _insane != nullptr) {
InsaneRebel2 *rebel2 = static_cast<InsaneRebel2 *>(_insane);
- if (rebel2->getHandler() == 25) {
- debug("SmushPlayer::handleFetch: Skipping FTCH for Handler 25 - preserving overlays");
+ int handler = rebel2->getHandler();
+ if (handler == 8 || handler == 25) {
+ debug("SmushPlayer::handleFetch: Skipping FTCH for Handler %d - preserving overlays", handler);
return;
}
}
@@ -1018,10 +1023,39 @@ void SmushPlayer::decodeFrameObject(int codec, const uint8 *src, int left, int t
_height = height;
}
}
- // Use special buffer if allocated (for oversized videos like Level 1)
- // Otherwise use virtual screen (for Level 2 small sprites)
- if (_specialBuffer != nullptr && _specialBufferSize >= _vm->_screenWidth * _vm->_screenHeight) {
+ // Use special buffer ONLY for oversized frames that need it.
+ // Small enemy sprites (like Level 2's 9x38 stormtroopers) should draw
+ // directly to the virtual screen, not to _specialBuffer.
+ // The special buffer is only needed when the CURRENT frame is larger than screen.
+ if (bufSize > _vm->_screenWidth * _vm->_screenHeight &&
+ _specialBuffer != nullptr && _specialBufferSize >= bufSize) {
_dst = _specialBuffer;
+ debug("SmushPlayer: Using _specialBuffer for oversized FOBJ %dx%d", width, height);
+ } else {
+ // For small RA2 sprites, check if we should use _specialBuffer or virtual screen.
+ //
+ // If _specialBuffer was allocated in this video (by a larger frame like Level 1's
+ // 424x260 background), small sprites should use it too so everything composites
+ // in the same buffer.
+ //
+ // If _specialBuffer is null (no large frames in this video, like Level 2), small
+ // sprites should use the virtual screen directly.
+ //
+ // This is important because release() frees _specialBuffer at the end of each video,
+ // so a new video starts with _specialBuffer = nullptr. Without this check, small
+ // sprites in Level 2 could incorrectly use a stale _specialBuffer pointer (though
+ // release() should have freed it, init() might not reset _dst if video flags are set).
+ if (_specialBuffer == nullptr) {
+ VirtScreen *vs = &_vm->_virtscr[kMainVirtScreen];
+ _dst = vs->getPixels(0, 0);
+ debug("SmushPlayer: Reset _dst to virtual screen for FOBJ %dx%d at (%d,%d) _dst=%p",
+ width, height, left, top, (void*)_dst);
+ } else {
+ // Large frame was in this video, use _specialBuffer for compositing
+ _dst = _specialBuffer;
+ debug("SmushPlayer: Using _specialBuffer for small FOBJ %dx%d (compositing with large frame)",
+ width, height);
+ }
}
} else if ((height > _vm->_screenHeight) || (width > _vm->_screenWidth))
return;
Commit: 0983d2c688de0317dc7483dd4786feff4b864756
https://github.com/scummvm/scummvm/commit/0983d2c688de0317dc7483dd4786feff4b864756
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:01+02:00
Commit Message:
SCUMM: RA2: Improve main menu
Changed paths:
engines/scumm/insane/insane_rebel.cpp
engines/scumm/scumm.cpp
diff --git a/engines/scumm/insane/insane_rebel.cpp b/engines/scumm/insane/insane_rebel.cpp
index 80bd3ca95f8..bf9fa303ca8 100644
--- a/engines/scumm/insane/insane_rebel.cpp
+++ b/engines/scumm/insane/insane_rebel.cpp
@@ -349,10 +349,12 @@ InsaneRebel2::InsaneRebel2(ScummEngine_v7 *scumm) {
// Initialize menu system
_gameState = kStateMainMenu; // Start at main menu
_menuSelection = 0; // First item selected
- // Main menu has 6 selectable items (0-5): START, OPTIONS, CONTINUE INTRO, TOP PILOTS, CREDITS, QUIT
- // Note: The coordinate formula uses param_3 = 7 (includes title) for Y position calculation
- // but _menuItemCount is the number of SELECTABLE items for bounds checking
- _menuItemCount = 6;
+ // Main menu has 7 selectable items (0-6) matching GAME.TRS indices 11-17:
+ // 0: Start Game, 1: Options, 2: Calibrate Joystick, 3: Continue Intro,
+ // 4: Show Top Pilots, 5: Show Credits, 6: Return to Launcher
+ // Note: The coordinate formula uses numItemsTotal = 8 (includes title) for Y position calculation
+ // Formula from FUN_0041f5ae: (DAT_0047a806 == 0) + 6 = 7 items for keyboard mode
+ _menuItemCount = 7;
_menuInactivityTimer = 0;
_lastMenuVariant = -1; // No previous menu video
_menuRepeatDelay = 0;
@@ -457,9 +459,20 @@ bool InsaneRebel2::notifyEvent(const Common::Event &event) {
switch (event.kbd.keycode) {
case Common::KEYCODE_ESCAPE:
- // ESC skips videos
+ // ESC handling depends on game state:
+ // - In menus: Select quit option and confirm
+ // - During gameplay/cutscenes: Skip video
if (splayer) {
- debug("Rebel2: ESC pressed - skipping video");
+ if (_menuInputActive && (_gameState == kStateMainMenu ||
+ _gameState == kStatePilotSelect ||
+ _gameState == kStateChapterSelect)) {
+ // In menu mode: Select quit option and confirm selection
+ // This emulates the assembly behavior from FUN_0041f5ae
+ _menuSelection = _menuItemCount - 1; // Select last item (quit/back)
+ debug("Rebel2: ESC pressed in menu - selecting quit (item %d)", _menuSelection);
+ } else {
+ debug("Rebel2: ESC pressed - skipping video");
+ }
_vm->_smushVideoShouldFinish = true;
return true; // Consume the event
}
@@ -3936,6 +3949,14 @@ void InsaneRebel2::procPostRendering(byte *renderBitmap, int32 codecparam, int32
}
// Handle menu input and rendering if in menu mode
+ // Debug: Log menu mode detection on first few frames
+ static int menuDebugFrames = 0;
+ if (menuDebugFrames < 5 && _gameState == kStateMainMenu) {
+ debug("Rebel2: procPostRendering frame check - introPlaying=%d menuMode=%d _menuInputActive=%d flags=0x%x",
+ introPlaying, menuMode, _menuInputActive, _player ? _player->_curVideoFlags : -1);
+ menuDebugFrames++;
+ }
+
if (menuMode) {
// The original game uses the standard Windows arrow cursor (IDC_ARROW)
// loaded via LoadCursorA(NULL, 0x7f00) in FUN_420C70.decompiled.txt
@@ -3948,14 +3969,22 @@ void InsaneRebel2::procPostRendering(byte *renderBitmap, int32 codecparam, int32
// Process menu input during each frame
int selection = processMenuInput();
- // Update inactivity timer
+ // Update inactivity timer (only increments when no input is received)
+ // Input resets timer in processMenuInput()
_menuInactivityTimer++;
- // Check for inactivity timeout (300 frames = ~10 sec at 30fps, or ~25 sec at 12fps)
+ // Check for inactivity timeout
+ // From FUN_004147b2: 300 frames of inactivity returns 0 (exit to intro/attract mode)
+ // At 12fps video rate, 300 frames = ~25 seconds of inactivity
+ // The original checks: if (local_8 > 299) return 0;
if (_menuInactivityTimer > 300) {
- debug("Rebel2: Menu inactivity timeout");
- // Reset timer but don't take action yet
+ debug("Rebel2: Menu inactivity timeout - ending video to loop");
+ // Signal video to end so menu loop plays new video
+ // This emulates the attract mode behavior where a new random
+ // menu video is selected after inactivity
_menuInactivityTimer = 0;
+ // Don't set _smushVideoShouldFinish here - let video end naturally
+ // This will cause runMainMenu to loop and play a new random video
}
// Draw menu selection overlay
@@ -5396,19 +5425,26 @@ Common::String InsaneRebel2::getRandomMenuVideo() {
int InsaneRebel2::processMenuInput() {
// Emulates FUN_0041f5ae menu input handling
- // Returns: -1 = no action, 0-6 = menu item selected
+ // Returns: -1 = no action, 0-4 = menu item selected
//
// Events are captured by notifyEvent() (EventObserver) which runs before
// ScummEngine::parseEvents() consumes them. This ensures we don't miss
// any input events even though we only process them on video frames.
+ //
+ // From FUN_0041f5ae disassembly:
+ // - Keyboard: Up/Down arrows navigate, Enter confirms
+ // - Mouse mode (DAT_0047a806 == 1): Y position maps to selection
+ // - Key codes: Up=0x148, Down=0x150, Enter=0x0d, ESC=0x1b
int result = -1;
// Menu item Y positions (low-res 320x200 mode):
- // From assembly: baseY = numItems * -5 + 0x68 = 7 * -5 + 104 = 69
- // Items at Y = 69, 79, 89, 99, 109, 119 with spacing of 10
- const int baseY = 69;
- const int itemHeight = 10;
+ // From FUN_0041f5ae: baseY = numItems * -5 + 0x68
+ // With 8 total items (title + 7 options): 8 * -5 + 104 = 64
+ // Items at Y = 64, 74, 84, 94, 104, 114, 124 with spacing of 10
+ const int numItemsTotal = 8; // Title + 7 selectable items (matching assembly)
+ const int baseY = numItemsTotal * -5 + 0x68; // = 64
+ const int itemSpacing = 10;
// Process events from the queue (populated by notifyEvent)
while (!_menuEventQueue.empty()) {
@@ -5419,26 +5455,29 @@ int InsaneRebel2::processMenuInput() {
switch (event.kbd.keycode) {
case Common::KEYCODE_UP:
- // Navigate up (wrap around)
+ // Navigate up (wrap around) - emulates key code 0x148
_menuSelection--;
if (_menuSelection < 0) {
_menuSelection = _menuItemCount - 1;
}
+ // Reset repeat delay counter (DAT_00459ce0)
+ _menuRepeatDelay = 3;
debug("Menu: Selection changed to %d (UP)", _menuSelection);
break;
case Common::KEYCODE_DOWN:
- // Navigate down (wrap around)
+ // Navigate down (wrap around) - emulates key code 0x150
_menuSelection++;
if (_menuSelection >= _menuItemCount) {
_menuSelection = 0;
}
+ _menuRepeatDelay = 3;
debug("Menu: Selection changed to %d (DOWN)", _menuSelection);
break;
case Common::KEYCODE_RETURN:
case Common::KEYCODE_KP_ENTER:
- // Confirm selection
+ // Confirm selection - emulates key code 0x0d
if (_menuSelection >= 0 && _menuSelection < _menuItemCount) {
result = _menuSelection;
debug("Menu: Item %d selected (ENTER)", _menuSelection);
@@ -5446,9 +5485,9 @@ int InsaneRebel2::processMenuInput() {
break;
case Common::KEYCODE_ESCAPE:
- // ESC - Exit/Quit (return special value)
- result = 5; // Quit option (index 5)
- debug("Menu: ESC pressed - quit");
+ // ESC - Quit (index 4 = last item) - emulates key code 0x1b
+ result = _menuItemCount - 1; // Select quit option
+ debug("Menu: ESC pressed - selecting quit (item %d)", result);
break;
default:
@@ -5458,22 +5497,22 @@ int InsaneRebel2::processMenuInput() {
case Common::EVENT_LBUTTONDOWN:
_menuInactivityTimer = 0;
-
{
// Get mouse position from the event
- int mouseX = event.mouse.x;
int mouseY = event.mouse.y;
- debug("Menu: Click detected at (%d, %d)", mouseX, mouseY);
+ debug("Menu: Left click at Y=%d", mouseY);
- // Check which item was clicked (larger hit area for better usability)
+ // Check which item was clicked
+ // From FUN_0041f5ae mouse mode: selection = (mouseY + 100 - baseY) / 10
+ // But we use a simpler direct hit-test approach
for (int i = 0; i < _menuItemCount; i++) {
- int itemY = baseY + i * itemHeight;
- // Use a larger vertical hit area (full item height)
- if (mouseY >= itemY - 4 && mouseY < itemY + 6) {
+ int itemY = baseY + i * itemSpacing;
+ // Hit area: itemY - 2 to itemY + 8 (10 pixel height)
+ if (mouseY >= itemY - 2 && mouseY < itemY + 8) {
_menuSelection = i;
result = i;
- debug("Menu: Item %d clicked at Y=%d (itemY=%d)", i, mouseY, itemY);
+ debug("Menu: Item %d clicked (itemY=%d)", i, itemY);
break;
}
}
@@ -5481,15 +5520,36 @@ int InsaneRebel2::processMenuInput() {
break;
case Common::EVENT_MOUSEMOVE:
- // Update mouse position for hover effects (optional)
+ // Update hover selection based on Y position
+ // This emulates FUN_0041f5ae mouse mode behavior (DAT_0047a806 == 1)
+ {
+ int mouseY = event.mouse.y;
+ // Calculate selection from mouse Y position
+ // From assembly: DAT_00459988 = ((mouseY + 100) - (param_3 * -5 + 0x67)) / 10
+ int newSelection = (mouseY + 100 - (numItemsTotal * -5 + 0x67)) / 10;
+
+ // Clamp to valid range
+ if (newSelection < 0) newSelection = 0;
+ if (newSelection >= _menuItemCount) newSelection = _menuItemCount - 1;
+
+ // Only update if within menu area (not too far above/below)
+ int topY = baseY - 5;
+ int bottomY = baseY + (_menuItemCount - 1) * itemSpacing + 10;
+ if (mouseY >= topY && mouseY <= bottomY) {
+ if (newSelection != _menuSelection) {
+ _menuSelection = newSelection;
+ debug(5, "Menu: Hover selection changed to %d (mouseY=%d)", _menuSelection, mouseY);
+ }
+ }
+ }
_vm->_mouse.x = event.mouse.x;
_vm->_mouse.y = event.mouse.y;
break;
case Common::EVENT_QUIT:
case Common::EVENT_RETURN_TO_LAUNCHER:
- // Handle quit request
- result = 5; // Quit option
+ // Handle quit request - select quit option
+ result = _menuItemCount - 1;
break;
default:
@@ -5497,147 +5557,331 @@ int InsaneRebel2::processMenuInput() {
}
}
+ // Decrement repeat delay counter (for smooth keyboard navigation)
+ if (_menuRepeatDelay > 0) {
+ _menuRepeatDelay--;
+ }
+
return result;
}
void InsaneRebel2::drawMenuOverlay(byte *renderBitmap, int pitch, int width, int height) {
- // Emulates FUN_0041f5ae menu text overlay rendering
+ // =====================================================================
+ // Emulates FUN_0041f5ae - Menu Text Overlay Renderer
+ // Address: 0x41F5AE
+ // =====================================================================
+ //
+ // Call chain from main menu:
+ // FUN_004147b2 (Main Menu Handler) -> FUN_0041f5ae (this function)
//
// IMPORTANT: The menu background comes from the O_MENU_X.SAN video file, NOT from MSTOVER.NUT.
// The O_MENU_X.SAN files (A through O) each contain a full 320x200 FOBJ frame in Frame 0
// which is decoded by SmushPlayer and stored in renderBitmap before this function is called.
// MSTOVER.NUT is only used in cheat mode (when DAT_0047aba4 != 0).
//
- // From FUN_4147B2: param_3 = (DAT_0047a806 == 0) + 6 = 7 for keyboard mode
- // Menu structure: 1 title + 6 selectable items = 7 total items
- static const char *menuItems[] = {
- "GAME MAIN MENU", // Title (index 0)
- "START GAME", // Item 0 (index 1)
- "OPTIONS", // Item 1 (index 2)
- "CONTINUE INTRO", // Item 2 (index 3)
- "SHOW TOP PILOTS", // Item 3 (index 4)
- "SHOW CREDITS", // Item 4 (index 5)
- "QUIT" // Item 5 (index 6)
+ // FUN_0041f5ae parameters:
+ // param_1: 0 = render mode, 1 = init mode (reset selection)
+ // param_2: Array of string pointers (menu items from TRS)
+ // param_3: Number of selectable menu items
+ // param_4: 0 = main menu (keyboard), 1 = level selection menu
+ //
+ // Menu strings loaded from GAME.TRS (keyboard mode indices 10-17):
+ // TRS index 10: "^f02Game Main Menu" -> Title (uses TITLFONT)
+ // TRS index 11: "^f01^c005Start Game" -> Item 0 (uses SMALFONT, color 5)
+ // TRS index 12: "^f01^c009Options" -> Item 1 (uses SMALFONT, color 9)
+ // TRS index 13: "^f01^c009Calibrate Joystick" -> Item 2
+ // TRS index 14: "^f01^c009Continue Intro" -> Item 3
+ // TRS index 15: "^f01^c009Show Top Pilots" -> Item 4
+ // TRS index 16: "^f01^c009Show Credits" -> Item 5
+ // TRS index 17: "^f01^c240Return to Launcher" -> Item 6 (color 240)
+
+ // Load menu strings from GAME.TRS via SmushPlayer
+ // TRS indices 10-17 correspond to main menu items (from FUN_00414073)
+ SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
+ const char *menuItems[8];
+ static const char *fallbackItems[] = {
+ "GAME MAIN MENU", "START GAME", "OPTIONS", "CALIBRATE JOYSTICK",
+ "CONTINUE INTRO", "SHOW TOP PILOTS", "SHOW CREDITS", "RETURN TO LAUNCHER"
};
- const int numItemsTotal = 7; // Title + 6 menu options (used for Y calculations)
- const int numSelectableItems = 6; // Selectable menu options (0-5)
+ for (int i = 0; i < 8; i++) {
+ const char *trsStr = splayer ? splayer->getString(10 + i) : nullptr;
+ menuItems[i] = (trsStr && trsStr[0]) ? trsStr : fallbackItems[i];
+ }
- // The O_MENU_X.SAN video frame is already in renderBitmap from SmushPlayer.
- // We only draw text and selection highlights on top.
+ const int numItemsTotal = 8; // Title + 7 menu options (matching assembly)
+ const int numSelectableItems = 7; // Selectable menu options (0-6)
- // From assembly FUN_0041f5ae (low-res mode, DAT_0047a808 < 2):
- // Center X = 160 for 320px, scale for actual width
- // Title Y = 46, Item base Y = 69, spacing = 10
- const int centerX = width / 2; // Center in actual buffer
- const int titleY = numItemsTotal * -5 + 0x51; // 46
- const int itemBaseY = numItemsTotal * -5 + 0x68; // 69
- const int itemSpacing = 10;
+ // =====================================================================
+ // Coordinate calculations from FUN_0041f5ae (lines 18-32)
+ // =====================================================================
+ // Low-res mode (DAT_0047a808 < 2):
+ // Title Y: param_3 * -5 + 0x51 = 8 * -5 + 81 = 41 (line 19)
+ // Item base Y: param_3 * -5 + 0x68 = 8 * -5 + 104 = 64 (line 28)
+ // Center X: ((DAT_0047a808 < 2) - 1 & 0xa0) + 0xa0 = 160 (line 25)
+ //
+ // High-res mode (DAT_0047a808 >= 2):
+ // Title Y: (param_3 * -5 + 0x5a) * 2 + -0x12 (line 22-23)
+ // Item Y: (param_3 * -5 + 0x5a + i * 10) * 2 + 0x1c (line 31)
+ // Center X: 320
+ const int centerX = width / 2; // 160 for 320px width
+ const int titleY = numItemsTotal * -5 + 0x51; // 41
+ const int itemBaseY = numItemsTotal * -5 + 0x68; // 64
+ const int itemSpacing = 10; // Line 28: local_c * 10
debug(5, "drawMenuOverlay: buffer %dx%d, centerX=%d", width, height, centerX);
- // Use SMALFONT.NUT for menu text rendering
- NutRenderer *font = _smush_smalfontNut;
- if (!font) {
- debug(1, "drawMenuOverlay: font is NULL!");
+ // =====================================================================
+ // Font system - Emulates linked list from FUN_00403bd0 (lines 302-348)
+ // =====================================================================
+ // Font linked list structure (DAT_00485058):
+ // offset 0x00: pointer to previous font in chain
+ // offset 0x04: pointer to next font in chain
+ // offset 0x08: pointer to font data (NUT)
+ // offset 0x0C: font index/ID
+ // offset 0x0E: chain terminator flag (0 = end of chain)
+ //
+ // Fonts loaded in low-res mode:
+ // Font 0 (^f00): TALKFONT.NUT - DAT_00485058 (root)
+ // Font 1 (^f01): SMALFONT.NUT - linked via offset 0x04
+ // Font 2 (^f02): TITLFONT.NUT - linked via offset 0x04
+ NutRenderer *fonts[3] = {
+ _smush_talkfontNut, // Font 0 - TALKFONT.NUT (default)
+ _smush_smalfontNut, // Font 1 - SMALFONT.NUT (menu items)
+ _smush_titlefontNut // Font 2 - TITLFONT.NUT (title)
+ };
+
+ // FUN_004341a0 line 35-37: Default font when param_1 == -1
+ // if (param_1 == (int *)0xffffffff) param_1 = DAT_00485058;
+ NutRenderer *defaultFont = fonts[0] ? fonts[0] : _smush_smalfontNut;
+ if (!defaultFont) {
+ debug(1, "drawMenuOverlay: no fonts available!");
return;
}
- // Get the number of characters in the font
- int numFontChars = font->getNumChars();
- debug(5, "drawMenuOverlay: font has %d chars", numFontChars);
+ // Set up clipRect for the entire rendering area
+ Common::Rect clipRect(0, 0, _vm->_screenWidth, _vm->_screenHeight);
+ int actualPitch = _vm->_screenWidth;
- // Helper: calculate string width with bounds checking
+ // =====================================================================
+ // Format code parser - Emulates FUN_00434d10 and FUN_00433da0
+ // =====================================================================
+ // Format codes parsed by the original game:
+ // 0x5e 0x5e (^^) : Literal ^ character (FUN_00434d10 line 34-36)
+ // 0x5e 0x66 (^f) : Font switch ^fNN (FUN_00434d10 lines 41-66)
+ // 0x5e 0x63 (^c) : Color code ^cNNN (FUN_00434d10 lines 70-84)
+ // 0x5e 0x6c (^l) : Newline
+ //
+ // Font switching algorithm from FUN_00433da0 (lines 86-107):
+ // sVar4 = cVar1 * 10 + (short)*pcVar5; // Parse 2-digit font index
+ // Font index 0x210 (528) = '0'*10 + '0' = font 0
+ // Font index 0x211 (529) = '0'*10 + '1' = font 1
+ // Font index 0x212 (530) = '0'*10 + '2' = font 2
+ //
+ // Color parsing from FUN_00434d10 (line 81):
+ // color = (digit1 - '0') * 100 + (digit2 - '0') * 10 + (digit3 - '0')
+ // e.g., ^c005 -> color = 5, ^c240 -> color = 240
+ auto parseFormatCode = [&](const char *&str, int &outColor) -> int {
+ if (*str != '^') return -1; // 0x5e check
+
+ const char *p = str + 1;
+ if (*p == '^') {
+ // ^^ = literal ^ (FUN_00434d10 line 34-36)
+ str = p;
+ return -1; // Process ^ as character
+ }
+ if (*p == 'f') {
+ // ^fNN = font switch (FUN_00434d10 lines 41-66)
+ // Parse 2-digit font index: sVar4 = cVar1 * 10 + (short)*pcVar5
+ p++;
+ int fontIdx = 0;
+ while (*p >= '0' && *p <= '9') {
+ fontIdx = fontIdx * 10 + (*p - '0');
+ p++;
+ }
+ str = p;
+ return (fontIdx >= 0 && fontIdx < 3) ? fontIdx : 0;
+ }
+ if (*p == 'c') {
+ // ^cNNN = color code (FUN_00434d10 lines 70-84)
+ // Parse 3-digit color: (d1-'0')*100 + (d2-'0')*10 + (d3-'0')
+ p++;
+ int color = 0;
+ while (*p >= '0' && *p <= '9') {
+ color = color * 10 + (*p - '0');
+ p++;
+ }
+ str = p;
+ outColor = color;
+ return -2; // Color changed, no font change
+ }
+ if (*p == 'l') {
+ // ^l = newline
+ str = p + 1;
+ return -2;
+ }
+ // Unknown code, skip
+ return -1;
+ };
+
+ // =====================================================================
+ // String width calculation - Emulates FUN_00433da0
+ // Address: 0x433DA0
+ // =====================================================================
+ // Iterates through string, parsing format codes and summing character widths
+ // Character width lookup (FUN_00433da0 lines 129-133):
+ // piVar2 = (int *)DAT_0046a5e4[2]; // Font data pointer
+ // sVar8 = sVar8 + *(short *)(*piVar2 + 0x16 + iVar3); // Add char width
auto getStringWidth = [&](const char *str) -> int {
int w = 0;
+ NutRenderer *curFont = defaultFont;
+ int curColor = -1;
+
while (*str) {
+ int fontChange = parseFormatCode(str, curColor);
+ if (fontChange >= 0) {
+ // Font switch via linked list traversal (FUN_00433da0 lines 94-106)
+ curFont = fonts[fontChange] ? fonts[fontChange] : defaultFont;
+ continue;
+ }
+ if (fontChange == -2) continue; // Color or newline code
+
byte c = (byte)*str++;
- if (c >= 'a' && c <= 'z') c = c - 'a' + 'A';
- if (c < numFontChars) {
- w += font->getCharWidth(c);
+ if (c >= 'a' && c <= 'z') c = c - 'a' + 'A'; // Uppercase conversion
+ if (curFont && c < curFont->getNumChars()) {
+ w += curFont->getCharWidth(c);
}
}
return w;
};
- // Debug: Check if renderBitmap has video content (non-zero pixels)
- static int debugFrameCount = 0;
- if (debugFrameCount < 5) {
- int nonZeroCount = 0;
- for (int i = 0; i < MIN(width * height, 1000); i++) {
- if (renderBitmap[i] != 0) nonZeroCount++;
- }
- debug(1, "drawMenuOverlay: frame %d, buffer sample has %d non-zero pixels in first 1000",
- debugFrameCount, nonZeroCount);
- debugFrameCount++;
- }
-
- // Set up clipRect for the entire rendering area
- // Use screen dimensions (320x200) for the clip rect, not the video frame dimensions.
- // The menu text is designed for a 320x200 screen, and the underlying renderBitmap
- // (from SmushPlayer's virtual screen buffer) is always screen-sized. The width/height
- // parameters come from _player->_width/_height which may be smaller if the video
- // frame doesn't cover the full screen, but that would incorrectly clip the menu text.
- Common::Rect clipRect(0, 0, _vm->_screenWidth, _vm->_screenHeight);
-
- // Use screen width as the actual pitch for rendering. During SMUSH playback,
- // SmushPlayer sets vs->pitch = vs->w = _screenWidth (see smush_player.cpp init()).
- // The 'pitch' parameter comes from _player->_width which may differ from the actual
- // buffer pitch if the video frame has different dimensions. Using the wrong pitch
- // would cause character rows to be written at incorrect offsets in the buffer.
- int actualPitch = _vm->_screenWidth;
-
+ // =====================================================================
+ // String rendering - Emulates FUN_00434d10
+ // Address: 0x434D10
+ // =====================================================================
+ // Renders string character-by-character with format code support
+ // Calls FUN_004236e0 for each character glyph (FUN_00434d10 lines 96-97)
+ //
+ // Color handling analysis from assembly:
+ // FUN_00434d10 parses ^cNNN and passes color to FUN_004236e0 as param_8
+ // FUN_004236e0 passes it to FUN_0042cba0, which passes to codec functions
+ // In the codecs (FUN_0042cc50, FUN_0042ce90), the color is stored in
+ // _DAT_00483fd0 and used as a CLIPPING BOUNDARY, not for pixel coloring!
+ //
+ // The actual pixel colors come from the NUT font's embedded palette data.
+ // Each font (TALKFONT, SMALFONT, TITLFONT) has its own color scheme.
+ // The ^cNNN codes in TRS strings are metadata, not used for NUT font
+ // pixel rendering - the codecs write pixel data directly from the font.
+ //
+ // Therefore: Always use hardcodedColors=true to render fonts with their
+ // embedded palette colors, which matches the original behavior.
auto drawString = [&](const char *str, int x, int y) {
+ NutRenderer *curFont = defaultFont;
+ int curColor = -1; // Parsed but not used for NUT font pixel rendering
+
while (*str) {
+ int fontChange = parseFormatCode(str, curColor);
+ if (fontChange >= 0) {
+ // Font switch (FUN_00434d10 lines 41-66)
+ curFont = fonts[fontChange] ? fonts[fontChange] : defaultFont;
+ continue;
+ }
+ if (fontChange == -2) continue; // Color code parsed (not used for NUT)
+
byte c = (byte)*str++;
if (c >= 'a' && c <= 'z') c = c - 'a' + 'A';
- // Skip characters outside font range
- if (c >= numFontChars) {
- debug(5, "drawMenuOverlay: char %d out of range (max %d)", c, numFontChars);
- continue;
- }
+ if (!curFont) continue;
+ int numChars = curFont->getNumChars();
+ if (c >= numChars) continue;
- int charW = font->getCharWidth(c);
+ int charW = curFont->getCharWidth(c);
- // Use NutRenderer's drawCharV7 which properly handles character rendering
- // col=-1 means use original font colors, hardcodedColors=true, smushColorMode=true
+ // FUN_004236e0 -> FUN_0042cba0 -> codec: Render character glyph
+ // NUT fonts contain embedded palette indices in their pixel data.
+ // The codec writes these values directly without color transformation.
+ // Use hardcodedColors=true and smushColorMode=true for proper rendering.
if (x >= 0 && y >= 0 && charW > 0) {
- font->drawCharV7(renderBitmap, clipRect, x, y, actualPitch, -1,
- kStyleAlignLeft, c, true, true);
+ curFont->drawCharV7(renderBitmap, clipRect, x, y, actualPitch, -1,
+ kStyleAlignLeft, c, true, true);
}
x += charW;
}
};
- // Draw title centered at Y = 46
+ // =====================================================================
+ // Draw title - FUN_0041f5ae lines 24-25
+ // =====================================================================
+ // FUN_004341a0((int *)0xffffffff, DAT_0047a7d0, DAT_0047a7d8,
+ // ((DAT_0047a808 < 2) - 1 & 0xa0) + 0xa0, sVar2, 1, 0, 1, (char *)*param_2);
+ // X position: centerX (160 for low-res)
+ // Y position: sVar2 = param_3 * -5 + 0x51
{
int titleWidth = getStringWidth(menuItems[0]);
- int titleX = centerX - titleWidth / 2;
+ int titleX = centerX - titleWidth / 2; // Center alignment (param_8 & 1)
drawString(menuItems[0], titleX, titleY);
}
- // Draw menu items starting at Y = 69
+ // =====================================================================
+ // Draw menu items - FUN_0041f5ae lines 26-48
+ // =====================================================================
+ // for (local_c = 0; local_c < param_3; local_c = local_c + 1) {
+ // FUN_004341a0(..., (char *)param_2[local_c + 1]);
+ // if (DAT_00459988 == local_c) { // Selected item
+ // sVar2 = FUN_00433da0(...); // Get string width
+ // FUN_004292d0(...); // Draw selection box
+ // }
+ // }
for (int i = 0; i < numSelectableItems; i++) {
+ // Item Y calculation (FUN_0041f5ae line 28):
+ // sVar2 = param_3 * -5 + local_c * 10 + 0x68
int itemY = itemBaseY + i * itemSpacing;
const char *text = menuItems[i + 1];
int textWidth = getStringWidth(text);
- int textX = centerX - textWidth / 2;
+ int textX = centerX - textWidth / 2; // Center alignment
drawString(text, textX, itemY);
- // Draw selection highlight box around selected item
+ // =====================================================================
+ // Selection highlight box - FUN_0041f5ae lines 36-47
+ // Calls FUN_004292d0 (Rectangle Drawer) at 0x4292D0
+ // =====================================================================
+ // if (DAT_00459988 == local_c) {
+ // sVar2 = FUN_00433da0((int *)0xffffffff, (byte *)param_2[local_c + 1]);
+ // sVar2 = sVar2 + ((DAT_0047a808 < 2) - 1 & 6) + 6; // Width padding
+ // FUN_004292d0(DAT_0047a7d0, DAT_0047a7d8,
+ // (((DAT_0047a808 < 2) - 1 & 0xa0) + 0xa0) - sVar2 / 2, // X
+ // sVar1, // Y = param_3 * -5 + local_c * 10 + 0x67
+ // sVar2, // Width
+ // ((DAT_0047a808 < 2) - 1 & 10) + 10, // Height = 10
+ // (-((DAT_0047a7e4 & 1) == 0) & 8U) - 0x10); // Color
+ // }
if (i == _menuSelection) {
- int bracketWidth = textWidth + 12;
+ // Width: sVar2 + ((DAT_0047a808 < 2) - 1 & 6) + 6
+ // For low-res: textWidth + (0 & 6) + 6 = textWidth + 6
+ int bracketWidth = textWidth + 6;
+
+ // Height: ((DAT_0047a808 < 2) - 1 & 10) + 10
+ // For low-res: (0 & 10) + 10 = 10
int bracketHeight = 10;
- byte highlightColor = 255; // White/bright color for visibility
+ // Flash color (FUN_0041f5ae line 47):
+ // (-((DAT_0047a7e4 & 1) == 0) & 8U) - 0x10
+ // When mouse button NOT pressed (bit 0 == 0): (-1 & 8) - 16 = 8 - 16 = -8 = 248
+ // When mouse button pressed (bit 0 == 1): (0 & 8) - 16 = -16 = 240
+ static int frameCounter = 0;
+ frameCounter++;
+ byte highlightColor = ((frameCounter / 8) & 1) ? 248 : 240;
+
+ // Box position (FUN_0041f5ae lines 40, 45-46):
+ // X: centerX - sVar2 / 2
+ // Y: sVar1 = param_3 * -5 + local_c * 10 + 0x67 (one pixel above text)
int leftX = centerX - bracketWidth / 2;
int rightX = centerX + bracketWidth / 2;
- int topY = itemY - 1;
+ int topY = itemY - 1; // 0x67 vs 0x68 = 1 pixel difference
int bottomY = itemY + bracketHeight - 1;
- // Clamp to screen bounds (use screen dimensions, not video frame dimensions)
+ // Clamp to screen bounds
int screenW = _vm->_screenWidth;
int screenH = _vm->_screenHeight;
if (leftX < 0) leftX = 0;
@@ -5645,18 +5889,25 @@ void InsaneRebel2::drawMenuOverlay(byte *renderBitmap, int pitch, int width, int
if (topY < 0) topY = 0;
if (bottomY >= screenH) bottomY = screenH - 1;
- // Draw selection rectangle using actualPitch (screen width)
+ // =====================================================================
+ // FUN_004292d0 - Rectangle Drawer (Address: 0x4292D0)
+ // =====================================================================
+ // Draws rectangle border as 4 lines:
+ // FUN_004290d0(param_1, param_2, x, y, width, color); // Top horizontal
+ // FUN_004291d0(param_1, param_2, x, y, height, color); // Left vertical
+ // FUN_004291d0(param_1, param_2, x+w-1, y, height, color); // Right vertical
+ // FUN_004290d0(param_1, param_2, x, y+h-1, width, color); // Bottom horizontal
for (int x = leftX; x <= rightX && x < screenW; x++) {
if (topY >= 0 && topY < screenH)
- renderBitmap[topY * actualPitch + x] = highlightColor;
+ renderBitmap[topY * actualPitch + x] = highlightColor; // Top line
if (bottomY >= 0 && bottomY < screenH)
- renderBitmap[bottomY * actualPitch + x] = highlightColor;
+ renderBitmap[bottomY * actualPitch + x] = highlightColor; // Bottom line
}
for (int py = topY; py <= bottomY && py < screenH; py++) {
if (leftX >= 0 && leftX < screenW)
- renderBitmap[py * actualPitch + leftX] = highlightColor;
+ renderBitmap[py * actualPitch + leftX] = highlightColor; // Left line
if (rightX >= 0 && rightX < screenW)
- renderBitmap[py * actualPitch + rightX] = highlightColor;
+ renderBitmap[py * actualPitch + rightX] = highlightColor; // Right line
}
}
}
@@ -5880,55 +6131,62 @@ int InsaneRebel2::runMainMenu() {
debug("Rebel2: Menu video ended with selection=%d", _menuSelection);
// Process the menu result based on current selection
- // Menu items (from FUN_004147B2 disassembly):
- // case 0: return 2 (New Game)
- // case 1: return 4 (Continue)
- // case 2: Options menu (stays in loop)
- // case 3: return 0 (Exit)
- // case 4: Unknown function
- // case 5: Credits (play O_CREDIT.SAN, then return 1)
- // case 6: Quit (stop video, exit)
+ // Menu items matching GAME.TRS indices 11-17 (FUN_004147B2):
+ // case 0 (TRS 11): Start Game -> pilot selection, returns 2
+ // case 1 (TRS 12): Options -> FUN_00416787 options screen
+ // case 2 (TRS 13): Calibrate Joystick -> FUN_00425820
+ // case 3 (TRS 14): Continue Intro -> replay O_OPEN videos
+ // case 4 (TRS 15): Show Top Pilots -> FUN_00420116(-1)
+ // case 5 (TRS 16): Show Credits -> play O_CREDIT.SAN, returns 1
+ // case 6 (TRS 17): Return to Launcher -> quit, returns 0
switch (_menuSelection) {
- case 0: // Start Game -> Pilot Selection
+ case 0: // Start Game -> go to pilot selection
debug("Rebel2: Start Game selected - going to pilot selection");
_gameState = kStatePilotSelect;
_menuInputActive = false;
- return kMenuContinue; // Go to pilot selection
+ return kMenuNewGame; // Return 2 (kMenuNewGame)
- case 1: // Continue (same as Start Game for now)
- debug("Rebel2: Continue selected - going to pilot selection");
- _gameState = kStatePilotSelect;
- _menuInputActive = false;
- return kMenuContinue;
-
- case 2: // Options
+ case 1: // Options -> show options menu
debug("Rebel2: Options selected");
- // TODO: Show options menu (FUN_00406ed2)
- // For now, just continue menu loop
+ // TODO: Implement options menu (FUN_004167a6)
+ // Options: Music, Sound, Voices, Auto Control, Indicators,
+ // Arrows, Difficulty (0-5), Music Volume, SFX Volume
break;
- case 3: // Exit (back to title/intro)
- debug("Rebel2: Exit selected");
- // Return to menu loop
+ case 2: // Calibrate Joystick
+ debug("Rebel2: Calibrate Joystick selected");
+ // TODO: Implement joystick calibration (FUN_00425820)
+ // Plays O_CALIB.SAN with joystick calibration prompts
break;
- case 4: // Unknown function (FUN_00420116)
- debug("Rebel2: Unknown menu item 4 selected");
+ case 3: // Continue Intro -> replay intro videos
+ debug("Rebel2: Continue Intro selected - replaying intro");
+ // Play intro sequence again (O_OPEN_A/B)
+ splayer->setCurVideoFlags(0x20);
+ splayer->play("OPEN/O_OPEN_A.SAN", 12);
+ if (!_vm->shouldQuit()) {
+ splayer->play("OPEN/O_OPEN_B.SAN", 12);
+ }
+ break;
+
+ case 4: // Show Top Pilots -> high score display
+ debug("Rebel2: Show Top Pilots selected");
+ // TODO: Implement high score display (FUN_00420116(-1))
break;
- case 5: // Credits
- debug("Rebel2: Credits selected");
+ case 5: // Show Credits -> play credits video
+ debug("Rebel2: Show Credits selected - playing O_CREDIT.SAN");
_gameState = kStateCredits;
splayer->setCurVideoFlags(0x20);
splayer->play("OPEN/O_CREDIT.SAN", 12);
_gameState = kStateMainMenu;
- // After credits, return to menu
+ // Returns 1 in original -> stays at stage 1 (main menu)
break;
- case 6: // Quit
- debug("Rebel2: Quit selected");
+ case 6: // Return to Launcher -> quit game
+ debug("Rebel2: Return to Launcher selected");
_menuInputActive = false;
- return 0;
+ return 0; // Return 0 to exit
default:
debug("Rebel2: Unknown menu selection %d", _menuSelection);
diff --git a/engines/scumm/scumm.cpp b/engines/scumm/scumm.cpp
index b87601c85b5..32e0de1006d 100644
--- a/engines/scumm/scumm.cpp
+++ b/engines/scumm/scumm.cpp
@@ -2696,8 +2696,8 @@ Common::Error ScummEngine::go() {
break;
}
- if (menuResult == InsaneRebel2::kMenuContinue) {
- // Continue: Show pilot selection screen (FUN_00414A41)
+ if (menuResult == InsaneRebel2::kMenuNewGame || menuResult == InsaneRebel2::kMenuContinue) {
+ // Start Game or Continue: Show pilot selection screen (FUN_00414A41)
int pilotResult = rebel->runLevelSelect();
if (pilotResult == InsaneRebel2::kLevelSelectQuit || shouldQuit()) {
Commit: 4dd1e28b95fd6385befcd57b5da5b2b236b8706b
https://github.com/scummvm/scummvm/commit/4dd1e28b95fd6385befcd57b5da5b2b236b8706b
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:01+02:00
Commit Message:
SCUMM: RA2: Play intro correctly from main menu
Changed paths:
engines/scumm/insane/insane_rebel.cpp
diff --git a/engines/scumm/insane/insane_rebel.cpp b/engines/scumm/insane/insane_rebel.cpp
index bf9fa303ca8..95396a5c5bc 100644
--- a/engines/scumm/insane/insane_rebel.cpp
+++ b/engines/scumm/insane/insane_rebel.cpp
@@ -6161,12 +6161,19 @@ int InsaneRebel2::runMainMenu() {
case 3: // Continue Intro -> replay intro videos
debug("Rebel2: Continue Intro selected - replaying intro");
+ // Temporarily switch to intro state to disable menu overlay
+ // This emulates FUN_004142BD case 0 behavior
+ _gameState = kStateIntro;
+ _menuInputActive = false;
// Play intro sequence again (O_OPEN_A/B)
splayer->setCurVideoFlags(0x20);
splayer->play("OPEN/O_OPEN_A.SAN", 12);
if (!_vm->shouldQuit()) {
splayer->play("OPEN/O_OPEN_B.SAN", 12);
}
+ // Restore menu state
+ _gameState = kStateMainMenu;
+ _menuInputActive = true;
break;
case 4: // Show Top Pilots -> high score display
@@ -6177,9 +6184,11 @@ int InsaneRebel2::runMainMenu() {
case 5: // Show Credits -> play credits video
debug("Rebel2: Show Credits selected - playing O_CREDIT.SAN");
_gameState = kStateCredits;
+ _menuInputActive = false;
splayer->setCurVideoFlags(0x20);
splayer->play("OPEN/O_CREDIT.SAN", 12);
_gameState = kStateMainMenu;
+ _menuInputActive = true;
// Returns 1 in original -> stays at stage 1 (main menu)
break;
Commit: 84cf81e8e64f3c4254219ea03229701bbf0dcbf4
https://github.com/scummvm/scummvm/commit/84cf81e8e64f3c4254219ea03229701bbf0dcbf4
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:01+02:00
Commit Message:
SCUMM: RA2: Load menu strings from TRS files
Changed paths:
engines/scumm/insane/insane_rebel.cpp
diff --git a/engines/scumm/insane/insane_rebel.cpp b/engines/scumm/insane/insane_rebel.cpp
index 95396a5c5bc..ccd5c017563 100644
--- a/engines/scumm/insane/insane_rebel.cpp
+++ b/engines/scumm/insane/insane_rebel.cpp
@@ -3949,14 +3949,6 @@ void InsaneRebel2::procPostRendering(byte *renderBitmap, int32 codecparam, int32
}
// Handle menu input and rendering if in menu mode
- // Debug: Log menu mode detection on first few frames
- static int menuDebugFrames = 0;
- if (menuDebugFrames < 5 && _gameState == kStateMainMenu) {
- debug("Rebel2: procPostRendering frame check - introPlaying=%d menuMode=%d _menuInputActive=%d flags=0x%x",
- introPlaying, menuMode, _menuInputActive, _player ? _player->_curVideoFlags : -1);
- menuDebugFrames++;
- }
-
if (menuMode) {
// The original game uses the standard Windows arrow cursor (IDC_ARROW)
// loaded via LoadCursorA(NULL, 0x7f00) in FUN_420C70.decompiled.txt
@@ -5597,16 +5589,21 @@ void InsaneRebel2::drawMenuOverlay(byte *renderBitmap, int pitch, int width, int
// Load menu strings from GAME.TRS via SmushPlayer
// TRS indices 10-17 correspond to main menu items (from FUN_00414073)
+ // No fallback strings - all text must come from TRS for localization support
SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
const char *menuItems[8];
- static const char *fallbackItems[] = {
- "GAME MAIN MENU", "START GAME", "OPTIONS", "CALIBRATE JOYSTICK",
- "CONTINUE INTRO", "SHOW TOP PILOTS", "SHOW CREDITS", "RETURN TO LAUNCHER"
- };
+
+ if (!splayer) {
+ debug(1, "drawMenuOverlay: SmushPlayer not available for TRS strings!");
+ return;
+ }
for (int i = 0; i < 8; i++) {
- const char *trsStr = splayer ? splayer->getString(10 + i) : nullptr;
- menuItems[i] = (trsStr && trsStr[0]) ? trsStr : fallbackItems[i];
+ menuItems[i] = splayer->getString(10 + i);
+ if (!menuItems[i] || !menuItems[i][0]) {
+ debug(1, "drawMenuOverlay: TRS string %d not found!", 10 + i);
+ menuItems[i] = ""; // Empty string to avoid crashes
+ }
}
const int numItemsTotal = 8; // Title + 7 menu options (matching assembly)
@@ -6016,8 +6013,14 @@ void InsaneRebel2::showPauseOverlay() {
}
// Draw "PAUSED" text centered
- // Use hardcoded "PAUSED" string (TRS string 0x79 is "Quit Game" in RA2)
- const char *pauseText = "PAUSED";
+ // Try to load from TRS - the exact index may vary by language version
+ // TRS index 80 (0x50) is likely "PAUSED" or equivalent (from DAT_004573f8)
+ // Note: splayer is already defined at the start of this function
+ const char *pauseText = splayer ? splayer->getString(80) : nullptr;
+ if (!pauseText || !pauseText[0]) {
+ // Fallback only if TRS string not available
+ pauseText = "PAUSED";
+ }
// Draw text using SmushFont if available
if (_menuFont) {
@@ -6742,25 +6745,37 @@ void InsaneRebel2::drawChapterSelectOverlay(byte *renderBitmap, int pitch, int w
// - Three preview boxes on right side
// - Status bar at bottom: "PILOTS: X SCORE: Y RANK:"
- // Chapter names from original game (DAT_00457820)
- // Format: "CHAPTER X - NAME" for unlocked, "CHAPTER X -" for locked
- static const char *chapterNames[] = {
- "THE DREIGHTON TRIANGLE", // Chapter 1
- "ASTEROID PURSUIT", // Chapter 2
- "ABOARD THE TERROR", // Chapter 3
- "TIE FIGHTER ATTACK", // Chapter 4
- "DREIGHTON NEBULA", // Chapter 5
- "CORELLIA", // Chapter 6
- "SPEEDER PURSUIT", // Chapter 7
- "CANYON CHASE", // Chapter 8
- "DEATH STAR APPROACH", // Chapter 9
- "THE ASTEROID FIELD", // Chapter 10
- "INSIDE THE DEATH STAR", // Chapter 11
- "TRENCH RUN", // Chapter 12
- "THE MAIN REACTOR", // Chapter 13
- "ESCAPE FROM YAVIN", // Chapter 14
- "FINALE", // Chapter 15 is actually FINALE
- };
+ // Chapter names from GAME.TRS (DAT_00457820 and DAT_00457868)
+ // From FUN_00414073:
+ // TRS indices 0x28-0x39 (40-57): 18 unlocked chapter strings -> DAT_00457820
+ // TRS indices 0x3c-0x4d (60-77): 18 locked chapter strings -> DAT_00457868
+ // These contain complete strings like "CHAPTER 1 - THE DREIGHTON TRIANGLE"
+ SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
+ const char *chapterNamesUnlocked[18];
+ const char *chapterNamesLocked[18];
+
+ if (!splayer) {
+ debug(1, "drawChapterSelectOverlay: SmushPlayer not available for TRS strings!");
+ return;
+ }
+
+ // Load unlocked chapter strings (TRS indices 40-57)
+ for (int i = 0; i < 18; i++) {
+ chapterNamesUnlocked[i] = splayer->getString(40 + i);
+ if (!chapterNamesUnlocked[i] || !chapterNamesUnlocked[i][0]) {
+ debug(1, "drawChapterSelectOverlay: TRS unlocked string %d not found!", 40 + i);
+ chapterNamesUnlocked[i] = "";
+ }
+ }
+
+ // Load locked chapter strings (TRS indices 60-77)
+ for (int i = 0; i < 18; i++) {
+ chapterNamesLocked[i] = splayer->getString(60 + i);
+ if (!chapterNamesLocked[i] || !chapterNamesLocked[i][0]) {
+ debug(1, "drawChapterSelectOverlay: TRS locked string %d not found!", 60 + i);
+ chapterNamesLocked[i] = "";
+ }
+ }
// Frame counter for flashing selection box
static int frameCounter = 0;
@@ -6811,10 +6826,11 @@ void InsaneRebel2::drawChapterSelectOverlay(byte *renderBitmap, int pitch, int w
int itemY = itemBaseY + i * itemSpacing;
bool isSelected = (i == _chapterSelection);
- // Build chapter string: "CHAPTER X - NAME" or "CHAPTER X -"
- char chapterStr[64];
- snprintf(chapterStr, sizeof(chapterStr), "CHAPTER %d - %s", i + 1,
- _chapterUnlocked[i] ? chapterNames[i] : "");
+ // Get chapter string from TRS (already contains "CHAPTER X - NAME" or "CHAPTER X -")
+ const char *chapterStr = _chapterUnlocked[i] ? chapterNamesUnlocked[i] : chapterNamesLocked[i];
+ if (!chapterStr || !chapterStr[0]) {
+ continue; // Skip if no string available
+ }
// Calculate text width for selection box
int textWidth = 0;
@@ -6867,9 +6883,13 @@ void InsaneRebel2::drawChapterSelectOverlay(byte *renderBitmap, int pitch, int w
// Draw FINALE (chapter 16 = index 15)
// Position: Y = 19 + 15*10 = 169
+ // FINALE uses TRS index 55 (unlocked) or 75 (locked) - index 15 in the arrays
int finaleY = itemBaseY + 15 * itemSpacing;
bool finaleSelected = (_chapterSelection == 15);
- const char *finaleStr = "FINALE -";
+ const char *finaleStr = _chapterUnlocked[15] ? chapterNamesUnlocked[15] : chapterNamesLocked[15];
+ if (!finaleStr || !finaleStr[0]) {
+ finaleStr = ""; // Fallback to empty if TRS not available
+ }
int finaleWidth = 0;
for (const char *c = finaleStr; *c; c++) {
@@ -6911,9 +6931,15 @@ void InsaneRebel2::drawChapterSelectOverlay(byte *renderBitmap, int pitch, int w
// Draw "RETURN TO PILOTS" (index 16)
// Position: Y = 19 + 16*10 = 179
+ // This string comes from TRS - likely in DAT_00457400 (indices 89-109)
+ // Using TRS index 89 for "RETURN TO PILOTS" or equivalent
int returnY = itemBaseY + 16 * itemSpacing;
bool returnSelected = (_chapterSelection == 16);
- const char *returnStr = "RETURN TO PILOTS";
+ const char *returnStr = splayer->getString(89);
+ if (!returnStr || !returnStr[0]) {
+ debug(1, "drawChapterSelectOverlay: TRS string 89 (RETURN TO PILOTS) not found!");
+ returnStr = "";
+ }
int returnWidth = 0;
for (const char *c = returnStr; *c; c++) {
@@ -6970,11 +6996,22 @@ void InsaneRebel2::drawChapterSelectOverlay(byte *renderBitmap, int pitch, int w
// From FUN_00415CF8 lines 101-103 (unlocked chapter score display):
// X = 0x19 + 0x19 = 50 (but we use 23 to align with menu items)
// Y = 0xbe = 190
+ // TODO: The status bar format should use TRS strings from DAT_00457400 (indices 89-109)
+ // for proper localization. Currently using minimal display.
int statusY = 190;
int statusX = 23; // Align with menu items
+ // Build status string using TRS format strings where available
+ // TRS indices 89-109 contain format strings for status display
char statusStr[64];
- snprintf(statusStr, sizeof(statusStr), "PILOTS: %d SCORE: %d RANK:", 4, _playerScore);
+ const char *pilotsLabel = splayer->getString(90); // "PILOTS:" or equivalent
+ const char *scoreLabel = splayer->getString(91); // "SCORE:" or equivalent
+ if (pilotsLabel && pilotsLabel[0] && scoreLabel && scoreLabel[0]) {
+ snprintf(statusStr, sizeof(statusStr), "%s %d %s %d", pilotsLabel, 4, scoreLabel, _playerScore);
+ } else {
+ // Minimal fallback if TRS strings not available
+ snprintf(statusStr, sizeof(statusStr), "%d %d", 4, _playerScore);
+ }
curX = statusX;
for (const char *c = statusStr; *c; c++) {
@@ -7196,24 +7233,32 @@ void InsaneRebel2::drawLevelSelectOverlay(byte *renderBitmap, int pitch, int wid
frameCounter++;
// Pilot selection menu items - matches original structure from FUN_00414A41:
- // Items 0-5: Pilot save slots (PILOT 1 through PILOT 6)
- // Item 6: NEW PILOT
+ // Load strings from GAME.TRS via SmushPlayer (indices from FUN_00414073)
+ // TRS indices 0x14-0x23 (20-35) are stored at DAT_004573b8
+ // Items 0-5: Pilot save slots (would come from save data, using TRS placeholder)
+ // Item 6: NEW PILOT (TRS index 20+6=26 or similar)
// Item 7: DELETE PILOT
// Item 8: COPY PILOT
// Item 9: MAIN MENU
- static const char *pilotItems[] = {
- "SELECT PILOT", // Title (index 0) - not selectable
- "PILOT 1", // Pilot slot 1 (index 1) - selectable
- "PILOT 2", // Pilot slot 2 (index 2) - selectable
- "PILOT 3", // Pilot slot 3 (index 3) - selectable
- "PILOT 4", // Pilot slot 4 (index 4) - selectable
- "PILOT 5", // Pilot slot 5 (index 5) - selectable
- "PILOT 6", // Pilot slot 6 (index 6) - selectable
- "NEW PILOT", // Create new pilot (index 7) - selectable
- "DELETE PILOT", // Delete pilot (index 8) - selectable
- "COPY PILOT", // Copy pilot (index 9) - selectable
- "MAIN MENU" // Back to menu (index 10) - selectable
- };
+ SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
+ const char *pilotItems[11];
+
+ if (!splayer) {
+ debug(1, "drawLevelSelectOverlay: SmushPlayer not available for TRS strings!");
+ return;
+ }
+
+ // TRS index mapping from FUN_00414073:
+ // DAT_004573b8[0-15] = TRS indices 20-35
+ // Title: TRS 20, Pilot slots use save data or TRS template, Options: TRS 26-29 or similar
+ // Load from TRS indices 20-30 for the menu structure
+ for (int i = 0; i < 11; i++) {
+ pilotItems[i] = splayer->getString(20 + i);
+ if (!pilotItems[i] || !pilotItems[i][0]) {
+ debug(1, "drawLevelSelectOverlay: TRS string %d not found!", 20 + i);
+ pilotItems[i] = "";
+ }
+ }
const int numItemsTotal = 11; // Title + 10 selectable items
const int numSelectableItems = 10;
Commit: 79010d66044dbf0ca9dabc216117c3bcaf24e9d8
https://github.com/scummvm/scummvm/commit/79010d66044dbf0ca9dabc216117c3bcaf24e9d8
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:01+02:00
Commit Message:
SCUMM: RA2: Support colored fonts
Changed paths:
engines/scumm/insane/insane_rebel.cpp
engines/scumm/nut_renderer.cpp
diff --git a/engines/scumm/insane/insane_rebel.cpp b/engines/scumm/insane/insane_rebel.cpp
index ccd5c017563..3d16c682d2c 100644
--- a/engines/scumm/insane/insane_rebel.cpp
+++ b/engines/scumm/insane/insane_rebel.cpp
@@ -5760,21 +5760,22 @@ void InsaneRebel2::drawMenuOverlay(byte *renderBitmap, int pitch, int width, int
// Calls FUN_004236e0 for each character glyph (FUN_00434d10 lines 96-97)
//
// Color handling analysis from assembly:
- // FUN_00434d10 parses ^cNNN and passes color to FUN_004236e0 as param_8
- // FUN_004236e0 passes it to FUN_0042cba0, which passes to codec functions
- // In the codecs (FUN_0042cc50, FUN_0042ce90), the color is stored in
- // _DAT_00483fd0 and used as a CLIPPING BOUNDARY, not for pixel coloring!
+ // Color handling for NUT fonts (from assembly analysis):
//
- // The actual pixel colors come from the NUT font's embedded palette data.
- // Each font (TALKFONT, SMALFONT, TITLFONT) has its own color scheme.
- // The ^cNNN codes in TRS strings are metadata, not used for NUT font
- // pixel rendering - the codecs write pixel data directly from the font.
+ // For codec 44 (used by RA2 fonts): The ^cNNN color IS used for pixel coloring!
+ // - Font pixels with value 1 are replaced with the ^cNNN color
+ // - Font pixels with value 255 are replaced with 0
+ // - Other values are written directly
//
- // Therefore: Always use hardcodedColors=true to render fonts with their
- // embedded palette colors, which matches the original behavior.
+ // FUN_00434d10 parses ^cNNN and passes the color to FUN_004236e0 as param_8,
+ // which passes it through to the codec for byte 1 substitution.
+ //
+ // In drawCharV7's default mode (hardcodedColors=false, smushColorMode=false):
+ // dst[i] = (value == 1) ? color : value;
+ // This implements the codec 44 color substitution.
auto drawString = [&](const char *str, int x, int y) {
NutRenderer *curFont = defaultFont;
- int curColor = -1; // Parsed but not used for NUT font pixel rendering
+ int curColor = 1; // Default color if no ^cNNN specified (white/foreground)
while (*str) {
int fontChange = parseFormatCode(str, curColor);
@@ -5783,7 +5784,7 @@ void InsaneRebel2::drawMenuOverlay(byte *renderBitmap, int pitch, int width, int
curFont = fonts[fontChange] ? fonts[fontChange] : defaultFont;
continue;
}
- if (fontChange == -2) continue; // Color code parsed (not used for NUT)
+ if (fontChange == -2) continue; // Color code parsed, curColor updated
byte c = (byte)*str++;
if (c >= 'a' && c <= 'z') c = c - 'a' + 'A';
@@ -5795,12 +5796,10 @@ void InsaneRebel2::drawMenuOverlay(byte *renderBitmap, int pitch, int width, int
int charW = curFont->getCharWidth(c);
// FUN_004236e0 -> FUN_0042cba0 -> codec: Render character glyph
- // NUT fonts contain embedded palette indices in their pixel data.
- // The codec writes these values directly without color transformation.
- // Use hardcodedColors=true and smushColorMode=true for proper rendering.
+ // Use default mode for codec 44 color substitution: byte 1 -> curColor
if (x >= 0 && y >= 0 && charW > 0) {
- curFont->drawCharV7(renderBitmap, clipRect, x, y, actualPitch, -1,
- kStyleAlignLeft, c, true, true);
+ curFont->drawCharV7(renderBitmap, clipRect, x, y, actualPitch, curColor,
+ kStyleAlignLeft, c, false, false);
}
x += charW;
}
diff --git a/engines/scumm/nut_renderer.cpp b/engines/scumm/nut_renderer.cpp
index 3448672be24..583a2019d8f 100644
--- a/engines/scumm/nut_renderer.cpp
+++ b/engines/scumm/nut_renderer.cpp
@@ -439,7 +439,20 @@ int NutRenderer::drawCharV7(byte *buffer, Common::Rect &clipRect, int x, int y,
}
}
} else {
- if (smushColorMode) {
+ if (hardcodedColors) {
+ // Direct pixel write for NUT fonts with embedded palette colors (e.g., RA2 menu fonts)
+ // This mirrors the version 7 hardcodedColors behavior
+ for (int j = minY; j < height; j++) {
+ for (int i = minX; i < width; i++) {
+ int8 value = *src++;
+ if (value != _chars[chr].transparency)
+ dst[i] = value;
+ }
+ src += clipWdth;
+ dst += pitch;
+ }
+ } else if (smushColorMode) {
+ // SMUSH subtitle color mode: remap specific values
for (int j = minY; j < height; j++) {
for (int i = minX; i < width; i++) {
int8 value = *src++;
Commit: 2bc1457374b257b43da66e35b94013f0d2851de2
https://github.com/scummvm/scummvm/commit/2bc1457374b257b43da66e35b94013f0d2851de2
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:02+02:00
Commit Message:
SCUMM: RA2: Read fonts correctly
Changed paths:
engines/scumm/nut_renderer.cpp
diff --git a/engines/scumm/nut_renderer.cpp b/engines/scumm/nut_renderer.cpp
index 583a2019d8f..9612093b54d 100644
--- a/engines/scumm/nut_renderer.cpp
+++ b/engines/scumm/nut_renderer.cpp
@@ -185,7 +185,12 @@ void NutRenderer::loadFont(const char *filename) {
// If characters have transparency, then bytes just get skipped and
// so there may appear some garbage. That's why we have to fill it
// with a default color first.
- if (codec == 44) {
+ //
+ // For codec 44: standard SCUMM v7/v8 fonts use value 2 as the
+ // transparent color. But RA2 codec 44 fonts use value 2 as an actual
+ // glyph color (medium body shade), so we must use 0 instead to avoid
+ // making those pixels invisible during rendering.
+ if (codec == 44 && _vm->_game.id != GID_REBEL2) {
memset(_chars[l].src, kSmush44TransparentColor, _chars[l].width * _chars[l].height);
_chars[l].transparency = kSmush44TransparentColor;
} else {
@@ -293,7 +298,9 @@ void NutRenderer::loadFontFromData(const byte *data, int32 dataSize) {
decodedPtr += (_chars[l].width * _chars[l].height);
- if (codec == 44) {
+ // Same transparency logic as loadFont: RA2 codec 44 fonts use
+ // value 2 as a glyph color, not as transparency.
+ if (codec == 44 && _vm->_game.id != GID_REBEL2) {
memset(_chars[l].src, kSmush44TransparentColor, _chars[l].width * _chars[l].height);
_chars[l].transparency = kSmush44TransparentColor;
} else {
@@ -414,7 +421,20 @@ int NutRenderer::drawCharV7(byte *buffer, Common::Rect &clipRect, int x, int y,
int clipWdth = (_chars[chr].width - width);
char color = (col != -1) ? col : 1;
- if (_vm->_game.version == 7) {
+ if (_vm->_game.id == GID_REBEL2) {
+ // RA2 codec 44 fonts use pixel values 1-4 as body/gradient.
+ // The AHDR palette indices don't match the SMUSH video palette,
+ // so all non-transparent pixels get the caller's text color.
+ for (int j = minY; j < height; j++) {
+ for (int i = minX; i < width; i++) {
+ int8 value = *src++;
+ if (value != _chars[chr].transparency)
+ dst[i] = color;
+ }
+ src += clipWdth;
+ dst += pitch;
+ }
+ } else if (_vm->_game.version == 7) {
if (hardcodedColors) {
for (int j = minY; j < height; j++) {
for (int i = minX; i < width; i++) {
@@ -440,8 +460,7 @@ int NutRenderer::drawCharV7(byte *buffer, Common::Rect &clipRect, int x, int y,
}
} else {
if (hardcodedColors) {
- // Direct pixel write for NUT fonts with embedded palette colors (e.g., RA2 menu fonts)
- // This mirrors the version 7 hardcodedColors behavior
+ // Direct pixel write for NUT fonts with embedded palette colors
for (int j = minY; j < height; j++) {
for (int i = minX; i < width; i++) {
int8 value = *src++;
Commit: 8ea7634eeea92ebfdc891c94caf83c4df2cebe0e
https://github.com/scummvm/scummvm/commit/8ea7634eeea92ebfdc891c94caf83c4df2cebe0e
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:02+02:00
Commit Message:
SCUMM: RA2: Implement damage effect
Changed paths:
engines/scumm/insane/insane_rebel.cpp
engines/scumm/insane/insane_rebel.h
diff --git a/engines/scumm/insane/insane_rebel.cpp b/engines/scumm/insane/insane_rebel.cpp
index 3d16c682d2c..782e83b356d 100644
--- a/engines/scumm/insane/insane_rebel.cpp
+++ b/engines/scumm/insane/insane_rebel.cpp
@@ -128,9 +128,14 @@ InsaneRebel2::InsaneRebel2(ScummEngine_v7 *scumm) {
_viewX = 0;
_viewY = 0;
+ // Damage visual effect counters (FUN_420515/420562/420754/42073B)
+ _damageFlashCounter = 0;
+ _damageHighFlashCounter = 0;
+ _damageShakeCounter = 0;
+ memset(_damageSavedPalette, 0, sizeof(_damageSavedPalette));
+
// Retail globals mapped: hit counter, cooldown, invulnerability flag
_rebelHitCounter = 0;
- _rebelHitCooldown = 0;
_rebelInvulnerable = false;
// Opcode 6 state variables
@@ -1243,70 +1248,57 @@ void InsaneRebel2::iactRebel2Opcode2(Common::SeekableReadStream &b, int16 par2,
}
}
void InsaneRebel2::iactRebel2Opcode3(Common::SeekableReadStream &b, int16 par2, int16 par3, int16 par4) {
- // Handle IACT opcode 3 subcases (damage, counters, special 100 branch)
- // Mirrors retail FUN_0041CADB case 1 behavior where possible.
-
- // Very small cooldown counter decremented on each IACT to emulate DAT_0045790a behavior
- if (_rebelHitCooldown > 0) _rebelHitCooldown--;
+ // FUN_00401234 case 1 (Handler 8): IACT opcode 3 â damage and hit counter.
+ //
+ // par3 == 5: Damage path â probability check, accumulate damage, trigger visual effect
+ // Original reads probability from per-level table DAT_0047e0fc[levelIdx]
+ // and damage amount from DAT_0047e0f8[levelIdx].
+ // triggerDamageEffect (FUN_0042073B) is called OUTSIDE invulnerability check.
+ //
+ // par3 == 1: Hit counter increment (DAT_0047ab80)
- // Subcase: par3 == 5 -> damage logic, expects extra param at +10 (source enemy ID)
if (par3 == 5) {
- b.skip(2); // Offset +8
- int16 srcId = b.readSint16LE(); // Offset +10 (Enemy ID)
+ b.skip(2); // Offset +8 (unused)
+ int16 srcId = b.readSint16LE(); // Offset +10: source enemy ID (local_14[5])
- // Only proceed if source is active (bit clear)
- if (!isBitSet(srcId)) {
- if (_rebelHitCooldown < 2) {
- int limit = 20 + _difficulty * 20; // heuristic mapping for probability table
- if (limit < 5) limit = 5;
- if (limit > 90) limit = 90;
- if (_vm->_rnd.getRandomNumber(100) < limit) {
- // Apply damage unless invulnerable flag set (DAT_0047ab64)
- if (!_rebelInvulnerable) {
- int damageAmount = 5 + (_difficulty * 2);
- // Apply to shields first (do not end game on depletion during tests)
-
-
- // Update the retail-like damage accumulator (DAT_0047a7ec equivalent)
- _playerDamage += damageAmount;
- if (_playerDamage > 255) _playerDamage = 255;
- debug("Rebel2: Damage HIT by Enemy %d. Damage=%d (limit=%d)", srcId, _playerDamage, limit);
- // TODO: call UI update / flash screen / play sound to match retail (FUN_00420515 / FUN_0041189e)
- }
- // Impose short cooldown to prevent immediate repeated damage
- _rebelHitCooldown = 6;
- }
- }
- }
- }
- // Subcase: par3 == 1 -> increment hit counter when source active and par4 != 4
- else if (par3 == 1) {
- b.skip(2); // read extra param (source id)
- int16 srcId = b.readSint16LE();
- if (!isBitSet(srcId) && par4 != 4) {
- _rebelHitCounter++;
- debug("Rebel2: Incremented hit counter DAT_0047ab80 -> %d (source=%d)", _rebelHitCounter, srcId);
- }
- }
- // Special-case branch when par2 == 100 (retail: triggers damage/sound via different offsets)
- else if (par2 == 100) {
- b.skip(2);
- int16 srcId = b.readSint16LE();
+ debug("Rebel2 Opcode3: par3=5 srcId=%d isBitSet=%d", srcId, isBitSet(srcId));
+
+ // FUN_00423970(srcId): only proceed if source enemy is active (bit clear)
if (!isBitSet(srcId)) {
- int limit = 20 + _difficulty * 20;
- if (_vm->_rnd.getRandomNumber(100) < limit) {
+ // Probability check: original reads from per-level table DAT_0047e0fc
+ // TODO: Use actual per-level probability table instead of hardcoded values
+ int probability = 20 + _difficulty * 20;
+ if (probability < 5) probability = 5;
+ if (probability > 90) probability = 90;
+
+ int roll = _vm->_rnd.getRandomNumber(99); // FUN_004233a0(100) returns [0,99]
+ debug("Rebel2 Opcode3: probability=%d roll=%d (need roll < prob)", probability, roll);
+
+ if (roll < probability) {
+ // Apply damage unless invulnerable (DAT_0047ab64 == 0)
if (!_rebelInvulnerable) {
+ // TODO: Read damage amount from per-level table DAT_0047e0f8
int damageAmount = 5 + (_difficulty * 2);
- // Increment the retail-like damage accumulator (DAT_0047a7ec equivalent)
_playerDamage += damageAmount;
if (_playerDamage > 255) _playerDamage = 255;
- debug("Rebel2: Damage HIT (special) by Enemy %d. Damage=%d (limit=%d)", srcId, _playerDamage, limit);
+ debug("Rebel2: Damage HIT by Enemy %d. Damage=%d", srcId, _playerDamage);
+ }
+ // Visual effect â called regardless of invulnerability.
+ // Handler 8: FUN_0042073B â palette flash + screen shake
+ // Other handlers: FUN_00420515 â palette flash only (no screen shake)
+ if (_rebelHandler == 8) {
+ triggerDamageEffect();
+ } else {
+ initDamageFlash();
}
- _rebelHitCooldown = 6;
+ // TODO: FUN_0041189e(1, 0, 0x7f, 0, 0) â play hit sound
}
}
+ } else if (par3 == 1) {
+ // Hit counter increment: unconditional in original (no isBitSet or par4 check)
+ _rebelHitCounter++;
+ debug("Rebel2: Incremented hit counter DAT_0047ab80 -> %d", _rebelHitCounter);
}
- // other subcases not implemented yet
}
void InsaneRebel2::iactRebel2Opcode6(byte *renderBitmap, Common::SeekableReadStream &b, int32 chunkSize, int16 par2, int16 par3, int16 par4) {
@@ -4071,6 +4063,22 @@ void InsaneRebel2::procPostRendering(byte *renderBitmap, int32 codecparam, int32
// Laser shot beams and impacts
renderLaserShots(renderBitmap, pitch, width, height);
+ // Damage visual effects â handler-specific per original architecture:
+ // Handler 8: FUN_401CCF line 119 â FUN_00420754 (palette flash + screen shake)
+ // Handler 0x19: FUN_41DB5E line 192 â FUN_00420562 (palette flash only, every frame)
+ // Handler 0x26: FUN_4092D9 lines 135/225/237 â FUN_00420515 trigger + palette flash
+ // Handler 7: No damage effects
+ if (_rebelHandler == 8) {
+ // Full damage effect: palette flash + screen shake
+ // Suppressed during autopilot (mode 4) and cutscene (mode 5)
+ if (_shipLevelMode != 4 && _shipLevelMode != 5) {
+ updateDamageEffect(renderBitmap, pitch, width, height);
+ }
+ } else if (_rebelHandler == 0x19 || _rebelHandler == 0x26) {
+ // Palette flash only â no screen shake for turret/FPS handlers
+ updateDamageFlashPalette();
+ }
+
// Collision zone visualization (debug - for Handler 7/8 pilot modes)
if (_rebelHandler == 7 || _rebelHandler == 8) {
drawCollisionZones(renderBitmap, pitch, width, height, 0);
@@ -4086,6 +4094,143 @@ void InsaneRebel2::procPostRendering(byte *renderBitmap, int32 codecparam, int32
frameEndCleanup();
}
+// ======================= Damage Visual Effect Functions =======================
+// Palette flash + screen shake when the player takes damage.
+// Original retail functions: FUN_420515, FUN_420562, FUN_420754, FUN_42073B, FUN_420501
+
+// FUN_00420501 - Reset palette flash counter.
+// Called at level start / scene transitions to clear any in-progress flash.
+void InsaneRebel2::resetDamageFlash() {
+ _damageFlashCounter = 0;
+}
+
+// FUN_00420515 - Save current palette and initiate a 5-frame flash.
+// If a flash is already in progress, just resets the counter to 5
+// (the palette was already saved on the first hit).
+void InsaneRebel2::initDamageFlash() {
+ if (_damageFlashCounter == 0) {
+ // Save current SMUSH palette before modifying it
+ memcpy(_damageSavedPalette, _player->_pal, 0x300);
+ }
+ _damageFlashCounter = 5;
+}
+
+// FUN_0042073B - Trigger both palette flash and screen shake.
+// Called from the damage hit handler when the player takes damage.
+void InsaneRebel2::triggerDamageEffect() {
+ initDamageFlash();
+ _damageShakeCounter = 10;
+}
+
+// FUN_00420562 - Per-frame palette modification.
+//
+// Two modes determined by _damageHighFlashCounter:
+//
+// Normal hit flash (_damageHighFlashCounter == 0 or odd):
+// Decrements _damageFlashCounter. On even counter values, all 768 palette
+// bytes (RGB) are blended from inverted toward the saved original:
+// output[i] = 0xFF - ((0xFF - saved[i]) * (0x10 - counter)) >> 4
+// Counter 5â4(apply)â3(skip)â2(apply)â1(skip)â0(apply=original). The
+// alternating apply/skip creates a strobe-like flash effect.
+//
+// High-damage red pulse (_playerDamage >= 0xFF, even counter):
+// Only the R channel (every 3rd byte) is modified using the same formula
+// with _damageHighFlashCounter. Creates a pulsing red tint overlay.
+void InsaneRebel2::updateDamageFlashPalette() {
+ // High-damage mode: persistent red pulsing when damage is maxed out
+ if (_playerDamage < 0xFF) {
+ _damageHighFlashCounter = 0;
+ } else {
+ if (_damageHighFlashCounter == 0) {
+ // Save palette on first frame of high-damage mode
+ memcpy(_damageSavedPalette, _player->_pal, 0x300);
+ }
+ if (_damageHighFlashCounter < 0x10) {
+ _damageHighFlashCounter++;
+ }
+ }
+
+ if (_damageHighFlashCounter == 0 || (_damageHighFlashCounter & 1) != 0) {
+ // Normal hit flash path: decrement counter, apply on even values.
+ // Original C: if ((counter != 0) && (counter--, (counter & 1) == 0))
+ if (_damageFlashCounter != 0) {
+ _damageFlashCounter--;
+ if ((_damageFlashCounter & 1) == 0) {
+ // Apply palette inversion on ALL RGB channels
+ byte modPal[0x300];
+ int blend = 0x10 - _damageFlashCounter;
+ for (int i = 0; i < 0x300; i++) {
+ modPal[i] = 0xFF - (((0xFF - _damageSavedPalette[i]) * blend) >> 4);
+ }
+ _player->setPalette(modPal);
+ }
+ }
+ } else {
+ // High-damage red-only flash (even _damageHighFlashCounter):
+ // Modify only R channel (stride 3), G and B stay unchanged.
+ byte modPal[0x300];
+ memcpy(modPal, _player->_pal, 0x300);
+ int blend = 0x10 - _damageHighFlashCounter;
+ for (int i = 0; i < 0x300; i += 3) {
+ modPal[i] = 0xFF - (((0xFF - _damageSavedPalette[i]) * blend) >> 4);
+ }
+ _player->setPalette(modPal);
+ }
+}
+
+// FUN_00420754 - Per-frame screen shake + palette flash.
+//
+// Screen shake randomly shifts scanlines left or right for visual distortion.
+// The number of affected scanlines decreases each frame (counter * 5),
+// creating a diminishing shake effect over 10 frames.
+//
+// Called every frame from procPostRendering when not in cutscene modes
+// (shipLevelMode != 4 and != 5, matching original: DAT_0043e000 != 4 && != 5).
+void InsaneRebel2::updateDamageEffect(byte *renderBitmap, int pitch, int width, int height) {
+ if (_damageShakeCounter != 0) {
+ _damageShakeCounter--;
+ int numLines = _damageShakeCounter * 5;
+
+ // Temporary buffer for scanline rotation (case 1 in original)
+ byte tempLine[640];
+
+ for (int n = numLines; n > 0; n--) {
+ // Pick a random scanline within the gameplay area (0..179, not status bar)
+ int maxY = MIN(height, 180);
+ int scanline = _vm->_rnd.getRandomNumber(maxY - 1);
+
+ byte *linePtr = renderBitmap + pitch * scanline;
+ int offset = _vm->_rnd.getRandomNumber(4) + 1; // 1..5 pixel shift
+ int direction = _vm->_rnd.getRandomNumber(4); // 0..4
+
+ int copyLen = pitch - offset;
+ if (copyLen <= 0)
+ continue;
+
+ switch (direction) {
+ case 0:
+ case 3:
+ // Shift left: copy line[offset..] -> line[0..]
+ memmove(linePtr, linePtr + offset, copyLen);
+ break;
+ case 1:
+ // Shift right with wrap: save, then copy
+ memcpy(tempLine, linePtr, MIN(copyLen, (int)sizeof(tempLine)));
+ memmove(linePtr + offset, tempLine, MIN(copyLen, (int)sizeof(tempLine)));
+ break;
+ case 2:
+ case 4:
+ // Shift right: copy line[0..] -> line[offset..]
+ memmove(linePtr + offset, linePtr, copyLen);
+ break;
+ }
+ }
+ }
+
+ // Palette flash runs every frame (even without shake)
+ updateDamageFlashPalette();
+}
+
// ======================= Rendering Helper Functions =======================
// These are extracted from procPostRendering for better readability
@@ -4436,9 +4581,14 @@ void InsaneRebel2::renderHandler8Ship(byte *renderBitmap, int pitch, int width,
renderNutSprite(renderBitmap, pitch, width, height, drawX, drawY, _shipSprite, spriteIndex);
- // Secondary ship sprite
- if (_shipSprite2 && _shipSprite2->getNumChars() > spriteIndex) {
- renderNutSprite(renderBitmap, pitch, width, height, drawX, drawY, _shipSprite2, spriteIndex);
+ // Shadow sprite (POV004 / DAT_0047e028): drawn at same position as primary ship.
+ // Original FUN_401CCF lines 91-92 uses param_5 & 1 (firing flag) as sprite index
+ // for both primary and shadow, NOT the direction-based spriteIndex.
+ if (_shipSprite2) {
+ int shadowIndex = _shipFiring ? 1 : 0;
+ if (shadowIndex < _shipSprite2->getNumChars()) {
+ renderNutSprite(renderBitmap, pitch, width, height, drawX, drawY, _shipSprite2, shadowIndex);
+ }
}
debug("Rebel2 Handler8: Ship at (%d,%d) raw(%d,%d) offset(%d,%d) sprite=%d/%d dir=(%d,%d)",
@@ -7769,6 +7919,9 @@ int InsaneRebel2::runLevel(int levelId) {
_playerShield = 255;
_playerScore = 0;
_playerDamage = 0;
+ resetDamageFlash();
+ _damageHighFlashCounter = 0;
+ _damageShakeCounter = 0;
_currentPhase = 1;
_phaseScore = 0;
_phaseMisses = 0;
diff --git a/engines/scumm/insane/insane_rebel.h b/engines/scumm/insane/insane_rebel.h
index 0a32966f5ab..ba460e2fd8f 100644
--- a/engines/scumm/insane/insane_rebel.h
+++ b/engines/scumm/insane/insane_rebel.h
@@ -578,9 +578,32 @@ public:
int _viewX;
int _viewY;
+ // ======================= Damage Visual Effect System =======================
+ // Palette flash + screen shake on taking damage.
+ // Original functions: FUN_420515, FUN_420562, FUN_420754, FUN_42073B
+ //
+ // FUN_42073B (triggerDamageEffect): Called on damage hit. Initiates palette
+ // flash via FUN_420515 and sets screen shake counter to 10.
+ // FUN_420515 (initDamageFlash): Saves current palette, sets 5-frame flash.
+ // FUN_420562 (updateDamageFlashPalette): Per-frame palette inversion.
+ // Normal hit: all RGB channels inverted toward white, fades over 5 frames.
+ // High damage (>=255): red channel pulsing on even frames.
+ // FUN_420754 (updateDamageEffect): Per-frame screen shake (random scanline
+ // shifts) + calls FUN_420562. Called every frame from the render loop.
+
+ void triggerDamageEffect(); // FUN_0042073B
+ void initDamageFlash(); // FUN_00420515
+ void updateDamageFlashPalette(); // FUN_00420562
+ void updateDamageEffect(byte *renderBitmap, int pitch, int width, int height); // FUN_00420754
+ void resetDamageFlash(); // FUN_00420501
+
+ int16 _damageFlashCounter; // DAT_00482404 - palette flash countdown (0..5)
+ int16 _damageHighFlashCounter; // DAT_00482408 - high-damage red flash (0..16)
+ int16 _damageShakeCounter; // DAT_0048240c - screen shake countdown (0..10)
+ byte _damageSavedPalette[0x300]; // DAT_00459990 - palette snapshot before flash
+
// Rebel per-level counters / flags mapped from retail globals
int _rebelHitCounter; // DAT_0047ab80 - hit counter / state tracker
- int _rebelHitCooldown; // DAT_0045790a - cooldown / timing for damage checks
bool _rebelInvulnerable; // DAT_0047ab64 - toggles invulnerability / state
// Opcode 6 state variables (from FUN_41CADB case 4)
Commit: 1f53cb0d7ef911560d74d7803bfdaf51007e12f2
https://github.com/scummvm/scummvm/commit/1f53cb0d7ef911560d74d7803bfdaf51007e12f2
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:02+02:00
Commit Message:
SCUMM: RA2: Improve pilot menu
Changed paths:
engines/scumm/insane/insane_rebel.cpp
engines/scumm/insane/insane_rebel.h
diff --git a/engines/scumm/insane/insane_rebel.cpp b/engines/scumm/insane/insane_rebel.cpp
index 782e83b356d..769f0c56ab0 100644
--- a/engines/scumm/insane/insane_rebel.cpp
+++ b/engines/scumm/insane/insane_rebel.cpp
@@ -392,9 +392,9 @@ InsaneRebel2::InsaneRebel2(ScummEngine_v7 *scumm) {
_previewOffsetY = 75; // Chapter 0: 0 * -50 + 75 = 75
// Initialize pilot selection system (FUN_00414A41)
- // Menu structure: 6 levels + 4 options (NEW PILOT, DELETE PILOT, COPY PILOT, MAIN MENU)
- _levelSelection = 0; // First level selected
- _levelItemCount = 10; // 6 levels + 4 options
+ // Menu structure: [saved pilots] + 4 fixed options (NEW/DUPE/DELETE/MAIN MENU)
+ _levelSelection = 0; // First item selected
+ _levelItemCount = 4; // 0 saved pilots + 4 fixed options
_selectedLevel = 1; // Default selected level
// Initialize menu input capture system
@@ -5707,139 +5707,63 @@ int InsaneRebel2::processMenuInput() {
return result;
}
-void InsaneRebel2::drawMenuOverlay(byte *renderBitmap, int pitch, int width, int height) {
+void InsaneRebel2::drawMenuItems(byte *renderBitmap, int pitch, int width, int height,
+ const char **items, int numItems, int selection) {
// =====================================================================
- // Emulates FUN_0041f5ae - Menu Text Overlay Renderer
+ // Shared menu renderer - Emulates FUN_0041f5ae param_4==0 (centered mode)
// Address: 0x41F5AE
// =====================================================================
//
- // Call chain from main menu:
- // FUN_004147b2 (Main Menu Handler) -> FUN_0041f5ae (this function)
- //
- // IMPORTANT: The menu background comes from the O_MENU_X.SAN video file, NOT from MSTOVER.NUT.
- // The O_MENU_X.SAN files (A through O) each contain a full 320x200 FOBJ frame in Frame 0
- // which is decoded by SmushPlayer and stored in renderBitmap before this function is called.
- // MSTOVER.NUT is only used in cheat mode (when DAT_0047aba4 != 0).
- //
- // FUN_0041f5ae parameters:
- // param_1: 0 = render mode, 1 = init mode (reset selection)
- // param_2: Array of string pointers (menu items from TRS)
- // param_3: Number of selectable menu items
- // param_4: 0 = main menu (keyboard), 1 = level selection menu
+ // items[0] = title string, items[1..numItems] = selectable items
+ // numItems = number of selectable items (FUN_0041f5ae param_3)
+ // selection = currently highlighted item (0-based, maps to DAT_00459988)
//
- // Menu strings loaded from GAME.TRS (keyboard mode indices 10-17):
- // TRS index 10: "^f02Game Main Menu" -> Title (uses TITLFONT)
- // TRS index 11: "^f01^c005Start Game" -> Item 0 (uses SMALFONT, color 5)
- // TRS index 12: "^f01^c009Options" -> Item 1 (uses SMALFONT, color 9)
- // TRS index 13: "^f01^c009Calibrate Joystick" -> Item 2
- // TRS index 14: "^f01^c009Continue Intro" -> Item 3
- // TRS index 15: "^f01^c009Show Top Pilots" -> Item 4
- // TRS index 16: "^f01^c009Show Credits" -> Item 5
- // TRS index 17: "^f01^c240Return to Launcher" -> Item 6 (color 240)
-
- // Load menu strings from GAME.TRS via SmushPlayer
- // TRS indices 10-17 correspond to main menu items (from FUN_00414073)
- // No fallback strings - all text must come from TRS for localization support
- SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
- const char *menuItems[8];
-
- if (!splayer) {
- debug(1, "drawMenuOverlay: SmushPlayer not available for TRS strings!");
- return;
- }
-
- for (int i = 0; i < 8; i++) {
- menuItems[i] = splayer->getString(10 + i);
- if (!menuItems[i] || !menuItems[i][0]) {
- debug(1, "drawMenuOverlay: TRS string %d not found!", 10 + i);
- menuItems[i] = ""; // Empty string to avoid crashes
- }
- }
-
- const int numItemsTotal = 8; // Title + 7 menu options (matching assembly)
- const int numSelectableItems = 7; // Selectable menu options (0-6)
-
- // =====================================================================
- // Coordinate calculations from FUN_0041f5ae (lines 18-32)
- // =====================================================================
- // Low-res mode (DAT_0047a808 < 2):
- // Title Y: param_3 * -5 + 0x51 = 8 * -5 + 81 = 41 (line 19)
- // Item base Y: param_3 * -5 + 0x68 = 8 * -5 + 104 = 64 (line 28)
- // Center X: ((DAT_0047a808 < 2) - 1 & 0xa0) + 0xa0 = 160 (line 25)
- //
- // High-res mode (DAT_0047a808 >= 2):
- // Title Y: (param_3 * -5 + 0x5a) * 2 + -0x12 (line 22-23)
- // Item Y: (param_3 * -5 + 0x5a + i * 10) * 2 + 0x1c (line 31)
- // Center X: 320
- const int centerX = width / 2; // 160 for 320px width
- const int titleY = numItemsTotal * -5 + 0x51; // 41
- const int itemBaseY = numItemsTotal * -5 + 0x68; // 64
- const int itemSpacing = 10; // Line 28: local_c * 10
-
- debug(5, "drawMenuOverlay: buffer %dx%d, centerX=%d", width, height, centerX);
+ // Coordinate formulas from FUN_0041f5ae (low-res, DAT_0047a808 < 2):
+ // Title Y: param_3 * -5 + 0x51 (line 19)
+ // Item base Y: param_3 * -5 + 0x68 (line 28)
+ // Item Y: param_3 * -5 + i * 10 + 0x68 (line 28)
+ // Center X: ((DAT_0047a808 < 2) - 1 & 0xa0) + 0xa0 = 160
+ // Box Y: param_3 * -5 + i * 10 + 0x67 (1px above text)
+
+ const int centerX = width / 2;
+ const int titleY = numItems * -5 + 0x51;
+ const int itemBaseY = numItems * -5 + 0x68;
+ const int itemSpacing = 10;
// =====================================================================
- // Font system - Emulates linked list from FUN_00403bd0 (lines 302-348)
+ // Font system - Emulates linked list from FUN_00403bd0
// =====================================================================
- // Font linked list structure (DAT_00485058):
- // offset 0x00: pointer to previous font in chain
- // offset 0x04: pointer to next font in chain
- // offset 0x08: pointer to font data (NUT)
- // offset 0x0C: font index/ID
- // offset 0x0E: chain terminator flag (0 = end of chain)
- //
- // Fonts loaded in low-res mode:
- // Font 0 (^f00): TALKFONT.NUT - DAT_00485058 (root)
- // Font 1 (^f01): SMALFONT.NUT - linked via offset 0x04
- // Font 2 (^f02): TITLFONT.NUT - linked via offset 0x04
+ // Font 0 (^f00): TALKFONT.NUT
+ // Font 1 (^f01): SMALFONT.NUT (menu items)
+ // Font 2 (^f02): TITLFONT.NUT (title)
NutRenderer *fonts[3] = {
- _smush_talkfontNut, // Font 0 - TALKFONT.NUT (default)
- _smush_smalfontNut, // Font 1 - SMALFONT.NUT (menu items)
- _smush_titlefontNut // Font 2 - TITLFONT.NUT (title)
+ _smush_talkfontNut,
+ _smush_smalfontNut,
+ _smush_titlefontNut
};
- // FUN_004341a0 line 35-37: Default font when param_1 == -1
- // if (param_1 == (int *)0xffffffff) param_1 = DAT_00485058;
NutRenderer *defaultFont = fonts[0] ? fonts[0] : _smush_smalfontNut;
if (!defaultFont) {
- debug(1, "drawMenuOverlay: no fonts available!");
+ debug(1, "drawMenuItems: no fonts available!");
return;
}
- // Set up clipRect for the entire rendering area
Common::Rect clipRect(0, 0, _vm->_screenWidth, _vm->_screenHeight);
int actualPitch = _vm->_screenWidth;
// =====================================================================
- // Format code parser - Emulates FUN_00434d10 and FUN_00433da0
+ // Format code parser - Emulates FUN_00434d10 / FUN_00433da0
// =====================================================================
- // Format codes parsed by the original game:
- // 0x5e 0x5e (^^) : Literal ^ character (FUN_00434d10 line 34-36)
- // 0x5e 0x66 (^f) : Font switch ^fNN (FUN_00434d10 lines 41-66)
- // 0x5e 0x63 (^c) : Color code ^cNNN (FUN_00434d10 lines 70-84)
- // 0x5e 0x6c (^l) : Newline
- //
- // Font switching algorithm from FUN_00433da0 (lines 86-107):
- // sVar4 = cVar1 * 10 + (short)*pcVar5; // Parse 2-digit font index
- // Font index 0x210 (528) = '0'*10 + '0' = font 0
- // Font index 0x211 (529) = '0'*10 + '1' = font 1
- // Font index 0x212 (530) = '0'*10 + '2' = font 2
- //
- // Color parsing from FUN_00434d10 (line 81):
- // color = (digit1 - '0') * 100 + (digit2 - '0') * 10 + (digit3 - '0')
- // e.g., ^c005 -> color = 5, ^c240 -> color = 240
+ // ^^ = literal ^, ^fNN = font switch, ^cNNN = color code, ^l = newline
auto parseFormatCode = [&](const char *&str, int &outColor) -> int {
- if (*str != '^') return -1; // 0x5e check
+ if (*str != '^') return -1;
const char *p = str + 1;
if (*p == '^') {
- // ^^ = literal ^ (FUN_00434d10 line 34-36)
str = p;
- return -1; // Process ^ as character
+ return -1;
}
if (*p == 'f') {
- // ^fNN = font switch (FUN_00434d10 lines 41-66)
- // Parse 2-digit font index: sVar4 = cVar1 * 10 + (short)*pcVar5
p++;
int fontIdx = 0;
while (*p >= '0' && *p <= '9') {
@@ -5850,8 +5774,6 @@ void InsaneRebel2::drawMenuOverlay(byte *renderBitmap, int pitch, int width, int
return (fontIdx >= 0 && fontIdx < 3) ? fontIdx : 0;
}
if (*p == 'c') {
- // ^cNNN = color code (FUN_00434d10 lines 70-84)
- // Parse 3-digit color: (d1-'0')*100 + (d2-'0')*10 + (d3-'0')
p++;
int color = 0;
while (*p >= '0' && *p <= '9') {
@@ -5860,25 +5782,16 @@ void InsaneRebel2::drawMenuOverlay(byte *renderBitmap, int pitch, int width, int
}
str = p;
outColor = color;
- return -2; // Color changed, no font change
+ return -2;
}
if (*p == 'l') {
- // ^l = newline
str = p + 1;
return -2;
}
- // Unknown code, skip
return -1;
};
- // =====================================================================
// String width calculation - Emulates FUN_00433da0
- // Address: 0x433DA0
- // =====================================================================
- // Iterates through string, parsing format codes and summing character widths
- // Character width lookup (FUN_00433da0 lines 129-133):
- // piVar2 = (int *)DAT_0046a5e4[2]; // Font data pointer
- // sVar8 = sVar8 + *(short *)(*piVar2 + 0x16 + iVar3); // Add char width
auto getStringWidth = [&](const char *str) -> int {
int w = 0;
NutRenderer *curFont = defaultFont;
@@ -5887,14 +5800,13 @@ void InsaneRebel2::drawMenuOverlay(byte *renderBitmap, int pitch, int width, int
while (*str) {
int fontChange = parseFormatCode(str, curColor);
if (fontChange >= 0) {
- // Font switch via linked list traversal (FUN_00433da0 lines 94-106)
curFont = fonts[fontChange] ? fonts[fontChange] : defaultFont;
continue;
}
- if (fontChange == -2) continue; // Color or newline code
+ if (fontChange == -2) continue;
byte c = (byte)*str++;
- if (c >= 'a' && c <= 'z') c = c - 'a' + 'A'; // Uppercase conversion
+ if (c >= 'a' && c <= 'z') c = c - 'a' + 'A';
if (curFont && c < curFont->getNumChars()) {
w += curFont->getCharWidth(c);
}
@@ -5902,39 +5814,19 @@ void InsaneRebel2::drawMenuOverlay(byte *renderBitmap, int pitch, int width, int
return w;
};
- // =====================================================================
// String rendering - Emulates FUN_00434d10
- // Address: 0x434D10
- // =====================================================================
- // Renders string character-by-character with format code support
- // Calls FUN_004236e0 for each character glyph (FUN_00434d10 lines 96-97)
- //
- // Color handling analysis from assembly:
- // Color handling for NUT fonts (from assembly analysis):
- //
- // For codec 44 (used by RA2 fonts): The ^cNNN color IS used for pixel coloring!
- // - Font pixels with value 1 are replaced with the ^cNNN color
- // - Font pixels with value 255 are replaced with 0
- // - Other values are written directly
- //
- // FUN_00434d10 parses ^cNNN and passes the color to FUN_004236e0 as param_8,
- // which passes it through to the codec for byte 1 substitution.
- //
- // In drawCharV7's default mode (hardcodedColors=false, smushColorMode=false):
- // dst[i] = (value == 1) ? color : value;
- // This implements the codec 44 color substitution.
+ // Codec 44 color substitution: font pixels with value 1 â ^cNNN color
auto drawString = [&](const char *str, int x, int y) {
NutRenderer *curFont = defaultFont;
- int curColor = 1; // Default color if no ^cNNN specified (white/foreground)
+ int curColor = 1;
while (*str) {
int fontChange = parseFormatCode(str, curColor);
if (fontChange >= 0) {
- // Font switch (FUN_00434d10 lines 41-66)
curFont = fonts[fontChange] ? fonts[fontChange] : defaultFont;
continue;
}
- if (fontChange == -2) continue; // Color code parsed, curColor updated
+ if (fontChange == -2) continue;
byte c = (byte)*str++;
if (c >= 'a' && c <= 'z') c = c - 'a' + 'A';
@@ -5945,8 +5837,6 @@ void InsaneRebel2::drawMenuOverlay(byte *renderBitmap, int pitch, int width, int
int charW = curFont->getCharWidth(c);
- // FUN_004236e0 -> FUN_0042cba0 -> codec: Render character glyph
- // Use default mode for codec 44 color substitution: byte 1 -> curColor
if (x >= 0 && y >= 0 && charW > 0) {
curFont->drawCharV7(renderBitmap, clipRect, x, y, actualPitch, curColor,
kStyleAlignLeft, c, false, false);
@@ -5956,78 +5846,44 @@ void InsaneRebel2::drawMenuOverlay(byte *renderBitmap, int pitch, int width, int
};
// =====================================================================
- // Draw title - FUN_0041f5ae lines 24-25
+ // Draw title - items[0], centered at titleY
// =====================================================================
- // FUN_004341a0((int *)0xffffffff, DAT_0047a7d0, DAT_0047a7d8,
- // ((DAT_0047a808 < 2) - 1 & 0xa0) + 0xa0, sVar2, 1, 0, 1, (char *)*param_2);
- // X position: centerX (160 for low-res)
- // Y position: sVar2 = param_3 * -5 + 0x51
{
- int titleWidth = getStringWidth(menuItems[0]);
- int titleX = centerX - titleWidth / 2; // Center alignment (param_8 & 1)
- drawString(menuItems[0], titleX, titleY);
+ int titleWidth = getStringWidth(items[0]);
+ int titleX = centerX - titleWidth / 2;
+ drawString(items[0], titleX, titleY);
}
// =====================================================================
- // Draw menu items - FUN_0041f5ae lines 26-48
+ // Draw selectable items with selection highlight box
// =====================================================================
- // for (local_c = 0; local_c < param_3; local_c = local_c + 1) {
- // FUN_004341a0(..., (char *)param_2[local_c + 1]);
- // if (DAT_00459988 == local_c) { // Selected item
- // sVar2 = FUN_00433da0(...); // Get string width
- // FUN_004292d0(...); // Draw selection box
- // }
- // }
- for (int i = 0; i < numSelectableItems; i++) {
- // Item Y calculation (FUN_0041f5ae line 28):
- // sVar2 = param_3 * -5 + local_c * 10 + 0x68
+ for (int i = 0; i < numItems; i++) {
int itemY = itemBaseY + i * itemSpacing;
- const char *text = menuItems[i + 1];
+ const char *text = items[i + 1];
int textWidth = getStringWidth(text);
- int textX = centerX - textWidth / 2; // Center alignment
+ int textX = centerX - textWidth / 2;
drawString(text, textX, itemY);
- // =====================================================================
- // Selection highlight box - FUN_0041f5ae lines 36-47
- // Calls FUN_004292d0 (Rectangle Drawer) at 0x4292D0
- // =====================================================================
- // if (DAT_00459988 == local_c) {
- // sVar2 = FUN_00433da0((int *)0xffffffff, (byte *)param_2[local_c + 1]);
- // sVar2 = sVar2 + ((DAT_0047a808 < 2) - 1 & 6) + 6; // Width padding
- // FUN_004292d0(DAT_0047a7d0, DAT_0047a7d8,
- // (((DAT_0047a808 < 2) - 1 & 0xa0) + 0xa0) - sVar2 / 2, // X
- // sVar1, // Y = param_3 * -5 + local_c * 10 + 0x67
- // sVar2, // Width
- // ((DAT_0047a808 < 2) - 1 & 10) + 10, // Height = 10
- // (-((DAT_0047a7e4 & 1) == 0) & 8U) - 0x10); // Color
- // }
- if (i == _menuSelection) {
- // Width: sVar2 + ((DAT_0047a808 < 2) - 1 & 6) + 6
- // For low-res: textWidth + (0 & 6) + 6 = textWidth + 6
+ // Selection highlight box - FUN_004292d0
+ if (i == selection) {
+ // Width: textWidth + ((DAT_0047a808 < 2) - 1 & 6) + 6 = textWidth + 6
int bracketWidth = textWidth + 6;
-
- // Height: ((DAT_0047a808 < 2) - 1 & 10) + 10
- // For low-res: (0 & 10) + 10 = 10
+ // Height: ((DAT_0047a808 < 2) - 1 & 10) + 10 = 10
int bracketHeight = 10;
- // Flash color (FUN_0041f5ae line 47):
- // (-((DAT_0047a7e4 & 1) == 0) & 8U) - 0x10
- // When mouse button NOT pressed (bit 0 == 0): (-1 & 8) - 16 = 8 - 16 = -8 = 248
- // When mouse button pressed (bit 0 == 1): (0 & 8) - 16 = -16 = 240
+ // Flash color: (-((DAT_0047a7e4 & 1) == 0) & 8U) - 0x10
+ // bit0==0: 8-16=248(0xF8), bit0==1: 0-16=240(0xF0)
static int frameCounter = 0;
frameCounter++;
byte highlightColor = ((frameCounter / 8) & 1) ? 248 : 240;
- // Box position (FUN_0041f5ae lines 40, 45-46):
- // X: centerX - sVar2 / 2
- // Y: sVar1 = param_3 * -5 + local_c * 10 + 0x67 (one pixel above text)
+ // Box position: X centered, Y = itemY - 1 (0x67 vs 0x68)
int leftX = centerX - bracketWidth / 2;
int rightX = centerX + bracketWidth / 2;
- int topY = itemY - 1; // 0x67 vs 0x68 = 1 pixel difference
+ int topY = itemY - 1;
int bottomY = itemY + bracketHeight - 1;
- // Clamp to screen bounds
int screenW = _vm->_screenWidth;
int screenH = _vm->_screenHeight;
if (leftX < 0) leftX = 0;
@@ -6035,30 +5891,59 @@ void InsaneRebel2::drawMenuOverlay(byte *renderBitmap, int pitch, int width, int
if (topY < 0) topY = 0;
if (bottomY >= screenH) bottomY = screenH - 1;
- // =====================================================================
- // FUN_004292d0 - Rectangle Drawer (Address: 0x4292D0)
- // =====================================================================
- // Draws rectangle border as 4 lines:
- // FUN_004290d0(param_1, param_2, x, y, width, color); // Top horizontal
- // FUN_004291d0(param_1, param_2, x, y, height, color); // Left vertical
- // FUN_004291d0(param_1, param_2, x+w-1, y, height, color); // Right vertical
- // FUN_004290d0(param_1, param_2, x, y+h-1, width, color); // Bottom horizontal
+ // FUN_004292d0 - Draw rectangle border (4 lines)
for (int x = leftX; x <= rightX && x < screenW; x++) {
if (topY >= 0 && topY < screenH)
- renderBitmap[topY * actualPitch + x] = highlightColor; // Top line
+ renderBitmap[topY * actualPitch + x] = highlightColor;
if (bottomY >= 0 && bottomY < screenH)
- renderBitmap[bottomY * actualPitch + x] = highlightColor; // Bottom line
+ renderBitmap[bottomY * actualPitch + x] = highlightColor;
}
for (int py = topY; py <= bottomY && py < screenH; py++) {
if (leftX >= 0 && leftX < screenW)
- renderBitmap[py * actualPitch + leftX] = highlightColor; // Left line
+ renderBitmap[py * actualPitch + leftX] = highlightColor;
if (rightX >= 0 && rightX < screenW)
- renderBitmap[py * actualPitch + rightX] = highlightColor; // Right line
+ renderBitmap[py * actualPitch + rightX] = highlightColor;
}
}
}
}
+void InsaneRebel2::drawMenuOverlay(byte *renderBitmap, int pitch, int width, int height) {
+ // =====================================================================
+ // Main menu renderer - calls shared drawMenuItems()
+ // Emulates FUN_004147b2 -> FUN_0041f5ae with param_3=7, param_4=0
+ // =====================================================================
+ //
+ // Menu strings loaded from GAME.TRS (keyboard mode indices 10-17):
+ // TRS index 10: "^f02Game Main Menu" -> Title (uses TITLFONT)
+ // TRS index 11: "^f01^c005Start Game" -> Item 0 (uses SMALFONT, color 5)
+ // TRS index 12: "^f01^c009Options" -> Item 1
+ // TRS index 13: "^f01^c009Calibrate Joystick" -> Item 2
+ // TRS index 14: "^f01^c009Continue Intro" -> Item 3
+ // TRS index 15: "^f01^c009Show Top Pilots" -> Item 4
+ // TRS index 16: "^f01^c009Show Credits" -> Item 5
+ // TRS index 17: "^f01^c240Return to Launcher" -> Item 6 (color 240)
+
+ SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
+ if (!splayer) {
+ debug(1, "drawMenuOverlay: SmushPlayer not available for TRS strings!");
+ return;
+ }
+
+ // Load TRS strings 10-17 (title + 7 selectable items)
+ const char *menuItems[8];
+ for (int i = 0; i < 8; i++) {
+ menuItems[i] = splayer->getString(10 + i);
+ if (!menuItems[i] || !menuItems[i][0]) {
+ debug(1, "drawMenuOverlay: TRS string %d not found!", 10 + i);
+ menuItems[i] = "";
+ }
+ }
+
+ // FUN_004147b2 line 25: param_3 = (DAT_0047a806 == 0) + 6 = 7 (keyboard mode)
+ drawMenuItems(renderBitmap, pitch, width, height, menuItems, 7, _menuSelection);
+}
+
// ======================= Pause Overlay =======================
// Emulates FUN_405A21 pause rendering (lines 242-305)
// Creates a dimmed overlay effect and displays "PAUSED" text
@@ -7201,7 +7086,7 @@ int InsaneRebel2::runLevelSelect() {
// Item 8: COPY PILOT
// Item 9: MAIN MENU
_levelSelection = 0;
- _levelItemCount = 10; // Selectable items: 6 pilots + 4 options
+ _levelItemCount = 4; // Selectable items: 0 saved pilots + 4 fixed options (NEW/DUPE/DELETE/MAIN MENU)
_selectedLevel = 1;
_menuRepeatDelay = 0;
_gameState = kStatePilotSelect;
@@ -7280,11 +7165,9 @@ int InsaneRebel2::processLevelSelectInput() {
int result = -1;
- // Level menu Y positions (similar to main menu)
- // Using same formula: base Y = numItemsTotal * -5 + 104
- // numItemsTotal = 3 (title + 2 selectable items)
- const int numItemsTotal = 3;
- const int baseY = numItemsTotal * -5 + 104; // 89
+ // Level menu Y positions â must match drawMenuItems() formula
+ // itemBaseY = numItems * -5 + 0x68
+ const int baseY = _levelItemCount * -5 + 0x68;
const int itemHeight = 10;
// Process events from the queue (populated by notifyEvent)
@@ -7370,168 +7253,63 @@ int InsaneRebel2::processLevelSelectInput() {
}
void InsaneRebel2::drawLevelSelectOverlay(byte *renderBitmap, int pitch, int width, int height) {
- // Draw level selection menu overlay
- // Emulates FUN_0041f5ae for level selection mode
+ // =====================================================================
+ // Pilot selection menu renderer - Emulates FUN_00414A41
+ // =====================================================================
//
- // From info.md - Low Resolution Coordinate Formulas (320x200 mode):
- // Center X = 160, Title Y = numItems * -5 + 81, Item Base Y = numItems * -5 + 104
- // Item spacing = 10 pixels, Selection box: width = textWidth + 6, height = 10
-
- // Frame counter for flashing selection box (emulates DAT_0047a7e4)
- static int frameCounter = 0;
- frameCounter++;
+ // FUN_00414A41 builds the menu dynamically:
+ // items[0] = DAT_004573b8[0] = TRS 20 (title "PILOTS")
+ // items[1..N] = saved pilot slot strings (dynamically formatted)
+ // items[N+1] = DAT_004573b8[1] = TRS 21 (ADD NEW PILOT)
+ // items[N+2] = DAT_004573b8[2] = TRS 22 (DUPE PILOT)
+ // items[N+3] = DAT_004573b8[3] = TRS 23 (DELETE PILOT)
+ // items[N+4] = DAT_004573b8[4] = TRS 24 (RETURN TO MAIN MENU)
+ //
+ // FUN_0041f5ae called with param_3 = N + 4 (selectable items)
+ //
+ // TRS 25+ are NOT menu items â they are info format strings
+ // ("DIFFICULTY: %S", "CHAPTER: %HO") rendered separately by FUN_00434cb0
+ // at fixed coordinates when a saved pilot is selected.
- // Pilot selection menu items - matches original structure from FUN_00414A41:
- // Load strings from GAME.TRS via SmushPlayer (indices from FUN_00414073)
- // TRS indices 0x14-0x23 (20-35) are stored at DAT_004573b8
- // Items 0-5: Pilot save slots (would come from save data, using TRS placeholder)
- // Item 6: NEW PILOT (TRS index 20+6=26 or similar)
- // Item 7: DELETE PILOT
- // Item 8: COPY PILOT
- // Item 9: MAIN MENU
SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
- const char *pilotItems[11];
-
if (!splayer) {
debug(1, "drawLevelSelectOverlay: SmushPlayer not available for TRS strings!");
return;
}
- // TRS index mapping from FUN_00414073:
- // DAT_004573b8[0-15] = TRS indices 20-35
- // Title: TRS 20, Pilot slots use save data or TRS template, Options: TRS 26-29 or similar
- // Load from TRS indices 20-30 for the menu structure
- for (int i = 0; i < 11; i++) {
- pilotItems[i] = splayer->getString(20 + i);
- if (!pilotItems[i] || !pilotItems[i][0]) {
- debug(1, "drawLevelSelectOverlay: TRS string %d not found!", 20 + i);
- pilotItems[i] = "";
- }
- }
-
- const int numItemsTotal = 11; // Title + 10 selectable items
- const int numSelectableItems = 10;
-
- // Calculate positions (low-res 320x200 mode)
- // Formula from FUN_0041F5AE: titleY = numItems * -5 + 81, itemBaseY = numItems * -5 + 104
- const int centerX = width / 2; // 160 for 320 width
- const int titleY = numSelectableItems * -5 + 81; // 10 * -5 + 81 = 31
- const int itemBaseY = numSelectableItems * -5 + 104; // 10 * -5 + 104 = 54
- const int itemSpacing = 10;
-
- NutRenderer *font = _smush_smalfontNut;
- if (!font) {
- debug(1, "drawLevelSelectOverlay: font is NULL!");
- return;
- }
-
- int numFontChars = font->getNumChars();
- int actualPitch = pitch;
- Common::Rect clipRect(0, 0, width, height);
-
- // Helper function to draw centered text with optional highlight box
- // We'll use a simple loop instead of lambda for compatibility
- auto drawTextCentered = [&](const char *text, int y, bool highlight) {
- // Calculate text width first
- int textWidth = 0;
- for (const char *c = text; *c; c++) {
- int charIdx = (unsigned char)*c;
- if (charIdx < numFontChars) {
- textWidth += font->getCharWidth(charIdx);
- }
- }
+ // Number of saved pilots (TODO: implement save system, 0 for now)
+ int numPilots = 0;
- int curX = centerX - textWidth / 2;
+ // Build menu item array: title + [pilot slots] + 4 fixed options
+ // Max: 1 title + 6 pilots + 4 options = 11
+ const char *pilotItems[11];
+ int idx = 0;
- // Draw each character using drawCharV7
- for (const char *c = text; *c; c++) {
- int charIdx = (unsigned char)*c;
- if (charIdx < numFontChars) {
- int charWidth = font->getCharWidth(charIdx);
- if (curX >= 0 && curX + charWidth <= width && y >= 0) {
- // Use drawCharV7 with color -1 (original colors), hardcodedColors=true, smushColorMode=true
- font->drawCharV7(renderBitmap, clipRect, curX, y, actualPitch, -1,
- kStyleAlignLeft, charIdx, true, true);
- }
- curX += charWidth;
- }
- }
+ // Title: TRS 20
+ pilotItems[idx++] = splayer->getString(20);
- // If highlighted, draw selection box around text
- if (highlight) {
- // Box dimensions from FUN_0041F5AE:
- // Width = textWidth + 6 (low-res), Height = 10
- int boxWidth = textWidth + 6;
- int boxHeight = 10;
- int boxX = centerX - boxWidth / 2;
- int boxY = y - 1; // 1 pixel above text (itemY - 1 = numItems * -5 + idx * 10 + 103)
+ // Saved pilot slots would be inserted here (items[1..numPilots])
+ // Each formatted as "^f01^c005<name>^f00" by the original
- // Flashing highlight color (emulates original behavior)
- // Original uses palette-relative colors -8 and -16 alternating on frameCounter & 1
- // We use bright colors that approximate the visual effect
- byte highlightColor = (frameCounter & 1) ? 0xF8 : 0xF0;
+ // Fixed options: TRS 21-24
+ for (int i = 0; i < 4; i++) {
+ pilotItems[idx++] = splayer->getString(21 + i);
+ }
- // Draw box border (top, bottom, left, right edges)
- if (boxY >= 0 && boxY < height && boxX >= 0 && boxX + boxWidth <= width) {
- // Top edge
- for (int px = boxX; px < boxX + boxWidth && px < width; px++) {
- if (px >= 0) renderBitmap[boxY * actualPitch + px] = highlightColor;
- }
- // Bottom edge
- int bottomY = boxY + boxHeight - 1;
- if (bottomY < height) {
- for (int px = boxX; px < boxX + boxWidth && px < width; px++) {
- if (px >= 0) renderBitmap[bottomY * actualPitch + px] = highlightColor;
- }
- }
- // Left edge
- for (int py = boxY; py < boxY + boxHeight && py < height; py++) {
- if (py >= 0 && boxX >= 0) renderBitmap[py * actualPitch + boxX] = highlightColor;
- }
- // Right edge
- int rightX = boxX + boxWidth - 1;
- if (rightX < width) {
- for (int py = boxY; py < boxY + boxHeight && py < height; py++) {
- if (py >= 0) renderBitmap[py * actualPitch + rightX] = highlightColor;
- }
- }
- }
+ // Validate strings
+ for (int i = 0; i < idx; i++) {
+ if (!pilotItems[i] || !pilotItems[i][0]) {
+ pilotItems[i] = "";
}
- };
+ }
- // Draw title (not selectable)
- drawTextCentered(pilotItems[0], titleY, false);
+ int numSelectableItems = numPilots + 4;
+ drawMenuItems(renderBitmap, pitch, width, height, pilotItems, numSelectableItems, _levelSelection);
- // Draw selectable items
- for (int i = 0; i < numSelectableItems; i++) {
- int itemY = itemBaseY + i * itemSpacing;
- bool isSelected = (i == _levelSelection);
- drawTextCentered(pilotItems[i + 1], itemY, isSelected);
- }
-
- // Draw info text at bottom if a pilot slot is selected (items 0-5)
- // From FUN_00414A41: info is shown when local_18 < local_10 (selection < num_pilots)
- if (_levelSelection >= 0 && _levelSelection <= 5) {
- // Show difficulty or score info for selected pilot
- // From info.md: Info displayed at X=30, Y=180
- const char *pilotInfoText = "DIFFICULTY: EASY";
- int infoY = 180;
- int infoX = 30;
-
- // Draw left-aligned text using drawCharV7
- int curX = infoX;
- for (const char *c = pilotInfoText; *c; c++) {
- int charIdx = (unsigned char)*c;
- if (charIdx < numFontChars) {
- int charWidth = font->getCharWidth(charIdx);
- if (curX >= 0 && curX + charWidth <= width && infoY >= 0) {
- font->drawCharV7(renderBitmap, clipRect, curX, infoY, actualPitch, -1,
- kStyleAlignLeft, charIdx, true, true);
- }
- curX += charWidth;
- }
- }
- }
+ // When a saved pilot is selected, show info at fixed coordinates
+ // FUN_00414A41 lines 78-86: FUN_00434cb0 at Y=0xb4(180) and Y=0xbe(190)
+ // DAT_004573cc = TRS 25 ("DIFFICULTY: %S"), DAT_004573d0 = TRS 26 ("CHAPTER: %HO")
+ // TODO: Implement pilot info display when save system is added
}
// ======================= Level Loading System =======================
diff --git a/engines/scumm/insane/insane_rebel.h b/engines/scumm/insane/insane_rebel.h
index ba460e2fd8f..ac542e841da 100644
--- a/engines/scumm/insane/insane_rebel.h
+++ b/engines/scumm/insane/insane_rebel.h
@@ -95,6 +95,11 @@ public:
// Process menu input (keyboard/mouse) - returns selected item or -1
int processMenuInput();
+ // Shared menu item renderer - emulates FUN_0041F5AE param_4==0 (centered mode)
+ // items[0] = title, items[1..numItems] = selectable items, selection = highlighted item
+ void drawMenuItems(byte *renderBitmap, int pitch, int width, int height,
+ const char **items, int numItems, int selection);
+
// Draw menu overlay (selection highlight) on current frame
void drawMenuOverlay(byte *renderBitmap, int pitch, int width, int height);
Commit: 267ddb67fe32eb6cce59ae6b8edd78f49ba988e0
https://github.com/scummvm/scummvm/commit/267ddb67fe32eb6cce59ae6b8edd78f49ba988e0
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:03+02:00
Commit Message:
SCUMM: RA2: Implement difficulty selection menu
Changed paths:
engines/scumm/insane/insane_rebel.cpp
engines/scumm/insane/insane_rebel.h
diff --git a/engines/scumm/insane/insane_rebel.cpp b/engines/scumm/insane/insane_rebel.cpp
index 769f0c56ab0..f3b98fc56bd 100644
--- a/engines/scumm/insane/insane_rebel.cpp
+++ b/engines/scumm/insane/insane_rebel.cpp
@@ -396,6 +396,7 @@ InsaneRebel2::InsaneRebel2(ScummEngine_v7 *scumm) {
_levelSelection = 0; // First item selected
_levelItemCount = 4; // 0 saved pilots + 4 fixed options
_selectedLevel = 1; // Default selected level
+ _difficultySelection = 2; // Default to 3rd difficulty (matching original init param_3=2)
// Initialize menu input capture system
_menuInputActive = false;
@@ -470,6 +471,7 @@ bool InsaneRebel2::notifyEvent(const Common::Event &event) {
if (splayer) {
if (_menuInputActive && (_gameState == kStateMainMenu ||
_gameState == kStatePilotSelect ||
+ _gameState == kStateDifficultySelect ||
_gameState == kStateChapterSelect)) {
// In menu mode: Select quit option and confirm selection
// This emulates the assembly behavior from FUN_0041f5ae
@@ -3876,7 +3878,7 @@ void InsaneRebel2::procPostRendering(byte *renderBitmap, int32 codecparam, int32
// Check if we're in menu mode (menu state + intro flag)
bool menuMode = (introPlaying && _gameState == kStateMainMenu);
- bool pilotSelectMode = (introPlaying && _gameState == kStatePilotSelect);
+ bool pilotSelectMode = (introPlaying && (_gameState == kStatePilotSelect || _gameState == kStateDifficultySelect));
bool chapterSelectMode = (introPlaying && _gameState == kStateChapterSelect);
// Handle pilot selection input and rendering (FUN_00414A41)
@@ -7068,25 +7070,28 @@ void InsaneRebel2::drawChapterSelectOverlay(byte *renderBitmap, int pitch, int w
int InsaneRebel2::runLevelSelect() {
// Pilot selection menu loop - emulates FUN_00414A41
// Returns:
- // kLevelSelectPlay (1) = Go to chapter selection
- // kLevelSelectBack (0) = Return to main menu
+ // kLevelSelectPlay (1) = Go to chapter selection (pilot selected or NEW+difficulty chosen)
+ // kLevelSelectBack (0) = Return to main menu (MAIN MENU or ESC)
// kLevelSelectQuit (2) = Quit game
+ //
+ // Original action dispatch (FUN_00414A41):
+ // sel < N â saved pilot selected â return 3 (start game)
+ // sel == N â ADD NEW PILOT â difficulty submenu â loop back
+ // sel == N+1 â COPY PILOT â source select (no-op if N==0)
+ // sel == N+2 â DELETE PILOT â confirm select (no-op if N==0)
+ // sel == N+3 â RETURN TO MAIN MENU â return 1
+ // ESC â return 1
debug("Rebel2: Entering pilot selection (FUN_00414A41)");
- // Enable menu input capture via EventObserver and clear any stale events
_menuInputActive = true;
while (!_menuEventQueue.empty()) _menuEventQueue.pop();
- // Initialize pilot selection state
- // Menu structure from FUN_00414A41:
- // Items 0-5: Pilot slots (PILOT 1-6)
- // Item 6: NEW PILOT
- // Item 7: DELETE PILOT
- // Item 8: COPY PILOT
- // Item 9: MAIN MENU
+ // Number of saved pilots (TODO: implement save system)
+ int numPilots = 0;
+
_levelSelection = 0;
- _levelItemCount = 4; // Selectable items: 0 saved pilots + 4 fixed options (NEW/DUPE/DELETE/MAIN MENU)
+ _levelItemCount = numPilots + 4; // N pilots + NEW/COPY/DELETE/MAIN MENU
_selectedLevel = 1;
_menuRepeatDelay = 0;
_gameState = kStatePilotSelect;
@@ -7114,38 +7119,53 @@ int InsaneRebel2::runLevelSelect() {
_vm->_smushVideoShouldFinish = false;
- debug("Rebel2: Pilot selection made: %d", _levelSelection);
-
- // Process pilot selection - all options go to chapter selection except MAIN MENU
- // Menu items (from FUN_00414A41):
- // 0-5: Pilot slots
- // 6: NEW PILOT
- // 7: DELETE PILOT
- // 8: COPY PILOT
- // 9: MAIN MENU (back)
- if (_levelSelection >= 0 && _levelSelection <= 5) {
- // Pilot selected - go to chapter selection
- _selectedLevel = _levelSelection + 1;
- debug("Rebel2: Pilot %d selected - going to chapter selection", _selectedLevel);
- _menuInputActive = false;
- return kLevelSelectPlay;
- } else if (_levelSelection == 6) {
- // NEW PILOT - go to chapter selection
- debug("Rebel2: NEW PILOT selected - going to chapter selection");
- _menuInputActive = false;
- return kLevelSelectPlay;
- } else if (_levelSelection == 7) {
- // DELETE PILOT - go to chapter selection
- debug("Rebel2: DELETE PILOT selected - going to chapter selection");
+ // Dispatch based on current game state
+ if (_gameState == kStateDifficultySelect) {
+ // Difficulty submenu â selection made
+ debug("Rebel2: Difficulty %d selected", _difficultySelection);
+ // Original stores difficulty in pilot record and loops back.
+ // Since we have no save system, proceed to chapter select.
+ _gameState = kStatePilotSelect;
_menuInputActive = false;
return kLevelSelectPlay;
- } else if (_levelSelection == 8) {
- // COPY PILOT - go to chapter selection
- debug("Rebel2: COPY PILOT selected - going to chapter selection");
+ }
+
+ // Pilot menu â process selection
+ debug("Rebel2: Pilot selection made: %d (numPilots=%d)", _levelSelection, numPilots);
+
+ if (_levelSelection < numPilots) {
+ // Saved pilot selected â go to chapter selection
+ _selectedLevel = _levelSelection + 1;
+ debug("Rebel2: Pilot %d selected - going to chapter selection", _selectedLevel);
_menuInputActive = false;
return kLevelSelectPlay;
- } else if (_levelSelection == 9) {
- // Main Menu (back)
+ } else if (_levelSelection == numPilots) {
+ // ADD NEW PILOT â show difficulty submenu
+ debug("Rebel2: ADD NEW PILOT - showing difficulty submenu");
+ _gameState = kStateDifficultySelect;
+ _difficultySelection = 2; // Default to 3rd option (matching original init)
+ // Continue loop â next video frame will render difficulty menu
+ continue;
+ } else if (_levelSelection == numPilots + 1) {
+ // COPY PILOT â no-op when numPilots==0 (original checks local_10 != 0)
+ if (numPilots > 0) {
+ debug("Rebel2: COPY PILOT selected");
+ // TODO: implement copy pilot sub-flow
+ } else {
+ debug("Rebel2: COPY PILOT - no pilots to copy");
+ }
+ continue;
+ } else if (_levelSelection == numPilots + 2) {
+ // DELETE PILOT â no-op when numPilots==0 (original checks local_10 != 0)
+ if (numPilots > 0) {
+ debug("Rebel2: DELETE PILOT selected");
+ // TODO: implement delete pilot sub-flow
+ } else {
+ debug("Rebel2: DELETE PILOT - no pilots to delete");
+ }
+ continue;
+ } else if (_levelSelection == numPilots + 3) {
+ // RETURN TO MAIN MENU
debug("Rebel2: Back to main menu selected");
_menuInputActive = false;
return kLevelSelectBack;
@@ -7157,53 +7177,65 @@ int InsaneRebel2::runLevelSelect() {
}
int InsaneRebel2::processLevelSelectInput() {
- // Process input for level selection menu
- // Similar to processMenuInput but for level selection
+ // Process input for pilot selection and difficulty submenu
+ // Handles both kStatePilotSelect and kStateDifficultySelect modes
// Returns: -1 = no action, 0+ = item selected
- //
- // Events are captured by notifyEvent() - see processMenuInput for details.
int result = -1;
- // Level menu Y positions â must match drawMenuItems() formula
- // itemBaseY = numItems * -5 + 0x68
- const int baseY = _levelItemCount * -5 + 0x68;
+ // Determine which menu mode we're in
+ bool isDifficultyMode = (_gameState == kStateDifficultySelect);
+ int &selection = isDifficultyMode ? _difficultySelection : _levelSelection;
+ int itemCount = isDifficultyMode ? 6 : _levelItemCount;
+
+ // Mouse hit Y positions â must match drawMenuItems() formula
+ const int baseY = itemCount * -5 + 0x68;
const int itemHeight = 10;
- // Process events from the queue (populated by notifyEvent)
while (!_menuEventQueue.empty()) {
Common::Event event = _menuEventQueue.pop();
switch (event.type) {
case Common::EVENT_KEYDOWN:
switch (event.kbd.keycode) {
case Common::KEYCODE_UP:
- _levelSelection--;
- if (_levelSelection < 0) {
- _levelSelection = _levelItemCount - 1;
+ selection--;
+ if (selection < 0) {
+ selection = itemCount - 1;
}
- debug("LevelSelect: Selection changed to %d (UP)", _levelSelection);
+ debug("LevelSelect: Selection changed to %d (UP, %s)",
+ selection, isDifficultyMode ? "difficulty" : "pilot");
break;
case Common::KEYCODE_DOWN:
- _levelSelection++;
- if (_levelSelection >= _levelItemCount) {
- _levelSelection = 0;
+ selection++;
+ if (selection >= itemCount) {
+ selection = 0;
}
- debug("LevelSelect: Selection changed to %d (DOWN)", _levelSelection);
+ debug("LevelSelect: Selection changed to %d (DOWN, %s)",
+ selection, isDifficultyMode ? "difficulty" : "pilot");
break;
case Common::KEYCODE_RETURN:
case Common::KEYCODE_KP_ENTER:
- if (_levelSelection >= 0 && _levelSelection < _levelItemCount) {
- result = _levelSelection;
- debug("LevelSelect: Item %d selected (ENTER)", _levelSelection);
+ if (selection >= 0 && selection < itemCount) {
+ result = selection;
+ debug("LevelSelect: Item %d selected (ENTER, %s)",
+ selection, isDifficultyMode ? "difficulty" : "pilot");
}
break;
case Common::KEYCODE_ESCAPE:
- // ESC - Back to main menu
- result = _levelItemCount - 1; // Last item is "MAIN MENU"
- debug("LevelSelect: ESC pressed - back to menu");
+ if (isDifficultyMode) {
+ // ESC in difficulty submenu â return to pilot menu
+ // Note: original has no ESC in difficulty submenu, but we add it as a
+ // graceful fallback
+ _gameState = kStatePilotSelect;
+ debug("LevelSelect: ESC in difficulty - back to pilot menu");
+ } else {
+ // ESC in pilot menu â back to main menu (return 1 in original)
+ result = _levelItemCount - 1; // Last item = MAIN MENU
+ debug("LevelSelect: ESC pressed - back to main menu");
+ }
break;
default:
@@ -7213,17 +7245,14 @@ int InsaneRebel2::processLevelSelectInput() {
case Common::EVENT_LBUTTONDOWN:
{
- // Get mouse position from the event
- int mouseX = event.mouse.x;
int mouseY = event.mouse.y;
+ debug("LevelSelect: Click at Y=%d (%s)", mouseY,
+ isDifficultyMode ? "difficulty" : "pilot");
- debug("LevelSelect: Click detected at (%d, %d)", mouseX, mouseY);
-
- for (int i = 0; i < _levelItemCount; i++) {
+ for (int i = 0; i < itemCount; i++) {
int itemY = baseY + i * itemHeight;
- // Use a larger vertical hit area (full item height)
if (mouseY >= itemY - 4 && mouseY < itemY + 6) {
- _levelSelection = i;
+ selection = i;
result = i;
debug("LevelSelect: Item %d clicked at Y=%d (itemY=%d)", i, mouseY, itemY);
break;
@@ -7233,15 +7262,17 @@ int InsaneRebel2::processLevelSelectInput() {
break;
case Common::EVENT_MOUSEMOVE:
- // Update mouse position for hover effects
_vm->_mouse.x = event.mouse.x;
_vm->_mouse.y = event.mouse.y;
break;
case Common::EVENT_QUIT:
case Common::EVENT_RETURN_TO_LAUNCHER:
- // Handle quit request - go back to main menu
- result = _levelItemCount - 1;
+ if (isDifficultyMode) {
+ _gameState = kStatePilotSelect;
+ } else {
+ result = _levelItemCount - 1;
+ }
break;
default:
@@ -7254,22 +7285,9 @@ int InsaneRebel2::processLevelSelectInput() {
void InsaneRebel2::drawLevelSelectOverlay(byte *renderBitmap, int pitch, int width, int height) {
// =====================================================================
- // Pilot selection menu renderer - Emulates FUN_00414A41
+ // Pilot selection / difficulty submenu renderer
+ // Emulates FUN_00414A41 â FUN_0041f5ae
// =====================================================================
- //
- // FUN_00414A41 builds the menu dynamically:
- // items[0] = DAT_004573b8[0] = TRS 20 (title "PILOTS")
- // items[1..N] = saved pilot slot strings (dynamically formatted)
- // items[N+1] = DAT_004573b8[1] = TRS 21 (ADD NEW PILOT)
- // items[N+2] = DAT_004573b8[2] = TRS 22 (DUPE PILOT)
- // items[N+3] = DAT_004573b8[3] = TRS 23 (DELETE PILOT)
- // items[N+4] = DAT_004573b8[4] = TRS 24 (RETURN TO MAIN MENU)
- //
- // FUN_0041f5ae called with param_3 = N + 4 (selectable items)
- //
- // TRS 25+ are NOT menu items â they are info format strings
- // ("DIFFICULTY: %S", "CHAPTER: %HO") rendered separately by FUN_00434cb0
- // at fixed coordinates when a saved pilot is selected.
SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
if (!splayer) {
@@ -7277,11 +7295,36 @@ void InsaneRebel2::drawLevelSelectOverlay(byte *renderBitmap, int pitch, int wid
return;
}
- // Number of saved pilots (TODO: implement save system, 0 for now)
- int numPilots = 0;
+ if (_gameState == kStateDifficultySelect) {
+ // =====================================================================
+ // Difficulty submenu - LAB_00414ff6
+ // FUN_0041f5ae(0, &DAT_00457458, 6, 0)
+ // DAT_00457458 = 7 entries loaded from TRS 110-116 (FUN_00414073 lines 47-50)
+ // param_3 = 6 â items[0]=title(TRS 110), items[1..6]=selectable(TRS 111-116)
+ // =====================================================================
+ const char *diffItems[7];
+ for (int i = 0; i < 7; i++) {
+ diffItems[i] = splayer->getString(110 + i);
+ if (!diffItems[i] || !diffItems[i][0]) {
+ diffItems[i] = "";
+ }
+ }
+ drawMenuItems(renderBitmap, pitch, width, height, diffItems, 6, _difficultySelection);
+ return;
+ }
+
+ // =====================================================================
+ // Pilot menu - FUN_0041f5ae(0, &DAT_00457768, N+4, 0)
+ // =====================================================================
+ // items[0] = title (TRS 20)
+ // items[1..N] = saved pilots (formatted with ^f01^c005)
+ // items[N+1] = TRS 21 (ADD NEW PILOT)
+ // items[N+2] = TRS 22 (COPY PILOT)
+ // items[N+3] = TRS 23 (DELETE PILOT)
+ // items[N+4] = TRS 24 (RETURN TO MAIN MENU)
+
+ int numPilots = 0; // TODO: implement save system
- // Build menu item array: title + [pilot slots] + 4 fixed options
- // Max: 1 title + 6 pilots + 4 options = 11
const char *pilotItems[11];
int idx = 0;
@@ -7296,20 +7339,17 @@ void InsaneRebel2::drawLevelSelectOverlay(byte *renderBitmap, int pitch, int wid
pilotItems[idx++] = splayer->getString(21 + i);
}
- // Validate strings
for (int i = 0; i < idx; i++) {
if (!pilotItems[i] || !pilotItems[i][0]) {
pilotItems[i] = "";
}
}
- int numSelectableItems = numPilots + 4;
- drawMenuItems(renderBitmap, pitch, width, height, pilotItems, numSelectableItems, _levelSelection);
+ drawMenuItems(renderBitmap, pitch, width, height, pilotItems, numPilots + 4, _levelSelection);
- // When a saved pilot is selected, show info at fixed coordinates
+ // Pilot info display at fixed coordinates when saved pilot selected
// FUN_00414A41 lines 78-86: FUN_00434cb0 at Y=0xb4(180) and Y=0xbe(190)
- // DAT_004573cc = TRS 25 ("DIFFICULTY: %S"), DAT_004573d0 = TRS 26 ("CHAPTER: %HO")
- // TODO: Implement pilot info display when save system is added
+ // TODO: Implement when save system is added
}
// ======================= Level Loading System =======================
diff --git a/engines/scumm/insane/insane_rebel.h b/engines/scumm/insane/insane_rebel.h
index ac542e841da..e8ec2f4c3f6 100644
--- a/engines/scumm/insane/insane_rebel.h
+++ b/engines/scumm/insane/insane_rebel.h
@@ -66,7 +66,8 @@ public:
kStateChapterSelect = 3,// Stage 3: Chapter selection (FUN_00415CF8)
kStateGameplay = 4, // Stage 4: Gameplay (FUN_00416787)
kStateCredits = 5, // Credits sequence
- kStateQuit = 6 // Exit game
+ kStateQuit = 6, // Exit game
+ kStateDifficultySelect = 7 // Difficulty submenu within pilot select
};
// Menu selection results (return values from FUN_004147B2)
@@ -171,6 +172,7 @@ public:
int _levelSelection; // Current level selection (0-based)
int _levelItemCount; // Number of level items (levels + options)
int _selectedLevel; // Final selected level ID (1-15)
+ int _difficultySelection; // Current difficulty selection in submenu (0-based)
// Run pilot selection menu - emulates FUN_00414A41
int runLevelSelect();
Commit: 9b76b0247a38f481c2b45d2dc78fca0046f85868
https://github.com/scummvm/scummvm/commit/9b76b0247a38f481c2b45d2dc78fca0046f85868
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:03+02:00
Commit Message:
SCUMM: RA2: Improve level menu
Changed paths:
engines/scumm/insane/insane_rebel.cpp
engines/scumm/insane/insane_rebel.h
diff --git a/engines/scumm/insane/insane_rebel.cpp b/engines/scumm/insane/insane_rebel.cpp
index f3b98fc56bd..2577983d670 100644
--- a/engines/scumm/insane/insane_rebel.cpp
+++ b/engines/scumm/insane/insane_rebel.cpp
@@ -5710,25 +5710,36 @@ int InsaneRebel2::processMenuInput() {
}
void InsaneRebel2::drawMenuItems(byte *renderBitmap, int pitch, int width, int height,
- const char **items, int numItems, int selection) {
+ const char **items, int numItems, int selection,
+ bool leftAligned) {
// =====================================================================
- // Shared menu renderer - Emulates FUN_0041f5ae param_4==0 (centered mode)
+ // Shared menu renderer - Emulates FUN_0041f5ae
// Address: 0x41F5AE
// =====================================================================
//
// items[0] = title string, items[1..numItems] = selectable items
// numItems = number of selectable items (FUN_0041f5ae param_3)
// selection = currently highlighted item (0-based, maps to DAT_00459988)
+ // leftAligned = false: param_4==0 (centered), true: param_4==1 (left-aligned)
//
// Coordinate formulas from FUN_0041f5ae (low-res, DAT_0047a808 < 2):
- // Title Y: param_3 * -5 + 0x51 (line 19)
- // Item base Y: param_3 * -5 + 0x68 (line 28)
- // Item Y: param_3 * -5 + i * 10 + 0x68 (line 28)
- // Center X: ((DAT_0047a808 < 2) - 1 & 0xa0) + 0xa0 = 160
+ // Centered (param_4=0):
+ // Title X: center - titleWidth/2 (centerX = 160)
+ // Title Y: param_3 * -5 + 0x51
+ // Item X: center - textWidth/2
+ // Box X: center - bracketWidth/2
+ // Left-aligned (param_4=1):
+ // Title X: 0x28 = 40
+ // Title Y: param_3 * -5 + 0x56
+ // Item X: 0x17 = 23
+ // Box X: 0x14 = 20
+ // Both modes:
+ // Item base Y: param_3 * -5 + 0x68
+ // Item Y: param_3 * -5 + i * 10 + 0x68
// Box Y: param_3 * -5 + i * 10 + 0x67 (1px above text)
const int centerX = width / 2;
- const int titleY = numItems * -5 + 0x51;
+ const int titleY = numItems * -5 + (leftAligned ? 0x56 : 0x51);
const int itemBaseY = numItems * -5 + 0x68;
const int itemSpacing = 10;
@@ -5848,23 +5859,27 @@ void InsaneRebel2::drawMenuItems(byte *renderBitmap, int pitch, int width, int h
};
// =====================================================================
- // Draw title - items[0], centered at titleY
+ // Draw title - items[0]
+ // Centered: X = center - titleWidth/2
+ // Left-aligned: X = 40 (0x28)
// =====================================================================
{
int titleWidth = getStringWidth(items[0]);
- int titleX = centerX - titleWidth / 2;
+ int titleX = leftAligned ? 40 : (centerX - titleWidth / 2);
drawString(items[0], titleX, titleY);
}
// =====================================================================
// Draw selectable items with selection highlight box
+ // Centered: item X = center - textWidth/2, box X = center - bracketWidth/2
+ // Left-aligned: item X = 23 (0x17), box X = 20 (0x14)
// =====================================================================
for (int i = 0; i < numItems; i++) {
int itemY = itemBaseY + i * itemSpacing;
const char *text = items[i + 1];
int textWidth = getStringWidth(text);
- int textX = centerX - textWidth / 2;
+ int textX = leftAligned ? 23 : (centerX - textWidth / 2);
drawString(text, textX, itemY);
// Selection highlight box - FUN_004292d0
@@ -5880,9 +5895,9 @@ void InsaneRebel2::drawMenuItems(byte *renderBitmap, int pitch, int width, int h
frameCounter++;
byte highlightColor = ((frameCounter / 8) & 1) ? 248 : 240;
- // Box position: X centered, Y = itemY - 1 (0x67 vs 0x68)
- int leftX = centerX - bracketWidth / 2;
- int rightX = centerX + bracketWidth / 2;
+ // Box position: Y = itemY - 1 (0x67 vs 0x68)
+ int leftX = leftAligned ? 20 : (centerX - bracketWidth / 2);
+ int rightX = leftX + bracketWidth;
int topY = itemY - 1;
int bottomY = itemY + bracketHeight - 1;
@@ -6264,8 +6279,13 @@ int InsaneRebel2::runChapterSelect() {
while (!_menuEventQueue.empty()) _menuEventQueue.pop();
// Initialize chapter selection state
- _chapterSelection = 0;
- _chapterItemCount = 17; // 16 chapters + BACK
+ // Original (lines 51-54): local_10 = 0xf; while (local_10 > 0 && locked) local_10--;
+ // Finds highest unlocked chapter. With debug unlock all = 15 (FINALE).
+ _chapterSelection = 15;
+ while (_chapterSelection > 0 && !_chapterUnlocked[_chapterSelection]) {
+ _chapterSelection--;
+ }
+ _chapterItemCount = 17; // 16 chapters + RETURN TO PILOTS
_selectedChapter = 0;
_passwordInput = "";
_menuRepeatDelay = 0;
@@ -6399,41 +6419,20 @@ int InsaneRebel2::processChapterSelectInput() {
case Common::EVENT_LBUTTONDOWN:
{
// Mouse click - check if clicking on a menu item
- // From FUN_0041F5AE assembly (low-res 320x200):
+ // From FUN_0041F5AE (low-res, left-aligned):
// Item Y = 17 * -5 + i * 10 + 104 = 19 + i * 10
- // Chapters 0-14 at Y = 19 + i*10
- // FINALE (15) at Y = 19 + 15*10 = 169
- // RETURN TO PILOTS (16) at Y = 19 + 16*10 = 179
- int baseY = 19;
- int itemHeight = 10;
+ int baseY = _chapterItemCount * -5 + 0x68; // = 19
int mouseY = event.mouse.y;
- // Check chapters 0-14
- for (int i = 0; i < 15; i++) {
- int itemY = baseY + i * itemHeight;
+ for (int i = 0; i < _chapterItemCount; i++) {
+ int itemY = baseY + i * 10;
if (mouseY >= itemY - 4 && mouseY < itemY + 6) {
_chapterSelection = i;
result = i;
- debug("ChapterSelect: Chapter %d clicked at Y=%d", i + 1, mouseY);
+ debug("ChapterSelect: Item %d clicked at Y=%d", i, mouseY);
break;
}
}
-
- // Check FINALE (index 15) at Y=169
- int finaleY = baseY + 15 * itemHeight;
- if (mouseY >= finaleY - 4 && mouseY < finaleY + 6) {
- _chapterSelection = 15;
- result = 15;
- debug("ChapterSelect: FINALE clicked");
- }
-
- // Check RETURN TO PILOTS (index 16) at Y=179
- int returnY = baseY + 16 * itemHeight;
- if (mouseY >= returnY - 4 && mouseY < returnY + 6) {
- _chapterSelection = 16;
- result = 16;
- debug("ChapterSelect: RETURN TO PILOTS clicked");
- }
}
break;
@@ -6631,140 +6630,39 @@ void InsaneRebel2::drawPreviewThumbnail(byte *renderBitmap, int pitch, int width
}
}
-// Draw left-aligned menu item - emulates FUN_0041F5AE with param_4=1
-void InsaneRebel2::drawLeftAlignedMenuItem(byte *renderBitmap, int pitch, int width, int height,
- const char *text, int x, int y, bool selected) {
- NutRenderer *font = _smush_smalfontNut;
- if (!font) return;
-
- int numFontChars = font->getNumChars();
- Common::Rect clipRect(0, 0, width, height);
-
- // Calculate text width for selection box
- int textWidth = 0;
- for (const char *c = text; *c; c++) {
- int charIdx = (unsigned char)*c;
- if (charIdx < numFontChars) {
- textWidth += font->getCharWidth(charIdx);
- }
- }
-
- // Draw selection box if selected
- // Box dimensions: width = textWidth + 6, height = 10 (low-res)
- if (selected) {
- static int frameCounter = 0;
- frameCounter++;
-
- int boxWidth = textWidth + 6;
- int boxHeight = 10;
- int boxX = x - 3; // Left-aligned, so box starts 3 pixels before text
- int boxY = y - 1;
+// Draw score/info line at bottom of chapter select - emulates FUN_00434cb0 calls
+// For unlocked chapters: score display using TRS 80 at (25, 190)
+// For locked chapters: password prompt at (30, 190)
+void InsaneRebel2::drawChapterInfoLine(byte *renderBitmap, int pitch, int width, int height) {
+ if (_chapterSelection < 0 || _chapterSelection >= 16) return;
- // Flashing color (emulates (-((DAT_0047a7e4 & 1) == 0) & 8U) - 0x10)
- byte highlightColor = (frameCounter & 1) ? 0xF8 : 0xF0;
-
- // Draw box border
- if (boxY >= 0 && boxY < height && boxX >= 0) {
- // Top edge
- for (int px = boxX; px < boxX + boxWidth && px < width; px++) {
- if (px >= 0) renderBitmap[boxY * pitch + px] = highlightColor;
- }
- // Bottom edge
- int bottomY = boxY + boxHeight - 1;
- if (bottomY < height) {
- for (int px = boxX; px < boxX + boxWidth && px < width; px++) {
- if (px >= 0) renderBitmap[bottomY * pitch + px] = highlightColor;
- }
- }
- // Left edge
- for (int py = boxY; py < boxY + boxHeight && py < height; py++) {
- if (py >= 0 && boxX >= 0) renderBitmap[py * pitch + boxX] = highlightColor;
- }
- // Right edge
- int rightX = boxX + boxWidth - 1;
- if (rightX < width) {
- for (int py = boxY; py < boxY + boxHeight && py < height; py++) {
- if (py >= 0) renderBitmap[py * pitch + rightX] = highlightColor;
- }
- }
- }
- }
-
- // Draw text characters
- int curX = x;
- for (const char *c = text; *c; c++) {
- int charIdx = (unsigned char)*c;
- if (charIdx < numFontChars) {
- int charWidth = font->getCharWidth(charIdx);
- if (curX >= 0 && curX + charWidth <= width && y >= 0 && y < height) {
- font->drawCharV7(renderBitmap, clipRect, curX, y, pitch, -1,
- kStyleAlignLeft, charIdx, true, true);
- }
- curX += charWidth;
- }
- }
-}
-
-// Draw password input field - emulates lines 106-125 of FUN_00415CF8
-void InsaneRebel2::drawPasswordInput(byte *renderBitmap, int pitch, int width, int height) {
- // Password display position for low-res: X=30 (0x1e), Y=190 (0xbe)
- int infoX = 30;
- int infoY = 190;
-
- // Build display string with cursor
- static int frameCounter = 0;
- frameCounter++;
- char cursor = (frameCounter & 2) ? '_' : ' '; // Blinking cursor
-
- char displayText[32];
- snprintf(displayText, sizeof(displayText), "ACCESS CODE: %s%c", _passwordInput.c_str(), cursor);
-
- NutRenderer *font = _smush_smalfontNut;
- if (!font) return;
-
- int numFontChars = font->getNumChars();
- Common::Rect clipRect(0, 0, width, height);
-
- int curX = infoX;
- for (const char *c = displayText; *c; c++) {
- int charIdx = (unsigned char)*c;
- if (charIdx < numFontChars) {
- int charWidth = font->getCharWidth(charIdx);
- if (curX >= 0 && curX + charWidth <= width && infoY >= 0 && infoY < height) {
- font->drawCharV7(renderBitmap, clipRect, curX, infoY, pitch, -1,
- kStyleAlignLeft, charIdx, true, true);
- }
- curX += charWidth;
- }
- }
-}
-
-// Draw score/time display - emulates lines 99-104 of FUN_00415CF8
-void InsaneRebel2::drawScoreDisplay(byte *renderBitmap, int pitch, int width, int height, int chapter) {
- // Score display position for low-res: X=25 (0x19), Y=190 (0xbe)
- int infoX = 25;
- int infoY = 190;
-
- // For now, just display a placeholder score
- char displayText[32];
- snprintf(displayText, sizeof(displayText), "SCORE: %d", 0);
+ SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
+ if (!splayer) return;
NutRenderer *font = _smush_smalfontNut;
if (!font) return;
- int numFontChars = font->getNumChars();
- Common::Rect clipRect(0, 0, width, height);
+ Common::Rect clipRect(0, 0, _vm->_screenWidth, _vm->_screenHeight);
+ int actualPitch = _vm->_screenWidth;
- int curX = infoX;
- for (const char *c = displayText; *c; c++) {
- int charIdx = (unsigned char)*c;
- if (charIdx < numFontChars) {
- int charWidth = font->getCharWidth(charIdx);
- if (curX >= 0 && curX + charWidth <= width && infoY >= 0 && infoY < height) {
- font->drawCharV7(renderBitmap, clipRect, curX, infoY, pitch, -1,
- kStyleAlignLeft, charIdx, true, true);
+ if (_chapterUnlocked[_chapterSelection]) {
+ // Unlocked: show score info using TRS 80 at X=25 (0x19), Y=190 (0xbe)
+ const char *scoreStr = splayer->getString(80);
+ if (!scoreStr || !scoreStr[0]) return;
+
+ int curX = 25;
+ int numChars = font->getNumChars();
+ for (const char *c = scoreStr; *c; c++) {
+ byte ch = (byte)*c;
+ if (ch >= 'a' && ch <= 'z') ch = ch - 'a' + 'A';
+ if (ch < numChars) {
+ int charW = font->getCharWidth(ch);
+ if (curX >= 0 && curX + charW <= width && 190 < height) {
+ font->drawCharV7(renderBitmap, clipRect, curX, 190, actualPitch, 1,
+ kStyleAlignLeft, ch, false, false);
+ }
+ curX += charW;
}
- curX += charWidth;
}
}
}
@@ -6772,295 +6670,50 @@ void InsaneRebel2::drawScoreDisplay(byte *renderBitmap, int pitch, int width, in
// Draw chapter selection overlay - called during O_LEVEL.SAN playback
// FUN_00415CF8 - Chapter selection screen with preview thumbnail
void InsaneRebel2::drawChapterSelectOverlay(byte *renderBitmap, int pitch, int width, int height) {
- // Draw chapter selection screen overlay
- // Emulates rendering in FUN_00415CF8
+ // Emulates FUN_00415CF8 rendering via shared drawMenuItems(leftAligned=true)
//
- // Layout (320x200 mode):
- // - Title "Chapters" at top-left using TITLFONT
- // - 15 chapters + FINALE + RETURN TO PILOTS (17 items total)
- // - Three preview boxes on right side
- // - Status bar at bottom: "PILOTS: X SCORE: Y RANK:"
-
- // Chapter names from GAME.TRS (DAT_00457820 and DAT_00457868)
- // From FUN_00414073:
- // TRS indices 0x28-0x39 (40-57): 18 unlocked chapter strings -> DAT_00457820
- // TRS indices 0x3c-0x4d (60-77): 18 locked chapter strings -> DAT_00457868
- // These contain complete strings like "CHAPTER 1 - THE DREIGHTON TRIANGLE"
- SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
- const char *chapterNamesUnlocked[18];
- const char *chapterNamesLocked[18];
-
- if (!splayer) {
- debug(1, "drawChapterSelectOverlay: SmushPlayer not available for TRS strings!");
- return;
- }
-
- // Load unlocked chapter strings (TRS indices 40-57)
- for (int i = 0; i < 18; i++) {
- chapterNamesUnlocked[i] = splayer->getString(40 + i);
- if (!chapterNamesUnlocked[i] || !chapterNamesUnlocked[i][0]) {
- debug(1, "drawChapterSelectOverlay: TRS unlocked string %d not found!", 40 + i);
- chapterNamesUnlocked[i] = "";
- }
- }
-
- // Load locked chapter strings (TRS indices 60-77)
- for (int i = 0; i < 18; i++) {
- chapterNamesLocked[i] = splayer->getString(60 + i);
- if (!chapterNamesLocked[i] || !chapterNamesLocked[i][0]) {
- debug(1, "drawChapterSelectOverlay: TRS locked string %d not found!", 60 + i);
- chapterNamesLocked[i] = "";
- }
- }
-
- // Frame counter for flashing selection box
- static int frameCounter = 0;
- frameCounter++;
-
- NutRenderer *menuFont = _smush_smalfontNut;
- NutRenderer *titleFont = _smush_titlefontNut;
- if (!menuFont) return;
-
- Common::Rect clipRect(0, 0, width, height);
- int numFontChars = menuFont->getNumChars();
-
- // === Draw title "Chapters" using TITLFONT ===
- // From FUN_0041F5AE param_4=1 (left-aligned mode), low-res (320x200):
- // Title X = 0x28 = 40, Title Y = param_3 * -5 + 0x56 = 17 * -5 + 86 = 1
- int titleX = 40;
- int titleY = 1;
-
- if (titleFont) {
- const char *titleText = "Chapters";
- int curX = titleX;
- int numTitleChars = titleFont->getNumChars();
- for (const char *c = titleText; *c; c++) {
- int charIdx = (unsigned char)*c;
- if (charIdx < numTitleChars) {
- int charWidth = titleFont->getCharWidth(charIdx);
- if (curX >= 0 && curX + charWidth <= width && titleY >= 0) {
- titleFont->drawCharV7(renderBitmap, clipRect, curX, titleY, pitch, -1,
- kStyleAlignLeft, charIdx, true, true);
- }
- curX += charWidth;
- }
- }
- }
-
- // === Draw chapter list (left-aligned) ===
- // From FUN_0041F5AE param_4=1 (left-aligned mode), low-res (320x200):
- // Item X = 0x17 = 23
- // Item Y = param_3 * -5 + local_c * 10 + 0x68 = 17 * -5 + i * 10 + 104 = 19 + i * 10
- // Selection Box X = 0x14 = 20, Y = 18 + i * 10 (item Y - 1)
- int itemX = 23; // From assembly: 0x17 = 23
- int itemBaseY = 19; // From assembly: 17 * -5 + 104 = 19
- int itemSpacing = 10; // From assembly: 10 pixels between items
- int selBoxX = 20; // From assembly: 0x14 = 20
-
- // Draw 15 chapters (indices 0-14)
- for (int i = 0; i < 15; i++) {
- int itemY = itemBaseY + i * itemSpacing;
- bool isSelected = (i == _chapterSelection);
-
- // Get chapter string from TRS (already contains "CHAPTER X - NAME" or "CHAPTER X -")
- const char *chapterStr = _chapterUnlocked[i] ? chapterNamesUnlocked[i] : chapterNamesLocked[i];
- if (!chapterStr || !chapterStr[0]) {
- continue; // Skip if no string available
- }
-
- // Calculate text width for selection box
- int textWidth = 0;
- for (const char *c = chapterStr; *c; c++) {
- int charIdx = (unsigned char)*c;
- if (charIdx < numFontChars) {
- textWidth += menuFont->getCharWidth(charIdx);
- }
- }
-
- // Draw selection box if selected (red border)
- // From assembly: box starts at X=0x14=20, Y = itemY - 1
- if (isSelected) {
- int boxWidth = textWidth + 6; // From assembly: textWidth + 6
- int boxHeight = 10; // From assembly: 10 pixels
- int boxX = selBoxX; // From assembly: 0x14 = 20
- int boxY = itemY - 1; // From assembly: itemY - 1
-
- // Flashing color from assembly: (-((DAT_0047a7e4 & 1) == 0) & 8U) - 0x10
- // This gives -16 or -8, which are palette-relative negative offsets
- byte boxColor = (frameCounter & 1) ? 0xF8 : 0xF0;
-
- // Draw box border
- if (boxY >= 0 && boxY + boxHeight <= height && boxX >= 0 && boxX + boxWidth <= width) {
- for (int px = boxX; px < boxX + boxWidth; px++) {
- renderBitmap[boxY * pitch + px] = boxColor;
- renderBitmap[(boxY + boxHeight - 1) * pitch + px] = boxColor;
- }
- for (int py = boxY; py < boxY + boxHeight; py++) {
- renderBitmap[py * pitch + boxX] = boxColor;
- renderBitmap[py * pitch + boxX + boxWidth - 1] = boxColor;
- }
- }
- }
-
- // Draw chapter text
- int curX = itemX;
- for (const char *c = chapterStr; *c; c++) {
- int charIdx = (unsigned char)*c;
- if (charIdx < numFontChars) {
- int charWidth = menuFont->getCharWidth(charIdx);
- if (curX >= 0 && curX + charWidth <= width && itemY >= 0 && itemY < height) {
- menuFont->drawCharV7(renderBitmap, clipRect, curX, itemY, pitch, -1,
- kStyleAlignLeft, charIdx, true, true);
- }
- curX += charWidth;
- }
- }
- }
-
- // Draw FINALE (chapter 16 = index 15)
- // Position: Y = 19 + 15*10 = 169
- // FINALE uses TRS index 55 (unlocked) or 75 (locked) - index 15 in the arrays
- int finaleY = itemBaseY + 15 * itemSpacing;
- bool finaleSelected = (_chapterSelection == 15);
- const char *finaleStr = _chapterUnlocked[15] ? chapterNamesUnlocked[15] : chapterNamesLocked[15];
- if (!finaleStr || !finaleStr[0]) {
- finaleStr = ""; // Fallback to empty if TRS not available
- }
-
- int finaleWidth = 0;
- for (const char *c = finaleStr; *c; c++) {
- int charIdx = (unsigned char)*c;
- if (charIdx < numFontChars) finaleWidth += menuFont->getCharWidth(charIdx);
- }
+ // Menu structure (18 entries, 17 selectable):
+ // items[0] = title = TRS 60 (locked chapter 0 header, contains ^f02 for TITLFONT)
+ // items[1..16] = chapters 1-16 = TRS 41-56 (unlocked) or TRS 61-76 (locked)
+ // items[17] = "RETURN TO PILOTS" = TRS 77
+ //
+ // FUN_0041f5ae(0, &DAT_004577a8, 0x11, 1): param_3=17, param_4=1 (left-aligned)
- if (finaleSelected) {
- int boxWidth = finaleWidth + 6;
- int boxHeight = 10;
- int boxX = selBoxX; // Use assembly value: 20
- int boxY = finaleY - 1;
- byte boxColor = (frameCounter & 1) ? 0xF8 : 0xF0;
+ SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
+ if (!splayer) return;
- if (boxY >= 0 && boxY + boxHeight <= height && boxX >= 0 && boxX + boxWidth <= width) {
- for (int px = boxX; px < boxX + boxWidth; px++) {
- renderBitmap[boxY * pitch + px] = boxColor;
- renderBitmap[(boxY + boxHeight - 1) * pitch + px] = boxColor;
- }
- for (int py = boxY; py < boxY + boxHeight; py++) {
- renderBitmap[py * pitch + boxX] = boxColor;
- renderBitmap[py * pitch + boxX + boxWidth - 1] = boxColor;
- }
- }
- }
+ // Build items array matching original DAT_004577a8 layout
+ const char *items[18];
- int curX = itemX;
- for (const char *c = finaleStr; *c; c++) {
- int charIdx = (unsigned char)*c;
- if (charIdx < numFontChars) {
- int charWidth = menuFont->getCharWidth(charIdx);
- if (curX >= 0 && curX + charWidth <= width && finaleY >= 0) {
- menuFont->drawCharV7(renderBitmap, clipRect, curX, finaleY, pitch, -1,
- kStyleAlignLeft, charIdx, true, true);
- }
- curX += charWidth;
- }
- }
+ // items[0] = title (always locked version = TRS 60, contains "^f02CHAPTERS")
+ items[0] = splayer->getString(60);
+ if (!items[0] || !items[0][0]) items[0] = "^f02CHAPTERS";
- // Draw "RETURN TO PILOTS" (index 16)
- // Position: Y = 19 + 16*10 = 179
- // This string comes from TRS - likely in DAT_00457400 (indices 89-109)
- // Using TRS index 89 for "RETURN TO PILOTS" or equivalent
- int returnY = itemBaseY + 16 * itemSpacing;
- bool returnSelected = (_chapterSelection == 16);
- const char *returnStr = splayer->getString(89);
- if (!returnStr || !returnStr[0]) {
- debug(1, "drawChapterSelectOverlay: TRS string 89 (RETURN TO PILOTS) not found!");
- returnStr = "";
+ // items[1..16] = chapters, using unlocked (TRS 41-56) or locked (TRS 61-76) strings
+ for (int i = 1; i <= 16; i++) {
+ bool unlocked = (i - 1 < 16) && _chapterUnlocked[i - 1];
+ int trsIdx = unlocked ? (40 + i) : (60 + i);
+ items[i] = splayer->getString(trsIdx);
+ if (!items[i] || !items[i][0]) items[i] = "";
}
- int returnWidth = 0;
- for (const char *c = returnStr; *c; c++) {
- int charIdx = (unsigned char)*c;
- if (charIdx < numFontChars) returnWidth += menuFont->getCharWidth(charIdx);
- }
+ // items[17] = "RETURN TO PILOTS" (always locked version = TRS 77)
+ items[17] = splayer->getString(77);
+ if (!items[17] || !items[17][0]) items[17] = "RETURN TO PILOTS";
- if (returnSelected) {
- int boxWidth = returnWidth + 6;
- int boxHeight = 10;
- int boxX = selBoxX; // Use assembly value: 20
- int boxY = returnY - 1;
- byte boxColor = (frameCounter & 1) ? 0xF8 : 0xF0;
+ // Render menu using shared renderer with left-aligned mode
+ drawMenuItems(renderBitmap, pitch, width, height, items, 17, _chapterSelection, true);
- if (boxY >= 0 && boxY + boxHeight <= height && boxX >= 0 && boxX + boxWidth <= width) {
- for (int px = boxX; px < boxX + boxWidth; px++) {
- renderBitmap[boxY * pitch + px] = boxColor;
- renderBitmap[(boxY + boxHeight - 1) * pitch + px] = boxColor;
- }
- for (int py = boxY; py < boxY + boxHeight; py++) {
- renderBitmap[py * pitch + boxX] = boxColor;
- renderBitmap[py * pitch + boxX + boxWidth - 1] = boxColor;
- }
- }
- }
-
- curX = itemX;
- for (const char *c = returnStr; *c; c++) {
- int charIdx = (unsigned char)*c;
- if (charIdx < numFontChars) {
- int charWidth = menuFont->getCharWidth(charIdx);
- if (curX >= 0 && curX + charWidth <= width && returnY >= 0) {
- menuFont->drawCharV7(renderBitmap, clipRect, curX, returnY, pitch, -1,
- kStyleAlignLeft, charIdx, true, true);
- }
- curX += charWidth;
- }
- }
-
- // === Draw preview box on the right side ===
- // From FUN_00415CF8 lines 128-133:
- // Outer: X=228 (0xe4), Y=73 (0x49), W=84 (0x54), H=54 (0x36), color=0xF8
- // Inner: X=229 (0xe5), Y=74 (0x4a), W=82 (0x52), H=52 (0x34), color=4
+ // Draw preview box on the right side (FUN_004292d0 calls at lines 128-133)
drawPreviewBox(renderBitmap, pitch, width, height);
- // === Draw preview thumbnail inside the box ===
- // Shows chapter number and unlock status for currently selected chapter
- // Only draw for chapters 0-15 (not RETURN TO PILOTS at index 16)
+ // Draw preview thumbnail for chapters 0-15 (not RETURN TO PILOTS)
if (_chapterSelection >= 0 && _chapterSelection < 16) {
drawPreviewThumbnail(renderBitmap, pitch, width, height, _chapterSelection);
}
- // === Draw status bar at bottom ===
- // From FUN_00415CF8 lines 101-103 (unlocked chapter score display):
- // X = 0x19 + 0x19 = 50 (but we use 23 to align with menu items)
- // Y = 0xbe = 190
- // TODO: The status bar format should use TRS strings from DAT_00457400 (indices 89-109)
- // for proper localization. Currently using minimal display.
- int statusY = 190;
- int statusX = 23; // Align with menu items
-
- // Build status string using TRS format strings where available
- // TRS indices 89-109 contain format strings for status display
- char statusStr[64];
- const char *pilotsLabel = splayer->getString(90); // "PILOTS:" or equivalent
- const char *scoreLabel = splayer->getString(91); // "SCORE:" or equivalent
- if (pilotsLabel && pilotsLabel[0] && scoreLabel && scoreLabel[0]) {
- snprintf(statusStr, sizeof(statusStr), "%s %d %s %d", pilotsLabel, 4, scoreLabel, _playerScore);
- } else {
- // Minimal fallback if TRS strings not available
- snprintf(statusStr, sizeof(statusStr), "%d %d", 4, _playerScore);
- }
-
- curX = statusX;
- for (const char *c = statusStr; *c; c++) {
- int charIdx = (unsigned char)*c;
- if (charIdx < numFontChars) {
- int charWidth = menuFont->getCharWidth(charIdx);
- if (curX >= 0 && curX + charWidth <= width && statusY >= 0 && statusY < height) {
- menuFont->drawCharV7(renderBitmap, clipRect, curX, statusY, pitch, -1,
- kStyleAlignLeft, charIdx, true, true);
- }
- curX += charWidth;
- }
- }
+ // Draw score/info line at bottom
+ drawChapterInfoLine(renderBitmap, pitch, width, height);
}
// ==================== Pilot Selection Menu (FUN_00414A41) ====================
diff --git a/engines/scumm/insane/insane_rebel.h b/engines/scumm/insane/insane_rebel.h
index e8ec2f4c3f6..c6e96de2d28 100644
--- a/engines/scumm/insane/insane_rebel.h
+++ b/engines/scumm/insane/insane_rebel.h
@@ -96,10 +96,13 @@ public:
// Process menu input (keyboard/mouse) - returns selected item or -1
int processMenuInput();
- // Shared menu item renderer - emulates FUN_0041F5AE param_4==0 (centered mode)
+ // Shared menu item renderer - emulates FUN_0041F5AE
// items[0] = title, items[1..numItems] = selectable items, selection = highlighted item
+ // leftAligned=false: param_4==0 (centered, for main menu / pilot select)
+ // leftAligned=true: param_4==1 (left-aligned, for chapter select)
void drawMenuItems(byte *renderBitmap, int pitch, int width, int height,
- const char **items, int numItems, int selection);
+ const char **items, int numItems, int selection,
+ bool leftAligned = false);
// Draw menu overlay (selection highlight) on current frame
void drawMenuOverlay(byte *renderBitmap, int pitch, int width, int height);
@@ -150,15 +153,8 @@ public:
int16 _previewOffsetX; // X offset = -90 for chapter select
int16 _previewOffsetY; // Y offset = chapter * -50 + 75
- // Draw left-aligned menu text - emulates FUN_0041F5AE with param_4=1
- void drawLeftAlignedMenuItem(byte *renderBitmap, int pitch, int width, int height,
- const char *text, int x, int y, bool selected);
-
- // Draw password input field - emulates lines 106-125 of FUN_00415CF8
- void drawPasswordInput(byte *renderBitmap, int pitch, int width, int height);
-
- // Draw score/time display - emulates lines 99-104 of FUN_00415CF8
- void drawScoreDisplay(byte *renderBitmap, int pitch, int width, int height, int chapter);
+ // Draw score/info display at bottom of chapter select - emulates FUN_00434cb0 calls
+ void drawChapterInfoLine(byte *renderBitmap, int pitch, int width, int height);
// ================= Pilot Selection Menu (FUN_00414A41) ====================
// This is the pilot/save selection menu (separate from chapter selection)
Commit: 1ae852f6e98521338851e2b4d59febaf1b6705a2
https://github.com/scummvm/scummvm/commit/1ae852f6e98521338851e2b4d59febaf1b6705a2
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:03+02:00
Commit Message:
SCUMM: RA2: Improve level 2
Changed paths:
engines/scumm/insane/insane_rebel.cpp
engines/scumm/insane/insane_rebel.h
diff --git a/engines/scumm/insane/insane_rebel.cpp b/engines/scumm/insane/insane_rebel.cpp
index 2577983d670..480bca2168f 100644
--- a/engines/scumm/insane/insane_rebel.cpp
+++ b/engines/scumm/insane/insane_rebel.cpp
@@ -136,7 +136,10 @@ InsaneRebel2::InsaneRebel2(ScummEngine_v7 *scumm) {
// Retail globals mapped: hit counter, cooldown, invulnerability flag
_rebelHitCounter = 0;
+ _rebelKillCounter = 0;
_rebelInvulnerable = false;
+ _rebelWaveState = 0;
+ _rebelPhaseState = 0;
// Opcode 6 state variables
_rebelAutopilot = 0;
@@ -727,19 +730,30 @@ int32 InsaneRebel2::processMouse() {
// Enemy hit!
it->active = false;
it->destroyed = true; // Mark as destroyed so IACT won't re-activate
- debug("Rebel2: HIT enemy ID=%d at (%d,%d) - Rect: (%d,%d)-(%d,%d)",
- it->id, mousePos.x, mousePos.y,
+ debug("Rebel2: HIT enemy ID=%d type=%d at (%d,%d) - Rect: (%d,%d)-(%d,%d)",
+ it->id, it->type, mousePos.x, mousePos.y,
it->rect.left, it->rect.top, it->rect.right, it->rect.bottom);
// Spawn explosion using native system
// Use width / 2 as the scale parameter
- spawnExplosion((it->rect.left + it->rect.right) / 2,
- (it->rect.top + it->rect.bottom) / 2,
+ spawnExplosion((it->rect.left + it->rect.right) / 2,
+ (it->rect.top + it->rect.bottom) / 2,
it->rect.width() / 2);
- // Disable self
+ // Disable self (prevents sprite from rendering via SKIP chunks)
setBit(it->id);
+ // Set enemy type bit in wave state (FUN_004028c5 line 74)
+ // DAT_0047ab98 |= 1 << (enemyType & 0x1f)
+ // This tracks which enemy GROUPS have been killed in this wave
+ if (it->type > 0 && it->type < 32) {
+ _rebelWaveState |= (1 << it->type);
+ debug("Rebel2: Wave state updated: 0x%x (set bit %d)", _rebelWaveState, it->type);
+ }
+
+ // Increment kill counter (DAT_0047ab88)
+ _rebelKillCounter++;
+
// Handle dependencies
int id = it->id;
if (id >= 0 && id < 512) {
@@ -1204,7 +1218,19 @@ void InsaneRebel2::iactRebel2Opcode2(Common::SeekableReadStream &b, int16 par2,
// The original game's setBit(0)/clearBit(0) affects ALL bits, not intended here
if (targetId < 1 || targetId >= 0x200)
return;
-
+
+ // Handler 8 specific: FUN_401234 case 0, par3==1, par4!=0
+ // "If enemy type par4 has been killed (bit set in wave state), disable targetId"
+ // This conditionally hides entities based on which enemy groups have been destroyed
+ if (_rebelHandler == 8 && value != 0) {
+ int bitMask = 1 << (value & 0x1f);
+ if ((_rebelWaveState & bitMask) != 0) {
+ setBit(targetId);
+ debug("Rebel2 Opcode2 (H8): Disable target=%d (type %d killed, wave=0x%x)", targetId, value, _rebelWaveState);
+ }
+ return;
+ }
+
if (value > 1 && value < 10) { // 1 < value < 10: random disable
if (_vm->_rnd.getRandomNumber(value) == 0) {
setBit(targetId);
@@ -1333,16 +1359,22 @@ void InsaneRebel2::iactRebel2Opcode6(byte *renderBitmap, Common::SeekableReadStr
// Set ship level mode (DAT_0043e000 = par3)
_shipLevelMode = par3;
- // If par4 == 1, enable status bar and reset state
+ // If par4 == 1, enable status bar
if (par4 == 1) {
_rebelStatusBarSprite = 5; // Status bar sprite for Handler 8
- // Reset link tables
+ }
+
+ // Reset state when shipLevelMode != 0 && par4 == 1 (FUN_401234 lines 97-103)
+ if (_shipLevelMode != 0 && par4 == 1) {
+ // Clear link tables
for (int i = 0; i < 512; i++) {
_rebelLinks[i][0] = 0;
_rebelLinks[i][1] = 0;
_rebelLinks[i][2] = 0;
}
- debug("Rebel2 Opcode 6 (Handler 8): Status bar enabled, state reset");
+ // DAT_0047ab98 = DAT_0047ab9c: Reset wave state to accumulated phase state
+ _rebelWaveState = _rebelPhaseState;
+ debug("Rebel2 Opcode 6 (Handler 8): State reset, wave=0x%x", _rebelWaveState);
}
// Skip position calculation for special modes 4 and 5
@@ -1763,9 +1795,12 @@ void InsaneRebel2::iactRebel2Opcode6(byte *renderBitmap, Common::SeekableReadStr
_rebelLinks[i][2] = 0;
}
- // DAT_0047ab98 = DAT_0047ab9c (reset state flags)
- // We don't have a direct equivalent, but we can reset relevant counters
+ // DAT_0047ab98 = DAT_0047ab9c: At the start of each wave video,
+ // reset wave state to accumulated phase state. Enemies killed in
+ // previous waves stay killed; new kills add during this wave.
+ _rebelWaveState = _rebelPhaseState;
_rebelHitCounter = 0;
+ debug("Rebel2 Opcode 6: Wave state reset to phase state 0x%x", _rebelWaveState);
}
// Step 2: Set level type (DAT_00457900 = par3)
@@ -2692,6 +2727,7 @@ void InsaneRebel2::enemyUpdate(byte *renderBitmap, Common::SeekableReadStream &b
for (it = _enemies.begin(); it != _enemies.end(); ++it) {
if (it->id == enemyId) {
it->rect = Common::Rect(x, y, x + w, y + h);
+ it->type = par3; // Update enemy type/group
// Only re-activate if not destroyed
if (!it->destroyed) {
it->active = true;
@@ -2701,13 +2737,14 @@ void InsaneRebel2::enemyUpdate(byte *renderBitmap, Common::SeekableReadStream &b
}
}
if (!found) {
- init_enemyStruct(enemyId, x, y, w, h, true, false, -1);
+ init_enemyStruct(enemyId, x, y, w, h, true, false, -1, par3);
}
}
-void InsaneRebel2::init_enemyStruct(int id, int32 x, int32 y, int32 w, int32 h, bool active, bool destroyed, int32 explosionFrame) {
+void InsaneRebel2::init_enemyStruct(int id, int32 x, int32 y, int32 w, int32 h, bool active, bool destroyed, int32 explosionFrame, int type) {
enemy e;
e.id = id;
+ e.type = type;
e.rect = Common::Rect(x, y, x + w, y + h);
e.active = active;
e.destroyed = destroyed;
@@ -7602,8 +7639,17 @@ int InsaneRebel2::runLevel1() {
// =============================================================================
int InsaneRebel2::runLevel2() {
+ // FUN_00418063: Level 2 "Corellia Star" - Third-person on-foot shooting
+ // Three phases, each with looping enemy waves until all enemy types killed.
+ // Phase completion: (_rebelPhaseState & mask) == mask
+ // Phase 1 mask: 0x06 (enemy types 1,2)
+ // Phase 2 mask: 0x0e (enemy types 1,2,3)
+ // Phase 3 mask: 0x0e (enemy types 1,2,3)
SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
- int bonusCount = 0; // Tracks special events (local_1c in assembly)
+ int bonusCount = 0; // local_1c: tracks bonus events (DAT_0047ab9c & 0x10)
+ int totalKills = 0; // local_c: accumulated kill count across phases
+ int totalMisses = 0; // Accumulated misses (sVar1 + sVar2 from hit counters)
+ int prevWaveState = 0; // local_8: previous wave's state for Phase 3 randomization
// Play cutscene (02CUT.SAN)
playCinematic("LEV02/02CUT.SAN");
@@ -7619,12 +7665,12 @@ int InsaneRebel2::runLevel2() {
_playerDamage = 0;
_currentPhase = 1;
bonusCount = 0;
+ totalKills = 0;
+ totalMisses = 0;
- // Reset bit table before gameplay starts - FUN_00423880 calls FUN_00423a00(0)
- // This ensures all enemies are visible (not skipped by SKIP chunks)
+ // FUN_00401000 + FUN_00407d10 + FUN_0040c040: Reset game state
clearBit(0);
-
- // Also reset link tables for enemy dependencies
+ _enemies.clear();
for (int i = 0; i < 512; i++) {
_rebelLinks[i][0] = 0;
_rebelLinks[i][1] = 0;
@@ -7632,154 +7678,231 @@ int InsaneRebel2::runLevel2() {
}
// ===== PHASE 1: P1/02P01_X.SAN =====
- // First play variant A which contains the background IACT (opcode 8, par4=5)
- // The background is loaded during this video and persists for B/C/D variants
- {
- debug("Rebel2: Level 2 Phase 1 - playing 02P01_A.SAN (background loader)");
- splayer->setCurVideoFlags(0x28);
- splayer->play("LEV02/P1/02P01_A.SAN", 12);
- _deathFrame = splayer->_frame;
- }
+ // FUN_0041c7d0: Reset per-phase counters
+ _rebelKillCounter = 0;
+ _rebelHitCounter = 0;
+ _rebelPhaseState = 0;
+ _rebelWaveState = 0;
+
+ // Play A.SAN (background loader)
+ debug("Rebel2: Level 2 Phase 1 - playing 02P01_A.SAN (background)");
+ splayer->setCurVideoFlags(0x28);
+ splayer->play("LEV02/P1/02P01_A.SAN", 12);
+ _deathFrame = splayer->_frame;
if (_vm->shouldQuit()) return kLevelQuit;
- // Now select random variant (B, C, or D) for actual gameplay
- if (_playerShield > 0) {
- int variant = getRandomVariant(3);
- const char *variants[] = {"B", "C", "D"};
- Common::String filename = Common::String::format("LEV02/P1/02P01_%s.SAN", variants[variant]);
+ // Copy wave state to phase state after A.SAN
+ _rebelPhaseState = _rebelWaveState;
- debug("Rebel2: Level 2 Phase 1 - playing %s (gameplay)", filename.c_str());
+ // Phase 1 wave loop: random B/C/D until all type 1,2 enemies killed
+ // Original: while (uVar3 >= 0 && (DAT_0047ab9c & 6) != 6)
+ while (_playerDamage < 255 && (_rebelPhaseState & 0x06) != 0x06) {
+ if (_vm->shouldQuit()) return kLevelQuit;
+
+ // Random variant B, C, or D
+ int variant = _vm->_rnd.getRandomNumber(2); // 0-2
+ const char *variants[] = {
+ "LEV02/P1/02P01_B.SAN",
+ "LEV02/P1/02P01_C.SAN",
+ "LEV02/P1/02P01_D.SAN"
+ };
+ debug("Rebel2: Phase 1 wave - playing %s (state=0x%x)", variants[variant], _rebelPhaseState);
splayer->setCurVideoFlags(0x28);
- splayer->play(filename.c_str(), 12);
+ splayer->play(variants[variant], 12);
_deathFrame = splayer->_frame;
- }
- if (_vm->shouldQuit()) return kLevelQuit;
+ // After wave: copy wave state to phase state (FUN_00417b61 line 33)
+ _rebelPhaseState = _rebelWaveState;
+ debug("Rebel2: Phase 1 wave done - state=0x%x (need 0x06)", _rebelPhaseState);
+ }
- if (_playerShield == 0) {
- // Died in phase 1
- debug("Rebel2: Level 2 Phase 1 death");
- playLevelDeathVariant(2, 1, _deathFrame);
- if (_vm->shouldQuit()) return kLevelQuit;
+ // Check for bonus (bit 4 = 0x10)
+ if ((_rebelPhaseState & 0x10) != 0) bonusCount++;
- _playerLives--;
- if (_playerLives <= 0) {
- playLevelGameOver(2);
- return kLevelGameOver;
- }
- playLevelRetry(2);
- if (_vm->shouldQuit()) return kLevelQuit;
- continue; // Restart from beginning
- }
+ if (_playerDamage >= 255) goto level2_death;
+ if (_vm->shouldQuit()) return kLevelQuit;
// Post segment 1 (02PST1.SAN)
splayer->setCurVideoFlags(0x20);
splayer->play("LEV02/02PST1.SAN", 12);
if (_vm->shouldQuit()) return kLevelQuit;
- _currentPhase = 2;
-
- // Reset bit table before Phase 2 gameplay starts
- clearBit(0);
+ totalKills += _rebelKillCounter;
+ totalMisses += _rebelHitCounter;
// ===== PHASE 2: P2/02P02_X.SAN =====
- // First play variant A which contains the background IACT for this phase
- {
- debug("Rebel2: Level 2 Phase 2 - playing 02P02_A.SAN (background loader)");
- splayer->setCurVideoFlags(0x28);
- splayer->play("LEV02/P2/02P02_A.SAN", 12);
- _deathFrame = splayer->_frame;
- }
+ _currentPhase = 2;
+ _rebelKillCounter = 0;
+ _rebelHitCounter = 0;
+ _rebelPhaseState = 0;
+ _rebelWaveState = 0;
+ _enemies.clear();
+
+ // Play A.SAN (background loader)
+ debug("Rebel2: Level 2 Phase 2 - playing 02P02_A.SAN (background)");
+ splayer->setCurVideoFlags(0x28);
+ splayer->play("LEV02/P2/02P02_A.SAN", 12);
+ _deathFrame = splayer->_frame;
if (_vm->shouldQuit()) return kLevelQuit;
+ _rebelPhaseState = _rebelWaveState;
- // Now select gameplay variant (B through F)
- if (_playerShield > 0) {
- int variant = getRandomVariant(5);
- const char *variants[] = {"B", "C", "D", "E", "F"};
- Common::String filename = Common::String::format("LEV02/P2/02P02_%s.SAN", variants[variant]);
+ // Phase 2 wave loop: state-based variant selection
+ // Original: while (local_10 >= 0 && (DAT_0047ab9c & 0xe) != 0xe)
+ while (_playerDamage < 255 && (_rebelPhaseState & 0x0e) != 0x0e) {
+ if (_vm->shouldQuit()) return kLevelQuit;
- debug("Rebel2: Level 2 Phase 2 - playing %s (gameplay)", filename.c_str());
+ int waveSelect = _rebelPhaseState & 0x0e; // masked enemy state
+
+ // If no specific pattern: randomize high bits (original lines 71-74)
+ // When (local_10 & 0xc) == 0: add random 0x10/0x11/0x12
+ if ((waveSelect & 0x0c) == 0) {
+ waveSelect = _vm->_rnd.getRandomNumber(2) + 0x10;
+ }
+
+ // Variant selection matching original switch (FUN_418063 lines 75-96)
+ const char *filename;
+ switch (waveSelect) {
+ case 4: case 6:
+ filename = "LEV02/P2/02P02_B.SAN"; break;
+ case 8: case 10:
+ filename = "LEV02/P2/02P02_C.SAN"; break;
+ case 0x0c: case 0x0e:
+ filename = "LEV02/P2/02P02_A.SAN"; break;
+ case 0x11:
+ filename = "LEV02/P2/02P02_E.SAN"; break;
+ case 0x12:
+ filename = "LEV02/P2/02P02_F.SAN"; break;
+ default:
+ filename = "LEV02/P2/02P02_D.SAN"; break;
+ }
+
+ debug("Rebel2: Phase 2 wave - playing %s (state=0x%x sel=0x%x)", filename, _rebelPhaseState, waveSelect);
splayer->setCurVideoFlags(0x28);
- splayer->play(filename.c_str(), 12);
+ splayer->play(filename, 12);
_deathFrame = splayer->_frame;
- }
- if (_vm->shouldQuit()) return kLevelQuit;
+ _rebelPhaseState = _rebelWaveState;
+ debug("Rebel2: Phase 2 wave done - state=0x%x (need 0x0e)", _rebelPhaseState);
+ }
- if (_playerShield == 0) {
- // Died in phase 2
- debug("Rebel2: Level 2 Phase 2 death");
- playLevelDeathVariant(2, 2, _deathFrame);
- if (_vm->shouldQuit()) return kLevelQuit;
+ if ((_rebelPhaseState & 0x10) != 0) bonusCount++;
- _playerLives--;
- if (_playerLives <= 0) {
- playLevelGameOver(2);
- return kLevelGameOver;
- }
- playLevelRetry(2);
- if (_vm->shouldQuit()) return kLevelQuit;
- continue; // Restart from beginning
- }
+ if (_playerDamage >= 255) goto level2_death;
+ if (_vm->shouldQuit()) return kLevelQuit;
// Post segment 2 (02PST2.SAN)
splayer->setCurVideoFlags(0x20);
splayer->play("LEV02/02PST2.SAN", 12);
if (_vm->shouldQuit()) return kLevelQuit;
+ totalKills += _rebelKillCounter;
+ totalMisses += _rebelHitCounter;
+
+ // ===== PHASE 3: P3/02P03_X.SAN =====
_currentPhase = 3;
+ _rebelKillCounter = 0;
+ _rebelHitCounter = 0;
+ _rebelPhaseState = 0;
+ _rebelWaveState = 0;
+ _enemies.clear();
+ prevWaveState = 0;
- // Reset bit table before Phase 3 gameplay starts
- clearBit(0);
+ // Play A.SAN (background loader)
+ debug("Rebel2: Level 2 Phase 3 - playing 02P03_A.SAN (background)");
+ splayer->setCurVideoFlags(0x28);
+ splayer->play("LEV02/P3/02P03_A.SAN", 12);
+ _deathFrame = splayer->_frame;
- // ===== PHASE 3: P3/02P03_X.SAN =====
- // First play variant A which contains the background IACT for this phase
+ if (_vm->shouldQuit()) return kLevelQuit;
+ _rebelPhaseState = _rebelWaveState;
+
+ // Phase 3 wave loop: state-based with randomization
+ // Original: while (local_10 >= 0 && (DAT_0047ab9c & 0xe) != 0xe)
{
- debug("Rebel2: Level 2 Phase 3 - playing 02P03_A.SAN (background loader)");
- splayer->setCurVideoFlags(0x28);
- splayer->play("LEV02/P3/02P03_A.SAN", 12);
- _deathFrame = splayer->_frame;
- }
+ int waveSelect = _rebelPhaseState & 0x0e;
- if (_vm->shouldQuit()) return kLevelQuit;
+ while (_playerDamage < 255 && (_rebelPhaseState & 0x0e) != 0x0e) {
+ if (_vm->shouldQuit()) return kLevelQuit;
- // Now select gameplay variant (B through I)
- if (_playerShield > 0) {
- int variant = getRandomVariant(8);
- const char *variants[] = {"B", "C", "D", "E", "F", "G", "H", "I"};
- Common::String filename = Common::String::format("LEV02/P3/02P03_%s.SAN", variants[variant]);
+ // Phase 3 randomization (original lines 113-115):
+ // If previous wave state bit 0 was clear AND random(8)==0, set bit 0
+ if (((prevWaveState & 1) == 0) && (_vm->_rnd.getRandomNumber(7) == 0)) {
+ waveSelect |= 1;
+ }
+ prevWaveState = waveSelect;
+
+ // Variant selection matching original switch (FUN_418063 lines 117-144)
+ const char *filename;
+ switch (waveSelect) {
+ case 0:
+ filename = "LEV02/P3/02P03_H.SAN"; break;
+ case 2:
+ filename = "LEV02/P3/02P03_G.SAN"; break;
+ case 4:
+ filename = "LEV02/P3/02P03_F.SAN"; break;
+ case 6:
+ filename = "LEV02/P3/02P03_E.SAN"; break;
+ case 8:
+ filename = "LEV02/P3/02P03_D.SAN"; break;
+ case 10:
+ filename = "LEV02/P3/02P03_C.SAN"; break;
+ case 0x0c:
+ filename = "LEV02/P3/02P03_B.SAN"; break;
+ case 0x0e:
+ filename = "LEV02/P3/02P03_A.SAN"; break;
+ default:
+ filename = "LEV02/P3/02P03_I.SAN"; break;
+ }
- debug("Rebel2: Level 2 Phase 3 - playing %s (gameplay)", filename.c_str());
- splayer->setCurVideoFlags(0x28);
- splayer->play(filename.c_str(), 12);
- _deathFrame = splayer->_frame;
+ debug("Rebel2: Phase 3 wave - playing %s (state=0x%x sel=0x%x)", filename, _rebelPhaseState, waveSelect);
+ splayer->setCurVideoFlags(0x28);
+ splayer->play(filename, 12);
+ _deathFrame = splayer->_frame;
+
+ _rebelPhaseState = _rebelWaveState;
+ waveSelect = _rebelPhaseState & 0x0e;
+ debug("Rebel2: Phase 3 wave done - state=0x%x (need 0x0e)", _rebelPhaseState);
+ }
}
- if (_vm->shouldQuit()) return kLevelQuit;
+ if ((_rebelPhaseState & 0x10) != 0) bonusCount++;
+ totalKills += _rebelKillCounter;
- if (_playerShield == 0) {
- // Died in phase 3
- debug("Rebel2: Level 2 Phase 3 death");
- playLevelDeathVariant(2, 3, _deathFrame);
- if (_vm->shouldQuit()) return kLevelQuit;
+ if (_playerDamage >= 255) goto level2_death;
+ if (_vm->shouldQuit()) return kLevelQuit;
- _playerLives--;
- if (_playerLives <= 0) {
- playLevelGameOver(2);
- return kLevelGameOver;
+ // Level completed! Calculate accuracy score.
+ {
+ int accuracy = 0;
+ int totalShots = totalKills + totalMisses + _rebelHitCounter;
+ if (totalKills > 0 && totalShots > 0) {
+ accuracy = (totalKills * 100) / totalShots;
}
- playLevelRetry(2);
- if (_vm->shouldQuit()) return kLevelQuit;
- continue; // Restart from beginning
+ debug("Rebel2: Level 2 completed! kills=%d misses=%d accuracy=%d%% bonus=%d",
+ totalKills, totalMisses, accuracy, bonusCount);
}
- // Level completed!
- debug("Rebel2: Level 2 completed! bonusCount=%d", bonusCount);
playLevelEnd(2);
_levelUnlocked[2] = true; // Unlock level 3
return kLevelNextLevel;
+
+ level2_death:
+ // Player died â play death sequence and retry or game over
+ debug("Rebel2: Level 2 Phase %d death", _currentPhase);
+ playCinematic("LEV02/02DIE.SAN");
+ if (_vm->shouldQuit()) return kLevelQuit;
+
+ _playerLives--;
+ if (_playerLives <= 0) {
+ playLevelGameOver(2);
+ return kLevelGameOver;
+ }
+ playCinematic("LEV02/02RETRY.SAN");
+ _playerDamage = 0;
+ if (_vm->shouldQuit()) return kLevelQuit;
+ continue; // Restart from beginning
}
return kLevelQuit;
diff --git a/engines/scumm/insane/insane_rebel.h b/engines/scumm/insane/insane_rebel.h
index c6e96de2d28..3c5e92b1492 100644
--- a/engines/scumm/insane/insane_rebel.h
+++ b/engines/scumm/insane/insane_rebel.h
@@ -429,6 +429,7 @@ public:
struct enemy {
int id;
+ int type; // Enemy type/group from IACT opcode 4 par3 (determines DAT_0047ab98 bit)
Common::Rect rect;
bool active;
bool destroyed; // Set when enemy is shot - prevents re-activation
@@ -439,7 +440,7 @@ public:
int savedBgHeight; // Height of saved background
};
- void init_enemyStruct(int id, int32 x, int32 y, int32 w, int32 h, bool active, bool destroyed, int32 explosionFrame);
+ void init_enemyStruct(int id, int32 x, int32 y, int32 w, int32 h, bool active, bool destroyed, int32 explosionFrame, int type = 0);
void enemyUpdate(byte *renderBitmap, Common::SeekableReadStream &b, int16 par2, int16 par3, int16 par4);
Common::List<enemy> _enemies;
@@ -607,8 +608,16 @@ public:
// Rebel per-level counters / flags mapped from retail globals
int _rebelHitCounter; // DAT_0047ab80 - hit counter / state tracker
+ int _rebelKillCounter; // DAT_0047ab88 - enemies destroyed this phase
bool _rebelInvulnerable; // DAT_0047ab64 - toggles invulnerability / state
+ // Enemy wave/phase state tracking (FUN_004028c5 / FUN_00417b61)
+ // DAT_0047ab98: Per-wave enemy kill state. Bits set when enemy types are destroyed.
+ // DAT_0047ab9c: Per-phase accumulated state. Copied from _rebelWaveState between waves.
+ // Phase completion: (_rebelPhaseState & mask) == mask (all required enemy types killed)
+ int _rebelWaveState; // DAT_0047ab98 - current wave enemy kill flags
+ int _rebelPhaseState; // DAT_0047ab9c - accumulated phase enemy kill flags
+
// Opcode 6 state variables (from FUN_41CADB case 4)
int _rebelAutopilot; // DAT_00457904 - autopilot flag (0 or 1)
int _rebelDamageLevel; // DAT_0045790a - damage level (0-5)
Commit: f2c9358f1b04086e9cbcceb016a9a20cd4177aeb
https://github.com/scummvm/scummvm/commit/f2c9358f1b04086e9cbcceb016a9a20cd4177aeb
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:04+02:00
Commit Message:
SCUMM: RA2: Refactor level 2 rendering code
Changed paths:
engines/scumm/insane/insane_rebel.cpp
engines/scumm/smush/smush_player.cpp
engines/scumm/smush/smush_player.h
diff --git a/engines/scumm/insane/insane_rebel.cpp b/engines/scumm/insane/insane_rebel.cpp
index 480bca2168f..28fd6a5d3d1 100644
--- a/engines/scumm/insane/insane_rebel.cpp
+++ b/engines/scumm/insane/insane_rebel.cpp
@@ -917,96 +917,9 @@ void InsaneRebel2::procPreRendering(byte *renderBitmap) {
// For Mode 1: DAT_0045790c = damageLevel * -5 - 14, range -39 (covered) to -14 (uncovered)
//
// From FUN_00428a10: When position is negative, we skip source pixels and draw at 0.
- // This creates a "scrolling window" effect as the character enters/exits cover.
- if (_rebelHandler == 25 && renderBitmap) {
- // Calculate pitch and buffer height for both corridor overlay and GRD001 rendering
- int pitch = (_player && _player->_width > 0) ? _player->_width : 320;
- int bufHeight = (_player && _player->_height > 0) ? _player->_height : 200;
-
- // FIRST: Restore Level 2 background (par4=5) as the base layer
- // This is the scene background that enemies (FOBJ) draw on top of.
- // Without this, enemies are drawn onto black/empty buffer and appear invisible.
- // The corridor overlay (par4=4) is a HUD frame drawn ON TOP of this background.
- if (_level2BackgroundLoaded && _level2Background) {
- for (int y = 0; y < MIN(200, bufHeight); y++) {
- memcpy(renderBitmap + y * pitch, _level2Background + y * 320, MIN(320, pitch));
- }
- debug("Rebel2 Handler25 PRE: Restored _level2Background to renderBitmap");
- } else {
- debug("Rebel2 Handler25 PRE: WARNING - _level2Background NOT restored (loaded=%d ptr=%p)",
- _level2BackgroundLoaded, (void*)_level2Background);
- }
-
- EmbeddedSanFrame &corridorOverlay = _rebelEmbeddedHud[4];
- if (corridorOverlay.valid && corridorOverlay.pixels) {
-
- // Calculate source offset and destination position (FUN_00428a10 lines 31-43)
- // If position is negative, skip source pixels and draw at screen edge
- // Use _rebelViewOffsetX which is DAT_0045790c (damageLevel * -5 - 14)
- int srcOffsetX = 0;
- int srcOffsetY = 0;
- int destX = _rebelViewOffsetX; // DAT_0045790c
- int destY = _rebelViewOffsetY; // DAT_0045790e
- int drawWidth = corridorOverlay.width;
- int drawHeight = corridorOverlay.height;
-
- // Handle negative X position: skip source pixels, draw at X=0
- if (destX < 0) {
- srcOffsetX = -destX;
- drawWidth -= srcOffsetX;
- destX = 0;
- }
- // Handle negative Y position: skip source pixels, draw at Y=0
- if (destY < 0) {
- srcOffsetY = -destY;
- drawHeight -= srcOffsetY;
- destY = 0;
- }
-
- // Clip to screen bounds
- if (destX + drawWidth > pitch) {
- drawWidth = pitch - destX;
- }
- if (destY + drawHeight > bufHeight) {
- drawHeight = bufHeight - destY;
- }
-
- // Bounds check: ensure srcOffsetX doesn't exceed image width
- if (srcOffsetX >= corridorOverlay.width) {
- debug("Rebel2 procPreRendering: srcOffsetX (%d) >= image width (%d), skipping", srcOffsetX, corridorOverlay.width);
- }
- // Bounds check: ensure we have valid draw dimensions
- else if (drawWidth > 0 && drawHeight > 0) {
- // Additional safety: clamp to source image bounds
- int maxDrawWidth = corridorOverlay.width - srcOffsetX;
- int maxDrawHeight = corridorOverlay.height - srcOffsetY;
- if (drawWidth > maxDrawWidth) drawWidth = maxDrawWidth;
- if (drawHeight > maxDrawHeight) drawHeight = maxDrawHeight;
-
- if (drawWidth > 0 && drawHeight > 0) {
- for (int y = 0; y < drawHeight; y++) {
- for (int x = 0; x < drawWidth; x++) {
- int srcIdx = (srcOffsetY + y) * corridorOverlay.width + (srcOffsetX + x);
- byte pixel = corridorOverlay.pixels[srcIdx];
- if (pixel != 0 && pixel != 231) { // 0 and 231 = transparent
- renderBitmap[(destY + y) * pitch + (destX + x)] = pixel;
- }
- }
- }
- }
- }
-
- debug("Rebel2 procPreRendering: Corridor overlay viewOff=(%d,%d) damageLevel=%d autopilot=%d srcOff=(%d,%d) dest=(%d,%d) draw=(%d,%d) imgSize=(%d,%d)",
- _rebelViewOffsetX, _rebelViewOffsetY, _rebelDamageLevel, _rebelAutopilot,
- srcOffsetX, srcOffsetY, destX, destY, drawWidth, drawHeight,
- corridorOverlay.width, corridorOverlay.height);
- }
-
- // Draw GRD001 (wall/cockpit overlay) AFTER corridor but BEFORE FOBJ enemies.
- // This ensures enemies from FOBJ chunks draw ON TOP of the cockpit overlay.
- // Uses width-halving logic from FUN_0041db5e lines 202-221.
- renderHandler25ShipPre(renderBitmap, pitch, pitch, bufHeight);
- }
+ // Handler 25: Corridor overlay and FOBJ position offsets are set during
+ // IACT opcode 6 processing (iactRebel2Opcode6), matching the original
+ // FUN_41CADB architecture. No corridor drawing needed here.
}
void InsaneRebel2::procIACT(byte *renderBitmap, int32 codecparam, int32 setupsan12,
@@ -1219,14 +1132,15 @@ void InsaneRebel2::iactRebel2Opcode2(Common::SeekableReadStream &b, int16 par2,
if (targetId < 1 || targetId >= 0x200)
return;
- // Handler 8 specific: FUN_401234 case 0, par3==1, par4!=0
+ // Handler 8/25: FUN_401234 case 0 / FUN_41E7C2 par3==1, par4!=0
// "If enemy type par4 has been killed (bit set in wave state), disable targetId"
- // This conditionally hides entities based on which enemy groups have been destroyed
- if (_rebelHandler == 8 && value != 0) {
+ // This conditionally hides entities based on which enemy groups have been destroyed.
+ // Both Handler 8 (on-foot) and Handler 25 (FPS/cover) use DAT_0047ab98 for wave state.
+ if ((_rebelHandler == 8 || _rebelHandler == 25) && value != 0) {
int bitMask = 1 << (value & 0x1f);
if ((_rebelWaveState & bitMask) != 0) {
setBit(targetId);
- debug("Rebel2 Opcode2 (H8): Disable target=%d (type %d killed, wave=0x%x)", targetId, value, _rebelWaveState);
+ debug("Rebel2 Opcode2 (H%d): Disable target=%d (type %d killed, wave=0x%x)", _rebelHandler, targetId, value, _rebelWaveState);
}
return;
}
@@ -1631,11 +1545,14 @@ void InsaneRebel2::iactRebel2Opcode6(byte *renderBitmap, Common::SeekableReadStr
_rebelLinks[i][1] = 0;
_rebelLinks[i][2] = 0;
}
+ // Reset wave state to accumulated phase state (same as Handler 8)
+ // DAT_0047ab98 = DAT_0047ab9c: ensures new wave starts with correct state
+ _rebelWaveState = _rebelPhaseState;
// Initialize to covered state - player starts behind cover
// This ensures the corridor overlay shows the "covered" position initially
_rebelAutopilot = 1; // DAT_00457904 = 1 (covered)
_rebelDamageLevel = 5; // DAT_0045790a = 5 (fully in cover)
- debug("Rebel2 Opcode 6 (Handler 25): Status bar enabled, state reset, starting COVERED");
+ debug("Rebel2 Opcode 6 (Handler 25): Status bar enabled, state reset, starting COVERED, wave=0x%x", _rebelWaveState);
}
// Set sprite mode (DAT_00457900 = local_14[3]) - controls which GRD sprite to render
@@ -1778,6 +1695,47 @@ void InsaneRebel2::iactRebel2Opcode6(byte *renderBitmap, Common::SeekableReadStr
_grdSpriteMode, _rebelDamageLevel, _rebelFlightDir, _rebelAutopilot,
_rebelViewOffsetX, _rebelViewOffsetY, _rebelViewOffset2X, _rebelViewOffset2Y);
+ // Set FOBJ position offsets (FUN_00424510 in original, line 214)
+ // All subsequent FOBJs in this frame will be shifted by these offsets
+ if (_player) {
+ _player->_fobjOffsetX = _rebelViewOffsetX;
+ _player->_fobjOffsetY = _rebelViewOffsetY;
+ }
+
+ // Draw corridor overlay OPAQUELY (FUN_00428A10 in original, line 216)
+ // This wipes previous frame content so codec 23 delta skip regions show clean corridor
+ if (renderBitmap) {
+ EmbeddedSanFrame &corridorOverlay = _rebelEmbeddedHud[4];
+ if (corridorOverlay.valid && corridorOverlay.pixels) {
+ int pitch = (_player && _player->_width > 0) ? _player->_width : 320;
+ int bufHeight = (_player && _player->_height > 0) ? _player->_height : 200;
+
+ int srcOffsetX = 0;
+ int srcOffsetY = 0;
+ int destX = _rebelViewOffsetX;
+ int destY = _rebelViewOffsetY;
+ int drawWidth = corridorOverlay.width;
+ int drawHeight = corridorOverlay.height;
+
+ if (destX < 0) { srcOffsetX = -destX; drawWidth -= srcOffsetX; destX = 0; }
+ if (destY < 0) { srcOffsetY = -destY; drawHeight -= srcOffsetY; destY = 0; }
+ if (destX + drawWidth > pitch) drawWidth = pitch - destX;
+ if (destY + drawHeight > bufHeight) drawHeight = bufHeight - destY;
+ if (drawWidth > corridorOverlay.width - srcOffsetX) drawWidth = corridorOverlay.width - srcOffsetX;
+ if (drawHeight > corridorOverlay.height - srcOffsetY) drawHeight = corridorOverlay.height - srcOffsetY;
+
+ if (drawWidth > 0 && drawHeight > 0) {
+ for (int y = 0; y < drawHeight; y++) {
+ memcpy(renderBitmap + (destY + y) * pitch + destX,
+ corridorOverlay.pixels + (srcOffsetY + y) * corridorOverlay.width + srcOffsetX,
+ drawWidth);
+ }
+ }
+ debug("Rebel2 Opcode 6: Corridor overlay drawn at (%d,%d) size(%d,%d)",
+ _rebelViewOffsetX, _rebelViewOffsetY, corridorOverlay.width, corridorOverlay.height);
+ }
+ }
+
return;
}
@@ -2722,12 +2680,16 @@ void InsaneRebel2::enemyUpdate(byte *renderBitmap, Common::SeekableReadStream &b
// But for drawing the bounding box, we want the top-left corner (x, y) and full dimensions.
// Update enemy list for hit detection
+ // Enemy type comes from par4 (IACT offset +6), NOT par3 (offset +4).
+ // In the original (FUN_004028C5/FUN_0041E7C2): sVar5/sVar2 = *(short *)(*local + 6)
+ // This maps to par4 (userId field). Used for DAT_0047ab98 wave state bitmask:
+ // DAT_0047ab98 |= 1 << (type & 0x1f)
bool found = false;
Common::List<enemy>::iterator it;
for (it = _enemies.begin(); it != _enemies.end(); ++it) {
if (it->id == enemyId) {
it->rect = Common::Rect(x, y, x + w, y + h);
- it->type = par3; // Update enemy type/group
+ it->type = par4; // Enemy type from IACT offset +6 (userId)
// Only re-activate if not destroyed
if (!it->destroyed) {
it->active = true;
@@ -2737,7 +2699,7 @@ void InsaneRebel2::enemyUpdate(byte *renderBitmap, Common::SeekableReadStream &b
}
}
if (!found) {
- init_enemyStruct(enemyId, x, y, w, h, true, false, -1, par3);
+ init_enemyStruct(enemyId, x, y, w, h, true, false, -1, par4);
}
}
@@ -4090,6 +4052,8 @@ void InsaneRebel2::procPostRendering(byte *renderBitmap, int32 codecparam, int32
renderHandler7Ship(renderBitmap, pitch, width, height);
renderHandler8Ship(renderBitmap, pitch, width, height);
+ // GRD001 (wall/cockpit) drawn AFTER FOBJs per original FUN_0041DB5E lines 202-210
+ renderHandler25ShipPre(renderBitmap, pitch, width, height);
renderHandler25Ship(renderBitmap, pitch, width, height);
renderFallbackShip(renderBitmap, pitch, width, height);
@@ -4129,6 +4093,12 @@ void InsaneRebel2::procPostRendering(byte *renderBitmap, int32 codecparam, int32
// HUD score/lives rendering (FUN_0041c012)
renderScoreHUD(renderBitmap, pitch, width, height, 0);
+ // Reset FOBJ position offsets (FUN_00424510(0,0) in original FUN_0041DB5E line 271)
+ if (_player) {
+ _player->_fobjOffsetX = 0;
+ _player->_fobjOffsetY = 0;
+ }
+
// Frame end cleanup: reset enemy active flags and collision zones (FUN_403240)
frameEndCleanup();
}
@@ -4364,9 +4334,10 @@ void InsaneRebel2::renderEmbeddedHudOverlays(byte *renderBitmap, int pitch, int
if (!frame.valid || !frame.pixels || frame.width <= 0 || frame.height <= 0)
continue;
- // For Handler 25: Skip slot 4 (corridor overlay) here - it's already drawn
- // in procPreRendering BEFORE FOBJ enemies. Drawing it again here (after FOBJs)
- // would cover the enemies and make them invisible.
+ // Handler 25: Skip slot 4 (corridor overlay) in post-rendering.
+ // The corridor is a full background image (no color 0 transparent center).
+ // Drawing it here would cover enemies. It's already drawn in procPreRendering
+ // with transparency to preserve frame persistence for codec 23 delta.
if (_rebelHandler == 25 && hudSlot == 4) {
continue;
}
@@ -4635,8 +4606,8 @@ void InsaneRebel2::renderHandler8Ship(byte *renderBitmap, int pitch, int width,
spriteIndex, numSprites, _shipDirectionH, _shipDirectionV);
}
-// Handler 25 PRE-rendering: Draw GRD001 BEFORE FOBJ decoding
-// This is called from procPreRendering so enemies draw ON TOP of GRD001
+// Handler 25: Draw GRD001 (wall/cockpit overlay) in procPostRendering.
+// Per original FUN_0041DB5E, GRD sprites are drawn AFTER FOBJ enemies, before GRD002.
//
// From FUN_0041db5e disassembly (lines 202-221):
// - Mode 1 with damage==0: Width halved (left half only, pixels 0-159)
@@ -4714,9 +4685,8 @@ void InsaneRebel2::renderHandler25ShipPre(byte *renderBitmap, int pitch, int wid
}
void InsaneRebel2::renderHandler25Ship(byte *renderBitmap, int pitch, int width, int height) {
- // Handler 25 POST-rendering: Draw ONLY GRD002 (character sprite)
- // GRD001 (wall/cockpit) is now drawn in renderHandler25ShipPre() during procPreRendering
- // so that FOBJ enemies can draw ON TOP of it.
+ // Handler 25 POST-rendering: Draw GRD002 (character sprite) on top of enemies.
+ // GRD001 (wall/cockpit) is drawn before this via renderHandler25ShipPre().
//
// From FUN_0041db5e disassembly (lines 230-248):
// GRD002 is drawn LAST (after enemies) so the character appears in front.
diff --git a/engines/scumm/smush/smush_player.cpp b/engines/scumm/smush/smush_player.cpp
index bed00ea3049..806e14e4435 100644
--- a/engines/scumm/smush/smush_player.cpp
+++ b/engines/scumm/smush/smush_player.cpp
@@ -262,6 +262,8 @@ SmushPlayer::SmushPlayer(ScummEngine_v7 *scumm, IMuseDigital *imuseDigital, Insa
_skipNext = false;
_ra2FastForwarding = false;
+ _fobjOffsetX = 0;
+ _fobjOffsetY = 0;
_dst = nullptr;
_storeFrame = false;
_compressedFileMode = false;
@@ -436,19 +438,23 @@ void SmushPlayer::handleFetch(int32 subSize, Common::SeekableReadStream &b) {
debugC(DEBUG_SMUSH, "SmushPlayer::handleFetch()");
assert(subSize >= 6);
- // For RA2 Handlers 8 and 25, skip FTCH because the frame buffer only contains the
+ // For RA2 Handler 25, skip FTCH because the frame buffer only contains the
// par4=5 base background without the overlays (par4=4, 6, 7) that were drawn
// immediately in frame 0. Restoring would erase those overlays and make
// enemies invisible since they draw on top of the erased areas.
//
- // Handler 8 (Level 2 on-foot): Background is drawn in procPreRendering from
- // _level2Background. FTCH would overwrite FOBJ-drawn enemies.
- // Handler 25 (Level 2 speeder bike): Same issue with corridor overlays.
+ // Handler 25 (Level 2 speeder bike): Corridor overlay is drawn in procPreRendering
+ // BEFORE FOBJs. FTCH would erase the overlay, making enemies draw on wrong background.
+ //
+ // Handler 8 (Level 2 on-foot): FTCH is NOT skipped here. FTCH restores the clean
+ // background each frame, which properly erases old enemy sprite positions before
+ // new FOBJs draw updated positions. procPreRendering also restores _level2Background,
+ // but FTCH provides the authoritative clean slate from frame 0's stored background.
if (_vm->_game.id == GID_REBEL2 && _insane != nullptr) {
InsaneRebel2 *rebel2 = static_cast<InsaneRebel2 *>(_insane);
int handler = rebel2->getHandler();
- if (handler == 8 || handler == 25) {
- debug("SmushPlayer::handleFetch: Skipping FTCH for Handler %d - preserving overlays", handler);
+ if (handler == 25) {
+ debug("SmushPlayer::handleFetch: Skipping FTCH for Handler 25 - preserving overlays");
return;
}
}
@@ -1081,6 +1087,32 @@ void SmushPlayer::decodeFrameObject(int codec, const uint8 *src, int left, int t
if (_dst == _specialBuffer)
pitch = _width;
+ // RA2: Apply global FOBJ position offsets (matching original FUN_00423A50)
+ // These are set by InsaneRebel2 during IACT opcode 6 processing
+ if (_vm->_game.id == GID_REBEL2) {
+ left += _fobjOffsetX;
+ top += _fobjOffsetY;
+ }
+
+ // Bounds check: clamp FOBJ to destination buffer dimensions
+ int bufHeight = (_dst == _specialBuffer) ? _height : _vm->_screenHeight;
+ if (top < 0) {
+ height += top;
+ top = 0;
+ }
+ if (left < 0) {
+ width += left;
+ left = 0;
+ }
+ if (top + height > bufHeight) {
+ height = bufHeight - top;
+ }
+ if (left + width > pitch) {
+ width = pitch - left;
+ }
+ if (width <= 0 || height <= 0)
+ return;
+
switch (codec) {
case SMUSH_CODEC_RLE:
case SMUSH_CODEC_RLE_ALT:
@@ -1208,11 +1240,28 @@ void SmushPlayer::handleFrame(int32 frameSize, Common::SeekableReadStream &b) {
const int32 subSize = b.readUint32BE();
const int32 subOffset = b.pos();
- // Debug: Log all chunk types for first few frames
- if (_vm->_game.id == GID_REBEL2 && _frame < 5) {
+ // Debug: Log all chunk types for first 25 frames
+ if (_vm->_game.id == GID_REBEL2 && _frame < 25) {
debug("SmushPlayer::handleFrame: frame=%d chunk=%s size=%d", _frame, tag2str(subType), subSize);
}
+ // RA2 SKIP mechanism (matching original FUN_00423A50 bVar6):
+ // When _skipNext is set, skip the NEXT chunk of ANY type (FOBJ, PSAD, SKIP, etc.)
+ // In the original, bVar6 is checked at the top of the loop before the type switch,
+ // corrupting the tag so no handler matches. This consumes exactly one chunk.
+ // Critical: this must also skip SKIP chunks to prevent SKIPâSKIPâFOBJ misalignment.
+ if (_vm->_game.id == GID_REBEL2 && _skipNext) {
+ _skipNext = false;
+ debug("SmushPlayer::handleFrame: SKIP consumed chunk %s frame=%d", tag2str(subType), _frame);
+ frameSize -= subSize + 8;
+ b.seek(subOffset + subSize, SEEK_SET);
+ if (subSize & 1) {
+ b.skip(1);
+ frameSize--;
+ }
+ continue;
+ }
+
switch (subType) {
case MKTAG('N','P','A','L'):
handleNewPalette(subSize, b);
@@ -1224,13 +1273,6 @@ void SmushPlayer::handleFrame(int32 frameSize, Common::SeekableReadStream &b) {
handleZlibFrameObject(subSize, b);
break;
case MKTAG('P','S','A','D'):
- // Rebel Assault 2 only: Skip sound when _skipNext is set (enemy killed)
- // This mirrors the original game's FUN_00423A50 behavior where
- // SKIP tags cause subsequent PSAD/SAUD chunks to be skipped
- if (_vm->_game.id == GID_REBEL2 && _skipNext) {
- _skipNext = false;
- break;
- }
if (!_compressedFileMode) {
audioChunk = (uint8 *)malloc(subSize + 8);
b.seek(-8, SEEK_CUR);
diff --git a/engines/scumm/smush/smush_player.h b/engines/scumm/smush/smush_player.h
index 62b0e0c1500..43ecc269d9b 100644
--- a/engines/scumm/smush/smush_player.h
+++ b/engines/scumm/smush/smush_player.h
@@ -162,6 +162,12 @@ private:
bool _ra2FastForwarding; // Fast-forwarding RA2 BEG video to establish background
uint32 _frame;
+ // RA2: Global FOBJ position offsets (DAT_00482c1c / DAT_00482c20 in original)
+ // Set by InsaneRebel2 during IACT opcode 6 processing, reset in procPostRendering.
+ // Applied to all FOBJ left/top positions during decoding.
+ int16 _fobjOffsetX;
+ int16 _fobjOffsetY;
+
Audio::SoundHandle *_IACTchannel;
Audio::QueuingAudioStream *_IACTstream;
Commit: 41488ed0aaaa25e04c79d55863b0c720675559d6
https://github.com/scummvm/scummvm/commit/41488ed0aaaa25e04c79d55863b0c720675559d6
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:04+02:00
Commit Message:
SCUMM: RA2: Implement per-enemy explosion handling
Changed paths:
engines/scumm/insane/insane_rebel.cpp
engines/scumm/insane/insane_rebel.h
diff --git a/engines/scumm/insane/insane_rebel.cpp b/engines/scumm/insane/insane_rebel.cpp
index 28fd6a5d3d1..c8b8f7235e2 100644
--- a/engines/scumm/insane/insane_rebel.cpp
+++ b/engines/scumm/insane/insane_rebel.cpp
@@ -734,11 +734,33 @@ int32 InsaneRebel2::processMouse() {
it->id, it->type, mousePos.x, mousePos.y,
it->rect.left, it->rect.top, it->rect.right, it->rect.bottom);
- // Spawn explosion using native system
- // Use width / 2 as the scale parameter
- spawnExplosion((it->rect.left + it->rect.right) / 2,
- (it->rect.top + it->rect.bottom) / 2,
- it->rect.width() / 2);
+ // Spawn visual explosion based on handler and enemy type.
+ //
+ // Each handler's explosion rendering (FUN_409FBC, FUN_402696,
+ // FUN_40F1C5, FUN_41F29A) checks a per-level flags field:
+ // (*(ushort *)(&DAT_0047e108 + chapter*0x242 + level*0x22) & 1) == 0
+ // When bit 0 is SET, explosion NUT sprites are NOT rendered even
+ // though the counter ticks down. The flags come from GAME.TRS.
+ //
+ // Handler 8 (FUN_4028C5 line 94): Only type 0 sets the explosion
+ // counter at all. Types 1-4 get BLAST sound, no visual explosion.
+ //
+ // Handler 25 (FUN_41E7C2 line 74): Types > 3 DO set the counter,
+ // but rendering is suppressed by flags & 1 for on-foot levels.
+ // The counter serves only as a timer (sound panning, tracking).
+ // Handler 25 is specifically for on-foot corridor/FPS sections;
+ // space combat uses handler 7 instead.
+ //
+ // Handlers 0x26, 7: All types get visual explosions.
+ if (_rebelHandler != 8 && _rebelHandler != 25) {
+ spawnExplosion((it->rect.left + it->rect.right) / 2,
+ (it->rect.top + it->rect.bottom) / 2,
+ it->rect.width() / 2);
+ } else if (_rebelHandler == 8 && it->type == 0) {
+ spawnExplosion((it->rect.left + it->rect.right) / 2,
+ (it->rect.top + it->rect.bottom) / 2,
+ it->rect.width() / 2);
+ }
// Disable self (prevents sprite from rendering via SKIP chunks)
setBit(it->id);
@@ -759,7 +781,7 @@ int32 InsaneRebel2::processMouse() {
if (id >= 0 && id < 512) {
// Slot 2: Enable (Explosion?)
if (_rebelLinks[id][2] != 0) {
- clearBit(_rebelLinks[id][2]);
+ clearBit(_rebelLinks[id][2]);
debug("Rebel2: Enabled dependency Slot 2 (ID=%d) for Parent %d", _rebelLinks[id][2], id);
}
// Slot 1: Enable (Explosion?)
@@ -774,9 +796,15 @@ int32 InsaneRebel2::processMouse() {
}
}
- // Note: Background saving and masking is handled in procPostRendering
- // where we have access to the render bitmap
- // TODO: Play explosion sound
+ // Play explosion sound.
+ // Handler 8 types 1-4: BLAST.SAD (slot 0) via different sound function
+ // All others: EXPLODE.SAD (slot 2)
+ // TODO: Implement actual SAD sound playback
+ if (_rebelHandler == 8 && it->type >= 1 && it->type <= 4) {
+ debug("Rebel2: BLAST sound for enemy type %d (no visual explosion)", it->type);
+ } else {
+ debug("Rebel2: EXPLODE sound for enemy type %d", it->type);
+ }
// Award score for destroying enemy (FUN_0041bf8d called from FUN_40A2E0)
// Score value comes from lookup table DAT_0047e0fe indexed by difficulty
@@ -2684,6 +2712,9 @@ void InsaneRebel2::enemyUpdate(byte *renderBitmap, Common::SeekableReadStream &b
// In the original (FUN_004028C5/FUN_0041E7C2): sVar5/sVar2 = *(short *)(*local + 6)
// This maps to par4 (userId field). Used for DAT_0047ab98 wave state bitmask:
// DAT_0047ab98 |= 1 << (type & 0x1f)
+ debug(5, "Rebel2 Opcode4: handler=%d enemyId=%d par2=%d par3=%d par4/type=%d pos=(%d,%d) size=(%d,%d)",
+ _rebelHandler, enemyId, par2, par3, par4, x, y, w, h);
+
bool found = false;
Common::List<enemy>::iterator it;
for (it = _enemies.begin(); it != _enemies.end(); ++it) {
@@ -3124,8 +3155,15 @@ void InsaneRebel2::loadEmbeddedSan(int userId, byte *animData, int32 size, byte
end_parsing:;
}
+// Spawn explosion into the shared 5-slot system.
+// In the original, each handler has its own spawn logic inside its enemy processing function:
+// Handler 0x26: FUN_40A2E0 (0x40A2E0) â spawns in slot arrays DAT_0044368e[]
+// Handler 8: FUN_4028C5 (0x4028C5) â spawns in slot arrays DAT_0043f854[]
+// Handler 7: FUN_40F628 (0x40F628) â spawns in slot arrays DAT_00443770[]
+// Handler 25: FUN_41E7C2 (0x41E7C2) â spawns in slot arrays DAT_0045792c[]
+// All share the same logic: find first free slot (counter==0), set counter=10,
+// scale=objectHalfWidth, position=enemy center, velocity=0.
void InsaneRebel2::spawnExplosion(int x, int y, int objectHalfWidth) {
- // Find first free slot (FUN_40A2E0 logic)
for (int i = 0; i < 5; i++) {
if (!_explosions[i].active || _explosions[i].counter <= 0) {
_explosions[i].active = true;
@@ -3133,7 +3171,6 @@ void InsaneRebel2::spawnExplosion(int x, int y, int objectHalfWidth) {
_explosions[i].x = x;
_explosions[i].y = y;
_explosions[i].scale = objectHalfWidth;
- // TODO: Play sound via FUN_0041189e equivalent
break;
}
}
@@ -4915,9 +4952,172 @@ void InsaneRebel2::renderEnemyOverlays(byte *renderBitmap, int pitch, int width,
}
}
+// Dispatcher â calls per-handler explosion render function.
+// Original code has separate functions per handler, each with its own
+// position transformation, scale thresholds, and secondary NUT rendering.
void InsaneRebel2::renderExplosions(byte *renderBitmap, int pitch, int width, int height) {
- // Draw explosion animations from 5-slot system
+ switch (_rebelHandler) {
+ case 0x26:
+ renderTurretExplosions(renderBitmap, pitch, width, height);
+ break;
+ case 8:
+ renderVehicleExplosions(renderBitmap, pitch, width, height);
+ break;
+ case 7:
+ renderSpaceExplosions(renderBitmap, pitch, width, height);
+ break;
+ case 25:
+ renderHandler25Explosions(renderBitmap, pitch, width, height);
+ break;
+ default:
+ break;
+ }
+}
+
+// FUN_409FBC â Handler 0x26 (Turret/Cockpit) explosion rendering.
+// Position: Uses FUN_0041c720 for 3Dâ2D projection. At low-res, world coords â screen coords.
+// Scale thresholds: Fixed (<11, <21).
+// Secondary NUT: DAT_0047fe80 (rendered if DAT_0047a7fc >= 0).
+// Hi-res: Coordinates doubled when DAT_0047a808 >= 2.
+void InsaneRebel2::renderTurretExplosions(byte *renderBitmap, int pitch, int width, int height) {
+ if (!_smush_iconsNut)
+ return;
+
+ for (int i = 0; i < 5; i++) {
+ if (!_explosions[i].active)
+ continue;
+
+ if (_explosions[i].counter <= 0) {
+ _explosions[i].active = false;
+ continue;
+ }
+
+ // FUN_409FBC: Fixed thresholds (0x0b=11, 0x15=21)
+ int baseIndex;
+ if (_explosions[i].scale < 11) {
+ baseIndex = 9; // Small (sprites 11-20)
+ } else if (_explosions[i].scale < 21) {
+ baseIndex = 19; // Medium (sprites 21-30)
+ } else {
+ baseIndex = 29; // Large (sprites 31-40)
+ }
+
+ int spriteIndex = baseIndex + (12 - _explosions[i].counter);
+
+ // Position: world coords passed through FUN_0041c720 (3Dâ2D projection).
+ // At 320x200 low-res turret view, projection is effectively identity.
+ int screenX = _explosions[i].x;
+ int screenY = _explosions[i].y;
+
+ if (_smush_iconsNut->getNumChars() > spriteIndex) {
+ int ew = _smush_iconsNut->getCharWidth(spriteIndex);
+ int eh = _smush_iconsNut->getCharHeight(spriteIndex);
+ renderNutSprite(renderBitmap, pitch, width, height,
+ screenX - ew / 2, screenY - eh / 2, _smush_iconsNut, spriteIndex);
+ }
+
+ _explosions[i].counter--;
+ }
+}
+
+// FUN_402696 â Handler 8 (Third-Person On-Foot) explosion rendering.
+// Position: World coords minus camera offset (DAT_0043e006/DAT_0043e008 = _viewX/_viewY).
+// Scale thresholds: Fixed (<11, <21) â same as handler 0x26.
+// Secondary NUT: None (only DAT_0047a828).
+void InsaneRebel2::renderVehicleExplosions(byte *renderBitmap, int pitch, int width, int height) {
+ if (!_smush_iconsNut)
+ return;
+
+ for (int i = 0; i < 5; i++) {
+ if (!_explosions[i].active)
+ continue;
+
+ if (_explosions[i].counter <= 0) {
+ _explosions[i].active = false;
+ continue;
+ }
+
+ // FUN_402696: Fixed thresholds (0x0b=11, 0x15=21)
+ int baseIndex;
+ if (_explosions[i].scale < 11) {
+ baseIndex = 9;
+ } else if (_explosions[i].scale < 21) {
+ baseIndex = 19;
+ } else {
+ baseIndex = 29;
+ }
+
+ int spriteIndex = baseIndex + (12 - _explosions[i].counter);
+
+ // FUN_402696 line 22-23: screenX = worldX - DAT_0043e006, screenY = worldY - DAT_0043e008
+ int screenX = _explosions[i].x - _viewX;
+ int screenY = _explosions[i].y - _viewY;
+
+ if (_smush_iconsNut->getNumChars() > spriteIndex) {
+ int ew = _smush_iconsNut->getCharWidth(spriteIndex);
+ int eh = _smush_iconsNut->getCharHeight(spriteIndex);
+ renderNutSprite(renderBitmap, pitch, width, height,
+ screenX - ew / 2, screenY - eh / 2, _smush_iconsNut, spriteIndex);
+ }
+
+ _explosions[i].counter--;
+ }
+}
+
+// FUN_40F1C5 â Handler 7 (Third-Person Ship) explosion rendering.
+// Position: Uses FUN_0041c720 for 3Dâ2D projection.
+// Scale thresholds: Resolution-dependent (low-res: <11/<21, high-res: <21/<41).
+// Secondary NUT: DAT_0047ff00 (FLY004, rendered if DAT_0047a7fc >= 0).
+void InsaneRebel2::renderSpaceExplosions(byte *renderBitmap, int pitch, int width, int height) {
+ if (!_smush_iconsNut)
+ return;
+
+ for (int i = 0; i < 5; i++) {
+ if (!_explosions[i].active)
+ continue;
+
+ if (_explosions[i].counter <= 0) {
+ _explosions[i].active = false;
+ continue;
+ }
+
+ // FUN_40F1C5 lines 41-51: Resolution-dependent thresholds.
+ // Low-res (DAT_0047a808 < 2): thresholds 20, 10
+ // High-res: thresholds 40, 20
+ // We run at low-res (320x200), so use 10/20 (same as fixed handlers).
+ int baseIndex;
+ if (_explosions[i].scale < 11) {
+ baseIndex = 9;
+ } else if (_explosions[i].scale < 21) {
+ baseIndex = 19;
+ } else {
+ baseIndex = 29;
+ }
+ int spriteIndex = baseIndex + (12 - _explosions[i].counter);
+
+ // Position: world coords through FUN_0041c720 (3Dâ2D projection).
+ // At low-res, this is close to identity for the ship view.
+ int screenX = _explosions[i].x;
+ int screenY = _explosions[i].y;
+
+ if (_smush_iconsNut->getNumChars() > spriteIndex) {
+ int ew = _smush_iconsNut->getCharWidth(spriteIndex);
+ int eh = _smush_iconsNut->getCharHeight(spriteIndex);
+ renderNutSprite(renderBitmap, pitch, width, height,
+ screenX - ew / 2, screenY - eh / 2, _smush_iconsNut, spriteIndex);
+ }
+
+ _explosions[i].counter--;
+ }
+}
+
+// FUN_41F29A â Handler 25 (FPS/Mixed) explosion rendering.
+// Position: World coords + view offset (DAT_0045790c/DAT_0045790e = _rebelViewOffsetX/_rebelViewOffsetY).
+// Scale thresholds: Resolution-dependent (same formula as Handler 7).
+// Secondary NUT: DAT_00482260 (hi-res HUD alternative, rendered if DAT_0047a7fc >= 0).
+// Note: No per-frame sound panning update (unlike handlers 0x26, 8, 7).
+void InsaneRebel2::renderHandler25Explosions(byte *renderBitmap, int pitch, int width, int height) {
if (!_smush_iconsNut)
return;
@@ -4930,25 +5130,27 @@ void InsaneRebel2::renderExplosions(byte *renderBitmap, int pitch, int width, in
continue;
}
- // Determine base sprite index based on scale (FUN_409FBC logic)
+ // FUN_41F29A lines 27-37: Resolution-dependent thresholds (same as Handler 7).
int baseIndex;
if (_explosions[i].scale < 11) {
- baseIndex = 9; // Small/Medium
+ baseIndex = 9;
} else if (_explosions[i].scale < 21) {
- baseIndex = 19; // Medium/Large
+ baseIndex = 19;
} else {
- baseIndex = 29; // Large/XL
+ baseIndex = 29;
}
- // Formula: Base + (12 - Counter)
int spriteIndex = baseIndex + (12 - _explosions[i].counter);
+ // FUN_41F29A line 22-23: screenX = worldX + DAT_0045790c, screenY = worldY + DAT_0045790e
+ int screenX = _explosions[i].x + _rebelViewOffsetX;
+ int screenY = _explosions[i].y + _rebelViewOffsetY;
+
if (_smush_iconsNut->getNumChars() > spriteIndex) {
int ew = _smush_iconsNut->getCharWidth(spriteIndex);
int eh = _smush_iconsNut->getCharHeight(spriteIndex);
- int cx = _explosions[i].x - ew / 2;
- int cy = _explosions[i].y - eh / 2;
- renderNutSprite(renderBitmap, pitch, width, height, cx, cy, _smush_iconsNut, spriteIndex);
+ renderNutSprite(renderBitmap, pitch, width, height,
+ screenX - ew / 2, screenY - eh / 2, _smush_iconsNut, spriteIndex);
}
_explosions[i].counter--;
diff --git a/engines/scumm/insane/insane_rebel.h b/engines/scumm/insane/insane_rebel.h
index 3c5e92b1492..e526234ac13 100644
--- a/engines/scumm/insane/insane_rebel.h
+++ b/engines/scumm/insane/insane_rebel.h
@@ -350,7 +350,7 @@ public:
// Draw enemy indicator brackets and erase destroyed enemy areas
void renderEnemyOverlays(byte *renderBitmap, int pitch, int width, int height, int videoWidth);
- // Draw explosion animations from 5-slot system
+ // Draw explosion animations from 5-slot system (dispatcher)
void renderExplosions(byte *renderBitmap, int pitch, int width, int height);
// Draw laser shot beams and impacts
@@ -709,6 +709,12 @@ public:
void spawnHandler25Shot(int x, int y); // Handler 25 (speeder bike)
void spawnShot(int x, int y); // Dispatcher based on current handler
+ // Handler-specific explosion rendering
+ void renderTurretExplosions(byte *renderBitmap, int pitch, int width, int height); // FUN_409FBC (Handler 0x26)
+ void renderVehicleExplosions(byte *renderBitmap, int pitch, int width, int height); // FUN_402696 (Handler 8)
+ void renderSpaceExplosions(byte *renderBitmap, int pitch, int width, int height); // FUN_40F1C5 (Handler 7)
+ void renderHandler25Explosions(byte *renderBitmap, int pitch, int width, int height); // FUN_41F29A (Handler 25)
+
// Handler-specific laser rendering (FUN_40AD63, FUN_402ED0, FUN_40FADF, FUN_0041f004)
void renderTurretLaserShots(byte *renderBitmap, int pitch, int width, int height);
void renderVehicleLaserShots(byte *renderBitmap, int pitch, int width, int height);
Commit: bcd6615899b34794ee059d90d9c09dd411172652
https://github.com/scummvm/scummvm/commit/bcd6615899b34794ee059d90d9c09dd411172652
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:04+02:00
Commit Message:
SCUMM: RA2: Implement damage handling
Changed paths:
engines/scumm/insane/insane_rebel.cpp
engines/scumm/insane/insane_rebel.h
diff --git a/engines/scumm/insane/insane_rebel.cpp b/engines/scumm/insane/insane_rebel.cpp
index c8b8f7235e2..b4b17c1881a 100644
--- a/engines/scumm/insane/insane_rebel.cpp
+++ b/engines/scumm/insane/insane_rebel.cpp
@@ -1065,7 +1065,9 @@ void InsaneRebel2::iactRebel2Scene1(byte *renderBitmap, int32 codecparam, int32
if (par2 == 0x0D || par2 == 0x0E) {
// Register the collision zone from the remaining IACT data
- registerCollisionZone(b, par2);
+ // par4 (userId from IACT header) is the filter value used by FUN_4092D9
+ // for the < 1000 test (offset +6 in the original stored pointer)
+ registerCollisionZone(b, par2, par4);
}
} else if (par1 == 7) {
@@ -1218,18 +1220,77 @@ void InsaneRebel2::iactRebel2Opcode2(Common::SeekableReadStream &b, int16 par2,
}
}
void InsaneRebel2::iactRebel2Opcode3(Common::SeekableReadStream &b, int16 par2, int16 par3, int16 par4) {
- // FUN_00401234 case 1 (Handler 8): IACT opcode 3 â damage and hit counter.
+ // IACT opcode 3 â damage and hit counter processing.
+ // Based on FUN_4092D9 (Handler 0x26), FUN_40E35E (Handler 7), FUN_401234 (Handler 8).
//
- // par3 == 5: Damage path â probability check, accumulate damage, trigger visual effect
- // Original reads probability from per-level table DAT_0047e0fc[levelIdx]
- // and damage amount from DAT_0047e0f8[levelIdx].
- // triggerDamageEffect (FUN_0042073B) is called OUTSIDE invulnerability check.
+ // The common dispatcher (FUN_4033CF) stores opcode 3 entries in the projectile impact
+ // list (DAT_0043f9e0). For handlers 0x26/7 these are processed per-frame by the
+ // per-handler collision function (FUN_4092D9/FUN_40E35E). For handlers 8/25 they're
+ // processed immediately during IACT dispatch.
//
- // par3 == 1: Hit counter increment (DAT_0047ab80)
+ // FUN_403ba9() loop in FUN_4092D9 (lines 209-239):
+ // par3 == 1/2: Direct hit â increment hit counter, apply damage if conditions met
+ // - body[0] (offset +8): srcId for isBitSet check
+ // - par4 != 0: damage from DAT_0047e0f4 (direct hit damage table)
+ // - par3==1: par4 must be 1..9 for damage
+ // - par3==2: par4 must be > 99, with wave state bit check for par4 >= 101
+ //
+ // par3 == 5: Probabilistic damage â probability check from DAT_0047e0fc
+ // - body[1] (offset +10): srcId for isBitSet check (different from par3=1/2!)
+ // - Damage from DAT_0047e0f8 (probabilistic damage table)
+ //
+ // Stream position on entry: at offset +8 (body[0], first word after 8-byte header)
+
+ if (par3 == 1 || par3 == 2) {
+ // Direct hit path â FUN_4092D9 lines 209-227
+ int16 srcId = b.readSint16LE(); // body[0] (offset +8): source enemy ID
+
+ debug("Rebel2 Opcode3: par3=%d par4=%d srcId=%d isBitSet=%d",
+ par3, par4, srcId, isBitSet(srcId));
+
+ // FUN_00423970(srcId): only proceed if source enemy is active (bit clear)
+ if (!isBitSet(srcId)) {
+ // Always increment hit counter (DAT_0047ab80) â line 215
+ _rebelHitCounter++;
+ debug("Rebel2: Incremented hit counter -> %d", _rebelHitCounter);
+
+ // Damage path: par4 must be non-zero AND direct hit damage table > 0
+ // TODO: Read actual damage from per-level table DAT_0047e0f4
+ int directHitDamage = 8 + (_difficulty * 4);
+
+ if (par4 != 0 && directHitDamage > 0) {
+ bool shouldDamage = false;
+
+ if (par3 == 1 && par4 < 10) {
+ // par3=1: simple direct hit, par4 is hit strength (1..9)
+ shouldDamage = true;
+ } else if (par3 == 2 && par4 > 99) {
+ // par3=2: wave-gated hit, par4 > 99 required
+ // Additional check: for par4 >= 101 (0x65), verify wave state bit
+ // Original: (DAT_0047ab9c & (1 << ((par4 + 0x9b) & 0x1f))) == 0
+ if (par4 < 0x65 || (_rebelPhaseState & (1 << ((par4 + 0x9b) & 0x1f))) == 0) {
+ shouldDamage = true;
+ }
+ }
- if (par3 == 5) {
- b.skip(2); // Offset +8 (unused)
- int16 srcId = b.readSint16LE(); // Offset +10: source enemy ID (local_14[5])
+ if (shouldDamage) {
+ // Apply damage unless invulnerable (DAT_0047ab64 == 0)
+ if (!_rebelInvulnerable) {
+ _playerDamage += directHitDamage;
+ if (_playerDamage > 255) _playerDamage = 255;
+ debug("Rebel2: DIRECT HIT damage from enemy %d. par3=%d par4=%d damage=%d total=%d",
+ srcId, par3, par4, directHitDamage, _playerDamage);
+ }
+ // Visual effect â FUN_00420515 (palette flash)
+ initDamageFlash();
+ // TODO: FUN_0041189e(1, 0, 0x7f, 0, 0) â play hit sound
+ }
+ }
+ }
+ } else if (par3 == 5) {
+ // Probabilistic damage path â FUN_4092D9 lines 228-239
+ b.skip(2); // Skip body[0] (offset +8, not used for par3=5)
+ int16 srcId = b.readSint16LE(); // body[1] (offset +10): source enemy ID
debug("Rebel2 Opcode3: par3=5 srcId=%d isBitSet=%d", srcId, isBitSet(srcId));
@@ -1251,7 +1312,8 @@ void InsaneRebel2::iactRebel2Opcode3(Common::SeekableReadStream &b, int16 par2,
int damageAmount = 5 + (_difficulty * 2);
_playerDamage += damageAmount;
if (_playerDamage > 255) _playerDamage = 255;
- debug("Rebel2: Damage HIT by Enemy %d. Damage=%d", srcId, _playerDamage);
+ debug("Rebel2: PROBABILISTIC damage from enemy %d. Damage=%d total=%d",
+ srcId, damageAmount, _playerDamage);
}
// Visual effect â called regardless of invulnerability.
// Handler 8: FUN_0042073B â palette flash + screen shake
@@ -1264,10 +1326,8 @@ void InsaneRebel2::iactRebel2Opcode3(Common::SeekableReadStream &b, int16 par2,
// TODO: FUN_0041189e(1, 0, 0x7f, 0, 0) â play hit sound
}
}
- } else if (par3 == 1) {
- // Hit counter increment: unconditional in original (no isBitSet or par4 check)
- _rebelHitCounter++;
- debug("Rebel2: Incremented hit counter DAT_0047ab80 -> %d", _rebelHitCounter);
+ } else {
+ debug("Rebel2 Opcode3: UNHANDLED par3=%d par4=%d", par3, par4);
}
}
@@ -3663,31 +3723,39 @@ void InsaneRebel2::drawCornerBrackets(byte *dst, int pitch, int width, int heigh
// Based on FUN_40E35E, FUN_40C3CC disassembly from info.md
// Zones are quadrilaterals registered via IACT opcode 5
-void InsaneRebel2::registerCollisionZone(Common::SeekableReadStream &b, int16 subOpcode) {
- // IACT Opcode 5 data layout (from info.md):
- // +0x00: opcode (5) - already read by caller
- // +0x02: sub-opcode (0x0D or 0x0E) - passed as parameter
- // +0x04: par3 (flags)
- // +0x06: zoneType
- // +0x08: frameStart
- // +0x0A: frameEnd
- // +0x0C-0x1A: X1,Y1,X2,Y2,X3,Y3,X4,Y4 vertex coordinates
+void InsaneRebel2::registerCollisionZone(Common::SeekableReadStream &b, int16 subOpcode, int16 par4) {
+ // IACT Opcode 5 data layout â corrected from FUN_4033CF / FUN_4092D9 analysis:
+ //
+ // Original game stores pointer to full IACT data (starting at opcode).
+ // SmushPlayer reads the first 8 bytes as header (code/flags/unknown/userId),
+ // so our stream starts at body[0] (IACT byte offset +8).
//
- // The stream position is currently at offset +0x04 (after opcode and sub-opcode)
-
- int16 par3 = b.readSint16LE(); // +0x04 (flags - unused for now)
- (void)par3; // Suppress unused variable warning
- int16 zoneType = b.readSint16LE(); // +0x06
- int16 frameStart = b.readSint16LE(); // +0x08
- int16 frameEnd = b.readSint16LE(); // +0x0A
- int16 x1 = b.readSint16LE(); // +0x0C
- int16 y1 = b.readSint16LE(); // +0x0E
- int16 x2 = b.readSint16LE(); // +0x10
- int16 y2 = b.readSint16LE(); // +0x12
- int16 x3 = b.readSint16LE(); // +0x14
- int16 y3 = b.readSint16LE(); // +0x16
- int16 x4 = b.readSint16LE(); // +0x18
- int16 y4 = b.readSint16LE(); // +0x1A
+ // FUN_4092D9 field mapping (byte offsets from stored pointer):
+ // +0x00: opcode (5) â already consumed by SmushPlayer
+ // +0x02: par2 (sub-opcode) â already consumed, passed as parameter
+ // +0x04: par3 â already consumed by SmushPlayer
+ // +0x06: par4 (userId) â filter value for < 1000 test, passed as parameter
+ // +0x08: body[0] (sVar1) â control field 1 (frame check: field2-1 == field1)
+ // +0x0A: body[1] (sVar2) â control field 2
+ // +0x0C: body[2] â vertex 1 X
+ // +0x0E: body[3] â vertex 1 Y
+ // +0x10: body[4] â vertex 2 X
+ // +0x12: body[5] â vertex 2 Y
+ // +0x14: body[6] â vertex 3 X
+ // +0x16: body[7] â vertex 3 Y
+ // +0x18: body[8] â vertex 4 X
+ // +0x1A: body[9] â vertex 4 Y
+
+ int16 field1 = b.readSint16LE(); // body[0] â control field 1
+ int16 field2 = b.readSint16LE(); // body[1] â control field 2
+ int16 x1 = b.readSint16LE(); // body[2] â vertex 1 X
+ int16 y1 = b.readSint16LE(); // body[3] â vertex 1 Y
+ int16 x2 = b.readSint16LE(); // body[4] â vertex 2 X
+ int16 y2 = b.readSint16LE(); // body[5] â vertex 2 Y
+ int16 x3 = b.readSint16LE(); // body[6] â vertex 3 X
+ int16 y3 = b.readSint16LE(); // body[7] â vertex 3 Y
+ int16 x4 = b.readSint16LE(); // body[8] â vertex 4 X
+ int16 y4 = b.readSint16LE(); // body[9] â vertex 4 Y
CollisionZone zone;
zone.x1 = x1;
@@ -3698,24 +3766,22 @@ void InsaneRebel2::registerCollisionZone(Common::SeekableReadStream &b, int16 su
zone.y3 = y3;
zone.x4 = x4;
zone.y4 = y4;
- zone.frameStart = frameStart;
- zone.frameEnd = frameEnd;
- zone.zoneType = zoneType;
+ zone.field1 = field1;
+ zone.field2 = field2;
+ zone.filterValue = par4;
zone.subOpcode = subOpcode;
zone.active = true;
// Register zone into appropriate table based on sub-opcode
if (subOpcode == 0x0D && _primaryZoneCount < kMaxCollisionZones) {
- // Primary collision zones (obstacles)
_primaryZones[_primaryZoneCount++] = zone;
- debug("Rebel2: Registered PRIMARY collision zone %d: type=%d frames=[%d-%d] quad=(%d,%d)-(%d,%d)-(%d,%d)-(%d,%d)",
- _primaryZoneCount - 1, zoneType, frameStart, frameEnd,
+ debug("Rebel2: Registered PRIMARY zone %d: filter=%d fields=[%d,%d] quad=(%d,%d)-(%d,%d)-(%d,%d)-(%d,%d)",
+ _primaryZoneCount - 1, par4, field1, field2,
x1, y1, x2, y2, x3, y3, x4, y4);
} else if (subOpcode == 0x0E && _secondaryZoneCount < kMaxCollisionZones) {
- // Secondary collision zones (boundaries)
_secondaryZones[_secondaryZoneCount++] = zone;
- debug("Rebel2: Registered SECONDARY collision zone %d: type=%d frames=[%d-%d] quad=(%d,%d)-(%d,%d)-(%d,%d)-(%d,%d)",
- _secondaryZoneCount - 1, zoneType, frameStart, frameEnd,
+ debug("Rebel2: Registered SECONDARY zone %d: filter=%d fields=[%d,%d] quad=(%d,%d)-(%d,%d)-(%d,%d)-(%d,%d)",
+ _secondaryZoneCount - 1, par4, field1, field2,
x1, y1, x2, y2, x3, y3, x4, y4);
} else {
debug("Rebel2: WARNING - Could not register zone (subOpcode=%d, primary=%d, secondary=%d)",
@@ -3730,6 +3796,112 @@ void InsaneRebel2::resetCollisionZones() {
_secondaryZoneCount = 0;
}
+void InsaneRebel2::checkCollisionZones() {
+ // Per-frame collision checking â FUN_4092D9 first loop (lines 39-202).
+ // Tests aim/ship position against primary collision zone quadrilaterals.
+ //
+ // Original coordinate system:
+ // Zone vertices are in 424x260 buffer space, centered by subtracting (0xD4=212, 0x82=130).
+ // Aim position (DAT_00443668/DAT_0044366a) is in centered coords [-52..52, -45..45].
+ // In FUN_407FCB: DAT_00443668 is a smoothed mouse-derived position.
+ //
+ // For our implementation:
+ // Map mouse position to centered coords matching the original range.
+ // Mouse X 0..320 â centered X â [-52..52] (with smoothing in original)
+ // Mouse Y 0..200 â centered Y â [-45..45]
+
+ if (_primaryZoneCount == 0) return;
+
+ // Calculate aim position in centered coordinates.
+ // Original: local_10 = mouseOffset + 0xa0, then smoothed and clamped to [-0x34..0x34]
+ // Simplified mapping: mouse 0..320 â [-52..52], mouse 0..200 â [-45..45]
+ int16 aimX = (int16)((_vm->_mouse.x - 160) * 52 / 160);
+ int16 aimY = (int16)((100 - _vm->_mouse.y) * 45 / 100);
+
+ // Clamp to original ranges (DAT_0047a7fc < 1 path)
+ if (aimX > 0x34) aimX = 0x34;
+ if (aimX < -0x34) aimX = -0x34;
+ if (aimY > 0x2d) aimY = 0x2d;
+ if (aimY < -0x2d) aimY = -0x2d;
+
+ for (int i = 0; i < _primaryZoneCount; i++) {
+ CollisionZone &zone = _primaryZones[i];
+ if (!zone.active) continue;
+
+ // Filter: only process zones with filterValue < 1000 (par4 from IACT header)
+ // Original: *(short *)(*local_c + 6) < 1000
+ if (zone.filterValue >= 1000) continue;
+
+ // Frame check: field2 - 1 == field1
+ // Original: sVar2 + -1 == (int)sVar1
+ if (zone.field2 - 1 != zone.field1) continue;
+
+ // Center zone vertices by subtracting buffer center (0xD4=212, 0x82=130)
+ // Original: sVar4 = x1 - 0xD4, sVar8 = y1 - 0x82, etc.
+ int cx1 = zone.x1 - 0xD4;
+ int cy1 = zone.y1 - 0x82;
+ int cx2 = zone.x2 - 0xD4;
+ int cy2 = zone.y2 - 0x82;
+ int cx3 = zone.x3 - 0xD4;
+ int cy3 = zone.y3 - 0x82;
+ int cx4 = zone.x4 - 0xD4;
+ int cy4 = zone.y4 - 0x82;
+
+ // Point-in-quadrilateral test â FUN_4092D9 lines 119-128
+ // Tests if aim position is OUTSIDE the safe corridor (= collision with obstacle).
+ // Original uses 4 edge interpolation tests connected by OR (any failure = collision).
+ //
+ // Edge 1: interpolate Y along top edge (v1âv2) at aim X position
+ // if aimY < interpolated Y â outside top edge â collision
+ // Edge 2: interpolate Y along bottom edge (v4âv3) at aim X position
+ // if interpolated Y < aimY â outside bottom edge â collision
+ // Edge 3: interpolate X along left edge (v1âv4) at aim Y position
+ // if aimX < interpolated X â outside left edge â collision
+ // Edge 4: interpolate X along right edge (v2âv3) at aim Y position
+ // if interpolated X < aimX â outside right edge â collision
+ bool collision = false;
+
+ // Avoid division by zero for degenerate edges
+ if (cx2 != cx1) {
+ int interpY1 = ((aimX - cx1) * (cy2 - cy1)) / (cx2 - cx1) + cy1;
+ if (aimY < interpY1) collision = true;
+ }
+ if (!collision && cx3 != cx4) {
+ int interpY2 = ((aimX - cx4) * (cy3 - cy4)) / (cx3 - cx4) + cy4;
+ if (interpY2 < aimY) collision = true;
+ }
+ if (!collision && cy4 != cy1) {
+ int interpX1 = ((aimY - cy1) * (cx4 - cx1)) / (cy4 - cy1) + cx1;
+ if (aimX < interpX1) collision = true;
+ }
+ if (!collision && cy3 != cy2) {
+ int interpX2 = ((aimY - cy2) * (cx3 - cx2)) / (cy3 - cy2) + cx2;
+ if (interpX2 < aimX) collision = true;
+ }
+
+ if (collision) {
+ // Collision detected â apply damage from collision damage table
+ // Original: DAT_0047a7ec += DAT_0047e0f6[levelIdx]
+ // TODO: Read from per-level collision damage table DAT_0047e0f6
+ int collisionDamage = 3 + (_difficulty * 2);
+
+ if (!_rebelInvulnerable) {
+ _playerDamage += collisionDamage;
+ if (_playerDamage > 255) _playerDamage = 255;
+ debug("Rebel2: COLLISION damage! zone=%d aim=(%d,%d) damage=%d total=%d",
+ i, aimX, aimY, collisionDamage, _playerDamage);
+ }
+ // Visual effect â FUN_00420515 (palette flash)
+ initDamageFlash();
+ // TODO: FUN_0041189e sound based on collision direction
+ } else {
+ // Safely passed â award score bonus
+ // Original: FUN_0041bf8d(DAT_0047e100[levelIdx])
+ addScore(1);
+ }
+ }
+}
+
void InsaneRebel2::drawQuad(byte *dst, int pitch, int width, int height,
int x1, int y1, int x2, int y2, int x3, int y3, int x4, int y4, byte color) {
// Draw a quadrilateral by connecting its 4 vertices with lines
@@ -4119,6 +4291,12 @@ void InsaneRebel2::procPostRendering(byte *renderBitmap, int32 codecparam, int32
updateDamageFlashPalette();
}
+ // Per-frame collision checking against registered zones (FUN_4092D9 first loop)
+ // Active for Handler 0x26 (turret) and Handler 7 (third-person ship)
+ if (_rebelHandler == 0x26 || _rebelHandler == 7) {
+ checkCollisionZones();
+ }
+
// Collision zone visualization (debug - for Handler 7/8 pilot modes)
if (_rebelHandler == 7 || _rebelHandler == 8) {
drawCollisionZones(renderBitmap, pitch, width, height, 0);
diff --git a/engines/scumm/insane/insane_rebel.h b/engines/scumm/insane/insane_rebel.h
index e526234ac13..10282a8de88 100644
--- a/engines/scumm/insane/insane_rebel.h
+++ b/engines/scumm/insane/insane_rebel.h
@@ -533,14 +533,14 @@ public:
// +0x0C-0x1A: X1,Y1,X2,Y2,X3,Y3,X4,Y4 vertex coordinates
struct CollisionZone {
- int16 x1, y1; // Top-left
- int16 x2, y2; // Top-right
- int16 x3, y3; // Bottom-right
- int16 x4, y4; // Bottom-left
- int16 frameStart;
- int16 frameEnd;
- int16 zoneType;
- int16 subOpcode; // 0x0D = primary, 0x0E = secondary
+ int16 x1, y1; // Vertex 1 (body[2], body[3])
+ int16 x2, y2; // Vertex 2 (body[4], body[5])
+ int16 x3, y3; // Vertex 3 (body[6], body[7])
+ int16 x4, y4; // Vertex 4 (body[8], body[9])
+ int16 field1; // body[0] - control field (frame check: field2 - 1 == field1)
+ int16 field2; // body[1] - control field
+ int16 filterValue; // par4 from IACT header - used for < 1000 filter
+ int16 subOpcode; // 0x0D = primary, 0x0E = secondary
bool active;
};
@@ -569,11 +569,16 @@ public:
int x1, int y1, int x2, int y2, int x3, int y3, int x4, int y4, byte color);
// Register a collision zone from IACT opcode 5 data
- void registerCollisionZone(Common::SeekableReadStream &b, int16 subOpcode);
+ void registerCollisionZone(Common::SeekableReadStream &b, int16 subOpcode, int16 par4);
// Reset collision zone counters (called at end of frame)
void resetCollisionZones();
+ // Per-frame collision checking against registered zones (FUN_4092D9 first loop)
+ // Tests aim/ship position against primary zone quadrilaterals
+ // Applies collision damage from DAT_0047e0f6 when inside obstacle zone
+ void checkCollisionZones();
+
int16 _playerDamage; // Legacy damage counter (kept for compatibility/telemetry)
int16 _playerShield; // Shields: 0..255 where 255 = full
int16 _playerLives;
Commit: ca8874e261869f35a37126bf65b22eff5204a61e
https://github.com/scummvm/scummvm/commit/ca8874e261869f35a37126bf65b22eff5204a61e
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:05+02:00
Commit Message:
SCUMM: RA2: Handle damage in level 3
Changed paths:
engines/scumm/insane/insane_rebel.cpp
engines/scumm/insane/insane_rebel.h
diff --git a/engines/scumm/insane/insane_rebel.cpp b/engines/scumm/insane/insane_rebel.cpp
index b4b17c1881a..661df671547 100644
--- a/engines/scumm/insane/insane_rebel.cpp
+++ b/engines/scumm/insane/insane_rebel.cpp
@@ -260,10 +260,12 @@ InsaneRebel2::InsaneRebel2(ScummEngine_v7 *scumm) {
_primaryZones[i].active = false;
_secondaryZones[i].active = false;
}
+ // Corridor boundaries in game coordinate space (FUN_40C040 lines 21-24)
+ // DAT_00443b0a=0, DAT_00443b0c=0, DAT_00443b0e=0x1a8(424), DAT_00443b10=0x104(260)
_corridorLeftX = 0;
_corridorTopY = 0;
- _corridorRightX = 320;
- _corridorBottomY = 200;
+ _corridorRightX = 0x1A8; // 424 â full game buffer width
+ _corridorBottomY = 0x104; // 260 â full game buffer height
_hitCooldown = 0;
// Initialize legacy shot system (backwards compatibility)
@@ -1071,41 +1073,61 @@ void InsaneRebel2::iactRebel2Scene1(byte *renderBitmap, int32 codecparam, int32
}
} else if (par1 == 7) {
- // Opcode 7: Sprite/HUD control for Handler 7 (third-person ship levels like Level 3)
- // par2 = control type (41 = sprite selection?)
- // par3 = usually 0
- // par4 = sprite/slot ID (0 or 5 seen in Level 3)
+ // Opcode 7: Handler 7 corridor/velocity control (FUN_40C3CC case 5)
+ // IACT header: par1=7, par2=flags, par3=0, par4=sub-opcode
+ // Body contains 2 int16 values (body[0], body[1])
//
- // This opcode may control which ship direction sprite to display
- // or reference embedded graphics loaded elsewhere
- debug("Rebel2 IACT Opcode 7: par2=%d par3=%d par4=%d handler=%d",
- par2, par3, par4, _rebelHandler);
-
- // Read remaining IACT data to understand structure
- int64 startPos = b.pos();
- int64 remaining = b.size() - startPos;
- if (remaining > 0 && remaining <= 64) {
- byte payload[64];
- int bytesRead = b.read(payload, MIN((int64)64, remaining));
- debug("Rebel2 Opcode 7: payload (%d bytes): %02X %02X %02X %02X %02X %02X %02X %02X",
- bytesRead,
- bytesRead > 0 ? payload[0] : 0, bytesRead > 1 ? payload[1] : 0,
- bytesRead > 2 ? payload[2] : 0, bytesRead > 3 ? payload[3] : 0,
- bytesRead > 4 ? payload[4] : 0, bytesRead > 5 ? payload[5] : 0,
- bytesRead > 6 ? payload[6] : 0, bytesRead > 7 ? payload[7] : 0);
- b.seek(startPos);
- }
-
- // par2 == 41 (0x29) seems to be a common value
- // This might be a "show sprite" command referencing par4 as the slot
- if (par2 == 41) {
- // par4 could be a HUD slot or sprite index
- // For Handler 7, set which embedded HUD frame to display
- if (_rebelHandler == 7 && par4 >= 0 && par4 < 16) {
- // Mark this slot as the active one for direction-based rendering
- // This will be used in post-rendering to know which frame to show
- debug("Rebel2 Opcode 7: Activating HUD slot %d for Handler 7", par4);
+ // par4 sub-opcodes (from FUN_40C3CC case 5 switch on param_5[3]):
+ // 0: Set velocity params (DAT_00443b12, DAT_00443b14)
+ // 1: Set left X + top Y corridor boundaries (DAT_00443b0a, DAT_00443b0c)
+ // 2: Set right X + bottom Y corridor boundaries (DAT_00443b0e, DAT_00443b10)
+ // 5: Set flag (DAT_00443b52)
+
+ int16 body0 = 0, body1 = 0;
+ if (b.size() - b.pos() >= 4) {
+ body0 = b.readSint16LE();
+ body1 = b.readSint16LE();
+ }
+
+ switch (par4) {
+ case 0:
+ // Velocity/wind data â affects ship drift in FUN_40C3CC physics
+ // DAT_00443b12 = horizontal wind, DAT_00443b14 = vertical wind
+ debug("Rebel2 Opcode 7 par4=0: velocity=(%d,%d)", body0, body1);
+ break;
+ case 1:
+ // Set LEFT X boundary and TOP Y boundary
+ _corridorLeftX = body0;
+ _corridorTopY = body1;
+ // Mode-dependent margin adjustment (FUN_40C3CC lines 341-351)
+ if (_flyControlMode == 2) {
+ _corridorLeftX += 15;
+ } else if (_flyControlMode == 0) {
+ _corridorLeftX += 20;
+ }
+ debug("Rebel2 Opcode 7 par4=1: corridor left=%d top=%d (adjusted left=%d)",
+ body0, body1, _corridorLeftX);
+ break;
+ case 2:
+ // Set RIGHT X boundary and BOTTOM Y boundary
+ _corridorRightX = body0;
+ _corridorBottomY = body1;
+ // Mode-dependent margin adjustment (FUN_40C3CC lines 356-365)
+ if (_flyControlMode == 2) {
+ _corridorRightX -= 15;
+ } else if (_flyControlMode == 0) {
+ _corridorRightX -= 20;
}
+ debug("Rebel2 Opcode 7 par4=2: corridor right=%d bottom=%d (adjusted right=%d)",
+ body0, body1, _corridorRightX);
+ break;
+ case 5:
+ // Flag value
+ debug("Rebel2 Opcode 7 par4=5: flag=%d", body0);
+ break;
+ default:
+ debug("Rebel2 Opcode 7 par4=%d: body=(%d,%d) â unknown sub-opcode", par4, body0, body1);
+ break;
}
} else if (par1 == 6) {
@@ -1497,24 +1519,38 @@ void InsaneRebel2::iactRebel2Opcode6(byte *renderBitmap, Common::SeekableReadStr
// Handler 7 specific logic (third-person ship) - FUN_0040d836 / FUN_0040c3cc
// Used for Level 3 and similar space combat levels
if (_rebelHandler == 7) {
- // Set control mode (DAT_004437c0 = par3 in FUN_40C3CC case 4)
- // This determines shooting capability:
- // Mode 0: Flight/avoid mode - no shooting
- // Mode 1: Alternate flight mode - no shooting
- // Mode 2: Combat mode - shooting ENABLED
- _flyControlMode = par3;
+ // Set control mode: DAT_004437c0 = param_5[3] = par4 in FUN_40C3CC case 4.
+ // This determines collision mode and shooting capability:
+ // Mode 0: Obstacle avoidance â SECONDARY zones, corridor boundaries
+ // Mode 1: Tunnel flight â PRIMARY zones, per-edge push-back (hMargin=0x28)
+ // Mode 2: Combat mode â shooting ENABLED, SECONDARY zones
+ // Mode 3: Tunnel flight â PRIMARY zones, per-edge push-back (hMargin=0x0f)
+ _flyControlMode = par4;
debug("Rebel2 Opcode 6 (Handler 7): Control mode set to %d (shooting %s)",
- par3, (par3 == 2) ? "ENABLED" : "DISABLED");
-
- // If par4 == 1, enable status bar
- if (par4 == 1) {
+ par4, (par4 == 2) ? "ENABLED" : "DISABLED");
+
+ // Status bar: param_5[4] == 1 in original (first body word, 5th IACT word)
+ // In our parsing, par3 maps to param_5[2] and the body follows par4.
+ // FUN_40C3CC: if (param_5[4] == 1) FUN_0040bb87(DAT_0047a828,5);
+ // par3 is param_5[2], which the original doesn't use here.
+ // The body word for status bar is read separately below.
+ int16 bodyStatusFlag = 0;
+ if (b.size() - b.pos() >= 2) {
+ bodyStatusFlag = b.readSint16LE();
+ }
+ if (bodyStatusFlag == 1) {
_rebelStatusBarSprite = 5; // Status bar sprite
- debug("Rebel2 Opcode 6 (Handler 7): Status bar enabled");
+ debug("Rebel2 Opcode 6 (Handler 7): Status bar enabled (body flag=%d)", bodyStatusFlag);
}
- // Update ship screen position from mouse
- // Handler 7 uses DAT_0044370c (Y) and DAT_0044370e (X) for screen position
- // Get raw mouse position
+ // Update ship position from mouse input.
+ // CRITICAL: Ship position is in game coordinate space [20,404]x[20,240],
+ // centered at (212,130) â matching DAT_00443708/DAT_0044370a in the original.
+ // Collision zones are in this same space. Mouse coords must be converted.
+ //
+ // FUN_40C3CC case 4: original uses smoothed mouse velocity â position delta (±12/frame).
+ // Simplified here: direct mouse position mapping with smooth interpolation.
+
int16 rawMouseX = _vm->_mouse.x;
int16 rawMouseY = _vm->_mouse.y;
@@ -1528,66 +1564,97 @@ void InsaneRebel2::iactRebel2Opcode6(byte *renderBitmap, Common::SeekableReadStr
mouseY = (rawMouseY * 200) / _player->_height;
}
- // Clamp to screen bounds (matching FUN_0040c3cc bounds)
- if (mouseX < 0) mouseX = 0;
- if (mouseX > 319) mouseX = 319;
- if (mouseY < 0) mouseY = 0;
- if (mouseY > 199) mouseY = 199;
-
- // Update ship position with smooth interpolation
- // FUN_0040c3cc uses complex smoothing, we use simpler immediate response
- const int16 maxStep = 15;
- if (_shipPosX < mouseX) {
- _shipPosX = MIN((int16)(_shipPosX + maxStep), mouseX);
- } else if (_shipPosX > mouseX) {
- _shipPosX = MAX((int16)(_shipPosX - maxStep), mouseX);
- }
- if (_shipPosY < mouseY) {
- _shipPosY = MIN((int16)(_shipPosY + maxStep), mouseY);
- } else if (_shipPosY > mouseY) {
- _shipPosY = MAX((int16)(_shipPosY - maxStep), mouseY);
- }
-
- // Update Handler 7 screen position (DAT_0044370c/e)
- // These track the actual on-screen position for direction calculation
- _flyShipScreenX = _shipPosX;
- _flyShipScreenY = _shipPosY;
-
- // Calculate ship direction from position (FUN_0040d836 lines 88-106)
- // Formula from assembly:
- // hDir = (0xa0 - posX) >> 6 (with signed rounding)
- // vDir = (0x95 - posY) / 0x2b
- // dirIndex = hDir * 7 + vDir
- //
- // Note: The assembly formula gives:
- // hDir: 0-4 where 2 is center (0xa0=160, range is -96 to +96, >> 6 gives -1 to 1, but clamped to 0-4)
- // vDir: 0-6 where 3 is center (0x95=149, 0x2b=43, so 149/43 â 3.5)
- //
- // Simplified direction calculation based on mouse position relative to center
+ // Convert mouse coords [0,319]x[0,199] â game coords [20,404]x[20,240]
+ // Game center (212,130) maps to screen center (160,100)
+ int16 targetGameX = (int16)(mouseX * 384 / 319 + 20);
+ int16 targetGameY = (int16)(mouseY * 220 / 199 + 20);
+
+ // Clamp to game coordinate bounds (FUN_40C3CC lines 245-256)
+ if (targetGameX < 0x14) targetGameX = 0x14; // 20
+ if (targetGameX > 0x194) targetGameX = 0x194; // 404
+ if (targetGameY < 0x14) targetGameY = 0x14; // 20
+ if (targetGameY > 0xF0) targetGameY = 0xF0; // 240
+
+ // Smooth interpolation (original uses ±12 per frame max delta)
+ const int16 maxStep = 12;
+ if (_flyShipScreenX < targetGameX) {
+ _flyShipScreenX = MIN((int16)(_flyShipScreenX + maxStep), targetGameX);
+ } else if (_flyShipScreenX > targetGameX) {
+ _flyShipScreenX = MAX((int16)(_flyShipScreenX - maxStep), targetGameX);
+ }
+ if (_flyShipScreenY < targetGameY) {
+ _flyShipScreenY = MIN((int16)(_flyShipScreenY + maxStep), targetGameY);
+ } else if (_flyShipScreenY > targetGameY) {
+ _flyShipScreenY = MAX((int16)(_flyShipScreenY - maxStep), targetGameY);
+ }
+
+ // Corridor boundary collision (FUN_40C3CC lines 257-284)
+ // Mode 0/2: Ship X is clamped to corridor boundaries with damage
+ if (_flyControlMode == 0 || _flyControlMode == 2) {
+ // Right boundary collision
+ if (_corridorRightX < _flyShipScreenX) {
+ _flyShipScreenX = _corridorRightX;
+ if (_hitCooldown < 5 && !_rebelInvulnerable) {
+ int damage = 3 + (_difficulty * 2);
+ _playerDamage += damage;
+ if (_playerDamage > 255) _playerDamage = 255;
+ _rebelHitCounter++;
+ _hitCooldown = 10;
+ _spaceShotDirection = 1;
+ initDamageFlash();
+ debug("Rebel2: Handler7 RIGHT CORRIDOR HIT ship=(%d,%d) boundary=%d",
+ _flyShipScreenX, _flyShipScreenY, _corridorRightX);
+ }
+ }
+ // Left boundary collision
+ if (_flyShipScreenX < _corridorLeftX) {
+ _flyShipScreenX = _corridorLeftX;
+ if (_hitCooldown < 5 && !_rebelInvulnerable) {
+ int damage = 3 + (_difficulty * 2);
+ _playerDamage += damage;
+ if (_playerDamage > 255) _playerDamage = 255;
+ _rebelHitCounter++;
+ _hitCooldown = 10;
+ _spaceShotDirection = 0;
+ initDamageFlash();
+ debug("Rebel2: Handler7 LEFT CORRIDOR HIT ship=(%d,%d) boundary=%d",
+ _flyShipScreenX, _flyShipScreenY, _corridorLeftX);
+ }
+ }
+ // Y boundary clamping (no damage, just clamp) â lines 285-292
+ if (_corridorBottomY < _flyShipScreenY) {
+ _flyShipScreenY = _corridorBottomY;
+ }
+ if (_flyShipScreenY < _corridorTopY) {
+ _flyShipScreenY = _corridorTopY;
+ }
+ }
- // Horizontal direction (0-4, center=2)
- // Formula: (160 - posX) >> 6, clamped to 0-4
- int16 hDiff = 160 - _flyShipScreenX;
- int16 hDir = (hDiff + 64) >> 6; // Add 64 to shift range, divide by 64
+ // Also update _shipPosX/Y for other systems that use screen coords
+ _shipPosX = (int16)((_flyShipScreenX - 20) * 319 / 384);
+ _shipPosY = (int16)((_flyShipScreenY - 20) * 199 / 220);
+
+ // Direction calculation from ship game position
+ // Original FUN_0040d836 uses DAT_0044370c (smoothed velocity) for direction.
+ // Simplified: derive direction from position offset relative to center (212, 130).
+ // hDir: 0-4 where 2=center, based on offset from center X (212)
+ // vDir: 0-6 where 3=center, based on offset from center Y (130)
+ int16 hDiff = 0xD4 - _flyShipScreenX; // 212 - shipX
+ int16 hDir = (hDiff + 64) >> 6;
if (hDir < 0) hDir = 0;
if (hDir > 4) hDir = 4;
- // Vertical direction (0-6, center=3)
- // Formula: (149 - posY) / 43, clamped to 0-6
- int16 vDir = (149 - _flyShipScreenY) / 43;
+ int16 vDir = (130 - _flyShipScreenY) / 37; // adjusted divisor for game coord range
if (vDir < 0) vDir = 0;
if (vDir > 6) vDir = 6;
- // Additional adjustment from assembly (lines 90-105):
- // If vDir==3 and abs(posY) > 10, adjust by +/-1
- // If hDir==2 and abs(posX) > 15, adjust by +/-1
- // This creates a "deadzone" at center to reduce flicker
- if (vDir == 3 && ABS(_flyShipScreenY - 100) > 10) {
- if (_flyShipScreenY < 100) vDir = 2;
+ // Deadzone at center to reduce flicker
+ if (vDir == 3 && ABS(_flyShipScreenY - 130) > 13) {
+ if (_flyShipScreenY < 130) vDir = 2;
else vDir = 4;
}
- if (hDir == 2 && ABS(_flyShipScreenX - 160) > 15) {
- if (_flyShipScreenX < 160) hDir = 3;
+ if (hDir == 2 && ABS(_flyShipScreenX - 212) > 20) {
+ if (_flyShipScreenX < 212) hDir = 3;
else hDir = 1;
}
@@ -3902,6 +3969,184 @@ void InsaneRebel2::checkCollisionZones() {
}
}
+void InsaneRebel2::checkHandler7CollisionZones() {
+ // FUN_40E35E â Handler 7 per-frame collision system.
+ // Uses ship position (_flyShipScreenX/_flyShipScreenY) in raw buffer coords.
+ // Two modes depending on _flyControlMode:
+ // Mode 0/2: Obstacle collision using SECONDARY zones (inside quad = hit)
+ // Mode 1/3: Wall/boundary collision using PRIMARY zones (per-edge push-back)
+
+ // Decrement hit cooldown per frame
+ if (_hitCooldown > 0)
+ _hitCooldown--;
+
+ if (_flyControlMode == 0 || _flyControlMode == 2) {
+ // ---- Mode 0/2: Obstacle collision using SECONDARY zones (FUN_403b5b) ----
+ // Original lines 52-132: Point-in-quad test with 15px inward margin.
+ // Inside the quad = collision with obstacle.
+ const int margin = 15; // local_14 = 0x0f, local_20 = 0x0f
+
+ for (int i = 0; i < _secondaryZoneCount; i++) {
+ CollisionZone &zone = _secondaryZones[i];
+ if (!zone.active) continue;
+
+ int x1 = zone.x1, y1 = zone.y1;
+ int x2 = zone.x2, y2 = zone.y2;
+ int x3 = zone.x3, y3 = zone.y3;
+ int x4 = zone.x4, y4 = zone.y4;
+
+ // Point-in-quad test (lines 75-89)
+ // Start assuming inside, clear if outside any edge (with margin)
+ bool inside = true;
+
+ // Top edge: interpolate Y along v1âv2 at shipX, +15 margin
+ if (x2 != x1) {
+ int interpY = (_flyShipScreenX - x1) * (y2 - y1) / (x2 - x1) + margin + y1;
+ if (_flyShipScreenY < interpY) inside = false;
+ }
+ // Bottom edge: interpolate Y along v4âv3 at shipX, -15 margin
+ if (inside && x3 != x4) {
+ int interpY = (_flyShipScreenX - x4) * (y3 - y4) / (x3 - x4) + y4 - margin;
+ if (interpY < _flyShipScreenY) inside = false;
+ }
+ // Left edge: interpolate X along v1âv4 at shipY, +15 margin
+ if (inside && y4 != y1) {
+ int interpX = (_flyShipScreenY - y1) * (x4 - x1) / (y4 - y1) + margin + x1;
+ if (_flyShipScreenX < interpX) inside = false;
+ }
+ // Right edge: interpolate X along v2âv3 at shipY, -15 margin
+ if (inside && y3 != y2) {
+ int interpX = (_flyShipScreenY - y2) * (x3 - x2) / (y3 - y2) + x2 - margin;
+ if (interpX < _flyShipScreenX) inside = false;
+ }
+
+ // Frame match: field2 - 1 == field1 (line 90)
+ if (zone.field2 - 1 == zone.field1) {
+ if (inside) {
+ // Collision with obstacle â apply damage and break
+ _hitCooldown = 10;
+ _spaceShotDirection = zone.filterValue + 2;
+
+ int collisionDamage = 3 + (_difficulty * 2);
+ if (!_rebelInvulnerable) {
+ _playerDamage += collisionDamage;
+ if (_playerDamage > 255) _playerDamage = 255;
+ }
+ _rebelHitCounter++;
+ initDamageFlash();
+ debug("Rebel2: Handler7 Mode0/2 OBSTACLE HIT zone=%d ship=(%d,%d) damage=%d",
+ i, _flyShipScreenX, _flyShipScreenY, collisionDamage);
+ break; // Only one collision per frame (original breaks)
+ } else {
+ // Safely avoided obstacle â award score
+ addScore(1);
+ }
+ }
+ }
+
+ // Corridor boundary proximity (lines 127-131)
+ // These flags are used for directional indicators (not critical for damage)
+
+ } else {
+ // ---- Mode 1/3: Wall/boundary collision using PRIMARY zones (FUN_403b34) ----
+ // Original lines 133-235: Per-edge interpolation with push-back.
+ // Ship position is clamped to wall boundaries when hitting.
+ int16 hMargin = (_flyControlMode == 1) ? 0x28 : 0x0f; // local_14
+ const int16 vMargin = 0x0f; // local_20
+
+ for (int i = 0; i < _primaryZoneCount; i++) {
+ CollisionZone &zone = _primaryZones[i];
+ if (!zone.active) continue;
+
+ int x1 = zone.x1, y1 = zone.y1;
+ int x2 = zone.x2, y2 = zone.y2;
+ int x3 = zone.x3, y3 = zone.y3;
+ int x4 = zone.x4, y4 = zone.y4;
+
+ // Top edge: interpolate Y along v1âv2 at shipX (lines 152-166)
+ if (x2 != x1) {
+ int16 edgeY = (int16)((_flyShipScreenX - x1) * (y2 - y1) / (x2 - x1) + y1 + vMargin);
+ if (_flyShipScreenY < edgeY) {
+ // Ship above top wall â push down
+ if (_hitCooldown < 5 && !_rebelInvulnerable) {
+ int damage = 3 + (_difficulty * 2);
+ _playerDamage += damage;
+ if (_playerDamage > 255) _playerDamage = 255;
+ _rebelHitCounter++;
+ _hitCooldown = 10;
+ debug("Rebel2: Handler7 Mode1/3 TOP WALL ship=(%d,%d) edgeY=%d damage=%d",
+ _flyShipScreenX, _flyShipScreenY, edgeY, damage);
+ }
+ _spaceShotDirection = 2; // Direction: pushed down
+ _flyShipScreenY = edgeY; // Push-back
+ initDamageFlash();
+ }
+ }
+
+ // Bottom edge: interpolate Y along v4âv3 at shipX (lines 167-183)
+ if (x3 != x4) {
+ int16 edgeY = (int16)((_flyShipScreenX - x4) * (y3 - y4) / (x3 - x4) + y4 - vMargin);
+ _corridorBottomY = vMargin + edgeY; // DAT_00443b10 update
+ if (edgeY < _flyShipScreenY) {
+ // Ship below bottom wall â push up
+ if (_hitCooldown < 5 && !_rebelInvulnerable) {
+ int damage = 3 + (_difficulty * 2);
+ _playerDamage += damage;
+ if (_playerDamage > 255) _playerDamage = 255;
+ _rebelHitCounter++;
+ _hitCooldown = 10;
+ debug("Rebel2: Handler7 Mode1/3 BOTTOM WALL ship=(%d,%d) edgeY=%d damage=%d",
+ _flyShipScreenX, _flyShipScreenY, edgeY, damage);
+ }
+ _spaceShotDirection = 3; // Direction: pushed up
+ _flyShipScreenY = edgeY; // Push-back
+ initDamageFlash();
+ }
+ }
+
+ // Left edge: interpolate X along v1âv4 at shipY (lines 184-199)
+ if (y4 != y1) {
+ int16 edgeX = (int16)((_flyShipScreenY - y1) * (x4 - x1) / (y4 - y1) + x1 + hMargin);
+ if (_flyShipScreenX < edgeX) {
+ // Ship left of left wall â push right
+ _flyShipScreenX = edgeX; // Push-back
+ if (_hitCooldown < 5 && !_rebelInvulnerable) {
+ int damage = 3 + (_difficulty * 2);
+ _playerDamage += damage;
+ if (_playerDamage > 255) _playerDamage = 255;
+ _rebelHitCounter++;
+ _hitCooldown = 10;
+ debug("Rebel2: Handler7 Mode1/3 LEFT WALL ship=(%d,%d) edgeX=%d damage=%d",
+ _flyShipScreenX, _flyShipScreenY, edgeX, damage);
+ }
+ _spaceShotDirection = 0; // Direction: pushed right
+ initDamageFlash();
+ }
+ }
+
+ // Right edge: interpolate X along v2âv3 at shipY (lines 200-215)
+ if (y3 != y2) {
+ int16 edgeX = (int16)((_flyShipScreenY - y2) * (x3 - x2) / (y3 - y2) + x2 - hMargin);
+ if (edgeX < _flyShipScreenX) {
+ // Ship right of right wall â push left
+ _flyShipScreenX = edgeX; // Push-back
+ if (_hitCooldown < 5 && !_rebelInvulnerable) {
+ int damage = 3 + (_difficulty * 2);
+ _playerDamage += damage;
+ if (_playerDamage > 255) _playerDamage = 255;
+ _rebelHitCounter++;
+ _hitCooldown = 10;
+ debug("Rebel2: Handler7 Mode1/3 RIGHT WALL ship=(%d,%d) edgeX=%d damage=%d",
+ _flyShipScreenX, _flyShipScreenY, edgeX, damage);
+ }
+ _spaceShotDirection = 1; // Direction: pushed left
+ initDamageFlash();
+ }
+ }
+ }
+ }
+}
+
void InsaneRebel2::drawQuad(byte *dst, int pitch, int width, int height,
int x1, int y1, int x2, int y2, int x3, int y3, int x4, int y4, byte color) {
// Draw a quadrilateral by connecting its 4 vertices with lines
@@ -3956,7 +4201,7 @@ void InsaneRebel2::drawCollisionZones(byte *dst, int pitch, int width, int heigh
}
// Draw corridor boundaries as a rectangle (from IACT opcode 7)
- if (_corridorLeftX != 0 || _corridorRightX != 320) {
+ if (_corridorLeftX != 0 || _corridorRightX != 0x1A8) {
const byte corridorColor = 45; // Cyan for corridor boundaries
// Draw vertical lines for left/right boundaries
drawLine(dst, pitch, width, height,
@@ -4279,22 +4524,31 @@ void InsaneRebel2::procPostRendering(byte *renderBitmap, int32 codecparam, int32
// Handler 8: FUN_401CCF line 119 â FUN_00420754 (palette flash + screen shake)
// Handler 0x19: FUN_41DB5E line 192 â FUN_00420562 (palette flash only, every frame)
// Handler 0x26: FUN_4092D9 lines 135/225/237 â FUN_00420515 trigger + palette flash
- // Handler 7: No damage effects
+ // Handler 7: FUN_40E35E â FUN_00420515 trigger + palette flash
if (_rebelHandler == 8) {
// Full damage effect: palette flash + screen shake
// Suppressed during autopilot (mode 4) and cutscene (mode 5)
if (_shipLevelMode != 4 && _shipLevelMode != 5) {
updateDamageEffect(renderBitmap, pitch, width, height);
}
- } else if (_rebelHandler == 0x19 || _rebelHandler == 0x26) {
- // Palette flash only â no screen shake for turret/FPS handlers
+ } else if (_rebelHandler == 0x19 || _rebelHandler == 0x26 || _rebelHandler == 7) {
+ // Palette flash only â no screen shake for turret/FPS/ship handlers
updateDamageFlashPalette();
}
- // Per-frame collision checking against registered zones (FUN_4092D9 first loop)
- // Active for Handler 0x26 (turret) and Handler 7 (third-person ship)
- if (_rebelHandler == 0x26 || _rebelHandler == 7) {
+ // Per-frame collision checking against registered zones.
+ //
+ // Handler 0x26 (turret): FUN_4092D9 â aim position vs primary zones (centered coords)
+ // Zones with filterValue < 1000 tested via point-in-quad against mouse/aim position.
+ //
+ // Handler 7 (ship): FUN_40E35E â ship position vs zones per control mode:
+ // Mode 0/2: SECONDARY zones (0x0E) â obstacle collision (inside quad = hit)
+ // Mode 1/3: PRIMARY zones (0x0D) â wall/boundary per-edge with push-back
+ // Uses ship position in raw buffer coords, hit cooldown, directional damage.
+ if (_rebelHandler == 0x26) {
checkCollisionZones();
+ } else if (_rebelHandler == 7) {
+ checkHandler7CollisionZones();
}
// Collision zone visualization (debug - for Handler 7/8 pilot modes)
@@ -4726,10 +4980,11 @@ void InsaneRebel2::renderHandler7Ship(byte *renderBitmap, int pitch, int width,
return;
// Base position at screen center with direction offset
+ // Ship position is in game coords (center 212,130), convert to screen offset
int baseX = 160;
int baseY = 105;
- int16 posOffsetX = (_flyShipScreenX - 160) / 10;
- int16 posOffsetY = (_flyShipScreenY - 100) / 10;
+ int16 posOffsetX = (_flyShipScreenX - 0xD4) / 13; // (shipX - 212) / 13
+ int16 posOffsetY = (_flyShipScreenY - 0x82) / 11; // (shipY - 130) / 11
int shipScreenX = baseX + posOffsetX;
int shipScreenY = baseY + posOffsetY;
diff --git a/engines/scumm/insane/insane_rebel.h b/engines/scumm/insane/insane_rebel.h
index 10282a8de88..a1342cad786 100644
--- a/engines/scumm/insane/insane_rebel.h
+++ b/engines/scumm/insane/insane_rebel.h
@@ -579,6 +579,12 @@ public:
// Applies collision damage from DAT_0047e0f6 when inside obstacle zone
void checkCollisionZones();
+ // Handler 7 collision system (FUN_40E35E)
+ // Mode 0/2: Obstacle collision using secondary zones â inside quad = hit
+ // Mode 1/3: Wall/boundary collision using primary zones â per-edge push-back
+ // Uses ship position (_flyShipScreenX/_flyShipScreenY) in raw buffer coords
+ void checkHandler7CollisionZones();
+
int16 _playerDamage; // Legacy damage counter (kept for compatibility/telemetry)
int16 _playerShield; // Shields: 0..255 where 255 = full
int16 _playerLives;
Commit: 7d6172651310b73b59bb577afbc63a0564b24471
https://github.com/scummvm/scummvm/commit/7d6172651310b73b59bb577afbc63a0564b24471
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:05+02:00
Commit Message:
SCUMM: RA2: Improve ship control for level 3
Changed paths:
engines/scumm/insane/insane_rebel.cpp
engines/scumm/insane/insane_rebel.h
diff --git a/engines/scumm/insane/insane_rebel.cpp b/engines/scumm/insane/insane_rebel.cpp
index 661df671547..61aa13ac4d0 100644
--- a/engines/scumm/insane/insane_rebel.cpp
+++ b/engines/scumm/insane/insane_rebel.cpp
@@ -339,6 +339,17 @@ InsaneRebel2::InsaneRebel2(ScummEngine_v7 *scumm) {
_flyHiResSprite = nullptr; // FLY004 - high-res alternative
_flyShipScreenX = 0xd4; // Start at center (212) - matches DAT_00443708 default
_flyShipScreenY = 0x82; // Start at center (130) - matches DAT_0044370a default
+ _smoothedVelocity = 0; // DAT_0044370c
+ _verticalInput = 0; // DAT_0044370e
+ memset(_velocityHistory, 0, sizeof(_velocityHistory)); // DAT_00443716
+ memset(_windHistoryX, 0, sizeof(_windHistoryX)); // DAT_00443b16
+ memset(_windHistoryY, 0, sizeof(_windHistoryY)); // DAT_00443b34
+ _windParamX = 0; // DAT_00443b12
+ _windParamY = 0; // DAT_00443b14
+ _perspectiveX = 0; // DAT_00443712
+ _perspectiveY = 0; // DAT_00443714
+ _viewShift = 0; // DAT_00443710
+ _facingRight = false; // DAT_0047ab8c
// Initialize Handler 25 GRD ship system
_grd001Sprite = nullptr; // DAT_00482240 - GRD001 primary ship
@@ -1093,7 +1104,9 @@ void InsaneRebel2::iactRebel2Scene1(byte *renderBitmap, int32 codecparam, int32
case 0:
// Velocity/wind data â affects ship drift in FUN_40C3CC physics
// DAT_00443b12 = horizontal wind, DAT_00443b14 = vertical wind
- debug("Rebel2 Opcode 7 par4=0: velocity=(%d,%d)", body0, body1);
+ _windParamX = body0;
+ _windParamY = body1;
+ debug("Rebel2 Opcode 7 par4=0: wind=(%d,%d)", body0, body1);
break;
case 1:
// Set LEFT X boundary and TOP Y boundary
@@ -1543,85 +1556,165 @@ void InsaneRebel2::iactRebel2Opcode6(byte *renderBitmap, Common::SeekableReadStr
debug("Rebel2 Opcode 6 (Handler 7): Status bar enabled (body flag=%d)", bodyStatusFlag);
}
- // Update ship position from mouse input.
- // CRITICAL: Ship position is in game coordinate space [20,404]x[20,240],
- // centered at (212,130) â matching DAT_00443708/DAT_0044370a in the original.
- // Collision zones are in this same space. Mouse coords must be converted.
+ // ============================================================
+ // Ship position update â FUN_40C3CC case 4, lines 49-327
+ // ============================================================
+ // Velocity-based physics with momentum/inertia:
+ // Mouse offset from center â scaled input [-127,127]
+ // â velocity history averaging â physics delta (clamped ±12/frame)
+ // â position clamping â corridor collision â perspective offsets
//
- // FUN_40C3CC case 4: original uses smoothed mouse velocity â position delta (±12/frame).
- // Simplified here: direct mouse position mapping with smooth interpolation.
+ // Level data table (DAT_0047e0e8 + level*0x242 + difficulty*0x22):
+ // offset 0: smoothing param (>>4 +1 = window size)
+ // offset 2: Y speed offset 4: X speed (levelSpeed)
+ // offset 6: wind multiplier offset 14: corridor damage
+ // We don't have the actual level data, so we use calibrated defaults.
+
+ // --- Step 1: Mouse input as offset from screen center ---
+ // DAT_0047a7e0 = mouseX - 160, DAT_0047a7e2 = mouseY - 100
+ // _vm->_mouse.x/y are in virtual screen coords (0-319, 0-199)
+ // consistent with handler 8 which uses _vm->_mouse.x directly.
+ int16 inputX = (int16)(_vm->_mouse.x - 160); // DAT_0047a7e0
+ int16 inputY = (int16)(_vm->_mouse.y - 100); // DAT_0047a7e2
+
+ // Clamp: mouse mode uses [-160, 160] for X, [-127, 127] for Y (lines 55-70)
+ if (inputX > 160) inputX = 160;
+ if (inputX < -160) inputX = -160;
+ if (inputY > 127) inputY = 127;
+ if (inputY < -127) inputY = -127;
+
+ // --- Step 2: Scale to [-127, 127] (lines 82-84) ---
+ // Mouse mode: local_c = (DAT_0047a7e0 * 0x7f) / 0xa0
+ int16 local_c = (int16)((inputX * 127) / 160);
+ int16 local_14 = inputY; // Y already in [-127, 127]
+
+ // --- Step 3: Velocity history + smoothed average (lines 141-157) ---
+ for (int i = 24; i > 0; i--) {
+ _velocityHistory[i] = _velocityHistory[i - 1];
+ }
+ _velocityHistory[0] = local_c;
+
+ // Window size = (levelData[0] >> 4) + 1. Calibrated default: 5.
+ const int smoothWindow = 5;
+ int velSum = 0;
+ for (int i = 0; i < smoothWindow; i++) {
+ velSum += _velocityHistory[i];
+ }
+ _smoothedVelocity = (int16)(velSum / smoothWindow); // DAT_0044370c
+
+ // --- Step 4: Wind history (lines 158-173) ---
+ // Wind multiplier comes from level data[6]. Without data, use 0 (no wind).
+ const int16 windMult = 0;
+ int windSumX = 0, windSumY = 0;
+ for (int i = 14; i > 0; i--) {
+ _windHistoryX[i] = _windHistoryX[i - 1];
+ windSumX += _windHistoryX[i];
+ }
+ _windHistoryX[0] = _windParamX;
+ int16 windEffectX = (int16)((windMult * (windSumX + _windParamX)) / 15);
+
+ for (int i = 14; i > 0; i--) {
+ _windHistoryY[i] = _windHistoryY[i - 1];
+ windSumY += _windHistoryY[i];
+ }
+ _windHistoryY[0] = _windParamY;
+ int16 windEffectY = (int16)((windMult * (windSumY + _windParamY)) / 15);
+
+ // --- Step 5: Position delta (lines 174-242) ---
+ // levelSpeed (offset 4): calibrated so max velocity (127) â delta 12.
+ // 12 = (speed * 127) >> 9 â speed â 48
+ // levelYSpeed (offset 2): calibrated so max input (127) â delta ~8.
+ // 8 = (speed * 127) >> 10 â speed â 64
+ const int16 levelSpeed = 48;
+ const int16 levelYSpeed = 64;
+ int16 absSmoothVel = ABS(_smoothedVelocity);
+ int16 positionDeltaX;
+
+ if (_flyControlMode == 1) {
+ // Mode 1: Full cross-axis coupling (lines 174-186)
+ // Banking: vertical input deflects horizontal movement
+ if (local_c < 1) {
+ positionDeltaX = (int16)((levelSpeed * _smoothedVelocity - absSmoothVel * local_14 - windEffectX) >> 9);
+ } else {
+ positionDeltaX = (int16)((levelSpeed * _smoothedVelocity + absSmoothVel * local_14 - windEffectX) >> 9);
+ }
+ } else {
+ // Mode 0/2/3: Reduced cross-axis coupling (lines 218-230)
+ if (local_c < 1) {
+ positionDeltaX = (int16)((levelSpeed * _smoothedVelocity - (absSmoothVel * local_14 >> 2) - windEffectX) >> 9);
+ } else {
+ positionDeltaX = (int16)((levelSpeed * _smoothedVelocity + (absSmoothVel * local_14 >> 2) - windEffectX) >> 9);
+ }
+ }
- int16 rawMouseX = _vm->_mouse.x;
- int16 rawMouseY = _vm->_mouse.y;
+ // Clamp X delta to ±12 per frame (lines 187-192 / 231-236)
+ if (positionDeltaX < -11) positionDeltaX = -12;
+ if (positionDeltaX > 11) positionDeltaX = 12;
- // Scale mouse to 320x200 logical space if video is larger
- int16 mouseX = rawMouseX;
- int16 mouseY = rawMouseY;
- if (_player && _player->_width > 320) {
- mouseX = (rawMouseX * 320) / _player->_width;
- }
- if (_player && _player->_height > 200) {
- mouseY = (rawMouseY * 200) / _player->_height;
+ // Apply X delta (line 193 / 237)
+ _flyShipScreenX += positionDeltaX;
+
+ // Y delta
+ if (_flyControlMode == 1) {
+ // Mode 1: clamped to ±12 with wind (lines 194-216)
+ int yCalc = levelYSpeed * local_14 - (windEffectY >> 1);
+ int yDelta = yCalc >> 10;
+ if (yDelta < -12) yDelta = -12;
+ if (yDelta > 12) yDelta = 12;
+ _flyShipScreenY -= (int16)yDelta;
+ } else {
+ // Mode 0/2/3: unclamped (lines 238-241)
+ _flyShipScreenY -= (int16)((levelYSpeed * local_14) >> 10);
}
- // Convert mouse coords [0,319]x[0,199] â game coords [20,404]x[20,240]
- // Game center (212,130) maps to screen center (160,100)
- int16 targetGameX = (int16)(mouseX * 384 / 319 + 20);
- int16 targetGameY = (int16)(mouseY * 220 / 199 + 20);
+ // Store vertical input for direction sprite (line 243)
+ _verticalInput = local_14; // DAT_0044370e
- // Clamp to game coordinate bounds (FUN_40C3CC lines 245-256)
- if (targetGameX < 0x14) targetGameX = 0x14; // 20
- if (targetGameX > 0x194) targetGameX = 0x194; // 404
- if (targetGameY < 0x14) targetGameY = 0x14; // 20
- if (targetGameY > 0xF0) targetGameY = 0xF0; // 240
+ // Ship facing direction (line 244)
+ _facingRight = (0xd4 < _smoothedVelocity + _flyShipScreenX);
- // Smooth interpolation (original uses ±12 per frame max delta)
- const int16 maxStep = 12;
- if (_flyShipScreenX < targetGameX) {
- _flyShipScreenX = MIN((int16)(_flyShipScreenX + maxStep), targetGameX);
- } else if (_flyShipScreenX > targetGameX) {
- _flyShipScreenX = MAX((int16)(_flyShipScreenX - maxStep), targetGameX);
- }
- if (_flyShipScreenY < targetGameY) {
- _flyShipScreenY = MIN((int16)(_flyShipScreenY + maxStep), targetGameY);
- } else if (_flyShipScreenY > targetGameY) {
- _flyShipScreenY = MAX((int16)(_flyShipScreenY - maxStep), targetGameY);
- }
+ // --- Step 6: Position clamping (lines 245-256) ---
+ if (_flyShipScreenX > 0x194) _flyShipScreenX = 0x194; // 404
+ if (_flyShipScreenY > 0xF0) _flyShipScreenY = 0xF0; // 240
+ if (_flyShipScreenX < 0x14) _flyShipScreenX = 0x14; // 20
+ if (_flyShipScreenY < 0x14) _flyShipScreenY = 0x14; // 20
- // Corridor boundary collision (FUN_40C3CC lines 257-284)
- // Mode 0/2: Ship X is clamped to corridor boundaries with damage
+ // --- Step 7: Corridor collision â mode 0/2 only (lines 257-292) ---
if (_flyControlMode == 0 || _flyControlMode == 2) {
- // Right boundary collision
+ // Right boundary (lines 258-270)
+ // Original: position is ALWAYS clamped; damage/bounce only when cooldown < 5
if (_corridorRightX < _flyShipScreenX) {
_flyShipScreenX = _corridorRightX;
- if (_hitCooldown < 5 && !_rebelInvulnerable) {
- int damage = 3 + (_difficulty * 2);
- _playerDamage += damage;
- if (_playerDamage > 255) _playerDamage = 255;
- _rebelHitCounter++;
+ if (_hitCooldown < 5) {
+ for (int i = 0; i < 25; i++) _velocityHistory[i] = -127;
_hitCooldown = 10;
_spaceShotDirection = 1;
initDamageFlash();
- debug("Rebel2: Handler7 RIGHT CORRIDOR HIT ship=(%d,%d) boundary=%d",
- _flyShipScreenX, _flyShipScreenY, _corridorRightX);
+ if (!_rebelInvulnerable) {
+ int damage = 3 + (_difficulty * 2);
+ _playerDamage += damage;
+ if (_playerDamage > 255) _playerDamage = 255;
+ }
+ _rebelHitCounter++;
}
}
- // Left boundary collision
+ // Left boundary (lines 271-283)
if (_flyShipScreenX < _corridorLeftX) {
_flyShipScreenX = _corridorLeftX;
- if (_hitCooldown < 5 && !_rebelInvulnerable) {
- int damage = 3 + (_difficulty * 2);
- _playerDamage += damage;
- if (_playerDamage > 255) _playerDamage = 255;
- _rebelHitCounter++;
+ if (_hitCooldown < 5) {
+ for (int i = 0; i < 25; i++) _velocityHistory[i] = 127;
_hitCooldown = 10;
_spaceShotDirection = 0;
initDamageFlash();
- debug("Rebel2: Handler7 LEFT CORRIDOR HIT ship=(%d,%d) boundary=%d",
- _flyShipScreenX, _flyShipScreenY, _corridorLeftX);
+ if (!_rebelInvulnerable) {
+ int damage = 3 + (_difficulty * 2);
+ _playerDamage += damage;
+ if (_playerDamage > 255) _playerDamage = 255;
+ }
+ _rebelHitCounter++;
}
}
- // Y boundary clamping (no damage, just clamp) â lines 285-292
+ // Y boundary clamping â no damage (lines 285-292)
if (_corridorBottomY < _flyShipScreenY) {
_flyShipScreenY = _corridorBottomY;
}
@@ -1630,49 +1723,66 @@ void InsaneRebel2::iactRebel2Opcode6(byte *renderBitmap, Common::SeekableReadStr
}
}
- // Also update _shipPosX/Y for other systems that use screen coords
- _shipPosX = (int16)((_flyShipScreenX - 20) * 319 / 384);
- _shipPosY = (int16)((_flyShipScreenY - 20) * 199 / 220);
+ // --- Step 8: Perspective offsets (lines 293-316) ---
+ // f(x) = (focal * center * |offset|) / ((center - focal) * |offset| + focal * center)
+ // Close view (DAT_0047a7fc < 1): focalX=0x34, focalY=0x2d
+ // Far view (DAT_0047a7fc >= 1): focalX=0x2b, focalY=0x19
+ {
+ int absOffX = ABS(_flyShipScreenX - 0xd4);
+ int16 focalX = 0x2b; // Far view default for Level 3
+ if (absOffX > 0) {
+ _perspectiveX = (int16)((focalX * 0xd4 * absOffX) /
+ ((0xd4 - focalX) * absOffX + focalX * 0xd4));
+ } else {
+ _perspectiveX = 0;
+ }
+ if (_flyShipScreenX < 0xd5) _perspectiveX = -_perspectiveX;
+
+ int absOffY = ABS(_flyShipScreenY - 0x82);
+ int16 focalY = 0x19; // Far view default for Level 3
+ if (absOffY > 0) {
+ _perspectiveY = (int16)((focalY * 0x82 * absOffY) /
+ ((0x82 - focalY) * absOffY + focalY * 0x82));
+ } else {
+ _perspectiveY = 0;
+ }
+ if (_flyShipScreenY < 0x83) _perspectiveY = -_perspectiveY;
+ }
- // Direction calculation from ship game position
- // Original FUN_0040d836 uses DAT_0044370c (smoothed velocity) for direction.
- // Simplified: derive direction from position offset relative to center (212, 130).
- // hDir: 0-4 where 2=center, based on offset from center X (212)
- // vDir: 0-6 where 3=center, based on offset from center Y (130)
- int16 hDiff = 0xD4 - _flyShipScreenX; // 212 - shipX
- int16 hDir = (hDiff + 64) >> 6;
- if (hDir < 0) hDir = 0;
- if (hDir > 4) hDir = 4;
+ // View shift = clamped smoothed velocity (FUN_0040d836 lines 68-74)
+ _viewShift = _smoothedVelocity;
+ if (_viewShift > 127) _viewShift = 127;
+ if (_viewShift < -127) _viewShift = -127;
- int16 vDir = (130 - _flyShipScreenY) / 37; // adjusted divisor for game coord range
+ // --- Step 9: Direction sprite (FUN_0040d836 lines 88-106) ---
+ // 5x7 grid: vDir(0-4) * 7 + hDir(0-6) = sprite index (0-34)
+ // vDir from vertical input: (0xa0 - verticalInput) >> 6
+ int16 vDir = (int16)(((int)(0xa0 - _verticalInput) + ((0xa0 - _verticalInput) < 0 ? 63 : 0)) >> 6);
if (vDir < 0) vDir = 0;
- if (vDir > 6) vDir = 6;
+ if (vDir > 4) vDir = 4;
- // Deadzone at center to reduce flicker
- if (vDir == 3 && ABS(_flyShipScreenY - 130) > 13) {
- if (_flyShipScreenY < 130) vDir = 2;
- else vDir = 4;
+ // hDir from smoothed velocity: (0x95 - smoothedVelocity) / 0x2b
+ int16 hDir = (int16)((0x95 - _smoothedVelocity) / 0x2b);
+ if (hDir < 0) hDir = 0;
+ if (hDir > 6) hDir = 6;
+
+ // Hysteresis at center (lines 90-97, 98-105)
+ if (hDir == 3 && ABS(_smoothedVelocity) > 10) {
+ hDir = (_smoothedVelocity < 1) ? 4 : 2;
}
- if (hDir == 2 && ABS(_flyShipScreenX - 212) > 20) {
- if (_flyShipScreenX < 212) hDir = 3;
- else hDir = 1;
+ if (vDir == 2 && ABS(_verticalInput) > 15) {
+ vDir = (_verticalInput < 1) ? 3 : 1;
}
- _shipDirectionH = hDir;
- _shipDirectionV = vDir;
- _shipDirectionIndex = hDir * 7 + vDir;
-
- // Clamp direction index to valid range (0-34)
+ _shipDirectionIndex = vDir * 7 + hDir;
if (_shipDirectionIndex < 0) _shipDirectionIndex = 0;
if (_shipDirectionIndex > 34) _shipDirectionIndex = 34;
- // Update firing state
- _shipFiring = (_vm->VAR(_vm->VAR_LEFTBTN_HOLD) != 0);
+ _shipFiring = (_flyControlMode == 2) && (_vm->VAR(_vm->VAR_LEFTBTN_HOLD) != 0);
- debug("Rebel2 Handler7: rawMouse=(%d,%d) scaled=(%d,%d) shipPos=(%d,%d) screenPos=(%d,%d) dir=(%d,%d) idx=%d flySprite=%p",
- rawMouseX, rawMouseY, mouseX, mouseY, _shipPosX, _shipPosY,
- _flyShipScreenX, _flyShipScreenY, _shipDirectionH, _shipDirectionV, _shipDirectionIndex,
- (void*)_flyShipSprite);
+ debug("Rebel2 H7: pos=(%d,%d) vel=%d vIn=%d dx=%d dir=%d mode=%d",
+ _flyShipScreenX, _flyShipScreenY, _smoothedVelocity,
+ _verticalInput, positionDeltaX, _shipDirectionIndex, _flyControlMode);
return;
}
@@ -4974,50 +5084,57 @@ void InsaneRebel2::renderStatusBarSprites(byte *renderBitmap, int pitch, int wid
void InsaneRebel2::renderHandler7Ship(byte *renderBitmap, int pitch, int width, int height) {
// Handler 7 Ship Rendering (Third-Person Ship - FLY sprites)
- // Uses _flyShipSprite (FLY001) with 35 direction frames (5x7 grid)
+ // Based on FUN_0040d836 lines 173-185:
+ // FUN_004236e0(buf, frameInfo, screenX - 0xd4, screenY - 0x82, 0, sprite, frameIdx, 1, 0)
+ // The ship sprite is drawn at the perspective-transformed position offset from center.
+ // FUN_0041c720 transforms game coords (shipX, shipY) using perspective offsets.
if (_rebelHandler != 7 || !_flyShipSprite || _shipLevelMode == 5)
return;
- // Base position at screen center with direction offset
- // Ship position is in game coords (center 212,130), convert to screen offset
- int baseX = 160;
- int baseY = 105;
- int16 posOffsetX = (_flyShipScreenX - 0xD4) / 13; // (shipX - 212) / 13
- int16 posOffsetY = (_flyShipScreenY - 0x82) / 11; // (shipY - 130) / 11
- int shipScreenX = baseX + posOffsetX;
- int shipScreenY = baseY + posOffsetY;
-
int numSprites = _flyShipSprite->getNumChars();
int spriteIndex = _shipDirectionIndex;
if (spriteIndex < 0) spriteIndex = 0;
if (spriteIndex >= numSprites) spriteIndex = numSprites - 1;
- // Center sprite at position
+ // Transform game coordinates to screen coordinates (FUN_0041c720 equivalent)
+ // The perspective transform shifts the ship position based on perspective offsets.
+ // Close view: FOBJ offset = (-52 - perspX, -45 - perspY), ship at screen center.
+ // For now, use a simplified perspective: ship position = center + offset from center
+ // scaled by perspective. In the original, FUN_00424510 shifts all FOBJ sprites.
+ //
+ // Screen position for sprite drawing (FUN_0040d836 line 174):
+ // drawX = transformedX - 0xd4, drawY = transformedY - 0x82
+ // Where transformedX/Y come from FUN_0041c720(shipX, shipY, perspX, perspY, viewShift)
+ //
+ // Simplified: screenX = 160 + (shipX - 212) * perspFactor
+ // With the perspective formula, objects near center barely move, objects at edges move more.
+ int drawX = (_flyShipScreenX - 0xd4) + _perspectiveX;
+ int drawY = (_flyShipScreenY - 0x82) + _perspectiveY;
+
+ // Convert from game-center-relative to screen coordinates
+ // The sprite system expects coordinates relative to the 320x200 frame
+ // Center of frame = (160, 100), so offset = game position - game center
+ drawX += 160 + _viewX;
+ drawY += 100 + _viewY;
+
+ // Center the sprite on the position
int spriteW = _flyShipSprite->getCharWidth(spriteIndex);
int spriteH = _flyShipSprite->getCharHeight(spriteIndex);
- int drawX = shipScreenX - spriteW / 2 + _viewX;
- int drawY = shipScreenY - spriteH / 2 + _viewY;
+ drawX -= spriteW / 2;
+ drawY -= spriteH / 2;
renderNutSprite(renderBitmap, pitch, width, height, drawX, drawY, _flyShipSprite, spriteIndex);
- // Laser overlay if firing
+ // Laser overlay if firing (same position as ship)
if (_shipFiring && _flyLaserSprite && _flyLaserSprite->getNumChars() > 0) {
int laserIndex = spriteIndex % _flyLaserSprite->getNumChars();
renderNutSprite(renderBitmap, pitch, width, height, drawX, drawY, _flyLaserSprite, laserIndex);
}
- // Targeting overlay
- if (_flyTargetSprite && _flyTargetSprite->getNumChars() > 0) {
- int targetW = _flyTargetSprite->getCharWidth(0);
- int targetH = _flyTargetSprite->getCharHeight(0);
- int targetX = shipScreenX - targetW / 2 + _viewX;
- int targetY = shipScreenY - targetH / 2 + _viewY;
- renderNutSprite(renderBitmap, pitch, width, height, targetX, targetY, _flyTargetSprite, 0);
- }
-
- debug("Rebel2 Handler7: Ship at (%d,%d) sprite=%d/%d dir=(%d,%d) idx=%d",
- drawX, drawY, spriteIndex, numSprites, _shipDirectionH, _shipDirectionV, _shipDirectionIndex);
+ debug("Rebel2 Handler7Ship: draw=(%d,%d) sprite=%d/%d shipPos=(%d,%d) persp=(%d,%d) smoothVel=%d vertIn=%d",
+ drawX, drawY, spriteIndex, numSprites, _flyShipScreenX, _flyShipScreenY,
+ _perspectiveX, _perspectiveY, _smoothedVelocity, _verticalInput);
}
void InsaneRebel2::renderHandler8Ship(byte *renderBitmap, int pitch, int width, int height) {
diff --git a/engines/scumm/insane/insane_rebel.h b/engines/scumm/insane/insane_rebel.h
index a1342cad786..ec3067cc805 100644
--- a/engines/scumm/insane/insane_rebel.h
+++ b/engines/scumm/insane/insane_rebel.h
@@ -820,17 +820,33 @@ public:
// - DAT_0047fef0: Laser fire sprites (FLY002, par3=3)
// - DAT_0047fef8: Targeting overlay (FLY003, par3=2)
// - DAT_0047ff00: High-res alternative (FLY004, par3=11)
- // - DAT_0044370c: Ship Y screen position
- // - DAT_0044370e: Ship X screen position
+ // - DAT_00443708: Ship X position, DAT_0044370a: Ship Y position
+ // - DAT_0044370c: Smoothed horizontal velocity, DAT_0044370e: Vertical input
NutRenderer *_flyShipSprite; // DAT_0047fee8 - FLY001 (35 direction frames)
NutRenderer *_flyLaserSprite; // DAT_0047fef0 - FLY002
NutRenderer *_flyTargetSprite; // DAT_0047fef8 - FLY003
NutRenderer *_flyHiResSprite; // DAT_0047ff00 - FLY004
- // Handler 7 screen position (different from Handler 8's raw positions)
- int16 _flyShipScreenX; // DAT_0044370e - Ship X screen position
- int16 _flyShipScreenY; // DAT_0044370c - Ship Y screen position
+ // Handler 7 ship state (FUN_40C3CC / FUN_0040d836)
+ // Position in game coordinate space [20,404]x[20,240], center=(212,130)
+ int16 _flyShipScreenX; // DAT_00443708 - Ship X game position
+ int16 _flyShipScreenY; // DAT_0044370a - Ship Y game position
+
+ // Physics state (velocity-based movement system from FUN_40C3CC case 4)
+ int16 _smoothedVelocity; // DAT_0044370c - Averaged horizontal velocity (from history)
+ int16 _verticalInput; // DAT_0044370e - Stored vertical input component
+ int16 _velocityHistory[25]; // DAT_00443716 - Horizontal velocity ring buffer (25 entries)
+ int16 _windHistoryX[15]; // DAT_00443b16 - Wind X history buffer
+ int16 _windHistoryY[15]; // DAT_00443b34 - Wind Y history buffer
+ int16 _windParamX; // DAT_00443b12 - Wind X (from opcode 7 par4=0)
+ int16 _windParamY; // DAT_00443b14 - Wind Y (from opcode 7 par4=0)
+
+ // Perspective view offsets (computed from ship position, used for rendering)
+ int16 _perspectiveX; // DAT_00443712 - Perspective shift X
+ int16 _perspectiveY; // DAT_00443714 - Perspective shift Y
+ int16 _viewShift; // DAT_00443710 - Clamped smoothed velocity for view transform
+ bool _facingRight; // DAT_0047ab8c - Ship facing right of center
// ======================= Handler 25 (0x19) GRD Ship System =======================
// For mixed mode missions (Level 2 speeder bike, etc.), Handler 25 uses GRD NUT
Commit: 1f23ead15c614a553930171395a246403931e186
https://github.com/scummvm/scummvm/commit/1f23ead15c614a553930171395a246403931e186
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:05+02:00
Commit Message:
SCUMM: RA2: Implement ship explosions for level 3
Changed paths:
engines/scumm/insane/insane_rebel.cpp
diff --git a/engines/scumm/insane/insane_rebel.cpp b/engines/scumm/insane/insane_rebel.cpp
index 61aa13ac4d0..8089125e3d4 100644
--- a/engines/scumm/insane/insane_rebel.cpp
+++ b/engines/scumm/insane/insane_rebel.cpp
@@ -1622,11 +1622,11 @@ void InsaneRebel2::iactRebel2Opcode6(byte *renderBitmap, Common::SeekableReadStr
// --- Step 5: Position delta (lines 174-242) ---
// levelSpeed (offset 4): calibrated so max velocity (127) â delta 12.
- // 12 = (speed * 127) >> 9 â speed â 48
- // levelYSpeed (offset 2): calibrated so max input (127) â delta ~8.
- // 8 = (speed * 127) >> 10 â speed â 64
- const int16 levelSpeed = 48;
- const int16 levelYSpeed = 64;
+ // 8 = (speed * 127) >> 9 â speed â 32
+ // levelYSpeed (offset 2): calibrated so max input (127) â delta ~6.
+ // 6 = (speed * 127) >> 10 â speed â 48
+ const int16 levelSpeed = 32;
+ const int16 levelYSpeed = 48;
int16 absSmoothVel = ABS(_smoothedVelocity);
int16 positionDeltaX;
@@ -4086,9 +4086,8 @@ void InsaneRebel2::checkHandler7CollisionZones() {
// Mode 0/2: Obstacle collision using SECONDARY zones (inside quad = hit)
// Mode 1/3: Wall/boundary collision using PRIMARY zones (per-edge push-back)
- // Decrement hit cooldown per frame
- if (_hitCooldown > 0)
- _hitCooldown--;
+ // Note: _hitCooldown is decremented in renderSpaceExplosions (FUN_40F1C5)
+ // to match the original where the decrement happens during rendering.
if (_flyControlMode == 0 || _flyControlMode == 2) {
// ---- Mode 0/2: Obstacle collision using SECONDARY zones (FUN_403b5b) ----
@@ -5622,6 +5621,7 @@ void InsaneRebel2::renderSpaceExplosions(byte *renderBitmap, int pitch, int widt
if (!_smush_iconsNut)
return;
+ // --- Part 1: Space shot explosions (FUN_40F1C5 lines 19-60) ---
for (int i = 0; i < 5; i++) {
if (!_explosions[i].active)
continue;
@@ -5660,6 +5660,59 @@ void InsaneRebel2::renderSpaceExplosions(byte *renderBitmap, int pitch, int widt
_explosions[i].counter--;
}
+
+ // --- Part 2: Corridor/zone hit explosion (FUN_40F1C5 lines 61-85) ---
+ // Rendered when _hitCooldown > 0 (DAT_0044374c). Decrement happens HERE
+ // (matching original where FUN_40F1C5 decrements DAT_0044374c during render).
+ // _spaceShotDirection (DAT_0044374e) determines explosion side:
+ // 0 = left side (hit left boundary), 1 = right side (hit right boundary)
+ // 2 = bottom (zone push down), 3 = top (zone push up)
+ // Sprite frames: 0x15 - cooldown = 21 - cooldown (frames 12â21 as cooldown 9â0)
+ if (_hitCooldown != 0) {
+ _hitCooldown--;
+
+ int numChars = _smush_iconsNut->getNumChars();
+ int spriteIndex = 0x15 - _hitCooldown; // 21 - remaining cooldown
+
+ if (spriteIndex >= 0 && spriteIndex < numChars) {
+ // Compute ship screen position (simplified FUN_0041c720 transform)
+ int shipDrawX = (_flyShipScreenX - 0xd4) + _perspectiveX + 160 + _viewX;
+ int shipDrawY = (_flyShipScreenY - 0x82) + _perspectiveY + 100 + _viewY;
+
+ // Per-direction offset from ship center.
+ // Original uses lookup tables (DAT_004438da etc.) indexed by
+ // _shipDirectionIndex (35 entries per direction). We approximate
+ // with fixed offsets since we don't have the table data.
+ int offsetX = 0, offsetY = 0;
+ switch (_spaceShotDirection) {
+ case 0: // Left wall hit â explosion on left side of ship
+ offsetX = -35;
+ break;
+ case 1: // Right wall hit â explosion on right side of ship
+ offsetX = 35;
+ break;
+ case 2: // Zone push down â explosion on bottom
+ offsetY = 20;
+ break;
+ case 3: // Zone push up â explosion on top
+ offsetY = -20;
+ break;
+ default:
+ break;
+ }
+
+ int drawX = shipDrawX + offsetX;
+ int drawY = shipDrawY + offsetY;
+
+ int ew = _smush_iconsNut->getCharWidth(spriteIndex);
+ int eh = _smush_iconsNut->getCharHeight(spriteIndex);
+ renderNutSprite(renderBitmap, pitch, width, height,
+ drawX - ew / 2, drawY - eh / 2, _smush_iconsNut, spriteIndex);
+
+ debug("Rebel2 H7 corridor explosion: dir=%d frame=%d pos=(%d,%d) cooldown=%d",
+ _spaceShotDirection, spriteIndex, drawX, drawY, _hitCooldown);
+ }
+ }
}
// FUN_41F29A â Handler 25 (FPS/Mixed) explosion rendering.
Commit: 1e2e7e108e7a7b545dcce43461d89bc69f01864b
https://github.com/scummvm/scummvm/commit/1e2e7e108e7a7b545dcce43461d89bc69f01864b
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:06+02:00
Commit Message:
SCUMM: RA2: Add explosion sounds
Changed paths:
engines/scumm/insane/insane_rebel.cpp
engines/scumm/insane/insane_rebel.h
diff --git a/engines/scumm/insane/insane_rebel.cpp b/engines/scumm/insane/insane_rebel.cpp
index 8089125e3d4..352c30efc11 100644
--- a/engines/scumm/insane/insane_rebel.cpp
+++ b/engines/scumm/insane/insane_rebel.cpp
@@ -367,6 +367,13 @@ InsaneRebel2::InsaneRebel2(ScummEngine_v7 *scumm) {
_audioTrackActive[i] = false;
}
+ // Initialize and load sound effects (SYSTM/*.SAD files)
+ for (i = 0; i < kRA2NumSfx; i++) {
+ _sfxData[i] = nullptr;
+ _sfxSize[i] = 0;
+ }
+ loadSfx();
+
// Initialize menu system
_gameState = kStateMainMenu; // Start at main menu
_menuSelection = 0; // First item selected
@@ -433,6 +440,7 @@ InsaneRebel2::~InsaneRebel2() {
_vm->_system->getEventManager()->getEventDispatcher()->unregisterObserver(this);
terminateAudio();
+ freeSfx();
delete _rebelMsgFont;
delete _menuFont;
delete _smush_dispfontNut;
@@ -809,14 +817,16 @@ int32 InsaneRebel2::processMouse() {
}
}
- // Play explosion sound.
- // Handler 8 types 1-4: BLAST.SAD (slot 0) via different sound function
- // All others: EXPLODE.SAD (slot 2)
- // TODO: Implement actual SAD sound playback
- if (_rebelHandler == 8 && it->type >= 1 && it->type <= 4) {
- debug("Rebel2: BLAST sound for enemy type %d (no visual explosion)", it->type);
- } else {
- debug("Rebel2: EXPLODE sound for enemy type %d", it->type);
+ // Play explosion sound (FUN_0041189e).
+ // Pan based on enemy center X position: (screenX - 160) mapped to [-127,127]
+ {
+ int enemyCenterX = (it->rect.left + it->rect.right) / 2 - _viewX;
+ int sfxPan = CLIP((enemyCenterX - 160) * 127 / 160, -127, 127);
+ if (_rebelHandler == 8 && it->type >= 1 && it->type <= 4) {
+ playSfx(0, 127, sfxPan); // BLAST.SAD for handler 8 types 1-4
+ } else {
+ playSfx(2, 127, sfxPan); // EXPLODE.SAD for all other enemies
+ }
}
// Award score for destroying enemy (FUN_0041bf8d called from FUN_40A2E0)
@@ -1696,6 +1706,7 @@ void InsaneRebel2::iactRebel2Opcode6(byte *renderBitmap, Common::SeekableReadStr
if (_playerDamage > 255) _playerDamage = 255;
}
_rebelHitCounter++;
+ playSfx(1, 127, 100); // CRASH.SAD, right wall â pan right
}
}
// Left boundary (lines 271-283)
@@ -1712,6 +1723,7 @@ void InsaneRebel2::iactRebel2Opcode6(byte *renderBitmap, Common::SeekableReadStr
if (_playerDamage > 255) _playerDamage = 255;
}
_rebelHitCounter++;
+ playSfx(1, 127, -100); // CRASH.SAD, left wall â pan left
}
}
// Y boundary clamping â no damage (lines 285-292)
@@ -4143,6 +4155,8 @@ void InsaneRebel2::checkHandler7CollisionZones() {
}
_rebelHitCounter++;
initDamageFlash();
+ // Pan based on ship X position relative to screen center
+ playSfx(1, 127, CLIP((_flyShipScreenX - 212) * 127 / 160, -127, 127));
debug("Rebel2: Handler7 Mode0/2 OBSTACLE HIT zone=%d ship=(%d,%d) damage=%d",
i, _flyShipScreenX, _flyShipScreenY, collisionDamage);
break; // Only one collision per frame (original breaks)
@@ -4183,6 +4197,7 @@ void InsaneRebel2::checkHandler7CollisionZones() {
if (_playerDamage > 255) _playerDamage = 255;
_rebelHitCounter++;
_hitCooldown = 10;
+ playSfx(1, 127, 0); // CRASH.SAD, top wall â center pan
debug("Rebel2: Handler7 Mode1/3 TOP WALL ship=(%d,%d) edgeY=%d damage=%d",
_flyShipScreenX, _flyShipScreenY, edgeY, damage);
}
@@ -4204,6 +4219,7 @@ void InsaneRebel2::checkHandler7CollisionZones() {
if (_playerDamage > 255) _playerDamage = 255;
_rebelHitCounter++;
_hitCooldown = 10;
+ playSfx(1, 127, 0); // CRASH.SAD, bottom wall â center pan
debug("Rebel2: Handler7 Mode1/3 BOTTOM WALL ship=(%d,%d) edgeY=%d damage=%d",
_flyShipScreenX, _flyShipScreenY, edgeY, damage);
}
@@ -4225,6 +4241,7 @@ void InsaneRebel2::checkHandler7CollisionZones() {
if (_playerDamage > 255) _playerDamage = 255;
_rebelHitCounter++;
_hitCooldown = 10;
+ playSfx(1, 127, -100); // CRASH.SAD, left wall â pan left
debug("Rebel2: Handler7 Mode1/3 LEFT WALL ship=(%d,%d) edgeX=%d damage=%d",
_flyShipScreenX, _flyShipScreenY, edgeX, damage);
}
@@ -4245,6 +4262,7 @@ void InsaneRebel2::checkHandler7CollisionZones() {
if (_playerDamage > 255) _playerDamage = 255;
_rebelHitCounter++;
_hitCooldown = 10;
+ playSfx(1, 127, 100); // CRASH.SAD, right wall â pan right
debug("Rebel2: Handler7 Mode1/3 RIGHT WALL ship=(%d,%d) edgeX=%d damage=%d",
_flyShipScreenX, _flyShipScreenY, edgeX, damage);
}
@@ -6334,6 +6352,121 @@ void InsaneRebel2::processAudioFrame(int16 feedSize) {
}
}
+// ========== Sound Effects (SAD files) ==========
+// Loads standalone SAUD files from SYSTM/ for one-shot SFX playback.
+// Original game loads these via FUN_0042a3b0 at init into DAT_00456888[0..7].
+
+static const char *const kRA2SfxFiles[InsaneRebel2::kRA2NumSfx] = {
+ "SYSTM/BLAST.SAD", // 0 - Player laser fire
+ "SYSTM/CRASH.SAD", // 1 - Corridor/wall collision
+ "SYSTM/EXPLODE.SAD", // 2 - Enemy explosion
+ "SYSTM/ALERT.SAD", // 3 - Alert/warning
+ "SYSTM/LOCKON.SAD", // 4 - Target lock-on
+ "SYSTM/BONUS.SAD", // 5 - Bonus pickup
+ "SYSTM/HBLAST.SAD", // 6 - Heavy blast (player weapon)
+ "SYSTM/TBLAST.SAD" // 7 - TIE blast
+};
+
+void InsaneRebel2::loadSfx() {
+ for (int i = 0; i < kRA2NumSfx; i++) {
+ ScummFile *file = _vm->instantiateScummFile();
+ _vm->openFile(*file, kRA2SfxFiles[i]);
+ if (!file->isOpen()) {
+ debug("InsaneRebel2::loadSfx: Could not open %s", kRA2SfxFiles[i]);
+ delete file;
+ continue;
+ }
+
+ // SAUD file structure: SAUD header (8) + STRK sub-chunk + SDAT sub-chunk
+ // We scan for the SDAT tag to find the PCM data.
+ uint32 fileSize = file->size();
+ if (fileSize < 38) { // Minimum: 8 (SAUD) + 22 (STRK) + 8 (SDAT header)
+ debug("InsaneRebel2::loadSfx: %s too small (%d bytes)", kRA2SfxFiles[i], fileSize);
+ file->close();
+ delete file;
+ continue;
+ }
+
+ // Verify SAUD tag
+ uint32 tag = file->readUint32BE();
+ if (tag != MKTAG('S', 'A', 'U', 'D')) {
+ debug("InsaneRebel2::loadSfx: %s not a SAUD file (tag=0x%08x)", kRA2SfxFiles[i], tag);
+ file->close();
+ delete file;
+ continue;
+ }
+ file->readUint32BE(); // Skip SAUD size
+
+ // Scan for SDAT chunk (skip STRK and any other sub-chunks)
+ bool foundSdat = false;
+ while (file->pos() + 8 <= (int64)fileSize) {
+ uint32 chunkTag = file->readUint32BE();
+ uint32 chunkSize = file->readUint32BE();
+
+ if (chunkTag == MKTAG('S', 'D', 'A', 'T')) {
+ // Found PCM data
+ uint32 pcmSize = MIN(chunkSize, fileSize - (uint32)file->pos());
+ _sfxData[i] = (byte *)malloc(pcmSize);
+ if (_sfxData[i]) {
+ file->read(_sfxData[i], pcmSize);
+ _sfxSize[i] = pcmSize;
+ debug("InsaneRebel2::loadSfx: Loaded %s (%d bytes PCM)", kRA2SfxFiles[i], pcmSize);
+ }
+ foundSdat = true;
+ break;
+ } else {
+ // Skip this sub-chunk
+ file->seek(chunkSize, SEEK_CUR);
+ }
+ }
+
+ if (!foundSdat) {
+ debug("InsaneRebel2::loadSfx: No SDAT chunk in %s", kRA2SfxFiles[i]);
+ }
+
+ file->close();
+ delete file;
+ }
+}
+
+void InsaneRebel2::freeSfx() {
+ for (int i = 0; i < kRA2NumSfx; i++) {
+ // Stop any playing SFX on this slot
+ _vm->_mixer->stopHandle(_sfxHandles[i]);
+ free(_sfxData[i]);
+ _sfxData[i] = nullptr;
+ _sfxSize[i] = 0;
+ }
+}
+
+void InsaneRebel2::playSfx(int slot, int volume, int pan) {
+ if (slot < 0 || slot >= kRA2NumSfx || !_sfxData[slot] || _sfxSize[slot] == 0) {
+ return;
+ }
+
+ // Stop any previous instance of this SFX slot
+ _vm->_mixer->stopHandle(_sfxHandles[slot]);
+
+ // Make a copy of the PCM data (makeRawStream with DisposeAfterUse::YES will free it)
+ byte *pcmCopy = (byte *)malloc(_sfxSize[slot]);
+ if (!pcmCopy) {
+ return;
+ }
+ memcpy(pcmCopy, _sfxData[slot], _sfxSize[slot]);
+
+ // Create a one-shot raw audio stream: 8-bit unsigned mono at 11025 Hz
+ Audio::SeekableAudioStream *stream = Audio::makeRawStream(
+ pcmCopy, _sfxSize[slot], 11025, Audio::FLAG_UNSIGNED, DisposeAfterUse::YES);
+
+ // Scale volume from 0-127 to ScummVM's 0-255 range
+ int scaledVolume = (volume * Audio::Mixer::kMaxChannelVolume) / 127;
+
+ _vm->_mixer->playStream(Audio::Mixer::kSFXSoundType, &_sfxHandles[slot],
+ stream, -1, scaledVolume, pan);
+
+ debug(5, "InsaneRebel2::playSfx: slot=%d vol=%d pan=%d size=%d", slot, volume, pan, _sfxSize[slot]);
+}
+
// ======================= Menu System Implementation =======================
// Emulates retail menu system from FUN_004147B2 and FUN_0041FDC8
diff --git a/engines/scumm/insane/insane_rebel.h b/engines/scumm/insane/insane_rebel.h
index ec3067cc805..199bcb9ed02 100644
--- a/engines/scumm/insane/insane_rebel.h
+++ b/engines/scumm/insane/insane_rebel.h
@@ -946,6 +946,28 @@ public:
// Queue audio data for playback on a specific track
void queueAudioData(int trackIdx, uint8 *data, int32 size, int volume, int pan);
+ // ========== Sound Effects (SAD files) ==========
+ // 8 standalone SAUD files in SYSTM/ loaded at init for one-shot SFX.
+ // Slot mapping (from FUN_0042a3b0 init):
+ // 0=BLAST.SAD 1=CRASH.SAD 2=EXPLODE.SAD 3=ALERT.SAD
+ // 4=LOCKON.SAD 5=BONUS.SAD 6=HBLAST.SAD 7=TBLAST.SAD
+
+ static const int kRA2NumSfx = 8;
+
+ byte *_sfxData[kRA2NumSfx]; // Loaded PCM data for each SAD slot
+ uint32 _sfxSize[kRA2NumSfx]; // PCM data size per slot
+ Audio::SoundHandle _sfxHandles[kRA2NumSfx]; // Mixer handles for SFX playback
+
+ // Load all SAD files from SYSTM/ directory
+ void loadSfx();
+
+ // Free all loaded SFX data
+ void freeSfx();
+
+ // Play a one-shot sound effect
+ // slot: 0-7 (SAD file index), volume: 0-127, pan: -127..+127
+ void playSfx(int slot, int volume, int pan);
+
};
} // End of namespace Insane
Commit: 07c45898d71a4001048892c99b0e44557db98a6b
https://github.com/scummvm/scummvm/commit/07c45898d71a4001048892c99b0e44557db98a6b
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:06+02:00
Commit Message:
SCUMM: RA2: Add shooting sounds
Changed paths:
engines/scumm/insane/insane_rebel.cpp
diff --git a/engines/scumm/insane/insane_rebel.cpp b/engines/scumm/insane/insane_rebel.cpp
index 352c30efc11..08158b978d6 100644
--- a/engines/scumm/insane/insane_rebel.cpp
+++ b/engines/scumm/insane/insane_rebel.cpp
@@ -3467,10 +3467,9 @@ void InsaneRebel2::spawnShot(int x, int y) {
void InsaneRebel2::spawnTurretShot(int x, int y) {
for (int i = 0; i < 2; i++) {
if (_turretShots[i].counter == 0) {
- // Play sound based on level type
- // FUN_0041189e(-(ushort)(_rebelLevelType == 5) & 7, i + 1, 0x7f, 0, 0)
- // Sound ID: 0 for type 5, 7 for others
- // TODO: Play laser sound via audio system
+ // FUN_0041189e(-(ushort)(DAT_004436de == 5) & 7, i + 1, 0x7f, 0, 0)
+ // levelType 5: BLAST.SAD (slot 0), otherwise: TBLAST.SAD (slot 7)
+ playSfx((_rebelLevelType == 5) ? 0 : 7, 127, 0);
_turretShots[i].counter = getShotMaxDuration();
_turretShots[i].seqNum = _turretShotSeqCounter;
@@ -3482,11 +3481,12 @@ void InsaneRebel2::spawnTurretShot(int x, int y) {
}
}
-// Handler 8 Vehicle shot spawn (based on FUN_401CCF)
+// Handler 8 Vehicle shot spawn (based on FUN_401CCF lines 65-69)
void InsaneRebel2::spawnVehicleShot(int x, int y) {
for (int i = 0; i < 2; i++) {
if (_vehicleShots[i].counter == 0) {
- // TODO: Play laser sound
+ // FUN_0041189e(6, local_c + 1, 0x7f, 0, 0) â HBLAST.SAD
+ playSfx(6, 127, 0);
_vehicleShots[i].counter = getShotMaxDuration();
_vehicleShots[i].targetX = x + _viewX;
_vehicleShots[i].targetY = y + _viewY;
@@ -3505,8 +3505,8 @@ void InsaneRebel2::spawnHandler25Shot(int x, int y) {
for (int i = 0; i < 2; i++) {
if (_turretShots[i].counter == 0) {
- // Play sound: FUN_0041189e(6, i + 1, 0x7f, 0, 0)
- // TODO: Play laser sound
+ // FUN_0041189e(6, local_1c + 1, 0x7f, 0, 0) â HBLAST.SAD
+ playSfx(6, 127, 0);
_turretShots[i].counter = getShotMaxDuration();
_turretShots[i].seqNum = _turretShotSeqCounter;
@@ -3527,8 +3527,8 @@ void InsaneRebel2::spawnHandler25Shot(int x, int y) {
void InsaneRebel2::spawnSpaceShot(int x, int y) {
for (int i = 0; i < 2; i++) {
if (_spaceShots[i].counter == 0) {
- // Play sound: FUN_0041189e(6, i + 1, 0x7f, 0, 0)
- // TODO: Play laser sound
+ // FUN_0041189e(6, local_2c + 1, 0x7f, 0, 0) â HBLAST.SAD
+ playSfx(6, 127, 0);
_spaceShots[i].counter = getShotMaxDuration();
_spaceShots[i].targetX = x; // Screen coords
Commit: 31a1ecf0cf2e46d4a911bf14b7bee8d05bcdbeb2
https://github.com/scummvm/scummvm/commit/31a1ecf0cf2e46d4a911bf14b7bee8d05bcdbeb2
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:06+02:00
Commit Message:
SCUMM: RA2: Render gun shooting
Changed paths:
engines/scumm/insane/insane_rebel.cpp
engines/scumm/insane/insane_rebel.h
diff --git a/engines/scumm/insane/insane_rebel.cpp b/engines/scumm/insane/insane_rebel.cpp
index 08158b978d6..7b769ee3ad4 100644
--- a/engines/scumm/insane/insane_rebel.cpp
+++ b/engines/scumm/insane/insane_rebel.cpp
@@ -280,6 +280,8 @@ InsaneRebel2::InsaneRebel2(ScummEngine_v7 *scumm) {
_turretShots[i].targetX = 0;
_turretShots[i].targetY = 0;
_turretShots[i].seqNum = 0;
+ _turretShots[i].gunX = 0;
+ _turretShots[i].gunY = 0;
}
_turretShotSeqCounter = 0;
@@ -3495,8 +3497,11 @@ void InsaneRebel2::spawnVehicleShot(int x, int y) {
}
}
-// Handler 25 Speeder bike shot spawn (based on FUN_0041db5e lines 170-190)
-// Similar to turret but with character-based gun position
+// Handler 25 on-foot shot spawn (based on FUN_0041db5e lines 170-190)
+// Gun position computed from GRD002 character sprite.
+// Original stores: DAT_0045791c[i] = gunOffsetTable[spriteIdx] + DAT_00457910 - DAT_0045790c
+// DAT_00457920[i] = gunYTable[spriteIdx] + DAT_00457912 - DAT_0045790e
+// Render adds view offset back, so screen gun = table[idx] + spriteOffset.
void InsaneRebel2::spawnHandler25Shot(int x, int y) {
// Handler 25 can only shoot when uncovered (damage == 0)
if (_rebelDamageLevel != 0) {
@@ -3516,8 +3521,68 @@ void InsaneRebel2::spawnHandler25Shot(int x, int y) {
_turretShots[i].targetX = x;
_turretShots[i].targetY = y;
- debug("Rebel2 Handler25: Spawned shot %d target (%d,%d)",
- i, _turretShots[i].targetX, _turretShots[i].targetY);
+ // Compute gun position from GRD002 character sprite.
+ // Original uses per-direction lookup tables DAT_004578a6/DAT_004578c6.
+ // We approximate from the NUT sprite center + directional offset.
+ if (_grd002Sprite && _grd002Sprite->getNumChars() > 0) {
+ // Compute current sprite index (same logic as renderHandler25Ship)
+ int spriteIdx;
+ if (_rebelDamageLevel == 0) {
+ // Uncovered: compute from crosshair position zones
+ int16 areaLeft = (_corridorLeftX > 0) ? _corridorLeftX : 0;
+ int16 areaRight = (_corridorRightX > 0) ? _corridorRightX : 320;
+ int16 areaTop = (_corridorTopY > 0) ? _corridorTopY : 0;
+ int16 areaBottom = (_corridorBottomY > 0) ? _corridorBottomY : 180;
+ int areaWidth = areaRight - areaLeft;
+ int areaHeight = areaBottom - areaTop;
+ int zoneWidth = (areaWidth > 0) ? (areaWidth + 3) / 4 : 80;
+ int zoneHeight = (areaHeight > 0) ? areaHeight / 2 : 90;
+ int xZone = (zoneWidth > 0) ? ((zoneWidth / 2) + (x - areaLeft)) / zoneWidth : 2;
+ int yZone = (zoneHeight > 0) ? ((zoneHeight / 2) + (y - areaTop)) / zoneHeight : 0;
+ if (xZone < 0) xZone = 0;
+ if (xZone > 4) xZone = 4;
+ if (yZone < 0) yZone = 0;
+ if (yZone > 1) yZone = 1;
+ if (_rebelFlightDir == (yZone & 1)) {
+ xZone = 4 - xZone;
+ }
+ spriteIdx = yZone * 5 + xZone + 5;
+ } else {
+ spriteIdx = (_rebelFlightDir == 0) ? (5 - _rebelDamageLevel) : (25 - _rebelDamageLevel);
+ }
+ int numSprites = _grd002Sprite->getNumChars();
+ if (spriteIdx < 0) spriteIdx = 0;
+ if (spriteIdx >= numSprites) spriteIdx = numSprites - 1;
+
+ // Get sprite rendering position (same as in renderHandler25Ship)
+ int16 spriteXOffset = _grd002Sprite->getCharXOffset(spriteIdx);
+ int16 spriteYOffset = _grd002Sprite->getCharYOffset(spriteIdx);
+ int spriteW = _grd002Sprite->getCharWidth(spriteIdx);
+ int spriteH = _grd002Sprite->getCharHeight(spriteIdx);
+ bool shouldMirror = (_rebelFlightDir != 0 && _rebelDamageLevel == 0);
+
+ int drawX;
+ if (shouldMirror) {
+ drawX = _rebelViewOffset2X + (320 - spriteW - spriteXOffset);
+ } else {
+ drawX = _rebelViewOffset2X + spriteXOffset;
+ }
+ int drawY = spriteYOffset + _rebelViewOffset2Y;
+
+ // Gun barrel is approximately at the character's hand level:
+ // X: center of sprite ± directional offset toward the target
+ // Y: about 60% down the sprite height (hand/arm level)
+ _turretShots[i].gunX = drawX + spriteW / 2;
+ _turretShots[i].gunY = drawY + (spriteH * 3) / 5;
+ } else {
+ // Fallback: approximate center-bottom of character area
+ _turretShots[i].gunX = _rebelViewOffset2X + 160;
+ _turretShots[i].gunY = _rebelViewOffset2Y + 140;
+ }
+
+ debug("Rebel2 Handler25: Spawned shot %d target (%d,%d) gun (%d,%d)",
+ i, _turretShots[i].targetX, _turretShots[i].targetY,
+ _turretShots[i].gunX, _turretShots[i].gunY);
break;
}
}
@@ -6008,16 +6073,10 @@ void InsaneRebel2::renderHandler25LaserShots(byte *renderBitmap, int pitch, int
int16 targetX = _turretShots[i].targetX;
int16 targetY = _turretShots[i].targetY;
- // Gun position: In FUN_0041f004, the gun is at a fixed offset based on character sprite
- // From FUN_0041db5e lines 178-187, the gun position is:
- // gunX = (spriteOffset + DAT_00457910) - viewOffsetX
- // gunY = (spriteOffset + DAT_00457912) - viewOffsetY
- //
- // For simplicity, use a position near the bottom-center of the screen
- // where the character's gun would be in the speeder bike cockpit.
- // The character is typically around (160, 150) in the cockpit view.
- int16 gunX = 160; // Center of screen
- int16 gunY = 170; // Near bottom where character sits
+ // Gun position computed at spawn time from GRD002 sprite data
+ // Original: DAT_0045791c[i] + DAT_0045790c, DAT_00457920[i] + DAT_0045790e
+ int16 gunX = _turretShots[i].gunX;
+ int16 gunY = _turretShots[i].gunY;
int16 progress = maxDuration - _turretShots[i].counter;
diff --git a/engines/scumm/insane/insane_rebel.h b/engines/scumm/insane/insane_rebel.h
index 199bcb9ed02..be88484b07d 100644
--- a/engines/scumm/insane/insane_rebel.h
+++ b/engines/scumm/insane/insane_rebel.h
@@ -663,6 +663,8 @@ public:
int16 targetX; // DAT_0044367e[i] - target X position
int16 targetY; // DAT_00443682[i] - target Y position
int16 seqNum; // DAT_0044368a[i] - shot sequence (for alternating)
+ int16 gunX; // DAT_0045791c[i] - gun barrel X (Handler 25, screen coords)
+ int16 gunY; // DAT_00457920[i] - gun barrel Y (Handler 25, screen coords)
};
TurretShot _turretShots[2];
int16 _turretShotSeqCounter; // DAT_0047fe94 - global sequence counter
Commit: 285f7ff2b11ed928858db12aeebdf0f360edc492
https://github.com/scummvm/scummvm/commit/285f7ff2b11ed928858db12aeebdf0f360edc492
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:07+02:00
Commit Message:
SCUMM: RA2: Prevent damage while covered
Changed paths:
engines/scumm/insane/insane_rebel.cpp
engines/scumm/insane/insane_rebel.h
diff --git a/engines/scumm/insane/insane_rebel.cpp b/engines/scumm/insane/insane_rebel.cpp
index 7b769ee3ad4..781e3717bf1 100644
--- a/engines/scumm/insane/insane_rebel.cpp
+++ b/engines/scumm/insane/insane_rebel.cpp
@@ -1288,89 +1288,125 @@ void InsaneRebel2::iactRebel2Opcode3(Common::SeekableReadStream &b, int16 par2,
//
// Stream position on entry: at offset +8 (body[0], first word after 8-byte header)
- if (par3 == 1 || par3 == 2) {
- // Direct hit path â FUN_4092D9 lines 209-227
+ // Handler 25 has a different opcode 3 structure (FUN_41CADB case 1):
+ // par3==5: probabilistic damage WITH cover check (DAT_0045790a < 2)
+ // par3==1: increment hit counter ONLY (NO damage), requires par4 != 4
+ // par4==100: direct damage (separate check after par3 branches, NO cover check)
+ // Other handlers (0x26/7/8) use FUN_4092D9/FUN_40E35E/FUN_401234 with different logic.
+ if (_rebelHandler == 25) {
+ // Handler 25 opcode 3 â FUN_41CADB case 1
+ int16 srcIdBody0 = b.readSint16LE(); // body[0] (offset +8)
+ int16 srcIdBody1 = b.readSint16LE(); // body[1] (offset +10)
+
+ if (par3 == 5) {
+ // Probabilistic damage with cover check (lines 81-92)
+ debug("Rebel2 Opcode3: H25 par3=5 srcId=%d isBitSet=%d damageLevel=%d",
+ srcIdBody1, isBitSet(srcIdBody1), _rebelDamageLevel);
+
+ if (_rebelDamageLevel < 2 && !isBitSet(srcIdBody1)) {
+ int probability = 20 + _difficulty * 20;
+ if (probability < 5) probability = 5;
+ if (probability > 90) probability = 90;
+ int roll = _vm->_rnd.getRandomNumber(99);
+ debug("Rebel2 Opcode3: probability=%d roll=%d (need roll < prob)", probability, roll);
+
+ if (roll < probability) {
+ if (!_rebelInvulnerable) {
+ int damageAmount = 5 + (_difficulty * 2);
+ _playerDamage += damageAmount;
+ if (_playerDamage > 255) _playerDamage = 255;
+ debug("Rebel2: H25 PROBABILISTIC damage from %d. Damage=%d total=%d",
+ srcIdBody1, damageAmount, _playerDamage);
+ }
+ initDamageFlash();
+ }
+ } else {
+ debug("Rebel2 Opcode3: H25 par3=5 BLOCKED (damageLevel=%d isBitSet=%d)",
+ _rebelDamageLevel, isBitSet(srcIdBody1));
+ }
+ } else if (par3 == 1 && !isBitSet(srcIdBody0) && par4 != 4) {
+ // Hit counter only â NO damage (lines 94-98)
+ _rebelHitCounter++;
+ debug("Rebel2: H25 hit counter++ -> %d (par3=1 par4=%d, no damage)",
+ _rebelHitCounter, par4);
+ } else {
+ debug("Rebel2 Opcode3: H25 par3=%d par4=%d (no action)", par3, par4);
+ }
+
+ // Direct damage: par4==100, separate from par3 branches (lines 99-111)
+ if (par4 == 100 && !isBitSet(srcIdBody0)) {
+ if (!_rebelInvulnerable) {
+ int directHitDamage = 8 + (_difficulty * 4);
+ _playerDamage += directHitDamage;
+ if (_playerDamage > 255) _playerDamage = 255;
+ debug("Rebel2: H25 DIRECT HIT par4=100 damage=%d total=%d",
+ directHitDamage, _playerDamage);
+ }
+ initDamageFlash();
+ }
+ } else if (par3 == 1 || par3 == 2) {
+ // Non-Handler-25 direct hit path â FUN_4092D9 lines 209-227
int16 srcId = b.readSint16LE(); // body[0] (offset +8): source enemy ID
debug("Rebel2 Opcode3: par3=%d par4=%d srcId=%d isBitSet=%d",
par3, par4, srcId, isBitSet(srcId));
- // FUN_00423970(srcId): only proceed if source enemy is active (bit clear)
if (!isBitSet(srcId)) {
- // Always increment hit counter (DAT_0047ab80) â line 215
_rebelHitCounter++;
debug("Rebel2: Incremented hit counter -> %d", _rebelHitCounter);
- // Damage path: par4 must be non-zero AND direct hit damage table > 0
- // TODO: Read actual damage from per-level table DAT_0047e0f4
int directHitDamage = 8 + (_difficulty * 4);
if (par4 != 0 && directHitDamage > 0) {
bool shouldDamage = false;
if (par3 == 1 && par4 < 10) {
- // par3=1: simple direct hit, par4 is hit strength (1..9)
shouldDamage = true;
} else if (par3 == 2 && par4 > 99) {
- // par3=2: wave-gated hit, par4 > 99 required
- // Additional check: for par4 >= 101 (0x65), verify wave state bit
- // Original: (DAT_0047ab9c & (1 << ((par4 + 0x9b) & 0x1f))) == 0
if (par4 < 0x65 || (_rebelPhaseState & (1 << ((par4 + 0x9b) & 0x1f))) == 0) {
shouldDamage = true;
}
}
if (shouldDamage) {
- // Apply damage unless invulnerable (DAT_0047ab64 == 0)
if (!_rebelInvulnerable) {
_playerDamage += directHitDamage;
if (_playerDamage > 255) _playerDamage = 255;
debug("Rebel2: DIRECT HIT damage from enemy %d. par3=%d par4=%d damage=%d total=%d",
srcId, par3, par4, directHitDamage, _playerDamage);
}
- // Visual effect â FUN_00420515 (palette flash)
initDamageFlash();
- // TODO: FUN_0041189e(1, 0, 0x7f, 0, 0) â play hit sound
}
}
}
} else if (par3 == 5) {
- // Probabilistic damage path â FUN_4092D9 lines 228-239
- b.skip(2); // Skip body[0] (offset +8, not used for par3=5)
- int16 srcId = b.readSint16LE(); // body[1] (offset +10): source enemy ID
+ // Non-Handler-25 probabilistic damage â FUN_4092D9 lines 228-239
+ b.skip(2); // Skip body[0]
+ int16 srcId = b.readSint16LE(); // body[1] (offset +10)
debug("Rebel2 Opcode3: par3=5 srcId=%d isBitSet=%d", srcId, isBitSet(srcId));
- // FUN_00423970(srcId): only proceed if source enemy is active (bit clear)
if (!isBitSet(srcId)) {
- // Probability check: original reads from per-level table DAT_0047e0fc
- // TODO: Use actual per-level probability table instead of hardcoded values
int probability = 20 + _difficulty * 20;
if (probability < 5) probability = 5;
if (probability > 90) probability = 90;
- int roll = _vm->_rnd.getRandomNumber(99); // FUN_004233a0(100) returns [0,99]
+ int roll = _vm->_rnd.getRandomNumber(99);
debug("Rebel2 Opcode3: probability=%d roll=%d (need roll < prob)", probability, roll);
if (roll < probability) {
- // Apply damage unless invulnerable (DAT_0047ab64 == 0)
if (!_rebelInvulnerable) {
- // TODO: Read damage amount from per-level table DAT_0047e0f8
int damageAmount = 5 + (_difficulty * 2);
_playerDamage += damageAmount;
if (_playerDamage > 255) _playerDamage = 255;
debug("Rebel2: PROBABILISTIC damage from enemy %d. Damage=%d total=%d",
srcId, damageAmount, _playerDamage);
}
- // Visual effect â called regardless of invulnerability.
- // Handler 8: FUN_0042073B â palette flash + screen shake
- // Other handlers: FUN_00420515 â palette flash only (no screen shake)
if (_rebelHandler == 8) {
triggerDamageEffect();
} else {
initDamageFlash();
}
- // TODO: FUN_0041189e(1, 0, 0x7f, 0, 0) â play hit sound
}
}
} else {
@@ -8599,6 +8635,99 @@ int InsaneRebel2::runLevel1() {
return kLevelQuit;
}
+// =============================================================================
+// Wave State Management - FUN_00417b61
+// Waits for video completion, accumulates kill state, redistributes kill credits.
+// Used by all multi-wave levels (Level 2, 3, 6, etc.) as the core wave loop primitive.
+// =============================================================================
+
+uint16 InsaneRebel2::processWaveEnd(int16 mask, int16 *budget, int16 threshold, uint16 flags) {
+ // FUN_00417b61: Core wave management function
+ // Called after each wave video plays. Handles:
+ // 1. Waiting for video to finish (with early exit on enemy completion)
+ // 2. Copying wave state to accumulated phase state
+ // 3. Redistributing kill credits from the budget
+ //
+ // Returns: kill bits credited this wave, or 0xFFFF on death/quit/completion
+
+ uint16 result = 0;
+
+ // Step 1: Wait for video to finish (lines 21-32)
+ // Original loop: while (damage < 0xff && frame < maxFrame-1 && !escPressed)
+ // The SmushPlayer::play() call already blocks until video ends, so this step
+ // is handled implicitly. The early-exit logic (threshold > 0: if frame > 50
+ // AND all required enemy type bits are set, count up and break when > threshold)
+ // would need per-frame callbacks to work precisely. For now, the primary effect
+ // is covered by the video playing to completion and accumulating state.
+ // TODO: Implement per-frame early exit callback for threshold-based wave termination.
+
+ // Step 2: Copy wave state to phase state (line 33)
+ // DAT_0047ab9c = DAT_0047ab98
+ _rebelPhaseState = _rebelWaveState;
+ debug("Rebel2 processWaveEnd: waveState=0x%x -> phaseState=0x%x mask=0x%x budget=%d threshold=%d flags=%d",
+ _rebelWaveState, _rebelPhaseState, mask, budget ? *budget : -1, threshold, flags);
+
+ // Step 3: Kill redistribution - add random unkilled types (lines 34-47)
+ // Only when (flags & 2) != 0. Level 2 always passes flags=0, so inactive for Level 2.
+ if ((flags & 2) != 0) {
+ // Collect unkilled enemy type bits that are within the mask
+ byte unkilled[8];
+ int16 numUnkilled = 0;
+ for (byte b = 0; (2 << (b & 0x1f)) < (int)(mask & 0x0e); b++) {
+ if ((_rebelPhaseState & (2 << (b & 0x1f))) == 0) {
+ unkilled[numUnkilled] = (byte)(2 << (b & 0x1f));
+ numUnkilled++;
+ }
+ }
+ if (numUnkilled > 0) {
+ // Randomly add one unkilled type to phase state
+ int idx = _vm->_rnd.getRandomNumber(numUnkilled - 1);
+ _rebelPhaseState |= unkilled[idx];
+ if (budget) (*budget)++;
+ }
+ }
+
+ // Step 4: Kill credit transfer (lines 48-73)
+ // Collect all SET enemy type bits from phase state
+ byte killed[8];
+ int16 numKilled = 0;
+ for (byte b = 0; (2 << (b & 0x1f)) < (int)(mask & 0x0e); b++) {
+ if ((_rebelPhaseState & (2 << (b & 0x1f))) != 0) {
+ killed[numKilled] = (byte)(2 << (b & 0x1f));
+ numKilled++;
+ }
+ }
+
+ // Max credits: 8 normally, 2 if flag bit 0 set
+ int16 maxCredits = ((flags & 1) == 0) ? 8 : 2;
+
+ // Transfer kills from phase state to result, limited by budget
+ int16 creditCount = 0;
+ while (creditCount < maxCredits && numKilled > 0 && budget && *budget > 0) {
+ int idx = _vm->_rnd.getRandomNumber(numKilled - 1);
+ _rebelPhaseState -= killed[idx]; // Remove from accumulated state
+ result |= killed[idx]; // Credit to return value
+ (*budget)--;
+
+ // Remove from array (shift remaining elements)
+ for (int i = idx; i + 1 < numKilled; i++) {
+ killed[i] = killed[i + 1];
+ }
+ numKilled--;
+ creditCount++;
+ }
+
+ debug("Rebel2 processWaveEnd: result=0x%x phaseState=0x%x (after redistribution) budget=%d",
+ result, _rebelPhaseState, budget ? *budget : -1);
+
+ // Step 5: Return value (lines 74-78)
+ // Return 0xFFFF if: dead, phase complete, or quit
+ if (_playerDamage >= 255 || (int16)_rebelPhaseState >= mask || _vm->shouldQuit()) {
+ return 0xFFFF;
+ }
+ return result;
+}
+
// =============================================================================
// Level 2 Handler - FUN_00418063
// Multiple parts with P1/P2/P3 subdirectories
@@ -8612,6 +8741,13 @@ int InsaneRebel2::runLevel2() {
// Phase 1 mask: 0x06 (enemy types 1,2)
// Phase 2 mask: 0x0e (enemy types 1,2,3)
// Phase 3 mask: 0x0e (enemy types 1,2,3)
+ //
+ // Kill credit budget (from level data table DAT_0047e0e8):
+ // Each phase gets a budget = tableBase + random(3). processWaveEnd() uses
+ // this budget to randomly redistribute kill credits, creating non-deterministic
+ // wave progression. Using calibrated defaults until exact table values extracted.
+ static const int16 kLevel2BudgetBase[3] = { 3, 3, 3 }; // Phase 1, 2, 3
+
SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
int bonusCount = 0; // local_1c: tracks bonus events (DAT_0047ab9c & 0x10)
int totalKills = 0; // local_c: accumulated kill count across phases
@@ -8623,9 +8759,14 @@ int InsaneRebel2::runLevel2() {
if (_vm->shouldQuit()) return kLevelQuit;
// Play level beginning cinematic (02BEG.SAN)
+ // Original: FUN_004171c5("LEV02/02BEG.SAN", 0x20, 0xab, 0xa0, 10, 2, 0x46)
+ // Includes text overlay from GAME.TRS â deferred until text rendering is ready.
playLevelBegin(2);
if (_vm->shouldQuit()) return kLevelQuit;
+ // FUN_00401000 + FUN_00407d10 + FUN_0040c040: Reset game state (before retry loop)
+ clearBit(0);
+
// Main gameplay retry loop (restarts from beginning on death)
while (!_vm->shouldQuit()) {
_playerShield = 255;
@@ -8635,8 +8776,7 @@ int InsaneRebel2::runLevel2() {
totalKills = 0;
totalMisses = 0;
- // FUN_00401000 + FUN_00407d10 + FUN_0040c040: Reset game state
- clearBit(0);
+ // FUN_0041c7d0: Reset per-attempt state
_enemies.clear();
for (int i = 0; i < 512; i++) {
_rebelLinks[i][0] = 0;
@@ -8651,16 +8791,20 @@ int InsaneRebel2::runLevel2() {
_rebelPhaseState = 0;
_rebelWaveState = 0;
- // Play A.SAN (background loader)
- debug("Rebel2: Level 2 Phase 1 - playing 02P01_A.SAN (background)");
+ // Initialize kill budget from level data table + random(3)
+ // Original: sVar4 = levelData[phase1Offset]; local_14[0] = sVar4 + random(3)
+ int16 budget = kLevel2BudgetBase[0] + _vm->_rnd.getRandomNumber(2);
+
+ // Play A.SAN (background loader) â flags 0x28 (preserve buffer, gameplay mode)
+ debug("Rebel2: Level 2 Phase 1 - playing 02P01_A.SAN (background) budget=%d", budget);
splayer->setCurVideoFlags(0x28);
splayer->play("LEV02/P1/02P01_A.SAN", 12);
_deathFrame = splayer->_frame;
if (_vm->shouldQuit()) return kLevelQuit;
- // Copy wave state to phase state after A.SAN
- _rebelPhaseState = _rebelWaveState;
+ // processWaveEnd after A.SAN (threshold=0, no early exit for background loader)
+ processWaveEnd(0x36, &budget, 0, 0);
// Phase 1 wave loop: random B/C/D until all type 1,2 enemies killed
// Original: while (uVar3 >= 0 && (DAT_0047ab9c & 6) != 6)
@@ -8674,14 +8818,15 @@ int InsaneRebel2::runLevel2() {
"LEV02/P1/02P01_C.SAN",
"LEV02/P1/02P01_D.SAN"
};
- debug("Rebel2: Phase 1 wave - playing %s (state=0x%x)", variants[variant], _rebelPhaseState);
- splayer->setCurVideoFlags(0x28);
+ debug("Rebel2: Phase 1 wave - playing %s (state=0x%x budget=%d)", variants[variant], _rebelPhaseState, budget);
+ // Wave videos use flags 0x428 (original: FUN_0041f4d0 param_2=0x428)
+ splayer->setCurVideoFlags(0x428);
splayer->play(variants[variant], 12);
_deathFrame = splayer->_frame;
- // After wave: copy wave state to phase state (FUN_00417b61 line 33)
- _rebelPhaseState = _rebelWaveState;
- debug("Rebel2: Phase 1 wave done - state=0x%x (need 0x06)", _rebelPhaseState);
+ // processWaveEnd with threshold=0x14 (20) â enables early exit when enemies killed
+ processWaveEnd(0x36, &budget, 0x14, 0);
+ debug("Rebel2: Phase 1 wave done - state=0x%x (need 0x06) budget=%d", _rebelPhaseState, budget);
}
// Check for bonus (bit 4 = 0x10)
@@ -8706,22 +8851,24 @@ int InsaneRebel2::runLevel2() {
_rebelWaveState = 0;
_enemies.clear();
+ // Initialize Phase 2 budget
+ budget = kLevel2BudgetBase[1] + _vm->_rnd.getRandomNumber(2);
+
// Play A.SAN (background loader)
- debug("Rebel2: Level 2 Phase 2 - playing 02P02_A.SAN (background)");
+ debug("Rebel2: Level 2 Phase 2 - playing 02P02_A.SAN (background) budget=%d", budget);
splayer->setCurVideoFlags(0x28);
splayer->play("LEV02/P2/02P02_A.SAN", 12);
_deathFrame = splayer->_frame;
if (_vm->shouldQuit()) return kLevelQuit;
- _rebelPhaseState = _rebelWaveState;
- // Phase 2 wave loop: state-based variant selection
- // Original: while (local_10 >= 0 && (DAT_0047ab9c & 0xe) != 0xe)
- while (_playerDamage < 255 && (_rebelPhaseState & 0x0e) != 0x0e) {
+ // Phase 2 wave loop: processWaveEnd at TOP of loop (matches assembly structure)
+ // Original: local_10 = FUN_00417b61(0x3e, local_14, 0, 0); then switch(local_10)
+ while (true) {
+ uint16 waveSelect = processWaveEnd(0x3e, &budget, 0, 0);
+ if (waveSelect == 0xFFFF || (_rebelPhaseState & 0x0e) == 0x0e) break;
if (_vm->shouldQuit()) return kLevelQuit;
- int waveSelect = _rebelPhaseState & 0x0e; // masked enemy state
-
// If no specific pattern: randomize high bits (original lines 71-74)
// When (local_10 & 0xc) == 0: add random 0x10/0x11/0x12
if ((waveSelect & 0x0c) == 0) {
@@ -8745,13 +8892,10 @@ int InsaneRebel2::runLevel2() {
filename = "LEV02/P2/02P02_D.SAN"; break;
}
- debug("Rebel2: Phase 2 wave - playing %s (state=0x%x sel=0x%x)", filename, _rebelPhaseState, waveSelect);
- splayer->setCurVideoFlags(0x28);
+ debug("Rebel2: Phase 2 wave - playing %s (state=0x%x sel=0x%x budget=%d)", filename, _rebelPhaseState, waveSelect, budget);
+ splayer->setCurVideoFlags(0x428);
splayer->play(filename, 12);
_deathFrame = splayer->_frame;
-
- _rebelPhaseState = _rebelWaveState;
- debug("Rebel2: Phase 2 wave done - state=0x%x (need 0x0e)", _rebelPhaseState);
}
if ((_rebelPhaseState & 0x10) != 0) bonusCount++;
@@ -8776,21 +8920,23 @@ int InsaneRebel2::runLevel2() {
_enemies.clear();
prevWaveState = 0;
+ // Initialize Phase 3 budget
+ budget = kLevel2BudgetBase[2] + _vm->_rnd.getRandomNumber(2);
+
// Play A.SAN (background loader)
- debug("Rebel2: Level 2 Phase 3 - playing 02P03_A.SAN (background)");
+ debug("Rebel2: Level 2 Phase 3 - playing 02P03_A.SAN (background) budget=%d", budget);
splayer->setCurVideoFlags(0x28);
splayer->play("LEV02/P3/02P03_A.SAN", 12);
_deathFrame = splayer->_frame;
if (_vm->shouldQuit()) return kLevelQuit;
- _rebelPhaseState = _rebelWaveState;
- // Phase 3 wave loop: state-based with randomization
- // Original: while (local_10 >= 0 && (DAT_0047ab9c & 0xe) != 0xe)
+ // Phase 3: processWaveEnd at BOTTOM (like Phase 1), waveSelect carried across iterations
+ // Original: local_10 = FUN_00417b61(0x3e, local_14, 0, 0); while (loop) { ...; local_10 = FUN_00417b61(0x3e, local_14, 0x14, 0); }
{
- int waveSelect = _rebelPhaseState & 0x0e;
+ uint16 waveSelect = processWaveEnd(0x3e, &budget, 0, 0);
- while (_playerDamage < 255 && (_rebelPhaseState & 0x0e) != 0x0e) {
+ while (waveSelect != 0xFFFF && (_rebelPhaseState & 0x0e) != 0x0e) {
if (_vm->shouldQuit()) return kLevelQuit;
// Phase 3 randomization (original lines 113-115):
@@ -8823,14 +8969,14 @@ int InsaneRebel2::runLevel2() {
filename = "LEV02/P3/02P03_I.SAN"; break;
}
- debug("Rebel2: Phase 3 wave - playing %s (state=0x%x sel=0x%x)", filename, _rebelPhaseState, waveSelect);
- splayer->setCurVideoFlags(0x28);
+ debug("Rebel2: Phase 3 wave - playing %s (state=0x%x sel=0x%x budget=%d)", filename, _rebelPhaseState, waveSelect, budget);
+ splayer->setCurVideoFlags(0x428);
splayer->play(filename, 12);
_deathFrame = splayer->_frame;
- _rebelPhaseState = _rebelWaveState;
- waveSelect = _rebelPhaseState & 0x0e;
- debug("Rebel2: Phase 3 wave done - state=0x%x (need 0x0e)", _rebelPhaseState);
+ // processWaveEnd at BOTTOM with threshold=0x14
+ waveSelect = processWaveEnd(0x3e, &budget, 0x14, 0);
+ debug("Rebel2: Phase 3 wave done - state=0x%x (need 0x0e) budget=%d", _rebelPhaseState, budget);
}
}
@@ -8841,9 +8987,12 @@ int InsaneRebel2::runLevel2() {
if (_vm->shouldQuit()) return kLevelQuit;
// Level completed! Calculate accuracy score.
+ // Original: FUN_00417327 with score thresholds and medal ranks
+ // Score presentation deferred until GAME.TRS text rendering is implemented.
{
+ totalMisses += _rebelHitCounter;
int accuracy = 0;
- int totalShots = totalKills + totalMisses + _rebelHitCounter;
+ int totalShots = totalKills + totalMisses;
if (totalKills > 0 && totalShots > 0) {
accuracy = (totalKills * 100) / totalShots;
}
@@ -8857,12 +9006,17 @@ int InsaneRebel2::runLevel2() {
level2_death:
// Player died â play death sequence and retry or game over
+ // Original: FUN_00417168("LEV02/02DIE.SAN", 0x20)
debug("Rebel2: Level 2 Phase %d death", _currentPhase);
playCinematic("LEV02/02DIE.SAN");
if (_vm->shouldQuit()) return kLevelQuit;
+ // Original: if (DAT_0047ab5c != 0) DAT_0047a7ee++ (bonus life award)
+ // DAT_0047ab5c is set when player earns a bonus life (e.g., score threshold).
+ // Currently not tracked â will be wired when bonus life system is implemented.
_playerLives--;
if (_playerLives <= 0) {
+ // Original: FUN_00417ab2("LEV02/02OVER.SAN", 0x20, 2)
playLevelGameOver(2);
return kLevelGameOver;
}
diff --git a/engines/scumm/insane/insane_rebel.h b/engines/scumm/insane/insane_rebel.h
index be88484b07d..c91ba5ac25b 100644
--- a/engines/scumm/insane/insane_rebel.h
+++ b/engines/scumm/insane/insane_rebel.h
@@ -243,6 +243,15 @@ public:
int runLevel5(); // FUN_00418EC6 - Single gameplay phase
int runLevel6(); // FUN_00419317 - Two phases with per-phase retry
+ // Wave state management (FUN_00417b61)
+ // Waits for current video to finish, accumulates kill state, redistributes
+ // kill credits from the budget. Returns credited kill bits, or 0xFFFF on death/quit.
+ // mask: required enemy bits (0x36 for Phase 1, 0x3e for Phases 2/3)
+ // budget: kill credit budget counter (decremented per credit transfer)
+ // threshold: early-exit frame threshold (0=disabled, 0x14=20 for wave loops)
+ // flags: bit 1 = add random unkilled types, bit 0 = limit credits to 2 (else 8)
+ uint16 processWaveEnd(int16 mask, int16 *budget, int16 threshold, uint16 flags);
+
// Random number helper (emulates FUN_004233a0)
int getRandomVariant(int max);
Commit: 713aa7bd917e1f02a2d04aaca2e7997039379693
https://github.com/scummvm/scummvm/commit/713aa7bd917e1f02a2d04aaca2e7997039379693
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:07+02:00
Commit Message:
SCUMM: RA2: Fix covered and uncovered phase cycling
Changed paths:
engines/scumm/insane/insane_rebel.cpp
diff --git a/engines/scumm/insane/insane_rebel.cpp b/engines/scumm/insane/insane_rebel.cpp
index 781e3717bf1..47bc3acc47b 100644
--- a/engines/scumm/insane/insane_rebel.cpp
+++ b/engines/scumm/insane/insane_rebel.cpp
@@ -1863,11 +1863,11 @@ void InsaneRebel2::iactRebel2Opcode6(byte *renderBitmap, Common::SeekableReadStr
// Reset wave state to accumulated phase state (same as Handler 8)
// DAT_0047ab98 = DAT_0047ab9c: ensures new wave starts with correct state
_rebelWaveState = _rebelPhaseState;
- // Initialize to covered state - player starts behind cover
- // This ensures the corridor overlay shows the "covered" position initially
- _rebelAutopilot = 1; // DAT_00457904 = 1 (covered)
- _rebelDamageLevel = 5; // DAT_0045790a = 5 (fully in cover)
- debug("Rebel2 Opcode 6 (Handler 25): Status bar enabled, state reset, starting COVERED, wave=0x%x", _rebelWaveState);
+ // NOTE: autopilot and damageLevel are NOT reset here (verified against
+ // FUN_41CADB case 4 lines 114-121). They persist across video boundaries
+ // so the player keeps their cover state when transitioning between waves.
+ debug("Rebel2 Opcode 6 (Handler 25): Status bar enabled, state reset, wave=0x%x autopilot=%d damageLevel=%d",
+ _rebelWaveState, _rebelAutopilot, _rebelDamageLevel);
}
// Set sprite mode (DAT_00457900 = local_14[3]) - controls which GRD sprite to render
@@ -8776,6 +8776,12 @@ int InsaneRebel2::runLevel2() {
totalKills = 0;
totalMisses = 0;
+ // Reset Handler 25 cover state â player starts uncovered at level start
+ // DAT_00457904 and DAT_0045790a are zero-initialized globals in the original
+ _rebelAutopilot = 0;
+ _rebelDamageLevel = 0;
+ _rebelControlMode = 0;
+
// FUN_0041c7d0: Reset per-attempt state
_enemies.clear();
for (int i = 0; i < 512; i++) {
Commit: 7ae1ddbd5a4342f07d3ed98fbdc1218541e28d78
https://github.com/scummvm/scummvm/commit/7ae1ddbd5a4342f07d3ed98fbdc1218541e28d78
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:07+02:00
Commit Message:
SCUMM: RA2: Reset handlers around level 2 cinematics
Changed paths:
engines/scumm/insane/insane_rebel.cpp
engines/scumm/smush/smush_player.cpp
diff --git a/engines/scumm/insane/insane_rebel.cpp b/engines/scumm/insane/insane_rebel.cpp
index 47bc3acc47b..615c2411851 100644
--- a/engines/scumm/insane/insane_rebel.cpp
+++ b/engines/scumm/insane/insane_rebel.cpp
@@ -8116,15 +8116,16 @@ void InsaneRebel2::playCinematic(const char *filename) {
// Play a cinematic/cutscene video with proper intro mode setup
// This helper ensures:
// 1. Handler is reset to 0 (no HUD, no shooting)
- // 2. Video flags are set to 0x20 (cinematic mode)
+ // 2. Video flags are set to 0x28 (cinematic with buffer preserve)
//
- // Original: DAT_0047ee84 is only set by IACT opcode 6 during gameplay videos
- // Cinematics don't have opcode 6, so handler stays 0
+ // Original: All video wrapper functions (FUN_00417168, FUN_004171c5,
+ // FUN_00417ab2, FUN_00417327) add | 8 to the base flags before calling
+ // FUN_0041f4d0, so the 0x08 bit (preserve buffer) is always set.
_rebelHandler = 0;
_rebelStatusBarSprite = 0; // No status bar during cinematics
SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
- splayer->setCurVideoFlags(0x20); // Cinematic mode
+ splayer->setCurVideoFlags(0x28); // Cinematic mode + buffer preserve (0x20 | 0x08)
splayer->play(filename, 12);
}
@@ -8182,7 +8183,7 @@ bool InsaneRebel2::playLevelGameplay(int levelId) {
// First play the cutscene
filename = Common::String::format("%s/%sCUT.SAN", dir.c_str(), prefix.c_str());
debug("Rebel2: Playing cutscene %s", filename.c_str());
- splayer->setCurVideoFlags(0x20);
+ splayer->setCurVideoFlags(0x28);
splayer->play(filename.c_str(), 12);
if (_vm->shouldQuit() || _playerShield == 0) return false;
@@ -8196,14 +8197,17 @@ bool InsaneRebel2::playLevelGameplay(int levelId) {
if (_vm->shouldQuit() || _playerShield == 0) return false;
// Post segment 1
+ _rebelHandler = 0;
+ _rebelStatusBarSprite = 0;
filename = Common::String::format("%s/%sPST1.SAN", dir.c_str(), prefix.c_str());
debug("Rebel2: Playing %s", filename.c_str());
- splayer->setCurVideoFlags(0x20);
+ splayer->setCurVideoFlags(0x28);
splayer->play(filename.c_str(), 12);
if (_vm->shouldQuit() || _playerShield == 0) return false;
// Part 2
+ _rebelHandler = 8;
splayer->setCurVideoFlags(0x28);
filename = Common::String::format("%s/P2/%sP02_A.SAN", dir.c_str(), prefix.c_str());
debug("Rebel2: Playing %s", filename.c_str());
@@ -8212,14 +8216,17 @@ bool InsaneRebel2::playLevelGameplay(int levelId) {
if (_vm->shouldQuit() || _playerShield == 0) return false;
// Post segment 2
+ _rebelHandler = 0;
+ _rebelStatusBarSprite = 0;
filename = Common::String::format("%s/%sPST2.SAN", dir.c_str(), prefix.c_str());
debug("Rebel2: Playing %s", filename.c_str());
- splayer->setCurVideoFlags(0x20);
+ splayer->setCurVideoFlags(0x28);
splayer->play(filename.c_str(), 12);
if (_vm->shouldQuit() || _playerShield == 0) return false;
// Part 3
+ _rebelHandler = 8;
splayer->setCurVideoFlags(0x28);
filename = Common::String::format("%s/P3/%sP03_A.SAN", dir.c_str(), prefix.c_str());
debug("Rebel2: Playing %s", filename.c_str());
@@ -8237,14 +8244,16 @@ bool InsaneRebel2::playLevelGameplay(int levelId) {
if (_vm->shouldQuit() || _playerShield == 0) return false;
// Post segment
+ _rebelHandler = 0;
+ _rebelStatusBarSprite = 0;
filename = Common::String::format("%s/%sPOST1.SAN", dir.c_str(), prefix.c_str());
debug("Rebel2: Playing %s", filename.c_str());
- splayer->setCurVideoFlags(0x20);
+ splayer->setCurVideoFlags(0x28);
splayer->play(filename.c_str(), 12);
if (_vm->shouldQuit() || _playerShield == 0) return false;
- // Phase 2
+ // Phase 2 â handler will be re-set by IACT opcode 6
splayer->setCurVideoFlags(0x28);
filename = Common::String::format("%s/%sPLAY2.SAN", dir.c_str(), prefix.c_str());
debug("Rebel2: Playing %s", filename.c_str());
@@ -8256,7 +8265,7 @@ bool InsaneRebel2::playLevelGameplay(int levelId) {
// Level 4: Has cutscene, then single gameplay
filename = Common::String::format("%s/%sCUT.SAN", dir.c_str(), prefix.c_str());
debug("Rebel2: Playing cutscene %s", filename.c_str());
- splayer->setCurVideoFlags(0x20);
+ splayer->setCurVideoFlags(0x28);
splayer->play(filename.c_str(), 12);
if (_vm->shouldQuit()) return false;
@@ -8283,14 +8292,16 @@ bool InsaneRebel2::playLevelGameplay(int levelId) {
if (_vm->shouldQuit() || _playerShield == 0) return false;
// Post segment
+ _rebelHandler = 0;
+ _rebelStatusBarSprite = 0;
filename = Common::String::format("%s/%sPOST1.SAN", dir.c_str(), prefix.c_str());
debug("Rebel2: Playing %s", filename.c_str());
- splayer->setCurVideoFlags(0x20);
+ splayer->setCurVideoFlags(0x28);
splayer->play(filename.c_str(), 12);
if (_vm->shouldQuit() || _playerShield == 0) return false;
- // Phase 2
+ // Phase 2 â handler will be re-set by IACT opcode 6
splayer->setCurVideoFlags(0x28);
filename = Common::String::format("%s/%sPLAY2.SAN", dir.c_str(), prefix.c_str());
debug("Rebel2: Playing %s", filename.c_str());
@@ -8324,7 +8335,8 @@ void InsaneRebel2::playLevelEnd(int levelId) {
debug("Rebel2: Playing level %d end: %s", levelId, filename.c_str());
SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
- splayer->setCurVideoFlags(0x20); // Cinematic mode
+ // Original: FUN_00417327 adds | 8, so flags = 0x20 | 0x08 = 0x28
+ splayer->setCurVideoFlags(0x28);
splayer->play(filename.c_str(), 12);
}
@@ -8350,7 +8362,8 @@ void InsaneRebel2::playLevelDeath(int levelId) {
debug("Rebel2: Playing level %d death: %s", levelId, filename.c_str());
SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
- splayer->setCurVideoFlags(0x20);
+ // Original: FUN_00417168 adds | 8, so flags = 0x20 | 0x08 = 0x28
+ splayer->setCurVideoFlags(0x28);
splayer->play(filename.c_str(), 12);
}
@@ -8368,7 +8381,8 @@ void InsaneRebel2::playLevelRetry(int levelId) {
debug("Rebel2: Playing level %d retry: %s", levelId, filename.c_str());
SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
- splayer->setCurVideoFlags(0x20);
+ // Original: FUN_00417168 adds | 8, so flags = 0x20 | 0x08 = 0x28
+ splayer->setCurVideoFlags(0x28);
splayer->play(filename.c_str(), 12);
}
@@ -8386,7 +8400,8 @@ void InsaneRebel2::playLevelGameOver(int levelId) {
debug("Rebel2: Playing level %d game over: %s", levelId, filename.c_str());
SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
- splayer->setCurVideoFlags(0x20);
+ // Original: FUN_00417ab2 adds | 8, so flags = 0x20 | 0x08 = 0x28
+ splayer->setCurVideoFlags(0x28);
splayer->play(filename.c_str(), 12);
}
@@ -8546,7 +8561,8 @@ void InsaneRebel2::playLevelDeathVariant(int levelId, int phase, int frame) {
debug("Rebel2: Playing death video: %s (phase=%d, frame=%d)", filename.c_str(), phase, frame);
SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
- splayer->setCurVideoFlags(0x20);
+ // Original: FUN_00417168 adds | 8, so flags = 0x20 | 0x08 = 0x28
+ splayer->setCurVideoFlags(0x28);
splayer->play(filename.c_str(), 12);
}
@@ -8571,7 +8587,8 @@ void InsaneRebel2::playLevelRetryVariant(int levelId, int phase) {
debug("Rebel2: Playing retry video: %s (phase=%d)", filename.c_str(), phase);
SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
- splayer->setCurVideoFlags(0x20);
+ // Original: FUN_00417168 adds | 8, so flags = 0x20 | 0x08 = 0x28
+ splayer->setCurVideoFlags(0x28);
splayer->play(filename.c_str(), 12);
}
@@ -8842,7 +8859,12 @@ int InsaneRebel2::runLevel2() {
if (_vm->shouldQuit()) return kLevelQuit;
// Post segment 1 (02PST1.SAN)
- splayer->setCurVideoFlags(0x20);
+ // Original: FUN_00417168("02PST1.SAN", 0x20) â flags = 0x20 | 0x08 = 0x28
+ // FUN_00417168 adds | 8 to preserve the screen buffer between gameplay and transition
+ // Reset handler to 0 so procPostRendering skips HUD/sprite drawing during cinematic
+ _rebelHandler = 0;
+ _rebelStatusBarSprite = 0;
+ splayer->setCurVideoFlags(0x28);
splayer->play("LEV02/02PST1.SAN", 12);
if (_vm->shouldQuit()) return kLevelQuit;
@@ -8860,6 +8882,9 @@ int InsaneRebel2::runLevel2() {
// Initialize Phase 2 budget
budget = kLevel2BudgetBase[1] + _vm->_rnd.getRandomNumber(2);
+ // Restore handler for gameplay â will be confirmed by IACT opcode 6
+ _rebelHandler = 8;
+
// Play A.SAN (background loader)
debug("Rebel2: Level 2 Phase 2 - playing 02P02_A.SAN (background) budget=%d", budget);
splayer->setCurVideoFlags(0x28);
@@ -8910,7 +8935,11 @@ int InsaneRebel2::runLevel2() {
if (_vm->shouldQuit()) return kLevelQuit;
// Post segment 2 (02PST2.SAN)
- splayer->setCurVideoFlags(0x20);
+ // Original: FUN_00417168("02PST2.SAN", 0x20) â flags = 0x20 | 0x08 = 0x28
+ // Reset handler to 0 so procPostRendering skips HUD/sprite drawing during cinematic
+ _rebelHandler = 0;
+ _rebelStatusBarSprite = 0;
+ splayer->setCurVideoFlags(0x28);
splayer->play("LEV02/02PST2.SAN", 12);
if (_vm->shouldQuit()) return kLevelQuit;
@@ -8929,6 +8958,9 @@ int InsaneRebel2::runLevel2() {
// Initialize Phase 3 budget
budget = kLevel2BudgetBase[2] + _vm->_rnd.getRandomNumber(2);
+ // Restore handler for gameplay â will be confirmed by IACT opcode 6
+ _rebelHandler = 8;
+
// Play A.SAN (background loader)
debug("Rebel2: Level 2 Phase 3 - playing 02P03_A.SAN (background) budget=%d", budget);
splayer->setCurVideoFlags(0x28);
@@ -9091,7 +9123,11 @@ int InsaneRebel2::runLevel3() {
if (_vm->shouldQuit()) return kLevelQuit;
// Post segment 1 (03POST1.SAN)
- splayer->setCurVideoFlags(0x20);
+ // Original: FUN_00417168 adds | 8, so flags = 0x20 | 0x08 = 0x28
+ // Reset handler to 0 so procPostRendering skips HUD/sprite drawing during cinematic
+ _rebelHandler = 0;
+ _rebelStatusBarSprite = 0;
+ splayer->setCurVideoFlags(0x28);
splayer->play("LEV03/03POST1.SAN", 12);
if (_vm->shouldQuit()) return kLevelQuit;
@@ -9150,7 +9186,8 @@ int InsaneRebel2::runLevel4() {
SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
// Play cutscene (04CUT.SAN)
- splayer->setCurVideoFlags(0x20);
+ // Original: FUN_00417168 adds | 8, so flags = 0x20 | 0x08 = 0x28
+ splayer->setCurVideoFlags(0x28);
splayer->play("LEV04/04CUT.SAN", 12);
if (_vm->shouldQuit()) return kLevelQuit;
@@ -9311,7 +9348,11 @@ int InsaneRebel2::runLevel6() {
if (_vm->shouldQuit()) return kLevelQuit;
// Post segment 1 (06POST1.SAN)
- splayer->setCurVideoFlags(0x20);
+ // Original: FUN_00417168 adds | 8, so flags = 0x20 | 0x08 = 0x28
+ // Reset handler to 0 so procPostRendering skips HUD/sprite drawing during cinematic
+ _rebelHandler = 0;
+ _rebelStatusBarSprite = 0;
+ splayer->setCurVideoFlags(0x28);
splayer->play("LEV06/06POST1.SAN", 12);
if (_vm->shouldQuit()) return kLevelQuit;
diff --git a/engines/scumm/smush/smush_player.cpp b/engines/scumm/smush/smush_player.cpp
index 806e14e4435..314c2748953 100644
--- a/engines/scumm/smush/smush_player.cpp
+++ b/engines/scumm/smush/smush_player.cpp
@@ -1369,6 +1369,16 @@ void SmushPlayer::handleAnimHeader(int32 subSize, Common::SeekableReadStream &b)
if (!_skipPalette) {
byte *palettePtr = &headerContent[6];
memcpy(_pal, palettePtr, sizeof(_pal));
+
+ // Reset XPAL delta palette state from the new base palette.
+ // Without this, stale _deltaPal/_shiftedDeltaPal values from a
+ // previous video leak into the new one, corrupting the palette
+ // when XPAL command 256 (apply deltas) is encountered.
+ for (int j = 0; j < 768; ++j) {
+ _shiftedDeltaPal[j] = _pal[j] << 7;
+ }
+ memset(_deltaPal, 0, sizeof(_deltaPal));
+
setDirtyColors(0, 255);
}
Commit: 9130d5bd4b1753dd4230b0f92601acfd963b5ff4
https://github.com/scummvm/scummvm/commit/9130d5bd4b1753dd4230b0f92601acfd963b5ff4
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:08+02:00
Commit Message:
SCUMM: RA2: Fix SKIP and opcode 2 bit handling
Changed paths:
engines/scumm/insane/insane_rebel.cpp
engines/scumm/smush/smush_player.cpp
diff --git a/engines/scumm/insane/insane_rebel.cpp b/engines/scumm/insane/insane_rebel.cpp
index 615c2411851..d4e9638ffce 100644
--- a/engines/scumm/insane/insane_rebel.cpp
+++ b/engines/scumm/insane/insane_rebel.cpp
@@ -933,16 +933,19 @@ void InsaneRebel2::procSKIP(int32 subSize, Common::SeekableReadStream &b) {
if (!par2) {
// Single ID mode: skip next chunk if this object's bit is set (disabled)
- if (isBitSet(par1)) {
+ bool bit1 = isBitSet(par1);
+ if (bit1) {
_player->_skipNext = true;
- debug("Rebel2 SKIP: ID=%d bit is set, skipping next chunk", par1);
}
+ debug("Rebel2 SKIP: single ID=%d bit=%d skip=%d frame=%d", par1, bit1 ? 1 : 0, _player->_skipNext ? 1 : 0, _player->_frame);
} else {
// Dual ID mode: skip if bits are different (XOR logic)
- if (isBitSet(par1) != isBitSet(par2)) {
+ bool bit1 = isBitSet(par1);
+ bool bit2 = isBitSet(par2);
+ if (bit1 != bit2) {
_player->_skipNext = true;
- debug("Rebel2 SKIP: ID=%d and ID=%d bits differ, skipping next chunk", par1, par2);
}
+ debug("Rebel2 SKIP: dual ID1=%d(bit=%d) ID2=%d(bit=%d) skip=%d frame=%d", par1, bit1 ? 1 : 0, par2, bit2 ? 1 : 0, _player->_skipNext ? 1 : 0, _player->_frame);
}
}
@@ -1184,18 +1187,26 @@ void InsaneRebel2::iactRebel2Opcode2(Common::SeekableReadStream &b, int16 par2,
// when childId <= 0. The original game's setBit(0)/clearBit(0) affects ALL bits,
// which would disable/enable all enemies at once - not the intended linking behavior.
if (parentId >= 1 && parentId < 512 && childId >= 1 && childId < 512) {
- // Shift links
+ // Shift links (original: 4 link slots at DAT_0045797c/817c/897c/917c)
_rebelLinks[parentId][2] = _rebelLinks[parentId][1];
_rebelLinks[parentId][1] = _rebelLinks[parentId][0];
_rebelLinks[parentId][0] = childId;
- // DO NOT modify bits here! All bits are reset to CLEAR by clearBit(0) at level start.
- // SKIP chunks use XOR logic: skip when bits DIFFER.
- // - Both bits CLEAR (alive) -> same -> don't skip -> enemy visible
- // - Parent SET (dead), child SET (disabled) -> same -> skip -> enemy hidden
- // The bits are only modified when enemies are actually destroyed (setBit/clearBit
- // calls in processMouse() enemy destruction handling).
- debug("Rebel2: Linked ID=%d to Parent=%d (Slot 0)", childId, parentId);
+ // Mirror parent's bit state to child (INVERTED):
+ // - Parent alive (bit clear) â setBit(child) â child hidden
+ // - Parent dead (bit set) â clearBit(child) â child shown
+ // From FUN_0041CADB case 0, par3==4:
+ // bVar3 = FUN_00423970(parentId);
+ // if (bVar3 == 0) setBit(childId); else clearBit(childId);
+ // This ensures linked children (explosion/death sprites) are hidden
+ // while the parent is alive, and revealed when the parent is destroyed.
+ if (!isBitSet(parentId)) {
+ setBit(childId);
+ debug("Rebel2: Linked ID=%d to Parent=%d (Slot 0) - child DISABLED (parent alive)", childId, parentId);
+ } else {
+ clearBit(childId);
+ debug("Rebel2: Linked ID=%d to Parent=%d (Slot 0) - child ENABLED (parent dead)", childId, parentId);
+ }
} else {
debug("Rebel2: Skipping link with invalid IDs childId=%d parentId=%d", childId, parentId);
}
@@ -1209,15 +1220,22 @@ void InsaneRebel2::iactRebel2Opcode2(Common::SeekableReadStream &b, int16 par2,
if (targetId < 1 || targetId >= 0x200)
return;
- // Handler 8/25: FUN_401234 case 0 / FUN_41E7C2 par3==1, par4!=0
- // "If enemy type par4 has been killed (bit set in wave state), disable targetId"
- // This conditionally hides entities based on which enemy groups have been destroyed.
- // Both Handler 8 (on-foot) and Handler 25 (FPS/cover) use DAT_0047ab98 for wave state.
+ // Handler 8/25: FUN_401234 case 0 / FUN_0041CADB case 0 par3==1
+ // From original FUN_0041CADB:
+ // if (par4 == 100) clearBit(body0); // Force enable
+ // else { bitMask = 1 << (par4 & 0x1f); if (waveState & bitMask) setBit(body0); }
if ((_rebelHandler == 8 || _rebelHandler == 25) && value != 0) {
- int bitMask = 1 << (value & 0x1f);
- if ((_rebelWaveState & bitMask) != 0) {
- setBit(targetId);
- debug("Rebel2 Opcode2 (H%d): Disable target=%d (type %d killed, wave=0x%x)", _rebelHandler, targetId, value, _rebelWaveState);
+ if (value == 100) {
+ // par4==100: Force enable the target (original: FUN_00423a00)
+ clearBit(targetId);
+ debug("Rebel2 Opcode2 (H%d): Force ENABLE target=%d (par4=100)", _rebelHandler, targetId);
+ } else {
+ // Check wave state: if enemy type has been killed, disable target
+ int bitMask = 1 << (value & 0x1f);
+ if ((_rebelWaveState & bitMask) != 0) {
+ setBit(targetId);
+ debug("Rebel2 Opcode2 (H%d): Disable target=%d (type %d killed, wave=0x%x)", _rebelHandler, targetId, value, _rebelWaveState);
+ }
}
return;
}
diff --git a/engines/scumm/smush/smush_player.cpp b/engines/scumm/smush/smush_player.cpp
index 314c2748953..16edd4fadc7 100644
--- a/engines/scumm/smush/smush_player.cpp
+++ b/engines/scumm/smush/smush_player.cpp
@@ -338,6 +338,7 @@ void SmushPlayer::init(int32 speed) {
_frame = 0;
_speed = speed;
_endOfFile = false;
+ _storeFrame = false;
_vm->_smushVideoShouldFinish = false;
_vm->_smushActive = true;
@@ -438,18 +439,20 @@ void SmushPlayer::handleFetch(int32 subSize, Common::SeekableReadStream &b) {
debugC(DEBUG_SMUSH, "SmushPlayer::handleFetch()");
assert(subSize >= 6);
+ // Read FTCH data: 2 unknown + 2 X offset + 2 Y offset
+ // Original (FUN_00423A50 lines 91-103): reads X/Y from chunk data and
+ // re-renders stored FOBJ at position (X + param_3 + DAT_00482c1c, Y + param_4 + DAT_00482c20)
+ int16 ftchUnknown = b.readSint16LE();
+ int16 ftchX = b.readSint16LE();
+ int16 ftchY = b.readSint16LE();
+
+ debug("SmushPlayer::handleFetch: frame=%d unknown=%d x=%d y=%d",
+ _frame, ftchUnknown, ftchX, ftchY);
+
// For RA2 Handler 25, skip FTCH because the frame buffer only contains the
// par4=5 base background without the overlays (par4=4, 6, 7) that were drawn
// immediately in frame 0. Restoring would erase those overlays and make
// enemies invisible since they draw on top of the erased areas.
- //
- // Handler 25 (Level 2 speeder bike): Corridor overlay is drawn in procPreRendering
- // BEFORE FOBJs. FTCH would erase the overlay, making enemies draw on wrong background.
- //
- // Handler 8 (Level 2 on-foot): FTCH is NOT skipped here. FTCH restores the clean
- // background each frame, which properly erases old enemy sprite positions before
- // new FOBJs draw updated positions. procPreRendering also restores _level2Background,
- // but FTCH provides the authoritative clean slate from frame 0's stored background.
if (_vm->_game.id == GID_REBEL2 && _insane != nullptr) {
InsaneRebel2 *rebel2 = static_cast<InsaneRebel2 *>(_insane);
int handler = rebel2->getHandler();
Commit: 899667bc9541ebfa179253aa61ec8430f1ad5878
https://github.com/scummvm/scummvm/commit/899667bc9541ebfa179253aa61ec8430f1ad5878
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:08+02:00
Commit Message:
SCUMM: RA2: Clear IACT bits for level 2 waves
Changed paths:
engines/scumm/insane/insane_rebel.cpp
diff --git a/engines/scumm/insane/insane_rebel.cpp b/engines/scumm/insane/insane_rebel.cpp
index d4e9638ffce..bdb3c923125 100644
--- a/engines/scumm/insane/insane_rebel.cpp
+++ b/engines/scumm/insane/insane_rebel.cpp
@@ -1469,6 +1469,8 @@ void InsaneRebel2::iactRebel2Opcode6(byte *renderBitmap, Common::SeekableReadStr
// Reset state when shipLevelMode != 0 && par4 == 1 (FUN_401234 lines 97-103)
if (_shipLevelMode != 0 && par4 == 1) {
+ // Clear ALL iactBits â matches FUN_00423880 calling FUN_00423a00(0)
+ clearBit(0);
// Clear link tables
for (int i = 0; i < 512; i++) {
_rebelLinks[i][0] = 0;
@@ -1872,6 +1874,10 @@ void InsaneRebel2::iactRebel2Opcode6(byte *renderBitmap, Common::SeekableReadStr
// Note: This is local_14[4] in the decompiled code, NOT local_14[3] (par4)
if (par5 == 1) {
_rebelStatusBarSprite = 5;
+ // Clear ALL iactBits â matches FUN_00423880 calling FUN_00423a00(0)
+ // at IACT callback registration time. Each new wave video starts with
+ // a clean bit table so enemy IDs reused across videos work correctly.
+ clearBit(0);
// Reset link tables (DAT_0045797c through DAT_0045917c)
for (int i = 0; i < 512; i++) {
_rebelLinks[i][0] = 0;
@@ -1881,9 +1887,6 @@ void InsaneRebel2::iactRebel2Opcode6(byte *renderBitmap, Common::SeekableReadStr
// Reset wave state to accumulated phase state (same as Handler 8)
// DAT_0047ab98 = DAT_0047ab9c: ensures new wave starts with correct state
_rebelWaveState = _rebelPhaseState;
- // NOTE: autopilot and damageLevel are NOT reset here (verified against
- // FUN_41CADB case 4 lines 114-121). They persist across video boundaries
- // so the player keeps their cover state when transitioning between waves.
debug("Rebel2 Opcode 6 (Handler 25): Status bar enabled, state reset, wave=0x%x autopilot=%d damageLevel=%d",
_rebelWaveState, _rebelAutopilot, _rebelDamageLevel);
}
@@ -2078,8 +2081,10 @@ void InsaneRebel2::iactRebel2Opcode6(byte *renderBitmap, Common::SeekableReadStr
_rebelStatusBarSprite = (_rebelLevelType == 5) ? 53 : 5;
debug("Rebel2 Opcode 6: Status Bar ENABLED - sprite %d", _rebelStatusBarSprite);
+ // Clear ALL iactBits â matches FUN_00423880 calling FUN_00423a00(0)
+ clearBit(0);
+
// Clear link tables (DAT_0045797c through DAT_0045917c)
- // These are 4 tables of 0x400 (1024) shorts each
for (int i = 0; i < 512; i++) {
_rebelLinks[i][0] = 0;
_rebelLinks[i][1] = 0;
Commit: 6af2ff75d6c63cfee3aad70125122ae26202f584
https://github.com/scummvm/scummvm/commit/6af2ff75d6c63cfee3aad70125122ae26202f584
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:08+02:00
Commit Message:
SCUMM: RA2: Apply FOBJ offsets to enemy overlays
Changed paths:
engines/scumm/insane/insane_rebel.cpp
diff --git a/engines/scumm/insane/insane_rebel.cpp b/engines/scumm/insane/insane_rebel.cpp
index bdb3c923125..abcb7b2c2bc 100644
--- a/engines/scumm/insane/insane_rebel.cpp
+++ b/engines/scumm/insane/insane_rebel.cpp
@@ -5627,13 +5627,19 @@ void InsaneRebel2::renderEnemyOverlays(byte *renderBitmap, int pitch, int width,
if (_difficulty >= 2)
return;
+ // FOBJ sprites are rendered with _fobjOffsetX/Y applied (set from _rebelViewOffsetX/Y
+ // for Handler 25). Brackets must use the same offset so they align with the sprites.
+ int fobjOffX = _player ? _player->_fobjOffsetX : 0;
+ int fobjOffY = _player ? _player->_fobjOffsetY : 0;
+
Common::Rect viewRect(_viewX, _viewY, _viewX + videoWidth, _viewY + 200);
for (Common::List<enemy>::iterator it = _enemies.begin(); it != _enemies.end(); ++it) {
if (it->destroyed || !it->active || isBitSet(it->id))
continue;
- Common::Rect r = it->rect;
+ Common::Rect r(it->rect.left + fobjOffX, it->rect.top + fobjOffY,
+ it->rect.right + fobjOffX, it->rect.bottom + fobjOffY);
if (r.right <= viewRect.left || r.left >= viewRect.right ||
r.bottom <= viewRect.top || r.top >= viewRect.bottom)
continue;
Commit: 6da68ffd2bd2c947b51c109436393188658d4526
https://github.com/scummvm/scummvm/commit/6da68ffd2bd2c947b51c109436393188658d4526
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:09+02:00
Commit Message:
SCUMM: RA2: Implement additional level handlers
Changed paths:
engines/scumm/insane/insane_rebel.cpp
engines/scumm/insane/insane_rebel.h
diff --git a/engines/scumm/insane/insane_rebel.cpp b/engines/scumm/insane/insane_rebel.cpp
index abcb7b2c2bc..5b7f98334f1 100644
--- a/engines/scumm/insane/insane_rebel.cpp
+++ b/engines/scumm/insane/insane_rebel.cpp
@@ -8382,7 +8382,7 @@ void InsaneRebel2::playLevelDeath(int levelId) {
// Most levels have DIE_A, some have just DIE
Common::String filename;
- if (levelId == 2 || levelId == 4) {
+ if (levelId == 2 || levelId == 4 || levelId == 10 || levelId == 12 || levelId == 14) {
filename = Common::String::format("%s/%sDIE.SAN", dir.c_str(), prefix.c_str());
} else {
filename = Common::String::format("%s/%sDIE_A.SAN", dir.c_str(), prefix.c_str());
@@ -8495,8 +8495,25 @@ int InsaneRebel2::runLevel(int levelId) {
return runLevel5();
case 6:
return runLevel6();
+ case 7:
+ return runLevel7();
+ case 8:
+ return runLevel8();
+ case 9:
+ return runLevel9();
+ case 10:
+ return runLevel10();
+ case 11:
+ return runLevel11();
+ case 12:
+ return runLevel12();
+ case 13:
+ return runLevel13();
+ case 14:
+ return runLevel14();
+ case 15:
+ return runLevel15();
default:
- // Levels 7-15: Use generic handler (similar to Level 1)
return runLevel1();
}
}
@@ -8553,17 +8570,87 @@ Common::String InsaneRebel2::selectDeathVideoVariant(int levelId, int phase, int
return (getRandomVariant(2) == 0) ? "A" : "B";
case 6:
- // Level 6: Similar to Level 3 (two phases with frame-based selection)
+ // Level 6 (FUN_004190D6): Phase-based with detailed frame selection
if (phase == 1) {
- if (frame < 300) return "A";
- if (frame < 600) return "B";
- return "C";
+ // DAT_0047a7f8 == 5 (phase 1)
+ if (frame < 0x4e) return "D";
+ if (frame < 0xe0) return "A";
+ if (frame < 0x122) return "D";
+ if (frame < 0x1b4) return "B";
+ if (frame < 499) return "D";
+ if (frame < 0x286) return "C";
+ return "D";
} else {
- if (frame < 400) return "A";
- if (frame < 800) return "B";
- return "C";
+ // DAT_0047a7f8 == 6 (phase 2)
+ if (frame < 0xcc) return "E";
+ if (frame < 0xfe) return "G";
+ if (frame < 0x122) return "E";
+ if (frame < 0x149) return "G";
+ if (frame < 0x166) return "F";
+ if (frame < 0x174) return "E";
+ if (frame < 0x19f) return "F";
+ if (frame < 0x1b2) return "G";
+ if (frame < 0x1c8) return "F";
+ if (frame < 0x207) return "E";
+ if (frame < 0x217) return "F";
+ if (frame < 0x23b) return "G";
+ if (frame < 0x25b) return "F";
+ if (frame < 0x285) return "E";
+ return "G";
}
+ case 7:
+ // Level 7 (FUN_0041974C): Based on DAT_0047ab8c (fork state)
+ // DAT_0047ab8c != 0 â DIE_B; == 0 â DIE_A
+ // We use phase as a proxy (phase 2 = reached fork)
+ return (phase >= 2) ? "B" : "A";
+
+ case 8:
+ // Level 8 (FUN_00419976): Random A or B
+ return (getRandomVariant(2) == 0) ? "A" : "B";
+
+ case 9:
+ // Level 9 (FUN_00419B86): Based on DAT_0047ab94 (death cause)
+ // 0âA, 1âC, elseâB. Use phase as proxy.
+ return "A"; // Default; exact tracking of DAT_0047ab94 deferred
+
+ case 10:
+ // Level 10 (FUN_00419E0A): Single death video (no variant suffix)
+ return "";
+
+ case 11:
+ // Level 11 (FUN_0041A00C): Phase-based death videos
+ // Phase 1 â DIE_A, Phase 2 â DIE_B, Phase 3 â DIE_C
+ if (phase <= 1) return "A";
+ if (phase == 2) return "B";
+ return "C";
+
+ case 12:
+ // Level 12 (FUN_0041AA14): Single death video (no variants)
+ return "";
+
+ case 13:
+ // Level 13 (FUN_0041B3E1): Frame-based
+ if (frame < 0x1c2) return "A";
+ if (frame < 0x302) return "B";
+ if (frame < 0x4ec) return "C";
+ if (frame < 0x5b4) return "B";
+ return "D";
+
+ case 14:
+ // Level 14 (FUN_0041B6E8): Single death video (no variant suffix)
+ return "";
+
+ case 15:
+ // Level 15 (FUN_0041B8D7): Frame-based with many thresholds
+ if (frame < 0x21e) return "A";
+ if (frame < 0x2f9) return "B";
+ if (frame < 0x3e5) return "C";
+ if (frame < 0x4a0) return "B";
+ if (frame < 0x588) return "C";
+ if (frame < 0x65e) return "B";
+ return "D";
+
default:
return "A";
}
@@ -8605,8 +8692,8 @@ void InsaneRebel2::playLevelRetryVariant(int levelId, int phase) {
Common::String prefix = getLevelPrefix(levelId);
Common::String filename;
- if (levelId == 3 && phase == 2) {
- // Level 3 phase 2 has its own retry video: 03RETRYB.SAN
+ if ((levelId == 3 || levelId == 6) && phase == 2) {
+ // Level 3/6 phase 2 has its own retry video: xxRETRYB.SAN
filename = Common::String::format("%s/%sRETRYB.SAN", dir.c_str(), prefix.c_str());
} else {
// Standard retry video
@@ -9330,99 +9417,1275 @@ int InsaneRebel2::runLevel5() {
// =============================================================================
int InsaneRebel2::runLevel6() {
+ // FUN_004190d6 â Mos Eisley: two-phase on-foot (Handler 8)
+ // Phase 1 (levelId=5): 06PLAY1.SAN, mid-switch to 06PLAY1B.SAN at frame 0x2a8
+ // Phase 2 (levelId=6): 06PLAY2.SAN, play until near-end
+ // Original structure: outer do-while for phase 1 retries, inner while(true) for
+ // phase 2 retries + death handling. Phase 1 death breaks inner â RETRY at outer bottom.
+ // Phase 2 death â RETRYB â re-enters phase 2 within inner loop.
SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
- int phase1Score = 0;
+ int totalScore = 0;
// Play level beginning cinematic (06BEG.SAN)
+ // Original: FUN_004171c5(s_LEV06_06BEG_SAN, 0x20, 0xaf, 0xa0, 10, 5, 0x4b)
playLevelBegin(6);
if (_vm->shouldQuit()) return kLevelQuit;
- // ===== PHASE 1 retry loop =====
+ // FUN_00401000 + FUN_0041c7d0 + FUN_0040c040 â handler init done by IACT opcode 6
+
+ // Outer retry loop â restarts phase 1 on phase 1 death
+ while (!_vm->shouldQuit()) {
+ // FUN_00407d10 â reset shot/hit counters
+ clearBit(0);
+
+ // DAT_0047ab9c = 0xffffffff â init phase state
+ _rebelPhaseState = 0xffffffff;
+
+ // ===== PHASE 1 =====
+ _rebelLevelType = 5; // DAT_0047a7f8 = 5
+ _currentPhase = 1;
+
+ debug("Rebel2: Level 6 Phase 1");
+ splayer->setCurVideoFlags(0x28);
+ splayer->play("LEV06/06PLAY1.SAN", 12);
+ // TODO: Mid-level switch at frame 0x2a8 to 06PLAY1B.SAN (flags 0x468)
+ // + score checkpoint (FUN_00407f55) â needs per-frame callback
+ _deathFrame = splayer->_frame;
+
+ if (_vm->shouldQuit()) return kLevelQuit;
+
+ if (_playerShield <= 0) {
+ // Died in phase 1
+ debug("Rebel2: Level 6 Phase 1 death at frame %d", _deathFrame);
+ playLevelDeathVariant(6, 1, _deathFrame);
+ if (_vm->shouldQuit()) return kLevelQuit;
+
+ _playerLives--;
+ if (_playerLives <= 0) {
+ playLevelGameOver(6);
+ return kLevelGameOver;
+ }
+
+ // Phase 1 retry (06RETRY.SAN) â restart outer loop
+ playLevelRetryVariant(6, 1);
+ if (_vm->shouldQuit()) return kLevelQuit;
+ continue;
+ }
+
+ // Phase 1 survived â save score, play POST1
+ totalScore = _playerScore; // local_8 = DAT_0047ab84
+
+ _rebelHandler = 0;
+ _rebelStatusBarSprite = 0;
+ splayer->setCurVideoFlags(0x28);
+ splayer->play("LEV06/06POST1.SAN", 12);
+ if (_vm->shouldQuit()) return kLevelQuit;
+
+ // ===== PHASE 2 retry loop (inner while(true) in original) =====
+ while (!_vm->shouldQuit()) {
+ _rebelLevelType = 6; // DAT_0047a7f8 = 6
+ _currentPhase = 2;
+ clearBit(0); // FUN_00407d10
+
+ debug("Rebel2: Level 6 Phase 2");
+ splayer->setCurVideoFlags(0x28);
+ splayer->play("LEV06/06PLAY2.SAN", 12);
+ _deathFrame = splayer->_frame;
+
+ if (_vm->shouldQuit()) return kLevelQuit;
+
+ // Accumulate score: local_8 = DAT_0047ab84 + local_8
+ totalScore += _playerScore;
+
+ if (_playerShield > 0) {
+ // Level completed!
+ debug("Rebel2: Level 6 completed!");
+ playLevelEnd(6);
+ _levelUnlocked[6] = true;
+ return kLevelNextLevel;
+ }
+
+ // Died in phase 2
+ debug("Rebel2: Level 6 Phase 2 death at frame %d", _deathFrame);
+ playLevelDeathVariant(6, 2, _deathFrame);
+ if (_vm->shouldQuit()) return kLevelQuit;
+
+ _playerLives--;
+ if (_playerLives <= 0) {
+ playLevelGameOver(6);
+ return kLevelGameOver;
+ }
+
+ // Phase 2 retry (06RETRYB.SAN) â re-enter phase 2
+ playLevelRetryVariant(6, 2);
+ if (_vm->shouldQuit()) return kLevelQuit;
+ }
+
+ break; // Should only reach here on shouldQuit
+ }
+
+ return kLevelQuit;
+}
+
+// =============================================================================
+// Level 7 Handler - FUN_0041974C
+// "TIE Training" - Canyon flight with fork at frame 1592
+// Single gameplay phase (07PLAY.SAN), optional second segment (07PLAYB.SAN)
+// Death: DAT_0047ab8c-based (fork reached â DIE_B, not reached â DIE_A)
+// =============================================================================
+
+int InsaneRebel2::runLevel7() {
+ SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
+ bool reachedFork = false; // DAT_0047ab8c equivalent â tracks if 07PLAYB was played
+
+ // Play cutscene (07CUT.SAN)
+ playCinematic("LEV07/07CUT.SAN");
+ if (_vm->shouldQuit()) return kLevelQuit;
+
+ // Play level beginning cinematic (07BEG.SAN)
+ playLevelBegin(7);
+ if (_vm->shouldQuit()) return kLevelQuit;
+
+ // FUN_00401000 + FUN_0041c7d0 + FUN_00407d10
+ clearBit(0);
+
while (!_vm->shouldQuit()) {
_playerShield = 255;
_playerDamage = 0;
- _currentPhase = 1;
+ _deathFrame = 0;
+ reachedFork = false;
- // Reset bit table before gameplay starts
clearBit(0);
- // Play phase 1 gameplay (06PLAY1.SAN)
- debug("Rebel2: Level 6 Phase 1");
+ // Play gameplay (07PLAY.SAN)
+ // Original: FUN_0041f4d0("07PLAY.SAN", 0x28, -1, -1, 0)
+ // At frame 0x638 (1592), if DAT_0047ab8c != 0: play 07PLAYB.SAN (0x468)
+ // TODO: Mid-level fork at frame 1592 requires per-frame callback.
+ // For now, play the main video. The fork video (07PLAYB) would be triggered
+ // by IACT callbacks setting a state flag during gameplay.
splayer->setCurVideoFlags(0x28);
- splayer->play("LEV06/06PLAY1.SAN", 12);
+ splayer->play("LEV07/07PLAY.SAN", 12);
_deathFrame = splayer->_frame;
if (_vm->shouldQuit()) return kLevelQuit;
if (_playerShield > 0) {
- phase1Score = _playerScore;
- break;
+ debug("Rebel2: Level 7 completed!");
+ playLevelEnd(7);
+ _levelUnlocked[7] = true;
+ return kLevelNextLevel;
}
- // Died in phase 1
- debug("Rebel2: Level 6 Phase 1 death at frame %d", _deathFrame);
- playLevelDeathVariant(6, 1, _deathFrame);
+ // Death video: DIE_B if fork reached, DIE_A if not
+ // Original: s_LEV07_07DIE_B + ((DAT_0047ab8c != 0) - 1 & 0x14)
+ debug("Rebel2: Level 7 death at frame %d, fork=%d", _deathFrame, reachedFork);
+ if (reachedFork) {
+ playCinematic("LEV07/07DIE_B.SAN");
+ } else {
+ playCinematic("LEV07/07DIE_A.SAN");
+ }
if (_vm->shouldQuit()) return kLevelQuit;
_playerLives--;
if (_playerLives <= 0) {
- playLevelGameOver(6);
+ playLevelGameOver(7);
return kLevelGameOver;
}
- playLevelRetryVariant(6, 1);
+ playCinematic("LEV07/07RETRY.SAN");
if (_vm->shouldQuit()) return kLevelQuit;
}
+ return kLevelQuit;
+}
+
+// =============================================================================
+// Level 8 Handler - FUN_00419976
+// "Flight to Imdaar" - Y-Wing space battle (single phase)
+// No cutscene (starts with BEG). flags=0x08 for gameplay.
+// Death: random A or B
+// =============================================================================
+
+int InsaneRebel2::runLevel8() {
+ SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
+
+ // No cutscene â starts directly with BEG
+ // Original: FUN_004171c5("08BEG.SAN", 0x20, 0xb1, 0xa0, 10, 5, 0x4b)
+ playLevelBegin(8);
if (_vm->shouldQuit()) return kLevelQuit;
- // Post segment 1 (06POST1.SAN)
- // Original: FUN_00417168 adds | 8, so flags = 0x20 | 0x08 = 0x28
- // Reset handler to 0 so procPostRendering skips HUD/sprite drawing during cinematic
- _rebelHandler = 0;
- _rebelStatusBarSprite = 0;
- splayer->setCurVideoFlags(0x28);
- splayer->play("LEV06/06POST1.SAN", 12);
+ // FUN_00401000 + FUN_0041c7d0 + FUN_0040c040
+ clearBit(0);
+
+ while (!_vm->shouldQuit()) {
+ _playerShield = 255;
+ _playerDamage = 0;
+ _deathFrame = 0;
+
+ clearBit(0);
+
+ // Play gameplay (08PLAY.SAN)
+ // Original: FUN_0041f4d0("08PLAY.SAN", 8, -1, -1, 0) â note flags=0x08
+ splayer->setCurVideoFlags(0x08);
+ splayer->play("LEV08/08PLAY.SAN", 12);
+ _deathFrame = splayer->_frame;
+
+ if (_vm->shouldQuit()) return kLevelQuit;
+
+ if (_playerShield > 0) {
+ int accuracy = 0;
+ if (_rebelKillCounter > 0) {
+ accuracy = (_rebelKillCounter * 100) / (_rebelHitCounter + _rebelKillCounter);
+ }
+ debug("Rebel2: Level 8 completed! accuracy=%d%%", accuracy);
+ playLevelEnd(8);
+ _levelUnlocked[8] = true;
+ return kLevelNextLevel;
+ }
+
+ // Death: random A or B
+ // Original: random(2) â A or B via string pointer arithmetic
+ debug("Rebel2: Level 8 death at frame %d", _deathFrame);
+ playLevelDeathVariant(8, 1, _deathFrame);
+ if (_vm->shouldQuit()) return kLevelQuit;
+
+ _playerLives--;
+ if (_playerLives <= 0) {
+ playLevelGameOver(8);
+ return kLevelGameOver;
+ }
+
+ playCinematic("LEV08/08RETRY.SAN");
+ if (_vm->shouldQuit()) return kLevelQuit;
+ }
+
+ return kLevelQuit;
+}
+
+// =============================================================================
+// Level 9 Handler - FUN_00419B86
+// "The Mine Field" - Navigate through force fields (single phase)
+// No cutscene. Initial phaseState = 0xfffffffe (all bits set except bit 0).
+// Mid-events at frames 0x19f (415) and 0x352 (850): FUN_00407f55 (score checkpoint)
+// Death: DAT_0047ab94-based (0âA, 1âC, elseâB)
+// =============================================================================
+
+int InsaneRebel2::runLevel9() {
+ SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
+
+ // No cutscene â starts directly with BEG
+ // Original: FUN_004171c5("09BEG.SAN", 0x20, 0xb2, 0xa0, 10, 200, 0x10e)
+ playLevelBegin(9);
if (_vm->shouldQuit()) return kLevelQuit;
- // ===== PHASE 2 retry loop =====
- _currentPhase = 2;
+ // FUN_00401000 + FUN_0041c7d0 + FUN_0040c040
+ clearBit(0);
while (!_vm->shouldQuit()) {
_playerShield = 255;
_playerDamage = 0;
+ _deathFrame = 0;
- // Reset bit table before gameplay starts
clearBit(0);
- // Play phase 2 gameplay (06PLAY2.SAN)
- debug("Rebel2: Level 6 Phase 2");
+ // Original: DAT_0047ab9c = 0xfffffffe (initial phase state â all bits except 0)
+ _rebelPhaseState = 0xfffffffe;
+
+ // Play gameplay (09PLAY.SAN)
+ // Original: FUN_0041f4d0("09PLAY.SAN", 0x28, -1, -1, 0)
+ // Mid-events at frames 415 and 850: FUN_00407f55 (score save)
+ // These are handled implicitly â the IACT callbacks manage scoring.
splayer->setCurVideoFlags(0x28);
- splayer->play("LEV06/06PLAY2.SAN", 12);
+ splayer->play("LEV09/09PLAY.SAN", 12);
_deathFrame = splayer->_frame;
if (_vm->shouldQuit()) return kLevelQuit;
if (_playerShield > 0) {
- // Level completed!
- debug("Rebel2: Level 6 completed!");
- playLevelEnd(6);
- _levelUnlocked[6] = true; // Unlock level 7
+ int accuracy = 0;
+ if (_rebelKillCounter > 0) {
+ accuracy = (_rebelKillCounter * 100) / (_rebelHitCounter + _rebelKillCounter);
+ }
+ debug("Rebel2: Level 9 completed! accuracy=%d%%", accuracy);
+ playLevelEnd(9);
+ _levelUnlocked[9] = true;
+ return kLevelNextLevel;
+ }
+
+ // Death: based on DAT_0047ab94 (death cause tracking)
+ // Original: 0âDIE_A, 1âDIE_C, elseâDIE_B
+ debug("Rebel2: Level 9 death at frame %d", _deathFrame);
+ playLevelDeathVariant(9, 1, _deathFrame);
+ if (_vm->shouldQuit()) return kLevelQuit;
+
+ _playerLives--;
+ if (_playerLives <= 0) {
+ playLevelGameOver(9);
+ return kLevelGameOver;
+ }
+
+ playCinematic("LEV09/09RETRY.SAN");
+ if (_vm->shouldQuit()) return kLevelQuit;
+ }
+
+ return kLevelQuit;
+}
+
+// =============================================================================
+// Level 10 Handler - FUN_00419E0A
+// "Speeder Bikes" - Forest speeder chase (single phase)
+// Has cutscene. Single death video (10DIE.SAN, no variants).
+// Original plays DIE then RETRY in sequence (no separate check).
+// =============================================================================
+
+int InsaneRebel2::runLevel10() {
+ SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
+
+ // Play cutscene (10CUT.SAN)
+ playCinematic("LEV10/10CUT.SAN");
+ if (_vm->shouldQuit()) return kLevelQuit;
+
+ // Play level beginning cinematic (10BEG.SAN)
+ // Original: FUN_004171c5("10BEG.SAN", 0x20, 0xb3, 0xa0, 10, 2, 0x46)
+ playLevelBegin(10);
+ if (_vm->shouldQuit()) return kLevelQuit;
+
+ // FUN_00401000 + FUN_0041c7d0 + FUN_00407d10
+ clearBit(0);
+
+ while (!_vm->shouldQuit()) {
+ _playerShield = 255;
+ _playerDamage = 0;
+ _deathFrame = 0;
+
+ clearBit(0);
+
+ // Play gameplay (10PLAY.SAN)
+ // Original: FUN_0041f4d0("10PLAY.SAN", 0x28, -1, -1, 0)
+ splayer->setCurVideoFlags(0x28);
+ splayer->play("LEV10/10PLAY.SAN", 12);
+ _deathFrame = splayer->_frame;
+
+ if (_vm->shouldQuit()) return kLevelQuit;
+
+ if (_playerShield > 0) {
+ int accuracy = 0;
+ if (_rebelKillCounter > 0) {
+ accuracy = (_rebelKillCounter * 100) / (_rebelHitCounter + _rebelKillCounter);
+ }
+ debug("Rebel2: Level 10 completed! accuracy=%d%%", accuracy);
+ playLevelEnd(10);
+ _levelUnlocked[10] = true;
+ return kLevelNextLevel;
+ }
+
+ // Death + Retry: original plays both in sequence
+ // Original: lives--, if 0 break to game over, else DIE+RETRY
+ debug("Rebel2: Level 10 death at frame %d", _deathFrame);
+ _playerLives--;
+ if (_playerLives <= 0) {
+ playLevelGameOver(10);
+ return kLevelGameOver;
+ }
+
+ playCinematic("LEV10/10DIE.SAN");
+ if (_vm->shouldQuit()) return kLevelQuit;
+ playCinematic("LEV10/10RETRY.SAN");
+ if (_vm->shouldQuit()) return kLevelQuit;
+ }
+
+ return kLevelQuit;
+}
+
+// =============================================================================
+// Level 11 Handler - FUN_0041A00C
+// "Inside the Terror" - Three phases + bridge puzzle (Handler 8, on-foot)
+//
+// Phase 1: P1/11P01_X (A,B,C,D) - behind barrels, mask 0x0e
+// Phase 2: P2/11P02_X (A,B,C,D) - walls on right, mask 0x0e, flags=3
+// Phase 3 first half: P3/11P03_X (A-F) - bridge puzzle, mask 0x7e
+// Exit when (phaseState & 0x70) == 0x70
+// POST3/POST3B/POST3C bridge cinematics
+// Phase 3 second half: P3/11P03_X (G-L) - after bridge, mask 0x0e
+// =============================================================================
+
+int InsaneRebel2::runLevel11() {
+ SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
+ int totalKills = 0;
+ int totalMisses = 0;
+ int prevPhaseState = 0;
+
+ // Kill credit budget bases per phase (from level data table DAT_0047e0e8)
+ static const int16 kLevel11BudgetBase[4] = { 3, 3, 3, 3 };
+
+ // Play cutscene (11CUT.SAN)
+ playCinematic("LEV11/11CUT.SAN");
+ if (_vm->shouldQuit()) return kLevelQuit;
+
+ // Play level beginning cinematic (11BEG.SAN)
+ playLevelBegin(11);
+ if (_vm->shouldQuit()) return kLevelQuit;
+
+ // FUN_00401000 + FUN_00407d10 + FUN_0040c040: Reset game state
+ clearBit(0);
+
+ // Main gameplay retry loop (restarts from Phase 1 on death)
+ while (!_vm->shouldQuit()) {
+ _playerShield = 255;
+ _playerDamage = 0;
+ _currentPhase = 1;
+ totalKills = 0;
+ totalMisses = 0;
+ prevPhaseState = 0;
+
+ // Reset Handler 8 cover state
+ _rebelAutopilot = 0;
+ _rebelDamageLevel = 0;
+ _rebelControlMode = 0;
+
+ // FUN_0041c7d0: Reset per-attempt state
+ _enemies.clear();
+ for (int i = 0; i < 512; i++) {
+ _rebelLinks[i][0] = 0;
+ _rebelLinks[i][1] = 0;
+ _rebelLinks[i][2] = 0;
+ }
+
+ // ===== PHASE 1: P1/11P01_X.SAN =====
+ _rebelKillCounter = 0;
+ _rebelHitCounter = 0;
+ _rebelPhaseState = 0;
+ _rebelWaveState = 0;
+
+ int16 budget = kLevel11BudgetBase[0] + _vm->_rnd.getRandomNumber(2);
+
+ // Play A.SAN (background loader)
+ debug("Rebel2: Level 11 Phase 1 - playing 11P01_A.SAN budget=%d", budget);
+ splayer->setCurVideoFlags(0x28);
+ splayer->play("LEV11/P1/11P01_A.SAN", 12);
+ _deathFrame = splayer->_frame;
+
+ if (_vm->shouldQuit()) return kLevelQuit;
+
+ {
+ uint16 waveSelect = processWaveEnd(0x0e, &budget, 0, 0);
+
+ // Phase 1 wave loop: random(2) | (waveSelect & 8) â variants
+ // 0âD, 1âC, 8âB, 9âA
+ while (waveSelect != 0xFFFF) {
+ if (_vm->shouldQuit()) return kLevelQuit;
+
+ // Bonus sound check
+ if ((_rebelPhaseState & 0x10) != 0 && (prevPhaseState & 0x10) == 0) {
+ // FUN_00411931 bonus sound â not yet implemented
+ }
+ prevPhaseState = _rebelPhaseState;
+
+ int sel = _vm->_rnd.getRandomNumber(1) | (waveSelect & 8);
+ const char *filename;
+ switch (sel) {
+ case 1: filename = "LEV11/P1/11P01_C.SAN"; break;
+ case 8: filename = "LEV11/P1/11P01_B.SAN"; break;
+ case 9: filename = "LEV11/P1/11P01_A.SAN"; break;
+ default: filename = "LEV11/P1/11P01_D.SAN"; break; // sel == 0
+ }
+
+ debug("Rebel2: Level 11 Phase 1 wave - %s (state=0x%x sel=%d)", filename, _rebelPhaseState, sel);
+ splayer->setCurVideoFlags(0x428);
+ splayer->play(filename, 12);
+ _deathFrame = splayer->_frame;
+
+ waveSelect = processWaveEnd(0x0e, &budget, 0x14, 0);
+ }
+ }
+
+ if (_playerDamage >= 255) goto level11_death_phase1;
+ if (_vm->shouldQuit()) return kLevelQuit;
+
+ // Post segment 1 (11POST1.SAN)
+ _rebelHandler = 0;
+ _rebelStatusBarSprite = 0;
+ splayer->setCurVideoFlags(0x28);
+ splayer->play("LEV11/11POST1.SAN", 12);
+ if (_vm->shouldQuit()) return kLevelQuit;
+
+ totalKills += _rebelKillCounter;
+ totalMisses += _rebelHitCounter;
+
+ // ===== PHASE 2: P2/11P02_X.SAN =====
+ _currentPhase = 2;
+ _rebelKillCounter = 0;
+ _rebelHitCounter = 0;
+ _rebelPhaseState = 0;
+ _rebelWaveState = 0;
+ _enemies.clear();
+
+ budget = kLevel11BudgetBase[1] + _vm->_rnd.getRandomNumber(2);
+ _rebelHandler = 8;
+
+ // Play A.SAN (background loader)
+ debug("Rebel2: Level 11 Phase 2 - playing 11P02_A.SAN budget=%d", budget);
+ splayer->setCurVideoFlags(0x28);
+ splayer->play("LEV11/P2/11P02_A.SAN", 12);
+ _deathFrame = splayer->_frame;
+
+ if (_vm->shouldQuit()) return kLevelQuit;
+
+ {
+ // Phase 2: flags=3 (maxCredits=2, redistribution ON)
+ uint16 waveSelect = processWaveEnd(0x0e, &budget, 0, 3);
+
+ // Random(4) for variant selection: A, B, C, D
+ while (waveSelect != 0xFFFF) {
+ if (_vm->shouldQuit()) return kLevelQuit;
+
+ int variant = _vm->_rnd.getRandomNumber(3);
+ const char *filename;
+ switch (variant) {
+ case 0: filename = "LEV11/P2/11P02_A.SAN"; break;
+ case 1: filename = "LEV11/P2/11P02_B.SAN"; break;
+ case 2: filename = "LEV11/P2/11P02_C.SAN"; break;
+ default: filename = "LEV11/P2/11P02_D.SAN"; break;
+ }
+
+ debug("Rebel2: Level 11 Phase 2 wave - %s (state=0x%x)", filename, _rebelPhaseState);
+ splayer->setCurVideoFlags(0x428);
+ splayer->play(filename, 12);
+ _deathFrame = splayer->_frame;
+
+ waveSelect = processWaveEnd(0x0e, &budget, 0x14, 3);
+ }
+ }
+
+ if (_playerDamage >= 255) goto level11_death_phase2;
+ if (_vm->shouldQuit()) return kLevelQuit;
+
+ // Post segment 2 (11POST2.SAN)
+ _rebelHandler = 0;
+ _rebelStatusBarSprite = 0;
+ splayer->setCurVideoFlags(0x28);
+ splayer->play("LEV11/11POST2.SAN", 12);
+ if (_vm->shouldQuit()) return kLevelQuit;
+
+ totalKills += _rebelKillCounter;
+ totalMisses += _rebelHitCounter;
+
+ // ===== PHASE 3 FIRST HALF: P3/11P03_X (A-F) =====
+ // Bridge puzzle â exit when (phaseState & 0x70) == 0x70
+ _currentPhase = 3;
+ _rebelKillCounter = 0;
+ _rebelHitCounter = 0;
+ _rebelPhaseState = 0;
+ _rebelWaveState = 0;
+ _enemies.clear();
+ prevPhaseState = 0;
+
+ budget = kLevel11BudgetBase[2] + _vm->_rnd.getRandomNumber(2);
+ _rebelHandler = 8;
+
+ debug("Rebel2: Level 11 Phase 3 first half - playing 11P03_A.SAN budget=%d", budget);
+ splayer->setCurVideoFlags(0x28);
+ splayer->play("LEV11/P3/11P03_A.SAN", 12);
+ _deathFrame = splayer->_frame;
+
+ if (_vm->shouldQuit()) return kLevelQuit;
+
+ {
+ uint16 waveSelect = processWaveEnd(0x7e, &budget, 0, 0);
+ int local_8 = 0; // Tracks variant for randomization threshold
+
+ // Loop until (phaseState & 0x70) == 0x70 (bridge targets destroyed)
+ while (waveSelect != 0xFFFF && (_rebelPhaseState & 0x70) != 0x70) {
+ if (_vm->shouldQuit()) return kLevelQuit;
+
+ // Bonus sound: (phaseState & 0xe) == 0xe and previous wasn't
+ if ((_rebelPhaseState & 0x0e) == 0x0e && (prevPhaseState & 0x0e) != 0x0e) {
+ // FUN_00411931 bonus sound â not yet implemented
+ }
+ prevPhaseState = _rebelPhaseState;
+
+ // Randomization: wider range for first few waves
+ if (local_8 < 3) {
+ local_8 = _vm->_rnd.getRandomNumber(7); // 0-7
+ } else {
+ local_8 = _vm->_rnd.getRandomNumber(2); // 0-2
+ }
+
+ const char *filename;
+ switch (local_8) {
+ case 0: filename = "LEV11/P3/11P03_A.SAN"; break;
+ case 1: filename = "LEV11/P3/11P03_B.SAN"; break;
+ case 2: filename = "LEV11/P3/11P03_C.SAN"; break;
+ case 3: filename = "LEV11/P3/11P03_D.SAN"; break;
+ case 4: filename = "LEV11/P3/11P03_E.SAN"; break;
+ case 5: filename = "LEV11/P3/11P03_F.SAN"; break;
+ case 6: filename = "LEV11/P3/11P03_F.SAN"; break; // duplicate F
+ default: filename = "LEV11/P3/11P03_E.SAN"; break; // duplicate E
+ }
+
+ debug("Rebel2: Level 11 Phase 3a wave - %s (state=0x%x local_8=%d)", filename, _rebelPhaseState, local_8);
+ splayer->setCurVideoFlags(0x428);
+ splayer->play(filename, 12);
+ _deathFrame = splayer->_frame;
+
+ // Threshold only for higher variants (original: (2 < local_8) - 1 & 0x14)
+ int16 threshold = (local_8 > 2) ? 0x14 : 0;
+ waveSelect = processWaveEnd(0x7e, &budget, threshold, 0);
+ }
+ }
+
+ if (_playerDamage >= 255) goto level11_death_phase3;
+ if (_vm->shouldQuit()) return kLevelQuit;
+
+ // ===== PHASE 3 BRIDGE CINEMATICS =====
+ {
+ bool allBasicKilled = (_rebelPhaseState & 0x0e) >= 0x0e;
+ if (!allBasicKilled) {
+ // Normal bridge drop cinematic
+ playCinematic("LEV11/11POST3.SAN");
+ // Bonus checks (FUN_0042aa70) â deferred, play standard path
+ // Original checks 0x77 and 0x62 for special POST3C cinematic
+ } else {
+ // All enemy types killed â bridge dropped successfully
+ playCinematic("LEV11/11POST3B.SAN");
+ }
+ }
+
+ if (_vm->shouldQuit()) return kLevelQuit;
+
+ // ===== PHASE 3 SECOND HALF: P3/11P03_X (G-L) =====
+ // Reset shots/explosions (FUN_0041ca6a equivalent)
+ for (int i = 0; i < 5; i++) {
+ _explosions[i].active = false;
+ }
+ _enemies.clear();
+
+ // Preserve only bits 1-3 of phase state (original: DAT_0047ab9c &= 0xe)
+ _rebelPhaseState &= 0x0e;
+ _rebelWaveState &= 0x0e;
+
+ _rebelHandler = 8;
+
+ budget = kLevel11BudgetBase[3] + _vm->_rnd.getRandomNumber(2);
+
+ // Play G.SAN (background loader for second half)
+ debug("Rebel2: Level 11 Phase 3 second half - playing 11P03_G.SAN budget=%d", budget);
+ splayer->setCurVideoFlags(0x28);
+ splayer->play("LEV11/P3/11P03_G.SAN", 12);
+ _deathFrame = splayer->_frame;
+
+ if (_vm->shouldQuit()) return kLevelQuit;
+
+ // Only enter wave loop if not all basic types killed already
+ if ((_rebelPhaseState & 0x0e) < 0x0e) {
+ int local_8 = 0;
+ uint16 waveSelect = processWaveEnd(0x0e, &budget, 0, 0);
+
+ while (waveSelect != 0xFFFF) {
+ if (_vm->shouldQuit()) return kLevelQuit;
+
+ // Wider randomization for first few waves
+ if (local_8 < 4) {
+ local_8 = _vm->_rnd.getRandomNumber(8); // 0-8
+ } else {
+ local_8 = _vm->_rnd.getRandomNumber(2); // 0-2
+ }
+
+ const char *filename;
+ switch (local_8) {
+ case 0: filename = "LEV11/P3/11P03_G.SAN"; break;
+ case 1: filename = "LEV11/P3/11P03_H.SAN"; break;
+ case 2: filename = "LEV11/P3/11P03_I.SAN"; break;
+ case 3: filename = "LEV11/P3/11P03_G.SAN"; break; // G again
+ case 4: filename = "LEV11/P3/11P03_H.SAN"; break; // H again
+ case 5: filename = "LEV11/P3/11P03_I.SAN"; break; // I again
+ case 6: filename = "LEV11/P3/11P03_J.SAN"; break;
+ case 7: filename = "LEV11/P3/11P03_K.SAN"; break;
+ default: filename = "LEV11/P3/11P03_L.SAN"; break;
+ }
+
+ debug("Rebel2: Level 11 Phase 3b wave - %s (state=0x%x local_8=%d)", filename, _rebelPhaseState, local_8);
+ splayer->setCurVideoFlags(0x428);
+ splayer->play(filename, 12);
+ _deathFrame = splayer->_frame;
+
+ int16 threshold = (local_8 > 2) ? 0x14 : 0;
+ waveSelect = processWaveEnd(0x0e, &budget, threshold, 0);
+ }
+ }
+
+ totalKills += _rebelKillCounter;
+
+ if (_playerDamage >= 255) goto level11_death_phase3;
+ if (_vm->shouldQuit()) return kLevelQuit;
+
+ // ===== LEVEL COMPLETED =====
+ {
+ totalMisses += _rebelHitCounter;
+ int accuracy = 0;
+ int totalShots = totalKills + totalMisses;
+ if (totalKills > 0 && totalShots > 0) {
+ accuracy = (totalKills * 100) / totalShots;
+ }
+ debug("Rebel2: Level 11 completed! kills=%d misses=%d accuracy=%d%%",
+ totalKills, totalMisses, accuracy);
+ }
+
+ playLevelEnd(11);
+ _levelUnlocked[11] = true; // Unlock level 12
+ return kLevelNextLevel;
+
+ level11_death_phase1:
+ debug("Rebel2: Level 11 Phase 1 death");
+ playCinematic("LEV11/11DIE_A.SAN");
+ goto level11_retry;
+
+ level11_death_phase2:
+ debug("Rebel2: Level 11 Phase 2 death");
+ playCinematic("LEV11/11DIE_B.SAN");
+ goto level11_retry;
+
+ level11_death_phase3:
+ debug("Rebel2: Level 11 Phase 3 death");
+ playCinematic("LEV11/11DIE_C.SAN");
+ goto level11_retry;
+
+ level11_retry:
+ if (_vm->shouldQuit()) return kLevelQuit;
+ _playerLives--;
+ if (_playerLives <= 0) {
+ playLevelGameOver(11);
+ return kLevelGameOver;
+ }
+ playCinematic("LEV11/11RETRY.SAN");
+ _playerDamage = 0;
+ if (_vm->shouldQuit()) return kLevelQuit;
+ continue; // Restart from Phase 1
+ }
+
+ return kLevelQuit;
+}
+
+// =============================================================================
+// Level 12 Handler - FUN_0041AA14
+// "Sewers" - Four phases FPS corridor shooting (Handler 25)
+//
+// Each phase: init video (P05/P06/P07/P08) â first wave â wave loop
+// Phase 1: P1/12P01_X (A,B,C,D) mask=6
+// Phase 2: P2/12P02_X (A,B,C,D,E,F) mask=6
+// Phase 3: P3/12P03_X (A,B,C,D,F) mask=6
+// Phase 4: P4/12P04_X (A,B,C,D,E,F) mask=0xe
+// Closing: 12P09.SAN
+// =============================================================================
+
+int InsaneRebel2::runLevel12() {
+ SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
+
+ // Kill credit budget bases per phase
+ static const int16 kLevel12BudgetBase[4] = { 3, 4, 4, 4 };
+
+ // Play cutscene (12CUT.SAN)
+ playCinematic("LEV12/12CUT.SAN");
+ if (_vm->shouldQuit()) return kLevelQuit;
+
+ // Play level beginning cinematic (12BEG.SAN)
+ playLevelBegin(12);
+ if (_vm->shouldQuit()) return kLevelQuit;
+
+ // FUN_0041c7d0 + FUN_00407d10 + FUN_0040c040: Reset game state
+ clearBit(0);
+
+ // Main gameplay retry loop (restarts from Phase 1 on death)
+ while (!_vm->shouldQuit()) {
+ _playerShield = 255;
+ _playerDamage = 0;
+ _currentPhase = 1;
+
+ // Reset state
+ _rebelAutopilot = 0;
+ _rebelDamageLevel = 0;
+ _rebelControlMode = 0;
+
+ _enemies.clear();
+ for (int i = 0; i < 512; i++) {
+ _rebelLinks[i][0] = 0;
+ _rebelLinks[i][1] = 0;
+ _rebelLinks[i][2] = 0;
+ }
+
+ // ===== PHASE 1: 12P05 â P1/12P01_X =====
+ // FUN_00401000: Reset at top of each retry
+ _rebelKillCounter = 0;
+ _rebelHitCounter = 0;
+ _rebelPhaseState = 0;
+ _rebelWaveState = 0;
+
+ int16 budget = kLevel12BudgetBase[0] + _vm->_rnd.getRandomNumber(2);
+
+ // Initialization video (12P05.SAN)
+ debug("Rebel2: Level 12 Phase 1 - init 12P05.SAN budget=%d", budget);
+ splayer->setCurVideoFlags(0x28);
+ splayer->play("LEV12/12P05.SAN", 12);
+ if (_vm->shouldQuit()) return kLevelQuit;
+ processWaveEnd(1, &budget, 0, 0);
+
+ // First wave (P1/12P01_A.SAN)
+ splayer->setCurVideoFlags(0x428);
+ splayer->play("LEV12/P1/12P01_A.SAN", 12);
+ _deathFrame = splayer->_frame;
+ if (_vm->shouldQuit()) return kLevelQuit;
+
+ {
+ uint16 waveSelect = processWaveEnd(6, &budget, 0x14, 0);
+
+ // Wave loop: random(2) | (waveSelect & 2) â 0:C, 1:D, 2:A, 3:B
+ while (waveSelect != 0xFFFF) {
+ if (_vm->shouldQuit()) return kLevelQuit;
+
+ int sel = _vm->_rnd.getRandomNumber(1) | (waveSelect & 2);
+ const char *filename;
+ switch (sel) {
+ case 0: filename = "LEV12/P1/12P01_C.SAN"; break;
+ case 1: filename = "LEV12/P1/12P01_D.SAN"; break;
+ case 2: filename = "LEV12/P1/12P01_A.SAN"; break;
+ default: filename = "LEV12/P1/12P01_B.SAN"; break;
+ }
+
+ debug("Rebel2: Level 12 Phase 1 wave - %s (state=0x%x sel=%d)", filename, _rebelPhaseState, sel);
+ splayer->setCurVideoFlags(0x428);
+ splayer->play(filename, 12);
+ _deathFrame = splayer->_frame;
+
+ waveSelect = processWaveEnd(6, &budget, 0x14, 0);
+ }
+ }
+
+ if (_playerDamage >= 255) goto level12_death;
+ if (_vm->shouldQuit()) return kLevelQuit;
+
+ // ===== PHASE 2: 12P06 â P2/12P02_X =====
+ _currentPhase = 2;
+ _rebelPhaseState = 0;
+ _rebelWaveState = 0;
+
+ budget = kLevel12BudgetBase[1] + _vm->_rnd.getRandomNumber(3);
+
+ // Initialization video (12P06.SAN)
+ debug("Rebel2: Level 12 Phase 2 - init 12P06.SAN budget=%d", budget);
+ splayer->setCurVideoFlags(0x428);
+ splayer->play("LEV12/12P06.SAN", 12);
+ if (_vm->shouldQuit()) return kLevelQuit;
+ processWaveEnd(1, &budget, 0, 0);
+
+ // First wave (P2/12P02_A.SAN)
+ splayer->setCurVideoFlags(0x428);
+ splayer->play("LEV12/P2/12P02_A.SAN", 12);
+ _deathFrame = splayer->_frame;
+ if (_vm->shouldQuit()) return kLevelQuit;
+
+ {
+ uint16 waveSelect = processWaveEnd(6, &budget, 0x14, 0);
+
+ while (waveSelect != 0xFFFF) {
+ if (_vm->shouldQuit()) return kLevelQuit;
+
+ // Variant selection: (waveSelect & 2) controls which set
+ int local_8;
+ if ((waveSelect & 2) == 0) {
+ local_8 = _vm->_rnd.getRandomNumber(2) + 3; // 3, 4, or 5
+ } else {
+ local_8 = _vm->_rnd.getRandomNumber(2); // 0, 1, or 2
+ }
+
+ const char *filename;
+ switch (local_8) {
+ case 0: filename = "LEV12/P2/12P02_A.SAN"; break;
+ case 1: filename = "LEV12/P2/12P02_B.SAN"; break;
+ case 2: filename = "LEV12/P2/12P02_E.SAN"; break;
+ case 3: filename = "LEV12/P2/12P02_C.SAN"; break;
+ case 4: filename = "LEV12/P2/12P02_D.SAN"; break;
+ default: filename = "LEV12/P2/12P02_F.SAN"; break;
+ }
+
+ debug("Rebel2: Level 12 Phase 2 wave - %s (state=0x%x local_8=%d)", filename, _rebelPhaseState, local_8);
+ splayer->setCurVideoFlags(0x428);
+ splayer->play(filename, 12);
+ _deathFrame = splayer->_frame;
+
+ // Variants E(2) and F(5) reset threshold to 0
+ int16 threshold = (local_8 == 2 || local_8 == 5) ? 0 : 0x14;
+ waveSelect = processWaveEnd(6, &budget, threshold, 0);
+ }
+ }
+
+ if (_playerDamage >= 255) goto level12_death;
+ if (_vm->shouldQuit()) return kLevelQuit;
+
+ // ===== PHASE 3: 12P07 â P3/12P03_X =====
+ _currentPhase = 3;
+ _rebelPhaseState = 0;
+ _rebelWaveState = 0;
+
+ budget = kLevel12BudgetBase[2] + _vm->_rnd.getRandomNumber(3);
+
+ // Initialization video (12P07.SAN)
+ debug("Rebel2: Level 12 Phase 3 - init 12P07.SAN budget=%d", budget);
+ splayer->setCurVideoFlags(0x428);
+ splayer->play("LEV12/12P07.SAN", 12);
+ if (_vm->shouldQuit()) return kLevelQuit;
+ processWaveEnd(1, &budget, 0, 0);
+
+ // First wave (P3/12P03_A.SAN)
+ splayer->setCurVideoFlags(0x428);
+ splayer->play("LEV12/P3/12P03_A.SAN", 12);
+ _deathFrame = splayer->_frame;
+ if (_vm->shouldQuit()) return kLevelQuit;
+
+ {
+ int local_8 = 0;
+ uint16 waveSelect = processWaveEnd(6, &budget, 0x14, 0);
+
+ while (waveSelect != 0xFFFF) {
+ if (_vm->shouldQuit()) return kLevelQuit;
+
+ // Wider randomization for first few waves
+ if (local_8 < 4) {
+ local_8 = _vm->_rnd.getRandomNumber(5); // 0-5
+ } else {
+ local_8 = _vm->_rnd.getRandomNumber(3); // 0-3
+ }
+
+ const char *filename;
+ switch (local_8) {
+ case 0: filename = "LEV12/P3/12P03_C.SAN"; break;
+ case 1: filename = "LEV12/P3/12P03_D.SAN"; break;
+ case 2: filename = "LEV12/P3/12P03_A.SAN"; break;
+ case 3: filename = "LEV12/P3/12P03_B.SAN"; break;
+ case 4: filename = "LEV12/P3/12P03_F.SAN"; break;
+ default: filename = "LEV12/P3/12P03_F.SAN"; break; // duplicate F
+ }
+
+ debug("Rebel2: Level 12 Phase 3 wave - %s (state=0x%x local_8=%d)", filename, _rebelPhaseState, local_8);
+ splayer->setCurVideoFlags(0x428);
+ splayer->play(filename, 12);
+ _deathFrame = splayer->_frame;
+
+ waveSelect = processWaveEnd(6, &budget, 0x14, 0);
+ }
+ }
+
+ if (_playerDamage >= 255) goto level12_death;
+ if (_vm->shouldQuit()) return kLevelQuit;
+
+ // ===== PHASE 4: 12P08 â P4/12P04_X =====
+ _currentPhase = 4;
+ _rebelPhaseState = 0;
+ _rebelWaveState = 0;
+
+ budget = kLevel12BudgetBase[3] + _vm->_rnd.getRandomNumber(3);
+
+ // Initialization video (12P08.SAN)
+ debug("Rebel2: Level 12 Phase 4 - init 12P08.SAN budget=%d", budget);
+ splayer->setCurVideoFlags(0x428);
+ splayer->play("LEV12/12P08.SAN", 12);
+ if (_vm->shouldQuit()) return kLevelQuit;
+ processWaveEnd(1, &budget, 0, 0);
+
+ // First wave (P4/12P04_A.SAN)
+ splayer->setCurVideoFlags(0x428);
+ splayer->play("LEV12/P4/12P04_A.SAN", 12);
+ _deathFrame = splayer->_frame;
+ if (_vm->shouldQuit()) return kLevelQuit;
+
+ {
+ int local_8 = 0;
+ uint16 waveSelect = processWaveEnd(0x0e, &budget, 0x14, 0);
+
+ while (waveSelect != 0xFFFF) {
+ if (_vm->shouldQuit()) return kLevelQuit;
+
+ if (local_8 < 4) {
+ local_8 = _vm->_rnd.getRandomNumber(5); // 0-5
+ } else {
+ local_8 = _vm->_rnd.getRandomNumber(3); // 0-3
+ }
+
+ const char *filename;
+ switch (local_8) {
+ case 0: filename = "LEV12/P4/12P04_C.SAN"; break;
+ case 1: filename = "LEV12/P4/12P04_D.SAN"; break;
+ case 2: filename = "LEV12/P4/12P04_A.SAN"; break;
+ case 3: filename = "LEV12/P4/12P04_B.SAN"; break;
+ case 4: filename = "LEV12/P4/12P04_E.SAN"; break;
+ default: filename = "LEV12/P4/12P04_F.SAN"; break;
+ }
+
+ debug("Rebel2: Level 12 Phase 4 wave - %s (state=0x%x local_8=%d)", filename, _rebelPhaseState, local_8);
+ splayer->setCurVideoFlags(0x428);
+ splayer->play(filename, 12);
+ _deathFrame = splayer->_frame;
+
+ waveSelect = processWaveEnd(0x0e, &budget, 0x14, 0);
+ }
+ }
+
+ if (_playerDamage >= 255) goto level12_death;
+ if (_vm->shouldQuit()) return kLevelQuit;
+
+ // ===== CLOSING: 12P09.SAN =====
+ splayer->setCurVideoFlags(0x428);
+ splayer->play("LEV12/12P09.SAN", 12);
+ if (_vm->shouldQuit()) return kLevelQuit;
+ processWaveEnd(1, &budget, 0, 0);
+
+ // ===== LEVEL COMPLETED =====
+ {
+ int accuracy = 0;
+ if (_rebelKillCounter > 0) {
+ accuracy = (_rebelKillCounter * 100) / (_rebelHitCounter + _rebelKillCounter);
+ }
+ debug("Rebel2: Level 12 completed! kills=%d misses=%d accuracy=%d%%",
+ _rebelKillCounter, _rebelHitCounter, accuracy);
+ }
+
+ // Bonus checks: FUN_0042aa70(0x61), FUN_0042aa70(99), FUN_0042aa70(0x74)
+ // If all three bonuses found, play special ending (12END_Z.SAN)
+ // Deferred until bonus tracking is implemented
+
+ playLevelEnd(12);
+ _levelUnlocked[12] = true; // Unlock level 13
+ return kLevelNextLevel;
+
+ level12_death:
+ // Single death video for all phases
+ debug("Rebel2: Level 12 Phase %d death", _currentPhase);
+ playCinematic("LEV12/12DIE.SAN");
+
+ if (_vm->shouldQuit()) return kLevelQuit;
+ _playerLives--;
+ if (_playerLives <= 0) {
+ playLevelGameOver(12);
+ return kLevelGameOver;
+ }
+ playCinematic("LEV12/12RETRY.SAN");
+ _playerDamage = 0;
+ if (_vm->shouldQuit()) return kLevelQuit;
+ continue; // Restart from Phase 1
+ }
+
+ return kLevelQuit;
+}
+
+// =============================================================================
+// Level 13 Handler - FUN_0041B3E1
+// "Escaping the Star Destroyer" - Two-phase flight/escape
+// Phase A: 13PLAY_A.SAN (main flight), transitions to Phase B at maxFrame-10
+// Phase B: 13PLAY_B.SAN (reactor loop, flags 0x468) â plays until
+// (DAT_0047ab90 == 0 && DAT_0047ab7c == 0) meaning all targets destroyed.
+// Death: frame-based (A/B/C/B/D pattern)
+// =============================================================================
+
+int InsaneRebel2::runLevel13() {
+ SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
+
+ // No cutscene â starts directly with BEG
+ // Original: FUN_004171c5("13BEG.SAN", 0x20, 0xb6, 0xa0, 10, 2, 0x46)
+ playLevelBegin(13);
+ if (_vm->shouldQuit()) return kLevelQuit;
+
+ // FUN_00401000 + FUN_0041c7d0 + FUN_0040c040
+ clearBit(0);
+
+ while (!_vm->shouldQuit()) {
+ _playerShield = 255;
+ _playerDamage = 0;
+ _deathFrame = 0;
+
+ clearBit(0);
+
+ // Phase A: Main escape flight (13PLAY_A.SAN)
+ // Original: FUN_0041f4d0("13PLAY_A.SAN", 0x28, -1, -1, 0)
+ // First inner loop runs until frame reaches maxFrame-10
+ // Then Phase B (13PLAY_B.SAN, flags 0x468) plays at that exact frame
+ // The 0x468 flags indicate seamless mid-video transition.
+ splayer->setCurVideoFlags(0x28);
+ splayer->play("LEV13/13PLAY_A.SAN", 12);
+ _deathFrame = splayer->_frame;
+
+ if (_vm->shouldQuit()) return kLevelQuit;
+
+ // If alive after Phase A, play Phase B (reactor destruction loop)
+ // Original: at frame == maxFrame-10, play 13PLAY_B.SAN (0x468)
+ // Then loop while (DAT_0047ab90 != 0 || DAT_0047ab7c != 0)
+ // For now, play B as a sequential video. The IACT callbacks will manage
+ // the reactor target state through opcode interactions.
+ if (_playerShield > 0) {
+ splayer->setCurVideoFlags(0x468);
+ splayer->play("LEV13/13PLAY_B.SAN", 12);
+ _deathFrame = splayer->_frame;
+ }
+
+ if (_vm->shouldQuit()) return kLevelQuit;
+
+ if (_playerShield > 0) {
+ int accuracy = 0;
+ if (_rebelKillCounter > 0) {
+ accuracy = (_rebelKillCounter * 100) / (_rebelHitCounter + _rebelKillCounter);
+ }
+ debug("Rebel2: Level 13 completed! accuracy=%d%%", accuracy);
+ playLevelEnd(13);
+ _levelUnlocked[13] = true;
+ return kLevelNextLevel;
+ }
+
+ // Death: frame-based variant selection (FUN_0041B3E1 lines 47-61)
+ debug("Rebel2: Level 13 death at frame %d", _deathFrame);
+ playLevelDeathVariant(13, 1, _deathFrame);
+ if (_vm->shouldQuit()) return kLevelQuit;
+
+ _playerLives--;
+ if (_playerLives <= 0) {
+ playLevelGameOver(13);
+ return kLevelGameOver;
+ }
+
+ playCinematic("LEV13/13RETRY.SAN");
+ if (_vm->shouldQuit()) return kLevelQuit;
+ }
+
+ return kLevelQuit;
+}
+
+// =============================================================================
+// Level 14 Handler - FUN_0041B6E8
+// "TIE Attack" - Final space battle (single phase)
+// No cutscene. Single death video (14DIE.SAN, no variants).
+// =============================================================================
+
+int InsaneRebel2::runLevel14() {
+ SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
+
+ // No cutscene â starts directly with BEG
+ // Original: FUN_004171c5("14BEG.SAN", 0x20, 0xb7, 0xa0, 10, 2, 0x46)
+ playLevelBegin(14);
+ if (_vm->shouldQuit()) return kLevelQuit;
+
+ // FUN_00401000 + FUN_0041c7d0 + FUN_0040c040
+ clearBit(0);
+
+ while (!_vm->shouldQuit()) {
+ _playerShield = 255;
+ _playerDamage = 0;
+ _deathFrame = 0;
+
+ clearBit(0);
+
+ // Play gameplay (14PLAY.SAN)
+ // Original: FUN_0041f4d0("14PLAY.SAN", 0x28, -1, -1, 0)
+ splayer->setCurVideoFlags(0x28);
+ splayer->play("LEV14/14PLAY.SAN", 12);
+ _deathFrame = splayer->_frame;
+
+ if (_vm->shouldQuit()) return kLevelQuit;
+
+ if (_playerShield > 0) {
+ int accuracy = 0;
+ if (_rebelKillCounter > 0) {
+ accuracy = (_rebelKillCounter * 100) / (_rebelHitCounter + _rebelKillCounter);
+ }
+ debug("Rebel2: Level 14 completed! accuracy=%d%%", accuracy);
+ playLevelEnd(14);
+ _levelUnlocked[14] = true;
+ return kLevelNextLevel;
+ }
+
+ // Death: single video (14DIE.SAN)
+ debug("Rebel2: Level 14 death at frame %d", _deathFrame);
+ playCinematic("LEV14/14DIE.SAN");
+ if (_vm->shouldQuit()) return kLevelQuit;
+
+ _playerLives--;
+ if (_playerLives <= 0) {
+ playLevelGameOver(14);
+ return kLevelGameOver;
+ }
+
+ playCinematic("LEV14/14RETRY.SAN");
+ if (_vm->shouldQuit()) return kLevelQuit;
+ }
+
+ return kLevelQuit;
+}
+
+// =============================================================================
+// Level 15 Handler - FUN_0041B8D7
+// "Imdaar Alpha" - Final mission (single long phase with level ID switch)
+// Has cutscene. Mid-level: DAT_0047a7f8 changes from 0xf to 0x10 at frame 0x21e.
+// This represents a transition from the tunnel section to the core section.
+// Death: frame-based (A/B/C/B/C/B/D pattern with 7 thresholds)
+// On completion â FUN_0041BBE8 (credits/end game, not a playable level)
+// =============================================================================
+
+int InsaneRebel2::runLevel15() {
+ SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
+
+ // Play cutscene (15CUT.SAN)
+ playCinematic("LEV15/15CUT.SAN");
+ if (_vm->shouldQuit()) return kLevelQuit;
+
+ // Play level beginning cinematic (15BEG.SAN)
+ // Original: FUN_004171c5("15BEG.SAN", 0x20, 0xb8, 0xa0, 10, 2, 0x46)
+ playLevelBegin(15);
+ if (_vm->shouldQuit()) return kLevelQuit;
+
+ // FUN_00401000 + FUN_0041c7d0 + FUN_0040c040
+ clearBit(0);
+
+ while (!_vm->shouldQuit()) {
+ _playerShield = 255;
+ _playerDamage = 0;
+ _deathFrame = 0;
+
+ clearBit(0);
+
+ // Original: DAT_0047a7f8 = 0xf (level 15) before gameplay
+ // At frame 0x21e (542): DAT_0047a7f8 = 0x10 (switches to "level 16" internally)
+ // After gameplay: DAT_0047a7f8 = 0x10 (stays at 16)
+ // This level ID switch affects which difficulty data is used mid-level.
+ // The IACT callbacks handle gameplay regardless of this ID.
+
+ // Play gameplay (15PLAY.SAN)
+ // Original: FUN_0041f4d0("15PLAY.SAN", 0x28, -1, -1, 0)
+ splayer->setCurVideoFlags(0x28);
+ splayer->play("LEV15/15PLAY.SAN", 12);
+ _deathFrame = splayer->_frame;
+
+ if (_vm->shouldQuit()) return kLevelQuit;
+
+ if (_playerShield > 0) {
+ int accuracy = 0;
+ if (_rebelKillCounter > 0) {
+ accuracy = (_rebelKillCounter * 100) / (_rebelHitCounter + _rebelKillCounter);
+ }
+ debug("Rebel2: Level 15 completed! accuracy=%d%%", accuracy);
+ playLevelEnd(15);
+ _levelUnlocked[15] = true;
+ // Level 15 completion leads to credits (FUN_0041BBE8)
return kLevelNextLevel;
}
- // Died in phase 2
- debug("Rebel2: Level 6 Phase 2 death at frame %d", _deathFrame);
- playLevelDeathVariant(6, 2, _deathFrame);
+ // Death: frame-based variant selection (FUN_0041B8D7 lines 46-65)
+ debug("Rebel2: Level 15 death at frame %d", _deathFrame);
+ playLevelDeathVariant(15, 1, _deathFrame);
if (_vm->shouldQuit()) return kLevelQuit;
_playerLives--;
if (_playerLives <= 0) {
- playLevelGameOver(6);
+ playLevelGameOver(15);
return kLevelGameOver;
}
- playLevelRetryVariant(6, 2);
+ playCinematic("LEV15/15RETRY.SAN");
if (_vm->shouldQuit()) return kLevelQuit;
}
diff --git a/engines/scumm/insane/insane_rebel.h b/engines/scumm/insane/insane_rebel.h
index c91ba5ac25b..7cbedcecef2 100644
--- a/engines/scumm/insane/insane_rebel.h
+++ b/engines/scumm/insane/insane_rebel.h
@@ -241,7 +241,16 @@ public:
int runLevel3(); // FUN_0041885F - Two phases with per-phase retry
int runLevel4(); // Cutscene + single gameplay
int runLevel5(); // FUN_00418EC6 - Single gameplay phase
- int runLevel6(); // FUN_00419317 - Two phases with per-phase retry
+ int runLevel6(); // FUN_004190D6 - Two phases with mid-level video switch
+ int runLevel7(); // FUN_0041974C - TIE Training: single + fork at frame 1592
+ int runLevel8(); // FUN_00419976 - Flight to Imdaar: single phase space battle
+ int runLevel9(); // FUN_00419B86 - Mine Field: single phase with mid-events
+ int runLevel10(); // FUN_00419E0A - Speeder Bikes: single phase
+ int runLevel11(); // FUN_0041A00C - Inside the Terror: 3 phases + bridge (Handler 8)
+ int runLevel12(); // FUN_0041AA14 - Sewers: 4 phases FPS (Handler 25)
+ int runLevel13(); // FUN_0041B3E1 - Escaping Star Destroyer: two-phase AâB
+ int runLevel14(); // FUN_0041B6E8 - TIE Attack: single phase
+ int runLevel15(); // FUN_0041B8D7 - Imdaar Alpha: single + level ID switch
// Wave state management (FUN_00417b61)
// Waits for current video to finish, accumulates kill state, redistributes
Commit: 06cc4d01d428c9f2c9bd673c755ef95bf7eb3742
https://github.com/scummvm/scummvm/commit/06cc4d01d428c9f2c9bd673c755ef95bf7eb3742
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:09+02:00
Commit Message:
SCUMM: RA2: Improve menu video handling
Changed paths:
engines/scumm/insane/insane_rebel.cpp
engines/scumm/smush/smush_player.cpp
engines/scumm/smush/smush_player.h
diff --git a/engines/scumm/insane/insane_rebel.cpp b/engines/scumm/insane/insane_rebel.cpp
index 5b7f98334f1..ae4125e38bb 100644
--- a/engines/scumm/insane/insane_rebel.cpp
+++ b/engines/scumm/insane/insane_rebel.cpp
@@ -976,6 +976,25 @@ void InsaneRebel2::procPreRendering(byte *renderBitmap) {
// Handler 25: Corridor overlay and FOBJ position offsets are set during
// IACT opcode 6 processing (iactRebel2Opcode6), matching the original
// FUN_41CADB architecture. No corridor drawing needed here.
+
+ // Chapter selection: Set FOBJ offset to scroll preview thumbnails in O_LEVEL.SAN.
+ // Original (FUN_00415CF8): offsets start at (0,0) for the first display update,
+ // then FUN_00425170 sets them to (-90, chapter*-50+75) AFTER each frame.
+ // Frame 0 must use (0,0) so the 80x800 preview strip at X=320 renders off-screen
+ // and STOR captures it cleanly. Frames 1+ use the scroll offset so FTCH re-renders
+ // the strip at the correct preview position.
+ if (_gameState == kStateChapterSelect && _player) {
+ if (_player->_frame > 0) {
+ // Clear screen to black before FTCH re-renders the preview strip.
+ // Our FTCH only re-draws the preview area (80px wide at X=230);
+ // without clearing, old menu text and preview artifacts persist.
+ if (renderBitmap) {
+ memset(renderBitmap, 0, _vm->_screenWidth * _vm->_screenHeight);
+ }
+ _player->_fobjOffsetX = _previewOffsetX;
+ _player->_fobjOffsetY = _previewOffsetY;
+ }
+ }
}
void InsaneRebel2::procIACT(byte *renderBitmap, int32 codecparam, int32 setupsan12,
@@ -4619,16 +4638,9 @@ void InsaneRebel2::procPostRendering(byte *renderBitmap, int32 codecparam, int32
delete cursor;
CursorMan.showMouse(true);
- // Fill screen with BLACK background
- // The original game plays O_LEVEL.SAN (640x400) which has a dark/black background
- // with chapter preview thumbnails embedded. For 320x200 mode, we fill with black
- // to match the visual appearance and overlay the UI elements.
- // Note: O_MENU_X.SAN (320x200) doesn't contain chapter-specific preview images,
- // those are only in O_LEVEL.SAN (640x400). We use a styled placeholder instead.
- // From FUN_00415CF8 line 57: FUN_0041f4d0(s_OPEN_O_LEVEL_SAN_004806e0,8,0xffff,0xffff,0);
- for (int y = 0; y < height; y++) {
- memset(renderBitmap + y * pitch, 0, width);
- }
+ // O_LEVEL.SAN provides the background with chapter preview thumbnails.
+ // The FOBJ offset system (set in procPreRendering) scrolls the correct preview
+ // into the preview box area. No black fill needed â video frame shows through.
// Process chapter selection input - emulates FUN_00415CF8 input handling
int selection = processChapterSelectInput();
@@ -7362,19 +7374,36 @@ int InsaneRebel2::runChapterSelect() {
SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
- // Chapter selection background - emulates FUN_00415CF8 line 57
- // Original uses O_LEVEL.SAN (640x400). We use menu video for 320x200 mode.
+ // Initialize preview offset for initial selection
+ _previewOffsetX = -90;
+ _previewOffsetY = _chapterSelection * -50 + 75;
+
+ // Set iactBits for chapter unlock state (FUN_00415CF8 lines 79-86)
+ // Bits 16..1 correspond to chapters 0..15: set if unlocked, clear if locked.
+ // These control SKIP chunks in O_LEVEL.SAN for locked/unlocked preview variants.
+ for (int i = 0; i < 16; i++) {
+ if (_chapterUnlocked[i])
+ setBit(16 - i);
+ else
+ clearBit(16 - i);
+ }
+
+ // Chapter selection background - FUN_00415CF8 line 57:
+ // FUN_0041f4d0(s_OPEN_O_LEVEL_SAN, 8, 0xffff, 0xffff, 0)
+ // O_LEVEL.SAN contains chapter preview thumbnails at specific FOBJ positions.
+ // The FOBJ offset system scrolls the correct preview into the preview box area.
while (!_vm->shouldQuit()) {
_vm->_smushVideoShouldFinish = false;
- Common::String menuVideo = getRandomMenuVideo();
- debug("Rebel2: Playing chapter select background: %s", menuVideo.c_str());
+ debug("Rebel2: Playing chapter select background: OPEN/O_LEVEL.SAN");
- // Set video flags for menu mode
+ // Flags: 0x20 (overlay drawing). No 0x08 (preserve) â we want a black
+ // background. O_LEVEL.SAN has no full-screen background FOBJ; the visible
+ // screen area stays black, and preview thumbnails render at X=230 via offset.
splayer->setCurVideoFlags(0x20);
- // Play the menu video as chapter selection background
- splayer->play(menuVideo.c_str(), 12);
+ // Play O_LEVEL.SAN â preview thumbnails are rendered by FOBJ offset
+ splayer->play("OPEN/O_LEVEL.SAN", 12);
if (_vm->shouldQuit()) {
_menuInputActive = false;
@@ -7439,7 +7468,9 @@ int InsaneRebel2::processChapterSelectInput() {
if (_chapterSelection < 0) {
_chapterSelection = _chapterItemCount - 1;
}
- debug("ChapterSelect: Selection changed to %d (UP)", _chapterSelection);
+ // Update preview offset (FUN_00425170: Y = selected * -50 + 75)
+ _previewOffsetY = _chapterSelection * -50 + 75;
+ debug("ChapterSelect: Selection changed to %d (UP) offsetY=%d", _chapterSelection, _previewOffsetY);
break;
case Common::KEYCODE_DOWN:
@@ -7448,7 +7479,9 @@ int InsaneRebel2::processChapterSelectInput() {
if (_chapterSelection >= _chapterItemCount) {
_chapterSelection = 0;
}
- debug("ChapterSelect: Selection changed to %d (DOWN)", _chapterSelection);
+ // Update preview offset (FUN_00425170: Y = selected * -50 + 75)
+ _previewOffsetY = _chapterSelection * -50 + 75;
+ debug("ChapterSelect: Selection changed to %d (DOWN) offsetY=%d", _chapterSelection, _previewOffsetY);
break;
case Common::KEYCODE_RETURN:
@@ -7486,19 +7519,28 @@ int InsaneRebel2::processChapterSelectInput() {
break;
case Common::EVENT_LBUTTONDOWN:
+ // Click confirms the current selection (original: DAT_0047a7e4 & 1)
+ if (_chapterSelection >= 0 && _chapterSelection < _chapterItemCount) {
+ result = _chapterSelection;
+ debug("ChapterSelect: Item %d confirmed (CLICK)", _chapterSelection);
+ }
+ break;
+
+ case Common::EVENT_MOUSEMOVE:
{
- // Mouse click - check if clicking on a menu item
- // From FUN_0041F5AE (low-res, left-aligned):
- // Item Y = 17 * -5 + i * 10 + 104 = 19 + i * 10
- int baseY = _chapterItemCount * -5 + 0x68; // = 19
+ // Mouse hover changes highlight (original FUN_0041f5ae mouse mode).
+ // Item Y = numItems * -5 + i * 10 + 0x68
+ int baseY = _chapterItemCount * -5 + 0x68;
int mouseY = event.mouse.y;
for (int i = 0; i < _chapterItemCount; i++) {
int itemY = baseY + i * 10;
if (mouseY >= itemY - 4 && mouseY < itemY + 6) {
- _chapterSelection = i;
- result = i;
- debug("ChapterSelect: Item %d clicked at Y=%d", i, mouseY);
+ if (i != _chapterSelection) {
+ _chapterSelection = i;
+ _previewOffsetY = _chapterSelection * -50 + 75;
+ debug(5, "ChapterSelect: Hover changed to %d", _chapterSelection);
+ }
break;
}
}
@@ -7741,11 +7783,15 @@ void InsaneRebel2::drawChapterInfoLine(byte *renderBitmap, int pitch, int width,
void InsaneRebel2::drawChapterSelectOverlay(byte *renderBitmap, int pitch, int width, int height) {
// Emulates FUN_00415CF8 rendering via shared drawMenuItems(leftAligned=true)
//
- // Menu structure (18 entries, 17 selectable):
- // items[0] = title = TRS 60 (locked chapter 0 header, contains ^f02 for TITLFONT)
- // items[1..16] = chapters 1-16 = TRS 41-56 (unlocked) or TRS 61-76 (locked)
- // items[17] = "RETURN TO PILOTS" = TRS 77
+ // GAME.TRS chapter selection strings:
+ // TRS 40 = "^f02Chapters" (title)
+ // TRS 41-56 = unlocked chapter names (e.g. "^f01^c244Chapter 1 - The Dreighton Triangle")
+ // TRS 57 = "^f01^c240RETURN TO PILOTS"
+ // TRS 60 = "^f02Chapters" (title, locked section duplicate)
+ // TRS 61-76 = locked chapter names (e.g. "^f01^c244Chapter 1 -")
+ // TRS 77 = "^f01^c240RETURN TO PILOTS" (locked section duplicate)
//
+ // Menu array: items[0]=title, items[1..16]=chapters, items[17]=RETURN TO PILOTS
// FUN_0041f5ae(0, &DAT_004577a8, 0x11, 1): param_3=17, param_4=1 (left-aligned)
SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
@@ -7754,9 +7800,9 @@ void InsaneRebel2::drawChapterSelectOverlay(byte *renderBitmap, int pitch, int w
// Build items array matching original DAT_004577a8 layout
const char *items[18];
- // items[0] = title (always locked version = TRS 60, contains "^f02CHAPTERS")
- items[0] = splayer->getString(60);
- if (!items[0] || !items[0][0]) items[0] = "^f02CHAPTERS";
+ // items[0] = title = TRS 40 ("^f02Chapters")
+ items[0] = splayer->getString(40);
+ if (!items[0] || !items[0][0]) items[0] = "^f02Chapters";
// items[1..16] = chapters, using unlocked (TRS 41-56) or locked (TRS 61-76) strings
for (int i = 1; i <= 16; i++) {
@@ -7766,21 +7812,17 @@ void InsaneRebel2::drawChapterSelectOverlay(byte *renderBitmap, int pitch, int w
if (!items[i] || !items[i][0]) items[i] = "";
}
- // items[17] = "RETURN TO PILOTS" (always locked version = TRS 77)
- items[17] = splayer->getString(77);
- if (!items[17] || !items[17][0]) items[17] = "RETURN TO PILOTS";
+ // items[17] = "RETURN TO PILOTS" = TRS 57 ("^f01^c240RETURN TO PILOTS")
+ items[17] = splayer->getString(57);
+ if (!items[17] || !items[17][0]) items[17] = "^f01^c240RETURN TO PILOTS";
// Render menu using shared renderer with left-aligned mode
drawMenuItems(renderBitmap, pitch, width, height, items, 17, _chapterSelection, true);
- // Draw preview box on the right side (FUN_004292d0 calls at lines 128-133)
+ // Draw preview box border on the right side (FUN_004292d0 calls at lines 128-133)
+ // The actual preview image is rendered by O_LEVEL.SAN FOBJ via the offset system
drawPreviewBox(renderBitmap, pitch, width, height);
- // Draw preview thumbnail for chapters 0-15 (not RETURN TO PILOTS)
- if (_chapterSelection >= 0 && _chapterSelection < 16) {
- drawPreviewThumbnail(renderBitmap, pitch, width, height, _chapterSelection);
- }
-
// Draw score/info line at bottom
drawChapterInfoLine(renderBitmap, pitch, width, height);
}
diff --git a/engines/scumm/smush/smush_player.cpp b/engines/scumm/smush/smush_player.cpp
index 16edd4fadc7..9b172697af6 100644
--- a/engines/scumm/smush/smush_player.cpp
+++ b/engines/scumm/smush/smush_player.cpp
@@ -257,6 +257,13 @@ SmushPlayer::SmushPlayer(ScummEngine_v7 *scumm, IMuseDigital *imuseDigital, Insa
_frameBuffer = nullptr;
_specialBuffer = nullptr;
_specialBufferSize = 0;
+ _storedFobjData = nullptr;
+ _storedFobjDataSize = 0;
+ _storedFobjCodec = 0;
+ _storedFobjLeft = 0;
+ _storedFobjTop = 0;
+ _storedFobjWidth = 0;
+ _storedFobjHeight = 0;
_seekPos = -1;
@@ -328,6 +335,8 @@ SmushPlayer::~SmushPlayer() {
_frameBuffer = nullptr;
free(_specialBuffer);
_specialBuffer = nullptr;
+ free(_storedFobjData);
+ _storedFobjData = nullptr;
free(_loadBuffer);
_loadBuffer = nullptr;
}
@@ -340,12 +349,28 @@ void SmushPlayer::init(int32 speed) {
_endOfFile = false;
_storeFrame = false;
+ // Reset FOBJ offsets between videos. These are set per-video by
+ // procPreRendering (chapter select preview scrolling) or IACT opcode 6
+ // (corridor overlay positioning). Without this, offsets from O_LEVEL.SAN
+ // would persist and shift FOBJs in subsequent videos â buffer overflows.
+ _fobjOffsetX = 0;
+ _fobjOffsetY = 0;
+
_vm->_smushVideoShouldFinish = false;
_vm->_smushActive = true;
_vm->setDirtyColors(0, 255);
_dst = vs->getPixels(0, 0);
+ // For RA2: Re-push the SMUSH palette to the system. Videos like O_LEVEL.SAN
+ // have no NPAL chunk and inherit the palette from the previous video. Since
+ // play() resets _palDirtyMin/Max, the palette would never be pushed otherwise.
+ // This is safe for videos WITH NPAL too â the NPAL handler immediately
+ // overwrites _pal and re-marks dirty.
+ if (_vm->_game.id == GID_REBEL2) {
+ setDirtyColors(0, 255);
+ }
+
// For Rebel Assault 2, handle background preservation between videos:
// - Cinematic videos (flags 0x20) clear the buffer for a fresh start
// - Gameplay videos (flags 0x28) preserve the existing screen content
@@ -404,6 +429,10 @@ void SmushPlayer::release() {
free(_specialBuffer);
_specialBuffer = nullptr;
+ free(_storedFobjData);
+ _storedFobjData = nullptr;
+ _storedFobjDataSize = 0;
+
// For Rebel Assault 2, preserve _frameBuffer across videos so that
// gameplay videos (which have no background FOBJ) can use the stored
// background from the previous BEG cinematic video.
@@ -462,6 +491,17 @@ void SmushPlayer::handleFetch(int32 subSize, Common::SeekableReadStream &b) {
}
}
+ // RA2: Re-decode stored FOBJ data with current offsets (matching original FUN_004246d0).
+ // The stored FOBJ's original position is combined with the current _fobjOffsetX/Y,
+ // so scrolling the chapter preview works correctly on each FTCH.
+ if (_vm->_game.id == GID_REBEL2 && _storedFobjData != nullptr) {
+ decodeFrameObject(_storedFobjCodec, _storedFobjData,
+ _storedFobjLeft, _storedFobjTop,
+ _storedFobjWidth, _storedFobjHeight,
+ _storedFobjDataSize);
+ return;
+ }
+
if (_frameBuffer != nullptr) {
memcpy(_dst, _frameBuffer, _width * _height);
}
@@ -512,9 +552,9 @@ void SmushPlayer::handleLoad(int32 subSize, Common::SeekableReadStream &b) {
_totalLoadChunks = totalChunks;
// Allocate/reallocate buffer if needed
- // Estimate buffer size: typical LOAD data per chunk is ~350 bytes
- // Total expected: totalChunks * 400 bytes (with margin)
- int32 estimatedSize = totalChunks * 400;
+ // LOAD data per chunk varies (up to ~500 bytes observed).
+ // Allocate generously to avoid overflow.
+ int32 estimatedSize = totalChunks * 600;
if (_loadBuffer == nullptr || _loadBufferSize < estimatedSize) {
free(_loadBuffer);
_loadBufferSize = estimatedSize;
@@ -1160,7 +1200,9 @@ void SmushPlayer::decodeFrameObject(int codec, const uint8 *src, int left, int t
error("Invalid codec for frame object : %d", codec);
}
- if (_storeFrame) {
+ // For non-RA2 games, save rendered bitmap when STOR is pending.
+ // RA2 handles STOR in handleFrameObject by saving raw FOBJ data instead.
+ if (_storeFrame && _vm->_game.id != GID_REBEL2) {
if (_frameBuffer == nullptr) {
_frameBuffer = (byte *)malloc(_width * _height);
}
@@ -1224,6 +1266,24 @@ void SmushPlayer::handleFrameObject(int32 subSize, Common::SeekableReadStream &b
assert(chunk_buffer);
b.read(chunk_buffer, chunk_size);
+ // RA2: When STOR is pending, save raw FOBJ data for later re-decoding by FTCH.
+ // The original (FUN_00423A50 bVar5) stores the next FOBJ's raw chunk data in
+ // DAT_00482c04. FTCH then re-renders from this stored data with current FOBJ
+ // offsets. This is critical for O_LEVEL.SAN where the stored FOBJ is the 80x800
+ // preview strip, and FTCH must re-render it at the current scroll offset.
+ if (_storeFrame && _vm->_game.id == GID_REBEL2) {
+ free(_storedFobjData);
+ _storedFobjData = (byte *)malloc(chunk_size);
+ memcpy(_storedFobjData, chunk_buffer, chunk_size);
+ _storedFobjDataSize = chunk_size;
+ _storedFobjCodec = codec;
+ _storedFobjLeft = left;
+ _storedFobjTop = top;
+ _storedFobjWidth = width;
+ _storedFobjHeight = height;
+ _storeFrame = false;
+ }
+
decodeFrameObject(codec, chunk_buffer, left, top, width, height, chunk_size);
free(chunk_buffer);
@@ -1310,6 +1370,13 @@ void SmushPlayer::handleFrame(int32 frameSize, Common::SeekableReadStream &b) {
case MKTAG('L','O','A','D'):
handleLoad(subSize, b);
break;
+ case MKTAG('G','O','S','T'):
+ // GOST = ghost sprite overlay. Re-renders previous FOBJ data at a new
+ // position with priority/transparency flags. Data: 2-byte priority type
+ // (0/1/2 â 0x2000/0x4000/0x6000), 2-byte x, 2-byte y.
+ // TODO: Implement proper ghost rendering by saving previous FOBJ data
+ // and re-rendering it with modified coordinates and priority flags.
+ break;
default:
error("Unknown frame subChunk found : %s, %d", tag2str(subType), subSize);
}
diff --git a/engines/scumm/smush/smush_player.h b/engines/scumm/smush/smush_player.h
index 43ecc269d9b..2ebd816193b 100644
--- a/engines/scumm/smush/smush_player.h
+++ b/engines/scumm/smush/smush_player.h
@@ -152,6 +152,19 @@ private:
byte *_specialBuffer;
int _specialBufferSize;
+ // RA2: Raw FOBJ data stored by STOR chunk (matching original DAT_00482c04).
+ // The original stores raw FOBJ chunk data and re-decodes it on FTCH with
+ // current FOBJ offsets. This is essential for O_LEVEL.SAN where the stored
+ // FOBJ is the 80x800 preview strip at X=320, and FTCH must re-render it
+ // at the current scroll offset each frame.
+ byte *_storedFobjData;
+ int32 _storedFobjDataSize;
+ int _storedFobjCodec;
+ int _storedFobjLeft;
+ int _storedFobjTop;
+ int _storedFobjWidth;
+ int _storedFobjHeight;
+
Common::String _seekFile;
uint32 _startFrame;
uint32 _startTime;
Commit: c9a07cc6a1dffeb4b28cec89fb16b7465fcb77b0
https://github.com/scummvm/scummvm/commit/c9a07cc6a1dffeb4b28cec89fb16b7465fcb77b0
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:09+02:00
Commit Message:
SCUMM: RA2: Clean up menu handling
Changed paths:
engines/scumm/insane/insane_rebel.cpp
diff --git a/engines/scumm/insane/insane_rebel.cpp b/engines/scumm/insane/insane_rebel.cpp
index ae4125e38bb..c4e5e1442b6 100644
--- a/engines/scumm/insane/insane_rebel.cpp
+++ b/engines/scumm/insane/insane_rebel.cpp
@@ -7428,19 +7428,12 @@ int InsaneRebel2::runChapterSelect() {
}
if (_chapterSelection >= 0 && _chapterSelection < 16) {
- // Chapter selected
- if (_chapterUnlocked[_chapterSelection]) {
- // Chapter is unlocked - start it
- _selectedChapter = _chapterSelection;
- debug("Rebel2: Chapter %d selected (unlocked)", _selectedChapter + 1);
- _menuInputActive = false;
- return kChapterSelectPlay;
- } else {
- // Chapter is locked - check password (lines 239-257 of FUN_00415CF8)
- // For now, just play error sound and continue
- debug("Rebel2: Chapter %d is locked", _chapterSelection + 1);
- continue;
- }
+ // Chapter selected - start it regardless of unlock state.
+ // TODO: locked chapters should require password (FUN_00415CF8 lines 239-257)
+ _selectedChapter = _chapterSelection;
+ debug("Rebel2: Chapter %d selected", _selectedChapter + 1);
+ _menuInputActive = false;
+ return kChapterSelectPlay;
}
}
Commit: 20b45a1ce0ac4c573cd7606b2e83347b85294fe9
https://github.com/scummvm/scummvm/commit/20b45a1ce0ac4c573cd7606b2e83347b85294fe9
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:09+02:00
Commit Message:
SCUMM: RA2: Split implementation into subsystem files
Changed paths:
A engines/scumm/insane/insane_rebel_audio.cpp
A engines/scumm/insane/insane_rebel_iact.cpp
A engines/scumm/insane/insane_rebel_levels.cpp
A engines/scumm/insane/insane_rebel_menu.cpp
A engines/scumm/insane/insane_rebel_render.cpp
A engines/scumm/insane/insane_rebel_runlevels.cpp
engines/scumm/insane/insane_rebel.cpp
engines/scumm/module.mk
diff --git a/engines/scumm/insane/insane_rebel.cpp b/engines/scumm/insane/insane_rebel.cpp
index c4e5e1442b6..5dacef70047 100644
--- a/engines/scumm/insane/insane_rebel.cpp
+++ b/engines/scumm/insane/insane_rebel.cpp
@@ -23,13 +23,9 @@
#include "engines/engine.h"
#include "common/system.h"
-#include "common/memstream.h"
#include "common/events.h"
#include "common/util.h"
-#include "graphics/cursorman.h"
-#include "graphics/wincursor.h"
-
#include "scumm/actor.h"
#include "scumm/file.h"
#include "scumm/resource.h"
@@ -49,10 +45,6 @@
namespace Scumm {
-// External codec functions from codec1.cpp
-extern void smushDecodeRLE(byte *dst, const byte *src, int left, int top, int width, int height, int pitch);
-extern void smushDecodeRLEOpaque(byte *dst, const byte *src, int left, int top, int width, int height, int pitch);
-extern void smushDecodeUncompressed(byte *dst, const byte *src, int left, int top, int width, int height, int pitch);
InsaneRebel2::InsaneRebel2(ScummEngine_v7 *scumm) {
_vm = scumm;
@@ -949,9782 +941,5 @@ void InsaneRebel2::procSKIP(int32 subSize, Common::SeekableReadStream &b) {
}
}
-void InsaneRebel2::procPreRendering(byte *renderBitmap) {
- // Call base class implementation first (handles Full Throttle state machine)
- Insane::procPreRendering(renderBitmap);
-
- // For Level 2 gameplay (Handler 8 only), restore the background BEFORE FOBJ decoding.
- // The tiny FOBJ sprites (7x10, 9x38 pixels) only draw new sprite positions but don't
- // clear old ones. By restoring the full background each frame, we ensure old sprite
- // positions are erased before new ones are drawn.
- //
- // This is called at the start of handleFrame(), before any FOBJ chunks are processed.
- if (_rebelHandler == 8 && _level2BackgroundLoaded && _level2Background && renderBitmap) {
- for (int y = 0; y < 200; y++) {
- memcpy(renderBitmap + y * 320, _level2Background + y * 320, 320);
- }
- }
-
- // For Handler 25 (Level 2 speeder bike), draw the corridor overlay BEFORE FOBJ decoding.
- // The corridor overlay (par3=4 -> _rebelEmbeddedHud[4]) is DAT_00482268, a 350x230 buffer.
- // From FUN_0041cadb line 216: FUN_00428a10(param_1,0,DAT_0045790c,DAT_0045790e,DAT_00482268)
- // It's drawn at (DAT_0045790c, DAT_0045790e) which are _rebelViewOffsetX/Y.
- //
- // For Mode 1: DAT_0045790c = damageLevel * -5 - 14, range -39 (covered) to -14 (uncovered)
- //
- // From FUN_00428a10: When position is negative, we skip source pixels and draw at 0.
- // Handler 25: Corridor overlay and FOBJ position offsets are set during
- // IACT opcode 6 processing (iactRebel2Opcode6), matching the original
- // FUN_41CADB architecture. No corridor drawing needed here.
-
- // Chapter selection: Set FOBJ offset to scroll preview thumbnails in O_LEVEL.SAN.
- // Original (FUN_00415CF8): offsets start at (0,0) for the first display update,
- // then FUN_00425170 sets them to (-90, chapter*-50+75) AFTER each frame.
- // Frame 0 must use (0,0) so the 80x800 preview strip at X=320 renders off-screen
- // and STOR captures it cleanly. Frames 1+ use the scroll offset so FTCH re-renders
- // the strip at the correct preview position.
- if (_gameState == kStateChapterSelect && _player) {
- if (_player->_frame > 0) {
- // Clear screen to black before FTCH re-renders the preview strip.
- // Our FTCH only re-draws the preview area (80px wide at X=230);
- // without clearing, old menu text and preview artifacts persist.
- if (renderBitmap) {
- memset(renderBitmap, 0, _vm->_screenWidth * _vm->_screenHeight);
- }
- _player->_fobjOffsetX = _previewOffsetX;
- _player->_fobjOffsetY = _previewOffsetY;
- }
- }
-}
-
-void InsaneRebel2::procIACT(byte *renderBitmap, int32 codecparam, int32 setupsan12,
- int32 setupsan13, Common::SeekableReadStream &b, int32 size, int32 flags,
- int16 par1, int16 par2, int16 par3, int16 par4) {
- // Debug: Log all IACT opcodes
- debug("Rebel2 IACT: opcode=%d par2=%d par3=%d par4=%d gameState=%d sceneId=%d",
- par1, par2, par3, par4, _gameState, _currSceneId);
-
- if (_keyboardDisable)
- return;
-
- // Handle menu IACT - menu videos have embedded ANIM data in IACT chunks
- // Menu IACTs have par1=8 (code), par2=46 (flags), par4>=1000 (userId)
- // The embedded ANIM contains the full menu frame
- if (_gameState == kStateMainMenu && par1 == 8 && par4 >= 1000) {
- debug("Rebel2 IACT: Menu mode - processing embedded ANIM (userId=%d)", par4);
-
- // Scan for embedded ANIM tag in the IACT data
- int64 startPos = b.pos();
- int64 totalSize = b.size();
- debug("Rebel2 IACT: stream pos=%d, size=%d, remaining=%d",
- (int)startPos, (int)totalSize, (int)(totalSize - startPos));
-
- if (totalSize > startPos) {
- int64 remaining = totalSize - startPos;
- int scanSize = (int)MIN<int64>(remaining, 65536);
- byte *scanBuf = (byte *)malloc(scanSize);
- if (scanBuf) {
- int bytesRead = b.read(scanBuf, scanSize);
- debug("Rebel2 IACT: Read %d bytes, first 16: %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X",
- bytesRead, scanBuf[0], scanBuf[1], scanBuf[2], scanBuf[3],
- scanBuf[4], scanBuf[5], scanBuf[6], scanBuf[7],
- scanBuf[8], scanBuf[9], scanBuf[10], scanBuf[11],
- scanBuf[12], scanBuf[13], scanBuf[14], scanBuf[15]);
-
- // Look for ANIM tag (embedded SAN containing menu frame)
- for (int i = 0; i + 8 <= bytesRead; ++i) {
- if (READ_BE_UINT32(scanBuf + i) == MKTAG('A','N','I','M')) {
- int64 animStreamPos = startPos + i;
- uint32 animReportedSize = READ_BE_UINT32(scanBuf + i + 4);
- int32 toCopy = (int)MIN<int64>((int64)animReportedSize + 8, totalSize - animStreamPos);
- debug("Rebel2 IACT: Found embedded ANIM at offset %d, size %d", (int)i, (int)animReportedSize);
- if (toCopy > 0) {
- byte *animData = (byte *)malloc(toCopy);
- if (animData) {
- b.seek(animStreamPos);
- b.read(animData, toCopy);
- // Use userId as the HUD slot (1000 -> slot 0 for menu background)
- loadEmbeddedSan(0, animData, toCopy, renderBitmap);
- free(animData);
- }
- }
- b.seek(startPos);
- free(scanBuf);
- return;
- }
- }
-
- debug("Rebel2 IACT: No ANIM tag found in menu IACT data");
- b.seek(startPos);
- free(scanBuf);
- }
- }
- return;
- }
-
- if (_currSceneId == 1)
- iactRebel2Scene1(renderBitmap, codecparam, setupsan12, setupsan13, b, size, flags, par1, par2, par3, par4);
-}
-
-
-void InsaneRebel2::iactRebel2Scene1(byte *renderBitmap, int32 codecparam, int32 setupsan12,
- int32 setupsan13, Common::SeekableReadStream &b, int32 size, int32 flags,
- int16 par1, int16 par2, int16 par3, int16 par4) {
- // par1 is the Opcode (word at offset +0)
- // par2 is word at offset +2
- // par3 is word at offset +4
- // par4 is word at offset +6
- //
- // Based on disassembly of FUN_4028C5 and FUN_4033CF:
- //
- // For IACT opcode 4 (enemy position update), the structure is:
- // Offset +0x06: Type/SubType (par3)
- // Offset +0x08: Enemy ID
- // Offset +0x0A: X position
- // Offset +0x0C: Y position
- // Offset +0x0E: Width
- // Offset +0x10: Height
- //
- // The original game calculates bounding box center:
- // centerX = X + (Width / 2)
- // centerY = Y + (Height / 2)
- // Then subtracts scroll offsets:
- // screenX = centerX - DAT_0043e006 (scrollX)
- // screenY = centerY - DAT_0043e008 (scrollY)
-
- // screenX = centerX - DAT_0043e006 (scrollX)
- // screenY = centerY - DAT_0043e008 (scrollY)
-
- if (par1 == 4) {
- enemyUpdate(renderBitmap, b, par2, par3, par4);
- } else if (par1 == 2) {
- // Delegate handling to dedicated opcode 2 handler
- iactRebel2Opcode2(b, par2, par3, par4);
- } else if (par1 == 3) {
- iactRebel2Opcode3(b, par2, par3, par4);
- }
- else if (par1 == 5) {
- // Opcode 5: Collision Zone Registration (FUN_004033cf case 5)
- // Sub-opcode 0x0D (13) = Primary collision zones (obstacles)
- // Sub-opcode 0x0E (14) = Secondary collision zones (boundaries)
- // par2 is the sub-opcode that determines which zone table to use
- debug("Rebel2 IACT Opcode 5: par2=%d par3=%d par4=%d", par2, par3, par4);
-
- if (par2 == 0x0D || par2 == 0x0E) {
- // Register the collision zone from the remaining IACT data
- // par4 (userId from IACT header) is the filter value used by FUN_4092D9
- // for the < 1000 test (offset +6 in the original stored pointer)
- registerCollisionZone(b, par2, par4);
- }
-
- } else if (par1 == 7) {
- // Opcode 7: Handler 7 corridor/velocity control (FUN_40C3CC case 5)
- // IACT header: par1=7, par2=flags, par3=0, par4=sub-opcode
- // Body contains 2 int16 values (body[0], body[1])
- //
- // par4 sub-opcodes (from FUN_40C3CC case 5 switch on param_5[3]):
- // 0: Set velocity params (DAT_00443b12, DAT_00443b14)
- // 1: Set left X + top Y corridor boundaries (DAT_00443b0a, DAT_00443b0c)
- // 2: Set right X + bottom Y corridor boundaries (DAT_00443b0e, DAT_00443b10)
- // 5: Set flag (DAT_00443b52)
-
- int16 body0 = 0, body1 = 0;
- if (b.size() - b.pos() >= 4) {
- body0 = b.readSint16LE();
- body1 = b.readSint16LE();
- }
-
- switch (par4) {
- case 0:
- // Velocity/wind data â affects ship drift in FUN_40C3CC physics
- // DAT_00443b12 = horizontal wind, DAT_00443b14 = vertical wind
- _windParamX = body0;
- _windParamY = body1;
- debug("Rebel2 Opcode 7 par4=0: wind=(%d,%d)", body0, body1);
- break;
- case 1:
- // Set LEFT X boundary and TOP Y boundary
- _corridorLeftX = body0;
- _corridorTopY = body1;
- // Mode-dependent margin adjustment (FUN_40C3CC lines 341-351)
- if (_flyControlMode == 2) {
- _corridorLeftX += 15;
- } else if (_flyControlMode == 0) {
- _corridorLeftX += 20;
- }
- debug("Rebel2 Opcode 7 par4=1: corridor left=%d top=%d (adjusted left=%d)",
- body0, body1, _corridorLeftX);
- break;
- case 2:
- // Set RIGHT X boundary and BOTTOM Y boundary
- _corridorRightX = body0;
- _corridorBottomY = body1;
- // Mode-dependent margin adjustment (FUN_40C3CC lines 356-365)
- if (_flyControlMode == 2) {
- _corridorRightX -= 15;
- } else if (_flyControlMode == 0) {
- _corridorRightX -= 20;
- }
- debug("Rebel2 Opcode 7 par4=2: corridor right=%d bottom=%d (adjusted right=%d)",
- body0, body1, _corridorRightX);
- break;
- case 5:
- // Flag value
- debug("Rebel2 Opcode 7 par4=5: flag=%d", body0);
- break;
- default:
- debug("Rebel2 Opcode 7 par4=%d: body=(%d,%d) â unknown sub-opcode", par4, body0, body1);
- break;
- }
-
- } else if (par1 == 6) {
- // Opcode 6: Level setup / mode switch (FUN_41CADB case 4)
- iactRebel2Opcode6(renderBitmap, b, size, par2, par3, par4);
- } else if (par1 == 8) {
- // Opcode 8: HUD resource loading (FUN_41CADB case 6)
- iactRebel2Opcode8(renderBitmap, b, size, par2, par3, par4);
- } else if (par1 == 9) {
- // Opcode 9: Text/subtitle display
- iactRebel2Opcode9(renderBitmap, b, par2, par3, par4);
- } else if (par1 == 0 || par1 == 1) {
- // Low Opcodes seen in logs
- debug("Rebel2 IACT: Low Opcode %d (par2=%d par3=%d par4=%d)", par1, par2, par3, par4);
- } else {
- debug("Rebel2 IACT: Unknown Opcode %d (par2=%d par3=%d par4=%d)", par1, par2, par3, par4);
- }
-}
-void InsaneRebel2::iactRebel2Opcode2(Common::SeekableReadStream &b, int16 par2, int16 par3, int16 par4) {
- // Handle IACT opcode 2 subcases based on par3 (type). Mirrors FUN_00407fcb behavior where relevant.
- // Keep existing linking behavior (par3 == 4) for compatibility.
-
- // Link case: par3 == 4
- if (par3 == 4) {
- int16 childId = b.readSint16LE(); // Offset +8
- int16 parentId = b.readSint16LE(); // Offset +10
-
- // Validate BOTH parentId AND childId to avoid triggering "set/clear ALL bits" behavior
- // when childId <= 0. The original game's setBit(0)/clearBit(0) affects ALL bits,
- // which would disable/enable all enemies at once - not the intended linking behavior.
- if (parentId >= 1 && parentId < 512 && childId >= 1 && childId < 512) {
- // Shift links (original: 4 link slots at DAT_0045797c/817c/897c/917c)
- _rebelLinks[parentId][2] = _rebelLinks[parentId][1];
- _rebelLinks[parentId][1] = _rebelLinks[parentId][0];
- _rebelLinks[parentId][0] = childId;
-
- // Mirror parent's bit state to child (INVERTED):
- // - Parent alive (bit clear) â setBit(child) â child hidden
- // - Parent dead (bit set) â clearBit(child) â child shown
- // From FUN_0041CADB case 0, par3==4:
- // bVar3 = FUN_00423970(parentId);
- // if (bVar3 == 0) setBit(childId); else clearBit(childId);
- // This ensures linked children (explosion/death sprites) are hidden
- // while the parent is alive, and revealed when the parent is destroyed.
- if (!isBitSet(parentId)) {
- setBit(childId);
- debug("Rebel2: Linked ID=%d to Parent=%d (Slot 0) - child DISABLED (parent alive)", childId, parentId);
- } else {
- clearBit(childId);
- debug("Rebel2: Linked ID=%d to Parent=%d (Slot 0) - child ENABLED (parent dead)", childId, parentId);
- }
- } else {
- debug("Rebel2: Skipping link with invalid IDs childId=%d parentId=%d", childId, parentId);
- }
- return;
- } else if (par3 == 1) { // Probabilistic / counter cases: par3 == 1
- int16 value = par4; // sVar6
- int16 targetId = b.readSint16LE(); // Offset +8 (sVar7)
-
- // Validate targetId >= 1 to avoid triggering "set/clear ALL bits" behavior
- // The original game's setBit(0)/clearBit(0) affects ALL bits, not intended here
- if (targetId < 1 || targetId >= 0x200)
- return;
-
- // Handler 8/25: FUN_401234 case 0 / FUN_0041CADB case 0 par3==1
- // From original FUN_0041CADB:
- // if (par4 == 100) clearBit(body0); // Force enable
- // else { bitMask = 1 << (par4 & 0x1f); if (waveState & bitMask) setBit(body0); }
- if ((_rebelHandler == 8 || _rebelHandler == 25) && value != 0) {
- if (value == 100) {
- // par4==100: Force enable the target (original: FUN_00423a00)
- clearBit(targetId);
- debug("Rebel2 Opcode2 (H%d): Force ENABLE target=%d (par4=100)", _rebelHandler, targetId);
- } else {
- // Check wave state: if enemy type has been killed, disable target
- int bitMask = 1 << (value & 0x1f);
- if ((_rebelWaveState & bitMask) != 0) {
- setBit(targetId);
- debug("Rebel2 Opcode2 (H%d): Disable target=%d (type %d killed, wave=0x%x)", _rebelHandler, targetId, value, _rebelWaveState);
- }
- }
- return;
- }
-
- if (value > 1 && value < 10) { // 1 < value < 10: random disable
- if (_vm->_rnd.getRandomNumber(value) == 0) {
- setBit(targetId);
- debug("Rebel2 IACT Opcode2: Random DISABLE target=%d (value=%d)", targetId, value);
- }
- } else if (value > 10 && value < 20) { // 10 < value < 20: enable/disable with special value==11 = force enable
- if (value == 11) {
- clearBit(targetId);
- debug("Rebel2 IACT Opcode2: FORCE ENABLE target=%d (value=11)", targetId);
- } else {
- if (_vm->_rnd.getRandomNumber(value - 10) == 0) {
- clearBit(targetId);
- debug("Rebel2 IACT Opcode2: Random ENABLE target=%d (value=%d)", targetId, value);
- } else {
- setBit(targetId);
- debug("Rebel2 IACT Opcode2: Random DISABLE target=%d (value=%d)", targetId, value);
- }
- }
- } else if (value > 99 && value < 110) { // 99 < value < 110: increment value counter if target active
- if (!isBitSet(targetId)) {
- int idx = value - 100;
- if (idx >= 0 && idx < 10) {
- _rebelValueCounters[idx]++;
- _rebelLastCounter = _rebelValueCounters[idx];
- debug("Rebel2 IACT Opcode2: Increment VAL counter[%d] -> %d (target=%d)", value, _rebelValueCounters[idx], targetId);
- }
- }
-
- } else if (value > 0x3ff) { // Bitmask case: value > 0x3FF
- for (int slot = 1; slot <= 9; ++slot) {
- if ((value & (1 << (slot - 1))) != 0) {
- if (!isBitSet(targetId)) {
- _rebelMaskCounters[slot]++;
- _rebelLastCounter = _rebelMaskCounters[slot];
- debug("Rebel2 IACT Opcode2: Increment MASK counter[%d] -> %d (target=%d)", slot, _rebelMaskCounters[slot], targetId);
- }
- }
- }
- }
-
- // Unknown sub-type: log and return
- debug("Rebel2 IACT Opcode2: Unhandled par3=%d par4=%d", par3, par4);
- }
-}
-void InsaneRebel2::iactRebel2Opcode3(Common::SeekableReadStream &b, int16 par2, int16 par3, int16 par4) {
- // IACT opcode 3 â damage and hit counter processing.
- // Based on FUN_4092D9 (Handler 0x26), FUN_40E35E (Handler 7), FUN_401234 (Handler 8).
- //
- // The common dispatcher (FUN_4033CF) stores opcode 3 entries in the projectile impact
- // list (DAT_0043f9e0). For handlers 0x26/7 these are processed per-frame by the
- // per-handler collision function (FUN_4092D9/FUN_40E35E). For handlers 8/25 they're
- // processed immediately during IACT dispatch.
- //
- // FUN_403ba9() loop in FUN_4092D9 (lines 209-239):
- // par3 == 1/2: Direct hit â increment hit counter, apply damage if conditions met
- // - body[0] (offset +8): srcId for isBitSet check
- // - par4 != 0: damage from DAT_0047e0f4 (direct hit damage table)
- // - par3==1: par4 must be 1..9 for damage
- // - par3==2: par4 must be > 99, with wave state bit check for par4 >= 101
- //
- // par3 == 5: Probabilistic damage â probability check from DAT_0047e0fc
- // - body[1] (offset +10): srcId for isBitSet check (different from par3=1/2!)
- // - Damage from DAT_0047e0f8 (probabilistic damage table)
- //
- // Stream position on entry: at offset +8 (body[0], first word after 8-byte header)
-
- // Handler 25 has a different opcode 3 structure (FUN_41CADB case 1):
- // par3==5: probabilistic damage WITH cover check (DAT_0045790a < 2)
- // par3==1: increment hit counter ONLY (NO damage), requires par4 != 4
- // par4==100: direct damage (separate check after par3 branches, NO cover check)
- // Other handlers (0x26/7/8) use FUN_4092D9/FUN_40E35E/FUN_401234 with different logic.
- if (_rebelHandler == 25) {
- // Handler 25 opcode 3 â FUN_41CADB case 1
- int16 srcIdBody0 = b.readSint16LE(); // body[0] (offset +8)
- int16 srcIdBody1 = b.readSint16LE(); // body[1] (offset +10)
-
- if (par3 == 5) {
- // Probabilistic damage with cover check (lines 81-92)
- debug("Rebel2 Opcode3: H25 par3=5 srcId=%d isBitSet=%d damageLevel=%d",
- srcIdBody1, isBitSet(srcIdBody1), _rebelDamageLevel);
-
- if (_rebelDamageLevel < 2 && !isBitSet(srcIdBody1)) {
- int probability = 20 + _difficulty * 20;
- if (probability < 5) probability = 5;
- if (probability > 90) probability = 90;
- int roll = _vm->_rnd.getRandomNumber(99);
- debug("Rebel2 Opcode3: probability=%d roll=%d (need roll < prob)", probability, roll);
-
- if (roll < probability) {
- if (!_rebelInvulnerable) {
- int damageAmount = 5 + (_difficulty * 2);
- _playerDamage += damageAmount;
- if (_playerDamage > 255) _playerDamage = 255;
- debug("Rebel2: H25 PROBABILISTIC damage from %d. Damage=%d total=%d",
- srcIdBody1, damageAmount, _playerDamage);
- }
- initDamageFlash();
- }
- } else {
- debug("Rebel2 Opcode3: H25 par3=5 BLOCKED (damageLevel=%d isBitSet=%d)",
- _rebelDamageLevel, isBitSet(srcIdBody1));
- }
- } else if (par3 == 1 && !isBitSet(srcIdBody0) && par4 != 4) {
- // Hit counter only â NO damage (lines 94-98)
- _rebelHitCounter++;
- debug("Rebel2: H25 hit counter++ -> %d (par3=1 par4=%d, no damage)",
- _rebelHitCounter, par4);
- } else {
- debug("Rebel2 Opcode3: H25 par3=%d par4=%d (no action)", par3, par4);
- }
-
- // Direct damage: par4==100, separate from par3 branches (lines 99-111)
- if (par4 == 100 && !isBitSet(srcIdBody0)) {
- if (!_rebelInvulnerable) {
- int directHitDamage = 8 + (_difficulty * 4);
- _playerDamage += directHitDamage;
- if (_playerDamage > 255) _playerDamage = 255;
- debug("Rebel2: H25 DIRECT HIT par4=100 damage=%d total=%d",
- directHitDamage, _playerDamage);
- }
- initDamageFlash();
- }
- } else if (par3 == 1 || par3 == 2) {
- // Non-Handler-25 direct hit path â FUN_4092D9 lines 209-227
- int16 srcId = b.readSint16LE(); // body[0] (offset +8): source enemy ID
-
- debug("Rebel2 Opcode3: par3=%d par4=%d srcId=%d isBitSet=%d",
- par3, par4, srcId, isBitSet(srcId));
-
- if (!isBitSet(srcId)) {
- _rebelHitCounter++;
- debug("Rebel2: Incremented hit counter -> %d", _rebelHitCounter);
-
- int directHitDamage = 8 + (_difficulty * 4);
-
- if (par4 != 0 && directHitDamage > 0) {
- bool shouldDamage = false;
-
- if (par3 == 1 && par4 < 10) {
- shouldDamage = true;
- } else if (par3 == 2 && par4 > 99) {
- if (par4 < 0x65 || (_rebelPhaseState & (1 << ((par4 + 0x9b) & 0x1f))) == 0) {
- shouldDamage = true;
- }
- }
-
- if (shouldDamage) {
- if (!_rebelInvulnerable) {
- _playerDamage += directHitDamage;
- if (_playerDamage > 255) _playerDamage = 255;
- debug("Rebel2: DIRECT HIT damage from enemy %d. par3=%d par4=%d damage=%d total=%d",
- srcId, par3, par4, directHitDamage, _playerDamage);
- }
- initDamageFlash();
- }
- }
- }
- } else if (par3 == 5) {
- // Non-Handler-25 probabilistic damage â FUN_4092D9 lines 228-239
- b.skip(2); // Skip body[0]
- int16 srcId = b.readSint16LE(); // body[1] (offset +10)
-
- debug("Rebel2 Opcode3: par3=5 srcId=%d isBitSet=%d", srcId, isBitSet(srcId));
-
- if (!isBitSet(srcId)) {
- int probability = 20 + _difficulty * 20;
- if (probability < 5) probability = 5;
- if (probability > 90) probability = 90;
-
- int roll = _vm->_rnd.getRandomNumber(99);
- debug("Rebel2 Opcode3: probability=%d roll=%d (need roll < prob)", probability, roll);
-
- if (roll < probability) {
- if (!_rebelInvulnerable) {
- int damageAmount = 5 + (_difficulty * 2);
- _playerDamage += damageAmount;
- if (_playerDamage > 255) _playerDamage = 255;
- debug("Rebel2: PROBABILISTIC damage from enemy %d. Damage=%d total=%d",
- srcId, damageAmount, _playerDamage);
- }
- if (_rebelHandler == 8) {
- triggerDamageEffect();
- } else {
- initDamageFlash();
- }
- }
- }
- } else {
- debug("Rebel2 Opcode3: UNHANDLED par3=%d par4=%d", par3, par4);
- }
-}
-
-void InsaneRebel2::iactRebel2Opcode6(byte *renderBitmap, Common::SeekableReadStream &b, int32 chunkSize, int16 par2, int16 par3, int16 par4) {
- // Opcode 6: Level setup / mode switch
- // Based on FUN_41CADB case 4 (switch on *local_14 - 2 == 4, meaning opcode 6)
- //
- // For Handler 8 (third-person on foot) - FUN_00401234 case 4:
- // - par3 sets ship level mode (DAT_0043e000)
- // - par4 == 1 triggers status bar display and state reset
- // - Updates ship position based on mouse input
- //
- // For Handler 0x26/0x19 (turret/FPS):
- // - Same par4 == 1 behavior
- // - Different view offset calculations
-
- debug("Rebel2 IACT Opcode 6: par2=%d par3=%d par4=%d", par2, par3, par4);
-
- // Update handler type if par2 is a known handler value (from FUN_4033CF case 6)
- if (par2 == 7 || par2 == 8 || par2 == 0x19 || par2 == 0x26) {
- // Reset Level 2 background flag when transitioning away from Handler 8
- if (_rebelHandler == 8 && par2 != 8) {
- _level2BackgroundLoaded = false;
- }
- _rebelHandler = par2;
- debug("Rebel2 Opcode 6: Setting handler=%d", par2);
- }
-
- // Handler 8 specific logic (third-person on foot) - FUN_00401234 case 4
- if (_rebelHandler == 8) {
- // Set ship level mode (DAT_0043e000 = par3)
- _shipLevelMode = par3;
-
- // If par4 == 1, enable status bar
- if (par4 == 1) {
- _rebelStatusBarSprite = 5; // Status bar sprite for Handler 8
- }
-
- // Reset state when shipLevelMode != 0 && par4 == 1 (FUN_401234 lines 97-103)
- if (_shipLevelMode != 0 && par4 == 1) {
- // Clear ALL iactBits â matches FUN_00423880 calling FUN_00423a00(0)
- clearBit(0);
- // Clear link tables
- for (int i = 0; i < 512; i++) {
- _rebelLinks[i][0] = 0;
- _rebelLinks[i][1] = 0;
- _rebelLinks[i][2] = 0;
- }
- // DAT_0047ab98 = DAT_0047ab9c: Reset wave state to accumulated phase state
- _rebelWaveState = _rebelPhaseState;
- debug("Rebel2 Opcode 6 (Handler 8): State reset, wave=0x%x", _rebelWaveState);
- }
-
- // Skip position calculation for special modes 4 and 5
- if (_shipLevelMode != 4 && _shipLevelMode != 5) {
- // ===== Movement Range Transition (Covered vs Shooting) =====
- // Based on FUN_00401234 lines 85-120:
- // Mode 2 = "Covered" state - contract movement range to 41 (0x29)
- // Other modes = "Shooting" state - expand movement range to 127 (0x7f)
- // Transition happens gradually at ±10 per frame for smooth animation
- if (_shipLevelMode == 2) {
- // Covered state - contract movement range
- if (_movementRangeLimit > 41) {
- _movementRangeLimit -= 10;
- }
- if (_movementRangeLimit < 41) {
- _movementRangeLimit = 41;
- }
- } else {
- // Shooting state - expand movement range
- if (_movementRangeLimit < 127) {
- _movementRangeLimit += 10;
- }
- if (_movementRangeLimit > 127) {
- _movementRangeLimit = 127;
- }
- }
-
- // Calculate target position from mouse input
- // Mouse X maps to ship horizontal tilt, Mouse Y to vertical tilt
- // Based on FUN_00401234 lines 151-166:
- // local_18 = ((DAT_0047a7e0 * 5 + 0x27b) * 0x40) / 0xfe
- // local_1c = ((DAT_0047a7e2 * 5 + 0x27b) * 0x10) / 0xfe
-
- // Map mouse position (-127 to 127 range) to ship target
- // Mouse is 0-320, center is 160. Map to -127 to 127 range
- int16 mouseOffsetX = (int16)((_vm->_mouse.x - 160) * 127 / 160);
- int16 mouseOffsetY = (int16)((_vm->_mouse.y - 100) * 127 / 100);
-
- // Clamp X offset to movement range limit (covered/shooting state)
- // Based on FUN_00401234 lines 119-136
- if (mouseOffsetX > _movementRangeLimit) mouseOffsetX = _movementRangeLimit;
- if (mouseOffsetX < -_movementRangeLimit) mouseOffsetX = -_movementRangeLimit;
- // Y offset always uses full range (±127)
- if (mouseOffsetY > 127) mouseOffsetY = 127;
- if (mouseOffsetY < -127) mouseOffsetY = -127;
-
- // Calculate target positions using the original formula
- _shipTargetX = (int16)(((mouseOffsetX * 5 + 0x27b) * 0x40) / 0xfe);
- _shipTargetY = (int16)(-((mouseOffsetY * 5 + 0x27b) * 0x10) / 0xfe);
-
- // Smooth interpolation toward target (max 50 pixels per frame)
- const int16 maxStep = 50; // 0x32 in hex
- if (_shipPosX < _shipTargetX) {
- int16 newX = _shipPosX + maxStep;
- _shipPosX = (newX > _shipTargetX) ? _shipTargetX : newX;
- } else if (_shipPosX > _shipTargetX) {
- int16 newX = _shipPosX - maxStep;
- _shipPosX = (newX < _shipTargetX) ? _shipTargetX : newX;
- }
-
- if (_shipPosY < _shipTargetY) {
- int16 newY = _shipPosY + maxStep;
- _shipPosY = (newY > _shipTargetY) ? _shipTargetY : newY;
- } else if (_shipPosY > _shipTargetY) {
- int16 newY = _shipPosY - maxStep;
- _shipPosY = (newY < _shipTargetY) ? _shipTargetY : newY;
- }
-
- // Calculate ship direction indices for sprite selection
- // Map mouse position to 5x7 direction grid (like Handler 7)
- int16 mouseX = _vm->_mouse.x;
- int16 mouseY = _vm->_mouse.y;
-
- // Scale mouse if video is larger than 320x200
- if (_player && _player->_width > 320) {
- mouseX = (mouseX * 320) / _player->_width;
- }
- if (_player && _player->_height > 200) {
- mouseY = (mouseY * 200) / _player->_height;
- }
-
- // Horizontal: 5 zones (0=far left, 2=center, 4=far right)
- if (mouseX < 64) _shipDirectionH = 0;
- else if (mouseX < 128) _shipDirectionH = 1;
- else if (mouseX < 192) _shipDirectionH = 2;
- else if (mouseX < 256) _shipDirectionH = 3;
- else _shipDirectionH = 4;
-
- // Vertical: 7 zones (0=far up, 3=center, 6=far down)
- if (mouseY < 28) _shipDirectionV = 0;
- else if (mouseY < 57) _shipDirectionV = 1;
- else if (mouseY < 86) _shipDirectionV = 2;
- else if (mouseY < 114) _shipDirectionV = 3;
- else if (mouseY < 143) _shipDirectionV = 4;
- else if (mouseY < 171) _shipDirectionV = 5;
- else _shipDirectionV = 6;
-
- _shipDirectionIndex = _shipDirectionH * 7 + _shipDirectionV;
- }
-
- // Update firing state from mouse button
- // Mode 4 (autopilot) disables shooting - FUN_00401CCF line 82-84
- if (_shipLevelMode == 4) {
- _shipFiring = false;
- } else {
- _shipFiring = (_vm->VAR(_vm->VAR_LEFTBTN_HOLD) != 0);
- }
-
- debug("Rebel2 Opcode 6 (Handler 8): mode=%d range=%d shipPos=(%d,%d) target=(%d,%d) firing=%d dir=(%d,%d,%d)",
- _shipLevelMode, _movementRangeLimit, _shipPosX, _shipPosY, _shipTargetX, _shipTargetY, _shipFiring,
- _shipDirectionH, _shipDirectionV, _shipDirectionIndex);
-
- // Handler 8 doesn't use the same view offset logic as other handlers
- // Skip the rest of the function for Handler 8
- return;
- }
-
- // Handler 7 specific logic (third-person ship) - FUN_0040d836 / FUN_0040c3cc
- // Used for Level 3 and similar space combat levels
- if (_rebelHandler == 7) {
- // Set control mode: DAT_004437c0 = param_5[3] = par4 in FUN_40C3CC case 4.
- // This determines collision mode and shooting capability:
- // Mode 0: Obstacle avoidance â SECONDARY zones, corridor boundaries
- // Mode 1: Tunnel flight â PRIMARY zones, per-edge push-back (hMargin=0x28)
- // Mode 2: Combat mode â shooting ENABLED, SECONDARY zones
- // Mode 3: Tunnel flight â PRIMARY zones, per-edge push-back (hMargin=0x0f)
- _flyControlMode = par4;
- debug("Rebel2 Opcode 6 (Handler 7): Control mode set to %d (shooting %s)",
- par4, (par4 == 2) ? "ENABLED" : "DISABLED");
-
- // Status bar: param_5[4] == 1 in original (first body word, 5th IACT word)
- // In our parsing, par3 maps to param_5[2] and the body follows par4.
- // FUN_40C3CC: if (param_5[4] == 1) FUN_0040bb87(DAT_0047a828,5);
- // par3 is param_5[2], which the original doesn't use here.
- // The body word for status bar is read separately below.
- int16 bodyStatusFlag = 0;
- if (b.size() - b.pos() >= 2) {
- bodyStatusFlag = b.readSint16LE();
- }
- if (bodyStatusFlag == 1) {
- _rebelStatusBarSprite = 5; // Status bar sprite
- debug("Rebel2 Opcode 6 (Handler 7): Status bar enabled (body flag=%d)", bodyStatusFlag);
- }
-
- // ============================================================
- // Ship position update â FUN_40C3CC case 4, lines 49-327
- // ============================================================
- // Velocity-based physics with momentum/inertia:
- // Mouse offset from center â scaled input [-127,127]
- // â velocity history averaging â physics delta (clamped ±12/frame)
- // â position clamping â corridor collision â perspective offsets
- //
- // Level data table (DAT_0047e0e8 + level*0x242 + difficulty*0x22):
- // offset 0: smoothing param (>>4 +1 = window size)
- // offset 2: Y speed offset 4: X speed (levelSpeed)
- // offset 6: wind multiplier offset 14: corridor damage
- // We don't have the actual level data, so we use calibrated defaults.
-
- // --- Step 1: Mouse input as offset from screen center ---
- // DAT_0047a7e0 = mouseX - 160, DAT_0047a7e2 = mouseY - 100
- // _vm->_mouse.x/y are in virtual screen coords (0-319, 0-199)
- // consistent with handler 8 which uses _vm->_mouse.x directly.
- int16 inputX = (int16)(_vm->_mouse.x - 160); // DAT_0047a7e0
- int16 inputY = (int16)(_vm->_mouse.y - 100); // DAT_0047a7e2
-
- // Clamp: mouse mode uses [-160, 160] for X, [-127, 127] for Y (lines 55-70)
- if (inputX > 160) inputX = 160;
- if (inputX < -160) inputX = -160;
- if (inputY > 127) inputY = 127;
- if (inputY < -127) inputY = -127;
-
- // --- Step 2: Scale to [-127, 127] (lines 82-84) ---
- // Mouse mode: local_c = (DAT_0047a7e0 * 0x7f) / 0xa0
- int16 local_c = (int16)((inputX * 127) / 160);
- int16 local_14 = inputY; // Y already in [-127, 127]
-
- // --- Step 3: Velocity history + smoothed average (lines 141-157) ---
- for (int i = 24; i > 0; i--) {
- _velocityHistory[i] = _velocityHistory[i - 1];
- }
- _velocityHistory[0] = local_c;
-
- // Window size = (levelData[0] >> 4) + 1. Calibrated default: 5.
- const int smoothWindow = 5;
- int velSum = 0;
- for (int i = 0; i < smoothWindow; i++) {
- velSum += _velocityHistory[i];
- }
- _smoothedVelocity = (int16)(velSum / smoothWindow); // DAT_0044370c
-
- // --- Step 4: Wind history (lines 158-173) ---
- // Wind multiplier comes from level data[6]. Without data, use 0 (no wind).
- const int16 windMult = 0;
- int windSumX = 0, windSumY = 0;
- for (int i = 14; i > 0; i--) {
- _windHistoryX[i] = _windHistoryX[i - 1];
- windSumX += _windHistoryX[i];
- }
- _windHistoryX[0] = _windParamX;
- int16 windEffectX = (int16)((windMult * (windSumX + _windParamX)) / 15);
-
- for (int i = 14; i > 0; i--) {
- _windHistoryY[i] = _windHistoryY[i - 1];
- windSumY += _windHistoryY[i];
- }
- _windHistoryY[0] = _windParamY;
- int16 windEffectY = (int16)((windMult * (windSumY + _windParamY)) / 15);
-
- // --- Step 5: Position delta (lines 174-242) ---
- // levelSpeed (offset 4): calibrated so max velocity (127) â delta 12.
- // 8 = (speed * 127) >> 9 â speed â 32
- // levelYSpeed (offset 2): calibrated so max input (127) â delta ~6.
- // 6 = (speed * 127) >> 10 â speed â 48
- const int16 levelSpeed = 32;
- const int16 levelYSpeed = 48;
- int16 absSmoothVel = ABS(_smoothedVelocity);
- int16 positionDeltaX;
-
- if (_flyControlMode == 1) {
- // Mode 1: Full cross-axis coupling (lines 174-186)
- // Banking: vertical input deflects horizontal movement
- if (local_c < 1) {
- positionDeltaX = (int16)((levelSpeed * _smoothedVelocity - absSmoothVel * local_14 - windEffectX) >> 9);
- } else {
- positionDeltaX = (int16)((levelSpeed * _smoothedVelocity + absSmoothVel * local_14 - windEffectX) >> 9);
- }
- } else {
- // Mode 0/2/3: Reduced cross-axis coupling (lines 218-230)
- if (local_c < 1) {
- positionDeltaX = (int16)((levelSpeed * _smoothedVelocity - (absSmoothVel * local_14 >> 2) - windEffectX) >> 9);
- } else {
- positionDeltaX = (int16)((levelSpeed * _smoothedVelocity + (absSmoothVel * local_14 >> 2) - windEffectX) >> 9);
- }
- }
-
- // Clamp X delta to ±12 per frame (lines 187-192 / 231-236)
- if (positionDeltaX < -11) positionDeltaX = -12;
- if (positionDeltaX > 11) positionDeltaX = 12;
-
- // Apply X delta (line 193 / 237)
- _flyShipScreenX += positionDeltaX;
-
- // Y delta
- if (_flyControlMode == 1) {
- // Mode 1: clamped to ±12 with wind (lines 194-216)
- int yCalc = levelYSpeed * local_14 - (windEffectY >> 1);
- int yDelta = yCalc >> 10;
- if (yDelta < -12) yDelta = -12;
- if (yDelta > 12) yDelta = 12;
- _flyShipScreenY -= (int16)yDelta;
- } else {
- // Mode 0/2/3: unclamped (lines 238-241)
- _flyShipScreenY -= (int16)((levelYSpeed * local_14) >> 10);
- }
-
- // Store vertical input for direction sprite (line 243)
- _verticalInput = local_14; // DAT_0044370e
-
- // Ship facing direction (line 244)
- _facingRight = (0xd4 < _smoothedVelocity + _flyShipScreenX);
-
- // --- Step 6: Position clamping (lines 245-256) ---
- if (_flyShipScreenX > 0x194) _flyShipScreenX = 0x194; // 404
- if (_flyShipScreenY > 0xF0) _flyShipScreenY = 0xF0; // 240
- if (_flyShipScreenX < 0x14) _flyShipScreenX = 0x14; // 20
- if (_flyShipScreenY < 0x14) _flyShipScreenY = 0x14; // 20
-
- // --- Step 7: Corridor collision â mode 0/2 only (lines 257-292) ---
- if (_flyControlMode == 0 || _flyControlMode == 2) {
- // Right boundary (lines 258-270)
- // Original: position is ALWAYS clamped; damage/bounce only when cooldown < 5
- if (_corridorRightX < _flyShipScreenX) {
- _flyShipScreenX = _corridorRightX;
- if (_hitCooldown < 5) {
- for (int i = 0; i < 25; i++) _velocityHistory[i] = -127;
- _hitCooldown = 10;
- _spaceShotDirection = 1;
- initDamageFlash();
- if (!_rebelInvulnerable) {
- int damage = 3 + (_difficulty * 2);
- _playerDamage += damage;
- if (_playerDamage > 255) _playerDamage = 255;
- }
- _rebelHitCounter++;
- playSfx(1, 127, 100); // CRASH.SAD, right wall â pan right
- }
- }
- // Left boundary (lines 271-283)
- if (_flyShipScreenX < _corridorLeftX) {
- _flyShipScreenX = _corridorLeftX;
- if (_hitCooldown < 5) {
- for (int i = 0; i < 25; i++) _velocityHistory[i] = 127;
- _hitCooldown = 10;
- _spaceShotDirection = 0;
- initDamageFlash();
- if (!_rebelInvulnerable) {
- int damage = 3 + (_difficulty * 2);
- _playerDamage += damage;
- if (_playerDamage > 255) _playerDamage = 255;
- }
- _rebelHitCounter++;
- playSfx(1, 127, -100); // CRASH.SAD, left wall â pan left
- }
- }
- // Y boundary clamping â no damage (lines 285-292)
- if (_corridorBottomY < _flyShipScreenY) {
- _flyShipScreenY = _corridorBottomY;
- }
- if (_flyShipScreenY < _corridorTopY) {
- _flyShipScreenY = _corridorTopY;
- }
- }
-
- // --- Step 8: Perspective offsets (lines 293-316) ---
- // f(x) = (focal * center * |offset|) / ((center - focal) * |offset| + focal * center)
- // Close view (DAT_0047a7fc < 1): focalX=0x34, focalY=0x2d
- // Far view (DAT_0047a7fc >= 1): focalX=0x2b, focalY=0x19
- {
- int absOffX = ABS(_flyShipScreenX - 0xd4);
- int16 focalX = 0x2b; // Far view default for Level 3
- if (absOffX > 0) {
- _perspectiveX = (int16)((focalX * 0xd4 * absOffX) /
- ((0xd4 - focalX) * absOffX + focalX * 0xd4));
- } else {
- _perspectiveX = 0;
- }
- if (_flyShipScreenX < 0xd5) _perspectiveX = -_perspectiveX;
-
- int absOffY = ABS(_flyShipScreenY - 0x82);
- int16 focalY = 0x19; // Far view default for Level 3
- if (absOffY > 0) {
- _perspectiveY = (int16)((focalY * 0x82 * absOffY) /
- ((0x82 - focalY) * absOffY + focalY * 0x82));
- } else {
- _perspectiveY = 0;
- }
- if (_flyShipScreenY < 0x83) _perspectiveY = -_perspectiveY;
- }
-
- // View shift = clamped smoothed velocity (FUN_0040d836 lines 68-74)
- _viewShift = _smoothedVelocity;
- if (_viewShift > 127) _viewShift = 127;
- if (_viewShift < -127) _viewShift = -127;
-
- // --- Step 9: Direction sprite (FUN_0040d836 lines 88-106) ---
- // 5x7 grid: vDir(0-4) * 7 + hDir(0-6) = sprite index (0-34)
- // vDir from vertical input: (0xa0 - verticalInput) >> 6
- int16 vDir = (int16)(((int)(0xa0 - _verticalInput) + ((0xa0 - _verticalInput) < 0 ? 63 : 0)) >> 6);
- if (vDir < 0) vDir = 0;
- if (vDir > 4) vDir = 4;
-
- // hDir from smoothed velocity: (0x95 - smoothedVelocity) / 0x2b
- int16 hDir = (int16)((0x95 - _smoothedVelocity) / 0x2b);
- if (hDir < 0) hDir = 0;
- if (hDir > 6) hDir = 6;
-
- // Hysteresis at center (lines 90-97, 98-105)
- if (hDir == 3 && ABS(_smoothedVelocity) > 10) {
- hDir = (_smoothedVelocity < 1) ? 4 : 2;
- }
- if (vDir == 2 && ABS(_verticalInput) > 15) {
- vDir = (_verticalInput < 1) ? 3 : 1;
- }
-
- _shipDirectionIndex = vDir * 7 + hDir;
- if (_shipDirectionIndex < 0) _shipDirectionIndex = 0;
- if (_shipDirectionIndex > 34) _shipDirectionIndex = 34;
-
- _shipFiring = (_flyControlMode == 2) && (_vm->VAR(_vm->VAR_LEFTBTN_HOLD) != 0);
-
- debug("Rebel2 H7: pos=(%d,%d) vel=%d vIn=%d dx=%d dir=%d mode=%d",
- _flyShipScreenX, _flyShipScreenY, _smoothedVelocity,
- _verticalInput, positionDeltaX, _shipDirectionIndex, _flyControlMode);
-
- return;
- }
-
- // Handler 25 (0x19) specific logic (mixed mode - speeder bike)
- // Based on FUN_0041cadb case 4 (opcode 6) lines 113-229
- if (_rebelHandler == 25) {
- // Read the reset flag from IACT data at offset 8-9 (local_14[4] in decompiled code)
- // The stream position should be at offset 8 after par4 was read
- // From FUN_0041cadb line 114: if (local_14[4] == 1) { ... reset ... }
- int16 par5 = 0;
- if (b.pos() + 2 <= b.size()) {
- int64 savedPos = b.pos();
- par5 = b.readSint16LE();
- b.seek(savedPos); // Don't consume the stream
- }
-
- // If par5 == 1, enable status bar and reset state (lines 114-121)
- // Note: This is local_14[4] in the decompiled code, NOT local_14[3] (par4)
- if (par5 == 1) {
- _rebelStatusBarSprite = 5;
- // Clear ALL iactBits â matches FUN_00423880 calling FUN_00423a00(0)
- // at IACT callback registration time. Each new wave video starts with
- // a clean bit table so enemy IDs reused across videos work correctly.
- clearBit(0);
- // Reset link tables (DAT_0045797c through DAT_0045917c)
- for (int i = 0; i < 512; i++) {
- _rebelLinks[i][0] = 0;
- _rebelLinks[i][1] = 0;
- _rebelLinks[i][2] = 0;
- }
- // Reset wave state to accumulated phase state (same as Handler 8)
- // DAT_0047ab98 = DAT_0047ab9c: ensures new wave starts with correct state
- _rebelWaveState = _rebelPhaseState;
- debug("Rebel2 Opcode 6 (Handler 25): Status bar enabled, state reset, wave=0x%x autopilot=%d damageLevel=%d",
- _rebelWaveState, _rebelAutopilot, _rebelDamageLevel);
- }
-
- // Set sprite mode (DAT_00457900 = local_14[3]) - controls which GRD sprite to render
- // From FUN_0041cadb line 122: DAT_00457900 = local_14[3];
- // In ScummVM's IACT parsing: local_14[3] = offset 6-7 = par4
- // Mode 1: Uncovered, shooting position - sprite on left
- // Mode 2: Covered, vertical shift
- // Mode 3: Transition between covered/uncovered - sprite position depends on direction
- // Mode 4: Alternative uncovered position - sprite on right
- _grdSpriteMode = par4; // local_14[3] maps to par4 (offset 6-7)
-
- debug("Rebel2 Handler25 Opcode6: par2=%d par3=%d par4=%d(mode) par5=%d(reset) autopilot=%d damageLevel=%d controlMode=%d",
- par2, par3, par4, par5, _rebelAutopilot, _rebelDamageLevel, _rebelControlMode);
-
- // Autopilot logic (lines 123-146)
- // From original FUN_0041cadb - NO damageLevel check, toggle happens immediately
- // The damage level counter provides the smooth visual transition
- if (!_rebelInvulnerable) {
- if (_rebelAutopilot == 0) {
- // Uncovered: RIGHT button enters cover
- if ((_rebelControlMode & 2) != 0) {
- _rebelAutopilot = 1;
- debug("Rebel2 Handler25: Entering cover (right click), controlMode=%d", _rebelControlMode);
- }
- } else {
- // Covered: ANY button exits cover
- if (_rebelControlMode != 0) {
- _rebelAutopilot = 0;
- debug("Rebel2 Handler25: Exiting cover (button click), controlMode=%d", _rebelControlMode);
- }
- }
- // Clear control mode after processing (sticky flags consumed)
- _rebelControlMode = 0;
- } else {
- // Invulnerable mode: random autopilot changes
- if (_rebelAutopilot == 0) {
- if (_vm->_rnd.getRandomNumber(100) == 0) {
- _rebelAutopilot = 1;
- }
- } else {
- if (_vm->_rnd.getRandomNumber(15) == 0) {
- _rebelAutopilot = 0;
- _rebelFlightDir = _vm->_rnd.getRandomNumber(2);
- }
- }
- }
-
- // Update damage level counter (lines 147-154)
- // This provides the smooth transition animation between covered/uncovered states
- int prevDamageLevel = _rebelDamageLevel;
- if (_rebelAutopilot == 0) {
- // Uncovered: decrement damage level towards 0
- if (_rebelDamageLevel > 0) {
- _rebelDamageLevel--;
- }
- } else {
- // Covered: increment damage level towards 5
- if (_rebelDamageLevel < 5) {
- _rebelDamageLevel++;
- }
- }
- if (_rebelDamageLevel != prevDamageLevel) {
- debug("Rebel2 Handler25: damageLevel transition %d -> %d (autopilot=%d)",
- prevDamageLevel, _rebelDamageLevel, _rebelAutopilot);
- }
-
- // Flight direction logic for mode 3 (lines 155-177)
- if (_grdSpriteMode == 3) {
- if (_rebelDamageLevel == 5) {
- // At max damage, check for direction change input
- // For now, use mouse X position to determine direction
- int16 mouseX = _vm->_mouse.x;
- if (_player && _player->_width > 320) {
- mouseX = (mouseX * 320) / _player->_width;
- }
- if (mouseX > 235) { // 0x4b + 160 = 235
- _rebelFlightDir = 1;
- }
- if (mouseX < 85) { // 160 - 0x4b = 85
- _rebelFlightDir = 0;
- }
- }
- } else {
- _rebelFlightDir = 0;
- }
-
- // Calculate sprite and view offset positions based on mode (lines 182-213)
- // DAT_0045790c = view offset X (for corridor overlay)
- // DAT_0045790e = view offset Y (for corridor overlay)
- // DAT_00457910 = sprite position X (relative to center)
- // DAT_00457912 = sprite position Y (relative to center)
- if (_grdSpriteMode == 1) {
- // Mode 1: Uncovered, shooting - sprite shifts left as damage increases
- _rebelViewMode1 = 0x0e;
- _rebelViewMode2 = 0;
- _rebelViewOffsetX = _rebelDamageLevel * -5 + -14; // DAT_0045790c
- _rebelViewOffset2X = _rebelDamageLevel * -22; // DAT_00457910
- _rebelViewOffsetY = 0; // DAT_0045790e
- _rebelViewOffset2Y = 0; // DAT_00457912
- } else if (_grdSpriteMode == 4) {
- // Mode 4: Alternative uncovered - sprite shifts right
- _rebelViewMode1 = 0x22;
- _rebelViewMode2 = 0;
- _rebelViewOffsetX = _rebelDamageLevel * 10 + -16; // DAT_0045790c
- _rebelViewOffset2X = _rebelDamageLevel * 17 + -85; // DAT_00457910 (0x11 = 17, -0x55 = -85)
- _rebelViewOffsetY = 0;
- _rebelViewOffset2Y = 0;
- } else if (_grdSpriteMode == 2) {
- // Mode 2: Covered - vertical shift
- _rebelViewMode1 = 0;
- _rebelViewMode2 = 0x0e;
- _rebelViewOffsetY = _rebelDamageLevel * -5 + -14; // DAT_0045790e
- _rebelViewOffset2Y = (5 - _rebelDamageLevel) * 15 + -60; // DAT_00457912 (0xf = 15, -0x3c = -60)
- _rebelViewOffsetX = 0;
- _rebelViewOffset2X = 0;
- } else if (_grdSpriteMode == 3) {
- // Mode 3: Transition - direction-dependent horizontal shift
- _rebelViewMode1 = 0x0f;
- _rebelViewMode2 = 0;
- // (-(DAT_00457902 == 0) & 6) - 3 = if dir==0: 6-3=3, else 0-3=-3
- int16 dirMultX = (_rebelFlightDir == 0) ? 3 : -3;
- // (-(DAT_00457902 == 0) & 0x28) - 0x14 = if dir==0: 40-20=20, else 0-20=-20
- int16 dirMultX2 = (_rebelFlightDir == 0) ? 20 : -20;
- _rebelViewOffsetX = dirMultX * (5 - _rebelDamageLevel) + -15; // DAT_0045790c
- _rebelViewOffset2X = dirMultX2 * (5 - _rebelDamageLevel); // DAT_00457910
- _rebelViewOffsetY = 0;
- _rebelViewOffset2Y = 0;
- } else {
- // Mode 0 or unknown: use Mode 1 defaults as fallback
- _rebelViewMode1 = 0x0e;
- _rebelViewMode2 = 0;
- _rebelViewOffsetX = _rebelDamageLevel * -5 + -14;
- _rebelViewOffset2X = _rebelDamageLevel * -22;
- _rebelViewOffsetY = 0;
- _rebelViewOffset2Y = 0;
- debug("Rebel2 Opcode 6 (Handler 25): Unknown mode %d, using Mode 1 fallback", _grdSpriteMode);
- }
-
- debug("Rebel2 Opcode 6 (Handler 25): mode=%d damage=%d dir=%d autopilot=%d viewOff=(%d,%d) spritePos=(%d,%d)",
- _grdSpriteMode, _rebelDamageLevel, _rebelFlightDir, _rebelAutopilot,
- _rebelViewOffsetX, _rebelViewOffsetY, _rebelViewOffset2X, _rebelViewOffset2Y);
-
- // Set FOBJ position offsets (FUN_00424510 in original, line 214)
- // All subsequent FOBJs in this frame will be shifted by these offsets
- if (_player) {
- _player->_fobjOffsetX = _rebelViewOffsetX;
- _player->_fobjOffsetY = _rebelViewOffsetY;
- }
-
- // Draw corridor overlay OPAQUELY (FUN_00428A10 in original, line 216)
- // This wipes previous frame content so codec 23 delta skip regions show clean corridor
- if (renderBitmap) {
- EmbeddedSanFrame &corridorOverlay = _rebelEmbeddedHud[4];
- if (corridorOverlay.valid && corridorOverlay.pixels) {
- int pitch = (_player && _player->_width > 0) ? _player->_width : 320;
- int bufHeight = (_player && _player->_height > 0) ? _player->_height : 200;
-
- int srcOffsetX = 0;
- int srcOffsetY = 0;
- int destX = _rebelViewOffsetX;
- int destY = _rebelViewOffsetY;
- int drawWidth = corridorOverlay.width;
- int drawHeight = corridorOverlay.height;
-
- if (destX < 0) { srcOffsetX = -destX; drawWidth -= srcOffsetX; destX = 0; }
- if (destY < 0) { srcOffsetY = -destY; drawHeight -= srcOffsetY; destY = 0; }
- if (destX + drawWidth > pitch) drawWidth = pitch - destX;
- if (destY + drawHeight > bufHeight) drawHeight = bufHeight - destY;
- if (drawWidth > corridorOverlay.width - srcOffsetX) drawWidth = corridorOverlay.width - srcOffsetX;
- if (drawHeight > corridorOverlay.height - srcOffsetY) drawHeight = corridorOverlay.height - srcOffsetY;
-
- if (drawWidth > 0 && drawHeight > 0) {
- for (int y = 0; y < drawHeight; y++) {
- memcpy(renderBitmap + (destY + y) * pitch + destX,
- corridorOverlay.pixels + (srcOffsetY + y) * corridorOverlay.width + srcOffsetX,
- drawWidth);
- }
- }
- debug("Rebel2 Opcode 6: Corridor overlay drawn at (%d,%d) size(%d,%d)",
- _rebelViewOffsetX, _rebelViewOffsetY, corridorOverlay.width, corridorOverlay.height);
- }
- }
-
- return;
- }
-
- // Step 1: If par4 == 1, initialize/reset state (lines 114-121)
- if (par4 == 1) {
- // Draw status bar sprite 5 (FUN_0040bb87 equivalent)
- _rebelStatusBarSprite = (_rebelLevelType == 5) ? 53 : 5;
- debug("Rebel2 Opcode 6: Status Bar ENABLED - sprite %d", _rebelStatusBarSprite);
-
- // Clear ALL iactBits â matches FUN_00423880 calling FUN_00423a00(0)
- clearBit(0);
-
- // Clear link tables (DAT_0045797c through DAT_0045917c)
- for (int i = 0; i < 512; i++) {
- _rebelLinks[i][0] = 0;
- _rebelLinks[i][1] = 0;
- _rebelLinks[i][2] = 0;
- }
-
- // DAT_0047ab98 = DAT_0047ab9c: At the start of each wave video,
- // reset wave state to accumulated phase state. Enemies killed in
- // previous waves stay killed; new kills add during this wave.
- _rebelWaveState = _rebelPhaseState;
- _rebelHitCounter = 0;
- debug("Rebel2 Opcode 6: Wave state reset to phase state 0x%x", _rebelWaveState);
- }
-
- // Step 2: Set level type (DAT_00457900 = par3)
- _rebelLevelType = par3;
-
- // Step 3: Autopilot/control mode logic (lines 123-146)
- // This determines whether the ship flies on autopilot or manual control
- if (!_rebelInvulnerable) {
- // Normal mode: check control mode flags
- if (_rebelAutopilot == 0) {
- if ((_rebelControlMode & 2) != 0) {
- _rebelAutopilot = 1;
- }
- } else {
- if (_rebelControlMode != 0) {
- _rebelAutopilot = 0;
- }
- }
- } else {
- // Invulnerable mode: random autopilot changes
- if (_rebelAutopilot == 0) {
- if (_vm->_rnd.getRandomNumber(100) == 0) {
- _rebelAutopilot = 1;
- }
- } else {
- if (_vm->_rnd.getRandomNumber(15) == 0) {
- _rebelAutopilot = 0;
- _rebelFlightDir = _vm->_rnd.getRandomNumber(2);
- }
- }
- }
-
- // Step 4: Update damage level counter (lines 147-154)
- if (_rebelAutopilot == 0) {
- if (_rebelDamageLevel > 0) {
- _rebelDamageLevel--;
- }
- } else {
- if (_rebelDamageLevel < 5) {
- _rebelDamageLevel++;
- }
- }
-
- // Handle level type 3 special direction logic (lines 155-181)
- if (_rebelLevelType == 3) {
- if (_rebelDamageLevel == 5) {
- // Check for joystick/key input to change direction
- // Simplified: use mouse position
- if (_vm->_mouse.x > 75) {
- _rebelFlightDir = 1;
- }
- if (_vm->_mouse.x < -75) {
- _rebelFlightDir = 0;
- }
- }
- } else {
- _rebelFlightDir = 0;
- }
-
- // Step 5: Calculate view offsets based on level type (lines 182-213)
- switch (_rebelLevelType) {
- case 1:
- // Type 1: Vertical movement
- _rebelViewMode1 = 0x0e;
- _rebelViewMode2 = 0;
- _rebelViewOffsetX = _rebelDamageLevel * -5 - 0x0e;
- _rebelViewOffset2X = _rebelDamageLevel * -0x16;
- _rebelViewOffsetY = 0;
- _rebelViewOffset2Y = 0;
- break;
-
- case 4:
- // Type 4: Different vertical movement
- _rebelViewMode1 = 0x22;
- _rebelViewMode2 = 0;
- _rebelViewOffsetX = _rebelDamageLevel * 10 - 0x10;
- _rebelViewOffset2X = _rebelDamageLevel * 0x11 - 0x55;
- _rebelViewOffsetY = 0;
- _rebelViewOffset2Y = 0;
- break;
-
- case 2:
- // Type 2: Horizontal movement
- _rebelViewMode1 = 0;
- _rebelViewMode2 = 0x0e;
- _rebelViewOffsetY = _rebelDamageLevel * -5 - 0x0e;
- _rebelViewOffset2Y = (5 - _rebelDamageLevel) * 0x0f - 0x3c;
- _rebelViewOffsetX = 0;
- _rebelViewOffset2X = 0;
- break;
-
- case 3:
- // Type 3: Direction-based movement
- _rebelViewMode1 = 0x0f;
- _rebelViewMode2 = 0;
- {
- int dirFactor = (_rebelFlightDir == 0) ? 3 : -3; // (-(ushort)(DAT_00457902 == 0) & 6) - 3
- int dirFactor2 = (_rebelFlightDir == 0) ? 0x14 : -0x14; // (-(ushort)(DAT_00457902 == 0) & 0x28) - 0x14
- _rebelViewOffsetX = dirFactor * (5 - _rebelDamageLevel) - 0x0f;
- _rebelViewOffset2X = dirFactor2 * (5 - _rebelDamageLevel);
- }
- _rebelViewOffsetY = 0;
- _rebelViewOffset2Y = 0;
- break;
-
- default:
- // Default: No special offsets
- _rebelViewMode1 = 0;
- _rebelViewMode2 = 0;
- _rebelViewOffsetX = 0;
- _rebelViewOffsetY = 0;
- _rebelViewOffset2X = 0;
- _rebelViewOffset2Y = 0;
- break;
- }
-
- debug("Rebel2 Opcode 6: levelType=%d autopilot=%d damageLevel=%d viewOffset=(%d,%d)",
- _rebelLevelType, _rebelAutopilot, _rebelDamageLevel, _rebelViewOffsetX, _rebelViewOffsetY);
-
- // Detect and load embedded ANIM (SAN) within the remaining IACT payload
- // Note: chunkSize is the remaining IACT payload size after par1-par4 header
- {
- int64 startPos = b.pos();
- // Use chunkSize (remaining IACT payload) rather than b.size() (entire FRME stream)
- int64 remaining = chunkSize;
- if (remaining > 0) {
- int scanSize = (int)MIN<int64>(remaining, 65536);
- byte *scanBuf = (byte *)malloc(scanSize);
- if (scanBuf) {
- int bytesRead = b.read(scanBuf, scanSize);
- for (int i = 0; i + 8 <= bytesRead; ++i) {
- if (READ_BE_UINT32(scanBuf + i) == MKTAG('A','N','I','M')) {
- int64 animStreamPos = startPos + i;
- uint32 animReportedSize = READ_BE_UINT32(scanBuf + i + 4);
- // Limit to remaining IACT payload (chunkSize - offset into payload)
- int32 toCopy = (int)MIN<int64>((int64)animReportedSize + 8, chunkSize - i);
- if (toCopy > 0) {
- byte *animData = (byte *)malloc(toCopy);
- if (animData) {
- b.seek(animStreamPos);
- b.read(animData, toCopy);
- loadEmbeddedSan(par4, animData, toCopy, renderBitmap);
- free(animData);
- }
- }
- b.seek(startPos);
- free(scanBuf);
- return;
- }
- }
- b.seek(startPos);
- free(scanBuf);
- }
- }
- }
-}
-
-void InsaneRebel2::iactRebel2Opcode8(byte *renderBitmap, Common::SeekableReadStream &b, int32 chunkSize, int16 par2, int16 par3, int16 par4) {
- // Opcode 8: HUD/Ship resource loading
- // Dispatches to handler-specific loading functions based on current handler and parameters.
- //
- // Handler-specific routing (based on retail disassembly):
- // Handler 7 (FUN_0040c3cc): FLY NUT sprites via par4 (1, 2, 3, 11)
- // Handler 8 (FUN_00401234): POV NUT sprites via par3 (1, 3, 6, 7) or background via par4=5
- // Handler 0x26 (FUN_00407fcb): Turret HUD NUT via par3 (1-4)
- // Handler 0x19: Mixed turret mode, similar to 0x26
- //
- // Sound loading: par3 in range 21-47
-
- debug("Rebel2 IACT Opcode 8: handler=%d par2=%d par3=%d par4=%d (gameState=%d)",
- _rebelHandler, par2, par3, par4, _gameState);
-
- int64 startPos = b.pos();
- int64 remaining = (chunkSize > 0) ? chunkSize : (b.size() - startPos);
-
- // ===== Handler 7: FLY NUT Loading (Third-Person Ship) =====
- // FUN_0040c3cc case 6: par4 determines FLY sprite slot
- bool isHandler7FLY = (_rebelHandler == 7 && (par4 == 1 || par4 == 2 || par4 == 3 || par4 == 11));
- if (isHandler7FLY && remaining >= 14) {
- if (loadHandler7FlySprites(b, remaining, par4)) {
- b.seek(startPos);
- return;
- }
- b.seek(startPos);
- }
-
- // ===== Sound Loading (par3 21-47) =====
- if (par3 >= 21 && par3 <= 47) {
- debug("Rebel2 Opcode 8: Sound loading subcase %d (not implemented)", par3);
- // TODO: Implement sound loading via FUN_004118df equivalent
- return;
- }
-
- // ===== Scan for embedded ANIM data =====
- // Remaining handlers require finding ANIM tag in the stream
- debug("Rebel2 Opcode 8: Scanning for ANIM tag (startPos=%lld remaining=%lld)",
- (long long)startPos, (long long)remaining);
-
- if (remaining <= 0) {
- return;
- }
-
- int scanSize = (int)MIN<int64>(remaining, 65536);
- byte *scanBuf = (byte *)malloc(scanSize);
- if (!scanBuf) {
- return;
- }
-
- int bytesRead = b.read(scanBuf, scanSize);
- debug("Rebel2 Opcode 8: Read %d bytes for ANIM scan", bytesRead);
-
- // Find ANIM tag
- int animOffset = -1;
- for (int i = 0; i + 8 <= bytesRead; ++i) {
- if (READ_BE_UINT32(scanBuf + i) == MKTAG('A','N','I','M')) {
- animOffset = i;
- debug("Rebel2 Opcode 8: Found ANIM at offset %d", i);
- break;
- }
- }
-
- if (animOffset < 0) {
- debug("Rebel2 Opcode 8: No ANIM tag found");
- free(scanBuf);
- b.seek(startPos);
- return;
- }
-
- // Extract ANIM data
- uint32 animReportedSize = READ_BE_UINT32(scanBuf + animOffset + 4);
- int32 animDataSize = (int)MIN<int64>((int64)animReportedSize + 8, remaining - animOffset);
- if (animDataSize <= 0) {
- free(scanBuf);
- b.seek(startPos);
- return;
- }
-
- byte *animData = (byte *)malloc(animDataSize);
- if (!animData) {
- free(scanBuf);
- b.seek(startPos);
- return;
- }
-
- b.seek(startPos + animOffset);
- b.read(animData, animDataSize);
-
- bool handled = false;
-
- // ===== Handler 0x26/0x19: Turret HUD Overlays =====
- // FUN_00407fcb case 8: par3 1-4 for HUD NUT loading
- if (!handled && (_rebelHandler == 0x26 || _rebelHandler == 0x19)) {
- if (par3 >= 1 && par3 <= 4) {
- handled = loadTurretHudOverlay(animData, animDataSize, par3);
- }
- }
-
- // ===== Handler 8: POV Ship Sprites or Background =====
- // FUN_00401234 case 6: par4 selects POV NUT type (1,3,6,7) or background (5)
- // NOTE: par3 is always 0 for Handler 8; par4 contains the actual sprite type
- if (!handled && _rebelHandler == 8) {
- // Check for background loading first (par4=5)
- if (par4 == 5) {
- handled = loadLevel2Background(animData, animDataSize, renderBitmap);
- }
- // Check for POV NUT sprites (par4=1,3,6,7)
- else if (par4 == 1 || par4 == 3 || par4 == 6 || par4 == 7) {
- handled = loadHandler8ShipSprites(animData, animDataSize, par4);
- }
- }
-
- // ===== Handler 25 (0x19): Level 2 GRD Ship Sprites and Background =====
- // FUN_0041cadb case 6 (opcode 8): Uses PAR4 for switch selection
- // par4=1: GRD001 - Primary ship sprite -> DAT_00482240 / _grd001Sprite
- // par4=2: GRD002 - Secondary ship sprite -> DAT_00482238 / _grd002Sprite
- // par4=4: 350x230 corridor overlay -> DAT_00482268, draws immediately
- // par4=5: 320x200 background -> DAT_0048226c
- // par4=6: Overlay -> DAT_00482250, draws immediately
- // par4=7: Overlay -> DAT_00482248, draws immediately
- if (!handled && _rebelHandler == 25) {
- if (par4 == 1 || par4 == 2) {
- // GRD ship sprites - load into NutRenderer for per-frame rendering
- handled = loadHandler25GrdSprites(animData, animDataSize, par4);
- } else if (par4 == 5) {
- // Background (320x200) - stored for per-frame restoration
- handled = loadLevel2Background(animData, animDataSize, renderBitmap);
- } else if (par4 == 4 || par4 == 6 || par4 == 7) {
- // Overlays - draw immediately to renderBitmap
- // These complete the visual scene along with the background
- debug("Rebel2 Opcode 8: Handler 25 overlay par4=%d - drawing to screen", par4);
- loadEmbeddedSan(par4, animData, animDataSize, renderBitmap);
- handled = true;
- }
- }
-
- // ===== Fallback: Embedded SAN HUD overlays =====
- // For other cases, load as embedded SAN frame to HUD overlay slots
- if (!handled) {
- // Skip high-res data (par3 == 2, 4)
- if (par3 == 2 || par3 == 4) {
- debug("Rebel2 Opcode 8: Skipping high-res HUD par3=%d", par3);
- handled = true;
- } else {
- // Determine userId: Handler 0x19 uses par3, others use par4
- // Heuristic: if par3 is valid GRD range (1-13) and par4 is invalid, prefer par3
- int userId;
- bool usePar3 = (_rebelHandler == 0x19);
- if (!usePar3 && par3 >= 1 && par3 <= 13 && (par4 <= 0 || par4 >= 1000)) {
- usePar3 = true;
- }
- userId = usePar3 ? par3 : par4;
-
- // Skip audio tracks (userId >= 1000)
- if (userId > 0 && userId < 1000) {
- debug("Rebel2 Opcode 8: Loading embedded SAN HUD userId=%d (handler=%d par3=%d par4=%d)",
- userId, _rebelHandler, par3, par4);
- loadEmbeddedSan(userId, animData, animDataSize, renderBitmap);
- handled = true;
- }
- }
- }
-
- if (!handled) {
- debug("Rebel2 Opcode 8: Unhandled case - handler=%d par3=%d par4=%d", _rebelHandler, par3, par4);
- }
-
- free(animData);
- free(scanBuf);
- b.seek(startPos);
-}
-
-// ======================= Opcode 8 Helper Functions =======================
-// These helper functions are extracted from the original monolithic iactRebel2Opcode8
-// to improve code readability and match the retail FUN_* function structure.
-
-bool InsaneRebel2::loadHandler7FlySprites(Common::SeekableReadStream &b, int64 remaining, int16 par4) {
- // Handler 7 FLY NUT loading - FUN_0040c3cc case 6 (opcode 8)
- // IACT structure after par1-par4 (we're at offset +8):
- // +0-5 (6 bytes): additional header
- // +6-9 (4 bytes): NUT data size (little-endian)
- // +10+: NUT data
- //
- // par4 values (param_5[3] - 1 in assembly):
- // 1 -> case 0: FLY001 - Ship direction sprites (DAT_0047fee8)
- // 2 -> case 1: FLY003 - Targeting overlay (DAT_0047fef8)
- // 3 -> case 2: FLY002 - Laser fire sprites (DAT_0047fef0)
- // 11 -> case 10: FLY004 - High-res alternative (DAT_0047ff00)
-
- if (remaining < 14) {
- return false;
- }
-
- // Read additional header and size from fixed offset
- byte header[10];
- if (b.read(header, 10) != 10) {
- return false;
- }
-
- debug("Rebel2 loadHandler7FlySprites: header bytes: %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X",
- header[0], header[1], header[2], header[3], header[4],
- header[5], header[6], header[7], header[8], header[9]);
-
- // Size is at offset 14 from IACT start = bytes 6-9 of our header buffer
- uint32 nutSize = READ_LE_UINT32(header + 6);
- debug("Rebel2 loadHandler7FlySprites: par4=%d nutSize=%u remaining=%lld",
- par4, nutSize, (long long)remaining);
-
- if (nutSize == 0 || nutSize > (uint32)(remaining - 10)) {
- return false;
- }
-
- byte *nutData = (byte *)malloc(nutSize);
- if (!nutData) {
- return false;
- }
-
- int bytesRead = b.read(nutData, nutSize);
- debug("Rebel2 loadHandler7FlySprites: Read %d/%u bytes, first 16: %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X",
- bytesRead, nutSize,
- bytesRead > 0 ? nutData[0] : 0, bytesRead > 1 ? nutData[1] : 0,
- bytesRead > 2 ? nutData[2] : 0, bytesRead > 3 ? nutData[3] : 0,
- bytesRead > 4 ? nutData[4] : 0, bytesRead > 5 ? nutData[5] : 0,
- bytesRead > 6 ? nutData[6] : 0, bytesRead > 7 ? nutData[7] : 0,
- bytesRead > 8 ? nutData[8] : 0, bytesRead > 9 ? nutData[9] : 0,
- bytesRead > 10 ? nutData[10] : 0, bytesRead > 11 ? nutData[11] : 0,
- bytesRead > 12 ? nutData[12] : 0, bytesRead > 13 ? nutData[13] : 0,
- bytesRead > 14 ? nutData[14] : 0, bytesRead > 15 ? nutData[15] : 0);
-
- if (bytesRead != (int)nutSize) {
- warning("Rebel2 loadHandler7FlySprites: Short read! Got %d expected %u", bytesRead, nutSize);
- free(nutData);
- return false;
- }
-
- // Verify ANIM header
- if (bytesRead >= 8) {
- uint32 animTag = READ_BE_UINT32(nutData);
- if (animTag != MKTAG('A','N','I','M')) {
- warning("Rebel2 loadHandler7FlySprites: No ANIM tag! Data may be corrupted");
- free(nutData);
- return false;
- }
- }
-
- // Load as NUT
- NutRenderer *newNut = new NutRenderer(_vm, nutData, bytesRead);
- if (!newNut || newNut->getNumChars() <= 0) {
- debug("Rebel2 loadHandler7FlySprites: NUT load failed for par4=%d", par4);
- delete newNut;
- free(nutData);
- return false;
- }
-
- debug("Rebel2 loadHandler7FlySprites: Loaded FLY NUT par4=%d with %d sprites",
- par4, newNut->getNumChars());
-
- // Assign to appropriate slot based on par4 (matches FUN_0040c3cc case 6 switch)
- bool assigned = true;
- switch (par4) {
- case 1: // FLY001 - Ship direction sprites (35 frames)
- delete _flyShipSprite;
- _flyShipSprite = newNut;
- debug("Rebel2: _flyShipSprite set with %d sprites", newNut->getNumChars());
- break;
- case 2: // FLY003 - Targeting overlay
- delete _flyTargetSprite;
- _flyTargetSprite = newNut;
- break;
- case 3: // FLY002 - Laser fire sprites
- delete _flyLaserSprite;
- _flyLaserSprite = newNut;
- break;
- case 11: // FLY004 - High-res alternative
- delete _flyHiResSprite;
- _flyHiResSprite = newNut;
- break;
- default:
- delete newNut;
- assigned = false;
- break;
- }
-
- free(nutData);
- return assigned;
-}
-
-bool InsaneRebel2::loadTurretHudOverlay(byte *animData, int32 size, int16 par3) {
- // Handler 0x26/0x19 turret HUD overlay loading - FUN_00407fcb case 8
- // Resolution-dependent loading:
- // par3 == 1: Low-res primary HUD (DAT_0047fe78 / _hudOverlayNut)
- // par3 == 2: High-res primary HUD (skip in 320x200 mode)
- // par3 == 3: Low-res secondary HUD (DAT_0047fe80 / _hudOverlay2Nut)
- // par3 == 4: High-res secondary HUD (skip in 320x200 mode)
-
- if (!animData || size <= 0) {
- return false;
- }
-
- // ScummVM runs at 320x200 (low-res), skip high-res data
- if (par3 == 2 || par3 == 4) {
- debug("Rebel2 loadTurretHudOverlay: Skipping high-res HUD par3=%d (running in low-res mode)", par3);
- return true; // Successfully "handled" by skipping
- }
-
- if (par3 != 1 && par3 != 3) {
- return false; // Not a turret HUD slot
- }
-
- NutRenderer *newNut = new NutRenderer(_vm, animData, size);
- if (!newNut || newNut->getNumChars() <= 0) {
- debug("Rebel2 loadTurretHudOverlay: NUT load failed for par3=%d", par3);
- delete newNut;
- return false;
- }
-
- debug("Rebel2 loadTurretHudOverlay: Loaded turret HUD NUT par3=%d with %d sprites",
- par3, newNut->getNumChars());
-
- if (par3 == 1) {
- // Low-res primary HUD overlay
- delete _hudOverlayNut;
- _hudOverlayNut = newNut;
- } else { // par3 == 3
- // Low-res secondary HUD overlay
- delete _hudOverlay2Nut;
- _hudOverlay2Nut = newNut;
- }
-
- return true;
-}
-
-bool InsaneRebel2::loadHandler8ShipSprites(byte *animData, int32 size, int16 par4) {
- // Handler 8 ship POV NUT loading - FUN_00401234 case 6 (opcode 8)
- // par4 values (from IACT data offset +6, NOT par3 which is always 0):
- // 1: POV001 - Primary ship sprite (DAT_0047e010 / _shipSprite)
- // 3: POV004 - Secondary ship sprite (DAT_0047e028 / _shipSprite2)
- // 6: POV002 - Ship overlay 1 (DAT_0047e020 / _shipOverlay1)
- // 7: POV003 - Ship overlay 2 (DAT_0047e018 / _shipOverlay2)
-
- if (!animData || size <= 0) {
- return false;
- }
-
- // Only handle valid POV sprite slots
- if (par4 != 1 && par4 != 3 && par4 != 6 && par4 != 7) {
- return false;
- }
-
- NutRenderer *newNut = new NutRenderer(_vm, animData, size);
- if (!newNut || newNut->getNumChars() <= 0) {
- debug("Rebel2 loadHandler8ShipSprites: NUT load failed for par4=%d", par4);
- delete newNut;
- return false;
- }
-
- debug("Rebel2 loadHandler8ShipSprites: Loaded ship NUT par4=%d with %d sprites",
- par4, newNut->getNumChars());
-
- switch (par4) {
- case 1: // POV001 - Primary ship sprite
- delete _shipSprite;
- _shipSprite = newNut;
- break;
- case 3: // POV004 - Secondary ship sprite
- delete _shipSprite2;
- _shipSprite2 = newNut;
- break;
- case 6: // POV002 - Ship overlay 1
- delete _shipOverlay1;
- _shipOverlay1 = newNut;
- break;
- case 7: // POV003 - Ship overlay 2
- delete _shipOverlay2;
- _shipOverlay2 = newNut;
- break;
- default:
- delete newNut;
- return false;
- }
-
- return true;
-}
-
-bool InsaneRebel2::loadHandler25GrdSprites(byte *animData, int32 size, int16 par4) {
- // Handler 25 GRD ship NUT loading - FUN_0041cadb case 6 (opcode 8)
- // par4 values (from IACT data offset +6):
- // 1: GRD001 - Primary ship sprite (DAT_00482240 / _grd001Sprite)
- // 2: GRD002 - Secondary ship sprite (DAT_00482238 / _grd002Sprite)
-
- if (!animData || size <= 0) {
- return false;
- }
-
- // Only handle valid GRD sprite slots
- if (par4 != 1 && par4 != 2) {
- return false;
- }
-
- NutRenderer *newNut = new NutRenderer(_vm, animData, size);
- if (!newNut || newNut->getNumChars() <= 0) {
- debug("Rebel2 loadHandler25GrdSprites: NUT load failed for par4=%d", par4);
- delete newNut;
- return false;
- }
-
- debug("Rebel2 loadHandler25GrdSprites: Loaded GRD NUT par4=%d with %d sprites",
- par4, newNut->getNumChars());
-
- switch (par4) {
- case 1: // GRD001 - Primary ship sprite
- delete _grd001Sprite;
- _grd001Sprite = newNut;
- debug("Rebel2: _grd001Sprite set with %d sprites", newNut->getNumChars());
- break;
- case 2: // GRD002 - Secondary ship sprite
- delete _grd002Sprite;
- _grd002Sprite = newNut;
- debug("Rebel2: _grd002Sprite set with %d sprites", newNut->getNumChars());
- break;
- default:
- delete newNut;
- return false;
- }
-
- return true;
-}
-
-bool InsaneRebel2::loadLevel2Background(byte *animData, int32 size, byte *renderBitmap) {
- // Level 2 background loading from embedded ANIM - FUN_00401234 case 5
- // par4=5 contains the background image embedded as ANIM with FOBJ codec 3
- // Creates 320x200 buffer (DAT_0047e030 / _level2Background)
-
- if (!animData || size < 8) {
- return false;
- }
-
- debug("Rebel2 loadLevel2Background: Loading Level 2 background (animSize=%d)", size);
-
- // Allocate background buffer if needed (320x200 = 64000 bytes)
- if (_level2Background == nullptr) {
- _level2Background = (byte *)malloc(320 * 200);
- if (!_level2Background) {
- return false;
- }
- memset(_level2Background, 0, 320 * 200);
- }
-
- // Parse embedded ANIM to find FOBJ
- // Structure: ANIM tag at offset 0, AHDR, then FRME with FOBJ
- int animOffset = 0;
- if (READ_BE_UINT32(animData) == MKTAG('A','N','I','M')) {
- uint32 animSize = READ_BE_UINT32(animData + 4);
- debug("Rebel2 loadLevel2Background: Found ANIM tag, size=%u", animSize);
-
- // Skip ANIM header (8 bytes) + AHDR chunk
- if (size >= 16 && READ_BE_UINT32(animData + 8) == MKTAG('A','H','D','R')) {
- uint32 ahdrSize = READ_BE_UINT32(animData + 12);
- animOffset = 8 + 8 + ahdrSize; // After ANIM tag + AHDR
- debug("Rebel2 loadLevel2Background: AHDR size=%u, FRME expected at offset %d", ahdrSize, animOffset);
- }
- }
-
- // Look for FRME containing FOBJ
- bool foundBackground = false;
- for (int scanPos = animOffset; scanPos + 16 < size && !foundBackground; scanPos++) {
- if (READ_BE_UINT32(animData + scanPos) == MKTAG('F','R','M','E')) {
- int frmeSize = READ_BE_UINT32(animData + scanPos + 4);
- debug("Rebel2 loadLevel2Background: Found FRME at %d, size=%d", scanPos, frmeSize);
-
- for (int fobjPos = scanPos + 8; fobjPos + 18 < scanPos + 8 + frmeSize && fobjPos + 18 < size; fobjPos++) {
- if (READ_BE_UINT32(animData + fobjPos) == MKTAG('F','O','B','J')) {
- byte *fobjData = animData + fobjPos + 8;
-
- // FOBJ header: codec(2), x(2), y(2), w(2), h(2)
- int16 codec = READ_LE_INT16(fobjData);
- int16 fobjX = READ_LE_INT16(fobjData + 2);
- int16 fobjY = READ_LE_INT16(fobjData + 4);
- int16 fobjW = READ_LE_INT16(fobjData + 6);
- int16 fobjH = READ_LE_INT16(fobjData + 8);
-
- debug("Rebel2 loadLevel2Background: Found FOBJ: codec=%d pos=(%d,%d) size=%dx%d",
- codec, fobjX, fobjY, fobjW, fobjH);
-
- // Decode codec 3 (RLE) into background buffer
- // Use smushDecodeRLEOpaque to write ALL colors including color 0 (black).
- // The standard smushDecodeRLE treats color 0 as transparent, which causes
- // the background to appear as a "sketch" with black pixels missing.
- if (codec == 3 && fobjW > 0 && fobjH > 0 && fobjW <= 320 && fobjH <= 200) {
- byte *rleData = fobjData + 14; // Skip full 14-byte FOBJ header
- smushDecodeRLEOpaque(_level2Background, rleData, fobjX, fobjY, fobjW, fobjH, 320);
-
- debug("Rebel2 loadLevel2Background: Decoded Level 2 background (%dx%d at %d,%d)",
- fobjW, fobjH, fobjX, fobjY);
- _level2BackgroundLoaded = true;
- foundBackground = true;
-
- // Copy to render bitmap immediately if provided
- if (renderBitmap) {
- for (int by = 0; by < 200; by++) {
- memcpy(renderBitmap + by * 320, _level2Background + by * 320, 320);
- }
- debug("Rebel2 loadLevel2Background: Copied to renderBitmap");
- }
- }
- break;
- }
- }
- break;
- }
- }
-
- if (!foundBackground) {
- debug("Rebel2 loadLevel2Background: Failed to find/decode background FOBJ");
- }
-
- return foundBackground;
-}
-
-void InsaneRebel2::iactRebel2Opcode9(byte *renderBitmap, Common::SeekableReadStream &b, int16 par2, int16 par3, int16 par4) {
- // Opcode 9: Text/Subtitle Display via IACT chunk
- // Note: Most RA2 subtitles use TRES chunks handled by SmushPlayer::handleTextResource()
- // This opcode handles inline text in IACT chunks (less common)
- //
- // IACT Chunk Layout (par1-par4 already read by handleIACT):
- // +0x00 (2): opcode = 9 (par1, already read)
- // +0x02 (2): par2 (already read)
- // +0x04 (2): par3 (already read)
- // +0x06 (2): par4 (already read)
- // +0x08 onwards: Text data structure
- //
- // Text Data Structure:
- // +0x00 (2): X position
- // +0x02 (2): Y position
- // +0x04 (2): flags (bit 0=center, bit 1=right, bit 2=wrap, bit 3=difficulty gated)
- // +0x06 (2): clipX (when flag & 4)
- // +0x08 (2): clipY
- // +0x0A (2): clipW
- // +0x0C (2): clipH
- // +0x10 onwards: NUL-terminated text string
-
- int64 startPos = b.pos();
-
- // Check for "TRES" tag (0x54524553) indicating string resource lookup
- uint32 tag = b.readUint32BE();
-
- const char *textStr = nullptr;
- char textBuffer[512];
- int16 posX = 160; // Default center position
- int16 posY = 150; // Default bottom-ish position
- int16 textFlags = 1; // Default: center aligned
- int16 clipX = 16, clipY = 16, clipW = 288, clipH = 168;
-
- if (tag == MKTAG('T','R','E','S')) {
- // String resource lookup via TRES tag
- // The string index follows after the tag
- int32 stringIndex = b.readSint32LE();
-
- // Try to get string from SMUSH player's string resource
- if (_player && _player->getString(stringIndex)) {
- textStr = _player->getString(stringIndex);
- debug("Rebel2 Opcode 9: TRES string index=%d -> \"%s\"", stringIndex, textStr);
- } else {
- debug("Rebel2 Opcode 9: TRES string index=%d not found", stringIndex);
- return;
- }
-
- // After TRES + index, read positioning data
- // The remaining data contains X, Y, flags etc.
- if (b.size() - b.pos() >= 14) {
- posX = b.readSint16LE();
- posY = b.readSint16LE();
- textFlags = b.readSint16LE();
- clipX = b.readSint16LE();
- clipY = b.readSint16LE();
- clipW = b.readSint16LE();
- clipH = b.readSint16LE();
- }
- } else {
- // Inline text data - go back and read positioning structure
- b.seek(startPos);
-
- // Read text data structure
- posX = b.readSint16LE(); // +0x00
- posY = b.readSint16LE(); // +0x02
- textFlags = b.readSint16LE(); // +0x04
- clipX = b.readSint16LE(); // +0x06
- clipY = b.readSint16LE(); // +0x08
- clipW = b.readSint16LE(); // +0x0A
- clipH = b.readSint16LE(); // +0x0C
- b.skip(2); // +0x0E padding
-
- // Read inline text string (NUL-terminated)
- int textLen = 0;
- while (textLen < (int)sizeof(textBuffer) - 1) {
- byte ch = b.readByte();
- if (ch == 0 || b.eos()) break;
- textBuffer[textLen++] = ch;
- }
- textBuffer[textLen] = '\0';
- textStr = textBuffer;
-
- debug("Rebel2 Opcode 9: Inline text at (%d,%d) flags=0x%x -> \"%s\"", posX, posY, textFlags, textStr);
- }
-
- if (!textStr || textStr[0] == '\0') {
- debug("Rebel2 Opcode 9: Empty text string, skipping");
- return;
- }
-
- // Check difficulty gate (flag bit 3 = 0x08)
- // If set, only show text if difficulty check passes (we skip this check for simplicity)
- // In retail: FUN_00425d30(0) is called
-
- // Get render buffer dimensions
- int width = (_player && _player->_width > 0) ? _player->_width : 320;
- int height = (_player && _player->_height > 0) ? _player->_height : 200;
-
- // Apply coordinate clamping (from FUN_004033cf disassembly)
- // Low-res: X clamped to [16, 304], Y clamped to [16, 196]
- if (posX < 16) posX = 16;
- if (posX > 304) posX = 304;
- if (posY < 16) posY = 16;
- if (posY > 196) posY = 196;
-
- // Use the message font loaded during initialization (DIHIFONT.NUT)
- if (!_rebelMsgFont) {
- debug("Rebel2 Opcode 9: No message font loaded (_rebelMsgFont is null)");
- return;
- }
-
- // Calculate clipping rectangle
- if (!(textFlags & 0x04)) {
- // No clip rect specified, use default full-screen clip
- clipX = 0;
- clipY = 0;
- clipW = width;
- clipH = height;
- }
-
- Common::Rect clipRect(
- MAX<int>(0, clipX),
- MAX<int>(0, clipY),
- MIN<int>(clipX + clipW, width),
- MIN<int>(clipY + clipH, height)
- );
-
- // Determine text alignment flags
- TextStyleFlags styleFlags = kStyleAlignLeft;
- if (textFlags & 0x01) {
- styleFlags = kStyleAlignCenter;
- } else if (textFlags & 0x02) {
- styleFlags = kStyleAlignRight;
- }
- if (textFlags & 0x04) {
- styleFlags = (TextStyleFlags)(styleFlags | kStyleWordWrap);
- }
-
- // Use white color (index 255) for subtitle text
- // The original uses colors from the palette, commonly white or yellow for subtitles
- int16 textColor = 255;
-
- // RA2 fonts (like DIHIFONT.NUT) have only 58 characters starting at ASCII 32 (space).
- // We need to convert ASCII codes to font indices by subtracting 32.
- // Character mapping: font index = ASCII code - 32
- // So 'D' (68) becomes index 36, 'A' (65) becomes index 33, etc.
- // IMPORTANT: Skip format codes (^f00, ^c255, ^l) which TextRenderer parses as raw ASCII.
- char convertedText[512];
- int srcLen = strlen(textStr);
- int dstIdx = 0;
- int numChars = _rebelMsgFont->getNumChars();
-
- for (int i = 0; i < srcLen && dstIdx < (int)sizeof(convertedText) - 1; i++) {
- byte ch = (byte)textStr[i];
-
- // Check for format codes (^f, ^c, ^l) - keep them as raw ASCII
- if (ch == '^' && i + 1 < srcLen) {
- byte next = (byte)textStr[i + 1];
- if (next == 'f' && i + 3 < srcLen) {
- // ^fXX - font switch (4 chars total)
- convertedText[dstIdx++] = textStr[i++]; // ^
- convertedText[dstIdx++] = textStr[i++]; // f
- convertedText[dstIdx++] = textStr[i++]; // X
- convertedText[dstIdx++] = textStr[i]; // X
- continue;
- } else if (next == 'c' && i + 4 < srcLen) {
- // ^cXXX - color switch (5 chars total)
- convertedText[dstIdx++] = textStr[i++]; // ^
- convertedText[dstIdx++] = textStr[i++]; // c
- convertedText[dstIdx++] = textStr[i++]; // X
- convertedText[dstIdx++] = textStr[i++]; // X
- convertedText[dstIdx++] = textStr[i]; // X
- continue;
- } else if (next == 'l') {
- // ^l - line break marker (2 chars)
- convertedText[dstIdx++] = textStr[i++]; // ^
- convertedText[dstIdx++] = textStr[i]; // l
- continue;
- } else if (next == '^') {
- // ^^ - escaped caret (becomes single ^)
- i++; // Skip first ^
- // Fall through to convert second ^ as normal char
- ch = '^';
- }
- }
-
- // Convert regular characters from ASCII to font index
- // First convert lowercase to uppercase (the font likely only has uppercase)
- if (ch >= 'a' && ch <= 'z') {
- ch = ch - 'a' + 'A'; // Convert to uppercase
- }
-
- if (ch >= 32 && ch < (byte)(32 + numChars)) {
- convertedText[dstIdx++] = ch - 32; // Convert ASCII to font index
- } else if (ch == '\n' || ch == '\r') {
- convertedText[dstIdx++] = ch; // Keep control characters as-is
- } else {
- convertedText[dstIdx++] = 0; // Replace invalid characters with space (index 0)
- }
- }
- convertedText[dstIdx] = '\0';
-
- // Draw the text string (with converted character indices)
- if (textFlags & 0x04) {
- // Word-wrapped text
- _rebelMsgFont->drawStringWrap(convertedText, renderBitmap, clipRect, posX, posY, textColor, styleFlags);
- } else {
- // Single-line text
- _rebelMsgFont->drawString(convertedText, renderBitmap, clipRect, posX, posY, textColor, styleFlags);
- }
-
- debug("Rebel2 Opcode 9: Rendered subtitle at (%d,%d) flags=0x%x clip=(%d,%d,%d,%d)",
- posX, posY, textFlags, clipX, clipY, clipW, clipH);
-}
-
-void InsaneRebel2::enemyUpdate(byte *renderBitmap, Common::SeekableReadStream &b, int16 par2, int16 par3, int16 par4) {
- // Opcode 4: Enemy position update
- // Read 5 shorts from the stream (offset +8 through +16)
- int16 enemyId = b.readSint16LE(); // Offset +8
- int16 x = b.readSint16LE(); // Offset +10 (0x0A)
-
- // If enemy is disabled in bit table, skip update
- bool disabled = isBitSet(enemyId);
-
- int16 y = b.readSint16LE(); // Offset +12 (0x0C)
- int16 w = b.readSint16LE(); // Offset +14 (0x0E) - Width
- int16 h = b.readSint16LE(); // Offset +16 (0x10) - Height
-
- // If disabled, stop processing this object
- if (disabled) {
- // debug("Rebel2: Skipping Opcode 4 for disabled enemy ID=%d", enemyId);
- return;
- }
-
- // The disassembly shows half-width/half-height are used for centering:
- // halfW = w >> 1
- // halfH = h >> 1
- // centerX = x + halfW
- // centerY = y + halfH
- // But for drawing the bounding box, we want the top-left corner (x, y) and full dimensions.
-
- // Update enemy list for hit detection
- // Enemy type comes from par4 (IACT offset +6), NOT par3 (offset +4).
- // In the original (FUN_004028C5/FUN_0041E7C2): sVar5/sVar2 = *(short *)(*local + 6)
- // This maps to par4 (userId field). Used for DAT_0047ab98 wave state bitmask:
- // DAT_0047ab98 |= 1 << (type & 0x1f)
- debug(5, "Rebel2 Opcode4: handler=%d enemyId=%d par2=%d par3=%d par4/type=%d pos=(%d,%d) size=(%d,%d)",
- _rebelHandler, enemyId, par2, par3, par4, x, y, w, h);
-
- bool found = false;
- Common::List<enemy>::iterator it;
- for (it = _enemies.begin(); it != _enemies.end(); ++it) {
- if (it->id == enemyId) {
- it->rect = Common::Rect(x, y, x + w, y + h);
- it->type = par4; // Enemy type from IACT offset +6 (userId)
- // Only re-activate if not destroyed
- if (!it->destroyed) {
- it->active = true;
- }
- found = true;
- break;
- }
- }
- if (!found) {
- init_enemyStruct(enemyId, x, y, w, h, true, false, -1, par4);
- }
-}
-
-void InsaneRebel2::init_enemyStruct(int id, int32 x, int32 y, int32 w, int32 h, bool active, bool destroyed, int32 explosionFrame, int type) {
- enemy e;
- e.id = id;
- e.type = type;
- e.rect = Common::Rect(x, y, x + w, y + h);
- e.active = active;
- e.destroyed = destroyed;
- e.explosionFrame = explosionFrame;
- e.savedBackground = nullptr;
- e.savedBgWidth = 0;
- e.savedBgHeight = 0;
- _enemies.push_back(e);
-}
-
-// ======================= Embedded Frame Codec Decoders =======================
-// These implement the retail codec functions FUN_0042BD60, FUN_0042BBF0, FUN_0042B5F0
-
-void InsaneRebel2::decodeCodec21(byte *dst, const byte *src, int width, int height) {
- // Codec 21/44: Line Update codec (FUN_0042BD60)
- // Format: each line has 2-byte size header, then pairs of (skip, count+1, literal_bytes)
- for (int row = 0; row < height; row++) {
- int lineDataSize = READ_LE_UINT16(src);
- src += 2;
- const byte *lineEnd = src + lineDataSize;
- byte *lineDst = dst + row * width;
- int x = 0;
-
- while (src < lineEnd && x < width) {
- int skip = READ_LE_UINT16(src);
- src += 2;
- x += skip;
- if (src >= lineEnd) break;
-
- int count = READ_LE_UINT16(src) + 1;
- src += 2;
- while (count-- > 0 && x < width && src < lineEnd) {
- lineDst[x++] = *src++;
- }
- }
- src = lineEnd;
- }
-}
-
-void InsaneRebel2::decodeCodec23(byte *dst, const byte *src, int width, int height, int dataSize) {
- // Codec 23: Skip/Copy with embedded RLE (FUN_0042BBF0)
- // Format: each line has 2-byte size, then pairs of (skip, runSize, RLE_data)
- const byte *dataEnd = src + dataSize;
-
- for (int row = 0; row < height && src < dataEnd; row++) {
- int lineDataSize = READ_LE_UINT16(src);
- src += 2;
- const byte *lineEnd = src + lineDataSize;
- byte *lineDst = dst + row * width;
- int x = 0;
-
- while (src < lineEnd && x < width) {
- int skip = READ_LE_UINT16(src);
- src += 2;
- x += skip;
- if (src >= lineEnd || x >= width) break;
-
- int runSize = READ_LE_UINT16(src);
- src += 2;
-
- // Decode RLE within this run
- const byte *runEnd = src + runSize;
- while (src < runEnd && x < width) {
- byte code = *src++;
- int num = (code >> 1) + 1;
- if (num > width - x) num = width - x;
-
- if (code & 1) {
- // RLE run
- byte color = (src < runEnd) ? *src++ : 0;
- for (int i = 0; i < num && x < width; i++) {
- lineDst[x++] = color;
- }
- } else {
- // Literal run
- for (int i = 0; i < num && x < width && src < runEnd; i++) {
- lineDst[x++] = *src++;
- }
- }
- }
- src = runEnd;
- }
- src = lineEnd;
- }
-}
-
-void InsaneRebel2::decodeCodec45(byte *dst, const byte *src, int width, int height, int dataSize) {
- // Codec 45: RA2-specific BOMP RLE with variable header (FUN_0042B5F0)
- // May have a 6-byte sub-header starting with "01 FE"
-
- debug("Rebel2: Codec 45 first 20 bytes: %02X %02X %02X %02X %02X %02X | %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X",
- src[0], src[1], src[2], src[3], src[4], src[5], src[6], src[7],
- src[8], src[9], src[10], src[11], src[12], src[13], src[14], src[15],
- src[16], src[17], src[18], src[19]);
-
- // Probe for header offset
- int headerSkip = 0;
- bool foundValidOffset = false;
-
- // Check for known 6-byte header pattern: 01 FE XX XX XX XX
- if (dataSize > 6 && src[0] == 0x01 && src[1] == 0xFE) {
- headerSkip = 6;
- debug("Rebel2: Codec 45 found 01 FE header, skipping 6 bytes");
- foundValidOffset = true;
- }
-
- // If no known header found, probe offsets 0, 2, 4, 6 to find valid RLE start
- if (!foundValidOffset) {
- for (int testOffset = 0; testOffset <= 6 && testOffset + 2 <= dataSize; testOffset += 2) {
- int testLineSize = READ_LE_UINT16(src + testOffset);
- // A valid first line size should be: > 0, <= width*2
- if (testLineSize > 0 && testLineSize <= width * 2 && testLineSize < dataSize - testOffset) {
- // Validate by summing line sizes
- int sumTest = 0;
- int linesTest = 0;
- const byte *testPtr = src + testOffset;
- bool validSum = true;
-
- while (linesTest < height && testPtr + 2 <= src + dataSize) {
- int ls = READ_LE_UINT16(testPtr);
- if (ls <= 0 || ls > width * 2) {
- validSum = false;
- break;
- }
- sumTest += ls + 2;
- testPtr += ls + 2;
- linesTest++;
- }
-
- // Accept if we got close to expected number of lines
- if (validSum && linesTest >= height - 1) {
- headerSkip = testOffset;
- foundValidOffset = true;
- debug("Rebel2: Codec 45 found valid RLE at offset %d (tested %d lines)", testOffset, linesTest);
- break;
- }
- }
- }
- }
-
- if (!foundValidOffset) {
- debug("Rebel2: Codec 45 couldn't find valid RLE offset, using offset 0");
- }
-
- const byte *srcPtr = src + headerSkip;
- const byte *dataEnd = src + dataSize;
-
- // Check if this is per-line RLE or continuous RLE
- int firstVal = READ_LE_UINT16(srcPtr);
- bool perLineMode = (firstVal > 0 && firstVal <= width * 2);
-
- if (perLineMode) {
- debug("Rebel2: Codec 45 using per-line RLE (firstLineSize=%d)", firstVal);
- for (int row = 0; row < height && srcPtr < dataEnd; row++) {
- int lineSize = READ_LE_UINT16(srcPtr);
- srcPtr += 2;
- if (lineSize <= 0 || lineSize > (int)(dataEnd - srcPtr)) break;
-
- const byte *lineEnd = srcPtr + lineSize;
- byte *rowDst = dst + row * width;
- int x = 0;
-
- while (srcPtr < lineEnd && x < width) {
- byte ctrl = *srcPtr++;
- int count = (ctrl >> 1) + 1;
- if (ctrl & 1) {
- byte color = (srcPtr < lineEnd) ? *srcPtr++ : 0;
- for (int i = 0; i < count && x < width; i++) rowDst[x++] = color;
- } else {
- for (int i = 0; i < count && x < width && srcPtr < lineEnd; i++)
- rowDst[x++] = *srcPtr++;
- }
- }
- srcPtr = lineEnd;
- }
- } else {
- // Continuous BOMP RLE (no per-line headers)
- debug("Rebel2: Codec 45 using continuous BOMP RLE");
- for (int row = 0; row < height && srcPtr < dataEnd; row++) {
- byte *rowDst = dst + row * width;
- int x = 0;
-
- while (x < width && srcPtr < dataEnd) {
- byte ctrl = *srcPtr++;
- int count = (ctrl >> 1) + 1;
-
- if (ctrl & 1) {
- // RLE fill
- byte color = (srcPtr < dataEnd) ? *srcPtr++ : 0;
- for (int i = 0; i < count && x < width; i++) {
- rowDst[x++] = color;
- }
- } else {
- // Literal copy
- for (int i = 0; i < count && x < width && srcPtr < dataEnd; i++) {
- rowDst[x++] = *srcPtr++;
- }
- }
- }
- }
- }
-
- // Count non-zero pixels for debug
- int nonZero = 0;
- for (int i = 0; i < width * height; i++) {
- if (dst[i] != 0) nonZero++;
- }
- debug("Rebel2: Decoded codec 45: %dx%d, %d non-zero (%d%%)",
- width, height, nonZero, (nonZero * 100) / (width * height));
-}
-
-void InsaneRebel2::renderEmbeddedFrame(byte *renderBitmap, const EmbeddedSanFrame &frame, int userId) {
- // Render the decoded embedded frame to the video buffer
- // Skip immediate draw for handlers that render HUD during post-processing:
- // - Handler 7/8: Ship direction sprites selected based on direction
- // - Handler 0x26/0x19: Cockpit HUD positioned based on mouse/crosshair
- //
- // Exception: Handler 25 (0x19) background overlays (par4/userId=4, 6, 7) should draw immediately.
- // These complete the visual scene and are NOT positioned by mouse/crosshair.
- bool skipImmediateDraw = (_rebelHandler == 7 || _rebelHandler == 8 ||
- _rebelHandler == 0x26 || _rebelHandler == 0x19);
-
- // Handler 25 overlays:
- // - userId 4 (corridor overlay): Draw during procPostRendering at view offset, NOT immediately
- // - userId 6, 7 (static overlays): Draw immediately (they don't move)
- if (_rebelHandler == 0x19 && (userId == 6 || userId == 7)) {
- skipImmediateDraw = false;
- debug("Rebel2: Handler 25 static overlay userId=%d - forcing immediate draw", userId);
- }
- // userId 4 should NOT draw immediately - it will be drawn at view offset each frame
-
- if (!frame.valid || !renderBitmap || skipImmediateDraw) {
- if (skipImmediateDraw && frame.valid) {
- debug("Rebel2: Skipped immediate draw for Handler %d HUD %d (will render during post-processing)",
- _rebelHandler, userId);
- }
- return;
- }
-
- int pitch = (_player && _player->_width > 0) ? _player->_width : 320;
- int bufHeight = (_player && _player->_height > 0) ? _player->_height : 200;
-
- for (int y = 0; y < frame.height && (frame.renderY + y) < bufHeight; y++) {
- for (int x = 0; x < frame.width && (frame.renderX + x) < pitch; x++) {
- byte pixel = frame.pixels[y * frame.width + x];
- if (pixel != 0 && pixel != 231) { // 0 and 231 = transparent
- int destX = frame.renderX + x;
- int destY = frame.renderY + y;
- if (destX >= 0 && destY >= 0) {
- renderBitmap[destY * pitch + destX] = pixel;
- }
- }
- }
- }
- debug("Rebel2: Rendered embedded HUD %d at (%d,%d)", userId, frame.renderX, frame.renderY);
-}
-
-void InsaneRebel2::loadEmbeddedSan(int userId, byte *animData, int32 size, byte *renderBitmap) {
- // Validate userId - Level 3 uses slots 0-11, allow up to 15 for safety
- if (userId < 0 || userId > 15 || !animData || size < 32) {
- debug("Rebel2: Invalid embedded SAN: userId=%d, size=%d", userId, size);
- return;
- }
-
- Common::MemoryReadStream stream(animData, size);
-
- // Read ANIM header
- uint32 animTag = stream.readUint32BE();
- if (animTag != MKTAG('A','N','I','M')) {
- debug("Rebel2: Embedded SAN missing ANIM tag, got 0x%08X", animTag);
- return;
- }
- uint32 animSize = stream.readUint32BE();
- debug("Rebel2: Parsing embedded ANIM: userId=%d, reported size=%u, actual=%d", userId, animSize, size - 8);
-
- // Iterate through chunks to find FRME -> FOBJ
- while (!stream.eos() && stream.pos() < size) {
- uint32 tag = stream.readUint32BE();
- uint32 chunkSize = stream.readUint32BE();
- int32 nextChunkPos = stream.pos() + chunkSize;
-
- if (tag == MKTAG('F','R','M','E')) {
- // Iterate sub-chunks in FRME
- while (stream.pos() < nextChunkPos && !stream.eos()) {
- uint32 subTag = stream.readUint32BE();
- uint32 subSize = stream.readUint32BE();
- int32 nextSubPos = stream.pos() + subSize;
-
- if (subTag == MKTAG('F','O','B','J')) {
- // Found FOBJ - Embedded HUD Frame
- // Dump raw FOBJ bytes for analysis
- int32 fobjStart = stream.pos();
- byte rawHeader[20];
- int headerBytesToRead = MIN((int)subSize, 20);
- stream.read(rawHeader, headerBytesToRead);
- stream.seek(fobjStart); // Reset to read normally
-
- debug("Rebel2: Raw FOBJ header (%d bytes): %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X",
- headerBytesToRead,
- rawHeader[0], rawHeader[1], rawHeader[2], rawHeader[3],
- rawHeader[4], rawHeader[5], rawHeader[6], rawHeader[7],
- rawHeader[8], rawHeader[9], rawHeader[10], rawHeader[11],
- rawHeader[12], rawHeader[13], rawHeader[14], rawHeader[15],
- rawHeader[16], rawHeader[17], rawHeader[18], rawHeader[19]);
-
- // Read FOBJ header
- int codec = stream.readUint16LE();
- int left = stream.readUint16LE();
- int top = stream.readUint16LE();
- int width = stream.readUint16LE();
- int height = stream.readUint16LE();
- stream.readUint16LE(); // unknown
- stream.readUint16LE(); // unknown
-
- debug("Rebel2: Embedded HUD frame: userId=%d, %dx%d at (%d,%d), codec=%d",
- userId, width, height, left, top, codec);
-
- // Skip high-resolution frames - ScummVM runs at 320x200
- // If frame dimensions exceed low-res screen size, it's high-res data
- if (width > 400 || height > 250) {
- debug("Rebel2: SKIPPING high-res embedded frame: userId=%d, %dx%d (exceeds 400x250)",
- userId, width, height);
- stream.seek(nextSubPos);
- continue;
- }
-
- // Allocate storage for the decoded frame
- EmbeddedSanFrame &frame = _rebelEmbeddedHud[userId];
-
- if (width > 0 && height > 0 && width <= 800 && height <= 480) {
- if (frame.width != width || frame.height != height || !frame.pixels) {
- free(frame.pixels);
- frame.pixels = (byte *)malloc(width * height);
- frame.width = width;
- frame.height = height;
- }
- // Clear buffer before decode (important for delta codecs)
- memset(frame.pixels, 0, width * height);
-
- // Update render position from FOBJ header
- frame.renderX = left;
- frame.renderY = top;
-
- // Read the raw FOBJ data
- int32 dataSize = subSize - 14;
- if (dataSize > 0) {
- byte *fobjData = (byte *)malloc(dataSize);
- stream.read(fobjData, dataSize);
-
- // Decode based on codec - use extracted helper functions (FUN_0042BD60, etc.)
- if (codec == 1 || codec == 3) {
- // Codec 1/3: RLE - use existing decoder (FUN_0042C590)
- smushDecodeRLE(frame.pixels, fobjData, 0, 0, width, height, width);
- frame.valid = true;
- debug("Rebel2: Decoded embedded HUD (codec %d/RLE): %dx%d", codec, width, height);
- } else if (codec == 20) {
- // Codec 20: Uncompressed (FUN_0042C400)
- smushDecodeUncompressed(frame.pixels, fobjData, 0, 0, width, height, width);
- frame.valid = true;
- debug("Rebel2: Decoded embedded HUD (codec 20/raw): %dx%d", width, height);
- } else if (codec == 21 || codec == 44) {
- // Codec 21/44: Line update (FUN_0042BD60)
- decodeCodec21(frame.pixels, fobjData, width, height);
- frame.valid = true;
- debug("Rebel2: Decoded embedded HUD (codec %d/line update): %dx%d", codec, width, height);
- } else if (codec == 45) {
- // Codec 45: RA2-specific BOMP RLE (FUN_0042B5F0)
- decodeCodec45(frame.pixels, fobjData, width, height, dataSize);
- frame.valid = true;
- } else if (codec == 23) {
- // Codec 23: Skip/copy with embedded RLE (FUN_0042BBF0)
- decodeCodec23(frame.pixels, fobjData, width, height, dataSize);
- frame.valid = true;
- debug("Rebel2: Decoded embedded HUD (codec 23/skip-RLE): %dx%d", width, height);
- } else {
- debug("Rebel2: TODO: Decode codec %d for embedded HUD", codec);
- frame.valid = false;
- }
-
- // Count non-zero pixels to verify frame has content
- if (frame.valid) {
- int nonZeroPixels = 0;
- for (int i = 0; i < width * height; i++) {
- if (frame.pixels[i] != 0) nonZeroPixels++;
- }
- debug("Rebel2: Frame userId=%d has %d non-zero pixels (%d%%)",
- userId, nonZeroPixels, (nonZeroPixels * 100) / (width * height));
- }
-
- // Render the decoded frame to the video buffer
- renderEmbeddedFrame(renderBitmap, frame, userId);
-
- free(fobjData);
- }
- }
-
- // Done with FOBJ - assume only one relevant frame per embedded SAN
- stream.seek(nextChunkPos);
- goto end_parsing;
- } else {
- // Skip other sub-chunks (AHDR inside FRME?) or padding
- stream.seek(nextSubPos);
- if (subSize & 1) stream.skip(1);
- }
- }
- } else {
- // Skip non-FRME chunks (AHDR, etc at top level)
- stream.seek(nextChunkPos);
- if (chunkSize & 1) stream.skip(1);
- }
- }
-
- debug("Rebel2: No FOBJ found in embedded SAN userId=%d", userId);
-
-end_parsing:;
-}
-
-// Spawn explosion into the shared 5-slot system.
-// In the original, each handler has its own spawn logic inside its enemy processing function:
-// Handler 0x26: FUN_40A2E0 (0x40A2E0) â spawns in slot arrays DAT_0044368e[]
-// Handler 8: FUN_4028C5 (0x4028C5) â spawns in slot arrays DAT_0043f854[]
-// Handler 7: FUN_40F628 (0x40F628) â spawns in slot arrays DAT_00443770[]
-// Handler 25: FUN_41E7C2 (0x41E7C2) â spawns in slot arrays DAT_0045792c[]
-// All share the same logic: find first free slot (counter==0), set counter=10,
-// scale=objectHalfWidth, position=enemy center, velocity=0.
-void InsaneRebel2::spawnExplosion(int x, int y, int objectHalfWidth) {
- for (int i = 0; i < 5; i++) {
- if (!_explosions[i].active || _explosions[i].counter <= 0) {
- _explosions[i].active = true;
- _explosions[i].counter = 10;
- _explosions[i].x = x;
- _explosions[i].y = y;
- _explosions[i].scale = objectHalfWidth;
- break;
- }
- }
-}
-
-// Get max shot duration from level table (DAT_0047e0f0 indexed by DAT_0047a7fa/DAT_0047a7f8)
-// For now, use a reasonable default based on observed values
-int16 InsaneRebel2::getShotMaxDuration() {
- // The original uses: DAT_0047e0f0 + DAT_0047a7fa * 0x242 + DAT_0047a7f8 * 0x22
- // Typical values observed: 2-5 frames
- return 4;
-}
-
-// Dispatcher - calls appropriate spawn function based on current handler
-void InsaneRebel2::spawnShot(int x, int y) {
- switch (_rebelHandler) {
- case 0x26: // Turret
- spawnTurretShot(x, y);
- break;
- case 8: // Vehicle
- spawnVehicleShot(x, y);
- break;
- case 7: // Space combat
- spawnSpaceShot(x, y);
- break;
- case 25: // Speeder bike - uses turret shot array with different gun position
- spawnHandler25Shot(x, y);
- break;
- default:
- // Legacy fallback
- for (int i = 0; i < 2; i++) {
- if (!_shots[i].active) {
- _shots[i].active = true;
- _shots[i].counter = getShotMaxDuration();
- _shots[i].x = x + _viewX;
- _shots[i].y = y + _viewY;
- break;
- }
- }
- break;
- }
-}
-
-// Handler 0x26 Turret shot spawn (based on FUN_4089AB lines 127-140)
-void InsaneRebel2::spawnTurretShot(int x, int y) {
- for (int i = 0; i < 2; i++) {
- if (_turretShots[i].counter == 0) {
- // FUN_0041189e(-(ushort)(DAT_004436de == 5) & 7, i + 1, 0x7f, 0, 0)
- // levelType 5: BLAST.SAD (slot 0), otherwise: TBLAST.SAD (slot 7)
- playSfx((_rebelLevelType == 5) ? 0 : 7, 127, 0);
-
- _turretShots[i].counter = getShotMaxDuration();
- _turretShots[i].seqNum = _turretShotSeqCounter;
- _turretShotSeqCounter++;
- _turretShots[i].targetX = x + _viewX; // DAT_0044366e in original
- _turretShots[i].targetY = y + _viewY; // DAT_00443670 in original
- break;
- }
- }
-}
-
-// Handler 8 Vehicle shot spawn (based on FUN_401CCF lines 65-69)
-void InsaneRebel2::spawnVehicleShot(int x, int y) {
- for (int i = 0; i < 2; i++) {
- if (_vehicleShots[i].counter == 0) {
- // FUN_0041189e(6, local_c + 1, 0x7f, 0, 0) â HBLAST.SAD
- playSfx(6, 127, 0);
- _vehicleShots[i].counter = getShotMaxDuration();
- _vehicleShots[i].targetX = x + _viewX;
- _vehicleShots[i].targetY = y + _viewY;
- break;
- }
- }
-}
-
-// Handler 25 on-foot shot spawn (based on FUN_0041db5e lines 170-190)
-// Gun position computed from GRD002 character sprite.
-// Original stores: DAT_0045791c[i] = gunOffsetTable[spriteIdx] + DAT_00457910 - DAT_0045790c
-// DAT_00457920[i] = gunYTable[spriteIdx] + DAT_00457912 - DAT_0045790e
-// Render adds view offset back, so screen gun = table[idx] + spriteOffset.
-void InsaneRebel2::spawnHandler25Shot(int x, int y) {
- // Handler 25 can only shoot when uncovered (damage == 0)
- if (_rebelDamageLevel != 0) {
- return; // Can't shoot while taking cover
- }
-
- for (int i = 0; i < 2; i++) {
- if (_turretShots[i].counter == 0) {
- // FUN_0041189e(6, local_1c + 1, 0x7f, 0, 0) â HBLAST.SAD
- playSfx(6, 127, 0);
-
- _turretShots[i].counter = getShotMaxDuration();
- _turretShots[i].seqNum = _turretShotSeqCounter;
- _turretShotSeqCounter++;
-
- // Target position is where player clicked (screen coords)
- _turretShots[i].targetX = x;
- _turretShots[i].targetY = y;
-
- // Compute gun position from GRD002 character sprite.
- // Original uses per-direction lookup tables DAT_004578a6/DAT_004578c6.
- // We approximate from the NUT sprite center + directional offset.
- if (_grd002Sprite && _grd002Sprite->getNumChars() > 0) {
- // Compute current sprite index (same logic as renderHandler25Ship)
- int spriteIdx;
- if (_rebelDamageLevel == 0) {
- // Uncovered: compute from crosshair position zones
- int16 areaLeft = (_corridorLeftX > 0) ? _corridorLeftX : 0;
- int16 areaRight = (_corridorRightX > 0) ? _corridorRightX : 320;
- int16 areaTop = (_corridorTopY > 0) ? _corridorTopY : 0;
- int16 areaBottom = (_corridorBottomY > 0) ? _corridorBottomY : 180;
- int areaWidth = areaRight - areaLeft;
- int areaHeight = areaBottom - areaTop;
- int zoneWidth = (areaWidth > 0) ? (areaWidth + 3) / 4 : 80;
- int zoneHeight = (areaHeight > 0) ? areaHeight / 2 : 90;
- int xZone = (zoneWidth > 0) ? ((zoneWidth / 2) + (x - areaLeft)) / zoneWidth : 2;
- int yZone = (zoneHeight > 0) ? ((zoneHeight / 2) + (y - areaTop)) / zoneHeight : 0;
- if (xZone < 0) xZone = 0;
- if (xZone > 4) xZone = 4;
- if (yZone < 0) yZone = 0;
- if (yZone > 1) yZone = 1;
- if (_rebelFlightDir == (yZone & 1)) {
- xZone = 4 - xZone;
- }
- spriteIdx = yZone * 5 + xZone + 5;
- } else {
- spriteIdx = (_rebelFlightDir == 0) ? (5 - _rebelDamageLevel) : (25 - _rebelDamageLevel);
- }
- int numSprites = _grd002Sprite->getNumChars();
- if (spriteIdx < 0) spriteIdx = 0;
- if (spriteIdx >= numSprites) spriteIdx = numSprites - 1;
-
- // Get sprite rendering position (same as in renderHandler25Ship)
- int16 spriteXOffset = _grd002Sprite->getCharXOffset(spriteIdx);
- int16 spriteYOffset = _grd002Sprite->getCharYOffset(spriteIdx);
- int spriteW = _grd002Sprite->getCharWidth(spriteIdx);
- int spriteH = _grd002Sprite->getCharHeight(spriteIdx);
- bool shouldMirror = (_rebelFlightDir != 0 && _rebelDamageLevel == 0);
-
- int drawX;
- if (shouldMirror) {
- drawX = _rebelViewOffset2X + (320 - spriteW - spriteXOffset);
- } else {
- drawX = _rebelViewOffset2X + spriteXOffset;
- }
- int drawY = spriteYOffset + _rebelViewOffset2Y;
-
- // Gun barrel is approximately at the character's hand level:
- // X: center of sprite ± directional offset toward the target
- // Y: about 60% down the sprite height (hand/arm level)
- _turretShots[i].gunX = drawX + spriteW / 2;
- _turretShots[i].gunY = drawY + (spriteH * 3) / 5;
- } else {
- // Fallback: approximate center-bottom of character area
- _turretShots[i].gunX = _rebelViewOffset2X + 160;
- _turretShots[i].gunY = _rebelViewOffset2Y + 140;
- }
-
- debug("Rebel2 Handler25: Spawned shot %d target (%d,%d) gun (%d,%d)",
- i, _turretShots[i].targetX, _turretShots[i].targetY,
- _turretShots[i].gunX, _turretShots[i].gunY);
- break;
- }
- }
-}
-
-// Handler 7 Space combat shot spawn (based on FUN_40D836 lines 146-166)
-void InsaneRebel2::spawnSpaceShot(int x, int y) {
- for (int i = 0; i < 2; i++) {
- if (_spaceShots[i].counter == 0) {
- // FUN_0041189e(6, local_2c + 1, 0x7f, 0, 0) â HBLAST.SAD
- playSfx(6, 127, 0);
-
- _spaceShots[i].counter = getShotMaxDuration();
- _spaceShots[i].targetX = x; // Screen coords
- _spaceShots[i].targetY = y;
-
- // Calculate gun positions from direction-based lookup tables
- // In the original, these come from tables indexed by _shipDirectionIndex
- // DAT_004437c2/DAT_00443808 for left gun, DAT_0044384e/DAT_00443894 for right gun
- // For now, use simplified positions relative to ship
- int shipScreenX = 160 + ((_shipPosX - 160) >> 3);
- int shipScreenY = 105 + ((_shipPosY - 40) >> 2);
-
- // Gun offsets (approximate from disassembly)
- _spaceShots[i].leftGunX = shipScreenX - 28;
- _spaceShots[i].leftGunY = shipScreenY + 10;
- _spaceShots[i].rightGunX = shipScreenX + 28;
- _spaceShots[i].rightGunY = shipScreenY + 10;
- _spaceShots[i].variant = _spaceShotDirection;
- break;
- }
- }
-}
-
-void InsaneRebel2::drawTexturedLine(byte *dst, int pitch, int width, int height, int x0, int y0, int x1, int y1, NutRenderer *nut, int spriteIdx, int v, bool mask231) {
- if (!nut || spriteIdx >= nut->getNumChars()) return;
-
- const byte *srcData = nut->getCharData(spriteIdx);
- int texW = nut->getCharWidth(spriteIdx);
- int texH = nut->getCharHeight(spriteIdx);
-
- if (!srcData || texW <= 0 || texH <= 0) return;
- if (v < 0) v = 0;
- if (v >= texH) v = texH - 1;
-
- int dx = abs(x1 - x0), sx = x0 < x1 ? 1 : -1;
- int dy = -abs(y1 - y0), sy = y0 < y1 ? 1 : -1;
- int err = dx + dy, e2;
-
- // Total length approximation for UV mapping
- int totalDist = (abs(dx) > abs(dy)) ? abs(dx) : abs(dy);
- if (totalDist == 0) totalDist = 1;
-
- int currentDist = 0;
-
- for (;;) {
- if (x0 >= 0 && x0 < width && y0 >= 0 && y0 < height) {
- // Map currentDist/totalDist to 0..texW (Run along texture width)
- int u = (currentDist * texW) / totalDist;
- if (u >= texW) u = texW - 1;
-
- byte color = srcData[v * texW + u];
-
- // Check for transparency (0 and optionally 231)
- if (color != 0 && (!mask231 || color != 231)) {
- dst[y0 * pitch + x0] = color;
- }
- }
-
- if (x0 == x1 && y0 == y1) break;
- e2 = 2 * err;
- if (e2 >= dy) { err += dy; x0 += sx; }
- if (e2 <= dx) { err += dx; y0 += sy; }
-
- currentDist++;
- }
-}
-
-// Helper: draw a textured segment between two points using the game's original routine (FUN_00429360 port)
-void drawTexturedSegment(byte *dst, int pitch, int width, int height, int param_3, int param_4, int param_5, int param_6, int param_7, const byte *param_8) {
- // Ported from FUN_00429360 (decompiled). Only 0 in texture is transparent.
- int sVar4 = 0; // left
- int sVar1 = 0; // top
- int sVar7 = width - 1; // right
- int sVar10 = height - 1; // bottom
-
- int px0 = param_3;
- int py0 = param_4;
- int px1 = param_5;
- int py1 = param_6;
-
- // Clip against screen bounds (translation of original clipping logic)
- if (px0 == px1) {
- if (px0 < sVar4 || px0 > sVar7) return;
- } else {
- if (px0 < sVar4) {
- if (px1 < sVar4) return;
- py0 = py1 + ((py0 - py1) * (sVar4 - px1)) / (px0 - px1);
- px0 = sVar4;
- } else if (px0 > sVar7) {
- if (px1 > sVar7) return;
- py0 = py1 + ((py0 - py1) * (sVar7 - px1)) / (px0 - px1);
- px0 = sVar7;
- }
- if (px1 < sVar4) {
- py1 = py0 + ((py1 - py0) * (sVar4 - px0)) / (px1 - px0);
- px1 = sVar4;
- } else if (px1 > sVar7) {
- py1 = py0 + ((py1 - py0) * (sVar7 - px0)) / (px1 - px0);
- px1 = sVar7;
- }
- }
-
- if (py0 == py1) {
- if (py0 < sVar1 || py0 > sVar10) return;
- } else {
- if (py0 < sVar1) {
- if (py1 < sVar1) return;
- px0 = px1 + ((px0 - px1) * (sVar1 - py1)) / (py0 - py1);
- py0 = sVar1;
- } else if (py0 > sVar10) {
- if (py1 > sVar10) return;
- px0 = px1 + ((px0 - px1) * (sVar10 - py1)) / (py0 - py1);
- py0 = sVar10;
- }
- if (py1 < sVar1) {
- px1 = px0 + ((px1 - px0) * (sVar1 - py0)) / (py1 - py0);
- py1 = sVar1;
- } else if (py1 > sVar10) {
- px1 = px0 + ((px1 - px0) * (sVar10 - py0)) / (py1 - py0);
- py1 = sVar10;
- }
- }
-
- int dx = px1 - px0;
- int dy = py1 - py0;
- int absdx = dx < 0 ? -dx : dx;
- int absdy = dy < 0 ? -dy : dy;
-
- // pointer into destination and texture
- byte *baseDst = dst;
- const byte *texPtr = param_8;
-
- if (absdx == 0) {
- if (absdy == 0) {
- if (*texPtr != 0) baseDst[py0 * pitch + px0] = *texPtr;
- return;
- }
- // vertical-ish
- int step = absdy + 1;
- int curY = py0;
- int signY = dy > 0 ? 1 : -1;
- int iVar9 = step; // adv counter
- for (int i = 0; i < step; i++) {
- if (*texPtr != 0) baseDst[curY * pitch + px0] = *texPtr;
- curY += signY;
- iVar9 -= param_7;
- while (iVar9 < 0) { texPtr++; iVar9 += step; }
- }
- return;
- }
-
- if (absdy == 0) {
- // horizontal-ish
- int step = absdx + 1;
- int curX = px0;
- int signX = dx > 0 ? 1 : -1;
- int iVar11 = step;
- for (int i = 0; i < step; i++) {
- if (*texPtr != 0) baseDst[py0 * pitch + curX] = *texPtr;
- curX += signX;
- iVar11 -= param_7;
- while (iVar11 < 0) { texPtr++; iVar11 += step; }
- }
- return;
- }
-
- // general case
- int steps = (absdx > absdy) ? absdx + 1 : absdy + 1;
- int x = px0, y = py0;
- int sx = dx > 0 ? 1 : -1;
- int sy = dy > 0 ? 1 : -1;
- int err = absdx - absdy;
- int iVar12 = steps;
-
- for (int i = 0; i < steps; i++) {
- if (x >= 0 && x < width && y >= 0 && y < height) {
- if (*texPtr != 0) baseDst[y * pitch + x] = *texPtr;
- }
- int e2 = 2 * err;
- if (e2 > -absdy) { err -= absdy; x += sx; }
- if (e2 < absdx) { err += absdx; y += sy; }
- iVar12 -= param_7;
- if (iVar12 < 0) { texPtr++; iVar12 += steps; }
- }
-}
-
-
-// Initialize laser texture buffer from NUT sprite (FUN_0040BAB0)
-// This pre-renders a sprite into a buffer that drawLaserBeam uses
-void InsaneRebel2::initLaserTexture(NutRenderer *nut, int spriteIdx) {
- if (!nut || spriteIdx >= nut->getNumChars())
- return;
-
- // Get sprite dimensions (FUN_0040BAB0 lines 13-14)
- int16 texWidth = nut->getCharWidth(spriteIdx);
- int16 texHeight = nut->getCharHeight(spriteIdx);
-
- // Clamp height to max 15 pixels (FUN_0040BAB0 lines 15-17)
- if (texHeight > 15) {
- texHeight = 15;
- }
-
- // Free existing texture if any (FUN_0040BAB0 lines 18-20)
- freeLaserTexture();
-
- // Allocate new buffer (FUN_0040BAB0 line 21)
- _laserTexture.width = texWidth;
- _laserTexture.height = texHeight;
- _laserTexture.pixels = (byte *)calloc(texWidth * texHeight, 1);
-
- if (!_laserTexture.pixels)
- return;
-
- // Render sprite into buffer (FUN_0040BAB0 lines 23-24)
- // We copy the sprite data directly since it's already in the right format
- const byte *srcData = nut->getCharData(spriteIdx);
- if (srcData) {
- int srcHeight = nut->getCharHeight(spriteIdx);
- int copyHeight = MIN(texHeight, (int16)srcHeight);
- memcpy(_laserTexture.pixels, srcData, texWidth * copyHeight);
- }
-
- debug("Rebel2: Initialized laser texture %dx%d from sprite %d", texWidth, texHeight, spriteIdx);
-}
-
-// Free laser texture buffer (FUN_0040BBD1)
-void InsaneRebel2::freeLaserTexture() {
- free(_laserTexture.pixels);
- _laserTexture.pixels = nullptr;
- _laserTexture.width = 0;
- _laserTexture.height = 0;
-}
-
-// Draw laser beam using pre-initialized texture (FUN_0040BBF6)
-// This is a direct port of the assembly function
-//
-// Parameters (matching FUN_0040bbf6):
-// dst, pitch, width, height: destination buffer info
-// gunX, gunY (param_3, param_4): gun/start position
-// targetX, targetY (param_5, param_6): target/end position
-// animFrame (param_7): current animation frame (shot counter)
-// maxFrames (param_8): max animation frames (shot duration)
-// widthScale (param_9): width scaling factor for perspective
-// heightScale (param_10): height/thickness multiplier
-// thickness (param_11): base line thickness
-void InsaneRebel2::drawLaserBeam(byte *dst, int pitch, int width, int height,
- int16 gunX, int16 gunY, int16 targetX, int16 targetY,
- int16 animFrame, int16 maxFrames,
- int16 widthScale, int16 heightScale, int16 thickness) {
- // Check if laser texture is initialized
- if (!_laserTexture.pixels || _laserTexture.width <= 0 || _laserTexture.height <= 0)
- return;
-
- int16 texW = _laserTexture.width;
- int16 texH = _laserTexture.height;
- byte *texPixels = _laserTexture.pixels;
-
- // FUN_0040BBF6 line 23: sVar7 = (thickness * animFrame * 16) / maxFrames
- if (maxFrames == 0) maxFrames = 1;
- int16 sVar7 = (int16)(((int)thickness * (int)animFrame * 16) / (int)maxFrames);
-
- // FUN_0040BBF6 lines 24-25: Calculate delta with scaling
- int16 dx = targetX - gunX;
- int16 dy = targetY - gunY;
- int16 sVar6 = (int16)(((int)dx * (thickness + 1)) / (int)thickness);
- int16 sVar1 = (int16)(((int)dy * (thickness + 1)) / (int)thickness);
-
- // FUN_0040BBF6 lines 26-29: Calculate adjusted start and end points
- // Start point (closer to gun, adjusted by animation progress)
- int16 startX = (sVar6 + gunX) - (int16)(((int)sVar6 * 16) / (sVar7 + 16));
- int16 startY = (sVar1 + gunY) - (int16)(((int)sVar1 * 16) / (sVar7 + 16));
- // End point (closer to target)
- int16 endX = (sVar6 + gunX) - (int16)(((int)sVar6 * 16) / (widthScale + sVar7 + 16));
- int16 endY = (sVar1 + gunY) - (int16)(((int)sVar1 * 16) / (widthScale + sVar7 + 16));
-
- // FUN_0040BBF6 line 30: Get texture pixel pointer
- byte *local_28 = texPixels;
-
- // FUN_0040BBF6 lines 31-32: Calculate abs differences (FUN_004356e4 = abs)
- int iVar2 = abs(startY - endY); // |dy| of beam
- int iVar3 = abs(startX - endX); // |dx| of beam
-
- // FUN_0040BBF6 line 33: Choose rendering path based on beam orientation
- if (iVar2 < iVar3) {
- // Mostly horizontal beam - draw vertical scanlines
- // FUN_0040BBF6 lines 34-37
- iVar2 = abs(startX - endX);
- int temp = iVar2 * texH * heightScale;
- int16 numLines = (int16)((temp >> 3) / texW) + 2;
- int16 local_24 = -numLines;
- int16 halfLines = numLines >> 1;
-
- // FUN_0040BBF6 lines 39-46: Draw parallel lines
- for (int16 lineIdx = 0; lineIdx < numLines; lineIdx++) {
- // Draw one textured segment (vertical offset for this scanline)
- drawTexturedSegment(dst, pitch, width, height,
- startX, (startY - halfLines) + lineIdx,
- endX, (endY - halfLines) + lineIdx,
- texW, local_28);
-
- // Advance texture pointer (step through texture rows)
- for (local_24 = texH + local_24; local_24 > 0; local_24 -= numLines) {
- local_28 += texW;
- }
- }
- } else {
- // Mostly vertical beam - draw horizontal scanlines
- // FUN_0040BBF6 lines 54-56
- iVar2 = abs(startY - endY);
- int16 numLines = (int16)((iVar2 * texH) / texW) + 2;
- int16 local_24 = -numLines;
-
- // FUN_0040BBF6 lines 58-60: Clamp to texture height
- if (texH < numLines) {
- numLines = texH;
- }
-
- int16 halfLines = numLines >> 1;
-
- // FUN_0040BBF6 lines 61-68: Draw parallel lines
- for (int16 lineIdx = 0; lineIdx < numLines; lineIdx++) {
- // Draw one textured segment (horizontal offset for this scanline)
- drawTexturedSegment(dst, pitch, width, height,
- (startX - halfLines) + lineIdx, startY,
- (endX - halfLines) + lineIdx, endY,
- texW, local_28);
-
- // Advance texture pointer
- for (local_24 = texH + local_24; local_24 > 0; local_24 -= numLines) {
- local_28 += texW;
- }
- }
- }
-}
-void InsaneRebel2::drawLine(byte *dst, int pitch, int width, int height, int x0, int y0, int x1, int y1, byte color) {
- int dx = abs(x1 - x0), sx = x0 < x1 ? 1 : -1;
- int dy = -abs(y1 - y0), sy = y0 < y1 ? 1 : -1;
- int err = dx + dy, e2;
-
- for (;;) {
- if (x0 >= 0 && x0 < width && y0 >= 0 && y0 < height) {
- dst[y0 * pitch + x0] = color;
- }
- if (x0 == x1 && y0 == y1) break;
- e2 = 2 * err;
- if (e2 >= dy) { err += dy; x0 += sx; }
- if (e2 <= dx) { err += dx; y0 += sy; }
- }
-}
-
-void InsaneRebel2::drawCornerBrackets(byte *dst, int pitch, int width, int height, int x, int y, int w, int h, byte color) {
- // Draw L-shaped brackets at corners of the rect (x,y,w,h)
- // Bracket size: approx 8 pixels
- int armLen = 2;
- if (armLen > w / 2) armLen = w / 2;
- if (armLen > h / 2) armLen = h / 2;
-
- int x2 = x + w - 1;
- int y2 = y + h - 1;
-
- // Top-Left Corner
- drawLine(dst, pitch, width, height, x, y, x + armLen, y, color);
- drawLine(dst, pitch, width, height, x, y, x, y + armLen, color);
-
- // Top-Right Corner
- drawLine(dst, pitch, width, height, x2 - armLen, y, x2, y, color);
- drawLine(dst, pitch, width, height, x2, y, x2, y + armLen, color);
-
- // Bottom-Left Corner
- drawLine(dst, pitch, width, height, x, y2, x + armLen, y2, color);
- drawLine(dst, pitch, width, height, x, y2 - armLen, x, y2, color);
-
- // Bottom-Right Corner
- drawLine(dst, pitch, width, height, x2 - armLen, y2, x2, y2, color);
- drawLine(dst, pitch, width, height, x2, y2 - armLen, x2, y2, color);
-}
-
-// ============================================================
-// COLLISION ZONE SYSTEM (for Level 3 pilot ship obstacle avoidance)
-// ============================================================
-// Based on FUN_40E35E, FUN_40C3CC disassembly from info.md
-// Zones are quadrilaterals registered via IACT opcode 5
-
-void InsaneRebel2::registerCollisionZone(Common::SeekableReadStream &b, int16 subOpcode, int16 par4) {
- // IACT Opcode 5 data layout â corrected from FUN_4033CF / FUN_4092D9 analysis:
- //
- // Original game stores pointer to full IACT data (starting at opcode).
- // SmushPlayer reads the first 8 bytes as header (code/flags/unknown/userId),
- // so our stream starts at body[0] (IACT byte offset +8).
- //
- // FUN_4092D9 field mapping (byte offsets from stored pointer):
- // +0x00: opcode (5) â already consumed by SmushPlayer
- // +0x02: par2 (sub-opcode) â already consumed, passed as parameter
- // +0x04: par3 â already consumed by SmushPlayer
- // +0x06: par4 (userId) â filter value for < 1000 test, passed as parameter
- // +0x08: body[0] (sVar1) â control field 1 (frame check: field2-1 == field1)
- // +0x0A: body[1] (sVar2) â control field 2
- // +0x0C: body[2] â vertex 1 X
- // +0x0E: body[3] â vertex 1 Y
- // +0x10: body[4] â vertex 2 X
- // +0x12: body[5] â vertex 2 Y
- // +0x14: body[6] â vertex 3 X
- // +0x16: body[7] â vertex 3 Y
- // +0x18: body[8] â vertex 4 X
- // +0x1A: body[9] â vertex 4 Y
-
- int16 field1 = b.readSint16LE(); // body[0] â control field 1
- int16 field2 = b.readSint16LE(); // body[1] â control field 2
- int16 x1 = b.readSint16LE(); // body[2] â vertex 1 X
- int16 y1 = b.readSint16LE(); // body[3] â vertex 1 Y
- int16 x2 = b.readSint16LE(); // body[4] â vertex 2 X
- int16 y2 = b.readSint16LE(); // body[5] â vertex 2 Y
- int16 x3 = b.readSint16LE(); // body[6] â vertex 3 X
- int16 y3 = b.readSint16LE(); // body[7] â vertex 3 Y
- int16 x4 = b.readSint16LE(); // body[8] â vertex 4 X
- int16 y4 = b.readSint16LE(); // body[9] â vertex 4 Y
-
- CollisionZone zone;
- zone.x1 = x1;
- zone.y1 = y1;
- zone.x2 = x2;
- zone.y2 = y2;
- zone.x3 = x3;
- zone.y3 = y3;
- zone.x4 = x4;
- zone.y4 = y4;
- zone.field1 = field1;
- zone.field2 = field2;
- zone.filterValue = par4;
- zone.subOpcode = subOpcode;
- zone.active = true;
-
- // Register zone into appropriate table based on sub-opcode
- if (subOpcode == 0x0D && _primaryZoneCount < kMaxCollisionZones) {
- _primaryZones[_primaryZoneCount++] = zone;
- debug("Rebel2: Registered PRIMARY zone %d: filter=%d fields=[%d,%d] quad=(%d,%d)-(%d,%d)-(%d,%d)-(%d,%d)",
- _primaryZoneCount - 1, par4, field1, field2,
- x1, y1, x2, y2, x3, y3, x4, y4);
- } else if (subOpcode == 0x0E && _secondaryZoneCount < kMaxCollisionZones) {
- _secondaryZones[_secondaryZoneCount++] = zone;
- debug("Rebel2: Registered SECONDARY zone %d: filter=%d fields=[%d,%d] quad=(%d,%d)-(%d,%d)-(%d,%d)-(%d,%d)",
- _secondaryZoneCount - 1, par4, field1, field2,
- x1, y1, x2, y2, x3, y3, x4, y4);
- } else {
- debug("Rebel2: WARNING - Could not register zone (subOpcode=%d, primary=%d, secondary=%d)",
- subOpcode, _primaryZoneCount, _secondaryZoneCount);
- }
-}
-
-void InsaneRebel2::resetCollisionZones() {
- // Reset zone counters at end of frame (FUN_403240 equivalent)
- // This clears the zone tables so they can be rebuilt from the next frame's IACT chunks
- _primaryZoneCount = 0;
- _secondaryZoneCount = 0;
-}
-
-void InsaneRebel2::checkCollisionZones() {
- // Per-frame collision checking â FUN_4092D9 first loop (lines 39-202).
- // Tests aim/ship position against primary collision zone quadrilaterals.
- //
- // Original coordinate system:
- // Zone vertices are in 424x260 buffer space, centered by subtracting (0xD4=212, 0x82=130).
- // Aim position (DAT_00443668/DAT_0044366a) is in centered coords [-52..52, -45..45].
- // In FUN_407FCB: DAT_00443668 is a smoothed mouse-derived position.
- //
- // For our implementation:
- // Map mouse position to centered coords matching the original range.
- // Mouse X 0..320 â centered X â [-52..52] (with smoothing in original)
- // Mouse Y 0..200 â centered Y â [-45..45]
-
- if (_primaryZoneCount == 0) return;
-
- // Calculate aim position in centered coordinates.
- // Original: local_10 = mouseOffset + 0xa0, then smoothed and clamped to [-0x34..0x34]
- // Simplified mapping: mouse 0..320 â [-52..52], mouse 0..200 â [-45..45]
- int16 aimX = (int16)((_vm->_mouse.x - 160) * 52 / 160);
- int16 aimY = (int16)((100 - _vm->_mouse.y) * 45 / 100);
-
- // Clamp to original ranges (DAT_0047a7fc < 1 path)
- if (aimX > 0x34) aimX = 0x34;
- if (aimX < -0x34) aimX = -0x34;
- if (aimY > 0x2d) aimY = 0x2d;
- if (aimY < -0x2d) aimY = -0x2d;
-
- for (int i = 0; i < _primaryZoneCount; i++) {
- CollisionZone &zone = _primaryZones[i];
- if (!zone.active) continue;
-
- // Filter: only process zones with filterValue < 1000 (par4 from IACT header)
- // Original: *(short *)(*local_c + 6) < 1000
- if (zone.filterValue >= 1000) continue;
-
- // Frame check: field2 - 1 == field1
- // Original: sVar2 + -1 == (int)sVar1
- if (zone.field2 - 1 != zone.field1) continue;
-
- // Center zone vertices by subtracting buffer center (0xD4=212, 0x82=130)
- // Original: sVar4 = x1 - 0xD4, sVar8 = y1 - 0x82, etc.
- int cx1 = zone.x1 - 0xD4;
- int cy1 = zone.y1 - 0x82;
- int cx2 = zone.x2 - 0xD4;
- int cy2 = zone.y2 - 0x82;
- int cx3 = zone.x3 - 0xD4;
- int cy3 = zone.y3 - 0x82;
- int cx4 = zone.x4 - 0xD4;
- int cy4 = zone.y4 - 0x82;
-
- // Point-in-quadrilateral test â FUN_4092D9 lines 119-128
- // Tests if aim position is OUTSIDE the safe corridor (= collision with obstacle).
- // Original uses 4 edge interpolation tests connected by OR (any failure = collision).
- //
- // Edge 1: interpolate Y along top edge (v1âv2) at aim X position
- // if aimY < interpolated Y â outside top edge â collision
- // Edge 2: interpolate Y along bottom edge (v4âv3) at aim X position
- // if interpolated Y < aimY â outside bottom edge â collision
- // Edge 3: interpolate X along left edge (v1âv4) at aim Y position
- // if aimX < interpolated X â outside left edge â collision
- // Edge 4: interpolate X along right edge (v2âv3) at aim Y position
- // if interpolated X < aimX â outside right edge â collision
- bool collision = false;
-
- // Avoid division by zero for degenerate edges
- if (cx2 != cx1) {
- int interpY1 = ((aimX - cx1) * (cy2 - cy1)) / (cx2 - cx1) + cy1;
- if (aimY < interpY1) collision = true;
- }
- if (!collision && cx3 != cx4) {
- int interpY2 = ((aimX - cx4) * (cy3 - cy4)) / (cx3 - cx4) + cy4;
- if (interpY2 < aimY) collision = true;
- }
- if (!collision && cy4 != cy1) {
- int interpX1 = ((aimY - cy1) * (cx4 - cx1)) / (cy4 - cy1) + cx1;
- if (aimX < interpX1) collision = true;
- }
- if (!collision && cy3 != cy2) {
- int interpX2 = ((aimY - cy2) * (cx3 - cx2)) / (cy3 - cy2) + cx2;
- if (interpX2 < aimX) collision = true;
- }
-
- if (collision) {
- // Collision detected â apply damage from collision damage table
- // Original: DAT_0047a7ec += DAT_0047e0f6[levelIdx]
- // TODO: Read from per-level collision damage table DAT_0047e0f6
- int collisionDamage = 3 + (_difficulty * 2);
-
- if (!_rebelInvulnerable) {
- _playerDamage += collisionDamage;
- if (_playerDamage > 255) _playerDamage = 255;
- debug("Rebel2: COLLISION damage! zone=%d aim=(%d,%d) damage=%d total=%d",
- i, aimX, aimY, collisionDamage, _playerDamage);
- }
- // Visual effect â FUN_00420515 (palette flash)
- initDamageFlash();
- // TODO: FUN_0041189e sound based on collision direction
- } else {
- // Safely passed â award score bonus
- // Original: FUN_0041bf8d(DAT_0047e100[levelIdx])
- addScore(1);
- }
- }
-}
-
-void InsaneRebel2::checkHandler7CollisionZones() {
- // FUN_40E35E â Handler 7 per-frame collision system.
- // Uses ship position (_flyShipScreenX/_flyShipScreenY) in raw buffer coords.
- // Two modes depending on _flyControlMode:
- // Mode 0/2: Obstacle collision using SECONDARY zones (inside quad = hit)
- // Mode 1/3: Wall/boundary collision using PRIMARY zones (per-edge push-back)
-
- // Note: _hitCooldown is decremented in renderSpaceExplosions (FUN_40F1C5)
- // to match the original where the decrement happens during rendering.
-
- if (_flyControlMode == 0 || _flyControlMode == 2) {
- // ---- Mode 0/2: Obstacle collision using SECONDARY zones (FUN_403b5b) ----
- // Original lines 52-132: Point-in-quad test with 15px inward margin.
- // Inside the quad = collision with obstacle.
- const int margin = 15; // local_14 = 0x0f, local_20 = 0x0f
-
- for (int i = 0; i < _secondaryZoneCount; i++) {
- CollisionZone &zone = _secondaryZones[i];
- if (!zone.active) continue;
-
- int x1 = zone.x1, y1 = zone.y1;
- int x2 = zone.x2, y2 = zone.y2;
- int x3 = zone.x3, y3 = zone.y3;
- int x4 = zone.x4, y4 = zone.y4;
-
- // Point-in-quad test (lines 75-89)
- // Start assuming inside, clear if outside any edge (with margin)
- bool inside = true;
-
- // Top edge: interpolate Y along v1âv2 at shipX, +15 margin
- if (x2 != x1) {
- int interpY = (_flyShipScreenX - x1) * (y2 - y1) / (x2 - x1) + margin + y1;
- if (_flyShipScreenY < interpY) inside = false;
- }
- // Bottom edge: interpolate Y along v4âv3 at shipX, -15 margin
- if (inside && x3 != x4) {
- int interpY = (_flyShipScreenX - x4) * (y3 - y4) / (x3 - x4) + y4 - margin;
- if (interpY < _flyShipScreenY) inside = false;
- }
- // Left edge: interpolate X along v1âv4 at shipY, +15 margin
- if (inside && y4 != y1) {
- int interpX = (_flyShipScreenY - y1) * (x4 - x1) / (y4 - y1) + margin + x1;
- if (_flyShipScreenX < interpX) inside = false;
- }
- // Right edge: interpolate X along v2âv3 at shipY, -15 margin
- if (inside && y3 != y2) {
- int interpX = (_flyShipScreenY - y2) * (x3 - x2) / (y3 - y2) + x2 - margin;
- if (interpX < _flyShipScreenX) inside = false;
- }
-
- // Frame match: field2 - 1 == field1 (line 90)
- if (zone.field2 - 1 == zone.field1) {
- if (inside) {
- // Collision with obstacle â apply damage and break
- _hitCooldown = 10;
- _spaceShotDirection = zone.filterValue + 2;
-
- int collisionDamage = 3 + (_difficulty * 2);
- if (!_rebelInvulnerable) {
- _playerDamage += collisionDamage;
- if (_playerDamage > 255) _playerDamage = 255;
- }
- _rebelHitCounter++;
- initDamageFlash();
- // Pan based on ship X position relative to screen center
- playSfx(1, 127, CLIP((_flyShipScreenX - 212) * 127 / 160, -127, 127));
- debug("Rebel2: Handler7 Mode0/2 OBSTACLE HIT zone=%d ship=(%d,%d) damage=%d",
- i, _flyShipScreenX, _flyShipScreenY, collisionDamage);
- break; // Only one collision per frame (original breaks)
- } else {
- // Safely avoided obstacle â award score
- addScore(1);
- }
- }
- }
-
- // Corridor boundary proximity (lines 127-131)
- // These flags are used for directional indicators (not critical for damage)
-
- } else {
- // ---- Mode 1/3: Wall/boundary collision using PRIMARY zones (FUN_403b34) ----
- // Original lines 133-235: Per-edge interpolation with push-back.
- // Ship position is clamped to wall boundaries when hitting.
- int16 hMargin = (_flyControlMode == 1) ? 0x28 : 0x0f; // local_14
- const int16 vMargin = 0x0f; // local_20
-
- for (int i = 0; i < _primaryZoneCount; i++) {
- CollisionZone &zone = _primaryZones[i];
- if (!zone.active) continue;
-
- int x1 = zone.x1, y1 = zone.y1;
- int x2 = zone.x2, y2 = zone.y2;
- int x3 = zone.x3, y3 = zone.y3;
- int x4 = zone.x4, y4 = zone.y4;
-
- // Top edge: interpolate Y along v1âv2 at shipX (lines 152-166)
- if (x2 != x1) {
- int16 edgeY = (int16)((_flyShipScreenX - x1) * (y2 - y1) / (x2 - x1) + y1 + vMargin);
- if (_flyShipScreenY < edgeY) {
- // Ship above top wall â push down
- if (_hitCooldown < 5 && !_rebelInvulnerable) {
- int damage = 3 + (_difficulty * 2);
- _playerDamage += damage;
- if (_playerDamage > 255) _playerDamage = 255;
- _rebelHitCounter++;
- _hitCooldown = 10;
- playSfx(1, 127, 0); // CRASH.SAD, top wall â center pan
- debug("Rebel2: Handler7 Mode1/3 TOP WALL ship=(%d,%d) edgeY=%d damage=%d",
- _flyShipScreenX, _flyShipScreenY, edgeY, damage);
- }
- _spaceShotDirection = 2; // Direction: pushed down
- _flyShipScreenY = edgeY; // Push-back
- initDamageFlash();
- }
- }
-
- // Bottom edge: interpolate Y along v4âv3 at shipX (lines 167-183)
- if (x3 != x4) {
- int16 edgeY = (int16)((_flyShipScreenX - x4) * (y3 - y4) / (x3 - x4) + y4 - vMargin);
- _corridorBottomY = vMargin + edgeY; // DAT_00443b10 update
- if (edgeY < _flyShipScreenY) {
- // Ship below bottom wall â push up
- if (_hitCooldown < 5 && !_rebelInvulnerable) {
- int damage = 3 + (_difficulty * 2);
- _playerDamage += damage;
- if (_playerDamage > 255) _playerDamage = 255;
- _rebelHitCounter++;
- _hitCooldown = 10;
- playSfx(1, 127, 0); // CRASH.SAD, bottom wall â center pan
- debug("Rebel2: Handler7 Mode1/3 BOTTOM WALL ship=(%d,%d) edgeY=%d damage=%d",
- _flyShipScreenX, _flyShipScreenY, edgeY, damage);
- }
- _spaceShotDirection = 3; // Direction: pushed up
- _flyShipScreenY = edgeY; // Push-back
- initDamageFlash();
- }
- }
-
- // Left edge: interpolate X along v1âv4 at shipY (lines 184-199)
- if (y4 != y1) {
- int16 edgeX = (int16)((_flyShipScreenY - y1) * (x4 - x1) / (y4 - y1) + x1 + hMargin);
- if (_flyShipScreenX < edgeX) {
- // Ship left of left wall â push right
- _flyShipScreenX = edgeX; // Push-back
- if (_hitCooldown < 5 && !_rebelInvulnerable) {
- int damage = 3 + (_difficulty * 2);
- _playerDamage += damage;
- if (_playerDamage > 255) _playerDamage = 255;
- _rebelHitCounter++;
- _hitCooldown = 10;
- playSfx(1, 127, -100); // CRASH.SAD, left wall â pan left
- debug("Rebel2: Handler7 Mode1/3 LEFT WALL ship=(%d,%d) edgeX=%d damage=%d",
- _flyShipScreenX, _flyShipScreenY, edgeX, damage);
- }
- _spaceShotDirection = 0; // Direction: pushed right
- initDamageFlash();
- }
- }
-
- // Right edge: interpolate X along v2âv3 at shipY (lines 200-215)
- if (y3 != y2) {
- int16 edgeX = (int16)((_flyShipScreenY - y2) * (x3 - x2) / (y3 - y2) + x2 - hMargin);
- if (edgeX < _flyShipScreenX) {
- // Ship right of right wall â push left
- _flyShipScreenX = edgeX; // Push-back
- if (_hitCooldown < 5 && !_rebelInvulnerable) {
- int damage = 3 + (_difficulty * 2);
- _playerDamage += damage;
- if (_playerDamage > 255) _playerDamage = 255;
- _rebelHitCounter++;
- _hitCooldown = 10;
- playSfx(1, 127, 100); // CRASH.SAD, right wall â pan right
- debug("Rebel2: Handler7 Mode1/3 RIGHT WALL ship=(%d,%d) edgeX=%d damage=%d",
- _flyShipScreenX, _flyShipScreenY, edgeX, damage);
- }
- _spaceShotDirection = 1; // Direction: pushed left
- initDamageFlash();
- }
- }
- }
- }
-}
-
-void InsaneRebel2::drawQuad(byte *dst, int pitch, int width, int height,
- int x1, int y1, int x2, int y2, int x3, int y3, int x4, int y4, byte color) {
- // Draw a quadrilateral by connecting its 4 vertices with lines
- // Vertex order: top-left (1), top-right (2), bottom-right (3), bottom-left (4)
- drawLine(dst, pitch, width, height, x1, y1, x2, y2, color); // Top edge
- drawLine(dst, pitch, width, height, x2, y2, x3, y3, color); // Right edge
- drawLine(dst, pitch, width, height, x3, y3, x4, y4, color); // Bottom edge
- drawLine(dst, pitch, width, height, x4, y4, x1, y1, color); // Left edge
-}
-
-void InsaneRebel2::drawCollisionZones(byte *dst, int pitch, int width, int height, byte color) {
- // Draw all active collision zones as wireframe quadrilaterals for debugging
- // Uses different colors for primary vs secondary zones
-
- const byte primaryColor = 44; // Bright red for primary (obstacle) zones
- const byte secondaryColor = 47; // Yellow for secondary (boundary) zones
-
- // Draw primary zones (sub-opcode 0x0D - obstacles)
- for (int i = 0; i < _primaryZoneCount; i++) {
- CollisionZone &zone = _primaryZones[i];
- if (!zone.active) continue;
-
- // Apply view offset to convert from video coords to screen coords
- int x1 = zone.x1 + _viewX;
- int y1 = zone.y1 + _viewY;
- int x2 = zone.x2 + _viewX;
- int y2 = zone.y2 + _viewY;
- int x3 = zone.x3 + _viewX;
- int y3 = zone.y3 + _viewY;
- int x4 = zone.x4 + _viewX;
- int y4 = zone.y4 + _viewY;
-
- drawQuad(dst, pitch, width, height, x1, y1, x2, y2, x3, y3, x4, y4, primaryColor);
- }
-
- // Draw secondary zones (sub-opcode 0x0E - boundaries)
- for (int i = 0; i < _secondaryZoneCount; i++) {
- CollisionZone &zone = _secondaryZones[i];
- if (!zone.active) continue;
-
- // Apply view offset
- int x1 = zone.x1 + _viewX;
- int y1 = zone.y1 + _viewY;
- int x2 = zone.x2 + _viewX;
- int y2 = zone.y2 + _viewY;
- int x3 = zone.x3 + _viewX;
- int y3 = zone.y3 + _viewY;
- int x4 = zone.x4 + _viewX;
- int y4 = zone.y4 + _viewY;
-
- drawQuad(dst, pitch, width, height, x1, y1, x2, y2, x3, y3, x4, y4, secondaryColor);
- }
-
- // Draw corridor boundaries as a rectangle (from IACT opcode 7)
- if (_corridorLeftX != 0 || _corridorRightX != 0x1A8) {
- const byte corridorColor = 45; // Cyan for corridor boundaries
- // Draw vertical lines for left/right boundaries
- drawLine(dst, pitch, width, height,
- _corridorLeftX + _viewX, _corridorTopY + _viewY,
- _corridorLeftX + _viewX, _corridorBottomY + _viewY, corridorColor);
- drawLine(dst, pitch, width, height,
- _corridorRightX + _viewX, _corridorTopY + _viewY,
- _corridorRightX + _viewX, _corridorBottomY + _viewY, corridorColor);
- // Draw horizontal lines for top/bottom boundaries
- drawLine(dst, pitch, width, height,
- _corridorLeftX + _viewX, _corridorTopY + _viewY,
- _corridorRightX + _viewX, _corridorTopY + _viewY, corridorColor);
- drawLine(dst, pitch, width, height,
- _corridorLeftX + _viewX, _corridorBottomY + _viewY,
- _corridorRightX + _viewX, _corridorBottomY + _viewY, corridorColor);
- }
-}
-
-void InsaneRebel2::renderNutSprite(byte *dst, int pitch, int width, int height, int x, int y, NutRenderer *nut, int spriteIdx) {
- renderNutSpriteMirrored(dst, pitch, width, height, x, y, nut, spriteIdx, false);
-}
-
-// Render NUT sprite with optional horizontal mirroring
-// Based on FUN_004236e0 disassembly - flags=0x2001 triggers horizontal flip
-void InsaneRebel2::renderNutSpriteMirrored(byte *dst, int pitch, int width, int height, int x, int y, NutRenderer *nut, int spriteIdx, bool mirror) {
- if (!nut || spriteIdx < 0 || spriteIdx >= nut->getNumChars()) return;
-
- int w = nut->getCharWidth(spriteIdx);
- int h = nut->getCharHeight(spriteIdx);
- const byte *src = nut->getCharData(spriteIdx);
-
- // Clipping
- int drawX = x;
- int drawY = y;
- int drawW = w;
- int drawH = h;
- int srcOffsetX = 0;
- int srcOffsetY = 0;
-
- if (drawX < 0) {
- srcOffsetX = -drawX;
- drawW += drawX;
- drawX = 0;
- }
- if (drawY < 0) {
- srcOffsetY = -drawY;
- drawH += drawY;
- drawY = 0;
- }
-
- if (drawX + drawW > width) {
- drawW = width - drawX;
- }
- if (drawY + drawH > height) {
- drawH = height - drawY;
- }
-
- if (drawW <= 0 || drawH <= 0) return;
-
- // Draw loop - with optional horizontal mirroring
- for (int iy = 0; iy < drawH; iy++) {
- const byte *s = src + (srcOffsetY + iy) * w;
- byte *d = dst + (drawY + iy) * pitch + drawX;
- for (int ix = 0; ix < drawW; ix++) {
- int srcX;
- if (mirror) {
- // When mirrored, read from the opposite side of the sprite
- srcX = (w - 1) - (srcOffsetX + ix);
- } else {
- srcX = srcOffsetX + ix;
- }
- byte px = s[srcX];
- if (px != 231 && px != 0) { // Check both 0 and 231 (0xE7) for transparency
- d[ix] = px;
- }
- }
- }
-}
-
-void InsaneRebel2::procPostRendering(byte *renderBitmap, int32 codecparam, int32 setupsan12,
- int32 setupsan13, int32 curFrame, int32 maxFrame) {
-
- // Determine correct pitch for the video buffer (usually 320 for Rebel2)
- int width = _player->_width;
- int height = _player->_height;
- if (width == 0) width = _vm->_screenWidth;
- if (height == 0) height = _vm->_screenHeight;
- int pitch = width;
-
- // Calculate View/Scroll Offsets
- // Rebel Assault 2 uses a buffer larger (424x260) than screen (320x200)
- // Map mouse X (0-320) to Scroll X (0-104)
- // Map mouse Y (0-200) to Scroll Y (0-60)
- int maxScrollX = width - _vm->_screenWidth;
- int maxScrollY = height - _vm->_screenHeight;
-
- if (maxScrollX < 0) maxScrollX = 0;
- if (maxScrollY < 0) maxScrollY = 0;
-
- // Simple linear mapping: Center of screen corresponds to center of buffer
- _viewX = (_vm->_mouse.x * maxScrollX) / _vm->_screenWidth;
- _viewY = (_vm->_mouse.y * maxScrollY) / _vm->_screenHeight;
-
- _player->setScrollOffset(_viewX, _viewY);
-
- // --- HUD Drawing Order (from FUN_004089ab assembly analysis) ---
- // Based on FUN_004089ab:
- // 1. Line 156: FUN_004288c0 fills status bar background at Y=0xb4 (180)
- // 2. Lines 171-226: Draw turret overlays, targeting reticle, crosshair
- // 3. Line 243: FUN_0041c012 draws status bar sprites LAST (on top)
- //
- // In FUN_0041c012:
- // - Sprites are drawn to buffer DAT_00482204 at position (0,0)
- // - Buffer is composited at Y=0xb4 (180) via FUN_0042f780
- // - DISPFONT.NUT (DAT_00482200) sprites 1-7 contain the status bar elements
- //
- // We draw directly to screen at Y=180
-
- // Use video content coordinates, NOT buffer coordinates
- const int videoWidth = 320; // Native video width
- const int videoHeight = 200; // Native video height
- const int statusBarY = 180; // 0xb4 - status bar starts at Y=180 in video coords
-
- // Hide HUD/status bar during intro videos (marked by SmushPlayer video flag 0x20)
- // The 0x20 flag indicates a non-interactive cutscene/intro sequence OR menu
- bool introPlaying = ((_player->_curVideoFlags & 0x20) != 0);
-
- // Check if we're in menu mode (menu state + intro flag)
- bool menuMode = (introPlaying && _gameState == kStateMainMenu);
- bool pilotSelectMode = (introPlaying && (_gameState == kStatePilotSelect || _gameState == kStateDifficultySelect));
- bool chapterSelectMode = (introPlaying && _gameState == kStateChapterSelect);
-
- // Handle pilot selection input and rendering (FUN_00414A41)
- // This is the pilot/save slot selection screen with centered menu
- if (pilotSelectMode) {
- // Show the standard Windows arrow cursor
- Graphics::Cursor *cursor = Graphics::makeDefaultWinCursor();
- CursorMan.replaceCursor(cursor);
- delete cursor;
- CursorMan.showMouse(true);
-
- // Process pilot selection input - emulates FUN_00414A41 input handling
- int selection = processLevelSelectInput();
-
- // Draw pilot selection overlay - centered menu like main menu
- drawLevelSelectOverlay(renderBitmap, pitch, width, height);
-
- // If a selection was confirmed, signal video to stop
- if (selection >= 0) {
- debug("Rebel2: Pilot selection confirmed: %d", selection);
- _vm->_smushVideoShouldFinish = true;
- }
-
- // Skip normal HUD rendering in pilot select mode
- return;
- }
-
- // Handle chapter selection input and rendering (FUN_00415CF8)
- // This is the actual level/chapter selection screen with preview and password
- if (chapterSelectMode) {
- // Show the standard Windows arrow cursor (same as menu)
- Graphics::Cursor *cursor = Graphics::makeDefaultWinCursor();
- CursorMan.replaceCursor(cursor);
- delete cursor;
- CursorMan.showMouse(true);
-
- // O_LEVEL.SAN provides the background with chapter preview thumbnails.
- // The FOBJ offset system (set in procPreRendering) scrolls the correct preview
- // into the preview box area. No black fill needed â video frame shows through.
-
- // Process chapter selection input - emulates FUN_00415CF8 input handling
- int selection = processChapterSelectInput();
-
- // Draw chapter selection overlay - emulates FUN_00415CF8 rendering
- drawChapterSelectOverlay(renderBitmap, pitch, width, height);
-
- // If a selection was confirmed, signal video to stop
- if (selection >= 0) {
- debug("Rebel2: Chapter selection confirmed: %d", selection);
- _vm->_smushVideoShouldFinish = true;
- }
-
- // Skip normal HUD rendering in chapter select mode
- return;
- }
-
- // Handle menu input and rendering if in menu mode
- if (menuMode) {
- // The original game uses the standard Windows arrow cursor (IDC_ARROW)
- // loaded via LoadCursorA(NULL, 0x7f00) in FUN_420C70.decompiled.txt
- // MSTOVER.NUT is a background overlay, NOT a cursor
- Graphics::Cursor *cursor = Graphics::makeDefaultWinCursor();
- CursorMan.replaceCursor(cursor);
- delete cursor;
- CursorMan.showMouse(true);
-
- // Process menu input during each frame
- int selection = processMenuInput();
-
- // Update inactivity timer (only increments when no input is received)
- // Input resets timer in processMenuInput()
- _menuInactivityTimer++;
-
- // Check for inactivity timeout
- // From FUN_004147b2: 300 frames of inactivity returns 0 (exit to intro/attract mode)
- // At 12fps video rate, 300 frames = ~25 seconds of inactivity
- // The original checks: if (local_8 > 299) return 0;
- if (_menuInactivityTimer > 300) {
- debug("Rebel2: Menu inactivity timeout - ending video to loop");
- // Signal video to end so menu loop plays new video
- // This emulates the attract mode behavior where a new random
- // menu video is selected after inactivity
- _menuInactivityTimer = 0;
- // Don't set _smushVideoShouldFinish here - let video end naturally
- // This will cause runMainMenu to loop and play a new random video
- }
-
- // Draw menu selection overlay
- drawMenuOverlay(renderBitmap, pitch, width, height);
-
- // If a selection was confirmed, signal video to stop
- if (selection >= 0) {
- debug("Rebel2: Menu selection confirmed: %d", selection);
- _vm->_smushVideoShouldFinish = true;
- }
-
- // Skip normal HUD rendering in menu mode
- return;
- }
-
- // During intro/cinematic sequences:
- // - Hide the mouse cursor (original: ShowCursor(0) at startup in FUN_00420c70)
- // - Skip all HUD/status bar/crosshair rendering
- // - Skip mouse input processing (no shooting during intros)
- //
- // Original behavior from FUN_00403240:
- // - if (DAT_0047a814 == 0) { switch(DAT_0047ee84) { ... } }
- // - DAT_0047ee84 (handler) is only set by IACT opcode 6 during gameplay videos
- // - Cinematics/intros don't have opcode 6, so handler stays 0
- // - We use _rebelHandler == 0 as the primary indicator for intro/cinematic mode
- if (_rebelHandler == 0) {
- // Hide mouse cursor during intro - no crosshair, no clicking
- CursorMan.showMouse(false);
-
- // Track state transition for debugging
- if (!_introCursorPushed) {
- _introCursorPushed = true;
- debug("Rebel2: Intro/cinematic mode (handler=0, flags=0x%x, state=%d) - HUD disabled, mouse hidden",
- _player->_curVideoFlags, _gameState);
- }
- // Skip all HUD rendering during intro - subtitles are rendered via opcode 9
- return;
- } else {
- // Gameplay mode - handler was set by IACT opcode 6
- if (_introCursorPushed) {
- _introCursorPushed = false;
- debug("Rebel2: Gameplay mode (handler=%d, flags=0x%x, state=%d) - HUD enabled",
- _rebelHandler, _player->_curVideoFlags, _gameState);
- }
- }
-
- // From here on, we're in gameplay mode (_rebelHandler != 0)
- // Process mouse input for shooting
- // Original: FUN_00403240 only runs handlers when DAT_0047a814 == 0
- processMouse();
-
- // NOTE: Level 2 background is drawn ONCE during IACT opcode 8 par4=5 processing
- // (in procIACT when the background ANIM is first loaded). The 0x08 video flag
- // (preserve background) prevents the frame buffer from being cleared, so the
- // background persists. FOBJ sprites (enemies) are then decoded on top by SMUSH.
- // We do NOT redraw the background here as that would overwrite FOBJ content.
-
- // --- HUD Drawing Order (from FUN_004089ab assembly analysis) ---
- // 1. FUN_004288c0: Fill status bar background at Y=0xb4 (180)
- // 2. FUN_004089ab: Draw turret overlays, targeting reticle, crosshair
- // 3. FUN_0041c012: Draw status bar sprites LAST (on top)
-
- // STEP 0: Fill status bar background (FUN_004288c0)
- renderStatusBarBackground(renderBitmap, pitch, width, height, videoWidth, videoHeight, statusBarY);
-
- // STEP 1A: Draw NUT-based HUD overlays for Handler 0x26/0x19 (FUN_004089ab lines 195-226)
- renderTurretHudOverlays(renderBitmap, pitch, width, height, curFrame);
-
- // STEP 1B: Draw embedded SAN HUD overlays (from IACT chunks)
- renderEmbeddedHudOverlays(renderBitmap, pitch, width, height);
-
- // STEP 2: Draw DISPFONT.NUT status bar sprites (FUN_0041c012)
- renderStatusBarSprites(renderBitmap, pitch, width, height, statusBarY, curFrame);
-
- // Ship rendering (FUN_00401ccf for Handler 8, FUN_0040d836 for Handler 7)
- debug("Rebel2 Ship Check: handler=%d shipSprite=%p flyShipSprite=%p shipLevelMode=%d numSprites=%d/%d",
- _rebelHandler, (void*)_shipSprite, (void*)_flyShipSprite, _shipLevelMode,
- _shipSprite ? _shipSprite->getNumChars() : 0,
- _flyShipSprite ? _flyShipSprite->getNumChars() : 0);
-
- renderHandler7Ship(renderBitmap, pitch, width, height);
- renderHandler8Ship(renderBitmap, pitch, width, height);
- // GRD001 (wall/cockpit) drawn AFTER FOBJs per original FUN_0041DB5E lines 202-210
- renderHandler25ShipPre(renderBitmap, pitch, width, height);
- renderHandler25Ship(renderBitmap, pitch, width, height);
- renderFallbackShip(renderBitmap, pitch, width, height);
-
- // Enemy indicators and destroyed enemy area erase
- renderEnemyOverlays(renderBitmap, pitch, width, height, videoWidth);
-
- // Explosion animations (FUN_409FBC)
- renderExplosions(renderBitmap, pitch, width, height);
-
- // Laser shot beams and impacts
- renderLaserShots(renderBitmap, pitch, width, height);
-
- // Damage visual effects â handler-specific per original architecture:
- // Handler 8: FUN_401CCF line 119 â FUN_00420754 (palette flash + screen shake)
- // Handler 0x19: FUN_41DB5E line 192 â FUN_00420562 (palette flash only, every frame)
- // Handler 0x26: FUN_4092D9 lines 135/225/237 â FUN_00420515 trigger + palette flash
- // Handler 7: FUN_40E35E â FUN_00420515 trigger + palette flash
- if (_rebelHandler == 8) {
- // Full damage effect: palette flash + screen shake
- // Suppressed during autopilot (mode 4) and cutscene (mode 5)
- if (_shipLevelMode != 4 && _shipLevelMode != 5) {
- updateDamageEffect(renderBitmap, pitch, width, height);
- }
- } else if (_rebelHandler == 0x19 || _rebelHandler == 0x26 || _rebelHandler == 7) {
- // Palette flash only â no screen shake for turret/FPS/ship handlers
- updateDamageFlashPalette();
- }
-
- // Per-frame collision checking against registered zones.
- //
- // Handler 0x26 (turret): FUN_4092D9 â aim position vs primary zones (centered coords)
- // Zones with filterValue < 1000 tested via point-in-quad against mouse/aim position.
- //
- // Handler 7 (ship): FUN_40E35E â ship position vs zones per control mode:
- // Mode 0/2: SECONDARY zones (0x0E) â obstacle collision (inside quad = hit)
- // Mode 1/3: PRIMARY zones (0x0D) â wall/boundary per-edge with push-back
- // Uses ship position in raw buffer coords, hit cooldown, directional damage.
- if (_rebelHandler == 0x26) {
- checkCollisionZones();
- } else if (_rebelHandler == 7) {
- checkHandler7CollisionZones();
- }
-
- // Collision zone visualization (debug - for Handler 7/8 pilot modes)
- if (_rebelHandler == 7 || _rebelHandler == 8) {
- drawCollisionZones(renderBitmap, pitch, width, height, 0);
- }
-
- // Crosshair/reticle (FUN_004089ab, FUN_0040d836)
- renderCrosshair(renderBitmap, pitch, width, height);
-
- // HUD score/lives rendering (FUN_0041c012)
- renderScoreHUD(renderBitmap, pitch, width, height, 0);
-
- // Reset FOBJ position offsets (FUN_00424510(0,0) in original FUN_0041DB5E line 271)
- if (_player) {
- _player->_fobjOffsetX = 0;
- _player->_fobjOffsetY = 0;
- }
-
- // Frame end cleanup: reset enemy active flags and collision zones (FUN_403240)
- frameEndCleanup();
-}
-
-// ======================= Damage Visual Effect Functions =======================
-// Palette flash + screen shake when the player takes damage.
-// Original retail functions: FUN_420515, FUN_420562, FUN_420754, FUN_42073B, FUN_420501
-
-// FUN_00420501 - Reset palette flash counter.
-// Called at level start / scene transitions to clear any in-progress flash.
-void InsaneRebel2::resetDamageFlash() {
- _damageFlashCounter = 0;
-}
-
-// FUN_00420515 - Save current palette and initiate a 5-frame flash.
-// If a flash is already in progress, just resets the counter to 5
-// (the palette was already saved on the first hit).
-void InsaneRebel2::initDamageFlash() {
- if (_damageFlashCounter == 0) {
- // Save current SMUSH palette before modifying it
- memcpy(_damageSavedPalette, _player->_pal, 0x300);
- }
- _damageFlashCounter = 5;
-}
-
-// FUN_0042073B - Trigger both palette flash and screen shake.
-// Called from the damage hit handler when the player takes damage.
-void InsaneRebel2::triggerDamageEffect() {
- initDamageFlash();
- _damageShakeCounter = 10;
-}
-
-// FUN_00420562 - Per-frame palette modification.
-//
-// Two modes determined by _damageHighFlashCounter:
-//
-// Normal hit flash (_damageHighFlashCounter == 0 or odd):
-// Decrements _damageFlashCounter. On even counter values, all 768 palette
-// bytes (RGB) are blended from inverted toward the saved original:
-// output[i] = 0xFF - ((0xFF - saved[i]) * (0x10 - counter)) >> 4
-// Counter 5â4(apply)â3(skip)â2(apply)â1(skip)â0(apply=original). The
-// alternating apply/skip creates a strobe-like flash effect.
-//
-// High-damage red pulse (_playerDamage >= 0xFF, even counter):
-// Only the R channel (every 3rd byte) is modified using the same formula
-// with _damageHighFlashCounter. Creates a pulsing red tint overlay.
-void InsaneRebel2::updateDamageFlashPalette() {
- // High-damage mode: persistent red pulsing when damage is maxed out
- if (_playerDamage < 0xFF) {
- _damageHighFlashCounter = 0;
- } else {
- if (_damageHighFlashCounter == 0) {
- // Save palette on first frame of high-damage mode
- memcpy(_damageSavedPalette, _player->_pal, 0x300);
- }
- if (_damageHighFlashCounter < 0x10) {
- _damageHighFlashCounter++;
- }
- }
-
- if (_damageHighFlashCounter == 0 || (_damageHighFlashCounter & 1) != 0) {
- // Normal hit flash path: decrement counter, apply on even values.
- // Original C: if ((counter != 0) && (counter--, (counter & 1) == 0))
- if (_damageFlashCounter != 0) {
- _damageFlashCounter--;
- if ((_damageFlashCounter & 1) == 0) {
- // Apply palette inversion on ALL RGB channels
- byte modPal[0x300];
- int blend = 0x10 - _damageFlashCounter;
- for (int i = 0; i < 0x300; i++) {
- modPal[i] = 0xFF - (((0xFF - _damageSavedPalette[i]) * blend) >> 4);
- }
- _player->setPalette(modPal);
- }
- }
- } else {
- // High-damage red-only flash (even _damageHighFlashCounter):
- // Modify only R channel (stride 3), G and B stay unchanged.
- byte modPal[0x300];
- memcpy(modPal, _player->_pal, 0x300);
- int blend = 0x10 - _damageHighFlashCounter;
- for (int i = 0; i < 0x300; i += 3) {
- modPal[i] = 0xFF - (((0xFF - _damageSavedPalette[i]) * blend) >> 4);
- }
- _player->setPalette(modPal);
- }
-}
-
-// FUN_00420754 - Per-frame screen shake + palette flash.
-//
-// Screen shake randomly shifts scanlines left or right for visual distortion.
-// The number of affected scanlines decreases each frame (counter * 5),
-// creating a diminishing shake effect over 10 frames.
-//
-// Called every frame from procPostRendering when not in cutscene modes
-// (shipLevelMode != 4 and != 5, matching original: DAT_0043e000 != 4 && != 5).
-void InsaneRebel2::updateDamageEffect(byte *renderBitmap, int pitch, int width, int height) {
- if (_damageShakeCounter != 0) {
- _damageShakeCounter--;
- int numLines = _damageShakeCounter * 5;
-
- // Temporary buffer for scanline rotation (case 1 in original)
- byte tempLine[640];
-
- for (int n = numLines; n > 0; n--) {
- // Pick a random scanline within the gameplay area (0..179, not status bar)
- int maxY = MIN(height, 180);
- int scanline = _vm->_rnd.getRandomNumber(maxY - 1);
-
- byte *linePtr = renderBitmap + pitch * scanline;
- int offset = _vm->_rnd.getRandomNumber(4) + 1; // 1..5 pixel shift
- int direction = _vm->_rnd.getRandomNumber(4); // 0..4
-
- int copyLen = pitch - offset;
- if (copyLen <= 0)
- continue;
-
- switch (direction) {
- case 0:
- case 3:
- // Shift left: copy line[offset..] -> line[0..]
- memmove(linePtr, linePtr + offset, copyLen);
- break;
- case 1:
- // Shift right with wrap: save, then copy
- memcpy(tempLine, linePtr, MIN(copyLen, (int)sizeof(tempLine)));
- memmove(linePtr + offset, tempLine, MIN(copyLen, (int)sizeof(tempLine)));
- break;
- case 2:
- case 4:
- // Shift right: copy line[0..] -> line[offset..]
- memmove(linePtr + offset, linePtr, copyLen);
- break;
- }
- }
- }
-
- // Palette flash runs every frame (even without shake)
- updateDamageFlashPalette();
-}
-
-// ======================= Rendering Helper Functions =======================
-// These are extracted from procPostRendering for better readability
-
-void InsaneRebel2::renderStatusBarBackground(byte *renderBitmap, int pitch, int width, int height,
- int videoWidth, int videoHeight, int statusBarY) {
- // Fill status bar background (FUN_004288c0 equivalent)
- // Original assembly: FUN_004288c0(local_8, 0, 0, 0xb4, 0x140, 0x14, 4)
- // This fills width=320, height=20 starting at Y=180 with color index 4
- const byte statusBarBgColor = 4;
-
- for (int y = statusBarY; y < videoHeight; y++) {
- int destY = y + _viewY;
- if (destY >= height) continue;
- for (int x = 0; x < videoWidth; x++) {
- int destX = x + _viewX;
- if (destX >= pitch) continue;
- renderBitmap[destY * pitch + destX] = statusBarBgColor;
- }
- }
-}
-
-void InsaneRebel2::renderTurretHudOverlays(byte *renderBitmap, int pitch, int width, int height, int32 curFrame) {
- // Draw NUT-based HUD overlays for Handler 0x26/0x19 (turret modes)
- // From FUN_004089ab disassembly (lines 195-226):
- // - DAT_0047fe78 (_hudOverlayNut): Primary HUD overlay with 6 animation frames
- // - Position formula (low-res):
- // X = 160 + (mouseOffsetX >> 4) - (width / 2) - spriteOffsetX
- // Y = 182 - (mouseOffsetY >> 4) - height - spriteOffsetY
- // - Animation: spriteIndex = (frameCounter / 2) % 6
-
- if ((_rebelHandler != 0x26 && _rebelHandler != 0x19) || !_hudOverlayNut || _hudOverlayNut->getNumChars() <= 0)
- return;
-
- // Calculate mouse offset (clamped to -127..127)
- int mouseOffsetX = (_vm->_mouse.x - 160);
- int mouseOffsetY = (_vm->_mouse.y - 100);
- if (mouseOffsetX > 127) mouseOffsetX = 127;
- if (mouseOffsetX < -127) mouseOffsetX = -127;
- if (mouseOffsetY > 127) mouseOffsetY = 127;
- if (mouseOffsetY < -127) mouseOffsetY = -127;
-
- // Animation frame cycling: (frameCounter / 2) % 6
- int numSprites = _hudOverlayNut->getNumChars();
- int animFrameCount = MIN(numSprites, 6);
- int animFrame = 0;
- if (animFrameCount > 0) {
- animFrame = (curFrame / 2) % animFrameCount;
- }
-
- // Get sprite dimensions
- int spriteW = _hudOverlayNut->getCharWidth(animFrame);
- int spriteH = _hudOverlayNut->getCharHeight(animFrame);
-
- // Position calculation from assembly (low-res mode)
- int spriteOffsetX = 0;
- int spriteOffsetY = 0;
- int hudX = 160 + (mouseOffsetX >> 4) - (spriteW / 2) - spriteOffsetX;
- int hudY = 182 - (mouseOffsetY >> 4) - spriteH - spriteOffsetY;
-
- // Apply view offset for scrolling background
- hudX += _viewX;
- hudY += _viewY;
-
- // Draw base cockpit (sprite 0 always drawn first)
- renderNutSprite(renderBitmap, pitch, width, height, hudX, hudY, _hudOverlayNut, 0);
-
- // Draw animation overlay frame if not frame 0
- if (animFrame != 0 && animFrame < numSprites) {
- renderNutSprite(renderBitmap, pitch, width, height, hudX, hudY, _hudOverlayNut, animFrame);
- }
-
- debug(5, "Rebel2 HUD: Drawing NUT overlay frame %d/%d at (%d,%d) mouseOffset=(%d,%d)",
- animFrame, numSprites, hudX, hudY, mouseOffsetX, mouseOffsetY);
-
- // Draw secondary HUD overlay if present (DAT_0047fe80)
- if (_hudOverlay2Nut && _hudOverlay2Nut->getNumChars() > 0) {
- int spr2W = _hudOverlay2Nut->getCharWidth(0);
- int spr2H = _hudOverlay2Nut->getCharHeight(0);
- int hud2X = 160 + (mouseOffsetX >> 4) - (spr2W / 2) + _viewX;
- int hud2Y = 182 - (mouseOffsetY >> 4) - spr2H + _viewY;
- renderNutSprite(renderBitmap, pitch, width, height, hud2X, hud2Y, _hudOverlay2Nut, 0);
- }
-}
-
-void InsaneRebel2::renderEmbeddedHudOverlays(byte *renderBitmap, int pitch, int width, int height) {
- // Draw embedded SAN HUD overlays (from IACT chunks)
- // For Handler 7 (Level 3): HUD elements are scattered across the screen
- // For turret handlers: slots 1-2 form a two-part cockpit overlay
-
- for (int hudSlot = 1; hudSlot < 16; hudSlot++) {
- EmbeddedSanFrame &frame = _rebelEmbeddedHud[hudSlot];
- if (!frame.valid || !frame.pixels || frame.width <= 0 || frame.height <= 0)
- continue;
-
- // Handler 25: Skip slot 4 (corridor overlay) in post-rendering.
- // The corridor is a full background image (no color 0 transparent center).
- // Drawing it here would cover enemies. It's already drawn in procPreRendering
- // with transparency to preserve frame persistence for codec 23 delta.
- if (_rebelHandler == 25 && hudSlot == 4) {
- continue;
- }
-
- // Skip small frames at (0,0) - likely animation patches
- if (frame.renderX == 0 && frame.renderY == 0 && frame.width < 50 && frame.height < 60) {
- debug(3, "Rebel2: Skipping small embedded frame at (0,0): slot=%d size=%dx%d",
- hudSlot, frame.width, frame.height);
- continue;
- }
-
- // For Handler 7: handle direction-based frame selection
- if (_rebelHandler == 7) {
- int groupMembers[16];
- int groupCount = 0;
-
- for (int id = 1; id < 16; id++) {
- EmbeddedSanFrame &g = _rebelEmbeddedHud[id];
- if (g.valid && g.renderX == frame.renderX && g.renderY == frame.renderY &&
- g.width == frame.width && g.height == frame.height) {
- groupMembers[groupCount++] = id;
- }
- }
-
- if (groupCount > 1) {
- int selectedOffset = _shipDirectionIndex % groupCount;
- int selectedId = groupMembers[selectedOffset];
-
- // Verify selected frame has pixels
- EmbeddedSanFrame &selectedFrame = _rebelEmbeddedHud[selectedId];
- int nonZero = 0;
- for (int i = 0; i < selectedFrame.width * selectedFrame.height; i++) {
- if (selectedFrame.pixels[i] != 0) nonZero++;
- }
-
- if (nonZero == 0) {
- for (int i = 0; i < groupCount; i++) {
- EmbeddedSanFrame &altFrame = _rebelEmbeddedHud[groupMembers[i]];
- int altNonZero = 0;
- for (int j = 0; j < altFrame.width * altFrame.height; j++) {
- if (altFrame.pixels[j] != 0) altNonZero++;
- }
- if (altNonZero > 0) {
- selectedId = groupMembers[i];
- break;
- }
- }
- }
-
- if (hudSlot != selectedId)
- continue;
- }
- }
-
- // Calculate destination position
- int destX = frame.renderX;
- int destY = frame.renderY;
-
- // Handler 0x26/0x19 turret positioning
- if ((_rebelHandler == 0x26 || _rebelHandler == 0x19) && (hudSlot == 1 || hudSlot == 2)) {
- destX = 160 - frame.width / 2 - frame.renderX;
- destY = 200 - frame.height - frame.renderY;
- }
-
- // Handler 7 large cockpit frame positioning
- if (_rebelHandler == 7 && (hudSlot == 1 || hudSlot == 2) && frame.width > 100) {
- destX = 160 - frame.width / 2 - frame.renderX;
- destY = 170 - frame.height - frame.renderY;
- } else if (_rebelHandler == 7 && destX > 100 && destY > 50) {
- int16 offsetX = (_shipPosX - 160) / 8;
- int16 offsetY = (_shipPosY - 100) / 8;
- destX += offsetX;
- destY += offsetY;
- }
-
- destX += _viewX;
- destY += _viewY;
-
- debug(3, "Rebel2: Rendering embedded HUD slot=%d size=%dx%d at (%d,%d)",
- hudSlot, frame.width, frame.height, destX, destY);
-
- // Draw frame with transparency (pixel 0 and 231 = transparent)
- for (int y = 0; y < frame.height && (destY + y) < height; y++) {
- for (int x = 0; x < frame.width && (destX + x) < pitch; x++) {
- byte pixel = frame.pixels[y * frame.width + x];
- if (pixel != 0 && pixel != 231) {
- int fx = destX + x;
- int fy = destY + y;
- if (fx >= 0 && fy >= 0) {
- renderBitmap[fy * pitch + fx] = pixel;
- }
- }
- }
- }
- }
-}
-
-void InsaneRebel2::renderStatusBarSprites(byte *renderBitmap, int pitch, int width, int height,
- int statusBarY, int32 curFrame) {
- // Draw DISPFONT.NUT status bar sprites (FUN_0041c012 equivalent)
- // DISPFONT.NUT contains:
- // Sprite 1: Status bar background frame
- // Sprites 2-5: Difficulty stars (1-4)
- // Sprite 6: Damage bar fill (with clip rect X=63, Y=9, W=64, H=6)
- // Sprite 7: Damage alert (flashing red when critical)
-
- if (!_smush_cockpitNut)
- return;
-
- // Sprite 1: Status bar background
- if (_smush_cockpitNut->getNumChars() > 1) {
- renderNutSprite(renderBitmap, pitch, width, height, _viewX, statusBarY + _viewY, _smush_cockpitNut, 1);
- }
-
- // Difficulty indicator (sprites 2-5)
- int difficulty = 0; // TODO: Read from game state
- if (difficulty > 3) difficulty = 3;
- int difficultySprite = difficulty + 2;
- if (_smush_cockpitNut->getNumChars() > difficultySprite) {
- renderNutSprite(renderBitmap, pitch, width, height, _viewX, statusBarY + _viewY, _smush_cockpitNut, difficultySprite);
- }
-
- // Damage bar (sprite 6) with clipped width
- if (_smush_cockpitNut->getNumChars() > 6) {
- int drawWidth = (64 * _playerDamage) / 255;
- if (drawWidth < 0) drawWidth = 0;
- if (drawWidth > 64) drawWidth = 64;
-
- const byte *src = _smush_cockpitNut->getCharData(6);
- int sw = _smush_cockpitNut->getCharWidth(6);
- int sh = _smush_cockpitNut->getCharHeight(6);
-
- // Clip rect inside sprite: X=63, Y=9, W=64, H=6
- const int clipX = 63, clipY = 9, clipW = 64, clipH = 6;
-
- if (src && sw > 0 && sh > 0) {
- int maxClipW = sw - clipX;
- if (maxClipW < 0) maxClipW = 0;
- int drawW = MIN(drawWidth, MIN(clipW, maxClipW));
- int drawH = MIN(clipH, sh - clipY);
- if (drawH < 0) drawH = 0;
-
- for (int y = 0; y < drawH; y++) {
- for (int x = 0; x < drawW; x++) {
- int destX = clipX + x + _viewX;
- int destY = statusBarY + clipY + y + _viewY;
- if (destX >= 0 && destX < pitch && destY >= 0 && destY < height) {
- byte pixel = src[(clipY + y) * sw + (clipX + x)];
- if (pixel != 0) {
- renderBitmap[destY * pitch + destX] = pixel;
- }
- }
- }
- }
- }
- }
-
- // Damage alert overlay (sprite 7) when damage > 170 and flashing
- if (_smush_cockpitNut->getNumChars() > 7) {
- if (_playerDamage > 170 && ((curFrame & 8) == 0)) {
- renderNutSprite(renderBitmap, pitch, width, height, 63 + _viewX, statusBarY + 9 + _viewY, _smush_cockpitNut, 7);
- }
- }
-}
-
-void InsaneRebel2::renderHandler7Ship(byte *renderBitmap, int pitch, int width, int height) {
- // Handler 7 Ship Rendering (Third-Person Ship - FLY sprites)
- // Based on FUN_0040d836 lines 173-185:
- // FUN_004236e0(buf, frameInfo, screenX - 0xd4, screenY - 0x82, 0, sprite, frameIdx, 1, 0)
- // The ship sprite is drawn at the perspective-transformed position offset from center.
- // FUN_0041c720 transforms game coords (shipX, shipY) using perspective offsets.
-
- if (_rebelHandler != 7 || !_flyShipSprite || _shipLevelMode == 5)
- return;
-
- int numSprites = _flyShipSprite->getNumChars();
- int spriteIndex = _shipDirectionIndex;
- if (spriteIndex < 0) spriteIndex = 0;
- if (spriteIndex >= numSprites) spriteIndex = numSprites - 1;
-
- // Transform game coordinates to screen coordinates (FUN_0041c720 equivalent)
- // The perspective transform shifts the ship position based on perspective offsets.
- // Close view: FOBJ offset = (-52 - perspX, -45 - perspY), ship at screen center.
- // For now, use a simplified perspective: ship position = center + offset from center
- // scaled by perspective. In the original, FUN_00424510 shifts all FOBJ sprites.
- //
- // Screen position for sprite drawing (FUN_0040d836 line 174):
- // drawX = transformedX - 0xd4, drawY = transformedY - 0x82
- // Where transformedX/Y come from FUN_0041c720(shipX, shipY, perspX, perspY, viewShift)
- //
- // Simplified: screenX = 160 + (shipX - 212) * perspFactor
- // With the perspective formula, objects near center barely move, objects at edges move more.
- int drawX = (_flyShipScreenX - 0xd4) + _perspectiveX;
- int drawY = (_flyShipScreenY - 0x82) + _perspectiveY;
-
- // Convert from game-center-relative to screen coordinates
- // The sprite system expects coordinates relative to the 320x200 frame
- // Center of frame = (160, 100), so offset = game position - game center
- drawX += 160 + _viewX;
- drawY += 100 + _viewY;
-
- // Center the sprite on the position
- int spriteW = _flyShipSprite->getCharWidth(spriteIndex);
- int spriteH = _flyShipSprite->getCharHeight(spriteIndex);
- drawX -= spriteW / 2;
- drawY -= spriteH / 2;
-
- renderNutSprite(renderBitmap, pitch, width, height, drawX, drawY, _flyShipSprite, spriteIndex);
-
- // Laser overlay if firing (same position as ship)
- if (_shipFiring && _flyLaserSprite && _flyLaserSprite->getNumChars() > 0) {
- int laserIndex = spriteIndex % _flyLaserSprite->getNumChars();
- renderNutSprite(renderBitmap, pitch, width, height, drawX, drawY, _flyLaserSprite, laserIndex);
- }
-
- debug("Rebel2 Handler7Ship: draw=(%d,%d) sprite=%d/%d shipPos=(%d,%d) persp=(%d,%d) smoothVel=%d vertIn=%d",
- drawX, drawY, spriteIndex, numSprites, _flyShipScreenX, _flyShipScreenY,
- _perspectiveX, _perspectiveY, _smoothedVelocity, _verticalInput);
-}
-
-void InsaneRebel2::renderHandler8Ship(byte *renderBitmap, int pitch, int width, int height) {
- // Handler 8 Ship Rendering (Third-Person On Foot - POV sprites)
- // Uses _shipSprite (POV001) with position-based offset
-
- if (_rebelHandler != 8 || !_shipSprite || _shipLevelMode == 5)
- return;
-
- // Calculate display offset from raw ship position (FUN_00401ccf lines 88-89)
- int16 displayOffsetX = (_shipPosX - 0xa0) >> 3;
- int16 displayOffsetY = (_shipPosY - 0x28) >> 2;
-
- // Base screen position (low-res: X=160, Y=105)
- int shipScreenX = 0xa0 + displayOffsetX;
- int shipScreenY = 0x69 + displayOffsetY;
-
- int numSprites = _shipSprite->getNumChars();
- int spriteIndex = 0;
-
- // Select sprite based on direction and sprite count
- if (numSprites >= 35) {
- spriteIndex = _shipDirectionH * 7 + _shipDirectionV;
- if (spriteIndex >= numSprites) spriteIndex = numSprites - 1;
- } else if (numSprites >= 25) {
- int vDir5 = (_shipDirectionV * 5) / 7;
- spriteIndex = _shipDirectionH * 5 + vDir5;
- if (spriteIndex >= numSprites) spriteIndex = numSprites - 1;
- } else if (numSprites >= 5) {
- spriteIndex = _shipDirectionH;
- if (spriteIndex >= numSprites) spriteIndex = numSprites - 1;
- } else if (numSprites == 2) {
- spriteIndex = _shipFiring ? 1 : 0;
- }
-
- // Center sprite at position
- int spriteW = _shipSprite->getCharWidth(spriteIndex);
- int spriteH = _shipSprite->getCharHeight(spriteIndex);
- int drawX = shipScreenX - spriteW / 2 + _viewX;
- int drawY = shipScreenY - spriteH / 2 + _viewY;
-
- renderNutSprite(renderBitmap, pitch, width, height, drawX, drawY, _shipSprite, spriteIndex);
-
- // Shadow sprite (POV004 / DAT_0047e028): drawn at same position as primary ship.
- // Original FUN_401CCF lines 91-92 uses param_5 & 1 (firing flag) as sprite index
- // for both primary and shadow, NOT the direction-based spriteIndex.
- if (_shipSprite2) {
- int shadowIndex = _shipFiring ? 1 : 0;
- if (shadowIndex < _shipSprite2->getNumChars()) {
- renderNutSprite(renderBitmap, pitch, width, height, drawX, drawY, _shipSprite2, shadowIndex);
- }
- }
-
- debug("Rebel2 Handler8: Ship at (%d,%d) raw(%d,%d) offset(%d,%d) sprite=%d/%d dir=(%d,%d)",
- drawX, drawY, _shipPosX, _shipPosY, displayOffsetX, displayOffsetY,
- spriteIndex, numSprites, _shipDirectionH, _shipDirectionV);
-}
-
-// Handler 25: Draw GRD001 (wall/cockpit overlay) in procPostRendering.
-// Per original FUN_0041DB5E, GRD sprites are drawn AFTER FOBJ enemies, before GRD002.
-//
-// From FUN_0041db5e disassembly (lines 202-221):
-// - Mode 1 with damage==0: Width halved (left half only, pixels 0-159)
-// - Mode 4 with damage==0: Width halved AND buffer offset (right half only, pixels 160-319)
-// - All other cases: Full width (320 pixels)
-void InsaneRebel2::renderHandler25ShipPre(byte *renderBitmap, int pitch, int width, int height) {
- if (_rebelHandler != 25)
- return;
-
- if (!_grd001Sprite || _grd001Sprite->getNumChars() <= 0)
- return;
-
- // CRITICAL: Clip height to 180 (0xb4) to avoid drawing over status bar
- const int clipHeight = 180;
- int renderHeight = MIN(height, clipHeight);
-
- // Draw _grd001Sprite based on _grdSpriteMode (DAT_00457900)
- // Each mode has specific conditions from FUN_0041db5e:
- bool shouldDraw = false;
- bool useHalfWidth = false;
- bool useRightHalf = false;
-
- // Mode 1 (lines 202-210): Draw with width halving when uncovered
- if (_grdSpriteMode == 1) {
- shouldDraw = true;
- useHalfWidth = (_rebelDamageLevel == 0); // Half width when uncovered
- }
- // Mode 2 (lines 222-224): Only draw when damaged (covered)
- else if (_grdSpriteMode == 2 && _rebelDamageLevel != 0) {
- shouldDraw = true;
- }
- // Mode 3 (lines 225-228): Always draw full width
- else if (_grdSpriteMode == 3) {
- shouldDraw = true;
- }
- // Mode 4 (lines 211-221): Draw to right half when uncovered
- else if (_grdSpriteMode == 4) {
- shouldDraw = true;
- useHalfWidth = (_rebelDamageLevel == 0);
- useRightHalf = (_rebelDamageLevel == 0);
- }
-
- if (shouldDraw) {
- int spriteW = _grd001Sprite->getCharWidth(0);
- int spriteH = _grd001Sprite->getCharHeight(0);
- int16 spriteXOffset = _grd001Sprite->getCharXOffset(0);
- int16 spriteYOffset = _grd001Sprite->getCharYOffset(0);
-
- int drawX = _rebelViewOffset2X + spriteXOffset;
- int drawY = _rebelViewOffset2Y + spriteYOffset;
-
- // Apply width-halving logic from original assembly:
- // When damage==0 (uncovered), the original halves DAT_00482234 (buffer width)
- // This clips the sprite to only half the screen.
- int renderWidth = width;
- byte *dstBitmap = renderBitmap;
-
- if (useHalfWidth) {
- renderWidth = width / 2; // Clip to half width (160 pixels)
-
- if (useRightHalf) {
- // Mode 4: Draw to right half by offsetting the destination buffer
- // Original: DAT_00482230 += DAT_00482234 (adds 160 to buffer start)
- // This makes drawing appear on the right half (pixels 160-319)
- dstBitmap = renderBitmap + (width / 2);
- }
- }
-
- renderNutSprite(dstBitmap, pitch, renderWidth, renderHeight, drawX, drawY, _grd001Sprite, 0);
-
- debug("Rebel2 Handler25 PRE: GRD001 at (%d,%d) nutOff(%d,%d) viewOff(%d,%d) size(%d,%d) mode=%d dmg=%d halfW=%d rightHalf=%d renderW=%d",
- drawX, drawY, spriteXOffset, spriteYOffset, _rebelViewOffset2X, _rebelViewOffset2Y,
- spriteW, spriteH, _grdSpriteMode, _rebelDamageLevel, useHalfWidth ? 1 : 0, useRightHalf ? 1 : 0, renderWidth);
- }
-}
-
-void InsaneRebel2::renderHandler25Ship(byte *renderBitmap, int pitch, int width, int height) {
- // Handler 25 POST-rendering: Draw GRD002 (character sprite) on top of enemies.
- // GRD001 (wall/cockpit) is drawn before this via renderHandler25ShipPre().
- //
- // From FUN_0041db5e disassembly (lines 230-248):
- // GRD002 is drawn LAST (after enemies) so the character appears in front.
-
- if (_rebelHandler != 25)
- return;
-
- // CRITICAL: Clip height to 180 (0xb4) to avoid drawing over status bar
- const int clipHeight = 180;
- int renderHeight = MIN(height, clipHeight);
-
- // _grd002Sprite (GRD002) is always drawn if it exists (from FUN_41DB5E line 230)
- // The sprite index is calculated based on damage level and aiming position
- // From FUN_0041db5e lines 160-168:
- // If damage == 0: index = yZone * 5 + xZone + 5 (aiming-based, 5-14)
- // If damage != 0:
- // If direction == 0: index = 5 - damage (0-5, covered left)
- // If direction != 0: index = 25 - damage (20-25, covered right)
- if (_grd002Sprite && _grd002Sprite->getNumChars() > 0) {
- // Calculate sprite index based on damage level and direction
- int spriteIdx;
- int numSprites = _grd002Sprite->getNumChars();
-
- // Determine if we should mirror the sprite (from FUN_41DB5E lines 231-235)
- // Mirror when: direction != 0 AND damage == 0 (fully uncovered, facing right)
- bool shouldMirror = (_rebelFlightDir != 0) && (_rebelDamageLevel == 0);
-
- if (_rebelDamageLevel == 0) {
- // Uncovered state: use aiming-based sprite selection (5-14)
- // Calculate zones from crosshair position relative to playable area
- // From FUN_41DB5E lines 155-164
- //
- // The playable area bounds are defined by corridor boundaries.
- // xZone = 0-4 (left to right), yZone = 0-1 (top to bottom)
- // Default to center if bounds not set
- int16 areaLeft = (_corridorLeftX > 0) ? _corridorLeftX : 0;
- int16 areaRight = (_corridorRightX > 0) ? _corridorRightX : 320;
- int16 areaTop = (_corridorTopY > 0) ? _corridorTopY : 0;
- int16 areaBottom = (_corridorBottomY > 0) ? _corridorBottomY : 180;
-
- // Get crosshair position (using mouse position scaled to game coords)
- int16 crosshairX = _vm->_mouse.x;
- int16 crosshairY = _vm->_mouse.y;
- if (_player && _player->_width > 320) {
- crosshairX = (crosshairX * 320) / _player->_width;
- crosshairY = (crosshairY * 200) / _player->_height;
- }
-
- // Calculate zone widths
- int areaWidth = areaRight - areaLeft;
- int areaHeight = areaBottom - areaTop;
- int zoneWidth = (areaWidth > 0) ? (areaWidth + 3) / 4 : 80; // Divide into ~4 zones
- int zoneHeight = (areaHeight > 0) ? areaHeight / 2 : 90; // Divide into 2 zones
-
- // Calculate xZone (0-4) and yZone (0-1) from crosshair position
- int xZone = (zoneWidth > 0) ? ((zoneWidth / 2) + (crosshairX - areaLeft)) / zoneWidth : 2;
- int yZone = (zoneHeight > 0) ? ((zoneHeight / 2) + (crosshairY - areaTop)) / zoneHeight : 0;
-
- // Clamp to valid ranges
- if (xZone < 0) xZone = 0;
- if (xZone > 4) xZone = 4;
- if (yZone < 0) yZone = 0;
- if (yZone > 1) yZone = 1;
-
- // Direction-based sprite flip logic (line 161-162 in decompiled)
- // if (DAT_00457902 == (uVar7 & 1)) { local_58 = 4 - local_58; }
- if (_rebelFlightDir == (yZone & 1)) {
- xZone = 4 - xZone;
- }
-
- spriteIdx = yZone * 5 + xZone + 5;
- } else {
- // Transitioning/covered state: use direction-based sprite
- // From FUN_41DB5E lines 166-168:
- // sVar8 = ((-(ushort)(DAT_00457902 == 0) & 0xffec) + 0x19) - DAT_0045790a
- // direction == 0: 5 - damage
- // direction != 0: 25 - damage
- if (_rebelFlightDir == 0) {
- // Direction 0: sprites 0-5 (transition left)
- spriteIdx = 5 - _rebelDamageLevel;
- } else {
- // Direction 1: sprites 20-25 (transition right)
- spriteIdx = 25 - _rebelDamageLevel;
- }
- }
-
- // Clamp to valid range
- if (spriteIdx < 0) spriteIdx = 0;
- if (spriteIdx >= numSprites) spriteIdx = numSprites - 1;
-
- int spriteW = _grd002Sprite->getCharWidth(spriteIdx);
- int spriteH = _grd002Sprite->getCharHeight(spriteIdx);
-
- // Position calculation from FUN_41DB5E lines 237-247:
- // GRD002 explicitly adds sprite internal offsets from NUT header:
- //
- // Normal case (direction==0 OR damage!=0):
- // local_60 = sprite_internal_x_offset (from NUT header +0x12)
- // X = DAT_00457910 + local_60
- // Y = sprite_internal_y_offset (from NUT header +0x14) + DAT_00457912
- //
- // Mirrored case (direction!=0 AND damage==0):
- // local_60 = 320 - sprite_width - sprite_internal_x_offset
- // X = DAT_00457910 + local_60
- // Y = sprite_internal_y_offset + DAT_00457912
- //
- // Now using actual NUT sprite offsets from NutRenderer!
- int16 spriteXOffset = _grd002Sprite->getCharXOffset(spriteIdx);
- int16 spriteYOffset = _grd002Sprite->getCharYOffset(spriteIdx);
-
- int drawX, drawY;
-
- if (shouldMirror) {
- // Mirrored position: X = DAT_00457910 + (320 - sprite_width - sprite_x_offset)
- // From assembly lines 240-243
- drawX = _rebelViewOffset2X + (320 - spriteW - spriteXOffset);
- } else {
- // Normal position: X = DAT_00457910 + sprite_internal_x_offset
- // From assembly line 238
- drawX = _rebelViewOffset2X + spriteXOffset;
- }
-
- // Y = sprite_internal_y_offset + DAT_00457912
- // From assembly line 246
- drawY = spriteYOffset + _rebelViewOffset2Y;
-
- renderNutSpriteMirrored(renderBitmap, pitch, width, renderHeight, drawX, drawY, _grd002Sprite, spriteIdx, shouldMirror);
-
- debug("Rebel2 Handler25: GRD002 at (%d,%d) nutOffset(%d,%d) viewOffset(%d,%d) size(%d,%d) spriteIdx=%d damage=%d dir=%d mirror=%d",
- drawX, drawY, spriteXOffset, spriteYOffset, _rebelViewOffset2X, _rebelViewOffset2Y, spriteW, spriteH, spriteIdx, _rebelDamageLevel, _rebelFlightDir, shouldMirror ? 1 : 0);
- }
-}
-
-void InsaneRebel2::renderFallbackShip(byte *renderBitmap, int pitch, int width, int height) {
- // Fallback: Use embedded HUD frame as ship sprite (Level 3 style)
- // userId=11 contains the ship sprite strip
-
- if ((_rebelHandler != 7 && _rebelHandler != 8) || _shipLevelMode == 5)
- return;
-
- // Skip if we have proper sprites
- if (_rebelHandler == 7 && _flyShipSprite)
- return;
- if (_rebelHandler == 8 && _shipSprite)
- return;
-
- EmbeddedSanFrame &shipFrame = _rebelEmbeddedHud[11];
- if (!shipFrame.valid || !shipFrame.pixels || shipFrame.width <= 0 || shipFrame.height <= 0)
- return;
-
- // Calculate display offset
- int16 displayOffsetX = (_shipPosX - 0xa0) >> 3;
- int16 displayOffsetY = (_shipPosY - 0x28) >> 2;
- int shipScreenX = 0xa0 + displayOffsetX;
- int shipScreenY = 0x69 + displayOffsetY;
-
- // Detect sprite strip layout
- int spriteW = shipFrame.width;
- int spriteH = shipFrame.height;
- int srcX = 0, srcY = 0;
- int numHorizontal = 1, numVertical = 1;
-
- if (spriteW >= 200 && spriteW % 5 == 0) {
- numHorizontal = 5;
- spriteW = shipFrame.width / 5;
- }
- if (spriteH >= 350 && spriteH % 7 == 0) {
- numVertical = 7;
- spriteH = shipFrame.height / 7;
- }
-
- int hDir = MIN((int)_shipDirectionH, numHorizontal - 1);
- int vDir = MIN((int)_shipDirectionV, numVertical - 1);
- srcX = hDir * spriteW;
- srcY = vDir * spriteH;
-
- int drawX = shipScreenX - spriteW / 2 + _viewX;
- int drawY = shipScreenY - spriteH / 2 + _viewY;
-
- // Blit from embedded HUD
- for (int y = 0; y < spriteH && (drawY + y) < height; y++) {
- if (drawY + y < 0) continue;
- for (int x = 0; x < spriteW && (drawX + x) < width; x++) {
- if (drawX + x < 0) continue;
- int srcIdx = (srcY + y) * shipFrame.width + (srcX + x);
- byte pixel = shipFrame.pixels[srcIdx];
- if (pixel != 0 && pixel != 231) {
- int dstIdx = (drawY + y) * pitch + (drawX + x);
- renderBitmap[dstIdx] = pixel;
- }
- }
- }
-
- debug("Rebel2: Ship (fallback) at (%d,%d) strip=(%d,%d) of (%dx%d) dir=(%d,%d)",
- drawX, drawY, srcX, srcY, numHorizontal, numVertical, _shipDirectionH, _shipDirectionV);
-}
-
-void InsaneRebel2::renderEnemyOverlays(byte *renderBitmap, int pitch, int width, int height, int videoWidth) {
- // Draw enemy indicator brackets for active enemies
- //
- // NOTE: Do NOT fill destroyed enemy areas with black. The original game does not do this.
- // When an enemy is destroyed:
- // 1. setBit(enemy_id) disables the enemy in the bit table
- // 2. clearBit(dependency_id) enables dependent objects (explosion animations)
- // 3. SKIP chunks in the video cause enemy FOBJ sprites to be skipped (via procSKIP)
- // 4. renderExplosions() draws the explosion animation from the 5-slot system
- // 5. The background video shows through where the enemy was
-
- // Draw green brackets for active enemies (Easy/Medium difficulty only)
- if (_difficulty >= 2)
- return;
-
- // FOBJ sprites are rendered with _fobjOffsetX/Y applied (set from _rebelViewOffsetX/Y
- // for Handler 25). Brackets must use the same offset so they align with the sprites.
- int fobjOffX = _player ? _player->_fobjOffsetX : 0;
- int fobjOffY = _player ? _player->_fobjOffsetY : 0;
-
- Common::Rect viewRect(_viewX, _viewY, _viewX + videoWidth, _viewY + 200);
-
- for (Common::List<enemy>::iterator it = _enemies.begin(); it != _enemies.end(); ++it) {
- if (it->destroyed || !it->active || isBitSet(it->id))
- continue;
-
- Common::Rect r(it->rect.left + fobjOffX, it->rect.top + fobjOffY,
- it->rect.right + fobjOffX, it->rect.bottom + fobjOffY);
- if (r.right <= viewRect.left || r.left >= viewRect.right ||
- r.bottom <= viewRect.top || r.top >= viewRect.bottom)
- continue;
-
- const byte color = 5; // Green
- drawCornerBrackets(renderBitmap, pitch, width, height, r.left, r.top, r.width(), r.height(), color);
- }
-}
-
-// Dispatcher â calls per-handler explosion render function.
-// Original code has separate functions per handler, each with its own
-// position transformation, scale thresholds, and secondary NUT rendering.
-void InsaneRebel2::renderExplosions(byte *renderBitmap, int pitch, int width, int height) {
- switch (_rebelHandler) {
- case 0x26:
- renderTurretExplosions(renderBitmap, pitch, width, height);
- break;
- case 8:
- renderVehicleExplosions(renderBitmap, pitch, width, height);
- break;
- case 7:
- renderSpaceExplosions(renderBitmap, pitch, width, height);
- break;
- case 25:
- renderHandler25Explosions(renderBitmap, pitch, width, height);
- break;
- default:
- break;
- }
-}
-
-// FUN_409FBC â Handler 0x26 (Turret/Cockpit) explosion rendering.
-// Position: Uses FUN_0041c720 for 3Dâ2D projection. At low-res, world coords â screen coords.
-// Scale thresholds: Fixed (<11, <21).
-// Secondary NUT: DAT_0047fe80 (rendered if DAT_0047a7fc >= 0).
-// Hi-res: Coordinates doubled when DAT_0047a808 >= 2.
-void InsaneRebel2::renderTurretExplosions(byte *renderBitmap, int pitch, int width, int height) {
- if (!_smush_iconsNut)
- return;
-
- for (int i = 0; i < 5; i++) {
- if (!_explosions[i].active)
- continue;
-
- if (_explosions[i].counter <= 0) {
- _explosions[i].active = false;
- continue;
- }
-
- // FUN_409FBC: Fixed thresholds (0x0b=11, 0x15=21)
- int baseIndex;
- if (_explosions[i].scale < 11) {
- baseIndex = 9; // Small (sprites 11-20)
- } else if (_explosions[i].scale < 21) {
- baseIndex = 19; // Medium (sprites 21-30)
- } else {
- baseIndex = 29; // Large (sprites 31-40)
- }
-
- int spriteIndex = baseIndex + (12 - _explosions[i].counter);
-
- // Position: world coords passed through FUN_0041c720 (3Dâ2D projection).
- // At 320x200 low-res turret view, projection is effectively identity.
- int screenX = _explosions[i].x;
- int screenY = _explosions[i].y;
-
- if (_smush_iconsNut->getNumChars() > spriteIndex) {
- int ew = _smush_iconsNut->getCharWidth(spriteIndex);
- int eh = _smush_iconsNut->getCharHeight(spriteIndex);
- renderNutSprite(renderBitmap, pitch, width, height,
- screenX - ew / 2, screenY - eh / 2, _smush_iconsNut, spriteIndex);
- }
-
- _explosions[i].counter--;
- }
-}
-
-// FUN_402696 â Handler 8 (Third-Person On-Foot) explosion rendering.
-// Position: World coords minus camera offset (DAT_0043e006/DAT_0043e008 = _viewX/_viewY).
-// Scale thresholds: Fixed (<11, <21) â same as handler 0x26.
-// Secondary NUT: None (only DAT_0047a828).
-void InsaneRebel2::renderVehicleExplosions(byte *renderBitmap, int pitch, int width, int height) {
- if (!_smush_iconsNut)
- return;
-
- for (int i = 0; i < 5; i++) {
- if (!_explosions[i].active)
- continue;
-
- if (_explosions[i].counter <= 0) {
- _explosions[i].active = false;
- continue;
- }
-
- // FUN_402696: Fixed thresholds (0x0b=11, 0x15=21)
- int baseIndex;
- if (_explosions[i].scale < 11) {
- baseIndex = 9;
- } else if (_explosions[i].scale < 21) {
- baseIndex = 19;
- } else {
- baseIndex = 29;
- }
-
- int spriteIndex = baseIndex + (12 - _explosions[i].counter);
-
- // FUN_402696 line 22-23: screenX = worldX - DAT_0043e006, screenY = worldY - DAT_0043e008
- int screenX = _explosions[i].x - _viewX;
- int screenY = _explosions[i].y - _viewY;
-
- if (_smush_iconsNut->getNumChars() > spriteIndex) {
- int ew = _smush_iconsNut->getCharWidth(spriteIndex);
- int eh = _smush_iconsNut->getCharHeight(spriteIndex);
- renderNutSprite(renderBitmap, pitch, width, height,
- screenX - ew / 2, screenY - eh / 2, _smush_iconsNut, spriteIndex);
- }
-
- _explosions[i].counter--;
- }
-}
-
-// FUN_40F1C5 â Handler 7 (Third-Person Ship) explosion rendering.
-// Position: Uses FUN_0041c720 for 3Dâ2D projection.
-// Scale thresholds: Resolution-dependent (low-res: <11/<21, high-res: <21/<41).
-// Secondary NUT: DAT_0047ff00 (FLY004, rendered if DAT_0047a7fc >= 0).
-void InsaneRebel2::renderSpaceExplosions(byte *renderBitmap, int pitch, int width, int height) {
- if (!_smush_iconsNut)
- return;
-
- // --- Part 1: Space shot explosions (FUN_40F1C5 lines 19-60) ---
- for (int i = 0; i < 5; i++) {
- if (!_explosions[i].active)
- continue;
-
- if (_explosions[i].counter <= 0) {
- _explosions[i].active = false;
- continue;
- }
-
- // FUN_40F1C5 lines 41-51: Resolution-dependent thresholds.
- // Low-res (DAT_0047a808 < 2): thresholds 20, 10
- // High-res: thresholds 40, 20
- // We run at low-res (320x200), so use 10/20 (same as fixed handlers).
- int baseIndex;
- if (_explosions[i].scale < 11) {
- baseIndex = 9;
- } else if (_explosions[i].scale < 21) {
- baseIndex = 19;
- } else {
- baseIndex = 29;
- }
-
- int spriteIndex = baseIndex + (12 - _explosions[i].counter);
-
- // Position: world coords through FUN_0041c720 (3Dâ2D projection).
- // At low-res, this is close to identity for the ship view.
- int screenX = _explosions[i].x;
- int screenY = _explosions[i].y;
-
- if (_smush_iconsNut->getNumChars() > spriteIndex) {
- int ew = _smush_iconsNut->getCharWidth(spriteIndex);
- int eh = _smush_iconsNut->getCharHeight(spriteIndex);
- renderNutSprite(renderBitmap, pitch, width, height,
- screenX - ew / 2, screenY - eh / 2, _smush_iconsNut, spriteIndex);
- }
-
- _explosions[i].counter--;
- }
-
- // --- Part 2: Corridor/zone hit explosion (FUN_40F1C5 lines 61-85) ---
- // Rendered when _hitCooldown > 0 (DAT_0044374c). Decrement happens HERE
- // (matching original where FUN_40F1C5 decrements DAT_0044374c during render).
- // _spaceShotDirection (DAT_0044374e) determines explosion side:
- // 0 = left side (hit left boundary), 1 = right side (hit right boundary)
- // 2 = bottom (zone push down), 3 = top (zone push up)
- // Sprite frames: 0x15 - cooldown = 21 - cooldown (frames 12â21 as cooldown 9â0)
- if (_hitCooldown != 0) {
- _hitCooldown--;
-
- int numChars = _smush_iconsNut->getNumChars();
- int spriteIndex = 0x15 - _hitCooldown; // 21 - remaining cooldown
-
- if (spriteIndex >= 0 && spriteIndex < numChars) {
- // Compute ship screen position (simplified FUN_0041c720 transform)
- int shipDrawX = (_flyShipScreenX - 0xd4) + _perspectiveX + 160 + _viewX;
- int shipDrawY = (_flyShipScreenY - 0x82) + _perspectiveY + 100 + _viewY;
-
- // Per-direction offset from ship center.
- // Original uses lookup tables (DAT_004438da etc.) indexed by
- // _shipDirectionIndex (35 entries per direction). We approximate
- // with fixed offsets since we don't have the table data.
- int offsetX = 0, offsetY = 0;
- switch (_spaceShotDirection) {
- case 0: // Left wall hit â explosion on left side of ship
- offsetX = -35;
- break;
- case 1: // Right wall hit â explosion on right side of ship
- offsetX = 35;
- break;
- case 2: // Zone push down â explosion on bottom
- offsetY = 20;
- break;
- case 3: // Zone push up â explosion on top
- offsetY = -20;
- break;
- default:
- break;
- }
-
- int drawX = shipDrawX + offsetX;
- int drawY = shipDrawY + offsetY;
-
- int ew = _smush_iconsNut->getCharWidth(spriteIndex);
- int eh = _smush_iconsNut->getCharHeight(spriteIndex);
- renderNutSprite(renderBitmap, pitch, width, height,
- drawX - ew / 2, drawY - eh / 2, _smush_iconsNut, spriteIndex);
-
- debug("Rebel2 H7 corridor explosion: dir=%d frame=%d pos=(%d,%d) cooldown=%d",
- _spaceShotDirection, spriteIndex, drawX, drawY, _hitCooldown);
- }
- }
-}
-
-// FUN_41F29A â Handler 25 (FPS/Mixed) explosion rendering.
-// Position: World coords + view offset (DAT_0045790c/DAT_0045790e = _rebelViewOffsetX/_rebelViewOffsetY).
-// Scale thresholds: Resolution-dependent (same formula as Handler 7).
-// Secondary NUT: DAT_00482260 (hi-res HUD alternative, rendered if DAT_0047a7fc >= 0).
-// Note: No per-frame sound panning update (unlike handlers 0x26, 8, 7).
-void InsaneRebel2::renderHandler25Explosions(byte *renderBitmap, int pitch, int width, int height) {
- if (!_smush_iconsNut)
- return;
-
- for (int i = 0; i < 5; i++) {
- if (!_explosions[i].active)
- continue;
-
- if (_explosions[i].counter <= 0) {
- _explosions[i].active = false;
- continue;
- }
-
- // FUN_41F29A lines 27-37: Resolution-dependent thresholds (same as Handler 7).
- int baseIndex;
- if (_explosions[i].scale < 11) {
- baseIndex = 9;
- } else if (_explosions[i].scale < 21) {
- baseIndex = 19;
- } else {
- baseIndex = 29;
- }
-
- int spriteIndex = baseIndex + (12 - _explosions[i].counter);
-
- // FUN_41F29A line 22-23: screenX = worldX + DAT_0045790c, screenY = worldY + DAT_0045790e
- int screenX = _explosions[i].x + _rebelViewOffsetX;
- int screenY = _explosions[i].y + _rebelViewOffsetY;
-
- if (_smush_iconsNut->getNumChars() > spriteIndex) {
- int ew = _smush_iconsNut->getCharWidth(spriteIndex);
- int eh = _smush_iconsNut->getCharHeight(spriteIndex);
- renderNutSprite(renderBitmap, pitch, width, height,
- screenX - ew / 2, screenY - eh / 2, _smush_iconsNut, spriteIndex);
- }
-
- _explosions[i].counter--;
- }
-}
-
-// Dispatcher - calls appropriate render function based on current handler
-void InsaneRebel2::renderLaserShots(byte *renderBitmap, int pitch, int width, int height) {
- switch (_rebelHandler) {
- case 0x26: // Turret - FUN_40AD63
- renderTurretLaserShots(renderBitmap, pitch, width, height);
- break;
- case 8: // Vehicle - FUN_402ED0
- renderVehicleLaserShots(renderBitmap, pitch, width, height);
- break;
- case 7: // Space combat - FUN_40FADF
- renderSpaceLaserShots(renderBitmap, pitch, width, height);
- break;
- case 25: // Speeder bike - FUN_0041f004
- renderHandler25LaserShots(renderBitmap, pitch, width, height);
- break;
- default:
- // No laser rendering for other handlers
- break;
- }
-}
-
-// Handler 0x26 Turret laser rendering (FUN_40AD63)
-// Gun positions depend on _rebelLevelType (DAT_004436de)
-void InsaneRebel2::renderTurretLaserShots(byte *renderBitmap, int pitch, int width, int height) {
- // Uses pre-initialized _laserTexture from sprite 5 of CPITIMAG.NUT
-
- int16 maxDuration = getShotMaxDuration();
-
- for (int i = 0; i < 2; i++) {
- if (_turretShots[i].counter <= 0)
- continue;
-
- // Calculate sound panning from target X position (FUN_004262f0 call)
- // sVar1 = ((2 - counter) * (targetX - 160)) / 2, clamped to [-127, 127]
- int16 pan = ((2 - _turretShots[i].counter) * (_turretShots[i].targetX - _viewX - 160)) / 2;
- pan = CLIP<int16>(pan, -127, 127);
- // TODO: Apply panning to sound channel i+1
-
- int16 targetX = _turretShots[i].targetX;
- int16 targetY = _turretShots[i].targetY;
- int16 progress = maxDuration - _turretShots[i].counter;
-
- // Gun positions based on level type (from FUN_40AD63 switch statement)
- // Parameters from assembly: widthScale=0xC(12), heightScale=8, thickness=0xC(12)
- switch (_rebelLevelType) {
- case 1:
- // Type 1: 3 guns (triple cannon configuration)
- // Gun 1: (0x136, 0xaa) = (310, 170)
- // Gun 2: (0xa0, 0x17c) = (160, 380)
- // Gun 3: (0x0a, 0xaa) = (10, 170)
- drawLaserBeam(renderBitmap, pitch, width, height,
- 310 + _viewX, 170 + _viewY, targetX, targetY,
- progress, maxDuration, 12, 8, 12);
-
- drawLaserBeam(renderBitmap, pitch, width, height,
- 160 + _viewX, 380 + _viewY, targetX, targetY,
- progress, maxDuration, 12, 8, 12);
-
- drawLaserBeam(renderBitmap, pitch, width, height,
- 10 + _viewX, 170 + _viewY, targetX, targetY,
- progress, maxDuration, 12, 8, 12);
- break;
-
- case 2:
- case 5:
- // Type 2/5: 2 guns (wing cannons)
- // Left: (0x6e, 0xe6) = (110, 230)
- // Right: (0xd2, 0xe6) = (210, 230)
- // Assembly uses widthScale=0x19(25) for these types
- drawLaserBeam(renderBitmap, pitch, width, height,
- 110 + _viewX, 230 + _viewY, targetX, targetY,
- progress, maxDuration, 25, 8, 25);
-
- drawLaserBeam(renderBitmap, pitch, width, height,
- 210 + _viewX, 230 + _viewY, targetX, targetY,
- progress, maxDuration, 25, 8, 25);
- break;
-
- case 6:
- // Type 6: 2 guns (offscreen - cinematic effect)
- // Gun 1: (-100, 0)
- // Gun 2: (0, 0)
- drawLaserBeam(renderBitmap, pitch, width, height,
- -100 + _viewX, 0 + _viewY, targetX, targetY,
- progress, maxDuration, 25, 8, 25);
-
- drawLaserBeam(renderBitmap, pitch, width, height,
- 0 + _viewX, 0 + _viewY, targetX, targetY,
- progress, maxDuration, 25, 8, 25);
- break;
-
- default:
- // Default: 2 guns with alternating pattern based on shot sequence
- // When seqNum & 1 == 0: Left (10, 50), Right (310, 130)
- // When seqNum & 1 == 1: Left (310, 50), Right (10, 130)
- if ((_turretShots[i].seqNum & 1) == 0) {
- drawLaserBeam(renderBitmap, pitch, width, height,
- 10 + _viewX, 50 + _viewY, targetX, targetY,
- progress, maxDuration, 25, 8, 25);
-
- drawLaserBeam(renderBitmap, pitch, width, height,
- 310 + _viewX, 130 + _viewY, targetX, targetY,
- progress, maxDuration, 25, 8, 25);
- } else {
- drawLaserBeam(renderBitmap, pitch, width, height,
- 310 + _viewX, 50 + _viewY, targetX, targetY,
- progress, maxDuration, 25, 8, 25);
-
- drawLaserBeam(renderBitmap, pitch, width, height,
- 10 + _viewX, 130 + _viewY, targetX, targetY,
- progress, maxDuration, 25, 8, 25);
- }
- break;
- }
-
- _turretShots[i].counter--;
- }
-}
-
-// Handler 8 Vehicle laser rendering (FUN_402ED0)
-// In the original, the laser is a short muzzle flash from gun barrel toward ship center,
-// NOT a projectile traveling across the screen. The "hit" effect is handled separately.
-void InsaneRebel2::renderVehicleLaserShots(byte *renderBitmap, int pitch, int width, int height) {
- // No NUT check needed - uses pre-initialized _laserTexture
-
- int16 maxDuration = getShotMaxDuration();
-
- for (int i = 0; i < 2; i++) {
- if (_vehicleShots[i].counter <= 0)
- continue;
-
- // Calculate sound panning from STORED target position (FUN_402ED0 lines 24-51)
- // pan = ((2 - counter) * (targetX - 160)) / 2, clamped to [-127, 127]
- int16 pan = ((2 - _vehicleShots[i].counter) * (_vehicleShots[i].targetX - _viewX - 160)) / 2;
- pan = CLIP<int16>(pan, -127, 127);
- // TODO: Apply panning
-
- // Calculate positions from CURRENT ship position (FUN_402ED0 lines 53-122)
- // The original game draws the laser from gun position toward ship center,
- // creating a short muzzle flash effect (7 pixels horizontal, 25 pixels vertical).
- //
- // Low-res formula (DAT_0047a808 < 2):
- // shipScreenY = ((shipPosY - 0x28) >> 2) + 0x69 = ((shipPosY - 40) >> 2) + 105
- // shipScreenX = ((shipPosX - 0xa0) >> 3) + 0xa0 = ((shipPosX - 160) >> 3) + 160
- // gunY = ((shipPosY - 0x28) >> 2) + 0x82 = shipScreenY + 25
- // gunX = ((shipPosX - 0xa0) >> 3) + 0xa7 = shipScreenX + 7
- int16 shipScreenX = ((_shipPosX - 160) >> 3) + 160;
- int16 shipScreenY = ((_shipPosY - 40) >> 2) + 105;
- int16 gunX = shipScreenX + 7;
- int16 gunY = shipScreenY + 25;
-
- int16 progress = maxDuration - _vehicleShots[i].counter;
-
- // Draw beam from gun toward ship center (muzzle flash effect)
- // From FUN_402ED0: widthScale=0x14(20), heightScale=8, thickness=4
- // Parameters: gunX, gunY -> shipScreenX, shipScreenY (NOT the stored target!)
- drawLaserBeam(renderBitmap, pitch, width, height,
- gunX + _viewX, gunY + _viewY,
- shipScreenX + _viewX, shipScreenY + _viewY,
- progress, maxDuration, 20, 8, 4);
-
- _vehicleShots[i].counter--;
- }
-}
-
-// Handler 7 Space combat laser rendering (FUN_40FADF)
-// Dual beams from left and right gun positions
-void InsaneRebel2::renderSpaceLaserShots(byte *renderBitmap, int pitch, int width, int height) {
- // No NUT check needed - uses pre-initialized _laserTexture
-
- int16 maxDuration = getShotMaxDuration();
-
- for (int i = 0; i < 2; i++) {
- if (_spaceShots[i].counter <= 0)
- continue;
-
- // Calculate sound panning
- int16 pan = ((_spaceShots[i].targetX - 160) * (2 - _spaceShots[i].counter)) / 2;
- pan = CLIP<int16>(pan, -127, 127);
- // TODO: Apply panning
-
- int16 targetX = _spaceShots[i].targetX;
- int16 targetY = _spaceShots[i].targetY;
- int16 leftGunX = _spaceShots[i].leftGunX;
- int16 leftGunY = _spaceShots[i].leftGunY;
- int16 rightGunX = _spaceShots[i].rightGunX;
- int16 rightGunY = _spaceShots[i].rightGunY;
- int16 progress = maxDuration - _spaceShots[i].counter;
-
- // Draw dual beams
- // From FUN_40FADF: widthScale=0xC(12), heightScale=4, thickness=6
- // Left gun beam
- drawLaserBeam(renderBitmap, pitch, width, height,
- leftGunX, leftGunY, targetX, targetY,
- progress, maxDuration, 12, 4, 6);
-
- // Right gun beam
- drawLaserBeam(renderBitmap, pitch, width, height,
- rightGunX, rightGunY, targetX, targetY,
- progress, maxDuration, 12, 4, 6);
-
- _spaceShots[i].counter--;
- }
-}
-
-// Handler 25 laser rendering (FUN_0041f004)
-// Speeder bike laser shots - draws beam from gun position to target
-void InsaneRebel2::renderHandler25LaserShots(byte *renderBitmap, int pitch, int width, int height) {
- // FUN_0041f004 uses turret-style shot slots with view offset adjustment
- // Only render when player is uncovered (damage == 0)
- if (_rebelDamageLevel != 0) {
- return; // Can't shoot while taking cover
- }
-
- int16 maxDuration = getShotMaxDuration();
-
- for (int i = 0; i < 2; i++) {
- if (_turretShots[i].counter <= 0)
- continue;
-
- // Calculate sound panning from target X position (FUN_004262f0)
- // sVar1 = ((2 - counter) * (targetX - 160)) / 2, clamped to [-127, 127]
- int16 pan = ((2 - _turretShots[i].counter) * (_turretShots[i].targetX - 160)) / 2;
- pan = CLIP<int16>(pan, -127, 127);
- // TODO: Apply panning to sound channel i+1
-
- // Target position (where player clicked)
- int16 targetX = _turretShots[i].targetX;
- int16 targetY = _turretShots[i].targetY;
-
- // Gun position computed at spawn time from GRD002 sprite data
- // Original: DAT_0045791c[i] + DAT_0045790c, DAT_00457920[i] + DAT_0045790e
- int16 gunX = _turretShots[i].gunX;
- int16 gunY = _turretShots[i].gunY;
-
- int16 progress = maxDuration - _turretShots[i].counter;
-
- // From FUN_0041f004 parameters for FUN_0040bbf6:
- // widthScale=0xC(12), heightScale=4, thickness=6
- drawLaserBeam(renderBitmap, pitch, width, height,
- gunX, gunY, targetX, targetY,
- progress, maxDuration, 12, 4, 6);
-
- _turretShots[i].counter--;
-
- debug("Rebel2 Handler25: Laser shot %d from (%d,%d) to (%d,%d) progress=%d/%d",
- i, gunX, gunY, targetX, targetY, progress, maxDuration);
- }
-}
-
-void InsaneRebel2::renderCrosshair(byte *renderBitmap, int pitch, int width, int height) {
- // From FUN_0040d836 (Handler 7) line 167-168: crosshair only drawn when DAT_004437c0 == 2
- // Don't draw crosshair when shooting is disabled (flight-only segments)
- if (!isShootingAllowed()) {
- return;
- }
-
- // Handler 25 (0x19): From FUN_41DB5E lines 195-197, crosshair only drawn when
- // DAT_0045790a == 0 (fully uncovered). Hide crosshair during cover transition.
- if (_rebelHandler == 25 && _rebelDamageLevel != 0) {
- return;
- }
-
- // Update target lock state and draw crosshair/reticle
-
- // Target lock detection (DAT_00443676 equivalent)
- Common::Point worldMousePos(_vm->_mouse.x + _viewX, _vm->_mouse.y + _viewY);
- bool targetLocked = false;
-
- for (Common::List<enemy>::iterator it = _enemies.begin(); it != _enemies.end(); ++it) {
- if (it->active && !it->destroyed && it->rect.contains(worldMousePos)) {
- targetLocked = true;
- break;
- }
- }
-
- if (targetLocked) {
- _targetLockTimer = 7;
- } else if (_targetLockTimer > 0) {
- _targetLockTimer--;
- }
-
- // Draw crosshair
- if (!_smush_iconsNut)
- return;
-
- int reticleIndex;
- switch (_rebelHandler) {
- case 7: // Third-Person Ship
- case 0x19: // FPS/Mixed (Handler 25)
- reticleIndex = 47; // 0x2F
- break;
- case 0x26: { // Turret/Cockpit - animated crosshair
- static int turretAnimCounter = 0;
- turretAnimCounter++;
-
- int animOffset = (_targetLockTimer == 0) ? 0 : 3 - (turretAnimCounter & 3);
-
- if (_rebelLevelType == 5) {
- reticleIndex = 0x30 + animOffset;
- } else {
- reticleIndex = animOffset;
- }
- break;
- }
- case 8: // Third-Person On Foot
- default:
- reticleIndex = 46;
- break;
- }
-
- if (_smush_iconsNut->getNumChars() > reticleIndex) {
- int cw = _smush_iconsNut->getCharWidth(reticleIndex);
- int ch = _smush_iconsNut->getCharHeight(reticleIndex);
-
- // Calculate crosshair position
- int crosshairX = _vm->_mouse.x - cw / 2 + _viewX;
- int crosshairY = _vm->_mouse.y - ch / 2 + _viewY;
-
- // Handler 25 (0x19): Add view offset to crosshair position
- // From FUN_41DB5E lines 198-199: X = DAT_00457914 + DAT_0045790c, Y = DAT_00457916 + DAT_0045790e
- if (_rebelHandler == 25) {
- crosshairX += _rebelViewOffsetX;
- crosshairY += _rebelViewOffsetY;
- }
-
- renderNutSprite(renderBitmap, pitch, width, height,
- crosshairX, crosshairY,
- _smush_iconsNut, reticleIndex);
- }
-}
-
-void InsaneRebel2::frameEndCleanup() {
- // Reset enemy active flags and collision zones at frame end
- // The original game rebuilds lists from scratch each frame
-
- for (Common::List<enemy>::iterator it = _enemies.begin(); it != _enemies.end(); ++it) {
- if (!it->destroyed) {
- it->active = false;
- }
- }
-
- resetCollisionZones();
-}
-
-// ========== Audio Handling for Rebel Assault 2 ==========
-// RA2 doesn't use iMUSE - we handle audio directly through the mixer
-
-void InsaneRebel2::initAudio(int sampleRate) {
- _audioSampleRate = sampleRate;
- for (int i = 0; i < kRA2MaxAudioTracks; i++) {
- _audioStreams[i] = nullptr;
- _audioTrackActive[i] = false;
- }
-}
-
-void InsaneRebel2::terminateAudio() {
- for (int i = 0; i < kRA2MaxAudioTracks; i++) {
- if (_audioTrackActive[i]) {
- _vm->_mixer->stopHandle(_audioHandles[i]);
- _audioTrackActive[i] = false;
- }
- if (_audioStreams[i]) {
- _audioStreams[i]->finish();
- _audioStreams[i] = nullptr;
- }
- }
-}
-
-void InsaneRebel2::queueAudioData(int trackIdx, uint8 *data, int32 size, int volume, int pan) {
- if (trackIdx < 0 || trackIdx >= kRA2MaxAudioTracks || size <= 0 || !data) {
- debug(5, "InsaneRebel2::queueAudioData: Invalid params trackIdx=%d size=%d data=%p", trackIdx, size, (void*)data);
- return;
- }
-
- debug(5, "InsaneRebel2::queueAudioData: trackIdx=%d size=%d volume=%d pan=%d", trackIdx, size, volume, pan);
-
- // Create audio stream if not already active
- if (!_audioStreams[trackIdx]) {
- // RA2 audio is 8-bit unsigned mono at the track's sample rate
- debug("InsaneRebel2: Creating audio stream for track %d at %d Hz", trackIdx, _audioSampleRate);
- _audioStreams[trackIdx] = Audio::makeQueuingAudioStream(_audioSampleRate, false);
- _audioTrackActive[trackIdx] = true;
- _vm->_mixer->playStream(Audio::Mixer::kSFXSoundType, &_audioHandles[trackIdx],
- _audioStreams[trackIdx], -1, Audio::Mixer::kMaxChannelVolume, 0,
- DisposeAfterUse::NO);
- }
-
- debug(6, "InsaneRebel2: Queueing %d bytes to track %d (vol=%d)", size, trackIdx, volume);
-
- // Copy the audio data since queueBuffer may need to own it
- byte *audioCopy = (byte *)malloc(size);
- if (!audioCopy) {
- return;
- }
- memcpy(audioCopy, data, size);
-
- // Queue the audio data - RA2 SMUSH audio is 8-bit unsigned mono
- _audioStreams[trackIdx]->queueBuffer(audioCopy, size, DisposeAfterUse::YES, Audio::FLAG_UNSIGNED);
-
- // Apply volume and pan to the channel
- int scaledVolume = (volume * Audio::Mixer::kMaxChannelVolume) / 127;
- int scaledPan = (pan * 127) / 128; // Convert -128..127 to -127..127
- _vm->_mixer->setChannelVolume(_audioHandles[trackIdx], scaledVolume);
- _vm->_mixer->setChannelBalance(_audioHandles[trackIdx], scaledPan);
-}
-
-void InsaneRebel2::processAudioFrame(int16 feedSize) {
- if (!_player) {
- return;
- }
-
- // Initialize dispatch data if needed (normally done in processDispatches for iMUSE games)
- if (_player->_smushTracksNeedInit) {
- _player->_smushTracksNeedInit = false;
- for (int i = 0; i < SMUSH_MAX_TRACKS; i++) {
- _player->_smushDispatch[i].fadeRemaining = 0;
- _player->_smushDispatch[i].fadeVolume = 0;
- _player->_smushDispatch[i].fadeSampleRate = 0;
- _player->_smushDispatch[i].elapsedAudio = 0;
- _player->_smushDispatch[i].audioLength = 0;
- }
- }
-
- // Access SmushPlayer's audio track data (InsaneRebel2 is a friend class)
- // Only iterate over actually allocated tracks (not SMUSH_MAX_TRACKS)
- for (int i = 0; i < _player->_smushNumTracks; i++) {
- SmushPlayer::SmushAudioTrack &track = _player->_smushTracks[i];
- SmushPlayer::SmushAudioDispatch &dispatch = _player->_smushDispatch[i];
-
- if (track.state == TRK_STATE_INACTIVE) {
- continue;
- }
-
- // Skip tracks that don't have valid buffer pointers yet
- // Note: dispatch.dataBuf is set when transitioning from FADING to PLAYING,
- // so tracks in FADING state won't have it set yet - that's OK, they'll be
- // transitioned below and then processed
- if (!track.blockPtr) {
- debug(5, "InsaneRebel2: Skipping track %d - blockPtr=%p state=%d",
- i, (void*)track.blockPtr, track.state);
- continue;
- }
-
- // Check if this track type should be played
- bool isPlayableTrack = ((track.flags & TRK_TYPE_MASK) == IS_SPEECH && _player->isChanActive(CHN_SPEECH)) ||
- ((track.flags & TRK_TYPE_MASK) == IS_BKG_MUSIC && _player->isChanActive(CHN_BKGMUS)) ||
- ((track.flags & TRK_TYPE_MASK) == IS_SFX && _player->isChanActive(CHN_OTHER));
-
- if (!isPlayableTrack) {
- continue;
- }
-
- // Calculate base volume for this track type
- int baseVolume;
- switch (track.flags & TRK_TYPE_MASK) {
- case IS_SFX:
- baseVolume = (_player->_smushTrackVols[1] * track.volume) >> 7;
- break;
- case IS_BKG_MUSIC:
- baseVolume = (_player->_smushTrackVols[3] * track.volume) >> 7;
- break;
- case IS_SPEECH:
- baseVolume = (_player->_smushTrackVols[2] * track.volume) >> 7;
- break;
- default:
- baseVolume = track.volume;
- break;
- }
- int mixVolume = baseVolume * _player->_smushTrackVols[0] / 127;
-
- // Handle track state transitions: FADING -> PLAYING
- if (track.state == TRK_STATE_FADING) {
- dispatch.headerPtr = track.dataBuf;
- dispatch.dataBuf = track.subChunkPtr;
- dispatch.dataSize = track.dataSize;
- dispatch.currentOffset = 0;
- dispatch.audioLength = 0;
- track.state = TRK_STATE_PLAYING;
- }
-
- // Process audio for this track
- if (track.state != TRK_STATE_INACTIVE) {
- int32 tmpFeedSize = feedSize;
-
- while (tmpFeedSize > 0) {
- int32 mixInFrameCount = dispatch.currentOffset;
-
- // Use dispatch.dataBuf and dispatch.dataSize which are set consistently
- // when the track transitions from FADING to PLAYING, and audioRemaining
- // is calculated relative to these values by processAudioCodes
- if (mixInFrameCount > 0 && dispatch.dataBuf && dispatch.dataSize > 0) {
- // Ensure audioRemaining is non-negative for proper circular buffer access
- if (dispatch.audioRemaining < 0) {
- debug(5, "InsaneRebel2: Resetting negative audioRemaining=%d for track %d", dispatch.audioRemaining, i);
- dispatch.audioRemaining = 0;
- }
- int32 offset = dispatch.audioRemaining % dispatch.dataSize;
-
- // Limit to feed size proportional to sample rate
- if (dispatch.sampleRate > 0 && _player->_smushAudioSampleRate > 0) {
- int32 maxFrames = dispatch.sampleRate * tmpFeedSize / _player->_smushAudioSampleRate;
- if (mixInFrameCount > maxFrames) {
- mixInFrameCount = maxFrames;
- }
- }
-
- // Don't read past the buffer
- if (offset + mixInFrameCount > dispatch.dataSize) {
- mixInFrameCount = dispatch.dataSize - offset;
- }
-
- // Make sure we don't exceed available data
- if (dispatch.audioRemaining + mixInFrameCount > track.availableSize) {
- mixInFrameCount = track.availableSize - dispatch.audioRemaining;
- if (mixInFrameCount <= 0) {
- // Track is ending - no more data
- track.state = TRK_STATE_ENDING;
- break;
- }
- }
-
- if (mixInFrameCount > 0) {
- // Safety check: verify the pointer and offset are within bounds
- if (!dispatch.dataBuf || offset < 0 || offset + mixInFrameCount > dispatch.dataSize) {
- debug(1, "InsaneRebel2: Invalid audio buffer access track=%d dataBuf=%p offset=%d mixInFrameCount=%d dataSize=%d",
- i, (void*)dispatch.dataBuf, offset, mixInFrameCount, dispatch.dataSize);
- break;
- }
-
- // Queue audio data directly to our audio streams
- queueAudioData(i, &dispatch.dataBuf[offset], mixInFrameCount, mixVolume, track.pan);
-
- // Update dispatch state
- dispatch.currentOffset -= mixInFrameCount;
- dispatch.audioRemaining += mixInFrameCount;
-
- // Calculate how much feed time was consumed
- if (dispatch.sampleRate > 0) {
- int32 consumedFeed = mixInFrameCount * _player->_smushAudioSampleRate / dispatch.sampleRate;
- tmpFeedSize -= consumedFeed;
- } else {
- tmpFeedSize -= mixInFrameCount;
- }
- }
- }
-
- // If currentOffset is depleted, process audio codes to get more
- if (dispatch.currentOffset <= 0) {
- // processAudioCodes returns true if there's more audio, false if done
- if (!_player->processAudioCodes(i, tmpFeedSize, mixVolume)) {
- break;
- }
- // If still no offset after processing codes, we're done
- if (dispatch.currentOffset <= 0) {
- break;
- }
- } else if (tmpFeedSize <= 0) {
- break;
- }
- }
- }
-
- track.audioRemaining = dispatch.audioRemaining;
- dispatch.state = track.state;
- }
-}
-
-// ========== Sound Effects (SAD files) ==========
-// Loads standalone SAUD files from SYSTM/ for one-shot SFX playback.
-// Original game loads these via FUN_0042a3b0 at init into DAT_00456888[0..7].
-
-static const char *const kRA2SfxFiles[InsaneRebel2::kRA2NumSfx] = {
- "SYSTM/BLAST.SAD", // 0 - Player laser fire
- "SYSTM/CRASH.SAD", // 1 - Corridor/wall collision
- "SYSTM/EXPLODE.SAD", // 2 - Enemy explosion
- "SYSTM/ALERT.SAD", // 3 - Alert/warning
- "SYSTM/LOCKON.SAD", // 4 - Target lock-on
- "SYSTM/BONUS.SAD", // 5 - Bonus pickup
- "SYSTM/HBLAST.SAD", // 6 - Heavy blast (player weapon)
- "SYSTM/TBLAST.SAD" // 7 - TIE blast
-};
-
-void InsaneRebel2::loadSfx() {
- for (int i = 0; i < kRA2NumSfx; i++) {
- ScummFile *file = _vm->instantiateScummFile();
- _vm->openFile(*file, kRA2SfxFiles[i]);
- if (!file->isOpen()) {
- debug("InsaneRebel2::loadSfx: Could not open %s", kRA2SfxFiles[i]);
- delete file;
- continue;
- }
-
- // SAUD file structure: SAUD header (8) + STRK sub-chunk + SDAT sub-chunk
- // We scan for the SDAT tag to find the PCM data.
- uint32 fileSize = file->size();
- if (fileSize < 38) { // Minimum: 8 (SAUD) + 22 (STRK) + 8 (SDAT header)
- debug("InsaneRebel2::loadSfx: %s too small (%d bytes)", kRA2SfxFiles[i], fileSize);
- file->close();
- delete file;
- continue;
- }
-
- // Verify SAUD tag
- uint32 tag = file->readUint32BE();
- if (tag != MKTAG('S', 'A', 'U', 'D')) {
- debug("InsaneRebel2::loadSfx: %s not a SAUD file (tag=0x%08x)", kRA2SfxFiles[i], tag);
- file->close();
- delete file;
- continue;
- }
- file->readUint32BE(); // Skip SAUD size
-
- // Scan for SDAT chunk (skip STRK and any other sub-chunks)
- bool foundSdat = false;
- while (file->pos() + 8 <= (int64)fileSize) {
- uint32 chunkTag = file->readUint32BE();
- uint32 chunkSize = file->readUint32BE();
-
- if (chunkTag == MKTAG('S', 'D', 'A', 'T')) {
- // Found PCM data
- uint32 pcmSize = MIN(chunkSize, fileSize - (uint32)file->pos());
- _sfxData[i] = (byte *)malloc(pcmSize);
- if (_sfxData[i]) {
- file->read(_sfxData[i], pcmSize);
- _sfxSize[i] = pcmSize;
- debug("InsaneRebel2::loadSfx: Loaded %s (%d bytes PCM)", kRA2SfxFiles[i], pcmSize);
- }
- foundSdat = true;
- break;
- } else {
- // Skip this sub-chunk
- file->seek(chunkSize, SEEK_CUR);
- }
- }
-
- if (!foundSdat) {
- debug("InsaneRebel2::loadSfx: No SDAT chunk in %s", kRA2SfxFiles[i]);
- }
-
- file->close();
- delete file;
- }
-}
-
-void InsaneRebel2::freeSfx() {
- for (int i = 0; i < kRA2NumSfx; i++) {
- // Stop any playing SFX on this slot
- _vm->_mixer->stopHandle(_sfxHandles[i]);
- free(_sfxData[i]);
- _sfxData[i] = nullptr;
- _sfxSize[i] = 0;
- }
-}
-
-void InsaneRebel2::playSfx(int slot, int volume, int pan) {
- if (slot < 0 || slot >= kRA2NumSfx || !_sfxData[slot] || _sfxSize[slot] == 0) {
- return;
- }
-
- // Stop any previous instance of this SFX slot
- _vm->_mixer->stopHandle(_sfxHandles[slot]);
-
- // Make a copy of the PCM data (makeRawStream with DisposeAfterUse::YES will free it)
- byte *pcmCopy = (byte *)malloc(_sfxSize[slot]);
- if (!pcmCopy) {
- return;
- }
- memcpy(pcmCopy, _sfxData[slot], _sfxSize[slot]);
-
- // Create a one-shot raw audio stream: 8-bit unsigned mono at 11025 Hz
- Audio::SeekableAudioStream *stream = Audio::makeRawStream(
- pcmCopy, _sfxSize[slot], 11025, Audio::FLAG_UNSIGNED, DisposeAfterUse::YES);
-
- // Scale volume from 0-127 to ScummVM's 0-255 range
- int scaledVolume = (volume * Audio::Mixer::kMaxChannelVolume) / 127;
-
- _vm->_mixer->playStream(Audio::Mixer::kSFXSoundType, &_sfxHandles[slot],
- stream, -1, scaledVolume, pan);
-
- debug(5, "InsaneRebel2::playSfx: slot=%d vol=%d pan=%d size=%d", slot, volume, pan, _sfxSize[slot]);
-}
-
-// ======================= Menu System Implementation =======================
-// Emulates retail menu system from FUN_004147B2 and FUN_0041FDC8
-
-void InsaneRebel2::resetMenu() {
- _menuSelection = 0;
- _menuInactivityTimer = 0;
- _menuRepeatDelay = 0;
-}
-
-// Unlock all chapters for testing
-// Emulates the debug mode from original FUN_00415CF8 (lines 60-71)
-// where DAT_0047ab34 == 'd' enables level unlock via special codes
-void InsaneRebel2::unlockAllChapters() {
- debug("Rebel2: Unlocking all chapters for testing");
- _debugUnlockAll = true;
- for (int i = 0; i < 16; i++) {
- _chapterUnlocked[i] = true;
- _levelUnlocked[i] = true;
- }
-}
-
-Common::String InsaneRebel2::getRandomMenuVideo() {
- // Emulates FUN_0041FDC8 - selects random menu video variant
- //
- // NOTE: The original game plays O_MENU.SAN when no progress flags are set,
- // but that file contains ONLY audio (no FOBJ video frames). The O_MENU_X.SAN
- // variants (A through O) contain actual 320x200 background images in Frame 0.
- //
- // We ALWAYS use a random variant to ensure a proper background is displayed.
- // The original behavior of showing O_MENU.SAN (audio-only) would result in
- // a black/undefined background which doesn't match the intended experience.
-
- // Select random variant (0-14 maps to A-O), ensuring different from last
- int variant;
- do {
- variant = _vm->_rnd.getRandomNumber(14); // 0-14
- } while (variant == _lastMenuVariant && _lastMenuVariant >= 0);
- _lastMenuVariant = variant;
-
- // Map 0-14 to A-O (case 0/default = A, 1 = B, etc.)
- char letter = 'A' + variant;
- debug("Rebel2: Selected menu variant %c", letter);
- return Common::String::format("OPEN/O_MENU_%c.SAN", letter);
-}
-
-int InsaneRebel2::processMenuInput() {
- // Emulates FUN_0041f5ae menu input handling
- // Returns: -1 = no action, 0-4 = menu item selected
- //
- // Events are captured by notifyEvent() (EventObserver) which runs before
- // ScummEngine::parseEvents() consumes them. This ensures we don't miss
- // any input events even though we only process them on video frames.
- //
- // From FUN_0041f5ae disassembly:
- // - Keyboard: Up/Down arrows navigate, Enter confirms
- // - Mouse mode (DAT_0047a806 == 1): Y position maps to selection
- // - Key codes: Up=0x148, Down=0x150, Enter=0x0d, ESC=0x1b
-
- int result = -1;
-
- // Menu item Y positions (low-res 320x200 mode):
- // From FUN_0041f5ae: baseY = numItems * -5 + 0x68
- // With 8 total items (title + 7 options): 8 * -5 + 104 = 64
- // Items at Y = 64, 74, 84, 94, 104, 114, 124 with spacing of 10
- const int numItemsTotal = 8; // Title + 7 selectable items (matching assembly)
- const int baseY = numItemsTotal * -5 + 0x68; // = 64
- const int itemSpacing = 10;
-
- // Process events from the queue (populated by notifyEvent)
- while (!_menuEventQueue.empty()) {
- Common::Event event = _menuEventQueue.pop();
- switch (event.type) {
- case Common::EVENT_KEYDOWN:
- _menuInactivityTimer = 0; // Reset inactivity timer on any input
-
- switch (event.kbd.keycode) {
- case Common::KEYCODE_UP:
- // Navigate up (wrap around) - emulates key code 0x148
- _menuSelection--;
- if (_menuSelection < 0) {
- _menuSelection = _menuItemCount - 1;
- }
- // Reset repeat delay counter (DAT_00459ce0)
- _menuRepeatDelay = 3;
- debug("Menu: Selection changed to %d (UP)", _menuSelection);
- break;
-
- case Common::KEYCODE_DOWN:
- // Navigate down (wrap around) - emulates key code 0x150
- _menuSelection++;
- if (_menuSelection >= _menuItemCount) {
- _menuSelection = 0;
- }
- _menuRepeatDelay = 3;
- debug("Menu: Selection changed to %d (DOWN)", _menuSelection);
- break;
-
- case Common::KEYCODE_RETURN:
- case Common::KEYCODE_KP_ENTER:
- // Confirm selection - emulates key code 0x0d
- if (_menuSelection >= 0 && _menuSelection < _menuItemCount) {
- result = _menuSelection;
- debug("Menu: Item %d selected (ENTER)", _menuSelection);
- }
- break;
-
- case Common::KEYCODE_ESCAPE:
- // ESC - Quit (index 4 = last item) - emulates key code 0x1b
- result = _menuItemCount - 1; // Select quit option
- debug("Menu: ESC pressed - selecting quit (item %d)", result);
- break;
-
- default:
- break;
- }
- break;
-
- case Common::EVENT_LBUTTONDOWN:
- _menuInactivityTimer = 0;
- {
- // Get mouse position from the event
- int mouseY = event.mouse.y;
-
- debug("Menu: Left click at Y=%d", mouseY);
-
- // Check which item was clicked
- // From FUN_0041f5ae mouse mode: selection = (mouseY + 100 - baseY) / 10
- // But we use a simpler direct hit-test approach
- for (int i = 0; i < _menuItemCount; i++) {
- int itemY = baseY + i * itemSpacing;
- // Hit area: itemY - 2 to itemY + 8 (10 pixel height)
- if (mouseY >= itemY - 2 && mouseY < itemY + 8) {
- _menuSelection = i;
- result = i;
- debug("Menu: Item %d clicked (itemY=%d)", i, itemY);
- break;
- }
- }
- }
- break;
-
- case Common::EVENT_MOUSEMOVE:
- // Update hover selection based on Y position
- // This emulates FUN_0041f5ae mouse mode behavior (DAT_0047a806 == 1)
- {
- int mouseY = event.mouse.y;
- // Calculate selection from mouse Y position
- // From assembly: DAT_00459988 = ((mouseY + 100) - (param_3 * -5 + 0x67)) / 10
- int newSelection = (mouseY + 100 - (numItemsTotal * -5 + 0x67)) / 10;
-
- // Clamp to valid range
- if (newSelection < 0) newSelection = 0;
- if (newSelection >= _menuItemCount) newSelection = _menuItemCount - 1;
-
- // Only update if within menu area (not too far above/below)
- int topY = baseY - 5;
- int bottomY = baseY + (_menuItemCount - 1) * itemSpacing + 10;
- if (mouseY >= topY && mouseY <= bottomY) {
- if (newSelection != _menuSelection) {
- _menuSelection = newSelection;
- debug(5, "Menu: Hover selection changed to %d (mouseY=%d)", _menuSelection, mouseY);
- }
- }
- }
- _vm->_mouse.x = event.mouse.x;
- _vm->_mouse.y = event.mouse.y;
- break;
-
- case Common::EVENT_QUIT:
- case Common::EVENT_RETURN_TO_LAUNCHER:
- // Handle quit request - select quit option
- result = _menuItemCount - 1;
- break;
-
- default:
- break;
- }
- }
-
- // Decrement repeat delay counter (for smooth keyboard navigation)
- if (_menuRepeatDelay > 0) {
- _menuRepeatDelay--;
- }
-
- return result;
-}
-
-void InsaneRebel2::drawMenuItems(byte *renderBitmap, int pitch, int width, int height,
- const char **items, int numItems, int selection,
- bool leftAligned) {
- // =====================================================================
- // Shared menu renderer - Emulates FUN_0041f5ae
- // Address: 0x41F5AE
- // =====================================================================
- //
- // items[0] = title string, items[1..numItems] = selectable items
- // numItems = number of selectable items (FUN_0041f5ae param_3)
- // selection = currently highlighted item (0-based, maps to DAT_00459988)
- // leftAligned = false: param_4==0 (centered), true: param_4==1 (left-aligned)
- //
- // Coordinate formulas from FUN_0041f5ae (low-res, DAT_0047a808 < 2):
- // Centered (param_4=0):
- // Title X: center - titleWidth/2 (centerX = 160)
- // Title Y: param_3 * -5 + 0x51
- // Item X: center - textWidth/2
- // Box X: center - bracketWidth/2
- // Left-aligned (param_4=1):
- // Title X: 0x28 = 40
- // Title Y: param_3 * -5 + 0x56
- // Item X: 0x17 = 23
- // Box X: 0x14 = 20
- // Both modes:
- // Item base Y: param_3 * -5 + 0x68
- // Item Y: param_3 * -5 + i * 10 + 0x68
- // Box Y: param_3 * -5 + i * 10 + 0x67 (1px above text)
-
- const int centerX = width / 2;
- const int titleY = numItems * -5 + (leftAligned ? 0x56 : 0x51);
- const int itemBaseY = numItems * -5 + 0x68;
- const int itemSpacing = 10;
-
- // =====================================================================
- // Font system - Emulates linked list from FUN_00403bd0
- // =====================================================================
- // Font 0 (^f00): TALKFONT.NUT
- // Font 1 (^f01): SMALFONT.NUT (menu items)
- // Font 2 (^f02): TITLFONT.NUT (title)
- NutRenderer *fonts[3] = {
- _smush_talkfontNut,
- _smush_smalfontNut,
- _smush_titlefontNut
- };
-
- NutRenderer *defaultFont = fonts[0] ? fonts[0] : _smush_smalfontNut;
- if (!defaultFont) {
- debug(1, "drawMenuItems: no fonts available!");
- return;
- }
-
- Common::Rect clipRect(0, 0, _vm->_screenWidth, _vm->_screenHeight);
- int actualPitch = _vm->_screenWidth;
-
- // =====================================================================
- // Format code parser - Emulates FUN_00434d10 / FUN_00433da0
- // =====================================================================
- // ^^ = literal ^, ^fNN = font switch, ^cNNN = color code, ^l = newline
- auto parseFormatCode = [&](const char *&str, int &outColor) -> int {
- if (*str != '^') return -1;
-
- const char *p = str + 1;
- if (*p == '^') {
- str = p;
- return -1;
- }
- if (*p == 'f') {
- p++;
- int fontIdx = 0;
- while (*p >= '0' && *p <= '9') {
- fontIdx = fontIdx * 10 + (*p - '0');
- p++;
- }
- str = p;
- return (fontIdx >= 0 && fontIdx < 3) ? fontIdx : 0;
- }
- if (*p == 'c') {
- p++;
- int color = 0;
- while (*p >= '0' && *p <= '9') {
- color = color * 10 + (*p - '0');
- p++;
- }
- str = p;
- outColor = color;
- return -2;
- }
- if (*p == 'l') {
- str = p + 1;
- return -2;
- }
- return -1;
- };
-
- // String width calculation - Emulates FUN_00433da0
- auto getStringWidth = [&](const char *str) -> int {
- int w = 0;
- NutRenderer *curFont = defaultFont;
- int curColor = -1;
-
- while (*str) {
- int fontChange = parseFormatCode(str, curColor);
- if (fontChange >= 0) {
- curFont = fonts[fontChange] ? fonts[fontChange] : defaultFont;
- continue;
- }
- if (fontChange == -2) continue;
-
- byte c = (byte)*str++;
- if (c >= 'a' && c <= 'z') c = c - 'a' + 'A';
- if (curFont && c < curFont->getNumChars()) {
- w += curFont->getCharWidth(c);
- }
- }
- return w;
- };
-
- // String rendering - Emulates FUN_00434d10
- // Codec 44 color substitution: font pixels with value 1 â ^cNNN color
- auto drawString = [&](const char *str, int x, int y) {
- NutRenderer *curFont = defaultFont;
- int curColor = 1;
-
- while (*str) {
- int fontChange = parseFormatCode(str, curColor);
- if (fontChange >= 0) {
- curFont = fonts[fontChange] ? fonts[fontChange] : defaultFont;
- continue;
- }
- if (fontChange == -2) continue;
-
- byte c = (byte)*str++;
- if (c >= 'a' && c <= 'z') c = c - 'a' + 'A';
-
- if (!curFont) continue;
- int numChars = curFont->getNumChars();
- if (c >= numChars) continue;
-
- int charW = curFont->getCharWidth(c);
-
- if (x >= 0 && y >= 0 && charW > 0) {
- curFont->drawCharV7(renderBitmap, clipRect, x, y, actualPitch, curColor,
- kStyleAlignLeft, c, false, false);
- }
- x += charW;
- }
- };
-
- // =====================================================================
- // Draw title - items[0]
- // Centered: X = center - titleWidth/2
- // Left-aligned: X = 40 (0x28)
- // =====================================================================
- {
- int titleWidth = getStringWidth(items[0]);
- int titleX = leftAligned ? 40 : (centerX - titleWidth / 2);
- drawString(items[0], titleX, titleY);
- }
-
- // =====================================================================
- // Draw selectable items with selection highlight box
- // Centered: item X = center - textWidth/2, box X = center - bracketWidth/2
- // Left-aligned: item X = 23 (0x17), box X = 20 (0x14)
- // =====================================================================
- for (int i = 0; i < numItems; i++) {
- int itemY = itemBaseY + i * itemSpacing;
- const char *text = items[i + 1];
-
- int textWidth = getStringWidth(text);
- int textX = leftAligned ? 23 : (centerX - textWidth / 2);
- drawString(text, textX, itemY);
-
- // Selection highlight box - FUN_004292d0
- if (i == selection) {
- // Width: textWidth + ((DAT_0047a808 < 2) - 1 & 6) + 6 = textWidth + 6
- int bracketWidth = textWidth + 6;
- // Height: ((DAT_0047a808 < 2) - 1 & 10) + 10 = 10
- int bracketHeight = 10;
-
- // Flash color: (-((DAT_0047a7e4 & 1) == 0) & 8U) - 0x10
- // bit0==0: 8-16=248(0xF8), bit0==1: 0-16=240(0xF0)
- static int frameCounter = 0;
- frameCounter++;
- byte highlightColor = ((frameCounter / 8) & 1) ? 248 : 240;
-
- // Box position: Y = itemY - 1 (0x67 vs 0x68)
- int leftX = leftAligned ? 20 : (centerX - bracketWidth / 2);
- int rightX = leftX + bracketWidth;
- int topY = itemY - 1;
- int bottomY = itemY + bracketHeight - 1;
-
- int screenW = _vm->_screenWidth;
- int screenH = _vm->_screenHeight;
- if (leftX < 0) leftX = 0;
- if (rightX >= screenW) rightX = screenW - 1;
- if (topY < 0) topY = 0;
- if (bottomY >= screenH) bottomY = screenH - 1;
-
- // FUN_004292d0 - Draw rectangle border (4 lines)
- for (int x = leftX; x <= rightX && x < screenW; x++) {
- if (topY >= 0 && topY < screenH)
- renderBitmap[topY * actualPitch + x] = highlightColor;
- if (bottomY >= 0 && bottomY < screenH)
- renderBitmap[bottomY * actualPitch + x] = highlightColor;
- }
- for (int py = topY; py <= bottomY && py < screenH; py++) {
- if (leftX >= 0 && leftX < screenW)
- renderBitmap[py * actualPitch + leftX] = highlightColor;
- if (rightX >= 0 && rightX < screenW)
- renderBitmap[py * actualPitch + rightX] = highlightColor;
- }
- }
- }
-}
-
-void InsaneRebel2::drawMenuOverlay(byte *renderBitmap, int pitch, int width, int height) {
- // =====================================================================
- // Main menu renderer - calls shared drawMenuItems()
- // Emulates FUN_004147b2 -> FUN_0041f5ae with param_3=7, param_4=0
- // =====================================================================
- //
- // Menu strings loaded from GAME.TRS (keyboard mode indices 10-17):
- // TRS index 10: "^f02Game Main Menu" -> Title (uses TITLFONT)
- // TRS index 11: "^f01^c005Start Game" -> Item 0 (uses SMALFONT, color 5)
- // TRS index 12: "^f01^c009Options" -> Item 1
- // TRS index 13: "^f01^c009Calibrate Joystick" -> Item 2
- // TRS index 14: "^f01^c009Continue Intro" -> Item 3
- // TRS index 15: "^f01^c009Show Top Pilots" -> Item 4
- // TRS index 16: "^f01^c009Show Credits" -> Item 5
- // TRS index 17: "^f01^c240Return to Launcher" -> Item 6 (color 240)
-
- SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
- if (!splayer) {
- debug(1, "drawMenuOverlay: SmushPlayer not available for TRS strings!");
- return;
- }
-
- // Load TRS strings 10-17 (title + 7 selectable items)
- const char *menuItems[8];
- for (int i = 0; i < 8; i++) {
- menuItems[i] = splayer->getString(10 + i);
- if (!menuItems[i] || !menuItems[i][0]) {
- debug(1, "drawMenuOverlay: TRS string %d not found!", 10 + i);
- menuItems[i] = "";
- }
- }
-
- // FUN_004147b2 line 25: param_3 = (DAT_0047a806 == 0) + 6 = 7 (keyboard mode)
- drawMenuItems(renderBitmap, pitch, width, height, menuItems, 7, _menuSelection);
-}
-
-// ======================= Pause Overlay =======================
-// Emulates FUN_405A21 pause rendering (lines 242-305)
-// Creates a dimmed overlay effect and displays "PAUSED" text
-void InsaneRebel2::showPauseOverlay() {
- SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
- if (!splayer) {
- debug("showPauseOverlay: No SmushPlayer active");
- return;
- }
-
- // Get frame buffer and palette from SmushPlayer
- // _dst points to the virtual screen pixels (the actual rendering destination)
- // _frameBuffer is only used for store/fetch operations, not general rendering
- byte *frameBuffer = splayer->_dst;
- byte *palette = splayer->_pal;
- int width = splayer->_width;
- int height = splayer->_height;
-
- if (!frameBuffer || !palette || width <= 0 || height <= 0) {
- debug("showPauseOverlay: No frame buffer (%p), palette (%p), or invalid dimensions (%dx%d)",
- (void*)frameBuffer, (void*)palette, width, height);
- return;
- }
-
- debug("showPauseOverlay: Applying dimming effect to %dx%d buffer", width, height);
-
- // Apply dimming effect (emulates FUN_405A21 lines 242-251)
- // Original algorithm:
- // For each pixel, take the green component of its palette entry
- // and the green component of the previous pixel's palette entry,
- // add them, divide by 8, add 16.
- // This creates a dark dimmed effect.
- int bufferSize = width * height;
- byte prevPixel = 0;
-
- for (int i = 0; i < bufferSize; i++) {
- byte curPixel = frameBuffer[i];
-
- // Get green components from palette (offset +1 in RGB triplets)
- int greenCur = palette[curPixel * 3 + 1];
- int greenPrev = palette[prevPixel * 3 + 1];
-
- // Apply dimming formula: (green1 + green2) >> 3 + 0x10
- byte dimmedValue = ((greenCur + greenPrev) >> 3) + 0x10;
-
- frameBuffer[i] = dimmedValue;
- prevPixel = curPixel;
- }
-
- // Draw border decorations (simplified version of FUN_405A21 lines 261-283)
- // Draw horizontal lines at top and bottom of a centered box
- int boxLeft = 12;
- int boxRight = width - 12;
- int boxTop = 23; // 0x17
- int boxBottom = height - 23; // ~175 for 200 height
-
- byte borderColor = 0x50; // Gray border color
-
- // Top and bottom borders
- for (int x = boxLeft; x < boxRight; x++) {
- if (boxTop >= 0 && boxTop < height)
- frameBuffer[boxTop * width + x] = borderColor;
- if (boxBottom >= 0 && boxBottom < height)
- frameBuffer[boxBottom * width + x] = borderColor;
- }
-
- // Left and right borders
- for (int y = boxTop; y < boxBottom; y++) {
- if (boxLeft >= 0 && boxLeft < width)
- frameBuffer[y * width + boxLeft] = borderColor;
- if (boxRight >= 0 && boxRight < width)
- frameBuffer[y * width + boxRight] = borderColor;
- }
-
- // Draw corner decorations (simplified)
- byte cornerColor = 0x51; // Slightly brighter for corners
- for (int i = 0; i < 5; i++) {
- // Top-left corner
- if (boxTop + i < height && boxLeft + 5 < width)
- frameBuffer[(boxTop + i) * width + boxLeft + 5] = cornerColor;
- if (boxTop + 5 < height && boxLeft + i < width)
- frameBuffer[(boxTop + 5) * width + boxLeft + i] = cornerColor;
-
- // Top-right corner
- if (boxTop + i < height && boxRight - 5 >= 0)
- frameBuffer[(boxTop + i) * width + boxRight - 5] = cornerColor;
- if (boxTop + 5 < height && boxRight - i >= 0)
- frameBuffer[(boxTop + 5) * width + boxRight - i] = cornerColor;
-
- // Bottom-left corner
- if (boxBottom - i >= 0 && boxLeft + 5 < width)
- frameBuffer[(boxBottom - i) * width + boxLeft + 5] = cornerColor;
- if (boxBottom - 5 >= 0 && boxLeft + i < width)
- frameBuffer[(boxBottom - 5) * width + boxLeft + i] = cornerColor;
-
- // Bottom-right corner
- if (boxBottom - i >= 0 && boxRight - 5 >= 0)
- frameBuffer[(boxBottom - i) * width + boxRight - 5] = cornerColor;
- if (boxBottom - 5 >= 0 && boxRight - i >= 0)
- frameBuffer[(boxBottom - 5) * width + boxRight - i] = cornerColor;
- }
-
- // Draw "PAUSED" text centered
- // Try to load from TRS - the exact index may vary by language version
- // TRS index 80 (0x50) is likely "PAUSED" or equivalent (from DAT_004573f8)
- // Note: splayer is already defined at the start of this function
- const char *pauseText = splayer ? splayer->getString(80) : nullptr;
- if (!pauseText || !pauseText[0]) {
- // Fallback only if TRS string not available
- pauseText = "PAUSED";
- }
-
- // Draw text using SmushFont if available
- if (_menuFont) {
- Common::Rect clipRect(0, 0, width, height);
-
- // Calculate centered position
- // Text should be centered horizontally and vertically in the box
- int textX = width / 2; // SmushFont handles centering with kStyleAlignCenter
- int textY = height / 2 - 4; // Slightly above center
-
- // Draw with color 4 and background 0x10 (matching original parameters)
- // FUN_00434cb0 params: x=10, y=10 or 20, color=4, bg=0x10
- _menuFont->drawString(pauseText, frameBuffer, clipRect, textX, textY, 0x10, kStyleAlignCenter);
- } else if (_smush_smalfontNut) {
- // Fallback: draw using NutRenderer directly
- NutRenderer *font = _smush_smalfontNut;
- int numFontChars = font->getNumChars();
- Common::Rect clipRect(0, 0, width, height);
-
- // Calculate text width
- int textWidth = 0;
- const char *p = pauseText;
- while (*p) {
- byte c = (byte)*p++;
- if (c >= 'a' && c <= 'z') c = c - 'a' + 'A';
- if (c < numFontChars) {
- textWidth += font->getCharWidth(c);
- }
- }
-
- // Draw centered
- int textX = (width - textWidth) / 2;
- int textY = height / 2 - 4;
-
- p = pauseText;
- while (*p) {
- byte c = (byte)*p++;
- if (c >= 'a' && c <= 'z') c = c - 'a' + 'A';
- if (c < numFontChars && textX >= 0 && textY >= 0) {
- font->drawCharV7(frameBuffer, clipRect, textX, textY, width, -1,
- kStyleAlignLeft, c, true, true);
- textX += font->getCharWidth(c);
- }
- }
- }
-
- // Update the screen to show the pause overlay
- // SmushPlayer uses copyRectToScreen to transfer the buffer to the display backend
- _vm->_system->copyRectToScreen(frameBuffer, width, 0, 0, width, height);
- _vm->_system->updateScreen();
-
- debug("showPauseOverlay: Overlay displayed");
-}
-
-int InsaneRebel2::runMainMenu() {
- // Main menu loop - emulates FUN_004147B2
- // Returns:
- // kMenuNewGame (2) = Start new game
- // kMenuContinue (4) = Continue game (level select)
- // kMenuCredits (1) = Show credits then return to menu
- // 0 = Quit game
-
- debug("Rebel2: Entering main menu");
-
- resetMenu();
- _gameState = kStateMainMenu;
-
- // Enable menu input capture via EventObserver
- _menuInputActive = true;
- while (!_menuEventQueue.empty()) _menuEventQueue.pop(); // Clear any stale events
-
- // Get the SmushPlayer from ScummEngine_v7
- // Note: _player isn't set until SmushPlayer::initAudio() is called during playback
- SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
-
- // Main menu loop
- while (!_vm->shouldQuit()) {
- // Reset video finish flag before playing menu
- _vm->_smushVideoShouldFinish = false;
-
- // Select and play a random menu video
- Common::String menuVideo = getRandomMenuVideo();
- debug("Rebel2: Playing menu video: %s", menuVideo.c_str());
-
- // Set video flags for menu (0x20 = intro/menu flag)
- // This tells procPostRendering we're in menu mode
- splayer->setCurVideoFlags(0x20);
-
- // Play the menu video
- // Input is processed in procPostRendering during playback
- // When user confirms selection, _vm->_smushVideoShouldFinish is set
- splayer->play(menuVideo.c_str(), 12);
-
- // Check for quit
- if (_vm->shouldQuit()) {
- _menuInputActive = false;
- return 0;
- }
-
- // If video ended naturally (not by selection), loop back
- if (!_vm->_smushVideoShouldFinish) {
- // Video ended without selection (reached end or ESC during video)
- // Continue looping menu videos
- continue;
- }
-
- // Clear the flag
- _vm->_smushVideoShouldFinish = false;
-
- // A selection was made - process it
- debug("Rebel2: Menu video ended with selection=%d", _menuSelection);
-
- // Process the menu result based on current selection
- // Menu items matching GAME.TRS indices 11-17 (FUN_004147B2):
- // case 0 (TRS 11): Start Game -> pilot selection, returns 2
- // case 1 (TRS 12): Options -> FUN_00416787 options screen
- // case 2 (TRS 13): Calibrate Joystick -> FUN_00425820
- // case 3 (TRS 14): Continue Intro -> replay O_OPEN videos
- // case 4 (TRS 15): Show Top Pilots -> FUN_00420116(-1)
- // case 5 (TRS 16): Show Credits -> play O_CREDIT.SAN, returns 1
- // case 6 (TRS 17): Return to Launcher -> quit, returns 0
- switch (_menuSelection) {
- case 0: // Start Game -> go to pilot selection
- debug("Rebel2: Start Game selected - going to pilot selection");
- _gameState = kStatePilotSelect;
- _menuInputActive = false;
- return kMenuNewGame; // Return 2 (kMenuNewGame)
-
- case 1: // Options -> show options menu
- debug("Rebel2: Options selected");
- // TODO: Implement options menu (FUN_004167a6)
- // Options: Music, Sound, Voices, Auto Control, Indicators,
- // Arrows, Difficulty (0-5), Music Volume, SFX Volume
- break;
-
- case 2: // Calibrate Joystick
- debug("Rebel2: Calibrate Joystick selected");
- // TODO: Implement joystick calibration (FUN_00425820)
- // Plays O_CALIB.SAN with joystick calibration prompts
- break;
-
- case 3: // Continue Intro -> replay intro videos
- debug("Rebel2: Continue Intro selected - replaying intro");
- // Temporarily switch to intro state to disable menu overlay
- // This emulates FUN_004142BD case 0 behavior
- _gameState = kStateIntro;
- _menuInputActive = false;
- // Play intro sequence again (O_OPEN_A/B)
- splayer->setCurVideoFlags(0x20);
- splayer->play("OPEN/O_OPEN_A.SAN", 12);
- if (!_vm->shouldQuit()) {
- splayer->play("OPEN/O_OPEN_B.SAN", 12);
- }
- // Restore menu state
- _gameState = kStateMainMenu;
- _menuInputActive = true;
- break;
-
- case 4: // Show Top Pilots -> high score display
- debug("Rebel2: Show Top Pilots selected");
- // TODO: Implement high score display (FUN_00420116(-1))
- break;
-
- case 5: // Show Credits -> play credits video
- debug("Rebel2: Show Credits selected - playing O_CREDIT.SAN");
- _gameState = kStateCredits;
- _menuInputActive = false;
- splayer->setCurVideoFlags(0x20);
- splayer->play("OPEN/O_CREDIT.SAN", 12);
- _gameState = kStateMainMenu;
- _menuInputActive = true;
- // Returns 1 in original -> stays at stage 1 (main menu)
- break;
-
- case 6: // Return to Launcher -> quit game
- debug("Rebel2: Return to Launcher selected");
- _menuInputActive = false;
- return 0; // Return 0 to exit
-
- default:
- debug("Rebel2: Unknown menu selection %d", _menuSelection);
- break;
- }
- }
-
- _menuInputActive = false;
- return 0;
-}
-
-// ==================== Chapter Selection Screen ====================
-// Emulates FUN_00415CF8 - Chapter selection with preview and password input
-// This is the actual level/chapter selection that players see after pilot select
-
-int InsaneRebel2::runChapterSelect() {
- // Chapter selection screen loop - emulates FUN_00415CF8
- // Returns:
- // kChapterSelectPlay (5) = Play selected chapter
- // kChapterSelectBack (2) = Return to main menu (ESC or BACK)
- // kChapterSelectQuit (0) = Quit game
-
- debug("Rebel2: Entering chapter selection (FUN_00415CF8)");
-
- // Enable menu input capture
- _menuInputActive = true;
- while (!_menuEventQueue.empty()) _menuEventQueue.pop();
-
- // Initialize chapter selection state
- // Original (lines 51-54): local_10 = 0xf; while (local_10 > 0 && locked) local_10--;
- // Finds highest unlocked chapter. With debug unlock all = 15 (FINALE).
- _chapterSelection = 15;
- while (_chapterSelection > 0 && !_chapterUnlocked[_chapterSelection]) {
- _chapterSelection--;
- }
- _chapterItemCount = 17; // 16 chapters + RETURN TO PILOTS
- _selectedChapter = 0;
- _passwordInput = "";
- _menuRepeatDelay = 0;
- _gameState = kStateChapterSelect;
-
- SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
-
- // Initialize preview offset for initial selection
- _previewOffsetX = -90;
- _previewOffsetY = _chapterSelection * -50 + 75;
-
- // Set iactBits for chapter unlock state (FUN_00415CF8 lines 79-86)
- // Bits 16..1 correspond to chapters 0..15: set if unlocked, clear if locked.
- // These control SKIP chunks in O_LEVEL.SAN for locked/unlocked preview variants.
- for (int i = 0; i < 16; i++) {
- if (_chapterUnlocked[i])
- setBit(16 - i);
- else
- clearBit(16 - i);
- }
-
- // Chapter selection background - FUN_00415CF8 line 57:
- // FUN_0041f4d0(s_OPEN_O_LEVEL_SAN, 8, 0xffff, 0xffff, 0)
- // O_LEVEL.SAN contains chapter preview thumbnails at specific FOBJ positions.
- // The FOBJ offset system scrolls the correct preview into the preview box area.
- while (!_vm->shouldQuit()) {
- _vm->_smushVideoShouldFinish = false;
-
- debug("Rebel2: Playing chapter select background: OPEN/O_LEVEL.SAN");
-
- // Flags: 0x20 (overlay drawing). No 0x08 (preserve) â we want a black
- // background. O_LEVEL.SAN has no full-screen background FOBJ; the visible
- // screen area stays black, and preview thumbnails render at X=230 via offset.
- splayer->setCurVideoFlags(0x20);
-
- // Play O_LEVEL.SAN â preview thumbnails are rendered by FOBJ offset
- splayer->play("OPEN/O_LEVEL.SAN", 12);
-
- if (_vm->shouldQuit()) {
- _menuInputActive = false;
- return kChapterSelectQuit;
- }
-
- // If video ended without selection, continue looping
- if (!_vm->_smushVideoShouldFinish) {
- continue;
- }
-
- _vm->_smushVideoShouldFinish = false;
-
- debug("Rebel2: Chapter selection made: %d", _chapterSelection);
-
- // Process chapter selection (lines 134-236 of FUN_00415CF8)
- if (_chapterSelection == 16) {
- // BACK selected (index 16 = 17th item)
- debug("Rebel2: BACK to main menu selected");
- _menuInputActive = false;
- return kChapterSelectBack;
- }
-
- if (_chapterSelection >= 0 && _chapterSelection < 16) {
- // Chapter selected - start it regardless of unlock state.
- // TODO: locked chapters should require password (FUN_00415CF8 lines 239-257)
- _selectedChapter = _chapterSelection;
- debug("Rebel2: Chapter %d selected", _selectedChapter + 1);
- _menuInputActive = false;
- return kChapterSelectPlay;
- }
- }
-
- _menuInputActive = false;
- return kChapterSelectQuit;
-}
-
-int InsaneRebel2::processChapterSelectInput() {
- // Process input for chapter selection screen
- // Emulates input handling in FUN_00415CF8 (lines 95-133)
- // Returns: -1 = no action, 0+ = item selected
-
- int result = -1;
-
- while (!_menuEventQueue.empty()) {
- Common::Event event = _menuEventQueue.front();
- _menuEventQueue.pop();
-
- switch (event.type) {
- case Common::EVENT_KEYDOWN:
- switch (event.kbd.keycode) {
- case Common::KEYCODE_UP:
- // Move selection up, wrap to bottom
- _chapterSelection--;
- if (_chapterSelection < 0) {
- _chapterSelection = _chapterItemCount - 1;
- }
- // Update preview offset (FUN_00425170: Y = selected * -50 + 75)
- _previewOffsetY = _chapterSelection * -50 + 75;
- debug("ChapterSelect: Selection changed to %d (UP) offsetY=%d", _chapterSelection, _previewOffsetY);
- break;
-
- case Common::KEYCODE_DOWN:
- // Move selection down, wrap to top
- _chapterSelection++;
- if (_chapterSelection >= _chapterItemCount) {
- _chapterSelection = 0;
- }
- // Update preview offset (FUN_00425170: Y = selected * -50 + 75)
- _previewOffsetY = _chapterSelection * -50 + 75;
- debug("ChapterSelect: Selection changed to %d (DOWN) offsetY=%d", _chapterSelection, _previewOffsetY);
- break;
-
- case Common::KEYCODE_RETURN:
- case Common::KEYCODE_KP_ENTER:
- if (_chapterSelection >= 0 && _chapterSelection < _chapterItemCount) {
- result = _chapterSelection;
- debug("ChapterSelect: Item %d selected (ENTER)", _chapterSelection);
- }
- break;
-
- case Common::KEYCODE_ESCAPE:
- // ESC = Back to main menu (same as selecting BACK)
- result = 16; // BACK index
- debug("ChapterSelect: ESC pressed - back to menu");
- break;
-
- case Common::KEYCODE_BACKSPACE:
- // Backspace for password input (line 107-112 of FUN_00415CF8)
- if (!_passwordInput.empty()) {
- _passwordInput.deleteLastChar();
- debug("ChapterSelect: Password backspace, now: %s", _passwordInput.c_str());
- }
- break;
-
- default:
- // Printable character for password input (lines 114-121 of FUN_00415CF8)
- if (event.kbd.ascii >= 0x20 && event.kbd.ascii <= 0x7E) {
- if (_passwordInput.size() < 8) {
- _passwordInput += (char)event.kbd.ascii;
- debug("ChapterSelect: Password input: %s", _passwordInput.c_str());
- }
- }
- break;
- }
- break;
-
- case Common::EVENT_LBUTTONDOWN:
- // Click confirms the current selection (original: DAT_0047a7e4 & 1)
- if (_chapterSelection >= 0 && _chapterSelection < _chapterItemCount) {
- result = _chapterSelection;
- debug("ChapterSelect: Item %d confirmed (CLICK)", _chapterSelection);
- }
- break;
-
- case Common::EVENT_MOUSEMOVE:
- {
- // Mouse hover changes highlight (original FUN_0041f5ae mouse mode).
- // Item Y = numItems * -5 + i * 10 + 0x68
- int baseY = _chapterItemCount * -5 + 0x68;
- int mouseY = event.mouse.y;
-
- for (int i = 0; i < _chapterItemCount; i++) {
- int itemY = baseY + i * 10;
- if (mouseY >= itemY - 4 && mouseY < itemY + 6) {
- if (i != _chapterSelection) {
- _chapterSelection = i;
- _previewOffsetY = _chapterSelection * -50 + 75;
- debug(5, "ChapterSelect: Hover changed to %d", _chapterSelection);
- }
- break;
- }
- }
- }
- break;
-
- default:
- break;
- }
- }
-
- return result;
-}
-
-// Draw preview box border - emulates FUN_004292D0 calls at lines 128-133 of FUN_00415CF8
-void InsaneRebel2::drawPreviewBox(byte *renderBitmap, int pitch, int width, int height) {
- // Low-res (320x200) coordinates from FUN_00415CF8:
- // Outer box: X=0xe4 (228), Y=0x49 (73), W=0x54 (84), H=0x36 (54), color=0xF8
- // Inner box: X=0xe5 (229), Y=0x4a (74), W=0x52 (82), H=0x34 (52), color=4
-
- // Outer border (bright)
- int outerX = 228, outerY = 73, outerW = 84, outerH = 54;
- byte outerColor = 0xF8;
-
- // Draw outer box edges
- // Top edge
- for (int px = outerX; px < outerX + outerW && px < width; px++) {
- if (outerY >= 0 && outerY < height && px >= 0)
- renderBitmap[outerY * pitch + px] = outerColor;
- }
- // Bottom edge
- int bottomY = outerY + outerH - 1;
- if (bottomY < height) {
- for (int px = outerX; px < outerX + outerW && px < width; px++) {
- if (px >= 0)
- renderBitmap[bottomY * pitch + px] = outerColor;
- }
- }
- // Left edge
- for (int py = outerY; py < outerY + outerH && py < height; py++) {
- if (py >= 0 && outerX >= 0 && outerX < width)
- renderBitmap[py * pitch + outerX] = outerColor;
- }
- // Right edge
- int rightX = outerX + outerW - 1;
- if (rightX < width) {
- for (int py = outerY; py < outerY + outerH && py < height; py++) {
- if (py >= 0)
- renderBitmap[py * pitch + rightX] = outerColor;
- }
- }
-
- // Inner border (dark)
- int innerX = 229, innerY = 74, innerW = 82, innerH = 52;
- byte innerColor = 4;
-
- // Top edge
- for (int px = innerX; px < innerX + innerW && px < width; px++) {
- if (innerY >= 0 && innerY < height && px >= 0)
- renderBitmap[innerY * pitch + px] = innerColor;
- }
- // Bottom edge
- bottomY = innerY + innerH - 1;
- if (bottomY < height) {
- for (int px = innerX; px < innerX + innerW && px < width; px++) {
- if (px >= 0)
- renderBitmap[bottomY * pitch + px] = innerColor;
- }
- }
- // Left edge
- for (int py = innerY; py < innerY + innerH && py < height; py++) {
- if (py >= 0 && innerX >= 0 && innerX < width)
- renderBitmap[py * pitch + innerX] = innerColor;
- }
- // Right edge
- rightX = innerX + innerW - 1;
- if (rightX < width) {
- for (int py = innerY; py < innerY + innerH && py < height; py++) {
- if (py >= 0)
- renderBitmap[py * pitch + rightX] = innerColor;
- }
- }
-}
-
-// Draw preview thumbnail content - emulates FUN_00428a10 + FUN_00429b40
-// Based on FUN_00415CF8 assembly analysis:
-//
-// The original uses O_LEVEL.SAN (640x400) with chapter previews stacked vertically.
-// Video offset (FUN_00425170) shifts which preview is visible:
-// X offset = -90 (0xffa6)
-// Y offset = chapter * -50 + 75
-//
-// For 320x200 mode, O_MENU_X.SAN doesn't contain chapter-specific preview images.
-// Those are only in O_LEVEL.SAN (640x400). We display a styled placeholder instead
-// with the chapter number and visual styling to match the original UI appearance.
-void InsaneRebel2::drawPreviewThumbnail(byte *renderBitmap, int pitch, int width, int height, int chapter) {
- // Preview destination area coordinates (inside the inner border)
- // From assembly: Inner box at X=230, Y=75, W=80, H=50
- const int destX = 230;
- const int destY = 75;
- const int thumbW = 80; // 0x50
- const int thumbH = 50; // 0x32
-
- // Fill preview area with a dark blue gradient background
- // This creates a styled placeholder since O_MENU_X.SAN doesn't have previews
- for (int py = 0; py < thumbH; py++) {
- int dy = destY + py;
- if (dy < 0 || dy >= height) continue;
-
- // Create vertical gradient: darker at top (0x10), lighter at bottom (0x18)
- byte bgColor = 0x10 + (py * 8 / thumbH);
-
- for (int px = 0; px < thumbW; px++) {
- int dx = destX + px;
- if (dx < 0 || dx >= width) continue;
- renderBitmap[dy * pitch + dx] = bgColor;
- }
- }
-
- // Draw chapter number overlay in the center of the preview
- NutRenderer *font = _smush_smalfontNut;
- if (!font) return;
-
- char chapterStr[16];
- if (chapter < 15) {
- snprintf(chapterStr, sizeof(chapterStr), "CH.%d", chapter + 1);
- } else {
- snprintf(chapterStr, sizeof(chapterStr), "FINALE");
- }
-
- // Calculate text width for centering
- int textWidth = 0;
- int numChars = font->getNumChars();
- for (const char *c = chapterStr; *c; c++) {
- int charIdx = (unsigned char)*c;
- if (charIdx < numChars) {
- textWidth += font->getCharWidth(charIdx);
- }
- }
-
- // Center the text in the preview area
- int textX = destX + (thumbW - textWidth) / 2;
- int textY = destY + thumbH / 2 - 4;
-
- Common::Rect clipRect(0, 0, width, height);
-
- // Draw text shadow (offset by 1,1)
- int curX = textX + 1;
- for (const char *c = chapterStr; *c; c++) {
- int charIdx = (unsigned char)*c;
- if (charIdx < numChars) {
- int charWidth = font->getCharWidth(charIdx);
- if (curX >= 0 && curX + charWidth <= width && textY + 1 >= 0 && textY + 1 < height) {
- font->drawCharV7(renderBitmap, clipRect, curX, textY + 1, pitch, 0,
- kStyleAlignLeft, charIdx, true, true);
- }
- curX += charWidth;
- }
- }
-
- // Draw main text (bright)
- curX = textX;
- for (const char *c = chapterStr; *c; c++) {
- int charIdx = (unsigned char)*c;
- if (charIdx < numChars) {
- int charWidth = font->getCharWidth(charIdx);
- if (curX >= 0 && curX + charWidth <= width && textY >= 0 && textY < height) {
- font->drawCharV7(renderBitmap, clipRect, curX, textY, pitch, -1,
- kStyleAlignLeft, charIdx, true, true);
- }
- curX += charWidth;
- }
- }
-
- // Draw lock icon for locked chapters
- if (!_chapterUnlocked[chapter]) {
- byte lockColor = 0xF8;
- int lockX = destX + thumbW - 15;
- int lockY = destY + 5;
-
- // Draw padlock shape
- for (int i = 2; i < 6; i++) {
- if (lockX + i < width && lockY < height && lockY >= 0)
- renderBitmap[lockY * pitch + lockX + i] = lockColor;
- }
- for (int i = 1; i < 4; i++) {
- if (lockX + 2 < width && lockY + i < height && lockY + i >= 0)
- renderBitmap[(lockY + i) * pitch + lockX + 2] = lockColor;
- if (lockX + 5 < width && lockY + i < height && lockY + i >= 0)
- renderBitmap[(lockY + i) * pitch + lockX + 5] = lockColor;
- }
- for (int y = 0; y < 4; y++) {
- for (int x = 1; x < 7; x++) {
- if (lockX + x < width && lockY + 4 + y < height && lockY + 4 + y >= 0)
- renderBitmap[(lockY + 4 + y) * pitch + lockX + x] = lockColor;
- }
- }
- }
-}
-
-// Draw score/info line at bottom of chapter select - emulates FUN_00434cb0 calls
-// For unlocked chapters: score display using TRS 80 at (25, 190)
-// For locked chapters: password prompt at (30, 190)
-void InsaneRebel2::drawChapterInfoLine(byte *renderBitmap, int pitch, int width, int height) {
- if (_chapterSelection < 0 || _chapterSelection >= 16) return;
-
- SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
- if (!splayer) return;
-
- NutRenderer *font = _smush_smalfontNut;
- if (!font) return;
-
- Common::Rect clipRect(0, 0, _vm->_screenWidth, _vm->_screenHeight);
- int actualPitch = _vm->_screenWidth;
-
- if (_chapterUnlocked[_chapterSelection]) {
- // Unlocked: show score info using TRS 80 at X=25 (0x19), Y=190 (0xbe)
- const char *scoreStr = splayer->getString(80);
- if (!scoreStr || !scoreStr[0]) return;
-
- int curX = 25;
- int numChars = font->getNumChars();
- for (const char *c = scoreStr; *c; c++) {
- byte ch = (byte)*c;
- if (ch >= 'a' && ch <= 'z') ch = ch - 'a' + 'A';
- if (ch < numChars) {
- int charW = font->getCharWidth(ch);
- if (curX >= 0 && curX + charW <= width && 190 < height) {
- font->drawCharV7(renderBitmap, clipRect, curX, 190, actualPitch, 1,
- kStyleAlignLeft, ch, false, false);
- }
- curX += charW;
- }
- }
- }
-}
-
-// Draw chapter selection overlay - called during O_LEVEL.SAN playback
-// FUN_00415CF8 - Chapter selection screen with preview thumbnail
-void InsaneRebel2::drawChapterSelectOverlay(byte *renderBitmap, int pitch, int width, int height) {
- // Emulates FUN_00415CF8 rendering via shared drawMenuItems(leftAligned=true)
- //
- // GAME.TRS chapter selection strings:
- // TRS 40 = "^f02Chapters" (title)
- // TRS 41-56 = unlocked chapter names (e.g. "^f01^c244Chapter 1 - The Dreighton Triangle")
- // TRS 57 = "^f01^c240RETURN TO PILOTS"
- // TRS 60 = "^f02Chapters" (title, locked section duplicate)
- // TRS 61-76 = locked chapter names (e.g. "^f01^c244Chapter 1 -")
- // TRS 77 = "^f01^c240RETURN TO PILOTS" (locked section duplicate)
- //
- // Menu array: items[0]=title, items[1..16]=chapters, items[17]=RETURN TO PILOTS
- // FUN_0041f5ae(0, &DAT_004577a8, 0x11, 1): param_3=17, param_4=1 (left-aligned)
-
- SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
- if (!splayer) return;
-
- // Build items array matching original DAT_004577a8 layout
- const char *items[18];
-
- // items[0] = title = TRS 40 ("^f02Chapters")
- items[0] = splayer->getString(40);
- if (!items[0] || !items[0][0]) items[0] = "^f02Chapters";
-
- // items[1..16] = chapters, using unlocked (TRS 41-56) or locked (TRS 61-76) strings
- for (int i = 1; i <= 16; i++) {
- bool unlocked = (i - 1 < 16) && _chapterUnlocked[i - 1];
- int trsIdx = unlocked ? (40 + i) : (60 + i);
- items[i] = splayer->getString(trsIdx);
- if (!items[i] || !items[i][0]) items[i] = "";
- }
-
- // items[17] = "RETURN TO PILOTS" = TRS 57 ("^f01^c240RETURN TO PILOTS")
- items[17] = splayer->getString(57);
- if (!items[17] || !items[17][0]) items[17] = "^f01^c240RETURN TO PILOTS";
-
- // Render menu using shared renderer with left-aligned mode
- drawMenuItems(renderBitmap, pitch, width, height, items, 17, _chapterSelection, true);
-
- // Draw preview box border on the right side (FUN_004292d0 calls at lines 128-133)
- // The actual preview image is rendered by O_LEVEL.SAN FOBJ via the offset system
- drawPreviewBox(renderBitmap, pitch, width, height);
-
- // Draw score/info line at bottom
- drawChapterInfoLine(renderBitmap, pitch, width, height);
-}
-
-// ==================== Pilot Selection Menu (FUN_00414A41) ====================
-// Emulates FUN_00414A41 - Pilot/save selection menu
-// This appears before chapter selection. All options go to chapter selection except MAIN MENU.
-
-int InsaneRebel2::runLevelSelect() {
- // Pilot selection menu loop - emulates FUN_00414A41
- // Returns:
- // kLevelSelectPlay (1) = Go to chapter selection (pilot selected or NEW+difficulty chosen)
- // kLevelSelectBack (0) = Return to main menu (MAIN MENU or ESC)
- // kLevelSelectQuit (2) = Quit game
- //
- // Original action dispatch (FUN_00414A41):
- // sel < N â saved pilot selected â return 3 (start game)
- // sel == N â ADD NEW PILOT â difficulty submenu â loop back
- // sel == N+1 â COPY PILOT â source select (no-op if N==0)
- // sel == N+2 â DELETE PILOT â confirm select (no-op if N==0)
- // sel == N+3 â RETURN TO MAIN MENU â return 1
- // ESC â return 1
-
- debug("Rebel2: Entering pilot selection (FUN_00414A41)");
-
- _menuInputActive = true;
- while (!_menuEventQueue.empty()) _menuEventQueue.pop();
-
- // Number of saved pilots (TODO: implement save system)
- int numPilots = 0;
-
- _levelSelection = 0;
- _levelItemCount = numPilots + 4; // N pilots + NEW/COPY/DELETE/MAIN MENU
- _selectedLevel = 1;
- _menuRepeatDelay = 0;
- _gameState = kStatePilotSelect;
-
- SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
-
- // Pilot selection uses menu video as background (320x200 mode)
- while (!_vm->shouldQuit()) {
- _vm->_smushVideoShouldFinish = false;
-
- Common::String menuVideo = getRandomMenuVideo();
- debug("Rebel2: Playing pilot select background: %s", menuVideo.c_str());
-
- splayer->setCurVideoFlags(0x20);
- splayer->play(menuVideo.c_str(), 12);
-
- if (_vm->shouldQuit()) {
- _menuInputActive = false;
- return kLevelSelectQuit;
- }
-
- if (!_vm->_smushVideoShouldFinish) {
- continue;
- }
-
- _vm->_smushVideoShouldFinish = false;
-
- // Dispatch based on current game state
- if (_gameState == kStateDifficultySelect) {
- // Difficulty submenu â selection made
- debug("Rebel2: Difficulty %d selected", _difficultySelection);
- // Original stores difficulty in pilot record and loops back.
- // Since we have no save system, proceed to chapter select.
- _gameState = kStatePilotSelect;
- _menuInputActive = false;
- return kLevelSelectPlay;
- }
-
- // Pilot menu â process selection
- debug("Rebel2: Pilot selection made: %d (numPilots=%d)", _levelSelection, numPilots);
-
- if (_levelSelection < numPilots) {
- // Saved pilot selected â go to chapter selection
- _selectedLevel = _levelSelection + 1;
- debug("Rebel2: Pilot %d selected - going to chapter selection", _selectedLevel);
- _menuInputActive = false;
- return kLevelSelectPlay;
- } else if (_levelSelection == numPilots) {
- // ADD NEW PILOT â show difficulty submenu
- debug("Rebel2: ADD NEW PILOT - showing difficulty submenu");
- _gameState = kStateDifficultySelect;
- _difficultySelection = 2; // Default to 3rd option (matching original init)
- // Continue loop â next video frame will render difficulty menu
- continue;
- } else if (_levelSelection == numPilots + 1) {
- // COPY PILOT â no-op when numPilots==0 (original checks local_10 != 0)
- if (numPilots > 0) {
- debug("Rebel2: COPY PILOT selected");
- // TODO: implement copy pilot sub-flow
- } else {
- debug("Rebel2: COPY PILOT - no pilots to copy");
- }
- continue;
- } else if (_levelSelection == numPilots + 2) {
- // DELETE PILOT â no-op when numPilots==0 (original checks local_10 != 0)
- if (numPilots > 0) {
- debug("Rebel2: DELETE PILOT selected");
- // TODO: implement delete pilot sub-flow
- } else {
- debug("Rebel2: DELETE PILOT - no pilots to delete");
- }
- continue;
- } else if (_levelSelection == numPilots + 3) {
- // RETURN TO MAIN MENU
- debug("Rebel2: Back to main menu selected");
- _menuInputActive = false;
- return kLevelSelectBack;
- }
- }
-
- _menuInputActive = false;
- return kLevelSelectQuit;
-}
-
-int InsaneRebel2::processLevelSelectInput() {
- // Process input for pilot selection and difficulty submenu
- // Handles both kStatePilotSelect and kStateDifficultySelect modes
- // Returns: -1 = no action, 0+ = item selected
-
- int result = -1;
-
- // Determine which menu mode we're in
- bool isDifficultyMode = (_gameState == kStateDifficultySelect);
- int &selection = isDifficultyMode ? _difficultySelection : _levelSelection;
- int itemCount = isDifficultyMode ? 6 : _levelItemCount;
-
- // Mouse hit Y positions â must match drawMenuItems() formula
- const int baseY = itemCount * -5 + 0x68;
- const int itemHeight = 10;
-
- while (!_menuEventQueue.empty()) {
- Common::Event event = _menuEventQueue.pop();
- switch (event.type) {
- case Common::EVENT_KEYDOWN:
- switch (event.kbd.keycode) {
- case Common::KEYCODE_UP:
- selection--;
- if (selection < 0) {
- selection = itemCount - 1;
- }
- debug("LevelSelect: Selection changed to %d (UP, %s)",
- selection, isDifficultyMode ? "difficulty" : "pilot");
- break;
-
- case Common::KEYCODE_DOWN:
- selection++;
- if (selection >= itemCount) {
- selection = 0;
- }
- debug("LevelSelect: Selection changed to %d (DOWN, %s)",
- selection, isDifficultyMode ? "difficulty" : "pilot");
- break;
-
- case Common::KEYCODE_RETURN:
- case Common::KEYCODE_KP_ENTER:
- if (selection >= 0 && selection < itemCount) {
- result = selection;
- debug("LevelSelect: Item %d selected (ENTER, %s)",
- selection, isDifficultyMode ? "difficulty" : "pilot");
- }
- break;
-
- case Common::KEYCODE_ESCAPE:
- if (isDifficultyMode) {
- // ESC in difficulty submenu â return to pilot menu
- // Note: original has no ESC in difficulty submenu, but we add it as a
- // graceful fallback
- _gameState = kStatePilotSelect;
- debug("LevelSelect: ESC in difficulty - back to pilot menu");
- } else {
- // ESC in pilot menu â back to main menu (return 1 in original)
- result = _levelItemCount - 1; // Last item = MAIN MENU
- debug("LevelSelect: ESC pressed - back to main menu");
- }
- break;
-
- default:
- break;
- }
- break;
-
- case Common::EVENT_LBUTTONDOWN:
- {
- int mouseY = event.mouse.y;
- debug("LevelSelect: Click at Y=%d (%s)", mouseY,
- isDifficultyMode ? "difficulty" : "pilot");
-
- for (int i = 0; i < itemCount; i++) {
- int itemY = baseY + i * itemHeight;
- if (mouseY >= itemY - 4 && mouseY < itemY + 6) {
- selection = i;
- result = i;
- debug("LevelSelect: Item %d clicked at Y=%d (itemY=%d)", i, mouseY, itemY);
- break;
- }
- }
- }
- break;
-
- case Common::EVENT_MOUSEMOVE:
- _vm->_mouse.x = event.mouse.x;
- _vm->_mouse.y = event.mouse.y;
- break;
-
- case Common::EVENT_QUIT:
- case Common::EVENT_RETURN_TO_LAUNCHER:
- if (isDifficultyMode) {
- _gameState = kStatePilotSelect;
- } else {
- result = _levelItemCount - 1;
- }
- break;
-
- default:
- break;
- }
- }
-
- return result;
-}
-
-void InsaneRebel2::drawLevelSelectOverlay(byte *renderBitmap, int pitch, int width, int height) {
- // =====================================================================
- // Pilot selection / difficulty submenu renderer
- // Emulates FUN_00414A41 â FUN_0041f5ae
- // =====================================================================
-
- SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
- if (!splayer) {
- debug(1, "drawLevelSelectOverlay: SmushPlayer not available for TRS strings!");
- return;
- }
-
- if (_gameState == kStateDifficultySelect) {
- // =====================================================================
- // Difficulty submenu - LAB_00414ff6
- // FUN_0041f5ae(0, &DAT_00457458, 6, 0)
- // DAT_00457458 = 7 entries loaded from TRS 110-116 (FUN_00414073 lines 47-50)
- // param_3 = 6 â items[0]=title(TRS 110), items[1..6]=selectable(TRS 111-116)
- // =====================================================================
- const char *diffItems[7];
- for (int i = 0; i < 7; i++) {
- diffItems[i] = splayer->getString(110 + i);
- if (!diffItems[i] || !diffItems[i][0]) {
- diffItems[i] = "";
- }
- }
- drawMenuItems(renderBitmap, pitch, width, height, diffItems, 6, _difficultySelection);
- return;
- }
-
- // =====================================================================
- // Pilot menu - FUN_0041f5ae(0, &DAT_00457768, N+4, 0)
- // =====================================================================
- // items[0] = title (TRS 20)
- // items[1..N] = saved pilots (formatted with ^f01^c005)
- // items[N+1] = TRS 21 (ADD NEW PILOT)
- // items[N+2] = TRS 22 (COPY PILOT)
- // items[N+3] = TRS 23 (DELETE PILOT)
- // items[N+4] = TRS 24 (RETURN TO MAIN MENU)
-
- int numPilots = 0; // TODO: implement save system
-
- const char *pilotItems[11];
- int idx = 0;
-
- // Title: TRS 20
- pilotItems[idx++] = splayer->getString(20);
-
- // Saved pilot slots would be inserted here (items[1..numPilots])
- // Each formatted as "^f01^c005<name>^f00" by the original
-
- // Fixed options: TRS 21-24
- for (int i = 0; i < 4; i++) {
- pilotItems[idx++] = splayer->getString(21 + i);
- }
-
- for (int i = 0; i < idx; i++) {
- if (!pilotItems[i] || !pilotItems[i][0]) {
- pilotItems[i] = "";
- }
- }
-
- drawMenuItems(renderBitmap, pitch, width, height, pilotItems, numPilots + 4, _levelSelection);
-
- // Pilot info display at fixed coordinates when saved pilot selected
- // FUN_00414A41 lines 78-86: FUN_00434cb0 at Y=0xb4(180) and Y=0xbe(190)
- // TODO: Implement when save system is added
-}
-
-// ======================= Level Loading System =======================
-// Emulates the level handler functions from FUN_00417E53 through FUN_0041BBE8
-// Based on disassembly analysis of the retail Rebel Assault 2 executable.
-
-Common::String InsaneRebel2::getLevelDir(int levelId) {
- // Returns directory name like "LEV01" for level 1
- return Common::String::format("LEV%02d", levelId);
-}
-
-Common::String InsaneRebel2::getLevelPrefix(int levelId) {
- // Returns file prefix like "01" for level 1
- return Common::String::format("%02d", levelId);
-}
-
-void InsaneRebel2::playIntroSequence() {
- // Emulates case 0 in FUN_004142BD
- // Plays the game intro sequence:
- // 1. CREDITS/O_OPEN_C.SAN - Fox logo (if certain conditions)
- // 2. CREDITS/O_OPEN_D.SAN - LucasArts logo (if certain conditions)
- // 3. OPEN/O_OPEN_A.SAN - Main intro
- // 4. OPEN/O_OPEN_B.SAN - Additional intro (if conditions)
-
- debug("Rebel2: Playing intro sequence");
-
- SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
-
- // Set intro flags (non-interactive)
- splayer->setCurVideoFlags(0x20);
-
- // Play Fox logo (CREDITS/O_OPEN_C.SAN)
- // In retail, this checks if 'f', 'o', 'x' keys are held (easter egg)
- // We'll play it unconditionally for now
- debug("Rebel2: Playing Fox logo");
- splayer->play("CREDITS/O_OPEN_C.SAN", 12);
-
- if (_vm->shouldQuit()) return;
-
- // Play LucasArts logo (CREDITS/O_OPEN_D.SAN)
- // In retail, this checks if 'b', 'o', 't' keys are held
- debug("Rebel2: Playing LucasArts logo");
- splayer->play("CREDITS/O_OPEN_D.SAN", 12);
-
- if (_vm->shouldQuit()) return;
-
- // Play main intro (OPEN/O_OPEN_A.SAN)
- debug("Rebel2: Playing main intro");
- splayer->play("OPEN/O_OPEN_A.SAN", 12);
-
- if (_vm->shouldQuit()) return;
-
- // Play additional intro (OPEN/O_OPEN_B.SAN)
- // In retail, this plays if DAT_0047ab45 or DAT_0047ab47 != 0
- debug("Rebel2: Playing additional intro");
- splayer->play("OPEN/O_OPEN_B.SAN", 12);
-}
-
-void InsaneRebel2::playMissionBriefing() {
- // Emulates FUN_00415CF8 (partial - just the video)
- // Plays OPEN/O_LEVEL.SAN which shows the mission briefing screen
-
- debug("Rebel2: Playing mission briefing");
-
- SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
- splayer->setCurVideoFlags(0x08); // Briefing mode flag
- splayer->play("OPEN/O_LEVEL.SAN", 12);
-}
-
-void InsaneRebel2::playCinematic(const char *filename) {
- // Play a cinematic/cutscene video with proper intro mode setup
- // This helper ensures:
- // 1. Handler is reset to 0 (no HUD, no shooting)
- // 2. Video flags are set to 0x28 (cinematic with buffer preserve)
- //
- // Original: All video wrapper functions (FUN_00417168, FUN_004171c5,
- // FUN_00417ab2, FUN_00417327) add | 8 to the base flags before calling
- // FUN_0041f4d0, so the 0x08 bit (preserve buffer) is always set.
- _rebelHandler = 0;
- _rebelStatusBarSprite = 0; // No status bar during cinematics
-
- SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
- splayer->setCurVideoFlags(0x28); // Cinematic mode + buffer preserve (0x20 | 0x08)
- splayer->play(filename, 12);
-}
-
-void InsaneRebel2::playLevelBegin(int levelId) {
- // Play the level beginning cinematic (LEVXX/XXBEG.SAN)
- // Emulates FUN_004171c5 call in each level handler
-
- Common::String dir = getLevelDir(levelId);
- Common::String prefix = getLevelPrefix(levelId);
- Common::String filename = Common::String::format("%s/%sBEG.SAN", dir.c_str(), prefix.c_str());
-
- debug("Rebel2: Playing level %d beginning: %s", levelId, filename.c_str());
- playCinematic(filename.c_str());
-}
-
-bool InsaneRebel2::playLevelGameplay(int levelId) {
- // Play the main gameplay video(s) for a level
- // Returns true if level completed (damage < 0xff), false if died
- //
- // Different levels have different gameplay structures:
- // - Level 1, 4, 5: Single gameplay SAN (XXPXX.SAN or XXPLAY.SAN)
- // - Level 2: Multiple parts with subdirectories (P1/, P2/, P3/)
- // - Level 3, 6: Two gameplay phases (XXPLAY1.SAN, XXPLAY2.SAN)
-
- Common::String dir = getLevelDir(levelId);
- Common::String prefix = getLevelPrefix(levelId);
- Common::String filename;
-
- SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
-
- // Set gameplay flags (interactive with HUD)
- splayer->setCurVideoFlags(0x28);
-
- // Reset damage/shield for this level
- _playerShield = 255;
- _rebelHandler = 0;
- _rebelStatusBarSprite = 0; // Will be set by IACT opcode 6 if par4==1
-
- debug("Rebel2: Starting gameplay for level %d", levelId);
-
- switch (levelId) {
- case 1:
- // Level 1: Single gameplay file (01P01.SAN)
- // Level 1 uses Handler 0x26 (turret mode) - set before gameplay
- _rebelHandler = 0x26;
- filename = Common::String::format("%s/%sP01.SAN", dir.c_str(), prefix.c_str());
- debug("Rebel2: Playing %s", filename.c_str());
- splayer->play(filename.c_str(), 12);
- break;
-
- case 2:
- // Level 2: Has cutscene first, then multiple parts
- // Level 2 uses Handler 8 (third-person on foot mode) - set before gameplay
- _rebelHandler = 8;
- // First play the cutscene
- filename = Common::String::format("%s/%sCUT.SAN", dir.c_str(), prefix.c_str());
- debug("Rebel2: Playing cutscene %s", filename.c_str());
- splayer->setCurVideoFlags(0x28);
- splayer->play(filename.c_str(), 12);
-
- if (_vm->shouldQuit() || _playerShield == 0) return false;
-
- // Part 1 (multiple variations - play A for now)
- splayer->setCurVideoFlags(0x28);
- filename = Common::String::format("%s/P1/%sP01_A.SAN", dir.c_str(), prefix.c_str());
- debug("Rebel2: Playing %s", filename.c_str());
- splayer->play(filename.c_str(), 12);
-
- if (_vm->shouldQuit() || _playerShield == 0) return false;
-
- // Post segment 1
- _rebelHandler = 0;
- _rebelStatusBarSprite = 0;
- filename = Common::String::format("%s/%sPST1.SAN", dir.c_str(), prefix.c_str());
- debug("Rebel2: Playing %s", filename.c_str());
- splayer->setCurVideoFlags(0x28);
- splayer->play(filename.c_str(), 12);
-
- if (_vm->shouldQuit() || _playerShield == 0) return false;
-
- // Part 2
- _rebelHandler = 8;
- splayer->setCurVideoFlags(0x28);
- filename = Common::String::format("%s/P2/%sP02_A.SAN", dir.c_str(), prefix.c_str());
- debug("Rebel2: Playing %s", filename.c_str());
- splayer->play(filename.c_str(), 12);
-
- if (_vm->shouldQuit() || _playerShield == 0) return false;
-
- // Post segment 2
- _rebelHandler = 0;
- _rebelStatusBarSprite = 0;
- filename = Common::String::format("%s/%sPST2.SAN", dir.c_str(), prefix.c_str());
- debug("Rebel2: Playing %s", filename.c_str());
- splayer->setCurVideoFlags(0x28);
- splayer->play(filename.c_str(), 12);
-
- if (_vm->shouldQuit() || _playerShield == 0) return false;
-
- // Part 3
- _rebelHandler = 8;
- splayer->setCurVideoFlags(0x28);
- filename = Common::String::format("%s/P3/%sP03_A.SAN", dir.c_str(), prefix.c_str());
- debug("Rebel2: Playing %s", filename.c_str());
- splayer->play(filename.c_str(), 12);
- break;
-
- case 3:
- // Level 3: Two gameplay phases (third-person ship)
- // Level 3 uses Handler 7 (third-person ship mode) - FUN_0040d836/FUN_0040c3cc
- _rebelHandler = 7;
- filename = Common::String::format("%s/%sPLAY1.SAN", dir.c_str(), prefix.c_str());
- debug("Rebel2: Playing %s", filename.c_str());
- splayer->play(filename.c_str(), 12);
-
- if (_vm->shouldQuit() || _playerShield == 0) return false;
-
- // Post segment
- _rebelHandler = 0;
- _rebelStatusBarSprite = 0;
- filename = Common::String::format("%s/%sPOST1.SAN", dir.c_str(), prefix.c_str());
- debug("Rebel2: Playing %s", filename.c_str());
- splayer->setCurVideoFlags(0x28);
- splayer->play(filename.c_str(), 12);
-
- if (_vm->shouldQuit() || _playerShield == 0) return false;
-
- // Phase 2 â handler will be re-set by IACT opcode 6
- splayer->setCurVideoFlags(0x28);
- filename = Common::String::format("%s/%sPLAY2.SAN", dir.c_str(), prefix.c_str());
- debug("Rebel2: Playing %s", filename.c_str());
- splayer->play(filename.c_str(), 12);
- break;
-
- case 4:
- _rebelHandler = 0x26;
- // Level 4: Has cutscene, then single gameplay
- filename = Common::String::format("%s/%sCUT.SAN", dir.c_str(), prefix.c_str());
- debug("Rebel2: Playing cutscene %s", filename.c_str());
- splayer->setCurVideoFlags(0x28);
- splayer->play(filename.c_str(), 12);
-
- if (_vm->shouldQuit()) return false;
-
- splayer->setCurVideoFlags(0x28);
- filename = Common::String::format("%s/%sPLAY.SAN", dir.c_str(), prefix.c_str());
- debug("Rebel2: Playing %s", filename.c_str());
- splayer->play(filename.c_str(), 12);
- break;
-
- case 5:
- // Level 5: Single gameplay file
- filename = Common::String::format("%s/%sPLAY.SAN", dir.c_str(), prefix.c_str());
- debug("Rebel2: Playing %s", filename.c_str());
- splayer->play(filename.c_str(), 12);
- break;
-
- case 6:
- // Level 6: Two gameplay phases
- filename = Common::String::format("%s/%sPLAY1.SAN", dir.c_str(), prefix.c_str());
- debug("Rebel2: Playing %s", filename.c_str());
- splayer->play(filename.c_str(), 12);
-
- if (_vm->shouldQuit() || _playerShield == 0) return false;
-
- // Post segment
- _rebelHandler = 0;
- _rebelStatusBarSprite = 0;
- filename = Common::String::format("%s/%sPOST1.SAN", dir.c_str(), prefix.c_str());
- debug("Rebel2: Playing %s", filename.c_str());
- splayer->setCurVideoFlags(0x28);
- splayer->play(filename.c_str(), 12);
-
- if (_vm->shouldQuit() || _playerShield == 0) return false;
-
- // Phase 2 â handler will be re-set by IACT opcode 6
- splayer->setCurVideoFlags(0x28);
- filename = Common::String::format("%s/%sPLAY2.SAN", dir.c_str(), prefix.c_str());
- debug("Rebel2: Playing %s", filename.c_str());
- splayer->play(filename.c_str(), 12);
- break;
-
- default:
- // For levels 7-15 (not in demo), try common patterns
- // First try XXPLAY.SAN
- filename = Common::String::format("%s/%sPLAY.SAN", dir.c_str(), prefix.c_str());
- debug("Rebel2: Trying %s", filename.c_str());
- splayer->play(filename.c_str(), 12);
- break;
- }
-
- // Return true if player survived (shield > 0), false if died
- return (_playerShield > 0);
-}
-
-void InsaneRebel2::playLevelEnd(int levelId) {
- // Play level completion video (LEVXX/XXEND.SAN)
- // Emulates FUN_00417327 call
-
- _rebelHandler = 0;
- _rebelStatusBarSprite = 0; // No status bar during end cinematic
-
- Common::String dir = getLevelDir(levelId);
- Common::String prefix = getLevelPrefix(levelId);
- Common::String filename = Common::String::format("%s/%sEND.SAN", dir.c_str(), prefix.c_str());
-
- debug("Rebel2: Playing level %d end: %s", levelId, filename.c_str());
-
- SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
- // Original: FUN_00417327 adds | 8, so flags = 0x20 | 0x08 = 0x28
- splayer->setCurVideoFlags(0x28);
- splayer->play(filename.c_str(), 12);
-}
-
-void InsaneRebel2::playLevelDeath(int levelId) {
- // Play death video (LEVXX/XXDIE_X.SAN)
- // The variant depends on the frame where player died
- // For simplicity, we'll play the A variant
-
- _rebelHandler = 0;
- _rebelStatusBarSprite = 0; // No status bar during death cinematic
-
- Common::String dir = getLevelDir(levelId);
- Common::String prefix = getLevelPrefix(levelId);
-
- // Most levels have DIE_A, some have just DIE
- Common::String filename;
- if (levelId == 2 || levelId == 4 || levelId == 10 || levelId == 12 || levelId == 14) {
- filename = Common::String::format("%s/%sDIE.SAN", dir.c_str(), prefix.c_str());
- } else {
- filename = Common::String::format("%s/%sDIE_A.SAN", dir.c_str(), prefix.c_str());
- }
-
- debug("Rebel2: Playing level %d death: %s", levelId, filename.c_str());
-
- SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
- // Original: FUN_00417168 adds | 8, so flags = 0x20 | 0x08 = 0x28
- splayer->setCurVideoFlags(0x28);
- splayer->play(filename.c_str(), 12);
-}
-
-void InsaneRebel2::playLevelRetry(int levelId) {
- // Play retry prompt video (LEVXX/XXRETRY.SAN)
- // Reset handler state for the retry cinematic
-
- _rebelHandler = 0;
- _rebelStatusBarSprite = 0; // Reset for retry - will be set by IACT opcode 6 if needed
-
- Common::String dir = getLevelDir(levelId);
- Common::String prefix = getLevelPrefix(levelId);
- Common::String filename = Common::String::format("%s/%sRETRY.SAN", dir.c_str(), prefix.c_str());
-
- debug("Rebel2: Playing level %d retry: %s", levelId, filename.c_str());
-
- SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
- // Original: FUN_00417168 adds | 8, so flags = 0x20 | 0x08 = 0x28
- splayer->setCurVideoFlags(0x28);
- splayer->play(filename.c_str(), 12);
-}
-
-void InsaneRebel2::playLevelGameOver(int levelId) {
- // Play game over video (LEVXX/XXOVER.SAN)
- // Emulates FUN_00417ab2 call
-
- _rebelHandler = 0;
- _rebelStatusBarSprite = 0; // No status bar during game over cinematic
-
- Common::String dir = getLevelDir(levelId);
- Common::String prefix = getLevelPrefix(levelId);
- Common::String filename = Common::String::format("%s/%sOVER.SAN", dir.c_str(), prefix.c_str());
-
- debug("Rebel2: Playing level %d game over: %s", levelId, filename.c_str());
-
- SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
- // Original: FUN_00417ab2 adds | 8, so flags = 0x20 | 0x08 = 0x28
- splayer->setCurVideoFlags(0x28);
- splayer->play(filename.c_str(), 12);
-}
-
-void InsaneRebel2::playCreditsSequence() {
- // Play the end credits (OPEN/O_CREDIT.SAN)
- // Individual credits are in CREDITS/CRED_XX.SAN
-
- debug("Rebel2: Playing credits");
-
- SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
- splayer->setCurVideoFlags(0x20);
- splayer->play("OPEN/O_CREDIT.SAN", 12);
-}
-
-int InsaneRebel2::runLevel(int levelId) {
- // Main level dispatcher - calls per-level handlers
- // Each level handler emulates its retail counterpart (FUN_00417E53 etc.)
-
- debug("Rebel2: Starting level %d", levelId);
-
- // Validate level ID
- if (levelId < 1 || levelId > 15) {
- warning("Rebel2: Invalid level ID %d", levelId);
- return kLevelReturnToMenu;
- }
-
- // Switch to gameplay state to stop menu overlay rendering
- _gameState = kStateGameplay;
- _menuInputActive = false;
-
- // Clear the screen to remove any leftover menu pixels
- VirtScreen *vs = &_vm->_virtscr[kMainVirtScreen];
- memset(vs->getPixels(0, 0), 0, vs->pitch * vs->h);
- _vm->markRectAsDirty(kMainVirtScreen, 0, vs->w, 0, vs->h);
-
- // Set the current level
- _selectedLevel = levelId;
-
- // Initialize common player state
- _playerLives = 3;
- _playerShield = 255;
- _playerScore = 0;
- _playerDamage = 0;
- resetDamageFlash();
- _damageHighFlashCounter = 0;
- _damageShakeCounter = 0;
- _currentPhase = 1;
- _phaseScore = 0;
- _phaseMisses = 0;
-
- // Dispatch to per-level handler
- switch (levelId) {
- case 1:
- return runLevel1();
- case 2:
- return runLevel2();
- case 3:
- return runLevel3();
- case 4:
- return runLevel4();
- case 5:
- return runLevel5();
- case 6:
- return runLevel6();
- case 7:
- return runLevel7();
- case 8:
- return runLevel8();
- case 9:
- return runLevel9();
- case 10:
- return runLevel10();
- case 11:
- return runLevel11();
- case 12:
- return runLevel12();
- case 13:
- return runLevel13();
- case 14:
- return runLevel14();
- case 15:
- return runLevel15();
- default:
- return runLevel1();
- }
-}
-
-// =============================================================================
-// Helper functions
-// =============================================================================
-
-int InsaneRebel2::getRandomVariant(int max) {
- // Emulates FUN_004233a0 - returns random number 0 to max-1
- return _vm->_rnd.getRandomNumber(max - 1);
-}
-
-Common::String InsaneRebel2::selectDeathVideoVariant(int levelId, int phase, int frame) {
- // Select death video variant based on level, phase, and death frame
- // Emulates the frame-based death video selection in retail level handlers
- //
- // Returns variant suffix: "A", "B", "C", etc.
-
- switch (levelId) {
- case 1:
- // Level 1: Random between A and B
- return (getRandomVariant(2) == 0) ? "A" : "B";
-
- case 2:
- // Level 2: Just "DIE" (no variants)
- return "";
-
- case 3:
- // Level 3: Based on death frame and phase
- if (phase == 1) {
- // Phase 1 death video selection (from FUN_0041885F lines 80-96)
- if (frame < 0x10c) return "B"; // < 268
- if (frame < 0x1a9) return "A"; // < 425
- if (frame < 0x247) return "C"; // < 583
- if (frame < 700) return "A";
- if (frame < 900) return "B";
- return "A";
- } else {
- // Phase 2 death video selection (from FUN_0041885F lines 53-67)
- if (frame < 0x2f1) return "A"; // < 753
- if (frame < 0x347) return "B"; // < 839
- if (frame < 0x3b1) return "C"; // < 945
- if (frame < 0x405) return "A"; // < 1029
- return "C";
- }
-
- case 4:
- // Level 4: Just "DIE" (no variants)
- return "";
-
- case 5:
- // Level 5: Random between A and B (like Level 1)
- return (getRandomVariant(2) == 0) ? "A" : "B";
-
- case 6:
- // Level 6 (FUN_004190D6): Phase-based with detailed frame selection
- if (phase == 1) {
- // DAT_0047a7f8 == 5 (phase 1)
- if (frame < 0x4e) return "D";
- if (frame < 0xe0) return "A";
- if (frame < 0x122) return "D";
- if (frame < 0x1b4) return "B";
- if (frame < 499) return "D";
- if (frame < 0x286) return "C";
- return "D";
- } else {
- // DAT_0047a7f8 == 6 (phase 2)
- if (frame < 0xcc) return "E";
- if (frame < 0xfe) return "G";
- if (frame < 0x122) return "E";
- if (frame < 0x149) return "G";
- if (frame < 0x166) return "F";
- if (frame < 0x174) return "E";
- if (frame < 0x19f) return "F";
- if (frame < 0x1b2) return "G";
- if (frame < 0x1c8) return "F";
- if (frame < 0x207) return "E";
- if (frame < 0x217) return "F";
- if (frame < 0x23b) return "G";
- if (frame < 0x25b) return "F";
- if (frame < 0x285) return "E";
- return "G";
- }
-
- case 7:
- // Level 7 (FUN_0041974C): Based on DAT_0047ab8c (fork state)
- // DAT_0047ab8c != 0 â DIE_B; == 0 â DIE_A
- // We use phase as a proxy (phase 2 = reached fork)
- return (phase >= 2) ? "B" : "A";
-
- case 8:
- // Level 8 (FUN_00419976): Random A or B
- return (getRandomVariant(2) == 0) ? "A" : "B";
-
- case 9:
- // Level 9 (FUN_00419B86): Based on DAT_0047ab94 (death cause)
- // 0âA, 1âC, elseâB. Use phase as proxy.
- return "A"; // Default; exact tracking of DAT_0047ab94 deferred
-
- case 10:
- // Level 10 (FUN_00419E0A): Single death video (no variant suffix)
- return "";
-
- case 11:
- // Level 11 (FUN_0041A00C): Phase-based death videos
- // Phase 1 â DIE_A, Phase 2 â DIE_B, Phase 3 â DIE_C
- if (phase <= 1) return "A";
- if (phase == 2) return "B";
- return "C";
-
- case 12:
- // Level 12 (FUN_0041AA14): Single death video (no variants)
- return "";
-
- case 13:
- // Level 13 (FUN_0041B3E1): Frame-based
- if (frame < 0x1c2) return "A";
- if (frame < 0x302) return "B";
- if (frame < 0x4ec) return "C";
- if (frame < 0x5b4) return "B";
- return "D";
-
- case 14:
- // Level 14 (FUN_0041B6E8): Single death video (no variant suffix)
- return "";
-
- case 15:
- // Level 15 (FUN_0041B8D7): Frame-based with many thresholds
- if (frame < 0x21e) return "A";
- if (frame < 0x2f9) return "B";
- if (frame < 0x3e5) return "C";
- if (frame < 0x4a0) return "B";
- if (frame < 0x588) return "C";
- if (frame < 0x65e) return "B";
- return "D";
-
- default:
- return "A";
- }
-}
-
-void InsaneRebel2::playLevelDeathVariant(int levelId, int phase, int frame) {
- // Play death video with proper variant selection
-
- _rebelHandler = 0;
- _rebelStatusBarSprite = 0; // No status bar during death cinematic
-
- Common::String dir = getLevelDir(levelId);
- Common::String prefix = getLevelPrefix(levelId);
- Common::String variant = selectDeathVideoVariant(levelId, phase, frame);
- Common::String filename;
-
- if (variant.empty()) {
- // No variant suffix (Level 2, 4)
- filename = Common::String::format("%s/%sDIE.SAN", dir.c_str(), prefix.c_str());
- } else {
- filename = Common::String::format("%s/%sDIE_%s.SAN", dir.c_str(), prefix.c_str(), variant.c_str());
- }
-
- debug("Rebel2: Playing death video: %s (phase=%d, frame=%d)", filename.c_str(), phase, frame);
-
- SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
- // Original: FUN_00417168 adds | 8, so flags = 0x20 | 0x08 = 0x28
- splayer->setCurVideoFlags(0x28);
- splayer->play(filename.c_str(), 12);
-}
-
-void InsaneRebel2::playLevelRetryVariant(int levelId, int phase) {
- // Play retry video - phase-specific for multi-phase levels
-
- _rebelHandler = 0;
- _rebelStatusBarSprite = 0; // Reset for retry - will be set by IACT opcode 6 if needed
-
- Common::String dir = getLevelDir(levelId);
- Common::String prefix = getLevelPrefix(levelId);
- Common::String filename;
-
- if ((levelId == 3 || levelId == 6) && phase == 2) {
- // Level 3/6 phase 2 has its own retry video: xxRETRYB.SAN
- filename = Common::String::format("%s/%sRETRYB.SAN", dir.c_str(), prefix.c_str());
- } else {
- // Standard retry video
- filename = Common::String::format("%s/%sRETRY.SAN", dir.c_str(), prefix.c_str());
- }
-
- debug("Rebel2: Playing retry video: %s (phase=%d)", filename.c_str(), phase);
-
- SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
- // Original: FUN_00417168 adds | 8, so flags = 0x20 | 0x08 = 0x28
- splayer->setCurVideoFlags(0x28);
- splayer->play(filename.c_str(), 12);
-}
-
-// =============================================================================
-// Level 1 Handler - FUN_00417E53
-// Single gameplay phase (01P01.SAN)
-// =============================================================================
-
-int InsaneRebel2::runLevel1() {
- SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
-
- // Play level beginning cinematic (01BEG.SAN)
- playLevelBegin(1);
- if (_vm->shouldQuit()) return kLevelQuit;
-
- // Main gameplay retry loop
- while (!_vm->shouldQuit()) {
- // Reset shield for this attempt
- _playerShield = 255;
- _playerDamage = 0;
- _deathFrame = 0;
-
- // Reset bit table before gameplay starts - FUN_00423880 calls FUN_00423a00(0)
- // This ensures all enemies are visible (not skipped by SKIP chunks)
- clearBit(0);
-
- // Play gameplay (01P01.SAN with 0x28 flags)
- splayer->setCurVideoFlags(0x28);
- splayer->play("LEV01/01P01.SAN", 12);
-
- // Store death frame for video selection
- _deathFrame = splayer->_frame;
-
- if (_vm->shouldQuit()) return kLevelQuit;
-
- if (_playerShield > 0) {
- // Level completed!
- debug("Rebel2: Level 1 completed!");
- playLevelEnd(1);
- _levelUnlocked[1] = true; // Unlock level 2
- return kLevelNextLevel;
- }
-
- // Player died - play death video with random A/B variant
- debug("Rebel2: Level 1 death at frame %d, lives=%d", _deathFrame, _playerLives - 1);
- playLevelDeathVariant(1, 1, _deathFrame);
-
- if (_vm->shouldQuit()) return kLevelQuit;
-
- _playerLives--;
- if (_playerLives <= 0) {
- playLevelGameOver(1);
- return kLevelGameOver;
- }
-
- // Play retry prompt and loop
- playLevelRetry(1);
- if (_vm->shouldQuit()) return kLevelQuit;
- }
-
- return kLevelQuit;
-}
-
-// =============================================================================
-// Wave State Management - FUN_00417b61
-// Waits for video completion, accumulates kill state, redistributes kill credits.
-// Used by all multi-wave levels (Level 2, 3, 6, etc.) as the core wave loop primitive.
-// =============================================================================
-
-uint16 InsaneRebel2::processWaveEnd(int16 mask, int16 *budget, int16 threshold, uint16 flags) {
- // FUN_00417b61: Core wave management function
- // Called after each wave video plays. Handles:
- // 1. Waiting for video to finish (with early exit on enemy completion)
- // 2. Copying wave state to accumulated phase state
- // 3. Redistributing kill credits from the budget
- //
- // Returns: kill bits credited this wave, or 0xFFFF on death/quit/completion
-
- uint16 result = 0;
-
- // Step 1: Wait for video to finish (lines 21-32)
- // Original loop: while (damage < 0xff && frame < maxFrame-1 && !escPressed)
- // The SmushPlayer::play() call already blocks until video ends, so this step
- // is handled implicitly. The early-exit logic (threshold > 0: if frame > 50
- // AND all required enemy type bits are set, count up and break when > threshold)
- // would need per-frame callbacks to work precisely. For now, the primary effect
- // is covered by the video playing to completion and accumulating state.
- // TODO: Implement per-frame early exit callback for threshold-based wave termination.
-
- // Step 2: Copy wave state to phase state (line 33)
- // DAT_0047ab9c = DAT_0047ab98
- _rebelPhaseState = _rebelWaveState;
- debug("Rebel2 processWaveEnd: waveState=0x%x -> phaseState=0x%x mask=0x%x budget=%d threshold=%d flags=%d",
- _rebelWaveState, _rebelPhaseState, mask, budget ? *budget : -1, threshold, flags);
-
- // Step 3: Kill redistribution - add random unkilled types (lines 34-47)
- // Only when (flags & 2) != 0. Level 2 always passes flags=0, so inactive for Level 2.
- if ((flags & 2) != 0) {
- // Collect unkilled enemy type bits that are within the mask
- byte unkilled[8];
- int16 numUnkilled = 0;
- for (byte b = 0; (2 << (b & 0x1f)) < (int)(mask & 0x0e); b++) {
- if ((_rebelPhaseState & (2 << (b & 0x1f))) == 0) {
- unkilled[numUnkilled] = (byte)(2 << (b & 0x1f));
- numUnkilled++;
- }
- }
- if (numUnkilled > 0) {
- // Randomly add one unkilled type to phase state
- int idx = _vm->_rnd.getRandomNumber(numUnkilled - 1);
- _rebelPhaseState |= unkilled[idx];
- if (budget) (*budget)++;
- }
- }
-
- // Step 4: Kill credit transfer (lines 48-73)
- // Collect all SET enemy type bits from phase state
- byte killed[8];
- int16 numKilled = 0;
- for (byte b = 0; (2 << (b & 0x1f)) < (int)(mask & 0x0e); b++) {
- if ((_rebelPhaseState & (2 << (b & 0x1f))) != 0) {
- killed[numKilled] = (byte)(2 << (b & 0x1f));
- numKilled++;
- }
- }
-
- // Max credits: 8 normally, 2 if flag bit 0 set
- int16 maxCredits = ((flags & 1) == 0) ? 8 : 2;
-
- // Transfer kills from phase state to result, limited by budget
- int16 creditCount = 0;
- while (creditCount < maxCredits && numKilled > 0 && budget && *budget > 0) {
- int idx = _vm->_rnd.getRandomNumber(numKilled - 1);
- _rebelPhaseState -= killed[idx]; // Remove from accumulated state
- result |= killed[idx]; // Credit to return value
- (*budget)--;
-
- // Remove from array (shift remaining elements)
- for (int i = idx; i + 1 < numKilled; i++) {
- killed[i] = killed[i + 1];
- }
- numKilled--;
- creditCount++;
- }
-
- debug("Rebel2 processWaveEnd: result=0x%x phaseState=0x%x (after redistribution) budget=%d",
- result, _rebelPhaseState, budget ? *budget : -1);
-
- // Step 5: Return value (lines 74-78)
- // Return 0xFFFF if: dead, phase complete, or quit
- if (_playerDamage >= 255 || (int16)_rebelPhaseState >= mask || _vm->shouldQuit()) {
- return 0xFFFF;
- }
- return result;
-}
-
-// =============================================================================
-// Level 2 Handler - FUN_00418063
-// Multiple parts with P1/P2/P3 subdirectories
-// Random animation variants for each part
-// =============================================================================
-
-int InsaneRebel2::runLevel2() {
- // FUN_00418063: Level 2 "Corellia Star" - Third-person on-foot shooting
- // Three phases, each with looping enemy waves until all enemy types killed.
- // Phase completion: (_rebelPhaseState & mask) == mask
- // Phase 1 mask: 0x06 (enemy types 1,2)
- // Phase 2 mask: 0x0e (enemy types 1,2,3)
- // Phase 3 mask: 0x0e (enemy types 1,2,3)
- //
- // Kill credit budget (from level data table DAT_0047e0e8):
- // Each phase gets a budget = tableBase + random(3). processWaveEnd() uses
- // this budget to randomly redistribute kill credits, creating non-deterministic
- // wave progression. Using calibrated defaults until exact table values extracted.
- static const int16 kLevel2BudgetBase[3] = { 3, 3, 3 }; // Phase 1, 2, 3
-
- SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
- int bonusCount = 0; // local_1c: tracks bonus events (DAT_0047ab9c & 0x10)
- int totalKills = 0; // local_c: accumulated kill count across phases
- int totalMisses = 0; // Accumulated misses (sVar1 + sVar2 from hit counters)
- int prevWaveState = 0; // local_8: previous wave's state for Phase 3 randomization
-
- // Play cutscene (02CUT.SAN)
- playCinematic("LEV02/02CUT.SAN");
- if (_vm->shouldQuit()) return kLevelQuit;
-
- // Play level beginning cinematic (02BEG.SAN)
- // Original: FUN_004171c5("LEV02/02BEG.SAN", 0x20, 0xab, 0xa0, 10, 2, 0x46)
- // Includes text overlay from GAME.TRS â deferred until text rendering is ready.
- playLevelBegin(2);
- if (_vm->shouldQuit()) return kLevelQuit;
-
- // FUN_00401000 + FUN_00407d10 + FUN_0040c040: Reset game state (before retry loop)
- clearBit(0);
-
- // Main gameplay retry loop (restarts from beginning on death)
- while (!_vm->shouldQuit()) {
- _playerShield = 255;
- _playerDamage = 0;
- _currentPhase = 1;
- bonusCount = 0;
- totalKills = 0;
- totalMisses = 0;
-
- // Reset Handler 25 cover state â player starts uncovered at level start
- // DAT_00457904 and DAT_0045790a are zero-initialized globals in the original
- _rebelAutopilot = 0;
- _rebelDamageLevel = 0;
- _rebelControlMode = 0;
-
- // FUN_0041c7d0: Reset per-attempt state
- _enemies.clear();
- for (int i = 0; i < 512; i++) {
- _rebelLinks[i][0] = 0;
- _rebelLinks[i][1] = 0;
- _rebelLinks[i][2] = 0;
- }
-
- // ===== PHASE 1: P1/02P01_X.SAN =====
- // FUN_0041c7d0: Reset per-phase counters
- _rebelKillCounter = 0;
- _rebelHitCounter = 0;
- _rebelPhaseState = 0;
- _rebelWaveState = 0;
-
- // Initialize kill budget from level data table + random(3)
- // Original: sVar4 = levelData[phase1Offset]; local_14[0] = sVar4 + random(3)
- int16 budget = kLevel2BudgetBase[0] + _vm->_rnd.getRandomNumber(2);
-
- // Play A.SAN (background loader) â flags 0x28 (preserve buffer, gameplay mode)
- debug("Rebel2: Level 2 Phase 1 - playing 02P01_A.SAN (background) budget=%d", budget);
- splayer->setCurVideoFlags(0x28);
- splayer->play("LEV02/P1/02P01_A.SAN", 12);
- _deathFrame = splayer->_frame;
-
- if (_vm->shouldQuit()) return kLevelQuit;
-
- // processWaveEnd after A.SAN (threshold=0, no early exit for background loader)
- processWaveEnd(0x36, &budget, 0, 0);
-
- // Phase 1 wave loop: random B/C/D until all type 1,2 enemies killed
- // Original: while (uVar3 >= 0 && (DAT_0047ab9c & 6) != 6)
- while (_playerDamage < 255 && (_rebelPhaseState & 0x06) != 0x06) {
- if (_vm->shouldQuit()) return kLevelQuit;
-
- // Random variant B, C, or D
- int variant = _vm->_rnd.getRandomNumber(2); // 0-2
- const char *variants[] = {
- "LEV02/P1/02P01_B.SAN",
- "LEV02/P1/02P01_C.SAN",
- "LEV02/P1/02P01_D.SAN"
- };
- debug("Rebel2: Phase 1 wave - playing %s (state=0x%x budget=%d)", variants[variant], _rebelPhaseState, budget);
- // Wave videos use flags 0x428 (original: FUN_0041f4d0 param_2=0x428)
- splayer->setCurVideoFlags(0x428);
- splayer->play(variants[variant], 12);
- _deathFrame = splayer->_frame;
-
- // processWaveEnd with threshold=0x14 (20) â enables early exit when enemies killed
- processWaveEnd(0x36, &budget, 0x14, 0);
- debug("Rebel2: Phase 1 wave done - state=0x%x (need 0x06) budget=%d", _rebelPhaseState, budget);
- }
-
- // Check for bonus (bit 4 = 0x10)
- if ((_rebelPhaseState & 0x10) != 0) bonusCount++;
-
- if (_playerDamage >= 255) goto level2_death;
- if (_vm->shouldQuit()) return kLevelQuit;
-
- // Post segment 1 (02PST1.SAN)
- // Original: FUN_00417168("02PST1.SAN", 0x20) â flags = 0x20 | 0x08 = 0x28
- // FUN_00417168 adds | 8 to preserve the screen buffer between gameplay and transition
- // Reset handler to 0 so procPostRendering skips HUD/sprite drawing during cinematic
- _rebelHandler = 0;
- _rebelStatusBarSprite = 0;
- splayer->setCurVideoFlags(0x28);
- splayer->play("LEV02/02PST1.SAN", 12);
- if (_vm->shouldQuit()) return kLevelQuit;
-
- totalKills += _rebelKillCounter;
- totalMisses += _rebelHitCounter;
-
- // ===== PHASE 2: P2/02P02_X.SAN =====
- _currentPhase = 2;
- _rebelKillCounter = 0;
- _rebelHitCounter = 0;
- _rebelPhaseState = 0;
- _rebelWaveState = 0;
- _enemies.clear();
-
- // Initialize Phase 2 budget
- budget = kLevel2BudgetBase[1] + _vm->_rnd.getRandomNumber(2);
-
- // Restore handler for gameplay â will be confirmed by IACT opcode 6
- _rebelHandler = 8;
-
- // Play A.SAN (background loader)
- debug("Rebel2: Level 2 Phase 2 - playing 02P02_A.SAN (background) budget=%d", budget);
- splayer->setCurVideoFlags(0x28);
- splayer->play("LEV02/P2/02P02_A.SAN", 12);
- _deathFrame = splayer->_frame;
-
- if (_vm->shouldQuit()) return kLevelQuit;
-
- // Phase 2 wave loop: processWaveEnd at TOP of loop (matches assembly structure)
- // Original: local_10 = FUN_00417b61(0x3e, local_14, 0, 0); then switch(local_10)
- while (true) {
- uint16 waveSelect = processWaveEnd(0x3e, &budget, 0, 0);
- if (waveSelect == 0xFFFF || (_rebelPhaseState & 0x0e) == 0x0e) break;
- if (_vm->shouldQuit()) return kLevelQuit;
-
- // If no specific pattern: randomize high bits (original lines 71-74)
- // When (local_10 & 0xc) == 0: add random 0x10/0x11/0x12
- if ((waveSelect & 0x0c) == 0) {
- waveSelect = _vm->_rnd.getRandomNumber(2) + 0x10;
- }
-
- // Variant selection matching original switch (FUN_418063 lines 75-96)
- const char *filename;
- switch (waveSelect) {
- case 4: case 6:
- filename = "LEV02/P2/02P02_B.SAN"; break;
- case 8: case 10:
- filename = "LEV02/P2/02P02_C.SAN"; break;
- case 0x0c: case 0x0e:
- filename = "LEV02/P2/02P02_A.SAN"; break;
- case 0x11:
- filename = "LEV02/P2/02P02_E.SAN"; break;
- case 0x12:
- filename = "LEV02/P2/02P02_F.SAN"; break;
- default:
- filename = "LEV02/P2/02P02_D.SAN"; break;
- }
-
- debug("Rebel2: Phase 2 wave - playing %s (state=0x%x sel=0x%x budget=%d)", filename, _rebelPhaseState, waveSelect, budget);
- splayer->setCurVideoFlags(0x428);
- splayer->play(filename, 12);
- _deathFrame = splayer->_frame;
- }
-
- if ((_rebelPhaseState & 0x10) != 0) bonusCount++;
-
- if (_playerDamage >= 255) goto level2_death;
- if (_vm->shouldQuit()) return kLevelQuit;
-
- // Post segment 2 (02PST2.SAN)
- // Original: FUN_00417168("02PST2.SAN", 0x20) â flags = 0x20 | 0x08 = 0x28
- // Reset handler to 0 so procPostRendering skips HUD/sprite drawing during cinematic
- _rebelHandler = 0;
- _rebelStatusBarSprite = 0;
- splayer->setCurVideoFlags(0x28);
- splayer->play("LEV02/02PST2.SAN", 12);
- if (_vm->shouldQuit()) return kLevelQuit;
-
- totalKills += _rebelKillCounter;
- totalMisses += _rebelHitCounter;
-
- // ===== PHASE 3: P3/02P03_X.SAN =====
- _currentPhase = 3;
- _rebelKillCounter = 0;
- _rebelHitCounter = 0;
- _rebelPhaseState = 0;
- _rebelWaveState = 0;
- _enemies.clear();
- prevWaveState = 0;
-
- // Initialize Phase 3 budget
- budget = kLevel2BudgetBase[2] + _vm->_rnd.getRandomNumber(2);
-
- // Restore handler for gameplay â will be confirmed by IACT opcode 6
- _rebelHandler = 8;
-
- // Play A.SAN (background loader)
- debug("Rebel2: Level 2 Phase 3 - playing 02P03_A.SAN (background) budget=%d", budget);
- splayer->setCurVideoFlags(0x28);
- splayer->play("LEV02/P3/02P03_A.SAN", 12);
- _deathFrame = splayer->_frame;
-
- if (_vm->shouldQuit()) return kLevelQuit;
-
- // Phase 3: processWaveEnd at BOTTOM (like Phase 1), waveSelect carried across iterations
- // Original: local_10 = FUN_00417b61(0x3e, local_14, 0, 0); while (loop) { ...; local_10 = FUN_00417b61(0x3e, local_14, 0x14, 0); }
- {
- uint16 waveSelect = processWaveEnd(0x3e, &budget, 0, 0);
-
- while (waveSelect != 0xFFFF && (_rebelPhaseState & 0x0e) != 0x0e) {
- if (_vm->shouldQuit()) return kLevelQuit;
-
- // Phase 3 randomization (original lines 113-115):
- // If previous wave state bit 0 was clear AND random(8)==0, set bit 0
- if (((prevWaveState & 1) == 0) && (_vm->_rnd.getRandomNumber(7) == 0)) {
- waveSelect |= 1;
- }
- prevWaveState = waveSelect;
-
- // Variant selection matching original switch (FUN_418063 lines 117-144)
- const char *filename;
- switch (waveSelect) {
- case 0:
- filename = "LEV02/P3/02P03_H.SAN"; break;
- case 2:
- filename = "LEV02/P3/02P03_G.SAN"; break;
- case 4:
- filename = "LEV02/P3/02P03_F.SAN"; break;
- case 6:
- filename = "LEV02/P3/02P03_E.SAN"; break;
- case 8:
- filename = "LEV02/P3/02P03_D.SAN"; break;
- case 10:
- filename = "LEV02/P3/02P03_C.SAN"; break;
- case 0x0c:
- filename = "LEV02/P3/02P03_B.SAN"; break;
- case 0x0e:
- filename = "LEV02/P3/02P03_A.SAN"; break;
- default:
- filename = "LEV02/P3/02P03_I.SAN"; break;
- }
-
- debug("Rebel2: Phase 3 wave - playing %s (state=0x%x sel=0x%x budget=%d)", filename, _rebelPhaseState, waveSelect, budget);
- splayer->setCurVideoFlags(0x428);
- splayer->play(filename, 12);
- _deathFrame = splayer->_frame;
-
- // processWaveEnd at BOTTOM with threshold=0x14
- waveSelect = processWaveEnd(0x3e, &budget, 0x14, 0);
- debug("Rebel2: Phase 3 wave done - state=0x%x (need 0x0e) budget=%d", _rebelPhaseState, budget);
- }
- }
-
- if ((_rebelPhaseState & 0x10) != 0) bonusCount++;
- totalKills += _rebelKillCounter;
-
- if (_playerDamage >= 255) goto level2_death;
- if (_vm->shouldQuit()) return kLevelQuit;
-
- // Level completed! Calculate accuracy score.
- // Original: FUN_00417327 with score thresholds and medal ranks
- // Score presentation deferred until GAME.TRS text rendering is implemented.
- {
- totalMisses += _rebelHitCounter;
- int accuracy = 0;
- int totalShots = totalKills + totalMisses;
- if (totalKills > 0 && totalShots > 0) {
- accuracy = (totalKills * 100) / totalShots;
- }
- debug("Rebel2: Level 2 completed! kills=%d misses=%d accuracy=%d%% bonus=%d",
- totalKills, totalMisses, accuracy, bonusCount);
- }
-
- playLevelEnd(2);
- _levelUnlocked[2] = true; // Unlock level 3
- return kLevelNextLevel;
-
- level2_death:
- // Player died â play death sequence and retry or game over
- // Original: FUN_00417168("LEV02/02DIE.SAN", 0x20)
- debug("Rebel2: Level 2 Phase %d death", _currentPhase);
- playCinematic("LEV02/02DIE.SAN");
- if (_vm->shouldQuit()) return kLevelQuit;
-
- // Original: if (DAT_0047ab5c != 0) DAT_0047a7ee++ (bonus life award)
- // DAT_0047ab5c is set when player earns a bonus life (e.g., score threshold).
- // Currently not tracked â will be wired when bonus life system is implemented.
- _playerLives--;
- if (_playerLives <= 0) {
- // Original: FUN_00417ab2("LEV02/02OVER.SAN", 0x20, 2)
- playLevelGameOver(2);
- return kLevelGameOver;
- }
- playCinematic("LEV02/02RETRY.SAN");
- _playerDamage = 0;
- if (_vm->shouldQuit()) return kLevelQuit;
- continue; // Restart from beginning
- }
-
- return kLevelQuit;
-}
-
-// =============================================================================
-// Level 3 Handler - FUN_0041885F
-// Two phases with per-phase retry handling
-// Phase 1: 03PLAY1.SAN, Phase 2: 03PLAY2.SAN
-// =============================================================================
-
-int InsaneRebel2::runLevel3() {
- SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
- int phase1Score = 0; // Score preserved across phase 2 retries
-
- // Play level beginning cinematic (03BEG.SAN)
- playLevelBegin(3);
- if (_vm->shouldQuit()) return kLevelQuit;
-
- // ===== PHASE 1 retry loop =====
- while (!_vm->shouldQuit()) {
- _playerShield = 255;
- _playerDamage = 0;
- _currentPhase = 1;
-
- // Reset bit table before gameplay starts - FUN_00423880 calls FUN_00423a00(0)
- clearBit(0);
-
- // Play phase 1 gameplay (03PLAY1.SAN)
- debug("Rebel2: Level 3 Phase 1");
- splayer->setCurVideoFlags(0x28);
- splayer->play("LEV03/03PLAY1.SAN", 12);
- _deathFrame = splayer->_frame;
-
- if (_vm->shouldQuit()) return kLevelQuit;
-
- if (_playerShield > 0) {
- // Phase 1 completed - save score and proceed to phase 2
- phase1Score = _playerScore;
- break;
- }
-
- // Died in phase 1 - frame-based death video
- debug("Rebel2: Level 3 Phase 1 death at frame %d", _deathFrame);
- playLevelDeathVariant(3, 1, _deathFrame);
- if (_vm->shouldQuit()) return kLevelQuit;
-
- _playerLives--;
- if (_playerLives <= 0) {
- playLevelGameOver(3);
- return kLevelGameOver;
- }
-
- // Phase 1 retry (03RETRY.SAN)
- playLevelRetryVariant(3, 1);
- if (_vm->shouldQuit()) return kLevelQuit;
- }
-
- if (_vm->shouldQuit()) return kLevelQuit;
-
- // Post segment 1 (03POST1.SAN)
- // Original: FUN_00417168 adds | 8, so flags = 0x20 | 0x08 = 0x28
- // Reset handler to 0 so procPostRendering skips HUD/sprite drawing during cinematic
- _rebelHandler = 0;
- _rebelStatusBarSprite = 0;
- splayer->setCurVideoFlags(0x28);
- splayer->play("LEV03/03POST1.SAN", 12);
- if (_vm->shouldQuit()) return kLevelQuit;
-
- // ===== PHASE 2 retry loop (preserves phase 1 score) =====
- _currentPhase = 2;
-
- while (!_vm->shouldQuit()) {
- _playerShield = 255;
- _playerDamage = 0;
-
- // Reset bit table before gameplay starts
- clearBit(0);
-
- // Play phase 2 gameplay (03PLAY2.SAN)
- debug("Rebel2: Level 3 Phase 2");
- splayer->setCurVideoFlags(0x28);
- splayer->play("LEV03/03PLAY2.SAN", 12);
- _deathFrame = splayer->_frame;
-
- if (_vm->shouldQuit()) return kLevelQuit;
-
- if (_playerShield > 0) {
- // Level completed!
- debug("Rebel2: Level 3 completed!");
- playLevelEnd(3);
- _levelUnlocked[3] = true; // Unlock level 4
- return kLevelNextLevel;
- }
-
- // Died in phase 2 - frame-based death video
- debug("Rebel2: Level 3 Phase 2 death at frame %d", _deathFrame);
- playLevelDeathVariant(3, 2, _deathFrame);
- if (_vm->shouldQuit()) return kLevelQuit;
-
- _playerLives--;
- if (_playerLives <= 0) {
- // Use phase 2 specific game over (03OVER.SAN, same file but at different point)
- playLevelGameOver(3);
- return kLevelGameOver;
- }
-
- // Phase 2 retry (03RETRYB.SAN)
- playLevelRetryVariant(3, 2);
- if (_vm->shouldQuit()) return kLevelQuit;
- }
-
- return kLevelQuit;
-}
-
-// =============================================================================
-// Level 4 Handler
-// Cutscene + single gameplay phase
-// =============================================================================
-
-int InsaneRebel2::runLevel4() {
- SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
-
- // Play cutscene (04CUT.SAN)
- // Original: FUN_00417168 adds | 8, so flags = 0x20 | 0x08 = 0x28
- splayer->setCurVideoFlags(0x28);
- splayer->play("LEV04/04CUT.SAN", 12);
- if (_vm->shouldQuit()) return kLevelQuit;
-
- // Play level beginning cinematic (04BEG.SAN)
- playLevelBegin(4);
- if (_vm->shouldQuit()) return kLevelQuit;
-
- // Main gameplay retry loop
- while (!_vm->shouldQuit()) {
- _playerShield = 255;
- _playerDamage = 0;
- _currentPhase = 1;
-
- // Reset bit table before gameplay starts
- clearBit(0);
-
- // Play gameplay (04PLAY.SAN)
- debug("Rebel2: Level 4 gameplay");
- splayer->setCurVideoFlags(0x28);
- splayer->play("LEV04/04PLAY.SAN", 12);
- _deathFrame = splayer->_frame;
-
- if (_vm->shouldQuit()) return kLevelQuit;
-
- if (_playerShield > 0) {
- // Level completed!
- debug("Rebel2: Level 4 completed!");
- playLevelEnd(4);
- _levelUnlocked[4] = true; // Unlock level 5
- return kLevelNextLevel;
- }
-
- // Died - play death video (04DIE.SAN, no variants)
- debug("Rebel2: Level 4 death");
- playLevelDeathVariant(4, 1, _deathFrame);
- if (_vm->shouldQuit()) return kLevelQuit;
-
- _playerLives--;
- if (_playerLives <= 0) {
- playLevelGameOver(4);
- return kLevelGameOver;
- }
-
- playLevelRetry(4);
- if (_vm->shouldQuit()) return kLevelQuit;
- }
-
- return kLevelQuit;
-}
-
-// =============================================================================
-// Level 5 Handler - FUN_00418EC6
-// Single gameplay phase (05PLAY.SAN)
-// Random A/B death video like Level 1
-// =============================================================================
-
-int InsaneRebel2::runLevel5() {
- SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
-
- // Play level beginning cinematic (05BEG.SAN)
- playLevelBegin(5);
- if (_vm->shouldQuit()) return kLevelQuit;
-
- // Main gameplay retry loop
- while (!_vm->shouldQuit()) {
- _playerShield = 255;
- _playerDamage = 0;
- _currentPhase = 1;
-
- // Reset bit table before gameplay starts
- clearBit(0);
-
- // Play gameplay (05PLAY.SAN)
- debug("Rebel2: Level 5 gameplay");
- splayer->setCurVideoFlags(0x28);
- splayer->play("LEV05/05PLAY.SAN", 12);
- _deathFrame = splayer->_frame;
-
- if (_vm->shouldQuit()) return kLevelQuit;
-
- if (_playerShield > 0) {
- // Level completed!
- debug("Rebel2: Level 5 completed!");
- playLevelEnd(5);
- _levelUnlocked[5] = true; // Unlock level 6
- return kLevelNextLevel;
- }
-
- // Died - play death video with random A/B variant
- debug("Rebel2: Level 5 death at frame %d", _deathFrame);
- playLevelDeathVariant(5, 1, _deathFrame);
- if (_vm->shouldQuit()) return kLevelQuit;
-
- _playerLives--;
- if (_playerLives <= 0) {
- playLevelGameOver(5);
- return kLevelGameOver;
- }
-
- playLevelRetry(5);
- if (_vm->shouldQuit()) return kLevelQuit;
- }
-
- return kLevelQuit;
-}
-
-// =============================================================================
-// Level 6 Handler - FUN_00419317
-// Two phases with per-phase retry (like Level 3)
-// Phase 1: 06PLAY1.SAN, Phase 2: 06PLAY2.SAN
-// =============================================================================
-
-int InsaneRebel2::runLevel6() {
- // FUN_004190d6 â Mos Eisley: two-phase on-foot (Handler 8)
- // Phase 1 (levelId=5): 06PLAY1.SAN, mid-switch to 06PLAY1B.SAN at frame 0x2a8
- // Phase 2 (levelId=6): 06PLAY2.SAN, play until near-end
- // Original structure: outer do-while for phase 1 retries, inner while(true) for
- // phase 2 retries + death handling. Phase 1 death breaks inner â RETRY at outer bottom.
- // Phase 2 death â RETRYB â re-enters phase 2 within inner loop.
- SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
- int totalScore = 0;
-
- // Play level beginning cinematic (06BEG.SAN)
- // Original: FUN_004171c5(s_LEV06_06BEG_SAN, 0x20, 0xaf, 0xa0, 10, 5, 0x4b)
- playLevelBegin(6);
- if (_vm->shouldQuit()) return kLevelQuit;
-
- // FUN_00401000 + FUN_0041c7d0 + FUN_0040c040 â handler init done by IACT opcode 6
-
- // Outer retry loop â restarts phase 1 on phase 1 death
- while (!_vm->shouldQuit()) {
- // FUN_00407d10 â reset shot/hit counters
- clearBit(0);
-
- // DAT_0047ab9c = 0xffffffff â init phase state
- _rebelPhaseState = 0xffffffff;
-
- // ===== PHASE 1 =====
- _rebelLevelType = 5; // DAT_0047a7f8 = 5
- _currentPhase = 1;
-
- debug("Rebel2: Level 6 Phase 1");
- splayer->setCurVideoFlags(0x28);
- splayer->play("LEV06/06PLAY1.SAN", 12);
- // TODO: Mid-level switch at frame 0x2a8 to 06PLAY1B.SAN (flags 0x468)
- // + score checkpoint (FUN_00407f55) â needs per-frame callback
- _deathFrame = splayer->_frame;
-
- if (_vm->shouldQuit()) return kLevelQuit;
-
- if (_playerShield <= 0) {
- // Died in phase 1
- debug("Rebel2: Level 6 Phase 1 death at frame %d", _deathFrame);
- playLevelDeathVariant(6, 1, _deathFrame);
- if (_vm->shouldQuit()) return kLevelQuit;
-
- _playerLives--;
- if (_playerLives <= 0) {
- playLevelGameOver(6);
- return kLevelGameOver;
- }
-
- // Phase 1 retry (06RETRY.SAN) â restart outer loop
- playLevelRetryVariant(6, 1);
- if (_vm->shouldQuit()) return kLevelQuit;
- continue;
- }
-
- // Phase 1 survived â save score, play POST1
- totalScore = _playerScore; // local_8 = DAT_0047ab84
-
- _rebelHandler = 0;
- _rebelStatusBarSprite = 0;
- splayer->setCurVideoFlags(0x28);
- splayer->play("LEV06/06POST1.SAN", 12);
- if (_vm->shouldQuit()) return kLevelQuit;
-
- // ===== PHASE 2 retry loop (inner while(true) in original) =====
- while (!_vm->shouldQuit()) {
- _rebelLevelType = 6; // DAT_0047a7f8 = 6
- _currentPhase = 2;
- clearBit(0); // FUN_00407d10
-
- debug("Rebel2: Level 6 Phase 2");
- splayer->setCurVideoFlags(0x28);
- splayer->play("LEV06/06PLAY2.SAN", 12);
- _deathFrame = splayer->_frame;
-
- if (_vm->shouldQuit()) return kLevelQuit;
-
- // Accumulate score: local_8 = DAT_0047ab84 + local_8
- totalScore += _playerScore;
-
- if (_playerShield > 0) {
- // Level completed!
- debug("Rebel2: Level 6 completed!");
- playLevelEnd(6);
- _levelUnlocked[6] = true;
- return kLevelNextLevel;
- }
-
- // Died in phase 2
- debug("Rebel2: Level 6 Phase 2 death at frame %d", _deathFrame);
- playLevelDeathVariant(6, 2, _deathFrame);
- if (_vm->shouldQuit()) return kLevelQuit;
-
- _playerLives--;
- if (_playerLives <= 0) {
- playLevelGameOver(6);
- return kLevelGameOver;
- }
-
- // Phase 2 retry (06RETRYB.SAN) â re-enter phase 2
- playLevelRetryVariant(6, 2);
- if (_vm->shouldQuit()) return kLevelQuit;
- }
-
- break; // Should only reach here on shouldQuit
- }
-
- return kLevelQuit;
-}
-
-// =============================================================================
-// Level 7 Handler - FUN_0041974C
-// "TIE Training" - Canyon flight with fork at frame 1592
-// Single gameplay phase (07PLAY.SAN), optional second segment (07PLAYB.SAN)
-// Death: DAT_0047ab8c-based (fork reached â DIE_B, not reached â DIE_A)
-// =============================================================================
-
-int InsaneRebel2::runLevel7() {
- SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
- bool reachedFork = false; // DAT_0047ab8c equivalent â tracks if 07PLAYB was played
-
- // Play cutscene (07CUT.SAN)
- playCinematic("LEV07/07CUT.SAN");
- if (_vm->shouldQuit()) return kLevelQuit;
-
- // Play level beginning cinematic (07BEG.SAN)
- playLevelBegin(7);
- if (_vm->shouldQuit()) return kLevelQuit;
-
- // FUN_00401000 + FUN_0041c7d0 + FUN_00407d10
- clearBit(0);
-
- while (!_vm->shouldQuit()) {
- _playerShield = 255;
- _playerDamage = 0;
- _deathFrame = 0;
- reachedFork = false;
-
- clearBit(0);
-
- // Play gameplay (07PLAY.SAN)
- // Original: FUN_0041f4d0("07PLAY.SAN", 0x28, -1, -1, 0)
- // At frame 0x638 (1592), if DAT_0047ab8c != 0: play 07PLAYB.SAN (0x468)
- // TODO: Mid-level fork at frame 1592 requires per-frame callback.
- // For now, play the main video. The fork video (07PLAYB) would be triggered
- // by IACT callbacks setting a state flag during gameplay.
- splayer->setCurVideoFlags(0x28);
- splayer->play("LEV07/07PLAY.SAN", 12);
- _deathFrame = splayer->_frame;
-
- if (_vm->shouldQuit()) return kLevelQuit;
-
- if (_playerShield > 0) {
- debug("Rebel2: Level 7 completed!");
- playLevelEnd(7);
- _levelUnlocked[7] = true;
- return kLevelNextLevel;
- }
-
- // Death video: DIE_B if fork reached, DIE_A if not
- // Original: s_LEV07_07DIE_B + ((DAT_0047ab8c != 0) - 1 & 0x14)
- debug("Rebel2: Level 7 death at frame %d, fork=%d", _deathFrame, reachedFork);
- if (reachedFork) {
- playCinematic("LEV07/07DIE_B.SAN");
- } else {
- playCinematic("LEV07/07DIE_A.SAN");
- }
- if (_vm->shouldQuit()) return kLevelQuit;
-
- _playerLives--;
- if (_playerLives <= 0) {
- playLevelGameOver(7);
- return kLevelGameOver;
- }
-
- playCinematic("LEV07/07RETRY.SAN");
- if (_vm->shouldQuit()) return kLevelQuit;
- }
-
- return kLevelQuit;
-}
-
-// =============================================================================
-// Level 8 Handler - FUN_00419976
-// "Flight to Imdaar" - Y-Wing space battle (single phase)
-// No cutscene (starts with BEG). flags=0x08 for gameplay.
-// Death: random A or B
-// =============================================================================
-
-int InsaneRebel2::runLevel8() {
- SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
-
- // No cutscene â starts directly with BEG
- // Original: FUN_004171c5("08BEG.SAN", 0x20, 0xb1, 0xa0, 10, 5, 0x4b)
- playLevelBegin(8);
- if (_vm->shouldQuit()) return kLevelQuit;
-
- // FUN_00401000 + FUN_0041c7d0 + FUN_0040c040
- clearBit(0);
-
- while (!_vm->shouldQuit()) {
- _playerShield = 255;
- _playerDamage = 0;
- _deathFrame = 0;
-
- clearBit(0);
-
- // Play gameplay (08PLAY.SAN)
- // Original: FUN_0041f4d0("08PLAY.SAN", 8, -1, -1, 0) â note flags=0x08
- splayer->setCurVideoFlags(0x08);
- splayer->play("LEV08/08PLAY.SAN", 12);
- _deathFrame = splayer->_frame;
-
- if (_vm->shouldQuit()) return kLevelQuit;
-
- if (_playerShield > 0) {
- int accuracy = 0;
- if (_rebelKillCounter > 0) {
- accuracy = (_rebelKillCounter * 100) / (_rebelHitCounter + _rebelKillCounter);
- }
- debug("Rebel2: Level 8 completed! accuracy=%d%%", accuracy);
- playLevelEnd(8);
- _levelUnlocked[8] = true;
- return kLevelNextLevel;
- }
-
- // Death: random A or B
- // Original: random(2) â A or B via string pointer arithmetic
- debug("Rebel2: Level 8 death at frame %d", _deathFrame);
- playLevelDeathVariant(8, 1, _deathFrame);
- if (_vm->shouldQuit()) return kLevelQuit;
-
- _playerLives--;
- if (_playerLives <= 0) {
- playLevelGameOver(8);
- return kLevelGameOver;
- }
-
- playCinematic("LEV08/08RETRY.SAN");
- if (_vm->shouldQuit()) return kLevelQuit;
- }
-
- return kLevelQuit;
-}
-
-// =============================================================================
-// Level 9 Handler - FUN_00419B86
-// "The Mine Field" - Navigate through force fields (single phase)
-// No cutscene. Initial phaseState = 0xfffffffe (all bits set except bit 0).
-// Mid-events at frames 0x19f (415) and 0x352 (850): FUN_00407f55 (score checkpoint)
-// Death: DAT_0047ab94-based (0âA, 1âC, elseâB)
-// =============================================================================
-
-int InsaneRebel2::runLevel9() {
- SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
-
- // No cutscene â starts directly with BEG
- // Original: FUN_004171c5("09BEG.SAN", 0x20, 0xb2, 0xa0, 10, 200, 0x10e)
- playLevelBegin(9);
- if (_vm->shouldQuit()) return kLevelQuit;
-
- // FUN_00401000 + FUN_0041c7d0 + FUN_0040c040
- clearBit(0);
-
- while (!_vm->shouldQuit()) {
- _playerShield = 255;
- _playerDamage = 0;
- _deathFrame = 0;
-
- clearBit(0);
-
- // Original: DAT_0047ab9c = 0xfffffffe (initial phase state â all bits except 0)
- _rebelPhaseState = 0xfffffffe;
-
- // Play gameplay (09PLAY.SAN)
- // Original: FUN_0041f4d0("09PLAY.SAN", 0x28, -1, -1, 0)
- // Mid-events at frames 415 and 850: FUN_00407f55 (score save)
- // These are handled implicitly â the IACT callbacks manage scoring.
- splayer->setCurVideoFlags(0x28);
- splayer->play("LEV09/09PLAY.SAN", 12);
- _deathFrame = splayer->_frame;
-
- if (_vm->shouldQuit()) return kLevelQuit;
-
- if (_playerShield > 0) {
- int accuracy = 0;
- if (_rebelKillCounter > 0) {
- accuracy = (_rebelKillCounter * 100) / (_rebelHitCounter + _rebelKillCounter);
- }
- debug("Rebel2: Level 9 completed! accuracy=%d%%", accuracy);
- playLevelEnd(9);
- _levelUnlocked[9] = true;
- return kLevelNextLevel;
- }
-
- // Death: based on DAT_0047ab94 (death cause tracking)
- // Original: 0âDIE_A, 1âDIE_C, elseâDIE_B
- debug("Rebel2: Level 9 death at frame %d", _deathFrame);
- playLevelDeathVariant(9, 1, _deathFrame);
- if (_vm->shouldQuit()) return kLevelQuit;
-
- _playerLives--;
- if (_playerLives <= 0) {
- playLevelGameOver(9);
- return kLevelGameOver;
- }
-
- playCinematic("LEV09/09RETRY.SAN");
- if (_vm->shouldQuit()) return kLevelQuit;
- }
-
- return kLevelQuit;
-}
-
-// =============================================================================
-// Level 10 Handler - FUN_00419E0A
-// "Speeder Bikes" - Forest speeder chase (single phase)
-// Has cutscene. Single death video (10DIE.SAN, no variants).
-// Original plays DIE then RETRY in sequence (no separate check).
-// =============================================================================
-
-int InsaneRebel2::runLevel10() {
- SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
-
- // Play cutscene (10CUT.SAN)
- playCinematic("LEV10/10CUT.SAN");
- if (_vm->shouldQuit()) return kLevelQuit;
-
- // Play level beginning cinematic (10BEG.SAN)
- // Original: FUN_004171c5("10BEG.SAN", 0x20, 0xb3, 0xa0, 10, 2, 0x46)
- playLevelBegin(10);
- if (_vm->shouldQuit()) return kLevelQuit;
-
- // FUN_00401000 + FUN_0041c7d0 + FUN_00407d10
- clearBit(0);
-
- while (!_vm->shouldQuit()) {
- _playerShield = 255;
- _playerDamage = 0;
- _deathFrame = 0;
-
- clearBit(0);
-
- // Play gameplay (10PLAY.SAN)
- // Original: FUN_0041f4d0("10PLAY.SAN", 0x28, -1, -1, 0)
- splayer->setCurVideoFlags(0x28);
- splayer->play("LEV10/10PLAY.SAN", 12);
- _deathFrame = splayer->_frame;
-
- if (_vm->shouldQuit()) return kLevelQuit;
-
- if (_playerShield > 0) {
- int accuracy = 0;
- if (_rebelKillCounter > 0) {
- accuracy = (_rebelKillCounter * 100) / (_rebelHitCounter + _rebelKillCounter);
- }
- debug("Rebel2: Level 10 completed! accuracy=%d%%", accuracy);
- playLevelEnd(10);
- _levelUnlocked[10] = true;
- return kLevelNextLevel;
- }
-
- // Death + Retry: original plays both in sequence
- // Original: lives--, if 0 break to game over, else DIE+RETRY
- debug("Rebel2: Level 10 death at frame %d", _deathFrame);
- _playerLives--;
- if (_playerLives <= 0) {
- playLevelGameOver(10);
- return kLevelGameOver;
- }
-
- playCinematic("LEV10/10DIE.SAN");
- if (_vm->shouldQuit()) return kLevelQuit;
- playCinematic("LEV10/10RETRY.SAN");
- if (_vm->shouldQuit()) return kLevelQuit;
- }
-
- return kLevelQuit;
-}
-
-// =============================================================================
-// Level 11 Handler - FUN_0041A00C
-// "Inside the Terror" - Three phases + bridge puzzle (Handler 8, on-foot)
-//
-// Phase 1: P1/11P01_X (A,B,C,D) - behind barrels, mask 0x0e
-// Phase 2: P2/11P02_X (A,B,C,D) - walls on right, mask 0x0e, flags=3
-// Phase 3 first half: P3/11P03_X (A-F) - bridge puzzle, mask 0x7e
-// Exit when (phaseState & 0x70) == 0x70
-// POST3/POST3B/POST3C bridge cinematics
-// Phase 3 second half: P3/11P03_X (G-L) - after bridge, mask 0x0e
-// =============================================================================
-
-int InsaneRebel2::runLevel11() {
- SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
- int totalKills = 0;
- int totalMisses = 0;
- int prevPhaseState = 0;
-
- // Kill credit budget bases per phase (from level data table DAT_0047e0e8)
- static const int16 kLevel11BudgetBase[4] = { 3, 3, 3, 3 };
-
- // Play cutscene (11CUT.SAN)
- playCinematic("LEV11/11CUT.SAN");
- if (_vm->shouldQuit()) return kLevelQuit;
-
- // Play level beginning cinematic (11BEG.SAN)
- playLevelBegin(11);
- if (_vm->shouldQuit()) return kLevelQuit;
-
- // FUN_00401000 + FUN_00407d10 + FUN_0040c040: Reset game state
- clearBit(0);
-
- // Main gameplay retry loop (restarts from Phase 1 on death)
- while (!_vm->shouldQuit()) {
- _playerShield = 255;
- _playerDamage = 0;
- _currentPhase = 1;
- totalKills = 0;
- totalMisses = 0;
- prevPhaseState = 0;
-
- // Reset Handler 8 cover state
- _rebelAutopilot = 0;
- _rebelDamageLevel = 0;
- _rebelControlMode = 0;
-
- // FUN_0041c7d0: Reset per-attempt state
- _enemies.clear();
- for (int i = 0; i < 512; i++) {
- _rebelLinks[i][0] = 0;
- _rebelLinks[i][1] = 0;
- _rebelLinks[i][2] = 0;
- }
-
- // ===== PHASE 1: P1/11P01_X.SAN =====
- _rebelKillCounter = 0;
- _rebelHitCounter = 0;
- _rebelPhaseState = 0;
- _rebelWaveState = 0;
-
- int16 budget = kLevel11BudgetBase[0] + _vm->_rnd.getRandomNumber(2);
-
- // Play A.SAN (background loader)
- debug("Rebel2: Level 11 Phase 1 - playing 11P01_A.SAN budget=%d", budget);
- splayer->setCurVideoFlags(0x28);
- splayer->play("LEV11/P1/11P01_A.SAN", 12);
- _deathFrame = splayer->_frame;
-
- if (_vm->shouldQuit()) return kLevelQuit;
-
- {
- uint16 waveSelect = processWaveEnd(0x0e, &budget, 0, 0);
-
- // Phase 1 wave loop: random(2) | (waveSelect & 8) â variants
- // 0âD, 1âC, 8âB, 9âA
- while (waveSelect != 0xFFFF) {
- if (_vm->shouldQuit()) return kLevelQuit;
-
- // Bonus sound check
- if ((_rebelPhaseState & 0x10) != 0 && (prevPhaseState & 0x10) == 0) {
- // FUN_00411931 bonus sound â not yet implemented
- }
- prevPhaseState = _rebelPhaseState;
-
- int sel = _vm->_rnd.getRandomNumber(1) | (waveSelect & 8);
- const char *filename;
- switch (sel) {
- case 1: filename = "LEV11/P1/11P01_C.SAN"; break;
- case 8: filename = "LEV11/P1/11P01_B.SAN"; break;
- case 9: filename = "LEV11/P1/11P01_A.SAN"; break;
- default: filename = "LEV11/P1/11P01_D.SAN"; break; // sel == 0
- }
-
- debug("Rebel2: Level 11 Phase 1 wave - %s (state=0x%x sel=%d)", filename, _rebelPhaseState, sel);
- splayer->setCurVideoFlags(0x428);
- splayer->play(filename, 12);
- _deathFrame = splayer->_frame;
-
- waveSelect = processWaveEnd(0x0e, &budget, 0x14, 0);
- }
- }
-
- if (_playerDamage >= 255) goto level11_death_phase1;
- if (_vm->shouldQuit()) return kLevelQuit;
-
- // Post segment 1 (11POST1.SAN)
- _rebelHandler = 0;
- _rebelStatusBarSprite = 0;
- splayer->setCurVideoFlags(0x28);
- splayer->play("LEV11/11POST1.SAN", 12);
- if (_vm->shouldQuit()) return kLevelQuit;
-
- totalKills += _rebelKillCounter;
- totalMisses += _rebelHitCounter;
-
- // ===== PHASE 2: P2/11P02_X.SAN =====
- _currentPhase = 2;
- _rebelKillCounter = 0;
- _rebelHitCounter = 0;
- _rebelPhaseState = 0;
- _rebelWaveState = 0;
- _enemies.clear();
-
- budget = kLevel11BudgetBase[1] + _vm->_rnd.getRandomNumber(2);
- _rebelHandler = 8;
-
- // Play A.SAN (background loader)
- debug("Rebel2: Level 11 Phase 2 - playing 11P02_A.SAN budget=%d", budget);
- splayer->setCurVideoFlags(0x28);
- splayer->play("LEV11/P2/11P02_A.SAN", 12);
- _deathFrame = splayer->_frame;
-
- if (_vm->shouldQuit()) return kLevelQuit;
-
- {
- // Phase 2: flags=3 (maxCredits=2, redistribution ON)
- uint16 waveSelect = processWaveEnd(0x0e, &budget, 0, 3);
-
- // Random(4) for variant selection: A, B, C, D
- while (waveSelect != 0xFFFF) {
- if (_vm->shouldQuit()) return kLevelQuit;
-
- int variant = _vm->_rnd.getRandomNumber(3);
- const char *filename;
- switch (variant) {
- case 0: filename = "LEV11/P2/11P02_A.SAN"; break;
- case 1: filename = "LEV11/P2/11P02_B.SAN"; break;
- case 2: filename = "LEV11/P2/11P02_C.SAN"; break;
- default: filename = "LEV11/P2/11P02_D.SAN"; break;
- }
-
- debug("Rebel2: Level 11 Phase 2 wave - %s (state=0x%x)", filename, _rebelPhaseState);
- splayer->setCurVideoFlags(0x428);
- splayer->play(filename, 12);
- _deathFrame = splayer->_frame;
-
- waveSelect = processWaveEnd(0x0e, &budget, 0x14, 3);
- }
- }
-
- if (_playerDamage >= 255) goto level11_death_phase2;
- if (_vm->shouldQuit()) return kLevelQuit;
-
- // Post segment 2 (11POST2.SAN)
- _rebelHandler = 0;
- _rebelStatusBarSprite = 0;
- splayer->setCurVideoFlags(0x28);
- splayer->play("LEV11/11POST2.SAN", 12);
- if (_vm->shouldQuit()) return kLevelQuit;
-
- totalKills += _rebelKillCounter;
- totalMisses += _rebelHitCounter;
-
- // ===== PHASE 3 FIRST HALF: P3/11P03_X (A-F) =====
- // Bridge puzzle â exit when (phaseState & 0x70) == 0x70
- _currentPhase = 3;
- _rebelKillCounter = 0;
- _rebelHitCounter = 0;
- _rebelPhaseState = 0;
- _rebelWaveState = 0;
- _enemies.clear();
- prevPhaseState = 0;
-
- budget = kLevel11BudgetBase[2] + _vm->_rnd.getRandomNumber(2);
- _rebelHandler = 8;
-
- debug("Rebel2: Level 11 Phase 3 first half - playing 11P03_A.SAN budget=%d", budget);
- splayer->setCurVideoFlags(0x28);
- splayer->play("LEV11/P3/11P03_A.SAN", 12);
- _deathFrame = splayer->_frame;
-
- if (_vm->shouldQuit()) return kLevelQuit;
-
- {
- uint16 waveSelect = processWaveEnd(0x7e, &budget, 0, 0);
- int local_8 = 0; // Tracks variant for randomization threshold
-
- // Loop until (phaseState & 0x70) == 0x70 (bridge targets destroyed)
- while (waveSelect != 0xFFFF && (_rebelPhaseState & 0x70) != 0x70) {
- if (_vm->shouldQuit()) return kLevelQuit;
-
- // Bonus sound: (phaseState & 0xe) == 0xe and previous wasn't
- if ((_rebelPhaseState & 0x0e) == 0x0e && (prevPhaseState & 0x0e) != 0x0e) {
- // FUN_00411931 bonus sound â not yet implemented
- }
- prevPhaseState = _rebelPhaseState;
-
- // Randomization: wider range for first few waves
- if (local_8 < 3) {
- local_8 = _vm->_rnd.getRandomNumber(7); // 0-7
- } else {
- local_8 = _vm->_rnd.getRandomNumber(2); // 0-2
- }
-
- const char *filename;
- switch (local_8) {
- case 0: filename = "LEV11/P3/11P03_A.SAN"; break;
- case 1: filename = "LEV11/P3/11P03_B.SAN"; break;
- case 2: filename = "LEV11/P3/11P03_C.SAN"; break;
- case 3: filename = "LEV11/P3/11P03_D.SAN"; break;
- case 4: filename = "LEV11/P3/11P03_E.SAN"; break;
- case 5: filename = "LEV11/P3/11P03_F.SAN"; break;
- case 6: filename = "LEV11/P3/11P03_F.SAN"; break; // duplicate F
- default: filename = "LEV11/P3/11P03_E.SAN"; break; // duplicate E
- }
-
- debug("Rebel2: Level 11 Phase 3a wave - %s (state=0x%x local_8=%d)", filename, _rebelPhaseState, local_8);
- splayer->setCurVideoFlags(0x428);
- splayer->play(filename, 12);
- _deathFrame = splayer->_frame;
-
- // Threshold only for higher variants (original: (2 < local_8) - 1 & 0x14)
- int16 threshold = (local_8 > 2) ? 0x14 : 0;
- waveSelect = processWaveEnd(0x7e, &budget, threshold, 0);
- }
- }
-
- if (_playerDamage >= 255) goto level11_death_phase3;
- if (_vm->shouldQuit()) return kLevelQuit;
-
- // ===== PHASE 3 BRIDGE CINEMATICS =====
- {
- bool allBasicKilled = (_rebelPhaseState & 0x0e) >= 0x0e;
- if (!allBasicKilled) {
- // Normal bridge drop cinematic
- playCinematic("LEV11/11POST3.SAN");
- // Bonus checks (FUN_0042aa70) â deferred, play standard path
- // Original checks 0x77 and 0x62 for special POST3C cinematic
- } else {
- // All enemy types killed â bridge dropped successfully
- playCinematic("LEV11/11POST3B.SAN");
- }
- }
-
- if (_vm->shouldQuit()) return kLevelQuit;
-
- // ===== PHASE 3 SECOND HALF: P3/11P03_X (G-L) =====
- // Reset shots/explosions (FUN_0041ca6a equivalent)
- for (int i = 0; i < 5; i++) {
- _explosions[i].active = false;
- }
- _enemies.clear();
-
- // Preserve only bits 1-3 of phase state (original: DAT_0047ab9c &= 0xe)
- _rebelPhaseState &= 0x0e;
- _rebelWaveState &= 0x0e;
-
- _rebelHandler = 8;
-
- budget = kLevel11BudgetBase[3] + _vm->_rnd.getRandomNumber(2);
-
- // Play G.SAN (background loader for second half)
- debug("Rebel2: Level 11 Phase 3 second half - playing 11P03_G.SAN budget=%d", budget);
- splayer->setCurVideoFlags(0x28);
- splayer->play("LEV11/P3/11P03_G.SAN", 12);
- _deathFrame = splayer->_frame;
-
- if (_vm->shouldQuit()) return kLevelQuit;
-
- // Only enter wave loop if not all basic types killed already
- if ((_rebelPhaseState & 0x0e) < 0x0e) {
- int local_8 = 0;
- uint16 waveSelect = processWaveEnd(0x0e, &budget, 0, 0);
-
- while (waveSelect != 0xFFFF) {
- if (_vm->shouldQuit()) return kLevelQuit;
-
- // Wider randomization for first few waves
- if (local_8 < 4) {
- local_8 = _vm->_rnd.getRandomNumber(8); // 0-8
- } else {
- local_8 = _vm->_rnd.getRandomNumber(2); // 0-2
- }
-
- const char *filename;
- switch (local_8) {
- case 0: filename = "LEV11/P3/11P03_G.SAN"; break;
- case 1: filename = "LEV11/P3/11P03_H.SAN"; break;
- case 2: filename = "LEV11/P3/11P03_I.SAN"; break;
- case 3: filename = "LEV11/P3/11P03_G.SAN"; break; // G again
- case 4: filename = "LEV11/P3/11P03_H.SAN"; break; // H again
- case 5: filename = "LEV11/P3/11P03_I.SAN"; break; // I again
- case 6: filename = "LEV11/P3/11P03_J.SAN"; break;
- case 7: filename = "LEV11/P3/11P03_K.SAN"; break;
- default: filename = "LEV11/P3/11P03_L.SAN"; break;
- }
-
- debug("Rebel2: Level 11 Phase 3b wave - %s (state=0x%x local_8=%d)", filename, _rebelPhaseState, local_8);
- splayer->setCurVideoFlags(0x428);
- splayer->play(filename, 12);
- _deathFrame = splayer->_frame;
-
- int16 threshold = (local_8 > 2) ? 0x14 : 0;
- waveSelect = processWaveEnd(0x0e, &budget, threshold, 0);
- }
- }
-
- totalKills += _rebelKillCounter;
-
- if (_playerDamage >= 255) goto level11_death_phase3;
- if (_vm->shouldQuit()) return kLevelQuit;
-
- // ===== LEVEL COMPLETED =====
- {
- totalMisses += _rebelHitCounter;
- int accuracy = 0;
- int totalShots = totalKills + totalMisses;
- if (totalKills > 0 && totalShots > 0) {
- accuracy = (totalKills * 100) / totalShots;
- }
- debug("Rebel2: Level 11 completed! kills=%d misses=%d accuracy=%d%%",
- totalKills, totalMisses, accuracy);
- }
-
- playLevelEnd(11);
- _levelUnlocked[11] = true; // Unlock level 12
- return kLevelNextLevel;
-
- level11_death_phase1:
- debug("Rebel2: Level 11 Phase 1 death");
- playCinematic("LEV11/11DIE_A.SAN");
- goto level11_retry;
-
- level11_death_phase2:
- debug("Rebel2: Level 11 Phase 2 death");
- playCinematic("LEV11/11DIE_B.SAN");
- goto level11_retry;
-
- level11_death_phase3:
- debug("Rebel2: Level 11 Phase 3 death");
- playCinematic("LEV11/11DIE_C.SAN");
- goto level11_retry;
-
- level11_retry:
- if (_vm->shouldQuit()) return kLevelQuit;
- _playerLives--;
- if (_playerLives <= 0) {
- playLevelGameOver(11);
- return kLevelGameOver;
- }
- playCinematic("LEV11/11RETRY.SAN");
- _playerDamage = 0;
- if (_vm->shouldQuit()) return kLevelQuit;
- continue; // Restart from Phase 1
- }
-
- return kLevelQuit;
-}
-
-// =============================================================================
-// Level 12 Handler - FUN_0041AA14
-// "Sewers" - Four phases FPS corridor shooting (Handler 25)
-//
-// Each phase: init video (P05/P06/P07/P08) â first wave â wave loop
-// Phase 1: P1/12P01_X (A,B,C,D) mask=6
-// Phase 2: P2/12P02_X (A,B,C,D,E,F) mask=6
-// Phase 3: P3/12P03_X (A,B,C,D,F) mask=6
-// Phase 4: P4/12P04_X (A,B,C,D,E,F) mask=0xe
-// Closing: 12P09.SAN
-// =============================================================================
-
-int InsaneRebel2::runLevel12() {
- SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
-
- // Kill credit budget bases per phase
- static const int16 kLevel12BudgetBase[4] = { 3, 4, 4, 4 };
-
- // Play cutscene (12CUT.SAN)
- playCinematic("LEV12/12CUT.SAN");
- if (_vm->shouldQuit()) return kLevelQuit;
-
- // Play level beginning cinematic (12BEG.SAN)
- playLevelBegin(12);
- if (_vm->shouldQuit()) return kLevelQuit;
-
- // FUN_0041c7d0 + FUN_00407d10 + FUN_0040c040: Reset game state
- clearBit(0);
-
- // Main gameplay retry loop (restarts from Phase 1 on death)
- while (!_vm->shouldQuit()) {
- _playerShield = 255;
- _playerDamage = 0;
- _currentPhase = 1;
-
- // Reset state
- _rebelAutopilot = 0;
- _rebelDamageLevel = 0;
- _rebelControlMode = 0;
-
- _enemies.clear();
- for (int i = 0; i < 512; i++) {
- _rebelLinks[i][0] = 0;
- _rebelLinks[i][1] = 0;
- _rebelLinks[i][2] = 0;
- }
-
- // ===== PHASE 1: 12P05 â P1/12P01_X =====
- // FUN_00401000: Reset at top of each retry
- _rebelKillCounter = 0;
- _rebelHitCounter = 0;
- _rebelPhaseState = 0;
- _rebelWaveState = 0;
-
- int16 budget = kLevel12BudgetBase[0] + _vm->_rnd.getRandomNumber(2);
-
- // Initialization video (12P05.SAN)
- debug("Rebel2: Level 12 Phase 1 - init 12P05.SAN budget=%d", budget);
- splayer->setCurVideoFlags(0x28);
- splayer->play("LEV12/12P05.SAN", 12);
- if (_vm->shouldQuit()) return kLevelQuit;
- processWaveEnd(1, &budget, 0, 0);
-
- // First wave (P1/12P01_A.SAN)
- splayer->setCurVideoFlags(0x428);
- splayer->play("LEV12/P1/12P01_A.SAN", 12);
- _deathFrame = splayer->_frame;
- if (_vm->shouldQuit()) return kLevelQuit;
-
- {
- uint16 waveSelect = processWaveEnd(6, &budget, 0x14, 0);
-
- // Wave loop: random(2) | (waveSelect & 2) â 0:C, 1:D, 2:A, 3:B
- while (waveSelect != 0xFFFF) {
- if (_vm->shouldQuit()) return kLevelQuit;
-
- int sel = _vm->_rnd.getRandomNumber(1) | (waveSelect & 2);
- const char *filename;
- switch (sel) {
- case 0: filename = "LEV12/P1/12P01_C.SAN"; break;
- case 1: filename = "LEV12/P1/12P01_D.SAN"; break;
- case 2: filename = "LEV12/P1/12P01_A.SAN"; break;
- default: filename = "LEV12/P1/12P01_B.SAN"; break;
- }
-
- debug("Rebel2: Level 12 Phase 1 wave - %s (state=0x%x sel=%d)", filename, _rebelPhaseState, sel);
- splayer->setCurVideoFlags(0x428);
- splayer->play(filename, 12);
- _deathFrame = splayer->_frame;
-
- waveSelect = processWaveEnd(6, &budget, 0x14, 0);
- }
- }
-
- if (_playerDamage >= 255) goto level12_death;
- if (_vm->shouldQuit()) return kLevelQuit;
-
- // ===== PHASE 2: 12P06 â P2/12P02_X =====
- _currentPhase = 2;
- _rebelPhaseState = 0;
- _rebelWaveState = 0;
-
- budget = kLevel12BudgetBase[1] + _vm->_rnd.getRandomNumber(3);
-
- // Initialization video (12P06.SAN)
- debug("Rebel2: Level 12 Phase 2 - init 12P06.SAN budget=%d", budget);
- splayer->setCurVideoFlags(0x428);
- splayer->play("LEV12/12P06.SAN", 12);
- if (_vm->shouldQuit()) return kLevelQuit;
- processWaveEnd(1, &budget, 0, 0);
-
- // First wave (P2/12P02_A.SAN)
- splayer->setCurVideoFlags(0x428);
- splayer->play("LEV12/P2/12P02_A.SAN", 12);
- _deathFrame = splayer->_frame;
- if (_vm->shouldQuit()) return kLevelQuit;
-
- {
- uint16 waveSelect = processWaveEnd(6, &budget, 0x14, 0);
-
- while (waveSelect != 0xFFFF) {
- if (_vm->shouldQuit()) return kLevelQuit;
-
- // Variant selection: (waveSelect & 2) controls which set
- int local_8;
- if ((waveSelect & 2) == 0) {
- local_8 = _vm->_rnd.getRandomNumber(2) + 3; // 3, 4, or 5
- } else {
- local_8 = _vm->_rnd.getRandomNumber(2); // 0, 1, or 2
- }
-
- const char *filename;
- switch (local_8) {
- case 0: filename = "LEV12/P2/12P02_A.SAN"; break;
- case 1: filename = "LEV12/P2/12P02_B.SAN"; break;
- case 2: filename = "LEV12/P2/12P02_E.SAN"; break;
- case 3: filename = "LEV12/P2/12P02_C.SAN"; break;
- case 4: filename = "LEV12/P2/12P02_D.SAN"; break;
- default: filename = "LEV12/P2/12P02_F.SAN"; break;
- }
-
- debug("Rebel2: Level 12 Phase 2 wave - %s (state=0x%x local_8=%d)", filename, _rebelPhaseState, local_8);
- splayer->setCurVideoFlags(0x428);
- splayer->play(filename, 12);
- _deathFrame = splayer->_frame;
-
- // Variants E(2) and F(5) reset threshold to 0
- int16 threshold = (local_8 == 2 || local_8 == 5) ? 0 : 0x14;
- waveSelect = processWaveEnd(6, &budget, threshold, 0);
- }
- }
-
- if (_playerDamage >= 255) goto level12_death;
- if (_vm->shouldQuit()) return kLevelQuit;
-
- // ===== PHASE 3: 12P07 â P3/12P03_X =====
- _currentPhase = 3;
- _rebelPhaseState = 0;
- _rebelWaveState = 0;
-
- budget = kLevel12BudgetBase[2] + _vm->_rnd.getRandomNumber(3);
-
- // Initialization video (12P07.SAN)
- debug("Rebel2: Level 12 Phase 3 - init 12P07.SAN budget=%d", budget);
- splayer->setCurVideoFlags(0x428);
- splayer->play("LEV12/12P07.SAN", 12);
- if (_vm->shouldQuit()) return kLevelQuit;
- processWaveEnd(1, &budget, 0, 0);
-
- // First wave (P3/12P03_A.SAN)
- splayer->setCurVideoFlags(0x428);
- splayer->play("LEV12/P3/12P03_A.SAN", 12);
- _deathFrame = splayer->_frame;
- if (_vm->shouldQuit()) return kLevelQuit;
-
- {
- int local_8 = 0;
- uint16 waveSelect = processWaveEnd(6, &budget, 0x14, 0);
-
- while (waveSelect != 0xFFFF) {
- if (_vm->shouldQuit()) return kLevelQuit;
-
- // Wider randomization for first few waves
- if (local_8 < 4) {
- local_8 = _vm->_rnd.getRandomNumber(5); // 0-5
- } else {
- local_8 = _vm->_rnd.getRandomNumber(3); // 0-3
- }
-
- const char *filename;
- switch (local_8) {
- case 0: filename = "LEV12/P3/12P03_C.SAN"; break;
- case 1: filename = "LEV12/P3/12P03_D.SAN"; break;
- case 2: filename = "LEV12/P3/12P03_A.SAN"; break;
- case 3: filename = "LEV12/P3/12P03_B.SAN"; break;
- case 4: filename = "LEV12/P3/12P03_F.SAN"; break;
- default: filename = "LEV12/P3/12P03_F.SAN"; break; // duplicate F
- }
-
- debug("Rebel2: Level 12 Phase 3 wave - %s (state=0x%x local_8=%d)", filename, _rebelPhaseState, local_8);
- splayer->setCurVideoFlags(0x428);
- splayer->play(filename, 12);
- _deathFrame = splayer->_frame;
-
- waveSelect = processWaveEnd(6, &budget, 0x14, 0);
- }
- }
-
- if (_playerDamage >= 255) goto level12_death;
- if (_vm->shouldQuit()) return kLevelQuit;
-
- // ===== PHASE 4: 12P08 â P4/12P04_X =====
- _currentPhase = 4;
- _rebelPhaseState = 0;
- _rebelWaveState = 0;
-
- budget = kLevel12BudgetBase[3] + _vm->_rnd.getRandomNumber(3);
-
- // Initialization video (12P08.SAN)
- debug("Rebel2: Level 12 Phase 4 - init 12P08.SAN budget=%d", budget);
- splayer->setCurVideoFlags(0x428);
- splayer->play("LEV12/12P08.SAN", 12);
- if (_vm->shouldQuit()) return kLevelQuit;
- processWaveEnd(1, &budget, 0, 0);
-
- // First wave (P4/12P04_A.SAN)
- splayer->setCurVideoFlags(0x428);
- splayer->play("LEV12/P4/12P04_A.SAN", 12);
- _deathFrame = splayer->_frame;
- if (_vm->shouldQuit()) return kLevelQuit;
-
- {
- int local_8 = 0;
- uint16 waveSelect = processWaveEnd(0x0e, &budget, 0x14, 0);
-
- while (waveSelect != 0xFFFF) {
- if (_vm->shouldQuit()) return kLevelQuit;
-
- if (local_8 < 4) {
- local_8 = _vm->_rnd.getRandomNumber(5); // 0-5
- } else {
- local_8 = _vm->_rnd.getRandomNumber(3); // 0-3
- }
-
- const char *filename;
- switch (local_8) {
- case 0: filename = "LEV12/P4/12P04_C.SAN"; break;
- case 1: filename = "LEV12/P4/12P04_D.SAN"; break;
- case 2: filename = "LEV12/P4/12P04_A.SAN"; break;
- case 3: filename = "LEV12/P4/12P04_B.SAN"; break;
- case 4: filename = "LEV12/P4/12P04_E.SAN"; break;
- default: filename = "LEV12/P4/12P04_F.SAN"; break;
- }
-
- debug("Rebel2: Level 12 Phase 4 wave - %s (state=0x%x local_8=%d)", filename, _rebelPhaseState, local_8);
- splayer->setCurVideoFlags(0x428);
- splayer->play(filename, 12);
- _deathFrame = splayer->_frame;
-
- waveSelect = processWaveEnd(0x0e, &budget, 0x14, 0);
- }
- }
-
- if (_playerDamage >= 255) goto level12_death;
- if (_vm->shouldQuit()) return kLevelQuit;
-
- // ===== CLOSING: 12P09.SAN =====
- splayer->setCurVideoFlags(0x428);
- splayer->play("LEV12/12P09.SAN", 12);
- if (_vm->shouldQuit()) return kLevelQuit;
- processWaveEnd(1, &budget, 0, 0);
-
- // ===== LEVEL COMPLETED =====
- {
- int accuracy = 0;
- if (_rebelKillCounter > 0) {
- accuracy = (_rebelKillCounter * 100) / (_rebelHitCounter + _rebelKillCounter);
- }
- debug("Rebel2: Level 12 completed! kills=%d misses=%d accuracy=%d%%",
- _rebelKillCounter, _rebelHitCounter, accuracy);
- }
-
- // Bonus checks: FUN_0042aa70(0x61), FUN_0042aa70(99), FUN_0042aa70(0x74)
- // If all three bonuses found, play special ending (12END_Z.SAN)
- // Deferred until bonus tracking is implemented
-
- playLevelEnd(12);
- _levelUnlocked[12] = true; // Unlock level 13
- return kLevelNextLevel;
-
- level12_death:
- // Single death video for all phases
- debug("Rebel2: Level 12 Phase %d death", _currentPhase);
- playCinematic("LEV12/12DIE.SAN");
-
- if (_vm->shouldQuit()) return kLevelQuit;
- _playerLives--;
- if (_playerLives <= 0) {
- playLevelGameOver(12);
- return kLevelGameOver;
- }
- playCinematic("LEV12/12RETRY.SAN");
- _playerDamage = 0;
- if (_vm->shouldQuit()) return kLevelQuit;
- continue; // Restart from Phase 1
- }
-
- return kLevelQuit;
-}
-
-// =============================================================================
-// Level 13 Handler - FUN_0041B3E1
-// "Escaping the Star Destroyer" - Two-phase flight/escape
-// Phase A: 13PLAY_A.SAN (main flight), transitions to Phase B at maxFrame-10
-// Phase B: 13PLAY_B.SAN (reactor loop, flags 0x468) â plays until
-// (DAT_0047ab90 == 0 && DAT_0047ab7c == 0) meaning all targets destroyed.
-// Death: frame-based (A/B/C/B/D pattern)
-// =============================================================================
-
-int InsaneRebel2::runLevel13() {
- SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
-
- // No cutscene â starts directly with BEG
- // Original: FUN_004171c5("13BEG.SAN", 0x20, 0xb6, 0xa0, 10, 2, 0x46)
- playLevelBegin(13);
- if (_vm->shouldQuit()) return kLevelQuit;
-
- // FUN_00401000 + FUN_0041c7d0 + FUN_0040c040
- clearBit(0);
-
- while (!_vm->shouldQuit()) {
- _playerShield = 255;
- _playerDamage = 0;
- _deathFrame = 0;
-
- clearBit(0);
-
- // Phase A: Main escape flight (13PLAY_A.SAN)
- // Original: FUN_0041f4d0("13PLAY_A.SAN", 0x28, -1, -1, 0)
- // First inner loop runs until frame reaches maxFrame-10
- // Then Phase B (13PLAY_B.SAN, flags 0x468) plays at that exact frame
- // The 0x468 flags indicate seamless mid-video transition.
- splayer->setCurVideoFlags(0x28);
- splayer->play("LEV13/13PLAY_A.SAN", 12);
- _deathFrame = splayer->_frame;
-
- if (_vm->shouldQuit()) return kLevelQuit;
-
- // If alive after Phase A, play Phase B (reactor destruction loop)
- // Original: at frame == maxFrame-10, play 13PLAY_B.SAN (0x468)
- // Then loop while (DAT_0047ab90 != 0 || DAT_0047ab7c != 0)
- // For now, play B as a sequential video. The IACT callbacks will manage
- // the reactor target state through opcode interactions.
- if (_playerShield > 0) {
- splayer->setCurVideoFlags(0x468);
- splayer->play("LEV13/13PLAY_B.SAN", 12);
- _deathFrame = splayer->_frame;
- }
-
- if (_vm->shouldQuit()) return kLevelQuit;
-
- if (_playerShield > 0) {
- int accuracy = 0;
- if (_rebelKillCounter > 0) {
- accuracy = (_rebelKillCounter * 100) / (_rebelHitCounter + _rebelKillCounter);
- }
- debug("Rebel2: Level 13 completed! accuracy=%d%%", accuracy);
- playLevelEnd(13);
- _levelUnlocked[13] = true;
- return kLevelNextLevel;
- }
-
- // Death: frame-based variant selection (FUN_0041B3E1 lines 47-61)
- debug("Rebel2: Level 13 death at frame %d", _deathFrame);
- playLevelDeathVariant(13, 1, _deathFrame);
- if (_vm->shouldQuit()) return kLevelQuit;
-
- _playerLives--;
- if (_playerLives <= 0) {
- playLevelGameOver(13);
- return kLevelGameOver;
- }
-
- playCinematic("LEV13/13RETRY.SAN");
- if (_vm->shouldQuit()) return kLevelQuit;
- }
-
- return kLevelQuit;
-}
-
-// =============================================================================
-// Level 14 Handler - FUN_0041B6E8
-// "TIE Attack" - Final space battle (single phase)
-// No cutscene. Single death video (14DIE.SAN, no variants).
-// =============================================================================
-
-int InsaneRebel2::runLevel14() {
- SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
-
- // No cutscene â starts directly with BEG
- // Original: FUN_004171c5("14BEG.SAN", 0x20, 0xb7, 0xa0, 10, 2, 0x46)
- playLevelBegin(14);
- if (_vm->shouldQuit()) return kLevelQuit;
-
- // FUN_00401000 + FUN_0041c7d0 + FUN_0040c040
- clearBit(0);
-
- while (!_vm->shouldQuit()) {
- _playerShield = 255;
- _playerDamage = 0;
- _deathFrame = 0;
-
- clearBit(0);
-
- // Play gameplay (14PLAY.SAN)
- // Original: FUN_0041f4d0("14PLAY.SAN", 0x28, -1, -1, 0)
- splayer->setCurVideoFlags(0x28);
- splayer->play("LEV14/14PLAY.SAN", 12);
- _deathFrame = splayer->_frame;
-
- if (_vm->shouldQuit()) return kLevelQuit;
-
- if (_playerShield > 0) {
- int accuracy = 0;
- if (_rebelKillCounter > 0) {
- accuracy = (_rebelKillCounter * 100) / (_rebelHitCounter + _rebelKillCounter);
- }
- debug("Rebel2: Level 14 completed! accuracy=%d%%", accuracy);
- playLevelEnd(14);
- _levelUnlocked[14] = true;
- return kLevelNextLevel;
- }
-
- // Death: single video (14DIE.SAN)
- debug("Rebel2: Level 14 death at frame %d", _deathFrame);
- playCinematic("LEV14/14DIE.SAN");
- if (_vm->shouldQuit()) return kLevelQuit;
-
- _playerLives--;
- if (_playerLives <= 0) {
- playLevelGameOver(14);
- return kLevelGameOver;
- }
-
- playCinematic("LEV14/14RETRY.SAN");
- if (_vm->shouldQuit()) return kLevelQuit;
- }
-
- return kLevelQuit;
-}
-
-// =============================================================================
-// Level 15 Handler - FUN_0041B8D7
-// "Imdaar Alpha" - Final mission (single long phase with level ID switch)
-// Has cutscene. Mid-level: DAT_0047a7f8 changes from 0xf to 0x10 at frame 0x21e.
-// This represents a transition from the tunnel section to the core section.
-// Death: frame-based (A/B/C/B/C/B/D pattern with 7 thresholds)
-// On completion â FUN_0041BBE8 (credits/end game, not a playable level)
-// =============================================================================
-
-int InsaneRebel2::runLevel15() {
- SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
-
- // Play cutscene (15CUT.SAN)
- playCinematic("LEV15/15CUT.SAN");
- if (_vm->shouldQuit()) return kLevelQuit;
-
- // Play level beginning cinematic (15BEG.SAN)
- // Original: FUN_004171c5("15BEG.SAN", 0x20, 0xb8, 0xa0, 10, 2, 0x46)
- playLevelBegin(15);
- if (_vm->shouldQuit()) return kLevelQuit;
-
- // FUN_00401000 + FUN_0041c7d0 + FUN_0040c040
- clearBit(0);
-
- while (!_vm->shouldQuit()) {
- _playerShield = 255;
- _playerDamage = 0;
- _deathFrame = 0;
-
- clearBit(0);
-
- // Original: DAT_0047a7f8 = 0xf (level 15) before gameplay
- // At frame 0x21e (542): DAT_0047a7f8 = 0x10 (switches to "level 16" internally)
- // After gameplay: DAT_0047a7f8 = 0x10 (stays at 16)
- // This level ID switch affects which difficulty data is used mid-level.
- // The IACT callbacks handle gameplay regardless of this ID.
-
- // Play gameplay (15PLAY.SAN)
- // Original: FUN_0041f4d0("15PLAY.SAN", 0x28, -1, -1, 0)
- splayer->setCurVideoFlags(0x28);
- splayer->play("LEV15/15PLAY.SAN", 12);
- _deathFrame = splayer->_frame;
-
- if (_vm->shouldQuit()) return kLevelQuit;
-
- if (_playerShield > 0) {
- int accuracy = 0;
- if (_rebelKillCounter > 0) {
- accuracy = (_rebelKillCounter * 100) / (_rebelHitCounter + _rebelKillCounter);
- }
- debug("Rebel2: Level 15 completed! accuracy=%d%%", accuracy);
- playLevelEnd(15);
- _levelUnlocked[15] = true;
- // Level 15 completion leads to credits (FUN_0041BBE8)
- return kLevelNextLevel;
- }
-
- // Death: frame-based variant selection (FUN_0041B8D7 lines 46-65)
- debug("Rebel2: Level 15 death at frame %d", _deathFrame);
- playLevelDeathVariant(15, 1, _deathFrame);
- if (_vm->shouldQuit()) return kLevelQuit;
-
- _playerLives--;
- if (_playerLives <= 0) {
- playLevelGameOver(15);
- return kLevelGameOver;
- }
-
- playCinematic("LEV15/15RETRY.SAN");
- if (_vm->shouldQuit()) return kLevelQuit;
- }
-
- return kLevelQuit;
-}
} // End of namespace Scumm
diff --git a/engines/scumm/insane/insane_rebel_audio.cpp b/engines/scumm/insane/insane_rebel_audio.cpp
new file mode 100644
index 00000000000..01f0be50075
--- /dev/null
+++ b/engines/scumm/insane/insane_rebel_audio.cpp
@@ -0,0 +1,374 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include "common/system.h"
+
+#include "scumm/file.h"
+#include "scumm/scumm_v7.h"
+
+#include "scumm/smush/smush_player.h"
+
+#include "scumm/insane/insane_rebel.h"
+
+#include "audio/audiostream.h"
+#include "audio/decoders/raw.h"
+
+namespace Scumm {
+
+// ========== Audio Handling for Rebel Assault 2 ==========
+// RA2 doesn't use iMUSE - we handle audio directly through the mixer
+
+void InsaneRebel2::initAudio(int sampleRate) {
+ _audioSampleRate = sampleRate;
+ for (int i = 0; i < kRA2MaxAudioTracks; i++) {
+ _audioStreams[i] = nullptr;
+ _audioTrackActive[i] = false;
+ }
+}
+
+void InsaneRebel2::terminateAudio() {
+ for (int i = 0; i < kRA2MaxAudioTracks; i++) {
+ if (_audioTrackActive[i]) {
+ _vm->_mixer->stopHandle(_audioHandles[i]);
+ _audioTrackActive[i] = false;
+ }
+ if (_audioStreams[i]) {
+ _audioStreams[i]->finish();
+ _audioStreams[i] = nullptr;
+ }
+ }
+}
+
+void InsaneRebel2::queueAudioData(int trackIdx, uint8 *data, int32 size, int volume, int pan) {
+ if (trackIdx < 0 || trackIdx >= kRA2MaxAudioTracks || size <= 0 || !data) {
+ debug(5, "InsaneRebel2::queueAudioData: Invalid params trackIdx=%d size=%d data=%p", trackIdx, size, (void*)data);
+ return;
+ }
+
+ debug(5, "InsaneRebel2::queueAudioData: trackIdx=%d size=%d volume=%d pan=%d", trackIdx, size, volume, pan);
+
+ // Create audio stream if not already active
+ if (!_audioStreams[trackIdx]) {
+ // RA2 audio is 8-bit unsigned mono at the track's sample rate
+ debug("InsaneRebel2: Creating audio stream for track %d at %d Hz", trackIdx, _audioSampleRate);
+ _audioStreams[trackIdx] = Audio::makeQueuingAudioStream(_audioSampleRate, false);
+ _audioTrackActive[trackIdx] = true;
+ _vm->_mixer->playStream(Audio::Mixer::kSFXSoundType, &_audioHandles[trackIdx],
+ _audioStreams[trackIdx], -1, Audio::Mixer::kMaxChannelVolume, 0,
+ DisposeAfterUse::NO);
+ }
+
+ debug(6, "InsaneRebel2: Queueing %d bytes to track %d (vol=%d)", size, trackIdx, volume);
+
+ // Copy the audio data since queueBuffer may need to own it
+ byte *audioCopy = (byte *)malloc(size);
+ if (!audioCopy) {
+ return;
+ }
+ memcpy(audioCopy, data, size);
+
+ // Queue the audio data - RA2 SMUSH audio is 8-bit unsigned mono
+ _audioStreams[trackIdx]->queueBuffer(audioCopy, size, DisposeAfterUse::YES, Audio::FLAG_UNSIGNED);
+
+ // Apply volume and pan to the channel
+ int scaledVolume = (volume * Audio::Mixer::kMaxChannelVolume) / 127;
+ int scaledPan = (pan * 127) / 128; // Convert -128..127 to -127..127
+ _vm->_mixer->setChannelVolume(_audioHandles[trackIdx], scaledVolume);
+ _vm->_mixer->setChannelBalance(_audioHandles[trackIdx], scaledPan);
+}
+
+void InsaneRebel2::processAudioFrame(int16 feedSize) {
+ if (!_player) {
+ return;
+ }
+
+ // Initialize dispatch data if needed (normally done in processDispatches for iMUSE games)
+ if (_player->_smushTracksNeedInit) {
+ _player->_smushTracksNeedInit = false;
+ for (int i = 0; i < SMUSH_MAX_TRACKS; i++) {
+ _player->_smushDispatch[i].fadeRemaining = 0;
+ _player->_smushDispatch[i].fadeVolume = 0;
+ _player->_smushDispatch[i].fadeSampleRate = 0;
+ _player->_smushDispatch[i].elapsedAudio = 0;
+ _player->_smushDispatch[i].audioLength = 0;
+ }
+ }
+
+ // Access SmushPlayer's audio track data (InsaneRebel2 is a friend class)
+ // Only iterate over actually allocated tracks (not SMUSH_MAX_TRACKS)
+ for (int i = 0; i < _player->_smushNumTracks; i++) {
+ SmushPlayer::SmushAudioTrack &track = _player->_smushTracks[i];
+ SmushPlayer::SmushAudioDispatch &dispatch = _player->_smushDispatch[i];
+
+ if (track.state == TRK_STATE_INACTIVE) {
+ continue;
+ }
+
+ // Skip tracks that don't have valid buffer pointers yet
+ // Note: dispatch.dataBuf is set when transitioning from FADING to PLAYING,
+ // so tracks in FADING state won't have it set yet - that's OK, they'll be
+ // transitioned below and then processed
+ if (!track.blockPtr) {
+ debug(5, "InsaneRebel2: Skipping track %d - blockPtr=%p state=%d",
+ i, (void*)track.blockPtr, track.state);
+ continue;
+ }
+
+ // Check if this track type should be played
+ bool isPlayableTrack = ((track.flags & TRK_TYPE_MASK) == IS_SPEECH && _player->isChanActive(CHN_SPEECH)) ||
+ ((track.flags & TRK_TYPE_MASK) == IS_BKG_MUSIC && _player->isChanActive(CHN_BKGMUS)) ||
+ ((track.flags & TRK_TYPE_MASK) == IS_SFX && _player->isChanActive(CHN_OTHER));
+
+ if (!isPlayableTrack) {
+ continue;
+ }
+
+ // Calculate base volume for this track type
+ int baseVolume;
+ switch (track.flags & TRK_TYPE_MASK) {
+ case IS_SFX:
+ baseVolume = (_player->_smushTrackVols[1] * track.volume) >> 7;
+ break;
+ case IS_BKG_MUSIC:
+ baseVolume = (_player->_smushTrackVols[3] * track.volume) >> 7;
+ break;
+ case IS_SPEECH:
+ baseVolume = (_player->_smushTrackVols[2] * track.volume) >> 7;
+ break;
+ default:
+ baseVolume = track.volume;
+ break;
+ }
+ int mixVolume = baseVolume * _player->_smushTrackVols[0] / 127;
+
+ // Handle track state transitions: FADING -> PLAYING
+ if (track.state == TRK_STATE_FADING) {
+ dispatch.headerPtr = track.dataBuf;
+ dispatch.dataBuf = track.subChunkPtr;
+ dispatch.dataSize = track.dataSize;
+ dispatch.currentOffset = 0;
+ dispatch.audioLength = 0;
+ track.state = TRK_STATE_PLAYING;
+ }
+
+ // Process audio for this track
+ if (track.state != TRK_STATE_INACTIVE) {
+ int32 tmpFeedSize = feedSize;
+
+ while (tmpFeedSize > 0) {
+ int32 mixInFrameCount = dispatch.currentOffset;
+
+ // Use dispatch.dataBuf and dispatch.dataSize which are set consistently
+ // when the track transitions from FADING to PLAYING, and audioRemaining
+ // is calculated relative to these values by processAudioCodes
+ if (mixInFrameCount > 0 && dispatch.dataBuf && dispatch.dataSize > 0) {
Commit: 274be9ad63c8be10aed14b2d578239071ee816f6
https://github.com/scummvm/scummvm/commit/274be9ad63c8be10aed14b2d578239071ee816f6
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:10+02:00
Commit Message:
SCUMM: RA2: Improve intro playback
Changed paths:
engines/scumm/insane/insane_rebel_levels.cpp
engines/scumm/scumm.cpp
diff --git a/engines/scumm/insane/insane_rebel_levels.cpp b/engines/scumm/insane/insane_rebel_levels.cpp
index 160a8fdaa9d..07e4b0087b5 100644
--- a/engines/scumm/insane/insane_rebel_levels.cpp
+++ b/engines/scumm/insane/insane_rebel_levels.cpp
@@ -45,43 +45,38 @@ Common::String InsaneRebel2::getLevelPrefix(int levelId) {
void InsaneRebel2::playIntroSequence() {
// Emulates case 0 in FUN_004142BD
- // Plays the game intro sequence:
- // 1. CREDITS/O_OPEN_C.SAN - Fox logo (if certain conditions)
- // 2. CREDITS/O_OPEN_D.SAN - LucasArts logo (if certain conditions)
- // 3. OPEN/O_OPEN_A.SAN - Main intro
- // 4. OPEN/O_OPEN_B.SAN - Additional intro (if conditions)
+ //
+ // Original flow:
+ // - If 'f','o','x' keys all held: play CREDITS/O_OPEN_C.SAN (Fox logo easter egg)
+ // - If 'b','o','t' keys all held: play CREDITS/O_OPEN_D.SAN (LucasArts logo)
+ // INSTEAD of the normal intro
+ // - Else: play OPEN/O_OPEN_A.SAN (main intro - normal path)
+ // - If DAT_0047ab45 || DAT_0047ab47: play OPEN/O_OPEN_B.SAN (additional intro)
+ // - Fade out over 10 frames, clear palette, show top pilots, then -> main menu
+ //
+ // We skip the Fox/LucasArts easter eggs (require real-time key state during boot)
+ // and play both O_OPEN_A + O_OPEN_B unconditionally.
debug("Rebel2: Playing intro sequence");
- SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
-
- // Set intro flags (non-interactive)
- splayer->setCurVideoFlags(0x20);
-
- // Play Fox logo (CREDITS/O_OPEN_C.SAN)
- // In retail, this checks if 'f', 'o', 'x' keys are held (easter egg)
- // We'll play it unconditionally for now
- debug("Rebel2: Playing Fox logo");
- splayer->play("CREDITS/O_OPEN_C.SAN", 12);
-
- if (_vm->shouldQuit()) return;
-
- // Play LucasArts logo (CREDITS/O_OPEN_D.SAN)
- // In retail, this checks if 'b', 'o', 't' keys are held
- debug("Rebel2: Playing LucasArts logo");
- splayer->play("CREDITS/O_OPEN_D.SAN", 12);
+ _gameState = kStateIntro;
+ _menuInputActive = false;
- if (_vm->shouldQuit()) return;
+ SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
// Play main intro (OPEN/O_OPEN_A.SAN)
- debug("Rebel2: Playing main intro");
+ // Original: FUN_0041f4d0("OPEN/O_OPEN_A.SAN", 0x28, 0xffff, 0xffff, 0)
+ debug("Rebel2: Playing main intro (O_OPEN_A.SAN)");
+ splayer->setCurVideoFlags(0x28);
splayer->play("OPEN/O_OPEN_A.SAN", 12);
if (_vm->shouldQuit()) return;
// Play additional intro (OPEN/O_OPEN_B.SAN)
- // In retail, this plays if DAT_0047ab45 or DAT_0047ab47 != 0
- debug("Rebel2: Playing additional intro");
+ // Original: conditional on DAT_0047ab45 || DAT_0047ab47
+ // We play unconditionally (matches "Continue Intro" menu behavior)
+ debug("Rebel2: Playing additional intro (O_OPEN_B.SAN)");
+ splayer->setCurVideoFlags(0x28);
splayer->play("OPEN/O_OPEN_B.SAN", 12);
}
diff --git a/engines/scumm/scumm.cpp b/engines/scumm/scumm.cpp
index 32e0de1006d..0c5ea36f7b7 100644
--- a/engines/scumm/scumm.cpp
+++ b/engines/scumm/scumm.cpp
@@ -2685,8 +2685,11 @@ Common::Error ScummEngine::go() {
return Common::kNoError;
}
- // Full game: Run the main menu loop
- // This emulates the retail game flow from FUN_004142BD
+ // Full game: Emulates the retail game flow from FUN_004142BD
+ // Case 0: Play intro sequence (Fox logo, LucasArts logo, O_OPEN_A, O_OPEN_B)
+ rebel->playIntroSequence();
+
+ // Cases 1-4: Main menu -> pilot select -> chapter select -> gameplay loop
while (!shouldQuit()) {
// Run main menu and get result
int menuResult = rebel->runMainMenu();
Commit: 8723af81dbad8c0fb28b2538ebf416c838f02233
https://github.com/scummvm/scummvm/commit/8723af81dbad8c0fb28b2538ebf416c838f02233
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:10+02:00
Commit Message:
SCUMM: RA2: Improve subtitles
Changed paths:
engines/scumm/nut_renderer.cpp
engines/scumm/smush/smush_player.cpp
diff --git a/engines/scumm/nut_renderer.cpp b/engines/scumm/nut_renderer.cpp
index 9612093b54d..8d81f1a302c 100644
--- a/engines/scumm/nut_renderer.cpp
+++ b/engines/scumm/nut_renderer.cpp
@@ -422,14 +422,20 @@ int NutRenderer::drawCharV7(byte *buffer, Common::Rect &clipRect, int x, int y,
char color = (col != -1) ? col : 1;
if (_vm->_game.id == GID_REBEL2) {
- // RA2 codec 44 fonts use pixel values 1-4 as body/gradient.
- // The AHDR palette indices don't match the SMUSH video palette,
- // so all non-transparent pixels get the caller's text color.
+ // RA2 codec 44 font rendering, matching the original behavior:
+ // - Pixel value 1 â remapped to the caller's text color
+ // - Other non-transparent values â used as-is (palette indices)
+ // The font's pixel layout: value 4 = black outline (38% of pixels),
+ // value 1 = body (11%, remapped to color), value 3 = gray AA (2%).
+ // The SMUSH video palette reserves indices 0-4 for text rendering:
+ // 0=(0,0,0), 1=(255,255,255), 2=(188,188,188), 3=(128,128,128), 4=(0,0,0).
for (int j = minY; j < height; j++) {
for (int i = minX; i < width; i++) {
int8 value = *src++;
- if (value != _chars[chr].transparency)
+ if (value == 1)
dst[i] = color;
+ else if (value != _chars[chr].transparency)
+ dst[i] = value;
}
src += clipWdth;
dst += pitch;
diff --git a/engines/scumm/smush/smush_player.cpp b/engines/scumm/smush/smush_player.cpp
index 9b172697af6..4ca180b97c7 100644
--- a/engines/scumm/smush/smush_player.cpp
+++ b/engines/scumm/smush/smush_player.cpp
@@ -843,7 +843,11 @@ void SmushPlayer::handleTextResource(uint32 subType, int32 subSize, Common::Seek
// Query ConfMan here. However it may be slower, but
// player may want to switch the subtitles on or off during the
// playback. This fixes bug #2812
- if ((!ConfMan.getBool("subtitles")) && ((flags & 8) == 8))
+ //
+ // RA2: The original game always shows subtitle text during cinematics
+ // (there is no subtitle toggle in the retail options menu). Skip
+ // this check so TRES text is always rendered.
+ if (_vm->_game.id != GID_REBEL2 && (!ConfMan.getBool("subtitles")) && ((flags & 8) == 8))
return;
bool isCJKComi = (_vm->_game.id == GID_CMI && _vm->_useCJKMode);
@@ -921,6 +925,9 @@ void SmushPlayer::handleTextResource(uint32 subType, int32 subSize, Common::Seek
}
_multiFont->setDefaultFont(fontId);
+ debug("SmushPlayer::handleTextResource: RA2 TRES frame=%d fontId=%d color=%d flags=0x%x flg=%d pos=(%d,%d) clip=(%d,%d,%d,%d) str=\"%.40s\"",
+ _frame, fontId, color, flags, (int)flg, pos_x, pos_y, left, top, width, height, str);
+
if (flg & kStyleWordWrap) {
Common::Rect clipRect(MAX<int>(0, left), MAX<int>(0, top), MIN<int>(left + width, _width), MIN<int>(top + height, _height));
_multiFont->drawStringWrap(str, _dst, clipRect, pos_x, pos_y, color, flg);
Commit: 49e3be755df26de433b45b732279ddb34668fd0b
https://github.com/scummvm/scummvm/commit/49e3be755df26de433b45b732279ddb34668fd0b
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:10+02:00
Commit Message:
SCUMM: RA2: Guard wave initialization in opcode 6
Changed paths:
engines/scumm/insane/insane_rebel.cpp
engines/scumm/insane/insane_rebel.h
engines/scumm/insane/insane_rebel_iact.cpp
diff --git a/engines/scumm/insane/insane_rebel.cpp b/engines/scumm/insane/insane_rebel.cpp
index 5dacef70047..bcfbb8d8c75 100644
--- a/engines/scumm/insane/insane_rebel.cpp
+++ b/engines/scumm/insane/insane_rebel.cpp
@@ -127,6 +127,7 @@ InsaneRebel2::InsaneRebel2(ScummEngine_v7 *scumm) {
memset(_damageSavedPalette, 0, sizeof(_damageSavedPalette));
// Retail globals mapped: hit counter, cooldown, invulnerability flag
+ _rebelOp6Initialized = false;
_rebelHitCounter = 0;
_rebelKillCounter = 0;
_rebelInvulnerable = false;
diff --git a/engines/scumm/insane/insane_rebel.h b/engines/scumm/insane/insane_rebel.h
index 7cbedcecef2..82a5ab56bb2 100644
--- a/engines/scumm/insane/insane_rebel.h
+++ b/engines/scumm/insane/insane_rebel.h
@@ -636,6 +636,7 @@ public:
byte _damageSavedPalette[0x300]; // DAT_00459990 - palette snapshot before flash
// Rebel per-level counters / flags mapped from retail globals
+ bool _rebelOp6Initialized; // Guard: opcode 6 init block (clearBit/links/wave) runs once per video
int _rebelHitCounter; // DAT_0047ab80 - hit counter / state tracker
int _rebelKillCounter; // DAT_0047ab88 - enemies destroyed this phase
bool _rebelInvulnerable; // DAT_0047ab64 - toggles invulnerability / state
diff --git a/engines/scumm/insane/insane_rebel_iact.cpp b/engines/scumm/insane/insane_rebel_iact.cpp
index 99f90a4a0b1..36c3cc87ca1 100644
--- a/engines/scumm/insane/insane_rebel_iact.cpp
+++ b/engines/scumm/insane/insane_rebel_iact.cpp
@@ -39,6 +39,13 @@ void InsaneRebel2::procPreRendering(byte *renderBitmap) {
// Call base class implementation first (handles Full Throttle state machine)
Insane::procPreRendering(renderBitmap);
+ // Reset opcode 6 init flag at the start of each new video.
+ // This ensures the per-wave init (clearBit, link table reset, wave state)
+ // fires exactly once per wave video, not every frame.
+ if (_player && _player->_frame == 0) {
+ _rebelOp6Initialized = false;
+ }
+
// For Level 2 gameplay (Handler 8 only), restore the background BEFORE FOBJ decoding.
// The tiny FOBJ sprites (7x10, 9x38 pixels) only draw new sprite positions but don't
// clear old ones. By restoring the full background each frame, we ensure old sprite
@@ -573,18 +580,17 @@ void InsaneRebel2::iactRebel2Opcode6(byte *renderBitmap, Common::SeekableReadStr
}
// Reset state when shipLevelMode != 0 && par4 == 1 (FUN_401234 lines 97-103)
- if (_shipLevelMode != 0 && par4 == 1) {
- // Clear ALL iactBits â matches FUN_00423880 calling FUN_00423a00(0)
+ // Guard with _rebelOp6Initialized: runs once per wave video, not per frame.
+ if (_shipLevelMode != 0 && par4 == 1 && !_rebelOp6Initialized) {
clearBit(0);
- // Clear link tables
for (int i = 0; i < 512; i++) {
_rebelLinks[i][0] = 0;
_rebelLinks[i][1] = 0;
_rebelLinks[i][2] = 0;
}
- // DAT_0047ab98 = DAT_0047ab9c: Reset wave state to accumulated phase state
_rebelWaveState = _rebelPhaseState;
- debug("Rebel2 Opcode 6 (Handler 8): State reset, wave=0x%x", _rebelWaveState);
+ _rebelOp6Initialized = true;
+ debug("Rebel2 Opcode 6 (Handler 8): Wave init, wave=0x%x", _rebelWaveState);
}
// Skip position calculation for special modes 4 and 5
@@ -979,21 +985,19 @@ void InsaneRebel2::iactRebel2Opcode6(byte *renderBitmap, Common::SeekableReadStr
// Note: This is local_14[4] in the decompiled code, NOT local_14[3] (par4)
if (par5 == 1) {
_rebelStatusBarSprite = 5;
- // Clear ALL iactBits â matches FUN_00423880 calling FUN_00423a00(0)
- // at IACT callback registration time. Each new wave video starts with
- // a clean bit table so enemy IDs reused across videos work correctly.
- clearBit(0);
- // Reset link tables (DAT_0045797c through DAT_0045917c)
- for (int i = 0; i < 512; i++) {
- _rebelLinks[i][0] = 0;
- _rebelLinks[i][1] = 0;
- _rebelLinks[i][2] = 0;
+ // Guard with _rebelOp6Initialized: runs once per wave video, not per frame.
+ if (!_rebelOp6Initialized) {
+ clearBit(0);
+ for (int i = 0; i < 512; i++) {
+ _rebelLinks[i][0] = 0;
+ _rebelLinks[i][1] = 0;
+ _rebelLinks[i][2] = 0;
+ }
+ _rebelWaveState = _rebelPhaseState;
+ _rebelOp6Initialized = true;
+ debug("Rebel2 Opcode 6 (Handler 25): Wave init, wave=0x%x autopilot=%d damageLevel=%d",
+ _rebelWaveState, _rebelAutopilot, _rebelDamageLevel);
}
- // Reset wave state to accumulated phase state (same as Handler 8)
- // DAT_0047ab98 = DAT_0047ab9c: ensures new wave starts with correct state
- _rebelWaveState = _rebelPhaseState;
- debug("Rebel2 Opcode 6 (Handler 25): Status bar enabled, state reset, wave=0x%x autopilot=%d damageLevel=%d",
- _rebelWaveState, _rebelAutopilot, _rebelDamageLevel);
}
// Set sprite mode (DAT_00457900 = local_14[3]) - controls which GRD sprite to render
@@ -1184,24 +1188,23 @@ void InsaneRebel2::iactRebel2Opcode6(byte *renderBitmap, Common::SeekableReadStr
if (par4 == 1) {
// Draw status bar sprite 5 (FUN_0040bb87 equivalent)
_rebelStatusBarSprite = (_rebelLevelType == 5) ? 53 : 5;
- debug("Rebel2 Opcode 6: Status Bar ENABLED - sprite %d", _rebelStatusBarSprite);
- // Clear ALL iactBits â matches FUN_00423880 calling FUN_00423a00(0)
- clearBit(0);
-
- // Clear link tables (DAT_0045797c through DAT_0045917c)
- for (int i = 0; i < 512; i++) {
- _rebelLinks[i][0] = 0;
- _rebelLinks[i][1] = 0;
- _rebelLinks[i][2] = 0;
+ // Per-wave init: clear bits, links, reset wave state.
+ // In the original game, FUN_00423880 runs ONCE at video-start callback
+ // registration time, not per-frame. Guard with _rebelOp6Initialized so
+ // this fires once per wave video (reset in procPreRendering at frame 0).
+ if (!_rebelOp6Initialized) {
+ clearBit(0);
+ for (int i = 0; i < 512; i++) {
+ _rebelLinks[i][0] = 0;
+ _rebelLinks[i][1] = 0;
+ _rebelLinks[i][2] = 0;
+ }
+ _rebelWaveState = _rebelPhaseState;
+ _rebelHitCounter = 0;
+ _rebelOp6Initialized = true;
+ debug("Rebel2 Opcode 6: Wave init - cleared bits/links, waveState=0x%x", _rebelWaveState);
}
-
- // DAT_0047ab98 = DAT_0047ab9c: At the start of each wave video,
- // reset wave state to accumulated phase state. Enemies killed in
- // previous waves stay killed; new kills add during this wave.
- _rebelWaveState = _rebelPhaseState;
- _rebelHitCounter = 0;
- debug("Rebel2 Opcode 6: Wave state reset to phase state 0x%x", _rebelWaveState);
}
// Step 2: Set level type (DAT_00457900 = par3)
Commit: 3d79ed052683379452e7d4d813e24268051457eb
https://github.com/scummvm/scummvm/commit/3d79ed052683379452e7d4d813e24268051457eb
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:11+02:00
Commit Message:
SCUMM: RA2: Minimize impact outside the RA2 engine
Changed paths:
A engines/scumm/smush/codec_ra2.cpp
A engines/scumm/smush/smush_player_ra2.cpp
engines/scumm/module.mk
engines/scumm/smush/codec1.cpp
engines/scumm/smush/smush_player.cpp
engines/scumm/smush/smush_player.h
diff --git a/engines/scumm/module.mk b/engines/scumm/module.mk
index d7f021aa6dd..55a00dd601d 100644
--- a/engines/scumm/module.mk
+++ b/engines/scumm/module.mk
@@ -144,8 +144,10 @@ MODULE_OBJS += \
smush/codec20.o \
smush/codec37.o \
smush/codec47.o \
+ smush/codec_ra2.o \
smush/smush_multi_font.o \
- smush/smush_player.o
+ smush/smush_player.o \
+ smush/smush_player_ra2.o
ifdef USE_ARM_SMUSH_ASM
MODULE_OBJS += \
diff --git a/engines/scumm/smush/codec1.cpp b/engines/scumm/smush/codec1.cpp
index 0020c51d8cc..087e4d53aaf 100644
--- a/engines/scumm/smush/codec1.cpp
+++ b/engines/scumm/smush/codec1.cpp
@@ -36,245 +36,4 @@ void smushDecodeRLE(byte *dst, const byte *src, int left, int top, int width, in
} while (--height);
}
-/**
- * Codec 3 RLE decoder that writes ALL colors including color 0 (black).
- * Use this for background images where color 0 should NOT be treated as transparent.
- * The standard smushDecodeRLE() treats color 0 as transparent, which is correct
- * for overlay sprites but wrong for background images.
- *
- * Used by: Rebel Assault 2 Level 2 background loading (IACT opcode 8, par4=5)
- */
-void smushDecodeRLEOpaque(byte *dst, const byte *src, int left, int top, int width, int height, int pitch) {
- dst += top * pitch;
- do {
- dst += left;
- bompDecodeLine(dst, src + 2, width, true); // setZero = TRUE to write all colors
- src += READ_LE_UINT16(src) + 2;
- dst += pitch - left;
- } while (--height);
-}
-
-/**
- * Codec 21/44: Line Update codec
- * Used for fonts (NUT files) and some embedded HUD frames.
- * Format: Each line has a 2-byte size header, then 2-byte skip, 2-byte count pairs with literal pixels.
- * The count value needs +1 to get the actual number of pixels to copy.
- * Note: Skip regions preserve previous frame content (delta compression).
- */
-void smushDecodeLineUpdate(byte *dst, const byte *src, int left, int top, int width, int height, int pitch) {
- dst += top * pitch + left;
-
- while (height--) {
- byte *dstPtrNext = dst + pitch;
- const byte *srcPtrNext = src + 2 + READ_LE_UINT16(src);
- src += 2; // Skip line size header
- int len = width;
- byte *lineDst = dst;
-
- while (len > 0) {
- // Read 2-byte LE skip value
- int skip = READ_LE_UINT16(src);
- src += 2;
- lineDst += skip;
- len -= skip;
- if (len <= 0)
- break;
-
- // Read 2-byte LE copy count (+1 for actual count)
- int count = READ_LE_UINT16(src) + 1;
- src += 2;
- if (count > len)
- count = len;
- len -= count;
-
- // Copy literal pixels
- memcpy(lineDst, src, count);
- lineDst += count;
- src += count;
- }
- dst = dstPtrNext;
- src = srcPtrNext;
- }
-}
-
-/**
- * Codec 23: Skip/Copy with embedded RLE
- * Used for video frames with skip regions.
- * Format: Each line has 2-byte size, then (skip, runSize, RLE_data) triplets.
- * Note: Skip regions preserve previous frame content (delta compression).
- */
-void smushDecodeSkipRLE(byte *dst, const byte *src, int left, int top, int width, int height, int pitch) {
- dst += top * pitch + left;
-
- for (int row = 0; row < height; row++) {
- int lineDataSize = READ_LE_UINT16(src);
- src += 2;
- const byte *lineEnd = src + lineDataSize;
- byte *lineDst = dst;
- int x = 0;
-
- while (src < lineEnd && x < width) {
- int skip = READ_LE_UINT16(src);
- src += 2;
- x += skip; // Skip preserves previous frame content
- if (src >= lineEnd || x >= width)
- break;
-
- int runSize = READ_LE_UINT16(src);
- src += 2;
- const byte *runEnd = src + runSize;
-
- // Decode RLE within this run - write ALL colors including 0
- while (src < runEnd && x < width) {
- byte code = *src++;
- int num = (code >> 1) + 1;
- if (num > width - x)
- num = width - x;
-
- if (code & 1) {
- // RLE run - repeat color
- byte color = (src < runEnd) ? *src++ : 0;
- memset(lineDst + x, color, num);
- x += num;
- } else {
- // Literal run - copy bytes
- int toCopy = num;
- if (toCopy > (int)(runEnd - src))
- toCopy = (int)(runEnd - src);
- memcpy(lineDst + x, src, toCopy);
- src += toCopy;
- x += toCopy;
- }
- }
- src = runEnd;
- }
- src = lineEnd;
- dst += pitch;
- }
-}
-
-/**
- * Codec 45: RA2-specific BOMP RLE with variable header
- * Used for embedded ANIM frames, particularly small animation elements.
- * Has a variable-length header (commonly 6 bytes starting with 01 FE).
- * Note: For overlay sprites, color 0 is treated as transparent.
- */
-void smushDecodeRA2Bomp(byte *dst, const byte *src, int left, int top, int width, int height, int pitch, int dataSize) {
- dst += top * pitch + left;
-
- // Detect header pattern and find RLE data start
- int headerSkip = 0;
-
- // Check for common 6-byte header pattern: 01 FE XX XX XX XX
- if (dataSize > 6 && src[0] == 0x01 && src[1] == 0xFE) {
- headerSkip = 6;
- } else {
- // Probe offsets to find valid RLE start
- // Valid start should have reasonable line size values
- for (int testOffset = 0; testOffset <= 6 && testOffset + 2 <= dataSize; testOffset += 2) {
- int testLineSize = READ_LE_UINT16(src + testOffset);
- // A valid line size should be positive and reasonable for the width
- if (testLineSize > 0 && testLineSize <= width * 2 && testLineSize < dataSize - testOffset) {
- // Further validation: try to count valid line sizes
- int linesTest = 0;
- const byte *testPtr = src + testOffset;
- bool validSum = true;
-
- while (linesTest < height && testPtr + 2 <= src + dataSize) {
- int ls = READ_LE_UINT16(testPtr);
- if (ls <= 0 || ls > width * 2) {
- validSum = false;
- break;
- }
- testPtr += ls + 2;
- linesTest++;
- }
-
- if (validSum && linesTest >= height - 1) {
- headerSkip = testOffset;
- break;
- }
- }
- }
- }
-
- src += headerSkip;
- const byte *dataEnd = src + (dataSize - headerSkip);
-
- // Check first value to determine per-line vs continuous mode
- int firstVal = (src + 2 <= dataEnd) ? READ_LE_UINT16(src) : 0;
- bool perLineMode = (firstVal > 0 && firstVal <= width * 2);
-
- if (perLineMode) {
- // Per-line RLE with 2-byte size headers
- for (int row = 0; row < height && src < dataEnd; row++) {
- int lineSize = READ_LE_UINT16(src);
- src += 2;
- if (lineSize <= 0 || lineSize > (int)(dataEnd - src))
- break;
-
- const byte *lineEnd = src + lineSize;
- byte *rowDst = dst + row * pitch;
- int x = 0;
-
- while (src < lineEnd && x < width) {
- byte ctrl = *src++;
- int count = (ctrl >> 1) + 1;
-
- if (ctrl & 1) {
- // RLE fill - color 0 is transparent for overlay sprites
- byte color = (src < lineEnd) ? *src++ : 0;
- if (color != 0) {
- int num = (count > width - x) ? width - x : count;
- memset(rowDst + x, color, num);
- }
- x += count;
- if (x > width)
- x = width;
- } else {
- // Literal copy - color 0 is transparent for overlay sprites
- for (int i = 0; i < count && x < width && src < lineEnd; i++) {
- byte color = *src++;
- if (color != 0)
- rowDst[x] = color;
- x++;
- }
- }
- }
- src = lineEnd;
- }
- } else {
- // Continuous BOMP RLE (no per-line headers)
- for (int row = 0; row < height && src < dataEnd; row++) {
- byte *rowDst = dst + row * pitch;
- int x = 0;
-
- while (x < width && src < dataEnd) {
- byte ctrl = *src++;
- int count = (ctrl >> 1) + 1;
-
- if (ctrl & 1) {
- // RLE fill - color 0 is transparent for overlay sprites
- byte color = (src < dataEnd) ? *src++ : 0;
- if (color != 0) {
- int num = (count > width - x) ? width - x : count;
- memset(rowDst + x, color, num);
- }
- x += count;
- if (x > width)
- x = width;
- } else {
- // Literal copy - color 0 is transparent for overlay sprites
- for (int i = 0; i < count && x < width && src < dataEnd; i++) {
- byte color = *src++;
- if (color != 0)
- rowDst[x] = color;
- x++;
- }
- }
- }
- }
- }
-}
-
} // End of namespace Scumm
diff --git a/engines/scumm/smush/codec_ra2.cpp b/engines/scumm/smush/codec_ra2.cpp
new file mode 100644
index 00000000000..7cda7943b59
--- /dev/null
+++ b/engines/scumm/smush/codec_ra2.cpp
@@ -0,0 +1,271 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+// Rebel Assault 2 SMUSH video codecs
+
+#include "common/endian.h"
+
+#include "scumm/bomp.h"
+
+namespace Scumm {
+
+/**
+ * Codec 3 RLE decoder that writes ALL colors including color 0 (black).
+ * Use this for background images where color 0 should NOT be treated as transparent.
+ * The standard smushDecodeRLE() treats color 0 as transparent, which is correct
+ * for overlay sprites but wrong for background images.
+ *
+ * Used by: Rebel Assault 2 Level 2 background loading (IACT opcode 8, par4=5)
+ */
+void smushDecodeRLEOpaque(byte *dst, const byte *src, int left, int top, int width, int height, int pitch) {
+ dst += top * pitch;
+ do {
+ dst += left;
+ bompDecodeLine(dst, src + 2, width, true); // setZero = TRUE to write all colors
+ src += READ_LE_UINT16(src) + 2;
+ dst += pitch - left;
+ } while (--height);
+}
+
+/**
+ * Codec 21/44: Line Update codec
+ * Used for fonts (NUT files) and some embedded HUD frames.
+ * Format: Each line has a 2-byte size header, then 2-byte skip, 2-byte count pairs with literal pixels.
+ * The count value needs +1 to get the actual number of pixels to copy.
+ * Note: Skip regions preserve previous frame content (delta compression).
+ */
+void smushDecodeLineUpdate(byte *dst, const byte *src, int left, int top, int width, int height, int pitch) {
+ dst += top * pitch + left;
+
+ while (height--) {
+ byte *dstPtrNext = dst + pitch;
+ const byte *srcPtrNext = src + 2 + READ_LE_UINT16(src);
+ src += 2; // Skip line size header
+ int len = width;
+ byte *lineDst = dst;
+
+ while (len > 0) {
+ // Read 2-byte LE skip value
+ int skip = READ_LE_UINT16(src);
+ src += 2;
+ lineDst += skip;
+ len -= skip;
+ if (len <= 0)
+ break;
+
+ // Read 2-byte LE copy count (+1 for actual count)
+ int count = READ_LE_UINT16(src) + 1;
+ src += 2;
+ if (count > len)
+ count = len;
+ len -= count;
+
+ // Copy literal pixels
+ memcpy(lineDst, src, count);
+ lineDst += count;
+ src += count;
+ }
+ dst = dstPtrNext;
+ src = srcPtrNext;
+ }
+}
+
+/**
+ * Codec 23: Skip/Copy with embedded RLE
+ * Used for video frames with skip regions.
+ * Format: Each line has 2-byte size, then (skip, runSize, RLE_data) triplets.
+ * Note: Skip regions preserve previous frame content (delta compression).
+ */
+void smushDecodeSkipRLE(byte *dst, const byte *src, int left, int top, int width, int height, int pitch) {
+ dst += top * pitch + left;
+
+ for (int row = 0; row < height; row++) {
+ int lineDataSize = READ_LE_UINT16(src);
+ src += 2;
+ const byte *lineEnd = src + lineDataSize;
+ byte *lineDst = dst;
+ int x = 0;
+
+ while (src < lineEnd && x < width) {
+ int skip = READ_LE_UINT16(src);
+ src += 2;
+ x += skip; // Skip preserves previous frame content
+ if (src >= lineEnd || x >= width)
+ break;
+
+ int runSize = READ_LE_UINT16(src);
+ src += 2;
+ const byte *runEnd = src + runSize;
+
+ // Decode RLE within this run - write ALL colors including 0
+ while (src < runEnd && x < width) {
+ byte code = *src++;
+ int num = (code >> 1) + 1;
+ if (num > width - x)
+ num = width - x;
+
+ if (code & 1) {
+ // RLE run - repeat color
+ byte color = (src < runEnd) ? *src++ : 0;
+ memset(lineDst + x, color, num);
+ x += num;
+ } else {
+ // Literal run - copy bytes
+ int toCopy = num;
+ if (toCopy > (int)(runEnd - src))
+ toCopy = (int)(runEnd - src);
+ memcpy(lineDst + x, src, toCopy);
+ src += toCopy;
+ x += toCopy;
+ }
+ }
+ src = runEnd;
+ }
+ src = lineEnd;
+ dst += pitch;
+ }
+}
+
+/**
+ * Codec 45: RA2-specific BOMP RLE with variable header
+ * Used for embedded ANIM frames, particularly small animation elements.
+ * Has a variable-length header (commonly 6 bytes starting with 01 FE).
+ * Note: For overlay sprites, color 0 is treated as transparent.
+ */
+void smushDecodeRA2Bomp(byte *dst, const byte *src, int left, int top, int width, int height, int pitch, int dataSize) {
+ dst += top * pitch + left;
+
+ // Detect header pattern and find RLE data start
+ int headerSkip = 0;
+
+ // Check for common 6-byte header pattern: 01 FE XX XX XX XX
+ if (dataSize > 6 && src[0] == 0x01 && src[1] == 0xFE) {
+ headerSkip = 6;
+ } else {
+ // Probe offsets to find valid RLE start
+ // Valid start should have reasonable line size values
+ for (int testOffset = 0; testOffset <= 6 && testOffset + 2 <= dataSize; testOffset += 2) {
+ int testLineSize = READ_LE_UINT16(src + testOffset);
+ // A valid line size should be positive and reasonable for the width
+ if (testLineSize > 0 && testLineSize <= width * 2 && testLineSize < dataSize - testOffset) {
+ // Further validation: try to count valid line sizes
+ int linesTest = 0;
+ const byte *testPtr = src + testOffset;
+ bool validSum = true;
+
+ while (linesTest < height && testPtr + 2 <= src + dataSize) {
+ int ls = READ_LE_UINT16(testPtr);
+ if (ls <= 0 || ls > width * 2) {
+ validSum = false;
+ break;
+ }
+ testPtr += ls + 2;
+ linesTest++;
+ }
+
+ if (validSum && linesTest >= height - 1) {
+ headerSkip = testOffset;
+ break;
+ }
+ }
+ }
+ }
+
+ src += headerSkip;
+ const byte *dataEnd = src + (dataSize - headerSkip);
+
+ // Check first value to determine per-line vs continuous mode
+ int firstVal = (src + 2 <= dataEnd) ? READ_LE_UINT16(src) : 0;
+ bool perLineMode = (firstVal > 0 && firstVal <= width * 2);
+
+ if (perLineMode) {
+ // Per-line RLE with 2-byte size headers
+ for (int row = 0; row < height && src < dataEnd; row++) {
+ int lineSize = READ_LE_UINT16(src);
+ src += 2;
+ if (lineSize <= 0 || lineSize > (int)(dataEnd - src))
+ break;
+
+ const byte *lineEnd = src + lineSize;
+ byte *rowDst = dst + row * pitch;
+ int x = 0;
+
+ while (src < lineEnd && x < width) {
+ byte ctrl = *src++;
+ int count = (ctrl >> 1) + 1;
+
+ if (ctrl & 1) {
+ // RLE fill - color 0 is transparent for overlay sprites
+ byte color = (src < lineEnd) ? *src++ : 0;
+ if (color != 0) {
+ int num = (count > width - x) ? width - x : count;
+ memset(rowDst + x, color, num);
+ }
+ x += count;
+ if (x > width)
+ x = width;
+ } else {
+ // Literal copy - color 0 is transparent for overlay sprites
+ for (int i = 0; i < count && x < width && src < lineEnd; i++) {
+ byte color = *src++;
+ if (color != 0)
+ rowDst[x] = color;
+ x++;
+ }
+ }
+ }
+ src = lineEnd;
+ }
+ } else {
+ // Continuous BOMP RLE (no per-line headers)
+ for (int row = 0; row < height && src < dataEnd; row++) {
+ byte *rowDst = dst + row * pitch;
+ int x = 0;
+
+ while (x < width && src < dataEnd) {
+ byte ctrl = *src++;
+ int count = (ctrl >> 1) + 1;
+
+ if (ctrl & 1) {
+ // RLE fill - color 0 is transparent for overlay sprites
+ byte color = (src < dataEnd) ? *src++ : 0;
+ if (color != 0) {
+ int num = (count > width - x) ? width - x : count;
+ memset(rowDst + x, color, num);
+ }
+ x += count;
+ if (x > width)
+ x = width;
+ } else {
+ // Literal copy - color 0 is transparent for overlay sprites
+ for (int i = 0; i < count && x < width && src < dataEnd; i++) {
+ byte color = *src++;
+ if (color != 0)
+ rowDst[x] = color;
+ x++;
+ }
+ }
+ }
+ }
+ }
+}
+
+} // End of namespace Scumm
diff --git a/engines/scumm/smush/smush_player.cpp b/engines/scumm/smush/smush_player.cpp
index 4ca180b97c7..e0391672c68 100644
--- a/engines/scumm/smush/smush_player.cpp
+++ b/engines/scumm/smush/smush_player.cpp
@@ -40,7 +40,6 @@
#include "scumm/smush/codec37.h"
#include "scumm/smush/codec47.h"
#include "scumm/smush/smush_font.h"
-#include "scumm/smush/smush_multi_font.h"
#include "scumm/smush/smush_player.h"
#include "scumm/insane/insane.h"
@@ -257,35 +256,15 @@ SmushPlayer::SmushPlayer(ScummEngine_v7 *scumm, IMuseDigital *imuseDigital, Insa
_frameBuffer = nullptr;
_specialBuffer = nullptr;
_specialBufferSize = 0;
- _storedFobjData = nullptr;
- _storedFobjDataSize = 0;
- _storedFobjCodec = 0;
- _storedFobjLeft = 0;
- _storedFobjTop = 0;
- _storedFobjWidth = 0;
- _storedFobjHeight = 0;
_seekPos = -1;
- _skipNext = false;
- _ra2FastForwarding = false;
- _fobjOffsetX = 0;
- _fobjOffsetY = 0;
_dst = nullptr;
- _storeFrame = false;
_compressedFileMode = false;
_width = 0;
_height = 0;
- // LOAD chunk streaming buffer (RA2)
- _loadBuffer = nullptr;
- _loadBufferSize = 0;
- _loadBufferOffset = 0;
- _loadReadOffset = 8; // Original starts reading at offset 8 (skips header)
- _lastLoadChunkIdx = -1;
- _totalLoadChunks = 0;
- _scrollX = 0;
- _scrollY = 0;
+ ra2InitFields();
_IACTpos = 0;
_speed = -1;
_insanity = false;
@@ -314,11 +293,9 @@ SmushPlayer::SmushPlayer(ScummEngine_v7 *scumm, IMuseDigital *imuseDigital, Insa
_smushAudioInitialized = false;
_smushAudioCallbackEnabled = false;
- // Rebel Assault 2 doesn't use iMUSE for audio, so _imuseDigital may be null
if (_imuseDigital) {
initAudio(_imuseDigital->getSampleRate(), 200000);
} else {
- // RA2 audio is 11025 Hz
initAudio(11025, 200000);
}
}
@@ -326,19 +303,13 @@ SmushPlayer::SmushPlayer(ScummEngine_v7 *scumm, IMuseDigital *imuseDigital, Insa
SmushPlayer::~SmushPlayer() {
delete _IACTchannel;
delete _compressedFileSoundHandle;
- delete _multiFont;
- _multiFont = nullptr;
+ ra2DestroyFields();
terminateAudio();
- // Free any preserved frame buffer (RA2 preserves this across videos)
free(_frameBuffer);
_frameBuffer = nullptr;
free(_specialBuffer);
_specialBuffer = nullptr;
- free(_storedFobjData);
- _storedFobjData = nullptr;
- free(_loadBuffer);
- _loadBuffer = nullptr;
}
void SmushPlayer::init(int32 speed) {
@@ -348,11 +319,6 @@ void SmushPlayer::init(int32 speed) {
_speed = speed;
_endOfFile = false;
_storeFrame = false;
-
- // Reset FOBJ offsets between videos. These are set per-video by
- // procPreRendering (chapter select preview scrolling) or IACT opcode 6
- // (corridor overlay positioning). Without this, offsets from O_LEVEL.SAN
- // would persist and shift FOBJs in subsequent videos â buffer overflows.
_fobjOffsetX = 0;
_fobjOffsetY = 0;
@@ -362,39 +328,8 @@ void SmushPlayer::init(int32 speed) {
_vm->setDirtyColors(0, 255);
_dst = vs->getPixels(0, 0);
- // For RA2: Re-push the SMUSH palette to the system. Videos like O_LEVEL.SAN
- // have no NPAL chunk and inherit the palette from the previous video. Since
- // play() resets _palDirtyMin/Max, the palette would never be pushed otherwise.
- // This is safe for videos WITH NPAL too â the NPAL handler immediately
- // overwrites _pal and re-marks dirty.
- if (_vm->_game.id == GID_REBEL2) {
- setDirtyColors(0, 255);
- }
-
- // For Rebel Assault 2, handle background preservation between videos:
- // - Cinematic videos (flags 0x20) clear the buffer for a fresh start
- // - Gameplay videos (flags 0x28) preserve the existing screen content
- //
- // The virtual screen (_dst = vs->getPixels) persists between videos, so
- // when a gameplay video starts, _dst already contains the last frame of
- // the previous cinematic video - which is exactly what we want.
- // We do NOT restore from _frameBuffer because STOR captures frame 0
- // (often a black initialization frame), not the last frame.
- if (_vm->_game.id == GID_REBEL2 && _dst != nullptr) {
- if ((_curVideoFlags & 0x08) == 0) {
- // Cinematic mode (flags 0x20) - clear buffer for fresh video
- memset(_dst, 0, vs->w * vs->h);
- } else {
- // Gameplay mode (flags 0x28) - do nothing, preserve existing screen content
- // Count non-zero pixels to verify there's actual content
- int nonZero = 0;
- for (int i = 0; i < vs->w * vs->h; i++) {
- if (_dst[i] != 0) nonZero++;
- }
- debug("SmushPlayer::init: Preserving screen for gameplay video (%dx%d, %d%% non-zero)",
- vs->w, vs->h, (nonZero * 100) / (vs->w * vs->h));
- }
- }
+ if (isRA2())
+ ra2InitVideo();
// HACK HACK HACK: This is an *evil* trick, beware!
// We do this to fix bug #1792. A proper solution would change all the
@@ -429,15 +364,9 @@ void SmushPlayer::release() {
free(_specialBuffer);
_specialBuffer = nullptr;
- free(_storedFobjData);
- _storedFobjData = nullptr;
- _storedFobjDataSize = 0;
-
- // For Rebel Assault 2, preserve _frameBuffer across videos so that
- // gameplay videos (which have no background FOBJ) can use the stored
- // background from the previous BEG cinematic video.
- // The _frameBuffer will be freed in the destructor or reused by the next video.
- if (_vm->_game.id != GID_REBEL2) {
+ if (isRA2()) {
+ ra2ReleaseVideo();
+ } else {
free(_frameBuffer);
_frameBuffer = nullptr;
}
@@ -468,37 +397,8 @@ void SmushPlayer::handleFetch(int32 subSize, Common::SeekableReadStream &b) {
debugC(DEBUG_SMUSH, "SmushPlayer::handleFetch()");
assert(subSize >= 6);
- // Read FTCH data: 2 unknown + 2 X offset + 2 Y offset
- // Original (FUN_00423A50 lines 91-103): reads X/Y from chunk data and
- // re-renders stored FOBJ at position (X + param_3 + DAT_00482c1c, Y + param_4 + DAT_00482c20)
- int16 ftchUnknown = b.readSint16LE();
- int16 ftchX = b.readSint16LE();
- int16 ftchY = b.readSint16LE();
-
- debug("SmushPlayer::handleFetch: frame=%d unknown=%d x=%d y=%d",
- _frame, ftchUnknown, ftchX, ftchY);
-
- // For RA2 Handler 25, skip FTCH because the frame buffer only contains the
- // par4=5 base background without the overlays (par4=4, 6, 7) that were drawn
- // immediately in frame 0. Restoring would erase those overlays and make
- // enemies invisible since they draw on top of the erased areas.
- if (_vm->_game.id == GID_REBEL2 && _insane != nullptr) {
- InsaneRebel2 *rebel2 = static_cast<InsaneRebel2 *>(_insane);
- int handler = rebel2->getHandler();
- if (handler == 25) {
- debug("SmushPlayer::handleFetch: Skipping FTCH for Handler 25 - preserving overlays");
- return;
- }
- }
-
- // RA2: Re-decode stored FOBJ data with current offsets (matching original FUN_004246d0).
- // The stored FOBJ's original position is combined with the current _fobjOffsetX/Y,
- // so scrolling the chapter preview works correctly on each FTCH.
- if (_vm->_game.id == GID_REBEL2 && _storedFobjData != nullptr) {
- decodeFrameObject(_storedFobjCodec, _storedFobjData,
- _storedFobjLeft, _storedFobjTop,
- _storedFobjWidth, _storedFobjHeight,
- _storedFobjDataSize);
+ if (isRA2()) {
+ ra2HandleFetch(b);
return;
}
@@ -507,124 +407,23 @@ void SmushPlayer::handleFetch(int32 subSize, Common::SeekableReadStream &b) {
}
}
-/**
- * Handle LOAD chunk for Rebel Assault 2
- *
- * LOAD chunks stream embedded resource data (likely audio for streaming playback)
- * across multiple frames. The data is accumulated in a buffer and consumed by
- * the audio system.
- *
- * Chunk format (from FUN_00424450 in original):
- * Offset 0 (2 bytes): totalChunks - Total number of LOAD chunks in sequence
- * Offset 2 (2 bytes): chunkIndex - Current chunk index (0-based)
- * Offset 4 (6 bytes): unknown/padding
- * Offset 10+: Actual data payload
- *
- * Processing:
- * - First chunk (index 0) resets the buffer
- * - Chunks must arrive sequentially (lastIndex + 1 == currentIndex)
- * - Data is accumulated until all chunks are received
- * - Consumer reads from buffer via getLoadData() (DAT_00482c18 read offset)
- */
-void SmushPlayer::handleLoad(int32 subSize, Common::SeekableReadStream &b) {
- debugC(DEBUG_SMUSH, "SmushPlayer::handleLoad()");
-
- if (subSize < 10) {
- warning("SmushPlayer::handleLoad: chunk too small (%d bytes)", subSize);
- return;
- }
-
- // Read LOAD header
- int16 totalChunks = b.readUint16LE();
- int16 chunkIndex = b.readUint16LE();
- b.skip(6); // Unknown/padding
-
- int32 dataSize = subSize - 10;
-
- debugC(DEBUG_SMUSH, "SmushPlayer::handleLoad: chunk %d/%d, dataSize=%d, bufferOffset=%d",
- chunkIndex, totalChunks, dataSize, _loadBufferOffset);
-
- // First chunk in sequence - reset buffer state
- if (chunkIndex == 0) {
- _loadBufferOffset = 0;
- _loadReadOffset = 8; // Original skips 8 bytes at start (header in accumulated data?)
- _lastLoadChunkIdx = -1;
- _totalLoadChunks = totalChunks;
-
- // Allocate/reallocate buffer if needed
- // LOAD data per chunk varies (up to ~500 bytes observed).
- // Allocate generously to avoid overflow.
- int32 estimatedSize = totalChunks * 600;
- if (_loadBuffer == nullptr || _loadBufferSize < estimatedSize) {
- free(_loadBuffer);
- _loadBufferSize = estimatedSize;
- _loadBuffer = (byte *)malloc(_loadBufferSize);
- if (_loadBuffer == nullptr) {
- warning("SmushPlayer::handleLoad: Failed to allocate %d bytes for LOAD buffer",
- _loadBufferSize);
- _loadBufferSize = 0;
- return;
- }
- debugC(DEBUG_SMUSH, "SmushPlayer::handleLoad: Allocated %d bytes for LOAD buffer",
- _loadBufferSize);
- }
- }
-
- // Check sequential order (original: DAT_00482c3c - sVar1 == -1)
- if (_lastLoadChunkIdx + 1 != chunkIndex) {
- debugC(DEBUG_SMUSH, "SmushPlayer::handleLoad: Non-sequential chunk %d (expected %d), skipping",
- chunkIndex, _lastLoadChunkIdx + 1);
- return;
- }
-
- // Check buffer capacity (original: DAT_00482c14 + param_2 < DAT_00482c10)
- if (_loadBuffer == nullptr || _loadBufferOffset + dataSize >= _loadBufferSize) {
- warning("SmushPlayer::handleLoad: Buffer overflow - offset=%d size=%d limit=%d",
- _loadBufferOffset, dataSize, _loadBufferSize);
- return;
- }
-
- // Copy data to buffer
- b.read(_loadBuffer + _loadBufferOffset, dataSize);
- _loadBufferOffset += dataSize;
- _lastLoadChunkIdx = chunkIndex;
-
- debugC(DEBUG_SMUSH, "SmushPlayer::handleLoad: Accumulated %d bytes total", _loadBufferOffset);
-
- // Check if sequence is complete
- if (chunkIndex == totalChunks - 1) {
- debugC(DEBUG_SMUSH, "SmushPlayer::handleLoad: Sequence complete - %d chunks, %d bytes total",
- totalChunks, _loadBufferOffset);
- }
-}
-
void SmushPlayer::handleIACT(int32 subSize, Common::SeekableReadStream &b) {
debugC(DEBUG_SMUSH, "SmushPlayer::IACT()");
assert(subSize >= 8);
- // Embedded SAN detection moved to InsaneRebel2::iactRebel2Scene1
- // (previously handled here in SmushPlayer; centralized to the engine)
-
-
int code = b.readUint16LE();
int flags = b.readUint16LE();
int unknown = b.readSint16LE();
int userId = b.readUint16LE();
- debug("SmushPlayer::handleIACT: code=%d flags=%d unknown=%d userId=%d subSize=%d",
+ debugC(DEBUG_SMUSH, "SmushPlayer::handleIACT: code=%d flags=%d unknown=%d userId=%d subSize=%d",
code, flags, unknown, userId, subSize);
- // Route to procIACT for:
- // 1. Non-audio IACT (code != 8 or flags != 46) - Full Throttle uses code=8, flags=46 for audio
- // 2. ALL Rebel Assault 2 IACTs - RA2 uses a different IACT format where code=opcode, flags=par2
- // RA2 audio is handled through PSAD chunks, not IACT, so all RA2 IACTs go to procIACT
+ // Route to procIACT for non-audio IACTs and ALL RA2 IACTs
+ // (RA2 audio uses PSAD chunks, not IACT)
bool isAudioIACT = (code == 8) && (flags == 46);
- bool isRA2 = (_vm->_game.id == GID_REBEL2);
- if (!isAudioIACT || isRA2) {
- debug("SmushPlayer::handleIACT: Routing to procIACT (isAudioIACT=%d, isRA2=%d)",
- isAudioIACT, isRA2);
- // Pass subSize - 8 as the remaining payload size (after 8-byte header)
+ if (!isAudioIACT || isRA2()) {
_vm->_insane->procIACT(_dst, 0, 0, 0, b, subSize - 8, 0, code, flags, unknown, userId);
return;
}
@@ -847,7 +646,7 @@ void SmushPlayer::handleTextResource(uint32 subType, int32 subSize, Common::Seek
// RA2: The original game always shows subtitle text during cinematics
// (there is no subtitle toggle in the retail options menu). Skip
// this check so TRES text is always rendered.
- if (_vm->_game.id != GID_REBEL2 && (!ConfMan.getBool("subtitles")) && ((flags & 8) == 8))
+ if (!isRA2() && (!ConfMan.getBool("subtitles")) && ((flags & 8) == 8))
return;
bool isCJKComi = (_vm->_game.id == GID_CMI && _vm->_useCJKMode);
@@ -915,28 +714,9 @@ void SmushPlayer::handleTextResource(uint32 subType, int32 subSize, Common::Seek
// bit 7 - skip ^ codes (COMI) 0x80 (should be irrelevant for Smush, we strip these commands anyway)
// bit 8 - no vertical fix (COMI) 0x100 (COMI handles this in the printing method, but I haven't seen a case where it is used)
- // For Rebel Assault 2, use SmushMultiFont to support inline font switching via ^fXX codes.
- // The original game uses a linked list of fonts and switches between them mid-string.
- // Other games (FT, DIG, CMI) only use ^f codes at the start of strings.
- if (_vm->_game.id == GID_REBEL2) {
- // Create multi-font renderer on first use
- if (!_multiFont) {
- _multiFont = new SmushMultiFont(_vm, this, true);
- }
- _multiFont->setDefaultFont(fontId);
-
- debug("SmushPlayer::handleTextResource: RA2 TRES frame=%d fontId=%d color=%d flags=0x%x flg=%d pos=(%d,%d) clip=(%d,%d,%d,%d) str=\"%.40s\"",
- _frame, fontId, color, flags, (int)flg, pos_x, pos_y, left, top, width, height, str);
-
- if (flg & kStyleWordWrap) {
- Common::Rect clipRect(MAX<int>(0, left), MAX<int>(0, top), MIN<int>(left + width, _width), MIN<int>(top + height, _height));
- _multiFont->drawStringWrap(str, _dst, clipRect, pos_x, pos_y, color, flg);
- } else {
- Common::Rect clipRect(0, 0, _width, _height);
- _multiFont->drawString(str, _dst, clipRect, pos_x, pos_y, color, flg);
- }
+ if (isRA2()) {
+ ra2HandleTextResource(str, fontId, color, pos_x, pos_y, left, top, width, height, flg);
} else {
- // For other games, use single font (original behavior)
SmushFont *sf = getFont(fontId);
assert(sf != nullptr);
@@ -1044,9 +824,6 @@ byte *SmushPlayer::getVideoPalette() {
void smushDecodeRLE(byte *dst, const byte *src, int left, int top, int width, int height, int pitch);
void smushDecodeUncompressed(byte *dst, const byte *src, int left, int top, int width, int height, int pitch);
-void smushDecodeLineUpdate(byte *dst, const byte *src, int left, int top, int width, int height, int pitch);
-void smushDecodeSkipRLE(byte *dst, const byte *src, int left, int top, int width, int height, int pitch);
-void smushDecodeRA2Bomp(byte *dst, const byte *src, int left, int top, int width, int height, int pitch, int dataSize);
void SmushPlayer::decodeFrameObject(int codec, const uint8 *src, int left, int top, int width, int height, int dataSize) {
if ((height == 242) && (width == 384)) {
@@ -1057,62 +834,8 @@ void SmushPlayer::decodeFrameObject(int codec, const uint8 *src, int left, int t
_specialBufferSize = bufSize;
}
_dst = _specialBuffer;
- } else if (_vm->_game.id == GID_REBEL2 && ((height != _vm->_screenHeight) || (width != _vm->_screenWidth))) {
- // Rebel2 uses SKIP chunks to conditionally skip FOBJ frames for destroyed enemies.
- // The SKIP chunk mechanism (via procSKIP -> _skipNext) is checked at the START
- // of handleFrameObject(), so destroyed enemy sprites are already skipped before
- // reaching this point. No additional skip logic needed here.
- //
- // Rebel2 uses a special buffer for all non-matching frames.
- // Level 1: First frame is 424x260 (background), small sprites reuse same buffer
- // Level 2: Uses virtual screen directly (handled below when _specialBuffer stays null
- // because first frames are small and don't need oversized buffer)
- // Only allocate/expand buffer for frames LARGER than current buffer or screen
- int bufSize = width * height;
- if (bufSize > _vm->_screenWidth * _vm->_screenHeight) {
- // Frame is larger than screen - need special buffer
- if (_specialBuffer == nullptr || bufSize > _specialBufferSize) {
- free(_specialBuffer);
- _specialBuffer = (byte *)malloc(bufSize);
- _specialBufferSize = bufSize;
- _width = width;
- _height = height;
- }
- }
- // Use special buffer ONLY for oversized frames that need it.
- // Small enemy sprites (like Level 2's 9x38 stormtroopers) should draw
- // directly to the virtual screen, not to _specialBuffer.
- // The special buffer is only needed when the CURRENT frame is larger than screen.
- if (bufSize > _vm->_screenWidth * _vm->_screenHeight &&
- _specialBuffer != nullptr && _specialBufferSize >= bufSize) {
- _dst = _specialBuffer;
- debug("SmushPlayer: Using _specialBuffer for oversized FOBJ %dx%d", width, height);
- } else {
- // For small RA2 sprites, check if we should use _specialBuffer or virtual screen.
- //
- // If _specialBuffer was allocated in this video (by a larger frame like Level 1's
- // 424x260 background), small sprites should use it too so everything composites
- // in the same buffer.
- //
- // If _specialBuffer is null (no large frames in this video, like Level 2), small
- // sprites should use the virtual screen directly.
- //
- // This is important because release() frees _specialBuffer at the end of each video,
- // so a new video starts with _specialBuffer = nullptr. Without this check, small
- // sprites in Level 2 could incorrectly use a stale _specialBuffer pointer (though
- // release() should have freed it, init() might not reset _dst if video flags are set).
- if (_specialBuffer == nullptr) {
- VirtScreen *vs = &_vm->_virtscr[kMainVirtScreen];
- _dst = vs->getPixels(0, 0);
- debug("SmushPlayer: Reset _dst to virtual screen for FOBJ %dx%d at (%d,%d) _dst=%p",
- width, height, left, top, (void*)_dst);
- } else {
- // Large frame was in this video, use _specialBuffer for compositing
- _dst = _specialBuffer;
- debug("SmushPlayer: Using _specialBuffer for small FOBJ %dx%d (compositing with large frame)",
- width, height);
- }
- }
+ } else if (isRA2() && ((height != _vm->_screenHeight) || (width != _vm->_screenWidth))) {
+ ra2SelectFrameBuffer(width, height);
} else if ((height > _vm->_screenHeight) || (width > _vm->_screenWidth))
return;
// FT Insane uses smaller frames to draw overlays with moving objects
@@ -1124,10 +847,8 @@ void SmushPlayer::decodeFrameObject(int codec, const uint8 *src, int left, int t
if ((height == 242) && (width == 384)) {
_width = width;
_height = height;
- } else if (_vm->_game.id == GID_REBEL2 && ((height != _vm->_screenHeight) || (width != _vm->_screenWidth))) {
- // Do not update _width/_height here - preserve original video size set during
- // buffer allocation. Small overlay sprites (e.g., 8x7) need to use the background
- // frame's pitch (424) to be drawn at the correct position in the buffer.
+ } else if (isRA2() && ((height != _vm->_screenHeight) || (width != _vm->_screenWidth))) {
+ // RA2: preserve _width/_height set during buffer allocation
} else {
_width = _vm->_screenWidth;
_height = _vm->_screenHeight;
@@ -1137,31 +858,11 @@ void SmushPlayer::decodeFrameObject(int codec, const uint8 *src, int left, int t
if (_dst == _specialBuffer)
pitch = _width;
- // RA2: Apply global FOBJ position offsets (matching original FUN_00423A50)
- // These are set by InsaneRebel2 during IACT opcode 6 processing
- if (_vm->_game.id == GID_REBEL2) {
- left += _fobjOffsetX;
- top += _fobjOffsetY;
- }
-
- // Bounds check: clamp FOBJ to destination buffer dimensions
- int bufHeight = (_dst == _specialBuffer) ? _height : _vm->_screenHeight;
- if (top < 0) {
- height += top;
- top = 0;
- }
- if (left < 0) {
- width += left;
- left = 0;
- }
- if (top + height > bufHeight) {
- height = bufHeight - top;
- }
- if (left + width > pitch) {
- width = pitch - left;
+ if (isRA2()) {
+ ra2AdjustFrameCoords(left, top, width, height, pitch);
+ if (width <= 0 || height <= 0)
+ return;
}
- if (width <= 0 || height <= 0)
- return;
switch (codec) {
case SMUSH_CODEC_RLE:
@@ -1181,25 +882,12 @@ void SmushPlayer::decodeFrameObject(int codec, const uint8 *src, int left, int t
_deltaGlyphsCodec->decode(_dst, src);
break;
case SMUSH_CODEC_UNCOMPRESSED:
- // Used by Full Throttle Classic (from Remastered)
smushDecodeUncompressed(_dst, src, left, top, width, height, _vm->_screenWidth);
break;
- case SMUSH_CODEC_LINE_UPDATE:
- case SMUSH_CODEC_LINE_UPDATE2:
- // RA2: Skip/copy with literal pixels (used for fonts and HUD overlays)
- smushDecodeLineUpdate(_dst, src, left, top, width, height, pitch);
- break;
- case SMUSH_CODEC_SKIP_RLE:
- // RA2: Skip/copy with embedded RLE (used for HUD frames with transparency)
- smushDecodeSkipRLE(_dst, src, left, top, width, height, pitch);
- break;
- case SMUSH_CODEC_RA2_BOMP:
- // RA2: BOMP RLE with variable header (used for small animation elements)
- smushDecodeRA2Bomp(_dst, src, left, top, width, height, pitch, dataSize);
- break;
default:
- if (_vm->_game.id == GID_REBEL2) {
- // Rebel Assault 2 may have other unknown codecs
+ if (isRA2() && ra2DecodeCodec(codec, src, left, top, width, height, pitch, dataSize))
+ break;
+ if (isRA2()) {
debugC(DEBUG_SMUSH, "SmushPlayer::decodeFrameObject: Skipping unknown codec %d (left=%d, top=%d, %dx%d)",
codec, left, top, width, height);
break;
@@ -1207,9 +895,7 @@ void SmushPlayer::decodeFrameObject(int codec, const uint8 *src, int left, int t
error("Invalid codec for frame object : %d", codec);
}
- // For non-RA2 games, save rendered bitmap when STOR is pending.
- // RA2 handles STOR in handleFrameObject by saving raw FOBJ data instead.
- if (_storeFrame && _vm->_game.id != GID_REBEL2) {
+ if (_storeFrame && !isRA2()) {
if (_frameBuffer == nullptr) {
_frameBuffer = (byte *)malloc(_width * _height);
}
@@ -1251,7 +937,7 @@ void SmushPlayer::handleZlibFrameObject(int32 subSize, Common::SeekableReadStrea
void SmushPlayer::handleFrameObject(int32 subSize, Common::SeekableReadStream &b) {
assert(subSize >= 14);
if (_skipNext) {
- debug("SmushPlayer::handleFrameObject: SKIPPING due to _skipNext, frame=%d", _frame);
+ debugC(DEBUG_SMUSH, "SmushPlayer::handleFrameObject: SKIPPING due to _skipNext, frame=%d", _frame);
_skipNext = false;
return;
}
@@ -1265,7 +951,7 @@ void SmushPlayer::handleFrameObject(int32 subSize, Common::SeekableReadStream &b
b.readUint16LE();
b.readUint16LE();
- debug("SmushPlayer::handleFrameObject: frame=%d codec=%d pos=(%d,%d) size=%dx%d dataSize=%d",
+ debugC(DEBUG_SMUSH, "SmushPlayer::handleFrameObject: frame=%d codec=%d pos=(%d,%d) size=%dx%d dataSize=%d",
_frame, codec, left, top, width, height, subSize - 14);
int32 chunk_size = subSize - 14;
@@ -1273,22 +959,8 @@ void SmushPlayer::handleFrameObject(int32 subSize, Common::SeekableReadStream &b
assert(chunk_buffer);
b.read(chunk_buffer, chunk_size);
- // RA2: When STOR is pending, save raw FOBJ data for later re-decoding by FTCH.
- // The original (FUN_00423A50 bVar5) stores the next FOBJ's raw chunk data in
- // DAT_00482c04. FTCH then re-renders from this stored data with current FOBJ
- // offsets. This is critical for O_LEVEL.SAN where the stored FOBJ is the 80x800
- // preview strip, and FTCH must re-render it at the current scroll offset.
- if (_storeFrame && _vm->_game.id == GID_REBEL2) {
- free(_storedFobjData);
- _storedFobjData = (byte *)malloc(chunk_size);
- memcpy(_storedFobjData, chunk_buffer, chunk_size);
- _storedFobjDataSize = chunk_size;
- _storedFobjCodec = codec;
- _storedFobjLeft = left;
- _storedFobjTop = top;
- _storedFobjWidth = width;
- _storedFobjHeight = height;
- _storeFrame = false;
+ if (_storeFrame && isRA2()) {
+ ra2StoreFobjData(codec, chunk_buffer, chunk_size, left, top, width, height);
}
decodeFrameObject(codec, chunk_buffer, left, top, width, height, chunk_size);
@@ -1310,19 +982,10 @@ void SmushPlayer::handleFrame(int32 frameSize, Common::SeekableReadStream &b) {
const int32 subSize = b.readUint32BE();
const int32 subOffset = b.pos();
- // Debug: Log all chunk types for first 25 frames
- if (_vm->_game.id == GID_REBEL2 && _frame < 25) {
- debug("SmushPlayer::handleFrame: frame=%d chunk=%s size=%d", _frame, tag2str(subType), subSize);
- }
-
- // RA2 SKIP mechanism (matching original FUN_00423A50 bVar6):
- // When _skipNext is set, skip the NEXT chunk of ANY type (FOBJ, PSAD, SKIP, etc.)
- // In the original, bVar6 is checked at the top of the loop before the type switch,
- // corrupting the tag so no handler matches. This consumes exactly one chunk.
- // Critical: this must also skip SKIP chunks to prevent SKIPâSKIPâFOBJ misalignment.
- if (_vm->_game.id == GID_REBEL2 && _skipNext) {
+ // RA2: When _skipNext is set, skip the NEXT chunk of ANY type
+ if (isRA2() && _skipNext) {
_skipNext = false;
- debug("SmushPlayer::handleFrame: SKIP consumed chunk %s frame=%d", tag2str(subType), _frame);
+ debugC(DEBUG_SMUSH, "SmushPlayer::handleFrame: SKIP consumed chunk %s frame=%d", tag2str(subType), _frame);
frameSize -= subSize + 8;
b.seek(subOffset + subSize, SEEK_SET);
if (subSize & 1) {
@@ -1400,15 +1063,8 @@ void SmushPlayer::handleFrame(int32 frameSize, Common::SeekableReadStream &b) {
_vm->_insane->procPostRendering(_dst, 0, 0, 0, _frame, _nbframes-1);
}
- // Debug: Check if updateScreen is being called
- if (_vm->_game.id == GID_REBEL2 && _frame < 3) {
- debug("SmushPlayer: frame=%d _width=%d _height=%d _dst=%p", _frame, _width, _height, (void*)_dst);
- }
-
if (_width != 0 && _height != 0) {
updateScreen();
- } else if (_vm->_game.id == GID_REBEL2 && _frame < 3) {
- debug("SmushPlayer: SKIPPING updateScreen (width=%d height=%d)", _width, _height);
}
_frame++;
@@ -1416,7 +1072,6 @@ void SmushPlayer::handleFrame(int32 frameSize, Common::SeekableReadStream &b) {
void SmushPlayer::handleAnimHeader(int32 subSize, Common::SeekableReadStream &b) {
debugC(DEBUG_SMUSH, "SmushPlayer::handleAnimHeader()");
- debug("SmushPlayer::handleAnimHeader: subSize=%d", subSize);
assert(subSize >= 0x300 + 6);
byte *headerContent = (byte *)malloc(subSize * sizeof(byte));
@@ -1447,31 +1102,17 @@ void SmushPlayer::handleAnimHeader(int32 subSize, Common::SeekableReadStream &b)
byte *palettePtr = &headerContent[6];
memcpy(_pal, palettePtr, sizeof(_pal));
- // Reset XPAL delta palette state from the new base palette.
- // Without this, stale _deltaPal/_shiftedDeltaPal values from a
- // previous video leak into the new one, corrupting the palette
- // when XPAL command 256 (apply deltas) is encountered.
- for (int j = 0; j < 768; ++j) {
- _shiftedDeltaPal[j] = _pal[j] << 7;
- }
- memset(_deltaPal, 0, sizeof(_deltaPal));
+ if (isRA2())
+ ra2ResetDeltaPalette();
setDirtyColors(0, 255);
}
_width = READ_LE_UINT16(&headerContent[4]);
_height = READ_LE_UINT16(&headerContent[6]);
- debug("SmushPlayer::handleAnimHeader: nbframes=%d width=%d height=%d", _nbframes, _width, _height);
-
- // RA2 menu videos (O_MENU*.SAN) report width/height=0 in AHDR, but they DO have
- // FOBJ frames with full 320x200 images. The FOBJ chunks contain the correct dimensions.
- // We set default screen dimensions here so updateScreen() gets called and the
- // _frameBuffer allocation in handleStore/handleFetch uses correct size.
- if (_vm->_game.id == GID_REBEL2 && _width == 0 && _height == 0) {
- _width = _vm->_screenWidth; // 320
- _height = _vm->_screenHeight; // 200
- debug("SmushPlayer::handleAnimHeader: RA2 AHDR has 0x0 dims - using screen size %dx%d", _width, _height);
- }
+
+ if (isRA2())
+ ra2FixupAnimHeader();
free(headerContent);
}
@@ -1479,15 +1120,8 @@ void SmushPlayer::handleAnimHeader(int32 subSize, Common::SeekableReadStream &b)
void SmushPlayer::setupAnim(const char *file) {
if (_insanity) {
- if (_vm->_game.id == GID_REBEL2) {
- // Rebel Assault 2 uses SYSTM/GAME.TRS for all subtitle strings
- // The TRS file is ETRS-encoded (XOR with 0xCC)
+ if (isRA2()) {
_strings = getStrings(_vm, "SYSTM/GAME.TRS", true);
- if (_strings) {
- debugC(DEBUG_SMUSH, "SmushPlayer::setupAnim: Loaded GAME.TRS string resources successfully");
- } else {
- debugC(DEBUG_SMUSH, "SmushPlayer::setupAnim: Failed to load GAME.TRS!");
- }
} else if (!((_vm->_game.features & GF_DEMO) && (_vm->_game.platform == Common::kPlatformDOS))) {
readString("mineroad.trs");
}
@@ -1518,26 +1152,8 @@ SmushFont *SmushPlayer::getFont(int font) {
_sf[font] = new SmushFont(_vm, ft_fonts[font], true);
}
- } else if (_vm->_game.id == GID_REBEL2) {
- // Rebel Assault 2 fonts:
- // font 0: TALKFONT.NUT - main dialog/subtitle font
- // font 1: DIHIFONT.NUT - high-res dialog font
- // font 2: TITLFONT.NUT - title font
- // font 3: SMALFONT.NUT - small font for HUD
- const char *ra2_fonts[] = {
- "SYSTM/TALKFONT.NUT",
- "SYSTM/DIHIFONT.NUT",
- "SYSTM/TITLFONT.NUT",
- "SYSTM/SMALFONT.NUT"
- };
- int numFonts = ARRAYSIZE(ra2_fonts);
- if (font >= 0 && font < numFonts) {
- _sf[font] = new SmushFont(_vm, ra2_fonts[font], true);
- } else {
- // Fallback to font 0 for unknown font indices
- debugC(DEBUG_SMUSH, "SmushPlayer::getFont: RA2 unknown font %d, using TALKFONT", font);
- _sf[font] = new SmushFont(_vm, ra2_fonts[0], true);
- }
+ } else if (isRA2()) {
+ return ra2GetFont(font);
} else {
int numFonts = (_vm->_game.id == GID_CMI && !(_vm->_game.features & GF_DEMO)) ? 5 : 4;
assert(font >= 0 && font < numFonts);
@@ -1627,12 +1243,8 @@ void SmushPlayer::parseNextFrame() {
if (_vm->_imuseDigital)
_vm->_imuseDigital->flushTracks();
- // Rebel Assault 2 audio processing - call processDispatches directly since no iMUSE
- if (!_imuseDigital && _vm->_game.id == GID_REBEL2) {
- // Use a feed size based on frame rate (similar to iMUSE)
- // 11025 Hz / 12 fps = ~918 samples per frame
- processDispatches(_smushAudioSampleRate / 12);
- }
+ if (!_imuseDigital && isRA2())
+ ra2ParseNextFrame();
}
void SmushPlayer::setPalette(const byte *palette) {
@@ -1749,9 +1361,8 @@ void SmushPlayer::play(const char *filename, int32 speed, int32 offset, int32 st
// Hide mouse
bool oldMouseState = CursorMan.showMouse(false);
- if (_vm->_game.id == GID_REBEL2) {
+ if (isRA2())
insanity(true);
- }
// Load the video
_seekFile = filename;
@@ -1908,7 +1519,6 @@ void SmushPlayer::play(const char *filename, int32 speed, int32 offset, int32 st
void SmushPlayer::initAudio(int samplerate, int32 maxChunkSize) {
int32 maxSizes[SMUSH_MAX_TRACKS] = {100000, 100000, 100000, 400000};
- // Rebel Assault 2 doesn't use iMUSE for audio
if (_imuseDigital)
_imuseDigital->setSmushPlayer(this);
@@ -2206,9 +1816,8 @@ void SmushPlayer::processDispatches(int16 feedSize) {
bool isPlayableTrack;
bool speechIsPlaying = false;
- // Rebel Assault 2 doesn't use iMUSE for audio - use InsaneRebel2's audio handler
if (!_imuseDigital) {
- if (_vm->_game.id == GID_REBEL2 && _insane) {
+ if (isRA2() && _insane) {
InsaneRebel2 *rebel2 = static_cast<InsaneRebel2 *>(_insane);
rebel2->processAudioFrame(feedSize);
}
@@ -2644,7 +2253,6 @@ bool SmushPlayer::processAudioCodes(int idx, int32 &tmpFeedSize, int &mixVolume)
}
void SmushPlayer::sendAudioToDiMUSE(uint8 *mixBuf, int32 mixStartingPoint, int32 mixFeedSize, int32 mixInFrameCount, int volume, int pan) {
- // Rebel Assault 2 doesn't use iMUSE for audio
if (!_imuseDigital)
return;
@@ -2716,36 +2324,4 @@ bool SmushPlayer::isAudioCallbackEnabled() {
return _smushAudioCallbackEnabled;
}
-// Only used by Rebel Assault 2
-void SmushPlayer::addMaskedRegion(const Common::Rect &rect) {
- // Check if the region already exists
- for (Common::List<Common::Rect>::iterator it = _maskedRegions.begin(); it != _maskedRegions.end(); ++it) {
- if (*it == rect) {
- return; // Already exists
- }
- }
- _maskedRegions.push_back(rect);
-}
-
-// Only used by Rebel Assault 2
-void SmushPlayer::removeMaskedRegion(const Common::Rect &rect) {
- for (Common::List<Common::Rect>::iterator it = _maskedRegions.begin(); it != _maskedRegions.end(); ++it) {
- if (*it == rect) {
- _maskedRegions.erase(it);
- return;
- }
- }
-}
-
-// Only used by Rebel Assault 2
-void SmushPlayer::clearMaskedRegions() {
- _maskedRegions.clear();
-}
-
-// Only used by Rebel Assault 2
-void SmushPlayer::setScrollOffset(int x, int y) {
- _scrollX = x;
- _scrollY = y;
-}
-
} // End of namespace Scumm
diff --git a/engines/scumm/smush/smush_player.h b/engines/scumm/smush/smush_player.h
index 2ebd816193b..85d6b60edbd 100644
--- a/engines/scumm/smush/smush_player.h
+++ b/engines/scumm/smush/smush_player.h
@@ -25,6 +25,7 @@
#include "common/util.h"
#include "common/list.h"
#include "common/rect.h"
+#include "scumm/charset_v7.h"
namespace Audio {
class SoundHandle;
@@ -289,9 +290,30 @@ private:
void handleIACT(int32 subSize, Common::SeekableReadStream &);
void handleTextResource(uint32 subType, int32 subSize, Common::SeekableReadStream &);
void handleDeltaPalette(int32 subSize, Common::SeekableReadStream &);
- void handleLoad(int32 subSize, Common::SeekableReadStream &);
+ void handleLoad(int32 subSize, Common::SeekableReadStream &); // RA2 only (impl in smush_player_ra2.cpp)
void readPalette(byte *, Common::SeekableReadStream &);
+ // RA2-specific methods (implemented in smush_player_ra2.cpp)
+ bool isRA2() const;
+ void ra2InitFields();
+ void ra2DestroyFields();
+ void ra2InitVideo();
+ void ra2ReleaseVideo();
+ void ra2HandleFetch(Common::SeekableReadStream &b);
+ void ra2HandleTextResource(const char *str, int fontId, int color,
+ int pos_x, int pos_y, int left, int top,
+ int width, int height, TextStyleFlags flg);
+ void ra2SelectFrameBuffer(int width, int height);
+ void ra2AdjustFrameCoords(int &left, int &top, int &width, int &height, int pitch);
+ bool ra2DecodeCodec(int codec, const uint8 *src, int left, int top,
+ int width, int height, int pitch, int dataSize);
+ void ra2StoreFobjData(int codec, const byte *data, int32 dataSize,
+ int left, int top, int width, int height);
+ void ra2ResetDeltaPalette();
+ SmushFont *ra2GetFont(int font);
+ void ra2ParseNextFrame();
+ void ra2FixupAnimHeader();
+
// LOAD chunk streaming buffer (RA2 - embedded resource data)
byte *_loadBuffer; // Accumulated LOAD data
int32 _loadBufferSize; // Allocated buffer size
diff --git a/engines/scumm/smush/smush_player_ra2.cpp b/engines/scumm/smush/smush_player_ra2.cpp
new file mode 100644
index 00000000000..198ca7c200f
--- /dev/null
+++ b/engines/scumm/smush/smush_player_ra2.cpp
@@ -0,0 +1,449 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+// Rebel Assault 2 specific SmushPlayer methods
+//
+// These are methods of the SmushPlayer class that contain RA2-specific logic.
+// Keeping them in a separate file minimizes the diff on smush_player.cpp and
+// reduces the risk of regressions in Full Throttle / The Dig / CMI.
+
+#include "common/endian.h"
+#include "common/rect.h"
+#include "common/system.h"
+
+#include "scumm/scumm.h"
+#include "scumm/scumm_v7.h"
+#include "scumm/smush/smush_font.h"
+#include "scumm/smush/smush_multi_font.h"
+#include "scumm/smush/smush_player.h"
+
+#include "scumm/insane/insane.h"
+#include "scumm/insane/insane_rebel.h"
+
+namespace Scumm {
+
+bool SmushPlayer::isRA2() const {
+ return _vm->_game.id == GID_REBEL2;
+}
+
+// Forward declarations for RA2 codec functions (defined in codec_ra2.cpp)
+void smushDecodeLineUpdate(byte *dst, const byte *src, int left, int top, int width, int height, int pitch);
+void smushDecodeSkipRLE(byte *dst, const byte *src, int left, int top, int width, int height, int pitch);
+void smushDecodeRA2Bomp(byte *dst, const byte *src, int left, int top, int width, int height, int pitch, int dataSize);
+
+/**
+ * Initialize RA2-specific fields in the SmushPlayer constructor.
+ */
+void SmushPlayer::ra2InitFields() {
+ _multiFont = nullptr;
+ _storedFobjData = nullptr;
+ _storedFobjDataSize = 0;
+ _storedFobjCodec = 0;
+ _storedFobjLeft = 0;
+ _storedFobjTop = 0;
+ _storedFobjWidth = 0;
+ _storedFobjHeight = 0;
+ _skipNext = false;
+ _ra2FastForwarding = false;
+ _fobjOffsetX = 0;
+ _fobjOffsetY = 0;
+ _storeFrame = false;
+ _loadBuffer = nullptr;
+ _loadBufferSize = 0;
+ _loadBufferOffset = 0;
+ _loadReadOffset = 8; // Original starts reading at offset 8 (skips header)
+ _lastLoadChunkIdx = -1;
+ _totalLoadChunks = 0;
+ _scrollX = 0;
+ _scrollY = 0;
+}
+
+/**
+ * Free RA2-specific resources in the SmushPlayer destructor.
+ */
+void SmushPlayer::ra2DestroyFields() {
+ delete _multiFont;
+ _multiFont = nullptr;
+ free(_storedFobjData);
+ _storedFobjData = nullptr;
+ free(_loadBuffer);
+ _loadBuffer = nullptr;
+}
+
+/**
+ * RA2-specific initialization in SmushPlayer::init().
+ * Re-pushes the SMUSH palette (videos without NPAL inherit from previous),
+ * and handles background preservation between cinematic and gameplay videos.
+ */
+void SmushPlayer::ra2InitVideo() {
+ // Re-push the SMUSH palette to the system. Videos like O_LEVEL.SAN
+ // have no NPAL chunk and inherit the palette from the previous video.
+ // Since play() resets _palDirtyMin/Max, the palette would never be pushed otherwise.
+ setDirtyColors(0, 255);
+
+ // Handle background preservation between videos:
+ // - Cinematic videos (flags 0x20) clear the buffer for a fresh start
+ // - Gameplay videos (flags 0x28) preserve the existing screen content
+ if (_dst != nullptr) {
+ VirtScreen *vs = &_vm->_virtscr[kMainVirtScreen];
+ if ((_curVideoFlags & 0x08) == 0) {
+ // Cinematic mode (flags 0x20) - clear buffer for fresh video
+ memset(_dst, 0, vs->w * vs->h);
+ } else {
+ // Gameplay mode (flags 0x28) - do nothing, preserve existing screen content
+ int nonZero = 0;
+ for (int i = 0; i < vs->w * vs->h; i++) {
+ if (_dst[i] != 0) nonZero++;
+ }
+ debug("SmushPlayer::init: Preserving screen for gameplay video (%dx%d, %d%% non-zero)",
+ vs->w, vs->h, (nonZero * 100) / (vs->w * vs->h));
+ }
+ }
+}
+
+/**
+ * RA2-specific cleanup in SmushPlayer::release().
+ * Frees stored FOBJ data but preserves _frameBuffer across videos.
+ */
+void SmushPlayer::ra2ReleaseVideo() {
+ free(_storedFobjData);
+ _storedFobjData = nullptr;
+ _storedFobjDataSize = 0;
+ // Preserve _frameBuffer across videos so that gameplay videos (which have no
+ // background FOBJ) can use the stored background from the previous BEG video.
+}
+
+/**
+ * RA2-specific FTCH handling.
+ * For Handler 25, skips FTCH to preserve overlays.
+ * For other handlers, re-decodes stored FOBJ with current offsets.
+ */
+void SmushPlayer::ra2HandleFetch(Common::SeekableReadStream &b) {
+ int16 ftchUnknown = b.readSint16LE();
+ int16 ftchX = b.readSint16LE();
+ int16 ftchY = b.readSint16LE();
+
+ debug("SmushPlayer::handleFetch: frame=%d unknown=%d x=%d y=%d",
+ _frame, ftchUnknown, ftchX, ftchY);
+
+ // For Handler 25, skip FTCH because the frame buffer only contains the
+ // par4=5 base background without the overlays (par4=4, 6, 7) that were drawn
+ // immediately in frame 0. Restoring would erase those overlays.
+ if (_insane != nullptr) {
+ InsaneRebel2 *rebel2 = static_cast<InsaneRebel2 *>(_insane);
+ int handler = rebel2->getHandler();
+ if (handler == 25) {
+ debug("SmushPlayer::handleFetch: Skipping FTCH for Handler 25 - preserving overlays");
+ return;
+ }
+ }
+
+ // Re-decode stored FOBJ data with current offsets (matching original FUN_004246d0).
+ if (_storedFobjData != nullptr) {
+ decodeFrameObject(_storedFobjCodec, _storedFobjData,
+ _storedFobjLeft, _storedFobjTop,
+ _storedFobjWidth, _storedFobjHeight,
+ _storedFobjDataSize);
+ }
+}
+
+/**
+ * Handle LOAD chunk for Rebel Assault 2.
+ *
+ * LOAD chunks stream embedded resource data across multiple frames.
+ * The data is accumulated in a buffer and consumed by the audio system.
+ */
+void SmushPlayer::handleLoad(int32 subSize, Common::SeekableReadStream &b) {
+ debugC(DEBUG_SMUSH, "SmushPlayer::handleLoad()");
+
+ if (subSize < 10) {
+ warning("SmushPlayer::handleLoad: chunk too small (%d bytes)", subSize);
+ return;
+ }
+
+ int16 totalChunks = b.readUint16LE();
+ int16 chunkIndex = b.readUint16LE();
+ b.skip(6); // Unknown/padding
+
+ int32 dataSize = subSize - 10;
+
+ debugC(DEBUG_SMUSH, "SmushPlayer::handleLoad: chunk %d/%d, dataSize=%d, bufferOffset=%d",
+ chunkIndex, totalChunks, dataSize, _loadBufferOffset);
+
+ // First chunk in sequence - reset buffer state
+ if (chunkIndex == 0) {
+ _loadBufferOffset = 0;
+ _loadReadOffset = 8;
+ _lastLoadChunkIdx = -1;
+ _totalLoadChunks = totalChunks;
+
+ int32 estimatedSize = totalChunks * 600;
+ if (_loadBuffer == nullptr || _loadBufferSize < estimatedSize) {
+ free(_loadBuffer);
+ _loadBufferSize = estimatedSize;
+ _loadBuffer = (byte *)malloc(_loadBufferSize);
+ if (_loadBuffer == nullptr) {
+ warning("SmushPlayer::handleLoad: Failed to allocate %d bytes for LOAD buffer",
+ _loadBufferSize);
+ _loadBufferSize = 0;
+ return;
+ }
+ debugC(DEBUG_SMUSH, "SmushPlayer::handleLoad: Allocated %d bytes for LOAD buffer",
+ _loadBufferSize);
+ }
+ }
+
+ // Check sequential order
+ if (_lastLoadChunkIdx + 1 != chunkIndex) {
+ debugC(DEBUG_SMUSH, "SmushPlayer::handleLoad: Non-sequential chunk %d (expected %d), skipping",
+ chunkIndex, _lastLoadChunkIdx + 1);
+ return;
+ }
+
+ // Check buffer capacity
+ if (_loadBuffer == nullptr || _loadBufferOffset + dataSize >= _loadBufferSize) {
+ warning("SmushPlayer::handleLoad: Buffer overflow - offset=%d size=%d limit=%d",
+ _loadBufferOffset, dataSize, _loadBufferSize);
+ return;
+ }
+
+ // Copy data to buffer
+ b.read(_loadBuffer + _loadBufferOffset, dataSize);
+ _loadBufferOffset += dataSize;
+ _lastLoadChunkIdx = chunkIndex;
+
+ debugC(DEBUG_SMUSH, "SmushPlayer::handleLoad: Accumulated %d bytes total", _loadBufferOffset);
+
+ if (chunkIndex == totalChunks - 1) {
+ debugC(DEBUG_SMUSH, "SmushPlayer::handleLoad: Sequence complete - %d chunks, %d bytes total",
+ totalChunks, _loadBufferOffset);
+ }
+}
+
+/**
+ * RA2-specific text rendering using SmushMultiFont for inline font switching.
+ */
+void SmushPlayer::ra2HandleTextResource(const char *str, int fontId, int color,
+ int pos_x, int pos_y, int left, int top,
+ int width, int height, TextStyleFlags flg) {
+ // Create multi-font renderer on first use
+ if (!_multiFont) {
+ _multiFont = new SmushMultiFont(_vm, this, true);
+ }
+ _multiFont->setDefaultFont(fontId);
+
+ debug("SmushPlayer::handleTextResource: RA2 TRES frame=%d fontId=%d color=%d flags=0x%x flg=%d pos=(%d,%d) clip=(%d,%d,%d,%d) str=\"%.40s\"",
+ _frame, fontId, color, (int)flg, (int)flg, pos_x, pos_y, left, top, width, height, str);
+
+ if (flg & kStyleWordWrap) {
+ Common::Rect clipRect(MAX<int>(0, left), MAX<int>(0, top), MIN<int>(left + width, _width), MIN<int>(top + height, _height));
+ _multiFont->drawStringWrap(str, _dst, clipRect, pos_x, pos_y, color, flg);
+ } else {
+ Common::Rect clipRect(0, 0, _width, _height);
+ _multiFont->drawString(str, _dst, clipRect, pos_x, pos_y, color, flg);
+ }
+}
+
+/**
+ * RA2-specific buffer selection for non-standard FOBJ dimensions.
+ * Returns the destination buffer to use and updates _dst, _width, _height.
+ */
+void SmushPlayer::ra2SelectFrameBuffer(int width, int height) {
+ // Rebel2 uses a special buffer for all non-matching frames.
+ // Level 1: First frame is 424x260 (background), small sprites reuse same buffer
+ // Level 2: Uses virtual screen directly (handled below when _specialBuffer stays null)
+ int bufSize = width * height;
+ if (bufSize > _vm->_screenWidth * _vm->_screenHeight) {
+ // Frame is larger than screen - need special buffer
+ if (_specialBuffer == nullptr || bufSize > _specialBufferSize) {
+ free(_specialBuffer);
+ _specialBuffer = (byte *)malloc(bufSize);
+ _specialBufferSize = bufSize;
+ _width = width;
+ _height = height;
+ }
+ }
+
+ if (bufSize > _vm->_screenWidth * _vm->_screenHeight &&
+ _specialBuffer != nullptr && _specialBufferSize >= bufSize) {
+ _dst = _specialBuffer;
+ debug("SmushPlayer: Using _specialBuffer for oversized FOBJ %dx%d", width, height);
+ } else {
+ if (_specialBuffer == nullptr) {
+ VirtScreen *vs = &_vm->_virtscr[kMainVirtScreen];
+ _dst = vs->getPixels(0, 0);
+ debug("SmushPlayer: Reset _dst to virtual screen for FOBJ %dx%d at (%d,%d) _dst=%p",
+ width, height, 0, 0, (void*)_dst);
+ } else {
+ // Large frame was in this video, use _specialBuffer for compositing
+ _dst = _specialBuffer;
+ debug("SmushPlayer: Using _specialBuffer for small FOBJ %dx%d (compositing with large frame)",
+ width, height);
+ }
+ }
+}
+
+/**
+ * Apply RA2 FOBJ position offsets and clamp to buffer bounds.
+ * Modifies left, top, width, height in place.
+ */
+void SmushPlayer::ra2AdjustFrameCoords(int &left, int &top, int &width, int &height, int pitch) {
+ left += _fobjOffsetX;
+ top += _fobjOffsetY;
+
+ int bufHeight = (_dst == _specialBuffer) ? _height : _vm->_screenHeight;
+ if (top < 0) {
+ height += top;
+ top = 0;
+ }
+ if (left < 0) {
+ width += left;
+ left = 0;
+ }
+ if (top + height > bufHeight) {
+ height = bufHeight - top;
+ }
+ if (left + width > pitch) {
+ width = pitch - left;
+ }
+}
+
+/**
+ * Dispatch to RA2-specific codec functions.
+ * Returns true if the codec was handled, false for standard codecs.
+ */
+bool SmushPlayer::ra2DecodeCodec(int codec, const uint8 *src, int left, int top,
+ int width, int height, int pitch, int dataSize) {
+ switch (codec) {
+ case SMUSH_CODEC_LINE_UPDATE:
+ case SMUSH_CODEC_LINE_UPDATE2:
+ smushDecodeLineUpdate(_dst, src, left, top, width, height, pitch);
+ return true;
+ case SMUSH_CODEC_SKIP_RLE:
+ smushDecodeSkipRLE(_dst, src, left, top, width, height, pitch);
+ return true;
+ case SMUSH_CODEC_RA2_BOMP:
+ smushDecodeRA2Bomp(_dst, src, left, top, width, height, pitch, dataSize);
+ return true;
+ default:
+ return false;
+ }
+}
+
+/**
+ * Save raw FOBJ data when STOR is pending (for later re-decoding by FTCH).
+ */
+void SmushPlayer::ra2StoreFobjData(int codec, const byte *data, int32 dataSize,
+ int left, int top, int width, int height) {
+ free(_storedFobjData);
+ _storedFobjData = (byte *)malloc(dataSize);
+ memcpy(_storedFobjData, data, dataSize);
+ _storedFobjDataSize = dataSize;
+ _storedFobjCodec = codec;
+ _storedFobjLeft = left;
+ _storedFobjTop = top;
+ _storedFobjWidth = width;
+ _storedFobjHeight = height;
+ _storeFrame = false;
+}
+
+/**
+ * Reset XPAL delta palette from the current base palette.
+ * Prevents stale delta values from a previous video corrupting the palette.
+ */
+void SmushPlayer::ra2ResetDeltaPalette() {
+ for (int j = 0; j < 768; ++j) {
+ _shiftedDeltaPal[j] = _pal[j] << 7;
+ }
+ memset(_deltaPal, 0, sizeof(_deltaPal));
+}
+
+/**
+ * RA2 font path table.
+ */
+SmushFont *SmushPlayer::ra2GetFont(int font) {
+ const char *ra2_fonts[] = {
+ "SYSTM/TALKFONT.NUT",
+ "SYSTM/DIHIFONT.NUT",
+ "SYSTM/TITLFONT.NUT",
+ "SYSTM/SMALFONT.NUT"
+ };
+ int numFonts = ARRAYSIZE(ra2_fonts);
+ if (font >= 0 && font < numFonts) {
+ _sf[font] = new SmushFont(_vm, ra2_fonts[font], true);
+ } else {
+ debugC(DEBUG_SMUSH, "SmushPlayer::getFont: RA2 unknown font %d, using TALKFONT", font);
+ _sf[font] = new SmushFont(_vm, ra2_fonts[0], true);
+ }
+ return _sf[font];
+}
+
+/**
+ * RA2 per-frame audio processing (called from parseNextFrame).
+ */
+void SmushPlayer::ra2ParseNextFrame() {
+ // Call processDispatches directly since RA2 has no iMUSE
+ // 11025 Hz / 12 fps = ~918 samples per frame
+ processDispatches(_smushAudioSampleRate / 12);
+}
+
+/**
+ * RA2-specific handleAnimHeader fixup: when AHDR reports 0x0 dimensions,
+ * use screen dimensions instead.
+ */
+void SmushPlayer::ra2FixupAnimHeader() {
+ if (_width == 0 && _height == 0) {
+ _width = _vm->_screenWidth; // 320
+ _height = _vm->_screenHeight; // 200
+ debug("SmushPlayer::handleAnimHeader: RA2 AHDR has 0x0 dims - using screen size %dx%d", _width, _height);
+ }
+}
+
+// Masked region management â used by InsaneRebel2
+
+void SmushPlayer::addMaskedRegion(const Common::Rect &rect) {
+ for (Common::List<Common::Rect>::iterator it = _maskedRegions.begin(); it != _maskedRegions.end(); ++it) {
+ if (*it == rect) {
+ return; // Already exists
+ }
+ }
+ _maskedRegions.push_back(rect);
+}
+
+void SmushPlayer::removeMaskedRegion(const Common::Rect &rect) {
+ for (Common::List<Common::Rect>::iterator it = _maskedRegions.begin(); it != _maskedRegions.end(); ++it) {
+ if (*it == rect) {
+ _maskedRegions.erase(it);
+ return;
+ }
+ }
+}
+
+void SmushPlayer::clearMaskedRegions() {
+ _maskedRegions.clear();
+}
+
+void SmushPlayer::setScrollOffset(int x, int y) {
+ _scrollX = x;
+ _scrollY = y;
+}
+
+} // End of namespace Scumm
Commit: 38af869f7c41f6268408b0a7a5f6994f57115697
https://github.com/scummvm/scummvm/commit/38af869f7c41f6268408b0a7a5f6994f57115697
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:11+02:00
Commit Message:
SCUMM: RA2: Improve UI rendering
Changed paths:
engines/scumm/insane/insane_rebel.cpp
engines/scumm/insane/insane_rebel_render.cpp
diff --git a/engines/scumm/insane/insane_rebel.cpp b/engines/scumm/insane/insane_rebel.cpp
index bcfbb8d8c75..2742e9a8bc7 100644
--- a/engines/scumm/insane/insane_rebel.cpp
+++ b/engines/scumm/insane/insane_rebel.cpp
@@ -607,74 +607,39 @@ void InsaneRebel2::addScore(int points) {
debug("Rebel2: Score +%d = %d", points, _playerScore);
}
-// Render score to HUD (part of FUN_0041c012)
-// Score is drawn using FUN_00434cb0 with format string "%07ld"
-// In retail, score is rendered to a status bar buffer, then blitted to screen at Y=180 (0xb4)
-// The text within the status bar is at local Y=4, so screen Y = 180 + 4 = 184
+// Render score text to HUD (part of FUN_0041c012)
+// FUN_0041c012 lines 133-137: calls FUN_00434cb0 with format "%07ld"
+// Position (low-res): X = 0x101 (257), Y = 4 within status bar â screen Y = 184
void InsaneRebel2::renderScoreHUD(byte *renderBitmap, int pitch, int width, int height, int statusBarY) {
- // In retail, score is rendered by FUN_0041c012 which calls FUN_00434cb0
- // The status bar is blitted to screen at Y = DAT_0047ab2c + 0xb4 (typically 0 + 180 = 180)
- // Text position within status bar from FUN_0041c012 line 136-137:
- // X = ((DAT_0047a808 < 2) - 1 & 0x101) + 0x101 = 0x101 (257) for low-res
- // Y = ((DAT_0047a808 < 2) - 1 & 4) + 4 = 4 for low-res
- // So final screen position: X=257, Y=180+4=184
- // Format: 7-digit zero-padded decimal ("%07ld")
-
- (void)statusBarY; // Not used - we use fixed Y positions
-
- // Use SMALFONT.NUT (NutRenderer) for rendering digits
- // If not available, skip rendering
- if (!_smush_dispfontNut) {
- debug(1, "renderScoreHUD: _smush_dispfontNut is NULL!");
- return;
- }
+ (void)statusBarY;
- // The SMUSH buffer is 424x260, but the visible screen is 320x200
- // The view offset (_viewX, _viewY) determines where in the buffer the screen is showing
- // To render at fixed screen positions, we add the view offset
+ if (!_smush_dispfontNut)
+ return;
- // Convert score to 7-digit string
char scoreStr[16];
Common::sprintf_s(scoreStr, "%07d", _playerScore);
- // Status bar is at Y=180 (0xb4), text within it at Y=4, so total Y=184
- // Score X position is 257 (0x101)
- const int STATUS_BAR_Y = 180; // 0xb4 from FUN_41C012 line 149/152
- const int SCORE_TEXT_Y = 4; // Text position within status bar
-
+ // Score position from FUN_0041c012 assembly (low-res mode):
+ // X = ((DAT_0047a808 < 2) - 1 & 0x101) + 0x101 = 0x101 = 257
+ // Y = ((DAT_0047a808 < 2) - 1 & 4) + 4 = 4 (within status bar at Y=180)
int scoreX = 257 + _viewX;
- int scoreY = STATUS_BAR_Y + SCORE_TEXT_Y + _viewY;
-
- debug(5, "renderScoreHUD: Drawing score=%d at buffer(%d,%d) viewOffset(%d,%d)",
- _playerScore, scoreX, scoreY, _viewX, _viewY);
-
- // Draw each character manually using NutRenderer::drawCharV7
- Common::Rect clipRect(0, 0, width, height);
+ int scoreY = 180 + 4 + _viewY;
+
+ // Render each digit as a NUT sprite (direct pixel blit with color 0 transparency).
+ // This matches the original's FUN_00434cb0 â FUN_004341a0 text rendering which
+ // uses the NUT font's embedded palette colors (1=white, 3=gray, 4=black outline).
+ // Render each character applying xoffs/yoffs from NUT frame headers,
+ // matching FUN_0042cba0 lines 13-14:
+ // param_3 = *(short *)(param_6 + 2) + param_3; // X += xoffs
+ // param_4 = *(short *)(param_6 + 4) + param_4; // Y += yoffs
int x = scoreX;
for (int i = 0; scoreStr[i] != '\0'; i++) {
byte ch = (byte)scoreStr[i];
- int charWidth = _smush_dispfontNut->getCharWidth(ch);
- if (charWidth > 0) {
- // Use drawCharV7 with color 255 (white) for visibility
- _smush_dispfontNut->drawCharV7(renderBitmap, clipRect, x, scoreY, pitch, 255, kStyleAlignLeft, ch, true, true);
- x += charWidth;
- }
- }
-
- // Also draw lives counter - in status bar at X=168 (0xa8), Y=7 within bar
- const int LIVES_TEXT_Y = 7;
- char livesStr[8];
- Common::sprintf_s(livesStr, "%d", _playerLives);
- int livesX = 168 + _viewX;
- int livesY = STATUS_BAR_Y + LIVES_TEXT_Y + _viewY;
-
- x = livesX;
- for (int i = 0; livesStr[i] != '\0'; i++) {
- byte ch = (byte)livesStr[i];
- int charWidth = _smush_dispfontNut->getCharWidth(ch);
- if (charWidth > 0) {
- _smush_dispfontNut->drawCharV7(renderBitmap, clipRect, x, livesY, pitch, 255, kStyleAlignLeft, ch, true, true);
- x += charWidth;
+ if (ch < _smush_dispfontNut->getNumChars()) {
+ int charX = x + _smush_dispfontNut->getCharXOffset(ch);
+ int charY = scoreY + _smush_dispfontNut->getCharYOffset(ch);
+ renderNutSprite(renderBitmap, pitch, width, height, charX, charY, _smush_dispfontNut, ch);
+ x += _smush_dispfontNut->getCharWidth(ch);
}
}
}
diff --git a/engines/scumm/insane/insane_rebel_render.cpp b/engines/scumm/insane/insane_rebel_render.cpp
index 0eb02660d2c..d288980a18d 100644
--- a/engines/scumm/insane/insane_rebel_render.cpp
+++ b/engines/scumm/insane/insane_rebel_render.cpp
@@ -2132,55 +2132,60 @@ void InsaneRebel2::renderEmbeddedHudOverlays(byte *renderBitmap, int pitch, int
void InsaneRebel2::renderStatusBarSprites(byte *renderBitmap, int pitch, int width, int height,
int statusBarY, int32 curFrame) {
- // Draw DISPFONT.NUT status bar sprites (FUN_0041c012 equivalent)
- // DISPFONT.NUT contains:
- // Sprite 1: Status bar background frame
- // Sprites 2-5: Difficulty stars (1-4)
- // Sprite 6: Damage bar fill (with clip rect X=63, Y=9, W=64, H=6)
- // Sprite 7: Damage alert (flashing red when critical)
+ // FUN_0041c012 equivalent â renders DISPFONT.NUT status bar sprites.
+ // DISPFONT.NUT sprite layout:
+ // Sprite 1: Status bar background
+ // Sprites 2-5: Difficulty variants (full status bar with 1-4 stars)
+ // Sprite 6: Bar fill element (reused for both damage and lives bars)
+ // Sprite 7: Damage alert overlay (flashing when critical)
if (!_smush_cockpitNut)
return;
- // Sprite 1: Status bar background
- if (_smush_cockpitNut->getNumChars() > 1) {
- renderNutSprite(renderBitmap, pitch, width, height, _viewX, statusBarY + _viewY, _smush_cockpitNut, 1);
+ int numSprites = _smush_cockpitNut->getNumChars();
+
+ // --- Sprite 1: Status bar background (always drawn first as base layer) ---
+ if (numSprites > 1) {
+ renderNutSprite(renderBitmap, pitch, width, height,
+ _viewX, statusBarY + _viewY, _smush_cockpitNut, 1);
}
- // Difficulty indicator (sprites 2-5)
- int difficulty = 0; // TODO: Read from game state
+ // --- Difficulty sprite (2-5) overlaid on top ---
+ // FUN_0041c012 lines 33-43: sprite index = min(difficulty, 4) + 1
+ int difficulty = _difficulty;
if (difficulty > 3) difficulty = 3;
int difficultySprite = difficulty + 2;
- if (_smush_cockpitNut->getNumChars() > difficultySprite) {
- renderNutSprite(renderBitmap, pitch, width, height, _viewX, statusBarY + _viewY, _smush_cockpitNut, difficultySprite);
+ if (numSprites > difficultySprite) {
+ renderNutSprite(renderBitmap, pitch, width, height,
+ _viewX, statusBarY + _viewY, _smush_cockpitNut, difficultySprite);
}
- // Damage bar (sprite 6) with clipped width
- if (_smush_cockpitNut->getNumChars() > 6) {
- int drawWidth = (64 * _playerDamage) / 255;
- if (drawWidth < 0) drawWidth = 0;
- if (drawWidth > 64) drawWidth = 64;
+ // --- Damage/shield bar (sprite 6 within damage clip rect) ---
+ // FUN_0041c012 lines 44-76:
+ // Clip rect (low-res): {X=0x3f, Y=9, W=0x40, H=6} = {63, 9, 64, 6}
+ // Bar width = shield_value >> 2 (divide by 4, range 0-63)
+ // If shield > 0xAA (170): alert blink with sprite 7
+ if (numSprites > 6) {
+ // Bar width from assembly: param_1 >> 2 (low-res)
+ int damageBarWidth = _playerDamage >> 2;
const byte *src = _smush_cockpitNut->getCharData(6);
int sw = _smush_cockpitNut->getCharWidth(6);
int sh = _smush_cockpitNut->getCharHeight(6);
- // Clip rect inside sprite: X=63, Y=9, W=64, H=6
- const int clipX = 63, clipY = 9, clipW = 64, clipH = 6;
-
- if (src && sw > 0 && sh > 0) {
- int maxClipW = sw - clipX;
- if (maxClipW < 0) maxClipW = 0;
- int drawW = MIN(drawWidth, MIN(clipW, maxClipW));
- int drawH = MIN(clipH, sh - clipY);
- if (drawH < 0) drawH = 0;
-
- for (int y = 0; y < drawH; y++) {
- for (int x = 0; x < drawW; x++) {
- int destX = clipX + x + _viewX;
- int destY = statusBarY + clipY + y + _viewY;
- if (destX >= 0 && destX < pitch && destY >= 0 && destY < height) {
- byte pixel = src[(clipY + y) * sw + (clipX + x)];
+ const int dmgClipX = 63, dmgClipY = 9, dmgClipW = 64, dmgClipH = 6;
+
+ if (src && sw > 0 && sh > 0 && damageBarWidth > 0) {
+ int drawW = MIN(damageBarWidth, MIN(dmgClipW, sw - dmgClipX));
+ int drawH = MIN(dmgClipH, sh - dmgClipY);
+ if (drawW > 0 && drawH > 0) {
+ for (int y = 0; y < drawH; y++) {
+ int destY = statusBarY + dmgClipY + y + _viewY;
+ if (destY < 0 || destY >= height) continue;
+ for (int x = 0; x < drawW; x++) {
+ int destX = dmgClipX + x + _viewX;
+ if (destX < 0 || destX >= pitch) continue;
+ byte pixel = src[(dmgClipY + y) * sw + (dmgClipX + x)];
if (pixel != 0) {
renderBitmap[destY * pitch + destX] = pixel;
}
@@ -2188,12 +2193,70 @@ void InsaneRebel2::renderStatusBarSprites(byte *renderBitmap, int pitch, int wid
}
}
}
+
+ // Damage alert overlay (sprite 7) â FUN_0041c012 lines 68-76:
+ // When damage > 0xAA (170) and frame counter bit 3 is clear, draw sprite 7
+ // at full clip rect width (64 pixels) to show flashing alert
+ if (numSprites > 7 && _playerDamage > 170 && ((curFrame & 8) == 0)) {
+ if (src && sw > 0 && sh > 0) {
+ int alertW = MIN(dmgClipW, sw - dmgClipX);
+ int alertH = MIN(dmgClipH, sh - dmgClipY);
+ if (alertW > 0 && alertH > 0) {
+ const byte *alertSrc = _smush_cockpitNut->getCharData(7);
+ int alertSW = _smush_cockpitNut->getCharWidth(7);
+ int alertSH = _smush_cockpitNut->getCharHeight(7);
+ if (alertSrc && alertSW > 0 && alertSH > 0) {
+ int aW = MIN(alertW, alertSW - dmgClipX);
+ int aH = MIN(alertH, alertSH - dmgClipY);
+ for (int y = 0; y < aH; y++) {
+ int destY = statusBarY + dmgClipY + y + _viewY;
+ if (destY < 0 || destY >= height) continue;
+ for (int x = 0; x < aW; x++) {
+ int destX = dmgClipX + x + _viewX;
+ if (destX < 0 || destX >= pitch) continue;
+ byte pixel = alertSrc[(dmgClipY + y) * alertSW + (dmgClipX + x)];
+ if (pixel != 0) {
+ renderBitmap[destY * pitch + destX] = pixel;
+ }
+ }
+ }
+ }
+ }
+ }
+ }
}
- // Damage alert overlay (sprite 7) when damage > 170 and flashing
- if (_smush_cockpitNut->getNumChars() > 7) {
- if (_playerDamage > 170 && ((curFrame & 8) == 0)) {
- renderNutSprite(renderBitmap, pitch, width, height, 63 + _viewX, statusBarY + 9 + _viewY, _smush_cockpitNut, 7);
+ // --- Lives bar (sprite 6 within lives clip rect) ---
+ // FUN_0041c012 lines 99-131:
+ // Clip rect (low-res): {X=0xa8, Y=7, W=0x32, H=9} = {168, 7, 50, 9}
+ // Bar width = min((lives * 5 - 5) * 2, 50) â only drawn when lives > 1
+ if (numSprites > 6 && _playerLives > 1) {
+ int livesBarWidth = (_playerLives * 5 - 5) * 2;
+ if (livesBarWidth > 50) livesBarWidth = 50;
+
+ const byte *src = _smush_cockpitNut->getCharData(6);
+ int sw = _smush_cockpitNut->getCharWidth(6);
+ int sh = _smush_cockpitNut->getCharHeight(6);
+
+ const int livClipX = 168, livClipY = 7, livClipW = 50, livClipH = 9;
+
+ if (src && sw > 0 && sh > 0 && livesBarWidth > 0) {
+ int drawW = MIN(livesBarWidth, MIN(livClipW, sw - livClipX));
+ int drawH = MIN(livClipH, sh - livClipY);
+ if (drawW > 0 && drawH > 0) {
+ for (int y = 0; y < drawH; y++) {
+ int destY = statusBarY + livClipY + y + _viewY;
+ if (destY < 0 || destY >= height) continue;
+ for (int x = 0; x < drawW; x++) {
+ int destX = livClipX + x + _viewX;
+ if (destX < 0 || destX >= pitch) continue;
+ byte pixel = src[(livClipY + y) * sw + (livClipX + x)];
+ if (pixel != 0) {
+ renderBitmap[destY * pitch + destX] = pixel;
+ }
+ }
+ }
+ }
}
}
}
Commit: 696c68cd857acb181002a29a139e62edffe628a5
https://github.com/scummvm/scummvm/commit/696c68cd857acb181002a29a139e62edffe628a5
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:11+02:00
Commit Message:
SCUMM: RA2: Add pilot and difficulty menus
Changed paths:
engines/scumm/insane/insane_rebel.cpp
engines/scumm/insane/insane_rebel.h
engines/scumm/insane/insane_rebel_iact.cpp
engines/scumm/insane/insane_rebel_menu.cpp
engines/scumm/insane/insane_rebel_render.cpp
engines/scumm/scumm.cpp
diff --git a/engines/scumm/insane/insane_rebel.cpp b/engines/scumm/insane/insane_rebel.cpp
index 2742e9a8bc7..4c690606fa7 100644
--- a/engines/scumm/insane/insane_rebel.cpp
+++ b/engines/scumm/insane/insane_rebel.cpp
@@ -24,6 +24,7 @@
#include "engines/engine.h"
#include "common/system.h"
#include "common/events.h"
+#include "common/savefile.h"
#include "common/util.h"
#include "scumm/actor.h"
@@ -409,12 +410,23 @@ InsaneRebel2::InsaneRebel2(ScummEngine_v7 *scumm) {
_previewOffsetX = -90;
_previewOffsetY = 75; // Chapter 0: 0 * -50 + 75 = 75
+ // Initialize pilot data system
+ _numPilots = 0;
+ _activePilot = 0;
+ for (i = 0; i < kMaxPilots; i++) {
+ _pilots[i].init();
+ }
+ loadPilots(); // Load saved pilots from disk
+
// Initialize pilot selection system (FUN_00414A41)
// Menu structure: [saved pilots] + 4 fixed options (NEW/DUPE/DELETE/MAIN MENU)
_levelSelection = 0; // First item selected
- _levelItemCount = 4; // 0 saved pilots + 4 fixed options
+ _levelItemCount = _numPilots + 4; // N saved pilots + 4 fixed options
_selectedLevel = 1; // Default selected level
_difficultySelection = 2; // Default to 3rd difficulty (matching original init param_3=2)
+ _pilotMenuMode = kPilotModeSelect;
+ _pilotNameInput = "";
+ _pilotEditIndex = -1;
// Initialize menu input capture system
_menuInputActive = false;
@@ -550,6 +562,58 @@ bool InsaneRebel2::notifyEvent(const Common::Event &event) {
return false;
}
+// Per-level difficulty parameters extracted from RA2WIN95.EXE at VA 0x47e0f0
+// Original: 2D table indexed by DAT_0047a7fa (chapter/difficulty) * 0x242 + DAT_0047a7f8 (level+1) * 0x22
+// -1 = not applicable for this level type (e.g., no walls in turret levels)
+const InsaneRebel2::LevelDifficultyParams InsaneRebel2::kDifficultyTable[5][15] = {
+ // Difficulty 0 (Easy) - levels 1-15
+ {
+ { 30, 15, 3, 75}, { 17, 18, 2, 75}, { -1, 18, 3, 75},
+ { 19, -1, 2, 75}, { 27, 180, 3, 75}, { 40, -1, -1, -1},
+ { -1, 21, 3, 50}, { 30, 15, 4, 60}, { 30, -1, 70, 75},
+ { -1, 10, 5, 65}, { -1, -1, 6, 65}, { 30, 20, 1, 85},
+ { -1, 24, 2, 75}, { -1, -1, 3, 75}, { 30, 255, 4, 75},
+ },
+ // Difficulty 1 (Medium) - levels 1-15
+ {
+ { 35, 17, 5, 75}, { 30, 60, 4, 75}, { -1, 28, 3, 75},
+ { 25, -1, 4, 75}, { 35, 190, 4, 75}, { 65, -1, -1, -1},
+ { -1, 24, 3, 50}, { 45, 17, 5, 75}, { 35, -1, 75, 75},
+ { -1, 15, 5, 75}, { -1, -1, 8, 75}, { 35, 30, 1, 85},
+ { -1, 28, 2, 75}, { -1, -1, 4, 75}, { 35, 255, 4, 75},
+ },
+ // Difficulty 2 (Hard) - levels 1-15
+ {
+ { 38, 20, 7, 75}, { 35, 100, 6, 75}, { -1, 30, 4, 75},
+ { 30, -1, 6, 75}, { 50, 200, 12, 75}, { 80, -1, -1, -1},
+ { -1, 27, 3, 60}, { 60, 19, 7, 75}, { 40, -1, 100, 85},
+ { -1, 20, 6, 75}, { -1, -1, 11, 75}, { 40, 40, 3, 76},
+ { -1, 38, 4, 75}, { -1, -1, 7, 75}, { 40, 255, 7, 75},
+ },
+ // Difficulty 3 (Expert) - levels 1-15
+ {
+ { 42, 23, 12, 75}, { 50, 120, 16, 75}, { -1, 55, 4, 75},
+ { 50, 0, 15, 79}, { 90, 220, 15, 90}, { 90, -1, -1, -1},
+ { -1, 35, 3, 68}, { 75, 30, 20, 80}, { 50, -1, 110, 90},
+ { -1, 30, 7, 75}, { -1, -1, 13, 75}, { 55, 55, 5, 77},
+ { -1, 49, 4, 75}, { -1, -1, 10, 79}, { 45, 255, 8, 80},
+ },
+ // Difficulty 4 (identical to difficulty 2 in original data) - levels 1-15
+ {
+ { 38, 20, 7, 75}, { 35, 100, 6, 75}, { -1, 30, 4, 75},
+ { 30, -1, 6, 75}, { 50, 200, 12, 75}, { 80, -1, -1, -1},
+ { -1, 27, 3, 60}, { 60, 19, 7, 75}, { 40, -1, 100, 85},
+ { -1, 20, 6, 75}, { -1, -1, 11, 75}, { 40, 40, 3, 76},
+ { -1, 38, 4, 75}, { -1, -1, 7, 75}, { 40, 255, 7, 75},
+ },
+};
+
+InsaneRebel2::LevelDifficultyParams InsaneRebel2::getDifficultyParams(int levelId) const {
+ int diff = CLIP(_difficulty, 0, 4);
+ int lvIdx = CLIP(levelId - 1, 0, 14); // levelId 1-15 â index 0-14
+ return kDifficultyTable[diff][lvIdx];
+}
+
// Score lookup tables (from DAT_0047e0fe, DAT_0047e100, DAT_0047e102)
// These are indexed by: DAT_0047a7fa * 0x242 + DAT_0047a7f8 * 0x22
// For simplicity, we use fixed values based on difficulty level
@@ -644,6 +708,175 @@ void InsaneRebel2::renderScoreHUD(byte *renderBitmap, int pitch, int width, int
}
}
+// ======================= Pilot Data System =======================
+// Save/load pilot profiles using ScummVM's save file system (one pilot per slot).
+// Follows the Hypno/Wetlands pattern: each pilot = one ScummVM save slot.
+// Uses ScummEngine::makeSavegameName() for standard ScummVM file naming.
+// Original: FUN_00411980 (load) / FUN_00411A5D (save)
+
+static const uint32 kPilotSaveMagic = MKTAG('R', 'A', '2', 'P');
+static const uint16 kPilotSaveVersion = 1;
+
+bool InsaneRebel2::loadPilots() {
+ _numPilots = 0;
+
+ for (int i = 0; i < kMaxPilots; i++) {
+ Common::String filename = _vm->makeSavegameName(i, false);
+ Common::InSaveFile *sf = _vm->_saveFileMan->openForLoading(filename);
+ if (!sf)
+ break; // Slots are contiguous
+
+ uint32 magic = sf->readUint32BE();
+ if (magic != kPilotSaveMagic) {
+ delete sf;
+ break;
+ }
+
+ /* uint16 version = */ sf->readUint16LE();
+
+ PilotData &p = _pilots[i];
+ sf->read(p.name, kMaxPilotNameLen + 1);
+ p.name[kMaxPilotNameLen] = '\0';
+ for (int j = 0; j < kNumLevels; j++)
+ p.score[j] = sf->readSint32LE();
+ for (int j = 0; j < kNumLevels; j++)
+ p.lives[j] = sf->readSint32LE();
+ for (int j = 0; j < kNumLevels; j++)
+ p.damage[j] = sf->readSint32LE();
+ p.difficulty = sf->readSint16LE();
+ delete sf;
+
+ _numPilots = i + 1;
+ }
+
+ debug("Rebel2: Loaded %d pilot(s)", _numPilots);
+ return _numPilots > 0;
+}
+
+bool InsaneRebel2::savePilots() {
+ bool ok = true;
+
+ for (int i = 0; i < _numPilots; i++) {
+ Common::String filename = _vm->makeSavegameName(i, false);
+ Common::OutSaveFile *sf = _vm->_saveFileMan->openForSaving(filename, false);
+ if (!sf) {
+ warning("Rebel2: Failed to save pilot %d", i);
+ ok = false;
+ continue;
+ }
+
+ sf->writeUint32BE(kPilotSaveMagic);
+ sf->writeUint16LE(kPilotSaveVersion);
+
+ const PilotData &p = _pilots[i];
+ sf->write(p.name, kMaxPilotNameLen + 1);
+ for (int j = 0; j < kNumLevels; j++)
+ sf->writeSint32LE(p.score[j]);
+ for (int j = 0; j < kNumLevels; j++)
+ sf->writeSint32LE(p.lives[j]);
+ for (int j = 0; j < kNumLevels; j++)
+ sf->writeSint32LE(p.damage[j]);
+ sf->writeSint16LE(p.difficulty);
+
+ sf->finalize();
+ delete sf;
+ }
+
+ // Remove leftover files beyond current count
+ for (int i = _numPilots; i < kMaxPilots; i++) {
+ Common::String filename = _vm->makeSavegameName(i, false);
+ _vm->_saveFileMan->removeSavefile(filename);
+ }
+
+ debug("Rebel2: Saved %d pilot(s)", _numPilots);
+ return ok;
+}
+
+int InsaneRebel2::createNewPilot() {
+ // FUN_00411B9A: Create new pilot slot
+ if (_numPilots >= kMaxPilots)
+ return -1;
+
+ int idx = _numPilots;
+ _pilots[idx].init();
+ _numPilots++;
+ return idx;
+}
+
+void InsaneRebel2::deletePilot(int index) {
+ // FUN_00411D29: Delete pilot and shift remaining down
+ if (index < 0 || index >= _numPilots)
+ return;
+
+ for (int i = index; i < _numPilots - 1; i++) {
+ _pilots[i] = _pilots[i + 1];
+ }
+ _numPilots--;
+
+ // Clear the now-unused last slot
+ _pilots[_numPilots].init();
+}
+
+void InsaneRebel2::copyPilot(int srcIndex) {
+ // Copy pilot from srcIndex to a new slot
+ if (srcIndex < 0 || srcIndex >= _numPilots || _numPilots >= kMaxPilots)
+ return;
+
+ int newIdx = _numPilots;
+ _pilots[newIdx] = _pilots[srcIndex];
+
+ // Append " COPY" or truncate name to fit
+ Common::String name(_pilots[newIdx].name);
+ if (name.size() + 5 <= kMaxPilotNameLen) {
+ name += " COPY";
+ } else if (name.size() > 0) {
+ // Truncate and add marker
+ name = name.substr(0, kMaxPilotNameLen - 2) + " C";
+ }
+ Common::strlcpy(_pilots[newIdx].name, name.c_str(), sizeof(_pilots[newIdx].name));
+
+ _numPilots++;
+}
+
+void InsaneRebel2::updatePilotProgress(int levelIndex, int32 score, int32 lives, int32 damage) {
+ if (_activePilot < 0 || _activePilot >= _numPilots)
+ return;
+ if (levelIndex < 0 || levelIndex >= kNumLevels)
+ return;
+
+ PilotData &pilot = _pilots[_activePilot];
+ pilot.score[levelIndex] = score;
+ pilot.lives[levelIndex] = lives;
+ pilot.damage[levelIndex] = damage;
+
+ // Unlock next level if this one completed with damage < 0xFF
+ if (damage < 0xFF && levelIndex + 1 < kNumLevels) {
+ if (pilot.damage[levelIndex + 1] == 0xFF) {
+ // Initialize next level as playable
+ pilot.score[levelIndex + 1] = 0;
+ pilot.lives[levelIndex + 1] = 4;
+ pilot.damage[levelIndex + 1] = 0;
+ }
+ }
+
+ savePilots();
+}
+
+int InsaneRebel2::getPilotHighestLevel() const {
+ if (_activePilot < 0 || _activePilot >= _numPilots)
+ return 0;
+
+ const PilotData &pilot = _pilots[_activePilot];
+ int highest = 0;
+ for (int i = kNumLevels - 1; i >= 0; i--) {
+ if (pilot.damage[i] < 0xFF) {
+ highest = i;
+ break;
+ }
+ }
+ return highest;
+}
+
int32 InsaneRebel2::processMouse() {
int32 buttons = 0;
diff --git a/engines/scumm/insane/insane_rebel.h b/engines/scumm/insane/insane_rebel.h
index 82a5ab56bb2..bb2abaa4cd4 100644
--- a/engines/scumm/insane/insane_rebel.h
+++ b/engines/scumm/insane/insane_rebel.h
@@ -156,6 +156,54 @@ public:
// Draw score/info display at bottom of chapter select - emulates FUN_00434cb0 calls
void drawChapterInfoLine(byte *renderBitmap, int pitch, int width, int height);
+ // ================= Pilot Data System (FUN_00411B9A / FUN_00411980 / FUN_00411A5D) ===========
+ // Original: 10 pilot slots à 0x118 (280) bytes at DAT_004568A8
+ // Stored via SaveFileManager in a custom save file
+
+ static const int kMaxPilots = 10;
+ static const int kMaxPilotNameLen = 15;
+ static const int kNumLevels = 16;
+
+ struct PilotData {
+ char name[kMaxPilotNameLen + 1]; // +0x04: Pilot name (15 chars + null)
+ int32 score[kNumLevels]; // +0x2C: Per-level score (0 = default, 0xFF = unplayed)
+ int32 lives[kNumLevels]; // +0x6C: Per-level lives (4 = default, 0xFF = unplayed)
+ int32 damage[kNumLevels]; // +0xAC: Per-level damage (0xFF = unplayed)
+ int16 difficulty; // +0x10C: Difficulty setting (0-5)
+
+ void init() {
+ memset(name, 0, sizeof(name));
+ difficulty = 2; // Default to 3rd option
+ score[0] = 0;
+ lives[0] = 4;
+ damage[0] = 0;
+ for (int i = 1; i < kNumLevels; i++) {
+ score[i] = 0;
+ lives[i] = 0xFF;
+ damage[i] = 0xFF;
+ }
+ }
+ };
+
+ PilotData _pilots[kMaxPilots]; // DAT_004568A8 pilot array
+ int _numPilots; // DAT_00480318 number of valid pilots
+ int _activePilot; // DAT_0047a7ea selected pilot index
+
+ // Pilot save/load via SaveFileManager
+ bool loadPilots();
+ bool savePilots();
+
+ // Pilot management (FUN_00411B9A, FUN_00411D29)
+ int createNewPilot(); // Returns index of new pilot, or -1 if full
+ void deletePilot(int index); // FUN_00411D29: shift remaining pilots down
+ void copyPilot(int srcIndex); // Copy pilot to new slot
+
+ // Update pilot progress after level completion
+ void updatePilotProgress(int levelIndex, int32 score, int32 lives, int32 damage);
+
+ // Get highest unlocked level for active pilot (checks damage[] < 0xFF)
+ int getPilotHighestLevel() const;
+
// ================= Pilot Selection Menu (FUN_00414A41) ====================
// This is the pilot/save selection menu (separate from chapter selection)
@@ -165,6 +213,18 @@ public:
kLevelSelectQuit = 2 // Quit game
};
+ // Pilot name input state (for NEW PILOT name entry)
+ enum PilotMenuMode {
+ kPilotModeSelect = 0, // Normal pilot list selection
+ kPilotModeNameInput = 1, // Typing a new pilot name
+ kPilotModeDifficulty = 2, // Difficulty submenu
+ kPilotModeDeleteConfirm = 3, // Delete confirmation
+ kPilotModeCopySelect = 4 // Copy source selection
+ };
+ PilotMenuMode _pilotMenuMode;
+ Common::String _pilotNameInput; // Current name being typed
+ int _pilotEditIndex; // Index of pilot being edited/created
+
int _levelSelection; // Current level selection (0-based)
int _levelItemCount; // Number of level items (levels + options)
int _selectedLevel; // Final selected level ID (1-15)
@@ -917,10 +977,32 @@ public:
NutRenderer *_hudOverlayNut; // DAT_0047fe78 - Primary HUD overlay (animated)
NutRenderer *_hudOverlay2Nut; // DAT_0047fe80 - Secondary HUD overlay
- /* Difficulty Level (0, 1, 2 = Easy, Med, Hard) */
+ /* Difficulty Level (0-5, from pilot menu; clamped to 0-4 for table lookup) */
int _difficulty;
void drawCornerBrackets(byte *dst, int pitch, int width, int height, int x, int y, int w, int h, byte color);
+ // ======================= Per-Level Difficulty Parameters =======================
+ // Extracted from RA2WIN95.EXE at VA 0x47e0f0
+ // 2D table indexed by difficulty (0-4) Ã game level (1-15)
+ // Original indexing: &DAT_0047e0f6 + chapter * 0x242 + level * 0x22
+ // -1 = not applicable for this level type (e.g., no walls in turret levels)
+
+ struct LevelDifficultyParams {
+ int16 wallDamage; // +0x06 DAT_0047e0f6: Wall/obstacle collision damage
+ int16 directHitDamage; // +0x04 DAT_0047e0f4: Direct hit damage (par4=100)
+ int16 enemyProjectileDamage;// +0x08 DAT_0047e0f8: Enemy projectile/probabilistic damage
+ int16 hitProbability; // +0x0C DAT_0047e0fc: Enemy hit chance (0-100, -1=disabled)
+ };
+
+ // Table: 5 difficulty levels à 15 game levels
+ // Difficulty 4 is identical to difficulty 2 in the original data
+ static const LevelDifficultyParams kDifficultyTable[5][15];
+
+ // Look up difficulty parameters for current difficulty and level
+ // Returns the entry from kDifficultyTable, clamping difficulty to 0-4
+ // levelId is 1-based (1-15)
+ LevelDifficultyParams getDifficultyParams(int levelId) const;
+
// Score system (FUN_0041bf8d equivalent)
// Adds points to score and awards bonus life when crossing threshold
void addScore(int points);
diff --git a/engines/scumm/insane/insane_rebel_iact.cpp b/engines/scumm/insane/insane_rebel_iact.cpp
index 36c3cc87ca1..a6ff44b1aaf 100644
--- a/engines/scumm/insane/insane_rebel_iact.cpp
+++ b/engines/scumm/insane/insane_rebel_iact.cpp
@@ -434,15 +434,14 @@ void InsaneRebel2::iactRebel2Opcode3(Common::SeekableReadStream &b, int16 par2,
srcIdBody1, isBitSet(srcIdBody1), _rebelDamageLevel);
if (_rebelDamageLevel < 2 && !isBitSet(srcIdBody1)) {
- int probability = 20 + _difficulty * 20;
- if (probability < 5) probability = 5;
- if (probability > 90) probability = 90;
+ LevelDifficultyParams params = getDifficultyParams(_selectedLevel);
+ int probability = (params.hitProbability >= 0) ? params.hitProbability : 0;
int roll = _vm->_rnd.getRandomNumber(99);
debug("Rebel2 Opcode3: probability=%d roll=%d (need roll < prob)", probability, roll);
if (roll < probability) {
if (!_rebelInvulnerable) {
- int damageAmount = 5 + (_difficulty * 2);
+ int damageAmount = (params.enemyProjectileDamage >= 0) ? params.enemyProjectileDamage : 0;
_playerDamage += damageAmount;
if (_playerDamage > 255) _playerDamage = 255;
debug("Rebel2: H25 PROBABILISTIC damage from %d. Damage=%d total=%d",
@@ -466,7 +465,8 @@ void InsaneRebel2::iactRebel2Opcode3(Common::SeekableReadStream &b, int16 par2,
// Direct damage: par4==100, separate from par3 branches (lines 99-111)
if (par4 == 100 && !isBitSet(srcIdBody0)) {
if (!_rebelInvulnerable) {
- int directHitDamage = 8 + (_difficulty * 4);
+ LevelDifficultyParams dparams = getDifficultyParams(_selectedLevel);
+ int directHitDamage = (dparams.directHitDamage >= 0) ? dparams.directHitDamage : 0;
_playerDamage += directHitDamage;
if (_playerDamage > 255) _playerDamage = 255;
debug("Rebel2: H25 DIRECT HIT par4=100 damage=%d total=%d",
@@ -485,7 +485,8 @@ void InsaneRebel2::iactRebel2Opcode3(Common::SeekableReadStream &b, int16 par2,
_rebelHitCounter++;
debug("Rebel2: Incremented hit counter -> %d", _rebelHitCounter);
- int directHitDamage = 8 + (_difficulty * 4);
+ LevelDifficultyParams dparams = getDifficultyParams(_selectedLevel);
+ int directHitDamage = (dparams.directHitDamage >= 0) ? dparams.directHitDamage : 0;
if (par4 != 0 && directHitDamage > 0) {
bool shouldDamage = false;
@@ -517,16 +518,15 @@ void InsaneRebel2::iactRebel2Opcode3(Common::SeekableReadStream &b, int16 par2,
debug("Rebel2 Opcode3: par3=5 srcId=%d isBitSet=%d", srcId, isBitSet(srcId));
if (!isBitSet(srcId)) {
- int probability = 20 + _difficulty * 20;
- if (probability < 5) probability = 5;
- if (probability > 90) probability = 90;
+ LevelDifficultyParams params = getDifficultyParams(_selectedLevel);
+ int probability = (params.hitProbability >= 0) ? params.hitProbability : 0;
int roll = _vm->_rnd.getRandomNumber(99);
debug("Rebel2 Opcode3: probability=%d roll=%d (need roll < prob)", probability, roll);
if (roll < probability) {
if (!_rebelInvulnerable) {
- int damageAmount = 5 + (_difficulty * 2);
+ int damageAmount = (params.enemyProjectileDamage >= 0) ? params.enemyProjectileDamage : 0;
_playerDamage += damageAmount;
if (_playerDamage > 255) _playerDamage = 255;
debug("Rebel2: PROBABILISTIC damage from enemy %d. Damage=%d total=%d",
@@ -860,6 +860,9 @@ void InsaneRebel2::iactRebel2Opcode6(byte *renderBitmap, Common::SeekableReadStr
// --- Step 7: Corridor collision â mode 0/2 only (lines 257-292) ---
if (_flyControlMode == 0 || _flyControlMode == 2) {
+ LevelDifficultyParams wallParams = getDifficultyParams(_selectedLevel);
+ int corridorWallDmg = (wallParams.wallDamage >= 0) ? wallParams.wallDamage : 0;
+
// Right boundary (lines 258-270)
// Original: position is ALWAYS clamped; damage/bounce only when cooldown < 5
if (_corridorRightX < _flyShipScreenX) {
@@ -870,8 +873,7 @@ void InsaneRebel2::iactRebel2Opcode6(byte *renderBitmap, Common::SeekableReadStr
_spaceShotDirection = 1;
initDamageFlash();
if (!_rebelInvulnerable) {
- int damage = 3 + (_difficulty * 2);
- _playerDamage += damage;
+ _playerDamage += corridorWallDmg;
if (_playerDamage > 255) _playerDamage = 255;
}
_rebelHitCounter++;
@@ -887,8 +889,7 @@ void InsaneRebel2::iactRebel2Opcode6(byte *renderBitmap, Common::SeekableReadStr
_spaceShotDirection = 0;
initDamageFlash();
if (!_rebelInvulnerable) {
- int damage = 3 + (_difficulty * 2);
- _playerDamage += damage;
+ _playerDamage += corridorWallDmg;
if (_playerDamage > 255) _playerDamage = 255;
}
_rebelHitCounter++;
diff --git a/engines/scumm/insane/insane_rebel_menu.cpp b/engines/scumm/insane/insane_rebel_menu.cpp
index c298f0f7e09..06ef3445784 100644
--- a/engines/scumm/insane/insane_rebel_menu.cpp
+++ b/engines/scumm/insane/insane_rebel_menu.cpp
@@ -1262,28 +1262,20 @@ int InsaneRebel2::runLevelSelect() {
// kLevelSelectPlay (1) = Go to chapter selection (pilot selected or NEW+difficulty chosen)
// kLevelSelectBack (0) = Return to main menu (MAIN MENU or ESC)
// kLevelSelectQuit (2) = Quit game
- //
- // Original action dispatch (FUN_00414A41):
- // sel < N â saved pilot selected â return 3 (start game)
- // sel == N â ADD NEW PILOT â difficulty submenu â loop back
- // sel == N+1 â COPY PILOT â source select (no-op if N==0)
- // sel == N+2 â DELETE PILOT â confirm select (no-op if N==0)
- // sel == N+3 â RETURN TO MAIN MENU â return 1
- // ESC â return 1
- debug("Rebel2: Entering pilot selection (FUN_00414A41)");
+ debug("Rebel2: Entering pilot selection (FUN_00414A41), %d pilots loaded", _numPilots);
_menuInputActive = true;
while (!_menuEventQueue.empty()) _menuEventQueue.pop();
- // Number of saved pilots (TODO: implement save system)
- int numPilots = 0;
-
_levelSelection = 0;
- _levelItemCount = numPilots + 4; // N pilots + NEW/COPY/DELETE/MAIN MENU
+ _levelItemCount = _numPilots + 4; // N pilots + NEW/COPY/DELETE/MAIN MENU
_selectedLevel = 1;
_menuRepeatDelay = 0;
_gameState = kStatePilotSelect;
+ _pilotMenuMode = kPilotModeSelect;
+ _pilotNameInput = "";
+ _pilotEditIndex = -1;
SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
@@ -1292,8 +1284,6 @@ int InsaneRebel2::runLevelSelect() {
_vm->_smushVideoShouldFinish = false;
Common::String menuVideo = getRandomMenuVideo();
- debug("Rebel2: Playing pilot select background: %s", menuVideo.c_str());
-
splayer->setCurVideoFlags(0x20);
splayer->play(menuVideo.c_str(), 12);
@@ -1302,60 +1292,102 @@ int InsaneRebel2::runLevelSelect() {
return kLevelSelectQuit;
}
- if (!_vm->_smushVideoShouldFinish) {
+ if (!_vm->_smushVideoShouldFinish)
continue;
- }
_vm->_smushVideoShouldFinish = false;
- // Dispatch based on current game state
- if (_gameState == kStateDifficultySelect) {
- // Difficulty submenu â selection made
- debug("Rebel2: Difficulty %d selected", _difficultySelection);
- // Original stores difficulty in pilot record and loops back.
- // Since we have no save system, proceed to chapter select.
+ // --- Difficulty submenu completed ---
+ if (_pilotMenuMode == kPilotModeDifficulty) {
+ // Store difficulty in the new pilot and save
+ if (_pilotEditIndex >= 0 && _pilotEditIndex < _numPilots) {
+ _pilots[_pilotEditIndex].difficulty = _difficultySelection;
+ _difficulty = _difficultySelection;
+ savePilots();
+ _activePilot = _pilotEditIndex;
+
+ // Update chapter unlock state from pilot data
+ for (int i = 0; i < 16; i++) {
+ _chapterUnlocked[i] = _debugUnlockAll || (_pilots[_activePilot].damage[i] < 0xFF);
+ }
+ }
+ _pilotMenuMode = kPilotModeSelect;
+ _levelItemCount = _numPilots + 4;
_gameState = kStatePilotSelect;
_menuInputActive = false;
return kLevelSelectPlay;
}
- // Pilot menu â process selection
- debug("Rebel2: Pilot selection made: %d (numPilots=%d)", _levelSelection, numPilots);
+ // --- Name input completed ---
+ if (_pilotMenuMode == kPilotModeNameInput) {
+ // Name was confirmed â now show difficulty submenu
+ if (_pilotEditIndex >= 0 && _pilotEditIndex < _numPilots) {
+ Common::strlcpy(_pilots[_pilotEditIndex].name, _pilotNameInput.c_str(),
+ sizeof(_pilots[_pilotEditIndex].name));
+ }
+ _pilotMenuMode = kPilotModeDifficulty;
+ _gameState = kStateDifficultySelect;
+ _difficultySelection = 2;
+ continue;
+ }
+
+ // --- Normal pilot menu selection ---
+ debug("Rebel2: Pilot selection: %d (numPilots=%d)", _levelSelection, _numPilots);
+
+ if (_levelSelection < _numPilots) {
+ // Existing pilot selected â activate and go to chapter select
+ _activePilot = _levelSelection;
+ _difficulty = _pilots[_activePilot].difficulty;
+
+ // Update chapter unlock state from pilot data
+ for (int i = 0; i < 16; i++) {
+ _chapterUnlocked[i] = _debugUnlockAll || (_pilots[_activePilot].damage[i] < 0xFF);
+ }
- if (_levelSelection < numPilots) {
- // Saved pilot selected â go to chapter selection
- _selectedLevel = _levelSelection + 1;
- debug("Rebel2: Pilot %d selected - going to chapter selection", _selectedLevel);
+ debug("Rebel2: Pilot '%s' selected (slot %d, difficulty %d)",
+ _pilots[_activePilot].name, _activePilot, _difficulty);
_menuInputActive = false;
return kLevelSelectPlay;
- } else if (_levelSelection == numPilots) {
- // ADD NEW PILOT â show difficulty submenu
- debug("Rebel2: ADD NEW PILOT - showing difficulty submenu");
- _gameState = kStateDifficultySelect;
- _difficultySelection = 2; // Default to 3rd option (matching original init)
- // Continue loop â next video frame will render difficulty menu
+
+ } else if (_levelSelection == _numPilots) {
+ // ADD NEW PILOT â create slot, enter name input mode
+ int newIdx = createNewPilot();
+ if (newIdx >= 0) {
+ _pilotEditIndex = newIdx;
+ _pilotNameInput = "";
+ _pilotMenuMode = kPilotModeNameInput;
+ _levelItemCount = _numPilots + 4;
+ debug("Rebel2: NEW PILOT - entering name for slot %d", newIdx);
+ }
continue;
- } else if (_levelSelection == numPilots + 1) {
- // COPY PILOT â no-op when numPilots==0 (original checks local_10 != 0)
- if (numPilots > 0) {
- debug("Rebel2: COPY PILOT selected");
- // TODO: implement copy pilot sub-flow
- } else {
- debug("Rebel2: COPY PILOT - no pilots to copy");
+
+ } else if (_levelSelection == _numPilots + 1) {
+ // COPY PILOT
+ if (_numPilots > 0 && _numPilots < kMaxPilots) {
+ // Copy first pilot (slot 0) by default â original swaps with selected
+ int srcIdx = (_levelSelection > 0 && _levelSelection <= _numPilots) ? _levelSelection - 1 : 0;
+ copyPilot(srcIdx);
+ savePilots();
+ _levelItemCount = _numPilots + 4;
+ debug("Rebel2: Copied pilot %d, now %d pilots", srcIdx, _numPilots);
}
continue;
- } else if (_levelSelection == numPilots + 2) {
- // DELETE PILOT â no-op when numPilots==0 (original checks local_10 != 0)
- if (numPilots > 0) {
- debug("Rebel2: DELETE PILOT selected");
- // TODO: implement delete pilot sub-flow
- } else {
- debug("Rebel2: DELETE PILOT - no pilots to delete");
+
+ } else if (_levelSelection == _numPilots + 2) {
+ // DELETE PILOT
+ if (_numPilots > 0) {
+ // Delete the first pilot (slot 0) â original has confirm sub-flow
+ deletePilot(0);
+ savePilots();
+ _levelItemCount = _numPilots + 4;
+ if (_levelSelection >= _levelItemCount)
+ _levelSelection = _levelItemCount - 1;
+ debug("Rebel2: Deleted pilot, %d remaining", _numPilots);
}
continue;
- } else if (_levelSelection == numPilots + 3) {
+
+ } else if (_levelSelection == _numPilots + 3) {
// RETURN TO MAIN MENU
- debug("Rebel2: Back to main menu selected");
_menuInputActive = false;
return kLevelSelectBack;
}
@@ -1367,12 +1399,61 @@ int InsaneRebel2::runLevelSelect() {
int InsaneRebel2::processLevelSelectInput() {
// Process input for pilot selection and difficulty submenu
- // Handles both kStatePilotSelect and kStateDifficultySelect modes
+ // Handles kPilotModeSelect, kPilotModeNameInput, and kStateDifficultySelect
// Returns: -1 = no action, 0+ = item selected
int result = -1;
- // Determine which menu mode we're in
+ // Name input mode â keyboard goes to _pilotNameInput instead of menu nav
+ // Original: FUN_00414A41 lines 87-116
+ if (_pilotMenuMode == kPilotModeNameInput) {
+ while (!_menuEventQueue.empty()) {
+ Common::Event event = _menuEventQueue.pop();
+ if (event.type == Common::EVENT_KEYDOWN) {
+ if (event.kbd.keycode == Common::KEYCODE_RETURN ||
+ event.kbd.keycode == Common::KEYCODE_KP_ENTER) {
+ // Confirm name â signal back to runLevelSelect()
+ if (_pilotNameInput.size() > 0) {
+ _vm->_smushVideoShouldFinish = true;
+ debug("PilotName: confirmed '%s'", _pilotNameInput.c_str());
+ }
+ } else if (event.kbd.keycode == Common::KEYCODE_ESCAPE) {
+ // Cancel name entry â delete the pilot slot we created
+ if (_pilotEditIndex >= 0 && _pilotEditIndex < _numPilots) {
+ deletePilot(_pilotEditIndex);
+ }
+ _pilotMenuMode = kPilotModeSelect;
+ _levelItemCount = _numPilots + 4;
+ debug("PilotName: cancelled");
+ } else if (event.kbd.keycode == Common::KEYCODE_BACKSPACE) {
+ // Backspace â remove last character
+ if (_pilotNameInput.size() > 0) {
+ _pilotNameInput.deleteLastChar();
+ }
+ } else {
+ // Printable ASCII (0x20-0x7E), max 15 chars
+ char c = (char)event.kbd.ascii;
+ if (c >= 0x20 && c <= 0x7E &&
+ (int)_pilotNameInput.size() < kMaxPilotNameLen) {
+ _pilotNameInput += c;
+ }
+ }
+ } else if (event.type == Common::EVENT_MOUSEMOVE) {
+ _vm->_mouse.x = event.mouse.x;
+ _vm->_mouse.y = event.mouse.y;
+ } else if (event.type == Common::EVENT_QUIT ||
+ event.type == Common::EVENT_RETURN_TO_LAUNCHER) {
+ if (_pilotEditIndex >= 0 && _pilotEditIndex < _numPilots) {
+ deletePilot(_pilotEditIndex);
+ }
+ _pilotMenuMode = kPilotModeSelect;
+ _levelItemCount = _numPilots + 4;
+ }
+ }
+ return -1;
+ }
+
+ // Normal menu navigation (pilot select or difficulty submenu)
bool isDifficultyMode = (_gameState == kStateDifficultySelect);
int &selection = isDifficultyMode ? _difficultySelection : _levelSelection;
int itemCount = isDifficultyMode ? 6 : _levelItemCount;
@@ -1391,8 +1472,6 @@ int InsaneRebel2::processLevelSelectInput() {
if (selection < 0) {
selection = itemCount - 1;
}
- debug("LevelSelect: Selection changed to %d (UP, %s)",
- selection, isDifficultyMode ? "difficulty" : "pilot");
break;
case Common::KEYCODE_DOWN:
@@ -1400,30 +1479,20 @@ int InsaneRebel2::processLevelSelectInput() {
if (selection >= itemCount) {
selection = 0;
}
- debug("LevelSelect: Selection changed to %d (DOWN, %s)",
- selection, isDifficultyMode ? "difficulty" : "pilot");
break;
case Common::KEYCODE_RETURN:
case Common::KEYCODE_KP_ENTER:
if (selection >= 0 && selection < itemCount) {
result = selection;
- debug("LevelSelect: Item %d selected (ENTER, %s)",
- selection, isDifficultyMode ? "difficulty" : "pilot");
}
break;
case Common::KEYCODE_ESCAPE:
if (isDifficultyMode) {
- // ESC in difficulty submenu â return to pilot menu
- // Note: original has no ESC in difficulty submenu, but we add it as a
- // graceful fallback
_gameState = kStatePilotSelect;
- debug("LevelSelect: ESC in difficulty - back to pilot menu");
} else {
- // ESC in pilot menu â back to main menu (return 1 in original)
result = _levelItemCount - 1; // Last item = MAIN MENU
- debug("LevelSelect: ESC pressed - back to main menu");
}
break;
@@ -1435,15 +1504,11 @@ int InsaneRebel2::processLevelSelectInput() {
case Common::EVENT_LBUTTONDOWN:
{
int mouseY = event.mouse.y;
- debug("LevelSelect: Click at Y=%d (%s)", mouseY,
- isDifficultyMode ? "difficulty" : "pilot");
-
for (int i = 0; i < itemCount; i++) {
int itemY = baseY + i * itemHeight;
if (mouseY >= itemY - 4 && mouseY < itemY + 6) {
selection = i;
result = i;
- debug("LevelSelect: Item %d clicked at Y=%d (itemY=%d)", i, mouseY, itemY);
break;
}
}
@@ -1506,22 +1571,35 @@ void InsaneRebel2::drawLevelSelectOverlay(byte *renderBitmap, int pitch, int wid
// Pilot menu - FUN_0041f5ae(0, &DAT_00457768, N+4, 0)
// =====================================================================
// items[0] = title (TRS 20)
- // items[1..N] = saved pilots (formatted with ^f01^c005)
+ // items[1..N] = saved pilots (formatted with ^f01^c005<name>^f00)
// items[N+1] = TRS 21 (ADD NEW PILOT)
// items[N+2] = TRS 22 (COPY PILOT)
// items[N+3] = TRS 23 (DELETE PILOT)
// items[N+4] = TRS 24 (RETURN TO MAIN MENU)
- int numPilots = 0; // TODO: implement save system
+ // Build pilot name strings with font/color formatting
+ // Original uses "^f01^c005<name>^f00" for pilot entries
+ Common::String pilotNameStrs[kMaxPilots];
+ for (int i = 0; i < _numPilots; i++) {
+ if (_pilotMenuMode == kPilotModeNameInput && i == _pilotEditIndex) {
+ // Show name being typed with cursor (underscore)
+ pilotNameStrs[i] = Common::String::format("^f01^c005%s_^f00", _pilotNameInput.c_str());
+ } else {
+ pilotNameStrs[i] = Common::String::format("^f01^c005%s^f00", _pilots[i].name);
+ }
+ }
- const char *pilotItems[11];
+ // Max items: 1 title + 10 pilots + 4 options = 15
+ const char *pilotItems[15];
int idx = 0;
// Title: TRS 20
pilotItems[idx++] = splayer->getString(20);
- // Saved pilot slots would be inserted here (items[1..numPilots])
- // Each formatted as "^f01^c005<name>^f00" by the original
+ // Saved pilot slots
+ for (int i = 0; i < _numPilots; i++) {
+ pilotItems[idx++] = pilotNameStrs[i].c_str();
+ }
// Fixed options: TRS 21-24
for (int i = 0; i < 4; i++) {
@@ -1534,11 +1612,7 @@ void InsaneRebel2::drawLevelSelectOverlay(byte *renderBitmap, int pitch, int wid
}
}
- drawMenuItems(renderBitmap, pitch, width, height, pilotItems, numPilots + 4, _levelSelection);
-
- // Pilot info display at fixed coordinates when saved pilot selected
- // FUN_00414A41 lines 78-86: FUN_00434cb0 at Y=0xb4(180) and Y=0xbe(190)
- // TODO: Implement when save system is added
+ drawMenuItems(renderBitmap, pitch, width, height, pilotItems, _numPilots + 4, _levelSelection);
}
} // End of namespace Scumm
diff --git a/engines/scumm/insane/insane_rebel_render.cpp b/engines/scumm/insane/insane_rebel_render.cpp
index d288980a18d..9f59af3d288 100644
--- a/engines/scumm/insane/insane_rebel_render.cpp
+++ b/engines/scumm/insane/insane_rebel_render.cpp
@@ -1176,9 +1176,9 @@ void InsaneRebel2::checkCollisionZones() {
if (collision) {
// Collision detected â apply damage from collision damage table
- // Original: DAT_0047a7ec += DAT_0047e0f6[levelIdx]
- // TODO: Read from per-level collision damage table DAT_0047e0f6
- int collisionDamage = 3 + (_difficulty * 2);
+ // Original: DAT_0047a7ec += DAT_0047e0f6[chapter * 0x242 + level * 0x22]
+ LevelDifficultyParams params = getDifficultyParams(_selectedLevel);
+ int collisionDamage = (params.wallDamage >= 0) ? params.wallDamage : 0;
if (!_rebelInvulnerable) {
_playerDamage += collisionDamage;
@@ -1254,7 +1254,8 @@ void InsaneRebel2::checkHandler7CollisionZones() {
_hitCooldown = 10;
_spaceShotDirection = zone.filterValue + 2;
- int collisionDamage = 3 + (_difficulty * 2);
+ LevelDifficultyParams params = getDifficultyParams(_selectedLevel);
+ int collisionDamage = (params.wallDamage >= 0) ? params.wallDamage : 0;
if (!_rebelInvulnerable) {
_playerDamage += collisionDamage;
if (_playerDamage > 255) _playerDamage = 255;
@@ -1282,6 +1283,8 @@ void InsaneRebel2::checkHandler7CollisionZones() {
// Ship position is clamped to wall boundaries when hitting.
int16 hMargin = (_flyControlMode == 1) ? 0x28 : 0x0f; // local_14
const int16 vMargin = 0x0f; // local_20
+ LevelDifficultyParams wallParams = getDifficultyParams(_selectedLevel);
+ int wallDamage = (wallParams.wallDamage >= 0) ? wallParams.wallDamage : 0;
for (int i = 0; i < _primaryZoneCount; i++) {
CollisionZone &zone = _primaryZones[i];
@@ -1298,7 +1301,7 @@ void InsaneRebel2::checkHandler7CollisionZones() {
if (_flyShipScreenY < edgeY) {
// Ship above top wall â push down
if (_hitCooldown < 5 && !_rebelInvulnerable) {
- int damage = 3 + (_difficulty * 2);
+ int damage = wallDamage;
_playerDamage += damage;
if (_playerDamage > 255) _playerDamage = 255;
_rebelHitCounter++;
@@ -1320,7 +1323,7 @@ void InsaneRebel2::checkHandler7CollisionZones() {
if (edgeY < _flyShipScreenY) {
// Ship below bottom wall â push up
if (_hitCooldown < 5 && !_rebelInvulnerable) {
- int damage = 3 + (_difficulty * 2);
+ int damage = wallDamage;
_playerDamage += damage;
if (_playerDamage > 255) _playerDamage = 255;
_rebelHitCounter++;
@@ -1342,7 +1345,7 @@ void InsaneRebel2::checkHandler7CollisionZones() {
// Ship left of left wall â push right
_flyShipScreenX = edgeX; // Push-back
if (_hitCooldown < 5 && !_rebelInvulnerable) {
- int damage = 3 + (_difficulty * 2);
+ int damage = wallDamage;
_playerDamage += damage;
if (_playerDamage > 255) _playerDamage = 255;
_rebelHitCounter++;
@@ -1363,7 +1366,7 @@ void InsaneRebel2::checkHandler7CollisionZones() {
// Ship right of right wall â push left
_flyShipScreenX = edgeX; // Push-back
if (_hitCooldown < 5 && !_rebelInvulnerable) {
- int damage = 3 + (_difficulty * 2);
+ int damage = wallDamage;
_playerDamage += damage;
if (_playerDamage > 255) _playerDamage = 255;
_rebelHitCounter++;
diff --git a/engines/scumm/scumm.cpp b/engines/scumm/scumm.cpp
index 0c5ea36f7b7..ec0e19e74e2 100644
--- a/engines/scumm/scumm.cpp
+++ b/engines/scumm/scumm.cpp
@@ -2728,12 +2728,17 @@ Common::Error ScummEngine::go() {
// Run the complete level (handles BEG, gameplay, END/DIE/RETRY/OVER)
int result = rebel->runLevel(selectedLevel);
+ // Save pilot progress after level completion
+ if (result == InsaneRebel2::kLevelNextLevel) {
+ rebel->updatePilotProgress(selectedLevel - 1,
+ rebel->_playerScore, rebel->_playerLives, rebel->_playerDamage);
+ }
+
if (shouldQuit() || result == InsaneRebel2::kLevelQuit) {
break;
}
// After level completion or game over, return to menu
- // Could also handle kLevelNextLevel to auto-start next level
}
// If kChapterSelectBack, loop back to main menu
}
Commit: 9f75d49f081fdeb0e89e7eb99bce214736547f4e
https://github.com/scummvm/scummvm/commit/9f75d49f081fdeb0e89e7eb99bce214736547f4e
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:12+02:00
Commit Message:
SCUMM: RA2: Tune difficulty parameters
Changed paths:
engines/scumm/insane/insane_rebel.cpp
engines/scumm/insane/insane_rebel.h
engines/scumm/insane/insane_rebel_iact.cpp
engines/scumm/insane/insane_rebel_render.cpp
diff --git a/engines/scumm/insane/insane_rebel.cpp b/engines/scumm/insane/insane_rebel.cpp
index 4c690606fa7..ad9204e895e 100644
--- a/engines/scumm/insane/insane_rebel.cpp
+++ b/engines/scumm/insane/insane_rebel.cpp
@@ -563,82 +563,121 @@ bool InsaneRebel2::notifyEvent(const Common::Event &event) {
}
// Per-level difficulty parameters extracted from RA2WIN95.EXE at VA 0x47e0f0
-// Original: 2D table indexed by DAT_0047a7fa (chapter/difficulty) * 0x242 + DAT_0047a7f8 (level+1) * 0x22
-// -1 = not applicable for this level type (e.g., no walls in turret levels)
-const InsaneRebel2::LevelDifficultyParams InsaneRebel2::kDifficultyTable[5][15] = {
- // Difficulty 0 (Easy) - levels 1-15
+// Original: 2D table indexed by DAT_0047a7fa (difficulty) * 0x242 + DAT_0047a7f8 (levelType) * 0x22
+// 17 fields per entry (34 bytes), 17 entries per difficulty, 5 difficulties
+// Field names from official Difficulty Editor: {laserDelay, snapDistance, missDamage, dodgeDamage,
+// shotDamage, specialDamage, shotAccuracy, hitPoints, dodgePoints, timePoints,
+// levelPoints, specialPoints, flags, rollRate, liftRate, slideRate, driftRate}
+// -1 = not applicable for this level type
+const InsaneRebel2::LevelDifficultyParams InsaneRebel2::kDifficultyTable[5][17] = {
+ // Difficulty 0 (Beginner) - 17 level types
{
- { 30, 15, 3, 75}, { 17, 18, 2, 75}, { -1, 18, 3, 75},
- { 19, -1, 2, 75}, { 27, 180, 3, 75}, { 40, -1, -1, -1},
- { -1, 21, 3, 50}, { 30, 15, 4, 60}, { 30, -1, 70, 75},
- { -1, 10, 5, 65}, { -1, -1, 6, 65}, { 30, 20, 1, 85},
- { -1, 24, 2, 75}, { -1, -1, 3, 75}, { 30, 255, 4, 75},
+ { 5, 3, 15, -1, 2, -1, 75, 25, -1, 2, 500, 250, 8, 5, 5, 6, -1}, // Lv1
+ { 4, 3, -1, -1, 2, -1, 40, 25, -1, 0, 500, 250, 8, 90, 90, 120, 25}, // Lv2
+ { 6, 5, 15, 30, 3, 12, 75, 25, 50, 2, 500, 250, 8, -1, -1, -1, -1}, // Lv3
+ { 5, 3, 18, 17, 2, 20, 75, 25, 50, 2, 500, 250, 8, -1, -1, -1, -1}, // Lv4
+ { 5, 4, 18, -1, 3, -1, 75, 25, -1, 2, 500, 250, 8, -1, -1, -1, -1}, // Lv5
+ { 5, 5, -1, 19, 2, 15, 75, 25, 50, 2, 500, -1, 8, -1, -1, -1, -1}, // Lv6A
+ { 5, 5, 180, 27, 3, -1, 75, 25, 50, 2, 500, 250, 8, 120, 120, 120, 75}, // Lv6B
+ { -1, -1, -1, 40, -1, -1, -1, -1, 50, 2, 500, 250, 8, -1, -1, -1, -1}, // Lv7
+ { 5, 5, 21, -1, 3, -1, 50, 25, -1, 2, 500, 250, 8, -1, -1, -1, -1}, // Lv8
+ { 5, 6, 15, 30, 4, -1, 60, 25, 50, 2, 500, 250, 8, 90, 90, 90, 135}, // Lv9
+ { 5, 15, -1, 30, 70, -1, 75, 25, 50, 2, 500, 250, 8, 10, 6, 7, -1}, // Lv10
+ { 4, 4, 10, -1, 5, -1, 65, 25, -1, 0, 500, 250, 8, 5, 6, 7, 8}, // Lv11
+ { 4, 2, -1, -1, 6, -1, 65, 25, -1, 2, 500, 250, 8, -1, -1, -1, -1}, // Lv12
+ { 5, 6, 20, 30, 1, 20, 85, 25, 50, 2, 500, 250, 8, -1, -1, -1, -1}, // Lv13
+ { 5, 3, 24, -1, 2, -1, 75, 25, -1, 2, 500, 250, 8, -1, -1, -1, -1}, // Lv14
+ { 5, 8, -1, -1, 3, -1, 75, 25, -1, 2, 500, -1, 8, -1, -1, -1, -1}, // Lv15A
+ { 5, 6, 255, 30, 4, 10, 75, 25, 50, 2, 500, 250, 8, -1, -1, -1, -1}, // Lv15B
},
- // Difficulty 1 (Medium) - levels 1-15
+ // Difficulty 1 (Easy) - 17 level types
{
- { 35, 17, 5, 75}, { 30, 60, 4, 75}, { -1, 28, 3, 75},
- { 25, -1, 4, 75}, { 35, 190, 4, 75}, { 65, -1, -1, -1},
- { -1, 24, 3, 50}, { 45, 17, 5, 75}, { 35, -1, 75, 75},
- { -1, 15, 5, 75}, { -1, -1, 8, 75}, { 35, 30, 1, 85},
- { -1, 28, 2, 75}, { -1, -1, 4, 75}, { 35, 255, 4, 75},
+ { 6, 1, 25, -1, 3, -1, 75, 50, -1, 4, 1000, 500, 16, 6, 6, 7, -1}, // Lv1
+ { 4, 2, -1, -1, 4, -1, 40, 50, -1, 0, 1000, 500, 16, 100, 100, 135, 30}, // Lv2
+ { 6, 4, 17, 35, 5, 12, 75, 50, 100, 4, 1000, 500, 16, -1, -1, -1, -1}, // Lv3
+ { 5, 2, 60, 30, 4, 20, 75, 50, 100, 4, 1000, 500, 16, -1, -1, -1, -1}, // Lv4
+ { 5, 1, 28, -1, 3, -1, 75, 50, -1, 4, 1000, 500, 16, -1, -1, -1, -1}, // Lv5
+ { 5, 2, -1, 25, 4, 15, 75, 50, 100, 4, 1000, -1, 16, -1, -1, -1, -1}, // Lv6A
+ { 5, 2, 190, 35, 4, -1, 75, 50, 100, 4, 1000, 500, 16, 140, 140, 140, 90}, // Lv6B
+ { -1, -1, -1, 65, -1, -1, -1, -1, 100, 4, 1000, 500, 16, -1, -1, -1, -1}, // Lv7
+ { 5, 3, 24, -1, 3, -1, 50, 50, -1, 4, 1000, 500, 16, -1, -1, -1, -1}, // Lv8
+ { 5, 4, 17, 45, 5, -1, 75, 50, 100, 4, 1000, 500, 16, 100, 100, 100, 140}, // Lv9
+ { 5, 12, -1, 35, 75, -1, 75, 50, 100, 4, 1000, 500, 16, 10, 6, 7, -1}, // Lv10
+ { 4, 2, 15, -1, 5, -1, 75, 50, -1, 0, 1000, 500, 16, 5, 6, 7, 8}, // Lv11
+ { 4, 1, -1, -1, 8, -1, 75, 50, -1, 4, 1000, 500, 16, -1, -1, -1, -1}, // Lv12
+ { 5, 5, 30, 35, 1, 20, 85, 50, 100, 4, 1000, 500, 16, -1, -1, -1, -1}, // Lv13
+ { 5, 2, 28, -1, 2, -1, 75, 50, -1, 4, 1000, 500, 16, -1, -1, -1, -1}, // Lv14
+ { 5, 7, -1, -1, 4, -1, 75, 50, -1, 4, 1000, -1, 16, -1, -1, -1, -1}, // Lv15A
+ { 5, 6, 255, 35, 4, 10, 75, 50, 100, 4, 1000, 500, 16, -1, -1, -1, -1}, // Lv15B
},
- // Difficulty 2 (Hard) - levels 1-15
+ // Difficulty 2 (Medium) - 17 level types
{
- { 38, 20, 7, 75}, { 35, 100, 6, 75}, { -1, 30, 4, 75},
- { 30, -1, 6, 75}, { 50, 200, 12, 75}, { 80, -1, -1, -1},
- { -1, 27, 3, 60}, { 60, 19, 7, 75}, { 40, -1, 100, 85},
- { -1, 20, 6, 75}, { -1, -1, 11, 75}, { 40, 40, 3, 76},
- { -1, 38, 4, 75}, { -1, -1, 7, 75}, { 40, 255, 7, 75},
+ { 7, 0, 35, -1, 5, -1, 75, 75, -1, 6, 1500, 750, 0, 7, 7, 8, -1}, // Lv1
+ { 4, 1, -1, -1, 6, -1, 40, 75, -1, 0, 1500, 750, 0, 110, 110, 150, 35}, // Lv2
+ { 6, 1, 20, 38, 7, 12, 75, 75, 150, 6, 1500, 750, 0, -1, -1, -1, -1}, // Lv3
+ { 5, 1, 100, 35, 6, 20, 75, 75, 150, 6, 1500, 750, 0, -1, -1, -1, -1}, // Lv4
+ { 6, 1, 30, -1, 4, -1, 75, 75, -1, 6, 1500, 750, 0, -1, -1, -1, -1}, // Lv5
+ { 6, 1, -1, 30, 6, 15, 75, 75, 150, 6, 1500, -1, 0, -1, -1, -1, -1}, // Lv6A
+ { 6, 1, 200, 50, 12, -1, 75, 75, 150, 6, 1500, 750, 0, 160, 160, 160, 105}, // Lv6B
+ { -1, -1, -1, 80, -1, -1, -1, -1, 150, 6, 1500, 750, 0, -1, -1, -1, -1}, // Lv7
+ { 5, 1, 27, -1, 3, -1, 60, 75, -1, 6, 1500, 750, 0, -1, -1, -1, -1}, // Lv8
+ { 5, 3, 19, 60, 7, -1, 75, 75, 150, 6, 1500, 750, 0, 110, 110, 110, 150}, // Lv9
+ { 5, 9, -1, 40, 100, -1, 85, 75, 150, 6, 1500, 750, 0, 11, 7, 8, -1}, // Lv10
+ { 4, 1, 20, -1, 6, -1, 75, 75, -1, 0, 1500, 750, 0, 6, 7, 8, 9}, // Lv11
+ { 4, 0, -1, -1, 11, -1, 75, 75, -1, 6, 1500, 750, 0, -1, -1, -1, -1}, // Lv12
+ { 5, 3, 40, 40, 3, 15, 76, 75, 150, 6, 1500, 750, 0, -1, -1, -1, -1}, // Lv13
+ { 5, 0, 38, -1, 4, -1, 75, 75, -1, 6, 1500, 750, 0, -1, -1, -1, -1}, // Lv14
+ { 5, 4, -1, -1, 7, -1, 75, 75, -1, 6, 1500, -1, 0, -1, -1, -1, -1}, // Lv15A
+ { 5, 5, 255, 40, 7, 10, 75, 75, 150, 6, 1500, 750, 0, -1, -1, -1, -1}, // Lv15B
},
- // Difficulty 3 (Expert) - levels 1-15
+ // Difficulty 3 (Hard) - 17 level types
{
- { 42, 23, 12, 75}, { 50, 120, 16, 75}, { -1, 55, 4, 75},
- { 50, 0, 15, 79}, { 90, 220, 15, 90}, { 90, -1, -1, -1},
- { -1, 35, 3, 68}, { 75, 30, 20, 80}, { 50, -1, 110, 90},
- { -1, 30, 7, 75}, { -1, -1, 13, 75}, { 55, 55, 5, 77},
- { -1, 49, 4, 75}, { -1, -1, 10, 79}, { 45, 255, 8, 80},
+ { 8, 0, 77, -1, 7, -1, 80, 100, -1, 8, 2000, 1000, 4, 8, 8, 9, -1}, // Lv1
+ { 4, 0, -1, -1, 7, -1, 40, 100, -1, 0, 2000, 1000, 4, 120, 120, 165, 40}, // Lv2
+ { 6, 0, 23, 42, 12, 10, 75, 100, 200, 8, 2000, 1000, 4, -1, -1, -1, -1}, // Lv3
+ { 5, 0, 120, 50, 16, 10, 75, 100, 200, 8, 2000, 1000, 4, -1, -1, -1, -1}, // Lv4
+ { 5, 0, 55, -1, 4, -1, 75, 100, -1, 8, 2000, 1000, 4, -1, -1, -1, -1}, // Lv5
+ { 6, 0, 0, 50, 15, 15, 79, 100, 200, 8, 2000, -1, 4, -1, -1, -1, -1}, // Lv6A
+ { 6, 0, 220, 90, 15, -1, 90, 100, 200, 8, 2000, 1000, 4, 180, 180, 180, 140}, // Lv6B
+ { -1, -1, -1, 90, -1, -1, -1, -1, 200, 8, 2000, 1000, 4, -1, -1, -1, -1}, // Lv7
+ { 5, 0, 35, -1, 3, -1, 68, 100, -1, 8, 2000, 1000, 4, -1, -1, -1, -1}, // Lv8
+ { 5, 2, 30, 75, 20, -1, 80, 100, 200, 8, 2000, 1000, 4, 120, 120, 120, 200}, // Lv9
+ { 5, 8, -1, 50, 110, -1, 90, 100, 200, 8, 2000, 1000, 4, 12, 8, 9, -1}, // Lv10
+ { 4, 0, 30, -1, 7, -1, 75, 100, -1, 0, 2000, 1000, 4, 7, 8, 9, 10}, // Lv11
+ { 4, 0, -1, -1, 13, -1, 75, 100, -1, 8, 2000, 1000, 4, -1, -1, -1, -1}, // Lv12
+ { 5, 3, 55, 55, 5, 12, 77, 100, 200, 8, 2000, 1000, 4, -1, -1, -1, -1}, // Lv13
+ { 5, 0, 49, -1, 4, -1, 75, 100, -1, 8, 2000, 1000, 4, -1, -1, -1, -1}, // Lv14
+ { 5, 4, -1, -1, 10, -1, 79, 100, -1, 8, 2000, -1, 4, -1, -1, -1, -1}, // Lv15A
+ { 5, 4, 255, 45, 8, 5, 80, 100, 200, 8, 2000, 1000, 4, -1, -1, -1, -1}, // Lv15B
},
- // Difficulty 4 (identical to difficulty 2 in original data) - levels 1-15
+ // Difficulty 4 (Jedi) â identical to difficulty 2 (Medium) in original data
{
- { 38, 20, 7, 75}, { 35, 100, 6, 75}, { -1, 30, 4, 75},
- { 30, -1, 6, 75}, { 50, 200, 12, 75}, { 80, -1, -1, -1},
- { -1, 27, 3, 60}, { 60, 19, 7, 75}, { 40, -1, 100, 85},
- { -1, 20, 6, 75}, { -1, -1, 11, 75}, { 40, 40, 3, 76},
- { -1, 38, 4, 75}, { -1, -1, 7, 75}, { 40, 255, 7, 75},
+ { 7, 0, 35, -1, 5, -1, 75, 75, -1, 6, 1500, 750, 0, 7, 7, 8, -1}, // Lv1
+ { 4, 1, -1, -1, 6, -1, 40, 75, -1, 0, 1500, 750, 0, 110, 110, 150, 35}, // Lv2
+ { 6, 1, 20, 38, 7, 12, 75, 75, 150, 6, 1500, 750, 0, -1, -1, -1, -1}, // Lv3
+ { 5, 1, 100, 35, 6, 20, 75, 75, 150, 6, 1500, 750, 0, -1, -1, -1, -1}, // Lv4
+ { 6, 1, 30, -1, 4, -1, 75, 75, -1, 6, 1500, 750, 0, -1, -1, -1, -1}, // Lv5
+ { 6, 1, -1, 30, 6, 15, 75, 75, 150, 6, 1500, -1, 0, -1, -1, -1, -1}, // Lv6A
+ { 6, 1, 200, 50, 12, -1, 75, 75, 150, 6, 1500, 750, 0, 160, 160, 160, 105}, // Lv6B
+ { -1, -1, -1, 80, -1, -1, -1, -1, 150, 6, 1500, 750, 0, -1, -1, -1, -1}, // Lv7
+ { 5, 1, 27, -1, 3, -1, 60, 75, -1, 6, 1500, 750, 0, -1, -1, -1, -1}, // Lv8
+ { 5, 3, 19, 60, 7, -1, 75, 75, 150, 6, 1500, 750, 0, 110, 110, 110, 150}, // Lv9
+ { 5, 9, -1, 40, 100, -1, 85, 75, 150, 6, 1500, 750, 0, 11, 7, 8, -1}, // Lv10
+ { 4, 1, 20, -1, 6, -1, 75, 75, -1, 0, 1500, 750, 0, 6, 7, 8, 9}, // Lv11
+ { 4, 0, -1, -1, 11, -1, 75, 75, -1, 6, 1500, 750, 0, -1, -1, -1, -1}, // Lv12
+ { 5, 3, 40, 40, 3, 15, 76, 75, 150, 6, 1500, 750, 0, -1, -1, -1, -1}, // Lv13
+ { 5, 0, 38, -1, 4, -1, 75, 75, -1, 6, 1500, 750, 0, -1, -1, -1, -1}, // Lv14
+ { 5, 4, -1, -1, 7, -1, 75, 75, -1, 6, 1500, -1, 0, -1, -1, -1, -1}, // Lv15A
+ { 5, 5, 255, 40, 7, 10, 75, 75, 150, 6, 1500, 750, 0, -1, -1, -1, -1}, // Lv15B
},
};
-InsaneRebel2::LevelDifficultyParams InsaneRebel2::getDifficultyParams(int levelId) const {
+InsaneRebel2::LevelDifficultyParams InsaneRebel2::getDifficultyParams() const {
int diff = CLIP(_difficulty, 0, 4);
- int lvIdx = CLIP(levelId - 1, 0, 14); // levelId 1-15 â index 0-14
+ int lvIdx = CLIP((int)_rebelLevelType, 0, 16);
return kDifficultyTable[diff][lvIdx];
}
-// Score lookup tables (from DAT_0047e0fe, DAT_0047e100, DAT_0047e102)
-// These are indexed by: DAT_0047a7fa * 0x242 + DAT_0047a7f8 * 0x22
-// For simplicity, we use fixed values based on difficulty level
-// Values estimated from disassembly patterns (actual values would need extraction from game data)
-const int16 InsaneRebel2::kScoreTableEnemyDestroy[16] = {
- 100, 100, 100, 100, // Easy (difficulty 0)
- 150, 150, 150, 150, // Medium (difficulty 1)
- 200, 200, 200, 200, // Hard (difficulty 2)
- 250, 250, 250, 250 // Expert (difficulty 3)
-};
-
-const int16 InsaneRebel2::kScoreTableSpecial[16] = {
- 50, 50, 50, 50,
- 75, 75, 75, 75,
- 100, 100, 100, 100,
- 125, 125, 125, 125
-};
-
-const int16 InsaneRebel2::kScoreTableTimeBonus[16] = {
- 1, 1, 1, 1,
- 2, 2, 2, 2,
- 3, 3, 3, 3,
- 4, 4, 4, 4
-};
-
// Score system implementation (FUN_0041bf8d equivalent)
// Adds points to score and awards bonus life when crossing threshold
void InsaneRebel2::addScore(int points) {
@@ -948,23 +987,16 @@ int32 InsaneRebel2::processMouse() {
it->id, it->type, mousePos.x, mousePos.y,
it->rect.left, it->rect.top, it->rect.right, it->rect.bottom);
- // Spawn visual explosion based on handler and enemy type.
- //
- // Each handler's explosion rendering (FUN_409FBC, FUN_402696,
- // FUN_40F1C5, FUN_41F29A) checks a per-level flags field:
- // (*(ushort *)(&DAT_0047e108 + chapter*0x242 + level*0x22) & 1) == 0
- // When bit 0 is SET, explosion NUT sprites are NOT rendered even
- // though the counter ticks down. The flags come from GAME.TRS.
+ // Spawn visual explosion based on handler, enemy type, and flags.
//
- // Handler 8 (FUN_4028C5 line 94): Only type 0 sets the explosion
- // counter at all. Types 1-4 get BLAST sound, no visual explosion.
- //
- // Handler 25 (FUN_41E7C2 line 74): Types > 3 DO set the counter,
- // but rendering is suppressed by flags & 1 for on-foot levels.
- // The counter serves only as a timer (sound panning, tracking).
- // Handler 25 is specifically for on-foot corridor/FPS sections;
- // space combat uses handler 7 instead.
+ // Rendering functions (FUN_409FBC, FUN_402696, FUN_40F1C5,
+ // FUN_41F29A) check DAT_0047e108 flags & 1 â when set,
+ // explosion NUT sprites are suppressed. This is checked
+ // during rendering in renderExplosions().
//
+ // Handler 8 (FUN_4028C5): Only type 0 spawns explosion.
+ // Handler 25 (FUN_41E7C2): Types > 3 set counter but
+ // rendering suppressed by flags bit 0.
// Handlers 0x26, 7: All types get visual explosions.
if (_rebelHandler != 8 && _rebelHandler != 25) {
spawnExplosion((it->rect.left + it->rect.right) / 2,
@@ -974,6 +1006,12 @@ int32 InsaneRebel2::processMouse() {
spawnExplosion((it->rect.left + it->rect.right) / 2,
(it->rect.top + it->rect.bottom) / 2,
it->rect.width() / 2);
+ } else if (_rebelHandler == 25 && it->type > 3) {
+ // Counter is set for timing/sound, but rendering
+ // may be suppressed by flags bit 0
+ spawnExplosion((it->rect.left + it->rect.right) / 2,
+ (it->rect.top + it->rect.bottom) / 2,
+ it->rect.width() / 2);
}
// Disable self (prevents sprite from rendering via SKIP chunks)
@@ -1023,10 +1061,12 @@ int32 InsaneRebel2::processMouse() {
}
// Award score for destroying enemy (FUN_0041bf8d called from FUN_40A2E0)
- // Score value comes from lookup table DAT_0047e0fe indexed by difficulty
- int scoreIndex = _difficulty * 4; // Simplified index
- if (scoreIndex >= 0 && scoreIndex < 16) {
- addScore(kScoreTableEnemyDestroy[scoreIndex]);
+ // Score value comes from DAT_0047e0fe indexed by difficultyÃlevel
+ {
+ LevelDifficultyParams dparams = getDifficultyParams();
+ if (dparams.hitPoints > 0) {
+ addScore(dparams.hitPoints);
+ }
}
// Only hit one enemy per click
diff --git a/engines/scumm/insane/insane_rebel.h b/engines/scumm/insane/insane_rebel.h
index bb2abaa4cd4..73f780de4a1 100644
--- a/engines/scumm/insane/insane_rebel.h
+++ b/engines/scumm/insane/insane_rebel.h
@@ -983,37 +983,45 @@ public:
// ======================= Per-Level Difficulty Parameters =======================
// Extracted from RA2WIN95.EXE at VA 0x47e0f0
- // 2D table indexed by difficulty (0-4) Ã game level (1-15)
- // Original indexing: &DAT_0047e0f6 + chapter * 0x242 + level * 0x22
- // -1 = not applicable for this level type (e.g., no walls in turret levels)
+ // 2D table indexed by difficulty (0-4) Ã level type (0-16)
+ // Original indexing: &DAT_0047e0f0 + chapter * 0x242 + levelType * 0x22
+ // Level type (_rebelLevelType) is set by IACT opcode 6 par3
+ // 17 entries: Lv1-5(0-4), Lv6A/6B(5-6), Lv7-14(7-14), Lv15A/15B(15-16)
+ // -1 = not applicable for this level type
struct LevelDifficultyParams {
- int16 wallDamage; // +0x06 DAT_0047e0f6: Wall/obstacle collision damage
- int16 directHitDamage; // +0x04 DAT_0047e0f4: Direct hit damage (par4=100)
- int16 enemyProjectileDamage;// +0x08 DAT_0047e0f8: Enemy projectile/probabilistic damage
- int16 hitProbability; // +0x0C DAT_0047e0fc: Enemy hit chance (0-100, -1=disabled)
+ int16 laserDelay; // +0x00: Laser fire delay (lower = faster)
+ int16 snapDistance; // +0x02: Crosshair snap distance to targets
+ int16 missDamage; // +0x04: Damage from enemy misses / grazing hits
+ int16 dodgeDamage; // +0x06: Damage from wall/obstacle collisions
+ int16 shotDamage; // +0x08: Damage from enemy projectile hits
+ int16 specialDamage; // +0x0A: Damage from special attacks
+ int16 shotAccuracy; // +0x0C: Enemy shot accuracy (0-100, -1=disabled)
+ int16 hitPoints; // +0x0E: Points awarded for destroying an enemy
+ int16 dodgePoints; // +0x10: Points awarded for dodging an obstacle
+ int16 timePoints; // +0x12: Time bonus points
+ int16 levelPoints; // +0x14: End-of-level bonus points
+ int16 specialPoints; // +0x16: Special action bonus points
+ int16 flags; // +0x18: Behavior flags bitfield
+ int16 rollRate; // +0x1A: Ship roll rate (flight controls)
+ int16 liftRate; // +0x1C: Ship lift rate (flight controls)
+ int16 slideRate; // +0x1E: Ship slide rate (flight controls)
+ int16 driftRate; // +0x20: Ship drift rate (flight controls)
};
- // Table: 5 difficulty levels à 15 game levels
- // Difficulty 4 is identical to difficulty 2 in the original data
- static const LevelDifficultyParams kDifficultyTable[5][15];
+ // Table: 5 difficulty levels à 17 level types
+ // Difficulty 4 (Jedi) is identical to difficulty 2 (Medium) in the original data
+ static const LevelDifficultyParams kDifficultyTable[5][17];
- // Look up difficulty parameters for current difficulty and level
- // Returns the entry from kDifficultyTable, clamping difficulty to 0-4
- // levelId is 1-based (1-15)
- LevelDifficultyParams getDifficultyParams(int levelId) const;
+ // Look up difficulty parameters for current _difficulty and _rebelLevelType
+ LevelDifficultyParams getDifficultyParams() const;
// Score system (FUN_0041bf8d equivalent)
// Adds points to score and awards bonus life when crossing threshold
void addScore(int points);
- // Score lookup tables (indices into per-level point values)
- // DAT_0047e0fe: Points for destroying enemies
- // DAT_0047e100: Points for certain special events
- // DAT_0047e102: Points awarded per frame (time bonus)
- static const int16 kScoreTableEnemyDestroy[16]; // Per difficulty/level
- static const int16 kScoreTableSpecial[16];
- static const int16 kScoreTableTimeBonus[16];
+ // Score lookup uses LevelDifficultyParams fields:
+ // hitPoints (DAT_0047e0fe), dodgePoints (DAT_0047e100), timePoints (DAT_0047e102)
// Render score text to HUD (called from procPostRendering)
void renderScoreHUD(byte *renderBitmap, int pitch, int width, int height, int statusBarY);
diff --git a/engines/scumm/insane/insane_rebel_iact.cpp b/engines/scumm/insane/insane_rebel_iact.cpp
index a6ff44b1aaf..72697f8c4b0 100644
--- a/engines/scumm/insane/insane_rebel_iact.cpp
+++ b/engines/scumm/insane/insane_rebel_iact.cpp
@@ -434,14 +434,14 @@ void InsaneRebel2::iactRebel2Opcode3(Common::SeekableReadStream &b, int16 par2,
srcIdBody1, isBitSet(srcIdBody1), _rebelDamageLevel);
if (_rebelDamageLevel < 2 && !isBitSet(srcIdBody1)) {
- LevelDifficultyParams params = getDifficultyParams(_selectedLevel);
- int probability = (params.hitProbability >= 0) ? params.hitProbability : 0;
+ LevelDifficultyParams params = getDifficultyParams();
+ int probability = (params.shotAccuracy >= 0) ? params.shotAccuracy : 0;
int roll = _vm->_rnd.getRandomNumber(99);
debug("Rebel2 Opcode3: probability=%d roll=%d (need roll < prob)", probability, roll);
if (roll < probability) {
if (!_rebelInvulnerable) {
- int damageAmount = (params.enemyProjectileDamage >= 0) ? params.enemyProjectileDamage : 0;
+ int damageAmount = (params.shotDamage >= 0) ? params.shotDamage : 0;
_playerDamage += damageAmount;
if (_playerDamage > 255) _playerDamage = 255;
debug("Rebel2: H25 PROBABILISTIC damage from %d. Damage=%d total=%d",
@@ -465,8 +465,8 @@ void InsaneRebel2::iactRebel2Opcode3(Common::SeekableReadStream &b, int16 par2,
// Direct damage: par4==100, separate from par3 branches (lines 99-111)
if (par4 == 100 && !isBitSet(srcIdBody0)) {
if (!_rebelInvulnerable) {
- LevelDifficultyParams dparams = getDifficultyParams(_selectedLevel);
- int directHitDamage = (dparams.directHitDamage >= 0) ? dparams.directHitDamage : 0;
+ LevelDifficultyParams dparams = getDifficultyParams();
+ int directHitDamage = (dparams.missDamage >= 0) ? dparams.missDamage : 0;
_playerDamage += directHitDamage;
if (_playerDamage > 255) _playerDamage = 255;
debug("Rebel2: H25 DIRECT HIT par4=100 damage=%d total=%d",
@@ -485,8 +485,8 @@ void InsaneRebel2::iactRebel2Opcode3(Common::SeekableReadStream &b, int16 par2,
_rebelHitCounter++;
debug("Rebel2: Incremented hit counter -> %d", _rebelHitCounter);
- LevelDifficultyParams dparams = getDifficultyParams(_selectedLevel);
- int directHitDamage = (dparams.directHitDamage >= 0) ? dparams.directHitDamage : 0;
+ LevelDifficultyParams dparams = getDifficultyParams();
+ int directHitDamage = (dparams.missDamage >= 0) ? dparams.missDamage : 0;
if (par4 != 0 && directHitDamage > 0) {
bool shouldDamage = false;
@@ -518,15 +518,15 @@ void InsaneRebel2::iactRebel2Opcode3(Common::SeekableReadStream &b, int16 par2,
debug("Rebel2 Opcode3: par3=5 srcId=%d isBitSet=%d", srcId, isBitSet(srcId));
if (!isBitSet(srcId)) {
- LevelDifficultyParams params = getDifficultyParams(_selectedLevel);
- int probability = (params.hitProbability >= 0) ? params.hitProbability : 0;
+ LevelDifficultyParams params = getDifficultyParams();
+ int probability = (params.shotAccuracy >= 0) ? params.shotAccuracy : 0;
int roll = _vm->_rnd.getRandomNumber(99);
debug("Rebel2 Opcode3: probability=%d roll=%d (need roll < prob)", probability, roll);
if (roll < probability) {
if (!_rebelInvulnerable) {
- int damageAmount = (params.enemyProjectileDamage >= 0) ? params.enemyProjectileDamage : 0;
+ int damageAmount = (params.shotDamage >= 0) ? params.shotDamage : 0;
_playerDamage += damageAmount;
if (_playerDamage > 255) _playerDamage = 255;
debug("Rebel2: PROBABILISTIC damage from enemy %d. Damage=%d total=%d",
@@ -860,8 +860,8 @@ void InsaneRebel2::iactRebel2Opcode6(byte *renderBitmap, Common::SeekableReadStr
// --- Step 7: Corridor collision â mode 0/2 only (lines 257-292) ---
if (_flyControlMode == 0 || _flyControlMode == 2) {
- LevelDifficultyParams wallParams = getDifficultyParams(_selectedLevel);
- int corridorWallDmg = (wallParams.wallDamage >= 0) ? wallParams.wallDamage : 0;
+ LevelDifficultyParams wallParams = getDifficultyParams();
+ int corridorWallDmg = (wallParams.dodgeDamage >= 0) ? wallParams.dodgeDamage : 0;
// Right boundary (lines 258-270)
// Original: position is ALWAYS clamped; damage/bounce only when cooldown < 5
diff --git a/engines/scumm/insane/insane_rebel_render.cpp b/engines/scumm/insane/insane_rebel_render.cpp
index 9f59af3d288..93a8a79cbee 100644
--- a/engines/scumm/insane/insane_rebel_render.cpp
+++ b/engines/scumm/insane/insane_rebel_render.cpp
@@ -1177,8 +1177,8 @@ void InsaneRebel2::checkCollisionZones() {
if (collision) {
// Collision detected â apply damage from collision damage table
// Original: DAT_0047a7ec += DAT_0047e0f6[chapter * 0x242 + level * 0x22]
- LevelDifficultyParams params = getDifficultyParams(_selectedLevel);
- int collisionDamage = (params.wallDamage >= 0) ? params.wallDamage : 0;
+ LevelDifficultyParams params = getDifficultyParams();
+ int collisionDamage = (params.dodgeDamage >= 0) ? params.dodgeDamage : 0;
if (!_rebelInvulnerable) {
_playerDamage += collisionDamage;
@@ -1192,7 +1192,10 @@ void InsaneRebel2::checkCollisionZones() {
} else {
// Safely passed â award score bonus
// Original: FUN_0041bf8d(DAT_0047e100[levelIdx])
- addScore(1);
+ LevelDifficultyParams scoreParams = getDifficultyParams();
+ if (scoreParams.dodgePoints > 0) {
+ addScore(scoreParams.dodgePoints);
+ }
}
}
}
@@ -1254,8 +1257,8 @@ void InsaneRebel2::checkHandler7CollisionZones() {
_hitCooldown = 10;
_spaceShotDirection = zone.filterValue + 2;
- LevelDifficultyParams params = getDifficultyParams(_selectedLevel);
- int collisionDamage = (params.wallDamage >= 0) ? params.wallDamage : 0;
+ LevelDifficultyParams params = getDifficultyParams();
+ int collisionDamage = (params.dodgeDamage >= 0) ? params.dodgeDamage : 0;
if (!_rebelInvulnerable) {
_playerDamage += collisionDamage;
if (_playerDamage > 255) _playerDamage = 255;
@@ -1269,7 +1272,11 @@ void InsaneRebel2::checkHandler7CollisionZones() {
break; // Only one collision per frame (original breaks)
} else {
// Safely avoided obstacle â award score
- addScore(1);
+ // Original: FUN_0041bf8d(DAT_0047e100[levelIdx])
+ LevelDifficultyParams scoreParams = getDifficultyParams();
+ if (scoreParams.dodgePoints > 0) {
+ addScore(scoreParams.dodgePoints);
+ }
}
}
}
@@ -1283,8 +1290,8 @@ void InsaneRebel2::checkHandler7CollisionZones() {
// Ship position is clamped to wall boundaries when hitting.
int16 hMargin = (_flyControlMode == 1) ? 0x28 : 0x0f; // local_14
const int16 vMargin = 0x0f; // local_20
- LevelDifficultyParams wallParams = getDifficultyParams(_selectedLevel);
- int wallDamage = (wallParams.wallDamage >= 0) ? wallParams.wallDamage : 0;
+ LevelDifficultyParams wallParams = getDifficultyParams();
+ int wallDamage = (wallParams.dodgeDamage >= 0) ? wallParams.dodgeDamage : 0;
for (int i = 0; i < _primaryZoneCount; i++) {
CollisionZone &zone = _primaryZones[i];
@@ -2693,7 +2700,25 @@ void InsaneRebel2::renderEnemyOverlays(byte *renderBitmap, int pitch, int width,
// Dispatcher â calls per-handler explosion render function.
// Original code has separate functions per handler, each with its own
// position transformation, scale thresholds, and secondary NUT rendering.
+// Each handler's render function checks DAT_0047e108 flags & 1:
+// when bit 0 is set, explosion NUT sprites are suppressed (counter still ticks).
void InsaneRebel2::renderExplosions(byte *renderBitmap, int pitch, int width, int height) {
+ // Check flags bit 0: suppress explosion sprite rendering
+ LevelDifficultyParams dparams = getDifficultyParams();
+ bool suppressExplosionSprites = (dparams.flags & 1) != 0;
+
+ // Even when suppressed, still tick down explosion counters
+ if (suppressExplosionSprites) {
+ for (int i = 0; i < 5; i++) {
+ if (_explosions[i].active && _explosions[i].counter > 0) {
+ _explosions[i].counter--;
+ if (_explosions[i].counter <= 0)
+ _explosions[i].active = false;
+ }
+ }
+ return;
+ }
+
switch (_rebelHandler) {
case 0x26:
renderTurretExplosions(renderBitmap, pitch, width, height);
Commit: 9506f4af42656c1b2f60b1d808e6c7161ed42b7c
https://github.com/scummvm/scummvm/commit/9506f4af42656c1b2f60b1d808e6c7161ed42b7c
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:12+02:00
Commit Message:
SCUMM: RA2: Apply code conventions
Changed paths:
engines/scumm/insane/insane_rebel_iact.cpp
engines/scumm/insane/insane_rebel_levels.cpp
engines/scumm/insane/insane_rebel_menu.cpp
engines/scumm/insane/insane_rebel_render.cpp
engines/scumm/insane/insane_rebel_runlevels.cpp
diff --git a/engines/scumm/insane/insane_rebel_iact.cpp b/engines/scumm/insane/insane_rebel_iact.cpp
index 72697f8c4b0..a4300324b92 100644
--- a/engines/scumm/insane/insane_rebel_iact.cpp
+++ b/engines/scumm/insane/insane_rebel_iact.cpp
@@ -443,7 +443,8 @@ void InsaneRebel2::iactRebel2Opcode3(Common::SeekableReadStream &b, int16 par2,
if (!_rebelInvulnerable) {
int damageAmount = (params.shotDamage >= 0) ? params.shotDamage : 0;
_playerDamage += damageAmount;
- if (_playerDamage > 255) _playerDamage = 255;
+ if (_playerDamage > 255)
+ _playerDamage = 255;
debug("Rebel2: H25 PROBABILISTIC damage from %d. Damage=%d total=%d",
srcIdBody1, damageAmount, _playerDamage);
}
@@ -468,7 +469,8 @@ void InsaneRebel2::iactRebel2Opcode3(Common::SeekableReadStream &b, int16 par2,
LevelDifficultyParams dparams = getDifficultyParams();
int directHitDamage = (dparams.missDamage >= 0) ? dparams.missDamage : 0;
_playerDamage += directHitDamage;
- if (_playerDamage > 255) _playerDamage = 255;
+ if (_playerDamage > 255)
+ _playerDamage = 255;
debug("Rebel2: H25 DIRECT HIT par4=100 damage=%d total=%d",
directHitDamage, _playerDamage);
}
@@ -502,7 +504,8 @@ void InsaneRebel2::iactRebel2Opcode3(Common::SeekableReadStream &b, int16 par2,
if (shouldDamage) {
if (!_rebelInvulnerable) {
_playerDamage += directHitDamage;
- if (_playerDamage > 255) _playerDamage = 255;
+ if (_playerDamage > 255)
+ _playerDamage = 255;
debug("Rebel2: DIRECT HIT damage from enemy %d. par3=%d par4=%d damage=%d total=%d",
srcId, par3, par4, directHitDamage, _playerDamage);
}
@@ -528,7 +531,8 @@ void InsaneRebel2::iactRebel2Opcode3(Common::SeekableReadStream &b, int16 par2,
if (!_rebelInvulnerable) {
int damageAmount = (params.shotDamage >= 0) ? params.shotDamage : 0;
_playerDamage += damageAmount;
- if (_playerDamage > 255) _playerDamage = 255;
+ if (_playerDamage > 255)
+ _playerDamage = 255;
debug("Rebel2: PROBABILISTIC damage from enemy %d. Damage=%d total=%d",
srcId, damageAmount, _playerDamage);
}
@@ -631,11 +635,15 @@ void InsaneRebel2::iactRebel2Opcode6(byte *renderBitmap, Common::SeekableReadStr
// Clamp X offset to movement range limit (covered/shooting state)
// Based on FUN_00401234 lines 119-136
- if (mouseOffsetX > _movementRangeLimit) mouseOffsetX = _movementRangeLimit;
- if (mouseOffsetX < -_movementRangeLimit) mouseOffsetX = -_movementRangeLimit;
+ if (mouseOffsetX > _movementRangeLimit)
+ mouseOffsetX = _movementRangeLimit;
+ if (mouseOffsetX < -_movementRangeLimit)
+ mouseOffsetX = -_movementRangeLimit;
// Y offset always uses full range (±127)
- if (mouseOffsetY > 127) mouseOffsetY = 127;
- if (mouseOffsetY < -127) mouseOffsetY = -127;
+ if (mouseOffsetY > 127)
+ mouseOffsetY = 127;
+ if (mouseOffsetY < -127)
+ mouseOffsetY = -127;
// Calculate target positions using the original formula
_shipTargetX = (int16)(((mouseOffsetX * 5 + 0x27b) * 0x40) / 0xfe);
@@ -673,20 +681,32 @@ void InsaneRebel2::iactRebel2Opcode6(byte *renderBitmap, Common::SeekableReadStr
}
// Horizontal: 5 zones (0=far left, 2=center, 4=far right)
- if (mouseX < 64) _shipDirectionH = 0;
- else if (mouseX < 128) _shipDirectionH = 1;
- else if (mouseX < 192) _shipDirectionH = 2;
- else if (mouseX < 256) _shipDirectionH = 3;
- else _shipDirectionH = 4;
+ if (mouseX < 64)
+ _shipDirectionH = 0;
+ else if (mouseX < 128)
+ _shipDirectionH = 1;
+ else if (mouseX < 192)
+ _shipDirectionH = 2;
+ else if (mouseX < 256)
+ _shipDirectionH = 3;
+ else
+ _shipDirectionH = 4;
// Vertical: 7 zones (0=far up, 3=center, 6=far down)
- if (mouseY < 28) _shipDirectionV = 0;
- else if (mouseY < 57) _shipDirectionV = 1;
- else if (mouseY < 86) _shipDirectionV = 2;
- else if (mouseY < 114) _shipDirectionV = 3;
- else if (mouseY < 143) _shipDirectionV = 4;
- else if (mouseY < 171) _shipDirectionV = 5;
- else _shipDirectionV = 6;
+ if (mouseY < 28)
+ _shipDirectionV = 0;
+ else if (mouseY < 57)
+ _shipDirectionV = 1;
+ else if (mouseY < 86)
+ _shipDirectionV = 2;
+ else if (mouseY < 114)
+ _shipDirectionV = 3;
+ else if (mouseY < 143)
+ _shipDirectionV = 4;
+ else if (mouseY < 171)
+ _shipDirectionV = 5;
+ else
+ _shipDirectionV = 6;
_shipDirectionIndex = _shipDirectionH * 7 + _shipDirectionV;
}
@@ -757,10 +777,14 @@ void InsaneRebel2::iactRebel2Opcode6(byte *renderBitmap, Common::SeekableReadStr
int16 inputY = (int16)(_vm->_mouse.y - 100); // DAT_0047a7e2
// Clamp: mouse mode uses [-160, 160] for X, [-127, 127] for Y (lines 55-70)
- if (inputX > 160) inputX = 160;
- if (inputX < -160) inputX = -160;
- if (inputY > 127) inputY = 127;
- if (inputY < -127) inputY = -127;
+ if (inputX > 160)
+ inputX = 160;
+ if (inputX < -160)
+ inputX = -160;
+ if (inputY > 127)
+ inputY = 127;
+ if (inputY < -127)
+ inputY = -127;
// --- Step 2: Scale to [-127, 127] (lines 82-84) ---
// Mouse mode: local_c = (DAT_0047a7e0 * 0x7f) / 0xa0
@@ -827,8 +851,10 @@ void InsaneRebel2::iactRebel2Opcode6(byte *renderBitmap, Common::SeekableReadStr
}
// Clamp X delta to ±12 per frame (lines 187-192 / 231-236)
- if (positionDeltaX < -11) positionDeltaX = -12;
- if (positionDeltaX > 11) positionDeltaX = 12;
+ if (positionDeltaX < -11)
+ positionDeltaX = -12;
+ if (positionDeltaX > 11)
+ positionDeltaX = 12;
// Apply X delta (line 193 / 237)
_flyShipScreenX += positionDeltaX;
@@ -838,8 +864,10 @@ void InsaneRebel2::iactRebel2Opcode6(byte *renderBitmap, Common::SeekableReadStr
// Mode 1: clamped to ±12 with wind (lines 194-216)
int yCalc = levelYSpeed * local_14 - (windEffectY >> 1);
int yDelta = yCalc >> 10;
- if (yDelta < -12) yDelta = -12;
- if (yDelta > 12) yDelta = 12;
+ if (yDelta < -12)
+ yDelta = -12;
+ if (yDelta > 12)
+ yDelta = 12;
_flyShipScreenY -= (int16)yDelta;
} else {
// Mode 0/2/3: unclamped (lines 238-241)
@@ -853,10 +881,14 @@ void InsaneRebel2::iactRebel2Opcode6(byte *renderBitmap, Common::SeekableReadStr
_facingRight = (0xd4 < _smoothedVelocity + _flyShipScreenX);
// --- Step 6: Position clamping (lines 245-256) ---
- if (_flyShipScreenX > 0x194) _flyShipScreenX = 0x194; // 404
- if (_flyShipScreenY > 0xF0) _flyShipScreenY = 0xF0; // 240
- if (_flyShipScreenX < 0x14) _flyShipScreenX = 0x14; // 20
- if (_flyShipScreenY < 0x14) _flyShipScreenY = 0x14; // 20
+ if (_flyShipScreenX > 0x194)
+ _flyShipScreenX = 0x194; // 404
+ if (_flyShipScreenY > 0xF0)
+ _flyShipScreenY = 0xF0; // 240
+ if (_flyShipScreenX < 0x14)
+ _flyShipScreenX = 0x14; // 20
+ if (_flyShipScreenY < 0x14)
+ _flyShipScreenY = 0x14; // 20
// --- Step 7: Corridor collision â mode 0/2 only (lines 257-292) ---
if (_flyControlMode == 0 || _flyControlMode == 2) {
@@ -868,13 +900,15 @@ void InsaneRebel2::iactRebel2Opcode6(byte *renderBitmap, Common::SeekableReadStr
if (_corridorRightX < _flyShipScreenX) {
_flyShipScreenX = _corridorRightX;
if (_hitCooldown < 5) {
- for (int i = 0; i < 25; i++) _velocityHistory[i] = -127;
+ for (int i = 0; i < 25; i++)
+ _velocityHistory[i] = -127;
_hitCooldown = 10;
_spaceShotDirection = 1;
initDamageFlash();
if (!_rebelInvulnerable) {
_playerDamage += corridorWallDmg;
- if (_playerDamage > 255) _playerDamage = 255;
+ if (_playerDamage > 255)
+ _playerDamage = 255;
}
_rebelHitCounter++;
playSfx(1, 127, 100); // CRASH.SAD, right wall â pan right
@@ -884,13 +918,15 @@ void InsaneRebel2::iactRebel2Opcode6(byte *renderBitmap, Common::SeekableReadStr
if (_flyShipScreenX < _corridorLeftX) {
_flyShipScreenX = _corridorLeftX;
if (_hitCooldown < 5) {
- for (int i = 0; i < 25; i++) _velocityHistory[i] = 127;
+ for (int i = 0; i < 25; i++)
+ _velocityHistory[i] = 127;
_hitCooldown = 10;
_spaceShotDirection = 0;
initDamageFlash();
if (!_rebelInvulnerable) {
_playerDamage += corridorWallDmg;
- if (_playerDamage > 255) _playerDamage = 255;
+ if (_playerDamage > 255)
+ _playerDamage = 255;
}
_rebelHitCounter++;
playSfx(1, 127, -100); // CRASH.SAD, left wall â pan left
@@ -918,7 +954,8 @@ void InsaneRebel2::iactRebel2Opcode6(byte *renderBitmap, Common::SeekableReadStr
} else {
_perspectiveX = 0;
}
- if (_flyShipScreenX < 0xd5) _perspectiveX = -_perspectiveX;
+ if (_flyShipScreenX < 0xd5)
+ _perspectiveX = -_perspectiveX;
int absOffY = ABS(_flyShipScreenY - 0x82);
int16 focalY = 0x19; // Far view default for Level 3
@@ -928,25 +965,32 @@ void InsaneRebel2::iactRebel2Opcode6(byte *renderBitmap, Common::SeekableReadStr
} else {
_perspectiveY = 0;
}
- if (_flyShipScreenY < 0x83) _perspectiveY = -_perspectiveY;
+ if (_flyShipScreenY < 0x83)
+ _perspectiveY = -_perspectiveY;
}
// View shift = clamped smoothed velocity (FUN_0040d836 lines 68-74)
_viewShift = _smoothedVelocity;
- if (_viewShift > 127) _viewShift = 127;
- if (_viewShift < -127) _viewShift = -127;
+ if (_viewShift > 127)
+ _viewShift = 127;
+ if (_viewShift < -127)
+ _viewShift = -127;
// --- Step 9: Direction sprite (FUN_0040d836 lines 88-106) ---
// 5x7 grid: vDir(0-4) * 7 + hDir(0-6) = sprite index (0-34)
// vDir from vertical input: (0xa0 - verticalInput) >> 6
int16 vDir = (int16)(((int)(0xa0 - _verticalInput) + ((0xa0 - _verticalInput) < 0 ? 63 : 0)) >> 6);
- if (vDir < 0) vDir = 0;
- if (vDir > 4) vDir = 4;
+ if (vDir < 0)
+ vDir = 0;
+ if (vDir > 4)
+ vDir = 4;
// hDir from smoothed velocity: (0x95 - smoothedVelocity) / 0x2b
int16 hDir = (int16)((0x95 - _smoothedVelocity) / 0x2b);
- if (hDir < 0) hDir = 0;
- if (hDir > 6) hDir = 6;
+ if (hDir < 0)
+ hDir = 0;
+ if (hDir > 6)
+ hDir = 6;
// Hysteresis at center (lines 90-97, 98-105)
if (hDir == 3 && ABS(_smoothedVelocity) > 10) {
@@ -957,8 +1001,10 @@ void InsaneRebel2::iactRebel2Opcode6(byte *renderBitmap, Common::SeekableReadStr
}
_shipDirectionIndex = vDir * 7 + hDir;
- if (_shipDirectionIndex < 0) _shipDirectionIndex = 0;
- if (_shipDirectionIndex > 34) _shipDirectionIndex = 34;
+ if (_shipDirectionIndex < 0)
+ _shipDirectionIndex = 0;
+ if (_shipDirectionIndex > 34)
+ _shipDirectionIndex = 34;
_shipFiring = (_flyControlMode == 2) && (_vm->VAR(_vm->VAR_LEFTBTN_HOLD) != 0);
@@ -1165,10 +1211,14 @@ void InsaneRebel2::iactRebel2Opcode6(byte *renderBitmap, Common::SeekableReadStr
if (destX < 0) { srcOffsetX = -destX; drawWidth -= srcOffsetX; destX = 0; }
if (destY < 0) { srcOffsetY = -destY; drawHeight -= srcOffsetY; destY = 0; }
- if (destX + drawWidth > pitch) drawWidth = pitch - destX;
- if (destY + drawHeight > bufHeight) drawHeight = bufHeight - destY;
- if (drawWidth > corridorOverlay.width - srcOffsetX) drawWidth = corridorOverlay.width - srcOffsetX;
- if (drawHeight > corridorOverlay.height - srcOffsetY) drawHeight = corridorOverlay.height - srcOffsetY;
+ if (destX + drawWidth > pitch)
+ drawWidth = pitch - destX;
+ if (destY + drawHeight > bufHeight)
+ drawHeight = bufHeight - destY;
+ if (drawWidth > corridorOverlay.width - srcOffsetX)
+ drawWidth = corridorOverlay.width - srcOffsetX;
+ if (drawHeight > corridorOverlay.height - srcOffsetY)
+ drawHeight = corridorOverlay.height - srcOffsetY;
if (drawWidth > 0 && drawHeight > 0) {
for (int y = 0; y < drawHeight; y++) {
@@ -1961,7 +2011,8 @@ void InsaneRebel2::iactRebel2Opcode9(byte *renderBitmap, Common::SeekableReadStr
int textLen = 0;
while (textLen < (int)sizeof(textBuffer) - 1) {
byte ch = b.readByte();
- if (ch == 0 || b.eos()) break;
+ if (ch == 0 || b.eos())
+ break;
textBuffer[textLen++] = ch;
}
textBuffer[textLen] = '\0';
@@ -1985,10 +2036,14 @@ void InsaneRebel2::iactRebel2Opcode9(byte *renderBitmap, Common::SeekableReadStr
// Apply coordinate clamping (from FUN_004033cf disassembly)
// Low-res: X clamped to [16, 304], Y clamped to [16, 196]
- if (posX < 16) posX = 16;
- if (posX > 304) posX = 304;
- if (posY < 16) posY = 16;
- if (posY > 196) posY = 196;
+ if (posX < 16)
+ posX = 16;
+ if (posX > 304)
+ posX = 304;
+ if (posY < 16)
+ posY = 16;
+ if (posY > 196)
+ posY = 196;
// Use the message font loaded during initialization (DIHIFONT.NUT)
if (!_rebelMsgFont) {
diff --git a/engines/scumm/insane/insane_rebel_levels.cpp b/engines/scumm/insane/insane_rebel_levels.cpp
index 07e4b0087b5..432ad35bb01 100644
--- a/engines/scumm/insane/insane_rebel_levels.cpp
+++ b/engines/scumm/insane/insane_rebel_levels.cpp
@@ -70,7 +70,8 @@ void InsaneRebel2::playIntroSequence() {
splayer->setCurVideoFlags(0x28);
splayer->play("OPEN/O_OPEN_A.SAN", 12);
- if (_vm->shouldQuit()) return;
+ if (_vm->shouldQuit())
+ return;
// Play additional intro (OPEN/O_OPEN_B.SAN)
// Original: conditional on DAT_0047ab45 || DAT_0047ab47
@@ -165,7 +166,8 @@ bool InsaneRebel2::playLevelGameplay(int levelId) {
splayer->setCurVideoFlags(0x28);
splayer->play(filename.c_str(), 12);
- if (_vm->shouldQuit() || _playerShield == 0) return false;
+ if (_vm->shouldQuit() || _playerShield == 0)
+ return false;
// Part 1 (multiple variations - play A for now)
splayer->setCurVideoFlags(0x28);
@@ -173,7 +175,8 @@ bool InsaneRebel2::playLevelGameplay(int levelId) {
debug("Rebel2: Playing %s", filename.c_str());
splayer->play(filename.c_str(), 12);
- if (_vm->shouldQuit() || _playerShield == 0) return false;
+ if (_vm->shouldQuit() || _playerShield == 0)
+ return false;
// Post segment 1
_rebelHandler = 0;
@@ -183,7 +186,8 @@ bool InsaneRebel2::playLevelGameplay(int levelId) {
splayer->setCurVideoFlags(0x28);
splayer->play(filename.c_str(), 12);
- if (_vm->shouldQuit() || _playerShield == 0) return false;
+ if (_vm->shouldQuit() || _playerShield == 0)
+ return false;
// Part 2
_rebelHandler = 8;
@@ -192,7 +196,8 @@ bool InsaneRebel2::playLevelGameplay(int levelId) {
debug("Rebel2: Playing %s", filename.c_str());
splayer->play(filename.c_str(), 12);
- if (_vm->shouldQuit() || _playerShield == 0) return false;
+ if (_vm->shouldQuit() || _playerShield == 0)
+ return false;
// Post segment 2
_rebelHandler = 0;
@@ -202,7 +207,8 @@ bool InsaneRebel2::playLevelGameplay(int levelId) {
splayer->setCurVideoFlags(0x28);
splayer->play(filename.c_str(), 12);
- if (_vm->shouldQuit() || _playerShield == 0) return false;
+ if (_vm->shouldQuit() || _playerShield == 0)
+ return false;
// Part 3
_rebelHandler = 8;
@@ -220,7 +226,8 @@ bool InsaneRebel2::playLevelGameplay(int levelId) {
debug("Rebel2: Playing %s", filename.c_str());
splayer->play(filename.c_str(), 12);
- if (_vm->shouldQuit() || _playerShield == 0) return false;
+ if (_vm->shouldQuit() || _playerShield == 0)
+ return false;
// Post segment
_rebelHandler = 0;
@@ -230,7 +237,8 @@ bool InsaneRebel2::playLevelGameplay(int levelId) {
splayer->setCurVideoFlags(0x28);
splayer->play(filename.c_str(), 12);
- if (_vm->shouldQuit() || _playerShield == 0) return false;
+ if (_vm->shouldQuit() || _playerShield == 0)
+ return false;
// Phase 2 â handler will be re-set by IACT opcode 6
splayer->setCurVideoFlags(0x28);
@@ -247,7 +255,8 @@ bool InsaneRebel2::playLevelGameplay(int levelId) {
splayer->setCurVideoFlags(0x28);
splayer->play(filename.c_str(), 12);
- if (_vm->shouldQuit()) return false;
+ if (_vm->shouldQuit())
+ return false;
splayer->setCurVideoFlags(0x28);
filename = Common::String::format("%s/%sPLAY.SAN", dir.c_str(), prefix.c_str());
@@ -268,7 +277,8 @@ bool InsaneRebel2::playLevelGameplay(int levelId) {
debug("Rebel2: Playing %s", filename.c_str());
splayer->play(filename.c_str(), 12);
- if (_vm->shouldQuit() || _playerShield == 0) return false;
+ if (_vm->shouldQuit() || _playerShield == 0)
+ return false;
// Post segment
_rebelHandler = 0;
@@ -278,7 +288,8 @@ bool InsaneRebel2::playLevelGameplay(int levelId) {
splayer->setCurVideoFlags(0x28);
splayer->play(filename.c_str(), 12);
- if (_vm->shouldQuit() || _playerShield == 0) return false;
+ if (_vm->shouldQuit() || _playerShield == 0)
+ return false;
// Phase 2 â handler will be re-set by IACT opcode 6
splayer->setCurVideoFlags(0x28);
@@ -496,18 +507,27 @@ Common::String InsaneRebel2::selectDeathVideoVariant(int levelId, int phase, int
// Level 3: Based on death frame and phase
if (phase == 1) {
// Phase 1 death video selection (from FUN_0041885F lines 80-96)
- if (frame < 0x10c) return "B"; // < 268
- if (frame < 0x1a9) return "A"; // < 425
- if (frame < 0x247) return "C"; // < 583
- if (frame < 700) return "A";
- if (frame < 900) return "B";
+ if (frame < 0x10c)
+ return "B"; // < 268
+ if (frame < 0x1a9)
+ return "A"; // < 425
+ if (frame < 0x247)
+ return "C"; // < 583
+ if (frame < 700)
+ return "A";
+ if (frame < 900)
+ return "B";
return "A";
} else {
// Phase 2 death video selection (from FUN_0041885F lines 53-67)
- if (frame < 0x2f1) return "A"; // < 753
- if (frame < 0x347) return "B"; // < 839
- if (frame < 0x3b1) return "C"; // < 945
- if (frame < 0x405) return "A"; // < 1029
+ if (frame < 0x2f1)
+ return "A"; // < 753
+ if (frame < 0x347)
+ return "B"; // < 839
+ if (frame < 0x3b1)
+ return "C"; // < 945
+ if (frame < 0x405)
+ return "A"; // < 1029
return "C";
}
@@ -523,29 +543,49 @@ Common::String InsaneRebel2::selectDeathVideoVariant(int levelId, int phase, int
// Level 6 (FUN_004190D6): Phase-based with detailed frame selection
if (phase == 1) {
// DAT_0047a7f8 == 5 (phase 1)
- if (frame < 0x4e) return "D";
- if (frame < 0xe0) return "A";
- if (frame < 0x122) return "D";
- if (frame < 0x1b4) return "B";
- if (frame < 499) return "D";
- if (frame < 0x286) return "C";
+ if (frame < 0x4e)
+ return "D";
+ if (frame < 0xe0)
+ return "A";
+ if (frame < 0x122)
+ return "D";
+ if (frame < 0x1b4)
+ return "B";
+ if (frame < 499)
+ return "D";
+ if (frame < 0x286)
+ return "C";
return "D";
} else {
// DAT_0047a7f8 == 6 (phase 2)
- if (frame < 0xcc) return "E";
- if (frame < 0xfe) return "G";
- if (frame < 0x122) return "E";
- if (frame < 0x149) return "G";
- if (frame < 0x166) return "F";
- if (frame < 0x174) return "E";
- if (frame < 0x19f) return "F";
- if (frame < 0x1b2) return "G";
- if (frame < 0x1c8) return "F";
- if (frame < 0x207) return "E";
- if (frame < 0x217) return "F";
- if (frame < 0x23b) return "G";
- if (frame < 0x25b) return "F";
- if (frame < 0x285) return "E";
+ if (frame < 0xcc)
+ return "E";
+ if (frame < 0xfe)
+ return "G";
+ if (frame < 0x122)
+ return "E";
+ if (frame < 0x149)
+ return "G";
+ if (frame < 0x166)
+ return "F";
+ if (frame < 0x174)
+ return "E";
+ if (frame < 0x19f)
+ return "F";
+ if (frame < 0x1b2)
+ return "G";
+ if (frame < 0x1c8)
+ return "F";
+ if (frame < 0x207)
+ return "E";
+ if (frame < 0x217)
+ return "F";
+ if (frame < 0x23b)
+ return "G";
+ if (frame < 0x25b)
+ return "F";
+ if (frame < 0x285)
+ return "E";
return "G";
}
@@ -571,8 +611,10 @@ Common::String InsaneRebel2::selectDeathVideoVariant(int levelId, int phase, int
case 11:
// Level 11 (FUN_0041A00C): Phase-based death videos
// Phase 1 â DIE_A, Phase 2 â DIE_B, Phase 3 â DIE_C
- if (phase <= 1) return "A";
- if (phase == 2) return "B";
+ if (phase <= 1)
+ return "A";
+ if (phase == 2)
+ return "B";
return "C";
case 12:
@@ -581,10 +623,14 @@ Common::String InsaneRebel2::selectDeathVideoVariant(int levelId, int phase, int
case 13:
// Level 13 (FUN_0041B3E1): Frame-based
- if (frame < 0x1c2) return "A";
- if (frame < 0x302) return "B";
- if (frame < 0x4ec) return "C";
- if (frame < 0x5b4) return "B";
+ if (frame < 0x1c2)
+ return "A";
+ if (frame < 0x302)
+ return "B";
+ if (frame < 0x4ec)
+ return "C";
+ if (frame < 0x5b4)
+ return "B";
return "D";
case 14:
@@ -593,12 +639,18 @@ Common::String InsaneRebel2::selectDeathVideoVariant(int levelId, int phase, int
case 15:
// Level 15 (FUN_0041B8D7): Frame-based with many thresholds
- if (frame < 0x21e) return "A";
- if (frame < 0x2f9) return "B";
- if (frame < 0x3e5) return "C";
- if (frame < 0x4a0) return "B";
- if (frame < 0x588) return "C";
- if (frame < 0x65e) return "B";
+ if (frame < 0x21e)
+ return "A";
+ if (frame < 0x2f9)
+ return "B";
+ if (frame < 0x3e5)
+ return "C";
+ if (frame < 0x4a0)
+ return "B";
+ if (frame < 0x588)
+ return "C";
+ if (frame < 0x65e)
+ return "B";
return "D";
default:
diff --git a/engines/scumm/insane/insane_rebel_menu.cpp b/engines/scumm/insane/insane_rebel_menu.cpp
index 06ef3445784..4ccbc90adca 100644
--- a/engines/scumm/insane/insane_rebel_menu.cpp
+++ b/engines/scumm/insane/insane_rebel_menu.cpp
@@ -186,8 +186,10 @@ int InsaneRebel2::processMenuInput() {
int newSelection = (mouseY + 100 - (numItemsTotal * -5 + 0x67)) / 10;
// Clamp to valid range
- if (newSelection < 0) newSelection = 0;
- if (newSelection >= _menuItemCount) newSelection = _menuItemCount - 1;
+ if (newSelection < 0)
+ newSelection = 0;
+ if (newSelection >= _menuItemCount)
+ newSelection = _menuItemCount - 1;
// Only update if within menu area (not too far above/below)
int topY = baseY - 5;
@@ -282,7 +284,8 @@ void InsaneRebel2::drawMenuItems(byte *renderBitmap, int pitch, int width, int h
// =====================================================================
// ^^ = literal ^, ^fNN = font switch, ^cNNN = color code, ^l = newline
auto parseFormatCode = [&](const char *&str, int &outColor) -> int {
- if (*str != '^') return -1;
+ if (*str != '^')
+ return -1;
const char *p = str + 1;
if (*p == '^') {
@@ -329,10 +332,12 @@ void InsaneRebel2::drawMenuItems(byte *renderBitmap, int pitch, int width, int h
curFont = fonts[fontChange] ? fonts[fontChange] : defaultFont;
continue;
}
- if (fontChange == -2) continue;
+ if (fontChange == -2)
+ continue;
byte c = (byte)*str++;
- if (c >= 'a' && c <= 'z') c = c - 'a' + 'A';
+ if (c >= 'a' && c <= 'z')
+ c = c - 'a' + 'A';
if (curFont && c < curFont->getNumChars()) {
w += curFont->getCharWidth(c);
}
@@ -352,14 +357,18 @@ void InsaneRebel2::drawMenuItems(byte *renderBitmap, int pitch, int width, int h
curFont = fonts[fontChange] ? fonts[fontChange] : defaultFont;
continue;
}
- if (fontChange == -2) continue;
+ if (fontChange == -2)
+ continue;
byte c = (byte)*str++;
- if (c >= 'a' && c <= 'z') c = c - 'a' + 'A';
+ if (c >= 'a' && c <= 'z')
+ c = c - 'a' + 'A';
- if (!curFont) continue;
+ if (!curFont)
+ continue;
int numChars = curFont->getNumChars();
- if (c >= numChars) continue;
+ if (c >= numChars)
+ continue;
int charW = curFont->getCharWidth(c);
@@ -416,10 +425,14 @@ void InsaneRebel2::drawMenuItems(byte *renderBitmap, int pitch, int width, int h
int screenW = _vm->_screenWidth;
int screenH = _vm->_screenHeight;
- if (leftX < 0) leftX = 0;
- if (rightX >= screenW) rightX = screenW - 1;
- if (topY < 0) topY = 0;
- if (bottomY >= screenH) bottomY = screenH - 1;
+ if (leftX < 0)
+ leftX = 0;
+ if (rightX >= screenW)
+ rightX = screenW - 1;
+ if (topY < 0)
+ topY = 0;
+ if (bottomY >= screenH)
+ bottomY = screenH - 1;
// FUN_004292d0 - Draw rectangle border (4 lines)
for (int x = leftX; x <= rightX && x < screenW; x++) {
@@ -609,7 +622,8 @@ void InsaneRebel2::showPauseOverlay() {
const char *p = pauseText;
while (*p) {
byte c = (byte)*p++;
- if (c >= 'a' && c <= 'z') c = c - 'a' + 'A';
+ if (c >= 'a' && c <= 'z')
+ c = c - 'a' + 'A';
if (c < numFontChars) {
textWidth += font->getCharWidth(c);
}
@@ -622,7 +636,8 @@ void InsaneRebel2::showPauseOverlay() {
p = pauseText;
while (*p) {
byte c = (byte)*p++;
- if (c >= 'a' && c <= 'z') c = c - 'a' + 'A';
+ if (c >= 'a' && c <= 'z')
+ c = c - 'a' + 'A';
if (c < numFontChars && textX >= 0 && textY >= 0) {
font->drawCharV7(frameBuffer, clipRect, textX, textY, width, -1,
kStyleAlignLeft, c, true, true);
@@ -654,7 +669,9 @@ int InsaneRebel2::runMainMenu() {
// Enable menu input capture via EventObserver
_menuInputActive = true;
- while (!_menuEventQueue.empty()) _menuEventQueue.pop(); // Clear any stale events
+ // Clear any stale events
+ while (!_menuEventQueue.empty())
+ _menuEventQueue.pop();
// Get the SmushPlayer from ScummEngine_v7
// Note: _player isn't set until SmushPlayer::initAudio() is called during playback
@@ -789,7 +806,8 @@ int InsaneRebel2::runChapterSelect() {
// Enable menu input capture
_menuInputActive = true;
- while (!_menuEventQueue.empty()) _menuEventQueue.pop();
+ while (!_menuEventQueue.empty())
+ _menuEventQueue.pop();
// Initialize chapter selection state
// Original (lines 51-54): local_10 = 0xf; while (local_10 > 0 && locked) local_10--;
@@ -1073,21 +1091,24 @@ void InsaneRebel2::drawPreviewThumbnail(byte *renderBitmap, int pitch, int width
// This creates a styled placeholder since O_MENU_X.SAN doesn't have previews
for (int py = 0; py < thumbH; py++) {
int dy = destY + py;
- if (dy < 0 || dy >= height) continue;
+ if (dy < 0 || dy >= height)
+ continue;
// Create vertical gradient: darker at top (0x10), lighter at bottom (0x18)
byte bgColor = 0x10 + (py * 8 / thumbH);
for (int px = 0; px < thumbW; px++) {
int dx = destX + px;
- if (dx < 0 || dx >= width) continue;
+ if (dx < 0 || dx >= width)
+ continue;
renderBitmap[dy * pitch + dx] = bgColor;
}
}
// Draw chapter number overlay in the center of the preview
NutRenderer *font = _smush_smalfontNut;
- if (!font) return;
+ if (!font)
+ return;
char chapterStr[16];
if (chapter < 15) {
@@ -1170,13 +1191,16 @@ void InsaneRebel2::drawPreviewThumbnail(byte *renderBitmap, int pitch, int width
// For unlocked chapters: score display using TRS 80 at (25, 190)
// For locked chapters: password prompt at (30, 190)
void InsaneRebel2::drawChapterInfoLine(byte *renderBitmap, int pitch, int width, int height) {
- if (_chapterSelection < 0 || _chapterSelection >= 16) return;
+ if (_chapterSelection < 0 || _chapterSelection >= 16)
+ return;
SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
- if (!splayer) return;
+ if (!splayer)
+ return;
NutRenderer *font = _smush_smalfontNut;
- if (!font) return;
+ if (!font)
+ return;
Common::Rect clipRect(0, 0, _vm->_screenWidth, _vm->_screenHeight);
int actualPitch = _vm->_screenWidth;
@@ -1184,13 +1208,15 @@ void InsaneRebel2::drawChapterInfoLine(byte *renderBitmap, int pitch, int width,
if (_chapterUnlocked[_chapterSelection]) {
// Unlocked: show score info using TRS 80 at X=25 (0x19), Y=190 (0xbe)
const char *scoreStr = splayer->getString(80);
- if (!scoreStr || !scoreStr[0]) return;
+ if (!scoreStr || !scoreStr[0])
+ return;
int curX = 25;
int numChars = font->getNumChars();
for (const char *c = scoreStr; *c; c++) {
byte ch = (byte)*c;
- if (ch >= 'a' && ch <= 'z') ch = ch - 'a' + 'A';
+ if (ch >= 'a' && ch <= 'z')
+ ch = ch - 'a' + 'A';
if (ch < numChars) {
int charW = font->getCharWidth(ch);
if (curX >= 0 && curX + charW <= width && 190 < height) {
@@ -1220,26 +1246,30 @@ void InsaneRebel2::drawChapterSelectOverlay(byte *renderBitmap, int pitch, int w
// FUN_0041f5ae(0, &DAT_004577a8, 0x11, 1): param_3=17, param_4=1 (left-aligned)
SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
- if (!splayer) return;
+ if (!splayer)
+ return;
// Build items array matching original DAT_004577a8 layout
const char *items[18];
// items[0] = title = TRS 40 ("^f02Chapters")
items[0] = splayer->getString(40);
- if (!items[0] || !items[0][0]) items[0] = "^f02Chapters";
+ if (!items[0] || !items[0][0])
+ items[0] = "^f02Chapters";
// items[1..16] = chapters, using unlocked (TRS 41-56) or locked (TRS 61-76) strings
for (int i = 1; i <= 16; i++) {
bool unlocked = (i - 1 < 16) && _chapterUnlocked[i - 1];
int trsIdx = unlocked ? (40 + i) : (60 + i);
items[i] = splayer->getString(trsIdx);
- if (!items[i] || !items[i][0]) items[i] = "";
+ if (!items[i] || !items[i][0])
+ items[i] = "";
}
// items[17] = "RETURN TO PILOTS" = TRS 57 ("^f01^c240RETURN TO PILOTS")
items[17] = splayer->getString(57);
- if (!items[17] || !items[17][0]) items[17] = "^f01^c240RETURN TO PILOTS";
+ if (!items[17] || !items[17][0])
+ items[17] = "^f01^c240RETURN TO PILOTS";
// Render menu using shared renderer with left-aligned mode
drawMenuItems(renderBitmap, pitch, width, height, items, 17, _chapterSelection, true);
@@ -1266,7 +1296,8 @@ int InsaneRebel2::runLevelSelect() {
debug("Rebel2: Entering pilot selection (FUN_00414A41), %d pilots loaded", _numPilots);
_menuInputActive = true;
- while (!_menuEventQueue.empty()) _menuEventQueue.pop();
+ while (!_menuEventQueue.empty())
+ _menuEventQueue.pop();
_levelSelection = 0;
_levelItemCount = _numPilots + 4; // N pilots + NEW/COPY/DELETE/MAIN MENU
diff --git a/engines/scumm/insane/insane_rebel_render.cpp b/engines/scumm/insane/insane_rebel_render.cpp
index 93a8a79cbee..aeacb6abaee 100644
--- a/engines/scumm/insane/insane_rebel_render.cpp
+++ b/engines/scumm/insane/insane_rebel_render.cpp
@@ -57,7 +57,8 @@ void InsaneRebel2::decodeCodec21(byte *dst, const byte *src, int width, int heig
int skip = READ_LE_UINT16(src);
src += 2;
x += skip;
- if (src >= lineEnd) break;
+ if (src >= lineEnd)
+ break;
int count = READ_LE_UINT16(src) + 1;
src += 2;
@@ -85,7 +86,8 @@ void InsaneRebel2::decodeCodec23(byte *dst, const byte *src, int width, int heig
int skip = READ_LE_UINT16(src);
src += 2;
x += skip;
- if (src >= lineEnd || x >= width) break;
+ if (src >= lineEnd || x >= width)
+ break;
int runSize = READ_LE_UINT16(src);
src += 2;
@@ -95,7 +97,8 @@ void InsaneRebel2::decodeCodec23(byte *dst, const byte *src, int width, int heig
while (src < runEnd && x < width) {
byte code = *src++;
int num = (code >> 1) + 1;
- if (num > width - x) num = width - x;
+ if (num > width - x)
+ num = width - x;
if (code & 1) {
// RLE run
@@ -186,7 +189,8 @@ void InsaneRebel2::decodeCodec45(byte *dst, const byte *src, int width, int heig
for (int row = 0; row < height && srcPtr < dataEnd; row++) {
int lineSize = READ_LE_UINT16(srcPtr);
srcPtr += 2;
- if (lineSize <= 0 || lineSize > (int)(dataEnd - srcPtr)) break;
+ if (lineSize <= 0 || lineSize > (int)(dataEnd - srcPtr))
+ break;
const byte *lineEnd = srcPtr + lineSize;
byte *rowDst = dst + row * width;
@@ -197,7 +201,8 @@ void InsaneRebel2::decodeCodec45(byte *dst, const byte *src, int width, int heig
int count = (ctrl >> 1) + 1;
if (ctrl & 1) {
byte color = (srcPtr < lineEnd) ? *srcPtr++ : 0;
- for (int i = 0; i < count && x < width; i++) rowDst[x++] = color;
+ for (int i = 0; i < count && x < width; i++)
+ rowDst[x++] = color;
} else {
for (int i = 0; i < count && x < width && srcPtr < lineEnd; i++)
rowDst[x++] = *srcPtr++;
@@ -235,7 +240,8 @@ void InsaneRebel2::decodeCodec45(byte *dst, const byte *src, int width, int heig
// Count non-zero pixels for debug
int nonZero = 0;
for (int i = 0; i < width * height; i++) {
- if (dst[i] != 0) nonZero++;
+ if (dst[i] != 0)
+ nonZero++;
}
debug("Rebel2: Decoded codec 45: %dx%d, %d non-zero (%d%%)",
width, height, nonZero, (nonZero * 100) / (width * height));
@@ -413,7 +419,8 @@ void InsaneRebel2::loadEmbeddedSan(int userId, byte *animData, int32 size, byte
if (frame.valid) {
int nonZeroPixels = 0;
for (int i = 0; i < width * height; i++) {
- if (frame.pixels[i] != 0) nonZeroPixels++;
+ if (frame.pixels[i] != 0)
+ nonZeroPixels++;
}
debug("Rebel2: Frame userId=%d has %d non-zero pixels (%d%%)",
userId, nonZeroPixels, (nonZeroPixels * 100) / (width * height));
@@ -432,13 +439,15 @@ void InsaneRebel2::loadEmbeddedSan(int userId, byte *animData, int32 size, byte
} else {
// Skip other sub-chunks (AHDR inside FRME?) or padding
stream.seek(nextSubPos);
- if (subSize & 1) stream.skip(1);
+ if (subSize & 1)
+ stream.skip(1);
}
}
} else {
// Skip non-FRME chunks (AHDR, etc at top level)
stream.seek(nextChunkPos);
- if (chunkSize & 1) stream.skip(1);
+ if (chunkSize & 1)
+ stream.skip(1);
}
}
@@ -580,10 +589,14 @@ void InsaneRebel2::spawnHandler25Shot(int x, int y) {
int zoneHeight = (areaHeight > 0) ? areaHeight / 2 : 90;
int xZone = (zoneWidth > 0) ? ((zoneWidth / 2) + (x - areaLeft)) / zoneWidth : 2;
int yZone = (zoneHeight > 0) ? ((zoneHeight / 2) + (y - areaTop)) / zoneHeight : 0;
- if (xZone < 0) xZone = 0;
- if (xZone > 4) xZone = 4;
- if (yZone < 0) yZone = 0;
- if (yZone > 1) yZone = 1;
+ if (xZone < 0)
+ xZone = 0;
+ if (xZone > 4)
+ xZone = 4;
+ if (yZone < 0)
+ yZone = 0;
+ if (yZone > 1)
+ yZone = 1;
if (_rebelFlightDir == (yZone & 1)) {
xZone = 4 - xZone;
}
@@ -592,8 +605,10 @@ void InsaneRebel2::spawnHandler25Shot(int x, int y) {
spriteIdx = (_rebelFlightDir == 0) ? (5 - _rebelDamageLevel) : (25 - _rebelDamageLevel);
}
int numSprites = _grd002Sprite->getNumChars();
- if (spriteIdx < 0) spriteIdx = 0;
- if (spriteIdx >= numSprites) spriteIdx = numSprites - 1;
+ if (spriteIdx < 0)
+ spriteIdx = 0;
+ if (spriteIdx >= numSprites)
+ spriteIdx = numSprites - 1;
// Get sprite rendering position (same as in renderHandler25Ship)
int16 spriteXOffset = _grd002Sprite->getCharXOffset(spriteIdx);
@@ -659,15 +674,19 @@ void InsaneRebel2::spawnSpaceShot(int x, int y) {
}
void InsaneRebel2::drawTexturedLine(byte *dst, int pitch, int width, int height, int x0, int y0, int x1, int y1, NutRenderer *nut, int spriteIdx, int v, bool mask231) {
- if (!nut || spriteIdx >= nut->getNumChars()) return;
+ if (!nut || spriteIdx >= nut->getNumChars())
+ return;
const byte *srcData = nut->getCharData(spriteIdx);
int texW = nut->getCharWidth(spriteIdx);
int texH = nut->getCharHeight(spriteIdx);
- if (!srcData || texW <= 0 || texH <= 0) return;
- if (v < 0) v = 0;
- if (v >= texH) v = texH - 1;
+ if (!srcData || texW <= 0 || texH <= 0)
+ return;
+ if (v < 0)
+ v = 0;
+ if (v >= texH)
+ v = texH - 1;
int dx = abs(x1 - x0), sx = x0 < x1 ? 1 : -1;
int dy = -abs(y1 - y0), sy = y0 < y1 ? 1 : -1;
@@ -675,7 +694,8 @@ void InsaneRebel2::drawTexturedLine(byte *dst, int pitch, int width, int height,
// Total length approximation for UV mapping
int totalDist = (abs(dx) > abs(dy)) ? abs(dx) : abs(dy);
- if (totalDist == 0) totalDist = 1;
+ if (totalDist == 0)
+ totalDist = 1;
int currentDist = 0;
@@ -683,7 +703,8 @@ void InsaneRebel2::drawTexturedLine(byte *dst, int pitch, int width, int height,
if (x0 >= 0 && x0 < width && y0 >= 0 && y0 < height) {
// Map currentDist/totalDist to 0..texW (Run along texture width)
int u = (currentDist * texW) / totalDist;
- if (u >= texW) u = texW - 1;
+ if (u >= texW)
+ u = texW - 1;
byte color = srcData[v * texW + u];
@@ -693,7 +714,8 @@ void InsaneRebel2::drawTexturedLine(byte *dst, int pitch, int width, int height,
}
}
- if (x0 == x1 && y0 == y1) break;
+ if (x0 == x1 && y0 == y1)
+ break;
e2 = 2 * err;
if (e2 >= dy) { err += dy; x0 += sx; }
if (e2 <= dx) { err += dx; y0 += sy; }
@@ -717,14 +739,17 @@ void drawTexturedSegment(byte *dst, int pitch, int width, int height, int param_
// Clip against screen bounds (translation of original clipping logic)
if (px0 == px1) {
- if (px0 < sVar4 || px0 > sVar7) return;
+ if (px0 < sVar4 || px0 > sVar7)
+ return;
} else {
if (px0 < sVar4) {
- if (px1 < sVar4) return;
+ if (px1 < sVar4)
+ return;
py0 = py1 + ((py0 - py1) * (sVar4 - px1)) / (px0 - px1);
px0 = sVar4;
} else if (px0 > sVar7) {
- if (px1 > sVar7) return;
+ if (px1 > sVar7)
+ return;
py0 = py1 + ((py0 - py1) * (sVar7 - px1)) / (px0 - px1);
px0 = sVar7;
}
@@ -738,14 +763,17 @@ void drawTexturedSegment(byte *dst, int pitch, int width, int height, int param_
}
if (py0 == py1) {
- if (py0 < sVar1 || py0 > sVar10) return;
+ if (py0 < sVar1 || py0 > sVar10)
+ return;
} else {
if (py0 < sVar1) {
- if (py1 < sVar1) return;
+ if (py1 < sVar1)
+ return;
px0 = px1 + ((px0 - px1) * (sVar1 - py1)) / (py0 - py1);
py0 = sVar1;
} else if (py0 > sVar10) {
- if (py1 > sVar10) return;
+ if (py1 > sVar10)
+ return;
px0 = px1 + ((px0 - px1) * (sVar10 - py1)) / (py0 - py1);
py0 = sVar10;
}
@@ -769,7 +797,8 @@ void drawTexturedSegment(byte *dst, int pitch, int width, int height, int param_
if (absdx == 0) {
if (absdy == 0) {
- if (*texPtr != 0) baseDst[py0 * pitch + px0] = *texPtr;
+ if (*texPtr != 0)
+ baseDst[py0 * pitch + px0] = *texPtr;
return;
}
// vertical-ish
@@ -778,7 +807,8 @@ void drawTexturedSegment(byte *dst, int pitch, int width, int height, int param_
int signY = dy > 0 ? 1 : -1;
int iVar9 = step; // adv counter
for (int i = 0; i < step; i++) {
- if (*texPtr != 0) baseDst[curY * pitch + px0] = *texPtr;
+ if (*texPtr != 0)
+ baseDst[curY * pitch + px0] = *texPtr;
curY += signY;
iVar9 -= param_7;
while (iVar9 < 0) { texPtr++; iVar9 += step; }
@@ -793,7 +823,8 @@ void drawTexturedSegment(byte *dst, int pitch, int width, int height, int param_
int signX = dx > 0 ? 1 : -1;
int iVar11 = step;
for (int i = 0; i < step; i++) {
- if (*texPtr != 0) baseDst[py0 * pitch + curX] = *texPtr;
+ if (*texPtr != 0)
+ baseDst[py0 * pitch + curX] = *texPtr;
curX += signX;
iVar11 -= param_7;
while (iVar11 < 0) { texPtr++; iVar11 += step; }
@@ -811,7 +842,8 @@ void drawTexturedSegment(byte *dst, int pitch, int width, int height, int param_
for (int i = 0; i < steps; i++) {
if (x >= 0 && x < width && y >= 0 && y < height) {
- if (*texPtr != 0) baseDst[y * pitch + x] = *texPtr;
+ if (*texPtr != 0)
+ baseDst[y * pitch + x] = *texPtr;
}
int e2 = 2 * err;
if (e2 > -absdy) { err -= absdy; x += sx; }
@@ -893,7 +925,8 @@ void InsaneRebel2::drawLaserBeam(byte *dst, int pitch, int width, int height,
byte *texPixels = _laserTexture.pixels;
// FUN_0040BBF6 line 23: sVar7 = (thickness * animFrame * 16) / maxFrames
- if (maxFrames == 0) maxFrames = 1;
+ if (maxFrames == 0)
+ maxFrames = 1;
int16 sVar7 = (int16)(((int)thickness * (int)animFrame * 16) / (int)maxFrames);
// FUN_0040BBF6 lines 24-25: Calculate delta with scaling
@@ -978,7 +1011,8 @@ void InsaneRebel2::drawLine(byte *dst, int pitch, int width, int height, int x0,
if (x0 >= 0 && x0 < width && y0 >= 0 && y0 < height) {
dst[y0 * pitch + x0] = color;
}
- if (x0 == x1 && y0 == y1) break;
+ if (x0 == x1 && y0 == y1)
+ break;
e2 = 2 * err;
if (e2 >= dy) { err += dy; x0 += sx; }
if (e2 <= dx) { err += dx; y0 += sy; }
@@ -989,8 +1023,10 @@ void InsaneRebel2::drawCornerBrackets(byte *dst, int pitch, int width, int heigh
// Draw L-shaped brackets at corners of the rect (x,y,w,h)
// Bracket size: approx 8 pixels
int armLen = 2;
- if (armLen > w / 2) armLen = w / 2;
- if (armLen > h / 2) armLen = h / 2;
+ if (armLen > w / 2)
+ armLen = w / 2;
+ if (armLen > h / 2)
+ armLen = h / 2;
int x2 = x + w - 1;
int y2 = y + h - 1;
@@ -1105,7 +1141,8 @@ void InsaneRebel2::checkCollisionZones() {
// Mouse X 0..320 â centered X â [-52..52] (with smoothing in original)
// Mouse Y 0..200 â centered Y â [-45..45]
- if (_primaryZoneCount == 0) return;
+ if (_primaryZoneCount == 0)
+ return;
// Calculate aim position in centered coordinates.
// Original: local_10 = mouseOffset + 0xa0, then smoothed and clamped to [-0x34..0x34]
@@ -1114,22 +1151,29 @@ void InsaneRebel2::checkCollisionZones() {
int16 aimY = (int16)((100 - _vm->_mouse.y) * 45 / 100);
// Clamp to original ranges (DAT_0047a7fc < 1 path)
- if (aimX > 0x34) aimX = 0x34;
- if (aimX < -0x34) aimX = -0x34;
- if (aimY > 0x2d) aimY = 0x2d;
- if (aimY < -0x2d) aimY = -0x2d;
+ if (aimX > 0x34)
+ aimX = 0x34;
+ if (aimX < -0x34)
+ aimX = -0x34;
+ if (aimY > 0x2d)
+ aimY = 0x2d;
+ if (aimY < -0x2d)
+ aimY = -0x2d;
for (int i = 0; i < _primaryZoneCount; i++) {
CollisionZone &zone = _primaryZones[i];
- if (!zone.active) continue;
+ if (!zone.active)
+ continue;
// Filter: only process zones with filterValue < 1000 (par4 from IACT header)
// Original: *(short *)(*local_c + 6) < 1000
- if (zone.filterValue >= 1000) continue;
+ if (zone.filterValue >= 1000)
+ continue;
// Frame check: field2 - 1 == field1
// Original: sVar2 + -1 == (int)sVar1
- if (zone.field2 - 1 != zone.field1) continue;
+ if (zone.field2 - 1 != zone.field1)
+ continue;
// Center zone vertices by subtracting buffer center (0xD4=212, 0x82=130)
// Original: sVar4 = x1 - 0xD4, sVar8 = y1 - 0x82, etc.
@@ -1159,19 +1203,23 @@ void InsaneRebel2::checkCollisionZones() {
// Avoid division by zero for degenerate edges
if (cx2 != cx1) {
int interpY1 = ((aimX - cx1) * (cy2 - cy1)) / (cx2 - cx1) + cy1;
- if (aimY < interpY1) collision = true;
+ if (aimY < interpY1)
+ collision = true;
}
if (!collision && cx3 != cx4) {
int interpY2 = ((aimX - cx4) * (cy3 - cy4)) / (cx3 - cx4) + cy4;
- if (interpY2 < aimY) collision = true;
+ if (interpY2 < aimY)
+ collision = true;
}
if (!collision && cy4 != cy1) {
int interpX1 = ((aimY - cy1) * (cx4 - cx1)) / (cy4 - cy1) + cx1;
- if (aimX < interpX1) collision = true;
+ if (aimX < interpX1)
+ collision = true;
}
if (!collision && cy3 != cy2) {
int interpX2 = ((aimY - cy2) * (cx3 - cx2)) / (cy3 - cy2) + cx2;
- if (interpX2 < aimX) collision = true;
+ if (interpX2 < aimX)
+ collision = true;
}
if (collision) {
@@ -1182,7 +1230,8 @@ void InsaneRebel2::checkCollisionZones() {
if (!_rebelInvulnerable) {
_playerDamage += collisionDamage;
- if (_playerDamage > 255) _playerDamage = 255;
+ if (_playerDamage > 255)
+ _playerDamage = 255;
debug("Rebel2: COLLISION damage! zone=%d aim=(%d,%d) damage=%d total=%d",
i, aimX, aimY, collisionDamage, _playerDamage);
}
@@ -1218,7 +1267,8 @@ void InsaneRebel2::checkHandler7CollisionZones() {
for (int i = 0; i < _secondaryZoneCount; i++) {
CollisionZone &zone = _secondaryZones[i];
- if (!zone.active) continue;
+ if (!zone.active)
+ continue;
int x1 = zone.x1, y1 = zone.y1;
int x2 = zone.x2, y2 = zone.y2;
@@ -1232,22 +1282,26 @@ void InsaneRebel2::checkHandler7CollisionZones() {
// Top edge: interpolate Y along v1âv2 at shipX, +15 margin
if (x2 != x1) {
int interpY = (_flyShipScreenX - x1) * (y2 - y1) / (x2 - x1) + margin + y1;
- if (_flyShipScreenY < interpY) inside = false;
+ if (_flyShipScreenY < interpY)
+ inside = false;
}
// Bottom edge: interpolate Y along v4âv3 at shipX, -15 margin
if (inside && x3 != x4) {
int interpY = (_flyShipScreenX - x4) * (y3 - y4) / (x3 - x4) + y4 - margin;
- if (interpY < _flyShipScreenY) inside = false;
+ if (interpY < _flyShipScreenY)
+ inside = false;
}
// Left edge: interpolate X along v1âv4 at shipY, +15 margin
if (inside && y4 != y1) {
int interpX = (_flyShipScreenY - y1) * (x4 - x1) / (y4 - y1) + margin + x1;
- if (_flyShipScreenX < interpX) inside = false;
+ if (_flyShipScreenX < interpX)
+ inside = false;
}
// Right edge: interpolate X along v2âv3 at shipY, -15 margin
if (inside && y3 != y2) {
int interpX = (_flyShipScreenY - y2) * (x3 - x2) / (y3 - y2) + x2 - margin;
- if (interpX < _flyShipScreenX) inside = false;
+ if (interpX < _flyShipScreenX)
+ inside = false;
}
// Frame match: field2 - 1 == field1 (line 90)
@@ -1261,7 +1315,8 @@ void InsaneRebel2::checkHandler7CollisionZones() {
int collisionDamage = (params.dodgeDamage >= 0) ? params.dodgeDamage : 0;
if (!_rebelInvulnerable) {
_playerDamage += collisionDamage;
- if (_playerDamage > 255) _playerDamage = 255;
+ if (_playerDamage > 255)
+ _playerDamage = 255;
}
_rebelHitCounter++;
initDamageFlash();
@@ -1295,7 +1350,8 @@ void InsaneRebel2::checkHandler7CollisionZones() {
for (int i = 0; i < _primaryZoneCount; i++) {
CollisionZone &zone = _primaryZones[i];
- if (!zone.active) continue;
+ if (!zone.active)
+ continue;
int x1 = zone.x1, y1 = zone.y1;
int x2 = zone.x2, y2 = zone.y2;
@@ -1310,7 +1366,8 @@ void InsaneRebel2::checkHandler7CollisionZones() {
if (_hitCooldown < 5 && !_rebelInvulnerable) {
int damage = wallDamage;
_playerDamage += damage;
- if (_playerDamage > 255) _playerDamage = 255;
+ if (_playerDamage > 255)
+ _playerDamage = 255;
_rebelHitCounter++;
_hitCooldown = 10;
playSfx(1, 127, 0); // CRASH.SAD, top wall â center pan
@@ -1332,7 +1389,8 @@ void InsaneRebel2::checkHandler7CollisionZones() {
if (_hitCooldown < 5 && !_rebelInvulnerable) {
int damage = wallDamage;
_playerDamage += damage;
- if (_playerDamage > 255) _playerDamage = 255;
+ if (_playerDamage > 255)
+ _playerDamage = 255;
_rebelHitCounter++;
_hitCooldown = 10;
playSfx(1, 127, 0); // CRASH.SAD, bottom wall â center pan
@@ -1354,7 +1412,8 @@ void InsaneRebel2::checkHandler7CollisionZones() {
if (_hitCooldown < 5 && !_rebelInvulnerable) {
int damage = wallDamage;
_playerDamage += damage;
- if (_playerDamage > 255) _playerDamage = 255;
+ if (_playerDamage > 255)
+ _playerDamage = 255;
_rebelHitCounter++;
_hitCooldown = 10;
playSfx(1, 127, -100); // CRASH.SAD, left wall â pan left
@@ -1375,7 +1434,8 @@ void InsaneRebel2::checkHandler7CollisionZones() {
if (_hitCooldown < 5 && !_rebelInvulnerable) {
int damage = wallDamage;
_playerDamage += damage;
- if (_playerDamage > 255) _playerDamage = 255;
+ if (_playerDamage > 255)
+ _playerDamage = 255;
_rebelHitCounter++;
_hitCooldown = 10;
playSfx(1, 127, 100); // CRASH.SAD, right wall â pan right
@@ -1410,7 +1470,8 @@ void InsaneRebel2::drawCollisionZones(byte *dst, int pitch, int width, int heigh
// Draw primary zones (sub-opcode 0x0D - obstacles)
for (int i = 0; i < _primaryZoneCount; i++) {
CollisionZone &zone = _primaryZones[i];
- if (!zone.active) continue;
+ if (!zone.active)
+ continue;
// Apply view offset to convert from video coords to screen coords
int x1 = zone.x1 + _viewX;
@@ -1428,7 +1489,8 @@ void InsaneRebel2::drawCollisionZones(byte *dst, int pitch, int width, int heigh
// Draw secondary zones (sub-opcode 0x0E - boundaries)
for (int i = 0; i < _secondaryZoneCount; i++) {
CollisionZone &zone = _secondaryZones[i];
- if (!zone.active) continue;
+ if (!zone.active)
+ continue;
// Apply view offset
int x1 = zone.x1 + _viewX;
@@ -1470,7 +1532,8 @@ void InsaneRebel2::renderNutSprite(byte *dst, int pitch, int width, int height,
// Render NUT sprite with optional horizontal mirroring
// Based on FUN_004236e0 disassembly - flags=0x2001 triggers horizontal flip
void InsaneRebel2::renderNutSpriteMirrored(byte *dst, int pitch, int width, int height, int x, int y, NutRenderer *nut, int spriteIdx, bool mirror) {
- if (!nut || spriteIdx < 0 || spriteIdx >= nut->getNumChars()) return;
+ if (!nut || spriteIdx < 0 || spriteIdx >= nut->getNumChars())
+ return;
int w = nut->getCharWidth(spriteIdx);
int h = nut->getCharHeight(spriteIdx);
@@ -1502,7 +1565,8 @@ void InsaneRebel2::renderNutSpriteMirrored(byte *dst, int pitch, int width, int
drawH = height - drawY;
}
- if (drawW <= 0 || drawH <= 0) return;
+ if (drawW <= 0 || drawH <= 0)
+ return;
// Draw loop - with optional horizontal mirroring
for (int iy = 0; iy < drawH; iy++) {
@@ -1530,8 +1594,10 @@ void InsaneRebel2::procPostRendering(byte *renderBitmap, int32 codecparam, int32
// Determine correct pitch for the video buffer (usually 320 for Rebel2)
int width = _player->_width;
int height = _player->_height;
- if (width == 0) width = _vm->_screenWidth;
- if (height == 0) height = _vm->_screenHeight;
+ if (width == 0)
+ width = _vm->_screenWidth;
+ if (height == 0)
+ height = _vm->_screenHeight;
int pitch = width;
// Calculate View/Scroll Offsets
@@ -1541,8 +1607,10 @@ void InsaneRebel2::procPostRendering(byte *renderBitmap, int32 codecparam, int32
int maxScrollX = width - _vm->_screenWidth;
int maxScrollY = height - _vm->_screenHeight;
- if (maxScrollX < 0) maxScrollX = 0;
- if (maxScrollY < 0) maxScrollY = 0;
+ if (maxScrollX < 0)
+ maxScrollX = 0;
+ if (maxScrollY < 0)
+ maxScrollY = 0;
// Simple linear mapping: Center of screen corresponds to center of buffer
_viewX = (_vm->_mouse.x * maxScrollX) / _vm->_screenWidth;
@@ -1957,10 +2025,12 @@ void InsaneRebel2::renderStatusBarBackground(byte *renderBitmap, int pitch, int
for (int y = statusBarY; y < videoHeight; y++) {
int destY = y + _viewY;
- if (destY >= height) continue;
+ if (destY >= height)
+ continue;
for (int x = 0; x < videoWidth; x++) {
int destX = x + _viewX;
- if (destX >= pitch) continue;
+ if (destX >= pitch)
+ continue;
renderBitmap[destY * pitch + destX] = statusBarBgColor;
}
}
@@ -1981,10 +2051,14 @@ void InsaneRebel2::renderTurretHudOverlays(byte *renderBitmap, int pitch, int wi
// Calculate mouse offset (clamped to -127..127)
int mouseOffsetX = (_vm->_mouse.x - 160);
int mouseOffsetY = (_vm->_mouse.y - 100);
- if (mouseOffsetX > 127) mouseOffsetX = 127;
- if (mouseOffsetX < -127) mouseOffsetX = -127;
- if (mouseOffsetY > 127) mouseOffsetY = 127;
- if (mouseOffsetY < -127) mouseOffsetY = -127;
+ if (mouseOffsetX > 127)
+ mouseOffsetX = 127;
+ if (mouseOffsetX < -127)
+ mouseOffsetX = -127;
+ if (mouseOffsetY > 127)
+ mouseOffsetY = 127;
+ if (mouseOffsetY < -127)
+ mouseOffsetY = -127;
// Animation frame cycling: (frameCounter / 2) % 6
int numSprites = _hudOverlayNut->getNumChars();
@@ -2075,7 +2149,8 @@ void InsaneRebel2::renderEmbeddedHudOverlays(byte *renderBitmap, int pitch, int
EmbeddedSanFrame &selectedFrame = _rebelEmbeddedHud[selectedId];
int nonZero = 0;
for (int i = 0; i < selectedFrame.width * selectedFrame.height; i++) {
- if (selectedFrame.pixels[i] != 0) nonZero++;
+ if (selectedFrame.pixels[i] != 0)
+ nonZero++;
}
if (nonZero == 0) {
@@ -2083,7 +2158,8 @@ void InsaneRebel2::renderEmbeddedHudOverlays(byte *renderBitmap, int pitch, int
EmbeddedSanFrame &altFrame = _rebelEmbeddedHud[groupMembers[i]];
int altNonZero = 0;
for (int j = 0; j < altFrame.width * altFrame.height; j++) {
- if (altFrame.pixels[j] != 0) altNonZero++;
+ if (altFrame.pixels[j] != 0)
+ altNonZero++;
}
if (altNonZero > 0) {
selectedId = groupMembers[i];
@@ -2163,7 +2239,8 @@ void InsaneRebel2::renderStatusBarSprites(byte *renderBitmap, int pitch, int wid
// --- Difficulty sprite (2-5) overlaid on top ---
// FUN_0041c012 lines 33-43: sprite index = min(difficulty, 4) + 1
int difficulty = _difficulty;
- if (difficulty > 3) difficulty = 3;
+ if (difficulty > 3)
+ difficulty = 3;
int difficultySprite = difficulty + 2;
if (numSprites > difficultySprite) {
renderNutSprite(renderBitmap, pitch, width, height,
@@ -2191,10 +2268,12 @@ void InsaneRebel2::renderStatusBarSprites(byte *renderBitmap, int pitch, int wid
if (drawW > 0 && drawH > 0) {
for (int y = 0; y < drawH; y++) {
int destY = statusBarY + dmgClipY + y + _viewY;
- if (destY < 0 || destY >= height) continue;
+ if (destY < 0 || destY >= height)
+ continue;
for (int x = 0; x < drawW; x++) {
int destX = dmgClipX + x + _viewX;
- if (destX < 0 || destX >= pitch) continue;
+ if (destX < 0 || destX >= pitch)
+ continue;
byte pixel = src[(dmgClipY + y) * sw + (dmgClipX + x)];
if (pixel != 0) {
renderBitmap[destY * pitch + destX] = pixel;
@@ -2220,10 +2299,12 @@ void InsaneRebel2::renderStatusBarSprites(byte *renderBitmap, int pitch, int wid
int aH = MIN(alertH, alertSH - dmgClipY);
for (int y = 0; y < aH; y++) {
int destY = statusBarY + dmgClipY + y + _viewY;
- if (destY < 0 || destY >= height) continue;
+ if (destY < 0 || destY >= height)
+ continue;
for (int x = 0; x < aW; x++) {
int destX = dmgClipX + x + _viewX;
- if (destX < 0 || destX >= pitch) continue;
+ if (destX < 0 || destX >= pitch)
+ continue;
byte pixel = alertSrc[(dmgClipY + y) * alertSW + (dmgClipX + x)];
if (pixel != 0) {
renderBitmap[destY * pitch + destX] = pixel;
@@ -2242,7 +2323,8 @@ void InsaneRebel2::renderStatusBarSprites(byte *renderBitmap, int pitch, int wid
// Bar width = min((lives * 5 - 5) * 2, 50) â only drawn when lives > 1
if (numSprites > 6 && _playerLives > 1) {
int livesBarWidth = (_playerLives * 5 - 5) * 2;
- if (livesBarWidth > 50) livesBarWidth = 50;
+ if (livesBarWidth > 50)
+ livesBarWidth = 50;
const byte *src = _smush_cockpitNut->getCharData(6);
int sw = _smush_cockpitNut->getCharWidth(6);
@@ -2256,10 +2338,12 @@ void InsaneRebel2::renderStatusBarSprites(byte *renderBitmap, int pitch, int wid
if (drawW > 0 && drawH > 0) {
for (int y = 0; y < drawH; y++) {
int destY = statusBarY + livClipY + y + _viewY;
- if (destY < 0 || destY >= height) continue;
+ if (destY < 0 || destY >= height)
+ continue;
for (int x = 0; x < drawW; x++) {
int destX = livClipX + x + _viewX;
- if (destX < 0 || destX >= pitch) continue;
+ if (destX < 0 || destX >= pitch)
+ continue;
byte pixel = src[(livClipY + y) * sw + (livClipX + x)];
if (pixel != 0) {
renderBitmap[destY * pitch + destX] = pixel;
@@ -2283,8 +2367,10 @@ void InsaneRebel2::renderHandler7Ship(byte *renderBitmap, int pitch, int width,
int numSprites = _flyShipSprite->getNumChars();
int spriteIndex = _shipDirectionIndex;
- if (spriteIndex < 0) spriteIndex = 0;
- if (spriteIndex >= numSprites) spriteIndex = numSprites - 1;
+ if (spriteIndex < 0)
+ spriteIndex = 0;
+ if (spriteIndex >= numSprites)
+ spriteIndex = numSprites - 1;
// Transform game coordinates to screen coordinates (FUN_0041c720 equivalent)
// The perspective transform shifts the ship position based on perspective offsets.
@@ -2347,14 +2433,17 @@ void InsaneRebel2::renderHandler8Ship(byte *renderBitmap, int pitch, int width,
// Select sprite based on direction and sprite count
if (numSprites >= 35) {
spriteIndex = _shipDirectionH * 7 + _shipDirectionV;
- if (spriteIndex >= numSprites) spriteIndex = numSprites - 1;
+ if (spriteIndex >= numSprites)
+ spriteIndex = numSprites - 1;
} else if (numSprites >= 25) {
int vDir5 = (_shipDirectionV * 5) / 7;
spriteIndex = _shipDirectionH * 5 + vDir5;
- if (spriteIndex >= numSprites) spriteIndex = numSprites - 1;
+ if (spriteIndex >= numSprites)
+ spriteIndex = numSprites - 1;
} else if (numSprites >= 5) {
spriteIndex = _shipDirectionH;
- if (spriteIndex >= numSprites) spriteIndex = numSprites - 1;
+ if (spriteIndex >= numSprites)
+ spriteIndex = numSprites - 1;
} else if (numSprites == 2) {
spriteIndex = _shipFiring ? 1 : 0;
}
@@ -2522,10 +2611,14 @@ void InsaneRebel2::renderHandler25Ship(byte *renderBitmap, int pitch, int width,
int yZone = (zoneHeight > 0) ? ((zoneHeight / 2) + (crosshairY - areaTop)) / zoneHeight : 0;
// Clamp to valid ranges
- if (xZone < 0) xZone = 0;
- if (xZone > 4) xZone = 4;
- if (yZone < 0) yZone = 0;
- if (yZone > 1) yZone = 1;
+ if (xZone < 0)
+ xZone = 0;
+ if (xZone > 4)
+ xZone = 4;
+ if (yZone < 0)
+ yZone = 0;
+ if (yZone > 1)
+ yZone = 1;
// Direction-based sprite flip logic (line 161-162 in decompiled)
// if (DAT_00457902 == (uVar7 & 1)) { local_58 = 4 - local_58; }
@@ -2550,8 +2643,10 @@ void InsaneRebel2::renderHandler25Ship(byte *renderBitmap, int pitch, int width,
}
// Clamp to valid range
- if (spriteIdx < 0) spriteIdx = 0;
- if (spriteIdx >= numSprites) spriteIdx = numSprites - 1;
+ if (spriteIdx < 0)
+ spriteIdx = 0;
+ if (spriteIdx >= numSprites)
+ spriteIdx = numSprites - 1;
int spriteW = _grd002Sprite->getCharWidth(spriteIdx);
int spriteH = _grd002Sprite->getCharHeight(spriteIdx);
@@ -2644,9 +2739,11 @@ void InsaneRebel2::renderFallbackShip(byte *renderBitmap, int pitch, int width,
// Blit from embedded HUD
for (int y = 0; y < spriteH && (drawY + y) < height; y++) {
- if (drawY + y < 0) continue;
+ if (drawY + y < 0)
+ continue;
for (int x = 0; x < spriteW && (drawX + x) < width; x++) {
- if (drawX + x < 0) continue;
+ if (drawX + x < 0)
+ continue;
int srcIdx = (srcY + y) * shipFrame.width + (srcX + x);
byte pixel = shipFrame.pixels[srcIdx];
if (pixel != 0 && pixel != 231) {
diff --git a/engines/scumm/insane/insane_rebel_runlevels.cpp b/engines/scumm/insane/insane_rebel_runlevels.cpp
index 55c793689df..3a729f4deca 100644
--- a/engines/scumm/insane/insane_rebel_runlevels.cpp
+++ b/engines/scumm/insane/insane_rebel_runlevels.cpp
@@ -39,7 +39,8 @@ int InsaneRebel2::runLevel1() {
// Play level beginning cinematic (01BEG.SAN)
playLevelBegin(1);
- if (_vm->shouldQuit()) return kLevelQuit;
+ if (_vm->shouldQuit())
+ return kLevelQuit;
// Main gameplay retry loop
while (!_vm->shouldQuit()) {
@@ -59,7 +60,8 @@ int InsaneRebel2::runLevel1() {
// Store death frame for video selection
_deathFrame = splayer->_frame;
- if (_vm->shouldQuit()) return kLevelQuit;
+ if (_vm->shouldQuit())
+ return kLevelQuit;
if (_playerShield > 0) {
// Level completed!
@@ -73,7 +75,8 @@ int InsaneRebel2::runLevel1() {
debug("Rebel2: Level 1 death at frame %d, lives=%d", _deathFrame, _playerLives - 1);
playLevelDeathVariant(1, 1, _deathFrame);
- if (_vm->shouldQuit()) return kLevelQuit;
+ if (_vm->shouldQuit())
+ return kLevelQuit;
_playerLives--;
if (_playerLives <= 0) {
@@ -83,7 +86,8 @@ int InsaneRebel2::runLevel1() {
// Play retry prompt and loop
playLevelRetry(1);
- if (_vm->shouldQuit()) return kLevelQuit;
+ if (_vm->shouldQuit())
+ return kLevelQuit;
}
return kLevelQuit;
@@ -137,7 +141,8 @@ uint16 InsaneRebel2::processWaveEnd(int16 mask, int16 *budget, int16 threshold,
// Randomly add one unkilled type to phase state
int idx = _vm->_rnd.getRandomNumber(numUnkilled - 1);
_rebelPhaseState |= unkilled[idx];
- if (budget) (*budget)++;
+ if (budget)
+ (*budget)++;
}
}
@@ -210,13 +215,15 @@ int InsaneRebel2::runLevel2() {
// Play cutscene (02CUT.SAN)
playCinematic("LEV02/02CUT.SAN");
- if (_vm->shouldQuit()) return kLevelQuit;
+ if (_vm->shouldQuit())
+ return kLevelQuit;
// Play level beginning cinematic (02BEG.SAN)
// Original: FUN_004171c5("LEV02/02BEG.SAN", 0x20, 0xab, 0xa0, 10, 2, 0x46)
// Includes text overlay from GAME.TRS â deferred until text rendering is ready.
playLevelBegin(2);
- if (_vm->shouldQuit()) return kLevelQuit;
+ if (_vm->shouldQuit())
+ return kLevelQuit;
// FUN_00401000 + FUN_00407d10 + FUN_0040c040: Reset game state (before retry loop)
clearBit(0);
@@ -261,7 +268,8 @@ int InsaneRebel2::runLevel2() {
splayer->play("LEV02/P1/02P01_A.SAN", 12);
_deathFrame = splayer->_frame;
- if (_vm->shouldQuit()) return kLevelQuit;
+ if (_vm->shouldQuit())
+ return kLevelQuit;
// processWaveEnd after A.SAN (threshold=0, no early exit for background loader)
processWaveEnd(0x36, &budget, 0, 0);
@@ -269,7 +277,8 @@ int InsaneRebel2::runLevel2() {
// Phase 1 wave loop: random B/C/D until all type 1,2 enemies killed
// Original: while (uVar3 >= 0 && (DAT_0047ab9c & 6) != 6)
while (_playerDamage < 255 && (_rebelPhaseState & 0x06) != 0x06) {
- if (_vm->shouldQuit()) return kLevelQuit;
+ if (_vm->shouldQuit())
+ return kLevelQuit;
// Random variant B, C, or D
int variant = _vm->_rnd.getRandomNumber(2); // 0-2
@@ -290,10 +299,13 @@ int InsaneRebel2::runLevel2() {
}
// Check for bonus (bit 4 = 0x10)
- if ((_rebelPhaseState & 0x10) != 0) bonusCount++;
+ if ((_rebelPhaseState & 0x10) != 0)
+ bonusCount++;
- if (_playerDamage >= 255) goto level2_death;
- if (_vm->shouldQuit()) return kLevelQuit;
+ if (_playerDamage >= 255)
+ goto level2_death;
+ if (_vm->shouldQuit())
+ return kLevelQuit;
// Post segment 1 (02PST1.SAN)
// Original: FUN_00417168("02PST1.SAN", 0x20) â flags = 0x20 | 0x08 = 0x28
@@ -303,7 +315,8 @@ int InsaneRebel2::runLevel2() {
_rebelStatusBarSprite = 0;
splayer->setCurVideoFlags(0x28);
splayer->play("LEV02/02PST1.SAN", 12);
- if (_vm->shouldQuit()) return kLevelQuit;
+ if (_vm->shouldQuit())
+ return kLevelQuit;
totalKills += _rebelKillCounter;
totalMisses += _rebelHitCounter;
@@ -328,14 +341,17 @@ int InsaneRebel2::runLevel2() {
splayer->play("LEV02/P2/02P02_A.SAN", 12);
_deathFrame = splayer->_frame;
- if (_vm->shouldQuit()) return kLevelQuit;
+ if (_vm->shouldQuit())
+ return kLevelQuit;
// Phase 2 wave loop: processWaveEnd at TOP of loop (matches assembly structure)
// Original: local_10 = FUN_00417b61(0x3e, local_14, 0, 0); then switch(local_10)
while (true) {
uint16 waveSelect = processWaveEnd(0x3e, &budget, 0, 0);
- if (waveSelect == 0xFFFF || (_rebelPhaseState & 0x0e) == 0x0e) break;
- if (_vm->shouldQuit()) return kLevelQuit;
+ if (waveSelect == 0xFFFF || (_rebelPhaseState & 0x0e) == 0x0e)
+ break;
+ if (_vm->shouldQuit())
+ return kLevelQuit;
// If no specific pattern: randomize high bits (original lines 71-74)
// When (local_10 & 0xc) == 0: add random 0x10/0x11/0x12
@@ -366,10 +382,13 @@ int InsaneRebel2::runLevel2() {
_deathFrame = splayer->_frame;
}
- if ((_rebelPhaseState & 0x10) != 0) bonusCount++;
+ if ((_rebelPhaseState & 0x10) != 0)
+ bonusCount++;
- if (_playerDamage >= 255) goto level2_death;
- if (_vm->shouldQuit()) return kLevelQuit;
+ if (_playerDamage >= 255)
+ goto level2_death;
+ if (_vm->shouldQuit())
+ return kLevelQuit;
// Post segment 2 (02PST2.SAN)
// Original: FUN_00417168("02PST2.SAN", 0x20) â flags = 0x20 | 0x08 = 0x28
@@ -378,7 +397,8 @@ int InsaneRebel2::runLevel2() {
_rebelStatusBarSprite = 0;
splayer->setCurVideoFlags(0x28);
splayer->play("LEV02/02PST2.SAN", 12);
- if (_vm->shouldQuit()) return kLevelQuit;
+ if (_vm->shouldQuit())
+ return kLevelQuit;
totalKills += _rebelKillCounter;
totalMisses += _rebelHitCounter;
@@ -404,7 +424,8 @@ int InsaneRebel2::runLevel2() {
splayer->play("LEV02/P3/02P03_A.SAN", 12);
_deathFrame = splayer->_frame;
- if (_vm->shouldQuit()) return kLevelQuit;
+ if (_vm->shouldQuit())
+ return kLevelQuit;
// Phase 3: processWaveEnd at BOTTOM (like Phase 1), waveSelect carried across iterations
// Original: local_10 = FUN_00417b61(0x3e, local_14, 0, 0); while (loop) { ...; local_10 = FUN_00417b61(0x3e, local_14, 0x14, 0); }
@@ -412,7 +433,8 @@ int InsaneRebel2::runLevel2() {
uint16 waveSelect = processWaveEnd(0x3e, &budget, 0, 0);
while (waveSelect != 0xFFFF && (_rebelPhaseState & 0x0e) != 0x0e) {
- if (_vm->shouldQuit()) return kLevelQuit;
+ if (_vm->shouldQuit())
+ return kLevelQuit;
// Phase 3 randomization (original lines 113-115):
// If previous wave state bit 0 was clear AND random(8)==0, set bit 0
@@ -455,11 +477,14 @@ int InsaneRebel2::runLevel2() {
}
}
- if ((_rebelPhaseState & 0x10) != 0) bonusCount++;
+ if ((_rebelPhaseState & 0x10) != 0)
+ bonusCount++;
totalKills += _rebelKillCounter;
- if (_playerDamage >= 255) goto level2_death;
- if (_vm->shouldQuit()) return kLevelQuit;
+ if (_playerDamage >= 255)
+ goto level2_death;
+ if (_vm->shouldQuit())
+ return kLevelQuit;
// Level completed! Calculate accuracy score.
// Original: FUN_00417327 with score thresholds and medal ranks
@@ -484,7 +509,8 @@ int InsaneRebel2::runLevel2() {
// Original: FUN_00417168("LEV02/02DIE.SAN", 0x20)
debug("Rebel2: Level 2 Phase %d death", _currentPhase);
playCinematic("LEV02/02DIE.SAN");
- if (_vm->shouldQuit()) return kLevelQuit;
+ if (_vm->shouldQuit())
+ return kLevelQuit;
// Original: if (DAT_0047ab5c != 0) DAT_0047a7ee++ (bonus life award)
// DAT_0047ab5c is set when player earns a bonus life (e.g., score threshold).
@@ -497,7 +523,8 @@ int InsaneRebel2::runLevel2() {
}
playCinematic("LEV02/02RETRY.SAN");
_playerDamage = 0;
- if (_vm->shouldQuit()) return kLevelQuit;
+ if (_vm->shouldQuit())
+ return kLevelQuit;
continue; // Restart from beginning
}
@@ -516,7 +543,8 @@ int InsaneRebel2::runLevel3() {
// Play level beginning cinematic (03BEG.SAN)
playLevelBegin(3);
- if (_vm->shouldQuit()) return kLevelQuit;
+ if (_vm->shouldQuit())
+ return kLevelQuit;
// ===== PHASE 1 retry loop =====
while (!_vm->shouldQuit()) {
@@ -533,7 +561,8 @@ int InsaneRebel2::runLevel3() {
splayer->play("LEV03/03PLAY1.SAN", 12);
_deathFrame = splayer->_frame;
- if (_vm->shouldQuit()) return kLevelQuit;
+ if (_vm->shouldQuit())
+ return kLevelQuit;
if (_playerShield > 0) {
// Phase 1 completed - save score and proceed to phase 2
@@ -544,7 +573,8 @@ int InsaneRebel2::runLevel3() {
// Died in phase 1 - frame-based death video
debug("Rebel2: Level 3 Phase 1 death at frame %d", _deathFrame);
playLevelDeathVariant(3, 1, _deathFrame);
- if (_vm->shouldQuit()) return kLevelQuit;
+ if (_vm->shouldQuit())
+ return kLevelQuit;
_playerLives--;
if (_playerLives <= 0) {
@@ -554,10 +584,12 @@ int InsaneRebel2::runLevel3() {
// Phase 1 retry (03RETRY.SAN)
playLevelRetryVariant(3, 1);
- if (_vm->shouldQuit()) return kLevelQuit;
+ if (_vm->shouldQuit())
+ return kLevelQuit;
}
- if (_vm->shouldQuit()) return kLevelQuit;
+ if (_vm->shouldQuit())
+ return kLevelQuit;
// Post segment 1 (03POST1.SAN)
// Original: FUN_00417168 adds | 8, so flags = 0x20 | 0x08 = 0x28
@@ -566,7 +598,8 @@ int InsaneRebel2::runLevel3() {
_rebelStatusBarSprite = 0;
splayer->setCurVideoFlags(0x28);
splayer->play("LEV03/03POST1.SAN", 12);
- if (_vm->shouldQuit()) return kLevelQuit;
+ if (_vm->shouldQuit())
+ return kLevelQuit;
// ===== PHASE 2 retry loop (preserves phase 1 score) =====
_currentPhase = 2;
@@ -584,7 +617,8 @@ int InsaneRebel2::runLevel3() {
splayer->play("LEV03/03PLAY2.SAN", 12);
_deathFrame = splayer->_frame;
- if (_vm->shouldQuit()) return kLevelQuit;
+ if (_vm->shouldQuit())
+ return kLevelQuit;
if (_playerShield > 0) {
// Level completed!
@@ -597,7 +631,8 @@ int InsaneRebel2::runLevel3() {
// Died in phase 2 - frame-based death video
debug("Rebel2: Level 3 Phase 2 death at frame %d", _deathFrame);
playLevelDeathVariant(3, 2, _deathFrame);
- if (_vm->shouldQuit()) return kLevelQuit;
+ if (_vm->shouldQuit())
+ return kLevelQuit;
_playerLives--;
if (_playerLives <= 0) {
@@ -608,7 +643,8 @@ int InsaneRebel2::runLevel3() {
// Phase 2 retry (03RETRYB.SAN)
playLevelRetryVariant(3, 2);
- if (_vm->shouldQuit()) return kLevelQuit;
+ if (_vm->shouldQuit())
+ return kLevelQuit;
}
return kLevelQuit;
@@ -626,11 +662,13 @@ int InsaneRebel2::runLevel4() {
// Original: FUN_00417168 adds | 8, so flags = 0x20 | 0x08 = 0x28
splayer->setCurVideoFlags(0x28);
splayer->play("LEV04/04CUT.SAN", 12);
- if (_vm->shouldQuit()) return kLevelQuit;
+ if (_vm->shouldQuit())
+ return kLevelQuit;
// Play level beginning cinematic (04BEG.SAN)
playLevelBegin(4);
- if (_vm->shouldQuit()) return kLevelQuit;
+ if (_vm->shouldQuit())
+ return kLevelQuit;
// Main gameplay retry loop
while (!_vm->shouldQuit()) {
@@ -647,7 +685,8 @@ int InsaneRebel2::runLevel4() {
splayer->play("LEV04/04PLAY.SAN", 12);
_deathFrame = splayer->_frame;
- if (_vm->shouldQuit()) return kLevelQuit;
+ if (_vm->shouldQuit())
+ return kLevelQuit;
if (_playerShield > 0) {
// Level completed!
@@ -660,7 +699,8 @@ int InsaneRebel2::runLevel4() {
// Died - play death video (04DIE.SAN, no variants)
debug("Rebel2: Level 4 death");
playLevelDeathVariant(4, 1, _deathFrame);
- if (_vm->shouldQuit()) return kLevelQuit;
+ if (_vm->shouldQuit())
+ return kLevelQuit;
_playerLives--;
if (_playerLives <= 0) {
@@ -669,7 +709,8 @@ int InsaneRebel2::runLevel4() {
}
playLevelRetry(4);
- if (_vm->shouldQuit()) return kLevelQuit;
+ if (_vm->shouldQuit())
+ return kLevelQuit;
}
return kLevelQuit;
@@ -686,7 +727,8 @@ int InsaneRebel2::runLevel5() {
// Play level beginning cinematic (05BEG.SAN)
playLevelBegin(5);
- if (_vm->shouldQuit()) return kLevelQuit;
+ if (_vm->shouldQuit())
+ return kLevelQuit;
// Main gameplay retry loop
while (!_vm->shouldQuit()) {
@@ -703,7 +745,8 @@ int InsaneRebel2::runLevel5() {
splayer->play("LEV05/05PLAY.SAN", 12);
_deathFrame = splayer->_frame;
- if (_vm->shouldQuit()) return kLevelQuit;
+ if (_vm->shouldQuit())
+ return kLevelQuit;
if (_playerShield > 0) {
// Level completed!
@@ -716,7 +759,8 @@ int InsaneRebel2::runLevel5() {
// Died - play death video with random A/B variant
debug("Rebel2: Level 5 death at frame %d", _deathFrame);
playLevelDeathVariant(5, 1, _deathFrame);
- if (_vm->shouldQuit()) return kLevelQuit;
+ if (_vm->shouldQuit())
+ return kLevelQuit;
_playerLives--;
if (_playerLives <= 0) {
@@ -725,7 +769,8 @@ int InsaneRebel2::runLevel5() {
}
playLevelRetry(5);
- if (_vm->shouldQuit()) return kLevelQuit;
+ if (_vm->shouldQuit())
+ return kLevelQuit;
}
return kLevelQuit;
@@ -750,7 +795,8 @@ int InsaneRebel2::runLevel6() {
// Play level beginning cinematic (06BEG.SAN)
// Original: FUN_004171c5(s_LEV06_06BEG_SAN, 0x20, 0xaf, 0xa0, 10, 5, 0x4b)
playLevelBegin(6);
- if (_vm->shouldQuit()) return kLevelQuit;
+ if (_vm->shouldQuit())
+ return kLevelQuit;
// FUN_00401000 + FUN_0041c7d0 + FUN_0040c040 â handler init done by IACT opcode 6
@@ -773,13 +819,15 @@ int InsaneRebel2::runLevel6() {
// + score checkpoint (FUN_00407f55) â needs per-frame callback
_deathFrame = splayer->_frame;
- if (_vm->shouldQuit()) return kLevelQuit;
+ if (_vm->shouldQuit())
+ return kLevelQuit;
if (_playerShield <= 0) {
// Died in phase 1
debug("Rebel2: Level 6 Phase 1 death at frame %d", _deathFrame);
playLevelDeathVariant(6, 1, _deathFrame);
- if (_vm->shouldQuit()) return kLevelQuit;
+ if (_vm->shouldQuit())
+ return kLevelQuit;
_playerLives--;
if (_playerLives <= 0) {
@@ -789,7 +837,8 @@ int InsaneRebel2::runLevel6() {
// Phase 1 retry (06RETRY.SAN) â restart outer loop
playLevelRetryVariant(6, 1);
- if (_vm->shouldQuit()) return kLevelQuit;
+ if (_vm->shouldQuit())
+ return kLevelQuit;
continue;
}
@@ -800,7 +849,8 @@ int InsaneRebel2::runLevel6() {
_rebelStatusBarSprite = 0;
splayer->setCurVideoFlags(0x28);
splayer->play("LEV06/06POST1.SAN", 12);
- if (_vm->shouldQuit()) return kLevelQuit;
+ if (_vm->shouldQuit())
+ return kLevelQuit;
// ===== PHASE 2 retry loop (inner while(true) in original) =====
while (!_vm->shouldQuit()) {
@@ -813,7 +863,8 @@ int InsaneRebel2::runLevel6() {
splayer->play("LEV06/06PLAY2.SAN", 12);
_deathFrame = splayer->_frame;
- if (_vm->shouldQuit()) return kLevelQuit;
+ if (_vm->shouldQuit())
+ return kLevelQuit;
// Accumulate score: local_8 = DAT_0047ab84 + local_8
totalScore += _playerScore;
@@ -829,7 +880,8 @@ int InsaneRebel2::runLevel6() {
// Died in phase 2
debug("Rebel2: Level 6 Phase 2 death at frame %d", _deathFrame);
playLevelDeathVariant(6, 2, _deathFrame);
- if (_vm->shouldQuit()) return kLevelQuit;
+ if (_vm->shouldQuit())
+ return kLevelQuit;
_playerLives--;
if (_playerLives <= 0) {
@@ -839,7 +891,8 @@ int InsaneRebel2::runLevel6() {
// Phase 2 retry (06RETRYB.SAN) â re-enter phase 2
playLevelRetryVariant(6, 2);
- if (_vm->shouldQuit()) return kLevelQuit;
+ if (_vm->shouldQuit())
+ return kLevelQuit;
}
break; // Should only reach here on shouldQuit
@@ -861,11 +914,13 @@ int InsaneRebel2::runLevel7() {
// Play cutscene (07CUT.SAN)
playCinematic("LEV07/07CUT.SAN");
- if (_vm->shouldQuit()) return kLevelQuit;
+ if (_vm->shouldQuit())
+ return kLevelQuit;
// Play level beginning cinematic (07BEG.SAN)
playLevelBegin(7);
- if (_vm->shouldQuit()) return kLevelQuit;
+ if (_vm->shouldQuit())
+ return kLevelQuit;
// FUN_00401000 + FUN_0041c7d0 + FUN_00407d10
clearBit(0);
@@ -888,7 +943,8 @@ int InsaneRebel2::runLevel7() {
splayer->play("LEV07/07PLAY.SAN", 12);
_deathFrame = splayer->_frame;
- if (_vm->shouldQuit()) return kLevelQuit;
+ if (_vm->shouldQuit())
+ return kLevelQuit;
if (_playerShield > 0) {
debug("Rebel2: Level 7 completed!");
@@ -905,7 +961,8 @@ int InsaneRebel2::runLevel7() {
} else {
playCinematic("LEV07/07DIE_A.SAN");
}
- if (_vm->shouldQuit()) return kLevelQuit;
+ if (_vm->shouldQuit())
+ return kLevelQuit;
_playerLives--;
if (_playerLives <= 0) {
@@ -914,7 +971,8 @@ int InsaneRebel2::runLevel7() {
}
playCinematic("LEV07/07RETRY.SAN");
- if (_vm->shouldQuit()) return kLevelQuit;
+ if (_vm->shouldQuit())
+ return kLevelQuit;
}
return kLevelQuit;
@@ -933,7 +991,8 @@ int InsaneRebel2::runLevel8() {
// No cutscene â starts directly with BEG
// Original: FUN_004171c5("08BEG.SAN", 0x20, 0xb1, 0xa0, 10, 5, 0x4b)
playLevelBegin(8);
- if (_vm->shouldQuit()) return kLevelQuit;
+ if (_vm->shouldQuit())
+ return kLevelQuit;
// FUN_00401000 + FUN_0041c7d0 + FUN_0040c040
clearBit(0);
@@ -951,7 +1010,8 @@ int InsaneRebel2::runLevel8() {
splayer->play("LEV08/08PLAY.SAN", 12);
_deathFrame = splayer->_frame;
- if (_vm->shouldQuit()) return kLevelQuit;
+ if (_vm->shouldQuit())
+ return kLevelQuit;
if (_playerShield > 0) {
int accuracy = 0;
@@ -968,7 +1028,8 @@ int InsaneRebel2::runLevel8() {
// Original: random(2) â A or B via string pointer arithmetic
debug("Rebel2: Level 8 death at frame %d", _deathFrame);
playLevelDeathVariant(8, 1, _deathFrame);
- if (_vm->shouldQuit()) return kLevelQuit;
+ if (_vm->shouldQuit())
+ return kLevelQuit;
_playerLives--;
if (_playerLives <= 0) {
@@ -977,7 +1038,8 @@ int InsaneRebel2::runLevel8() {
}
playCinematic("LEV08/08RETRY.SAN");
- if (_vm->shouldQuit()) return kLevelQuit;
+ if (_vm->shouldQuit())
+ return kLevelQuit;
}
return kLevelQuit;
@@ -997,7 +1059,8 @@ int InsaneRebel2::runLevel9() {
// No cutscene â starts directly with BEG
// Original: FUN_004171c5("09BEG.SAN", 0x20, 0xb2, 0xa0, 10, 200, 0x10e)
playLevelBegin(9);
- if (_vm->shouldQuit()) return kLevelQuit;
+ if (_vm->shouldQuit())
+ return kLevelQuit;
// FUN_00401000 + FUN_0041c7d0 + FUN_0040c040
clearBit(0);
@@ -1020,7 +1083,8 @@ int InsaneRebel2::runLevel9() {
splayer->play("LEV09/09PLAY.SAN", 12);
_deathFrame = splayer->_frame;
- if (_vm->shouldQuit()) return kLevelQuit;
+ if (_vm->shouldQuit())
+ return kLevelQuit;
if (_playerShield > 0) {
int accuracy = 0;
@@ -1037,7 +1101,8 @@ int InsaneRebel2::runLevel9() {
// Original: 0âDIE_A, 1âDIE_C, elseâDIE_B
debug("Rebel2: Level 9 death at frame %d", _deathFrame);
playLevelDeathVariant(9, 1, _deathFrame);
- if (_vm->shouldQuit()) return kLevelQuit;
+ if (_vm->shouldQuit())
+ return kLevelQuit;
_playerLives--;
if (_playerLives <= 0) {
@@ -1046,7 +1111,8 @@ int InsaneRebel2::runLevel9() {
}
playCinematic("LEV09/09RETRY.SAN");
- if (_vm->shouldQuit()) return kLevelQuit;
+ if (_vm->shouldQuit())
+ return kLevelQuit;
}
return kLevelQuit;
@@ -1064,12 +1130,14 @@ int InsaneRebel2::runLevel10() {
// Play cutscene (10CUT.SAN)
playCinematic("LEV10/10CUT.SAN");
- if (_vm->shouldQuit()) return kLevelQuit;
+ if (_vm->shouldQuit())
+ return kLevelQuit;
// Play level beginning cinematic (10BEG.SAN)
// Original: FUN_004171c5("10BEG.SAN", 0x20, 0xb3, 0xa0, 10, 2, 0x46)
playLevelBegin(10);
- if (_vm->shouldQuit()) return kLevelQuit;
+ if (_vm->shouldQuit())
+ return kLevelQuit;
// FUN_00401000 + FUN_0041c7d0 + FUN_00407d10
clearBit(0);
@@ -1087,7 +1155,8 @@ int InsaneRebel2::runLevel10() {
splayer->play("LEV10/10PLAY.SAN", 12);
_deathFrame = splayer->_frame;
- if (_vm->shouldQuit()) return kLevelQuit;
+ if (_vm->shouldQuit())
+ return kLevelQuit;
if (_playerShield > 0) {
int accuracy = 0;
@@ -1110,9 +1179,11 @@ int InsaneRebel2::runLevel10() {
}
playCinematic("LEV10/10DIE.SAN");
- if (_vm->shouldQuit()) return kLevelQuit;
+ if (_vm->shouldQuit())
+ return kLevelQuit;
playCinematic("LEV10/10RETRY.SAN");
- if (_vm->shouldQuit()) return kLevelQuit;
+ if (_vm->shouldQuit())
+ return kLevelQuit;
}
return kLevelQuit;
@@ -1141,11 +1212,13 @@ int InsaneRebel2::runLevel11() {
// Play cutscene (11CUT.SAN)
playCinematic("LEV11/11CUT.SAN");
- if (_vm->shouldQuit()) return kLevelQuit;
+ if (_vm->shouldQuit())
+ return kLevelQuit;
// Play level beginning cinematic (11BEG.SAN)
playLevelBegin(11);
- if (_vm->shouldQuit()) return kLevelQuit;
+ if (_vm->shouldQuit())
+ return kLevelQuit;
// FUN_00401000 + FUN_00407d10 + FUN_0040c040: Reset game state
clearBit(0);
@@ -1186,7 +1259,8 @@ int InsaneRebel2::runLevel11() {
splayer->play("LEV11/P1/11P01_A.SAN", 12);
_deathFrame = splayer->_frame;
- if (_vm->shouldQuit()) return kLevelQuit;
+ if (_vm->shouldQuit())
+ return kLevelQuit;
{
uint16 waveSelect = processWaveEnd(0x0e, &budget, 0, 0);
@@ -1194,7 +1268,8 @@ int InsaneRebel2::runLevel11() {
// Phase 1 wave loop: random(2) | (waveSelect & 8) â variants
// 0âD, 1âC, 8âB, 9âA
while (waveSelect != 0xFFFF) {
- if (_vm->shouldQuit()) return kLevelQuit;
+ if (_vm->shouldQuit())
+ return kLevelQuit;
// Bonus sound check
if ((_rebelPhaseState & 0x10) != 0 && (prevPhaseState & 0x10) == 0) {
@@ -1220,15 +1295,18 @@ int InsaneRebel2::runLevel11() {
}
}
- if (_playerDamage >= 255) goto level11_death_phase1;
- if (_vm->shouldQuit()) return kLevelQuit;
+ if (_playerDamage >= 255)
+ goto level11_death_phase1;
+ if (_vm->shouldQuit())
+ return kLevelQuit;
// Post segment 1 (11POST1.SAN)
_rebelHandler = 0;
_rebelStatusBarSprite = 0;
splayer->setCurVideoFlags(0x28);
splayer->play("LEV11/11POST1.SAN", 12);
- if (_vm->shouldQuit()) return kLevelQuit;
+ if (_vm->shouldQuit())
+ return kLevelQuit;
totalKills += _rebelKillCounter;
totalMisses += _rebelHitCounter;
@@ -1250,7 +1328,8 @@ int InsaneRebel2::runLevel11() {
splayer->play("LEV11/P2/11P02_A.SAN", 12);
_deathFrame = splayer->_frame;
- if (_vm->shouldQuit()) return kLevelQuit;
+ if (_vm->shouldQuit())
+ return kLevelQuit;
{
// Phase 2: flags=3 (maxCredits=2, redistribution ON)
@@ -1258,7 +1337,8 @@ int InsaneRebel2::runLevel11() {
// Random(4) for variant selection: A, B, C, D
while (waveSelect != 0xFFFF) {
- if (_vm->shouldQuit()) return kLevelQuit;
+ if (_vm->shouldQuit())
+ return kLevelQuit;
int variant = _vm->_rnd.getRandomNumber(3);
const char *filename;
@@ -1278,15 +1358,18 @@ int InsaneRebel2::runLevel11() {
}
}
- if (_playerDamage >= 255) goto level11_death_phase2;
- if (_vm->shouldQuit()) return kLevelQuit;
+ if (_playerDamage >= 255)
+ goto level11_death_phase2;
+ if (_vm->shouldQuit())
+ return kLevelQuit;
// Post segment 2 (11POST2.SAN)
_rebelHandler = 0;
_rebelStatusBarSprite = 0;
splayer->setCurVideoFlags(0x28);
splayer->play("LEV11/11POST2.SAN", 12);
- if (_vm->shouldQuit()) return kLevelQuit;
+ if (_vm->shouldQuit())
+ return kLevelQuit;
totalKills += _rebelKillCounter;
totalMisses += _rebelHitCounter;
@@ -1309,7 +1392,8 @@ int InsaneRebel2::runLevel11() {
splayer->play("LEV11/P3/11P03_A.SAN", 12);
_deathFrame = splayer->_frame;
- if (_vm->shouldQuit()) return kLevelQuit;
+ if (_vm->shouldQuit())
+ return kLevelQuit;
{
uint16 waveSelect = processWaveEnd(0x7e, &budget, 0, 0);
@@ -1317,7 +1401,8 @@ int InsaneRebel2::runLevel11() {
// Loop until (phaseState & 0x70) == 0x70 (bridge targets destroyed)
while (waveSelect != 0xFFFF && (_rebelPhaseState & 0x70) != 0x70) {
- if (_vm->shouldQuit()) return kLevelQuit;
+ if (_vm->shouldQuit())
+ return kLevelQuit;
// Bonus sound: (phaseState & 0xe) == 0xe and previous wasn't
if ((_rebelPhaseState & 0x0e) == 0x0e && (prevPhaseState & 0x0e) != 0x0e) {
@@ -1355,8 +1440,10 @@ int InsaneRebel2::runLevel11() {
}
}
- if (_playerDamage >= 255) goto level11_death_phase3;
- if (_vm->shouldQuit()) return kLevelQuit;
+ if (_playerDamage >= 255)
+ goto level11_death_phase3;
+ if (_vm->shouldQuit())
+ return kLevelQuit;
// ===== PHASE 3 BRIDGE CINEMATICS =====
{
@@ -1372,7 +1459,8 @@ int InsaneRebel2::runLevel11() {
}
}
- if (_vm->shouldQuit()) return kLevelQuit;
+ if (_vm->shouldQuit())
+ return kLevelQuit;
// ===== PHASE 3 SECOND HALF: P3/11P03_X (G-L) =====
// Reset shots/explosions (FUN_0041ca6a equivalent)
@@ -1395,7 +1483,8 @@ int InsaneRebel2::runLevel11() {
splayer->play("LEV11/P3/11P03_G.SAN", 12);
_deathFrame = splayer->_frame;
- if (_vm->shouldQuit()) return kLevelQuit;
+ if (_vm->shouldQuit())
+ return kLevelQuit;
// Only enter wave loop if not all basic types killed already
if ((_rebelPhaseState & 0x0e) < 0x0e) {
@@ -1403,7 +1492,8 @@ int InsaneRebel2::runLevel11() {
uint16 waveSelect = processWaveEnd(0x0e, &budget, 0, 0);
while (waveSelect != 0xFFFF) {
- if (_vm->shouldQuit()) return kLevelQuit;
+ if (_vm->shouldQuit())
+ return kLevelQuit;
// Wider randomization for first few waves
if (local_8 < 4) {
@@ -1437,8 +1527,10 @@ int InsaneRebel2::runLevel11() {
totalKills += _rebelKillCounter;
- if (_playerDamage >= 255) goto level11_death_phase3;
- if (_vm->shouldQuit()) return kLevelQuit;
+ if (_playerDamage >= 255)
+ goto level11_death_phase3;
+ if (_vm->shouldQuit())
+ return kLevelQuit;
// ===== LEVEL COMPLETED =====
{
@@ -1472,7 +1564,8 @@ int InsaneRebel2::runLevel11() {
goto level11_retry;
level11_retry:
- if (_vm->shouldQuit()) return kLevelQuit;
+ if (_vm->shouldQuit())
+ return kLevelQuit;
_playerLives--;
if (_playerLives <= 0) {
playLevelGameOver(11);
@@ -1480,7 +1573,8 @@ int InsaneRebel2::runLevel11() {
}
playCinematic("LEV11/11RETRY.SAN");
_playerDamage = 0;
- if (_vm->shouldQuit()) return kLevelQuit;
+ if (_vm->shouldQuit())
+ return kLevelQuit;
continue; // Restart from Phase 1
}
@@ -1507,11 +1601,13 @@ int InsaneRebel2::runLevel12() {
// Play cutscene (12CUT.SAN)
playCinematic("LEV12/12CUT.SAN");
- if (_vm->shouldQuit()) return kLevelQuit;
+ if (_vm->shouldQuit())
+ return kLevelQuit;
// Play level beginning cinematic (12BEG.SAN)
playLevelBegin(12);
- if (_vm->shouldQuit()) return kLevelQuit;
+ if (_vm->shouldQuit())
+ return kLevelQuit;
// FUN_0041c7d0 + FUN_00407d10 + FUN_0040c040: Reset game state
clearBit(0);
@@ -1547,21 +1643,24 @@ int InsaneRebel2::runLevel12() {
debug("Rebel2: Level 12 Phase 1 - init 12P05.SAN budget=%d", budget);
splayer->setCurVideoFlags(0x28);
splayer->play("LEV12/12P05.SAN", 12);
- if (_vm->shouldQuit()) return kLevelQuit;
+ if (_vm->shouldQuit())
+ return kLevelQuit;
processWaveEnd(1, &budget, 0, 0);
// First wave (P1/12P01_A.SAN)
splayer->setCurVideoFlags(0x428);
splayer->play("LEV12/P1/12P01_A.SAN", 12);
_deathFrame = splayer->_frame;
- if (_vm->shouldQuit()) return kLevelQuit;
+ if (_vm->shouldQuit())
+ return kLevelQuit;
{
uint16 waveSelect = processWaveEnd(6, &budget, 0x14, 0);
// Wave loop: random(2) | (waveSelect & 2) â 0:C, 1:D, 2:A, 3:B
while (waveSelect != 0xFFFF) {
- if (_vm->shouldQuit()) return kLevelQuit;
+ if (_vm->shouldQuit())
+ return kLevelQuit;
int sel = _vm->_rnd.getRandomNumber(1) | (waveSelect & 2);
const char *filename;
@@ -1581,8 +1680,10 @@ int InsaneRebel2::runLevel12() {
}
}
- if (_playerDamage >= 255) goto level12_death;
- if (_vm->shouldQuit()) return kLevelQuit;
+ if (_playerDamage >= 255)
+ goto level12_death;
+ if (_vm->shouldQuit())
+ return kLevelQuit;
// ===== PHASE 2: 12P06 â P2/12P02_X =====
_currentPhase = 2;
@@ -1595,20 +1696,23 @@ int InsaneRebel2::runLevel12() {
debug("Rebel2: Level 12 Phase 2 - init 12P06.SAN budget=%d", budget);
splayer->setCurVideoFlags(0x428);
splayer->play("LEV12/12P06.SAN", 12);
- if (_vm->shouldQuit()) return kLevelQuit;
+ if (_vm->shouldQuit())
+ return kLevelQuit;
processWaveEnd(1, &budget, 0, 0);
// First wave (P2/12P02_A.SAN)
splayer->setCurVideoFlags(0x428);
splayer->play("LEV12/P2/12P02_A.SAN", 12);
_deathFrame = splayer->_frame;
- if (_vm->shouldQuit()) return kLevelQuit;
+ if (_vm->shouldQuit())
+ return kLevelQuit;
{
uint16 waveSelect = processWaveEnd(6, &budget, 0x14, 0);
while (waveSelect != 0xFFFF) {
- if (_vm->shouldQuit()) return kLevelQuit;
+ if (_vm->shouldQuit())
+ return kLevelQuit;
// Variant selection: (waveSelect & 2) controls which set
int local_8;
@@ -1639,8 +1743,10 @@ int InsaneRebel2::runLevel12() {
}
}
- if (_playerDamage >= 255) goto level12_death;
- if (_vm->shouldQuit()) return kLevelQuit;
+ if (_playerDamage >= 255)
+ goto level12_death;
+ if (_vm->shouldQuit())
+ return kLevelQuit;
// ===== PHASE 3: 12P07 â P3/12P03_X =====
_currentPhase = 3;
@@ -1653,21 +1759,24 @@ int InsaneRebel2::runLevel12() {
debug("Rebel2: Level 12 Phase 3 - init 12P07.SAN budget=%d", budget);
splayer->setCurVideoFlags(0x428);
splayer->play("LEV12/12P07.SAN", 12);
- if (_vm->shouldQuit()) return kLevelQuit;
+ if (_vm->shouldQuit())
+ return kLevelQuit;
processWaveEnd(1, &budget, 0, 0);
// First wave (P3/12P03_A.SAN)
splayer->setCurVideoFlags(0x428);
splayer->play("LEV12/P3/12P03_A.SAN", 12);
_deathFrame = splayer->_frame;
- if (_vm->shouldQuit()) return kLevelQuit;
+ if (_vm->shouldQuit())
+ return kLevelQuit;
{
int local_8 = 0;
uint16 waveSelect = processWaveEnd(6, &budget, 0x14, 0);
while (waveSelect != 0xFFFF) {
- if (_vm->shouldQuit()) return kLevelQuit;
+ if (_vm->shouldQuit())
+ return kLevelQuit;
// Wider randomization for first few waves
if (local_8 < 4) {
@@ -1695,8 +1804,10 @@ int InsaneRebel2::runLevel12() {
}
}
- if (_playerDamage >= 255) goto level12_death;
- if (_vm->shouldQuit()) return kLevelQuit;
+ if (_playerDamage >= 255)
+ goto level12_death;
+ if (_vm->shouldQuit())
+ return kLevelQuit;
// ===== PHASE 4: 12P08 â P4/12P04_X =====
_currentPhase = 4;
@@ -1709,21 +1820,24 @@ int InsaneRebel2::runLevel12() {
debug("Rebel2: Level 12 Phase 4 - init 12P08.SAN budget=%d", budget);
splayer->setCurVideoFlags(0x428);
splayer->play("LEV12/12P08.SAN", 12);
- if (_vm->shouldQuit()) return kLevelQuit;
+ if (_vm->shouldQuit())
+ return kLevelQuit;
processWaveEnd(1, &budget, 0, 0);
// First wave (P4/12P04_A.SAN)
splayer->setCurVideoFlags(0x428);
splayer->play("LEV12/P4/12P04_A.SAN", 12);
_deathFrame = splayer->_frame;
- if (_vm->shouldQuit()) return kLevelQuit;
+ if (_vm->shouldQuit())
+ return kLevelQuit;
{
int local_8 = 0;
uint16 waveSelect = processWaveEnd(0x0e, &budget, 0x14, 0);
while (waveSelect != 0xFFFF) {
- if (_vm->shouldQuit()) return kLevelQuit;
+ if (_vm->shouldQuit())
+ return kLevelQuit;
if (local_8 < 4) {
local_8 = _vm->_rnd.getRandomNumber(5); // 0-5
@@ -1750,13 +1864,16 @@ int InsaneRebel2::runLevel12() {
}
}
- if (_playerDamage >= 255) goto level12_death;
- if (_vm->shouldQuit()) return kLevelQuit;
+ if (_playerDamage >= 255)
+ goto level12_death;
+ if (_vm->shouldQuit())
+ return kLevelQuit;
// ===== CLOSING: 12P09.SAN =====
splayer->setCurVideoFlags(0x428);
splayer->play("LEV12/12P09.SAN", 12);
- if (_vm->shouldQuit()) return kLevelQuit;
+ if (_vm->shouldQuit())
+ return kLevelQuit;
processWaveEnd(1, &budget, 0, 0);
// ===== LEVEL COMPLETED =====
@@ -1782,7 +1899,8 @@ int InsaneRebel2::runLevel12() {
debug("Rebel2: Level 12 Phase %d death", _currentPhase);
playCinematic("LEV12/12DIE.SAN");
- if (_vm->shouldQuit()) return kLevelQuit;
+ if (_vm->shouldQuit())
+ return kLevelQuit;
_playerLives--;
if (_playerLives <= 0) {
playLevelGameOver(12);
@@ -1790,7 +1908,8 @@ int InsaneRebel2::runLevel12() {
}
playCinematic("LEV12/12RETRY.SAN");
_playerDamage = 0;
- if (_vm->shouldQuit()) return kLevelQuit;
+ if (_vm->shouldQuit())
+ return kLevelQuit;
continue; // Restart from Phase 1
}
@@ -1812,7 +1931,8 @@ int InsaneRebel2::runLevel13() {
// No cutscene â starts directly with BEG
// Original: FUN_004171c5("13BEG.SAN", 0x20, 0xb6, 0xa0, 10, 2, 0x46)
playLevelBegin(13);
- if (_vm->shouldQuit()) return kLevelQuit;
+ if (_vm->shouldQuit())
+ return kLevelQuit;
// FUN_00401000 + FUN_0041c7d0 + FUN_0040c040
clearBit(0);
@@ -1833,7 +1953,8 @@ int InsaneRebel2::runLevel13() {
splayer->play("LEV13/13PLAY_A.SAN", 12);
_deathFrame = splayer->_frame;
- if (_vm->shouldQuit()) return kLevelQuit;
+ if (_vm->shouldQuit())
+ return kLevelQuit;
// If alive after Phase A, play Phase B (reactor destruction loop)
// Original: at frame == maxFrame-10, play 13PLAY_B.SAN (0x468)
@@ -1846,7 +1967,8 @@ int InsaneRebel2::runLevel13() {
_deathFrame = splayer->_frame;
}
- if (_vm->shouldQuit()) return kLevelQuit;
+ if (_vm->shouldQuit())
+ return kLevelQuit;
if (_playerShield > 0) {
int accuracy = 0;
@@ -1862,7 +1984,8 @@ int InsaneRebel2::runLevel13() {
// Death: frame-based variant selection (FUN_0041B3E1 lines 47-61)
debug("Rebel2: Level 13 death at frame %d", _deathFrame);
playLevelDeathVariant(13, 1, _deathFrame);
- if (_vm->shouldQuit()) return kLevelQuit;
+ if (_vm->shouldQuit())
+ return kLevelQuit;
_playerLives--;
if (_playerLives <= 0) {
@@ -1871,7 +1994,8 @@ int InsaneRebel2::runLevel13() {
}
playCinematic("LEV13/13RETRY.SAN");
- if (_vm->shouldQuit()) return kLevelQuit;
+ if (_vm->shouldQuit())
+ return kLevelQuit;
}
return kLevelQuit;
@@ -1889,7 +2013,8 @@ int InsaneRebel2::runLevel14() {
// No cutscene â starts directly with BEG
// Original: FUN_004171c5("14BEG.SAN", 0x20, 0xb7, 0xa0, 10, 2, 0x46)
playLevelBegin(14);
- if (_vm->shouldQuit()) return kLevelQuit;
+ if (_vm->shouldQuit())
+ return kLevelQuit;
// FUN_00401000 + FUN_0041c7d0 + FUN_0040c040
clearBit(0);
@@ -1907,7 +2032,8 @@ int InsaneRebel2::runLevel14() {
splayer->play("LEV14/14PLAY.SAN", 12);
_deathFrame = splayer->_frame;
- if (_vm->shouldQuit()) return kLevelQuit;
+ if (_vm->shouldQuit())
+ return kLevelQuit;
if (_playerShield > 0) {
int accuracy = 0;
@@ -1923,7 +2049,8 @@ int InsaneRebel2::runLevel14() {
// Death: single video (14DIE.SAN)
debug("Rebel2: Level 14 death at frame %d", _deathFrame);
playCinematic("LEV14/14DIE.SAN");
- if (_vm->shouldQuit()) return kLevelQuit;
+ if (_vm->shouldQuit())
+ return kLevelQuit;
_playerLives--;
if (_playerLives <= 0) {
@@ -1932,7 +2059,8 @@ int InsaneRebel2::runLevel14() {
}
playCinematic("LEV14/14RETRY.SAN");
- if (_vm->shouldQuit()) return kLevelQuit;
+ if (_vm->shouldQuit())
+ return kLevelQuit;
}
return kLevelQuit;
@@ -1952,12 +2080,14 @@ int InsaneRebel2::runLevel15() {
// Play cutscene (15CUT.SAN)
playCinematic("LEV15/15CUT.SAN");
- if (_vm->shouldQuit()) return kLevelQuit;
+ if (_vm->shouldQuit())
+ return kLevelQuit;
// Play level beginning cinematic (15BEG.SAN)
// Original: FUN_004171c5("15BEG.SAN", 0x20, 0xb8, 0xa0, 10, 2, 0x46)
playLevelBegin(15);
- if (_vm->shouldQuit()) return kLevelQuit;
+ if (_vm->shouldQuit())
+ return kLevelQuit;
// FUN_00401000 + FUN_0041c7d0 + FUN_0040c040
clearBit(0);
@@ -1981,7 +2111,8 @@ int InsaneRebel2::runLevel15() {
splayer->play("LEV15/15PLAY.SAN", 12);
_deathFrame = splayer->_frame;
- if (_vm->shouldQuit()) return kLevelQuit;
+ if (_vm->shouldQuit())
+ return kLevelQuit;
if (_playerShield > 0) {
int accuracy = 0;
@@ -1998,7 +2129,8 @@ int InsaneRebel2::runLevel15() {
// Death: frame-based variant selection (FUN_0041B8D7 lines 46-65)
debug("Rebel2: Level 15 death at frame %d", _deathFrame);
playLevelDeathVariant(15, 1, _deathFrame);
- if (_vm->shouldQuit()) return kLevelQuit;
+ if (_vm->shouldQuit())
+ return kLevelQuit;
_playerLives--;
if (_playerLives <= 0) {
@@ -2007,7 +2139,8 @@ int InsaneRebel2::runLevel15() {
}
playCinematic("LEV15/15RETRY.SAN");
- if (_vm->shouldQuit()) return kLevelQuit;
+ if (_vm->shouldQuit())
+ return kLevelQuit;
}
return kLevelQuit;
Commit: a03ea4774fb3f3059409c24fe07a1e52fd7dd42c
https://github.com/scummvm/scummvm/commit/a03ea4774fb3f3059409c24fe07a1e52fd7dd42c
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:12+02:00
Commit Message:
SCUMM: RA2: Improve game detection
Changed paths:
engines/scumm/detection.h
engines/scumm/detection_tables.h
engines/scumm/metaengine.cpp
diff --git a/engines/scumm/detection.h b/engines/scumm/detection.h
index fbf94c37023..5daff20041f 100644
--- a/engines/scumm/detection.h
+++ b/engines/scumm/detection.h
@@ -38,6 +38,7 @@ namespace Scumm {
#define GAMEOPTION_COPY_PROTECTION GUIO_GAMEOPTIONS7
#define GAMEOPTION_USE_REMASTERED_AUDIO GUIO_GAMEOPTIONS8
#define GAMEOPTION_TTS GUIO_GAMEOPTIONS9
+#define GAMEOPTION_REBEL2_HIRES GUIO_GAMEOPTIONS10
/**
* Descriptor of a specific SCUMM game. Used internally to store
diff --git a/engines/scumm/detection_tables.h b/engines/scumm/detection_tables.h
index ced21e29990..34835f71af0 100644
--- a/engines/scumm/detection_tables.h
+++ b/engines/scumm/detection_tables.h
@@ -49,6 +49,7 @@ static const char *const directoryGlobs[] = {
"Contents", // Mac Steam versions
"MacOS", // Mac Steam versions
"Resources", // Mac SE/Remastered versions
+ "OPEN", // RA2 demo detection (O_DEMO.SAN)
0
};
@@ -227,8 +228,8 @@ static const GameSettings gameVariantsTable[] = {
{"ft", "Remastered", 0, GID_FT, 7, 0, MDT_NONE, GF_DOUBLEFINE_PAK, UNK, GUIO5(GUIO_NOMIDI, GAMEOPTION_ENHANCEMENTS, GAMEOPTION_ORIGINALGUI, GAMEOPTION_LOWLATENCYAUDIO, GAMEOPTION_TTS)},
{"ft", "Demo", 0, GID_FT, 7, 0, MDT_NONE, GF_DEMO, UNK, GUIO4(GUIO_NOMIDI, GAMEOPTION_ORIGINALGUI, GAMEOPTION_LOWLATENCYAUDIO, GAMEOPTION_TTS)},
- {"rebel2", "", 0, GID_REBEL2, 7, 0, MDT_NONE, 0, Common::kPlatformDOS, GUIO5(GUIO_NOMIDI, GAMEOPTION_ENHANCEMENTS, GAMEOPTION_ORIGINALGUI, GAMEOPTION_LOWLATENCYAUDIO, GAMEOPTION_TTS)},
- {"rebel2", "Demo", 0, GID_REBEL2, 7, 0, MDT_NONE, GF_DEMO, Common::kPlatformDOS, GUIO5(GUIO_NOMIDI, GAMEOPTION_ENHANCEMENTS, GAMEOPTION_ORIGINALGUI, GAMEOPTION_LOWLATENCYAUDIO, GAMEOPTION_TTS)},
+ {"rebel2", "", 0, GID_REBEL2, 7, 0, MDT_NONE, 0, Common::kPlatformDOS, GUIO2(GUIO_NOMIDI, GAMEOPTION_REBEL2_HIRES)},
+ {"rebel2", "Demo", 0, GID_REBEL2, 7, 0, MDT_NONE, GF_DEMO, Common::kPlatformDOS, GUIO2(GUIO_NOMIDI, GAMEOPTION_REBEL2_HIRES)},
{"dig", "", 0, GID_DIG, 7, 0, MDT_NONE, 0, UNK, GUIO5(GUIO_NOMIDI, GAMEOPTION_ENHANCEMENTS, GAMEOPTION_ORIGINALGUI, GAMEOPTION_LOWLATENCYAUDIO, GAMEOPTION_TTS)},
{"dig", "Demo", 0, GID_DIG, 7, 0, MDT_NONE, GF_DEMO, UNK, GUIO4(GUIO_NOMIDI, GAMEOPTION_ORIGINALGUI, GAMEOPTION_LOWLATENCYAUDIO, GAMEOPTION_TTS)},
@@ -498,7 +499,8 @@ static const GameFilenamePattern gameFilenamesTable[] = {
{ "ft", "Vollgas Data", kGenUnchanged, Common::DE_DEU, Common::kPlatformMacintosh, 0 },
{ "ft", "Vollgas Demo Data", kGenUnchanged, Common::DE_DEU, Common::kPlatformMacintosh, "Demo" },
- { "rebel2", "REBEL2.EXE", kGenUnchanged, UNK_LANG, Common::kPlatformDOS, 0 },
+ { "rebel2", "RA2START.EXE", kGenUnchanged, UNK_LANG, Common::kPlatformDOS, "" },
+ { "rebel2", "O_DEMO.SAN", kGenUnchanged, UNK_LANG, Common::kPlatformDOS, "Demo" },
{ "comi", "comi.la%d", kGenDiskNum, UNK_LANG, UNK, 0 },
diff --git a/engines/scumm/metaengine.cpp b/engines/scumm/metaengine.cpp
index 6254aec88a0..4fe0f9a1511 100644
--- a/engines/scumm/metaengine.cpp
+++ b/engines/scumm/metaengine.cpp
@@ -878,6 +878,15 @@ static const ExtraGuiOption enableTTS = {
};
#endif
+static const ExtraGuiOption enableRebel2HiRes = {
+ _s("High resolution mode"),
+ _s("Run the game in 640x400 high resolution mode instead of 320x200."),
+ "rebel2_hires",
+ true,
+ 0,
+ 0
+};
+
const ExtraGuiOptions ScummMetaEngine::getExtraGuiOptions(const Common::String &target) const {
ExtraGuiOptions options;
// Query the GUI options
@@ -919,6 +928,9 @@ const ExtraGuiOptions ScummMetaEngine::getExtraGuiOptions(const Common::String &
options.push_back(enableTTS);
}
#endif
+ if (target.empty() || guiOptions.contains(GAMEOPTION_REBEL2_HIRES)) {
+ options.push_back(enableRebel2HiRes);
+ }
if (target.empty() || gameid == "comi") {
options.push_back(comiObjectLabelsOption);
Commit: de4c041f00325eda2051ca33d878b59d4c9eb4b1
https://github.com/scummvm/scummvm/commit/de4c041f00325eda2051ca33d878b59d4c9eb4b1
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:13+02:00
Commit Message:
SCUMM: RA2: Reset enemy state for level 2 waves
Changed paths:
engines/scumm/insane/insane_rebel_iact.cpp
diff --git a/engines/scumm/insane/insane_rebel_iact.cpp b/engines/scumm/insane/insane_rebel_iact.cpp
index a4300324b92..7a17b021d63 100644
--- a/engines/scumm/insane/insane_rebel_iact.cpp
+++ b/engines/scumm/insane/insane_rebel_iact.cpp
@@ -2195,10 +2195,14 @@ void InsaneRebel2::enemyUpdate(byte *renderBitmap, Common::SeekableReadStream &b
if (it->id == enemyId) {
it->rect = Common::Rect(x, y, x + w, y + h);
it->type = par4; // Enemy type from IACT offset +6 (userId)
- // Only re-activate if not destroyed
- if (!it->destroyed) {
- it->active = true;
- }
+ // The _iactBits[] bit table is the authoritative alive/dead state.
+ // We only reach here when isBitSet(enemyId) == false, meaning
+ // the game considers this enemy alive. Reset destroyed/active
+ // to match â this is critical when clearBit(0) re-enables all
+ // enemies at wave start but the _enemies list still has stale
+ // destroyed=true from a previous wave.
+ it->active = true;
+ it->destroyed = false;
found = true;
break;
}
Commit: c4e1df0f2f63e5ae58b994faf00ab16a948801d8
https://github.com/scummvm/scummvm/commit/c4e1df0f2f63e5ae58b994faf00ab16a948801d8
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:13+02:00
Commit Message:
SCUMM: RA2: Add intro level text overlay
Changed paths:
engines/scumm/insane/insane_rebel.cpp
engines/scumm/insane/insane_rebel.h
engines/scumm/insane/insane_rebel_levels.cpp
engines/scumm/insane/insane_rebel_render.cpp
diff --git a/engines/scumm/insane/insane_rebel.cpp b/engines/scumm/insane/insane_rebel.cpp
index ad9204e895e..0c57a5ab083 100644
--- a/engines/scumm/insane/insane_rebel.cpp
+++ b/engines/scumm/insane/insane_rebel.cpp
@@ -127,6 +127,14 @@ InsaneRebel2::InsaneRebel2(ScummEngine_v7 *scumm) {
_damageShakeCounter = 0;
memset(_damageSavedPalette, 0, sizeof(_damageSavedPalette));
+ // Text overlay state (FUN_004171c5 chapter title rendering)
+ _textOverlayActive = false;
+ _textOverlayID = 0;
+ _textOverlayX = 0;
+ _textOverlayY = 0;
+ _textOverlayFadeIn = 0;
+ _textOverlayFadeOut = 0;
+
// Retail globals mapped: hit counter, cooldown, invulnerability flag
_rebelOp6Initialized = false;
_rebelHitCounter = 0;
diff --git a/engines/scumm/insane/insane_rebel.h b/engines/scumm/insane/insane_rebel.h
index 73f780de4a1..8bc1c9d47cf 100644
--- a/engines/scumm/insane/insane_rebel.h
+++ b/engines/scumm/insane/insane_rebel.h
@@ -331,6 +331,19 @@ public:
// Play cinematic video by filename
void playCinematic(const char *filename);
+ // Play cinematic with text overlay (emulates FUN_004171c5)
+ // Text is progressively revealed during [fadeInFrame, fadeOutFrame)
+ void playVideoWithText(const char *filename, int textID, int textX, int textY,
+ int fadeInFrame, int fadeOutFrame);
+
+ // Text overlay state (active during playVideoWithText cinematics)
+ bool _textOverlayActive; // True when text overlay should render
+ int _textOverlayID; // TRS string ID
+ int _textOverlayX; // X position for text rendering
+ int _textOverlayY; // Y position for text rendering
+ int _textOverlayFadeIn; // Frame to start progressive text reveal
+ int _textOverlayFadeOut; // Frame to stop text rendering
+
// Play death video with proper variant selection
void playLevelDeathVariant(int levelId, int phase, int frame);
diff --git a/engines/scumm/insane/insane_rebel_levels.cpp b/engines/scumm/insane/insane_rebel_levels.cpp
index 432ad35bb01..7bf13aa5c2c 100644
--- a/engines/scumm/insane/insane_rebel_levels.cpp
+++ b/engines/scumm/insane/insane_rebel_levels.cpp
@@ -109,16 +109,82 @@ void InsaneRebel2::playCinematic(const char *filename) {
splayer->play(filename, 12);
}
+void InsaneRebel2::playVideoWithText(const char *filename, int textID, int textX, int textY,
+ int fadeInFrame, int fadeOutFrame) {
+ // Emulates FUN_004171c5: plays video with progressive text overlay
+ // Text string loaded from GAME.TRS via getString(textID)
+ // During frame range [fadeInFrame, fadeOutFrame):
+ // displayLength = currentFrame + 10 - fadeInFrame, capped at 0xBE (190) chars
+ // Text rendered at (textX, textY) using FUN_004341a0
+
+ _rebelHandler = 0;
+ _rebelStatusBarSprite = 0;
+
+ // Set up text overlay state â procPostRendering reads these each frame
+ _textOverlayActive = true;
+ _textOverlayID = textID;
+ _textOverlayX = textX;
+ _textOverlayY = textY;
+ _textOverlayFadeIn = fadeInFrame;
+ _textOverlayFadeOut = fadeOutFrame;
+
+ SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
+ splayer->setCurVideoFlags(0x28);
+ splayer->play(filename, 12);
+
+ _textOverlayActive = false;
+}
+
void InsaneRebel2::playLevelBegin(int levelId) {
// Play the level beginning cinematic (LEVXX/XXBEG.SAN)
// Emulates FUN_004171c5 call in each level handler
+ //
+ // Per-level text overlay parameters from original disassembly:
+ // All levels use FUN_004171c5 with a chapter title overlay from GAME.TRS.
+
+ struct TextOverlayParams {
+ int textID; // TRS string ID (-1 = no text overlay)
+ int textX;
+ int textY;
+ int fadeInFrame;
+ int fadeOutFrame;
+ };
+
+ // Table of per-level text overlay parameters
+ // All levels use FUN_004171c5 (verified against decompiled level handlers)
+ // Text IDs are sequential: 0xAA (level 1) through 0xB8 (level 15)
+ static const TextOverlayParams levelTextParams[16] = {
+ { -1, 0, 0, 0, 0}, // Level 0 (unused)
+ {0xAA, 0xA0, 10, 5, 0x4B}, // Level 1: FUN_00417E53
+ {0xAB, 0xA0, 10, 2, 0x46}, // Level 2: FUN_00418063
+ {0xAC, 0xA0, 10, 2, 0x46}, // Level 3: FUN_0041885F
+ {0xAD, 0xA0, 10, 2, 100}, // Level 4: FUN_00418CC4
+ {0xAE, 0xA0, 10, 5, 0x3C}, // Level 5: FUN_00418EC6
+ {0xAF, 0xA0, 10, 5, 0x4B}, // Level 6: FUN_004190D6
+ {0xB0, 0xA0, 10, 5, 0x4B}, // Level 7: FUN_0041974C
+ {0xB1, 0xA0, 10, 5, 0x4B}, // Level 8: FUN_00419976
+ {0xB2, 0xA0, 10, 200, 0x10E}, // Level 9: FUN_00419B86
+ {0xB3, 0xA0, 10, 2, 0x46}, // Level 10: FUN_00419E0A
+ {0xB4, 0xA0, 10, 2, 0x46}, // Level 11: FUN_0041A00C
+ {0xB5, 0xA0, 10, 5, 0x4B}, // Level 12: FUN_0041A3EB
+ {0xB6, 0xA0, 10, 2, 0x46}, // Level 13: FUN_0041A806
+ {0xB7, 0xA0, 10, 2, 0x46}, // Level 14: FUN_0041ABB2
+ {0xB8, 0xA0, 10, 2, 0x46}, // Level 15: FUN_0041AEE8
+ };
Common::String dir = getLevelDir(levelId);
Common::String prefix = getLevelPrefix(levelId);
Common::String filename = Common::String::format("%s/%sBEG.SAN", dir.c_str(), prefix.c_str());
debug("Rebel2: Playing level %d beginning: %s", levelId, filename.c_str());
- playCinematic(filename.c_str());
+
+ if (levelId >= 1 && levelId <= 15 && levelTextParams[levelId].textID >= 0) {
+ const TextOverlayParams &p = levelTextParams[levelId];
+ playVideoWithText(filename.c_str(), p.textID, p.textX, p.textY,
+ p.fadeInFrame, p.fadeOutFrame);
+ } else {
+ playCinematic(filename.c_str());
+ }
}
bool InsaneRebel2::playLevelGameplay(int levelId) {
diff --git a/engines/scumm/insane/insane_rebel_render.cpp b/engines/scumm/insane/insane_rebel_render.cpp
index aeacb6abaee..0fbf154869f 100644
--- a/engines/scumm/insane/insane_rebel_render.cpp
+++ b/engines/scumm/insane/insane_rebel_render.cpp
@@ -1763,6 +1763,152 @@ void InsaneRebel2::procPostRendering(byte *renderBitmap, int32 codecparam, int32
debug("Rebel2: Intro/cinematic mode (handler=0, flags=0x%x, state=%d) - HUD disabled, mouse hidden",
_player->_curVideoFlags, _gameState);
}
+
+ // Text overlay rendering for FUN_004171c5-style cinematics
+ // Draws progressive chapter title text during [fadeInFrame, fadeOutFrame)
+ if (_textOverlayActive && curFrame >= _textOverlayFadeIn && curFrame < _textOverlayFadeOut) {
+ SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
+ const char *text = splayer->getString(_textOverlayID);
+ debug(5, "Rebel2: Text overlay frame %d/%d-%d textID=0x%x text='%s'",
+ curFrame, _textOverlayFadeIn, _textOverlayFadeOut, _textOverlayID,
+ text ? text : "(null)");
+ if (text) {
+ // Progressive reveal: display length = currentFrame + 10 - fadeInFrame
+ // Capped at 0xBE (190) visible chars per original
+ int displayLen = curFrame + 10 - _textOverlayFadeIn;
+ if (displayLen > 0xBE)
+ displayLen = 0xBE;
+ if (displayLen < 0)
+ displayLen = 0;
+
+ // Font system â same as menu text (FUN_00434d10 / FUN_00433da0)
+ // ^fNN = font switch, ^cNNN = color code
+ NutRenderer *fonts[3] = {
+ _smush_talkfontNut,
+ _smush_smalfontNut,
+ _smush_titlefontNut
+ };
+ NutRenderer *defaultFont = fonts[0] ? fonts[0] : _smush_smalfontNut;
+ if (!defaultFont)
+ goto textOverlayDone;
+
+ {
+ Common::Rect clipRect(0, 0, width, height);
+
+ // Format code parser (same as drawMenuItems)
+ auto parseFormat = [&](const char *&s, NutRenderer *&curFont, int &curColor) {
+ if (*s != '^')
+ return false;
+ const char *p = s + 1;
+ if (*p == '^') { s = p; return false; } // ^^ = literal ^
+ if (*p == 'f') {
+ p++;
+ int idx = 0;
+ while (*p >= '0' && *p <= '9') { idx = idx * 10 + (*p - '0'); p++; }
+ s = p;
+ curFont = (idx >= 0 && idx < 3 && fonts[idx]) ? fonts[idx] : defaultFont;
+ return true;
+ }
+ if (*p == 'c') {
+ p++;
+ int col = 0;
+ while (*p >= '0' && *p <= '9') { col = col * 10 + (*p - '0'); p++; }
+ s = p;
+ curColor = col;
+ return true;
+ }
+ return false;
+ };
+
+ // FUN_004341a0 splits on \n and renders each line centered independently.
+ // The TRS parser joins multi-line strings with spaces (stripping \n//),
+ // so " ^f" marks where a line break was in the original TRS file.
+ // We split the text into lines first, then render each centered.
+
+ // Split text into lines at " ^f" boundaries
+ Common::Array<Common::String> lines;
+ {
+ Common::String cur;
+ const char *s = text;
+ while (*s) {
+ // Check for line break: space followed by ^f (font switch)
+ if (*s == ' ' && s[1] == '^' && s[2] == 'f') {
+ lines.push_back(cur);
+ cur.clear();
+ s++; // skip the space, keep the ^f for the next line
+ continue;
+ }
+ cur += *s++;
+ }
+ if (!cur.empty())
+ lines.push_back(cur);
+ }
+
+ int drawY = _textOverlayY;
+ int visCount = 0;
+
+ for (uint lineIdx = 0; lineIdx < lines.size() && visCount < displayLen; lineIdx++) {
+ const char *lineStr = lines[lineIdx].c_str();
+ const char *lineEnd = lineStr + lines[lineIdx].size();
+
+ // Measure this line's width (only visible chars up to displayLen)
+ int lineWidth = 0;
+ int lineVisCount = 0;
+ NutRenderer *lineFont = defaultFont;
+ {
+ const char *s = lineStr;
+ NutRenderer *mFont = defaultFont;
+ int mColor = 1;
+ while (s < lineEnd && (visCount + lineVisCount) < displayLen) {
+ if (parseFormat(s, mFont, mColor))
+ continue;
+ lineFont = mFont; // track font for line height
+ byte c = (byte)*s++;
+ if (c >= 'a' && c <= 'z')
+ c = c - 'a' + 'A';
+ if (mFont && c < mFont->getNumChars())
+ lineWidth += mFont->getCharWidth(c);
+ lineVisCount++;
+ }
+ }
+
+ // Draw this line centered at textX
+ int drawX = _textOverlayX - lineWidth / 2;
+ int lineCharsDrawn = 0;
+ {
+ const char *s = lineStr;
+ NutRenderer *curFont = defaultFont;
+ int curColor = 1;
+ while (s < lineEnd && (visCount + lineCharsDrawn) < displayLen) {
+ if (parseFormat(s, curFont, curColor))
+ continue;
+ byte c = (byte)*s++;
+ if (c >= 'a' && c <= 'z')
+ c = c - 'a' + 'A';
+ if (!curFont || c >= curFont->getNumChars()) {
+ lineCharsDrawn++;
+ continue;
+ }
+ int charW = curFont->getCharWidth(c);
+ if (drawX >= 0 && drawY >= 0 && charW > 0) {
+ curFont->drawCharV7(renderBitmap, clipRect, drawX, drawY,
+ pitch, curColor, kStyleAlignLeft, c, false, false);
+ }
+ drawX += charW;
+ lineCharsDrawn++;
+ }
+ }
+ visCount += lineCharsDrawn;
+
+ // Advance to next line â use the line's font height for spacing
+ int lineHeight = lineFont->getCharHeight('A') + 2;
+ drawY += lineHeight;
+ }
+ }
+ }
+ }
+ textOverlayDone:
+
// Skip all HUD rendering during intro - subtitles are rendered via opcode 9
return;
} else {
Commit: da4840d6b22e9278e1da06cea5623ad388411cd1
https://github.com/scummvm/scummvm/commit/da4840d6b22e9278e1da06cea5623ad388411cd1
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:13+02:00
Commit Message:
SCUMM: RA2: Refactor ship sprite rendering
Changed paths:
engines/scumm/insane/insane_rebel.h
engines/scumm/insane/insane_rebel_render.cpp
diff --git a/engines/scumm/insane/insane_rebel.h b/engines/scumm/insane/insane_rebel.h
index 8bc1c9d47cf..807184f52fe 100644
--- a/engines/scumm/insane/insane_rebel.h
+++ b/engines/scumm/insane/insane_rebel.h
@@ -344,6 +344,9 @@ public:
int _textOverlayFadeIn; // Frame to start progressive text reveal
int _textOverlayFadeOut; // Frame to stop text rendering
+ // Render chapter title text overlay (emulates FUN_004341a0 in FUN_004171c5)
+ void renderTextOverlay(byte *renderBitmap, int pitch, int width, int height, int curFrame);
+
// Play death video with proper variant selection
void playLevelDeathVariant(int levelId, int phase, int frame);
diff --git a/engines/scumm/insane/insane_rebel_render.cpp b/engines/scumm/insane/insane_rebel_render.cpp
index 0fbf154869f..01ab72b60e5 100644
--- a/engines/scumm/insane/insane_rebel_render.cpp
+++ b/engines/scumm/insane/insane_rebel_render.cpp
@@ -1764,150 +1764,9 @@ void InsaneRebel2::procPostRendering(byte *renderBitmap, int32 codecparam, int32
_player->_curVideoFlags, _gameState);
}
- // Text overlay rendering for FUN_004171c5-style cinematics
- // Draws progressive chapter title text during [fadeInFrame, fadeOutFrame)
- if (_textOverlayActive && curFrame >= _textOverlayFadeIn && curFrame < _textOverlayFadeOut) {
- SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
- const char *text = splayer->getString(_textOverlayID);
- debug(5, "Rebel2: Text overlay frame %d/%d-%d textID=0x%x text='%s'",
- curFrame, _textOverlayFadeIn, _textOverlayFadeOut, _textOverlayID,
- text ? text : "(null)");
- if (text) {
- // Progressive reveal: display length = currentFrame + 10 - fadeInFrame
- // Capped at 0xBE (190) visible chars per original
- int displayLen = curFrame + 10 - _textOverlayFadeIn;
- if (displayLen > 0xBE)
- displayLen = 0xBE;
- if (displayLen < 0)
- displayLen = 0;
-
- // Font system â same as menu text (FUN_00434d10 / FUN_00433da0)
- // ^fNN = font switch, ^cNNN = color code
- NutRenderer *fonts[3] = {
- _smush_talkfontNut,
- _smush_smalfontNut,
- _smush_titlefontNut
- };
- NutRenderer *defaultFont = fonts[0] ? fonts[0] : _smush_smalfontNut;
- if (!defaultFont)
- goto textOverlayDone;
-
- {
- Common::Rect clipRect(0, 0, width, height);
-
- // Format code parser (same as drawMenuItems)
- auto parseFormat = [&](const char *&s, NutRenderer *&curFont, int &curColor) {
- if (*s != '^')
- return false;
- const char *p = s + 1;
- if (*p == '^') { s = p; return false; } // ^^ = literal ^
- if (*p == 'f') {
- p++;
- int idx = 0;
- while (*p >= '0' && *p <= '9') { idx = idx * 10 + (*p - '0'); p++; }
- s = p;
- curFont = (idx >= 0 && idx < 3 && fonts[idx]) ? fonts[idx] : defaultFont;
- return true;
- }
- if (*p == 'c') {
- p++;
- int col = 0;
- while (*p >= '0' && *p <= '9') { col = col * 10 + (*p - '0'); p++; }
- s = p;
- curColor = col;
- return true;
- }
- return false;
- };
-
- // FUN_004341a0 splits on \n and renders each line centered independently.
- // The TRS parser joins multi-line strings with spaces (stripping \n//),
- // so " ^f" marks where a line break was in the original TRS file.
- // We split the text into lines first, then render each centered.
-
- // Split text into lines at " ^f" boundaries
- Common::Array<Common::String> lines;
- {
- Common::String cur;
- const char *s = text;
- while (*s) {
- // Check for line break: space followed by ^f (font switch)
- if (*s == ' ' && s[1] == '^' && s[2] == 'f') {
- lines.push_back(cur);
- cur.clear();
- s++; // skip the space, keep the ^f for the next line
- continue;
- }
- cur += *s++;
- }
- if (!cur.empty())
- lines.push_back(cur);
- }
-
- int drawY = _textOverlayY;
- int visCount = 0;
-
- for (uint lineIdx = 0; lineIdx < lines.size() && visCount < displayLen; lineIdx++) {
- const char *lineStr = lines[lineIdx].c_str();
- const char *lineEnd = lineStr + lines[lineIdx].size();
-
- // Measure this line's width (only visible chars up to displayLen)
- int lineWidth = 0;
- int lineVisCount = 0;
- NutRenderer *lineFont = defaultFont;
- {
- const char *s = lineStr;
- NutRenderer *mFont = defaultFont;
- int mColor = 1;
- while (s < lineEnd && (visCount + lineVisCount) < displayLen) {
- if (parseFormat(s, mFont, mColor))
- continue;
- lineFont = mFont; // track font for line height
- byte c = (byte)*s++;
- if (c >= 'a' && c <= 'z')
- c = c - 'a' + 'A';
- if (mFont && c < mFont->getNumChars())
- lineWidth += mFont->getCharWidth(c);
- lineVisCount++;
- }
- }
-
- // Draw this line centered at textX
- int drawX = _textOverlayX - lineWidth / 2;
- int lineCharsDrawn = 0;
- {
- const char *s = lineStr;
- NutRenderer *curFont = defaultFont;
- int curColor = 1;
- while (s < lineEnd && (visCount + lineCharsDrawn) < displayLen) {
- if (parseFormat(s, curFont, curColor))
- continue;
- byte c = (byte)*s++;
- if (c >= 'a' && c <= 'z')
- c = c - 'a' + 'A';
- if (!curFont || c >= curFont->getNumChars()) {
- lineCharsDrawn++;
- continue;
- }
- int charW = curFont->getCharWidth(c);
- if (drawX >= 0 && drawY >= 0 && charW > 0) {
- curFont->drawCharV7(renderBitmap, clipRect, drawX, drawY,
- pitch, curColor, kStyleAlignLeft, c, false, false);
- }
- drawX += charW;
- lineCharsDrawn++;
- }
- }
- visCount += lineCharsDrawn;
-
- // Advance to next line â use the line's font height for spacing
- int lineHeight = lineFont->getCharHeight('A') + 2;
- drawY += lineHeight;
- }
- }
- }
- }
- textOverlayDone:
+ // Chapter title text overlay (FUN_004171c5)
+ if (_textOverlayActive)
+ renderTextOverlay(renderBitmap, pitch, width, height, curFrame);
// Skip all HUD rendering during intro - subtitles are rendered via opcode 9
return;
@@ -2162,6 +2021,140 @@ void InsaneRebel2::updateDamageEffect(byte *renderBitmap, int pitch, int width,
// ======================= Rendering Helper Functions =======================
// These are extracted from procPostRendering for better readability
+void InsaneRebel2::renderTextOverlay(byte *renderBitmap, int pitch, int width, int height, int curFrame) {
+ // Emulates FUN_004171c5 text overlay: progressive chapter title during [fadeIn, fadeOut)
+ if (curFrame < _textOverlayFadeIn || curFrame >= _textOverlayFadeOut)
+ return;
+
+ SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
+ const char *text = splayer->getString(_textOverlayID);
+ debug(5, "Rebel2: Text overlay frame %d/%d-%d textID=0x%x text='%s'",
+ curFrame, _textOverlayFadeIn, _textOverlayFadeOut, _textOverlayID,
+ text ? text : "(null)");
+ if (!text)
+ return;
+
+ // Progressive reveal: displayLen = currentFrame + 10 - fadeInFrame, capped at 0xBE (190)
+ int displayLen = curFrame + 10 - _textOverlayFadeIn;
+ if (displayLen > 0xBE)
+ displayLen = 0xBE;
+ if (displayLen < 0)
+ return;
+
+ // Font system â ^fNN = font switch, ^cNNN = color code
+ NutRenderer *fonts[3] = { _smush_talkfontNut, _smush_smalfontNut, _smush_titlefontNut };
+ NutRenderer *defaultFont = fonts[0] ? fonts[0] : _smush_smalfontNut;
+ if (!defaultFont)
+ return;
+
+ Common::Rect clipRect(0, 0, width, height);
+
+ // Format code parser (same as drawMenuItems / FUN_00434d10)
+ auto parseFormat = [&](const char *&s, NutRenderer *&curFont, int &curColor) {
+ if (*s != '^')
+ return false;
+ const char *p = s + 1;
+ if (*p == '^') { s = p; return false; }
+ if (*p == 'f') {
+ p++;
+ int idx = 0;
+ while (*p >= '0' && *p <= '9') { idx = idx * 10 + (*p - '0'); p++; }
+ s = p;
+ curFont = (idx >= 0 && idx < 3 && fonts[idx]) ? fonts[idx] : defaultFont;
+ return true;
+ }
+ if (*p == 'c') {
+ p++;
+ int col = 0;
+ while (*p >= '0' && *p <= '9') { col = col * 10 + (*p - '0'); p++; }
+ s = p;
+ curColor = col;
+ return true;
+ }
+ return false;
+ };
+
+ // The TRS parser joins multi-line strings with spaces (stripping \n//),
+ // so " ^f" marks where a line break was in the original TRS file.
+ // Split into lines, then render each centered at textX (FUN_004341a0).
+ Common::Array<Common::String> lines;
+ {
+ Common::String cur;
+ const char *s = text;
+ while (*s) {
+ if (*s == ' ' && s[1] == '^' && s[2] == 'f') {
+ lines.push_back(cur);
+ cur.clear();
+ s++; // skip the space, keep ^f for the next line
+ continue;
+ }
+ cur += *s++;
+ }
+ if (!cur.empty())
+ lines.push_back(cur);
+ }
+
+ int drawY = _textOverlayY;
+ int visCount = 0;
+
+ for (uint lineIdx = 0; lineIdx < lines.size() && visCount < displayLen; lineIdx++) {
+ const char *lineStr = lines[lineIdx].c_str();
+ const char *lineEnd = lineStr + lines[lineIdx].size();
+
+ // Measure visible chars up to displayLen
+ int lineWidth = 0;
+ int lineVisCount = 0;
+ NutRenderer *lineFont = defaultFont;
+ {
+ const char *s = lineStr;
+ NutRenderer *mFont = defaultFont;
+ int mColor = 1;
+ while (s < lineEnd && (visCount + lineVisCount) < displayLen) {
+ if (parseFormat(s, mFont, mColor))
+ continue;
+ lineFont = mFont;
+ byte c = (byte)*s++;
+ if (c >= 'a' && c <= 'z')
+ c = c - 'a' + 'A';
+ if (mFont && c < mFont->getNumChars())
+ lineWidth += mFont->getCharWidth(c);
+ lineVisCount++;
+ }
+ }
+
+ // Draw line centered at textX
+ int drawX = _textOverlayX - lineWidth / 2;
+ int lineCharsDrawn = 0;
+ {
+ const char *s = lineStr;
+ NutRenderer *curFont = defaultFont;
+ int curColor = 1;
+ while (s < lineEnd && (visCount + lineCharsDrawn) < displayLen) {
+ if (parseFormat(s, curFont, curColor))
+ continue;
+ byte c = (byte)*s++;
+ if (c >= 'a' && c <= 'z')
+ c = c - 'a' + 'A';
+ if (!curFont || c >= curFont->getNumChars()) {
+ lineCharsDrawn++;
+ continue;
+ }
+ int charW = curFont->getCharWidth(c);
+ if (drawX >= 0 && drawY >= 0 && charW > 0) {
+ curFont->drawCharV7(renderBitmap, clipRect, drawX, drawY,
+ pitch, curColor, kStyleAlignLeft, c, false, false);
+ }
+ drawX += charW;
+ lineCharsDrawn++;
+ }
+ }
+ visCount += lineCharsDrawn;
+
+ int lineHeight = lineFont->getCharHeight('A') + 2;
+ drawY += lineHeight;
+ }
+}
+
void InsaneRebel2::renderStatusBarBackground(byte *renderBitmap, int pitch, int width, int height,
int videoWidth, int videoHeight, int statusBarY) {
// Fill status bar background (FUN_004288c0 equivalent)
Commit: 528969fe7900e6b232576c027f1c933bdb8a8b7e
https://github.com/scummvm/scummvm/commit/528969fe7900e6b232576c027f1c933bdb8a8b7e
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:14+02:00
Commit Message:
SCUMM: RA2: Add basic unlocking and password handling
Changed paths:
engines/scumm/detection.h
engines/scumm/detection_tables.h
engines/scumm/insane/insane_rebel.cpp
engines/scumm/insane/insane_rebel.h
engines/scumm/insane/insane_rebel_menu.cpp
engines/scumm/insane/insane_rebel_render.cpp
engines/scumm/metaengine.cpp
diff --git a/engines/scumm/detection.h b/engines/scumm/detection.h
index 5daff20041f..f0bf20c91ac 100644
--- a/engines/scumm/detection.h
+++ b/engines/scumm/detection.h
@@ -39,6 +39,7 @@ namespace Scumm {
#define GAMEOPTION_USE_REMASTERED_AUDIO GUIO_GAMEOPTIONS8
#define GAMEOPTION_TTS GUIO_GAMEOPTIONS9
#define GAMEOPTION_REBEL2_HIRES GUIO_GAMEOPTIONS10
+#define GAMEOPTION_REBEL2_UNLOCK_ALL GUIO_GAMEOPTIONS11
/**
* Descriptor of a specific SCUMM game. Used internally to store
diff --git a/engines/scumm/detection_tables.h b/engines/scumm/detection_tables.h
index 34835f71af0..33b26872482 100644
--- a/engines/scumm/detection_tables.h
+++ b/engines/scumm/detection_tables.h
@@ -228,8 +228,8 @@ static const GameSettings gameVariantsTable[] = {
{"ft", "Remastered", 0, GID_FT, 7, 0, MDT_NONE, GF_DOUBLEFINE_PAK, UNK, GUIO5(GUIO_NOMIDI, GAMEOPTION_ENHANCEMENTS, GAMEOPTION_ORIGINALGUI, GAMEOPTION_LOWLATENCYAUDIO, GAMEOPTION_TTS)},
{"ft", "Demo", 0, GID_FT, 7, 0, MDT_NONE, GF_DEMO, UNK, GUIO4(GUIO_NOMIDI, GAMEOPTION_ORIGINALGUI, GAMEOPTION_LOWLATENCYAUDIO, GAMEOPTION_TTS)},
- {"rebel2", "", 0, GID_REBEL2, 7, 0, MDT_NONE, 0, Common::kPlatformDOS, GUIO2(GUIO_NOMIDI, GAMEOPTION_REBEL2_HIRES)},
- {"rebel2", "Demo", 0, GID_REBEL2, 7, 0, MDT_NONE, GF_DEMO, Common::kPlatformDOS, GUIO2(GUIO_NOMIDI, GAMEOPTION_REBEL2_HIRES)},
+ {"rebel2", "", 0, GID_REBEL2, 7, 0, MDT_NONE, 0, Common::kPlatformDOS, GUIO3(GUIO_NOMIDI, GAMEOPTION_REBEL2_HIRES, GAMEOPTION_REBEL2_UNLOCK_ALL)},
+ {"rebel2", "Demo", 0, GID_REBEL2, 7, 0, MDT_NONE, GF_DEMO, Common::kPlatformDOS, GUIO3(GUIO_NOMIDI, GAMEOPTION_REBEL2_HIRES, GAMEOPTION_REBEL2_UNLOCK_ALL)},
{"dig", "", 0, GID_DIG, 7, 0, MDT_NONE, 0, UNK, GUIO5(GUIO_NOMIDI, GAMEOPTION_ENHANCEMENTS, GAMEOPTION_ORIGINALGUI, GAMEOPTION_LOWLATENCYAUDIO, GAMEOPTION_TTS)},
{"dig", "Demo", 0, GID_DIG, 7, 0, MDT_NONE, GF_DEMO, UNK, GUIO4(GUIO_NOMIDI, GAMEOPTION_ORIGINALGUI, GAMEOPTION_LOWLATENCYAUDIO, GAMEOPTION_TTS)},
diff --git a/engines/scumm/insane/insane_rebel.cpp b/engines/scumm/insane/insane_rebel.cpp
index 0c57a5ab083..cc0a28aac3d 100644
--- a/engines/scumm/insane/insane_rebel.cpp
+++ b/engines/scumm/insane/insane_rebel.cpp
@@ -41,6 +41,7 @@
#include "scumm/insane/insane_rebel.h"
+#include "common/config-manager.h"
#include "audio/audiostream.h"
#include "audio/decoders/raw.h"
@@ -390,6 +391,7 @@ InsaneRebel2::InsaneRebel2(ScummEngine_v7 *scumm) {
_menuInactivityTimer = 0;
_lastMenuVariant = -1; // No previous menu video
_menuRepeatDelay = 0;
+ _menuSelectionConfirmed = false;
for (i = 0; i < 16; i++) {
_levelUnlocked[i] = (i == 0); // Only level 1 unlocked initially
}
@@ -404,7 +406,7 @@ InsaneRebel2::InsaneRebel2(ScummEngine_v7 *scumm) {
// Debug flag to unlock all chapters for testing
// Based on original debug mode (DAT_0047ab34 == 'd') from FUN_00415CF8
// Set to true to bypass normal unlock progression
- _debugUnlockAll = true; // TODO: Set to false for release, or read from ScummVM config
+ _debugUnlockAll = ConfMan.getBool("rebel2_unlock_all");
for (i = 0; i < 16; i++) {
// If debug unlock is enabled, unlock all chapters
@@ -515,6 +517,7 @@ bool InsaneRebel2::notifyEvent(const Common::Event &event) {
// In menu mode: Select quit option and confirm selection
// This emulates the assembly behavior from FUN_0041f5ae
_menuSelection = _menuItemCount - 1; // Select last item (quit/back)
+ _menuSelectionConfirmed = true;
debug("Rebel2: ESC pressed in menu - selecting quit (item %d)", _menuSelection);
} else {
debug("Rebel2: ESC pressed - skipping video");
@@ -762,7 +765,7 @@ void InsaneRebel2::renderScoreHUD(byte *renderBitmap, int pitch, int width, int
// Original: FUN_00411980 (load) / FUN_00411A5D (save)
static const uint32 kPilotSaveMagic = MKTAG('R', 'A', '2', 'P');
-static const uint16 kPilotSaveVersion = 1;
+static const uint16 kPilotSaveVersion = 2;
bool InsaneRebel2::loadPilots() {
_numPilots = 0;
@@ -779,7 +782,7 @@ bool InsaneRebel2::loadPilots() {
break;
}
- /* uint16 version = */ sf->readUint16LE();
+ uint16 version = sf->readUint16LE();
PilotData &p = _pilots[i];
sf->read(p.name, kMaxPilotNameLen + 1);
@@ -791,6 +794,10 @@ bool InsaneRebel2::loadPilots() {
for (int j = 0; j < kNumLevels; j++)
p.damage[j] = sf->readSint32LE();
p.difficulty = sf->readSint16LE();
+ if (version >= 2) {
+ for (int j = 0; j < kNumLevels; j++)
+ p.rating[j] = sf->readSint16LE();
+ }
delete sf;
_numPilots = i + 1;
@@ -824,6 +831,8 @@ bool InsaneRebel2::savePilots() {
for (int j = 0; j < kNumLevels; j++)
sf->writeSint32LE(p.damage[j]);
sf->writeSint16LE(p.difficulty);
+ for (int j = 0; j < kNumLevels; j++)
+ sf->writeSint16LE(p.rating[j]);
sf->finalize();
delete sf;
diff --git a/engines/scumm/insane/insane_rebel.h b/engines/scumm/insane/insane_rebel.h
index 807184f52fe..606a95bd3fb 100644
--- a/engines/scumm/insane/insane_rebel.h
+++ b/engines/scumm/insane/insane_rebel.h
@@ -87,6 +87,7 @@ public:
int _menuInactivityTimer; // Timeout counter (300 frames = ~10 sec)
int _lastMenuVariant; // Last random menu video shown (DAT_00482400)
int _menuRepeatDelay; // Delay for key repeat (DAT_00459ce0)
+ bool _menuSelectionConfirmed; // True only when user explicitly confirmed a selection
bool _levelUnlocked[16]; // Which levels are available (progress flags)
// Run the main menu loop - returns when game should start or quit
@@ -156,6 +157,12 @@ public:
// Draw score/info display at bottom of chapter select - emulates FUN_00434cb0 calls
void drawChapterInfoLine(byte *renderBitmap, int pitch, int width, int height);
+ // Rating-to-medal string conversion (FUN_0042001f)
+ Common::String getRankString(int rating);
+
+ // Password table lookup (FUN_0041BCE0)
+ Common::String getChapterPassword(int level, int difficulty);
+
// ================= Pilot Data System (FUN_00411B9A / FUN_00411980 / FUN_00411A5D) ===========
// Original: 10 pilot slots à 0x118 (280) bytes at DAT_004568A8
// Stored via SaveFileManager in a custom save file
@@ -169,10 +176,12 @@ public:
int32 score[kNumLevels]; // +0x2C: Per-level score (0 = default, 0xFF = unplayed)
int32 lives[kNumLevels]; // +0x6C: Per-level lives (4 = default, 0xFF = unplayed)
int32 damage[kNumLevels]; // +0xAC: Per-level damage (0xFF = unplayed)
+ int16 rating[kNumLevels]; // +0xEC: Per-level difficulty rating (0-50)
int16 difficulty; // +0x10C: Difficulty setting (0-5)
void init() {
memset(name, 0, sizeof(name));
+ memset(rating, 0, sizeof(rating));
difficulty = 2; // Default to 3rd option
score[0] = 0;
lives[0] = 4;
diff --git a/engines/scumm/insane/insane_rebel_menu.cpp b/engines/scumm/insane/insane_rebel_menu.cpp
index 4ccbc90adca..415d209bf31 100644
--- a/engines/scumm/insane/insane_rebel_menu.cpp
+++ b/engines/scumm/insane/insane_rebel_menu.cpp
@@ -42,6 +42,7 @@ void InsaneRebel2::resetMenu() {
_menuSelection = 0;
_menuInactivityTimer = 0;
_menuRepeatDelay = 0;
+ _menuSelectionConfirmed = false;
}
// Unlock all chapters for testing
@@ -49,7 +50,6 @@ void InsaneRebel2::resetMenu() {
// where DAT_0047ab34 == 'd' enables level unlock via special codes
void InsaneRebel2::unlockAllChapters() {
debug("Rebel2: Unlocking all chapters for testing");
- _debugUnlockAll = true;
for (int i = 0; i < 16; i++) {
_chapterUnlocked[i] = true;
_levelUnlocked[i] = true;
@@ -154,26 +154,8 @@ int InsaneRebel2::processMenuInput() {
case Common::EVENT_LBUTTONDOWN:
_menuInactivityTimer = 0;
- {
- // Get mouse position from the event
- int mouseY = event.mouse.y;
-
- debug("Menu: Left click at Y=%d", mouseY);
-
- // Check which item was clicked
- // From FUN_0041f5ae mouse mode: selection = (mouseY + 100 - baseY) / 10
- // But we use a simpler direct hit-test approach
- for (int i = 0; i < _menuItemCount; i++) {
- int itemY = baseY + i * itemSpacing;
- // Hit area: itemY - 2 to itemY + 8 (10 pixel height)
- if (mouseY >= itemY - 2 && mouseY < itemY + 8) {
- _menuSelection = i;
- result = i;
- debug("Menu: Item %d clicked (itemY=%d)", i, itemY);
- break;
- }
- }
- }
+ // TODO: Re-enable click-to-confirm (currently disabled for easier testing)
+ // Original behavior: clicking on a menu item both highlights and confirms it.
break;
case Common::EVENT_MOUSEMOVE:
@@ -701,15 +683,15 @@ int InsaneRebel2::runMainMenu() {
return 0;
}
- // If video ended naturally (not by selection), loop back
- if (!_vm->_smushVideoShouldFinish) {
- // Video ended without selection (reached end or ESC during video)
- // Continue looping menu videos
+ // Only process selection if user explicitly confirmed (ENTER/ESC),
+ // not when video ended naturally (EOF sets _smushVideoShouldFinish too)
+ if (!_menuSelectionConfirmed) {
continue;
}
- // Clear the flag
+ // Clear the flags
_vm->_smushVideoShouldFinish = false;
+ _menuSelectionConfirmed = false;
// A selection was made - process it
debug("Rebel2: Menu video ended with selection=%d", _menuSelection);
@@ -860,12 +842,13 @@ int InsaneRebel2::runChapterSelect() {
return kChapterSelectQuit;
}
- // If video ended without selection, continue looping
- if (!_vm->_smushVideoShouldFinish) {
+ // Only process selection if user explicitly confirmed
+ if (!_menuSelectionConfirmed) {
continue;
}
_vm->_smushVideoShouldFinish = false;
+ _menuSelectionConfirmed = false;
debug("Rebel2: Chapter selection made: %d", _chapterSelection);
@@ -878,12 +861,36 @@ int InsaneRebel2::runChapterSelect() {
}
if (_chapterSelection >= 0 && _chapterSelection < 16) {
- // Chapter selected - start it regardless of unlock state.
- // TODO: locked chapters should require password (FUN_00415CF8 lines 239-257)
- _selectedChapter = _chapterSelection;
- debug("Rebel2: Chapter %d selected", _selectedChapter + 1);
- _menuInputActive = false;
- return kChapterSelectPlay;
+ if (_chapterUnlocked[_chapterSelection]) {
+ // Unlocked chapter â play it
+ _selectedChapter = _chapterSelection;
+ debug("Rebel2: Chapter %d selected (unlocked)", _selectedChapter + 1);
+ _menuInputActive = false;
+ return kChapterSelectPlay;
+ }
+
+ // Locked chapter â validate password (FUN_00415CF8 lines 239-257)
+ if (_activePilot >= 0 && _activePilot < _numPilots &&
+ _pilots[_activePilot].difficulty < 6 && _chapterSelection > 0) {
+ Common::String expected = getChapterPassword(
+ _chapterSelection, _pilots[_activePilot].difficulty);
+ if (expected.empty() || _passwordInput.equalsIgnoreCase(expected)) {
+ // Password accepted â unlock chapter
+ PilotData &pilot = _pilots[_activePilot];
+ pilot.score[_chapterSelection] = 0;
+ pilot.damage[_chapterSelection] = 0;
+ pilot.lives[_chapterSelection] = 3;
+ pilot.rating[_chapterSelection] = 0;
+ savePilots();
+ _chapterUnlocked[_chapterSelection] = true;
+ _passwordInput.clear();
+ debug("Rebel2: Chapter %d unlocked via password", _chapterSelection + 1);
+ continue; // Re-render with updated unlock state
+ }
+ }
+ // Wrong password or no password entered
+ _passwordInput.clear();
+ debug("Rebel2: Password rejected for chapter %d", _chapterSelection + 1);
}
}
@@ -911,6 +918,7 @@ int InsaneRebel2::processChapterSelectInput() {
if (_chapterSelection < 0) {
_chapterSelection = _chapterItemCount - 1;
}
+ _passwordInput.clear();
// Update preview offset (FUN_00425170: Y = selected * -50 + 75)
_previewOffsetY = _chapterSelection * -50 + 75;
debug("ChapterSelect: Selection changed to %d (UP) offsetY=%d", _chapterSelection, _previewOffsetY);
@@ -922,6 +930,7 @@ int InsaneRebel2::processChapterSelectInput() {
if (_chapterSelection >= _chapterItemCount) {
_chapterSelection = 0;
}
+ _passwordInput.clear();
// Update preview offset (FUN_00425170: Y = selected * -50 + 75)
_previewOffsetY = _chapterSelection * -50 + 75;
debug("ChapterSelect: Selection changed to %d (DOWN) offsetY=%d", _chapterSelection, _previewOffsetY);
@@ -962,11 +971,8 @@ int InsaneRebel2::processChapterSelectInput() {
break;
case Common::EVENT_LBUTTONDOWN:
- // Click confirms the current selection (original: DAT_0047a7e4 & 1)
- if (_chapterSelection >= 0 && _chapterSelection < _chapterItemCount) {
- result = _chapterSelection;
- debug("ChapterSelect: Item %d confirmed (CLICK)", _chapterSelection);
- }
+ // TODO: Re-enable click-to-confirm (currently disabled for easier testing)
+ // Original behavior: any click confirms current selection (DAT_0047a7e4 & 1)
break;
case Common::EVENT_MOUSEMOVE:
@@ -1187,9 +1193,36 @@ void InsaneRebel2::drawPreviewThumbnail(byte *renderBitmap, int pitch, int width
}
}
+// Rating-to-medal string conversion - emulates FUN_0042001f
+// Converts a rating value (0-50) to a string of medal characters for TALKFONT.NUT:
+// Every 9 points â big medal (DAT_00482550)
+// Every 3 points â medium medal (DAT_00482558)
+// Every 1 point â small medal (DAT_00482560)
+Common::String InsaneRebel2::getRankString(int rating) {
+ if (rating > 50)
+ rating = 50;
+ Common::String result;
+ // TODO: Medal char bytes are placeholders â verify against actual NUT font glyphs
+ // from DAT_00482550/58/60 in the game binary
+ while (rating >= 9) { result += '\x83'; rating -= 9; } // big medal
+ while (rating >= 3) { result += '\x82'; rating -= 3; } // medium medal
+ while (rating >= 1) { result += '\x81'; rating -= 1; } // small medal
+ return result;
+}
+
+// Password table lookup - emulates FUN_0041BCE0
+// Original: 90-entry table at DAT_00481af0, 20 bytes each, XOR 0xAA
+// Index formula: (difficulty + (level * 3 - 3) * 2) * 20, level is 1-based
+Common::String InsaneRebel2::getChapterPassword(int level, int difficulty) {
+ // TODO: Extract actual password table from game binary (FUN_0041BCE0)
+ // Table: 90 entries x 20 bytes at exe offset 0x481AF0, XOR 0xAA
+ // For now, return empty string (no password required â debug mode)
+ return "";
+}
+
// Draw score/info line at bottom of chapter select - emulates FUN_00434cb0 calls
// For unlocked chapters: score display using TRS 80 at (25, 190)
-// For locked chapters: password prompt at (30, 190)
+// For locked chapters: password prompt using TRS 81 at (30, 190)
void InsaneRebel2::drawChapterInfoLine(byte *renderBitmap, int pitch, int width, int height) {
if (_chapterSelection < 0 || _chapterSelection >= 16)
return;
@@ -1198,34 +1231,113 @@ void InsaneRebel2::drawChapterInfoLine(byte *renderBitmap, int pitch, int width,
if (!splayer)
return;
- NutRenderer *font = _smush_smalfontNut;
- if (!font)
+ // Font system â same as drawMenuItems()
+ NutRenderer *fonts[3] = {
+ _smush_talkfontNut,
+ _smush_smalfontNut,
+ _smush_titlefontNut
+ };
+ NutRenderer *defaultFont = fonts[0] ? fonts[0] : _smush_smalfontNut;
+ if (!defaultFont)
return;
Common::Rect clipRect(0, 0, _vm->_screenWidth, _vm->_screenHeight);
int actualPitch = _vm->_screenWidth;
+ // Format code parser â same as drawMenuItems()
+ auto parseFormatCode = [&](const char *&str, int &outColor) -> int {
+ if (*str != '^')
+ return -1;
+ const char *p = str + 1;
+ if (*p == '^') { str = p; return -1; }
+ if (*p == 'f') {
+ p++;
+ int fontIdx = 0;
+ while (*p >= '0' && *p <= '9') { fontIdx = fontIdx * 10 + (*p - '0'); p++; }
+ str = p;
+ return (fontIdx >= 0 && fontIdx < 3) ? fontIdx : 0;
+ }
+ if (*p == 'c') {
+ p++;
+ int color = 0;
+ while (*p >= '0' && *p <= '9') { color = color * 10 + (*p - '0'); p++; }
+ str = p;
+ outColor = color;
+ return -2;
+ }
+ return -1;
+ };
+
+ // String rendering with format codes
+ auto drawString = [&](const char *str, int x, int y) {
+ NutRenderer *curFont = defaultFont;
+ int curColor = 1;
+
+ while (*str) {
+ int fontChange = parseFormatCode(str, curColor);
+ if (fontChange >= 0) {
+ curFont = fonts[fontChange] ? fonts[fontChange] : defaultFont;
+ continue;
+ }
+ if (fontChange == -2)
+ continue;
+
+ byte c = (byte)*str++;
+ if (c >= 'a' && c <= 'z')
+ c = c - 'a' + 'A';
+ if (!curFont)
+ continue;
+ int numChars = curFont->getNumChars();
+ if (c >= numChars)
+ continue;
+ int charW = curFont->getCharWidth(c);
+ if (x >= 0 && y >= 0 && charW > 0) {
+ curFont->drawCharV7(renderBitmap, clipRect, x, y, actualPitch, curColor,
+ kStyleAlignLeft, c, false, false);
+ }
+ x += charW;
+ }
+ };
+
if (_chapterUnlocked[_chapterSelection]) {
// Unlocked: show score info using TRS 80 at X=25 (0x19), Y=190 (0xbe)
- const char *scoreStr = splayer->getString(80);
- if (!scoreStr || !scoreStr[0])
+ // TRS 80 = "^f01^c248Pilots: %hd Score: %ld Rank: ^f00%s"
+ const char *fmtStr = splayer->getString(80);
+ if (!fmtStr || !fmtStr[0])
return;
- int curX = 25;
- int numChars = font->getNumChars();
- for (const char *c = scoreStr; *c; c++) {
- byte ch = (byte)*c;
- if (ch >= 'a' && ch <= 'z')
- ch = ch - 'a' + 'A';
- if (ch < numChars) {
- int charW = font->getCharWidth(ch);
- if (curX >= 0 && curX + charW <= width && 190 < height) {
- font->drawCharV7(renderBitmap, clipRect, curX, 190, actualPitch, 1,
- kStyleAlignLeft, ch, false, false);
- }
- curX += charW;
- }
+ // Get pilot data for this chapter
+ int32 pilotLives = 0;
+ int32 pilotScore = 0;
+ int16 pilotRating = 0;
+ if (_activePilot >= 0 && _activePilot < _numPilots) {
+ pilotLives = _pilots[_activePilot].lives[_chapterSelection];
+ pilotScore = _pilots[_activePilot].score[_chapterSelection];
+ pilotRating = _pilots[_activePilot].rating[_chapterSelection];
}
+ Common::String rankStr = getRankString(pilotRating);
+
+ // sprintf substitution: %hd â lives, %ld â score, %s â rank
+ Common::String displayStr = Common::String::format(fmtStr,
+ (short)pilotLives, (long)pilotScore, rankStr.c_str());
+
+ drawString(displayStr.c_str(), 25, 190);
+ } else {
+ // Locked: show password prompt using TRS 81 at X=30 (0x1e), Y=190 (0xbe)
+ // Format: "%s ^c005%s%c" (TRS 81 + green password input + blinking cursor)
+ const char *lockStr = splayer->getString(81);
+ if (!lockStr || !lockStr[0])
+ lockStr = "^f01^c248UNREGISTERED - PASSCODE REQUIRED";
+
+ // Blinking cursor: alternate '_' and ' ' (original uses bit 1 of frame counter)
+ static int cursorCounter = 0;
+ cursorCounter++;
+ char cursor = ((cursorCounter / 8) & 1) ? '_' : ' ';
+
+ Common::String displayStr = Common::String::format("%s ^c005%s%c",
+ lockStr, _passwordInput.c_str(), cursor);
+
+ drawString(displayStr.c_str(), 30, 190);
}
}
@@ -1323,10 +1435,11 @@ int InsaneRebel2::runLevelSelect() {
return kLevelSelectQuit;
}
- if (!_vm->_smushVideoShouldFinish)
+ if (!_menuSelectionConfirmed)
continue;
_vm->_smushVideoShouldFinish = false;
+ _menuSelectionConfirmed = false;
// --- Difficulty submenu completed ---
if (_pilotMenuMode == kPilotModeDifficulty) {
@@ -1445,6 +1558,7 @@ int InsaneRebel2::processLevelSelectInput() {
event.kbd.keycode == Common::KEYCODE_KP_ENTER) {
// Confirm name â signal back to runLevelSelect()
if (_pilotNameInput.size() > 0) {
+ _menuSelectionConfirmed = true;
_vm->_smushVideoShouldFinish = true;
debug("PilotName: confirmed '%s'", _pilotNameInput.c_str());
}
@@ -1491,7 +1605,6 @@ int InsaneRebel2::processLevelSelectInput() {
// Mouse hit Y positions â must match drawMenuItems() formula
const int baseY = itemCount * -5 + 0x68;
- const int itemHeight = 10;
while (!_menuEventQueue.empty()) {
Common::Event event = _menuEventQueue.pop();
@@ -1533,17 +1646,7 @@ int InsaneRebel2::processLevelSelectInput() {
break;
case Common::EVENT_LBUTTONDOWN:
- {
- int mouseY = event.mouse.y;
- for (int i = 0; i < itemCount; i++) {
- int itemY = baseY + i * itemHeight;
- if (mouseY >= itemY - 4 && mouseY < itemY + 6) {
- selection = i;
- result = i;
- break;
- }
- }
- }
+ // TODO: Re-enable click-to-confirm (currently disabled for easier testing)
break;
case Common::EVENT_MOUSEMOVE:
diff --git a/engines/scumm/insane/insane_rebel_render.cpp b/engines/scumm/insane/insane_rebel_render.cpp
index 01ab72b60e5..fe76fe6e137 100644
--- a/engines/scumm/insane/insane_rebel_render.cpp
+++ b/engines/scumm/insane/insane_rebel_render.cpp
@@ -1663,6 +1663,7 @@ void InsaneRebel2::procPostRendering(byte *renderBitmap, int32 codecparam, int32
// If a selection was confirmed, signal video to stop
if (selection >= 0) {
debug("Rebel2: Pilot selection confirmed: %d", selection);
+ _menuSelectionConfirmed = true;
_vm->_smushVideoShouldFinish = true;
}
@@ -1692,6 +1693,7 @@ void InsaneRebel2::procPostRendering(byte *renderBitmap, int32 codecparam, int32
// If a selection was confirmed, signal video to stop
if (selection >= 0) {
debug("Rebel2: Chapter selection confirmed: %d", selection);
+ _menuSelectionConfirmed = true;
_vm->_smushVideoShouldFinish = true;
}
@@ -1736,6 +1738,7 @@ void InsaneRebel2::procPostRendering(byte *renderBitmap, int32 codecparam, int32
// If a selection was confirmed, signal video to stop
if (selection >= 0) {
debug("Rebel2: Menu selection confirmed: %d", selection);
+ _menuSelectionConfirmed = true;
_vm->_smushVideoShouldFinish = true;
}
diff --git a/engines/scumm/metaengine.cpp b/engines/scumm/metaengine.cpp
index 4fe0f9a1511..0d9479e420b 100644
--- a/engines/scumm/metaengine.cpp
+++ b/engines/scumm/metaengine.cpp
@@ -887,6 +887,15 @@ static const ExtraGuiOption enableRebel2HiRes = {
0
};
+static const ExtraGuiOption enableRebel2UnlockAll = {
+ _s("Unlock all levels"),
+ _s("All levels will be available without requiring passwords."),
+ "rebel2_unlock_all",
+ false,
+ 0,
+ 0
+};
+
const ExtraGuiOptions ScummMetaEngine::getExtraGuiOptions(const Common::String &target) const {
ExtraGuiOptions options;
// Query the GUI options
@@ -931,6 +940,9 @@ const ExtraGuiOptions ScummMetaEngine::getExtraGuiOptions(const Common::String &
if (target.empty() || guiOptions.contains(GAMEOPTION_REBEL2_HIRES)) {
options.push_back(enableRebel2HiRes);
}
+ if (target.empty() || guiOptions.contains(GAMEOPTION_REBEL2_UNLOCK_ALL)) {
+ options.push_back(enableRebel2UnlockAll);
+ }
if (target.empty() || gameid == "comi") {
options.push_back(comiObjectLabelsOption);
Commit: 52fcf017dcfb54abb24d346e347a368f84288e0c
https://github.com/scummvm/scummvm/commit/52fcf017dcfb54abb24d346e347a368f84288e0c
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:14+02:00
Commit Message:
SCUMM: RA2: Improve password handling
Changed paths:
engines/scumm/insane/insane_rebel_iact.cpp
engines/scumm/insane/insane_rebel_menu.cpp
engines/scumm/smush/smush_player.cpp
engines/scumm/smush/smush_player.h
engines/scumm/smush/smush_player_ra2.cpp
diff --git a/engines/scumm/insane/insane_rebel_iact.cpp b/engines/scumm/insane/insane_rebel_iact.cpp
index 7a17b021d63..f86f9ff630e 100644
--- a/engines/scumm/insane/insane_rebel_iact.cpp
+++ b/engines/scumm/insane/insane_rebel_iact.cpp
@@ -70,23 +70,18 @@ void InsaneRebel2::procPreRendering(byte *renderBitmap) {
// IACT opcode 6 processing (iactRebel2Opcode6), matching the original
// FUN_41CADB architecture. No corridor drawing needed here.
- // Chapter selection: Set FOBJ offset to scroll preview thumbnails in O_LEVEL.SAN.
- // Original (FUN_00415CF8): offsets start at (0,0) for the first display update,
- // then FUN_00425170 sets them to (-90, chapter*-50+75) AFTER each frame.
- // Frame 0 must use (0,0) so the 80x800 preview strip at X=320 renders off-screen
- // and STOR captures it cleanly. Frames 1+ use the scroll offset so FTCH re-renders
- // the strip at the correct preview position.
+ // Chapter selection: Set FOBJ offset for O_LEVEL.SAN preview strip.
+ // The 80x800 FOBJ strip is at left=320. With offset X=-90, it renders at
+ // X=230 (inside the preview box). The Y offset scrolls the strip vertically
+ // so only the selected chapter's 50px slice appears at Y=75.
+ // STOR captures raw FOBJ data regardless of screen rendering, so the
+ // offset on frame 0 doesn't affect the STOR/FTCH mechanism.
if (_gameState == kStateChapterSelect && _player) {
- if (_player->_frame > 0) {
- // Clear screen to black before FTCH re-renders the preview strip.
- // Our FTCH only re-draws the preview area (80px wide at X=230);
- // without clearing, old menu text and preview artifacts persist.
- if (renderBitmap) {
- memset(renderBitmap, 0, _vm->_screenWidth * _vm->_screenHeight);
- }
- _player->_fobjOffsetX = _previewOffsetX;
- _player->_fobjOffsetY = _previewOffsetY;
+ if (renderBitmap) {
+ memset(renderBitmap, 0, _vm->_screenWidth * _vm->_screenHeight);
}
+ _player->_fobjOffsetX = _previewOffsetX;
+ _player->_fobjOffsetY = _previewOffsetY;
}
}
diff --git a/engines/scumm/insane/insane_rebel_menu.cpp b/engines/scumm/insane/insane_rebel_menu.cpp
index 415d209bf31..92166f4a927 100644
--- a/engines/scumm/insane/insane_rebel_menu.cpp
+++ b/engines/scumm/insane/insane_rebel_menu.cpp
@@ -829,10 +829,12 @@ int InsaneRebel2::runChapterSelect() {
debug("Rebel2: Playing chapter select background: OPEN/O_LEVEL.SAN");
- // Flags: 0x20 (overlay drawing). No 0x08 (preserve) â we want a black
- // background. O_LEVEL.SAN has no full-screen background FOBJ; the visible
- // screen area stays black, and preview thumbnails render at X=230 via offset.
- splayer->setCurVideoFlags(0x20);
+ // Flags: 0x20 (overlay/menu rendering) | 0x08 (preserve buffer, suppress
+ // AHDR speed override). Matches original FUN_0041f4d0 parameter 8.
+ // O_LEVEL.SAN AHDR specifies 15fps; flag 0x08 suppresses this override
+ // so we use our intended 12fps. The preview screen is cleared each frame
+ // by procPreRendering's memset, so buffer preservation is harmless.
+ splayer->setCurVideoFlags(0x28);
// Play O_LEVEL.SAN â preview thumbnails are rendered by FOBJ offset
splayer->play("OPEN/O_LEVEL.SAN", 12);
@@ -874,8 +876,8 @@ int InsaneRebel2::runChapterSelect() {
_pilots[_activePilot].difficulty < 6 && _chapterSelection > 0) {
Common::String expected = getChapterPassword(
_chapterSelection, _pilots[_activePilot].difficulty);
- if (expected.empty() || _passwordInput.equalsIgnoreCase(expected)) {
- // Password accepted â unlock chapter
+ if (!expected.empty() && _passwordInput.equalsIgnoreCase(expected)) {
+ // Password accepted â unlock chapter (FUN_00415CF8 lines 253-257)
PilotData &pilot = _pilots[_activePilot];
pilot.score[_chapterSelection] = 0;
pilot.damage[_chapterSelection] = 0;
@@ -883,6 +885,8 @@ int InsaneRebel2::runChapterSelect() {
pilot.rating[_chapterSelection] = 0;
savePilots();
_chapterUnlocked[_chapterSelection] = true;
+ // Update iactBit for video preview (original jumps to LAB_00415d88)
+ setBit(16 - _chapterSelection);
_passwordInput.clear();
debug("Rebel2: Chapter %d unlocked via password", _chapterSelection + 1);
continue; // Re-render with updated unlock state
@@ -1211,13 +1215,46 @@ Common::String InsaneRebel2::getRankString(int rating) {
}
// Password table lookup - emulates FUN_0041BCE0
-// Original: 90-entry table at DAT_00481af0, 20 bytes each, XOR 0xAA
-// Index formula: (difficulty + (level * 3 - 3) * 2) * 20, level is 1-based
+// 90 entries: 15 levels à 6 difficulty slots, extracted from RA2WIN95.EXE at 0x481AF0
+// Index formula: difficulty + (level * 3 - 3) * 2, level is 1-based (1-15), difficulty 0-5
+static const char *const kPasswordTable[90] = {
+ // Level 1: diff 0-5
+ "JABBA", "EWOKS", "BANTHA", "ANAKIN", "WOOKIEE", "WOOKIEE",
+ // Level 2: diff 0-5
+ "ENDOR", "CHEWIE", "KATANA", "KENOBI", "DROID", "DROID",
+ // Level 3: diff 0-5
+ "LACHTON", "DANKIN", "DENGAR", "FORTUNA", "RODIAN", "RODIAN",
+ // Level 4: diff 0-5
+ "BORSK", "NOGHRI", "PELLAEON", "MODON", "BPFASSH", "BPFASSH",
+ // Level 5: diff 0-5
+ "KROYIES", "CHAMMA", "ITHULL", "OMMIN", "KSHYY", "KSHYY",
+ // Level 6: diff 0-5
+ "AURIL", "BOGGA", "STENNESS", "REKKON", "TORVE", "TORVE",
+ // Level 7: diff 0-5
+ "KAMPL", "INCOM", "MYRKR", "SHAZEEN", "SLUISSI", "SLUISSI",
+ // Level 8: diff 0-5
+ "FERRIER", "KOTHLIS", "CHURBA", "KIIRIUM", "PALANHI", "PALANHI",
+ // Level 9: diff 0-5
+ "GALIA", "KRATH", "ARTOO", "GUNDARK", "DROKKO", "DROKKO",
+ // Level 10: diff 0-5
+ "DENARII", "SIOSK", "SATAL", "DIANOGA", "NATTH", "NATTH",
+ // Level 11: diff 0-5
+ "SADOW", "ADEGAN", "LOBUE", "ATUARRE", "SABACC", "SABACC",
+ // Level 12: diff 0-5
+ "ONDERON", "AMANOA", "DENEBA", "ESSADA", "ANDUR", "ANDUR",
+ // Level 13: diff 0-5
+ "ALEEMA", "AMBRIA", "STURM", "PAPLOO", "ARKANIA", "ARKANIA",
+ // Level 14: diff 0-5
+ "CATHAR", "SYLVAR", "CRADO", "NASHTAH", "DIATH", "DIATH",
+ // Level 15: diff 0-5
+ "DOMINIS", "MIRALUKA", "CARRACK", "PESTAGE", "DREEBO", "DREEBO",
+};
+
Common::String InsaneRebel2::getChapterPassword(int level, int difficulty) {
- // TODO: Extract actual password table from game binary (FUN_0041BCE0)
- // Table: 90 entries x 20 bytes at exe offset 0x481AF0, XOR 0xAA
- // For now, return empty string (no password required â debug mode)
- return "";
+ if (level < 1 || level > 15 || difficulty < 0 || difficulty > 5)
+ return "";
+ int idx = difficulty + (level * 3 - 3) * 2;
+ return kPasswordTable[idx];
}
// Draw score/info line at bottom of chapter select - emulates FUN_00434cb0 calls
@@ -1364,22 +1401,24 @@ void InsaneRebel2::drawChapterSelectOverlay(byte *renderBitmap, int pitch, int w
// Build items array matching original DAT_004577a8 layout
const char *items[18];
- // items[0] = title = TRS 40 ("^f02Chapters")
- items[0] = splayer->getString(40);
+ // Original (FUN_00415CF8 lines 55-77): starts with ALL locked strings,
+ // then overrides unlocked chapters individually.
+ // items[0] = title = TRS 60 ("^f02Chapters")
+ items[0] = splayer->getString(60);
if (!items[0] || !items[0][0])
items[0] = "^f02Chapters";
// items[1..16] = chapters, using unlocked (TRS 41-56) or locked (TRS 61-76) strings
for (int i = 1; i <= 16; i++) {
- bool unlocked = (i - 1 < 16) && _chapterUnlocked[i - 1];
+ bool unlocked = _chapterUnlocked[i - 1];
int trsIdx = unlocked ? (40 + i) : (60 + i);
items[i] = splayer->getString(trsIdx);
if (!items[i] || !items[i][0])
items[i] = "";
}
- // items[17] = "RETURN TO PILOTS" = TRS 57 ("^f01^c240RETURN TO PILOTS")
- items[17] = splayer->getString(57);
+ // items[17] = "RETURN TO PILOTS" = TRS 77
+ items[17] = splayer->getString(77);
if (!items[17] || !items[17][0])
items[17] = "^f01^c240RETURN TO PILOTS";
@@ -1387,7 +1426,8 @@ void InsaneRebel2::drawChapterSelectOverlay(byte *renderBitmap, int pitch, int w
drawMenuItems(renderBitmap, pitch, width, height, items, 17, _chapterSelection, true);
// Draw preview box border on the right side (FUN_004292d0 calls at lines 128-133)
- // The actual preview image is rendered by O_LEVEL.SAN FOBJ via the offset system
+ // The preview thumbnail is rendered by O_LEVEL.SAN via FOBJ offset + STOR/FTCH.
+ // SKIP chunks in O_LEVEL.SAN use iactBits to show locked/unlocked preview variants.
drawPreviewBox(renderBitmap, pitch, width, height);
// Draw score/info line at bottom
diff --git a/engines/scumm/smush/smush_player.cpp b/engines/scumm/smush/smush_player.cpp
index e0391672c68..7338951cd68 100644
--- a/engines/scumm/smush/smush_player.cpp
+++ b/engines/scumm/smush/smush_player.cpp
@@ -858,16 +858,26 @@ void SmushPlayer::decodeFrameObject(int codec, const uint8 *src, int left, int t
if (_dst == _specialBuffer)
pitch = _width;
+ int srcSkipY = 0;
if (isRA2()) {
- ra2AdjustFrameCoords(left, top, width, height, pitch);
+ ra2AdjustFrameCoords(left, top, width, height, pitch, &srcSkipY);
if (width <= 0 || height <= 0)
return;
}
+ // For RLE codecs, skip source rows that were clipped from the top.
+ // Each RLE row has a 2-byte size prefix, so we can advance past them.
+ const uint8 *adjustedSrc = src;
+ if (srcSkipY > 0 && (codec == SMUSH_CODEC_RLE || codec == SMUSH_CODEC_RLE_ALT)) {
+ for (int i = 0; i < srcSkipY; i++) {
+ adjustedSrc += READ_LE_UINT16(adjustedSrc) + 2;
+ }
+ }
+
switch (codec) {
case SMUSH_CODEC_RLE:
case SMUSH_CODEC_RLE_ALT:
- smushDecodeRLE(_dst, src, left, top, width, height, pitch);
+ smushDecodeRLE(_dst, adjustedSrc, left, top, width, height, pitch);
break;
case SMUSH_CODEC_DELTA_BLOCKS:
if (!_deltaBlocksCodec)
diff --git a/engines/scumm/smush/smush_player.h b/engines/scumm/smush/smush_player.h
index 85d6b60edbd..7c12fcfa9ac 100644
--- a/engines/scumm/smush/smush_player.h
+++ b/engines/scumm/smush/smush_player.h
@@ -304,7 +304,7 @@ private:
int pos_x, int pos_y, int left, int top,
int width, int height, TextStyleFlags flg);
void ra2SelectFrameBuffer(int width, int height);
- void ra2AdjustFrameCoords(int &left, int &top, int &width, int &height, int pitch);
+ void ra2AdjustFrameCoords(int &left, int &top, int &width, int &height, int pitch, int *srcSkipY = nullptr);
bool ra2DecodeCodec(int codec, const uint8 *src, int left, int top,
int width, int height, int pitch, int dataSize);
void ra2StoreFobjData(int codec, const byte *data, int32 dataSize,
diff --git a/engines/scumm/smush/smush_player_ra2.cpp b/engines/scumm/smush/smush_player_ra2.cpp
index 198ca7c200f..63a314fd99f 100644
--- a/engines/scumm/smush/smush_player_ra2.cpp
+++ b/engines/scumm/smush/smush_player_ra2.cpp
@@ -304,13 +304,17 @@ void SmushPlayer::ra2SelectFrameBuffer(int width, int height) {
/**
* Apply RA2 FOBJ position offsets and clamp to buffer bounds.
* Modifies left, top, width, height in place.
+ * When srcSkipY is non-null, outputs the number of source rows to skip
+ * when top is clipped from negative (for codecs with row-size prefixes).
*/
-void SmushPlayer::ra2AdjustFrameCoords(int &left, int &top, int &width, int &height, int pitch) {
+void SmushPlayer::ra2AdjustFrameCoords(int &left, int &top, int &width, int &height, int pitch, int *srcSkipY) {
left += _fobjOffsetX;
top += _fobjOffsetY;
int bufHeight = (_dst == _specialBuffer) ? _height : _vm->_screenHeight;
if (top < 0) {
+ if (srcSkipY)
+ *srcSkipY = -top;
height += top;
top = 0;
}
Commit: 7c74f5e1348bbe728827a4af7e50c10533a8e5ea
https://github.com/scummvm/scummvm/commit/7c74f5e1348bbe728827a4af7e50c10533a8e5ea
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:14+02:00
Commit Message:
SCUMM: RA2: Refactor player shot rendering
Changed paths:
engines/scumm/insane/insane_rebel.cpp
engines/scumm/insane/insane_rebel.h
engines/scumm/insane/insane_rebel_iact.cpp
engines/scumm/insane/insane_rebel_render.cpp
diff --git a/engines/scumm/insane/insane_rebel.cpp b/engines/scumm/insane/insane_rebel.cpp
index cc0a28aac3d..d541d1aa47d 100644
--- a/engines/scumm/insane/insane_rebel.cpp
+++ b/engines/scumm/insane/insane_rebel.cpp
@@ -82,6 +82,16 @@ InsaneRebel2::InsaneRebel2(ScummEngine_v7 *scumm) {
if (_smush_iconsNut && _smush_iconsNut->getNumChars() > 5) {
initLaserTexture(_smush_iconsNut, 5);
}
+
+ // Initialize edge blend table with default identity table (FUN_404BCE -> FUN_410510(NULL))
+ // Per-level tables are loaded later via IACT opcode 8 par4=1000
+ initEdgeTable(nullptr);
+ // DAT_0047a7fc: Controls edge highlight rendering and widescreen features.
+ // Set from param_10 of FUN_403BD0 (main game init). Values:
+ // < 0: Edge highlights disabled (low-detail mode)
+ // >= 0: Edge highlights enabled, >= 1: high-detail (secondary NUTs, widescreen)
+ // Always use high detail in ScummVM.
+ _rebelDetailMode = 1;
_smush_cockpitNut = new NutRenderer(_vm, "SYSTM/DISPFONT.NUT");
// Load SMALFONT.NUT for HUD score/lives rendering (DAT_00482200 equivalent)
diff --git a/engines/scumm/insane/insane_rebel.h b/engines/scumm/insane/insane_rebel.h
index 606a95bd3fb..43734d7398b 100644
--- a/engines/scumm/insane/insane_rebel.h
+++ b/engines/scumm/insane/insane_rebel.h
@@ -517,12 +517,42 @@ public:
};
LaserTexture _laserTexture; // DAT_0047fee4
+ // ======================= Edge Blend Table (DAT_0046a7d0) =======================
+ // 256x256 lookup table used by drawEdgeHighlightLine() (FUN_410962) to compute
+ // glow colors at beam edges. For each pixel on the beam edge, the table maps
+ // [adjacent_pixel_above][adjacent_pixel_below] -> output color, producing a
+ // color-blended highlight that gives beams their distinctive glow.
+ //
+ // Default table (FUN_410510 with NULL): table[a][b] = min(a,b) (identity/transparent).
+ // Per-level tables loaded via IACT opcode 8 par4=1000 (FUN_405663 -> FUN_410510).
+ // The per-level table tunes the glow to the current palette (red for some levels,
+ // green for others).
+ //
+ // _rebelDetailMode (DAT_0047a7fc): Controls whether edge highlights are drawn.
+ // Set from IACT opcode 6. When >= 0, edge highlights are enabled.
+ byte _edgeTable[256 * 256]; // DAT_0046a7d0 - primary edge blend table
+ byte _edgeTableAlt[256 * 256]; // DAT_00443fb0 - secondary blend table (hi-res mode)
+ int16 _rebelDetailMode; // DAT_0047a7fc - edge highlight enable flag
+
+ // Initialize edge blend table (FUN_410510)
+ // data == nullptr: fill with default identity table (min(a,b))
+ // data != nullptr: load 256x256 symmetric table from data (skips 8-byte header)
+ void initEdgeTable(const byte *data);
+
+ // Draw edge highlight line using the edge blend table (FUN_410962)
+ // Each pixel is blended from its perpendicular neighbors via _edgeTable lookup.
+ // For horizontal-dominant beams, reads pixels above/below the line.
+ // For vertical-dominant beams, reads pixels left/right of the line.
+ void drawEdgeHighlightLine(byte *dst, int pitch, int width, int height,
+ int16 x0, int16 y0, int16 x1, int16 y1);
+
// Initialize laser texture from NUT sprite (FUN_0040BAB0)
void initLaserTexture(NutRenderer *nut, int spriteIdx);
void freeLaserTexture();
// Draw laser beam using pre-initialized texture (FUN_0040BBF6)
- // Parameters match the original assembly function
+ // Two-layer rendering: textured scanlines (beam body) + edge highlights (glow).
+ // Edge highlights are only drawn when _rebelDetailMode >= 0.
void drawLaserBeam(byte *dst, int pitch, int width, int height,
int16 gunX, int16 gunY, int16 targetX, int16 targetY,
int16 animFrame, int16 maxFrames,
diff --git a/engines/scumm/insane/insane_rebel_iact.cpp b/engines/scumm/insane/insane_rebel_iact.cpp
index f86f9ff630e..59fdc9ddcdf 100644
--- a/engines/scumm/insane/insane_rebel_iact.cpp
+++ b/engines/scumm/insane/insane_rebel_iact.cpp
@@ -573,9 +573,12 @@ void InsaneRebel2::iactRebel2Opcode6(byte *renderBitmap, Common::SeekableReadStr
// Set ship level mode (DAT_0043e000 = par3)
_shipLevelMode = par3;
- // If par4 == 1, enable status bar
+ // If par4 == 1, enable status bar and re-render laser texture (FUN_0040bb87)
if (par4 == 1) {
- _rebelStatusBarSprite = 5; // Status bar sprite for Handler 8
+ _rebelStatusBarSprite = 5;
+ if (_smush_iconsNut && _smush_iconsNut->getNumChars() > 5) {
+ initLaserTexture(_smush_iconsNut, 5);
+ }
}
// Reset state when shipLevelMode != 0 && par4 == 1 (FUN_401234 lines 97-103)
@@ -746,7 +749,10 @@ void InsaneRebel2::iactRebel2Opcode6(byte *renderBitmap, Common::SeekableReadStr
bodyStatusFlag = b.readSint16LE();
}
if (bodyStatusFlag == 1) {
- _rebelStatusBarSprite = 5; // Status bar sprite
+ _rebelStatusBarSprite = 5;
+ if (_smush_iconsNut && _smush_iconsNut->getNumChars() > 5) {
+ initLaserTexture(_smush_iconsNut, 5);
+ }
debug("Rebel2 Opcode 6 (Handler 7): Status bar enabled (body flag=%d)", bodyStatusFlag);
}
@@ -1027,6 +1033,9 @@ void InsaneRebel2::iactRebel2Opcode6(byte *renderBitmap, Common::SeekableReadStr
// Note: This is local_14[4] in the decompiled code, NOT local_14[3] (par4)
if (par5 == 1) {
_rebelStatusBarSprite = 5;
+ if (_smush_iconsNut && _smush_iconsNut->getNumChars() > 5) {
+ initLaserTexture(_smush_iconsNut, 5);
+ }
// Guard with _rebelOp6Initialized: runs once per wave video, not per frame.
if (!_rebelOp6Initialized) {
clearBit(0);
@@ -1230,15 +1239,50 @@ void InsaneRebel2::iactRebel2Opcode6(byte *renderBitmap, Common::SeekableReadStr
return;
}
- // Step 1: If par4 == 1, initialize/reset state (lines 114-121)
- if (par4 == 1) {
- // Draw status bar sprite 5 (FUN_0040bb87 equivalent)
- _rebelStatusBarSprite = (_rebelLevelType == 5) ? 53 : 5;
+ // Handler 0x26: FUN_407FCB line 77-79 â set level type from par4, read par5 for init trigger
+ // param_5[3] = par4 = levelType, param_5[4] = par5 = init flag
+ if (_rebelHandler == 0x26) {
+ _rebelLevelType = par4;
+
+ // Read par5 from IACT body (param_5[4])
+ int16 par5 = 0;
+ if (b.pos() + 2 <= b.size()) {
+ int64 savedPos = b.pos();
+ par5 = b.readSint16LE();
+ b.seek(savedPos);
+ }
+
+ if (par5 == 1) {
+ // Re-render laser texture for this level (FUN_0040bb87)
+ // levelType 5 uses sprite 53, all others use sprite 5
+ _rebelStatusBarSprite = (_rebelLevelType == 5) ? 53 : 5;
+ if (_smush_iconsNut && _smush_iconsNut->getNumChars() > _rebelStatusBarSprite) {
+ initLaserTexture(_smush_iconsNut, _rebelStatusBarSprite);
+ }
+
+ if (!_rebelOp6Initialized) {
+ clearBit(0);
+ for (int i = 0; i < 512; i++) {
+ _rebelLinks[i][0] = 0;
+ _rebelLinks[i][1] = 0;
+ _rebelLinks[i][2] = 0;
+ }
+ _rebelWaveState = _rebelPhaseState;
+ _rebelHitCounter = 0;
+ _rebelOp6Initialized = true;
+ debug("Rebel2 Opcode 6 (Handler 0x26): Wave init, levelType=%d waveState=0x%x",
+ _rebelLevelType, _rebelWaveState);
+ }
+ }
+ }
+
+ // Other handlers: par4 == 1 triggers init (NOT level type)
+ if (_rebelHandler != 0x26 && par4 == 1) {
+ _rebelStatusBarSprite = 5;
+ if (_smush_iconsNut && _smush_iconsNut->getNumChars() > 5) {
+ initLaserTexture(_smush_iconsNut, 5);
+ }
- // Per-wave init: clear bits, links, reset wave state.
- // In the original game, FUN_00423880 runs ONCE at video-start callback
- // registration time, not per-frame. Guard with _rebelOp6Initialized so
- // this fires once per wave video (reset in procPreRendering at frame 0).
if (!_rebelOp6Initialized) {
clearBit(0);
for (int i = 0; i < 512; i++) {
@@ -1253,9 +1297,6 @@ void InsaneRebel2::iactRebel2Opcode6(byte *renderBitmap, Common::SeekableReadStr
}
}
- // Step 2: Set level type (DAT_00457900 = par3)
- _rebelLevelType = par3;
-
// Step 3: Autopilot/control mode logic (lines 123-146)
// This determines whether the ship flies on autopilot or manual control
if (!_rebelInvulnerable) {
@@ -1437,6 +1478,29 @@ void InsaneRebel2::iactRebel2Opcode8(byte *renderBitmap, Common::SeekableReadStr
b.seek(startPos);
}
+ // ===== Edge Blend Table Loading (par4 == 1000) =====
+ // FUN_405663: After all handler-specific opcode 8 processing, checks if par4==1000.
+ // If so, loads a per-level 256x256 color blend table from the IACT chunk data.
+ // This table controls the edge glow color of laser beams (e.g. red vs green).
+ // Data starts at byte offset 18 in the IACT chunk (in_stack_00000014 + 9 shorts).
+ if (par4 == 1000 && remaining >= 18 + 8 + 32896) {
+ // Read the raw edge table data from the stream
+ // Layout: 18 bytes IACT header params already consumed by caller,
+ // but 'b' is positioned at startPos which is after par1..par4.
+ // The original code passes (param + 9 shorts) = data at byte offset 18 from chunk start.
+ // Since our stream starts after the 6 par shorts (12 bytes), the data is at offset 6 from startPos.
+ byte *edgeData = (byte *)malloc(8 + 32896);
+ if (edgeData) {
+ b.seek(startPos + 6); // Skip 3 remaining shorts (par2, par3, par4 already read; 3 more padding shorts)
+ b.read(edgeData, 8 + 32896);
+ initEdgeTable(edgeData);
+ free(edgeData);
+ debug("Rebel2 Opcode 8: Loaded per-level edge blend table (par4=1000)");
+ }
+ b.seek(startPos);
+ return;
+ }
+
// ===== Sound Loading (par3 21-47) =====
if (par3 >= 21 && par3 <= 47) {
debug("Rebel2 Opcode 8: Sound loading subcase %d (not implemented)", par3);
diff --git a/engines/scumm/insane/insane_rebel_render.cpp b/engines/scumm/insane/insane_rebel_render.cpp
index fe76fe6e137..8fe2735945e 100644
--- a/engines/scumm/insane/insane_rebel_render.cpp
+++ b/engines/scumm/insane/insane_rebel_render.cpp
@@ -478,11 +478,17 @@ void InsaneRebel2::spawnExplosion(int x, int y, int objectHalfWidth) {
}
// Get max shot duration from level table (DAT_0047e0f0 indexed by DAT_0047a7fa/DAT_0047a7f8)
-// For now, use a reasonable default based on observed values
+// The original reads from a per-level per-difficulty table. The field at offset +0x00
+// (DAT_0047e0f0) is the first field of each record â our laserDelay field.
+// This value is used both as the initial shot counter AND as maxFrames for beam rendering.
int16 InsaneRebel2::getShotMaxDuration() {
- // The original uses: DAT_0047e0f0 + DAT_0047a7fa * 0x242 + DAT_0047a7f8 * 0x22
- // Typical values observed: 2-5 frames
- return 4;
+ LevelDifficultyParams params = getDifficultyParams();
+ // laserDelay = DAT_0047e0f0 field: shot duration in frames
+ // Clamp to reasonable range to avoid division by zero or extreme beams
+ int16 duration = params.laserDelay;
+ if (duration <= 0)
+ duration = 4; // Fallback for -1 entries (disabled levels)
+ return duration;
}
// Dispatcher - calls appropriate spawn function based on current handler
@@ -838,7 +844,7 @@ void drawTexturedSegment(byte *dst, int pitch, int width, int height, int param_
int sx = dx > 0 ? 1 : -1;
int sy = dy > 0 ? 1 : -1;
int err = absdx - absdy;
- int iVar12 = steps;
+ int iVar12 = steps - 1; // Original starts at majorAxis, not majorAxis+1
for (int i = 0; i < steps; i++) {
if (x >= 0 && x < width && y >= 0 && y < height) {
@@ -849,7 +855,7 @@ void drawTexturedSegment(byte *dst, int pitch, int width, int height, int param_
if (e2 > -absdy) { err -= absdy; x += sx; }
if (e2 < absdx) { err += absdx; y += sy; }
iVar12 -= param_7;
- if (iVar12 < 0) { texPtr++; iVar12 += steps; }
+ while (iVar12 < 0) { texPtr++; iVar12 += steps; }
}
}
@@ -890,6 +896,31 @@ void InsaneRebel2::initLaserTexture(NutRenderer *nut, int spriteIdx) {
}
debug("Rebel2: Initialized laser texture %dx%d from sprite %d", texWidth, texHeight, spriteIdx);
+
+ // Diagnostic: dump texture pixel stats to verify data is loaded correctly
+ if (_laserTexture.pixels && texWidth > 0 && texHeight > 0) {
+ int third = texWidth / 3;
+ int band1 = 0, band2 = 0, band3 = 0;
+ for (int row = 0; row < texHeight; row++) {
+ for (int col = 0; col < texWidth; col++) {
+ if (_laserTexture.pixels[row * texWidth + col] != 0) {
+ if (col < third) band1++;
+ else if (col < third * 2) band2++;
+ else band3++;
+ }
+ }
+ }
+ debug("Rebel2: Texture non-zero pixels by band: [0-%d]=%d [%d-%d]=%d [%d-%d]=%d",
+ third - 1, band1, third, third * 2 - 1, band2, third * 2, texWidth - 1, band3);
+
+ // Dump first row hex (first 64 bytes)
+ Common::String hexRow;
+ int dumpLen = MIN(texWidth, (int16)64);
+ for (int col = 0; col < dumpLen; col++) {
+ hexRow += Common::String::format("%02x ", _laserTexture.pixels[col]);
+ }
+ debug("Rebel2: Texture row 0 (first %d): %s", dumpLen, hexRow.c_str());
+ }
}
// Free laser texture buffer (FUN_0040BBD1)
@@ -900,8 +931,300 @@ void InsaneRebel2::freeLaserTexture() {
_laserTexture.height = 0;
}
+// Initialize edge blend tables (FUN_410510)
+// When data is nullptr, fills with default tables:
+// _edgeTable[a*256+b] = min(a,b) (symmetric identity blend)
+// _edgeTableAlt[a*256+b] = special blend for hi-res mode
+// When data is non-null, loads the primary table from data+8 (upper triangle, symmetric).
+void InsaneRebel2::initEdgeTable(const byte *data) {
+ if (data == nullptr) {
+ // Default table initialization (FUN_410510 param_1==NULL path, lines 12-36)
+ for (int a = 0; a < 256; a++) {
+ for (int b = a; b < 256; b++) {
+ // Primary table: table[a][b] = a (i.e. min(a,b) since b >= a)
+ _edgeTable[a + b * 256] = (byte)a;
+ _edgeTable[b + a * 256] = (byte)a;
+
+ // Secondary table: special blend rules (FUN_410510 lines 17-31)
+ if (a < 0x10 || b > 0x4f) {
+ // Outside blend range: use b if b==0, or (0xf < b && b < 0x50), or b==4
+ if (b == 0 || (b > 0xf && b < 0x50) || b == 4) {
+ _edgeTableAlt[a + b * 256] = (byte)b;
+ } else {
+ _edgeTableAlt[a + b * 256] = (byte)a;
+ }
+ } else {
+ // Blend range [0x10..0x4f]: average of a and b
+ _edgeTableAlt[a + b * 256] = (byte)((a + b) / 2);
+ }
+ _edgeTableAlt[b + a * 256] = _edgeTableAlt[a + b * 256];
+ }
+ }
+ // Special entries (FUN_410510 lines 33-36)
+ _edgeTable[0x42 * 256 + 0xf1] = 0x42; // DAT_00447ff1
+ _edgeTable[0x42 + 0xf0 * 256] = 0x42; // DAT_004480f0 (symmetric)
+ _edgeTable[0x41 * 256 + 0xb0] = 0x41; // DAT_00447fb0
+ _edgeTableAlt[0x41 * 256 + 0xf0] = 0x41; // DAT_00443ff0
+ } else {
+ // Load table from IACT data (FUN_410510 non-NULL path, lines 39-47)
+ // Data format: 8-byte header + upper triangle of 256x256 symmetric table
+ const byte *src = data + 8;
+ for (int a = 0; a < 256; a++) {
+ for (int b = a; b < 256; b++) {
+ _edgeTable[a + b * 256] = *src;
+ _edgeTable[b + a * 256] = *src;
+ src++;
+ }
+ }
+ }
+}
+
+// Draw edge highlight line using the edge blend table (FUN_410962)
+// For each pixel along the line, reads the two adjacent pixels perpendicular to
+// the line direction and uses _edgeTable[above*256+below] as the output color.
+// This creates a glow/blend effect at beam edges.
+//
+// For horizontal-dominant lines (dx > dy), reads pixels above and below.
+// For vertical-dominant lines (dy > dx), reads pixels left and right.
+//
+// param_1 = dst buffer info (base pointer at [0], pitch at [1], width at word [1], height at byte offset 6)
+// param_2 = clip rect (or NULL for full buffer)
+// param_3..param_6 = x0, y0, x1, y1 line endpoints
+void InsaneRebel2::drawEdgeHighlightLine(byte *dst, int pitch, int width, int height,
+ int16 x0, int16 y0, int16 x1, int16 y1) {
+ // Clip region (FUN_410962 lines 19-30, simplified for our buffer layout)
+ int16 clipLeft = 1;
+ int16 clipTop = 1;
+ int16 clipRight = width - 2;
+ int16 clipBottom = height - 2;
+
+ // Clip X endpoints (FUN_410962 lines 35-69)
+ if (x0 == x1) {
+ if (x0 < clipLeft || x0 > clipRight)
+ return;
+ } else {
+ if (x0 < clipLeft) {
+ if (x1 < clipLeft) return;
+ y0 = y1 + (int16)(((int)(y0 - y1) * (int)(clipLeft - x1)) / (int)(x0 - x1));
+ x0 = clipLeft;
+ } else if (x0 > clipRight) {
+ if (x1 > clipRight) return;
+ y0 = y1 + (int16)(((int)(y0 - y1) * (int)(clipRight - x1)) / (int)(x0 - x1));
+ x0 = clipRight;
+ }
+ if (x1 < clipLeft) {
+ y1 = y0 + (int16)(((int)(y1 - y0) * (int)(clipLeft - x0)) / (int)(x1 - x0));
+ x1 = clipLeft;
+ } else if (x1 > clipRight) {
+ y1 = y0 + (int16)(((int)(y1 - y0) * (int)(clipRight - x0)) / (int)(x1 - x0));
+ x1 = clipRight;
+ }
+ }
+
+ // Clip Y endpoints (FUN_410962 lines 71-106)
+ if (y0 == y1) {
+ if (y0 < clipTop || y0 > clipBottom)
+ return;
+ } else {
+ if (y0 < clipTop) {
+ if (y1 < clipTop) return;
+ x0 = x1 + (int16)(((int)(x0 - x1) * (int)(clipTop - y1)) / (int)(y0 - y1));
+ y0 = clipTop;
+ } else if (y0 > clipBottom) {
+ if (y1 > clipBottom) return;
+ x0 = x1 + (int16)(((int)(x0 - x1) * (int)(clipBottom - y1)) / (int)(y0 - y1));
+ y0 = clipBottom;
+ }
+ if (y1 < clipTop) {
+ x1 = x0 + (int16)(((int)(x1 - x0) * (int)(clipTop - y0)) / (int)(y1 - y0));
+ y1 = clipTop;
+ } else if (y1 > clipBottom) {
+ x1 = x0 + (int16)(((int)(x1 - x0) * (int)(clipBottom - y0)) / (int)(y1 - y0));
+ y1 = clipBottom;
+ }
+ }
+
+ // Calculate starting pixel address and deltas (FUN_410962 lines 107-110)
+ byte *pixel = dst + y0 * pitch + x0;
+ int16 dx = x1 - x0;
+ int16 dy = y1 - y0;
+
+ // Bresenham line with perpendicular neighbor lookup (FUN_410962 lines 111-270)
+ // The key insight: for each pixel, the blend reads neighbors PERPENDICULAR to the line.
+ // - Horizontal lines: blend pixel_above * 256 + pixel_below
+ // - Vertical lines: blend pixel_left * 256 + pixel_right (reversed: left=[-1], right=[+1])
+ if (dx == 0) {
+ if (dy == 0) {
+ // Single pixel: blend from above/below
+ *pixel = _edgeTable[(uint)pixel[pitch] + (uint)pixel[-pitch] * 256];
+ } else if (dy < 0) {
+ // Vertical line going up: read left/right neighbors
+ while (dy < 1) {
+ *pixel = _edgeTable[(uint)pixel[1] + (uint)pixel[-1] * 256];
+ pixel -= pitch;
+ dy++;
+ }
+ } else {
+ // Vertical line going down: read left/right neighbors
+ while (dy >= 0) {
+ *pixel = _edgeTable[(uint)pixel[1] + (uint)pixel[-1] * 256];
+ pixel += pitch;
+ dy--;
+ }
+ }
+ } else if (dy == 0) {
+ if (dx < 0) {
+ // Horizontal line going left: read above/below neighbors
+ while (dx < 1) {
+ *pixel = _edgeTable[(uint)pixel[pitch] + (uint)pixel[-pitch] * 256];
+ pixel--;
+ dx++;
+ }
+ } else {
+ // Horizontal line going right: read above/below neighbors
+ while (dx >= 0) {
+ *pixel = _edgeTable[(uint)pixel[pitch] + (uint)pixel[-pitch] * 256];
+ pixel++;
+ dx--;
+ }
+ }
+ } else if (dx < 0 || dy < 0) {
+ // Mixed negative direction cases (FUN_410962 lines 149-240)
+ if (dy < 0) {
+ if (dx < 0) {
+ // Both negative: going up-left
+ if (dx < dy) {
+ // X-major (|dx| > |dy|): read above/below
+ int err = (-dx) >> 1;
+ int steps = 1 - dx;
+ while (steps > 0) {
+ *pixel = _edgeTable[(uint)pixel[pitch] + (uint)pixel[-pitch] * 256];
+ pixel--;
+ err += dy;
+ steps--;
+ if (err < 0) {
+ err -= dx;
+ pixel -= pitch;
+ }
+ }
+ } else {
+ // Y-major (|dy| > |dx|): read left/right
+ int err = (-dy) >> 1;
+ int steps = 1 - dy;
+ while (steps > 0) {
+ *pixel = _edgeTable[(uint)pixel[1] + (uint)pixel[-1] * 256];
+ pixel -= pitch;
+ err += dx;
+ steps--;
+ if (err < 0) {
+ err -= dy;
+ pixel--;
+ }
+ }
+ }
+ } else {
+ // dx > 0, dy < 0: going right-up
+ if (-dy < dx) {
+ // X-major: read above/below
+ int err = dx >> 1;
+ int steps = dx + 1;
+ while (steps > 0) {
+ *pixel = _edgeTable[(uint)pixel[pitch] + (uint)pixel[-pitch] * 256];
+ pixel++;
+ err += dy;
+ steps--;
+ if (err < 0) {
+ err += dx;
+ pixel -= pitch;
+ }
+ }
+ } else {
+ // Y-major: read left/right
+ int err = (-dy) >> 1;
+ int steps = 1 - dy;
+ while (steps > 0) {
+ *pixel = _edgeTable[(uint)pixel[1] + (uint)pixel[-1] * 256];
+ pixel -= pitch;
+ err -= dx;
+ steps--;
+ if (err < 0) {
+ err -= dy;
+ pixel++;
+ }
+ }
+ }
+ }
+ } else {
+ // dx < 0, dy > 0: going left-down
+ if (-dx == dy || -dx < dy) {
+ // Y-major: read left/right
+ int err = dy >> 1;
+ int steps = dy + 1;
+ while (steps > 0) {
+ *pixel = _edgeTable[(uint)pixel[1] + (uint)pixel[-1] * 256];
+ pixel += pitch;
+ err += dx;
+ steps--;
+ if (err < 0) {
+ err += dy;
+ pixel--;
+ }
+ }
+ } else {
+ // X-major: read above/below
+ int err = (-dx) >> 1;
+ int steps = 1 - dx;
+ while (steps > 0) {
+ *pixel = _edgeTable[(uint)pixel[pitch] + (uint)pixel[-pitch] * 256];
+ pixel--;
+ err -= dy;
+ steps--;
+ if (err < 0) {
+ err -= dx;
+ pixel += pitch;
+ }
+ }
+ }
+ }
+ } else {
+ // Both positive: going right-down (FUN_410962 lines 242-270)
+ if (dy < dx) {
+ // X-major: read above/below
+ int err = dx >> 1;
+ int steps = dx + 1;
+ while (steps > 0) {
+ *pixel = _edgeTable[(uint)pixel[pitch] + (uint)pixel[-pitch] * 256];
+ pixel++;
+ err -= dy;
+ steps--;
+ if (err < 0) {
+ err += dx;
+ pixel += pitch;
+ }
+ }
+ } else {
+ // Y-major: read left/right
+ int err = dy >> 1;
+ int steps = dy + 1;
+ while (steps > 0) {
+ *pixel = _edgeTable[(uint)pixel[1] + (uint)pixel[-1] * 256];
+ pixel += pitch;
+ err -= dx;
+ steps--;
+ if (err < 0) {
+ err += dy;
+ pixel++;
+ }
+ }
+ }
+ }
+}
+
// Draw laser beam using pre-initialized texture (FUN_0040BBF6)
-// This is a direct port of the assembly function
+// This is a direct port of the assembly function.
+// Two-layer rendering:
+// Layer 1: Textured scanlines (beam body) via drawTexturedSegment()
+// Layer 2: Edge highlights (glow) via drawEdgeHighlightLine(), gated by _rebelDetailMode >= 0
//
// Parameters (matching FUN_0040bbf6):
// dst, pitch, width, height: destination buffer info
@@ -946,6 +1269,10 @@ void InsaneRebel2::drawLaserBeam(byte *dst, int pitch, int width, int height,
// FUN_0040BBF6 line 30: Get texture pixel pointer
byte *local_28 = texPixels;
+ debug(5, "Rebel2: drawLaserBeam gun(%d,%d) tgt(%d,%d) start(%d,%d) end(%d,%d) anim=%d/%d ws=%d hs=%d th=%d",
+ gunX, gunY, targetX, targetY, startX, startY, endX, endY,
+ animFrame, maxFrames, widthScale, heightScale, thickness);
+
// FUN_0040BBF6 lines 31-32: Calculate abs differences (FUN_004356e4 = abs)
int iVar2 = abs(startY - endY); // |dy| of beam
int iVar3 = abs(startX - endX); // |dx| of beam
@@ -960,9 +1287,8 @@ void InsaneRebel2::drawLaserBeam(byte *dst, int pitch, int width, int height,
int16 local_24 = -numLines;
int16 halfLines = numLines >> 1;
- // FUN_0040BBF6 lines 39-46: Draw parallel lines
+ // FUN_0040BBF6 lines 39-46: Draw parallel textured scanlines (beam body)
for (int16 lineIdx = 0; lineIdx < numLines; lineIdx++) {
- // Draw one textured segment (vertical offset for this scanline)
drawTexturedSegment(dst, pitch, width, height,
startX, (startY - halfLines) + lineIdx,
endX, (endY - halfLines) + lineIdx,
@@ -973,6 +1299,16 @@ void InsaneRebel2::drawLaserBeam(byte *dst, int pitch, int width, int height,
local_28 += texW;
}
}
+
+ // FUN_0040BBF6 lines 47-51: Edge highlights along top and bottom beam edges
+ if (_rebelDetailMode >= 0) {
+ drawEdgeHighlightLine(dst, pitch, width, height,
+ startX, startY - halfLines,
+ endX, endY - halfLines);
+ drawEdgeHighlightLine(dst, pitch, width, height,
+ startX, (startY - halfLines) + numLines - 1,
+ endX, (endY - halfLines) + numLines - 1);
+ }
} else {
// Mostly vertical beam - draw horizontal scanlines
// FUN_0040BBF6 lines 54-56
@@ -987,9 +1323,8 @@ void InsaneRebel2::drawLaserBeam(byte *dst, int pitch, int width, int height,
int16 halfLines = numLines >> 1;
- // FUN_0040BBF6 lines 61-68: Draw parallel lines
+ // FUN_0040BBF6 lines 61-68: Draw parallel textured scanlines (beam body)
for (int16 lineIdx = 0; lineIdx < numLines; lineIdx++) {
- // Draw one textured segment (horizontal offset for this scanline)
drawTexturedSegment(dst, pitch, width, height,
(startX - halfLines) + lineIdx, startY,
(endX - halfLines) + lineIdx, endY,
@@ -1000,6 +1335,16 @@ void InsaneRebel2::drawLaserBeam(byte *dst, int pitch, int width, int height,
local_28 += texW;
}
}
+
+ // FUN_0040BBF6 lines 69-73: Edge highlights along left and right beam edges
+ if (_rebelDetailMode >= 0) {
+ drawEdgeHighlightLine(dst, pitch, width, height,
+ startX - halfLines, startY,
+ endX - halfLines, endY);
+ drawEdgeHighlightLine(dst, pitch, width, height,
+ (startX - halfLines) + numLines - 1, startY,
+ (endX - halfLines) + numLines - 1, endY);
+ }
}
}
void InsaneRebel2::drawLine(byte *dst, int pitch, int width, int height, int x0, int y0, int x1, int y1, byte color) {
@@ -1793,23 +2138,20 @@ void InsaneRebel2::procPostRendering(byte *renderBitmap, int32 codecparam, int32
// background persists. FOBJ sprites (enemies) are then decoded on top by SMUSH.
// We do NOT redraw the background here as that would overwrite FOBJ content.
- // --- HUD Drawing Order (from FUN_004089ab assembly analysis) ---
- // 1. FUN_004288c0: Fill status bar background at Y=0xb4 (180)
- // 2. FUN_004089ab: Draw turret overlays, targeting reticle, crosshair
- // 3. FUN_0041c012: Draw status bar sprites LAST (on top)
+ // --- HUD Drawing Order (from FUN_004089ab / FUN_40D836 assembly analysis) ---
+ // Original assembly render order for handler 0x26:
+ // 1. FUN_004288c0: Fill status bar background
+ // 2. FUN_004092d9: Collision/hit processing
+ // 3. FUN_00409fbc: Explosion rendering
+ // 4. FUN_0040ad63: LASER SHOTS (drawn BEFORE cockpit overlays)
+ // 5. FUN_004236e0: Crosshair + cockpit NUT overlays (drawn ON TOP of lasers)
+ // 6. FUN_0041c012: Status bar text/numbers
+ // The cockpit frame covers laser beam edges, giving the appearance
+ // that beams emerge from behind the cockpit.
// STEP 0: Fill status bar background (FUN_004288c0)
renderStatusBarBackground(renderBitmap, pitch, width, height, videoWidth, videoHeight, statusBarY);
- // STEP 1A: Draw NUT-based HUD overlays for Handler 0x26/0x19 (FUN_004089ab lines 195-226)
- renderTurretHudOverlays(renderBitmap, pitch, width, height, curFrame);
-
- // STEP 1B: Draw embedded SAN HUD overlays (from IACT chunks)
- renderEmbeddedHudOverlays(renderBitmap, pitch, width, height);
-
- // STEP 2: Draw DISPFONT.NUT status bar sprites (FUN_0041c012)
- renderStatusBarSprites(renderBitmap, pitch, width, height, statusBarY, curFrame);
-
// Ship rendering (FUN_00401ccf for Handler 8, FUN_0040d836 for Handler 7)
debug("Rebel2 Ship Check: handler=%d shipSprite=%p flyShipSprite=%p shipLevelMode=%d numSprites=%d/%d",
_rebelHandler, (void*)_shipSprite, (void*)_flyShipSprite, _shipLevelMode,
@@ -1826,12 +2168,22 @@ void InsaneRebel2::procPostRendering(byte *renderBitmap, int32 codecparam, int32
// Enemy indicators and destroyed enemy area erase
renderEnemyOverlays(renderBitmap, pitch, width, height, videoWidth);
- // Explosion animations (FUN_409FBC)
+ // Explosion animations (FUN_409FBC) â drawn before lasers in original
renderExplosions(renderBitmap, pitch, width, height);
- // Laser shot beams and impacts
+ // Laser shot beams â drawn BEFORE cockpit/HUD overlays so cockpit covers beam edges
renderLaserShots(renderBitmap, pitch, width, height);
+ // STEP 1A: Draw NUT-based HUD overlays for Handler 0x26/0x19 (FUN_004089ab lines 195-226)
+ // These are cockpit frame, crosshair, and reticle â drawn ON TOP of laser beams
+ renderTurretHudOverlays(renderBitmap, pitch, width, height, curFrame);
+
+ // STEP 1B: Draw embedded SAN HUD overlays (from IACT chunks)
+ renderEmbeddedHudOverlays(renderBitmap, pitch, width, height);
+
+ // STEP 2: Draw DISPFONT.NUT status bar sprites (FUN_0041c012)
+ renderStatusBarSprites(renderBitmap, pitch, width, height, statusBarY, curFrame);
+
// Damage visual effects â handler-specific per original architecture:
// Handler 8: FUN_401CCF line 119 â FUN_00420754 (palette flash + screen shake)
// Handler 0x19: FUN_41DB5E line 192 â FUN_00420562 (palette flash only, every frame)
@@ -3260,16 +3612,16 @@ void InsaneRebel2::renderTurretLaserShots(byte *renderBitmap, int pitch, int wid
switch (_rebelLevelType) {
case 1:
// Type 1: 3 guns (triple cannon configuration)
- // Gun 1: (0x136, 0xaa) = (310, 170)
- // Gun 2: (0xa0, 0x17c) = (160, 380)
- // Gun 3: (0x0a, 0xaa) = (10, 170)
+ // Gun 1: (0x136, 0xaa) = (310, 170) - right
+ // Gun 2: (0xa0, 0x17c) = (160, 380) - center bottom (off-screen, clipped)
+ // Gun 3: (0x0a, 0xaa) = (10, 170) - left
drawLaserBeam(renderBitmap, pitch, width, height,
310 + _viewX, 170 + _viewY, targetX, targetY,
progress, maxDuration, 12, 8, 12);
drawLaserBeam(renderBitmap, pitch, width, height,
160 + _viewX, 380 + _viewY, targetX, targetY,
- progress, maxDuration, 12, 8, 12);
+ progress, maxDuration, 8, 5, 12);
drawLaserBeam(renderBitmap, pitch, width, height,
10 + _viewX, 170 + _viewY, targetX, targetY,
@@ -3281,49 +3633,51 @@ void InsaneRebel2::renderTurretLaserShots(byte *renderBitmap, int pitch, int wid
// Type 2/5: 2 guns (wing cannons)
// Left: (0x6e, 0xe6) = (110, 230)
// Right: (0xd2, 0xe6) = (210, 230)
- // Assembly uses widthScale=0x19(25) for these types
+ // Assembly: widthScale=0x19(25), heightScale=8, thickness=0xC(12)
drawLaserBeam(renderBitmap, pitch, width, height,
110 + _viewX, 230 + _viewY, targetX, targetY,
- progress, maxDuration, 25, 8, 25);
+ progress, maxDuration, 25, 8, 12);
drawLaserBeam(renderBitmap, pitch, width, height,
210 + _viewX, 230 + _viewY, targetX, targetY,
- progress, maxDuration, 25, 8, 25);
+ progress, maxDuration, 25, 8, 12);
break;
case 6:
// Type 6: 2 guns (offscreen - cinematic effect)
// Gun 1: (-100, 0)
// Gun 2: (0, 0)
+ // Assembly: widthScale=0x19(25), heightScale=8, thickness=0xC(12)
drawLaserBeam(renderBitmap, pitch, width, height,
-100 + _viewX, 0 + _viewY, targetX, targetY,
- progress, maxDuration, 25, 8, 25);
+ progress, maxDuration, 25, 8, 12);
drawLaserBeam(renderBitmap, pitch, width, height,
0 + _viewX, 0 + _viewY, targetX, targetY,
- progress, maxDuration, 25, 8, 25);
+ progress, maxDuration, 25, 8, 12);
break;
default:
// Default: 2 guns with alternating pattern based on shot sequence
// When seqNum & 1 == 0: Left (10, 50), Right (310, 130)
// When seqNum & 1 == 1: Left (310, 50), Right (10, 130)
+ // Assembly: widthScale=0x19(25), heightScale=8, thickness=0xC(12)
if ((_turretShots[i].seqNum & 1) == 0) {
drawLaserBeam(renderBitmap, pitch, width, height,
10 + _viewX, 50 + _viewY, targetX, targetY,
- progress, maxDuration, 25, 8, 25);
+ progress, maxDuration, 25, 8, 12);
drawLaserBeam(renderBitmap, pitch, width, height,
310 + _viewX, 130 + _viewY, targetX, targetY,
- progress, maxDuration, 25, 8, 25);
+ progress, maxDuration, 25, 8, 12);
} else {
drawLaserBeam(renderBitmap, pitch, width, height,
310 + _viewX, 50 + _viewY, targetX, targetY,
- progress, maxDuration, 25, 8, 25);
+ progress, maxDuration, 25, 8, 12);
drawLaserBeam(renderBitmap, pitch, width, height,
10 + _viewX, 130 + _viewY, targetX, targetY,
- progress, maxDuration, 25, 8, 25);
+ progress, maxDuration, 25, 8, 12);
}
break;
}
Commit: fd61a9b4f3f746282a632d452af0aaf80d918195
https://github.com/scummvm/scummvm/commit/fd61a9b4f3f746282a632d452af0aaf80d918195
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:15+02:00
Commit Message:
SCUMM: RA2: Guard background copies by buffer pitch
Changed paths:
engines/scumm/insane/insane_rebel_iact.cpp
engines/scumm/smush/smush_player.cpp
engines/scumm/smush/smush_player_ra2.cpp
diff --git a/engines/scumm/insane/insane_rebel_iact.cpp b/engines/scumm/insane/insane_rebel_iact.cpp
index 59fdc9ddcdf..21ca7728202 100644
--- a/engines/scumm/insane/insane_rebel_iact.cpp
+++ b/engines/scumm/insane/insane_rebel_iact.cpp
@@ -52,9 +52,17 @@ void InsaneRebel2::procPreRendering(byte *renderBitmap) {
// positions are erased before new ones are drawn.
//
// This is called at the start of handleFrame(), before any FOBJ chunks are processed.
+ //
+ // IMPORTANT: Only restore when the render buffer pitch matches the background pitch (320).
+ // Levels like Level 12 (Sewers) use oversized buffers (640x260) where FOBJ/FETCH handles
+ // background restoration. Copying the 320-wide background into a 640-wide buffer with
+ // hardcoded pitch=320 would corrupt the corridor rendering.
if (_rebelHandler == 8 && _level2BackgroundLoaded && _level2Background && renderBitmap) {
- for (int y = 0; y < 200; y++) {
- memcpy(renderBitmap + y * 320, _level2Background + y * 320, 320);
+ int bufferPitch = (_player && _player->_width > 0) ? _player->_width : 320;
+ if (bufferPitch == 320) {
+ for (int y = 0; y < 200; y++) {
+ memcpy(renderBitmap + y * 320, _level2Background + y * 320, 320);
+ }
}
}
@@ -1971,12 +1979,21 @@ bool InsaneRebel2::loadLevel2Background(byte *animData, int32 size, byte *render
_level2BackgroundLoaded = true;
foundBackground = true;
- // Copy to render bitmap immediately if provided
+ // Copy to render bitmap immediately if provided.
+ // Only copy when render buffer pitch is 320 (standard screen size).
+ // For oversized buffers (e.g., Level 12's 640x260 corridor),
+ // the FOBJ/FETCH system handles background rendering and copying
+ // 320-wide data into a wider buffer would corrupt the corridor.
if (renderBitmap) {
- for (int by = 0; by < 200; by++) {
- memcpy(renderBitmap + by * 320, _level2Background + by * 320, 320);
+ int bufferPitch = (_player && _player->_width > 0) ? _player->_width : 320;
+ if (bufferPitch == 320) {
+ for (int by = 0; by < 200; by++) {
+ memcpy(renderBitmap + by * 320, _level2Background + by * 320, 320);
+ }
+ debug("Rebel2 loadLevel2Background: Copied to renderBitmap (pitch=%d)", bufferPitch);
+ } else {
+ debug("Rebel2 loadLevel2Background: Skipping renderBitmap copy (pitch=%d != 320)", bufferPitch);
}
- debug("Rebel2 loadLevel2Background: Copied to renderBitmap");
}
}
break;
diff --git a/engines/scumm/smush/smush_player.cpp b/engines/scumm/smush/smush_player.cpp
index 7338951cd68..614cba00a93 100644
--- a/engines/scumm/smush/smush_player.cpp
+++ b/engines/scumm/smush/smush_player.cpp
@@ -391,6 +391,9 @@ void SmushPlayer::handleStore(int32 subSize, Common::SeekableReadStream &b) {
debugC(DEBUG_SMUSH, "SmushPlayer::handleStore()");
assert(subSize >= 4);
_storeFrame = true;
+ if (isRA2()) {
+ debug("SmushPlayer STOR: frame=%d - will store next FOBJ", _frame);
+ }
}
void SmushPlayer::handleFetch(int32 subSize, Common::SeekableReadStream &b) {
@@ -858,6 +861,12 @@ void SmushPlayer::decodeFrameObject(int codec, const uint8 *src, int left, int t
if (_dst == _specialBuffer)
pitch = _width;
+ // Save original FOBJ dimensions before clipping. Codec 37/47 (delta block/glyph)
+ // decode the full frame into the buffer starting at (0,0) regardless of FOBJ
+ // left/top position. They must use the original un-clipped dimensions.
+ int origWidth = width;
+ int origHeight = height;
+
int srcSkipY = 0;
if (isRA2()) {
ra2AdjustFrameCoords(left, top, width, height, pitch, &srcSkipY);
@@ -880,19 +889,22 @@ void SmushPlayer::decodeFrameObject(int codec, const uint8 *src, int left, int t
smushDecodeRLE(_dst, adjustedSrc, left, top, width, height, pitch);
break;
case SMUSH_CODEC_DELTA_BLOCKS:
+ // Codec 37 writes the full frame to dst via memcpy â always uses original
+ // FOBJ dimensions, not position-clipped dimensions.
if (!_deltaBlocksCodec)
- _deltaBlocksCodec = new SmushDeltaBlocksDecoder(width, height);
+ _deltaBlocksCodec = new SmushDeltaBlocksDecoder(origWidth, origHeight);
if (_deltaBlocksCodec)
_deltaBlocksCodec->decode(_dst, src);
break;
case SMUSH_CODEC_DELTA_GLYPHS:
+ // Codec 47 also writes the full frame â use original dimensions.
if (!_deltaGlyphsCodec)
- _deltaGlyphsCodec = new SmushDeltaGlyphsDecoder(width, height);
+ _deltaGlyphsCodec = new SmushDeltaGlyphsDecoder(origWidth, origHeight);
if (_deltaGlyphsCodec)
_deltaGlyphsCodec->decode(_dst, src);
break;
case SMUSH_CODEC_UNCOMPRESSED:
- smushDecodeUncompressed(_dst, src, left, top, width, height, _vm->_screenWidth);
+ smushDecodeUncompressed(_dst, src, left, top, width, height, pitch);
break;
default:
if (isRA2() && ra2DecodeCodec(codec, src, left, top, width, height, pitch, dataSize))
@@ -905,6 +917,25 @@ void SmushPlayer::decodeFrameObject(int codec, const uint8 *src, int left, int t
error("Invalid codec for frame object : %d", codec);
}
+ // RA2 debug: check buffer fill after decode
+ if (isRA2() && _dst == _specialBuffer && _frame < 3) {
+ int nonZero = 0;
+ int total = _width * _height;
+ for (int i = 0; i < total; i++) {
+ if (_dst[i] != 0) nonZero++;
+ }
+ // Sample bottom half
+ int bottomNonZero = 0;
+ int bottomStart = (_height / 2) * _width;
+ int bottomTotal = total - bottomStart;
+ for (int i = bottomStart; i < total; i++) {
+ if (_dst[i] != 0) bottomNonZero++;
+ }
+ debug("SmushPlayer FOBJ decode done: frame=%d codec=%d buf=%dx%d total=%d nonzero=%d (%d%%) bottomHalf=%d/%d (%d%%)",
+ _frame, codec, _width, _height, total, nonZero, (nonZero * 100) / total,
+ bottomNonZero, bottomTotal, bottomTotal > 0 ? (bottomNonZero * 100) / bottomTotal : 0);
+ }
+
if (_storeFrame && !isRA2()) {
if (_frameBuffer == nullptr) {
_frameBuffer = (byte *)malloc(_width * _height);
@@ -964,6 +995,11 @@ void SmushPlayer::handleFrameObject(int32 subSize, Common::SeekableReadStream &b
debugC(DEBUG_SMUSH, "SmushPlayer::handleFrameObject: frame=%d codec=%d pos=(%d,%d) size=%dx%d dataSize=%d",
_frame, codec, left, top, width, height, subSize - 14);
+ if (isRA2()) {
+ debug("SmushPlayer FOBJ: frame=%d codec=%d pos=(%d,%d) size=%dx%d dataSize=%d storeFrame=%d _width=%d _height=%d",
+ _frame, codec, left, top, width, height, subSize - 14, _storeFrame, _width, _height);
+ }
+
int32 chunk_size = subSize - 14;
byte *chunk_buffer = (byte *)malloc(chunk_size);
assert(chunk_buffer);
diff --git a/engines/scumm/smush/smush_player_ra2.cpp b/engines/scumm/smush/smush_player_ra2.cpp
index 63a314fd99f..4c003c662e8 100644
--- a/engines/scumm/smush/smush_player_ra2.cpp
+++ b/engines/scumm/smush/smush_player_ra2.cpp
@@ -158,10 +158,15 @@ void SmushPlayer::ra2HandleFetch(Common::SeekableReadStream &b) {
// Re-decode stored FOBJ data with current offsets (matching original FUN_004246d0).
if (_storedFobjData != nullptr) {
+ debug("SmushPlayer FTCH: Re-decoding stored FOBJ codec=%d pos=(%d,%d) size=%dx%d dataSize=%d",
+ _storedFobjCodec, _storedFobjLeft, _storedFobjTop,
+ _storedFobjWidth, _storedFobjHeight, _storedFobjDataSize);
decodeFrameObject(_storedFobjCodec, _storedFobjData,
_storedFobjLeft, _storedFobjTop,
_storedFobjWidth, _storedFobjHeight,
_storedFobjDataSize);
+ } else {
+ debug("SmushPlayer FTCH: No stored FOBJ data! (frame=%d)", _frame);
}
}
@@ -279,6 +284,9 @@ void SmushPlayer::ra2SelectFrameBuffer(int width, int height) {
_specialBufferSize = bufSize;
_width = width;
_height = height;
+ // Zero-fill the new buffer to avoid garbage in areas not written by FOBJ codec
+ memset(_specialBuffer, 0, bufSize);
+ debug("SmushPlayer: Allocated new _specialBuffer %dx%d (%d bytes)", width, height, bufSize);
}
}
Commit: 15c5136ca5f82edd1670ce23f1a1f1fe7b2adf64
https://github.com/scummvm/scummvm/commit/15c5136ca5f82edd1670ce23f1a1f1fe7b2adf64
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:15+02:00
Commit Message:
SCUMM: RA2: Position ship sprites with viewport offsets
Changed paths:
engines/scumm/insane/insane_rebel_iact.cpp
engines/scumm/insane/insane_rebel_render.cpp
diff --git a/engines/scumm/insane/insane_rebel_iact.cpp b/engines/scumm/insane/insane_rebel_iact.cpp
index 21ca7728202..885ee5d2a59 100644
--- a/engines/scumm/insane/insane_rebel_iact.cpp
+++ b/engines/scumm/insane/insane_rebel_iact.cpp
@@ -652,8 +652,14 @@ void InsaneRebel2::iactRebel2Opcode6(byte *renderBitmap, Common::SeekableReadStr
mouseOffsetY = -127;
// Calculate target positions using the original formula
+ // Original FUN_00401234 lines 151-166:
+ // local_18 = ((mouseX * 5 + 0x27b) * 0x40) / 0xfe -> X target
+ // local_1c = ((mouseY * 5 + 0x27b) * 0x10) / 0xfe -> Y target
+ // _DAT_0043e004 = -local_1c (stored negated for cursor display)
+ // The interpolation (lines 181-193) uses local_1c (positive), NOT _DAT_0043e004.
+ // So the interpolation target must be the positive formula result.
_shipTargetX = (int16)(((mouseOffsetX * 5 + 0x27b) * 0x40) / 0xfe);
- _shipTargetY = (int16)(-((mouseOffsetY * 5 + 0x27b) * 0x10) / 0xfe);
+ _shipTargetY = (int16)(((mouseOffsetY * 5 + 0x27b) * 0x10) / 0xfe);
// Smooth interpolation toward target (max 50 pixels per frame)
const int16 maxStep = 50; // 0x32 in hex
diff --git a/engines/scumm/insane/insane_rebel_render.cpp b/engines/scumm/insane/insane_rebel_render.cpp
index 8fe2735945e..14624911434 100644
--- a/engines/scumm/insane/insane_rebel_render.cpp
+++ b/engines/scumm/insane/insane_rebel_render.cpp
@@ -573,9 +573,9 @@ void InsaneRebel2::spawnHandler25Shot(int x, int y) {
_turretShots[i].seqNum = _turretShotSeqCounter;
_turretShotSeqCounter++;
- // Target position is where player clicked (screen coords)
- _turretShots[i].targetX = x;
- _turretShots[i].targetY = y;
+ // Target position is where player clicked, in buffer coords
+ _turretShots[i].targetX = x + _viewX;
+ _turretShots[i].targetY = y + _viewY;
// Compute gun position from GRD002 character sprite.
// Original uses per-direction lookup tables DAT_004578a6/DAT_004578c6.
@@ -625,11 +625,11 @@ void InsaneRebel2::spawnHandler25Shot(int x, int y) {
int drawX;
if (shouldMirror) {
- drawX = _rebelViewOffset2X + (320 - spriteW - spriteXOffset);
+ drawX = _rebelViewOffset2X + (320 - spriteW - spriteXOffset) + _viewX;
} else {
- drawX = _rebelViewOffset2X + spriteXOffset;
+ drawX = _rebelViewOffset2X + spriteXOffset + _viewX;
}
- int drawY = spriteYOffset + _rebelViewOffset2Y;
+ int drawY = spriteYOffset + _rebelViewOffset2Y + _viewY;
// Gun barrel is approximately at the character's hand level:
// X: center of sprite ± directional offset toward the target
@@ -638,8 +638,8 @@ void InsaneRebel2::spawnHandler25Shot(int x, int y) {
_turretShots[i].gunY = drawY + (spriteH * 3) / 5;
} else {
// Fallback: approximate center-bottom of character area
- _turretShots[i].gunX = _rebelViewOffset2X + 160;
- _turretShots[i].gunY = _rebelViewOffset2Y + 140;
+ _turretShots[i].gunX = _rebelViewOffset2X + 160 + _viewX;
+ _turretShots[i].gunY = _rebelViewOffset2Y + 140 + _viewY;
}
debug("Rebel2 Handler25: Spawned shot %d target (%d,%d) gun (%d,%d)",
@@ -2909,60 +2909,82 @@ void InsaneRebel2::renderHandler7Ship(byte *renderBitmap, int pitch, int width,
void InsaneRebel2::renderHandler8Ship(byte *renderBitmap, int pitch, int width, int height) {
// Handler 8 Ship Rendering (Third-Person On Foot - POV sprites)
// Uses _shipSprite (POV001) with position-based offset
+ //
+ // Original FUN_00401CCF lines 87-94:
+ // FUN_004236e0(bitmap, param_2,
+ // (short)(shipPosX - 0xa0) >> 3, // small X offset
+ // (short)(shipPosY - 0x28) >> 2, // small Y offset
+ // 0, DAT_0047e010, param_5 & 1, 1, 0);
+ //
+ // FUN_004236e0 adds the NUT sprite's internal X/Y offsets to the position
+ // parameters. The sprite's built-in offsets encode where it should appear
+ // on screen (e.g., center for Level 2/11, bottom for Level 12 FPS gun).
if (_rebelHandler != 8 || !_shipSprite || _shipLevelMode == 5)
return;
- // Calculate display offset from raw ship position (FUN_00401ccf lines 88-89)
+ // Small position offsets from dampened ship movement (FUN_00401ccf lines 88-89)
int16 displayOffsetX = (_shipPosX - 0xa0) >> 3;
int16 displayOffsetY = (_shipPosY - 0x28) >> 2;
- // Base screen position (low-res: X=160, Y=105)
- int shipScreenX = 0xa0 + displayOffsetX;
- int shipScreenY = 0x69 + displayOffsetY;
-
int numSprites = _shipSprite->getNumChars();
- int spriteIndex = 0;
-
- // Select sprite based on direction and sprite count
- if (numSprites >= 35) {
- spriteIndex = _shipDirectionH * 7 + _shipDirectionV;
- if (spriteIndex >= numSprites)
- spriteIndex = numSprites - 1;
- } else if (numSprites >= 25) {
- int vDir5 = (_shipDirectionV * 5) / 7;
- spriteIndex = _shipDirectionH * 5 + vDir5;
- if (spriteIndex >= numSprites)
- spriteIndex = numSprites - 1;
- } else if (numSprites >= 5) {
- spriteIndex = _shipDirectionH;
- if (spriteIndex >= numSprites)
- spriteIndex = numSprites - 1;
- } else if (numSprites == 2) {
- spriteIndex = _shipFiring ? 1 : 0;
- }
-
- // Center sprite at position
- int spriteW = _shipSprite->getCharWidth(spriteIndex);
- int spriteH = _shipSprite->getCharHeight(spriteIndex);
- int drawX = shipScreenX - spriteW / 2 + _viewX;
- int drawY = shipScreenY - spriteH / 2 + _viewY;
+ // Original uses param_5 & 1 (firing flag) as sprite index â NOT direction-based
+ int spriteIndex = _shipFiring ? 1 : 0;
+ if (spriteIndex >= numSprites)
+ spriteIndex = 0;
+
+ // FUN_004236e0 adds sprite internal offsets to the x/y parameters.
+ // The internal offsets position the sprite correctly for each level type
+ // (centered for Level 2/11 third-person, bottom for Level 12 FPS).
+ int16 spriteXOffset = _shipSprite->getCharXOffset(spriteIndex);
+ int16 spriteYOffset = _shipSprite->getCharYOffset(spriteIndex);
+ int drawX = displayOffsetX + spriteXOffset + _viewX;
+ int drawY = displayOffsetY + spriteYOffset + _viewY;
renderNutSprite(renderBitmap, pitch, width, height, drawX, drawY, _shipSprite, spriteIndex);
// Shadow sprite (POV004 / DAT_0047e028): drawn at same position as primary ship.
- // Original FUN_401CCF lines 91-92 uses param_5 & 1 (firing flag) as sprite index
- // for both primary and shadow, NOT the direction-based spriteIndex.
+ // Original FUN_401CCF lines 91-92 uses param_5 & 1 (firing flag) as sprite index.
if (_shipSprite2) {
int shadowIndex = _shipFiring ? 1 : 0;
if (shadowIndex < _shipSprite2->getNumChars()) {
- renderNutSprite(renderBitmap, pitch, width, height, drawX, drawY, _shipSprite2, shadowIndex);
- }
- }
-
- debug("Rebel2 Handler8: Ship at (%d,%d) raw(%d,%d) offset(%d,%d) sprite=%d/%d dir=(%d,%d)",
+ int16 shadowXOff = _shipSprite2->getCharXOffset(shadowIndex);
+ int16 shadowYOff = _shipSprite2->getCharYOffset(shadowIndex);
+ int shadowX = displayOffsetX + shadowXOff + _viewX;
+ int shadowY = displayOffsetY + shadowYOff + _viewY;
+ renderNutSprite(renderBitmap, pitch, width, height, shadowX, shadowY, _shipSprite2, shadowIndex);
+ }
+ }
+
+ // Also render ship overlays (POV002 / POV003) if loaded.
+ // These are weapon/character overlays loaded via IACT opcode 8 par4=6,7.
+ if (_shipOverlay1) {
+ int overlayIdx = _shipFiring ? 1 : 0;
+ if (overlayIdx >= _shipOverlay1->getNumChars())
+ overlayIdx = 0;
+ int16 ovlXOff = _shipOverlay1->getCharXOffset(overlayIdx);
+ int16 ovlYOff = _shipOverlay1->getCharYOffset(overlayIdx);
+ int ovlX = displayOffsetX + ovlXOff + _viewX;
+ int ovlY = displayOffsetY + ovlYOff + _viewY;
+ renderNutSprite(renderBitmap, pitch, width, height, ovlX, ovlY, _shipOverlay1, overlayIdx);
+ }
+ if (_shipOverlay2) {
+ int overlayIdx = _shipFiring ? 1 : 0;
+ if (overlayIdx >= _shipOverlay2->getNumChars())
+ overlayIdx = 0;
+ int16 ovlXOff = _shipOverlay2->getCharXOffset(overlayIdx);
+ int16 ovlYOff = _shipOverlay2->getCharYOffset(overlayIdx);
+ int ovlX = displayOffsetX + ovlXOff + _viewX;
+ int ovlY = displayOffsetY + ovlYOff + _viewY;
+ renderNutSprite(renderBitmap, pitch, width, height, ovlX, ovlY, _shipOverlay2, overlayIdx);
+ }
+
+ int sprW = _shipSprite->getCharWidth(spriteIndex);
+ int sprH = _shipSprite->getCharHeight(spriteIndex);
+ debug("Rebel2 Handler8: Ship at (%d,%d) raw(%d,%d) offset(%d,%d) nutOff(%d,%d) size(%d,%d) bottom=%d view(%d,%d) sprite=%d/%d",
drawX, drawY, _shipPosX, _shipPosY, displayOffsetX, displayOffsetY,
- spriteIndex, numSprites, _shipDirectionH, _shipDirectionV);
+ spriteXOffset, spriteYOffset, sprW, sprH, drawY + sprH - _viewY,
+ _viewX, _viewY, spriteIndex, numSprites);
}
// Handler 25: Draw GRD001 (wall/cockpit overlay) in procPostRendering.
@@ -2979,9 +3001,10 @@ void InsaneRebel2::renderHandler25ShipPre(byte *renderBitmap, int pitch, int wid
if (!_grd001Sprite || _grd001Sprite->getNumChars() <= 0)
return;
- // CRITICAL: Clip height to 180 (0xb4) to avoid drawing over status bar
- const int clipHeight = 180;
- int renderHeight = MIN(height, clipHeight);
+ // CRITICAL: Clip height to 180 (0xb4) + viewport Y to avoid drawing over status bar.
+ // For oversized buffers (e.g., Level 12's 640x260), the status bar is at
+ // Y = 180 + _viewY in buffer coordinates.
+ int renderHeight = MIN(height, 180 + _viewY);
// Draw _grd001Sprite based on _grdSpriteMode (DAT_00457900)
// Each mode has specific conditions from FUN_0041db5e:
@@ -3015,8 +3038,12 @@ void InsaneRebel2::renderHandler25ShipPre(byte *renderBitmap, int pitch, int wid
int16 spriteXOffset = _grd001Sprite->getCharXOffset(0);
int16 spriteYOffset = _grd001Sprite->getCharYOffset(0);
- int drawX = _rebelViewOffset2X + spriteXOffset;
- int drawY = _rebelViewOffset2Y + spriteYOffset;
+ // Add viewport offset so sprite follows the visible area.
+ // For 320x200 buffers (Level 2), _viewX/_viewY are 0 â no change.
+ // For oversized buffers (Level 12's 640x260), the viewport scrolls
+ // and sprites must be drawn at the correct position within it.
+ int drawX = _rebelViewOffset2X + spriteXOffset + _viewX;
+ int drawY = _rebelViewOffset2Y + spriteYOffset + _viewY;
// Apply width-halving logic from original assembly:
// When damage==0 (uncovered), the original halves DAT_00482234 (buffer width)
@@ -3053,9 +3080,8 @@ void InsaneRebel2::renderHandler25Ship(byte *renderBitmap, int pitch, int width,
if (_rebelHandler != 25)
return;
- // CRITICAL: Clip height to 180 (0xb4) to avoid drawing over status bar
- const int clipHeight = 180;
- int renderHeight = MIN(height, clipHeight);
+ // CRITICAL: Clip height to 180 (0xb4) + viewport Y to avoid drawing over status bar.
+ int renderHeight = MIN(height, 180 + _viewY);
// _grd002Sprite (GRD002) is always drawn if it exists (from FUN_41DB5E line 230)
// The sprite index is calculated based on damage level and aiming position
@@ -3167,16 +3193,16 @@ void InsaneRebel2::renderHandler25Ship(byte *renderBitmap, int pitch, int width,
if (shouldMirror) {
// Mirrored position: X = DAT_00457910 + (320 - sprite_width - sprite_x_offset)
// From assembly lines 240-243
- drawX = _rebelViewOffset2X + (320 - spriteW - spriteXOffset);
+ drawX = _rebelViewOffset2X + (320 - spriteW - spriteXOffset) + _viewX;
} else {
// Normal position: X = DAT_00457910 + sprite_internal_x_offset
// From assembly line 238
- drawX = _rebelViewOffset2X + spriteXOffset;
+ drawX = _rebelViewOffset2X + spriteXOffset + _viewX;
}
// Y = sprite_internal_y_offset + DAT_00457912
// From assembly line 246
- drawY = spriteYOffset + _rebelViewOffset2Y;
+ drawY = spriteYOffset + _rebelViewOffset2Y + _viewY;
renderNutSpriteMirrored(renderBitmap, pitch, width, renderHeight, drawX, drawY, _grd002Sprite, spriteIdx, shouldMirror);
Commit: 972dba7f591dc585cd5077eadb93432d52bb898c
https://github.com/scummvm/scummvm/commit/972dba7f591dc585cd5077eadb93432d52bb898c
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:15+02:00
Commit Message:
SCUMM: RA2: Fix Escape key bug
Changed paths:
engines/scumm/insane/insane_rebel.cpp
diff --git a/engines/scumm/insane/insane_rebel.cpp b/engines/scumm/insane/insane_rebel.cpp
index d541d1aa47d..934346aae24 100644
--- a/engines/scumm/insane/insane_rebel.cpp
+++ b/engines/scumm/insane/insane_rebel.cpp
@@ -518,7 +518,8 @@ bool InsaneRebel2::notifyEvent(const Common::Event &event) {
case Common::KEYCODE_ESCAPE:
// ESC handling depends on game state:
// - In menus: Select quit option and confirm
- // - During gameplay/cutscenes: Skip video
+ // - During gameplay: Pause and open ScummVM menu
+ // - During cutscenes/intros: Skip video
if (splayer) {
if (_menuInputActive && (_gameState == kStateMainMenu ||
_gameState == kStatePilotSelect ||
@@ -529,10 +530,23 @@ bool InsaneRebel2::notifyEvent(const Common::Event &event) {
_menuSelection = _menuItemCount - 1; // Select last item (quit/back)
_menuSelectionConfirmed = true;
debug("Rebel2: ESC pressed in menu - selecting quit (item %d)", _menuSelection);
+ _vm->_smushVideoShouldFinish = true;
+ } else if (_gameState == kStateGameplay && _rebelHandler != 0) {
+ // During active gameplay (handler != 0): pause and open ScummVM menu.
+ // _rebelHandler is non-zero (7, 8, 0x19, 0x26) only during interactive
+ // gameplay sections, and 0 during intro/cutscene/post videos within a level.
+ debug("Rebel2: ESC pressed during gameplay - opening ScummVM menu");
+ bool wasPaused = splayer->_paused;
+ if (!wasPaused)
+ splayer->pause();
+ _vm->openMainMenuDialog();
+ if (!wasPaused)
+ splayer->unpause();
} else {
+ // During cutscenes/intros/mission briefings: skip video
debug("Rebel2: ESC pressed - skipping video");
+ _vm->_smushVideoShouldFinish = true;
}
- _vm->_smushVideoShouldFinish = true;
return true; // Consume the event
}
break;
Commit: ce750db5b1126cb6f6b4c136d2cc9bed06401bde
https://github.com/scummvm/scummvm/commit/ce750db5b1126cb6f6b4c136d2cc9bed06401bde
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:16+02:00
Commit Message:
SCUMM: RA2: Use a large enough bitmap
Changed paths:
engines/scumm/insane/insane.cpp
engines/scumm/insane/insane.h
engines/scumm/insane/insane_rebel.cpp
diff --git a/engines/scumm/insane/insane.cpp b/engines/scumm/insane/insane.cpp
index e4ea4e0bece..8560e42394a 100644
--- a/engines/scumm/insane/insane.cpp
+++ b/engines/scumm/insane/insane.cpp
@@ -184,7 +184,7 @@ void Insane::initvars() {
for (j = 0; j < 9; j++)
_enHdlVar[i][j] = 0;
- for (i = 0; i < 0x200; i++)
+ for (i = 0; i < 0x401; i++)
_iactBits[i] = 0;
diff --git a/engines/scumm/insane/insane.h b/engines/scumm/insane/insane.h
index 85647829a36..d8dddfeb305 100644
--- a/engines/scumm/insane/insane.h
+++ b/engines/scumm/insane/insane.h
@@ -158,7 +158,7 @@ public:
int16 _smush_frameNum2;
byte _smush_earlyFluContents[0x31a];
int16 _enemyState[10][10];
- byte _iactBits[0x200];
+ byte _iactBits[0x401];
int16 _mainRoadPos;
int16 _posBrokenCar;
int16 _posBrokenTruck;
diff --git a/engines/scumm/insane/insane_rebel.cpp b/engines/scumm/insane/insane_rebel.cpp
index 934346aae24..bf3b1c32463 100644
--- a/engines/scumm/insane/insane_rebel.cpp
+++ b/engines/scumm/insane/insane_rebel.cpp
@@ -252,7 +252,7 @@ InsaneRebel2::InsaneRebel2(ScummEngine_v7 *scumm) {
for (j = 0; j < 9; j++)
_enHdlVar[i][j] = 0;
- for (i = 0; i < 0x200; i++)
+ for (i = 0; i < 0x401; i++)
_iactBits[i] = 0;
for (i = 0; i < 512; i++) {
@@ -1124,7 +1124,7 @@ bool InsaneRebel2::isBitSet(int n) {
if (n < 1) {
return false;
}
- assert (n < 0x200);
+ assert (n < 0x401);
return (_iactBits[n] != 0);
}
@@ -1133,11 +1133,11 @@ void InsaneRebel2::setBit(int n) {
// FUN_004239b0: When n < 1 (i.e., n == 0 or negative), set ALL bits to 1 (disable all objects)
// This is used to disable all enemies/objects at once
if (n < 1) {
- for (int i = 0; i < 0x200; i++)
+ for (int i = 0; i < 0x401; i++)
_iactBits[i] = 1;
return;
}
- assert (n < 0x200);
+ assert (n < 0x401);
_iactBits[n] = 1;
}
@@ -1146,11 +1146,11 @@ void InsaneRebel2::clearBit(int n) {
// This is called by FUN_00423880 at the start of video playback to reset the bit table,
// ensuring all enemies are visible when a new level/segment starts.
if (n < 1) {
- for (int i = 0; i < 0x200; i++)
+ for (int i = 0; i < 0x401; i++)
_iactBits[i] = 0;
return;
}
- assert (n < 0x200);
+ assert (n < 0x401);
_iactBits[n] = 0;
}
Commit: f72c255c6ad4a20a6a10703e8647d6f2bfa9ed96
https://github.com/scummvm/scummvm/commit/f72c255c6ad4a20a6a10703e8647d6f2bfa9ed96
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:16+02:00
Commit Message:
SCUMM: RA2: Fix crash in smushDecodeSkipRLE
Changed paths:
engines/scumm/smush/codec_ra2.cpp
engines/scumm/smush/smush_player_ra2.cpp
diff --git a/engines/scumm/smush/codec_ra2.cpp b/engines/scumm/smush/codec_ra2.cpp
index 7cda7943b59..7b9309f8d51 100644
--- a/engines/scumm/smush/codec_ra2.cpp
+++ b/engines/scumm/smush/codec_ra2.cpp
@@ -94,26 +94,33 @@ void smushDecodeLineUpdate(byte *dst, const byte *src, int left, int top, int wi
* Format: Each line has 2-byte size, then (skip, runSize, RLE_data) triplets.
* Note: Skip regions preserve previous frame content (delta compression).
*/
-void smushDecodeSkipRLE(byte *dst, const byte *src, int left, int top, int width, int height, int pitch) {
+void smushDecodeSkipRLE(byte *dst, const byte *src, int left, int top, int width, int height, int pitch, int dataSize) {
dst += top * pitch + left;
+ const byte *srcEnd = src + dataSize;
for (int row = 0; row < height; row++) {
+ if (src + 2 > srcEnd)
+ break;
int lineDataSize = READ_LE_UINT16(src);
src += 2;
const byte *lineEnd = src + lineDataSize;
+ if (lineEnd > srcEnd)
+ lineEnd = srcEnd;
byte *lineDst = dst;
int x = 0;
- while (src < lineEnd && x < width) {
+ while (src + 2 <= lineEnd && x < width) {
int skip = READ_LE_UINT16(src);
src += 2;
x += skip; // Skip preserves previous frame content
- if (src >= lineEnd || x >= width)
+ if (src + 2 > lineEnd || x >= width)
break;
int runSize = READ_LE_UINT16(src);
src += 2;
const byte *runEnd = src + runSize;
+ if (runEnd > lineEnd)
+ runEnd = lineEnd;
// Decode RLE within this run - write ALL colors including 0
while (src < runEnd && x < width) {
diff --git a/engines/scumm/smush/smush_player_ra2.cpp b/engines/scumm/smush/smush_player_ra2.cpp
index 4c003c662e8..913efd064a8 100644
--- a/engines/scumm/smush/smush_player_ra2.cpp
+++ b/engines/scumm/smush/smush_player_ra2.cpp
@@ -46,7 +46,7 @@ bool SmushPlayer::isRA2() const {
// Forward declarations for RA2 codec functions (defined in codec_ra2.cpp)
void smushDecodeLineUpdate(byte *dst, const byte *src, int left, int top, int width, int height, int pitch);
-void smushDecodeSkipRLE(byte *dst, const byte *src, int left, int top, int width, int height, int pitch);
+void smushDecodeSkipRLE(byte *dst, const byte *src, int left, int top, int width, int height, int pitch, int dataSize);
void smushDecodeRA2Bomp(byte *dst, const byte *src, int left, int top, int width, int height, int pitch, int dataSize);
/**
@@ -350,7 +350,7 @@ bool SmushPlayer::ra2DecodeCodec(int codec, const uint8 *src, int left, int top,
smushDecodeLineUpdate(_dst, src, left, top, width, height, pitch);
return true;
case SMUSH_CODEC_SKIP_RLE:
- smushDecodeSkipRLE(_dst, src, left, top, width, height, pitch);
+ smushDecodeSkipRLE(_dst, src, left, top, width, height, pitch, dataSize);
return true;
case SMUSH_CODEC_RA2_BOMP:
smushDecodeRA2Bomp(_dst, src, left, top, width, height, pitch, dataSize);
Commit: 0ef613d4c1c42bfde888ce477999615d43b8a8a7
https://github.com/scummvm/scummvm/commit/0ef613d4c1c42bfde888ce477999615d43b8a8a7
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:16+02:00
Commit Message:
SCUMM: RA2: Update death handling and level progression
Changed paths:
engines/scumm/insane/insane_rebel_render.cpp
engines/scumm/scumm.cpp
diff --git a/engines/scumm/insane/insane_rebel_render.cpp b/engines/scumm/insane/insane_rebel_render.cpp
index 14624911434..04e50800ee3 100644
--- a/engines/scumm/insane/insane_rebel_render.cpp
+++ b/engines/scumm/insane/insane_rebel_render.cpp
@@ -1963,6 +1963,17 @@ void InsaneRebel2::procPostRendering(byte *renderBitmap, int32 codecparam, int32
_player->setScrollOffset(_viewX, _viewY);
+ // Death check: original game (FUN_417E53 line 25) exits video playback
+ // when DAT_0047a7ec >= 0xff (damage accumulator reaches 255).
+ // Sync _playerShield from _playerDamage and break out of video on death.
+ if (_rebelHandler != 0) {
+ _playerShield = 255 - _playerDamage;
+ if (_playerShield <= 0) {
+ _playerShield = 0;
+ _vm->_smushVideoShouldFinish = true;
+ }
+ }
+
// --- HUD Drawing Order (from FUN_004089ab assembly analysis) ---
// Based on FUN_004089ab:
// 1. Line 156: FUN_004288c0 fills status bar background at Y=0xb4 (180)
diff --git a/engines/scumm/scumm.cpp b/engines/scumm/scumm.cpp
index ec0e19e74e2..8f09594eaa2 100644
--- a/engines/scumm/scumm.cpp
+++ b/engines/scumm/scumm.cpp
@@ -2725,20 +2725,33 @@ Common::Error ScummEngine::go() {
int selectedLevel = rebel->_selectedChapter + 1;
debug("ScummEngine: Starting chapter %d (level %d)", rebel->_selectedChapter + 1, selectedLevel);
- // Run the complete level (handles BEG, gameplay, END/DIE/RETRY/OVER)
- int result = rebel->runLevel(selectedLevel);
-
- // Save pilot progress after level completion
- if (result == InsaneRebel2::kLevelNextLevel) {
- rebel->updatePilotProgress(selectedLevel - 1,
- rebel->_playerScore, rebel->_playerLives, rebel->_playerDamage);
+ // Level progression loop: on success, advance to next level
+ // Original game chains levels directly (e.g. FUN_0040598c(FUN_00418063,0))
+ while (!shouldQuit() && selectedLevel >= 1 && selectedLevel <= 15) {
+ int result = rebel->runLevel(selectedLevel);
+
+ if (result == InsaneRebel2::kLevelNextLevel) {
+ rebel->updatePilotProgress(selectedLevel - 1,
+ rebel->_playerScore, rebel->_playerLives, rebel->_playerDamage);
+ selectedLevel++;
+ if (selectedLevel > 15) {
+ // Beat the game â play credits
+ rebel->playCreditsSequence();
+ break;
+ }
+ } else {
+ // kLevelGameOver, kLevelQuit, kLevelReturnToMenu â back to menu
+ if (shouldQuit() || result == InsaneRebel2::kLevelQuit) {
+ // Propagate quit to outer loop
+ break;
+ }
+ break;
+ }
}
- if (shouldQuit() || result == InsaneRebel2::kLevelQuit) {
+ if (shouldQuit()) {
break;
}
-
- // After level completion or game over, return to menu
}
// If kChapterSelectBack, loop back to main menu
}
Commit: 825c3c8547af7b2e528d9e535813629d7f91123c
https://github.com/scummvm/scummvm/commit/825c3c8547af7b2e528d9e535813629d7f91123c
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:17+02:00
Commit Message:
SCUMM: RA2: Improve sound handling
Changed paths:
engines/scumm/insane/insane_rebel.cpp
engines/scumm/insane/insane_rebel.h
engines/scumm/insane/insane_rebel_audio.cpp
engines/scumm/insane/insane_rebel_iact.cpp
diff --git a/engines/scumm/insane/insane_rebel.cpp b/engines/scumm/insane/insane_rebel.cpp
index bf3b1c32463..3c2e245b9f3 100644
--- a/engines/scumm/insane/insane_rebel.cpp
+++ b/engines/scumm/insane/insane_rebel.cpp
@@ -389,6 +389,12 @@ InsaneRebel2::InsaneRebel2(ScummEngine_v7 *scumm) {
}
loadSfx();
+ // Initialize auxiliary sound buffers (4 Ã 30000 bytes, loaded from IACT stream)
+ for (i = 0; i < kRA2NumAuxSfx; i++) {
+ _auxSfxData[i] = (byte *)calloc(kRA2AuxBufSize, 1);
+ _auxSfxSize[i] = 0;
+ }
+
// Initialize menu system
_gameState = kStateMainMenu; // Start at main menu
_menuSelection = 0; // First item selected
@@ -1089,15 +1095,19 @@ int32 InsaneRebel2::processMouse() {
}
}
- // Play explosion sound (FUN_0041189e).
+ // Play enemy death sound.
// Pan based on enemy center X position: (screenX - 160) mapped to [-127,127]
{
int enemyCenterX = (it->rect.left + it->rect.right) / 2 - _viewX;
int sfxPan = CLIP((enemyCenterX - 160) * 127 / 160, -127, 127);
if (_rebelHandler == 8 && it->type >= 1 && it->type <= 4) {
- playSfx(0, 127, sfxPan); // BLAST.SAD for handler 8 types 1-4
+ // Handler 8 soldier types 1-4: play from auxiliary buffer 0
+ // Original: FUN_00411931(0, slot+3, 0x7f, pan, 0)
+ playAuxSfx(0, 127, sfxPan);
} else {
- playSfx(2, 127, sfxPan); // EXPLODE.SAD for all other enemies
+ // All other enemies: EXPLODE.SAD
+ // Original: FUN_0041189e(2, slot+3, 0x7f, pan, 0)
+ playSfx(2, 127, sfxPan);
}
}
diff --git a/engines/scumm/insane/insane_rebel.h b/engines/scumm/insane/insane_rebel.h
index 43734d7398b..0649f799e66 100644
--- a/engines/scumm/insane/insane_rebel.h
+++ b/engines/scumm/insane/insane_rebel.h
@@ -1134,6 +1134,23 @@ public:
// slot: 0-7 (SAD file index), volume: 0-127, pan: -127..+127
void playSfx(int slot, int volume, int pan);
+ // ========== Auxiliary Sound Buffers ==========
+ // 4 pre-allocated buffers (30000 bytes each) loaded from IACT stream data.
+ // Original: DAT_00480308[0..3], loaded via FUN_004118df, played via FUN_00411931.
+ // Used for embedded sound effects (e.g., soldier death sounds in handler 8 levels).
+ static const int kRA2NumAuxSfx = 4;
+ static const int kRA2AuxBufSize = 30000;
+
+ byte *_auxSfxData[kRA2NumAuxSfx]; // Pre-allocated buffer pointers
+ uint32 _auxSfxSize[kRA2NumAuxSfx]; // Current data size in each buffer
+ Audio::SoundHandle _auxSfxHandles[kRA2NumAuxSfx]; // Mixer handles
+
+ // Load sound data into auxiliary buffer (FUN_004118df equivalent)
+ void loadAuxSfx(int buffer, const byte *data, uint32 size);
+
+ // Play from auxiliary buffer (FUN_00411931 equivalent)
+ void playAuxSfx(int buffer, int volume, int pan);
+
};
} // End of namespace Insane
diff --git a/engines/scumm/insane/insane_rebel_audio.cpp b/engines/scumm/insane/insane_rebel_audio.cpp
index 01f0be50075..7b971f8eea1 100644
--- a/engines/scumm/insane/insane_rebel_audio.cpp
+++ b/engines/scumm/insane/insane_rebel_audio.cpp
@@ -341,6 +341,12 @@ void InsaneRebel2::freeSfx() {
_sfxData[i] = nullptr;
_sfxSize[i] = 0;
}
+ for (int i = 0; i < kRA2NumAuxSfx; i++) {
+ _vm->_mixer->stopHandle(_auxSfxHandles[i]);
+ free(_auxSfxData[i]);
+ _auxSfxData[i] = nullptr;
+ _auxSfxSize[i] = 0;
+ }
}
void InsaneRebel2::playSfx(int slot, int volume, int pan) {
@@ -371,4 +377,65 @@ void InsaneRebel2::playSfx(int slot, int volume, int pan) {
debug(5, "InsaneRebel2::playSfx: slot=%d vol=%d pan=%d size=%d", slot, volume, pan, _sfxSize[slot]);
}
+void InsaneRebel2::loadAuxSfx(int buffer, const byte *data, uint32 size) {
+ if (buffer < 0 || buffer >= kRA2NumAuxSfx || !data || size == 0) {
+ return;
+ }
+ if ((int)size > kRA2AuxBufSize) {
+ debug("InsaneRebel2::loadAuxSfx: buffer %d size %d exceeds max %d, truncating",
+ buffer, size, kRA2AuxBufSize);
+ size = kRA2AuxBufSize;
+ }
+
+ memcpy(_auxSfxData[buffer], data, size);
+ _auxSfxSize[buffer] = size;
+
+ debug(5, "InsaneRebel2::loadAuxSfx: buffer=%d size=%d", buffer, size);
+}
+
+void InsaneRebel2::playAuxSfx(int buffer, int volume, int pan) {
+ if (buffer < 0 || buffer >= kRA2NumAuxSfx || !_auxSfxData[buffer] || _auxSfxSize[buffer] == 0) {
+ return;
+ }
+
+ _vm->_mixer->stopHandle(_auxSfxHandles[buffer]);
+
+ // The auxiliary buffer data goes through FUN_00425fc0 (format dispatch) in the original.
+ // Check if data has SAUD header; if so, extract PCM from SDAT chunk.
+ // Otherwise treat as raw 8-bit unsigned PCM at 11025 Hz.
+ const byte *pcmStart = _auxSfxData[buffer];
+ uint32 pcmSize = _auxSfxSize[buffer];
+
+ if (pcmSize > 8 && READ_BE_UINT32(pcmStart) == MKTAG('S', 'A', 'U', 'D')) {
+ // Parse SAUD container to find SDAT chunk
+ uint32 pos = 8; // Skip SAUD tag + size
+ while (pos + 8 <= pcmSize) {
+ uint32 chunkTag = READ_BE_UINT32(pcmStart + pos);
+ uint32 chunkSize = READ_BE_UINT32(pcmStart + pos + 4);
+ if (chunkTag == MKTAG('S', 'D', 'A', 'T')) {
+ pcmStart = pcmStart + pos + 8;
+ pcmSize = MIN(chunkSize, pcmSize - pos - 8);
+ break;
+ }
+ pos += 8 + chunkSize;
+ }
+ }
+
+ byte *pcmCopy = (byte *)malloc(pcmSize);
+ if (!pcmCopy) {
+ return;
+ }
+ memcpy(pcmCopy, pcmStart, pcmSize);
+
+ Audio::SeekableAudioStream *stream = Audio::makeRawStream(
+ pcmCopy, pcmSize, 11025, Audio::FLAG_UNSIGNED, DisposeAfterUse::YES);
+
+ int scaledVolume = (volume * Audio::Mixer::kMaxChannelVolume) / 127;
+
+ _vm->_mixer->playStream(Audio::Mixer::kSFXSoundType, &_auxSfxHandles[buffer],
+ stream, -1, scaledVolume, pan);
+
+ debug(5, "InsaneRebel2::playAuxSfx: buffer=%d vol=%d pan=%d pcmSize=%d", buffer, volume, pan, pcmSize);
+}
+
} // End of namespace Scumm
diff --git a/engines/scumm/insane/insane_rebel_iact.cpp b/engines/scumm/insane/insane_rebel_iact.cpp
index 885ee5d2a59..f1d3e192218 100644
--- a/engines/scumm/insane/insane_rebel_iact.cpp
+++ b/engines/scumm/insane/insane_rebel_iact.cpp
@@ -1515,10 +1515,43 @@ void InsaneRebel2::iactRebel2Opcode8(byte *renderBitmap, Common::SeekableReadStr
return;
}
- // ===== Sound Loading (par3 21-47) =====
- if (par3 >= 21 && par3 <= 47) {
- debug("Rebel2 Opcode 8: Sound loading subcase %d (not implemented)", par3);
- // TODO: Implement sound loading via FUN_004118df equivalent
+ // ===== Auxiliary Sound Buffer Loading (par4 20-47) =====
+ // FUN_401234 case 6 (handler 8): par4 0x14-0x1b (20-27) â aux buffer 0
+ // FUN_41CADB case 6 (handler 25): par4 0x15-0x1b (21-27) â aux buffer 0,
+ // 0x1f-0x25 (31-37) â aux buffer 1, 0x28 (40) â aux buffer 3,
+ // 0x29-0x2f (41-47) â aux buffer 2
+ // Data layout: offset 14 = uint32 data size, offset 18 = PCM data start.
+ // Stream is at offset 8 (after par1-par4), so data size at +6, PCM at +10.
+ if (par4 >= 20 && par4 <= 47) {
+ int auxBuffer = -1;
+ if (par4 >= 20 && par4 <= 27) {
+ auxBuffer = 0;
+ } else if (par4 >= 31 && par4 <= 37) {
+ auxBuffer = 1;
+ } else if (par4 == 40) {
+ auxBuffer = 3;
+ } else if (par4 >= 41 && par4 <= 47) {
+ auxBuffer = 2;
+ }
+
+ if (auxBuffer >= 0 && remaining >= 10) {
+ b.seek(startPos + 6); // Skip to data size field (byte offset 14 from IACT start)
+ uint32 dataSize = b.readUint32LE();
+ if (dataSize > 0 && remaining >= (int64)(10 + dataSize)) {
+ byte *soundData = (byte *)malloc(dataSize);
+ if (soundData) {
+ b.read(soundData, dataSize);
+ loadAuxSfx(auxBuffer, soundData, dataSize);
+ free(soundData);
+ debug("Rebel2 Opcode 8: Loaded %d bytes into aux sound buffer %d (par4=%d)",
+ dataSize, auxBuffer, par4);
+ }
+ } else {
+ debug("Rebel2 Opcode 8: Aux sound par4=%d dataSize=%d exceeds remaining=%lld",
+ par4, dataSize, (long long)remaining);
+ }
+ }
+ b.seek(startPos);
return;
}
Commit: 589580d5b681e77f9dfbce55121c3790231b2280
https://github.com/scummvm/scummvm/commit/589580d5b681e77f9dfbce55121c3790231b2280
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:17+02:00
Commit Message:
SCUMM: RA2: Fix handler 25 ship rendering
Changed paths:
engines/scumm/insane/insane_rebel_render.cpp
diff --git a/engines/scumm/insane/insane_rebel_render.cpp b/engines/scumm/insane/insane_rebel_render.cpp
index 04e50800ee3..ce964bbb20e 100644
--- a/engines/scumm/insane/insane_rebel_render.cpp
+++ b/engines/scumm/insane/insane_rebel_render.cpp
@@ -35,8 +35,9 @@
namespace Scumm {
-// External codec functions from codec1.cpp
+// External codec functions from codec1.cpp / codec_ra2.cpp
extern void smushDecodeRLE(byte *dst, const byte *src, int left, int top, int width, int height, int pitch);
+extern void smushDecodeRLEOpaque(byte *dst, const byte *src, int left, int top, int width, int height, int pitch);
extern void smushDecodeUncompressed(byte *dst, const byte *src, int left, int top, int width, int height, int pitch);
@@ -2171,9 +2172,6 @@ void InsaneRebel2::procPostRendering(byte *renderBitmap, int32 codecparam, int32
renderHandler7Ship(renderBitmap, pitch, width, height);
renderHandler8Ship(renderBitmap, pitch, width, height);
- // GRD001 (wall/cockpit) drawn AFTER FOBJs per original FUN_0041DB5E lines 202-210
- renderHandler25ShipPre(renderBitmap, pitch, width, height);
- renderHandler25Ship(renderBitmap, pitch, width, height);
renderFallbackShip(renderBitmap, pitch, width, height);
// Enemy indicators and destroyed enemy area erase
@@ -2185,6 +2183,15 @@ void InsaneRebel2::procPostRendering(byte *renderBitmap, int32 codecparam, int32
// Laser shot beams â drawn BEFORE cockpit/HUD overlays so cockpit covers beam edges
renderLaserShots(renderBitmap, pitch, width, height);
+ // Handler 25 GRD sprites drawn AFTER enemies/explosions/lasers per original FUN_0041DB5E:
+ // Line 193: FUN_0041f29a (enemies)
+ // Line 194: FUN_0041e7c2 (explosions)
+ // Line 201: FUN_0041f004 (lasers)
+ // Lines 202-229: GRD001 (wall, opaque, covers enemies behind wall)
+ // Lines 230-248: GRD002 (character, transparent, drawn last)
+ renderHandler25ShipPre(renderBitmap, pitch, width, height);
+ renderHandler25Ship(renderBitmap, pitch, width, height);
+
// STEP 1A: Draw NUT-based HUD overlays for Handler 0x26/0x19 (FUN_004089ab lines 195-226)
// These are cockpit frame, crosshair, and reticle â drawn ON TOP of laser beams
renderTurretHudOverlays(renderBitmap, pitch, width, height, curFrame);
@@ -3073,6 +3080,11 @@ void InsaneRebel2::renderHandler25ShipPre(byte *renderBitmap, int pitch, int wid
}
}
+ // GRD001 uses transparent rendering (color 0 = transparent).
+ // The original uses flags=0 (opaque) in FUN_004236e0, but the NUT pre-decode
+ // already fills color-0 positions with kDefaultTransparentColor (0).
+ // Using transparent rendering here lets the corridor show through any
+ // color-0 border/padding pixels in GRD001, avoiding black-over-corridor artifacts.
renderNutSprite(dstBitmap, pitch, renderWidth, renderHeight, drawX, drawY, _grd001Sprite, 0);
debug("Rebel2 Handler25 PRE: GRD001 at (%d,%d) nutOff(%d,%d) viewOff(%d,%d) size(%d,%d) mode=%d dmg=%d halfW=%d rightHalf=%d renderW=%d",
Commit: a97db6f7fdba93e50719f5b759d5addd2d28aa69
https://github.com/scummvm/scummvm/commit/a97db6f7fdba93e50719f5b759d5addd2d28aa69
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:17+02:00
Commit Message:
SCUMM: RA2: Improve shot beam rendering
Changed paths:
engines/scumm/insane/insane_rebel.cpp
engines/scumm/insane/insane_rebel.h
engines/scumm/insane/insane_rebel_iact.cpp
engines/scumm/insane/insane_rebel_render.cpp
diff --git a/engines/scumm/insane/insane_rebel.cpp b/engines/scumm/insane/insane_rebel.cpp
index 3c2e245b9f3..98f1e4c1580 100644
--- a/engines/scumm/insane/insane_rebel.cpp
+++ b/engines/scumm/insane/insane_rebel.cpp
@@ -370,6 +370,9 @@ InsaneRebel2::InsaneRebel2(ScummEngine_v7 *scumm) {
_grd001Sprite = nullptr; // DAT_00482240 - GRD001 primary ship
_grd002Sprite = nullptr; // DAT_00482238 - GRD002 secondary ship
_grdSpriteMode = 0; // DAT_00457900 - sprite mode (1,2,3,4)
+ memset(_grdShotOriginX, 0, sizeof(_grdShotOriginX));
+ memset(_grdShotOriginY, 0, sizeof(_grdShotOriginY));
+ _grdShotOriginTableLoaded = false;
// Initialize Handler 0x26 turret HUD overlay system
_hudOverlayNut = nullptr; // DAT_0047fe78 - Primary HUD overlay (GRD files, animated)
@@ -605,12 +608,12 @@ bool InsaneRebel2::notifyEvent(const Common::Event &event) {
// Per-level difficulty parameters extracted from RA2WIN95.EXE at VA 0x47e0f0
// Original: 2D table indexed by DAT_0047a7fa (difficulty) * 0x242 + DAT_0047a7f8 (levelType) * 0x22
-// 17 fields per entry (34 bytes), 17 entries per difficulty, 5 difficulties
+// 17 fields per entry (34 bytes), 17 entries per difficulty, 6 difficulties
// Field names from official Difficulty Editor: {laserDelay, snapDistance, missDamage, dodgeDamage,
// shotDamage, specialDamage, shotAccuracy, hitPoints, dodgePoints, timePoints,
// levelPoints, specialPoints, flags, rollRate, liftRate, slideRate, driftRate}
// -1 = not applicable for this level type
-const InsaneRebel2::LevelDifficultyParams InsaneRebel2::kDifficultyTable[5][17] = {
+const InsaneRebel2::LevelDifficultyParams InsaneRebel2::kDifficultyTable[6][17] = {
// Difficulty 0 (Beginner) - 17 level types
{
{ 5, 3, 15, -1, 2, -1, 75, 25, -1, 2, 500, 250, 8, 5, 5, 6, -1}, // Lv1
@@ -631,7 +634,7 @@ const InsaneRebel2::LevelDifficultyParams InsaneRebel2::kDifficultyTable[5][17]
{ 5, 8, -1, -1, 3, -1, 75, 25, -1, 2, 500, -1, 8, -1, -1, -1, -1}, // Lv15A
{ 5, 6, 255, 30, 4, 10, 75, 25, 50, 2, 500, 250, 8, -1, -1, -1, -1}, // Lv15B
},
- // Difficulty 1 (Easy) - 17 level types
+ // Difficulty 1 (Novice) - 17 level types
{
{ 6, 1, 25, -1, 3, -1, 75, 50, -1, 4, 1000, 500, 16, 6, 6, 7, -1}, // Lv1
{ 4, 2, -1, -1, 4, -1, 40, 50, -1, 0, 1000, 500, 16, 100, 100, 135, 30}, // Lv2
@@ -651,7 +654,7 @@ const InsaneRebel2::LevelDifficultyParams InsaneRebel2::kDifficultyTable[5][17]
{ 5, 7, -1, -1, 4, -1, 75, 50, -1, 4, 1000, -1, 16, -1, -1, -1, -1}, // Lv15A
{ 5, 6, 255, 35, 4, 10, 75, 50, 100, 4, 1000, 500, 16, -1, -1, -1, -1}, // Lv15B
},
- // Difficulty 2 (Medium) - 17 level types
+ // Difficulty 2 (Standard) - 17 level types
{
{ 7, 0, 35, -1, 5, -1, 75, 75, -1, 6, 1500, 750, 0, 7, 7, 8, -1}, // Lv1
{ 4, 1, -1, -1, 6, -1, 40, 75, -1, 0, 1500, 750, 0, 110, 110, 150, 35}, // Lv2
@@ -671,7 +674,7 @@ const InsaneRebel2::LevelDifficultyParams InsaneRebel2::kDifficultyTable[5][17]
{ 5, 4, -1, -1, 7, -1, 75, 75, -1, 6, 1500, -1, 0, -1, -1, -1, -1}, // Lv15A
{ 5, 5, 255, 40, 7, 10, 75, 75, 150, 6, 1500, 750, 0, -1, -1, -1, -1}, // Lv15B
},
- // Difficulty 3 (Hard) - 17 level types
+ // Difficulty 3 (Expert) - 17 level types
{
{ 8, 0, 77, -1, 7, -1, 80, 100, -1, 8, 2000, 1000, 4, 8, 8, 9, -1}, // Lv1
{ 4, 0, -1, -1, 7, -1, 40, 100, -1, 0, 2000, 1000, 4, 120, 120, 165, 40}, // Lv2
@@ -691,7 +694,7 @@ const InsaneRebel2::LevelDifficultyParams InsaneRebel2::kDifficultyTable[5][17]
{ 5, 4, -1, -1, 10, -1, 79, 100, -1, 8, 2000, -1, 4, -1, -1, -1, -1}, // Lv15A
{ 5, 4, 255, 45, 8, 5, 80, 100, 200, 8, 2000, 1000, 4, -1, -1, -1, -1}, // Lv15B
},
- // Difficulty 4 (Jedi) â identical to difficulty 2 (Medium) in original data
+ // Difficulty 4 (Custom1) â identical to difficulty 2 (Standard) in original data
{
{ 7, 0, 35, -1, 5, -1, 75, 75, -1, 6, 1500, 750, 0, 7, 7, 8, -1}, // Lv1
{ 4, 1, -1, -1, 6, -1, 40, 75, -1, 0, 1500, 750, 0, 110, 110, 150, 35}, // Lv2
@@ -711,11 +714,54 @@ const InsaneRebel2::LevelDifficultyParams InsaneRebel2::kDifficultyTable[5][17]
{ 5, 4, -1, -1, 7, -1, 75, 75, -1, 6, 1500, -1, 0, -1, -1, -1, -1}, // Lv15A
{ 5, 5, 255, 40, 7, 10, 75, 75, 150, 6, 1500, 750, 0, -1, -1, -1, -1}, // Lv15B
},
+ // Difficulty 5 (Custom2) â same as Custom1 except Lv15B roll/lift/slide/drift are 0
+ {
+ { 7, 0, 35, -1, 5, -1, 75, 75, -1, 6, 1500, 750, 0, 7, 7, 8, -1}, // Lv1
+ { 4, 1, -1, -1, 6, -1, 40, 75, -1, 0, 1500, 750, 0, 110, 110, 150, 35}, // Lv2
+ { 6, 1, 20, 38, 7, 12, 75, 75, 150, 6, 1500, 750, 0, -1, -1, -1, -1}, // Lv3
+ { 5, 1, 100, 35, 6, 20, 75, 75, 150, 6, 1500, 750, 0, -1, -1, -1, -1}, // Lv4
+ { 6, 1, 30, -1, 4, -1, 75, 75, -1, 6, 1500, 750, 0, -1, -1, -1, -1}, // Lv5
+ { 6, 1, -1, 30, 6, 15, 75, 75, 150, 6, 1500, -1, 0, -1, -1, -1, -1}, // Lv6A
+ { 6, 1, 200, 50, 12, -1, 75, 75, 150, 6, 1500, 750, 0, 160, 160, 160, 105}, // Lv6B
+ { -1, -1, -1, 80, -1, -1, -1, -1, 150, 6, 1500, 750, 0, -1, -1, -1, -1}, // Lv7
+ { 5, 1, 27, -1, 3, -1, 60, 75, -1, 6, 1500, 750, 0, -1, -1, -1, -1}, // Lv8
+ { 5, 3, 19, 60, 7, -1, 75, 75, 150, 6, 1500, 750, 0, 110, 110, 110, 150}, // Lv9
+ { 5, 9, -1, 40, 100, -1, 85, 75, 150, 6, 1500, 750, 0, 11, 7, 8, -1}, // Lv10
+ { 4, 1, 20, -1, 6, -1, 75, 75, -1, 0, 1500, 750, 0, 6, 7, 8, 9}, // Lv11
+ { 4, 0, -1, -1, 11, -1, 75, 75, -1, 6, 1500, 750, 0, -1, -1, -1, -1}, // Lv12
+ { 5, 3, 40, 40, 3, 15, 76, 75, 150, 6, 1500, 750, 0, -1, -1, -1, -1}, // Lv13
+ { 5, 0, 38, -1, 4, -1, 75, 75, -1, 6, 1500, 750, 0, -1, -1, -1, -1}, // Lv14
+ { 5, 4, -1, -1, 7, -1, 75, 75, -1, 6, 1500, -1, 0, -1, -1, -1, -1}, // Lv15A
+ { 5, 5, 255, 40, 7, 10, 75, 75, 150, 6, 1500, 750, 0, 0, 0, 0, 0}, // Lv15B
+ },
};
InsaneRebel2::LevelDifficultyParams InsaneRebel2::getDifficultyParams() const {
- int diff = CLIP(_difficulty, 0, 4);
- int lvIdx = CLIP((int)_rebelLevelType, 0, 16);
+ int diff = CLIP(_difficulty, 0, 5);
+ int lvIdx = 0;
+
+ // Retail uses DAT_0047a7f8 as the per-segment difficulty table index.
+ // This is NOT the same as handler 0x26's gun "levelType" from opcode 6.
+ //
+ // Index mapping reconstructed from level handlers:
+ // Lv1->0, Lv2->1, Lv3->2, Lv4->3, Lv5->4,
+ // Lv6A->5, Lv6B->6, Lv7->7 ... Lv14->14, Lv15A->15, Lv15B->16.
+ // Our Level 6 phase flow sets _currentPhase to 1/2 accordingly.
+ // Level 15 phase switch to 16 is currently approximated by _currentPhase >= 2.
+ if (_selectedLevel <= 0) {
+ // Fallback during non-gameplay contexts before level selection is initialized.
+ lvIdx = CLIP((int)_rebelLevelType, 0, 16);
+ } else if (_selectedLevel <= 5) {
+ lvIdx = _selectedLevel - 1;
+ } else if (_selectedLevel == 6) {
+ lvIdx = (_currentPhase >= 2) ? 6 : 5;
+ } else if (_selectedLevel <= 14) {
+ lvIdx = _selectedLevel;
+ } else { // _selectedLevel == 15
+ lvIdx = (_currentPhase >= 2) ? 16 : 15;
+ }
+
+ lvIdx = CLIP(lvIdx, 0, 16);
return kDifficultyTable[diff][lvIdx];
}
@@ -1004,10 +1050,11 @@ int32 InsaneRebel2::processMouse() {
}
}
- // Left button: Trigger shot on button press (not hold)
- // From FUN_0040d836 (Handler 7) line 141: shots only spawn when DAT_004437c0 == 2
- // From FUN_00401CCF (Handler 8) line 82-84: mode 4 disables shooting
- if (leftPressed && !leftWasPressed && isShootingAllowed()) {
+ // Shot trigger behavior:
+ // - Handler 25 keeps edge-triggered clicks due cover-toggle/sticky input semantics.
+ // - Other gameplay handlers fire while button is held; slot counters still rate-limit.
+ bool triggerShot = (_rebelHandler == 25) ? (leftPressed && !leftWasPressed) : leftPressed;
+ if (triggerShot && isShootingAllowed()) {
Common::Point mousePos(_vm->_mouse.x, _vm->_mouse.y);
debug("Rebel2 Click: Mouse=(%d,%d) Enemies=%d",
mousePos.x, mousePos.y, _enemies.size());
diff --git a/engines/scumm/insane/insane_rebel.h b/engines/scumm/insane/insane_rebel.h
index 0649f799e66..2c30a1efbe5 100644
--- a/engines/scumm/insane/insane_rebel.h
+++ b/engines/scumm/insane/insane_rebel.h
@@ -492,6 +492,10 @@ public:
// Load Handler 25 GRD NUT sprites from ANIM data (par4 = sprite type: 1,2)
bool loadHandler25GrdSprites(byte *animData, int32 size, int16 par4);
+ // Parse Handler 25 shot-origin table text from opcode 8 (par4 = 8).
+ // Retail stores values into DAT_004578a6 / DAT_004578c6 (indices 5..19).
+ bool loadHandler25ShotOriginTable(Common::SeekableReadStream &b, int64 startPos, int64 remaining);
+
// Load Level 2 background from embedded ANIM
bool loadLevel2Background(byte *animData, int32 size, byte *renderBitmap);
@@ -544,7 +548,8 @@ public:
// For horizontal-dominant beams, reads pixels above/below the line.
// For vertical-dominant beams, reads pixels left/right of the line.
void drawEdgeHighlightLine(byte *dst, int pitch, int width, int height,
- int16 x0, int16 y0, int16 x1, int16 y1);
+ int16 x0, int16 y0, int16 x1, int16 y1,
+ int16 clipLeft, int16 clipTop, int16 clipRight, int16 clipBottom);
// Initialize laser texture from NUT sprite (FUN_0040BAB0)
void initLaserTexture(NutRenderer *nut, int spriteIdx);
@@ -797,8 +802,8 @@ public:
int16 targetX; // DAT_0044367e[i] - target X position
int16 targetY; // DAT_00443682[i] - target Y position
int16 seqNum; // DAT_0044368a[i] - shot sequence (for alternating)
- int16 gunX; // DAT_0045791c[i] - gun barrel X (Handler 25, screen coords)
- int16 gunY; // DAT_00457920[i] - gun barrel Y (Handler 25, screen coords)
+ int16 gunX; // DAT_0045791c[i] - gun barrel X (Handler 25 base coords; render adds view offset)
+ int16 gunY; // DAT_00457920[i] - gun barrel Y (Handler 25 base coords; render adds view offset)
};
TurretShot _turretShots[2];
int16 _turretShotSeqCounter; // DAT_0047fe94 - global sequence counter
@@ -1000,6 +1005,13 @@ public:
NutRenderer *_grd001Sprite; // DAT_00482240 - GRD001 primary ship NUT
NutRenderer *_grd002Sprite; // DAT_00482238 - GRD002 secondary ship NUT
+ // Handler 25 shot-origin lookup tables from opcode 8/par4=8 text payload.
+ // Indices 5..19 are filled by the retail "%hd %hd ..." parser (FUN_0041CADB case 6).
+ // Uncovered Level 2 firing uses indices 5..14.
+ int16 _grdShotOriginX[30]; // DAT_004578a6 equivalent
+ int16 _grdShotOriginY[30]; // DAT_004578c6 equivalent
+ bool _grdShotOriginTableLoaded;
+
// Handler 25 sprite mode (DAT_00457900) - set by opcode 6 par3
// Controls which sprite variant to draw:
// 1: Draw _grd001Sprite normally
@@ -1032,13 +1044,13 @@ public:
NutRenderer *_hudOverlayNut; // DAT_0047fe78 - Primary HUD overlay (animated)
NutRenderer *_hudOverlay2Nut; // DAT_0047fe80 - Secondary HUD overlay
- /* Difficulty Level (0-5, from pilot menu; clamped to 0-4 for table lookup) */
+ /* Difficulty Level (0-5, from pilot menu; maps directly to table rows) */
int _difficulty;
void drawCornerBrackets(byte *dst, int pitch, int width, int height, int x, int y, int w, int h, byte color);
// ======================= Per-Level Difficulty Parameters =======================
// Extracted from RA2WIN95.EXE at VA 0x47e0f0
- // 2D table indexed by difficulty (0-4) Ã level type (0-16)
+ // 2D table indexed by difficulty (0-5) Ã level type (0-16)
// Original indexing: &DAT_0047e0f0 + chapter * 0x242 + levelType * 0x22
// Level type (_rebelLevelType) is set by IACT opcode 6 par3
// 17 entries: Lv1-5(0-4), Lv6A/6B(5-6), Lv7-14(7-14), Lv15A/15B(15-16)
@@ -1064,9 +1076,10 @@ public:
int16 driftRate; // +0x20: Ship drift rate (flight controls)
};
- // Table: 5 difficulty levels à 17 level types
- // Difficulty 4 (Jedi) is identical to difficulty 2 (Medium) in the original data
- static const LevelDifficultyParams kDifficultyTable[5][17];
+ // Table: 6 difficulty levels à 17 level types.
+ // Menu labels in GAME.TRS are: Beginner, Novice, Standard, Expert, Custom1, Custom2.
+ // Custom1 is identical to Standard; Custom2 matches Custom1 except Lv15B drift fields.
+ static const LevelDifficultyParams kDifficultyTable[6][17];
// Look up difficulty parameters for current _difficulty and _rebelLevelType
LevelDifficultyParams getDifficultyParams() const;
diff --git a/engines/scumm/insane/insane_rebel_iact.cpp b/engines/scumm/insane/insane_rebel_iact.cpp
index f1d3e192218..416fb281f2b 100644
--- a/engines/scumm/insane/insane_rebel_iact.cpp
+++ b/engines/scumm/insane/insane_rebel_iact.cpp
@@ -1555,6 +1555,18 @@ void InsaneRebel2::iactRebel2Opcode8(byte *renderBitmap, Common::SeekableReadStr
return;
}
+ // ===== Handler 25 (0x19): Shot-Origin Lookup Table (par4 == 8) =====
+ // FUN_0041CADB case 6 pushes 30 short pointers into sscanf with format at 0x482360:
+ // "%hd %hd %hd %hd ... %hd %hd" (15 X/Y pairs).
+ // Parsed values are written into DAT_004578a6 / DAT_004578c6 at indices 5..19.
+ if (_rebelHandler == 25 && par4 == 8) {
+ bool loaded = loadHandler25ShotOriginTable(b, startPos, remaining);
+ if (loaded) {
+ b.seek(startPos);
+ return;
+ }
+ }
+
// ===== Scan for embedded ANIM data =====
// Remaining handlers require finding ANIM tag in the stream
debug("Rebel2 Opcode 8: Scanning for ANIM tag (startPos=%lld remaining=%lld)",
@@ -1693,6 +1705,93 @@ void InsaneRebel2::iactRebel2Opcode8(byte *renderBitmap, Common::SeekableReadStr
b.seek(startPos);
}
+bool InsaneRebel2::loadHandler25ShotOriginTable(Common::SeekableReadStream &b, int64 startPos, int64 remaining) {
+ // IACT layout at this point:
+ // - stream is positioned after par1..par4 (8 bytes consumed by caller)
+ // - retail parser reads from offset +18 relative to IACT start -> startPos + 10
+ // - payload size for this opcode family is at offset +14 -> startPos + 6
+ if (remaining < 12)
+ return false;
+
+ int64 savedPos = b.pos();
+ b.seek(startPos + 6);
+ uint32 textSize = b.readUint32LE();
+
+ int64 textPos = startPos + 10;
+ int64 maxAvail = remaining - 10;
+ if (maxAvail <= 0) {
+ b.seek(savedPos);
+ return false;
+ }
+
+ int64 bytesToRead = maxAvail;
+ if (textSize > 0 && (int64)textSize <= maxAvail) {
+ bytesToRead = textSize;
+ }
+
+ char *buf = new char[(size_t)bytesToRead + 1];
+
+ b.seek(textPos);
+ b.read((byte *)buf, bytesToRead);
+ buf[bytesToRead] = '\0';
+
+ // Parse signed 16-bit integers from the ASCII payload.
+ int16 vals[30];
+ int count = 0;
+ const char *p = buf;
+ const char *end = buf + bytesToRead;
+ while (p < end && count < 30) {
+ while (p < end && *p != '-' && *p != '+' && !Common::isDigit(*p))
+ ++p;
+ if (p >= end)
+ break;
+
+ int sign = 1;
+ if (*p == '-' || *p == '+') {
+ if (*p == '-')
+ sign = -1;
+ ++p;
+ }
+
+ if (p >= end || !Common::isDigit(*p))
+ continue;
+
+ int value = 0;
+ while (p < end && Common::isDigit(*p)) {
+ value = value * 10 + (*p - '0');
+ if (value > 32768)
+ value = 32768; // Keep accumulation bounded before sign/clamp.
+ ++p;
+ }
+
+ vals[count++] = (int16)CLIP<int>(sign * value, -32768, 32767);
+ }
+
+ delete[] buf;
+ b.seek(savedPos);
+
+ if (count < 20) {
+ debug("Rebel2 Opcode 8: Handler25 par4=8 parse failed (count=%d, expected up to 30)", count);
+ return false;
+ }
+
+ // Retail mapping (from FUN_41CADB disassembly):
+ // token1->0x4578b0 (X index 5), token2->0x4578d0 (Y index 5), ...
+ // token29->0x4578cc (X index 19), token30->0x4578ec (Y index 19).
+ for (int i = 0; i < 15; ++i) {
+ int pair = i * 2;
+ if (pair + 1 >= count)
+ break;
+ _grdShotOriginX[5 + i] = vals[pair];
+ _grdShotOriginY[5 + i] = vals[pair + 1];
+ }
+ _grdShotOriginTableLoaded = true;
+
+ debug("Rebel2 Opcode 8: Loaded Handler25 shot-origin table (pairs=%d) idx5=(%d,%d) idx14=(%d,%d)",
+ count / 2, _grdShotOriginX[5], _grdShotOriginY[5], _grdShotOriginX[14], _grdShotOriginY[14]);
+ return true;
+}
+
// ======================= Opcode 8 Helper Functions =======================
// These helper functions are extracted from the original monolithic iactRebel2Opcode8
// to improve code readability and match the retail FUN_* function structure.
diff --git a/engines/scumm/insane/insane_rebel_render.cpp b/engines/scumm/insane/insane_rebel_render.cpp
index ce964bbb20e..b13e2a76d48 100644
--- a/engines/scumm/insane/insane_rebel_render.cpp
+++ b/engines/scumm/insane/insane_rebel_render.cpp
@@ -146,8 +146,7 @@ void InsaneRebel2::decodeCodec45(byte *dst, const byte *src, int width, int heig
int testLineSize = READ_LE_UINT16(src + testOffset);
// A valid first line size should be: > 0, <= width*2
if (testLineSize > 0 && testLineSize <= width * 2 && testLineSize < dataSize - testOffset) {
- // Validate by summing line sizes
- int sumTest = 0;
+ // Validate line-size sequence
int linesTest = 0;
const byte *testPtr = src + testOffset;
bool validSum = true;
@@ -158,7 +157,6 @@ void InsaneRebel2::decodeCodec45(byte *dst, const byte *src, int width, int heig
validSum = false;
break;
}
- sumTest += ls + 2;
testPtr += ls + 2;
linesTest++;
}
@@ -530,14 +528,14 @@ void InsaneRebel2::spawnTurretShot(int x, int y) {
// levelType 5: BLAST.SAD (slot 0), otherwise: TBLAST.SAD (slot 7)
playSfx((_rebelLevelType == 5) ? 0 : 7, 127, 0);
- _turretShots[i].counter = getShotMaxDuration();
- _turretShots[i].seqNum = _turretShotSeqCounter;
- _turretShotSeqCounter++;
- _turretShots[i].targetX = x + _viewX; // DAT_0044366e in original
- _turretShots[i].targetY = y + _viewY; // DAT_00443670 in original
- break;
+ _turretShots[i].counter = getShotMaxDuration();
+ _turretShots[i].seqNum = _turretShotSeqCounter;
+ _turretShotSeqCounter++;
+ _turretShots[i].targetX = x + _viewX; // DAT_0044366e in original
+ _turretShots[i].targetY = y + _viewY; // DAT_00443670 in original
+ break;
+ }
}
- }
}
// Handler 8 Vehicle shot spawn (based on FUN_401CCF lines 65-69)
@@ -578,10 +576,12 @@ void InsaneRebel2::spawnHandler25Shot(int x, int y) {
_turretShots[i].targetX = x + _viewX;
_turretShots[i].targetY = y + _viewY;
- // Compute gun position from GRD002 character sprite.
- // Original uses per-direction lookup tables DAT_004578a6/DAT_004578c6.
- // We approximate from the NUT sprite center + directional offset.
- if (_grd002Sprite && _grd002Sprite->getNumChars() > 0) {
+ // Compute gun position from retail lookup tables.
+ // Original (FUN_41DB5E) stores:
+ // DAT_0045791c[i] = gunXTable[spriteIdx] + DAT_00457910 - DAT_0045790c
+ // DAT_00457920[i] = gunYTable[spriteIdx] + DAT_00457912 - DAT_0045790e
+ // where gunXTable/gunYTable are loaded by opcode 8, par4=8.
+ if (_grdShotOriginTableLoaded) {
// Compute current sprite index (same logic as renderHandler25Ship)
int spriteIdx;
if (_rebelDamageLevel == 0) {
@@ -611,34 +611,23 @@ void InsaneRebel2::spawnHandler25Shot(int x, int y) {
} else {
spriteIdx = (_rebelFlightDir == 0) ? (5 - _rebelDamageLevel) : (25 - _rebelDamageLevel);
}
- int numSprites = _grd002Sprite->getNumChars();
if (spriteIdx < 0)
spriteIdx = 0;
- if (spriteIdx >= numSprites)
- spriteIdx = numSprites - 1;
-
- // Get sprite rendering position (same as in renderHandler25Ship)
- int16 spriteXOffset = _grd002Sprite->getCharXOffset(spriteIdx);
- int16 spriteYOffset = _grd002Sprite->getCharYOffset(spriteIdx);
- int spriteW = _grd002Sprite->getCharWidth(spriteIdx);
- int spriteH = _grd002Sprite->getCharHeight(spriteIdx);
- bool shouldMirror = (_rebelFlightDir != 0 && _rebelDamageLevel == 0);
-
- int drawX;
- if (shouldMirror) {
- drawX = _rebelViewOffset2X + (320 - spriteW - spriteXOffset) + _viewX;
- } else {
- drawX = _rebelViewOffset2X + spriteXOffset + _viewX;
+ if (spriteIdx >= ARRAYSIZE(_grdShotOriginX))
+ spriteIdx = ARRAYSIZE(_grdShotOriginX) - 1;
+
+ int16 gunXTable = _grdShotOriginX[spriteIdx];
+ int16 gunYTable = _grdShotOriginY[spriteIdx];
+
+ // Mirrored X when DAT_00457902 != 0 in retail.
+ if (_rebelFlightDir != 0) {
+ gunXTable = 320 - gunXTable;
}
- int drawY = spriteYOffset + _rebelViewOffset2Y + _viewY;
- // Gun barrel is approximately at the character's hand level:
- // X: center of sprite ± directional offset toward the target
- // Y: about 60% down the sprite height (hand/arm level)
- _turretShots[i].gunX = drawX + spriteW / 2;
- _turretShots[i].gunY = drawY + (spriteH * 3) / 5;
+ _turretShots[i].gunX = gunXTable + _rebelViewOffset2X - _rebelViewOffsetX + _viewX;
+ _turretShots[i].gunY = gunYTable + _rebelViewOffset2Y - _rebelViewOffsetY + _viewY;
} else {
- // Fallback: approximate center-bottom of character area
+ // Fallback when table payload (opcode 8/par4=8) was not loaded.
_turretShots[i].gunX = _rebelViewOffset2X + 160 + _viewX;
_turretShots[i].gunY = _rebelViewOffset2Y + 140 + _viewY;
}
@@ -732,131 +721,344 @@ void InsaneRebel2::drawTexturedLine(byte *dst, int pitch, int width, int height,
}
// Helper: draw a textured segment between two points using the game's original routine (FUN_00429360 port)
-void drawTexturedSegment(byte *dst, int pitch, int width, int height, int param_3, int param_4, int param_5, int param_6, int param_7, const byte *param_8) {
- // Ported from FUN_00429360 (decompiled). Only 0 in texture is transparent.
- int sVar4 = 0; // left
- int sVar1 = 0; // top
- int sVar7 = width - 1; // right
- int sVar10 = height - 1; // bottom
-
- int px0 = param_3;
- int py0 = param_4;
- int px1 = param_5;
- int py1 = param_6;
-
- // Clip against screen bounds (translation of original clipping logic)
- if (px0 == px1) {
- if (px0 < sVar4 || px0 > sVar7)
+void drawTexturedSegment(byte *dst, int pitch, int width, int height,
+ int param_3, int param_4, int param_5, int param_6, int param_7, const byte *param_8,
+ int clipLeft, int clipTop, int clipRight, int clipBottom) {
+ // Near-direct port of FUN_00429360.
+ // Only color 0 is transparent.
+ int sVar4 = clipLeft;
+ int sVar1 = clipTop;
+ int sVar7 = clipRight;
+ int sVar10 = clipBottom;
+ int p3 = param_3;
+ int p4 = param_4;
+ int p5 = param_5;
+ int p6 = param_6;
+
+ if (p5 == p3) {
+ if (p5 < sVar4 || sVar7 < p5)
return;
} else {
- if (px0 < sVar4) {
- if (px1 < sVar4)
+ if (p3 < sVar4) {
+ if (p5 < sVar4)
return;
- py0 = py1 + ((py0 - py1) * (sVar4 - px1)) / (px0 - px1);
- px0 = sVar4;
- } else if (px0 > sVar7) {
- if (px1 > sVar7)
+ p4 = p6 + (((p4 - p6) * (sVar4 - p5)) / (p3 - p5));
+ p3 = sVar4;
+ } else if (sVar7 < p3) {
+ if (sVar7 < p5)
return;
- py0 = py1 + ((py0 - py1) * (sVar7 - px1)) / (px0 - px1);
- px0 = sVar7;
+ p4 = p6 + (((p4 - p6) * (sVar7 - p5)) / (p3 - p5));
+ p3 = sVar7;
}
- if (px1 < sVar4) {
- py1 = py0 + ((py1 - py0) * (sVar4 - px0)) / (px1 - px0);
- px1 = sVar4;
- } else if (px1 > sVar7) {
- py1 = py0 + ((py1 - py0) * (sVar7 - px0)) / (px1 - px0);
- px1 = sVar7;
+ if (p5 < sVar4) {
+ p6 = p4 + (((p6 - p4) * (sVar4 - p3)) / (p5 - p3));
+ p5 = sVar4;
+ } else if (sVar7 < p5) {
+ p6 = p4 + (((p6 - p4) * (sVar7 - p3)) / (p5 - p3));
+ p5 = sVar7;
}
}
- if (py0 == py1) {
- if (py0 < sVar1 || py0 > sVar10)
+ if (p6 == p4) {
+ if (p6 < sVar1 || sVar10 < p6)
return;
} else {
- if (py0 < sVar1) {
- if (py1 < sVar1)
+ if (p4 < sVar1) {
+ if (p6 < sVar1)
return;
- px0 = px1 + ((px0 - px1) * (sVar1 - py1)) / (py0 - py1);
- py0 = sVar1;
- } else if (py0 > sVar10) {
- if (py1 > sVar10)
+ p3 = p5 + (((p3 - p5) * (sVar1 - p6)) / (p4 - p6));
+ p4 = sVar1;
+ } else if (sVar10 < p4) {
+ if (sVar10 < p6)
return;
- px0 = px1 + ((px0 - px1) * (sVar10 - py1)) / (py0 - py1);
- py0 = sVar10;
+ p3 = p5 + (((p3 - p5) * (sVar10 - p6)) / (p4 - p6));
+ p4 = sVar10;
}
- if (py1 < sVar1) {
- px1 = px0 + ((px1 - px0) * (sVar1 - py0)) / (py1 - py0);
- py1 = sVar1;
- } else if (py1 > sVar10) {
- px1 = px0 + ((px1 - px0) * (sVar10 - py0)) / (py1 - py0);
- py1 = sVar10;
+ if (p6 < sVar1) {
+ p5 = (((p5 - p3) * (sVar1 - p4)) / (p6 - p4)) + p3;
+ p6 = sVar1;
+ } else if (sVar10 < p6) {
+ p5 = (((p5 - p3) * (sVar10 - p4)) / (p6 - p4)) + p3;
+ p6 = sVar10;
}
}
- int dx = px1 - px0;
- int dy = py1 - py0;
- int absdx = dx < 0 ? -dx : dx;
- int absdy = dy < 0 ? -dy : dy;
-
- // pointer into destination and texture
- byte *baseDst = dst;
+ int iVar5 = pitch;
+ int sD = p5 - p3;
+ int sE = p6 - p4;
+ byte *pcVar6 = dst + p4 * iVar5 + p3;
const byte *texPtr = param_8;
- if (absdx == 0) {
- if (absdy == 0) {
+ if (sD == 0) {
+ if (sE == 0) {
if (*texPtr != 0)
- baseDst[py0 * pitch + px0] = *texPtr;
+ *pcVar6 = *texPtr;
+ return;
+ } else if (sE < 1) {
+ int iVar9 = -(sE - 1);
+ if (iVar9 <= 0)
+ return;
+ int iVar11 = iVar9;
+ do {
+ iVar11--;
+ if (*texPtr != 0)
+ *pcVar6 = *texPtr;
+ pcVar6 -= iVar5;
+ for (iVar9 = iVar9 - param_7; iVar9 < 0; iVar9 = iVar9 - (sE - 1))
+ texPtr++;
+ } while (0 < iVar11);
+ return;
+ } else {
+ int iVar9 = sE + 1;
+ if (iVar9 <= 0)
+ return;
+ int iVar8 = iVar9;
+ int iVar11 = iVar9;
+ do {
+ iVar11--;
+ if (*texPtr != 0)
+ *pcVar6 = *texPtr;
+ pcVar6 += iVar5;
+ for (iVar8 = iVar8 - param_7; iVar8 < 0; iVar8 += iVar9)
+ texPtr++;
+ } while (0 < iVar11);
return;
}
- // vertical-ish
- int step = absdy + 1;
- int curY = py0;
- int signY = dy > 0 ? 1 : -1;
- int iVar9 = step; // adv counter
- for (int i = 0; i < step; i++) {
- if (*texPtr != 0)
- baseDst[curY * pitch + px0] = *texPtr;
- curY += signY;
- iVar9 -= param_7;
- while (iVar9 < 0) { texPtr++; iVar9 += step; }
+ } else if (sE == 0) {
+ if (sD < 1) {
+ int iVar5h = -(sD - 1);
+ if (iVar5h <= 0)
+ return;
+ int iVar9 = iVar5h;
+ do {
+ iVar9--;
+ if (*texPtr != 0)
+ *pcVar6 = *texPtr;
+ pcVar6 -= 1;
+ for (iVar5h = iVar5h - param_7; iVar5h < 0; iVar5h = iVar5h - (sD - 1))
+ texPtr++;
+ } while (0 < iVar9);
+ return;
+ } else {
+ int iVar5h = sD + 1;
+ if (iVar5h <= 0)
+ return;
+ int iVar11 = iVar5h;
+ int iVar9 = iVar5h;
+ do {
+ iVar9--;
+ if (*texPtr != 0)
+ *pcVar6 = *texPtr;
+ pcVar6 += 1;
+ for (iVar11 = iVar11 - param_7; iVar11 < 0; iVar11 += iVar5h)
+ texPtr++;
+ } while (0 < iVar9);
+ return;
}
- return;
}
- if (absdy == 0) {
- // horizontal-ish
- int step = absdx + 1;
- int curX = px0;
- int signX = dx > 0 ? 1 : -1;
- int iVar11 = step;
- for (int i = 0; i < step; i++) {
- if (*texPtr != 0)
- baseDst[py0 * pitch + curX] = *texPtr;
- curX += signX;
- iVar11 -= param_7;
- while (iVar11 < 0) { texPtr++; iVar11 += step; }
+ if (sD < 0) {
+ if (-1 < sE) {
+ int iVar11 = sD;
+ int iVar8 = sE;
+ int iVar9 = -iVar11;
+ if (-iVar8 == iVar11 || iVar9 < iVar8) {
+ iVar9 = sE >> 1;
+ if (iVar8 + 1 < 1)
+ return;
+ int iVar3 = iVar8;
+ int iVar12 = iVar8;
+ do {
+ if (*texPtr != 0)
+ *pcVar6 = *texPtr;
+ pcVar6 += iVar5;
+ iVar9 += iVar11;
+ if (iVar9 < 0) {
+ iVar9 += iVar8;
+ pcVar6 -= 1;
+ }
+ for (iVar12 = iVar12 - param_7; iVar12 < 0; iVar12 = iVar8 + 1 + iVar12)
+ texPtr++;
+ bool bVar2 = 0 < iVar3;
+ iVar3--;
+ if (!bVar2)
+ break;
+ } while (true);
+ return;
+ }
+ int iVar12 = iVar9 >> 1;
+ if (-iVar11 + 1 < 1)
+ return;
+ int iVar3 = -iVar11;
+ do {
+ if (*texPtr != 0)
+ *pcVar6 = *texPtr;
+ pcVar6 -= 1;
+ iVar12 -= iVar8;
+ if (iVar12 < 0) {
+ iVar12 -= iVar11;
+ pcVar6 += iVar5;
+ }
+ for (iVar9 = iVar9 - param_7; iVar9 < 0; iVar9 = (iVar9 - iVar11) + 1)
+ texPtr++;
+ bool bVar2 = 0 < iVar3;
+ iVar3--;
+ if (!bVar2)
+ break;
+ } while (true);
+ return;
}
+ } else if (-1 < sE) {
+ if (sD <= sE) {
+ int iVar11 = sE;
+ int iVar9 = sE >> 1;
+ if (iVar11 + 1 < 1)
+ return;
+ int iVar8 = iVar11;
+ int iVar12 = iVar11;
+ do {
+ if (*texPtr != 0)
+ *pcVar6 = *texPtr;
+ pcVar6 += iVar5;
+ iVar9 -= sD;
+ if (iVar9 < 0) {
+ iVar9 += iVar11;
+ pcVar6 += 1;
+ }
+ for (iVar8 = iVar8 - param_7; iVar8 < 0; iVar8 = iVar11 + 1 + iVar8)
+ texPtr++;
+ bool bVar2 = 0 < iVar12;
+ iVar12--;
+ if (!bVar2)
+ break;
+ } while (true);
+ return;
+ }
+ int iVar11 = sD;
+ int iVar9 = sD >> 1;
+ if (iVar11 + 1 < 1)
+ return;
+ int iVar8 = iVar11;
+ int iVar12 = iVar11;
+ do {
+ if (*texPtr != 0)
+ *pcVar6 = *texPtr;
+ pcVar6 += 1;
+ iVar9 -= sE;
+ if (iVar9 < 0) {
+ iVar9 += iVar11;
+ pcVar6 += iVar5;
+ }
+ for (iVar8 = iVar8 - param_7; iVar8 < 0; iVar8 = iVar11 + 1 + iVar8)
+ texPtr++;
+ bool bVar2 = 0 < iVar12;
+ iVar12--;
+ if (!bVar2)
+ break;
+ } while (true);
return;
}
- // general case
- int steps = (absdx > absdy) ? absdx + 1 : absdy + 1;
- int x = px0, y = py0;
- int sx = dx > 0 ? 1 : -1;
- int sy = dy > 0 ? 1 : -1;
- int err = absdx - absdy;
- int iVar12 = steps - 1; // Original starts at majorAxis, not majorAxis+1
-
- for (int i = 0; i < steps; i++) {
- if (x >= 0 && x < width && y >= 0 && y < height) {
- if (*texPtr != 0)
- baseDst[y * pitch + x] = *texPtr;
+ if (sD < 0) {
+ if (sD < sE) {
+ int iVar11 = sD;
+ int iVar9 = -iVar11;
+ int iVar8 = iVar9 >> 1;
+ if (0 < -iVar11 + 1) {
+ int iVar12 = -iVar11;
+ do {
+ if (*texPtr != 0)
+ *pcVar6 = *texPtr;
+ pcVar6 -= 1;
+ iVar8 += sE;
+ if (iVar8 < 0) {
+ iVar8 -= iVar11;
+ pcVar6 -= iVar5;
+ }
+ for (iVar9 = iVar9 - param_7; iVar9 < 0; iVar9 = (iVar9 - iVar11) + 1)
+ texPtr++;
+ bool bVar2 = 0 < iVar12;
+ iVar12--;
+ if (!bVar2)
+ break;
+ } while (true);
+ return;
+ }
+ } else {
+ int iVar11 = sE;
+ int iVar9 = -iVar11;
+ int iVar8 = iVar9 >> 1;
+ if (0 < -iVar11 + 1) {
+ int iVar12 = -iVar11;
+ do {
+ if (*texPtr != 0)
+ *pcVar6 = *texPtr;
+ pcVar6 -= iVar5;
+ iVar8 += sD;
+ if (iVar8 < 0) {
+ iVar8 -= iVar11;
+ pcVar6 -= 1;
+ }
+ for (iVar9 = iVar9 - param_7; iVar9 < 0; iVar9 = (iVar9 - iVar11) + 1)
+ texPtr++;
+ bool bVar2 = 0 < iVar12;
+ iVar12--;
+ if (!bVar2)
+ break;
+ } while (true);
+ return;
+ }
+ }
+ } else {
+ int iVar11 = sE;
+ int iVar8 = sD;
+ int iVar9 = -iVar11;
+ if (iVar9 < iVar8) {
+ iVar9 = sD >> 1;
+ if (0 < iVar8 + 1) {
+ int iVar3 = iVar8;
+ int iVar12 = iVar8;
+ do {
+ if (*texPtr != 0)
+ *pcVar6 = *texPtr;
+ pcVar6 += 1;
+ iVar9 += iVar11;
+ if (iVar9 < 0) {
+ iVar9 += iVar8;
+ pcVar6 -= iVar5;
+ }
+ for (iVar12 = iVar12 - param_7; iVar12 < 0; iVar12 = iVar8 + 1 + iVar12)
+ texPtr++;
+ bool bVar2 = 0 < iVar3;
+ iVar3--;
+ if (!bVar2)
+ break;
+ } while (true);
+ return;
+ }
+ } else {
+ int iVar12 = iVar9 >> 1;
+ if (0 < -iVar11 + 1) {
+ int iVar3 = -iVar11;
+ do {
+ if (*texPtr != 0)
+ *pcVar6 = *texPtr;
+ pcVar6 -= iVar5;
+ iVar12 -= iVar8;
+ if (iVar12 < 0) {
+ iVar12 -= iVar11;
+ pcVar6 += 1;
+ }
+ for (iVar9 = iVar9 - param_7; iVar9 < 0; iVar9 = (iVar9 - iVar11) + 1)
+ texPtr++;
+ bool bVar2 = 0 < iVar3;
+ iVar3--;
+ if (!bVar2)
+ break;
+ } while (true);
+ return;
+ }
}
- int e2 = 2 * err;
- if (e2 > -absdy) { err -= absdy; x += sx; }
- if (e2 < absdx) { err += absdx; y += sy; }
- iVar12 -= param_7;
- while (iVar12 < 0) { texPtr++; iVar12 += steps; }
}
}
@@ -887,16 +1089,38 @@ void InsaneRebel2::initLaserTexture(NutRenderer *nut, int spriteIdx) {
if (!_laserTexture.pixels)
return;
- // Render sprite into buffer (FUN_0040BAB0 lines 23-24)
- // We copy the sprite data directly since it's already in the right format
+ // FUN_0040BAB0 draws the sprite through the normal NUT blitter (FUN_004236e0),
+ // so we must honor x/y offsets and transparency, not just memcpy glyph rows.
const byte *srcData = nut->getCharData(spriteIdx);
- if (srcData) {
- int srcHeight = nut->getCharHeight(spriteIdx);
- int copyHeight = MIN(texHeight, (int16)srcHeight);
- memcpy(_laserTexture.pixels, srcData, texWidth * copyHeight);
+ const int srcWidth = nut->getCharWidth(spriteIdx);
+ const int srcHeight = nut->getCharHeight(spriteIdx);
+ const int srcXOff = nut->getCharXOffset(spriteIdx);
+ const int srcYOff = nut->getCharYOffset(spriteIdx);
+ if (srcData && srcWidth > 0 && srcHeight > 0) {
+ for (int sy = 0; sy < srcHeight; sy++) {
+ int dy = srcYOff + sy;
+ if (dy < 0 || dy >= texHeight)
+ continue;
+
+ const byte *srcRow = srcData + sy * srcWidth;
+ byte *dstRow = _laserTexture.pixels + dy * texWidth;
+ for (int sx = 0; sx < srcWidth; sx++) {
+ int dx = srcXOff + sx;
+ if (dx < 0 || dx >= texWidth)
+ continue;
+
+ byte px = srcRow[sx];
+ // FUN_00429360 (beam raster) only treats 0 as transparent.
+ // Keep 231 pixels from the source texture to avoid dropping beam sub-segments.
+ if (px != 0) {
+ dstRow[dx] = px;
+ }
+ }
+ }
}
- debug("Rebel2: Initialized laser texture %dx%d from sprite %d", texWidth, texHeight, spriteIdx);
+ debug("Rebel2: Initialized laser texture %dx%d from sprite %d (xoff=%d yoff=%d src=%dx%d)",
+ texWidth, texHeight, spriteIdx, srcXOff, srcYOff, srcWidth, srcHeight);
// Diagnostic: dump texture pixel stats to verify data is loaded correctly
if (_laserTexture.pixels && texWidth > 0 && texHeight > 0) {
@@ -992,12 +1216,15 @@ void InsaneRebel2::initEdgeTable(const byte *data) {
// param_2 = clip rect (or NULL for full buffer)
// param_3..param_6 = x0, y0, x1, y1 line endpoints
void InsaneRebel2::drawEdgeHighlightLine(byte *dst, int pitch, int width, int height,
- int16 x0, int16 y0, int16 x1, int16 y1) {
- // Clip region (FUN_410962 lines 19-30, simplified for our buffer layout)
- int16 clipLeft = 1;
- int16 clipTop = 1;
- int16 clipRight = width - 2;
- int16 clipBottom = height - 2;
+ int16 x0, int16 y0, int16 x1, int16 y1,
+ int16 clipLeftIn, int16 clipTopIn, int16 clipRightIn, int16 clipBottomIn) {
+ // Clip region (FUN_410962 lines 19-30). Clip is provided by caller (gameplay viewport).
+ int16 clipLeft = CLIP<int16>(clipLeftIn, 1, width - 2);
+ int16 clipTop = CLIP<int16>(clipTopIn, 1, height - 2);
+ int16 clipRight = CLIP<int16>(clipRightIn, 1, width - 2);
+ int16 clipBottom = CLIP<int16>(clipBottomIn, 1, height - 2);
+ if (clipLeft > clipRight || clipTop > clipBottom)
+ return;
// Clip X endpoints (FUN_410962 lines 35-69)
if (x0 == x1) {
@@ -1249,9 +1476,13 @@ void InsaneRebel2::drawLaserBeam(byte *dst, int pitch, int width, int height,
byte *texPixels = _laserTexture.pixels;
// FUN_0040BBF6 line 23: sVar7 = (thickness * animFrame * 16) / maxFrames
+ // Tuned beam segment spacing: 60% of original.
+ constexpr int kBeamAnimScaleNumerator = 48; // 16 * 0.6 * 5
+ constexpr int kBeamAnimScaleDenominator = 5;
if (maxFrames == 0)
maxFrames = 1;
- int16 sVar7 = (int16)(((int)thickness * (int)animFrame * 16) / (int)maxFrames);
+ int16 sVar7 = (int16)(((int)thickness * (int)animFrame * kBeamAnimScaleNumerator) /
+ ((int)maxFrames * kBeamAnimScaleDenominator));
// FUN_0040BBF6 lines 24-25: Calculate delta with scaling
int16 dx = targetX - gunX;
@@ -1270,6 +1501,19 @@ void InsaneRebel2::drawLaserBeam(byte *dst, int pitch, int width, int height,
// FUN_0040BBF6 line 30: Get texture pixel pointer
byte *local_28 = texPixels;
+ // Original callers pass a clip rect for the gameplay viewport (excluding status bar).
+ // This preserves texture phase at the viewport edge and avoids visibly "chopped" beams.
+ int clipLeft = CLIP<int>(_viewX, 0, width - 1);
+ int clipTop = CLIP<int>(_viewY, 0, height - 1);
+ int clipRight = CLIP<int>(_viewX + 319, 0, width - 1);
+ int clipBottom = CLIP<int>(_viewY + 179, 0, height - 1);
+ if (clipLeft > clipRight || clipTop > clipBottom)
+ return;
+ int edgeClipLeft = CLIP<int>(clipLeft + 1, 1, width - 2);
+ int edgeClipTop = CLIP<int>(clipTop + 1, 1, height - 2);
+ int edgeClipRight = CLIP<int>(clipRight - 1, 1, width - 2);
+ int edgeClipBottom = CLIP<int>(clipBottom - 1, 1, height - 2);
+
debug(5, "Rebel2: drawLaserBeam gun(%d,%d) tgt(%d,%d) start(%d,%d) end(%d,%d) anim=%d/%d ws=%d hs=%d th=%d",
gunX, gunY, targetX, targetY, startX, startY, endX, endY,
animFrame, maxFrames, widthScale, heightScale, thickness);
@@ -1293,7 +1537,8 @@ void InsaneRebel2::drawLaserBeam(byte *dst, int pitch, int width, int height,
drawTexturedSegment(dst, pitch, width, height,
startX, (startY - halfLines) + lineIdx,
endX, (endY - halfLines) + lineIdx,
- texW, local_28);
+ texW, local_28,
+ clipLeft, clipTop, clipRight, clipBottom);
// Advance texture pointer (step through texture rows)
for (local_24 = texH + local_24; local_24 > 0; local_24 -= numLines) {
@@ -1302,13 +1547,15 @@ void InsaneRebel2::drawLaserBeam(byte *dst, int pitch, int width, int height,
}
// FUN_0040BBF6 lines 47-51: Edge highlights along top and bottom beam edges
- if (_rebelDetailMode >= 0) {
+ if (_rebelDetailMode >= 0 && edgeClipLeft <= edgeClipRight && edgeClipTop <= edgeClipBottom) {
drawEdgeHighlightLine(dst, pitch, width, height,
startX, startY - halfLines,
- endX, endY - halfLines);
+ endX, endY - halfLines,
+ edgeClipLeft, edgeClipTop, edgeClipRight, edgeClipBottom);
drawEdgeHighlightLine(dst, pitch, width, height,
startX, (startY - halfLines) + numLines - 1,
- endX, (endY - halfLines) + numLines - 1);
+ endX, (endY - halfLines) + numLines - 1,
+ edgeClipLeft, edgeClipTop, edgeClipRight, edgeClipBottom);
}
} else {
// Mostly vertical beam - draw horizontal scanlines
@@ -1329,7 +1576,8 @@ void InsaneRebel2::drawLaserBeam(byte *dst, int pitch, int width, int height,
drawTexturedSegment(dst, pitch, width, height,
(startX - halfLines) + lineIdx, startY,
(endX - halfLines) + lineIdx, endY,
- texW, local_28);
+ texW, local_28,
+ clipLeft, clipTop, clipRight, clipBottom);
// Advance texture pointer
for (local_24 = texH + local_24; local_24 > 0; local_24 -= numLines) {
@@ -1338,15 +1586,18 @@ void InsaneRebel2::drawLaserBeam(byte *dst, int pitch, int width, int height,
}
// FUN_0040BBF6 lines 69-73: Edge highlights along left and right beam edges
- if (_rebelDetailMode >= 0) {
+ if (_rebelDetailMode >= 0 && edgeClipLeft <= edgeClipRight && edgeClipTop <= edgeClipBottom) {
drawEdgeHighlightLine(dst, pitch, width, height,
startX - halfLines, startY,
- endX - halfLines, endY);
+ endX - halfLines, endY,
+ edgeClipLeft, edgeClipTop, edgeClipRight, edgeClipBottom);
drawEdgeHighlightLine(dst, pitch, width, height,
(startX - halfLines) + numLines - 1, startY,
- (endX - halfLines) + numLines - 1, endY);
+ (endX - halfLines) + numLines - 1, endY,
+ edgeClipLeft, edgeClipTop, edgeClipRight, edgeClipBottom);
}
}
+
}
void InsaneRebel2::drawLine(byte *dst, int pitch, int width, int height, int x0, int y0, int x1, int y1, byte color) {
int dx = abs(x1 - x0), sx = x0 < x1 ? 1 : -1;
@@ -3842,14 +4093,13 @@ void InsaneRebel2::renderHandler25LaserShots(byte *renderBitmap, int pitch, int
pan = CLIP<int16>(pan, -127, 127);
// TODO: Apply panning to sound channel i+1
- // Target position (where player clicked)
- int16 targetX = _turretShots[i].targetX;
- int16 targetY = _turretShots[i].targetY;
+ // Retail adds DAT_0045790c/0e at render time to both gun and target.
+ int16 targetX = _turretShots[i].targetX + _rebelViewOffsetX;
+ int16 targetY = _turretShots[i].targetY + _rebelViewOffsetY;
- // Gun position computed at spawn time from GRD002 sprite data
- // Original: DAT_0045791c[i] + DAT_0045790c, DAT_00457920[i] + DAT_0045790e
- int16 gunX = _turretShots[i].gunX;
- int16 gunY = _turretShots[i].gunY;
+ // Gun position computed at spawn time in base coords (DAT_0045791c/20).
+ int16 gunX = _turretShots[i].gunX + _rebelViewOffsetX;
+ int16 gunY = _turretShots[i].gunY + _rebelViewOffsetY;
int16 progress = maxDuration - _turretShots[i].counter;
Commit: 0aacf5d751af287602bd6f27d519592402832aa3
https://github.com/scummvm/scummvm/commit/0aacf5d751af287602bd6f27d519592402832aa3
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:18+02:00
Commit Message:
SCUMM: RA2: Improve level selection
Changed paths:
engines/scumm/smush/smush_player.cpp
engines/scumm/smush/smush_player.h
engines/scumm/smush/smush_player_ra2.cpp
diff --git a/engines/scumm/smush/smush_player.cpp b/engines/scumm/smush/smush_player.cpp
index 614cba00a93..10b62b142ef 100644
--- a/engines/scumm/smush/smush_player.cpp
+++ b/engines/scumm/smush/smush_player.cpp
@@ -964,8 +964,8 @@ void SmushPlayer::handleZlibFrameObject(int32 subSize, Common::SeekableReadStrea
byte *ptr = fobjBuffer;
int codec = READ_LE_UINT16(ptr); ptr += 2;
- int left = READ_LE_UINT16(ptr); ptr += 2;
- int top = READ_LE_UINT16(ptr); ptr += 2;
+ int left = isRA2() ? (int16)READ_LE_UINT16(ptr) : (int)READ_LE_UINT16(ptr); ptr += 2;
+ int top = isRA2() ? (int16)READ_LE_UINT16(ptr) : (int)READ_LE_UINT16(ptr); ptr += 2;
int width = READ_LE_UINT16(ptr); ptr += 2;
int height = READ_LE_UINT16(ptr); ptr += 2;
@@ -984,8 +984,8 @@ void SmushPlayer::handleFrameObject(int32 subSize, Common::SeekableReadStream &b
}
int codec = b.readUint16LE();
- int left = b.readUint16LE();
- int top = b.readUint16LE();
+ int left = isRA2() ? (int)b.readSint16LE() : (int)b.readUint16LE();
+ int top = isRA2() ? (int)b.readSint16LE() : (int)b.readUint16LE();
int width = b.readUint16LE();
int height = b.readUint16LE();
@@ -1005,6 +1005,10 @@ void SmushPlayer::handleFrameObject(int32 subSize, Common::SeekableReadStream &b
assert(chunk_buffer);
b.read(chunk_buffer, chunk_size);
+ if (isRA2()) {
+ ra2RememberLastFobj(codec, chunk_buffer, chunk_size, left, top, width, height);
+ }
+
if (_storeFrame && isRA2()) {
ra2StoreFobjData(codec, chunk_buffer, chunk_size, left, top, width, height);
}
@@ -1018,6 +1022,8 @@ void SmushPlayer::handleFrame(int32 frameSize, Common::SeekableReadStream &b) {
debugC(DEBUG_SMUSH, "SmushPlayer::handleFrame(%d)", _frame);
uint8 *audioChunk = nullptr;
_skipNext = false;
+ if (isRA2())
+ _hasFrameFobjForGost = false;
if (_insanity) {
_vm->_insane->procPreRendering(_dst);
@@ -1087,11 +1093,8 @@ void SmushPlayer::handleFrame(int32 frameSize, Common::SeekableReadStream &b) {
handleLoad(subSize, b);
break;
case MKTAG('G','O','S','T'):
- // GOST = ghost sprite overlay. Re-renders previous FOBJ data at a new
- // position with priority/transparency flags. Data: 2-byte priority type
- // (0/1/2 â 0x2000/0x4000/0x6000), 2-byte x, 2-byte y.
- // TODO: Implement proper ghost rendering by saving previous FOBJ data
- // and re-rendering it with modified coordinates and priority flags.
+ if (isRA2())
+ ra2HandleGost(subSize, b);
break;
default:
error("Unknown frame subChunk found : %s, %d", tag2str(subType), subSize);
diff --git a/engines/scumm/smush/smush_player.h b/engines/scumm/smush/smush_player.h
index 7c12fcfa9ac..cc51ccd96f6 100644
--- a/engines/scumm/smush/smush_player.h
+++ b/engines/scumm/smush/smush_player.h
@@ -166,6 +166,17 @@ private:
int _storedFobjWidth;
int _storedFobjHeight;
+ // RA2: Most recently decoded FOBJ in the current frame, used by GOST chunks
+ // to re-render the same sprite payload at a different position.
+ byte *_lastFobjData;
+ int32 _lastFobjDataSize;
+ int _lastFobjCodec;
+ int _lastFobjLeft;
+ int _lastFobjTop;
+ int _lastFobjWidth;
+ int _lastFobjHeight;
+ bool _hasFrameFobjForGost;
+
Common::String _seekFile;
uint32 _startFrame;
uint32 _startTime;
@@ -309,6 +320,9 @@ private:
int width, int height, int pitch, int dataSize);
void ra2StoreFobjData(int codec, const byte *data, int32 dataSize,
int left, int top, int width, int height);
+ void ra2RememberLastFobj(int codec, const byte *data, int32 dataSize,
+ int left, int top, int width, int height);
+ void ra2HandleGost(int32 subSize, Common::SeekableReadStream &b);
void ra2ResetDeltaPalette();
SmushFont *ra2GetFont(int font);
void ra2ParseNextFrame();
diff --git a/engines/scumm/smush/smush_player_ra2.cpp b/engines/scumm/smush/smush_player_ra2.cpp
index 913efd064a8..fc2da674632 100644
--- a/engines/scumm/smush/smush_player_ra2.cpp
+++ b/engines/scumm/smush/smush_player_ra2.cpp
@@ -61,6 +61,14 @@ void SmushPlayer::ra2InitFields() {
_storedFobjTop = 0;
_storedFobjWidth = 0;
_storedFobjHeight = 0;
+ _lastFobjData = nullptr;
+ _lastFobjDataSize = 0;
+ _lastFobjCodec = 0;
+ _lastFobjLeft = 0;
+ _lastFobjTop = 0;
+ _lastFobjWidth = 0;
+ _lastFobjHeight = 0;
+ _hasFrameFobjForGost = false;
_skipNext = false;
_ra2FastForwarding = false;
_fobjOffsetX = 0;
@@ -84,6 +92,8 @@ void SmushPlayer::ra2DestroyFields() {
_multiFont = nullptr;
free(_storedFobjData);
_storedFobjData = nullptr;
+ free(_lastFobjData);
+ _lastFobjData = nullptr;
free(_loadBuffer);
_loadBuffer = nullptr;
}
@@ -127,6 +137,10 @@ void SmushPlayer::ra2ReleaseVideo() {
free(_storedFobjData);
_storedFobjData = nullptr;
_storedFobjDataSize = 0;
+ free(_lastFobjData);
+ _lastFobjData = nullptr;
+ _lastFobjDataSize = 0;
+ _hasFrameFobjForGost = false;
// Preserve _frameBuffer across videos so that gameplay videos (which have no
// background FOBJ) can use the stored background from the previous BEG video.
}
@@ -377,6 +391,82 @@ void SmushPlayer::ra2StoreFobjData(int codec, const byte *data, int32 dataSize,
_storeFrame = false;
}
+/**
+ * Cache the most recent frame FOBJ for GOST re-rendering.
+ */
+void SmushPlayer::ra2RememberLastFobj(int codec, const byte *data, int32 dataSize,
+ int left, int top, int width, int height) {
+ if (dataSize <= 0) {
+ _hasFrameFobjForGost = false;
+ return;
+ }
+
+ byte *newData = (byte *)realloc(_lastFobjData, dataSize);
+ if (newData == nullptr) {
+ warning("SmushPlayer::ra2RememberLastFobj: Failed to allocate %d bytes", dataSize);
+ free(_lastFobjData);
+ _lastFobjData = nullptr;
+ _lastFobjDataSize = 0;
+ _hasFrameFobjForGost = false;
+ return;
+ }
+
+ _lastFobjData = newData;
+ memcpy(_lastFobjData, data, dataSize);
+ _lastFobjDataSize = dataSize;
+ _lastFobjCodec = codec;
+ _lastFobjLeft = left;
+ _lastFobjTop = top;
+ _lastFobjWidth = width;
+ _lastFobjHeight = height;
+ _hasFrameFobjForGost = true;
+}
+
+/**
+ * RA2 GOST chunk handler.
+ * Re-renders the most recent frame FOBJ at the supplied ghost position.
+ */
+void SmushPlayer::ra2HandleGost(int32 subSize, Common::SeekableReadStream &b) {
+ if (subSize < 6) {
+ warning("SmushPlayer::ra2HandleGost: chunk too small (%d bytes)", subSize);
+ return;
+ }
+
+ int16 ghostType = b.readSint16LE();
+ int16 ghostX = b.readSint16LE();
+ int16 ghostY = b.readSint16LE();
+
+ if (!_hasFrameFobjForGost || _lastFobjData == nullptr || _lastFobjDataSize <= 0) {
+ debug("SmushPlayer GOST: frame=%d ignored (no current-frame FOBJ cached)", _frame);
+ return;
+ }
+
+ uint16 priorityFlags = 0;
+ if (ghostType == 0) {
+ priorityFlags = 0x2000;
+ } else if (ghostType == 1) {
+ priorityFlags = 0x4000;
+ } else if (ghostType == 2) {
+ priorityFlags = 0x6000;
+ }
+
+ // Match FUN_0042cba0 default behavior (flags bit 0 clear): GOST coordinates
+ // are relative to the cached FOBJ header position.
+ int left = _lastFobjLeft + ghostX;
+ int top = _lastFobjTop + ghostY;
+
+ debug("SmushPlayer GOST: frame=%d type=%d flags=0x%04x gostPos=(%d,%d) basePos=(%d,%d) finalPos=(%d,%d) size=%dx%d codec=%d",
+ _frame, ghostType, priorityFlags, ghostX, ghostY,
+ _lastFobjLeft, _lastFobjTop, left, top,
+ _lastFobjWidth, _lastFobjHeight, _lastFobjCodec);
+
+ // Priority bits (0x2000/0x4000/0x6000) are currently not modeled in
+ // ScummVM's SMUSH decoders. Coordinate-correct re-decode restores expected
+ // RA2 chapter preview behavior.
+ decodeFrameObject(_lastFobjCodec, _lastFobjData, left, top,
+ _lastFobjWidth, _lastFobjHeight, _lastFobjDataSize);
+}
+
/**
* Reset XPAL delta palette from the current base palette.
* Prevents stale delta values from a previous video corrupting the palette.
Commit: 3653d5839f06a0d169782a50c270e3d38c707c78
https://github.com/scummvm/scummvm/commit/3653d5839f06a0d169782a50c270e3d38c707c78
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:18+02:00
Commit Message:
SCUMM: RA2: Fix score font rendering
Changed paths:
engines/scumm/insane/insane_rebel.cpp
engines/scumm/insane/insane_rebel_render.cpp
diff --git a/engines/scumm/insane/insane_rebel.cpp b/engines/scumm/insane/insane_rebel.cpp
index 98f1e4c1580..a33ae7f0511 100644
--- a/engines/scumm/insane/insane_rebel.cpp
+++ b/engines/scumm/insane/insane_rebel.cpp
@@ -799,11 +799,11 @@ void InsaneRebel2::addScore(int points) {
// Render score text to HUD (part of FUN_0041c012)
// FUN_0041c012 lines 133-137: calls FUN_00434cb0 with format "%07ld"
-// Position (low-res): X = 0x101 (257), Y = 4 within status bar â screen Y = 184
+// using DAT_00482200 (DISPFONT in low-res mode).
void InsaneRebel2::renderScoreHUD(byte *renderBitmap, int pitch, int width, int height, int statusBarY) {
- (void)statusBarY;
-
- if (!_smush_dispfontNut)
+ // In low-res mode, score text shares the same NUT as the status bar sprites.
+ NutRenderer *statusFont = _smush_cockpitNut;
+ if (!statusFont)
return;
char scoreStr[16];
@@ -811,9 +811,9 @@ void InsaneRebel2::renderScoreHUD(byte *renderBitmap, int pitch, int width, int
// Score position from FUN_0041c012 assembly (low-res mode):
// X = ((DAT_0047a808 < 2) - 1 & 0x101) + 0x101 = 0x101 = 257
- // Y = ((DAT_0047a808 < 2) - 1 & 4) + 4 = 4 (within status bar at Y=180)
+ // Y = ((DAT_0047a808 < 2) - 1 & 4) + 4 = 4 (within status bar)
int scoreX = 257 + _viewX;
- int scoreY = 180 + 4 + _viewY;
+ int scoreY = statusBarY + 4 + _viewY;
// Render each digit as a NUT sprite (direct pixel blit with color 0 transparency).
// This matches the original's FUN_00434cb0 â FUN_004341a0 text rendering which
@@ -825,11 +825,11 @@ void InsaneRebel2::renderScoreHUD(byte *renderBitmap, int pitch, int width, int
int x = scoreX;
for (int i = 0; scoreStr[i] != '\0'; i++) {
byte ch = (byte)scoreStr[i];
- if (ch < _smush_dispfontNut->getNumChars()) {
- int charX = x + _smush_dispfontNut->getCharXOffset(ch);
- int charY = scoreY + _smush_dispfontNut->getCharYOffset(ch);
- renderNutSprite(renderBitmap, pitch, width, height, charX, charY, _smush_dispfontNut, ch);
- x += _smush_dispfontNut->getCharWidth(ch);
+ if (ch < statusFont->getNumChars()) {
+ int charX = x + statusFont->getCharXOffset(ch);
+ int charY = scoreY + statusFont->getCharYOffset(ch);
+ renderNutSprite(renderBitmap, pitch, width, height, charX, charY, statusFont, ch);
+ x += statusFont->getCharWidth(ch);
}
}
}
diff --git a/engines/scumm/insane/insane_rebel_render.cpp b/engines/scumm/insane/insane_rebel_render.cpp
index b13e2a76d48..fcda6c0c086 100644
--- a/engines/scumm/insane/insane_rebel_render.cpp
+++ b/engines/scumm/insane/insane_rebel_render.cpp
@@ -2493,7 +2493,7 @@ void InsaneRebel2::procPostRendering(byte *renderBitmap, int32 codecparam, int32
renderCrosshair(renderBitmap, pitch, width, height);
// HUD score/lives rendering (FUN_0041c012)
- renderScoreHUD(renderBitmap, pitch, width, height, 0);
+ renderScoreHUD(renderBitmap, pitch, width, height, statusBarY);
// Reset FOBJ position offsets (FUN_00424510(0,0) in original FUN_0041DB5E line 271)
if (_player) {
Commit: 8d927d5377d03d5acb32f55004ae99912abff036
https://github.com/scummvm/scummvm/commit/8d927d5377d03d5acb32f55004ae99912abff036
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:18+02:00
Commit Message:
SCUMM: RA2: Fix clipped sprite decoding
Changed paths:
engines/scumm/insane/insane_rebel.cpp
engines/scumm/insane/insane_rebel.h
engines/scumm/insane/insane_rebel_levels.cpp
engines/scumm/insane/insane_rebel_render.cpp
engines/scumm/insane/insane_rebel_runlevels.cpp
diff --git a/engines/scumm/insane/insane_rebel.cpp b/engines/scumm/insane/insane_rebel.cpp
index a33ae7f0511..c66b660d78c 100644
--- a/engines/scumm/insane/insane_rebel.cpp
+++ b/engines/scumm/insane/insane_rebel.cpp
@@ -465,6 +465,7 @@ InsaneRebel2::InsaneRebel2(ScummEngine_v7 *scumm) {
_deathFrame = 0;
_phaseScore = 0;
_phaseMisses = 0;
+ _skipSectionRequested = false;
// Register as EventObserver to capture input events before ScummEngine consumes them
_vm->_system->getEventManager()->getEventDispatcher()->registerObserver(this, 1, false);
@@ -577,6 +578,19 @@ bool InsaneRebel2::notifyEvent(const Common::Event &event) {
}
break;
+ case Common::KEYCODE_s:
+ // Debug shortcut: Shift+S skips the current gameplay section.
+ if (splayer &&
+ _gameState == kStateGameplay &&
+ _rebelHandler != 0 &&
+ event.kbd.hasFlags(Common::KBD_SHIFT)) {
+ _skipSectionRequested = true;
+ debug("Rebel2: Shift+S pressed - requesting gameplay section skip");
+ _vm->_smushVideoShouldFinish = true;
+ return true; // Consume the event
+ }
+ break;
+
default:
break;
}
diff --git a/engines/scumm/insane/insane_rebel.h b/engines/scumm/insane/insane_rebel.h
index 2c30a1efbe5..807ecc0a8b3 100644
--- a/engines/scumm/insane/insane_rebel.h
+++ b/engines/scumm/insane/insane_rebel.h
@@ -367,6 +367,7 @@ public:
int _deathFrame; // Frame number where player died (for death video selection)
int _phaseScore; // Accumulated score from previous phases (preserved on phase retry)
int _phaseMisses; // Accumulated misses from previous phases
+ bool _skipSectionRequested; // Debug shortcut (Shift+S): force current gameplay section to end
// =============================================================
diff --git a/engines/scumm/insane/insane_rebel_levels.cpp b/engines/scumm/insane/insane_rebel_levels.cpp
index 7bf13aa5c2c..23acf9645da 100644
--- a/engines/scumm/insane/insane_rebel_levels.cpp
+++ b/engines/scumm/insane/insane_rebel_levels.cpp
@@ -507,6 +507,7 @@ int InsaneRebel2::runLevel(int levelId) {
_currentPhase = 1;
_phaseScore = 0;
_phaseMisses = 0;
+ _skipSectionRequested = false;
// Dispatch to per-level handler
switch (levelId) {
diff --git a/engines/scumm/insane/insane_rebel_render.cpp b/engines/scumm/insane/insane_rebel_render.cpp
index fcda6c0c086..f32197b8b40 100644
--- a/engines/scumm/insane/insane_rebel_render.cpp
+++ b/engines/scumm/insane/insane_rebel_render.cpp
@@ -2126,6 +2126,58 @@ void InsaneRebel2::renderNutSprite(byte *dst, int pitch, int width, int height,
renderNutSpriteMirrored(dst, pitch, width, height, x, y, nut, spriteIdx, false);
}
+static void renderNutSpriteClipped(byte *dst, int pitch, int dstH,
+ int clipLeft, int clipTop, int clipRight, int clipBottom,
+ int x, int y, NutRenderer *nut, int spriteIdx) {
+ if (!nut || spriteIdx < 0 || spriteIdx >= nut->getNumChars())
+ return;
+
+ if (clipLeft < 0)
+ clipLeft = 0;
+ if (clipTop < 0)
+ clipTop = 0;
+ if (clipRight > pitch)
+ clipRight = pitch;
+ if (clipBottom > dstH)
+ clipBottom = dstH;
+ if (clipLeft >= clipRight || clipTop >= clipBottom)
+ return;
+
+ int w = nut->getCharWidth(spriteIdx);
+ int h = nut->getCharHeight(spriteIdx);
+ const byte *src = nut->getCharData(spriteIdx);
+ if (!src || w <= 0 || h <= 0)
+ return;
+
+ for (int iy = 0; iy < h; ++iy) {
+ int dstY = y + iy;
+ if (dstY < clipTop || dstY >= clipBottom)
+ continue;
+
+ const byte *s = src + iy * w;
+ byte *d = dst + dstY * pitch;
+
+ int dstStart = x;
+ int dstEnd = x + w;
+ int srcStart = 0;
+ if (dstStart < clipLeft) {
+ srcStart = clipLeft - dstStart;
+ dstStart = clipLeft;
+ }
+ if (dstEnd > clipRight)
+ dstEnd = clipRight;
+ if (dstStart >= dstEnd)
+ continue;
+
+ int copyCount = dstEnd - dstStart;
+ for (int ix = 0; ix < copyCount; ++ix) {
+ byte px = s[srcStart + ix];
+ if (px != 0)
+ d[dstStart + ix] = px;
+ }
+ }
+}
+
// Render NUT sprite with optional horizontal mirroring
// Based on FUN_004236e0 disassembly - flags=0x2001 triggers horizontal flip
void InsaneRebel2::renderNutSpriteMirrored(byte *dst, int pitch, int width, int height, int x, int y, NutRenderer *nut, int spriteIdx, bool mirror) {
@@ -3314,33 +3366,26 @@ void InsaneRebel2::renderHandler25ShipPre(byte *renderBitmap, int pitch, int wid
int drawX = _rebelViewOffset2X + spriteXOffset + _viewX;
int drawY = _rebelViewOffset2Y + spriteYOffset + _viewY;
- // Apply width-halving logic from original assembly:
- // When damage==0 (uncovered), the original halves DAT_00482234 (buffer width)
- // This clips the sprite to only half the screen.
- int renderWidth = width;
- byte *dstBitmap = renderBitmap;
-
+ // Apply half-width clipping from FUN_41DB5E:
+ // - mode1 uncovered: left half
+ // - mode4 uncovered: right half
+ int clipLeft = 0;
+ int clipRight = width;
if (useHalfWidth) {
- renderWidth = width / 2; // Clip to half width (160 pixels)
-
- if (useRightHalf) {
- // Mode 4: Draw to right half by offsetting the destination buffer
- // Original: DAT_00482230 += DAT_00482234 (adds 160 to buffer start)
- // This makes drawing appear on the right half (pixels 160-319)
- dstBitmap = renderBitmap + (width / 2);
- }
+ const int halfWidth = width / 2;
+ clipLeft = useRightHalf ? halfWidth : 0;
+ clipRight = clipLeft + halfWidth;
}
- // GRD001 uses transparent rendering (color 0 = transparent).
- // The original uses flags=0 (opaque) in FUN_004236e0, but the NUT pre-decode
- // already fills color-0 positions with kDefaultTransparentColor (0).
- // Using transparent rendering here lets the corridor show through any
- // color-0 border/padding pixels in GRD001, avoiding black-over-corridor artifacts.
- renderNutSprite(dstBitmap, pitch, renderWidth, renderHeight, drawX, drawY, _grd001Sprite, 0);
+ // FUN_41DB5E mode-4 uncovered mutates DAT_00482230/34 (clip region),
+ // not DAT_00457910 (draw X). Keep drawX unchanged and clip only.
+ renderNutSpriteClipped(renderBitmap, pitch, renderHeight,
+ clipLeft, 0, clipRight, renderHeight,
+ drawX, drawY, _grd001Sprite, 0);
- debug("Rebel2 Handler25 PRE: GRD001 at (%d,%d) nutOff(%d,%d) viewOff(%d,%d) size(%d,%d) mode=%d dmg=%d halfW=%d rightHalf=%d renderW=%d",
+ debug("Rebel2 Handler25 PRE: GRD001 at (%d,%d) nutOff(%d,%d) viewOff(%d,%d) size(%d,%d) mode=%d dmg=%d halfW=%d rightHalf=%d clip=[%d,%d)",
drawX, drawY, spriteXOffset, spriteYOffset, _rebelViewOffset2X, _rebelViewOffset2Y,
- spriteW, spriteH, _grdSpriteMode, _rebelDamageLevel, useHalfWidth ? 1 : 0, useRightHalf ? 1 : 0, renderWidth);
+ spriteW, spriteH, _grdSpriteMode, _rebelDamageLevel, useHalfWidth ? 1 : 0, useRightHalf ? 1 : 0, clipLeft, clipRight);
}
}
diff --git a/engines/scumm/insane/insane_rebel_runlevels.cpp b/engines/scumm/insane/insane_rebel_runlevels.cpp
index 3a729f4deca..8ee4482d399 100644
--- a/engines/scumm/insane/insane_rebel_runlevels.cpp
+++ b/engines/scumm/insane/insane_rebel_runlevels.cpp
@@ -110,6 +110,16 @@ uint16 InsaneRebel2::processWaveEnd(int16 mask, int16 *budget, int16 threshold,
uint16 result = 0;
+ // Debug shortcut path: force-end current section when requested via Shift+S.
+ // This returns the same sentinel (0xFFFF) used for section completion/death/quit.
+ if (_skipSectionRequested) {
+ _skipSectionRequested = false;
+ _rebelPhaseState = mask;
+ _rebelWaveState = mask;
+ debug("Rebel2 processWaveEnd: Shift+S skip consumed (mask=0x%x)", (uint16)mask);
+ return 0xFFFF;
+ }
+
// Step 1: Wait for video to finish (lines 21-32)
// Original loop: while (damage < 0xff && frame < maxFrame-1 && !escPressed)
// The SmushPlayer::play() call already blocks until video ends, so this step
Commit: a988ea1aae675b8f9bbd9d775e7a4ce1c739c1de
https://github.com/scummvm/scummvm/commit/a988ea1aae675b8f9bbd9d775e7a4ce1c739c1de
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:19+02:00
Commit Message:
SCUMM: RA2: Improve collisions and shadow indicators
Changed paths:
engines/scumm/insane/insane_rebel.cpp
engines/scumm/insane/insane_rebel.h
engines/scumm/insane/insane_rebel_iact.cpp
engines/scumm/insane/insane_rebel_render.cpp
diff --git a/engines/scumm/insane/insane_rebel.cpp b/engines/scumm/insane/insane_rebel.cpp
index c66b660d78c..7c5c829c344 100644
--- a/engines/scumm/insane/insane_rebel.cpp
+++ b/engines/scumm/insane/insane_rebel.cpp
@@ -349,9 +349,11 @@ InsaneRebel2::InsaneRebel2(ScummEngine_v7 *scumm) {
// Initialize Handler 7 FLY ship system
_flyShipSprite = nullptr; // FLY001 - 35 direction frames
- _flyLaserSprite = nullptr; // FLY002 - laser sprites
+ _flyLaserSprite = nullptr; // FLY002 - effect sprites (danger/overlay cues)
_flyTargetSprite = nullptr; // FLY003 - targeting overlay
_flyHiResSprite = nullptr; // FLY004 - high-res alternative
+ _flyEffectAnimCounter = 0; // DAT_0047ff1c
+ _flyOverlayRepeatCount = 0; // DAT_00443b52
_flyShipScreenX = 0xd4; // Start at center (212) - matches DAT_00443708 default
_flyShipScreenY = 0x82; // Start at center (130) - matches DAT_0044370a default
_smoothedVelocity = 0; // DAT_0044370c
diff --git a/engines/scumm/insane/insane_rebel.h b/engines/scumm/insane/insane_rebel.h
index 807ecc0a8b3..3a0d47784a3 100644
--- a/engines/scumm/insane/insane_rebel.h
+++ b/engines/scumm/insane/insane_rebel.h
@@ -722,7 +722,8 @@ public:
// Mode 0/2: Obstacle collision using secondary zones â inside quad = hit
// Mode 1/3: Wall/boundary collision using primary zones â per-edge push-back
// Uses ship position (_flyShipScreenX/_flyShipScreenY) in raw buffer coords
- void checkHandler7CollisionZones();
+ // and draws proximity shadow cues for nearby danger zones.
+ void checkHandler7CollisionZones(byte *renderBitmap, int pitch, int width, int height, int32 curFrame);
int16 _playerDamage; // Legacy damage counter (kept for compatibility/telemetry)
int16 _playerShield; // Shields: 0..255 where 255 = full
@@ -959,16 +960,18 @@ public:
//
// Based on FUN_0040c3cc and FUN_0040d836 disassembly:
// - DAT_0047fee8: Ship direction sprites (FLY001, par3=1, 35 frames)
- // - DAT_0047fef0: Laser fire sprites (FLY002, par3=3)
+ // - DAT_0047fef0: Ship effect sprites (FLY002, par3=3)
// - DAT_0047fef8: Targeting overlay (FLY003, par3=2)
// - DAT_0047ff00: High-res alternative (FLY004, par3=11)
// - DAT_00443708: Ship X position, DAT_0044370a: Ship Y position
// - DAT_0044370c: Smoothed horizontal velocity, DAT_0044370e: Vertical input
NutRenderer *_flyShipSprite; // DAT_0047fee8 - FLY001 (35 direction frames)
- NutRenderer *_flyLaserSprite; // DAT_0047fef0 - FLY002
+ NutRenderer *_flyLaserSprite; // DAT_0047fef0 - FLY002 (danger/overlay effects)
NutRenderer *_flyTargetSprite; // DAT_0047fef8 - FLY003
NutRenderer *_flyHiResSprite; // DAT_0047ff00 - FLY004
+ int16 _flyEffectAnimCounter; // DAT_0047ff1c - animated FLY002 cue counter
+ int16 _flyOverlayRepeatCount; // DAT_00443b52 - repeats for ship overlay effect
// Handler 7 ship state (FUN_40C3CC / FUN_0040d836)
// Position in game coordinate space [20,404]x[20,240], center=(212,130)
diff --git a/engines/scumm/insane/insane_rebel_iact.cpp b/engines/scumm/insane/insane_rebel_iact.cpp
index 416fb281f2b..1ecd8052fa1 100644
--- a/engines/scumm/insane/insane_rebel_iact.cpp
+++ b/engines/scumm/insane/insane_rebel_iact.cpp
@@ -265,8 +265,9 @@ void InsaneRebel2::iactRebel2Scene1(byte *renderBitmap, int32 codecparam, int32
body0, body1, _corridorRightX);
break;
case 5:
- // Flag value
- debug("Rebel2 Opcode 7 par4=5: flag=%d", body0);
+ // DAT_00443b52: repeats FLY002 ship overlay in FUN_40D836.
+ _flyOverlayRepeatCount = body0;
+ debug("Rebel2 Opcode 7 par4=5: flyOverlayRepeat=%d", _flyOverlayRepeatCount);
break;
default:
debug("Rebel2 Opcode 7 par4=%d: body=(%d,%d) â unknown sub-opcode", par4, body0, body1);
diff --git a/engines/scumm/insane/insane_rebel_render.cpp b/engines/scumm/insane/insane_rebel_render.cpp
index f32197b8b40..6bf314d3f40 100644
--- a/engines/scumm/insane/insane_rebel_render.cpp
+++ b/engines/scumm/insane/insane_rebel_render.cpp
@@ -1846,7 +1846,7 @@ void InsaneRebel2::checkCollisionZones() {
}
}
-void InsaneRebel2::checkHandler7CollisionZones() {
+void InsaneRebel2::checkHandler7CollisionZones(byte *renderBitmap, int pitch, int width, int height, int32 curFrame) {
// FUN_40E35E â Handler 7 per-frame collision system.
// Uses ship position (_flyShipScreenX/_flyShipScreenY) in raw buffer coords.
// Two modes depending on _flyControlMode:
@@ -1855,6 +1855,10 @@ void InsaneRebel2::checkHandler7CollisionZones() {
// Note: _hitCooldown is decremented in renderSpaceExplosions (FUN_40F1C5)
// to match the original where the decrement happens during rendering.
+ //
+ // local_c in FUN_40E35E: proximity mask for nearby danger-zone shadow cues.
+ // bit 0=left, bit 1=right, bit 2=top, bit 3=bottom
+ uint16 warningMask = 0;
if (_flyControlMode == 0 || _flyControlMode == 2) {
// ---- Mode 0/2: Obstacle collision using SECONDARY zones (FUN_403b5b) ----
@@ -1931,10 +1935,20 @@ void InsaneRebel2::checkHandler7CollisionZones() {
}
}
}
+
+ // FUN_40E35E line 104: mark near-danger proximity for shadow cue rendering.
+ // Uses the low byte of zone.filterValue (retail local_1c) to pick direction bits.
+ if (zone.field2 - 13 < zone.field1) {
+ uint32 bit = 4u << ((byte)zone.filterValue & 0x1f);
+ warningMask = (uint16)(warningMask | (uint16)bit);
+ }
}
- // Corridor boundary proximity (lines 127-131)
- // These flags are used for directional indicators (not critical for damage)
+ // Corridor side proximity (FUN_40E35E lines 127-131)
+ if (_flyShipScreenX < _corridorLeftX + 0x28)
+ warningMask |= 1;
+ if (_corridorRightX - 0x28 < _flyShipScreenX)
+ warningMask |= 2;
} else {
// ---- Mode 1/3: Wall/boundary collision using PRIMARY zones (FUN_403b34) ----
@@ -1967,13 +1981,15 @@ void InsaneRebel2::checkHandler7CollisionZones() {
_playerDamage = 255;
_rebelHitCounter++;
_hitCooldown = 10;
- playSfx(1, 127, 0); // CRASH.SAD, top wall â center pan
debug("Rebel2: Handler7 Mode1/3 TOP WALL ship=(%d,%d) edgeY=%d damage=%d",
_flyShipScreenX, _flyShipScreenY, edgeY, damage);
}
_spaceShotDirection = 2; // Direction: pushed down
_flyShipScreenY = edgeY; // Push-back
+ playSfx(1, 127, 0); // CRASH.SAD, top wall â center pan (always)
initDamageFlash();
+ } else if (_flyShipScreenY < edgeY + 0x28) {
+ warningMask |= 4;
}
}
@@ -1990,13 +2006,15 @@ void InsaneRebel2::checkHandler7CollisionZones() {
_playerDamage = 255;
_rebelHitCounter++;
_hitCooldown = 10;
- playSfx(1, 127, 0); // CRASH.SAD, bottom wall â center pan
debug("Rebel2: Handler7 Mode1/3 BOTTOM WALL ship=(%d,%d) edgeY=%d damage=%d",
_flyShipScreenX, _flyShipScreenY, edgeY, damage);
}
_spaceShotDirection = 3; // Direction: pushed up
_flyShipScreenY = edgeY; // Push-back
+ playSfx(1, 127, 0); // CRASH.SAD, bottom wall â center pan (always)
initDamageFlash();
+ } else if (edgeY - 0x28 < _flyShipScreenY) {
+ warningMask |= 8;
}
}
@@ -2006,6 +2024,12 @@ void InsaneRebel2::checkHandler7CollisionZones() {
if (_flyShipScreenX < edgeX) {
// Ship left of left wall â push right
_flyShipScreenX = edgeX; // Push-back
+
+ // FUN_40E35E resets horizontal history to force immediate rightward correction.
+ for (int j = 0; j < ARRAYSIZE(_velocityHistory); j++) {
+ _velocityHistory[j] = 127;
+ }
+
if (_hitCooldown < 5 && !_rebelInvulnerable) {
int damage = wallDamage;
_playerDamage += damage;
@@ -2013,11 +2037,11 @@ void InsaneRebel2::checkHandler7CollisionZones() {
_playerDamage = 255;
_rebelHitCounter++;
_hitCooldown = 10;
- playSfx(1, 127, -100); // CRASH.SAD, left wall â pan left
debug("Rebel2: Handler7 Mode1/3 LEFT WALL ship=(%d,%d) edgeX=%d damage=%d",
_flyShipScreenX, _flyShipScreenY, edgeX, damage);
}
_spaceShotDirection = 0; // Direction: pushed right
+ playSfx(1, 127, -100); // CRASH.SAD, left wall â pan left (always)
initDamageFlash();
}
}
@@ -2028,6 +2052,12 @@ void InsaneRebel2::checkHandler7CollisionZones() {
if (edgeX < _flyShipScreenX) {
// Ship right of right wall â push left
_flyShipScreenX = edgeX; // Push-back
+
+ // FUN_40E35E resets horizontal history to force immediate leftward correction.
+ for (int j = 0; j < ARRAYSIZE(_velocityHistory); j++) {
+ _velocityHistory[j] = -127;
+ }
+
if (_hitCooldown < 5 && !_rebelInvulnerable) {
int damage = wallDamage;
_playerDamage += damage;
@@ -2035,16 +2065,40 @@ void InsaneRebel2::checkHandler7CollisionZones() {
_playerDamage = 255;
_rebelHitCounter++;
_hitCooldown = 10;
- playSfx(1, 127, 100); // CRASH.SAD, right wall â pan right
debug("Rebel2: Handler7 Mode1/3 RIGHT WALL ship=(%d,%d) edgeX=%d damage=%d",
_flyShipScreenX, _flyShipScreenY, edgeX, damage);
}
_spaceShotDirection = 1; // Direction: pushed left
+ playSfx(1, 127, 100); // CRASH.SAD, right wall â pan right (always)
initDamageFlash();
}
}
}
}
+
+ // FUN_40E35E tail: draw proximity danger shadow cues when enabled by frame/flags.
+ // Note: These are cue sprites (often perceived as "shadows"), not the aiming reticle.
+ LevelDifficultyParams dparams = getDifficultyParams();
+ if ((curFrame & 2) != 0 && (dparams.flags & 8) != 0 && _smush_iconsNut) {
+ int scale = (_vm->_screenWidth > 320 || _vm->_screenHeight > 200) ? 2 : 1;
+
+ if ((warningMask & 1) != 0 && _smush_iconsNut->getNumChars() > 0x2d) {
+ renderNutSprite(renderBitmap, pitch, width, height,
+ 0xd7 * scale + _viewX, 0x55 * scale + _viewY, _smush_iconsNut, 0x2d);
+ }
+ if ((warningMask & 2) != 0 && _smush_iconsNut->getNumChars() > 0x2c) {
+ renderNutSprite(renderBitmap, pitch, width, height,
+ 0x69 * scale + _viewX, 0x55 * scale + _viewY, _smush_iconsNut, 0x2c);
+ }
+ if ((warningMask & 4) != 0 && _smush_iconsNut->getNumChars() > 0x2b) {
+ renderNutSprite(renderBitmap, pitch, width, height,
+ 0xa0 * scale + _viewX, 0x82 * scale + _viewY, _smush_iconsNut, 0x2b);
+ }
+ if ((warningMask & 8) != 0 && _smush_iconsNut->getNumChars() > 0x2a) {
+ renderNutSprite(renderBitmap, pitch, width, height,
+ 0xa0 * scale + _viewX, 0x28 * scale + _viewY, _smush_iconsNut, 0x2a);
+ }
+ }
}
void InsaneRebel2::drawQuad(byte *dst, int pitch, int width, int height,
@@ -2533,7 +2587,7 @@ void InsaneRebel2::procPostRendering(byte *renderBitmap, int32 codecparam, int32
if (_rebelHandler == 0x26) {
checkCollisionZones();
} else if (_rebelHandler == 7) {
- checkHandler7CollisionZones();
+ checkHandler7CollisionZones(renderBitmap, pitch, width, height, curFrame);
}
// Collision zone visualization (debug - for Handler 7/8 pilot modes)
@@ -3187,44 +3241,84 @@ void InsaneRebel2::renderHandler7Ship(byte *renderBitmap, int pitch, int width,
if (spriteIndex >= numSprites)
spriteIndex = numSprites - 1;
- // Transform game coordinates to screen coordinates (FUN_0041c720 equivalent)
- // The perspective transform shifts the ship position based on perspective offsets.
- // Close view: FOBJ offset = (-52 - perspX, -45 - perspY), ship at screen center.
- // For now, use a simplified perspective: ship position = center + offset from center
- // scaled by perspective. In the original, FUN_00424510 shifts all FOBJ sprites.
- //
- // Screen position for sprite drawing (FUN_0040d836 line 174):
- // drawX = transformedX - 0xd4, drawY = transformedY - 0x82
- // Where transformedX/Y come from FUN_0041c720(shipX, shipY, perspX, perspY, viewShift)
- //
- // Simplified: screenX = 160 + (shipX - 212) * perspFactor
- // With the perspective formula, objects near center barely move, objects at edges move more.
- int drawX = (_flyShipScreenX - 0xd4) + _perspectiveX;
- int drawY = (_flyShipScreenY - 0x82) + _perspectiveY;
+ // Simplified FUN_41C720-like transform to current render buffer coordinates.
+ int shipCenterX = (_flyShipScreenX - 0xd4) + _perspectiveX + 160 + _viewX;
+ int shipCenterY = (_flyShipScreenY - 0x82) + _perspectiveY + 100 + _viewY;
+
+ // FUN_40D836 lines 108-136: FLY002 proximity cues near corridor danger.
+ if (_flyLaserSprite && _flyLaserSprite->getNumChars() > 0) {
+ const int laserChars = _flyLaserSprite->getNumChars();
+ _flyEffectAnimCounter++;
+
+ if (_flyControlMode == 0) {
+ int16 leftDist = _flyShipScreenX - _corridorLeftX;
+ if (leftDist < 0x32) {
+ int cueIndex = _flyEffectAnimCounter % 10;
+ if (cueIndex >= 0 && cueIndex < laserChars) {
+ int cueX = ((shipCenterX - 0x28) - leftDist) - leftDist / 2;
+ renderNutSprite(renderBitmap, pitch, width, height, cueX, shipCenterY, _flyLaserSprite, cueIndex);
+ }
+ }
+
+ int16 rightDist = _corridorRightX - _flyShipScreenX;
+ if (rightDist < 0x32) {
+ int cueIndex = (_flyEffectAnimCounter % 10) + 10;
+ if (cueIndex >= 0 && cueIndex < laserChars) {
+ int cueX = rightDist / 2 + rightDist + shipCenterX + 0x28;
+ renderNutSprite(renderBitmap, pitch, width, height, cueX, shipCenterY, _flyLaserSprite, cueIndex);
+ }
+ }
+ } else {
+ int16 bottomDist = _corridorBottomY - _flyShipScreenY;
+ int bottomX = shipCenterX;
+ int bottomY = (_corridorBottomY - 0x82) + _perspectiveY + 100 + _viewY;
+
+ if (bottomDist < 0x19) {
+ _flyEffectAnimCounter++;
+ int cueIndex = _flyEffectAnimCounter % 10;
+ if (cueIndex >= 0 && cueIndex < laserChars)
+ renderNutSprite(renderBitmap, pitch, width, height, bottomX, bottomY, _flyLaserSprite, cueIndex);
+ }
+ if (bottomDist < 0x32) {
+ _flyEffectAnimCounter++;
+ int cueIndex = _flyEffectAnimCounter % 10;
+ if (cueIndex >= 0 && cueIndex < laserChars)
+ renderNutSprite(renderBitmap, pitch, width, height, bottomX, bottomY, _flyLaserSprite, cueIndex);
+ }
- // Convert from game-center-relative to screen coordinates
- // The sprite system expects coordinates relative to the 320x200 frame
- // Center of frame = (160, 100), so offset = game position - game center
- drawX += 160 + _viewX;
- drawY += 100 + _viewY;
+ int cueIndex = _flyEffectAnimCounter % 10;
+ if (cueIndex >= 0 && cueIndex < laserChars)
+ renderNutSprite(renderBitmap, pitch, width, height, bottomX, bottomY, _flyLaserSprite, cueIndex);
+ }
+ }
// Center the sprite on the position
int spriteW = _flyShipSprite->getCharWidth(spriteIndex);
int spriteH = _flyShipSprite->getCharHeight(spriteIndex);
- drawX -= spriteW / 2;
- drawY -= spriteH / 2;
+ int drawX = shipCenterX - spriteW / 2;
+ int drawY = shipCenterY - spriteH / 2;
renderNutSprite(renderBitmap, pitch, width, height, drawX, drawY, _flyShipSprite, spriteIndex);
- // Laser overlay if firing (same position as ship)
- if (_shipFiring && _flyLaserSprite && _flyLaserSprite->getNumChars() > 0) {
- int laserIndex = spriteIndex % _flyLaserSprite->getNumChars();
- renderNutSprite(renderBitmap, pitch, width, height, drawX, drawY, _flyLaserSprite, laserIndex);
+ // FUN_40D836 lines 176-180: optional FLY002 overlay pass at ship position.
+ if (_flyLaserSprite && _flyOverlayRepeatCount > 0) {
+ int overlayIndex = spriteIndex + 0x14;
+ if (overlayIndex >= 0 && overlayIndex < _flyLaserSprite->getNumChars()) {
+ for (int i = 0; i < _flyOverlayRepeatCount; i++) {
+ renderNutSprite(renderBitmap, pitch, width, height, drawX, drawY, _flyLaserSprite, overlayIndex);
+ }
+ }
+ }
+
+ // FUN_40D836 lines 181-183: optional FLY003 overlay in high detail mode.
+ if (_flyTargetSprite && _rebelDetailMode >= 0 &&
+ spriteIndex >= 0 && spriteIndex < _flyTargetSprite->getNumChars()) {
+ renderNutSprite(renderBitmap, pitch, width, height, drawX, drawY, _flyTargetSprite, spriteIndex);
}
- debug("Rebel2 Handler7Ship: draw=(%d,%d) sprite=%d/%d shipPos=(%d,%d) persp=(%d,%d) smoothVel=%d vertIn=%d",
+ debug("Rebel2 Handler7Ship: draw=(%d,%d) sprite=%d/%d shipPos=(%d,%d) persp=(%d,%d) smoothVel=%d vertIn=%d fxCtr=%d fxRep=%d",
drawX, drawY, spriteIndex, numSprites, _flyShipScreenX, _flyShipScreenY,
- _perspectiveX, _perspectiveY, _smoothedVelocity, _verticalInput);
+ _perspectiveX, _perspectiveY, _smoothedVelocity, _verticalInput, _flyEffectAnimCounter, _flyOverlayRepeatCount);
}
void InsaneRebel2::renderHandler8Ship(byte *renderBitmap, int pitch, int width, int height) {
Commit: 8c4dfa645aac621f374efa32fd3b5fd6d1393da9
https://github.com/scummvm/scummvm/commit/8c4dfa645aac621f374efa32fd3b5fd6d1393da9
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:19+02:00
Commit Message:
SCUMM: RA2: Remove debug code
Changed paths:
engines/scumm/insane/insane_rebel.h
engines/scumm/insane/insane_rebel_render.cpp
diff --git a/engines/scumm/insane/insane_rebel.h b/engines/scumm/insane/insane_rebel.h
index 3a0d47784a3..a37d3347fce 100644
--- a/engines/scumm/insane/insane_rebel.h
+++ b/engines/scumm/insane/insane_rebel.h
@@ -699,14 +699,6 @@ public:
// Hit cooldown timer (DAT_0044374c) - prevents rapid damage stacking
int16 _hitCooldown;
- // Draw collision zone quadrilaterals for visualization/debugging
- // Called from procPostRendering when collision zones should be visible
- void drawCollisionZones(byte *dst, int pitch, int width, int height, byte color);
-
- // Draw a single quadrilateral (4 edges)
- void drawQuad(byte *dst, int pitch, int width, int height,
- int x1, int y1, int x2, int y2, int x3, int y3, int x4, int y4, byte color);
-
// Register a collision zone from IACT opcode 5 data
void registerCollisionZone(Common::SeekableReadStream &b, int16 subOpcode, int16 par4);
diff --git a/engines/scumm/insane/insane_rebel_render.cpp b/engines/scumm/insane/insane_rebel_render.cpp
index 6bf314d3f40..a62c1405bbe 100644
--- a/engines/scumm/insane/insane_rebel_render.cpp
+++ b/engines/scumm/insane/insane_rebel_render.cpp
@@ -2101,81 +2101,6 @@ void InsaneRebel2::checkHandler7CollisionZones(byte *renderBitmap, int pitch, in
}
}
-void InsaneRebel2::drawQuad(byte *dst, int pitch, int width, int height,
- int x1, int y1, int x2, int y2, int x3, int y3, int x4, int y4, byte color) {
- // Draw a quadrilateral by connecting its 4 vertices with lines
- // Vertex order: top-left (1), top-right (2), bottom-right (3), bottom-left (4)
- drawLine(dst, pitch, width, height, x1, y1, x2, y2, color); // Top edge
- drawLine(dst, pitch, width, height, x2, y2, x3, y3, color); // Right edge
- drawLine(dst, pitch, width, height, x3, y3, x4, y4, color); // Bottom edge
- drawLine(dst, pitch, width, height, x4, y4, x1, y1, color); // Left edge
-}
-
-void InsaneRebel2::drawCollisionZones(byte *dst, int pitch, int width, int height, byte color) {
- // Draw all active collision zones as wireframe quadrilaterals for debugging
- // Uses different colors for primary vs secondary zones
-
- const byte primaryColor = 44; // Bright red for primary (obstacle) zones
- const byte secondaryColor = 47; // Yellow for secondary (boundary) zones
-
- // Draw primary zones (sub-opcode 0x0D - obstacles)
- for (int i = 0; i < _primaryZoneCount; i++) {
- CollisionZone &zone = _primaryZones[i];
- if (!zone.active)
- continue;
-
- // Apply view offset to convert from video coords to screen coords
- int x1 = zone.x1 + _viewX;
- int y1 = zone.y1 + _viewY;
- int x2 = zone.x2 + _viewX;
- int y2 = zone.y2 + _viewY;
- int x3 = zone.x3 + _viewX;
- int y3 = zone.y3 + _viewY;
- int x4 = zone.x4 + _viewX;
- int y4 = zone.y4 + _viewY;
-
- drawQuad(dst, pitch, width, height, x1, y1, x2, y2, x3, y3, x4, y4, primaryColor);
- }
-
- // Draw secondary zones (sub-opcode 0x0E - boundaries)
- for (int i = 0; i < _secondaryZoneCount; i++) {
- CollisionZone &zone = _secondaryZones[i];
- if (!zone.active)
- continue;
-
- // Apply view offset
- int x1 = zone.x1 + _viewX;
- int y1 = zone.y1 + _viewY;
- int x2 = zone.x2 + _viewX;
- int y2 = zone.y2 + _viewY;
- int x3 = zone.x3 + _viewX;
- int y3 = zone.y3 + _viewY;
- int x4 = zone.x4 + _viewX;
- int y4 = zone.y4 + _viewY;
-
- drawQuad(dst, pitch, width, height, x1, y1, x2, y2, x3, y3, x4, y4, secondaryColor);
- }
-
- // Draw corridor boundaries as a rectangle (from IACT opcode 7)
- if (_corridorLeftX != 0 || _corridorRightX != 0x1A8) {
- const byte corridorColor = 45; // Cyan for corridor boundaries
- // Draw vertical lines for left/right boundaries
- drawLine(dst, pitch, width, height,
- _corridorLeftX + _viewX, _corridorTopY + _viewY,
- _corridorLeftX + _viewX, _corridorBottomY + _viewY, corridorColor);
- drawLine(dst, pitch, width, height,
- _corridorRightX + _viewX, _corridorTopY + _viewY,
- _corridorRightX + _viewX, _corridorBottomY + _viewY, corridorColor);
- // Draw horizontal lines for top/bottom boundaries
- drawLine(dst, pitch, width, height,
- _corridorLeftX + _viewX, _corridorTopY + _viewY,
- _corridorRightX + _viewX, _corridorTopY + _viewY, corridorColor);
- drawLine(dst, pitch, width, height,
- _corridorLeftX + _viewX, _corridorBottomY + _viewY,
- _corridorRightX + _viewX, _corridorBottomY + _viewY, corridorColor);
- }
-}
-
void InsaneRebel2::renderNutSprite(byte *dst, int pitch, int width, int height, int x, int y, NutRenderer *nut, int spriteIdx) {
renderNutSpriteMirrored(dst, pitch, width, height, x, y, nut, spriteIdx, false);
}
@@ -2590,11 +2515,6 @@ void InsaneRebel2::procPostRendering(byte *renderBitmap, int32 codecparam, int32
checkHandler7CollisionZones(renderBitmap, pitch, width, height, curFrame);
}
- // Collision zone visualization (debug - for Handler 7/8 pilot modes)
- if (_rebelHandler == 7 || _rebelHandler == 8) {
- drawCollisionZones(renderBitmap, pitch, width, height, 0);
- }
-
// Crosshair/reticle (FUN_004089ab, FUN_0040d836)
renderCrosshair(renderBitmap, pitch, width, height);
Commit: 25cb578e5f093415331a368cc61c4beaad415c49
https://github.com/scummvm/scummvm/commit/25cb578e5f093415331a368cc61c4beaad415c49
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:19+02:00
Commit Message:
SCUMM: RA2: Remove remaining debug code
Changed paths:
engines/scumm/insane/insane_rebel.cpp
engines/scumm/insane/insane_rebel.h
engines/scumm/insane/insane_rebel_render.cpp
diff --git a/engines/scumm/insane/insane_rebel.cpp b/engines/scumm/insane/insane_rebel.cpp
index 7c5c829c344..cbd915447ee 100644
--- a/engines/scumm/insane/insane_rebel.cpp
+++ b/engines/scumm/insane/insane_rebel.cpp
@@ -1088,7 +1088,7 @@ int32 InsaneRebel2::processMouse() {
it->id, it->active, it->destroyed,
it->rect.left, it->rect.top, it->rect.right, it->rect.bottom,
it->rect.contains(worldMousePos));
-
+
if (it->active && it->rect.contains(worldMousePos)) {
// Enemy hit!
it->active = false;
@@ -1097,10 +1097,21 @@ int32 InsaneRebel2::processMouse() {
it->id, it->type, mousePos.x, mousePos.y,
it->rect.left, it->rect.top, it->rect.right, it->rect.bottom);
+ // Explosion scale is handler-specific in retail:
+ // - H8/H7/H26 use object half-width
+ // - H25 uses half-width + snapDistance (and type 100 doubles it)
+ int explosionHalfWidth = it->rect.width() / 2;
+ if (_rebelHandler == 25) {
+ LevelDifficultyParams dparams = getDifficultyParams();
+ explosionHalfWidth += dparams.snapDistance;
+ if (it->type == 100)
+ explosionHalfWidth *= 2;
+ }
+
// Spawn visual explosion based on handler, enemy type, and flags.
//
// Rendering functions (FUN_409FBC, FUN_402696, FUN_40F1C5,
- // FUN_41F29A) check DAT_0047e108 flags & 1 â when set,
+ // FUN_41F29A) check DAT_0047e108 flags & 1 - when set,
// explosion NUT sprites are suppressed. This is checked
// during rendering in renderExplosions().
//
@@ -1111,17 +1122,17 @@ int32 InsaneRebel2::processMouse() {
if (_rebelHandler != 8 && _rebelHandler != 25) {
spawnExplosion((it->rect.left + it->rect.right) / 2,
(it->rect.top + it->rect.bottom) / 2,
- it->rect.width() / 2);
+ explosionHalfWidth);
} else if (_rebelHandler == 8 && it->type == 0) {
spawnExplosion((it->rect.left + it->rect.right) / 2,
(it->rect.top + it->rect.bottom) / 2,
- it->rect.width() / 2);
+ explosionHalfWidth);
} else if (_rebelHandler == 25 && it->type > 3) {
// Counter is set for timing/sound, but rendering
// may be suppressed by flags bit 0
spawnExplosion((it->rect.left + it->rect.right) / 2,
(it->rect.top + it->rect.bottom) / 2,
- it->rect.width() / 2);
+ explosionHalfWidth);
}
// Disable self (prevents sprite from rendering via SKIP chunks)
@@ -1138,9 +1149,14 @@ int32 InsaneRebel2::processMouse() {
// Increment kill counter (DAT_0047ab88)
_rebelKillCounter++;
- // Handle dependencies
+ // Handle dependencies.
+ // Handler 25 (FUN_41E7C2) has two kill paths:
+ // - type <= 3: process dependency tables
+ // - type > 3 (and 100): skip dependency handling
+ // Other handlers always use link-table side effects.
+ bool handleDependencies = !(_rebelHandler == 25 && it->type > 3);
int id = it->id;
- if (id >= 0 && id < 512) {
+ if (handleDependencies && id >= 0 && id < 512) {
// Slot 2: Enable (Explosion?)
if (_rebelLinks[id][2] != 0) {
clearBit(_rebelLinks[id][2]);
@@ -1175,7 +1191,7 @@ int32 InsaneRebel2::processMouse() {
}
// Award score for destroying enemy (FUN_0041bf8d called from FUN_40A2E0)
- // Score value comes from DAT_0047e0fe indexed by difficultyÃlevel
+ // Score value comes from DAT_0047e0fe indexed by difficulty*level
{
LevelDifficultyParams dparams = getDifficultyParams();
if (dparams.hitPoints > 0) {
diff --git a/engines/scumm/insane/insane_rebel.h b/engines/scumm/insane/insane_rebel.h
index a37d3347fce..998179cd9ba 100644
--- a/engines/scumm/insane/insane_rebel.h
+++ b/engines/scumm/insane/insane_rebel.h
@@ -451,7 +451,7 @@ public:
// Draw fallback ship using embedded HUD frame
void renderFallbackShip(byte *renderBitmap, int pitch, int width, int height);
- // Draw enemy indicator brackets and erase destroyed enemy areas
+ // Draw per-enemy target indicators from the cockpit icon sheet.
void renderEnemyOverlays(byte *renderBitmap, int pitch, int width, int height, int videoWidth);
// Draw explosion animations from 5-slot system (dispatcher)
@@ -508,7 +508,6 @@ public:
// This is called at the start of each frame, before FOBJ sprites are decoded
void procPreRendering(byte *renderBitmap) override;
- void drawLine(byte *dst, int pitch, int width, int height, int x0, int y0, int x1, int y1, byte color);
// mask231: when true, color 231 is treated as transparent (legacy sprites). For laser beams set false.
void drawTexturedLine(byte *dst, int pitch, int width, int height, int x0, int y0, int x1, int y1, NutRenderer *nut, int spriteIdx, int v, bool mask231 = true);
@@ -1042,7 +1041,6 @@ public:
/* Difficulty Level (0-5, from pilot menu; maps directly to table rows) */
int _difficulty;
- void drawCornerBrackets(byte *dst, int pitch, int width, int height, int x, int y, int w, int h, byte color);
// ======================= Per-Level Difficulty Parameters =======================
// Extracted from RA2WIN95.EXE at VA 0x47e0f0
diff --git a/engines/scumm/insane/insane_rebel_render.cpp b/engines/scumm/insane/insane_rebel_render.cpp
index a62c1405bbe..8fa2fde4c6a 100644
--- a/engines/scumm/insane/insane_rebel_render.cpp
+++ b/engines/scumm/insane/insane_rebel_render.cpp
@@ -1599,52 +1599,6 @@ void InsaneRebel2::drawLaserBeam(byte *dst, int pitch, int width, int height,
}
}
-void InsaneRebel2::drawLine(byte *dst, int pitch, int width, int height, int x0, int y0, int x1, int y1, byte color) {
- int dx = abs(x1 - x0), sx = x0 < x1 ? 1 : -1;
- int dy = -abs(y1 - y0), sy = y0 < y1 ? 1 : -1;
- int err = dx + dy, e2;
-
- for (;;) {
- if (x0 >= 0 && x0 < width && y0 >= 0 && y0 < height) {
- dst[y0 * pitch + x0] = color;
- }
- if (x0 == x1 && y0 == y1)
- break;
- e2 = 2 * err;
- if (e2 >= dy) { err += dy; x0 += sx; }
- if (e2 <= dx) { err += dx; y0 += sy; }
- }
-}
-
-void InsaneRebel2::drawCornerBrackets(byte *dst, int pitch, int width, int height, int x, int y, int w, int h, byte color) {
- // Draw L-shaped brackets at corners of the rect (x,y,w,h)
- // Bracket size: approx 8 pixels
- int armLen = 2;
- if (armLen > w / 2)
- armLen = w / 2;
- if (armLen > h / 2)
- armLen = h / 2;
-
- int x2 = x + w - 1;
- int y2 = y + h - 1;
-
- // Top-Left Corner
- drawLine(dst, pitch, width, height, x, y, x + armLen, y, color);
- drawLine(dst, pitch, width, height, x, y, x, y + armLen, color);
-
- // Top-Right Corner
- drawLine(dst, pitch, width, height, x2 - armLen, y, x2, y, color);
- drawLine(dst, pitch, width, height, x2, y, x2, y + armLen, color);
-
- // Bottom-Left Corner
- drawLine(dst, pitch, width, height, x, y2, x + armLen, y2, color);
- drawLine(dst, pitch, width, height, x, y2 - armLen, x, y2, color);
-
- // Bottom-Right Corner
- drawLine(dst, pitch, width, height, x2 - armLen, y2, x2, y2, color);
- drawLine(dst, pitch, width, height, x2, y2 - armLen, x2, y2, color);
-}
-
// ============================================================
// COLLISION ZONE SYSTEM (for Level 3 pilot ship obstacle avoidance)
// ============================================================
@@ -2456,7 +2410,7 @@ void InsaneRebel2::procPostRendering(byte *renderBitmap, int32 codecparam, int32
renderHandler8Ship(renderBitmap, pitch, width, height);
renderFallbackShip(renderBitmap, pitch, width, height);
- // Enemy indicators and destroyed enemy area erase
+ // Enemy target indicators (handler-specific; sprite-based in turret mode)
renderEnemyOverlays(renderBitmap, pitch, width, height, videoWidth);
// Explosion animations (FUN_409FBC) â drawn before lasers in original
@@ -3611,26 +3565,26 @@ void InsaneRebel2::renderFallbackShip(byte *renderBitmap, int pitch, int width,
}
void InsaneRebel2::renderEnemyOverlays(byte *renderBitmap, int pitch, int width, int height, int videoWidth) {
- // Draw enemy indicator brackets for active enemies
+ // Original per-enemy target indicator behavior comes from FUN_40A2E0 (handler 0x26):
+ // - Draws cockpit icon sprites 6..10 at enemy centers.
+ // - Enabled when level flags bit 2 (0x04) is clear.
+ // - Sprite index depends on object half-width bucket.
//
- // NOTE: Do NOT fill destroyed enemy areas with black. The original game does not do this.
- // When an enemy is destroyed:
- // 1. setBit(enemy_id) disables the enemy in the bit table
- // 2. clearBit(dependency_id) enables dependent objects (explosion animations)
- // 3. SKIP chunks in the video cause enemy FOBJ sprites to be skipped (via procSKIP)
- // 4. renderExplosions() draws the explosion animation from the 5-slot system
- // 5. The background video shows through where the enemy was
-
- // Draw green brackets for active enemies (Easy/Medium difficulty only)
- if (_difficulty >= 2)
+ // It is not a generic all-handler bracket overlay.
+ if (_rebelHandler != 0x26 || !_smush_iconsNut)
+ return;
+
+ LevelDifficultyParams dparams = getDifficultyParams();
+ if ((dparams.flags & 4) != 0)
return;
- // FOBJ sprites are rendered with _fobjOffsetX/Y applied (set from _rebelViewOffsetX/Y
- // for Handler 25). Brackets must use the same offset so they align with the sprites.
+ // FOBJ sprites are rendered with _fobjOffsetX/Y applied. Use the same offsets
+ // so indicators stay aligned with decoded enemy sprites.
int fobjOffX = _player ? _player->_fobjOffsetX : 0;
int fobjOffY = _player ? _player->_fobjOffsetY : 0;
Common::Rect viewRect(_viewX, _viewY, _viewX + videoWidth, _viewY + 200);
+ const int sizeClamp = dparams.specialDamage; // DAT_0047e0fa in FUN_40A2E0
for (Common::List<enemy>::iterator it = _enemies.begin(); it != _enemies.end(); ++it) {
if (it->destroyed || !it->active || isBitSet(it->id))
@@ -3642,8 +3596,40 @@ void InsaneRebel2::renderEnemyOverlays(byte *renderBitmap, int pitch, int width,
r.bottom <= viewRect.top || r.top >= viewRect.bottom)
continue;
- const byte color = 5; // Green
- drawCornerBrackets(renderBitmap, pitch, width, height, r.left, r.top, r.width(), r.height(), color);
+ int halfW = r.width() / 2;
+ int halfH = r.height() / 2;
+ if (halfW <= 0 || halfH <= 0)
+ continue;
+
+ // Match size-bucket selection from FUN_40A2E0:
+ // class 0..4 -> sprite 6..10.
+ int indicatorHalfW = halfW;
+ if (sizeClamp > 0)
+ indicatorHalfW = MIN(indicatorHalfW, sizeClamp / 2);
+
+ int sizeClass;
+ if (indicatorHalfW < 3) {
+ sizeClass = 0;
+ } else if (indicatorHalfW < 6) {
+ sizeClass = 1;
+ } else if (indicatorHalfW < 9) {
+ sizeClass = 2;
+ } else if (indicatorHalfW < 12) {
+ sizeClass = 3;
+ } else {
+ sizeClass = 4;
+ }
+
+ int spriteIndex = sizeClass + 6;
+ if (spriteIndex < 0 || spriteIndex >= _smush_iconsNut->getNumChars())
+ continue;
+
+ int centerX = r.left + halfW;
+ int centerY = r.top + halfH;
+ int iw = _smush_iconsNut->getCharWidth(spriteIndex);
+ int ih = _smush_iconsNut->getCharHeight(spriteIndex);
+ renderNutSprite(renderBitmap, pitch, width, height,
+ centerX - iw / 2, centerY - ih / 2, _smush_iconsNut, spriteIndex);
}
}
@@ -3897,6 +3883,9 @@ void InsaneRebel2::renderHandler25Explosions(byte *renderBitmap, int pitch, int
continue;
}
+ // Match FUN_41F29A exactly: decrement first, then select frame.
+ _explosions[i].counter--;
+
// FUN_41F29A lines 27-37: Resolution-dependent thresholds (same as Handler 7).
int baseIndex;
if (_explosions[i].scale < 11) {
@@ -3919,8 +3908,6 @@ void InsaneRebel2::renderHandler25Explosions(byte *renderBitmap, int pitch, int
renderNutSprite(renderBitmap, pitch, width, height,
screenX - ew / 2, screenY - eh / 2, _smush_iconsNut, spriteIndex);
}
-
- _explosions[i].counter--;
}
}
Commit: b8518e989ec994cdc84f77a84bbd67130c5c3914
https://github.com/scummvm/scummvm/commit/b8518e989ec994cdc84f77a84bbd67130c5c3914
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:19+02:00
Commit Message:
SCUMM: RA2: Add top pilots screen
Changed paths:
engines/scumm/insane/insane_rebel.cpp
engines/scumm/insane/insane_rebel.h
engines/scumm/insane/insane_rebel_menu.cpp
engines/scumm/insane/insane_rebel_render.cpp
engines/scumm/nut_renderer.cpp
diff --git a/engines/scumm/insane/insane_rebel.cpp b/engines/scumm/insane/insane_rebel.cpp
index cbd915447ee..3f0684dfee4 100644
--- a/engines/scumm/insane/insane_rebel.cpp
+++ b/engines/scumm/insane/insane_rebel.cpp
@@ -459,6 +459,11 @@ InsaneRebel2::InsaneRebel2(ScummEngine_v7 *scumm) {
_pilotNameInput = "";
_pilotEditIndex = -1;
+ // Initialize top pilots display state and ranking table (FUN_0040FF00)
+ _topPilotsFrameCount = 0;
+ _topPilotsMaxFrames = 120;
+ initDefaultRankings();
+
// Initialize menu input capture system
_menuInputActive = false;
diff --git a/engines/scumm/insane/insane_rebel.h b/engines/scumm/insane/insane_rebel.h
index 998179cd9ba..c356eaa5d95 100644
--- a/engines/scumm/insane/insane_rebel.h
+++ b/engines/scumm/insane/insane_rebel.h
@@ -67,6 +67,7 @@ public:
kStateGameplay = 4, // Stage 4: Gameplay (FUN_00416787)
kStateCredits = 5, // Credits sequence
kStateQuit = 6, // Exit game
+ kStateTopPilots = 8, // Top Pilots score display (FUN_00420116)
kStateDifficultySelect = 7 // Difficulty submenu within pilot select
};
@@ -97,10 +98,13 @@ public:
// Process menu input (keyboard/mouse) - returns selected item or -1
int processMenuInput();
+ // Format-code-aware string rendering (^fNN=font, ^cNNN=color)
+ int getMenuStringWidth(const char *str) const;
+ void drawMenuString(byte *renderBitmap, const char *str, int x, int y, int defaultColor = 1);
+ void drawMenuStringCentered(byte *renderBitmap, const char *str, int cx, int y, int defaultColor = 1);
+ void drawMenuStringRight(byte *renderBitmap, const char *str, int rx, int y, int defaultColor = 1);
+
// Shared menu item renderer - emulates FUN_0041F5AE
- // items[0] = title, items[1..numItems] = selectable items, selection = highlighted item
- // leftAligned=false: param_4==0 (centered, for main menu / pilot select)
- // leftAligned=true: param_4==1 (left-aligned, for chapter select)
void drawMenuItems(byte *renderBitmap, int pitch, int width, int height,
const char **items, int numItems, int selection,
bool leftAligned = false);
@@ -163,6 +167,38 @@ public:
// Password table lookup (FUN_0041BCE0)
Common::String getChapterPassword(int level, int difficulty);
+ // ================= Top Pilots Screen (FUN_00420116) ====================
+ // Shows ranked pilot scores with animated reveal, played over menu video
+ // Original: DAT_00443b58 ranking table, 0x4a-byte records, max 15 entries
+
+ static const int kMaxRankings = 15;
+
+ struct RankingEntry {
+ char name[40]; // +0x04: Pilot name (or "-----" for defaults)
+ int32 score; // +0x36: Total score
+ int32 rating; // +0x3a: Total rating (converted to rank medals)
+ int16 difficulty; // +0x3e: Difficulty tier (0-5), TRS index = difficulty + 155
+ int16 chapter; // +0x40: Highest chapter completed (1-15)
+ };
+
+ RankingEntry _rankings[kMaxRankings];
+ int _numRankings;
+
+ // Initialize ranking table with defaults (FUN_0040FF00)
+ void initDefaultRankings();
+
+ // Insert pilot score into sorted ranking table (FUN_00410271)
+ void insertRanking(const char *name, int32 score, int32 rating, int16 difficulty, int16 chapter);
+
+ // Run top pilots display - called from main menu "Show Top Pilots"
+ void showTopPilots();
+
+ // Draw top pilots overlay on current frame during video playback
+ void drawTopPilotsOverlay(byte *renderBitmap, int pitch, int width, int height);
+
+ int _topPilotsFrameCount; // Animation frame counter (pilots revealed one per frame)
+ int _topPilotsMaxFrames; // Total frames to display (120 or 240)
+
// ================= Pilot Data System (FUN_00411B9A / FUN_00411980 / FUN_00411A5D) ===========
// Original: 10 pilot slots à 0x118 (280) bytes at DAT_004568A8
// Stored via SaveFileManager in a custom save file
diff --git a/engines/scumm/insane/insane_rebel_menu.cpp b/engines/scumm/insane/insane_rebel_menu.cpp
index 92166f4a927..290a5b77258 100644
--- a/engines/scumm/insane/insane_rebel_menu.cpp
+++ b/engines/scumm/insane/insane_rebel_menu.cpp
@@ -433,8 +433,100 @@ void InsaneRebel2::drawMenuItems(byte *renderBitmap, int pitch, int width, int h
}
}
+// Format-code-aware string width calculation
+// Handles ^fNN (font switch), ^cNNN (color), ^^ (literal ^)
+int InsaneRebel2::getMenuStringWidth(const char *str) const {
+ NutRenderer *fonts[3] = { _smush_talkfontNut, _smush_smalfontNut, _smush_titlefontNut };
+ NutRenderer *defaultFont = fonts[0] ? fonts[0] : _smush_smalfontNut;
+ if (!defaultFont)
+ return 0;
+
+ int w = 0;
+ NutRenderer *curFont = defaultFont;
+ while (*str) {
+ if (*str == '^') {
+ const char *p = str + 1;
+ if (*p == '^') { str = p + 1; continue; }
+ if (*p == 'f') {
+ p++;
+ int idx = 0;
+ while (*p >= '0' && *p <= '9') idx = idx * 10 + (*p++ - '0');
+ curFont = (idx >= 0 && idx < 3 && fonts[idx]) ? fonts[idx] : defaultFont;
+ str = p;
+ continue;
+ }
+ if (*p == 'c' || *p == 'l') {
+ p++;
+ while (*p >= '0' && *p <= '9') p++;
+ str = p;
+ continue;
+ }
+ }
+ byte c = (byte)*str++;
+ if (c >= 'a' && c <= 'z') c = c - 'a' + 'A';
+ if (curFont && c < curFont->getNumChars())
+ w += curFont->getCharWidth(c);
+ }
+ return w;
+}
+
+// Format-code-aware string rendering at (x, y)
+void InsaneRebel2::drawMenuString(byte *renderBitmap, const char *str, int x, int y, int defaultColor) {
+ NutRenderer *fonts[3] = { _smush_talkfontNut, _smush_smalfontNut, _smush_titlefontNut };
+ NutRenderer *defaultFont = fonts[0] ? fonts[0] : _smush_smalfontNut;
+ if (!defaultFont)
+ return;
+
+ Common::Rect clipRect(0, 0, _vm->_screenWidth, _vm->_screenHeight);
+ int pitch = _vm->_screenWidth;
+
+ NutRenderer *curFont = defaultFont;
+ int curColor = defaultColor;
+ while (*str) {
+ if (*str == '^') {
+ const char *p = str + 1;
+ if (*p == '^') { str = p + 1; continue; }
+ if (*p == 'f') {
+ p++;
+ int idx = 0;
+ while (*p >= '0' && *p <= '9') idx = idx * 10 + (*p++ - '0');
+ curFont = (idx >= 0 && idx < 3 && fonts[idx]) ? fonts[idx] : defaultFont;
+ str = p;
+ continue;
+ }
+ if (*p == 'c') {
+ p++;
+ int color = 0;
+ while (*p >= '0' && *p <= '9') color = color * 10 + (*p++ - '0');
+ curColor = color;
+ str = p;
+ continue;
+ }
+ if (*p == 'l') { str = p + 1; continue; }
+ }
+ byte c = (byte)*str++;
+ if (c >= 'a' && c <= 'z') c = c - 'a' + 'A';
+ if (!curFont) continue;
+ if (c >= curFont->getNumChars()) continue;
+ int charW = curFont->getCharWidth(c);
+ if (x >= 0 && y >= 0 && charW > 0)
+ curFont->drawCharV7(renderBitmap, clipRect, x, y, pitch, curColor,
+ kStyleAlignLeft, c, false, false);
+ x += charW;
+ }
+}
+
+void InsaneRebel2::drawMenuStringCentered(byte *renderBitmap, const char *str, int cx, int y, int defaultColor) {
+ int w = getMenuStringWidth(str);
+ drawMenuString(renderBitmap, str, cx - w / 2, y, defaultColor);
+}
+
+void InsaneRebel2::drawMenuStringRight(byte *renderBitmap, const char *str, int rx, int y, int defaultColor) {
+ int w = getMenuStringWidth(str);
+ drawMenuString(renderBitmap, str, rx - w, y, defaultColor);
+}
+
void InsaneRebel2::drawMenuOverlay(byte *renderBitmap, int pitch, int width, int height) {
- // =====================================================================
// Main menu renderer - calls shared drawMenuItems()
// Emulates FUN_004147b2 -> FUN_0041f5ae with param_3=7, param_4=0
// =====================================================================
@@ -742,9 +834,9 @@ int InsaneRebel2::runMainMenu() {
_menuInputActive = true;
break;
- case 4: // Show Top Pilots -> high score display
+ case 4: // Show Top Pilots -> high score display (FUN_00420116(-1))
debug("Rebel2: Show Top Pilots selected");
- // TODO: Implement high score display (FUN_00420116(-1))
+ showTopPilots();
break;
case 5: // Show Credits -> play credits video
@@ -1197,20 +1289,14 @@ void InsaneRebel2::drawPreviewThumbnail(byte *renderBitmap, int pitch, int width
}
}
-// Rating-to-medal string conversion - emulates FUN_0042001f
-// Converts a rating value (0-50) to a string of medal characters for TALKFONT.NUT:
-// Every 9 points â big medal (DAT_00482550)
-// Every 3 points â medium medal (DAT_00482558)
-// Every 1 point â small medal (DAT_00482560)
+// Rating to medal string (FUN_0042001f): TALKFONT glyphs 3=big, 2=medium, 1=small
Common::String InsaneRebel2::getRankString(int rating) {
if (rating > 50)
rating = 50;
Common::String result;
- // TODO: Medal char bytes are placeholders â verify against actual NUT font glyphs
- // from DAT_00482550/58/60 in the game binary
- while (rating >= 9) { result += '\x83'; rating -= 9; } // big medal
- while (rating >= 3) { result += '\x82'; rating -= 3; } // medium medal
- while (rating >= 1) { result += '\x81'; rating -= 1; } // small medal
+ while (rating >= 9) { result += (char)3; rating -= 9; }
+ while (rating >= 3) { result += (char)2; rating -= 3; }
+ while (rating >= 1) { result += (char)1; rating -= 1; }
return result;
}
@@ -1257,9 +1343,7 @@ Common::String InsaneRebel2::getChapterPassword(int level, int difficulty) {
return kPasswordTable[idx];
}
-// Draw score/info line at bottom of chapter select - emulates FUN_00434cb0 calls
-// For unlocked chapters: score display using TRS 80 at (25, 190)
-// For locked chapters: password prompt using TRS 81 at (30, 190)
+// Draw score/info line at bottom of chapter select
void InsaneRebel2::drawChapterInfoLine(byte *renderBitmap, int pitch, int width, int height) {
if (_chapterSelection < 0 || _chapterSelection >= 16)
return;
@@ -1268,74 +1352,6 @@ void InsaneRebel2::drawChapterInfoLine(byte *renderBitmap, int pitch, int width,
if (!splayer)
return;
- // Font system â same as drawMenuItems()
- NutRenderer *fonts[3] = {
- _smush_talkfontNut,
- _smush_smalfontNut,
- _smush_titlefontNut
- };
- NutRenderer *defaultFont = fonts[0] ? fonts[0] : _smush_smalfontNut;
- if (!defaultFont)
- return;
-
- Common::Rect clipRect(0, 0, _vm->_screenWidth, _vm->_screenHeight);
- int actualPitch = _vm->_screenWidth;
-
- // Format code parser â same as drawMenuItems()
- auto parseFormatCode = [&](const char *&str, int &outColor) -> int {
- if (*str != '^')
- return -1;
- const char *p = str + 1;
- if (*p == '^') { str = p; return -1; }
- if (*p == 'f') {
- p++;
- int fontIdx = 0;
- while (*p >= '0' && *p <= '9') { fontIdx = fontIdx * 10 + (*p - '0'); p++; }
- str = p;
- return (fontIdx >= 0 && fontIdx < 3) ? fontIdx : 0;
- }
- if (*p == 'c') {
- p++;
- int color = 0;
- while (*p >= '0' && *p <= '9') { color = color * 10 + (*p - '0'); p++; }
- str = p;
- outColor = color;
- return -2;
- }
- return -1;
- };
-
- // String rendering with format codes
- auto drawString = [&](const char *str, int x, int y) {
- NutRenderer *curFont = defaultFont;
- int curColor = 1;
-
- while (*str) {
- int fontChange = parseFormatCode(str, curColor);
- if (fontChange >= 0) {
- curFont = fonts[fontChange] ? fonts[fontChange] : defaultFont;
- continue;
- }
- if (fontChange == -2)
- continue;
-
- byte c = (byte)*str++;
- if (c >= 'a' && c <= 'z')
- c = c - 'a' + 'A';
- if (!curFont)
- continue;
- int numChars = curFont->getNumChars();
- if (c >= numChars)
- continue;
- int charW = curFont->getCharWidth(c);
- if (x >= 0 && y >= 0 && charW > 0) {
- curFont->drawCharV7(renderBitmap, clipRect, x, y, actualPitch, curColor,
- kStyleAlignLeft, c, false, false);
- }
- x += charW;
- }
- };
-
if (_chapterUnlocked[_chapterSelection]) {
// Unlocked: show score info using TRS 80 at X=25 (0x19), Y=190 (0xbe)
// TRS 80 = "^f01^c248Pilots: %hd Score: %ld Rank: ^f00%s"
@@ -1358,15 +1374,12 @@ void InsaneRebel2::drawChapterInfoLine(byte *renderBitmap, int pitch, int width,
Common::String displayStr = Common::String::format(fmtStr,
(short)pilotLives, (long)pilotScore, rankStr.c_str());
- drawString(displayStr.c_str(), 25, 190);
+ drawMenuString(renderBitmap, displayStr.c_str(), 25, 190);
} else {
- // Locked: show password prompt using TRS 81 at X=30 (0x1e), Y=190 (0xbe)
- // Format: "%s ^c005%s%c" (TRS 81 + green password input + blinking cursor)
const char *lockStr = splayer->getString(81);
if (!lockStr || !lockStr[0])
lockStr = "^f01^c248UNREGISTERED - PASSCODE REQUIRED";
- // Blinking cursor: alternate '_' and ' ' (original uses bit 1 of frame counter)
static int cursorCounter = 0;
cursorCounter++;
char cursor = ((cursorCounter / 8) & 1) ? '_' : ' ';
@@ -1374,7 +1387,7 @@ void InsaneRebel2::drawChapterInfoLine(byte *renderBitmap, int pitch, int width,
Common::String displayStr = Common::String::format("%s ^c005%s%c",
lockStr, _passwordInput.c_str(), cursor);
- drawString(displayStr.c_str(), 30, 190);
+ drawMenuString(renderBitmap, displayStr.c_str(), 30, 190);
}
}
@@ -1789,4 +1802,189 @@ void InsaneRebel2::drawLevelSelectOverlay(byte *renderBitmap, int pitch, int wid
drawMenuItems(renderBitmap, pitch, width, height, pilotItems, _numPilots + 4, _levelSelection);
}
+// ==================== Top Pilots Screen (FUN_00420116) ====================
+// Displays ranked pilot scores with animated reveal over a menu background video.
+//
+// Original FUN_00420116 at 0x420116:
+// - Plays random menu video as background (FUN_0041fdc8)
+// - Draws title from TRS string 0x96 (150) centered at (152, 10) in low-res
+// - Iterates through ranking table (DAT_00443b58, 0x4a-byte records, max 15)
+// - Each row shows: rank medals, pilot name, difficulty, chapter, total score
+// - Rows appear one per frame (animated reveal up to local_10)
+// - param_1 == -2: 240 frame loop (from options); else 120 frames (from main menu)
+// - Exits on mouse click (DAT_0047a7e4 & 1) or any keypress (DAT_0047a7e8 != 0)
+//
+// Ranking table record (0x4a = 74 bytes, from FUN_00410271):
+// +0x00 (4): timestamp
+// +0x04 (40): pilot name (or "-----" for defaults)
+// +0x36 (4): total score
+// +0x3a (4): total rating
+// +0x3e (2): difficulty tier (1-3) â TRS lookup: value + 0x9b (155)
+// +0x40 (2): highest chapter completed (1-15)
+//
+// Column X positions (low-res 320x200):
+// Rank medals: 43 (0x2b) - FUN_004341a0, alignment=1
+// Name: 88 (0x58) - FUN_00434cb0, alignment=0 (left), "^f01%s"
+// Difficulty: 195 (0xc3) - FUN_00434cb0, alignment=1, "^f01%s" (TRS)
+// Chapter: 245 (0xf5) - FUN_00434cb0, alignment=1, "^f01%hd"
+// Score: 295 (0x127) - FUN_00434cb0, alignment=2, "^f01%ld"
+// Row Y: sVar1 * 10 + 42 (0x2a)
+
+// Initialize ranking table with 15 default entries (FUN_0040FF00)
+// Called when no ranking save file exists. Generates placeholder entries with:
+// name = "-----", score = (15-i)*1500, rating = (15-i)*2,
+// difficulty = ((15-i)*3+14)/15, chapter = ((15-i)*15+14)/15
+void InsaneRebel2::initDefaultRankings() {
+ _numRankings = 0;
+ memset(_rankings, 0, sizeof(_rankings));
+ for (int i = 0; i < kMaxRankings; i++) {
+ int k = kMaxRankings - i; // 15 down to 1
+ RankingEntry &r = _rankings[_numRankings];
+ Common::strlcpy(r.name, "-----", sizeof(r.name));
+ r.score = k * 1500;
+ r.rating = k * 2;
+ r.difficulty = (int16)((k * 3 + 14) / 15);
+ r.chapter = (int16)((k * 15 + 14) / 15);
+ _numRankings++;
+ }
+}
+
+// Insert a new entry into the sorted ranking table (FUN_00410271)
+// Maintains descending score order, max kMaxRankings entries
+void InsaneRebel2::insertRanking(const char *name, int32 score, int32 rating,
+ int16 difficulty, int16 chapter) {
+ if (score == 0)
+ return;
+
+ // Find insertion point (first entry with score < new score)
+ int insertPos = 0;
+ while (insertPos < _numRankings && score <= _rankings[insertPos].score) {
+ insertPos++;
+ }
+ if (insertPos > kMaxRankings - 1)
+ return;
+
+ // Remove any existing entry with same name
+ for (int i = 0; i < _numRankings; i++) {
+ if (strcmp(_rankings[i].name, name) == 0) {
+ if (score <= _rankings[i].score)
+ return; // Existing entry has higher score
+ // Remove old entry by shifting
+ for (int j = i; j < _numRankings - 1; j++)
+ _rankings[j] = _rankings[j + 1];
+ _numRankings--;
+ if (insertPos > i)
+ insertPos--;
+ break;
+ }
+ }
+
+ // Shift entries down to make room
+ int lastIdx = MIN(_numRankings, kMaxRankings - 1);
+ for (int i = lastIdx; i > insertPos; i--)
+ _rankings[i] = _rankings[i - 1];
+
+ // Insert new entry
+ RankingEntry &r = _rankings[insertPos];
+ Common::strlcpy(r.name, name, sizeof(r.name));
+ r.score = score;
+ r.rating = rating;
+ r.difficulty = difficulty;
+ r.chapter = chapter;
+
+ if (_numRankings < kMaxRankings)
+ _numRankings++;
+}
+
+void InsaneRebel2::showTopPilots() {
+ debug("Rebel2: Showing Top Pilots screen (FUN_00420116)");
+
+ _menuInputActive = true;
+ while (!_menuEventQueue.empty())
+ _menuEventQueue.pop();
+
+ // param_1 = -1 from main menu: maxFrames = 120 (0x78)
+ _topPilotsMaxFrames = 120;
+ _topPilotsFrameCount = 0;
+
+ _gameState = kStateTopPilots;
+
+ SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
+
+ _vm->_smushVideoShouldFinish = false;
+
+ Common::String menuVideo = getRandomMenuVideo();
+ splayer->setCurVideoFlags(0x20);
+ splayer->play(menuVideo.c_str(), 12);
+
+ _gameState = kStateMainMenu;
+ _menuInputActive = true;
+
+ debug("Rebel2: Top Pilots screen finished");
+}
+
+void InsaneRebel2::drawTopPilotsOverlay(byte *renderBitmap, int pitch, int width, int height) {
+ if (_topPilotsFrameCount >= _topPilotsMaxFrames) {
+ _vm->_smushVideoShouldFinish = true;
+ return;
+ }
+
+ while (!_menuEventQueue.empty()) {
+ Common::Event event = _menuEventQueue.pop();
+ if (event.type == Common::EVENT_KEYDOWN ||
+ event.type == Common::EVENT_LBUTTONDOWN) {
+ _vm->_smushVideoShouldFinish = true;
+ return;
+ }
+ }
+
+ SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
+ if (!splayer)
+ return;
+
+ // Title + column headers: TRS 150 centered at X=152, Y=10
+ const char *titleStr = splayer->getString(150);
+ if (!titleStr || !titleStr[0])
+ titleStr = "^f02Top Pilots ^f01^c005 Rank Name Difficulty Chapter Score";
+ drawMenuStringCentered(renderBitmap, titleStr, 152, 10);
+
+ // Animated reveal: show up to _topPilotsFrameCount entries
+ int showCount = MIN(_topPilotsFrameCount, _numRankings);
+
+ for (int row = 0; row < showCount; row++) {
+ const RankingEntry &r = _rankings[row];
+ int rowY = row * 10 + 42;
+ int color = 244; // 0xF4
+
+ // Column 1: Rank medals at X=43, centered (font 0 = TALKFONT)
+ Common::String rankStr = getRankString(r.rating);
+ if (!rankStr.empty()) {
+ Common::String rankFmt = Common::String::format("^f00%s", rankStr.c_str());
+ drawMenuStringCentered(renderBitmap, rankFmt.c_str(), 43, rowY, color);
+ }
+
+ // Column 2: Pilot name at X=88, left-aligned (font 1 = SMALFONT)
+ Common::String nameFmt = Common::String::format("^f01%s", r.name);
+ drawMenuString(renderBitmap, nameFmt.c_str(), 88, rowY, color);
+
+ // Column 3: Difficulty at X=195, centered - TRS (difficulty + 155)
+ int trsIdx = CLIP((int)r.difficulty, 0, 5) + 155;
+ const char *diffStr = splayer->getString(trsIdx);
+ if (diffStr && diffStr[0]) {
+ Common::String diffFmt = Common::String::format("^f01%s", diffStr);
+ drawMenuStringCentered(renderBitmap, diffFmt.c_str(), 195, rowY, color);
+ }
+
+ // Column 4: Highest chapter at X=245, centered
+ Common::String chFmt = Common::String::format("^f01%d", (int)r.chapter);
+ drawMenuStringCentered(renderBitmap, chFmt.c_str(), 245, rowY, color);
+
+ // Column 5: Total score at X=295, right-aligned
+ Common::String scoreFmt = Common::String::format("^f01%ld", (long)r.score);
+ drawMenuStringRight(renderBitmap, scoreFmt.c_str(), 295, rowY, color);
+ }
+
+ _topPilotsFrameCount++;
+}
+
} // End of namespace Scumm
diff --git a/engines/scumm/insane/insane_rebel_render.cpp b/engines/scumm/insane/insane_rebel_render.cpp
index 8fa2fde4c6a..be5fc25a4ce 100644
--- a/engines/scumm/insane/insane_rebel_render.cpp
+++ b/engines/scumm/insane/insane_rebel_render.cpp
@@ -2294,6 +2294,13 @@ void InsaneRebel2::procPostRendering(byte *renderBitmap, int32 codecparam, int32
return;
}
+ // Handle Top Pilots screen (FUN_00420116)
+ bool topPilotsMode = (introPlaying && _gameState == kStateTopPilots);
+ if (topPilotsMode) {
+ drawTopPilotsOverlay(renderBitmap, pitch, width, height);
+ return;
+ }
+
// Handle menu input and rendering if in menu mode
if (menuMode) {
// The original game uses the standard Windows arrow cursor (IDC_ARROW)
diff --git a/engines/scumm/nut_renderer.cpp b/engines/scumm/nut_renderer.cpp
index 8d81f1a302c..8fd02c39e1d 100644
--- a/engines/scumm/nut_renderer.cpp
+++ b/engines/scumm/nut_renderer.cpp
@@ -397,6 +397,8 @@ int NutRenderer::drawCharV7(byte *buffer, Common::Rect &clipRect, int x, int y,
if (chr >= _numChars)
return 0;
+
+
if (_direction < 0)
x -= _chars[chr].width;
Commit: b3a0be791d80623ff57ba9ecf8a7690decc93c94
https://github.com/scummvm/scummvm/commit/b3a0be791d80623ff57ba9ecf8a7690decc93c94
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:20+02:00
Commit Message:
SCUMM: RA2: Improve top pilots screen
Changed paths:
engines/scumm/insane/insane_rebel_menu.cpp
diff --git a/engines/scumm/insane/insane_rebel_menu.cpp b/engines/scumm/insane/insane_rebel_menu.cpp
index 290a5b77258..6dd6e15e29c 100644
--- a/engines/scumm/insane/insane_rebel_menu.cpp
+++ b/engines/scumm/insane/insane_rebel_menu.cpp
@@ -265,6 +265,7 @@ void InsaneRebel2::drawMenuItems(byte *renderBitmap, int pitch, int width, int h
// Format code parser - Emulates FUN_00434d10 / FUN_00433da0
// =====================================================================
// ^^ = literal ^, ^fNN = font switch, ^cNNN = color code, ^l = newline
+ // Fixed-width format codes: ^fNN (2-digit font), ^cNNN (3-digit color)
auto parseFormatCode = [&](const char *&str, int &outColor) -> int {
if (*str != '^')
return -1;
@@ -276,21 +277,16 @@ void InsaneRebel2::drawMenuItems(byte *renderBitmap, int pitch, int width, int h
}
if (*p == 'f') {
p++;
- int fontIdx = 0;
- while (*p >= '0' && *p <= '9') {
- fontIdx = fontIdx * 10 + (*p - '0');
- p++;
- }
+ int fontIdx = (*p >= '0' && *p <= '9') ? (*p++ - '0') : 0;
+ fontIdx = fontIdx * 10 + ((*p >= '0' && *p <= '9') ? (*p++ - '0') : 0);
str = p;
return (fontIdx >= 0 && fontIdx < 3) ? fontIdx : 0;
}
if (*p == 'c') {
p++;
int color = 0;
- while (*p >= '0' && *p <= '9') {
- color = color * 10 + (*p - '0');
- p++;
- }
+ for (int d = 0; d < 3 && *p >= '0' && *p <= '9'; d++)
+ color = color * 10 + (*p++ - '0');
str = p;
outColor = color;
return -2;
@@ -434,7 +430,7 @@ void InsaneRebel2::drawMenuItems(byte *renderBitmap, int pitch, int width, int h
}
// Format-code-aware string width calculation
-// Handles ^fNN (font switch), ^cNNN (color), ^^ (literal ^)
+// Fixed-width codes: ^fNN (2-digit font), ^cNNN (3-digit color), ^^ (literal ^)
int InsaneRebel2::getMenuStringWidth(const char *str) const {
NutRenderer *fonts[3] = { _smush_talkfontNut, _smush_smalfontNut, _smush_titlefontNut };
NutRenderer *defaultFont = fonts[0] ? fonts[0] : _smush_smalfontNut;
@@ -449,18 +445,19 @@ int InsaneRebel2::getMenuStringWidth(const char *str) const {
if (*p == '^') { str = p + 1; continue; }
if (*p == 'f') {
p++;
- int idx = 0;
- while (*p >= '0' && *p <= '9') idx = idx * 10 + (*p++ - '0');
+ int idx = (*p >= '0' && *p <= '9') ? (*p++ - '0') : 0;
+ idx = idx * 10 + ((*p >= '0' && *p <= '9') ? (*p++ - '0') : 0);
curFont = (idx >= 0 && idx < 3 && fonts[idx]) ? fonts[idx] : defaultFont;
str = p;
continue;
}
- if (*p == 'c' || *p == 'l') {
+ if (*p == 'c') {
p++;
- while (*p >= '0' && *p <= '9') p++;
+ for (int d = 0; d < 3 && *p >= '0' && *p <= '9'; d++) p++;
str = p;
continue;
}
+ if (*p == 'l') { str = p + 1; continue; }
}
byte c = (byte)*str++;
if (c >= 'a' && c <= 'z') c = c - 'a' + 'A';
@@ -488,8 +485,8 @@ void InsaneRebel2::drawMenuString(byte *renderBitmap, const char *str, int x, in
if (*p == '^') { str = p + 1; continue; }
if (*p == 'f') {
p++;
- int idx = 0;
- while (*p >= '0' && *p <= '9') idx = idx * 10 + (*p++ - '0');
+ int idx = (*p >= '0' && *p <= '9') ? (*p++ - '0') : 0;
+ idx = idx * 10 + ((*p >= '0' && *p <= '9') ? (*p++ - '0') : 0);
curFont = (idx >= 0 && idx < 3 && fonts[idx]) ? fonts[idx] : defaultFont;
str = p;
continue;
@@ -497,7 +494,8 @@ void InsaneRebel2::drawMenuString(byte *renderBitmap, const char *str, int x, in
if (*p == 'c') {
p++;
int color = 0;
- while (*p >= '0' && *p <= '9') color = color * 10 + (*p++ - '0');
+ for (int d = 0; d < 3 && *p >= '0' && *p <= '9'; d++)
+ color = color * 10 + (*p++ - '0');
curColor = color;
str = p;
continue;
@@ -1942,11 +1940,17 @@ void InsaneRebel2::drawTopPilotsOverlay(byte *renderBitmap, int pitch, int width
if (!splayer)
return;
- // Title + column headers: TRS 150 centered at X=152, Y=10
- const char *titleStr = splayer->getString(150);
- if (!titleStr || !titleStr[0])
- titleStr = "^f02Top Pilots ^f01^c005 Rank Name Difficulty Chapter Score";
- drawMenuStringCentered(renderBitmap, titleStr, 152, 10);
+ // Title centered at X=152, Y=10 (TITLFONT)
+ drawMenuStringCentered(renderBitmap, "^f02Top Pilots", 152, 10);
+
+ // Column headers at Y=30 (SMALFONT), positioned to match data columns
+ int headerY = 30;
+ int headerColor = 5;
+ drawMenuStringCentered(renderBitmap, "^f01Rank", 43, headerY, headerColor);
+ drawMenuString(renderBitmap, "^f01Name", 88, headerY, headerColor);
+ drawMenuStringCentered(renderBitmap, "^f01Difficulty", 195, headerY, headerColor);
+ drawMenuStringCentered(renderBitmap, "^f01Chapter", 245, headerY, headerColor);
+ drawMenuStringRight(renderBitmap, "^f01Score", 295, headerY, headerColor);
// Animated reveal: show up to _topPilotsFrameCount entries
int showCount = MIN(_topPilotsFrameCount, _numRankings);
Commit: 632c63e17ebb34235e50a43628d3df8a13b9a493
https://github.com/scummvm/scummvm/commit/632c63e17ebb34235e50a43628d3df8a13b9a493
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:20+02:00
Commit Message:
SCUMM: RA2: Add options menu
Changed paths:
engines/scumm/insane/insane_rebel.cpp
engines/scumm/insane/insane_rebel.h
engines/scumm/insane/insane_rebel_menu.cpp
engines/scumm/insane/insane_rebel_render.cpp
diff --git a/engines/scumm/insane/insane_rebel.cpp b/engines/scumm/insane/insane_rebel.cpp
index 3f0684dfee4..a5fdd71cbc6 100644
--- a/engines/scumm/insane/insane_rebel.cpp
+++ b/engines/scumm/insane/insane_rebel.cpp
@@ -23,10 +23,13 @@
#include "engines/engine.h"
#include "common/system.h"
+#include "common/config-manager.h"
#include "common/events.h"
#include "common/savefile.h"
#include "common/util.h"
+#include "audio/mixer.h"
+
#include "scumm/actor.h"
#include "scumm/file.h"
#include "scumm/resource.h"
@@ -464,6 +467,17 @@ InsaneRebel2::InsaneRebel2(ScummEngine_v7 *scumm) {
_topPilotsMaxFrames = 120;
initDefaultRankings();
+ // Initialize options menu state (FUN_004167A6 defaults)
+ _optionsSelection = 0;
+ _optionsItemCount = 9;
+ _optMusicEnabled = !_vm->_mixer->isSoundTypeMuted(Audio::Mixer::kMusicSoundType);
+ _optSfxEnabled = !_vm->_mixer->isSoundTypeMuted(Audio::Mixer::kSFXSoundType);
+ _optVoicesEnabled = !_vm->_mixer->isSoundTypeMuted(Audio::Mixer::kSpeechSoundType);
+ _optTextEnabled = true;
+ _optControlsFlipped = false;
+ _optRapidFire = false;
+ _optVolumeLevel = _vm->_mixer->getVolumeForSoundType(Audio::Mixer::kMusicSoundType) / 2;
+
// Initialize menu input capture system
_menuInputActive = false;
diff --git a/engines/scumm/insane/insane_rebel.h b/engines/scumm/insane/insane_rebel.h
index c356eaa5d95..d16c306e937 100644
--- a/engines/scumm/insane/insane_rebel.h
+++ b/engines/scumm/insane/insane_rebel.h
@@ -68,7 +68,8 @@ public:
kStateCredits = 5, // Credits sequence
kStateQuit = 6, // Exit game
kStateTopPilots = 8, // Top Pilots score display (FUN_00420116)
- kStateDifficultySelect = 7 // Difficulty submenu within pilot select
+ kStateDifficultySelect = 7, // Difficulty submenu within pilot select
+ kStateOptions = 9 // Options menu (FUN_004167A6)
};
// Menu selection results (return values from FUN_004147B2)
@@ -199,6 +200,28 @@ public:
int _topPilotsFrameCount; // Animation frame counter (pilots revealed one per frame)
int _topPilotsMaxFrames; // Total frames to display (120 or 240)
+ // ================= Options Menu (FUN_004167A6) ====================
+ // TRS strings: 89 (title), 90-101 (toggle labels), 103 (volume), 107/109 (back)
+ // Original settings array at DAT_00482e20[0..4]:
+ // [0]=auto control, [1]=music, [2]=voices, [3]=sound
+ // Additional flags: DAT_0047a7fe (text/indicators), DAT_0047a80a (rapid fire/arrows)
+ // Volume: DAT_0047a804 (0-127), SFX vol: DAT_0047a802 (127-768)
+
+ void showOptionsMenu();
+ void drawOptionsOverlay(byte *renderBitmap, int pitch, int width, int height);
+ int processOptionsInput();
+
+ int _optionsSelection;
+ int _optionsItemCount;
+ bool _optionsExitRequested;
+ bool _optMusicEnabled;
+ bool _optSfxEnabled;
+ bool _optVoicesEnabled;
+ bool _optTextEnabled;
+ bool _optControlsFlipped;
+ bool _optRapidFire;
+ int _optVolumeLevel; // 0-127 (DAT_0047a804)
+
// ================= Pilot Data System (FUN_00411B9A / FUN_00411980 / FUN_00411A5D) ===========
// Original: 10 pilot slots à 0x118 (280) bytes at DAT_004568A8
// Stored via SaveFileManager in a custom save file
diff --git a/engines/scumm/insane/insane_rebel_menu.cpp b/engines/scumm/insane/insane_rebel_menu.cpp
index 6dd6e15e29c..0ea75e2821c 100644
--- a/engines/scumm/insane/insane_rebel_menu.cpp
+++ b/engines/scumm/insane/insane_rebel_menu.cpp
@@ -23,6 +23,8 @@
#include "common/events.h"
#include "common/util.h"
+#include "audio/mixer.h"
+
#include "graphics/cursorman.h"
#include "graphics/wincursor.h"
@@ -802,11 +804,9 @@ int InsaneRebel2::runMainMenu() {
_menuInputActive = false;
return kMenuNewGame; // Return 2 (kMenuNewGame)
- case 1: // Options -> show options menu
+ case 1: // Options -> show options menu (FUN_00416787)
debug("Rebel2: Options selected");
- // TODO: Implement options menu (FUN_004167a6)
- // Options: Music, Sound, Voices, Auto Control, Indicators,
- // Arrows, Difficulty (0-5), Music Volume, SFX Volume
+ showOptionsMenu();
break;
case 2: // Calibrate Joystick
@@ -1991,4 +1991,220 @@ void InsaneRebel2::drawTopPilotsOverlay(byte *renderBitmap, int pitch, int width
_topPilotsFrameCount++;
}
+// ======================= Options Menu (FUN_004167A6) =======================
+// Original: FUN_00416787 calls FUN_0041fdc8 (init video) + FUN_004167a6(1)
+// FUN_004167a6 runs a loop over FUN_0041f5ae (drawMenuItems) with dynamic
+// toggle labels and slider items. Settings stored at DAT_00482e20[0..3]
+// (get/set via FUN_00425d30/FUN_00425d40) and DAT_0047a7fe, DAT_0047a80a.
+//
+// Menu items for keyboard mode (9 selectable):
+// [0] Title: TRS 89 "Game Options"
+// [1] Music: TRS 90/91
+// [2] SFX: TRS 92/93
+// [3] Voices: TRS 94/95
+// [4] Text: TRS 96/97
+// [5] Controls: TRS 98/99
+// [6] Rapid Fire: TRS 100/101
+// [7] Volume: TRS 103 "Volume Level: %hd%%" (slider, left/right ±4)
+// [8] Back: TRS 107 "Return to Main Menu"
+
+void InsaneRebel2::showOptionsMenu() {
+ debug("Rebel2: Showing Options menu (FUN_00416787)");
+
+ _menuInputActive = true;
+ while (!_menuEventQueue.empty())
+ _menuEventQueue.pop();
+
+ _optionsSelection = 0;
+ _optionsItemCount = 8;
+ _optionsExitRequested = false;
+
+ _gameState = kStateOptions;
+
+ SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
+
+ // Loop videos until user exits options (same pattern as runMainMenu)
+ while (!_vm->shouldQuit() && !_optionsExitRequested) {
+ _vm->_smushVideoShouldFinish = false;
+
+ Common::String menuVideo = getRandomMenuVideo();
+ splayer->setCurVideoFlags(0x20);
+ splayer->play(menuVideo.c_str(), 12);
+ }
+
+ _gameState = kStateMainMenu;
+ _menuInputActive = true;
+
+ debug("Rebel2: Options menu finished");
+}
+
+int InsaneRebel2::processOptionsInput() {
+ while (!_menuEventQueue.empty()) {
+ Common::Event event = _menuEventQueue.pop();
+
+ if (event.type == Common::EVENT_KEYDOWN) {
+ _menuInactivityTimer = 0;
+
+ switch (event.kbd.keycode) {
+ case Common::KEYCODE_UP:
+ _optionsSelection--;
+ if (_optionsSelection < 0)
+ _optionsSelection = _optionsItemCount - 1;
+ return -1;
+
+ case Common::KEYCODE_DOWN:
+ _optionsSelection++;
+ if (_optionsSelection >= _optionsItemCount)
+ _optionsSelection = 0;
+ return -1;
+
+ case Common::KEYCODE_LEFT:
+ // Volume slider: decrease by 4 (original step size)
+ if (_optionsSelection == 6) {
+ _optVolumeLevel = MAX(0, _optVolumeLevel - 4);
+ _vm->_mixer->setVolumeForSoundType(Audio::Mixer::kMusicSoundType,
+ CLIP<int>(_optVolumeLevel * 2, 0, (int)Audio::Mixer::kMaxMixerVolume));
+ _vm->_mixer->setVolumeForSoundType(Audio::Mixer::kSFXSoundType,
+ CLIP<int>(_optVolumeLevel * 2, 0, (int)Audio::Mixer::kMaxMixerVolume));
+ }
+ return -1;
+
+ case Common::KEYCODE_RIGHT:
+ // Volume slider: increase by 4
+ if (_optionsSelection == 6) {
+ _optVolumeLevel = MIN(127, _optVolumeLevel + 4);
+ _vm->_mixer->setVolumeForSoundType(Audio::Mixer::kMusicSoundType,
+ CLIP<int>(_optVolumeLevel * 2, 0, (int)Audio::Mixer::kMaxMixerVolume));
+ _vm->_mixer->setVolumeForSoundType(Audio::Mixer::kSFXSoundType,
+ CLIP<int>(_optVolumeLevel * 2, 0, (int)Audio::Mixer::kMaxMixerVolume));
+ }
+ return -1;
+
+ case Common::KEYCODE_RETURN:
+ case Common::KEYCODE_KP_ENTER:
+ // Toggle items 0-5, back item 7
+ switch (_optionsSelection) {
+ case 0: // Music toggle
+ _optMusicEnabled = !_optMusicEnabled;
+ _vm->_mixer->muteSoundType(Audio::Mixer::kMusicSoundType, !_optMusicEnabled);
+ break;
+ case 1: // SFX toggle
+ _optSfxEnabled = !_optSfxEnabled;
+ _vm->_mixer->muteSoundType(Audio::Mixer::kSFXSoundType, !_optSfxEnabled);
+ break;
+ case 2: // Voices toggle
+ _optVoicesEnabled = !_optVoicesEnabled;
+ _vm->_mixer->muteSoundType(Audio::Mixer::kSpeechSoundType, !_optVoicesEnabled);
+ break;
+ case 3: // Text toggle
+ _optTextEnabled = !_optTextEnabled;
+ break;
+ case 4: // Controls toggle
+ _optControlsFlipped = !_optControlsFlipped;
+ break;
+ case 5: // Rapid fire toggle
+ _optRapidFire = !_optRapidFire;
+ break;
+ case 6: // Volume (handled by left/right)
+ break;
+ case 7: // Back
+ _optionsExitRequested = true;
+ _vm->_smushVideoShouldFinish = true;
+ return 7;
+ }
+ return _optionsSelection;
+
+ case Common::KEYCODE_ESCAPE:
+ _optionsExitRequested = true;
+ _vm->_smushVideoShouldFinish = true;
+ return -2;
+
+ default:
+ break;
+ }
+ }
+
+ if (event.type == Common::EVENT_LBUTTONDOWN) {
+ // Mouse click on items â match drawMenuItems Y positions
+ int mouseY = event.mouse.y;
+ int baseY = _optionsItemCount * -5 + 0x68;
+ for (int i = 0; i < _optionsItemCount; i++) {
+ int itemY = baseY + i * 10;
+ if (mouseY >= itemY - 1 && mouseY < itemY + 9) {
+ _optionsSelection = i;
+ // Simulate enter for this item
+ Common::Event enterEvent;
+ enterEvent.type = Common::EVENT_KEYDOWN;
+ enterEvent.kbd.keycode = Common::KEYCODE_RETURN;
+ _menuEventQueue.push(enterEvent);
+ return -1;
+ }
+ }
+ }
+ }
+ return -1;
+}
+
+void InsaneRebel2::drawOptionsOverlay(byte *renderBitmap, int pitch, int width, int height) {
+ SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
+ if (!splayer)
+ return;
+
+ // Build items array from TRS strings based on current toggle states
+ // TRS 89: title, 90/91: music, 92/93: sfx, 94/95: voices,
+ // 96/97: text, 98/99: controls, 100/101: rapid fire, 103: volume, 107: back
+ const char *items[10]; // title + up to 9 selectable
+
+ // [0] Title
+ items[0] = splayer->getString(89);
+ if (!items[0] || !items[0][0])
+ items[0] = "^f02Game Options";
+
+ // [1] Music On/Off
+ items[1] = splayer->getString(_optMusicEnabled ? 90 : 91);
+ if (!items[1] || !items[1][0])
+ items[1] = _optMusicEnabled ? "^f01^c005Music is On" : "^f01^c005Music is Off";
+
+ // [2] SFX On/Off
+ items[2] = splayer->getString(_optSfxEnabled ? 92 : 93);
+ if (!items[2] || !items[2][0])
+ items[2] = _optSfxEnabled ? "^f01^c005SFX are On" : "^f01^c005SFX are Off";
+
+ // [3] Voices On/Off
+ items[3] = splayer->getString(_optVoicesEnabled ? 94 : 95);
+ if (!items[3] || !items[3][0])
+ items[3] = _optVoicesEnabled ? "^f01^c005Voices are On" : "^f01^c005Voices are Off";
+
+ // [4] Text On/Off
+ items[4] = splayer->getString(_optTextEnabled ? 96 : 97);
+ if (!items[4] || !items[4][0])
+ items[4] = _optTextEnabled ? "^f01^c005Text is On" : "^f01^c005Text is Off";
+
+ // [5] Controls Normal/Flipped
+ items[5] = splayer->getString(_optControlsFlipped ? 99 : 98);
+ if (!items[5] || !items[5][0])
+ items[5] = _optControlsFlipped ? "^f01^c005Controls are Flipped" : "^f01^c005Controls are Normal";
+
+ // [6] Rapid Fire On/Off
+ items[6] = splayer->getString(_optRapidFire ? 100 : 101);
+ if (!items[6] || !items[6][0])
+ items[6] = _optRapidFire ? "^f01^c005Rapid Fire On" : "^f01^c005Rapid Fire Off";
+
+ // [7] Volume Level (slider) â TRS 103 = "^f01^c005Volume Level: %hd%%"
+ static char volumeBuf[64];
+ const char *volFmt = splayer->getString(103);
+ if (volFmt && volFmt[0])
+ Common::sprintf_s(volumeBuf, volFmt, (short)(_optVolumeLevel * 100 / 127));
+ else
+ Common::sprintf_s(volumeBuf, "^f01^c005Volume Level: %hd%%", (short)(_optVolumeLevel * 100 / 127));
+ items[7] = volumeBuf;
+
+ // [8] Back â TRS 107
+ items[8] = splayer->getString(107);
+ if (!items[8] || !items[8][0])
+ items[8] = "^f01^c240Return to Main Menu";
+
+ drawMenuItems(renderBitmap, pitch, width, height, items, _optionsItemCount, _optionsSelection);
+}
+
} // End of namespace Scumm
diff --git a/engines/scumm/insane/insane_rebel_render.cpp b/engines/scumm/insane/insane_rebel_render.cpp
index be5fc25a4ce..71d94ff7f2c 100644
--- a/engines/scumm/insane/insane_rebel_render.cpp
+++ b/engines/scumm/insane/insane_rebel_render.cpp
@@ -2301,6 +2301,14 @@ void InsaneRebel2::procPostRendering(byte *renderBitmap, int32 codecparam, int32
return;
}
+ // Handle Options menu (FUN_004167A6)
+ bool optionsMode = (introPlaying && _gameState == kStateOptions);
+ if (optionsMode) {
+ processOptionsInput();
+ drawOptionsOverlay(renderBitmap, pitch, width, height);
+ return;
+ }
+
// Handle menu input and rendering if in menu mode
if (menuMode) {
// The original game uses the standard Windows arrow cursor (IDC_ARROW)
Commit: 0f3ad175bc6b864c6ff7073582c1e3d50cb6aff3
https://github.com/scummvm/scummvm/commit/0f3ad175bc6b864c6ff7073582c1e3d50cb6aff3
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:20+02:00
Commit Message:
SCUMM: RA2: Standardize declarations and comments
Changed paths:
engines/scumm/insane/insane_rebel.cpp
engines/scumm/insane/insane_rebel.h
engines/scumm/insane/insane_rebel_audio.cpp
engines/scumm/insane/insane_rebel_iact.cpp
engines/scumm/insane/insane_rebel_levels.cpp
engines/scumm/insane/insane_rebel_menu.cpp
engines/scumm/insane/insane_rebel_render.cpp
engines/scumm/insane/insane_rebel_runlevels.cpp
diff --git a/engines/scumm/insane/insane_rebel.cpp b/engines/scumm/insane/insane_rebel.cpp
index a5fdd71cbc6..ea2dce7a674 100644
--- a/engines/scumm/insane/insane_rebel.cpp
+++ b/engines/scumm/insane/insane_rebel.cpp
@@ -539,9 +539,8 @@ InsaneRebel2::~InsaneRebel2() {
}
}
+// notifyEvent -- EventObserver callback for global input dispatch.
bool InsaneRebel2::notifyEvent(const Common::Event &event) {
- // Handle global key events (ESC to skip, SPACE to pause)
- // These work regardless of menu state
if (event.type == Common::EVENT_KEYDOWN) {
SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
@@ -771,6 +770,7 @@ const InsaneRebel2::LevelDifficultyParams InsaneRebel2::kDifficultyTable[6][17]
},
};
+// getDifficultyParams -- Look up difficulty parameters for current level.
InsaneRebel2::LevelDifficultyParams InsaneRebel2::getDifficultyParams() const {
int diff = CLIP(_difficulty, 0, 5);
int lvIdx = 0;
@@ -800,8 +800,7 @@ InsaneRebel2::LevelDifficultyParams InsaneRebel2::getDifficultyParams() const {
return kDifficultyTable[diff][lvIdx];
}
-// Score system implementation (FUN_0041bf8d equivalent)
-// Adds points to score and awards bonus life when crossing threshold
+// addScore -- Score system with bonus life awards (FUN_0041bf8d).
void InsaneRebel2::addScore(int points) {
// Calculate bonus life threshold based on difficulty (DAT_0047a7fa)
// From FUN_0041bf8d:
@@ -869,15 +868,16 @@ void InsaneRebel2::renderScoreHUD(byte *renderBitmap, int pitch, int width, int
}
}
-// ======================= Pilot Data System =======================
-// Save/load pilot profiles using ScummVM's save file system (one pilot per slot).
-// Follows the Hypno/Wetlands pattern: each pilot = one ScummVM save slot.
-// Uses ScummEngine::makeSavegameName() for standard ScummVM file naming.
-// Original: FUN_00411980 (load) / FUN_00411A5D (save)
+// ---------------------------------------------------------------------------
+// Pilot Data System
+// ---------------------------------------------------------------------------
+// Save/load pilot profiles using ScummVM's save file system.
+// Original: FUN_00411980 (load) / FUN_00411A5D (save).
static const uint32 kPilotSaveMagic = MKTAG('R', 'A', '2', 'P');
static const uint16 kPilotSaveVersion = 2;
+// loadPilots -- Load all pilot profiles from save files (FUN_00411980).
bool InsaneRebel2::loadPilots() {
_numPilots = 0;
@@ -918,6 +918,7 @@ bool InsaneRebel2::loadPilots() {
return _numPilots > 0;
}
+// savePilots -- Save all pilot profiles to save files (FUN_00411A5D).
bool InsaneRebel2::savePilots() {
bool ok = true;
@@ -1044,6 +1045,7 @@ int InsaneRebel2::getPilotHighestLevel() const {
return highest;
}
+// processMouse -- Mouse input with edge detection for buttons.
int32 InsaneRebel2::processMouse() {
int32 buttons = 0;
@@ -1262,10 +1264,8 @@ void InsaneRebel2::clearBit(int n) {
_iactBits[n] = 0;
}
-// Check if shooting is allowed based on current handler and control mode
-// From FUN_0040d836 (Handler 7): shooting only allowed when DAT_004437c0 == 2
-// From FUN_00401CCF (Handler 8): mode 4/5 disable shooting
-// From FUN_41DB5E (Handler 25): only shoot when fully uncovered (DAT_0045790a == 0)
+// isShootingAllowed -- Check control mode before spawning shots.
+// Handler 7: only mode 2. Handler 8: not mode 4/5. Handler 25: not damaged.
bool InsaneRebel2::isShootingAllowed() {
// Handler 7 (Third-Person Ship): Only mode 2 allows shooting
// FUN_0040d836 line 141: if (DAT_004437c0 == 2) { /* spawn shots, draw crosshair */ }
@@ -1291,18 +1291,9 @@ bool InsaneRebel2::isShootingAllowed() {
return (_rebelHandler != 0);
}
+// procSKIP -- Conditional FOBJ/PSAD skip via bit table (FUN_00423A50).
+// RA2 uses SKIP chunks to hide destroyed enemy sprites.
void InsaneRebel2::procSKIP(int32 subSize, Common::SeekableReadStream &b) {
- // Rebel Assault 2 uses SKIP chunks to conditionally skip the next FOBJ/PSAD chunk.
- // The SKIP chunk contains one or two object IDs. If the bit for the object is set
- // (i.e., the object is disabled/destroyed), skip the next chunk.
- //
- // This is the same mechanism as Full Throttle, but RA2 uses it for enemy objects:
- // - When an enemy is destroyed, setBit(enemy_id) is called
- // - SKIP chunks in the video contain the enemy ID
- // - If the bit is set, the next FOBJ (enemy sprite) is skipped
- // - This prevents destroyed enemy sprites from being rendered
- //
- // The original game's FUN_00423A50 chunk reader uses this mechanism.
int16 par1, par2;
_player->_skipNext = false;
diff --git a/engines/scumm/insane/insane_rebel.h b/engines/scumm/insane/insane_rebel.h
index d16c306e937..f3bced79181 100644
--- a/engines/scumm/insane/insane_rebel.h
+++ b/engines/scumm/insane/insane_rebel.h
@@ -57,7 +57,9 @@ public:
Common::Queue<Common::Event> _menuEventQueue;
bool _menuInputActive; // True when we're capturing menu input events
- // ======================= Menu System =======================
+ // ---------------------------------------------------------------------------
+ // Menu System
+ // ---------------------------------------------------------------------------
// Main game states (emulates retail state machine from FUN_004142BD)
enum GameState {
kStateIntro = 0, // Stage 0: Intro/Credits sequence
@@ -119,9 +121,11 @@ public:
// Reset menu state for fresh start
void resetMenu();
- // ================= Chapter Selection Screen (FUN_00415CF8) ====================
- // This is the actual level/chapter selection screen with preview thumbnail
- // Distinct from pilot selection (FUN_00414A41)
+ // ---------------------------------------------------------------------------
+ // Chapter Selection Screen (FUN_00415CF8)
+ // ---------------------------------------------------------------------------
+ // Actual level/chapter selection screen with preview thumbnail.
+ // Distinct from pilot selection (FUN_00414A41).
enum ChapterSelectResult {
kChapterSelectBack = 2, // Return to main menu (ESC or BACK selected)
@@ -168,9 +172,11 @@ public:
// Password table lookup (FUN_0041BCE0)
Common::String getChapterPassword(int level, int difficulty);
- // ================= Top Pilots Screen (FUN_00420116) ====================
- // Shows ranked pilot scores with animated reveal, played over menu video
- // Original: DAT_00443b58 ranking table, 0x4a-byte records, max 15 entries
+ // ---------------------------------------------------------------------------
+ // Top Pilots Screen (FUN_00420116)
+ // ---------------------------------------------------------------------------
+ // Ranked pilot scores with animated reveal, played over menu video.
+ // Original: DAT_00443b58 ranking table, 0x4a-byte records, max 15 entries.
static const int kMaxRankings = 15;
@@ -200,7 +206,9 @@ public:
int _topPilotsFrameCount; // Animation frame counter (pilots revealed one per frame)
int _topPilotsMaxFrames; // Total frames to display (120 or 240)
- // ================= Options Menu (FUN_004167A6) ====================
+ // ---------------------------------------------------------------------------
+ // Options Menu (FUN_004167A6)
+ // ---------------------------------------------------------------------------
// TRS strings: 89 (title), 90-101 (toggle labels), 103 (volume), 107/109 (back)
// Original settings array at DAT_00482e20[0..4]:
// [0]=auto control, [1]=music, [2]=voices, [3]=sound
@@ -222,9 +230,11 @@ public:
bool _optRapidFire;
int _optVolumeLevel; // 0-127 (DAT_0047a804)
- // ================= Pilot Data System (FUN_00411B9A / FUN_00411980 / FUN_00411A5D) ===========
- // Original: 10 pilot slots à 0x118 (280) bytes at DAT_004568A8
- // Stored via SaveFileManager in a custom save file
+ // ---------------------------------------------------------------------------
+ // Pilot Data System (FUN_00411B9A / FUN_00411980 / FUN_00411A5D)
+ // ---------------------------------------------------------------------------
+ // 10 pilot slots à 0x118 (280) bytes at DAT_004568A8.
+ // Stored via SaveFileManager in a custom save file.
static const int kMaxPilots = 10;
static const int kMaxPilotNameLen = 15;
@@ -272,8 +282,10 @@ public:
// Get highest unlocked level for active pilot (checks damage[] < 0xFF)
int getPilotHighestLevel() const;
- // ================= Pilot Selection Menu (FUN_00414A41) ====================
- // This is the pilot/save selection menu (separate from chapter selection)
+ // ---------------------------------------------------------------------------
+ // Pilot Selection Menu (FUN_00414A41)
+ // ---------------------------------------------------------------------------
+ // Pilot/save selection menu (separate from chapter selection).
enum LevelSelectResult {
kLevelSelectBack = 0, // Return to main menu
@@ -307,8 +319,10 @@ public:
// Process pilot select input - returns -1 (no action) or action code
int processLevelSelectInput();
- // ======================= Level Loading System =======================
- // Emulates the level handler functions FUN_00417E53 through FUN_0041BBE8
+ // ---------------------------------------------------------------------------
+ // Level Loading System
+ // ---------------------------------------------------------------------------
+ // Emulates the level handler functions FUN_00417E53 through FUN_0041BBE8.
// Each level has: BEG (intro), gameplay SANs, END (completion), DIE variants,
// RETRY, and OVER (game over) videos.
@@ -428,7 +442,9 @@ public:
int _phaseMisses; // Accumulated misses from previous phases
bool _skipSectionRequested; // Debug shortcut (Shift+S): force current gameplay section to end
- // =============================================================
+ // ---------------------------------------------------------------------------
+ // Resources and Fonts
+ // ---------------------------------------------------------------------------
NutRenderer *_smush_cockpitNut;
@@ -484,8 +500,10 @@ public:
int32 setupsan13, Common::SeekableReadStream &b, int32 size, int32 flags,
int16 par1, int16 par2, int16 par3, int16 par4) override;
- // ======================= Rendering Helper Functions =======================
- // These are extracted from procPostRendering for better readability
+ // ---------------------------------------------------------------------------
+ // Rendering Helper Functions
+ // ---------------------------------------------------------------------------
+ // Extracted from procPostRendering for better readability.
// Fill status bar background area (FUN_004288c0 equivalent)
void renderStatusBarBackground(byte *renderBitmap, int pitch, int width, int height,
@@ -525,8 +543,10 @@ public:
// Reset enemy active flags and collision zones at frame end
void frameEndCleanup();
- // ======================= Opcode 6 Helper Functions =======================
- // Handler-specific setup extracted from iactRebel2Opcode6
+ // ---------------------------------------------------------------------------
+ // Opcode 6 Helper Functions
+ // ---------------------------------------------------------------------------
+ // Handler-specific setup extracted from iactRebel2Opcode6.
// Handler 8 (third-person on foot) setup - FUN_00401234 case 4
void opcode6Handler8Setup(int16 par3, int16 par4);
@@ -537,8 +557,10 @@ public:
// Calculate view offsets based on level type (lines 182-213)
void opcode6CalcViewOffsets();
- // ======================= Opcode 8 Helper Functions =======================
- // Resource loading extracted from iactRebel2Opcode8
+ // ---------------------------------------------------------------------------
+ // Opcode 8 Helper Functions
+ // ---------------------------------------------------------------------------
+ // Resource loading extracted from iactRebel2Opcode8.
// Load Handler 7 FLY NUT sprites from IACT data
bool loadHandler7FlySprites(Common::SeekableReadStream &b, int64 remaining, int16 par4);
@@ -570,8 +592,10 @@ public:
// mask231: when true, color 231 is treated as transparent (legacy sprites). For laser beams set false.
void drawTexturedLine(byte *dst, int pitch, int width, int height, int x0, int y0, int x1, int y1, NutRenderer *nut, int spriteIdx, int v, bool mask231 = true);
- // ======================= Laser Texture Buffer (DAT_0047fee4) =======================
- // Pre-rendered laser texture used by FUN_0040BBF6
+ // ---------------------------------------------------------------------------
+ // Laser Texture Buffer (DAT_0047fee4)
+ // ---------------------------------------------------------------------------
+ // Pre-rendered laser texture used by FUN_0040BBF6.
// Initialized from CPITIMAG.NUT sprite 0 via initLaserTexture() (FUN_0040BAB0)
struct LaserTexture {
byte *pixels; // Pixel data (rendered from NUT sprite)
@@ -580,7 +604,9 @@ public:
};
LaserTexture _laserTexture; // DAT_0047fee4
- // ======================= Edge Blend Table (DAT_0046a7d0) =======================
+ // ---------------------------------------------------------------------------
+ // Edge Blend Table (DAT_0046a7d0)
+ // ---------------------------------------------------------------------------
// 256x256 lookup table used by drawEdgeHighlightLine() (FUN_410962) to compute
// glow colors at beam edges. For each pixel on the beam edge, the table maps
// [adjacent_pixel_above][adjacent_pixel_below] -> output color, producing a
@@ -678,8 +704,10 @@ public:
// userId: HUD slot (1-4), animData: raw ANIM data, size: data size, renderBitmap: current frame buffer
void loadEmbeddedSan(int userId, byte *animData, int32 size, byte *renderBitmap) override;
- // ======================= Embedded Frame Codec Decoders =======================
- // These functions decode different codec formats used in embedded ANIM/FOBJ data
+ // ---------------------------------------------------------------------------
+ // Embedded Frame Codec Decoders
+ // ---------------------------------------------------------------------------
+ // Decode different codec formats used in embedded ANIM/FOBJ data.
// Based on retail FUN_0042C590 (codec 1), FUN_0042BD60 (codec 21), etc.
// Decode codec 21/44 (Line Update) - skip/copy pairs per line
@@ -715,8 +743,10 @@ public:
Explosion _explosions[5];
void spawnExplosion(int x, int y, int objectHalfWidth);
- // ======================= Collision Zone System =======================
- // For Level 3 "pilot" ship obstacle avoidance (FUN_40E35E, FUN_40C3CC)
+ // ---------------------------------------------------------------------------
+ // Collision Zone System
+ // ---------------------------------------------------------------------------
+ // For Level 3 "pilot" ship obstacle avoidance (FUN_40E35E, FUN_40C3CC).
// Collision zones are quadrilaterals defined by IACT Opcode 5
// The player's ship position is tested against these zones each frame
//
@@ -783,7 +813,9 @@ public:
int _viewX;
int _viewY;
- // ======================= Damage Visual Effect System =======================
+ // ---------------------------------------------------------------------------
+ // Damage Visual Effect System
+ // ---------------------------------------------------------------------------
// Palette flash + screen shake on taking damage.
// Original functions: FUN_420515, FUN_420562, FUN_420754, FUN_42073B
//
@@ -841,8 +873,10 @@ public:
int _rebelLastCounter; // Mirrors DAT_0047ab90 (last updated counter)
- // ======================= Handler 0x26 Turret Shot System =======================
- // Based on FUN_40AD63 disassembly - Turret laser rendering
+ // ---------------------------------------------------------------------------
+ // Handler 0x26 Turret Shot System
+ // ---------------------------------------------------------------------------
+ // Based on FUN_40AD63 disassembly - Turret laser rendering.
// DAT_0044367a[2]: Shot duration counter (0=inactive)
// DAT_0044367e[2]: Target X position
// DAT_00443682[2]: Target Y position
@@ -860,8 +894,10 @@ public:
TurretShot _turretShots[2];
int16 _turretShotSeqCounter; // DAT_0047fe94 - global sequence counter
- // ======================= Handler 8 Vehicle Shot System =======================
- // Based on FUN_402ED0 disassembly - Vehicle laser rendering
+ // ---------------------------------------------------------------------------
+ // Handler 8 Vehicle Shot System
+ // ---------------------------------------------------------------------------
+ // Based on FUN_402ED0 disassembly - Vehicle laser rendering.
// DAT_0043e00a[2]: Shot duration counter
// DAT_0043e00e[2]: Target X position
// DAT_0043e012[2]: Target Y position
@@ -874,8 +910,10 @@ public:
};
VehicleShot _vehicleShots[2];
- // ======================= Handler 7 Third-Person Ship Shot System =======================
- // Based on FUN_40FADF disassembly - Third-Person Ship laser rendering
+ // ---------------------------------------------------------------------------
+ // Handler 7 Third-Person Ship Shot System
+ // ---------------------------------------------------------------------------
+ // Based on FUN_40FADF disassembly - Third-Person Ship laser rendering.
// DAT_00443750[2]: Shot duration counter
// DAT_00443754[2]: Target X position
// DAT_00443758[2]: Target Y position
@@ -928,7 +966,9 @@ public:
// Get max shot duration from level table (DAT_0047e0f0 indexed by DAT_0047a7fa/DAT_0047a7f8)
int16 getShotMaxDuration();
- // ======================= Handler 8 Ship System =======================
+ // ---------------------------------------------------------------------------
+ // Handler 8 Ship System
+ // ---------------------------------------------------------------------------
// For third-person on foot missions (Level 2, 11), the player controls Rookie One
// that can turn in different directions. The ship sprite comes from
// NUT files loaded via IACT opcode 8.
@@ -1003,7 +1043,9 @@ public:
// Helper to load a NUT file from IACT chunk data
NutRenderer *loadNutFromIact(Common::SeekableReadStream &b, int dataSize);
- // ======================= Handler 7 FLY Ship System =======================
+ // ---------------------------------------------------------------------------
+ // Handler 7 FLY Ship System
+ // ---------------------------------------------------------------------------
// For third-person ship missions (Level 3, etc.), Handler 7 uses a 35-frame
// direction-based ship sprite system. The ship visually banks and turns
// based on player position using a 5x7 grid of sprites.
@@ -1043,7 +1085,9 @@ public:
int16 _viewShift; // DAT_00443710 - Clamped smoothed velocity for view transform
bool _facingRight; // DAT_0047ab8c - Ship facing right of center
- // ======================= Handler 25 (0x19) GRD Ship System =======================
+ // ---------------------------------------------------------------------------
+ // Handler 25 (0x19) GRD Ship System
+ // ---------------------------------------------------------------------------
// For mixed mode missions (Level 2 speeder bike, etc.), Handler 25 uses GRD NUT
// sprites loaded via IACT opcode 8. The ship is rendered based on DAT_00457900 mode.
//
@@ -1080,7 +1124,9 @@ public:
void renderHandler25ShipPre(byte *renderBitmap, int pitch, int width, int height);
void renderHandler25Ship(byte *renderBitmap, int pitch, int width, int height);
- // ======================= Handler 0x26 Turret HUD Overlays =======================
+ // ---------------------------------------------------------------------------
+ // Handler 0x26 Turret HUD Overlays
+ // ---------------------------------------------------------------------------
// For turret missions (Level 1, etc.), Handler 0x26 uses NUT-based HUD overlays
// loaded via IACT opcode 8. These contain animated cockpit panel elements.
//
@@ -1101,8 +1147,10 @@ public:
/* Difficulty Level (0-5, from pilot menu; maps directly to table rows) */
int _difficulty;
- // ======================= Per-Level Difficulty Parameters =======================
- // Extracted from RA2WIN95.EXE at VA 0x47e0f0
+ // ---------------------------------------------------------------------------
+ // Per-Level Difficulty Parameters
+ // ---------------------------------------------------------------------------
+ // Extracted from RA2WIN95.EXE at VA 0x47e0f0.
// 2D table indexed by difficulty (0-5) Ã level type (0-16)
// Original indexing: &DAT_0047e0f0 + chapter * 0x242 + levelType * 0x22
// Level type (_rebelLevelType) is set by IACT opcode 6 par3
@@ -1147,16 +1195,20 @@ public:
// Render score text to HUD (called from procPostRendering)
void renderScoreHUD(byte *renderBitmap, int pitch, int width, int height, int statusBarY);
- // ======================= Pause Overlay =======================
- // Show pause overlay with dimming effect and "PAUSED" text
+ // ---------------------------------------------------------------------------
+ // Pause Overlay
+ // ---------------------------------------------------------------------------
+ // Show pause overlay with dimming effect and "PAUSED" text.
// Emulates FUN_405A21 pause rendering (lines 242-305)
void showPauseOverlay();
// Target lock timer (DAT_00443676) - set to 7 when crosshair is over enemy
int _targetLockTimer;
- // ========== Audio Handling ==========
- // Rebel Assault 2 doesn't use iMUSE - audio is handled directly here
+ // ---------------------------------------------------------------------------
+ // Audio Handling
+ // ---------------------------------------------------------------------------
+ // RA2 doesn't use iMUSE -- audio is handled directly through the mixer.
static const int kRA2MaxAudioTracks = 4;
@@ -1178,7 +1230,9 @@ public:
// Queue audio data for playback on a specific track
void queueAudioData(int trackIdx, uint8 *data, int32 size, int volume, int pan);
- // ========== Sound Effects (SAD files) ==========
+ // ---------------------------------------------------------------------------
+ // Sound Effects (SAD files)
+ // ---------------------------------------------------------------------------
// 8 standalone SAUD files in SYSTM/ loaded at init for one-shot SFX.
// Slot mapping (from FUN_0042a3b0 init):
// 0=BLAST.SAD 1=CRASH.SAD 2=EXPLODE.SAD 3=ALERT.SAD
@@ -1200,7 +1254,9 @@ public:
// slot: 0-7 (SAD file index), volume: 0-127, pan: -127..+127
void playSfx(int slot, int volume, int pan);
- // ========== Auxiliary Sound Buffers ==========
+ // ---------------------------------------------------------------------------
+ // Auxiliary Sound Buffers
+ // ---------------------------------------------------------------------------
// 4 pre-allocated buffers (30000 bytes each) loaded from IACT stream data.
// Original: DAT_00480308[0..3], loaded via FUN_004118df, played via FUN_00411931.
// Used for embedded sound effects (e.g., soldier death sounds in handler 8 levels).
diff --git a/engines/scumm/insane/insane_rebel_audio.cpp b/engines/scumm/insane/insane_rebel_audio.cpp
index 7b971f8eea1..a063144f40b 100644
--- a/engines/scumm/insane/insane_rebel_audio.cpp
+++ b/engines/scumm/insane/insane_rebel_audio.cpp
@@ -33,9 +33,12 @@
namespace Scumm {
-// ========== Audio Handling for Rebel Assault 2 ==========
-// RA2 doesn't use iMUSE - we handle audio directly through the mixer
+// ---------------------------------------------------------------------------
+// Audio Handling
+// ---------------------------------------------------------------------------
+// RA2 doesn't use iMUSE -- audio is handled directly through the mixer.
+// initAudio -- Initialize audio system for RA2.
void InsaneRebel2::initAudio(int sampleRate) {
_audioSampleRate = sampleRate;
for (int i = 0; i < kRA2MaxAudioTracks; i++) {
@@ -44,6 +47,7 @@ void InsaneRebel2::initAudio(int sampleRate) {
}
}
+// terminateAudio -- Stop all tracks and release audio streams.
void InsaneRebel2::terminateAudio() {
for (int i = 0; i < kRA2MaxAudioTracks; i++) {
if (_audioTrackActive[i]) {
@@ -57,6 +61,8 @@ void InsaneRebel2::terminateAudio() {
}
}
+// queueAudioData -- Queue raw PCM data for playback on a track.
+// Creates the queuing stream on first use. RA2 audio is 8-bit unsigned mono.
void InsaneRebel2::queueAudioData(int trackIdx, uint8 *data, int32 size, int volume, int pan) {
if (trackIdx < 0 || trackIdx >= kRA2MaxAudioTracks || size <= 0 || !data) {
debug(5, "InsaneRebel2::queueAudioData: Invalid params trackIdx=%d size=%d data=%p", trackIdx, size, (void*)data);
@@ -95,6 +101,13 @@ void InsaneRebel2::queueAudioData(int trackIdx, uint8 *data, int32 size, int vol
_vm->_mixer->setChannelBalance(_audioHandles[trackIdx], scaledPan);
}
+//
+// processAudioFrame -- Per-frame audio dispatch (replaces iMUSE path)
+//
+// Iterates SmushPlayer audio tracks, handles FADING->PLAYING transitions,
+// and feeds PCM data through queueAudioData. Called from SmushPlayer when
+// iMUSE is null.
+//
void InsaneRebel2::processAudioFrame(int16 feedSize) {
if (!_player) {
return;
@@ -256,9 +269,11 @@ void InsaneRebel2::processAudioFrame(int16 feedSize) {
}
}
-// ========== Sound Effects (SAD files) ==========
-// Loads standalone SAUD files from SYSTM/ for one-shot SFX playback.
-// Original game loads these via FUN_0042a3b0 at init into DAT_00456888[0..7].
+// ---------------------------------------------------------------------------
+// Sound Effects (SAD files)
+// ---------------------------------------------------------------------------
+// Standalone SAUD files from SYSTM/ loaded at init for one-shot SFX.
+// Original: FUN_0042a3b0 loads into DAT_00456888[0..7].
static const char *const kRA2SfxFiles[InsaneRebel2::kRA2NumSfx] = {
"SYSTM/BLAST.SAD", // 0 - Player laser fire
@@ -271,6 +286,7 @@ static const char *const kRA2SfxFiles[InsaneRebel2::kRA2NumSfx] = {
"SYSTM/TBLAST.SAD" // 7 - TIE blast
};
+// loadSfx -- Load all SAD files from SYSTM/ directory (FUN_0042a3b0).
void InsaneRebel2::loadSfx() {
for (int i = 0; i < kRA2NumSfx; i++) {
ScummFile *file = _vm->instantiateScummFile();
@@ -333,6 +349,7 @@ void InsaneRebel2::loadSfx() {
}
}
+// freeSfx -- Free all loaded SFX data and auxiliary buffers.
void InsaneRebel2::freeSfx() {
for (int i = 0; i < kRA2NumSfx; i++) {
// Stop any playing SFX on this slot
@@ -349,6 +366,7 @@ void InsaneRebel2::freeSfx() {
}
}
+// playSfx -- Play a one-shot sound effect (8-bit unsigned mono, 11025 Hz).
void InsaneRebel2::playSfx(int slot, int volume, int pan) {
if (slot < 0 || slot >= kRA2NumSfx || !_sfxData[slot] || _sfxSize[slot] == 0) {
return;
@@ -377,6 +395,7 @@ void InsaneRebel2::playSfx(int slot, int volume, int pan) {
debug(5, "InsaneRebel2::playSfx: slot=%d vol=%d pan=%d size=%d", slot, volume, pan, _sfxSize[slot]);
}
+// loadAuxSfx -- Load sound data into auxiliary buffer (FUN_004118df).
void InsaneRebel2::loadAuxSfx(int buffer, const byte *data, uint32 size) {
if (buffer < 0 || buffer >= kRA2NumAuxSfx || !data || size == 0) {
return;
@@ -393,6 +412,8 @@ void InsaneRebel2::loadAuxSfx(int buffer, const byte *data, uint32 size) {
debug(5, "InsaneRebel2::loadAuxSfx: buffer=%d size=%d", buffer, size);
}
+// playAuxSfx -- Play from auxiliary buffer (FUN_00411931).
+// Handles both raw PCM and SAUD-wrapped data.
void InsaneRebel2::playAuxSfx(int buffer, int volume, int pan) {
if (buffer < 0 || buffer >= kRA2NumAuxSfx || !_auxSfxData[buffer] || _auxSfxSize[buffer] == 0) {
return;
diff --git a/engines/scumm/insane/insane_rebel_iact.cpp b/engines/scumm/insane/insane_rebel_iact.cpp
index 1ecd8052fa1..104d64dc9d1 100644
--- a/engines/scumm/insane/insane_rebel_iact.cpp
+++ b/engines/scumm/insane/insane_rebel_iact.cpp
@@ -35,8 +35,13 @@ namespace Scumm {
// External codec functions from codec1.cpp
extern void smushDecodeRLEOpaque(byte *dst, const byte *src, int left, int top, int width, int height, int pitch);
+//
+// procPreRendering -- Pre-frame setup: background restore and corridor overlays.
+//
+// Restores Level 2 background before FOBJ decoding (Handler 8) and handles
+// Handler 25 corridor overlay positioning.
+//
void InsaneRebel2::procPreRendering(byte *renderBitmap) {
- // Call base class implementation first (handles Full Throttle state machine)
Insane::procPreRendering(renderBitmap);
// Reset opcode 6 init flag at the start of each new video.
@@ -93,10 +98,10 @@ void InsaneRebel2::procPreRendering(byte *renderBitmap) {
}
}
+// procIACT -- Main IACT chunk dispatcher (overrides Insane::procIACT).
void InsaneRebel2::procIACT(byte *renderBitmap, int32 codecparam, int32 setupsan12,
int32 setupsan13, Common::SeekableReadStream &b, int32 size, int32 flags,
int16 par1, int16 par2, int16 par3, int16 par4) {
- // Debug: Log all IACT opcodes
debug("Rebel2 IACT: opcode=%d par2=%d par3=%d par4=%d gameState=%d sceneId=%d",
par1, par2, par3, par4, _gameState, _currSceneId);
@@ -162,7 +167,7 @@ void InsaneRebel2::procIACT(byte *renderBitmap, int32 codecparam, int32 setupsan
iactRebel2Scene1(renderBitmap, codecparam, setupsan12, setupsan13, b, size, flags, par1, par2, par3, par4);
}
-
+// iactRebel2Scene1 -- Scene 1 IACT dispatcher (FUN_4028C5 / FUN_4033CF).
void InsaneRebel2::iactRebel2Scene1(byte *renderBitmap, int32 codecparam, int32 setupsan12,
int32 setupsan13, Common::SeekableReadStream &b, int32 size, int32 flags,
int16 par1, int16 par2, int16 par3, int16 par4) {
@@ -290,6 +295,8 @@ void InsaneRebel2::iactRebel2Scene1(byte *renderBitmap, int32 codecparam, int32
debug("Rebel2 IACT: Unknown Opcode %d (par2=%d par3=%d par4=%d)", par1, par2, par3, par4);
}
}
+
+// iactRebel2Opcode2 -- Link table and state setup (FUN_00407fcb).
void InsaneRebel2::iactRebel2Opcode2(Common::SeekableReadStream &b, int16 par2, int16 par3, int16 par4) {
// Handle IACT opcode 2 subcases based on par3 (type). Mirrors FUN_00407fcb behavior where relevant.
// Keep existing linking behavior (par3 == 4) for compatibility.
@@ -400,6 +407,8 @@ void InsaneRebel2::iactRebel2Opcode2(Common::SeekableReadStream &b, int16 par2,
debug("Rebel2 IACT Opcode2: Unhandled par3=%d par4=%d", par3, par4);
}
}
+
+// iactRebel2Opcode3 -- Damage and hit counter processing (FUN_4092D9 / FUN_40E35E / FUN_401234).
void InsaneRebel2::iactRebel2Opcode3(Common::SeekableReadStream &b, int16 par2, int16 par3, int16 par4) {
// IACT opcode 3 â damage and hit counter processing.
// Based on FUN_4092D9 (Handler 0x26), FUN_40E35E (Handler 7), FUN_401234 (Handler 8).
@@ -552,6 +561,13 @@ void InsaneRebel2::iactRebel2Opcode3(Common::SeekableReadStream &b, int16 par2,
}
}
+//
+// iactRebel2Opcode6 -- Level setup / mode switch (FUN_41CADB case 4)
+//
+// Per-wave initialization: clears bit table, resets link tables, configures
+// handler mode (ship/turret/corridor), and loads collision zones. Called once
+// per wave video on frame 0.
+//
void InsaneRebel2::iactRebel2Opcode6(byte *renderBitmap, Common::SeekableReadStream &b, int32 chunkSize, int16 par2, int16 par3, int16 par4) {
// Opcode 6: Level setup / mode switch
// Based on FUN_41CADB case 4 (switch on *local_14 - 2 == 4, meaning opcode 6)
@@ -1464,16 +1480,19 @@ void InsaneRebel2::iactRebel2Opcode6(byte *renderBitmap, Common::SeekableReadStr
}
}
+//
+// iactRebel2Opcode8 -- HUD/Ship resource loading (FUN_0040c3cc / FUN_00401234 / FUN_00407fcb)
+//
+// Decodes embedded ANIM data from IACT chunks and dispatches to
+// handler-specific loaders for NUT sprites, HUD overlays, and backgrounds.
+//
+// Handler-specific routing:
+// Handler 7 (FLY): FLY NUT sprites via par4 (1, 2, 3, 11)
+// Handler 8 (POV): POV NUT sprites via par3 (1, 3, 6, 7) or background via par4=5
+// Handler 0x26 (turret): Turret HUD NUT via par3 (1-4)
+// Handler 0x19: Mixed turret mode, similar to 0x26
+//
void InsaneRebel2::iactRebel2Opcode8(byte *renderBitmap, Common::SeekableReadStream &b, int32 chunkSize, int16 par2, int16 par3, int16 par4) {
- // Opcode 8: HUD/Ship resource loading
- // Dispatches to handler-specific loading functions based on current handler and parameters.
- //
- // Handler-specific routing (based on retail disassembly):
- // Handler 7 (FUN_0040c3cc): FLY NUT sprites via par4 (1, 2, 3, 11)
- // Handler 8 (FUN_00401234): POV NUT sprites via par3 (1, 3, 6, 7) or background via par4=5
- // Handler 0x26 (FUN_00407fcb): Turret HUD NUT via par3 (1-4)
- // Handler 0x19: Mixed turret mode, similar to 0x26
- //
// Sound loading: par3 in range 21-47
debug("Rebel2 IACT Opcode 8: handler=%d par2=%d par3=%d par4=%d (gameState=%d)",
@@ -1706,6 +1725,7 @@ void InsaneRebel2::iactRebel2Opcode8(byte *renderBitmap, Common::SeekableReadStr
b.seek(startPos);
}
+// loadHandler25ShotOriginTable -- Parse shot origin coordinate pairs from IACT payload.
bool InsaneRebel2::loadHandler25ShotOriginTable(Common::SeekableReadStream &b, int64 startPos, int64 remaining) {
// IACT layout at this point:
// - stream is positioned after par1..par4 (8 bytes consumed by caller)
@@ -1793,10 +1813,13 @@ bool InsaneRebel2::loadHandler25ShotOriginTable(Common::SeekableReadStream &b, i
return true;
}
-// ======================= Opcode 8 Helper Functions =======================
-// These helper functions are extracted from the original monolithic iactRebel2Opcode8
-// to improve code readability and match the retail FUN_* function structure.
+// ---------------------------------------------------------------------------
+// Opcode 8 Helper Functions
+// ---------------------------------------------------------------------------
+// Extracted from the original monolithic iactRebel2Opcode8 to match
+// the retail FUN_* function structure.
+// loadHandler7FlySprites -- Handler 7 FLY NUT loading (FUN_0040c3cc case 6).
bool InsaneRebel2::loadHandler7FlySprites(Common::SeekableReadStream &b, int64 remaining, int16 par4) {
// Handler 7 FLY NUT loading - FUN_0040c3cc case 6 (opcode 8)
// IACT structure after par1-par4 (we're at offset +8):
@@ -1908,6 +1931,7 @@ bool InsaneRebel2::loadHandler7FlySprites(Common::SeekableReadStream &b, int64 r
return assigned;
}
+// loadTurretHudOverlay -- Handler 0x26/0x19 turret HUD loading (FUN_00407fcb case 8).
bool InsaneRebel2::loadTurretHudOverlay(byte *animData, int32 size, int16 par3) {
// Handler 0x26/0x19 turret HUD overlay loading - FUN_00407fcb case 8
// Resolution-dependent loading:
@@ -1953,6 +1977,7 @@ bool InsaneRebel2::loadTurretHudOverlay(byte *animData, int32 size, int16 par3)
return true;
}
+// loadHandler8ShipSprites -- Handler 8 POV NUT loading (FUN_00401234 case 6).
bool InsaneRebel2::loadHandler8ShipSprites(byte *animData, int32 size, int16 par4) {
// Handler 8 ship POV NUT loading - FUN_00401234 case 6 (opcode 8)
// par4 values (from IACT data offset +6, NOT par3 which is always 0):
@@ -2005,6 +2030,7 @@ bool InsaneRebel2::loadHandler8ShipSprites(byte *animData, int32 size, int16 par
return true;
}
+// loadHandler25GrdSprites -- Handler 25 GRD NUT loading (FUN_0041cadb case 6).
bool InsaneRebel2::loadHandler25GrdSprites(byte *animData, int32 size, int16 par4) {
// Handler 25 GRD ship NUT loading - FUN_0041cadb case 6 (opcode 8)
// par4 values (from IACT data offset +6):
@@ -2049,6 +2075,7 @@ bool InsaneRebel2::loadHandler25GrdSprites(byte *animData, int32 size, int16 par
return true;
}
+// loadLevel2Background -- Decode Level 2 background from embedded ANIM (FUN_00401234 case 5).
bool InsaneRebel2::loadLevel2Background(byte *animData, int32 size, byte *renderBitmap) {
// Level 2 background loading from embedded ANIM - FUN_00401234 case 5
// par4=5 contains the background image embedded as ANIM with FOBJ codec 3
@@ -2149,6 +2176,13 @@ bool InsaneRebel2::loadLevel2Background(byte *animData, int32 size, byte *render
return foundBackground;
}
+//
+// iactRebel2Opcode9 -- Text/subtitle display via IACT chunk
+//
+// Handles inline text in IACT chunks. Most RA2 subtitles use TRES chunks
+// (handled by SmushPlayer::handleTextResource); this opcode is less common.
+// Supports multi-line wrapping, centered/shadowed text, and clip regions.
+//
void InsaneRebel2::iactRebel2Opcode9(byte *renderBitmap, Common::SeekableReadStream &b, int16 par2, int16 par3, int16 par4) {
// Opcode 9: Text/Subtitle Display via IACT chunk
// Note: Most RA2 subtitles use TRES chunks handled by SmushPlayer::handleTextResource()
@@ -2370,6 +2404,7 @@ void InsaneRebel2::iactRebel2Opcode9(byte *renderBitmap, Common::SeekableReadStr
posX, posY, textFlags, clipX, clipY, clipW, clipH);
}
+// enemyUpdate -- Opcode 4: update enemy position and state (FUN_004028C5 / FUN_0041E7C2).
void InsaneRebel2::enemyUpdate(byte *renderBitmap, Common::SeekableReadStream &b, int16 par2, int16 par3, int16 par4) {
// Opcode 4: Enemy position update
// Read 5 shorts from the stream (offset +8 through +16)
@@ -2427,6 +2462,7 @@ void InsaneRebel2::enemyUpdate(byte *renderBitmap, Common::SeekableReadStream &b
}
}
+// init_enemyStruct -- Create and append a new enemy entry.
void InsaneRebel2::init_enemyStruct(int id, int32 x, int32 y, int32 w, int32 h, bool active, bool destroyed, int32 explosionFrame, int type) {
enemy e;
e.id = id;
diff --git a/engines/scumm/insane/insane_rebel_levels.cpp b/engines/scumm/insane/insane_rebel_levels.cpp
index 23acf9645da..1d5709f9f1a 100644
--- a/engines/scumm/insane/insane_rebel_levels.cpp
+++ b/engines/scumm/insane/insane_rebel_levels.cpp
@@ -29,34 +29,32 @@
namespace Scumm {
-// ======================= Level Loading System =======================
-// Emulates the level handler functions from FUN_00417E53 through FUN_0041BBE8
+// ---------------------------------------------------------------------------
+// Level Loading System
+// ---------------------------------------------------------------------------
+// Emulates the level handler functions from FUN_00417E53 through FUN_0041BBE8.
// Based on disassembly analysis of the retail Rebel Assault 2 executable.
Common::String InsaneRebel2::getLevelDir(int levelId) {
- // Returns directory name like "LEV01" for level 1
return Common::String::format("LEV%02d", levelId);
}
Common::String InsaneRebel2::getLevelPrefix(int levelId) {
- // Returns file prefix like "01" for level 1
return Common::String::format("%02d", levelId);
}
+//
+// playIntroSequence -- Intro sequence (FUN_004142BD case 0)
+//
+// Original flow:
+// - If 'f','o','x' keys all held: play CREDITS/O_OPEN_C.SAN (Fox logo easter egg)
+// - If 'b','o','t' keys all held: play CREDITS/O_OPEN_D.SAN (LucasArts logo)
+// - Else: play OPEN/O_OPEN_A.SAN (main intro)
+// - If DAT_0047ab45 || DAT_0047ab47: play OPEN/O_OPEN_B.SAN (additional intro)
+//
+// We skip the easter eggs and play both O_OPEN_A + O_OPEN_B unconditionally.
+//
void InsaneRebel2::playIntroSequence() {
- // Emulates case 0 in FUN_004142BD
- //
- // Original flow:
- // - If 'f','o','x' keys all held: play CREDITS/O_OPEN_C.SAN (Fox logo easter egg)
- // - If 'b','o','t' keys all held: play CREDITS/O_OPEN_D.SAN (LucasArts logo)
- // INSTEAD of the normal intro
- // - Else: play OPEN/O_OPEN_A.SAN (main intro - normal path)
- // - If DAT_0047ab45 || DAT_0047ab47: play OPEN/O_OPEN_B.SAN (additional intro)
- // - Fade out over 10 frames, clear palette, show top pilots, then -> main menu
- //
- // We skip the Fox/LucasArts easter eggs (require real-time key state during boot)
- // and play both O_OPEN_A + O_OPEN_B unconditionally.
-
debug("Rebel2: Playing intro sequence");
_gameState = kStateIntro;
@@ -81,9 +79,8 @@ void InsaneRebel2::playIntroSequence() {
splayer->play("OPEN/O_OPEN_B.SAN", 12);
}
+// playMissionBriefing -- Mission briefing screen (FUN_00415CF8).
void InsaneRebel2::playMissionBriefing() {
- // Emulates FUN_00415CF8 (partial - just the video)
- // Plays OPEN/O_LEVEL.SAN which shows the mission briefing screen
debug("Rebel2: Playing mission briefing");
@@ -92,15 +89,9 @@ void InsaneRebel2::playMissionBriefing() {
splayer->play("OPEN/O_LEVEL.SAN", 12);
}
+// playCinematic -- Play a cinematic/cutscene video.
+// Resets handler to 0 (no HUD) and sets flags to 0x28 (cinematic + buffer preserve).
void InsaneRebel2::playCinematic(const char *filename) {
- // Play a cinematic/cutscene video with proper intro mode setup
- // This helper ensures:
- // 1. Handler is reset to 0 (no HUD, no shooting)
- // 2. Video flags are set to 0x28 (cinematic with buffer preserve)
- //
- // Original: All video wrapper functions (FUN_00417168, FUN_004171c5,
- // FUN_00417ab2, FUN_00417327) add | 8 to the base flags before calling
- // FUN_0041f4d0, so the 0x08 bit (preserve buffer) is always set.
_rebelHandler = 0;
_rebelStatusBarSprite = 0; // No status bar during cinematics
@@ -109,13 +100,10 @@ void InsaneRebel2::playCinematic(const char *filename) {
splayer->play(filename, 12);
}
+// playVideoWithText -- Video with progressive text overlay (FUN_004171c5).
+// Text is progressively revealed during [fadeInFrame, fadeOutFrame).
void InsaneRebel2::playVideoWithText(const char *filename, int textID, int textX, int textY,
int fadeInFrame, int fadeOutFrame) {
- // Emulates FUN_004171c5: plays video with progressive text overlay
- // Text string loaded from GAME.TRS via getString(textID)
- // During frame range [fadeInFrame, fadeOutFrame):
- // displayLength = currentFrame + 10 - fadeInFrame, capped at 0xBE (190) chars
- // Text rendered at (textX, textY) using FUN_004341a0
_rebelHandler = 0;
_rebelStatusBarSprite = 0;
@@ -135,12 +123,9 @@ void InsaneRebel2::playVideoWithText(const char *filename, int textID, int textX
_textOverlayActive = false;
}
+// playLevelBegin -- Level beginning cinematic (LEVXX/XXBEG.SAN).
+// Uses per-level text overlay parameters from GAME.TRS via FUN_004171c5.
void InsaneRebel2::playLevelBegin(int levelId) {
- // Play the level beginning cinematic (LEVXX/XXBEG.SAN)
- // Emulates FUN_004171c5 call in each level handler
- //
- // Per-level text overlay parameters from original disassembly:
- // All levels use FUN_004171c5 with a chapter title overlay from GAME.TRS.
struct TextOverlayParams {
int textID; // TRS string ID (-1 = no text overlay)
@@ -187,14 +172,13 @@ void InsaneRebel2::playLevelBegin(int levelId) {
}
}
+//
+// playLevelGameplay -- Main gameplay video(s) for a level
+//
+// Returns true if level completed (shield > 0), false if died.
+// Structures vary by level: single SAN, multi-part subdirs, or two phases.
+//
bool InsaneRebel2::playLevelGameplay(int levelId) {
- // Play the main gameplay video(s) for a level
- // Returns true if level completed (damage < 0xff), false if died
- //
- // Different levels have different gameplay structures:
- // - Level 1, 4, 5: Single gameplay SAN (XXPXX.SAN or XXPLAY.SAN)
- // - Level 2: Multiple parts with subdirectories (P1/, P2/, P3/)
- // - Level 3, 6: Two gameplay phases (XXPLAY1.SAN, XXPLAY2.SAN)
Common::String dir = getLevelDir(levelId);
Common::String prefix = getLevelPrefix(levelId);
@@ -377,9 +361,8 @@ bool InsaneRebel2::playLevelGameplay(int levelId) {
return (_playerShield > 0);
}
+// playLevelEnd -- Level completion video (FUN_00417327).
void InsaneRebel2::playLevelEnd(int levelId) {
- // Play level completion video (LEVXX/XXEND.SAN)
- // Emulates FUN_00417327 call
_rebelHandler = 0;
_rebelStatusBarSprite = 0; // No status bar during end cinematic
@@ -396,10 +379,8 @@ void InsaneRebel2::playLevelEnd(int levelId) {
splayer->play(filename.c_str(), 12);
}
+// playLevelDeath -- Death video (LEVXX/XXDIE_X.SAN, FUN_00417168).
void InsaneRebel2::playLevelDeath(int levelId) {
- // Play death video (LEVXX/XXDIE_X.SAN)
- // The variant depends on the frame where player died
- // For simplicity, we'll play the A variant
_rebelHandler = 0;
_rebelStatusBarSprite = 0; // No status bar during death cinematic
@@ -423,9 +404,8 @@ void InsaneRebel2::playLevelDeath(int levelId) {
splayer->play(filename.c_str(), 12);
}
+// playLevelRetry -- Retry prompt video (LEVXX/XXRETRY.SAN, FUN_00417168).
void InsaneRebel2::playLevelRetry(int levelId) {
- // Play retry prompt video (LEVXX/XXRETRY.SAN)
- // Reset handler state for the retry cinematic
_rebelHandler = 0;
_rebelStatusBarSprite = 0; // Reset for retry - will be set by IACT opcode 6 if needed
@@ -442,9 +422,8 @@ void InsaneRebel2::playLevelRetry(int levelId) {
splayer->play(filename.c_str(), 12);
}
+// playLevelGameOver -- Game over video (FUN_00417ab2).
void InsaneRebel2::playLevelGameOver(int levelId) {
- // Play game over video (LEVXX/XXOVER.SAN)
- // Emulates FUN_00417ab2 call
_rebelHandler = 0;
_rebelStatusBarSprite = 0; // No status bar during game over cinematic
@@ -461,9 +440,8 @@ void InsaneRebel2::playLevelGameOver(int levelId) {
splayer->play(filename.c_str(), 12);
}
+// playCreditsSequence -- End credits (OPEN/O_CREDIT.SAN).
void InsaneRebel2::playCreditsSequence() {
- // Play the end credits (OPEN/O_CREDIT.SAN)
- // Individual credits are in CREDITS/CRED_XX.SAN
debug("Rebel2: Playing credits");
@@ -472,9 +450,8 @@ void InsaneRebel2::playCreditsSequence() {
splayer->play("OPEN/O_CREDIT.SAN", 12);
}
+// runLevel -- Main level dispatcher, calls per-level handlers.
int InsaneRebel2::runLevel(int levelId) {
- // Main level dispatcher - calls per-level handlers
- // Each level handler emulates its retail counterpart (FUN_00417E53 etc.)
debug("Rebel2: Starting level %d", levelId);
@@ -546,20 +523,23 @@ int InsaneRebel2::runLevel(int levelId) {
}
}
-// =============================================================================
-// Helper functions
-// =============================================================================
+// ---------------------------------------------------------------------------
+// Helper Functions
+// ---------------------------------------------------------------------------
+// Emulates FUN_004233a0.
int InsaneRebel2::getRandomVariant(int max) {
- // Emulates FUN_004233a0 - returns random number 0 to max-1
return _vm->_rnd.getRandomNumber(max - 1);
}
+//
+// selectDeathVideoVariant -- Frame-based death video selection
+//
+// Returns variant suffix ("A", "B", "C", etc.) based on level, phase,
+// and the frame where the player died. Emulates the per-level frame
+// threshold tables in the retail level handlers.
+//
Common::String InsaneRebel2::selectDeathVideoVariant(int levelId, int phase, int frame) {
- // Select death video variant based on level, phase, and death frame
- // Emulates the frame-based death video selection in retail level handlers
- //
- // Returns variant suffix: "A", "B", "C", etc.
switch (levelId) {
case 1:
@@ -725,8 +705,8 @@ Common::String InsaneRebel2::selectDeathVideoVariant(int levelId, int phase, int
}
}
+// playLevelDeathVariant -- Death video with variant selection.
void InsaneRebel2::playLevelDeathVariant(int levelId, int phase, int frame) {
- // Play death video with proper variant selection
_rebelHandler = 0;
_rebelStatusBarSprite = 0; // No status bar during death cinematic
@@ -751,8 +731,8 @@ void InsaneRebel2::playLevelDeathVariant(int levelId, int phase, int frame) {
splayer->play(filename.c_str(), 12);
}
+// playLevelRetryVariant -- Phase-specific retry video.
void InsaneRebel2::playLevelRetryVariant(int levelId, int phase) {
- // Play retry video - phase-specific for multi-phase levels
_rebelHandler = 0;
_rebelStatusBarSprite = 0; // Reset for retry - will be set by IACT opcode 6 if needed
diff --git a/engines/scumm/insane/insane_rebel_menu.cpp b/engines/scumm/insane/insane_rebel_menu.cpp
index 0ea75e2821c..f86c8557a6c 100644
--- a/engines/scumm/insane/insane_rebel_menu.cpp
+++ b/engines/scumm/insane/insane_rebel_menu.cpp
@@ -37,8 +37,10 @@
namespace Scumm {
-// ======================= Menu System Implementation =======================
-// Emulates retail menu system from FUN_004147B2 and FUN_0041FDC8
+// ---------------------------------------------------------------------------
+// Menu System Implementation
+// ---------------------------------------------------------------------------
+// Emulates retail menu system from FUN_004147B2 and FUN_0041FDC8.
void InsaneRebel2::resetMenu() {
_menuSelection = 0;
@@ -47,9 +49,7 @@ void InsaneRebel2::resetMenu() {
_menuSelectionConfirmed = false;
}
-// Unlock all chapters for testing
-// Emulates the debug mode from original FUN_00415CF8 (lines 60-71)
-// where DAT_0047ab34 == 'd' enables level unlock via special codes
+// unlockAllChapters -- Debug mode unlock (FUN_00415CF8).
void InsaneRebel2::unlockAllChapters() {
debug("Rebel2: Unlocking all chapters for testing");
for (int i = 0; i < 16; i++) {
@@ -58,16 +58,9 @@ void InsaneRebel2::unlockAllChapters() {
}
}
+// getRandomMenuVideo -- Select random menu video variant (FUN_0041FDC8).
+// Always uses O_MENU_X.SAN (A-O) instead of audio-only O_MENU.SAN.
Common::String InsaneRebel2::getRandomMenuVideo() {
- // Emulates FUN_0041FDC8 - selects random menu video variant
- //
- // NOTE: The original game plays O_MENU.SAN when no progress flags are set,
- // but that file contains ONLY audio (no FOBJ video frames). The O_MENU_X.SAN
- // variants (A through O) contain actual 320x200 background images in Frame 0.
- //
- // We ALWAYS use a random variant to ensure a proper background is displayed.
- // The original behavior of showing O_MENU.SAN (audio-only) would result in
- // a black/undefined background which doesn't match the intended experience.
// Select random variant (0-14 maps to A-O), ensuring different from last
int variant;
@@ -82,18 +75,14 @@ Common::String InsaneRebel2::getRandomMenuVideo() {
return Common::String::format("OPEN/O_MENU_%c.SAN", letter);
}
+//
+// processMenuInput -- Menu input handling (FUN_0041f5ae)
+//
+// Returns -1 (no action) or 0-4 (menu item selected).
+// Events captured by notifyEvent() before ScummEngine consumes them.
+// Keyboard: Up/Down navigate, Enter confirms. Mouse: Y maps to selection.
+//
int InsaneRebel2::processMenuInput() {
- // Emulates FUN_0041f5ae menu input handling
- // Returns: -1 = no action, 0-4 = menu item selected
- //
- // Events are captured by notifyEvent() (EventObserver) which runs before
- // ScummEngine::parseEvents() consumes them. This ensures we don't miss
- // any input events even though we only process them on video frames.
- //
- // From FUN_0041f5ae disassembly:
- // - Keyboard: Up/Down arrows navigate, Enter confirms
- // - Mouse mode (DAT_0047a806 == 1): Y position maps to selection
- // - Key codes: Up=0x148, Down=0x150, Enter=0x0d, ESC=0x1b
int result = -1;
@@ -208,13 +197,12 @@ int InsaneRebel2::processMenuInput() {
return result;
}
+//
+// drawMenuItems -- Shared menu item renderer (FUN_0041f5ae)
+//
void InsaneRebel2::drawMenuItems(byte *renderBitmap, int pitch, int width, int height,
const char **items, int numItems, int selection,
bool leftAligned) {
- // =====================================================================
- // Shared menu renderer - Emulates FUN_0041f5ae
- // Address: 0x41F5AE
- // =====================================================================
//
// items[0] = title string, items[1..numItems] = selectable items
// numItems = number of selectable items (FUN_0041f5ae param_3)
@@ -431,8 +419,7 @@ void InsaneRebel2::drawMenuItems(byte *renderBitmap, int pitch, int width, int h
}
}
-// Format-code-aware string width calculation
-// Fixed-width codes: ^fNN (2-digit font), ^cNNN (3-digit color), ^^ (literal ^)
+// getMenuStringWidth -- Format-code-aware string width (^fNN, ^cNNN, ^^).
int InsaneRebel2::getMenuStringWidth(const char *str) const {
NutRenderer *fonts[3] = { _smush_talkfontNut, _smush_smalfontNut, _smush_titlefontNut };
NutRenderer *defaultFont = fonts[0] ? fonts[0] : _smush_smalfontNut;
@@ -561,9 +548,11 @@ void InsaneRebel2::drawMenuOverlay(byte *renderBitmap, int pitch, int width, int
drawMenuItems(renderBitmap, pitch, width, height, menuItems, 7, _menuSelection);
}
-// ======================= Pause Overlay =======================
-// Emulates FUN_405A21 pause rendering (lines 242-305)
-// Creates a dimmed overlay effect and displays "PAUSED" text
+// ---------------------------------------------------------------------------
+// Pause Overlay
+// ---------------------------------------------------------------------------
+
+// showPauseOverlay -- Dimmed overlay with "PAUSED" text (FUN_405A21).
void InsaneRebel2::showPauseOverlay() {
SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
if (!splayer) {
@@ -728,13 +717,9 @@ void InsaneRebel2::showPauseOverlay() {
debug("showPauseOverlay: Overlay displayed");
}
+// runMainMenu -- Main menu loop (FUN_004147B2).
+// Returns kMenuNewGame, kMenuContinue, kMenuCredits, or 0 (quit).
int InsaneRebel2::runMainMenu() {
- // Main menu loop - emulates FUN_004147B2
- // Returns:
- // kMenuNewGame (2) = Start new game
- // kMenuContinue (4) = Continue game (level select)
- // kMenuCredits (1) = Show credits then return to menu
- // 0 = Quit game
debug("Rebel2: Entering main menu");
@@ -863,16 +848,13 @@ int InsaneRebel2::runMainMenu() {
return 0;
}
-// ==================== Chapter Selection Screen ====================
-// Emulates FUN_00415CF8 - Chapter selection with preview and password input
-// This is the actual level/chapter selection that players see after pilot select
+// ---------------------------------------------------------------------------
+// Chapter Selection Screen
+// ---------------------------------------------------------------------------
+// runChapterSelect -- Chapter selection with preview (FUN_00415CF8).
+// Returns kChapterSelectPlay, kChapterSelectBack, or kChapterSelectQuit.
int InsaneRebel2::runChapterSelect() {
- // Chapter selection screen loop - emulates FUN_00415CF8
- // Returns:
- // kChapterSelectPlay (5) = Play selected chapter
- // kChapterSelectBack (2) = Return to main menu (ESC or BACK)
- // kChapterSelectQuit (0) = Quit game
debug("Rebel2: Entering chapter selection (FUN_00415CF8)");
@@ -1445,16 +1427,14 @@ void InsaneRebel2::drawChapterSelectOverlay(byte *renderBitmap, int pitch, int w
drawChapterInfoLine(renderBitmap, pitch, width, height);
}
-// ==================== Pilot Selection Menu (FUN_00414A41) ====================
-// Emulates FUN_00414A41 - Pilot/save selection menu
-// This appears before chapter selection. All options go to chapter selection except MAIN MENU.
+// ---------------------------------------------------------------------------
+// Pilot Selection Menu (FUN_00414A41)
+// ---------------------------------------------------------------------------
+// Pilot/save selection before chapter selection.
+// runLevelSelect -- Pilot selection menu (FUN_00414A41).
+// Returns kLevelSelectPlay, kLevelSelectBack, or kLevelSelectQuit.
int InsaneRebel2::runLevelSelect() {
- // Pilot selection menu loop - emulates FUN_00414A41
- // Returns:
- // kLevelSelectPlay (1) = Go to chapter selection (pilot selected or NEW+difficulty chosen)
- // kLevelSelectBack (0) = Return to main menu (MAIN MENU or ESC)
- // kLevelSelectQuit (2) = Quit game
debug("Rebel2: Entering pilot selection (FUN_00414A41), %d pilots loaded", _numPilots);
@@ -1800,38 +1780,14 @@ void InsaneRebel2::drawLevelSelectOverlay(byte *renderBitmap, int pitch, int wid
drawMenuItems(renderBitmap, pitch, width, height, pilotItems, _numPilots + 4, _levelSelection);
}
-// ==================== Top Pilots Screen (FUN_00420116) ====================
-// Displays ranked pilot scores with animated reveal over a menu background video.
-//
-// Original FUN_00420116 at 0x420116:
-// - Plays random menu video as background (FUN_0041fdc8)
-// - Draws title from TRS string 0x96 (150) centered at (152, 10) in low-res
-// - Iterates through ranking table (DAT_00443b58, 0x4a-byte records, max 15)
-// - Each row shows: rank medals, pilot name, difficulty, chapter, total score
-// - Rows appear one per frame (animated reveal up to local_10)
-// - param_1 == -2: 240 frame loop (from options); else 120 frames (from main menu)
-// - Exits on mouse click (DAT_0047a7e4 & 1) or any keypress (DAT_0047a7e8 != 0)
-//
-// Ranking table record (0x4a = 74 bytes, from FUN_00410271):
-// +0x00 (4): timestamp
-// +0x04 (40): pilot name (or "-----" for defaults)
-// +0x36 (4): total score
-// +0x3a (4): total rating
-// +0x3e (2): difficulty tier (1-3) â TRS lookup: value + 0x9b (155)
-// +0x40 (2): highest chapter completed (1-15)
-//
-// Column X positions (low-res 320x200):
-// Rank medals: 43 (0x2b) - FUN_004341a0, alignment=1
-// Name: 88 (0x58) - FUN_00434cb0, alignment=0 (left), "^f01%s"
-// Difficulty: 195 (0xc3) - FUN_00434cb0, alignment=1, "^f01%s" (TRS)
-// Chapter: 245 (0xf5) - FUN_00434cb0, alignment=1, "^f01%hd"
-// Score: 295 (0x127) - FUN_00434cb0, alignment=2, "^f01%ld"
-// Row Y: sVar1 * 10 + 42 (0x2a)
-
-// Initialize ranking table with 15 default entries (FUN_0040FF00)
-// Called when no ranking save file exists. Generates placeholder entries with:
-// name = "-----", score = (15-i)*1500, rating = (15-i)*2,
-// difficulty = ((15-i)*3+14)/15, chapter = ((15-i)*15+14)/15
+// ---------------------------------------------------------------------------
+// Top Pilots Screen (FUN_00420116)
+// ---------------------------------------------------------------------------
+// Ranked pilot scores with animated reveal over a menu background video.
+// 0x4a-byte records (max 15): name, score, rating, difficulty, chapter.
+// Column X positions (low-res): medals=43, name=88, diff=195, ch=245, score=295.
+
+// initDefaultRankings -- Fill ranking table with defaults (FUN_0040FF00).
void InsaneRebel2::initDefaultRankings() {
_numRankings = 0;
memset(_rankings, 0, sizeof(_rankings));
@@ -1847,8 +1803,7 @@ void InsaneRebel2::initDefaultRankings() {
}
}
-// Insert a new entry into the sorted ranking table (FUN_00410271)
-// Maintains descending score order, max kMaxRankings entries
+// insertRanking -- Insert into sorted ranking table (FUN_00410271).
void InsaneRebel2::insertRanking(const char *name, int32 score, int32 rating,
int16 difficulty, int16 chapter) {
if (score == 0)
@@ -1991,23 +1946,13 @@ void InsaneRebel2::drawTopPilotsOverlay(byte *renderBitmap, int pitch, int width
_topPilotsFrameCount++;
}
-// ======================= Options Menu (FUN_004167A6) =======================
-// Original: FUN_00416787 calls FUN_0041fdc8 (init video) + FUN_004167a6(1)
-// FUN_004167a6 runs a loop over FUN_0041f5ae (drawMenuItems) with dynamic
-// toggle labels and slider items. Settings stored at DAT_00482e20[0..3]
-// (get/set via FUN_00425d30/FUN_00425d40) and DAT_0047a7fe, DAT_0047a80a.
-//
-// Menu items for keyboard mode (9 selectable):
-// [0] Title: TRS 89 "Game Options"
-// [1] Music: TRS 90/91
-// [2] SFX: TRS 92/93
-// [3] Voices: TRS 94/95
-// [4] Text: TRS 96/97
-// [5] Controls: TRS 98/99
-// [6] Rapid Fire: TRS 100/101
-// [7] Volume: TRS 103 "Volume Level: %hd%%" (slider, left/right ±4)
-// [8] Back: TRS 107 "Return to Main Menu"
+// ---------------------------------------------------------------------------
+// Options Menu (FUN_004167A6)
+// ---------------------------------------------------------------------------
+// Toggle labels and slider items. Settings at DAT_00482e20[0..3].
+// Menu items: Music, SFX, Voices, Text, Controls, Rapid Fire, Volume, Back.
+// showOptionsMenu -- Options menu loop (FUN_00416787).
void InsaneRebel2::showOptionsMenu() {
debug("Rebel2: Showing Options menu (FUN_00416787)");
diff --git a/engines/scumm/insane/insane_rebel_render.cpp b/engines/scumm/insane/insane_rebel_render.cpp
index 71d94ff7f2c..e12ab1cd340 100644
--- a/engines/scumm/insane/insane_rebel_render.cpp
+++ b/engines/scumm/insane/insane_rebel_render.cpp
@@ -41,9 +41,12 @@ extern void smushDecodeRLEOpaque(byte *dst, const byte *src, int left, int top,
extern void smushDecodeUncompressed(byte *dst, const byte *src, int left, int top, int width, int height, int pitch);
-// ======================= Embedded Frame Codec Decoders =======================
-// These implement the retail codec functions FUN_0042BD60, FUN_0042BBF0, FUN_0042B5F0
+// ---------------------------------------------------------------------------
+// Embedded Frame Codec Decoders
+// ---------------------------------------------------------------------------
+// Retail codec functions FUN_0042BD60, FUN_0042BBF0, FUN_0042B5F0.
+// decodeCodec21 -- Codec 21/44: line update codec (FUN_0042BD60).
void InsaneRebel2::decodeCodec21(byte *dst, const byte *src, int width, int height) {
// Codec 21/44: Line Update codec (FUN_0042BD60)
// Format: each line has 2-byte size header, then pairs of (skip, count+1, literal_bytes)
@@ -71,6 +74,7 @@ void InsaneRebel2::decodeCodec21(byte *dst, const byte *src, int width, int heig
}
}
+// decodeCodec23 -- Codec 23: skip/copy with embedded RLE (FUN_0042BBF0).
void InsaneRebel2::decodeCodec23(byte *dst, const byte *src, int width, int height, int dataSize) {
// Codec 23: Skip/Copy with embedded RLE (FUN_0042BBF0)
// Format: each line has 2-byte size, then pairs of (skip, runSize, RLE_data)
@@ -120,6 +124,7 @@ void InsaneRebel2::decodeCodec23(byte *dst, const byte *src, int width, int heig
}
}
+// decodeCodec45 -- Codec 45: RA2-specific BOMP RLE with variable header (FUN_0042B5F0).
void InsaneRebel2::decodeCodec45(byte *dst, const byte *src, int width, int height, int dataSize) {
// Codec 45: RA2-specific BOMP RLE with variable header (FUN_0042B5F0)
// May have a 6-byte sub-header starting with "01 FE"
@@ -246,6 +251,7 @@ void InsaneRebel2::decodeCodec45(byte *dst, const byte *src, int width, int heig
width, height, nonZero, (nonZero * 100) / (width * height));
}
+// renderEmbeddedFrame -- Blit a decoded embedded frame to the video buffer.
void InsaneRebel2::renderEmbeddedFrame(byte *renderBitmap, const EmbeddedSanFrame &frame, int userId) {
// Render the decoded embedded frame to the video buffer
// Skip immediate draw for handlers that render HUD during post-processing:
@@ -292,6 +298,12 @@ void InsaneRebel2::renderEmbeddedFrame(byte *renderBitmap, const EmbeddedSanFram
debug("Rebel2: Rendered embedded HUD %d at (%d,%d)", userId, frame.renderX, frame.renderY);
}
+//
+// loadEmbeddedSan -- Decode an embedded SAN (ANIM/FOBJ) from IACT opcode 8 data.
+//
+// Parses ANIM container, extracts FOBJ codec data, decodes using codec 21/23/45,
+// and stores the result in _rebelEmbeddedHud[userId] for later rendering.
+//
void InsaneRebel2::loadEmbeddedSan(int userId, byte *animData, int32 size, byte *renderBitmap) {
// Validate userId - Level 3 uses slots 0-11, allow up to 15 for safety
if (userId < 0 || userId > 15 || !animData || size < 32) {
@@ -456,13 +468,9 @@ end_parsing:;
}
// Spawn explosion into the shared 5-slot system.
-// In the original, each handler has its own spawn logic inside its enemy processing function:
-// Handler 0x26: FUN_40A2E0 (0x40A2E0) â spawns in slot arrays DAT_0044368e[]
-// Handler 8: FUN_4028C5 (0x4028C5) â spawns in slot arrays DAT_0043f854[]
-// Handler 7: FUN_40F628 (0x40F628) â spawns in slot arrays DAT_00443770[]
-// Handler 25: FUN_41E7C2 (0x41E7C2) â spawns in slot arrays DAT_0045792c[]
-// All share the same logic: find first free slot (counter==0), set counter=10,
-// scale=objectHalfWidth, position=enemy center, velocity=0.
+// spawnExplosion -- Allocate an explosion slot at the given position.
+// Per-handler FUN_40A2E0/FUN_4028C5/FUN_40F628/FUN_41E7C2: find first free
+// slot (counter==0), set counter=10, scale=objectHalfWidth, position=center.
void InsaneRebel2::spawnExplosion(int x, int y, int objectHalfWidth) {
for (int i = 0; i < 5; i++) {
if (!_explosions[i].active || _explosions[i].counter <= 0) {
@@ -476,10 +484,7 @@ void InsaneRebel2::spawnExplosion(int x, int y, int objectHalfWidth) {
}
}
-// Get max shot duration from level table (DAT_0047e0f0 indexed by DAT_0047a7fa/DAT_0047a7f8)
-// The original reads from a per-level per-difficulty table. The field at offset +0x00
-// (DAT_0047e0f0) is the first field of each record â our laserDelay field.
-// This value is used both as the initial shot counter AND as maxFrames for beam rendering.
+// getShotMaxDuration -- Shot duration from per-level difficulty table (DAT_0047e0f0).
int16 InsaneRebel2::getShotMaxDuration() {
LevelDifficultyParams params = getDifficultyParams();
// laserDelay = DAT_0047e0f0 field: shot duration in frames
@@ -490,7 +495,7 @@ int16 InsaneRebel2::getShotMaxDuration() {
return duration;
}
-// Dispatcher - calls appropriate spawn function based on current handler
+// spawnShot -- Dispatch to per-handler shot spawn.
void InsaneRebel2::spawnShot(int x, int y) {
switch (_rebelHandler) {
case 0x26: // Turret
@@ -520,7 +525,7 @@ void InsaneRebel2::spawnShot(int x, int y) {
}
}
-// Handler 0x26 Turret shot spawn (based on FUN_4089AB lines 127-140)
+// spawnTurretShot -- Handler 0x26 turret shot spawn (FUN_4089AB).
void InsaneRebel2::spawnTurretShot(int x, int y) {
for (int i = 0; i < 2; i++) {
if (_turretShots[i].counter == 0) {
@@ -538,7 +543,7 @@ void InsaneRebel2::spawnTurretShot(int x, int y) {
}
}
-// Handler 8 Vehicle shot spawn (based on FUN_401CCF lines 65-69)
+// spawnVehicleShot -- Handler 8 vehicle shot spawn (FUN_401CCF).
void InsaneRebel2::spawnVehicleShot(int x, int y) {
for (int i = 0; i < 2; i++) {
if (_vehicleShots[i].counter == 0) {
@@ -552,11 +557,8 @@ void InsaneRebel2::spawnVehicleShot(int x, int y) {
}
}
-// Handler 25 on-foot shot spawn (based on FUN_0041db5e lines 170-190)
-// Gun position computed from GRD002 character sprite.
-// Original stores: DAT_0045791c[i] = gunOffsetTable[spriteIdx] + DAT_00457910 - DAT_0045790c
-// DAT_00457920[i] = gunYTable[spriteIdx] + DAT_00457912 - DAT_0045790e
-// Render adds view offset back, so screen gun = table[idx] + spriteOffset.
+// spawnHandler25Shot -- Handler 25 on-foot shot spawn (FUN_0041db5e).
+// Gun position computed from GRD002 character sprite offset tables.
void InsaneRebel2::spawnHandler25Shot(int x, int y) {
// Handler 25 can only shoot when uncovered (damage == 0)
if (_rebelDamageLevel != 0) {
@@ -640,7 +642,7 @@ void InsaneRebel2::spawnHandler25Shot(int x, int y) {
}
}
-// Handler 7 Space combat shot spawn (based on FUN_40D836 lines 146-166)
+// spawnSpaceShot -- Handler 7 space combat shot spawn (FUN_40D836).
void InsaneRebel2::spawnSpaceShot(int x, int y) {
for (int i = 0; i < 2; i++) {
if (_spaceShots[i].counter == 0) {
@@ -669,6 +671,7 @@ void InsaneRebel2::spawnSpaceShot(int x, int y) {
}
}
+// drawTexturedLine -- Draw a line segment textured from a NUT sprite row.
void InsaneRebel2::drawTexturedLine(byte *dst, int pitch, int width, int height, int x0, int y0, int x1, int y1, NutRenderer *nut, int spriteIdx, int v, bool mask231) {
if (!nut || spriteIdx >= nut->getNumChars())
return;
@@ -720,8 +723,8 @@ void InsaneRebel2::drawTexturedLine(byte *dst, int pitch, int width, int height,
}
}
-// Helper: draw a textured segment between two points using the game's original routine (FUN_00429360 port)
-void drawTexturedSegment(byte *dst, int pitch, int width, int height,
+// drawTexturedSegment -- Textured segment between two points (FUN_00429360 port).
+static void drawTexturedSegment(byte *dst, int pitch, int width, int height,
int param_3, int param_4, int param_5, int param_6, int param_7, const byte *param_8,
int clipLeft, int clipTop, int clipRight, int clipBottom) {
// Near-direct port of FUN_00429360.
@@ -1063,8 +1066,7 @@ void drawTexturedSegment(byte *dst, int pitch, int width, int height,
}
-// Initialize laser texture buffer from NUT sprite (FUN_0040BAB0)
-// This pre-renders a sprite into a buffer that drawLaserBeam uses
+// initLaserTexture -- Pre-render a NUT sprite into the laser texture buffer (FUN_0040BAB0).
void InsaneRebel2::initLaserTexture(NutRenderer *nut, int spriteIdx) {
if (!nut || spriteIdx >= nut->getNumChars())
return;
@@ -1148,7 +1150,7 @@ void InsaneRebel2::initLaserTexture(NutRenderer *nut, int spriteIdx) {
}
}
-// Free laser texture buffer (FUN_0040BBD1)
+// freeLaserTexture -- Emulates FUN_0040BBD1.
void InsaneRebel2::freeLaserTexture() {
free(_laserTexture.pixels);
_laserTexture.pixels = nullptr;
@@ -1156,11 +1158,14 @@ void InsaneRebel2::freeLaserTexture() {
_laserTexture.height = 0;
}
-// Initialize edge blend tables (FUN_410510)
+//
+// initEdgeTable -- Initialize edge blend tables (FUN_410510).
+//
// When data is nullptr, fills with default tables:
// _edgeTable[a*256+b] = min(a,b) (symmetric identity blend)
// _edgeTableAlt[a*256+b] = special blend for hi-res mode
// When data is non-null, loads the primary table from data+8 (upper triangle, symmetric).
+//
void InsaneRebel2::initEdgeTable(const byte *data) {
if (data == nullptr) {
// Default table initialization (FUN_410510 param_1==NULL path, lines 12-36)
@@ -1204,7 +1209,8 @@ void InsaneRebel2::initEdgeTable(const byte *data) {
}
}
-// Draw edge highlight line using the edge blend table (FUN_410962)
+//
+// drawEdgeHighlightLine -- Edge highlight line using the blend table (FUN_410962).
// For each pixel along the line, reads the two adjacent pixels perpendicular to
// the line direction and uses _edgeTable[above*256+below] as the output color.
// This creates a glow/blend effect at beam edges.
@@ -1448,8 +1454,9 @@ void InsaneRebel2::drawEdgeHighlightLine(byte *dst, int pitch, int width, int he
}
}
-// Draw laser beam using pre-initialized texture (FUN_0040BBF6)
-// This is a direct port of the assembly function.
+//
+// drawLaserBeam -- Laser beam using pre-initialized texture (FUN_0040BBF6).
+//
// Two-layer rendering:
// Layer 1: Textured scanlines (beam body) via drawTexturedSegment()
// Layer 2: Edge highlights (glow) via drawEdgeHighlightLine(), gated by _rebelDetailMode >= 0
@@ -1599,12 +1606,15 @@ void InsaneRebel2::drawLaserBeam(byte *dst, int pitch, int width, int height,
}
}
-// ============================================================
-// COLLISION ZONE SYSTEM (for Level 3 pilot ship obstacle avoidance)
-// ============================================================
-// Based on FUN_40E35E, FUN_40C3CC disassembly from info.md
-// Zones are quadrilaterals registered via IACT opcode 5
+// ---------------------------------------------------------------------------
+// Collision Zone System
+// ---------------------------------------------------------------------------
+// Level 3 pilot ship obstacle avoidance. Zones are quadrilaterals
+// registered via IACT opcode 5 (FUN_40E35E, FUN_40C3CC).
+//
+// registerCollisionZone -- Register a quad zone from IACT opcode 5 (FUN_4092D9 / FUN_4033CF).
+//
void InsaneRebel2::registerCollisionZone(Common::SeekableReadStream &b, int16 subOpcode, int16 par4) {
// IACT Opcode 5 data layout â corrected from FUN_4033CF / FUN_4092D9 analysis:
//
@@ -1671,6 +1681,7 @@ void InsaneRebel2::registerCollisionZone(Common::SeekableReadStream &b, int16 su
}
}
+// resetCollisionZones -- Clear zone tables at end of frame (FUN_403240).
void InsaneRebel2::resetCollisionZones() {
// Reset zone counters at end of frame (FUN_403240 equivalent)
// This clears the zone tables so they can be rebuilt from the next frame's IACT chunks
@@ -1678,6 +1689,9 @@ void InsaneRebel2::resetCollisionZones() {
_secondaryZoneCount = 0;
}
+//
+// checkCollisionZones -- Per-frame collision test against primary zones (FUN_4092D9).
+//
void InsaneRebel2::checkCollisionZones() {
// Per-frame collision checking â FUN_4092D9 first loop (lines 39-202).
// Tests aim/ship position against primary collision zone quadrilaterals.
@@ -1800,6 +1814,12 @@ void InsaneRebel2::checkCollisionZones() {
}
}
+//
+// checkHandler7CollisionZones -- Handler 7 per-frame collision (FUN_40E35E).
+//
+// Two modes: obstacle collision (secondary zones) and wall/boundary
+// collision (primary zones with per-edge push-back).
+//
void InsaneRebel2::checkHandler7CollisionZones(byte *renderBitmap, int pitch, int width, int height, int32 curFrame) {
// FUN_40E35E â Handler 7 per-frame collision system.
// Uses ship position (_flyShipScreenX/_flyShipScreenY) in raw buffer coords.
@@ -2055,10 +2075,12 @@ void InsaneRebel2::checkHandler7CollisionZones(byte *renderBitmap, int pitch, in
}
}
+// renderNutSprite -- Draw a NUT sprite with transparency.
void InsaneRebel2::renderNutSprite(byte *dst, int pitch, int width, int height, int x, int y, NutRenderer *nut, int spriteIdx) {
renderNutSpriteMirrored(dst, pitch, width, height, x, y, nut, spriteIdx, false);
}
+// renderNutSpriteClipped -- Draw a NUT sprite with explicit clip rectangle.
static void renderNutSpriteClipped(byte *dst, int pitch, int dstH,
int clipLeft, int clipTop, int clipRight, int clipBottom,
int x, int y, NutRenderer *nut, int spriteIdx) {
@@ -2111,8 +2133,7 @@ static void renderNutSpriteClipped(byte *dst, int pitch, int dstH,
}
}
-// Render NUT sprite with optional horizontal mirroring
-// Based on FUN_004236e0 disassembly - flags=0x2001 triggers horizontal flip
+// renderNutSpriteMirrored -- NUT sprite with optional horizontal flip (FUN_004236e0).
void InsaneRebel2::renderNutSpriteMirrored(byte *dst, int pitch, int width, int height, int x, int y, NutRenderer *nut, int spriteIdx, bool mirror) {
if (!nut || spriteIdx < 0 || spriteIdx >= nut->getNumChars())
return;
@@ -2170,6 +2191,12 @@ void InsaneRebel2::renderNutSpriteMirrored(byte *dst, int pitch, int width, int
}
}
+//
+// procPostRendering -- Post-frame rendering: HUD, ships, enemies, effects, status bar.
+//
+// Called after FOBJ decoding. Dispatches to per-handler rendering functions
+// for ship sprites, laser shots, explosions, crosshair, and damage effects.
+//
void InsaneRebel2::procPostRendering(byte *renderBitmap, int32 codecparam, int32 setupsan12,
int32 setupsan13, int32 curFrame, int32 maxFrame) {
@@ -2500,19 +2527,18 @@ void InsaneRebel2::procPostRendering(byte *renderBitmap, int32 codecparam, int32
frameEndCleanup();
}
-// ======================= Damage Visual Effect Functions =======================
+// ---------------------------------------------------------------------------
+// Damage Visual Effect Functions
+// ---------------------------------------------------------------------------
// Palette flash + screen shake when the player takes damage.
-// Original retail functions: FUN_420515, FUN_420562, FUN_420754, FUN_42073B, FUN_420501
+// FUN_420515, FUN_420562, FUN_420754, FUN_42073B, FUN_420501.
-// FUN_00420501 - Reset palette flash counter.
-// Called at level start / scene transitions to clear any in-progress flash.
+// resetDamageFlash -- Reset palette flash counter (FUN_00420501).
void InsaneRebel2::resetDamageFlash() {
_damageFlashCounter = 0;
}
-// FUN_00420515 - Save current palette and initiate a 5-frame flash.
-// If a flash is already in progress, just resets the counter to 5
-// (the palette was already saved on the first hit).
+// initDamageFlash -- Save palette and initiate 5-frame flash (FUN_00420515).
void InsaneRebel2::initDamageFlash() {
if (_damageFlashCounter == 0) {
// Save current SMUSH palette before modifying it
@@ -2521,27 +2547,18 @@ void InsaneRebel2::initDamageFlash() {
_damageFlashCounter = 5;
}
-// FUN_0042073B - Trigger both palette flash and screen shake.
-// Called from the damage hit handler when the player takes damage.
+// triggerDamageEffect -- Trigger palette flash and screen shake (FUN_0042073B).
void InsaneRebel2::triggerDamageEffect() {
initDamageFlash();
_damageShakeCounter = 10;
}
-// FUN_00420562 - Per-frame palette modification.
//
-// Two modes determined by _damageHighFlashCounter:
+// updateDamageFlashPalette -- Per-frame palette modification (FUN_00420562).
//
-// Normal hit flash (_damageHighFlashCounter == 0 or odd):
-// Decrements _damageFlashCounter. On even counter values, all 768 palette
-// bytes (RGB) are blended from inverted toward the saved original:
-// output[i] = 0xFF - ((0xFF - saved[i]) * (0x10 - counter)) >> 4
-// Counter 5â4(apply)â3(skip)â2(apply)â1(skip)â0(apply=original). The
-// alternating apply/skip creates a strobe-like flash effect.
+// Normal hit flash: alternating inverted-to-original blend on even counter.
+// High-damage red pulse: R-channel only modification at max damage.
//
-// High-damage red pulse (_playerDamage >= 0xFF, even counter):
-// Only the R channel (every 3rd byte) is modified using the same formula
-// with _damageHighFlashCounter. Creates a pulsing red tint overlay.
void InsaneRebel2::updateDamageFlashPalette() {
// High-damage mode: persistent red pulsing when damage is maxed out
if (_playerDamage < 0xFF) {
@@ -2584,14 +2601,8 @@ void InsaneRebel2::updateDamageFlashPalette() {
}
}
-// FUN_00420754 - Per-frame screen shake + palette flash.
-//
-// Screen shake randomly shifts scanlines left or right for visual distortion.
-// The number of affected scanlines decreases each frame (counter * 5),
-// creating a diminishing shake effect over 10 frames.
-//
-// Called every frame from procPostRendering when not in cutscene modes
-// (shipLevelMode != 4 and != 5, matching original: DAT_0043e000 != 4 && != 5).
+// updateDamageEffect -- Per-frame screen shake + palette flash (FUN_00420754).
+// Randomly shifts scanlines for visual distortion, diminishing over 10 frames.
void InsaneRebel2::updateDamageEffect(byte *renderBitmap, int pitch, int width, int height) {
if (_damageShakeCounter != 0) {
_damageShakeCounter--;
@@ -2637,9 +2648,12 @@ void InsaneRebel2::updateDamageEffect(byte *renderBitmap, int pitch, int width,
updateDamageFlashPalette();
}
-// ======================= Rendering Helper Functions =======================
-// These are extracted from procPostRendering for better readability
+// ---------------------------------------------------------------------------
+// Rendering Helper Functions
+// ---------------------------------------------------------------------------
+// Extracted from procPostRendering for better readability.
+// renderTextOverlay -- Progressive chapter title overlay (FUN_004171c5).
void InsaneRebel2::renderTextOverlay(byte *renderBitmap, int pitch, int width, int height, int curFrame) {
// Emulates FUN_004171c5 text overlay: progressive chapter title during [fadeIn, fadeOut)
if (curFrame < _textOverlayFadeIn || curFrame >= _textOverlayFadeOut)
@@ -2774,6 +2788,7 @@ void InsaneRebel2::renderTextOverlay(byte *renderBitmap, int pitch, int width, i
}
}
+// renderStatusBarBackground -- Fill status bar area with color 4 (FUN_004288c0).
void InsaneRebel2::renderStatusBarBackground(byte *renderBitmap, int pitch, int width, int height,
int videoWidth, int videoHeight, int statusBarY) {
// Fill status bar background (FUN_004288c0 equivalent)
@@ -2794,6 +2809,7 @@ void InsaneRebel2::renderStatusBarBackground(byte *renderBitmap, int pitch, int
}
}
+// renderTurretHudOverlays -- NUT-based HUD for Handler 0x26/0x19 (FUN_004089ab).
void InsaneRebel2::renderTurretHudOverlays(byte *renderBitmap, int pitch, int width, int height, int32 curFrame) {
// Draw NUT-based HUD overlays for Handler 0x26/0x19 (turret modes)
// From FUN_004089ab disassembly (lines 195-226):
@@ -2861,6 +2877,7 @@ void InsaneRebel2::renderTurretHudOverlays(byte *renderBitmap, int pitch, int wi
}
}
+// renderEmbeddedHudOverlays -- Draw embedded SAN HUD overlays from IACT chunks.
void InsaneRebel2::renderEmbeddedHudOverlays(byte *renderBitmap, int pitch, int width, int height) {
// Draw embedded SAN HUD overlays (from IACT chunks)
// For Handler 7 (Level 3): HUD elements are scattered across the screen
@@ -2974,6 +2991,7 @@ void InsaneRebel2::renderEmbeddedHudOverlays(byte *renderBitmap, int pitch, int
}
}
+// renderStatusBarSprites -- DISPFONT.NUT status bar rendering (FUN_0041c012).
void InsaneRebel2::renderStatusBarSprites(byte *renderBitmap, int pitch, int width, int height,
int statusBarY, int32 curFrame) {
// FUN_0041c012 equivalent â renders DISPFONT.NUT status bar sprites.
@@ -3113,6 +3131,7 @@ void InsaneRebel2::renderStatusBarSprites(byte *renderBitmap, int pitch, int wid
}
}
+// renderHandler7Ship -- Handler 7 third-person ship rendering (FUN_0040d836).
void InsaneRebel2::renderHandler7Ship(byte *renderBitmap, int pitch, int width, int height) {
// Handler 7 Ship Rendering (Third-Person Ship - FLY sprites)
// Based on FUN_0040d836 lines 173-185:
@@ -3210,6 +3229,7 @@ void InsaneRebel2::renderHandler7Ship(byte *renderBitmap, int pitch, int width,
_perspectiveX, _perspectiveY, _smoothedVelocity, _verticalInput, _flyEffectAnimCounter, _flyOverlayRepeatCount);
}
+// renderHandler8Ship -- Handler 8 third-person on-foot rendering (FUN_00401CCF).
void InsaneRebel2::renderHandler8Ship(byte *renderBitmap, int pitch, int width, int height) {
// Handler 8 Ship Rendering (Third-Person On Foot - POV sprites)
// Uses _shipSprite (POV001) with position-based offset
@@ -3292,12 +3312,9 @@ void InsaneRebel2::renderHandler8Ship(byte *renderBitmap, int pitch, int width,
}
// Handler 25: Draw GRD001 (wall/cockpit overlay) in procPostRendering.
-// Per original FUN_0041DB5E, GRD sprites are drawn AFTER FOBJ enemies, before GRD002.
-//
-// From FUN_0041db5e disassembly (lines 202-221):
-// - Mode 1 with damage==0: Width halved (left half only, pixels 0-159)
-// - Mode 4 with damage==0: Width halved AND buffer offset (right half only, pixels 160-319)
-// - All other cases: Full width (320 pixels)
+// renderHandler25ShipPre -- Handler 25 GRD001 pre-rendering (FUN_0041DB5E).
+// GRD sprites are drawn AFTER FOBJ enemies, before GRD002. Mode-based
+// half-width clipping for cover/uncover transitions.
void InsaneRebel2::renderHandler25ShipPre(byte *renderBitmap, int pitch, int width, int height) {
if (_rebelHandler != 25)
return;
@@ -3372,6 +3389,7 @@ void InsaneRebel2::renderHandler25ShipPre(byte *renderBitmap, int pitch, int wid
}
}
+// renderHandler25Ship -- Handler 25 GRD002 post-rendering (FUN_0041db5e).
void InsaneRebel2::renderHandler25Ship(byte *renderBitmap, int pitch, int width, int height) {
// Handler 25 POST-rendering: Draw GRD002 (character sprite) on top of enemies.
// GRD001 (wall/cockpit) is drawn before this via renderHandler25ShipPre().
@@ -3513,6 +3531,7 @@ void InsaneRebel2::renderHandler25Ship(byte *renderBitmap, int pitch, int width,
}
}
+// renderFallbackShip -- Fallback ship rendering using embedded HUD frame.
void InsaneRebel2::renderFallbackShip(byte *renderBitmap, int pitch, int width, int height) {
// Fallback: Use embedded HUD frame as ship sprite (Level 3 style)
// userId=11 contains the ship sprite strip
@@ -3579,6 +3598,7 @@ void InsaneRebel2::renderFallbackShip(byte *renderBitmap, int pitch, int width,
drawX, drawY, srcX, srcY, numHorizontal, numVertical, _shipDirectionH, _shipDirectionV);
}
+// renderEnemyOverlays -- Handler 0x26 target indicator sprites (FUN_40A2E0).
void InsaneRebel2::renderEnemyOverlays(byte *renderBitmap, int pitch, int width, int height, int videoWidth) {
// Original per-enemy target indicator behavior comes from FUN_40A2E0 (handler 0x26):
// - Draws cockpit icon sprites 6..10 at enemy centers.
@@ -3648,11 +3668,7 @@ void InsaneRebel2::renderEnemyOverlays(byte *renderBitmap, int pitch, int width,
}
}
-// Dispatcher â calls per-handler explosion render function.
-// Original code has separate functions per handler, each with its own
-// position transformation, scale thresholds, and secondary NUT rendering.
-// Each handler's render function checks DAT_0047e108 flags & 1:
-// when bit 0 is set, explosion NUT sprites are suppressed (counter still ticks).
+// renderExplosions -- Dispatch to per-handler explosion renderer.
void InsaneRebel2::renderExplosions(byte *renderBitmap, int pitch, int width, int height) {
// Check flags bit 0: suppress explosion sprite rendering
LevelDifficultyParams dparams = getDifficultyParams();
@@ -3688,11 +3704,7 @@ void InsaneRebel2::renderExplosions(byte *renderBitmap, int pitch, int width, in
}
}
-// FUN_409FBC â Handler 0x26 (Turret/Cockpit) explosion rendering.
-// Position: Uses FUN_0041c720 for 3Dâ2D projection. At low-res, world coords â screen coords.
-// Scale thresholds: Fixed (<11, <21).
-// Secondary NUT: DAT_0047fe80 (rendered if DAT_0047a7fc >= 0).
-// Hi-res: Coordinates doubled when DAT_0047a808 >= 2.
+// renderTurretExplosions -- Handler 0x26 turret explosion rendering (FUN_409FBC).
void InsaneRebel2::renderTurretExplosions(byte *renderBitmap, int pitch, int width, int height) {
if (!_smush_iconsNut)
return;
@@ -3734,10 +3746,7 @@ void InsaneRebel2::renderTurretExplosions(byte *renderBitmap, int pitch, int wid
}
}
-// FUN_402696 â Handler 8 (Third-Person On-Foot) explosion rendering.
-// Position: World coords minus camera offset (DAT_0043e006/DAT_0043e008 = _viewX/_viewY).
-// Scale thresholds: Fixed (<11, <21) â same as handler 0x26.
-// Secondary NUT: None (only DAT_0047a828).
+// renderVehicleExplosions -- Handler 8 on-foot explosion rendering (FUN_402696).
void InsaneRebel2::renderVehicleExplosions(byte *renderBitmap, int pitch, int width, int height) {
if (!_smush_iconsNut)
return;
@@ -3778,10 +3787,7 @@ void InsaneRebel2::renderVehicleExplosions(byte *renderBitmap, int pitch, int wi
}
}
-// FUN_40F1C5 â Handler 7 (Third-Person Ship) explosion rendering.
-// Position: Uses FUN_0041c720 for 3Dâ2D projection.
-// Scale thresholds: Resolution-dependent (low-res: <11/<21, high-res: <21/<41).
-// Secondary NUT: DAT_0047ff00 (FLY004, rendered if DAT_0047a7fc >= 0).
+// renderSpaceExplosions -- Handler 7 space explosion rendering (FUN_40F1C5).
void InsaneRebel2::renderSpaceExplosions(byte *renderBitmap, int pitch, int width, int height) {
if (!_smush_iconsNut)
return;
@@ -3880,11 +3886,7 @@ void InsaneRebel2::renderSpaceExplosions(byte *renderBitmap, int pitch, int widt
}
}
-// FUN_41F29A â Handler 25 (FPS/Mixed) explosion rendering.
-// Position: World coords + view offset (DAT_0045790c/DAT_0045790e = _rebelViewOffsetX/_rebelViewOffsetY).
-// Scale thresholds: Resolution-dependent (same formula as Handler 7).
-// Secondary NUT: DAT_00482260 (hi-res HUD alternative, rendered if DAT_0047a7fc >= 0).
-// Note: No per-frame sound panning update (unlike handlers 0x26, 8, 7).
+// renderHandler25Explosions -- Handler 25 FPS explosion rendering (FUN_41F29A).
void InsaneRebel2::renderHandler25Explosions(byte *renderBitmap, int pitch, int width, int height) {
if (!_smush_iconsNut)
return;
@@ -3926,7 +3928,7 @@ void InsaneRebel2::renderHandler25Explosions(byte *renderBitmap, int pitch, int
}
}
-// Dispatcher - calls appropriate render function based on current handler
+// renderLaserShots -- Dispatch to per-handler laser renderer.
void InsaneRebel2::renderLaserShots(byte *renderBitmap, int pitch, int width, int height) {
switch (_rebelHandler) {
case 0x26: // Turret - FUN_40AD63
@@ -3947,8 +3949,7 @@ void InsaneRebel2::renderLaserShots(byte *renderBitmap, int pitch, int width, in
}
}
-// Handler 0x26 Turret laser rendering (FUN_40AD63)
-// Gun positions depend on _rebelLevelType (DAT_004436de)
+// renderTurretLaserShots -- Handler 0x26 turret laser rendering (FUN_40AD63).
void InsaneRebel2::renderTurretLaserShots(byte *renderBitmap, int pitch, int width, int height) {
// Uses pre-initialized _laserTexture from sprite 5 of CPITIMAG.NUT
@@ -4047,9 +4048,7 @@ void InsaneRebel2::renderTurretLaserShots(byte *renderBitmap, int pitch, int wid
}
}
-// Handler 8 Vehicle laser rendering (FUN_402ED0)
-// In the original, the laser is a short muzzle flash from gun barrel toward ship center,
-// NOT a projectile traveling across the screen. The "hit" effect is handled separately.
+// renderVehicleLaserShots -- Handler 8 vehicle laser rendering (FUN_402ED0).
void InsaneRebel2::renderVehicleLaserShots(byte *renderBitmap, int pitch, int width, int height) {
// No NUT check needed - uses pre-initialized _laserTexture
@@ -4093,8 +4092,7 @@ void InsaneRebel2::renderVehicleLaserShots(byte *renderBitmap, int pitch, int wi
}
}
-// Handler 7 Space combat laser rendering (FUN_40FADF)
-// Dual beams from left and right gun positions
+// renderSpaceLaserShots -- Handler 7 space laser rendering (FUN_40FADF).
void InsaneRebel2::renderSpaceLaserShots(byte *renderBitmap, int pitch, int width, int height) {
// No NUT check needed - uses pre-initialized _laserTexture
@@ -4133,8 +4131,7 @@ void InsaneRebel2::renderSpaceLaserShots(byte *renderBitmap, int pitch, int widt
}
}
-// Handler 25 laser rendering (FUN_0041f004)
-// Speeder bike laser shots - draws beam from gun position to target
+// renderHandler25LaserShots -- Handler 25 speeder bike laser rendering (FUN_0041f004).
void InsaneRebel2::renderHandler25LaserShots(byte *renderBitmap, int pitch, int width, int height) {
// FUN_0041f004 uses turret-style shot slots with view offset adjustment
// Only render when player is uncovered (damage == 0)
@@ -4177,6 +4174,7 @@ void InsaneRebel2::renderHandler25LaserShots(byte *renderBitmap, int pitch, int
}
}
+// renderCrosshair -- Draw crosshair/reticle at mouse position.
void InsaneRebel2::renderCrosshair(byte *renderBitmap, int pitch, int width, int height) {
// From FUN_0040d836 (Handler 7) line 167-168: crosshair only drawn when DAT_004437c0 == 2
// Don't draw crosshair when shooting is disabled (flight-only segments)
@@ -4259,6 +4257,7 @@ void InsaneRebel2::renderCrosshair(byte *renderBitmap, int pitch, int width, int
}
}
+// frameEndCleanup -- Reset enemy flags and collision zones at frame end (FUN_403240).
void InsaneRebel2::frameEndCleanup() {
// Reset enemy active flags and collision zones at frame end
// The original game rebuilds lists from scratch each frame
diff --git a/engines/scumm/insane/insane_rebel_runlevels.cpp b/engines/scumm/insane/insane_rebel_runlevels.cpp
index 8ee4482d399..eee2f72bc61 100644
--- a/engines/scumm/insane/insane_rebel_runlevels.cpp
+++ b/engines/scumm/insane/insane_rebel_runlevels.cpp
@@ -29,10 +29,10 @@
namespace Scumm {
-// =============================================================================
+// ---------------------------------------------------------------------------
// Level 1 Handler - FUN_00417E53
// Single gameplay phase (01P01.SAN)
-// =============================================================================
+// ---------------------------------------------------------------------------
int InsaneRebel2::runLevel1() {
SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
@@ -93,11 +93,11 @@ int InsaneRebel2::runLevel1() {
return kLevelQuit;
}
-// =============================================================================
+// ---------------------------------------------------------------------------
// Wave State Management - FUN_00417b61
// Waits for video completion, accumulates kill state, redistributes kill credits.
// Used by all multi-wave levels (Level 2, 3, 6, etc.) as the core wave loop primitive.
-// =============================================================================
+// ---------------------------------------------------------------------------
uint16 InsaneRebel2::processWaveEnd(int16 mask, int16 *budget, int16 threshold, uint16 flags) {
// FUN_00417b61: Core wave management function
@@ -197,11 +197,11 @@ uint16 InsaneRebel2::processWaveEnd(int16 mask, int16 *budget, int16 threshold,
return result;
}
-// =============================================================================
+// ---------------------------------------------------------------------------
// Level 2 Handler - FUN_00418063
// Multiple parts with P1/P2/P3 subdirectories
// Random animation variants for each part
-// =============================================================================
+// ---------------------------------------------------------------------------
int InsaneRebel2::runLevel2() {
// FUN_00418063: Level 2 "Corellia Star" - Third-person on-foot shooting
@@ -541,11 +541,11 @@ int InsaneRebel2::runLevel2() {
return kLevelQuit;
}
-// =============================================================================
+// ---------------------------------------------------------------------------
// Level 3 Handler - FUN_0041885F
// Two phases with per-phase retry handling
// Phase 1: 03PLAY1.SAN, Phase 2: 03PLAY2.SAN
-// =============================================================================
+// ---------------------------------------------------------------------------
int InsaneRebel2::runLevel3() {
SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
@@ -660,10 +660,10 @@ int InsaneRebel2::runLevel3() {
return kLevelQuit;
}
-// =============================================================================
+// ---------------------------------------------------------------------------
// Level 4 Handler
// Cutscene + single gameplay phase
-// =============================================================================
+// ---------------------------------------------------------------------------
int InsaneRebel2::runLevel4() {
SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
@@ -726,11 +726,11 @@ int InsaneRebel2::runLevel4() {
return kLevelQuit;
}
-// =============================================================================
+// ---------------------------------------------------------------------------
// Level 5 Handler - FUN_00418EC6
// Single gameplay phase (05PLAY.SAN)
// Random A/B death video like Level 1
-// =============================================================================
+// ---------------------------------------------------------------------------
int InsaneRebel2::runLevel5() {
SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
@@ -786,11 +786,11 @@ int InsaneRebel2::runLevel5() {
return kLevelQuit;
}
-// =============================================================================
+// ---------------------------------------------------------------------------
// Level 6 Handler - FUN_00419317
// Two phases with per-phase retry (like Level 3)
// Phase 1: 06PLAY1.SAN, Phase 2: 06PLAY2.SAN
-// =============================================================================
+// ---------------------------------------------------------------------------
int InsaneRebel2::runLevel6() {
// FUN_004190d6 â Mos Eisley: two-phase on-foot (Handler 8)
@@ -911,12 +911,12 @@ int InsaneRebel2::runLevel6() {
return kLevelQuit;
}
-// =============================================================================
+// ---------------------------------------------------------------------------
// Level 7 Handler - FUN_0041974C
// "TIE Training" - Canyon flight with fork at frame 1592
// Single gameplay phase (07PLAY.SAN), optional second segment (07PLAYB.SAN)
// Death: DAT_0047ab8c-based (fork reached â DIE_B, not reached â DIE_A)
-// =============================================================================
+// ---------------------------------------------------------------------------
int InsaneRebel2::runLevel7() {
SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
@@ -988,12 +988,12 @@ int InsaneRebel2::runLevel7() {
return kLevelQuit;
}
-// =============================================================================
+// ---------------------------------------------------------------------------
// Level 8 Handler - FUN_00419976
// "Flight to Imdaar" - Y-Wing space battle (single phase)
// No cutscene (starts with BEG). flags=0x08 for gameplay.
// Death: random A or B
-// =============================================================================
+// ---------------------------------------------------------------------------
int InsaneRebel2::runLevel8() {
SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
@@ -1055,13 +1055,13 @@ int InsaneRebel2::runLevel8() {
return kLevelQuit;
}
-// =============================================================================
+// ---------------------------------------------------------------------------
// Level 9 Handler - FUN_00419B86
// "The Mine Field" - Navigate through force fields (single phase)
// No cutscene. Initial phaseState = 0xfffffffe (all bits set except bit 0).
// Mid-events at frames 0x19f (415) and 0x352 (850): FUN_00407f55 (score checkpoint)
// Death: DAT_0047ab94-based (0âA, 1âC, elseâB)
-// =============================================================================
+// ---------------------------------------------------------------------------
int InsaneRebel2::runLevel9() {
SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
@@ -1128,12 +1128,12 @@ int InsaneRebel2::runLevel9() {
return kLevelQuit;
}
-// =============================================================================
+// ---------------------------------------------------------------------------
// Level 10 Handler - FUN_00419E0A
// "Speeder Bikes" - Forest speeder chase (single phase)
// Has cutscene. Single death video (10DIE.SAN, no variants).
// Original plays DIE then RETRY in sequence (no separate check).
-// =============================================================================
+// ---------------------------------------------------------------------------
int InsaneRebel2::runLevel10() {
SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
@@ -1199,7 +1199,7 @@ int InsaneRebel2::runLevel10() {
return kLevelQuit;
}
-// =============================================================================
+// ---------------------------------------------------------------------------
// Level 11 Handler - FUN_0041A00C
// "Inside the Terror" - Three phases + bridge puzzle (Handler 8, on-foot)
//
@@ -1209,7 +1209,7 @@ int InsaneRebel2::runLevel10() {
// Exit when (phaseState & 0x70) == 0x70
// POST3/POST3B/POST3C bridge cinematics
// Phase 3 second half: P3/11P03_X (G-L) - after bridge, mask 0x0e
-// =============================================================================
+// ---------------------------------------------------------------------------
int InsaneRebel2::runLevel11() {
SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
@@ -1591,7 +1591,7 @@ int InsaneRebel2::runLevel11() {
return kLevelQuit;
}
-// =============================================================================
+// ---------------------------------------------------------------------------
// Level 12 Handler - FUN_0041AA14
// "Sewers" - Four phases FPS corridor shooting (Handler 25)
//
@@ -1601,7 +1601,7 @@ int InsaneRebel2::runLevel11() {
// Phase 3: P3/12P03_X (A,B,C,D,F) mask=6
// Phase 4: P4/12P04_X (A,B,C,D,E,F) mask=0xe
// Closing: 12P09.SAN
-// =============================================================================
+// ---------------------------------------------------------------------------
int InsaneRebel2::runLevel12() {
SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
@@ -1926,14 +1926,14 @@ int InsaneRebel2::runLevel12() {
return kLevelQuit;
}
-// =============================================================================
+// ---------------------------------------------------------------------------
// Level 13 Handler - FUN_0041B3E1
// "Escaping the Star Destroyer" - Two-phase flight/escape
// Phase A: 13PLAY_A.SAN (main flight), transitions to Phase B at maxFrame-10
// Phase B: 13PLAY_B.SAN (reactor loop, flags 0x468) â plays until
// (DAT_0047ab90 == 0 && DAT_0047ab7c == 0) meaning all targets destroyed.
// Death: frame-based (A/B/C/B/D pattern)
-// =============================================================================
+// ---------------------------------------------------------------------------
int InsaneRebel2::runLevel13() {
SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
@@ -2011,11 +2011,11 @@ int InsaneRebel2::runLevel13() {
return kLevelQuit;
}
-// =============================================================================
+// ---------------------------------------------------------------------------
// Level 14 Handler - FUN_0041B6E8
// "TIE Attack" - Final space battle (single phase)
// No cutscene. Single death video (14DIE.SAN, no variants).
-// =============================================================================
+// ---------------------------------------------------------------------------
int InsaneRebel2::runLevel14() {
SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
@@ -2076,14 +2076,14 @@ int InsaneRebel2::runLevel14() {
return kLevelQuit;
}
-// =============================================================================
+// ---------------------------------------------------------------------------
// Level 15 Handler - FUN_0041B8D7
// "Imdaar Alpha" - Final mission (single long phase with level ID switch)
// Has cutscene. Mid-level: DAT_0047a7f8 changes from 0xf to 0x10 at frame 0x21e.
// This represents a transition from the tunnel section to the core section.
// Death: frame-based (A/B/C/B/C/B/D pattern with 7 thresholds)
// On completion â FUN_0041BBE8 (credits/end game, not a playable level)
-// =============================================================================
+// ---------------------------------------------------------------------------
int InsaneRebel2::runLevel15() {
SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
Commit: c390c411cfdd197a82c3c733a24470ea41f771d0
https://github.com/scummvm/scummvm/commit/c390c411cfdd197a82c3c733a24470ea41f771d0
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:21+02:00
Commit Message:
SCUMM: RA2: Standardize menu and rendering comments
Changed paths:
engines/scumm/insane/insane_rebel.cpp
engines/scumm/insane/insane_rebel_levels.cpp
engines/scumm/insane/insane_rebel_menu.cpp
engines/scumm/insane/insane_rebel_render.cpp
diff --git a/engines/scumm/insane/insane_rebel.cpp b/engines/scumm/insane/insane_rebel.cpp
index ea2dce7a674..42284a4254d 100644
--- a/engines/scumm/insane/insane_rebel.cpp
+++ b/engines/scumm/insane/insane_rebel.cpp
@@ -540,6 +540,7 @@ InsaneRebel2::~InsaneRebel2() {
}
// notifyEvent -- EventObserver callback for global input dispatch.
+// Handles ESC (skip) and SPACE (pause) regardless of menu state.
bool InsaneRebel2::notifyEvent(const Common::Event &event) {
if (event.type == Common::EVENT_KEYDOWN) {
SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
@@ -1292,7 +1293,9 @@ bool InsaneRebel2::isShootingAllowed() {
}
// procSKIP -- Conditional FOBJ/PSAD skip via bit table (FUN_00423A50).
-// RA2 uses SKIP chunks to hide destroyed enemy sprites.
+// Same mechanism as Full Throttle, but RA2 uses it for enemy objects:
+// when setBit(enemy_id) is called on destruction, SKIP chunks containing
+// that ID cause the next FOBJ (enemy sprite) to be skipped.
void InsaneRebel2::procSKIP(int32 subSize, Common::SeekableReadStream &b) {
int16 par1, par2;
diff --git a/engines/scumm/insane/insane_rebel_levels.cpp b/engines/scumm/insane/insane_rebel_levels.cpp
index 1d5709f9f1a..cdb0d1850b5 100644
--- a/engines/scumm/insane/insane_rebel_levels.cpp
+++ b/engines/scumm/insane/insane_rebel_levels.cpp
@@ -91,6 +91,7 @@ void InsaneRebel2::playMissionBriefing() {
// playCinematic -- Play a cinematic/cutscene video.
// Resets handler to 0 (no HUD) and sets flags to 0x28 (cinematic + buffer preserve).
+// All wrapper functions (FUN_00417168/4171c5/417ab2/417327) add | 8 before calling FUN_0041f4d0.
void InsaneRebel2::playCinematic(const char *filename) {
_rebelHandler = 0;
_rebelStatusBarSprite = 0; // No status bar during cinematics
@@ -101,7 +102,8 @@ void InsaneRebel2::playCinematic(const char *filename) {
}
// playVideoWithText -- Video with progressive text overlay (FUN_004171c5).
-// Text is progressively revealed during [fadeInFrame, fadeOutFrame).
+// displayLength = currentFrame + 10 - fadeInFrame, capped at 0xBE (190) chars.
+// Text rendered at (textX, textY) via FUN_004341a0.
void InsaneRebel2::playVideoWithText(const char *filename, int textID, int textX, int textY,
int fadeInFrame, int fadeOutFrame) {
diff --git a/engines/scumm/insane/insane_rebel_menu.cpp b/engines/scumm/insane/insane_rebel_menu.cpp
index f86c8557a6c..336d8209388 100644
--- a/engines/scumm/insane/insane_rebel_menu.cpp
+++ b/engines/scumm/insane/insane_rebel_menu.cpp
@@ -49,7 +49,7 @@ void InsaneRebel2::resetMenu() {
_menuSelectionConfirmed = false;
}
-// unlockAllChapters -- Debug mode unlock (FUN_00415CF8).
+// unlockAllChapters -- Debug mode unlock (FUN_00415CF8 lines 60-71, DAT_0047ab34=='d').
void InsaneRebel2::unlockAllChapters() {
debug("Rebel2: Unlocking all chapters for testing");
for (int i = 0; i < 16; i++) {
@@ -59,7 +59,9 @@ void InsaneRebel2::unlockAllChapters() {
}
// getRandomMenuVideo -- Select random menu video variant (FUN_0041FDC8).
-// Always uses O_MENU_X.SAN (A-O) instead of audio-only O_MENU.SAN.
+// Original plays O_MENU.SAN when no progress flags are set, but that file
+// contains ONLY audio (no FOBJ frames) resulting in a black background.
+// We always use O_MENU_X.SAN (A-O) which have 320x200 background images.
Common::String InsaneRebel2::getRandomMenuVideo() {
// Select random variant (0-14 maps to A-O), ensuring different from last
@@ -80,7 +82,8 @@ Common::String InsaneRebel2::getRandomMenuVideo() {
//
// Returns -1 (no action) or 0-4 (menu item selected).
// Events captured by notifyEvent() before ScummEngine consumes them.
-// Keyboard: Up/Down navigate, Enter confirms. Mouse: Y maps to selection.
+// Keyboard: Up=0x148, Down=0x150, Enter=0x0d, ESC=0x1b.
+// Mouse mode (DAT_0047a806 == 1): Y position maps to selection.
//
int InsaneRebel2::processMenuInput() {
@@ -1784,10 +1787,16 @@ void InsaneRebel2::drawLevelSelectOverlay(byte *renderBitmap, int pitch, int wid
// Top Pilots Screen (FUN_00420116)
// ---------------------------------------------------------------------------
// Ranked pilot scores with animated reveal over a menu background video.
-// 0x4a-byte records (max 15): name, score, rating, difficulty, chapter.
+// 0x4a (74) byte records (max 15, from FUN_00410271):
+// +0x00 (4): timestamp, +0x04 (40): name, +0x36 (4): score,
+// +0x3a (4): rating, +0x3e (2): difficulty tier (1-3, TRS=value+0x9b),
+// +0x40 (2): highest chapter (1-15).
// Column X positions (low-res): medals=43, name=88, diff=195, ch=245, score=295.
+// Row Y: sVar1 * 10 + 42.
// initDefaultRankings -- Fill ranking table with defaults (FUN_0040FF00).
+// Generates 15 placeholder entries: score=(15-i)*1500, rating=(15-i)*2,
+// difficulty=((15-i)*3+14)/15, chapter=((15-i)*15+14)/15.
void InsaneRebel2::initDefaultRankings() {
_numRankings = 0;
memset(_rankings, 0, sizeof(_rankings));
@@ -1950,7 +1959,8 @@ void InsaneRebel2::drawTopPilotsOverlay(byte *renderBitmap, int pitch, int width
// Options Menu (FUN_004167A6)
// ---------------------------------------------------------------------------
// Toggle labels and slider items. Settings at DAT_00482e20[0..3].
-// Menu items: Music, SFX, Voices, Text, Controls, Rapid Fire, Volume, Back.
+// TRS IDs: Title=89, Music=90/91, SFX=92/93, Voices=94/95, Text=96/97,
+// Controls=98/99, Rapid Fire=100/101, Volume=103 "%hd%%", Back=107.
// showOptionsMenu -- Options menu loop (FUN_00416787).
void InsaneRebel2::showOptionsMenu() {
diff --git a/engines/scumm/insane/insane_rebel_render.cpp b/engines/scumm/insane/insane_rebel_render.cpp
index e12ab1cd340..62af6905343 100644
--- a/engines/scumm/insane/insane_rebel_render.cpp
+++ b/engines/scumm/insane/insane_rebel_render.cpp
@@ -469,8 +469,9 @@ end_parsing:;
// Spawn explosion into the shared 5-slot system.
// spawnExplosion -- Allocate an explosion slot at the given position.
-// Per-handler FUN_40A2E0/FUN_4028C5/FUN_40F628/FUN_41E7C2: find first free
-// slot (counter==0), set counter=10, scale=objectHalfWidth, position=center.
+// Per-handler slot arrays: 0x26=DAT_0044368e[], 8=DAT_0043f854[],
+// 7=DAT_00443770[], 25=DAT_0045792c[]. All share same logic: find first
+// free slot (counter==0), set counter=10, scale=objectHalfWidth, position=center.
void InsaneRebel2::spawnExplosion(int x, int y, int objectHalfWidth) {
for (int i = 0; i < 5; i++) {
if (!_explosions[i].active || _explosions[i].counter <= 0) {
@@ -485,6 +486,7 @@ void InsaneRebel2::spawnExplosion(int x, int y, int objectHalfWidth) {
}
// getShotMaxDuration -- Shot duration from per-level difficulty table (DAT_0047e0f0).
+// Used both as initial shot counter AND maxFrames for beam rendering.
int16 InsaneRebel2::getShotMaxDuration() {
LevelDifficultyParams params = getDifficultyParams();
// laserDelay = DAT_0047e0f0 field: shot duration in frames
@@ -558,7 +560,9 @@ void InsaneRebel2::spawnVehicleShot(int x, int y) {
}
// spawnHandler25Shot -- Handler 25 on-foot shot spawn (FUN_0041db5e).
-// Gun position computed from GRD002 character sprite offset tables.
+// Gun position from GRD002 offset tables:
+// DAT_0045791c[i] = gunOffsetTable[spriteIdx] + DAT_00457910 - DAT_0045790c
+// DAT_00457920[i] = gunYTable[spriteIdx] + DAT_00457912 - DAT_0045790e
void InsaneRebel2::spawnHandler25Shot(int x, int y) {
// Handler 25 can only shoot when uncovered (damage == 0)
if (_rebelDamageLevel != 0) {
@@ -2556,8 +2560,12 @@ void InsaneRebel2::triggerDamageEffect() {
//
// updateDamageFlashPalette -- Per-frame palette modification (FUN_00420562).
//
-// Normal hit flash: alternating inverted-to-original blend on even counter.
-// High-damage red pulse: R-channel only modification at max damage.
+// Normal hit flash (_damageHighFlashCounter == 0 or odd):
+// Blend formula: output[i] = 0xFF - ((0xFF - saved[i]) * (0x10 - counter)) >> 4
+// Counter 5->4(apply)->3(skip)->2(apply)->1(skip)->0(apply=original) = strobe.
+//
+// High-damage red pulse (_playerDamage >= 0xFF, even counter):
+// R channel only (every 3rd byte) using same formula with _damageHighFlashCounter.
//
void InsaneRebel2::updateDamageFlashPalette() {
// High-damage mode: persistent red pulsing when damage is maxed out
@@ -2602,7 +2610,8 @@ void InsaneRebel2::updateDamageFlashPalette() {
}
// updateDamageEffect -- Per-frame screen shake + palette flash (FUN_00420754).
-// Randomly shifts scanlines for visual distortion, diminishing over 10 frames.
+// Shifts counter*5 random scanlines per frame, diminishing over 10 frames.
+// Only called when not in cutscene modes (DAT_0043e000 != 4 && != 5).
void InsaneRebel2::updateDamageEffect(byte *renderBitmap, int pitch, int width, int height) {
if (_damageShakeCounter != 0) {
_damageShakeCounter--;
@@ -3312,9 +3321,11 @@ void InsaneRebel2::renderHandler8Ship(byte *renderBitmap, int pitch, int width,
}
// Handler 25: Draw GRD001 (wall/cockpit overlay) in procPostRendering.
-// renderHandler25ShipPre -- Handler 25 GRD001 pre-rendering (FUN_0041DB5E).
-// GRD sprites are drawn AFTER FOBJ enemies, before GRD002. Mode-based
-// half-width clipping for cover/uncover transitions.
+// renderHandler25ShipPre -- Handler 25 GRD001 pre-rendering (FUN_0041DB5E lines 202-221).
+// GRD sprites drawn AFTER FOBJ enemies, before GRD002. Mode-based clipping:
+// Mode 1, damage==0: left half only (pixels 0-159)
+// Mode 4, damage==0: right half only (pixels 160-319)
+// All other cases: full width (320 pixels)
void InsaneRebel2::renderHandler25ShipPre(byte *renderBitmap, int pitch, int width, int height) {
if (_rebelHandler != 25)
return;
@@ -3669,6 +3680,7 @@ void InsaneRebel2::renderEnemyOverlays(byte *renderBitmap, int pitch, int width,
}
// renderExplosions -- Dispatch to per-handler explosion renderer.
+// DAT_0047e108 flags & 1: suppress explosion sprites (counters still tick).
void InsaneRebel2::renderExplosions(byte *renderBitmap, int pitch, int width, int height) {
// Check flags bit 0: suppress explosion sprite rendering
LevelDifficultyParams dparams = getDifficultyParams();
@@ -3705,6 +3717,8 @@ void InsaneRebel2::renderExplosions(byte *renderBitmap, int pitch, int width, in
}
// renderTurretExplosions -- Handler 0x26 turret explosion rendering (FUN_409FBC).
+// Position: FUN_0041c720 3D->2D projection (identity at low-res).
+// Scale thresholds: <11, <21. Secondary NUT: DAT_0047fe80 (if DAT_0047a7fc >= 0).
void InsaneRebel2::renderTurretExplosions(byte *renderBitmap, int pitch, int width, int height) {
if (!_smush_iconsNut)
return;
@@ -3747,6 +3761,8 @@ void InsaneRebel2::renderTurretExplosions(byte *renderBitmap, int pitch, int wid
}
// renderVehicleExplosions -- Handler 8 on-foot explosion rendering (FUN_402696).
+// Position: world coords minus camera offset (DAT_0043e006/08 = _viewX/_viewY).
+// Scale thresholds: <11, <21. No secondary NUT.
void InsaneRebel2::renderVehicleExplosions(byte *renderBitmap, int pitch, int width, int height) {
if (!_smush_iconsNut)
return;
@@ -3788,6 +3804,9 @@ void InsaneRebel2::renderVehicleExplosions(byte *renderBitmap, int pitch, int wi
}
// renderSpaceExplosions -- Handler 7 space explosion rendering (FUN_40F1C5).
+// Position: FUN_0041c720 3D->2D projection.
+// Scale thresholds: resolution-dependent (low-res: <11/<21, hi-res: <21/<41).
+// Secondary NUT: DAT_0047ff00 (FLY004, if DAT_0047a7fc >= 0).
void InsaneRebel2::renderSpaceExplosions(byte *renderBitmap, int pitch, int width, int height) {
if (!_smush_iconsNut)
return;
@@ -3887,6 +3906,8 @@ void InsaneRebel2::renderSpaceExplosions(byte *renderBitmap, int pitch, int widt
}
// renderHandler25Explosions -- Handler 25 FPS explosion rendering (FUN_41F29A).
+// Position: world coords + view offset (DAT_0045790c/0e = _rebelViewOffsetX/Y).
+// Scale thresholds: resolution-dependent (same as Handler 7). No sound panning.
void InsaneRebel2::renderHandler25Explosions(byte *renderBitmap, int pitch, int width, int height) {
if (!_smush_iconsNut)
return;
@@ -4049,6 +4070,7 @@ void InsaneRebel2::renderTurretLaserShots(byte *renderBitmap, int pitch, int wid
}
// renderVehicleLaserShots -- Handler 8 vehicle laser rendering (FUN_402ED0).
+// In the original, the laser is a short muzzle flash, NOT a traveling projectile.
void InsaneRebel2::renderVehicleLaserShots(byte *renderBitmap, int pitch, int width, int height) {
// No NUT check needed - uses pre-initialized _laserTexture
Commit: 60c4652e2a03009001ff55d95e020d71354a8914
https://github.com/scummvm/scummvm/commit/60c4652e2a03009001ff55d95e020d71354a8914
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:21+02:00
Commit Message:
SCUMM: RA2: Standardize IACT and runlevel comments
Changed paths:
engines/scumm/insane/insane_rebel_iact.cpp
engines/scumm/insane/insane_rebel_menu.cpp
engines/scumm/insane/insane_rebel_runlevels.cpp
diff --git a/engines/scumm/insane/insane_rebel_iact.cpp b/engines/scumm/insane/insane_rebel_iact.cpp
index 104d64dc9d1..d53069fdd89 100644
--- a/engines/scumm/insane/insane_rebel_iact.cpp
+++ b/engines/scumm/insane/insane_rebel_iact.cpp
@@ -622,7 +622,7 @@ void InsaneRebel2::iactRebel2Opcode6(byte *renderBitmap, Common::SeekableReadStr
// Skip position calculation for special modes 4 and 5
if (_shipLevelMode != 4 && _shipLevelMode != 5) {
- // ===== Movement Range Transition (Covered vs Shooting) =====
+ // ----- Movement Range Transition (Covered vs Shooting) -----
// Based on FUN_00401234 lines 85-120:
// Mode 2 = "Covered" state - contract movement range to 41 (0x29)
// Other modes = "Shooting" state - expand movement range to 127 (0x7f)
@@ -787,9 +787,9 @@ void InsaneRebel2::iactRebel2Opcode6(byte *renderBitmap, Common::SeekableReadStr
debug("Rebel2 Opcode 6 (Handler 7): Status bar enabled (body flag=%d)", bodyStatusFlag);
}
- // ============================================================
+ // ------------------------------------------------------------
// Ship position update â FUN_40C3CC case 4, lines 49-327
- // ============================================================
+ // ------------------------------------------------------------
// Velocity-based physics with momentum/inertia:
// Mouse offset from center â scaled input [-127,127]
// â velocity history averaging â physics delta (clamped ±12/frame)
@@ -1501,7 +1501,7 @@ void InsaneRebel2::iactRebel2Opcode8(byte *renderBitmap, Common::SeekableReadStr
int64 startPos = b.pos();
int64 remaining = (chunkSize > 0) ? chunkSize : (b.size() - startPos);
- // ===== Handler 7: FLY NUT Loading (Third-Person Ship) =====
+ // ----- Handler 7: FLY NUT Loading (Third-Person Ship) -----
// FUN_0040c3cc case 6: par4 determines FLY sprite slot
bool isHandler7FLY = (_rebelHandler == 7 && (par4 == 1 || par4 == 2 || par4 == 3 || par4 == 11));
if (isHandler7FLY && remaining >= 14) {
@@ -1512,7 +1512,7 @@ void InsaneRebel2::iactRebel2Opcode8(byte *renderBitmap, Common::SeekableReadStr
b.seek(startPos);
}
- // ===== Edge Blend Table Loading (par4 == 1000) =====
+ // ----- Edge Blend Table Loading (par4 == 1000) -----
// FUN_405663: After all handler-specific opcode 8 processing, checks if par4==1000.
// If so, loads a per-level 256x256 color blend table from the IACT chunk data.
// This table controls the edge glow color of laser beams (e.g. red vs green).
@@ -1535,7 +1535,7 @@ void InsaneRebel2::iactRebel2Opcode8(byte *renderBitmap, Common::SeekableReadStr
return;
}
- // ===== Auxiliary Sound Buffer Loading (par4 20-47) =====
+ // ----- Auxiliary Sound Buffer Loading (par4 20-47) -----
// FUN_401234 case 6 (handler 8): par4 0x14-0x1b (20-27) â aux buffer 0
// FUN_41CADB case 6 (handler 25): par4 0x15-0x1b (21-27) â aux buffer 0,
// 0x1f-0x25 (31-37) â aux buffer 1, 0x28 (40) â aux buffer 3,
@@ -1575,7 +1575,7 @@ void InsaneRebel2::iactRebel2Opcode8(byte *renderBitmap, Common::SeekableReadStr
return;
}
- // ===== Handler 25 (0x19): Shot-Origin Lookup Table (par4 == 8) =====
+ // ----- Handler 25 (0x19): Shot-Origin Lookup Table (par4 == 8) -----
// FUN_0041CADB case 6 pushes 30 short pointers into sscanf with format at 0x482360:
// "%hd %hd %hd %hd ... %hd %hd" (15 X/Y pairs).
// Parsed values are written into DAT_004578a6 / DAT_004578c6 at indices 5..19.
@@ -1587,7 +1587,7 @@ void InsaneRebel2::iactRebel2Opcode8(byte *renderBitmap, Common::SeekableReadStr
}
}
- // ===== Scan for embedded ANIM data =====
+ // ----- Scan for embedded ANIM data -----
// Remaining handlers require finding ANIM tag in the stream
debug("Rebel2 Opcode 8: Scanning for ANIM tag (startPos=%lld remaining=%lld)",
(long long)startPos, (long long)remaining);
@@ -1643,7 +1643,7 @@ void InsaneRebel2::iactRebel2Opcode8(byte *renderBitmap, Common::SeekableReadStr
bool handled = false;
- // ===== Handler 0x26/0x19: Turret HUD Overlays =====
+ // ----- Handler 0x26/0x19: Turret HUD Overlays -----
// FUN_00407fcb case 8: par3 1-4 for HUD NUT loading
if (!handled && (_rebelHandler == 0x26 || _rebelHandler == 0x19)) {
if (par3 >= 1 && par3 <= 4) {
@@ -1651,7 +1651,7 @@ void InsaneRebel2::iactRebel2Opcode8(byte *renderBitmap, Common::SeekableReadStr
}
}
- // ===== Handler 8: POV Ship Sprites or Background =====
+ // ----- Handler 8: POV Ship Sprites or Background -----
// FUN_00401234 case 6: par4 selects POV NUT type (1,3,6,7) or background (5)
// NOTE: par3 is always 0 for Handler 8; par4 contains the actual sprite type
if (!handled && _rebelHandler == 8) {
@@ -1665,7 +1665,7 @@ void InsaneRebel2::iactRebel2Opcode8(byte *renderBitmap, Common::SeekableReadStr
}
}
- // ===== Handler 25 (0x19): Level 2 GRD Ship Sprites and Background =====
+ // ----- Handler 25 (0x19): Level 2 GRD Ship Sprites and Background -----
// FUN_0041cadb case 6 (opcode 8): Uses PAR4 for switch selection
// par4=1: GRD001 - Primary ship sprite -> DAT_00482240 / _grd001Sprite
// par4=2: GRD002 - Secondary ship sprite -> DAT_00482238 / _grd002Sprite
@@ -1689,7 +1689,7 @@ void InsaneRebel2::iactRebel2Opcode8(byte *renderBitmap, Common::SeekableReadStr
}
}
- // ===== Fallback: Embedded SAN HUD overlays =====
+ // ----- Fallback: Embedded SAN HUD overlays -----
// For other cases, load as embedded SAN frame to HUD overlay slots
if (!handled) {
// Skip high-res data (par3 == 2, 4)
diff --git a/engines/scumm/insane/insane_rebel_menu.cpp b/engines/scumm/insane/insane_rebel_menu.cpp
index 336d8209388..26c574777a9 100644
--- a/engines/scumm/insane/insane_rebel_menu.cpp
+++ b/engines/scumm/insane/insane_rebel_menu.cpp
@@ -233,9 +233,9 @@ void InsaneRebel2::drawMenuItems(byte *renderBitmap, int pitch, int width, int h
const int itemBaseY = numItems * -5 + 0x68;
const int itemSpacing = 10;
- // =====================================================================
+ // -------------------------------------------------------------------
// Font system - Emulates linked list from FUN_00403bd0
- // =====================================================================
+ // -------------------------------------------------------------------
// Font 0 (^f00): TALKFONT.NUT
// Font 1 (^f01): SMALFONT.NUT (menu items)
// Font 2 (^f02): TITLFONT.NUT (title)
@@ -254,9 +254,9 @@ void InsaneRebel2::drawMenuItems(byte *renderBitmap, int pitch, int width, int h
Common::Rect clipRect(0, 0, _vm->_screenWidth, _vm->_screenHeight);
int actualPitch = _vm->_screenWidth;
- // =====================================================================
+ // -------------------------------------------------------------------
// Format code parser - Emulates FUN_00434d10 / FUN_00433da0
- // =====================================================================
+ // -------------------------------------------------------------------
// ^^ = literal ^, ^fNN = font switch, ^cNNN = color code, ^l = newline
// Fixed-width format codes: ^fNN (2-digit font), ^cNNN (3-digit color)
auto parseFormatCode = [&](const char *&str, int &outColor) -> int {
@@ -351,22 +351,22 @@ void InsaneRebel2::drawMenuItems(byte *renderBitmap, int pitch, int width, int h
}
};
- // =====================================================================
+ // -------------------------------------------------------------------
// Draw title - items[0]
// Centered: X = center - titleWidth/2
// Left-aligned: X = 40 (0x28)
- // =====================================================================
+ // -------------------------------------------------------------------
{
int titleWidth = getStringWidth(items[0]);
int titleX = leftAligned ? 40 : (centerX - titleWidth / 2);
drawString(items[0], titleX, titleY);
}
- // =====================================================================
+ // -------------------------------------------------------------------
// Draw selectable items with selection highlight box
// Centered: item X = center - textWidth/2, box X = center - bracketWidth/2
// Left-aligned: item X = 23 (0x17), box X = 20 (0x14)
- // =====================================================================
+ // -------------------------------------------------------------------
for (int i = 0; i < numItems; i++) {
int itemY = itemBaseY + i * itemSpacing;
const char *text = items[i + 1];
@@ -519,7 +519,7 @@ void InsaneRebel2::drawMenuStringRight(byte *renderBitmap, const char *str, int
void InsaneRebel2::drawMenuOverlay(byte *renderBitmap, int pitch, int width, int height) {
// Main menu renderer - calls shared drawMenuItems()
// Emulates FUN_004147b2 -> FUN_0041f5ae with param_3=7, param_4=0
- // =====================================================================
+ // -------------------------------------------------------------------
//
// Menu strings loaded from GAME.TRS (keyboard mode indices 10-17):
// TRS index 10: "^f02Game Main Menu" -> Title (uses TITLFONT)
@@ -1706,10 +1706,10 @@ int InsaneRebel2::processLevelSelectInput() {
}
void InsaneRebel2::drawLevelSelectOverlay(byte *renderBitmap, int pitch, int width, int height) {
- // =====================================================================
+ // -------------------------------------------------------------------
// Pilot selection / difficulty submenu renderer
// Emulates FUN_00414A41 â FUN_0041f5ae
- // =====================================================================
+ // -------------------------------------------------------------------
SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
if (!splayer) {
@@ -1718,12 +1718,12 @@ void InsaneRebel2::drawLevelSelectOverlay(byte *renderBitmap, int pitch, int wid
}
if (_gameState == kStateDifficultySelect) {
- // =====================================================================
+ // -------------------------------------------------------------------
// Difficulty submenu - LAB_00414ff6
// FUN_0041f5ae(0, &DAT_00457458, 6, 0)
// DAT_00457458 = 7 entries loaded from TRS 110-116 (FUN_00414073 lines 47-50)
// param_3 = 6 â items[0]=title(TRS 110), items[1..6]=selectable(TRS 111-116)
- // =====================================================================
+ // -------------------------------------------------------------------
const char *diffItems[7];
for (int i = 0; i < 7; i++) {
diffItems[i] = splayer->getString(110 + i);
@@ -1735,9 +1735,9 @@ void InsaneRebel2::drawLevelSelectOverlay(byte *renderBitmap, int pitch, int wid
return;
}
- // =====================================================================
+ // -------------------------------------------------------------------
// Pilot menu - FUN_0041f5ae(0, &DAT_00457768, N+4, 0)
- // =====================================================================
+ // -------------------------------------------------------------------
// items[0] = title (TRS 20)
// items[1..N] = saved pilots (formatted with ^f01^c005<name>^f00)
// items[N+1] = TRS 21 (ADD NEW PILOT)
diff --git a/engines/scumm/insane/insane_rebel_runlevels.cpp b/engines/scumm/insane/insane_rebel_runlevels.cpp
index eee2f72bc61..85fc00054f1 100644
--- a/engines/scumm/insane/insane_rebel_runlevels.cpp
+++ b/engines/scumm/insane/insane_rebel_runlevels.cpp
@@ -261,7 +261,7 @@ int InsaneRebel2::runLevel2() {
_rebelLinks[i][2] = 0;
}
- // ===== PHASE 1: P1/02P01_X.SAN =====
+ // ----- PHASE 1: P1/02P01_X.SAN -----
// FUN_0041c7d0: Reset per-phase counters
_rebelKillCounter = 0;
_rebelHitCounter = 0;
@@ -331,7 +331,7 @@ int InsaneRebel2::runLevel2() {
totalKills += _rebelKillCounter;
totalMisses += _rebelHitCounter;
- // ===== PHASE 2: P2/02P02_X.SAN =====
+ // ----- PHASE 2: P2/02P02_X.SAN -----
_currentPhase = 2;
_rebelKillCounter = 0;
_rebelHitCounter = 0;
@@ -413,7 +413,7 @@ int InsaneRebel2::runLevel2() {
totalKills += _rebelKillCounter;
totalMisses += _rebelHitCounter;
- // ===== PHASE 3: P3/02P03_X.SAN =====
+ // ----- PHASE 3: P3/02P03_X.SAN -----
_currentPhase = 3;
_rebelKillCounter = 0;
_rebelHitCounter = 0;
@@ -556,7 +556,7 @@ int InsaneRebel2::runLevel3() {
if (_vm->shouldQuit())
return kLevelQuit;
- // ===== PHASE 1 retry loop =====
+ // ----- PHASE 1 retry loop -----
while (!_vm->shouldQuit()) {
_playerShield = 255;
_playerDamage = 0;
@@ -611,7 +611,7 @@ int InsaneRebel2::runLevel3() {
if (_vm->shouldQuit())
return kLevelQuit;
- // ===== PHASE 2 retry loop (preserves phase 1 score) =====
+ // ----- PHASE 2 retry loop (preserves phase 1 score) -----
_currentPhase = 2;
while (!_vm->shouldQuit()) {
@@ -818,7 +818,7 @@ int InsaneRebel2::runLevel6() {
// DAT_0047ab9c = 0xffffffff â init phase state
_rebelPhaseState = 0xffffffff;
- // ===== PHASE 1 =====
+ // ----- PHASE 1 -----
_rebelLevelType = 5; // DAT_0047a7f8 = 5
_currentPhase = 1;
@@ -862,7 +862,7 @@ int InsaneRebel2::runLevel6() {
if (_vm->shouldQuit())
return kLevelQuit;
- // ===== PHASE 2 retry loop (inner while(true) in original) =====
+ // ----- PHASE 2 retry loop (inner while(true) in original) -----
while (!_vm->shouldQuit()) {
_rebelLevelType = 6; // DAT_0047a7f8 = 6
_currentPhase = 2;
@@ -1255,7 +1255,7 @@ int InsaneRebel2::runLevel11() {
_rebelLinks[i][2] = 0;
}
- // ===== PHASE 1: P1/11P01_X.SAN =====
+ // ----- PHASE 1: P1/11P01_X.SAN -----
_rebelKillCounter = 0;
_rebelHitCounter = 0;
_rebelPhaseState = 0;
@@ -1321,7 +1321,7 @@ int InsaneRebel2::runLevel11() {
totalKills += _rebelKillCounter;
totalMisses += _rebelHitCounter;
- // ===== PHASE 2: P2/11P02_X.SAN =====
+ // ----- PHASE 2: P2/11P02_X.SAN -----
_currentPhase = 2;
_rebelKillCounter = 0;
_rebelHitCounter = 0;
@@ -1384,7 +1384,7 @@ int InsaneRebel2::runLevel11() {
totalKills += _rebelKillCounter;
totalMisses += _rebelHitCounter;
- // ===== PHASE 3 FIRST HALF: P3/11P03_X (A-F) =====
+ // ----- PHASE 3 FIRST HALF: P3/11P03_X (A-F) -----
// Bridge puzzle â exit when (phaseState & 0x70) == 0x70
_currentPhase = 3;
_rebelKillCounter = 0;
@@ -1455,7 +1455,7 @@ int InsaneRebel2::runLevel11() {
if (_vm->shouldQuit())
return kLevelQuit;
- // ===== PHASE 3 BRIDGE CINEMATICS =====
+ // ----- PHASE 3 BRIDGE CINEMATICS -----
{
bool allBasicKilled = (_rebelPhaseState & 0x0e) >= 0x0e;
if (!allBasicKilled) {
@@ -1472,7 +1472,7 @@ int InsaneRebel2::runLevel11() {
if (_vm->shouldQuit())
return kLevelQuit;
- // ===== PHASE 3 SECOND HALF: P3/11P03_X (G-L) =====
+ // ----- PHASE 3 SECOND HALF: P3/11P03_X (G-L) -----
// Reset shots/explosions (FUN_0041ca6a equivalent)
for (int i = 0; i < 5; i++) {
_explosions[i].active = false;
@@ -1542,7 +1542,7 @@ int InsaneRebel2::runLevel11() {
if (_vm->shouldQuit())
return kLevelQuit;
- // ===== LEVEL COMPLETED =====
+ // ----- LEVEL COMPLETED -----
{
totalMisses += _rebelHitCounter;
int accuracy = 0;
@@ -1640,7 +1640,7 @@ int InsaneRebel2::runLevel12() {
_rebelLinks[i][2] = 0;
}
- // ===== PHASE 1: 12P05 â P1/12P01_X =====
+ // ----- PHASE 1: 12P05 â P1/12P01_X -----
// FUN_00401000: Reset at top of each retry
_rebelKillCounter = 0;
_rebelHitCounter = 0;
@@ -1695,7 +1695,7 @@ int InsaneRebel2::runLevel12() {
if (_vm->shouldQuit())
return kLevelQuit;
- // ===== PHASE 2: 12P06 â P2/12P02_X =====
+ // ----- PHASE 2: 12P06 â P2/12P02_X -----
_currentPhase = 2;
_rebelPhaseState = 0;
_rebelWaveState = 0;
@@ -1758,7 +1758,7 @@ int InsaneRebel2::runLevel12() {
if (_vm->shouldQuit())
return kLevelQuit;
- // ===== PHASE 3: 12P07 â P3/12P03_X =====
+ // ----- PHASE 3: 12P07 â P3/12P03_X -----
_currentPhase = 3;
_rebelPhaseState = 0;
_rebelWaveState = 0;
@@ -1819,7 +1819,7 @@ int InsaneRebel2::runLevel12() {
if (_vm->shouldQuit())
return kLevelQuit;
- // ===== PHASE 4: 12P08 â P4/12P04_X =====
+ // ----- PHASE 4: 12P08 â P4/12P04_X -----
_currentPhase = 4;
_rebelPhaseState = 0;
_rebelWaveState = 0;
@@ -1879,14 +1879,14 @@ int InsaneRebel2::runLevel12() {
if (_vm->shouldQuit())
return kLevelQuit;
- // ===== CLOSING: 12P09.SAN =====
+ // ----- CLOSING: 12P09.SAN -----
splayer->setCurVideoFlags(0x428);
splayer->play("LEV12/12P09.SAN", 12);
if (_vm->shouldQuit())
return kLevelQuit;
processWaveEnd(1, &budget, 0, 0);
- // ===== LEVEL COMPLETED =====
+ // ----- LEVEL COMPLETED -----
{
int accuracy = 0;
if (_rebelKillCounter > 0) {
Commit: 523bd629c6d292bc3fcf4a4e1dd8971d4a9a8679
https://github.com/scummvm/scummvm/commit/523bd629c6d292bc3fcf4a4e1dd8971d4a9a8679
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:21+02:00
Commit Message:
SCUMM: RA2: Clean up variable names
Changed paths:
engines/scumm/insane/insane_rebel.h
engines/scumm/insane/insane_rebel_iact.cpp
engines/scumm/insane/insane_rebel_menu.cpp
engines/scumm/insane/insane_rebel_render.cpp
engines/scumm/insane/insane_rebel_runlevels.cpp
diff --git a/engines/scumm/insane/insane_rebel.h b/engines/scumm/insane/insane_rebel.h
index f3bced79181..8bcb0e23170 100644
--- a/engines/scumm/insane/insane_rebel.h
+++ b/engines/scumm/insane/insane_rebel.h
@@ -663,7 +663,7 @@ public:
int savedBgHeight; // Height of saved background
};
- void init_enemyStruct(int id, int32 x, int32 y, int32 w, int32 h, bool active, bool destroyed, int32 explosionFrame, int type = 0);
+ void initEnemyStruct(int id, int32 x, int32 y, int32 w, int32 h, bool active, bool destroyed, int32 explosionFrame, int type = 0);
void enemyUpdate(byte *renderBitmap, Common::SeekableReadStream &b, int16 par2, int16 par3, int16 par4);
Common::List<enemy> _enemies;
diff --git a/engines/scumm/insane/insane_rebel_iact.cpp b/engines/scumm/insane/insane_rebel_iact.cpp
index d53069fdd89..47f818bd034 100644
--- a/engines/scumm/insane/insane_rebel_iact.cpp
+++ b/engines/scumm/insane/insane_rebel_iact.cpp
@@ -819,15 +819,15 @@ void InsaneRebel2::iactRebel2Opcode6(byte *renderBitmap, Common::SeekableReadStr
inputY = -127;
// --- Step 2: Scale to [-127, 127] (lines 82-84) ---
- // Mouse mode: local_c = (DAT_0047a7e0 * 0x7f) / 0xa0
- int16 local_c = (int16)((inputX * 127) / 160);
- int16 local_14 = inputY; // Y already in [-127, 127]
+ // Mouse mode: scaledInputX = (DAT_0047a7e0 * 0x7f) / 0xa0
+ int16 scaledInputX = (int16)((inputX * 127) / 160);
+ int16 scaledInputY = inputY; // Y already in [-127, 127]
// --- Step 3: Velocity history + smoothed average (lines 141-157) ---
for (int i = 24; i > 0; i--) {
_velocityHistory[i] = _velocityHistory[i - 1];
}
- _velocityHistory[0] = local_c;
+ _velocityHistory[0] = scaledInputX;
// Window size = (levelData[0] >> 4) + 1. Calibrated default: 5.
const int smoothWindow = 5;
@@ -868,17 +868,17 @@ void InsaneRebel2::iactRebel2Opcode6(byte *renderBitmap, Common::SeekableReadStr
if (_flyControlMode == 1) {
// Mode 1: Full cross-axis coupling (lines 174-186)
// Banking: vertical input deflects horizontal movement
- if (local_c < 1) {
- positionDeltaX = (int16)((levelSpeed * _smoothedVelocity - absSmoothVel * local_14 - windEffectX) >> 9);
+ if (scaledInputX < 1) {
+ positionDeltaX = (int16)((levelSpeed * _smoothedVelocity - absSmoothVel * scaledInputY - windEffectX) >> 9);
} else {
- positionDeltaX = (int16)((levelSpeed * _smoothedVelocity + absSmoothVel * local_14 - windEffectX) >> 9);
+ positionDeltaX = (int16)((levelSpeed * _smoothedVelocity + absSmoothVel * scaledInputY - windEffectX) >> 9);
}
} else {
// Mode 0/2/3: Reduced cross-axis coupling (lines 218-230)
- if (local_c < 1) {
- positionDeltaX = (int16)((levelSpeed * _smoothedVelocity - (absSmoothVel * local_14 >> 2) - windEffectX) >> 9);
+ if (scaledInputX < 1) {
+ positionDeltaX = (int16)((levelSpeed * _smoothedVelocity - (absSmoothVel * scaledInputY >> 2) - windEffectX) >> 9);
} else {
- positionDeltaX = (int16)((levelSpeed * _smoothedVelocity + (absSmoothVel * local_14 >> 2) - windEffectX) >> 9);
+ positionDeltaX = (int16)((levelSpeed * _smoothedVelocity + (absSmoothVel * scaledInputY >> 2) - windEffectX) >> 9);
}
}
@@ -894,7 +894,7 @@ void InsaneRebel2::iactRebel2Opcode6(byte *renderBitmap, Common::SeekableReadStr
// Y delta
if (_flyControlMode == 1) {
// Mode 1: clamped to ±12 with wind (lines 194-216)
- int yCalc = levelYSpeed * local_14 - (windEffectY >> 1);
+ int yCalc = levelYSpeed * scaledInputY - (windEffectY >> 1);
int yDelta = yCalc >> 10;
if (yDelta < -12)
yDelta = -12;
@@ -903,11 +903,11 @@ void InsaneRebel2::iactRebel2Opcode6(byte *renderBitmap, Common::SeekableReadStr
_flyShipScreenY -= (int16)yDelta;
} else {
// Mode 0/2/3: unclamped (lines 238-241)
- _flyShipScreenY -= (int16)((levelYSpeed * local_14) >> 10);
+ _flyShipScreenY -= (int16)((levelYSpeed * scaledInputY) >> 10);
}
// Store vertical input for direction sprite (line 243)
- _verticalInput = local_14; // DAT_0044370e
+ _verticalInput = scaledInputY; // DAT_0044370e
// Ship facing direction (line 244)
_facingRight = (0xd4 < _smoothedVelocity + _flyShipScreenX);
@@ -2458,12 +2458,12 @@ void InsaneRebel2::enemyUpdate(byte *renderBitmap, Common::SeekableReadStream &b
}
}
if (!found) {
- init_enemyStruct(enemyId, x, y, w, h, true, false, -1, par4);
+ initEnemyStruct(enemyId, x, y, w, h, true, false, -1, par4);
}
}
-// init_enemyStruct -- Create and append a new enemy entry.
-void InsaneRebel2::init_enemyStruct(int id, int32 x, int32 y, int32 w, int32 h, bool active, bool destroyed, int32 explosionFrame, int type) {
+// initEnemyStruct -- Create and append a new enemy entry.
+void InsaneRebel2::initEnemyStruct(int id, int32 x, int32 y, int32 w, int32 h, bool active, bool destroyed, int32 explosionFrame, int type) {
enemy e;
e.id = id;
e.type = type;
diff --git a/engines/scumm/insane/insane_rebel_menu.cpp b/engines/scumm/insane/insane_rebel_menu.cpp
index 26c574777a9..82561d88b33 100644
--- a/engines/scumm/insane/insane_rebel_menu.cpp
+++ b/engines/scumm/insane/insane_rebel_menu.cpp
@@ -452,7 +452,8 @@ int InsaneRebel2::getMenuStringWidth(const char *str) const {
if (*p == 'l') { str = p + 1; continue; }
}
byte c = (byte)*str++;
- if (c >= 'a' && c <= 'z') c = c - 'a' + 'A';
+ if (c >= 'a' && c <= 'z')
+ c = c - 'a' + 'A';
if (curFont && c < curFont->getNumChars())
w += curFont->getCharWidth(c);
}
@@ -495,9 +496,12 @@ void InsaneRebel2::drawMenuString(byte *renderBitmap, const char *str, int x, in
if (*p == 'l') { str = p + 1; continue; }
}
byte c = (byte)*str++;
- if (c >= 'a' && c <= 'z') c = c - 'a' + 'A';
- if (!curFont) continue;
- if (c >= curFont->getNumChars()) continue;
+ if (c >= 'a' && c <= 'z')
+ c = c - 'a' + 'A';
+ if (!curFont)
+ continue;
+ if (c >= curFont->getNumChars())
+ continue;
int charW = curFont->getCharWidth(c);
if (x >= 0 && y >= 0 && charW > 0)
curFont->drawCharV7(renderBitmap, clipRect, x, y, pitch, curColor,
diff --git a/engines/scumm/insane/insane_rebel_render.cpp b/engines/scumm/insane/insane_rebel_render.cpp
index 62af6905343..143f72db23c 100644
--- a/engines/scumm/insane/insane_rebel_render.cpp
+++ b/engines/scumm/insane/insane_rebel_render.cpp
@@ -1135,9 +1135,12 @@ void InsaneRebel2::initLaserTexture(NutRenderer *nut, int spriteIdx) {
for (int row = 0; row < texHeight; row++) {
for (int col = 0; col < texWidth; col++) {
if (_laserTexture.pixels[row * texWidth + col] != 0) {
- if (col < third) band1++;
- else if (col < third * 2) band2++;
- else band3++;
+ if (col < third)
+ band1++;
+ else if (col < third * 2)
+ band2++;
+ else
+ band3++;
}
}
}
@@ -1242,11 +1245,13 @@ void InsaneRebel2::drawEdgeHighlightLine(byte *dst, int pitch, int width, int he
return;
} else {
if (x0 < clipLeft) {
- if (x1 < clipLeft) return;
+ if (x1 < clipLeft)
+ return;
y0 = y1 + (int16)(((int)(y0 - y1) * (int)(clipLeft - x1)) / (int)(x0 - x1));
x0 = clipLeft;
} else if (x0 > clipRight) {
- if (x1 > clipRight) return;
+ if (x1 > clipRight)
+ return;
y0 = y1 + (int16)(((int)(y0 - y1) * (int)(clipRight - x1)) / (int)(x0 - x1));
x0 = clipRight;
}
@@ -1265,11 +1270,13 @@ void InsaneRebel2::drawEdgeHighlightLine(byte *dst, int pitch, int width, int he
return;
} else {
if (y0 < clipTop) {
- if (y1 < clipTop) return;
+ if (y1 < clipTop)
+ return;
x0 = x1 + (int16)(((int)(x0 - x1) * (int)(clipTop - y1)) / (int)(y0 - y1));
y0 = clipTop;
} else if (y0 > clipBottom) {
- if (y1 > clipBottom) return;
+ if (y1 > clipBottom)
+ return;
x0 = x1 + (int16)(((int)(x0 - x1) * (int)(clipBottom - y1)) / (int)(y0 - y1));
y0 = clipBottom;
}
diff --git a/engines/scumm/insane/insane_rebel_runlevels.cpp b/engines/scumm/insane/insane_rebel_runlevels.cpp
index 85fc00054f1..f1365a0024a 100644
--- a/engines/scumm/insane/insane_rebel_runlevels.cpp
+++ b/engines/scumm/insane/insane_rebel_runlevels.cpp
@@ -221,7 +221,7 @@ int InsaneRebel2::runLevel2() {
int bonusCount = 0; // local_1c: tracks bonus events (DAT_0047ab9c & 0x10)
int totalKills = 0; // local_c: accumulated kill count across phases
int totalMisses = 0; // Accumulated misses (sVar1 + sVar2 from hit counters)
- int prevWaveState = 0; // local_8: previous wave's state for Phase 3 randomization
+ int prevWaveState = 0; // variantIdx: previous wave's state for Phase 3 randomization
// Play cutscene (02CUT.SAN)
playCinematic("LEV02/02CUT.SAN");
@@ -853,7 +853,7 @@ int InsaneRebel2::runLevel6() {
}
// Phase 1 survived â save score, play POST1
- totalScore = _playerScore; // local_8 = DAT_0047ab84
+ totalScore = _playerScore; // variantIdx = DAT_0047ab84
_rebelHandler = 0;
_rebelStatusBarSprite = 0;
@@ -876,7 +876,7 @@ int InsaneRebel2::runLevel6() {
if (_vm->shouldQuit())
return kLevelQuit;
- // Accumulate score: local_8 = DAT_0047ab84 + local_8
+ // Accumulate score: variantIdx = DAT_0047ab84 + variantIdx
totalScore += _playerScore;
if (_playerShield > 0) {
@@ -1407,7 +1407,7 @@ int InsaneRebel2::runLevel11() {
{
uint16 waveSelect = processWaveEnd(0x7e, &budget, 0, 0);
- int local_8 = 0; // Tracks variant for randomization threshold
+ int variantIdx = 0; // Tracks variant for randomization threshold
// Loop until (phaseState & 0x70) == 0x70 (bridge targets destroyed)
while (waveSelect != 0xFFFF && (_rebelPhaseState & 0x70) != 0x70) {
@@ -1421,14 +1421,14 @@ int InsaneRebel2::runLevel11() {
prevPhaseState = _rebelPhaseState;
// Randomization: wider range for first few waves
- if (local_8 < 3) {
- local_8 = _vm->_rnd.getRandomNumber(7); // 0-7
+ if (variantIdx < 3) {
+ variantIdx = _vm->_rnd.getRandomNumber(7); // 0-7
} else {
- local_8 = _vm->_rnd.getRandomNumber(2); // 0-2
+ variantIdx = _vm->_rnd.getRandomNumber(2); // 0-2
}
const char *filename;
- switch (local_8) {
+ switch (variantIdx) {
case 0: filename = "LEV11/P3/11P03_A.SAN"; break;
case 1: filename = "LEV11/P3/11P03_B.SAN"; break;
case 2: filename = "LEV11/P3/11P03_C.SAN"; break;
@@ -1439,13 +1439,13 @@ int InsaneRebel2::runLevel11() {
default: filename = "LEV11/P3/11P03_E.SAN"; break; // duplicate E
}
- debug("Rebel2: Level 11 Phase 3a wave - %s (state=0x%x local_8=%d)", filename, _rebelPhaseState, local_8);
+ debug("Rebel2: Level 11 Phase 3a wave - %s (state=0x%x variantIdx=%d)", filename, _rebelPhaseState, variantIdx);
splayer->setCurVideoFlags(0x428);
splayer->play(filename, 12);
_deathFrame = splayer->_frame;
- // Threshold only for higher variants (original: (2 < local_8) - 1 & 0x14)
- int16 threshold = (local_8 > 2) ? 0x14 : 0;
+ // Threshold only for higher variants (original: (2 < variantIdx) - 1 & 0x14)
+ int16 threshold = (variantIdx > 2) ? 0x14 : 0;
waveSelect = processWaveEnd(0x7e, &budget, threshold, 0);
}
}
@@ -1498,7 +1498,7 @@ int InsaneRebel2::runLevel11() {
// Only enter wave loop if not all basic types killed already
if ((_rebelPhaseState & 0x0e) < 0x0e) {
- int local_8 = 0;
+ int variantIdx = 0;
uint16 waveSelect = processWaveEnd(0x0e, &budget, 0, 0);
while (waveSelect != 0xFFFF) {
@@ -1506,14 +1506,14 @@ int InsaneRebel2::runLevel11() {
return kLevelQuit;
// Wider randomization for first few waves
- if (local_8 < 4) {
- local_8 = _vm->_rnd.getRandomNumber(8); // 0-8
+ if (variantIdx < 4) {
+ variantIdx = _vm->_rnd.getRandomNumber(8); // 0-8
} else {
- local_8 = _vm->_rnd.getRandomNumber(2); // 0-2
+ variantIdx = _vm->_rnd.getRandomNumber(2); // 0-2
}
const char *filename;
- switch (local_8) {
+ switch (variantIdx) {
case 0: filename = "LEV11/P3/11P03_G.SAN"; break;
case 1: filename = "LEV11/P3/11P03_H.SAN"; break;
case 2: filename = "LEV11/P3/11P03_I.SAN"; break;
@@ -1525,12 +1525,12 @@ int InsaneRebel2::runLevel11() {
default: filename = "LEV11/P3/11P03_L.SAN"; break;
}
- debug("Rebel2: Level 11 Phase 3b wave - %s (state=0x%x local_8=%d)", filename, _rebelPhaseState, local_8);
+ debug("Rebel2: Level 11 Phase 3b wave - %s (state=0x%x variantIdx=%d)", filename, _rebelPhaseState, variantIdx);
splayer->setCurVideoFlags(0x428);
splayer->play(filename, 12);
_deathFrame = splayer->_frame;
- int16 threshold = (local_8 > 2) ? 0x14 : 0;
+ int16 threshold = (variantIdx > 2) ? 0x14 : 0;
waveSelect = processWaveEnd(0x0e, &budget, threshold, 0);
}
}
@@ -1725,15 +1725,15 @@ int InsaneRebel2::runLevel12() {
return kLevelQuit;
// Variant selection: (waveSelect & 2) controls which set
- int local_8;
+ int variantIdx;
if ((waveSelect & 2) == 0) {
- local_8 = _vm->_rnd.getRandomNumber(2) + 3; // 3, 4, or 5
+ variantIdx = _vm->_rnd.getRandomNumber(2) + 3; // 3, 4, or 5
} else {
- local_8 = _vm->_rnd.getRandomNumber(2); // 0, 1, or 2
+ variantIdx = _vm->_rnd.getRandomNumber(2); // 0, 1, or 2
}
const char *filename;
- switch (local_8) {
+ switch (variantIdx) {
case 0: filename = "LEV12/P2/12P02_A.SAN"; break;
case 1: filename = "LEV12/P2/12P02_B.SAN"; break;
case 2: filename = "LEV12/P2/12P02_E.SAN"; break;
@@ -1742,13 +1742,13 @@ int InsaneRebel2::runLevel12() {
default: filename = "LEV12/P2/12P02_F.SAN"; break;
}
- debug("Rebel2: Level 12 Phase 2 wave - %s (state=0x%x local_8=%d)", filename, _rebelPhaseState, local_8);
+ debug("Rebel2: Level 12 Phase 2 wave - %s (state=0x%x variantIdx=%d)", filename, _rebelPhaseState, variantIdx);
splayer->setCurVideoFlags(0x428);
splayer->play(filename, 12);
_deathFrame = splayer->_frame;
// Variants E(2) and F(5) reset threshold to 0
- int16 threshold = (local_8 == 2 || local_8 == 5) ? 0 : 0x14;
+ int16 threshold = (variantIdx == 2 || variantIdx == 5) ? 0 : 0x14;
waveSelect = processWaveEnd(6, &budget, threshold, 0);
}
}
@@ -1781,7 +1781,7 @@ int InsaneRebel2::runLevel12() {
return kLevelQuit;
{
- int local_8 = 0;
+ int variantIdx = 0;
uint16 waveSelect = processWaveEnd(6, &budget, 0x14, 0);
while (waveSelect != 0xFFFF) {
@@ -1789,14 +1789,14 @@ int InsaneRebel2::runLevel12() {
return kLevelQuit;
// Wider randomization for first few waves
- if (local_8 < 4) {
- local_8 = _vm->_rnd.getRandomNumber(5); // 0-5
+ if (variantIdx < 4) {
+ variantIdx = _vm->_rnd.getRandomNumber(5); // 0-5
} else {
- local_8 = _vm->_rnd.getRandomNumber(3); // 0-3
+ variantIdx = _vm->_rnd.getRandomNumber(3); // 0-3
}
const char *filename;
- switch (local_8) {
+ switch (variantIdx) {
case 0: filename = "LEV12/P3/12P03_C.SAN"; break;
case 1: filename = "LEV12/P3/12P03_D.SAN"; break;
case 2: filename = "LEV12/P3/12P03_A.SAN"; break;
@@ -1805,7 +1805,7 @@ int InsaneRebel2::runLevel12() {
default: filename = "LEV12/P3/12P03_F.SAN"; break; // duplicate F
}
- debug("Rebel2: Level 12 Phase 3 wave - %s (state=0x%x local_8=%d)", filename, _rebelPhaseState, local_8);
+ debug("Rebel2: Level 12 Phase 3 wave - %s (state=0x%x variantIdx=%d)", filename, _rebelPhaseState, variantIdx);
splayer->setCurVideoFlags(0x428);
splayer->play(filename, 12);
_deathFrame = splayer->_frame;
@@ -1842,21 +1842,21 @@ int InsaneRebel2::runLevel12() {
return kLevelQuit;
{
- int local_8 = 0;
+ int variantIdx = 0;
uint16 waveSelect = processWaveEnd(0x0e, &budget, 0x14, 0);
while (waveSelect != 0xFFFF) {
if (_vm->shouldQuit())
return kLevelQuit;
- if (local_8 < 4) {
- local_8 = _vm->_rnd.getRandomNumber(5); // 0-5
+ if (variantIdx < 4) {
+ variantIdx = _vm->_rnd.getRandomNumber(5); // 0-5
} else {
- local_8 = _vm->_rnd.getRandomNumber(3); // 0-3
+ variantIdx = _vm->_rnd.getRandomNumber(3); // 0-3
}
const char *filename;
- switch (local_8) {
+ switch (variantIdx) {
case 0: filename = "LEV12/P4/12P04_C.SAN"; break;
case 1: filename = "LEV12/P4/12P04_D.SAN"; break;
case 2: filename = "LEV12/P4/12P04_A.SAN"; break;
@@ -1865,7 +1865,7 @@ int InsaneRebel2::runLevel12() {
default: filename = "LEV12/P4/12P04_F.SAN"; break;
}
- debug("Rebel2: Level 12 Phase 4 wave - %s (state=0x%x local_8=%d)", filename, _rebelPhaseState, local_8);
+ debug("Rebel2: Level 12 Phase 4 wave - %s (state=0x%x variantIdx=%d)", filename, _rebelPhaseState, variantIdx);
splayer->setCurVideoFlags(0x428);
splayer->play(filename, 12);
_deathFrame = splayer->_frame;
Commit: 74da7770742ed0c33647dbf25a8a44f95a160e66
https://github.com/scummvm/scummvm/commit/74da7770742ed0c33647dbf25a8a44f95a160e66
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:22+02:00
Commit Message:
SCUMM: RA1: Add initial proof of concept
Changed paths:
A engines/scumm/insane/insane_rebel1.cpp
A engines/scumm/insane/insane_rebel1.h
engines/scumm/detection.h
engines/scumm/detection_tables.h
engines/scumm/module.mk
engines/scumm/scumm-md5.h
engines/scumm/scumm.cpp
engines/scumm/smush/smush_player.cpp
engines/scumm/smush/smush_player.h
engines/scumm/smush/smush_player_ra2.cpp
diff --git a/engines/scumm/detection.h b/engines/scumm/detection.h
index f0bf20c91ac..db1da1ba8df 100644
--- a/engines/scumm/detection.h
+++ b/engines/scumm/detection.h
@@ -223,6 +223,7 @@ enum ScummGameId {
GID_CMI,
GID_DIG,
GID_FT,
+ GID_REBEL1,
GID_REBEL2,
GID_INDY3,
GID_INDY4,
diff --git a/engines/scumm/detection_tables.h b/engines/scumm/detection_tables.h
index 33b26872482..d06d0de36c4 100644
--- a/engines/scumm/detection_tables.h
+++ b/engines/scumm/detection_tables.h
@@ -73,6 +73,7 @@ static const PlainGameDescriptor gameDescriptions[] = {
{ "indyzak", "Indiana Jones and the Last Crusade & Zak McKracken" },
{ "zakloom", "Zak McKracken & Loom" },
{ "ft", "Full Throttle" },
+ { "rebel1", "Star Wars: Rebel Assault" },
{ "rebel2", "Star Wars: Rebel Assault II: The Hidden Empire" },
{ "dig", "The Dig" },
{ "comi", "The Curse of Monkey Island" },
@@ -228,6 +229,8 @@ static const GameSettings gameVariantsTable[] = {
{"ft", "Remastered", 0, GID_FT, 7, 0, MDT_NONE, GF_DOUBLEFINE_PAK, UNK, GUIO5(GUIO_NOMIDI, GAMEOPTION_ENHANCEMENTS, GAMEOPTION_ORIGINALGUI, GAMEOPTION_LOWLATENCYAUDIO, GAMEOPTION_TTS)},
{"ft", "Demo", 0, GID_FT, 7, 0, MDT_NONE, GF_DEMO, UNK, GUIO4(GUIO_NOMIDI, GAMEOPTION_ORIGINALGUI, GAMEOPTION_LOWLATENCYAUDIO, GAMEOPTION_TTS)},
+ {"rebel1", "", 0, GID_REBEL1, 7, 0, MDT_NONE, 0, Common::kPlatformDOS, GUIO1(GUIO_NOMIDI)},
+
{"rebel2", "", 0, GID_REBEL2, 7, 0, MDT_NONE, 0, Common::kPlatformDOS, GUIO3(GUIO_NOMIDI, GAMEOPTION_REBEL2_HIRES, GAMEOPTION_REBEL2_UNLOCK_ALL)},
{"rebel2", "Demo", 0, GID_REBEL2, 7, 0, MDT_NONE, GF_DEMO, Common::kPlatformDOS, GUIO3(GUIO_NOMIDI, GAMEOPTION_REBEL2_HIRES, GAMEOPTION_REBEL2_UNLOCK_ALL)},
@@ -499,6 +502,8 @@ static const GameFilenamePattern gameFilenamesTable[] = {
{ "ft", "Vollgas Data", kGenUnchanged, Common::DE_DEU, Common::kPlatformMacintosh, 0 },
{ "ft", "Vollgas Demo Data", kGenUnchanged, Common::DE_DEU, Common::kPlatformMacintosh, "Demo" },
+ { "rebel1", "ASSAULT.EXE", kGenUnchanged, UNK_LANG, Common::kPlatformDOS, "" },
+
{ "rebel2", "RA2START.EXE", kGenUnchanged, UNK_LANG, Common::kPlatformDOS, "" },
{ "rebel2", "O_DEMO.SAN", kGenUnchanged, UNK_LANG, Common::kPlatformDOS, "Demo" },
diff --git a/engines/scumm/insane/insane_rebel1.cpp b/engines/scumm/insane/insane_rebel1.cpp
new file mode 100644
index 00000000000..32a63fc0a43
--- /dev/null
+++ b/engines/scumm/insane/insane_rebel1.cpp
@@ -0,0 +1,84 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include "scumm/scumm_v7.h"
+#include "scumm/smush/smush_player.h"
+#include "scumm/insane/insane_rebel1.h"
+
+namespace Scumm {
+
+InsaneRebel1::InsaneRebel1(ScummEngine_v7 *scumm) : Insane(), _vm(scumm) {
+ // Null out Insane base class pointers that the default constructor doesn't initialize
+ _smush_roadrashRip = nullptr;
+ _smush_roadrsh2Rip = nullptr;
+ _smush_roadrsh3Rip = nullptr;
+ _smush_goglpaltRip = nullptr;
+ _smush_tovista1Flu = nullptr;
+ _smush_tovista2Flu = nullptr;
+ _smush_toranchFlu = nullptr;
+ _smush_minedrivFlu = nullptr;
+ _smush_minefiteFlu = nullptr;
+ _smush_bensgoggNut = nullptr;
+ _smush_bencutNut = nullptr;
+ _smush_iconsNut = nullptr;
+ _smush_icons2Nut = nullptr;
+}
+
+InsaneRebel1::~InsaneRebel1() {
+}
+
+void InsaneRebel1::procPreRendering(byte *renderBitmap) {
+ // Stub: no pre-rendering for RA1 yet
+}
+
+void InsaneRebel1::procPostRendering(byte *renderBitmap, int32 codecparam, int32 setupsan12,
+ int32 setupsan13, int32 curFrame, int32 maxFrame) {
+ // Stub: no post-rendering for RA1 yet
+}
+
+void InsaneRebel1::procIACT(byte *renderBitmap, int32 codecparam, int32 setupsan12,
+ int32 setupsan13, Common::SeekableReadStream &b, int32 size, int32 flags,
+ int16 par1, int16 par2, int16 par3, int16 par4) {
+ // RA1 does not use IACT chunks
+}
+
+void InsaneRebel1::procSKIP(int32 subSize, Common::SeekableReadStream &b) {
+ // Stub
+}
+
+void InsaneRebel1::handleGameChunk(int32 subSize, Common::SeekableReadStream &b) {
+ // GAME chunks contain gameplay data (opcodes for corridor, turret, etc.)
+ // For now just log and skip
+ if (subSize >= 4) {
+ uint32 opcode = b.readUint32BE();
+ debug(5, "InsaneRebel1::handleGameChunk: opcode=0x%02x size=%d", opcode >> 24, subSize);
+ }
+}
+
+void InsaneRebel1::playLevel(int level) {
+ Common::String filename = Common::String::format("LVL%d/L%dPLAY1L.ANM", level, level);
+ debug(1, "InsaneRebel1::playLevel(%d): playing %s", level, filename.c_str());
+
+ SmushPlayer *splayer = _vm->_splayer;
+ splayer->play(filename.c_str(), 12);
+}
+
+} // End of namespace Scumm
diff --git a/engines/scumm/insane/insane_rebel1.h b/engines/scumm/insane/insane_rebel1.h
new file mode 100644
index 00000000000..0ae72492f8e
--- /dev/null
+++ b/engines/scumm/insane/insane_rebel1.h
@@ -0,0 +1,59 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#if !defined(SCUMM_INSANE_REBEL1_H) && defined(ENABLE_SCUMM_7_8)
+#define SCUMM_INSANE_REBEL1_H
+
+#include "scumm/insane/insane.h"
+
+namespace Scumm {
+
+class ScummEngine_v7;
+class SmushPlayer;
+
+/**
+ * Minimal stub for Star Wars: Rebel Assault (RA1).
+ * This is a proof-of-concept to load level 1.
+ */
+class InsaneRebel1 : public Insane {
+public:
+ InsaneRebel1(ScummEngine_v7 *scumm);
+ ~InsaneRebel1() override;
+
+ void procPreRendering(byte *renderBitmap) override;
+ void procPostRendering(byte *renderBitmap, int32 codecparam, int32 setupsan12,
+ int32 setupsan13, int32 curFrame, int32 maxFrame) override;
+ void procIACT(byte *renderBitmap, int32 codecparam, int32 setupsan12,
+ int32 setupsan13, Common::SeekableReadStream &b, int32 size, int32 flags,
+ int16 par1, int16 par2, int16 par3, int16 par4) override;
+ void procSKIP(int32 subSize, Common::SeekableReadStream &b) override;
+
+ void handleGameChunk(int32 subSize, Common::SeekableReadStream &b);
+
+ void playLevel(int level);
+
+private:
+ ScummEngine_v7 *_vm;
+};
+
+} // End of namespace Scumm
+
+#endif
diff --git a/engines/scumm/module.mk b/engines/scumm/module.mk
index 55a00dd601d..9ab40bf738f 100644
--- a/engines/scumm/module.mk
+++ b/engines/scumm/module.mk
@@ -134,6 +134,7 @@ MODULE_OBJS += \
insane/insane_scenes.o \
insane/insane_iact.o \
insane/insane_rebel.o \
+ insane/insane_rebel1.o \
insane/insane_rebel_audio.o \
insane/insane_rebel_iact.o \
insane/insane_rebel_levels.o \
diff --git a/engines/scumm/scumm-md5.h b/engines/scumm/scumm-md5.h
index 90088d90b8a..72d64bcb65c 100644
--- a/engines/scumm/scumm-md5.h
+++ b/engines/scumm/scumm-md5.h
@@ -602,6 +602,7 @@ static const MD5Table md5table[] = {
{ "b886b0a5d909c7158a914e1d7c1c6c65", "loom", "EGA", "EGA", -1, Common::FR_FRA, Common::kPlatformDOS },
{ "b8955d7d23b4972229060d1592489fef", "freddicove", "HE 100", "", 41182, Common::NL_NLD, Common::kPlatformUnknown },
{ "b95347da0052a623f7a4bb15f425d897", "pajama3", "", "Demo", 13911, Common::IT_ITA, Common::kPlatformWindows },
+ { "b9ac90e4411da1b6f7dd075cc4b03c2a", "rebel1", "", "", 214435, Common::EN_ANY, Common::kPlatformDOS },
{ "b9ba19ce376efc69be78ef3baef8d2b9", "monkey", "Mac", "", 8955, Common::EN_ANY, Common::kPlatformMacintosh },
{ "b9bb68c5d2c9b6e2d9c513a29a754a57", "puttmoon", "", "", 7828, Common::EN_ANY, Common::kPlatformDOS },
{ "b9fce749c3e9da0d41c78213c179631b", "samnmax", "CD", "Demo", 7505, Common::EN_ANY, Common::kPlatformDOS },
diff --git a/engines/scumm/scumm.cpp b/engines/scumm/scumm.cpp
index 8f09594eaa2..102fb518908 100644
--- a/engines/scumm/scumm.cpp
+++ b/engines/scumm/scumm.cpp
@@ -53,6 +53,7 @@
#include "scumm/players/player_towns.h"
#include "scumm/insane/insane.h"
#include "scumm/insane/insane_rebel.h"
+#include "scumm/insane/insane_rebel1.h"
#include "scumm/he/animation_he.h"
#include "scumm/he/font_he.h"
#include "scumm/he/intern_he.h"
@@ -1158,7 +1159,7 @@ Common::Error ScummEngine::init() {
_filenamePattern.pattern = "%.2d.LFL";
_filenamePattern.genMethod = kGenRoomNum;
- } else if (_game.id == GID_REBEL2) {
+ } else if (_game.id == GID_REBEL1 || _game.id == GID_REBEL2) {
_fileHandle = new ScummFile(this);
} else if (_game.platform == Common::kPlatformMacintosh) {
// The mac versions of Indy4, Sam&Max, DOTT, FT and The Dig used a
@@ -1507,7 +1508,7 @@ Common::Error ScummEngine::init() {
setupScumm(macResourceFile);
- if (_game.id == GID_REBEL2) {
+ if (_game.id == GID_REBEL1 || _game.id == GID_REBEL2) {
_setupIsComplete = true;
return Common::kNoError;
}
@@ -1786,6 +1787,38 @@ void ScummEngine::setupScumm(const Common::Path &macResourceFile) {
#ifdef ENABLE_SCUMM_7_8
void ScummEngine_v7::setupScumm(const Common::Path &macResourceFile) {
+ if (_game.id == GID_REBEL1) {
+ _res->allocResTypeData(rtBuffer, 0, 10, kDynamicResTypeMode);
+ initScreens(0, 200);
+
+ _numVariables = 256;
+ _scummVars = (int32 *)calloc(_numVariables, sizeof(int32));
+
+ _numArray = 50;
+ _res->allocResTypeData(rtString, 0, _numArray, kDynamicResTypeMode);
+ _res->allocResTypeData(rtSound, 0, 200, kDynamicResTypeMode);
+ _res->allocResTypeData(rtCostume, 0, 200, kDynamicResTypeMode);
+ _res->allocResTypeData(rtRoom, 0, 20, kDynamicResTypeMode);
+
+ defineArray(0, kIntArray, 0, 1000);
+ _numActors = 0;
+
+ setupScummVars();
+
+ _useOriginalGUI = false;
+
+ _sound = new Sound(this, _mixer, false);
+ _musicEngine = _imuseDigital = nullptr;
+ _insane = new InsaneRebel1(this);
+ _splayer = new SmushPlayer(this, nullptr, _insane);
+
+ _macGui = nullptr;
+ _charset = new CharsetRendererV7(this);
+
+ initBanners();
+ return;
+ }
+
if (_game.id == GID_REBEL2) {
_res->allocResTypeData(rtBuffer, 0, 10, kDynamicResTypeMode);
initScreens(0, 200);
@@ -2674,6 +2707,13 @@ int ScummEngine::getTalkSpeed() {
Common::Error ScummEngine::go() {
#ifdef ENABLE_SCUMM_7_8
+ if (_game.id == GID_REBEL1) {
+ ScummEngine_v7 *vm7 = (ScummEngine_v7 *)this;
+ InsaneRebel1 *rebel = (InsaneRebel1 *)vm7->getInsane();
+ rebel->playLevel(1);
+ return Common::kNoError;
+ }
+
if (_game.id == GID_REBEL2) {
ScummEngine_v7 *vm7 = (ScummEngine_v7 *)this;
InsaneRebel2 *rebel = (InsaneRebel2 *)vm7->getInsane();
@@ -4177,7 +4217,7 @@ void ScummEngine_v7::scummLoop_handleSound() {
_imuseDigital->refreshScripts();
}
- if (_game.id == GID_REBEL2)
+ if (_game.id == GID_REBEL1 || _game.id == GID_REBEL2)
return;
_splayer->setChanFlag(0, VAR(VAR_VOICE_MODE) != 0);
diff --git a/engines/scumm/smush/smush_player.cpp b/engines/scumm/smush/smush_player.cpp
index 10b62b142ef..f0df820560a 100644
--- a/engines/scumm/smush/smush_player.cpp
+++ b/engines/scumm/smush/smush_player.cpp
@@ -44,6 +44,7 @@
#include "scumm/insane/insane.h"
#include "scumm/insane/insane_rebel.h"
+#include "scumm/insane/insane_rebel1.h"
#include "audio/audiostream.h"
#include "audio/mixer.h"
@@ -827,8 +828,161 @@ byte *SmushPlayer::getVideoPalette() {
void smushDecodeRLE(byte *dst, const byte *src, int left, int top, int width, int height, int pitch);
void smushDecodeUncompressed(byte *dst, const byte *src, int left, int top, int width, int height, int pitch);
+void smushDecodeRA1Block(byte *dst, const byte *src, int left, int top, int width, int height, int pitch, int dataSize, uint8 param, uint16 parm2, int codec);
-void SmushPlayer::decodeFrameObject(int codec, const uint8 *src, int left, int top, int width, int height, int dataSize) {
+/**
+ * RA1 codec 4/5: Block-based frame decoder.
+ * Adapted from FFmpeg's libavcodec/sanm.c old_codec4() (LGPL 2.1+).
+ *
+ * Two tile tables (c4tbl[2][256][16]):
+ * Table 0: Generated dither patterns from 16-color sub-palette
+ * Table 1: Loaded from frame data (4bpp nibble-split tiles)
+ *
+ * Each 4x4 block is decoded by reading a bit flag (from mask bytes)
+ * and an index byte. c4tbl[bit][index] gives the 16-pixel pattern.
+ * Codec 4: index 0x80 means "skip block" (delta frame).
+ * Codec 5: all indices are drawn (keyframe).
+ */
+
+// Persistent tile table state for RA1 codec 4/5
+static uint8 s_ra1C4Tbl[2][256][16];
+static uint16 s_ra1C4Param = 0xFFFF;
+
+static void ra1Codec4GenTiles(uint16 param1) {
+ uint8 *dst = &s_ra1C4Tbl[0][0][0];
+
+ for (int i = 1; i < 16; i += 2) {
+ for (int k = 0; k < 16; k++) {
+ int j = i + param1;
+ int l = k + param1;
+ int m = (j + l) / 2;
+ int n = (j + m) / 2;
+ int o = (l + m) / 2;
+ if (j == m || l == m) {
+ *dst++ = l; *dst++ = j; *dst++ = l; *dst++ = j;
+ *dst++ = j; *dst++ = l; *dst++ = j; *dst++ = j;
+ *dst++ = l; *dst++ = j; *dst++ = l; *dst++ = j;
+ *dst++ = l; *dst++ = l; *dst++ = j; *dst++ = l;
+ } else {
+ *dst++ = m; *dst++ = m; *dst++ = n; *dst++ = j;
+ *dst++ = m; *dst++ = m; *dst++ = n; *dst++ = j;
+ *dst++ = o; *dst++ = o; *dst++ = m; *dst++ = n;
+ *dst++ = l; *dst++ = l; *dst++ = o; *dst++ = m;
+ }
+ }
+ }
+
+ for (int i = 0; i < 16; i += 2) {
+ for (int k = 0; k < 16; k++) {
+ int j = i + param1;
+ int l = k + param1;
+ int m = (j + l) / 2;
+ int n = (j + m) / 2;
+ int o = (l + m) / 2;
+ if (m == j || m == l) {
+ *dst++ = j; *dst++ = j; *dst++ = l; *dst++ = j;
+ *dst++ = j; *dst++ = j; *dst++ = j; *dst++ = l;
+ *dst++ = l; *dst++ = j; *dst++ = l; *dst++ = l;
+ *dst++ = j; *dst++ = l; *dst++ = j; *dst++ = l;
+ } else {
+ *dst++ = j; *dst++ = j; *dst++ = n; *dst++ = m;
+ *dst++ = j; *dst++ = j; *dst++ = n; *dst++ = m;
+ *dst++ = n; *dst++ = n; *dst++ = m; *dst++ = o;
+ *dst++ = m; *dst++ = m; *dst++ = o; *dst++ = l;
+ }
+ }
+ }
+}
+
+static bool ra1Codec4LoadTiles(const byte *&src, int &remaining, uint16 param2, uint8 clr) {
+ uint8 *dst = &s_ra1C4Tbl[1][0][0];
+ int loop = param2 * 8;
+
+ if (param2 > 256 || remaining < loop)
+ return false;
+
+ for (int i = 0; i < loop; i++) {
+ byte c = *src++;
+ remaining--;
+ *dst++ = (c >> 4) + clr;
+ *dst++ = (c & 0xF) + clr;
+ }
+ return true;
+}
+
+void smushDecodeRA1Block(byte *dst, const byte *src, int left, int top, int width, int height,
+ int pitch, int dataSize, uint8 param, uint16 parm2, int codec) {
+ const int mx = pitch; // framebuffer width
+ const int my = height; // framebuffer height
+
+ // Generate dither tile table if palette base changed
+ if (s_ra1C4Param != param) {
+ ra1Codec4GenTiles(param);
+ s_ra1C4Param = param;
+ }
+
+ // Load frame-specific tiles from data stream (4bpp nibble-split)
+ int remaining = dataSize;
+ const byte *data = src;
+ if (parm2 > 0) {
+ if (!ra1Codec4LoadTiles(data, remaining, parm2, param)) {
+ warning("smushDecodeRA1Block: not enough data for tile load (parm2=%d)", parm2);
+ return;
+ }
+ }
+
+ // Decode blocks: iterate columns by 4, then rows by 4 (column-major order)
+ for (int j = 0; j < width; j += 4) {
+ byte mask = 0, bits = 0;
+ int x = left + j;
+ for (int i = 0; i < height; i += 4) {
+ int y = top + i;
+ int bit = 0;
+
+ if (parm2 > 0) {
+ if (bits == 0) {
+ if (remaining < 1)
+ return;
+ mask = *data++;
+ remaining--;
+ bits = 8;
+ }
+ bit = !!(mask & 0x80);
+ mask <<= 1;
+ bits--;
+ }
+
+ if (remaining < 1)
+ return;
+ byte idx = *data++;
+ remaining--;
+
+ // Codec 4: index 0x80 = skip (delta). Codec 5: no skip.
+ if (bit == 0 && idx == 0x80 && codec != 5)
+ continue;
+ if (y >= my || (y + 4) < 0 || (x + 4) < 0 || x >= mx)
+ continue;
+
+ const byte *gs = &s_ra1C4Tbl[bit][idx][0];
+ if (y >= 0 && x >= 0 && (y + 4) <= my && (x + 4) <= mx) {
+ // Fast path: fully within bounds
+ for (int k = 0; k < 4; k++, gs += 4)
+ memcpy(dst + x + (y + k) * pitch, gs, 4);
+ } else {
+ // Slow path: clipping
+ for (int k = 0; k < 4; k++) {
+ for (int l = 0; l < 4; l++, gs++) {
+ int yo = y + k, xo = x + l;
+ if (yo >= 0 && yo < my && xo >= 0 && xo < mx)
+ *(dst + yo * pitch + xo) = *gs;
+ }
+ }
+ }
+ }
+ }
+}
+
+void SmushPlayer::decodeFrameObject(int codec, const uint8 *src, int left, int top, int width, int height, int dataSize, uint8 ra1Param, uint16 ra1Parm2) {
if ((height == 242) && (width == 384)) {
int bufSize = 242 * 384;
if (_specialBuffer == nullptr || bufSize > _specialBufferSize) {
@@ -903,13 +1057,17 @@ void SmushPlayer::decodeFrameObject(int codec, const uint8 *src, int left, int t
if (_deltaGlyphsCodec)
_deltaGlyphsCodec->decode(_dst, src);
break;
+ case SMUSH_CODEC_RA1_DELTA:
+ case SMUSH_CODEC_RA1_BLOCK:
+ smushDecodeRA1Block(_dst, src, left, top, width, height, pitch, dataSize, ra1Param, ra1Parm2, codec);
+ break;
case SMUSH_CODEC_UNCOMPRESSED:
smushDecodeUncompressed(_dst, src, left, top, width, height, pitch);
break;
default:
if (isRA2() && ra2DecodeCodec(codec, src, left, top, width, height, pitch, dataSize))
break;
- if (isRA2()) {
+ if (isRA1() || isRA2()) {
debugC(DEBUG_SMUSH, "SmushPlayer::decodeFrameObject: Skipping unknown codec %d (left=%d, top=%d, %dx%d)",
codec, left, top, width, height);
break;
@@ -984,13 +1142,19 @@ void SmushPlayer::handleFrameObject(int32 subSize, Common::SeekableReadStream &b
}
int codec = b.readUint16LE();
+ uint8 ra1Param = 0; // RA1: palette base byte (FOBJ byte[1])
+ uint16 ra1Parm2 = 0; // RA1: tile count (FOBJ bytes[12-13])
+ if (isRA1()) {
+ ra1Param = (codec >> 8) & 0xFF; // byte[1] = palette base (e.g. 0xF0)
+ codec &= 0xFF; // byte[0] = actual codec number
+ }
int left = isRA2() ? (int)b.readSint16LE() : (int)b.readUint16LE();
int top = isRA2() ? (int)b.readSint16LE() : (int)b.readUint16LE();
int width = b.readUint16LE();
int height = b.readUint16LE();
b.readUint16LE();
- b.readUint16LE();
+ ra1Parm2 = b.readUint16LE();
debugC(DEBUG_SMUSH, "SmushPlayer::handleFrameObject: frame=%d codec=%d pos=(%d,%d) size=%dx%d dataSize=%d",
_frame, codec, left, top, width, height, subSize - 14);
@@ -1013,7 +1177,7 @@ void SmushPlayer::handleFrameObject(int32 subSize, Common::SeekableReadStream &b
ra2StoreFobjData(codec, chunk_buffer, chunk_size, left, top, width, height);
}
- decodeFrameObject(codec, chunk_buffer, left, top, width, height, chunk_size);
+ decodeFrameObject(codec, chunk_buffer, left, top, width, height, chunk_size, ra1Param, ra1Parm2);
free(chunk_buffer);
}
@@ -1096,6 +1260,24 @@ void SmushPlayer::handleFrame(int32 frameSize, Common::SeekableReadStream &b) {
if (isRA2())
ra2HandleGost(subSize, b);
break;
+ // RA1-specific chunk types: skip gracefully
+ case MKTAG('G','A','M','E'):
+ if (isRA1()) {
+ InsaneRebel1 *rebel1 = (InsaneRebel1 *)_vm->_insane;
+ rebel1->handleGameChunk(subSize, b);
+ }
+ break;
+ case MKTAG('G','A','M','2'):
+ case MKTAG('F','A','D','E'):
+ case MKTAG('S','E','G','A'):
+ case MKTAG('A','D','L',' '):
+ case MKTAG('A','D','L','2'):
+ case MKTAG('S','B','L',' '):
+ case MKTAG('S','B','L','2'):
+ case MKTAG('P','S','D','2'):
+ case MKTAG('P','V','O','C'):
+ debugC(DEBUG_SMUSH, "SmushPlayer::handleFrame: skipping RA1 chunk %s (%d bytes)", tag2str(subType), subSize);
+ break;
default:
error("Unknown frame subChunk found : %s, %d", tag2str(subType), subSize);
}
@@ -1157,8 +1339,15 @@ void SmushPlayer::handleAnimHeader(int32 subSize, Common::SeekableReadStream &b)
setDirtyColors(0, 255);
}
- _width = READ_LE_UINT16(&headerContent[4]);
- _height = READ_LE_UINT16(&headerContent[6]);
+ if (isRA1()) {
+ // v1 AHDR: offset 4-5 is not width/height, palette starts at offset 6
+ // RA1 resolution is always 384x242, set from first FOBJ
+ _width = 384;
+ _height = 242;
+ } else {
+ _width = READ_LE_UINT16(&headerContent[4]);
+ _height = READ_LE_UINT16(&headerContent[6]);
+ }
if (isRA2())
ra2FixupAnimHeader();
diff --git a/engines/scumm/smush/smush_player.h b/engines/scumm/smush/smush_player.h
index cc51ccd96f6..b3914043bea 100644
--- a/engines/scumm/smush/smush_player.h
+++ b/engines/scumm/smush/smush_player.h
@@ -78,6 +78,8 @@ namespace Scumm {
#define SMUSH_CODEC_RLE 1
#define SMUSH_CODEC_RLE_ALT 3
+#define SMUSH_CODEC_RA1_DELTA 4 // RA1: Delta block codec (skip on idx 0x80)
+#define SMUSH_CODEC_RA1_BLOCK 5 // RA1: Block-based frame codec (no skip)
#define SMUSH_CODEC_UNCOMPRESSED 20
#define SMUSH_CODEC_LINE_UPDATE 21 // RA2: Skip/copy with literal pixels
#define SMUSH_CODEC_SKIP_RLE 23 // RA2: Skip/copy with embedded RLE
@@ -289,7 +291,7 @@ private:
void tryCmpFile(const char *filename);
bool readString(const char *file);
- void decodeFrameObject(int codec, const uint8 *src, int left, int top, int width, int height, int dataSize = 0);
+ void decodeFrameObject(int codec, const uint8 *src, int left, int top, int width, int height, int dataSize = 0, uint8 ra1Param = 0, uint16 ra1Parm2 = 0);
void handleAnimHeader(int32 subSize, Common::SeekableReadStream &);
void handleFrame(int32 frameSize, Common::SeekableReadStream &);
void handleNewPalette(int32 subSize, Common::SeekableReadStream &);
@@ -304,7 +306,8 @@ private:
void handleLoad(int32 subSize, Common::SeekableReadStream &); // RA2 only (impl in smush_player_ra2.cpp)
void readPalette(byte *, Common::SeekableReadStream &);
- // RA2-specific methods (implemented in smush_player_ra2.cpp)
+ // RA1/RA2 identification
+ bool isRA1() const;
bool isRA2() const;
void ra2InitFields();
void ra2DestroyFields();
diff --git a/engines/scumm/smush/smush_player_ra2.cpp b/engines/scumm/smush/smush_player_ra2.cpp
index fc2da674632..0bfe0e2ae8b 100644
--- a/engines/scumm/smush/smush_player_ra2.cpp
+++ b/engines/scumm/smush/smush_player_ra2.cpp
@@ -40,6 +40,10 @@
namespace Scumm {
+bool SmushPlayer::isRA1() const {
+ return _vm->_game.id == GID_REBEL1;
+}
+
bool SmushPlayer::isRA2() const {
return _vm->_game.id == GID_REBEL2;
}
Commit: 4c5afc1201bf9200eeb1b05254088b751a32a15c
https://github.com/scummvm/scummvm/commit/4c5afc1201bf9200eeb1b05254088b751a32a15c
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:22+02:00
Commit Message:
SCUMM: RA1: Extend initial proof of concept
Changed paths:
engines/scumm/insane/insane_rebel1.cpp
engines/scumm/insane/insane_rebel1.h
engines/scumm/smush/smush_player.cpp
engines/scumm/smush/smush_player.h
diff --git a/engines/scumm/insane/insane_rebel1.cpp b/engines/scumm/insane/insane_rebel1.cpp
index 32a63fc0a43..b621ccf72a4 100644
--- a/engines/scumm/insane/insane_rebel1.cpp
+++ b/engines/scumm/insane/insane_rebel1.cpp
@@ -19,13 +19,51 @@
*
*/
+#include "common/system.h"
+#include "common/events.h"
+#include "common/endian.h"
#include "scumm/scumm_v7.h"
+#include "scumm/scumm.h"
#include "scumm/smush/smush_player.h"
#include "scumm/insane/insane_rebel1.h"
namespace Scumm {
+// Decode BOMP RLE (codec 21) sprite data into a flat pixel buffer.
+// Same algorithm as NutRenderer::codec21 but without palette tracking.
+static void decodeBomp(byte *dst, const byte *src, int width, int height, int pitch) {
+ while (height--) {
+ byte *dstNext = dst + pitch;
+ const byte *srcNext = src + 2 + READ_LE_UINT16(src);
+ src += 2;
+ int len = width;
+ byte *d = dst;
+ do {
+ int offs = READ_LE_UINT16(src); src += 2;
+ d += offs;
+ len -= offs;
+ if (len <= 0)
+ break;
+ int w = READ_LE_UINT16(src) + 1; src += 2;
+ len -= w;
+ if (len < 0)
+ w += len;
+ memcpy(d, src, w);
+ src += w;
+ d += w;
+ } while (len > 0);
+ dst = dstNext;
+ src = srcNext;
+ }
+}
+
InsaneRebel1::InsaneRebel1(ScummEngine_v7 *scumm) : Insane(), _vm(scumm) {
+ _shipPosX = 192;
+ _shipPosY = 160;
+ _shipDirIndex = 0;
+ _screenWidth = 384;
+ _screenHeight = 242;
+
// Null out Insane base class pointers that the default constructor doesn't initialize
_smush_roadrashRip = nullptr;
_smush_roadrsh2Rip = nullptr;
@@ -45,39 +83,219 @@ InsaneRebel1::InsaneRebel1(ScummEngine_v7 *scumm) : Insane(), _vm(scumm) {
InsaneRebel1::~InsaneRebel1() {
}
+// Load an RA1 NUT sprite file (ANIM v1).
+// RA1 NUTs can have odd-size FOBJ chunks padded to 2-byte alignment within
+// FRME containers. This loader handles that padding properly, unlike the
+// shared NutRenderer::loadFont which assumes even-size chunks.
+bool InsaneRebel1::loadRA1Nut(const char *filename, RA1SpriteBank &bank) {
+ ScummFile *file = _vm->instantiateScummFile();
+ _vm->openFile(*file, filename);
+ if (!file->isOpen()) {
+ warning("InsaneRebel1::loadRA1Nut: can't open %s", filename);
+ delete file;
+ return false;
+ }
+
+ uint32 tag = file->readUint32BE();
+ if (tag != MKTAG('A','N','I','M')) {
+ warning("InsaneRebel1::loadRA1Nut: no ANIM tag in %s", filename);
+ delete file;
+ return false;
+ }
+ uint32 animSize = file->readUint32BE();
+ byte *data = (byte *)malloc(animSize);
+ file->read(data, animSize);
+ file->close();
+ delete file;
+
+ // data[0..3] = AHDR tag, data[4..7] = AHDR size
+ if (READ_BE_UINT32(data) != MKTAG('A','H','D','R')) {
+ warning("InsaneRebel1::loadRA1Nut: no AHDR in %s", filename);
+ free(data);
+ return false;
+ }
+
+ bank.numSprites = READ_LE_UINT16(data + 10);
+ bank.sprites = new RA1Sprite[bank.numSprites];
+
+ // Pass 1: Walk chunks with alignment to compute total decoded size.
+ uint32 offset = 0;
+ uint32 decodedSize = 0;
+ for (int i = 0; i < bank.numSprites; i++) {
+ // Skip current chunk (AHDR or previous FRME)
+ uint32 chunkSize = READ_BE_UINT32(data + offset + 4);
+ offset += chunkSize + 8;
+ if (chunkSize & 1) offset++; // Word-align
+
+ // Now at FRME; skip its header to reach FOBJ
+ offset += 8;
+ if (offset + 22 > animSize) break;
+
+ uint16 w = READ_LE_UINT16(data + offset + 14);
+ uint16 h = READ_LE_UINT16(data + offset + 16);
+ decodedSize += w * h;
+ }
+
+ bank.decodedData = (byte *)calloc(decodedSize, 1);
+ byte *decPtr = bank.decodedData;
+
+ // Pass 2: Decode sprites.
+ offset = 0;
+ for (int i = 0; i < bank.numSprites; i++) {
+ uint32 chunkSize = READ_BE_UINT32(data + offset + 4);
+ offset += chunkSize + 8;
+ if (chunkSize & 1) offset++;
+
+ offset += 8; // Skip FRME header â now at FOBJ
+ if (offset + 22 > animSize) break;
+
+ int codec = READ_LE_UINT16(data + offset + 8);
+ bank.sprites[i].xoffs = READ_LE_INT16(data + offset + 10);
+ bank.sprites[i].yoffs = READ_LE_INT16(data + offset + 12);
+ bank.sprites[i].width = READ_LE_UINT16(data + offset + 14);
+ bank.sprites[i].height = READ_LE_UINT16(data + offset + 16);
+ bank.sprites[i].data = decPtr;
+
+ int pixelCount = bank.sprites[i].width * bank.sprites[i].height;
+ const byte *fobjData = data + offset + 22;
+
+ if (codec == 21) {
+ decodeBomp(decPtr, fobjData, bank.sprites[i].width,
+ bank.sprites[i].height, bank.sprites[i].width);
+ } else {
+ warning("InsaneRebel1::loadRA1Nut: unsupported codec %d in sprite %d", codec, i);
+ }
+
+ decPtr += pixelCount;
+ }
+
+ free(data);
+ debug(1, "InsaneRebel1::loadRA1Nut('%s'): %d sprites, %d bytes decoded",
+ filename, bank.numSprites, decodedSize);
+ return true;
+}
+
+void InsaneRebel1::loadLevelSprites(int level) {
+ Common::String filename = Common::String::format("LVL%d/L%dBANK1.NUT", level, level);
+ loadRA1Nut(filename.c_str(), _shipBank);
+}
+
void InsaneRebel1::procPreRendering(byte *renderBitmap) {
- // Stub: no pre-rendering for RA1 yet
}
void InsaneRebel1::procPostRendering(byte *renderBitmap, int32 codecparam, int32 setupsan12,
int32 setupsan13, int32 curFrame, int32 maxFrame) {
- // Stub: no post-rendering for RA1 yet
+
+ if (_shipBank.numSprites == 0 || !renderBitmap)
+ return;
+
+ int width = _player->_width;
+ int height = _player->_height;
+ if (width == 0) width = _screenWidth;
+ if (height == 0) height = _screenHeight;
+ int pitch = width;
+
+ // Read mouse position and map to normalized range (-1.0 to 1.0)
+ Common::Point mousePos = g_system->getEventManager()->getMousePos();
+ float normX = CLIP((float)(mousePos.x - width / 2) / (float)(width / 2), -1.0f, 1.0f);
+ float normY = CLIP((float)(mousePos.y - height / 2) / (float)(height / 2), -1.0f, 1.0f);
+
+ // Smooth ship position toward mouse (max 8 pixels/frame horizontal, 6 vertical)
+ int targetX = width / 2 + (int)(normX * width / 4);
+ int targetY = height / 2 + (int)(normY * height / 4);
+ _shipPosX += CLIP(targetX - _shipPosX, -8, 8);
+ _shipPosY += CLIP(targetY - _shipPosY, -6, 6);
+
+ // Map mouse to direction index (5 horizontal x 7 vertical = 35 core sprites).
+ int hZone = CLIP((int)((normX + 1.0f) * 2.5f), 0, 4);
+ int vZone = CLIP((int)((normY + 1.0f) * 3.5f), 0, 6);
+ _shipDirIndex = CLIP(hZone * 7 + vZone, 0, _shipBank.numSprites - 1);
+
+ renderShip(renderBitmap, pitch, width, height);
+}
+
+void InsaneRebel1::renderShip(byte *dst, int pitch, int width, int height) {
+ if (_shipDirIndex < 0 || _shipDirIndex >= _shipBank.numSprites)
+ return;
+
+ const RA1Sprite &spr = _shipBank.sprites[_shipDirIndex];
+ int drawX = _shipPosX - spr.width / 2;
+ int drawY = _shipPosY - spr.height / 2;
+ renderSprite(dst, pitch, width, height, drawX, drawY, spr);
+}
+
+void InsaneRebel1::renderSprite(byte *dst, int pitch, int width, int height,
+ int x, int y, const RA1Sprite &spr) {
+ if (!spr.data || spr.width <= 0 || spr.height <= 0)
+ return;
+
+ int drawX = x, drawY = y, drawW = spr.width, drawH = spr.height;
+ int srcOffsetX = 0, srcOffsetY = 0;
+
+ if (drawX < 0) { srcOffsetX = -drawX; drawW += drawX; drawX = 0; }
+ if (drawY < 0) { srcOffsetY = -drawY; drawH += drawY; drawY = 0; }
+ if (drawX + drawW > width) drawW = width - drawX;
+ if (drawY + drawH > height) drawH = height - drawY;
+ if (drawW <= 0 || drawH <= 0)
+ return;
+
+ for (int iy = 0; iy < drawH; iy++) {
+ const byte *s = spr.data + (srcOffsetY + iy) * spr.width + srcOffsetX;
+ byte *d = dst + (drawY + iy) * pitch + drawX;
+ for (int ix = 0; ix < drawW; ix++) {
+ byte px = s[ix];
+ if (px != 0)
+ d[ix] = px;
+ }
+ }
}
void InsaneRebel1::procIACT(byte *renderBitmap, int32 codecparam, int32 setupsan12,
int32 setupsan13, Common::SeekableReadStream &b, int32 size, int32 flags,
int16 par1, int16 par2, int16 par3, int16 par4) {
- // RA1 does not use IACT chunks
}
void InsaneRebel1::procSKIP(int32 subSize, Common::SeekableReadStream &b) {
- // Stub
}
void InsaneRebel1::handleGameChunk(int32 subSize, Common::SeekableReadStream &b) {
- // GAME chunks contain gameplay data (opcodes for corridor, turret, etc.)
- // For now just log and skip
- if (subSize >= 4) {
- uint32 opcode = b.readUint32BE();
- debug(5, "InsaneRebel1::handleGameChunk: opcode=0x%02x size=%d", opcode >> 24, subSize);
+ if (subSize < 8)
+ return;
+
+ uint32 opcode = b.readUint32BE();
+ uint32 param1 = b.readUint32BE();
+
+ switch (opcode) {
+ case 0x5E:
+ debug(5, "InsaneRebel1: GAME 0x5E (mode) param=%d", param1);
+ break;
+ case 0x5D:
+ debug(5, "InsaneRebel1: GAME 0x5D (link) param=%d", param1);
+ break;
+ case 0x5F:
+ debug(5, "InsaneRebel1: GAME 0x5F (event) param=%d", param1);
+ break;
+ case 0x07: case 0x08: case 0x09: case 0x0A: case 0x0B:
+ case 0x19: case 0x1A:
+ case 0x0D: case 0x0E:
+ if (subSize >= 20) {
+ b.readUint32BE(); b.readUint32BE(); b.readUint32BE();
+ }
+ break;
+ default:
+ debug(7, "InsaneRebel1: GAME unknown 0x%02x size=%d", opcode, subSize);
+ break;
}
}
void InsaneRebel1::playLevel(int level) {
+ loadLevelSprites(level);
+
Common::String filename = Common::String::format("LVL%d/L%dPLAY1L.ANM", level, level);
debug(1, "InsaneRebel1::playLevel(%d): playing %s", level, filename.c_str());
SmushPlayer *splayer = _vm->_splayer;
+ _player = splayer;
splayer->play(filename.c_str(), 12);
}
diff --git a/engines/scumm/insane/insane_rebel1.h b/engines/scumm/insane/insane_rebel1.h
index 0ae72492f8e..0ea0d7b2266 100644
--- a/engines/scumm/insane/insane_rebel1.h
+++ b/engines/scumm/insane/insane_rebel1.h
@@ -29,9 +29,28 @@ namespace Scumm {
class ScummEngine_v7;
class SmushPlayer;
+// Simple sprite bank for RA1 NUT files (ANIM v1 with odd-alignment padding).
+// Separate from NutRenderer to avoid modifying shared NUT parsing code.
+struct RA1Sprite {
+ int16 xoffs;
+ int16 yoffs;
+ uint16 width;
+ uint16 height;
+ byte *data; // Decoded pixel data (width * height bytes, 0 = transparent)
+};
+
+struct RA1SpriteBank {
+ int numSprites;
+ RA1Sprite *sprites;
+ byte *decodedData; // Single allocation for all decoded pixels
+
+ RA1SpriteBank() : numSprites(0), sprites(nullptr), decodedData(nullptr) {}
+ ~RA1SpriteBank() { delete[] sprites; free(decodedData); }
+};
+
/**
- * Minimal stub for Star Wars: Rebel Assault (RA1).
- * This is a proof-of-concept to load level 1.
+ * Proof-of-concept for Star Wars: Rebel Assault (RA1).
+ * Loads level 1 and renders the player ship sprite.
*/
class InsaneRebel1 : public Insane {
public:
@@ -51,7 +70,22 @@ public:
void playLevel(int level);
private:
+ bool loadRA1Nut(const char *filename, RA1SpriteBank &bank);
+ void loadLevelSprites(int level);
+ void renderShip(byte *dst, int pitch, int width, int height);
+ void renderSprite(byte *dst, int pitch, int width, int height,
+ int x, int y, const RA1Sprite &sprite);
+
ScummEngine_v7 *_vm;
+
+ RA1SpriteBank _shipBank;
+
+ int _shipPosX;
+ int _shipPosY;
+ int _shipDirIndex;
+
+ int _screenWidth;
+ int _screenHeight;
};
} // End of namespace Scumm
diff --git a/engines/scumm/smush/smush_player.cpp b/engines/scumm/smush/smush_player.cpp
index f0df820560a..4a093e53b64 100644
--- a/engines/scumm/smush/smush_player.cpp
+++ b/engines/scumm/smush/smush_player.cpp
@@ -1004,6 +1004,9 @@ void SmushPlayer::decodeFrameObject(int codec, const uint8 *src, int left, int t
if ((height == 242) && (width == 384)) {
_width = width;
_height = height;
+ } else if (isRA1() && _dst == _specialBuffer) {
+ // RA1: small overlay FOBJs (codec 1) should not override dimensions
+ // set by the main 384x242 codec 5 FOBJ
} else if (isRA2() && ((height != _vm->_screenHeight) || (width != _vm->_screenWidth))) {
// RA2: preserve _width/_height set during buffer allocation
} else {
@@ -1599,7 +1602,7 @@ void SmushPlayer::play(const char *filename, int32 speed, int32 offset, int32 st
// Hide mouse
bool oldMouseState = CursorMan.showMouse(false);
- if (isRA2())
+ if (isRA1() || isRA2())
insanity(true);
// Load the video
diff --git a/engines/scumm/smush/smush_player.h b/engines/scumm/smush/smush_player.h
index b3914043bea..3d43b97fb51 100644
--- a/engines/scumm/smush/smush_player.h
+++ b/engines/scumm/smush/smush_player.h
@@ -100,6 +100,7 @@ class Insane;
class SmushPlayer {
friend class Insane;
+ friend class InsaneRebel1;
friend class InsaneRebel2;
friend class SmushMultiFont;
private:
Commit: 8917d7f0091b0fd3c1ea8721335125b41241fe57
https://github.com/scummvm/scummvm/commit/8917d7f0091b0fd3c1ea8721335125b41241fe57
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:22+02:00
Commit Message:
SCUMM: RA1: Add initial UI rendering code
Changed paths:
engines/scumm/insane/insane_rebel1.cpp
engines/scumm/insane/insane_rebel1.h
diff --git a/engines/scumm/insane/insane_rebel1.cpp b/engines/scumm/insane/insane_rebel1.cpp
index b621ccf72a4..fdd20e2c2fe 100644
--- a/engines/scumm/insane/insane_rebel1.cpp
+++ b/engines/scumm/insane/insane_rebel1.cpp
@@ -29,6 +29,20 @@
namespace Scumm {
+// RA1 coordinate constants (scaled from RA2's 424x260 â 384x242)
+// RA2 center: (212, 130), RA1 center: (192, 121)
+// RA2 bounds: [20, 404]x[20, 240], RA1 bounds: [18, 366]x[18, 224]
+static const int16 kCenterX = 192;
+static const int16 kCenterY = 121;
+static const int16 kMinX = 18;
+static const int16 kMaxX = 366;
+static const int16 kMinY = 18;
+static const int16 kMaxY = 224;
+
+// Perspective focal lengths (scaled from RA2: focalX=0x2b, focalY=0x19)
+static const int16 kFocalX = 39; // 0x2b * 384/424 â 39
+static const int16 kFocalY = 23; // 0x19 * 242/260 â 23
+
// Decode BOMP RLE (codec 21) sprite data into a flat pixel buffer.
// Same algorithm as NutRenderer::codec21 but without palette tracking.
static void decodeBomp(byte *dst, const byte *src, int width, int height, int pitch) {
@@ -58,12 +72,37 @@ static void decodeBomp(byte *dst, const byte *src, int width, int height, int pi
}
InsaneRebel1::InsaneRebel1(ScummEngine_v7 *scumm) : Insane(), _vm(scumm) {
- _shipPosX = 192;
- _shipPosY = 160;
- _shipDirIndex = 0;
_screenWidth = 384;
_screenHeight = 242;
+ _shipPosX = kCenterX;
+ _shipPosY = kCenterY;
+ _shipDirIndex = 17; // Center of 5x7 grid (2*7 + 3)
+
+ _corridorLeftX = kMinX;
+ _corridorTopY = kMinY;
+ _corridorRightX = kMaxX;
+ _corridorBottomY = kMaxY;
+
+ _smoothedVelocity = 0;
+ _verticalInput = 0;
+ memset(_velocityHistory, 0, sizeof(_velocityHistory));
+ memset(_windHistoryX, 0, sizeof(_windHistoryX));
+ memset(_windHistoryY, 0, sizeof(_windHistoryY));
+ _windParamX = 0;
+ _windParamY = 0;
+
+ _perspectiveX = 0;
+ _perspectiveY = 0;
+ _viewShift = 0;
+
+ _flyControlMode = 0;
+ _hitCooldown = 0;
+
+ _playerDamage = 0;
+ _score = 0;
+ _pilots = 3;
+
// Null out Insane base class pointers that the default constructor doesn't initialize
_smush_roadrashRip = nullptr;
_smush_roadrsh2Rip = nullptr;
@@ -178,6 +217,7 @@ bool InsaneRebel1::loadRA1Nut(const char *filename, RA1SpriteBank &bank) {
void InsaneRebel1::loadLevelSprites(int level) {
Common::String filename = Common::String::format("LVL%d/L%dBANK1.NUT", level, level);
loadRA1Nut(filename.c_str(), _shipBank);
+ loadRA1Nut("SYS/DISPLAY.NUT", _displayBank);
}
void InsaneRebel1::procPreRendering(byte *renderBitmap) {
@@ -195,23 +235,164 @@ void InsaneRebel1::procPostRendering(byte *renderBitmap, int32 codecparam, int32
if (height == 0) height = _screenHeight;
int pitch = width;
- // Read mouse position and map to normalized range (-1.0 to 1.0)
+ updateShipPhysics();
+ renderShip(renderBitmap, pitch, width, height);
+ renderHUD(renderBitmap, pitch, width, height);
+}
+
+// Velocity-based ship physics adapted from RA2 Handler 7 (FUN_40C3CC case 4).
+// Mouse input â velocity history averaging â position delta â corridor collision â perspective.
+void InsaneRebel1::updateShipPhysics() {
+ // Decrement hit cooldown
+ if (_hitCooldown > 0)
+ _hitCooldown--;
+
+ // --- Step 1: Mouse input as offset from screen center ---
+ // RA2 uses _vm->_mouse (0-319, 0-199), center (160, 100)
+ // RA1 uses event manager mouse pos, center (192, 121)
Common::Point mousePos = g_system->getEventManager()->getMousePos();
- float normX = CLIP((float)(mousePos.x - width / 2) / (float)(width / 2), -1.0f, 1.0f);
- float normY = CLIP((float)(mousePos.y - height / 2) / (float)(height / 2), -1.0f, 1.0f);
+ int16 inputX = (int16)(mousePos.x - kCenterX);
+ int16 inputY = (int16)(mousePos.y - kCenterY);
+
+ // Clamp to ±kCenterX horizontal, ±127 vertical
+ inputX = CLIP<int16>(inputX, -kCenterX, kCenterX);
+ inputY = CLIP<int16>(inputY, -127, 127);
+
+ // --- Step 2: Scale to [-127, 127] ---
+ int16 scaledInputX = (int16)((inputX * 127) / kCenterX);
+ int16 scaledInputY = inputY;
+
+ // --- Step 3: Velocity history + smoothed average ---
+ for (int i = 24; i > 0; i--)
+ _velocityHistory[i] = _velocityHistory[i - 1];
+ _velocityHistory[0] = scaledInputX;
+
+ const int smoothWindow = 5;
+ int velSum = 0;
+ for (int i = 0; i < smoothWindow; i++)
+ velSum += _velocityHistory[i];
+ _smoothedVelocity = (int16)(velSum / smoothWindow);
+
+ // --- Step 4: Wind history ---
+ for (int i = 14; i > 0; i--)
+ _windHistoryX[i] = _windHistoryX[i - 1];
+ _windHistoryX[0] = _windParamX;
+
+ for (int i = 14; i > 0; i--)
+ _windHistoryY[i] = _windHistoryY[i - 1];
+ _windHistoryY[0] = _windParamY;
+
+ // Wind effect (multiplier = 0 for now, no level data)
+ int16 windEffectX = 0;
+ int16 windEffectY = 0;
+
+ // --- Step 5: Position delta ---
+ const int16 levelSpeed = 32;
+ const int16 levelYSpeed = 48;
+ int16 absSmoothVel = ABS(_smoothedVelocity);
+ int16 positionDeltaX;
+
+ if (_flyControlMode == 1) {
+ // Mode 1: Full cross-axis coupling
+ if (scaledInputX < 1)
+ positionDeltaX = (int16)((levelSpeed * _smoothedVelocity - absSmoothVel * scaledInputY - windEffectX) >> 9);
+ else
+ positionDeltaX = (int16)((levelSpeed * _smoothedVelocity + absSmoothVel * scaledInputY - windEffectX) >> 9);
+ } else {
+ // Mode 0/2/3: Reduced cross-axis coupling
+ if (scaledInputX < 1)
+ positionDeltaX = (int16)((levelSpeed * _smoothedVelocity - (absSmoothVel * scaledInputY >> 2) - windEffectX) >> 9);
+ else
+ positionDeltaX = (int16)((levelSpeed * _smoothedVelocity + (absSmoothVel * scaledInputY >> 2) - windEffectX) >> 9);
+ }
+
+ // Clamp X delta to ±12 per frame
+ positionDeltaX = CLIP<int16>(positionDeltaX, -12, 12);
+ _shipPosX += positionDeltaX;
+
+ // Y delta
+ if (_flyControlMode == 1) {
+ int yDelta = (levelYSpeed * scaledInputY - (windEffectY >> 1)) >> 10;
+ yDelta = CLIP(yDelta, -12, 12);
+ _shipPosY -= (int16)yDelta;
+ } else {
+ _shipPosY -= (int16)((levelYSpeed * scaledInputY) >> 10);
+ }
- // Smooth ship position toward mouse (max 8 pixels/frame horizontal, 6 vertical)
- int targetX = width / 2 + (int)(normX * width / 4);
- int targetY = height / 2 + (int)(normY * height / 4);
- _shipPosX += CLIP(targetX - _shipPosX, -8, 8);
- _shipPosY += CLIP(targetY - _shipPosY, -6, 6);
+ _verticalInput = scaledInputY;
+
+ // --- Step 6: Position clamping ---
+ _shipPosX = CLIP<int16>(_shipPosX, kMinX, kMaxX);
+ _shipPosY = CLIP<int16>(_shipPosY, kMinY, kMaxY);
+
+ // --- Step 7: Corridor collision (modes 0 and 2) ---
+ if (_flyControlMode == 0 || _flyControlMode == 2) {
+ if (_corridorRightX < _shipPosX) {
+ _shipPosX = _corridorRightX;
+ if (_hitCooldown < 5) {
+ for (int i = 0; i < 25; i++)
+ _velocityHistory[i] = -127;
+ _hitCooldown = 10;
+ }
+ }
+ if (_shipPosX < _corridorLeftX) {
+ _shipPosX = _corridorLeftX;
+ if (_hitCooldown < 5) {
+ for (int i = 0; i < 25; i++)
+ _velocityHistory[i] = 127;
+ _hitCooldown = 10;
+ }
+ }
+ if (_corridorBottomY < _shipPosY)
+ _shipPosY = _corridorBottomY;
+ if (_shipPosY < _corridorTopY)
+ _shipPosY = _corridorTopY;
+ }
- // Map mouse to direction index (5 horizontal x 7 vertical = 35 core sprites).
- int hZone = CLIP((int)((normX + 1.0f) * 2.5f), 0, 4);
- int vZone = CLIP((int)((normY + 1.0f) * 3.5f), 0, 6);
- _shipDirIndex = CLIP(hZone * 7 + vZone, 0, _shipBank.numSprites - 1);
+ // --- Step 8: Perspective offsets ---
+ {
+ int absOffX = ABS(_shipPosX - kCenterX);
+ if (absOffX > 0)
+ _perspectiveX = (int16)((kFocalX * kCenterX * absOffX) /
+ ((kCenterX - kFocalX) * absOffX + kFocalX * kCenterX));
+ else
+ _perspectiveX = 0;
+ if (_shipPosX < kCenterX + 1)
+ _perspectiveX = -_perspectiveX;
+
+ int absOffY = ABS(_shipPosY - kCenterY);
+ if (absOffY > 0)
+ _perspectiveY = (int16)((kFocalY * kCenterY * absOffY) /
+ ((kCenterY - kFocalY) * absOffY + kFocalY * kCenterY));
+ else
+ _perspectiveY = 0;
+ if (_shipPosY < kCenterY + 1)
+ _perspectiveY = -_perspectiveY;
+ }
- renderShip(renderBitmap, pitch, width, height);
+ _viewShift = CLIP<int16>(_smoothedVelocity, -127, 127);
+
+ // --- Step 9: Direction sprite (5x7 grid with hysteresis) ---
+ // vDir from vertical input: (0xa0 - verticalInput) >> 6
+ int16 vDir = (int16)(((int)(0xa0 - _verticalInput) + ((0xa0 - _verticalInput) < 0 ? 63 : 0)) >> 6);
+ vDir = CLIP<int16>(vDir, 0, 4);
+
+ // hDir from smoothed velocity: (0x95 - smoothedVelocity) / 0x2b
+ int16 hDir = (int16)((0x95 - _smoothedVelocity) / 0x2b);
+ hDir = CLIP<int16>(hDir, 0, 6);
+
+ // Hysteresis at center positions
+ if (hDir == 3 && ABS(_smoothedVelocity) > 10)
+ hDir = (_smoothedVelocity < 1) ? 4 : 2;
+ if (vDir == 2 && ABS(_verticalInput) > 15)
+ vDir = (_verticalInput < 1) ? 3 : 1;
+
+ _shipDirIndex = CLIP<int16>(vDir * 7 + hDir, 0, _shipBank.numSprites - 1);
+
+ debug(7, "RA1 ship: pos=(%d,%d) vel=%d vIn=%d dx=%d dir=%d corridor=[%d,%d]-[%d,%d]",
+ _shipPosX, _shipPosY, _smoothedVelocity, _verticalInput,
+ positionDeltaX, _shipDirIndex,
+ _corridorLeftX, _corridorTopY, _corridorRightX, _corridorBottomY);
}
void InsaneRebel1::renderShip(byte *dst, int pitch, int width, int height) {
@@ -219,11 +400,117 @@ void InsaneRebel1::renderShip(byte *dst, int pitch, int width, int height) {
return;
const RA1Sprite &spr = _shipBank.sprites[_shipDirIndex];
- int drawX = _shipPosX - spr.width / 2;
- int drawY = _shipPosY - spr.height / 2;
+
+ // Position: game coords â screen coords via perspective transform
+ // Adapted from RA2's renderHandler7Ship:
+ // shipCenterX = (shipX - center) + perspX + screenCenterX
+ int drawX = (_shipPosX - kCenterX) + _perspectiveX + kCenterX - spr.width / 2;
+ int drawY = (_shipPosY - kCenterY) + _perspectiveY + kCenterY - spr.height / 2;
+
renderSprite(dst, pitch, width, height, drawX, drawY, spr);
}
+// Render bottom status bar from DISPLAY.NUT with dynamic damage bar and score.
+// Original layout (320-wide): DAMAGE [green bar] | PILOTS [3 icons] | SCORE [number]
+void InsaneRebel1::renderHUD(byte *dst, int pitch, int width, int height) {
+ if (_displayBank.numSprites == 0)
+ return;
+
+ const RA1Sprite &bar = _displayBank.sprites[0];
+
+ // DISPLAY.NUT sprite is 320Ã19 at xoffs=0, yoffs=176 in the original game.
+ // Video FOBJs fill the full 384Ã242 buffer from (0,0), so use sprite offsets directly.
+ int hudX = bar.xoffs;
+ int hudY = bar.yoffs;
+
+ // Draw the status bar background with transparency (pixel 0 = transparent)
+ if (bar.data && bar.width > 0 && bar.height > 0) {
+ int drawX = hudX, drawY = hudY, drawW = bar.width, drawH = bar.height;
+ int srcOffX = 0, srcOffY = 0;
+ if (drawX < 0) { srcOffX = -drawX; drawW += drawX; drawX = 0; }
+ if (drawY < 0) { srcOffY = -drawY; drawH += drawY; drawY = 0; }
+ if (drawX + drawW > width) drawW = width - drawX;
+ if (drawY + drawH > height) drawH = height - drawY;
+
+ for (int iy = 0; iy < drawH; iy++) {
+ const byte *s = bar.data + (srcOffY + iy) * bar.width + srcOffX;
+ byte *d = dst + (drawY + iy) * pitch + drawX;
+ for (int ix = 0; ix < drawW; ix++) {
+ byte px = s[ix];
+ if (px != 0)
+ d[ix] = px;
+ }
+ }
+
+ debug(5, "RA1 HUD: drawn at (%d,%d) size=%dx%d",
+ hudX, hudY, bar.width, bar.height);
+ }
+
+ // Draw damage bar (green rectangle).
+ // From the screenshot: bar starts around x=56, y=8 within the HUD sprite,
+ // max width ~76 pixels, height ~5 pixels. Green = palette index ~0xA0.
+ {
+ int barMaxW = 76;
+ int barH = 5;
+ int barX = hudX + 56;
+ int barY = hudY + 8;
+ int fillW = barMaxW - (barMaxW * _playerDamage / 255);
+ if (fillW < 0) fillW = 0;
+ if (fillW > barMaxW) fillW = barMaxW;
+
+ // Find a green palette index â use 0xA0 (common green in RA1 palette)
+ byte greenColor = 0xA0;
+
+ for (int iy = 0; iy < barH && barY + iy < height; iy++) {
+ byte *d = dst + (barY + iy) * pitch + barX;
+ for (int ix = 0; ix < fillW && barX + ix < width; ix++) {
+ d[ix] = greenColor;
+ }
+ }
+ }
+
+ // Draw score as decimal digits.
+ // From screenshot: score area starts around x=265 within HUD, using palette text color.
+ // For now, just draw simple 4Ã7 digit bitmaps.
+ {
+ char scoreStr[8];
+ snprintf(scoreStr, sizeof(scoreStr), "%06d", _score);
+ int digitX = hudX + 265;
+ int digitY = hudY + 7;
+ byte textColor = 0xFF; // White
+
+ for (int c = 0; c < 6 && scoreStr[c]; c++) {
+ int digit = scoreStr[c] - '0';
+ // Simple 3Ã5 digit rendering
+ static const uint16 digitPatterns[10] = {
+ 0x7B6F, // 0: 111 011 011 011 111
+ 0x2492, // 1: 010 010 010 010 010
+ 0x73E7, // 2: 111 001 111 100 111
+ 0x73CF, // 3: 111 001 111 001 111
+ 0x5BC9, // 4: 101 101 111 001 001
+ 0x7E3F, // 5: 111 110 111 001 111
+ 0x7E7F, // 6: 111 110 111 101 111
+ 0x7249, // 7: 111 001 001 001 001
+ 0x7FFF, // 8: 111 111 111 111 111 (simplified)
+ 0x7FCF, // 9: 111 111 111 001 111
+ };
+ if (digit < 0 || digit > 9) digit = 0;
+ uint16 pat = digitPatterns[digit];
+ for (int py = 0; py < 5; py++) {
+ for (int px = 0; px < 3; px++) {
+ int bit = 14 - (py * 3 + px);
+ if (pat & (1 << bit)) {
+ int sx = digitX + c * 5 + px;
+ int sy = digitY + py;
+ if (sx >= 0 && sx < width && sy >= 0 && sy < height)
+ dst[sy * pitch + sx] = textColor;
+ }
+ }
+ }
+ }
+ }
+}
+
void InsaneRebel1::renderSprite(byte *dst, int pitch, int width, int height,
int x, int y, const RA1Sprite &spr) {
if (!spr.data || spr.width <= 0 || spr.height <= 0)
@@ -258,6 +545,7 @@ void InsaneRebel1::procIACT(byte *renderBitmap, int32 codecparam, int32 setupsan
void InsaneRebel1::procSKIP(int32 subSize, Common::SeekableReadStream &b) {
}
+// Parse RA1 GAME chunks.
void InsaneRebel1::handleGameChunk(int32 subSize, Common::SeekableReadStream &b) {
if (subSize < 8)
return;
@@ -267,23 +555,67 @@ void InsaneRebel1::handleGameChunk(int32 subSize, Common::SeekableReadStream &b)
switch (opcode) {
case 0x5E:
- debug(5, "InsaneRebel1: GAME 0x5E (mode) param=%d", param1);
+ // Mode control
+ _flyControlMode = (int16)param1;
+ debug(5, "RA1 GAME 0x5E: flyControlMode=%d", _flyControlMode);
break;
+
case 0x5D:
- debug(5, "InsaneRebel1: GAME 0x5D (link) param=%d", param1);
+ debug(5, "RA1 GAME 0x5D (link) param=%d", param1);
break;
+
case 0x5F:
- debug(5, "InsaneRebel1: GAME 0x5F (event) param=%d", param1);
+ debug(5, "RA1 GAME 0x5F (event) param=%d", param1);
break;
- case 0x07: case 0x08: case 0x09: case 0x0A: case 0x0B:
+
+ case 0x07:
+ // Per-frame metadata (param1=counter, param2=constant, param3/4=wind?)
+ if (subSize >= 20) {
+ uint32 param2 = b.readUint32BE();
+ uint32 param3 = b.readUint32BE();
+ uint32 param4 = b.readUint32BE();
+ _windParamX = (int16)param3;
+ _windParamY = (int16)param4;
+ debug(7, "RA1 GAME 0x07: idx=%d val=%d wind=(%d,%d)",
+ param1, param2, _windParamX, _windParamY);
+ }
+ break;
+
+ case 0x0D:
+ // Corridor boundaries: per-frame flight corridor
+ // Raw: 0x0D, left, top, right, bottom (all 32-bit BE)
+ if (subSize >= 20) {
+ _corridorLeftX = (int16)param1;
+ _corridorTopY = (int16)b.readUint32BE();
+ _corridorRightX = (int16)b.readUint32BE();
+ _corridorBottomY = (int16)b.readUint32BE();
+ debug(5, "RA1 GAME 0x0D: corridor left=%d top=%d right=%d bottom=%d",
+ _corridorLeftX, _corridorTopY, _corridorRightX, _corridorBottomY);
+ }
+ break;
+
+ case 0x0E:
+ // Secondary collision zone
+ if (subSize >= 20) {
+ uint32 param2 = b.readUint32BE();
+ uint32 param3 = b.readUint32BE();
+ uint32 param4 = b.readUint32BE();
+ debug(7, "RA1 GAME 0x0E: params=(%d,%d,%d,%d)", param1, param2, param3, param4);
+ }
+ break;
+
+ case 0x08: case 0x09: case 0x0A: case 0x0B:
case 0x19: case 0x1A:
- case 0x0D: case 0x0E:
if (subSize >= 20) {
- b.readUint32BE(); b.readUint32BE(); b.readUint32BE();
+ uint32 param2 = b.readUint32BE();
+ uint32 param3 = b.readUint32BE();
+ uint32 param4 = b.readUint32BE();
+ debug(7, "RA1 GAME 0x%02x: params=(%d,%d,%d,%d)", opcode, param1, param2, param3, param4);
}
break;
+
default:
- debug(7, "InsaneRebel1: GAME unknown 0x%02x size=%d", opcode, subSize);
+ debug(7, "RA1 GAME unknown 0x%02x size=%d", opcode, subSize);
break;
}
}
diff --git a/engines/scumm/insane/insane_rebel1.h b/engines/scumm/insane/insane_rebel1.h
index 0ea0d7b2266..6d308a66e1d 100644
--- a/engines/scumm/insane/insane_rebel1.h
+++ b/engines/scumm/insane/insane_rebel1.h
@@ -49,8 +49,8 @@ struct RA1SpriteBank {
};
/**
- * Proof-of-concept for Star Wars: Rebel Assault (RA1).
- * Loads level 1 and renders the player ship sprite.
+ * Star Wars: Rebel Assault (RA1) game logic.
+ * Adapts RA2 Handler 7 (ship flight) physics for RA1's 384x242 resolution.
*/
class InsaneRebel1 : public Insane {
public:
@@ -72,20 +72,59 @@ public:
private:
bool loadRA1Nut(const char *filename, RA1SpriteBank &bank);
void loadLevelSprites(int level);
+ void updateShipPhysics();
void renderShip(byte *dst, int pitch, int width, int height);
+ void renderHUD(byte *dst, int pitch, int width, int height);
void renderSprite(byte *dst, int pitch, int width, int height,
int x, int y, const RA1Sprite &sprite);
ScummEngine_v7 *_vm;
RA1SpriteBank _shipBank;
+ RA1SpriteBank _displayBank; // SYS/DISPLAY.NUT â bottom status bar
- int _shipPosX;
- int _shipPosY;
- int _shipDirIndex;
-
+ // RA1 screen dimensions (384x242)
int _screenWidth;
int _screenHeight;
+
+ // Ship game-coordinate position (adapted from RA2's [20,404]x[20,240])
+ // RA1 coordinate space: [18,366]x[18,224], center=(192,121)
+ int16 _shipPosX;
+ int16 _shipPosY;
+
+ // Direction sprite index (5x7 grid = 35 sprites, vDir*7 + hDir)
+ int16 _shipDirIndex;
+
+ // Corridor boundaries (set by GAME opcode 0x07)
+ int16 _corridorLeftX;
+ int16 _corridorTopY;
+ int16 _corridorRightX;
+ int16 _corridorBottomY;
+
+ // Physics state (velocity-based movement from RA2 Handler 7)
+ int16 _smoothedVelocity; // Averaged horizontal velocity
+ int16 _verticalInput; // Stored vertical input component
+ int16 _velocityHistory[25]; // Horizontal velocity ring buffer
+ int16 _windHistoryX[15]; // Wind X history buffer
+ int16 _windHistoryY[15]; // Wind Y history buffer
+ int16 _windParamX; // Wind X (from GAME opcode 0x07 sub-opcode 0)
+ int16 _windParamY; // Wind Y (from GAME opcode 0x07 sub-opcode 0)
+
+ // Perspective view offsets
+ int16 _perspectiveX;
+ int16 _perspectiveY;
+ int16 _viewShift; // Clamped smoothed velocity for view transform
+
+ // Control mode (from GAME opcode 0x5E)
+ int16 _flyControlMode;
+
+ // Hit cooldown timer
+ int16 _hitCooldown;
+
+ // HUD state
+ int16 _playerDamage; // 0-255, higher = more damage
+ int _score;
+ int _pilots; // Lives remaining
};
} // End of namespace Scumm
Commit: 3ac282b4a831a6ac8528e27ae180f9997de561a4
https://github.com/scummvm/scummvm/commit/3ac282b4a831a6ac8528e27ae180f9997de561a4
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:23+02:00
Commit Message:
SCUMM: RA1: Add mouse handling
Changed paths:
engines/scumm/insane/insane_rebel1.cpp
engines/scumm/scumm_v7.h
diff --git a/engines/scumm/insane/insane_rebel1.cpp b/engines/scumm/insane/insane_rebel1.cpp
index fdd20e2c2fe..1db60024dc5 100644
--- a/engines/scumm/insane/insane_rebel1.cpp
+++ b/engines/scumm/insane/insane_rebel1.cpp
@@ -22,6 +22,8 @@
#include "common/system.h"
#include "common/events.h"
#include "common/endian.h"
+#include "graphics/cursorman.h"
+#include "graphics/wincursor.h"
#include "scumm/scumm_v7.h"
#include "scumm/scumm.h"
#include "scumm/smush/smush_player.h"
@@ -248,18 +250,17 @@ void InsaneRebel1::updateShipPhysics() {
_hitCooldown--;
// --- Step 1: Mouse input as offset from screen center ---
- // RA2 uses _vm->_mouse (0-319, 0-199), center (160, 100)
- // RA1 uses event manager mouse pos, center (192, 121)
- Common::Point mousePos = g_system->getEventManager()->getMousePos();
- int16 inputX = (int16)(mousePos.x - kCenterX);
- int16 inputY = (int16)(mousePos.y - kCenterY);
-
- // Clamp to ±kCenterX horizontal, ±127 vertical
- inputX = CLIP<int16>(inputX, -kCenterX, kCenterX);
+ // Use _vm->_mouse (0-319, 0-199 virtual screen coords), same as RA2.
+ // Center = (160, 100) in virtual screen space.
+ int16 inputX = (int16)(_vm->_mouse.x - 160);
+ int16 inputY = (int16)(_vm->_mouse.y - 100);
+
+ // Clamp: [-160, 160] horizontal, [-127, 127] vertical (same as RA2)
+ inputX = CLIP<int16>(inputX, -160, 160);
inputY = CLIP<int16>(inputY, -127, 127);
- // --- Step 2: Scale to [-127, 127] ---
- int16 scaledInputX = (int16)((inputX * 127) / kCenterX);
+ // --- Step 2: Scale to [-127, 127] (same as RA2: scaledInputX = inputX * 127 / 160) ---
+ int16 scaledInputX = (int16)((inputX * 127) / 160);
int16 scaledInputY = inputY;
// --- Step 3: Velocity history + smoothed average ---
@@ -628,7 +629,15 @@ void InsaneRebel1::playLevel(int level) {
SmushPlayer *splayer = _vm->_splayer;
_player = splayer;
+
+ // Center mouse, hide cursor, and lock mouse to window (like RA2 flight)
+ smush_warpMouse(160, 100, -1);
+ CursorMan.showMouse(false);
+ g_system->lockMouse(true);
+
splayer->play(filename.c_str(), 12);
+
+ g_system->lockMouse(false);
}
} // End of namespace Scumm
diff --git a/engines/scumm/scumm_v7.h b/engines/scumm/scumm_v7.h
index 4c76060c16b..5e96c695c8c 100644
--- a/engines/scumm/scumm_v7.h
+++ b/engines/scumm/scumm_v7.h
@@ -38,6 +38,7 @@ class TextRenderer_v7;
class ScummEngine_v7 : public ScummEngine_v6 {
friend class SmushPlayer;
friend class Insane;
+ friend class InsaneRebel1;
friend class InsaneRebel2;
public:
ScummEngine_v7(OSystem *syst, const DetectorResult &dr);
Commit: 73fbf2b8d5291230cfb499ae4e9d64b8d920ab98
https://github.com/scummvm/scummvm/commit/73fbf2b8d5291230cfb499ae4e9d64b8d920ab98
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:23+02:00
Commit Message:
SCUMM: RA1: Add sound support
Changed paths:
engines/scumm/insane/insane_rebel1.cpp
engines/scumm/insane/insane_rebel1.h
engines/scumm/smush/smush_player.cpp
diff --git a/engines/scumm/insane/insane_rebel1.cpp b/engines/scumm/insane/insane_rebel1.cpp
index 1db60024dc5..1292a858ebe 100644
--- a/engines/scumm/insane/insane_rebel1.cpp
+++ b/engines/scumm/insane/insane_rebel1.cpp
@@ -22,6 +22,9 @@
#include "common/system.h"
#include "common/events.h"
#include "common/endian.h"
+#include "audio/audiostream.h"
+#include "audio/decoders/raw.h"
+#include "audio/mixer.h"
#include "graphics/cursorman.h"
#include "graphics/wincursor.h"
#include "scumm/scumm_v7.h"
@@ -105,6 +108,9 @@ InsaneRebel1::InsaneRebel1(ScummEngine_v7 *scumm) : Insane(), _vm(scumm) {
_score = 0;
_pilots = 3;
+ // Audio
+ initAudio(11025);
+
// Null out Insane base class pointers that the default constructor doesn't initialize
_smush_roadrashRip = nullptr;
_smush_roadrsh2Rip = nullptr;
@@ -122,6 +128,179 @@ InsaneRebel1::InsaneRebel1(ScummEngine_v7 *scumm) : Insane(), _vm(scumm) {
}
InsaneRebel1::~InsaneRebel1() {
+ terminateAudio();
+}
+
+// ---------------------------------------------------------------------------
+// Audio
+// ---------------------------------------------------------------------------
+
+void InsaneRebel1::initAudio(int sampleRate) {
+ _audioSampleRate = sampleRate;
+ for (int i = 0; i < kMaxAudioTracks; i++) {
+ _audioStreams[i] = nullptr;
+ _audioTrackActive[i] = false;
+ }
+}
+
+void InsaneRebel1::terminateAudio() {
+ for (int i = 0; i < kMaxAudioTracks; i++) {
+ if (_audioTrackActive[i]) {
+ _vm->_mixer->stopHandle(_audioHandles[i]);
+ _audioTrackActive[i] = false;
+ }
+ if (_audioStreams[i]) {
+ _audioStreams[i]->finish();
+ _audioStreams[i] = nullptr;
+ }
+ }
+}
+
+void InsaneRebel1::queueAudioData(int trackIdx, uint8 *data, int32 size, int volume, int pan) {
+ if (trackIdx < 0 || trackIdx >= kMaxAudioTracks || size <= 0 || !data)
+ return;
+
+ if (!_audioStreams[trackIdx]) {
+ debug(1, "InsaneRebel1: Creating audio stream for track %d at %d Hz", trackIdx, _audioSampleRate);
+ _audioStreams[trackIdx] = Audio::makeQueuingAudioStream(_audioSampleRate, false);
+ _audioTrackActive[trackIdx] = true;
+ _vm->_mixer->playStream(Audio::Mixer::kSFXSoundType, &_audioHandles[trackIdx],
+ _audioStreams[trackIdx], -1, Audio::Mixer::kMaxChannelVolume, 0,
+ DisposeAfterUse::NO);
+ }
+
+ byte *audioCopy = (byte *)malloc(size);
+ if (!audioCopy)
+ return;
+ memcpy(audioCopy, data, size);
+
+ _audioStreams[trackIdx]->queueBuffer(audioCopy, size, DisposeAfterUse::YES, Audio::FLAG_UNSIGNED);
+
+ int scaledVolume = (volume * Audio::Mixer::kMaxChannelVolume) / 127;
+ int scaledPan = (pan * 127) / 128;
+ _vm->_mixer->setChannelVolume(_audioHandles[trackIdx], scaledVolume);
+ _vm->_mixer->setChannelBalance(_audioHandles[trackIdx], scaledPan);
+}
+
+void InsaneRebel1::processAudioFrame(int16 feedSize) {
+ if (!_player)
+ return;
+
+ SmushPlayer *sp = _player;
+
+ if (sp->_smushTracksNeedInit) {
+ sp->_smushTracksNeedInit = false;
+ for (int i = 0; i < SMUSH_MAX_TRACKS; i++) {
+ sp->_smushDispatch[i].fadeRemaining = 0;
+ sp->_smushDispatch[i].fadeVolume = 0;
+ sp->_smushDispatch[i].fadeSampleRate = 0;
+ sp->_smushDispatch[i].elapsedAudio = 0;
+ sp->_smushDispatch[i].audioLength = 0;
+ }
+ }
+
+ for (int i = 0; i < sp->_smushNumTracks; i++) {
+ SmushPlayer::SmushAudioTrack &track = sp->_smushTracks[i];
+ SmushPlayer::SmushAudioDispatch &dispatch = sp->_smushDispatch[i];
+
+ if (track.state == TRK_STATE_INACTIVE || !track.blockPtr)
+ continue;
+
+ bool isPlayableTrack = ((track.flags & TRK_TYPE_MASK) == IS_SPEECH && sp->isChanActive(CHN_SPEECH)) ||
+ ((track.flags & TRK_TYPE_MASK) == IS_BKG_MUSIC && sp->isChanActive(CHN_BKGMUS)) ||
+ ((track.flags & TRK_TYPE_MASK) == IS_SFX && sp->isChanActive(CHN_OTHER));
+
+ if (!isPlayableTrack)
+ continue;
+
+ int baseVolume;
+ switch (track.flags & TRK_TYPE_MASK) {
+ case IS_SFX:
+ baseVolume = (sp->_smushTrackVols[1] * track.volume) >> 7;
+ break;
+ case IS_BKG_MUSIC:
+ baseVolume = (sp->_smushTrackVols[3] * track.volume) >> 7;
+ break;
+ case IS_SPEECH:
+ baseVolume = (sp->_smushTrackVols[2] * track.volume) >> 7;
+ break;
+ default:
+ baseVolume = track.volume;
+ break;
+ }
+ int mixVolume = baseVolume * sp->_smushTrackVols[0] / 127;
+
+ // Handle FADING -> PLAYING transition
+ if (track.state == TRK_STATE_FADING) {
+ dispatch.headerPtr = track.dataBuf;
+ dispatch.dataBuf = track.subChunkPtr;
+ dispatch.dataSize = track.dataSize;
+ dispatch.currentOffset = 0;
+ dispatch.audioLength = 0;
+ track.state = TRK_STATE_PLAYING;
+ }
+
+ if (track.state != TRK_STATE_INACTIVE) {
+ int32 tmpFeedSize = feedSize;
+
+ while (tmpFeedSize > 0) {
+ int32 mixInFrameCount = dispatch.currentOffset;
+
+ if (mixInFrameCount > 0 && dispatch.dataBuf && dispatch.dataSize > 0) {
+ if (dispatch.audioRemaining < 0)
+ dispatch.audioRemaining = 0;
+
+ int32 offset = dispatch.audioRemaining % dispatch.dataSize;
+
+ if (dispatch.sampleRate > 0 && sp->_smushAudioSampleRate > 0) {
+ int32 maxFrames = dispatch.sampleRate * tmpFeedSize / sp->_smushAudioSampleRate;
+ if (mixInFrameCount > maxFrames)
+ mixInFrameCount = maxFrames;
+ }
+
+ if (offset + mixInFrameCount > dispatch.dataSize)
+ mixInFrameCount = dispatch.dataSize - offset;
+
+ if (dispatch.audioRemaining + mixInFrameCount > track.availableSize) {
+ mixInFrameCount = track.availableSize - dispatch.audioRemaining;
+ if (mixInFrameCount <= 0) {
+ track.state = TRK_STATE_ENDING;
+ break;
+ }
+ }
+
+ if (mixInFrameCount > 0) {
+ if (!dispatch.dataBuf || offset < 0 || offset + mixInFrameCount > dispatch.dataSize)
+ break;
+
+ queueAudioData(i, &dispatch.dataBuf[offset], mixInFrameCount, mixVolume, track.pan);
+
+ dispatch.currentOffset -= mixInFrameCount;
+ dispatch.audioRemaining += mixInFrameCount;
+
+ if (dispatch.sampleRate > 0) {
+ int32 consumedFeed = mixInFrameCount * sp->_smushAudioSampleRate / dispatch.sampleRate;
+ tmpFeedSize -= consumedFeed;
+ } else {
+ tmpFeedSize -= mixInFrameCount;
+ }
+ }
+ }
+
+ if (dispatch.currentOffset <= 0) {
+ if (!sp->processAudioCodes(i, tmpFeedSize, mixVolume))
+ break;
+ if (dispatch.currentOffset <= 0)
+ break;
+ } else if (tmpFeedSize <= 0) {
+ break;
+ }
+ }
+ }
+
+ track.audioRemaining = dispatch.audioRemaining;
+ dispatch.state = track.state;
+ }
}
// Load an RA1 NUT sprite file (ANIM v1).
diff --git a/engines/scumm/insane/insane_rebel1.h b/engines/scumm/insane/insane_rebel1.h
index 6d308a66e1d..0a475abd7ba 100644
--- a/engines/scumm/insane/insane_rebel1.h
+++ b/engines/scumm/insane/insane_rebel1.h
@@ -22,6 +22,8 @@
#if !defined(SCUMM_INSANE_REBEL1_H) && defined(ENABLE_SCUMM_7_8)
#define SCUMM_INSANE_REBEL1_H
+#include "audio/audiostream.h"
+#include "audio/mixer.h"
#include "scumm/insane/insane.h"
namespace Scumm {
@@ -78,6 +80,14 @@ private:
void renderSprite(byte *dst, int pitch, int width, int height,
int x, int y, const RA1Sprite &sprite);
+ // Audio
+ void initAudio(int sampleRate);
+ void terminateAudio();
+ void queueAudioData(int trackIdx, uint8 *data, int32 size, int volume, int pan);
+public:
+ void processAudioFrame(int16 feedSize);
+private:
+
ScummEngine_v7 *_vm;
RA1SpriteBank _shipBank;
@@ -125,6 +135,13 @@ private:
int16 _playerDamage; // 0-255, higher = more damage
int _score;
int _pilots; // Lives remaining
+
+ // Audio state (same structure as RA2)
+ static const int kMaxAudioTracks = 4;
+ Audio::QueuingAudioStream *_audioStreams[kMaxAudioTracks];
+ Audio::SoundHandle _audioHandles[kMaxAudioTracks];
+ bool _audioTrackActive[kMaxAudioTracks];
+ int _audioSampleRate;
};
} // End of namespace Scumm
diff --git a/engines/scumm/smush/smush_player.cpp b/engines/scumm/smush/smush_player.cpp
index 4a093e53b64..782c2ffd926 100644
--- a/engines/scumm/smush/smush_player.cpp
+++ b/engines/scumm/smush/smush_player.cpp
@@ -1486,6 +1486,9 @@ void SmushPlayer::parseNextFrame() {
if (!_imuseDigital && isRA2())
ra2ParseNextFrame();
+
+ if (!_imuseDigital && isRA1())
+ processDispatches(_smushAudioSampleRate / 15);
}
void SmushPlayer::setPalette(const byte *palette) {
@@ -2061,6 +2064,9 @@ void SmushPlayer::processDispatches(int16 feedSize) {
if (isRA2() && _insane) {
InsaneRebel2 *rebel2 = static_cast<InsaneRebel2 *>(_insane);
rebel2->processAudioFrame(feedSize);
+ } else if (isRA1() && _insane) {
+ InsaneRebel1 *rebel1 = static_cast<InsaneRebel1 *>(_insane);
+ rebel1->processAudioFrame(feedSize);
}
return;
}
Commit: 4ed9384177f0f9d8b1a961605c434cc29e5135fa
https://github.com/scummvm/scummvm/commit/4ed9384177f0f9d8b1a961605c434cc29e5135fa
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:23+02:00
Commit Message:
SCUMM: RA1: Improve controls
Changed paths:
engines/scumm/insane/insane_rebel1.cpp
engines/scumm/insane/insane_rebel1.h
diff --git a/engines/scumm/insane/insane_rebel1.cpp b/engines/scumm/insane/insane_rebel1.cpp
index 1292a858ebe..d00b6f25bde 100644
--- a/engines/scumm/insane/insane_rebel1.cpp
+++ b/engines/scumm/insane/insane_rebel1.cpp
@@ -92,10 +92,8 @@ InsaneRebel1::InsaneRebel1(ScummEngine_v7 *scumm) : Insane(), _vm(scumm) {
_smoothedVelocity = 0;
_verticalInput = 0;
memset(_velocityHistory, 0, sizeof(_velocityHistory));
- memset(_windHistoryX, 0, sizeof(_windHistoryX));
- memset(_windHistoryY, 0, sizeof(_windHistoryY));
- _windParamX = 0;
- _windParamY = 0;
+ _driftParam = 0;
+ _driftAccum = 0;
_perspectiveX = 0;
_perspectiveY = 0;
@@ -438,6 +436,11 @@ void InsaneRebel1::updateShipPhysics() {
inputX = CLIP<int16>(inputX, -160, 160);
inputY = CLIP<int16>(inputY, -127, 127);
+ // Dead zone: ignore small offsets from center to prevent drift
+ const int16 kDeadZone = 8;
+ if (ABS(inputX) < kDeadZone) inputX = 0;
+ if (ABS(inputY) < kDeadZone) inputY = 0;
+
// --- Step 2: Scale to [-127, 127] (same as RA2: scaledInputX = inputX * 127 / 160) ---
int16 scaledInputX = (int16)((inputX * 127) / 160);
int16 scaledInputY = inputY;
@@ -453,20 +456,7 @@ void InsaneRebel1::updateShipPhysics() {
velSum += _velocityHistory[i];
_smoothedVelocity = (int16)(velSum / smoothWindow);
- // --- Step 4: Wind history ---
- for (int i = 14; i > 0; i--)
- _windHistoryX[i] = _windHistoryX[i - 1];
- _windHistoryX[0] = _windParamX;
-
- for (int i = 14; i > 0; i--)
- _windHistoryY[i] = _windHistoryY[i - 1];
- _windHistoryY[0] = _windParamY;
-
- // Wind effect (multiplier = 0 for now, no level data)
- int16 windEffectX = 0;
- int16 windEffectY = 0;
-
- // --- Step 5: Position delta ---
+ // --- Step 4: Position delta ---
const int16 levelSpeed = 32;
const int16 levelYSpeed = 48;
int16 absSmoothVel = ABS(_smoothedVelocity);
@@ -475,29 +465,41 @@ void InsaneRebel1::updateShipPhysics() {
if (_flyControlMode == 1) {
// Mode 1: Full cross-axis coupling
if (scaledInputX < 1)
- positionDeltaX = (int16)((levelSpeed * _smoothedVelocity - absSmoothVel * scaledInputY - windEffectX) >> 9);
+ positionDeltaX = (int16)((levelSpeed * _smoothedVelocity - absSmoothVel * scaledInputY) >> 9);
else
- positionDeltaX = (int16)((levelSpeed * _smoothedVelocity + absSmoothVel * scaledInputY - windEffectX) >> 9);
+ positionDeltaX = (int16)((levelSpeed * _smoothedVelocity + absSmoothVel * scaledInputY) >> 9);
} else {
// Mode 0/2/3: Reduced cross-axis coupling
if (scaledInputX < 1)
- positionDeltaX = (int16)((levelSpeed * _smoothedVelocity - (absSmoothVel * scaledInputY >> 2) - windEffectX) >> 9);
+ positionDeltaX = (int16)((levelSpeed * _smoothedVelocity - (absSmoothVel * scaledInputY >> 2)) >> 9);
else
- positionDeltaX = (int16)((levelSpeed * _smoothedVelocity + (absSmoothVel * scaledInputY >> 2) - windEffectX) >> 9);
+ positionDeltaX = (int16)((levelSpeed * _smoothedVelocity + (absSmoothVel * scaledInputY >> 2)) >> 9);
}
+ // Original asm drift pipeline (0x1e3fc-0x1e5cb):
+ // xDelta = (random(200) - 100 - driftTuning * _driftParam) / 2
+ // accumX += xDelta (32-bit accumulator)
+ // shipDeltaX = accumX >> 8 (divide by 256 for pixel position)
+ // So drift contributes ~±0.2 px/frame. We approximate by accumulating and shifting.
+ const int16 kDriftTuning = 3; // TODO: load from per-difficulty/level tuning table
+ int16 driftBias = (int16)(_vm->_rnd.getRandomNumber(199) - 100 - kDriftTuning * _driftParam);
+ _driftAccum += driftBias >> 1;
+ _driftAccum = CLIP<int32>(_driftAccum, -0x8200, 0x8200);
+ positionDeltaX += (int16)(_driftAccum >> 8);
+
// Clamp X delta to ±12 per frame
positionDeltaX = CLIP<int16>(positionDeltaX, -12, 12);
_shipPosX += positionDeltaX;
- // Y delta
+ // Y delta (no drift in original assembly â field4 is unused)
+ int16 positionDeltaY;
if (_flyControlMode == 1) {
- int yDelta = (levelYSpeed * scaledInputY - (windEffectY >> 1)) >> 10;
- yDelta = CLIP(yDelta, -12, 12);
- _shipPosY -= (int16)yDelta;
+ positionDeltaY = (int16)((levelYSpeed * scaledInputY) >> 10);
+ positionDeltaY = CLIP<int16>(positionDeltaY, -12, 12);
} else {
- _shipPosY -= (int16)((levelYSpeed * scaledInputY) >> 10);
+ positionDeltaY = (int16)((levelYSpeed * scaledInputY) >> 10);
}
+ _shipPosY -= positionDeltaY;
_verticalInput = scaledInputY;
@@ -749,15 +751,14 @@ void InsaneRebel1::handleGameChunk(int32 subSize, Common::SeekableReadStream &b)
break;
case 0x07:
- // Per-frame metadata (param1=counter, param2=constant, param3/4=wind?)
+ // Per-frame corridor data: f1=frame index, f2=constant(788), f3=drift bias, f4=unused
+ // Original asm: drift bias * tuning "drift" param, combined with random turbulence
+ // f4 is never referenced in the original handler function
if (subSize >= 20) {
- uint32 param2 = b.readUint32BE();
- uint32 param3 = b.readUint32BE();
- uint32 param4 = b.readUint32BE();
- _windParamX = (int16)param3;
- _windParamY = (int16)param4;
- debug(7, "RA1 GAME 0x07: idx=%d val=%d wind=(%d,%d)",
- param1, param2, _windParamX, _windParamY);
+ b.readUint32BE(); // f2 (constant 788, unused in physics)
+ _driftParam = (int16)(int32)b.readUint32BE();
+ b.readUint32BE(); // f4 (unused in original assembly)
+ debug(7, "RA1 GAME 0x07: frame=%d driftParam=%d", param1, _driftParam);
}
break;
diff --git a/engines/scumm/insane/insane_rebel1.h b/engines/scumm/insane/insane_rebel1.h
index 0a475abd7ba..2bb88e26688 100644
--- a/engines/scumm/insane/insane_rebel1.h
+++ b/engines/scumm/insane/insane_rebel1.h
@@ -115,10 +115,12 @@ private:
int16 _smoothedVelocity; // Averaged horizontal velocity
int16 _verticalInput; // Stored vertical input component
int16 _velocityHistory[25]; // Horizontal velocity ring buffer
- int16 _windHistoryX[15]; // Wind X history buffer
- int16 _windHistoryY[15]; // Wind Y history buffer
- int16 _windParamX; // Wind X (from GAME opcode 0x07 sub-opcode 0)
- int16 _windParamY; // Wind Y (from GAME opcode 0x07 sub-opcode 0)
+
+ // Per-frame drift bias from GAME 0x07 field3 (multiplied by tuning "drift" param)
+ // Original pipeline: xDelta added to 32-bit accumulator, position = accum >> 8
+ // field4 is unused in the original assembly
+ int16 _driftParam;
+ int32 _driftAccum; // 32-bit drift accumulator (position = accum >> 8)
// Perspective view offsets
int16 _perspectiveX;
Commit: 4c5eda6d074bfd6833531d06e9e0e5cf2ee3c959
https://github.com/scummvm/scummvm/commit/4c5eda6d074bfd6833531d06e9e0e5cf2ee3c959
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:24+02:00
Commit Message:
SCUMM: RA1: Add basic damage handling
Changed paths:
engines/scumm/insane/insane_rebel1.cpp
engines/scumm/insane/insane_rebel1.h
diff --git a/engines/scumm/insane/insane_rebel1.cpp b/engines/scumm/insane/insane_rebel1.cpp
index d00b6f25bde..b9f43dabfa1 100644
--- a/engines/scumm/insane/insane_rebel1.cpp
+++ b/engines/scumm/insane/insane_rebel1.cpp
@@ -100,11 +100,15 @@ InsaneRebel1::InsaneRebel1(ScummEngine_v7 *scumm) : Insane(), _vm(scumm) {
_viewShift = 0;
_flyControlMode = 0;
- _hitCooldown = 0;
- _playerDamage = 0;
+ _health = kMaxHealth;
+ _lives = 3;
_score = 0;
- _pilots = 3;
+ _damageFlags = 0;
+ _damageCooldown = 0;
+ _deathTimer = 0;
+ _screenFlash = 0;
+ _frameCounter = 0;
// Audio
initAudio(11025);
@@ -422,9 +426,11 @@ void InsaneRebel1::procPostRendering(byte *renderBitmap, int32 codecparam, int32
// Velocity-based ship physics adapted from RA2 Handler 7 (FUN_40C3CC case 4).
// Mouse input â velocity history averaging â position delta â corridor collision â perspective.
void InsaneRebel1::updateShipPhysics() {
- // Decrement hit cooldown
- if (_hitCooldown > 0)
- _hitCooldown--;
+ _frameCounter++;
+
+ // Decrement cooldown
+ if (_damageCooldown > 0)
+ _damageCooldown--;
// --- Step 1: Mouse input as offset from screen center ---
// Use _vm->_mouse (0-319, 0-199 virtual screen coords), same as RA2.
@@ -508,27 +514,33 @@ void InsaneRebel1::updateShipPhysics() {
_shipPosY = CLIP<int16>(_shipPosY, kMinY, kMaxY);
// --- Step 7: Corridor collision (modes 0 and 2) ---
+ // From FUN_1C54D: wall contact sets damageFlags bits 0-3 (directional)
+ // and pushes velocity history to bounce away from the wall.
if (_flyControlMode == 0 || _flyControlMode == 2) {
if (_corridorRightX < _shipPosX) {
_shipPosX = _corridorRightX;
- if (_hitCooldown < 5) {
+ _damageFlags |= 0x02; // Right wall
+ if (_damageCooldown < 5) {
for (int i = 0; i < 25; i++)
_velocityHistory[i] = -127;
- _hitCooldown = 10;
}
}
if (_shipPosX < _corridorLeftX) {
_shipPosX = _corridorLeftX;
- if (_hitCooldown < 5) {
+ _damageFlags |= 0x04; // Left wall
+ if (_damageCooldown < 5) {
for (int i = 0; i < 25; i++)
_velocityHistory[i] = 127;
- _hitCooldown = 10;
}
}
- if (_corridorBottomY < _shipPosY)
+ if (_corridorBottomY < _shipPosY) {
_shipPosY = _corridorBottomY;
- if (_shipPosY < _corridorTopY)
+ _damageFlags |= 0x01; // Bottom wall
+ }
+ if (_shipPosY < _corridorTopY) {
_shipPosY = _corridorTopY;
+ _damageFlags |= 0x08; // Top wall
+ }
}
// --- Step 8: Perspective offsets ---
@@ -571,9 +583,42 @@ void InsaneRebel1::updateShipPhysics() {
_shipDirIndex = CLIP<int16>(vDir * 7 + hDir, 0, _shipBank.numSprites - 1);
- debug(7, "RA1 ship: pos=(%d,%d) vel=%d vIn=%d dx=%d dir=%d corridor=[%d,%d]-[%d,%d]",
+ // --- Step 10: Damage processing (from FUN_1DEB5 decompilation) ---
+ // damageFlags & 0x96 = bits 1,2,4,7 = wall collisions (0x16) + projectile hit (0x80)
+ if ((_damageFlags & 0x96) != 0 && _damageCooldown == 0 &&
+ _health >= 0 && _deathTimer <= 0) {
+ // Projectile hit (bit 7 = 0x80)
+ if (_damageFlags & 0x80)
+ _health -= kHeavyDamage;
+ // Wall collision (bits 1,2,4 = 0x16)
+ if (_damageFlags & 0x16)
+ _health -= kLightDamage;
+
+ if (_health < 0)
+ _deathTimer = kDeathTimerInit;
+
+ _damageCooldown = kDamageCooldownInit;
+ _screenFlash = 3;
+ }
+
+ // Death animation countdown
+ if (_health < 0 && _deathTimer > 0)
+ _deathTimer--;
+
+ // Health regeneration: +1 every 32 frames (from original asm)
+ if (_health >= 0 && _health < kMaxHealth && (_frameCounter & 0x1F) == 0)
+ _health++;
+
+ // Screen flash decay
+ if (_screenFlash > 0)
+ _screenFlash--;
+
+ // Clear per-frame damage flags
+ _damageFlags = 0;
+
+ debug(7, "RA1 ship: pos=(%d,%d) vel=%d vIn=%d dx=%d dir=%d health=%d corridor=[%d,%d]-[%d,%d]",
_shipPosX, _shipPosY, _smoothedVelocity, _verticalInput,
- positionDeltaX, _shipDirIndex,
+ positionDeltaX, _shipDirIndex, _health,
_corridorLeftX, _corridorTopY, _corridorRightX, _corridorBottomY);
}
@@ -628,25 +673,35 @@ void InsaneRebel1::renderHUD(byte *dst, int pitch, int width, int height) {
hudX, hudY, bar.width, bar.height);
}
- // Draw damage bar (green rectangle).
- // From the screenshot: bar starts around x=56, y=8 within the HUD sprite,
- // max width ~76 pixels, height ~5 pixels. Green = palette index ~0xA0.
+ // Draw health bar (from FUN_1BBCB decompilation).
+ // Bar starts at x=56, y=8 within HUD, max width ~76px, height ~5px.
+ // Color thresholds: green (>50%), yellow (25-50%), red (<25%).
{
int barMaxW = 76;
int barH = 5;
int barX = hudX + 56;
int barY = hudY + 8;
- int fillW = barMaxW - (barMaxW * _playerDamage / 255);
- if (fillW < 0) fillW = 0;
- if (fillW > barMaxW) fillW = barMaxW;
+ int damage = kMaxHealth - CLIP<int16>(_health, 0, kMaxHealth);
+ int fillW = barMaxW * damage / kMaxHealth;
+ fillW = CLIP(fillW, 0, barMaxW);
+
+ // Color based on damage level (matching original thresholds from FUN_1BBCB)
+ byte barColor;
+ if (_health > kMaxHealth / 2)
+ barColor = 0xA0; // Green â low damage
+ else if (_health > kMaxHealth / 4)
+ barColor = 0x2C; // Yellow â moderate damage
+ else
+ barColor = 0x30; // Red â critical
- // Find a green palette index â use 0xA0 (common green in RA1 palette)
- byte greenColor = 0xA0;
+ // Flash effect on damage
+ if (_screenFlash > 0)
+ barColor = 0xFF; // White flash
for (int iy = 0; iy < barH && barY + iy < height; iy++) {
byte *d = dst + (barY + iy) * pitch + barX;
for (int ix = 0; ix < fillW && barX + ix < width; ix++) {
- d[ix] = greenColor;
+ d[ix] = barColor;
}
}
}
@@ -776,12 +831,18 @@ void InsaneRebel1::handleGameChunk(int32 subSize, Common::SeekableReadStream &b)
break;
case 0x0E:
- // Secondary collision zone
+ // Secondary collision zone (FUN_1C6E9): AABB test, sets damageFlags bit 4 (0x10)
if (subSize >= 20) {
- uint32 param2 = b.readUint32BE();
- uint32 param3 = b.readUint32BE();
- uint32 param4 = b.readUint32BE();
- debug(7, "RA1 GAME 0x0E: params=(%d,%d,%d,%d)", param1, param2, param3, param4);
+ int16 zoneLeft = (int16)param1;
+ int16 zoneTop = (int16)b.readUint32BE();
+ int16 zoneRight = (int16)b.readUint32BE();
+ int16 zoneBottom = (int16)b.readUint32BE();
+ if (_shipPosX >= zoneLeft && _shipPosX <= zoneRight &&
+ _shipPosY >= zoneTop && _shipPosY <= zoneBottom) {
+ _damageFlags |= 0x10;
+ }
+ debug(7, "RA1 GAME 0x0E: zone=[%d,%d]-[%d,%d] flags=0x%02x",
+ zoneLeft, zoneTop, zoneRight, zoneBottom, _damageFlags);
}
break;
diff --git a/engines/scumm/insane/insane_rebel1.h b/engines/scumm/insane/insane_rebel1.h
index 2bb88e26688..745ee84c906 100644
--- a/engines/scumm/insane/insane_rebel1.h
+++ b/engines/scumm/insane/insane_rebel1.h
@@ -130,13 +130,23 @@ private:
// Control mode (from GAME opcode 0x5E)
int16 _flyControlMode;
- // Hit cooldown timer
- int16 _hitCooldown;
-
- // HUD state
- int16 _playerDamage; // 0-255, higher = more damage
- int _score;
- int _pilots; // Lives remaining
+ // Damage system (from Ghidra decompilation of FUN_1DEB5)
+ int16 _health; // 0x7560: current health (init=98, negative=dead, max=98)
+ int16 _lives; // 0x7562: remaining extra lives
+ int _score; // 0x7564: current score
+ byte _damageFlags; // 0x74D4: per-frame collision bitmask (cleared each frame)
+ int16 _damageCooldown; // 0x74D8: invulnerability timer (10 frames after hit)
+ int16 _deathTimer; // 0x756A: death animation countdown (30 on death)
+ int16 _screenFlash; // 0x7736: screen flash timer on hit
+ uint32 _frameCounter; // 0x7740: global frame counter
+
+ static const int16 kMaxHealth = 98;
+ static const int16 kDeathTimerInit = 30;
+ static const int16 kDamageCooldownInit = 10;
+
+ // Tuning damage values (TODO: load from data section per difficulty/level)
+ static const int16 kLightDamage = 5; // "wham" â wall/zone collision
+ static const int16 kHeavyDamage = 15; // "shot" â projectile hit
// Audio state (same structure as RA2)
static const int kMaxAudioTracks = 4;
Commit: c8264eba60d169a2c99f0b5eb9271c6d4a21d5dc
https://github.com/scummvm/scummvm/commit/c8264eba60d169a2c99f0b5eb9271c6d4a21d5dc
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:24+02:00
Commit Message:
SCUMM: RA1: Play additional videos
Changed paths:
engines/scumm/insane/insane_rebel1.cpp
engines/scumm/insane/insane_rebel1.h
engines/scumm/nut_renderer.cpp
engines/scumm/scumm.cpp
engines/scumm/smush/smush_player.cpp
engines/scumm/smush/smush_player.h
engines/scumm/smush/smush_player_ra2.cpp
diff --git a/engines/scumm/insane/insane_rebel1.cpp b/engines/scumm/insane/insane_rebel1.cpp
index b9f43dabfa1..8c2c3268879 100644
--- a/engines/scumm/insane/insane_rebel1.cpp
+++ b/engines/scumm/insane/insane_rebel1.cpp
@@ -109,6 +109,7 @@ InsaneRebel1::InsaneRebel1(ScummEngine_v7 *scumm) : Insane(), _vm(scumm) {
_deathTimer = 0;
_screenFlash = 0;
_frameCounter = 0;
+ _interactiveVideoActive = false;
// Audio
initAudio(11025);
@@ -127,12 +128,27 @@ InsaneRebel1::InsaneRebel1(ScummEngine_v7 *scumm) : Insane(), _vm(scumm) {
_smush_bencutNut = nullptr;
_smush_iconsNut = nullptr;
_smush_icons2Nut = nullptr;
+
+ _vm->_system->getEventManager()->getEventDispatcher()->registerObserver(this, 1, false);
}
InsaneRebel1::~InsaneRebel1() {
+ _vm->_system->getEventManager()->getEventDispatcher()->unregisterObserver(this);
terminateAudio();
}
+bool InsaneRebel1::notifyEvent(const Common::Event &event) {
+ if (event.type == Common::EVENT_KEYDOWN && event.kbd.keycode == Common::KEYCODE_ESCAPE) {
+ if (_player) {
+ debug("Rebel1: ESC pressed - skipping video");
+ _vm->_smushVideoShouldFinish = true;
+ return true;
+ }
+ }
+
+ return false;
+}
+
// ---------------------------------------------------------------------------
// Audio
// ---------------------------------------------------------------------------
@@ -409,7 +425,7 @@ void InsaneRebel1::procPreRendering(byte *renderBitmap) {
void InsaneRebel1::procPostRendering(byte *renderBitmap, int32 codecparam, int32 setupsan12,
int32 setupsan13, int32 curFrame, int32 maxFrame) {
- if (_shipBank.numSprites == 0 || !renderBitmap)
+ if (!_interactiveVideoActive || _shipBank.numSprites == 0 || !renderBitmap)
return;
int width = _player->_width;
@@ -862,21 +878,243 @@ void InsaneRebel1::handleGameChunk(int32 subSize, Common::SeekableReadStream &b)
}
}
-void InsaneRebel1::playLevel(int level) {
- loadLevelSprites(level);
+// ---------------------------------------------------------------------------
+// Game flow (matching original at 0x15597)
+// ---------------------------------------------------------------------------
+
+// Play a passive cinematic (no game callback, skippable).
+// Reuses RA2's pattern: reset handler, set cinematic flags, play video.
+void InsaneRebel1::playCinematic(const char *filename) {
+ debug(1, "InsaneRebel1::playCinematic('%s')", filename);
+ SmushPlayer *splayer = _vm->_splayer;
+ _player = splayer;
+ _interactiveVideoActive = false;
+ _vm->_smushVideoShouldFinish = false;
+ splayer->setCurVideoFlags(0x28); // Cinematic mode + buffer preserve
+ splayer->play(filename, 12);
+}
+
+void InsaneRebel1::clearVideoBuffer() {
+ if (_vm->_screenWidth <= 0 || _vm->_screenHeight <= 0)
+ return;
+
+ const int pixelCount = _vm->_screenWidth * _vm->_screenHeight;
+ byte *clearBuffer = (byte *)calloc(pixelCount, 1);
+ if (!clearBuffer)
+ return;
+
+ if (_vm->_macScreen) {
+ _vm->mac_drawBufferToScreen(clearBuffer, _vm->_screenWidth, 0, 0, _vm->_screenWidth, _vm->_screenHeight);
+ } else {
+ _vm->_system->copyRectToScreen(clearBuffer, _vm->_screenWidth, 0, 0, _vm->_screenWidth, _vm->_screenHeight);
+ }
+ _vm->_system->updateScreen();
+
+ free(clearBuffer);
+}
+
+// Intro sequence (0x155ef-0x158f8):
+// 1. O1LOGO.ANM â LucasArts logo
+// 2. O1OPEN.ANM â Star Wars opening crawl
+void InsaneRebel1::playIntroSequence() {
+ debug(1, "InsaneRebel1: Playing intro sequence");
+
+ // LucasArts logo (original: PUSH 0x57cc, CALL FUN_1BA32 with flags 0x0420)
+ playCinematic("OPEN/O1LOGO.ANM");
+ if (_vm->shouldQuit())
+ return;
+ clearVideoBuffer();
+
+ // Star Wars opening crawl (original: PUSH 0x5800, CALL FUN_1BA32)
+ playCinematic("OPEN/O1OPEN.ANM");
+}
+
+// Main menu on O1OPTION.ANM background (0x15968).
+// Original renders text overlay with 5 menu items via FUN_21F7A.
+// For now, we play the menu video as a passive cinematic (non-interactive)
+// and return "Start New Game" immediately.
+// TODO: Implement interactive menu with keyboard/mouse selection.
+int InsaneRebel1::runMainMenu() {
+ debug(1, "InsaneRebel1: Main menu");
+
+ // Play menu background video
+ playCinematic("OPEN/O1OPTION.ANM");
+
+ if (_vm->shouldQuit())
+ return 5; // Exit
+
+ // TODO: Render menu items overlay:
+ // "MAIN MENU" (x=160, y=30)
+ // "START NEW GAME" (x=160, y=60)
+ // "GAME OPTIONS" (x=160, y=75)
+ // "ENTER PASSCODE" (x=160, y=90)
+ // "CONTINUE DEMO" (x=160, y=105)
+ // "EXIT TO DOS" (x=160, y=120)
+ // For now, auto-select "Start New Game"
+ return 1;
+}
+
+// Level 1 flow (0x16100-0x16737):
+// 1. Load NUTs (L1BANK1, L1BANK2, L1EXPLD, L1BANG, L1LASER)
+// 2. L1HANGAR.ANM â Hangar departure cutscene
+// 3. "Chapter 1" text
+// 4. L1CU1.ANM â Pre-flight cutscene
+// 5. L1PLAY1L.ANM â Stage 1 gameplay (left path) â INTERACTIVE
+// 6. L1PLAY1R.ANM â Stage 1 gameplay (right path) â INTERACTIVE
+// 7. L1CU2.ANM â Mid-level cutscene
+// 8. L1PLAY2.ANM â Stage 2 turret â INTERACTIVE
+// 9. L1END.ANM â Level end cutscene
+// Death: L1CRASHA/B.ANM â L1DEATH.ANM â L1RETRY.ANM â retry from L1NEW
+bool InsaneRebel1::runLevel1() {
+ debug(1, "InsaneRebel1: Running level 1");
+
+ // Load level sprites (original: pushes L1BANK1..L1BANG NUT filenames)
+ loadLevelSprites(1);
+
+ // L1HANGAR.ANM â Hangar departure intro (original: 0x5918, flags 0x0420)
+ playCinematic("LVL1/L1HANGAR.ANM");
+ if (_vm->shouldQuit())
+ return false;
+
+ // L1CU1.ANM â Pre-flight cutscene (original: 0x5944, flags 0x0400)
+ playCinematic("LVL1/L1CU1.ANM");
+ if (_vm->shouldQuit())
+ return false;
+
+ // Retry loop
+ while (!_vm->shouldQuit()) {
+ // Reset health for this attempt
+ _health = kMaxHealth;
+ _damageFlags = 0;
+ _damageCooldown = 0;
+ _deathTimer = 0;
+ _screenFlash = 0;
+ _frameCounter = 0;
+
+ // L1PLAY1L.ANM â Stage 1 gameplay (left path, original: 0x5953)
+ playInteractiveVideo("LVL1/L1PLAY1L.ANM");
+ if (_vm->shouldQuit())
+ return false;
+
+ if (_health >= 0) {
+ // Survived stage 1 â continue to right path
+ // L1PLAY1R.ANM (original: 0x5965, flags with seekframe=0x187)
+ playInteractiveVideo("LVL1/L1PLAY1R.ANM");
+ if (_vm->shouldQuit())
+ return false;
+
+ if (_health >= 0) {
+ // L1CU2.ANM â Mid-level cutscene (original: 0x5977)
+ playCinematic("LVL1/L1CU2.ANM");
+ if (_vm->shouldQuit())
+ return false;
+
+ // L1PLAY2.ANM â Stage 2 turret (original: 0x5986)
+ playInteractiveVideo("LVL1/L1PLAY2.ANM");
+ if (_vm->shouldQuit())
+ return false;
+
+ // L1END.ANM â Level complete! (original: 0x59a3)
+ playCinematic("LVL1/L1END.ANM");
+ return true;
+ }
+ }
+
+ // Death sequence (original: 0x165e8-0x16737)
+ // Random crash variant A or B
+ if (_vm->_rnd.getRandomNumber(1) == 0)
+ playCinematic("LVL1/L1CRASHA.ANM");
+ else
+ playCinematic("LVL1/L1CRASHB.ANM");
+ if (_vm->shouldQuit())
+ return false;
+
+ // L1DEATH.ANM (original: 0x5a4b)
+ playCinematic("LVL1/L1DEATH.ANM");
+ if (_vm->shouldQuit())
+ return false;
+
+ _lives--;
+ if (_lives <= 0) {
+ // Game over â no more retries
+ debug(1, "InsaneRebel1: Game over (no lives left)");
+ return false;
+ }
+
+ // L1RETRY.ANM â Retry prompt (original: 0x5a5c)
+ // After retry, original jumps back to L1NEWâL1PLAY1L (0x16214â0x16680)
+ playCinematic("LVL1/L1RETRY.ANM");
+ if (_vm->shouldQuit())
+ return false;
+
+ // L1NEW.ANM â Briefing before retry (original: 0x5a3c at retry path 0x16680)
+ playCinematic("LVL1/L1NEW.ANM");
+ if (_vm->shouldQuit())
+ return false;
+
+ // Loop back to gameplay
+ }
+
+ return false;
+}
+
+// Main game entry point â called from ScummEngine::go().
+// Matches original flow at 0x15597: intro â menu â level.
+void InsaneRebel1::runGame() {
+ // Play intro sequence (logo + opening)
+ playIntroSequence();
+ if (_vm->shouldQuit())
+ return;
+
+ // Main menu â gameplay loop
+ while (!_vm->shouldQuit()) {
+ int menuResult = runMainMenu();
+ if (_vm->shouldQuit())
+ return;
+
+ switch (menuResult) {
+ case 1: {
+ // Start New Game â play L1NEW briefing then level 1
+ playCinematic("LVL1/L1NEW.ANM");
+ if (_vm->shouldQuit())
+ return;
+
+ bool completed = runLevel1();
+ if (completed) {
+ debug(1, "InsaneRebel1: Level 1 completed!");
+ // TODO: Continue to level 2
+ }
+ // Return to menu after level ends
+ break;
+ }
+ case 5:
+ // Exit
+ return;
+ default:
+ // Options, Passcode, Demo â not yet implemented, return to menu
+ break;
+ }
+ }
+}
- Common::String filename = Common::String::format("LVL%d/L%dPLAY1L.ANM", level, level);
- debug(1, "InsaneRebel1::playLevel(%d): playing %s", level, filename.c_str());
+// Play interactive gameplay video (with ship physics + HUD).
+void InsaneRebel1::playInteractiveVideo(const char *filename) {
+ debug(1, "InsaneRebel1::playInteractiveVideo('%s')", filename);
SmushPlayer *splayer = _vm->_splayer;
_player = splayer;
+ clearBit(0);
+ _interactiveVideoActive = true;
+ _vm->_smushVideoShouldFinish = false;
+ splayer->setCurVideoFlags(0x28);
// Center mouse, hide cursor, and lock mouse to window (like RA2 flight)
smush_warpMouse(160, 100, -1);
CursorMan.showMouse(false);
g_system->lockMouse(true);
- splayer->play(filename.c_str(), 12);
+ splayer->play(filename, 12);
+ _interactiveVideoActive = false;
g_system->lockMouse(false);
}
diff --git a/engines/scumm/insane/insane_rebel1.h b/engines/scumm/insane/insane_rebel1.h
index 745ee84c906..2c07cfb9e31 100644
--- a/engines/scumm/insane/insane_rebel1.h
+++ b/engines/scumm/insane/insane_rebel1.h
@@ -24,6 +24,7 @@
#include "audio/audiostream.h"
#include "audio/mixer.h"
+#include "common/events.h"
#include "scumm/insane/insane.h"
namespace Scumm {
@@ -54,11 +55,13 @@ struct RA1SpriteBank {
* Star Wars: Rebel Assault (RA1) game logic.
* Adapts RA2 Handler 7 (ship flight) physics for RA1's 384x242 resolution.
*/
-class InsaneRebel1 : public Insane {
+class InsaneRebel1 : public Insane, public Common::EventObserver {
public:
InsaneRebel1(ScummEngine_v7 *scumm);
~InsaneRebel1() override;
+ bool notifyEvent(const Common::Event &event) override;
+
void procPreRendering(byte *renderBitmap) override;
void procPostRendering(byte *renderBitmap, int32 codecparam, int32 setupsan12,
int32 setupsan13, int32 curFrame, int32 maxFrame) override;
@@ -69,9 +72,27 @@ public:
void handleGameChunk(int32 subSize, Common::SeekableReadStream &b);
- void playLevel(int level);
+ // Game flow (matching original at 0x15597)
+ void runGame();
private:
+ // Intro sequence: O1LOGO â O1OPEN (0x155ef-0x158f8)
+ void playIntroSequence();
+ void clearVideoBuffer();
+
+ // Main menu loop on O1OPTION.ANM background (0x15968)
+ // Returns: 1=Start New Game, 2=Game Options, 3=Enter Passcode, 4=Continue Demo, 5=Exit
+ int runMainMenu();
+
+ // Level 1 flow (0x16100): hangar â CU1 â gameplay â CU2 â turret â end
+ // Returns true if level completed, false if player quit
+ bool runLevel1();
+
+ // Play a passive cinematic (no game callback, skippable)
+ void playCinematic(const char *filename);
+
+ // Play interactive gameplay video (with ship physics + HUD)
+ void playInteractiveVideo(const char *filename);
bool loadRA1Nut(const char *filename, RA1SpriteBank &bank);
void loadLevelSprites(int level);
void updateShipPhysics();
@@ -154,6 +175,9 @@ private:
Audio::SoundHandle _audioHandles[kMaxAudioTracks];
bool _audioTrackActive[kMaxAudioTracks];
int _audioSampleRate;
+
+ // True only while an interactive gameplay SMUSH is running.
+ bool _interactiveVideoActive;
};
} // End of namespace Scumm
diff --git a/engines/scumm/nut_renderer.cpp b/engines/scumm/nut_renderer.cpp
index 8fd02c39e1d..8b16fbd99c2 100644
--- a/engines/scumm/nut_renderer.cpp
+++ b/engines/scumm/nut_renderer.cpp
@@ -143,19 +143,38 @@ void NutRenderer::loadFont(const char *filename) {
// whole of the undecoded font file.
_numChars = READ_LE_UINT16(dataSrc + 10);
- assert(_numChars <= ARRAYSIZE(_chars));
+ if (_numChars > ARRAYSIZE(_chars)) {
+ warning("NutRenderer::loadFont(%s) numChars (%d) exceeds max, clamping", filename, _numChars);
+ _numChars = ARRAYSIZE(_chars);
+ }
uint32 offset = 0;
uint32 decodedLength = 0;
int l;
for (l = 0; l < _numChars; l++) {
- offset += READ_BE_UINT32(dataSrc + offset + 4) + 16;
+ if (offset + 8 > length) {
+ warning("NutRenderer::loadFont(%s) truncated before char %d (offset %x), clamping", filename, l, offset);
+ break;
+ }
+ uint32 chunkSize = READ_BE_UINT32(dataSrc + offset + 4);
+ uint64 nextOffset = (uint64)offset + chunkSize + 16 + (chunkSize & 1);
+ if (nextOffset + 18 > length) {
+ warning("NutRenderer::loadFont(%s) font chunk exceeds file at char %d (offset %x), clamping", filename, l, offset);
+ break;
+ }
+ offset = (uint32)nextOffset;
int width = READ_LE_UINT16(dataSrc + offset + 14);
_fontHeight = READ_LE_UINT16(dataSrc + offset + 16);
decodedLength += width * _fontHeight;
}
+ if (l < _numChars)
+ _numChars = l;
+
+ if (_numChars <= 0 || decodedLength == 0)
+ error("NutRenderer::loadFont(%s) no decodable characters", filename);
+
debug(1, "NutRenderer::loadFont('%s') - decodedLength = %d", filename, decodedLength);
_decodedData = new byte[decodedLength];
@@ -163,14 +182,28 @@ void NutRenderer::loadFont(const char *filename) {
offset = 0;
for (l = 0; l < _numChars; l++) {
- offset += READ_BE_UINT32(dataSrc + offset + 4) + 8;
+ if (offset + 8 > length) {
+ warning("NutRenderer::loadFont(%s) invalid font chunk header %d (offset %x), stopping decode", filename, l, offset);
+ break;
+ }
+ uint32 chunkSize = READ_BE_UINT32(dataSrc + offset + 4);
+ uint64 nextOffset = (uint64)offset + chunkSize + 8 + (chunkSize & 1);
+ if (nextOffset + 8 > length) {
+ warning("NutRenderer::loadFont(%s) FRME chunk exceeds file %d (offset %x), stopping decode", filename, l, offset);
+ break;
+ }
+ offset = (uint32)nextOffset;
if (READ_BE_UINT32(dataSrc + offset) != MKTAG('F','R','M','E')) {
- error("NutRenderer::loadFont(%s) there is no FRME chunk %d (offset %x)", filename, l, offset);
+ warning("NutRenderer::loadFont(%s) no FRME chunk %d (offset %x), stopping decode", filename, l, offset);
break;
}
offset += 8;
+ if (offset + 22 > length) {
+ warning("NutRenderer::loadFont(%s) FOBJ chunk exceeds file %d (offset %x), stopping decode", filename, l, offset);
+ break;
+ }
if (READ_BE_UINT32(dataSrc + offset) != MKTAG('F','O','B','J')) {
- error("NutRenderer::loadFont(%s) there is no FOBJ chunk in FRME chunk %d (offset %x)", filename, l, offset);
+ warning("NutRenderer::loadFont(%s) no FOBJ chunk in FRME chunk %d (offset %x), stopping decode", filename, l, offset);
break;
}
int codec = READ_LE_UINT16(dataSrc + offset + 8);
@@ -258,9 +291,11 @@ void NutRenderer::loadFontFromData(const byte *data, int32 dataSize) {
for (l = 0; l < _numChars; l++) {
if (offset + 8 > length)
break;
- offset += READ_BE_UINT32(dataSrc + offset + 4) + 16;
- if (offset + 18 > length)
+ uint32 chunkSize = READ_BE_UINT32(dataSrc + offset + 4);
+ uint64 nextOffset = (uint64)offset + chunkSize + 16 + (chunkSize & 1);
+ if (nextOffset + 18 > length)
break;
+ offset = (uint32)nextOffset;
int width = READ_LE_UINT16(dataSrc + offset + 14);
_fontHeight = READ_LE_UINT16(dataSrc + offset + 16);
decodedLength += width * _fontHeight;
@@ -275,7 +310,11 @@ void NutRenderer::loadFontFromData(const byte *data, int32 dataSize) {
for (l = 0; l < _numChars; l++) {
if (offset + 8 > length)
break;
- offset += READ_BE_UINT32(dataSrc + offset + 4) + 8;
+ uint32 chunkSize = READ_BE_UINT32(dataSrc + offset + 4);
+ uint64 nextOffset = (uint64)offset + chunkSize + 8 + (chunkSize & 1);
+ if (nextOffset + 8 > length)
+ break;
+ offset = (uint32)nextOffset;
if (offset + 8 > length)
break;
if (READ_BE_UINT32(dataSrc + offset) != MKTAG('F','R','M','E')) {
diff --git a/engines/scumm/scumm.cpp b/engines/scumm/scumm.cpp
index 102fb518908..e609f134749 100644
--- a/engines/scumm/scumm.cpp
+++ b/engines/scumm/scumm.cpp
@@ -1809,6 +1809,9 @@ void ScummEngine_v7::setupScumm(const Common::Path &macResourceFile) {
_sound = new Sound(this, _mixer, false);
_musicEngine = _imuseDigital = nullptr;
+ _res->allocResTypeData(rtBuffer, 0, 10, kDynamicResTypeMode);
+ initScreens(0, 200);
+
_insane = new InsaneRebel1(this);
_splayer = new SmushPlayer(this, nullptr, _insane);
@@ -2710,7 +2713,7 @@ Common::Error ScummEngine::go() {
if (_game.id == GID_REBEL1) {
ScummEngine_v7 *vm7 = (ScummEngine_v7 *)this;
InsaneRebel1 *rebel = (InsaneRebel1 *)vm7->getInsane();
- rebel->playLevel(1);
+ rebel->runGame();
return Common::kNoError;
}
diff --git a/engines/scumm/smush/smush_player.cpp b/engines/scumm/smush/smush_player.cpp
index 782c2ffd926..56fd68776a0 100644
--- a/engines/scumm/smush/smush_player.cpp
+++ b/engines/scumm/smush/smush_player.cpp
@@ -1025,7 +1025,7 @@ void SmushPlayer::decodeFrameObject(int codec, const uint8 *src, int left, int t
int origHeight = height;
int srcSkipY = 0;
- if (isRA2()) {
+ if (isRA1() || isRA2()) {
ra2AdjustFrameCoords(left, top, width, height, pitch, &srcSkipY);
if (width <= 0 || height <= 0)
return;
@@ -1034,7 +1034,7 @@ void SmushPlayer::decodeFrameObject(int codec, const uint8 *src, int left, int t
// For RLE codecs, skip source rows that were clipped from the top.
// Each RLE row has a 2-byte size prefix, so we can advance past them.
const uint8 *adjustedSrc = src;
- if (srcSkipY > 0 && (codec == SMUSH_CODEC_RLE || codec == SMUSH_CODEC_RLE_ALT)) {
+ if (isRA1() && srcSkipY > 0 && (codec == SMUSH_CODEC_RLE || codec == SMUSH_CODEC_RLE_ALT)) {
for (int i = 0; i < srcSkipY; i++) {
adjustedSrc += READ_LE_UINT16(adjustedSrc) + 2;
}
@@ -1125,8 +1125,8 @@ void SmushPlayer::handleZlibFrameObject(int32 subSize, Common::SeekableReadStrea
byte *ptr = fobjBuffer;
int codec = READ_LE_UINT16(ptr); ptr += 2;
- int left = isRA2() ? (int16)READ_LE_UINT16(ptr) : (int)READ_LE_UINT16(ptr); ptr += 2;
- int top = isRA2() ? (int16)READ_LE_UINT16(ptr) : (int)READ_LE_UINT16(ptr); ptr += 2;
+ int left = (int16)READ_LE_UINT16(ptr); ptr += 2;
+ int top = (int16)READ_LE_UINT16(ptr); ptr += 2;
int width = READ_LE_UINT16(ptr); ptr += 2;
int height = READ_LE_UINT16(ptr); ptr += 2;
@@ -1151,8 +1151,8 @@ void SmushPlayer::handleFrameObject(int32 subSize, Common::SeekableReadStream &b
ra1Param = (codec >> 8) & 0xFF; // byte[1] = palette base (e.g. 0xF0)
codec &= 0xFF; // byte[0] = actual codec number
}
- int left = isRA2() ? (int)b.readSint16LE() : (int)b.readUint16LE();
- int top = isRA2() ? (int)b.readSint16LE() : (int)b.readUint16LE();
+ int left = (int)b.readSint16LE();
+ int top = (int)b.readSint16LE();
int width = b.readUint16LE();
int height = b.readUint16LE();
@@ -1343,10 +1343,11 @@ void SmushPlayer::handleAnimHeader(int32 subSize, Common::SeekableReadStream &b)
}
if (isRA1()) {
- // v1 AHDR: offset 4-5 is not width/height, palette starts at offset 6
- // RA1 resolution is always 384x242, set from first FOBJ
- _width = 384;
- _height = 242;
+ // v1 AHDR: offset 4-5 is not width/height, palette starts at offset 6.
+ // Keep dimensions unset here and let FOBJ decoding select buffer and
+ // effective width/height (screen-sized or 384x242 special buffer).
+ _width = 0;
+ _height = 0;
} else {
_width = READ_LE_UINT16(&headerContent[4]);
_height = READ_LE_UINT16(&headerContent[6]);
@@ -1395,6 +1396,8 @@ SmushFont *SmushPlayer::getFont(int font) {
}
} else if (isRA2()) {
return ra2GetFont(font);
+ } else if (isRA1()) {
+ return ra1GetFont(font);
} else {
int numFonts = (_vm->_game.id == GID_CMI && !(_vm->_game.features & GF_DEMO)) ? 5 : 4;
assert(font >= 0 && font < numFonts);
@@ -1407,6 +1410,43 @@ SmushFont *SmushPlayer::getFont(int font) {
return _sf[font];
}
+SmushFont *SmushPlayer::ra1GetFont(int font) {
+ const char *ra1Fonts[] = {
+ "SYS/TALKFONT.NUT",
+ "SYS/TECHFONT.NUT",
+ "SYS/TITLFONT.NUT",
+ "SYS/DISPLAY.NUT"
+ };
+ const char *ra2FallbackFonts[] = {
+ "SYSTM/TALKFONT.NUT",
+ "SYSTM/SMALFONT.NUT",
+ "SYSTM/TITLFONT.NUT",
+ "SYSTM/SMALFONT.NUT"
+ };
+
+ int numFonts = ARRAYSIZE(ra1Fonts);
+ if (font < 0 || font >= numFonts) {
+ debugC(DEBUG_SMUSH, "SmushPlayer::ra1GetFont: unknown font %d, using TALKFONT", font);
+ font = 0;
+ }
+
+ if (_sf[font])
+ return _sf[font];
+
+ const char *fontPath = ra1Fonts[font];
+ ScummFile *testFile = _vm->instantiateScummFile();
+ bool ok = _vm->openFile(*testFile, Common::Path(fontPath));
+ if (ok)
+ testFile->close();
+ delete testFile;
+
+ if (!ok)
+ fontPath = ra2FallbackFonts[font];
+
+ _sf[font] = new SmushFont(_vm, fontPath, true);
+ return _sf[font];
+}
+
void SmushPlayer::parseNextFrame() {
if (_seekPos >= 0) {
@@ -1714,13 +1754,36 @@ void SmushPlayer::play(const char *filename, int32 speed, int32 offset, int32 st
if (!skipFrame) {
// WORKAROUND for bug #2415: "FT DEMO: assertion triggered
// when playing movie". Some frames there are 384 x 224
- int frameWidth = MIN(_width, _vm->_screenWidth);
- int frameHeight = MIN(_height, _vm->_screenHeight);
+ int frameWidth;
+ int frameHeight;
+ const byte *dst;
+
+ if (isRA1()) {
+ if (_dst == nullptr || _width <= 0 || _height <= 0) {
+ _updateNeeded = false;
+ continue;
+ }
+
+ const int srcX = CLIP(_scrollX, 0, _width - 1);
+ const int srcY = CLIP(_scrollY, 0, _height - 1);
- const byte *dst = _dst + _scrollY * _width + _scrollX;
+ frameWidth = MIN(_width - srcX, _vm->_screenWidth);
+ frameHeight = MIN(_height - srcY, _vm->_screenHeight);
+ if (frameWidth <= 0 || frameHeight <= 0) {
+ _updateNeeded = false;
+ continue;
+ }
+
+ dst = _dst + srcY * _width + srcX;
+ } else {
+ frameWidth = MIN(_width, _vm->_screenWidth);
+ frameHeight = MIN(_height, _vm->_screenHeight);
+ dst = _dst + _scrollY * _width + _scrollX;
+ }
if (_vm->_macScreen) {
- _vm->mac_drawBufferToScreen(dst, frameWidth, 0, 0, frameWidth, frameHeight);
+ int srcPitch = isRA1() ? _width : frameWidth;
+ _vm->mac_drawBufferToScreen(dst, srcPitch, 0, 0, frameWidth, frameHeight);
} else {
_vm->_system->copyRectToScreen(dst, _width, 0, 0, frameWidth, frameHeight);
}
diff --git a/engines/scumm/smush/smush_player.h b/engines/scumm/smush/smush_player.h
index 3d43b97fb51..a2454d93546 100644
--- a/engines/scumm/smush/smush_player.h
+++ b/engines/scumm/smush/smush_player.h
@@ -328,6 +328,7 @@ private:
int left, int top, int width, int height);
void ra2HandleGost(int32 subSize, Common::SeekableReadStream &b);
void ra2ResetDeltaPalette();
+ SmushFont *ra1GetFont(int font);
SmushFont *ra2GetFont(int font);
void ra2ParseNextFrame();
void ra2FixupAnimHeader();
diff --git a/engines/scumm/smush/smush_player_ra2.cpp b/engines/scumm/smush/smush_player_ra2.cpp
index 0bfe0e2ae8b..176dc179b42 100644
--- a/engines/scumm/smush/smush_player_ra2.cpp
+++ b/engines/scumm/smush/smush_player_ra2.cpp
@@ -548,8 +548,8 @@ void SmushPlayer::clearMaskedRegions() {
}
void SmushPlayer::setScrollOffset(int x, int y) {
- _scrollX = x;
- _scrollY = y;
+ _scrollX = MAX(0, x);
+ _scrollY = MAX(0, y);
}
} // End of namespace Scumm
Commit: 3073b37e7988883a21223c3493f3058d407af70b
https://github.com/scummvm/scummvm/commit/3073b37e7988883a21223c3493f3058d407af70b
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:24+02:00
Commit Message:
SCUMM: RA1: Improve UI
Changed paths:
engines/scumm/insane/insane_rebel1.cpp
engines/scumm/insane/insane_rebel1.h
diff --git a/engines/scumm/insane/insane_rebel1.cpp b/engines/scumm/insane/insane_rebel1.cpp
index 8c2c3268879..3d0abd34496 100644
--- a/engines/scumm/insane/insane_rebel1.cpp
+++ b/engines/scumm/insane/insane_rebel1.cpp
@@ -29,6 +29,7 @@
#include "graphics/wincursor.h"
#include "scumm/scumm_v7.h"
#include "scumm/scumm.h"
+#include "scumm/nut_renderer.h"
#include "scumm/smush/smush_player.h"
#include "scumm/insane/insane_rebel1.h"
@@ -105,11 +106,36 @@ InsaneRebel1::InsaneRebel1(ScummEngine_v7 *scumm) : Insane(), _vm(scumm) {
_lives = 3;
_score = 0;
_damageFlags = 0;
+ _gameLatch5D = 0;
+ _gameLatch5F = 0;
_damageCooldown = 0;
_deathTimer = 0;
_screenFlash = 0;
_frameCounter = 0;
_interactiveVideoActive = false;
+ _hudFont = nullptr;
+ const char *hudFontCandidates[] = {
+ "SYS/TECHFONT.NUT",
+ "SYS/TALKFONT.NUT",
+ "SYS/SMALFONT.NUT",
+ "SYSTM/SMALFONT.NUT"
+ };
+ for (uint i = 0; i < ARRAYSIZE(hudFontCandidates); i++) {
+ ScummFile *fontFile = _vm->instantiateScummFile();
+ _vm->openFile(*fontFile, hudFontCandidates[i]);
+ const bool found = fontFile->isOpen();
+ if (found)
+ fontFile->close();
+ delete fontFile;
+
+ if (found) {
+ _hudFont = new NutRenderer(_vm, hudFontCandidates[i]);
+ debug(1, "InsaneRebel1: HUD font loaded from %s", hudFontCandidates[i]);
+ break;
+ }
+ }
+ if (!_hudFont)
+ warning("InsaneRebel1: no HUD font found (TECHFONT/TALKFONT/SMALFONT), HUD numbers disabled");
// Audio
initAudio(11025);
@@ -134,6 +160,7 @@ InsaneRebel1::InsaneRebel1(ScummEngine_v7 *scumm) : Insane(), _vm(scumm) {
InsaneRebel1::~InsaneRebel1() {
_vm->_system->getEventManager()->getEventDispatcher()->unregisterObserver(this);
+ delete _hudFont;
terminateAudio();
}
@@ -458,11 +485,6 @@ void InsaneRebel1::updateShipPhysics() {
inputX = CLIP<int16>(inputX, -160, 160);
inputY = CLIP<int16>(inputY, -127, 127);
- // Dead zone: ignore small offsets from center to prevent drift
- const int16 kDeadZone = 8;
- if (ABS(inputX) < kDeadZone) inputX = 0;
- if (ABS(inputY) < kDeadZone) inputY = 0;
-
// --- Step 2: Scale to [-127, 127] (same as RA2: scaledInputX = inputX * 127 / 160) ---
int16 scaledInputX = (int16)((inputX * 127) / 160);
int16 scaledInputY = inputY;
@@ -599,7 +621,16 @@ void InsaneRebel1::updateShipPhysics() {
_shipDirIndex = CLIP<int16>(vDir * 7 + hDir, 0, _shipBank.numSprites - 1);
- // --- Step 10: Damage processing (from FUN_1DEB5 decompilation) ---
+ // --- Step 10: Damage/event bit synthesis + damage processing ---
+ // RA1 FUN_1B297-style latches from GAME opcodes:
+ // 0x5D latch 0xFFFF -> bit 0x40 (obstacle/contact)
+ // 0x5F non-zero + RNG -> bit 0x80 (projectile-like hit)
+ if (_gameLatch5D == 0xFFFF)
+ _damageFlags |= 0x40;
+ if (_gameLatch5F != 0 && _vm->_rnd.getRandomNumber((uint16)(_gameLatch5F - 1)) == 0)
+ _damageFlags |= 0x80;
+
+ // Damage guard/mask from FUN_1DEB5: (_damageFlags & 0x96) != 0
// damageFlags & 0x96 = bits 1,2,4,7 = wall collisions (0x16) + projectile hit (0x80)
if ((_damageFlags & 0x96) != 0 && _damageCooldown == 0 &&
_health >= 0 && _deathTimer <= 0) {
@@ -617,13 +648,21 @@ void InsaneRebel1::updateShipPhysics() {
_screenFlash = 3;
}
+ // Latches are per-frame event inputs in the original pipeline.
+ _gameLatch5D = 0;
+ _gameLatch5F = 0;
+
// Death animation countdown
if (_health < 0 && _deathTimer > 0)
_deathTimer--;
// Health regeneration: +1 every 32 frames (from original asm)
- if (_health >= 0 && _health < kMaxHealth && (_frameCounter & 0x1F) == 0)
- _health++;
+ if ((_frameCounter & 0x1F) == 0) {
+ if (_health >= 0 && _health < kMaxHealth)
+ _health++;
+ if (_health >= 0)
+ _score += 1;
+ }
// Screen flash decay
if (_screenFlash > 0)
@@ -689,23 +728,22 @@ void InsaneRebel1::renderHUD(byte *dst, int pitch, int width, int height) {
hudX, hudY, bar.width, bar.height);
}
- // Draw health bar (from FUN_1BBCB decompilation).
- // Bar starts at x=56, y=8 within HUD, max width ~76px, height ~5px.
- // Color thresholds: green (>50%), yellow (25-50%), red (<25%).
+ // Draw health bar from FUN_1BBCB behavior.
+ // Original logic uses current health as fill width and computes x as (0x92 - health),
+ // so the bar is right-anchored and shrinks from left to right as damage increases.
{
- int barMaxW = 76;
+ int barMaxW = kMaxHealth;
int barH = 5;
- int barX = hudX + 56;
+ int healthWidth = CLIP<int16>(_health, 0, kMaxHealth);
+ int barX = hudX + (0x92 - healthWidth);
int barY = hudY + 8;
- int damage = kMaxHealth - CLIP<int16>(_health, 0, kMaxHealth);
- int fillW = barMaxW * damage / kMaxHealth;
- fillW = CLIP(fillW, 0, barMaxW);
+ int fillW = CLIP(healthWidth, 0, barMaxW);
// Color based on damage level (matching original thresholds from FUN_1BBCB)
byte barColor;
- if (_health > kMaxHealth / 2)
+ if (_health > kHeavyDamage * 2)
barColor = 0xA0; // Green â low damage
- else if (_health > kMaxHealth / 4)
+ else if (_health > kLightDamage * 2)
barColor = 0x2C; // Yellow â moderate damage
else
barColor = 0x30; // Red â critical
@@ -722,46 +760,50 @@ void InsaneRebel1::renderHUD(byte *dst, int pitch, int width, int height) {
}
}
- // Draw score as decimal digits.
- // From screenshot: score area starts around x=265 within HUD, using palette text color.
- // For now, just draw simple 4Ã7 digit bitmaps.
- {
- char scoreStr[8];
- snprintf(scoreStr, sizeof(scoreStr), "%06d", _score);
- int digitX = hudX + 265;
- int digitY = hudY + 7;
- byte textColor = 0xFF; // White
-
- for (int c = 0; c < 6 && scoreStr[c]; c++) {
- int digit = scoreStr[c] - '0';
- // Simple 3Ã5 digit rendering
- static const uint16 digitPatterns[10] = {
- 0x7B6F, // 0: 111 011 011 011 111
- 0x2492, // 1: 010 010 010 010 010
- 0x73E7, // 2: 111 001 111 100 111
- 0x73CF, // 3: 111 001 111 001 111
- 0x5BC9, // 4: 101 101 111 001 001
- 0x7E3F, // 5: 111 110 111 001 111
- 0x7E7F, // 6: 111 110 111 101 111
- 0x7249, // 7: 111 001 001 001 001
- 0x7FFF, // 8: 111 111 111 111 111 (simplified)
- 0x7FCF, // 9: 111 111 111 001 111
- };
- if (digit < 0 || digit > 9) digit = 0;
- uint16 pat = digitPatterns[digit];
- for (int py = 0; py < 5; py++) {
- for (int px = 0; px < 3; px++) {
- int bit = 14 - (py * 3 + px);
- if (pat & (1 << bit)) {
- int sx = digitX + c * 5 + px;
- int sy = digitY + py;
- if (sx >= 0 && sx < width && sy >= 0 && sy < height)
- dst[sy * pitch + sx] = textColor;
+ // Draw lives and score using the RA1 small NUT font (RA2-style glyph blit).
+ if (_hudFont && _hudFont->getNumChars() > 0) {
+ auto drawHudString = [&](const char *text, int x, int y) {
+ for (int i = 0; text[i] != '\0'; i++) {
+ byte ch = (byte)text[i];
+ if (ch >= _hudFont->getNumChars()) {
+ x += 4;
+ continue;
+ }
+
+ const byte *glyph = _hudFont->getCharData(ch);
+ int gw = _hudFont->getCharWidth(ch);
+ int gh = _hudFont->getCharHeight(ch);
+ int gx = x + _hudFont->getCharXOffset(ch);
+ int gy = y + _hudFont->getCharYOffset(ch);
+
+ if (glyph && gw > 0 && gh > 0) {
+ for (int py = 0; py < gh; py++) {
+ int sy = gy + py;
+ if (sy < 0 || sy >= height)
+ continue;
+ for (int px = 0; px < gw; px++) {
+ int sx = gx + px;
+ if (sx < 0 || sx >= width)
+ continue;
+ byte pixel = glyph[py * gw + px];
+ if (pixel != 0)
+ dst[sy * pitch + sx] = pixel;
+ }
}
}
+ x += gw > 0 ? gw : 4;
}
- }
+ };
+
+ char livesStr[8];
+ Common::sprintf_s(livesStr, "%d", MAX<int>(_lives, 0));
+ drawHudString(livesStr, hudX + 180, hudY + 6);
+
+ char scoreStr[16];
+ Common::sprintf_s(scoreStr, "%07d", MAX<int>(_score, 0));
+ drawHudString(scoreStr, hudX + 257, hudY + 4);
}
+
}
void InsaneRebel1::renderSprite(byte *dst, int pitch, int width, int height,
@@ -808,17 +850,39 @@ void InsaneRebel1::handleGameChunk(int32 subSize, Common::SeekableReadStream &b)
switch (opcode) {
case 0x5E:
- // Mode control
- _flyControlMode = (int16)param1;
- debug(5, "RA1 GAME 0x5E: flyControlMode=%d", _flyControlMode);
+ // RA1 dispatcher inline reset/init path (FUN_1BE1B case 0x5E).
+ // This is not a pure control-mode assignment.
+ _damageFlags = 0;
+ _damageCooldown = 0;
+ _deathTimer = 0;
+ _screenFlash = 0;
+ _gameLatch5D = 0;
+ _gameLatch5F = 0;
+ _driftParam = 0;
+ _driftAccum = 0;
+ _smoothedVelocity = 0;
+ _verticalInput = 0;
+ memset(_velocityHistory, 0, sizeof(_velocityHistory));
+
+ // Field1 == 0 corresponds to baseline recenter behavior in the original.
+ if ((int32)param1 == 0) {
+ _shipPosX = kCenterX;
+ _shipPosY = kCenterY;
+ }
+
+ // Keep a conservative default mode after reset.
+ _flyControlMode = 0;
+ debug(5, "RA1 GAME 0x5E: reset state field1=%d", (int32)param1);
break;
case 0x5D:
- debug(5, "RA1 GAME 0x5D (link) param=%d", param1);
+ _gameLatch5D = (uint16)param1;
+ debug(5, "RA1 GAME 0x5D (link/event latch) param=%u", _gameLatch5D);
break;
case 0x5F:
- debug(5, "RA1 GAME 0x5F (event) param=%d", param1);
+ _gameLatch5F = (uint16)param1;
+ debug(5, "RA1 GAME 0x5F (random-hit latch) param=%u", _gameLatch5F);
break;
case 0x07:
diff --git a/engines/scumm/insane/insane_rebel1.h b/engines/scumm/insane/insane_rebel1.h
index 2c07cfb9e31..9b033ed2689 100644
--- a/engines/scumm/insane/insane_rebel1.h
+++ b/engines/scumm/insane/insane_rebel1.h
@@ -31,6 +31,7 @@ namespace Scumm {
class ScummEngine_v7;
class SmushPlayer;
+class NutRenderer;
// Simple sprite bank for RA1 NUT files (ANIM v1 with odd-alignment padding).
// Separate from NutRenderer to avoid modifying shared NUT parsing code.
@@ -113,6 +114,7 @@ private:
RA1SpriteBank _shipBank;
RA1SpriteBank _displayBank; // SYS/DISPLAY.NUT â bottom status bar
+ NutRenderer *_hudFont; // SMALFONT.NUT (SYS or SYSTM) â numeric HUD text (score/lives)
// RA1 screen dimensions (384x242)
int _screenWidth;
@@ -156,6 +158,8 @@ private:
int16 _lives; // 0x7562: remaining extra lives
int _score; // 0x7564: current score
byte _damageFlags; // 0x74D4: per-frame collision bitmask (cleared each frame)
+ uint16 _gameLatch5D; // 0x75D2: GAME 0x5D latch (scene/obstacle/event trigger)
+ uint16 _gameLatch5F; // 0x75D4: GAME 0x5F latch (probabilistic hit trigger)
int16 _damageCooldown; // 0x74D8: invulnerability timer (10 frames after hit)
int16 _deathTimer; // 0x756A: death animation countdown (30 on death)
int16 _screenFlash; // 0x7736: screen flash timer on hit
Commit: 7fee7db76e4fac47c3b741057fde8d5fd4bb9e1d
https://github.com/scummvm/scummvm/commit/7fee7db76e4fac47c3b741057fde8d5fd4bb9e1d
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:25+02:00
Commit Message:
SCUMM: RA1: Add menu
Changed paths:
engines/scumm/insane/insane_rebel1.cpp
engines/scumm/insane/insane_rebel1.h
diff --git a/engines/scumm/insane/insane_rebel1.cpp b/engines/scumm/insane/insane_rebel1.cpp
index 3d0abd34496..2c998ab7f1a 100644
--- a/engines/scumm/insane/insane_rebel1.cpp
+++ b/engines/scumm/insane/insane_rebel1.cpp
@@ -29,7 +29,6 @@
#include "graphics/wincursor.h"
#include "scumm/scumm_v7.h"
#include "scumm/scumm.h"
-#include "scumm/nut_renderer.h"
#include "scumm/smush/smush_player.h"
#include "scumm/insane/insane_rebel1.h"
@@ -113,29 +112,16 @@ InsaneRebel1::InsaneRebel1(ScummEngine_v7 *scumm) : Insane(), _vm(scumm) {
_screenFlash = 0;
_frameCounter = 0;
_interactiveVideoActive = false;
- _hudFont = nullptr;
- const char *hudFontCandidates[] = {
- "SYS/TECHFONT.NUT",
- "SYS/TALKFONT.NUT",
- "SYS/SMALFONT.NUT",
- "SYSTM/SMALFONT.NUT"
- };
- for (uint i = 0; i < ARRAYSIZE(hudFontCandidates); i++) {
- ScummFile *fontFile = _vm->instantiateScummFile();
- _vm->openFile(*fontFile, hudFontCandidates[i]);
- const bool found = fontFile->isOpen();
- if (found)
- fontFile->close();
- delete fontFile;
-
- if (found) {
- _hudFont = new NutRenderer(_vm, hudFontCandidates[i]);
- debug(1, "InsaneRebel1: HUD font loaded from %s", hudFontCandidates[i]);
- break;
- }
+ _menuActive = false;
+ _menuConfirmed = false;
+ _menuSelection = 0;
+ if (loadRA1Nut("SYS/TALKFONT.NUT", _hudFontBank)) {
+ debug(1, "InsaneRebel1: HUD/menu glyph font loaded from SYS/TALKFONT.NUT (%d chars)", _hudFontBank.numSprites);
+ } else if (loadRA1Nut("SYS/TECHFONT.NUT", _hudFontBank)) {
+ debug(1, "InsaneRebel1: HUD/menu glyph font loaded from SYS/TECHFONT.NUT (%d chars)", _hudFontBank.numSprites);
+ } else {
+ warning("InsaneRebel1: failed to load RA1 HUD font bank (TECHFONT/TALKFONT)");
}
- if (!_hudFont)
- warning("InsaneRebel1: no HUD font found (TECHFONT/TALKFONT/SMALFONT), HUD numbers disabled");
// Audio
initAudio(11025);
@@ -160,11 +146,45 @@ InsaneRebel1::InsaneRebel1(ScummEngine_v7 *scumm) : Insane(), _vm(scumm) {
InsaneRebel1::~InsaneRebel1() {
_vm->_system->getEventManager()->getEventDispatcher()->unregisterObserver(this);
- delete _hudFont;
terminateAudio();
}
bool InsaneRebel1::notifyEvent(const Common::Event &event) {
+ if (_menuActive && event.type == Common::EVENT_KEYDOWN) {
+ switch (event.kbd.keycode) {
+ case Common::KEYCODE_UP:
+ case Common::KEYCODE_w:
+ _menuSelection = (_menuSelection + 4) % 5;
+ return true;
+ case Common::KEYCODE_DOWN:
+ case Common::KEYCODE_s:
+ _menuSelection = (_menuSelection + 1) % 5;
+ return true;
+ case Common::KEYCODE_RETURN:
+ case Common::KEYCODE_KP_ENTER:
+ case Common::KEYCODE_SPACE:
+ _menuConfirmed = true;
+ _vm->_smushVideoShouldFinish = true;
+ return true;
+ case Common::KEYCODE_1:
+ case Common::KEYCODE_2:
+ case Common::KEYCODE_3:
+ case Common::KEYCODE_4:
+ case Common::KEYCODE_5:
+ _menuSelection = event.kbd.keycode - Common::KEYCODE_1;
+ _menuConfirmed = true;
+ _vm->_smushVideoShouldFinish = true;
+ return true;
+ case Common::KEYCODE_ESCAPE:
+ _menuSelection = 4;
+ _menuConfirmed = true;
+ _vm->_smushVideoShouldFinish = true;
+ return true;
+ default:
+ break;
+ }
+ }
+
if (event.type == Common::EVENT_KEYDOWN && event.kbd.keycode == Common::KEYCODE_ESCAPE) {
if (_player) {
debug("Rebel1: ESC pressed - skipping video");
@@ -380,63 +400,103 @@ bool InsaneRebel1::loadRA1Nut(const char *filename, RA1SpriteBank &bank) {
return false;
}
- bank.numSprites = READ_LE_UINT16(data + 10);
+ const uint16 expectedSprites = READ_LE_UINT16(data + 10);
+ bank.numSprites = expectedSprites;
bank.sprites = new RA1Sprite[bank.numSprites];
+ memset(bank.sprites, 0, sizeof(RA1Sprite) * bank.numSprites);
- // Pass 1: Walk chunks with alignment to compute total decoded size.
- uint32 offset = 0;
+ uint32 *fobjOffsets = (uint32 *)calloc(expectedSprites, sizeof(uint32));
+ if (!fobjOffsets) {
+ free(data);
+ return false;
+ }
+
+ // Pass 1: Parse ANIM chunks properly and collect FRME->FOBJ offsets in-order.
uint32 decodedSize = 0;
- for (int i = 0; i < bank.numSprites; i++) {
- // Skip current chunk (AHDR or previous FRME)
- uint32 chunkSize = READ_BE_UINT32(data + offset + 4);
- offset += chunkSize + 8;
- if (chunkSize & 1) offset++; // Word-align
-
- // Now at FRME; skip its header to reach FOBJ
- offset += 8;
- if (offset + 22 > animSize) break;
-
- uint16 w = READ_LE_UINT16(data + offset + 14);
- uint16 h = READ_LE_UINT16(data + offset + 16);
- decodedSize += w * h;
+ uint16 foundSprites = 0;
+ uint32 chunkOffset = 0;
+ while (chunkOffset + 8 <= animSize && foundSprites < expectedSprites) {
+ uint32 chunkTag = READ_BE_UINT32(data + chunkOffset);
+ uint32 chunkSize = READ_BE_UINT32(data + chunkOffset + 4);
+ uint32 chunkDataOffset = chunkOffset + 8;
+ uint32 chunkEnd = chunkDataOffset + chunkSize;
+ if (chunkEnd > animSize)
+ break;
+
+ if (chunkTag == MKTAG('F','R','M','E')) {
+ bool foundFobj = false;
+ uint32 subOffset = chunkDataOffset;
+ while (subOffset + 8 <= chunkEnd) {
+ uint32 subTag = READ_BE_UINT32(data + subOffset);
+ uint32 subSize = READ_BE_UINT32(data + subOffset + 4);
+ uint32 subDataOffset = subOffset + 8;
+ uint32 subEnd = subDataOffset + subSize;
+ if (subEnd > chunkEnd)
+ break;
+
+ if (subTag == MKTAG('F','O','B','J') && subOffset + 22 <= animSize) {
+ uint16 w = READ_LE_UINT16(data + subOffset + 14);
+ uint16 h = READ_LE_UINT16(data + subOffset + 16);
+ decodedSize += (uint32)w * (uint32)h;
+ fobjOffsets[foundSprites] = subOffset;
+ foundFobj = true;
+ break;
+ }
+
+ subOffset = subEnd;
+ if (subSize & 1)
+ subOffset++;
+ }
+ // Always increment for every FRME to preserve char-to-glyph alignment.
+ // Empty FRMEs (no FOBJ) keep fobjOffsets[i] = 0, decoded as blank sprites.
+ foundSprites++;
+ }
+
+ chunkOffset = chunkEnd;
+ if (chunkSize & 1)
+ chunkOffset++;
}
- bank.decodedData = (byte *)calloc(decodedSize, 1);
+ bank.decodedData = (byte *)calloc(decodedSize ? decodedSize : 1, 1);
+ bank.decodedSize = decodedSize;
byte *decPtr = bank.decodedData;
- // Pass 2: Decode sprites.
- offset = 0;
- for (int i = 0; i < bank.numSprites; i++) {
- uint32 chunkSize = READ_BE_UINT32(data + offset + 4);
- offset += chunkSize + 8;
- if (chunkSize & 1) offset++;
-
- offset += 8; // Skip FRME header â now at FOBJ
- if (offset + 22 > animSize) break;
+ // Pass 2: Decode collected FOBJ entries.
+ for (uint16 i = 0; i < foundSprites; i++) {
+ uint32 fobjOffset = fobjOffsets[i];
+ if (fobjOffset == 0) {
+ // Empty FRME (no FOBJ) â leave sprite as blank (zeroed by memset).
+ continue;
+ }
- int codec = READ_LE_UINT16(data + offset + 8);
- bank.sprites[i].xoffs = READ_LE_INT16(data + offset + 10);
- bank.sprites[i].yoffs = READ_LE_INT16(data + offset + 12);
- bank.sprites[i].width = READ_LE_UINT16(data + offset + 14);
- bank.sprites[i].height = READ_LE_UINT16(data + offset + 16);
- bank.sprites[i].data = decPtr;
+ int codec = READ_LE_UINT16(data + fobjOffset + 8);
+ bank.sprites[i].xoffs = READ_LE_INT16(data + fobjOffset + 10);
+ bank.sprites[i].yoffs = READ_LE_INT16(data + fobjOffset + 12);
+ bank.sprites[i].width = READ_LE_UINT16(data + fobjOffset + 14);
+ bank.sprites[i].height = READ_LE_UINT16(data + fobjOffset + 16);
int pixelCount = bank.sprites[i].width * bank.sprites[i].height;
- const byte *fobjData = data + offset + 22;
+ const byte *fobjData = data + fobjOffset + 22;
if (codec == 21) {
+ bank.sprites[i].data = decPtr;
decodeBomp(decPtr, fobjData, bank.sprites[i].width,
bank.sprites[i].height, bank.sprites[i].width);
} else {
+ bank.sprites[i].width = 0;
+ bank.sprites[i].height = 0;
+ bank.sprites[i].data = nullptr;
warning("InsaneRebel1::loadRA1Nut: unsupported codec %d in sprite %d", codec, i);
}
decPtr += pixelCount;
}
+ free(fobjOffsets);
+
free(data);
- debug(1, "InsaneRebel1::loadRA1Nut('%s'): %d sprites, %d bytes decoded",
- filename, bank.numSprites, decodedSize);
+ debug(1, "InsaneRebel1::loadRA1Nut('%s'): expected=%d found=%d decoded=%d bytes",
+ filename, expectedSprites, foundSprites, decodedSize);
return true;
}
@@ -451,6 +511,14 @@ void InsaneRebel1::procPreRendering(byte *renderBitmap) {
void InsaneRebel1::procPostRendering(byte *renderBitmap, int32 codecparam, int32 setupsan12,
int32 setupsan13, int32 curFrame, int32 maxFrame) {
+ if (_menuActive && renderBitmap) {
+ int width = _player ? _player->_width : 0;
+ int height = _player ? _player->_height : 0;
+ if (width == 0) width = _screenWidth;
+ if (height == 0) height = _screenHeight;
+ int pitch = width;
+ renderMainMenuOverlay(renderBitmap, pitch, width, height);
+ }
if (!_interactiveVideoActive || _shipBank.numSprites == 0 || !renderBitmap)
return;
@@ -466,6 +534,105 @@ void InsaneRebel1::procPostRendering(byte *renderBitmap, int32 codecparam, int32
renderHUD(renderBitmap, pitch, width, height);
}
+void InsaneRebel1::drawFontBankString(byte *dst, int pitch, int width, int height, int x, int y, const char *text) {
+ if (!dst || !text || _hudFontBank.numSprites <= 0)
+ return;
+
+ for (int i = 0; text[i] != '\0'; i++) {
+ const byte ch = (byte)text[i];
+
+ if (ch == ' ') {
+ x += 6;
+ continue;
+ }
+
+ // RA1 font renderer indexes printable characters from '!' (0x21), not raw ASCII.
+ if (ch < 0x21) {
+ x += 4;
+ continue;
+ }
+ const int fontIdx = (int)ch - 0x21;
+ if (fontIdx < 0 || fontIdx >= _hudFontBank.numSprites) {
+ x += 4;
+ continue;
+ }
+
+ const RA1Sprite &glyph = _hudFontBank.sprites[fontIdx];
+ const int gw = glyph.width;
+ const int gh = glyph.height;
+ const int gx = x + glyph.xoffs;
+ const int gy = y + glyph.yoffs;
+ const uint64 glyphPixels = (uint64)gw * (uint64)gh;
+ if (!glyph.data || gw <= 0 || gh <= 0 || glyphPixels == 0 || glyphPixels > 0x10000) {
+ x += 4;
+ continue;
+ }
+ if (!(_hudFontBank.decodedData && _hudFontBank.decodedSize > 0)) {
+ x += 4;
+ continue;
+ }
+ const byte *bankStart = _hudFontBank.decodedData;
+ const byte *bankEnd = _hudFontBank.decodedData + _hudFontBank.decodedSize;
+ if (glyph.data < bankStart || glyph.data >= bankEnd || glyph.data + glyphPixels > bankEnd) {
+ x += 4;
+ continue;
+ }
+
+ for (int py = 0; py < gh; py++) {
+ const int sy = gy + py;
+ if (sy < 0 || sy >= height)
+ continue;
+ for (int px = 0; px < gw; px++) {
+ const int sx = gx + px;
+ if (sx < 0 || sx >= width)
+ continue;
+ const byte pixel = glyph.data[py * gw + px];
+ if (pixel != 0)
+ dst[sy * pitch + sx] = pixel;
+ }
+ }
+
+ x += gw > 0 ? gw : 4;
+ }
+}
+
+void InsaneRebel1::renderMainMenuOverlay(byte *dst, int pitch, int width, int height) {
+ static const char *kMenuItems[5] = {
+ "START NEW GAME",
+ "GAME OPTIONS",
+ "ENTER PASSCODE",
+ "CONTINUE DEMO",
+ "EXIT"
+ };
+
+ const int menuX = 92;
+ const int menuY = 60;
+ const int rowH = 16;
+ const int boxW = 190;
+
+ for (int i = 0; i < 5; i++) {
+ const int y = menuY + i * rowH;
+ if (i == _menuSelection) {
+ for (int yy = 0; yy < 12; yy++) {
+ const int sy = y + yy;
+ if (sy < 0 || sy >= height)
+ continue;
+ for (int xx = 0; xx < boxW; xx++) {
+ const int sx = menuX + xx;
+ if (sx < 0 || sx >= width)
+ continue;
+ if (xx < 2 || yy < 2 || xx >= boxW - 2 || yy >= 10)
+ dst[sy * pitch + sx] = 0xFF;
+ }
+ }
+ }
+
+ drawFontBankString(dst, pitch, width, height, menuX + 10, y + 1, kMenuItems[i]);
+ }
+
+ drawFontBankString(dst, pitch, width, height, 118, 36, "MAIN MENU");
+}
+
// Velocity-based ship physics adapted from RA2 Handler 7 (FUN_40C3CC case 4).
// Mouse input â velocity history averaging â position delta â corridor collision â perspective.
void InsaneRebel1::updateShipPhysics() {
@@ -760,48 +927,15 @@ void InsaneRebel1::renderHUD(byte *dst, int pitch, int width, int height) {
}
}
- // Draw lives and score using the RA1 small NUT font (RA2-style glyph blit).
- if (_hudFont && _hudFont->getNumChars() > 0) {
- auto drawHudString = [&](const char *text, int x, int y) {
- for (int i = 0; text[i] != '\0'; i++) {
- byte ch = (byte)text[i];
- if (ch >= _hudFont->getNumChars()) {
- x += 4;
- continue;
- }
-
- const byte *glyph = _hudFont->getCharData(ch);
- int gw = _hudFont->getCharWidth(ch);
- int gh = _hudFont->getCharHeight(ch);
- int gx = x + _hudFont->getCharXOffset(ch);
- int gy = y + _hudFont->getCharYOffset(ch);
-
- if (glyph && gw > 0 && gh > 0) {
- for (int py = 0; py < gh; py++) {
- int sy = gy + py;
- if (sy < 0 || sy >= height)
- continue;
- for (int px = 0; px < gw; px++) {
- int sx = gx + px;
- if (sx < 0 || sx >= width)
- continue;
- byte pixel = glyph[py * gw + px];
- if (pixel != 0)
- dst[sy * pitch + sx] = pixel;
- }
- }
- }
- x += gw > 0 ? gw : 4;
- }
- };
-
+ // Draw lives and score from DISPLAY.NUT glyphs.
+ if (_hudFontBank.numSprites > 0) {
char livesStr[8];
Common::sprintf_s(livesStr, "%d", MAX<int>(_lives, 0));
- drawHudString(livesStr, hudX + 180, hudY + 6);
+ drawFontBankString(dst, pitch, width, height, hudX + 180, hudY + 6, livesStr);
char scoreStr[16];
Common::sprintf_s(scoreStr, "%07d", MAX<int>(_score, 0));
- drawHudString(scoreStr, hudX + 257, hudY + 4);
+ drawFontBankString(dst, pitch, width, height, hudX + 257, hudY + 4, scoreStr);
}
}
@@ -1001,21 +1135,21 @@ void InsaneRebel1::playIntroSequence() {
int InsaneRebel1::runMainMenu() {
debug(1, "InsaneRebel1: Main menu");
- // Play menu background video
- playCinematic("OPEN/O1OPTION.ANM");
+ _menuSelection = 0;
+ while (!_vm->shouldQuit()) {
+ _menuActive = true;
+ _menuConfirmed = false;
+ playCinematic("OPEN/O1OPTION.ANM");
+ _menuActive = false;
+
+ if (_vm->shouldQuit())
+ return 5;
- if (_vm->shouldQuit())
- return 5; // Exit
-
- // TODO: Render menu items overlay:
- // "MAIN MENU" (x=160, y=30)
- // "START NEW GAME" (x=160, y=60)
- // "GAME OPTIONS" (x=160, y=75)
- // "ENTER PASSCODE" (x=160, y=90)
- // "CONTINUE DEMO" (x=160, y=105)
- // "EXIT TO DOS" (x=160, y=120)
- // For now, auto-select "Start New Game"
- return 1;
+ if (_menuConfirmed)
+ return _menuSelection + 1;
+ }
+
+ return 5;
}
// Level 1 flow (0x16100-0x16737):
diff --git a/engines/scumm/insane/insane_rebel1.h b/engines/scumm/insane/insane_rebel1.h
index 9b033ed2689..1be3e662bd6 100644
--- a/engines/scumm/insane/insane_rebel1.h
+++ b/engines/scumm/insane/insane_rebel1.h
@@ -31,7 +31,7 @@ namespace Scumm {
class ScummEngine_v7;
class SmushPlayer;
-class NutRenderer;
+class SmushFont;
// Simple sprite bank for RA1 NUT files (ANIM v1 with odd-alignment padding).
// Separate from NutRenderer to avoid modifying shared NUT parsing code.
@@ -47,8 +47,9 @@ struct RA1SpriteBank {
int numSprites;
RA1Sprite *sprites;
byte *decodedData; // Single allocation for all decoded pixels
+ uint32 decodedSize;
- RA1SpriteBank() : numSprites(0), sprites(nullptr), decodedData(nullptr) {}
+ RA1SpriteBank() : numSprites(0), sprites(nullptr), decodedData(nullptr), decodedSize(0) {}
~RA1SpriteBank() { delete[] sprites; free(decodedData); }
};
@@ -99,6 +100,8 @@ private:
void updateShipPhysics();
void renderShip(byte *dst, int pitch, int width, int height);
void renderHUD(byte *dst, int pitch, int width, int height);
+ void renderMainMenuOverlay(byte *dst, int pitch, int width, int height);
+ void drawFontBankString(byte *dst, int pitch, int width, int height, int x, int y, const char *text);
void renderSprite(byte *dst, int pitch, int width, int height,
int x, int y, const RA1Sprite &sprite);
@@ -114,7 +117,8 @@ private:
RA1SpriteBank _shipBank;
RA1SpriteBank _displayBank; // SYS/DISPLAY.NUT â bottom status bar
- NutRenderer *_hudFont; // SMALFONT.NUT (SYS or SYSTM) â numeric HUD text (score/lives)
+ RA1SpriteBank _hudFontBank; // RA1 HUD text glyphs (TECHFONT/TALKFONT via RA1 loader)
+ SmushFont *_menuFont; // Use engine text renderer for correct TALKFONT character mapping
// RA1 screen dimensions (384x242)
int _screenWidth;
@@ -182,6 +186,11 @@ private:
// True only while an interactive gameplay SMUSH is running.
bool _interactiveVideoActive;
+
+ // Main menu state (for O1OPTION interactive overlay)
+ bool _menuActive;
+ bool _menuConfirmed;
+ int _menuSelection; // 0..4 maps to return values 1..5
};
} // End of namespace Scumm
Commit: 50959ca0d3e40493a11769a3f81e26d54c0979ca
https://github.com/scummvm/scummvm/commit/50959ca0d3e40493a11769a3f81e26d54c0979ca
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:25+02:00
Commit Message:
SCUMM: RA1: Improve video decoding
Changed paths:
engines/scumm/insane/insane_rebel1.cpp
engines/scumm/insane/insane_rebel1.h
engines/scumm/smush/codec1.cpp
engines/scumm/smush/smush_player.cpp
engines/scumm/smush/smush_player.h
diff --git a/engines/scumm/insane/insane_rebel1.cpp b/engines/scumm/insane/insane_rebel1.cpp
index 2c998ab7f1a..068597e8c32 100644
--- a/engines/scumm/insane/insane_rebel1.cpp
+++ b/engines/scumm/insane/insane_rebel1.cpp
@@ -115,6 +115,7 @@ InsaneRebel1::InsaneRebel1(ScummEngine_v7 *scumm) : Insane(), _vm(scumm) {
_menuActive = false;
_menuConfirmed = false;
_menuSelection = 0;
+ _menuFrameCounter = 0;
if (loadRA1Nut("SYS/TALKFONT.NUT", _hudFontBank)) {
debug(1, "InsaneRebel1: HUD/menu glyph font loaded from SYS/TALKFONT.NUT (%d chars)", _hudFontBank.numSprites);
} else if (loadRA1Nut("SYS/TECHFONT.NUT", _hudFontBank)) {
@@ -596,41 +597,97 @@ void InsaneRebel1::drawFontBankString(byte *dst, int pitch, int width, int heigh
}
}
+// getFontBankStringWidth -- Measure pixel width of a string using the HUD font bank.
+// Matches the pre-pass width calculation in the original drawString (FUN_221B7).
+int InsaneRebel1::getFontBankStringWidth(const char *text) {
+ if (!text || _hudFontBank.numSprites <= 0)
+ return 0;
+
+ int w = 0;
+ for (int i = 0; text[i] != '\0'; i++) {
+ const byte ch = (byte)text[i];
+ if (ch == ' ') {
+ w += 6;
+ continue;
+ }
+ if (ch < 0x21) {
+ w += 4;
+ continue;
+ }
+ const int fontIdx = (int)ch - 0x21;
+ if (fontIdx < 0 || fontIdx >= _hudFontBank.numSprites) {
+ w += 4;
+ continue;
+ }
+ const RA1Sprite &glyph = _hudFontBank.sprites[fontIdx];
+ w += glyph.width > 0 ? glyph.width : 4;
+ }
+ return w;
+}
+
+// renderMainMenuOverlay -- Draw menu text and selection highlight box.
+// Original menu strings from assault_data_3.bin at 0x5822.
+// Highlight uses RA2-style flashing border box (FUN_004292d0 pattern).
void InsaneRebel1::renderMainMenuOverlay(byte *dst, int pitch, int width, int height) {
static const char *kMenuItems[5] = {
"START NEW GAME",
"GAME OPTIONS",
"ENTER PASSCODE",
"CONTINUE DEMO",
- "EXIT"
+ "EXIT TO DOS"
};
- const int menuX = 92;
+ _menuFrameCounter++;
+
+ // Center title
+ const int titleW = getFontBankStringWidth("MAIN MENU");
+ const int titleX = (width - titleW) / 2;
+ drawFontBankString(dst, pitch, width, height, titleX, 36, "MAIN MENU");
+
+ // Draw menu items centered horizontally
const int menuY = 60;
const int rowH = 16;
- const int boxW = 190;
for (int i = 0; i < 5; i++) {
+ const int textW = getFontBankStringWidth(kMenuItems[i]);
+ const int textX = (width - textW) / 2;
const int y = menuY + i * rowH;
+
+ drawFontBankString(dst, pitch, width, height, textX, y + 1, kMenuItems[i]);
+
+ // Selection highlight box â flashing border (FUN_004292d0 pattern from RA2)
if (i == _menuSelection) {
- for (int yy = 0; yy < 12; yy++) {
- const int sy = y + yy;
- if (sy < 0 || sy >= height)
- continue;
- for (int xx = 0; xx < boxW; xx++) {
- const int sx = menuX + xx;
- if (sx < 0 || sx >= width)
- continue;
- if (xx < 2 || yy < 2 || xx >= boxW - 2 || yy >= 10)
- dst[sy * pitch + sx] = 0xFF;
- }
+ // Flash between two palette colors every 8 frames
+ byte highlightColor = ((_menuFrameCounter / 8) & 1) ? 248 : 240;
+
+ int bracketWidth = textW + 12;
+ int bracketHeight = rowH;
+ int leftX = textX - 6;
+ int rightX = leftX + bracketWidth;
+ int topY = y - 1;
+ int bottomY = y + bracketHeight - 2;
+
+ // Clamp
+ if (leftX < 0) leftX = 0;
+ if (rightX >= width) rightX = width - 1;
+ if (topY < 0) topY = 0;
+ if (bottomY >= height) bottomY = height - 1;
+
+ // Draw rectangle border (4 lines)
+ for (int x = leftX; x <= rightX && x < width; x++) {
+ if (topY >= 0 && topY < height)
+ dst[topY * pitch + x] = highlightColor;
+ if (bottomY >= 0 && bottomY < height)
+ dst[bottomY * pitch + x] = highlightColor;
+ }
+ for (int py = topY; py <= bottomY && py < height; py++) {
+ if (leftX >= 0 && leftX < width)
+ dst[py * pitch + leftX] = highlightColor;
+ if (rightX >= 0 && rightX < width)
+ dst[py * pitch + rightX] = highlightColor;
}
}
-
- drawFontBankString(dst, pitch, width, height, menuX + 10, y + 1, kMenuItems[i]);
}
-
- drawFontBankString(dst, pitch, width, height, 118, 36, "MAIN MENU");
}
// Velocity-based ship physics adapted from RA2 Handler 7 (FUN_40C3CC case 4).
@@ -1139,6 +1196,8 @@ int InsaneRebel1::runMainMenu() {
while (!_vm->shouldQuit()) {
_menuActive = true;
_menuConfirmed = false;
+ _menuFrameCounter = 0;
+ clearVideoBuffer();
playCinematic("OPEN/O1OPTION.ANM");
_menuActive = false;
diff --git a/engines/scumm/insane/insane_rebel1.h b/engines/scumm/insane/insane_rebel1.h
index 1be3e662bd6..16f1b4c14a5 100644
--- a/engines/scumm/insane/insane_rebel1.h
+++ b/engines/scumm/insane/insane_rebel1.h
@@ -102,6 +102,7 @@ private:
void renderHUD(byte *dst, int pitch, int width, int height);
void renderMainMenuOverlay(byte *dst, int pitch, int width, int height);
void drawFontBankString(byte *dst, int pitch, int width, int height, int x, int y, const char *text);
+ int getFontBankStringWidth(const char *text);
void renderSprite(byte *dst, int pitch, int width, int height,
int x, int y, const RA1Sprite &sprite);
@@ -191,6 +192,7 @@ private:
bool _menuActive;
bool _menuConfirmed;
int _menuSelection; // 0..4 maps to return values 1..5
+ int _menuFrameCounter;
};
} // End of namespace Scumm
diff --git a/engines/scumm/smush/codec1.cpp b/engines/scumm/smush/codec1.cpp
index 087e4d53aaf..4ad3dd455db 100644
--- a/engines/scumm/smush/codec1.cpp
+++ b/engines/scumm/smush/codec1.cpp
@@ -36,4 +36,47 @@ void smushDecodeRLE(byte *dst, const byte *src, int left, int top, int width, in
} while (--height);
}
+/**
+ * RA1 codec 1: RLE with transparency on pixel 0.
+ * Same BOMP encoding as smushDecodeRLE but pixel value 0 is not written,
+ * allowing the background (restored via FTCH) to show through.
+ * Matches FFmpeg's old_codec1() with opaque=0.
+ */
+void smushDecodeRA1Transparent(byte *dst, const byte *src, int left, int top, int width, int height, int pitch) {
+ dst += top * pitch;
+ do {
+ byte *rowDst = dst + left;
+ const byte *lineData = src + 2;
+ int remaining = width;
+
+ while (remaining > 0) {
+ byte code = *lineData++;
+ byte num = (code >> 1) + 1;
+ if (num > remaining)
+ num = remaining;
+ if (code & 1) {
+ // Fill: repeat single byte
+ byte color = *lineData++;
+ if (color != 0) {
+ memset(rowDst, color, num);
+ }
+ // If color == 0: skip (transparent)
+ } else {
+ // Copy: write each byte, skipping 0
+ for (int j = 0; j < num; j++) {
+ byte c = lineData[j];
+ if (c != 0)
+ rowDst[j] = c;
+ }
+ lineData += num;
+ }
+ rowDst += num;
+ remaining -= num;
+ }
+
+ src += READ_LE_UINT16(src) + 2;
+ dst += pitch;
+ } while (--height);
+}
+
} // End of namespace Scumm
diff --git a/engines/scumm/smush/smush_player.cpp b/engines/scumm/smush/smush_player.cpp
index 56fd68776a0..68ca58682a2 100644
--- a/engines/scumm/smush/smush_player.cpp
+++ b/engines/scumm/smush/smush_player.cpp
@@ -395,6 +395,9 @@ void SmushPlayer::handleStore(int32 subSize, Common::SeekableReadStream &b) {
if (isRA2()) {
debug("SmushPlayer STOR: frame=%d - will store next FOBJ", _frame);
}
+ if (isRA1()) {
+ debug("RA1 STOR: frame=%d", _frame);
+ }
}
void SmushPlayer::handleFetch(int32 subSize, Common::SeekableReadStream &b) {
@@ -406,6 +409,11 @@ void SmushPlayer::handleFetch(int32 subSize, Common::SeekableReadStream &b) {
return;
}
+ if (isRA1()) {
+ debug("RA1 FTCH: frame=%d _frameBuffer=%p _dst=%p _width=%d _height=%d",
+ _frame, (void*)_frameBuffer, (void*)_dst, _width, _height);
+ }
+
if (_frameBuffer != nullptr) {
memcpy(_dst, _frameBuffer, _width * _height);
}
@@ -797,6 +805,10 @@ void SmushPlayer::handleDeltaPalette(int32 subSize, Common::SeekableReadStream &
_pal[i] = CLIP<int32>(_shiftedDeltaPal[i] >> 7, 0, 255);
}
+ if (isRA1()) {
+ _pal[0] = _pal[1] = _pal[2] = 0;
+ }
+
setDirtyColors(0, 255);
} else {
for (int j = 0; j < 768; ++j) {
@@ -819,6 +831,11 @@ void SmushPlayer::handleNewPalette(int32 subSize, Common::SeekableReadStream &b)
return;
readPalette(_pal, b);
+
+ if (isRA1()) {
+ _pal[0] = _pal[1] = _pal[2] = 0;
+ }
+
setDirtyColors(0, 255);
}
@@ -827,9 +844,91 @@ byte *SmushPlayer::getVideoPalette() {
}
void smushDecodeRLE(byte *dst, const byte *src, int left, int top, int width, int height, int pitch);
+void smushDecodeRA1Transparent(byte *dst, const byte *src, int left, int top, int width, int height, int pitch);
void smushDecodeUncompressed(byte *dst, const byte *src, int left, int top, int width, int height, int pitch);
void smushDecodeRA1Block(byte *dst, const byte *src, int left, int top, int width, int height, int pitch, int dataSize, uint8 param, uint16 parm2, int codec);
+/**
+ * RA1 codec 21: Skip/copy line codec (FUN_10D41).
+ *
+ * Each line: [u16 lineSize] then alternating [u16 skip] [u16 copyLen]
+ * followed by (copyLen+1) literal bytes. Destination advances by pitch
+ * per line, source advances by lineSize+2 per line.
+ */
+static void smushDecodeRA1SkipCopy(byte *dst, const byte *src, int left, int top, int width, int height, int pitch) {
+ dst += top * pitch + left;
+
+ for (int row = 0; row < height; row++) {
+ uint16 lineSize = READ_LE_UINT16(src);
+ const byte *lineData = src + 2;
+ const byte *lineEnd = lineData + lineSize;
+ byte *dstRow = dst;
+ int remaining = width;
+
+ while (remaining > 0 && lineData < lineEnd) {
+ // Read skip distance
+ if (lineData + 2 > lineEnd)
+ break;
+ uint16 skip = READ_LE_UINT16(lineData);
+ lineData += 2;
+ dstRow += skip;
+ remaining -= skip;
+ if (remaining <= 0)
+ break;
+
+ // Read copy count (+1)
+ if (lineData + 2 > lineEnd)
+ break;
+ uint16 copyLen = READ_LE_UINT16(lineData) + 1;
+ lineData += 2;
+
+ int toCopy = MIN<int>(copyLen, remaining);
+ if (lineData + toCopy > lineEnd)
+ toCopy = (int)(lineEnd - lineData);
+ if (toCopy > 0) {
+ memcpy(dstRow, lineData, toCopy);
+ lineData += toCopy;
+ dstRow += toCopy;
+ remaining -= toCopy;
+ }
+ // If copyLen was clamped by remaining, skip rest of source
+ if (copyLen > toCopy)
+ lineData += (copyLen - toCopy);
+ }
+
+ src += lineSize + 2;
+ dst += pitch;
+ }
+}
+
+/**
+ * RA1 codec 2: Scatter/point draw (FUN_110D7).
+ *
+ * Draws individual pixels at accumulated offsets â used for starfield
+ * backgrounds. Each 4-byte entry: [dx:int16_le, dy:uint8, pixel:uint8].
+ * Position starts at (left, top) and accumulates (dx, dy) per entry.
+ * Pixel is drawn if position is within buffer bounds.
+ */
+static void smushDecodeRA1Scatter(byte *dst, const byte *src, int left, int top, int bufWidth, int bufHeight, int pitch, int dataSize) {
+ int curX = left;
+ int curY = top;
+
+ while (dataSize >= 4) {
+ int16 dx = (int16)READ_LE_UINT16(src);
+ uint8 dy = src[2];
+ uint8 pixel = src[3];
+ src += 4;
+ dataSize -= 4;
+
+ curX += dx;
+ curY += dy;
+
+ if (curX >= 0 && curY >= 0 && curX < bufWidth && curY < bufHeight) {
+ dst[curY * pitch + curX] = pixel;
+ }
+ }
+}
+
/**
* RA1 codec 4/5: Block-based frame decoder.
* Adapted from FFmpeg's libavcodec/sanm.c old_codec4() (LGPL 2.1+).
@@ -991,6 +1090,17 @@ void SmushPlayer::decodeFrameObject(int codec, const uint8 *src, int left, int t
_specialBufferSize = bufSize;
}
_dst = _specialBuffer;
+ } else if (isRA1()) {
+ // RA1 sub-fullscreen frames (e.g. O1OPTION.ANM uses ~319x196 codec 2
+ // frames for the starfield animation). Render into _specialBuffer at
+ // their (left, top) offset position.
+ int bufSize = 384 * 242;
+ if (_specialBuffer == nullptr || bufSize > _specialBufferSize) {
+ free(_specialBuffer);
+ _specialBuffer = (byte *)calloc(bufSize, 1);
+ _specialBufferSize = bufSize;
+ }
+ _dst = _specialBuffer;
} else if (isRA2() && ((height != _vm->_screenHeight) || (width != _vm->_screenWidth))) {
ra2SelectFrameBuffer(width, height);
} else if ((height > _vm->_screenHeight) || (width > _vm->_screenWidth))
@@ -1005,8 +1115,12 @@ void SmushPlayer::decodeFrameObject(int codec, const uint8 *src, int left, int t
_width = width;
_height = height;
} else if (isRA1() && _dst == _specialBuffer) {
- // RA1: small overlay FOBJs (codec 1) should not override dimensions
- // set by the main 384x242 codec 5 FOBJ
+ // RA1: sub-fullscreen FOBJs should not override the 384x242 dimensions.
+ // Set dimensions on first use if not yet established.
+ if (_width == 0 || _height == 0) {
+ _width = 384;
+ _height = 242;
+ }
} else if (isRA2() && ((height != _vm->_screenHeight) || (width != _vm->_screenWidth))) {
// RA2: preserve _width/_height set during buffer allocation
} else {
@@ -1018,9 +1132,11 @@ void SmushPlayer::decodeFrameObject(int codec, const uint8 *src, int left, int t
if (_dst == _specialBuffer)
pitch = _width;
- // Save original FOBJ dimensions before clipping. Codec 37/47 (delta block/glyph)
- // decode the full frame into the buffer starting at (0,0) regardless of FOBJ
- // left/top position. They must use the original un-clipped dimensions.
+ // Save original FOBJ position and dimensions before clipping. Codec 37/47
+ // (delta block/glyph) decode the full frame starting at (0,0). Codec 2
+ // (scatter draw) needs the original start position for accumulation.
+ int origLeft = left;
+ int origTop = top;
int origWidth = width;
int origHeight = height;
@@ -1034,7 +1150,7 @@ void SmushPlayer::decodeFrameObject(int codec, const uint8 *src, int left, int t
// For RLE codecs, skip source rows that were clipped from the top.
// Each RLE row has a 2-byte size prefix, so we can advance past them.
const uint8 *adjustedSrc = src;
- if (isRA1() && srcSkipY > 0 && (codec == SMUSH_CODEC_RLE || codec == SMUSH_CODEC_RLE_ALT)) {
+ if (isRA1() && srcSkipY > 0 && (codec == SMUSH_CODEC_RLE || codec == SMUSH_CODEC_RLE_ALT || codec == SMUSH_CODEC_LINE_UPDATE)) {
for (int i = 0; i < srcSkipY; i++) {
adjustedSrc += READ_LE_UINT16(adjustedSrc) + 2;
}
@@ -1042,9 +1158,22 @@ void SmushPlayer::decodeFrameObject(int codec, const uint8 *src, int left, int t
switch (codec) {
case SMUSH_CODEC_RLE:
+ if (isRA1()) {
+ // RA1 codec 1: pixel 0 is transparent (background shows through)
+ smushDecodeRA1Transparent(_dst, adjustedSrc, left, top, width, height, pitch);
+ } else {
+ smushDecodeRLE(_dst, adjustedSrc, left, top, width, height, pitch);
+ }
+ break;
case SMUSH_CODEC_RLE_ALT:
+ // Codec 3: all pixels opaque (pixel 0 is written)
smushDecodeRLE(_dst, adjustedSrc, left, top, width, height, pitch);
break;
+ case SMUSH_CODEC_RA1_SCATTER:
+ // Codec 2: Scatter draw uses absolute buffer coords, not clipped FOBJ coords.
+ // Pass full buffer dimensions for clipping (not the FOBJ width/height).
+ smushDecodeRA1Scatter(_dst, src, origLeft, origTop, _width, _height, pitch, dataSize);
+ break;
case SMUSH_CODEC_DELTA_BLOCKS:
// Codec 37 writes the full frame to dst via memcpy â always uses original
// FOBJ dimensions, not position-clipped dimensions.
@@ -1067,6 +1196,12 @@ void SmushPlayer::decodeFrameObject(int codec, const uint8 *src, int left, int t
case SMUSH_CODEC_UNCOMPRESSED:
smushDecodeUncompressed(_dst, src, left, top, width, height, pitch);
break;
+ case SMUSH_CODEC_LINE_UPDATE:
+ if (isRA1()) {
+ smushDecodeRA1SkipCopy(_dst, adjustedSrc, left, top, width, height, pitch);
+ break;
+ }
+ // Fall through for RA2
default:
if (isRA2() && ra2DecodeCodec(codec, src, left, top, width, height, pitch, dataSize))
break;
@@ -1166,6 +1301,10 @@ void SmushPlayer::handleFrameObject(int32 subSize, Common::SeekableReadStream &b
debug("SmushPlayer FOBJ: frame=%d codec=%d pos=(%d,%d) size=%dx%d dataSize=%d storeFrame=%d _width=%d _height=%d",
_frame, codec, left, top, width, height, subSize - 14, _storeFrame, _width, _height);
}
+ if (isRA1()) {
+ debug("RA1 FOBJ: frame=%d codec=%d pos=(%d,%d) size=%dx%d dataSize=%d storeFrame=%d",
+ _frame, codec, left, top, width, height, subSize - 14, _storeFrame);
+ }
int32 chunk_size = subSize - 14;
byte *chunk_buffer = (byte *)malloc(chunk_size);
@@ -1196,6 +1335,15 @@ void SmushPlayer::handleFrame(int32 frameSize, Common::SeekableReadStream &b) {
_vm->_insane->procPreRendering(_dst);
}
+ // RA1: Clear framebuffer before each frame's first draw operation.
+ // Matches FFmpeg's first_fob memset: process_frame_obj() zeroes fbuf
+ // before the first FOBJ (or FTCH re-decode) of each frame. Without this,
+ // codec 1's transparent pixels (pixel 0 = skip) show previous frame
+ // content, causing ghost trails on the Star Wars opening crawl.
+ if (isRA1() && _dst && _width > 0 && _height > 0) {
+ memset(_dst, 0, _width * _height);
+ }
+
while (frameSize > 0) {
const uint32 subType = b.readUint32BE();
const int32 subSize = b.readUint32BE();
@@ -1336,6 +1484,14 @@ void SmushPlayer::handleAnimHeader(int32 subSize, Common::SeekableReadStream &b)
byte *palettePtr = &headerContent[6];
memcpy(_pal, palettePtr, sizeof(_pal));
+ if (isRA1()) {
+ // RA1: force palette index 0 to black. Some ANM files store
+ // non-black values (e.g. O1OPEN.ANM has palette[0] = blue)
+ // but the original engine always displays index 0 as black.
+ // XPAL delta animation skips index 0, so it's never corrected.
+ _pal[0] = _pal[1] = _pal[2] = 0;
+ }
+
if (isRA2())
ra2ResetDeltaPalette();
@@ -1343,11 +1499,20 @@ void SmushPlayer::handleAnimHeader(int32 subSize, Common::SeekableReadStream &b)
}
if (isRA1()) {
- // v1 AHDR: offset 4-5 is not width/height, palette starts at offset 6.
- // Keep dimensions unset here and let FOBJ decoding select buffer and
- // effective width/height (screen-sized or 384x242 special buffer).
+ // v1 AHDR: pre-allocate 384x242 special buffer and set _dst so that
+ // procPostRendering has a valid target. Keep _width/_height at 0 so
+ // updateScreen() is NOT called until the first FOBJ sets dimensions â
+ // this avoids blitting an empty buffer with palette[0] (which may be
+ // non-black, e.g. O1OPEN.ANM has palette[0] = blue).
_width = 0;
_height = 0;
+ int bufSize = 384 * 242;
+ if (_specialBuffer == nullptr || bufSize > _specialBufferSize) {
+ free(_specialBuffer);
+ _specialBuffer = (byte *)calloc(bufSize, 1);
+ _specialBufferSize = bufSize;
+ }
+ _dst = _specialBuffer;
} else {
_width = READ_LE_UINT16(&headerContent[4]);
_height = READ_LE_UINT16(&headerContent[6]);
diff --git a/engines/scumm/smush/smush_player.h b/engines/scumm/smush/smush_player.h
index a2454d93546..e2b6427bf90 100644
--- a/engines/scumm/smush/smush_player.h
+++ b/engines/scumm/smush/smush_player.h
@@ -77,6 +77,7 @@ namespace Scumm {
#define TRK_USERID_SFX 3
#define SMUSH_CODEC_RLE 1
+#define SMUSH_CODEC_RA1_SCATTER 2 // RA1: Scatter/point draw (starfield)
#define SMUSH_CODEC_RLE_ALT 3
#define SMUSH_CODEC_RA1_DELTA 4 // RA1: Delta block codec (skip on idx 0x80)
#define SMUSH_CODEC_RA1_BLOCK 5 // RA1: Block-based frame codec (no skip)
Commit: 591fc6920521410bd8229eef0052f5ccd25ff70c
https://github.com/scummvm/scummvm/commit/591fc6920521410bd8229eef0052f5ccd25ff70c
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:25+02:00
Commit Message:
SCUMM: RA1: Add sound and subtitles
Changed paths:
engines/scumm/insane/insane_rebel1.h
engines/scumm/smush/smush_player.cpp
engines/scumm/smush/smush_player.h
diff --git a/engines/scumm/insane/insane_rebel1.h b/engines/scumm/insane/insane_rebel1.h
index 16f1b4c14a5..0126587c9db 100644
--- a/engines/scumm/insane/insane_rebel1.h
+++ b/engines/scumm/insane/insane_rebel1.h
@@ -101,8 +101,6 @@ private:
void renderShip(byte *dst, int pitch, int width, int height);
void renderHUD(byte *dst, int pitch, int width, int height);
void renderMainMenuOverlay(byte *dst, int pitch, int width, int height);
- void drawFontBankString(byte *dst, int pitch, int width, int height, int x, int y, const char *text);
- int getFontBankStringWidth(const char *text);
void renderSprite(byte *dst, int pitch, int width, int height,
int x, int y, const RA1Sprite &sprite);
@@ -111,6 +109,8 @@ private:
void terminateAudio();
void queueAudioData(int trackIdx, uint8 *data, int32 size, int volume, int pan);
public:
+ void drawFontBankString(byte *dst, int pitch, int width, int height, int x, int y, const char *text);
+ int getFontBankStringWidth(const char *text);
void processAudioFrame(int16 feedSize);
private:
diff --git a/engines/scumm/smush/smush_player.cpp b/engines/scumm/smush/smush_player.cpp
index 68ca58682a2..7da104cfcc7 100644
--- a/engines/scumm/smush/smush_player.cpp
+++ b/engines/scumm/smush/smush_player.cpp
@@ -618,6 +618,13 @@ void SmushPlayer::handleIACT(int32 subSize, Common::SeekableReadStream &b) {
}
void SmushPlayer::handleTextResource(uint32 subType, int32 subSize, Common::SeekableReadStream &b) {
+ // RA1 TEXT chunks have a different format: 2 Ã BE32 header + text data.
+ // Route to dedicated handler.
+ if (isRA1() && subType == MKTAG('T','E','X','T')) {
+ ra1HandleText(subSize, b);
+ return;
+ }
+
int pos_x = b.readSint16LE();
int pos_y = b.readSint16LE();
int flags = b.readSint16LE();
@@ -1418,6 +1425,19 @@ void SmushPlayer::handleFrame(int32 frameSize, Common::SeekableReadStream &b) {
rebel1->handleGameChunk(subSize, b);
}
break;
+ case MKTAG('P','V','O','C'):
+ // RA1 voice-over audio: same 12-byte header format as PSAD
+ // (3 Ã BE32: trackId, seqNum, param) followed by SAUD data.
+ // Feed to audio system identically to PSAD.
+ if (!_compressedFileMode) {
+ audioChunk = (uint8 *)malloc(subSize + 8);
+ b.seek(-8, SEEK_CUR);
+ b.read(audioChunk, subSize + 8);
+ feedAudio(audioChunk, 0, 127, 0, 0);
+ free(audioChunk);
+ audioChunk = nullptr;
+ }
+ break;
case MKTAG('G','A','M','2'):
case MKTAG('F','A','D','E'):
case MKTAG('S','E','G','A'):
@@ -1426,7 +1446,6 @@ void SmushPlayer::handleFrame(int32 frameSize, Common::SeekableReadStream &b) {
case MKTAG('S','B','L',' '):
case MKTAG('S','B','L','2'):
case MKTAG('P','S','D','2'):
- case MKTAG('P','V','O','C'):
debugC(DEBUG_SMUSH, "SmushPlayer::handleFrame: skipping RA1 chunk %s (%d bytes)", tag2str(subType), subSize);
break;
default:
@@ -1612,6 +1631,80 @@ SmushFont *SmushPlayer::ra1GetFont(int font) {
return _sf[font];
}
+/**
+ * RA1 TEXT chunk handler.
+ *
+ * RA1 TEXT format (different from SCUMM7/COMI TRES):
+ * Bytes 0-3 (BE32): y position on screen
+ * Bytes 4-7 (BE32): parameter (x hint or style)
+ * Bytes 8+: text content with 0x00 as line separator
+ * '<' / '>' switch font layers (multi-font markup)
+ *
+ * Text is rendered centered horizontally at the given y position.
+ * The '<' font-switch prefix is stripped before rendering.
+ */
+void SmushPlayer::ra1HandleText(int32 subSize, Common::SeekableReadStream &b) {
+ if (subSize < 8 || !_dst || _width <= 0 || _height <= 0)
+ return;
+
+ InsaneRebel1 *rebel1 = (InsaneRebel1 *)_vm->_insane;
+ if (!rebel1)
+ return;
+
+ int yPos = b.readUint32BE();
+ /*int param =*/ b.readUint32BE();
+
+ int textLen = subSize - 8;
+ if (textLen <= 0)
+ return;
+
+ char *textBuf = (char *)malloc(textLen + 1);
+ b.read(textBuf, textLen);
+ textBuf[textLen] = '\0';
+
+ int pitch = _width;
+
+ // Split on 0x00 line separators and render each line centered.
+ // Strip '<' / '>' font layer markers (multi-font not yet supported).
+ const char *lineStart = textBuf;
+ int lineY = yPos;
+ int lineHeight = 10;
+
+ while (lineStart < textBuf + textLen) {
+ const char *lineEnd = lineStart;
+ while (lineEnd < textBuf + textLen && *lineEnd != '\0')
+ lineEnd++;
+
+ int len = (int)(lineEnd - lineStart);
+ if (len > 0) {
+ const char *cleaned = lineStart;
+ while (cleaned < lineEnd && (*cleaned == '<' || *cleaned == '>'))
+ cleaned++;
+
+ int cleanLen = (int)(lineEnd - cleaned);
+ if (cleanLen > 0 && lineY >= 0 && lineY < _height) {
+ char *line = (char *)malloc(cleanLen + 1);
+ memcpy(line, cleaned, cleanLen);
+ line[cleanLen] = '\0';
+
+ // Center the line horizontally
+ int strWidth = rebel1->getFontBankStringWidth(line);
+ int x = (_width - strWidth) / 2;
+ if (x < 0) x = 0;
+
+ rebel1->drawFontBankString(_dst, pitch, _width, _height, x, lineY, line);
+
+ free(line);
+ lineY += lineHeight;
+ }
+ }
+
+ lineStart = lineEnd + 1;
+ }
+
+ free(textBuf);
+}
+
void SmushPlayer::parseNextFrame() {
if (_seekPos >= 0) {
@@ -1693,7 +1786,7 @@ void SmushPlayer::parseNextFrame() {
ra2ParseNextFrame();
if (!_imuseDigital && isRA1())
- processDispatches(_smushAudioSampleRate / 15);
+ processDispatches(_smushAudioSampleRate / _speed);
}
void SmushPlayer::setPalette(const byte *palette) {
diff --git a/engines/scumm/smush/smush_player.h b/engines/scumm/smush/smush_player.h
index e2b6427bf90..f594df5866f 100644
--- a/engines/scumm/smush/smush_player.h
+++ b/engines/scumm/smush/smush_player.h
@@ -330,6 +330,7 @@ private:
void ra2HandleGost(int32 subSize, Common::SeekableReadStream &b);
void ra2ResetDeltaPalette();
SmushFont *ra1GetFont(int font);
+ void ra1HandleText(int32 subSize, Common::SeekableReadStream &b);
SmushFont *ra2GetFont(int font);
void ra2ParseNextFrame();
void ra2FixupAnimHeader();
Commit: d1a0b4d9bf9eda1115c76cdf40b5f37d6a392d20
https://github.com/scummvm/scummvm/commit/d1a0b4d9bf9eda1115c76cdf40b5f37d6a392d20
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:26+02:00
Commit Message:
SCUMM: RA1: Add damage handling
Changed paths:
engines/scumm/insane/insane_rebel1.cpp
engines/scumm/insane/insane_rebel1.h
diff --git a/engines/scumm/insane/insane_rebel1.cpp b/engines/scumm/insane/insane_rebel1.cpp
index 068597e8c32..0d8673c20fa 100644
--- a/engines/scumm/insane/insane_rebel1.cpp
+++ b/engines/scumm/insane/insane_rebel1.cpp
@@ -105,6 +105,7 @@ InsaneRebel1::InsaneRebel1(ScummEngine_v7 *scumm) : Insane(), _vm(scumm) {
_lives = 3;
_score = 0;
_damageFlags = 0;
+ _prevDamageFlags = 0;
_gameLatch5D = 0;
_gameLatch5F = 0;
_damageCooldown = 0;
@@ -505,6 +506,9 @@ void InsaneRebel1::loadLevelSprites(int level) {
Common::String filename = Common::String::format("LVL%d/L%dBANK1.NUT", level, level);
loadRA1Nut(filename.c_str(), _shipBank);
loadRA1Nut("SYS/DISPLAY.NUT", _displayBank);
+
+ Common::String bangFile = Common::String::format("LVL%d/L%dBANG.NUT", level, level);
+ loadRA1Nut(bangFile.c_str(), _bangBank);
}
void InsaneRebel1::procPreRendering(byte *renderBitmap) {
@@ -532,6 +536,7 @@ void InsaneRebel1::procPostRendering(byte *renderBitmap, int32 codecparam, int32
updateShipPhysics();
renderShip(renderBitmap, pitch, width, height);
+ renderExplosions(renderBitmap, pitch, width, height);
renderHUD(renderBitmap, pitch, width, height);
}
@@ -868,6 +873,7 @@ void InsaneRebel1::updateShipPhysics() {
if (_health < 0)
_deathTimer = kDeathTimerInit;
+ _prevDamageFlags = _damageFlags;
_damageCooldown = kDamageCooldownInit;
_screenFlash = 3;
}
@@ -902,6 +908,11 @@ void InsaneRebel1::updateShipPhysics() {
}
void InsaneRebel1::renderShip(byte *dst, int pitch, int width, int height) {
+ // From FUN_1DEB5 LAB_1e2b2: ship drawn when health >= 0 OR deathTimer > 20
+ // Hidden during last 20 frames of death sequence (deathTimer 20â0)
+ if (_health < 0 && _deathTimer <= 20)
+ return;
+
if (_shipDirIndex < 0 || _shipDirIndex >= _shipBank.numSprites)
return;
@@ -916,6 +927,65 @@ void InsaneRebel1::renderShip(byte *dst, int pitch, int width, int height) {
renderSprite(dst, pitch, width, height, drawX, drawY, spr);
}
+// Render explosion sprites during damage cooldown and death sequence.
+// From FUN_1DEB5 at LAB_1e185 (damage hit) and LAB_1e0e3 (death shake).
+void InsaneRebel1::renderExplosions(byte *dst, int pitch, int width, int height) {
+ if (_bangBank.numSprites <= 0)
+ return;
+
+ // Ship screen center position (matches assembly: DAT_74b6+DAT_74ba, DAT_74b8+DAT_74bc)
+ int shipScreenX = (_shipPosX - kCenterX) + _perspectiveX + kCenterX;
+ int shipScreenY = (_shipPosY - kCenterY) + _perspectiveY + kCenterY;
+
+ // --- Death shake explosions (FUN_1DEB5 LAB_1e0e3) ---
+ // When dead and deathTimer > 10: random explosion sprites scatter around ship
+ if (_health < 0 && _deathTimer > 10) {
+ int intensity = _deathTimer - 10; // 20â1 as timer goes 30â11
+ if (intensity > 10)
+ intensity = 20 - intensity; // Triangle: 0â10â0
+
+ // di = intensity * 4 + 1 (vertical scatter range)
+ // si = -20 + intensity * 4 (horizontal scatter range, DAT_75d8 is 0)
+ int rangeY = intensity * 4 + 1;
+ int rangeX = -20 + intensity * 4;
+ if (rangeX < 1) rangeX = 1;
+
+ for (int i = 0; i < intensity; i++) {
+ // Random sprite from bang bank (FUN_21db0(10))
+ int sprIdx = _vm->_rnd.getRandomNumber(_bangBank.numSprites - 1);
+
+ // Random position around ship (matching assembly random scatter)
+ int randX = (int)_vm->_rnd.getRandomNumber(rangeX * 2) - rangeX;
+ int randY = (int)_vm->_rnd.getRandomNumber(rangeY * 2) - rangeY;
+
+ int drawX = shipScreenX + randX;
+ int drawY = shipScreenY + randY;
+
+ const RA1Sprite &spr = _bangBank.sprites[sprIdx];
+ renderSprite(dst, pitch, width, height,
+ drawX - spr.width / 2, drawY - spr.height / 2, spr);
+ }
+ return;
+ }
+
+ // --- Damage hit explosion (FUN_1DEB5 LAB_1e185) ---
+ // When alive, in cooldown, and bang bank loaded
+ if (_health >= 0 && _damageCooldown > 0) {
+ // Sprite index = 10 - damageCooldown (frames 0â9 as cooldown 10â1)
+ int sprIdx = _bangBank.numSprites - _damageCooldown;
+ if (sprIdx < 0 || sprIdx >= _bangBank.numSprites)
+ return;
+
+ // Position at ship center (DAT_75d8 is always 0 in RA1)
+ int drawX = shipScreenX;
+ int drawY = shipScreenY;
+
+ const RA1Sprite &spr = _bangBank.sprites[sprIdx];
+ renderSprite(dst, pitch, width, height,
+ drawX - spr.width / 2, drawY - spr.height / 2, spr);
+ }
+}
+
// Render bottom status bar from DISPLAY.NUT with dynamic damage bar and score.
// Original layout (320-wide): DAMAGE [green bar] | PILOTS [3 icons] | SCORE [number]
void InsaneRebel1::renderHUD(byte *dst, int pitch, int width, int height) {
@@ -1044,6 +1114,7 @@ void InsaneRebel1::handleGameChunk(int32 subSize, Common::SeekableReadStream &b)
// RA1 dispatcher inline reset/init path (FUN_1BE1B case 0x5E).
// This is not a pure control-mode assignment.
_damageFlags = 0;
+ _prevDamageFlags = 0;
_damageCooldown = 0;
_deathTimer = 0;
_screenFlash = 0;
@@ -1243,6 +1314,7 @@ bool InsaneRebel1::runLevel1() {
// Reset health for this attempt
_health = kMaxHealth;
_damageFlags = 0;
+ _prevDamageFlags = 0;
_damageCooldown = 0;
_deathTimer = 0;
_screenFlash = 0;
diff --git a/engines/scumm/insane/insane_rebel1.h b/engines/scumm/insane/insane_rebel1.h
index 0126587c9db..bd10c389489 100644
--- a/engines/scumm/insane/insane_rebel1.h
+++ b/engines/scumm/insane/insane_rebel1.h
@@ -101,6 +101,7 @@ private:
void renderShip(byte *dst, int pitch, int width, int height);
void renderHUD(byte *dst, int pitch, int width, int height);
void renderMainMenuOverlay(byte *dst, int pitch, int width, int height);
+ void renderExplosions(byte *dst, int pitch, int width, int height);
void renderSprite(byte *dst, int pitch, int width, int height,
int x, int y, const RA1Sprite &sprite);
@@ -119,6 +120,7 @@ private:
RA1SpriteBank _shipBank;
RA1SpriteBank _displayBank; // SYS/DISPLAY.NUT â bottom status bar
RA1SpriteBank _hudFontBank; // RA1 HUD text glyphs (TECHFONT/TALKFONT via RA1 loader)
+ RA1SpriteBank _bangBank; // LxBANG.NUT â impact/explosion sprites (10 frames)
SmushFont *_menuFont; // Use engine text renderer for correct TALKFONT character mapping
// RA1 screen dimensions (384x242)
@@ -163,6 +165,7 @@ private:
int16 _lives; // 0x7562: remaining extra lives
int _score; // 0x7564: current score
byte _damageFlags; // 0x74D4: per-frame collision bitmask (cleared each frame)
+ byte _prevDamageFlags; // 0x74D6: previous frame's damage flags (for explosion direction)
uint16 _gameLatch5D; // 0x75D2: GAME 0x5D latch (scene/obstacle/event trigger)
uint16 _gameLatch5F; // 0x75D4: GAME 0x5F latch (probabilistic hit trigger)
int16 _damageCooldown; // 0x74D8: invulnerability timer (10 frames after hit)
Commit: ecfc1eefad1ae8ed200d87f357353292c6938096
https://github.com/scummvm/scummvm/commit/ecfc1eefad1ae8ed200d87f357353292c6938096
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:26+02:00
Commit Message:
SCUMM: RA1: Improve level 1 loop
Changed paths:
engines/scumm/insane/insane_rebel1.cpp
engines/scumm/smush/smush_player.cpp
diff --git a/engines/scumm/insane/insane_rebel1.cpp b/engines/scumm/insane/insane_rebel1.cpp
index 0d8673c20fa..c95abc44570 100644
--- a/engines/scumm/insane/insane_rebel1.cpp
+++ b/engines/scumm/insane/insane_rebel1.cpp
@@ -1285,13 +1285,13 @@ int InsaneRebel1::runMainMenu() {
// Level 1 flow (0x16100-0x16737):
// 1. Load NUTs (L1BANK1, L1BANK2, L1EXPLD, L1BANG, L1LASER)
// 2. L1HANGAR.ANM â Hangar departure cutscene
-// 3. "Chapter 1" text
-// 4. L1CU1.ANM â Pre-flight cutscene
-// 5. L1PLAY1L.ANM â Stage 1 gameplay (left path) â INTERACTIVE
-// 6. L1PLAY1R.ANM â Stage 1 gameplay (right path) â INTERACTIVE
-// 7. L1CU2.ANM â Mid-level cutscene
-// 8. L1PLAY2.ANM â Stage 2 turret â INTERACTIVE
-// 9. L1END.ANM â Level end cutscene
+// 3. L1CU1.ANM â Pre-flight cutscene
+// 4. L1PLAY1L.ANM or L1PLAY1R.ANM â Stage 1 flight (alternative paths)
+// L1PLAY1L = "Hard" left path (788 frames), L1PLAY1R = "Easy" right path (396 frames)
+// Original branches at frame 394 via nextSceneA/nextSceneB collision zones
+// 5. L1CU2.ANM â Mid-level cutscene
+// 6. L1PLAY2.ANM â Stage 2 turret â INTERACTIVE
+// 7. L1END.ANM â Level end cutscene
// Death: L1CRASHA/B.ANM â L1DEATH.ANM â L1RETRY.ANM â retry from L1NEW
bool InsaneRebel1::runLevel1() {
debug(1, "InsaneRebel1: Running level 1");
@@ -1320,33 +1320,28 @@ bool InsaneRebel1::runLevel1() {
_screenFlash = 0;
_frameCounter = 0;
- // L1PLAY1L.ANM â Stage 1 gameplay (left path, original: 0x5953)
+ // Stage 1 flight â L1PLAY1L (hard/left) or L1PLAY1R (easy/right).
+ // Original selects path via collision zones at frame 394 using
+ // nextSceneA(0x67)/nextSceneB(0x69). For now, always play PLAY1L.
+ // TODO: Implement path branching based on player ship position.
playInteractiveVideo("LVL1/L1PLAY1L.ANM");
if (_vm->shouldQuit())
return false;
if (_health >= 0) {
- // Survived stage 1 â continue to right path
- // L1PLAY1R.ANM (original: 0x5965, flags with seekframe=0x187)
- playInteractiveVideo("LVL1/L1PLAY1R.ANM");
+ // L1CU2.ANM â Mid-level cutscene (original: 0x5977)
+ playCinematic("LVL1/L1CU2.ANM");
if (_vm->shouldQuit())
return false;
- if (_health >= 0) {
- // L1CU2.ANM â Mid-level cutscene (original: 0x5977)
- playCinematic("LVL1/L1CU2.ANM");
- if (_vm->shouldQuit())
- return false;
-
- // L1PLAY2.ANM â Stage 2 turret (original: 0x5986)
- playInteractiveVideo("LVL1/L1PLAY2.ANM");
- if (_vm->shouldQuit())
- return false;
+ // L1PLAY2.ANM â Stage 2 turret (original: 0x5986)
+ playInteractiveVideo("LVL1/L1PLAY2.ANM");
+ if (_vm->shouldQuit())
+ return false;
- // L1END.ANM â Level complete! (original: 0x59a3)
- playCinematic("LVL1/L1END.ANM");
- return true;
- }
+ // L1END.ANM â Level complete! (original: 0x59a3)
+ playCinematic("LVL1/L1END.ANM");
+ return true;
}
// Death sequence (original: 0x165e8-0x16737)
diff --git a/engines/scumm/smush/smush_player.cpp b/engines/scumm/smush/smush_player.cpp
index 7da104cfcc7..429af0d73cf 100644
--- a/engines/scumm/smush/smush_player.cpp
+++ b/engines/scumm/smush/smush_player.cpp
@@ -264,6 +264,8 @@ SmushPlayer::SmushPlayer(ScummEngine_v7 *scumm, IMuseDigital *imuseDigital, Insa
_compressedFileMode = false;
_width = 0;
_height = 0;
+ _scrollX = 0;
+ _scrollY = 0;
ra2InitFields();
_IACTpos = 0;
@@ -322,6 +324,8 @@ void SmushPlayer::init(int32 speed) {
_storeFrame = false;
_fobjOffsetX = 0;
_fobjOffsetY = 0;
+ _scrollX = 0;
+ _scrollY = 0;
_vm->_smushVideoShouldFinish = false;
_vm->_smushActive = true;
Commit: 01e04961b098668a4b982aa2ee97d2e76f302892
https://github.com/scummvm/scummvm/commit/01e04961b098668a4b982aa2ee97d2e76f302892
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:26+02:00
Commit Message:
SCUMM: RA1: Add right-path branching in level 1
Changed paths:
engines/scumm/insane/insane_rebel1.cpp
engines/scumm/insane/insane_rebel1.h
engines/scumm/smush/smush_player.cpp
engines/scumm/smush/smush_player.h
diff --git a/engines/scumm/insane/insane_rebel1.cpp b/engines/scumm/insane/insane_rebel1.cpp
index c95abc44570..dad2a5b5e68 100644
--- a/engines/scumm/insane/insane_rebel1.cpp
+++ b/engines/scumm/insane/insane_rebel1.cpp
@@ -113,6 +113,8 @@ InsaneRebel1::InsaneRebel1(ScummEngine_v7 *scumm) : Insane(), _vm(scumm) {
_screenFlash = 0;
_frameCounter = 0;
_interactiveVideoActive = false;
+ _pathBranchEnabled = false;
+ _rightPathSelected = false;
_menuActive = false;
_menuConfirmed = false;
_menuSelection = 0;
@@ -901,6 +903,21 @@ void InsaneRebel1::updateShipPhysics() {
// Clear per-frame damage flags
_damageFlags = 0;
+ // --- Path branching detection ---
+ // Original enables branching at frame 394 (nextSceneA/nextSceneB in FUN_1B297).
+ // After this frame, if the ship is on the right side of the screen, the player
+ // has chosen the right/easy path. We signal the video to stop so runLevel1()
+ // can switch to L1PLAY1R.
+ if (_pathBranchEnabled && !_rightPathSelected && _frameCounter > (uint32)kPathBranchFrame) {
+ // Ship past center-right threshold = right path chosen
+ static const int16 kRightPathThreshold = kCenterX + 40;
+ if (_shipPosX > kRightPathThreshold) {
+ _rightPathSelected = true;
+ _vm->_smushVideoShouldFinish = true;
+ debug(1, "RA1: Right path selected at frame %d (shipX=%d)", _frameCounter, _shipPosX);
+ }
+ }
+
debug(7, "RA1 ship: pos=(%d,%d) vel=%d vIn=%d dx=%d dir=%d health=%d corridor=[%d,%d]-[%d,%d]",
_shipPosX, _shipPosY, _smoothedVelocity, _verticalInput,
positionDeltaX, _shipDirIndex, _health,
@@ -1210,13 +1227,16 @@ void InsaneRebel1::handleGameChunk(int32 subSize, Common::SeekableReadStream &b)
// Play a passive cinematic (no game callback, skippable).
// Reuses RA2's pattern: reset handler, set cinematic flags, play video.
-void InsaneRebel1::playCinematic(const char *filename) {
- debug(1, "InsaneRebel1::playCinematic('%s')", filename);
+// startFrame > 0: fast-forward (decode without display/audio) to that frame.
+void InsaneRebel1::playCinematic(const char *filename, int32 startFrame) {
+ debug(1, "InsaneRebel1::playCinematic('%s', startFrame=%d)", filename, startFrame);
SmushPlayer *splayer = _vm->_splayer;
_player = splayer;
_interactiveVideoActive = false;
_vm->_smushVideoShouldFinish = false;
splayer->setCurVideoFlags(0x28); // Cinematic mode + buffer preserve
+ if (startFrame > 0)
+ splayer->setFastForwardToFrame(startFrame);
splayer->play(filename, 12);
}
@@ -1282,36 +1302,41 @@ int InsaneRebel1::runMainMenu() {
return 5;
}
-// Level 1 flow (0x16100-0x16737):
+// Level 1 flow (0x16100-0x167A2, from disassembly):
// 1. Load NUTs (L1BANK1, L1BANK2, L1EXPLD, L1BANG, L1LASER)
-// 2. L1HANGAR.ANM â Hangar departure cutscene
-// 3. L1CU1.ANM â Pre-flight cutscene
-// 4. L1PLAY1L.ANM or L1PLAY1R.ANM â Stage 1 flight (alternative paths)
-// L1PLAY1L = "Hard" left path (788 frames), L1PLAY1R = "Easy" right path (396 frames)
-// Original branches at frame 394 via nextSceneA/nextSceneB collision zones
+// 2. L1HANGAR.ANM â Full hangar departure cutscene (782 frames, flags 0x0420)
+// 3. L1CU1.ANM â Pre-flight cutscene (flags 0x0400)
+// 4. L1PLAY1L.ANM â Stage 1 flight, hard/left path (788 frames)
+// At frame 394, if player steers right â L1PLAY1R (easy path, 396 frames)
// 5. L1CU2.ANM â Mid-level cutscene
-// 6. L1PLAY2.ANM â Stage 2 turret â INTERACTIVE
-// 7. L1END.ANM â Level end cutscene
-// Death: L1CRASHA/B.ANM â L1DEATH.ANM â L1RETRY.ANM â retry from L1NEW
+// 6. L1PLAY2.ANM â Stage 2 turret
+// If score < 5 (0x75D0): L1RETRY â retry Stage 2
+// 7. L1END.ANM â Level complete
+// Death (health<0): L1CRASHA/B â lives check:
+// lives>0: L1NEW â jump back to Stage 1 (skip L1HANGAR/L1CU1)
+// lives==0: L1DEATH â return to menu
+
bool InsaneRebel1::runLevel1() {
debug(1, "InsaneRebel1: Running level 1");
// Load level sprites (original: pushes L1BANK1..L1BANG NUT filenames)
loadLevelSprites(1);
- // L1HANGAR.ANM â Hangar departure intro (original: 0x5918, flags 0x0420)
+ // L1HANGAR.ANM â Hangar departure (original: 0x5918, flags 0x0420)
+ // Plays once at level start, never replayed on retry.
playCinematic("LVL1/L1HANGAR.ANM");
if (_vm->shouldQuit())
return false;
// L1CU1.ANM â Pre-flight cutscene (original: 0x5944, flags 0x0400)
+ // Plays once at level start, never replayed on retry.
playCinematic("LVL1/L1CU1.ANM");
if (_vm->shouldQuit())
return false;
- // Retry loop
+ // Retry loop â on death with lives, L1NEW plays then jumps back here
while (!_vm->shouldQuit()) {
- // Reset health for this attempt
+ // Reset health for this attempt (original: MOV WORD [0x7560], 98 at 0x16214)
_health = kMaxHealth;
_damageFlags = 0;
_prevDamageFlags = 0;
@@ -1319,15 +1344,23 @@ bool InsaneRebel1::runLevel1() {
_deathTimer = 0;
_screenFlash = 0;
_frameCounter = 0;
+ _pathBranchEnabled = true;
+ _rightPathSelected = false;
- // Stage 1 flight â L1PLAY1L (hard/left) or L1PLAY1R (easy/right).
- // Original selects path via collision zones at frame 394 using
- // nextSceneA(0x67)/nextSceneB(0x69). For now, always play PLAY1L.
- // TODO: Implement path branching based on player ship position.
+ // Stage 1 flight â L1PLAY1L (hard/left path)
playInteractiveVideo("LVL1/L1PLAY1L.ANM");
if (_vm->shouldQuit())
return false;
+ if (_rightPathSelected && _health >= 0) {
+ debug(1, "InsaneRebel1: Switching to right path (L1PLAY1R)");
+ _pathBranchEnabled = false;
+ playInteractiveVideo("LVL1/L1PLAY1R.ANM");
+ if (_vm->shouldQuit())
+ return false;
+ }
+ _pathBranchEnabled = false;
+
if (_health >= 0) {
// L1CU2.ANM â Mid-level cutscene (original: 0x5977)
playCinematic("LVL1/L1CU2.ANM");
@@ -1339,12 +1372,15 @@ bool InsaneRebel1::runLevel1() {
if (_vm->shouldQuit())
return false;
+ // TODO: Check score threshold (original: CMP WORD [0x75D0], 5)
+ // If score < 5: L1RETRY â retry Stage 2
+
// L1END.ANM â Level complete! (original: 0x59a3)
playCinematic("LVL1/L1END.ANM");
return true;
}
- // Death sequence (original: 0x165e8-0x16737)
+ // Death sequence (original: 0x165dd-0x166bb)
// Random crash variant A or B
if (_vm->_rnd.getRandomNumber(1) == 0)
playCinematic("LVL1/L1CRASHA.ANM");
@@ -1353,30 +1389,21 @@ bool InsaneRebel1::runLevel1() {
if (_vm->shouldQuit())
return false;
- // L1DEATH.ANM (original: 0x5a4b)
- playCinematic("LVL1/L1DEATH.ANM");
- if (_vm->shouldQuit())
- return false;
-
+ // Check lives (original: CMP WORD [0x7562], 0 at 0x1666B)
_lives--;
if (_lives <= 0) {
- // Game over â no more retries
+ // Game over â L1DEATH then return (original: 0x166C0)
+ playCinematic("LVL1/L1DEATH.ANM");
debug(1, "InsaneRebel1: Game over (no lives left)");
return false;
}
- // L1RETRY.ANM â Retry prompt (original: 0x5a5c)
- // After retry, original jumps back to L1NEWâL1PLAY1L (0x16214â0x16680)
- playCinematic("LVL1/L1RETRY.ANM");
- if (_vm->shouldQuit())
- return false;
-
- // L1NEW.ANM â Briefing before retry (original: 0x5a3c at retry path 0x16680)
+ // Lives remaining â L1NEW briefing then retry (original: 0x16675)
playCinematic("LVL1/L1NEW.ANM");
if (_vm->shouldQuit())
return false;
- // Loop back to gameplay
+ // Loop back to gameplay (original: JMP 0x16214 â health reset + Stage 1)
}
return false;
diff --git a/engines/scumm/insane/insane_rebel1.h b/engines/scumm/insane/insane_rebel1.h
index bd10c389489..82ce46cd084 100644
--- a/engines/scumm/insane/insane_rebel1.h
+++ b/engines/scumm/insane/insane_rebel1.h
@@ -91,7 +91,8 @@ private:
bool runLevel1();
// Play a passive cinematic (no game callback, skippable)
- void playCinematic(const char *filename);
+ // startFrame > 0: fast-forward (decode without display) to that frame
+ void playCinematic(const char *filename, int32 startFrame = 0);
// Play interactive gameplay video (with ship physics + HUD)
void playInteractiveVideo(const char *filename);
@@ -191,6 +192,13 @@ private:
// True only while an interactive gameplay SMUSH is running.
bool _interactiveVideoActive;
+ // Path branching for levels with left/right alternative videos.
+ // Original sets nextSceneA/nextSceneB at frame 394 to enable branching.
+ // In our implementation, we check ship position after the branch frame.
+ static const int kPathBranchFrame = 394;
+ bool _pathBranchEnabled; // True after branch frame is reached
+ bool _rightPathSelected; // True if player chose the right/easy path
+
// Main menu state (for O1OPTION interactive overlay)
bool _menuActive;
bool _menuConfirmed;
diff --git a/engines/scumm/smush/smush_player.cpp b/engines/scumm/smush/smush_player.cpp
index 429af0d73cf..fbae471f39d 100644
--- a/engines/scumm/smush/smush_player.cpp
+++ b/engines/scumm/smush/smush_player.cpp
@@ -326,6 +326,7 @@ void SmushPlayer::init(int32 speed) {
_fobjOffsetY = 0;
_scrollX = 0;
_scrollY = 0;
+ _fastForwardToFrame = 0;
_vm->_smushVideoShouldFinish = false;
_vm->_smushActive = true;
@@ -1384,7 +1385,7 @@ void SmushPlayer::handleFrame(int32 frameSize, Common::SeekableReadStream &b) {
handleZlibFrameObject(subSize, b);
break;
case MKTAG('P','S','A','D'):
- if (!_compressedFileMode) {
+ if (!_compressedFileMode && _fastForwardToFrame == 0) {
audioChunk = (uint8 *)malloc(subSize + 8);
b.seek(-8, SEEK_CUR);
b.read(audioChunk, subSize + 8);
@@ -1433,7 +1434,7 @@ void SmushPlayer::handleFrame(int32 frameSize, Common::SeekableReadStream &b) {
// RA1 voice-over audio: same 12-byte header format as PSAD
// (3 Ã BE32: trackId, seqNum, param) followed by SAUD data.
// Feed to audio system identically to PSAD.
- if (!_compressedFileMode) {
+ if (!_compressedFileMode && _fastForwardToFrame == 0) {
audioChunk = (uint8 *)malloc(subSize + 8);
b.seek(-8, SEEK_CUR);
b.read(audioChunk, subSize + 8);
@@ -1942,7 +1943,15 @@ void SmushPlayer::play(const char *filename, int32 speed, int32 offset, int32 st
for (;;) {
bool skipFrame = false;
- if (!_paused) {
+ // RA1 fast-forward: process frames rapidly without display/audio
+ // until reaching the target frame. Used to skip recap sections.
+ bool fastForwarding = (_fastForwardToFrame > 0 && _frame < _fastForwardToFrame);
+
+ if (fastForwarding) {
+ // Process frame immediately without timing
+ timerCallback();
+ _updateNeeded = false;
+ } else if (!_paused) {
uint32 now, elapsed;
if (_insanity) {
@@ -1980,10 +1989,20 @@ void SmushPlayer::play(const char *filename, int32 speed, int32 offset, int32 st
}
}
- _vm->parseEvents();
- _vm->processInput();
+ // When fast-forwarding completes, reset timing so playback
+ // starts from the correct point without trying to catch up.
+ if (_fastForwardToFrame > 0 && _frame >= _fastForwardToFrame) {
+ _startFrame = _frame;
+ _startTime = _vm->_system->getMillis();
+ _fastForwardToFrame = 0;
+ }
+
+ if (!fastForwarding) {
+ _vm->parseEvents();
+ _vm->processInput();
+ }
- if (!_paused) {
+ if (!_paused && !fastForwarding) {
if (_palDirtyMax >= _palDirtyMin) {
// Apply gamma correction for Mac versions
if (_vm->_macScreen) {
@@ -2076,7 +2095,8 @@ void SmushPlayer::play(const char *filename, int32 speed, int32 offset, int32 st
_vm->_system->updateScreen();
}
- _vm->_system->delayMillis(10);
+ if (!fastForwarding)
+ _vm->_system->delayMillis(10);
}
release();
diff --git a/engines/scumm/smush/smush_player.h b/engines/scumm/smush/smush_player.h
index f594df5866f..ae942278dd6 100644
--- a/engines/scumm/smush/smush_player.h
+++ b/engines/scumm/smush/smush_player.h
@@ -190,6 +190,7 @@ private:
bool _skipNext;
bool _ra2FastForwarding; // Fast-forwarding RA2 BEG video to establish background
uint32 _frame;
+ uint32 _fastForwardToFrame; // RA1: skip display/audio until this frame (0 = disabled)
// RA2: Global FOBJ position offsets (DAT_00482c1c / DAT_00482c20 in original)
// Set by InsaneRebel2 during IACT opcode 6 processing, reset in procPostRendering.
@@ -255,6 +256,7 @@ public:
bool isAudioCallbackEnabled();
byte *getVideoPalette();
void setCurVideoFlags(int16 flags);
+ void setFastForwardToFrame(uint32 frame) { _fastForwardToFrame = frame; }
// Masked regions - areas where video should not update (e.g., destroyed enemies)
// The Insane class can add/remove regions, and decodeFrameObject will restore
Commit: 8074a8c46090a0d45c127efd8e81cf8acd785f33
https://github.com/scummvm/scummvm/commit/8074a8c46090a0d45c127efd8e81cf8acd785f33
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:27+02:00
Commit Message:
SCUMM: RA1: Refine level 1 flight handling
Changed paths:
engines/scumm/insane/insane_rebel1.cpp
engines/scumm/insane/insane_rebel1.h
diff --git a/engines/scumm/insane/insane_rebel1.cpp b/engines/scumm/insane/insane_rebel1.cpp
index dad2a5b5e68..d90e6e43de7 100644
--- a/engines/scumm/insane/insane_rebel1.cpp
+++ b/engines/scumm/insane/insane_rebel1.cpp
@@ -35,18 +35,19 @@
namespace Scumm {
// RA1 coordinate constants (scaled from RA2's 424x260 â 384x242)
-// RA2 center: (212, 130), RA1 center: (192, 121)
-// RA2 bounds: [20, 404]x[20, 240], RA1 bounds: [18, 366]x[18, 224]
-static const int16 kCenterX = 192;
-static const int16 kCenterY = 121;
-static const int16 kMinX = 18;
-static const int16 kMaxX = 366;
-static const int16 kMinY = 18;
-static const int16 kMaxY = 224;
-
-// Perspective focal lengths (scaled from RA2: focalX=0x2b, focalY=0x19)
-static const int16 kFocalX = 39; // 0x2b * 384/424 â 39
-static const int16 kFocalY = 23; // 0x19 * 242/260 â 23
+// Original coordinate space: 320x200 game viewport at (0,0) in the 384x242 buffer.
+// Ship base position in the original: (0xA0, 100) = (160, 100) = center of 320x200.
+// Accumulator range: ±0x82 (~±130), so ship can reach ~(30..290, -30..230).
+static const int16 kCenterX = 160; // _DAT_74B6 init = 0xA0
+static const int16 kCenterY = 100; // _DAT_74B8 init = 100
+static const int16 kMinX = 20;
+static const int16 kMaxX = 300;
+static const int16 kMinY = 20;
+static const int16 kMaxY = 180;
+
+// Perspective focal lengths (from original tuning table)
+static const int16 kFocalX = 43; // 0x2b
+static const int16 kFocalY = 25; // 0x19
// Decode BOMP RLE (codec 21) sprite data into a flat pixel buffer.
// Same algorithm as NutRenderer::codec21 but without palette tracking.
@@ -89,15 +90,14 @@ InsaneRebel1::InsaneRebel1(ScummEngine_v7 *scumm) : Insane(), _vm(scumm) {
_corridorRightX = kMaxX;
_corridorBottomY = kMaxY;
- _smoothedVelocity = 0;
- _verticalInput = 0;
- memset(_velocityHistory, 0, sizeof(_velocityHistory));
+ _rollAccum = 0;
+ _liftSmooth = 0;
+ _posAccumX = 0;
+ _posAccumY = 0;
_driftParam = 0;
- _driftAccum = 0;
_perspectiveX = 0;
_perspectiveY = 0;
- _viewShift = 0;
_flyControlMode = 0;
@@ -119,6 +119,7 @@ InsaneRebel1::InsaneRebel1(ScummEngine_v7 *scumm) : Insane(), _vm(scumm) {
_menuConfirmed = false;
_menuSelection = 0;
_menuFrameCounter = 0;
+ _turbulenceEnabled = false;
if (loadRA1Nut("SYS/TALKFONT.NUT", _hudFontBank)) {
debug(1, "InsaneRebel1: HUD/menu glyph font loaded from SYS/TALKFONT.NUT (%d chars)", _hudFontBank.numSprites);
} else if (loadRA1Nut("SYS/TECHFONT.NUT", _hudFontBank)) {
@@ -697,8 +698,9 @@ void InsaneRebel1::renderMainMenuOverlay(byte *dst, int pitch, int width, int he
}
}
-// Velocity-based ship physics adapted from RA2 Handler 7 (FUN_40C3CC case 4).
-// Mouse input â velocity history averaging â position delta â corridor collision â perspective.
+// Ship physics matching FUN_1DEB5 (accumulator-based position system).
+// Roll accumulator (_74CA) driven by input, position accumulators (_74C2/_74C6)
+// driven by roll + drift + cross-coupling. Ship position = base + accum >> 8.
void InsaneRebel1::updateShipPhysics() {
_frameCounter++;
@@ -707,108 +709,102 @@ void InsaneRebel1::updateShipPhysics() {
_damageCooldown--;
// --- Step 1: Mouse input as offset from screen center ---
- // Use _vm->_mouse (0-319, 0-199 virtual screen coords), same as RA2.
- // Center = (160, 100) in virtual screen space.
+ // Original: _DAT_756C (horizontal), _DAT_756E (vertical)
int16 inputX = (int16)(_vm->_mouse.x - 160);
int16 inputY = (int16)(_vm->_mouse.y - 100);
-
- // Clamp: [-160, 160] horizontal, [-127, 127] vertical (same as RA2)
- inputX = CLIP<int16>(inputX, -160, 160);
+ inputX = CLIP<int16>(inputX, -127, 127);
inputY = CLIP<int16>(inputY, -127, 127);
- // --- Step 2: Scale to [-127, 127] (same as RA2: scaledInputX = inputX * 127 / 160) ---
- int16 scaledInputX = (int16)((inputX * 127) / 160);
- int16 scaledInputY = inputY;
-
- // --- Step 3: Velocity history + smoothed average ---
- for (int i = 24; i > 0; i--)
- _velocityHistory[i] = _velocityHistory[i - 1];
- _velocityHistory[0] = scaledInputX;
-
- const int smoothWindow = 5;
- int velSum = 0;
- for (int i = 0; i < smoothWindow; i++)
- velSum += _velocityHistory[i];
- _smoothedVelocity = (int16)(velSum / smoothWindow);
-
- // --- Step 4: Position delta ---
- const int16 levelSpeed = 32;
- const int16 levelYSpeed = 48;
- int16 absSmoothVel = ABS(_smoothedVelocity);
- int16 positionDeltaX;
-
- if (_flyControlMode == 1) {
- // Mode 1: Full cross-axis coupling
- if (scaledInputX < 1)
- positionDeltaX = (int16)((levelSpeed * _smoothedVelocity - absSmoothVel * scaledInputY) >> 9);
- else
- positionDeltaX = (int16)((levelSpeed * _smoothedVelocity + absSmoothVel * scaledInputY) >> 9);
- } else {
- // Mode 0/2/3: Reduced cross-axis coupling
- if (scaledInputX < 1)
- positionDeltaX = (int16)((levelSpeed * _smoothedVelocity - (absSmoothVel * scaledInputY >> 2)) >> 9);
- else
- positionDeltaX = (int16)((levelSpeed * _smoothedVelocity + (absSmoothVel * scaledInputY >> 2)) >> 9);
- }
-
- // Original asm drift pipeline (0x1e3fc-0x1e5cb):
- // xDelta = (random(200) - 100 - driftTuning * _driftParam) / 2
- // accumX += xDelta (32-bit accumulator)
- // shipDeltaX = accumX >> 8 (divide by 256 for pixel position)
- // So drift contributes ~±0.2 px/frame. We approximate by accumulating and shifting.
- const int16 kDriftTuning = 3; // TODO: load from per-difficulty/level tuning table
- int16 driftBias = (int16)(_vm->_rnd.getRandomNumber(199) - 100 - kDriftTuning * _driftParam);
- _driftAccum += driftBias >> 1;
- _driftAccum = CLIP<int32>(_driftAccum, -0x8200, 0x8200);
- positionDeltaX += (int16)(_driftAccum >> 8);
-
- // Clamp X delta to ±12 per frame
- positionDeltaX = CLIP<int16>(positionDeltaX, -12, 12);
- _shipPosX += positionDeltaX;
-
- // Y delta (no drift in original assembly â field4 is unused)
- int16 positionDeltaY;
- if (_flyControlMode == 1) {
- positionDeltaY = (int16)((levelYSpeed * scaledInputY) >> 10);
- positionDeltaY = CLIP<int16>(positionDeltaY, -12, 12);
- } else {
- positionDeltaY = (int16)((levelYSpeed * scaledInputY) >> 10);
- }
- _shipPosY -= positionDeltaY;
-
- _verticalInput = scaledInputY;
-
- // --- Step 6: Position clamping ---
+ // --- Step 2: Roll accumulator (_74CA) ---
+ // Tuning values for Level 1 difficulty 0 (from data section)
+ static const int16 kRoll = 100; // tuning[0x1B1B]
+ static const int16 kLift = 100; // tuning[0x1B1D]
+ static const int16 kSlide = 60; // tuning[0x1B1F]
+ static const int16 kDrift = 110; // tuning[0x1B21]
+
+ // Normal mode: accumulate; mode 0x10: snap to input
+ _rollAccum += (kRoll * (int32)inputX) >> 5;
+ _rollAccum = CLIP<int32>(_rollAccum, -0x47F, 0x47F);
+
+ // --- Step 3: Vertical smoothing (_74CE) ---
+ // Exponential decay toward -inputY
+ _liftSmooth += (-_liftSmooth - (int32)inputY) >> 1;
+ _liftSmooth = CLIP<int32>(_liftSmooth, -0x20, 0x20);
+
+ // --- Step 4: Position accumulator deltas ---
+ // X delta: drift + slide coupling - cross-coupling
+ int32 rng = _turbulenceEnabled ? (int32)_vm->_rnd.getRandomNumber(199) : 100; // 0-199, centered at 100
+ int32 crossTermX;
+ if (_liftSmooth < 0)
+ crossTermX = (kLift * _liftSmooth * _rollAccum) >> 11;
+ else
+ crossTermX = (kLift * _liftSmooth * _rollAccum) >> 12;
+
+ int32 deltaX = (((rng - 100) - (int32)kDrift * _driftParam) >> 1)
+ + ((kSlide * _rollAccum) >> 7)
+ - crossTermX;
+
+ // Y delta: roll magnitude + lift cross-coupling
+ int32 absRoll = ABS(_rollAccum);
+ int32 crossTermY;
+ if (_liftSmooth < 0)
+ crossTermY = (kLift * (0x7DE - absRoll) * _liftSmooth) >> 12;
+ else
+ crossTermY = (kLift * (0x7DE - absRoll) * _liftSmooth) >> 13;
+
+ int32 deltaY = (absRoll >> 1) + crossTermY;
+
+ // --- Step 5: Update position accumulators ---
+ _posAccumX += deltaX;
+ _posAccumX = CLIP<int32>(_posAccumX, -0x8200, 0x8200);
+ _posAccumY += deltaY;
+ _posAccumY = CLIP<int32>(_posAccumY, -0x3200, 0x4600);
+
+ // --- Step 6: Derive pixel position from accumulators ---
+ // Original: _74BA = _74C2 >> 8, _74BC = _74C6 >> 8
+ // Ship position = base + offset
+ _shipPosX = kCenterX + (int16)(_posAccumX >> 8);
+ _shipPosY = kCenterY + (int16)(_posAccumY >> 8);
+
+ // Clamp to screen bounds
_shipPosX = CLIP<int16>(_shipPosX, kMinX, kMaxX);
_shipPosY = CLIP<int16>(_shipPosY, kMinY, kMaxY);
- // --- Step 7: Corridor collision (modes 0 and 2) ---
- // From FUN_1C54D: wall contact sets damageFlags bits 0-3 (directional)
- // and pushes velocity history to bounce away from the wall.
- if (_flyControlMode == 0 || _flyControlMode == 2) {
- if (_corridorRightX < _shipPosX) {
+ // --- Step 7: Corridor collision (FUN_1C54D) ---
+ // Wall contact forces position accumulators to corridor edge and sets
+ // damage flags. Flag bit 0x10 (zone hit) suppresses damage bits only.
+ {
+ bool hasZoneHit = (_damageFlags & 0x10) != 0;
+
+ if (_shipPosX > _corridorRightX) {
+ _posAccumX = (_corridorRightX - kCenterX) << 8;
_shipPosX = _corridorRightX;
- _damageFlags |= 0x02; // Right wall
- if (_damageCooldown < 5) {
- for (int i = 0; i < 25; i++)
- _velocityHistory[i] = -127;
+ if (!hasZoneHit) {
+ if (_rollAccum > -0x100)
+ _rollAccum = -0x100; // Push left
+ _damageFlags |= 0x02; // Right wall
}
}
if (_shipPosX < _corridorLeftX) {
+ _posAccumX = (_corridorLeftX - kCenterX) << 8;
_shipPosX = _corridorLeftX;
- _damageFlags |= 0x04; // Left wall
- if (_damageCooldown < 5) {
- for (int i = 0; i < 25; i++)
- _velocityHistory[i] = 127;
+ if (!hasZoneHit) {
+ if (_rollAccum < 0x100)
+ _rollAccum = 0x100; // Push right
+ _damageFlags |= 0x04; // Left wall
}
}
- if (_corridorBottomY < _shipPosY) {
- _shipPosY = _corridorBottomY;
- _damageFlags |= 0x01; // Bottom wall
- }
if (_shipPosY < _corridorTopY) {
+ _posAccumY = ((_corridorTopY - kCenterY) << 8) + 0x100;
_shipPosY = _corridorTopY;
- _damageFlags |= 0x08; // Top wall
+ if (!hasZoneHit)
+ _damageFlags |= 0x01;
+ }
+ if (_shipPosY > _corridorBottomY) {
+ _posAccumY = ((_corridorBottomY - kCenterY) << 8) - 0x100;
+ _shipPosY = _corridorBottomY;
+ if (!hasZoneHit)
+ _damageFlags |= 0x08;
}
}
@@ -833,24 +829,25 @@ void InsaneRebel1::updateShipPhysics() {
_perspectiveY = -_perspectiveY;
}
- _viewShift = CLIP<int16>(_smoothedVelocity, -127, 127);
-
- // --- Step 9: Direction sprite (5x7 grid with hysteresis) ---
- // vDir from vertical input: (0xa0 - verticalInput) >> 6
- int16 vDir = (int16)(((int)(0xa0 - _verticalInput) + ((0xa0 - _verticalInput) < 0 ? 63 : 0)) >> 6);
- vDir = CLIP<int16>(vDir, 0, 4);
-
- // hDir from smoothed velocity: (0x95 - smoothedVelocity) / 0x2b
- int16 hDir = (int16)((0x95 - _smoothedVelocity) / 0x2b);
- hDir = CLIP<int16>(hDir, 0, 6);
+ // --- Step 9: Direction sprite index (FUN_1DEB5 LAB_1e23e) ---
+ // Horizontal component from _74CA (rollAccum):
+ // |rollAccum| <= 0x80: center (0)
+ // rollAccum > 0x80: ((rollAccum - 0x80) >> 8) * 5 + 5 (right: 5,10,15,20)
+ // rollAccum < -0x80: ((abs(rollAccum) - 0x80) >> 8) * 5 + 25 (left: 25,30,35,40)
+ int hComponent;
+ if (_rollAccum > 0x80) {
+ hComponent = ((_rollAccum - 0x80) >> 8) * 5 + 5;
+ } else if (_rollAccum < -0x80) {
+ hComponent = ((-_rollAccum - 0x80) >> 8) * 5 + 25;
+ } else {
+ hComponent = 0;
+ }
- // Hysteresis at center positions
- if (hDir == 3 && ABS(_smoothedVelocity) > 10)
- hDir = (_smoothedVelocity < 1) ? 4 : 2;
- if (vDir == 2 && ABS(_verticalInput) > 15)
- vDir = (_verticalInput < 1) ? 3 : 1;
+ // Vertical component from _74CE (liftSmooth):
+ // (_74CE + 0x20) * 5 / 0x41 â 0..4 (5 rows)
+ int vComponent = (_liftSmooth + 0x20) * 5 / 0x41;
- _shipDirIndex = CLIP<int16>(vDir * 7 + hDir, 0, _shipBank.numSprites - 1);
+ _shipDirIndex = CLIP<int16>((int16)(vComponent + hComponent), 0, _shipBank.numSprites - 1);
// --- Step 10: Damage/event bit synthesis + damage processing ---
// RA1 FUN_1B297-style latches from GAME opcodes:
@@ -918,9 +915,9 @@ void InsaneRebel1::updateShipPhysics() {
}
}
- debug(7, "RA1 ship: pos=(%d,%d) vel=%d vIn=%d dx=%d dir=%d health=%d corridor=[%d,%d]-[%d,%d]",
- _shipPosX, _shipPosY, _smoothedVelocity, _verticalInput,
- positionDeltaX, _shipDirIndex, _health,
+ debug(7, "RA1 ship: pos=(%d,%d) roll=%d lift=%d accX=%d accY=%d dir=%d health=%d corridor=[%d,%d]-[%d,%d]",
+ _shipPosX, _shipPosY, _rollAccum, _liftSmooth,
+ _posAccumX, _posAccumY, _shipDirIndex, _health,
_corridorLeftX, _corridorTopY, _corridorRightX, _corridorBottomY);
}
@@ -1138,10 +1135,10 @@ void InsaneRebel1::handleGameChunk(int32 subSize, Common::SeekableReadStream &b)
_gameLatch5D = 0;
_gameLatch5F = 0;
_driftParam = 0;
- _driftAccum = 0;
- _smoothedVelocity = 0;
- _verticalInput = 0;
- memset(_velocityHistory, 0, sizeof(_velocityHistory));
+ _rollAccum = 0;
+ _liftSmooth = 0;
+ _posAccumX = 0;
+ _posAccumY = 0;
// Field1 == 0 corresponds to baseline recenter behavior in the original.
if ((int32)param1 == 0) {
@@ -1178,26 +1175,33 @@ void InsaneRebel1::handleGameChunk(int32 subSize, Common::SeekableReadStream &b)
case 0x0D:
// Corridor boundaries: per-frame flight corridor
- // Raw: 0x0D, left, top, right, bottom (all 32-bit BE)
+ // Original params: left, top, WIDTH, HEIGHT (not right/bottom!)
+ // FUN_1C54D computes center = (left+width/2, top+height/2), transforms, then checks edges.
if (subSize >= 20) {
_corridorLeftX = (int16)param1;
_corridorTopY = (int16)b.readUint32BE();
- _corridorRightX = (int16)b.readUint32BE();
- _corridorBottomY = (int16)b.readUint32BE();
- debug(5, "RA1 GAME 0x0D: corridor left=%d top=%d right=%d bottom=%d",
- _corridorLeftX, _corridorTopY, _corridorRightX, _corridorBottomY);
+ int16 corridorWidth = (int16)b.readUint32BE();
+ int16 corridorHeight = (int16)b.readUint32BE();
+ _corridorRightX = _corridorLeftX + corridorWidth;
+ _corridorBottomY = _corridorTopY + corridorHeight;
+ debug(5, "RA1 GAME 0x0D: corridor left=%d top=%d right=%d bottom=%d (w=%d h=%d)",
+ _corridorLeftX, _corridorTopY, _corridorRightX, _corridorBottomY,
+ corridorWidth, corridorHeight);
}
break;
case 0x0E:
// Secondary collision zone (FUN_1C6E9): AABB test, sets damageFlags bit 4 (0x10)
+ // Original params: left, top, WIDTH, HEIGHT (same as 0x0D)
if (subSize >= 20) {
int16 zoneLeft = (int16)param1;
int16 zoneTop = (int16)b.readUint32BE();
- int16 zoneRight = (int16)b.readUint32BE();
- int16 zoneBottom = (int16)b.readUint32BE();
- if (_shipPosX >= zoneLeft && _shipPosX <= zoneRight &&
- _shipPosY >= zoneTop && _shipPosY <= zoneBottom) {
+ int16 zoneWidth = (int16)b.readUint32BE();
+ int16 zoneHeight = (int16)b.readUint32BE();
+ int16 zoneRight = zoneLeft + zoneWidth;
+ int16 zoneBottom = zoneTop + zoneHeight;
+ if (_shipPosX > zoneLeft && _shipPosX < zoneRight &&
+ _shipPosY > zoneTop && _shipPosY < zoneBottom) {
_damageFlags |= 0x10;
}
debug(7, "RA1 GAME 0x0E: zone=[%d,%d]-[%d,%d] flags=0x%02x",
diff --git a/engines/scumm/insane/insane_rebel1.h b/engines/scumm/insane/insane_rebel1.h
index 82ce46cd084..8c8e0f7f79b 100644
--- a/engines/scumm/insane/insane_rebel1.h
+++ b/engines/scumm/insane/insane_rebel1.h
@@ -128,35 +128,35 @@ private:
int _screenWidth;
int _screenHeight;
- // Ship game-coordinate position (adapted from RA2's [20,404]x[20,240])
- // RA1 coordinate space: [18,366]x[18,224], center=(192,121)
+ // Ship screen position = kCenter + (accumulator >> 8)
+ // Original: _DAT_74B6/_74B8 (base=160,100) + _DAT_74BA/_74BC (offset)
int16 _shipPosX;
int16 _shipPosY;
// Direction sprite index (5x7 grid = 35 sprites, vDir*7 + hDir)
int16 _shipDirIndex;
- // Corridor boundaries (set by GAME opcode 0x07)
+ // Corridor boundaries (set by GAME opcode 0x0D, computed as left+width, top+height)
int16 _corridorLeftX;
int16 _corridorTopY;
int16 _corridorRightX;
int16 _corridorBottomY;
- // Physics state (velocity-based movement from RA2 Handler 7)
- int16 _smoothedVelocity; // Averaged horizontal velocity
- int16 _verticalInput; // Stored vertical input component
- int16 _velocityHistory[25]; // Horizontal velocity ring buffer
+ // Physics state (accumulator-based, matching FUN_1DEB5)
+ // _74CA: horizontal roll accumulator, driven by input * roll_tuning
+ int32 _rollAccum;
+ // _74CE: vertical smoothing, exponential decay toward -inputY
+ int32 _liftSmooth;
+ // _74C2/_74C6: position accumulators (32-bit), pixel offset = accum >> 8
+ int32 _posAccumX;
+ int32 _posAccumY;
- // Per-frame drift bias from GAME 0x07 field3 (multiplied by tuning "drift" param)
- // Original pipeline: xDelta added to 32-bit accumulator, position = accum >> 8
- // field4 is unused in the original assembly
+ // Per-frame drift bias from GAME 0x07 field3
int16 _driftParam;
- int32 _driftAccum; // 32-bit drift accumulator (position = accum >> 8)
// Perspective view offsets
int16 _perspectiveX;
int16 _perspectiveY;
- int16 _viewShift; // Clamped smoothed velocity for view transform
// Control mode (from GAME opcode 0x5E)
int16 _flyControlMode;
@@ -204,6 +204,8 @@ private:
bool _menuConfirmed;
int _menuSelection; // 0..4 maps to return values 1..5
int _menuFrameCounter;
+
+ bool _turbulenceEnabled; // Random per-frame jitter in deltaX (original has it on)
};
} // End of namespace Scumm
Commit: eca65436dd224d38097e7e3cc75bc4b11ba08591
https://github.com/scummvm/scummvm/commit/eca65436dd224d38097e7e3cc75bc4b11ba08591
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:27+02:00
Commit Message:
SCUMM: RA1: Add options
Changed paths:
engines/scumm/insane/insane_rebel1.cpp
engines/scumm/insane/insane_rebel1.h
diff --git a/engines/scumm/insane/insane_rebel1.cpp b/engines/scumm/insane/insane_rebel1.cpp
index d90e6e43de7..63eba74a924 100644
--- a/engines/scumm/insane/insane_rebel1.cpp
+++ b/engines/scumm/insane/insane_rebel1.cpp
@@ -49,6 +49,16 @@ static const int16 kMaxY = 180;
static const int16 kFocalX = 43; // 0x2b
static const int16 kFocalY = 25; // 0x19
+// Per-difficulty tuning tables from assault_data_3.bin
+// Indexed: difficulty * 0x28B + level * 0x1F + offset
+// Level 1 values (level index 0):
+// roll lift slide drift wham shot
+static const int16 kTuningLevel1[3][6] = {
+ { 100, 100, 60, 110, 15, 0 }, // Easy
+ { 100, 105, 60, 115, 25, 0 }, // Normal
+ { 105, 110, 65, 120, 30, 0 }, // Hard
+};
+
// Decode BOMP RLE (codec 21) sprite data into a flat pixel buffer.
// Same algorithm as NutRenderer::codec21 but without palette tracking.
static void decodeBomp(byte *dst, const byte *src, int width, int height, int pitch) {
@@ -77,6 +87,20 @@ static void decodeBomp(byte *dst, const byte *src, int width, int height, int pi
}
}
+void InsaneRebel1::loadTuningForLevel(int level) {
+ int d = CLIP(_difficulty, 0, 2);
+ // Currently only Level 1 tuning is hardcoded; extend for other levels later
+ (void)level;
+ _tuning.roll = kTuningLevel1[d][0];
+ _tuning.lift = kTuningLevel1[d][1];
+ _tuning.slide = kTuningLevel1[d][2];
+ _tuning.drift = kTuningLevel1[d][3];
+ _tuning.wham = kTuningLevel1[d][4];
+ _tuning.shot = kTuningLevel1[d][5];
+ debug(1, "RA1: Loaded tuning for difficulty %d: roll=%d lift=%d slide=%d drift=%d wham=%d shot=%d",
+ d, _tuning.roll, _tuning.lift, _tuning.slide, _tuning.drift, _tuning.wham, _tuning.shot);
+}
+
InsaneRebel1::InsaneRebel1(ScummEngine_v7 *scumm) : Insane(), _vm(scumm) {
_screenWidth = 384;
_screenHeight = 242;
@@ -96,6 +120,9 @@ InsaneRebel1::InsaneRebel1(ScummEngine_v7 *scumm) : Insane(), _vm(scumm) {
_posAccumY = 0;
_driftParam = 0;
+ _difficulty = 0; // Easy by default
+ loadTuningForLevel(0);
+
_perspectiveX = 0;
_perspectiveY = 0;
@@ -119,6 +146,8 @@ InsaneRebel1::InsaneRebel1(ScummEngine_v7 *scumm) : Insane(), _vm(scumm) {
_menuConfirmed = false;
_menuSelection = 0;
_menuFrameCounter = 0;
+ _optionsActive = false;
+ _optionsSel = 0;
_turbulenceEnabled = false;
if (loadRA1Nut("SYS/TALKFONT.NUT", _hudFontBank)) {
debug(1, "InsaneRebel1: HUD/menu glyph font loaded from SYS/TALKFONT.NUT (%d chars)", _hudFontBank.numSprites);
@@ -155,7 +184,33 @@ InsaneRebel1::~InsaneRebel1() {
}
bool InsaneRebel1::notifyEvent(const Common::Event &event) {
- if (_menuActive && event.type == Common::EVENT_KEYDOWN) {
+ if (_menuActive && _optionsActive && event.type == Common::EVENT_KEYDOWN) {
+ switch (event.kbd.keycode) {
+ case Common::KEYCODE_UP:
+ case Common::KEYCODE_w:
+ _optionsSel = (_optionsSel + 2) % 3;
+ return true;
+ case Common::KEYCODE_DOWN:
+ case Common::KEYCODE_s:
+ _optionsSel = (_optionsSel + 1) % 3;
+ return true;
+ case Common::KEYCODE_RETURN:
+ case Common::KEYCODE_KP_ENTER:
+ case Common::KEYCODE_SPACE:
+ _menuConfirmed = true;
+ _vm->_smushVideoShouldFinish = true;
+ return true;
+ case Common::KEYCODE_ESCAPE:
+ _optionsSel = 2; // Back
+ _menuConfirmed = true;
+ _vm->_smushVideoShouldFinish = true;
+ return true;
+ default:
+ break;
+ }
+ }
+
+ if (_menuActive && !_optionsActive && event.type == Common::EVENT_KEYDOWN) {
switch (event.kbd.keycode) {
case Common::KEYCODE_UP:
case Common::KEYCODE_w:
@@ -637,6 +692,51 @@ int InsaneRebel1::getFontBankStringWidth(const char *text) {
// Original menu strings from assault_data_3.bin at 0x5822.
// Highlight uses RA2-style flashing border box (FUN_004292d0 pattern).
void InsaneRebel1::renderMainMenuOverlay(byte *dst, int pitch, int width, int height) {
+ _menuFrameCounter++;
+
+ if (_optionsActive) {
+ // --- Options submenu ---
+ static const char *kDiffNames[3] = { "EASY", "NORMAL", "HARD" };
+
+ const int titleW = getFontBankStringWidth("GAME OPTIONS");
+ drawFontBankString(dst, pitch, width, height, (width - titleW) / 2, 36, "GAME OPTIONS");
+
+ // Build dynamic option strings
+ char diffLine[64];
+ snprintf(diffLine, sizeof(diffLine), "DIFFICULTY: %s", kDiffNames[CLIP(_difficulty, 0, 2)]);
+ const char *turbLine = _turbulenceEnabled ? "TURBULENCE: ON" : "TURBULENCE: OFF";
+ const char *kOptionsItems[3] = { diffLine, turbLine, "BACK" };
+
+ const int menuY = 60;
+ const int rowH = 16;
+
+ for (int i = 0; i < 3; i++) {
+ const int textW = getFontBankStringWidth(kOptionsItems[i]);
+ const int textX = (width - textW) / 2;
+ const int y = menuY + i * rowH;
+ drawFontBankString(dst, pitch, width, height, textX, y + 1, kOptionsItems[i]);
+
+ if (i == _optionsSel) {
+ byte highlightColor = ((_menuFrameCounter / 8) & 1) ? 248 : 240;
+ int bracketWidth = textW + 12;
+ int leftX = CLIP(textX - 6, 0, width - 1);
+ int rightX = CLIP(leftX + bracketWidth, 0, width - 1);
+ int topY = CLIP(y - 1, 0, height - 1);
+ int bottomY = CLIP(y + rowH - 2, 0, height - 1);
+ for (int x = leftX; x <= rightX; x++) {
+ dst[topY * pitch + x] = highlightColor;
+ dst[bottomY * pitch + x] = highlightColor;
+ }
+ for (int py = topY; py <= bottomY; py++) {
+ dst[py * pitch + leftX] = highlightColor;
+ dst[py * pitch + rightX] = highlightColor;
+ }
+ }
+ }
+ return;
+ }
+
+ // --- Main menu ---
static const char *kMenuItems[5] = {
"START NEW GAME",
"GAME OPTIONS",
@@ -645,8 +745,6 @@ void InsaneRebel1::renderMainMenuOverlay(byte *dst, int pitch, int width, int he
"EXIT TO DOS"
};
- _menuFrameCounter++;
-
// Center title
const int titleW = getFontBankStringWidth("MAIN MENU");
const int titleX = (width - titleW) / 2;
@@ -716,14 +814,8 @@ void InsaneRebel1::updateShipPhysics() {
inputY = CLIP<int16>(inputY, -127, 127);
// --- Step 2: Roll accumulator (_74CA) ---
- // Tuning values for Level 1 difficulty 0 (from data section)
- static const int16 kRoll = 100; // tuning[0x1B1B]
- static const int16 kLift = 100; // tuning[0x1B1D]
- static const int16 kSlide = 60; // tuning[0x1B1F]
- static const int16 kDrift = 110; // tuning[0x1B21]
-
// Normal mode: accumulate; mode 0x10: snap to input
- _rollAccum += (kRoll * (int32)inputX) >> 5;
+ _rollAccum += (_tuning.roll * (int32)inputX) >> 5;
_rollAccum = CLIP<int32>(_rollAccum, -0x47F, 0x47F);
// --- Step 3: Vertical smoothing (_74CE) ---
@@ -736,21 +828,21 @@ void InsaneRebel1::updateShipPhysics() {
int32 rng = _turbulenceEnabled ? (int32)_vm->_rnd.getRandomNumber(199) : 100; // 0-199, centered at 100
int32 crossTermX;
if (_liftSmooth < 0)
- crossTermX = (kLift * _liftSmooth * _rollAccum) >> 11;
+ crossTermX = ((int32)_tuning.lift * _liftSmooth * _rollAccum) >> 11;
else
- crossTermX = (kLift * _liftSmooth * _rollAccum) >> 12;
+ crossTermX = ((int32)_tuning.lift * _liftSmooth * _rollAccum) >> 12;
- int32 deltaX = (((rng - 100) - (int32)kDrift * _driftParam) >> 1)
- + ((kSlide * _rollAccum) >> 7)
+ int32 deltaX = (((rng - 100) - (int32)_tuning.drift * _driftParam) >> 1)
+ + (((int32)_tuning.slide * _rollAccum) >> 7)
- crossTermX;
// Y delta: roll magnitude + lift cross-coupling
int32 absRoll = ABS(_rollAccum);
int32 crossTermY;
if (_liftSmooth < 0)
- crossTermY = (kLift * (0x7DE - absRoll) * _liftSmooth) >> 12;
+ crossTermY = ((int32)_tuning.lift * (0x7DE - absRoll) * _liftSmooth) >> 12;
else
- crossTermY = (kLift * (0x7DE - absRoll) * _liftSmooth) >> 13;
+ crossTermY = ((int32)_tuning.lift * (0x7DE - absRoll) * _liftSmooth) >> 13;
int32 deltaY = (absRoll >> 1) + crossTermY;
@@ -864,10 +956,10 @@ void InsaneRebel1::updateShipPhysics() {
_health >= 0 && _deathTimer <= 0) {
// Projectile hit (bit 7 = 0x80)
if (_damageFlags & 0x80)
- _health -= kHeavyDamage;
+ _health -= _tuning.shot;
// Wall collision (bits 1,2,4 = 0x16)
if (_damageFlags & 0x16)
- _health -= kLightDamage;
+ _health -= _tuning.wham;
if (_health < 0)
_deathTimer = kDeathTimerInit;
@@ -1049,9 +1141,9 @@ void InsaneRebel1::renderHUD(byte *dst, int pitch, int width, int height) {
// Color based on damage level (matching original thresholds from FUN_1BBCB)
byte barColor;
- if (_health > kHeavyDamage * 2)
+ if (_health > _tuning.shot * 2)
barColor = 0xA0; // Green â low damage
- else if (_health > kLightDamage * 2)
+ else if (_health > _tuning.wham * 2)
barColor = 0x2C; // Yellow â moderate damage
else
barColor = 0x30; // Red â critical
@@ -1306,6 +1398,42 @@ int InsaneRebel1::runMainMenu() {
return 5;
}
+void InsaneRebel1::runOptionsMenu() {
+ _optionsSel = 0;
+ _optionsActive = true;
+
+ while (!_vm->shouldQuit()) {
+ _menuActive = true;
+ _menuConfirmed = false;
+ _menuFrameCounter = 0;
+ clearVideoBuffer();
+ playCinematic("OPEN/O1OPTION.ANM");
+ _menuActive = false;
+
+ if (_vm->shouldQuit())
+ break;
+
+ if (_menuConfirmed) {
+ switch (_optionsSel) {
+ case 0:
+ // Cycle difficulty
+ _difficulty = (_difficulty + 1) % 3;
+ loadTuningForLevel(0);
+ break;
+ case 1:
+ // Toggle turbulence
+ _turbulenceEnabled = !_turbulenceEnabled;
+ break;
+ case 2:
+ // Back to main menu
+ _optionsActive = false;
+ return;
+ }
+ }
+ }
+ _optionsActive = false;
+}
+
// Level 1 flow (0x16100-0x167A2, from disassembly):
// 1. Load NUTs (L1BANK1, L1BANK2, L1EXPLD, L1BANG, L1LASER)
// 2. L1HANGAR.ANM â Full hangar departure cutscene (782 frames, flags 0x0420)
@@ -1442,11 +1570,15 @@ void InsaneRebel1::runGame() {
// Return to menu after level ends
break;
}
+ case 2:
+ // Game Options
+ runOptionsMenu();
+ break;
case 5:
// Exit
return;
default:
- // Options, Passcode, Demo â not yet implemented, return to menu
+ // Passcode, Demo â not yet implemented, return to menu
break;
}
}
diff --git a/engines/scumm/insane/insane_rebel1.h b/engines/scumm/insane/insane_rebel1.h
index 8c8e0f7f79b..ceca0ab629b 100644
--- a/engines/scumm/insane/insane_rebel1.h
+++ b/engines/scumm/insane/insane_rebel1.h
@@ -161,6 +161,22 @@ private:
// Control mode (from GAME opcode 0x5E)
int16 _flyControlMode;
+ // Difficulty (0=easy, 1=normal, 2=hard) â matches original DAT_22BC
+ int _difficulty;
+
+ // Per-difficulty tuning (from assault_data_3.bin, indexed: difficulty * 0x28B + level * 0x1F)
+ struct TuningParams {
+ int16 roll; // 0x1B1B: horizontal speed/sensitivity
+ int16 lift; // 0x1B1D: vertical speed/sensitivity
+ int16 slide; // 0x1B1F: cross-axis coupling
+ int16 drift; // 0x1B21: drift/turbulence multiplier
+ int16 wham; // 0x1B27: light/wall damage
+ int16 shot; // 0x1B29: heavy/projectile damage
+ };
+ TuningParams _tuning;
+
+ void loadTuningForLevel(int level);
+
// Damage system (from Ghidra decompilation of FUN_1DEB5)
int16 _health; // 0x7560: current health (init=98, negative=dead, max=98)
int16 _lives; // 0x7562: remaining extra lives
@@ -178,10 +194,6 @@ private:
static const int16 kDeathTimerInit = 30;
static const int16 kDamageCooldownInit = 10;
- // Tuning damage values (TODO: load from data section per difficulty/level)
- static const int16 kLightDamage = 5; // "wham" â wall/zone collision
- static const int16 kHeavyDamage = 15; // "shot" â projectile hit
-
// Audio state (same structure as RA2)
static const int kMaxAudioTracks = 4;
Audio::QueuingAudioStream *_audioStreams[kMaxAudioTracks];
@@ -199,12 +211,17 @@ private:
bool _pathBranchEnabled; // True after branch frame is reached
bool _rightPathSelected; // True if player chose the right/easy path
- // Main menu state (for O1OPTION interactive overlay)
+ // Main menu / options state
+ void runOptionsMenu();
bool _menuActive;
bool _menuConfirmed;
int _menuSelection; // 0..4 maps to return values 1..5
int _menuFrameCounter;
+ // Options submenu state
+ bool _optionsActive; // True when showing options instead of main menu
+ int _optionsSel; // 0=difficulty, 1=turbulence, 2=back
+
bool _turbulenceEnabled; // Random per-frame jitter in deltaX (original has it on)
};
Commit: 5a0f9d09a83c41de926638ee58e3c160f315a25c
https://github.com/scummvm/scummvm/commit/5a0f9d09a83c41de926638ee58e3c160f315a25c
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:27+02:00
Commit Message:
SCUMM: RA1: Add branching
Changed paths:
engines/scumm/insane/insane_rebel1.cpp
engines/scumm/insane/insane_rebel1.h
diff --git a/engines/scumm/insane/insane_rebel1.cpp b/engines/scumm/insane/insane_rebel1.cpp
index 63eba74a924..ac244829986 100644
--- a/engines/scumm/insane/insane_rebel1.cpp
+++ b/engines/scumm/insane/insane_rebel1.cpp
@@ -140,6 +140,7 @@ InsaneRebel1::InsaneRebel1(ScummEngine_v7 *scumm) : Insane(), _vm(scumm) {
_screenFlash = 0;
_frameCounter = 0;
_interactiveVideoActive = false;
+ _gameCounter = 0;
_pathBranchEnabled = false;
_rightPathSelected = false;
_menuActive = false;
@@ -993,17 +994,15 @@ void InsaneRebel1::updateShipPhysics() {
_damageFlags = 0;
// --- Path branching detection ---
- // Original enables branching at frame 394 (nextSceneA/nextSceneB in FUN_1B297).
- // After this frame, if the ship is on the right side of the screen, the player
- // has chosen the right/easy path. We signal the video to stop so runLevel1()
- // can switch to L1PLAY1R.
- if (_pathBranchEnabled && !_rightPathSelected && _frameCounter > (uint32)kPathBranchFrame) {
- // Ship past center-right threshold = right path chosen
- static const int16 kRightPathThreshold = kCenterX + 40;
- if (_shipPosX > kRightPathThreshold) {
+ // Original (FUN_1B297): at GAME counter 394 (0x18A), sets nextSceneA=0x67/nextSceneB=0x69.
+ // After this point, drift goes strongly negative (pushing ship left for the hard path).
+ // If ship is right of center, player chose the right/easy path â switch to L1PLAY1R.
+ // The check fires once when the game counter first reaches the branch point.
+ if (_pathBranchEnabled && !_rightPathSelected && _gameCounter >= kPathBranchCounter) {
+ if (_shipPosX > kCenterX) {
_rightPathSelected = true;
_vm->_smushVideoShouldFinish = true;
- debug(1, "RA1: Right path selected at frame %d (shipX=%d)", _frameCounter, _shipPosX);
+ debug(1, "RA1: Right path selected (counter=%d, shipX=%d)", _gameCounter, _shipPosX);
}
}
@@ -1254,14 +1253,15 @@ void InsaneRebel1::handleGameChunk(int32 subSize, Common::SeekableReadStream &b)
break;
case 0x07:
- // Per-frame corridor data: f1=frame index, f2=constant(788), f3=drift bias, f4=unused
- // Original asm: drift bias * tuning "drift" param, combined with random turbulence
- // f4 is never referenced in the original handler function
+ // Per-frame corridor data: f1=frame counter, f2=max frames, f3=drift bias, f4=unused
+ // f1 is the original's _DAT_7740 (game frame counter)
+ // f3 is the drift/wind parameter combined with tuning table
+ _gameCounter = param1;
if (subSize >= 20) {
- b.readUint32BE(); // f2 (constant 788, unused in physics)
+ b.readUint32BE(); // f2 (max frames, unused in physics)
_driftParam = (int16)(int32)b.readUint32BE();
b.readUint32BE(); // f4 (unused in original assembly)
- debug(7, "RA1 GAME 0x07: frame=%d driftParam=%d", param1, _driftParam);
+ debug(7, "RA1 GAME 0x07: counter=%d driftParam=%d", _gameCounter, _driftParam);
}
break;
@@ -1476,10 +1476,13 @@ bool InsaneRebel1::runLevel1() {
_deathTimer = 0;
_screenFlash = 0;
_frameCounter = 0;
+ _gameCounter = 0;
_pathBranchEnabled = true;
_rightPathSelected = false;
// Stage 1 flight â L1PLAY1L (hard/left path)
+ // The first 394 frames are the common section. At counter 394, if
+ // ship is right of center, we switch to L1PLAY1R (easy path).
playInteractiveVideo("LVL1/L1PLAY1L.ANM");
if (_vm->shouldQuit())
return false;
@@ -1588,6 +1591,10 @@ void InsaneRebel1::runGame() {
void InsaneRebel1::playInteractiveVideo(const char *filename) {
debug(1, "InsaneRebel1::playInteractiveVideo('%s')", filename);
+ // Stop any leftover audio from previous video
+ terminateAudio();
+ initAudio(_audioSampleRate);
+
SmushPlayer *splayer = _vm->_splayer;
_player = splayer;
clearBit(0);
diff --git a/engines/scumm/insane/insane_rebel1.h b/engines/scumm/insane/insane_rebel1.h
index ceca0ab629b..3f884d8b894 100644
--- a/engines/scumm/insane/insane_rebel1.h
+++ b/engines/scumm/insane/insane_rebel1.h
@@ -205,10 +205,11 @@ private:
bool _interactiveVideoActive;
// Path branching for levels with left/right alternative videos.
- // Original sets nextSceneA/nextSceneB at frame 394 to enable branching.
- // In our implementation, we check ship position after the branch frame.
- static const int kPathBranchFrame = 394;
- bool _pathBranchEnabled; // True after branch frame is reached
+ // Original sets nextSceneA/nextSceneB when GAME 0x07 counter == 394 (0x18A).
+ // We check ship position at that counter value to decide left vs right path.
+ static const int32 kPathBranchCounter = 394; // GAME 0x07 field1 value
+ int32 _gameCounter; // GAME 0x07 field1 â the original's _DAT_7740
+ bool _pathBranchEnabled; // True when branching is active for this video
bool _rightPathSelected; // True if player chose the right/easy path
// Main menu / options state
Commit: c0146e48e02b067d14d59c199e78dd2dd36decad
https://github.com/scummvm/scummvm/commit/c0146e48e02b067d14d59c199e78dd2dd36decad
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:28+02:00
Commit Message:
SCUMM: RA1: Add level 2 and HUD features
Changed paths:
engines/scumm/insane/insane_rebel1.cpp
engines/scumm/insane/insane_rebel1.h
engines/scumm/smush/smush_player.cpp
engines/scumm/smush/smush_player.h
engines/scumm/smush/smush_player_ra2.cpp
diff --git a/engines/scumm/insane/insane_rebel1.cpp b/engines/scumm/insane/insane_rebel1.cpp
index ac244829986..8911ad08a45 100644
--- a/engines/scumm/insane/insane_rebel1.cpp
+++ b/engines/scumm/insane/insane_rebel1.cpp
@@ -51,13 +51,22 @@ static const int16 kFocalY = 25; // 0x19
// Per-difficulty tuning tables from assault_data_3.bin
// Indexed: difficulty * 0x28B + level * 0x1F + offset
-// Level 1 values (level index 0):
-// roll lift slide drift wham shot
-static const int16 kTuningLevel1[3][6] = {
- { 100, 100, 60, 110, 15, 0 }, // Easy
- { 100, 105, 60, 115, 25, 0 }, // Normal
- { 105, 110, 65, 120, 30, 0 }, // Hard
+// Fields: roll, lift, slide, drift, snap, miss, wham, shot, kill
+static const int16 kTuningTable[2][3][9] = {
+ // Level 1 (Flight Training)
+ {
+ { 100, 100, 60, 110, 0, 0, 15, 0, 0 }, // Easy
+ { 100, 105, 60, 115, 0, 0, 25, 0, 0 }, // Normal
+ { 105, 110, 65, 120, 0, 0, 30, 0, 0 }, // Hard
+ },
+ // Level 2 (Asteroid Field Training)
+ {
+ { 100, 16, 120, 0, 7, 0, 15, 0, 25 }, // Easy
+ { 100, 18, 120, 0, 5, 0, 20, 0, 50 }, // Normal
+ { 100, 20, 150, 0, 1, 0, 25, 0, 75 }, // Hard
+ },
};
+static const int kNumTunedLevels = 2;
// Decode BOMP RLE (codec 21) sprite data into a flat pixel buffer.
// Same algorithm as NutRenderer::codec21 but without palette tracking.
@@ -89,16 +98,19 @@ static void decodeBomp(byte *dst, const byte *src, int width, int height, int pi
void InsaneRebel1::loadTuningForLevel(int level) {
int d = CLIP(_difficulty, 0, 2);
- // Currently only Level 1 tuning is hardcoded; extend for other levels later
- (void)level;
- _tuning.roll = kTuningLevel1[d][0];
- _tuning.lift = kTuningLevel1[d][1];
- _tuning.slide = kTuningLevel1[d][2];
- _tuning.drift = kTuningLevel1[d][3];
- _tuning.wham = kTuningLevel1[d][4];
- _tuning.shot = kTuningLevel1[d][5];
- debug(1, "RA1: Loaded tuning for difficulty %d: roll=%d lift=%d slide=%d drift=%d wham=%d shot=%d",
- d, _tuning.roll, _tuning.lift, _tuning.slide, _tuning.drift, _tuning.wham, _tuning.shot);
+ int l = CLIP(level, 0, kNumTunedLevels - 1);
+ _tuning.roll = kTuningTable[l][d][0];
+ _tuning.lift = kTuningTable[l][d][1];
+ _tuning.slide = kTuningTable[l][d][2];
+ _tuning.drift = kTuningTable[l][d][3];
+ _tuning.snap = kTuningTable[l][d][4];
+ _tuning.miss = kTuningTable[l][d][5];
+ _tuning.wham = kTuningTable[l][d][6];
+ _tuning.shot = kTuningTable[l][d][7];
+ _tuning.kill = kTuningTable[l][d][8];
+ debug(1, "RA1: Loaded tuning level=%d diff=%d: roll=%d lift=%d slide=%d snap=%d miss=%d wham=%d shot=%d kill=%d",
+ level, d, _tuning.roll, _tuning.lift, _tuning.slide, _tuning.snap, _tuning.miss,
+ _tuning.wham, _tuning.shot, _tuning.kill);
}
InsaneRebel1::InsaneRebel1(ScummEngine_v7 *scumm) : Insane(), _vm(scumm) {
@@ -126,11 +138,20 @@ InsaneRebel1::InsaneRebel1(ScummEngine_v7 *scumm) : Insane(), _vm(scumm) {
_perspectiveX = 0;
_perspectiveY = 0;
+ memset(_inputHistoryX, 0, sizeof(_inputHistoryX));
+ memset(_inputHistoryY, 0, sizeof(_inputHistoryY));
+ memset(_viewHistoryX, 0, sizeof(_viewHistoryX));
+ memset(_viewHistoryY, 0, sizeof(_viewHistoryY));
+ _avgInputX = 0;
+ _avgInputY = 0;
+
+ _currentLevel = 0;
_flyControlMode = 0;
_health = kMaxHealth;
_lives = 3;
_score = 0;
+ _prevScore = 0;
_damageFlags = 0;
_prevDamageFlags = 0;
_gameLatch5D = 0;
@@ -562,12 +583,19 @@ bool InsaneRebel1::loadRA1Nut(const char *filename, RA1SpriteBank &bank) {
}
void InsaneRebel1::loadLevelSprites(int level) {
- Common::String filename = Common::String::format("LVL%d/L%dBANK1.NUT", level, level);
- loadRA1Nut(filename.c_str(), _shipBank);
+ // Ship direction bank â not all levels have one (e.g. Level 2 is first-person)
+ Common::String bankFile = Common::String::format("LVL%d/L%dBANK1.NUT", level, level);
+ if (!loadRA1Nut(bankFile.c_str(), _shipBank)) {
+ debug(1, "InsaneRebel1: No BANK1 for level %d (first-person level)", level);
+ }
loadRA1Nut("SYS/DISPLAY.NUT", _displayBank);
+ // Explosion sprites â try BANG first, then EXPLD
Common::String bangFile = Common::String::format("LVL%d/L%dBANG.NUT", level, level);
- loadRA1Nut(bangFile.c_str(), _bangBank);
+ if (!loadRA1Nut(bangFile.c_str(), _bangBank)) {
+ Common::String expldFile = Common::String::format("LVL%d/L%dEXPLD.NUT", level, level);
+ loadRA1Nut(expldFile.c_str(), _bangBank);
+ }
}
void InsaneRebel1::procPreRendering(byte *renderBitmap) {
@@ -584,7 +612,7 @@ void InsaneRebel1::procPostRendering(byte *renderBitmap, int32 codecparam, int32
renderMainMenuOverlay(renderBitmap, pitch, width, height);
}
- if (!_interactiveVideoActive || _shipBank.numSprites == 0 || !renderBitmap)
+ if (!_interactiveVideoActive || !renderBitmap)
return;
int width = _player->_width;
@@ -593,12 +621,66 @@ void InsaneRebel1::procPostRendering(byte *renderBitmap, int32 codecparam, int32
if (height == 0) height = _screenHeight;
int pitch = width;
- updateShipPhysics();
- renderShip(renderBitmap, pitch, width, height);
+ if (_currentLevel == 1) {
+ // Level 2: first-person asteroid dodge â no ship sprite, input averaging physics
+ updateAsteroidPhysics();
+ } else {
+ // Level 1 (and others): third-person ship flight with accumulators
+ if (_shipBank.numSprites > 0) {
+ updateShipPhysics();
+ renderShip(renderBitmap, pitch, width, height);
+ }
+ }
renderExplosions(renderBitmap, pitch, width, height);
+ renderCrosshair(renderBitmap, pitch, width, height);
renderHUD(renderBitmap, pitch, width, height);
}
+// Draw crosshair/pointer at mouse position into the render buffer.
+// The original DOS game uses the INT 33h hardware mouse cursor (a red crosshair).
+// We replicate it by drawing a red cross into the render buffer each frame.
+void InsaneRebel1::renderCrosshair(byte *dst, int pitch, int width, int height) {
+ int cx = _vm->_mouse.x;
+ int cy = _vm->_mouse.y;
+
+ // Palette index 119 = (255,0,0) pure red in L2PLAY.ANM palette.
+ // Palette index 15 = (255,255,255) white, used as outline for visibility.
+ const byte colorInner = 119;
+ const byte colorOutline = 15;
+ const int size = 7; // arm length
+
+ // Helper lambda to draw a pixel with outline
+ auto drawPx = [&](int x, int y, byte c) {
+ if (x >= 0 && x < width && y >= 0 && y < height)
+ dst[y * pitch + x] = c;
+ };
+
+ // Draw outline first (1px border around each arm pixel)
+ for (int d = -size; d <= size; d++) {
+ if (d >= -1 && d <= 1) continue; // skip center area for outline
+ // Horizontal arm outline
+ drawPx(cx + d, cy - 1, colorOutline);
+ drawPx(cx + d, cy + 1, colorOutline);
+ // Vertical arm outline
+ drawPx(cx - 1, cy + d, colorOutline);
+ drawPx(cx + 1, cy + d, colorOutline);
+ }
+ // Arm endpoints
+ drawPx(cx - size - 1, cy, colorOutline);
+ drawPx(cx + size + 1, cy, colorOutline);
+ drawPx(cx, cy - size - 1, colorOutline);
+ drawPx(cx, cy + size + 1, colorOutline);
+
+ // Draw red cross arms
+ for (int d = -size; d <= size; d++) {
+ if (d == 0) continue; // gap at center
+ drawPx(cx + d, cy, colorInner); // horizontal
+ drawPx(cx, cy + d, colorInner); // vertical
+ }
+ // Center dot
+ drawPx(cx, cy, colorInner);
+}
+
void InsaneRebel1::drawFontBankString(byte *dst, int pitch, int width, int height, int x, int y, const char *text) {
if (!dst || !text || _hudFontBank.numSprites <= 0)
return;
@@ -1012,6 +1094,101 @@ void InsaneRebel1::updateShipPhysics() {
_corridorLeftX, _corridorTopY, _corridorRightX, _corridorBottomY);
}
+// Level 2+ asteroid/surface physics (FUN_1CDA7).
+// Uses 10-frame input history averaging instead of accumulators.
+// Ship position = averaged input + center offset.
+// Viewport = second history buffer for smooth camera scrolling.
+void InsaneRebel1::updateAsteroidPhysics() {
+ // Health regeneration (FUN_1BB0E): +1 every 32 frames when alive
+ if (_health >= 0 && _health < kMaxHealth && (_frameCounter & 0x1F) == 0) {
+ _health++;
+ }
+
+ // Damage application (FUN_1CDA7 lines 20-41)
+ // No cooldown â all three damage types can stack each frame
+ if (_damageFlags != 0 && _health >= 0 && _deathTimer < 1) {
+ _screenFlash = 5;
+ if (_damageFlags & 0x80)
+ _health -= _tuning.shot;
+ if (_damageFlags & 0x40)
+ _health -= _tuning.miss;
+ if (_damageFlags & 0x20)
+ _health -= _tuning.wham;
+ if (_health < 0) {
+ _deathTimer = 15; // 0x0F â shorter than Level 1's 30
+ }
+ _damageFlags = 0;
+ }
+
+ // Death fade countdown
+ if (_deathTimer > 1 && _health < 0) {
+ _deathTimer--;
+ }
+
+ // Screen flash countdown
+ if (_screenFlash > 0) {
+ _screenFlash--;
+ }
+
+ // Input history shift and averaging (FUN_1CDA7 lines 107-131)
+ // Shift history: [9] = [8], [8] = [7], ... [1] = [0], [0] = current input
+ for (int i = kInputHistorySize - 1; i > 0; i--) {
+ _inputHistoryX[i] = _inputHistoryX[i - 1];
+ _inputHistoryY[i] = _inputHistoryY[i - 1];
+ }
+
+ // Read current mouse input (mapped to -160..+160 range)
+ int mouseX = 0, mouseY = 0;
+ if (_vm->_mouse.x >= 0) {
+ mouseX = CLIP<int>(_vm->_mouse.x - kCenterX, -kCenterX, kCenterX);
+ mouseY = CLIP<int>(_vm->_mouse.y - kCenterY, -kCenterY, kCenterY);
+ }
+ _inputHistoryX[0] = (int16)mouseX;
+ _inputHistoryY[0] = (int16)mouseY;
+
+ // Average over 10 frames
+ int16 sumX = 0, sumY = 0;
+ for (int i = 0; i < kInputHistorySize; i++) {
+ sumX += _inputHistoryX[i];
+ sumY -= _inputHistoryY[i]; // original negates Y: sVar5 = sVar5 - history[i]
+ }
+ _avgInputX = (int16)(sumX / kInputHistorySize);
+ _avgInputY = (int16)(sumY / kInputHistorySize);
+
+ // Clamp (original: [-0xA0, 0xA0] horizontal, [-0x46, 0x41] vertical)
+ _avgInputX = CLIP<int16>(_avgInputX, -0xA0, 0xA0);
+ _avgInputY = CLIP<int16>(_avgInputY, -0x46, 0x41);
+
+ // Ship position = average + center offset
+ // Original: _74BE = _75A8 + 0xA0, _74C0 = _75BC + 0x46
+ _shipPosX = _avgInputX + 0xA0;
+ _shipPosY = _avgInputY + 0x46;
+
+ // Viewport history shift and averaging (FUN_1CDA7 lines 134-157)
+ for (int i = kInputHistorySize - 1; i > 0; i--) {
+ _viewHistoryX[i] = _viewHistoryX[i - 1];
+ _viewHistoryY[i] = _viewHistoryY[i - 1];
+ }
+ _viewHistoryX[0] = _avgInputX;
+ _viewHistoryY[0] = _avgInputY;
+
+ int16 viewSumX = 0, viewSumY = 0;
+ for (int i = 0; i < kInputHistorySize; i++) {
+ viewSumX += _viewHistoryX[i];
+ viewSumY += _viewHistoryY[i];
+ }
+ // Original: _74B6 = (viewAvgX >> 1) + 0x20, clamped [0, 0x40]
+ // Original: _74B8 = (viewAvgY >> 1) + 0x17, clamped [0, 0x2E]
+ _perspectiveX = CLIP<int16>((int16)(viewSumX / kInputHistorySize >> 1) + 0x20, 0, 0x40);
+ _perspectiveY = CLIP<int16>((int16)(viewSumY / kInputHistorySize >> 1) + 0x17, 0, 0x2E);
+
+ _frameCounter++;
+
+ debug(7, "RA1 asteroid: pos=(%d,%d) avg=(%d,%d) view=(%d,%d) health=%d flash=%d",
+ _shipPosX, _shipPosY, _avgInputX, _avgInputY,
+ _perspectiveX, _perspectiveY, _health, _screenFlash);
+}
+
void InsaneRebel1::renderShip(byte *dst, int pitch, int width, int height) {
// From FUN_1DEB5 LAB_1e2b2: ship drawn when health >= 0 OR deathTimer > 20
// Hidden during last 20 frames of death sequence (deathTimer 20â0)
@@ -1097,6 +1274,12 @@ void InsaneRebel1::renderHUD(byte *dst, int pitch, int width, int height) {
if (_displayBank.numSprites == 0)
return;
+ // Extra life bonus: every 10,000 points (FUN_1BBCB lines 11-27)
+ if (_score / 10000 > _prevScore / 10000) {
+ _lives++;
+ }
+ _prevScore = _score;
+
const RA1Sprite &bar = _displayBank.sprites[0];
// DISPLAY.NUT sprite is 320Ã19 at xoffs=0, yoffs=176 in the original game.
@@ -1139,13 +1322,14 @@ void InsaneRebel1::renderHUD(byte *dst, int pitch, int width, int height) {
int fillW = CLIP(healthWidth, 0, barMaxW);
// Color based on damage level (matching original thresholds from FUN_1BBCB)
+ // Palette indices: 0xD0-0xD7 = greens, 0x60-0x67 = yellows, 0xD8-0xDF = reds
byte barColor;
if (_health > _tuning.shot * 2)
- barColor = 0xA0; // Green â low damage
+ barColor = 0xD5; // Green (0,192,0) â low damage
else if (_health > _tuning.wham * 2)
- barColor = 0x2C; // Yellow â moderate damage
+ barColor = 0x63; // Yellow (255,255,31) â moderate damage
else
- barColor = 0x30; // Red â critical
+ barColor = 0xDD; // Red (192,0,0) â critical
// Flash effect on damage
if (_screenFlash > 0)
@@ -1159,15 +1343,29 @@ void InsaneRebel1::renderHUD(byte *dst, int pitch, int width, int height) {
}
}
- // Draw lives and score from DISPLAY.NUT glyphs.
- if (_hudFontBank.numSprites > 0) {
- char livesStr[8];
- Common::sprintf_s(livesStr, "%d", MAX<int>(_lives, 0));
- drawFontBankString(dst, pitch, width, height, hudX + 180, hudY + 6, livesStr);
+ // Lives: black out excess pilot icons embedded in DISPLAY.NUT background.
+ // Original FUN_1BBCB: FUN_21D66(buf, lives*10+186, 6, 51-lives*10, 9, 0, 320)
+ // Icons are 5 slots at x=186..236, each ~10px wide. Cover unused slots with black.
+ if (_lives >= 0 && _lives < 5) {
+ int coverX = hudX + _lives * 10 + 186;
+ int coverY = hudY + 6;
+ int coverW = 51 - _lives * 10;
+ int coverH = 9;
+ if (coverX >= 0 && coverY >= 0) {
+ for (int iy = 0; iy < coverH && coverY + iy < height; iy++) {
+ byte *d = dst + (coverY + iy) * pitch + coverX;
+ for (int ix = 0; ix < coverW && coverX + ix < width; ix++) {
+ d[ix] = 0x00;
+ }
+ }
+ }
+ }
+ // Score: 6-digit zero-padded at x=273, y=5 (original format '<<%%06ld' at 0x111,5)
+ if (_hudFontBank.numSprites > 0) {
char scoreStr[16];
- Common::sprintf_s(scoreStr, "%07d", MAX<int>(_score, 0));
- drawFontBankString(dst, pitch, width, height, hudX + 257, hudY + 4, scoreStr);
+ Common::sprintf_s(scoreStr, "%06d", MAX<int>(_score, 0));
+ drawFontBankString(dst, pitch, width, height, hudX + 273, hudY + 5, scoreStr);
}
}
@@ -1230,6 +1428,12 @@ void InsaneRebel1::handleGameChunk(int32 subSize, Common::SeekableReadStream &b)
_liftSmooth = 0;
_posAccumX = 0;
_posAccumY = 0;
+ memset(_inputHistoryX, 0, sizeof(_inputHistoryX));
+ memset(_inputHistoryY, 0, sizeof(_inputHistoryY));
+ memset(_viewHistoryX, 0, sizeof(_viewHistoryX));
+ memset(_viewHistoryY, 0, sizeof(_viewHistoryY));
+ _avgInputX = 0;
+ _avgInputY = 0;
// Field1 == 0 corresponds to baseline recenter behavior in the original.
if ((int32)param1 == 0) {
@@ -1301,7 +1505,19 @@ void InsaneRebel1::handleGameChunk(int32 subSize, Common::SeekableReadStream &b)
}
break;
- case 0x08: case 0x09: case 0x0A: case 0x0B:
+ case 0x0B:
+ // Asteroid/surface per-frame handler (FUN_1CDA7).
+ // field1 = frame counter, field2 = max frames
+ _gameCounter = param1;
+ if (subSize >= 20) {
+ b.readUint32BE(); // field2 (max frames)
+ b.readUint32BE(); // field3
+ b.readUint32BE(); // field4
+ }
+ debug(7, "RA1 GAME 0x0B: counter=%d", _gameCounter);
+ break;
+
+ case 0x08: case 0x09: case 0x0A:
case 0x19: case 0x1A:
if (subSize >= 20) {
uint32 param2 = b.readUint32BE();
@@ -1544,6 +1760,69 @@ bool InsaneRebel1::runLevel1() {
return false;
}
+// Level 2: Asteroid Field Training
+// Flow: L2NEW â L2INTRO â L2PLAY (interactive) â L2END/L2DEATH
+bool InsaneRebel1::runLevel2() {
+ debug(1, "InsaneRebel1: Running level 2");
+
+ _currentLevel = 1;
+ loadLevelSprites(2);
+ loadTuningForLevel(1);
+
+ // L2INTRO.ANM â intro cutscene (481 frames)
+ playCinematic("LVL2/L2INTRO.ANM");
+ if (_vm->shouldQuit())
+ return false;
+
+ // Retry loop
+ while (!_vm->shouldQuit()) {
+ // Reset state for this attempt
+ _health = kMaxHealth;
+ _damageFlags = 0;
+ _prevDamageFlags = 0;
+ _damageCooldown = 0;
+ _deathTimer = 0;
+ _screenFlash = 0;
+ _frameCounter = 0;
+ _gameCounter = 0;
+ memset(_inputHistoryX, 0, sizeof(_inputHistoryX));
+ memset(_inputHistoryY, 0, sizeof(_inputHistoryY));
+ memset(_viewHistoryX, 0, sizeof(_viewHistoryX));
+ memset(_viewHistoryY, 0, sizeof(_viewHistoryY));
+ _avgInputX = 0;
+ _avgInputY = 0;
+
+ // L2PLAY.ANM â asteroid dodge (800 frames, interactive)
+ playInteractiveVideo("LVL2/L2PLAY.ANM");
+ if (_vm->shouldQuit())
+ return false;
+
+ if (_health >= 0) {
+ // Level complete!
+ playCinematic("LVL2/L2END.ANM");
+ return true;
+ }
+
+ // Death
+ playCinematic("LVL2/L2DEATH.ANM");
+ if (_vm->shouldQuit())
+ return false;
+
+ _lives--;
+ if (_lives <= 0) {
+ debug(1, "InsaneRebel1: Game over (no lives left)");
+ return false;
+ }
+
+ // Retry briefing
+ playCinematic("LVL2/L2NEW.ANM");
+ if (_vm->shouldQuit())
+ return false;
+ }
+
+ return false;
+}
+
// Main game entry point â called from ScummEngine::go().
// Matches original flow at 0x15597: intro â menu â level.
void InsaneRebel1::runGame() {
@@ -1560,16 +1839,24 @@ void InsaneRebel1::runGame() {
switch (menuResult) {
case 1: {
+#if 0 // Skip level 1 for testing â jump straight to level 2
// Start New Game â play L1NEW briefing then level 1
playCinematic("LVL1/L1NEW.ANM");
if (_vm->shouldQuit())
return;
bool completed = runLevel1();
- if (completed) {
- debug(1, "InsaneRebel1: Level 1 completed!");
- // TODO: Continue to level 2
+#else
+ bool completed = true;
+#endif
+ if (completed && !_vm->shouldQuit()) {
+ completed = runLevel2();
+ if (completed) {
+ debug(1, "InsaneRebel1: Level 2 completed!");
+ // TODO: Continue to level 3
+ }
}
+ _currentLevel = 0;
// Return to menu after level ends
break;
}
@@ -1602,7 +1889,7 @@ void InsaneRebel1::playInteractiveVideo(const char *filename) {
_vm->_smushVideoShouldFinish = false;
splayer->setCurVideoFlags(0x28);
- // Center mouse, hide cursor, and lock mouse to window (like RA2 flight)
+ // Center mouse, hide system cursor (we draw our own), lock mouse to window
smush_warpMouse(160, 100, -1);
CursorMan.showMouse(false);
g_system->lockMouse(true);
diff --git a/engines/scumm/insane/insane_rebel1.h b/engines/scumm/insane/insane_rebel1.h
index 3f884d8b894..61e1b28f968 100644
--- a/engines/scumm/insane/insane_rebel1.h
+++ b/engines/scumm/insane/insane_rebel1.h
@@ -90,6 +90,9 @@ private:
// Returns true if level completed, false if player quit
bool runLevel1();
+ // Level 2 flow: NEW â INTRO â PLAY (asteroid dodge) â END/DEATH
+ bool runLevel2();
+
// Play a passive cinematic (no game callback, skippable)
// startFrame > 0: fast-forward (decode without display) to that frame
void playCinematic(const char *filename, int32 startFrame = 0);
@@ -103,6 +106,7 @@ private:
void renderHUD(byte *dst, int pitch, int width, int height);
void renderMainMenuOverlay(byte *dst, int pitch, int width, int height);
void renderExplosions(byte *dst, int pitch, int width, int height);
+ void renderCrosshair(byte *dst, int pitch, int width, int height);
void renderSprite(byte *dst, int pitch, int width, int height,
int x, int y, const RA1Sprite &sprite);
@@ -154,10 +158,25 @@ private:
// Per-frame drift bias from GAME 0x07 field3
int16 _driftParam;
- // Perspective view offsets
+ // Perspective view offsets (0x74B6/0x74B8: viewport scroll base)
int16 _perspectiveX;
int16 _perspectiveY;
+ // Input history buffers for 0x0B handler (FUN_1CDA7) â 10-frame averaging
+ static const int kInputHistorySize = 10;
+ int16 _inputHistoryX[kInputHistorySize]; // 0x7580: horizontal input history
+ int16 _inputHistoryY[kInputHistorySize]; // 0x7594: vertical input history
+ int16 _viewHistoryX[kInputHistorySize]; // 0x75A8: viewport horizontal history
+ int16 _viewHistoryY[kInputHistorySize]; // 0x75BC: viewport vertical history
+ int16 _avgInputX; // smoothed horizontal input (clamped to [-0xA0, 0xA0])
+ int16 _avgInputY; // smoothed vertical input (clamped to [-0x46, 0x41])
+
+ // 0x0B handler physics update (asteroid/surface levels)
+ void updateAsteroidPhysics();
+
+ // Current level index (0-based: 0=LVL1, 1=LVL2, etc.)
+ int _currentLevel;
+
// Control mode (from GAME opcode 0x5E)
int16 _flyControlMode;
@@ -170,8 +189,11 @@ private:
int16 lift; // 0x1B1D: vertical speed/sensitivity
int16 slide; // 0x1B1F: cross-axis coupling
int16 drift; // 0x1B21: drift/turbulence multiplier
+ int16 snap; // 0x1B23: hit radius for shooting targets
+ int16 miss; // 0x1B25: obstacle collision damage (0x0B bit 0x40)
int16 wham; // 0x1B27: light/wall damage
int16 shot; // 0x1B29: heavy/projectile damage
+ int16 kill; // 0x1B2B: score per target kill
};
TuningParams _tuning;
@@ -181,6 +203,7 @@ private:
int16 _health; // 0x7560: current health (init=98, negative=dead, max=98)
int16 _lives; // 0x7562: remaining extra lives
int _score; // 0x7564: current score
+ int _prevScore; // 0x8288: previous score (for extra life bonus at 10k intervals)
byte _damageFlags; // 0x74D4: per-frame collision bitmask (cleared each frame)
byte _prevDamageFlags; // 0x74D6: previous frame's damage flags (for explosion direction)
uint16 _gameLatch5D; // 0x75D2: GAME 0x5D latch (scene/obstacle/event trigger)
diff --git a/engines/scumm/smush/smush_player.cpp b/engines/scumm/smush/smush_player.cpp
index fbae471f39d..717e6c006c7 100644
--- a/engines/scumm/smush/smush_player.cpp
+++ b/engines/scumm/smush/smush_player.cpp
@@ -53,6 +53,7 @@
#include "audio/decoders/vorbis.h"
#include "common/compression/deflate.h"
+#include "common/memstream.h"
namespace Scumm {
@@ -1357,9 +1358,31 @@ void SmushPlayer::handleFrame(int32 frameSize, Common::SeekableReadStream &b) {
}
while (frameSize > 0) {
- const uint32 subType = b.readUint32BE();
- const int32 subSize = b.readUint32BE();
- const int32 subOffset = b.pos();
+ const int32 chunkStart = b.pos();
+ uint32 subType = b.readUint32BE();
+ int32 subSize = b.readUint32BE();
+ int32 subOffset = b.pos();
+
+ // RA1 workaround: some frames have missing padding bytes after
+ // odd-sized sub-chunks (authoring tool bug in 4 frames of L2PLAY.ANM).
+ // If the tag looks invalid, back up 1 byte and retry.
+ if (isRA1() && frameSize > 8) {
+ byte b0 = (subType >> 24) & 0xFF;
+ byte b1 = (subType >> 16) & 0xFF;
+ byte b2 = (subType >> 8) & 0xFF;
+ byte b3 = subType & 0xFF;
+ bool validTag = (b0 >= 0x20 && b0 <= 0x7E) && (b1 >= 0x20 && b1 <= 0x7E) &&
+ (b2 >= 0x20 && b2 <= 0x7E) && (b3 >= 0x00 && b3 <= 0x7E);
+ if (!validTag) {
+ // Try 1 byte earlier (missing padding byte)
+ b.seek(chunkStart - 1, SEEK_SET);
+ subType = b.readUint32BE();
+ subSize = b.readUint32BE();
+ subOffset = b.pos();
+ frameSize++;
+ debugC(DEBUG_SMUSH, "SmushPlayer::handleFrame: RA1 realigned to %s at frame %d", tag2str(subType), _frame);
+ }
+ }
// RA2: When _skipNext is set, skip the NEXT chunk of ANY type
if (isRA2() && _skipNext) {
@@ -1453,6 +1476,77 @@ void SmushPlayer::handleFrame(int32 frameSize, Common::SeekableReadStream &b) {
case MKTAG('P','S','D','2'):
debugC(DEBUG_SMUSH, "SmushPlayer::handleFrame: skipping RA1 chunk %s (%d bytes)", tag2str(subType), subSize);
break;
+ case MKTAG('O','B','J','\0'):
+ // RA1 object overlay chunk: variable-size header + embedded FOBJ
+ // sprites (including the cockpit overlay), GAME, and PSAD chunks.
+ // The reported size field is unreliable â remaining FRME data after
+ // OBJ\0 contains unstructured data between the reported end and
+ // subsequent sub-chunks. Read ALL remaining FRME data and scan for
+ // embedded sub-chunks, then stop frame parsing.
+ if (isRA1()) {
+ int32 objDataSize = frameSize - 8;
+ if (objDataSize > 0) {
+ byte *objBuf = (byte *)malloc(objDataSize);
+ b.read(objBuf, objDataSize);
+
+ int32 objPos = 0;
+ while (objPos + 8 < objDataSize) {
+ uint32 embTag = READ_BE_UINT32(objBuf + objPos);
+ uint32 embSize = READ_BE_UINT32(objBuf + objPos + 4);
+ int32 embRemaining = objDataSize - objPos - 8;
+
+ bool recognized = (embTag == MKTAG('F','O','B','J') ||
+ embTag == MKTAG('G','A','M','E') ||
+ embTag == MKTAG('P','S','A','D'));
+
+ if (!recognized || embSize > (uint32)embRemaining) {
+ // Not a recognized tag or size exceeds remaining data.
+ // Advance byte-by-byte through the OBJ header.
+ objPos++;
+ continue;
+ }
+
+ if (embTag == MKTAG('F','O','B','J') && embSize >= 14) {
+ Common::MemoryReadStream embStream(objBuf + objPos + 8, embSize);
+ handleFrameObject(embSize, embStream);
+
+ // Save the largest OBJ embedded FOBJ as the cockpit overlay
+ // to re-render every subsequent frame.
+ if (_ra1ObjOverlayData == nullptr ||
+ (int32)embSize > _ra1ObjOverlayDataSize) {
+ free(_ra1ObjOverlayData);
+ _ra1ObjOverlayDataSize = embSize;
+ _ra1ObjOverlayData = (byte *)malloc(embSize);
+ memcpy(_ra1ObjOverlayData, objBuf + objPos + 8, embSize);
+ // Parse FOBJ header for codec/position/size
+ _ra1ObjOverlayCodec = objBuf[objPos + 8] & 0xFF;
+ _ra1ObjOverlayLeft = (int16)READ_LE_UINT16(objBuf + objPos + 10);
+ _ra1ObjOverlayTop = (int16)READ_LE_UINT16(objBuf + objPos + 12);
+ _ra1ObjOverlayWidth = READ_LE_UINT16(objBuf + objPos + 14);
+ _ra1ObjOverlayHeight = READ_LE_UINT16(objBuf + objPos + 16);
+ }
+ } else if (embTag == MKTAG('G','A','M','E')) {
+ Common::MemoryReadStream embStream(objBuf + objPos + 8, embSize);
+ InsaneRebel1 *rebel1 = (InsaneRebel1 *)_vm->_insane;
+ rebel1->handleGameChunk(embSize, embStream);
+ } else if (embTag == MKTAG('P','S','A','D')) {
+ if (!_compressedFileMode && _fastForwardToFrame == 0) {
+ uint8 *audioBuf = (uint8 *)malloc(embSize + 8);
+ memcpy(audioBuf, objBuf + objPos, embSize + 8);
+ feedAudio(audioBuf, 0, 127, 0, 0);
+ free(audioBuf);
+ }
+ }
+
+ objPos += 8 + embSize;
+ if (embSize & 1) objPos++;
+ }
+ free(objBuf);
+ }
+ frameSize = 0;
+ continue;
+ }
+ break;
default:
error("Unknown frame subChunk found : %s, %d", tag2str(subType), subSize);
}
@@ -1465,6 +1559,13 @@ void SmushPlayer::handleFrame(int32 frameSize, Common::SeekableReadStream &b) {
}
}
+ // RA1: Re-render saved OBJ cockpit overlay every frame (drawn once in
+ // frame 0's OBJ chunk, then scene FOBJs overwrite it each frame).
+ if (isRA1() && _ra1ObjOverlayData != nullptr && _frame > 0) {
+ Common::MemoryReadStream overlayStream(_ra1ObjOverlayData, _ra1ObjOverlayDataSize);
+ handleFrameObject(_ra1ObjOverlayDataSize, overlayStream);
+ }
+
if (_insanity) {
_vm->_insane->procPostRendering(_dst, 0, 0, 0, _frame, _nbframes-1);
}
diff --git a/engines/scumm/smush/smush_player.h b/engines/scumm/smush/smush_player.h
index ae942278dd6..5b0eaefc674 100644
--- a/engines/scumm/smush/smush_player.h
+++ b/engines/scumm/smush/smush_player.h
@@ -170,6 +170,16 @@ private:
int _storedFobjWidth;
int _storedFobjHeight;
+ // RA1: OBJ\0 embedded cockpit overlay FOBJ â drawn once in frame 0,
+ // saved here and re-rendered every subsequent frame after scene FOBJs.
+ byte *_ra1ObjOverlayData;
+ int32 _ra1ObjOverlayDataSize;
+ int _ra1ObjOverlayCodec;
+ int _ra1ObjOverlayLeft;
+ int _ra1ObjOverlayTop;
+ int _ra1ObjOverlayWidth;
+ int _ra1ObjOverlayHeight;
+
// RA2: Most recently decoded FOBJ in the current frame, used by GOST chunks
// to re-render the same sprite payload at a different position.
byte *_lastFobjData;
diff --git a/engines/scumm/smush/smush_player_ra2.cpp b/engines/scumm/smush/smush_player_ra2.cpp
index 176dc179b42..4dcf9168838 100644
--- a/engines/scumm/smush/smush_player_ra2.cpp
+++ b/engines/scumm/smush/smush_player_ra2.cpp
@@ -73,6 +73,13 @@ void SmushPlayer::ra2InitFields() {
_lastFobjWidth = 0;
_lastFobjHeight = 0;
_hasFrameFobjForGost = false;
+ _ra1ObjOverlayData = nullptr;
+ _ra1ObjOverlayDataSize = 0;
+ _ra1ObjOverlayCodec = 0;
+ _ra1ObjOverlayLeft = 0;
+ _ra1ObjOverlayTop = 0;
+ _ra1ObjOverlayWidth = 0;
+ _ra1ObjOverlayHeight = 0;
_skipNext = false;
_ra2FastForwarding = false;
_fobjOffsetX = 0;
@@ -100,6 +107,8 @@ void SmushPlayer::ra2DestroyFields() {
_lastFobjData = nullptr;
free(_loadBuffer);
_loadBuffer = nullptr;
+ free(_ra1ObjOverlayData);
+ _ra1ObjOverlayData = nullptr;
}
/**
@@ -144,6 +153,9 @@ void SmushPlayer::ra2ReleaseVideo() {
free(_lastFobjData);
_lastFobjData = nullptr;
_lastFobjDataSize = 0;
+ free(_ra1ObjOverlayData);
+ _ra1ObjOverlayData = nullptr;
+ _ra1ObjOverlayDataSize = 0;
_hasFrameFobjForGost = false;
// Preserve _frameBuffer across videos so that gameplay videos (which have no
// background FOBJ) can use the stored background from the previous BEG video.
Commit: c33805415a93c96aa85c23f3b562eda23ad1965c
https://github.com/scummvm/scummvm/commit/c33805415a93c96aa85c23f3b562eda23ad1965c
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:28+02:00
Commit Message:
SCUMM: RA2: Capture mouse
Changed paths:
engines/scumm/insane/insane_rebel_levels.cpp
diff --git a/engines/scumm/insane/insane_rebel_levels.cpp b/engines/scumm/insane/insane_rebel_levels.cpp
index cdb0d1850b5..d30bc2f8ef4 100644
--- a/engines/scumm/insane/insane_rebel_levels.cpp
+++ b/engines/scumm/insane/insane_rebel_levels.cpp
@@ -21,6 +21,8 @@
#include "common/system.h"
+#include "graphics/cursorman.h"
+
#include "scumm/scumm_v7.h"
#include "scumm/smush/smush_player.h"
@@ -475,6 +477,14 @@ int InsaneRebel2::runLevel(int levelId) {
// Set the current level
_selectedLevel = levelId;
+ // Lock the mouse to the game window during gameplay.
+ // The original hides the cursor (ShowCursor(0)) and relies on Windows confining
+ // the mouse to the game window. Without locking, the cursor can escape the
+ // ScummVM window making the ship uncontrollable.
+ smush_warpMouse(160, 100, -1);
+ CursorMan.showMouse(false);
+ g_system->lockMouse(true);
+
// Initialize common player state
_playerLives = 3;
_playerShield = 255;
@@ -489,40 +499,63 @@ int InsaneRebel2::runLevel(int levelId) {
_skipSectionRequested = false;
// Dispatch to per-level handler
+ int result;
switch (levelId) {
case 1:
- return runLevel1();
+ result = runLevel1();
+ break;
case 2:
- return runLevel2();
+ result = runLevel2();
+ break;
case 3:
- return runLevel3();
+ result = runLevel3();
+ break;
case 4:
- return runLevel4();
+ result = runLevel4();
+ break;
case 5:
- return runLevel5();
+ result = runLevel5();
+ break;
case 6:
- return runLevel6();
+ result = runLevel6();
+ break;
case 7:
- return runLevel7();
+ result = runLevel7();
+ break;
case 8:
- return runLevel8();
+ result = runLevel8();
+ break;
case 9:
- return runLevel9();
+ result = runLevel9();
+ break;
case 10:
- return runLevel10();
+ result = runLevel10();
+ break;
case 11:
- return runLevel11();
+ result = runLevel11();
+ break;
case 12:
- return runLevel12();
+ result = runLevel12();
+ break;
case 13:
- return runLevel13();
+ result = runLevel13();
+ break;
case 14:
- return runLevel14();
+ result = runLevel14();
+ break;
case 15:
- return runLevel15();
+ result = runLevel15();
+ break;
default:
- return runLevel1();
+ result = runLevel1();
+ break;
}
+
+ // Unlock the mouse when returning to menu
+ g_system->lockMouse(false);
+ CursorMan.showMouse(true);
+
+ return result;
}
// ---------------------------------------------------------------------------
Commit: 2d99feb58f2e0944f7a0379649764cb3798f33e1
https://github.com/scummvm/scummvm/commit/2d99feb58f2e0944f7a0379649764cb3798f33e1
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:28+02:00
Commit Message:
SCUMM: RA2: Add level execution helpers
Changed paths:
engines/scumm/insane/insane_rebel.h
engines/scumm/insane/insane_rebel_levels.cpp
engines/scumm/insane/insane_rebel_runlevels.cpp
engines/scumm/scumm.cpp
diff --git a/engines/scumm/insane/insane_rebel.h b/engines/scumm/insane/insane_rebel.h
index 8bcb0e23170..d07ad57d577 100644
--- a/engines/scumm/insane/insane_rebel.h
+++ b/engines/scumm/insane/insane_rebel.h
@@ -367,7 +367,10 @@ public:
// Play game over video (LEVXX/XXOVER.SAN)
void playLevelGameOver(int levelId);
- // Play the ending/credits sequence
+ // Play the full ending sequence: finale + credits + epilogue (FUN_0041bbe8)
+ void playEndingSequence();
+
+ // Play main menu credits video (OPEN/O_CREDIT.SAN)
void playCreditsSequence();
// Get the directory name for a level (e.g., "LEV01" for level 1)
diff --git a/engines/scumm/insane/insane_rebel_levels.cpp b/engines/scumm/insane/insane_rebel_levels.cpp
index d30bc2f8ef4..1b61f13b70a 100644
--- a/engines/scumm/insane/insane_rebel_levels.cpp
+++ b/engines/scumm/insane/insane_rebel_levels.cpp
@@ -444,10 +444,57 @@ void InsaneRebel2::playLevelGameOver(int levelId) {
splayer->play(filename.c_str(), 12);
}
-// playCreditsSequence -- End credits (OPEN/O_CREDIT.SAN).
+// playEndingSequence -- Finale + credits + epilogue (FUN_0041bbe8).
+//
+// Original flow:
+// 1. Play difficulty-dependent finale video:
+// - Difficulty 2: FINAL/F_FIN_B.SAN
+// - Difficulty 3: FINAL/F_FIN_C.SAN
+// - Default: FINAL/F_FIN_A.SAN
+// 2. Play credits: FINAL/F_CREDIT.SAN
+// 3. Play epilogue: FINAL/F_EPILOG.SAN
+// 4. Return to main menu
+//
+void InsaneRebel2::playEndingSequence() {
+
+ debug("Rebel2: Playing ending sequence (difficulty=%d)", _difficulty);
+
+ // Switch to gameplay state to stop menu overlay rendering
+ _gameState = kStateGameplay;
+ _menuInputActive = false;
+
+ // Clear the screen to remove any leftover menu pixels
+ VirtScreen *vs = &_vm->_virtscr[kMainVirtScreen];
+ memset(vs->getPixels(0, 0), 0, vs->pitch * vs->h);
+ _vm->markRectAsDirty(kMainVirtScreen, 0, vs->w, 0, vs->h);
+
+ // Difficulty-dependent finale video
+ if (_difficulty == 2) {
+ playCinematic("FINAL/F_FIN_B.SAN");
+ } else if (_difficulty == 3) {
+ playCinematic("FINAL/F_FIN_C.SAN");
+ } else {
+ playCinematic("FINAL/F_FIN_A.SAN");
+ }
+
+ if (_vm->shouldQuit())
+ return;
+
+ // Credits
+ playCinematic("FINAL/F_CREDIT.SAN");
+
+ if (_vm->shouldQuit())
+ return;
+
+ // Epilogue
+ playCinematic("FINAL/F_EPILOG.SAN");
+}
+
+// playCreditsSequence -- Main menu credits (OPEN/O_CREDIT.SAN).
+// This is the credits accessible from the main menu, NOT the ending credits.
void InsaneRebel2::playCreditsSequence() {
- debug("Rebel2: Playing credits");
+ debug("Rebel2: Playing menu credits");
SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
splayer->setCurVideoFlags(0x20);
@@ -477,6 +524,16 @@ int InsaneRebel2::runLevel(int levelId) {
// Set the current level
_selectedLevel = levelId;
+ // Set the level type for difficulty table lookup (DAT_0047a7f8).
+ // Each original level function sets this before gameplay starts.
+ // Levels 1-6 use types 0-5, but Level 6 also uses type 6 mid-level.
+ // Levels 7-15 use types 7-15 (gap at type 6 which is Level 6 phase 2).
+ // Level 15 also switches to type 16 mid-level at frame 0x21e.
+ static const int kLevelTypeMap[16] = {
+ -1, 0, 1, 2, 3, 4, 5, 7, 8, 9, 10, 11, 12, 13, 14, 15
+ };
+ _rebelLevelType = kLevelTypeMap[levelId];
+
// Lock the mouse to the game window during gameplay.
// The original hides the cursor (ShowCursor(0)) and relies on Windows confining
// the mouse to the game window. Without locking, the cursor can escape the
@@ -579,7 +636,8 @@ Common::String InsaneRebel2::selectDeathVideoVariant(int levelId, int phase, int
switch (levelId) {
case 1:
// Level 1: Random between A and B
- return (getRandomVariant(2) == 0) ? "A" : "B";
+ // Original: random!=0 â A (offset 0), random==0 â B (offset 0x14)
+ return (getRandomVariant(2) == 0) ? "B" : "A";
case 2:
// Level 2: Just "DIE" (no variants)
@@ -614,12 +672,13 @@ Common::String InsaneRebel2::selectDeathVideoVariant(int levelId, int phase, int
}
case 4:
- // Level 4: Just "DIE" (no variants)
- return "";
+ // Level 4: Single variant "A" (original plays 04DIE_A.SAN)
+ return "A";
case 5:
- // Level 5: Random between A and B (like Level 1)
- return (getRandomVariant(2) == 0) ? "A" : "B";
+ // Level 5: Random between A and B
+ // Original: random!=0 â A (offset 0), random==0 â B (offset 0x14)
+ return (getRandomVariant(2) == 0) ? "B" : "A";
case 6:
// Level 6 (FUN_004190D6): Phase-based with detailed frame selection
@@ -679,7 +738,8 @@ Common::String InsaneRebel2::selectDeathVideoVariant(int levelId, int phase, int
case 8:
// Level 8 (FUN_00419976): Random A or B
- return (getRandomVariant(2) == 0) ? "A" : "B";
+ // Original: random!=0 â A (offset 0), random==0 â B (offset 0x14)
+ return (getRandomVariant(2) == 0) ? "B" : "A";
case 9:
// Level 9 (FUN_00419B86): Based on DAT_0047ab94 (death cause)
diff --git a/engines/scumm/insane/insane_rebel_runlevels.cpp b/engines/scumm/insane/insane_rebel_runlevels.cpp
index f1365a0024a..3071ee93dac 100644
--- a/engines/scumm/insane/insane_rebel_runlevels.cpp
+++ b/engines/scumm/insane/insane_rebel_runlevels.cpp
@@ -2102,6 +2102,9 @@ int InsaneRebel2::runLevel15() {
// FUN_00401000 + FUN_0041c7d0 + FUN_0040c040
clearBit(0);
+ // Original sets DAT_0047a7f8 = 0xf before the gameplay loop
+ _rebelLevelType = 0xf;
+
while (!_vm->shouldQuit()) {
_playerShield = 255;
_playerDamage = 0;
@@ -2109,11 +2112,10 @@ int InsaneRebel2::runLevel15() {
clearBit(0);
- // Original: DAT_0047a7f8 = 0xf (level 15) before gameplay
- // At frame 0x21e (542): DAT_0047a7f8 = 0x10 (switches to "level 16" internally)
- // After gameplay: DAT_0047a7f8 = 0x10 (stays at 16)
- // This level ID switch affects which difficulty data is used mid-level.
- // The IACT callbacks handle gameplay regardless of this ID.
+ // Original: DAT_0047a7f8 = 0xf again at start of each retry
+ // At frame 0x21e (542): switches to 0x10 (affects difficulty lookup mid-level)
+ // The frame-based switch is handled by IACT opcode 6 in the video data.
+ _rebelLevelType = 0xf;
// Play gameplay (15PLAY.SAN)
// Original: FUN_0041f4d0("15PLAY.SAN", 0x28, -1, -1, 0)
diff --git a/engines/scumm/scumm.cpp b/engines/scumm/scumm.cpp
index e609f134749..f0e8b348048 100644
--- a/engines/scumm/scumm.cpp
+++ b/engines/scumm/scumm.cpp
@@ -2768,6 +2768,11 @@ Common::Error ScummEngine::go() {
int selectedLevel = rebel->_selectedChapter + 1;
debug("ScummEngine: Starting chapter %d (level %d)", rebel->_selectedChapter + 1, selectedLevel);
+ // Ending selected directly from chapter select (FUN_0041bbe8, case 0xf)
+ if (selectedLevel == 16) {
+ rebel->playEndingSequence();
+ }
+
// Level progression loop: on success, advance to next level
// Original game chains levels directly (e.g. FUN_0040598c(FUN_00418063,0))
while (!shouldQuit() && selectedLevel >= 1 && selectedLevel <= 15) {
@@ -2778,8 +2783,8 @@ Common::Error ScummEngine::go() {
rebel->_playerScore, rebel->_playerLives, rebel->_playerDamage);
selectedLevel++;
if (selectedLevel > 15) {
- // Beat the game â play credits
- rebel->playCreditsSequence();
+ // Beat the game â play ending sequence (FUN_0041bbe8)
+ rebel->playEndingSequence();
break;
}
} else {
Commit: 1969645fbbb4df02611117b66157c9f75fe6d6a3
https://github.com/scummvm/scummvm/commit/1969645fbbb4df02611117b66157c9f75fe6d6a3
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:28+02:00
Commit Message:
SCUMM: RA2: Add credits
Changed paths:
engines/scumm/smush/smush_player.cpp
engines/scumm/smush/smush_player_ra2.cpp
engines/scumm/string_v7.cpp
diff --git a/engines/scumm/smush/smush_player.cpp b/engines/scumm/smush/smush_player.cpp
index 717e6c006c7..491d1751804 100644
--- a/engines/scumm/smush/smush_player.cpp
+++ b/engines/scumm/smush/smush_player.cpp
@@ -71,13 +71,15 @@ private:
int _nbStrings;
int _lastId;
const char *_lastString;
+ bool _preserveNewlines;
public:
StringResource() :
_nbStrings(0),
_lastId(-1),
- _lastString(nullptr) {
+ _lastString(nullptr),
+ _preserveNewlines(false) {
for (int i = 0; i < MAX_STRINGS; i++) {
_strings[i].id = 0;
_strings[i].string = nullptr;
@@ -89,6 +91,8 @@ public:
}
}
+ void setPreserveNewlines(bool preserve) { _preserveNewlines = preserve; }
+
bool init(char *buffer, int32 length) {
char *def_start = strchr(buffer, '#');
while (def_start != nullptr) {
@@ -168,11 +172,20 @@ public:
line_start = line_end+1;
if (line_start[0] == '/' && line_start[1] == '/') {
line_start += 2;
- if (line_end[-1] == '\r')
- line_end[-1] = ' ';
- else
- *line_end++ = ' ';
- memmove(line_end, line_start, strlen(line_start)+1);
+ // RA2: preserve newlines for multi-line TRES text
+ // (credits, cast lists). Other games join with spaces.
+ if (_preserveNewlines) {
+ if (line_end[-1] == '\r')
+ line_end[-1] = '\n';
+ // else line_end already points to '\n'
+ memmove(line_end + 1, line_start, strlen(line_start)+1);
+ } else {
+ if (line_end[-1] == '\r')
+ line_end[-1] = ' ';
+ else
+ *line_end++ = ' ';
+ memmove(line_end, line_start, strlen(line_start)+1);
+ }
}
}
_strings[_nbStrings].id = id;
@@ -231,6 +244,7 @@ static StringResource *getStrings(ScummEngine *vm, const char *file, bool is_enc
}
StringResource *sr = new StringResource;
assert(sr);
+ sr->setPreserveNewlines(vm->_game.id == GID_REBEL2);
sr->init(filebuffer, length);
delete[] filebuffer;
return sr;
diff --git a/engines/scumm/smush/smush_player_ra2.cpp b/engines/scumm/smush/smush_player_ra2.cpp
index 4dcf9168838..c93a67fedb3 100644
--- a/engines/scumm/smush/smush_player_ra2.cpp
+++ b/engines/scumm/smush/smush_player_ra2.cpp
@@ -498,11 +498,17 @@ void SmushPlayer::ra2ResetDeltaPalette() {
* RA2 font path table.
*/
SmushFont *SmushPlayer::ra2GetFont(int font) {
+ // Font table for low-res mode (320x200). Original exe uses pointer
+ // arithmetic to select hi/lo font pairs (e.g. TKHIFONT+0x14=TALKFONT).
+ // Font 0: TALKFONT (TKHIFONT hi-res)
+ // Font 1: SMALFONT (SMHIFONT hi-res)
+ // Font 2: TITLFONT (TIHIFONT hi-res)
+ // Font 3: POVFONT (POHIFONT hi-res)
const char *ra2_fonts[] = {
"SYSTM/TALKFONT.NUT",
- "SYSTM/DIHIFONT.NUT",
+ "SYSTM/SMALFONT.NUT",
"SYSTM/TITLFONT.NUT",
- "SYSTM/SMALFONT.NUT"
+ "SYSTM/POVFONT.NUT"
};
int numFonts = ARRAYSIZE(ra2_fonts);
if (font >= 0 && font < numFonts) {
diff --git a/engines/scumm/string_v7.cpp b/engines/scumm/string_v7.cpp
index 9088be0728d..a9fe0c80ab9 100644
--- a/engines/scumm/string_v7.cpp
+++ b/engines/scumm/string_v7.cpp
@@ -129,7 +129,10 @@ int TextRenderer_v7::getStringHeight(const char *str, uint numBytesMax) {
}
if (*str == '\n') {
- totalHeight += (lineHeight ? lineHeight : _gr->getFontHeight()) + 1;
+ int lh = lineHeight ? lineHeight : _gr->getFontHeight();
+ // RA2: add extra inter-line spacing for larger fonts (credits text)
+ int gap = (_gameId == GID_REBEL2 && lh > 8) ? lh / 2 : 1;
+ totalHeight += lh + gap;
lineHeight = 0;
} else if (*str != '\r' && *str != _lineBreakMarker) {
lineHeight = MAX<int>(lineHeight, _gr->getCharHeight(*str));
Commit: 6e1e672ffdbe0856ba1d0260a4be0b3afc71fcbb
https://github.com/scummvm/scummvm/commit/6e1e672ffdbe0856ba1d0260a4be0b3afc71fcbb
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:29+02:00
Commit Message:
SCUMM: RA2: Improve pause menu layout
Changed paths:
engines/scumm/insane/insane_rebel.cpp
engines/scumm/insane/insane_rebel.h
engines/scumm/insane/insane_rebel_menu.cpp
engines/scumm/smush/smush_multi_font.cpp
engines/scumm/smush/smush_multi_font.h
engines/scumm/smush/smush_player.h
engines/scumm/smush/smush_player_ra2.cpp
diff --git a/engines/scumm/insane/insane_rebel.cpp b/engines/scumm/insane/insane_rebel.cpp
index 42284a4254d..4496daf950e 100644
--- a/engines/scumm/insane/insane_rebel.cpp
+++ b/engines/scumm/insane/insane_rebel.cpp
@@ -27,6 +27,7 @@
#include "common/events.h"
#include "common/savefile.h"
#include "common/util.h"
+#include "graphics/paletteman.h"
#include "audio/mixer.h"
@@ -116,6 +117,8 @@ InsaneRebel2::InsaneRebel2(ScummEngine_v7 *scumm) {
// SmushFont for menu text rendering - uses SMALFONT with proper drawString support
_menuFont = new SmushFont(_vm, "SYSTM/SMALFONT.NUT", true);
+ _pauseOverlayActive = false;
+ memset(_savedPausePalette, 0, sizeof(_savedPausePalette));
// MSTOVER.NUT - Mouse Over background overlay (NOT a cursor!)
// This is loaded into DAT_0047aba8 and used as a background overlay via FUN_004236e0
@@ -541,10 +544,30 @@ InsaneRebel2::~InsaneRebel2() {
// notifyEvent -- EventObserver callback for global input dispatch.
// Handles ESC (skip) and SPACE (pause) regardless of menu state.
+// Pause behavior matches original FUN_405A21: SPACE pauses, ANY key unpauses.
bool InsaneRebel2::notifyEvent(const Common::Event &event) {
if (event.type == Common::EVENT_KEYDOWN) {
SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
+ // When paused during gameplay, ANY key unpauses (FUN_405A21 line 360-365).
+ // ESC additionally opens the ScummVM menu (original: quit key exits level).
+ if (splayer && splayer->_paused && _gameState == kStateGameplay) {
+ debug("Rebel2: Key pressed while paused - unpausing");
+ // Restore the original palette saved by showPauseOverlay
+ if (_pauseOverlayActive) {
+ _vm->_system->getPaletteManager()->setPalette(_savedPausePalette, 0, 256);
+ _pauseOverlayActive = false;
+ }
+ splayer->unpause();
+ if (event.kbd.keycode == Common::KEYCODE_ESCAPE && _rebelHandler != 0) {
+ debug("Rebel2: ESC during pause - opening ScummVM menu");
+ splayer->pause();
+ _vm->openMainMenuDialog();
+ splayer->unpause();
+ }
+ return true;
+ }
+
switch (event.kbd.keycode) {
case Common::KEYCODE_ESCAPE:
// ESC handling depends on game state:
@@ -564,8 +587,6 @@ bool InsaneRebel2::notifyEvent(const Common::Event &event) {
_vm->_smushVideoShouldFinish = true;
} else if (_gameState == kStateGameplay && _rebelHandler != 0) {
// During active gameplay (handler != 0): pause and open ScummVM menu.
- // _rebelHandler is non-zero (7, 8, 0x19, 0x26) only during interactive
- // gameplay sections, and 0 during intro/cutscene/post videos within a level.
debug("Rebel2: ESC pressed during gameplay - opening ScummVM menu");
bool wasPaused = splayer->_paused;
if (!wasPaused)
@@ -583,19 +604,13 @@ bool InsaneRebel2::notifyEvent(const Common::Event &event) {
break;
case Common::KEYCODE_SPACE:
- // SPACE toggles pause (emulates FUN_405A21 pause handling)
- // Only allow pausing during gameplay, not in menus
- if (splayer && _gameState == kStateGameplay) {
- if (splayer->_paused) {
- debug("Rebel2: SPACE pressed - unpausing");
- splayer->unpause();
- } else {
- debug("Rebel2: SPACE pressed - pausing");
- splayer->pause();
- // Show the pause overlay with dimming effect and "PAUSED" text
- showPauseOverlay();
- }
- return true; // Consume the event
+ // SPACE pauses during gameplay (FUN_405A21).
+ // Unpausing is handled above (any key while paused).
+ if (splayer && _gameState == kStateGameplay && !splayer->_paused) {
+ debug("Rebel2: SPACE pressed - pausing");
+ splayer->pause();
+ showPauseOverlay();
+ return true;
}
break;
diff --git a/engines/scumm/insane/insane_rebel.h b/engines/scumm/insane/insane_rebel.h
index d07ad57d577..88d20f74101 100644
--- a/engines/scumm/insane/insane_rebel.h
+++ b/engines/scumm/insane/insane_rebel.h
@@ -471,6 +471,10 @@ public:
// SmushFont for menu text rendering (uses SMALFONT.NUT with proper string drawing)
SmushFont *_menuFont;
+ // Saved palette for pause overlay restoration (FUN_405A21)
+ byte _savedPausePalette[768];
+ bool _pauseOverlayActive;
+
// MSTOVER.NUT - Mouse Over background overlay (NOT a cursor!)
// Loaded into DAT_0047aba8 and rendered via FUN_004236e0 as background
NutRenderer *_smush_mouseoverNut;
diff --git a/engines/scumm/insane/insane_rebel_menu.cpp b/engines/scumm/insane/insane_rebel_menu.cpp
index 82561d88b33..9bbd6115991 100644
--- a/engines/scumm/insane/insane_rebel_menu.cpp
+++ b/engines/scumm/insane/insane_rebel_menu.cpp
@@ -22,6 +22,7 @@
#include "common/system.h"
#include "common/events.h"
#include "common/util.h"
+#include "graphics/paletteman.h"
#include "audio/mixer.h"
@@ -32,6 +33,7 @@
#include "scumm/smush/smush_player.h"
#include "scumm/smush/smush_font.h"
+#include "scumm/smush/smush_multi_font.h"
#include "scumm/insane/insane_rebel.h"
@@ -559,169 +561,120 @@ void InsaneRebel2::drawMenuOverlay(byte *renderBitmap, int pitch, int width, int
// Pause Overlay
// ---------------------------------------------------------------------------
-// showPauseOverlay -- Dimmed overlay with "PAUSED" text (FUN_405A21).
+// pauseFillRect -- Helper to fill a rectangle in the frame buffer with bounds checking.
+static void pauseFillRect(byte *buf, int bufW, int bufH, int x, int y, int w, int h, byte color) {
+ if (x < 0) { w += x; x = 0; }
+ if (y < 0) { h += y; y = 0; }
+ if (x + w > bufW) w = bufW - x;
+ if (y + h > bufH) h = bufH - y;
+ if (w <= 0 || h <= 0) return;
+ for (int row = y; row < y + h; row++)
+ memset(buf + row * bufW + x, color, w);
+}
+
+// showPauseOverlay -- Dimmed overlay with metallic frame and "PAUSED" text.
+// Reproduces FUN_405A21 from the original executable.
+//
+// The original dims pixels using a green-channel formula that produces palette
+// indices 16-79, relying on a built-in grayscale ramp at those positions.
+// We instead dim the system palette (25% brightness) which achieves the same
+// visual effect regardless of palette layout. The pixel buffer is only modified
+// for the frame decorations and text â the game frame pixels stay unchanged.
+// The original palette is saved in _savedPausePalette and restored on unpause.
void InsaneRebel2::showPauseOverlay() {
SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
- if (!splayer) {
- debug("showPauseOverlay: No SmushPlayer active");
+ if (!splayer)
return;
- }
- // Get frame buffer and palette from SmushPlayer
- // _dst points to the virtual screen pixels (the actual rendering destination)
- // _frameBuffer is only used for store/fetch operations, not general rendering
byte *frameBuffer = splayer->_dst;
byte *palette = splayer->_pal;
int width = splayer->_width;
int height = splayer->_height;
- if (!frameBuffer || !palette || width <= 0 || height <= 0) {
- debug("showPauseOverlay: No frame buffer (%p), palette (%p), or invalid dimensions (%dx%d)",
- (void*)frameBuffer, (void*)palette, width, height);
+ if (!frameBuffer || !palette || width <= 0 || height <= 0)
return;
- }
-
- debug("showPauseOverlay: Applying dimming effect to %dx%d buffer", width, height);
-
- // Apply dimming effect (emulates FUN_405A21 lines 242-251)
- // Original algorithm:
- // For each pixel, take the green component of its palette entry
- // and the green component of the previous pixel's palette entry,
- // add them, divide by 8, add 16.
- // This creates a dark dimmed effect.
- int bufferSize = width * height;
- byte prevPixel = 0;
-
- for (int i = 0; i < bufferSize; i++) {
- byte curPixel = frameBuffer[i];
-
- // Get green components from palette (offset +1 in RGB triplets)
- int greenCur = palette[curPixel * 3 + 1];
- int greenPrev = palette[prevPixel * 3 + 1];
-
- // Apply dimming formula: (green1 + green2) >> 3 + 0x10
- byte dimmedValue = ((greenCur + greenPrev) >> 3) + 0x10;
-
- frameBuffer[i] = dimmedValue;
- prevPixel = curPixel;
- }
- // Draw border decorations (simplified version of FUN_405A21 lines 261-283)
- // Draw horizontal lines at top and bottom of a centered box
- int boxLeft = 12;
- int boxRight = width - 12;
- int boxTop = 23; // 0x17
- int boxBottom = height - 23; // ~175 for 200 height
-
- byte borderColor = 0x50; // Gray border color
-
- // Top and bottom borders
- for (int x = boxLeft; x < boxRight; x++) {
- if (boxTop >= 0 && boxTop < height)
- frameBuffer[boxTop * width + x] = borderColor;
- if (boxBottom >= 0 && boxBottom < height)
- frameBuffer[boxBottom * width + x] = borderColor;
+ int screenW = MIN(width, (int)_vm->_screenWidth);
+ int screenH = MIN(height, (int)_vm->_screenHeight);
+
+ // Step 1: Save current palette and create a dimmed version.
+ memcpy(_savedPausePalette, palette, 768);
+ _pauseOverlayActive = true;
+
+ byte dimPal[768];
+ memcpy(dimPal, palette, 768);
+ for (int i = 0; i < 768; i++)
+ dimPal[i] >>= 2; // 25% brightness
+
+ // Override specific palette entries for UI elements.
+ // Frame bars (0x50): medium gray
+ dimPal[0x50 * 3 + 0] = 80; dimPal[0x50 * 3 + 1] = 80; dimPal[0x50 * 3 + 2] = 80;
+ // Rivet outline (0x51): slightly lighter gray
+ dimPal[0x51 * 3 + 0] = 110; dimPal[0x51 * 3 + 1] = 110; dimPal[0x51 * 3 + 2] = 110;
+ // RA2 NUT fonts use hardcoded palette indices 1-4 for rendering:
+ // 1=body color (remapped to col param), 2=highlight, 3=anti-alias, 4=outline
+ // Index 5 is used by TRS ^c005 escape code as the text body color.
+ dimPal[1 * 3 + 0] = 255; dimPal[1 * 3 + 1] = 255; dimPal[1 * 3 + 2] = 255;
+ dimPal[2 * 3 + 0] = 188; dimPal[2 * 3 + 1] = 188; dimPal[2 * 3 + 2] = 188;
+ dimPal[3 * 3 + 0] = 128; dimPal[3 * 3 + 1] = 128; dimPal[3 * 3 + 2] = 128;
+ dimPal[4 * 3 + 0] = 0; dimPal[4 * 3 + 1] = 0; dimPal[4 * 3 + 2] = 0;
+ dimPal[5 * 3 + 0] = 252; dimPal[5 * 3 + 1] = 252; dimPal[5 * 3 + 2] = 252;
+
+ _vm->_system->getPaletteManager()->setPalette(dimPal, 0, 256);
+
+ // Step 2: Draw the metallic frame (FUN_405A21 lines 261-283).
+ // Horizontal border lines: 2px thick at y=23 and y=175
+ pauseFillRect(frameBuffer, width, height, 0, 0x17, 0x140, 2, 0x50);
+ pauseFillRect(frameBuffer, width, height, 0, 0xAF, 0x140, 2, 0x50);
+
+ // Thick side bars: 40px wide on left and right
+ pauseFillRect(frameBuffer, width, height, 0, 0, 0x28, 200, 0x50);
+ pauseFillRect(frameBuffer, width, height, 0x118, 0, 0x28, 200, 0x50);
+
+ // Rivet decorations along left side bar.
+ // Layered rectangles: outer ring (0x51), inner fill (4).
+ for (int i = 0; i < 6; i++) {
+ int yOff = i * 0x24; // i * 36
+ pauseFillRect(frameBuffer, width, height, 0x0C, yOff, 0x19, 0x11, 0x51);
+ pauseFillRect(frameBuffer, width, height, 0x0B, yOff + 1, 0x1B, 0x0F, 0x51);
+ pauseFillRect(frameBuffer, width, height, 0x0D, yOff, 0x17, 0x11, 4);
+ pauseFillRect(frameBuffer, width, height, 0x0B, yOff + 2, 0x1B, 0x0D, 4);
+ pauseFillRect(frameBuffer, width, height, 0x0C, yOff + 1, 0x19, 0x0F, 4);
}
- // Left and right borders
- for (int y = boxTop; y < boxBottom; y++) {
- if (boxLeft >= 0 && boxLeft < width)
- frameBuffer[y * width + boxLeft] = borderColor;
- if (boxRight >= 0 && boxRight < width)
- frameBuffer[y * width + boxRight] = borderColor;
+ // Right side bar rivets: same pattern at x=282 (0x11A).
+ for (int i = 0; i < 6; i++) {
+ int yOff = i * 0x24;
+ int xBase = 0x11A;
+ pauseFillRect(frameBuffer, width, height, xBase, yOff, 0x19, 0x11, 0x51);
+ pauseFillRect(frameBuffer, width, height, xBase - 1, yOff + 1, 0x1B, 0x0F, 0x51);
+ pauseFillRect(frameBuffer, width, height, xBase + 1, yOff, 0x17, 0x11, 4);
+ pauseFillRect(frameBuffer, width, height, xBase - 1, yOff + 2, 0x1B, 0x0D, 4);
+ pauseFillRect(frameBuffer, width, height, xBase, yOff + 1, 0x19, 0x0F, 4);
}
- // Draw corner decorations (simplified)
- byte cornerColor = 0x51; // Slightly brighter for corners
- for (int i = 0; i < 5; i++) {
- // Top-left corner
- if (boxTop + i < height && boxLeft + 5 < width)
- frameBuffer[(boxTop + i) * width + boxLeft + 5] = cornerColor;
- if (boxTop + 5 < height && boxLeft + i < width)
- frameBuffer[(boxTop + 5) * width + boxLeft + i] = cornerColor;
-
- // Top-right corner
- if (boxTop + i < height && boxRight - 5 >= 0)
- frameBuffer[(boxTop + i) * width + boxRight - 5] = cornerColor;
- if (boxTop + 5 < height && boxRight - i >= 0)
- frameBuffer[(boxTop + 5) * width + boxRight - i] = cornerColor;
-
- // Bottom-left corner
- if (boxBottom - i >= 0 && boxLeft + 5 < width)
- frameBuffer[(boxBottom - i) * width + boxLeft + 5] = cornerColor;
- if (boxBottom - 5 >= 0 && boxLeft + i < width)
- frameBuffer[(boxBottom - 5) * width + boxLeft + i] = cornerColor;
-
- // Bottom-right corner
- if (boxBottom - i >= 0 && boxRight - 5 >= 0)
- frameBuffer[(boxBottom - i) * width + boxRight - 5] = cornerColor;
- if (boxBottom - 5 >= 0 && boxRight - i >= 0)
- frameBuffer[(boxBottom - 5) * width + boxRight - i] = cornerColor;
+ // Step 3: Draw "Game Paused" text using TRS string 0x78 (120).
+ // Original: FUN_00434cb0(-1, buf, NULL, x=10, y=10, align=1, fg=4, bg=0x10, text)
+ // fg=4 is the foreground color (used by ^cNNN in the TRS string itself).
+ const char *pauseText = splayer->getString(0x78);
+ if (!pauseText || !pauseText[0])
+ pauseText = "Game Paused";
+
+ SmushMultiFont *multiFont = splayer->getMultiFont();
+ if (!multiFont) {
+ splayer->ensureMultiFont();
+ multiFont = splayer->getMultiFont();
}
-
- // Draw "PAUSED" text centered
- // Try to load from TRS - the exact index may vary by language version
- // TRS index 80 (0x50) is likely "PAUSED" or equivalent (from DAT_004573f8)
- // Note: splayer is already defined at the start of this function
- const char *pauseText = splayer ? splayer->getString(80) : nullptr;
- if (!pauseText || !pauseText[0]) {
- // Fallback only if TRS string not available
- pauseText = "PAUSED";
- }
-
- // Draw text using SmushFont if available
- if (_menuFont) {
- Common::Rect clipRect(0, 0, width, height);
-
- // Calculate centered position
- // Text should be centered horizontally and vertically in the box
- int textX = width / 2; // SmushFont handles centering with kStyleAlignCenter
- int textY = height / 2 - 4; // Slightly above center
-
- // Draw with color 4 and background 0x10 (matching original parameters)
- // FUN_00434cb0 params: x=10, y=10 or 20, color=4, bg=0x10
- _menuFont->drawString(pauseText, frameBuffer, clipRect, textX, textY, 0x10, kStyleAlignCenter);
- } else if (_smush_smalfontNut) {
- // Fallback: draw using NutRenderer directly
- NutRenderer *font = _smush_smalfontNut;
- int numFontChars = font->getNumChars();
- Common::Rect clipRect(0, 0, width, height);
-
- // Calculate text width
- int textWidth = 0;
- const char *p = pauseText;
- while (*p) {
- byte c = (byte)*p++;
- if (c >= 'a' && c <= 'z')
- c = c - 'a' + 'A';
- if (c < numFontChars) {
- textWidth += font->getCharWidth(c);
- }
- }
-
- // Draw centered
- int textX = (width - textWidth) / 2;
- int textY = height / 2 - 4;
-
- p = pauseText;
- while (*p) {
- byte c = (byte)*p++;
- if (c >= 'a' && c <= 'z')
- c = c - 'a' + 'A';
- if (c < numFontChars && textX >= 0 && textY >= 0) {
- font->drawCharV7(frameBuffer, clipRect, textX, textY, width, -1,
- kStyleAlignLeft, c, true, true);
- textX += font->getCharWidth(c);
- }
- }
+ if (multiFont) {
+ Common::Rect clipRect(0, 0, screenW, screenH);
+ // Original uses x=10, y=10 (single-buffer mode) or y=20 (double-buffer mode).
+ // We use single-buffer mode (y=10).
+ multiFont->drawString(pauseText, frameBuffer, clipRect, 10, 10, width, 4, kStyleAlignLeft);
}
- // Update the screen to show the pause overlay
- // SmushPlayer uses copyRectToScreen to transfer the buffer to the display backend
- _vm->_system->copyRectToScreen(frameBuffer, width, 0, 0, width, height);
+ // Step 4: Push to screen.
+ _vm->_system->copyRectToScreen(frameBuffer, width, 0, 0, screenW, screenH);
_vm->_system->updateScreen();
-
- debug("showPauseOverlay: Overlay displayed");
}
// runMainMenu -- Main menu loop (FUN_004147B2).
diff --git a/engines/scumm/smush/smush_multi_font.cpp b/engines/scumm/smush/smush_multi_font.cpp
index 7f6bbd603a7..e68871d6f33 100644
--- a/engines/scumm/smush/smush_multi_font.cpp
+++ b/engines/scumm/smush/smush_multi_font.cpp
@@ -53,6 +53,11 @@ void SmushMultiFont::drawString(const char *str, byte *buffer, Common::Rect &cli
_textRenderer->drawString(str, buffer, clipRect, x, y, _vm->_screenWidth, col, flags);
}
+void SmushMultiFont::drawString(const char *str, byte *buffer, Common::Rect &clipRect, int x, int y, int pitch, int16 col, TextStyleFlags flags) {
+ _currentFont = _defaultFont;
+ _textRenderer->drawString(str, buffer, clipRect, x, y, pitch, col, flags);
+}
+
void SmushMultiFont::drawStringWrap(const char *str, byte *buffer, Common::Rect &clipRect, int x, int y, int16 col, TextStyleFlags flags) {
// Reset to default font before drawing
_currentFont = _defaultFont;
diff --git a/engines/scumm/smush/smush_multi_font.h b/engines/scumm/smush/smush_multi_font.h
index 1dbb4c6e2e1..6c57acbe57f 100644
--- a/engines/scumm/smush/smush_multi_font.h
+++ b/engines/scumm/smush/smush_multi_font.h
@@ -52,6 +52,7 @@ public:
// String drawing methods
void drawString(const char *str, byte *buffer, Common::Rect &clipRect, int x, int y, int16 col, TextStyleFlags flags);
+ void drawString(const char *str, byte *buffer, Common::Rect &clipRect, int x, int y, int pitch, int16 col, TextStyleFlags flags);
void drawStringWrap(const char *str, byte *buffer, Common::Rect &clipRect, int x, int y, int16 col, TextStyleFlags flags);
// GlyphRenderer_v7 interface
diff --git a/engines/scumm/smush/smush_player.h b/engines/scumm/smush/smush_player.h
index 5b0eaefc674..1d63e11295e 100644
--- a/engines/scumm/smush/smush_player.h
+++ b/engines/scumm/smush/smush_player.h
@@ -266,6 +266,8 @@ public:
bool isAudioCallbackEnabled();
byte *getVideoPalette();
void setCurVideoFlags(int16 flags);
+ SmushMultiFont *getMultiFont() const { return _multiFont; }
+ void ensureMultiFont();
void setFastForwardToFrame(uint32 frame) { _fastForwardToFrame = frame; }
// Masked regions - areas where video should not update (e.g., destroyed enemies)
diff --git a/engines/scumm/smush/smush_player_ra2.cpp b/engines/scumm/smush/smush_player_ra2.cpp
index c93a67fedb3..07f4a0500dc 100644
--- a/engines/scumm/smush/smush_player_ra2.cpp
+++ b/engines/scumm/smush/smush_player_ra2.cpp
@@ -273,16 +273,19 @@ void SmushPlayer::handleLoad(int32 subSize, Common::SeekableReadStream &b) {
}
}
+void SmushPlayer::ensureMultiFont() {
+ if (!_multiFont) {
+ _multiFont = new SmushMultiFont(_vm, this, true);
+ }
+}
+
/**
* RA2-specific text rendering using SmushMultiFont for inline font switching.
*/
void SmushPlayer::ra2HandleTextResource(const char *str, int fontId, int color,
int pos_x, int pos_y, int left, int top,
int width, int height, TextStyleFlags flg) {
- // Create multi-font renderer on first use
- if (!_multiFont) {
- _multiFont = new SmushMultiFont(_vm, this, true);
- }
+ ensureMultiFont();
_multiFont->setDefaultFont(fontId);
debug("SmushPlayer::handleTextResource: RA2 TRES frame=%d fontId=%d color=%d flags=0x%x flg=%d pos=(%d,%d) clip=(%d,%d,%d,%d) str=\"%.40s\"",
Commit: 86c9b2d979e1897592938c6d0690a766aca373df
https://github.com/scummvm/scummvm/commit/86c9b2d979e1897592938c6d0690a766aca373df
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:29+02:00
Commit Message:
SCUMM: RA: Split Rebel Assault engine files
Changed paths:
A engines/scumm/insane/insane_rebel1_audio.cpp
A engines/scumm/insane/insane_rebel1_iact.cpp
A engines/scumm/insane/insane_rebel1_levels.cpp
A engines/scumm/insane/insane_rebel1_menu.cpp
A engines/scumm/insane/insane_rebel1_render.cpp
A engines/scumm/insane/insane_rebel1_runlevels.cpp
A engines/scumm/insane/insane_rebel2.cpp
A engines/scumm/insane/insane_rebel2.h
A engines/scumm/insane/insane_rebel2_audio.cpp
A engines/scumm/insane/insane_rebel2_iact.cpp
A engines/scumm/insane/insane_rebel2_levels.cpp
A engines/scumm/insane/insane_rebel2_menu.cpp
A engines/scumm/insane/insane_rebel2_render.cpp
A engines/scumm/insane/insane_rebel2_runlevels.cpp
R engines/scumm/insane/insane_rebel.cpp
R engines/scumm/insane/insane_rebel.h
R engines/scumm/insane/insane_rebel_audio.cpp
R engines/scumm/insane/insane_rebel_iact.cpp
R engines/scumm/insane/insane_rebel_levels.cpp
R engines/scumm/insane/insane_rebel_menu.cpp
R engines/scumm/insane/insane_rebel_render.cpp
R engines/scumm/insane/insane_rebel_runlevels.cpp
engines/scumm/insane/insane_rebel1.cpp
engines/scumm/insane/insane_rebel1.h
engines/scumm/module.mk
engines/scumm/scumm.cpp
engines/scumm/smush/smush_player.cpp
engines/scumm/smush/smush_player_ra2.cpp
diff --git a/engines/scumm/insane/insane_rebel1.cpp b/engines/scumm/insane/insane_rebel1.cpp
index 8911ad08a45..0a7f24ecc72 100644
--- a/engines/scumm/insane/insane_rebel1.cpp
+++ b/engines/scumm/insane/insane_rebel1.cpp
@@ -22,11 +22,13 @@
#include "common/system.h"
#include "common/events.h"
#include "common/endian.h"
+
#include "audio/audiostream.h"
-#include "audio/decoders/raw.h"
#include "audio/mixer.h"
+
#include "graphics/cursorman.h"
#include "graphics/wincursor.h"
+
#include "scumm/scumm_v7.h"
#include "scumm/scumm.h"
#include "scumm/smush/smush_player.h"
@@ -34,21 +36,6 @@
namespace Scumm {
-// RA1 coordinate constants (scaled from RA2's 424x260 â 384x242)
-// Original coordinate space: 320x200 game viewport at (0,0) in the 384x242 buffer.
-// Ship base position in the original: (0xA0, 100) = (160, 100) = center of 320x200.
-// Accumulator range: ±0x82 (~±130), so ship can reach ~(30..290, -30..230).
-static const int16 kCenterX = 160; // _DAT_74B6 init = 0xA0
-static const int16 kCenterY = 100; // _DAT_74B8 init = 100
-static const int16 kMinX = 20;
-static const int16 kMaxX = 300;
-static const int16 kMinY = 20;
-static const int16 kMaxY = 180;
-
-// Perspective focal lengths (from original tuning table)
-static const int16 kFocalX = 43; // 0x2b
-static const int16 kFocalY = 25; // 0x19
-
// Per-difficulty tuning tables from assault_data_3.bin
// Indexed: difficulty * 0x28B + level * 0x1F + offset
// Fields: roll, lift, slide, drift, snap, miss, wham, shot, kill
@@ -68,33 +55,6 @@ static const int16 kTuningTable[2][3][9] = {
};
static const int kNumTunedLevels = 2;
-// Decode BOMP RLE (codec 21) sprite data into a flat pixel buffer.
-// Same algorithm as NutRenderer::codec21 but without palette tracking.
-static void decodeBomp(byte *dst, const byte *src, int width, int height, int pitch) {
- while (height--) {
- byte *dstNext = dst + pitch;
- const byte *srcNext = src + 2 + READ_LE_UINT16(src);
- src += 2;
- int len = width;
- byte *d = dst;
- do {
- int offs = READ_LE_UINT16(src); src += 2;
- d += offs;
- len -= offs;
- if (len <= 0)
- break;
- int w = READ_LE_UINT16(src) + 1; src += 2;
- len -= w;
- if (len < 0)
- w += len;
- memcpy(d, src, w);
- src += w;
- d += w;
- } while (len > 0);
- dst = dstNext;
- src = srcNext;
- }
-}
void InsaneRebel1::loadTuningForLevel(int level) {
int d = CLIP(_difficulty, 0, 2);
@@ -117,14 +77,14 @@ InsaneRebel1::InsaneRebel1(ScummEngine_v7 *scumm) : Insane(), _vm(scumm) {
_screenWidth = 384;
_screenHeight = 242;
- _shipPosX = kCenterX;
- _shipPosY = kCenterY;
+ _shipPosX = kRA1CenterX;
+ _shipPosY = kRA1CenterY;
_shipDirIndex = 17; // Center of 5x7 grid (2*7 + 3)
- _corridorLeftX = kMinX;
- _corridorTopY = kMinY;
- _corridorRightX = kMaxX;
- _corridorBottomY = kMaxY;
+ _corridorLeftX = kRA1MinX;
+ _corridorTopY = kRA1MinY;
+ _corridorRightX = kRA1MaxX;
+ _corridorBottomY = kRA1MaxY;
_rollAccum = 0;
_liftSmooth = 0;
@@ -205,1699 +165,4 @@ InsaneRebel1::~InsaneRebel1() {
terminateAudio();
}
-bool InsaneRebel1::notifyEvent(const Common::Event &event) {
- if (_menuActive && _optionsActive && event.type == Common::EVENT_KEYDOWN) {
- switch (event.kbd.keycode) {
- case Common::KEYCODE_UP:
- case Common::KEYCODE_w:
- _optionsSel = (_optionsSel + 2) % 3;
- return true;
- case Common::KEYCODE_DOWN:
- case Common::KEYCODE_s:
- _optionsSel = (_optionsSel + 1) % 3;
- return true;
- case Common::KEYCODE_RETURN:
- case Common::KEYCODE_KP_ENTER:
- case Common::KEYCODE_SPACE:
- _menuConfirmed = true;
- _vm->_smushVideoShouldFinish = true;
- return true;
- case Common::KEYCODE_ESCAPE:
- _optionsSel = 2; // Back
- _menuConfirmed = true;
- _vm->_smushVideoShouldFinish = true;
- return true;
- default:
- break;
- }
- }
-
- if (_menuActive && !_optionsActive && event.type == Common::EVENT_KEYDOWN) {
- switch (event.kbd.keycode) {
- case Common::KEYCODE_UP:
- case Common::KEYCODE_w:
- _menuSelection = (_menuSelection + 4) % 5;
- return true;
- case Common::KEYCODE_DOWN:
- case Common::KEYCODE_s:
- _menuSelection = (_menuSelection + 1) % 5;
- return true;
- case Common::KEYCODE_RETURN:
- case Common::KEYCODE_KP_ENTER:
- case Common::KEYCODE_SPACE:
- _menuConfirmed = true;
- _vm->_smushVideoShouldFinish = true;
- return true;
- case Common::KEYCODE_1:
- case Common::KEYCODE_2:
- case Common::KEYCODE_3:
- case Common::KEYCODE_4:
- case Common::KEYCODE_5:
- _menuSelection = event.kbd.keycode - Common::KEYCODE_1;
- _menuConfirmed = true;
- _vm->_smushVideoShouldFinish = true;
- return true;
- case Common::KEYCODE_ESCAPE:
- _menuSelection = 4;
- _menuConfirmed = true;
- _vm->_smushVideoShouldFinish = true;
- return true;
- default:
- break;
- }
- }
-
- if (event.type == Common::EVENT_KEYDOWN && event.kbd.keycode == Common::KEYCODE_ESCAPE) {
- if (_player) {
- debug("Rebel1: ESC pressed - skipping video");
- _vm->_smushVideoShouldFinish = true;
- return true;
- }
- }
-
- return false;
-}
-
-// ---------------------------------------------------------------------------
-// Audio
-// ---------------------------------------------------------------------------
-
-void InsaneRebel1::initAudio(int sampleRate) {
- _audioSampleRate = sampleRate;
- for (int i = 0; i < kMaxAudioTracks; i++) {
- _audioStreams[i] = nullptr;
- _audioTrackActive[i] = false;
- }
-}
-
-void InsaneRebel1::terminateAudio() {
- for (int i = 0; i < kMaxAudioTracks; i++) {
- if (_audioTrackActive[i]) {
- _vm->_mixer->stopHandle(_audioHandles[i]);
- _audioTrackActive[i] = false;
- }
- if (_audioStreams[i]) {
- _audioStreams[i]->finish();
- _audioStreams[i] = nullptr;
- }
- }
-}
-
-void InsaneRebel1::queueAudioData(int trackIdx, uint8 *data, int32 size, int volume, int pan) {
- if (trackIdx < 0 || trackIdx >= kMaxAudioTracks || size <= 0 || !data)
- return;
-
- if (!_audioStreams[trackIdx]) {
- debug(1, "InsaneRebel1: Creating audio stream for track %d at %d Hz", trackIdx, _audioSampleRate);
- _audioStreams[trackIdx] = Audio::makeQueuingAudioStream(_audioSampleRate, false);
- _audioTrackActive[trackIdx] = true;
- _vm->_mixer->playStream(Audio::Mixer::kSFXSoundType, &_audioHandles[trackIdx],
- _audioStreams[trackIdx], -1, Audio::Mixer::kMaxChannelVolume, 0,
- DisposeAfterUse::NO);
- }
-
- byte *audioCopy = (byte *)malloc(size);
- if (!audioCopy)
- return;
- memcpy(audioCopy, data, size);
-
- _audioStreams[trackIdx]->queueBuffer(audioCopy, size, DisposeAfterUse::YES, Audio::FLAG_UNSIGNED);
-
- int scaledVolume = (volume * Audio::Mixer::kMaxChannelVolume) / 127;
- int scaledPan = (pan * 127) / 128;
- _vm->_mixer->setChannelVolume(_audioHandles[trackIdx], scaledVolume);
- _vm->_mixer->setChannelBalance(_audioHandles[trackIdx], scaledPan);
-}
-
-void InsaneRebel1::processAudioFrame(int16 feedSize) {
- if (!_player)
- return;
-
- SmushPlayer *sp = _player;
-
- if (sp->_smushTracksNeedInit) {
- sp->_smushTracksNeedInit = false;
- for (int i = 0; i < SMUSH_MAX_TRACKS; i++) {
- sp->_smushDispatch[i].fadeRemaining = 0;
- sp->_smushDispatch[i].fadeVolume = 0;
- sp->_smushDispatch[i].fadeSampleRate = 0;
- sp->_smushDispatch[i].elapsedAudio = 0;
- sp->_smushDispatch[i].audioLength = 0;
- }
- }
-
- for (int i = 0; i < sp->_smushNumTracks; i++) {
- SmushPlayer::SmushAudioTrack &track = sp->_smushTracks[i];
- SmushPlayer::SmushAudioDispatch &dispatch = sp->_smushDispatch[i];
-
- if (track.state == TRK_STATE_INACTIVE || !track.blockPtr)
- continue;
-
- bool isPlayableTrack = ((track.flags & TRK_TYPE_MASK) == IS_SPEECH && sp->isChanActive(CHN_SPEECH)) ||
- ((track.flags & TRK_TYPE_MASK) == IS_BKG_MUSIC && sp->isChanActive(CHN_BKGMUS)) ||
- ((track.flags & TRK_TYPE_MASK) == IS_SFX && sp->isChanActive(CHN_OTHER));
-
- if (!isPlayableTrack)
- continue;
-
- int baseVolume;
- switch (track.flags & TRK_TYPE_MASK) {
- case IS_SFX:
- baseVolume = (sp->_smushTrackVols[1] * track.volume) >> 7;
- break;
- case IS_BKG_MUSIC:
- baseVolume = (sp->_smushTrackVols[3] * track.volume) >> 7;
- break;
- case IS_SPEECH:
- baseVolume = (sp->_smushTrackVols[2] * track.volume) >> 7;
- break;
- default:
- baseVolume = track.volume;
- break;
- }
- int mixVolume = baseVolume * sp->_smushTrackVols[0] / 127;
-
- // Handle FADING -> PLAYING transition
- if (track.state == TRK_STATE_FADING) {
- dispatch.headerPtr = track.dataBuf;
- dispatch.dataBuf = track.subChunkPtr;
- dispatch.dataSize = track.dataSize;
- dispatch.currentOffset = 0;
- dispatch.audioLength = 0;
- track.state = TRK_STATE_PLAYING;
- }
-
- if (track.state != TRK_STATE_INACTIVE) {
- int32 tmpFeedSize = feedSize;
-
- while (tmpFeedSize > 0) {
- int32 mixInFrameCount = dispatch.currentOffset;
-
- if (mixInFrameCount > 0 && dispatch.dataBuf && dispatch.dataSize > 0) {
- if (dispatch.audioRemaining < 0)
- dispatch.audioRemaining = 0;
-
- int32 offset = dispatch.audioRemaining % dispatch.dataSize;
-
- if (dispatch.sampleRate > 0 && sp->_smushAudioSampleRate > 0) {
- int32 maxFrames = dispatch.sampleRate * tmpFeedSize / sp->_smushAudioSampleRate;
- if (mixInFrameCount > maxFrames)
- mixInFrameCount = maxFrames;
- }
-
- if (offset + mixInFrameCount > dispatch.dataSize)
- mixInFrameCount = dispatch.dataSize - offset;
-
- if (dispatch.audioRemaining + mixInFrameCount > track.availableSize) {
- mixInFrameCount = track.availableSize - dispatch.audioRemaining;
- if (mixInFrameCount <= 0) {
- track.state = TRK_STATE_ENDING;
- break;
- }
- }
-
- if (mixInFrameCount > 0) {
- if (!dispatch.dataBuf || offset < 0 || offset + mixInFrameCount > dispatch.dataSize)
- break;
-
- queueAudioData(i, &dispatch.dataBuf[offset], mixInFrameCount, mixVolume, track.pan);
-
- dispatch.currentOffset -= mixInFrameCount;
- dispatch.audioRemaining += mixInFrameCount;
-
- if (dispatch.sampleRate > 0) {
- int32 consumedFeed = mixInFrameCount * sp->_smushAudioSampleRate / dispatch.sampleRate;
- tmpFeedSize -= consumedFeed;
- } else {
- tmpFeedSize -= mixInFrameCount;
- }
- }
- }
-
- if (dispatch.currentOffset <= 0) {
- if (!sp->processAudioCodes(i, tmpFeedSize, mixVolume))
- break;
- if (dispatch.currentOffset <= 0)
- break;
- } else if (tmpFeedSize <= 0) {
- break;
- }
- }
- }
-
- track.audioRemaining = dispatch.audioRemaining;
- dispatch.state = track.state;
- }
-}
-
-// Load an RA1 NUT sprite file (ANIM v1).
-// RA1 NUTs can have odd-size FOBJ chunks padded to 2-byte alignment within
-// FRME containers. This loader handles that padding properly, unlike the
-// shared NutRenderer::loadFont which assumes even-size chunks.
-bool InsaneRebel1::loadRA1Nut(const char *filename, RA1SpriteBank &bank) {
- ScummFile *file = _vm->instantiateScummFile();
- _vm->openFile(*file, filename);
- if (!file->isOpen()) {
- warning("InsaneRebel1::loadRA1Nut: can't open %s", filename);
- delete file;
- return false;
- }
-
- uint32 tag = file->readUint32BE();
- if (tag != MKTAG('A','N','I','M')) {
- warning("InsaneRebel1::loadRA1Nut: no ANIM tag in %s", filename);
- delete file;
- return false;
- }
- uint32 animSize = file->readUint32BE();
- byte *data = (byte *)malloc(animSize);
- file->read(data, animSize);
- file->close();
- delete file;
-
- // data[0..3] = AHDR tag, data[4..7] = AHDR size
- if (READ_BE_UINT32(data) != MKTAG('A','H','D','R')) {
- warning("InsaneRebel1::loadRA1Nut: no AHDR in %s", filename);
- free(data);
- return false;
- }
-
- const uint16 expectedSprites = READ_LE_UINT16(data + 10);
- bank.numSprites = expectedSprites;
- bank.sprites = new RA1Sprite[bank.numSprites];
- memset(bank.sprites, 0, sizeof(RA1Sprite) * bank.numSprites);
-
- uint32 *fobjOffsets = (uint32 *)calloc(expectedSprites, sizeof(uint32));
- if (!fobjOffsets) {
- free(data);
- return false;
- }
-
- // Pass 1: Parse ANIM chunks properly and collect FRME->FOBJ offsets in-order.
- uint32 decodedSize = 0;
- uint16 foundSprites = 0;
- uint32 chunkOffset = 0;
- while (chunkOffset + 8 <= animSize && foundSprites < expectedSprites) {
- uint32 chunkTag = READ_BE_UINT32(data + chunkOffset);
- uint32 chunkSize = READ_BE_UINT32(data + chunkOffset + 4);
- uint32 chunkDataOffset = chunkOffset + 8;
- uint32 chunkEnd = chunkDataOffset + chunkSize;
- if (chunkEnd > animSize)
- break;
-
- if (chunkTag == MKTAG('F','R','M','E')) {
- bool foundFobj = false;
- uint32 subOffset = chunkDataOffset;
- while (subOffset + 8 <= chunkEnd) {
- uint32 subTag = READ_BE_UINT32(data + subOffset);
- uint32 subSize = READ_BE_UINT32(data + subOffset + 4);
- uint32 subDataOffset = subOffset + 8;
- uint32 subEnd = subDataOffset + subSize;
- if (subEnd > chunkEnd)
- break;
-
- if (subTag == MKTAG('F','O','B','J') && subOffset + 22 <= animSize) {
- uint16 w = READ_LE_UINT16(data + subOffset + 14);
- uint16 h = READ_LE_UINT16(data + subOffset + 16);
- decodedSize += (uint32)w * (uint32)h;
- fobjOffsets[foundSprites] = subOffset;
- foundFobj = true;
- break;
- }
-
- subOffset = subEnd;
- if (subSize & 1)
- subOffset++;
- }
- // Always increment for every FRME to preserve char-to-glyph alignment.
- // Empty FRMEs (no FOBJ) keep fobjOffsets[i] = 0, decoded as blank sprites.
- foundSprites++;
- }
-
- chunkOffset = chunkEnd;
- if (chunkSize & 1)
- chunkOffset++;
- }
-
- bank.decodedData = (byte *)calloc(decodedSize ? decodedSize : 1, 1);
- bank.decodedSize = decodedSize;
- byte *decPtr = bank.decodedData;
-
- // Pass 2: Decode collected FOBJ entries.
- for (uint16 i = 0; i < foundSprites; i++) {
- uint32 fobjOffset = fobjOffsets[i];
- if (fobjOffset == 0) {
- // Empty FRME (no FOBJ) â leave sprite as blank (zeroed by memset).
- continue;
- }
-
- int codec = READ_LE_UINT16(data + fobjOffset + 8);
- bank.sprites[i].xoffs = READ_LE_INT16(data + fobjOffset + 10);
- bank.sprites[i].yoffs = READ_LE_INT16(data + fobjOffset + 12);
- bank.sprites[i].width = READ_LE_UINT16(data + fobjOffset + 14);
- bank.sprites[i].height = READ_LE_UINT16(data + fobjOffset + 16);
-
- int pixelCount = bank.sprites[i].width * bank.sprites[i].height;
- const byte *fobjData = data + fobjOffset + 22;
-
- if (codec == 21) {
- bank.sprites[i].data = decPtr;
- decodeBomp(decPtr, fobjData, bank.sprites[i].width,
- bank.sprites[i].height, bank.sprites[i].width);
- } else {
- bank.sprites[i].width = 0;
- bank.sprites[i].height = 0;
- bank.sprites[i].data = nullptr;
- warning("InsaneRebel1::loadRA1Nut: unsupported codec %d in sprite %d", codec, i);
- }
-
- decPtr += pixelCount;
- }
-
- free(fobjOffsets);
-
- free(data);
- debug(1, "InsaneRebel1::loadRA1Nut('%s'): expected=%d found=%d decoded=%d bytes",
- filename, expectedSprites, foundSprites, decodedSize);
- return true;
-}
-
-void InsaneRebel1::loadLevelSprites(int level) {
- // Ship direction bank â not all levels have one (e.g. Level 2 is first-person)
- Common::String bankFile = Common::String::format("LVL%d/L%dBANK1.NUT", level, level);
- if (!loadRA1Nut(bankFile.c_str(), _shipBank)) {
- debug(1, "InsaneRebel1: No BANK1 for level %d (first-person level)", level);
- }
- loadRA1Nut("SYS/DISPLAY.NUT", _displayBank);
-
- // Explosion sprites â try BANG first, then EXPLD
- Common::String bangFile = Common::String::format("LVL%d/L%dBANG.NUT", level, level);
- if (!loadRA1Nut(bangFile.c_str(), _bangBank)) {
- Common::String expldFile = Common::String::format("LVL%d/L%dEXPLD.NUT", level, level);
- loadRA1Nut(expldFile.c_str(), _bangBank);
- }
-}
-
-void InsaneRebel1::procPreRendering(byte *renderBitmap) {
-}
-
-void InsaneRebel1::procPostRendering(byte *renderBitmap, int32 codecparam, int32 setupsan12,
- int32 setupsan13, int32 curFrame, int32 maxFrame) {
- if (_menuActive && renderBitmap) {
- int width = _player ? _player->_width : 0;
- int height = _player ? _player->_height : 0;
- if (width == 0) width = _screenWidth;
- if (height == 0) height = _screenHeight;
- int pitch = width;
- renderMainMenuOverlay(renderBitmap, pitch, width, height);
- }
-
- if (!_interactiveVideoActive || !renderBitmap)
- return;
-
- int width = _player->_width;
- int height = _player->_height;
- if (width == 0) width = _screenWidth;
- if (height == 0) height = _screenHeight;
- int pitch = width;
-
- if (_currentLevel == 1) {
- // Level 2: first-person asteroid dodge â no ship sprite, input averaging physics
- updateAsteroidPhysics();
- } else {
- // Level 1 (and others): third-person ship flight with accumulators
- if (_shipBank.numSprites > 0) {
- updateShipPhysics();
- renderShip(renderBitmap, pitch, width, height);
- }
- }
- renderExplosions(renderBitmap, pitch, width, height);
- renderCrosshair(renderBitmap, pitch, width, height);
- renderHUD(renderBitmap, pitch, width, height);
-}
-
-// Draw crosshair/pointer at mouse position into the render buffer.
-// The original DOS game uses the INT 33h hardware mouse cursor (a red crosshair).
-// We replicate it by drawing a red cross into the render buffer each frame.
-void InsaneRebel1::renderCrosshair(byte *dst, int pitch, int width, int height) {
- int cx = _vm->_mouse.x;
- int cy = _vm->_mouse.y;
-
- // Palette index 119 = (255,0,0) pure red in L2PLAY.ANM palette.
- // Palette index 15 = (255,255,255) white, used as outline for visibility.
- const byte colorInner = 119;
- const byte colorOutline = 15;
- const int size = 7; // arm length
-
- // Helper lambda to draw a pixel with outline
- auto drawPx = [&](int x, int y, byte c) {
- if (x >= 0 && x < width && y >= 0 && y < height)
- dst[y * pitch + x] = c;
- };
-
- // Draw outline first (1px border around each arm pixel)
- for (int d = -size; d <= size; d++) {
- if (d >= -1 && d <= 1) continue; // skip center area for outline
- // Horizontal arm outline
- drawPx(cx + d, cy - 1, colorOutline);
- drawPx(cx + d, cy + 1, colorOutline);
- // Vertical arm outline
- drawPx(cx - 1, cy + d, colorOutline);
- drawPx(cx + 1, cy + d, colorOutline);
- }
- // Arm endpoints
- drawPx(cx - size - 1, cy, colorOutline);
- drawPx(cx + size + 1, cy, colorOutline);
- drawPx(cx, cy - size - 1, colorOutline);
- drawPx(cx, cy + size + 1, colorOutline);
-
- // Draw red cross arms
- for (int d = -size; d <= size; d++) {
- if (d == 0) continue; // gap at center
- drawPx(cx + d, cy, colorInner); // horizontal
- drawPx(cx, cy + d, colorInner); // vertical
- }
- // Center dot
- drawPx(cx, cy, colorInner);
-}
-
-void InsaneRebel1::drawFontBankString(byte *dst, int pitch, int width, int height, int x, int y, const char *text) {
- if (!dst || !text || _hudFontBank.numSprites <= 0)
- return;
-
- for (int i = 0; text[i] != '\0'; i++) {
- const byte ch = (byte)text[i];
-
- if (ch == ' ') {
- x += 6;
- continue;
- }
-
- // RA1 font renderer indexes printable characters from '!' (0x21), not raw ASCII.
- if (ch < 0x21) {
- x += 4;
- continue;
- }
- const int fontIdx = (int)ch - 0x21;
- if (fontIdx < 0 || fontIdx >= _hudFontBank.numSprites) {
- x += 4;
- continue;
- }
-
- const RA1Sprite &glyph = _hudFontBank.sprites[fontIdx];
- const int gw = glyph.width;
- const int gh = glyph.height;
- const int gx = x + glyph.xoffs;
- const int gy = y + glyph.yoffs;
- const uint64 glyphPixels = (uint64)gw * (uint64)gh;
- if (!glyph.data || gw <= 0 || gh <= 0 || glyphPixels == 0 || glyphPixels > 0x10000) {
- x += 4;
- continue;
- }
- if (!(_hudFontBank.decodedData && _hudFontBank.decodedSize > 0)) {
- x += 4;
- continue;
- }
- const byte *bankStart = _hudFontBank.decodedData;
- const byte *bankEnd = _hudFontBank.decodedData + _hudFontBank.decodedSize;
- if (glyph.data < bankStart || glyph.data >= bankEnd || glyph.data + glyphPixels > bankEnd) {
- x += 4;
- continue;
- }
-
- for (int py = 0; py < gh; py++) {
- const int sy = gy + py;
- if (sy < 0 || sy >= height)
- continue;
- for (int px = 0; px < gw; px++) {
- const int sx = gx + px;
- if (sx < 0 || sx >= width)
- continue;
- const byte pixel = glyph.data[py * gw + px];
- if (pixel != 0)
- dst[sy * pitch + sx] = pixel;
- }
- }
-
- x += gw > 0 ? gw : 4;
- }
-}
-
-// getFontBankStringWidth -- Measure pixel width of a string using the HUD font bank.
-// Matches the pre-pass width calculation in the original drawString (FUN_221B7).
-int InsaneRebel1::getFontBankStringWidth(const char *text) {
- if (!text || _hudFontBank.numSprites <= 0)
- return 0;
-
- int w = 0;
- for (int i = 0; text[i] != '\0'; i++) {
- const byte ch = (byte)text[i];
- if (ch == ' ') {
- w += 6;
- continue;
- }
- if (ch < 0x21) {
- w += 4;
- continue;
- }
- const int fontIdx = (int)ch - 0x21;
- if (fontIdx < 0 || fontIdx >= _hudFontBank.numSprites) {
- w += 4;
- continue;
- }
- const RA1Sprite &glyph = _hudFontBank.sprites[fontIdx];
- w += glyph.width > 0 ? glyph.width : 4;
- }
- return w;
-}
-
-// renderMainMenuOverlay -- Draw menu text and selection highlight box.
-// Original menu strings from assault_data_3.bin at 0x5822.
-// Highlight uses RA2-style flashing border box (FUN_004292d0 pattern).
-void InsaneRebel1::renderMainMenuOverlay(byte *dst, int pitch, int width, int height) {
- _menuFrameCounter++;
-
- if (_optionsActive) {
- // --- Options submenu ---
- static const char *kDiffNames[3] = { "EASY", "NORMAL", "HARD" };
-
- const int titleW = getFontBankStringWidth("GAME OPTIONS");
- drawFontBankString(dst, pitch, width, height, (width - titleW) / 2, 36, "GAME OPTIONS");
-
- // Build dynamic option strings
- char diffLine[64];
- snprintf(diffLine, sizeof(diffLine), "DIFFICULTY: %s", kDiffNames[CLIP(_difficulty, 0, 2)]);
- const char *turbLine = _turbulenceEnabled ? "TURBULENCE: ON" : "TURBULENCE: OFF";
- const char *kOptionsItems[3] = { diffLine, turbLine, "BACK" };
-
- const int menuY = 60;
- const int rowH = 16;
-
- for (int i = 0; i < 3; i++) {
- const int textW = getFontBankStringWidth(kOptionsItems[i]);
- const int textX = (width - textW) / 2;
- const int y = menuY + i * rowH;
- drawFontBankString(dst, pitch, width, height, textX, y + 1, kOptionsItems[i]);
-
- if (i == _optionsSel) {
- byte highlightColor = ((_menuFrameCounter / 8) & 1) ? 248 : 240;
- int bracketWidth = textW + 12;
- int leftX = CLIP(textX - 6, 0, width - 1);
- int rightX = CLIP(leftX + bracketWidth, 0, width - 1);
- int topY = CLIP(y - 1, 0, height - 1);
- int bottomY = CLIP(y + rowH - 2, 0, height - 1);
- for (int x = leftX; x <= rightX; x++) {
- dst[topY * pitch + x] = highlightColor;
- dst[bottomY * pitch + x] = highlightColor;
- }
- for (int py = topY; py <= bottomY; py++) {
- dst[py * pitch + leftX] = highlightColor;
- dst[py * pitch + rightX] = highlightColor;
- }
- }
- }
- return;
- }
-
- // --- Main menu ---
- static const char *kMenuItems[5] = {
- "START NEW GAME",
- "GAME OPTIONS",
- "ENTER PASSCODE",
- "CONTINUE DEMO",
- "EXIT TO DOS"
- };
-
- // Center title
- const int titleW = getFontBankStringWidth("MAIN MENU");
- const int titleX = (width - titleW) / 2;
- drawFontBankString(dst, pitch, width, height, titleX, 36, "MAIN MENU");
-
- // Draw menu items centered horizontally
- const int menuY = 60;
- const int rowH = 16;
-
- for (int i = 0; i < 5; i++) {
- const int textW = getFontBankStringWidth(kMenuItems[i]);
- const int textX = (width - textW) / 2;
- const int y = menuY + i * rowH;
-
- drawFontBankString(dst, pitch, width, height, textX, y + 1, kMenuItems[i]);
-
- // Selection highlight box â flashing border (FUN_004292d0 pattern from RA2)
- if (i == _menuSelection) {
- // Flash between two palette colors every 8 frames
- byte highlightColor = ((_menuFrameCounter / 8) & 1) ? 248 : 240;
-
- int bracketWidth = textW + 12;
- int bracketHeight = rowH;
- int leftX = textX - 6;
- int rightX = leftX + bracketWidth;
- int topY = y - 1;
- int bottomY = y + bracketHeight - 2;
-
- // Clamp
- if (leftX < 0) leftX = 0;
- if (rightX >= width) rightX = width - 1;
- if (topY < 0) topY = 0;
- if (bottomY >= height) bottomY = height - 1;
-
- // Draw rectangle border (4 lines)
- for (int x = leftX; x <= rightX && x < width; x++) {
- if (topY >= 0 && topY < height)
- dst[topY * pitch + x] = highlightColor;
- if (bottomY >= 0 && bottomY < height)
- dst[bottomY * pitch + x] = highlightColor;
- }
- for (int py = topY; py <= bottomY && py < height; py++) {
- if (leftX >= 0 && leftX < width)
- dst[py * pitch + leftX] = highlightColor;
- if (rightX >= 0 && rightX < width)
- dst[py * pitch + rightX] = highlightColor;
- }
- }
- }
-}
-
-// Ship physics matching FUN_1DEB5 (accumulator-based position system).
-// Roll accumulator (_74CA) driven by input, position accumulators (_74C2/_74C6)
-// driven by roll + drift + cross-coupling. Ship position = base + accum >> 8.
-void InsaneRebel1::updateShipPhysics() {
- _frameCounter++;
-
- // Decrement cooldown
- if (_damageCooldown > 0)
- _damageCooldown--;
-
- // --- Step 1: Mouse input as offset from screen center ---
- // Original: _DAT_756C (horizontal), _DAT_756E (vertical)
- int16 inputX = (int16)(_vm->_mouse.x - 160);
- int16 inputY = (int16)(_vm->_mouse.y - 100);
- inputX = CLIP<int16>(inputX, -127, 127);
- inputY = CLIP<int16>(inputY, -127, 127);
-
- // --- Step 2: Roll accumulator (_74CA) ---
- // Normal mode: accumulate; mode 0x10: snap to input
- _rollAccum += (_tuning.roll * (int32)inputX) >> 5;
- _rollAccum = CLIP<int32>(_rollAccum, -0x47F, 0x47F);
-
- // --- Step 3: Vertical smoothing (_74CE) ---
- // Exponential decay toward -inputY
- _liftSmooth += (-_liftSmooth - (int32)inputY) >> 1;
- _liftSmooth = CLIP<int32>(_liftSmooth, -0x20, 0x20);
-
- // --- Step 4: Position accumulator deltas ---
- // X delta: drift + slide coupling - cross-coupling
- int32 rng = _turbulenceEnabled ? (int32)_vm->_rnd.getRandomNumber(199) : 100; // 0-199, centered at 100
- int32 crossTermX;
- if (_liftSmooth < 0)
- crossTermX = ((int32)_tuning.lift * _liftSmooth * _rollAccum) >> 11;
- else
- crossTermX = ((int32)_tuning.lift * _liftSmooth * _rollAccum) >> 12;
-
- int32 deltaX = (((rng - 100) - (int32)_tuning.drift * _driftParam) >> 1)
- + (((int32)_tuning.slide * _rollAccum) >> 7)
- - crossTermX;
-
- // Y delta: roll magnitude + lift cross-coupling
- int32 absRoll = ABS(_rollAccum);
- int32 crossTermY;
- if (_liftSmooth < 0)
- crossTermY = ((int32)_tuning.lift * (0x7DE - absRoll) * _liftSmooth) >> 12;
- else
- crossTermY = ((int32)_tuning.lift * (0x7DE - absRoll) * _liftSmooth) >> 13;
-
- int32 deltaY = (absRoll >> 1) + crossTermY;
-
- // --- Step 5: Update position accumulators ---
- _posAccumX += deltaX;
- _posAccumX = CLIP<int32>(_posAccumX, -0x8200, 0x8200);
- _posAccumY += deltaY;
- _posAccumY = CLIP<int32>(_posAccumY, -0x3200, 0x4600);
-
- // --- Step 6: Derive pixel position from accumulators ---
- // Original: _74BA = _74C2 >> 8, _74BC = _74C6 >> 8
- // Ship position = base + offset
- _shipPosX = kCenterX + (int16)(_posAccumX >> 8);
- _shipPosY = kCenterY + (int16)(_posAccumY >> 8);
-
- // Clamp to screen bounds
- _shipPosX = CLIP<int16>(_shipPosX, kMinX, kMaxX);
- _shipPosY = CLIP<int16>(_shipPosY, kMinY, kMaxY);
-
- // --- Step 7: Corridor collision (FUN_1C54D) ---
- // Wall contact forces position accumulators to corridor edge and sets
- // damage flags. Flag bit 0x10 (zone hit) suppresses damage bits only.
- {
- bool hasZoneHit = (_damageFlags & 0x10) != 0;
-
- if (_shipPosX > _corridorRightX) {
- _posAccumX = (_corridorRightX - kCenterX) << 8;
- _shipPosX = _corridorRightX;
- if (!hasZoneHit) {
- if (_rollAccum > -0x100)
- _rollAccum = -0x100; // Push left
- _damageFlags |= 0x02; // Right wall
- }
- }
- if (_shipPosX < _corridorLeftX) {
- _posAccumX = (_corridorLeftX - kCenterX) << 8;
- _shipPosX = _corridorLeftX;
- if (!hasZoneHit) {
- if (_rollAccum < 0x100)
- _rollAccum = 0x100; // Push right
- _damageFlags |= 0x04; // Left wall
- }
- }
- if (_shipPosY < _corridorTopY) {
- _posAccumY = ((_corridorTopY - kCenterY) << 8) + 0x100;
- _shipPosY = _corridorTopY;
- if (!hasZoneHit)
- _damageFlags |= 0x01;
- }
- if (_shipPosY > _corridorBottomY) {
- _posAccumY = ((_corridorBottomY - kCenterY) << 8) - 0x100;
- _shipPosY = _corridorBottomY;
- if (!hasZoneHit)
- _damageFlags |= 0x08;
- }
- }
-
- // --- Step 8: Perspective offsets ---
- {
- int absOffX = ABS(_shipPosX - kCenterX);
- if (absOffX > 0)
- _perspectiveX = (int16)((kFocalX * kCenterX * absOffX) /
- ((kCenterX - kFocalX) * absOffX + kFocalX * kCenterX));
- else
- _perspectiveX = 0;
- if (_shipPosX < kCenterX + 1)
- _perspectiveX = -_perspectiveX;
-
- int absOffY = ABS(_shipPosY - kCenterY);
- if (absOffY > 0)
- _perspectiveY = (int16)((kFocalY * kCenterY * absOffY) /
- ((kCenterY - kFocalY) * absOffY + kFocalY * kCenterY));
- else
- _perspectiveY = 0;
- if (_shipPosY < kCenterY + 1)
- _perspectiveY = -_perspectiveY;
- }
-
- // --- Step 9: Direction sprite index (FUN_1DEB5 LAB_1e23e) ---
- // Horizontal component from _74CA (rollAccum):
- // |rollAccum| <= 0x80: center (0)
- // rollAccum > 0x80: ((rollAccum - 0x80) >> 8) * 5 + 5 (right: 5,10,15,20)
- // rollAccum < -0x80: ((abs(rollAccum) - 0x80) >> 8) * 5 + 25 (left: 25,30,35,40)
- int hComponent;
- if (_rollAccum > 0x80) {
- hComponent = ((_rollAccum - 0x80) >> 8) * 5 + 5;
- } else if (_rollAccum < -0x80) {
- hComponent = ((-_rollAccum - 0x80) >> 8) * 5 + 25;
- } else {
- hComponent = 0;
- }
-
- // Vertical component from _74CE (liftSmooth):
- // (_74CE + 0x20) * 5 / 0x41 â 0..4 (5 rows)
- int vComponent = (_liftSmooth + 0x20) * 5 / 0x41;
-
- _shipDirIndex = CLIP<int16>((int16)(vComponent + hComponent), 0, _shipBank.numSprites - 1);
-
- // --- Step 10: Damage/event bit synthesis + damage processing ---
- // RA1 FUN_1B297-style latches from GAME opcodes:
- // 0x5D latch 0xFFFF -> bit 0x40 (obstacle/contact)
- // 0x5F non-zero + RNG -> bit 0x80 (projectile-like hit)
- if (_gameLatch5D == 0xFFFF)
- _damageFlags |= 0x40;
- if (_gameLatch5F != 0 && _vm->_rnd.getRandomNumber((uint16)(_gameLatch5F - 1)) == 0)
- _damageFlags |= 0x80;
-
- // Damage guard/mask from FUN_1DEB5: (_damageFlags & 0x96) != 0
- // damageFlags & 0x96 = bits 1,2,4,7 = wall collisions (0x16) + projectile hit (0x80)
- if ((_damageFlags & 0x96) != 0 && _damageCooldown == 0 &&
- _health >= 0 && _deathTimer <= 0) {
- // Projectile hit (bit 7 = 0x80)
- if (_damageFlags & 0x80)
- _health -= _tuning.shot;
- // Wall collision (bits 1,2,4 = 0x16)
- if (_damageFlags & 0x16)
- _health -= _tuning.wham;
-
- if (_health < 0)
- _deathTimer = kDeathTimerInit;
-
- _prevDamageFlags = _damageFlags;
- _damageCooldown = kDamageCooldownInit;
- _screenFlash = 3;
- }
-
- // Latches are per-frame event inputs in the original pipeline.
- _gameLatch5D = 0;
- _gameLatch5F = 0;
-
- // Death animation countdown
- if (_health < 0 && _deathTimer > 0)
- _deathTimer--;
-
- // Health regeneration: +1 every 32 frames (from original asm)
- if ((_frameCounter & 0x1F) == 0) {
- if (_health >= 0 && _health < kMaxHealth)
- _health++;
- if (_health >= 0)
- _score += 1;
- }
-
- // Screen flash decay
- if (_screenFlash > 0)
- _screenFlash--;
-
- // Clear per-frame damage flags
- _damageFlags = 0;
-
- // --- Path branching detection ---
- // Original (FUN_1B297): at GAME counter 394 (0x18A), sets nextSceneA=0x67/nextSceneB=0x69.
- // After this point, drift goes strongly negative (pushing ship left for the hard path).
- // If ship is right of center, player chose the right/easy path â switch to L1PLAY1R.
- // The check fires once when the game counter first reaches the branch point.
- if (_pathBranchEnabled && !_rightPathSelected && _gameCounter >= kPathBranchCounter) {
- if (_shipPosX > kCenterX) {
- _rightPathSelected = true;
- _vm->_smushVideoShouldFinish = true;
- debug(1, "RA1: Right path selected (counter=%d, shipX=%d)", _gameCounter, _shipPosX);
- }
- }
-
- debug(7, "RA1 ship: pos=(%d,%d) roll=%d lift=%d accX=%d accY=%d dir=%d health=%d corridor=[%d,%d]-[%d,%d]",
- _shipPosX, _shipPosY, _rollAccum, _liftSmooth,
- _posAccumX, _posAccumY, _shipDirIndex, _health,
- _corridorLeftX, _corridorTopY, _corridorRightX, _corridorBottomY);
-}
-
-// Level 2+ asteroid/surface physics (FUN_1CDA7).
-// Uses 10-frame input history averaging instead of accumulators.
-// Ship position = averaged input + center offset.
-// Viewport = second history buffer for smooth camera scrolling.
-void InsaneRebel1::updateAsteroidPhysics() {
- // Health regeneration (FUN_1BB0E): +1 every 32 frames when alive
- if (_health >= 0 && _health < kMaxHealth && (_frameCounter & 0x1F) == 0) {
- _health++;
- }
-
- // Damage application (FUN_1CDA7 lines 20-41)
- // No cooldown â all three damage types can stack each frame
- if (_damageFlags != 0 && _health >= 0 && _deathTimer < 1) {
- _screenFlash = 5;
- if (_damageFlags & 0x80)
- _health -= _tuning.shot;
- if (_damageFlags & 0x40)
- _health -= _tuning.miss;
- if (_damageFlags & 0x20)
- _health -= _tuning.wham;
- if (_health < 0) {
- _deathTimer = 15; // 0x0F â shorter than Level 1's 30
- }
- _damageFlags = 0;
- }
-
- // Death fade countdown
- if (_deathTimer > 1 && _health < 0) {
- _deathTimer--;
- }
-
- // Screen flash countdown
- if (_screenFlash > 0) {
- _screenFlash--;
- }
-
- // Input history shift and averaging (FUN_1CDA7 lines 107-131)
- // Shift history: [9] = [8], [8] = [7], ... [1] = [0], [0] = current input
- for (int i = kInputHistorySize - 1; i > 0; i--) {
- _inputHistoryX[i] = _inputHistoryX[i - 1];
- _inputHistoryY[i] = _inputHistoryY[i - 1];
- }
-
- // Read current mouse input (mapped to -160..+160 range)
- int mouseX = 0, mouseY = 0;
- if (_vm->_mouse.x >= 0) {
- mouseX = CLIP<int>(_vm->_mouse.x - kCenterX, -kCenterX, kCenterX);
- mouseY = CLIP<int>(_vm->_mouse.y - kCenterY, -kCenterY, kCenterY);
- }
- _inputHistoryX[0] = (int16)mouseX;
- _inputHistoryY[0] = (int16)mouseY;
-
- // Average over 10 frames
- int16 sumX = 0, sumY = 0;
- for (int i = 0; i < kInputHistorySize; i++) {
- sumX += _inputHistoryX[i];
- sumY -= _inputHistoryY[i]; // original negates Y: sVar5 = sVar5 - history[i]
- }
- _avgInputX = (int16)(sumX / kInputHistorySize);
- _avgInputY = (int16)(sumY / kInputHistorySize);
-
- // Clamp (original: [-0xA0, 0xA0] horizontal, [-0x46, 0x41] vertical)
- _avgInputX = CLIP<int16>(_avgInputX, -0xA0, 0xA0);
- _avgInputY = CLIP<int16>(_avgInputY, -0x46, 0x41);
-
- // Ship position = average + center offset
- // Original: _74BE = _75A8 + 0xA0, _74C0 = _75BC + 0x46
- _shipPosX = _avgInputX + 0xA0;
- _shipPosY = _avgInputY + 0x46;
-
- // Viewport history shift and averaging (FUN_1CDA7 lines 134-157)
- for (int i = kInputHistorySize - 1; i > 0; i--) {
- _viewHistoryX[i] = _viewHistoryX[i - 1];
- _viewHistoryY[i] = _viewHistoryY[i - 1];
- }
- _viewHistoryX[0] = _avgInputX;
- _viewHistoryY[0] = _avgInputY;
-
- int16 viewSumX = 0, viewSumY = 0;
- for (int i = 0; i < kInputHistorySize; i++) {
- viewSumX += _viewHistoryX[i];
- viewSumY += _viewHistoryY[i];
- }
- // Original: _74B6 = (viewAvgX >> 1) + 0x20, clamped [0, 0x40]
- // Original: _74B8 = (viewAvgY >> 1) + 0x17, clamped [0, 0x2E]
- _perspectiveX = CLIP<int16>((int16)(viewSumX / kInputHistorySize >> 1) + 0x20, 0, 0x40);
- _perspectiveY = CLIP<int16>((int16)(viewSumY / kInputHistorySize >> 1) + 0x17, 0, 0x2E);
-
- _frameCounter++;
-
- debug(7, "RA1 asteroid: pos=(%d,%d) avg=(%d,%d) view=(%d,%d) health=%d flash=%d",
- _shipPosX, _shipPosY, _avgInputX, _avgInputY,
- _perspectiveX, _perspectiveY, _health, _screenFlash);
-}
-
-void InsaneRebel1::renderShip(byte *dst, int pitch, int width, int height) {
- // From FUN_1DEB5 LAB_1e2b2: ship drawn when health >= 0 OR deathTimer > 20
- // Hidden during last 20 frames of death sequence (deathTimer 20â0)
- if (_health < 0 && _deathTimer <= 20)
- return;
-
- if (_shipDirIndex < 0 || _shipDirIndex >= _shipBank.numSprites)
- return;
-
- const RA1Sprite &spr = _shipBank.sprites[_shipDirIndex];
-
- // Position: game coords â screen coords via perspective transform
- // Adapted from RA2's renderHandler7Ship:
- // shipCenterX = (shipX - center) + perspX + screenCenterX
- int drawX = (_shipPosX - kCenterX) + _perspectiveX + kCenterX - spr.width / 2;
- int drawY = (_shipPosY - kCenterY) + _perspectiveY + kCenterY - spr.height / 2;
-
- renderSprite(dst, pitch, width, height, drawX, drawY, spr);
-}
-
-// Render explosion sprites during damage cooldown and death sequence.
-// From FUN_1DEB5 at LAB_1e185 (damage hit) and LAB_1e0e3 (death shake).
-void InsaneRebel1::renderExplosions(byte *dst, int pitch, int width, int height) {
- if (_bangBank.numSprites <= 0)
- return;
-
- // Ship screen center position (matches assembly: DAT_74b6+DAT_74ba, DAT_74b8+DAT_74bc)
- int shipScreenX = (_shipPosX - kCenterX) + _perspectiveX + kCenterX;
- int shipScreenY = (_shipPosY - kCenterY) + _perspectiveY + kCenterY;
-
- // --- Death shake explosions (FUN_1DEB5 LAB_1e0e3) ---
- // When dead and deathTimer > 10: random explosion sprites scatter around ship
- if (_health < 0 && _deathTimer > 10) {
- int intensity = _deathTimer - 10; // 20â1 as timer goes 30â11
- if (intensity > 10)
- intensity = 20 - intensity; // Triangle: 0â10â0
-
- // di = intensity * 4 + 1 (vertical scatter range)
- // si = -20 + intensity * 4 (horizontal scatter range, DAT_75d8 is 0)
- int rangeY = intensity * 4 + 1;
- int rangeX = -20 + intensity * 4;
- if (rangeX < 1) rangeX = 1;
-
- for (int i = 0; i < intensity; i++) {
- // Random sprite from bang bank (FUN_21db0(10))
- int sprIdx = _vm->_rnd.getRandomNumber(_bangBank.numSprites - 1);
-
- // Random position around ship (matching assembly random scatter)
- int randX = (int)_vm->_rnd.getRandomNumber(rangeX * 2) - rangeX;
- int randY = (int)_vm->_rnd.getRandomNumber(rangeY * 2) - rangeY;
-
- int drawX = shipScreenX + randX;
- int drawY = shipScreenY + randY;
-
- const RA1Sprite &spr = _bangBank.sprites[sprIdx];
- renderSprite(dst, pitch, width, height,
- drawX - spr.width / 2, drawY - spr.height / 2, spr);
- }
- return;
- }
-
- // --- Damage hit explosion (FUN_1DEB5 LAB_1e185) ---
- // When alive, in cooldown, and bang bank loaded
- if (_health >= 0 && _damageCooldown > 0) {
- // Sprite index = 10 - damageCooldown (frames 0â9 as cooldown 10â1)
- int sprIdx = _bangBank.numSprites - _damageCooldown;
- if (sprIdx < 0 || sprIdx >= _bangBank.numSprites)
- return;
-
- // Position at ship center (DAT_75d8 is always 0 in RA1)
- int drawX = shipScreenX;
- int drawY = shipScreenY;
-
- const RA1Sprite &spr = _bangBank.sprites[sprIdx];
- renderSprite(dst, pitch, width, height,
- drawX - spr.width / 2, drawY - spr.height / 2, spr);
- }
-}
-
-// Render bottom status bar from DISPLAY.NUT with dynamic damage bar and score.
-// Original layout (320-wide): DAMAGE [green bar] | PILOTS [3 icons] | SCORE [number]
-void InsaneRebel1::renderHUD(byte *dst, int pitch, int width, int height) {
- if (_displayBank.numSprites == 0)
- return;
-
- // Extra life bonus: every 10,000 points (FUN_1BBCB lines 11-27)
- if (_score / 10000 > _prevScore / 10000) {
- _lives++;
- }
- _prevScore = _score;
-
- const RA1Sprite &bar = _displayBank.sprites[0];
-
- // DISPLAY.NUT sprite is 320Ã19 at xoffs=0, yoffs=176 in the original game.
- // Video FOBJs fill the full 384Ã242 buffer from (0,0), so use sprite offsets directly.
- int hudX = bar.xoffs;
- int hudY = bar.yoffs;
-
- // Draw the status bar background with transparency (pixel 0 = transparent)
- if (bar.data && bar.width > 0 && bar.height > 0) {
- int drawX = hudX, drawY = hudY, drawW = bar.width, drawH = bar.height;
- int srcOffX = 0, srcOffY = 0;
- if (drawX < 0) { srcOffX = -drawX; drawW += drawX; drawX = 0; }
- if (drawY < 0) { srcOffY = -drawY; drawH += drawY; drawY = 0; }
- if (drawX + drawW > width) drawW = width - drawX;
- if (drawY + drawH > height) drawH = height - drawY;
-
- for (int iy = 0; iy < drawH; iy++) {
- const byte *s = bar.data + (srcOffY + iy) * bar.width + srcOffX;
- byte *d = dst + (drawY + iy) * pitch + drawX;
- for (int ix = 0; ix < drawW; ix++) {
- byte px = s[ix];
- if (px != 0)
- d[ix] = px;
- }
- }
-
- debug(5, "RA1 HUD: drawn at (%d,%d) size=%dx%d",
- hudX, hudY, bar.width, bar.height);
- }
-
- // Draw health bar from FUN_1BBCB behavior.
- // Original logic uses current health as fill width and computes x as (0x92 - health),
- // so the bar is right-anchored and shrinks from left to right as damage increases.
- {
- int barMaxW = kMaxHealth;
- int barH = 5;
- int healthWidth = CLIP<int16>(_health, 0, kMaxHealth);
- int barX = hudX + (0x92 - healthWidth);
- int barY = hudY + 8;
- int fillW = CLIP(healthWidth, 0, barMaxW);
-
- // Color based on damage level (matching original thresholds from FUN_1BBCB)
- // Palette indices: 0xD0-0xD7 = greens, 0x60-0x67 = yellows, 0xD8-0xDF = reds
- byte barColor;
- if (_health > _tuning.shot * 2)
- barColor = 0xD5; // Green (0,192,0) â low damage
- else if (_health > _tuning.wham * 2)
- barColor = 0x63; // Yellow (255,255,31) â moderate damage
- else
- barColor = 0xDD; // Red (192,0,0) â critical
-
- // Flash effect on damage
- if (_screenFlash > 0)
- barColor = 0xFF; // White flash
-
- for (int iy = 0; iy < barH && barY + iy < height; iy++) {
- byte *d = dst + (barY + iy) * pitch + barX;
- for (int ix = 0; ix < fillW && barX + ix < width; ix++) {
- d[ix] = barColor;
- }
- }
- }
-
- // Lives: black out excess pilot icons embedded in DISPLAY.NUT background.
- // Original FUN_1BBCB: FUN_21D66(buf, lives*10+186, 6, 51-lives*10, 9, 0, 320)
- // Icons are 5 slots at x=186..236, each ~10px wide. Cover unused slots with black.
- if (_lives >= 0 && _lives < 5) {
- int coverX = hudX + _lives * 10 + 186;
- int coverY = hudY + 6;
- int coverW = 51 - _lives * 10;
- int coverH = 9;
- if (coverX >= 0 && coverY >= 0) {
- for (int iy = 0; iy < coverH && coverY + iy < height; iy++) {
- byte *d = dst + (coverY + iy) * pitch + coverX;
- for (int ix = 0; ix < coverW && coverX + ix < width; ix++) {
- d[ix] = 0x00;
- }
- }
- }
- }
-
- // Score: 6-digit zero-padded at x=273, y=5 (original format '<<%%06ld' at 0x111,5)
- if (_hudFontBank.numSprites > 0) {
- char scoreStr[16];
- Common::sprintf_s(scoreStr, "%06d", MAX<int>(_score, 0));
- drawFontBankString(dst, pitch, width, height, hudX + 273, hudY + 5, scoreStr);
- }
-
-}
-
-void InsaneRebel1::renderSprite(byte *dst, int pitch, int width, int height,
- int x, int y, const RA1Sprite &spr) {
- if (!spr.data || spr.width <= 0 || spr.height <= 0)
- return;
-
- int drawX = x, drawY = y, drawW = spr.width, drawH = spr.height;
- int srcOffsetX = 0, srcOffsetY = 0;
-
- if (drawX < 0) { srcOffsetX = -drawX; drawW += drawX; drawX = 0; }
- if (drawY < 0) { srcOffsetY = -drawY; drawH += drawY; drawY = 0; }
- if (drawX + drawW > width) drawW = width - drawX;
- if (drawY + drawH > height) drawH = height - drawY;
- if (drawW <= 0 || drawH <= 0)
- return;
-
- for (int iy = 0; iy < drawH; iy++) {
- const byte *s = spr.data + (srcOffsetY + iy) * spr.width + srcOffsetX;
- byte *d = dst + (drawY + iy) * pitch + drawX;
- for (int ix = 0; ix < drawW; ix++) {
- byte px = s[ix];
- if (px != 0)
- d[ix] = px;
- }
- }
-}
-
-void InsaneRebel1::procIACT(byte *renderBitmap, int32 codecparam, int32 setupsan12,
- int32 setupsan13, Common::SeekableReadStream &b, int32 size, int32 flags,
- int16 par1, int16 par2, int16 par3, int16 par4) {
-}
-
-void InsaneRebel1::procSKIP(int32 subSize, Common::SeekableReadStream &b) {
-}
-
-// Parse RA1 GAME chunks.
-void InsaneRebel1::handleGameChunk(int32 subSize, Common::SeekableReadStream &b) {
- if (subSize < 8)
- return;
-
- uint32 opcode = b.readUint32BE();
- uint32 param1 = b.readUint32BE();
-
- switch (opcode) {
- case 0x5E:
- // RA1 dispatcher inline reset/init path (FUN_1BE1B case 0x5E).
- // This is not a pure control-mode assignment.
- _damageFlags = 0;
- _prevDamageFlags = 0;
- _damageCooldown = 0;
- _deathTimer = 0;
- _screenFlash = 0;
- _gameLatch5D = 0;
- _gameLatch5F = 0;
- _driftParam = 0;
- _rollAccum = 0;
- _liftSmooth = 0;
- _posAccumX = 0;
- _posAccumY = 0;
- memset(_inputHistoryX, 0, sizeof(_inputHistoryX));
- memset(_inputHistoryY, 0, sizeof(_inputHistoryY));
- memset(_viewHistoryX, 0, sizeof(_viewHistoryX));
- memset(_viewHistoryY, 0, sizeof(_viewHistoryY));
- _avgInputX = 0;
- _avgInputY = 0;
-
- // Field1 == 0 corresponds to baseline recenter behavior in the original.
- if ((int32)param1 == 0) {
- _shipPosX = kCenterX;
- _shipPosY = kCenterY;
- }
-
- // Keep a conservative default mode after reset.
- _flyControlMode = 0;
- debug(5, "RA1 GAME 0x5E: reset state field1=%d", (int32)param1);
- break;
-
- case 0x5D:
- _gameLatch5D = (uint16)param1;
- debug(5, "RA1 GAME 0x5D (link/event latch) param=%u", _gameLatch5D);
- break;
-
- case 0x5F:
- _gameLatch5F = (uint16)param1;
- debug(5, "RA1 GAME 0x5F (random-hit latch) param=%u", _gameLatch5F);
- break;
-
- case 0x07:
- // Per-frame corridor data: f1=frame counter, f2=max frames, f3=drift bias, f4=unused
- // f1 is the original's _DAT_7740 (game frame counter)
- // f3 is the drift/wind parameter combined with tuning table
- _gameCounter = param1;
- if (subSize >= 20) {
- b.readUint32BE(); // f2 (max frames, unused in physics)
- _driftParam = (int16)(int32)b.readUint32BE();
- b.readUint32BE(); // f4 (unused in original assembly)
- debug(7, "RA1 GAME 0x07: counter=%d driftParam=%d", _gameCounter, _driftParam);
- }
- break;
-
- case 0x0D:
- // Corridor boundaries: per-frame flight corridor
- // Original params: left, top, WIDTH, HEIGHT (not right/bottom!)
- // FUN_1C54D computes center = (left+width/2, top+height/2), transforms, then checks edges.
- if (subSize >= 20) {
- _corridorLeftX = (int16)param1;
- _corridorTopY = (int16)b.readUint32BE();
- int16 corridorWidth = (int16)b.readUint32BE();
- int16 corridorHeight = (int16)b.readUint32BE();
- _corridorRightX = _corridorLeftX + corridorWidth;
- _corridorBottomY = _corridorTopY + corridorHeight;
- debug(5, "RA1 GAME 0x0D: corridor left=%d top=%d right=%d bottom=%d (w=%d h=%d)",
- _corridorLeftX, _corridorTopY, _corridorRightX, _corridorBottomY,
- corridorWidth, corridorHeight);
- }
- break;
-
- case 0x0E:
- // Secondary collision zone (FUN_1C6E9): AABB test, sets damageFlags bit 4 (0x10)
- // Original params: left, top, WIDTH, HEIGHT (same as 0x0D)
- if (subSize >= 20) {
- int16 zoneLeft = (int16)param1;
- int16 zoneTop = (int16)b.readUint32BE();
- int16 zoneWidth = (int16)b.readUint32BE();
- int16 zoneHeight = (int16)b.readUint32BE();
- int16 zoneRight = zoneLeft + zoneWidth;
- int16 zoneBottom = zoneTop + zoneHeight;
- if (_shipPosX > zoneLeft && _shipPosX < zoneRight &&
- _shipPosY > zoneTop && _shipPosY < zoneBottom) {
- _damageFlags |= 0x10;
- }
- debug(7, "RA1 GAME 0x0E: zone=[%d,%d]-[%d,%d] flags=0x%02x",
- zoneLeft, zoneTop, zoneRight, zoneBottom, _damageFlags);
- }
- break;
-
- case 0x0B:
- // Asteroid/surface per-frame handler (FUN_1CDA7).
- // field1 = frame counter, field2 = max frames
- _gameCounter = param1;
- if (subSize >= 20) {
- b.readUint32BE(); // field2 (max frames)
- b.readUint32BE(); // field3
- b.readUint32BE(); // field4
- }
- debug(7, "RA1 GAME 0x0B: counter=%d", _gameCounter);
- break;
-
- case 0x08: case 0x09: case 0x0A:
- case 0x19: case 0x1A:
- if (subSize >= 20) {
- uint32 param2 = b.readUint32BE();
- uint32 param3 = b.readUint32BE();
- uint32 param4 = b.readUint32BE();
- debug(7, "RA1 GAME 0x%02x: params=(%d,%d,%d,%d)", opcode, param1, param2, param3, param4);
- }
- break;
-
- default:
- debug(7, "RA1 GAME unknown 0x%02x size=%d", opcode, subSize);
- break;
- }
-}
-
-// ---------------------------------------------------------------------------
-// Game flow (matching original at 0x15597)
-// ---------------------------------------------------------------------------
-
-// Play a passive cinematic (no game callback, skippable).
-// Reuses RA2's pattern: reset handler, set cinematic flags, play video.
-// startFrame > 0: fast-forward (decode without display/audio) to that frame.
-void InsaneRebel1::playCinematic(const char *filename, int32 startFrame) {
- debug(1, "InsaneRebel1::playCinematic('%s', startFrame=%d)", filename, startFrame);
- SmushPlayer *splayer = _vm->_splayer;
- _player = splayer;
- _interactiveVideoActive = false;
- _vm->_smushVideoShouldFinish = false;
- splayer->setCurVideoFlags(0x28); // Cinematic mode + buffer preserve
- if (startFrame > 0)
- splayer->setFastForwardToFrame(startFrame);
- splayer->play(filename, 12);
-}
-
-void InsaneRebel1::clearVideoBuffer() {
- if (_vm->_screenWidth <= 0 || _vm->_screenHeight <= 0)
- return;
-
- const int pixelCount = _vm->_screenWidth * _vm->_screenHeight;
- byte *clearBuffer = (byte *)calloc(pixelCount, 1);
- if (!clearBuffer)
- return;
-
- if (_vm->_macScreen) {
- _vm->mac_drawBufferToScreen(clearBuffer, _vm->_screenWidth, 0, 0, _vm->_screenWidth, _vm->_screenHeight);
- } else {
- _vm->_system->copyRectToScreen(clearBuffer, _vm->_screenWidth, 0, 0, _vm->_screenWidth, _vm->_screenHeight);
- }
- _vm->_system->updateScreen();
-
- free(clearBuffer);
-}
-
-// Intro sequence (0x155ef-0x158f8):
-// 1. O1LOGO.ANM â LucasArts logo
-// 2. O1OPEN.ANM â Star Wars opening crawl
-void InsaneRebel1::playIntroSequence() {
- debug(1, "InsaneRebel1: Playing intro sequence");
-
- // LucasArts logo (original: PUSH 0x57cc, CALL FUN_1BA32 with flags 0x0420)
- playCinematic("OPEN/O1LOGO.ANM");
- if (_vm->shouldQuit())
- return;
- clearVideoBuffer();
-
- // Star Wars opening crawl (original: PUSH 0x5800, CALL FUN_1BA32)
- playCinematic("OPEN/O1OPEN.ANM");
-}
-
-// Main menu on O1OPTION.ANM background (0x15968).
-// Original renders text overlay with 5 menu items via FUN_21F7A.
-// For now, we play the menu video as a passive cinematic (non-interactive)
-// and return "Start New Game" immediately.
-// TODO: Implement interactive menu with keyboard/mouse selection.
-int InsaneRebel1::runMainMenu() {
- debug(1, "InsaneRebel1: Main menu");
-
- _menuSelection = 0;
- while (!_vm->shouldQuit()) {
- _menuActive = true;
- _menuConfirmed = false;
- _menuFrameCounter = 0;
- clearVideoBuffer();
- playCinematic("OPEN/O1OPTION.ANM");
- _menuActive = false;
-
- if (_vm->shouldQuit())
- return 5;
-
- if (_menuConfirmed)
- return _menuSelection + 1;
- }
-
- return 5;
-}
-
-void InsaneRebel1::runOptionsMenu() {
- _optionsSel = 0;
- _optionsActive = true;
-
- while (!_vm->shouldQuit()) {
- _menuActive = true;
- _menuConfirmed = false;
- _menuFrameCounter = 0;
- clearVideoBuffer();
- playCinematic("OPEN/O1OPTION.ANM");
- _menuActive = false;
-
- if (_vm->shouldQuit())
- break;
-
- if (_menuConfirmed) {
- switch (_optionsSel) {
- case 0:
- // Cycle difficulty
- _difficulty = (_difficulty + 1) % 3;
- loadTuningForLevel(0);
- break;
- case 1:
- // Toggle turbulence
- _turbulenceEnabled = !_turbulenceEnabled;
- break;
- case 2:
- // Back to main menu
- _optionsActive = false;
- return;
- }
- }
- }
- _optionsActive = false;
-}
-
-// Level 1 flow (0x16100-0x167A2, from disassembly):
-// 1. Load NUTs (L1BANK1, L1BANK2, L1EXPLD, L1BANG, L1LASER)
-// 2. L1HANGAR.ANM â Full hangar departure cutscene (782 frames, flags 0x0420)
-// 3. L1CU1.ANM â Pre-flight cutscene (flags 0x0400)
-// 4. L1PLAY1L.ANM â Stage 1 flight, hard/left path (788 frames)
-// At frame 394, if player steers right â L1PLAY1R (easy path, 396 frames)
-// 5. L1CU2.ANM â Mid-level cutscene
-// 6. L1PLAY2.ANM â Stage 2 turret
-// If score < 5 (0x75D0): L1RETRY â retry Stage 2
-// 7. L1END.ANM â Level complete
-// Death (health<0): L1CRASHA/B â lives check:
-// lives>0: L1NEW â jump back to Stage 1 (skip L1HANGAR/L1CU1)
-// lives==0: L1DEATH â return to menu
-
-bool InsaneRebel1::runLevel1() {
- debug(1, "InsaneRebel1: Running level 1");
-
- // Load level sprites (original: pushes L1BANK1..L1BANG NUT filenames)
- loadLevelSprites(1);
-
- // L1HANGAR.ANM â Hangar departure (original: 0x5918, flags 0x0420)
- // Plays once at level start, never replayed on retry.
- playCinematic("LVL1/L1HANGAR.ANM");
- if (_vm->shouldQuit())
- return false;
-
- // L1CU1.ANM â Pre-flight cutscene (original: 0x5944, flags 0x0400)
- // Plays once at level start, never replayed on retry.
- playCinematic("LVL1/L1CU1.ANM");
- if (_vm->shouldQuit())
- return false;
-
- // Retry loop â on death with lives, L1NEW plays then jumps back here
- while (!_vm->shouldQuit()) {
- // Reset health for this attempt (original: MOV WORD [0x7560], 98 at 0x16214)
- _health = kMaxHealth;
- _damageFlags = 0;
- _prevDamageFlags = 0;
- _damageCooldown = 0;
- _deathTimer = 0;
- _screenFlash = 0;
- _frameCounter = 0;
- _gameCounter = 0;
- _pathBranchEnabled = true;
- _rightPathSelected = false;
-
- // Stage 1 flight â L1PLAY1L (hard/left path)
- // The first 394 frames are the common section. At counter 394, if
- // ship is right of center, we switch to L1PLAY1R (easy path).
- playInteractiveVideo("LVL1/L1PLAY1L.ANM");
- if (_vm->shouldQuit())
- return false;
-
- if (_rightPathSelected && _health >= 0) {
- debug(1, "InsaneRebel1: Switching to right path (L1PLAY1R)");
- _pathBranchEnabled = false;
- playInteractiveVideo("LVL1/L1PLAY1R.ANM");
- if (_vm->shouldQuit())
- return false;
- }
- _pathBranchEnabled = false;
-
- if (_health >= 0) {
- // L1CU2.ANM â Mid-level cutscene (original: 0x5977)
- playCinematic("LVL1/L1CU2.ANM");
- if (_vm->shouldQuit())
- return false;
-
- // L1PLAY2.ANM â Stage 2 turret (original: 0x5986)
- playInteractiveVideo("LVL1/L1PLAY2.ANM");
- if (_vm->shouldQuit())
- return false;
-
- // TODO: Check score threshold (original: CMP WORD [0x75D0], 5)
- // If score < 5: L1RETRY â retry Stage 2
-
- // L1END.ANM â Level complete! (original: 0x59a3)
- playCinematic("LVL1/L1END.ANM");
- return true;
- }
-
- // Death sequence (original: 0x165dd-0x166bb)
- // Random crash variant A or B
- if (_vm->_rnd.getRandomNumber(1) == 0)
- playCinematic("LVL1/L1CRASHA.ANM");
- else
- playCinematic("LVL1/L1CRASHB.ANM");
- if (_vm->shouldQuit())
- return false;
-
- // Check lives (original: CMP WORD [0x7562], 0 at 0x1666B)
- _lives--;
- if (_lives <= 0) {
- // Game over â L1DEATH then return (original: 0x166C0)
- playCinematic("LVL1/L1DEATH.ANM");
- debug(1, "InsaneRebel1: Game over (no lives left)");
- return false;
- }
-
- // Lives remaining â L1NEW briefing then retry (original: 0x16675)
- playCinematic("LVL1/L1NEW.ANM");
- if (_vm->shouldQuit())
- return false;
-
- // Loop back to gameplay (original: JMP 0x16214 â health reset + Stage 1)
- }
-
- return false;
-}
-
-// Level 2: Asteroid Field Training
-// Flow: L2NEW â L2INTRO â L2PLAY (interactive) â L2END/L2DEATH
-bool InsaneRebel1::runLevel2() {
- debug(1, "InsaneRebel1: Running level 2");
-
- _currentLevel = 1;
- loadLevelSprites(2);
- loadTuningForLevel(1);
-
- // L2INTRO.ANM â intro cutscene (481 frames)
- playCinematic("LVL2/L2INTRO.ANM");
- if (_vm->shouldQuit())
- return false;
-
- // Retry loop
- while (!_vm->shouldQuit()) {
- // Reset state for this attempt
- _health = kMaxHealth;
- _damageFlags = 0;
- _prevDamageFlags = 0;
- _damageCooldown = 0;
- _deathTimer = 0;
- _screenFlash = 0;
- _frameCounter = 0;
- _gameCounter = 0;
- memset(_inputHistoryX, 0, sizeof(_inputHistoryX));
- memset(_inputHistoryY, 0, sizeof(_inputHistoryY));
- memset(_viewHistoryX, 0, sizeof(_viewHistoryX));
- memset(_viewHistoryY, 0, sizeof(_viewHistoryY));
- _avgInputX = 0;
- _avgInputY = 0;
-
- // L2PLAY.ANM â asteroid dodge (800 frames, interactive)
- playInteractiveVideo("LVL2/L2PLAY.ANM");
- if (_vm->shouldQuit())
- return false;
-
- if (_health >= 0) {
- // Level complete!
- playCinematic("LVL2/L2END.ANM");
- return true;
- }
-
- // Death
- playCinematic("LVL2/L2DEATH.ANM");
- if (_vm->shouldQuit())
- return false;
-
- _lives--;
- if (_lives <= 0) {
- debug(1, "InsaneRebel1: Game over (no lives left)");
- return false;
- }
-
- // Retry briefing
- playCinematic("LVL2/L2NEW.ANM");
- if (_vm->shouldQuit())
- return false;
- }
-
- return false;
-}
-
-// Main game entry point â called from ScummEngine::go().
-// Matches original flow at 0x15597: intro â menu â level.
-void InsaneRebel1::runGame() {
- // Play intro sequence (logo + opening)
- playIntroSequence();
- if (_vm->shouldQuit())
- return;
-
- // Main menu â gameplay loop
- while (!_vm->shouldQuit()) {
- int menuResult = runMainMenu();
- if (_vm->shouldQuit())
- return;
-
- switch (menuResult) {
- case 1: {
-#if 0 // Skip level 1 for testing â jump straight to level 2
- // Start New Game â play L1NEW briefing then level 1
- playCinematic("LVL1/L1NEW.ANM");
- if (_vm->shouldQuit())
- return;
-
- bool completed = runLevel1();
-#else
- bool completed = true;
-#endif
- if (completed && !_vm->shouldQuit()) {
- completed = runLevel2();
- if (completed) {
- debug(1, "InsaneRebel1: Level 2 completed!");
- // TODO: Continue to level 3
- }
- }
- _currentLevel = 0;
- // Return to menu after level ends
- break;
- }
- case 2:
- // Game Options
- runOptionsMenu();
- break;
- case 5:
- // Exit
- return;
- default:
- // Passcode, Demo â not yet implemented, return to menu
- break;
- }
- }
-}
-
-// Play interactive gameplay video (with ship physics + HUD).
-void InsaneRebel1::playInteractiveVideo(const char *filename) {
- debug(1, "InsaneRebel1::playInteractiveVideo('%s')", filename);
-
- // Stop any leftover audio from previous video
- terminateAudio();
- initAudio(_audioSampleRate);
-
- SmushPlayer *splayer = _vm->_splayer;
- _player = splayer;
- clearBit(0);
- _interactiveVideoActive = true;
- _vm->_smushVideoShouldFinish = false;
- splayer->setCurVideoFlags(0x28);
-
- // Center mouse, hide system cursor (we draw our own), lock mouse to window
- smush_warpMouse(160, 100, -1);
- CursorMan.showMouse(false);
- g_system->lockMouse(true);
-
- splayer->play(filename, 12);
- _interactiveVideoActive = false;
-
- g_system->lockMouse(false);
-}
-
} // End of namespace Scumm
diff --git a/engines/scumm/insane/insane_rebel1.h b/engines/scumm/insane/insane_rebel1.h
index 61e1b28f968..941f27998ee 100644
--- a/engines/scumm/insane/insane_rebel1.h
+++ b/engines/scumm/insane/insane_rebel1.h
@@ -53,6 +53,16 @@ struct RA1SpriteBank {
~RA1SpriteBank() { delete[] sprites; free(decodedData); }
};
+// RA1 coordinate constants (scaled from RA2's 424x260 â 384x242)
+static const int16 kRA1CenterX = 160;
+static const int16 kRA1CenterY = 100;
+static const int16 kRA1MinX = 20;
+static const int16 kRA1MaxX = 300;
+static const int16 kRA1MinY = 20;
+static const int16 kRA1MaxY = 180;
+static const int16 kRA1FocalX = 43;
+static const int16 kRA1FocalY = 25;
+
/**
* Star Wars: Rebel Assault (RA1) game logic.
* Adapts RA2 Handler 7 (ship flight) physics for RA1's 384x242 resolution.
diff --git a/engines/scumm/insane/insane_rebel1_audio.cpp b/engines/scumm/insane/insane_rebel1_audio.cpp
new file mode 100644
index 00000000000..7ca9e73d7b5
--- /dev/null
+++ b/engines/scumm/insane/insane_rebel1_audio.cpp
@@ -0,0 +1,205 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include "common/system.h"
+
+#include "audio/audiostream.h"
+#include "audio/decoders/raw.h"
+#include "audio/mixer.h"
+
+#include "scumm/scumm_v7.h"
+#include "scumm/insane/insane_rebel1.h"
+
+namespace Scumm {
+
+// ---------------------------------------------------------------------------
+// Audio
+// ---------------------------------------------------------------------------
+
+void InsaneRebel1::initAudio(int sampleRate) {
+ _audioSampleRate = sampleRate;
+ for (int i = 0; i < kMaxAudioTracks; i++) {
+ _audioStreams[i] = nullptr;
+ _audioTrackActive[i] = false;
+ }
+}
+
+void InsaneRebel1::terminateAudio() {
+ for (int i = 0; i < kMaxAudioTracks; i++) {
+ if (_audioTrackActive[i]) {
+ _vm->_mixer->stopHandle(_audioHandles[i]);
+ _audioTrackActive[i] = false;
+ }
+ if (_audioStreams[i]) {
+ _audioStreams[i]->finish();
+ _audioStreams[i] = nullptr;
+ }
+ }
+}
+
+void InsaneRebel1::queueAudioData(int trackIdx, uint8 *data, int32 size, int volume, int pan) {
+ if (trackIdx < 0 || trackIdx >= kMaxAudioTracks || size <= 0 || !data)
+ return;
+
+ if (!_audioStreams[trackIdx]) {
+ debug(1, "InsaneRebel1: Creating audio stream for track %d at %d Hz", trackIdx, _audioSampleRate);
+ _audioStreams[trackIdx] = Audio::makeQueuingAudioStream(_audioSampleRate, false);
+ _audioTrackActive[trackIdx] = true;
+ _vm->_mixer->playStream(Audio::Mixer::kSFXSoundType, &_audioHandles[trackIdx],
+ _audioStreams[trackIdx], -1, Audio::Mixer::kMaxChannelVolume, 0,
+ DisposeAfterUse::NO);
+ }
+
+ byte *audioCopy = (byte *)malloc(size);
+ if (!audioCopy)
+ return;
+ memcpy(audioCopy, data, size);
+
+ _audioStreams[trackIdx]->queueBuffer(audioCopy, size, DisposeAfterUse::YES, Audio::FLAG_UNSIGNED);
+
+ int scaledVolume = (volume * Audio::Mixer::kMaxChannelVolume) / 127;
+ int scaledPan = (pan * 127) / 128;
+ _vm->_mixer->setChannelVolume(_audioHandles[trackIdx], scaledVolume);
+ _vm->_mixer->setChannelBalance(_audioHandles[trackIdx], scaledPan);
+}
+
+void InsaneRebel1::processAudioFrame(int16 feedSize) {
+ if (!_player)
+ return;
+
+ SmushPlayer *sp = _player;
+
+ if (sp->_smushTracksNeedInit) {
+ sp->_smushTracksNeedInit = false;
+ for (int i = 0; i < SMUSH_MAX_TRACKS; i++) {
+ sp->_smushDispatch[i].fadeRemaining = 0;
+ sp->_smushDispatch[i].fadeVolume = 0;
+ sp->_smushDispatch[i].fadeSampleRate = 0;
+ sp->_smushDispatch[i].elapsedAudio = 0;
+ sp->_smushDispatch[i].audioLength = 0;
+ }
+ }
+
+ for (int i = 0; i < sp->_smushNumTracks; i++) {
+ SmushPlayer::SmushAudioTrack &track = sp->_smushTracks[i];
+ SmushPlayer::SmushAudioDispatch &dispatch = sp->_smushDispatch[i];
+
+ if (track.state == TRK_STATE_INACTIVE || !track.blockPtr)
+ continue;
+
+ bool isPlayableTrack = ((track.flags & TRK_TYPE_MASK) == IS_SPEECH && sp->isChanActive(CHN_SPEECH)) ||
+ ((track.flags & TRK_TYPE_MASK) == IS_BKG_MUSIC && sp->isChanActive(CHN_BKGMUS)) ||
+ ((track.flags & TRK_TYPE_MASK) == IS_SFX && sp->isChanActive(CHN_OTHER));
+
+ if (!isPlayableTrack)
+ continue;
+
+ int baseVolume;
+ switch (track.flags & TRK_TYPE_MASK) {
+ case IS_SFX:
+ baseVolume = (sp->_smushTrackVols[1] * track.volume) >> 7;
+ break;
+ case IS_BKG_MUSIC:
+ baseVolume = (sp->_smushTrackVols[3] * track.volume) >> 7;
+ break;
+ case IS_SPEECH:
+ baseVolume = (sp->_smushTrackVols[2] * track.volume) >> 7;
+ break;
+ default:
+ baseVolume = track.volume;
+ break;
+ }
+ int mixVolume = baseVolume * sp->_smushTrackVols[0] / 127;
+
+ // Handle FADING -> PLAYING transition
+ if (track.state == TRK_STATE_FADING) {
+ dispatch.headerPtr = track.dataBuf;
+ dispatch.dataBuf = track.subChunkPtr;
+ dispatch.dataSize = track.dataSize;
+ dispatch.currentOffset = 0;
+ dispatch.audioLength = 0;
+ track.state = TRK_STATE_PLAYING;
+ }
+
+ if (track.state != TRK_STATE_INACTIVE) {
+ int32 tmpFeedSize = feedSize;
+
+ while (tmpFeedSize > 0) {
+ int32 mixInFrameCount = dispatch.currentOffset;
+
+ if (mixInFrameCount > 0 && dispatch.dataBuf && dispatch.dataSize > 0) {
+ if (dispatch.audioRemaining < 0)
+ dispatch.audioRemaining = 0;
+
+ int32 offset = dispatch.audioRemaining % dispatch.dataSize;
+
+ if (dispatch.sampleRate > 0 && sp->_smushAudioSampleRate > 0) {
+ int32 maxFrames = dispatch.sampleRate * tmpFeedSize / sp->_smushAudioSampleRate;
+ if (mixInFrameCount > maxFrames)
+ mixInFrameCount = maxFrames;
+ }
+
+ if (offset + mixInFrameCount > dispatch.dataSize)
+ mixInFrameCount = dispatch.dataSize - offset;
+
+ if (dispatch.audioRemaining + mixInFrameCount > track.availableSize) {
+ mixInFrameCount = track.availableSize - dispatch.audioRemaining;
+ if (mixInFrameCount <= 0) {
+ track.state = TRK_STATE_ENDING;
+ break;
+ }
+ }
+
+ if (mixInFrameCount > 0) {
+ if (!dispatch.dataBuf || offset < 0 || offset + mixInFrameCount > dispatch.dataSize)
+ break;
+
+ queueAudioData(i, &dispatch.dataBuf[offset], mixInFrameCount, mixVolume, track.pan);
+
+ dispatch.currentOffset -= mixInFrameCount;
+ dispatch.audioRemaining += mixInFrameCount;
+
+ if (dispatch.sampleRate > 0) {
+ int32 consumedFeed = mixInFrameCount * sp->_smushAudioSampleRate / dispatch.sampleRate;
+ tmpFeedSize -= consumedFeed;
+ } else {
+ tmpFeedSize -= mixInFrameCount;
+ }
+ }
+ }
+
+ if (dispatch.currentOffset <= 0) {
+ if (!sp->processAudioCodes(i, tmpFeedSize, mixVolume))
+ break;
+ if (dispatch.currentOffset <= 0)
+ break;
+ } else if (tmpFeedSize <= 0) {
+ break;
+ }
+ }
+ }
+
+ track.audioRemaining = dispatch.audioRemaining;
+ dispatch.state = track.state;
+ }
+}
+
+} // End of namespace Scumm
diff --git a/engines/scumm/insane/insane_rebel1_iact.cpp b/engines/scumm/insane/insane_rebel1_iact.cpp
new file mode 100644
index 00000000000..7500706e548
--- /dev/null
+++ b/engines/scumm/insane/insane_rebel1_iact.cpp
@@ -0,0 +1,478 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include "common/system.h"
+#include "common/endian.h"
+
+#include "scumm/scumm_v7.h"
+#include "scumm/insane/insane_rebel1.h"
+
+namespace Scumm {
+
+// Ship physics matching FUN_1DEB5 (accumulator-based position system).
+// Roll accumulator (_74CA) driven by input, position accumulators (_74C2/_74C6)
+// driven by roll + drift + cross-coupling. Ship position = base + accum >> 8.
+void InsaneRebel1::updateShipPhysics() {
+ _frameCounter++;
+
+ // Decrement cooldown
+ if (_damageCooldown > 0)
+ _damageCooldown--;
+
+ // --- Step 1: Mouse input as offset from screen center ---
+ // Original: _DAT_756C (horizontal), _DAT_756E (vertical)
+ int16 inputX = (int16)(_vm->_mouse.x - 160);
+ int16 inputY = (int16)(_vm->_mouse.y - 100);
+ inputX = CLIP<int16>(inputX, -127, 127);
+ inputY = CLIP<int16>(inputY, -127, 127);
+
+ // --- Step 2: Roll accumulator (_74CA) ---
+ // Normal mode: accumulate; mode 0x10: snap to input
+ _rollAccum += (_tuning.roll * (int32)inputX) >> 5;
+ _rollAccum = CLIP<int32>(_rollAccum, -0x47F, 0x47F);
+
+ // --- Step 3: Vertical smoothing (_74CE) ---
+ // Exponential decay toward -inputY
+ _liftSmooth += (-_liftSmooth - (int32)inputY) >> 1;
+ _liftSmooth = CLIP<int32>(_liftSmooth, -0x20, 0x20);
+
+ // --- Step 4: Position accumulator deltas ---
+ // X delta: drift + slide coupling - cross-coupling
+ int32 rng = _turbulenceEnabled ? (int32)_vm->_rnd.getRandomNumber(199) : 100; // 0-199, centered at 100
+ int32 crossTermX;
+ if (_liftSmooth < 0)
+ crossTermX = ((int32)_tuning.lift * _liftSmooth * _rollAccum) >> 11;
+ else
+ crossTermX = ((int32)_tuning.lift * _liftSmooth * _rollAccum) >> 12;
+
+ int32 deltaX = (((rng - 100) - (int32)_tuning.drift * _driftParam) >> 1)
+ + (((int32)_tuning.slide * _rollAccum) >> 7)
+ - crossTermX;
+
+ // Y delta: roll magnitude + lift cross-coupling
+ int32 absRoll = ABS(_rollAccum);
+ int32 crossTermY;
+ if (_liftSmooth < 0)
+ crossTermY = ((int32)_tuning.lift * (0x7DE - absRoll) * _liftSmooth) >> 12;
+ else
+ crossTermY = ((int32)_tuning.lift * (0x7DE - absRoll) * _liftSmooth) >> 13;
+
+ int32 deltaY = (absRoll >> 1) + crossTermY;
+
+ // --- Step 5: Update position accumulators ---
+ _posAccumX += deltaX;
+ _posAccumX = CLIP<int32>(_posAccumX, -0x8200, 0x8200);
+ _posAccumY += deltaY;
+ _posAccumY = CLIP<int32>(_posAccumY, -0x3200, 0x4600);
+
+ // --- Step 6: Derive pixel position from accumulators ---
+ // Original: _74BA = _74C2 >> 8, _74BC = _74C6 >> 8
+ // Ship position = base + offset
+ _shipPosX = kRA1CenterX + (int16)(_posAccumX >> 8);
+ _shipPosY = kRA1CenterY + (int16)(_posAccumY >> 8);
+
+ // Clamp to screen bounds
+ _shipPosX = CLIP<int16>(_shipPosX, kRA1MinX, kRA1MaxX);
+ _shipPosY = CLIP<int16>(_shipPosY, kRA1MinY, kRA1MaxY);
+
+ // --- Step 7: Corridor collision (FUN_1C54D) ---
+ // Wall contact forces position accumulators to corridor edge and sets
+ // damage flags. Flag bit 0x10 (zone hit) suppresses damage bits only.
+ {
+ bool hasZoneHit = (_damageFlags & 0x10) != 0;
+
+ if (_shipPosX > _corridorRightX) {
+ _posAccumX = (_corridorRightX - kRA1CenterX) << 8;
+ _shipPosX = _corridorRightX;
+ if (!hasZoneHit) {
+ if (_rollAccum > -0x100)
+ _rollAccum = -0x100; // Push left
+ _damageFlags |= 0x02; // Right wall
+ }
+ }
+ if (_shipPosX < _corridorLeftX) {
+ _posAccumX = (_corridorLeftX - kRA1CenterX) << 8;
+ _shipPosX = _corridorLeftX;
+ if (!hasZoneHit) {
+ if (_rollAccum < 0x100)
+ _rollAccum = 0x100; // Push right
+ _damageFlags |= 0x04; // Left wall
+ }
+ }
+ if (_shipPosY < _corridorTopY) {
+ _posAccumY = ((_corridorTopY - kRA1CenterY) << 8) + 0x100;
+ _shipPosY = _corridorTopY;
+ if (!hasZoneHit)
+ _damageFlags |= 0x01;
+ }
+ if (_shipPosY > _corridorBottomY) {
+ _posAccumY = ((_corridorBottomY - kRA1CenterY) << 8) - 0x100;
+ _shipPosY = _corridorBottomY;
+ if (!hasZoneHit)
+ _damageFlags |= 0x08;
+ }
+ }
+
+ // --- Step 8: Perspective offsets ---
+ {
+ int absOffX = ABS(_shipPosX - kRA1CenterX);
+ if (absOffX > 0)
+ _perspectiveX = (int16)((kRA1FocalX * kRA1CenterX * absOffX) /
+ ((kRA1CenterX - kRA1FocalX) * absOffX + kRA1FocalX * kRA1CenterX));
+ else
+ _perspectiveX = 0;
+ if (_shipPosX < kRA1CenterX + 1)
+ _perspectiveX = -_perspectiveX;
+
+ int absOffY = ABS(_shipPosY - kRA1CenterY);
+ if (absOffY > 0)
+ _perspectiveY = (int16)((kRA1FocalY * kRA1CenterY * absOffY) /
+ ((kRA1CenterY - kRA1FocalY) * absOffY + kRA1FocalY * kRA1CenterY));
+ else
+ _perspectiveY = 0;
+ if (_shipPosY < kRA1CenterY + 1)
+ _perspectiveY = -_perspectiveY;
+ }
+
+ // --- Step 9: Direction sprite index (FUN_1DEB5 LAB_1e23e) ---
+ // Horizontal component from _74CA (rollAccum):
+ // |rollAccum| <= 0x80: center (0)
+ // rollAccum > 0x80: ((rollAccum - 0x80) >> 8) * 5 + 5 (right: 5,10,15,20)
+ // rollAccum < -0x80: ((abs(rollAccum) - 0x80) >> 8) * 5 + 25 (left: 25,30,35,40)
+ int hComponent;
+ if (_rollAccum > 0x80) {
+ hComponent = ((_rollAccum - 0x80) >> 8) * 5 + 5;
+ } else if (_rollAccum < -0x80) {
+ hComponent = ((-_rollAccum - 0x80) >> 8) * 5 + 25;
+ } else {
+ hComponent = 0;
+ }
+
+ // Vertical component from _74CE (liftSmooth):
+ // (_74CE + 0x20) * 5 / 0x41 â 0..4 (5 rows)
+ int vComponent = (_liftSmooth + 0x20) * 5 / 0x41;
+
+ _shipDirIndex = CLIP<int16>((int16)(vComponent + hComponent), 0, _shipBank.numSprites - 1);
+
+ // --- Step 10: Damage/event bit synthesis + damage processing ---
+ // RA1 FUN_1B297-style latches from GAME opcodes:
+ // 0x5D latch 0xFFFF -> bit 0x40 (obstacle/contact)
+ // 0x5F non-zero + RNG -> bit 0x80 (projectile-like hit)
+ if (_gameLatch5D == 0xFFFF)
+ _damageFlags |= 0x40;
+ if (_gameLatch5F != 0 && _vm->_rnd.getRandomNumber((uint16)(_gameLatch5F - 1)) == 0)
+ _damageFlags |= 0x80;
+
+ // Damage guard/mask from FUN_1DEB5: (_damageFlags & 0x96) != 0
+ // damageFlags & 0x96 = bits 1,2,4,7 = wall collisions (0x16) + projectile hit (0x80)
+ if ((_damageFlags & 0x96) != 0 && _damageCooldown == 0 &&
+ _health >= 0 && _deathTimer <= 0) {
+ // Projectile hit (bit 7 = 0x80)
+ if (_damageFlags & 0x80)
+ _health -= _tuning.shot;
+ // Wall collision (bits 1,2,4 = 0x16)
+ if (_damageFlags & 0x16)
+ _health -= _tuning.wham;
+
+ if (_health < 0)
+ _deathTimer = kDeathTimerInit;
+
+ _prevDamageFlags = _damageFlags;
+ _damageCooldown = kDamageCooldownInit;
+ _screenFlash = 3;
+ }
+
+ // Latches are per-frame event inputs in the original pipeline.
+ _gameLatch5D = 0;
+ _gameLatch5F = 0;
+
+ // Death animation countdown
+ if (_health < 0 && _deathTimer > 0)
+ _deathTimer--;
+
+ // Health regeneration: +1 every 32 frames (from original asm)
+ if ((_frameCounter & 0x1F) == 0) {
+ if (_health >= 0 && _health < kMaxHealth)
+ _health++;
+ if (_health >= 0)
+ _score += 1;
+ }
+
+ // Screen flash decay
+ if (_screenFlash > 0)
+ _screenFlash--;
+
+ // Clear per-frame damage flags
+ _damageFlags = 0;
+
+ // --- Path branching detection ---
+ // Original (FUN_1B297): at GAME counter 394 (0x18A), sets nextSceneA=0x67/nextSceneB=0x69.
+ // After this point, drift goes strongly negative (pushing ship left for the hard path).
+ // If ship is right of center, player chose the right/easy path â switch to L1PLAY1R.
+ // The check fires once when the game counter first reaches the branch point.
+ if (_pathBranchEnabled && !_rightPathSelected && _gameCounter >= kPathBranchCounter) {
+ if (_shipPosX > kRA1CenterX) {
+ _rightPathSelected = true;
+ _vm->_smushVideoShouldFinish = true;
+ debug(1, "RA1: Right path selected (counter=%d, shipX=%d)", _gameCounter, _shipPosX);
+ }
+ }
+
+ debug(7, "RA1 ship: pos=(%d,%d) roll=%d lift=%d accX=%d accY=%d dir=%d health=%d corridor=[%d,%d]-[%d,%d]",
+ _shipPosX, _shipPosY, _rollAccum, _liftSmooth,
+ _posAccumX, _posAccumY, _shipDirIndex, _health,
+ _corridorLeftX, _corridorTopY, _corridorRightX, _corridorBottomY);
+}
+
+// Level 2+ asteroid/surface physics (FUN_1CDA7).
+// Uses 10-frame input history averaging instead of accumulators.
+// Ship position = averaged input + center offset.
+// Viewport = second history buffer for smooth camera scrolling.
+void InsaneRebel1::updateAsteroidPhysics() {
+ // Health regeneration (FUN_1BB0E): +1 every 32 frames when alive
+ if (_health >= 0 && _health < kMaxHealth && (_frameCounter & 0x1F) == 0) {
+ _health++;
+ }
+
+ // Damage application (FUN_1CDA7 lines 20-41)
+ // No cooldown â all three damage types can stack each frame
+ if (_damageFlags != 0 && _health >= 0 && _deathTimer < 1) {
+ _screenFlash = 5;
+ if (_damageFlags & 0x80)
+ _health -= _tuning.shot;
+ if (_damageFlags & 0x40)
+ _health -= _tuning.miss;
+ if (_damageFlags & 0x20)
+ _health -= _tuning.wham;
+ if (_health < 0) {
+ _deathTimer = 15; // 0x0F â shorter than Level 1's 30
+ }
+ _damageFlags = 0;
+ }
+
+ // Death fade countdown
+ if (_deathTimer > 1 && _health < 0) {
+ _deathTimer--;
+ }
+
+ // Screen flash countdown
+ if (_screenFlash > 0) {
+ _screenFlash--;
+ }
+
+ // Input history shift and averaging (FUN_1CDA7 lines 107-131)
+ // Shift history: [9] = [8], [8] = [7], ... [1] = [0], [0] = current input
+ for (int i = kInputHistorySize - 1; i > 0; i--) {
+ _inputHistoryX[i] = _inputHistoryX[i - 1];
+ _inputHistoryY[i] = _inputHistoryY[i - 1];
+ }
+
+ // Read current mouse input (mapped to -160..+160 range)
+ int mouseX = 0, mouseY = 0;
+ if (_vm->_mouse.x >= 0) {
+ mouseX = CLIP<int>(_vm->_mouse.x - kRA1CenterX, -kRA1CenterX, kRA1CenterX);
+ mouseY = CLIP<int>(_vm->_mouse.y - kRA1CenterY, -kRA1CenterY, kRA1CenterY);
+ }
+ _inputHistoryX[0] = (int16)mouseX;
+ _inputHistoryY[0] = (int16)mouseY;
+
+ // Average over 10 frames
+ int16 sumX = 0, sumY = 0;
+ for (int i = 0; i < kInputHistorySize; i++) {
+ sumX += _inputHistoryX[i];
+ sumY -= _inputHistoryY[i]; // original negates Y: sVar5 = sVar5 - history[i]
+ }
+ _avgInputX = (int16)(sumX / kInputHistorySize);
+ _avgInputY = (int16)(sumY / kInputHistorySize);
+
+ // Clamp (original: [-0xA0, 0xA0] horizontal, [-0x46, 0x41] vertical)
+ _avgInputX = CLIP<int16>(_avgInputX, -0xA0, 0xA0);
+ _avgInputY = CLIP<int16>(_avgInputY, -0x46, 0x41);
+
+ // Ship position = average + center offset
+ // Original: _74BE = _75A8 + 0xA0, _74C0 = _75BC + 0x46
+ _shipPosX = _avgInputX + 0xA0;
+ _shipPosY = _avgInputY + 0x46;
+
+ // Viewport history shift and averaging (FUN_1CDA7 lines 134-157)
+ for (int i = kInputHistorySize - 1; i > 0; i--) {
+ _viewHistoryX[i] = _viewHistoryX[i - 1];
+ _viewHistoryY[i] = _viewHistoryY[i - 1];
+ }
+ _viewHistoryX[0] = _avgInputX;
+ _viewHistoryY[0] = _avgInputY;
+
+ int16 viewSumX = 0, viewSumY = 0;
+ for (int i = 0; i < kInputHistorySize; i++) {
+ viewSumX += _viewHistoryX[i];
+ viewSumY += _viewHistoryY[i];
+ }
+ // Original: _74B6 = (viewAvgX >> 1) + 0x20, clamped [0, 0x40]
+ // Original: _74B8 = (viewAvgY >> 1) + 0x17, clamped [0, 0x2E]
+ _perspectiveX = CLIP<int16>((int16)(viewSumX / kInputHistorySize >> 1) + 0x20, 0, 0x40);
+ _perspectiveY = CLIP<int16>((int16)(viewSumY / kInputHistorySize >> 1) + 0x17, 0, 0x2E);
+
+ _frameCounter++;
+
+ debug(7, "RA1 asteroid: pos=(%d,%d) avg=(%d,%d) view=(%d,%d) health=%d flash=%d",
+ _shipPosX, _shipPosY, _avgInputX, _avgInputY,
+ _perspectiveX, _perspectiveY, _health, _screenFlash);
+}
+
+
+void InsaneRebel1::procIACT(byte *renderBitmap, int32 codecparam, int32 setupsan12,
+ int32 setupsan13, Common::SeekableReadStream &b, int32 size, int32 flags,
+ int16 par1, int16 par2, int16 par3, int16 par4) {
+}
+
+void InsaneRebel1::procSKIP(int32 subSize, Common::SeekableReadStream &b) {
+}
+
+// Parse RA1 GAME chunks.
+void InsaneRebel1::handleGameChunk(int32 subSize, Common::SeekableReadStream &b) {
+ if (subSize < 8)
+ return;
+
+ uint32 opcode = b.readUint32BE();
+ uint32 param1 = b.readUint32BE();
+
+ switch (opcode) {
+ case 0x5E:
+ // RA1 dispatcher inline reset/init path (FUN_1BE1B case 0x5E).
+ // This is not a pure control-mode assignment.
+ _damageFlags = 0;
+ _prevDamageFlags = 0;
+ _damageCooldown = 0;
+ _deathTimer = 0;
+ _screenFlash = 0;
+ _gameLatch5D = 0;
+ _gameLatch5F = 0;
+ _driftParam = 0;
+ _rollAccum = 0;
+ _liftSmooth = 0;
+ _posAccumX = 0;
+ _posAccumY = 0;
+ memset(_inputHistoryX, 0, sizeof(_inputHistoryX));
+ memset(_inputHistoryY, 0, sizeof(_inputHistoryY));
+ memset(_viewHistoryX, 0, sizeof(_viewHistoryX));
+ memset(_viewHistoryY, 0, sizeof(_viewHistoryY));
+ _avgInputX = 0;
+ _avgInputY = 0;
+
+ // Field1 == 0 corresponds to baseline recenter behavior in the original.
+ if ((int32)param1 == 0) {
+ _shipPosX = kRA1CenterX;
+ _shipPosY = kRA1CenterY;
+ }
+
+ // Keep a conservative default mode after reset.
+ _flyControlMode = 0;
+ debug(5, "RA1 GAME 0x5E: reset state field1=%d", (int32)param1);
+ break;
+
+ case 0x5D:
+ _gameLatch5D = (uint16)param1;
+ debug(5, "RA1 GAME 0x5D (link/event latch) param=%u", _gameLatch5D);
+ break;
+
+ case 0x5F:
+ _gameLatch5F = (uint16)param1;
+ debug(5, "RA1 GAME 0x5F (random-hit latch) param=%u", _gameLatch5F);
+ break;
+
+ case 0x07:
+ // Per-frame corridor data: f1=frame counter, f2=max frames, f3=drift bias, f4=unused
+ // f1 is the original's _DAT_7740 (game frame counter)
+ // f3 is the drift/wind parameter combined with tuning table
+ _gameCounter = param1;
+ if (subSize >= 20) {
+ b.readUint32BE(); // f2 (max frames, unused in physics)
+ _driftParam = (int16)(int32)b.readUint32BE();
+ b.readUint32BE(); // f4 (unused in original assembly)
+ debug(7, "RA1 GAME 0x07: counter=%d driftParam=%d", _gameCounter, _driftParam);
+ }
+ break;
+
+ case 0x0D:
+ // Corridor boundaries: per-frame flight corridor
+ // Original params: left, top, WIDTH, HEIGHT (not right/bottom!)
+ // FUN_1C54D computes center = (left+width/2, top+height/2), transforms, then checks edges.
+ if (subSize >= 20) {
+ _corridorLeftX = (int16)param1;
+ _corridorTopY = (int16)b.readUint32BE();
+ int16 corridorWidth = (int16)b.readUint32BE();
+ int16 corridorHeight = (int16)b.readUint32BE();
+ _corridorRightX = _corridorLeftX + corridorWidth;
+ _corridorBottomY = _corridorTopY + corridorHeight;
+ debug(5, "RA1 GAME 0x0D: corridor left=%d top=%d right=%d bottom=%d (w=%d h=%d)",
+ _corridorLeftX, _corridorTopY, _corridorRightX, _corridorBottomY,
+ corridorWidth, corridorHeight);
+ }
+ break;
+
+ case 0x0E:
+ // Secondary collision zone (FUN_1C6E9): AABB test, sets damageFlags bit 4 (0x10)
+ // Original params: left, top, WIDTH, HEIGHT (same as 0x0D)
+ if (subSize >= 20) {
+ int16 zoneLeft = (int16)param1;
+ int16 zoneTop = (int16)b.readUint32BE();
+ int16 zoneWidth = (int16)b.readUint32BE();
+ int16 zoneHeight = (int16)b.readUint32BE();
+ int16 zoneRight = zoneLeft + zoneWidth;
+ int16 zoneBottom = zoneTop + zoneHeight;
+ if (_shipPosX > zoneLeft && _shipPosX < zoneRight &&
+ _shipPosY > zoneTop && _shipPosY < zoneBottom) {
+ _damageFlags |= 0x10;
+ }
+ debug(7, "RA1 GAME 0x0E: zone=[%d,%d]-[%d,%d] flags=0x%02x",
+ zoneLeft, zoneTop, zoneRight, zoneBottom, _damageFlags);
+ }
+ break;
+
+ case 0x0B:
+ // Asteroid/surface per-frame handler (FUN_1CDA7).
+ // field1 = frame counter, field2 = max frames
+ _gameCounter = param1;
+ if (subSize >= 20) {
+ b.readUint32BE(); // field2 (max frames)
+ b.readUint32BE(); // field3
+ b.readUint32BE(); // field4
+ }
+ debug(7, "RA1 GAME 0x0B: counter=%d", _gameCounter);
+ break;
+
+ case 0x08: case 0x09: case 0x0A:
+ case 0x19: case 0x1A:
+ if (subSize >= 20) {
+ uint32 param2 = b.readUint32BE();
+ uint32 param3 = b.readUint32BE();
+ uint32 param4 = b.readUint32BE();
+ debug(7, "RA1 GAME 0x%02x: params=(%d,%d,%d,%d)", opcode, param1, param2, param3, param4);
+ }
+ break;
+
+ default:
+ debug(7, "RA1 GAME unknown 0x%02x size=%d", opcode, subSize);
+ break;
+ }
+}
+
+} // End of namespace Scumm
diff --git a/engines/scumm/insane/insane_rebel1_levels.cpp b/engines/scumm/insane/insane_rebel1_levels.cpp
new file mode 100644
index 00000000000..88ccd5afc24
--- /dev/null
+++ b/engines/scumm/insane/insane_rebel1_levels.cpp
@@ -0,0 +1,205 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include "common/system.h"
+#include "common/endian.h"
+
+#include "scumm/scumm_v7.h"
+#include "scumm/file.h"
+#include "scumm/insane/insane_rebel1.h"
+
+namespace Scumm {
+
+static void decodeBomp(byte *dst, const byte *src, int width, int height, int pitch) {
+ while (height--) {
+ byte *dstNext = dst + pitch;
+ const byte *srcNext = src + 2 + READ_LE_UINT16(src);
+ src += 2;
+ int len = width;
+ byte *d = dst;
+ do {
+ int offs = READ_LE_UINT16(src); src += 2;
+ d += offs;
+ len -= offs;
+ if (len <= 0)
+ break;
+ int w = READ_LE_UINT16(src) + 1; src += 2;
+ len -= w;
+ if (len < 0)
+ w += len;
+ memcpy(d, src, w);
+ src += w;
+ d += w;
+ } while (len > 0);
+ dst = dstNext;
+ src = srcNext;
+ }
+}
+
+// Load an RA1 NUT sprite file (ANIM v1).
+// RA1 NUTs can have odd-size FOBJ chunks padded to 2-byte alignment within
+// FRME containers. This loader handles that padding properly, unlike the
+// shared NutRenderer::loadFont which assumes even-size chunks.
+bool InsaneRebel1::loadRA1Nut(const char *filename, RA1SpriteBank &bank) {
+ ScummFile *file = _vm->instantiateScummFile();
+ _vm->openFile(*file, filename);
+ if (!file->isOpen()) {
+ warning("InsaneRebel1::loadRA1Nut: can't open %s", filename);
+ delete file;
+ return false;
+ }
+
+ uint32 tag = file->readUint32BE();
+ if (tag != MKTAG('A','N','I','M')) {
+ warning("InsaneRebel1::loadRA1Nut: no ANIM tag in %s", filename);
+ delete file;
+ return false;
+ }
+ uint32 animSize = file->readUint32BE();
+ byte *data = (byte *)malloc(animSize);
+ file->read(data, animSize);
+ file->close();
+ delete file;
+
+ // data[0..3] = AHDR tag, data[4..7] = AHDR size
+ if (READ_BE_UINT32(data) != MKTAG('A','H','D','R')) {
+ warning("InsaneRebel1::loadRA1Nut: no AHDR in %s", filename);
+ free(data);
+ return false;
+ }
+
+ const uint16 expectedSprites = READ_LE_UINT16(data + 10);
+ bank.numSprites = expectedSprites;
+ bank.sprites = new RA1Sprite[bank.numSprites];
+ memset(bank.sprites, 0, sizeof(RA1Sprite) * bank.numSprites);
+
+ uint32 *fobjOffsets = (uint32 *)calloc(expectedSprites, sizeof(uint32));
+ if (!fobjOffsets) {
+ free(data);
+ return false;
+ }
+
+ // Pass 1: Parse ANIM chunks properly and collect FRME->FOBJ offsets in-order.
+ uint32 decodedSize = 0;
+ uint16 foundSprites = 0;
+ uint32 chunkOffset = 0;
+ while (chunkOffset + 8 <= animSize && foundSprites < expectedSprites) {
+ uint32 chunkTag = READ_BE_UINT32(data + chunkOffset);
+ uint32 chunkSize = READ_BE_UINT32(data + chunkOffset + 4);
+ uint32 chunkDataOffset = chunkOffset + 8;
+ uint32 chunkEnd = chunkDataOffset + chunkSize;
+ if (chunkEnd > animSize)
+ break;
+
+ if (chunkTag == MKTAG('F','R','M','E')) {
+ bool foundFobj = false;
+ uint32 subOffset = chunkDataOffset;
+ while (subOffset + 8 <= chunkEnd) {
+ uint32 subTag = READ_BE_UINT32(data + subOffset);
+ uint32 subSize = READ_BE_UINT32(data + subOffset + 4);
+ uint32 subDataOffset = subOffset + 8;
+ uint32 subEnd = subDataOffset + subSize;
+ if (subEnd > chunkEnd)
+ break;
+
+ if (subTag == MKTAG('F','O','B','J') && subOffset + 22 <= animSize) {
+ uint16 w = READ_LE_UINT16(data + subOffset + 14);
+ uint16 h = READ_LE_UINT16(data + subOffset + 16);
+ decodedSize += (uint32)w * (uint32)h;
+ fobjOffsets[foundSprites] = subOffset;
+ foundFobj = true;
+ break;
+ }
+
+ subOffset = subEnd;
+ if (subSize & 1)
+ subOffset++;
+ }
+ // Always increment for every FRME to preserve char-to-glyph alignment.
+ // Empty FRMEs (no FOBJ) keep fobjOffsets[i] = 0, decoded as blank sprites.
+ foundSprites++;
+ }
+
+ chunkOffset = chunkEnd;
+ if (chunkSize & 1)
+ chunkOffset++;
+ }
+
+ bank.decodedData = (byte *)calloc(decodedSize ? decodedSize : 1, 1);
+ bank.decodedSize = decodedSize;
+ byte *decPtr = bank.decodedData;
+
+ // Pass 2: Decode collected FOBJ entries.
+ for (uint16 i = 0; i < foundSprites; i++) {
+ uint32 fobjOffset = fobjOffsets[i];
+ if (fobjOffset == 0) {
+ // Empty FRME (no FOBJ) â leave sprite as blank (zeroed by memset).
+ continue;
+ }
+
+ int codec = READ_LE_UINT16(data + fobjOffset + 8);
+ bank.sprites[i].xoffs = READ_LE_INT16(data + fobjOffset + 10);
+ bank.sprites[i].yoffs = READ_LE_INT16(data + fobjOffset + 12);
+ bank.sprites[i].width = READ_LE_UINT16(data + fobjOffset + 14);
+ bank.sprites[i].height = READ_LE_UINT16(data + fobjOffset + 16);
+
+ int pixelCount = bank.sprites[i].width * bank.sprites[i].height;
+ const byte *fobjData = data + fobjOffset + 22;
+
+ if (codec == 21) {
+ bank.sprites[i].data = decPtr;
+ decodeBomp(decPtr, fobjData, bank.sprites[i].width,
+ bank.sprites[i].height, bank.sprites[i].width);
+ } else {
+ bank.sprites[i].width = 0;
+ bank.sprites[i].height = 0;
+ bank.sprites[i].data = nullptr;
+ warning("InsaneRebel1::loadRA1Nut: unsupported codec %d in sprite %d", codec, i);
+ }
+
+ decPtr += pixelCount;
+ }
+
+ free(fobjOffsets);
+
+ free(data);
+ debug(1, "InsaneRebel1::loadRA1Nut('%s'): expected=%d found=%d decoded=%d bytes",
+ filename, expectedSprites, foundSprites, decodedSize);
+ return true;
+}
+
+void InsaneRebel1::loadLevelSprites(int level) {
+ // Ship direction bank â not all levels have one (e.g. Level 2 is first-person)
+ Common::String bankFile = Common::String::format("LVL%d/L%dBANK1.NUT", level, level);
+ if (!loadRA1Nut(bankFile.c_str(), _shipBank)) {
+ debug(1, "InsaneRebel1: No BANK1 for level %d (first-person level)", level);
+ }
+ loadRA1Nut("SYS/DISPLAY.NUT", _displayBank);
+
+ // Explosion sprites â try BANG first, then EXPLD
+ Common::String bangFile = Common::String::format("LVL%d/L%dBANG.NUT", level, level);
+ if (!loadRA1Nut(bangFile.c_str(), _bangBank)) {
+ Common::String expldFile = Common::String::format("LVL%d/L%dEXPLD.NUT", level, level);
+ loadRA1Nut(expldFile.c_str(), _bangBank);
+ }
+}
+
+} // End of namespace Scumm
diff --git a/engines/scumm/insane/insane_rebel1_menu.cpp b/engines/scumm/insane/insane_rebel1_menu.cpp
new file mode 100644
index 00000000000..241a5fc0801
--- /dev/null
+++ b/engines/scumm/insane/insane_rebel1_menu.cpp
@@ -0,0 +1,267 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include "common/system.h"
+#include "common/events.h"
+
+#include "scumm/scumm_v7.h"
+#include "scumm/smush/smush_player.h"
+#include "scumm/insane/insane_rebel1.h"
+
+namespace Scumm {
+
+bool InsaneRebel1::notifyEvent(const Common::Event &event) {
+ if (_menuActive && _optionsActive && event.type == Common::EVENT_KEYDOWN) {
+ switch (event.kbd.keycode) {
+ case Common::KEYCODE_UP:
+ case Common::KEYCODE_w:
+ _optionsSel = (_optionsSel + 2) % 3;
+ return true;
+ case Common::KEYCODE_DOWN:
+ case Common::KEYCODE_s:
+ _optionsSel = (_optionsSel + 1) % 3;
+ return true;
+ case Common::KEYCODE_RETURN:
+ case Common::KEYCODE_KP_ENTER:
+ case Common::KEYCODE_SPACE:
+ _menuConfirmed = true;
+ _vm->_smushVideoShouldFinish = true;
+ return true;
+ case Common::KEYCODE_ESCAPE:
+ _optionsSel = 2; // Back
+ _menuConfirmed = true;
+ _vm->_smushVideoShouldFinish = true;
+ return true;
+ default:
+ break;
+ }
+ }
+
+ if (_menuActive && !_optionsActive && event.type == Common::EVENT_KEYDOWN) {
+ switch (event.kbd.keycode) {
+ case Common::KEYCODE_UP:
+ case Common::KEYCODE_w:
+ _menuSelection = (_menuSelection + 4) % 5;
+ return true;
+ case Common::KEYCODE_DOWN:
+ case Common::KEYCODE_s:
+ _menuSelection = (_menuSelection + 1) % 5;
+ return true;
+ case Common::KEYCODE_RETURN:
+ case Common::KEYCODE_KP_ENTER:
+ case Common::KEYCODE_SPACE:
+ _menuConfirmed = true;
+ _vm->_smushVideoShouldFinish = true;
+ return true;
+ case Common::KEYCODE_1:
+ case Common::KEYCODE_2:
+ case Common::KEYCODE_3:
+ case Common::KEYCODE_4:
+ case Common::KEYCODE_5:
+ _menuSelection = event.kbd.keycode - Common::KEYCODE_1;
+ _menuConfirmed = true;
+ _vm->_smushVideoShouldFinish = true;
+ return true;
+ case Common::KEYCODE_ESCAPE:
+ _menuSelection = 4;
+ _menuConfirmed = true;
+ _vm->_smushVideoShouldFinish = true;
+ return true;
+ default:
+ break;
+ }
+ }
+
+ if (event.type == Common::EVENT_KEYDOWN && event.kbd.keycode == Common::KEYCODE_ESCAPE) {
+ if (_player) {
+ debug("Rebel1: ESC pressed - skipping video");
+ _vm->_smushVideoShouldFinish = true;
+ return true;
+ }
+ }
+
+ return false;
+}
+
+void InsaneRebel1::renderMainMenuOverlay(byte *dst, int pitch, int width, int height) {
+ _menuFrameCounter++;
+
+ if (_optionsActive) {
+ // --- Options submenu ---
+ static const char *kDiffNames[3] = { "EASY", "NORMAL", "HARD" };
+
+ const int titleW = getFontBankStringWidth("GAME OPTIONS");
+ drawFontBankString(dst, pitch, width, height, (width - titleW) / 2, 36, "GAME OPTIONS");
+
+ // Build dynamic option strings
+ char diffLine[64];
+ snprintf(diffLine, sizeof(diffLine), "DIFFICULTY: %s", kDiffNames[CLIP(_difficulty, 0, 2)]);
+ const char *turbLine = _turbulenceEnabled ? "TURBULENCE: ON" : "TURBULENCE: OFF";
+ const char *kOptionsItems[3] = { diffLine, turbLine, "BACK" };
+
+ const int menuY = 60;
+ const int rowH = 16;
+
+ for (int i = 0; i < 3; i++) {
+ const int textW = getFontBankStringWidth(kOptionsItems[i]);
+ const int textX = (width - textW) / 2;
+ const int y = menuY + i * rowH;
+ drawFontBankString(dst, pitch, width, height, textX, y + 1, kOptionsItems[i]);
+
+ if (i == _optionsSel) {
+ byte highlightColor = ((_menuFrameCounter / 8) & 1) ? 248 : 240;
+ int bracketWidth = textW + 12;
+ int leftX = CLIP(textX - 6, 0, width - 1);
+ int rightX = CLIP(leftX + bracketWidth, 0, width - 1);
+ int topY = CLIP(y - 1, 0, height - 1);
+ int bottomY = CLIP(y + rowH - 2, 0, height - 1);
+ for (int x = leftX; x <= rightX; x++) {
+ dst[topY * pitch + x] = highlightColor;
+ dst[bottomY * pitch + x] = highlightColor;
+ }
+ for (int py = topY; py <= bottomY; py++) {
+ dst[py * pitch + leftX] = highlightColor;
+ dst[py * pitch + rightX] = highlightColor;
+ }
+ }
+ }
+ return;
+ }
+
+ // --- Main menu ---
+ static const char *kMenuItems[5] = {
+ "START NEW GAME",
+ "GAME OPTIONS",
+ "ENTER PASSCODE",
+ "CONTINUE DEMO",
+ "EXIT TO DOS"
+ };
+
+ // Center title
+ const int titleW = getFontBankStringWidth("MAIN MENU");
+ const int titleX = (width - titleW) / 2;
+ drawFontBankString(dst, pitch, width, height, titleX, 36, "MAIN MENU");
+
+ // Draw menu items centered horizontally
+ const int menuY = 60;
+ const int rowH = 16;
+
+ for (int i = 0; i < 5; i++) {
+ const int textW = getFontBankStringWidth(kMenuItems[i]);
+ const int textX = (width - textW) / 2;
+ const int y = menuY + i * rowH;
+
+ drawFontBankString(dst, pitch, width, height, textX, y + 1, kMenuItems[i]);
+
+ // Selection highlight box â flashing border (FUN_004292d0 pattern from RA2)
+ if (i == _menuSelection) {
+ // Flash between two palette colors every 8 frames
+ byte highlightColor = ((_menuFrameCounter / 8) & 1) ? 248 : 240;
+
+ int bracketWidth = textW + 12;
+ int bracketHeight = rowH;
+ int leftX = textX - 6;
+ int rightX = leftX + bracketWidth;
+ int topY = y - 1;
+ int bottomY = y + bracketHeight - 2;
+
+ // Clamp
+ if (leftX < 0) leftX = 0;
+ if (rightX >= width) rightX = width - 1;
+ if (topY < 0) topY = 0;
+ if (bottomY >= height) bottomY = height - 1;
+
+ // Draw rectangle border (4 lines)
+ for (int x = leftX; x <= rightX && x < width; x++) {
+ if (topY >= 0 && topY < height)
+ dst[topY * pitch + x] = highlightColor;
+ if (bottomY >= 0 && bottomY < height)
+ dst[bottomY * pitch + x] = highlightColor;
+ }
+ for (int py = topY; py <= bottomY && py < height; py++) {
+ if (leftX >= 0 && leftX < width)
+ dst[py * pitch + leftX] = highlightColor;
+ if (rightX >= 0 && rightX < width)
+ dst[py * pitch + rightX] = highlightColor;
+ }
+ }
+ }
+}
+
+int InsaneRebel1::runMainMenu() {
+ debug(1, "InsaneRebel1: Main menu");
+
+ _menuSelection = 0;
+ while (!_vm->shouldQuit()) {
+ _menuActive = true;
+ _menuConfirmed = false;
+ _menuFrameCounter = 0;
+ clearVideoBuffer();
+ playCinematic("OPEN/O1OPTION.ANM");
+ _menuActive = false;
+
+ if (_vm->shouldQuit())
+ return 5;
+
+ if (_menuConfirmed)
+ return _menuSelection + 1;
+ }
+
+ return 5;
+}
+
+void InsaneRebel1::runOptionsMenu() {
+ _optionsSel = 0;
+ _optionsActive = true;
+
+ while (!_vm->shouldQuit()) {
+ _menuActive = true;
+ _menuConfirmed = false;
+ _menuFrameCounter = 0;
+ clearVideoBuffer();
+ playCinematic("OPEN/O1OPTION.ANM");
+ _menuActive = false;
+
+ if (_vm->shouldQuit())
+ break;
+
+ if (_menuConfirmed) {
+ switch (_optionsSel) {
+ case 0:
+ // Cycle difficulty
+ _difficulty = (_difficulty + 1) % 3;
+ loadTuningForLevel(0);
+ break;
+ case 1:
+ // Toggle turbulence
+ _turbulenceEnabled = !_turbulenceEnabled;
+ break;
+ case 2:
+ // Back to main menu
+ _optionsActive = false;
+ return;
+ }
+ }
+ }
+ _optionsActive = false;
+}
+
+} // End of namespace Scumm
diff --git a/engines/scumm/insane/insane_rebel1_render.cpp b/engines/scumm/insane/insane_rebel1_render.cpp
new file mode 100644
index 00000000000..5e71af01770
--- /dev/null
+++ b/engines/scumm/insane/insane_rebel1_render.cpp
@@ -0,0 +1,406 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include "common/system.h"
+
+#include "scumm/scumm_v7.h"
+#include "scumm/insane/insane_rebel1.h"
+
+namespace Scumm {
+
+void InsaneRebel1::procPreRendering(byte *renderBitmap) {
+}
+
+void InsaneRebel1::procPostRendering(byte *renderBitmap, int32 codecparam, int32 setupsan12,
+ int32 setupsan13, int32 curFrame, int32 maxFrame) {
+ if (_menuActive && renderBitmap) {
+ int width = _player ? _player->_width : 0;
+ int height = _player ? _player->_height : 0;
+ if (width == 0) width = _screenWidth;
+ if (height == 0) height = _screenHeight;
+ int pitch = width;
+ renderMainMenuOverlay(renderBitmap, pitch, width, height);
+ }
+
+ if (!_interactiveVideoActive || !renderBitmap)
+ return;
+
+ int width = _player->_width;
+ int height = _player->_height;
+ if (width == 0) width = _screenWidth;
+ if (height == 0) height = _screenHeight;
+ int pitch = width;
+
+ if (_currentLevel == 1) {
+ // Level 2: first-person asteroid dodge â no ship sprite, input averaging physics
+ updateAsteroidPhysics();
+ } else {
+ // Level 1 (and others): third-person ship flight with accumulators
+ if (_shipBank.numSprites > 0) {
+ updateShipPhysics();
+ renderShip(renderBitmap, pitch, width, height);
+ }
+ }
+ renderExplosions(renderBitmap, pitch, width, height);
+ renderCrosshair(renderBitmap, pitch, width, height);
+ renderHUD(renderBitmap, pitch, width, height);
+}
+
+void InsaneRebel1::renderCrosshair(byte *dst, int pitch, int width, int height) {
+ int cx = _vm->_mouse.x;
+ int cy = _vm->_mouse.y;
+
+ // Palette index 119 = (255,0,0) pure red in L2PLAY.ANM palette.
+ // Palette index 15 = (255,255,255) white, used as outline for visibility.
+ const byte colorInner = 119;
+ const byte colorOutline = 15;
+ const int size = 7; // arm length
+
+ // Helper lambda to draw a pixel with outline
+ auto drawPx = [&](int x, int y, byte c) {
+ if (x >= 0 && x < width && y >= 0 && y < height)
+ dst[y * pitch + x] = c;
+ };
+
+ // Draw outline first (1px border around each arm pixel)
+ for (int d = -size; d <= size; d++) {
+ if (d >= -1 && d <= 1) continue; // skip center area for outline
+ // Horizontal arm outline
+ drawPx(cx + d, cy - 1, colorOutline);
+ drawPx(cx + d, cy + 1, colorOutline);
+ // Vertical arm outline
+ drawPx(cx - 1, cy + d, colorOutline);
+ drawPx(cx + 1, cy + d, colorOutline);
+ }
+ // Arm endpoints
+ drawPx(cx - size - 1, cy, colorOutline);
+ drawPx(cx + size + 1, cy, colorOutline);
+ drawPx(cx, cy - size - 1, colorOutline);
+ drawPx(cx, cy + size + 1, colorOutline);
+
+ // Draw red cross arms
+ for (int d = -size; d <= size; d++) {
+ if (d == 0) continue; // gap at center
+ drawPx(cx + d, cy, colorInner); // horizontal
+ drawPx(cx, cy + d, colorInner); // vertical
+ }
+ // Center dot
+ drawPx(cx, cy, colorInner);
+}
+
+void InsaneRebel1::drawFontBankString(byte *dst, int pitch, int width, int height, int x, int y, const char *text) {
+ if (!dst || !text || _hudFontBank.numSprites <= 0)
+ return;
+
+ for (int i = 0; text[i] != '\0'; i++) {
+ const byte ch = (byte)text[i];
+
+ if (ch == ' ') {
+ x += 6;
+ continue;
+ }
+
+ // RA1 font renderer indexes printable characters from '!' (0x21), not raw ASCII.
+ if (ch < 0x21) {
+ x += 4;
+ continue;
+ }
+ const int fontIdx = (int)ch - 0x21;
+ if (fontIdx < 0 || fontIdx >= _hudFontBank.numSprites) {
+ x += 4;
+ continue;
+ }
+
+ const RA1Sprite &glyph = _hudFontBank.sprites[fontIdx];
+ const int gw = glyph.width;
+ const int gh = glyph.height;
+ const int gx = x + glyph.xoffs;
+ const int gy = y + glyph.yoffs;
+ const uint64 glyphPixels = (uint64)gw * (uint64)gh;
+ if (!glyph.data || gw <= 0 || gh <= 0 || glyphPixels == 0 || glyphPixels > 0x10000) {
+ x += 4;
+ continue;
+ }
+ if (!(_hudFontBank.decodedData && _hudFontBank.decodedSize > 0)) {
+ x += 4;
+ continue;
+ }
+ const byte *bankStart = _hudFontBank.decodedData;
+ const byte *bankEnd = _hudFontBank.decodedData + _hudFontBank.decodedSize;
+ if (glyph.data < bankStart || glyph.data >= bankEnd || glyph.data + glyphPixels > bankEnd) {
+ x += 4;
+ continue;
+ }
+
+ for (int py = 0; py < gh; py++) {
+ const int sy = gy + py;
+ if (sy < 0 || sy >= height)
+ continue;
+ for (int px = 0; px < gw; px++) {
+ const int sx = gx + px;
+ if (sx < 0 || sx >= width)
+ continue;
+ const byte pixel = glyph.data[py * gw + px];
+ if (pixel != 0)
+ dst[sy * pitch + sx] = pixel;
+ }
+ }
+
+ x += gw > 0 ? gw : 4;
+ }
+}
+
+// getFontBankStringWidth -- Measure pixel width of a string using the HUD font bank.
+// Matches the pre-pass width calculation in the original drawString (FUN_221B7).
+int InsaneRebel1::getFontBankStringWidth(const char *text) {
+ if (!text || _hudFontBank.numSprites <= 0)
+ return 0;
+
+ int w = 0;
+ for (int i = 0; text[i] != '\0'; i++) {
+ const byte ch = (byte)text[i];
+ if (ch == ' ') {
+ w += 6;
+ continue;
+ }
+ if (ch < 0x21) {
+ w += 4;
+ continue;
+ }
+ const int fontIdx = (int)ch - 0x21;
+ if (fontIdx < 0 || fontIdx >= _hudFontBank.numSprites) {
+ w += 4;
+ continue;
+ }
+ const RA1Sprite &glyph = _hudFontBank.sprites[fontIdx];
+ w += glyph.width > 0 ? glyph.width : 4;
+ }
+ return w;
+}
+
+void InsaneRebel1::renderShip(byte *dst, int pitch, int width, int height) {
+ // From FUN_1DEB5 LAB_1e2b2: ship drawn when health >= 0 OR deathTimer > 20
+ // Hidden during last 20 frames of death sequence (deathTimer 20â0)
+ if (_health < 0 && _deathTimer <= 20)
+ return;
+
+ if (_shipDirIndex < 0 || _shipDirIndex >= _shipBank.numSprites)
+ return;
+
+ const RA1Sprite &spr = _shipBank.sprites[_shipDirIndex];
+
+ // Position: game coords â screen coords via perspective transform
+ // Adapted from RA2's renderHandler7Ship:
+ // shipCenterX = (shipX - center) + perspX + screenCenterX
+ int drawX = (_shipPosX - kRA1CenterX) + _perspectiveX + kRA1CenterX - spr.width / 2;
+ int drawY = (_shipPosY - kRA1CenterY) + _perspectiveY + kRA1CenterY - spr.height / 2;
+
+ renderSprite(dst, pitch, width, height, drawX, drawY, spr);
+}
+
+// Render explosion sprites during damage cooldown and death sequence.
+// From FUN_1DEB5 at LAB_1e185 (damage hit) and LAB_1e0e3 (death shake).
+void InsaneRebel1::renderExplosions(byte *dst, int pitch, int width, int height) {
+ if (_bangBank.numSprites <= 0)
+ return;
+
+ // Ship screen center position (matches assembly: DAT_74b6+DAT_74ba, DAT_74b8+DAT_74bc)
+ int shipScreenX = (_shipPosX - kRA1CenterX) + _perspectiveX + kRA1CenterX;
+ int shipScreenY = (_shipPosY - kRA1CenterY) + _perspectiveY + kRA1CenterY;
+
+ // --- Death shake explosions (FUN_1DEB5 LAB_1e0e3) ---
+ // When dead and deathTimer > 10: random explosion sprites scatter around ship
+ if (_health < 0 && _deathTimer > 10) {
+ int intensity = _deathTimer - 10; // 20â1 as timer goes 30â11
+ if (intensity > 10)
+ intensity = 20 - intensity; // Triangle: 0â10â0
+
+ // di = intensity * 4 + 1 (vertical scatter range)
+ // si = -20 + intensity * 4 (horizontal scatter range, DAT_75d8 is 0)
+ int rangeY = intensity * 4 + 1;
+ int rangeX = -20 + intensity * 4;
+ if (rangeX < 1) rangeX = 1;
+
+ for (int i = 0; i < intensity; i++) {
+ // Random sprite from bang bank (FUN_21db0(10))
+ int sprIdx = _vm->_rnd.getRandomNumber(_bangBank.numSprites - 1);
+
+ // Random position around ship (matching assembly random scatter)
+ int randX = (int)_vm->_rnd.getRandomNumber(rangeX * 2) - rangeX;
+ int randY = (int)_vm->_rnd.getRandomNumber(rangeY * 2) - rangeY;
+
+ int drawX = shipScreenX + randX;
+ int drawY = shipScreenY + randY;
+
+ const RA1Sprite &spr = _bangBank.sprites[sprIdx];
+ renderSprite(dst, pitch, width, height,
+ drawX - spr.width / 2, drawY - spr.height / 2, spr);
+ }
+ return;
+ }
+
+ // --- Damage hit explosion (FUN_1DEB5 LAB_1e185) ---
+ // When alive, in cooldown, and bang bank loaded
+ if (_health >= 0 && _damageCooldown > 0) {
+ // Sprite index = 10 - damageCooldown (frames 0â9 as cooldown 10â1)
+ int sprIdx = _bangBank.numSprites - _damageCooldown;
+ if (sprIdx < 0 || sprIdx >= _bangBank.numSprites)
+ return;
+
+ // Position at ship center (DAT_75d8 is always 0 in RA1)
+ int drawX = shipScreenX;
+ int drawY = shipScreenY;
+
+ const RA1Sprite &spr = _bangBank.sprites[sprIdx];
+ renderSprite(dst, pitch, width, height,
+ drawX - spr.width / 2, drawY - spr.height / 2, spr);
+ }
+}
+
+// Render bottom status bar from DISPLAY.NUT with dynamic damage bar and score.
+// Original layout (320-wide): DAMAGE [green bar] | PILOTS [3 icons] | SCORE [number]
+void InsaneRebel1::renderHUD(byte *dst, int pitch, int width, int height) {
+ if (_displayBank.numSprites == 0)
+ return;
+
+ // Extra life bonus: every 10,000 points (FUN_1BBCB lines 11-27)
+ if (_score / 10000 > _prevScore / 10000) {
+ _lives++;
+ }
+ _prevScore = _score;
+
+ const RA1Sprite &bar = _displayBank.sprites[0];
+
+ // DISPLAY.NUT sprite is 320Ã19 at xoffs=0, yoffs=176 in the original game.
+ // Video FOBJs fill the full 384Ã242 buffer from (0,0), so use sprite offsets directly.
+ int hudX = bar.xoffs;
+ int hudY = bar.yoffs;
+
+ // Draw the status bar background with transparency (pixel 0 = transparent)
+ if (bar.data && bar.width > 0 && bar.height > 0) {
+ int drawX = hudX, drawY = hudY, drawW = bar.width, drawH = bar.height;
+ int srcOffX = 0, srcOffY = 0;
+ if (drawX < 0) { srcOffX = -drawX; drawW += drawX; drawX = 0; }
+ if (drawY < 0) { srcOffY = -drawY; drawH += drawY; drawY = 0; }
+ if (drawX + drawW > width) drawW = width - drawX;
+ if (drawY + drawH > height) drawH = height - drawY;
+
+ for (int iy = 0; iy < drawH; iy++) {
+ const byte *s = bar.data + (srcOffY + iy) * bar.width + srcOffX;
+ byte *d = dst + (drawY + iy) * pitch + drawX;
+ for (int ix = 0; ix < drawW; ix++) {
+ byte px = s[ix];
+ if (px != 0)
+ d[ix] = px;
+ }
+ }
+
+ debug(5, "RA1 HUD: drawn at (%d,%d) size=%dx%d",
+ hudX, hudY, bar.width, bar.height);
+ }
+
+ // Draw health bar from FUN_1BBCB behavior.
+ // Original logic uses current health as fill width and computes x as (0x92 - health),
+ // so the bar is right-anchored and shrinks from left to right as damage increases.
+ {
+ int barMaxW = kMaxHealth;
+ int barH = 5;
+ int healthWidth = CLIP<int16>(_health, 0, kMaxHealth);
+ int barX = hudX + (0x92 - healthWidth);
+ int barY = hudY + 8;
+ int fillW = CLIP(healthWidth, 0, barMaxW);
+
+ // Color based on damage level (matching original thresholds from FUN_1BBCB)
+ // Palette indices: 0xD0-0xD7 = greens, 0x60-0x67 = yellows, 0xD8-0xDF = reds
+ byte barColor;
+ if (_health > _tuning.shot * 2)
+ barColor = 0xD5; // Green (0,192,0) â low damage
+ else if (_health > _tuning.wham * 2)
+ barColor = 0x63; // Yellow (255,255,31) â moderate damage
+ else
+ barColor = 0xDD; // Red (192,0,0) â critical
+
+ // Flash effect on damage
+ if (_screenFlash > 0)
+ barColor = 0xFF; // White flash
+
+ for (int iy = 0; iy < barH && barY + iy < height; iy++) {
+ byte *d = dst + (barY + iy) * pitch + barX;
+ for (int ix = 0; ix < fillW && barX + ix < width; ix++) {
+ d[ix] = barColor;
+ }
+ }
+ }
+
+ // Lives: black out excess pilot icons embedded in DISPLAY.NUT background.
+ // Original FUN_1BBCB: FUN_21D66(buf, lives*10+186, 6, 51-lives*10, 9, 0, 320)
+ // Icons are 5 slots at x=186..236, each ~10px wide. Cover unused slots with black.
+ if (_lives >= 0 && _lives < 5) {
+ int coverX = hudX + _lives * 10 + 186;
+ int coverY = hudY + 6;
+ int coverW = 51 - _lives * 10;
+ int coverH = 9;
+ if (coverX >= 0 && coverY >= 0) {
+ for (int iy = 0; iy < coverH && coverY + iy < height; iy++) {
+ byte *d = dst + (coverY + iy) * pitch + coverX;
+ for (int ix = 0; ix < coverW && coverX + ix < width; ix++) {
+ d[ix] = 0x00;
+ }
+ }
+ }
+ }
+
+ // Score: 6-digit zero-padded at x=273, y=5 (original format '<<%%06ld' at 0x111,5)
+ if (_hudFontBank.numSprites > 0) {
+ char scoreStr[16];
+ Common::sprintf_s(scoreStr, "%06d", MAX<int>(_score, 0));
+ drawFontBankString(dst, pitch, width, height, hudX + 273, hudY + 5, scoreStr);
+ }
+
+}
+
+void InsaneRebel1::renderSprite(byte *dst, int pitch, int width, int height,
+ int x, int y, const RA1Sprite &spr) {
+ if (!spr.data || spr.width <= 0 || spr.height <= 0)
+ return;
+
+ int drawX = x, drawY = y, drawW = spr.width, drawH = spr.height;
+ int srcOffsetX = 0, srcOffsetY = 0;
+
+ if (drawX < 0) { srcOffsetX = -drawX; drawW += drawX; drawX = 0; }
+ if (drawY < 0) { srcOffsetY = -drawY; drawH += drawY; drawY = 0; }
+ if (drawX + drawW > width) drawW = width - drawX;
+ if (drawY + drawH > height) drawH = height - drawY;
+ if (drawW <= 0 || drawH <= 0)
+ return;
+
+ for (int iy = 0; iy < drawH; iy++) {
+ const byte *s = spr.data + (srcOffsetY + iy) * spr.width + srcOffsetX;
+ byte *d = dst + (drawY + iy) * pitch + drawX;
+ for (int ix = 0; ix < drawW; ix++) {
+ byte px = s[ix];
+ if (px != 0)
+ d[ix] = px;
+ }
+ }
+}
+
+} // End of namespace Scumm
diff --git a/engines/scumm/insane/insane_rebel1_runlevels.cpp b/engines/scumm/insane/insane_rebel1_runlevels.cpp
new file mode 100644
index 00000000000..a150cd2bddb
--- /dev/null
+++ b/engines/scumm/insane/insane_rebel1_runlevels.cpp
@@ -0,0 +1,342 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include "common/system.h"
+
+#include "graphics/cursorman.h"
+#include "graphics/wincursor.h"
+
+#include "scumm/scumm_v7.h"
+#include "scumm/smush/smush_player.h"
+#include "scumm/insane/insane_rebel1.h"
+
+namespace Scumm {
+
+// ---------------------------------------------------------------------------
+// Game flow (matching original at 0x15597)
+// ---------------------------------------------------------------------------
+
+// Play a passive cinematic (no game callback, skippable).
+// Reuses RA2's pattern: reset handler, set cinematic flags, play video.
+// startFrame > 0: fast-forward (decode without display/audio) to that frame.
+void InsaneRebel1::playCinematic(const char *filename, int32 startFrame) {
+ debug(1, "InsaneRebel1::playCinematic('%s', startFrame=%d)", filename, startFrame);
+ SmushPlayer *splayer = _vm->_splayer;
+ _player = splayer;
+ _interactiveVideoActive = false;
+ _vm->_smushVideoShouldFinish = false;
+ splayer->setCurVideoFlags(0x28); // Cinematic mode + buffer preserve
+ if (startFrame > 0)
+ splayer->setFastForwardToFrame(startFrame);
+ splayer->play(filename, 12);
+}
+
+void InsaneRebel1::clearVideoBuffer() {
+ if (_vm->_screenWidth <= 0 || _vm->_screenHeight <= 0)
+ return;
+
+ const int pixelCount = _vm->_screenWidth * _vm->_screenHeight;
+ byte *clearBuffer = (byte *)calloc(pixelCount, 1);
+ if (!clearBuffer)
+ return;
+
+ if (_vm->_macScreen) {
+ _vm->mac_drawBufferToScreen(clearBuffer, _vm->_screenWidth, 0, 0, _vm->_screenWidth, _vm->_screenHeight);
+ } else {
+ _vm->_system->copyRectToScreen(clearBuffer, _vm->_screenWidth, 0, 0, _vm->_screenWidth, _vm->_screenHeight);
+ }
+ _vm->_system->updateScreen();
+
+ free(clearBuffer);
+}
+
+// Intro sequence (0x155ef-0x158f8):
+// 1. O1LOGO.ANM â LucasArts logo
+// 2. O1OPEN.ANM â Star Wars opening crawl
+void InsaneRebel1::playIntroSequence() {
+ debug(1, "InsaneRebel1: Playing intro sequence");
+
+ // LucasArts logo (original: PUSH 0x57cc, CALL FUN_1BA32 with flags 0x0420)
+ playCinematic("OPEN/O1LOGO.ANM");
+ if (_vm->shouldQuit())
+ return;
+ clearVideoBuffer();
+
+ // Star Wars opening crawl (original: PUSH 0x5800, CALL FUN_1BA32)
+ playCinematic("OPEN/O1OPEN.ANM");
+}
+
+// Main menu on O1OPTION.ANM background (0x15968).
+// Original renders text overlay with 5 menu items via FUN_21F7A.
+// For now, we play the menu video as a passive cinematic (non-interactive)
+// and return "Start New Game" immediately.
+
+// Level 1 flow (0x16100-0x167A2, from disassembly):
+// 1. Load NUTs (L1BANK1, L1BANK2, L1EXPLD, L1BANG, L1LASER)
+// 2. L1HANGAR.ANM â Full hangar departure cutscene (782 frames, flags 0x0420)
+// 3. L1CU1.ANM â Pre-flight cutscene (flags 0x0400)
+// 4. L1PLAY1L.ANM â Stage 1 flight, hard/left path (788 frames)
+// At frame 394, if player steers right â L1PLAY1R (easy path, 396 frames)
+// 5. L1CU2.ANM â Mid-level cutscene
+// 6. L1PLAY2.ANM â Stage 2 turret
+// If score < 5 (0x75D0): L1RETRY â retry Stage 2
+// 7. L1END.ANM â Level complete
+// Death (health<0): L1CRASHA/B â lives check:
+// lives>0: L1NEW â jump back to Stage 1 (skip L1HANGAR/L1CU1)
+// lives==0: L1DEATH â return to menu
+
+bool InsaneRebel1::runLevel1() {
+ debug(1, "InsaneRebel1: Running level 1");
+
+ // Load level sprites (original: pushes L1BANK1..L1BANG NUT filenames)
+ loadLevelSprites(1);
+
+ // L1HANGAR.ANM â Hangar departure (original: 0x5918, flags 0x0420)
+ // Plays once at level start, never replayed on retry.
+ playCinematic("LVL1/L1HANGAR.ANM");
+ if (_vm->shouldQuit())
+ return false;
+
+ // L1CU1.ANM â Pre-flight cutscene (original: 0x5944, flags 0x0400)
+ // Plays once at level start, never replayed on retry.
+ playCinematic("LVL1/L1CU1.ANM");
+ if (_vm->shouldQuit())
+ return false;
+
+ // Retry loop â on death with lives, L1NEW plays then jumps back here
+ while (!_vm->shouldQuit()) {
+ // Reset health for this attempt (original: MOV WORD [0x7560], 98 at 0x16214)
+ _health = kMaxHealth;
+ _damageFlags = 0;
+ _prevDamageFlags = 0;
+ _damageCooldown = 0;
+ _deathTimer = 0;
+ _screenFlash = 0;
+ _frameCounter = 0;
+ _gameCounter = 0;
+ _pathBranchEnabled = true;
+ _rightPathSelected = false;
+
+ // Stage 1 flight â L1PLAY1L (hard/left path)
+ // The first 394 frames are the common section. At counter 394, if
+ // ship is right of center, we switch to L1PLAY1R (easy path).
+ playInteractiveVideo("LVL1/L1PLAY1L.ANM");
+ if (_vm->shouldQuit())
+ return false;
+
+ if (_rightPathSelected && _health >= 0) {
+ debug(1, "InsaneRebel1: Switching to right path (L1PLAY1R)");
+ _pathBranchEnabled = false;
+ playInteractiveVideo("LVL1/L1PLAY1R.ANM");
+ if (_vm->shouldQuit())
+ return false;
+ }
+ _pathBranchEnabled = false;
+
+ if (_health >= 0) {
+ // L1CU2.ANM â Mid-level cutscene (original: 0x5977)
+ playCinematic("LVL1/L1CU2.ANM");
+ if (_vm->shouldQuit())
+ return false;
+
+ // L1PLAY2.ANM â Stage 2 turret (original: 0x5986)
+ playInteractiveVideo("LVL1/L1PLAY2.ANM");
+ if (_vm->shouldQuit())
+ return false;
+
+ // TODO: Check score threshold (original: CMP WORD [0x75D0], 5)
+ // If score < 5: L1RETRY â retry Stage 2
+
+ // L1END.ANM â Level complete! (original: 0x59a3)
+ playCinematic("LVL1/L1END.ANM");
+ return true;
+ }
+
+ // Death sequence (original: 0x165dd-0x166bb)
+ // Random crash variant A or B
+ if (_vm->_rnd.getRandomNumber(1) == 0)
+ playCinematic("LVL1/L1CRASHA.ANM");
+ else
+ playCinematic("LVL1/L1CRASHB.ANM");
+ if (_vm->shouldQuit())
+ return false;
+
+ // Check lives (original: CMP WORD [0x7562], 0 at 0x1666B)
+ _lives--;
+ if (_lives <= 0) {
+ // Game over â L1DEATH then return (original: 0x166C0)
+ playCinematic("LVL1/L1DEATH.ANM");
+ debug(1, "InsaneRebel1: Game over (no lives left)");
+ return false;
+ }
+
+ // Lives remaining â L1NEW briefing then retry (original: 0x16675)
+ playCinematic("LVL1/L1NEW.ANM");
+ if (_vm->shouldQuit())
+ return false;
+
+ // Loop back to gameplay (original: JMP 0x16214 â health reset + Stage 1)
+ }
+
+ return false;
+}
+
+// Level 2: Asteroid Field Training
+// Flow: L2NEW â L2INTRO â L2PLAY (interactive) â L2END/L2DEATH
+bool InsaneRebel1::runLevel2() {
+ debug(1, "InsaneRebel1: Running level 2");
+
+ _currentLevel = 1;
+ loadLevelSprites(2);
+ loadTuningForLevel(1);
+
+ // L2INTRO.ANM â intro cutscene (481 frames)
+ playCinematic("LVL2/L2INTRO.ANM");
+ if (_vm->shouldQuit())
+ return false;
+
+ // Retry loop
+ while (!_vm->shouldQuit()) {
+ // Reset state for this attempt
+ _health = kMaxHealth;
+ _damageFlags = 0;
+ _prevDamageFlags = 0;
+ _damageCooldown = 0;
+ _deathTimer = 0;
+ _screenFlash = 0;
+ _frameCounter = 0;
+ _gameCounter = 0;
+ memset(_inputHistoryX, 0, sizeof(_inputHistoryX));
+ memset(_inputHistoryY, 0, sizeof(_inputHistoryY));
+ memset(_viewHistoryX, 0, sizeof(_viewHistoryX));
+ memset(_viewHistoryY, 0, sizeof(_viewHistoryY));
+ _avgInputX = 0;
+ _avgInputY = 0;
+
+ // L2PLAY.ANM â asteroid dodge (800 frames, interactive)
+ playInteractiveVideo("LVL2/L2PLAY.ANM");
+ if (_vm->shouldQuit())
+ return false;
+
+ if (_health >= 0) {
+ // Level complete!
+ playCinematic("LVL2/L2END.ANM");
+ return true;
+ }
+
+ // Death
+ playCinematic("LVL2/L2DEATH.ANM");
+ if (_vm->shouldQuit())
+ return false;
+
+ _lives--;
+ if (_lives <= 0) {
+ debug(1, "InsaneRebel1: Game over (no lives left)");
+ return false;
+ }
+
+ // Retry briefing
+ playCinematic("LVL2/L2NEW.ANM");
+ if (_vm->shouldQuit())
+ return false;
+ }
+
+ return false;
+}
+
+// Main game entry point â called from ScummEngine::go().
+// Matches original flow at 0x15597: intro â menu â level.
+void InsaneRebel1::runGame() {
+ // Play intro sequence (logo + opening)
+ playIntroSequence();
+ if (_vm->shouldQuit())
+ return;
+
+ // Main menu â gameplay loop
+ while (!_vm->shouldQuit()) {
+ int menuResult = runMainMenu();
+ if (_vm->shouldQuit())
+ return;
+
+ switch (menuResult) {
+ case 1: {
+#if 0 // Skip level 1 for testing â jump straight to level 2
+ // Start New Game â play L1NEW briefing then level 1
+ playCinematic("LVL1/L1NEW.ANM");
+ if (_vm->shouldQuit())
+ return;
+
+ bool completed = runLevel1();
+#else
+ bool completed = true;
+#endif
+ if (completed && !_vm->shouldQuit()) {
+ completed = runLevel2();
+ if (completed) {
+ debug(1, "InsaneRebel1: Level 2 completed!");
+ // TODO: Continue to level 3
+ }
+ }
+ _currentLevel = 0;
+ // Return to menu after level ends
+ break;
+ }
+ case 2:
+ // Game Options
+ runOptionsMenu();
+ break;
+ case 5:
+ // Exit
+ return;
+ default:
+ // Passcode, Demo â not yet implemented, return to menu
+ break;
+ }
+ }
+}
+
+// Play interactive gameplay video (with ship physics + HUD).
+void InsaneRebel1::playInteractiveVideo(const char *filename) {
+ debug(1, "InsaneRebel1::playInteractiveVideo('%s')", filename);
+
+ // Stop any leftover audio from previous video
+ terminateAudio();
+ initAudio(_audioSampleRate);
+
+ SmushPlayer *splayer = _vm->_splayer;
+ _player = splayer;
+ clearBit(0);
+ _interactiveVideoActive = true;
+ _vm->_smushVideoShouldFinish = false;
+ splayer->setCurVideoFlags(0x28);
+
+ // Center mouse, hide system cursor (we draw our own), lock mouse to window
+ smush_warpMouse(160, 100, -1);
+ CursorMan.showMouse(false);
+ g_system->lockMouse(true);
+
+ splayer->play(filename, 12);
+ _interactiveVideoActive = false;
+
+ g_system->lockMouse(false);
+}
+
+} // End of namespace Scumm
diff --git a/engines/scumm/insane/insane_rebel.cpp b/engines/scumm/insane/insane_rebel2.cpp
similarity index 99%
rename from engines/scumm/insane/insane_rebel.cpp
rename to engines/scumm/insane/insane_rebel2.cpp
index 4496daf950e..afa0f3ddfcb 100644
--- a/engines/scumm/insane/insane_rebel.cpp
+++ b/engines/scumm/insane/insane_rebel2.cpp
@@ -43,7 +43,7 @@
#include "scumm/smush/smush_player.h"
#include "scumm/smush/smush_font.h"
-#include "scumm/insane/insane_rebel.h"
+#include "scumm/insane/insane_rebel2.h"
#include "common/config-manager.h"
#include "audio/audiostream.h"
diff --git a/engines/scumm/insane/insane_rebel.h b/engines/scumm/insane/insane_rebel2.h
similarity index 99%
rename from engines/scumm/insane/insane_rebel.h
rename to engines/scumm/insane/insane_rebel2.h
index 88d20f74101..a8fac1ae4d5 100644
--- a/engines/scumm/insane/insane_rebel.h
+++ b/engines/scumm/insane/insane_rebel2.h
@@ -19,8 +19,8 @@
*
*/
-#if !defined(SCUMM_INSANE_REBEL_H) && defined(ENABLE_SCUMM_7_8)
-#define SCUMM_INSANE_REBEL_H
+#if !defined(SCUMM_INSANE_REBEL2_H) && defined(ENABLE_SCUMM_7_8)
+#define SCUMM_INSANE_REBEL2_H
#include "scumm/nut_renderer.h"
diff --git a/engines/scumm/insane/insane_rebel_audio.cpp b/engines/scumm/insane/insane_rebel2_audio.cpp
similarity index 99%
rename from engines/scumm/insane/insane_rebel_audio.cpp
rename to engines/scumm/insane/insane_rebel2_audio.cpp
index a063144f40b..3341bc18f72 100644
--- a/engines/scumm/insane/insane_rebel_audio.cpp
+++ b/engines/scumm/insane/insane_rebel2_audio.cpp
@@ -26,7 +26,7 @@
#include "scumm/smush/smush_player.h"
-#include "scumm/insane/insane_rebel.h"
+#include "scumm/insane/insane_rebel2.h"
#include "audio/audiostream.h"
#include "audio/decoders/raw.h"
diff --git a/engines/scumm/insane/insane_rebel_iact.cpp b/engines/scumm/insane/insane_rebel2_iact.cpp
similarity index 99%
rename from engines/scumm/insane/insane_rebel_iact.cpp
rename to engines/scumm/insane/insane_rebel2_iact.cpp
index 47f818bd034..28552eb5f28 100644
--- a/engines/scumm/insane/insane_rebel_iact.cpp
+++ b/engines/scumm/insane/insane_rebel2_iact.cpp
@@ -28,7 +28,7 @@
#include "scumm/smush/smush_player.h"
#include "scumm/smush/smush_font.h"
-#include "scumm/insane/insane_rebel.h"
+#include "scumm/insane/insane_rebel2.h"
namespace Scumm {
diff --git a/engines/scumm/insane/insane_rebel_levels.cpp b/engines/scumm/insane/insane_rebel2_levels.cpp
similarity index 99%
rename from engines/scumm/insane/insane_rebel_levels.cpp
rename to engines/scumm/insane/insane_rebel2_levels.cpp
index 1b61f13b70a..83d4f7f03fa 100644
--- a/engines/scumm/insane/insane_rebel_levels.cpp
+++ b/engines/scumm/insane/insane_rebel2_levels.cpp
@@ -27,7 +27,7 @@
#include "scumm/smush/smush_player.h"
-#include "scumm/insane/insane_rebel.h"
+#include "scumm/insane/insane_rebel2.h"
namespace Scumm {
diff --git a/engines/scumm/insane/insane_rebel_menu.cpp b/engines/scumm/insane/insane_rebel2_menu.cpp
similarity index 99%
rename from engines/scumm/insane/insane_rebel_menu.cpp
rename to engines/scumm/insane/insane_rebel2_menu.cpp
index 9bbd6115991..9ffbfa8ce55 100644
--- a/engines/scumm/insane/insane_rebel_menu.cpp
+++ b/engines/scumm/insane/insane_rebel2_menu.cpp
@@ -35,7 +35,7 @@
#include "scumm/smush/smush_font.h"
#include "scumm/smush/smush_multi_font.h"
-#include "scumm/insane/insane_rebel.h"
+#include "scumm/insane/insane_rebel2.h"
namespace Scumm {
diff --git a/engines/scumm/insane/insane_rebel_render.cpp b/engines/scumm/insane/insane_rebel2_render.cpp
similarity index 99%
rename from engines/scumm/insane/insane_rebel_render.cpp
rename to engines/scumm/insane/insane_rebel2_render.cpp
index 143f72db23c..77a07000f1d 100644
--- a/engines/scumm/insane/insane_rebel_render.cpp
+++ b/engines/scumm/insane/insane_rebel2_render.cpp
@@ -31,7 +31,7 @@
#include "scumm/smush/smush_player.h"
#include "scumm/smush/smush_font.h"
-#include "scumm/insane/insane_rebel.h"
+#include "scumm/insane/insane_rebel2.h"
namespace Scumm {
diff --git a/engines/scumm/insane/insane_rebel_runlevels.cpp b/engines/scumm/insane/insane_rebel2_runlevels.cpp
similarity index 99%
rename from engines/scumm/insane/insane_rebel_runlevels.cpp
rename to engines/scumm/insane/insane_rebel2_runlevels.cpp
index 3071ee93dac..a48dbc5b8c7 100644
--- a/engines/scumm/insane/insane_rebel_runlevels.cpp
+++ b/engines/scumm/insane/insane_rebel2_runlevels.cpp
@@ -25,7 +25,7 @@
#include "scumm/smush/smush_player.h"
-#include "scumm/insane/insane_rebel.h"
+#include "scumm/insane/insane_rebel2.h"
namespace Scumm {
diff --git a/engines/scumm/module.mk b/engines/scumm/module.mk
index 9ab40bf738f..2011a5049f7 100644
--- a/engines/scumm/module.mk
+++ b/engines/scumm/module.mk
@@ -133,14 +133,20 @@ MODULE_OBJS += \
insane/insane_enemy.o \
insane/insane_scenes.o \
insane/insane_iact.o \
- insane/insane_rebel.o \
insane/insane_rebel1.o \
- insane/insane_rebel_audio.o \
- insane/insane_rebel_iact.o \
- insane/insane_rebel_levels.o \
- insane/insane_rebel_menu.o \
- insane/insane_rebel_render.o \
- insane/insane_rebel_runlevels.o \
+ insane/insane_rebel1_audio.o \
+ insane/insane_rebel1_iact.o \
+ insane/insane_rebel1_levels.o \
+ insane/insane_rebel1_menu.o \
+ insane/insane_rebel1_render.o \
+ insane/insane_rebel1_runlevels.o \
+ insane/insane_rebel2.o \
+ insane/insane_rebel2_audio.o \
+ insane/insane_rebel2_iact.o \
+ insane/insane_rebel2_levels.o \
+ insane/insane_rebel2_menu.o \
+ insane/insane_rebel2_render.o \
+ insane/insane_rebel2_runlevels.o \
smush/codec1.o \
smush/codec20.o \
smush/codec37.o \
diff --git a/engines/scumm/scumm.cpp b/engines/scumm/scumm.cpp
index f0e8b348048..aac942458f6 100644
--- a/engines/scumm/scumm.cpp
+++ b/engines/scumm/scumm.cpp
@@ -52,7 +52,7 @@
#include "scumm/smush/smush_player.h"
#include "scumm/players/player_towns.h"
#include "scumm/insane/insane.h"
-#include "scumm/insane/insane_rebel.h"
+#include "scumm/insane/insane_rebel2.h"
#include "scumm/insane/insane_rebel1.h"
#include "scumm/he/animation_he.h"
#include "scumm/he/font_he.h"
diff --git a/engines/scumm/smush/smush_player.cpp b/engines/scumm/smush/smush_player.cpp
index 491d1751804..0321e192c7d 100644
--- a/engines/scumm/smush/smush_player.cpp
+++ b/engines/scumm/smush/smush_player.cpp
@@ -43,7 +43,7 @@
#include "scumm/smush/smush_player.h"
#include "scumm/insane/insane.h"
-#include "scumm/insane/insane_rebel.h"
+#include "scumm/insane/insane_rebel2.h"
#include "scumm/insane/insane_rebel1.h"
#include "audio/audiostream.h"
diff --git a/engines/scumm/smush/smush_player_ra2.cpp b/engines/scumm/smush/smush_player_ra2.cpp
index 07f4a0500dc..82a10ae4f13 100644
--- a/engines/scumm/smush/smush_player_ra2.cpp
+++ b/engines/scumm/smush/smush_player_ra2.cpp
@@ -36,7 +36,7 @@
#include "scumm/smush/smush_player.h"
#include "scumm/insane/insane.h"
-#include "scumm/insane/insane_rebel.h"
+#include "scumm/insane/insane_rebel2.h"
namespace Scumm {
Commit: f9aa62e2bc525c13632455158b51cb37f3c56b3b
https://github.com/scummvm/scummvm/commit/f9aa62e2bc525c13632455158b51cb37f3c56b3b
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:30+02:00
Commit Message:
SCUMM: RA2: Parse level 2 video chunks correctly
Changed paths:
engines/scumm/insane/insane_rebel1.cpp
engines/scumm/insane/insane_rebel1.h
engines/scumm/insane/insane_rebel1_iact.cpp
engines/scumm/insane/insane_rebel1_levels.cpp
engines/scumm/insane/insane_rebel1_menu.cpp
engines/scumm/insane/insane_rebel1_render.cpp
engines/scumm/insane/insane_rebel1_runlevels.cpp
engines/scumm/smush/smush_player.cpp
engines/scumm/smush/smush_player.h
engines/scumm/smush/smush_player_ra2.cpp
diff --git a/engines/scumm/insane/insane_rebel1.cpp b/engines/scumm/insane/insane_rebel1.cpp
index 0a7f24ecc72..3cef6a9895a 100644
--- a/engines/scumm/insane/insane_rebel1.cpp
+++ b/engines/scumm/insane/insane_rebel1.cpp
@@ -131,6 +131,21 @@ InsaneRebel1::InsaneRebel1(ScummEngine_v7 *scumm) : Insane(), _vm(scumm) {
_optionsActive = false;
_optionsSel = 0;
_turbulenceEnabled = false;
+
+ // Shooting/targeting state
+ _playerFired = false;
+ _fireCooldown = 0;
+ memset(_shotSlots, 0, sizeof(_shotSlots));
+ _shotAlternator = 0;
+ _targetProximity = 0;
+ _prevTargetProx = 0;
+ _targetCount = 0;
+ _prevTargetCount = 0;
+ memset(_gostSlots, 0, sizeof(_gostSlots));
+ _gostSlotIdx = 0;
+ _killCount = 0;
+ _lastHitTarget = 0;
+
if (loadRA1Nut("SYS/TALKFONT.NUT", _hudFontBank)) {
debug(1, "InsaneRebel1: HUD/menu glyph font loaded from SYS/TALKFONT.NUT (%d chars)", _hudFontBank.numSprites);
} else if (loadRA1Nut("SYS/TECHFONT.NUT", _hudFontBank)) {
diff --git a/engines/scumm/insane/insane_rebel1.h b/engines/scumm/insane/insane_rebel1.h
index 941f27998ee..14ceee76413 100644
--- a/engines/scumm/insane/insane_rebel1.h
+++ b/engines/scumm/insane/insane_rebel1.h
@@ -116,10 +116,17 @@ private:
void renderHUD(byte *dst, int pitch, int width, int height);
void renderMainMenuOverlay(byte *dst, int pitch, int width, int height);
void renderExplosions(byte *dst, int pitch, int width, int height);
- void renderCrosshair(byte *dst, int pitch, int width, int height);
+ void renderTargeting(byte *dst, int pitch, int width, int height);
+ void renderGostSlots(byte *dst, int pitch, int width, int height);
+ void renderLaserShots(byte *dst, int pitch, int width, int height);
void renderSprite(byte *dst, int pitch, int width, int height,
int x, int y, const RA1Sprite &sprite);
+ // Shooting pipeline â FUN_1CCA0 (0x1CCA0) shot spawner,
+ // FUN_1C0EF (0x1C0EF) target detection, FUN_1C940 (0x1C940) shot processing
+ void processShot();
+ void checkTargetHit(int16 targetIdx, int16 left, int16 top, int16 right, int16 bottom);
+
// Audio
void initAudio(int sampleRate);
void terminateAudio();
@@ -136,6 +143,7 @@ private:
RA1SpriteBank _displayBank; // SYS/DISPLAY.NUT â bottom status bar
RA1SpriteBank _hudFontBank; // RA1 HUD text glyphs (TECHFONT/TALKFONT via RA1 loader)
RA1SpriteBank _bangBank; // LxBANG.NUT â impact/explosion sprites (10 frames)
+ RA1SpriteBank _laserBank; // LxLASER.NUT â laser/shot effect sprites
SmushFont *_menuFont; // Use engine text renderer for correct TALKFONT character mapping
// RA1 screen dimensions (384x242)
@@ -257,6 +265,42 @@ private:
int _optionsSel; // 0=difficulty, 1=turbulence, 2=back
bool _turbulenceEnabled; // Random per-frame jitter in deltaX (original has it on)
+
+ // Shooting state â FUN_1CCA0 (0x1CCA0)
+ bool _playerFired; // 0x7570: fire button pressed this frame
+ int16 _fireCooldown; // 0x757C: frames until next shot allowed
+
+ // Explosion shot slots (2 slots) â FUN_1CCA0 (0x1CCA0)
+ static const int kMaxShotSlots = 2;
+ struct ShotSlot {
+ int16 timer; // 0x75E6: countdown (5 or 2, 0=inactive)
+ int16 posX; // 0x75F2: cursor X at time of shot
+ int16 posY; // 0x75F6: cursor Y at time of shot
+ int16 centerX; // 0x75EA: perspective-adjusted X
+ int16 centerY; // 0x75EE: perspective-adjusted Y
+ };
+ ShotSlot _shotSlots[kMaxShotSlots];
+ int16 _shotAlternator; // 0x241F: alternates between 0/1
+
+ // Targeting state â FUN_1C0EF (0x1C0EF)
+ int16 _targetProximity; // 0x7558: 0=none, 1=near, 2=on-target
+ int16 _prevTargetProx; // 0x755A: previous frame's proximity
+ int16 _targetCount; // 0x7552: active targets this frame
+ int16 _prevTargetCount; // 0x7554: previous frame target count
+
+ // GOST hit animation slots (10 slots) â FUN_1C9CD (0x1C9CD)
+ static const int kMaxGostSlots = 10;
+ struct GostSlot {
+ int16 targetId; // 0x23C3: target identifier (0=empty)
+ int16 frame; // 0x23D7: animation frame (0-9, >=10 = done)
+ int16 posX; // 0x239B: screen X
+ int16 posY; // 0x23AF: screen Y
+ };
+ GostSlot _gostSlots[kMaxGostSlots];
+ int16 _gostSlotIdx; // 0x23EB: next slot to write (circular 0-9)
+
+ int16 _killCount; // 0x75D0: targets destroyed this stage
+ int16 _lastHitTarget; // 0x75D6: prevents double-hit on same target
};
} // End of namespace Scumm
diff --git a/engines/scumm/insane/insane_rebel1_iact.cpp b/engines/scumm/insane/insane_rebel1_iact.cpp
index 7500706e548..9e0acecf064 100644
--- a/engines/scumm/insane/insane_rebel1_iact.cpp
+++ b/engines/scumm/insane/insane_rebel1_iact.cpp
@@ -27,7 +27,7 @@
namespace Scumm {
-// Ship physics matching FUN_1DEB5 (accumulator-based position system).
+// updateShipPhysics â FUN_1DEB5 (0x1DEB5). Accumulator-based position system.
// Roll accumulator (_74CA) driven by input, position accumulators (_74C2/_74C6)
// driven by roll + drift + cross-coupling. Ship position = base + accum >> 8.
void InsaneRebel1::updateShipPhysics() {
@@ -242,11 +242,19 @@ void InsaneRebel1::updateShipPhysics() {
_corridorLeftX, _corridorTopY, _corridorRightX, _corridorBottomY);
}
-// Level 2+ asteroid/surface physics (FUN_1CDA7).
+// updateAsteroidPhysics â FUN_1CDA7 (0x1CDA7). Opcode 0x0B handler.
// Uses 10-frame input history averaging instead of accumulators.
// Ship position = averaged input + center offset.
// Viewport = second history buffer for smooth camera scrolling.
void InsaneRebel1::updateAsteroidPhysics() {
+ // RA1 FUN_1B297-style per-frame latches for 0x0B sections:
+ // 0x5D latch 0xFFFF -> bit 0x40 (scripted obstacle/contact)
+ // 0x5F non-zero + RNG -> bit 0x80 (scripted random hit)
+ if (_gameLatch5D == 0xFFFF)
+ _damageFlags |= 0x40;
+ if (_gameLatch5F != 0 && _vm->_rnd.getRandomNumber((uint16)(_gameLatch5F - 1)) == 0)
+ _damageFlags |= 0x80;
+
// Health regeneration (FUN_1BB0E): +1 every 32 frames when alive
if (_health >= 0 && _health < kMaxHealth && (_frameCounter & 0x1F) == 0) {
_health++;
@@ -265,9 +273,14 @@ void InsaneRebel1::updateAsteroidPhysics() {
if (_health < 0) {
_deathTimer = 15; // 0x0F â shorter than Level 1's 30
}
+ _prevDamageFlags = _damageFlags;
_damageFlags = 0;
}
+ // Latches are frame-local event inputs in the original pipeline.
+ _gameLatch5D = 0;
+ _gameLatch5F = 0;
+
// Death fade countdown
if (_deathTimer > 1 && _health < 0) {
_deathTimer--;
@@ -278,55 +291,27 @@ void InsaneRebel1::updateAsteroidPhysics() {
_screenFlash--;
}
- // Input history shift and averaging (FUN_1CDA7 lines 107-131)
- // Shift history: [9] = [8], [8] = [7], ... [1] = [0], [0] = current input
- for (int i = kInputHistorySize - 1; i > 0; i--) {
- _inputHistoryX[i] = _inputHistoryX[i - 1];
- _inputHistoryY[i] = _inputHistoryY[i - 1];
- }
+ // --- Cursor position: direct mouse mapping (like RA2 first-person levels) ---
+ // RA2 crosshair = _vm->_mouse.x/y directly. RA1's original (FUN_1CDA7) averaged
+ // 10 frames of DOS mouse deltas; with ScummVM absolute coords, use direct mapping.
+ _shipPosX = CLIP<int16>((int16)_vm->_mouse.x, 0, 319);
+ _shipPosY = CLIP<int16>((int16)_vm->_mouse.y, 0, 199);
- // Read current mouse input (mapped to -160..+160 range)
- int mouseX = 0, mouseY = 0;
- if (_vm->_mouse.x >= 0) {
- mouseX = CLIP<int>(_vm->_mouse.x - kRA1CenterX, -kRA1CenterX, kRA1CenterX);
- mouseY = CLIP<int>(_vm->_mouse.y - kRA1CenterY, -kRA1CenterY, kRA1CenterY);
- }
- _inputHistoryX[0] = (int16)mouseX;
- _inputHistoryY[0] = (int16)mouseY;
-
- // Average over 10 frames
- int16 sumX = 0, sumY = 0;
- for (int i = 0; i < kInputHistorySize; i++) {
- sumX += _inputHistoryX[i];
- sumY -= _inputHistoryY[i]; // original negates Y: sVar5 = sVar5 - history[i]
- }
- _avgInputX = (int16)(sumX / kInputHistorySize);
- _avgInputY = (int16)(sumY / kInputHistorySize);
-
- // Clamp (original: [-0xA0, 0xA0] horizontal, [-0x46, 0x41] vertical)
- _avgInputX = CLIP<int16>(_avgInputX, -0xA0, 0xA0);
- _avgInputY = CLIP<int16>(_avgInputY, -0x46, 0x41);
-
- // Ship position = average + center offset
- // Original: _74BE = _75A8 + 0xA0, _74C0 = _75BC + 0x46
- _shipPosX = _avgInputX + 0xA0;
- _shipPosY = _avgInputY + 0x46;
-
- // Viewport history shift and averaging (FUN_1CDA7 lines 134-157)
+ // Viewport parallax: smoothed offset from center for background scrolling
+ int16 mouseOffX = (int16)(_vm->_mouse.x - kRA1CenterX);
+ int16 mouseOffY = (int16)(_vm->_mouse.y - kRA1CenterY);
for (int i = kInputHistorySize - 1; i > 0; i--) {
_viewHistoryX[i] = _viewHistoryX[i - 1];
_viewHistoryY[i] = _viewHistoryY[i - 1];
}
- _viewHistoryX[0] = _avgInputX;
- _viewHistoryY[0] = _avgInputY;
+ _viewHistoryX[0] = mouseOffX;
+ _viewHistoryY[0] = mouseOffY;
int16 viewSumX = 0, viewSumY = 0;
for (int i = 0; i < kInputHistorySize; i++) {
viewSumX += _viewHistoryX[i];
viewSumY += _viewHistoryY[i];
}
- // Original: _74B6 = (viewAvgX >> 1) + 0x20, clamped [0, 0x40]
- // Original: _74B8 = (viewAvgY >> 1) + 0x17, clamped [0, 0x2E]
_perspectiveX = CLIP<int16>((int16)(viewSumX / kInputHistorySize >> 1) + 0x20, 0, 0x40);
_perspectiveY = CLIP<int16>((int16)(viewSumY / kInputHistorySize >> 1) + 0x17, 0, 0x2E);
@@ -346,7 +331,8 @@ void InsaneRebel1::procIACT(byte *renderBitmap, int32 codecparam, int32 setupsan
void InsaneRebel1::procSKIP(int32 subSize, Common::SeekableReadStream &b) {
}
-// Parse RA1 GAME chunks.
+// handleGameChunk â FUN_1BE1B (0x1BE1B). Central GAME opcode dispatcher.
+// Reads 7x32-bit BE integers from GAME chunk, routes to per-opcode handlers.
void InsaneRebel1::handleGameChunk(int32 subSize, Common::SeekableReadStream &b) {
if (subSize < 8)
return;
@@ -377,6 +363,20 @@ void InsaneRebel1::handleGameChunk(int32 subSize, Common::SeekableReadStream &b)
_avgInputX = 0;
_avgInputY = 0;
+ // Shooting/targeting reset
+ _playerFired = false;
+ _fireCooldown = 0;
+ memset(_shotSlots, 0, sizeof(_shotSlots));
+ _shotAlternator = 0;
+ _targetProximity = 0;
+ _prevTargetProx = 0;
+ _targetCount = 0;
+ _prevTargetCount = 0;
+ memset(_gostSlots, 0, sizeof(_gostSlots));
+ _gostSlotIdx = 0;
+ _killCount = 0;
+ _lastHitTarget = 0;
+
// Field1 == 0 corresponds to baseline recenter behavior in the original.
if ((int32)param1 == 0) {
_shipPosX = kRA1CenterX;
@@ -452,20 +452,47 @@ void InsaneRebel1::handleGameChunk(int32 subSize, Common::SeekableReadStream &b)
// field1 = frame counter, field2 = max frames
_gameCounter = param1;
if (subSize >= 20) {
- b.readUint32BE(); // field2 (max frames)
+ uint32 maxFrames = b.readUint32BE(); // field2 (max frames)
b.readUint32BE(); // field3
b.readUint32BE(); // field4
+
+ // RA1 scripts drive progression with GAME counters. In LVL2, finish the
+ // interactive SMUSH once the script counter reaches the terminal frame.
+ // This avoids getting stuck if container/frame parsing continues past the
+ // intended gameplay endpoint.
+ if (_interactiveVideoActive && _currentLevel == 1 && maxFrames > 0 &&
+ _gameCounter >= (int32)maxFrames - 1) {
+ _vm->_smushVideoShouldFinish = true;
+ debug(1, "RA1 L2: finishing interactive video at GAME 0x0B counter=%d/%u", _gameCounter, maxFrames);
+ }
}
debug(7, "RA1 GAME 0x0B: counter=%d", _gameCounter);
break;
+ case 0x5A:
+ // Target detection â FUN_1C0EF (0x1C0EF). AABB from video stream.
+ // Params: targetIdx, left, top, width, height
+ if (subSize >= 24) {
+ int16 targetIdx = (int16)param1;
+ int16 left = (int16)b.readUint32BE();
+ int16 top = (int16)b.readUint32BE();
+ int16 w = (int16)b.readUint32BE();
+ int16 h = (int16)b.readUint32BE();
+ int16 right = left + w;
+ int16 bottom = top + h;
+ checkTargetHit(targetIdx, left, top, right, bottom);
+ debug(5, "RA1 GAME 0x5A: target=%d rect=[%d,%d]-[%d,%d] prox=%d",
+ targetIdx, left, top, right, bottom, _targetProximity);
+ }
+ break;
+
case 0x08: case 0x09: case 0x0A:
case 0x19: case 0x1A:
if (subSize >= 20) {
uint32 param2 = b.readUint32BE();
uint32 param3 = b.readUint32BE();
uint32 param4 = b.readUint32BE();
- debug(7, "RA1 GAME 0x%02x: params=(%d,%d,%d,%d)", opcode, param1, param2, param3, param4);
+ debug(5, "RA1 GAME 0x%02x: params=(%d,%d,%d,%d)", opcode, param1, param2, param3, param4);
}
break;
@@ -475,4 +502,91 @@ void InsaneRebel1::handleGameChunk(int32 subSize, Common::SeekableReadStream &b)
}
}
+// processShot â FUN_1CCA0 (0x1CCA0). Spawns shot into explosion slot when fired.
+// Called once per frame during interactive rendering.
+void InsaneRebel1::processShot() {
+ if (!_playerFired || _fireCooldown > 0) {
+ _playerFired = false;
+ if (_fireCooldown > 0)
+ _fireCooldown--;
+ return;
+ }
+
+ // Find an available slot (timer <= 0 or timer > 5)
+ int slot = -1;
+ for (int i = 0; i < kMaxShotSlots; i++) {
+ if (_shotSlots[i].timer <= 0 || _shotSlots[i].timer > 5) {
+ slot = i;
+ break;
+ }
+ }
+ if (slot < 0) {
+ _playerFired = false;
+ return;
+ }
+
+ // Record shot at current cursor position
+ _shotSlots[slot].timer = 5; // 5 frames active (original: uVar1 = 5 or 2 based on DAT_75FF)
+ _shotSlots[slot].posX = _shipPosX;
+ _shotSlots[slot].posY = _shipPosY;
+ _shotSlots[slot].centerX = _perspectiveX + (_shipPosX - kRA1CenterX) + kRA1CenterX;
+ _shotSlots[slot].centerY = _perspectiveY + (_shipPosY - kRA1CenterY) + kRA1CenterY;
+
+ _fireCooldown = 3; // Minimum frames between shots
+ _playerFired = false;
+
+ debug(5, "RA1 shot: slot=%d pos=(%d,%d)", slot, _shotSlots[slot].posX, _shotSlots[slot].posY);
+}
+
+// checkTargetHit â FUN_1C0EF (0x1C0EF). AABB target detection with snap tolerance.
+// Called from GAME 0x5A handler. Checks cursor proximity and shot hits.
+void InsaneRebel1::checkTargetHit(int16 targetIdx, int16 left, int16 top, int16 right, int16 bottom) {
+ int16 snap = _tuning.snap;
+ int16 curX = _shipPosX;
+ int16 curY = _shipPosY;
+
+ _targetCount++;
+
+ // Check proximity: cursor within target + snap + 5 margin
+ if (curX > left - snap - 5 && curX < right + snap + 5 &&
+ curY > top - snap - 5 && curY < bottom + snap + 5) {
+ if (_targetProximity == 0)
+ _targetProximity = 1; // Near
+
+ // Check tight lock: cursor within target + snap (no extra margin)
+ if (curX > left - snap && curX < right + snap &&
+ curY > top - snap && curY < bottom + snap) {
+ _targetProximity = 2; // On-target
+
+ // Check if any active shot slot hits this target
+ if (_lastHitTarget != targetIdx + 1) {
+ for (int i = 0; i < kMaxShotSlots; i++) {
+ if (_shotSlots[i].timer == 1) { // Shot in final frame = impact
+ // Hit! Record in GOST slot for explosion animation
+ int gi = _gostSlotIdx;
+ _gostSlots[gi].targetId = targetIdx + 1;
+ _gostSlots[gi].frame = 0;
+ _gostSlots[gi].posX = (left + right) / 2;
+ _gostSlots[gi].posY = (top + bottom) / 2;
+ _gostSlotIdx = (_gostSlotIdx + 1) % kMaxGostSlots;
+
+ _lastHitTarget = targetIdx + 1;
+ _score += _tuning.kill;
+ _killCount++;
+
+ // Snap cursor to target center (original: _DAT_74BE/74C0 = target center)
+ if (snap > 0) {
+ _shipPosX = (left + right) / 2;
+ _shipPosY = (top + bottom) / 2;
+ }
+
+ debug(5, "RA1 HIT: target=%d score=%d kills=%d", targetIdx, _score, _killCount);
+ return;
+ }
+ }
+ }
+ }
+ }
+}
+
} // End of namespace Scumm
diff --git a/engines/scumm/insane/insane_rebel1_levels.cpp b/engines/scumm/insane/insane_rebel1_levels.cpp
index 88ccd5afc24..9fe8e6e58aa 100644
--- a/engines/scumm/insane/insane_rebel1_levels.cpp
+++ b/engines/scumm/insane/insane_rebel1_levels.cpp
@@ -110,7 +110,6 @@ bool InsaneRebel1::loadRA1Nut(const char *filename, RA1SpriteBank &bank) {
break;
if (chunkTag == MKTAG('F','R','M','E')) {
- bool foundFobj = false;
uint32 subOffset = chunkDataOffset;
while (subOffset + 8 <= chunkEnd) {
uint32 subTag = READ_BE_UINT32(data + subOffset);
@@ -125,7 +124,6 @@ bool InsaneRebel1::loadRA1Nut(const char *filename, RA1SpriteBank &bank) {
uint16 h = READ_LE_UINT16(data + subOffset + 16);
decodedSize += (uint32)w * (uint32)h;
fobjOffsets[foundSprites] = subOffset;
- foundFobj = true;
break;
}
@@ -200,6 +198,10 @@ void InsaneRebel1::loadLevelSprites(int level) {
Common::String expldFile = Common::String::format("LVL%d/L%dEXPLD.NUT", level, level);
loadRA1Nut(expldFile.c_str(), _bangBank);
}
+
+ // Laser/shot effect sprites
+ Common::String laserFile = Common::String::format("LVL%d/L%dLASER.NUT", level, level);
+ loadRA1Nut(laserFile.c_str(), _laserBank);
}
} // End of namespace Scumm
diff --git a/engines/scumm/insane/insane_rebel1_menu.cpp b/engines/scumm/insane/insane_rebel1_menu.cpp
index 241a5fc0801..f4e4a034eb3 100644
--- a/engines/scumm/insane/insane_rebel1_menu.cpp
+++ b/engines/scumm/insane/insane_rebel1_menu.cpp
@@ -90,6 +90,15 @@ bool InsaneRebel1::notifyEvent(const Common::Event &event) {
}
}
+ // Shooting: mouse button during interactive gameplay â FUN_1CCA0 (0x1CCA0)
+ if (_interactiveVideoActive && !_menuActive) {
+ if (event.type == Common::EVENT_LBUTTONDOWN) {
+ if (_currentLevel != 1)
+ _playerFired = true;
+ return true;
+ }
+ }
+
if (event.type == Common::EVENT_KEYDOWN && event.kbd.keycode == Common::KEYCODE_ESCAPE) {
if (_player) {
debug("Rebel1: ESC pressed - skipping video");
diff --git a/engines/scumm/insane/insane_rebel1_render.cpp b/engines/scumm/insane/insane_rebel1_render.cpp
index 5e71af01770..131a796c726 100644
--- a/engines/scumm/insane/insane_rebel1_render.cpp
+++ b/engines/scumm/insane/insane_rebel1_render.cpp
@@ -22,11 +22,21 @@
#include "common/system.h"
#include "scumm/scumm_v7.h"
+#include "scumm/smush/smush_player.h"
#include "scumm/insane/insane_rebel1.h"
namespace Scumm {
+// procPreRendering â Sets viewport scroll offset before FOBJ decoding (FUN_224FD at 0x224FD).
+// For interactive levels, shifts the FOBJ decode position based on mouse-controlled perspective.
void InsaneRebel1::procPreRendering(byte *renderBitmap) {
+ if (_interactiveVideoActive && _player) {
+ _player->_ra1ViewportOffsetX = _perspectiveX;
+ _player->_ra1ViewportOffsetY = _perspectiveY;
+ } else if (_player) {
+ _player->_ra1ViewportOffsetX = 0;
+ _player->_ra1ViewportOffsetY = 0;
+ }
}
void InsaneRebel1::procPostRendering(byte *renderBitmap, int32 codecparam, int32 setupsan12,
@@ -59,53 +69,135 @@ void InsaneRebel1::procPostRendering(byte *renderBitmap, int32 codecparam, int32
renderShip(renderBitmap, pitch, width, height);
}
}
- renderExplosions(renderBitmap, pitch, width, height);
- renderCrosshair(renderBitmap, pitch, width, height);
+
+ // Shooting/targeting pipeline applies to ship/turret gameplay.
+ // LVL2 (opcode 0x0B asteroid dodge) is avoidance-only.
+ if (_currentLevel != 1) {
+ processShot();
+ for (int i = 0; i < kMaxShotSlots; i++) {
+ if (_shotSlots[i].timer > 0)
+ _shotSlots[i].timer--;
+ }
+
+ renderLaserShots(renderBitmap, pitch, width, height);
+ renderExplosions(renderBitmap, pitch, width, height);
+ renderGostSlots(renderBitmap, pitch, width, height);
+ renderTargeting(renderBitmap, pitch, width, height);
+ }
renderHUD(renderBitmap, pitch, width, height);
}
-void InsaneRebel1::renderCrosshair(byte *dst, int pitch, int width, int height) {
- int cx = _vm->_mouse.x;
- int cy = _vm->_mouse.y;
-
- // Palette index 119 = (255,0,0) pure red in L2PLAY.ANM palette.
- // Palette index 15 = (255,255,255) white, used as outline for visibility.
- const byte colorInner = 119;
- const byte colorOutline = 15;
- const int size = 7; // arm length
-
- // Helper lambda to draw a pixel with outline
- auto drawPx = [&](int x, int y, byte c) {
- if (x >= 0 && x < width && y >= 0 && y < height)
- dst[y * pitch + x] = c;
- };
-
- // Draw outline first (1px border around each arm pixel)
- for (int d = -size; d <= size; d++) {
- if (d >= -1 && d <= 1) continue; // skip center area for outline
- // Horizontal arm outline
- drawPx(cx + d, cy - 1, colorOutline);
- drawPx(cx + d, cy + 1, colorOutline);
- // Vertical arm outline
- drawPx(cx - 1, cy + d, colorOutline);
- drawPx(cx + 1, cy + d, colorOutline);
+// renderTargeting â FUN_1CB22 (0x1CB22). Targeting/lock-on crosshair at cursor position.
+// Draws a crosshair that changes color based on target proximity:
+// 0 (none) = dim grey, 1 (near) = yellow, 2 (on-target) = bright red
+void InsaneRebel1::renderTargeting(byte *dst, int pitch, int width, int height) {
+ int cx = _shipPosX;
+ int cy = _shipPosY;
+
+ // Color based on proximity state
+ // Palette indices chosen for L2PLAY visibility:
+ // 0xD7 = bright green (0,255,0), 0x63 = yellow (255,255,31), 0xDD = red (192,0,0)
+ byte color;
+ if (_targetProximity >= 2)
+ color = 0xDD; // Red â on target
+ else if (_targetProximity == 1)
+ color = 0x63; // Yellow â near
+ else
+ color = 0xD7; // Green â no target
+
+ // Animated crosshair: size pulses with frame counter
+ int armLen = 6 + ((_frameCounter >> 2) & 3);
+ int gap = 2; // Gap around center
+
+ // Horizontal arms
+ for (int i = gap; i <= armLen; i++) {
+ int lx = cx - i, rx = cx + i;
+ if (cy >= 0 && cy < height) {
+ if (lx >= 0 && lx < width)
+ dst[cy * pitch + lx] = color;
+ if (rx >= 0 && rx < width)
+ dst[cy * pitch + rx] = color;
+ }
+ }
+ // Vertical arms
+ for (int i = gap; i <= armLen; i++) {
+ int uy = cy - i, dy = cy + i;
+ if (cx >= 0 && cx < width) {
+ if (uy >= 0 && uy < height)
+ dst[uy * pitch + cx] = color;
+ if (dy >= 0 && dy < height)
+ dst[dy * pitch + cx] = color;
+ }
}
- // Arm endpoints
- drawPx(cx - size - 1, cy, colorOutline);
- drawPx(cx + size + 1, cy, colorOutline);
- drawPx(cx, cy - size - 1, colorOutline);
- drawPx(cx, cy + size + 1, colorOutline);
-
- // Draw red cross arms
- for (int d = -size; d <= size; d++) {
- if (d == 0) continue; // gap at center
- drawPx(cx + d, cy, colorInner); // horizontal
- drawPx(cx, cy + d, colorInner); // vertical
+
+ // Center dot for on-target
+ if (_targetProximity >= 2 && cx >= 0 && cx < width && cy >= 0 && cy < height) {
+ dst[cy * pitch + cx] = color;
+ }
+
+ // Save previous proximity for next frame
+ _prevTargetProx = _targetProximity;
+ _targetProximity = 0;
+
+ // Reset per-frame target count â FUN_1C940 (0x1C940)
+ _prevTargetCount = _targetCount;
+ _targetCount = 0;
+ _lastHitTarget = 0;
+}
+
+// renderGostSlots â FUN_1C9CD (0x1C9CD). Hit explosion animations at target positions.
+// Renders explosion sprites from bangBank at each GOST slot's recorded position.
+void InsaneRebel1::renderGostSlots(byte *dst, int pitch, int width, int height) {
+ if (_bangBank.numSprites <= 0)
+ return;
+
+ for (int i = 0; i < kMaxGostSlots; i++) {
+ if (_gostSlots[i].targetId != 0 && _gostSlots[i].frame < 10) {
+ int sprIdx = _gostSlots[i].frame;
+ if (sprIdx >= _bangBank.numSprites)
+ sprIdx = _bangBank.numSprites - 1;
+
+ const RA1Sprite &spr = _bangBank.sprites[sprIdx];
+ int drawX = _gostSlots[i].posX - spr.width / 2;
+ int drawY = _gostSlots[i].posY - spr.height / 2;
+ renderSprite(dst, pitch, width, height, drawX, drawY, spr);
+
+ _gostSlots[i].frame++;
+ if (_gostSlots[i].frame >= 10)
+ _gostSlots[i].targetId = 0; // Animation complete
+ }
+ }
+}
+
+// renderLaserShots â FUN_1CCA0 visual output. Renders laser sprites from _laserBank
+// when shot slots are active (timer > 0). Each set of 5 sprites is a laser animation
+// converging from the ship toward center screen. Timer 5â1 maps to sprite frames 0â4.
+void InsaneRebel1::renderLaserShots(byte *dst, int pitch, int width, int height) {
+ if (_laserBank.numSprites <= 0)
+ return;
+
+ int spritesPerSet = 5;
+ for (int i = 0; i < kMaxShotSlots; i++) {
+ if (_shotSlots[i].timer > 0 && _shotSlots[i].timer <= spritesPerSet) {
+ // Frame index: timer 5â1 maps to sprite 0â4
+ int frame = spritesPerSet - _shotSlots[i].timer;
+ // Alternate between sprite sets for left/right shots
+ int setOffset = (i % 3) * spritesPerSet;
+ int sprIdx = setOffset + frame;
+ if (sprIdx >= _laserBank.numSprites)
+ continue;
+
+ const RA1Sprite &spr = _laserBank.sprites[sprIdx];
+ // LASER sprites have embedded positions relative to screen center
+ int drawX = spr.xoffs + _perspectiveX;
+ int drawY = spr.yoffs + _perspectiveY;
+ renderSprite(dst, pitch, width, height, drawX, drawY, spr);
+ }
}
- // Center dot
- drawPx(cx, cy, colorInner);
}
+// drawFontBankString â Simplified version of FUN_221B7 (0x221B7).
+// Original is a multi-font markup-capable renderer; this uses a single font bank.
void InsaneRebel1::drawFontBankString(byte *dst, int pitch, int width, int height, int x, int y, const char *text) {
if (!dst || !text || _hudFontBank.numSprites <= 0)
return;
@@ -168,8 +260,7 @@ void InsaneRebel1::drawFontBankString(byte *dst, int pitch, int width, int heigh
}
}
-// getFontBankStringWidth -- Measure pixel width of a string using the HUD font bank.
-// Matches the pre-pass width calculation in the original drawString (FUN_221B7).
+// getFontBankStringWidth â Pre-pass width calculation from FUN_221B7 (0x221B7).
int InsaneRebel1::getFontBankStringWidth(const char *text) {
if (!text || _hudFontBank.numSprites <= 0)
return 0;
@@ -196,6 +287,8 @@ int InsaneRebel1::getFontBankStringWidth(const char *text) {
return w;
}
+// renderShip â Ship sprite rendering from FUN_1DEB5 (0x1DEB5) at LAB_1e2b2.
+// Also used by FUN_1E6A7 (0x1E6A7) turret handler via FUN_20BD3.
void InsaneRebel1::renderShip(byte *dst, int pitch, int width, int height) {
// From FUN_1DEB5 LAB_1e2b2: ship drawn when health >= 0 OR deathTimer > 20
// Hidden during last 20 frames of death sequence (deathTimer 20â0)
@@ -216,8 +309,8 @@ void InsaneRebel1::renderShip(byte *dst, int pitch, int width, int height) {
renderSprite(dst, pitch, width, height, drawX, drawY, spr);
}
-// Render explosion sprites during damage cooldown and death sequence.
-// From FUN_1DEB5 at LAB_1e185 (damage hit) and LAB_1e0e3 (death shake).
+// renderExplosions â Explosion sprites from FUN_1DEB5 (0x1DEB5) LAB_1e185 (damage hit)
+// and LAB_1e0e3 (death shake). See also FUN_1CCA0 (0x1CCA0) explosion spawner.
void InsaneRebel1::renderExplosions(byte *dst, int pitch, int width, int height) {
if (_bangBank.numSprites <= 0)
return;
@@ -275,7 +368,7 @@ void InsaneRebel1::renderExplosions(byte *dst, int pitch, int width, int height)
}
}
-// Render bottom status bar from DISPLAY.NUT with dynamic damage bar and score.
+// renderHUD â FUN_1BBCB (0x1BBCB). Status bar from DISPLAY.NUT with health bar and score.
// Original layout (320-wide): DAMAGE [green bar] | PILOTS [3 icons] | SCORE [number]
void InsaneRebel1::renderHUD(byte *dst, int pitch, int width, int height) {
if (_displayBank.numSprites == 0)
@@ -377,6 +470,8 @@ void InsaneRebel1::renderHUD(byte *dst, int pitch, int width, int height) {
}
+// renderSprite â Simplified version of FUN_20BD3 (0x20BD3) glyph/sprite renderer.
+// Original dispatches through full codec pipeline; this does flat pixel blit with transparency.
void InsaneRebel1::renderSprite(byte *dst, int pitch, int width, int height,
int x, int y, const RA1Sprite &spr) {
if (!spr.data || spr.width <= 0 || spr.height <= 0)
diff --git a/engines/scumm/insane/insane_rebel1_runlevels.cpp b/engines/scumm/insane/insane_rebel1_runlevels.cpp
index a150cd2bddb..ad0ca095540 100644
--- a/engines/scumm/insane/insane_rebel1_runlevels.cpp
+++ b/engines/scumm/insane/insane_rebel1_runlevels.cpp
@@ -230,6 +230,7 @@ bool InsaneRebel1::runLevel2() {
memset(_viewHistoryY, 0, sizeof(_viewHistoryY));
_avgInputX = 0;
_avgInputY = 0;
+ _killCount = 0;
// L2PLAY.ANM â asteroid dodge (800 frames, interactive)
playInteractiveVideo("LVL2/L2PLAY.ANM");
diff --git a/engines/scumm/smush/smush_player.cpp b/engines/scumm/smush/smush_player.cpp
index 0321e192c7d..ff01333384f 100644
--- a/engines/scumm/smush/smush_player.cpp
+++ b/engines/scumm/smush/smush_player.cpp
@@ -390,6 +390,17 @@ void SmushPlayer::release() {
} else {
free(_frameBuffer);
_frameBuffer = nullptr;
+ if (isRA1()) {
+ free(_storedFobjData);
+ _storedFobjData = nullptr;
+ _storedFobjDataSize = 0;
+ _storedFobjCodec = 0;
+ _storedFobjParm2 = 0;
+ _storedFobjLeft = 0;
+ _storedFobjTop = 0;
+ _storedFobjWidth = 0;
+ _storedFobjHeight = 0;
+ }
}
_IACTstream = nullptr;
@@ -422,7 +433,10 @@ void SmushPlayer::handleStore(int32 subSize, Common::SeekableReadStream &b) {
void SmushPlayer::handleFetch(int32 subSize, Common::SeekableReadStream &b) {
debugC(DEBUG_SMUSH, "SmushPlayer::handleFetch()");
- assert(subSize >= 6);
+ if (isRA1())
+ assert(subSize >= 4);
+ else
+ assert(subSize >= 6);
if (isRA2()) {
ra2HandleFetch(b);
@@ -430,8 +444,37 @@ void SmushPlayer::handleFetch(int32 subSize, Common::SeekableReadStream &b) {
}
if (isRA1()) {
- debug("RA1 FTCH: frame=%d _frameBuffer=%p _dst=%p _width=%d _height=%d",
- _frame, (void*)_frameBuffer, (void*)_dst, _width, _height);
+ // RA1 FTCH re-decodes the raw FOBJ captured by STOR (not a framebuffer memcpy).
+ // Chunk layout: BE32 id/flags, BE32 fetchX, BE32 fetchY.
+ uint32 fetchId = 0;
+ int32 fetchX = 0;
+ int32 fetchY = 0;
+ if (subSize >= 4)
+ fetchId = b.readUint32BE();
+ if (subSize >= 12) {
+ fetchX = b.readSint32BE();
+ fetchY = b.readSint32BE();
+ }
+
+ if (_storedFobjData != nullptr && _storedFobjDataSize > 0) {
+ const int storedCodec = _storedFobjCodec & 0xFF;
+ const uint8 storedParam = (uint8)((_storedFobjCodec >> 8) & 0xFF);
+ int left = _storedFobjLeft + fetchX;
+ int top = _storedFobjTop + fetchY;
+
+ // Apply the same interactive viewport offset used for regular FOBJ chunks.
+ left -= _ra1ViewportOffsetX;
+ top -= _ra1ViewportOffsetY;
+
+ debug("RA1 FTCH: frame=%d id=0x%08x pos=(%d,%d) using stored FOBJ codec=%d size=%dx%d",
+ _frame, fetchId, left, top, storedCodec, _storedFobjWidth, _storedFobjHeight);
+ decodeFrameObject(storedCodec, _storedFobjData, left, top,
+ _storedFobjWidth, _storedFobjHeight, _storedFobjDataSize,
+ storedParam, _storedFobjParm2);
+ } else {
+ debug("RA1 FTCH: frame=%d id=0x%08x with no stored FOBJ data", _frame, fetchId);
+ }
+ return;
}
if (_frameBuffer != nullptr) {
@@ -1259,7 +1302,7 @@ void SmushPlayer::decodeFrameObject(int codec, const uint8 *src, int left, int t
bottomNonZero, bottomTotal, bottomTotal > 0 ? (bottomNonZero * 100) / bottomTotal : 0);
}
- if (_storeFrame && !isRA2()) {
+ if (_storeFrame && !isRA1() && !isRA2()) {
if (_frameBuffer == nullptr) {
_frameBuffer = (byte *)malloc(_width * _height);
}
@@ -1315,6 +1358,8 @@ void SmushPlayer::handleFrameObject(int32 subSize, Common::SeekableReadStream &b
}
int left = (int)b.readSint16LE();
int top = (int)b.readSint16LE();
+ const int rawLeft = left;
+ const int rawTop = top;
int width = b.readUint16LE();
int height = b.readUint16LE();
@@ -1329,6 +1374,14 @@ void SmushPlayer::handleFrameObject(int32 subSize, Common::SeekableReadStream &b
_frame, codec, left, top, width, height, subSize - 14, _storeFrame, _width, _height);
}
if (isRA1()) {
+ // Viewport scroll for interactive gameplay (FUN_224FD sets _41A0/_41A2).
+ // Original renders FOBJs at raw positions, then displays a 320x200 window
+ // starting at (perspectiveX, perspectiveY) within the 384x242 buffer.
+ // FUN_22605/FUN_2289D subtract _41A0 from FOBJ X coords during rendering.
+ // We simulate the display window shift by subtracting from decode position.
+ left -= _ra1ViewportOffsetX;
+ top -= _ra1ViewportOffsetY;
+
debug("RA1 FOBJ: frame=%d codec=%d pos=(%d,%d) size=%dx%d dataSize=%d storeFrame=%d",
_frame, codec, left, top, width, height, subSize - 14, _storeFrame);
}
@@ -1345,6 +1398,23 @@ void SmushPlayer::handleFrameObject(int32 subSize, Common::SeekableReadStream &b
if (_storeFrame && isRA2()) {
ra2StoreFobjData(codec, chunk_buffer, chunk_size, left, top, width, height);
}
+ if (_storeFrame && isRA1()) {
+ free(_storedFobjData);
+ _storedFobjData = (byte *)malloc(chunk_size);
+ if (_storedFobjData != nullptr) {
+ memcpy(_storedFobjData, chunk_buffer, chunk_size);
+ _storedFobjDataSize = chunk_size;
+ _storedFobjCodec = codec | ((int)ra1Param << 8);
+ _storedFobjParm2 = ra1Parm2;
+ _storedFobjLeft = rawLeft;
+ _storedFobjTop = rawTop;
+ _storedFobjWidth = width;
+ _storedFobjHeight = height;
+ } else {
+ _storedFobjDataSize = 0;
+ }
+ _storeFrame = false;
+ }
decodeFrameObject(codec, chunk_buffer, left, top, width, height, chunk_size, ra1Param, ra1Parm2);
@@ -1372,30 +1442,38 @@ void SmushPlayer::handleFrame(int32 frameSize, Common::SeekableReadStream &b) {
}
while (frameSize > 0) {
- const int32 chunkStart = b.pos();
+ // RA1 parser exits when <=1 byte remains in a frame (FUN_1FDBC).
+ // Treat any tiny tail as frame trailer/padding and stop cleanly.
+ if (isRA1() && frameSize <= 1) {
+ if (frameSize == 1)
+ b.skip(1);
+ break;
+ }
+
+ // RA1: Top-of-loop alignment check matching original assembly FUN_1FDBC:
+ // if ((ptr & 1) && (*ptr == 0)) { ptr++; remaining--; }
+ if (isRA1() && (b.pos() & 1) && frameSize > 0) {
+ byte peek = b.readByte();
+ if (peek == 0) {
+ frameSize--;
+ } else {
+ b.seek(-1, SEEK_CUR);
+ }
+ }
+
+ if (frameSize < 8) {
+ b.skip(frameSize);
+ break;
+ }
+
uint32 subType = b.readUint32BE();
int32 subSize = b.readUint32BE();
int32 subOffset = b.pos();
- // RA1 workaround: some frames have missing padding bytes after
- // odd-sized sub-chunks (authoring tool bug in 4 frames of L2PLAY.ANM).
- // If the tag looks invalid, back up 1 byte and retry.
- if (isRA1() && frameSize > 8) {
- byte b0 = (subType >> 24) & 0xFF;
- byte b1 = (subType >> 16) & 0xFF;
- byte b2 = (subType >> 8) & 0xFF;
- byte b3 = subType & 0xFF;
- bool validTag = (b0 >= 0x20 && b0 <= 0x7E) && (b1 >= 0x20 && b1 <= 0x7E) &&
- (b2 >= 0x20 && b2 <= 0x7E) && (b3 >= 0x00 && b3 <= 0x7E);
- if (!validTag) {
- // Try 1 byte earlier (missing padding byte)
- b.seek(chunkStart - 1, SEEK_SET);
- subType = b.readUint32BE();
- subSize = b.readUint32BE();
- subOffset = b.pos();
- frameSize++;
- debugC(DEBUG_SMUSH, "SmushPlayer::handleFrame: RA1 realigned to %s at frame %d", tag2str(subType), _frame);
- }
+ // Guard against consuming the next frame marker as an in-frame chunk.
+ if (isRA1() && subType == MKTAG('F','R','M','E')) {
+ b.seek(-8, SEEK_CUR);
+ break;
}
// RA2: When _skipNext is set, skip the NEXT chunk of ANY type
@@ -1457,7 +1535,7 @@ void SmushPlayer::handleFrame(int32 frameSize, Common::SeekableReadStream &b) {
handleLoad(subSize, b);
break;
case MKTAG('G','O','S','T'):
- if (isRA2())
+ if (isRA2() || isRA1())
ra2HandleGost(subSize, b);
break;
// RA1-specific chunk types: skip gracefully
@@ -1562,12 +1640,25 @@ void SmushPlayer::handleFrame(int32 frameSize, Common::SeekableReadStream &b) {
}
break;
default:
+ if (isRA1()) {
+ // Original FUN_1FDBC lines 163-168: unknown tag with all uppercase
+ // letters (A-Z) â silently return. Otherwise error.
+ byte tb0 = (subType >> 24) & 0xFF, tb1 = (subType >> 16) & 0xFF;
+ byte tb2 = (subType >> 8) & 0xFF, tb3 = subType & 0xFF;
+ if (tb0 > 0x40 && tb0 < 0x5B && tb1 > 0x40 && tb1 < 0x5B &&
+ tb2 > 0x40 && tb2 < 0x5B && tb3 > 0x40 && tb3 < 0x5B) {
+ debug(5, "RA1: unknown uppercase tag %s at frame %d, stopping frame parse", tag2str(subType), _frame);
+ frameSize = 0;
+ continue;
+ }
+ }
error("Unknown frame subChunk found : %s, %d", tag2str(subType), subSize);
}
frameSize -= subSize + 8;
b.seek(subOffset + subSize, SEEK_SET);
- if (subSize & 1) {
+ // RA1 uses top-of-loop alignment (matching FUN_1FDBC), not bottom-of-loop padding.
+ if (!isRA1() && (subSize & 1)) {
b.skip(1);
frameSize--;
}
diff --git a/engines/scumm/smush/smush_player.h b/engines/scumm/smush/smush_player.h
index 1d63e11295e..6a10be52e04 100644
--- a/engines/scumm/smush/smush_player.h
+++ b/engines/scumm/smush/smush_player.h
@@ -165,6 +165,7 @@ private:
byte *_storedFobjData;
int32 _storedFobjDataSize;
int _storedFobjCodec;
+ uint16 _storedFobjParm2; // RA1: FOBJ bytes[12..13] needed by codec 4/5
int _storedFobjLeft;
int _storedFobjTop;
int _storedFobjWidth;
@@ -180,6 +181,11 @@ private:
int _ra1ObjOverlayWidth;
int _ra1ObjOverlayHeight;
+ // RA1: Viewport scroll offset for interactive gameplay (FUN_224FD at 0x224FD).
+ // Set by InsaneRebel1::procPreRendering(), applied to FOBJ decode positions.
+ int _ra1ViewportOffsetX;
+ int _ra1ViewportOffsetY;
+
// RA2: Most recently decoded FOBJ in the current frame, used by GOST chunks
// to re-render the same sprite payload at a different position.
byte *_lastFobjData;
diff --git a/engines/scumm/smush/smush_player_ra2.cpp b/engines/scumm/smush/smush_player_ra2.cpp
index 82a10ae4f13..e19f4b51a76 100644
--- a/engines/scumm/smush/smush_player_ra2.cpp
+++ b/engines/scumm/smush/smush_player_ra2.cpp
@@ -61,6 +61,7 @@ void SmushPlayer::ra2InitFields() {
_storedFobjData = nullptr;
_storedFobjDataSize = 0;
_storedFobjCodec = 0;
+ _storedFobjParm2 = 0;
_storedFobjLeft = 0;
_storedFobjTop = 0;
_storedFobjWidth = 0;
@@ -80,6 +81,8 @@ void SmushPlayer::ra2InitFields() {
_ra1ObjOverlayTop = 0;
_ra1ObjOverlayWidth = 0;
_ra1ObjOverlayHeight = 0;
+ _ra1ViewportOffsetX = 0;
+ _ra1ViewportOffsetY = 0;
_skipNext = false;
_ra2FastForwarding = false;
_fobjOffsetX = 0;
@@ -150,6 +153,7 @@ void SmushPlayer::ra2ReleaseVideo() {
free(_storedFobjData);
_storedFobjData = nullptr;
_storedFobjDataSize = 0;
+ _storedFobjParm2 = 0;
free(_lastFobjData);
_lastFobjData = nullptr;
_lastFobjDataSize = 0;
@@ -403,6 +407,7 @@ void SmushPlayer::ra2StoreFobjData(int codec, const byte *data, int32 dataSize,
memcpy(_storedFobjData, data, dataSize);
_storedFobjDataSize = dataSize;
_storedFobjCodec = codec;
+ _storedFobjParm2 = 0;
_storedFobjLeft = left;
_storedFobjTop = top;
_storedFobjWidth = width;
Commit: 0aa073970104a5bce20ee059ff7644a20735b6e7
https://github.com/scummvm/scummvm/commit/0aa073970104a5bce20ee059ff7644a20735b6e7
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:30+02:00
Commit Message:
SCUMM: RA1: Add level selection and level 2 pointer handling
Changed paths:
engines/scumm/insane/insane_rebel1.cpp
engines/scumm/insane/insane_rebel1.h
engines/scumm/insane/insane_rebel1_iact.cpp
engines/scumm/insane/insane_rebel1_menu.cpp
engines/scumm/insane/insane_rebel1_render.cpp
engines/scumm/insane/insane_rebel1_runlevels.cpp
diff --git a/engines/scumm/insane/insane_rebel1.cpp b/engines/scumm/insane/insane_rebel1.cpp
index 3cef6a9895a..64c5c4a7b6f 100644
--- a/engines/scumm/insane/insane_rebel1.cpp
+++ b/engines/scumm/insane/insane_rebel1.cpp
@@ -130,6 +130,9 @@ InsaneRebel1::InsaneRebel1(ScummEngine_v7 *scumm) : Insane(), _vm(scumm) {
_menuFrameCounter = 0;
_optionsActive = false;
_optionsSel = 0;
+ _levelSelectActive = false;
+ _levelSelectSel = 0;
+ _startLevel = 1;
_turbulenceEnabled = false;
// Shooting/targeting state
@@ -154,6 +157,16 @@ InsaneRebel1::InsaneRebel1(ScummEngine_v7 *scumm) : Insane(), _vm(scumm) {
warning("InsaneRebel1: failed to load RA1 HUD font bank (TECHFONT/TALKFONT)");
}
+ // FUN_1CB22 uses "<<" layer markers that resolve to TECHFONT in the original.
+ // Keep a dedicated TECH font bank for targeting markers/lock indicators.
+ if (loadRA1Nut("SYS/TECHFONT.NUT", _techFontBank)) {
+ debug(1, "InsaneRebel1: targeting glyph font loaded from SYS/TECHFONT.NUT (%d chars)", _techFontBank.numSprites);
+ } else if (loadRA1Nut("SYS/TALKFONT.NUT", _techFontBank)) {
+ debug(1, "InsaneRebel1: targeting glyph font fallback loaded from SYS/TALKFONT.NUT (%d chars)", _techFontBank.numSprites);
+ } else {
+ warning("InsaneRebel1: failed to load targeting font bank (TECHFONT/TALKFONT)");
+ }
+
// Audio
initAudio(11025);
diff --git a/engines/scumm/insane/insane_rebel1.h b/engines/scumm/insane/insane_rebel1.h
index 14ceee76413..be8cdb994de 100644
--- a/engines/scumm/insane/insane_rebel1.h
+++ b/engines/scumm/insane/insane_rebel1.h
@@ -93,8 +93,9 @@ private:
void clearVideoBuffer();
// Main menu loop on O1OPTION.ANM background (0x15968)
- // Returns: 1=Start New Game, 2=Game Options, 3=Enter Passcode, 4=Continue Demo, 5=Exit
+ // Returns: 1=Start New Game, 2=Game Options, 3=Level Select, 4=Continue Demo, 5=Exit
int runMainMenu();
+ void runLevelSelectMenu();
// Level 1 flow (0x16100): hangar â CU1 â gameplay â CU2 â turret â end
// Returns true if level completed, false if player quit
@@ -142,6 +143,7 @@ private:
RA1SpriteBank _shipBank;
RA1SpriteBank _displayBank; // SYS/DISPLAY.NUT â bottom status bar
RA1SpriteBank _hudFontBank; // RA1 HUD text glyphs (TECHFONT/TALKFONT via RA1 loader)
+ RA1SpriteBank _techFontBank; // SYS/TECHFONT.NUT â targeting glyph layer ("<<" markers)
RA1SpriteBank _bangBank; // LxBANG.NUT â impact/explosion sprites (10 frames)
RA1SpriteBank _laserBank; // LxLASER.NUT â laser/shot effect sprites
SmushFont *_menuFont; // Use engine text renderer for correct TALKFONT character mapping
@@ -263,12 +265,15 @@ private:
// Options submenu state
bool _optionsActive; // True when showing options instead of main menu
int _optionsSel; // 0=difficulty, 1=turbulence, 2=back
+ bool _levelSelectActive; // True when showing level-select submenu
+ int _levelSelectSel; // 0=Level1, 1=Level2, 2=Back
+ int _startLevel; // 1-based start level for "Start New Game"
bool _turbulenceEnabled; // Random per-frame jitter in deltaX (original has it on)
// Shooting state â FUN_1CCA0 (0x1CCA0)
bool _playerFired; // 0x7570: fire button pressed this frame
- int16 _fireCooldown; // 0x757C: frames until next shot allowed
+ int16 _fireCooldown; // 0x757C: button-edge gate in original input pipeline
// Explosion shot slots (2 slots) â FUN_1CCA0 (0x1CCA0)
static const int kMaxShotSlots = 2;
diff --git a/engines/scumm/insane/insane_rebel1_iact.cpp b/engines/scumm/insane/insane_rebel1_iact.cpp
index 9e0acecf064..fd52033ab8a 100644
--- a/engines/scumm/insane/insane_rebel1_iact.cpp
+++ b/engines/scumm/insane/insane_rebel1_iact.cpp
@@ -38,9 +38,16 @@ void InsaneRebel1::updateShipPhysics() {
_damageCooldown--;
// --- Step 1: Mouse input as offset from screen center ---
- // Original: _DAT_756C (horizontal), _DAT_756E (vertical)
- int16 inputX = (int16)(_vm->_mouse.x - 160);
- int16 inputY = (int16)(_vm->_mouse.y - 100);
+ // Original: _DAT_756C (horizontal), _DAT_756E (vertical) in 320x200 space.
+ int16 mouseX = (int16)_vm->_mouse.x;
+ int16 mouseY = (int16)_vm->_mouse.y;
+ if (_player && _player->_width > 0 && _player->_height > 0 &&
+ (mouseX >= 320 || mouseY >= 200)) {
+ mouseX = (int16)((mouseX * 320) / _player->_width);
+ mouseY = (int16)((mouseY * 200) / _player->_height);
+ }
+ int16 inputX = (int16)(mouseX - 160);
+ int16 inputY = (int16)(mouseY - 100);
inputX = CLIP<int16>(inputX, -127, 127);
inputY = CLIP<int16>(inputY, -127, 127);
@@ -132,25 +139,11 @@ void InsaneRebel1::updateShipPhysics() {
}
// --- Step 8: Perspective offsets ---
- {
- int absOffX = ABS(_shipPosX - kRA1CenterX);
- if (absOffX > 0)
- _perspectiveX = (int16)((kRA1FocalX * kRA1CenterX * absOffX) /
- ((kRA1CenterX - kRA1FocalX) * absOffX + kRA1FocalX * kRA1CenterX));
- else
- _perspectiveX = 0;
- if (_shipPosX < kRA1CenterX + 1)
- _perspectiveX = -_perspectiveX;
-
- int absOffY = ABS(_shipPosY - kRA1CenterY);
- if (absOffY > 0)
- _perspectiveY = (int16)((kRA1FocalY * kRA1CenterY * absOffY) /
- ((kRA1CenterY - kRA1FocalY) * absOffY + kRA1FocalY * kRA1CenterY));
- else
- _perspectiveY = 0;
- if (_shipPosY < kRA1CenterY + 1)
- _perspectiveY = -_perspectiveY;
- }
+ // FUN_1DEB5 computes these linearly from ship offsets:
+ // viewX = clamp((_74BA + 0x20), 0, 0x40)
+ // viewY = clamp((_74BC + 0x17), 0, 0x2E)
+ _perspectiveX = CLIP<int16>((int16)(_shipPosX - kRA1CenterX + 0x20), 0, 0x40);
+ _perspectiveY = CLIP<int16>((int16)(_shipPosY - kRA1CenterY + 0x17), 0, 0x2E);
// --- Step 9: Direction sprite index (FUN_1DEB5 LAB_1e23e) ---
// Horizontal component from _74CA (rollAccum):
@@ -291,29 +284,60 @@ void InsaneRebel1::updateAsteroidPhysics() {
_screenFlash--;
}
- // --- Cursor position: direct mouse mapping (like RA2 first-person levels) ---
- // RA2 crosshair = _vm->_mouse.x/y directly. RA1's original (FUN_1CDA7) averaged
- // 10 frames of DOS mouse deltas; with ScummVM absolute coords, use direct mapping.
- _shipPosX = CLIP<int16>((int16)_vm->_mouse.x, 0, 319);
- _shipPosY = CLIP<int16>((int16)_vm->_mouse.y, 0, 199);
+ // --- Cursor and perspective smoothing (FUN_1CDA7) ---
+ // _inputHistory* maps to 0x7580/0x7594, _viewHistory* to 0x75A8/0x75BC.
+ int16 mouseX = (int16)_vm->_mouse.x;
+ int16 mouseY = (int16)_vm->_mouse.y;
+ if (_player && _player->_width > 0 && _player->_height > 0 &&
+ (mouseX >= 320 || mouseY >= 200)) {
+ mouseX = (int16)((mouseX * 320) / _player->_width);
+ mouseY = (int16)((mouseY * 200) / _player->_height);
+ }
+ int16 inputX = (int16)(mouseX - kRA1CenterX);
+ int16 inputY = (int16)(mouseY - kRA1CenterY);
+ inputX = CLIP<int16>(inputX, -0xA0, 0xA0);
+ inputY = CLIP<int16>(inputY, -100, 100);
+
+ for (int i = kInputHistorySize - 1; i > 0; i--) {
+ _inputHistoryX[i] = _inputHistoryX[i - 1];
+ _inputHistoryY[i] = _inputHistoryY[i - 1];
+ }
+ _inputHistoryX[0] = inputX;
+ _inputHistoryY[0] = inputY;
+
+ int sumInputX = 0;
+ int sumInputY = 0;
+ for (int i = 0; i < kInputHistorySize; i++) {
+ sumInputX += _inputHistoryX[i];
+ sumInputY += _inputHistoryY[i];
+ }
+
+ _avgInputX = (int16)(sumInputX / kInputHistorySize);
+ _avgInputY = (int16)(-sumInputY / kInputHistorySize);
+ _avgInputX = CLIP<int16>(_avgInputX, -0xA0, 0xA0);
+ _avgInputY = CLIP<int16>(_avgInputY, -0x46, 0x41);
+
+ _shipPosX = _avgInputX + 0xA0;
+ _shipPosY = _avgInputY + 0x46;
- // Viewport parallax: smoothed offset from center for background scrolling
- int16 mouseOffX = (int16)(_vm->_mouse.x - kRA1CenterX);
- int16 mouseOffY = (int16)(_vm->_mouse.y - kRA1CenterY);
for (int i = kInputHistorySize - 1; i > 0; i--) {
_viewHistoryX[i] = _viewHistoryX[i - 1];
_viewHistoryY[i] = _viewHistoryY[i - 1];
}
- _viewHistoryX[0] = mouseOffX;
- _viewHistoryY[0] = mouseOffY;
+ _viewHistoryX[0] = _avgInputX;
+ _viewHistoryY[0] = _avgInputY;
- int16 viewSumX = 0, viewSumY = 0;
+ int sumViewX = 0;
+ int sumViewY = 0;
for (int i = 0; i < kInputHistorySize; i++) {
- viewSumX += _viewHistoryX[i];
- viewSumY += _viewHistoryY[i];
+ sumViewX += _viewHistoryX[i];
+ sumViewY += _viewHistoryY[i];
}
- _perspectiveX = CLIP<int16>((int16)(viewSumX / kInputHistorySize >> 1) + 0x20, 0, 0x40);
- _perspectiveY = CLIP<int16>((int16)(viewSumY / kInputHistorySize >> 1) + 0x17, 0, 0x2E);
+
+ int16 avgViewX = (int16)(sumViewX / kInputHistorySize);
+ int16 avgViewY = (int16)(sumViewY / kInputHistorySize);
+ _perspectiveX = CLIP<int16>((int16)((avgViewX >> 1) + 0x20), 0, 0x40);
+ _perspectiveY = CLIP<int16>((int16)((avgViewY >> 1) + 0x17), 0, 0x2E);
_frameCounter++;
@@ -505,14 +529,10 @@ void InsaneRebel1::handleGameChunk(int32 subSize, Common::SeekableReadStream &b)
// processShot â FUN_1CCA0 (0x1CCA0). Spawns shot into explosion slot when fired.
// Called once per frame during interactive rendering.
void InsaneRebel1::processShot() {
- if (!_playerFired || _fireCooldown > 0) {
- _playerFired = false;
- if (_fireCooldown > 0)
- _fireCooldown--;
+ if (!_playerFired)
return;
- }
- // Find an available slot (timer <= 0 or timer > 5)
+ // Find first available slot (timer < 1 or > 5), matching FUN_1CCA0.
int slot = -1;
for (int i = 0; i < kMaxShotSlots; i++) {
if (_shotSlots[i].timer <= 0 || _shotSlots[i].timer > 5) {
@@ -525,14 +545,14 @@ void InsaneRebel1::processShot() {
return;
}
- // Record shot at current cursor position
- _shotSlots[slot].timer = 5; // 5 frames active (original: uVar1 = 5 or 2 based on DAT_75FF)
+ // Record shot at current cursor position.
+ _shotSlots[slot].timer = 5;
_shotSlots[slot].posX = _shipPosX;
_shotSlots[slot].posY = _shipPosY;
- _shotSlots[slot].centerX = _perspectiveX + (_shipPosX - kRA1CenterX) + kRA1CenterX;
- _shotSlots[slot].centerY = _perspectiveY + (_shipPosY - kRA1CenterY) + kRA1CenterY;
+ _shotSlots[slot].centerX = _shipPosX;
+ _shotSlots[slot].centerY = _shipPosY;
+ _shotAlternator = 1 - _shotAlternator;
- _fireCooldown = 3; // Minimum frames between shots
_playerFired = false;
debug(5, "RA1 shot: slot=%d pos=(%d,%d)", slot, _shotSlots[slot].posX, _shotSlots[slot].posY);
diff --git a/engines/scumm/insane/insane_rebel1_menu.cpp b/engines/scumm/insane/insane_rebel1_menu.cpp
index f4e4a034eb3..2344eaf4964 100644
--- a/engines/scumm/insane/insane_rebel1_menu.cpp
+++ b/engines/scumm/insane/insane_rebel1_menu.cpp
@@ -29,6 +29,32 @@
namespace Scumm {
bool InsaneRebel1::notifyEvent(const Common::Event &event) {
+ if (_menuActive && _levelSelectActive && event.type == Common::EVENT_KEYDOWN) {
+ switch (event.kbd.keycode) {
+ case Common::KEYCODE_UP:
+ case Common::KEYCODE_w:
+ _levelSelectSel = (_levelSelectSel + 2) % 3;
+ return true;
+ case Common::KEYCODE_DOWN:
+ case Common::KEYCODE_s:
+ _levelSelectSel = (_levelSelectSel + 1) % 3;
+ return true;
+ case Common::KEYCODE_RETURN:
+ case Common::KEYCODE_KP_ENTER:
+ case Common::KEYCODE_SPACE:
+ _menuConfirmed = true;
+ _vm->_smushVideoShouldFinish = true;
+ return true;
+ case Common::KEYCODE_ESCAPE:
+ _levelSelectSel = 2; // Back
+ _menuConfirmed = true;
+ _vm->_smushVideoShouldFinish = true;
+ return true;
+ default:
+ break;
+ }
+ }
+
if (_menuActive && _optionsActive && event.type == Common::EVENT_KEYDOWN) {
switch (event.kbd.keycode) {
case Common::KEYCODE_UP:
@@ -55,7 +81,7 @@ bool InsaneRebel1::notifyEvent(const Common::Event &event) {
}
}
- if (_menuActive && !_optionsActive && event.type == Common::EVENT_KEYDOWN) {
+ if (_menuActive && !_optionsActive && !_levelSelectActive && event.type == Common::EVENT_KEYDOWN) {
switch (event.kbd.keycode) {
case Common::KEYCODE_UP:
case Common::KEYCODE_w:
@@ -93,8 +119,7 @@ bool InsaneRebel1::notifyEvent(const Common::Event &event) {
// Shooting: mouse button during interactive gameplay â FUN_1CCA0 (0x1CCA0)
if (_interactiveVideoActive && !_menuActive) {
if (event.type == Common::EVENT_LBUTTONDOWN) {
- if (_currentLevel != 1)
- _playerFired = true;
+ _playerFired = true;
return true;
}
}
@@ -155,11 +180,55 @@ void InsaneRebel1::renderMainMenuOverlay(byte *dst, int pitch, int width, int he
return;
}
+ if (_levelSelectActive) {
+ // --- Level select submenu ---
+ const int titleW = getFontBankStringWidth("LEVEL SELECT");
+ drawFontBankString(dst, pitch, width, height, (width - titleW) / 2, 36, "LEVEL SELECT");
+
+ const char *kLevelItems[3] = {
+ "LEVEL 1: FLIGHT TRAINING",
+ "LEVEL 2: ASTEROID FIELD",
+ "BACK"
+ };
+
+ const int menuY = 60;
+ const int rowH = 16;
+
+ for (int i = 0; i < 3; i++) {
+ const int textW = getFontBankStringWidth(kLevelItems[i]);
+ const int textX = (width - textW) / 2;
+ const int y = menuY + i * rowH;
+ drawFontBankString(dst, pitch, width, height, textX, y + 1, kLevelItems[i]);
+
+ if (i == _startLevel - 1) {
+ drawFontBankString(dst, pitch, width, height, textX - 12, y + 1, ">");
+ }
+
+ if (i == _levelSelectSel) {
+ byte highlightColor = ((_menuFrameCounter / 8) & 1) ? 248 : 240;
+ int bracketWidth = textW + 12;
+ int leftX = CLIP(textX - 6, 0, width - 1);
+ int rightX = CLIP(leftX + bracketWidth, 0, width - 1);
+ int topY = CLIP(y - 1, 0, height - 1);
+ int bottomY = CLIP(y + rowH - 2, 0, height - 1);
+ for (int x = leftX; x <= rightX; x++) {
+ dst[topY * pitch + x] = highlightColor;
+ dst[bottomY * pitch + x] = highlightColor;
+ }
+ for (int py = topY; py <= bottomY; py++) {
+ dst[py * pitch + leftX] = highlightColor;
+ dst[py * pitch + rightX] = highlightColor;
+ }
+ }
+ }
+ return;
+ }
+
// --- Main menu ---
static const char *kMenuItems[5] = {
"START NEW GAME",
"GAME OPTIONS",
- "ENTER PASSCODE",
+ "LEVEL SELECT",
"CONTINUE DEMO",
"EXIT TO DOS"
};
@@ -273,4 +342,38 @@ void InsaneRebel1::runOptionsMenu() {
_optionsActive = false;
}
+void InsaneRebel1::runLevelSelectMenu() {
+ _levelSelectSel = CLIP(_startLevel - 1, 0, 1);
+ _levelSelectActive = true;
+
+ while (!_vm->shouldQuit()) {
+ _menuActive = true;
+ _menuConfirmed = false;
+ _menuFrameCounter = 0;
+ clearVideoBuffer();
+ playCinematic("OPEN/O1OPTION.ANM");
+ _menuActive = false;
+
+ if (_vm->shouldQuit())
+ break;
+
+ if (_menuConfirmed) {
+ switch (_levelSelectSel) {
+ case 0:
+ _startLevel = 1;
+ _levelSelectActive = false;
+ return;
+ case 1:
+ _startLevel = 2;
+ _levelSelectActive = false;
+ return;
+ case 2:
+ _levelSelectActive = false;
+ return;
+ }
+ }
+ }
+ _levelSelectActive = false;
+}
+
} // End of namespace Scumm
diff --git a/engines/scumm/insane/insane_rebel1_render.cpp b/engines/scumm/insane/insane_rebel1_render.cpp
index 131a796c726..6f37737a2ce 100644
--- a/engines/scumm/insane/insane_rebel1_render.cpp
+++ b/engines/scumm/insane/insane_rebel1_render.cpp
@@ -27,12 +27,105 @@
namespace Scumm {
+static void drawBankString(const RA1SpriteBank &bank, byte *dst, int pitch, int width, int height,
+ int x, int y, const char *text) {
+ if (!dst || !text || bank.numSprites <= 0)
+ return;
+
+ for (int i = 0; text[i] != '\0'; i++) {
+ const byte ch = (byte)text[i];
+
+ if (ch == ' ') {
+ x += 6;
+ continue;
+ }
+
+ // RA1 font renderer indexes printable characters from '!' (0x21), not raw ASCII.
+ if (ch < 0x21) {
+ x += 4;
+ continue;
+ }
+ const int fontIdx = (int)ch - 0x21;
+ if (fontIdx < 0 || fontIdx >= bank.numSprites) {
+ x += 4;
+ continue;
+ }
+
+ const RA1Sprite &glyph = bank.sprites[fontIdx];
+ const int gw = glyph.width;
+ const int gh = glyph.height;
+ const int gx = x + glyph.xoffs;
+ const int gy = y + glyph.yoffs;
+ const uint64 glyphPixels = (uint64)gw * (uint64)gh;
+ if (!glyph.data || gw <= 0 || gh <= 0 || glyphPixels == 0 || glyphPixels > 0x10000) {
+ x += 4;
+ continue;
+ }
+ if (!(bank.decodedData && bank.decodedSize > 0)) {
+ x += 4;
+ continue;
+ }
+ const byte *bankStart = bank.decodedData;
+ const byte *bankEnd = bank.decodedData + bank.decodedSize;
+ if (glyph.data < bankStart || glyph.data >= bankEnd || glyph.data + glyphPixels > bankEnd) {
+ x += 4;
+ continue;
+ }
+
+ for (int py = 0; py < gh; py++) {
+ const int sy = gy + py;
+ if (sy < 0 || sy >= height)
+ continue;
+ for (int px = 0; px < gw; px++) {
+ const int sx = gx + px;
+ if (sx < 0 || sx >= width)
+ continue;
+ const byte pixel = glyph.data[py * gw + px];
+ if (pixel != 0)
+ dst[sy * pitch + sx] = pixel;
+ }
+ }
+
+ x += gw > 0 ? gw : 4;
+ }
+}
+
+static int getBankStringWidth(const RA1SpriteBank &bank, const char *text) {
+ if (!text || bank.numSprites <= 0)
+ return 0;
+
+ int w = 0;
+ for (int i = 0; text[i] != '\0'; i++) {
+ const byte ch = (byte)text[i];
+ if (ch == ' ') {
+ w += 6;
+ continue;
+ }
+ if (ch < 0x21) {
+ w += 4;
+ continue;
+ }
+ const int fontIdx = (int)ch - 0x21;
+ if (fontIdx < 0 || fontIdx >= bank.numSprites) {
+ w += 4;
+ continue;
+ }
+ const RA1Sprite &glyph = bank.sprites[fontIdx];
+ w += glyph.width > 0 ? glyph.width : 4;
+ }
+ return w;
+}
+
// procPreRendering â Sets viewport scroll offset before FOBJ decoding (FUN_224FD at 0x224FD).
// For interactive levels, shifts the FOBJ decode position based on mouse-controlled perspective.
void InsaneRebel1::procPreRendering(byte *renderBitmap) {
if (_interactiveVideoActive && _player) {
- _player->_ra1ViewportOffsetX = _perspectiveX;
- _player->_ra1ViewportOffsetY = _perspectiveY;
+ // FUN_224FD stores absolute 320x200 window origin in a 384x242 frame:
+ // X in [0..0x40], Y in [0..0x2E], centered at (0x20,0x17).
+ // ScummVM presents the full 384x242 frame, so use center-relative delta
+ // to avoid exposing black border over cockpit edges.
+ _player->_ra1ViewportOffsetX = _perspectiveX - 0x20;
+ _player->_ra1ViewportOffsetY = _perspectiveY - 0x17;
} else if (_player) {
_player->_ra1ViewportOffsetX = 0;
_player->_ra1ViewportOffsetY = 0;
@@ -70,69 +163,50 @@ void InsaneRebel1::procPostRendering(byte *renderBitmap, int32 codecparam, int32
}
}
- // Shooting/targeting pipeline applies to ship/turret gameplay.
- // LVL2 (opcode 0x0B asteroid dodge) is avoidance-only.
- if (_currentLevel != 1) {
- processShot();
- for (int i = 0; i < kMaxShotSlots; i++) {
- if (_shotSlots[i].timer > 0)
- _shotSlots[i].timer--;
- }
-
- renderLaserShots(renderBitmap, pitch, width, height);
- renderExplosions(renderBitmap, pitch, width, height);
- renderGostSlots(renderBitmap, pitch, width, height);
- renderTargeting(renderBitmap, pitch, width, height);
+ // FUN_1CDA7 (0x0B) still executes targeting/shot overlay pipeline
+ // (FUN_1C940, FUN_1CCA0, FUN_1C9CD, FUN_1CB22) while alive.
+ processShot();
+ for (int i = 0; i < kMaxShotSlots; i++) {
+ if (_shotSlots[i].timer > 0)
+ _shotSlots[i].timer--;
}
+
+ renderLaserShots(renderBitmap, pitch, width, height);
+ renderExplosions(renderBitmap, pitch, width, height);
+ renderGostSlots(renderBitmap, pitch, width, height);
+ renderTargeting(renderBitmap, pitch, width, height);
renderHUD(renderBitmap, pitch, width, height);
}
-// renderTargeting â FUN_1CB22 (0x1CB22). Targeting/lock-on crosshair at cursor position.
-// Draws a crosshair that changes color based on target proximity:
-// 0 (none) = dim grey, 1 (near) = yellow, 2 (on-target) = bright red
+// renderTargeting â FUN_1CB22 (0x1CB22). Targeting/lock-on indicator.
+// The original does not draw a hardcoded pixel cross; it renders glyph markers
+// whose state depends on _targetProximity.
void InsaneRebel1::renderTargeting(byte *dst, int pitch, int width, int height) {
- int cx = _shipPosX;
- int cy = _shipPosY;
-
- // Color based on proximity state
- // Palette indices chosen for L2PLAY visibility:
- // 0xD7 = bright green (0,255,0), 0x63 = yellow (255,255,31), 0xDD = red (192,0,0)
- byte color;
- if (_targetProximity >= 2)
- color = 0xDD; // Red â on target
- else if (_targetProximity == 1)
- color = 0x63; // Yellow â near
- else
- color = 0xD7; // Green â no target
-
- // Animated crosshair: size pulses with frame counter
- int armLen = 6 + ((_frameCounter >> 2) & 3);
- int gap = 2; // Gap around center
-
- // Horizontal arms
- for (int i = gap; i <= armLen; i++) {
- int lx = cx - i, rx = cx + i;
- if (cy >= 0 && cy < height) {
- if (lx >= 0 && lx < width)
- dst[cy * pitch + lx] = color;
- if (rx >= 0 && rx < width)
- dst[cy * pitch + rx] = color;
+ const RA1SpriteBank &markerBank = (_techFontBank.numSprites > 0) ? _techFontBank : _hudFontBank;
+ if (markerBank.numSprites > 0) {
+ // FUN_1CB22 can switch marker sets via DAT_75FF bit 1.
+ // Baseline RA1 targeting uses '^' and animation e..h.
+ const bool altMarkerSet = false;
+
+ // Lock indicator at fixed center positions:
+ // FUN_1CB22 draws marker strings at (0xA0,0x78) and (0xA0,0x7E).
+ if (_targetProximity > 0) {
+ drawBankString(markerBank, dst, pitch, width, height, 0xA0, 0x78, "]");
+ if (_targetProximity > 1)
+ drawBankString(markerBank, dst, pitch, width, height, 0xA0, 0x7E, "a");
}
- }
- // Vertical arms
- for (int i = gap; i <= armLen; i++) {
- int uy = cy - i, dy = cy + i;
- if (cx >= 0 && cx < width) {
- if (uy >= 0 && uy < height)
- dst[uy * pitch + cx] = color;
- if (dy >= 0 && dy < height)
- dst[dy * pitch + cx] = color;
+
+ // Pointer glyph at current aim position. Original uses two variants:
+ // default marker ('^' or 'x') and animated lock marker (e..h or y..|).
+ char marker[2] = { (char)(altMarkerSet ? 'x' : '^'), '\0' };
+ if (_targetProximity > 1) {
+ marker[0] = (char)((altMarkerSet ? 'y' : 'e') + (_frameCounter & 3));
}
- }
- // Center dot for on-target
- if (_targetProximity >= 2 && cx >= 0 && cx < width && cy >= 0 && cy < height) {
- dst[cy * pitch + cx] = color;
+ int cursorX = CLIP<int>(_shipPosX, 0, width - 1);
+ int cursorY = CLIP<int>(_shipPosY, 0, height - 1);
+ int markerW = getBankStringWidth(markerBank, marker);
+ drawBankString(markerBank, dst, pitch, width, height, cursorX - markerW / 2, cursorY, marker);
}
// Save previous proximity for next frame
@@ -179,18 +253,15 @@ void InsaneRebel1::renderLaserShots(byte *dst, int pitch, int width, int height)
int spritesPerSet = 5;
for (int i = 0; i < kMaxShotSlots; i++) {
if (_shotSlots[i].timer > 0 && _shotSlots[i].timer <= spritesPerSet) {
- // Frame index: timer 5â1 maps to sprite 0â4
int frame = spritesPerSet - _shotSlots[i].timer;
- // Alternate between sprite sets for left/right shots
- int setOffset = (i % 3) * spritesPerSet;
- int sprIdx = setOffset + frame;
+ int sprIdx = frame;
if (sprIdx >= _laserBank.numSprites)
continue;
const RA1Sprite &spr = _laserBank.sprites[sprIdx];
- // LASER sprites have embedded positions relative to screen center
- int drawX = spr.xoffs + _perspectiveX;
- int drawY = spr.yoffs + _perspectiveY;
+ // LASER sprite offsets are authored in screen space.
+ int drawX = spr.xoffs;
+ int drawY = spr.yoffs;
renderSprite(dst, pitch, width, height, drawX, drawY, spr);
}
}
@@ -199,92 +270,12 @@ void InsaneRebel1::renderLaserShots(byte *dst, int pitch, int width, int height)
// drawFontBankString â Simplified version of FUN_221B7 (0x221B7).
// Original is a multi-font markup-capable renderer; this uses a single font bank.
void InsaneRebel1::drawFontBankString(byte *dst, int pitch, int width, int height, int x, int y, const char *text) {
- if (!dst || !text || _hudFontBank.numSprites <= 0)
- return;
-
- for (int i = 0; text[i] != '\0'; i++) {
- const byte ch = (byte)text[i];
-
- if (ch == ' ') {
- x += 6;
- continue;
- }
-
- // RA1 font renderer indexes printable characters from '!' (0x21), not raw ASCII.
- if (ch < 0x21) {
- x += 4;
- continue;
- }
- const int fontIdx = (int)ch - 0x21;
- if (fontIdx < 0 || fontIdx >= _hudFontBank.numSprites) {
- x += 4;
- continue;
- }
-
- const RA1Sprite &glyph = _hudFontBank.sprites[fontIdx];
- const int gw = glyph.width;
- const int gh = glyph.height;
- const int gx = x + glyph.xoffs;
- const int gy = y + glyph.yoffs;
- const uint64 glyphPixels = (uint64)gw * (uint64)gh;
- if (!glyph.data || gw <= 0 || gh <= 0 || glyphPixels == 0 || glyphPixels > 0x10000) {
- x += 4;
- continue;
- }
- if (!(_hudFontBank.decodedData && _hudFontBank.decodedSize > 0)) {
- x += 4;
- continue;
- }
- const byte *bankStart = _hudFontBank.decodedData;
- const byte *bankEnd = _hudFontBank.decodedData + _hudFontBank.decodedSize;
- if (glyph.data < bankStart || glyph.data >= bankEnd || glyph.data + glyphPixels > bankEnd) {
- x += 4;
- continue;
- }
-
- for (int py = 0; py < gh; py++) {
- const int sy = gy + py;
- if (sy < 0 || sy >= height)
- continue;
- for (int px = 0; px < gw; px++) {
- const int sx = gx + px;
- if (sx < 0 || sx >= width)
- continue;
- const byte pixel = glyph.data[py * gw + px];
- if (pixel != 0)
- dst[sy * pitch + sx] = pixel;
- }
- }
-
- x += gw > 0 ? gw : 4;
- }
+ drawBankString(_hudFontBank, dst, pitch, width, height, x, y, text);
}
// getFontBankStringWidth â Pre-pass width calculation from FUN_221B7 (0x221B7).
int InsaneRebel1::getFontBankStringWidth(const char *text) {
- if (!text || _hudFontBank.numSprites <= 0)
- return 0;
-
- int w = 0;
- for (int i = 0; text[i] != '\0'; i++) {
- const byte ch = (byte)text[i];
- if (ch == ' ') {
- w += 6;
- continue;
- }
- if (ch < 0x21) {
- w += 4;
- continue;
- }
- const int fontIdx = (int)ch - 0x21;
- if (fontIdx < 0 || fontIdx >= _hudFontBank.numSprites) {
- w += 4;
- continue;
- }
- const RA1Sprite &glyph = _hudFontBank.sprites[fontIdx];
- w += glyph.width > 0 ? glyph.width : 4;
- }
- return w;
+ return getBankStringWidth(_hudFontBank, text);
}
// renderShip â Ship sprite rendering from FUN_1DEB5 (0x1DEB5) at LAB_1e2b2.
@@ -300,11 +291,10 @@ void InsaneRebel1::renderShip(byte *dst, int pitch, int width, int height) {
const RA1Sprite &spr = _shipBank.sprites[_shipDirIndex];
- // Position: game coords â screen coords via perspective transform
- // Adapted from RA2's renderHandler7Ship:
- // shipCenterX = (shipX - center) + perspX + screenCenterX
- int drawX = (_shipPosX - kRA1CenterX) + _perspectiveX + kRA1CenterX - spr.width / 2;
- int drawY = (_shipPosY - kRA1CenterY) + _perspectiveY + kRA1CenterY - spr.height / 2;
+ // FUN_1DEB5 draws at (_74B6 + _74BA, _74B8 + _74BC).
+ // In the current mapping, _shipPosX/_shipPosY already store that screen position.
+ int drawX = _shipPosX - spr.width / 2;
+ int drawY = _shipPosY - spr.height / 2;
renderSprite(dst, pitch, width, height, drawX, drawY, spr);
}
@@ -315,9 +305,9 @@ void InsaneRebel1::renderExplosions(byte *dst, int pitch, int width, int height)
if (_bangBank.numSprites <= 0)
return;
- // Ship screen center position (matches assembly: DAT_74b6+DAT_74ba, DAT_74b8+DAT_74bc)
- int shipScreenX = (_shipPosX - kRA1CenterX) + _perspectiveX + kRA1CenterX;
- int shipScreenY = (_shipPosY - kRA1CenterY) + _perspectiveY + kRA1CenterY;
+ // Ship screen center position (matches assembly DAT_74B6+DAT_74BA, DAT_74B8+DAT_74BC).
+ int shipScreenX = _shipPosX;
+ int shipScreenY = _shipPosY;
// --- Death shake explosions (FUN_1DEB5 LAB_1e0e3) ---
// When dead and deathTimer > 10: random explosion sprites scatter around ship
diff --git a/engines/scumm/insane/insane_rebel1_runlevels.cpp b/engines/scumm/insane/insane_rebel1_runlevels.cpp
index ad0ca095540..92a7c2d02a3 100644
--- a/engines/scumm/insane/insane_rebel1_runlevels.cpp
+++ b/engines/scumm/insane/insane_rebel1_runlevels.cpp
@@ -279,21 +279,29 @@ void InsaneRebel1::runGame() {
switch (menuResult) {
case 1: {
-#if 0 // Skip level 1 for testing â jump straight to level 2
- // Start New Game â play L1NEW briefing then level 1
- playCinematic("LVL1/L1NEW.ANM");
- if (_vm->shouldQuit())
- return;
-
- bool completed = runLevel1();
-#else
- bool completed = true;
-#endif
- if (completed && !_vm->shouldQuit()) {
+ bool completed = false;
+ if (_startLevel <= 1) {
+ // Start from Level 1 (default flow)
+ playCinematic("LVL1/L1NEW.ANM");
+ if (_vm->shouldQuit())
+ return;
+
+ completed = runLevel1();
+ if (completed && !_vm->shouldQuit()) {
+ completed = runLevel2();
+ if (completed) {
+ debug(1, "InsaneRebel1: Level 2 completed!");
+ // TODO: Continue to level 3
+ }
+ }
+ } else if (_startLevel == 2) {
+ // Direct Level 2 start from Level Select menu
+ playCinematic("LVL2/L2NEW.ANM");
+ if (_vm->shouldQuit())
+ return;
completed = runLevel2();
if (completed) {
debug(1, "InsaneRebel1: Level 2 completed!");
- // TODO: Continue to level 3
}
}
_currentLevel = 0;
@@ -304,6 +312,10 @@ void InsaneRebel1::runGame() {
// Game Options
runOptionsMenu();
break;
+ case 3:
+ // Level Select
+ runLevelSelectMenu();
+ break;
case 5:
// Exit
return;
Commit: 3e45c5db23df6d86ca48d494d5359375d6d910f9
https://github.com/scummvm/scummvm/commit/3e45c5db23df6d86ca48d494d5359375d6d910f9
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:30+02:00
Commit Message:
SCUMM: RA1: Render level 2 lasers
Changed paths:
engines/scumm/insane/insane_rebel1_iact.cpp
engines/scumm/insane/insane_rebel1_levels.cpp
engines/scumm/insane/insane_rebel1_render.cpp
engines/scumm/smush/smush_player.cpp
diff --git a/engines/scumm/insane/insane_rebel1_iact.cpp b/engines/scumm/insane/insane_rebel1_iact.cpp
index fd52033ab8a..ada0c41a9c8 100644
--- a/engines/scumm/insane/insane_rebel1_iact.cpp
+++ b/engines/scumm/insane/insane_rebel1_iact.cpp
@@ -240,6 +240,10 @@ void InsaneRebel1::updateShipPhysics() {
// Ship position = averaged input + center offset.
// Viewport = second history buffer for smooth camera scrolling.
void InsaneRebel1::updateAsteroidPhysics() {
+ // Control feel tweak: original uses full 10-sample average in FUN_1CDA7.
+ // We keep the same pipeline but average over fewer samples for responsiveness.
+ const int kAsteroidSmoothWindow = 2;
+
// RA1 FUN_1B297-style per-frame latches for 0x0B sections:
// 0x5D latch 0xFFFF -> bit 0x40 (scripted obstacle/contact)
// 0x5F non-zero + RNG -> bit 0x80 (scripted random hit)
@@ -294,7 +298,10 @@ void InsaneRebel1::updateAsteroidPhysics() {
mouseY = (int16)((mouseY * 200) / _player->_height);
}
int16 inputX = (int16)(mouseX - kRA1CenterX);
- int16 inputY = (int16)(mouseY - kRA1CenterY);
+ // Assembly uses an inverted-Y convention in the averaging path.
+ // In ScummVM screen coords (Y grows downward), convert here so moving
+ // mouse up moves the pointer up on screen.
+ int16 inputY = (int16)(kRA1CenterY - mouseY);
inputX = CLIP<int16>(inputX, -0xA0, 0xA0);
inputY = CLIP<int16>(inputY, -100, 100);
@@ -307,13 +314,13 @@ void InsaneRebel1::updateAsteroidPhysics() {
int sumInputX = 0;
int sumInputY = 0;
- for (int i = 0; i < kInputHistorySize; i++) {
+ for (int i = 0; i < kAsteroidSmoothWindow; i++) {
sumInputX += _inputHistoryX[i];
sumInputY += _inputHistoryY[i];
}
- _avgInputX = (int16)(sumInputX / kInputHistorySize);
- _avgInputY = (int16)(-sumInputY / kInputHistorySize);
+ _avgInputX = (int16)(sumInputX / kAsteroidSmoothWindow);
+ _avgInputY = (int16)(-sumInputY / kAsteroidSmoothWindow);
_avgInputX = CLIP<int16>(_avgInputX, -0xA0, 0xA0);
_avgInputY = CLIP<int16>(_avgInputY, -0x46, 0x41);
@@ -329,13 +336,13 @@ void InsaneRebel1::updateAsteroidPhysics() {
int sumViewX = 0;
int sumViewY = 0;
- for (int i = 0; i < kInputHistorySize; i++) {
+ for (int i = 0; i < kAsteroidSmoothWindow; i++) {
sumViewX += _viewHistoryX[i];
sumViewY += _viewHistoryY[i];
}
- int16 avgViewX = (int16)(sumViewX / kInputHistorySize);
- int16 avgViewY = (int16)(sumViewY / kInputHistorySize);
+ int16 avgViewX = (int16)(sumViewX / kAsteroidSmoothWindow);
+ int16 avgViewY = (int16)(sumViewY / kAsteroidSmoothWindow);
_perspectiveX = CLIP<int16>((int16)((avgViewX >> 1) + 0x20), 0, 0x40);
_perspectiveY = CLIP<int16>((int16)((avgViewY >> 1) + 0x17), 0, 0x2E);
diff --git a/engines/scumm/insane/insane_rebel1_levels.cpp b/engines/scumm/insane/insane_rebel1_levels.cpp
index 9fe8e6e58aa..195c7490265 100644
--- a/engines/scumm/insane/insane_rebel1_levels.cpp
+++ b/engines/scumm/insane/insane_rebel1_levels.cpp
@@ -28,6 +28,9 @@
namespace Scumm {
+// From smush/codec1.cpp
+void smushDecodeRA1Transparent(byte *dst, const byte *src, int left, int top, int width, int height, int pitch);
+
static void decodeBomp(byte *dst, const byte *src, int width, int height, int pitch) {
while (height--) {
byte *dstNext = dst + pitch;
@@ -166,6 +169,12 @@ bool InsaneRebel1::loadRA1Nut(const char *filename, RA1SpriteBank &bank) {
bank.sprites[i].data = decPtr;
decodeBomp(decPtr, fobjData, bank.sprites[i].width,
bank.sprites[i].height, bank.sprites[i].width);
+ } else if (codec == 1) {
+ // RA1 codec 1 in NUTs (e.g. LVL2/L2LASER.NUT): RLE where color 0 is transparent.
+ // Decode into a zero-cleared sprite buffer so skipped pixels stay transparent.
+ bank.sprites[i].data = decPtr;
+ smushDecodeRA1Transparent(decPtr, fobjData, 0, 0,
+ bank.sprites[i].width, bank.sprites[i].height, bank.sprites[i].width);
} else {
bank.sprites[i].width = 0;
bank.sprites[i].height = 0;
diff --git a/engines/scumm/insane/insane_rebel1_render.cpp b/engines/scumm/insane/insane_rebel1_render.cpp
index 6f37737a2ce..9829bd4f025 100644
--- a/engines/scumm/insane/insane_rebel1_render.cpp
+++ b/engines/scumm/insane/insane_rebel1_render.cpp
@@ -116,14 +116,111 @@ static int getBankStringWidth(const RA1SpriteBank &bank, const char *text) {
return w;
}
+// FUN_1C794: direction bucket in range -4..4 from two points.
+static int ra1ShotDirection(int16 x1, int16 y1, int16 x2, int16 y2) {
+ int dx = x2 - x1;
+ int dy = y1 - y2;
+ if (dy < 0) {
+ dy = -dy;
+ dx = -dx;
+ }
+
+ if (dx >= 0) {
+ if (dy > dx * 5)
+ return 0;
+ if (dx * 3 < dy * 2)
+ return 1;
+ if (dx * 2 < dy * 3)
+ return 2;
+ if (dx * 2 < dy * 9)
+ return 3;
+ return 4;
+ }
+
+ const int adx = -dx;
+ if (dy > adx * 5)
+ return 0;
+ if (adx * 3 < dy * 2)
+ return -1;
+ if (adx * 2 < dy * 3)
+ return -2;
+ if (adx * 2 < dy * 9)
+ return -3;
+ return -4;
+}
+
+// FUN_1CDA7 maps abs(FUN_1C794) to sprite base index: <=1 -> 0, ==2 -> 5, else -> 10.
+static int ra1ShotDirectionBucket(int dir) {
+ const int absDir = ABS(dir);
+ if (absDir <= 1)
+ return 0;
+ if (absDir == 2)
+ return 5;
+ return 10;
+}
+
+// Small subset of FUN_20D43 draw flags used by RA1 shot sprites.
+static void renderSpriteWithFlags(byte *dst, int pitch, int width, int height,
+ int x, int y, const RA1Sprite &spr, uint32 flags) {
+ if (!spr.data || spr.width <= 0 || spr.height <= 0)
+ return;
+
+ int drawX = x;
+ int drawY = y;
+ if ((flags & 0x1) == 0) {
+ drawX += spr.xoffs;
+ drawY += spr.yoffs;
+ }
+ if (flags & 0x2) {
+ drawX -= spr.width / 2;
+ drawY -= spr.height / 2;
+ }
+
+ const bool flipX = (flags & 0x2000) != 0;
+ const bool flipY = (flags & 0x4000) != 0;
+
+ int srcOffsetX = 0;
+ int srcOffsetY = 0;
+ int drawW = spr.width;
+ int drawH = spr.height;
+
+ if (drawX < 0) {
+ srcOffsetX = -drawX;
+ drawW += drawX;
+ drawX = 0;
+ }
+ if (drawY < 0) {
+ srcOffsetY = -drawY;
+ drawH += drawY;
+ drawY = 0;
+ }
+ if (drawX + drawW > width)
+ drawW = width - drawX;
+ if (drawY + drawH > height)
+ drawH = height - drawY;
+ if (drawW <= 0 || drawH <= 0)
+ return;
+
+ for (int iy = 0; iy < drawH; iy++) {
+ const int srcY = flipY ? (spr.height - 1 - (srcOffsetY + iy)) : (srcOffsetY + iy);
+ byte *d = dst + (drawY + iy) * pitch + drawX;
+ for (int ix = 0; ix < drawW; ix++) {
+ const int srcX = flipX ? (spr.width - 1 - (srcOffsetX + ix)) : (srcOffsetX + ix);
+ const byte px = spr.data[srcY * spr.width + srcX];
+ if (px != 0)
+ d[ix] = px;
+ }
+ }
+}
+
// procPreRendering â Sets viewport scroll offset before FOBJ decoding (FUN_224FD at 0x224FD).
// For interactive levels, shifts the FOBJ decode position based on mouse-controlled perspective.
void InsaneRebel1::procPreRendering(byte *renderBitmap) {
if (_interactiveVideoActive && _player) {
// FUN_224FD stores absolute 320x200 window origin in a 384x242 frame:
// X in [0..0x40], Y in [0..0x2E], centered at (0x20,0x17).
- // ScummVM presents the full 384x242 frame, so use center-relative delta
- // to avoid exposing black border over cockpit edges.
+ // Apply center-relative deltas so right aim shifts scene left, matching
+ // FUN_223FE/FUN_2245B subtracting _41A0 from world-space X.
_player->_ra1ViewportOffsetX = _perspectiveX - 0x20;
_player->_ra1ViewportOffsetY = _perspectiveY - 0x17;
} else if (_player) {
@@ -243,26 +340,69 @@ void InsaneRebel1::renderGostSlots(byte *dst, int pitch, int width, int height)
}
}
-// renderLaserShots â FUN_1CCA0 visual output. Renders laser sprites from _laserBank
-// when shot slots are active (timer > 0). Each set of 5 sprites is a laser animation
-// converging from the ship toward center screen. Timer 5â1 maps to sprite frames 0â4.
+// renderLaserShots â FUN_1CDA7/FUN_1D79C shot visual path:
+// per active slot, compute left/right direction with FUN_1C794, pick one
+// of 3x5 sprite bands, and render interpolated sprite positions via FUN_20BD3.
void InsaneRebel1::renderLaserShots(byte *dst, int pitch, int width, int height) {
if (_laserBank.numSprites <= 0)
return;
- int spritesPerSet = 5;
+ // DAT_2407 lookup used by FUN_1CDA7/FUN_1D79C for timer 1..5 interpolation.
+ // Entry 0 unused.
+ static const int kShotLerpByTimer[6] = { 0, 8, 7, 6, 4, 0 };
+ const int spritesPerSet = 5;
+ const int leftStartX = 0;
+ const int rightStartX = 0x13F; // 319
+
for (int i = 0; i < kMaxShotSlots; i++) {
if (_shotSlots[i].timer > 0 && _shotSlots[i].timer <= spritesPerSet) {
- int frame = spritesPerSet - _shotSlots[i].timer;
- int sprIdx = frame;
- if (sprIdx >= _laserBank.numSprites)
- continue;
+ const int timer = _shotSlots[i].timer;
+ const int lerp = kShotLerpByTimer[timer];
+ const int frame = spritesPerSet - timer;
+ const int targetX = CLIP<int>(_shipPosX, 0, width - 1);
+ const int targetY = CLIP<int>(_shipPosY, 0, height - 1);
+
+ int leftStartY = 0x96;
+ int rightStartY = 0x96;
+ uint32 leftFlags = 0x83;
+ uint32 rightFlags = 0x2083;
+
+ // FUN_1CDA7 special mode branch (_DAT_75E4 == 1): toggles emitter origin
+ // and flip flags via DAT_2423. Keep behavior for parity when mode is used.
+ if (_flyControlMode == 1) {
+ if (_shotAlternator != 0) {
+ leftFlags = 0x4083;
+ leftStartY = 0;
+ rightStartY = 0x96;
+ } else {
+ rightFlags = 0x6083;
+ leftStartY = 0x96;
+ rightStartY = 0;
+ }
+ if (timer == 1)
+ _shotAlternator = 1 - _shotAlternator;
+ }
- const RA1Sprite &spr = _laserBank.sprites[sprIdx];
- // LASER sprite offsets are authored in screen space.
- int drawX = spr.xoffs;
- int drawY = spr.yoffs;
- renderSprite(dst, pitch, width, height, drawX, drawY, spr);
+ const int dirLeft = ra1ShotDirection(targetX, targetY, leftStartX, leftStartY);
+ const int dirRight = ra1ShotDirection(rightStartX, targetY, targetX, rightStartY);
+ const int bucketLeft = ra1ShotDirectionBucket(dirLeft);
+ const int bucketRight = ra1ShotDirectionBucket(dirRight);
+ const int sprIdxLeft = frame + bucketLeft;
+ const int sprIdxRight = frame + bucketRight;
+
+ const int interpLeftX = leftStartX + (((targetX - leftStartX) * lerp) >> 3);
+ const int interpLeftY = leftStartY + (((targetY - leftStartY) * lerp) >> 3);
+ const int interpRightX = rightStartX + (((targetX - rightStartX) * lerp) >> 3);
+ const int interpRightY = rightStartY + (((targetY - rightStartY) * lerp) >> 3);
+
+ if (sprIdxLeft >= 0 && sprIdxLeft < _laserBank.numSprites) {
+ renderSpriteWithFlags(dst, pitch, width, height,
+ interpLeftX, interpLeftY, _laserBank.sprites[sprIdxLeft], leftFlags);
+ }
+ if (sprIdxRight >= 0 && sprIdxRight < _laserBank.numSprites) {
+ renderSpriteWithFlags(dst, pitch, width, height,
+ interpRightX, interpRightY, _laserBank.sprites[sprIdxRight], rightFlags);
+ }
}
}
}
diff --git a/engines/scumm/smush/smush_player.cpp b/engines/scumm/smush/smush_player.cpp
index ff01333384f..5743f24c858 100644
--- a/engines/scumm/smush/smush_player.cpp
+++ b/engines/scumm/smush/smush_player.cpp
@@ -462,10 +462,6 @@ void SmushPlayer::handleFetch(int32 subSize, Common::SeekableReadStream &b) {
int left = _storedFobjLeft + fetchX;
int top = _storedFobjTop + fetchY;
- // Apply the same interactive viewport offset used for regular FOBJ chunks.
- left -= _ra1ViewportOffsetX;
- top -= _ra1ViewportOffsetY;
-
debug("RA1 FTCH: frame=%d id=0x%08x pos=(%d,%d) using stored FOBJ codec=%d size=%dx%d",
_frame, fetchId, left, top, storedCodec, _storedFobjWidth, _storedFobjHeight);
decodeFrameObject(storedCodec, _storedFobjData, left, top,
Commit: a854f3fd00ebd4c48c4b05a0e6a65fda3bf2cf6f
https://github.com/scummvm/scummvm/commit/a854f3fd00ebd4c48c4b05a0e6a65fda3bf2cf6f
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:30+02:00
Commit Message:
SCUMM: RA1: Fix level 1 targeting pipeline
Changed paths:
engines/scumm/insane/insane_rebel1.cpp
engines/scumm/insane/insane_rebel1.h
engines/scumm/insane/insane_rebel1_iact.cpp
engines/scumm/insane/insane_rebel1_render.cpp
diff --git a/engines/scumm/insane/insane_rebel1.cpp b/engines/scumm/insane/insane_rebel1.cpp
index 64c5c4a7b6f..225b0678a09 100644
--- a/engines/scumm/insane/insane_rebel1.cpp
+++ b/engines/scumm/insane/insane_rebel1.cpp
@@ -107,6 +107,7 @@ InsaneRebel1::InsaneRebel1(ScummEngine_v7 *scumm) : Insane(), _vm(scumm) {
_currentLevel = 0;
_flyControlMode = 0;
+ _activeGameOpcode = 0;
_health = kMaxHealth;
_lives = 3;
diff --git a/engines/scumm/insane/insane_rebel1.h b/engines/scumm/insane/insane_rebel1.h
index be8cdb994de..3fd9d01cf5d 100644
--- a/engines/scumm/insane/insane_rebel1.h
+++ b/engines/scumm/insane/insane_rebel1.h
@@ -199,6 +199,9 @@ private:
// Control mode (from GAME opcode 0x5E)
int16 _flyControlMode;
+ // Last per-frame GAME movement handler opcode (0x07/0x08/0x09/0x0A/0x0B/0x1A).
+ // Used to mirror assembly handler-specific overlay pipeline behavior.
+ uint16 _activeGameOpcode;
// Difficulty (0=easy, 1=normal, 2=hard) â matches original DAT_22BC
int _difficulty;
diff --git a/engines/scumm/insane/insane_rebel1_iact.cpp b/engines/scumm/insane/insane_rebel1_iact.cpp
index ada0c41a9c8..34a52224555 100644
--- a/engines/scumm/insane/insane_rebel1_iact.cpp
+++ b/engines/scumm/insane/insane_rebel1_iact.cpp
@@ -106,35 +106,35 @@ void InsaneRebel1::updateShipPhysics() {
{
bool hasZoneHit = (_damageFlags & 0x10) != 0;
- if (_shipPosX > _corridorRightX) {
- _posAccumX = (_corridorRightX - kRA1CenterX) << 8;
- _shipPosX = _corridorRightX;
- if (!hasZoneHit) {
- if (_rollAccum > -0x100)
+ if (_shipPosX > _corridorRightX) {
+ _posAccumX = (int32)(_corridorRightX - kRA1CenterX) * 0x100;
+ _shipPosX = _corridorRightX;
+ if (!hasZoneHit) {
+ if (_rollAccum > -0x100)
_rollAccum = -0x100; // Push left
_damageFlags |= 0x02; // Right wall
}
- }
- if (_shipPosX < _corridorLeftX) {
- _posAccumX = (_corridorLeftX - kRA1CenterX) << 8;
- _shipPosX = _corridorLeftX;
- if (!hasZoneHit) {
- if (_rollAccum < 0x100)
+ }
+ if (_shipPosX < _corridorLeftX) {
+ _posAccumX = (int32)(_corridorLeftX - kRA1CenterX) * 0x100;
+ _shipPosX = _corridorLeftX;
+ if (!hasZoneHit) {
+ if (_rollAccum < 0x100)
_rollAccum = 0x100; // Push right
_damageFlags |= 0x04; // Left wall
}
- }
- if (_shipPosY < _corridorTopY) {
- _posAccumY = ((_corridorTopY - kRA1CenterY) << 8) + 0x100;
- _shipPosY = _corridorTopY;
- if (!hasZoneHit)
- _damageFlags |= 0x01;
- }
- if (_shipPosY > _corridorBottomY) {
- _posAccumY = ((_corridorBottomY - kRA1CenterY) << 8) - 0x100;
- _shipPosY = _corridorBottomY;
- if (!hasZoneHit)
- _damageFlags |= 0x08;
+ }
+ if (_shipPosY < _corridorTopY) {
+ _posAccumY = (int32)(_corridorTopY - kRA1CenterY) * 0x100 + 0x100;
+ _shipPosY = _corridorTopY;
+ if (!hasZoneHit)
+ _damageFlags |= 0x01;
+ }
+ if (_shipPosY > _corridorBottomY) {
+ _posAccumY = (int32)(_corridorBottomY - kRA1CenterY) * 0x100 - 0x100;
+ _shipPosY = _corridorBottomY;
+ if (!hasZoneHit)
+ _damageFlags |= 0x08;
}
}
@@ -416,6 +416,7 @@ void InsaneRebel1::handleGameChunk(int32 subSize, Common::SeekableReadStream &b)
// Keep a conservative default mode after reset.
_flyControlMode = 0;
+ _activeGameOpcode = 0;
debug(5, "RA1 GAME 0x5E: reset state field1=%d", (int32)param1);
break;
@@ -430,6 +431,7 @@ void InsaneRebel1::handleGameChunk(int32 subSize, Common::SeekableReadStream &b)
break;
case 0x07:
+ _activeGameOpcode = 0x07;
// Per-frame corridor data: f1=frame counter, f2=max frames, f3=drift bias, f4=unused
// f1 is the original's _DAT_7740 (game frame counter)
// f3 is the drift/wind parameter combined with tuning table
@@ -479,6 +481,7 @@ void InsaneRebel1::handleGameChunk(int32 subSize, Common::SeekableReadStream &b)
break;
case 0x0B:
+ _activeGameOpcode = 0x0B;
// Asteroid/surface per-frame handler (FUN_1CDA7).
// field1 = frame counter, field2 = max frames
_gameCounter = param1;
@@ -517,8 +520,12 @@ void InsaneRebel1::handleGameChunk(int32 subSize, Common::SeekableReadStream &b)
}
break;
- case 0x08: case 0x09: case 0x0A:
- case 0x19: case 0x1A:
+ case 0x08:
+ case 0x09:
+ case 0x0A:
+ case 0x19:
+ case 0x1A:
+ _activeGameOpcode = (uint16)opcode;
if (subSize >= 20) {
uint32 param2 = b.readUint32BE();
uint32 param3 = b.readUint32BE();
diff --git a/engines/scumm/insane/insane_rebel1_render.cpp b/engines/scumm/insane/insane_rebel1_render.cpp
index 9829bd4f025..6d819f1b2f1 100644
--- a/engines/scumm/insane/insane_rebel1_render.cpp
+++ b/engines/scumm/insane/insane_rebel1_render.cpp
@@ -219,9 +219,12 @@ void InsaneRebel1::procPreRendering(byte *renderBitmap) {
if (_interactiveVideoActive && _player) {
// FUN_224FD stores absolute 320x200 window origin in a 384x242 frame:
// X in [0..0x40], Y in [0..0x2E], centered at (0x20,0x17).
- // Apply center-relative deltas so right aim shifts scene left, matching
- // FUN_223FE/FUN_2245B subtracting _41A0 from world-space X.
- _player->_ra1ViewportOffsetX = _perspectiveX - 0x20;
+ // Keep L2 on assembly-oriented X mapping used by FUN_223FE/FUN_2245B
+ // emulation; preserve legacy L1 mapping to avoid flight-section regressions.
+ if (_currentLevel == 1)
+ _player->_ra1ViewportOffsetX = _perspectiveX - 0x20;
+ else
+ _player->_ra1ViewportOffsetX = 0x20 - _perspectiveX;
_player->_ra1ViewportOffsetY = _perspectiveY - 0x17;
} else if (_player) {
_player->_ra1ViewportOffsetX = 0;
@@ -260,18 +263,31 @@ void InsaneRebel1::procPostRendering(byte *renderBitmap, int32 codecparam, int32
}
}
- // FUN_1CDA7 (0x0B) still executes targeting/shot overlay pipeline
- // (FUN_1C940, FUN_1CCA0, FUN_1C9CD, FUN_1CB22) while alive.
- processShot();
- for (int i = 0; i < kMaxShotSlots; i++) {
- if (_shotSlots[i].timer > 0)
- _shotSlots[i].timer--;
+ // Assembly dispatch (FUN_1BE1B) only runs the targeting/shot overlay pipeline
+ // in handlers 0x09/0x0A/0x0B/0x1A (FUN_1DABB/FUN_1D79C/FUN_1CDA7/FUN_1D57E).
+ const bool hasTargetingPipeline =
+ (_activeGameOpcode == 0x09 || _activeGameOpcode == 0x0A ||
+ _activeGameOpcode == 0x0B || _activeGameOpcode == 0x1A);
+ if (hasTargetingPipeline) {
+ processShot();
+ for (int i = 0; i < kMaxShotSlots; i++) {
+ if (_shotSlots[i].timer > 0)
+ _shotSlots[i].timer--;
+ }
+ renderLaserShots(renderBitmap, pitch, width, height);
+ renderGostSlots(renderBitmap, pitch, width, height);
+ renderTargeting(renderBitmap, pitch, width, height);
+ } else {
+ // Keep lock/target accumulators quiescent when current handler doesn't
+ // execute FUN_1C940/FUN_1CCA0/FUN_1C9CD/FUN_1CB22.
+ _targetProximity = 0;
+ _prevTargetProx = 0;
+ _targetCount = 0;
+ _prevTargetCount = 0;
+ _lastHitTarget = 0;
}
- renderLaserShots(renderBitmap, pitch, width, height);
renderExplosions(renderBitmap, pitch, width, height);
- renderGostSlots(renderBitmap, pitch, width, height);
- renderTargeting(renderBitmap, pitch, width, height);
renderHUD(renderBitmap, pitch, width, height);
}
Commit: 1f6fdf96c8fb2ffb26893301e7f2098286a583e5
https://github.com/scummvm/scummvm/commit/1f6fdf96c8fb2ffb26893301e7f2098286a583e5
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:31+02:00
Commit Message:
SCUMM: RA1: Fix interactive video ghosting
Changed paths:
engines/scumm/insane/insane_rebel1.h
engines/scumm/insane/insane_rebel1_render.cpp
engines/scumm/smush/smush_player.cpp
engines/scumm/smush/smush_player.h
diff --git a/engines/scumm/insane/insane_rebel1.h b/engines/scumm/insane/insane_rebel1.h
index 3fd9d01cf5d..1599665b406 100644
--- a/engines/scumm/insane/insane_rebel1.h
+++ b/engines/scumm/insane/insane_rebel1.h
@@ -83,6 +83,8 @@ public:
void procSKIP(int32 subSize, Common::SeekableReadStream &b) override;
void handleGameChunk(int32 subSize, Common::SeekableReadStream &b);
+ bool isInteractiveVideoActive() const { return _interactiveVideoActive; }
+ int getCurrentLevel() const { return _currentLevel; }
// Game flow (matching original at 0x15597)
void runGame();
diff --git a/engines/scumm/insane/insane_rebel1_render.cpp b/engines/scumm/insane/insane_rebel1_render.cpp
index 6d819f1b2f1..9162a81ee09 100644
--- a/engines/scumm/insane/insane_rebel1_render.cpp
+++ b/engines/scumm/insane/insane_rebel1_render.cpp
@@ -213,19 +213,15 @@ static void renderSpriteWithFlags(byte *dst, int pitch, int width, int height,
}
}
-// procPreRendering â Sets viewport scroll offset before FOBJ decoding (FUN_224FD at 0x224FD).
-// For interactive levels, shifts the FOBJ decode position based on mouse-controlled perspective.
+// procPreRendering â Sets viewport window offset (FUN_224FD at 0x224FD).
+// RA1 decodes FOBJs at chunk coordinates, then displays a scrolled 320x200
+// window inside the 384x242 framebuffer.
void InsaneRebel1::procPreRendering(byte *renderBitmap) {
if (_interactiveVideoActive && _player) {
// FUN_224FD stores absolute 320x200 window origin in a 384x242 frame:
// X in [0..0x40], Y in [0..0x2E], centered at (0x20,0x17).
- // Keep L2 on assembly-oriented X mapping used by FUN_223FE/FUN_2245B
- // emulation; preserve legacy L1 mapping to avoid flight-section regressions.
- if (_currentLevel == 1)
- _player->_ra1ViewportOffsetX = _perspectiveX - 0x20;
- else
- _player->_ra1ViewportOffsetX = 0x20 - _perspectiveX;
- _player->_ra1ViewportOffsetY = _perspectiveY - 0x17;
+ _player->_ra1ViewportOffsetX = _perspectiveX;
+ _player->_ra1ViewportOffsetY = _perspectiveY;
} else if (_player) {
_player->_ra1ViewportOffsetX = 0;
_player->_ra1ViewportOffsetY = 0;
@@ -529,9 +525,19 @@ void InsaneRebel1::renderHUD(byte *dst, int pitch, int width, int height) {
const RA1Sprite &bar = _displayBank.sprites[0];
// DISPLAY.NUT sprite is 320Ã19 at xoffs=0, yoffs=176 in the original game.
- // Video FOBJs fill the full 384Ã242 buffer from (0,0), so use sprite offsets directly.
- int hudX = bar.xoffs;
- int hudY = bar.yoffs;
+ // FUN_224FD (0x224FD) sets the 320x200 window origin inside the 384x242 buffer.
+ // FUN_1BBCB (0x1BBCB) HUD coordinates are screen-space, so when we emulate
+ // perspective via source-window cropping, anchor HUD at window origin to keep
+ // it fixed on-screen.
+ int hudOriginX = 0;
+ int hudOriginY = 0;
+ if (_interactiveVideoActive && _player) {
+ hudOriginX = _player->_ra1ViewportOffsetX;
+ hudOriginY = _player->_ra1ViewportOffsetY;
+ }
+
+ int hudX = hudOriginX + bar.xoffs;
+ int hudY = hudOriginY + bar.yoffs;
// Draw the status bar background with transparency (pixel 0 = transparent)
if (bar.data && bar.width > 0 && bar.height > 0) {
diff --git a/engines/scumm/smush/smush_player.cpp b/engines/scumm/smush/smush_player.cpp
index 5743f24c858..34fadefaf20 100644
--- a/engines/scumm/smush/smush_player.cpp
+++ b/engines/scumm/smush/smush_player.cpp
@@ -272,6 +272,9 @@ SmushPlayer::SmushPlayer(ScummEngine_v7 *scumm, IMuseDigital *imuseDigital, Insa
_frameBuffer = nullptr;
_specialBuffer = nullptr;
_specialBufferSize = 0;
+ _ra1CleanFrame = nullptr;
+ _ra1CleanFrameSize = 0;
+ _ra1HasCleanFrame = false;
_seekPos = -1;
@@ -328,6 +331,10 @@ SmushPlayer::~SmushPlayer() {
_frameBuffer = nullptr;
free(_specialBuffer);
_specialBuffer = nullptr;
+ free(_ra1CleanFrame);
+ _ra1CleanFrame = nullptr;
+ _ra1CleanFrameSize = 0;
+ _ra1HasCleanFrame = false;
}
void SmushPlayer::init(int32 speed) {
@@ -342,6 +349,7 @@ void SmushPlayer::init(int32 speed) {
_scrollX = 0;
_scrollY = 0;
_fastForwardToFrame = 0;
+ _ra1HasCleanFrame = false;
_vm->_smushVideoShouldFinish = false;
_vm->_smushActive = true;
@@ -384,6 +392,10 @@ void SmushPlayer::release() {
free(_specialBuffer);
_specialBuffer = nullptr;
+ free(_ra1CleanFrame);
+ _ra1CleanFrame = nullptr;
+ _ra1CleanFrameSize = 0;
+ _ra1HasCleanFrame = false;
if (isRA2()) {
ra2ReleaseVideo();
@@ -1370,14 +1382,6 @@ void SmushPlayer::handleFrameObject(int32 subSize, Common::SeekableReadStream &b
_frame, codec, left, top, width, height, subSize - 14, _storeFrame, _width, _height);
}
if (isRA1()) {
- // Viewport scroll for interactive gameplay (FUN_224FD sets _41A0/_41A2).
- // Original renders FOBJs at raw positions, then displays a 320x200 window
- // starting at (perspectiveX, perspectiveY) within the 384x242 buffer.
- // FUN_22605/FUN_2289D subtract _41A0 from FOBJ X coords during rendering.
- // We simulate the display window shift by subtracting from decode position.
- left -= _ra1ViewportOffsetX;
- top -= _ra1ViewportOffsetY;
-
debug("RA1 FOBJ: frame=%d codec=%d pos=(%d,%d) size=%dx%d dataSize=%d storeFrame=%d",
_frame, codec, left, top, width, height, subSize - 14, _storeFrame);
}
@@ -1424,17 +1428,37 @@ void SmushPlayer::handleFrame(int32 frameSize, Common::SeekableReadStream &b) {
if (isRA2())
_hasFrameFobjForGost = false;
+ bool interactiveRA1 = false;
+ bool forceInteractiveClearRA1 = false;
+ if (isRA1() && _insane) {
+ InsaneRebel1 *rebel1 = static_cast<InsaneRebel1 *>(_insane);
+ interactiveRA1 = rebel1->isInteractiveVideoActive();
+ // Level 2 asteroid stream composes many partial codec1/2+FTCH layers.
+ // Clear per frame for this mode to avoid stale-trail ghosting.
+ forceInteractiveClearRA1 = interactiveRA1 && (rebel1->getCurrentLevel() == 1);
+ }
+
+ // Keep the previous decoded frame (without post-render overlays) as delta source.
+ // FUN_1FDBC (0x1FDBC) decodes frame data first; gameplay overlays from
+ // FUN_1BBCB/FUN_1CB22/FUN_1CDA7 are presentation-stage effects.
+ if (isRA1() && interactiveRA1 && !forceInteractiveClearRA1 &&
+ _ra1HasCleanFrame && _ra1CleanFrame &&
+ _dst && _width > 0 && _height > 0) {
+ const int frameBytes = _width * _height;
+ if (_ra1CleanFrameSize >= frameBytes)
+ memcpy(_dst, _ra1CleanFrame, frameBytes);
+ }
+
if (_insanity) {
_vm->_insane->procPreRendering(_dst);
}
- // RA1: Clear framebuffer before each frame's first draw operation.
- // Matches FFmpeg's first_fob memset: process_frame_obj() zeroes fbuf
- // before the first FOBJ (or FTCH re-decode) of each frame. Without this,
- // codec 1's transparent pixels (pixel 0 = skip) show previous frame
- // content, causing ghost trails on the Star Wars opening crawl.
+ // RA1: gameplay/interactivity relies on previous-frame history (delta codecs),
+ // but passive cinematics in current implementation need a per-frame clear to
+ // avoid trails in intro/text sequences.
if (isRA1() && _dst && _width > 0 && _height > 0) {
- memset(_dst, 0, _width * _height);
+ if (!interactiveRA1 || forceInteractiveClearRA1)
+ memset(_dst, 0, _width * _height);
}
while (frameSize > 0) {
@@ -1667,6 +1691,24 @@ void SmushPlayer::handleFrame(int32 frameSize, Common::SeekableReadStream &b) {
handleFrameObject(_ra1ObjOverlayDataSize, overlayStream);
}
+ // Snapshot decoded frame before post-render overlays so next frame starts from
+ // clean video data (avoids ghost trails from moving HUD/cursor overlays).
+ if (isRA1() && interactiveRA1 && !forceInteractiveClearRA1 &&
+ _dst && _width > 0 && _height > 0) {
+ const int frameBytes = _width * _height;
+ byte *newClean = (byte *)realloc(_ra1CleanFrame, frameBytes);
+ if (newClean != nullptr) {
+ _ra1CleanFrame = newClean;
+ _ra1CleanFrameSize = frameBytes;
+ memcpy(_ra1CleanFrame, _dst, frameBytes);
+ _ra1HasCleanFrame = true;
+ } else {
+ _ra1HasCleanFrame = false;
+ }
+ } else if (isRA1() && forceInteractiveClearRA1) {
+ _ra1HasCleanFrame = false;
+ }
+
if (_insanity) {
_vm->_insane->procPostRendering(_dst, 0, 0, 0, _frame, _nbframes-1);
}
@@ -2247,8 +2289,8 @@ void SmushPlayer::play(const char *filename, int32 speed, int32 offset, int32 st
continue;
}
- const int srcX = CLIP(_scrollX, 0, _width - 1);
- const int srcY = CLIP(_scrollY, 0, _height - 1);
+ const int srcX = CLIP(_scrollX + _ra1ViewportOffsetX, 0, _width - 1);
+ const int srcY = CLIP(_scrollY + _ra1ViewportOffsetY, 0, _height - 1);
frameWidth = MIN(_width - srcX, _vm->_screenWidth);
frameHeight = MIN(_height - srcY, _vm->_screenHeight);
diff --git a/engines/scumm/smush/smush_player.h b/engines/scumm/smush/smush_player.h
index 6a10be52e04..4c01847882c 100644
--- a/engines/scumm/smush/smush_player.h
+++ b/engines/scumm/smush/smush_player.h
@@ -156,6 +156,9 @@ private:
byte *_frameBuffer;
byte *_specialBuffer;
int _specialBufferSize;
+ byte *_ra1CleanFrame;
+ int32 _ra1CleanFrameSize;
+ bool _ra1HasCleanFrame;
// RA2: Raw FOBJ data stored by STOR chunk (matching original DAT_00482c04).
// The original stores raw FOBJ chunk data and re-decodes it on FTCH with
Commit: 43a9aeb6ee8b0248372b6204bb55f6141ea78533
https://github.com/scummvm/scummvm/commit/43a9aeb6ee8b0248372b6204bb55f6141ea78533
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:31+02:00
Commit Message:
SCUMM: RA1: Fix asteroid view offsets
Changed paths:
engines/scumm/insane/insane_rebel1.h
engines/scumm/insane/insane_rebel1_render.cpp
engines/scumm/smush/smush_player.cpp
diff --git a/engines/scumm/insane/insane_rebel1.h b/engines/scumm/insane/insane_rebel1.h
index 1599665b406..eba6366f77b 100644
--- a/engines/scumm/insane/insane_rebel1.h
+++ b/engines/scumm/insane/insane_rebel1.h
@@ -85,6 +85,7 @@ public:
void handleGameChunk(int32 subSize, Common::SeekableReadStream &b);
bool isInteractiveVideoActive() const { return _interactiveVideoActive; }
int getCurrentLevel() const { return _currentLevel; }
+ uint16 getActiveGameOpcode() const { return _activeGameOpcode; }
// Game flow (matching original at 0x15597)
void runGame();
diff --git a/engines/scumm/insane/insane_rebel1_render.cpp b/engines/scumm/insane/insane_rebel1_render.cpp
index 9162a81ee09..664adcabcfe 100644
--- a/engines/scumm/insane/insane_rebel1_render.cpp
+++ b/engines/scumm/insane/insane_rebel1_render.cpp
@@ -533,7 +533,11 @@ void InsaneRebel1::renderHUD(byte *dst, int pitch, int width, int height) {
int hudOriginY = 0;
if (_interactiveVideoActive && _player) {
hudOriginX = _player->_ra1ViewportOffsetX;
- hudOriginY = _player->_ra1ViewportOffsetY;
+ // Asteroid path (opcode 0x0B / FUN_1CDA7) applies Y correction through
+ // FUN_223FE coordinate transforms in the original renderer, not as a
+ // simple global framebuffer Y window shift.
+ if (_activeGameOpcode != 0x0B)
+ hudOriginY = _player->_ra1ViewportOffsetY;
}
int hudX = hudOriginX + bar.xoffs;
diff --git a/engines/scumm/smush/smush_player.cpp b/engines/scumm/smush/smush_player.cpp
index 34fadefaf20..db8e95be5f1 100644
--- a/engines/scumm/smush/smush_player.cpp
+++ b/engines/scumm/smush/smush_player.cpp
@@ -2289,8 +2289,19 @@ void SmushPlayer::play(const char *filename, int32 speed, int32 offset, int32 st
continue;
}
- const int srcX = CLIP(_scrollX + _ra1ViewportOffsetX, 0, _width - 1);
- const int srcY = CLIP(_scrollY + _ra1ViewportOffsetY, 0, _height - 1);
+ int ra1ViewX = _ra1ViewportOffsetX;
+ int ra1ViewY = _ra1ViewportOffsetY;
+ if (_insane) {
+ InsaneRebel1 *rebel1 = static_cast<InsaneRebel1 *>(_insane);
+ // Opcode 0x0B (FUN_1CDA7) uses FUN_223FE for Y compensation on
+ // transformed objects. A global framebuffer Y window shift causes
+ // floating HUD/perspective artifacts in L2 with current renderer.
+ if (rebel1->isInteractiveVideoActive() && rebel1->getActiveGameOpcode() == 0x0B)
+ ra1ViewY = 0;
+ }
+
+ const int srcX = CLIP(_scrollX + ra1ViewX, 0, _width - 1);
+ const int srcY = CLIP(_scrollY + ra1ViewY, 0, _height - 1);
frameWidth = MIN(_width - srcX, _vm->_screenWidth);
frameHeight = MIN(_height - srcY, _vm->_screenHeight);
Commit: 19140d2d213510b2dd7fcbd43f8f19c9376e5f71
https://github.com/scummvm/scummvm/commit/19140d2d213510b2dd7fcbd43f8f19c9376e5f71
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:31+02:00
Commit Message:
SCUMM: RA1: Clear oversized frame buffers
Changed paths:
engines/scumm/smush/smush_player.cpp
diff --git a/engines/scumm/smush/smush_player.cpp b/engines/scumm/smush/smush_player.cpp
index db8e95be5f1..97ddcc9cdfc 100644
--- a/engines/scumm/smush/smush_player.cpp
+++ b/engines/scumm/smush/smush_player.cpp
@@ -1433,9 +1433,12 @@ void SmushPlayer::handleFrame(int32 frameSize, Common::SeekableReadStream &b) {
if (isRA1() && _insane) {
InsaneRebel1 *rebel1 = static_cast<InsaneRebel1 *>(_insane);
interactiveRA1 = rebel1->isInteractiveVideoActive();
- // Level 2 asteroid stream composes many partial codec1/2+FTCH layers.
- // Clear per frame for this mode to avoid stale-trail ghosting.
- forceInteractiveClearRA1 = interactiveRA1 && (rebel1->getCurrentLevel() == 1);
+ const uint16 activeOpcode = rebel1->getActiveGameOpcode();
+ // Opcode 0x0B path (FUN_1CDA7) uses heavy partial-layer composition
+ // (codec1/2 + FTCH). Force clear there to avoid stale-trail ghosting.
+ // Keep a conservative fallback for early L2 frames before first 0x0B arrives.
+ forceInteractiveClearRA1 = interactiveRA1 &&
+ (activeOpcode == 0x0B || (activeOpcode == 0 && rebel1->getCurrentLevel() == 1));
}
// Keep the previous decoded frame (without post-render overlays) as delta source.
Commit: 9a2fd5d3e2e40ea52aeab46704ea41b713ef31f0
https://github.com/scummvm/scummvm/commit/9a2fd5d3e2e40ea52aeab46704ea41b713ef31f0
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:32+02:00
Commit Message:
SCUMM: RA1: Fix UI rendering
Changed paths:
engines/scumm/insane/insane_rebel1_render.cpp
diff --git a/engines/scumm/insane/insane_rebel1_render.cpp b/engines/scumm/insane/insane_rebel1_render.cpp
index 664adcabcfe..8a814695376 100644
--- a/engines/scumm/insane/insane_rebel1_render.cpp
+++ b/engines/scumm/insane/insane_rebel1_render.cpp
@@ -116,6 +116,15 @@ static int getBankStringWidth(const RA1SpriteBank &bank, const char *text) {
return w;
}
+// Approximate FUN_221B7/FUN_20BD3 space-advance behavior from available NUT glyphs.
+// The original reads per-font space width from metadata tables and caps it to 8.
+static int getBankSpaceAdvance(const RA1SpriteBank &bank) {
+ const int exclWidth = getBankStringWidth(bank, "!");
+ if (exclWidth <= 0)
+ return 6;
+ return MIN(exclWidth, 8);
+}
+
// FUN_1C794: direction bucket in range -4..4 from two points.
static int ra1ShotDirection(int16 x1, int16 y1, int16 x2, int16 y2) {
int dx = x2 - x1;
@@ -419,15 +428,84 @@ void InsaneRebel1::renderLaserShots(byte *dst, int pitch, int width, int height)
}
}
-// drawFontBankString â Simplified version of FUN_221B7 (0x221B7).
-// Original is a multi-font markup-capable renderer; this uses a single font bank.
+// drawFontBankString â FUN_221B7 (0x221B7), partial parity:
+// supports '<'/'>' layer markup and layer-2 space handling used by RA1 HUD/targeting strings.
void InsaneRebel1::drawFontBankString(byte *dst, int pitch, int width, int height, int x, int y, const char *text) {
- drawBankString(_hudFontBank, dst, pitch, width, height, x, y, text);
+ if (!text || !dst)
+ return;
+
+ // Original FUN_221B7 layer mapping is table-driven at DAT_2D56 (0x406-byte entries).
+ // Current RA1 integration mirrors the gameplay-relevant subset:
+ // layer 0/1 -> HUD font bank
+ // layer 2+ -> TECH font bank
+ int layer = 0;
+ for (int i = 0; text[i] != '\0'; i++) {
+ char ch = text[i];
+ if (ch == '<') {
+ layer++;
+ continue;
+ }
+ if (ch == '>') {
+ layer = MAX(0, layer - 1);
+ continue;
+ }
+
+ const bool techLayer = (layer >= 2);
+ const RA1SpriteBank &bank =
+ (techLayer && _techFontBank.numSprites > 0) ? _techFontBank : _hudFontBank;
+ if (bank.numSprites <= 0)
+ return;
+
+ // FUN_221B7 special-case: when layer==2 and char is space, remap to '!'.
+ if (ch == ' ') {
+ if (techLayer) {
+ drawBankString(bank, dst, pitch, width, height, x, y, "!");
+ x += getBankStringWidth(bank, "!");
+ } else {
+ x += getBankSpaceAdvance(bank);
+ }
+ continue;
+ }
+
+ char glyph[2] = { ch, '\0' };
+ drawBankString(bank, dst, pitch, width, height, x, y, glyph);
+ x += getBankStringWidth(bank, glyph);
+ }
}
-// getFontBankStringWidth â Pre-pass width calculation from FUN_221B7 (0x221B7).
+// getFontBankStringWidth â Width pre-pass from FUN_221B7 (0x221B7), including '<'/'>' markup.
int InsaneRebel1::getFontBankStringWidth(const char *text) {
- return getBankStringWidth(_hudFontBank, text);
+ if (!text)
+ return 0;
+
+ int w = 0;
+ int layer = 0;
+ for (int i = 0; text[i] != '\0'; i++) {
+ char ch = text[i];
+ if (ch == '<') {
+ layer++;
+ continue;
+ }
+ if (ch == '>') {
+ layer = MAX(0, layer - 1);
+ continue;
+ }
+
+ const bool techLayer = (layer >= 2);
+ const RA1SpriteBank &bank =
+ (techLayer && _techFontBank.numSprites > 0) ? _techFontBank : _hudFontBank;
+ if (bank.numSprites <= 0)
+ return w;
+
+ if (ch == ' ') {
+ w += techLayer ? getBankStringWidth(bank, "!") : getBankSpaceAdvance(bank);
+ continue;
+ }
+
+ char glyph[2] = { ch, '\0' };
+ w += getBankStringWidth(bank, glyph);
+ }
+ return w;
}
// renderShip â Ship sprite rendering from FUN_1DEB5 (0x1DEB5) at LAB_1e2b2.
@@ -510,18 +588,17 @@ void InsaneRebel1::renderExplosions(byte *dst, int pitch, int width, int height)
}
}
-// renderHUD â FUN_1BBCB (0x1BBCB). Status bar from DISPLAY.NUT with health bar and score.
-// Original layout (320-wide): DAMAGE [green bar] | PILOTS [3 icons] | SCORE [number]
+// renderHUD â FUN_1BBCB (0x1BBCB). Status bar from DISPLAY.NUT with health/lives/score overlays.
void InsaneRebel1::renderHUD(byte *dst, int pitch, int width, int height) {
- if (_displayBank.numSprites == 0)
- return;
-
// Extra life bonus: every 10,000 points (FUN_1BBCB lines 11-27)
if (_score / 10000 > _prevScore / 10000) {
_lives++;
}
_prevScore = _score;
+ if (_displayBank.numSprites == 0)
+ return;
+
const RA1Sprite &bar = _displayBank.sprites[0];
// DISPLAY.NUT sprite is 320Ã19 at xoffs=0, yoffs=176 in the original game.
@@ -566,9 +643,9 @@ void InsaneRebel1::renderHUD(byte *dst, int pitch, int width, int height) {
hudX, hudY, bar.width, bar.height);
}
- // Draw health bar from FUN_1BBCB behavior.
- // Original logic uses current health as fill width and computes x as (0x92 - health),
- // so the bar is right-anchored and shrinks from left to right as damage increases.
+ // Draw health bar from FUN_1BBCB (0x1BBCB) + FUN_21D66 (0x21D66):
+ // fill rect at (0x92-health, 8), width=health, height=5, color=0.
+ // This is a black "remaining health" fill over the HUD template.
{
int barMaxW = kMaxHealth;
int barH = 5;
@@ -577,24 +654,10 @@ void InsaneRebel1::renderHUD(byte *dst, int pitch, int width, int height) {
int barY = hudY + 8;
int fillW = CLIP(healthWidth, 0, barMaxW);
- // Color based on damage level (matching original thresholds from FUN_1BBCB)
- // Palette indices: 0xD0-0xD7 = greens, 0x60-0x67 = yellows, 0xD8-0xDF = reds
- byte barColor;
- if (_health > _tuning.shot * 2)
- barColor = 0xD5; // Green (0,192,0) â low damage
- else if (_health > _tuning.wham * 2)
- barColor = 0x63; // Yellow (255,255,31) â moderate damage
- else
- barColor = 0xDD; // Red (192,0,0) â critical
-
- // Flash effect on damage
- if (_screenFlash > 0)
- barColor = 0xFF; // White flash
-
for (int iy = 0; iy < barH && barY + iy < height; iy++) {
byte *d = dst + (barY + iy) * pitch + barX;
for (int ix = 0; ix < fillW && barX + ix < width; ix++) {
- d[ix] = barColor;
+ d[ix] = 0x00;
}
}
}
@@ -617,13 +680,35 @@ void InsaneRebel1::renderHUD(byte *dst, int pitch, int width, int height) {
}
}
- // Score: 6-digit zero-padded at x=273, y=5 (original format '<<%%06ld' at 0x111,5)
- if (_hudFontBank.numSprites > 0) {
- char scoreStr[16];
- Common::sprintf_s(scoreStr, "%06d", MAX<int>(_score, 0));
+ // Score: FUN_1BBCB (0x1BBCB) -> FUN_21FAF (0x21FAF) with format at 0x6713: "<<%06ld".
+ // Keep the leading "<<" markup so FUN_221B7-equivalent path selects TECH font layer.
+ if (_hudFontBank.numSprites > 0 || _techFontBank.numSprites > 0) {
+ char scoreStr[24];
+ Common::sprintf_s(scoreStr, "<<%06d", MAX<int>(_score, 0));
drawFontBankString(dst, pitch, width, height, hudX + 273, hudY + 5, scoreStr);
}
+ // Low-health indicator from FUN_1BBCB (0x1BBCB):
+ // if (health < miss*2 || health < wham*2 || health < shot*2) and (frame & 8),
+ // draw warning glyph at (0x49, 0x07). Two variants:
+ // "<<[" when above critical thresholds, "<<\\" when critical.
+ {
+ const bool lowHealthBand =
+ ((_health < _tuning.miss * 2) ||
+ (_health < _tuning.wham * 2) ||
+ (_health < _tuning.shot * 2)) &&
+ ((_frameCounter & 8) != 0);
+ if (lowHealthBand) {
+ const bool aboveCritical =
+ (_health > _tuning.miss) &&
+ (_health > _tuning.wham) &&
+ (_health > _tuning.shot);
+ // FUN_1BBCB pushes string pointers 0x671b ("<<[") or 0x671f ("<<\\") into FUN_221B7.
+ const char *warningStr = aboveCritical ? "<<[" : "<<\\";
+ drawFontBankString(dst, pitch, width, height, hudX + 0x49, hudY + 0x07, warningStr);
+ }
+ }
+
}
// renderSprite â Simplified version of FUN_20BD3 (0x20BD3) glyph/sprite renderer.
Commit: 8210b3de837696b0159c138ef1647e220fdd5fd9
https://github.com/scummvm/scummvm/commit/8210b3de837696b0159c138ef1647e220fdd5fd9
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:32+02:00
Commit Message:
SCUMM: RA1: Fix level 1 stage transition
Changed paths:
engines/scumm/insane/insane_rebel1_render.cpp
engines/scumm/insane/insane_rebel1_runlevels.cpp
diff --git a/engines/scumm/insane/insane_rebel1_render.cpp b/engines/scumm/insane/insane_rebel1_render.cpp
index 8a814695376..4e87e32d01a 100644
--- a/engines/scumm/insane/insane_rebel1_render.cpp
+++ b/engines/scumm/insane/insane_rebel1_render.cpp
@@ -264,6 +264,13 @@ void InsaneRebel1::procPostRendering(byte *renderBitmap, int32 codecparam, int32
// Level 1 (and others): third-person ship flight with accumulators
if (_shipBank.numSprites > 0) {
updateShipPhysics();
+ // LVL1 assembly flow exits gameplay loops as soon as health drops below 0
+ // (see 0x1626E/0x162EE -> 0x165DD and 0x1640B -> 0x16614), then plays crash video.
+ // Do not render the in-engine death overlay in this path; finish immediately.
+ if (_currentLevel == 0 && _health < 0) {
+ _vm->_smushVideoShouldFinish = true;
+ return;
+ }
renderShip(renderBitmap, pitch, width, height);
}
}
diff --git a/engines/scumm/insane/insane_rebel1_runlevels.cpp b/engines/scumm/insane/insane_rebel1_runlevels.cpp
index 92a7c2d02a3..e42c2e33718 100644
--- a/engines/scumm/insane/insane_rebel1_runlevels.cpp
+++ b/engines/scumm/insane/insane_rebel1_runlevels.cpp
@@ -123,6 +123,8 @@ bool InsaneRebel1::runLevel1() {
// Retry loop â on death with lives, L1NEW plays then jumps back here
while (!_vm->shouldQuit()) {
+ bool stage2Started = false;
+
// Reset health for this attempt (original: MOV WORD [0x7560], 98 at 0x16214)
_health = kMaxHealth;
_damageFlags = 0;
@@ -158,6 +160,7 @@ bool InsaneRebel1::runLevel1() {
return false;
// L1PLAY2.ANM â Stage 2 turret (original: 0x5986)
+ stage2Started = true;
playInteractiveVideo("LVL1/L1PLAY2.ANM");
if (_vm->shouldQuit())
return false;
@@ -170,17 +173,16 @@ bool InsaneRebel1::runLevel1() {
return true;
}
- // Death sequence (original: 0x165dd-0x166bb)
- // Random crash variant A or B
- if (_vm->_rnd.getRandomNumber(1) == 0)
- playCinematic("LVL1/L1CRASHA.ANM");
- else
+ // Death sequence (assembly-verified: 0x165dd / 0x16614):
+ // Stage 1 deaths use L1CRASHA; Stage 2 deaths use L1CRASHB.
+ if (stage2Started)
playCinematic("LVL1/L1CRASHB.ANM");
+ else
+ playCinematic("LVL1/L1CRASHA.ANM");
if (_vm->shouldQuit())
return false;
- // Check lives (original: CMP WORD [0x7562], 0 at 0x1666B)
- _lives--;
+ // Assembly order (0x1666B): check lives first; decrement only on retry path.
if (_lives <= 0) {
// Game over â L1DEATH then return (original: 0x166C0)
playCinematic("LVL1/L1DEATH.ANM");
@@ -192,6 +194,7 @@ bool InsaneRebel1::runLevel1() {
playCinematic("LVL1/L1NEW.ANM");
if (_vm->shouldQuit())
return false;
+ _lives--;
// Loop back to gameplay (original: JMP 0x16214 â health reset + Stage 1)
}
Commit: 8434efb10b0f194c9227caf5a3dfe3d66e9b4515
https://github.com/scummvm/scummvm/commit/8434efb10b0f194c9227caf5a3dfe3d66e9b4515
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:32+02:00
Commit Message:
SCUMM: RA1: Fix level 1 retry flow
Changed paths:
engines/scumm/insane/insane_rebel1_runlevels.cpp
diff --git a/engines/scumm/insane/insane_rebel1_runlevels.cpp b/engines/scumm/insane/insane_rebel1_runlevels.cpp
index e42c2e33718..89c8f3de938 100644
--- a/engines/scumm/insane/insane_rebel1_runlevels.cpp
+++ b/engines/scumm/insane/insane_rebel1_runlevels.cpp
@@ -284,11 +284,9 @@ void InsaneRebel1::runGame() {
case 1: {
bool completed = false;
if (_startLevel <= 1) {
- // Start from Level 1 (default flow)
- playCinematic("LVL1/L1NEW.ANM");
- if (_vm->shouldQuit())
- return;
-
+ // Start from Level 1 (default flow).
+ // Assembly Level 1 entry (0x16100) starts with L1HANGAR/L1CU1,
+ // not with L1NEW (L1NEW is retry-only at 0x16675).
completed = runLevel1();
if (completed && !_vm->shouldQuit()) {
completed = runLevel2();
@@ -298,10 +296,8 @@ void InsaneRebel1::runGame() {
}
}
} else if (_startLevel == 2) {
- // Direct Level 2 start from Level Select menu
- playCinematic("LVL2/L2NEW.ANM");
- if (_vm->shouldQuit())
- return;
+ // Direct Level 2 start from Level Select menu.
+ // Assembly Level 2 entry (0x16757) starts at L2INTRO; L2NEW is retry-only.
completed = runLevel2();
if (completed) {
debug(1, "InsaneRebel1: Level 2 completed!");
Commit: da5c1e19e044fb21313412170468f73012042f61
https://github.com/scummvm/scummvm/commit/da5c1e19e044fb21313412170468f73012042f61
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:33+02:00
Commit Message:
SCUMM: RA1: Add subtitle fonts
Changed paths:
engines/scumm/insane/insane_rebel1.cpp
engines/scumm/insane/insane_rebel1.h
engines/scumm/insane/insane_rebel1_render.cpp
engines/scumm/smush/smush_player.cpp
diff --git a/engines/scumm/insane/insane_rebel1.cpp b/engines/scumm/insane/insane_rebel1.cpp
index 225b0678a09..00630052e1e 100644
--- a/engines/scumm/insane/insane_rebel1.cpp
+++ b/engines/scumm/insane/insane_rebel1.cpp
@@ -158,6 +158,14 @@ InsaneRebel1::InsaneRebel1(ScummEngine_v7 *scumm) : Insane(), _vm(scumm) {
warning("InsaneRebel1: failed to load RA1 HUD font bank (TECHFONT/TALKFONT)");
}
+ if (loadRA1Nut("SYS/TITLFONT.NUT", _titleFontBank)) {
+ debug(1, "InsaneRebel1: title glyph font loaded from SYS/TITLFONT.NUT (%d chars)", _titleFontBank.numSprites);
+ } else if (loadRA1Nut("SYS/TALKFONT.NUT", _titleFontBank)) {
+ debug(1, "InsaneRebel1: title glyph font fallback loaded from SYS/TALKFONT.NUT (%d chars)", _titleFontBank.numSprites);
+ } else {
+ warning("InsaneRebel1: failed to load title font bank (TITLFONT/TALKFONT)");
+ }
+
// FUN_1CB22 uses "<<" layer markers that resolve to TECHFONT in the original.
// Keep a dedicated TECH font bank for targeting markers/lock indicators.
if (loadRA1Nut("SYS/TECHFONT.NUT", _techFontBank)) {
diff --git a/engines/scumm/insane/insane_rebel1.h b/engines/scumm/insane/insane_rebel1.h
index eba6366f77b..ac7c6e66d3f 100644
--- a/engines/scumm/insane/insane_rebel1.h
+++ b/engines/scumm/insane/insane_rebel1.h
@@ -145,6 +145,7 @@ private:
RA1SpriteBank _shipBank;
RA1SpriteBank _displayBank; // SYS/DISPLAY.NUT â bottom status bar
+ RA1SpriteBank _titleFontBank; // SYS/TITLFONT.NUT â default subtitle/title layer
RA1SpriteBank _hudFontBank; // RA1 HUD text glyphs (TECHFONT/TALKFONT via RA1 loader)
RA1SpriteBank _techFontBank; // SYS/TECHFONT.NUT â targeting glyph layer ("<<" markers)
RA1SpriteBank _bangBank; // LxBANG.NUT â impact/explosion sprites (10 frames)
diff --git a/engines/scumm/insane/insane_rebel1_render.cpp b/engines/scumm/insane/insane_rebel1_render.cpp
index 4e87e32d01a..6d8733bef9d 100644
--- a/engines/scumm/insane/insane_rebel1_render.cpp
+++ b/engines/scumm/insane/insane_rebel1_render.cpp
@@ -442,8 +442,9 @@ void InsaneRebel1::drawFontBankString(byte *dst, int pitch, int width, int heigh
return;
// Original FUN_221B7 layer mapping is table-driven at DAT_2D56 (0x406-byte entries).
- // Current RA1 integration mirrors the gameplay-relevant subset:
- // layer 0/1 -> HUD font bank
+ // Current RA1 integration maps subtitle/HUD-relevant layers as:
+ // layer 0 -> TITLE font bank
+ // layer 1 -> TALK/HUD font bank
// layer 2+ -> TECH font bank
int layer = 0;
for (int i = 0; text[i] != '\0'; i++) {
@@ -458,8 +459,12 @@ void InsaneRebel1::drawFontBankString(byte *dst, int pitch, int width, int heigh
}
const bool techLayer = (layer >= 2);
- const RA1SpriteBank &bank =
- (techLayer && _techFontBank.numSprites > 0) ? _techFontBank : _hudFontBank;
+ const bool talkLayer = (layer == 1);
+ const RA1SpriteBank &bank = techLayer ?
+ ((_techFontBank.numSprites > 0) ? _techFontBank : _hudFontBank) :
+ (talkLayer ?
+ _hudFontBank :
+ ((_titleFontBank.numSprites > 0) ? _titleFontBank : _hudFontBank));
if (bank.numSprites <= 0)
return;
@@ -499,8 +504,12 @@ int InsaneRebel1::getFontBankStringWidth(const char *text) {
}
const bool techLayer = (layer >= 2);
- const RA1SpriteBank &bank =
- (techLayer && _techFontBank.numSprites > 0) ? _techFontBank : _hudFontBank;
+ const bool talkLayer = (layer == 1);
+ const RA1SpriteBank &bank = techLayer ?
+ ((_techFontBank.numSprites > 0) ? _techFontBank : _hudFontBank) :
+ (talkLayer ?
+ _hudFontBank :
+ ((_titleFontBank.numSprites > 0) ? _titleFontBank : _hudFontBank));
if (bank.numSprites <= 0)
return w;
diff --git a/engines/scumm/smush/smush_player.cpp b/engines/scumm/smush/smush_player.cpp
index 97ddcc9cdfc..1daaee85f17 100644
--- a/engines/scumm/smush/smush_player.cpp
+++ b/engines/scumm/smush/smush_player.cpp
@@ -1884,74 +1884,68 @@ SmushFont *SmushPlayer::ra1GetFont(int font) {
}
/**
- * RA1 TEXT chunk handler.
- *
- * RA1 TEXT format (different from SCUMM7/COMI TRES):
- * Bytes 0-3 (BE32): y position on screen
- * Bytes 4-7 (BE32): parameter (x hint or style)
- * Bytes 8+: text content with 0x00 as line separator
- * '<' / '>' switch font layers (multi-font markup)
- *
- * Text is rendered centered horizontally at the given y position.
- * The '<' font-switch prefix is stripped before rendering.
+ * RA1 TEXT chunk handler (assembly parity):
+ * FUN_1FDBC (0x1FDBC) TEXT path -> FUN_221B7 (0x221B7)
+ * - payload uses two BE32 fields + text bytes with 0x00 separators
+ * - if first byte is '.', skip that sentinel before rendering
+ * - '<'/'>' switch font layers (handled by RA1 font-bank renderer)
*/
void SmushPlayer::ra1HandleText(int32 subSize, Common::SeekableReadStream &b) {
if (subSize < 8 || !_dst || _width <= 0 || _height <= 0)
return;
- InsaneRebel1 *rebel1 = (InsaneRebel1 *)_vm->_insane;
+ InsaneRebel1 *rebel1 = static_cast<InsaneRebel1 *>(_vm->_insane);
if (!rebel1)
return;
- int yPos = b.readUint32BE();
- /*int param =*/ b.readUint32BE();
+ // Keep RA1 on-screen placement path stable (first BE32 drives baseline Y
+ // in current renderer integration), while preserving original text markers.
+ int16 cursorY = (int16)b.readUint32BE();
+ /*int16 xParam =*/ (void)b.readUint32BE();
int textLen = subSize - 8;
if (textLen <= 0)
return;
- char *textBuf = (char *)malloc(textLen + 1);
+ byte *textBuf = (byte *)malloc(textLen);
+ if (!textBuf)
+ return;
b.read(textBuf, textLen);
- textBuf[textLen] = '\0';
-
- int pitch = _width;
-
- // Split on 0x00 line separators and render each line centered.
- // Strip '<' / '>' font layer markers (multi-font not yet supported).
- const char *lineStart = textBuf;
- int lineY = yPos;
- int lineHeight = 10;
- while (lineStart < textBuf + textLen) {
- const char *lineEnd = lineStart;
- while (lineEnd < textBuf + textLen && *lineEnd != '\0')
- lineEnd++;
+ // FUN_1FDBC checks first byte at payload+8 and skips a leading '.'
+ // when present (TEXT-at-0x2E sentinel path).
+ int start = 0;
+ if (textLen > 0 && textBuf[0] == '.')
+ start = 1;
- int len = (int)(lineEnd - lineStart);
- if (len > 0) {
- const char *cleaned = lineStart;
- while (cleaned < lineEnd && (*cleaned == '<' || *cleaned == '>'))
- cleaned++;
+ int remaining = textLen - start;
- int cleanLen = (int)(lineEnd - cleaned);
- if (cleanLen > 0 && lineY >= 0 && lineY < _height) {
- char *line = (char *)malloc(cleanLen + 1);
- memcpy(line, cleaned, cleanLen);
- line[cleanLen] = '\0';
+ while (remaining > 0) {
+ int lineLen = 0;
+ while (lineLen < remaining && textBuf[start + lineLen] != 0)
+ lineLen++;
- // Center the line horizontally
- int strWidth = rebel1->getFontBankStringWidth(line);
- int x = (_width - strWidth) / 2;
- if (x < 0) x = 0;
+ if (lineLen > 0) {
+ char *line = (char *)malloc(lineLen + 1);
+ if (line) {
+ memcpy(line, textBuf + start, lineLen);
+ line[lineLen] = '\0';
- rebel1->drawFontBankString(_dst, pitch, _width, _height, x, lineY, line);
+ int drawX = (_width - rebel1->getFontBankStringWidth(line)) / 2;
+ if (drawX < 0)
+ drawX = 0;
+ rebel1->drawFontBankString(_dst, _width, _width, _height, drawX, cursorY, line);
free(line);
- lineY += lineHeight;
}
}
- lineStart = lineEnd + 1;
+ cursorY += 10;
+ int consumed = lineLen;
+ if (consumed < remaining && textBuf[start + consumed] == 0)
+ consumed++;
+ start += consumed;
+ remaining -= consumed;
}
free(textBuf);
Commit: 360d4911f1b3ea64e4d241ce2c6a4545eba7d3f2
https://github.com/scummvm/scummvm/commit/360d4911f1b3ea64e4d241ce2c6a4545eba7d3f2
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:33+02:00
Commit Message:
SCUMM: RA1: Fix subtitle positioning
Changed paths:
engines/scumm/insane/insane_rebel1.h
engines/scumm/insane/insane_rebel1_menu.cpp
engines/scumm/insane/insane_rebel1_render.cpp
engines/scumm/smush/smush_player.cpp
diff --git a/engines/scumm/insane/insane_rebel1.h b/engines/scumm/insane/insane_rebel1.h
index ac7c6e66d3f..0c1d866a24a 100644
--- a/engines/scumm/insane/insane_rebel1.h
+++ b/engines/scumm/insane/insane_rebel1.h
@@ -138,6 +138,7 @@ private:
public:
void drawFontBankString(byte *dst, int pitch, int width, int height, int x, int y, const char *text);
int getFontBankStringWidth(const char *text);
+ int getFontBankLineAdvance(const char *text);
void processAudioFrame(int16 feedSize);
private:
diff --git a/engines/scumm/insane/insane_rebel1_menu.cpp b/engines/scumm/insane/insane_rebel1_menu.cpp
index 2344eaf4964..e1e27853e9a 100644
--- a/engines/scumm/insane/insane_rebel1_menu.cpp
+++ b/engines/scumm/insane/insane_rebel1_menu.cpp
@@ -21,6 +21,7 @@
#include "common/system.h"
#include "common/events.h"
+#include "common/str.h"
#include "scumm/scumm_v7.h"
#include "scumm/smush/smush_player.h"
@@ -137,13 +138,32 @@ bool InsaneRebel1::notifyEvent(const Common::Event &event) {
void InsaneRebel1::renderMainMenuOverlay(byte *dst, int pitch, int width, int height) {
_menuFrameCounter++;
+ auto makeTalkText = [](const char *text) {
+ Common::String out("<");
+ out += text;
+ return out;
+ };
+ auto getTalkTextWidth = [&](const char *text) {
+ Common::String styled = makeTalkText(text);
+ return getFontBankStringWidth(styled.c_str());
+ };
+ auto drawTalkText = [&](int x, int y, const char *text) {
+ Common::String styled = makeTalkText(text);
+ drawFontBankString(dst, pitch, width, height, x, y, styled.c_str());
+ };
+ auto getTitleTextWidth = [&](const char *text) {
+ return getFontBankStringWidth(text);
+ };
+ auto drawTitleText = [&](int x, int y, const char *text) {
+ drawFontBankString(dst, pitch, width, height, x, y, text);
+ };
if (_optionsActive) {
// --- Options submenu ---
static const char *kDiffNames[3] = { "EASY", "NORMAL", "HARD" };
- const int titleW = getFontBankStringWidth("GAME OPTIONS");
- drawFontBankString(dst, pitch, width, height, (width - titleW) / 2, 36, "GAME OPTIONS");
+ const int titleW = getTalkTextWidth("GAME OPTIONS");
+ drawTalkText((width - titleW) / 2, 36, "GAME OPTIONS");
// Build dynamic option strings
char diffLine[64];
@@ -155,10 +175,10 @@ void InsaneRebel1::renderMainMenuOverlay(byte *dst, int pitch, int width, int he
const int rowH = 16;
for (int i = 0; i < 3; i++) {
- const int textW = getFontBankStringWidth(kOptionsItems[i]);
+ const int textW = getTalkTextWidth(kOptionsItems[i]);
const int textX = (width - textW) / 2;
const int y = menuY + i * rowH;
- drawFontBankString(dst, pitch, width, height, textX, y + 1, kOptionsItems[i]);
+ drawTalkText(textX, y + 1, kOptionsItems[i]);
if (i == _optionsSel) {
byte highlightColor = ((_menuFrameCounter / 8) & 1) ? 248 : 240;
@@ -182,8 +202,8 @@ void InsaneRebel1::renderMainMenuOverlay(byte *dst, int pitch, int width, int he
if (_levelSelectActive) {
// --- Level select submenu ---
- const int titleW = getFontBankStringWidth("LEVEL SELECT");
- drawFontBankString(dst, pitch, width, height, (width - titleW) / 2, 36, "LEVEL SELECT");
+ const int titleW = getTalkTextWidth("LEVEL SELECT");
+ drawTalkText((width - titleW) / 2, 36, "LEVEL SELECT");
const char *kLevelItems[3] = {
"LEVEL 1: FLIGHT TRAINING",
@@ -195,13 +215,13 @@ void InsaneRebel1::renderMainMenuOverlay(byte *dst, int pitch, int width, int he
const int rowH = 16;
for (int i = 0; i < 3; i++) {
- const int textW = getFontBankStringWidth(kLevelItems[i]);
+ const int textW = getTalkTextWidth(kLevelItems[i]);
const int textX = (width - textW) / 2;
const int y = menuY + i * rowH;
- drawFontBankString(dst, pitch, width, height, textX, y + 1, kLevelItems[i]);
+ drawTalkText(textX, y + 1, kLevelItems[i]);
if (i == _startLevel - 1) {
- drawFontBankString(dst, pitch, width, height, textX - 12, y + 1, ">");
+ drawTalkText(textX - 12, y + 1, ">");
}
if (i == _levelSelectSel) {
@@ -234,20 +254,20 @@ void InsaneRebel1::renderMainMenuOverlay(byte *dst, int pitch, int width, int he
};
// Center title
- const int titleW = getFontBankStringWidth("MAIN MENU");
+ const int titleW = getTitleTextWidth("MAIN MENU");
const int titleX = (width - titleW) / 2;
- drawFontBankString(dst, pitch, width, height, titleX, 36, "MAIN MENU");
+ drawTitleText(titleX, 36, "MAIN MENU");
// Draw menu items centered horizontally
const int menuY = 60;
const int rowH = 16;
for (int i = 0; i < 5; i++) {
- const int textW = getFontBankStringWidth(kMenuItems[i]);
+ const int textW = getTalkTextWidth(kMenuItems[i]);
const int textX = (width - textW) / 2;
const int y = menuY + i * rowH;
- drawFontBankString(dst, pitch, width, height, textX, y + 1, kMenuItems[i]);
+ drawTalkText(textX, y + 1, kMenuItems[i]);
// Selection highlight box â flashing border (FUN_004292d0 pattern from RA2)
if (i == _menuSelection) {
diff --git a/engines/scumm/insane/insane_rebel1_render.cpp b/engines/scumm/insane/insane_rebel1_render.cpp
index 6d8733bef9d..93b97d1ef5f 100644
--- a/engines/scumm/insane/insane_rebel1_render.cpp
+++ b/engines/scumm/insane/insane_rebel1_render.cpp
@@ -125,6 +125,28 @@ static int getBankSpaceAdvance(const RA1SpriteBank &bank) {
return MIN(exclWidth, 8);
}
+static const RA1SpriteBank &selectLayerBank(const RA1SpriteBank &titleBank,
+ const RA1SpriteBank &hudBank, const RA1SpriteBank &techBank, int layer) {
+ const bool techLayer = (layer >= 2);
+ const bool talkLayer = (layer == 1);
+ if (techLayer)
+ return (techBank.numSprites > 0) ? techBank : hudBank;
+ if (talkLayer)
+ return hudBank;
+ return (titleBank.numSprites > 0) ? titleBank : hudBank;
+}
+
+static int getBankSpaceHeight(const RA1SpriteBank &bank) {
+ // In FUN_221B7 line advance is derived from the layer's space-glyph height (+4).
+ // With current NUT decoding we approximate that using the '!' glyph (index 0).
+ if (bank.numSprites > 0) {
+ const RA1Sprite &glyph = bank.sprites[0];
+ if (glyph.height > 0)
+ return glyph.height;
+ }
+ return 8;
+}
+
// FUN_1C794: direction bucket in range -4..4 from two points.
static int ra1ShotDirection(int16 x1, int16 y1, int16 x2, int16 y2) {
int dx = x2 - x1;
@@ -459,12 +481,7 @@ void InsaneRebel1::drawFontBankString(byte *dst, int pitch, int width, int heigh
}
const bool techLayer = (layer >= 2);
- const bool talkLayer = (layer == 1);
- const RA1SpriteBank &bank = techLayer ?
- ((_techFontBank.numSprites > 0) ? _techFontBank : _hudFontBank) :
- (talkLayer ?
- _hudFontBank :
- ((_titleFontBank.numSprites > 0) ? _titleFontBank : _hudFontBank));
+ const RA1SpriteBank &bank = selectLayerBank(_titleFontBank, _hudFontBank, _techFontBank, layer);
if (bank.numSprites <= 0)
return;
@@ -504,12 +521,7 @@ int InsaneRebel1::getFontBankStringWidth(const char *text) {
}
const bool techLayer = (layer >= 2);
- const bool talkLayer = (layer == 1);
- const RA1SpriteBank &bank = techLayer ?
- ((_techFontBank.numSprites > 0) ? _techFontBank : _hudFontBank) :
- (talkLayer ?
- _hudFontBank :
- ((_titleFontBank.numSprites > 0) ? _titleFontBank : _hudFontBank));
+ const RA1SpriteBank &bank = selectLayerBank(_titleFontBank, _hudFontBank, _techFontBank, layer);
if (bank.numSprites <= 0)
return w;
@@ -524,6 +536,21 @@ int InsaneRebel1::getFontBankStringWidth(const char *text) {
return w;
}
+int InsaneRebel1::getFontBankLineAdvance(const char *text) {
+ int layer = 0;
+ if (text) {
+ for (int i = 0; text[i] != '\0'; i++) {
+ if (text[i] == '<')
+ layer++;
+ else if (text[i] == '>')
+ layer = MAX(layer - 1, 0);
+ }
+ }
+
+ const RA1SpriteBank &bank = selectLayerBank(_titleFontBank, _hudFontBank, _techFontBank, layer);
+ return getBankSpaceHeight(bank) + 4;
+}
+
// renderShip â Ship sprite rendering from FUN_1DEB5 (0x1DEB5) at LAB_1e2b2.
// Also used by FUN_1E6A7 (0x1E6A7) turret handler via FUN_20BD3.
void InsaneRebel1::renderShip(byte *dst, int pitch, int width, int height) {
diff --git a/engines/scumm/smush/smush_player.cpp b/engines/scumm/smush/smush_player.cpp
index 1daaee85f17..074f1df0b20 100644
--- a/engines/scumm/smush/smush_player.cpp
+++ b/engines/scumm/smush/smush_player.cpp
@@ -1898,10 +1898,10 @@ void SmushPlayer::ra1HandleText(int32 subSize, Common::SeekableReadStream &b) {
if (!rebel1)
return;
- // Keep RA1 on-screen placement path stable (first BE32 drives baseline Y
- // in current renderer integration), while preserving original text markers.
- int16 cursorY = (int16)b.readUint32BE();
- /*int16 xParam =*/ (void)b.readUint32BE();
+ // FUN_1FDBC TEXT path passes BE32 x/y from payload to FUN_221B7 with 0x200
+ // center-alignment flag, so x is an anchor and y is line baseline.
+ const int textAnchorX = b.readSint32BE();
+ int cursorY = b.readSint32BE();
int textLen = subSize - 8;
if (textLen <= 0)
@@ -1927,20 +1927,20 @@ void SmushPlayer::ra1HandleText(int32 subSize, Common::SeekableReadStream &b) {
if (lineLen > 0) {
char *line = (char *)malloc(lineLen + 1);
- if (line) {
+ if (!line) {
+ cursorY += 12;
+ } else {
memcpy(line, textBuf + start, lineLen);
line[lineLen] = '\0';
- int drawX = (_width - rebel1->getFontBankStringWidth(line)) / 2;
- if (drawX < 0)
- drawX = 0;
-
+ const int drawX = textAnchorX - (rebel1->getFontBankStringWidth(line) / 2);
rebel1->drawFontBankString(_dst, _width, _width, _height, drawX, cursorY, line);
+ cursorY += rebel1->getFontBankLineAdvance(line);
free(line);
}
+ } else {
+ cursorY += rebel1->getFontBankLineAdvance(nullptr);
}
-
- cursorY += 10;
int consumed = lineLen;
if (consumed < remaining && textBuf[start + consumed] == 0)
consumed++;
Commit: 7cad2386cf927c3cbc7e0b3d198e1545c8737c7c
https://github.com/scummvm/scummvm/commit/7cad2386cf927c3cbc7e0b3d198e1545c8737c7c
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:33+02:00
Commit Message:
SCUMM: RA1: Transform GAME zones with camera offsets
Changed paths:
engines/scumm/insane/insane_rebel1_iact.cpp
diff --git a/engines/scumm/insane/insane_rebel1_iact.cpp b/engines/scumm/insane/insane_rebel1_iact.cpp
index 34a52224555..b22dc27e1be 100644
--- a/engines/scumm/insane/insane_rebel1_iact.cpp
+++ b/engines/scumm/insane/insane_rebel1_iact.cpp
@@ -27,6 +27,15 @@
namespace Scumm {
+// FUN_223FE (0x223FE) coordinate transform used by FUN_1C54D/FUN_1C6E9.
+// The original applies camera X offset directly and a Y term derived from
+// DAT_41A2 (+ curve-table contribution). In current RA1 integration we keep
+// the camera-offset part, which fixes left/right corridor asymmetry.
+static void transformPoint223FE(int16 &x, int16 &y, int16 cameraX, int16 cameraY) {
+ x = (int16)(x - cameraX);
+ y = (int16)(y - cameraY);
+}
+
// updateShipPhysics â FUN_1DEB5 (0x1DEB5). Accumulator-based position system.
// Roll accumulator (_74CA) driven by input, position accumulators (_74C2/_74C6)
// driven by roll + drift + cross-coupling. Ship position = base + accum >> 8.
@@ -449,15 +458,23 @@ void InsaneRebel1::handleGameChunk(int32 subSize, Common::SeekableReadStream &b)
// Original params: left, top, WIDTH, HEIGHT (not right/bottom!)
// FUN_1C54D computes center = (left+width/2, top+height/2), transforms, then checks edges.
if (subSize >= 20) {
- _corridorLeftX = (int16)param1;
- _corridorTopY = (int16)b.readUint32BE();
+ int16 corridorLeft = (int16)param1;
+ int16 corridorTop = (int16)b.readUint32BE();
int16 corridorWidth = (int16)b.readUint32BE();
int16 corridorHeight = (int16)b.readUint32BE();
+
+ int16 centerX = corridorLeft + corridorWidth / 2;
+ int16 centerY = corridorTop + corridorHeight / 2;
+ transformPoint223FE(centerX, centerY, _perspectiveX, _perspectiveY);
+
+ _corridorLeftX = centerX - corridorWidth / 2;
+ _corridorTopY = centerY - corridorHeight / 2;
_corridorRightX = _corridorLeftX + corridorWidth;
_corridorBottomY = _corridorTopY + corridorHeight;
- debug(5, "RA1 GAME 0x0D: corridor left=%d top=%d right=%d bottom=%d (w=%d h=%d)",
- _corridorLeftX, _corridorTopY, _corridorRightX, _corridorBottomY,
- corridorWidth, corridorHeight);
+ debug(5, "RA1 GAME 0x0D: raw=[%d,%d]+(%d,%d) cam=(%d,%d) transformed=[%d,%d]-[%d,%d]",
+ corridorLeft, corridorTop, corridorWidth, corridorHeight,
+ _perspectiveX, _perspectiveY,
+ _corridorLeftX, _corridorTopY, _corridorRightX, _corridorBottomY);
}
break;
@@ -469,14 +486,21 @@ void InsaneRebel1::handleGameChunk(int32 subSize, Common::SeekableReadStream &b)
int16 zoneTop = (int16)b.readUint32BE();
int16 zoneWidth = (int16)b.readUint32BE();
int16 zoneHeight = (int16)b.readUint32BE();
+
+ int16 centerX = zoneLeft + zoneWidth / 2;
+ int16 centerY = zoneTop + zoneHeight / 2;
+ transformPoint223FE(centerX, centerY, _perspectiveX, _perspectiveY);
+
+ zoneLeft = centerX - zoneWidth / 2;
+ zoneTop = centerY - zoneHeight / 2;
int16 zoneRight = zoneLeft + zoneWidth;
int16 zoneBottom = zoneTop + zoneHeight;
if (_shipPosX > zoneLeft && _shipPosX < zoneRight &&
_shipPosY > zoneTop && _shipPosY < zoneBottom) {
_damageFlags |= 0x10;
}
- debug(7, "RA1 GAME 0x0E: zone=[%d,%d]-[%d,%d] flags=0x%02x",
- zoneLeft, zoneTop, zoneRight, zoneBottom, _damageFlags);
+ debug(7, "RA1 GAME 0x0E: zone=[%d,%d]-[%d,%d] cam=(%d,%d) flags=0x%02x",
+ zoneLeft, zoneTop, zoneRight, zoneBottom, _perspectiveX, _perspectiveY, _damageFlags);
}
break;
Commit: 08430ff3f98a32949231a34a6f0e7701c0457366
https://github.com/scummvm/scummvm/commit/08430ff3f98a32949231a34a6f0e7701c0457366
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:34+02:00
Commit Message:
SCUMM: RA1: Preserve left-path branching
Changed paths:
engines/scumm/insane/insane_rebel1_iact.cpp
diff --git a/engines/scumm/insane/insane_rebel1_iact.cpp b/engines/scumm/insane/insane_rebel1_iact.cpp
index b22dc27e1be..54fc33c84f6 100644
--- a/engines/scumm/insane/insane_rebel1_iact.cpp
+++ b/engines/scumm/insane/insane_rebel1_iact.cpp
@@ -229,13 +229,16 @@ void InsaneRebel1::updateShipPhysics() {
// Original (FUN_1B297): at GAME counter 394 (0x18A), sets nextSceneA=0x67/nextSceneB=0x69.
// After this point, drift goes strongly negative (pushing ship left for the hard path).
// If ship is right of center, player chose the right/easy path â switch to L1PLAY1R.
- // The check fires once when the game counter first reaches the branch point.
- if (_pathBranchEnabled && !_rightPathSelected && _gameCounter >= kPathBranchCounter) {
+ // Keep this as a one-shot decision: once threshold is reached, lock path.
+ if (_pathBranchEnabled && _gameCounter >= kPathBranchCounter) {
if (_shipPosX > kRA1CenterX) {
_rightPathSelected = true;
_vm->_smushVideoShouldFinish = true;
debug(1, "RA1: Right path selected (counter=%d, shipX=%d)", _gameCounter, _shipPosX);
+ } else {
+ debug(1, "RA1: Left path retained (counter=%d, shipX=%d)", _gameCounter, _shipPosX);
}
+ _pathBranchEnabled = false;
}
debug(7, "RA1 ship: pos=(%d,%d) roll=%d lift=%d accX=%d accY=%d dir=%d health=%d corridor=[%d,%d]-[%d,%d]",
Commit: 151bad125b8a374c417701107580e9df205b825b
https://github.com/scummvm/scummvm/commit/151bad125b8a374c417701107580e9df205b825b
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:34+02:00
Commit Message:
SCUMM: RA1: Fix level 1 second phase
Changed paths:
engines/scumm/insane/insane_rebel1.cpp
engines/scumm/insane/insane_rebel1.h
engines/scumm/insane/insane_rebel1_iact.cpp
engines/scumm/insane/insane_rebel1_levels.cpp
engines/scumm/insane/insane_rebel1_render.cpp
engines/scumm/insane/insane_rebel1_runlevels.cpp
engines/scumm/smush/smush_player.cpp
diff --git a/engines/scumm/insane/insane_rebel1.cpp b/engines/scumm/insane/insane_rebel1.cpp
index 00630052e1e..58eb11105fc 100644
--- a/engines/scumm/insane/insane_rebel1.cpp
+++ b/engines/scumm/insane/insane_rebel1.cpp
@@ -107,6 +107,10 @@ InsaneRebel1::InsaneRebel1(ScummEngine_v7 *scumm) : Insane(), _vm(scumm) {
_currentLevel = 0;
_flyControlMode = 0;
+ _turretEmitterLeftX = 0;
+ _turretEmitterLeftY = 0;
+ _turretEmitterRightX = 0;
+ _turretEmitterRightY = 0;
_activeGameOpcode = 0;
_health = kMaxHealth;
diff --git a/engines/scumm/insane/insane_rebel1.h b/engines/scumm/insane/insane_rebel1.h
index 0c1d866a24a..f284c053543 100644
--- a/engines/scumm/insane/insane_rebel1.h
+++ b/engines/scumm/insane/insane_rebel1.h
@@ -116,6 +116,7 @@ private:
bool loadRA1Nut(const char *filename, RA1SpriteBank &bank);
void loadLevelSprites(int level);
void updateShipPhysics();
+ void updateTurretPhysics();
void renderShip(byte *dst, int pitch, int width, int height);
void renderHUD(byte *dst, int pitch, int width, int height);
void renderMainMenuOverlay(byte *dst, int pitch, int width, int height);
@@ -145,6 +146,7 @@ private:
ScummEngine_v7 *_vm;
RA1SpriteBank _shipBank;
+ RA1SpriteBank _shipBankAlt; // Secondary ship bank (e.g. L1BANK2 mode-2 sprites)
RA1SpriteBank _displayBank; // SYS/DISPLAY.NUT â bottom status bar
RA1SpriteBank _titleFontBank; // SYS/TITLFONT.NUT â default subtitle/title layer
RA1SpriteBank _hudFontBank; // RA1 HUD text glyphs (TECHFONT/TALKFONT via RA1 loader)
@@ -204,6 +206,11 @@ private:
// Control mode (from GAME opcode 0x5E)
int16 _flyControlMode;
+ // Mode-2 emitter offsets used by FUN_1D79C when _DAT_75E4 == 2.
+ int16 _turretEmitterLeftX;
+ int16 _turretEmitterLeftY;
+ int16 _turretEmitterRightX;
+ int16 _turretEmitterRightY;
// Last per-frame GAME movement handler opcode (0x07/0x08/0x09/0x0A/0x0B/0x1A).
// Used to mirror assembly handler-specific overlay pipeline behavior.
uint16 _activeGameOpcode;
@@ -291,6 +298,7 @@ private:
int16 posY; // 0x75F6: cursor Y at time of shot
int16 centerX; // 0x75EA: perspective-adjusted X
int16 centerY; // 0x75EE: perspective-adjusted Y
+ int16 variant; // 0x75FA: emitter table selector (DAT_241F snapshot)
};
ShotSlot _shotSlots[kMaxShotSlots];
int16 _shotAlternator; // 0x241F: alternates between 0/1
diff --git a/engines/scumm/insane/insane_rebel1_iact.cpp b/engines/scumm/insane/insane_rebel1_iact.cpp
index 54fc33c84f6..244580cf7a0 100644
--- a/engines/scumm/insane/insane_rebel1_iact.cpp
+++ b/engines/scumm/insane/insane_rebel1_iact.cpp
@@ -247,6 +247,148 @@ void InsaneRebel1::updateShipPhysics() {
_corridorLeftX, _corridorTopY, _corridorRightX, _corridorBottomY);
}
+// updateTurretPhysics â FUN_1E6A7 (0x1E6A7), opcode 0x08 path.
+// Stage-2 cockpit mode uses different smoothing/clamps than FUN_1DEB5.
+void InsaneRebel1::updateTurretPhysics() {
+ _frameCounter++;
+
+ // FUN_1E6A7 consumes GAME field1 as frame counter (arg6 in dispatcher call).
+ // The 0x10/0x40 gates come from dispatcher arg4 (callback control bits),
+ // not from GAME payload fields.
+ const int32 counter = _gameCounter;
+ const byte modeFlags = 0;
+
+ // RA1 latches consumed by handler family in FUN_1B297.
+ if (_gameLatch5D == 0xFFFF)
+ _damageFlags |= 0x40;
+ if (_gameLatch5F != 0 && _vm->_rnd.getRandomNumber((uint16)(_gameLatch5F - 1)) == 0)
+ _damageFlags |= 0x80;
+
+ if (counter == 0) {
+ _posAccumX = 0;
+ _posAccumY = 0;
+ _rollAccum = 0;
+ _liftSmooth = 0;
+ _shipPosX = kRA1CenterX;
+ _shipPosY = kRA1CenterY;
+ _damageFlags = 0;
+ _prevDamageFlags = 0;
+ _damageCooldown = 0;
+ }
+
+ // Damage gate from FUN_1E6A7.
+ if (_damageFlags != 0 && _damageCooldown == 0 && _health >= 0 && _deathTimer <= 0) {
+ if (_damageFlags == 0x80)
+ _health -= _tuning.shot;
+ else
+ _health -= _tuning.wham;
+
+ if (_health < 0)
+ _deathTimer = kDeathTimerInit;
+
+ _prevDamageFlags = _damageFlags;
+ _damageCooldown = kDamageCooldownInit;
+ _screenFlash = 3;
+ }
+
+ if (_damageCooldown > 0)
+ _damageCooldown--;
+
+ if (_health < 0 && _deathTimer > 0)
+ _deathTimer--;
+
+ // FUN_1E6A7 movement gate: counter > 8 or flags bit 0x40.
+ if (counter > 8 || (modeFlags & 0x40)) {
+ int16 mouseX = (int16)_vm->_mouse.x;
+ int16 mouseY = (int16)_vm->_mouse.y;
+ if (_player && _player->_width > 0 && _player->_height > 0 &&
+ (mouseX >= 320 || mouseY >= 200)) {
+ mouseX = (int16)((mouseX * 320) / _player->_width);
+ mouseY = (int16)((mouseY * 200) / _player->_height);
+ }
+
+ int16 inputX = CLIP<int16>((int16)(mouseX - 160), -127, 127);
+ int16 inputY = CLIP<int16>((int16)(mouseY - 100), -127, 127);
+
+ _rollAccum += (_tuning.roll * (int32)inputX) >> 4;
+ _rollAccum = (_rollAccum * 3) >> 2;
+ _rollAccum = CLIP<int32>(_rollAccum, -0x480, 0x480);
+
+ _liftSmooth += (((int32)_liftSmooth - (int32)inputY) * (int32)_tuning.lift) >> 8;
+ _liftSmooth = (_liftSmooth * 3) >> 2;
+ _liftSmooth = CLIP<int32>(_liftSmooth, -0x32, 0x32);
+
+ if ((modeFlags & 0x10) == 0) {
+ _posAccumX += ((int32)_tuning.slide * _rollAccum) >> 6;
+ _posAccumY -= ((int32)_liftSmooth * 64);
+ _posAccumX = CLIP<int32>(_posAccumX, -0x8C00, 0x8C00);
+ _posAccumY = CLIP<int32>(_posAccumY, -0x4600, 0x3C00);
+ }
+ }
+
+ const int16 offsetX = (int16)(_posAccumX >> 8);
+ const int16 offsetY = (int16)(_posAccumY >> 8);
+
+ // FUN_1D79C tail sets pointer center from offsets:
+ // _74BE = _74B6 + _74BA
+ // _74C0 = (_74B8 + _74BC - 0x23) - (_74BC >> 3)
+ _shipPosX = CLIP<int16>((int16)(kRA1CenterX + offsetX), kRA1MinX, kRA1MaxX);
+ _shipPosY = CLIP<int16>((int16)((kRA1CenterY + offsetY - 0x23) - (offsetY >> 3)),
+ kRA1MinY, kRA1MaxY);
+
+ _perspectiveX = CLIP<int16>((int16)(offsetX + 0x20), 0, 0x40);
+ _perspectiveY = CLIP<int16>((int16)(offsetY + 0x17), 0, 0x2E);
+
+ // Direction bucket synthesis from FUN_1E6A7.
+ int dir = 0;
+ if (_flyControlMode == 2) {
+ if (_rollAccum > 0x380) dir = 4;
+ else if (_rollAccum > 0x280) dir = 3;
+ else if (_rollAccum > 0x180) dir = 2;
+ else if (_rollAccum > 0x80) dir = 1;
+ else if (_rollAccum > -0x80) dir = 0;
+ else if (_rollAccum > -0x180) dir = 5;
+ else if (_rollAccum > -0x280) dir = 6;
+ else if (_rollAccum > -0x380) dir = 7;
+ else dir = 8;
+ } else {
+ if (_rollAccum > 0x380) dir = 8;
+ else if (_rollAccum > 0x280) dir = 7;
+ else if (_rollAccum > 0x180) dir = 6;
+ else if (_rollAccum > 0x80) dir = 5;
+ else if (_rollAccum > -0x80) dir = 4;
+ else if (_rollAccum > -0x180) dir = 3;
+ else if (_rollAccum > -0x280) dir = 2;
+ else if (_rollAccum > -0x380) dir = 1;
+ else dir = 0;
+
+ if (offsetY < -0x1E)
+ dir += 0x12;
+ else if (offsetY < 0x1E)
+ dir += 9;
+ }
+ const RA1SpriteBank *shipBank = &_shipBank;
+ if (_currentLevel == 0 && _flyControlMode == 2 && _shipBankAlt.numSprites > 0)
+ shipBank = &_shipBankAlt;
+ if (shipBank->numSprites > 0)
+ _shipDirIndex = CLIP<int16>((int16)dir, 0, shipBank->numSprites - 1);
+
+ // Regeneration via FUN_1BB0E call in this path.
+ if ((_frameCounter & 0x1F) == 0) {
+ if (_health >= 0 && _health < kMaxHealth)
+ _health++;
+ if (_health >= 0)
+ _score += 1;
+ }
+
+ if (_screenFlash > 0)
+ _screenFlash--;
+
+ _gameLatch5D = 0;
+ _gameLatch5F = 0;
+ _damageFlags = 0;
+}
+
// updateAsteroidPhysics â FUN_1CDA7 (0x1CDA7). Opcode 0x0B handler.
// Uses 10-frame input history averaging instead of accumulators.
// Ship position = averaged input + center offset.
@@ -426,10 +568,8 @@ void InsaneRebel1::handleGameChunk(int32 subSize, Common::SeekableReadStream &b)
_shipPosY = kRA1CenterY;
}
- // Keep a conservative default mode after reset.
- _flyControlMode = 0;
_activeGameOpcode = 0;
- debug(5, "RA1 GAME 0x5E: reset state field1=%d", (int32)param1);
+ debug(5, "RA1 GAME 0x5E: reset state field1=%d mode=%d", (int32)param1, (int)_flyControlMode);
break;
case 0x5D:
@@ -553,11 +693,13 @@ void InsaneRebel1::handleGameChunk(int32 subSize, Common::SeekableReadStream &b)
case 0x19:
case 0x1A:
_activeGameOpcode = (uint16)opcode;
+ _gameCounter = param1;
if (subSize >= 20) {
uint32 param2 = b.readUint32BE();
uint32 param3 = b.readUint32BE();
uint32 param4 = b.readUint32BE();
- debug(5, "RA1 GAME 0x%02x: params=(%d,%d,%d,%d)", opcode, param1, param2, param3, param4);
+ debug(5, "RA1 GAME 0x%02x: counter=%d params=(%d,%d,%d)",
+ opcode, _gameCounter, param2, param3, param4);
}
break;
@@ -587,11 +729,16 @@ void InsaneRebel1::processShot() {
}
// Record shot at current cursor position.
+ const bool turretMode = (_activeGameOpcode == 0x08 || _activeGameOpcode == 0x0A);
+ const int16 shipCenterX = turretMode ? (int16)(kRA1CenterX + (_perspectiveX - 0x20)) : _shipPosX;
+ const int16 shipCenterY = turretMode ? (int16)(kRA1CenterY + (_perspectiveY - 0x17)) : _shipPosY;
+
_shotSlots[slot].timer = 5;
_shotSlots[slot].posX = _shipPosX;
_shotSlots[slot].posY = _shipPosY;
- _shotSlots[slot].centerX = _shipPosX;
- _shotSlots[slot].centerY = _shipPosY;
+ _shotSlots[slot].centerX = shipCenterX;
+ _shotSlots[slot].centerY = shipCenterY;
+ _shotSlots[slot].variant = _shotAlternator;
_shotAlternator = 1 - _shotAlternator;
_playerFired = false;
diff --git a/engines/scumm/insane/insane_rebel1_levels.cpp b/engines/scumm/insane/insane_rebel1_levels.cpp
index 195c7490265..0128443f89c 100644
--- a/engines/scumm/insane/insane_rebel1_levels.cpp
+++ b/engines/scumm/insane/insane_rebel1_levels.cpp
@@ -57,11 +57,22 @@ static void decodeBomp(byte *dst, const byte *src, int width, int height, int pi
}
}
+static void resetSpriteBank(RA1SpriteBank &bank) {
+ delete[] bank.sprites;
+ bank.sprites = nullptr;
+ free(bank.decodedData);
+ bank.decodedData = nullptr;
+ bank.numSprites = 0;
+ bank.decodedSize = 0;
+}
+
// Load an RA1 NUT sprite file (ANIM v1).
// RA1 NUTs can have odd-size FOBJ chunks padded to 2-byte alignment within
// FRME containers. This loader handles that padding properly, unlike the
// shared NutRenderer::loadFont which assumes even-size chunks.
bool InsaneRebel1::loadRA1Nut(const char *filename, RA1SpriteBank &bank) {
+ resetSpriteBank(bank);
+
ScummFile *file = _vm->instantiateScummFile();
_vm->openFile(*file, filename);
if (!file->isOpen()) {
@@ -199,6 +210,13 @@ void InsaneRebel1::loadLevelSprites(int level) {
if (!loadRA1Nut(bankFile.c_str(), _shipBank)) {
debug(1, "InsaneRebel1: No BANK1 for level %d (first-person level)", level);
}
+
+ // Secondary ship bank used by some level-specific handlers (e.g. LVL1 mode-2).
+ Common::String bankFileAlt = Common::String::format("LVL%d/L%dBANK2.NUT", level, level);
+ if (!loadRA1Nut(bankFileAlt.c_str(), _shipBankAlt)) {
+ debug(1, "InsaneRebel1: No BANK2 for level %d", level);
+ }
+
loadRA1Nut("SYS/DISPLAY.NUT", _displayBank);
// Explosion sprites â try BANG first, then EXPLD
diff --git a/engines/scumm/insane/insane_rebel1_render.cpp b/engines/scumm/insane/insane_rebel1_render.cpp
index 93b97d1ef5f..3cd918f3165 100644
--- a/engines/scumm/insane/insane_rebel1_render.cpp
+++ b/engines/scumm/insane/insane_rebel1_render.cpp
@@ -190,6 +190,32 @@ static int ra1ShotDirectionBucket(int dir) {
return 10;
}
+struct RA1ShotEmitterPair {
+ int16 x1;
+ int16 y1;
+ int16 x2;
+ int16 y2;
+};
+
+// DAT_244A and DAT_251A in ASSAULT.EXE data section, used by FUN_1D79C.
+static const RA1ShotEmitterPair kRA1ShotEmitters244A[27] = {
+ { 11, -11, -11, 0 }, { 16, -9, -16, -1 }, { 20, -6, -19, -3 }, { 20, -5, -21, -4 }, { -20, -6, 20, -5 },
+ { -18, -9, 16, -1 }, { -13, -11, 13, 0 }, { -7, -13, 8, 2 }, { 1, -10, 3, 2 }, { 11, -16, -11, 4 },
+ { 16, -14, -15, 1 }, { 19, -10, -19, -2 }, { 20, -5, -20, -4 }, { -20, -8, 19, -2 }, { -17, -11, 17, 1 },
+ { -12, -15, 14, 3 }, { -7, -17, 8, 3 }, { 0, -18, 3, 0 }, { 10, -17, -10, 8 }, { 15, -14, -15, 5 },
+ { 18, -10, -19, 1 }, { 20, -8, -19, -3 }, { -19, -8, 18, -6 }, { -16, -12, 17, 1 }, { -12, -16, 12, 3 },
+ { -5, -18, 9, 6 }, { -1, -11, -3, -6 }
+};
+
+static const RA1ShotEmitterPair kRA1ShotEmitters251A[27] = {
+ { -1, -11, -3, -6 }, { 7, -12, -8, 1 }, { 14, -11, -12, 0 }, { 18, -9, -17, -1 }, { 21, -7, -19, -4 },
+ { -20, -6, 21, -5 }, { -18, -8, 19, -2 }, { -16, -10, 16, -1 }, { -11, -12, 11, 0 }, { 1, -18, -2, -1 },
+ { 8, -17, -5, 1 }, { 13, -15, -12, 2 }, { 17, -13, -15, 0 }, { 21, -8, -19, -2 }, { -19, -6, 21, -4 },
+ { -18, -10, 19, -3 }, { -15, -14, 17, 1 }, { -10, -15, 11, 4 }, { 1, -19, -2, 6 }, { 7, -18, -7, 8 },
+ { 13, -16, -11, 5 }, { 18, -12, -14, 3 }, { 19, -8, -18, -2 }, { -17, -7, 20, -3 }, { -17, -10, 19, 1 },
+ { -15, -14, 16, 5 }, { 0, -38, -14, 37 }
+};
+
// Small subset of FUN_20D43 draw flags used by RA1 shot sprites.
static void renderSpriteWithFlags(byte *dst, int pitch, int width, int height,
int x, int y, const RA1Sprite &spr, uint32 flags) {
@@ -280,27 +306,40 @@ void InsaneRebel1::procPostRendering(byte *renderBitmap, int32 codecparam, int32
int pitch = width;
if (_currentLevel == 1) {
- // Level 2: first-person asteroid dodge â no ship sprite, input averaging physics
+ // Level 2: first-person asteroid dodge â opcode 0x0B (FUN_1CDA7).
updateAsteroidPhysics();
} else {
- // Level 1 (and others): third-person ship flight with accumulators
- if (_shipBank.numSprites > 0) {
+ const bool turretMode = (_activeGameOpcode == 0x08 || _activeGameOpcode == 0x0A);
+ const bool flightMode = (_activeGameOpcode == 0x07 || _activeGameOpcode == 0x09 ||
+ _activeGameOpcode == 0x19 || _activeGameOpcode == 0x1A);
+
+ // Dispatch movement path by GAME handler family:
+ // 0x08/0x0A -> FUN_1E6A7/FUN_1D79C (turret/cockpit)
+ // 0x07/0x09/0x19/0x1A -> flight-family handlers
+ if (turretMode) {
+ updateTurretPhysics();
+ } else if (_shipBank.numSprites > 0 && flightMode) {
updateShipPhysics();
- // LVL1 assembly flow exits gameplay loops as soon as health drops below 0
- // (see 0x1626E/0x162EE -> 0x165DD and 0x1640B -> 0x16614), then plays crash video.
- // Do not render the in-engine death overlay in this path; finish immediately.
- if (_currentLevel == 0 && _health < 0) {
- _vm->_smushVideoShouldFinish = true;
- return;
- }
- renderShip(renderBitmap, pitch, width, height);
}
+
+ // LVL1 assembly flow exits gameplay loops as soon as health drops below 0
+ // (see 0x1626E/0x162EE -> 0x165DD and 0x1640B -> 0x16614), then plays crash video.
+ // Do not render the in-engine death overlay in this path; finish immediately.
+ if (_currentLevel == 0 && _health < 0) {
+ _vm->_smushVideoShouldFinish = true;
+ return;
+ }
+
+ // Ship sprite is present in both flight (0x07 family) and 0x08 turret path.
+ if (flightMode || turretMode)
+ renderShip(renderBitmap, pitch, width, height);
}
// Assembly dispatch (FUN_1BE1B) only runs the targeting/shot overlay pipeline
- // in handlers 0x09/0x0A/0x0B/0x1A (FUN_1DABB/FUN_1D79C/FUN_1CDA7/FUN_1D57E).
+ // in handlers 0x09/0x0A/0x0B/0x1A. In LVL1 stage-2 samples, 0x08 drives
+ // turret mode and needs the same targeting overlays enabled.
const bool hasTargetingPipeline =
- (_activeGameOpcode == 0x09 || _activeGameOpcode == 0x0A ||
+ (_activeGameOpcode == 0x08 || _activeGameOpcode == 0x09 || _activeGameOpcode == 0x0A ||
_activeGameOpcode == 0x0B || _activeGameOpcode == 0x1A);
if (hasTargetingPipeline) {
processShot();
@@ -390,9 +429,7 @@ void InsaneRebel1::renderGostSlots(byte *dst, int pitch, int width, int height)
}
}
-// renderLaserShots â FUN_1CDA7/FUN_1D79C shot visual path:
-// per active slot, compute left/right direction with FUN_1C794, pick one
-// of 3x5 sprite bands, and render interpolated sprite positions via FUN_20BD3.
+// renderLaserShots â FUN_1CDA7/FUN_1D79C shot visual path.
void InsaneRebel1::renderLaserShots(byte *dst, int pitch, int width, int height) {
if (_laserBank.numSprites <= 0)
return;
@@ -403,6 +440,9 @@ void InsaneRebel1::renderLaserShots(byte *dst, int pitch, int width, int height)
const int spritesPerSet = 5;
const int leftStartX = 0;
const int rightStartX = 0x13F; // 319
+ const bool turretMode = (_activeGameOpcode == 0x08 || _activeGameOpcode == 0x0A);
+ const int shipBaseX = turretMode ? (kRA1CenterX + (_perspectiveX - 0x20)) : _shipPosX;
+ const int shipBaseY = turretMode ? (kRA1CenterY + (_perspectiveY - 0x17)) : _shipPosY;
for (int i = 0; i < kMaxShotSlots; i++) {
if (_shotSlots[i].timer > 0 && _shotSlots[i].timer <= spritesPerSet) {
@@ -412,13 +452,59 @@ void InsaneRebel1::renderLaserShots(byte *dst, int pitch, int width, int height)
const int targetX = CLIP<int>(_shipPosX, 0, width - 1);
const int targetY = CLIP<int>(_shipPosY, 0, height - 1);
+ if (turretMode) {
+ // FUN_1D79C chooses emitters in two ways:
+ // - DAT_75E4 == 2: use DAT_75DC..DAT_75E2 fixed offsets
+ // - otherwise: table path DAT_244A/DAT_251A keyed by DAT_74D2
+ int start1X = 0;
+ int start1Y = 0;
+ int start2X = 0;
+ int start2Y = 0;
+ bool haveEmitters = false;
+ if (_flyControlMode == 2) {
+ start1X = shipBaseX - _turretEmitterLeftX;
+ start1Y = shipBaseY + _turretEmitterLeftY;
+ start2X = shipBaseX + _turretEmitterRightX;
+ start2Y = shipBaseY + _turretEmitterRightY;
+ haveEmitters = true;
+ } else if (_shipDirIndex >= 0 && _shipDirIndex < 27) {
+ const RA1ShotEmitterPair &emit =
+ (_shotSlots[i].variant != 0) ? kRA1ShotEmitters244A[_shipDirIndex]
+ : kRA1ShotEmitters251A[_shipDirIndex];
+ start1X = shipBaseX + emit.x1;
+ start1Y = shipBaseY + emit.y1;
+ start2X = shipBaseX + emit.x2;
+ start2Y = shipBaseY + emit.y2;
+ haveEmitters = true;
+ }
+
+ if (!haveEmitters)
+ continue;
+
+ const int dir1 = ra1ShotDirection((int16)start1X, (int16)start1Y, (int16)targetX, (int16)targetY);
+ const int dir2 = ra1ShotDirection((int16)start2X, (int16)start2Y, (int16)targetX, (int16)targetY);
+ const int sprIdx1 = MIN<int>(ABS(dir1), _laserBank.numSprites - 1);
+ const int sprIdx2 = MIN<int>(ABS(dir2), _laserBank.numSprites - 1);
+ const uint32 flags1 = 0x83 | ((dir1 < 0) ? 0x2000 : 0);
+ const uint32 flags2 = 0x83 | ((dir2 < 0) ? 0x2000 : 0);
+ const int interp1X = start1X + (((targetX - start1X) * lerp) >> 3);
+ const int interp1Y = start1Y + (((targetY - start1Y) * lerp) >> 3);
+ const int interp2X = start2X + (((targetX - start2X) * lerp) >> 3);
+ const int interp2Y = start2Y + (((targetY - start2Y) * lerp) >> 3);
+
+ renderSpriteWithFlags(dst, pitch, width, height,
+ interp1X, interp1Y, _laserBank.sprites[sprIdx1], flags1);
+ renderSpriteWithFlags(dst, pitch, width, height,
+ interp2X, interp2Y, _laserBank.sprites[sprIdx2], flags2);
+ continue;
+ }
+
+ // Fallback for non-turret handlers that still run shot overlays.
int leftStartY = 0x96;
int rightStartY = 0x96;
uint32 leftFlags = 0x83;
uint32 rightFlags = 0x2083;
- // FUN_1CDA7 special mode branch (_DAT_75E4 == 1): toggles emitter origin
- // and flip flags via DAT_2423. Keep behavior for parity when mode is used.
if (_flyControlMode == 1) {
if (_shotAlternator != 0) {
leftFlags = 0x4083;
@@ -439,7 +525,6 @@ void InsaneRebel1::renderLaserShots(byte *dst, int pitch, int width, int height)
const int bucketRight = ra1ShotDirectionBucket(dirRight);
const int sprIdxLeft = frame + bucketLeft;
const int sprIdxRight = frame + bucketRight;
-
const int interpLeftX = leftStartX + (((targetX - leftStartX) * lerp) >> 3);
const int interpLeftY = leftStartY + (((targetY - leftStartY) * lerp) >> 3);
const int interpRightX = rightStartX + (((targetX - rightStartX) * lerp) >> 3);
@@ -559,15 +644,26 @@ void InsaneRebel1::renderShip(byte *dst, int pitch, int width, int height) {
if (_health < 0 && _deathTimer <= 20)
return;
- if (_shipDirIndex < 0 || _shipDirIndex >= _shipBank.numSprites)
+ const RA1SpriteBank *shipBank = &_shipBank;
+ if (_currentLevel == 0 && _flyControlMode == 2 && _shipBankAlt.numSprites > 0)
+ shipBank = &_shipBankAlt;
+
+ if (_shipDirIndex < 0 || _shipDirIndex >= shipBank->numSprites)
return;
- const RA1Sprite &spr = _shipBank.sprites[_shipDirIndex];
+ const RA1Sprite &spr = shipBank->sprites[_shipDirIndex];
+
+ // In 0x08/0x0A turret handlers, _shipPos holds pointer center (_74BE/_74C0),
+ // while ship sprite center is still (_74B6+_74BA, _74B8+_74BC).
+ int shipScreenX = _shipPosX;
+ int shipScreenY = _shipPosY;
+ if (_activeGameOpcode == 0x08 || _activeGameOpcode == 0x0A) {
+ shipScreenX = kRA1CenterX + (_perspectiveX - 0x20);
+ shipScreenY = kRA1CenterY + (_perspectiveY - 0x17);
+ }
- // FUN_1DEB5 draws at (_74B6 + _74BA, _74B8 + _74BC).
- // In the current mapping, _shipPosX/_shipPosY already store that screen position.
- int drawX = _shipPosX - spr.width / 2;
- int drawY = _shipPosY - spr.height / 2;
+ int drawX = shipScreenX - spr.width / 2;
+ int drawY = shipScreenY - spr.height / 2;
renderSprite(dst, pitch, width, height, drawX, drawY, spr);
}
@@ -578,9 +674,14 @@ void InsaneRebel1::renderExplosions(byte *dst, int pitch, int width, int height)
if (_bangBank.numSprites <= 0)
return;
- // Ship screen center position (matches assembly DAT_74B6+DAT_74BA, DAT_74B8+DAT_74BC).
+ // In 0x08/0x0A turret handlers, explosion anchors use ship center
+ // (_74B6+_74BA, _74B8+_74BC), not pointer center (_74BE/_74C0).
int shipScreenX = _shipPosX;
int shipScreenY = _shipPosY;
+ if (_activeGameOpcode == 0x08 || _activeGameOpcode == 0x0A) {
+ shipScreenX = kRA1CenterX + (_perspectiveX - 0x20);
+ shipScreenY = kRA1CenterY + (_perspectiveY - 0x17);
+ }
// --- Death shake explosions (FUN_1DEB5 LAB_1e0e3) ---
// When dead and deathTimer > 10: random explosion sprites scatter around ship
diff --git a/engines/scumm/insane/insane_rebel1_runlevels.cpp b/engines/scumm/insane/insane_rebel1_runlevels.cpp
index 89c8f3de938..01ff651e813 100644
--- a/engines/scumm/insane/insane_rebel1_runlevels.cpp
+++ b/engines/scumm/insane/insane_rebel1_runlevels.cpp
@@ -136,6 +136,7 @@ bool InsaneRebel1::runLevel1() {
_gameCounter = 0;
_pathBranchEnabled = true;
_rightPathSelected = false;
+ _flyControlMode = 1;
// Stage 1 flight â L1PLAY1L (hard/left path)
// The first 394 frames are the common section. At counter 394, if
@@ -147,6 +148,7 @@ bool InsaneRebel1::runLevel1() {
if (_rightPathSelected && _health >= 0) {
debug(1, "InsaneRebel1: Switching to right path (L1PLAY1R)");
_pathBranchEnabled = false;
+ _flyControlMode = 1;
playInteractiveVideo("LVL1/L1PLAY1R.ANM");
if (_vm->shouldQuit())
return false;
@@ -160,6 +162,13 @@ bool InsaneRebel1::runLevel1() {
return false;
// L1PLAY2.ANM â Stage 2 turret (original: 0x5986)
+ // Assembly @0x16396..0x163E5 switches to mode 2 and sets
+ // FUN_1D79C emitter offsets in DAT_75DC..DAT_75E2.
+ _flyControlMode = 2;
+ _turretEmitterLeftX = 10;
+ _turretEmitterLeftY = -5;
+ _turretEmitterRightX = 10;
+ _turretEmitterRightY = -5;
stage2Started = true;
playInteractiveVideo("LVL1/L1PLAY2.ANM");
if (_vm->shouldQuit())
@@ -208,6 +217,7 @@ bool InsaneRebel1::runLevel2() {
debug(1, "InsaneRebel1: Running level 2");
_currentLevel = 1;
+ _flyControlMode = 1;
loadLevelSprites(2);
loadTuningForLevel(1);
diff --git a/engines/scumm/smush/smush_player.cpp b/engines/scumm/smush/smush_player.cpp
index 074f1df0b20..479d2242317 100644
--- a/engines/scumm/smush/smush_player.cpp
+++ b/engines/scumm/smush/smush_player.cpp
@@ -351,6 +351,17 @@ void SmushPlayer::init(int32 speed) {
_fastForwardToFrame = 0;
_ra1HasCleanFrame = false;
+ // RA1 OBJ overlay chunks are video-local. Reset cached overlay state for each
+ // new ANM so data from a previous segment isn't re-applied.
+ free(_ra1ObjOverlayData);
+ _ra1ObjOverlayData = nullptr;
+ _ra1ObjOverlayDataSize = 0;
+ _ra1ObjOverlayCodec = 0;
+ _ra1ObjOverlayLeft = 0;
+ _ra1ObjOverlayTop = 0;
+ _ra1ObjOverlayWidth = 0;
+ _ra1ObjOverlayHeight = 0;
+
_vm->_smushVideoShouldFinish = false;
_vm->_smushActive = true;
Commit: fe8658365701272b8c801be3f57217519820119e
https://github.com/scummvm/scummvm/commit/fe8658365701272b8c801be3f57217519820119e
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:34+02:00
Commit Message:
SCUMM: RA1: Refine level 1 second phase
Changed paths:
engines/scumm/insane/insane_rebel1_iact.cpp
diff --git a/engines/scumm/insane/insane_rebel1_iact.cpp b/engines/scumm/insane/insane_rebel1_iact.cpp
index 244580cf7a0..6f76afb2a91 100644
--- a/engines/scumm/insane/insane_rebel1_iact.cpp
+++ b/engines/scumm/insane/insane_rebel1_iact.cpp
@@ -27,6 +27,13 @@
namespace Scumm {
+// LVL1 stage-2 0x5D damage/event codes observed in L1PLAY2.ANM.
+// Original DOS loop uses table/mask-driven latch routing before FUN_1E6A7;
+// in ScummVM's direct GAME dispatch path, map this known range explicitly.
+static inline bool isL1Stage2DamageLatch(uint16 code) {
+ return code >= 6 && code <= 18;
+}
+
// FUN_223FE (0x223FE) coordinate transform used by FUN_1C54D/FUN_1C6E9.
// The original applies camera X offset directly and a Y term derived from
// DAT_41A2 (+ curve-table contribution). In current RA1 integration we keep
@@ -178,7 +185,8 @@ void InsaneRebel1::updateShipPhysics() {
// RA1 FUN_1B297-style latches from GAME opcodes:
// 0x5D latch 0xFFFF -> bit 0x40 (obstacle/contact)
// 0x5F non-zero + RNG -> bit 0x80 (projectile-like hit)
- if (_gameLatch5D == 0xFFFF)
+ if (_gameLatch5D == 0xFFFF || (_currentLevel == 0 && _flyControlMode == 2 &&
+ isL1Stage2DamageLatch(_gameLatch5D)))
_damageFlags |= 0x40;
if (_gameLatch5F != 0 && _vm->_rnd.getRandomNumber((uint16)(_gameLatch5F - 1)) == 0)
_damageFlags |= 0x80;
@@ -259,7 +267,8 @@ void InsaneRebel1::updateTurretPhysics() {
const byte modeFlags = 0;
// RA1 latches consumed by handler family in FUN_1B297.
- if (_gameLatch5D == 0xFFFF)
+ if (_gameLatch5D == 0xFFFF || (_currentLevel == 0 && _flyControlMode == 2 &&
+ isL1Stage2DamageLatch(_gameLatch5D)))
_damageFlags |= 0x40;
if (_gameLatch5F != 0 && _vm->_rnd.getRandomNumber((uint16)(_gameLatch5F - 1)) == 0)
_damageFlags |= 0x80;
@@ -307,7 +316,11 @@ void InsaneRebel1::updateTurretPhysics() {
mouseY = (int16)((mouseY * 200) / _player->_height);
}
- int16 inputX = CLIP<int16>((int16)(mouseX - 160), -127, 127);
+ // FUN_1F3F8/FUN_231BE preprocesses mouse deltas before FUN_1E6A7 consumes
+ // DAT_756C/DAT_756E. Keep X in 320-space then scale to ±127 (RA2 parity),
+ // Y clamped to ±127.
+ int16 rawInputX = CLIP<int16>((int16)(mouseX - 160), -160, 160);
+ int16 inputX = (int16)((rawInputX * 127) / 160);
int16 inputY = CLIP<int16>((int16)(mouseY - 100), -127, 127);
_rollAccum += (_tuning.roll * (int32)inputX) >> 4;
@@ -332,9 +345,8 @@ void InsaneRebel1::updateTurretPhysics() {
// FUN_1D79C tail sets pointer center from offsets:
// _74BE = _74B6 + _74BA
// _74C0 = (_74B8 + _74BC - 0x23) - (_74BC >> 3)
- _shipPosX = CLIP<int16>((int16)(kRA1CenterX + offsetX), kRA1MinX, kRA1MaxX);
- _shipPosY = CLIP<int16>((int16)((kRA1CenterY + offsetY - 0x23) - (offsetY >> 3)),
- kRA1MinY, kRA1MaxY);
+ _shipPosX = (int16)(kRA1CenterX + offsetX);
+ _shipPosY = (int16)((kRA1CenterY + offsetY - 0x23) - (offsetY >> 3));
_perspectiveX = CLIP<int16>((int16)(offsetX + 0x20), 0, 0x40);
_perspectiveY = CLIP<int16>((int16)(offsetY + 0x17), 0, 0x2E);
Commit: a5dd30066d934ec92644ca9d15b09d42292b5e67
https://github.com/scummvm/scummvm/commit/a5dd30066d934ec92644ca9d15b09d42292b5e67
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:35+02:00
Commit Message:
SCUMM: RA1: Improve level 2 perspective and movement
Changed paths:
engines/scumm/insane/insane_rebel1.cpp
engines/scumm/insane/insane_rebel1.h
engines/scumm/insane/insane_rebel1_iact.cpp
engines/scumm/insane/insane_rebel1_render.cpp
engines/scumm/smush/smush_player.cpp
diff --git a/engines/scumm/insane/insane_rebel1.cpp b/engines/scumm/insane/insane_rebel1.cpp
index 58eb11105fc..d6e80d6a921 100644
--- a/engines/scumm/insane/insane_rebel1.cpp
+++ b/engines/scumm/insane/insane_rebel1.cpp
@@ -104,6 +104,14 @@ InsaneRebel1::InsaneRebel1(ScummEngine_v7 *scumm) : Insane(), _vm(scumm) {
memset(_viewHistoryY, 0, sizeof(_viewHistoryY));
_avgInputX = 0;
_avgInputY = 0;
+ _mouseOffsetX = 0;
+ _mouseOffsetY = 0;
+ _mouseBiasX = 0;
+ _mouseBiasY = 0;
+ _mousePrevBiasX = 0;
+ _mousePrevBiasY = 0;
+ _mouseBiasLatch = false;
+ _mouseRecentering = false;
_currentLevel = 0;
_flyControlMode = 0;
@@ -145,10 +153,15 @@ InsaneRebel1::InsaneRebel1(ScummEngine_v7 *scumm) : Insane(), _vm(scumm) {
_fireCooldown = 0;
memset(_shotSlots, 0, sizeof(_shotSlots));
_shotAlternator = 0;
+ _shotSideToggle = false;
_targetProximity = 0;
_prevTargetProx = 0;
+ _targetAnimCounter = 0;
_targetCount = 0;
_prevTargetCount = 0;
+ memset(_targetBoxX, 0, sizeof(_targetBoxX));
+ memset(_targetBoxY, 0, sizeof(_targetBoxY));
+ memset(_targetBoxVariant, 0, sizeof(_targetBoxVariant));
memset(_gostSlots, 0, sizeof(_gostSlots));
_gostSlotIdx = 0;
_killCount = 0;
diff --git a/engines/scumm/insane/insane_rebel1.h b/engines/scumm/insane/insane_rebel1.h
index f284c053543..a49d0676215 100644
--- a/engines/scumm/insane/insane_rebel1.h
+++ b/engines/scumm/insane/insane_rebel1.h
@@ -86,6 +86,7 @@ public:
bool isInteractiveVideoActive() const { return _interactiveVideoActive; }
int getCurrentLevel() const { return _currentLevel; }
uint16 getActiveGameOpcode() const { return _activeGameOpcode; }
+ int16 getPerspectiveX() const { return _perspectiveX; }
// Game flow (matching original at 0x15597)
void runGame();
@@ -117,10 +118,12 @@ private:
void loadLevelSprites(int level);
void updateShipPhysics();
void updateTurretPhysics();
+ void preprocessMouseAxes(int16 &inputX, int16 &inputY);
void renderShip(byte *dst, int pitch, int width, int height);
void renderHUD(byte *dst, int pitch, int width, int height);
void renderMainMenuOverlay(byte *dst, int pitch, int width, int height);
void renderExplosions(byte *dst, int pitch, int width, int height);
+ void renderTargetBoxes(byte *dst, int pitch, int width, int height);
void renderTargeting(byte *dst, int pitch, int width, int height);
void renderGostSlots(byte *dst, int pitch, int width, int height);
void renderLaserShots(byte *dst, int pitch, int width, int height);
@@ -197,6 +200,14 @@ private:
int16 _viewHistoryY[kInputHistorySize]; // 0x75BC: viewport vertical history
int16 _avgInputX; // smoothed horizontal input (clamped to [-0xA0, 0xA0])
int16 _avgInputY; // smoothed vertical input (clamped to [-0x46, 0x41])
+ int16 _mouseOffsetX; // 0x9762-style accumulated recenter offset in DOS 640-space
+ int16 _mouseOffsetY; // 0x9760-style accumulated recenter offset in DOS 200-space
+ int16 _mouseBiasX; // 0x9774: current preprocessed horizontal bias
+ int16 _mouseBiasY; // 0x9772: current preprocessed vertical bias
+ int16 _mousePrevBiasX; // 0x9770: previous-frame biasX
+ int16 _mousePrevBiasY; // 0x976E: previous-frame biasY
+ bool _mouseBiasLatch; // 0x4486: one-frame large-jump latch
+ bool _mouseRecentering; // 0x976D: suppress recursive updates during warp
// 0x0B handler physics update (asteroid/surface levels)
void updateAsteroidPhysics();
@@ -302,12 +313,18 @@ private:
};
ShotSlot _shotSlots[kMaxShotSlots];
int16 _shotAlternator; // 0x241F: alternates between 0/1
+ bool _shotSideToggle; // 0x2423: 0x0B side-toggle for mode-1 beam emitters
// Targeting state â FUN_1C0EF (0x1C0EF)
int16 _targetProximity; // 0x7558: 0=none, 1=near, 2=on-target
int16 _prevTargetProx; // 0x755A: previous frame's proximity
+ int16 _targetAnimCounter; // 0x755C: lock-marker animation counter
int16 _targetCount; // 0x7552: active targets this frame
int16 _prevTargetCount; // 0x7554: previous frame target count
+ static const int kMaxTargetBoxes = 20;
+ int16 _targetBoxX[kMaxTargetBoxes]; // 0x74DA: per-target overlay X
+ int16 _targetBoxY[kMaxTargetBoxes]; // 0x7502: per-target overlay Y
+ int16 _targetBoxVariant[kMaxTargetBoxes]; // 0x752A: size/near bucket ('i' + bucket)
// GOST hit animation slots (10 slots) â FUN_1C9CD (0x1C9CD)
static const int kMaxGostSlots = 10;
diff --git a/engines/scumm/insane/insane_rebel1_iact.cpp b/engines/scumm/insane/insane_rebel1_iact.cpp
index 6f76afb2a91..623fb05b4af 100644
--- a/engines/scumm/insane/insane_rebel1_iact.cpp
+++ b/engines/scumm/insane/insane_rebel1_iact.cpp
@@ -43,6 +43,42 @@ static void transformPoint223FE(int16 &x, int16 &y, int16 cameraX, int16 cameraY
y = (int16)(y - cameraY);
}
+// preprocessMouseAxes â FUN_231BE (0x231BE) centered-axis output law, adapted to
+// ScummVM's absolute 320x200 mouse space.
+// The original DOS path combines a full-scale centered axis term with an extra
+// mouse-bias term `(rawX-0x140)>>2`, `(rawY-100)>>1` maintained by
+// FUN_23115/FUN_231BE recenter globals. In ScummVM, mirroring the DOS recenter
+// box makes LVL2 control unusably constrained, so keep the FUN_231BE output law
+// (normalized axis + bias) but feed it directly from logical mouse coordinates.
+// That preserves the handler's expected `_DAT_756C/_DAT_756E` amplitude without
+// introducing the DOS safe-window box or extra latency.
+void InsaneRebel1::preprocessMouseAxes(int16 &inputX, int16 &inputY) {
+ int16 logicalX = (int16)CLIP<int>(_vm->_mouse.x, 0, 319);
+ int16 logicalY = (int16)CLIP<int>(_vm->_mouse.y, 0, 199);
+ const int16 rawX = (int16)(logicalX << 1);
+ const int16 rawY = logicalY;
+ const int16 deltaX = (int16)(logicalX - kRA1CenterX);
+ const int16 deltaY = (int16)(logicalY - kRA1CenterY);
+ const int16 normX = (int16)(((int32)deltaX * 127) / 160);
+ const int16 normY = (int16)(((int32)deltaY * 127) / 100);
+ const int16 biasX = (int16)((rawX - 0x140) >> 2);
+ const int16 biasY = (int16)((rawY - 100) >> 1);
+ const int16 scaledX = (int16)(normX + biasX);
+ const int16 scaledY = (int16)(normY + biasY);
+
+ _mouseBiasX = scaledX;
+ _mouseBiasY = scaledY;
+ _mousePrevBiasX = scaledX;
+ _mousePrevBiasY = scaledY;
+ _mouseBiasLatch = false;
+ _mouseRecentering = false;
+ _mouseOffsetX = 0;
+ _mouseOffsetY = 0;
+
+ inputX = CLIP<int16>(scaledX, -0xA0, 0xA0);
+ inputY = CLIP<int16>(scaledY, -127, 127);
+}
+
// updateShipPhysics â FUN_1DEB5 (0x1DEB5). Accumulator-based position system.
// Roll accumulator (_74CA) driven by input, position accumulators (_74C2/_74C6)
// driven by roll + drift + cross-coupling. Ship position = base + accum >> 8.
@@ -456,18 +492,9 @@ void InsaneRebel1::updateAsteroidPhysics() {
// --- Cursor and perspective smoothing (FUN_1CDA7) ---
// _inputHistory* maps to 0x7580/0x7594, _viewHistory* to 0x75A8/0x75BC.
- int16 mouseX = (int16)_vm->_mouse.x;
- int16 mouseY = (int16)_vm->_mouse.y;
- if (_player && _player->_width > 0 && _player->_height > 0 &&
- (mouseX >= 320 || mouseY >= 200)) {
- mouseX = (int16)((mouseX * 320) / _player->_width);
- mouseY = (int16)((mouseY * 200) / _player->_height);
- }
- int16 inputX = (int16)(mouseX - kRA1CenterX);
- // Assembly uses an inverted-Y convention in the averaging path.
- // In ScummVM screen coords (Y grows downward), convert here so moving
- // mouse up moves the pointer up on screen.
- int16 inputY = (int16)(kRA1CenterY - mouseY);
+ int16 inputX = 0;
+ int16 inputY = 0;
+ preprocessMouseAxes(inputX, inputY);
inputX = CLIP<int16>(inputX, -0xA0, 0xA0);
inputY = CLIP<int16>(inputY, -100, 100);
@@ -559,16 +586,29 @@ void InsaneRebel1::handleGameChunk(int32 subSize, Common::SeekableReadStream &b)
memset(_viewHistoryY, 0, sizeof(_viewHistoryY));
_avgInputX = 0;
_avgInputY = 0;
+ _mouseOffsetX = 0;
+ _mouseOffsetY = 0;
+ _mouseBiasX = 0;
+ _mouseBiasY = 0;
+ _mousePrevBiasX = 0;
+ _mousePrevBiasY = 0;
+ _mouseBiasLatch = false;
+ _mouseRecentering = false;
// Shooting/targeting reset
_playerFired = false;
_fireCooldown = 0;
memset(_shotSlots, 0, sizeof(_shotSlots));
_shotAlternator = 0;
+ _shotSideToggle = false;
_targetProximity = 0;
_prevTargetProx = 0;
+ _targetAnimCounter = 0;
_targetCount = 0;
_prevTargetCount = 0;
+ memset(_targetBoxX, 0, sizeof(_targetBoxX));
+ memset(_targetBoxY, 0, sizeof(_targetBoxY));
+ memset(_targetBoxVariant, 0, sizeof(_targetBoxVariant));
memset(_gostSlots, 0, sizeof(_gostSlots));
_gostSlotIdx = 0;
_killCount = 0;
@@ -764,6 +804,20 @@ void InsaneRebel1::checkTargetHit(int16 targetIdx, int16 left, int16 top, int16
int16 snap = _tuning.snap;
int16 curX = _shipPosX;
int16 curY = _shipPosY;
+ const int slot = _targetCount;
+
+ if (slot < kMaxTargetBoxes) {
+ _targetBoxX[slot] = (int16)((left + right) / 2);
+ _targetBoxY[slot] = (int16)((top + bottom) / 2);
+
+ const int height = bottom - top;
+ int16 glyphVariant = 0;
+ if (height >= 0x10)
+ glyphVariant = 2;
+ else if (height > 3)
+ glyphVariant = 1;
+ _targetBoxVariant[slot] = glyphVariant;
+ }
_targetCount++;
@@ -772,6 +826,8 @@ void InsaneRebel1::checkTargetHit(int16 targetIdx, int16 left, int16 top, int16
curY > top - snap - 5 && curY < bottom + snap + 5) {
if (_targetProximity == 0)
_targetProximity = 1; // Near
+ if (slot < kMaxTargetBoxes)
+ _targetBoxVariant[slot] = CLIP<int16>((int16)(_targetBoxVariant[slot] + 3), 0, 5);
// Check tight lock: cursor within target + snap (no extra margin)
if (curX > left - snap && curX < right + snap &&
diff --git a/engines/scumm/insane/insane_rebel1_render.cpp b/engines/scumm/insane/insane_rebel1_render.cpp
index 3cd918f3165..ea9fdcb5f7a 100644
--- a/engines/scumm/insane/insane_rebel1_render.cpp
+++ b/engines/scumm/insane/insane_rebel1_render.cpp
@@ -27,6 +27,16 @@
namespace Scumm {
+static inline int ra1OverlayViewOffsetX(const InsaneRebel1 *rebel1) {
+ if (!rebel1 || !rebel1->isInteractiveVideoActive())
+ return 0;
+
+ // In opcode 0x0B (FUN_1CDA7), marker/shot coordinates are in the gameplay
+ // window. Under ScummVM's FUN_224FD crop emulation, shift them into the
+ // 384-wide source buffer so they stay aligned after the source-window crop.
+ return (rebel1->getActiveGameOpcode() == 0x0B) ? rebel1->getPerspectiveX() : 0;
+}
+
static void drawBankString(const RA1SpriteBank &bank, byte *dst, int pitch, int width, int height,
int x, int y, const char *text) {
if (!dst || !text || bank.numSprites <= 0)
@@ -335,6 +345,14 @@ void InsaneRebel1::procPostRendering(byte *renderBitmap, int32 codecparam, int32
renderShip(renderBitmap, pitch, width, height);
}
+ // GAME handlers in the original update FUN_224FD during the same frame that
+ // the new control state is computed. Sync the current frame's viewport window
+ // before HUD/screen copy so 0x0B doesn't lag one frame behind the mouse.
+ if (_player) {
+ _player->_ra1ViewportOffsetX = _perspectiveX;
+ _player->_ra1ViewportOffsetY = (_activeGameOpcode == 0x0B) ? 0 : _perspectiveY;
+ }
+
// Assembly dispatch (FUN_1BE1B) only runs the targeting/shot overlay pipeline
// in handlers 0x09/0x0A/0x0B/0x1A. In LVL1 stage-2 samples, 0x08 drives
// turret mode and needs the same targeting overlays enabled.
@@ -342,6 +360,7 @@ void InsaneRebel1::procPostRendering(byte *renderBitmap, int32 codecparam, int32
(_activeGameOpcode == 0x08 || _activeGameOpcode == 0x09 || _activeGameOpcode == 0x0A ||
_activeGameOpcode == 0x0B || _activeGameOpcode == 0x1A);
if (hasTargetingPipeline) {
+ renderTargetBoxes(renderBitmap, pitch, width, height);
processShot();
for (int i = 0; i < kMaxShotSlots; i++) {
if (_shotSlots[i].timer > 0)
@@ -364,11 +383,28 @@ void InsaneRebel1::procPostRendering(byte *renderBitmap, int32 codecparam, int32
renderHUD(renderBitmap, pitch, width, height);
}
+// renderTargetBoxes â FUN_1C940 (0x1C940). Per-target green box overlays.
+void InsaneRebel1::renderTargetBoxes(byte *dst, int pitch, int width, int height) {
+ const int overlayX = ra1OverlayViewOffsetX(this);
+
+ for (int i = _targetCount - 1; i >= 0; --i) {
+ if (i >= kMaxTargetBoxes)
+ continue;
+
+ char box[4] = { '<', '<', (char)('i' + CLIP<int16>(_targetBoxVariant[i], 0, 5)), '\0' };
+ drawFontBankString(dst, pitch, width, height, overlayX + _targetBoxX[i], _targetBoxY[i], box);
+ }
+
+ _prevTargetCount = _targetCount;
+ _targetCount = 0;
+}
+
// renderTargeting â FUN_1CB22 (0x1CB22). Targeting/lock-on indicator.
// The original does not draw a hardcoded pixel cross; it renders glyph markers
// whose state depends on _targetProximity.
void InsaneRebel1::renderTargeting(byte *dst, int pitch, int width, int height) {
const RA1SpriteBank &markerBank = (_techFontBank.numSprites > 0) ? _techFontBank : _hudFontBank;
+ const int overlayX = ra1OverlayViewOffsetX(this);
if (markerBank.numSprites > 0) {
// FUN_1CB22 can switch marker sets via DAT_75FF bit 1.
// Baseline RA1 targeting uses '^' and animation e..h.
@@ -377,19 +413,20 @@ void InsaneRebel1::renderTargeting(byte *dst, int pitch, int width, int height)
// Lock indicator at fixed center positions:
// FUN_1CB22 draws marker strings at (0xA0,0x78) and (0xA0,0x7E).
if (_targetProximity > 0) {
- drawBankString(markerBank, dst, pitch, width, height, 0xA0, 0x78, "]");
+ drawBankString(markerBank, dst, pitch, width, height, overlayX + 0xA0, 0x78, "]");
if (_targetProximity > 1)
- drawBankString(markerBank, dst, pitch, width, height, 0xA0, 0x7E, "a");
+ drawBankString(markerBank, dst, pitch, width, height, overlayX + 0xA0, 0x7E, "a");
}
// Pointer glyph at current aim position. Original uses two variants:
// default marker ('^' or 'x') and animated lock marker (e..h or y..|).
char marker[2] = { (char)(altMarkerSet ? 'x' : '^'), '\0' };
if (_targetProximity > 1) {
- marker[0] = (char)((altMarkerSet ? 'y' : 'e') + (_frameCounter & 3));
+ _targetAnimCounter++;
+ marker[0] = (char)((altMarkerSet ? 'y' : 'e') + (_targetAnimCounter & 3));
}
- int cursorX = CLIP<int>(_shipPosX, 0, width - 1);
+ int cursorX = CLIP<int>(overlayX + _shipPosX, 0, width - 1);
int cursorY = CLIP<int>(_shipPosY, 0, height - 1);
int markerW = getBankStringWidth(markerBank, marker);
drawBankString(markerBank, dst, pitch, width, height, cursorX - markerW / 2, cursorY, marker);
@@ -398,10 +435,6 @@ void InsaneRebel1::renderTargeting(byte *dst, int pitch, int width, int height)
// Save previous proximity for next frame
_prevTargetProx = _targetProximity;
_targetProximity = 0;
-
- // Reset per-frame target count â FUN_1C940 (0x1C940)
- _prevTargetCount = _targetCount;
- _targetCount = 0;
_lastHitTarget = 0;
}
@@ -411,6 +444,7 @@ void InsaneRebel1::renderGostSlots(byte *dst, int pitch, int width, int height)
if (_bangBank.numSprites <= 0)
return;
+ const int overlayX = ra1OverlayViewOffsetX(this);
for (int i = 0; i < kMaxGostSlots; i++) {
if (_gostSlots[i].targetId != 0 && _gostSlots[i].frame < 10) {
int sprIdx = _gostSlots[i].frame;
@@ -418,7 +452,7 @@ void InsaneRebel1::renderGostSlots(byte *dst, int pitch, int width, int height)
sprIdx = _bangBank.numSprites - 1;
const RA1Sprite &spr = _bangBank.sprites[sprIdx];
- int drawX = _gostSlots[i].posX - spr.width / 2;
+ int drawX = overlayX + _gostSlots[i].posX - spr.width / 2;
int drawY = _gostSlots[i].posY - spr.height / 2;
renderSprite(dst, pitch, width, height, drawX, drawY, spr);
@@ -438,6 +472,7 @@ void InsaneRebel1::renderLaserShots(byte *dst, int pitch, int width, int height)
// Entry 0 unused.
static const int kShotLerpByTimer[6] = { 0, 8, 7, 6, 4, 0 };
const int spritesPerSet = 5;
+ const int overlayX = ra1OverlayViewOffsetX(this);
const int leftStartX = 0;
const int rightStartX = 0x13F; // 319
const bool turretMode = (_activeGameOpcode == 0x08 || _activeGameOpcode == 0x0A);
@@ -449,7 +484,7 @@ void InsaneRebel1::renderLaserShots(byte *dst, int pitch, int width, int height)
const int timer = _shotSlots[i].timer;
const int lerp = kShotLerpByTimer[timer];
const int frame = spritesPerSet - timer;
- const int targetX = CLIP<int>(_shipPosX, 0, width - 1);
+ const int targetX = CLIP<int>(overlayX + _shipPosX, 0, width - 1);
const int targetY = CLIP<int>(_shipPosY, 0, height - 1);
if (turretMode) {
@@ -506,7 +541,7 @@ void InsaneRebel1::renderLaserShots(byte *dst, int pitch, int width, int height)
uint32 rightFlags = 0x2083;
if (_flyControlMode == 1) {
- if (_shotAlternator != 0) {
+ if (_shotSideToggle) {
leftFlags = 0x4083;
leftStartY = 0;
rightStartY = 0x96;
@@ -516,18 +551,20 @@ void InsaneRebel1::renderLaserShots(byte *dst, int pitch, int width, int height)
rightStartY = 0;
}
if (timer == 1)
- _shotAlternator = 1 - _shotAlternator;
+ _shotSideToggle = !_shotSideToggle;
}
- const int dirLeft = ra1ShotDirection(targetX, targetY, leftStartX, leftStartY);
- const int dirRight = ra1ShotDirection(rightStartX, targetY, targetX, rightStartY);
+ const int startLeftX = overlayX + leftStartX;
+ const int startRightX = overlayX + rightStartX;
+ const int dirLeft = ra1ShotDirection((int16)startLeftX, (int16)leftStartY, (int16)targetX, (int16)targetY);
+ const int dirRight = ra1ShotDirection((int16)startRightX, (int16)rightStartY, (int16)targetX, (int16)targetY);
const int bucketLeft = ra1ShotDirectionBucket(dirLeft);
const int bucketRight = ra1ShotDirectionBucket(dirRight);
const int sprIdxLeft = frame + bucketLeft;
const int sprIdxRight = frame + bucketRight;
- const int interpLeftX = leftStartX + (((targetX - leftStartX) * lerp) >> 3);
+ const int interpLeftX = startLeftX + (((targetX - startLeftX) * lerp) >> 3);
const int interpLeftY = leftStartY + (((targetY - leftStartY) * lerp) >> 3);
- const int interpRightX = rightStartX + (((targetX - rightStartX) * lerp) >> 3);
+ const int interpRightX = startRightX + (((targetX - startRightX) * lerp) >> 3);
const int interpRightY = rightStartY + (((targetY - rightStartY) * lerp) >> 3);
if (sprIdxLeft >= 0 && sprIdxLeft < _laserBank.numSprites) {
@@ -674,9 +711,10 @@ void InsaneRebel1::renderExplosions(byte *dst, int pitch, int width, int height)
if (_bangBank.numSprites <= 0)
return;
+ const int overlayX = ra1OverlayViewOffsetX(this);
// In 0x08/0x0A turret handlers, explosion anchors use ship center
// (_74B6+_74BA, _74B8+_74BC), not pointer center (_74BE/_74C0).
- int shipScreenX = _shipPosX;
+ int shipScreenX = overlayX + _shipPosX;
int shipScreenY = _shipPosY;
if (_activeGameOpcode == 0x08 || _activeGameOpcode == 0x0A) {
shipScreenX = kRA1CenterX + (_perspectiveX - 0x20);
diff --git a/engines/scumm/smush/smush_player.cpp b/engines/scumm/smush/smush_player.cpp
index 479d2242317..46e5184c6d3 100644
--- a/engines/scumm/smush/smush_player.cpp
+++ b/engines/scumm/smush/smush_player.cpp
@@ -485,6 +485,19 @@ void SmushPlayer::handleFetch(int32 subSize, Common::SeekableReadStream &b) {
int left = _storedFobjLeft + fetchX;
int top = _storedFobjTop + fetchY;
+ if (_insane) {
+ InsaneRebel1 *rebel1 = static_cast<InsaneRebel1 *>(_insane);
+ // FUN_1FDBC routes FTCH through FUN_28D0A, which forces FUN_20D43
+ // flag 0x8 instead of replaying a plain raw FOBJ blit. In LVL2,
+ // the stored object is the 320-wide gameplay-window patch, so under
+ // ScummVM's 384-buffer crop emulation it must be anchored to the
+ // current window origin rather than left in raw chunk space.
+ if (rebel1->isInteractiveVideoActive() && rebel1->getActiveGameOpcode() == 0x0B &&
+ _storedFobjWidth == _vm->_screenWidth) {
+ left += _ra1ViewportOffsetX;
+ }
+ }
+
debug("RA1 FTCH: frame=%d id=0x%08x pos=(%d,%d) using stored FOBJ codec=%d size=%dx%d",
_frame, fetchId, left, top, storedCodec, _storedFobjWidth, _storedFobjHeight);
decodeFrameObject(storedCodec, _storedFobjData, left, top,
Commit: a17bfbcf4b9de1a05f6d5fbb84424794efb28ff1
https://github.com/scummvm/scummvm/commit/a17bfbcf4b9de1a05f6d5fbb84424794efb28ff1
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:35+02:00
Commit Message:
SCUMM: RA1: Refactor SMUSH integration
Changed paths:
A engines/scumm/smush/smush_player_ra1.cpp
engines/scumm/module.mk
engines/scumm/scumm.cpp
engines/scumm/smush/smush_player.cpp
engines/scumm/smush/smush_player.h
diff --git a/engines/scumm/module.mk b/engines/scumm/module.mk
index 2011a5049f7..b7fe661ee74 100644
--- a/engines/scumm/module.mk
+++ b/engines/scumm/module.mk
@@ -154,6 +154,7 @@ MODULE_OBJS += \
smush/codec_ra2.o \
smush/smush_multi_font.o \
smush/smush_player.o \
+ smush/smush_player_ra1.o \
smush/smush_player_ra2.o
ifdef USE_ARM_SMUSH_ASM
diff --git a/engines/scumm/scumm.cpp b/engines/scumm/scumm.cpp
index aac942458f6..424b222793e 100644
--- a/engines/scumm/scumm.cpp
+++ b/engines/scumm/scumm.cpp
@@ -1813,7 +1813,7 @@ void ScummEngine_v7::setupScumm(const Common::Path &macResourceFile) {
initScreens(0, 200);
_insane = new InsaneRebel1(this);
- _splayer = new SmushPlayer(this, nullptr, _insane);
+ _splayer = new SmushPlayerRebel1(this, nullptr, _insane);
_macGui = nullptr;
_charset = new CharsetRendererV7(this);
diff --git a/engines/scumm/smush/smush_player.cpp b/engines/scumm/smush/smush_player.cpp
index 46e5184c6d3..b86c86f4f31 100644
--- a/engines/scumm/smush/smush_player.cpp
+++ b/engines/scumm/smush/smush_player.cpp
@@ -350,17 +350,9 @@ void SmushPlayer::init(int32 speed) {
_scrollY = 0;
_fastForwardToFrame = 0;
_ra1HasCleanFrame = false;
-
// RA1 OBJ overlay chunks are video-local. Reset cached overlay state for each
// new ANM so data from a previous segment isn't re-applied.
- free(_ra1ObjOverlayData);
- _ra1ObjOverlayData = nullptr;
- _ra1ObjOverlayDataSize = 0;
- _ra1ObjOverlayCodec = 0;
- _ra1ObjOverlayLeft = 0;
- _ra1ObjOverlayTop = 0;
- _ra1ObjOverlayWidth = 0;
- _ra1ObjOverlayHeight = 0;
+ resetGameVideoState();
_vm->_smushVideoShouldFinish = false;
_vm->_smushActive = true;
@@ -413,18 +405,8 @@ void SmushPlayer::release() {
} else {
free(_frameBuffer);
_frameBuffer = nullptr;
- if (isRA1()) {
- free(_storedFobjData);
- _storedFobjData = nullptr;
- _storedFobjDataSize = 0;
- _storedFobjCodec = 0;
- _storedFobjParm2 = 0;
- _storedFobjLeft = 0;
- _storedFobjTop = 0;
- _storedFobjWidth = 0;
- _storedFobjHeight = 0;
- }
}
+ releaseGameVideoState();
_IACTstream = nullptr;
@@ -456,58 +438,16 @@ void SmushPlayer::handleStore(int32 subSize, Common::SeekableReadStream &b) {
void SmushPlayer::handleFetch(int32 subSize, Common::SeekableReadStream &b) {
debugC(DEBUG_SMUSH, "SmushPlayer::handleFetch()");
- if (isRA1())
- assert(subSize >= 4);
- else
- assert(subSize >= 6);
+ assert(subSize >= (isRA1() ? 4 : 6));
if (isRA2()) {
ra2HandleFetch(b);
return;
}
- if (isRA1()) {
- // RA1 FTCH re-decodes the raw FOBJ captured by STOR (not a framebuffer memcpy).
- // Chunk layout: BE32 id/flags, BE32 fetchX, BE32 fetchY.
- uint32 fetchId = 0;
- int32 fetchX = 0;
- int32 fetchY = 0;
- if (subSize >= 4)
- fetchId = b.readUint32BE();
- if (subSize >= 12) {
- fetchX = b.readSint32BE();
- fetchY = b.readSint32BE();
- }
-
- if (_storedFobjData != nullptr && _storedFobjDataSize > 0) {
- const int storedCodec = _storedFobjCodec & 0xFF;
- const uint8 storedParam = (uint8)((_storedFobjCodec >> 8) & 0xFF);
- int left = _storedFobjLeft + fetchX;
- int top = _storedFobjTop + fetchY;
-
- if (_insane) {
- InsaneRebel1 *rebel1 = static_cast<InsaneRebel1 *>(_insane);
- // FUN_1FDBC routes FTCH through FUN_28D0A, which forces FUN_20D43
- // flag 0x8 instead of replaying a plain raw FOBJ blit. In LVL2,
- // the stored object is the 320-wide gameplay-window patch, so under
- // ScummVM's 384-buffer crop emulation it must be anchored to the
- // current window origin rather than left in raw chunk space.
- if (rebel1->isInteractiveVideoActive() && rebel1->getActiveGameOpcode() == 0x0B &&
- _storedFobjWidth == _vm->_screenWidth) {
- left += _ra1ViewportOffsetX;
- }
- }
-
- debug("RA1 FTCH: frame=%d id=0x%08x pos=(%d,%d) using stored FOBJ codec=%d size=%dx%d",
- _frame, fetchId, left, top, storedCodec, _storedFobjWidth, _storedFobjHeight);
- decodeFrameObject(storedCodec, _storedFobjData, left, top,
- _storedFobjWidth, _storedFobjHeight, _storedFobjDataSize,
- storedParam, _storedFobjParm2);
- } else {
- debug("RA1 FTCH: frame=%d id=0x%08x with no stored FOBJ data", _frame, fetchId);
- }
+ // RA1 FTCH re-decodes the raw FOBJ captured by STOR (not a framebuffer memcpy).
+ if (handleGameFetch(subSize, b))
return;
- }
if (_frameBuffer != nullptr) {
memcpy(_dst, _frameBuffer, _width * _height);
@@ -715,10 +655,8 @@ void SmushPlayer::handleIACT(int32 subSize, Common::SeekableReadStream &b) {
void SmushPlayer::handleTextResource(uint32 subType, int32 subSize, Common::SeekableReadStream &b) {
// RA1 TEXT chunks have a different format: 2 Ã BE32 header + text data.
// Route to dedicated handler.
- if (isRA1() && subType == MKTAG('T','E','X','T')) {
- ra1HandleText(subSize, b);
+ if (handleGameTextResource(subType, subSize, b))
return;
- }
int pos_x = b.readSint16LE();
int pos_y = b.readSint16LE();
@@ -907,9 +845,7 @@ void SmushPlayer::handleDeltaPalette(int32 subSize, Common::SeekableReadStream &
_pal[i] = CLIP<int32>(_shiftedDeltaPal[i] >> 7, 0, 255);
}
- if (isRA1()) {
- _pal[0] = _pal[1] = _pal[2] = 0;
- }
+ adjustGamePalette();
setDirtyColors(0, 255);
} else {
@@ -933,10 +869,7 @@ void SmushPlayer::handleNewPalette(int32 subSize, Common::SeekableReadStream &b)
return;
readPalette(_pal, b);
-
- if (isRA1()) {
- _pal[0] = _pal[1] = _pal[2] = 0;
- }
+ adjustGamePalette();
setDirtyColors(0, 255);
}
@@ -1678,7 +1611,8 @@ void SmushPlayer::handleFrame(int32 frameSize, Common::SeekableReadStream &b) {
}
objPos += 8 + embSize;
- if (embSize & 1) objPos++;
+ if (embSize & 1)
+ objPos++;
}
free(objBuf);
}
@@ -1711,15 +1645,11 @@ void SmushPlayer::handleFrame(int32 frameSize, Common::SeekableReadStream &b) {
}
}
- // RA1: Re-render saved OBJ cockpit overlay every frame (drawn once in
- // frame 0's OBJ chunk, then scene FOBJs overwrite it each frame).
if (isRA1() && _ra1ObjOverlayData != nullptr && _frame > 0) {
Common::MemoryReadStream overlayStream(_ra1ObjOverlayData, _ra1ObjOverlayDataSize);
handleFrameObject(_ra1ObjOverlayDataSize, overlayStream);
}
- // Snapshot decoded frame before post-render overlays so next frame starts from
- // clean video data (avoids ghost trails from moving HUD/cursor overlays).
if (isRA1() && interactiveRA1 && !forceInteractiveClearRA1 &&
_dst && _width > 0 && _height > 0) {
const int frameBytes = _width * _height;
@@ -1778,14 +1708,7 @@ void SmushPlayer::handleAnimHeader(int32 subSize, Common::SeekableReadStream &b)
if (!_skipPalette) {
byte *palettePtr = &headerContent[6];
memcpy(_pal, palettePtr, sizeof(_pal));
-
- if (isRA1()) {
- // RA1: force palette index 0 to black. Some ANM files store
- // non-black values (e.g. O1OPEN.ANM has palette[0] = blue)
- // but the original engine always displays index 0 as black.
- // XPAL delta animation skips index 0, so it's never corrected.
- _pal[0] = _pal[1] = _pal[2] = 0;
- }
+ adjustGamePalette();
if (isRA2())
ra2ResetDeltaPalette();
@@ -1793,22 +1716,7 @@ void SmushPlayer::handleAnimHeader(int32 subSize, Common::SeekableReadStream &b)
setDirtyColors(0, 255);
}
- if (isRA1()) {
- // v1 AHDR: pre-allocate 384x242 special buffer and set _dst so that
- // procPostRendering has a valid target. Keep _width/_height at 0 so
- // updateScreen() is NOT called until the first FOBJ sets dimensions â
- // this avoids blitting an empty buffer with palette[0] (which may be
- // non-black, e.g. O1OPEN.ANM has palette[0] = blue).
- _width = 0;
- _height = 0;
- int bufSize = 384 * 242;
- if (_specialBuffer == nullptr || bufSize > _specialBufferSize) {
- free(_specialBuffer);
- _specialBuffer = (byte *)calloc(bufSize, 1);
- _specialBufferSize = bufSize;
- }
- _dst = _specialBuffer;
- } else {
+ if (!handleGameAnimHeader(headerContent)) {
_width = READ_LE_UINT16(&headerContent[4]);
_height = READ_LE_UINT16(&headerContent[6]);
}
@@ -1841,6 +1749,9 @@ SmushFont *SmushPlayer::getFont(int font) {
if (_sf[font])
return _sf[font];
+ if (SmushFont *gameFont = getGameFont(font))
+ return gameFont;
+
if (_vm->_game.id == GID_FT) {
if (!((_vm->_game.features & GF_DEMO) && (_vm->_game.platform == Common::kPlatformDOS))) {
const char *ft_fonts[] = {
@@ -1856,8 +1767,6 @@ SmushFont *SmushPlayer::getFont(int font) {
}
} else if (isRA2()) {
return ra2GetFont(font);
- } else if (isRA1()) {
- return ra1GetFont(font);
} else {
int numFonts = (_vm->_game.id == GID_CMI && !(_vm->_game.features & GF_DEMO)) ? 5 : 4;
assert(font >= 0 && font < numFonts);
@@ -1907,23 +1816,14 @@ SmushFont *SmushPlayer::ra1GetFont(int font) {
return _sf[font];
}
-/**
- * RA1 TEXT chunk handler (assembly parity):
- * FUN_1FDBC (0x1FDBC) TEXT path -> FUN_221B7 (0x221B7)
- * - payload uses two BE32 fields + text bytes with 0x00 separators
- * - if first byte is '.', skip that sentinel before rendering
- * - '<'/'>' switch font layers (handled by RA1 font-bank renderer)
- */
void SmushPlayer::ra1HandleText(int32 subSize, Common::SeekableReadStream &b) {
if (subSize < 8 || !_dst || _width <= 0 || _height <= 0)
return;
- InsaneRebel1 *rebel1 = static_cast<InsaneRebel1 *>(_vm->_insane);
+ InsaneRebel1 *rebel1 = static_cast<InsaneRebel1 *>(_insane);
if (!rebel1)
return;
- // FUN_1FDBC TEXT path passes BE32 x/y from payload to FUN_221B7 with 0x200
- // center-alignment flag, so x is an anchor and y is line baseline.
const int textAnchorX = b.readSint32BE();
int cursorY = b.readSint32BE();
@@ -1936,14 +1836,11 @@ void SmushPlayer::ra1HandleText(int32 subSize, Common::SeekableReadStream &b) {
return;
b.read(textBuf, textLen);
- // FUN_1FDBC checks first byte at payload+8 and skips a leading '.'
- // when present (TEXT-at-0x2E sentinel path).
int start = 0;
if (textLen > 0 && textBuf[0] == '.')
start = 1;
int remaining = textLen - start;
-
while (remaining > 0) {
int lineLen = 0;
while (lineLen < remaining && textBuf[start + lineLen] != 0)
@@ -1956,7 +1853,6 @@ void SmushPlayer::ra1HandleText(int32 subSize, Common::SeekableReadStream &b) {
} else {
memcpy(line, textBuf + start, lineLen);
line[lineLen] = '\0';
-
const int drawX = textAnchorX - (rebel1->getFontBankStringWidth(line) / 2);
rebel1->drawFontBankString(_dst, _width, _width, _height, drawX, cursorY, line);
cursorY += rebel1->getFontBankLineAdvance(line);
@@ -1965,6 +1861,7 @@ void SmushPlayer::ra1HandleText(int32 subSize, Common::SeekableReadStream &b) {
} else {
cursorY += rebel1->getFontBankLineAdvance(nullptr);
}
+
int consumed = lineLen;
if (consumed < remaining && textBuf[start + consumed] == 0)
consumed++;
diff --git a/engines/scumm/smush/smush_player.h b/engines/scumm/smush/smush_player.h
index 4c01847882c..de3093cfe59 100644
--- a/engines/scumm/smush/smush_player.h
+++ b/engines/scumm/smush/smush_player.h
@@ -98,11 +98,13 @@ class SmushDeltaBlocksDecoder;
class SmushDeltaGlyphsDecoder;
class IMuseDigital;
class Insane;
+class SmushPlayerRebel1;
class SmushPlayer {
friend class Insane;
friend class InsaneRebel1;
friend class InsaneRebel2;
+ friend class SmushPlayerRebel1;
friend class SmushMultiFont;
private:
struct SmushAudioDispatch {
@@ -261,7 +263,7 @@ private:
public:
SmushPlayer(ScummEngine_v7 *scumm, IMuseDigital *_imuseDigital, Insane *insane);
- ~SmushPlayer();
+ virtual ~SmushPlayer();
void pause();
void unpause();
@@ -306,6 +308,15 @@ protected:
void setScrollOffset(int x, int y);
void seekSan(const char *file, int32 pos, int32 contFrame);
const char *getString(int id);
+ virtual void initGamePlayerFields() {}
+ virtual void destroyGamePlayerFields() {}
+ virtual void resetGameVideoState() {}
+ virtual void releaseGameVideoState() {}
+ virtual bool handleGameFetch(int32 subSize, Common::SeekableReadStream &b) { return false; }
+ virtual bool handleGameTextResource(uint32 subType, int32 subSize, Common::SeekableReadStream &b) { return false; }
+ virtual SmushFont *getGameFont(int font) { return nullptr; }
+ virtual void adjustGamePalette() {}
+ virtual bool handleGameAnimHeader(byte *headerContent) { return false; }
private:
SmushFont *getFont(int font);
@@ -380,6 +391,23 @@ private:
void timerCallback();
};
+class SmushPlayerRebel1 : public SmushPlayer {
+public:
+ SmushPlayerRebel1(ScummEngine_v7 *scumm, IMuseDigital *imuseDigital, Insane *insane);
+ ~SmushPlayerRebel1() override;
+
+protected:
+ void initGamePlayerFields() override;
+ void destroyGamePlayerFields() override;
+ void resetGameVideoState() override;
+ void releaseGameVideoState() override;
+ bool handleGameFetch(int32 subSize, Common::SeekableReadStream &b) override;
+ bool handleGameTextResource(uint32 subType, int32 subSize, Common::SeekableReadStream &b) override;
+ SmushFont *getGameFont(int font) override;
+ void adjustGamePalette() override;
+ bool handleGameAnimHeader(byte *headerContent) override;
+};
+
} // End of namespace Scumm
#endif
diff --git a/engines/scumm/smush/smush_player_ra1.cpp b/engines/scumm/smush/smush_player_ra1.cpp
new file mode 100644
index 00000000000..83a78367bfd
--- /dev/null
+++ b/engines/scumm/smush/smush_player_ra1.cpp
@@ -0,0 +1,162 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+// Rebel Assault 1 specific SmushPlayer methods.
+//
+// Keep these in a dedicated file so the shared smush_player.cpp stays close
+// to upstream while RA1 behavior is isolated in one place.
+
+#include "common/endian.h"
+
+#include "scumm/file.h"
+#include "scumm/scumm_v7.h"
+#include "scumm/smush/smush_font.h"
+#include "scumm/smush/smush_player.h"
+
+#include "scumm/insane/insane_rebel1.h"
+
+namespace Scumm {
+
+SmushPlayerRebel1::SmushPlayerRebel1(ScummEngine_v7 *scumm, IMuseDigital *imuseDigital, Insane *insane)
+ : SmushPlayer(scumm, imuseDigital, insane) {
+ initGamePlayerFields();
+}
+
+SmushPlayerRebel1::~SmushPlayerRebel1() {
+ destroyGamePlayerFields();
+}
+
+void SmushPlayerRebel1::initGamePlayerFields() {
+ _ra1ObjOverlayData = nullptr;
+ _ra1ObjOverlayDataSize = 0;
+ _ra1ObjOverlayCodec = 0;
+ _ra1ObjOverlayLeft = 0;
+ _ra1ObjOverlayTop = 0;
+ _ra1ObjOverlayWidth = 0;
+ _ra1ObjOverlayHeight = 0;
+ _ra1ViewportOffsetX = 0;
+ _ra1ViewportOffsetY = 0;
+}
+
+void SmushPlayerRebel1::destroyGamePlayerFields() {
+ free(_ra1ObjOverlayData);
+ _ra1ObjOverlayData = nullptr;
+ _ra1ObjOverlayDataSize = 0;
+}
+
+void SmushPlayerRebel1::resetGameVideoState() {
+ free(_ra1ObjOverlayData);
+ _ra1ObjOverlayData = nullptr;
+ _ra1ObjOverlayDataSize = 0;
+ _ra1ObjOverlayCodec = 0;
+ _ra1ObjOverlayLeft = 0;
+ _ra1ObjOverlayTop = 0;
+ _ra1ObjOverlayWidth = 0;
+ _ra1ObjOverlayHeight = 0;
+ _ra1ViewportOffsetX = 0;
+ _ra1ViewportOffsetY = 0;
+}
+
+void SmushPlayerRebel1::releaseGameVideoState() {
+ free(_storedFobjData);
+ _storedFobjData = nullptr;
+ _storedFobjDataSize = 0;
+ _storedFobjCodec = 0;
+ _storedFobjParm2 = 0;
+ _storedFobjLeft = 0;
+ _storedFobjTop = 0;
+ _storedFobjWidth = 0;
+ _storedFobjHeight = 0;
+
+ free(_ra1ObjOverlayData);
+ _ra1ObjOverlayData = nullptr;
+ _ra1ObjOverlayDataSize = 0;
+}
+
+bool SmushPlayerRebel1::handleGameFetch(int32 subSize, Common::SeekableReadStream &b) {
+ if (subSize < 4)
+ return false;
+
+ uint32 fetchId = b.readUint32BE();
+ int32 fetchX = 0;
+ int32 fetchY = 0;
+ if (subSize >= 12) {
+ fetchX = b.readSint32BE();
+ fetchY = b.readSint32BE();
+ }
+
+ if (_storedFobjData != nullptr && _storedFobjDataSize > 0) {
+ const int storedCodec = _storedFobjCodec & 0xFF;
+ const uint8 storedParam = (uint8)((_storedFobjCodec >> 8) & 0xFF);
+ int left = _storedFobjLeft + fetchX;
+ int top = _storedFobjTop + fetchY;
+
+ if (_insane) {
+ InsaneRebel1 *rebel1 = static_cast<InsaneRebel1 *>(_insane);
+ if (rebel1->isInteractiveVideoActive() && rebel1->getActiveGameOpcode() == 0x0B &&
+ _storedFobjWidth == _vm->_screenWidth) {
+ left += _ra1ViewportOffsetX;
+ }
+ }
+
+ debug("RA1 FTCH: frame=%d id=0x%08x pos=(%d,%d) using stored FOBJ codec=%d size=%dx%d",
+ _frame, fetchId, left, top, storedCodec, _storedFobjWidth, _storedFobjHeight);
+ decodeFrameObject(storedCodec, _storedFobjData, left, top,
+ _storedFobjWidth, _storedFobjHeight, _storedFobjDataSize,
+ storedParam, _storedFobjParm2);
+ } else {
+ debug("RA1 FTCH: frame=%d id=0x%08x with no stored FOBJ data", _frame, fetchId);
+ }
+
+ return true;
+}
+
+bool SmushPlayerRebel1::handleGameTextResource(uint32 subType, int32 subSize, Common::SeekableReadStream &b) {
+ if (subType != MKTAG('T','E','X','T'))
+ return false;
+
+ ra1HandleText(subSize, b);
+ return true;
+}
+
+SmushFont *SmushPlayerRebel1::getGameFont(int font) {
+ return ra1GetFont(font);
+}
+
+void SmushPlayerRebel1::adjustGamePalette() {
+ _pal[0] = _pal[1] = _pal[2] = 0;
+}
+
+bool SmushPlayerRebel1::handleGameAnimHeader(byte *headerContent) {
+ (void)headerContent;
+ _width = 0;
+ _height = 0;
+ const int bufSize = 384 * 242;
+ if (_specialBuffer == nullptr || bufSize > _specialBufferSize) {
+ free(_specialBuffer);
+ _specialBuffer = (byte *)calloc(bufSize, 1);
+ _specialBufferSize = bufSize;
+ }
+ _dst = _specialBuffer;
+ return true;
+}
+
+} // End of namespace Scumm
Commit: 133d39acac8ff0cb663dc0e4e09164db014fd046
https://github.com/scummvm/scummvm/commit/133d39acac8ff0cb663dc0e4e09164db014fd046
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:35+02:00
Commit Message:
SCUMM: RA: Add additional level handlers
Changed paths:
engines/scumm/insane/insane_rebel1.cpp
engines/scumm/insane/insane_rebel1.h
engines/scumm/insane/insane_rebel1_iact.cpp
engines/scumm/insane/insane_rebel1_levels.cpp
engines/scumm/insane/insane_rebel1_menu.cpp
engines/scumm/insane/insane_rebel1_render.cpp
engines/scumm/insane/insane_rebel1_runlevels.cpp
diff --git a/engines/scumm/insane/insane_rebel1.cpp b/engines/scumm/insane/insane_rebel1.cpp
index d6e80d6a921..d396aa04263 100644
--- a/engines/scumm/insane/insane_rebel1.cpp
+++ b/engines/scumm/insane/insane_rebel1.cpp
@@ -39,7 +39,7 @@ namespace Scumm {
// Per-difficulty tuning tables from assault_data_3.bin
// Indexed: difficulty * 0x28B + level * 0x1F + offset
// Fields: roll, lift, slide, drift, snap, miss, wham, shot, kill
-static const int16 kTuningTable[2][3][9] = {
+static const int16 kTuningTable[6][3][9] = {
// Level 1 (Flight Training)
{
{ 100, 100, 60, 110, 0, 0, 15, 0, 0 }, // Easy
@@ -52,8 +52,32 @@ static const int16 kTuningTable[2][3][9] = {
{ 100, 18, 120, 0, 5, 0, 20, 0, 50 }, // Normal
{ 100, 20, 150, 0, 1, 0, 25, 0, 75 }, // Hard
},
+ // Level 3 (Planet Kolaador)
+ {
+ { 0, 0, 0, 0, 4, 15, 25, 0, 25 }, // Easy
+ { 0, 0, 0, 0, 2, 18, 30, 0, 50 }, // Normal
+ { 0, 0, 0, 0, 0, 20, 35, 0, 75 }, // Hard
+ },
+ // Level 4 (Star Destroyer Attack)
+ {
+ { 70, 100, 150, 90, 0, 0, 20, 0, 0 }, // Easy
+ { 72, 105, 155, 105, 0, 0, 25, 0, 0 }, // Normal
+ { 75, 110, 160, 110, 0, 0, 28, 0, 0 }, // Hard
+ },
+ // Level 5 (Tatooine Attack)
+ {
+ { 0, 0, 0, 0, 2, 11, 0, 4, 25 }, // Easy
+ { 0, 0, 0, 0, 1, 25, 0, 6, 50 }, // Normal
+ { 0, 0, 0, 0, 1, 28, 0, 6, 75 }, // Hard
+ },
+ // Level 6 (Asteroid Field Chase)
+ {
+ { 0, 0, 0, 0, 3, 20, 0, 2, 50 }, // Easy
+ { 0, 0, 0, 0, 1, 25, 0, 5, 100 }, // Normal
+ { 0, 0, 0, 0, 1, 28, 0, 6, 200 }, // Hard
+ },
};
-static const int kNumTunedLevels = 2;
+static const int kNumTunedLevels = 6;
void InsaneRebel1::loadTuningForLevel(int level) {
diff --git a/engines/scumm/insane/insane_rebel1.h b/engines/scumm/insane/insane_rebel1.h
index a49d0676215..e38bc1e3a07 100644
--- a/engines/scumm/insane/insane_rebel1.h
+++ b/engines/scumm/insane/insane_rebel1.h
@@ -107,13 +107,17 @@ private:
// Level 2 flow: NEW â INTRO â PLAY (asteroid dodge) â END/DEATH
bool runLevel2();
+ bool runLevel3();
+ bool runLevel4();
+ bool runLevel5();
+ bool runLevel6();
// Play a passive cinematic (no game callback, skippable)
// startFrame > 0: fast-forward (decode without display) to that frame
void playCinematic(const char *filename, int32 startFrame = 0);
// Play interactive gameplay video (with ship physics + HUD)
- void playInteractiveVideo(const char *filename);
+ void playInteractiveVideo(const char *filename, int32 startFrame = 0);
bool loadRA1Nut(const char *filename, RA1SpriteBank &bank);
void loadLevelSprites(int level);
void updateShipPhysics();
diff --git a/engines/scumm/insane/insane_rebel1_iact.cpp b/engines/scumm/insane/insane_rebel1_iact.cpp
index 623fb05b4af..8747a05ea0c 100644
--- a/engines/scumm/insane/insane_rebel1_iact.cpp
+++ b/engines/scumm/insane/insane_rebel1_iact.cpp
@@ -709,14 +709,12 @@ void InsaneRebel1::handleGameChunk(int32 subSize, Common::SeekableReadStream &b)
b.readUint32BE(); // field3
b.readUint32BE(); // field4
- // RA1 scripts drive progression with GAME counters. In LVL2, finish the
- // interactive SMUSH once the script counter reaches the terminal frame.
- // This avoids getting stuck if container/frame parsing continues past the
- // intended gameplay endpoint.
- if (_interactiveVideoActive && _currentLevel == 1 && maxFrames > 0 &&
+ // RA1 scripts drive progression with GAME counters. Finish 0x0B-driven
+ // interactive videos once the script counter reaches the terminal frame.
+ if (_interactiveVideoActive && maxFrames > 0 &&
_gameCounter >= (int32)maxFrames - 1) {
_vm->_smushVideoShouldFinish = true;
- debug(1, "RA1 L2: finishing interactive video at GAME 0x0B counter=%d/%u", _gameCounter, maxFrames);
+ debug(1, "RA1: finishing 0x0B interactive video at counter=%d/%u", _gameCounter, maxFrames);
}
}
debug(7, "RA1 GAME 0x0B: counter=%d", _gameCounter);
diff --git a/engines/scumm/insane/insane_rebel1_levels.cpp b/engines/scumm/insane/insane_rebel1_levels.cpp
index 0128443f89c..1defb3d8cae 100644
--- a/engines/scumm/insane/insane_rebel1_levels.cpp
+++ b/engines/scumm/insane/insane_rebel1_levels.cpp
@@ -208,7 +208,9 @@ void InsaneRebel1::loadLevelSprites(int level) {
// Ship direction bank â not all levels have one (e.g. Level 2 is first-person)
Common::String bankFile = Common::String::format("LVL%d/L%dBANK1.NUT", level, level);
if (!loadRA1Nut(bankFile.c_str(), _shipBank)) {
- debug(1, "InsaneRebel1: No BANK1 for level %d (first-person level)", level);
+ Common::String legacyBankFile = Common::String::format("LVL%d/L%dBANK.NUT", level, level);
+ if (!loadRA1Nut(legacyBankFile.c_str(), _shipBank))
+ debug(1, "InsaneRebel1: No BANK1/BANK for level %d", level);
}
// Secondary ship bank used by some level-specific handlers (e.g. LVL1 mode-2).
diff --git a/engines/scumm/insane/insane_rebel1_menu.cpp b/engines/scumm/insane/insane_rebel1_menu.cpp
index e1e27853e9a..99f30618d78 100644
--- a/engines/scumm/insane/insane_rebel1_menu.cpp
+++ b/engines/scumm/insane/insane_rebel1_menu.cpp
@@ -29,16 +29,19 @@
namespace Scumm {
+static const int kRA1LevelSelectItemCount = 7;
+static const int kRA1LastImplementedLevel = 6;
+
bool InsaneRebel1::notifyEvent(const Common::Event &event) {
if (_menuActive && _levelSelectActive && event.type == Common::EVENT_KEYDOWN) {
switch (event.kbd.keycode) {
case Common::KEYCODE_UP:
case Common::KEYCODE_w:
- _levelSelectSel = (_levelSelectSel + 2) % 3;
+ _levelSelectSel = (_levelSelectSel + kRA1LevelSelectItemCount - 1) % kRA1LevelSelectItemCount;
return true;
case Common::KEYCODE_DOWN:
case Common::KEYCODE_s:
- _levelSelectSel = (_levelSelectSel + 1) % 3;
+ _levelSelectSel = (_levelSelectSel + 1) % kRA1LevelSelectItemCount;
return true;
case Common::KEYCODE_RETURN:
case Common::KEYCODE_KP_ENTER:
@@ -47,7 +50,7 @@ bool InsaneRebel1::notifyEvent(const Common::Event &event) {
_vm->_smushVideoShouldFinish = true;
return true;
case Common::KEYCODE_ESCAPE:
- _levelSelectSel = 2; // Back
+ _levelSelectSel = kRA1LevelSelectItemCount - 1; // Back
_menuConfirmed = true;
_vm->_smushVideoShouldFinish = true;
return true;
@@ -205,16 +208,20 @@ void InsaneRebel1::renderMainMenuOverlay(byte *dst, int pitch, int width, int he
const int titleW = getTalkTextWidth("LEVEL SELECT");
drawTalkText((width - titleW) / 2, 36, "LEVEL SELECT");
- const char *kLevelItems[3] = {
+ const char *kLevelItems[kRA1LevelSelectItemCount] = {
"LEVEL 1: FLIGHT TRAINING",
"LEVEL 2: ASTEROID FIELD",
+ "LEVEL 3: PLANET KOLAADOR",
+ "LEVEL 4: STAR DESTROYER",
+ "LEVEL 5: TATOOINE ATTACK",
+ "LEVEL 6: ASTEROID CHASE",
"BACK"
};
const int menuY = 60;
const int rowH = 16;
- for (int i = 0; i < 3; i++) {
+ for (int i = 0; i < kRA1LevelSelectItemCount; i++) {
const int textW = getTalkTextWidth(kLevelItems[i]);
const int textX = (width - textW) / 2;
const int y = menuY + i * rowH;
@@ -363,7 +370,7 @@ void InsaneRebel1::runOptionsMenu() {
}
void InsaneRebel1::runLevelSelectMenu() {
- _levelSelectSel = CLIP(_startLevel - 1, 0, 1);
+ _levelSelectSel = CLIP(_startLevel - 1, 0, kRA1LastImplementedLevel - 1);
_levelSelectActive = true;
while (!_vm->shouldQuit()) {
@@ -378,19 +385,14 @@ void InsaneRebel1::runLevelSelectMenu() {
break;
if (_menuConfirmed) {
- switch (_levelSelectSel) {
- case 0:
- _startLevel = 1;
- _levelSelectActive = false;
- return;
- case 1:
- _startLevel = 2;
- _levelSelectActive = false;
- return;
- case 2:
+ if (_levelSelectSel < kRA1LastImplementedLevel) {
+ _startLevel = _levelSelectSel + 1;
_levelSelectActive = false;
return;
}
+
+ _levelSelectActive = false;
+ return;
}
}
_levelSelectActive = false;
diff --git a/engines/scumm/insane/insane_rebel1_render.cpp b/engines/scumm/insane/insane_rebel1_render.cpp
index ea9fdcb5f7a..f814e0ee6b5 100644
--- a/engines/scumm/insane/insane_rebel1_render.cpp
+++ b/engines/scumm/insane/insane_rebel1_render.cpp
@@ -315,8 +315,9 @@ void InsaneRebel1::procPostRendering(byte *renderBitmap, int32 codecparam, int32
if (height == 0) height = _screenHeight;
int pitch = width;
- if (_currentLevel == 1) {
- // Level 2: first-person asteroid dodge â opcode 0x0B (FUN_1CDA7).
+ const bool asteroidMode = (_activeGameOpcode == 0x0B);
+ if (asteroidMode) {
+ // First-person asteroid/surface handler â opcode 0x0B (FUN_1CDA7).
updateAsteroidPhysics();
} else {
const bool turretMode = (_activeGameOpcode == 0x08 || _activeGameOpcode == 0x0A);
@@ -328,7 +329,7 @@ void InsaneRebel1::procPostRendering(byte *renderBitmap, int32 codecparam, int32
// 0x07/0x09/0x19/0x1A -> flight-family handlers
if (turretMode) {
updateTurretPhysics();
- } else if (_shipBank.numSprites > 0 && flightMode) {
+ } else if (flightMode) {
updateShipPhysics();
}
@@ -350,7 +351,7 @@ void InsaneRebel1::procPostRendering(byte *renderBitmap, int32 codecparam, int32
// before HUD/screen copy so 0x0B doesn't lag one frame behind the mouse.
if (_player) {
_player->_ra1ViewportOffsetX = _perspectiveX;
- _player->_ra1ViewportOffsetY = (_activeGameOpcode == 0x0B) ? 0 : _perspectiveY;
+ _player->_ra1ViewportOffsetY = asteroidMode ? 0 : _perspectiveY;
}
// Assembly dispatch (FUN_1BE1B) only runs the targeting/shot overlay pipeline
diff --git a/engines/scumm/insane/insane_rebel1_runlevels.cpp b/engines/scumm/insane/insane_rebel1_runlevels.cpp
index 01ff651e813..99399530357 100644
--- a/engines/scumm/insane/insane_rebel1_runlevels.cpp
+++ b/engines/scumm/insane/insane_rebel1_runlevels.cpp
@@ -106,26 +106,21 @@ void InsaneRebel1::playIntroSequence() {
bool InsaneRebel1::runLevel1() {
debug(1, "InsaneRebel1: Running level 1");
- // Load level sprites (original: pushes L1BANK1..L1BANG NUT filenames)
+ _currentLevel = 0;
+ loadTuningForLevel(0);
loadLevelSprites(1);
- // L1HANGAR.ANM â Hangar departure (original: 0x5918, flags 0x0420)
- // Plays once at level start, never replayed on retry.
playCinematic("LVL1/L1HANGAR.ANM");
if (_vm->shouldQuit())
return false;
- // L1CU1.ANM â Pre-flight cutscene (original: 0x5944, flags 0x0400)
- // Plays once at level start, never replayed on retry.
playCinematic("LVL1/L1CU1.ANM");
if (_vm->shouldQuit())
return false;
- // Retry loop â on death with lives, L1NEW plays then jumps back here
while (!_vm->shouldQuit()) {
bool stage2Started = false;
- // Reset health for this attempt (original: MOV WORD [0x7560], 98 at 0x16214)
_health = kMaxHealth;
_damageFlags = 0;
_prevDamageFlags = 0;
@@ -134,56 +129,74 @@ bool InsaneRebel1::runLevel1() {
_screenFlash = 0;
_frameCounter = 0;
_gameCounter = 0;
+ _activeGameOpcode = 0;
+ _gameLatch5D = 0;
+ _gameLatch5F = 0;
+ _killCount = 0;
+ _targetCount = 0;
+ _prevTargetCount = 0;
+ _lastHitTarget = 0;
+ _shipPosX = kRA1CenterX;
+ _shipPosY = kRA1CenterY;
+ _shipDirIndex = 17;
+ _rollAccum = 0;
+ _liftSmooth = 0;
+ _posAccumX = 0;
+ _posAccumY = 0;
+ _perspectiveX = 0;
+ _perspectiveY = 0;
_pathBranchEnabled = true;
_rightPathSelected = false;
_flyControlMode = 1;
- // Stage 1 flight â L1PLAY1L (hard/left path)
- // The first 394 frames are the common section. At counter 394, if
- // ship is right of center, we switch to L1PLAY1R (easy path).
playInteractiveVideo("LVL1/L1PLAY1L.ANM");
if (_vm->shouldQuit())
return false;
if (_rightPathSelected && _health >= 0) {
- debug(1, "InsaneRebel1: Switching to right path (L1PLAY1R)");
_pathBranchEnabled = false;
_flyControlMode = 1;
- playInteractiveVideo("LVL1/L1PLAY1R.ANM");
+ playInteractiveVideo("LVL1/L1PLAY1R.ANM", 0x187);
if (_vm->shouldQuit())
return false;
}
_pathBranchEnabled = false;
if (_health >= 0) {
- // L1CU2.ANM â Mid-level cutscene (original: 0x5977)
playCinematic("LVL1/L1CU2.ANM");
if (_vm->shouldQuit())
return false;
- // L1PLAY2.ANM â Stage 2 turret (original: 0x5986)
- // Assembly @0x16396..0x163E5 switches to mode 2 and sets
- // FUN_1D79C emitter offsets in DAT_75DC..DAT_75E2.
- _flyControlMode = 2;
- _turretEmitterLeftX = 10;
- _turretEmitterLeftY = -5;
- _turretEmitterRightX = 10;
- _turretEmitterRightY = -5;
- stage2Started = true;
- playInteractiveVideo("LVL1/L1PLAY2.ANM");
- if (_vm->shouldQuit())
- return false;
-
- // TODO: Check score threshold (original: CMP WORD [0x75D0], 5)
- // If score < 5: L1RETRY â retry Stage 2
+ while (!_vm->shouldQuit()) {
+ _flyControlMode = 2;
+ _turretEmitterLeftX = 10;
+ _turretEmitterLeftY = -5;
+ _turretEmitterRightX = 10;
+ _turretEmitterRightY = -5;
+ _activeGameOpcode = 0;
+ _gameLatch5D = 0;
+ _gameLatch5F = 0;
+ _killCount = 0;
+ stage2Started = true;
+
+ playInteractiveVideo("LVL1/L1PLAY2.ANM");
+ if (_vm->shouldQuit())
+ return false;
+
+ if (_health < 0)
+ break;
+
+ if (_killCount > 4) {
+ playCinematic("LVL1/L1END.ANM");
+ return !_vm->shouldQuit();
+ }
- // L1END.ANM â Level complete! (original: 0x59a3)
- playCinematic("LVL1/L1END.ANM");
- return true;
+ playCinematic("LVL1/L1RETRY.ANM");
+ if (_vm->shouldQuit())
+ return false;
+ }
}
- // Death sequence (assembly-verified: 0x165dd / 0x16614):
- // Stage 1 deaths use L1CRASHA; Stage 2 deaths use L1CRASHB.
if (stage2Started)
playCinematic("LVL1/L1CRASHB.ANM");
else
@@ -191,44 +204,33 @@ bool InsaneRebel1::runLevel1() {
if (_vm->shouldQuit())
return false;
- // Assembly order (0x1666B): check lives first; decrement only on retry path.
if (_lives <= 0) {
- // Game over â L1DEATH then return (original: 0x166C0)
playCinematic("LVL1/L1DEATH.ANM");
- debug(1, "InsaneRebel1: Game over (no lives left)");
return false;
}
- // Lives remaining â L1NEW briefing then retry (original: 0x16675)
playCinematic("LVL1/L1NEW.ANM");
if (_vm->shouldQuit())
return false;
_lives--;
-
- // Loop back to gameplay (original: JMP 0x16214 â health reset + Stage 1)
}
return false;
}
-// Level 2: Asteroid Field Training
-// Flow: L2NEW â L2INTRO â L2PLAY (interactive) â L2END/L2DEATH
bool InsaneRebel1::runLevel2() {
debug(1, "InsaneRebel1: Running level 2");
_currentLevel = 1;
- _flyControlMode = 1;
loadLevelSprites(2);
loadTuningForLevel(1);
- // L2INTRO.ANM â intro cutscene (481 frames)
playCinematic("LVL2/L2INTRO.ANM");
if (_vm->shouldQuit())
return false;
- // Retry loop
while (!_vm->shouldQuit()) {
- // Reset state for this attempt
+ _flyControlMode = 0;
_health = kMaxHealth;
_damageFlags = 0;
_prevDamageFlags = 0;
@@ -244,33 +246,297 @@ bool InsaneRebel1::runLevel2() {
_avgInputX = 0;
_avgInputY = 0;
_killCount = 0;
+ _activeGameOpcode = 0;
+ _gameLatch5D = 0;
+ _gameLatch5F = 0;
- // L2PLAY.ANM â asteroid dodge (800 frames, interactive)
playInteractiveVideo("LVL2/L2PLAY.ANM");
if (_vm->shouldQuit())
return false;
if (_health >= 0) {
- // Level complete!
playCinematic("LVL2/L2END.ANM");
- return true;
+ return !_vm->shouldQuit();
+ }
+
+ if (_lives > 0) {
+ playCinematic("LVL2/L2NEW.ANM");
+ if (_vm->shouldQuit())
+ return false;
+ _lives--;
+ continue;
}
- // Death
playCinematic("LVL2/L2DEATH.ANM");
+ return false;
+ }
+
+ return false;
+}
+
+bool InsaneRebel1::runLevel3() {
+ debug(1, "InsaneRebel1: Running level 3");
+
+ _currentLevel = 2;
+ loadLevelSprites(3);
+ loadTuningForLevel(2);
+
+ playCinematic("LVL3/L3INTRO.ANM");
+ if (_vm->shouldQuit())
+ return false;
+
+ while (!_vm->shouldQuit()) {
+ _flyControlMode = 1;
+ _health = kMaxHealth;
+ _damageFlags = 0;
+ _prevDamageFlags = 0;
+ _damageCooldown = 0;
+ _deathTimer = 0;
+ _screenFlash = 0;
+ _frameCounter = 0;
+ _gameCounter = 0;
+ _activeGameOpcode = 0;
+ _gameLatch5D = 0;
+ _gameLatch5F = 0;
+ _killCount = 0;
+ memset(_inputHistoryX, 0, sizeof(_inputHistoryX));
+ memset(_inputHistoryY, 0, sizeof(_inputHistoryY));
+ memset(_viewHistoryX, 0, sizeof(_viewHistoryX));
+ memset(_viewHistoryY, 0, sizeof(_viewHistoryY));
+ _avgInputX = 0;
+ _avgInputY = 0;
+
+ playInteractiveVideo("LVL3/L3PLAY.ANM");
if (_vm->shouldQuit())
return false;
- _lives--;
- if (_lives <= 0) {
- debug(1, "InsaneRebel1: Game over (no lives left)");
+ if (_health >= 0) {
+ playCinematic("LVL3/L3END.ANM");
+ return !_vm->shouldQuit();
+ }
+
+ if (_lives > 0) {
+ playCinematic("LVL3/L3NEW.ANM");
+ if (_vm->shouldQuit())
+ return false;
+ _lives--;
+ continue;
+ }
+
+ playCinematic("LVL3/L3DEATH.ANM");
+ return false;
+ }
+
+ return false;
+}
+
+bool InsaneRebel1::runLevel4() {
+ debug(1, "InsaneRebel1: Running level 4");
+
+ _currentLevel = 3;
+ loadLevelSprites(4);
+ loadTuningForLevel(3);
+
+ playCinematic("LVL4/L4INTRO.ANM");
+ if (_vm->shouldQuit())
+ return false;
+
+ while (!_vm->shouldQuit()) {
+ _flyControlMode = 1;
+ _health = kMaxHealth;
+ _damageFlags = 0;
+ _prevDamageFlags = 0;
+ _damageCooldown = 0;
+ _deathTimer = 0;
+ _screenFlash = 0;
+ _frameCounter = 0;
+ _gameCounter = 0;
+ _activeGameOpcode = 0;
+ _gameLatch5D = 0;
+ _gameLatch5F = 0;
+ _killCount = 0;
+
+ playInteractiveVideo("LVL4/L4PLAY1.ANM");
+ if (_vm->shouldQuit())
+ return false;
+
+ if (_health >= 0) {
+ _activeGameOpcode = 0;
+ _gameLatch5D = 0;
+ _gameLatch5F = 0;
+ _killCount = 0;
+ playInteractiveVideo("LVL4/L4PLAY2.ANM");
+ if (_vm->shouldQuit())
+ return false;
+ }
+
+ if (_health >= 0) {
+ playCinematic((_killCount != 0) ? "LVL4/L4END1.ANM" : "LVL4/L4END2.ANM");
+ return !_vm->shouldQuit();
+ }
+
+ if (_lives > 0) {
+ playCinematic("LVL4/L4NEW.ANM");
+ if (_vm->shouldQuit())
+ return false;
+ _lives--;
+ continue;
+ }
+
+ playCinematic("LVL4/L4DEATH.ANM");
+ return false;
+ }
+
+ return false;
+}
+
+bool InsaneRebel1::runLevel5() {
+ debug(1, "InsaneRebel1: Running level 5");
+
+ _currentLevel = 4;
+ loadLevelSprites(5);
+ loadTuningForLevel(4);
+
+ playCinematic("LVL5/L5INTRO.ANM");
+ if (_vm->shouldQuit())
+ return false;
+
+ while (!_vm->shouldQuit()) {
+ loadRA1Nut("LVL5/L5LASER.NUT", _laserBank);
+ _flyControlMode = 1;
+ _health = kMaxHealth;
+ _damageFlags = 0;
+ _prevDamageFlags = 0;
+ _damageCooldown = 0;
+ _deathTimer = 0;
+ _screenFlash = 0;
+ _frameCounter = 0;
+ _gameCounter = 0;
+ _activeGameOpcode = 0;
+ _gameLatch5D = 0;
+ _gameLatch5F = 0;
+ _killCount = 0;
+ memset(_inputHistoryX, 0, sizeof(_inputHistoryX));
+ memset(_inputHistoryY, 0, sizeof(_inputHistoryY));
+ memset(_viewHistoryX, 0, sizeof(_viewHistoryX));
+ memset(_viewHistoryY, 0, sizeof(_viewHistoryY));
+ _avgInputX = 0;
+ _avgInputY = 0;
+
+ playInteractiveVideo("LVL5/L5PLAY.ANM");
+ if (_vm->shouldQuit())
+ return false;
+
+ if (_health < 0) {
+ if (_lives > 0) {
+ playCinematic("LVL5/L5NEW.ANM");
+ if (_vm->shouldQuit())
+ return false;
+ _lives--;
+ continue;
+ }
+
+ playCinematic("LVL5/L5DEATH.ANM");
return false;
}
- // Retry briefing
- playCinematic("LVL2/L2NEW.ANM");
+ if (_killCount <= 2) {
+ if (_lives > 0) {
+ playCinematic("LVL5/L5RETRY.ANM");
+ if (_vm->shouldQuit())
+ return false;
+ _lives--;
+ continue;
+ }
+
+ playCinematic("LVL5/L5DEATH2.ANM");
+ return false;
+ }
+
+ playCinematic("LVL5/L5BINTRO.ANM");
+ if (_vm->shouldQuit())
+ return false;
+
+ loadRA1Nut("LVL5/L5LASER2.NUT", _laserBank);
+ _activeGameOpcode = 0;
+ _gameLatch5D = 0;
+ _gameLatch5F = 0;
+ _killCount = 0;
+ playInteractiveVideo("LVL5/L5PLAY2.ANM");
+ if (_vm->shouldQuit())
+ return false;
+
+ if (_health >= 0) {
+ playCinematic("LVL5/L5END.ANM");
+ return !_vm->shouldQuit();
+ }
+
+ if (_lives > 0) {
+ playCinematic("LVL5/L5NEW.ANM");
+ if (_vm->shouldQuit())
+ return false;
+ _lives--;
+ continue;
+ }
+
+ playCinematic("LVL5/L5DEATH.ANM");
+ return false;
+ }
+
+ return false;
+}
+
+bool InsaneRebel1::runLevel6() {
+ debug(1, "InsaneRebel1: Running level 6");
+
+ _currentLevel = 5;
+ loadLevelSprites(6);
+ loadTuningForLevel(5);
+
+ playCinematic("LVL6/L6INTRO.ANM");
+ if (_vm->shouldQuit())
+ return false;
+
+ while (!_vm->shouldQuit()) {
+ _flyControlMode = 1;
+ _health = kMaxHealth;
+ _damageFlags = 0;
+ _prevDamageFlags = 0;
+ _damageCooldown = 0;
+ _deathTimer = 0;
+ _screenFlash = 0;
+ _frameCounter = 0;
+ _gameCounter = 0;
+ _activeGameOpcode = 0;
+ _gameLatch5D = 0;
+ _gameLatch5F = 0;
+ _killCount = 0;
+ memset(_inputHistoryX, 0, sizeof(_inputHistoryX));
+ memset(_inputHistoryY, 0, sizeof(_inputHistoryY));
+ memset(_viewHistoryX, 0, sizeof(_viewHistoryX));
+ memset(_viewHistoryY, 0, sizeof(_viewHistoryY));
+ _avgInputX = 0;
+ _avgInputY = 0;
+
+ playInteractiveVideo("LVL6/L6PLAY.ANM");
if (_vm->shouldQuit())
return false;
+
+ if (_health >= 0) {
+ playCinematic("LVL6/L6END.ANM");
+ return !_vm->shouldQuit();
+ }
+
+ if (_lives > 0) {
+ playCinematic("LVL6/L6NEW.ANM");
+ if (_vm->shouldQuit())
+ return false;
+ _lives--;
+ continue;
+ }
+
+ playCinematic("LVL6/L6DEATH.ANM");
+ return false;
}
return false;
@@ -279,6 +545,16 @@ bool InsaneRebel1::runLevel2() {
// Main game entry point â called from ScummEngine::go().
// Matches original flow at 0x15597: intro â menu â level.
void InsaneRebel1::runGame() {
+ typedef bool (InsaneRebel1::*RunLevelMethod)();
+ static const RunLevelMethod kLevelRunners[] = {
+ &InsaneRebel1::runLevel1,
+ &InsaneRebel1::runLevel2,
+ &InsaneRebel1::runLevel3,
+ &InsaneRebel1::runLevel4,
+ &InsaneRebel1::runLevel5,
+ &InsaneRebel1::runLevel6
+ };
+
// Play intro sequence (logo + opening)
playIntroSequence();
if (_vm->shouldQuit())
@@ -292,29 +568,15 @@ void InsaneRebel1::runGame() {
switch (menuResult) {
case 1: {
- bool completed = false;
- if (_startLevel <= 1) {
- // Start from Level 1 (default flow).
- // Assembly Level 1 entry (0x16100) starts with L1HANGAR/L1CU1,
- // not with L1NEW (L1NEW is retry-only at 0x16675).
- completed = runLevel1();
- if (completed && !_vm->shouldQuit()) {
- completed = runLevel2();
- if (completed) {
- debug(1, "InsaneRebel1: Level 2 completed!");
- // TODO: Continue to level 3
- }
- }
- } else if (_startLevel == 2) {
- // Direct Level 2 start from Level Select menu.
- // Assembly Level 2 entry (0x16757) starts at L2INTRO; L2NEW is retry-only.
- completed = runLevel2();
- if (completed) {
- debug(1, "InsaneRebel1: Level 2 completed!");
- }
+ const int startLevel = CLIP<int>(_startLevel, 1, sizeof(kLevelRunners) / sizeof(kLevelRunners[0]));
+ bool completed = true;
+
+ for (int level = startLevel;
+ level <= (int)(sizeof(kLevelRunners) / sizeof(kLevelRunners[0])) && completed && !_vm->shouldQuit();
+ ++level) {
+ completed = (this->*kLevelRunners[level - 1])();
}
_currentLevel = 0;
- // Return to menu after level ends
break;
}
case 2:
@@ -336,8 +598,8 @@ void InsaneRebel1::runGame() {
}
// Play interactive gameplay video (with ship physics + HUD).
-void InsaneRebel1::playInteractiveVideo(const char *filename) {
- debug(1, "InsaneRebel1::playInteractiveVideo('%s')", filename);
+void InsaneRebel1::playInteractiveVideo(const char *filename, int32 startFrame) {
+ debug(1, "InsaneRebel1::playInteractiveVideo('%s', startFrame=%d)", filename, startFrame);
// Stop any leftover audio from previous video
terminateAudio();
@@ -349,6 +611,8 @@ void InsaneRebel1::playInteractiveVideo(const char *filename) {
_interactiveVideoActive = true;
_vm->_smushVideoShouldFinish = false;
splayer->setCurVideoFlags(0x28);
+ if (startFrame > 0)
+ splayer->setFastForwardToFrame(startFrame);
// Center mouse, hide system cursor (we draw our own), lock mouse to window
smush_warpMouse(160, 100, -1);
Commit: 26a309a37aaa4702f7cc4c5184a03544d89cbb1b
https://github.com/scummvm/scummvm/commit/26a309a37aaa4702f7cc4c5184a03544d89cbb1b
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:36+02:00
Commit Message:
SCUMM: RA1: Add projection table for gameplay points
Changed paths:
engines/scumm/insane/insane_rebel1.cpp
engines/scumm/insane/insane_rebel1.h
engines/scumm/insane/insane_rebel1_iact.cpp
engines/scumm/insane/insane_rebel1_render.cpp
diff --git a/engines/scumm/insane/insane_rebel1.cpp b/engines/scumm/insane/insane_rebel1.cpp
index d396aa04263..8424460425d 100644
--- a/engines/scumm/insane/insane_rebel1.cpp
+++ b/engines/scumm/insane/insane_rebel1.cpp
@@ -121,6 +121,8 @@ InsaneRebel1::InsaneRebel1(ScummEngine_v7 *scumm) : Insane(), _vm(scumm) {
_perspectiveX = 0;
_perspectiveY = 0;
+ _projectionCurveExtent = 1;
+ memset(_projectionTable, 0, sizeof(_projectionTable));
memset(_inputHistoryX, 0, sizeof(_inputHistoryX));
memset(_inputHistoryY, 0, sizeof(_inputHistoryY));
diff --git a/engines/scumm/insane/insane_rebel1.h b/engines/scumm/insane/insane_rebel1.h
index e38bc1e3a07..aa7d1255a91 100644
--- a/engines/scumm/insane/insane_rebel1.h
+++ b/engines/scumm/insane/insane_rebel1.h
@@ -87,6 +87,8 @@ public:
int getCurrentLevel() const { return _currentLevel; }
uint16 getActiveGameOpcode() const { return _activeGameOpcode; }
int16 getPerspectiveX() const { return _perspectiveX; }
+ void projectGameplayPoint(int16 &x, int16 &y) const;
+ void unprojectGameplayPoint(int16 &x, int16 &y) const;
// Game flow (matching original at 0x15597)
void runGame();
@@ -123,6 +125,8 @@ private:
void updateShipPhysics();
void updateTurretPhysics();
void preprocessMouseAxes(int16 &inputX, int16 &inputY);
+ void rebuildProjectionTable(int16 curveStep, int16 curveExtent);
+ void resetProjectionTable();
void renderShip(byte *dst, int pitch, int width, int height);
void renderHUD(byte *dst, int pitch, int width, int height);
void renderMainMenuOverlay(byte *dst, int pitch, int width, int height);
@@ -195,6 +199,9 @@ private:
// Perspective view offsets (0x74B6/0x74B8: viewport scroll base)
int16 _perspectiveX;
int16 _perspectiveY;
+ static const int kProjectionTableSize = 80;
+ int16 _projectionCurveExtent;
+ int16 _projectionTable[kProjectionTableSize];
// Input history buffers for 0x0B handler (FUN_1CDA7) â 10-frame averaging
static const int kInputHistorySize = 10;
diff --git a/engines/scumm/insane/insane_rebel1_iact.cpp b/engines/scumm/insane/insane_rebel1_iact.cpp
index 8747a05ea0c..1b53e5473a0 100644
--- a/engines/scumm/insane/insane_rebel1_iact.cpp
+++ b/engines/scumm/insane/insane_rebel1_iact.cpp
@@ -34,13 +34,55 @@ static inline bool isL1Stage2DamageLatch(uint16 code) {
return code >= 6 && code <= 18;
}
-// FUN_223FE (0x223FE) coordinate transform used by FUN_1C54D/FUN_1C6E9.
-// The original applies camera X offset directly and a Y term derived from
-// DAT_41A2 (+ curve-table contribution). In current RA1 integration we keep
-// the camera-offset part, which fixes left/right corridor asymmetry.
-static void transformPoint223FE(int16 &x, int16 &y, int16 cameraX, int16 cameraY) {
- x = (int16)(x - cameraX);
- y = (int16)(y - cameraY);
+void InsaneRebel1::rebuildProjectionTable(int16 curveStep, int16 curveExtent) {
+ _projectionCurveExtent = curveExtent;
+
+ int step = curveStep >> 1;
+ int accum = 0;
+ int threshold = 0;
+ int value = step;
+
+ for (int i = 0; i < kProjectionTableSize / 2; i++) {
+ if (step < 0) {
+ accum -= step;
+ if (threshold < accum) {
+ threshold += kProjectionTableSize / 2;
+ value += 1;
+ }
+ } else {
+ accum += step;
+ if (threshold < accum) {
+ threshold += kProjectionTableSize / 2;
+ value -= 1;
+ }
+ }
+
+ _projectionTable[i] = (int16)value;
+ _projectionTable[kProjectionTableSize - 1 - i] = (int16)-value;
+ }
+}
+
+void InsaneRebel1::resetProjectionTable() {
+ rebuildProjectionTable(0, 1);
+}
+
+void InsaneRebel1::projectGameplayPoint(int16 &x, int16 &y) const {
+ x = (int16)(x - _perspectiveX);
+
+ int curveIndex = 0x4F - (x >> 2);
+ curveIndex = CLIP<int>(curveIndex, 0, kProjectionTableSize - 1);
+
+ const int yCompensation = (_perspectiveY + (_projectionCurveExtent >> 1)) - _projectionTable[curveIndex];
+ y = (int16)(y - yCompensation);
+}
+
+void InsaneRebel1::unprojectGameplayPoint(int16 &x, int16 &y) const {
+ int curveIndex = 0x4F - (x >> 2);
+ curveIndex = CLIP<int>(curveIndex, 0, kProjectionTableSize - 1);
+
+ const int yCompensation = (_perspectiveY + (_projectionCurveExtent >> 1)) - _projectionTable[curveIndex];
+ y = (int16)(y + yCompensation);
+ x = (int16)(x + _perspectiveX);
}
// preprocessMouseAxes â FUN_231BE (0x231BE) centered-axis output law, adapted to
@@ -197,6 +239,11 @@ void InsaneRebel1::updateShipPhysics() {
_perspectiveX = CLIP<int16>((int16)(_shipPosX - kRA1CenterX + 0x20), 0, 0x40);
_perspectiveY = CLIP<int16>((int16)(_shipPosY - kRA1CenterY + 0x17), 0, 0x2E);
+ // FUN_1DEB5 updates the curve table via FUN_22549 after SetCameraOffset.
+ // The full DOS path blends a few roll-history terms; use the current roll
+ // accumulator so side-looking still bends the gameplay projection.
+ rebuildProjectionTable(CLIP<int16>((int16)(-(_rollAccum >> 7)), -0x1A, 0x1A), 0x1A);
+
// --- Step 9: Direction sprite index (FUN_1DEB5 LAB_1e23e) ---
// Horizontal component from _74CA (rollAccum):
// |rollAccum| <= 0x80: center (0)
@@ -387,6 +434,10 @@ void InsaneRebel1::updateTurretPhysics() {
_perspectiveX = CLIP<int16>((int16)(offsetX + 0x20), 0, 0x40);
_perspectiveY = CLIP<int16>((int16)(offsetY + 0x17), 0, 0x2E);
+ // FUN_1E6A7 rebuilds the side-look curve with a shallower table than the
+ // main flight handler, derived directly from roll.
+ rebuildProjectionTable((int16)(-(_rollAccum >> 9)), 0x0D);
+
// Direction bucket synthesis from FUN_1E6A7.
int dir = 0;
if (_flyControlMode == 2) {
@@ -538,6 +589,7 @@ void InsaneRebel1::updateAsteroidPhysics() {
int16 avgViewY = (int16)(sumViewY / kAsteroidSmoothWindow);
_perspectiveX = CLIP<int16>((int16)((avgViewX >> 1) + 0x20), 0, 0x40);
_perspectiveY = CLIP<int16>((int16)((avgViewY >> 1) + 0x17), 0, 0x2E);
+ resetProjectionTable();
_frameCounter++;
@@ -621,6 +673,7 @@ void InsaneRebel1::handleGameChunk(int32 subSize, Common::SeekableReadStream &b)
}
_activeGameOpcode = 0;
+ resetProjectionTable();
debug(5, "RA1 GAME 0x5E: reset state field1=%d mode=%d", (int32)param1, (int)_flyControlMode);
break;
@@ -660,7 +713,7 @@ void InsaneRebel1::handleGameChunk(int32 subSize, Common::SeekableReadStream &b)
int16 centerX = corridorLeft + corridorWidth / 2;
int16 centerY = corridorTop + corridorHeight / 2;
- transformPoint223FE(centerX, centerY, _perspectiveX, _perspectiveY);
+ projectGameplayPoint(centerX, centerY);
_corridorLeftX = centerX - corridorWidth / 2;
_corridorTopY = centerY - corridorHeight / 2;
@@ -684,7 +737,7 @@ void InsaneRebel1::handleGameChunk(int32 subSize, Common::SeekableReadStream &b)
int16 centerX = zoneLeft + zoneWidth / 2;
int16 centerY = zoneTop + zoneHeight / 2;
- transformPoint223FE(centerX, centerY, _perspectiveX, _perspectiveY);
+ projectGameplayPoint(centerX, centerY);
zoneLeft = centerX - zoneWidth / 2;
zoneTop = centerY - zoneHeight / 2;
@@ -804,6 +857,8 @@ void InsaneRebel1::checkTargetHit(int16 targetIdx, int16 left, int16 top, int16
int16 curY = _shipPosY;
const int slot = _targetCount;
+ unprojectGameplayPoint(curX, curY);
+
if (slot < kMaxTargetBoxes) {
_targetBoxX[slot] = (int16)((left + right) / 2);
_targetBoxY[slot] = (int16)((top + bottom) / 2);
@@ -848,10 +903,13 @@ void InsaneRebel1::checkTargetHit(int16 targetIdx, int16 left, int16 top, int16
_score += _tuning.kill;
_killCount++;
- // Snap cursor to target center (original: _DAT_74BE/74C0 = target center)
+ // Snap cursor to target center and re-project it, matching FUN_1C0EF.
if (snap > 0) {
- _shipPosX = (left + right) / 2;
- _shipPosY = (top + bottom) / 2;
+ int16 lockX = (left + right) / 2;
+ int16 lockY = (top + bottom) / 2;
+ projectGameplayPoint(lockX, lockY);
+ _shipPosX = lockX;
+ _shipPosY = lockY;
}
debug(5, "RA1 HIT: target=%d score=%d kills=%d", targetIdx, _score, _killCount);
diff --git a/engines/scumm/insane/insane_rebel1_render.cpp b/engines/scumm/insane/insane_rebel1_render.cpp
index f814e0ee6b5..840e355a9d8 100644
--- a/engines/scumm/insane/insane_rebel1_render.cpp
+++ b/engines/scumm/insane/insane_rebel1_render.cpp
@@ -386,14 +386,15 @@ void InsaneRebel1::procPostRendering(byte *renderBitmap, int32 codecparam, int32
// renderTargetBoxes â FUN_1C940 (0x1C940). Per-target green box overlays.
void InsaneRebel1::renderTargetBoxes(byte *dst, int pitch, int width, int height) {
- const int overlayX = ra1OverlayViewOffsetX(this);
-
for (int i = _targetCount - 1; i >= 0; --i) {
if (i >= kMaxTargetBoxes)
continue;
char box[4] = { '<', '<', (char)('i' + CLIP<int16>(_targetBoxVariant[i], 0, 5)), '\0' };
- drawFontBankString(dst, pitch, width, height, overlayX + _targetBoxX[i], _targetBoxY[i], box);
+ int16 drawX = _targetBoxX[i];
+ int16 drawY = _targetBoxY[i];
+ projectGameplayPoint(drawX, drawY);
+ drawFontBankString(dst, pitch, width, height, drawX, drawY, box);
}
_prevTargetCount = _targetCount;
@@ -445,7 +446,6 @@ void InsaneRebel1::renderGostSlots(byte *dst, int pitch, int width, int height)
if (_bangBank.numSprites <= 0)
return;
- const int overlayX = ra1OverlayViewOffsetX(this);
for (int i = 0; i < kMaxGostSlots; i++) {
if (_gostSlots[i].targetId != 0 && _gostSlots[i].frame < 10) {
int sprIdx = _gostSlots[i].frame;
@@ -453,8 +453,11 @@ void InsaneRebel1::renderGostSlots(byte *dst, int pitch, int width, int height)
sprIdx = _bangBank.numSprites - 1;
const RA1Sprite &spr = _bangBank.sprites[sprIdx];
- int drawX = overlayX + _gostSlots[i].posX - spr.width / 2;
- int drawY = _gostSlots[i].posY - spr.height / 2;
+ int16 drawX = _gostSlots[i].posX;
+ int16 drawY = _gostSlots[i].posY;
+ projectGameplayPoint(drawX, drawY);
+ drawX -= spr.width / 2;
+ drawY -= spr.height / 2;
renderSprite(dst, pitch, width, height, drawX, drawY, spr);
_gostSlots[i].frame++;
Commit: 4ceacc55a2a2637974229657924f38f72e5939b4
https://github.com/scummvm/scummvm/commit/4ceacc55a2a2637974229657924f38f72e5939b4
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:36+02:00
Commit Message:
SCUMM: RA1: Track frame object hit state
Changed paths:
engines/scumm/insane/insane_rebel1.cpp
engines/scumm/insane/insane_rebel1.h
engines/scumm/insane/insane_rebel1_iact.cpp
engines/scumm/insane/insane_rebel1_render.cpp
engines/scumm/insane/insane_rebel1_runlevels.cpp
engines/scumm/smush/smush_player.cpp
diff --git a/engines/scumm/insane/insane_rebel1.cpp b/engines/scumm/insane/insane_rebel1.cpp
index 8424460425d..8c1c7d38e93 100644
--- a/engines/scumm/insane/insane_rebel1.cpp
+++ b/engines/scumm/insane/insane_rebel1.cpp
@@ -192,6 +192,7 @@ InsaneRebel1::InsaneRebel1(ScummEngine_v7 *scumm) : Insane(), _vm(scumm) {
_gostSlotIdx = 0;
_killCount = 0;
_lastHitTarget = 0;
+ resetFrameObjectState();
if (loadRA1Nut("SYS/TALKFONT.NUT", _hudFontBank)) {
debug(1, "InsaneRebel1: HUD/menu glyph font loaded from SYS/TALKFONT.NUT (%d chars)", _hudFontBank.numSprites);
diff --git a/engines/scumm/insane/insane_rebel1.h b/engines/scumm/insane/insane_rebel1.h
index aa7d1255a91..101c5f6b50b 100644
--- a/engines/scumm/insane/insane_rebel1.h
+++ b/engines/scumm/insane/insane_rebel1.h
@@ -89,6 +89,9 @@ public:
int16 getPerspectiveX() const { return _perspectiveX; }
void projectGameplayPoint(int16 &x, int16 &y) const;
void unprojectGameplayPoint(int16 &x, int16 &y) const;
+ bool handleFrameObjectTarget(int16 objectId, int16 left, int16 top, int16 width, int16 height,
+ int codec, uint8 &ra1Param);
+ void resetFrameObjectState();
// Game flow (matching original at 0x15597)
void runGame();
@@ -137,6 +140,8 @@ private:
void renderLaserShots(byte *dst, int pitch, int width, int height);
void renderSprite(byte *dst, int pitch, int width, int height,
int x, int y, const RA1Sprite &sprite);
+ void updateGostSlotPosition(int16 targetIdx, int16 left, int16 top, int16 right, int16 bottom);
+ void applyFrameObjectHitState(int16 targetIdx);
// Shooting pipeline â FUN_1CCA0 (0x1CCA0) shot spawner,
// FUN_1C0EF (0x1C0EF) target detection, FUN_1C940 (0x1C940) shot processing
@@ -350,6 +355,9 @@ private:
int16 _killCount; // 0x75D0: targets destroyed this stage
int16 _lastHitTarget; // 0x75D6: prevents double-hit on same target
+
+ static const int kFrameObjectStateBytes = 300;
+ byte _frameObjectState[kFrameObjectStateBytes];
};
} // End of namespace Scumm
diff --git a/engines/scumm/insane/insane_rebel1_iact.cpp b/engines/scumm/insane/insane_rebel1_iact.cpp
index 1b53e5473a0..d48edddae88 100644
--- a/engines/scumm/insane/insane_rebel1_iact.cpp
+++ b/engines/scumm/insane/insane_rebel1_iact.cpp
@@ -34,6 +34,80 @@ static inline bool isL1Stage2DamageLatch(uint16 code) {
return code >= 6 && code <= 18;
}
+void InsaneRebel1::resetFrameObjectState() {
+ memset(_frameObjectState, 0, sizeof(_frameObjectState));
+ for (int i = 0x50; i < 0x96; i++)
+ _frameObjectState[i] = 0xFF;
+}
+
+void InsaneRebel1::updateGostSlotPosition(int16 targetIdx, int16 left, int16 top, int16 right, int16 bottom) {
+ const int16 targetKey = targetIdx + 1;
+ for (int i = 0; i < kMaxGostSlots; i++) {
+ if (_gostSlots[i].targetId == targetKey && _gostSlots[i].frame < 10) {
+ _gostSlots[i].posX = (left + right) / 2;
+ _gostSlots[i].posY = (top + bottom) / 2;
+ }
+ }
+}
+
+void InsaneRebel1::applyFrameObjectHitState(int16 targetIdx) {
+ if (targetIdx < 0)
+ return;
+
+ const int byteIndex = targetIdx >> 3;
+ if (byteIndex < 0 || byteIndex >= 0x96 || byteIndex >= kFrameObjectStateBytes)
+ return;
+
+ const byte bit = (byte)(0x80 >> (targetIdx & 7));
+ const int altIndex = byteIndex + 0x96;
+ if (altIndex >= kFrameObjectStateBytes)
+ return;
+
+ if ((_frameObjectState[altIndex] & bit) == 0)
+ _frameObjectState[byteIndex] |= bit;
+ else
+ _frameObjectState[altIndex] &= ~bit;
+}
+
+bool InsaneRebel1::handleFrameObjectTarget(int16 objectId, int16 left, int16 top, int16 width, int16 height,
+ int codec, uint8 &ra1Param) {
+ if (!_interactiveVideoActive)
+ return true;
+
+ int absObjectId = (objectId < 0) ? -objectId : objectId;
+ if (absObjectId == 0)
+ return true;
+
+ const int bitIndex = absObjectId - 1;
+ const int byteIndex = bitIndex >> 3;
+ if (byteIndex < 0 || byteIndex >= 0x96 || byteIndex >= kFrameObjectStateBytes)
+ return true;
+
+ const byte bit = (byte)(0x80 >> (bitIndex & 7));
+ const int altIndex = byteIndex + 0x96;
+ const bool primarySet = (_frameObjectState[byteIndex] & bit) != 0;
+ const bool secondarySet = (altIndex < kFrameObjectStateBytes) && ((_frameObjectState[altIndex] & bit) != 0);
+ const int16 right = left + width;
+ const int16 bottom = top + height;
+
+ if (objectId > 0 && objectId < 0x280) {
+ if (!primarySet || secondarySet)
+ checkTargetHit(objectId - 1, left, top, right, bottom);
+ else
+ updateGostSlotPosition(objectId - 1, left, top, right, bottom);
+ }
+
+ const bool updatedPrimarySet = (_frameObjectState[byteIndex] & bit) != 0;
+ const bool updatedSecondarySet = (altIndex < kFrameObjectStateBytes) && ((_frameObjectState[altIndex] & bit) != 0);
+ if (updatedPrimarySet)
+ return false;
+
+ if (updatedSecondarySet && codec == 0x17)
+ ra1Param = (uint8)(ra1Param - 0x10);
+
+ return true;
+}
+
void InsaneRebel1::rebuildProjectionTable(int16 curveStep, int16 curveExtent) {
_projectionCurveExtent = curveExtent;
@@ -850,14 +924,14 @@ void InsaneRebel1::processShot() {
}
// checkTargetHit â FUN_1C0EF (0x1C0EF). AABB target detection with snap tolerance.
-// Called from GAME 0x5A handler. Checks cursor proximity and shot hits.
+// The original compares target bounds against the cursor after
+// UnprojectScreenPoint(), then reprojects the snapped cursor center after a hit.
void InsaneRebel1::checkTargetHit(int16 targetIdx, int16 left, int16 top, int16 right, int16 bottom) {
int16 snap = _tuning.snap;
int16 curX = _shipPosX;
int16 curY = _shipPosY;
- const int slot = _targetCount;
-
unprojectGameplayPoint(curX, curY);
+ const int slot = _targetCount;
if (slot < kMaxTargetBoxes) {
_targetBoxX[slot] = (int16)((left + right) / 2);
@@ -902,14 +976,14 @@ void InsaneRebel1::checkTargetHit(int16 targetIdx, int16 left, int16 top, int16
_lastHitTarget = targetIdx + 1;
_score += _tuning.kill;
_killCount++;
+ applyFrameObjectHitState(targetIdx);
- // Snap cursor to target center and re-project it, matching FUN_1C0EF.
+ // Match FUN_1C0EF: snap in unprojected space, then project back
+ // into the current gameplay window before rendering the pointer.
if (snap > 0) {
- int16 lockX = (left + right) / 2;
- int16 lockY = (top + bottom) / 2;
- projectGameplayPoint(lockX, lockY);
- _shipPosX = lockX;
- _shipPosY = lockY;
+ _shipPosX = (left + right) / 2;
+ _shipPosY = (top + bottom) / 2;
+ projectGameplayPoint(_shipPosX, _shipPosY);
}
debug(5, "RA1 HIT: target=%d score=%d kills=%d", targetIdx, _score, _killCount);
diff --git a/engines/scumm/insane/insane_rebel1_render.cpp b/engines/scumm/insane/insane_rebel1_render.cpp
index 840e355a9d8..f814e0ee6b5 100644
--- a/engines/scumm/insane/insane_rebel1_render.cpp
+++ b/engines/scumm/insane/insane_rebel1_render.cpp
@@ -386,15 +386,14 @@ void InsaneRebel1::procPostRendering(byte *renderBitmap, int32 codecparam, int32
// renderTargetBoxes â FUN_1C940 (0x1C940). Per-target green box overlays.
void InsaneRebel1::renderTargetBoxes(byte *dst, int pitch, int width, int height) {
+ const int overlayX = ra1OverlayViewOffsetX(this);
+
for (int i = _targetCount - 1; i >= 0; --i) {
if (i >= kMaxTargetBoxes)
continue;
char box[4] = { '<', '<', (char)('i' + CLIP<int16>(_targetBoxVariant[i], 0, 5)), '\0' };
- int16 drawX = _targetBoxX[i];
- int16 drawY = _targetBoxY[i];
- projectGameplayPoint(drawX, drawY);
- drawFontBankString(dst, pitch, width, height, drawX, drawY, box);
+ drawFontBankString(dst, pitch, width, height, overlayX + _targetBoxX[i], _targetBoxY[i], box);
}
_prevTargetCount = _targetCount;
@@ -446,6 +445,7 @@ void InsaneRebel1::renderGostSlots(byte *dst, int pitch, int width, int height)
if (_bangBank.numSprites <= 0)
return;
+ const int overlayX = ra1OverlayViewOffsetX(this);
for (int i = 0; i < kMaxGostSlots; i++) {
if (_gostSlots[i].targetId != 0 && _gostSlots[i].frame < 10) {
int sprIdx = _gostSlots[i].frame;
@@ -453,11 +453,8 @@ void InsaneRebel1::renderGostSlots(byte *dst, int pitch, int width, int height)
sprIdx = _bangBank.numSprites - 1;
const RA1Sprite &spr = _bangBank.sprites[sprIdx];
- int16 drawX = _gostSlots[i].posX;
- int16 drawY = _gostSlots[i].posY;
- projectGameplayPoint(drawX, drawY);
- drawX -= spr.width / 2;
- drawY -= spr.height / 2;
+ int drawX = overlayX + _gostSlots[i].posX - spr.width / 2;
+ int drawY = _gostSlots[i].posY - spr.height / 2;
renderSprite(dst, pitch, width, height, drawX, drawY, spr);
_gostSlots[i].frame++;
diff --git a/engines/scumm/insane/insane_rebel1_runlevels.cpp b/engines/scumm/insane/insane_rebel1_runlevels.cpp
index 99399530357..b84b0cdd16a 100644
--- a/engines/scumm/insane/insane_rebel1_runlevels.cpp
+++ b/engines/scumm/insane/insane_rebel1_runlevels.cpp
@@ -609,6 +609,7 @@ void InsaneRebel1::playInteractiveVideo(const char *filename, int32 startFrame)
_player = splayer;
clearBit(0);
_interactiveVideoActive = true;
+ resetFrameObjectState();
_vm->_smushVideoShouldFinish = false;
splayer->setCurVideoFlags(0x28);
if (startFrame > 0)
diff --git a/engines/scumm/smush/smush_player.cpp b/engines/scumm/smush/smush_player.cpp
index b86c86f4f31..0c8ad49928d 100644
--- a/engines/scumm/smush/smush_player.cpp
+++ b/engines/scumm/smush/smush_player.cpp
@@ -1316,6 +1316,7 @@ void SmushPlayer::handleFrameObject(int32 subSize, Common::SeekableReadStream &b
int codec = b.readUint16LE();
uint8 ra1Param = 0; // RA1: palette base byte (FOBJ byte[1])
+ uint16 ra1ObjectId = 0; // RA1: object id / event target id (FOBJ bytes[10-11])
uint16 ra1Parm2 = 0; // RA1: tile count (FOBJ bytes[12-13])
if (isRA1()) {
ra1Param = (codec >> 8) & 0xFF; // byte[1] = palette base (e.g. 0xF0)
@@ -1328,7 +1329,7 @@ void SmushPlayer::handleFrameObject(int32 subSize, Common::SeekableReadStream &b
int width = b.readUint16LE();
int height = b.readUint16LE();
- b.readUint16LE();
+ ra1ObjectId = b.readUint16LE();
ra1Parm2 = b.readUint16LE();
debugC(DEBUG_SMUSH, "SmushPlayer::handleFrameObject: frame=%d codec=%d pos=(%d,%d) size=%dx%d dataSize=%d",
@@ -1339,8 +1340,8 @@ void SmushPlayer::handleFrameObject(int32 subSize, Common::SeekableReadStream &b
_frame, codec, left, top, width, height, subSize - 14, _storeFrame, _width, _height);
}
if (isRA1()) {
- debug("RA1 FOBJ: frame=%d codec=%d pos=(%d,%d) size=%dx%d dataSize=%d storeFrame=%d",
- _frame, codec, left, top, width, height, subSize - 14, _storeFrame);
+ debug("RA1 FOBJ: frame=%d codec=%d object=%d pos=(%d,%d) size=%dx%d dataSize=%d storeFrame=%d",
+ _frame, codec, ra1ObjectId, left, top, width, height, subSize - 14, _storeFrame);
}
int32 chunk_size = subSize - 14;
@@ -1373,6 +1374,15 @@ void SmushPlayer::handleFrameObject(int32 subSize, Common::SeekableReadStream &b
_storeFrame = false;
}
+ if (isRA1() && _insane) {
+ InsaneRebel1 *rebel1 = static_cast<InsaneRebel1 *>(_insane);
+ if (!rebel1->handleFrameObjectTarget((int16)ra1ObjectId, (int16)rawLeft, (int16)rawTop,
+ (int16)width, (int16)height, codec, ra1Param)) {
+ free(chunk_buffer);
+ return;
+ }
+ }
+
decodeFrameObject(codec, chunk_buffer, left, top, width, height, chunk_size, ra1Param, ra1Parm2);
free(chunk_buffer);
Commit: 851bb8fcba206f2e5c78ef739d7108e03f125800
https://github.com/scummvm/scummvm/commit/851bb8fcba206f2e5c78ef739d7108e03f125800
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:36+02:00
Commit Message:
SCUMM: RA1: Add hitboxes
Changed paths:
engines/scumm/insane/insane_rebel1_render.cpp
diff --git a/engines/scumm/insane/insane_rebel1_render.cpp b/engines/scumm/insane/insane_rebel1_render.cpp
index f814e0ee6b5..58460e65103 100644
--- a/engines/scumm/insane/insane_rebel1_render.cpp
+++ b/engines/scumm/insane/insane_rebel1_render.cpp
@@ -100,6 +100,40 @@ static void drawBankString(const RA1SpriteBank &bank, byte *dst, int pitch, int
}
}
+static const RA1Sprite *lookupBankGlyph(const RA1SpriteBank &bank, char ch) {
+ if (bank.numSprites <= 0)
+ return nullptr;
+ if ((byte)ch < 0x21)
+ return nullptr;
+
+ const int fontIdx = (int)(byte)ch - 0x21;
+ if (fontIdx < 0 || fontIdx >= bank.numSprites)
+ return nullptr;
+
+ const RA1Sprite &glyph = bank.sprites[fontIdx];
+ if (!glyph.data || glyph.width <= 0 || glyph.height <= 0)
+ return nullptr;
+
+ return &glyph;
+}
+
+// Glyph markers in FUN_1C940/FUN_1CB22 go through DrawStringEx(..., flags=3),
+// which centers the glyph and ignores the NUT x/y offsets. Use the same anchor
+// rules here instead of the generic left-anchored text path.
+static void drawCenteredBankGlyph(const RA1SpriteBank &bank, byte *dst, int pitch, int width, int height,
+ int centerX, int centerY, char ch) {
+ char glyphStr[2] = { ch, '\0' };
+ const RA1Sprite *glyph = lookupBankGlyph(bank, ch);
+ if (!glyph) {
+ drawBankString(bank, dst, pitch, width, height, centerX, centerY, glyphStr);
+ return;
+ }
+
+ const int drawX = centerX - glyph->xoffs - (int)glyph->width / 2;
+ const int drawY = centerY - glyph->yoffs - (int)glyph->height / 2;
+ drawBankString(bank, dst, pitch, width, height, drawX, drawY, glyphStr);
+}
+
static int getBankStringWidth(const RA1SpriteBank &bank, const char *text) {
if (!text || bank.numSprites <= 0)
return 0;
@@ -387,13 +421,21 @@ void InsaneRebel1::procPostRendering(byte *renderBitmap, int32 codecparam, int32
// renderTargetBoxes â FUN_1C940 (0x1C940). Per-target green box overlays.
void InsaneRebel1::renderTargetBoxes(byte *dst, int pitch, int width, int height) {
const int overlayX = ra1OverlayViewOffsetX(this);
+ const RA1SpriteBank &markerBank = (_techFontBank.numSprites > 0) ? _techFontBank : _hudFontBank;
+ const bool projectTargetMarkers = (_activeGameOpcode == 0x0B);
for (int i = _targetCount - 1; i >= 0; --i) {
if (i >= kMaxTargetBoxes)
continue;
- char box[4] = { '<', '<', (char)('i' + CLIP<int16>(_targetBoxVariant[i], 0, 5)), '\0' };
- drawFontBankString(dst, pitch, width, height, overlayX + _targetBoxX[i], _targetBoxY[i], box);
+ int16 drawX = _targetBoxX[i];
+ int16 drawY = _targetBoxY[i];
+ if (projectTargetMarkers)
+ drawX = (int16)(drawX - _perspectiveX);
+
+ const char boxGlyph = (char)('i' + CLIP<int16>(_targetBoxVariant[i], 0, 5));
+ drawCenteredBankGlyph(markerBank, dst, pitch, width, height,
+ overlayX + drawX, drawY, boxGlyph);
}
_prevTargetCount = _targetCount;
@@ -414,9 +456,9 @@ void InsaneRebel1::renderTargeting(byte *dst, int pitch, int width, int height)
// Lock indicator at fixed center positions:
// FUN_1CB22 draws marker strings at (0xA0,0x78) and (0xA0,0x7E).
if (_targetProximity > 0) {
- drawBankString(markerBank, dst, pitch, width, height, overlayX + 0xA0, 0x78, "]");
+ drawCenteredBankGlyph(markerBank, dst, pitch, width, height, overlayX + 0xA0, 0x78, ']');
if (_targetProximity > 1)
- drawBankString(markerBank, dst, pitch, width, height, overlayX + 0xA0, 0x7E, "a");
+ drawCenteredBankGlyph(markerBank, dst, pitch, width, height, overlayX + 0xA0, 0x7E, 'a');
}
// Pointer glyph at current aim position. Original uses two variants:
@@ -429,8 +471,7 @@ void InsaneRebel1::renderTargeting(byte *dst, int pitch, int width, int height)
int cursorX = CLIP<int>(overlayX + _shipPosX, 0, width - 1);
int cursorY = CLIP<int>(_shipPosY, 0, height - 1);
- int markerW = getBankStringWidth(markerBank, marker);
- drawBankString(markerBank, dst, pitch, width, height, cursorX - markerW / 2, cursorY, marker);
+ drawCenteredBankGlyph(markerBank, dst, pitch, width, height, cursorX, cursorY, marker[0]);
}
// Save previous proximity for next frame
@@ -446,6 +487,7 @@ void InsaneRebel1::renderGostSlots(byte *dst, int pitch, int width, int height)
return;
const int overlayX = ra1OverlayViewOffsetX(this);
+ const bool projectGostMarkers = (_activeGameOpcode == 0x0B);
for (int i = 0; i < kMaxGostSlots; i++) {
if (_gostSlots[i].targetId != 0 && _gostSlots[i].frame < 10) {
int sprIdx = _gostSlots[i].frame;
@@ -453,8 +495,13 @@ void InsaneRebel1::renderGostSlots(byte *dst, int pitch, int width, int height)
sprIdx = _bangBank.numSprites - 1;
const RA1Sprite &spr = _bangBank.sprites[sprIdx];
- int drawX = overlayX + _gostSlots[i].posX - spr.width / 2;
- int drawY = _gostSlots[i].posY - spr.height / 2;
+ int16 centerX = _gostSlots[i].posX;
+ int16 centerY = _gostSlots[i].posY;
+ if (projectGostMarkers)
+ centerX = (int16)(centerX - _perspectiveX);
+
+ int drawX = overlayX + centerX - spr.width / 2;
+ int drawY = centerY - spr.height / 2;
renderSprite(dst, pitch, width, height, drawX, drawY, spr);
_gostSlots[i].frame++;
Commit: cecedef4f75dc5948a72297a8cb400015bf3f06b
https://github.com/scummvm/scummvm/commit/cecedef4f75dc5948a72297a8cb400015bf3f06b
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:37+02:00
Commit Message:
SCUMM: RA1: Apply overlay Y offsets
Changed paths:
engines/scumm/insane/insane_rebel1.h
engines/scumm/insane/insane_rebel1_render.cpp
engines/scumm/smush/smush_player.cpp
diff --git a/engines/scumm/insane/insane_rebel1.h b/engines/scumm/insane/insane_rebel1.h
index 101c5f6b50b..6d3aa92d591 100644
--- a/engines/scumm/insane/insane_rebel1.h
+++ b/engines/scumm/insane/insane_rebel1.h
@@ -87,6 +87,7 @@ public:
int getCurrentLevel() const { return _currentLevel; }
uint16 getActiveGameOpcode() const { return _activeGameOpcode; }
int16 getPerspectiveX() const { return _perspectiveX; }
+ int16 getPerspectiveY() const { return _perspectiveY; }
void projectGameplayPoint(int16 &x, int16 &y) const;
void unprojectGameplayPoint(int16 &x, int16 &y) const;
bool handleFrameObjectTarget(int16 objectId, int16 left, int16 top, int16 width, int16 height,
diff --git a/engines/scumm/insane/insane_rebel1_render.cpp b/engines/scumm/insane/insane_rebel1_render.cpp
index 58460e65103..ca56df82c2f 100644
--- a/engines/scumm/insane/insane_rebel1_render.cpp
+++ b/engines/scumm/insane/insane_rebel1_render.cpp
@@ -37,6 +37,13 @@ static inline int ra1OverlayViewOffsetX(const InsaneRebel1 *rebel1) {
return (rebel1->getActiveGameOpcode() == 0x0B) ? rebel1->getPerspectiveX() : 0;
}
+static inline int ra1OverlayViewOffsetY(const InsaneRebel1 *rebel1) {
+ if (!rebel1 || !rebel1->isInteractiveVideoActive())
+ return 0;
+
+ return (rebel1->getActiveGameOpcode() == 0x0B) ? rebel1->getPerspectiveY() : 0;
+}
+
static void drawBankString(const RA1SpriteBank &bank, byte *dst, int pitch, int width, int height,
int x, int y, const char *text) {
if (!dst || !text || bank.numSprites <= 0)
@@ -385,7 +392,7 @@ void InsaneRebel1::procPostRendering(byte *renderBitmap, int32 codecparam, int32
// before HUD/screen copy so 0x0B doesn't lag one frame behind the mouse.
if (_player) {
_player->_ra1ViewportOffsetX = _perspectiveX;
- _player->_ra1ViewportOffsetY = asteroidMode ? 0 : _perspectiveY;
+ _player->_ra1ViewportOffsetY = _perspectiveY;
}
// Assembly dispatch (FUN_1BE1B) only runs the targeting/shot overlay pipeline
@@ -448,6 +455,7 @@ void InsaneRebel1::renderTargetBoxes(byte *dst, int pitch, int width, int height
void InsaneRebel1::renderTargeting(byte *dst, int pitch, int width, int height) {
const RA1SpriteBank &markerBank = (_techFontBank.numSprites > 0) ? _techFontBank : _hudFontBank;
const int overlayX = ra1OverlayViewOffsetX(this);
+ const int overlayY = ra1OverlayViewOffsetY(this);
if (markerBank.numSprites > 0) {
// FUN_1CB22 can switch marker sets via DAT_75FF bit 1.
// Baseline RA1 targeting uses '^' and animation e..h.
@@ -456,9 +464,9 @@ void InsaneRebel1::renderTargeting(byte *dst, int pitch, int width, int height)
// Lock indicator at fixed center positions:
// FUN_1CB22 draws marker strings at (0xA0,0x78) and (0xA0,0x7E).
if (_targetProximity > 0) {
- drawCenteredBankGlyph(markerBank, dst, pitch, width, height, overlayX + 0xA0, 0x78, ']');
+ drawCenteredBankGlyph(markerBank, dst, pitch, width, height, overlayX + 0xA0, overlayY + 0x78, ']');
if (_targetProximity > 1)
- drawCenteredBankGlyph(markerBank, dst, pitch, width, height, overlayX + 0xA0, 0x7E, 'a');
+ drawCenteredBankGlyph(markerBank, dst, pitch, width, height, overlayX + 0xA0, overlayY + 0x7E, 'a');
}
// Pointer glyph at current aim position. Original uses two variants:
@@ -470,7 +478,7 @@ void InsaneRebel1::renderTargeting(byte *dst, int pitch, int width, int height)
}
int cursorX = CLIP<int>(overlayX + _shipPosX, 0, width - 1);
- int cursorY = CLIP<int>(_shipPosY, 0, height - 1);
+ int cursorY = CLIP<int>(overlayY + _shipPosY, 0, height - 1);
drawCenteredBankGlyph(markerBank, dst, pitch, width, height, cursorX, cursorY, marker[0]);
}
@@ -521,11 +529,12 @@ void InsaneRebel1::renderLaserShots(byte *dst, int pitch, int width, int height)
static const int kShotLerpByTimer[6] = { 0, 8, 7, 6, 4, 0 };
const int spritesPerSet = 5;
const int overlayX = ra1OverlayViewOffsetX(this);
+ const int overlayY = ra1OverlayViewOffsetY(this);
const int leftStartX = 0;
const int rightStartX = 0x13F; // 319
const bool turretMode = (_activeGameOpcode == 0x08 || _activeGameOpcode == 0x0A);
const int shipBaseX = turretMode ? (kRA1CenterX + (_perspectiveX - 0x20)) : _shipPosX;
- const int shipBaseY = turretMode ? (kRA1CenterY + (_perspectiveY - 0x17)) : _shipPosY;
+ const int shipBaseY = turretMode ? (kRA1CenterY + (_perspectiveY - 0x17)) : (overlayY + _shipPosY);
for (int i = 0; i < kMaxShotSlots; i++) {
if (_shotSlots[i].timer > 0 && _shotSlots[i].timer <= spritesPerSet) {
@@ -533,7 +542,7 @@ void InsaneRebel1::renderLaserShots(byte *dst, int pitch, int width, int height)
const int lerp = kShotLerpByTimer[timer];
const int frame = spritesPerSet - timer;
const int targetX = CLIP<int>(overlayX + _shipPosX, 0, width - 1);
- const int targetY = CLIP<int>(_shipPosY, 0, height - 1);
+ const int targetY = CLIP<int>(overlayY + _shipPosY, 0, height - 1);
if (turretMode) {
// FUN_1D79C chooses emitters in two ways:
@@ -583,20 +592,20 @@ void InsaneRebel1::renderLaserShots(byte *dst, int pitch, int width, int height)
}
// Fallback for non-turret handlers that still run shot overlays.
- int leftStartY = 0x96;
- int rightStartY = 0x96;
+ int leftStartY = overlayY + 0x96;
+ int rightStartY = overlayY + 0x96;
uint32 leftFlags = 0x83;
uint32 rightFlags = 0x2083;
if (_flyControlMode == 1) {
if (_shotSideToggle) {
leftFlags = 0x4083;
- leftStartY = 0;
- rightStartY = 0x96;
+ leftStartY = overlayY;
+ rightStartY = overlayY + 0x96;
} else {
rightFlags = 0x6083;
- leftStartY = 0x96;
- rightStartY = 0;
+ leftStartY = overlayY + 0x96;
+ rightStartY = overlayY;
}
if (timer == 1)
_shotSideToggle = !_shotSideToggle;
@@ -760,10 +769,11 @@ void InsaneRebel1::renderExplosions(byte *dst, int pitch, int width, int height)
return;
const int overlayX = ra1OverlayViewOffsetX(this);
+ const int overlayY = ra1OverlayViewOffsetY(this);
// In 0x08/0x0A turret handlers, explosion anchors use ship center
// (_74B6+_74BA, _74B8+_74BC), not pointer center (_74BE/_74C0).
int shipScreenX = overlayX + _shipPosX;
- int shipScreenY = _shipPosY;
+ int shipScreenY = overlayY + _shipPosY;
if (_activeGameOpcode == 0x08 || _activeGameOpcode == 0x0A) {
shipScreenX = kRA1CenterX + (_perspectiveX - 0x20);
shipScreenY = kRA1CenterY + (_perspectiveY - 0x17);
@@ -840,11 +850,7 @@ void InsaneRebel1::renderHUD(byte *dst, int pitch, int width, int height) {
int hudOriginY = 0;
if (_interactiveVideoActive && _player) {
hudOriginX = _player->_ra1ViewportOffsetX;
- // Asteroid path (opcode 0x0B / FUN_1CDA7) applies Y correction through
- // FUN_223FE coordinate transforms in the original renderer, not as a
- // simple global framebuffer Y window shift.
- if (_activeGameOpcode != 0x0B)
- hudOriginY = _player->_ra1ViewportOffsetY;
+ hudOriginY = _player->_ra1ViewportOffsetY;
}
int hudX = hudOriginX + bar.xoffs;
diff --git a/engines/scumm/smush/smush_player.cpp b/engines/scumm/smush/smush_player.cpp
index 0c8ad49928d..e1301126370 100644
--- a/engines/scumm/smush/smush_player.cpp
+++ b/engines/scumm/smush/smush_player.cpp
@@ -2219,14 +2219,6 @@ void SmushPlayer::play(const char *filename, int32 speed, int32 offset, int32 st
int ra1ViewX = _ra1ViewportOffsetX;
int ra1ViewY = _ra1ViewportOffsetY;
- if (_insane) {
- InsaneRebel1 *rebel1 = static_cast<InsaneRebel1 *>(_insane);
- // Opcode 0x0B (FUN_1CDA7) uses FUN_223FE for Y compensation on
- // transformed objects. A global framebuffer Y window shift causes
- // floating HUD/perspective artifacts in L2 with current renderer.
- if (rebel1->isInteractiveVideoActive() && rebel1->getActiveGameOpcode() == 0x0B)
- ra1ViewY = 0;
- }
const int srcX = CLIP(_scrollX + ra1ViewX, 0, _width - 1);
const int srcY = CLIP(_scrollY + ra1ViewY, 0, _height - 1);
Commit: c5b5b86f8d97274e2fd102873b4628ea8c01f588
https://github.com/scummvm/scummvm/commit/c5b5b86f8d97274e2fd102873b4628ea8c01f588
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:37+02:00
Commit Message:
SCUMM: RA1: Add level 7 through 10 handlers
Changed paths:
engines/scumm/insane/insane_rebel1.cpp
engines/scumm/insane/insane_rebel1.h
engines/scumm/insane/insane_rebel1_iact.cpp
engines/scumm/insane/insane_rebel1_menu.cpp
engines/scumm/insane/insane_rebel1_runlevels.cpp
diff --git a/engines/scumm/insane/insane_rebel1.cpp b/engines/scumm/insane/insane_rebel1.cpp
index 8c1c7d38e93..e7d9441590d 100644
--- a/engines/scumm/insane/insane_rebel1.cpp
+++ b/engines/scumm/insane/insane_rebel1.cpp
@@ -39,7 +39,7 @@ namespace Scumm {
// Per-difficulty tuning tables from assault_data_3.bin
// Indexed: difficulty * 0x28B + level * 0x1F + offset
// Fields: roll, lift, slide, drift, snap, miss, wham, shot, kill
-static const int16 kTuningTable[6][3][9] = {
+static const int16 kTuningTable[10][3][9] = {
// Level 1 (Flight Training)
{
{ 100, 100, 60, 110, 0, 0, 15, 0, 0 }, // Easy
@@ -76,8 +76,32 @@ static const int16 kTuningTable[6][3][9] = {
{ 0, 0, 0, 0, 1, 25, 0, 5, 100 }, // Normal
{ 0, 0, 0, 0, 1, 28, 0, 6, 200 }, // Hard
},
+ // Level 7 (Imperial Probe Droids)
+ {
+ { 70, 150, 50, 25, 10, 0, 20, 0, 25 }, // Easy
+ { 72, 165, 155, 30, 8, 0, 30, 0, 50 }, // Normal
+ { 110, 190, 55, 65, 3, 0, 33, 0, 75 }, // Hard
+ },
+ // Level 8 (Imperial Walkers)
+ {
+ { 0, 0, 0, 0, 5, 0, 0, 2, 25 }, // Easy
+ { 0, 0, 0, 0, 3, 0, 0, 5, 50 }, // Normal
+ { 0, 0, 0, 0, 1, 0, 0, 6, 75 }, // Hard
+ },
+ // Level 9 (Stormtroopers)
+ {
+ { 0, 0, 0, 0, 2, 20, 20, 0, 25 }, // Easy
+ { 0, 0, 0, 0, 1, 25, 30, 0, 50 }, // Normal
+ { 0, 0, 0, 0, 0, 28, 33, 0, 75 }, // Hard
+ },
+ // Level 10 (Protect Rebel Transport)
+ {
+ { 100, 150, 150, 25, 7, 0, 12, 2, 50 }, // Easy
+ { 100, 160, 200, 35, 4, 0, 30, 4, 100 }, // Normal
+ { 100, 180, 250, 50, 3, 0, 33, 5, 100 }, // Hard
+ },
};
-static const int kNumTunedLevels = 6;
+static const int kNumTunedLevels = 10;
void InsaneRebel1::loadTuningForLevel(int level) {
@@ -163,6 +187,8 @@ InsaneRebel1::InsaneRebel1(ScummEngine_v7 *scumm) : Insane(), _vm(scumm) {
_gameCounter = 0;
_pathBranchEnabled = false;
_rightPathSelected = false;
+ _levelRouteIndex = -1;
+ _pendingRouteIndex = -1;
_menuActive = false;
_menuConfirmed = false;
_menuSelection = 0;
diff --git a/engines/scumm/insane/insane_rebel1.h b/engines/scumm/insane/insane_rebel1.h
index 6d3aa92d591..5a2a9d7b7b0 100644
--- a/engines/scumm/insane/insane_rebel1.h
+++ b/engines/scumm/insane/insane_rebel1.h
@@ -117,6 +117,10 @@ private:
bool runLevel4();
bool runLevel5();
bool runLevel6();
+ bool runLevel7();
+ bool runLevel8();
+ bool runLevel9();
+ bool runLevel10();
// Play a passive cinematic (no game callback, skippable)
// startFrame > 0: fast-forward (decode without display) to that frame
@@ -131,6 +135,7 @@ private:
void preprocessMouseAxes(int16 &inputX, int16 &inputY);
void rebuildProjectionTable(int16 curveStep, int16 curveExtent);
void resetProjectionTable();
+ void checkDynamicLevelBranch();
void renderShip(byte *dst, int pitch, int width, int height);
void renderHUD(byte *dst, int pitch, int width, int height);
void renderMainMenuOverlay(byte *dst, int pitch, int width, int height);
@@ -297,6 +302,8 @@ private:
int32 _gameCounter; // GAME 0x07 field1 â the original's _DAT_7740
bool _pathBranchEnabled; // True when branching is active for this video
bool _rightPathSelected; // True if player chose the right/easy path
+ int _levelRouteIndex; // Current mid-level route/segment for branching levels
+ int _pendingRouteIndex; // Next route requested by original frame-branch logic
// Main menu / options state
void runOptionsMenu();
@@ -309,7 +316,7 @@ private:
bool _optionsActive; // True when showing options instead of main menu
int _optionsSel; // 0=difficulty, 1=turbulence, 2=back
bool _levelSelectActive; // True when showing level-select submenu
- int _levelSelectSel; // 0=Level1, 1=Level2, 2=Back
+ int _levelSelectSel; // 0=Level1 ... N-1=Back
int _startLevel; // 1-based start level for "Start New Game"
bool _turbulenceEnabled; // Random per-frame jitter in deltaX (original has it on)
diff --git a/engines/scumm/insane/insane_rebel1_iact.cpp b/engines/scumm/insane/insane_rebel1_iact.cpp
index d48edddae88..7d3badbfc12 100644
--- a/engines/scumm/insane/insane_rebel1_iact.cpp
+++ b/engines/scumm/insane/insane_rebel1_iact.cpp
@@ -34,6 +34,29 @@ static inline bool isL1Stage2DamageLatch(uint16 code) {
return code >= 6 && code <= 18;
}
+static const int16 kLevel7BranchFrames[6][6] = {
+ { -1, 78, 267, 398, 556, 630 },
+ { -1, 187, 376, 507, 665, 739 },
+ { -1, 187, 376, 507, 665, 739 },
+ { -1, -1, -1, 284, 442, 516 },
+ { -1, -1, -1, 143, 301, 375 },
+ { -1, 112, 301, 432, 590, 664 }
+};
+
+static const int16 kLevel7BranchDir[6] = {
+ 0, 1, 1, -1, 1, 1
+};
+
+static const int16 kLevel7BranchThreshold[6] = {
+ 0, 170, 170, 160, 160, 160
+};
+
+static const int16 kLevel8BranchFrames[3][3] = {
+ { 2588, 1709, 262 },
+ { 2323, 1444, -2 },
+ { 877, -2, -2 }
+};
+
void InsaneRebel1::resetFrameObjectState() {
memset(_frameObjectState, 0, sizeof(_frameObjectState));
for (int i = 0x50; i < 0x96; i++)
@@ -140,6 +163,61 @@ void InsaneRebel1::resetProjectionTable() {
rebuildProjectionTable(0, 1);
}
+void InsaneRebel1::checkDynamicLevelBranch() {
+ if (!_interactiveVideoActive || _levelRouteIndex < 0 || _pendingRouteIndex >= 0 || _vm->_smushVideoShouldFinish)
+ return;
+
+ if (_currentLevel == 6) {
+ const int route = CLIP<int>(_levelRouteIndex, 0, 5);
+ for (int nextRoute = 1; nextRoute < 6; ++nextRoute) {
+ const int triggerFrame = kLevel7BranchFrames[route][nextRoute];
+ if (triggerFrame <= 0 || nextRoute == route || _frameCounter != (uint32)(triggerFrame - 1))
+ continue;
+
+ const bool takeBranch = (kLevel7BranchDir[nextRoute] > 0)
+ ? (_shipPosX > kLevel7BranchThreshold[nextRoute])
+ : (_shipPosX < kLevel7BranchThreshold[nextRoute]);
+ if (!takeBranch)
+ continue;
+
+ _pendingRouteIndex = nextRoute;
+ _vm->_smushVideoShouldFinish = true;
+ debug(1, "RA1 L7 branch: route=%d -> %d at frame=%u shipX=%d",
+ route, nextRoute, (unsigned)_frameCounter, _shipPosX);
+ return;
+ }
+ }
+
+ if (_currentLevel == 7) {
+ const int route = CLIP<int>(_levelRouteIndex, 0, 2);
+ const int frame = (int)_frameCounter;
+ const int leftBlockedFrame = kLevel8BranchFrames[route][2];
+ const int rightBlockedFrame = kLevel8BranchFrames[route][1];
+ int nextRoute = -1;
+
+ for (int i = 0; i < 3; ++i) {
+ const int triggerFrame = kLevel8BranchFrames[route][i];
+ if (triggerFrame >= 0 && frame == triggerFrame) {
+ if (_shipPosX < kRA1CenterX) {
+ if (frame != leftBlockedFrame)
+ nextRoute = 1;
+ } else {
+ if (frame != rightBlockedFrame)
+ nextRoute = 2;
+ }
+ break;
+ }
+ }
+
+ if (nextRoute >= 0 && nextRoute != route) {
+ _pendingRouteIndex = nextRoute;
+ _vm->_smushVideoShouldFinish = true;
+ debug(1, "RA1 L8 branch: route=%d -> %d at frame=%u shipX=%d",
+ route, nextRoute, (unsigned)_frameCounter, _shipPosX);
+ }
+ }
+}
+
void InsaneRebel1::projectGameplayPoint(int16 &x, int16 &y) const {
x = (int16)(x - _perspectiveX);
@@ -406,6 +484,8 @@ void InsaneRebel1::updateShipPhysics() {
_pathBranchEnabled = false;
}
+ checkDynamicLevelBranch();
+
debug(7, "RA1 ship: pos=(%d,%d) roll=%d lift=%d accX=%d accY=%d dir=%d health=%d corridor=[%d,%d]-[%d,%d]",
_shipPosX, _shipPosY, _rollAccum, _liftSmooth,
_posAccumX, _posAccumY, _shipDirIndex, _health,
@@ -666,6 +746,7 @@ void InsaneRebel1::updateAsteroidPhysics() {
resetProjectionTable();
_frameCounter++;
+ checkDynamicLevelBranch();
debug(7, "RA1 asteroid: pos=(%d,%d) avg=(%d,%d) view=(%d,%d) health=%d flash=%d",
_shipPosX, _shipPosY, _avgInputX, _avgInputY,
diff --git a/engines/scumm/insane/insane_rebel1_menu.cpp b/engines/scumm/insane/insane_rebel1_menu.cpp
index 99f30618d78..36436309bf0 100644
--- a/engines/scumm/insane/insane_rebel1_menu.cpp
+++ b/engines/scumm/insane/insane_rebel1_menu.cpp
@@ -29,8 +29,8 @@
namespace Scumm {
-static const int kRA1LevelSelectItemCount = 7;
-static const int kRA1LastImplementedLevel = 6;
+static const int kRA1LevelSelectItemCount = 11;
+static const int kRA1LastImplementedLevel = 10;
bool InsaneRebel1::notifyEvent(const Common::Event &event) {
if (_menuActive && _levelSelectActive && event.type == Common::EVENT_KEYDOWN) {
@@ -215,6 +215,10 @@ void InsaneRebel1::renderMainMenuOverlay(byte *dst, int pitch, int width, int he
"LEVEL 4: STAR DESTROYER",
"LEVEL 5: TATOOINE ATTACK",
"LEVEL 6: ASTEROID CHASE",
+ "LEVEL 7: PROBE DROIDS",
+ "LEVEL 8: IMPERIAL WALKERS",
+ "LEVEL 9: STORMTROOPERS",
+ "LEVEL 10: REBEL TRANSPORT",
"BACK"
};
diff --git a/engines/scumm/insane/insane_rebel1_runlevels.cpp b/engines/scumm/insane/insane_rebel1_runlevels.cpp
index b84b0cdd16a..9e526320822 100644
--- a/engines/scumm/insane/insane_rebel1_runlevels.cpp
+++ b/engines/scumm/insane/insane_rebel1_runlevels.cpp
@@ -542,6 +542,436 @@ bool InsaneRebel1::runLevel6() {
return false;
}
+bool InsaneRebel1::runLevel7() {
+ debug(1, "InsaneRebel1: Running level 7");
+
+ static const char *const kLevel7Segments[] = {
+ "LVL7/L7PLAY1.ANM",
+ "LVL7/L7PLAY2.ANM",
+ "LVL7/L7PLAY3.ANM",
+ "LVL7/L7PLAY4.ANM",
+ "LVL7/L7PLAY5.ANM",
+ "LVL7/L7PLAY6.ANM"
+ };
+
+ _currentLevel = 6;
+ loadLevelSprites(7);
+ loadTuningForLevel(6);
+
+ playCinematic("LVL7/L7INTRO.ANM");
+ if (_vm->shouldQuit())
+ return false;
+
+ while (!_vm->shouldQuit()) {
+ _flyControlMode = 3;
+ _health = kMaxHealth;
+ _damageFlags = 0;
+ _prevDamageFlags = 0;
+ _damageCooldown = 0;
+ _deathTimer = 0;
+ _screenFlash = 0;
+ _frameCounter = 0;
+ _gameCounter = 0;
+ _activeGameOpcode = 0;
+ _gameLatch5D = 0;
+ _gameLatch5F = 0;
+ _killCount = 0;
+ _targetCount = 0;
+ _prevTargetCount = 0;
+ _lastHitTarget = 0;
+ _shipPosX = kRA1CenterX;
+ _shipPosY = kRA1CenterY;
+ _shipDirIndex = 17;
+ _rollAccum = 0;
+ _liftSmooth = 0;
+ _posAccumX = 0;
+ _posAccumY = 0;
+ _perspectiveX = 0;
+ _perspectiveY = 0;
+ memset(_inputHistoryX, 0, sizeof(_inputHistoryX));
+ memset(_inputHistoryY, 0, sizeof(_inputHistoryY));
+ memset(_viewHistoryX, 0, sizeof(_viewHistoryX));
+ memset(_viewHistoryY, 0, sizeof(_viewHistoryY));
+ _avgInputX = 0;
+ _avgInputY = 0;
+
+ int route = 0;
+ while (!_vm->shouldQuit()) {
+ _levelRouteIndex = route;
+ _pendingRouteIndex = -1;
+ playInteractiveVideo(kLevel7Segments[route]);
+ if (_vm->shouldQuit())
+ return false;
+
+ if (_health < 0)
+ break;
+
+ if (_pendingRouteIndex < 0 || _pendingRouteIndex == route)
+ break;
+
+ route = _pendingRouteIndex;
+ }
+
+ _levelRouteIndex = -1;
+ _pendingRouteIndex = -1;
+
+ if (_health >= 0) {
+ playCinematic("LVL7/L7END.ANM");
+ return !_vm->shouldQuit();
+ }
+
+ if (_lives > 0) {
+ playCinematic("LVL7/L7NEW.ANM");
+ if (_vm->shouldQuit())
+ return false;
+ _lives--;
+ continue;
+ }
+
+ playCinematic("LVL7/L7DEATH.ANM");
+ return false;
+ }
+
+ return false;
+}
+
+bool InsaneRebel1::runLevel8() {
+ debug(1, "InsaneRebel1: Running level 8");
+
+ static const char *const kLevel8Routes[] = {
+ "LVL8/L8PLAY.ANM",
+ "LVL8/L8PLAY2.ANM",
+ "LVL8/L8PLAY3.ANM"
+ };
+
+ _currentLevel = 7;
+ loadLevelSprites(8);
+ loadTuningForLevel(7);
+
+ playCinematic("LVL8/L8INTRO.ANM");
+ if (_vm->shouldQuit())
+ return false;
+
+ while (!_vm->shouldQuit()) {
+ _flyControlMode = 3;
+ _health = kMaxHealth;
+ _damageFlags = 0;
+ _prevDamageFlags = 0;
+ _damageCooldown = 0;
+ _deathTimer = 0;
+ _screenFlash = 0;
+ _frameCounter = 0;
+ _gameCounter = 0;
+ _activeGameOpcode = 0;
+ _gameLatch5D = 0;
+ _gameLatch5F = 0;
+ _killCount = 0;
+ _targetCount = 0;
+ _prevTargetCount = 0;
+ _lastHitTarget = 0;
+ _shipPosX = kRA1CenterX;
+ _shipPosY = kRA1CenterY;
+ _shipDirIndex = 17;
+ _rollAccum = 0;
+ _liftSmooth = 0;
+ _posAccumX = 0;
+ _posAccumY = 0;
+ _perspectiveX = 0;
+ _perspectiveY = 0;
+ memset(_inputHistoryX, 0, sizeof(_inputHistoryX));
+ memset(_inputHistoryY, 0, sizeof(_inputHistoryY));
+ memset(_viewHistoryX, 0, sizeof(_viewHistoryX));
+ memset(_viewHistoryY, 0, sizeof(_viewHistoryY));
+ _avgInputX = 0;
+ _avgInputY = 0;
+
+ int route = 0;
+ while (!_vm->shouldQuit()) {
+ _levelRouteIndex = route;
+ _pendingRouteIndex = -1;
+ playInteractiveVideo(kLevel8Routes[route]);
+ if (_vm->shouldQuit())
+ return false;
+
+ if (_health < 0)
+ break;
+
+ if (_pendingRouteIndex < 0 || _pendingRouteIndex == route)
+ break;
+
+ route = _pendingRouteIndex;
+ }
+
+ _levelRouteIndex = -1;
+ _pendingRouteIndex = -1;
+
+ if (_health >= 0) {
+ playCinematic("LVL8/L8END.ANM");
+ return !_vm->shouldQuit();
+ }
+
+ if (_lives > 0) {
+ playCinematic("LVL8/L8NEW.ANM");
+ if (_vm->shouldQuit())
+ return false;
+ _lives--;
+ continue;
+ }
+
+ playCinematic("LVL8/L8DEATH.ANM");
+ return false;
+ }
+
+ return false;
+}
+
+bool InsaneRebel1::runLevel9() {
+ debug(1, "InsaneRebel1: Running level 9");
+
+ const int randPath1 = _vm->_rnd.getRandomNumber(1);
+ const int randPath2 = _vm->_rnd.getRandomNumber(1);
+ const int randPath3 = _vm->_rnd.getRandomNumber(1);
+
+ _currentLevel = 8;
+ loadLevelSprites(9);
+ loadTuningForLevel(8);
+
+ playCinematic("LVL9/L9INTRO.ANM");
+ if (_vm->shouldQuit())
+ return false;
+
+ while (!_vm->shouldQuit()) {
+ _flyControlMode = 0;
+ _health = kMaxHealth;
+ _damageFlags = 0;
+ _prevDamageFlags = 0;
+ _damageCooldown = 0;
+ _deathTimer = 0;
+ _screenFlash = 0;
+ _frameCounter = 0;
+ _gameCounter = 0;
+ _activeGameOpcode = 0;
+ _gameLatch5D = 0;
+ _gameLatch5F = 0;
+ _killCount = 0;
+ _targetCount = 0;
+ _prevTargetCount = 0;
+ _lastHitTarget = 0;
+ _shipPosX = kRA1CenterX;
+ _shipPosY = kRA1CenterY;
+ _shipDirIndex = 17;
+ _rollAccum = 0;
+ _liftSmooth = 0;
+ _posAccumX = 0;
+ _posAccumY = 0;
+ _perspectiveX = 0;
+ _perspectiveY = 0;
+ memset(_inputHistoryX, 0, sizeof(_inputHistoryX));
+ memset(_inputHistoryY, 0, sizeof(_inputHistoryY));
+ memset(_viewHistoryX, 0, sizeof(_viewHistoryX));
+ memset(_viewHistoryY, 0, sizeof(_viewHistoryY));
+ _avgInputX = 0;
+ _avgInputY = 0;
+
+ while (!_vm->shouldQuit()) {
+ playInteractiveVideo("LVL9/L9PLAY1.ANM");
+ if (_vm->shouldQuit())
+ return false;
+ if (_health < 0)
+ break;
+
+ playCinematic("LVL9/L9CUT1.ANM");
+ if (_vm->shouldQuit())
+ return false;
+
+ _shipPosX = kRA1CenterX;
+ _posAccumX = 0;
+ playInteractiveVideo("LVL9/L9PLAY2.ANM");
+ if (_vm->shouldQuit())
+ return false;
+ if (_health < 0)
+ break;
+
+ const int side1 = (_shipPosX < kRA1CenterX) ? 0 : 1;
+ playCinematic(side1 == 0 ? "LVL9/L9PLAY2A.ANM" : "LVL9/L9PLAY2B.ANM");
+ if (_vm->shouldQuit())
+ return false;
+
+ if (side1 == randPath1) {
+ playCinematic("LVL9/L9CUT2A.ANM");
+ if (_vm->shouldQuit())
+ return false;
+ continue;
+ }
+
+ playCinematic("LVL9/L9CUT2B.ANM");
+ if (_vm->shouldQuit())
+ return false;
+
+ playInteractiveVideo("LVL9/L9PLAY3A.ANM");
+ if (_vm->shouldQuit())
+ return false;
+ if (_health < 0)
+ break;
+
+ if (_killCount < 15) {
+ playInteractiveVideo("LVL9/L9PLAY3B.ANM");
+ if (_vm->shouldQuit())
+ return false;
+ if (_health < 0)
+ break;
+ }
+
+ playCinematic("LVL9/L9CUT3.ANM");
+ if (_vm->shouldQuit())
+ return false;
+
+ _shipPosX = kRA1CenterX;
+ _posAccumX = 0;
+ playInteractiveVideo("LVL9/L9PLAY4.ANM");
+ if (_vm->shouldQuit())
+ return false;
+ if (_health < 0)
+ break;
+
+ const int side2 = (_shipPosX < kRA1CenterX) ? 0 : 1;
+ playCinematic(side2 == 0 ? "LVL9/L9PLAY4A.ANM" : "LVL9/L9PLAY4B.ANM");
+ if (_vm->shouldQuit())
+ return false;
+
+ if (side2 == randPath2) {
+ playCinematic(side2 == 0 ? "LVL9/L9CUT4AX.ANM" : "LVL9/L9CUT4B.ANM");
+ if (_vm->shouldQuit())
+ return false;
+
+ playInteractiveVideo("LVL9/L9PLAY5.ANM");
+ if (_vm->shouldQuit())
+ return false;
+ if (_health < 0)
+ break;
+
+ playCinematic("LVL9/L9CUT5.ANM");
+ if (_vm->shouldQuit())
+ return false;
+
+ _shipPosX = kRA1CenterX;
+ _posAccumX = 0;
+ playInteractiveVideo("LVL9/L9PLAY6.ANM");
+ if (_vm->shouldQuit())
+ return false;
+ if (_health < 0)
+ break;
+
+ const int side3 = (_shipPosX < kRA1CenterX) ? 0 : 1;
+ if (side3 == randPath3) {
+ playCinematic("LVL9/L9CUT6A.ANM");
+ if (_vm->shouldQuit())
+ return false;
+ continue;
+ }
+
+ playCinematic("LVL9/L9CUT6B.ANM");
+ if (_vm->shouldQuit())
+ return false;
+ } else {
+ playCinematic(side2 == 0 ? "LVL9/L9CUT4A.ANM" : "LVL9/L9CUT4BX.ANM");
+ if (_vm->shouldQuit())
+ return false;
+ }
+
+ playInteractiveVideo("LVL9/L9PLAY7.ANM");
+ if (_vm->shouldQuit())
+ return false;
+ if (_health < 0)
+ break;
+
+ playCinematic("LVL9/L9END.ANM");
+ return !_vm->shouldQuit();
+ }
+
+ if (_lives > 0) {
+ playCinematic("LVL9/L9NEW.ANM");
+ if (_vm->shouldQuit())
+ return false;
+ _lives--;
+ continue;
+ }
+
+ playCinematic("LVL9/L9DEATH.ANM");
+ return false;
+ }
+
+ return false;
+}
+
+bool InsaneRebel1::runLevel10() {
+ debug(1, "InsaneRebel1: Running level 10");
+
+ _currentLevel = 9;
+ loadLevelSprites(10);
+ loadTuningForLevel(9);
+
+ playCinematic("LVL10/L10INTRO.ANM");
+ if (_vm->shouldQuit())
+ return false;
+
+ while (!_vm->shouldQuit()) {
+ _flyControlMode = 1;
+ _health = kMaxHealth;
+ _damageFlags = 0;
+ _prevDamageFlags = 0;
+ _damageCooldown = 0;
+ _deathTimer = 0;
+ _screenFlash = 0;
+ _frameCounter = 0;
+ _gameCounter = 0;
+ _activeGameOpcode = 0;
+ _gameLatch5D = 0;
+ _gameLatch5F = 0;
+ _killCount = 0;
+ _targetCount = 0;
+ _prevTargetCount = 0;
+ _lastHitTarget = 0;
+ _shipPosX = kRA1CenterX;
+ _shipPosY = kRA1CenterY;
+ _shipDirIndex = 17;
+ _rollAccum = 0;
+ _liftSmooth = 0;
+ _posAccumX = 0;
+ _posAccumY = 0;
+ _perspectiveX = 0;
+ _perspectiveY = 0;
+ memset(_inputHistoryX, 0, sizeof(_inputHistoryX));
+ memset(_inputHistoryY, 0, sizeof(_inputHistoryY));
+ memset(_viewHistoryX, 0, sizeof(_viewHistoryX));
+ memset(_viewHistoryY, 0, sizeof(_viewHistoryY));
+ _avgInputX = 0;
+ _avgInputY = 0;
+
+ playInteractiveVideo("LVL10/L10PLAY.ANM");
+ if (_vm->shouldQuit())
+ return false;
+
+ if (_health >= 0) {
+ playCinematic("LVL10/L10END.ANM");
+ return !_vm->shouldQuit();
+ }
+
+ if (_lives > 0) {
+ playCinematic("LVL10/L10NEW.ANM");
+ if (_vm->shouldQuit())
+ return false;
+ _lives--;
+ continue;
+ }
+
+ playCinematic("LVL10/L10DEATH.ANM");
+ return false;
+ }
+
+ return false;
+}
+
// Main game entry point â called from ScummEngine::go().
// Matches original flow at 0x15597: intro â menu â level.
void InsaneRebel1::runGame() {
@@ -552,7 +982,11 @@ void InsaneRebel1::runGame() {
&InsaneRebel1::runLevel3,
&InsaneRebel1::runLevel4,
&InsaneRebel1::runLevel5,
- &InsaneRebel1::runLevel6
+ &InsaneRebel1::runLevel6,
+ &InsaneRebel1::runLevel7,
+ &InsaneRebel1::runLevel8,
+ &InsaneRebel1::runLevel9,
+ &InsaneRebel1::runLevel10
};
// Play intro sequence (logo + opening)
Commit: 560d6f9ca5bb45cf5b4295e72cc3be864aacdfcd
https://github.com/scummvm/scummvm/commit/560d6f9ca5bb45cf5b4295e72cc3be864aacdfcd
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:37+02:00
Commit Message:
SCUMM: RA1: Center interactive FTCH placement
Changed paths:
engines/scumm/smush/smush_player_ra1.cpp
diff --git a/engines/scumm/smush/smush_player_ra1.cpp b/engines/scumm/smush/smush_player_ra1.cpp
index 83a78367bfd..dcf820eb29c 100644
--- a/engines/scumm/smush/smush_player_ra1.cpp
+++ b/engines/scumm/smush/smush_player_ra1.cpp
@@ -35,6 +35,22 @@
namespace Scumm {
+static void ra1ApplyCenteredFetchPlacement(InsaneRebel1 *rebel1, int width, int height, int &left, int &top) {
+ int16 centerX = (int16)(left + (width >> 1));
+ int16 centerY = (int16)(top + (height >> 1));
+
+ rebel1->projectGameplayPoint(centerX, centerY);
+
+ const int projectedLeft = (int)centerX - (width >> 1);
+ const int projectedTop = (int)centerY - (height >> 1);
+
+ // RestoreStoredFramePatch routes FTCH through DispatchFobjCodec with flag 0x800.
+ // That path applies ProjectPointToScreen() to the center point, then only moves
+ // a quarter of the projected delta before decoding the stored FOBJ.
+ left -= ((projectedLeft - left) >> 2);
+ top -= ((projectedTop - top) >> 2);
+}
+
SmushPlayerRebel1::SmushPlayerRebel1(ScummEngine_v7 *scumm, IMuseDigital *imuseDigital, Insane *insane)
: SmushPlayer(scumm, imuseDigital, insane) {
initGamePlayerFields();
@@ -111,9 +127,18 @@ bool SmushPlayerRebel1::handleGameFetch(int32 subSize, Common::SeekableReadStrea
if (_insane) {
InsaneRebel1 *rebel1 = static_cast<InsaneRebel1 *>(_insane);
- if (rebel1->isInteractiveVideoActive() && rebel1->getActiveGameOpcode() == 0x0B &&
- _storedFobjWidth == _vm->_screenWidth) {
- left += _ra1ViewportOffsetX;
+ if (rebel1->isInteractiveVideoActive()) {
+ if (rebel1->getActiveGameOpcode() == 0x0B && _storedFobjWidth == _vm->_screenWidth) {
+ left += _ra1ViewportOffsetX;
+ } else {
+ ra1ApplyCenteredFetchPlacement(rebel1, _storedFobjWidth, _storedFobjHeight, left, top);
+ // ScummVM currently emulates the RA1 camera with a source-window crop
+ // for interactive scenes. FTCH placement from the original executable
+ // is computed in fixed presentation space, so convert it back into the
+ // cropped buffer space used by the current renderer.
+ left += _ra1ViewportOffsetX;
+ top += _ra1ViewportOffsetY;
+ }
}
}
Commit: 2a4d224a45dc9e55d8da89659048c7aba7aaf2db
https://github.com/scummvm/scummvm/commit/2a4d224a45dc9e55d8da89659048c7aba7aaf2db
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:38+02:00
Commit Message:
SCUMM: RA1: Render HUD plate opaquely
Changed paths:
engines/scumm/insane/insane_rebel1_render.cpp
engines/scumm/smush/smush_player_ra1.cpp
diff --git a/engines/scumm/insane/insane_rebel1_render.cpp b/engines/scumm/insane/insane_rebel1_render.cpp
index ca56df82c2f..ee10d2ce5f5 100644
--- a/engines/scumm/insane/insane_rebel1_render.cpp
+++ b/engines/scumm/insane/insane_rebel1_render.cpp
@@ -856,7 +856,9 @@ void InsaneRebel1::renderHUD(byte *dst, int pitch, int width, int height) {
int hudX = hudOriginX + bar.xoffs;
int hudY = hudOriginY + bar.yoffs;
- // Draw the status bar background with transparency (pixel 0 = transparent)
+ // DOS RA1 draws the HUD plate through DrawFobjGlyph(..., flags=0x181),
+ // which selects the opaque blit path. Keep zero-valued pixels black instead
+ // of treating them as transparent.
if (bar.data && bar.width > 0 && bar.height > 0) {
int drawX = hudX, drawY = hudY, drawW = bar.width, drawH = bar.height;
int srcOffX = 0, srcOffY = 0;
@@ -868,11 +870,7 @@ void InsaneRebel1::renderHUD(byte *dst, int pitch, int width, int height) {
for (int iy = 0; iy < drawH; iy++) {
const byte *s = bar.data + (srcOffY + iy) * bar.width + srcOffX;
byte *d = dst + (drawY + iy) * pitch + drawX;
- for (int ix = 0; ix < drawW; ix++) {
- byte px = s[ix];
- if (px != 0)
- d[ix] = px;
- }
+ memcpy(d, s, drawW);
}
debug(5, "RA1 HUD: drawn at (%d,%d) size=%dx%d",
diff --git a/engines/scumm/smush/smush_player_ra1.cpp b/engines/scumm/smush/smush_player_ra1.cpp
index dcf820eb29c..1188ee84f3b 100644
--- a/engines/scumm/smush/smush_player_ra1.cpp
+++ b/engines/scumm/smush/smush_player_ra1.cpp
@@ -130,6 +130,7 @@ bool SmushPlayerRebel1::handleGameFetch(int32 subSize, Common::SeekableReadStrea
if (rebel1->isInteractiveVideoActive()) {
if (rebel1->getActiveGameOpcode() == 0x0B && _storedFobjWidth == _vm->_screenWidth) {
left += _ra1ViewportOffsetX;
+ top += _ra1ViewportOffsetY;
} else {
ra1ApplyCenteredFetchPlacement(rebel1, _storedFobjWidth, _storedFobjHeight, left, top);
// ScummVM currently emulates the RA1 camera with a source-window crop
Commit: ad1713032f7e4590ac3e35213bf8926b5c603658
https://github.com/scummvm/scummvm/commit/ad1713032f7e4590ac3e35213bf8926b5c603658
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:38+02:00
Commit Message:
SCUMM: RA1: Enable controls in level 3
Changed paths:
engines/scumm/insane/insane_rebel1_iact.cpp
engines/scumm/insane/insane_rebel1_runlevels.cpp
diff --git a/engines/scumm/insane/insane_rebel1_iact.cpp b/engines/scumm/insane/insane_rebel1_iact.cpp
index 7d3badbfc12..bfbc1f70b95 100644
--- a/engines/scumm/insane/insane_rebel1_iact.cpp
+++ b/engines/scumm/insane/insane_rebel1_iact.cpp
@@ -279,21 +279,35 @@ void InsaneRebel1::preprocessMouseAxes(int16 &inputX, int16 &inputY) {
void InsaneRebel1::updateShipPhysics() {
_frameCounter++;
+ // HandleGameOp07_ShipFlight resets the ship accumulators and camera when
+ // the GAME 0x07 frame counter enters at 0. Level 1 happened to work because
+ // its runlevel code pre-initialized the same state, but later 0x07-driven
+ // stages like L3 rely on the handler to do this reset itself.
+ if (_gameCounter == 0) {
+ _posAccumX = 0;
+ _posAccumY = 0;
+ _rollAccum = 0;
+ _liftSmooth = 0;
+ _shipPosX = kRA1CenterX;
+ _shipPosY = kRA1CenterY;
+ _damageFlags = 0;
+ _prevDamageFlags = 0;
+ _damageCooldown = 0;
+ _perspectiveX = 0x20;
+ _perspectiveY = 0x17;
+ resetProjectionTable();
+ }
+
// Decrement cooldown
if (_damageCooldown > 0)
_damageCooldown--;
- // --- Step 1: Mouse input as offset from screen center ---
- // Original: _DAT_756C (horizontal), _DAT_756E (vertical) in 320x200 space.
- int16 mouseX = (int16)_vm->_mouse.x;
- int16 mouseY = (int16)_vm->_mouse.y;
- if (_player && _player->_width > 0 && _player->_height > 0 &&
- (mouseX >= 320 || mouseY >= 200)) {
- mouseX = (int16)((mouseX * 320) / _player->_width);
- mouseY = (int16)((mouseY * 200) / _player->_height);
- }
- int16 inputX = (int16)(mouseX - 160);
- int16 inputY = (int16)(mouseY - 100);
+ // --- Step 1: Gameplay axes from FUN_231BE ---
+ // HandleGameOp07_ShipFlight consumes the preprocessed axes in DAT_756C/756E,
+ // not raw mouse coordinates. Reuse the same centered-axis law here.
+ int16 inputX = 0;
+ int16 inputY = 0;
+ preprocessMouseAxes(inputX, inputY);
inputX = CLIP<int16>(inputX, -127, 127);
inputY = CLIP<int16>(inputY, -127, 127);
@@ -545,20 +559,13 @@ void InsaneRebel1::updateTurretPhysics() {
// FUN_1E6A7 movement gate: counter > 8 or flags bit 0x40.
if (counter > 8 || (modeFlags & 0x40)) {
- int16 mouseX = (int16)_vm->_mouse.x;
- int16 mouseY = (int16)_vm->_mouse.y;
- if (_player && _player->_width > 0 && _player->_height > 0 &&
- (mouseX >= 320 || mouseY >= 200)) {
- mouseX = (int16)((mouseX * 320) / _player->_width);
- mouseY = (int16)((mouseY * 200) / _player->_height);
- }
-
- // FUN_1F3F8/FUN_231BE preprocesses mouse deltas before FUN_1E6A7 consumes
- // DAT_756C/DAT_756E. Keep X in 320-space then scale to ±127 (RA2 parity),
- // Y clamped to ±127.
- int16 rawInputX = CLIP<int16>((int16)(mouseX - 160), -160, 160);
- int16 inputX = (int16)((rawInputX * 127) / 160);
- int16 inputY = CLIP<int16>((int16)(mouseY - 100), -127, 127);
+ // FUN_1E6A7 consumes DAT_756C/DAT_756E from the shared input bridge,
+ // not raw mouse coordinates.
+ int16 inputX = 0;
+ int16 inputY = 0;
+ preprocessMouseAxes(inputX, inputY);
+ inputX = CLIP<int16>(inputX, -127, 127);
+ inputY = CLIP<int16>(inputY, -127, 127);
_rollAccum += (_tuning.roll * (int32)inputX) >> 4;
_rollAccum = (_rollAccum * 3) >> 2;
diff --git a/engines/scumm/insane/insane_rebel1_runlevels.cpp b/engines/scumm/insane/insane_rebel1_runlevels.cpp
index 9e526320822..562d30d09b3 100644
--- a/engines/scumm/insane/insane_rebel1_runlevels.cpp
+++ b/engines/scumm/insane/insane_rebel1_runlevels.cpp
@@ -223,7 +223,8 @@ bool InsaneRebel1::runLevel2() {
_currentLevel = 1;
loadLevelSprites(2);
- loadTuningForLevel(1);
+ // DOS RunLevel2Flow launches L2PLAY.ANM with gameplay selector 2.
+ loadTuningForLevel(2);
playCinematic("LVL2/L2INTRO.ANM");
if (_vm->shouldQuit())
@@ -279,7 +280,8 @@ bool InsaneRebel1::runLevel3() {
_currentLevel = 2;
loadLevelSprites(3);
- loadTuningForLevel(2);
+ // DOS RunLevel3Flow launches L3PLAY.ANM with gameplay selector 3.
+ loadTuningForLevel(3);
playCinematic("LVL3/L3INTRO.ANM");
if (_vm->shouldQuit())
@@ -335,7 +337,8 @@ bool InsaneRebel1::runLevel4() {
_currentLevel = 3;
loadLevelSprites(4);
- loadTuningForLevel(3);
+ // DOS RunLevel4Flow launches L4PLAY1/2.ANM with gameplay selector 4.
+ loadTuningForLevel(4);
playCinematic("LVL4/L4INTRO.ANM");
if (_vm->shouldQuit())
Commit: ce45d205dd279258a2e8340fe03ba08bbfe82b80
https://github.com/scummvm/scummvm/commit/ce45d205dd279258a2e8340fe03ba08bbfe82b80
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:38+02:00
Commit Message:
SCUMM: RA1: Refine mouse and frame opcode state
Changed paths:
engines/scumm/insane/insane_rebel1.cpp
engines/scumm/insane/insane_rebel1.h
engines/scumm/insane/insane_rebel1_iact.cpp
engines/scumm/insane/insane_rebel1_menu.cpp
engines/scumm/insane/insane_rebel1_render.cpp
diff --git a/engines/scumm/insane/insane_rebel1.cpp b/engines/scumm/insane/insane_rebel1.cpp
index e7d9441590d..47aecc90d06 100644
--- a/engines/scumm/insane/insane_rebel1.cpp
+++ b/engines/scumm/insane/insane_rebel1.cpp
@@ -170,6 +170,8 @@ InsaneRebel1::InsaneRebel1(ScummEngine_v7 *scumm) : Insane(), _vm(scumm) {
_turretEmitterRightX = 0;
_turretEmitterRightY = 0;
_activeGameOpcode = 0;
+ _frameGameOpcodeMask = 0;
+ _frameDispatchFlags = 0;
_health = kMaxHealth;
_lives = 3;
@@ -203,6 +205,8 @@ InsaneRebel1::InsaneRebel1(ScummEngine_v7 *scumm) : Insane(), _vm(scumm) {
// Shooting/targeting state
_playerFired = false;
_fireCooldown = 0;
+ _gameplayFlags75fe = 0;
+ _gameplayFlags75ff = 0;
memset(_shotSlots, 0, sizeof(_shotSlots));
_shotAlternator = 0;
_shotSideToggle = false;
diff --git a/engines/scumm/insane/insane_rebel1.h b/engines/scumm/insane/insane_rebel1.h
index 5a2a9d7b7b0..240467be46f 100644
--- a/engines/scumm/insane/insane_rebel1.h
+++ b/engines/scumm/insane/insane_rebel1.h
@@ -86,6 +86,9 @@ public:
bool isInteractiveVideoActive() const { return _interactiveVideoActive; }
int getCurrentLevel() const { return _currentLevel; }
uint16 getActiveGameOpcode() const { return _activeGameOpcode; }
+ bool hasFrameGameOpcode(uint16 opcode) const {
+ return opcode < 32 && (_frameGameOpcodeMask & (1u << opcode)) != 0;
+ }
int16 getPerspectiveX() const { return _perspectiveX; }
int16 getPerspectiveY() const { return _perspectiveY; }
void projectGameplayPoint(int16 &x, int16 &y) const;
@@ -244,9 +247,11 @@ private:
int16 _turretEmitterLeftY;
int16 _turretEmitterRightX;
int16 _turretEmitterRightY;
- // Last per-frame GAME movement handler opcode (0x07/0x08/0x09/0x0A/0x0B/0x1A).
- // Used to mirror assembly handler-specific overlay pipeline behavior.
+ // Last per-frame GAME opcode observed in the current playback stream.
+ // Kept for legacy call sites; frame-accurate dispatch uses _frameGameOpcodeMask.
uint16 _activeGameOpcode;
+ uint32 _frameGameOpcodeMask;
+ uint16 _frameDispatchFlags;
// Difficulty (0=easy, 1=normal, 2=hard) â matches original DAT_22BC
int _difficulty;
@@ -322,8 +327,10 @@ private:
bool _turbulenceEnabled; // Random per-frame jitter in deltaX (original has it on)
// Shooting state â FUN_1CCA0 (0x1CCA0)
- bool _playerFired; // 0x7570: fire button pressed this frame
- int16 _fireCooldown; // 0x757C: button-edge gate in original input pipeline
+ bool _playerFired; // 0x7570: current fire-button state
+ int16 _fireCooldown; // 0x757C: previous-frame fire-button state (edge gate)
+ uint16 _gameplayFlags75fe; // 0x75FE: gameplay mode flags
+ uint16 _gameplayFlags75ff; // 0x75FF: targeting / shot-style flags
// Explosion shot slots (2 slots) â FUN_1CCA0 (0x1CCA0)
static const int kMaxShotSlots = 2;
diff --git a/engines/scumm/insane/insane_rebel1_iact.cpp b/engines/scumm/insane/insane_rebel1_iact.cpp
index bfbc1f70b95..34571abf24d 100644
--- a/engines/scumm/insane/insane_rebel1_iact.cpp
+++ b/engines/scumm/insane/insane_rebel1_iact.cpp
@@ -27,11 +27,53 @@
namespace Scumm {
-// LVL1 stage-2 0x5D damage/event codes observed in L1PLAY2.ANM.
-// Original DOS loop uses table/mask-driven latch routing before FUN_1E6A7;
-// in ScummVM's direct GAME dispatch path, map this known range explicitly.
+// LVL1 stage-2 0x5D damage/event codes. The gameplay stream exposes low record ids
+// (6..18), while the recovered outer loop compares the post-latch state against the
+// later translated values seen in the executable. Accept both representations.
static inline bool isL1Stage2DamageLatch(uint16 code) {
- return code >= 6 && code <= 18;
+ switch (code) {
+ case 0x0006:
+ case 0x0007:
+ case 0x0008:
+ case 0x0009:
+ case 0x000A:
+ case 0x000B:
+ case 0x000C:
+ case 0x000D:
+ case 0x000E:
+ case 0x000F:
+ case 0x0010:
+ case 0x0011:
+ case 0x0012:
+ case 0x0049:
+ case 0x004B:
+ case 0x004E:
+ case 0x0051:
+ case 0x0054:
+ case 0x005C:
+ case 0x005E:
+ case 0x0060:
+ case 0x0062:
+ case 0x0064:
+ return true;
+ default:
+ return false;
+ }
+}
+
+static inline bool isL1Stage2SweepDamage(uint16 frameCounter, int16 perspectiveX) {
+ switch (frameCounter) {
+ case 0x0034:
+ case 0x00ED:
+ case 0x0173:
+ return perspectiveX <= 0x28;
+ case 0x0088:
+ case 0x00FA:
+ case 0x0151:
+ return perspectiveX >= 0x18;
+ default:
+ return false;
+ }
}
static const int16 kLevel7BranchFrames[6][6] = {
@@ -239,14 +281,13 @@ void InsaneRebel1::unprojectGameplayPoint(int16 &x, int16 &y) const {
// preprocessMouseAxes â FUN_231BE (0x231BE) centered-axis output law, adapted to
// ScummVM's absolute 320x200 mouse space.
-// The original DOS path combines a full-scale centered axis term with an extra
-// mouse-bias term `(rawX-0x140)>>2`, `(rawY-100)>>1` maintained by
-// FUN_23115/FUN_231BE recenter globals. In ScummVM, mirroring the DOS recenter
-// box makes LVL2 control unusably constrained, so keep the FUN_231BE output law
-// (normalized axis + bias) but feed it directly from logical mouse coordinates.
-// That preserves the handler's expected `_DAT_756C/_DAT_756E` amplitude without
-// introducing the DOS safe-window box or extra latency.
+// Preserve the DOS bias/offset persistence and one-frame jump latch from
+// FUN_231BE, but avoid hard recentring the host mouse into the DOS safe window.
+// The actual frame-averaging behavior stays untouched.
void InsaneRebel1::preprocessMouseAxes(int16 &inputX, int16 &inputY) {
+ if (_mouseRecentering)
+ return;
+
int16 logicalX = (int16)CLIP<int>(_vm->_mouse.x, 0, 319);
int16 logicalY = (int16)CLIP<int>(_vm->_mouse.y, 0, 199);
const int16 rawX = (int16)(logicalX << 1);
@@ -255,19 +296,77 @@ void InsaneRebel1::preprocessMouseAxes(int16 &inputX, int16 &inputY) {
const int16 deltaY = (int16)(logicalY - kRA1CenterY);
const int16 normX = (int16)(((int32)deltaX * 127) / 160);
const int16 normY = (int16)(((int32)deltaY * 127) / 100);
- const int16 biasX = (int16)((rawX - 0x140) >> 2);
- const int16 biasY = (int16)((rawY - 100) >> 1);
+ int16 biasX = (int16)((rawX + _mouseOffsetX - 0x140) >> 2);
+ int16 biasY = (int16)((rawY + _mouseOffsetY - 100) >> 1);
+
+ if (biasY < 0x65) {
+ const bool largeJump =
+ (_mousePrevBiasX + 0x14 < biasX) ||
+ (_mousePrevBiasY + 0x14 < biasY) ||
+ (biasX < _mousePrevBiasX - 0x14) ||
+ (biasY < _mousePrevBiasY - 0x14);
+ if (largeJump) {
+ if (!_mouseBiasLatch) {
+ biasX = _mousePrevBiasX;
+ biasY = _mousePrevBiasY;
+ _mouseBiasLatch = true;
+ }
+ } else {
+ _mouseBiasLatch = false;
+ }
+ } else {
+ biasX = _mousePrevBiasX;
+ biasY = _mousePrevBiasY;
+ _mouseBiasLatch = true;
+ }
+
const int16 scaledX = (int16)(normX + biasX);
const int16 scaledY = (int16)(normY + biasY);
- _mouseBiasX = scaledX;
- _mouseBiasY = scaledY;
- _mousePrevBiasX = scaledX;
- _mousePrevBiasY = scaledY;
- _mouseBiasLatch = false;
- _mouseRecentering = false;
- _mouseOffsetX = 0;
- _mouseOffsetY = 0;
+ _mouseBiasX = biasX;
+ _mouseBiasY = biasY;
+ _mousePrevBiasX = biasX;
+ _mousePrevBiasY = biasY;
+
+ int accumX = rawX + _mouseOffsetX;
+ if (accumX < 0xC0)
+ _mouseOffsetX = (int16)(0xC0 - rawX);
+ else if (accumX > 0x1C0)
+ _mouseOffsetX = (int16)(0x1C0 - rawX);
+
+ int accumY = rawY + _mouseOffsetY;
+ if (accumY < -0x1C)
+ _mouseOffsetY = (int16)(-0x1C - rawY);
+ else if (accumY > 0xE4)
+ _mouseOffsetY = (int16)(0xE4 - rawY);
+
+ accumX = rawX + _mouseOffsetX;
+ if (accumX < 0x145) {
+ if (accumX < 0x142) {
+ if (accumX < 0x13C)
+ _mouseOffsetX += 4;
+ else if (accumX < 0x13F)
+ _mouseOffsetX += 1;
+ } else {
+ _mouseOffsetX -= 1;
+ }
+ } else {
+ _mouseOffsetX -= 4;
+ }
+
+ accumY = rawY + _mouseOffsetY;
+ if (accumY < 0x69) {
+ if (accumY < 0x66) {
+ if (accumY < 0x60)
+ _mouseOffsetY += 4;
+ else if (accumY < 99)
+ _mouseOffsetY += 1;
+ } else {
+ _mouseOffsetY -= 1;
+ }
+ } else {
+ _mouseOffsetY -= 4;
+ }
inputX = CLIP<int16>(scaledX, -0xA0, 0xA0);
inputY = CLIP<int16>(scaledY, -127, 127);
@@ -515,13 +614,18 @@ void InsaneRebel1::updateTurretPhysics() {
// The 0x10/0x40 gates come from dispatcher arg4 (callback control bits),
// not from GAME payload fields.
const int32 counter = _gameCounter;
- const byte modeFlags = 0;
+ const uint16 modeFlags = _frameDispatchFlags;
// RA1 latches consumed by handler family in FUN_1B297.
+ if (_currentLevel == 0 && _flyControlMode == 2 && isL1Stage2SweepDamage((uint16)counter, _perspectiveX))
+ _damageFlags |= 0x20;
if (_gameLatch5D == 0xFFFF || (_currentLevel == 0 && _flyControlMode == 2 &&
isL1Stage2DamageLatch(_gameLatch5D)))
_damageFlags |= 0x40;
- if (_gameLatch5F != 0 && _vm->_rnd.getRandomNumber((uint16)(_gameLatch5F - 1)) == 0)
+ if (_gameLatch5F != 0 &&
+ ((_currentLevel == 0 && _flyControlMode == 2)
+ ? (_vm->_rnd.getRandomNumber(2) == 0)
+ : (_vm->_rnd.getRandomNumber((uint16)(_gameLatch5F - 1)) == 0)))
_damageFlags |= 0x80;
if (counter == 0) {
@@ -529,8 +633,6 @@ void InsaneRebel1::updateTurretPhysics() {
_posAccumY = 0;
_rollAccum = 0;
_liftSmooth = 0;
- _shipPosX = kRA1CenterX;
- _shipPosY = kRA1CenterY;
_damageFlags = 0;
_prevDamageFlags = 0;
_damageCooldown = 0;
@@ -586,12 +688,6 @@ void InsaneRebel1::updateTurretPhysics() {
const int16 offsetX = (int16)(_posAccumX >> 8);
const int16 offsetY = (int16)(_posAccumY >> 8);
- // FUN_1D79C tail sets pointer center from offsets:
- // _74BE = _74B6 + _74BA
- // _74C0 = (_74B8 + _74BC - 0x23) - (_74BC >> 3)
- _shipPosX = (int16)(kRA1CenterX + offsetX);
- _shipPosY = (int16)((kRA1CenterY + offsetY - 0x23) - (offsetY >> 3));
-
_perspectiveX = CLIP<int16>((int16)(offsetX + 0x20), 0, 0x40);
_perspectiveY = CLIP<int16>((int16)(offsetY + 0x17), 0, 0x2E);
@@ -835,22 +931,41 @@ void InsaneRebel1::handleGameChunk(int32 subSize, Common::SeekableReadStream &b)
}
_activeGameOpcode = 0;
+ _frameGameOpcodeMask = 0;
+ _frameDispatchFlags = 0;
resetProjectionTable();
debug(5, "RA1 GAME 0x5E: reset state field1=%d mode=%d", (int32)param1, (int)_flyControlMode);
break;
case 0x5D:
- _gameLatch5D = (uint16)param1;
+ if ((uint16)param1 == 0xFFFF) {
+ _gameLatch5D = 0xFFFF;
+ } else if (param1 > 0) {
+ const int bitIndex = (int)param1 - 1;
+ const int byteIndex = bitIndex >> 3;
+ if (byteIndex >= 0 && byteIndex < 0x96 &&
+ (_frameObjectState[byteIndex] & (byte)(0x80 >> (bitIndex & 7))) == 0) {
+ _gameLatch5D = (uint16)param1;
+ }
+ }
debug(5, "RA1 GAME 0x5D (link/event latch) param=%u", _gameLatch5D);
break;
case 0x5F:
- _gameLatch5F = (uint16)param1;
+ if (param1 > 0) {
+ const int bitIndex = (int)param1 - 1;
+ const int byteIndex = bitIndex >> 3;
+ if (byteIndex >= 0 && byteIndex < 0x96 &&
+ (_frameObjectState[byteIndex] & (byte)(0x80 >> (bitIndex & 7))) == 0) {
+ _gameLatch5F = (uint16)param1;
+ }
+ }
debug(5, "RA1 GAME 0x5F (random-hit latch) param=%u", _gameLatch5F);
break;
case 0x07:
_activeGameOpcode = 0x07;
+ _frameGameOpcodeMask |= (1u << 0x07);
// Per-frame corridor data: f1=frame counter, f2=max frames, f3=drift bias, f4=unused
// f1 is the original's _DAT_7740 (game frame counter)
// f3 is the drift/wind parameter combined with tuning table
@@ -916,6 +1031,7 @@ void InsaneRebel1::handleGameChunk(int32 subSize, Common::SeekableReadStream &b)
case 0x0B:
_activeGameOpcode = 0x0B;
+ _frameGameOpcodeMask |= (1u << 0x0B);
// Asteroid/surface per-frame handler (FUN_1CDA7).
// field1 = frame counter, field2 = max frames
_gameCounter = param1;
@@ -958,6 +1074,7 @@ void InsaneRebel1::handleGameChunk(int32 subSize, Common::SeekableReadStream &b)
case 0x19:
case 0x1A:
_activeGameOpcode = (uint16)opcode;
+ _frameGameOpcodeMask |= (1u << opcode);
_gameCounter = param1;
if (subSize >= 20) {
uint32 param2 = b.readUint32BE();
@@ -977,7 +1094,7 @@ void InsaneRebel1::handleGameChunk(int32 subSize, Common::SeekableReadStream &b)
// processShot â FUN_1CCA0 (0x1CCA0). Spawns shot into explosion slot when fired.
// Called once per frame during interactive rendering.
void InsaneRebel1::processShot() {
- if (!_playerFired)
+ if (!_playerFired || _fireCooldown != 0)
return;
// Find first available slot (timer < 1 or > 5), matching FUN_1CCA0.
@@ -989,7 +1106,6 @@ void InsaneRebel1::processShot() {
}
}
if (slot < 0) {
- _playerFired = false;
return;
}
@@ -998,7 +1114,7 @@ void InsaneRebel1::processShot() {
const int16 shipCenterX = turretMode ? (int16)(kRA1CenterX + (_perspectiveX - 0x20)) : _shipPosX;
const int16 shipCenterY = turretMode ? (int16)(kRA1CenterY + (_perspectiveY - 0x17)) : _shipPosY;
- _shotSlots[slot].timer = 5;
+ _shotSlots[slot].timer = (_gameplayFlags75ff & 0x2) ? 2 : 5;
_shotSlots[slot].posX = _shipPosX;
_shotSlots[slot].posY = _shipPosY;
_shotSlots[slot].centerX = shipCenterX;
@@ -1006,8 +1122,6 @@ void InsaneRebel1::processShot() {
_shotSlots[slot].variant = _shotAlternator;
_shotAlternator = 1 - _shotAlternator;
- _playerFired = false;
-
debug(5, "RA1 shot: slot=%d pos=(%d,%d)", slot, _shotSlots[slot].posX, _shotSlots[slot].posY);
}
diff --git a/engines/scumm/insane/insane_rebel1_menu.cpp b/engines/scumm/insane/insane_rebel1_menu.cpp
index 36436309bf0..59ca5357f74 100644
--- a/engines/scumm/insane/insane_rebel1_menu.cpp
+++ b/engines/scumm/insane/insane_rebel1_menu.cpp
@@ -126,6 +126,10 @@ bool InsaneRebel1::notifyEvent(const Common::Event &event) {
_playerFired = true;
return true;
}
+ if (event.type == Common::EVENT_LBUTTONUP) {
+ _playerFired = false;
+ return true;
+ }
}
if (event.type == Common::EVENT_KEYDOWN && event.kbd.keycode == Common::KEYCODE_ESCAPE) {
diff --git a/engines/scumm/insane/insane_rebel1_render.cpp b/engines/scumm/insane/insane_rebel1_render.cpp
index ee10d2ce5f5..ce61d86921a 100644
--- a/engines/scumm/insane/insane_rebel1_render.cpp
+++ b/engines/scumm/insane/insane_rebel1_render.cpp
@@ -325,6 +325,9 @@ static void renderSpriteWithFlags(byte *dst, int pitch, int width, int height,
// RA1 decodes FOBJs at chunk coordinates, then displays a scrolled 320x200
// window inside the 384x242 framebuffer.
void InsaneRebel1::procPreRendering(byte *renderBitmap) {
+ _frameGameOpcodeMask = 0;
+ _frameDispatchFlags = 0;
+
if (_interactiveVideoActive && _player) {
// FUN_224FD stores absolute 320x200 window origin in a 384x242 frame:
// X in [0..0x40], Y in [0..0x2E], centered at (0x20,0x17).
@@ -356,14 +359,20 @@ void InsaneRebel1::procPostRendering(byte *renderBitmap, int32 codecparam, int32
if (height == 0) height = _screenHeight;
int pitch = width;
- const bool asteroidMode = (_activeGameOpcode == 0x0B);
+ const bool haveFrameGameOpcodes = (_frameGameOpcodeMask != 0);
+ const bool asteroidMode = hasFrameGameOpcode(0x0B) ||
+ (!haveFrameGameOpcodes && _activeGameOpcode == 0x0B);
if (asteroidMode) {
// First-person asteroid/surface handler â opcode 0x0B (FUN_1CDA7).
updateAsteroidPhysics();
} else {
- const bool turretMode = (_activeGameOpcode == 0x08 || _activeGameOpcode == 0x0A);
- const bool flightMode = (_activeGameOpcode == 0x07 || _activeGameOpcode == 0x09 ||
- _activeGameOpcode == 0x19 || _activeGameOpcode == 0x1A);
+ const bool turretMode = hasFrameGameOpcode(0x08) || hasFrameGameOpcode(0x0A) ||
+ (!haveFrameGameOpcodes && (_activeGameOpcode == 0x08 || _activeGameOpcode == 0x0A));
+ const bool flightMode = hasFrameGameOpcode(0x07) || hasFrameGameOpcode(0x09) ||
+ hasFrameGameOpcode(0x19) || hasFrameGameOpcode(0x1A) ||
+ (!haveFrameGameOpcodes &&
+ (_activeGameOpcode == 0x07 || _activeGameOpcode == 0x09 ||
+ _activeGameOpcode == 0x19 || _activeGameOpcode == 0x1A));
// Dispatch movement path by GAME handler family:
// 0x08/0x0A -> FUN_1E6A7/FUN_1D79C (turret/cockpit)
@@ -378,6 +387,7 @@ void InsaneRebel1::procPostRendering(byte *renderBitmap, int32 codecparam, int32
// (see 0x1626E/0x162EE -> 0x165DD and 0x1640B -> 0x16614), then plays crash video.
// Do not render the in-engine death overlay in this path; finish immediately.
if (_currentLevel == 0 && _health < 0) {
+ _fireCooldown = _playerFired ? 1 : 0;
_vm->_smushVideoShouldFinish = true;
return;
}
@@ -396,12 +406,18 @@ void InsaneRebel1::procPostRendering(byte *renderBitmap, int32 codecparam, int32
}
// Assembly dispatch (FUN_1BE1B) only runs the targeting/shot overlay pipeline
- // in handlers 0x09/0x0A/0x0B/0x1A. In LVL1 stage-2 samples, 0x08 drives
- // turret mode and needs the same targeting overlays enabled.
+ // in handlers 0x09/0x0A/0x0B/0x1A. LVL1 stage-2 works because the stream emits
+ // both 0x0A and 0x08 in the same frame, not because 0x08 owns the overlay path.
const bool hasTargetingPipeline =
- (_activeGameOpcode == 0x08 || _activeGameOpcode == 0x09 || _activeGameOpcode == 0x0A ||
- _activeGameOpcode == 0x0B || _activeGameOpcode == 0x1A);
+ hasFrameGameOpcode(0x09) || hasFrameGameOpcode(0x0A) ||
+ hasFrameGameOpcode(0x0B) || hasFrameGameOpcode(0x1A) ||
+ (!haveFrameGameOpcodes &&
+ (_activeGameOpcode == 0x09 || _activeGameOpcode == 0x0A ||
+ _activeGameOpcode == 0x0B || _activeGameOpcode == 0x1A));
if (hasTargetingPipeline) {
+ const bool turretTargetingMode =
+ hasFrameGameOpcode(0x0A) ||
+ (!haveFrameGameOpcodes && _activeGameOpcode == 0x0A);
renderTargetBoxes(renderBitmap, pitch, width, height);
processShot();
for (int i = 0; i < kMaxShotSlots; i++) {
@@ -411,6 +427,17 @@ void InsaneRebel1::procPostRendering(byte *renderBitmap, int32 codecparam, int32
renderLaserShots(renderBitmap, pitch, width, height);
renderGostSlots(renderBitmap, pitch, width, height);
renderTargeting(renderBitmap, pitch, width, height);
+
+ // FUN_1D79C (GAME 0x0A) owns the cursor center in stage-2 turret mode.
+ // The preceding overlay/shot pass uses the previous frame's cursor; the
+ // handler then publishes the next cursor position from the current
+ // ship-offset and camera state.
+ if (turretTargetingMode) {
+ const int16 shipOffsetX = (int16)(_posAccumX >> 8);
+ const int16 shipOffsetY = (int16)(_posAccumY >> 8);
+ _shipPosX = (int16)(kRA1CenterX + shipOffsetX);
+ _shipPosY = (int16)((kRA1CenterY + shipOffsetY - 0x23) - (shipOffsetY >> 3));
+ }
} else {
// Keep lock/target accumulators quiescent when current handler doesn't
// execute FUN_1C940/FUN_1CCA0/FUN_1C9CD/FUN_1CB22.
@@ -423,6 +450,7 @@ void InsaneRebel1::procPostRendering(byte *renderBitmap, int32 codecparam, int32
renderExplosions(renderBitmap, pitch, width, height);
renderHUD(renderBitmap, pitch, width, height);
+ _fireCooldown = _playerFired ? 1 : 0;
}
// renderTargetBoxes â FUN_1C940 (0x1C940). Per-target green box overlays.
@@ -459,7 +487,7 @@ void InsaneRebel1::renderTargeting(byte *dst, int pitch, int width, int height)
if (markerBank.numSprites > 0) {
// FUN_1CB22 can switch marker sets via DAT_75FF bit 1.
// Baseline RA1 targeting uses '^' and animation e..h.
- const bool altMarkerSet = false;
+ const bool altMarkerSet = (_gameplayFlags75ff & 0x2) != 0;
// Lock indicator at fixed center positions:
// FUN_1CB22 draws marker strings at (0xA0,0x78) and (0xA0,0x7E).
Commit: 2a7ffb4168924d14ee45b1b38c800c6f336ff405
https://github.com/scummvm/scummvm/commit/2a7ffb4168924d14ee45b1b38c800c6f336ff405
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:39+02:00
Commit Message:
SCUMM: RA1: Trigger death video immediately
Changed paths:
engines/scumm/insane/insane_rebel1_render.cpp
engines/scumm/insane/insane_rebel1_runlevels.cpp
diff --git a/engines/scumm/insane/insane_rebel1_render.cpp b/engines/scumm/insane/insane_rebel1_render.cpp
index ce61d86921a..6d669ca12e6 100644
--- a/engines/scumm/insane/insane_rebel1_render.cpp
+++ b/engines/scumm/insane/insane_rebel1_render.cpp
@@ -383,10 +383,11 @@ void InsaneRebel1::procPostRendering(byte *renderBitmap, int32 codecparam, int32
updateShipPhysics();
}
- // LVL1 assembly flow exits gameplay loops as soon as health drops below 0
- // (see 0x1626E/0x162EE -> 0x165DD and 0x1640B -> 0x16614), then plays crash video.
- // Do not render the in-engine death overlay in this path; finish immediately.
- if (_currentLevel == 0 && _health < 0) {
+ // Most RA1 gameplay loops exit the current interactive movie as soon as
+ // health drops below 0, then dispatch to the level-specific retry/death
+ // clip. LVL9's on-foot flow is the main exception: it keeps pumping while
+ // deathTimer > 1 before branching out of the current segment.
+ if (_health < 0 && _currentLevel != 8) {
_fireCooldown = _playerFired ? 1 : 0;
_vm->_smushVideoShouldFinish = true;
return;
diff --git a/engines/scumm/insane/insane_rebel1_runlevels.cpp b/engines/scumm/insane/insane_rebel1_runlevels.cpp
index 562d30d09b3..1b7eb1e788d 100644
--- a/engines/scumm/insane/insane_rebel1_runlevels.cpp
+++ b/engines/scumm/insane/insane_rebel1_runlevels.cpp
@@ -445,10 +445,10 @@ bool InsaneRebel1::runLevel5() {
if (_killCount <= 2) {
if (_lives > 0) {
+ _lives--;
playCinematic("LVL5/L5RETRY.ANM");
if (_vm->shouldQuit())
return false;
- _lives--;
continue;
}
Commit: a6ab490d52fb6773025f7811aa2a30f955fcd921
https://github.com/scummvm/scummvm/commit/a6ab490d52fb6773025f7811aa2a30f955fcd921
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:39+02:00
Commit Message:
SCUMM: RA1: Add additional level implementations
Changed paths:
engines/scumm/insane/insane_rebel1.cpp
engines/scumm/insane/insane_rebel1.h
engines/scumm/insane/insane_rebel1_iact.cpp
engines/scumm/insane/insane_rebel1_runlevels.cpp
diff --git a/engines/scumm/insane/insane_rebel1.cpp b/engines/scumm/insane/insane_rebel1.cpp
index 47aecc90d06..d6d1e3ee582 100644
--- a/engines/scumm/insane/insane_rebel1.cpp
+++ b/engines/scumm/insane/insane_rebel1.cpp
@@ -187,11 +187,13 @@ InsaneRebel1::InsaneRebel1(ScummEngine_v7 *scumm) : Insane(), _vm(scumm) {
_frameCounter = 0;
_interactiveVideoActive = false;
_gameCounter = 0;
- _pathBranchEnabled = false;
- _rightPathSelected = false;
- _levelRouteIndex = -1;
- _pendingRouteIndex = -1;
- _menuActive = false;
+ _pathBranchEnabled = false;
+ _rightPathSelected = false;
+ _levelRouteIndex = -1;
+ _pendingRouteIndex = -1;
+ _levelRouteChoice = 0;
+ _levelGameplayPhase = 0;
+ _menuActive = false;
_menuConfirmed = false;
_menuSelection = 0;
_menuFrameCounter = 0;
diff --git a/engines/scumm/insane/insane_rebel1.h b/engines/scumm/insane/insane_rebel1.h
index 240467be46f..fa6456355ad 100644
--- a/engines/scumm/insane/insane_rebel1.h
+++ b/engines/scumm/insane/insane_rebel1.h
@@ -303,12 +303,14 @@ private:
// Path branching for levels with left/right alternative videos.
// Original sets nextSceneA/nextSceneB when GAME 0x07 counter == 394 (0x18A).
// We check ship position at that counter value to decide left vs right path.
- static const int32 kPathBranchCounter = 394; // GAME 0x07 field1 value
- int32 _gameCounter; // GAME 0x07 field1 â the original's _DAT_7740
- bool _pathBranchEnabled; // True when branching is active for this video
- bool _rightPathSelected; // True if player chose the right/easy path
- int _levelRouteIndex; // Current mid-level route/segment for branching levels
- int _pendingRouteIndex; // Next route requested by original frame-branch logic
+ static const int32 kPathBranchCounter = 394; // GAME 0x07 field1 value
+ int32 _gameCounter; // GAME 0x07 field1 â the original's _DAT_7740
+ bool _pathBranchEnabled; // True when branching is active for this video
+ bool _rightPathSelected; // True if player chose the right/easy path
+ int _levelRouteIndex; // Current mid-level route/segment for branching levels
+ int _pendingRouteIndex; // Next route requested by original frame-branch logic
+ int _levelRouteChoice; // Level-local pending branch choice (0=none, 1=left, 2=right)
+ int _levelGameplayPhase; // Level-local interactive phase (e.g. LVL4 PLAY1 vs PLAY2)
// Main menu / options state
void runOptionsMenu();
diff --git a/engines/scumm/insane/insane_rebel1_iact.cpp b/engines/scumm/insane/insane_rebel1_iact.cpp
index 34571abf24d..c01f5175bbd 100644
--- a/engines/scumm/insane/insane_rebel1_iact.cpp
+++ b/engines/scumm/insane/insane_rebel1_iact.cpp
@@ -99,6 +99,259 @@ static const int16 kLevel8BranchFrames[3][3] = {
{ 877, -2, -2 }
};
+static inline bool isLevel4DamageLatch(uint16 code) {
+ switch (code) {
+ case 0x0008:
+ case 0x000A:
+ case 0x000C:
+ case 0x0010:
+ case 0x0018:
+ case 0x001C:
+ case 0x001E:
+ case 0x0024:
+ case 0x0026:
+ case 0x0028:
+ case 0x002D:
+ return true;
+ default:
+ return false;
+ }
+}
+
+static inline bool isLevel6DamageLatch(uint16 code) {
+ switch (code) {
+ case 0x0003:
+ case 0x0008:
+ case 0x0009:
+ case 0x000D:
+ case 0x000F:
+ case 0x0028:
+ return true;
+ default:
+ return false;
+ }
+}
+
+static inline bool isLevel10DamageLatch(uint16 code) {
+ if (code < 0x7F) {
+ if (code > 0x3D) {
+ if (code > 0x3E) {
+ if (code < 0x5C) {
+ if (code < 0x54) {
+ if (code < 0x44)
+ return false;
+ if (code > 0x44)
+ return code == 0x4F;
+ } else if (code > 0x54) {
+ if (code < 0x58)
+ return code == 0x56;
+ if (code < 0x59)
+ return true;
+ return code == 0x5A;
+ }
+ } else if (code > 0x5C) {
+ if (code < 0x73) {
+ if (code < 0x6A)
+ return false;
+ if (code > 0x6A)
+ return code == 0x6F;
+ } else if (code > 0x73) {
+ if (code < 0x77)
+ return code == 0x75;
+ if (code < 0x78)
+ return true;
+ return code == 0x7B;
+ }
+ }
+ }
+ return true;
+ }
+
+ if (code > 0x1F) {
+ if (code > 0x20) {
+ if (code < 0x2E) {
+ if (code < 0x22)
+ return false;
+ if (code > 0x22)
+ return code == 0x24;
+ } else if (code > 0x2E) {
+ if (code < 0x35)
+ return code == 0x32;
+ if (code < 0x36)
+ return false;
+ return code == 0x3C;
+ }
+ }
+ return true;
+ }
+
+ if (code > 0x0E) {
+ if (code > 0x0F) {
+ if (code < 0x14)
+ return false;
+ if (code > 0x14)
+ return code == 0x1A;
+ }
+ return true;
+ }
+
+ return code > 3 && (code < 5 || code == 6);
+ }
+
+ if (code > 0x7F) {
+ if (code < 0xC7) {
+ if (code < 0xA2) {
+ if (code < 0x95) {
+ if (code < 0x83)
+ return false;
+ if (code > 0x83)
+ return code == 0x90;
+ } else if (code > 0x95) {
+ if (code < 0x9E)
+ return false;
+ if (code > 0x9E)
+ return code == 0xA0;
+ }
+ } else if (code > 0xA2) {
+ if (code < 0xB7) {
+ if (code < 0xA5)
+ return false;
+ if (code > 0xA5)
+ return code == 0xAD;
+ } else if (code > 0xB7) {
+ if (code < 0xBB)
+ return code == 0xB9;
+ if (code < 0xBC)
+ return true;
+ return code == 0xBE;
+ }
+ }
+ return true;
+ }
+
+ if (code > 0xC7) {
+ if (code < 0xE6) {
+ if (code < 0xD7) {
+ if (code < 0xC9)
+ return false;
+ if (code > 0xC9)
+ return code == 0xCF;
+ } else if (code > 0xD9) {
+ if (code < 0xDD)
+ return code == 0xDB;
+ if (code < 0xDE)
+ return true;
+ return code == 0xDF;
+ }
+ } else if (code > 0xE6) {
+ if (code < 0xF5) {
+ if (code < 0xF0)
+ return false;
+ if (code > 0xF0)
+ return code == 0xF3;
+ } else if (code > 0xF5) {
+ if (code < 0xFF)
+ return code == 0xFD;
+ if (code < 0x100)
+ return true;
+ return code == 0x103;
+ }
+ }
+ }
+ }
+
+ return true;
+}
+
+static inline bool hasLevel6PerspectiveHazard(uint16 frame, int16 perspectiveX, int16 perspectiveY) {
+ switch (frame) {
+ case 0x006A:
+ case 0x00FD:
+ case 0x011C:
+ case 0x0563:
+ return perspectiveX < 0x18;
+ case 0x0144:
+ return perspectiveX < 0x29 && perspectiveY < 0x20;
+ case 0x016F:
+ case 0x0222:
+ case 0x02EB:
+ return perspectiveX < 0x29;
+ case 0x01DE:
+ case 0x0492:
+ return perspectiveX < 8;
+ case 0x024A:
+ return perspectiveY < 0x27;
+ case 0x0318:
+ return perspectiveY < 0x0F;
+ case 0x0397:
+ return perspectiveX < 0x18 && perspectiveY < 0x20;
+ case 0x0405:
+ return perspectiveX > 0x28;
+ case 0x0462:
+ return perspectiveY < 0x20;
+ case 0x04F1:
+ return perspectiveX < 0x29 && perspectiveY >= 0x0F;
+ case 0x04FF:
+ return perspectiveX < 0x29 && perspectiveY < 0x20;
+ case 0x0617:
+ return !(perspectiveX > 7 && perspectiveX < 0x29 &&
+ perspectiveY > 0x0E && perspectiveY < 0x20);
+ default:
+ return false;
+ }
+}
+
+static inline bool hasLevel8PerspectiveHazardRoute0(uint16 frame, int16 perspectiveX, int16 perspectiveY) {
+ switch (frame) {
+ case 0x00CD:
+ return perspectiveX < 0x29;
+ case 0x00EF:
+ return perspectiveY < 0x0F;
+ case 0x0294:
+ case 0x04BE:
+ case 0x076C:
+ return perspectiveX < 0x29 && perspectiveY < 0x20;
+ case 0x03A2:
+ return perspectiveX < 0x18;
+ case 0x05C9:
+ case 0x085A:
+ case 0x096F:
+ return perspectiveY < 0x20;
+ default:
+ return false;
+ }
+}
+
+static inline bool hasLevel8PerspectiveHazardRoute1(uint16 frame, int16 perspectiveX, int16 perspectiveY) {
+ switch (frame) {
+ case 0x0189:
+ return perspectiveY < 0x0F;
+ case 0x0297:
+ return perspectiveX < 0x18;
+ case 0x03B3:
+ case 0x0661:
+ return perspectiveX < 0x29 && perspectiveY < 0x20;
+ case 0x04BE:
+ case 0x074F:
+ case 0x0864:
+ return perspectiveY < 0x20;
+ default:
+ return false;
+ }
+}
+
+static inline bool hasLevel8PerspectiveHazardRoute2(uint16 frame, int16 perspectiveX, int16 perspectiveY) {
+ switch (frame) {
+ case 0x00BB:
+ return perspectiveX < 0x29 && perspectiveY < 0x20;
+ case 0x01A9:
+ case 0x02BE:
+ return perspectiveY < 0x20;
+ default:
+ return false;
+ }
+}
+
void InsaneRebel1::resetFrameObjectState() {
memset(_frameObjectState, 0, sizeof(_frameObjectState));
for (int i = 0x50; i < 0x96; i++)
@@ -235,20 +488,31 @@ void InsaneRebel1::checkDynamicLevelBranch() {
const int frame = (int)_frameCounter;
const int leftBlockedFrame = kLevel8BranchFrames[route][2];
const int rightBlockedFrame = kLevel8BranchFrames[route][1];
+ const bool shotEdge = _playerFired && _fireCooldown == 0;
int nextRoute = -1;
for (int i = 0; i < 3; ++i) {
const int triggerFrame = kLevel8BranchFrames[route][i];
- if (triggerFrame >= 0 && frame == triggerFrame) {
- if (_shipPosX < kRA1CenterX) {
- if (frame != leftBlockedFrame)
- nextRoute = 1;
- } else {
- if (frame != rightBlockedFrame)
- nextRoute = 2;
- }
- break;
+ if (triggerFrame < 0)
+ continue;
+
+ if (shotEdge && frame > triggerFrame - 0x32 && frame <= triggerFrame)
+ _levelRouteChoice = (_shipPosX < kRA1CenterX) ? 1 : 2;
+
+ if (frame != triggerFrame)
+ continue;
+
+ const bool chooseLeft = (_levelRouteChoice == 1) ||
+ ((_shipPosX < kRA1CenterX) && (_levelRouteChoice != 2));
+ if (chooseLeft) {
+ if (frame != leftBlockedFrame)
+ nextRoute = 1;
+ } else {
+ if (frame != rightBlockedFrame)
+ nextRoute = 2;
}
+ _levelRouteChoice = 0;
+ break;
}
if (nextRoute >= 0 && nextRoute != route) {
@@ -757,11 +1021,38 @@ void InsaneRebel1::updateAsteroidPhysics() {
// RA1 FUN_1B297-style per-frame latches for 0x0B sections:
// 0x5D latch 0xFFFF -> bit 0x40 (scripted obstacle/contact)
// 0x5F non-zero + RNG -> bit 0x80 (scripted random hit)
- if (_gameLatch5D == 0xFFFF)
+ if (_gameLatch5D == 0xFFFF ||
+ (_currentLevel == 3 && isLevel4DamageLatch(_gameLatch5D)) ||
+ (_currentLevel == 5 && isLevel6DamageLatch(_gameLatch5D)) ||
+ (_currentLevel == 9 && isLevel10DamageLatch(_gameLatch5D)))
_damageFlags |= 0x40;
- if (_gameLatch5F != 0 && _vm->_rnd.getRandomNumber((uint16)(_gameLatch5F - 1)) == 0)
+ if (_gameLatch5F != 0 &&
+ ((_currentLevel == 3 || _currentLevel == 9)
+ ? (_vm->_rnd.getRandomNumber(2) == 0)
+ : (_vm->_rnd.getRandomNumber((uint16)(_gameLatch5F - 1)) == 0)))
_damageFlags |= 0x80;
+ if (_currentLevel == 5 && hasLevel6PerspectiveHazard((uint16)_frameCounter, _perspectiveX, _perspectiveY))
+ _damageFlags |= 0x20;
+
+ if (_currentLevel == 7) {
+ bool walkerHazard = false;
+ switch (CLIP<int>(_levelRouteIndex, 0, 2)) {
+ case 0:
+ walkerHazard = hasLevel8PerspectiveHazardRoute0((uint16)_frameCounter, _perspectiveX, _perspectiveY);
+ break;
+ case 1:
+ walkerHazard = hasLevel8PerspectiveHazardRoute1((uint16)_frameCounter, _perspectiveX, _perspectiveY);
+ break;
+ case 2:
+ walkerHazard = hasLevel8PerspectiveHazardRoute2((uint16)_frameCounter, _perspectiveX, _perspectiveY);
+ break;
+ }
+
+ if (walkerHazard)
+ _damageFlags |= 0x20;
+ }
+
// Health regeneration (FUN_1BB0E): +1 every 32 frames when alive
if (_health >= 0 && _health < kMaxHealth && (_frameCounter & 0x1F) == 0) {
_health++;
@@ -849,6 +1140,10 @@ void InsaneRebel1::updateAsteroidPhysics() {
resetProjectionTable();
_frameCounter++;
+
+ if (_currentLevel == 3 && _levelGameplayPhase == 2 && _frameCounter == 0x3E)
+ _gameplayFlags75ff |= 2;
+
checkDynamicLevelBranch();
debug(7, "RA1 asteroid: pos=(%d,%d) avg=(%d,%d) view=(%d,%d) health=%d flash=%d",
diff --git a/engines/scumm/insane/insane_rebel1_runlevels.cpp b/engines/scumm/insane/insane_rebel1_runlevels.cpp
index 1b7eb1e788d..5264bf83624 100644
--- a/engines/scumm/insane/insane_rebel1_runlevels.cpp
+++ b/engines/scumm/insane/insane_rebel1_runlevels.cpp
@@ -300,7 +300,9 @@ bool InsaneRebel1::runLevel3() {
_activeGameOpcode = 0;
_gameLatch5D = 0;
_gameLatch5F = 0;
+ _gameplayFlags75ff = 0;
_killCount = 0;
+ _levelGameplayPhase = 0;
memset(_inputHistoryX, 0, sizeof(_inputHistoryX));
memset(_inputHistoryY, 0, sizeof(_inputHistoryY));
memset(_viewHistoryX, 0, sizeof(_viewHistoryX));
@@ -357,8 +359,11 @@ bool InsaneRebel1::runLevel4() {
_activeGameOpcode = 0;
_gameLatch5D = 0;
_gameLatch5F = 0;
+ _gameplayFlags75ff = 0;
_killCount = 0;
+ _levelGameplayPhase = 0;
+ _levelGameplayPhase = 1;
playInteractiveVideo("LVL4/L4PLAY1.ANM");
if (_vm->shouldQuit())
return false;
@@ -367,7 +372,9 @@ bool InsaneRebel1::runLevel4() {
_activeGameOpcode = 0;
_gameLatch5D = 0;
_gameLatch5F = 0;
+ _gameplayFlags75ff = 0;
_killCount = 0;
+ _levelGameplayPhase = 2;
playInteractiveVideo("LVL4/L4PLAY2.ANM");
if (_vm->shouldQuit())
return false;
@@ -418,7 +425,9 @@ bool InsaneRebel1::runLevel5() {
_activeGameOpcode = 0;
_gameLatch5D = 0;
_gameLatch5F = 0;
+ _gameplayFlags75ff = 0;
_killCount = 0;
+ _levelGameplayPhase = 0;
memset(_inputHistoryX, 0, sizeof(_inputHistoryX));
memset(_inputHistoryY, 0, sizeof(_inputHistoryY));
memset(_viewHistoryX, 0, sizeof(_viewHistoryX));
@@ -513,7 +522,9 @@ bool InsaneRebel1::runLevel6() {
_activeGameOpcode = 0;
_gameLatch5D = 0;
_gameLatch5F = 0;
+ _gameplayFlags75ff = 0;
_killCount = 0;
+ _levelGameplayPhase = 0;
memset(_inputHistoryX, 0, sizeof(_inputHistoryX));
memset(_inputHistoryY, 0, sizeof(_inputHistoryY));
memset(_viewHistoryX, 0, sizeof(_viewHistoryX));
@@ -578,6 +589,7 @@ bool InsaneRebel1::runLevel7() {
_activeGameOpcode = 0;
_gameLatch5D = 0;
_gameLatch5F = 0;
+ _gameplayFlags75ff = 0;
_killCount = 0;
_targetCount = 0;
_prevTargetCount = 0;
@@ -591,6 +603,7 @@ bool InsaneRebel1::runLevel7() {
_posAccumY = 0;
_perspectiveX = 0;
_perspectiveY = 0;
+ _levelGameplayPhase = 0;
memset(_inputHistoryX, 0, sizeof(_inputHistoryX));
memset(_inputHistoryY, 0, sizeof(_inputHistoryY));
memset(_viewHistoryX, 0, sizeof(_viewHistoryX));
@@ -668,6 +681,7 @@ bool InsaneRebel1::runLevel8() {
_activeGameOpcode = 0;
_gameLatch5D = 0;
_gameLatch5F = 0;
+ _gameplayFlags75ff = 0;
_killCount = 0;
_targetCount = 0;
_prevTargetCount = 0;
@@ -681,6 +695,7 @@ bool InsaneRebel1::runLevel8() {
_posAccumY = 0;
_perspectiveX = 0;
_perspectiveY = 0;
+ _levelGameplayPhase = 0;
memset(_inputHistoryX, 0, sizeof(_inputHistoryX));
memset(_inputHistoryY, 0, sizeof(_inputHistoryY));
memset(_viewHistoryX, 0, sizeof(_viewHistoryX));
@@ -756,6 +771,7 @@ bool InsaneRebel1::runLevel9() {
_activeGameOpcode = 0;
_gameLatch5D = 0;
_gameLatch5F = 0;
+ _gameplayFlags75ff = 0;
_killCount = 0;
_targetCount = 0;
_prevTargetCount = 0;
@@ -769,6 +785,7 @@ bool InsaneRebel1::runLevel9() {
_posAccumY = 0;
_perspectiveX = 0;
_perspectiveY = 0;
+ _levelGameplayPhase = 0;
memset(_inputHistoryX, 0, sizeof(_inputHistoryX));
memset(_inputHistoryY, 0, sizeof(_inputHistoryY));
memset(_viewHistoryX, 0, sizeof(_viewHistoryX));
@@ -931,6 +948,7 @@ bool InsaneRebel1::runLevel10() {
_activeGameOpcode = 0;
_gameLatch5D = 0;
_gameLatch5F = 0;
+ _gameplayFlags75ff = 0;
_killCount = 0;
_targetCount = 0;
_prevTargetCount = 0;
@@ -944,6 +962,7 @@ bool InsaneRebel1::runLevel10() {
_posAccumY = 0;
_perspectiveX = 0;
_perspectiveY = 0;
+ _levelGameplayPhase = 0;
memset(_inputHistoryX, 0, sizeof(_inputHistoryX));
memset(_inputHistoryY, 0, sizeof(_inputHistoryY));
memset(_viewHistoryX, 0, sizeof(_viewHistoryX));
@@ -1046,6 +1065,7 @@ void InsaneRebel1::playInteractiveVideo(const char *filename, int32 startFrame)
_player = splayer;
clearBit(0);
_interactiveVideoActive = true;
+ _levelRouteChoice = 0;
resetFrameObjectState();
_vm->_smushVideoShouldFinish = false;
splayer->setCurVideoFlags(0x28);
Commit: 53bc68ed7be6d27e5f2f083ae821847fe3a5df82
https://github.com/scummvm/scummvm/commit/53bc68ed7be6d27e5f2f083ae821847fe3a5df82
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:39+02:00
Commit Message:
SCUMM: RA1: Add missing sound effects
Changed paths:
engines/scumm/insane/insane_rebel1.cpp
engines/scumm/insane/insane_rebel1.h
engines/scumm/insane/insane_rebel1_audio.cpp
engines/scumm/insane/insane_rebel1_iact.cpp
diff --git a/engines/scumm/insane/insane_rebel1.cpp b/engines/scumm/insane/insane_rebel1.cpp
index d6d1e3ee582..2f4a7462949 100644
--- a/engines/scumm/insane/insane_rebel1.cpp
+++ b/engines/scumm/insane/insane_rebel1.cpp
@@ -254,6 +254,9 @@ InsaneRebel1::InsaneRebel1(ScummEngine_v7 *scumm) : Insane(), _vm(scumm) {
// Audio
initAudio(11025);
+ memset(_sfxData, 0, sizeof(_sfxData));
+ memset(_sfxSize, 0, sizeof(_sfxSize));
+ loadSfx();
// Null out Insane base class pointers that the default constructor doesn't initialize
_smush_roadrashRip = nullptr;
@@ -276,6 +279,7 @@ InsaneRebel1::InsaneRebel1(ScummEngine_v7 *scumm) : Insane(), _vm(scumm) {
InsaneRebel1::~InsaneRebel1() {
_vm->_system->getEventManager()->getEventDispatcher()->unregisterObserver(this);
terminateAudio();
+ freeSfx();
}
} // End of namespace Scumm
diff --git a/engines/scumm/insane/insane_rebel1.h b/engines/scumm/insane/insane_rebel1.h
index fa6456355ad..eeb6f6bb4bf 100644
--- a/engines/scumm/insane/insane_rebel1.h
+++ b/engines/scumm/insane/insane_rebel1.h
@@ -160,6 +160,9 @@ private:
// Audio
void initAudio(int sampleRate);
void terminateAudio();
+ void loadSfx();
+ void freeSfx();
+ void playSfx(int slot, int volume, int pan);
void queueAudioData(int trackIdx, uint8 *data, int32 size, int volume, int pan);
public:
void drawFontBankString(byte *dst, int pitch, int width, int height, int x, int y, const char *text);
@@ -296,6 +299,20 @@ private:
Audio::SoundHandle _audioHandles[kMaxAudioTracks];
bool _audioTrackActive[kMaxAudioTracks];
int _audioSampleRate;
+ static const int kNumSfx = 8;
+ enum SfxSlot {
+ kSfxLaserShot = 0,
+ kSfxExplode = 1,
+ kSfxBoom = 2,
+ kSfxKlaxon = 3,
+ kSfxLockOn = 4,
+ kSfxAlert = 5,
+ kSfxBonus = 6,
+ kSfxBlast = 7
+ };
+ byte *_sfxData[kNumSfx];
+ uint32 _sfxSize[kNumSfx];
+ Audio::SoundHandle _sfxHandles[kNumSfx];
// True only while an interactive gameplay SMUSH is running.
bool _interactiveVideoActive;
diff --git a/engines/scumm/insane/insane_rebel1_audio.cpp b/engines/scumm/insane/insane_rebel1_audio.cpp
index 7ca9e73d7b5..5801d0dce6f 100644
--- a/engines/scumm/insane/insane_rebel1_audio.cpp
+++ b/engines/scumm/insane/insane_rebel1_audio.cpp
@@ -21,6 +21,8 @@
#include "common/system.h"
+#include "scumm/file.h"
+
#include "audio/audiostream.h"
#include "audio/decoders/raw.h"
#include "audio/mixer.h"
@@ -30,6 +32,17 @@
namespace Scumm {
+static const char *const kRA1SfxFiles[8] = {
+ "SYS/LASRSHOT.SAD",
+ "SYS/EXPLODE.SAD",
+ "SYS/BOOM.SAD",
+ "SYS/KLAXON.SAD",
+ "SYS/LOCKON.SAD",
+ "SYS/ALERT.SAD",
+ "SYS/BONUS.SAD",
+ "SYS/BLAST.SAD"
+};
+
// ---------------------------------------------------------------------------
// Audio
// ---------------------------------------------------------------------------
@@ -53,6 +66,10 @@ void InsaneRebel1::terminateAudio() {
_audioStreams[i] = nullptr;
}
}
+
+ for (int i = 0; i < kNumSfx; i++) {
+ _vm->_mixer->stopHandle(_sfxHandles[i]);
+ }
}
void InsaneRebel1::queueAudioData(int trackIdx, uint8 *data, int32 size, int volume, int pan) {
@@ -202,4 +219,97 @@ void InsaneRebel1::processAudioFrame(int16 feedSize) {
}
}
+void InsaneRebel1::loadSfx() {
+ for (int i = 0; i < kNumSfx; i++) {
+ if (_sfxData[i] || _sfxSize[i] != 0)
+ continue;
+
+ ScummFile *file = _vm->instantiateScummFile();
+ _vm->openFile(*file, kRA1SfxFiles[i]);
+ if (!file->isOpen()) {
+ debug("InsaneRebel1::loadSfx: could not open %s", kRA1SfxFiles[i]);
+ delete file;
+ continue;
+ }
+
+ const uint32 fileSize = file->size();
+ if (fileSize < 16) {
+ debug("InsaneRebel1::loadSfx: %s too small (%u bytes)", kRA1SfxFiles[i], fileSize);
+ file->close();
+ delete file;
+ continue;
+ }
+
+ const uint32 tag = file->readUint32BE();
+ if (tag != MKTAG('S', 'A', 'U', 'D')) {
+ debug("InsaneRebel1::loadSfx: %s not a SAUD file (tag=0x%08x)", kRA1SfxFiles[i], tag);
+ file->close();
+ delete file;
+ continue;
+ }
+ file->readUint32BE();
+
+ bool foundSdat = false;
+ while (file->pos() + 8 <= (int64)fileSize) {
+ const uint32 chunkTag = file->readUint32BE();
+ const uint32 chunkSize = file->readUint32BE();
+ if (chunkTag == MKTAG('S', 'D', 'A', 'T')) {
+ const uint32 pcmSize = MIN(chunkSize, fileSize - (uint32)file->pos());
+ byte *pcm = (byte *)malloc(pcmSize);
+ if (pcm) {
+ file->read(pcm, pcmSize);
+ _sfxData[i] = pcm;
+ _sfxSize[i] = pcmSize;
+ debug("InsaneRebel1::loadSfx: loaded %s (%u bytes PCM)", kRA1SfxFiles[i], pcmSize);
+ }
+ foundSdat = true;
+ break;
+ }
+ file->seek(chunkSize, SEEK_CUR);
+ }
+
+ if (!foundSdat)
+ debug("InsaneRebel1::loadSfx: no SDAT chunk in %s", kRA1SfxFiles[i]);
+
+ file->close();
+ delete file;
+ }
+}
+
+void InsaneRebel1::freeSfx() {
+ for (int i = 0; i < kNumSfx; i++) {
+ _vm->_mixer->stopHandle(_sfxHandles[i]);
+ free(_sfxData[i]);
+ _sfxData[i] = nullptr;
+ _sfxSize[i] = 0;
+ }
+}
+
+void InsaneRebel1::playSfx(int slot, int volume, int pan) {
+ if (slot < 0 || slot >= kNumSfx || !_sfxData[slot] || _sfxSize[slot] == 0)
+ return;
+ if (_player && !_player->isChanActive(CHN_OTHER))
+ return;
+
+ _vm->_mixer->stopHandle(_sfxHandles[slot]);
+
+ byte *pcmCopy = (byte *)malloc(_sfxSize[slot]);
+ if (!pcmCopy)
+ return;
+ memcpy(pcmCopy, _sfxData[slot], _sfxSize[slot]);
+
+ Audio::SeekableAudioStream *stream = Audio::makeRawStream(
+ pcmCopy, _sfxSize[slot], 11025, Audio::FLAG_UNSIGNED, DisposeAfterUse::YES);
+ int mixVolume = CLIP(volume, 0, 127);
+ if (_player) {
+ const int baseVolume = (_player->_smushTrackVols[1] * mixVolume) >> 7;
+ mixVolume = (baseVolume * _player->_smushTrackVols[0]) / 127;
+ }
+ const int scaledVolume = (mixVolume * Audio::Mixer::kMaxChannelVolume) / 127;
+ const int clampedPan = CLIP(pan, -127, 127);
+
+ _vm->_mixer->playStream(Audio::Mixer::kSFXSoundType, &_sfxHandles[slot],
+ stream, -1, scaledVolume, clampedPan);
+}
+
} // End of namespace Scumm
diff --git a/engines/scumm/insane/insane_rebel1_iact.cpp b/engines/scumm/insane/insane_rebel1_iact.cpp
index c01f5175bbd..611413d556a 100644
--- a/engines/scumm/insane/insane_rebel1_iact.cpp
+++ b/engines/scumm/insane/insane_rebel1_iact.cpp
@@ -1416,6 +1416,7 @@ void InsaneRebel1::processShot() {
_shotSlots[slot].centerY = shipCenterY;
_shotSlots[slot].variant = _shotAlternator;
_shotAlternator = 1 - _shotAlternator;
+ playSfx(kSfxLaserShot, 127, 0);
debug(5, "RA1 shot: slot=%d pos=(%d,%d)", slot, _shotSlots[slot].posX, _shotSlots[slot].posY);
}
@@ -1474,6 +1475,11 @@ void InsaneRebel1::checkTargetHit(int16 targetIdx, int16 left, int16 top, int16
_score += _tuning.kill;
_killCount++;
applyFrameObjectHitState(targetIdx);
+ int16 hitCenterX = (left + right) / 2;
+ int16 hitCenterY = (top + bottom) / 2;
+ projectGameplayPoint(hitCenterX, hitCenterY);
+ const int sfxPan = CLIP((hitCenterX - kRA1CenterX) * 127 / kRA1CenterX, -127, 127);
+ playSfx(kSfxExplode, 127, sfxPan);
// Match FUN_1C0EF: snap in unprojected space, then project back
// into the current gameplay window before rendering the pointer.
Commit: 26edb60399be48831890751ef01326bd5ba918ad
https://github.com/scummvm/scummvm/commit/26edb60399be48831890751ef01326bd5ba918ad
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:40+02:00
Commit Message:
SCUMM: RA1: Use named parameters
Changed paths:
engines/scumm/insane/insane_rebel1.cpp
engines/scumm/insane/insane_rebel1.h
engines/scumm/insane/insane_rebel1_iact.cpp
diff --git a/engines/scumm/insane/insane_rebel1.cpp b/engines/scumm/insane/insane_rebel1.cpp
index 2f4a7462949..b0bf2382788 100644
--- a/engines/scumm/insane/insane_rebel1.cpp
+++ b/engines/scumm/insane/insane_rebel1.cpp
@@ -36,89 +36,161 @@
namespace Scumm {
-// Per-difficulty tuning tables from assault_data_3.bin
-// Indexed: difficulty * 0x28B + level * 0x1F + offset
-// Fields: roll, lift, slide, drift, snap, miss, wham, shot, kill
-static const int16 kTuningTable[10][3][9] = {
- // Level 1 (Flight Training)
+// Per-difficulty tuning tables from assault_data_3.bin (also loadable from C:\rebltune.txt)
+// 21 sub-levels x 3 difficulties x 13 fields
+// Fields: roll, lift, slide, drift, snap, miss, wham, shot, kill, time, levelPts, bonus, flags
+static const int16 kTuningTable[21][3][13] = {
+ // Sub-level 0: "1A" (Flight Training - canyon flight)
{
- { 100, 100, 60, 110, 0, 0, 15, 0, 0 }, // Easy
- { 100, 105, 60, 115, 0, 0, 25, 0, 0 }, // Normal
- { 105, 110, 65, 120, 0, 0, 30, 0, 0 }, // Hard
+ { 100, 100, 60, 110, 0, 0, 15, 0, 0, 5, 500, 100, 2048 }, // Easy
+ { 100, 105, 60, 115, 0, 0, 25, 0, 0, 5, 1000, 200, 2048 }, // Normal
+ { 105, 110, 65, 120, 0, 0, 30, 0, 0, 10, 1500, 500, 2050 }, // Hard
},
- // Level 2 (Asteroid Field Training)
+ // Sub-level 1: "1B" (Flight Training - asteroid flight)
{
- { 100, 16, 120, 0, 7, 0, 15, 0, 25 }, // Easy
- { 100, 18, 120, 0, 5, 0, 20, 0, 50 }, // Normal
- { 100, 20, 150, 0, 1, 0, 25, 0, 75 }, // Hard
+ { 100, 16, 120, 0, 7, 0, 15, 0, 25, 5, 500, 100, 3072 }, // Easy
+ { 100, 18, 120, 0, 5, 0, 20, 0, 50, 5, 1000, 200, 3072 }, // Normal
+ { 100, 20, 150, 0, 1, 0, 25, 0, 75, 10, 1500, 500, 3074 }, // Hard
},
- // Level 3 (Planet Kolaador)
+ // Sub-level 2: "2" (Planet Kolaador)
{
- { 0, 0, 0, 0, 4, 15, 25, 0, 25 }, // Easy
- { 0, 0, 0, 0, 2, 18, 30, 0, 50 }, // Normal
- { 0, 0, 0, 0, 0, 20, 35, 0, 75 }, // Hard
+ { 0, 0, 0, 0, 4, 15, 25, 0, 25, 10, 500, 100, 2048 }, // Easy
+ { 0, 0, 0, 0, 2, 18, 30, 0, 50, 10, 1000, 200, 2048 }, // Normal
+ { 0, 0, 0, 0, 0, 20, 35, 0, 75, 10, 1500, 500, 2050 }, // Hard
},
- // Level 4 (Star Destroyer Attack)
+ // Sub-level 3: "3" (Star Destroyer Attack)
{
- { 70, 100, 150, 90, 0, 0, 20, 0, 0 }, // Easy
- { 72, 105, 155, 105, 0, 0, 25, 0, 0 }, // Normal
- { 75, 110, 160, 110, 0, 0, 28, 0, 0 }, // Hard
+ { 70, 100, 150, 90, 0, 0, 20, 0, 0, 5, 1000, 100, 2048 }, // Easy
+ { 72, 105, 155, 105, 0, 0, 25, 0, 0, 5, 2000, 200, 2048 }, // Normal
+ { 75, 110, 160, 110, 0, 0, 28, 0, 0, 10, 3000, 500, 2050 }, // Hard
},
- // Level 5 (Tatooine Attack)
+ // Sub-level 4: "4A" (Tatooine Attack)
{
- { 0, 0, 0, 0, 2, 11, 0, 4, 25 }, // Easy
- { 0, 0, 0, 0, 1, 25, 0, 6, 50 }, // Normal
- { 0, 0, 0, 0, 1, 28, 0, 6, 75 }, // Hard
+ { 0, 0, 0, 0, 2, 11, 0, 4, 25, 5, 500, 750, 2048 }, // Easy
+ { 0, 0, 0, 0, 1, 25, 0, 6, 50, 5, 1000, 1500, 2048 }, // Normal
+ { 0, 0, 0, 0, 1, 28, 0, 6, 75, 10, 1500, 2000, 2050 }, // Hard
},
- // Level 6 (Asteroid Field Chase)
+ // Sub-level 5: "4B" (Tatooine Attack part 2)
{
- { 0, 0, 0, 0, 3, 20, 0, 2, 50 }, // Easy
- { 0, 0, 0, 0, 1, 25, 0, 5, 100 }, // Normal
- { 0, 0, 0, 0, 1, 28, 0, 6, 200 }, // Hard
+ { 0, 0, 0, 0, 3, 20, 0, 2, 50, 5, 500, 750, 2064 }, // Easy
+ { 0, 0, 0, 0, 1, 25, 0, 5, 100, 5, 1000, 1500, 2064 }, // Normal
+ { 0, 0, 0, 0, 1, 28, 0, 6, 200, 10, 1500, 2000, 2064 }, // Hard
},
- // Level 7 (Imperial Probe Droids)
+ // Sub-level 6: "5A" (Imperial Probe Droids - speeder)
{
- { 70, 150, 50, 25, 10, 0, 20, 0, 25 }, // Easy
- { 72, 165, 155, 30, 8, 0, 30, 0, 50 }, // Normal
- { 110, 190, 55, 65, 3, 0, 33, 0, 75 }, // Hard
+ { 70, 150, 50, 25, 10, 0, 20, 0, 25, 5, 500, 15, 3072 }, // Easy
+ { 72, 165, 155, 30, 8, 0, 30, 0, 50, 5, 1000, 30, 3072 }, // Normal
+ { 110, 190, 55, 65, 3, 0, 33, 0, 75, 10, 1500, 75, 3074 }, // Hard
},
- // Level 8 (Imperial Walkers)
+ // Sub-level 7: "5B" (Imperial Walkers)
{
- { 0, 0, 0, 0, 5, 0, 0, 2, 25 }, // Easy
- { 0, 0, 0, 0, 3, 0, 0, 5, 50 }, // Normal
- { 0, 0, 0, 0, 1, 0, 0, 6, 75 }, // Hard
+ { 0, 0, 0, 0, 5, 0, 0, 2, 25, 0, 500, 15, 2048 }, // Easy
+ { 0, 0, 0, 0, 3, 0, 0, 5, 50, 5, 1000, 30, 2048 }, // Normal
+ { 0, 0, 0, 0, 1, 0, 0, 6, 75, 10, 1500, 75, 2050 }, // Hard
},
- // Level 9 (Stormtroopers)
+ // Sub-level 8: "6" (Stormtroopers)
{
- { 0, 0, 0, 0, 2, 20, 20, 0, 25 }, // Easy
- { 0, 0, 0, 0, 1, 25, 30, 0, 50 }, // Normal
- { 0, 0, 0, 0, 0, 28, 33, 0, 75 }, // Hard
+ { 0, 0, 0, 0, 2, 20, 20, 0, 25, 5, 500, 100, 2048 }, // Easy
+ { 0, 0, 0, 0, 1, 25, 30, 0, 50, 5, 1000, 200, 2048 }, // Normal
+ { 0, 0, 0, 0, 0, 28, 33, 0, 75, 10, 1500, 500, 2050 }, // Hard
},
- // Level 10 (Protect Rebel Transport)
+ // Sub-level 9: "7" (Protect Rebel Transport)
{
- { 100, 150, 150, 25, 7, 0, 12, 2, 50 }, // Easy
- { 100, 160, 200, 35, 4, 0, 30, 4, 100 }, // Normal
- { 100, 180, 250, 50, 3, 0, 33, 5, 100 }, // Hard
+ { 100, 150, 150, 25, 7, 0, 12, 2, 50, 5, 500, 100, 3072 }, // Easy
+ { 100, 160, 200, 35, 4, 0, 30, 4, 100, 5, 1000, 200, 3072 }, // Normal
+ { 100, 180, 250, 50, 3, 0, 33, 5, 100, 10, 1500, 500, 3074 }, // Hard
+ },
+ // Sub-level 10: "8" (Death Star surface)
+ {
+ { 0, 0, 0, 0, 0, 0, 30, 0, 25, 0, 1000, 100, 3074 }, // Easy
+ { 0, 0, 0, 0, 0, 0, 36, 0, 50, 0, 2000, 200, 3074 }, // Normal
+ { 0, 0, 0, 0, 0, 0, 39, 0, 75, 0, 3000, 500, 3074 }, // Hard
+ },
+ // Sub-level 11: "9A" (Death Star turrets part 1)
+ {
+ { 0, 0, 0, 0, 4, 0, 0, 15, 25, 0, 1000, 100, 3074 }, // Easy
+ { 0, 0, 0, 0, 2, 0, 0, 25, 50, 0, 2000, 200, 3078 }, // Normal
+ { 0, 0, 0, 0, 0, 0, 0, 30, 75, 0, 3000, 500, 3078 }, // Hard
+ },
+ // Sub-level 12: "9B" (Death Star turrets part 2)
+ {
+ { 0, 0, 0, 0, 0, 0, 0, 15, 25, 0, 1000, 100, 3098 }, // Easy
+ { 0, 0, 0, 0, 0, 0, 0, 25, 50, 0, 2000, 200, 3098 }, // Normal
+ { 0, 0, 0, 0, 0, 0, 0, 30, 75, 0, 3000, 500, 3098 }, // Hard
+ },
+ // Sub-level 13: "10" (Death Star trench approach)
+ {
+ { 0, 0, 0, 0, 3, 10, 0, 5, 25, 5, 500, 200, 2048 }, // Easy
+ { 0, 0, 0, 0, 1, 16, 0, 5, 50, 5, 1000, 400, 2048 }, // Normal
+ { 0, 0, 0, 0, 0, 18, 0, 7, 75, 10, 1500, 1000, 2050 }, // Hard
+ },
+ // Sub-level 14: "11" (Death Star trench - speeder)
+ {
+ { 70, 150, 150, 25, 12, 0, 30, 0, 50, 5, 500, 200, 3072 }, // Easy
+ { 72, 165, 155, 30, 7, 0, 36, 0, 50, 5, 1000, 400, 3072 }, // Normal
+ { 75, 170, 160, 33, 3, 0, 39, 0, 75, 10, 1500, 1000, 3074 }, // Hard
+ },
+ // Sub-level 15: "12" (Death Star trench run)
+ {
+ { 0, 0, 0, 0, 4, 13, 0, 5, 25, 5, 500, 100, 2048 }, // Easy
+ { 0, 0, 0, 0, 2, 20, 0, 5, 50, 5, 1000, 200, 2048 }, // Normal
+ { 0, 0, 0, 0, 0, 23, 0, 5, 75, 10, 1500, 500, 2050 }, // Hard
+ },
+ // Sub-level 16: "13" (Asteroid belt chase)
+ {
+ { 100, 16, 120, 0, 20, 0, 35, 8, 75, 5, 500, 100, 3072 }, // Easy
+ { 100, 18, 120, 0, 18, 0, 36, 10, 100, 5, 1000, 200, 3072 }, // Normal
+ { 100, 20, 150, 0, 15, 0, 39, 12, 200, 10, 1500, 500, 3074 }, // Hard
+ },
+ // Sub-level 17: "14A" (Star Destroyer attack 2 part 1)
+ {
+ { 0, 0, 0, 0, 0, 20, 35, 8, 25, 0, 1000, 100, 2048 }, // Easy
+ { 0, 0, 0, 0, 0, 27, 36, 12, 50, 0, 2000, 200, 2048 }, // Normal
+ { 0, 0, 0, 0, 0, 28, 39, 12, 75, 0, 3000, 500, 2050 }, // Hard
+ },
+ // Sub-level 18: "14B" (Star Destroyer attack 2 part 2)
+ {
+ { 0, 0, 0, 0, 10, 20, 35, 8, 25, 0, 1000, 100, 2048 }, // Easy
+ { 0, 0, 0, 0, 5, 25, 36, 10, 50, 0, 2000, 200, 2048 }, // Normal
+ { 0, 0, 0, 0, 4, 28, 39, 12, 75, 0, 3000, 500, 2050 }, // Hard
+ },
+ // Sub-level 19: "15A" (Death Star trench final part 1)
+ {
+ { 0, 0, 0, 0, 4, 0, 28, 3, 25, 5, 500, 100, 2048 }, // Easy
+ { 0, 0, 0, 0, 3, 0, 36, 3, 50, 5, 1000, 200, 2048 }, // Normal
+ { 0, 0, 0, 0, 3, 0, 39, 4, 75, 10, 1500, 500, 2050 }, // Hard
+ },
+ // Sub-level 20: "15B" (Death Star trench final part 2)
+ {
+ { 0, 0, 0, 0, 4, 10, 30, 3, 25, 5, 500, 100, 2048 }, // Easy
+ { 0, 0, 0, 0, 3, 20, 34, 3, 50, 5, 1000, 200, 2048 }, // Normal
+ { 0, 0, 0, 0, 2, 22, 35, 4, 75, 10, 1500, 500, 2050 }, // Hard
},
};
-static const int kNumTunedLevels = 10;
+static const int kNumTunedLevels = 21;
void InsaneRebel1::loadTuningForLevel(int level) {
int d = CLIP(_difficulty, 0, 2);
int l = CLIP(level, 0, kNumTunedLevels - 1);
- _tuning.roll = kTuningTable[l][d][0];
- _tuning.lift = kTuningTable[l][d][1];
- _tuning.slide = kTuningTable[l][d][2];
- _tuning.drift = kTuningTable[l][d][3];
- _tuning.snap = kTuningTable[l][d][4];
- _tuning.miss = kTuningTable[l][d][5];
- _tuning.wham = kTuningTable[l][d][6];
- _tuning.shot = kTuningTable[l][d][7];
- _tuning.kill = kTuningTable[l][d][8];
- debug(1, "RA1: Loaded tuning level=%d diff=%d: roll=%d lift=%d slide=%d snap=%d miss=%d wham=%d shot=%d kill=%d",
- level, d, _tuning.roll, _tuning.lift, _tuning.slide, _tuning.snap, _tuning.miss,
- _tuning.wham, _tuning.shot, _tuning.kill);
+ _tuning.roll = kTuningTable[l][d][0];
+ _tuning.lift = kTuningTable[l][d][1];
+ _tuning.slide = kTuningTable[l][d][2];
+ _tuning.drift = kTuningTable[l][d][3];
+ _tuning.snap = kTuningTable[l][d][4];
+ _tuning.miss = kTuningTable[l][d][5];
+ _tuning.wham = kTuningTable[l][d][6];
+ _tuning.shot = kTuningTable[l][d][7];
+ _tuning.kill = kTuningTable[l][d][8];
+ _tuning.time = kTuningTable[l][d][9];
+ _tuning.levelPts = kTuningTable[l][d][10];
+ _tuning.bonus = kTuningTable[l][d][11];
+ _tuning.flags = kTuningTable[l][d][12];
+ debug(1, "RA1: Loaded tuning level=%d diff=%d: roll=%d lift=%d slide=%d drift=%d snap=%d "
+ "miss=%d wham=%d shot=%d kill=%d time=%d levelPts=%d bonus=%d flags=0x%x",
+ level, d, _tuning.roll, _tuning.lift, _tuning.slide, _tuning.drift, _tuning.snap,
+ _tuning.miss, _tuning.wham, _tuning.shot, _tuning.kill,
+ _tuning.time, _tuning.levelPts, _tuning.bonus, _tuning.flags);
}
InsaneRebel1::InsaneRebel1(ScummEngine_v7 *scumm) : Insane(), _vm(scumm) {
diff --git a/engines/scumm/insane/insane_rebel1.h b/engines/scumm/insane/insane_rebel1.h
index eeb6f6bb4bf..9b36288eb1d 100644
--- a/engines/scumm/insane/insane_rebel1.h
+++ b/engines/scumm/insane/insane_rebel1.h
@@ -260,16 +260,22 @@ private:
int _difficulty;
// Per-difficulty tuning (from assault_data_3.bin, indexed: difficulty * 0x28B + level * 0x1F)
+ // Original game loads from C:\rebltune.txt; ScummVM uses hardcoded table.
+ // 21 sub-levels (1A,1B,2,3,4A,4B,5A,5B,6,7,8,9A,9B,10,11,12,13,14A,14B,15A,15B)
struct TuningParams {
- int16 roll; // 0x1B1B: horizontal speed/sensitivity
- int16 lift; // 0x1B1D: vertical speed/sensitivity
- int16 slide; // 0x1B1F: cross-axis coupling
- int16 drift; // 0x1B21: drift/turbulence multiplier
- int16 snap; // 0x1B23: hit radius for shooting targets
- int16 miss; // 0x1B25: obstacle collision damage (0x0B bit 0x40)
- int16 wham; // 0x1B27: light/wall damage
- int16 shot; // 0x1B29: heavy/projectile damage
- int16 kill; // 0x1B2B: score per target kill
+ int16 roll; // +0x05: horizontal speed/sensitivity
+ int16 lift; // +0x07: vertical speed/sensitivity
+ int16 slide; // +0x09: cross-axis coupling
+ int16 drift; // +0x0B: drift/turbulence multiplier
+ int16 snap; // +0x0D: hit radius for shooting targets
+ int16 miss; // +0x0F: obstacle collision damage (0x0B bit 0x40)
+ int16 wham; // +0x11: light/wall damage
+ int16 shot; // +0x13: heavy/projectile damage
+ int16 kill; // +0x15: score per target kill
+ int16 time; // +0x17: survival bonus (added every 32 frames)
+ int16 levelPts; // +0x19: chapter completion bonus (RunChapterCompleteSummaryScreen)
+ int16 bonus; // +0x1B: per-level bonus multiplier for kills/accuracy
+ int16 flags; // +0x1D: level behavior flags (loaded into DAT_75FE at level init)
};
TuningParams _tuning;
diff --git a/engines/scumm/insane/insane_rebel1_iact.cpp b/engines/scumm/insane/insane_rebel1_iact.cpp
index 611413d556a..9fd9b39f2b1 100644
--- a/engines/scumm/insane/insane_rebel1_iact.cpp
+++ b/engines/scumm/insane/insane_rebel1_iact.cpp
@@ -830,12 +830,12 @@ void InsaneRebel1::updateShipPhysics() {
if (_health < 0 && _deathTimer > 0)
_deathTimer--;
- // Health regeneration: +1 every 32 frames (from original asm)
+ // Health regeneration + survival bonus every 32 frames (UpdatePeriodicScoreAndHealth)
if ((_frameCounter & 0x1F) == 0) {
if (_health >= 0 && _health < kMaxHealth)
_health++;
if (_health >= 0)
- _score += 1;
+ _score += _tuning.time;
}
// Screen flash decay
@@ -993,12 +993,12 @@ void InsaneRebel1::updateTurretPhysics() {
if (shipBank->numSprites > 0)
_shipDirIndex = CLIP<int16>((int16)dir, 0, shipBank->numSprites - 1);
- // Regeneration via FUN_1BB0E call in this path.
+ // Regeneration + survival bonus via FUN_1BB0E call in this path.
if ((_frameCounter & 0x1F) == 0) {
if (_health >= 0 && _health < kMaxHealth)
_health++;
if (_health >= 0)
- _score += 1;
+ _score += _tuning.time;
}
if (_screenFlash > 0)
Commit: 0d84e60b0b78543e3dd1191fe49b20461e1e6fae
https://github.com/scummvm/scummvm/commit/0d84e60b0b78543e3dd1191fe49b20461e1e6fae
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:40+02:00
Commit Message:
SCUMM: RA1: Add chapter names in intro
Changed paths:
engines/scumm/insane/insane_rebel1.cpp
engines/scumm/insane/insane_rebel1.h
engines/scumm/insane/insane_rebel1_render.cpp
engines/scumm/insane/insane_rebel1_runlevels.cpp
diff --git a/engines/scumm/insane/insane_rebel1.cpp b/engines/scumm/insane/insane_rebel1.cpp
index b0bf2382788..d8b3d53264b 100644
--- a/engines/scumm/insane/insane_rebel1.cpp
+++ b/engines/scumm/insane/insane_rebel1.cpp
@@ -266,6 +266,10 @@ InsaneRebel1::InsaneRebel1(ScummEngine_v7 *scumm) : Insane(), _vm(scumm) {
_levelRouteChoice = 0;
_levelGameplayPhase = 0;
_menuActive = false;
+ _introTextActive = false;
+ _introTextStartFrame = 0;
+ _introTextEndFrame = 0;
+ _introTextLevel = 0;
_menuConfirmed = false;
_menuSelection = 0;
_menuFrameCounter = 0;
diff --git a/engines/scumm/insane/insane_rebel1.h b/engines/scumm/insane/insane_rebel1.h
index 9b36288eb1d..bf06bbf2ef2 100644
--- a/engines/scumm/insane/insane_rebel1.h
+++ b/engines/scumm/insane/insane_rebel1.h
@@ -243,6 +243,14 @@ private:
// Current level index (0-based: 0=LVL1, 1=LVL2, etc.)
int _currentLevel;
+ // Intro title overlay (RunTwoLineTextSplash from original)
+ bool _introTextActive;
+ int32 _introTextStartFrame; // revealBaseY: frame at which text begins appearing
+ int32 _introTextEndFrame; // stopY: frame after which text stops
+ int _introTextLevel; // index into kLevelTitles
+ void beginLevelTitleOverlay(int level);
+ void drawLevelTitleOverlay(byte *dst, int pitch, int width, int height, int32 curFrame, int32 maxFrame);
+
// Control mode (from GAME opcode 0x5E)
int16 _flyControlMode;
// Mode-2 emitter offsets used by FUN_1D79C when _DAT_75E4 == 2.
diff --git a/engines/scumm/insane/insane_rebel1_render.cpp b/engines/scumm/insane/insane_rebel1_render.cpp
index 6d669ca12e6..ffb050cd18a 100644
--- a/engines/scumm/insane/insane_rebel1_render.cpp
+++ b/engines/scumm/insane/insane_rebel1_render.cpp
@@ -350,6 +350,15 @@ void InsaneRebel1::procPostRendering(byte *renderBitmap, int32 codecparam, int32
renderMainMenuOverlay(renderBitmap, pitch, width, height);
}
+ // Intro title overlay (RunTwoLineTextSplash) â drawn during intro cinematics
+ if (_introTextActive && renderBitmap) {
+ int w = _player ? _player->_width : _screenWidth;
+ int h = _player ? _player->_height : _screenHeight;
+ if (w == 0) w = _screenWidth;
+ if (h == 0) h = _screenHeight;
+ drawLevelTitleOverlay(renderBitmap, w, w, h, curFrame, maxFrame);
+ }
+
if (!_interactiveVideoActive || !renderBitmap)
return;
@@ -665,6 +674,73 @@ void InsaneRebel1::renderLaserShots(byte *dst, int pitch, int width, int height)
}
}
+// Level intro title data (from RunTwoLineTextSplash calls in original binary).
+// titleText is drawn at y=10, subtitleText at y=25 (matching original DrawUiString positions).
+// revealStartFrame/revealEndFrame control the frame range during which text is visible.
+static const struct {
+ const char *titleText; // Top line (chapter number)
+ const char *subtitleText; // Bottom line (level name)
+ int16 revealStartFrame; // Frame at which text begins appearing
+ int16 revealEndFrame; // Frame after which text stops
+} kLevelTitles[] = {
+ { "Chapter 1", "Flight Training", -80, -30 }, // Original computes totalFrames-0x50..totalFrames-0x1e
+ { "Chapter 2", "Asteroid Field Training", 40, 100 },
+ { "Chapter 3", "Planet Kolaador", 0, 50 },
+ { "Chapter 4", "Star Destroyer Attack", 5, 50 },
+ { "Chapter 5", "Tatooine Attack", 346, 390 },
+ { "Chapter 6", "Asteroid Field Chase", 30, 80 },
+ { "Chapter 7", "Imperial Probe Droids", 166, 230 },
+ { "Chapter 8", "Imperial Walkers", 30, 75 },
+ { "Chapter 9", "Stormtroopers", 2, 45 },
+ { "Chapter 10", "Protect Rebel Transport", 10, 75 },
+ { "Chapter 11", "Yavin Training", 1, 45 },
+ { "Chapter 12", "Tie Attack", 15, 70 },
+ { "Chapter 13", "Death Star Surface", 10, 60 },
+ { "Chapter 14", "Surface Cannon", 122, 175 },
+ { "Chapter 15", "Death Star Trench", 1, 49 },
+};
+
+void InsaneRebel1::beginLevelTitleOverlay(int level) {
+ if (level < 0 || level >= ARRAYSIZE(kLevelTitles)) {
+ _introTextActive = false;
+ return;
+ }
+ _introTextActive = true;
+ _introTextLevel = level;
+ _introTextStartFrame = kLevelTitles[level].revealStartFrame;
+ _introTextEndFrame = kLevelTitles[level].revealEndFrame;
+}
+
+void InsaneRebel1::drawLevelTitleOverlay(byte *dst, int pitch, int width, int height, int32 curFrame, int32 maxFrame) {
+ if (!_introTextActive || _introTextLevel < 0 || _introTextLevel >= ARRAYSIZE(kLevelTitles))
+ return;
+
+ // Resolve negative frame values (relative to end of video, e.g. Chapter 1)
+ if (_introTextStartFrame < 0)
+ _introTextStartFrame = maxFrame + _introTextStartFrame;
+ if (_introTextEndFrame < 0)
+ _introTextEndFrame = maxFrame + _introTextEndFrame;
+
+ // Only draw within the frame window
+ if (curFrame < _introTextStartFrame || curFrame >= _introTextEndFrame) {
+ if (curFrame >= _introTextEndFrame)
+ _introTextActive = false;
+ return;
+ }
+
+ const char *title = kLevelTitles[_introTextLevel].titleText;
+ const char *subtitle = kLevelTitles[_introTextLevel].subtitleText;
+
+ // Center horizontally (matching original DrawUiString x_center=0xA0=160)
+ int titleW = getFontBankStringWidth(title);
+ int subtitleW = getFontBankStringWidth(subtitle);
+ int centerX = width / 2;
+
+ // Original positions: title at y=10, subtitle at y=25 (0x19)
+ drawFontBankString(dst, pitch, width, height, centerX - titleW / 2, 10, title);
+ drawFontBankString(dst, pitch, width, height, centerX - subtitleW / 2, 25, subtitle);
+}
+
// drawFontBankString â FUN_221B7 (0x221B7), partial parity:
// supports '<'/'>' layer markup and layer-2 space handling used by RA1 HUD/targeting strings.
void InsaneRebel1::drawFontBankString(byte *dst, int pitch, int width, int height, int x, int y, const char *text) {
diff --git a/engines/scumm/insane/insane_rebel1_runlevels.cpp b/engines/scumm/insane/insane_rebel1_runlevels.cpp
index 5264bf83624..420895d56d0 100644
--- a/engines/scumm/insane/insane_rebel1_runlevels.cpp
+++ b/engines/scumm/insane/insane_rebel1_runlevels.cpp
@@ -110,6 +110,7 @@ bool InsaneRebel1::runLevel1() {
loadTuningForLevel(0);
loadLevelSprites(1);
+ beginLevelTitleOverlay(0);
playCinematic("LVL1/L1HANGAR.ANM");
if (_vm->shouldQuit())
return false;
@@ -226,6 +227,7 @@ bool InsaneRebel1::runLevel2() {
// DOS RunLevel2Flow launches L2PLAY.ANM with gameplay selector 2.
loadTuningForLevel(2);
+ beginLevelTitleOverlay(1);
playCinematic("LVL2/L2INTRO.ANM");
if (_vm->shouldQuit())
return false;
@@ -283,6 +285,7 @@ bool InsaneRebel1::runLevel3() {
// DOS RunLevel3Flow launches L3PLAY.ANM with gameplay selector 3.
loadTuningForLevel(3);
+ beginLevelTitleOverlay(2);
playCinematic("LVL3/L3INTRO.ANM");
if (_vm->shouldQuit())
return false;
@@ -342,6 +345,7 @@ bool InsaneRebel1::runLevel4() {
// DOS RunLevel4Flow launches L4PLAY1/2.ANM with gameplay selector 4.
loadTuningForLevel(4);
+ beginLevelTitleOverlay(3);
playCinematic("LVL4/L4INTRO.ANM");
if (_vm->shouldQuit())
return false;
@@ -407,6 +411,7 @@ bool InsaneRebel1::runLevel5() {
loadLevelSprites(5);
loadTuningForLevel(4);
+ beginLevelTitleOverlay(4);
playCinematic("LVL5/L5INTRO.ANM");
if (_vm->shouldQuit())
return false;
@@ -505,6 +510,7 @@ bool InsaneRebel1::runLevel6() {
loadLevelSprites(6);
loadTuningForLevel(5);
+ beginLevelTitleOverlay(5);
playCinematic("LVL6/L6INTRO.ANM");
if (_vm->shouldQuit())
return false;
@@ -572,6 +578,7 @@ bool InsaneRebel1::runLevel7() {
loadLevelSprites(7);
loadTuningForLevel(6);
+ beginLevelTitleOverlay(6);
playCinematic("LVL7/L7INTRO.ANM");
if (_vm->shouldQuit())
return false;
@@ -664,6 +671,7 @@ bool InsaneRebel1::runLevel8() {
loadLevelSprites(8);
loadTuningForLevel(7);
+ beginLevelTitleOverlay(7);
playCinematic("LVL8/L8INTRO.ANM");
if (_vm->shouldQuit())
return false;
@@ -754,6 +762,7 @@ bool InsaneRebel1::runLevel9() {
loadLevelSprites(9);
loadTuningForLevel(8);
+ beginLevelTitleOverlay(8);
playCinematic("LVL9/L9INTRO.ANM");
if (_vm->shouldQuit())
return false;
@@ -931,6 +940,7 @@ bool InsaneRebel1::runLevel10() {
loadLevelSprites(10);
loadTuningForLevel(9);
+ beginLevelTitleOverlay(9);
playCinematic("LVL10/L10INTRO.ANM");
if (_vm->shouldQuit())
return false;
Commit: cf25b976b06f998d527e3fbc5c29b4e82a99689c
https://github.com/scummvm/scummvm/commit/cf25b976b06f998d527e3fbc5c29b4e82a99689c
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:40+02:00
Commit Message:
SCUMM: RA1: Improve level selection menu navigation
Changed paths:
engines/scumm/insane/insane_rebel1.h
engines/scumm/insane/insane_rebel1_menu.cpp
engines/scumm/insane/insane_rebel1_runlevels.cpp
diff --git a/engines/scumm/insane/insane_rebel1.h b/engines/scumm/insane/insane_rebel1.h
index bf06bbf2ef2..8c81e8ed0a3 100644
--- a/engines/scumm/insane/insane_rebel1.h
+++ b/engines/scumm/insane/insane_rebel1.h
@@ -108,7 +108,7 @@ private:
// Main menu loop on O1OPTION.ANM background (0x15968)
// Returns: 1=Start New Game, 2=Game Options, 3=Level Select, 4=Continue Demo, 5=Exit
int runMainMenu();
- void runLevelSelectMenu();
+ int runLevelSelectMenu();
// Level 1 flow (0x16100): hangar â CU1 â gameplay â CU2 â turret â end
// Returns true if level completed, false if player quit
diff --git a/engines/scumm/insane/insane_rebel1_menu.cpp b/engines/scumm/insane/insane_rebel1_menu.cpp
index 59ca5357f74..2ab05d280dd 100644
--- a/engines/scumm/insane/insane_rebel1_menu.cpp
+++ b/engines/scumm/insane/insane_rebel1_menu.cpp
@@ -29,19 +29,34 @@
namespace Scumm {
-static const int kRA1LevelSelectItemCount = 11;
-static const int kRA1LastImplementedLevel = 10;
+static const int kRA1LevelSelectItemCount = 16; // 15 levels + BACK
+static const int kRA1LevelSelectRowsPerCol = 8;
+static const int kRA1NumLevels = 15;
bool InsaneRebel1::notifyEvent(const Common::Event &event) {
if (_menuActive && _levelSelectActive && event.type == Common::EVENT_KEYDOWN) {
+ int col = _levelSelectSel / kRA1LevelSelectRowsPerCol;
+ int row = _levelSelectSel % kRA1LevelSelectRowsPerCol;
switch (event.kbd.keycode) {
case Common::KEYCODE_UP:
case Common::KEYCODE_w:
- _levelSelectSel = (_levelSelectSel + kRA1LevelSelectItemCount - 1) % kRA1LevelSelectItemCount;
+ row = (row + kRA1LevelSelectRowsPerCol - 1) % kRA1LevelSelectRowsPerCol;
+ _levelSelectSel = col * kRA1LevelSelectRowsPerCol + row;
return true;
case Common::KEYCODE_DOWN:
case Common::KEYCODE_s:
- _levelSelectSel = (_levelSelectSel + 1) % kRA1LevelSelectItemCount;
+ row = (row + 1) % kRA1LevelSelectRowsPerCol;
+ _levelSelectSel = col * kRA1LevelSelectRowsPerCol + row;
+ return true;
+ case Common::KEYCODE_LEFT:
+ case Common::KEYCODE_a:
+ if (col > 0)
+ _levelSelectSel -= kRA1LevelSelectRowsPerCol;
+ return true;
+ case Common::KEYCODE_RIGHT:
+ case Common::KEYCODE_d:
+ if (col < 1)
+ _levelSelectSel += kRA1LevelSelectRowsPerCol;
return true;
case Common::KEYCODE_RETURN:
case Common::KEYCODE_KP_ENTER:
@@ -208,36 +223,42 @@ void InsaneRebel1::renderMainMenuOverlay(byte *dst, int pitch, int width, int he
}
if (_levelSelectActive) {
- // --- Level select submenu ---
+ // --- Level select submenu (two-column layout) ---
const int titleW = getTalkTextWidth("LEVEL SELECT");
- drawTalkText((width - titleW) / 2, 36, "LEVEL SELECT");
-
- const char *kLevelItems[kRA1LevelSelectItemCount] = {
- "LEVEL 1: FLIGHT TRAINING",
- "LEVEL 2: ASTEROID FIELD",
- "LEVEL 3: PLANET KOLAADOR",
- "LEVEL 4: STAR DESTROYER",
- "LEVEL 5: TATOOINE ATTACK",
- "LEVEL 6: ASTEROID CHASE",
- "LEVEL 7: PROBE DROIDS",
- "LEVEL 8: IMPERIAL WALKERS",
- "LEVEL 9: STORMTROOPERS",
- "LEVEL 10: REBEL TRANSPORT",
+ drawTalkText((width - titleW) / 2, 30, "LEVEL SELECT");
+
+ static const char *kLevelItems[kRA1LevelSelectItemCount] = {
+ " 1 FLIGHT TRAINING",
+ " 2 ASTEROID FIELD",
+ " 3 PLANET KOLAADOR",
+ " 4 STAR DESTROYER",
+ " 5 TATOOINE ATTACK",
+ " 6 ASTEROID CHASE",
+ " 7 PROBE DROIDS",
+ " 8 IMPERIAL WALKERS",
+ " 9 STORMTROOPERS",
+ "10 REBEL TRANSPORT",
+ "11 YAVIN TRAINING",
+ "12 TIE ATTACK",
+ "13 DEATH STAR SURFACE",
+ "14 SURFACE CANNON",
+ "15 DEATH STAR TRENCH",
"BACK"
};
- const int menuY = 60;
- const int rowH = 16;
+ const int menuY = 50;
+ const int rowH = 14;
+ const int leftColX = 8;
+ const int rightColX = width / 2 + 4;
for (int i = 0; i < kRA1LevelSelectItemCount; i++) {
+ const int col = i / kRA1LevelSelectRowsPerCol;
+ const int row = i % kRA1LevelSelectRowsPerCol;
+ const int textX = (col == 0) ? leftColX : rightColX;
+ const int y = menuY + row * rowH;
const int textW = getTalkTextWidth(kLevelItems[i]);
- const int textX = (width - textW) / 2;
- const int y = menuY + i * rowH;
- drawTalkText(textX, y + 1, kLevelItems[i]);
- if (i == _startLevel - 1) {
- drawTalkText(textX - 12, y + 1, ">");
- }
+ drawTalkText(textX, y + 1, kLevelItems[i]);
if (i == _levelSelectSel) {
byte highlightColor = ((_menuFrameCounter / 8) & 1) ? 248 : 240;
@@ -377,8 +398,8 @@ void InsaneRebel1::runOptionsMenu() {
_optionsActive = false;
}
-void InsaneRebel1::runLevelSelectMenu() {
- _levelSelectSel = CLIP(_startLevel - 1, 0, kRA1LastImplementedLevel - 1);
+int InsaneRebel1::runLevelSelectMenu() {
+ _levelSelectSel = CLIP(_startLevel - 1, 0, kRA1NumLevels - 1);
_levelSelectActive = true;
while (!_vm->shouldQuit()) {
@@ -393,17 +414,17 @@ void InsaneRebel1::runLevelSelectMenu() {
break;
if (_menuConfirmed) {
- if (_levelSelectSel < kRA1LastImplementedLevel) {
- _startLevel = _levelSelectSel + 1;
+ if (_levelSelectSel < kRA1NumLevels) {
_levelSelectActive = false;
- return;
+ return _levelSelectSel + 1; // 1-based level number
}
-
+ // BACK
_levelSelectActive = false;
- return;
+ return 0;
}
}
_levelSelectActive = false;
+ return 0;
}
} // End of namespace Scumm
diff --git a/engines/scumm/insane/insane_rebel1_runlevels.cpp b/engines/scumm/insane/insane_rebel1_runlevels.cpp
index 420895d56d0..ab15110e0dc 100644
--- a/engines/scumm/insane/insane_rebel1_runlevels.cpp
+++ b/engines/scumm/insane/insane_rebel1_runlevels.cpp
@@ -1049,10 +1049,16 @@ void InsaneRebel1::runGame() {
// Game Options
runOptionsMenu();
break;
- case 3:
- // Level Select
- runLevelSelectMenu();
+ case 3: {
+ // Level Select â launch directly
+ int selectedLevel = runLevelSelectMenu();
+ if (selectedLevel >= 1 && selectedLevel <= (int)(sizeof(kLevelRunners) / sizeof(kLevelRunners[0]))) {
+ _startLevel = selectedLevel;
+ (this->*kLevelRunners[selectedLevel - 1])();
+ _currentLevel = 0;
+ }
break;
+ }
case 5:
// Exit
return;
Commit: 595f2a1fcb515ba2855919378399bf774c858736
https://github.com/scummvm/scummvm/commit/595f2a1fcb515ba2855919378399bf774c858736
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:40+02:00
Commit Message:
SCUMM: RA1: Improve level 9 rendering
Changed paths:
engines/scumm/insane/insane_rebel1.cpp
engines/scumm/insane/insane_rebel1.h
engines/scumm/insane/insane_rebel1_iact.cpp
engines/scumm/insane/insane_rebel1_levels.cpp
engines/scumm/insane/insane_rebel1_render.cpp
engines/scumm/insane/insane_rebel1_runlevels.cpp
engines/scumm/smush/codec1.cpp
engines/scumm/smush/smush_player.cpp
engines/scumm/smush/smush_player_ra1.cpp
diff --git a/engines/scumm/insane/insane_rebel1.cpp b/engines/scumm/insane/insane_rebel1.cpp
index d8b3d53264b..a31b45bdcc3 100644
--- a/engines/scumm/insane/insane_rebel1.cpp
+++ b/engines/scumm/insane/insane_rebel1.cpp
@@ -218,6 +218,10 @@ InsaneRebel1::InsaneRebel1(ScummEngine_v7 *scumm) : Insane(), _vm(scumm) {
_perspectiveX = 0;
_perspectiveY = 0;
_projectionCurveExtent = 1;
+ _onFootCharX = 0;
+ _onFootCharY = 0;
+ _onFootAnimCounter = 0;
+ _onFootInitialized = false;
memset(_projectionTable, 0, sizeof(_projectionTable));
memset(_inputHistoryX, 0, sizeof(_inputHistoryX));
diff --git a/engines/scumm/insane/insane_rebel1.h b/engines/scumm/insane/insane_rebel1.h
index 8c81e8ed0a3..e4fde7cbff6 100644
--- a/engines/scumm/insane/insane_rebel1.h
+++ b/engines/scumm/insane/insane_rebel1.h
@@ -240,6 +240,13 @@ private:
// 0x0B handler physics update (asteroid/surface levels)
void updateAsteroidPhysics();
+ // 0x19/0x1A on-foot handler (Level 9 Stormtroopers)
+ void updateOnFootPhysics();
+ int16 _onFootCharX; // Character draw X (g_shipOffsetX in original)
+ int16 _onFootCharY; // Character draw Y (g_shipOffsetY in original)
+ int16 _onFootAnimCounter; // DAT_0000828a: fire animation counter
+ bool _onFootInitialized; // First-frame init flag for 0x19
+
// Current level index (0-based: 0=LVL1, 1=LVL2, etc.)
int _currentLevel;
diff --git a/engines/scumm/insane/insane_rebel1_iact.cpp b/engines/scumm/insane/insane_rebel1_iact.cpp
index 9fd9b39f2b1..9f3c4492a65 100644
--- a/engines/scumm/insane/insane_rebel1_iact.cpp
+++ b/engines/scumm/insane/insane_rebel1_iact.cpp
@@ -791,7 +791,8 @@ void InsaneRebel1::updateShipPhysics() {
// (_74CE + 0x20) * 5 / 0x41 â 0..4 (5 rows)
int vComponent = (_liftSmooth + 0x20) * 5 / 0x41;
- _shipDirIndex = CLIP<int16>((int16)(vComponent + hComponent), 0, _shipBank.numSprites - 1);
+ if (_shipBank.numSprites > 0)
+ _shipDirIndex = CLIP<int16>((int16)(vComponent + hComponent), 0, _shipBank.numSprites - 1);
// --- Step 10: Damage/event bit synthesis + damage processing ---
// RA1 FUN_1B297-style latches from GAME opcodes:
@@ -1152,6 +1153,120 @@ void InsaneRebel1::updateAsteroidPhysics() {
}
+// updateOnFootPhysics â HandleGameOp19_OnFootSequence (0x19) + HandleGameOp1A_OnFootVariant (0x1A).
+// On-foot handler for Level 9 (Stormtroopers). Character walks left/right, crosshair tracks mouse.
+//
+// Original has TWO separate variable pairs:
+// DAT_000041a0/41a2 = camera offset (SetCameraOffset, ProjectPointToScreen)
+// g_perspectiveX/Y = crosshair center (on-foot targeting)
+// Our _perspectiveX/_perspectiveY maps to the camera offset (DAT_000041a0/41a2).
+// The crosshair center (0xA3, 0x82) is a separate constant for on-foot mode.
+static const int16 kOnFootCenterX = 0xA3; // g_perspectiveX in HandleGameOp19
+static const int16 kOnFootCenterY = 0x82; // g_perspectiveY in HandleGameOp19
+
+void InsaneRebel1::updateOnFootPhysics() {
+ // --- First-frame initialization (0x19 counter==0) ---
+ if (!_onFootInitialized) {
+ _onFootInitialized = true;
+ _shipDirIndex = 15; // Center facing
+ _onFootAnimCounter = 0;
+ _onFootCharX = 0;
+ _onFootCharY = 0;
+ _damageFlags = 0;
+ _prevDamageFlags = 0;
+ _damageCooldown = 0;
+
+ // SetCameraOffset(0,0) â no viewport crop for on-foot levels
+ _perspectiveX = 0;
+ _perspectiveY = 0;
+ resetProjectionTable();
+ }
+
+ // --- 0x19: Character walk animation + damage ---
+ // Track fire button for animation
+ if (!_playerFired)
+ _onFootAnimCounter = 0;
+ else
+ _onFootAnimCounter++;
+
+ // Walk direction state machine (from HandleGameOp19)
+ if (_shipDirIndex == 0) {
+ // Left edge: snap to center, step character left
+ _shipDirIndex = 15;
+ _onFootCharX -= 0x3A;
+ } else if (_shipDirIndex < 5) {
+ _shipDirIndex--;
+ } else if (_shipDirIndex < 10) {
+ _shipDirIndex++;
+ } else if (_shipDirIndex == 10) {
+ // Right edge: snap to center, step character right
+ _shipDirIndex = 15;
+ _onFootCharX += 0x3A;
+ } else if (_onFootAnimCounter < 5 && !_playerFired) {
+ // Aim direction from crosshair toward character (QuantizeDirection8Way)
+ int16 dx = _shipPosX - (_onFootCharX + kOnFootCenterX);
+ int16 aimDir = 0;
+ if (dx > 30)
+ aimDir = 4;
+ else if (dx > 10)
+ aimDir = 2;
+ else if (dx < -30)
+ aimDir = -4;
+ else if (dx < -10)
+ aimDir = -2;
+ _shipDirIndex = CLIP<int16>(aimDir + 15, 11, 19);
+ } else {
+ // Walking based on mouse input direction
+ int16 inputX = 0, inputY = 0;
+ preprocessMouseAxes(inputX, inputY);
+ if (inputX > 0x1E && _onFootCharX < 0x72)
+ _shipDirIndex = 6; // Walk right
+ else if (inputX < -0x1E && _onFootCharX > -0x72)
+ _shipDirIndex = 4; // Walk left
+ }
+
+ // --- 0x1A: Crosshair positioning (HandleGameOp1A_OnFootVariant) ---
+ // shipPosX/Y = mouse_input + crosshair_center + character_offset
+ int16 inputX = 0, inputY = 0;
+ preprocessMouseAxes(inputX, inputY);
+ inputX = CLIP<int16>(inputX, -100, 100);
+ int16 inputYNeg = CLIP<int16>((int16)(-inputY), -0x4B, 0x0F);
+ _shipPosX = inputX + kOnFootCenterX + _onFootCharX;
+ _shipPosY = inputYNeg + kOnFootCenterY + _onFootCharY - 0x32;
+
+ // --- Damage handling (from 0x19) ---
+ if (_damageFlags != 0 && _damageCooldown == 0 && _health >= 0 && _deathTimer < 1) {
+ _health -= _tuning.miss; // On-foot uses DAT_00001b29 offset = miss tuning
+ if (_health < 0)
+ _deathTimer = 15;
+ _prevDamageFlags = _damageFlags;
+ _damageCooldown = 3;
+ _screenFlash = 5;
+ }
+
+ if (_damageCooldown > 0)
+ _damageCooldown--;
+
+ // Health regen + survival bonus (UpdatePeriodicScoreAndHealth)
+ if ((_frameCounter & 0x1F) == 0) {
+ if (_health >= 0 && _health < kMaxHealth)
+ _health++;
+ if (_health >= 0)
+ _score += _tuning.time;
+ }
+
+ _gameLatch5D = 0;
+ _gameLatch5F = 0;
+
+ if (_deathTimer > 1 && _health < 0)
+ _deathTimer--;
+ if (_screenFlash > 0)
+ _screenFlash--;
+
+ _damageFlags = 0;
+ _frameCounter++;
+}
+
void InsaneRebel1::procIACT(byte *renderBitmap, int32 codecparam, int32 setupsan12,
int32 setupsan13, Common::SeekableReadStream &b, int32 size, int32 flags,
int16 par1, int16 par2, int16 par3, int16 par4) {
diff --git a/engines/scumm/insane/insane_rebel1_levels.cpp b/engines/scumm/insane/insane_rebel1_levels.cpp
index 1defb3d8cae..c51577b751c 100644
--- a/engines/scumm/insane/insane_rebel1_levels.cpp
+++ b/engines/scumm/insane/insane_rebel1_levels.cpp
@@ -205,12 +205,15 @@ bool InsaneRebel1::loadRA1Nut(const char *filename, RA1SpriteBank &bank) {
}
void InsaneRebel1::loadLevelSprites(int level) {
- // Ship direction bank â not all levels have one (e.g. Level 2 is first-person)
+ // Ship/character direction bank â try BANK1, BANK, then PILOT (Level 9 on-foot)
Common::String bankFile = Common::String::format("LVL%d/L%dBANK1.NUT", level, level);
if (!loadRA1Nut(bankFile.c_str(), _shipBank)) {
Common::String legacyBankFile = Common::String::format("LVL%d/L%dBANK.NUT", level, level);
- if (!loadRA1Nut(legacyBankFile.c_str(), _shipBank))
- debug(1, "InsaneRebel1: No BANK1/BANK for level %d", level);
+ if (!loadRA1Nut(legacyBankFile.c_str(), _shipBank)) {
+ Common::String pilotFile = Common::String::format("LVL%d/L%dPILOT.NUT", level, level);
+ if (!loadRA1Nut(pilotFile.c_str(), _shipBank))
+ debug(1, "InsaneRebel1: No BANK1/BANK/PILOT for level %d", level);
+ }
}
// Secondary ship bank used by some level-specific handlers (e.g. LVL1 mode-2).
diff --git a/engines/scumm/insane/insane_rebel1_render.cpp b/engines/scumm/insane/insane_rebel1_render.cpp
index ffb050cd18a..ab5a2c6f5b4 100644
--- a/engines/scumm/insane/insane_rebel1_render.cpp
+++ b/engines/scumm/insane/insane_rebel1_render.cpp
@@ -371,21 +371,40 @@ void InsaneRebel1::procPostRendering(byte *renderBitmap, int32 codecparam, int32
const bool haveFrameGameOpcodes = (_frameGameOpcodeMask != 0);
const bool asteroidMode = hasFrameGameOpcode(0x0B) ||
(!haveFrameGameOpcodes && _activeGameOpcode == 0x0B);
+ const bool onFootMode = hasFrameGameOpcode(0x19) || hasFrameGameOpcode(0x1A) ||
+ (!haveFrameGameOpcodes &&
+ (_activeGameOpcode == 0x19 || _activeGameOpcode == 0x1A));
if (asteroidMode) {
// First-person asteroid/surface handler â opcode 0x0B (FUN_1CDA7).
updateAsteroidPhysics();
+ } else if (onFootMode) {
+ // On-foot handler â opcodes 0x19/0x1A (Level 9 Stormtroopers)
+ updateOnFootPhysics();
+
+ if (_health < 0 && _deathTimer < 2) {
+ _fireCooldown = _playerFired ? 1 : 0;
+ _vm->_smushVideoShouldFinish = true;
+ return;
+ }
+
+ // Draw character sprite at on-foot position (DrawFobjGlyph with flag 0x80)
+ if (_shipBank.numSprites > 0 && _shipDirIndex >= 0 &&
+ _shipDirIndex < _shipBank.numSprites) {
+ const RA1Sprite &spr = _shipBank.sprites[_shipDirIndex];
+ int drawX = _onFootCharX + spr.xoffs;
+ int drawY = _onFootCharY + spr.yoffs;
+ renderSprite(renderBitmap, pitch, width, height, drawX, drawY, spr);
+ }
} else {
const bool turretMode = hasFrameGameOpcode(0x08) || hasFrameGameOpcode(0x0A) ||
(!haveFrameGameOpcodes && (_activeGameOpcode == 0x08 || _activeGameOpcode == 0x0A));
const bool flightMode = hasFrameGameOpcode(0x07) || hasFrameGameOpcode(0x09) ||
- hasFrameGameOpcode(0x19) || hasFrameGameOpcode(0x1A) ||
(!haveFrameGameOpcodes &&
- (_activeGameOpcode == 0x07 || _activeGameOpcode == 0x09 ||
- _activeGameOpcode == 0x19 || _activeGameOpcode == 0x1A));
+ (_activeGameOpcode == 0x07 || _activeGameOpcode == 0x09));
// Dispatch movement path by GAME handler family:
// 0x08/0x0A -> FUN_1E6A7/FUN_1D79C (turret/cockpit)
- // 0x07/0x09/0x19/0x1A -> flight-family handlers
+ // 0x07/0x09 -> flight-family handlers
if (turretMode) {
updateTurretPhysics();
} else if (flightMode) {
@@ -394,9 +413,8 @@ void InsaneRebel1::procPostRendering(byte *renderBitmap, int32 codecparam, int32
// Most RA1 gameplay loops exit the current interactive movie as soon as
// health drops below 0, then dispatch to the level-specific retry/death
- // clip. LVL9's on-foot flow is the main exception: it keeps pumping while
- // deathTimer > 1 before branching out of the current segment.
- if (_health < 0 && _currentLevel != 8) {
+ // clip.
+ if (_health < 0) {
_fireCooldown = _playerFired ? 1 : 0;
_vm->_smushVideoShouldFinish = true;
return;
@@ -410,9 +428,15 @@ void InsaneRebel1::procPostRendering(byte *renderBitmap, int32 codecparam, int32
// GAME handlers in the original update FUN_224FD during the same frame that
// the new control state is computed. Sync the current frame's viewport window
// before HUD/screen copy so 0x0B doesn't lag one frame behind the mouse.
+ // On-foot mode uses SetCameraOffset(0,0) â no viewport crop.
if (_player) {
- _player->_ra1ViewportOffsetX = _perspectiveX;
- _player->_ra1ViewportOffsetY = _perspectiveY;
+ if (onFootMode) {
+ _player->_ra1ViewportOffsetX = 0;
+ _player->_ra1ViewportOffsetY = 0;
+ } else {
+ _player->_ra1ViewportOffsetX = _perspectiveX;
+ _player->_ra1ViewportOffsetY = _perspectiveY;
+ }
}
// Assembly dispatch (FUN_1BE1B) only runs the targeting/shot overlay pipeline
diff --git a/engines/scumm/insane/insane_rebel1_runlevels.cpp b/engines/scumm/insane/insane_rebel1_runlevels.cpp
index ab15110e0dc..7b05e2f0814 100644
--- a/engines/scumm/insane/insane_rebel1_runlevels.cpp
+++ b/engines/scumm/insane/insane_rebel1_runlevels.cpp
@@ -787,7 +787,7 @@ bool InsaneRebel1::runLevel9() {
_lastHitTarget = 0;
_shipPosX = kRA1CenterX;
_shipPosY = kRA1CenterY;
- _shipDirIndex = 17;
+ _shipDirIndex = 15; // On-foot center direction
_rollAccum = 0;
_liftSmooth = 0;
_posAccumX = 0;
@@ -795,6 +795,10 @@ bool InsaneRebel1::runLevel9() {
_perspectiveX = 0;
_perspectiveY = 0;
_levelGameplayPhase = 0;
+ _onFootCharX = 0;
+ _onFootCharY = 0;
+ _onFootAnimCounter = 0;
+ _onFootInitialized = false;
memset(_inputHistoryX, 0, sizeof(_inputHistoryX));
memset(_inputHistoryY, 0, sizeof(_inputHistoryY));
memset(_viewHistoryX, 0, sizeof(_viewHistoryX));
@@ -821,6 +825,7 @@ bool InsaneRebel1::runLevel9() {
if (_health < 0)
break;
+ _gameplayFlags75fe |= 4;
const int side1 = (_shipPosX < kRA1CenterX) ? 0 : 1;
playCinematic(side1 == 0 ? "LVL9/L9PLAY2A.ANM" : "LVL9/L9PLAY2B.ANM");
if (_vm->shouldQuit())
@@ -863,6 +868,7 @@ bool InsaneRebel1::runLevel9() {
if (_health < 0)
break;
+ _gameplayFlags75fe |= 4;
const int side2 = (_shipPosX < kRA1CenterX) ? 0 : 1;
playCinematic(side2 == 0 ? "LVL9/L9PLAY4A.ANM" : "LVL9/L9PLAY4B.ANM");
if (_vm->shouldQuit())
@@ -891,6 +897,7 @@ bool InsaneRebel1::runLevel9() {
if (_health < 0)
break;
+ _gameplayFlags75fe |= 4;
const int side3 = (_shipPosX < kRA1CenterX) ? 0 : 1;
if (side3 == randPath3) {
playCinematic("LVL9/L9CUT6A.ANM");
diff --git a/engines/scumm/smush/codec1.cpp b/engines/scumm/smush/codec1.cpp
index 4ad3dd455db..7c6efb9bd61 100644
--- a/engines/scumm/smush/codec1.cpp
+++ b/engines/scumm/smush/codec1.cpp
@@ -36,6 +36,7 @@ void smushDecodeRLE(byte *dst, const byte *src, int left, int top, int width, in
} while (--height);
}
+
/**
* RA1 codec 1: RLE with transparency on pixel 0.
* Same BOMP encoding as smushDecodeRLE but pixel value 0 is not written,
diff --git a/engines/scumm/smush/smush_player.cpp b/engines/scumm/smush/smush_player.cpp
index e1301126370..f20a05d3357 100644
--- a/engines/scumm/smush/smush_player.cpp
+++ b/engines/scumm/smush/smush_player.cpp
@@ -879,6 +879,7 @@ byte *SmushPlayer::getVideoPalette() {
}
void smushDecodeRLE(byte *dst, const byte *src, int left, int top, int width, int height, int pitch);
+void smushDecodeRLEOpaque(byte *dst, const byte *src, int left, int top, int width, int height, int pitch);
void smushDecodeRA1Transparent(byte *dst, const byte *src, int left, int top, int width, int height, int pitch);
void smushDecodeUncompressed(byte *dst, const byte *src, int left, int top, int width, int height, int pitch);
void smushDecodeRA1Block(byte *dst, const byte *src, int left, int top, int width, int height, int pitch, int dataSize, uint8 param, uint16 parm2, int codec);
@@ -1201,8 +1202,13 @@ void SmushPlayer::decodeFrameObject(int codec, const uint8 *src, int left, int t
}
break;
case SMUSH_CODEC_RLE_ALT:
- // Codec 3: all pixels opaque (pixel 0 is written)
- smushDecodeRLE(_dst, adjustedSrc, left, top, width, height, pitch);
+ // Codec 3: all pixels opaque (pixel 0 is written).
+ // Original FUN_00010916 writes every byte including value 0.
+ if (isRA1()) {
+ smushDecodeRLEOpaque(_dst, adjustedSrc, left, top, width, height, pitch);
+ } else {
+ smushDecodeRLE(_dst, adjustedSrc, left, top, width, height, pitch);
+ }
break;
case SMUSH_CODEC_RA1_SCATTER:
// Codec 2: Scatter draw uses absolute buffer coords, not clipped FOBJ coords.
diff --git a/engines/scumm/smush/smush_player_ra1.cpp b/engines/scumm/smush/smush_player_ra1.cpp
index 1188ee84f3b..698c64e831e 100644
--- a/engines/scumm/smush/smush_player_ra1.cpp
+++ b/engines/scumm/smush/smush_player_ra1.cpp
@@ -128,7 +128,11 @@ bool SmushPlayerRebel1::handleGameFetch(int32 subSize, Common::SeekableReadStrea
if (_insane) {
InsaneRebel1 *rebel1 = static_cast<InsaneRebel1 *>(_insane);
if (rebel1->isInteractiveVideoActive()) {
- if (rebel1->getActiveGameOpcode() == 0x0B && _storedFobjWidth == _vm->_screenWidth) {
+ const uint16 gameOp = rebel1->getActiveGameOpcode();
+ // 0x0B (asteroid/surface) and 0x19/0x1A (on-foot) use SetCameraOffset
+ // directly â no projection-based FTCH placement.
+ if ((gameOp == 0x0B && _storedFobjWidth == _vm->_screenWidth) ||
+ gameOp == 0x19 || gameOp == 0x1A) {
left += _ra1ViewportOffsetX;
top += _ra1ViewportOffsetY;
} else {
Commit: 0235c711e158600470cb838f1ce2aa83b5a63268
https://github.com/scummvm/scummvm/commit/0235c711e158600470cb838f1ce2aa83b5a63268
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:41+02:00
Commit Message:
SCUMM: RA1: Improve on-foot level rendering
Changed paths:
engines/scumm/insane/insane_rebel1_iact.cpp
engines/scumm/insane/insane_rebel1_render.cpp
engines/scumm/insane/insane_rebel1_runlevels.cpp
diff --git a/engines/scumm/insane/insane_rebel1_iact.cpp b/engines/scumm/insane/insane_rebel1_iact.cpp
index 9f3c4492a65..2ac8f46b999 100644
--- a/engines/scumm/insane/insane_rebel1_iact.cpp
+++ b/engines/scumm/insane/insane_rebel1_iact.cpp
@@ -1234,9 +1234,18 @@ void InsaneRebel1::updateOnFootPhysics() {
_shipPosX = inputX + kOnFootCenterX + _onFootCharX;
_shipPosY = inputYNeg + kOnFootCenterY + _onFootCharY - 0x32;
- // --- Damage handling (from 0x19) ---
+ // --- Scripted damage latches â damageFlags (matching FUN_1B297 pattern) ---
+ // GAME 0x5D/0x5F set latches; convert to damage flags before the check.
+ if (_gameLatch5D == 0xFFFF)
+ _damageFlags |= 0x40;
+ if (_gameLatch5F != 0 &&
+ _vm->_rnd.getRandomNumber((uint16)(_gameLatch5F - 1)) == 0)
+ _damageFlags |= 0x80;
+
+ // --- Damage handling (from HandleGameOp19_OnFootSequence) ---
+ // On-foot uses single tuning value (DAT_00001b29 offset = miss) for all damage types.
if (_damageFlags != 0 && _damageCooldown == 0 && _health >= 0 && _deathTimer < 1) {
- _health -= _tuning.miss; // On-foot uses DAT_00001b29 offset = miss tuning
+ _health -= _tuning.miss;
if (_health < 0)
_deathTimer = 15;
_prevDamageFlags = _damageFlags;
@@ -1462,8 +1471,8 @@ void InsaneRebel1::handleGameChunk(int32 subSize, Common::SeekableReadStream &b)
break;
case 0x5A:
- // Target detection â FUN_1C0EF (0x1C0EF). AABB from video stream.
- // Params: targetIdx, left, top, width, height
+ // Target detection â HandleGameOp5A (0x1C0EF). AABB from video stream.
+ // Original checks event mask: if target already killed, skip to GOST update.
if (subSize >= 24) {
int16 targetIdx = (int16)param1;
int16 left = (int16)b.readUint32BE();
@@ -1472,7 +1481,25 @@ void InsaneRebel1::handleGameChunk(int32 subSize, Common::SeekableReadStream &b)
int16 h = (int16)b.readUint32BE();
int16 right = left + w;
int16 bottom = top + h;
- checkTargetHit(targetIdx, left, top, right, bottom);
+
+ if (targetIdx >= 0) {
+ const int byteIdx = targetIdx >> 3;
+ if (byteIdx >= 0 && byteIdx < 0x96 && byteIdx < kFrameObjectStateBytes) {
+ const byte bit = (byte)(0x80 >> (targetIdx & 7));
+ const int altIdx = byteIdx + 0x96;
+ const bool primarySet = (_frameObjectState[byteIdx] & bit) != 0;
+ const bool secondarySet = (altIdx < kFrameObjectStateBytes) &&
+ ((_frameObjectState[altIdx] & bit) != 0);
+
+ if (!primarySet || secondarySet) {
+ checkTargetHit(targetIdx, left, top, right, bottom);
+ } else {
+ updateGostSlotPosition(targetIdx, left, top, right, bottom);
+ }
+ } else {
+ checkTargetHit(targetIdx, left, top, right, bottom);
+ }
+ }
debug(5, "RA1 GAME 0x5A: target=%d rect=[%d,%d]-[%d,%d] prox=%d",
targetIdx, left, top, right, bottom, _targetProximity);
}
@@ -1507,6 +1534,14 @@ void InsaneRebel1::processShot() {
if (!_playerFired || _fireCooldown != 0)
return;
+ // On-foot mode: only spawn when in aiming stance (dirIndex 11-19) or flags force it.
+ // Original: if (((10 < g_shipDirIndex) && (g_shipDirIndex < 0x14)) || ((DAT_000075fe & 8) != 0))
+ const bool onFootMode = (_activeGameOpcode == 0x19 || _activeGameOpcode == 0x1A);
+ if (onFootMode) {
+ if (!(_shipDirIndex > 10 && _shipDirIndex < 20) && !(_gameplayFlags75fe & 8))
+ return;
+ }
+
// Find first available slot (timer < 1 or > 5), matching FUN_1CCA0.
int slot = -1;
for (int i = 0; i < kMaxShotSlots; i++) {
@@ -1519,21 +1554,34 @@ void InsaneRebel1::processShot() {
return;
}
- // Record shot at current cursor position.
+ // Shot origin depends on game mode:
+ // On-foot: character position (g_shipOffsetX + g_perspectiveX)
+ // Turret: perspective-adjusted center
+ // Flight: cursor position
const bool turretMode = (_activeGameOpcode == 0x08 || _activeGameOpcode == 0x0A);
- const int16 shipCenterX = turretMode ? (int16)(kRA1CenterX + (_perspectiveX - 0x20)) : _shipPosX;
- const int16 shipCenterY = turretMode ? (int16)(kRA1CenterY + (_perspectiveY - 0x17)) : _shipPosY;
+ int16 originX, originY;
+ if (onFootMode) {
+ originX = _onFootCharX + kOnFootCenterX;
+ originY = _onFootCharY + kOnFootCenterY;
+ } else if (turretMode) {
+ originX = (int16)(kRA1CenterX + (_perspectiveX - 0x20));
+ originY = (int16)(kRA1CenterY + (_perspectiveY - 0x17));
+ } else {
+ originX = _shipPosX;
+ originY = _shipPosY;
+ }
_shotSlots[slot].timer = (_gameplayFlags75ff & 0x2) ? 2 : 5;
_shotSlots[slot].posX = _shipPosX;
_shotSlots[slot].posY = _shipPosY;
- _shotSlots[slot].centerX = shipCenterX;
- _shotSlots[slot].centerY = shipCenterY;
+ _shotSlots[slot].centerX = originX;
+ _shotSlots[slot].centerY = originY;
_shotSlots[slot].variant = _shotAlternator;
_shotAlternator = 1 - _shotAlternator;
playSfx(kSfxLaserShot, 127, 0);
- debug(5, "RA1 shot: slot=%d pos=(%d,%d)", slot, _shotSlots[slot].posX, _shotSlots[slot].posY);
+ debug(5, "RA1 shot: slot=%d pos=(%d,%d) origin=(%d,%d)", slot,
+ _shotSlots[slot].posX, _shotSlots[slot].posY, originX, originY);
}
// checkTargetHit â FUN_1C0EF (0x1C0EF). AABB target detection with snap tolerance.
@@ -1604,7 +1652,9 @@ void InsaneRebel1::checkTargetHit(int16 targetIdx, int16 left, int16 top, int16
projectGameplayPoint(_shipPosX, _shipPosY);
}
- debug(5, "RA1 HIT: target=%d score=%d kills=%d", targetIdx, _score, _killCount);
+ debug(3, "RA1 HIT: target=%d gost=%d pos=(%d,%d) score=%d kills=%d bangSprites=%d",
+ targetIdx, gi, _gostSlots[gi].posX, _gostSlots[gi].posY,
+ _score, _killCount, _bangBank.numSprites);
return;
}
}
diff --git a/engines/scumm/insane/insane_rebel1_render.cpp b/engines/scumm/insane/insane_rebel1_render.cpp
index ab5a2c6f5b4..311841d370d 100644
--- a/engines/scumm/insane/insane_rebel1_render.cpp
+++ b/engines/scumm/insane/insane_rebel1_render.cpp
@@ -454,11 +454,13 @@ void InsaneRebel1::procPostRendering(byte *renderBitmap, int32 codecparam, int32
(!haveFrameGameOpcodes && _activeGameOpcode == 0x0A);
renderTargetBoxes(renderBitmap, pitch, width, height);
processShot();
+ renderLaserShots(renderBitmap, pitch, width, height);
+ // Timer decrement AFTER rendering (original decrements inside the render loop).
+ // This ensures timer==5 first frame is rendered with gun barrel offset and lerp=1.
for (int i = 0; i < kMaxShotSlots; i++) {
if (_shotSlots[i].timer > 0)
_shotSlots[i].timer--;
}
- renderLaserShots(renderBitmap, pitch, width, height);
renderGostSlots(renderBitmap, pitch, width, height);
renderTargeting(renderBitmap, pitch, width, height);
@@ -553,8 +555,17 @@ void InsaneRebel1::renderTargeting(byte *dst, int pitch, int width, int height)
// renderGostSlots â FUN_1C9CD (0x1C9CD). Hit explosion animations at target positions.
// Renders explosion sprites from bangBank at each GOST slot's recorded position.
void InsaneRebel1::renderGostSlots(byte *dst, int pitch, int width, int height) {
- if (_bangBank.numSprites <= 0)
+ if (_bangBank.numSprites <= 0) {
+ // Warn if there are active GOST slots but no bang sprites to render
+ for (int i = 0; i < kMaxGostSlots; i++) {
+ if (_gostSlots[i].targetId != 0 && _gostSlots[i].frame < 10) {
+ debug(1, "RA1 renderGostSlots: bangBank empty but gost slot %d active (target=%d frame=%d)",
+ i, _gostSlots[i].targetId, _gostSlots[i].frame);
+ _gostSlots[i].targetId = 0; // Clear stale slot
+ }
+ }
return;
+ }
const int overlayX = ra1OverlayViewOffsetX(this);
const bool projectGostMarkers = (_activeGameOpcode == 0x0B);
@@ -581,7 +592,7 @@ void InsaneRebel1::renderGostSlots(byte *dst, int pitch, int width, int height)
}
}
-// renderLaserShots â FUN_1CDA7/FUN_1D79C shot visual path.
+// renderLaserShots â FUN_1CDA7/FUN_1D79C/HandleGameOp1A shot visual path.
void InsaneRebel1::renderLaserShots(byte *dst, int pitch, int width, int height) {
if (_laserBank.numSprites <= 0)
return;
@@ -589,11 +600,24 @@ void InsaneRebel1::renderLaserShots(byte *dst, int pitch, int width, int height)
// DAT_2407 lookup used by FUN_1CDA7/FUN_1D79C for timer 1..5 interpolation.
// Entry 0 unused.
static const int kShotLerpByTimer[6] = { 0, 8, 7, 6, 4, 0 };
+ // DAT_2413: on-foot lerp table (timer 5 = 1, not 0 like flight mode).
+ static const int kOnFootShotLerp[6] = { 0, 8, 7, 6, 4, 1 };
+ // DAT_240e: gun barrel X offset indexed by shipDirIndex (for timer==5 first frame).
+ static const int16 kOnFootGunBarrelX[20] = {
+ 4, 0, 8, 8, 7, 6, 4, 1, 0, 0,
+ 0, -56, -47, -23, -13, 0, 13, 30, 54, 59
+ };
+ // DAT_2420: gun barrel Y offset indexed by shipDirIndex (for timer==5 first frame).
+ static const int16 kOnFootGunBarrelY[20] = {
+ 0, 0, -56, -47, -23, -13, 0, 13, 30, 54,
+ 59, -3, -19, -24, -30, -28, -30, -29, -20, -5
+ };
const int spritesPerSet = 5;
const int overlayX = ra1OverlayViewOffsetX(this);
const int overlayY = ra1OverlayViewOffsetY(this);
const int leftStartX = 0;
const int rightStartX = 0x13F; // 319
+ const bool onFootMode = (_activeGameOpcode == 0x19 || _activeGameOpcode == 0x1A);
const bool turretMode = (_activeGameOpcode == 0x08 || _activeGameOpcode == 0x0A);
const int shipBaseX = turretMode ? (kRA1CenterX + (_perspectiveX - 0x20)) : _shipPosX;
const int shipBaseY = turretMode ? (kRA1CenterY + (_perspectiveY - 0x17)) : (overlayY + _shipPosY);
@@ -601,11 +625,41 @@ void InsaneRebel1::renderLaserShots(byte *dst, int pitch, int width, int height)
for (int i = 0; i < kMaxShotSlots; i++) {
if (_shotSlots[i].timer > 0 && _shotSlots[i].timer <= spritesPerSet) {
const int timer = _shotSlots[i].timer;
- const int lerp = kShotLerpByTimer[timer];
const int frame = spritesPerSet - timer;
const int targetX = CLIP<int>(overlayX + _shipPosX, 0, width - 1);
const int targetY = CLIP<int>(overlayY + _shipPosY, 0, height - 1);
+ if (onFootMode) {
+ // HandleGameOp1A_OnFootVariant: single beam from character to crosshair.
+ // Gun barrel offset on first frame (timer==5, dirIndex in aiming range).
+ if (timer == 5 && _shipDirIndex > 10 && _shipDirIndex < 20) {
+ _shotSlots[i].centerX += kOnFootGunBarrelX[_shipDirIndex];
+ _shotSlots[i].centerY += kOnFootGunBarrelY[_shipDirIndex];
+ }
+
+ // Skip visible rendering when flags indicate invisible shots (auto-fire).
+ if ((_gameplayFlags75fe & 8) != 0)
+ continue;
+
+ const int srcX = _shotSlots[i].centerX;
+ const int srcY = _shotSlots[i].centerY;
+ const int dstX = _shotSlots[i].posX;
+ const int dstY = _shotSlots[i].posY;
+ const int dir = ra1ShotDirection((int16)srcX, (int16)srcY,
+ (int16)dstX, (int16)dstY);
+ const int sprIdx = MIN<int>(ABS(dir), _laserBank.numSprites - 1);
+ const uint32 flags = 0x83 | ((dir < 0) ? 0x2000 : 0);
+ const int onFootLerp = kOnFootShotLerp[timer];
+ const int interpX = srcX + (((dstX - srcX) * onFootLerp) >> 3);
+ const int interpY = srcY + (((dstY - srcY) * onFootLerp) >> 3);
+
+ renderSpriteWithFlags(dst, pitch, width, height,
+ interpX, interpY, _laserBank.sprites[sprIdx], flags);
+ continue;
+ }
+
+ const int lerp = kShotLerpByTimer[timer];
+
if (turretMode) {
// FUN_1D79C chooses emitters in two ways:
// - DAT_75E4 == 2: use DAT_75DC..DAT_75E2 fixed offsets
diff --git a/engines/scumm/insane/insane_rebel1_runlevels.cpp b/engines/scumm/insane/insane_rebel1_runlevels.cpp
index 7b05e2f0814..b19e957206f 100644
--- a/engines/scumm/insane/insane_rebel1_runlevels.cpp
+++ b/engines/scumm/insane/insane_rebel1_runlevels.cpp
@@ -1089,6 +1089,7 @@ void InsaneRebel1::playInteractiveVideo(const char *filename, int32 startFrame)
clearBit(0);
_interactiveVideoActive = true;
_levelRouteChoice = 0;
+ _onFootInitialized = false; // Reset so each segment triggers counter==0 init
resetFrameObjectState();
_vm->_smushVideoShouldFinish = false;
splayer->setCurVideoFlags(0x28);
Commit: 4dcc5d8d2df2e07a5f572b7ad67119132ca6657a
https://github.com/scummvm/scummvm/commit/4dcc5d8d2df2e07a5f572b7ad67119132ca6657a
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:41+02:00
Commit Message:
SCUMM: RA1: Improve walker level rendering
Changed paths:
engines/scumm/insane/insane_rebel1.cpp
engines/scumm/insane/insane_rebel1.h
engines/scumm/insane/insane_rebel1_render.cpp
engines/scumm/insane/insane_rebel1_runlevels.cpp
diff --git a/engines/scumm/insane/insane_rebel1.cpp b/engines/scumm/insane/insane_rebel1.cpp
index a31b45bdcc3..fbca7de4e95 100644
--- a/engines/scumm/insane/insane_rebel1.cpp
+++ b/engines/scumm/insane/insane_rebel1.cpp
@@ -186,6 +186,9 @@ void InsaneRebel1::loadTuningForLevel(int level) {
_tuning.levelPts = kTuningTable[l][d][10];
_tuning.bonus = kTuningTable[l][d][11];
_tuning.flags = kTuningTable[l][d][12];
+ // initLevelFromTuning (0x13E7B) copies tuning flags into g_hudDisableFlags (0x75FE).
+ _gameplayFlags75fe = (uint16)_tuning.flags;
+
debug(1, "RA1: Loaded tuning level=%d diff=%d: roll=%d lift=%d slide=%d drift=%d snap=%d "
"miss=%d wham=%d shot=%d kill=%d time=%d levelPts=%d bonus=%d flags=0x%x",
level, d, _tuning.roll, _tuning.lift, _tuning.slide, _tuning.drift, _tuning.snap,
@@ -304,6 +307,9 @@ InsaneRebel1::InsaneRebel1(ScummEngine_v7 *scumm) : Insane(), _vm(scumm) {
_gostSlotIdx = 0;
_killCount = 0;
_lastHitTarget = 0;
+ _walkerHealth = 100;
+ _walkerTimer = 0;
+ _walkerBranchChoice = 0;
resetFrameObjectState();
if (loadRA1Nut("SYS/TALKFONT.NUT", _hudFontBank)) {
diff --git a/engines/scumm/insane/insane_rebel1.h b/engines/scumm/insane/insane_rebel1.h
index e4fde7cbff6..21c65deea64 100644
--- a/engines/scumm/insane/insane_rebel1.h
+++ b/engines/scumm/insane/insane_rebel1.h
@@ -146,7 +146,11 @@ private:
void renderTargetBoxes(byte *dst, int pitch, int width, int height);
void renderTargeting(byte *dst, int pitch, int width, int height);
void renderGostSlots(byte *dst, int pitch, int width, int height);
+ void renderGostScorePopup(byte *dst, int pitch, int width, int height,
+ int16 centerX, int16 centerY, int16 frame);
void renderLaserShots(byte *dst, int pitch, int width, int height);
+ void renderLevel8Overlay(byte *dst, int pitch, int width, int height);
+ void updateLevel8WalkerState();
void renderSprite(byte *dst, int pitch, int width, int height,
int x, int y, const RA1Sprite &sprite);
void updateGostSlotPosition(int16 targetIdx, int16 left, int16 top, int16 right, int16 bottom);
@@ -411,6 +415,31 @@ private:
int16 _killCount; // 0x75D0: targets destroyed this stage
int16 _lastHitTarget; // 0x75D6: prevents double-hit on same target
+ // Level 8 walker-specific state â RunLevel8Flow (0x18546)
+ int16 _walkerHealth; // Walker health percentage (0-100), init=100
+ int16 _walkerTimer; // Attack window countdown (100â0)
+ int16 _walkerBranchChoice; // Directional choice: 0=none, 1=left, 2=right
+
+ // Attack window frame numbers per route (3 routes à 3 windows)
+ // Route 0: 2588/1709/262, Route 1: 2323/1444/-2, Route 2: 877/-2/-2
+ // -2 = disabled (no window at that slot for this route)
+ static const int16 kWalkerAttackWindow1[3];
+ static const int16 kWalkerAttackWindow2[3];
+ static const int16 kWalkerAttackWindow3[3];
+
+ // Per-route damage frame tables â FUN_12fe1/FUN_130c9/FUN_13195
+ // Each entry: {frameNumber, hitboxType} where hitboxType determines the check
+ struct WalkerDamageFrame {
+ int16 frame;
+ int16 type; // 0=proximity(41,32), 1=offsetY(15), 2=offsetX(24), 3=proximity(40,31), 4=offsetY(31)
+ };
+ static const WalkerDamageFrame kWalkerDamageRoute0[];
+ static const WalkerDamageFrame kWalkerDamageRoute1[];
+ static const WalkerDamageFrame kWalkerDamageRoute2[];
+ static const int kWalkerDamageRoute0Count;
+ static const int kWalkerDamageRoute1Count;
+ static const int kWalkerDamageRoute2Count;
+
static const int kFrameObjectStateBytes = 300;
byte _frameObjectState[kFrameObjectStateBytes];
};
diff --git a/engines/scumm/insane/insane_rebel1_render.cpp b/engines/scumm/insane/insane_rebel1_render.cpp
index 311841d370d..c498bc388a4 100644
--- a/engines/scumm/insane/insane_rebel1_render.cpp
+++ b/engines/scumm/insane/insane_rebel1_render.cpp
@@ -437,6 +437,13 @@ void InsaneRebel1::procPostRendering(byte *renderBitmap, int32 codecparam, int32
_player->_ra1ViewportOffsetX = _perspectiveX;
_player->_ra1ViewportOffsetY = _perspectiveY;
}
+
+ // Screen shake â SetCameraOffset (0x224FD): random [-2,+2] jitter when
+ // _screenFlash > 0. Original uses RandScaleByte(5) - 2 for each axis.
+ if (_screenFlash > 0) {
+ _player->_ra1ViewportOffsetX += (int16)(_vm->_rnd.getRandomNumber(4) - 2);
+ _player->_ra1ViewportOffsetY += (int16)(_vm->_rnd.getRandomNumber(4) - 2);
+ }
}
// Assembly dispatch (FUN_1BE1B) only runs the targeting/shot overlay pipeline
@@ -485,12 +492,27 @@ void InsaneRebel1::procPostRendering(byte *renderBitmap, int32 codecparam, int32
}
renderExplosions(renderBitmap, pitch, width, height);
+
+ // Level 8 (Imperial Walkers) â walker-specific state update + UI overlay.
+ // In the original, RunLevel8Flow runs the walker logic inline in the per-frame
+ // game loop. We call it from procPostRendering when _currentLevel == 7.
+ if (_currentLevel == 7) {
+ updateLevel8WalkerState();
+ renderLevel8Overlay(renderBitmap, pitch, width, height);
+ }
+
renderHUD(renderBitmap, pitch, width, height);
_fireCooldown = _playerFired ? 1 : 0;
}
// renderTargetBoxes â FUN_1C940 (0x1C940). Per-target green box overlays.
+// Original gates on g_hudDisableFlags (0x75FE) bit 1: skip when set (Hard difficulty).
void InsaneRebel1::renderTargetBoxes(byte *dst, int pitch, int width, int height) {
+ if (_gameplayFlags75fe & 2) {
+ _prevTargetCount = _targetCount;
+ _targetCount = 0;
+ return;
+ }
const int overlayX = ra1OverlayViewOffsetX(this);
const RA1SpriteBank &markerBank = (_techFontBank.numSprites > 0) ? _techFontBank : _hudFontBank;
const bool projectTargetMarkers = (_activeGameOpcode == 0x0B);
@@ -552,8 +574,32 @@ void InsaneRebel1::renderTargeting(byte *dst, int pitch, int width, int height)
_lastHitTarget = 0;
}
+// renderGostScorePopup â Per-kill score glyph from RenderGostOverlaySlots (0x1C9CD).
+// Maps kill score to tech-font glyph and draws it rising upward from the kill position.
+void InsaneRebel1::renderGostScorePopup(byte *dst, int pitch, int width, int height,
+ int16 centerX, int16 centerY, int16 frame) {
+ // Score-to-glyph mapping from original (0x1CA5D-0x1CACB)
+ char glyphChar = '\0';
+ uint16 scoreValue = (uint16)_tuning.kill;
+ if (scoreValue == 10) glyphChar = 0x72; // 'r'
+ else if (scoreValue == 25) glyphChar = 0x74; // 't'
+ else if (scoreValue == 50) glyphChar = 0x73; // 's'
+ else if (scoreValue == 100) glyphChar = 0x6F; // 'o'
+ else if (scoreValue == 200) glyphChar = 0x70; // 'p'
+ else if (scoreValue == 500) glyphChar = 0x71; // 'q'
+
+ if (glyphChar == '\0')
+ return;
+
+ // Original: DrawStringEx(buf, colorMap, centerX-4, centerY-frame, 1, 100, 3, scoreText)
+ // "<<{glyph}" string selects tech font layer via the << markup
+ char scoreText[4] = { '<', '<', glyphChar, '\0' };
+ drawFontBankString(dst, pitch, width, height,
+ centerX - 4, centerY - frame, scoreText);
+}
+
// renderGostSlots â FUN_1C9CD (0x1C9CD). Hit explosion animations at target positions.
-// Renders explosion sprites from bangBank at each GOST slot's recorded position.
+// Renders explosion sprites from bangBank + per-kill score popup glyphs.
void InsaneRebel1::renderGostSlots(byte *dst, int pitch, int width, int height) {
if (_bangBank.numSprites <= 0) {
// Warn if there are active GOST slots but no bang sprites to render
@@ -585,6 +631,13 @@ void InsaneRebel1::renderGostSlots(byte *dst, int pitch, int width, int height)
int drawY = centerY - spr.height / 2;
renderSprite(dst, pitch, width, height, drawX, drawY, spr);
+ // Per-kill score popup glyph â RenderGostOverlaySlots (0x1CA35)
+ // Suppressed in on-foot mode (combatModeFlags & 8)
+ if ((_gameplayFlags75fe & 8) == 0) {
+ renderGostScorePopup(dst, pitch, width, height,
+ overlayX + centerX, centerY, _gostSlots[i].frame);
+ }
+
_gostSlots[i].frame++;
if (_gostSlots[i].frame >= 10)
_gostSlots[i].targetId = 0; // Animation complete
@@ -1128,6 +1181,253 @@ void InsaneRebel1::renderHUD(byte *dst, int pitch, int width, int height) {
}
+// Attack window frame tables â RunLevel8Flow (0x18546), data at 0x236D/0x2373/0x2379.
+// Each route has up to 3 attack windows. -2 means disabled.
+const int16 InsaneRebel1::kWalkerAttackWindow1[3] = { 2588, 2323, 877 };
+const int16 InsaneRebel1::kWalkerAttackWindow2[3] = { 1709, 1444, -2 };
+const int16 InsaneRebel1::kWalkerAttackWindow3[3] = { 262, -2, -2 };
+
+// Per-route walker damage frame tables â FUN_12fe1/FUN_130c9/FUN_13195.
+// type: 0=proximity(41,32), 1=offsetY(15), 2=offsetX(24), 3=proximity(40,31), 4=offsetY(31)
+const InsaneRebel1::WalkerDamageFrame InsaneRebel1::kWalkerDamageRoute0[] = {
+ {0x00CD, 2}, {0x00EF, 2}, {0x0294, 1}, {0x03A2, 2},
+ {0x04BE, 0}, {0x05C9, 2}, {0x076C, 0}, {0x085A, 4}, {0x096F, 2}
+};
+const int InsaneRebel1::kWalkerDamageRoute0Count = ARRAYSIZE(kWalkerDamageRoute0);
+
+const InsaneRebel1::WalkerDamageFrame InsaneRebel1::kWalkerDamageRoute1[] = {
+ {0x0189, 1}, {0x0297, 2}, {0x03B3, 0}, {0x04BE, 4},
+ {0x0661, 0}, {0x074F, 4}, {0x0864, 4}
+};
+const int InsaneRebel1::kWalkerDamageRoute1Count = ARRAYSIZE(kWalkerDamageRoute1);
+
+const InsaneRebel1::WalkerDamageFrame InsaneRebel1::kWalkerDamageRoute2[] = {
+ {0x00BB, 0}, {0x01A9, 4}, {0x02BE, 4}
+};
+const int InsaneRebel1::kWalkerDamageRoute2Count = ARRAYSIZE(kWalkerDamageRoute2);
+
+// updateLevel8WalkerState â Per-frame walker health + attack window + damage logic.
+// Called from procPostRendering when _currentLevel == 7.
+void InsaneRebel1::updateLevel8WalkerState() {
+ // Walker health computation â RunLevel8Flow (0x18634-0x18655)
+ if (_walkerHealth >= 11) {
+ _walkerHealth = (int16)(100 - (_killCount + (_killCount >> 2)));
+ } else if (_walkerHealth > 0 && (_frameCounter & 3) == 0) {
+ _walkerHealth--;
+ }
+
+ // Walker destroyed â exit interactive video (original loop: `while (sVar6 != 0)`)
+ if (_walkerHealth <= 0) {
+ _vm->_smushVideoShouldFinish = true;
+ return;
+ }
+
+ // Per-route damage check â FUN_12fe1/FUN_130c9/FUN_13195
+ int route = CLIP(_levelRouteIndex, 0, 2);
+ const WalkerDamageFrame *table;
+ int tableCount;
+ switch (route) {
+ case 0: table = kWalkerDamageRoute0; tableCount = kWalkerDamageRoute0Count; break;
+ case 1: table = kWalkerDamageRoute1; tableCount = kWalkerDamageRoute1Count; break;
+ default: table = kWalkerDamageRoute2; tableCount = kWalkerDamageRoute2Count; break;
+ }
+
+ uint16 fc = (uint16)_frameCounter;
+ for (int i = 0; i < tableCount; i++) {
+ if (fc != (uint16)table[i].frame)
+ continue;
+
+ int16 dx = _shipPosX - kRA1CenterX; // distance from center
+ int16 dy = _shipPosY - kRA1CenterY;
+ if (dx < 0) dx = -dx;
+ if (dy < 0) dy = -dy;
+
+ bool hit = false;
+ switch (table[i].type) {
+ case 0: hit = (dx < 0x29 && dy < 0x20); break; // proximity(41,32)
+ case 1: hit = (dy < 0x0F); break; // offsetY(15)
+ case 2: hit = (dx < 0x18); break; // offsetX(24)
+ case 3: hit = (dx < 0x28 && dy < 0x1F); break; // proximity(40,31)
+ case 4: hit = (dy < 0x1F); break; // offsetY(31)
+ default: break;
+ }
+
+ if (hit)
+ _damageFlags |= 0x20;
+ break;
+ }
+
+ // Attack window logic â RunLevel8Flow (0x18778-0x18B4A)
+ const int16 *windows[3] = {
+ &kWalkerAttackWindow1[route],
+ &kWalkerAttackWindow2[route],
+ &kWalkerAttackWindow3[route]
+ };
+ int16 frameNum = (int16)fc;
+
+ // Check if we're inside any attack window (window-100 < frame <= window)
+ bool inWindow = false;
+ for (int w = 0; w < 3; w++) {
+ int16 windowEnd = *windows[w];
+ if (windowEnd < 0) continue;
+ if (frameNum > windowEnd - 100 && frameNum <= windowEnd) {
+ inWindow = true;
+
+ // Reset timer at window start (first frame of window)
+ if (frameNum == windowEnd - 99)
+ _walkerTimer = 100;
+ break;
+ }
+ }
+
+ if (inWindow && _walkerBranchChoice == 0) {
+ _walkerTimer--;
+
+ // Check if we're in the directional phase (last 50 frames)
+ bool inDirectionalPhase = false;
+ for (int w = 0; w < 3; w++) {
+ int16 windowEnd = *windows[w];
+ if (windowEnd < 0) continue;
+ if (frameNum > windowEnd - 0x32 && frameNum <= windowEnd) {
+ inDirectionalPhase = true;
+ break;
+ }
+ }
+
+ if (inDirectionalPhase) {
+ // Player can choose direction during last 50 frames
+ if (_playerFired && _avgInputX == 0) {
+ _walkerBranchChoice = (_shipPosX < 0xA0) ? 1 : 2;
+ }
+ } else {
+ // Torpedo sound every 8 frames during targeting phase
+ if ((_frameCounter & 7) == 0)
+ playSfx(kSfxLockOn, 127, 0);
+ }
+ }
+
+ // At window boundary: decide route branch â RunLevel8Flow (0x18A7F-0x18B4A)
+ // Original: left branches unless at window3, right branches unless at window2.
+ for (int w = 0; w < 3; w++) {
+ int16 windowEnd = *windows[w];
+ if (windowEnd < 0) continue;
+ if (fc != (uint16)windowEnd) continue;
+
+ int newRoute = 0;
+ bool goLeft = (_walkerBranchChoice == 1) ||
+ (_shipPosX < 0xA0 && _walkerBranchChoice != 2);
+
+ if (goLeft) {
+ // Left: branch to route 1 unless at window3 (w==2)
+ if (w != 2)
+ newRoute = 1;
+ } else {
+ // Right: branch to route 2 unless at window2 (w==1)
+ if (w != 1)
+ newRoute = 2;
+ }
+
+ if (newRoute != 0) {
+ _pendingRouteIndex = newRoute;
+ _vm->_smushVideoShouldFinish = true;
+ }
+ _walkerBranchChoice = 0;
+ break;
+ }
+}
+
+// renderLevel8Overlay â Walker-specific UI from RunLevel8Flow (0x18660-0x18A7E).
+// Draws walker health %, attack timer, directional arrows, and target reticle.
+// Original draws via DrawStringEx/FormatAndDrawText in screen-space (within viewport).
+// We draw into 384x242 buffer, so add viewport offset to convert screenâbuffer coords.
+void InsaneRebel1::renderLevel8Overlay(byte *dst, int pitch, int width, int height) {
+ if (_currentLevel != 7)
+ return;
+
+ // Viewport offset: screen-space â buffer-space (same approach as renderHUD)
+ int viewX = _player ? _player->_ra1ViewportOffsetX : _perspectiveX;
+ int viewY = _player ? _player->_ra1ViewportOffsetY : _perspectiveY;
+
+ // Walker health display â "<<WALKER %d%%" at projected (0x61, 0x8D)
+ // Blinks when health < 16: only drawn when (frameCounter & 2) != 0
+ if (_walkerHealth > 0 && (_walkerHealth >= 16 || (_frameCounter & 2) != 0)) {
+ int16 projX = 0x61, projY = 0x8D;
+ projectGameplayPoint(projX, projY);
+ // Apply 1/4 parallax compensation (original: 0x61 - (projX - 0x61) >> 2)
+ projX = (int16)(0x61 - ((projX - 0x61) >> 2));
+ projY = (int16)(0x8D - ((projY - 0x8D) >> 2));
+
+ char walkerStr[24];
+ Common::sprintf_s(walkerStr, "<<WALKER %d%%%%", (int)_walkerHealth);
+ drawFontBankString(dst, pitch, width, height, viewX + projX, viewY + projY, walkerStr);
+ }
+
+ // Attack window overlay (timer + arrows/reticle)
+ int route = CLIP(_levelRouteIndex, 0, 2);
+ const int16 *windows[3] = {
+ &kWalkerAttackWindow1[route],
+ &kWalkerAttackWindow2[route],
+ &kWalkerAttackWindow3[route]
+ };
+ int16 frameNum = (int16)(uint16)_frameCounter;
+
+ bool inWindow = false;
+ bool inDirectionalPhase = false;
+ for (int w = 0; w < 3; w++) {
+ int16 windowEnd = *windows[w];
+ if (windowEnd < 0) continue;
+ if (frameNum > windowEnd - 100 && frameNum <= windowEnd) {
+ inWindow = true;
+ if (frameNum > windowEnd - 0x32 && frameNum <= windowEnd)
+ inDirectionalPhase = true;
+ break;
+ }
+ }
+
+ if (!inWindow || _walkerBranchChoice != 0)
+ return;
+
+ // Timer countdown â "<<TIME %d" at projected (0x62, 0x9C)
+ {
+ int16 projX = 0x62, projY = 0x9C;
+ projectGameplayPoint(projX, projY);
+ projX = (int16)(0x62 - ((projX - 0x62) >> 2));
+ projY = (int16)(0x9C - ((projY - 0x9C) >> 2));
+
+ char timerStr[16];
+ Common::sprintf_s(timerStr, "<<TIME %d", (int)_walkerTimer);
+ drawFontBankString(dst, pitch, width, height, viewX + projX, viewY + projY, timerStr);
+ }
+
+ if (inDirectionalPhase) {
+ // Directional arrows â projected from (0,0) with parallax compensation
+ int16 projX = 0, projY = 0;
+ projectGameplayPoint(projX, projY);
+ int16 px = (int16)(-(projX >> 2));
+ int16 py = (int16)(-(projY >> 2));
+
+ if (_shipPosX < 0xA0) {
+ // Left arrow "<<v"
+ drawFontBankString(dst, pitch, width, height,
+ viewX + 0xA6 + px, viewY + 0x92 + py, "<<v");
+ } else {
+ // Right arrow "<<u"
+ drawFontBankString(dst, pitch, width, height,
+ viewX + 0xA8 - px, viewY + 0x93 - py, "<<u");
+ }
+ } else {
+ // Target reticle â "<<w" at projected (0xA9, 0x9A), blinks on (frame & 4)
+ if ((_frameCounter & 4) == 0) {
+ int16 projX = 0, projY = 0;
+ projectGameplayPoint(projX, projY);
+ int16 drawX = (int16)(0xA9 - (projX >> 2));
+ int16 drawY = (int16)(0x9A - (projY >> 2));
+ drawFontBankString(dst, pitch, width, height,
+ viewX + drawX, viewY + drawY, "<<w");
+ }
+ }
+}
+
// renderSprite â Simplified version of FUN_20BD3 (0x20BD3) glyph/sprite renderer.
// Original dispatches through full codec pipeline; this does flat pixel blit with transparency.
void InsaneRebel1::renderSprite(byte *dst, int pitch, int width, int height,
diff --git a/engines/scumm/insane/insane_rebel1_runlevels.cpp b/engines/scumm/insane/insane_rebel1_runlevels.cpp
index b19e957206f..2027d684974 100644
--- a/engines/scumm/insane/insane_rebel1_runlevels.cpp
+++ b/engines/scumm/insane/insane_rebel1_runlevels.cpp
@@ -711,6 +711,11 @@ bool InsaneRebel1::runLevel8() {
_avgInputX = 0;
_avgInputY = 0;
+ // Walker-specific state â RunLevel8Flow (0x18546)
+ _walkerHealth = 100;
+ _walkerTimer = 0;
+ _walkerBranchChoice = 0;
+
int route = 0;
while (!_vm->shouldQuit()) {
_levelRouteIndex = route;
Commit: 403d9675a91748fd2a325d100516d91c2979b819
https://github.com/scummvm/scummvm/commit/403d9675a91748fd2a325d100516d91c2979b819
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:41+02:00
Commit Message:
SCUMM: RA1: Complete menu handling
Changed paths:
engines/scumm/insane/insane_rebel1.cpp
engines/scumm/insane/insane_rebel1.h
engines/scumm/insane/insane_rebel1_iact.cpp
engines/scumm/insane/insane_rebel1_menu.cpp
engines/scumm/insane/insane_rebel1_runlevels.cpp
diff --git a/engines/scumm/insane/insane_rebel1.cpp b/engines/scumm/insane/insane_rebel1.cpp
index fbca7de4e95..1291a7b90f7 100644
--- a/engines/scumm/insane/insane_rebel1.cpp
+++ b/engines/scumm/insane/insane_rebel1.cpp
@@ -287,6 +287,27 @@ InsaneRebel1::InsaneRebel1(ScummEngine_v7 *scumm) : Insane(), _vm(scumm) {
_startLevel = 1;
_turbulenceEnabled = false;
+ // Options â read initial state from ScummVM mixer
+ _optMusicEnabled = !_vm->_mixer->isSoundTypeMuted(Audio::Mixer::kMusicSoundType);
+ _optSfxEnabled = !_vm->_mixer->isSoundTypeMuted(Audio::Mixer::kSFXSoundType);
+ _optTextEnabled = true;
+ _optControlsYFlip = false;
+ _optVolume = _vm->_mixer->getVolumeForSoundType(Audio::Mixer::kPlainSoundType) * 127 / Audio::Mixer::kMaxChannelVolume;
+
+ // Default high scores â from DS:0x1D0/0x298/0x2C0
+ static const struct { const char *name; int32 score; byte gender; } kDefaultScores[kHighScoreCount] = {
+ {"Vince", 10000, 2}, {"Tamlynn", 9000, 2}, {"Chip", 8000, 2},
+ {"Brett", 7000, 1}, {"Casey", 6000, 1}, {"Justin", 5000, 1},
+ {"Bill", 4000, 0}, {"Aaron", 3000, 0}, {"Mary", 2000, 0},
+ {"Ron", 1000, 0}
+ };
+ for (int i = 0; i < kHighScoreCount; i++) {
+ Common::sprintf_s(_highScores[i].name, "<%s", kDefaultScores[i].name);
+ _highScores[i].score = kDefaultScores[i].score;
+ _highScores[i].gender = kDefaultScores[i].gender;
+ }
+ _highScoresActive = false;
+
// Shooting/targeting state
_playerFired = false;
_fireCooldown = 0;
diff --git a/engines/scumm/insane/insane_rebel1.h b/engines/scumm/insane/insane_rebel1.h
index 21c65deea64..f42f612e441 100644
--- a/engines/scumm/insane/insane_rebel1.h
+++ b/engines/scumm/insane/insane_rebel1.h
@@ -361,15 +361,33 @@ private:
int _menuSelection; // 0..4 maps to return values 1..5
int _menuFrameCounter;
- // Options submenu state
+ // Options submenu state â RunGameOptionsMenu (0x14B42)
+ static const int kOptionsItemCount = 9; // 8 options + BACK
bool _optionsActive; // True when showing options instead of main menu
- int _optionsSel; // 0=difficulty, 1=turbulence, 2=back
+ int _optionsSel; // 0..8 selected option row
bool _levelSelectActive; // True when showing level-select submenu
int _levelSelectSel; // 0=Level1 ... N-1=Back
int _startLevel; // 1-based start level for "Start New Game"
+ // Per-option state (matching original RunGameOptionsMenu globals)
+ bool _optMusicEnabled; // DAT_22b7: music on/off
+ bool _optSfxEnabled; // DAT_22b8: sfx+voice on/off
+ bool _optTextEnabled; // DAT_22b9: dialogue text on/off
+ bool _optControlsYFlip; // DAT_22be: Y-axis inversion
+ int _optVolume; // DAT_22c1: master volume 0..127
bool _turbulenceEnabled; // Random per-frame jitter in deltaX (original has it on)
+ // High scores / TOP PILOTS display â data at DS:0x1D0
+ static const int kHighScoreCount = 10;
+ struct HighScoreEntry {
+ char name[20]; // 0x14 bytes per entry (includes '<' prefix)
+ int32 score;
+ byte gender; // 0/1/2 â tech font glyph '{','|','}'
+ };
+ HighScoreEntry _highScores[kHighScoreCount];
+ bool _highScoresActive; // True when showing TOP PILOTS overlay
+ void showHighScores();
+
// Shooting state â FUN_1CCA0 (0x1CCA0)
bool _playerFired; // 0x7570: current fire-button state
int16 _fireCooldown; // 0x757C: previous-frame fire-button state (edge gate)
diff --git a/engines/scumm/insane/insane_rebel1_iact.cpp b/engines/scumm/insane/insane_rebel1_iact.cpp
index 2ac8f46b999..699c181a71a 100644
--- a/engines/scumm/insane/insane_rebel1_iact.cpp
+++ b/engines/scumm/insane/insane_rebel1_iact.cpp
@@ -634,6 +634,10 @@ void InsaneRebel1::preprocessMouseAxes(int16 &inputX, int16 &inputY) {
inputX = CLIP<int16>(scaledX, -0xA0, 0xA0);
inputY = CLIP<int16>(scaledY, -127, 127);
+
+ // Controls Y-flip option (DAT_22be in original)
+ if (_optControlsYFlip)
+ inputY = -inputY;
}
// updateShipPhysics â FUN_1DEB5 (0x1DEB5). Accumulator-based position system.
diff --git a/engines/scumm/insane/insane_rebel1_menu.cpp b/engines/scumm/insane/insane_rebel1_menu.cpp
index 2ab05d280dd..231b94fdac8 100644
--- a/engines/scumm/insane/insane_rebel1_menu.cpp
+++ b/engines/scumm/insane/insane_rebel1_menu.cpp
@@ -20,9 +20,12 @@
*/
#include "common/system.h"
+#include "common/config-manager.h"
#include "common/events.h"
#include "common/str.h"
+#include "audio/mixer.h"
+
#include "scumm/scumm_v7.h"
#include "scumm/smush/smush_player.h"
#include "scumm/insane/insane_rebel1.h"
@@ -78,11 +81,35 @@ bool InsaneRebel1::notifyEvent(const Common::Event &event) {
switch (event.kbd.keycode) {
case Common::KEYCODE_UP:
case Common::KEYCODE_w:
- _optionsSel = (_optionsSel + 2) % 3;
+ _optionsSel = (_optionsSel + kOptionsItemCount - 1) % kOptionsItemCount;
return true;
case Common::KEYCODE_DOWN:
case Common::KEYCODE_s:
- _optionsSel = (_optionsSel + 1) % 3;
+ _optionsSel = (_optionsSel + 1) % kOptionsItemCount;
+ return true;
+ case Common::KEYCODE_LEFT:
+ case Common::KEYCODE_a:
+ // Volume down when on volume row (row 6)
+ if (_optionsSel == 6) {
+ _optVolume = MAX(0, _optVolume - 5);
+ _vm->_mixer->setVolumeForSoundType(Audio::Mixer::kPlainSoundType,
+ (_optVolume * Audio::Mixer::kMaxChannelVolume) / 127);
+ ConfMan.setInt("music_volume", (_optVolume * 256) / 127);
+ ConfMan.setInt("sfx_volume", (_optVolume * 256) / 127);
+ ConfMan.setInt("speech_volume", (_optVolume * 256) / 127);
+ }
+ return true;
+ case Common::KEYCODE_RIGHT:
+ case Common::KEYCODE_d:
+ // Volume up when on volume row (row 6)
+ if (_optionsSel == 6) {
+ _optVolume = MIN(127, _optVolume + 5);
+ _vm->_mixer->setVolumeForSoundType(Audio::Mixer::kPlainSoundType,
+ (_optVolume * Audio::Mixer::kMaxChannelVolume) / 127);
+ ConfMan.setInt("music_volume", (_optVolume * 256) / 127);
+ ConfMan.setInt("sfx_volume", (_optVolume * 256) / 127);
+ ConfMan.setInt("speech_volume", (_optVolume * 256) / 127);
+ }
return true;
case Common::KEYCODE_RETURN:
case Common::KEYCODE_KP_ENTER:
@@ -91,7 +118,7 @@ bool InsaneRebel1::notifyEvent(const Common::Event &event) {
_vm->_smushVideoShouldFinish = true;
return true;
case Common::KEYCODE_ESCAPE:
- _optionsSel = 2; // Back
+ _optionsSel = kOptionsItemCount - 1; // Back
_menuConfirmed = true;
_vm->_smushVideoShouldFinish = true;
return true;
@@ -147,6 +174,13 @@ bool InsaneRebel1::notifyEvent(const Common::Event &event) {
}
}
+ // High scores: any key or click dismisses
+ if (_highScoresActive && (event.type == Common::EVENT_KEYDOWN ||
+ event.type == Common::EVENT_LBUTTONDOWN)) {
+ _vm->_smushVideoShouldFinish = true;
+ return true;
+ }
+
if (event.type == Common::EVENT_KEYDOWN && event.kbd.keycode == Common::KEYCODE_ESCAPE) {
if (_player) {
debug("Rebel1: ESC pressed - skipping video");
@@ -180,27 +214,57 @@ void InsaneRebel1::renderMainMenuOverlay(byte *dst, int pitch, int width, int he
drawFontBankString(dst, pitch, width, height, x, y, text);
};
+ if (_highScoresActive) {
+ // --- TOP PILOTS high score display ---
+ // Original renders over O1SCORE.ANM. Title appears after frame 20,
+ // entries fade in one per frame. We show all immediately.
+ const int titleW = getTalkTextWidth("TOP PILOTS");
+ drawTalkText((width - titleW) / 2, 10, "TOP PILOTS");
+
+ for (int i = 0; i < kHighScoreCount; i++) {
+ const int y = 25 + i * 14;
+ // Name (left side)
+ drawFontBankString(dst, pitch, width, height, 40, y, _highScores[i].name);
+ // Score (right side)
+ char scoreLine[32];
+ Common::sprintf_s(scoreLine, "<%ld", (long)_highScores[i].score);
+ drawFontBankString(dst, pitch, width, height, 220, y, scoreLine);
+ }
+ return;
+ }
+
if (_optionsActive) {
- // --- Options submenu ---
+ // --- Options submenu (matching original RunGameOptionsMenu) ---
static const char *kDiffNames[3] = { "EASY", "NORMAL", "HARD" };
const int titleW = getTalkTextWidth("GAME OPTIONS");
- drawTalkText((width - titleW) / 2, 36, "GAME OPTIONS");
-
- // Build dynamic option strings
- char diffLine[64];
- snprintf(diffLine, sizeof(diffLine), "DIFFICULTY: %s", kDiffNames[CLIP(_difficulty, 0, 2)]);
- const char *turbLine = _turbulenceEnabled ? "TURBULENCE: ON" : "TURBULENCE: OFF";
- const char *kOptionsItems[3] = { diffLine, turbLine, "BACK" };
+ drawTalkText((width - titleW) / 2, 30, "GAME OPTIONS");
+
+ // Build dynamic option strings for each row
+ char diffLine[64], volLine[64];
+ Common::sprintf_s(diffLine, "DIFFICULTY IS %s", kDiffNames[CLIP(_difficulty, 0, 2)]);
+ Common::sprintf_s(volLine, "VOLUME AT %d PERCENT", (_optVolume * 100) / 127);
+
+ const char *optItems[kOptionsItemCount] = {
+ "EXIT MENU",
+ _optMusicEnabled ? "MUSIC IS ON" : "MUSIC IS OFF",
+ _optSfxEnabled ? "SFX AND VOICE ARE ON" : "SFX AND VOICE ARE OFF",
+ _optTextEnabled ? "DIALOGUE TEXT IS ON" : "DIALOGUE TEXT IS OFF",
+ _optControlsYFlip ? "CONTROLS ARE Y-FLIPPED" : "CONTROLS ARE NORMAL",
+ _turbulenceEnabled ? "TURBULENCE IS ON" : "TURBULENCE IS OFF",
+ volLine,
+ diffLine,
+ "BACK"
+ };
- const int menuY = 60;
- const int rowH = 16;
+ const int menuY = 44;
+ const int rowH = 14;
- for (int i = 0; i < 3; i++) {
- const int textW = getTalkTextWidth(kOptionsItems[i]);
+ for (int i = 0; i < kOptionsItemCount; i++) {
+ const int textW = getTalkTextWidth(optItems[i]);
const int textX = (width - textW) / 2;
const int y = menuY + i * rowH;
- drawTalkText(textX, y + 1, kOptionsItems[i]);
+ drawTalkText(textX, y + 1, optItems[i]);
if (i == _optionsSel) {
byte highlightColor = ((_menuFrameCounter / 8) & 1) ? 248 : 240;
@@ -228,21 +292,21 @@ void InsaneRebel1::renderMainMenuOverlay(byte *dst, int pitch, int width, int he
drawTalkText((width - titleW) / 2, 30, "LEVEL SELECT");
static const char *kLevelItems[kRA1LevelSelectItemCount] = {
- " 1 FLIGHT TRAINING",
- " 2 ASTEROID FIELD",
- " 3 PLANET KOLAADOR",
- " 4 STAR DESTROYER",
- " 5 TATOOINE ATTACK",
- " 6 ASTEROID CHASE",
- " 7 PROBE DROIDS",
- " 8 IMPERIAL WALKERS",
- " 9 STORMTROOPERS",
- "10 REBEL TRANSPORT",
- "11 YAVIN TRAINING",
- "12 TIE ATTACK",
- "13 DEATH STAR SURFACE",
- "14 SURFACE CANNON",
- "15 DEATH STAR TRENCH",
+ " 1 TRAINING",
+ " 2 ASTEROIDS",
+ " 3 KOLAADOR",
+ " 4 STAR DESTR",
+ " 5 TATOOINE",
+ " 6 AST CHASE",
+ " 7 PROBES",
+ " 8 WALKERS",
+ " 9 TROOPERS",
+ "10 TRANSPORT",
+ "11 YAVIN",
+ "12 TIE ATK",
+ "13 DS SURFACE",
+ "14 CANNON",
+ "15 DS TRENCH",
"BACK"
};
@@ -379,17 +443,35 @@ void InsaneRebel1::runOptionsMenu() {
if (_menuConfirmed) {
switch (_optionsSel) {
- case 0:
- // Cycle difficulty
- _difficulty = (_difficulty + 1) % 3;
- loadTuningForLevel(0);
+ case 0: // EXIT MENU (same as BACK)
+ _optionsActive = false;
+ return;
+ case 1: // Toggle music
+ _optMusicEnabled = !_optMusicEnabled;
+ _vm->_mixer->muteSoundType(Audio::Mixer::kMusicSoundType, !_optMusicEnabled);
+ break;
+ case 2: // Toggle SFX + Voice
+ _optSfxEnabled = !_optSfxEnabled;
+ _vm->_mixer->muteSoundType(Audio::Mixer::kSFXSoundType, !_optSfxEnabled);
+ _vm->_mixer->muteSoundType(Audio::Mixer::kSpeechSoundType, !_optSfxEnabled);
break;
- case 1:
- // Toggle turbulence
+ case 3: // Toggle dialogue text
+ _optTextEnabled = !_optTextEnabled;
+ ConfMan.setBool("subtitles", _optTextEnabled);
+ break;
+ case 4: // Toggle Y-flip controls
+ _optControlsYFlip = !_optControlsYFlip;
+ break;
+ case 5: // Toggle turbulence
_turbulenceEnabled = !_turbulenceEnabled;
break;
- case 2:
- // Back to main menu
+ case 6: // Volume â adjusted via left/right in notifyEvent
+ break;
+ case 7: // Cycle difficulty
+ _difficulty = (_difficulty + 1) % 3;
+ loadTuningForLevel(0);
+ break;
+ case 8: // BACK
_optionsActive = false;
return;
}
@@ -427,4 +509,15 @@ int InsaneRebel1::runLevelSelectMenu() {
return 0;
}
+void InsaneRebel1::showHighScores() {
+ // Original plays O1SCORE.ANM with TOP PILOTS overlay, dismissable by any key.
+ _highScoresActive = true;
+ _menuActive = true;
+ _menuFrameCounter = 0;
+ clearVideoBuffer();
+ playCinematic("OPEN/O1SCORE.ANM");
+ _menuActive = false;
+ _highScoresActive = false;
+}
+
} // End of namespace Scumm
diff --git a/engines/scumm/insane/insane_rebel1_runlevels.cpp b/engines/scumm/insane/insane_rebel1_runlevels.cpp
index 2027d684974..29aaadb1f2e 100644
--- a/engines/scumm/insane/insane_rebel1_runlevels.cpp
+++ b/engines/scumm/insane/insane_rebel1_runlevels.cpp
@@ -1046,13 +1046,17 @@ void InsaneRebel1::runGame() {
switch (menuResult) {
case 1: {
- const int startLevel = CLIP<int>(_startLevel, 1, sizeof(kLevelRunners) / sizeof(kLevelRunners[0]));
+ // START NEW GAME â sequential play from _startLevel
+ const int numLevels = (int)(sizeof(kLevelRunners) / sizeof(kLevelRunners[0]));
+ const int startLevel = CLIP<int>(_startLevel, 1, numLevels);
bool completed = true;
for (int level = startLevel;
- level <= (int)(sizeof(kLevelRunners) / sizeof(kLevelRunners[0])) && completed && !_vm->shouldQuit();
+ level <= numLevels && completed && !_vm->shouldQuit();
++level) {
completed = (this->*kLevelRunners[level - 1])();
+ if (completed && level < numLevels)
+ _startLevel = level + 1;
}
_currentLevel = 0;
break;
@@ -1071,11 +1075,17 @@ void InsaneRebel1::runGame() {
}
break;
}
+ case 4:
+ // CONTINUE DEMO â attract mode.
+ // Original shows TOP PILOTS (O1SCORE.ANM) then loops O1OPEN.ANM.
+ showHighScores();
+ if (!_vm->shouldQuit())
+ playCinematic("OPEN/O1OPEN.ANM");
+ break;
case 5:
// Exit
return;
default:
- // Passcode, Demo â not yet implemented, return to menu
break;
}
}
Commit: 3ec6f05b036171e0c78850cc21e322dfe2c677b1
https://github.com/scummvm/scummvm/commit/3ec6f05b036171e0c78850cc21e322dfe2c677b1
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:42+02:00
Commit Message:
SCUMM: RA1: Add level 11 through 15 handlers
Changed paths:
engines/scumm/insane/insane_rebel1.cpp
engines/scumm/insane/insane_rebel1.h
engines/scumm/insane/insane_rebel1_iact.cpp
engines/scumm/insane/insane_rebel1_menu.cpp
engines/scumm/insane/insane_rebel1_runlevels.cpp
diff --git a/engines/scumm/insane/insane_rebel1.cpp b/engines/scumm/insane/insane_rebel1.cpp
index 1291a7b90f7..c49dfceb683 100644
--- a/engines/scumm/insane/insane_rebel1.cpp
+++ b/engines/scumm/insane/insane_rebel1.cpp
@@ -186,8 +186,11 @@ void InsaneRebel1::loadTuningForLevel(int level) {
_tuning.levelPts = kTuningTable[l][d][10];
_tuning.bonus = kTuningTable[l][d][11];
_tuning.flags = kTuningTable[l][d][12];
- // initLevelFromTuning (0x13E7B) copies tuning flags into g_hudDisableFlags (0x75FE).
+ // initLevelFromTuning (0x13E7B) copies tuning flags into g_hudDisableFlags (0x75FE)
+ // and clears protected targets (DAT_00007732/7734).
_gameplayFlags75fe = (uint16)_tuning.flags;
+ _protectedTargetA = 0;
+ _protectedTargetB = 0;
debug(1, "RA1: Loaded tuning level=%d diff=%d: roll=%d lift=%d slide=%d drift=%d snap=%d "
"miss=%d wham=%d shot=%d kill=%d time=%d levelPts=%d bonus=%d flags=0x%x",
@@ -295,7 +298,7 @@ InsaneRebel1::InsaneRebel1(ScummEngine_v7 *scumm) : Insane(), _vm(scumm) {
_optVolume = _vm->_mixer->getVolumeForSoundType(Audio::Mixer::kPlainSoundType) * 127 / Audio::Mixer::kMaxChannelVolume;
// Default high scores â from DS:0x1D0/0x298/0x2C0
- static const struct { const char *name; int32 score; byte gender; } kDefaultScores[kHighScoreCount] = {
+ static const struct { const char *name; int32 score; byte difficulty; } kDefaultScores[kHighScoreCount] = {
{"Vince", 10000, 2}, {"Tamlynn", 9000, 2}, {"Chip", 8000, 2},
{"Brett", 7000, 1}, {"Casey", 6000, 1}, {"Justin", 5000, 1},
{"Bill", 4000, 0}, {"Aaron", 3000, 0}, {"Mary", 2000, 0},
@@ -304,7 +307,7 @@ InsaneRebel1::InsaneRebel1(ScummEngine_v7 *scumm) : Insane(), _vm(scumm) {
for (int i = 0; i < kHighScoreCount; i++) {
Common::sprintf_s(_highScores[i].name, "<%s", kDefaultScores[i].name);
_highScores[i].score = kDefaultScores[i].score;
- _highScores[i].gender = kDefaultScores[i].gender;
+ _highScores[i].difficulty = kDefaultScores[i].difficulty;
}
_highScoresActive = false;
@@ -328,6 +331,11 @@ InsaneRebel1::InsaneRebel1(ScummEngine_v7 *scumm) : Insane(), _vm(scumm) {
_gostSlotIdx = 0;
_killCount = 0;
_lastHitTarget = 0;
+ _protectedTargetA = 0;
+ _protectedTargetB = 0;
+ _shieldGenHitsA = 0;
+ _shieldGenHitsB = 0;
+ _torpedoFired = false;
_walkerHealth = 100;
_walkerTimer = 0;
_walkerBranchChoice = 0;
diff --git a/engines/scumm/insane/insane_rebel1.h b/engines/scumm/insane/insane_rebel1.h
index f42f612e441..66a7a74ec62 100644
--- a/engines/scumm/insane/insane_rebel1.h
+++ b/engines/scumm/insane/insane_rebel1.h
@@ -124,6 +124,11 @@ private:
bool runLevel8();
bool runLevel9();
bool runLevel10();
+ bool runLevel11();
+ bool runLevel12();
+ bool runLevel13();
+ bool runLevel14();
+ bool runLevel15();
// Play a passive cinematic (no game callback, skippable)
// startFrame > 0: fast-forward (decode without display) to that frame
@@ -382,7 +387,7 @@ private:
struct HighScoreEntry {
char name[20]; // 0x14 bytes per entry (includes '<' prefix)
int32 score;
- byte gender; // 0/1/2 â tech font glyph '{','|','}'
+ byte difficulty; // 0/1/2 â tech font glyph '{','|','}' (easy/normal/hard)
};
HighScoreEntry _highScores[kHighScoreCount];
bool _highScoresActive; // True when showing TOP PILOTS overlay
@@ -433,6 +438,19 @@ private:
int16 _killCount; // 0x75D0: targets destroyed this stage
int16 _lastHitTarget; // 0x75D6: prevents double-hit on same target
+ // Protected target IDs â 0x7732/0x7734 in original
+ // Targets listed here can be hit repeatedly (no event mask toggle).
+ // Used by Level 4 (shield generators) and Level 15 (torpedo targets).
+ int16 _protectedTargetA; // 0x7732: protected objectId+1 (0=none)
+ int16 _protectedTargetB; // 0x7734: protected objectId+1 (0=none)
+
+ // Per-target hit counters for shield generator tracking (Level 4)
+ int16 _shieldGenHitsA; // Hits on _protectedTargetA
+ int16 _shieldGenHitsB; // Hits on _protectedTargetB
+
+ // Torpedo fired flag â set when torpedo hits in Phase 2 (Level 15)
+ bool _torpedoFired; // 0x7602 bit 1 in original
+
// Level 8 walker-specific state â RunLevel8Flow (0x18546)
int16 _walkerHealth; // Walker health percentage (0-100), init=100
int16 _walkerTimer; // Attack window countdown (100â0)
diff --git a/engines/scumm/insane/insane_rebel1_iact.cpp b/engines/scumm/insane/insane_rebel1_iact.cpp
index 699c181a71a..1ecda9e047b 100644
--- a/engines/scumm/insane/insane_rebel1_iact.cpp
+++ b/engines/scumm/insane/insane_rebel1_iact.cpp
@@ -372,6 +372,12 @@ void InsaneRebel1::applyFrameObjectHitState(int16 targetIdx) {
if (targetIdx < 0)
return;
+ // Protected targets (shield generators in Level 4, etc.) can be hit
+ // repeatedly â skip event mask toggle. Original: DAT_00007732/7734 check
+ // in HandleGameOp5A.
+ if (targetIdx + 1 == _protectedTargetA || targetIdx + 1 == _protectedTargetB)
+ return;
+
const int byteIndex = targetIdx >> 3;
if (byteIndex < 0 || byteIndex >= 0x96 || byteIndex >= kFrameObjectStateBytes)
return;
@@ -1146,9 +1152,45 @@ void InsaneRebel1::updateAsteroidPhysics() {
_frameCounter++;
+ // Level 4 Phase 2: enable torpedo mode at frame 0x3E
if (_currentLevel == 3 && _levelGameplayPhase == 2 && _frameCounter == 0x3E)
_gameplayFlags75ff |= 2;
+ // Level 4 Phase 1: track shield generator hits per frame.
+ // Original (RunLevel4Flow): g_recentKillObjectIdPlus1 checked every frame.
+ // When enough hits accumulated (>0x30), generator is "destroyed" (clear protectedTarget).
+ // When both destroyed for 60 frames, phase ends.
+ if (_currentLevel == 3 && _levelGameplayPhase == 1) {
+ if (_lastHitTarget == _protectedTargetA && _protectedTargetA != 0)
+ _shieldGenHitsA++;
+ if (_lastHitTarget == _protectedTargetB && _protectedTargetB != 0)
+ _shieldGenHitsB++;
+
+ if (_shieldGenHitsA > 0x30)
+ _protectedTargetA = 0;
+ if (_shieldGenHitsB > 0x30)
+ _protectedTargetB = 0;
+
+ // Both destroyed: count down 60 frames then end phase
+ if (_protectedTargetA == 0 && _protectedTargetB == 0 &&
+ _shieldGenHitsA > 0x30 && _shieldGenHitsB > 0x30) {
+ _shieldGenHitsA++; // reuse as countdown
+ if (_shieldGenHitsA > 0x30 + 0x3C)
+ _vm->_smushVideoShouldFinish = true;
+ }
+ }
+
+ // Level 15 Phase 2: enable torpedo and end on hit.
+ // Original (RunLevel1GameLoop): torpedo at frame 0x18A, ends when DAT_00007602 & 2.
+ if (_currentLevel == 14 && _levelGameplayPhase == 2) {
+ if (_frameCounter == 0x18A)
+ _gameplayFlags75ff |= 2;
+ if (_killCount > 0) {
+ _torpedoFired = true;
+ _vm->_smushVideoShouldFinish = true;
+ }
+ }
+
checkDynamicLevelBranch();
debug(7, "RA1 asteroid: pos=(%d,%d) avg=(%d,%d) view=(%d,%d) health=%d flash=%d",
diff --git a/engines/scumm/insane/insane_rebel1_menu.cpp b/engines/scumm/insane/insane_rebel1_menu.cpp
index 231b94fdac8..bcb9da88cda 100644
--- a/engines/scumm/insane/insane_rebel1_menu.cpp
+++ b/engines/scumm/insane/insane_rebel1_menu.cpp
@@ -225,9 +225,12 @@ void InsaneRebel1::renderMainMenuOverlay(byte *dst, int pitch, int width, int he
const int y = 25 + i * 14;
// Name (left side)
drawFontBankString(dst, pitch, width, height, 40, y, _highScores[i].name);
- // Score (right side)
+ // Score + difficulty glyph (right side) â original format "<%ld %c"
+ // Difficulty byte 0/1/2 + 0x7B = '{','|','}' tech font glyphs (easy/normal/hard)
char scoreLine[32];
- Common::sprintf_s(scoreLine, "<%ld", (long)_highScores[i].score);
+ Common::sprintf_s(scoreLine, "<%ld %c",
+ (long)_highScores[i].score,
+ (char)(_highScores[i].difficulty + 0x7B));
drawFontBankString(dst, pitch, width, height, 220, y, scoreLine);
}
return;
diff --git a/engines/scumm/insane/insane_rebel1_runlevels.cpp b/engines/scumm/insane/insane_rebel1_runlevels.cpp
index 29aaadb1f2e..76a66edbdca 100644
--- a/engines/scumm/insane/insane_rebel1_runlevels.cpp
+++ b/engines/scumm/insane/insane_rebel1_runlevels.cpp
@@ -367,12 +367,23 @@ bool InsaneRebel1::runLevel4() {
_killCount = 0;
_levelGameplayPhase = 0;
+ // Phase 1: Destroy two shield generators.
+ // Original sets DAT_00007732=0x39, DAT_00007734=0x3A â protected target IDs
+ // that can be hit repeatedly without event mask toggle.
+ _protectedTargetA = 0x39;
+ _protectedTargetB = 0x3A;
+ _shieldGenHitsA = 0;
+ _shieldGenHitsB = 0;
_levelGameplayPhase = 1;
playInteractiveVideo("LVL4/L4PLAY1.ANM");
+ _protectedTargetA = 0;
+ _protectedTargetB = 0;
if (_vm->shouldQuit())
return false;
if (_health >= 0) {
+ // Phase 2: Torpedo run â torpedo enabled at frame 0x3E by IACT handler.
+ // killCount > 0 = torpedo hit, killCount == 0 = missed.
_activeGameOpcode = 0;
_gameLatch5D = 0;
_gameLatch5F = 0;
@@ -385,6 +396,7 @@ bool InsaneRebel1::runLevel4() {
}
if (_health >= 0) {
+ // L4END1 = torpedo hit, L4END2 = torpedo missed
playCinematic((_killCount != 0) ? "LVL4/L4END1.ANM" : "LVL4/L4END2.ANM");
return !_vm->shouldQuit();
}
@@ -1016,6 +1028,431 @@ bool InsaneRebel1::runLevel10() {
return false;
}
+// Level 11 flow (RunLevel11Flow, 0x19F67): Yavin Training
+// Turret-style level. Single interactive phase with kill-count retry.
+// Original: L11INTRO â L11PLAY (turret, killCount>4 to pass) â L11RETRY â retry/L11END
+bool InsaneRebel1::runLevel11() {
+ debug(1, "InsaneRebel1: Running level 11");
+
+ _currentLevel = 10;
+ loadLevelSprites(11);
+ loadTuningForLevel(10);
+
+ beginLevelTitleOverlay(10);
+ playCinematic("LVL11/L11INTRO.ANM");
+ if (_vm->shouldQuit())
+ return false;
+
+ while (!_vm->shouldQuit()) {
+ _flyControlMode = 1;
+ _health = kMaxHealth;
+ _damageFlags = 0;
+ _prevDamageFlags = 0;
+ _damageCooldown = 0;
+ _deathTimer = 0;
+ _screenFlash = 0;
+ _frameCounter = 0;
+ _gameCounter = 0;
+ _activeGameOpcode = 0;
+ _gameLatch5D = 0;
+ _gameLatch5F = 0;
+ _gameplayFlags75ff = 0;
+ _killCount = 0;
+ _targetCount = 0;
+ _prevTargetCount = 0;
+ _lastHitTarget = 0;
+ _shipPosX = kRA1CenterX;
+ _shipPosY = kRA1CenterY;
+ _shipDirIndex = 17;
+ _rollAccum = 0;
+ _liftSmooth = 0;
+ _posAccumX = 0;
+ _posAccumY = 0;
+ _perspectiveX = 0;
+ _perspectiveY = 0;
+ _levelGameplayPhase = 0;
+ memset(_inputHistoryX, 0, sizeof(_inputHistoryX));
+ memset(_inputHistoryY, 0, sizeof(_inputHistoryY));
+ memset(_viewHistoryX, 0, sizeof(_viewHistoryX));
+ memset(_viewHistoryY, 0, sizeof(_viewHistoryY));
+ _avgInputX = 0;
+ _avgInputY = 0;
+ _turretEmitterLeftX = 25;
+ _turretEmitterLeftY = 15;
+
+ while (!_vm->shouldQuit()) {
+ playInteractiveVideo("LVL11/L11PLAY.ANM");
+ if (_vm->shouldQuit())
+ return false;
+
+ if (_health < 0)
+ break;
+
+ // Original: killCount > 4 means pass
+ if (_killCount > 4)
+ break;
+
+ // Not enough kills â retry
+ playCinematic("LVL11/L11RETRY.ANM");
+ if (_vm->shouldQuit())
+ return false;
+ }
+
+ if (_health >= 0) {
+ playCinematic("LVL11/L11END.ANM");
+ return !_vm->shouldQuit();
+ }
+
+ if (_lives > 0) {
+ playCinematic("LVL11/L11NEW.ANM");
+ if (_vm->shouldQuit())
+ return false;
+ _lives--;
+ continue;
+ }
+
+ playCinematic("LVL11/L11DEATH.ANM");
+ return false;
+ }
+
+ return false;
+}
+
+// Level 12 flow (RunLevel12Flow, 0x1A2DD): TIE Attack
+// Single interactive phase with mid-level retry mechanism.
+// Original: L12INTRO â L12PLAY â (retry at specific frame) â L12END
+bool InsaneRebel1::runLevel12() {
+ debug(1, "InsaneRebel1: Running level 12");
+
+ _currentLevel = 11;
+ loadLevelSprites(12);
+ loadTuningForLevel(11);
+
+ beginLevelTitleOverlay(11);
+ playCinematic("LVL12/L12INTRO.ANM");
+ if (_vm->shouldQuit())
+ return false;
+
+ while (!_vm->shouldQuit()) {
+ _flyControlMode = 1;
+ _health = kMaxHealth;
+ _damageFlags = 0;
+ _prevDamageFlags = 0;
+ _damageCooldown = 0;
+ _deathTimer = 0;
+ _screenFlash = 0;
+ _frameCounter = 0;
+ _gameCounter = 0;
+ _activeGameOpcode = 0;
+ _gameLatch5D = 0;
+ _gameLatch5F = 0;
+ _gameplayFlags75ff = 0;
+ _killCount = 0;
+ _targetCount = 0;
+ _prevTargetCount = 0;
+ _lastHitTarget = 0;
+ _shipPosX = kRA1CenterX;
+ _shipPosY = kRA1CenterY;
+ _shipDirIndex = 17;
+ _rollAccum = 0;
+ _liftSmooth = 0;
+ _posAccumX = 0;
+ _posAccumY = 0;
+ _perspectiveX = 0;
+ _perspectiveY = 0;
+ _levelGameplayPhase = 0;
+ memset(_inputHistoryX, 0, sizeof(_inputHistoryX));
+ memset(_inputHistoryY, 0, sizeof(_inputHistoryY));
+ memset(_viewHistoryX, 0, sizeof(_viewHistoryX));
+ memset(_viewHistoryY, 0, sizeof(_viewHistoryY));
+ _avgInputX = 0;
+ _avgInputY = 0;
+
+ playInteractiveVideo("LVL12/L12PLAY.ANM");
+ if (_vm->shouldQuit())
+ return false;
+
+ if (_health >= 0) {
+ playCinematic("LVL12/L12END.ANM");
+ return !_vm->shouldQuit();
+ }
+
+ if (_lives > 0) {
+ playCinematic("LVL12/L12NEW.ANM");
+ if (_vm->shouldQuit())
+ return false;
+ _lives--;
+ continue;
+ }
+
+ playCinematic("LVL12/L12DEATH.ANM");
+ return false;
+ }
+
+ return false;
+}
+
+// Level 13 flow (RunLevel13Flow, 0x1A6E3): Death Star Surface
+// Flight level with enemy projectile system (original has 5-slot projectile tracking).
+// Original: L13INTRO â L13PLAY â L13END/L13NEW/L13DEATH
+bool InsaneRebel1::runLevel13() {
+ debug(1, "InsaneRebel1: Running level 13");
+
+ _currentLevel = 12;
+ loadLevelSprites(13);
+ loadTuningForLevel(12);
+
+ beginLevelTitleOverlay(12);
+ playCinematic("LVL13/L13INTRO.ANM");
+ if (_vm->shouldQuit())
+ return false;
+
+ while (!_vm->shouldQuit()) {
+ _flyControlMode = 1;
+ _health = kMaxHealth;
+ _damageFlags = 0;
+ _prevDamageFlags = 0;
+ _damageCooldown = 0;
+ _deathTimer = 0;
+ _screenFlash = 0;
+ _frameCounter = 0;
+ _gameCounter = 0;
+ _activeGameOpcode = 0;
+ _gameLatch5D = 0;
+ _gameLatch5F = 0;
+ _gameplayFlags75ff = 0;
+ _killCount = 0;
+ _targetCount = 0;
+ _prevTargetCount = 0;
+ _lastHitTarget = 0;
+ _shipPosX = kRA1CenterX;
+ _shipPosY = kRA1CenterY;
+ _shipDirIndex = 17;
+ _rollAccum = 0;
+ _liftSmooth = 0;
+ _posAccumX = 0;
+ _posAccumY = 0;
+ _perspectiveX = 0;
+ _perspectiveY = 0;
+ _levelGameplayPhase = 0;
+ memset(_inputHistoryX, 0, sizeof(_inputHistoryX));
+ memset(_inputHistoryY, 0, sizeof(_inputHistoryY));
+ memset(_viewHistoryX, 0, sizeof(_viewHistoryX));
+ memset(_viewHistoryY, 0, sizeof(_viewHistoryY));
+ _avgInputX = 0;
+ _avgInputY = 0;
+
+ playInteractiveVideo("LVL13/L13PLAY.ANM");
+ if (_vm->shouldQuit())
+ return false;
+
+ if (_health >= 0) {
+ playCinematic("LVL13/L13END.ANM");
+ return !_vm->shouldQuit();
+ }
+
+ if (_lives > 0) {
+ playCinematic("LVL13/L13NEW.ANM");
+ if (_vm->shouldQuit())
+ return false;
+ _lives--;
+ continue;
+ }
+
+ playCinematic("LVL13/L13DEATH.ANM");
+ return false;
+ }
+
+ return false;
+}
+
+// Level 14 flow (RunLevel14Flow, 0x1ACB0): Surface Cannon
+// Two interactive phases: L14PLAY (targeting cannons) + L14PLAY2 (exhaust port approach).
+// Original: L14INTRO â L14PLAY â L14PLAY2 â L14END/L14NEW/L14DEATH
+bool InsaneRebel1::runLevel14() {
+ debug(1, "InsaneRebel1: Running level 14");
+
+ _currentLevel = 13;
+ loadLevelSprites(14);
+ loadTuningForLevel(13);
+
+ beginLevelTitleOverlay(13);
+ playCinematic("LVL14/L14INTRO.ANM");
+ if (_vm->shouldQuit())
+ return false;
+
+ while (!_vm->shouldQuit()) {
+ _flyControlMode = 1;
+ _health = kMaxHealth;
+ _damageFlags = 0;
+ _prevDamageFlags = 0;
+ _damageCooldown = 0;
+ _deathTimer = 0;
+ _screenFlash = 0;
+ _frameCounter = 0;
+ _gameCounter = 0;
+ _activeGameOpcode = 0;
+ _gameLatch5D = 0;
+ _gameLatch5F = 0;
+ _gameplayFlags75ff = 0;
+ _killCount = 0;
+ _targetCount = 0;
+ _prevTargetCount = 0;
+ _lastHitTarget = 0;
+ _shipPosX = kRA1CenterX;
+ _shipPosY = kRA1CenterY;
+ _shipDirIndex = 17;
+ _rollAccum = 0;
+ _liftSmooth = 0;
+ _posAccumX = 0;
+ _posAccumY = 0;
+ _perspectiveX = 0;
+ _perspectiveY = 0;
+ _levelGameplayPhase = 0;
+ memset(_inputHistoryX, 0, sizeof(_inputHistoryX));
+ memset(_inputHistoryY, 0, sizeof(_inputHistoryY));
+ memset(_viewHistoryX, 0, sizeof(_viewHistoryX));
+ memset(_viewHistoryY, 0, sizeof(_viewHistoryY));
+ _avgInputX = 0;
+ _avgInputY = 0;
+
+ // Phase 1: targeting surface cannons
+ playInteractiveVideo("LVL14/L14PLAY.ANM");
+ if (_vm->shouldQuit())
+ return false;
+
+ if (_health >= 0) {
+ // Phase 2: exhaust port approach
+ _activeGameOpcode = 0;
+ _gameLatch5D = 0;
+ _gameLatch5F = 0;
+ _gameplayFlags75ff = 0;
+ _killCount = 0;
+
+ playInteractiveVideo("LVL14/L14PLAY2.ANM");
+ if (_vm->shouldQuit())
+ return false;
+ }
+
+ if (_health >= 0) {
+ playCinematic("LVL14/L14END.ANM");
+ return !_vm->shouldQuit();
+ }
+
+ if (_lives > 0) {
+ playCinematic("LVL14/L14NEW.ANM");
+ if (_vm->shouldQuit())
+ return false;
+ _lives--;
+ continue;
+ }
+
+ playCinematic("LVL14/L14DEATH.ANM");
+ return false;
+ }
+
+ return false;
+}
+
+// Level 15 flow (RunLevel1GameLoop, 0x1B283): Death Star Trench
+// Two interactive phases with mid-level cutscene.
+// Original: L15INTRO â L15PLAY1 (trench run) â L15INTR2 (torpedo lock cutscene)
+// â L15PLAY2 (final approach + torpedo) â L15END1/L15NEW/L15DEATH
+bool InsaneRebel1::runLevel15() {
+ debug(1, "InsaneRebel1: Running level 15");
+
+ _currentLevel = 14;
+ loadLevelSprites(15);
+ loadTuningForLevel(14);
+
+ beginLevelTitleOverlay(14);
+ playCinematic("LVL15/L15INTRO.ANM");
+ if (_vm->shouldQuit())
+ return false;
+
+ while (!_vm->shouldQuit()) {
+ _flyControlMode = 1;
+ _health = kMaxHealth;
+ _damageFlags = 0;
+ _prevDamageFlags = 0;
+ _damageCooldown = 0;
+ _deathTimer = 0;
+ _screenFlash = 0;
+ _frameCounter = 0;
+ _gameCounter = 0;
+ _activeGameOpcode = 0;
+ _gameLatch5D = 0;
+ _gameLatch5F = 0;
+ _gameplayFlags75ff = 0;
+ _killCount = 0;
+ _targetCount = 0;
+ _prevTargetCount = 0;
+ _lastHitTarget = 0;
+ _shipPosX = kRA1CenterX;
+ _shipPosY = kRA1CenterY;
+ _shipDirIndex = 17;
+ _rollAccum = 0;
+ _liftSmooth = 0;
+ _posAccumX = 0;
+ _posAccumY = 0;
+ _perspectiveX = 0;
+ _perspectiveY = 0;
+ _levelGameplayPhase = 0;
+ memset(_inputHistoryX, 0, sizeof(_inputHistoryX));
+ memset(_inputHistoryY, 0, sizeof(_inputHistoryY));
+ memset(_viewHistoryX, 0, sizeof(_viewHistoryX));
+ memset(_viewHistoryY, 0, sizeof(_viewHistoryY));
+ _avgInputX = 0;
+ _avgInputY = 0;
+
+ // Phase 1: trench run
+ _levelGameplayPhase = 1;
+ playInteractiveVideo("LVL15/L15PLAY1.ANM");
+ if (_vm->shouldQuit())
+ return false;
+
+ if (_health >= 0) {
+ // Torpedo lock cutscene
+ playCinematic("LVL15/L15INTR2.ANM");
+ if (_vm->shouldQuit())
+ return false;
+
+ // Phase 2: final approach and torpedo shot.
+ // Original: torpedo enabled at frame 0x18A via _gameplayFlags75ff |= 2.
+ // _torpedoFired set by IACT handler when killCount > 0 (torpedo hits exhaust port).
+ _activeGameOpcode = 0;
+ _gameLatch5D = 0;
+ _gameLatch5F = 0;
+ _gameplayFlags75ff = 0;
+ _killCount = 0;
+ _torpedoFired = false;
+ _levelGameplayPhase = 2;
+
+ playInteractiveVideo("LVL15/L15PLAY2.ANM");
+ if (_vm->shouldQuit())
+ return false;
+ }
+
+ if (_health >= 0) {
+ playCinematic("LVL15/L15END1.ANM");
+ return !_vm->shouldQuit();
+ }
+
+ if (_lives > 0) {
+ playCinematic("LVL15/L15NEW.ANM");
+ if (_vm->shouldQuit())
+ return false;
+ _lives--;
+ continue;
+ }
+
+ playCinematic("LVL15/L15DEATH.ANM");
+ return false;
+ }
+
+ return false;
+}
+
// Main game entry point â called from ScummEngine::go().
// Matches original flow at 0x15597: intro â menu â level.
void InsaneRebel1::runGame() {
@@ -1030,7 +1467,12 @@ void InsaneRebel1::runGame() {
&InsaneRebel1::runLevel7,
&InsaneRebel1::runLevel8,
&InsaneRebel1::runLevel9,
- &InsaneRebel1::runLevel10
+ &InsaneRebel1::runLevel10,
+ &InsaneRebel1::runLevel11,
+ &InsaneRebel1::runLevel12,
+ &InsaneRebel1::runLevel13,
+ &InsaneRebel1::runLevel14,
+ &InsaneRebel1::runLevel15
};
// Play intro sequence (logo + opening)
Commit: 730896f1c30379ad671f078d397c0b3731450f78
https://github.com/scummvm/scummvm/commit/730896f1c30379ad671f078d397c0b3731450f78
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:42+02:00
Commit Message:
SCUMM: RA1: Add missing globals
Changed paths:
engines/scumm/insane/insane_rebel1.cpp
engines/scumm/insane/insane_rebel1.h
engines/scumm/insane/insane_rebel1_iact.cpp
engines/scumm/insane/insane_rebel1_render.cpp
engines/scumm/insane/insane_rebel1_runlevels.cpp
diff --git a/engines/scumm/insane/insane_rebel1.cpp b/engines/scumm/insane/insane_rebel1.cpp
index c49dfceb683..4d4793f4c3c 100644
--- a/engines/scumm/insane/insane_rebel1.cpp
+++ b/engines/scumm/insane/insane_rebel1.cpp
@@ -267,6 +267,11 @@ InsaneRebel1::InsaneRebel1(ScummEngine_v7 *scumm) : Insane(), _vm(scumm) {
_deathTimer = 0;
_screenFlash = 0;
_frameCounter = 0;
+ _screenShakeEnabled = false;
+ _deathCauseIndicator = 0;
+ _hudRenderFlag = 0;
+ _hudDirtyFlag = 0;
+ _maxChapterUnlocked = 0;
_interactiveVideoActive = false;
_gameCounter = 0;
_pathBranchEnabled = false;
diff --git a/engines/scumm/insane/insane_rebel1.h b/engines/scumm/insane/insane_rebel1.h
index 66a7a74ec62..93b0601d0c5 100644
--- a/engines/scumm/insane/insane_rebel1.h
+++ b/engines/scumm/insane/insane_rebel1.h
@@ -318,6 +318,11 @@ private:
int16 _deathTimer; // 0x756A: death animation countdown (30 on death)
int16 _screenFlash; // 0x7736: screen flash timer on hit
uint32 _frameCounter; // 0x7740: global frame counter
+ bool _screenShakeEnabled; // 0x41AC: when true, SetCameraOffset adds ±2 random jitter
+ byte _deathCauseIndicator; // 0x772E: non-zero = player died; selects death animation variant
+ byte _hudRenderFlag; // 0x7600: 0xFF when HUD should render (set by combat mode handlers)
+ byte _hudDirtyFlag; // 0x7601: 0xFF after HUD redraw (set by renderHUD)
+ int16 _maxChapterUnlocked; // 0x7730: highest chapter with valid passcode (0=none, set on completion)
static const int16 kMaxHealth = 98;
static const int16 kDeathTimerInit = 30;
diff --git a/engines/scumm/insane/insane_rebel1_iact.cpp b/engines/scumm/insane/insane_rebel1_iact.cpp
index 1ecda9e047b..0a9ec942e9e 100644
--- a/engines/scumm/insane/insane_rebel1_iact.cpp
+++ b/engines/scumm/insane/insane_rebel1_iact.cpp
@@ -771,12 +771,17 @@ void InsaneRebel1::updateShipPhysics() {
}
}
- // --- Step 8: Perspective offsets ---
+ // --- Step 8: Perspective offsets (SetCameraOffset) ---
// FUN_1DEB5 computes these linearly from ship offsets:
// viewX = clamp((_74BA + 0x20), 0, 0x40)
// viewY = clamp((_74BC + 0x17), 0, 0x2E)
_perspectiveX = CLIP<int16>((int16)(_shipPosX - kRA1CenterX + 0x20), 0, 0x40);
_perspectiveY = CLIP<int16>((int16)(_shipPosY - kRA1CenterY + 0x17), 0, 0x2E);
+ // Screen shake: when enabled, add random ±2 jitter (original SetCameraOffset at 0x22514)
+ if (_screenShakeEnabled) {
+ _perspectiveX = CLIP<int16>((int16)(_perspectiveX + (_vm->_rnd.getRandomNumber(4) - 2)), 0, 0x40);
+ _perspectiveY = CLIP<int16>((int16)(_perspectiveY + (_vm->_rnd.getRandomNumber(4) - 2)), 0, 0x2E);
+ }
// FUN_1DEB5 updates the curve table via FUN_22549 after SetCameraOffset.
// The full DOS path blends a few roll-history terms; use the current roll
@@ -825,8 +830,14 @@ void InsaneRebel1::updateShipPhysics() {
if (_damageFlags & 0x16)
_health -= _tuning.wham;
- if (_health < 0)
+ if (_health < 0) {
_deathTimer = kDeathTimerInit;
+ // g_deathCauseIndicator (0x772E) â set based on damage source
+ if (_damageFlags & 0x80)
+ _deathCauseIndicator = 2; // Projectile hit death
+ else
+ _deathCauseIndicator = 1; // Collision death
+ }
_prevDamageFlags = _damageFlags;
_damageCooldown = kDamageCooldownInit;
@@ -849,9 +860,11 @@ void InsaneRebel1::updateShipPhysics() {
_score += _tuning.time;
}
- // Screen flash decay
- if (_screenFlash > 0)
+ // Screen flash decay â screen shake follows flash (EnableScreenShake/DisableScreenShake at 0x224ED)
+ if (_screenFlash > 0) {
_screenFlash--;
+ _screenShakeEnabled = (_screenFlash > 0);
+ }
// Clear per-frame damage flags
_damageFlags = 0;
@@ -920,8 +933,13 @@ void InsaneRebel1::updateTurretPhysics() {
else
_health -= _tuning.wham;
- if (_health < 0)
+ if (_health < 0) {
_deathTimer = kDeathTimerInit;
+ if (_damageFlags & 0x80)
+ _deathCauseIndicator = 2;
+ else
+ _deathCauseIndicator = 1;
+ }
_prevDamageFlags = _damageFlags;
_damageCooldown = kDamageCooldownInit;
@@ -1012,8 +1030,10 @@ void InsaneRebel1::updateTurretPhysics() {
_score += _tuning.time;
}
- if (_screenFlash > 0)
+ if (_screenFlash > 0) {
_screenFlash--;
+ _screenShakeEnabled = (_screenFlash > 0);
+ }
_gameLatch5D = 0;
_gameLatch5F = 0;
@@ -1081,6 +1101,10 @@ void InsaneRebel1::updateAsteroidPhysics() {
_health -= _tuning.wham;
if (_health < 0) {
_deathTimer = 15; // 0x0F â shorter than Level 1's 30
+ if (_damageFlags & 0x80)
+ _deathCauseIndicator = 2;
+ else
+ _deathCauseIndicator = 1;
}
_prevDamageFlags = _damageFlags;
_damageFlags = 0;
@@ -1095,9 +1119,10 @@ void InsaneRebel1::updateAsteroidPhysics() {
_deathTimer--;
}
- // Screen flash countdown
+ // Screen flash countdown â screen shake follows flash
if (_screenFlash > 0) {
_screenFlash--;
+ _screenShakeEnabled = (_screenFlash > 0);
}
// --- Cursor and perspective smoothing (FUN_1CDA7) ---
@@ -1292,8 +1317,10 @@ void InsaneRebel1::updateOnFootPhysics() {
// On-foot uses single tuning value (DAT_00001b29 offset = miss) for all damage types.
if (_damageFlags != 0 && _damageCooldown == 0 && _health >= 0 && _deathTimer < 1) {
_health -= _tuning.miss;
- if (_health < 0)
+ if (_health < 0) {
_deathTimer = 15;
+ _deathCauseIndicator = (_damageFlags & 0x80) ? 2 : 1;
+ }
_prevDamageFlags = _damageFlags;
_damageCooldown = 3;
_screenFlash = 5;
@@ -1315,8 +1342,10 @@ void InsaneRebel1::updateOnFootPhysics() {
if (_deathTimer > 1 && _health < 0)
_deathTimer--;
- if (_screenFlash > 0)
+ if (_screenFlash > 0) {
_screenFlash--;
+ _screenShakeEnabled = (_screenFlash > 0);
+ }
_damageFlags = 0;
_frameCounter++;
@@ -1348,6 +1377,7 @@ void InsaneRebel1::handleGameChunk(int32 subSize, Common::SeekableReadStream &b)
_damageCooldown = 0;
_deathTimer = 0;
_screenFlash = 0;
+ _screenShakeEnabled = false;
_gameLatch5D = 0;
_gameLatch5F = 0;
_driftParam = 0;
diff --git a/engines/scumm/insane/insane_rebel1_render.cpp b/engines/scumm/insane/insane_rebel1_render.cpp
index c498bc388a4..49283a5e2a4 100644
--- a/engines/scumm/insane/insane_rebel1_render.cpp
+++ b/engines/scumm/insane/insane_rebel1_render.cpp
@@ -368,6 +368,9 @@ void InsaneRebel1::procPostRendering(byte *renderBitmap, int32 codecparam, int32
if (height == 0) height = _screenHeight;
int pitch = width;
+ // Set HUD render flag when interactive gameplay is active (0x7600)
+ _hudRenderFlag = 0xFF;
+
const bool haveFrameGameOpcodes = (_frameGameOpcodeMask != 0);
const bool asteroidMode = hasFrameGameOpcode(0x0B) ||
(!haveFrameGameOpcodes && _activeGameOpcode == 0x0B);
@@ -1179,6 +1182,7 @@ void InsaneRebel1::renderHUD(byte *dst, int pitch, int width, int height) {
}
}
+ _hudDirtyFlag = 0xFF; // Mark HUD as freshly drawn (0x7601)
}
// Attack window frame tables â RunLevel8Flow (0x18546), data at 0x236D/0x2373/0x2379.
diff --git a/engines/scumm/insane/insane_rebel1_runlevels.cpp b/engines/scumm/insane/insane_rebel1_runlevels.cpp
index 76a66edbdca..6e88dd22332 100644
--- a/engines/scumm/insane/insane_rebel1_runlevels.cpp
+++ b/engines/scumm/insane/insane_rebel1_runlevels.cpp
@@ -128,6 +128,8 @@ bool InsaneRebel1::runLevel1() {
_damageCooldown = 0;
_deathTimer = 0;
_screenFlash = 0;
+ _screenShakeEnabled = false;
+ _deathCauseIndicator = 0;
_frameCounter = 0;
_gameCounter = 0;
_activeGameOpcode = 0;
@@ -189,6 +191,7 @@ bool InsaneRebel1::runLevel1() {
if (_killCount > 4) {
playCinematic("LVL1/L1END.ANM");
+ _maxChapterUnlocked = MAX(_maxChapterUnlocked, (int16)1);
return !_vm->shouldQuit();
}
@@ -240,6 +243,8 @@ bool InsaneRebel1::runLevel2() {
_damageCooldown = 0;
_deathTimer = 0;
_screenFlash = 0;
+ _screenShakeEnabled = false;
+ _deathCauseIndicator = 0;
_frameCounter = 0;
_gameCounter = 0;
memset(_inputHistoryX, 0, sizeof(_inputHistoryX));
@@ -259,6 +264,7 @@ bool InsaneRebel1::runLevel2() {
if (_health >= 0) {
playCinematic("LVL2/L2END.ANM");
+ _maxChapterUnlocked = MAX(_maxChapterUnlocked, (int16)2);
return !_vm->shouldQuit();
}
@@ -298,6 +304,8 @@ bool InsaneRebel1::runLevel3() {
_damageCooldown = 0;
_deathTimer = 0;
_screenFlash = 0;
+ _screenShakeEnabled = false;
+ _deathCauseIndicator = 0;
_frameCounter = 0;
_gameCounter = 0;
_activeGameOpcode = 0;
@@ -319,6 +327,7 @@ bool InsaneRebel1::runLevel3() {
if (_health >= 0) {
playCinematic("LVL3/L3END.ANM");
+ _maxChapterUnlocked = MAX(_maxChapterUnlocked, (int16)3);
return !_vm->shouldQuit();
}
@@ -358,6 +367,8 @@ bool InsaneRebel1::runLevel4() {
_damageCooldown = 0;
_deathTimer = 0;
_screenFlash = 0;
+ _screenShakeEnabled = false;
+ _deathCauseIndicator = 0;
_frameCounter = 0;
_gameCounter = 0;
_activeGameOpcode = 0;
@@ -398,6 +409,7 @@ bool InsaneRebel1::runLevel4() {
if (_health >= 0) {
// L4END1 = torpedo hit, L4END2 = torpedo missed
playCinematic((_killCount != 0) ? "LVL4/L4END1.ANM" : "LVL4/L4END2.ANM");
+ _maxChapterUnlocked = MAX(_maxChapterUnlocked, (int16)4);
return !_vm->shouldQuit();
}
@@ -437,6 +449,8 @@ bool InsaneRebel1::runLevel5() {
_damageCooldown = 0;
_deathTimer = 0;
_screenFlash = 0;
+ _screenShakeEnabled = false;
+ _deathCauseIndicator = 0;
_frameCounter = 0;
_gameCounter = 0;
_activeGameOpcode = 0;
@@ -497,6 +511,7 @@ bool InsaneRebel1::runLevel5() {
if (_health >= 0) {
playCinematic("LVL5/L5END.ANM");
+ _maxChapterUnlocked = MAX(_maxChapterUnlocked, (int16)5);
return !_vm->shouldQuit();
}
@@ -535,6 +550,8 @@ bool InsaneRebel1::runLevel6() {
_damageCooldown = 0;
_deathTimer = 0;
_screenFlash = 0;
+ _screenShakeEnabled = false;
+ _deathCauseIndicator = 0;
_frameCounter = 0;
_gameCounter = 0;
_activeGameOpcode = 0;
@@ -556,6 +573,7 @@ bool InsaneRebel1::runLevel6() {
if (_health >= 0) {
playCinematic("LVL6/L6END.ANM");
+ _maxChapterUnlocked = MAX(_maxChapterUnlocked, (int16)6);
return !_vm->shouldQuit();
}
@@ -603,6 +621,8 @@ bool InsaneRebel1::runLevel7() {
_damageCooldown = 0;
_deathTimer = 0;
_screenFlash = 0;
+ _screenShakeEnabled = false;
+ _deathCauseIndicator = 0;
_frameCounter = 0;
_gameCounter = 0;
_activeGameOpcode = 0;
@@ -652,6 +672,7 @@ bool InsaneRebel1::runLevel7() {
if (_health >= 0) {
playCinematic("LVL7/L7END.ANM");
+ _maxChapterUnlocked = MAX(_maxChapterUnlocked, (int16)7);
return !_vm->shouldQuit();
}
@@ -696,6 +717,8 @@ bool InsaneRebel1::runLevel8() {
_damageCooldown = 0;
_deathTimer = 0;
_screenFlash = 0;
+ _screenShakeEnabled = false;
+ _deathCauseIndicator = 0;
_frameCounter = 0;
_gameCounter = 0;
_activeGameOpcode = 0;
@@ -728,6 +751,11 @@ bool InsaneRebel1::runLevel8() {
_walkerTimer = 0;
_walkerBranchChoice = 0;
+ // g_level8HitboxBuffer (0x7698) = _frameObjectState[150..299] filled with 0xFF.
+ // This enables all frame object event masks in the secondary half of the array,
+ // which the IACT 0x5A handler uses to gate walker-related frame objects.
+ memset(_frameObjectState + 150, 0xFF, 150);
+
int route = 0;
while (!_vm->shouldQuit()) {
_levelRouteIndex = route;
@@ -750,6 +778,7 @@ bool InsaneRebel1::runLevel8() {
if (_health >= 0) {
playCinematic("LVL8/L8END.ANM");
+ _maxChapterUnlocked = MAX(_maxChapterUnlocked, (int16)8);
return !_vm->shouldQuit();
}
@@ -792,6 +821,8 @@ bool InsaneRebel1::runLevel9() {
_damageCooldown = 0;
_deathTimer = 0;
_screenFlash = 0;
+ _screenShakeEnabled = false;
+ _deathCauseIndicator = 0;
_frameCounter = 0;
_gameCounter = 0;
_activeGameOpcode = 0;
@@ -939,6 +970,7 @@ bool InsaneRebel1::runLevel9() {
break;
playCinematic("LVL9/L9END.ANM");
+ _maxChapterUnlocked = MAX(_maxChapterUnlocked, (int16)9);
return !_vm->shouldQuit();
}
@@ -977,6 +1009,8 @@ bool InsaneRebel1::runLevel10() {
_damageCooldown = 0;
_deathTimer = 0;
_screenFlash = 0;
+ _screenShakeEnabled = false;
+ _deathCauseIndicator = 0;
_frameCounter = 0;
_gameCounter = 0;
_activeGameOpcode = 0;
@@ -1010,6 +1044,7 @@ bool InsaneRebel1::runLevel10() {
if (_health >= 0) {
playCinematic("LVL10/L10END.ANM");
+ _maxChapterUnlocked = MAX(_maxChapterUnlocked, (int16)10);
return !_vm->shouldQuit();
}
@@ -1051,6 +1086,8 @@ bool InsaneRebel1::runLevel11() {
_damageCooldown = 0;
_deathTimer = 0;
_screenFlash = 0;
+ _screenShakeEnabled = false;
+ _deathCauseIndicator = 0;
_frameCounter = 0;
_gameCounter = 0;
_activeGameOpcode = 0;
@@ -1100,6 +1137,7 @@ bool InsaneRebel1::runLevel11() {
if (_health >= 0) {
playCinematic("LVL11/L11END.ANM");
+ _maxChapterUnlocked = MAX(_maxChapterUnlocked, (int16)11);
return !_vm->shouldQuit();
}
@@ -1141,6 +1179,8 @@ bool InsaneRebel1::runLevel12() {
_damageCooldown = 0;
_deathTimer = 0;
_screenFlash = 0;
+ _screenShakeEnabled = false;
+ _deathCauseIndicator = 0;
_frameCounter = 0;
_gameCounter = 0;
_activeGameOpcode = 0;
@@ -1174,6 +1214,7 @@ bool InsaneRebel1::runLevel12() {
if (_health >= 0) {
playCinematic("LVL12/L12END.ANM");
+ _maxChapterUnlocked = MAX(_maxChapterUnlocked, (int16)12);
return !_vm->shouldQuit();
}
@@ -1215,6 +1256,8 @@ bool InsaneRebel1::runLevel13() {
_damageCooldown = 0;
_deathTimer = 0;
_screenFlash = 0;
+ _screenShakeEnabled = false;
+ _deathCauseIndicator = 0;
_frameCounter = 0;
_gameCounter = 0;
_activeGameOpcode = 0;
@@ -1248,6 +1291,7 @@ bool InsaneRebel1::runLevel13() {
if (_health >= 0) {
playCinematic("LVL13/L13END.ANM");
+ _maxChapterUnlocked = MAX(_maxChapterUnlocked, (int16)13);
return !_vm->shouldQuit();
}
@@ -1289,6 +1333,8 @@ bool InsaneRebel1::runLevel14() {
_damageCooldown = 0;
_deathTimer = 0;
_screenFlash = 0;
+ _screenShakeEnabled = false;
+ _deathCauseIndicator = 0;
_frameCounter = 0;
_gameCounter = 0;
_activeGameOpcode = 0;
@@ -1336,6 +1382,7 @@ bool InsaneRebel1::runLevel14() {
if (_health >= 0) {
playCinematic("LVL14/L14END.ANM");
+ _maxChapterUnlocked = MAX(_maxChapterUnlocked, (int16)14);
return !_vm->shouldQuit();
}
@@ -1378,6 +1425,8 @@ bool InsaneRebel1::runLevel15() {
_damageCooldown = 0;
_deathTimer = 0;
_screenFlash = 0;
+ _screenShakeEnabled = false;
+ _deathCauseIndicator = 0;
_frameCounter = 0;
_gameCounter = 0;
_activeGameOpcode = 0;
@@ -1435,6 +1484,7 @@ bool InsaneRebel1::runLevel15() {
if (_health >= 0) {
playCinematic("LVL15/L15END1.ANM");
+ _maxChapterUnlocked = MAX(_maxChapterUnlocked, (int16)15);
return !_vm->shouldQuit();
}
Commit: a8581ef1f387eb6515daa186ad13d1fe5c9d9e78
https://github.com/scummvm/scummvm/commit/a8581ef1f387eb6515daa186ad13d1fe5c9d9e78
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:42+02:00
Commit Message:
SCUMM: RA: Add basic joystick support
Changed paths:
engines/scumm/insane/insane_rebel1.cpp
engines/scumm/insane/insane_rebel1.h
engines/scumm/insane/insane_rebel1_iact.cpp
engines/scumm/insane/insane_rebel1_menu.cpp
engines/scumm/insane/insane_rebel2.cpp
engines/scumm/insane/insane_rebel2.h
engines/scumm/insane/insane_rebel2_iact.cpp
engines/scumm/insane/insane_rebel2_menu.cpp
engines/scumm/insane/insane_rebel2_render.cpp
engines/scumm/metaengine.cpp
engines/scumm/scumm.h
diff --git a/engines/scumm/insane/insane_rebel1.cpp b/engines/scumm/insane/insane_rebel1.cpp
index 4d4793f4c3c..1984745d429 100644
--- a/engines/scumm/insane/insane_rebel1.cpp
+++ b/engines/scumm/insane/insane_rebel1.cpp
@@ -244,6 +244,11 @@ InsaneRebel1::InsaneRebel1(ScummEngine_v7 *scumm) : Insane(), _vm(scumm) {
_mousePrevBiasY = 0;
_mouseBiasLatch = false;
_mouseRecentering = false;
+ _joystickAxisX = 0;
+ _joystickAxisY = 0;
+ _level2JoystickFilteredX = 0;
+ _level2JoystickFilteredY = 0;
+ _activeInputSource = kInputSourceMouse;
_currentLevel = 0;
_flyControlMode = 0;
diff --git a/engines/scumm/insane/insane_rebel1.h b/engines/scumm/insane/insane_rebel1.h
index 93b0601d0c5..338aee4ccc6 100644
--- a/engines/scumm/insane/insane_rebel1.h
+++ b/engines/scumm/insane/insane_rebel1.h
@@ -140,7 +140,7 @@ private:
void loadLevelSprites(int level);
void updateShipPhysics();
void updateTurretPhysics();
- void preprocessMouseAxes(int16 &inputX, int16 &inputY);
+ void preprocessMouseAxes(int16 &inputX, int16 &inputY, bool *usedJoystick = nullptr);
void rebuildProjectionTable(int16 curveStep, int16 curveExtent);
void resetProjectionTable();
void checkDynamicLevelBranch();
@@ -240,14 +240,24 @@ private:
int16 _mouseOffsetX; // 0x9762-style accumulated recenter offset in DOS 640-space
int16 _mouseOffsetY; // 0x9760-style accumulated recenter offset in DOS 200-space
int16 _mouseBiasX; // 0x9774: current preprocessed horizontal bias
- int16 _mouseBiasY; // 0x9772: current preprocessed vertical bias
- int16 _mousePrevBiasX; // 0x9770: previous-frame biasX
- int16 _mousePrevBiasY; // 0x976E: previous-frame biasY
- bool _mouseBiasLatch; // 0x4486: one-frame large-jump latch
- bool _mouseRecentering; // 0x976D: suppress recursive updates during warp
-
- // 0x0B handler physics update (asteroid/surface levels)
- void updateAsteroidPhysics();
+ int16 _mouseBiasY; // 0x9772: current preprocessed vertical bias
+ int16 _mousePrevBiasX; // 0x9770: previous-frame biasX
+ int16 _mousePrevBiasY; // 0x976E: previous-frame biasY
+ bool _mouseBiasLatch; // 0x4486: one-frame large-jump latch
+ bool _mouseRecentering; // 0x976D: suppress recursive updates during warp
+ int16 _joystickAxisX; // Rebel-specific left-stick X captured from keymapper axis events
+ int16 _joystickAxisY; // Rebel-specific left-stick Y captured from keymapper axis events
+ int16 _level2JoystickFilteredX; // Smoothed Level 2 analog X input
+ int16 _level2JoystickFilteredY; // Smoothed Level 2 analog Y input
+ enum InputSource {
+ kInputSourceMouse,
+ kInputSourceJoystickAnalog,
+ kInputSourceJoystickDigital
+ };
+ InputSource _activeInputSource;
+
+ // 0x0B handler physics update (asteroid/surface levels)
+ void updateAsteroidPhysics();
// 0x19/0x1A on-foot handler (Level 9 Stormtroopers)
void updateOnFootPhysics();
diff --git a/engines/scumm/insane/insane_rebel1_iact.cpp b/engines/scumm/insane/insane_rebel1_iact.cpp
index 0a9ec942e9e..c5069953a82 100644
--- a/engines/scumm/insane/insane_rebel1_iact.cpp
+++ b/engines/scumm/insane/insane_rebel1_iact.cpp
@@ -20,6 +20,7 @@
*/
#include "common/system.h"
+#include "common/config-manager.h"
#include "common/endian.h"
#include "scumm/scumm_v7.h"
@@ -27,6 +28,22 @@
namespace Scumm {
+static inline int16 applyRebel1AnalogDeadzone(int16 axisValue) {
+ const int deadZone = MAX(0, ConfMan.getInt("joystick_deadzone")) * 1000;
+ return (ABS(axisValue) <= deadZone) ? 0 : axisValue;
+}
+
+static inline int16 smoothRebel1Op0BAnalogInput(int16 inputValue, int16 &filteredValue, int16 axisMax) {
+ const int delta = (int)inputValue - (int)filteredValue;
+ int step = delta / 10;
+
+ if (step == 0 && delta != 0)
+ step = (delta > 0) ? 1 : -1;
+
+ filteredValue = CLIP<int>(filteredValue + step, -axisMax, axisMax);
+ return filteredValue;
+}
+
// LVL1 stage-2 0x5D damage/event codes. The gameplay stream exposes low record ids
// (6..18), while the recovered outer loop compares the post-latch state against the
// later translated values seen in the executable. Accept both representations.
@@ -554,10 +571,53 @@ void InsaneRebel1::unprojectGameplayPoint(int16 &x, int16 &y) const {
// Preserve the DOS bias/offset persistence and one-frame jump latch from
// FUN_231BE, but avoid hard recentring the host mouse into the DOS safe window.
// The actual frame-averaging behavior stays untouched.
-void InsaneRebel1::preprocessMouseAxes(int16 &inputX, int16 &inputY) {
+void InsaneRebel1::preprocessMouseAxes(int16 &inputX, int16 &inputY, bool *usedJoystick) {
+ if (usedJoystick)
+ *usedJoystick = false;
+
if (_mouseRecentering)
return;
+ const int16 analogAxisX = applyRebel1AnalogDeadzone(_joystickAxisX);
+ const int16 analogAxisY = applyRebel1AnalogDeadzone(_joystickAxisY);
+ const int joyX =
+ (_vm->getActionState(kScummActionInsaneRight) ? 1 : 0) -
+ (_vm->getActionState(kScummActionInsaneLeft) ? 1 : 0);
+ const int joyY =
+ (_vm->getActionState(kScummActionInsaneUp) ? 1 : 0) -
+ (_vm->getActionState(kScummActionInsaneDown) ? 1 : 0);
+
+ if (_activeInputSource == kInputSourceJoystickAnalog) {
+ if (usedJoystick)
+ *usedJoystick = true;
+
+ if (analogAxisX != 0 || analogAxisY != 0) {
+ inputX = CLIP<int32>(((int32)analogAxisX * 127) / Common::JOYAXIS_MAX, -127, 127);
+ inputY = CLIP<int32>(((int32)analogAxisY * 127) / Common::JOYAXIS_MAX, -127, 127);
+ } else {
+ inputX = 0;
+ inputY = 0;
+ }
+
+ if (_optControlsYFlip)
+ inputY = -inputY;
+
+ return;
+ }
+
+ if (_activeInputSource == kInputSourceJoystickDigital || joyX != 0 || joyY != 0) {
+ if (usedJoystick)
+ *usedJoystick = true;
+
+ inputX = joyX * 127;
+ inputY = joyY * 127;
+
+ if (_optControlsYFlip)
+ inputY = -inputY;
+
+ return;
+ }
+
int16 logicalX = (int16)CLIP<int>(_vm->_mouse.x, 0, 319);
int16 logicalY = (int16)CLIP<int>(_vm->_mouse.y, 0, 199);
const int16 rawX = (int16)(logicalX << 1);
@@ -958,9 +1018,29 @@ void InsaneRebel1::updateTurretPhysics() {
// not raw mouse coordinates.
int16 inputX = 0;
int16 inputY = 0;
- preprocessMouseAxes(inputX, inputY);
+ bool usedJoystick = false;
+ preprocessMouseAxes(inputX, inputY, &usedJoystick);
inputX = CLIP<int16>(inputX, -127, 127);
inputY = CLIP<int16>(inputY, -127, 127);
+ const int16 rawInputX = inputX;
+ const int16 rawInputY = inputY;
+
+ if (usedJoystick) {
+ // First-person turret/cockpit stages are noticeably more sensitive on
+ // joystick than on mouse, so damp only the joystick-driven input here.
+ inputX /= 2;
+ inputY /= 2;
+ }
+
+ debug("RA1 turret input: source=%s mouse=(%d,%d) actions(L,R,U,D)=(%d,%d,%d,%d) raw=(%d,%d) final=(%d,%d) level=%d mode=%d opcode=0x%X",
+ usedJoystick ? "joystick-actions" : "mouse-path",
+ _vm->_mouse.x, _vm->_mouse.y,
+ _vm->getActionState(kScummActionInsaneLeft),
+ _vm->getActionState(kScummActionInsaneRight),
+ _vm->getActionState(kScummActionInsaneUp),
+ _vm->getActionState(kScummActionInsaneDown),
+ rawInputX, rawInputY, inputX, inputY,
+ _currentLevel, _flyControlMode, _activeGameOpcode);
_rollAccum += (_tuning.roll * (int32)inputX) >> 4;
_rollAccum = (_rollAccum * 3) >> 2;
@@ -1129,9 +1209,48 @@ void InsaneRebel1::updateAsteroidPhysics() {
// _inputHistory* maps to 0x7580/0x7594, _viewHistory* to 0x75A8/0x75BC.
int16 inputX = 0;
int16 inputY = 0;
- preprocessMouseAxes(inputX, inputY);
+ bool usedJoystick = false;
+ preprocessMouseAxes(inputX, inputY, &usedJoystick);
inputX = CLIP<int16>(inputX, -0xA0, 0xA0);
inputY = CLIP<int16>(inputY, -100, 100);
+ const int16 rawInputX = inputX;
+ const int16 rawInputY = inputY;
+ const bool op0BAnalogSmoothing = (_activeInputSource == kInputSourceJoystickAnalog);
+ const char *inputSourceName = "mouse-path";
+
+ if (_activeInputSource == kInputSourceJoystickAnalog)
+ inputSourceName = "joystick-analog";
+ else if (_activeInputSource == kInputSourceJoystickDigital)
+ inputSourceName = "joystick-dpad";
+
+ if (usedJoystick) {
+ // The 0x0B first-person handler is shared by multiple RA1 stages. Smooth
+ // analog stick input over time so these sections keep full reach without
+ // feeling hyper-sensitive, while leaving mouse behavior untouched.
+ if (op0BAnalogSmoothing) {
+ inputX = smoothRebel1Op0BAnalogInput(inputX, _level2JoystickFilteredX, 127);
+ inputY = smoothRebel1Op0BAnalogInput(inputY, _level2JoystickFilteredY, 100);
+ } else {
+ _level2JoystickFilteredX = 0;
+ _level2JoystickFilteredY = 0;
+ inputX /= 2;
+ inputY /= 2;
+ }
+ } else {
+ _level2JoystickFilteredX = 0;
+ _level2JoystickFilteredY = 0;
+ }
+
+ debug("RA1 asteroid input: source=%s axis=(%d,%d) mouse=(%d,%d) actions(L,R,U,D)=(%d,%d,%d,%d) raw=(%d,%d) final=(%d,%d) level=%d opcode=0x%X",
+ inputSourceName,
+ _joystickAxisX, _joystickAxisY,
+ _vm->_mouse.x, _vm->_mouse.y,
+ _vm->getActionState(kScummActionInsaneLeft),
+ _vm->getActionState(kScummActionInsaneRight),
+ _vm->getActionState(kScummActionInsaneUp),
+ _vm->getActionState(kScummActionInsaneDown),
+ rawInputX, rawInputY, inputX, inputY,
+ _currentLevel, _activeGameOpcode);
for (int i = kInputHistorySize - 1; i > 0; i--) {
_inputHistoryX[i] = _inputHistoryX[i - 1];
@@ -1399,6 +1518,8 @@ void InsaneRebel1::handleGameChunk(int32 subSize, Common::SeekableReadStream &b)
_mousePrevBiasY = 0;
_mouseBiasLatch = false;
_mouseRecentering = false;
+ _level2JoystickFilteredX = 0;
+ _level2JoystickFilteredY = 0;
// Shooting/targeting reset
_playerFired = false;
diff --git a/engines/scumm/insane/insane_rebel1_menu.cpp b/engines/scumm/insane/insane_rebel1_menu.cpp
index bcb9da88cda..0723a76928a 100644
--- a/engines/scumm/insane/insane_rebel1_menu.cpp
+++ b/engines/scumm/insane/insane_rebel1_menu.cpp
@@ -37,6 +37,148 @@ static const int kRA1LevelSelectRowsPerCol = 8;
static const int kRA1NumLevels = 15;
bool InsaneRebel1::notifyEvent(const Common::Event &event) {
+ if (event.type == Common::EVENT_MOUSEMOVE && !_mouseRecentering) {
+ _activeInputSource = kInputSourceMouse;
+ }
+
+ if (event.type == Common::EVENT_CUSTOM_BACKEND_ACTION_AXIS) {
+ _activeInputSource = kInputSourceJoystickAnalog;
+
+ switch (event.customType) {
+ case kScummBackendActionRebel1AxisUp:
+ if (event.joystick.position == 0 && _joystickAxisY < 0)
+ return true;
+ _joystickAxisY = event.joystick.position;
+ return true;
+ case kScummBackendActionRebel1AxisDown:
+ if (event.joystick.position == 0 && _joystickAxisY > 0)
+ return true;
+ _joystickAxisY = -event.joystick.position;
+ return true;
+ case kScummBackendActionRebel1AxisLeft:
+ if (event.joystick.position == 0 && _joystickAxisX > 0)
+ return true;
+ _joystickAxisX = -event.joystick.position;
+ return true;
+ case kScummBackendActionRebel1AxisRight:
+ if (event.joystick.position == 0 && _joystickAxisX < 0)
+ return true;
+ _joystickAxisX = event.joystick.position;
+ return true;
+ default:
+ break;
+ }
+ }
+
+ if (event.type == Common::EVENT_CUSTOM_ENGINE_ACTION_START ||
+ event.type == Common::EVENT_CUSTOM_ENGINE_ACTION_END) {
+ const bool pressed = (event.type == Common::EVENT_CUSTOM_ENGINE_ACTION_START);
+
+ if (pressed &&
+ (event.customType == kScummActionInsaneUp ||
+ event.customType == kScummActionInsaneDown ||
+ event.customType == kScummActionInsaneLeft ||
+ event.customType == kScummActionInsaneRight)) {
+ _activeInputSource = kInputSourceJoystickDigital;
+ }
+
+ if (_highScoresActive && pressed && event.customType == kScummActionInsaneAttack) {
+ _vm->_smushVideoShouldFinish = true;
+ return true;
+ }
+
+ if (_menuActive && !_highScoresActive && pressed) {
+ if (_levelSelectActive) {
+ int col = _levelSelectSel / kRA1LevelSelectRowsPerCol;
+ int row = _levelSelectSel % kRA1LevelSelectRowsPerCol;
+
+ switch (event.customType) {
+ case kScummActionInsaneUp:
+ row = (row + kRA1LevelSelectRowsPerCol - 1) % kRA1LevelSelectRowsPerCol;
+ _levelSelectSel = col * kRA1LevelSelectRowsPerCol + row;
+ return true;
+ case kScummActionInsaneDown:
+ row = (row + 1) % kRA1LevelSelectRowsPerCol;
+ _levelSelectSel = col * kRA1LevelSelectRowsPerCol + row;
+ return true;
+ case kScummActionInsaneLeft:
+ if (col > 0)
+ _levelSelectSel -= kRA1LevelSelectRowsPerCol;
+ return true;
+ case kScummActionInsaneRight:
+ if (col < 1)
+ _levelSelectSel += kRA1LevelSelectRowsPerCol;
+ return true;
+ case kScummActionInsaneAttack:
+ _menuConfirmed = true;
+ _vm->_smushVideoShouldFinish = true;
+ return true;
+ default:
+ break;
+ }
+ }
+
+ if (_optionsActive) {
+ switch (event.customType) {
+ case kScummActionInsaneUp:
+ _optionsSel = (_optionsSel + kOptionsItemCount - 1) % kOptionsItemCount;
+ return true;
+ case kScummActionInsaneDown:
+ _optionsSel = (_optionsSel + 1) % kOptionsItemCount;
+ return true;
+ case kScummActionInsaneLeft:
+ if (_optionsSel == 6) {
+ _optVolume = MAX(0, _optVolume - 5);
+ _vm->_mixer->setVolumeForSoundType(Audio::Mixer::kPlainSoundType,
+ (_optVolume * Audio::Mixer::kMaxChannelVolume) / 127);
+ ConfMan.setInt("music_volume", (_optVolume * 256) / 127);
+ ConfMan.setInt("sfx_volume", (_optVolume * 256) / 127);
+ ConfMan.setInt("speech_volume", (_optVolume * 256) / 127);
+ }
+ return true;
+ case kScummActionInsaneRight:
+ if (_optionsSel == 6) {
+ _optVolume = MIN(127, _optVolume + 5);
+ _vm->_mixer->setVolumeForSoundType(Audio::Mixer::kPlainSoundType,
+ (_optVolume * Audio::Mixer::kMaxChannelVolume) / 127);
+ ConfMan.setInt("music_volume", (_optVolume * 256) / 127);
+ ConfMan.setInt("sfx_volume", (_optVolume * 256) / 127);
+ ConfMan.setInt("speech_volume", (_optVolume * 256) / 127);
+ }
+ return true;
+ case kScummActionInsaneAttack:
+ _menuConfirmed = true;
+ _vm->_smushVideoShouldFinish = true;
+ return true;
+ default:
+ break;
+ }
+ }
+
+ if (!_optionsActive && !_levelSelectActive) {
+ switch (event.customType) {
+ case kScummActionInsaneUp:
+ _menuSelection = (_menuSelection + 4) % 5;
+ return true;
+ case kScummActionInsaneDown:
+ _menuSelection = (_menuSelection + 1) % 5;
+ return true;
+ case kScummActionInsaneAttack:
+ _menuConfirmed = true;
+ _vm->_smushVideoShouldFinish = true;
+ return true;
+ default:
+ break;
+ }
+ }
+ }
+
+ if (_interactiveVideoActive && !_menuActive && event.customType == kScummActionInsaneAttack) {
+ _playerFired = pressed;
+ return true;
+ }
+ }
+
if (_menuActive && _levelSelectActive && event.type == Common::EVENT_KEYDOWN) {
int col = _levelSelectSel / kRA1LevelSelectRowsPerCol;
int row = _levelSelectSel % kRA1LevelSelectRowsPerCol;
diff --git a/engines/scumm/insane/insane_rebel2.cpp b/engines/scumm/insane/insane_rebel2.cpp
index afa0f3ddfcb..ad54fabdcc7 100644
--- a/engines/scumm/insane/insane_rebel2.cpp
+++ b/engines/scumm/insane/insane_rebel2.cpp
@@ -546,9 +546,84 @@ InsaneRebel2::~InsaneRebel2() {
// Handles ESC (skip) and SPACE (pause) regardless of menu state.
// Pause behavior matches original FUN_405A21: SPACE pauses, ANY key unpauses.
bool InsaneRebel2::notifyEvent(const Common::Event &event) {
- if (event.type == Common::EVENT_KEYDOWN) {
- SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
+ SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
+
+ if (event.type == Common::EVENT_CUSTOM_ENGINE_ACTION_START ||
+ event.type == Common::EVENT_CUSTOM_ENGINE_ACTION_END) {
+ const bool pressed = (event.type == Common::EVENT_CUSTOM_ENGINE_ACTION_START);
+
+ if (pressed && splayer && splayer->_paused && _gameState == kStateGameplay) {
+ debug("Rebel2: Joystick action while paused - unpausing");
+ if (_pauseOverlayActive) {
+ _vm->_system->getPaletteManager()->setPalette(_savedPausePalette, 0, 256);
+ _pauseOverlayActive = false;
+ }
+ splayer->unpause();
+ return true;
+ }
+
+ if (_menuInputActive && pressed) {
+ Common::KeyCode keycode = Common::KEYCODE_INVALID;
+
+ switch (_gameState) {
+ case kStateMainMenu:
+ case kStatePilotSelect:
+ case kStateDifficultySelect:
+ case kStateChapterSelect:
+ case kStateOptions:
+ switch (event.customType) {
+ case kScummActionInsaneUp:
+ keycode = Common::KEYCODE_UP;
+ break;
+ case kScummActionInsaneDown:
+ keycode = Common::KEYCODE_DOWN;
+ break;
+ case kScummActionInsaneLeft:
+ keycode = Common::KEYCODE_LEFT;
+ break;
+ case kScummActionInsaneRight:
+ keycode = Common::KEYCODE_RIGHT;
+ break;
+ case kScummActionInsaneAttack:
+ keycode = Common::KEYCODE_RETURN;
+ break;
+ case kScummActionInsaneSwitch:
+ keycode = Common::KEYCODE_ESCAPE;
+ break;
+ default:
+ break;
+ }
+ break;
+
+ case kStateTopPilots:
+ if (event.customType == kScummActionInsaneAttack)
+ keycode = Common::KEYCODE_RETURN;
+ else if (event.customType == kScummActionInsaneSwitch)
+ keycode = Common::KEYCODE_ESCAPE;
+ break;
+
+ default:
+ break;
+ }
+ if (keycode != Common::KEYCODE_INVALID) {
+ Common::Event syntheticEvent = Common::Event();
+ syntheticEvent.type = Common::EVENT_KEYDOWN;
+ syntheticEvent.kbd.keycode = keycode;
+ syntheticEvent.kbd.ascii = (keycode == Common::KEYCODE_RETURN) ? '\r' :
+ (keycode == Common::KEYCODE_ESCAPE) ? Common::ASCII_ESCAPE : 0;
+ _menuEventQueue.push(syntheticEvent);
+ return true;
+ }
+ }
+
+ if (event.customType == kScummActionInsaneAttack ||
+ event.customType == kScummActionInsaneSwitch) {
+ return true;
+ }
+ }
+
+ if (event.type == Common::EVENT_KEYDOWN) {
// When paused during gameplay, ANY key unpauses (FUN_405A21 line 360-365).
// ESC additionally opens the ScummVM menu (original: quit key exits level).
if (splayer && splayer->_paused && _gameState == kStateGameplay) {
@@ -1068,6 +1143,10 @@ int32 InsaneRebel2::processMouse() {
// Get button state directly from event manager (SCUMM VARs aren't updated during SMUSH)
// Bit 0 = left button, Bit 1 = right button, Bit 2 = middle button
uint32 currentButtons = _vm->_system->getEventManager()->getButtonState();
+ if (_vm->getActionState(kScummActionInsaneAttack))
+ currentButtons |= 1;
+ if (_vm->getActionState(kScummActionInsaneSwitch))
+ currentButtons |= 2;
// Edge detection for buttons
bool leftPressed = (currentButtons & 1) != 0;
@@ -1108,7 +1187,7 @@ int32 InsaneRebel2::processMouse() {
// - Other gameplay handlers fire while button is held; slot counters still rate-limit.
bool triggerShot = (_rebelHandler == 25) ? (leftPressed && !leftWasPressed) : leftPressed;
if (triggerShot && isShootingAllowed()) {
- Common::Point mousePos(_vm->_mouse.x, _vm->_mouse.y);
+ Common::Point mousePos = getGameplayAimPoint();
debug("Rebel2 Click: Mouse=(%d,%d) Enemies=%d",
mousePos.x, mousePos.y, _enemies.size());
@@ -1244,6 +1323,32 @@ int32 InsaneRebel2::processMouse() {
return buttons;
}
+Common::Point InsaneRebel2::getGameplayAimPoint() {
+ Common::Point aimPos(_vm->_mouse.x, _vm->_mouse.y);
+
+ if (_menuInputActive || _gameState != kStateGameplay)
+ return aimPos;
+
+ int dx = 0;
+ int dy = 0;
+
+ if (_vm->getActionState(kScummActionInsaneLeft))
+ dx--;
+ if (_vm->getActionState(kScummActionInsaneRight))
+ dx++;
+ if (_vm->getActionState(kScummActionInsaneUp))
+ dy--;
+ if (_vm->getActionState(kScummActionInsaneDown))
+ dy++;
+
+ if (dx || dy) {
+ aimPos.x = (dx < 0) ? 0 : (dx > 0) ? 319 : 160;
+ aimPos.y = (dy < 0) ? 0 : (dy > 0) ? 199 : 100;
+ }
+
+ return aimPos;
+}
+
bool InsaneRebel2::isBitSet(int n) {
// FUN_00423970: When param_1 < 1 (0 or negative), the bounds check fails and returns false.
// This means ID 0 or negative IDs are always treated as "enabled" (not skipped).
diff --git a/engines/scumm/insane/insane_rebel2.h b/engines/scumm/insane/insane_rebel2.h
index a8fac1ae4d5..f123efb5bb7 100644
--- a/engines/scumm/insane/insane_rebel2.h
+++ b/engines/scumm/insane/insane_rebel2.h
@@ -483,6 +483,7 @@ public:
int32 processMouse() override;
+ Common::Point getGameplayAimPoint();
bool isBitSet(int n) override;
void setBit(int n) override;
diff --git a/engines/scumm/insane/insane_rebel2_iact.cpp b/engines/scumm/insane/insane_rebel2_iact.cpp
index 28552eb5f28..161c89192f3 100644
--- a/engines/scumm/insane/insane_rebel2_iact.cpp
+++ b/engines/scumm/insane/insane_rebel2_iact.cpp
@@ -651,10 +651,10 @@ void InsaneRebel2::iactRebel2Opcode6(byte *renderBitmap, Common::SeekableReadStr
// local_18 = ((DAT_0047a7e0 * 5 + 0x27b) * 0x40) / 0xfe
// local_1c = ((DAT_0047a7e2 * 5 + 0x27b) * 0x10) / 0xfe
- // Map mouse position (-127 to 127 range) to ship target
- // Mouse is 0-320, center is 160. Map to -127 to 127 range
- int16 mouseOffsetX = (int16)((_vm->_mouse.x - 160) * 127 / 160);
- int16 mouseOffsetY = (int16)((_vm->_mouse.y - 100) * 127 / 100);
+ // Map the effective aim position (-127 to 127 range) to the ship target.
+ Common::Point aimPos = getGameplayAimPoint();
+ int16 mouseOffsetX = (int16)((aimPos.x - 160) * 127 / 160);
+ int16 mouseOffsetY = (int16)((aimPos.y - 100) * 127 / 100);
// Clamp X offset to movement range limit (covered/shooting state)
// Based on FUN_00401234 lines 119-136
@@ -698,8 +698,8 @@ void InsaneRebel2::iactRebel2Opcode6(byte *renderBitmap, Common::SeekableReadStr
// Calculate ship direction indices for sprite selection
// Map mouse position to 5x7 direction grid (like Handler 7)
- int16 mouseX = _vm->_mouse.x;
- int16 mouseY = _vm->_mouse.y;
+ int16 mouseX = aimPos.x;
+ int16 mouseY = aimPos.y;
// Scale mouse if video is larger than 320x200
if (_player && _player->_width > 320) {
@@ -740,12 +740,13 @@ void InsaneRebel2::iactRebel2Opcode6(byte *renderBitmap, Common::SeekableReadStr
_shipDirectionIndex = _shipDirectionH * 7 + _shipDirectionV;
}
- // Update firing state from mouse button
+ // Update firing state from mouse button or joystick fire action
// Mode 4 (autopilot) disables shooting - FUN_00401CCF line 82-84
if (_shipLevelMode == 4) {
_shipFiring = false;
} else {
- _shipFiring = (_vm->VAR(_vm->VAR_LEFTBTN_HOLD) != 0);
+ _shipFiring = (_vm->VAR(_vm->VAR_LEFTBTN_HOLD) != 0) ||
+ _vm->getActionState(kScummActionInsaneAttack);
}
debug("Rebel2 Opcode 6 (Handler 8): mode=%d range=%d shipPos=(%d,%d) target=(%d,%d) firing=%d dir=(%d,%d,%d)",
@@ -805,8 +806,9 @@ void InsaneRebel2::iactRebel2Opcode6(byte *renderBitmap, Common::SeekableReadStr
// DAT_0047a7e0 = mouseX - 160, DAT_0047a7e2 = mouseY - 100
// _vm->_mouse.x/y are in virtual screen coords (0-319, 0-199)
// consistent with handler 8 which uses _vm->_mouse.x directly.
- int16 inputX = (int16)(_vm->_mouse.x - 160); // DAT_0047a7e0
- int16 inputY = (int16)(_vm->_mouse.y - 100); // DAT_0047a7e2
+ Common::Point aimPos = getGameplayAimPoint();
+ int16 inputX = (int16)(aimPos.x - 160); // DAT_0047a7e0
+ int16 inputY = (int16)(aimPos.y - 100); // DAT_0047a7e2
// Clamp: mouse mode uses [-160, 160] for X, [-127, 127] for Y (lines 55-70)
if (inputX > 160)
@@ -1038,7 +1040,9 @@ void InsaneRebel2::iactRebel2Opcode6(byte *renderBitmap, Common::SeekableReadStr
if (_shipDirectionIndex > 34)
_shipDirectionIndex = 34;
- _shipFiring = (_flyControlMode == 2) && (_vm->VAR(_vm->VAR_LEFTBTN_HOLD) != 0);
+ _shipFiring = (_flyControlMode == 2) &&
+ ((_vm->VAR(_vm->VAR_LEFTBTN_HOLD) != 0) ||
+ _vm->getActionState(kScummActionInsaneAttack));
debug("Rebel2 H7: pos=(%d,%d) vel=%d vIn=%d dx=%d dir=%d mode=%d",
_flyShipScreenX, _flyShipScreenY, _smoothedVelocity,
@@ -1151,7 +1155,7 @@ void InsaneRebel2::iactRebel2Opcode6(byte *renderBitmap, Common::SeekableReadStr
if (_rebelDamageLevel == 5) {
// At max damage, check for direction change input
// For now, use mouse X position to determine direction
- int16 mouseX = _vm->_mouse.x;
+ int16 mouseX = getGameplayAimPoint().x;
if (_player && _player->_width > 320) {
mouseX = (mouseX * 320) / _player->_width;
}
@@ -1371,10 +1375,11 @@ void InsaneRebel2::iactRebel2Opcode6(byte *renderBitmap, Common::SeekableReadStr
if (_rebelDamageLevel == 5) {
// Check for joystick/key input to change direction
// Simplified: use mouse position
- if (_vm->_mouse.x > 75) {
+ int16 mouseX = getGameplayAimPoint().x;
+ if (mouseX > 235) {
_rebelFlightDir = 1;
}
- if (_vm->_mouse.x < -75) {
+ if (mouseX < 85) {
_rebelFlightDir = 0;
}
}
diff --git a/engines/scumm/insane/insane_rebel2_menu.cpp b/engines/scumm/insane/insane_rebel2_menu.cpp
index 9ffbfa8ce55..15151d30504 100644
--- a/engines/scumm/insane/insane_rebel2_menu.cpp
+++ b/engines/scumm/insane/insane_rebel2_menu.cpp
@@ -755,9 +755,9 @@ int InsaneRebel2::runMainMenu() {
break;
case 2: // Calibrate Joystick
- debug("Rebel2: Calibrate Joystick selected");
- // TODO: Implement joystick calibration (FUN_00425820)
- // Plays O_CALIB.SAN with joystick calibration prompts
+ debug("Rebel2: Calibrate Joystick selected - no-op for modern joystick support");
+ // Modern controller support uses live keymapper actions; no explicit
+ // joystick calibration flow is required here.
break;
case 3: // Continue Intro -> replay intro videos
@@ -1594,9 +1594,6 @@ int InsaneRebel2::processLevelSelectInput() {
int &selection = isDifficultyMode ? _difficultySelection : _levelSelection;
int itemCount = isDifficultyMode ? 6 : _levelItemCount;
- // Mouse hit Y positions â must match drawMenuItems() formula
- const int baseY = itemCount * -5 + 0x68;
-
while (!_menuEventQueue.empty()) {
Common::Event event = _menuEventQueue.pop();
switch (event.type) {
diff --git a/engines/scumm/insane/insane_rebel2_render.cpp b/engines/scumm/insane/insane_rebel2_render.cpp
index 77a07000f1d..72ad585eed4 100644
--- a/engines/scumm/insane/insane_rebel2_render.cpp
+++ b/engines/scumm/insane/insane_rebel2_render.cpp
@@ -1723,8 +1723,9 @@ void InsaneRebel2::checkCollisionZones() {
// Calculate aim position in centered coordinates.
// Original: local_10 = mouseOffset + 0xa0, then smoothed and clamped to [-0x34..0x34]
// Simplified mapping: mouse 0..320 â [-52..52], mouse 0..200 â [-45..45]
- int16 aimX = (int16)((_vm->_mouse.x - 160) * 52 / 160);
- int16 aimY = (int16)((100 - _vm->_mouse.y) * 45 / 100);
+ Common::Point aimPos = getGameplayAimPoint();
+ int16 aimX = (int16)((aimPos.x - 160) * 52 / 160);
+ int16 aimY = (int16)((100 - aimPos.y) * 45 / 100);
// Clamp to original ranges (DAT_0047a7fc < 1 path)
if (aimX > 0x34)
@@ -2233,8 +2234,9 @@ void InsaneRebel2::procPostRendering(byte *renderBitmap, int32 codecparam, int32
maxScrollY = 0;
// Simple linear mapping: Center of screen corresponds to center of buffer
- _viewX = (_vm->_mouse.x * maxScrollX) / _vm->_screenWidth;
- _viewY = (_vm->_mouse.y * maxScrollY) / _vm->_screenHeight;
+ Common::Point aimPos = getGameplayAimPoint();
+ _viewX = (aimPos.x * maxScrollX) / _vm->_screenWidth;
+ _viewY = (aimPos.y * maxScrollY) / _vm->_screenHeight;
_player->setScrollOffset(_viewX, _viewY);
@@ -2839,8 +2841,9 @@ void InsaneRebel2::renderTurretHudOverlays(byte *renderBitmap, int pitch, int wi
return;
// Calculate mouse offset (clamped to -127..127)
- int mouseOffsetX = (_vm->_mouse.x - 160);
- int mouseOffsetY = (_vm->_mouse.y - 100);
+ Common::Point aimPos = getGameplayAimPoint();
+ int mouseOffsetX = (aimPos.x - 160);
+ int mouseOffsetY = (aimPos.y - 100);
if (mouseOffsetX > 127)
mouseOffsetX = 127;
if (mouseOffsetX < -127)
@@ -3451,8 +3454,9 @@ void InsaneRebel2::renderHandler25Ship(byte *renderBitmap, int pitch, int width,
int16 areaBottom = (_corridorBottomY > 0) ? _corridorBottomY : 180;
// Get crosshair position (using mouse position scaled to game coords)
- int16 crosshairX = _vm->_mouse.x;
- int16 crosshairY = _vm->_mouse.y;
+ Common::Point aimPos = getGameplayAimPoint();
+ int16 crosshairX = aimPos.x;
+ int16 crosshairY = aimPos.y;
if (_player && _player->_width > 320) {
crosshairX = (crosshairX * 320) / _player->_width;
crosshairY = (crosshairY * 200) / _player->_height;
@@ -4220,7 +4224,8 @@ void InsaneRebel2::renderCrosshair(byte *renderBitmap, int pitch, int width, int
// Update target lock state and draw crosshair/reticle
// Target lock detection (DAT_00443676 equivalent)
- Common::Point worldMousePos(_vm->_mouse.x + _viewX, _vm->_mouse.y + _viewY);
+ Common::Point aimPos = getGameplayAimPoint();
+ Common::Point worldMousePos(aimPos.x + _viewX, aimPos.y + _viewY);
bool targetLocked = false;
for (Common::List<enemy>::iterator it = _enemies.begin(); it != _enemies.end(); ++it) {
@@ -4270,8 +4275,8 @@ void InsaneRebel2::renderCrosshair(byte *renderBitmap, int pitch, int width, int
int ch = _smush_iconsNut->getCharHeight(reticleIndex);
// Calculate crosshair position
- int crosshairX = _vm->_mouse.x - cw / 2 + _viewX;
- int crosshairY = _vm->_mouse.y - ch / 2 + _viewY;
+ int crosshairX = aimPos.x - cw / 2 + _viewX;
+ int crosshairY = aimPos.y - ch / 2 + _viewY;
// Handler 25 (0x19): Add view offset to crosshair position
// From FUN_41DB5E lines 198-199: X = DAT_00457914 + DAT_0045790c, Y = DAT_00457916 + DAT_0045790e
diff --git a/engines/scumm/metaengine.cpp b/engines/scumm/metaengine.cpp
index 0d9479e420b..5cb8d9c1b88 100644
--- a/engines/scumm/metaengine.cpp
+++ b/engines/scumm/metaengine.cpp
@@ -1102,6 +1102,110 @@ Common::KeymapArray ScummMetaEngine::initKeymaps(const char *target) const {
keymaps.push_back(insaneKeymap);
}
+ if (gameId == "rebel1") {
+ Keymap *rebel1Keymap = new Keymap(Keymap::kKeymapTypeGame, "scumm-rebel1", _("Rebel Assault controls"));
+
+ act = new Action("RA1UP", _("Aim up / menu up"));
+ act->setCustomEngineActionEvent(kScummActionInsaneUp);
+ act->addDefaultInputMapping("JOY_UP");
+ rebel1Keymap->addAction(act);
+
+ act = new Action("RA1DOWN", _("Aim down / menu down"));
+ act->setCustomEngineActionEvent(kScummActionInsaneDown);
+ act->addDefaultInputMapping("JOY_DOWN");
+ rebel1Keymap->addAction(act);
+
+ act = new Action("RA1LEFT", _("Aim left / menu left"));
+ act->setCustomEngineActionEvent(kScummActionInsaneLeft);
+ act->addDefaultInputMapping("JOY_LEFT");
+ rebel1Keymap->addAction(act);
+
+ act = new Action("RA1RIGHT", _("Aim right / menu right"));
+ act->setCustomEngineActionEvent(kScummActionInsaneRight);
+ act->addDefaultInputMapping("JOY_RIGHT");
+ rebel1Keymap->addAction(act);
+
+ act = new Action("RA1STICKUP", _("Stick up"));
+ act->setCustomBackendActionAxisEvent(kScummBackendActionRebel1AxisUp);
+ act->addDefaultInputMapping("JOY_LEFT_STICK_Y-");
+ rebel1Keymap->addAction(act);
+
+ act = new Action("RA1STICKDOWN", _("Stick down"));
+ act->setCustomBackendActionAxisEvent(kScummBackendActionRebel1AxisDown);
+ act->addDefaultInputMapping("JOY_LEFT_STICK_Y+");
+ rebel1Keymap->addAction(act);
+
+ act = new Action("RA1STICKLEFT", _("Stick left"));
+ act->setCustomBackendActionAxisEvent(kScummBackendActionRebel1AxisLeft);
+ act->addDefaultInputMapping("JOY_LEFT_STICK_X-");
+ rebel1Keymap->addAction(act);
+
+ act = new Action("RA1STICKRIGHT", _("Stick right"));
+ act->setCustomBackendActionAxisEvent(kScummBackendActionRebel1AxisRight);
+ act->addDefaultInputMapping("JOY_LEFT_STICK_X+");
+ rebel1Keymap->addAction(act);
+
+ act = new Action("RA1FIRE", _("Fire / select"));
+ act->setCustomEngineActionEvent(kScummActionInsaneAttack);
+ act->addDefaultInputMapping("JOY_A");
+ rebel1Keymap->addAction(act);
+
+ act = new Action("RA1BACK", _("Back / skip"));
+ act->setKeyEvent(KeyState(KEYCODE_ESCAPE, ASCII_ESCAPE));
+ act->addDefaultInputMapping("JOY_B");
+ act->addDefaultInputMapping("JOY_Y");
+ act->addDefaultInputMapping("JOY_START");
+ rebel1Keymap->addAction(act);
+
+ keymaps.push_back(rebel1Keymap);
+ }
+
+ if (gameId == "rebel2") {
+ Keymap *rebel2Keymap = new Keymap(Keymap::kKeymapTypeGame, "scumm-rebel2", _("Rebel Assault II controls"));
+
+ act = new Action("RA2UP", _("Aim up / menu up"));
+ act->setCustomEngineActionEvent(kScummActionInsaneUp);
+ act->addDefaultInputMapping("JOY_UP");
+ act->addDefaultInputMapping("JOY_LEFT_STICK_Y-");
+ rebel2Keymap->addAction(act);
+
+ act = new Action("RA2DOWN", _("Aim down / menu down"));
+ act->setCustomEngineActionEvent(kScummActionInsaneDown);
+ act->addDefaultInputMapping("JOY_DOWN");
+ act->addDefaultInputMapping("JOY_LEFT_STICK_Y+");
+ rebel2Keymap->addAction(act);
+
+ act = new Action("RA2LEFT", _("Aim left / menu left"));
+ act->setCustomEngineActionEvent(kScummActionInsaneLeft);
+ act->addDefaultInputMapping("JOY_LEFT");
+ act->addDefaultInputMapping("JOY_LEFT_STICK_X-");
+ rebel2Keymap->addAction(act);
+
+ act = new Action("RA2RIGHT", _("Aim right / menu right"));
+ act->setCustomEngineActionEvent(kScummActionInsaneRight);
+ act->addDefaultInputMapping("JOY_RIGHT");
+ act->addDefaultInputMapping("JOY_LEFT_STICK_X+");
+ rebel2Keymap->addAction(act);
+
+ act = new Action("RA2FIRE", _("Fire / select"));
+ act->setCustomEngineActionEvent(kScummActionInsaneAttack);
+ act->addDefaultInputMapping("JOY_A");
+ rebel2Keymap->addAction(act);
+
+ act = new Action("RA2COVER", _("Cover / back"));
+ act->setCustomEngineActionEvent(kScummActionInsaneSwitch);
+ act->addDefaultInputMapping("JOY_B");
+ rebel2Keymap->addAction(act);
+
+ act = new Action("RA2BACK", _("Skip / menu"));
+ act->setKeyEvent(KeyState(KEYCODE_ESCAPE, ASCII_ESCAPE));
+ act->addDefaultInputMapping("JOY_Y");
+ act->addDefaultInputMapping("JOY_START");
+ rebel2Keymap->addAction(act);
+
+ keymaps.push_back(rebel2Keymap);
+ }
+
return keymaps;
}
diff --git a/engines/scumm/scumm.h b/engines/scumm/scumm.h
index 928cebd9349..4a7b88a8421 100644
--- a/engines/scumm/scumm.h
+++ b/engines/scumm/scumm.h
@@ -503,6 +503,13 @@ enum ScummAction {
kScummActionCount
};
+enum ScummBackendAction {
+ kScummBackendActionRebel1AxisUp = 11000,
+ kScummBackendActionRebel1AxisDown,
+ kScummBackendActionRebel1AxisLeft,
+ kScummBackendActionRebel1AxisRight
+};
+
extern const char *const insaneKeymapId;
/**
Commit: 51052ab299bf2f63bf0eb95b78704875173fcf2a
https://github.com/scummvm/scummvm/commit/51052ab299bf2f63bf0eb95b78704875173fcf2a
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:43+02:00
Commit Message:
SCUMM: RA1: Improve torpedo handling
Changed paths:
engines/scumm/insane/insane_rebel1.h
engines/scumm/insane/insane_rebel1_iact.cpp
engines/scumm/insane/insane_rebel1_render.cpp
engines/scumm/insane/insane_rebel1_runlevels.cpp
diff --git a/engines/scumm/insane/insane_rebel1.h b/engines/scumm/insane/insane_rebel1.h
index 338aee4ccc6..d1b865ab1f6 100644
--- a/engines/scumm/insane/insane_rebel1.h
+++ b/engines/scumm/insane/insane_rebel1.h
@@ -160,6 +160,7 @@ private:
int x, int y, const RA1Sprite &sprite);
void updateGostSlotPosition(int16 targetIdx, int16 left, int16 top, int16 right, int16 bottom);
void applyFrameObjectHitState(int16 targetIdx);
+ bool isFrameObjectPrimarySet(int16 objectId) const;
// Shooting pipeline â FUN_1CCA0 (0x1CCA0) shot spawner,
// FUN_1C0EF (0x1C0EF) target detection, FUN_1C940 (0x1C940) shot processing
@@ -463,8 +464,9 @@ private:
int16 _shieldGenHitsA; // Hits on _protectedTargetA
int16 _shieldGenHitsB; // Hits on _protectedTargetB
- // Torpedo fired flag â set when torpedo hits in Phase 2 (Level 15)
- bool _torpedoFired; // 0x7602 bit 1 in original
+ // Level 15 torpedo success latch. The original derives this from
+ // g_gameplayPhaseFlags bit 1, which is the primary object-state bit for object 7.
+ bool _torpedoFired;
// Level 8 walker-specific state â RunLevel8Flow (0x18546)
int16 _walkerHealth; // Walker health percentage (0-100), init=100
diff --git a/engines/scumm/insane/insane_rebel1_iact.cpp b/engines/scumm/insane/insane_rebel1_iact.cpp
index c5069953a82..59e02601943 100644
--- a/engines/scumm/insane/insane_rebel1_iact.cpp
+++ b/engines/scumm/insane/insane_rebel1_iact.cpp
@@ -410,6 +410,19 @@ void InsaneRebel1::applyFrameObjectHitState(int16 targetIdx) {
_frameObjectState[altIndex] &= ~bit;
}
+bool InsaneRebel1::isFrameObjectPrimarySet(int16 objectId) const {
+ if (objectId <= 0)
+ return false;
+
+ const int bitIndex = objectId - 1;
+ const int byteIndex = bitIndex >> 3;
+ if (byteIndex < 0 || byteIndex >= 0x96 || byteIndex >= kFrameObjectStateBytes)
+ return false;
+
+ const byte bit = (byte)(0x80 >> (bitIndex & 7));
+ return (_frameObjectState[byteIndex] & bit) != 0;
+}
+
bool InsaneRebel1::handleFrameObjectTarget(int16 objectId, int16 left, int16 top, int16 width, int16 height,
int codec, uint8 &ra1Param) {
if (!_interactiveVideoActive)
@@ -1296,9 +1309,14 @@ void InsaneRebel1::updateAsteroidPhysics() {
_frameCounter++;
- // Level 4 Phase 2: enable torpedo mode at frame 0x3E
- if (_currentLevel == 3 && _levelGameplayPhase == 2 && _frameCounter == 0x3E)
- _gameplayFlags75ff |= 2;
+ // Level 4 Phase 2: enable torpedo mode at frame 0x3E and finish as
+ // soon as the torpedo registers a hit. The DOS loop exits on killCount.
+ if (_currentLevel == 3 && _levelGameplayPhase == 2) {
+ if (_frameCounter == 0x3E)
+ _gameplayFlags75ff |= 2;
+ if (_killCount > 0)
+ _vm->_smushVideoShouldFinish = true;
+ }
// Level 4 Phase 1: track shield generator hits per frame.
// Original (RunLevel4Flow): g_recentKillObjectIdPlus1 checked every frame.
@@ -1324,12 +1342,16 @@ void InsaneRebel1::updateAsteroidPhysics() {
}
}
- // Level 15 Phase 2: enable torpedo and end on hit.
- // Original (RunLevel1GameLoop): torpedo at frame 0x18A, ends when DAT_00007602 & 2.
+ // Level 15 Phase 2: enable torpedo at frame 0x18A, expose the protected
+ // target IDs used by the original flow, and finish when object-state bit
+ // 0x7602 & 2 becomes set.
if (_currentLevel == 14 && _levelGameplayPhase == 2) {
- if (_frameCounter == 0x18A)
+ if (_frameCounter == 0x18A) {
_gameplayFlags75ff |= 2;
- if (_killCount > 0) {
+ _protectedTargetA = 0x67;
+ _protectedTargetB = 0x69;
+ }
+ if (isFrameObjectPrimarySet(7)) {
_torpedoFired = true;
_vm->_smushVideoShouldFinish = true;
}
@@ -1775,7 +1797,7 @@ void InsaneRebel1::processShot() {
_shotSlots[slot].centerY = originY;
_shotSlots[slot].variant = _shotAlternator;
_shotAlternator = 1 - _shotAlternator;
- playSfx(kSfxLaserShot, 127, 0);
+ playSfx((_gameplayFlags75ff & 0x2) ? kSfxAlert : kSfxLaserShot, 127, 0);
debug(5, "RA1 shot: slot=%d pos=(%d,%d) origin=(%d,%d)", slot,
_shotSlots[slot].posX, _shotSlots[slot].posY, originX, originY);
diff --git a/engines/scumm/insane/insane_rebel1_render.cpp b/engines/scumm/insane/insane_rebel1_render.cpp
index 49283a5e2a4..23b287561fc 100644
--- a/engines/scumm/insane/insane_rebel1_render.cpp
+++ b/engines/scumm/insane/insane_rebel1_render.cpp
@@ -542,6 +542,7 @@ void InsaneRebel1::renderTargetBoxes(byte *dst, int pitch, int width, int height
// The original does not draw a hardcoded pixel cross; it renders glyph markers
// whose state depends on _targetProximity.
void InsaneRebel1::renderTargeting(byte *dst, int pitch, int width, int height) {
+ static const char kRA1TorpedoIndicator[] = "<d";
const RA1SpriteBank &markerBank = (_techFontBank.numSprites > 0) ? _techFontBank : _hudFontBank;
const int overlayX = ra1OverlayViewOffsetX(this);
const int overlayY = ra1OverlayViewOffsetY(this);
@@ -569,6 +570,12 @@ void InsaneRebel1::renderTargeting(byte *dst, int pitch, int width, int height)
int cursorX = CLIP<int>(overlayX + _shipPosX, 0, width - 1);
int cursorY = CLIP<int>(overlayY + _shipPosY, 0, height - 1);
drawCenteredBankGlyph(markerBank, dst, pitch, width, height, cursorX, cursorY, marker[0]);
+
+ if (altMarkerSet) {
+ const int indicatorWidth = getFontBankStringWidth(kRA1TorpedoIndicator);
+ drawFontBankString(dst, pitch, width, height,
+ overlayX + 0xA0 - indicatorWidth / 2, overlayY + 0x6E, kRA1TorpedoIndicator);
+ }
}
// Save previous proximity for next frame
@@ -650,7 +657,11 @@ void InsaneRebel1::renderGostSlots(byte *dst, int pitch, int width, int height)
// renderLaserShots â FUN_1CDA7/FUN_1D79C/HandleGameOp1A shot visual path.
void InsaneRebel1::renderLaserShots(byte *dst, int pitch, int width, int height) {
- if (_laserBank.numSprites <= 0)
+ static const char kRA1TorpedoTrailLeft[] = "<<&";
+ static const char kRA1TorpedoTrailRight[] = "<<'";
+ const bool torpedoMode = (_gameplayFlags75ff & 0x2) != 0;
+
+ if (_laserBank.numSprites <= 0 && !torpedoMode)
return;
// DAT_2407 lookup used by FUN_1CDA7/FUN_1D79C for timer 1..5 interpolation.
@@ -763,6 +774,19 @@ void InsaneRebel1::renderLaserShots(byte *dst, int pitch, int width, int height)
continue;
}
+ if (torpedoMode) {
+ const int trailY = overlayY + _shipPosY;
+ const int leftTrailX = overlayX + _shipPosX - 0x14 - (timer << 3);
+ const int rightTrailX = overlayX + _shipPosX + 0x14 + (timer << 3);
+ const int leftWidth = getFontBankStringWidth(kRA1TorpedoTrailLeft);
+ const int rightWidth = getFontBankStringWidth(kRA1TorpedoTrailRight);
+ drawFontBankString(dst, pitch, width, height,
+ leftTrailX - leftWidth / 2, trailY, kRA1TorpedoTrailLeft);
+ drawFontBankString(dst, pitch, width, height,
+ rightTrailX - rightWidth / 2, trailY, kRA1TorpedoTrailRight);
+ continue;
+ }
+
// Fallback for non-turret handlers that still run shot overlays.
int leftStartY = overlayY + 0x96;
int rightStartY = overlayY + 0x96;
diff --git a/engines/scumm/insane/insane_rebel1_runlevels.cpp b/engines/scumm/insane/insane_rebel1_runlevels.cpp
index 6e88dd22332..a089917c017 100644
--- a/engines/scumm/insane/insane_rebel1_runlevels.cpp
+++ b/engines/scumm/insane/insane_rebel1_runlevels.cpp
@@ -393,8 +393,8 @@ bool InsaneRebel1::runLevel4() {
return false;
if (_health >= 0) {
- // Phase 2: Torpedo run â torpedo enabled at frame 0x3E by IACT handler.
- // killCount > 0 = torpedo hit, killCount == 0 = missed.
+ // Phase 2: torpedo run. The DOS loop enables torpedo mode at frame
+ // 0x3E and exits early as soon as killCount becomes nonzero.
_activeGameOpcode = 0;
_gameLatch5D = 0;
_gameLatch5F = 0;
@@ -1466,9 +1466,9 @@ bool InsaneRebel1::runLevel15() {
if (_vm->shouldQuit())
return false;
- // Phase 2: final approach and torpedo shot.
- // Original: torpedo enabled at frame 0x18A via _gameplayFlags75ff |= 2.
- // _torpedoFired set by IACT handler when killCount > 0 (torpedo hits exhaust port).
+ // Phase 2: final approach and torpedo shot. The DOS flow enables
+ // torpedo mode at frame 0x18A and completes only after object-state
+ // bit 0x7602 & 2 is set by the exhaust-port hit.
_activeGameOpcode = 0;
_gameLatch5D = 0;
_gameLatch5F = 0;
@@ -1482,6 +1482,11 @@ bool InsaneRebel1::runLevel15() {
return false;
}
+ if (_health >= 0 && !_torpedoFired) {
+ debug(1, "InsaneRebel1: Level 15 torpedo run ended without exhaust-port hit");
+ return false;
+ }
+
if (_health >= 0) {
playCinematic("LVL15/L15END1.ANM");
_maxChapterUnlocked = MAX(_maxChapterUnlocked, (int16)15);
Commit: ad7549ed3c6bebaa15fb347f84005f3789d57321
https://github.com/scummvm/scummvm/commit/ad7549ed3c6bebaa15fb347f84005f3789d57321
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:43+02:00
Commit Message:
SCUMM: RA1: Improve Escape key handling
Changed paths:
engines/scumm/insane/insane_rebel1_menu.cpp
engines/scumm/insane/insane_rebel1_runlevels.cpp
diff --git a/engines/scumm/insane/insane_rebel1_menu.cpp b/engines/scumm/insane/insane_rebel1_menu.cpp
index 0723a76928a..7d3857c673c 100644
--- a/engines/scumm/insane/insane_rebel1_menu.cpp
+++ b/engines/scumm/insane/insane_rebel1_menu.cpp
@@ -323,9 +323,29 @@ bool InsaneRebel1::notifyEvent(const Common::Event &event) {
return true;
}
- if (event.type == Common::EVENT_KEYDOWN && event.kbd.keycode == Common::KEYCODE_ESCAPE) {
- if (_player) {
- debug("Rebel1: ESC pressed - skipping video");
+ if (event.type == Common::EVENT_KEYDOWN && _player) {
+ if (_interactiveVideoActive && !_menuActive &&
+ event.kbd.keycode == Common::KEYCODE_ESCAPE) {
+ debug("Rebel1: ESC pressed during gameplay - opening ScummVM menu");
+ const bool wasPaused = _player->_paused;
+ if (!wasPaused)
+ _player->pause();
+ _vm->openMainMenuDialog();
+ if (!wasPaused)
+ _player->unpause();
+ return true;
+ }
+
+ if (_interactiveVideoActive && !_menuActive &&
+ event.kbd.keycode == Common::KEYCODE_s &&
+ event.kbd.hasFlags(Common::KBD_SHIFT)) {
+ debug("Rebel1: Shift+S pressed - skipping gameplay section");
+ _vm->_smushVideoShouldFinish = true;
+ return true;
+ }
+
+ if (!_interactiveVideoActive && event.kbd.keycode == Common::KEYCODE_ESCAPE) {
+ debug("Rebel1: ESC pressed - skipping cinematic");
_vm->_smushVideoShouldFinish = true;
return true;
}
diff --git a/engines/scumm/insane/insane_rebel1_runlevels.cpp b/engines/scumm/insane/insane_rebel1_runlevels.cpp
index a089917c017..44920dec6b3 100644
--- a/engines/scumm/insane/insane_rebel1_runlevels.cpp
+++ b/engines/scumm/insane/insane_rebel1_runlevels.cpp
@@ -47,6 +47,11 @@ void InsaneRebel1::playCinematic(const char *filename, int32 startFrame) {
if (startFrame > 0)
splayer->setFastForwardToFrame(startFrame);
splayer->play(filename, 12);
+
+ // Level-title text is only meant for the intro cinematic that armed it.
+ // Clear it even when the movie ended through ESC, so it cannot leak into
+ // the next cutscene or gameplay segment.
+ _introTextActive = false;
}
void InsaneRebel1::clearVideoBuffer() {
Commit: 326181870b0deef0548380538ab06b0ec69ceb65
https://github.com/scummvm/scummvm/commit/326181870b0deef0548380538ab06b0ec69ceb65
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:43+02:00
Commit Message:
SCUMM: RA1: Implement level 5
Changed paths:
engines/scumm/insane/insane_rebel1.cpp
engines/scumm/insane/insane_rebel1.h
engines/scumm/insane/insane_rebel1_iact.cpp
engines/scumm/insane/insane_rebel1_render.cpp
engines/scumm/insane/insane_rebel1_runlevels.cpp
diff --git a/engines/scumm/insane/insane_rebel1.cpp b/engines/scumm/insane/insane_rebel1.cpp
index 1984745d429..9c77eb506a6 100644
--- a/engines/scumm/insane/insane_rebel1.cpp
+++ b/engines/scumm/insane/insane_rebel1.cpp
@@ -205,6 +205,8 @@ InsaneRebel1::InsaneRebel1(ScummEngine_v7 *scumm) : Insane(), _vm(scumm) {
_shipPosX = kRA1CenterX;
_shipPosY = kRA1CenterY;
+ _flightAimX = kRA1CenterX;
+ _flightAimY = kRA1CenterY;
_shipDirIndex = 17; // Center of 5x7 grid (2*7 + 3)
_corridorLeftX = kRA1MinX;
@@ -285,6 +287,7 @@ InsaneRebel1::InsaneRebel1(ScummEngine_v7 *scumm) : Insane(), _vm(scumm) {
_pendingRouteIndex = -1;
_levelRouteChoice = 0;
_levelGameplayPhase = 0;
+ _level5SuccessFramesRemaining = 0;
_menuActive = false;
_introTextActive = false;
_introTextStartFrame = 0;
diff --git a/engines/scumm/insane/insane_rebel1.h b/engines/scumm/insane/insane_rebel1.h
index d1b865ab1f6..c7e970975d9 100644
--- a/engines/scumm/insane/insane_rebel1.h
+++ b/engines/scumm/insane/insane_rebel1.h
@@ -86,6 +86,7 @@ public:
bool isInteractiveVideoActive() const { return _interactiveVideoActive; }
int getCurrentLevel() const { return _currentLevel; }
uint16 getActiveGameOpcode() const { return _activeGameOpcode; }
+ uint16 getEffectiveGameOpcode() const;
bool hasFrameGameOpcode(uint16 opcode) const {
return opcode < 32 && (_frameGameOpcodeMask & (1u << opcode)) != 0;
}
@@ -93,6 +94,10 @@ public:
int16 getPerspectiveY() const { return _perspectiveY; }
void projectGameplayPoint(int16 &x, int16 &y) const;
void unprojectGameplayPoint(int16 &x, int16 &y) const;
+ int16 getGameplayCursorX() const;
+ int16 getGameplayCursorY() const;
+ void setGameplayCursor(int16 x, int16 y);
+ void updateFlightVariantCursor();
bool handleFrameObjectTarget(int16 objectId, int16 left, int16 top, int16 width, int16 height,
int codec, uint8 &ra1Param);
void resetFrameObjectState();
@@ -201,6 +206,9 @@ private:
// Original: _DAT_74B6/_74B8 (base=160,100) + _DAT_74BA/_74BC (offset)
int16 _shipPosX;
int16 _shipPosY;
+ // GAME opcode 0x09 uses a separate aim cursor; other handlers target via _shipPos.
+ int16 _flightAimX;
+ int16 _flightAimY;
// Direction sprite index (5x7 grid = 35 sprites, vDir*7 + hDir)
int16 _shipDirIndex;
@@ -463,6 +471,7 @@ private:
// Per-target hit counters for shield generator tracking (Level 4)
int16 _shieldGenHitsA; // Hits on _protectedTargetA
int16 _shieldGenHitsB; // Hits on _protectedTargetB
+ int16 _level5SuccessFramesRemaining; // DOS RunLevel5Flow: 20-frame hold after the third kill
// Level 15 torpedo success latch. The original derives this from
// g_gameplayPhaseFlags bit 1, which is the primary object-state bit for object 7.
diff --git a/engines/scumm/insane/insane_rebel1_iact.cpp b/engines/scumm/insane/insane_rebel1_iact.cpp
index 59e02601943..8a809478938 100644
--- a/engines/scumm/insane/insane_rebel1_iact.cpp
+++ b/engines/scumm/insane/insane_rebel1_iact.cpp
@@ -44,6 +44,9 @@ static inline int16 smoothRebel1Op0BAnalogInput(int16 inputValue, int16 &filtere
return filteredValue;
}
+static const int16 kRA1Op09AimXScale[5] = { 0, 44, 88, 128, 165 };
+static const int16 kRA1Op09AimYScale[5] = { 256, 252, 240, 221, 196 };
+
// LVL1 stage-2 0x5D damage/event codes. The gameplay stream exposes low record ids
// (6..18), while the recovered outer loop compares the post-latch state against the
// later translated values seen in the executable. Accept both representations.
@@ -579,6 +582,70 @@ void InsaneRebel1::unprojectGameplayPoint(int16 &x, int16 &y) const {
x = (int16)(x + _perspectiveX);
}
+uint16 InsaneRebel1::getEffectiveGameOpcode() const {
+ if (hasFrameGameOpcode(0x1A))
+ return 0x1A;
+ if (hasFrameGameOpcode(0x19))
+ return 0x19;
+ if (hasFrameGameOpcode(0x0B))
+ return 0x0B;
+ if (hasFrameGameOpcode(0x0A))
+ return 0x0A;
+ if (hasFrameGameOpcode(0x09))
+ return 0x09;
+ if (hasFrameGameOpcode(0x08))
+ return 0x08;
+ if (hasFrameGameOpcode(0x07))
+ return 0x07;
+
+ return _activeGameOpcode;
+}
+
+int16 InsaneRebel1::getGameplayCursorX() const {
+ return (getEffectiveGameOpcode() == 0x09) ? _flightAimX : _shipPosX;
+}
+
+int16 InsaneRebel1::getGameplayCursorY() const {
+ return (getEffectiveGameOpcode() == 0x09) ? _flightAimY : _shipPosY;
+}
+
+void InsaneRebel1::setGameplayCursor(int16 x, int16 y) {
+ if (getEffectiveGameOpcode() == 0x09) {
+ _flightAimX = x;
+ _flightAimY = y;
+ } else {
+ _shipPosX = x;
+ _shipPosY = y;
+ }
+}
+
+void InsaneRebel1::updateFlightVariantCursor() {
+ if (getEffectiveGameOpcode() != 0x09)
+ return;
+
+ const int bucket = CLIP<int>(ABS(_rollAccum) >> 8, 0, ARRAYSIZE(kRA1Op09AimXScale) - 1);
+ int32 xScale = kRA1Op09AimXScale[bucket];
+ if (_rollAccum > 0)
+ xScale = -xScale;
+
+ // Assembly-verified 0x09 layout:
+ // ship sprite center = (_74B6 + _74BA, _74B8 + _74BC)
+ // cursor center = (_74BE, _74C0)
+ // In ScummVM the flight sprite center already lives in _shipPos.
+ const int16 shipBaseX = _shipPosX;
+ const int16 shipBaseY = _shipPosY;
+ const int32 liftTerm = (int32)_liftSmooth - 0x0F;
+ _flightAimX = CLIP<int32>(shipBaseX + ((liftTerm * xScale) >> 8), kRA1MinX, kRA1MaxX);
+ _flightAimY = CLIP<int32>(shipBaseY + ((liftTerm * kRA1Op09AimYScale[bucket]) >> 8),
+ kRA1MinY, kRA1MaxY);
+
+ if (_currentLevel == 4) {
+ debug(1, "RA1 op09 cursor: frame=%d shipBase=(%d,%d) shipPos=(%d,%d) aim=(%d,%d) roll=%d lift=%d bucket=%d dir=%d persp=(%d,%d)",
+ _gameCounter, shipBaseX, shipBaseY, _shipPosX, _shipPosY, _flightAimX, _flightAimY,
+ _rollAccum, _liftSmooth, bucket, _shipDirIndex, _perspectiveX, _perspectiveY);
+ }
+}
+
// preprocessMouseAxes â FUN_231BE (0x231BE) centered-axis output law, adapted to
// ScummVM's absolute 320x200 mouse space.
// Preserve the DOS bias/offset persistence and one-frame jump latch from
@@ -736,6 +803,8 @@ void InsaneRebel1::updateShipPhysics() {
_liftSmooth = 0;
_shipPosX = kRA1CenterX;
_shipPosY = kRA1CenterY;
+ _flightAimX = kRA1CenterX;
+ _flightAimY = kRA1CenterY;
_damageFlags = 0;
_prevDamageFlags = 0;
_damageCooldown = 0;
@@ -1342,6 +1411,16 @@ void InsaneRebel1::updateAsteroidPhysics() {
}
}
+ // Level 5 Phase 1: DOS RunLevel5Flow exits L5PLAY only after killCount stays
+ // above 2 for 20 frontend frames. That countdown is carried by the runlevel,
+ // not by opcode 0x07 itself.
+ if (_currentLevel == 4 && _levelGameplayPhase == 1 &&
+ _level5SuccessFramesRemaining > 0 && _killCount > 2 && !_vm->_smushVideoShouldFinish) {
+ _level5SuccessFramesRemaining--;
+ if (_level5SuccessFramesRemaining == 0)
+ _vm->_smushVideoShouldFinish = true;
+ }
+
// Level 15 Phase 2: enable torpedo at frame 0x18A, expose the protected
// target IDs used by the original flow, and finish when object-state bit
// 0x7602 & 2 becomes set.
@@ -1736,8 +1815,13 @@ void InsaneRebel1::handleGameChunk(int32 subSize, Common::SeekableReadStream &b)
uint32 param2 = b.readUint32BE();
uint32 param3 = b.readUint32BE();
uint32 param4 = b.readUint32BE();
- debug(5, "RA1 GAME 0x%02x: counter=%d params=(%d,%d,%d)",
- opcode, _gameCounter, param2, param3, param4);
+ if (opcode == 0x09 && _currentLevel == 4) {
+ debug(1, "RA1 GAME 0x09: counter=%d params=(%d,%d,%d) opcodeMask=0x%08x",
+ _gameCounter, param2, param3, param4, _frameGameOpcodeMask);
+ } else {
+ debug(5, "RA1 GAME 0x%02x: counter=%d params=(%d,%d,%d)",
+ opcode, _gameCounter, param2, param3, param4);
+ }
}
break;
@@ -1755,7 +1839,8 @@ void InsaneRebel1::processShot() {
// On-foot mode: only spawn when in aiming stance (dirIndex 11-19) or flags force it.
// Original: if (((10 < g_shipDirIndex) && (g_shipDirIndex < 0x14)) || ((DAT_000075fe & 8) != 0))
- const bool onFootMode = (_activeGameOpcode == 0x19 || _activeGameOpcode == 0x1A);
+ const uint16 effectiveOpcode = getEffectiveGameOpcode();
+ const bool onFootMode = (effectiveOpcode == 0x19 || effectiveOpcode == 0x1A);
if (onFootMode) {
if (!(_shipDirIndex > 10 && _shipDirIndex < 20) && !(_gameplayFlags75fe & 8))
return;
@@ -1777,7 +1862,7 @@ void InsaneRebel1::processShot() {
// On-foot: character position (g_shipOffsetX + g_perspectiveX)
// Turret: perspective-adjusted center
// Flight: cursor position
- const bool turretMode = (_activeGameOpcode == 0x08 || _activeGameOpcode == 0x0A);
+ const bool turretMode = (effectiveOpcode == 0x08 || effectiveOpcode == 0x0A);
int16 originX, originY;
if (onFootMode) {
originX = _onFootCharX + kOnFootCenterX;
@@ -1790,17 +1875,25 @@ void InsaneRebel1::processShot() {
originY = _shipPosY;
}
+ const int16 cursorX = getGameplayCursorX();
+ const int16 cursorY = getGameplayCursorY();
_shotSlots[slot].timer = (_gameplayFlags75ff & 0x2) ? 2 : 5;
- _shotSlots[slot].posX = _shipPosX;
- _shotSlots[slot].posY = _shipPosY;
+ _shotSlots[slot].posX = cursorX;
+ _shotSlots[slot].posY = cursorY;
_shotSlots[slot].centerX = originX;
_shotSlots[slot].centerY = originY;
_shotSlots[slot].variant = _shotAlternator;
_shotAlternator = 1 - _shotAlternator;
playSfx((_gameplayFlags75ff & 0x2) ? kSfxAlert : kSfxLaserShot, 127, 0);
- debug(5, "RA1 shot: slot=%d pos=(%d,%d) origin=(%d,%d)", slot,
- _shotSlots[slot].posX, _shotSlots[slot].posY, originX, originY);
+ if (effectiveOpcode == 0x09 || _currentLevel == 4) {
+ debug(1, "RA1 shot: opcode=0x%02x frame=%d slot=%d cursor=(%d,%d) origin=(%d,%d) dir=%d mode=%d",
+ effectiveOpcode, _gameCounter, slot, cursorX, cursorY, originX, originY,
+ _shipDirIndex, _flyControlMode);
+ } else {
+ debug(5, "RA1 shot: slot=%d pos=(%d,%d) origin=(%d,%d)", slot,
+ cursorX, cursorY, originX, originY);
+ }
}
// checkTargetHit â FUN_1C0EF (0x1C0EF). AABB target detection with snap tolerance.
@@ -1808,8 +1901,8 @@ void InsaneRebel1::processShot() {
// UnprojectScreenPoint(), then reprojects the snapped cursor center after a hit.
void InsaneRebel1::checkTargetHit(int16 targetIdx, int16 left, int16 top, int16 right, int16 bottom) {
int16 snap = _tuning.snap;
- int16 curX = _shipPosX;
- int16 curY = _shipPosY;
+ int16 curX = getGameplayCursorX();
+ int16 curY = getGameplayCursorY();
unprojectGameplayPoint(curX, curY);
const int slot = _targetCount;
@@ -1866,9 +1959,10 @@ void InsaneRebel1::checkTargetHit(int16 targetIdx, int16 left, int16 top, int16
// Match FUN_1C0EF: snap in unprojected space, then project back
// into the current gameplay window before rendering the pointer.
if (snap > 0) {
- _shipPosX = (left + right) / 2;
- _shipPosY = (top + bottom) / 2;
- projectGameplayPoint(_shipPosX, _shipPosY);
+ int16 snappedX = (left + right) / 2;
+ int16 snappedY = (top + bottom) / 2;
+ projectGameplayPoint(snappedX, snappedY);
+ setGameplayCursor(snappedX, snappedY);
}
debug(3, "RA1 HIT: target=%d gost=%d pos=(%d,%d) score=%d kills=%d bangSprites=%d",
diff --git a/engines/scumm/insane/insane_rebel1_render.cpp b/engines/scumm/insane/insane_rebel1_render.cpp
index 23b287561fc..e634893131a 100644
--- a/engines/scumm/insane/insane_rebel1_render.cpp
+++ b/engines/scumm/insane/insane_rebel1_render.cpp
@@ -34,14 +34,14 @@ static inline int ra1OverlayViewOffsetX(const InsaneRebel1 *rebel1) {
// In opcode 0x0B (FUN_1CDA7), marker/shot coordinates are in the gameplay
// window. Under ScummVM's FUN_224FD crop emulation, shift them into the
// 384-wide source buffer so they stay aligned after the source-window crop.
- return (rebel1->getActiveGameOpcode() == 0x0B) ? rebel1->getPerspectiveX() : 0;
+ return (rebel1->getEffectiveGameOpcode() == 0x0B) ? rebel1->getPerspectiveX() : 0;
}
static inline int ra1OverlayViewOffsetY(const InsaneRebel1 *rebel1) {
if (!rebel1 || !rebel1->isInteractiveVideoActive())
return 0;
- return (rebel1->getActiveGameOpcode() == 0x0B) ? rebel1->getPerspectiveY() : 0;
+ return (rebel1->getEffectiveGameOpcode() == 0x0B) ? rebel1->getPerspectiveY() : 0;
}
static void drawBankString(const RA1SpriteBank &bank, byte *dst, int pitch, int width, int height,
@@ -267,6 +267,44 @@ static const RA1ShotEmitterPair kRA1ShotEmitters251A[27] = {
{ -15, -14, 16, 5 }, { 0, -38, -14, 37 }
};
+// DAT_25EC/DAT_25F0 and DAT_28BC in ASSAULT.EXE. GAME opcode 0x09 uses these
+// emitter offsets instead of the generic edge-beam fallback.
+static const RA1ShotEmitterPair kRA1FlightShotEmitters25EC[45] = {
+ { -38, -14, 37, 6 }, { 37, -14, 37, 7 }, { 42, -11, -40, 11 }, { -37, -6, 38, 14 }, { -37, -5, 38, 15 },
+ { -35, -19, 36, 11 }, { -35, -18, 35, 12 }, { -37, -15, 36, 16 }, { -37, -11, 34, 19 }, { -37, -10, 34, 20 },
+ { -31, -24, 33, 16 }, { -31, -23, 33, 17 }, { -32, -19, 33, 22 }, { -34, -17, 29, 24 }, { -35, -15, 28, 25 },
+ { -25, -28, 29, 21 }, { -25, -28, 29, 20 }, { -30, -25, 28, 27 }, { -30, -20, 24, 28 }, { -31, -19, 23, 29 },
+ { -18, -31, 26, 25 }, { -18, -31, 25, 25 }, { -23, -28, 22, 30 }, { -25, -24, 18, 32 }, { -26, -24, 18, 32 },
+ { 35, -19, -35, 12 }, { 36, -19, -35, 11 }, { 39, -16, -38, 16 }, { 37, -12, -35, 19 }, { 37, -11, -34, 20 },
+ { 30, -23, -33, 17 }, { 31, -23, -33, 17 }, { 33, -20, -32, 21 }, { 35, -17, -29, 23 }, { 34, -17, -30, 24 },
+ { 25, -28, -30, 21 }, { 26, -27, -28, 23 }, { 27, -25, -28, 25 }, { 29, -20, -24, 27 }, { 30, -22, -24, 28 },
+ { 18, -32, -26, 25 }, { 19, -31, -25, 26 }, { 22, -28, -22, 29 }, { 25, -24, -19, 31 }, { 25, -24, -17, 31 }
+};
+
+static const RA1ShotEmitterPair kRA1FlightShotEmitters25F0[45] = {
+ { 37, -14, -37, 6 }, { -38, -12, -36, 7 }, { -41, -10, 41, 10 }, { 39, -6, -36, 13 }, { 38, -5, -36, 15 },
+ { 41, -8, -39, 1 }, { 40, -7, -38, 1 }, { 41, -4, -40, 5 }, { 39, -1, -40, 9 }, { 39, 1, -40, 10 },
+ { -38, -5, 42, -3 }, { -39, -4, 42, -1 }, { -43, 0, 40, 2 }, { -41, 2, 39, 5 }, { -42, 4, 37, 6 },
+ { -36, -10, 42, 4 }, { -36, -10, 42, 4 }, { -42, -6, 41, 9 }, { -41, -3, 37, 11 }, { -42, -2, 36, 12 },
+ { -33, -15, 42, 10 }, { -33, -15, 42, 10 }, { -39, -12, 39, 14 }, { -39, -8, 36, 17 }, { -41, -8, 33, 17 },
+ { -41, -8, 39, 1 }, { -40, -8, 40, 1 }, { -43, -4, 42, 4 }, { -39, 0, 41, 7 }, { -39, 1, 41, 9 },
+ { 37, -4, -43, -1 }, { 39, -4, -42, -1 }, { 42, -1, -43, 3 }, { 42, 1, -39, 5 }, { 42, 2, -38, 6 },
+ { 36, -10, -43, 5 }, { 38, -8, -41, 5 }, { 38, -7, -42, 8 }, { 41, -3, -37, 11 }, { 41, -4, -37, 11 },
+ { 32, -15, -42, 10 }, { 34, -14, -40, 11 }, { 36, -12, -38, 13 }, { 39, -10, -35, 17 }, { 41, -9, -33, 17 }
+};
+
+static const RA1ShotEmitterPair kRA1FlightShotEmitters28BC[45] = {
+ { -18, 0, 18, 0 }, { -18, -1, 18, 0 }, { -18, 0, 18, 0 }, { -18, 0, 17, 0 }, { -18, -1, 18, -1 },
+ { -14, -3, 19, 3 }, { -15, -5, 20, 0 }, { -17, -4, 18, 1 }, { -15, -4, 18, 1 }, { -19, -4, 19, 2 },
+ { -13, -9, 20, 2 }, { -16, -8, 19, 3 }, { -15, -9, 21, 3 }, { -14, -4, 18, 5 }, { -14, -2, 17, 6 },
+ { -9, -11, 19, 1 }, { -8, -10, 21, 2 }, { -11, -11, 18, 3 }, { -13, -11, 20, 4 }, { -14, -9, 16, 4 },
+ { -7, -13, 20, 4 }, { -10, -14, 19, 5 }, { -11, -14, 19, 5 }, { -10, -13, 18, 6 }, { -11, -11, 16, 6 },
+ { 14, -5, -18, 0 }, { 16, -6, -19, -1 }, { 17, -5, -20, 1 }, { 17, -5, -19, 2 }, { 17, -2, -16, 2 },
+ { 12, -8, -19, 3 }, { 13, -8, -19, 3 }, { 11, -7, -19, 3 }, { 14, -8, -17, 2 }, { 14, -7, -17, 3 },
+ { -16, 3, 10, -12 }, { -20, 3, 11, -11 }, { -20, 4, 10, -10 }, { -17, 4, 12, -11 }, { -19, 3, 13, -11 },
+ { -18, 3, 5, -13 }, { -17, 3, 7, -14 }, { -18, 3, 8, -15 }, { -19, 3, 9, -11 }, { -16, 5, 11, -11 }
+};
+
// Small subset of FUN_20D43 draw flags used by RA1 shot sprites.
static void renderSpriteWithFlags(byte *dst, int pitch, int width, int height,
int x, int y, const RA1Sprite &spr, uint32 flags) {
@@ -459,6 +497,9 @@ void InsaneRebel1::procPostRendering(byte *renderBitmap, int32 codecparam, int32
(_activeGameOpcode == 0x09 || _activeGameOpcode == 0x0A ||
_activeGameOpcode == 0x0B || _activeGameOpcode == 0x1A));
if (hasTargetingPipeline) {
+ const bool flightVariantTargetingMode =
+ hasFrameGameOpcode(0x09) ||
+ (!haveFrameGameOpcodes && _activeGameOpcode == 0x09);
const bool turretTargetingMode =
hasFrameGameOpcode(0x0A) ||
(!haveFrameGameOpcodes && _activeGameOpcode == 0x0A);
@@ -483,6 +524,8 @@ void InsaneRebel1::procPostRendering(byte *renderBitmap, int32 codecparam, int32
const int16 shipOffsetY = (int16)(_posAccumY >> 8);
_shipPosX = (int16)(kRA1CenterX + shipOffsetX);
_shipPosY = (int16)((kRA1CenterY + shipOffsetY - 0x23) - (shipOffsetY >> 3));
+ } else if (flightVariantTargetingMode) {
+ updateFlightVariantCursor();
}
} else {
// Keep lock/target accumulators quiescent when current handler doesn't
@@ -518,7 +561,7 @@ void InsaneRebel1::renderTargetBoxes(byte *dst, int pitch, int width, int height
}
const int overlayX = ra1OverlayViewOffsetX(this);
const RA1SpriteBank &markerBank = (_techFontBank.numSprites > 0) ? _techFontBank : _hudFontBank;
- const bool projectTargetMarkers = (_activeGameOpcode == 0x0B);
+ const bool projectTargetMarkers = (getEffectiveGameOpcode() == 0x0B);
for (int i = _targetCount - 1; i >= 0; --i) {
if (i >= kMaxTargetBoxes)
@@ -567,8 +610,8 @@ void InsaneRebel1::renderTargeting(byte *dst, int pitch, int width, int height)
marker[0] = (char)((altMarkerSet ? 'y' : 'e') + (_targetAnimCounter & 3));
}
- int cursorX = CLIP<int>(overlayX + _shipPosX, 0, width - 1);
- int cursorY = CLIP<int>(overlayY + _shipPosY, 0, height - 1);
+ int cursorX = CLIP<int>(overlayX + getGameplayCursorX(), 0, width - 1);
+ int cursorY = CLIP<int>(overlayY + getGameplayCursorY(), 0, height - 1);
drawCenteredBankGlyph(markerBank, dst, pitch, width, height, cursorX, cursorY, marker[0]);
if (altMarkerSet) {
@@ -624,7 +667,7 @@ void InsaneRebel1::renderGostSlots(byte *dst, int pitch, int width, int height)
}
const int overlayX = ra1OverlayViewOffsetX(this);
- const bool projectGostMarkers = (_activeGameOpcode == 0x0B);
+ const bool projectGostMarkers = (getEffectiveGameOpcode() == 0x0B);
for (int i = 0; i < kMaxGostSlots; i++) {
if (_gostSlots[i].targetId != 0 && _gostSlots[i].frame < 10) {
int sprIdx = _gostSlots[i].frame;
@@ -684,17 +727,20 @@ void InsaneRebel1::renderLaserShots(byte *dst, int pitch, int width, int height)
const int overlayY = ra1OverlayViewOffsetY(this);
const int leftStartX = 0;
const int rightStartX = 0x13F; // 319
- const bool onFootMode = (_activeGameOpcode == 0x19 || _activeGameOpcode == 0x1A);
- const bool turretMode = (_activeGameOpcode == 0x08 || _activeGameOpcode == 0x0A);
+ const uint16 effectiveOpcode = getEffectiveGameOpcode();
+ const bool onFootMode = (effectiveOpcode == 0x19 || effectiveOpcode == 0x1A);
+ const bool turretMode = (effectiveOpcode == 0x08 || effectiveOpcode == 0x0A);
+ const bool flightVariantMode = (effectiveOpcode == 0x09);
const int shipBaseX = turretMode ? (kRA1CenterX + (_perspectiveX - 0x20)) : _shipPosX;
- const int shipBaseY = turretMode ? (kRA1CenterY + (_perspectiveY - 0x17)) : (overlayY + _shipPosY);
+ const int shipBaseY = turretMode ? (kRA1CenterY + (_perspectiveY - 0x17))
+ : (flightVariantMode ? _shipPosY : (overlayY + _shipPosY));
for (int i = 0; i < kMaxShotSlots; i++) {
if (_shotSlots[i].timer > 0 && _shotSlots[i].timer <= spritesPerSet) {
const int timer = _shotSlots[i].timer;
const int frame = spritesPerSet - timer;
- const int targetX = CLIP<int>(overlayX + _shipPosX, 0, width - 1);
- const int targetY = CLIP<int>(overlayY + _shipPosY, 0, height - 1);
+ const int targetX = CLIP<int>(overlayX + getGameplayCursorX(), 0, width - 1);
+ const int targetY = CLIP<int>(overlayY + getGameplayCursorY(), 0, height - 1);
if (onFootMode) {
// HandleGameOp1A_OnFootVariant: single beam from character to crosshair.
@@ -774,10 +820,51 @@ void InsaneRebel1::renderLaserShots(byte *dst, int pitch, int width, int height)
continue;
}
+ if (flightVariantMode) {
+ if ((_gameplayFlags75fe & 8) != 0)
+ continue;
+
+ if (_shipDirIndex < 0 || _shipDirIndex >= ARRAYSIZE(kRA1FlightShotEmitters28BC))
+ continue;
+
+ const RA1ShotEmitterPair &emit =
+ (_flyControlMode == 1)
+ ? ((_shotSlots[i].variant != 0) ? kRA1FlightShotEmitters25EC[_shipDirIndex]
+ : kRA1FlightShotEmitters25F0[_shipDirIndex])
+ : kRA1FlightShotEmitters28BC[_shipDirIndex];
+ const int start1X = shipBaseX + emit.x1;
+ const int start1Y = shipBaseY + emit.y1;
+ const int start2X = shipBaseX + emit.x2;
+ const int start2Y = shipBaseY + emit.y2;
+ const int dir1 = ra1ShotDirection((int16)start1X, (int16)start1Y, (int16)targetX, (int16)targetY);
+ const int dir2 = ra1ShotDirection((int16)start2X, (int16)start2Y, (int16)targetX, (int16)targetY);
+ const int sprIdx1 = MIN<int>(ABS(dir1), _laserBank.numSprites - 1);
+ const int sprIdx2 = MIN<int>(ABS(dir2), _laserBank.numSprites - 1);
+ const uint32 flags1 = 0x83 | ((dir1 < 0) ? 0x2000 : 0);
+ const uint32 flags2 = 0x83 | ((dir2 < 0) ? 0x2000 : 0);
+ const int interp1X = start1X + (((targetX - start1X) * lerp) >> 3);
+ const int interp1Y = start1Y + (((targetY - start1Y) * lerp) >> 3);
+ const int interp2X = start2X + (((targetX - start2X) * lerp) >> 3);
+ const int interp2Y = start2Y + (((targetY - start2Y) * lerp) >> 3);
+
+ if (_currentLevel == 4) {
+ debug(1, "RA1 op09 shotRender: frame=%d timer=%d shipBase=(%d,%d) target=(%d,%d) emit1=(%d,%d) emit2=(%d,%d) dir=%d variant=%d mode=%d",
+ _gameCounter, timer, shipBaseX, shipBaseY, targetX, targetY,
+ start1X, start1Y, start2X, start2Y, _shipDirIndex,
+ _shotSlots[i].variant, _flyControlMode);
+ }
+
+ renderSpriteWithFlags(dst, pitch, width, height,
+ interp1X, interp1Y, _laserBank.sprites[sprIdx1], flags1);
+ renderSpriteWithFlags(dst, pitch, width, height,
+ interp2X, interp2Y, _laserBank.sprites[sprIdx2], flags2);
+ continue;
+ }
+
if (torpedoMode) {
- const int trailY = overlayY + _shipPosY;
- const int leftTrailX = overlayX + _shipPosX - 0x14 - (timer << 3);
- const int rightTrailX = overlayX + _shipPosX + 0x14 + (timer << 3);
+ const int trailY = targetY;
+ const int leftTrailX = targetX - 0x14 - (timer << 3);
+ const int rightTrailX = targetX + 0x14 + (timer << 3);
const int leftWidth = getFontBankStringWidth(kRA1TorpedoTrailLeft);
const int rightWidth = getFontBankStringWidth(kRA1TorpedoTrailRight);
drawFontBankString(dst, pitch, width, height,
@@ -1010,11 +1097,13 @@ void InsaneRebel1::renderShip(byte *dst, int pitch, int width, int height) {
const RA1Sprite &spr = shipBank->sprites[_shipDirIndex];
- // In 0x08/0x0A turret handlers, _shipPos holds pointer center (_74BE/_74C0),
- // while ship sprite center is still (_74B6+_74BA, _74B8+_74BC).
+ // In 0x08/0x0A turret handlers, _shipPos holds targeting/cursor state, while
+ // the ship sprite is still anchored from camera perspective + ship drift
+ // offsets. Flight handlers already store the ship sprite center in _shipPos.
int shipScreenX = _shipPosX;
int shipScreenY = _shipPosY;
- if (_activeGameOpcode == 0x08 || _activeGameOpcode == 0x0A) {
+ const uint16 effectiveOpcode = getEffectiveGameOpcode();
+ if (effectiveOpcode == 0x08 || effectiveOpcode == 0x0A) {
shipScreenX = kRA1CenterX + (_perspectiveX - 0x20);
shipScreenY = kRA1CenterY + (_perspectiveY - 0x17);
}
@@ -1033,11 +1122,13 @@ void InsaneRebel1::renderExplosions(byte *dst, int pitch, int width, int height)
const int overlayX = ra1OverlayViewOffsetX(this);
const int overlayY = ra1OverlayViewOffsetY(this);
- // In 0x08/0x0A turret handlers, explosion anchors use ship center
- // (_74B6+_74BA, _74B8+_74BC), not pointer center (_74BE/_74C0).
+ // In 0x08/0x0A turret handlers, explosion anchors use the ship center, not
+ // the targeting cursor stored in _shipPos. Flight handlers already keep the
+ // ship center in _shipPos.
int shipScreenX = overlayX + _shipPosX;
int shipScreenY = overlayY + _shipPosY;
- if (_activeGameOpcode == 0x08 || _activeGameOpcode == 0x0A) {
+ const uint16 effectiveOpcode = getEffectiveGameOpcode();
+ if (effectiveOpcode == 0x08 || effectiveOpcode == 0x0A) {
shipScreenX = kRA1CenterX + (_perspectiveX - 0x20);
shipScreenY = kRA1CenterY + (_perspectiveY - 0x17);
}
diff --git a/engines/scumm/insane/insane_rebel1_runlevels.cpp b/engines/scumm/insane/insane_rebel1_runlevels.cpp
index 44920dec6b3..e5a88ef4f7d 100644
--- a/engines/scumm/insane/insane_rebel1_runlevels.cpp
+++ b/engines/scumm/insane/insane_rebel1_runlevels.cpp
@@ -438,7 +438,8 @@ bool InsaneRebel1::runLevel5() {
_currentLevel = 4;
loadLevelSprites(5);
- loadTuningForLevel(4);
+ // DOS RunLevel5Flow passes segment 6 for L5PLAY and segment 7 for L5PLAY2.
+ loadTuningForLevel(6);
beginLevelTitleOverlay(4);
playCinematic("LVL5/L5INTRO.ANM");
@@ -447,6 +448,7 @@ bool InsaneRebel1::runLevel5() {
while (!_vm->shouldQuit()) {
loadRA1Nut("LVL5/L5LASER.NUT", _laserBank);
+ loadTuningForLevel(6);
_flyControlMode = 1;
_health = kMaxHealth;
_damageFlags = 0;
@@ -463,7 +465,20 @@ bool InsaneRebel1::runLevel5() {
_gameLatch5F = 0;
_gameplayFlags75ff = 0;
_killCount = 0;
- _levelGameplayPhase = 0;
+ _targetCount = 0;
+ _prevTargetCount = 0;
+ _lastHitTarget = 0;
+ _shipPosX = kRA1CenterX;
+ _shipPosY = kRA1CenterY;
+ _shipDirIndex = 17;
+ _rollAccum = 0;
+ _liftSmooth = 0;
+ _posAccumX = 0;
+ _posAccumY = 0;
+ _perspectiveX = 0;
+ _perspectiveY = 0;
+ _levelGameplayPhase = 1;
+ _level5SuccessFramesRemaining = 0x14;
memset(_inputHistoryX, 0, sizeof(_inputHistoryX));
memset(_inputHistoryY, 0, sizeof(_inputHistoryY));
memset(_viewHistoryX, 0, sizeof(_viewHistoryX));
@@ -506,10 +521,13 @@ bool InsaneRebel1::runLevel5() {
return false;
loadRA1Nut("LVL5/L5LASER2.NUT", _laserBank);
+ loadTuningForLevel(7);
_activeGameOpcode = 0;
_gameLatch5D = 0;
_gameLatch5F = 0;
_killCount = 0;
+ _levelGameplayPhase = 2;
+ _level5SuccessFramesRemaining = 0;
playInteractiveVideo("LVL5/L5PLAY2.ANM");
if (_vm->shouldQuit())
return false;
Commit: 35220322a7da2dc60464ee8dc300ba22c9e2e6ba
https://github.com/scummvm/scummvm/commit/35220322a7da2dc60464ee8dc300ba22c9e2e6ba
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:44+02:00
Commit Message:
SCUMM: RA1: Improve collisions
Changed paths:
engines/scumm/insane/insane_rebel1_iact.cpp
diff --git a/engines/scumm/insane/insane_rebel1_iact.cpp b/engines/scumm/insane/insane_rebel1_iact.cpp
index 8a809478938..fbdcd9445f8 100644
--- a/engines/scumm/insane/insane_rebel1_iact.cpp
+++ b/engines/scumm/insane/insane_rebel1_iact.cpp
@@ -875,45 +875,7 @@ void InsaneRebel1::updateShipPhysics() {
_shipPosX = CLIP<int16>(_shipPosX, kRA1MinX, kRA1MaxX);
_shipPosY = CLIP<int16>(_shipPosY, kRA1MinY, kRA1MaxY);
- // --- Step 7: Corridor collision (FUN_1C54D) ---
- // Wall contact forces position accumulators to corridor edge and sets
- // damage flags. Flag bit 0x10 (zone hit) suppresses damage bits only.
- {
- bool hasZoneHit = (_damageFlags & 0x10) != 0;
-
- if (_shipPosX > _corridorRightX) {
- _posAccumX = (int32)(_corridorRightX - kRA1CenterX) * 0x100;
- _shipPosX = _corridorRightX;
- if (!hasZoneHit) {
- if (_rollAccum > -0x100)
- _rollAccum = -0x100; // Push left
- _damageFlags |= 0x02; // Right wall
- }
- }
- if (_shipPosX < _corridorLeftX) {
- _posAccumX = (int32)(_corridorLeftX - kRA1CenterX) * 0x100;
- _shipPosX = _corridorLeftX;
- if (!hasZoneHit) {
- if (_rollAccum < 0x100)
- _rollAccum = 0x100; // Push right
- _damageFlags |= 0x04; // Left wall
- }
- }
- if (_shipPosY < _corridorTopY) {
- _posAccumY = (int32)(_corridorTopY - kRA1CenterY) * 0x100 + 0x100;
- _shipPosY = _corridorTopY;
- if (!hasZoneHit)
- _damageFlags |= 0x01;
- }
- if (_shipPosY > _corridorBottomY) {
- _posAccumY = (int32)(_corridorBottomY - kRA1CenterY) * 0x100 - 0x100;
- _shipPosY = _corridorBottomY;
- if (!hasZoneHit)
- _damageFlags |= 0x08;
- }
- }
-
- // --- Step 8: Perspective offsets (SetCameraOffset) ---
+ // --- Step 7: Perspective offsets (SetCameraOffset) ---
// FUN_1DEB5 computes these linearly from ship offsets:
// viewX = clamp((_74BA + 0x20), 0, 0x40)
// viewY = clamp((_74BC + 0x17), 0, 0x2E)
@@ -930,7 +892,7 @@ void InsaneRebel1::updateShipPhysics() {
// accumulator so side-looking still bends the gameplay projection.
rebuildProjectionTable(CLIP<int16>((int16)(-(_rollAccum >> 7)), -0x1A, 0x1A), 0x1A);
- // --- Step 9: Direction sprite index (FUN_1DEB5 LAB_1e23e) ---
+ // --- Step 8: Direction sprite index (FUN_1DEB5 LAB_1e23e) ---
// Horizontal component from _74CA (rollAccum):
// |rollAccum| <= 0x80: center (0)
// rollAccum > 0x80: ((rollAccum - 0x80) >> 8) * 5 + 5 (right: 5,10,15,20)
@@ -951,7 +913,7 @@ void InsaneRebel1::updateShipPhysics() {
if (_shipBank.numSprites > 0)
_shipDirIndex = CLIP<int16>((int16)(vComponent + hComponent), 0, _shipBank.numSprites - 1);
- // --- Step 10: Damage/event bit synthesis + damage processing ---
+ // --- Step 9: Damage/event bit synthesis + damage processing ---
// RA1 FUN_1B297-style latches from GAME opcodes:
// 0x5D latch 0xFFFF -> bit 0x40 (obstacle/contact)
// 0x5F non-zero + RNG -> bit 0x80 (projectile-like hit)
@@ -1713,6 +1675,47 @@ void InsaneRebel1::handleGameChunk(int32 subSize, Common::SeekableReadStream &b)
_corridorTopY = centerY - corridorHeight / 2;
_corridorRightX = _corridorLeftX + corridorWidth;
_corridorBottomY = _corridorTopY + corridorHeight;
+
+ // Apply 0x0D immediately so it sees the same pre-physics ship state as
+ // the other per-frame GAME latches. Deferring this until after 0x07/0x09
+ // movement made right-wall hits fire too early when steering into a wall.
+ const bool suppressDirectionalDamage = (_damageFlags & 0x10) != 0;
+ const byte oldDirectionalFlags = _damageFlags & 0x0F;
+ if (_health >= 0) {
+ if (_shipPosX < _corridorLeftX) {
+ _posAccumX = (int32)(_corridorLeftX - kRA1CenterX) * 0x100;
+ if (!suppressDirectionalDamage) {
+ if (_rollAccum < 0x100)
+ _rollAccum = 0x100;
+ _damageFlags |= 0x04;
+ }
+ }
+ if (_shipPosX > _corridorRightX) {
+ _posAccumX = (int32)(_corridorRightX - kRA1CenterX) * 0x100;
+ if (!suppressDirectionalDamage) {
+ if (_rollAccum > -0x100)
+ _rollAccum = -0x100;
+ _damageFlags |= 0x02;
+ }
+ }
+ if (_shipPosY < _corridorTopY) {
+ _posAccumY = (int32)(_corridorTopY - kRA1CenterY) * 0x100 + 0x100;
+ if (!suppressDirectionalDamage)
+ _damageFlags |= 0x01;
+ }
+ if (_shipPosY > _corridorBottomY) {
+ _posAccumY = (int32)(_corridorBottomY - kRA1CenterY) * 0x100 - 0x100;
+ if (!suppressDirectionalDamage)
+ _damageFlags |= 0x08;
+ }
+ }
+ if ((_damageFlags & 0x0F) != oldDirectionalFlags) {
+ debug(1, "RA1 0x0D hit: ship=(%d,%d) corridor=[%d,%d]-[%d,%d] flags=0x%02x zoneSuppressed=%d",
+ _shipPosX, _shipPosY,
+ _corridorLeftX, _corridorTopY, _corridorRightX, _corridorBottomY,
+ _damageFlags, suppressDirectionalDamage ? 1 : 0);
+ }
+
debug(5, "RA1 GAME 0x0D: raw=[%d,%d]+(%d,%d) cam=(%d,%d) transformed=[%d,%d]-[%d,%d]",
corridorLeft, corridorTop, corridorWidth, corridorHeight,
_perspectiveX, _perspectiveY,
Commit: aaa9ef1714176428710c3af1361343095119d793
https://github.com/scummvm/scummvm/commit/aaa9ef1714176428710c3af1361343095119d793
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:44+02:00
Commit Message:
SCUMM: RA1: Fix level 4 progression
Changed paths:
engines/scumm/insane/insane_rebel1_render.cpp
engines/scumm/smush/smush_player.cpp
engines/scumm/smush/smush_player.h
engines/scumm/smush/smush_player_ra1.cpp
diff --git a/engines/scumm/insane/insane_rebel1_render.cpp b/engines/scumm/insane/insane_rebel1_render.cpp
index e634893131a..b6262e48ba4 100644
--- a/engines/scumm/insane/insane_rebel1_render.cpp
+++ b/engines/scumm/insane/insane_rebel1_render.cpp
@@ -367,10 +367,20 @@ void InsaneRebel1::procPreRendering(byte *renderBitmap) {
_frameDispatchFlags = 0;
if (_interactiveVideoActive && _player) {
- // FUN_224FD stores absolute 320x200 window origin in a 384x242 frame:
- // X in [0..0x40], Y in [0..0x2E], centered at (0x20,0x17).
- _player->_ra1ViewportOffsetX = _perspectiveX;
- _player->_ra1ViewportOffsetY = _perspectiveY;
+ const bool usePerspectiveViewport =
+ _activeGameOpcode == 0x07 || _activeGameOpcode == 0x08 ||
+ _activeGameOpcode == 0x09 || _activeGameOpcode == 0x0A ||
+ _activeGameOpcode == 0x0B;
+ // Only gameplay handlers that actually execute FUN_224FD own the scrolling
+ // 320x200 window inside the 384x242 buffer. Interactive movies with no
+ // GAME stream (for example LVL4/L4PLAY2.ANM) keep a static camera.
+ if (usePerspectiveViewport) {
+ _player->_ra1ViewportOffsetX = _perspectiveX;
+ _player->_ra1ViewportOffsetY = _perspectiveY;
+ } else {
+ _player->_ra1ViewportOffsetX = 0;
+ _player->_ra1ViewportOffsetY = 0;
+ }
} else if (_player) {
_player->_ra1ViewportOffsetX = 0;
_player->_ra1ViewportOffsetY = 0;
@@ -415,6 +425,11 @@ void InsaneRebel1::procPostRendering(byte *renderBitmap, int32 codecparam, int32
const bool onFootMode = hasFrameGameOpcode(0x19) || hasFrameGameOpcode(0x1A) ||
(!haveFrameGameOpcodes &&
(_activeGameOpcode == 0x19 || _activeGameOpcode == 0x1A));
+ const bool turretMode = hasFrameGameOpcode(0x08) || hasFrameGameOpcode(0x0A) ||
+ (!haveFrameGameOpcodes && (_activeGameOpcode == 0x08 || _activeGameOpcode == 0x0A));
+ const bool flightMode = hasFrameGameOpcode(0x07) || hasFrameGameOpcode(0x09) ||
+ (!haveFrameGameOpcodes &&
+ (_activeGameOpcode == 0x07 || _activeGameOpcode == 0x09));
if (asteroidMode) {
// First-person asteroid/surface handler â opcode 0x0B (FUN_1CDA7).
updateAsteroidPhysics();
@@ -437,12 +452,6 @@ void InsaneRebel1::procPostRendering(byte *renderBitmap, int32 codecparam, int32
renderSprite(renderBitmap, pitch, width, height, drawX, drawY, spr);
}
} else {
- const bool turretMode = hasFrameGameOpcode(0x08) || hasFrameGameOpcode(0x0A) ||
- (!haveFrameGameOpcodes && (_activeGameOpcode == 0x08 || _activeGameOpcode == 0x0A));
- const bool flightMode = hasFrameGameOpcode(0x07) || hasFrameGameOpcode(0x09) ||
- (!haveFrameGameOpcodes &&
- (_activeGameOpcode == 0x07 || _activeGameOpcode == 0x09));
-
// Dispatch movement path by GAME handler family:
// 0x08/0x0A -> FUN_1E6A7/FUN_1D79C (turret/cockpit)
// 0x07/0x09 -> flight-family handlers
@@ -471,7 +480,7 @@ void InsaneRebel1::procPostRendering(byte *renderBitmap, int32 codecparam, int32
// before HUD/screen copy so 0x0B doesn't lag one frame behind the mouse.
// On-foot mode uses SetCameraOffset(0,0) â no viewport crop.
if (_player) {
- if (onFootMode) {
+ if (onFootMode || (!asteroidMode && !turretMode && !flightMode)) {
_player->_ra1ViewportOffsetX = 0;
_player->_ra1ViewportOffsetY = 0;
} else {
diff --git a/engines/scumm/smush/smush_player.cpp b/engines/scumm/smush/smush_player.cpp
index f20a05d3357..030cd2c79de 100644
--- a/engines/scumm/smush/smush_player.cpp
+++ b/engines/scumm/smush/smush_player.cpp
@@ -1355,7 +1355,7 @@ void SmushPlayer::handleFrameObject(int32 subSize, Common::SeekableReadStream &b
assert(chunk_buffer);
b.read(chunk_buffer, chunk_size);
- if (isRA2()) {
+ if (isRA1() || isRA2()) {
ra2RememberLastFobj(codec, chunk_buffer, chunk_size, left, top, width, height);
}
@@ -1398,7 +1398,7 @@ void SmushPlayer::handleFrame(int32 frameSize, Common::SeekableReadStream &b) {
debugC(DEBUG_SMUSH, "SmushPlayer::handleFrame(%d)", _frame);
uint8 *audioChunk = nullptr;
_skipNext = false;
- if (isRA2())
+ if (isRA2() || isRA1())
_hasFrameFobjForGost = false;
bool interactiveRA1 = false;
@@ -1409,9 +1409,13 @@ void SmushPlayer::handleFrame(int32 frameSize, Common::SeekableReadStream &b) {
const uint16 activeOpcode = rebel1->getActiveGameOpcode();
// Opcode 0x0B path (FUN_1CDA7) uses heavy partial-layer composition
// (codec1/2 + FTCH). Force clear there to avoid stale-trail ghosting.
- // Keep a conservative fallback for early L2 frames before first 0x0B arrives.
+ // Keep conservative fallbacks for the early L2 frames before first 0x0B
+ // arrives and for L4PLAY2, whose static 320x180 FTCH background leaves the
+ // uncovered top band reliant on a clean frame base.
forceInteractiveClearRA1 = interactiveRA1 &&
- (activeOpcode == 0x0B || (activeOpcode == 0 && rebel1->getCurrentLevel() == 1));
+ (activeOpcode == 0x0B ||
+ (activeOpcode == 0 && rebel1->getCurrentLevel() == 1) ||
+ _seekFile.equalsIgnoreCase("LVL4/L4PLAY2.ANM"));
}
// Keep the previous decoded frame (without post-render overlays) as delta source.
@@ -1531,7 +1535,9 @@ void SmushPlayer::handleFrame(int32 frameSize, Common::SeekableReadStream &b) {
handleLoad(subSize, b);
break;
case MKTAG('G','O','S','T'):
- if (isRA2() || isRA1())
+ if (isRA1())
+ ra1HandleGost(subSize, b);
+ else if (isRA2())
ra2HandleGost(subSize, b);
break;
// RA1-specific chunk types: skip gracefully
diff --git a/engines/scumm/smush/smush_player.h b/engines/scumm/smush/smush_player.h
index de3093cfe59..4f6b4f2ab3f 100644
--- a/engines/scumm/smush/smush_player.h
+++ b/engines/scumm/smush/smush_player.h
@@ -191,8 +191,8 @@ private:
int _ra1ViewportOffsetX;
int _ra1ViewportOffsetY;
- // RA2: Most recently decoded FOBJ in the current frame, used by GOST chunks
- // to re-render the same sprite payload at a different position.
+ // RA1/RA2: Most recently decoded FOBJ in the current frame, used by GOST
+ // chunks to re-render the same sprite payload at a different position.
byte *_lastFobjData;
int32 _lastFobjDataSize;
int _lastFobjCodec;
@@ -361,6 +361,7 @@ private:
int left, int top, int width, int height);
void ra2RememberLastFobj(int codec, const byte *data, int32 dataSize,
int left, int top, int width, int height);
+ void ra1HandleGost(int32 subSize, Common::SeekableReadStream &b);
void ra2HandleGost(int32 subSize, Common::SeekableReadStream &b);
void ra2ResetDeltaPalette();
SmushFont *ra1GetFont(int font);
diff --git a/engines/scumm/smush/smush_player_ra1.cpp b/engines/scumm/smush/smush_player_ra1.cpp
index 698c64e831e..47b043829f4 100644
--- a/engines/scumm/smush/smush_player_ra1.cpp
+++ b/engines/scumm/smush/smush_player_ra1.cpp
@@ -129,10 +129,12 @@ bool SmushPlayerRebel1::handleGameFetch(int32 subSize, Common::SeekableReadStrea
InsaneRebel1 *rebel1 = static_cast<InsaneRebel1 *>(_insane);
if (rebel1->isInteractiveVideoActive()) {
const uint16 gameOp = rebel1->getActiveGameOpcode();
+ const bool fullWidthStoredPatch = (_storedFobjWidth == _vm->_screenWidth);
// 0x0B (asteroid/surface) and 0x19/0x1A (on-foot) use SetCameraOffset
- // directly â no projection-based FTCH placement.
- if ((gameOp == 0x0B && _storedFobjWidth == _vm->_screenWidth) ||
- gameOp == 0x19 || gameOp == 0x1A) {
+ // directly â no projection-based FTCH placement. Level 4 phase 2
+ // also stores a full-width screen-space patch (320x180) that DOS
+ // restores without the centered 1/4 projection warp.
+ if (fullWidthStoredPatch || gameOp == 0x19 || gameOp == 0x1A) {
left += _ra1ViewportOffsetX;
top += _ra1ViewportOffsetY;
} else {
@@ -159,6 +161,50 @@ bool SmushPlayerRebel1::handleGameFetch(int32 subSize, Common::SeekableReadStrea
return true;
}
+void SmushPlayer::ra1HandleGost(int32 subSize, Common::SeekableReadStream &b) {
+ if (subSize < 12) {
+ warning("SmushPlayer::ra1HandleGost: chunk too small (%d bytes)", subSize);
+ return;
+ }
+
+ const uint32 ghostType = b.readUint32BE();
+ const int32 ghostX = b.readSint32BE();
+ const int32 ghostY = b.readSint32BE();
+
+ if (!_hasFrameFobjForGost || _lastFobjData == nullptr || _lastFobjDataSize <= 0) {
+ debug("RA1 GOST: frame=%d ignored type=0x%08x pos=(%d,%d) (no current-frame FOBJ cached)",
+ _frame, ghostType, ghostX, ghostY);
+ return;
+ }
+
+ uint16 priorityFlags = 0;
+ switch (ghostType) {
+ case 0x1C:
+ priorityFlags = 0x2000;
+ break;
+ case 0x1D:
+ priorityFlags = 0x4000;
+ break;
+ case 0x1E:
+ priorityFlags = 0x6000;
+ break;
+ default:
+ debug("RA1 GOST: frame=%d ignored unknown type=0x%08x pos=(%d,%d)",
+ _frame, ghostType, ghostX, ghostY);
+ return;
+ }
+
+ debug("RA1 GOST: frame=%d type=0x%08x flags=0x%04x pos=(%d,%d) size=%dx%d codec=%d",
+ _frame, ghostType, priorityFlags, ghostX, ghostY,
+ _lastFobjWidth, _lastFobjHeight, _lastFobjCodec);
+
+ // DOS reuses the most recent FOBJ payload for RA1 GOST and places it at the
+ // absolute BE32 coordinates stored in the chunk. Priority bits are identified
+ // here but not yet modeled in the generic ScummVM decode path.
+ decodeFrameObject(_lastFobjCodec, _lastFobjData, ghostX, ghostY,
+ _lastFobjWidth, _lastFobjHeight, _lastFobjDataSize);
+}
+
bool SmushPlayerRebel1::handleGameTextResource(uint32 subType, int32 subSize, Common::SeekableReadStream &b) {
if (subType != MKTAG('T','E','X','T'))
return false;
@@ -185,6 +231,8 @@ bool SmushPlayerRebel1::handleGameAnimHeader(byte *headerContent) {
_specialBuffer = (byte *)calloc(bufSize, 1);
_specialBufferSize = bufSize;
}
+ if (_specialBuffer != nullptr)
+ memset(_specialBuffer, 0, bufSize);
_dst = _specialBuffer;
return true;
}
Commit: b3a580ac85f3cc88fcaf48474de899347610345c
https://github.com/scummvm/scummvm/commit/b3a580ac85f3cc88fcaf48474de899347610345c
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:44+02:00
Commit Message:
SCUMM: RA1: Remove level 4 workaround
Changed paths:
engines/scumm/smush/smush_player.cpp
diff --git a/engines/scumm/smush/smush_player.cpp b/engines/scumm/smush/smush_player.cpp
index 030cd2c79de..c75c4dae429 100644
--- a/engines/scumm/smush/smush_player.cpp
+++ b/engines/scumm/smush/smush_player.cpp
@@ -1394,6 +1394,42 @@ void SmushPlayer::handleFrameObject(int32 subSize, Common::SeekableReadStream &b
free(chunk_buffer);
}
+static bool ra1FrameHasGameChunk(Common::SeekableReadStream &b, int32 frameSize) {
+ const int64 frameStart = b.pos();
+ int32 remaining = frameSize;
+
+ while (remaining > 1) {
+ if ((b.pos() & 1) && remaining > 0) {
+ const byte pad = b.readByte();
+ if (pad == 0) {
+ remaining--;
+ } else {
+ b.seek(-1, SEEK_CUR);
+ }
+ }
+
+ if (remaining < 8)
+ break;
+
+ const uint32 subType = b.readUint32BE();
+ const int32 subSize = b.readUint32BE();
+ const int64 subDataPos = b.pos();
+
+ if (subType == MKTAG('F', 'R', 'M', 'E'))
+ break;
+ if (subType == MKTAG('G', 'A', 'M', 'E')) {
+ b.seek(frameStart, SEEK_SET);
+ return true;
+ }
+
+ remaining -= subSize + 8;
+ b.seek(subDataPos + subSize, SEEK_SET);
+ }
+
+ b.seek(frameStart, SEEK_SET);
+ return false;
+}
+
void SmushPlayer::handleFrame(int32 frameSize, Common::SeekableReadStream &b) {
debugC(DEBUG_SMUSH, "SmushPlayer::handleFrame(%d)", _frame);
uint8 *audioChunk = nullptr;
@@ -1403,25 +1439,33 @@ void SmushPlayer::handleFrame(int32 frameSize, Common::SeekableReadStream &b) {
bool interactiveRA1 = false;
bool forceInteractiveClearRA1 = false;
+ bool preserveFrameHistoryRA1 = false;
if (isRA1() && _insane) {
InsaneRebel1 *rebel1 = static_cast<InsaneRebel1 *>(_insane);
interactiveRA1 = rebel1->isInteractiveVideoActive();
const uint16 activeOpcode = rebel1->getActiveGameOpcode();
// Opcode 0x0B path (FUN_1CDA7) uses heavy partial-layer composition
// (codec1/2 + FTCH). Force clear there to avoid stale-trail ghosting.
- // Keep conservative fallbacks for the early L2 frames before first 0x0B
- // arrives and for L4PLAY2, whose static 320x180 FTCH background leaves the
- // uncovered top band reliant on a clean frame base.
+ // Keep a conservative fallback for the early L2 frames before first 0x0B
+ // arrives.
forceInteractiveClearRA1 = interactiveRA1 &&
(activeOpcode == 0x0B ||
- (activeOpcode == 0 && rebel1->getCurrentLevel() == 1) ||
- _seekFile.equalsIgnoreCase("LVL4/L4PLAY2.ANM"));
+ (activeOpcode == 0 && rebel1->getCurrentLevel() == 1));
+
+ // The clean-frame cache only exists to strip gameplay overlays back out of
+ // the previous decoded frame before the next gameplay frame is composed.
+ // Transitional interactive clips like LVL4/L4PLAY2 have no GAME chunks in
+ // their FRME stream, so restoring the prior clean frame there just smears
+ // stale gameplay pixels into the cutscene.
+ preserveFrameHistoryRA1 = interactiveRA1 &&
+ !forceInteractiveClearRA1 &&
+ ra1FrameHasGameChunk(b, frameSize);
}
// Keep the previous decoded frame (without post-render overlays) as delta source.
// FUN_1FDBC (0x1FDBC) decodes frame data first; gameplay overlays from
// FUN_1BBCB/FUN_1CB22/FUN_1CDA7 are presentation-stage effects.
- if (isRA1() && interactiveRA1 && !forceInteractiveClearRA1 &&
+ if (isRA1() && preserveFrameHistoryRA1 &&
_ra1HasCleanFrame && _ra1CleanFrame &&
_dst && _width > 0 && _height > 0) {
const int frameBytes = _width * _height;
@@ -1437,7 +1481,7 @@ void SmushPlayer::handleFrame(int32 frameSize, Common::SeekableReadStream &b) {
// but passive cinematics in current implementation need a per-frame clear to
// avoid trails in intro/text sequences.
if (isRA1() && _dst && _width > 0 && _height > 0) {
- if (!interactiveRA1 || forceInteractiveClearRA1)
+ if (!preserveFrameHistoryRA1)
memset(_dst, 0, _width * _height);
}
@@ -1672,7 +1716,7 @@ void SmushPlayer::handleFrame(int32 frameSize, Common::SeekableReadStream &b) {
handleFrameObject(_ra1ObjOverlayDataSize, overlayStream);
}
- if (isRA1() && interactiveRA1 && !forceInteractiveClearRA1 &&
+ if (isRA1() && preserveFrameHistoryRA1 &&
_dst && _width > 0 && _height > 0) {
const int frameBytes = _width * _height;
byte *newClean = (byte *)realloc(_ra1CleanFrame, frameBytes);
@@ -1684,7 +1728,7 @@ void SmushPlayer::handleFrame(int32 frameSize, Common::SeekableReadStream &b) {
} else {
_ra1HasCleanFrame = false;
}
- } else if (isRA1() && forceInteractiveClearRA1) {
+ } else if (isRA1()) {
_ra1HasCleanFrame = false;
}
Commit: 22518d3549e861b8778c6894efa12ca196596e52
https://github.com/scummvm/scummvm/commit/22518d3549e861b8778c6894efa12ca196596e52
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:45+02:00
Commit Message:
SCUMM: RA1: Improve difficulty and energy checks
Changed paths:
engines/scumm/insane/insane_rebel1.cpp
engines/scumm/insane/insane_rebel1.h
engines/scumm/insane/insane_rebel1_render.cpp
engines/scumm/insane/insane_rebel1_runlevels.cpp
diff --git a/engines/scumm/insane/insane_rebel1.cpp b/engines/scumm/insane/insane_rebel1.cpp
index 9c77eb506a6..1898a8393a8 100644
--- a/engines/scumm/insane/insane_rebel1.cpp
+++ b/engines/scumm/insane/insane_rebel1.cpp
@@ -186,9 +186,9 @@ void InsaneRebel1::loadTuningForLevel(int level) {
_tuning.levelPts = kTuningTable[l][d][10];
_tuning.bonus = kTuningTable[l][d][11];
_tuning.flags = kTuningTable[l][d][12];
- // initLevelFromTuning (0x13E7B) copies tuning flags into g_hudDisableFlags (0x75FE)
- // and clears protected targets (DAT_00007732/7734).
- _gameplayFlags75fe = (uint16)_tuning.flags;
+ // initLevelFromTuning (0x13E7B) writes the 16-bit tuning flags word across
+ // 0x75FE/0x75FF, so we must preserve both bytes.
+ resetGameplayFlagsFromTuning();
_protectedTargetA = 0;
_protectedTargetB = 0;
@@ -199,6 +199,12 @@ void InsaneRebel1::loadTuningForLevel(int level) {
_tuning.time, _tuning.levelPts, _tuning.bonus, _tuning.flags);
}
+void InsaneRebel1::resetGameplayFlagsFromTuning() {
+ const uint16 tuningFlags = (uint16)_tuning.flags;
+ _gameplayFlags75fe = tuningFlags & 0x00FF;
+ _gameplayFlags75ff = tuningFlags >> 8;
+}
+
InsaneRebel1::InsaneRebel1(ScummEngine_v7 *scumm) : Insane(), _vm(scumm) {
_screenWidth = 384;
_screenHeight = 242;
diff --git a/engines/scumm/insane/insane_rebel1.h b/engines/scumm/insane/insane_rebel1.h
index c7e970975d9..eed1e12a5e4 100644
--- a/engines/scumm/insane/insane_rebel1.h
+++ b/engines/scumm/insane/insane_rebel1.h
@@ -323,6 +323,7 @@ private:
TuningParams _tuning;
void loadTuningForLevel(int level);
+ void resetGameplayFlagsFromTuning();
// Damage system (from Ghidra decompilation of FUN_1DEB5)
int16 _health; // 0x7560: current health (init=98, negative=dead, max=98)
diff --git a/engines/scumm/insane/insane_rebel1_render.cpp b/engines/scumm/insane/insane_rebel1_render.cpp
index b6262e48ba4..f8e6d60f9d2 100644
--- a/engines/scumm/insane/insane_rebel1_render.cpp
+++ b/engines/scumm/insane/insane_rebel1_render.cpp
@@ -433,6 +433,16 @@ void InsaneRebel1::procPostRendering(byte *renderBitmap, int32 codecparam, int32
if (asteroidMode) {
// First-person asteroid/surface handler â opcode 0x0B (FUN_1CDA7).
updateAsteroidPhysics();
+
+ // DOS 0x0B loops test health after each frontend frame and leave the
+ // interactive movie as soon as it drops below zero. Mirror that here so
+ // asteroid/surface chapters transition to their retry/death clips like
+ // the other gameplay families do.
+ if (_health < 0) {
+ _fireCooldown = _playerFired ? 1 : 0;
+ _vm->_smushVideoShouldFinish = true;
+ return;
+ }
} else if (onFootMode) {
// On-foot handler â opcodes 0x19/0x1A (Level 9 Stormtroopers)
updateOnFootPhysics();
@@ -603,30 +613,36 @@ void InsaneRebel1::renderTargeting(byte *dst, int pitch, int width, int height)
// Baseline RA1 targeting uses '^' and animation e..h.
const bool altMarkerSet = (_gameplayFlags75ff & 0x2) != 0;
- // Lock indicator at fixed center positions:
- // FUN_1CB22 draws marker strings at (0xA0,0x78) and (0xA0,0x7E).
- if (_targetProximity > 0) {
- drawCenteredBankGlyph(markerBank, dst, pitch, width, height, overlayX + 0xA0, overlayY + 0x78, ']');
- if (_targetProximity > 1)
- drawCenteredBankGlyph(markerBank, dst, pitch, width, height, overlayX + 0xA0, overlayY + 0x7E, 'a');
+ // DAT_75FF bit 2 suppresses the fixed lock/readiness overlay.
+ if ((_gameplayFlags75ff & 0x4) == 0) {
+ // Lock indicator at fixed center positions:
+ // FUN_1CB22 draws marker strings at (0xA0,0x78) and (0xA0,0x7E).
+ if (_targetProximity > 0) {
+ drawCenteredBankGlyph(markerBank, dst, pitch, width, height, overlayX + 0xA0, overlayY + 0x78, ']');
+ if (_targetProximity > 1)
+ drawCenteredBankGlyph(markerBank, dst, pitch, width, height, overlayX + 0xA0, overlayY + 0x7E, 'a');
+ }
}
- // Pointer glyph at current aim position. Original uses two variants:
- // default marker ('^' or 'x') and animated lock marker (e..h or y..|).
- char marker[2] = { (char)(altMarkerSet ? 'x' : '^'), '\0' };
- if (_targetProximity > 1) {
- _targetAnimCounter++;
- marker[0] = (char)((altMarkerSet ? 'y' : 'e') + (_targetAnimCounter & 3));
- }
+ // DAT_75FE bit 2 suppresses the cursor glyph entirely.
+ if ((_gameplayFlags75fe & 0x4) == 0) {
+ // Pointer glyph at current aim position. Original uses two variants:
+ // default marker ('^' or 'x') and animated lock marker (e..h or y..|).
+ char marker[2] = { (char)(altMarkerSet ? 'x' : '^'), '\0' };
+ if (_targetProximity > 1) {
+ _targetAnimCounter++;
+ marker[0] = (char)((altMarkerSet ? 'y' : 'e') + (_targetAnimCounter & 3));
+ }
- int cursorX = CLIP<int>(overlayX + getGameplayCursorX(), 0, width - 1);
- int cursorY = CLIP<int>(overlayY + getGameplayCursorY(), 0, height - 1);
- drawCenteredBankGlyph(markerBank, dst, pitch, width, height, cursorX, cursorY, marker[0]);
+ int cursorX = CLIP<int>(overlayX + getGameplayCursorX(), 0, width - 1);
+ int cursorY = CLIP<int>(overlayY + getGameplayCursorY(), 0, height - 1);
+ drawCenteredBankGlyph(markerBank, dst, pitch, width, height, cursorX, cursorY, marker[0]);
- if (altMarkerSet) {
- const int indicatorWidth = getFontBankStringWidth(kRA1TorpedoIndicator);
- drawFontBankString(dst, pitch, width, height,
- overlayX + 0xA0 - indicatorWidth / 2, overlayY + 0x6E, kRA1TorpedoIndicator);
+ if (altMarkerSet) {
+ const int indicatorWidth = getFontBankStringWidth(kRA1TorpedoIndicator);
+ drawFontBankString(dst, pitch, width, height,
+ overlayX + 0xA0 - indicatorWidth / 2, overlayY + 0x6E, kRA1TorpedoIndicator);
+ }
}
}
@@ -694,8 +710,8 @@ void InsaneRebel1::renderGostSlots(byte *dst, int pitch, int width, int height)
renderSprite(dst, pitch, width, height, drawX, drawY, spr);
// Per-kill score popup glyph â RenderGostOverlaySlots (0x1CA35)
- // Suppressed in on-foot mode (combatModeFlags & 8)
- if ((_gameplayFlags75fe & 8) == 0) {
+ // Suppressed when DAT_75FF bit 3 is set.
+ if ((_gameplayFlags75ff & 8) == 0) {
renderGostScorePopup(dst, pitch, width, height,
overlayX + centerX, centerY, _gostSlots[i].frame);
}
diff --git a/engines/scumm/insane/insane_rebel1_runlevels.cpp b/engines/scumm/insane/insane_rebel1_runlevels.cpp
index e5a88ef4f7d..1908c89bb48 100644
--- a/engines/scumm/insane/insane_rebel1_runlevels.cpp
+++ b/engines/scumm/insane/insane_rebel1_runlevels.cpp
@@ -316,7 +316,7 @@ bool InsaneRebel1::runLevel3() {
_activeGameOpcode = 0;
_gameLatch5D = 0;
_gameLatch5F = 0;
- _gameplayFlags75ff = 0;
+ resetGameplayFlagsFromTuning();
_killCount = 0;
_levelGameplayPhase = 0;
memset(_inputHistoryX, 0, sizeof(_inputHistoryX));
@@ -379,7 +379,7 @@ bool InsaneRebel1::runLevel4() {
_activeGameOpcode = 0;
_gameLatch5D = 0;
_gameLatch5F = 0;
- _gameplayFlags75ff = 0;
+ resetGameplayFlagsFromTuning();
_killCount = 0;
_levelGameplayPhase = 0;
@@ -403,7 +403,7 @@ bool InsaneRebel1::runLevel4() {
_activeGameOpcode = 0;
_gameLatch5D = 0;
_gameLatch5F = 0;
- _gameplayFlags75ff = 0;
+ resetGameplayFlagsFromTuning();
_killCount = 0;
_levelGameplayPhase = 2;
playInteractiveVideo("LVL4/L4PLAY2.ANM");
@@ -463,7 +463,7 @@ bool InsaneRebel1::runLevel5() {
_activeGameOpcode = 0;
_gameLatch5D = 0;
_gameLatch5F = 0;
- _gameplayFlags75ff = 0;
+ resetGameplayFlagsFromTuning();
_killCount = 0;
_targetCount = 0;
_prevTargetCount = 0;
@@ -558,7 +558,9 @@ bool InsaneRebel1::runLevel6() {
_currentLevel = 5;
loadLevelSprites(6);
- loadTuningForLevel(5);
+ // DOS RunLevel6Flow starts L6PLAY with PlayAnmFile(..., 8), so chapter 6
+ // uses tuning slot 8 ("6"), not the chapter-4B slot 5.
+ loadTuningForLevel(8);
beginLevelTitleOverlay(5);
playCinematic("LVL6/L6INTRO.ANM");
@@ -580,7 +582,7 @@ bool InsaneRebel1::runLevel6() {
_activeGameOpcode = 0;
_gameLatch5D = 0;
_gameLatch5F = 0;
- _gameplayFlags75ff = 0;
+ resetGameplayFlagsFromTuning();
_killCount = 0;
_levelGameplayPhase = 0;
memset(_inputHistoryX, 0, sizeof(_inputHistoryX));
@@ -651,7 +653,7 @@ bool InsaneRebel1::runLevel7() {
_activeGameOpcode = 0;
_gameLatch5D = 0;
_gameLatch5F = 0;
- _gameplayFlags75ff = 0;
+ resetGameplayFlagsFromTuning();
_killCount = 0;
_targetCount = 0;
_prevTargetCount = 0;
@@ -747,7 +749,7 @@ bool InsaneRebel1::runLevel8() {
_activeGameOpcode = 0;
_gameLatch5D = 0;
_gameLatch5F = 0;
- _gameplayFlags75ff = 0;
+ resetGameplayFlagsFromTuning();
_killCount = 0;
_targetCount = 0;
_prevTargetCount = 0;
@@ -851,7 +853,7 @@ bool InsaneRebel1::runLevel9() {
_activeGameOpcode = 0;
_gameLatch5D = 0;
_gameLatch5F = 0;
- _gameplayFlags75ff = 0;
+ resetGameplayFlagsFromTuning();
_killCount = 0;
_targetCount = 0;
_prevTargetCount = 0;
@@ -1039,7 +1041,7 @@ bool InsaneRebel1::runLevel10() {
_activeGameOpcode = 0;
_gameLatch5D = 0;
_gameLatch5F = 0;
- _gameplayFlags75ff = 0;
+ resetGameplayFlagsFromTuning();
_killCount = 0;
_targetCount = 0;
_prevTargetCount = 0;
@@ -1116,7 +1118,7 @@ bool InsaneRebel1::runLevel11() {
_activeGameOpcode = 0;
_gameLatch5D = 0;
_gameLatch5F = 0;
- _gameplayFlags75ff = 0;
+ resetGameplayFlagsFromTuning();
_killCount = 0;
_targetCount = 0;
_prevTargetCount = 0;
@@ -1209,7 +1211,7 @@ bool InsaneRebel1::runLevel12() {
_activeGameOpcode = 0;
_gameLatch5D = 0;
_gameLatch5F = 0;
- _gameplayFlags75ff = 0;
+ resetGameplayFlagsFromTuning();
_killCount = 0;
_targetCount = 0;
_prevTargetCount = 0;
@@ -1286,7 +1288,7 @@ bool InsaneRebel1::runLevel13() {
_activeGameOpcode = 0;
_gameLatch5D = 0;
_gameLatch5F = 0;
- _gameplayFlags75ff = 0;
+ resetGameplayFlagsFromTuning();
_killCount = 0;
_targetCount = 0;
_prevTargetCount = 0;
@@ -1363,7 +1365,7 @@ bool InsaneRebel1::runLevel14() {
_activeGameOpcode = 0;
_gameLatch5D = 0;
_gameLatch5F = 0;
- _gameplayFlags75ff = 0;
+ resetGameplayFlagsFromTuning();
_killCount = 0;
_targetCount = 0;
_prevTargetCount = 0;
@@ -1395,7 +1397,7 @@ bool InsaneRebel1::runLevel14() {
_activeGameOpcode = 0;
_gameLatch5D = 0;
_gameLatch5F = 0;
- _gameplayFlags75ff = 0;
+ resetGameplayFlagsFromTuning();
_killCount = 0;
playInteractiveVideo("LVL14/L14PLAY2.ANM");
@@ -1455,7 +1457,7 @@ bool InsaneRebel1::runLevel15() {
_activeGameOpcode = 0;
_gameLatch5D = 0;
_gameLatch5F = 0;
- _gameplayFlags75ff = 0;
+ resetGameplayFlagsFromTuning();
_killCount = 0;
_targetCount = 0;
_prevTargetCount = 0;
@@ -1495,7 +1497,7 @@ bool InsaneRebel1::runLevel15() {
_activeGameOpcode = 0;
_gameLatch5D = 0;
_gameLatch5F = 0;
- _gameplayFlags75ff = 0;
+ resetGameplayFlagsFromTuning();
_killCount = 0;
_torpedoFired = false;
_levelGameplayPhase = 2;
Commit: 03a3e02a5a995ab79627fc08b9ad826498251df0
https://github.com/scummvm/scummvm/commit/03a3e02a5a995ab79627fc08b9ad826498251df0
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:45+02:00
Commit Message:
SCUMM: RA1: Add skip-RLE line update decoder
Changed paths:
engines/scumm/smush/smush_player.cpp
engines/scumm/smush/smush_player.h
diff --git a/engines/scumm/smush/smush_player.cpp b/engines/scumm/smush/smush_player.cpp
index c75c4dae429..5e4420e5833 100644
--- a/engines/scumm/smush/smush_player.cpp
+++ b/engines/scumm/smush/smush_player.cpp
@@ -937,6 +937,53 @@ static void smushDecodeRA1SkipCopy(byte *dst, const byte *src, int left, int top
}
}
+/**
+ * RA1 codec 23: Additive line-update overlay (FUN_10B40).
+ *
+ * Each line starts with a 16-bit row payload size followed by repeated
+ * [skip:u8][run:u8] pairs. Unlike codec 21, the run length is stored directly,
+ * not minus one. Instead of copying bytes from the stream, the decoder adds
+ * `(paletteBase - 0x30)` to the destination pixels already present in the
+ * framebuffer.
+ */
+static void smushDecodeRA1AdditiveLineUpdate(byte *dst, const byte *src, int left, int top, int width, int height,
+ int pitch, int bufWidth, int bufHeight, uint8 paletteBase) {
+ const uint8 colorDelta = (uint8)(paletteBase - 0x30);
+
+ for (int row = 0; row < height; row++) {
+ const uint16 lineSize = READ_LE_UINT16(src);
+ const byte *lineData = src + 2;
+ const byte *lineEnd = lineData + lineSize;
+ const int dstY = top + row;
+ int srcX = 0;
+
+ while (srcX < width && lineData < lineEnd) {
+ const int skip = *lineData++;
+ srcX += skip;
+ if (srcX >= width || lineData >= lineEnd)
+ break;
+
+ const int runLength = (int)(*lineData++);
+ const int dstStartX = left + srcX;
+ const int dstEndX = dstStartX + runLength;
+
+ if (dstY >= 0 && dstY < bufHeight) {
+ const int clippedStartX = MAX(dstStartX, 0);
+ const int clippedEndX = MIN(dstEndX, bufWidth);
+ if (clippedStartX < clippedEndX) {
+ byte *dstPixel = dst + dstY * pitch + clippedStartX;
+ for (int x = clippedStartX; x < clippedEndX; x++, dstPixel++)
+ *dstPixel = (byte)(*dstPixel + colorDelta);
+ }
+ }
+
+ srcX += runLength;
+ }
+
+ src += lineSize + 2;
+ }
+}
+
/**
* RA1 codec 2: Scatter/point draw (FUN_110D7).
*
@@ -1177,7 +1224,8 @@ void SmushPlayer::decodeFrameObject(int codec, const uint8 *src, int left, int t
int origHeight = height;
int srcSkipY = 0;
- if (isRA1() || isRA2()) {
+ const bool ra1AdditiveCodec = isRA1() && (codec == SMUSH_CODEC_SKIP_RLE);
+ if ((isRA1() || isRA2()) && !ra1AdditiveCodec) {
ra2AdjustFrameCoords(left, top, width, height, pitch, &srcSkipY);
if (width <= 0 || height <= 0)
return;
@@ -1243,6 +1291,15 @@ void SmushPlayer::decodeFrameObject(int codec, const uint8 *src, int left, int t
break;
}
// Fall through for RA2
+ case SMUSH_CODEC_SKIP_RLE:
+ if (isRA1()) {
+ const int bufWidth = pitch;
+ const int bufHeight = (_dst == _specialBuffer) ? _height : _vm->_screenHeight;
+ smushDecodeRA1AdditiveLineUpdate(_dst, src, origLeft, origTop, origWidth, origHeight,
+ pitch, bufWidth, bufHeight, ra1Param);
+ break;
+ }
+ // Fall through for RA2
default:
if (isRA2() && ra2DecodeCodec(codec, src, left, top, width, height, pitch, dataSize))
break;
diff --git a/engines/scumm/smush/smush_player.h b/engines/scumm/smush/smush_player.h
index 4f6b4f2ab3f..cd87ff57af0 100644
--- a/engines/scumm/smush/smush_player.h
+++ b/engines/scumm/smush/smush_player.h
@@ -83,7 +83,7 @@ namespace Scumm {
#define SMUSH_CODEC_RA1_BLOCK 5 // RA1: Block-based frame codec (no skip)
#define SMUSH_CODEC_UNCOMPRESSED 20
#define SMUSH_CODEC_LINE_UPDATE 21 // RA2: Skip/copy with literal pixels
-#define SMUSH_CODEC_SKIP_RLE 23 // RA2: Skip/copy with embedded RLE
+#define SMUSH_CODEC_SKIP_RLE 23 // RA1: additive line-update overlay; RA2: skip/copy with embedded RLE
#define SMUSH_CODEC_DELTA_BLOCKS 37
#define SMUSH_CODEC_LINE_UPDATE2 44 // RA2: Variant of codec 21
#define SMUSH_CODEC_RA2_BOMP 45 // RA2: BOMP RLE with variable header
Commit: c33d5d1c06c9bd0912d8ab0a15d0771749a34432
https://github.com/scummvm/scummvm/commit/c33d5d1c06c9bd0912d8ab0a15d0771749a34432
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:45+02:00
Commit Message:
SCUMM: RA1: Fix level 8 armor targets
Changed paths:
engines/scumm/insane/insane_rebel1.h
engines/scumm/insane/insane_rebel1_iact.cpp
engines/scumm/insane/insane_rebel1_runlevels.cpp
diff --git a/engines/scumm/insane/insane_rebel1.h b/engines/scumm/insane/insane_rebel1.h
index eed1e12a5e4..f28bbd45fae 100644
--- a/engines/scumm/insane/insane_rebel1.h
+++ b/engines/scumm/insane/insane_rebel1.h
@@ -461,7 +461,7 @@ private:
int16 _gostSlotIdx; // 0x23EB: next slot to write (circular 0-9)
int16 _killCount; // 0x75D0: targets destroyed this stage
- int16 _lastHitTarget; // 0x75D6: prevents double-hit on same target
+ int16 _lastHitTarget; // 0x75D6: recent-kill latch, allows at most one hit per frame
// Protected target IDs â 0x7732/0x7734 in original
// Targets listed here can be hit repeatedly (no event mask toggle).
diff --git a/engines/scumm/insane/insane_rebel1_iact.cpp b/engines/scumm/insane/insane_rebel1_iact.cpp
index fbdcd9445f8..b949fb62eae 100644
--- a/engines/scumm/insane/insane_rebel1_iact.cpp
+++ b/engines/scumm/insane/insane_rebel1_iact.cpp
@@ -1613,6 +1613,12 @@ void InsaneRebel1::handleGameChunk(int32 subSize, Common::SeekableReadStream &b)
_frameGameOpcodeMask = 0;
_frameDispatchFlags = 0;
resetProjectionTable();
+
+ // RunLevel8Flow seeds the walker armor mask after the interactive segment
+ // has entered its runtime reset path, not before playback begins.
+ if (_currentLevel == 7)
+ memset(_frameObjectState + 150, 0xFF, 150);
+
debug(5, "RA1 GAME 0x5E: reset state field1=%d mode=%d", (int32)param1, (int)_flyControlMode);
break;
@@ -1937,8 +1943,10 @@ void InsaneRebel1::checkTargetHit(int16 targetIdx, int16 left, int16 top, int16
curY > top - snap && curY < bottom + snap) {
_targetProximity = 2; // On-target
- // Check if any active shot slot hits this target
- if (_lastHitTarget != targetIdx + 1) {
+ // DOS uses g_recentKillObjectIdPlus1 as a frame-wide latch. Once one
+ // target is hit this frame, overlapping FOBJ layers must not consume the
+ // same shot again.
+ if (_lastHitTarget == 0) {
for (int i = 0; i < kMaxShotSlots; i++) {
if (_shotSlots[i].timer == 1) { // Shot in final frame = impact
// Hit! Record in GOST slot for explosion animation
@@ -1953,6 +1961,32 @@ void InsaneRebel1::checkTargetHit(int16 targetIdx, int16 left, int16 top, int16
_score += _tuning.kill;
_killCount++;
applyFrameObjectHitState(targetIdx);
+
+ if (_currentLevel == 7) {
+ Common::String damagedState;
+ Common::String hiddenState;
+ for (int objectId = 20; objectId <= 43; objectId++) {
+ const int bitIndex = objectId - 1;
+ const int byteIdx = bitIndex >> 3;
+ if (byteIdx < 0 || byteIdx >= 0x96 || byteIdx >= kFrameObjectStateBytes)
+ continue;
+
+ const byte stateBit = (byte)(0x80 >> (bitIndex & 7));
+ const int altIdx = byteIdx + 0x96;
+ const bool primarySet = (_frameObjectState[byteIdx] & stateBit) != 0;
+ const bool secondarySet = (altIdx < kFrameObjectStateBytes) &&
+ ((_frameObjectState[altIdx] & stateBit) != 0);
+
+ if (primarySet)
+ hiddenState += Common::String::format("%d,", objectId);
+ else if (!secondarySet)
+ damagedState += Common::String::format("%d,", objectId);
+ }
+
+ debug(1, "RA1 L8 armor: hitObject=%d damaged=[%s] hidden=[%s]",
+ targetIdx + 1, damagedState.c_str(), hiddenState.c_str());
+ }
+
int16 hitCenterX = (left + right) / 2;
int16 hitCenterY = (top + bottom) / 2;
projectGameplayPoint(hitCenterX, hitCenterY);
diff --git a/engines/scumm/insane/insane_rebel1_runlevels.cpp b/engines/scumm/insane/insane_rebel1_runlevels.cpp
index 1908c89bb48..77807baaf57 100644
--- a/engines/scumm/insane/insane_rebel1_runlevels.cpp
+++ b/engines/scumm/insane/insane_rebel1_runlevels.cpp
@@ -776,11 +776,6 @@ bool InsaneRebel1::runLevel8() {
_walkerTimer = 0;
_walkerBranchChoice = 0;
- // g_level8HitboxBuffer (0x7698) = _frameObjectState[150..299] filled with 0xFF.
- // This enables all frame object event masks in the secondary half of the array,
- // which the IACT 0x5A handler uses to gate walker-related frame objects.
- memset(_frameObjectState + 150, 0xFF, 150);
-
int route = 0;
while (!_vm->shouldQuit()) {
_levelRouteIndex = route;
Commit: d0f4a422ec6555ed392ac1091340c5d0f718358e
https://github.com/scummvm/scummvm/commit/d0f4a422ec6555ed392ac1091340c5d0f718358e
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:46+02:00
Commit Message:
SCUMM: RA1: Fix level 7 video transition
Changed paths:
engines/scumm/insane/insane_rebel1.cpp
engines/scumm/insane/insane_rebel1.h
engines/scumm/insane/insane_rebel1_iact.cpp
engines/scumm/insane/insane_rebel1_runlevels.cpp
engines/scumm/smush/smush_player.cpp
engines/scumm/smush/smush_player.h
diff --git a/engines/scumm/insane/insane_rebel1.cpp b/engines/scumm/insane/insane_rebel1.cpp
index 1898a8393a8..0610f978d45 100644
--- a/engines/scumm/insane/insane_rebel1.cpp
+++ b/engines/scumm/insane/insane_rebel1.cpp
@@ -291,6 +291,8 @@ InsaneRebel1::InsaneRebel1(ScummEngine_v7 *scumm) : Insane(), _vm(scumm) {
_rightPathSelected = false;
_levelRouteIndex = -1;
_pendingRouteIndex = -1;
+ _pendingRouteStartFrame = 0;
+ _pendingRouteCutoverFrame = -1;
_levelRouteChoice = 0;
_levelGameplayPhase = 0;
_level5SuccessFramesRemaining = 0;
diff --git a/engines/scumm/insane/insane_rebel1.h b/engines/scumm/insane/insane_rebel1.h
index f28bbd45fae..1ef1e542835 100644
--- a/engines/scumm/insane/insane_rebel1.h
+++ b/engines/scumm/insane/insane_rebel1.h
@@ -381,6 +381,8 @@ private:
bool _rightPathSelected; // True if player chose the right/easy path
int _levelRouteIndex; // Current mid-level route/segment for branching levels
int _pendingRouteIndex; // Next route requested by original frame-branch logic
+ int32 _pendingRouteStartFrame; // Resume frame for branch-driven route switches
+ int32 _pendingRouteCutoverFrame; // Delayed inline route splice frame (Level 7 uses branchFrame + 7)
int _levelRouteChoice; // Level-local pending branch choice (0=none, 1=left, 2=right)
int _levelGameplayPhase; // Level-local interactive phase (e.g. LVL4 PLAY1 vs PLAY2)
diff --git a/engines/scumm/insane/insane_rebel1_iact.cpp b/engines/scumm/insane/insane_rebel1_iact.cpp
index b949fb62eae..4a900b5dd5b 100644
--- a/engines/scumm/insane/insane_rebel1_iact.cpp
+++ b/engines/scumm/insane/insane_rebel1_iact.cpp
@@ -498,7 +498,22 @@ void InsaneRebel1::resetProjectionTable() {
}
void InsaneRebel1::checkDynamicLevelBranch() {
- if (!_interactiveVideoActive || _levelRouteIndex < 0 || _pendingRouteIndex >= 0 || _vm->_smushVideoShouldFinish)
+ if (!_interactiveVideoActive || _levelRouteIndex < 0)
+ return;
+
+ if (_currentLevel == 6 && _pendingRouteIndex >= 0) {
+ if (!_vm->_smushVideoShouldFinish &&
+ _pendingRouteCutoverFrame >= 0 &&
+ _frameCounter >= (uint32)_pendingRouteCutoverFrame) {
+ _vm->_smushVideoShouldFinish = true;
+ debug(1, "RA1 L7 cutover: route=%d -> %d at frame=%u (resumeTimelineFrame=%d)",
+ _levelRouteIndex, _pendingRouteIndex, (unsigned)_frameCounter,
+ (int)_pendingRouteStartFrame);
+ }
+ return;
+ }
+
+ if (_pendingRouteIndex >= 0 || _vm->_smushVideoShouldFinish)
return;
if (_currentLevel == 6) {
@@ -515,9 +530,11 @@ void InsaneRebel1::checkDynamicLevelBranch() {
continue;
_pendingRouteIndex = nextRoute;
- _vm->_smushVideoShouldFinish = true;
- debug(1, "RA1 L7 branch: route=%d -> %d at frame=%u shipX=%d",
- route, nextRoute, (unsigned)_frameCounter, _shipPosX);
+ _pendingRouteCutoverFrame = (int32)_frameCounter + 7;
+ _pendingRouteStartFrame = _pendingRouteCutoverFrame;
+ debug(1, "RA1 L7 branch: route=%d -> %d at frame=%u shipX=%d resumeTimelineFrame=%d cutoverFrame=%d",
+ route, nextRoute, (unsigned)_frameCounter, _shipPosX,
+ (int)_pendingRouteStartFrame, (int)_pendingRouteCutoverFrame);
return;
}
}
diff --git a/engines/scumm/insane/insane_rebel1_runlevels.cpp b/engines/scumm/insane/insane_rebel1_runlevels.cpp
index 77807baaf57..04392541bff 100644
--- a/engines/scumm/insane/insane_rebel1_runlevels.cpp
+++ b/engines/scumm/insane/insane_rebel1_runlevels.cpp
@@ -24,12 +24,125 @@
#include "graphics/cursorman.h"
#include "graphics/wincursor.h"
+#include "scumm/file.h"
#include "scumm/scumm_v7.h"
#include "scumm/smush/smush_player.h"
#include "scumm/insane/insane_rebel1.h"
namespace Scumm {
+struct RA1Level7ResumeSegment {
+ int16 timelineStart;
+ int16 timelineEnd;
+ int16 localStart;
+};
+
+static const RA1Level7ResumeSegment kLevel7ResumeSegments[6][4] = {
+ {
+ { 0, 638, 0 },
+ { 1416, 1468, 639 },
+ { -1, -1, -1 },
+ { -1, -1, -1 }
+ },
+ {
+ { 80, 639, 189 },
+ { 691, 879, 0 },
+ { 1416, 1468, 749 },
+ { -1, -1, -1 }
+ },
+ {
+ { 80, 639, 189 },
+ { 777, 879, 86 },
+ { 880, 965, 0 },
+ { 1416, 1468, 749 }
+ },
+ {
+ { 398, 639, 284 },
+ { 1132, 1415, 0 },
+ { 1416, 1468, 526 },
+ { -1, -1, -1 }
+ },
+ {
+ { 398, 639, 143 },
+ { 966, 1076, 0 },
+ { 1384, 1415, 111 },
+ { 1416, 1468, 385 }
+ },
+ {
+ { 80, 639, 114 },
+ { 821, 879, 55 },
+ { 1077, 1131, 0 },
+ { 1416, 1468, 674 }
+ }
+};
+
+static int32 mapLevel7TimelineFrameToLocal(int route, int32 timelineFrame) {
+ if (timelineFrame <= 0)
+ return 0;
+
+ const int clampedRoute = CLIP<int>(route, 0, ARRAYSIZE(kLevel7ResumeSegments) - 1);
+ const RA1Level7ResumeSegment *segments = kLevel7ResumeSegments[clampedRoute];
+
+ for (int i = 0; i < ARRAYSIZE(kLevel7ResumeSegments[0]); ++i) {
+ const RA1Level7ResumeSegment &segment = segments[i];
+ if (segment.timelineStart < 0)
+ break;
+ if (timelineFrame < segment.timelineStart)
+ return segment.localStart;
+ if (timelineFrame <= segment.timelineEnd)
+ return segment.localStart + (timelineFrame - segment.timelineStart);
+ }
+
+ for (int i = ARRAYSIZE(kLevel7ResumeSegments[0]) - 1; i >= 0; --i) {
+ const RA1Level7ResumeSegment &segment = segments[i];
+ if (segment.timelineStart >= 0)
+ return segment.localStart;
+ }
+
+ return 0;
+}
+
+static int32 findAnimFrameChunkOffset(ScummEngine_v7 *vm, const char *filename, int32 targetFrame) {
+ if (targetFrame <= 0)
+ return 0;
+
+ ScummFile *file = vm->instantiateScummFile();
+ if (!vm->openFile(*file, Common::Path(filename))) {
+ delete file;
+ return -1;
+ }
+
+ int32 result = -1;
+ if (file->size() >= 8) {
+ file->readUint32BE();
+ file->readUint32BE();
+
+ int32 frameIndex = 0;
+ while (file->pos() + 8 <= file->size()) {
+ const int32 chunkOffset = (int32)file->pos();
+ const uint32 chunkTag = file->readUint32BE();
+ const int32 chunkSize = (int32)file->readUint32BE();
+ const int32 nextChunkOffset = chunkOffset + 8 + chunkSize + ((chunkSize & 1) ? 1 : 0);
+ if (nextChunkOffset < chunkOffset || nextChunkOffset > file->size())
+ break;
+
+ if (chunkTag == MKTAG('F', 'R', 'M', 'E')) {
+ if (frameIndex == targetFrame) {
+ result = chunkOffset;
+ break;
+ }
+ frameIndex++;
+ }
+
+ file->seek(nextChunkOffset, SEEK_SET);
+ }
+ }
+
+ file->close();
+ delete file;
+ return result;
+}
+
// ---------------------------------------------------------------------------
// Game flow (matching original at 0x15597)
// ---------------------------------------------------------------------------
@@ -44,8 +157,8 @@ void InsaneRebel1::playCinematic(const char *filename, int32 startFrame) {
_interactiveVideoActive = false;
_vm->_smushVideoShouldFinish = false;
splayer->setCurVideoFlags(0x28); // Cinematic mode + buffer preserve
- if (startFrame > 0)
- splayer->setFastForwardToFrame(startFrame);
+ splayer->setFastForwardFromFrame(0);
+ splayer->setFastForwardToFrame(startFrame > 0 ? startFrame : 0);
splayer->play(filename, 12);
// Level-title text is only meant for the intro cinematic that armed it.
@@ -379,6 +492,7 @@ bool InsaneRebel1::runLevel4() {
_activeGameOpcode = 0;
_gameLatch5D = 0;
_gameLatch5F = 0;
+ _pendingRouteStartFrame = 0;
resetGameplayFlagsFromTuning();
_killCount = 0;
_levelGameplayPhase = 0;
@@ -676,10 +790,13 @@ bool InsaneRebel1::runLevel7() {
_avgInputY = 0;
int route = 0;
+ int32 routeStartFrame = 0;
while (!_vm->shouldQuit()) {
_levelRouteIndex = route;
_pendingRouteIndex = -1;
- playInteractiveVideo(kLevel7Segments[route]);
+ _pendingRouteStartFrame = routeStartFrame;
+ _pendingRouteCutoverFrame = -1;
+ playInteractiveVideo(kLevel7Segments[route], routeStartFrame);
if (_vm->shouldQuit())
return false;
@@ -689,11 +806,17 @@ bool InsaneRebel1::runLevel7() {
if (_pendingRouteIndex < 0 || _pendingRouteIndex == route)
break;
+ // RunLevel7Flow arms the next route inline, lets the current route run for
+ // seven more gameplay frames, then opens the destination ANM while keeping
+ // the existing video state alive.
+ routeStartFrame = _pendingRouteStartFrame;
route = _pendingRouteIndex;
}
_levelRouteIndex = -1;
_pendingRouteIndex = -1;
+ _pendingRouteStartFrame = 0;
+ _pendingRouteCutoverFrame = -1;
if (_health >= 0) {
playCinematic("LVL7/L7END.ANM");
@@ -1611,6 +1734,9 @@ void InsaneRebel1::runGame() {
// Play interactive gameplay video (with ship physics + HUD).
void InsaneRebel1::playInteractiveVideo(const char *filename, int32 startFrame) {
debug(1, "InsaneRebel1::playInteractiveVideo('%s', startFrame=%d)", filename, startFrame);
+ const bool resumingRoute = (startFrame > 0);
+ int32 videoStartFrame = 0;
+ int32 videoOffset = 0;
// Stop any leftover audio from previous video
terminateAudio();
@@ -1618,22 +1744,42 @@ void InsaneRebel1::playInteractiveVideo(const char *filename, int32 startFrame)
SmushPlayer *splayer = _vm->_splayer;
_player = splayer;
- clearBit(0);
+ if (!resumingRoute)
+ clearBit(0);
_interactiveVideoActive = true;
_levelRouteChoice = 0;
- _onFootInitialized = false; // Reset so each segment triggers counter==0 init
- resetFrameObjectState();
+ if (!resumingRoute) {
+ _onFootInitialized = false; // Reset so each segment triggers counter==0 init
+ resetFrameObjectState();
+ }
_vm->_smushVideoShouldFinish = false;
+ // Route resumes stay in the same gameplay flow in the original executable.
+ // Preserve the previous video/runtime state, but keep the destination clip
+ // fully interactive from its first visible frame.
+ splayer->setPreserveVideoStateOnNextPlay(resumingRoute);
splayer->setCurVideoFlags(0x28);
- if (startFrame > 0)
- splayer->setFastForwardToFrame(startFrame);
+ splayer->setFastForwardFromFrame(0);
+ splayer->setFastForwardToFrame(0);
+ if (_currentLevel == 6 && resumingRoute) {
+ videoStartFrame = mapLevel7TimelineFrameToLocal(_levelRouteIndex, startFrame);
+ videoOffset = findAnimFrameChunkOffset(_vm, filename, videoStartFrame);
+ if (videoOffset < 0) {
+ debug(1, "RA1 L7 resume: route=%d timelineFrame=%d localFrame=%d offset lookup failed",
+ _levelRouteIndex, (int)startFrame, (int)videoStartFrame);
+ videoStartFrame = 0;
+ videoOffset = 0;
+ } else {
+ debug(1, "RA1 L7 resume: route=%d timelineFrame=%d -> localFrame=%d offset=0x%x",
+ _levelRouteIndex, (int)startFrame, (int)videoStartFrame, (unsigned)videoOffset);
+ }
+ }
// Center mouse, hide system cursor (we draw our own), lock mouse to window
smush_warpMouse(160, 100, -1);
CursorMan.showMouse(false);
g_system->lockMouse(true);
- splayer->play(filename, 12);
+ splayer->play(filename, 12, videoOffset, videoStartFrame);
_interactiveVideoActive = false;
g_system->lockMouse(false);
diff --git a/engines/scumm/smush/smush_player.cpp b/engines/scumm/smush/smush_player.cpp
index 5e4420e5833..abece384a6a 100644
--- a/engines/scumm/smush/smush_player.cpp
+++ b/engines/scumm/smush/smush_player.cpp
@@ -284,6 +284,9 @@ SmushPlayer::SmushPlayer(ScummEngine_v7 *scumm, IMuseDigital *imuseDigital, Insa
_height = 0;
_scrollX = 0;
_scrollY = 0;
+ _fastForwardFromFrame = 0;
+ _fastForwardToFrame = 0;
+ _preserveVideoStateOnNextPlay = false;
ra2InitFields();
_IACTpos = 0;
@@ -348,11 +351,16 @@ void SmushPlayer::init(int32 speed) {
_fobjOffsetY = 0;
_scrollX = 0;
_scrollY = 0;
+ _fastForwardFromFrame = 0;
_fastForwardToFrame = 0;
- _ra1HasCleanFrame = false;
- // RA1 OBJ overlay chunks are video-local. Reset cached overlay state for each
- // new ANM so data from a previous segment isn't re-applied.
- resetGameVideoState();
+ const bool preserveVideoState = _preserveVideoStateOnNextPlay;
+ _preserveVideoStateOnNextPlay = false;
+ if (!preserveVideoState) {
+ _ra1HasCleanFrame = false;
+ // RA1 OBJ overlay chunks are video-local. Reset cached overlay state for each
+ // new ANM unless the original route-switch path requested state preservation.
+ resetGameVideoState();
+ }
_vm->_smushVideoShouldFinish = false;
_vm->_smushActive = true;
@@ -379,6 +387,12 @@ void SmushPlayer::init(int32 speed) {
_IACTpos = 0;
}
+bool SmushPlayer::isFastForwardingCurrentFrame() const {
+ return (_fastForwardToFrame > _fastForwardFromFrame &&
+ _frame >= _fastForwardFromFrame &&
+ _frame < _fastForwardToFrame);
+}
+
void SmushPlayer::release() {
_vm->_smushVideoShouldFinish = true;
@@ -1601,7 +1615,7 @@ void SmushPlayer::handleFrame(int32 frameSize, Common::SeekableReadStream &b) {
handleZlibFrameObject(subSize, b);
break;
case MKTAG('P','S','A','D'):
- if (!_compressedFileMode && _fastForwardToFrame == 0) {
+ if (!_compressedFileMode && !isFastForwardingCurrentFrame()) {
audioChunk = (uint8 *)malloc(subSize + 8);
b.seek(-8, SEEK_CUR);
b.read(audioChunk, subSize + 8);
@@ -1652,7 +1666,7 @@ void SmushPlayer::handleFrame(int32 frameSize, Common::SeekableReadStream &b) {
// RA1 voice-over audio: same 12-byte header format as PSAD
// (3 Ã BE32: trackId, seqNum, param) followed by SAUD data.
// Feed to audio system identically to PSAD.
- if (!_compressedFileMode && _fastForwardToFrame == 0) {
+ if (!_compressedFileMode && !isFastForwardingCurrentFrame()) {
audioChunk = (uint8 *)malloc(subSize + 8);
b.seek(-8, SEEK_CUR);
b.read(audioChunk, subSize + 8);
@@ -1725,7 +1739,7 @@ void SmushPlayer::handleFrame(int32 frameSize, Common::SeekableReadStream &b) {
InsaneRebel1 *rebel1 = (InsaneRebel1 *)_vm->_insane;
rebel1->handleGameChunk(embSize, embStream);
} else if (embTag == MKTAG('P','S','A','D')) {
- if (!_compressedFileMode && _fastForwardToFrame == 0) {
+ if (!_compressedFileMode && !isFastForwardingCurrentFrame()) {
uint8 *audioBuf = (uint8 *)malloc(embSize + 8);
memcpy(audioBuf, objBuf + objPos, embSize + 8);
feedAudio(audioBuf, 0, 127, 0, 0);
@@ -2201,9 +2215,15 @@ void SmushPlayer::play(const char *filename, int32 speed, int32 offset, int32 st
_seekPos = offset;
_seekFrame = startFrame;
_base = 0;
+ const uint32 fastForwardFromFrame = _fastForwardFromFrame;
+ const uint32 fastForwardToFrame = _fastForwardToFrame;
setupAnim(filename);
init(speed);
+ // Callers configure RA1 pre-roll before play(); preserve that target across
+ // init() so the playback loop can rebuild state up to the requested frame.
+ _fastForwardFromFrame = fastForwardFromFrame;
+ _fastForwardToFrame = fastForwardToFrame;
_startTime = _vm->_system->getMillis();
_startFrame = startFrame;
@@ -2228,9 +2248,10 @@ void SmushPlayer::play(const char *filename, int32 speed, int32 offset, int32 st
for (;;) {
bool skipFrame = false;
- // RA1 fast-forward: process frames rapidly without display/audio
- // until reaching the target frame. Used to skip recap sections.
- bool fastForwarding = (_fastForwardToFrame > 0 && _frame < _fastForwardToFrame);
+ // RA1 pre-roll: process the configured hidden frame window rapidly
+ // without display/audio. Route splices can leave the first frames visible
+ // and only suppress the middle span before the resume target.
+ bool fastForwarding = isFastForwardingCurrentFrame();
if (fastForwarding) {
// Process frame immediately without timing
@@ -2276,9 +2297,10 @@ void SmushPlayer::play(const char *filename, int32 speed, int32 offset, int32 st
// When fast-forwarding completes, reset timing so playback
// starts from the correct point without trying to catch up.
- if (_fastForwardToFrame > 0 && _frame >= _fastForwardToFrame) {
+ if (_fastForwardToFrame > _fastForwardFromFrame && _frame >= _fastForwardToFrame) {
_startFrame = _frame;
_startTime = _vm->_system->getMillis();
+ _fastForwardFromFrame = 0;
_fastForwardToFrame = 0;
}
diff --git a/engines/scumm/smush/smush_player.h b/engines/scumm/smush/smush_player.h
index cd87ff57af0..0421e032452 100644
--- a/engines/scumm/smush/smush_player.h
+++ b/engines/scumm/smush/smush_player.h
@@ -211,7 +211,9 @@ private:
bool _skipNext;
bool _ra2FastForwarding; // Fast-forwarding RA2 BEG video to establish background
uint32 _frame;
+ uint32 _fastForwardFromFrame; // First frame hidden by fast-forward (0 = hide from frame 0)
uint32 _fastForwardToFrame; // RA1: skip display/audio until this frame (0 = disabled)
+ bool _preserveVideoStateOnNextPlay;
// RA2: Global FOBJ position offsets (DAT_00482c1c / DAT_00482c20 in original)
// Set by InsaneRebel2 during IACT opcode 6 processing, reset in procPostRendering.
@@ -279,6 +281,9 @@ public:
void setCurVideoFlags(int16 flags);
SmushMultiFont *getMultiFont() const { return _multiFont; }
void ensureMultiFont();
+ bool isFastForwardingCurrentFrame() const;
+ void setPreserveVideoStateOnNextPlay(bool preserve) { _preserveVideoStateOnNextPlay = preserve; }
+ void setFastForwardFromFrame(uint32 frame) { _fastForwardFromFrame = frame; }
void setFastForwardToFrame(uint32 frame) { _fastForwardToFrame = frame; }
// Masked regions - areas where video should not update (e.g., destroyed enemies)
Commit: cea798d8c7e2d52cee16a4fe645c3f1e14b605d5
https://github.com/scummvm/scummvm/commit/cea798d8c7e2d52cee16a4fe645c3f1e14b605d5
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:46+02:00
Commit Message:
SCUMM: RA2: Move main loop out of scumm.cpp
Changed paths:
engines/scumm/insane/insane_rebel2.h
engines/scumm/insane/insane_rebel2_levels.cpp
engines/scumm/scumm.cpp
diff --git a/engines/scumm/insane/insane_rebel2.h b/engines/scumm/insane/insane_rebel2.h
index f123efb5bb7..bdc246cb85a 100644
--- a/engines/scumm/insane/insane_rebel2.h
+++ b/engines/scumm/insane/insane_rebel2.h
@@ -335,6 +335,10 @@ public:
kLevelReturnToMenu = 4 // Return to main menu
};
+ // Main game entry point â full game loop (intro, menu, pilot, chapter, levels)
+ // Emulates the retail game flow from FUN_004142BD
+ void runGame();
+
// Play the intro sequence (CREDITS/O_OPEN_C, O_OPEN_D, OPEN/O_OPEN_A, O_OPEN_B)
// Emulates case 0 in FUN_004142BD
void playIntroSequence();
diff --git a/engines/scumm/insane/insane_rebel2_levels.cpp b/engines/scumm/insane/insane_rebel2_levels.cpp
index 83d4f7f03fa..7b6907386e0 100644
--- a/engines/scumm/insane/insane_rebel2_levels.cpp
+++ b/engines/scumm/insane/insane_rebel2_levels.cpp
@@ -45,6 +45,82 @@ Common::String InsaneRebel2::getLevelPrefix(int levelId) {
return Common::String::format("%02d", levelId);
}
+//
+// runGame -- Main game entry point (FUN_004142BD)
+//
+// Full game loop: intro, main menu, pilot select, chapter select, level
+// progression. Called from ScummEngine::go().
+//
+void InsaneRebel2::runGame() {
+ SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
+
+ // Demo: just play the demo video and return
+ if (_vm->_game.features & GF_DEMO) {
+ splayer->play("OPEN/O_DEMO.SAN", 12);
+ return;
+ }
+
+ // Case 0: Play intro sequence (Fox logo, LucasArts logo, O_OPEN_A, O_OPEN_B)
+ playIntroSequence();
+
+ // Cases 1-4: Main menu -> pilot select -> chapter select -> gameplay loop
+ while (!_vm->shouldQuit()) {
+ int menuResult = runMainMenu();
+
+ if (menuResult == 0 || _vm->shouldQuit())
+ break;
+
+ if (menuResult == kMenuNewGame || menuResult == kMenuContinue) {
+ int pilotResult = runLevelSelect();
+
+ if (pilotResult == kLevelSelectQuit || _vm->shouldQuit())
+ break;
+
+ if (pilotResult == kLevelSelectBack)
+ continue;
+
+ int chapterResult = runChapterSelect();
+
+ if (chapterResult == kChapterSelectQuit || _vm->shouldQuit())
+ break;
+
+ if (chapterResult == kChapterSelectPlay) {
+ // _selectedChapter is 0-based, runLevel expects 1-based
+ int selectedLevel = _selectedChapter + 1;
+ debug("InsaneRebel2: Starting chapter %d (level %d)", _selectedChapter + 1, selectedLevel);
+
+ // Ending selected directly from chapter select (FUN_0041bbe8, case 0xf)
+ if (selectedLevel == 16) {
+ playEndingSequence();
+ }
+
+ // Level progression loop: on success, advance to next level
+ while (!_vm->shouldQuit() && selectedLevel >= 1 && selectedLevel <= 15) {
+ int result = runLevel(selectedLevel);
+
+ if (result == kLevelNextLevel) {
+ updatePilotProgress(selectedLevel - 1,
+ _playerScore, _playerLives, _playerDamage);
+ selectedLevel++;
+ if (selectedLevel > 15) {
+ playEndingSequence();
+ break;
+ }
+ } else {
+ if (_vm->shouldQuit() || result == kLevelQuit)
+ break;
+ break;
+ }
+ }
+
+ if (_vm->shouldQuit())
+ break;
+ }
+ // If kChapterSelectBack, loop back to main menu
+ }
+ }
+}
+
//
// playIntroSequence -- Intro sequence (FUN_004142BD case 0)
//
diff --git a/engines/scumm/scumm.cpp b/engines/scumm/scumm.cpp
index 424b222793e..36758e54d25 100644
--- a/engines/scumm/scumm.cpp
+++ b/engines/scumm/scumm.cpp
@@ -2720,90 +2720,7 @@ Common::Error ScummEngine::go() {
if (_game.id == GID_REBEL2) {
ScummEngine_v7 *vm7 = (ScummEngine_v7 *)this;
InsaneRebel2 *rebel = (InsaneRebel2 *)vm7->getInsane();
-
- // Use 12 FPS as default, same as The Dig. FT uses 10.
- // Since we don't have the standard scripts initializing this, we pass it here.
- if (_game.features & GF_DEMO) {
- vm7->_splayer->play("OPEN/O_DEMO.SAN", 12);
- return Common::kNoError;
- }
-
- // Full game: Emulates the retail game flow from FUN_004142BD
- // Case 0: Play intro sequence (Fox logo, LucasArts logo, O_OPEN_A, O_OPEN_B)
- rebel->playIntroSequence();
-
- // Cases 1-4: Main menu -> pilot select -> chapter select -> gameplay loop
- while (!shouldQuit()) {
- // Run main menu and get result
- int menuResult = rebel->runMainMenu();
-
- if (menuResult == 0 || shouldQuit()) {
- // Quit selected
- break;
- }
-
- if (menuResult == InsaneRebel2::kMenuNewGame || menuResult == InsaneRebel2::kMenuContinue) {
- // Start Game or Continue: Show pilot selection screen (FUN_00414A41)
- int pilotResult = rebel->runLevelSelect();
-
- if (pilotResult == InsaneRebel2::kLevelSelectQuit || shouldQuit()) {
- break;
- }
-
- if (pilotResult == InsaneRebel2::kLevelSelectBack) {
- // Back to main menu
- continue;
- }
-
- // Pilot selected: Show chapter selection screen (FUN_00415CF8)
- int chapterResult = rebel->runChapterSelect();
-
- if (chapterResult == InsaneRebel2::kChapterSelectQuit || shouldQuit()) {
- break;
- }
-
- if (chapterResult == InsaneRebel2::kChapterSelectPlay) {
- // Play the selected chapter using the level loading system
- // Note: _selectedChapter is 0-based, runLevel expects 1-based
- int selectedLevel = rebel->_selectedChapter + 1;
- debug("ScummEngine: Starting chapter %d (level %d)", rebel->_selectedChapter + 1, selectedLevel);
-
- // Ending selected directly from chapter select (FUN_0041bbe8, case 0xf)
- if (selectedLevel == 16) {
- rebel->playEndingSequence();
- }
-
- // Level progression loop: on success, advance to next level
- // Original game chains levels directly (e.g. FUN_0040598c(FUN_00418063,0))
- while (!shouldQuit() && selectedLevel >= 1 && selectedLevel <= 15) {
- int result = rebel->runLevel(selectedLevel);
-
- if (result == InsaneRebel2::kLevelNextLevel) {
- rebel->updatePilotProgress(selectedLevel - 1,
- rebel->_playerScore, rebel->_playerLives, rebel->_playerDamage);
- selectedLevel++;
- if (selectedLevel > 15) {
- // Beat the game â play ending sequence (FUN_0041bbe8)
- rebel->playEndingSequence();
- break;
- }
- } else {
- // kLevelGameOver, kLevelQuit, kLevelReturnToMenu â back to menu
- if (shouldQuit() || result == InsaneRebel2::kLevelQuit) {
- // Propagate quit to outer loop
- break;
- }
- break;
- }
- }
-
- if (shouldQuit()) {
- break;
- }
- }
- // If kChapterSelectBack, loop back to main menu
- }
- }
+ rebel->runGame();
return Common::kNoError;
}
#endif
Commit: 00f9be188bf7b9a8028635845fc6b05c0beaebd6
https://github.com/scummvm/scummvm/commit/00f9be188bf7b9a8028635845fc6b05c0beaebd6
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:46+02:00
Commit Message:
SCUMM: RA: Move Rebel Assault SMUSH logic into RA players
Changed paths:
engines/scumm/scumm.cpp
engines/scumm/smush/smush_player.cpp
engines/scumm/smush/smush_player.h
engines/scumm/smush/smush_player_ra1.cpp
engines/scumm/smush/smush_player_ra2.cpp
diff --git a/engines/scumm/scumm.cpp b/engines/scumm/scumm.cpp
index 36758e54d25..8363ec50108 100644
--- a/engines/scumm/scumm.cpp
+++ b/engines/scumm/scumm.cpp
@@ -1846,7 +1846,7 @@ void ScummEngine_v7::setupScumm(const Common::Path &macResourceFile) {
// Rebel Assault 2 doesn't use iMUSE for audio - audio is handled directly by INSANE
_musicEngine = _imuseDigital = nullptr;
_insane = new InsaneRebel2(this);
- _splayer = new SmushPlayer(this, nullptr, _insane);
+ _splayer = new SmushPlayerRebel2(this, nullptr, _insane);
// Initialize cursor
_macGui = nullptr; // Ensure this is null as we don't want MacGui behavior
diff --git a/engines/scumm/smush/smush_player.cpp b/engines/scumm/smush/smush_player.cpp
index abece384a6a..474324591c1 100644
--- a/engines/scumm/smush/smush_player.cpp
+++ b/engines/scumm/smush/smush_player.cpp
@@ -288,7 +288,6 @@ SmushPlayer::SmushPlayer(ScummEngine_v7 *scumm, IMuseDigital *imuseDigital, Insa
_fastForwardToFrame = 0;
_preserveVideoStateOnNextPlay = false;
- ra2InitFields();
_IACTpos = 0;
_speed = -1;
_insanity = false;
@@ -327,7 +326,6 @@ SmushPlayer::SmushPlayer(ScummEngine_v7 *scumm, IMuseDigital *imuseDigital, Insa
SmushPlayer::~SmushPlayer() {
delete _IACTchannel;
delete _compressedFileSoundHandle;
- ra2DestroyFields();
terminateAudio();
free(_frameBuffer);
@@ -368,8 +366,9 @@ void SmushPlayer::init(int32 speed) {
_vm->setDirtyColors(0, 255);
_dst = vs->getPixels(0, 0);
- if (isRA2())
- ra2InitVideo();
+ // Game-specific per-video init that needs _dst to be set
+ // (e.g. RA2 palette re-push + buffer clear/preserve).
+ initGameVideoState();
// HACK HACK HACK: This is an *evil* trick, beware!
// We do this to fix bug #1792. A proper solution would change all the
@@ -414,13 +413,11 @@ void SmushPlayer::release() {
_ra1CleanFrameSize = 0;
_ra1HasCleanFrame = false;
- if (isRA2()) {
- ra2ReleaseVideo();
- } else {
+ releaseGameVideoState();
+ if (!shouldPreserveFrameBuffer()) {
free(_frameBuffer);
_frameBuffer = nullptr;
}
- releaseGameVideoState();
_IACTstream = nullptr;
@@ -439,27 +436,15 @@ void SmushPlayer::release() {
}
void SmushPlayer::handleStore(int32 subSize, Common::SeekableReadStream &b) {
- debugC(DEBUG_SMUSH, "SmushPlayer::handleStore()");
+ debugC(DEBUG_SMUSH, "SmushPlayer::handleStore() frame=%d", _frame);
assert(subSize >= 4);
_storeFrame = true;
- if (isRA2()) {
- debug("SmushPlayer STOR: frame=%d - will store next FOBJ", _frame);
- }
- if (isRA1()) {
- debug("RA1 STOR: frame=%d", _frame);
- }
}
void SmushPlayer::handleFetch(int32 subSize, Common::SeekableReadStream &b) {
debugC(DEBUG_SMUSH, "SmushPlayer::handleFetch()");
- assert(subSize >= (isRA1() ? 4 : 6));
- if (isRA2()) {
- ra2HandleFetch(b);
- return;
- }
-
- // RA1 FTCH re-decodes the raw FOBJ captured by STOR (not a framebuffer memcpy).
+ // Game-specific FTCH handling (RA1 re-decodes stored FOBJ, RA2 does offset-aware re-decode)
if (handleGameFetch(subSize, b))
return;
@@ -484,7 +469,7 @@ void SmushPlayer::handleIACT(int32 subSize, Common::SeekableReadStream &b) {
// (RA2 audio uses PSAD chunks, not IACT)
bool isAudioIACT = (code == 8) && (flags == 46);
- if (!isAudioIACT || isRA2()) {
+ if (!isAudioIACT || shouldRouteAllIACTs()) {
_vm->_insane->procIACT(_dst, 0, 0, 0, b, subSize - 8, 0, code, flags, unknown, userId);
return;
}
@@ -712,7 +697,7 @@ void SmushPlayer::handleTextResource(uint32 subType, int32 subSize, Common::Seek
// RA2: The original game always shows subtitle text during cinematics
// (there is no subtitle toggle in the retail options menu). Skip
// this check so TRES text is always rendered.
- if (!isRA2() && (!ConfMan.getBool("subtitles")) && ((flags & 8) == 8))
+ if (!shouldAlwaysShowSubtitles() && (!ConfMan.getBool("subtitles")) && ((flags & 8) == 8))
return;
bool isCJKComi = (_vm->_game.id == GID_CMI && _vm->_useCJKMode);
@@ -780,8 +765,8 @@ void SmushPlayer::handleTextResource(uint32 subType, int32 subSize, Common::Seek
// bit 7 - skip ^ codes (COMI) 0x80 (should be irrelevant for Smush, we strip these commands anyway)
// bit 8 - no vertical fix (COMI) 0x100 (COMI handles this in the printing method, but I haven't seen a case where it is used)
- if (isRA2()) {
- ra2HandleTextResource(str, fontId, color, pos_x, pos_y, left, top, width, height, flg);
+ if (handleGameTextRendering(str, fontId, color, pos_x, pos_y, left, top, width, height, flg)) {
+ // Handled by game-specific renderer
} else {
SmushFont *sf = getFont(fontId);
assert(sf != nullptr);
@@ -905,7 +890,7 @@ void smushDecodeRA1Block(byte *dst, const byte *src, int left, int top, int widt
* followed by (copyLen+1) literal bytes. Destination advances by pitch
* per line, source advances by lineSize+2 per line.
*/
-static void smushDecodeRA1SkipCopy(byte *dst, const byte *src, int left, int top, int width, int height, int pitch) {
+void smushDecodeRA1SkipCopy(byte *dst, const byte *src, int left, int top, int width, int height, int pitch) {
dst += top * pitch + left;
for (int row = 0; row < height; row++) {
@@ -960,7 +945,7 @@ static void smushDecodeRA1SkipCopy(byte *dst, const byte *src, int left, int top
* `(paletteBase - 0x30)` to the destination pixels already present in the
* framebuffer.
*/
-static void smushDecodeRA1AdditiveLineUpdate(byte *dst, const byte *src, int left, int top, int width, int height,
+void smushDecodeRA1AdditiveLineUpdate(byte *dst, const byte *src, int left, int top, int width, int height,
int pitch, int bufWidth, int bufHeight, uint8 paletteBase) {
const uint8 colorDelta = (uint8)(paletteBase - 0x30);
@@ -1006,7 +991,7 @@ static void smushDecodeRA1AdditiveLineUpdate(byte *dst, const byte *src, int lef
* Position starts at (left, top) and accumulates (dx, dy) per entry.
* Pixel is drawn if position is within buffer bounds.
*/
-static void smushDecodeRA1Scatter(byte *dst, const byte *src, int left, int top, int bufWidth, int bufHeight, int pitch, int dataSize) {
+void smushDecodeRA1Scatter(byte *dst, const byte *src, int left, int top, int bufWidth, int bufHeight, int pitch, int dataSize) {
int curX = left;
int curY = top;
@@ -1187,19 +1172,8 @@ void SmushPlayer::decodeFrameObject(int codec, const uint8 *src, int left, int t
_specialBufferSize = bufSize;
}
_dst = _specialBuffer;
- } else if (isRA1()) {
- // RA1 sub-fullscreen frames (e.g. O1OPTION.ANM uses ~319x196 codec 2
- // frames for the starfield animation). Render into _specialBuffer at
- // their (left, top) offset position.
- int bufSize = 384 * 242;
- if (_specialBuffer == nullptr || bufSize > _specialBufferSize) {
- free(_specialBuffer);
- _specialBuffer = (byte *)calloc(bufSize, 1);
- _specialBufferSize = bufSize;
- }
- _dst = _specialBuffer;
- } else if (isRA2() && ((height != _vm->_screenHeight) || (width != _vm->_screenWidth))) {
- ra2SelectFrameBuffer(width, height);
+ } else if (handleGameFrameBufferSelect(codec, width, height)) {
+ // Game-specific buffer selection handled
} else if ((height > _vm->_screenHeight) || (width > _vm->_screenWidth))
return;
// FT Insane uses smaller frames to draw overlays with moving objects
@@ -1211,15 +1185,8 @@ void SmushPlayer::decodeFrameObject(int codec, const uint8 *src, int left, int t
if ((height == 242) && (width == 384)) {
_width = width;
_height = height;
- } else if (isRA1() && _dst == _specialBuffer) {
- // RA1: sub-fullscreen FOBJs should not override the 384x242 dimensions.
- // Set dimensions on first use if not yet established.
- if (_width == 0 || _height == 0) {
- _width = 384;
- _height = 242;
- }
- } else if (isRA2() && ((height != _vm->_screenHeight) || (width != _vm->_screenWidth))) {
- // RA2: preserve _width/_height set during buffer allocation
+ } else if (handleGameDimensionOverride(codec, width, height)) {
+ // Game-specific dimension handling
} else {
_width = _vm->_screenWidth;
_height = _vm->_screenHeight;
@@ -1232,15 +1199,11 @@ void SmushPlayer::decodeFrameObject(int codec, const uint8 *src, int left, int t
// Save original FOBJ position and dimensions before clipping. Codec 37/47
// (delta block/glyph) decode the full frame starting at (0,0). Codec 2
// (scatter draw) needs the original start position for accumulation.
- int origLeft = left;
- int origTop = top;
int origWidth = width;
int origHeight = height;
int srcSkipY = 0;
- const bool ra1AdditiveCodec = isRA1() && (codec == SMUSH_CODEC_SKIP_RLE);
- if ((isRA1() || isRA2()) && !ra1AdditiveCodec) {
- ra2AdjustFrameCoords(left, top, width, height, pitch, &srcSkipY);
+ if (handleGameAdjustCoords(codec, left, top, width, height, pitch, &srcSkipY)) {
if (width <= 0 || height <= 0)
return;
}
@@ -1248,103 +1211,46 @@ void SmushPlayer::decodeFrameObject(int codec, const uint8 *src, int left, int t
// For RLE codecs, skip source rows that were clipped from the top.
// Each RLE row has a 2-byte size prefix, so we can advance past them.
const uint8 *adjustedSrc = src;
- if (isRA1() && srcSkipY > 0 && (codec == SMUSH_CODEC_RLE || codec == SMUSH_CODEC_RLE_ALT || codec == SMUSH_CODEC_LINE_UPDATE)) {
- for (int i = 0; i < srcSkipY; i++) {
- adjustedSrc += READ_LE_UINT16(adjustedSrc) + 2;
- }
+ for (int i = 0; i < srcSkipY; i++) {
+ adjustedSrc += READ_LE_UINT16(adjustedSrc) + 2;
}
- switch (codec) {
- case SMUSH_CODEC_RLE:
- if (isRA1()) {
- // RA1 codec 1: pixel 0 is transparent (background shows through)
- smushDecodeRA1Transparent(_dst, adjustedSrc, left, top, width, height, pitch);
- } else {
+ // Try game-specific codec handler first (pass adjustedSrc for row-skip-aware codecs)
+ if (!handleGameCodecDecode(codec, adjustedSrc, left, top, width, height, pitch, dataSize)) {
+ // Standard SCUMM codec dispatch
+ switch (codec) {
+ case SMUSH_CODEC_RLE:
smushDecodeRLE(_dst, adjustedSrc, left, top, width, height, pitch);
- }
- break;
- case SMUSH_CODEC_RLE_ALT:
- // Codec 3: all pixels opaque (pixel 0 is written).
- // Original FUN_00010916 writes every byte including value 0.
- if (isRA1()) {
- smushDecodeRLEOpaque(_dst, adjustedSrc, left, top, width, height, pitch);
- } else {
+ break;
+ case SMUSH_CODEC_RLE_ALT:
smushDecodeRLE(_dst, adjustedSrc, left, top, width, height, pitch);
- }
- break;
- case SMUSH_CODEC_RA1_SCATTER:
- // Codec 2: Scatter draw uses absolute buffer coords, not clipped FOBJ coords.
- // Pass full buffer dimensions for clipping (not the FOBJ width/height).
- smushDecodeRA1Scatter(_dst, src, origLeft, origTop, _width, _height, pitch, dataSize);
- break;
- case SMUSH_CODEC_DELTA_BLOCKS:
- // Codec 37 writes the full frame to dst via memcpy â always uses original
- // FOBJ dimensions, not position-clipped dimensions.
- if (!_deltaBlocksCodec)
- _deltaBlocksCodec = new SmushDeltaBlocksDecoder(origWidth, origHeight);
- if (_deltaBlocksCodec)
- _deltaBlocksCodec->decode(_dst, src);
- break;
- case SMUSH_CODEC_DELTA_GLYPHS:
- // Codec 47 also writes the full frame â use original dimensions.
- if (!_deltaGlyphsCodec)
- _deltaGlyphsCodec = new SmushDeltaGlyphsDecoder(origWidth, origHeight);
- if (_deltaGlyphsCodec)
- _deltaGlyphsCodec->decode(_dst, src);
- break;
- case SMUSH_CODEC_RA1_DELTA:
- case SMUSH_CODEC_RA1_BLOCK:
- smushDecodeRA1Block(_dst, src, left, top, width, height, pitch, dataSize, ra1Param, ra1Parm2, codec);
- break;
- case SMUSH_CODEC_UNCOMPRESSED:
- smushDecodeUncompressed(_dst, src, left, top, width, height, pitch);
- break;
- case SMUSH_CODEC_LINE_UPDATE:
- if (isRA1()) {
- smushDecodeRA1SkipCopy(_dst, adjustedSrc, left, top, width, height, pitch);
break;
- }
- // Fall through for RA2
- case SMUSH_CODEC_SKIP_RLE:
- if (isRA1()) {
- const int bufWidth = pitch;
- const int bufHeight = (_dst == _specialBuffer) ? _height : _vm->_screenHeight;
- smushDecodeRA1AdditiveLineUpdate(_dst, src, origLeft, origTop, origWidth, origHeight,
- pitch, bufWidth, bufHeight, ra1Param);
+ case SMUSH_CODEC_DELTA_BLOCKS:
+ if (!_deltaBlocksCodec)
+ _deltaBlocksCodec = new SmushDeltaBlocksDecoder(origWidth, origHeight);
+ if (_deltaBlocksCodec)
+ _deltaBlocksCodec->decode(_dst, src);
break;
- }
- // Fall through for RA2
- default:
- if (isRA2() && ra2DecodeCodec(codec, src, left, top, width, height, pitch, dataSize))
+ case SMUSH_CODEC_DELTA_GLYPHS:
+ if (!_deltaGlyphsCodec)
+ _deltaGlyphsCodec = new SmushDeltaGlyphsDecoder(origWidth, origHeight);
+ if (_deltaGlyphsCodec)
+ _deltaGlyphsCodec->decode(_dst, src);
break;
- if (isRA1() || isRA2()) {
- debugC(DEBUG_SMUSH, "SmushPlayer::decodeFrameObject: Skipping unknown codec %d (left=%d, top=%d, %dx%d)",
- codec, left, top, width, height);
+ case SMUSH_CODEC_UNCOMPRESSED:
+ smushDecodeUncompressed(_dst, src, left, top, width, height, pitch);
break;
+ default:
+ if (isInsaneGame()) {
+ debugC(DEBUG_SMUSH, "SmushPlayer::decodeFrameObject: Skipping unknown codec %d (left=%d, top=%d, %dx%d)",
+ codec, left, top, width, height);
+ } else {
+ error("Invalid codec for frame object : %d", codec);
+ }
}
- error("Invalid codec for frame object : %d", codec);
- }
-
- // RA2 debug: check buffer fill after decode
- if (isRA2() && _dst == _specialBuffer && _frame < 3) {
- int nonZero = 0;
- int total = _width * _height;
- for (int i = 0; i < total; i++) {
- if (_dst[i] != 0) nonZero++;
- }
- // Sample bottom half
- int bottomNonZero = 0;
- int bottomStart = (_height / 2) * _width;
- int bottomTotal = total - bottomStart;
- for (int i = bottomStart; i < total; i++) {
- if (_dst[i] != 0) bottomNonZero++;
- }
- debug("SmushPlayer FOBJ decode done: frame=%d codec=%d buf=%dx%d total=%d nonzero=%d (%d%%) bottomHalf=%d/%d (%d%%)",
- _frame, codec, _width, _height, total, nonZero, (nonZero * 100) / total,
- bottomNonZero, bottomTotal, bottomTotal > 0 ? (bottomNonZero * 100) / bottomTotal : 0);
}
- if (_storeFrame && !isRA1() && !isRA2()) {
+ if (_storeFrame && !handleGameStoreFrame()) {
if (_frameBuffer == nullptr) {
_frameBuffer = (byte *)malloc(_width * _height);
}
@@ -1412,27 +1318,16 @@ void SmushPlayer::handleFrameObject(int32 subSize, Common::SeekableReadStream &b
debugC(DEBUG_SMUSH, "SmushPlayer::handleFrameObject: frame=%d codec=%d pos=(%d,%d) size=%dx%d dataSize=%d",
_frame, codec, left, top, width, height, subSize - 14);
- if (isRA2()) {
- debug("SmushPlayer FOBJ: frame=%d codec=%d pos=(%d,%d) size=%dx%d dataSize=%d storeFrame=%d _width=%d _height=%d",
- _frame, codec, left, top, width, height, subSize - 14, _storeFrame, _width, _height);
- }
- if (isRA1()) {
- debug("RA1 FOBJ: frame=%d codec=%d object=%d pos=(%d,%d) size=%dx%d dataSize=%d storeFrame=%d",
- _frame, codec, ra1ObjectId, left, top, width, height, subSize - 14, _storeFrame);
- }
+ handleGameFrameObjectPre(codec, left, top, width, height, subSize - 14);
int32 chunk_size = subSize - 14;
byte *chunk_buffer = (byte *)malloc(chunk_size);
assert(chunk_buffer);
b.read(chunk_buffer, chunk_size);
- if (isRA1() || isRA2()) {
- ra2RememberLastFobj(codec, chunk_buffer, chunk_size, left, top, width, height);
- }
+ handleGameFrameObjectPost(codec, chunk_buffer, chunk_size, left, top, width, height);
- if (_storeFrame && isRA2()) {
- ra2StoreFobjData(codec, chunk_buffer, chunk_size, left, top, width, height);
- }
+ // RA1 STOR needs ra1Param/rawLeft/rawTop which aren't available through the virtual hook
if (_storeFrame && isRA1()) {
free(_storedFobjData);
_storedFobjData = (byte *)malloc(chunk_size);
@@ -1505,8 +1400,7 @@ void SmushPlayer::handleFrame(int32 frameSize, Common::SeekableReadStream &b) {
debugC(DEBUG_SMUSH, "SmushPlayer::handleFrame(%d)", _frame);
uint8 *audioChunk = nullptr;
_skipNext = false;
- if (isRA2() || isRA1())
- _hasFrameFobjForGost = false;
+ handleGameFrameStart();
bool interactiveRA1 = false;
bool forceInteractiveClearRA1 = false;
@@ -1591,10 +1485,7 @@ void SmushPlayer::handleFrame(int32 frameSize, Common::SeekableReadStream &b) {
break;
}
- // RA2: When _skipNext is set, skip the NEXT chunk of ANY type
- if (isRA2() && _skipNext) {
- _skipNext = false;
- debugC(DEBUG_SMUSH, "SmushPlayer::handleFrame: SKIP consumed chunk %s frame=%d", tag2str(subType), _frame);
+ if (handleGameSkipChunk(subType, subSize, b)) {
frameSize -= subSize + 8;
b.seek(subOffset + subSize, SEEK_SET);
if (subSize & 1) {
@@ -1650,10 +1541,7 @@ void SmushPlayer::handleFrame(int32 frameSize, Common::SeekableReadStream &b) {
handleLoad(subSize, b);
break;
case MKTAG('G','O','S','T'):
- if (isRA1())
- ra1HandleGost(subSize, b);
- else if (isRA2())
- ra2HandleGost(subSize, b);
+ handleGameGost(subSize, b);
break;
// RA1-specific chunk types: skip gracefully
case MKTAG('G','A','M','E'):
@@ -1846,10 +1734,6 @@ void SmushPlayer::handleAnimHeader(int32 subSize, Common::SeekableReadStream &b)
byte *palettePtr = &headerContent[6];
memcpy(_pal, palettePtr, sizeof(_pal));
adjustGamePalette();
-
- if (isRA2())
- ra2ResetDeltaPalette();
-
setDirtyColors(0, 255);
}
@@ -1858,17 +1742,15 @@ void SmushPlayer::handleAnimHeader(int32 subSize, Common::SeekableReadStream &b)
_height = READ_LE_UINT16(&headerContent[6]);
}
- if (isRA2())
- ra2FixupAnimHeader();
-
free(headerContent);
}
}
void SmushPlayer::setupAnim(const char *file) {
if (_insanity) {
- if (isRA2()) {
- _strings = getStrings(_vm, "SYSTM/GAME.TRS", true);
+ const char *gameStringResource = getGameStringResource();
+ if (gameStringResource) {
+ _strings = getStrings(_vm, gameStringResource, true);
} else if (!((_vm->_game.features & GF_DEMO) && (_vm->_game.platform == Common::kPlatformDOS))) {
readString("mineroad.trs");
}
@@ -1902,8 +1784,6 @@ SmushFont *SmushPlayer::getFont(int font) {
_sf[font] = new SmushFont(_vm, ft_fonts[font], true);
}
- } else if (isRA2()) {
- return ra2GetFont(font);
} else {
int numFonts = (_vm->_game.id == GID_CMI && !(_vm->_game.features & GF_DEMO)) ? 5 : 4;
assert(font >= 0 && font < numFonts);
@@ -2086,11 +1966,8 @@ void SmushPlayer::parseNextFrame() {
if (_vm->_imuseDigital)
_vm->_imuseDigital->flushTracks();
- if (!_imuseDigital && isRA2())
- ra2ParseNextFrame();
-
- if (!_imuseDigital && isRA1())
- processDispatches(_smushAudioSampleRate / _speed);
+ if (!_imuseDigital)
+ handleGameParseNextFrame();
}
void SmushPlayer::setPalette(const byte *palette) {
@@ -2207,7 +2084,7 @@ void SmushPlayer::play(const char *filename, int32 speed, int32 offset, int32 st
// Hide mouse
bool oldMouseState = CursorMan.showMouse(false);
- if (isRA1() || isRA2())
+ if (isInsaneGame())
insanity(true);
// Load the video
@@ -2716,13 +2593,7 @@ void SmushPlayer::processDispatches(int16 feedSize) {
bool speechIsPlaying = false;
if (!_imuseDigital) {
- if (isRA2() && _insane) {
- InsaneRebel2 *rebel2 = static_cast<InsaneRebel2 *>(_insane);
- rebel2->processAudioFrame(feedSize);
- } else if (isRA1() && _insane) {
- InsaneRebel1 *rebel1 = static_cast<InsaneRebel1 *>(_insane);
- rebel1->processAudioFrame(feedSize);
- }
+ handleGameProcessAudio(feedSize);
return;
}
diff --git a/engines/scumm/smush/smush_player.h b/engines/scumm/smush/smush_player.h
index 0421e032452..8ae998f8a7c 100644
--- a/engines/scumm/smush/smush_player.h
+++ b/engines/scumm/smush/smush_player.h
@@ -99,12 +99,14 @@ class SmushDeltaGlyphsDecoder;
class IMuseDigital;
class Insane;
class SmushPlayerRebel1;
+class SmushPlayerRebel2;
class SmushPlayer {
friend class Insane;
friend class InsaneRebel1;
friend class InsaneRebel2;
friend class SmushPlayerRebel1;
+ friend class SmushPlayerRebel2;
friend class SmushMultiFont;
private:
struct SmushAudioDispatch {
@@ -316,12 +318,31 @@ protected:
virtual void initGamePlayerFields() {}
virtual void destroyGamePlayerFields() {}
virtual void resetGameVideoState() {}
+ virtual void initGameVideoState() {}
virtual void releaseGameVideoState() {}
+ virtual bool shouldPreserveFrameBuffer() const { return false; }
virtual bool handleGameFetch(int32 subSize, Common::SeekableReadStream &b) { return false; }
virtual bool handleGameTextResource(uint32 subType, int32 subSize, Common::SeekableReadStream &b) { return false; }
+ virtual bool handleGameTextRendering(const char *str, int fontId, int color, int pos_x, int pos_y, int left, int top, int width, int height, TextStyleFlags flg) { return false; }
+ virtual bool shouldAlwaysShowSubtitles() const { return false; }
virtual SmushFont *getGameFont(int font) { return nullptr; }
virtual void adjustGamePalette() {}
virtual bool handleGameAnimHeader(byte *headerContent) { return false; }
+ virtual const char *getGameStringResource() const { return nullptr; }
+ virtual void handleGameParseNextFrame() {}
+ virtual bool shouldRouteAllIACTs() const { return false; }
+ virtual bool handleGameFrameBufferSelect(int codec, int width, int height) { return false; }
+ virtual bool handleGameDimensionOverride(int codec, int width, int height) { return false; }
+ virtual bool handleGameAdjustCoords(int codec, int &left, int &top, int &width, int &height, int pitch, int *srcSkipY) { return false; }
+ virtual bool handleGameCodecDecode(int codec, const uint8 *src, int left, int top, int width, int height, int pitch, int dataSize) { return false; }
+ virtual bool handleGameStoreFrame() { return false; }
+ virtual void handleGameFrameObjectPre(int codec, int left, int top, int width, int height, int dataSize) {}
+ virtual void handleGameFrameObjectPost(int codec, const byte *data, int32 dataSize, int left, int top, int width, int height) {}
+ virtual void handleGameFrameStart() {}
+ virtual bool handleGameSkipChunk(uint32 subType, int32 subSize, Common::SeekableReadStream &b) { return false; }
+ virtual void handleGameGost(int32 subSize, Common::SeekableReadStream &b) {}
+ virtual void handleGameProcessAudio(int16 feedSize) {}
+ virtual bool isInsaneGame() const { return false; }
private:
SmushFont *getFont(int font);
@@ -347,14 +368,10 @@ private:
void handleLoad(int32 subSize, Common::SeekableReadStream &); // RA2 only (impl in smush_player_ra2.cpp)
void readPalette(byte *, Common::SeekableReadStream &);
- // RA1/RA2 identification
+ // RA1/RA2 identification (isRA1 still used in handleFrameObject/handleFrame RA1 paths)
bool isRA1() const;
bool isRA2() const;
- void ra2InitFields();
- void ra2DestroyFields();
- void ra2InitVideo();
- void ra2ReleaseVideo();
- void ra2HandleFetch(Common::SeekableReadStream &b);
+ // RA2 helper methods called from SmushPlayerRebel2 overrides
void ra2HandleTextResource(const char *str, int fontId, int color,
int pos_x, int pos_y, int left, int top,
int width, int height, TextStyleFlags flg);
@@ -368,12 +385,8 @@ private:
int left, int top, int width, int height);
void ra1HandleGost(int32 subSize, Common::SeekableReadStream &b);
void ra2HandleGost(int32 subSize, Common::SeekableReadStream &b);
- void ra2ResetDeltaPalette();
SmushFont *ra1GetFont(int font);
void ra1HandleText(int32 subSize, Common::SeekableReadStream &b);
- SmushFont *ra2GetFont(int font);
- void ra2ParseNextFrame();
- void ra2FixupAnimHeader();
// LOAD chunk streaming buffer (RA2 - embedded resource data)
byte *_loadBuffer; // Accumulated LOAD data
@@ -412,6 +425,52 @@ protected:
SmushFont *getGameFont(int font) override;
void adjustGamePalette() override;
bool handleGameAnimHeader(byte *headerContent) override;
+ void handleGameParseNextFrame() override;
+ bool handleGameFrameBufferSelect(int codec, int width, int height) override;
+ bool handleGameDimensionOverride(int codec, int width, int height) override;
+ bool handleGameAdjustCoords(int codec, int &left, int &top, int &width, int &height, int pitch, int *srcSkipY) override;
+ bool handleGameCodecDecode(int codec, const uint8 *src, int left, int top, int width, int height, int pitch, int dataSize) override;
+ bool handleGameStoreFrame() override;
+ void handleGameFrameObjectPre(int codec, int left, int top, int width, int height, int dataSize) override;
+ void handleGameFrameObjectPost(int codec, const byte *data, int32 dataSize, int left, int top, int width, int height) override;
+ void handleGameFrameStart() override;
+ void handleGameGost(int32 subSize, Common::SeekableReadStream &b) override;
+ void handleGameProcessAudio(int16 feedSize) override;
+ bool isInsaneGame() const override { return true; }
+};
+
+class SmushPlayerRebel2 : public SmushPlayer {
+public:
+ SmushPlayerRebel2(ScummEngine_v7 *scumm, IMuseDigital *imuseDigital, Insane *insane);
+ ~SmushPlayerRebel2() override;
+
+protected:
+ void initGamePlayerFields() override;
+ void destroyGamePlayerFields() override;
+ void initGameVideoState() override;
+ void releaseGameVideoState() override;
+ bool shouldPreserveFrameBuffer() const override { return true; }
+ bool handleGameFetch(int32 subSize, Common::SeekableReadStream &b) override;
+ bool handleGameTextRendering(const char *str, int fontId, int color, int pos_x, int pos_y, int left, int top, int width, int height, TextStyleFlags flg) override;
+ bool shouldAlwaysShowSubtitles() const override { return true; }
+ SmushFont *getGameFont(int font) override;
+ void adjustGamePalette() override;
+ bool handleGameAnimHeader(byte *headerContent) override;
+ const char *getGameStringResource() const override { return "SYSTM/GAME.TRS"; }
+ void handleGameParseNextFrame() override;
+ bool shouldRouteAllIACTs() const override { return true; }
+ bool handleGameFrameBufferSelect(int codec, int width, int height) override;
+ bool handleGameDimensionOverride(int codec, int width, int height) override;
+ bool handleGameAdjustCoords(int codec, int &left, int &top, int &width, int &height, int pitch, int *srcSkipY) override;
+ bool handleGameCodecDecode(int codec, const uint8 *src, int left, int top, int width, int height, int pitch, int dataSize) override;
+ bool handleGameStoreFrame() override;
+ void handleGameFrameObjectPre(int codec, int left, int top, int width, int height, int dataSize) override;
+ void handleGameFrameObjectPost(int codec, const byte *data, int32 dataSize, int left, int top, int width, int height) override;
+ void handleGameFrameStart() override;
+ bool handleGameSkipChunk(uint32 subType, int32 subSize, Common::SeekableReadStream &b) override;
+ void handleGameGost(int32 subSize, Common::SeekableReadStream &b) override;
+ void handleGameProcessAudio(int16 feedSize) override;
+ bool isInsaneGame() const override { return true; }
};
} // End of namespace Scumm
diff --git a/engines/scumm/smush/smush_player_ra1.cpp b/engines/scumm/smush/smush_player_ra1.cpp
index 47b043829f4..cbd47b224a1 100644
--- a/engines/scumm/smush/smush_player_ra1.cpp
+++ b/engines/scumm/smush/smush_player_ra1.cpp
@@ -237,4 +237,115 @@ bool SmushPlayerRebel1::handleGameAnimHeader(byte *headerContent) {
return true;
}
+void SmushPlayerRebel1::handleGameParseNextFrame() {
+ processDispatches(_smushAudioSampleRate / _speed);
+}
+
+// Forward declarations for RA1 codec functions (defined in smush_player.cpp and codec1.cpp)
+void smushDecodeRA1Transparent(byte *dst, const byte *src, int left, int top, int width, int height, int pitch);
+void smushDecodeRLEOpaque(byte *dst, const byte *src, int left, int top, int width, int height, int pitch);
+void smushDecodeRA1SkipCopy(byte *dst, const byte *src, int left, int top, int width, int height, int pitch);
+void smushDecodeRA1AdditiveLineUpdate(byte *dst, const byte *src, int left, int top, int width, int height,
+ int pitch, int bufWidth, int bufHeight, uint8 param);
+void smushDecodeRA1Scatter(byte *dst, const byte *src, int left, int top, int bufWidth, int bufHeight, int pitch, int dataSize);
+void smushDecodeRA1Block(byte *dst, const byte *src, int left, int top, int width, int height, int pitch, int dataSize, uint8 param, uint16 parm2, int codec);
+
+bool SmushPlayerRebel1::handleGameFrameBufferSelect(int codec, int width, int height) {
+ // RA1 sub-fullscreen frames render into _specialBuffer at their (left, top) offset position.
+ int bufSize = 384 * 242;
+ if (_specialBuffer == nullptr || bufSize > _specialBufferSize) {
+ free(_specialBuffer);
+ _specialBuffer = (byte *)calloc(bufSize, 1);
+ _specialBufferSize = bufSize;
+ }
+ _dst = _specialBuffer;
+ return true;
+}
+
+bool SmushPlayerRebel1::handleGameDimensionOverride(int codec, int width, int height) {
+ if (_dst == _specialBuffer) {
+ // RA1: sub-fullscreen FOBJs should not override the 384x242 dimensions.
+ if (_width == 0 || _height == 0) {
+ _width = 384;
+ _height = 242;
+ }
+ return true;
+ }
+ return false;
+}
+
+bool SmushPlayerRebel1::handleGameAdjustCoords(int codec, int &left, int &top, int &width, int &height, int pitch, int *srcSkipY) {
+ // RA1 additive codec (SKIP_RLE) uses original coords, not adjusted
+ if (codec == SMUSH_CODEC_SKIP_RLE)
+ return false;
+ ra2AdjustFrameCoords(left, top, width, height, pitch, srcSkipY);
+ return true;
+}
+
+bool SmushPlayerRebel1::handleGameCodecDecode(int codec, const uint8 *src, int left, int top, int width, int height, int pitch, int dataSize) {
+ // The base class passes clipped coords. For additive codec and scatter, we need original coords
+ // which are stored in the origLeft/origTop locals of decodeFrameObject. Since we can't access those
+ // from the override, the additive codec and scatter draw are special-cased.
+ // For now, handle the codecs that have RA1-specific behavior.
+ switch (codec) {
+ case SMUSH_CODEC_RLE:
+ smushDecodeRA1Transparent(_dst, src, left, top, width, height, pitch);
+ return true;
+ case SMUSH_CODEC_RLE_ALT:
+ smushDecodeRLEOpaque(_dst, src, left, top, width, height, pitch);
+ return true;
+ case SMUSH_CODEC_RA1_SCATTER:
+ smushDecodeRA1Scatter(_dst, src, left, top, _width, _height, pitch, dataSize);
+ return true;
+ case SMUSH_CODEC_RA1_DELTA:
+ case SMUSH_CODEC_RA1_BLOCK:
+ smushDecodeRA1Block(_dst, src, left, top, width, height, pitch, dataSize, 0, 0, codec);
+ return true;
+ case SMUSH_CODEC_LINE_UPDATE:
+ smushDecodeRA1SkipCopy(_dst, src, left, top, width, height, pitch);
+ return true;
+ case SMUSH_CODEC_SKIP_RLE: {
+ const int bufWidth = pitch;
+ const int bufHeight = (_dst == _specialBuffer) ? _height : _vm->_screenHeight;
+ smushDecodeRA1AdditiveLineUpdate(_dst, src, left, top, width, height,
+ pitch, bufWidth, bufHeight, 0);
+ return true;
+ }
+ default:
+ debugC(DEBUG_SMUSH, "SmushPlayer::decodeFrameObject: Skipping unknown codec %d (left=%d, top=%d, %dx%d)",
+ codec, left, top, width, height);
+ return true;
+ }
+}
+
+bool SmushPlayerRebel1::handleGameStoreFrame() {
+ // RA1 handles STOR via handleGameFrameObjectPost
+ return true;
+}
+
+void SmushPlayerRebel1::handleGameFrameObjectPre(int codec, int left, int top, int width, int height, int dataSize) {
+ debug("RA1 FOBJ: frame=%d codec=%d pos=(%d,%d) size=%dx%d dataSize=%d storeFrame=%d",
+ _frame, codec, left, top, width, height, dataSize, _storeFrame);
+}
+
+void SmushPlayerRebel1::handleGameFrameObjectPost(int codec, const byte *data, int32 dataSize, int left, int top, int width, int height) {
+ ra2RememberLastFobj(codec, data, dataSize, left, top, width, height);
+ // RA1 STOR handling remains in handleFrameObject (needs ra1Param/rawLeft/rawTop)
+}
+
+void SmushPlayerRebel1::handleGameFrameStart() {
+ _hasFrameFobjForGost = false;
+}
+
+void SmushPlayerRebel1::handleGameGost(int32 subSize, Common::SeekableReadStream &b) {
+ ra1HandleGost(subSize, b);
+}
+
+void SmushPlayerRebel1::handleGameProcessAudio(int16 feedSize) {
+ if (_insane) {
+ InsaneRebel1 *rebel1 = static_cast<InsaneRebel1 *>(_insane);
+ rebel1->processAudioFrame(feedSize);
+ }
+}
+
} // End of namespace Scumm
diff --git a/engines/scumm/smush/smush_player_ra2.cpp b/engines/scumm/smush/smush_player_ra2.cpp
index e19f4b51a76..8feab912d52 100644
--- a/engines/scumm/smush/smush_player_ra2.cpp
+++ b/engines/scumm/smush/smush_player_ra2.cpp
@@ -19,11 +19,10 @@
*
*/
-// Rebel Assault 2 specific SmushPlayer methods
+// SmushPlayerRebel2 â RA2-specific SmushPlayer subclass
//
-// These are methods of the SmushPlayer class that contain RA2-specific logic.
-// Keeping them in a separate file minimizes the diff on smush_player.cpp and
-// reduces the risk of regressions in Full Throttle / The Dig / CMI.
+// Overrides the virtual hooks defined in SmushPlayer to provide
+// Rebel Assault 2 specific video, font, text and codec handling.
#include "common/endian.h"
#include "common/rect.h"
@@ -53,10 +52,24 @@ void smushDecodeLineUpdate(byte *dst, const byte *src, int left, int top, int wi
void smushDecodeSkipRLE(byte *dst, const byte *src, int left, int top, int width, int height, int pitch, int dataSize);
void smushDecodeRA2Bomp(byte *dst, const byte *src, int left, int top, int width, int height, int pitch, int dataSize);
-/**
- * Initialize RA2-specific fields in the SmushPlayer constructor.
- */
-void SmushPlayer::ra2InitFields() {
+// ---------------------------------------------------------------------------
+// SmushPlayerRebel2 â construction / destruction
+// ---------------------------------------------------------------------------
+
+SmushPlayerRebel2::SmushPlayerRebel2(ScummEngine_v7 *scumm, IMuseDigital *imuseDigital, Insane *insane)
+ : SmushPlayer(scumm, imuseDigital, insane) {
+ initGamePlayerFields();
+}
+
+SmushPlayerRebel2::~SmushPlayerRebel2() {
+ destroyGamePlayerFields();
+}
+
+// ---------------------------------------------------------------------------
+// Virtual hook overrides
+// ---------------------------------------------------------------------------
+
+void SmushPlayerRebel2::initGamePlayerFields() {
_multiFont = nullptr;
_storedFobjData = nullptr;
_storedFobjDataSize = 0;
@@ -98,10 +111,7 @@ void SmushPlayer::ra2InitFields() {
_scrollY = 0;
}
-/**
- * Free RA2-specific resources in the SmushPlayer destructor.
- */
-void SmushPlayer::ra2DestroyFields() {
+void SmushPlayerRebel2::destroyGamePlayerFields() {
delete _multiFont;
_multiFont = nullptr;
free(_storedFobjData);
@@ -119,7 +129,7 @@ void SmushPlayer::ra2DestroyFields() {
* Re-pushes the SMUSH palette (videos without NPAL inherit from previous),
* and handles background preservation between cinematic and gameplay videos.
*/
-void SmushPlayer::ra2InitVideo() {
+void SmushPlayerRebel2::initGameVideoState() {
// Re-push the SMUSH palette to the system. Videos like O_LEVEL.SAN
// have no NPAL chunk and inherit the palette from the previous video.
// Since play() resets _palDirtyMin/Max, the palette would never be pushed otherwise.
@@ -149,7 +159,7 @@ void SmushPlayer::ra2InitVideo() {
* RA2-specific cleanup in SmushPlayer::release().
* Frees stored FOBJ data but preserves _frameBuffer across videos.
*/
-void SmushPlayer::ra2ReleaseVideo() {
+void SmushPlayerRebel2::releaseGameVideoState() {
free(_storedFobjData);
_storedFobjData = nullptr;
_storedFobjDataSize = 0;
@@ -170,7 +180,7 @@ void SmushPlayer::ra2ReleaseVideo() {
* For Handler 25, skips FTCH to preserve overlays.
* For other handlers, re-decodes stored FOBJ with current offsets.
*/
-void SmushPlayer::ra2HandleFetch(Common::SeekableReadStream &b) {
+bool SmushPlayerRebel2::handleGameFetch(int32 subSize, Common::SeekableReadStream &b) {
int16 ftchUnknown = b.readSint16LE();
int16 ftchX = b.readSint16LE();
int16 ftchY = b.readSint16LE();
@@ -186,7 +196,7 @@ void SmushPlayer::ra2HandleFetch(Common::SeekableReadStream &b) {
int handler = rebel2->getHandler();
if (handler == 25) {
debug("SmushPlayer::handleFetch: Skipping FTCH for Handler 25 - preserving overlays");
- return;
+ return true;
}
}
@@ -202,8 +212,77 @@ void SmushPlayer::ra2HandleFetch(Common::SeekableReadStream &b) {
} else {
debug("SmushPlayer FTCH: No stored FOBJ data! (frame=%d)", _frame);
}
+
+ return true;
+}
+
+/**
+ * RA2-specific text rendering using SmushMultiFont for inline font switching.
+ */
+bool SmushPlayerRebel2::handleGameTextRendering(const char *str, int fontId, int color,
+ int pos_x, int pos_y, int left, int top,
+ int width, int height, TextStyleFlags flg) {
+ ra2HandleTextResource(str, fontId, color, pos_x, pos_y, left, top, width, height, flg);
+ return true;
+}
+
+SmushFont *SmushPlayerRebel2::getGameFont(int font) {
+ // Font table for low-res mode (320x200). Original exe uses pointer
+ // arithmetic to select hi/lo font pairs (e.g. TKHIFONT+0x14=TALKFONT).
+ // Font 0: TALKFONT (TKHIFONT hi-res)
+ // Font 1: SMALFONT (SMHIFONT hi-res)
+ // Font 2: TITLFONT (TIHIFONT hi-res)
+ // Font 3: POVFONT (POHIFONT hi-res)
+ const char *ra2_fonts[] = {
+ "SYSTM/TALKFONT.NUT",
+ "SYSTM/SMALFONT.NUT",
+ "SYSTM/TITLFONT.NUT",
+ "SYSTM/POVFONT.NUT"
+ };
+ int numFonts = ARRAYSIZE(ra2_fonts);
+ if (font >= 0 && font < numFonts) {
+ _sf[font] = new SmushFont(_vm, ra2_fonts[font], true);
+ } else {
+ debugC(DEBUG_SMUSH, "SmushPlayer::getFont: RA2 unknown font %d, using TALKFONT", font);
+ _sf[font] = new SmushFont(_vm, ra2_fonts[0], true);
+ }
+ return _sf[font];
}
+/**
+ * Reset XPAL delta palette from the current base palette.
+ * Prevents stale delta values from a previous video corrupting the palette.
+ */
+void SmushPlayerRebel2::adjustGamePalette() {
+ for (int j = 0; j < 768; ++j) {
+ _shiftedDeltaPal[j] = _pal[j] << 7;
+ }
+ memset(_deltaPal, 0, sizeof(_deltaPal));
+}
+
+/**
+ * RA2-specific handleAnimHeader fixup: when AHDR reports 0x0 dimensions,
+ * use screen dimensions instead.
+ */
+bool SmushPlayerRebel2::handleGameAnimHeader(byte *headerContent) {
+ int width = READ_LE_UINT16(&headerContent[4]);
+ int height = READ_LE_UINT16(&headerContent[6]);
+
+ if (width == 0 && height == 0) {
+ _width = _vm->_screenWidth;
+ _height = _vm->_screenHeight;
+ debug("SmushPlayer::handleAnimHeader: RA2 AHDR has 0x0 dims - using screen size %dx%d", _width, _height);
+ } else {
+ _width = width;
+ _height = height;
+ }
+ return true;
+}
+
+// ---------------------------------------------------------------------------
+// RA2 helper methods (still on SmushPlayer for now, used by base class)
+// ---------------------------------------------------------------------------
+
/**
* Handle LOAD chunk for Rebel Assault 2.
*
@@ -492,60 +571,84 @@ void SmushPlayer::ra2HandleGost(int32 subSize, Common::SeekableReadStream &b) {
}
/**
- * Reset XPAL delta palette from the current base palette.
- * Prevents stale delta values from a previous video corrupting the palette.
+ * RA2 per-frame audio processing.
*/
-void SmushPlayer::ra2ResetDeltaPalette() {
- for (int j = 0; j < 768; ++j) {
- _shiftedDeltaPal[j] = _pal[j] << 7;
+void SmushPlayerRebel2::handleGameParseNextFrame() {
+ // Call processDispatches directly since RA2 has no iMUSE
+ // 11025 Hz / 12 fps = ~918 samples per frame
+ processDispatches(_smushAudioSampleRate / 12);
+}
+
+// ---------------------------------------------------------------------------
+// Frame decode pipeline overrides
+// ---------------------------------------------------------------------------
+
+bool SmushPlayerRebel2::handleGameFrameBufferSelect(int codec, int width, int height) {
+ if ((height != _vm->_screenHeight) || (width != _vm->_screenWidth)) {
+ ra2SelectFrameBuffer(width, height);
+ return true;
}
- memset(_deltaPal, 0, sizeof(_deltaPal));
+ return false;
}
-/**
- * RA2 font path table.
- */
-SmushFont *SmushPlayer::ra2GetFont(int font) {
- // Font table for low-res mode (320x200). Original exe uses pointer
- // arithmetic to select hi/lo font pairs (e.g. TKHIFONT+0x14=TALKFONT).
- // Font 0: TALKFONT (TKHIFONT hi-res)
- // Font 1: SMALFONT (SMHIFONT hi-res)
- // Font 2: TITLFONT (TIHIFONT hi-res)
- // Font 3: POVFONT (POHIFONT hi-res)
- const char *ra2_fonts[] = {
- "SYSTM/TALKFONT.NUT",
- "SYSTM/SMALFONT.NUT",
- "SYSTM/TITLFONT.NUT",
- "SYSTM/POVFONT.NUT"
- };
- int numFonts = ARRAYSIZE(ra2_fonts);
- if (font >= 0 && font < numFonts) {
- _sf[font] = new SmushFont(_vm, ra2_fonts[font], true);
- } else {
- debugC(DEBUG_SMUSH, "SmushPlayer::getFont: RA2 unknown font %d, using TALKFONT", font);
- _sf[font] = new SmushFont(_vm, ra2_fonts[0], true);
+bool SmushPlayerRebel2::handleGameDimensionOverride(int codec, int width, int height) {
+ if ((height != _vm->_screenHeight) || (width != _vm->_screenWidth)) {
+ // RA2: preserve _width/_height set during buffer allocation
+ return true;
}
- return _sf[font];
+ return false;
}
-/**
- * RA2 per-frame audio processing (called from parseNextFrame).
- */
-void SmushPlayer::ra2ParseNextFrame() {
- // Call processDispatches directly since RA2 has no iMUSE
- // 11025 Hz / 12 fps = ~918 samples per frame
- processDispatches(_smushAudioSampleRate / 12);
+bool SmushPlayerRebel2::handleGameAdjustCoords(int codec, int &left, int &top, int &width, int &height, int pitch, int *srcSkipY) {
+ ra2AdjustFrameCoords(left, top, width, height, pitch, srcSkipY);
+ return true;
}
-/**
- * RA2-specific handleAnimHeader fixup: when AHDR reports 0x0 dimensions,
- * use screen dimensions instead.
- */
-void SmushPlayer::ra2FixupAnimHeader() {
- if (_width == 0 && _height == 0) {
- _width = _vm->_screenWidth; // 320
- _height = _vm->_screenHeight; // 200
- debug("SmushPlayer::handleAnimHeader: RA2 AHDR has 0x0 dims - using screen size %dx%d", _width, _height);
+bool SmushPlayerRebel2::handleGameCodecDecode(int codec, const uint8 *src, int left, int top, int width, int height, int pitch, int dataSize) {
+ // Handle RA2-specific codecs (21, 23, 44, 45); return false for standard
+ // codecs (RLE, uncompressed, codec 37/47) so the base class decodes them.
+ return ra2DecodeCodec(codec, src, left, top, width, height, pitch, dataSize);
+}
+
+bool SmushPlayerRebel2::handleGameStoreFrame() {
+ // RA2 handles STOR via ra2StoreFobjData in handleGameFrameObjectPost
+ return true;
+}
+
+void SmushPlayerRebel2::handleGameFrameObjectPre(int codec, int left, int top, int width, int height, int dataSize) {
+ debug("SmushPlayer FOBJ: frame=%d codec=%d pos=(%d,%d) size=%dx%d dataSize=%d storeFrame=%d _width=%d _height=%d",
+ _frame, codec, left, top, width, height, dataSize, _storeFrame, _width, _height);
+}
+
+void SmushPlayerRebel2::handleGameFrameObjectPost(int codec, const byte *data, int32 dataSize, int left, int top, int width, int height) {
+ ra2RememberLastFobj(codec, data, dataSize, left, top, width, height);
+
+ if (_storeFrame) {
+ ra2StoreFobjData(codec, data, dataSize, left, top, width, height);
+ }
+}
+
+void SmushPlayerRebel2::handleGameFrameStart() {
+ _hasFrameFobjForGost = false;
+}
+
+bool SmushPlayerRebel2::handleGameSkipChunk(uint32 subType, int32 subSize, Common::SeekableReadStream &b) {
+ if (_skipNext) {
+ _skipNext = false;
+ debugC(DEBUG_SMUSH, "SmushPlayer::handleFrame: SKIP consumed chunk %s frame=%d", tag2str(subType), _frame);
+ return true;
+ }
+ return false;
+}
+
+void SmushPlayerRebel2::handleGameGost(int32 subSize, Common::SeekableReadStream &b) {
+ ra2HandleGost(subSize, b);
+}
+
+void SmushPlayerRebel2::handleGameProcessAudio(int16 feedSize) {
+ if (_insane) {
+ InsaneRebel2 *rebel2 = static_cast<InsaneRebel2 *>(_insane);
+ rebel2->processAudioFrame(feedSize);
}
}
Commit: 90cab45daee2a235ab9be9f5e3bc52707b017755
https://github.com/scummvm/scummvm/commit/90cab45daee2a235ab9be9f5e3bc52707b017755
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:47+02:00
Commit Message:
SCUMM: RA: Move RA1 SMUSH frame handling into RA player
Changed paths:
engines/scumm/insane/insane_rebel1.h
engines/scumm/insane/insane_rebel1_render.cpp
engines/scumm/smush/smush_player.cpp
engines/scumm/smush/smush_player.h
engines/scumm/smush/smush_player_ra1.cpp
engines/scumm/smush/smush_player_ra2.cpp
diff --git a/engines/scumm/insane/insane_rebel1.h b/engines/scumm/insane/insane_rebel1.h
index 1ef1e542835..db6f84a3890 100644
--- a/engines/scumm/insane/insane_rebel1.h
+++ b/engines/scumm/insane/insane_rebel1.h
@@ -67,11 +67,15 @@ static const int16 kRA1FocalY = 25;
* Star Wars: Rebel Assault (RA1) game logic.
* Adapts RA2 Handler 7 (ship flight) physics for RA1's 384x242 resolution.
*/
+class SmushPlayerRebel1;
+
class InsaneRebel1 : public Insane, public Common::EventObserver {
public:
InsaneRebel1(ScummEngine_v7 *scumm);
~InsaneRebel1() override;
+ SmushPlayerRebel1 *ra1Player() const { return static_cast<SmushPlayerRebel1 *>(_player); }
+
bool notifyEvent(const Common::Event &event) override;
void procPreRendering(byte *renderBitmap) override;
diff --git a/engines/scumm/insane/insane_rebel1_render.cpp b/engines/scumm/insane/insane_rebel1_render.cpp
index f8e6d60f9d2..93fdc31dc07 100644
--- a/engines/scumm/insane/insane_rebel1_render.cpp
+++ b/engines/scumm/insane/insane_rebel1_render.cpp
@@ -375,15 +375,15 @@ void InsaneRebel1::procPreRendering(byte *renderBitmap) {
// 320x200 window inside the 384x242 buffer. Interactive movies with no
// GAME stream (for example LVL4/L4PLAY2.ANM) keep a static camera.
if (usePerspectiveViewport) {
- _player->_ra1ViewportOffsetX = _perspectiveX;
- _player->_ra1ViewportOffsetY = _perspectiveY;
+ ra1Player()->_ra1ViewportOffsetX = _perspectiveX;
+ ra1Player()->_ra1ViewportOffsetY = _perspectiveY;
} else {
- _player->_ra1ViewportOffsetX = 0;
- _player->_ra1ViewportOffsetY = 0;
+ ra1Player()->_ra1ViewportOffsetX = 0;
+ ra1Player()->_ra1ViewportOffsetY = 0;
}
} else if (_player) {
- _player->_ra1ViewportOffsetX = 0;
- _player->_ra1ViewportOffsetY = 0;
+ ra1Player()->_ra1ViewportOffsetX = 0;
+ ra1Player()->_ra1ViewportOffsetY = 0;
}
}
@@ -491,18 +491,18 @@ void InsaneRebel1::procPostRendering(byte *renderBitmap, int32 codecparam, int32
// On-foot mode uses SetCameraOffset(0,0) â no viewport crop.
if (_player) {
if (onFootMode || (!asteroidMode && !turretMode && !flightMode)) {
- _player->_ra1ViewportOffsetX = 0;
- _player->_ra1ViewportOffsetY = 0;
+ ra1Player()->_ra1ViewportOffsetX = 0;
+ ra1Player()->_ra1ViewportOffsetY = 0;
} else {
- _player->_ra1ViewportOffsetX = _perspectiveX;
- _player->_ra1ViewportOffsetY = _perspectiveY;
+ ra1Player()->_ra1ViewportOffsetX = _perspectiveX;
+ ra1Player()->_ra1ViewportOffsetY = _perspectiveY;
}
// Screen shake â SetCameraOffset (0x224FD): random [-2,+2] jitter when
// _screenFlash > 0. Original uses RandScaleByte(5) - 2 for each axis.
if (_screenFlash > 0) {
- _player->_ra1ViewportOffsetX += (int16)(_vm->_rnd.getRandomNumber(4) - 2);
- _player->_ra1ViewportOffsetY += (int16)(_vm->_rnd.getRandomNumber(4) - 2);
+ ra1Player()->_ra1ViewportOffsetX += (int16)(_vm->_rnd.getRandomNumber(4) - 2);
+ ra1Player()->_ra1ViewportOffsetY += (int16)(_vm->_rnd.getRandomNumber(4) - 2);
}
}
@@ -1228,8 +1228,8 @@ void InsaneRebel1::renderHUD(byte *dst, int pitch, int width, int height) {
int hudOriginX = 0;
int hudOriginY = 0;
if (_interactiveVideoActive && _player) {
- hudOriginX = _player->_ra1ViewportOffsetX;
- hudOriginY = _player->_ra1ViewportOffsetY;
+ hudOriginX = ra1Player()->_ra1ViewportOffsetX;
+ hudOriginY = ra1Player()->_ra1ViewportOffsetY;
}
int hudX = hudOriginX + bar.xoffs;
@@ -1489,8 +1489,8 @@ void InsaneRebel1::renderLevel8Overlay(byte *dst, int pitch, int width, int heig
return;
// Viewport offset: screen-space â buffer-space (same approach as renderHUD)
- int viewX = _player ? _player->_ra1ViewportOffsetX : _perspectiveX;
- int viewY = _player ? _player->_ra1ViewportOffsetY : _perspectiveY;
+ int viewX = _player ? ra1Player()->_ra1ViewportOffsetX : _perspectiveX;
+ int viewY = _player ? ra1Player()->_ra1ViewportOffsetY : _perspectiveY;
// Walker health display â "<<WALKER %d%%" at projected (0x61, 0x8D)
// Blinks when health < 16: only drawn when (frameCounter & 2) != 0
diff --git a/engines/scumm/smush/smush_player.cpp b/engines/scumm/smush/smush_player.cpp
index 474324591c1..0a010f80aa3 100644
--- a/engines/scumm/smush/smush_player.cpp
+++ b/engines/scumm/smush/smush_player.cpp
@@ -40,6 +40,7 @@
#include "scumm/smush/codec37.h"
#include "scumm/smush/codec47.h"
#include "scumm/smush/smush_font.h"
+#include "scumm/smush/smush_multi_font.h"
#include "scumm/smush/smush_player.h"
#include "scumm/insane/insane.h"
@@ -272,9 +273,26 @@ SmushPlayer::SmushPlayer(ScummEngine_v7 *scumm, IMuseDigital *imuseDigital, Insa
_frameBuffer = nullptr;
_specialBuffer = nullptr;
_specialBufferSize = 0;
- _ra1CleanFrame = nullptr;
- _ra1CleanFrameSize = 0;
- _ra1HasCleanFrame = false;
+ _storedFobjData = nullptr;
+ _storedFobjDataSize = 0;
+ _storedFobjCodec = 0;
+ _storedFobjParm2 = 0;
+ _storedFobjLeft = 0;
+ _storedFobjTop = 0;
+ _storedFobjWidth = 0;
+ _storedFobjHeight = 0;
+ _lastFobjData = nullptr;
+ _lastFobjDataSize = 0;
+ _lastFobjCodec = 0;
+ _lastFobjLeft = 0;
+ _lastFobjTop = 0;
+ _lastFobjWidth = 0;
+ _lastFobjHeight = 0;
+ _hasFrameFobjForGost = false;
+ _fobjOffsetX = 0;
+ _fobjOffsetY = 0;
+ _skipNext = false;
+ _storeFrame = false;
_seekPos = -1;
@@ -332,10 +350,12 @@ SmushPlayer::~SmushPlayer() {
_frameBuffer = nullptr;
free(_specialBuffer);
_specialBuffer = nullptr;
- free(_ra1CleanFrame);
- _ra1CleanFrame = nullptr;
- _ra1CleanFrameSize = 0;
- _ra1HasCleanFrame = false;
+ free(_storedFobjData);
+ _storedFobjData = nullptr;
+ free(_lastFobjData);
+ _lastFobjData = nullptr;
+ delete _multiFont;
+ _multiFont = nullptr;
}
void SmushPlayer::init(int32 speed) {
@@ -354,9 +374,6 @@ void SmushPlayer::init(int32 speed) {
const bool preserveVideoState = _preserveVideoStateOnNextPlay;
_preserveVideoStateOnNextPlay = false;
if (!preserveVideoState) {
- _ra1HasCleanFrame = false;
- // RA1 OBJ overlay chunks are video-local. Reset cached overlay state for each
- // new ANM unless the original route-switch path requested state preservation.
resetGameVideoState();
}
@@ -408,10 +425,6 @@ void SmushPlayer::release() {
free(_specialBuffer);
_specialBuffer = nullptr;
- free(_ra1CleanFrame);
- _ra1CleanFrame = nullptr;
- _ra1CleanFrameSize = 0;
- _ra1HasCleanFrame = false;
releaseGameVideoState();
if (!shouldPreserveFrameBuffer()) {
@@ -1298,22 +1311,13 @@ void SmushPlayer::handleFrameObject(int32 subSize, Common::SeekableReadStream &b
}
int codec = b.readUint16LE();
- uint8 ra1Param = 0; // RA1: palette base byte (FOBJ byte[1])
- uint16 ra1ObjectId = 0; // RA1: object id / event target id (FOBJ bytes[10-11])
- uint16 ra1Parm2 = 0; // RA1: tile count (FOBJ bytes[12-13])
- if (isRA1()) {
- ra1Param = (codec >> 8) & 0xFF; // byte[1] = palette base (e.g. 0xF0)
- codec &= 0xFF; // byte[0] = actual codec number
- }
int left = (int)b.readSint16LE();
int top = (int)b.readSint16LE();
- const int rawLeft = left;
- const int rawTop = top;
int width = b.readUint16LE();
int height = b.readUint16LE();
- ra1ObjectId = b.readUint16LE();
- ra1Parm2 = b.readUint16LE();
+ b.readUint16LE(); // objectId (RA1 uses this)
+ b.readUint16LE(); // parm2 (RA1 uses this)
debugC(DEBUG_SMUSH, "SmushPlayer::handleFrameObject: frame=%d codec=%d pos=(%d,%d) size=%dx%d dataSize=%d",
_frame, codec, left, top, width, height, subSize - 14);
@@ -1327,149 +1331,22 @@ void SmushPlayer::handleFrameObject(int32 subSize, Common::SeekableReadStream &b
handleGameFrameObjectPost(codec, chunk_buffer, chunk_size, left, top, width, height);
- // RA1 STOR needs ra1Param/rawLeft/rawTop which aren't available through the virtual hook
- if (_storeFrame && isRA1()) {
- free(_storedFobjData);
- _storedFobjData = (byte *)malloc(chunk_size);
- if (_storedFobjData != nullptr) {
- memcpy(_storedFobjData, chunk_buffer, chunk_size);
- _storedFobjDataSize = chunk_size;
- _storedFobjCodec = codec | ((int)ra1Param << 8);
- _storedFobjParm2 = ra1Parm2;
- _storedFobjLeft = rawLeft;
- _storedFobjTop = rawTop;
- _storedFobjWidth = width;
- _storedFobjHeight = height;
- } else {
- _storedFobjDataSize = 0;
- }
- _storeFrame = false;
- }
-
- if (isRA1() && _insane) {
- InsaneRebel1 *rebel1 = static_cast<InsaneRebel1 *>(_insane);
- if (!rebel1->handleFrameObjectTarget((int16)ra1ObjectId, (int16)rawLeft, (int16)rawTop,
- (int16)width, (int16)height, codec, ra1Param)) {
- free(chunk_buffer);
- return;
- }
- }
-
- decodeFrameObject(codec, chunk_buffer, left, top, width, height, chunk_size, ra1Param, ra1Parm2);
+ decodeFrameObject(codec, chunk_buffer, left, top, width, height, chunk_size);
free(chunk_buffer);
}
-static bool ra1FrameHasGameChunk(Common::SeekableReadStream &b, int32 frameSize) {
- const int64 frameStart = b.pos();
- int32 remaining = frameSize;
-
- while (remaining > 1) {
- if ((b.pos() & 1) && remaining > 0) {
- const byte pad = b.readByte();
- if (pad == 0) {
- remaining--;
- } else {
- b.seek(-1, SEEK_CUR);
- }
- }
-
- if (remaining < 8)
- break;
-
- const uint32 subType = b.readUint32BE();
- const int32 subSize = b.readUint32BE();
- const int64 subDataPos = b.pos();
-
- if (subType == MKTAG('F', 'R', 'M', 'E'))
- break;
- if (subType == MKTAG('G', 'A', 'M', 'E')) {
- b.seek(frameStart, SEEK_SET);
- return true;
- }
-
- remaining -= subSize + 8;
- b.seek(subDataPos + subSize, SEEK_SET);
- }
-
- b.seek(frameStart, SEEK_SET);
- return false;
-}
-
void SmushPlayer::handleFrame(int32 frameSize, Common::SeekableReadStream &b) {
debugC(DEBUG_SMUSH, "SmushPlayer::handleFrame(%d)", _frame);
uint8 *audioChunk = nullptr;
_skipNext = false;
handleGameFrameStart();
- bool interactiveRA1 = false;
- bool forceInteractiveClearRA1 = false;
- bool preserveFrameHistoryRA1 = false;
- if (isRA1() && _insane) {
- InsaneRebel1 *rebel1 = static_cast<InsaneRebel1 *>(_insane);
- interactiveRA1 = rebel1->isInteractiveVideoActive();
- const uint16 activeOpcode = rebel1->getActiveGameOpcode();
- // Opcode 0x0B path (FUN_1CDA7) uses heavy partial-layer composition
- // (codec1/2 + FTCH). Force clear there to avoid stale-trail ghosting.
- // Keep a conservative fallback for the early L2 frames before first 0x0B
- // arrives.
- forceInteractiveClearRA1 = interactiveRA1 &&
- (activeOpcode == 0x0B ||
- (activeOpcode == 0 && rebel1->getCurrentLevel() == 1));
-
- // The clean-frame cache only exists to strip gameplay overlays back out of
- // the previous decoded frame before the next gameplay frame is composed.
- // Transitional interactive clips like LVL4/L4PLAY2 have no GAME chunks in
- // their FRME stream, so restoring the prior clean frame there just smears
- // stale gameplay pixels into the cutscene.
- preserveFrameHistoryRA1 = interactiveRA1 &&
- !forceInteractiveClearRA1 &&
- ra1FrameHasGameChunk(b, frameSize);
- }
-
- // Keep the previous decoded frame (without post-render overlays) as delta source.
- // FUN_1FDBC (0x1FDBC) decodes frame data first; gameplay overlays from
- // FUN_1BBCB/FUN_1CB22/FUN_1CDA7 are presentation-stage effects.
- if (isRA1() && preserveFrameHistoryRA1 &&
- _ra1HasCleanFrame && _ra1CleanFrame &&
- _dst && _width > 0 && _height > 0) {
- const int frameBytes = _width * _height;
- if (_ra1CleanFrameSize >= frameBytes)
- memcpy(_dst, _ra1CleanFrame, frameBytes);
- }
-
if (_insanity) {
_vm->_insane->procPreRendering(_dst);
}
- // RA1: gameplay/interactivity relies on previous-frame history (delta codecs),
- // but passive cinematics in current implementation need a per-frame clear to
- // avoid trails in intro/text sequences.
- if (isRA1() && _dst && _width > 0 && _height > 0) {
- if (!preserveFrameHistoryRA1)
- memset(_dst, 0, _width * _height);
- }
-
while (frameSize > 0) {
- // RA1 parser exits when <=1 byte remains in a frame (FUN_1FDBC).
- // Treat any tiny tail as frame trailer/padding and stop cleanly.
- if (isRA1() && frameSize <= 1) {
- if (frameSize == 1)
- b.skip(1);
- break;
- }
-
- // RA1: Top-of-loop alignment check matching original assembly FUN_1FDBC:
- // if ((ptr & 1) && (*ptr == 0)) { ptr++; remaining--; }
- if (isRA1() && (b.pos() & 1) && frameSize > 0) {
- byte peek = b.readByte();
- if (peek == 0) {
- frameSize--;
- } else {
- b.seek(-1, SEEK_CUR);
- }
- }
-
if (frameSize < 8) {
b.skip(frameSize);
break;
@@ -1479,12 +1356,6 @@ void SmushPlayer::handleFrame(int32 frameSize, Common::SeekableReadStream &b) {
int32 subSize = b.readUint32BE();
int32 subOffset = b.pos();
- // Guard against consuming the next frame marker as an in-frame chunk.
- if (isRA1() && subType == MKTAG('F','R','M','E')) {
- b.seek(-8, SEEK_CUR);
- break;
- }
-
if (handleGameSkipChunk(subType, subSize, b)) {
frameSize -= subSize + 8;
b.seek(subOffset + subSize, SEEK_SET);
@@ -1514,7 +1385,6 @@ void SmushPlayer::handleFrame(int32 frameSize, Common::SeekableReadStream &b) {
free(audioChunk);
audioChunk = nullptr;
}
-
break;
case MKTAG('T','R','E','S'):
handleTextResource(subType, subSize, b);
@@ -1538,159 +1408,28 @@ void SmushPlayer::handleFrame(int32 frameSize, Common::SeekableReadStream &b) {
handleTextResource(subType, subSize, b);
break;
case MKTAG('L','O','A','D'):
- handleLoad(subSize, b);
+ handleGameLoad(subSize, b);
break;
case MKTAG('G','O','S','T'):
handleGameGost(subSize, b);
break;
- // RA1-specific chunk types: skip gracefully
- case MKTAG('G','A','M','E'):
- if (isRA1()) {
- InsaneRebel1 *rebel1 = (InsaneRebel1 *)_vm->_insane;
- rebel1->handleGameChunk(subSize, b);
- }
- break;
- case MKTAG('P','V','O','C'):
- // RA1 voice-over audio: same 12-byte header format as PSAD
- // (3 Ã BE32: trackId, seqNum, param) followed by SAUD data.
- // Feed to audio system identically to PSAD.
- if (!_compressedFileMode && !isFastForwardingCurrentFrame()) {
- audioChunk = (uint8 *)malloc(subSize + 8);
- b.seek(-8, SEEK_CUR);
- b.read(audioChunk, subSize + 8);
- feedAudio(audioChunk, 0, 127, 0, 0);
- free(audioChunk);
- audioChunk = nullptr;
- }
- break;
- case MKTAG('G','A','M','2'):
- case MKTAG('F','A','D','E'):
- case MKTAG('S','E','G','A'):
- case MKTAG('A','D','L',' '):
- case MKTAG('A','D','L','2'):
- case MKTAG('S','B','L',' '):
- case MKTAG('S','B','L','2'):
- case MKTAG('P','S','D','2'):
- debugC(DEBUG_SMUSH, "SmushPlayer::handleFrame: skipping RA1 chunk %s (%d bytes)", tag2str(subType), subSize);
- break;
- case MKTAG('O','B','J','\0'):
- // RA1 object overlay chunk: variable-size header + embedded FOBJ
- // sprites (including the cockpit overlay), GAME, and PSAD chunks.
- // The reported size field is unreliable â remaining FRME data after
- // OBJ\0 contains unstructured data between the reported end and
- // subsequent sub-chunks. Read ALL remaining FRME data and scan for
- // embedded sub-chunks, then stop frame parsing.
- if (isRA1()) {
- int32 objDataSize = frameSize - 8;
- if (objDataSize > 0) {
- byte *objBuf = (byte *)malloc(objDataSize);
- b.read(objBuf, objDataSize);
-
- int32 objPos = 0;
- while (objPos + 8 < objDataSize) {
- uint32 embTag = READ_BE_UINT32(objBuf + objPos);
- uint32 embSize = READ_BE_UINT32(objBuf + objPos + 4);
- int32 embRemaining = objDataSize - objPos - 8;
-
- bool recognized = (embTag == MKTAG('F','O','B','J') ||
- embTag == MKTAG('G','A','M','E') ||
- embTag == MKTAG('P','S','A','D'));
-
- if (!recognized || embSize > (uint32)embRemaining) {
- // Not a recognized tag or size exceeds remaining data.
- // Advance byte-by-byte through the OBJ header.
- objPos++;
- continue;
- }
-
- if (embTag == MKTAG('F','O','B','J') && embSize >= 14) {
- Common::MemoryReadStream embStream(objBuf + objPos + 8, embSize);
- handleFrameObject(embSize, embStream);
-
- // Save the largest OBJ embedded FOBJ as the cockpit overlay
- // to re-render every subsequent frame.
- if (_ra1ObjOverlayData == nullptr ||
- (int32)embSize > _ra1ObjOverlayDataSize) {
- free(_ra1ObjOverlayData);
- _ra1ObjOverlayDataSize = embSize;
- _ra1ObjOverlayData = (byte *)malloc(embSize);
- memcpy(_ra1ObjOverlayData, objBuf + objPos + 8, embSize);
- // Parse FOBJ header for codec/position/size
- _ra1ObjOverlayCodec = objBuf[objPos + 8] & 0xFF;
- _ra1ObjOverlayLeft = (int16)READ_LE_UINT16(objBuf + objPos + 10);
- _ra1ObjOverlayTop = (int16)READ_LE_UINT16(objBuf + objPos + 12);
- _ra1ObjOverlayWidth = READ_LE_UINT16(objBuf + objPos + 14);
- _ra1ObjOverlayHeight = READ_LE_UINT16(objBuf + objPos + 16);
- }
- } else if (embTag == MKTAG('G','A','M','E')) {
- Common::MemoryReadStream embStream(objBuf + objPos + 8, embSize);
- InsaneRebel1 *rebel1 = (InsaneRebel1 *)_vm->_insane;
- rebel1->handleGameChunk(embSize, embStream);
- } else if (embTag == MKTAG('P','S','A','D')) {
- if (!_compressedFileMode && !isFastForwardingCurrentFrame()) {
- uint8 *audioBuf = (uint8 *)malloc(embSize + 8);
- memcpy(audioBuf, objBuf + objPos, embSize + 8);
- feedAudio(audioBuf, 0, 127, 0, 0);
- free(audioBuf);
- }
- }
-
- objPos += 8 + embSize;
- if (embSize & 1)
- objPos++;
- }
- free(objBuf);
- }
- frameSize = 0;
- continue;
- }
- break;
default:
- if (isRA1()) {
- // Original FUN_1FDBC lines 163-168: unknown tag with all uppercase
- // letters (A-Z) â silently return. Otherwise error.
- byte tb0 = (subType >> 24) & 0xFF, tb1 = (subType >> 16) & 0xFF;
- byte tb2 = (subType >> 8) & 0xFF, tb3 = subType & 0xFF;
- if (tb0 > 0x40 && tb0 < 0x5B && tb1 > 0x40 && tb1 < 0x5B &&
- tb2 > 0x40 && tb2 < 0x5B && tb3 > 0x40 && tb3 < 0x5B) {
- debug(5, "RA1: unknown uppercase tag %s at frame %d, stopping frame parse", tag2str(subType), _frame);
- frameSize = 0;
- continue;
- }
+ if (isInsaneGame()) {
+ debugC(DEBUG_SMUSH, "SmushPlayer::handleFrame: skipping unknown chunk %s (%d bytes) frame=%d",
+ tag2str(subType), subSize, _frame);
+ } else {
+ error("Unknown frame subChunk found : %s, %d", tag2str(subType), subSize);
}
- error("Unknown frame subChunk found : %s, %d", tag2str(subType), subSize);
}
frameSize -= subSize + 8;
b.seek(subOffset + subSize, SEEK_SET);
- // RA1 uses top-of-loop alignment (matching FUN_1FDBC), not bottom-of-loop padding.
- if (!isRA1() && (subSize & 1)) {
+ if (subSize & 1) {
b.skip(1);
frameSize--;
}
}
- if (isRA1() && _ra1ObjOverlayData != nullptr && _frame > 0) {
- Common::MemoryReadStream overlayStream(_ra1ObjOverlayData, _ra1ObjOverlayDataSize);
- handleFrameObject(_ra1ObjOverlayDataSize, overlayStream);
- }
-
- if (isRA1() && preserveFrameHistoryRA1 &&
- _dst && _width > 0 && _height > 0) {
- const int frameBytes = _width * _height;
- byte *newClean = (byte *)realloc(_ra1CleanFrame, frameBytes);
- if (newClean != nullptr) {
- _ra1CleanFrame = newClean;
- _ra1CleanFrameSize = frameBytes;
- memcpy(_ra1CleanFrame, _dst, frameBytes);
- _ra1HasCleanFrame = true;
- } else {
- _ra1HasCleanFrame = false;
- }
- } else if (isRA1()) {
- _ra1HasCleanFrame = false;
- }
-
if (_insanity) {
_vm->_insane->procPostRendering(_dst, 0, 0, 0, _frame, _nbframes-1);
}
@@ -2003,6 +1742,14 @@ void SmushPlayer::updateScreen() {
debugC(DEBUG_SMUSH, "Smush stats: updateScreen( %03d )", end_time - start_time);
}
+void SmushPlayer::handleGameUpdateScreen(const byte *src, int srcPitch, int width, int height) {
+ if (_vm->_macScreen) {
+ _vm->mac_drawBufferToScreen(src, srcPitch, 0, 0, width, height);
+ } else {
+ _vm->_system->copyRectToScreen(src, srcPitch, 0, 0, width, height);
+ }
+}
+
void SmushPlayer::insanity(bool flag) {
_insanity = flag;
}
@@ -2223,38 +1970,11 @@ void SmushPlayer::play(const char *filename, int32 speed, int32 offset, int32 st
int frameHeight;
const byte *dst;
- if (isRA1()) {
- if (_dst == nullptr || _width <= 0 || _height <= 0) {
- _updateNeeded = false;
- continue;
- }
-
- int ra1ViewX = _ra1ViewportOffsetX;
- int ra1ViewY = _ra1ViewportOffsetY;
-
- const int srcX = CLIP(_scrollX + ra1ViewX, 0, _width - 1);
- const int srcY = CLIP(_scrollY + ra1ViewY, 0, _height - 1);
-
- frameWidth = MIN(_width - srcX, _vm->_screenWidth);
- frameHeight = MIN(_height - srcY, _vm->_screenHeight);
- if (frameWidth <= 0 || frameHeight <= 0) {
- _updateNeeded = false;
- continue;
- }
+ frameWidth = MIN(_width, _vm->_screenWidth);
+ frameHeight = MIN(_height, _vm->_screenHeight);
+ dst = _dst + _scrollY * _width + _scrollX;
- dst = _dst + srcY * _width + srcX;
- } else {
- frameWidth = MIN(_width, _vm->_screenWidth);
- frameHeight = MIN(_height, _vm->_screenHeight);
- dst = _dst + _scrollY * _width + _scrollX;
- }
-
- if (_vm->_macScreen) {
- int srcPitch = isRA1() ? _width : frameWidth;
- _vm->mac_drawBufferToScreen(dst, srcPitch, 0, 0, frameWidth, frameHeight);
- } else {
- _vm->_system->copyRectToScreen(dst, _width, 0, 0, frameWidth, frameHeight);
- }
+ handleGameUpdateScreen(dst, _width, frameWidth, frameHeight);
_vm->_system->updateScreen();
_updateNeeded = false;
diff --git a/engines/scumm/smush/smush_player.h b/engines/scumm/smush/smush_player.h
index 8ae998f8a7c..ce56b23bbc5 100644
--- a/engines/scumm/smush/smush_player.h
+++ b/engines/scumm/smush/smush_player.h
@@ -160,39 +160,17 @@ private:
byte *_frameBuffer;
byte *_specialBuffer;
int _specialBufferSize;
- byte *_ra1CleanFrame;
- int32 _ra1CleanFrameSize;
- bool _ra1HasCleanFrame;
- // RA2: Raw FOBJ data stored by STOR chunk (matching original DAT_00482c04).
- // The original stores raw FOBJ chunk data and re-decodes it on FTCH with
- // current FOBJ offsets. This is essential for O_LEVEL.SAN where the stored
- // FOBJ is the 80x800 preview strip at X=320, and FTCH must re-render it
- // at the current scroll offset each frame.
+ // RA1/RA2: Raw FOBJ data stored by STOR chunk for later re-decoding by FTCH.
byte *_storedFobjData;
int32 _storedFobjDataSize;
int _storedFobjCodec;
- uint16 _storedFobjParm2; // RA1: FOBJ bytes[12..13] needed by codec 4/5
+ uint16 _storedFobjParm2;
int _storedFobjLeft;
int _storedFobjTop;
int _storedFobjWidth;
int _storedFobjHeight;
- // RA1: OBJ\0 embedded cockpit overlay FOBJ â drawn once in frame 0,
- // saved here and re-rendered every subsequent frame after scene FOBJs.
- byte *_ra1ObjOverlayData;
- int32 _ra1ObjOverlayDataSize;
- int _ra1ObjOverlayCodec;
- int _ra1ObjOverlayLeft;
- int _ra1ObjOverlayTop;
- int _ra1ObjOverlayWidth;
- int _ra1ObjOverlayHeight;
-
- // RA1: Viewport scroll offset for interactive gameplay (FUN_224FD at 0x224FD).
- // Set by InsaneRebel1::procPreRendering(), applied to FOBJ decode positions.
- int _ra1ViewportOffsetX;
- int _ra1ViewportOffsetY;
-
// RA1/RA2: Most recently decoded FOBJ in the current frame, used by GOST
// chunks to re-render the same sprite payload at a different position.
byte *_lastFobjData;
@@ -204,6 +182,11 @@ private:
int _lastFobjHeight;
bool _hasFrameFobjForGost;
+ // RA2: Global FOBJ position offsets.
+ // Set by InsaneRebel2 during IACT opcode 6 processing, reset in procPostRendering.
+ int16 _fobjOffsetX;
+ int16 _fobjOffsetY;
+
Common::String _seekFile;
uint32 _startFrame;
uint32 _startTime;
@@ -211,18 +194,11 @@ private:
uint32 _seekFrame;
bool _skipNext;
- bool _ra2FastForwarding; // Fast-forwarding RA2 BEG video to establish background
uint32 _frame;
- uint32 _fastForwardFromFrame; // First frame hidden by fast-forward (0 = hide from frame 0)
- uint32 _fastForwardToFrame; // RA1: skip display/audio until this frame (0 = disabled)
+ uint32 _fastForwardFromFrame;
+ uint32 _fastForwardToFrame;
bool _preserveVideoStateOnNextPlay;
- // RA2: Global FOBJ position offsets (DAT_00482c1c / DAT_00482c20 in original)
- // Set by InsaneRebel2 during IACT opcode 6 processing, reset in procPostRendering.
- // Applied to all FOBJ left/top positions during decoding.
- int16 _fobjOffsetX;
- int16 _fobjOffsetY;
-
Audio::SoundHandle *_IACTchannel;
Audio::QueuingAudioStream *_IACTstream;
@@ -343,6 +319,11 @@ protected:
virtual void handleGameGost(int32 subSize, Common::SeekableReadStream &b) {}
virtual void handleGameProcessAudio(int16 feedSize) {}
virtual bool isInsaneGame() const { return false; }
+ virtual void handleGameLoad(int32 subSize, Common::SeekableReadStream &b) {}
+ virtual void handleFrameObject(int32 subSize, Common::SeekableReadStream &b);
+ virtual void handleFrame(int32 frameSize, Common::SeekableReadStream &b);
+ virtual void handleGameUpdateScreen(const byte *
+ src, int srcPitch, int width, int height);
private:
SmushFont *getFont(int font);
@@ -355,22 +336,16 @@ private:
bool readString(const char *file);
void decodeFrameObject(int codec, const uint8 *src, int left, int top, int width, int height, int dataSize = 0, uint8 ra1Param = 0, uint16 ra1Parm2 = 0);
void handleAnimHeader(int32 subSize, Common::SeekableReadStream &);
- void handleFrame(int32 frameSize, Common::SeekableReadStream &);
void handleNewPalette(int32 subSize, Common::SeekableReadStream &);
void handleZlibFrameObject(int32 subSize, Common::SeekableReadStream &b);
- void handleFrameObject(int32 subSize, Common::SeekableReadStream &);
void handleSAUDChunk(uint8 *srcBuf, uint32 size, int groupId, int vol, int pan, int16 flags, int trkId, int index, int maxFrames);
void handleStore(int32 subSize, Common::SeekableReadStream &);
void handleFetch(int32 subSize, Common::SeekableReadStream &);
void handleIACT(int32 subSize, Common::SeekableReadStream &);
void handleTextResource(uint32 subType, int32 subSize, Common::SeekableReadStream &);
void handleDeltaPalette(int32 subSize, Common::SeekableReadStream &);
- void handleLoad(int32 subSize, Common::SeekableReadStream &); // RA2 only (impl in smush_player_ra2.cpp)
void readPalette(byte *, Common::SeekableReadStream &);
- // RA1/RA2 identification (isRA1 still used in handleFrameObject/handleFrame RA1 paths)
- bool isRA1() const;
- bool isRA2() const;
// RA2 helper methods called from SmushPlayerRebel2 overrides
void ra2HandleTextResource(const char *str, int fontId, int color,
int pos_x, int pos_y, int left, int top,
@@ -388,14 +363,6 @@ private:
SmushFont *ra1GetFont(int font);
void ra1HandleText(int32 subSize, Common::SeekableReadStream &b);
- // LOAD chunk streaming buffer (RA2 - embedded resource data)
- byte *_loadBuffer; // Accumulated LOAD data
- int32 _loadBufferSize; // Allocated buffer size
- int32 _loadBufferOffset; // Current write position (how much data accumulated)
- int32 _loadReadOffset; // Current read position (for streaming consumption)
- int16 _lastLoadChunkIdx; // Last processed chunk index (-1 = none)
- int16 _totalLoadChunks; // Total chunks expected in current sequence
-
void initAudio(int samplerate, int32 maxChunkSize);
void terminateAudio();
int isChanActive(int flagId);
@@ -411,6 +378,7 @@ private:
};
class SmushPlayerRebel1 : public SmushPlayer {
+ friend class InsaneRebel1;
public:
SmushPlayerRebel1(ScummEngine_v7 *scumm, IMuseDigital *imuseDigital, Insane *insane);
~SmushPlayerRebel1() override;
@@ -437,6 +405,27 @@ protected:
void handleGameGost(int32 subSize, Common::SeekableReadStream &b) override;
void handleGameProcessAudio(int16 feedSize) override;
bool isInsaneGame() const override { return true; }
+ void handleFrameObject(int32 subSize, Common::SeekableReadStream &b) override;
+ void handleFrame(int32 frameSize, Common::SeekableReadStream &b) override;
+ void handleGameUpdateScreen(const byte *src, int srcPitch, int width, int height) override;
+
+ // RA1 clean frame buffer for delta source restoration
+ byte *_ra1CleanFrame;
+ int32 _ra1CleanFrameSize;
+ bool _ra1HasCleanFrame;
+
+ // RA1 OBJ overlay FOBJ â cockpit drawn once frame 0, re-rendered every frame
+ byte *_ra1ObjOverlayData;
+ int32 _ra1ObjOverlayDataSize;
+ int _ra1ObjOverlayCodec;
+ int _ra1ObjOverlayLeft;
+ int _ra1ObjOverlayTop;
+ int _ra1ObjOverlayWidth;
+ int _ra1ObjOverlayHeight;
+
+ // RA1 viewport scroll offset for interactive gameplay
+ int _ra1ViewportOffsetX;
+ int _ra1ViewportOffsetY;
};
class SmushPlayerRebel2 : public SmushPlayer {
@@ -471,6 +460,18 @@ protected:
void handleGameGost(int32 subSize, Common::SeekableReadStream &b) override;
void handleGameProcessAudio(int16 feedSize) override;
bool isInsaneGame() const override { return true; }
+ void handleGameLoad(int32 subSize, Common::SeekableReadStream &b) override;
+
+private:
+ void handleLoad(int32 subSize, Common::SeekableReadStream &b);
+
+ // LOAD chunk streaming buffer (embedded resource data)
+ byte *_loadBuffer;
+ int32 _loadBufferSize;
+ int32 _loadBufferOffset;
+ int32 _loadReadOffset;
+ int16 _lastLoadChunkIdx;
+ int16 _totalLoadChunks;
};
} // End of namespace Scumm
diff --git a/engines/scumm/smush/smush_player_ra1.cpp b/engines/scumm/smush/smush_player_ra1.cpp
index cbd47b224a1..07f2e312f94 100644
--- a/engines/scumm/smush/smush_player_ra1.cpp
+++ b/engines/scumm/smush/smush_player_ra1.cpp
@@ -25,6 +25,7 @@
// to upstream while RA1 behavior is isolated in one place.
#include "common/endian.h"
+#include "common/memstream.h"
#include "scumm/file.h"
#include "scumm/scumm_v7.h"
@@ -61,6 +62,9 @@ SmushPlayerRebel1::~SmushPlayerRebel1() {
}
void SmushPlayerRebel1::initGamePlayerFields() {
+ _ra1CleanFrame = nullptr;
+ _ra1CleanFrameSize = 0;
+ _ra1HasCleanFrame = false;
_ra1ObjOverlayData = nullptr;
_ra1ObjOverlayDataSize = 0;
_ra1ObjOverlayCodec = 0;
@@ -76,9 +80,13 @@ void SmushPlayerRebel1::destroyGamePlayerFields() {
free(_ra1ObjOverlayData);
_ra1ObjOverlayData = nullptr;
_ra1ObjOverlayDataSize = 0;
+ free(_ra1CleanFrame);
+ _ra1CleanFrame = nullptr;
+ _ra1CleanFrameSize = 0;
}
void SmushPlayerRebel1::resetGameVideoState() {
+ _ra1HasCleanFrame = false;
free(_ra1ObjOverlayData);
_ra1ObjOverlayData = nullptr;
_ra1ObjOverlayDataSize = 0;
@@ -105,6 +113,11 @@ void SmushPlayerRebel1::releaseGameVideoState() {
free(_ra1ObjOverlayData);
_ra1ObjOverlayData = nullptr;
_ra1ObjOverlayDataSize = 0;
+
+ free(_ra1CleanFrame);
+ _ra1CleanFrame = nullptr;
+ _ra1CleanFrameSize = 0;
+ _ra1HasCleanFrame = false;
}
bool SmushPlayerRebel1::handleGameFetch(int32 subSize, Common::SeekableReadStream &b) {
@@ -348,4 +361,372 @@ void SmushPlayerRebel1::handleGameProcessAudio(int16 feedSize) {
}
}
+// ---------------------------------------------------------------------------
+// handleFrameObject override â RA1 FOBJ has extra fields in the codec word
+// ---------------------------------------------------------------------------
+
+void SmushPlayerRebel1::handleFrameObject(int32 subSize, Common::SeekableReadStream &b) {
+ assert(subSize >= 14);
+ if (_skipNext) {
+ _skipNext = false;
+ return;
+ }
+
+ int codec = b.readUint16LE();
+ uint8 ra1Param = (codec >> 8) & 0xFF;
+ codec &= 0xFF;
+
+ int left = (int)b.readSint16LE();
+ int top = (int)b.readSint16LE();
+ const int rawLeft = left;
+ const int rawTop = top;
+ int width = b.readUint16LE();
+ int height = b.readUint16LE();
+
+ uint16 ra1ObjectId = b.readUint16LE();
+ uint16 ra1Parm2 = b.readUint16LE();
+
+ handleGameFrameObjectPre(codec, left, top, width, height, subSize - 14);
+
+ int32 chunk_size = subSize - 14;
+ byte *chunk_buffer = (byte *)malloc(chunk_size);
+ assert(chunk_buffer);
+ b.read(chunk_buffer, chunk_size);
+
+ handleGameFrameObjectPost(codec, chunk_buffer, chunk_size, left, top, width, height);
+
+ // RA1 STOR: save raw FOBJ with original (pre-clipped) coords and full codec byte
+ if (_storeFrame) {
+ free(_storedFobjData);
+ _storedFobjData = (byte *)malloc(chunk_size);
+ if (_storedFobjData != nullptr) {
+ memcpy(_storedFobjData, chunk_buffer, chunk_size);
+ _storedFobjDataSize = chunk_size;
+ _storedFobjCodec = codec | ((int)ra1Param << 8);
+ _storedFobjParm2 = ra1Parm2;
+ _storedFobjLeft = rawLeft;
+ _storedFobjTop = rawTop;
+ _storedFobjWidth = width;
+ _storedFobjHeight = height;
+ } else {
+ _storedFobjDataSize = 0;
+ }
+ _storeFrame = false;
+ }
+
+ // RA1 target check â Insane can reject certain FOBJs
+ if (_insane) {
+ InsaneRebel1 *rebel1 = static_cast<InsaneRebel1 *>(_insane);
+ if (!rebel1->handleFrameObjectTarget((int16)ra1ObjectId, (int16)rawLeft, (int16)rawTop,
+ (int16)width, (int16)height, codec, ra1Param)) {
+ free(chunk_buffer);
+ return;
+ }
+ }
+
+ decodeFrameObject(codec, chunk_buffer, left, top, width, height, chunk_size, ra1Param, ra1Parm2);
+ free(chunk_buffer);
+}
+
+// ---------------------------------------------------------------------------
+// handleFrame override â RA1 frame parsing with alignment, OBJ chunks, clean frame
+// ---------------------------------------------------------------------------
+
+static bool ra1FrameHasGameChunk(Common::SeekableReadStream &b, int32 frameSize) {
+ const int64 frameStart = b.pos();
+ int32 remaining = frameSize;
+
+ while (remaining > 1) {
+ if ((b.pos() & 1) && remaining > 0) {
+ const byte pad = b.readByte();
+ if (pad == 0) {
+ remaining--;
+ } else {
+ b.seek(-1, SEEK_CUR);
+ }
+ }
+ if (remaining < 8)
+ break;
+
+ const uint32 subType = b.readUint32BE();
+ const int32 subSize = b.readUint32BE();
+ const int64 subDataPos = b.pos();
+
+ if (subType == MKTAG('F', 'R', 'M', 'E'))
+ break;
+ if (subType == MKTAG('G', 'A', 'M', 'E')) {
+ b.seek(frameStart, SEEK_SET);
+ return true;
+ }
+
+ remaining -= subSize + 8;
+ b.seek(subDataPos + subSize, SEEK_SET);
+ }
+
+ b.seek(frameStart, SEEK_SET);
+ return false;
+}
+
+void SmushPlayerRebel1::handleFrame(int32 frameSize, Common::SeekableReadStream &b) {
+ debugC(DEBUG_SMUSH, "SmushPlayerRebel1::handleFrame(%d)", _frame);
+ uint8 *audioChunk = nullptr;
+ _skipNext = false;
+ handleGameFrameStart();
+
+ bool preserveFrameHistory = false;
+ if (_insane) {
+ InsaneRebel1 *rebel1 = static_cast<InsaneRebel1 *>(_insane);
+ bool interactive = rebel1->isInteractiveVideoActive();
+ const uint16 activeOpcode = rebel1->getActiveGameOpcode();
+ bool forceClear = interactive &&
+ (activeOpcode == 0x0B ||
+ (activeOpcode == 0 && rebel1->getCurrentLevel() == 1));
+
+ preserveFrameHistory = interactive && !forceClear &&
+ ra1FrameHasGameChunk(b, frameSize);
+ }
+
+ // Restore clean frame for delta source
+ if (preserveFrameHistory &&
+ _ra1HasCleanFrame && _ra1CleanFrame &&
+ _dst && _width > 0 && _height > 0) {
+ const int frameBytes = _width * _height;
+ if (_ra1CleanFrameSize >= frameBytes)
+ memcpy(_dst, _ra1CleanFrame, frameBytes);
+ }
+
+ if (_insanity)
+ _insane->procPreRendering(_dst);
+
+ // Clear buffer for non-interactive frames to avoid trails
+ if (_dst && _width > 0 && _height > 0) {
+ if (!preserveFrameHistory)
+ memset(_dst, 0, _width * _height);
+ }
+
+ while (frameSize > 0) {
+ // RA1 exits when <=1 byte remains
+ if (frameSize <= 1) {
+ if (frameSize == 1)
+ b.skip(1);
+ break;
+ }
+
+ // RA1 top-of-loop alignment (FUN_1FDBC)
+ if ((b.pos() & 1) && frameSize > 0) {
+ byte peek = b.readByte();
+ if (peek == 0) {
+ frameSize--;
+ } else {
+ b.seek(-1, SEEK_CUR);
+ }
+ }
+
+ if (frameSize < 8) {
+ b.skip(frameSize);
+ break;
+ }
+
+ uint32 subType = b.readUint32BE();
+ int32 subSize = b.readUint32BE();
+ int32 subOffset = b.pos();
+
+ // Guard against consuming next frame marker
+ if (subType == MKTAG('F','R','M','E')) {
+ b.seek(-8, SEEK_CUR);
+ break;
+ }
+
+ switch (subType) {
+ case MKTAG('N','P','A','L'):
+ handleNewPalette(subSize, b);
+ break;
+ case MKTAG('F','O','B','J'):
+ handleFrameObject(subSize, b);
+ break;
+ case MKTAG('Z','F','O','B'):
+ handleZlibFrameObject(subSize, b);
+ break;
+ case MKTAG('P','S','A','D'):
+ case MKTAG('P','V','O','C'):
+ if (!_compressedFileMode && !isFastForwardingCurrentFrame()) {
+ audioChunk = (uint8 *)malloc(subSize + 8);
+ b.seek(-8, SEEK_CUR);
+ b.read(audioChunk, subSize + 8);
+ feedAudio(audioChunk, 0, 127, 0, 0);
+ free(audioChunk);
+ audioChunk = nullptr;
+ }
+ break;
+ case MKTAG('T','R','E','S'):
+ case MKTAG('T','E','X','T'):
+ handleTextResource(subType, subSize, b);
+ break;
+ case MKTAG('X','P','A','L'):
+ handleDeltaPalette(subSize, b);
+ break;
+ case MKTAG('I','A','C','T'):
+ handleIACT(subSize, b);
+ break;
+ case MKTAG('S','T','O','R'):
+ handleStore(subSize, b);
+ break;
+ case MKTAG('F','T','C','H'):
+ handleFetch(subSize, b);
+ break;
+ case MKTAG('S','K','I','P'):
+ _insane->procSKIP(subSize, b);
+ break;
+ case MKTAG('G','O','S','T'):
+ handleGameGost(subSize, b);
+ break;
+ case MKTAG('G','A','M','E'): {
+ InsaneRebel1 *rebel1 = (InsaneRebel1 *)_insane;
+ rebel1->handleGameChunk(subSize, b);
+ break;
+ }
+ case MKTAG('O','B','J','\0'): {
+ // RA1 object overlay chunk: variable-size header + embedded FOBJ/GAME/PSAD.
+ int32 objDataSize = frameSize - 8;
+ if (objDataSize > 0) {
+ byte *objBuf = (byte *)malloc(objDataSize);
+ b.read(objBuf, objDataSize);
+
+ int32 objPos = 0;
+ while (objPos + 8 < objDataSize) {
+ uint32 embTag = READ_BE_UINT32(objBuf + objPos);
+ uint32 embSize = READ_BE_UINT32(objBuf + objPos + 4);
+ int32 embRemaining = objDataSize - objPos - 8;
+
+ bool recognized = (embTag == MKTAG('F','O','B','J') ||
+ embTag == MKTAG('G','A','M','E') ||
+ embTag == MKTAG('P','S','A','D'));
+
+ if (!recognized || embSize > (uint32)embRemaining) {
+ objPos++;
+ continue;
+ }
+
+ if (embTag == MKTAG('F','O','B','J') && embSize >= 14) {
+ Common::MemoryReadStream embStream(objBuf + objPos + 8, embSize);
+ handleFrameObject(embSize, embStream);
+
+ if (_ra1ObjOverlayData == nullptr ||
+ (int32)embSize > _ra1ObjOverlayDataSize) {
+ free(_ra1ObjOverlayData);
+ _ra1ObjOverlayDataSize = embSize;
+ _ra1ObjOverlayData = (byte *)malloc(embSize);
+ memcpy(_ra1ObjOverlayData, objBuf + objPos + 8, embSize);
+ _ra1ObjOverlayCodec = objBuf[objPos + 8] & 0xFF;
+ _ra1ObjOverlayLeft = (int16)READ_LE_UINT16(objBuf + objPos + 10);
+ _ra1ObjOverlayTop = (int16)READ_LE_UINT16(objBuf + objPos + 12);
+ _ra1ObjOverlayWidth = READ_LE_UINT16(objBuf + objPos + 14);
+ _ra1ObjOverlayHeight = READ_LE_UINT16(objBuf + objPos + 16);
+ }
+ } else if (embTag == MKTAG('G','A','M','E')) {
+ Common::MemoryReadStream embStream(objBuf + objPos + 8, embSize);
+ InsaneRebel1 *rebel1 = (InsaneRebel1 *)_insane;
+ rebel1->handleGameChunk(embSize, embStream);
+ } else if (embTag == MKTAG('P','S','A','D')) {
+ if (!_compressedFileMode && !isFastForwardingCurrentFrame()) {
+ uint8 *audioBuf = (uint8 *)malloc(embSize + 8);
+ memcpy(audioBuf, objBuf + objPos, embSize + 8);
+ feedAudio(audioBuf, 0, 127, 0, 0);
+ free(audioBuf);
+ }
+ }
+
+ objPos += 8 + embSize;
+ if (embSize & 1)
+ objPos++;
+ }
+ free(objBuf);
+ }
+ frameSize = 0;
+ continue;
+ }
+ case MKTAG('G','A','M','2'):
+ case MKTAG('F','A','D','E'):
+ case MKTAG('S','E','G','A'):
+ case MKTAG('A','D','L',' '):
+ case MKTAG('A','D','L','2'):
+ case MKTAG('S','B','L',' '):
+ case MKTAG('S','B','L','2'):
+ case MKTAG('P','S','D','2'):
+ debugC(DEBUG_SMUSH, "SmushPlayerRebel1::handleFrame: skipping chunk %s (%d bytes)", tag2str(subType), subSize);
+ break;
+ default: {
+ // Original FUN_1FDBC: unknown uppercase tag â silently stop
+ byte tb0 = (subType >> 24) & 0xFF, tb1 = (subType >> 16) & 0xFF;
+ byte tb2 = (subType >> 8) & 0xFF, tb3 = subType & 0xFF;
+ if (tb0 > 0x40 && tb0 < 0x5B && tb1 > 0x40 && tb1 < 0x5B &&
+ tb2 > 0x40 && tb2 < 0x5B && tb3 > 0x40 && tb3 < 0x5B) {
+ debug(5, "RA1: unknown uppercase tag %s at frame %d, stopping frame parse", tag2str(subType), _frame);
+ frameSize = 0;
+ continue;
+ }
+ error("Unknown frame subChunk found : %s, %d", tag2str(subType), subSize);
+ }
+ }
+
+ frameSize -= subSize + 8;
+ b.seek(subOffset + subSize, SEEK_SET);
+ // RA1 uses top-of-loop alignment, not bottom-of-loop padding
+ }
+
+ // Re-render cockpit overlay
+ if (_ra1ObjOverlayData != nullptr && _frame > 0) {
+ Common::MemoryReadStream overlayStream(_ra1ObjOverlayData, _ra1ObjOverlayDataSize);
+ handleFrameObject(_ra1ObjOverlayDataSize, overlayStream);
+ }
+
+ // Save clean frame for next delta
+ if (preserveFrameHistory && _dst && _width > 0 && _height > 0) {
+ const int frameBytes = _width * _height;
+ byte *newClean = (byte *)realloc(_ra1CleanFrame, frameBytes);
+ if (newClean != nullptr) {
+ _ra1CleanFrame = newClean;
+ _ra1CleanFrameSize = frameBytes;
+ memcpy(_ra1CleanFrame, _dst, frameBytes);
+ _ra1HasCleanFrame = true;
+ } else {
+ _ra1HasCleanFrame = false;
+ }
+ } else {
+ _ra1HasCleanFrame = false;
+ }
+
+ if (_insanity)
+ _insane->procPostRendering(_dst, 0, 0, 0, _frame, _nbframes-1);
+
+ if (_width != 0 && _height != 0)
+ updateScreen();
+
+ _frame++;
+}
+
+// ---------------------------------------------------------------------------
+// handleGameUpdateScreen â RA1 viewport-aware screen blit
+// ---------------------------------------------------------------------------
+
+void SmushPlayerRebel1::handleGameUpdateScreen(const byte *src, int srcPitch, int width, int height) {
+ if (_dst == nullptr || _width <= 0 || _height <= 0)
+ return;
+
+ int ra1ViewX = _ra1ViewportOffsetX;
+ int ra1ViewY = _ra1ViewportOffsetY;
+
+ const int srcX = CLIP(_scrollX + ra1ViewX, 0, _width - 1);
+ const int srcY = CLIP(_scrollY + ra1ViewY, 0, _height - 1);
+
+ int frameWidth = MIN(_width - srcX, _vm->_screenWidth);
+ int frameHeight = MIN(_height - srcY, _vm->_screenHeight);
+ if (frameWidth <= 0 || frameHeight <= 0)
+ return;
+
+ const byte *dst = _dst + srcY * _width + srcX;
+
+ SmushPlayer::handleGameUpdateScreen(dst, _width, frameWidth, frameHeight);
+}
+
} // End of namespace Scumm
diff --git a/engines/scumm/smush/smush_player_ra2.cpp b/engines/scumm/smush/smush_player_ra2.cpp
index 8feab912d52..4c7f8eb3bc3 100644
--- a/engines/scumm/smush/smush_player_ra2.cpp
+++ b/engines/scumm/smush/smush_player_ra2.cpp
@@ -39,14 +39,6 @@
namespace Scumm {
-bool SmushPlayer::isRA1() const {
- return _vm->_game.id == GID_REBEL1;
-}
-
-bool SmushPlayer::isRA2() const {
- return _vm->_game.id == GID_REBEL2;
-}
-
// Forward declarations for RA2 codec functions (defined in codec_ra2.cpp)
void smushDecodeLineUpdate(byte *dst, const byte *src, int left, int top, int width, int height, int pitch);
void smushDecodeSkipRLE(byte *dst, const byte *src, int left, int top, int width, int height, int pitch, int dataSize);
@@ -87,17 +79,7 @@ void SmushPlayerRebel2::initGamePlayerFields() {
_lastFobjWidth = 0;
_lastFobjHeight = 0;
_hasFrameFobjForGost = false;
- _ra1ObjOverlayData = nullptr;
- _ra1ObjOverlayDataSize = 0;
- _ra1ObjOverlayCodec = 0;
- _ra1ObjOverlayLeft = 0;
- _ra1ObjOverlayTop = 0;
- _ra1ObjOverlayWidth = 0;
- _ra1ObjOverlayHeight = 0;
- _ra1ViewportOffsetX = 0;
- _ra1ViewportOffsetY = 0;
_skipNext = false;
- _ra2FastForwarding = false;
_fobjOffsetX = 0;
_fobjOffsetY = 0;
_storeFrame = false;
@@ -120,8 +102,6 @@ void SmushPlayerRebel2::destroyGamePlayerFields() {
_lastFobjData = nullptr;
free(_loadBuffer);
_loadBuffer = nullptr;
- free(_ra1ObjOverlayData);
- _ra1ObjOverlayData = nullptr;
}
/**
@@ -167,9 +147,6 @@ void SmushPlayerRebel2::releaseGameVideoState() {
free(_lastFobjData);
_lastFobjData = nullptr;
_lastFobjDataSize = 0;
- free(_ra1ObjOverlayData);
- _ra1ObjOverlayData = nullptr;
- _ra1ObjOverlayDataSize = 0;
_hasFrameFobjForGost = false;
// Preserve _frameBuffer across videos so that gameplay videos (which have no
// background FOBJ) can use the stored background from the previous BEG video.
@@ -283,13 +260,17 @@ bool SmushPlayerRebel2::handleGameAnimHeader(byte *headerContent) {
// RA2 helper methods (still on SmushPlayer for now, used by base class)
// ---------------------------------------------------------------------------
+void SmushPlayerRebel2::handleGameLoad(int32 subSize, Common::SeekableReadStream &b) {
+ handleLoad(subSize, b);
+}
+
/**
* Handle LOAD chunk for Rebel Assault 2.
*
* LOAD chunks stream embedded resource data across multiple frames.
* The data is accumulated in a buffer and consumed by the audio system.
*/
-void SmushPlayer::handleLoad(int32 subSize, Common::SeekableReadStream &b) {
+void SmushPlayerRebel2::handleLoad(int32 subSize, Common::SeekableReadStream &b) {
debugC(DEBUG_SMUSH, "SmushPlayer::handleLoad()");
if (subSize < 10) {
Commit: f3e88ea73219edd5ad3a271a77e5246867dc7e81
https://github.com/scummvm/scummvm/commit/f3e88ea73219edd5ad3a271a77e5246867dc7e81
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:47+02:00
Commit Message:
SCUMM: RA: Move text and GOST handlers into RA SMUSH players
Changed paths:
engines/scumm/smush/smush_player.cpp
engines/scumm/smush/smush_player.h
engines/scumm/smush/smush_player_ra1.cpp
engines/scumm/smush/smush_player_ra2.cpp
diff --git a/engines/scumm/smush/smush_player.cpp b/engines/scumm/smush/smush_player.cpp
index 0a010f80aa3..2f154e93fe1 100644
--- a/engines/scumm/smush/smush_player.cpp
+++ b/engines/scumm/smush/smush_player.cpp
@@ -1535,99 +1535,6 @@ SmushFont *SmushPlayer::getFont(int font) {
return _sf[font];
}
-SmushFont *SmushPlayer::ra1GetFont(int font) {
- const char *ra1Fonts[] = {
- "SYS/TALKFONT.NUT",
- "SYS/TECHFONT.NUT",
- "SYS/TITLFONT.NUT",
- "SYS/DISPLAY.NUT"
- };
- const char *ra2FallbackFonts[] = {
- "SYSTM/TALKFONT.NUT",
- "SYSTM/SMALFONT.NUT",
- "SYSTM/TITLFONT.NUT",
- "SYSTM/SMALFONT.NUT"
- };
-
- int numFonts = ARRAYSIZE(ra1Fonts);
- if (font < 0 || font >= numFonts) {
- debugC(DEBUG_SMUSH, "SmushPlayer::ra1GetFont: unknown font %d, using TALKFONT", font);
- font = 0;
- }
-
- if (_sf[font])
- return _sf[font];
-
- const char *fontPath = ra1Fonts[font];
- ScummFile *testFile = _vm->instantiateScummFile();
- bool ok = _vm->openFile(*testFile, Common::Path(fontPath));
- if (ok)
- testFile->close();
- delete testFile;
-
- if (!ok)
- fontPath = ra2FallbackFonts[font];
-
- _sf[font] = new SmushFont(_vm, fontPath, true);
- return _sf[font];
-}
-
-void SmushPlayer::ra1HandleText(int32 subSize, Common::SeekableReadStream &b) {
- if (subSize < 8 || !_dst || _width <= 0 || _height <= 0)
- return;
-
- InsaneRebel1 *rebel1 = static_cast<InsaneRebel1 *>(_insane);
- if (!rebel1)
- return;
-
- const int textAnchorX = b.readSint32BE();
- int cursorY = b.readSint32BE();
-
- int textLen = subSize - 8;
- if (textLen <= 0)
- return;
-
- byte *textBuf = (byte *)malloc(textLen);
- if (!textBuf)
- return;
- b.read(textBuf, textLen);
-
- int start = 0;
- if (textLen > 0 && textBuf[0] == '.')
- start = 1;
-
- int remaining = textLen - start;
- while (remaining > 0) {
- int lineLen = 0;
- while (lineLen < remaining && textBuf[start + lineLen] != 0)
- lineLen++;
-
- if (lineLen > 0) {
- char *line = (char *)malloc(lineLen + 1);
- if (!line) {
- cursorY += 12;
- } else {
- memcpy(line, textBuf + start, lineLen);
- line[lineLen] = '\0';
- const int drawX = textAnchorX - (rebel1->getFontBankStringWidth(line) / 2);
- rebel1->drawFontBankString(_dst, _width, _width, _height, drawX, cursorY, line);
- cursorY += rebel1->getFontBankLineAdvance(line);
- free(line);
- }
- } else {
- cursorY += rebel1->getFontBankLineAdvance(nullptr);
- }
-
- int consumed = lineLen;
- if (consumed < remaining && textBuf[start + consumed] == 0)
- consumed++;
- start += consumed;
- remaining -= consumed;
- }
-
- free(textBuf);
-}
-
void SmushPlayer::parseNextFrame() {
if (_seekPos >= 0) {
diff --git a/engines/scumm/smush/smush_player.h b/engines/scumm/smush/smush_player.h
index ce56b23bbc5..22f2665069e 100644
--- a/engines/scumm/smush/smush_player.h
+++ b/engines/scumm/smush/smush_player.h
@@ -346,22 +346,10 @@ private:
void handleDeltaPalette(int32 subSize, Common::SeekableReadStream &);
void readPalette(byte *, Common::SeekableReadStream &);
- // RA2 helper methods called from SmushPlayerRebel2 overrides
- void ra2HandleTextResource(const char *str, int fontId, int color,
- int pos_x, int pos_y, int left, int top,
- int width, int height, TextStyleFlags flg);
- void ra2SelectFrameBuffer(int width, int height);
- void ra2AdjustFrameCoords(int &left, int &top, int &width, int &height, int pitch, int *srcSkipY = nullptr);
- bool ra2DecodeCodec(int codec, const uint8 *src, int left, int top,
- int width, int height, int pitch, int dataSize);
- void ra2StoreFobjData(int codec, const byte *data, int32 dataSize,
+ // Shared RA1/RA2 helpers (access _storedFobj*/_lastFobj* on base)
+ void adjustFrameCoords(int &left, int &top, int &width, int &height, int pitch, int *srcSkipY = nullptr);
+ void rememberLastFobj(int codec, const byte *data, int32 dataSize,
int left, int top, int width, int height);
- void ra2RememberLastFobj(int codec, const byte *data, int32 dataSize,
- int left, int top, int width, int height);
- void ra1HandleGost(int32 subSize, Common::SeekableReadStream &b);
- void ra2HandleGost(int32 subSize, Common::SeekableReadStream &b);
- SmushFont *ra1GetFont(int font);
- void ra1HandleText(int32 subSize, Common::SeekableReadStream &b);
void initAudio(int samplerate, int32 maxChunkSize);
void terminateAudio();
@@ -409,6 +397,11 @@ protected:
void handleFrame(int32 frameSize, Common::SeekableReadStream &b) override;
void handleGameUpdateScreen(const byte *src, int srcPitch, int width, int height) override;
+private:
+ void ra1HandleGost(int32 subSize, Common::SeekableReadStream &b);
+ SmushFont *ra1GetFont(int font);
+ void ra1HandleText(int32 subSize, Common::SeekableReadStream &b);
+
// RA1 clean frame buffer for delta source restoration
byte *_ra1CleanFrame;
int32 _ra1CleanFrameSize;
@@ -464,6 +457,15 @@ protected:
private:
void handleLoad(int32 subSize, Common::SeekableReadStream &b);
+ void ra2HandleTextResource(const char *str, int fontId, int color,
+ int pos_x, int pos_y, int left, int top,
+ int width, int height, TextStyleFlags flg);
+ void ra2SelectFrameBuffer(int width, int height);
+ bool ra2DecodeCodec(int codec, const uint8 *src, int left, int top,
+ int width, int height, int pitch, int dataSize);
+ void ra2StoreFobjData(int codec, const byte *data, int32 dataSize,
+ int left, int top, int width, int height);
+ void ra2HandleGost(int32 subSize, Common::SeekableReadStream &b);
// LOAD chunk streaming buffer (embedded resource data)
byte *_loadBuffer;
diff --git a/engines/scumm/smush/smush_player_ra1.cpp b/engines/scumm/smush/smush_player_ra1.cpp
index 07f2e312f94..4846ad016f0 100644
--- a/engines/scumm/smush/smush_player_ra1.cpp
+++ b/engines/scumm/smush/smush_player_ra1.cpp
@@ -174,9 +174,9 @@ bool SmushPlayerRebel1::handleGameFetch(int32 subSize, Common::SeekableReadStrea
return true;
}
-void SmushPlayer::ra1HandleGost(int32 subSize, Common::SeekableReadStream &b) {
+void SmushPlayerRebel1::ra1HandleGost(int32 subSize, Common::SeekableReadStream &b) {
if (subSize < 12) {
- warning("SmushPlayer::ra1HandleGost: chunk too small (%d bytes)", subSize);
+ warning("SmushPlayerRebel1::ra1HandleGost: chunk too small (%d bytes)", subSize);
return;
}
@@ -291,7 +291,7 @@ bool SmushPlayerRebel1::handleGameAdjustCoords(int codec, int &left, int &top, i
// RA1 additive codec (SKIP_RLE) uses original coords, not adjusted
if (codec == SMUSH_CODEC_SKIP_RLE)
return false;
- ra2AdjustFrameCoords(left, top, width, height, pitch, srcSkipY);
+ adjustFrameCoords(left, top, width, height, pitch, srcSkipY);
return true;
}
@@ -342,7 +342,7 @@ void SmushPlayerRebel1::handleGameFrameObjectPre(int codec, int left, int top, i
}
void SmushPlayerRebel1::handleGameFrameObjectPost(int codec, const byte *data, int32 dataSize, int left, int top, int width, int height) {
- ra2RememberLastFobj(codec, data, dataSize, left, top, width, height);
+ rememberLastFobj(codec, data, dataSize, left, top, width, height);
// RA1 STOR handling remains in handleFrameObject (needs ra1Param/rawLeft/rawTop)
}
@@ -729,4 +729,97 @@ void SmushPlayerRebel1::handleGameUpdateScreen(const byte *src, int srcPitch, in
SmushPlayer::handleGameUpdateScreen(dst, _width, frameWidth, frameHeight);
}
+SmushFont *SmushPlayerRebel1::ra1GetFont(int font) {
+ const char *ra1Fonts[] = {
+ "SYS/TALKFONT.NUT",
+ "SYS/TECHFONT.NUT",
+ "SYS/TITLFONT.NUT",
+ "SYS/DISPLAY.NUT"
+ };
+ const char *ra2FallbackFonts[] = {
+ "SYSTM/TALKFONT.NUT",
+ "SYSTM/SMALFONT.NUT",
+ "SYSTM/TITLFONT.NUT",
+ "SYSTM/SMALFONT.NUT"
+ };
+
+ int numFonts = ARRAYSIZE(ra1Fonts);
+ if (font < 0 || font >= numFonts) {
+ debugC(DEBUG_SMUSH, "SmushPlayerRebel1::ra1GetFont: unknown font %d, using TALKFONT", font);
+ font = 0;
+ }
+
+ if (_sf[font])
+ return _sf[font];
+
+ const char *fontPath = ra1Fonts[font];
+ ScummFile *testFile = _vm->instantiateScummFile();
+ bool ok = _vm->openFile(*testFile, Common::Path(fontPath));
+ if (ok)
+ testFile->close();
+ delete testFile;
+
+ if (!ok)
+ fontPath = ra2FallbackFonts[font];
+
+ _sf[font] = new SmushFont(_vm, fontPath, true);
+ return _sf[font];
+}
+
+void SmushPlayerRebel1::ra1HandleText(int32 subSize, Common::SeekableReadStream &b) {
+ if (subSize < 8 || !_dst || _width <= 0 || _height <= 0)
+ return;
+
+ InsaneRebel1 *rebel1 = static_cast<InsaneRebel1 *>(_insane);
+ if (!rebel1)
+ return;
+
+ const int textAnchorX = b.readSint32BE();
+ int cursorY = b.readSint32BE();
+
+ int textLen = subSize - 8;
+ if (textLen <= 0)
+ return;
+
+ byte *textBuf = (byte *)malloc(textLen);
+ if (!textBuf)
+ return;
+ b.read(textBuf, textLen);
+
+ int start = 0;
+ if (textLen > 0 && textBuf[0] == '.')
+ start = 1;
+
+ int remaining = textLen - start;
+ while (remaining > 0) {
+ int lineLen = 0;
+ while (lineLen < remaining && textBuf[start + lineLen] != 0)
+ lineLen++;
+
+ if (lineLen > 0) {
+ char *line = (char *)malloc(lineLen + 1);
+ if (!line) {
+ cursorY += 12;
+ } else {
+ memcpy(line, textBuf + start, lineLen);
+ line[lineLen] = '\0';
+ const int drawX = textAnchorX - (rebel1->getFontBankStringWidth(line) / 2);
+ rebel1->drawFontBankString(_dst, _width, _width, _height, drawX, cursorY, line);
+ cursorY += rebel1->getFontBankLineAdvance(line);
+ free(line);
+ }
+ } else {
+ cursorY += rebel1->getFontBankLineAdvance(nullptr);
+ }
+
+ int consumed = lineLen;
+ if (consumed < remaining && textBuf[start + consumed] == 0)
+ consumed++;
+ start += consumed;
+ remaining -= consumed;
+ }
+
+ free(textBuf);
+}
+
} // End of namespace Scumm
diff --git a/engines/scumm/smush/smush_player_ra2.cpp b/engines/scumm/smush/smush_player_ra2.cpp
index 4c7f8eb3bc3..e59e6831254 100644
--- a/engines/scumm/smush/smush_player_ra2.cpp
+++ b/engines/scumm/smush/smush_player_ra2.cpp
@@ -346,7 +346,7 @@ void SmushPlayer::ensureMultiFont() {
/**
* RA2-specific text rendering using SmushMultiFont for inline font switching.
*/
-void SmushPlayer::ra2HandleTextResource(const char *str, int fontId, int color,
+void SmushPlayerRebel2::ra2HandleTextResource(const char *str, int fontId, int color,
int pos_x, int pos_y, int left, int top,
int width, int height, TextStyleFlags flg) {
ensureMultiFont();
@@ -368,7 +368,7 @@ void SmushPlayer::ra2HandleTextResource(const char *str, int fontId, int color,
* RA2-specific buffer selection for non-standard FOBJ dimensions.
* Returns the destination buffer to use and updates _dst, _width, _height.
*/
-void SmushPlayer::ra2SelectFrameBuffer(int width, int height) {
+void SmushPlayerRebel2::ra2SelectFrameBuffer(int width, int height) {
// Rebel2 uses a special buffer for all non-matching frames.
// Level 1: First frame is 424x260 (background), small sprites reuse same buffer
// Level 2: Uses virtual screen directly (handled below when _specialBuffer stays null)
@@ -412,7 +412,7 @@ void SmushPlayer::ra2SelectFrameBuffer(int width, int height) {
* When srcSkipY is non-null, outputs the number of source rows to skip
* when top is clipped from negative (for codecs with row-size prefixes).
*/
-void SmushPlayer::ra2AdjustFrameCoords(int &left, int &top, int &width, int &height, int pitch, int *srcSkipY) {
+void SmushPlayer::adjustFrameCoords(int &left, int &top, int &width, int &height, int pitch, int *srcSkipY) {
left += _fobjOffsetX;
top += _fobjOffsetY;
@@ -439,7 +439,7 @@ void SmushPlayer::ra2AdjustFrameCoords(int &left, int &top, int &width, int &hei
* Dispatch to RA2-specific codec functions.
* Returns true if the codec was handled, false for standard codecs.
*/
-bool SmushPlayer::ra2DecodeCodec(int codec, const uint8 *src, int left, int top,
+bool SmushPlayerRebel2::ra2DecodeCodec(int codec, const uint8 *src, int left, int top,
int width, int height, int pitch, int dataSize) {
switch (codec) {
case SMUSH_CODEC_LINE_UPDATE:
@@ -460,7 +460,7 @@ bool SmushPlayer::ra2DecodeCodec(int codec, const uint8 *src, int left, int top,
/**
* Save raw FOBJ data when STOR is pending (for later re-decoding by FTCH).
*/
-void SmushPlayer::ra2StoreFobjData(int codec, const byte *data, int32 dataSize,
+void SmushPlayerRebel2::ra2StoreFobjData(int codec, const byte *data, int32 dataSize,
int left, int top, int width, int height) {
free(_storedFobjData);
_storedFobjData = (byte *)malloc(dataSize);
@@ -478,7 +478,7 @@ void SmushPlayer::ra2StoreFobjData(int codec, const byte *data, int32 dataSize,
/**
* Cache the most recent frame FOBJ for GOST re-rendering.
*/
-void SmushPlayer::ra2RememberLastFobj(int codec, const byte *data, int32 dataSize,
+void SmushPlayer::rememberLastFobj(int codec, const byte *data, int32 dataSize,
int left, int top, int width, int height) {
if (dataSize <= 0) {
_hasFrameFobjForGost = false;
@@ -487,7 +487,7 @@ void SmushPlayer::ra2RememberLastFobj(int codec, const byte *data, int32 dataSiz
byte *newData = (byte *)realloc(_lastFobjData, dataSize);
if (newData == nullptr) {
- warning("SmushPlayer::ra2RememberLastFobj: Failed to allocate %d bytes", dataSize);
+ warning("SmushPlayer::rememberLastFobj: Failed to allocate %d bytes", dataSize);
free(_lastFobjData);
_lastFobjData = nullptr;
_lastFobjDataSize = 0;
@@ -510,9 +510,9 @@ void SmushPlayer::ra2RememberLastFobj(int codec, const byte *data, int32 dataSiz
* RA2 GOST chunk handler.
* Re-renders the most recent frame FOBJ at the supplied ghost position.
*/
-void SmushPlayer::ra2HandleGost(int32 subSize, Common::SeekableReadStream &b) {
+void SmushPlayerRebel2::ra2HandleGost(int32 subSize, Common::SeekableReadStream &b) {
if (subSize < 6) {
- warning("SmushPlayer::ra2HandleGost: chunk too small (%d bytes)", subSize);
+ warning("SmushPlayerRebel2::ra2HandleGost: chunk too small (%d bytes)", subSize);
return;
}
@@ -581,7 +581,7 @@ bool SmushPlayerRebel2::handleGameDimensionOverride(int codec, int width, int he
}
bool SmushPlayerRebel2::handleGameAdjustCoords(int codec, int &left, int &top, int &width, int &height, int pitch, int *srcSkipY) {
- ra2AdjustFrameCoords(left, top, width, height, pitch, srcSkipY);
+ adjustFrameCoords(left, top, width, height, pitch, srcSkipY);
return true;
}
@@ -602,7 +602,7 @@ void SmushPlayerRebel2::handleGameFrameObjectPre(int codec, int left, int top, i
}
void SmushPlayerRebel2::handleGameFrameObjectPost(int codec, const byte *data, int32 dataSize, int left, int top, int width, int height) {
- ra2RememberLastFobj(codec, data, dataSize, left, top, width, height);
+ rememberLastFobj(codec, data, dataSize, left, top, width, height);
if (_storeFrame) {
ra2StoreFobjData(codec, data, dataSize, left, top, width, height);
Commit: cbbdde480bac5a130c9358263c94b26cf6c685ca
https://github.com/scummvm/scummvm/commit/cbbdde480bac5a130c9358263c94b26cf6c685ca
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:47+02:00
Commit Message:
SCUMM: RA: Move RA1 codec 1 decoder into RA player
Changed paths:
engines/scumm/smush/codec1.cpp
engines/scumm/smush/smush_player.cpp
engines/scumm/smush/smush_player_ra1.cpp
diff --git a/engines/scumm/smush/codec1.cpp b/engines/scumm/smush/codec1.cpp
index 7c6efb9bd61..087e4d53aaf 100644
--- a/engines/scumm/smush/codec1.cpp
+++ b/engines/scumm/smush/codec1.cpp
@@ -36,48 +36,4 @@ void smushDecodeRLE(byte *dst, const byte *src, int left, int top, int width, in
} while (--height);
}
-
-/**
- * RA1 codec 1: RLE with transparency on pixel 0.
- * Same BOMP encoding as smushDecodeRLE but pixel value 0 is not written,
- * allowing the background (restored via FTCH) to show through.
- * Matches FFmpeg's old_codec1() with opaque=0.
- */
-void smushDecodeRA1Transparent(byte *dst, const byte *src, int left, int top, int width, int height, int pitch) {
- dst += top * pitch;
- do {
- byte *rowDst = dst + left;
- const byte *lineData = src + 2;
- int remaining = width;
-
- while (remaining > 0) {
- byte code = *lineData++;
- byte num = (code >> 1) + 1;
- if (num > remaining)
- num = remaining;
- if (code & 1) {
- // Fill: repeat single byte
- byte color = *lineData++;
- if (color != 0) {
- memset(rowDst, color, num);
- }
- // If color == 0: skip (transparent)
- } else {
- // Copy: write each byte, skipping 0
- for (int j = 0; j < num; j++) {
- byte c = lineData[j];
- if (c != 0)
- rowDst[j] = c;
- }
- lineData += num;
- }
- rowDst += num;
- remaining -= num;
- }
-
- src += READ_LE_UINT16(src) + 2;
- dst += pitch;
- } while (--height);
-}
-
} // End of namespace Scumm
diff --git a/engines/scumm/smush/smush_player.cpp b/engines/scumm/smush/smush_player.cpp
index 2f154e93fe1..0ed39b3ae9d 100644
--- a/engines/scumm/smush/smush_player.cpp
+++ b/engines/scumm/smush/smush_player.cpp
@@ -891,10 +891,7 @@ byte *SmushPlayer::getVideoPalette() {
}
void smushDecodeRLE(byte *dst, const byte *src, int left, int top, int width, int height, int pitch);
-void smushDecodeRLEOpaque(byte *dst, const byte *src, int left, int top, int width, int height, int pitch);
-void smushDecodeRA1Transparent(byte *dst, const byte *src, int left, int top, int width, int height, int pitch);
void smushDecodeUncompressed(byte *dst, const byte *src, int left, int top, int width, int height, int pitch);
-void smushDecodeRA1Block(byte *dst, const byte *src, int left, int top, int width, int height, int pitch, int dataSize, uint8 param, uint16 parm2, int codec);
/**
* RA1 codec 21: Skip/copy line codec (FUN_10D41).
@@ -1039,143 +1036,6 @@ void smushDecodeRA1Scatter(byte *dst, const byte *src, int left, int top, int bu
*/
// Persistent tile table state for RA1 codec 4/5
-static uint8 s_ra1C4Tbl[2][256][16];
-static uint16 s_ra1C4Param = 0xFFFF;
-
-static void ra1Codec4GenTiles(uint16 param1) {
- uint8 *dst = &s_ra1C4Tbl[0][0][0];
-
- for (int i = 1; i < 16; i += 2) {
- for (int k = 0; k < 16; k++) {
- int j = i + param1;
- int l = k + param1;
- int m = (j + l) / 2;
- int n = (j + m) / 2;
- int o = (l + m) / 2;
- if (j == m || l == m) {
- *dst++ = l; *dst++ = j; *dst++ = l; *dst++ = j;
- *dst++ = j; *dst++ = l; *dst++ = j; *dst++ = j;
- *dst++ = l; *dst++ = j; *dst++ = l; *dst++ = j;
- *dst++ = l; *dst++ = l; *dst++ = j; *dst++ = l;
- } else {
- *dst++ = m; *dst++ = m; *dst++ = n; *dst++ = j;
- *dst++ = m; *dst++ = m; *dst++ = n; *dst++ = j;
- *dst++ = o; *dst++ = o; *dst++ = m; *dst++ = n;
- *dst++ = l; *dst++ = l; *dst++ = o; *dst++ = m;
- }
- }
- }
-
- for (int i = 0; i < 16; i += 2) {
- for (int k = 0; k < 16; k++) {
- int j = i + param1;
- int l = k + param1;
- int m = (j + l) / 2;
- int n = (j + m) / 2;
- int o = (l + m) / 2;
- if (m == j || m == l) {
- *dst++ = j; *dst++ = j; *dst++ = l; *dst++ = j;
- *dst++ = j; *dst++ = j; *dst++ = j; *dst++ = l;
- *dst++ = l; *dst++ = j; *dst++ = l; *dst++ = l;
- *dst++ = j; *dst++ = l; *dst++ = j; *dst++ = l;
- } else {
- *dst++ = j; *dst++ = j; *dst++ = n; *dst++ = m;
- *dst++ = j; *dst++ = j; *dst++ = n; *dst++ = m;
- *dst++ = n; *dst++ = n; *dst++ = m; *dst++ = o;
- *dst++ = m; *dst++ = m; *dst++ = o; *dst++ = l;
- }
- }
- }
-}
-
-static bool ra1Codec4LoadTiles(const byte *&src, int &remaining, uint16 param2, uint8 clr) {
- uint8 *dst = &s_ra1C4Tbl[1][0][0];
- int loop = param2 * 8;
-
- if (param2 > 256 || remaining < loop)
- return false;
-
- for (int i = 0; i < loop; i++) {
- byte c = *src++;
- remaining--;
- *dst++ = (c >> 4) + clr;
- *dst++ = (c & 0xF) + clr;
- }
- return true;
-}
-
-void smushDecodeRA1Block(byte *dst, const byte *src, int left, int top, int width, int height,
- int pitch, int dataSize, uint8 param, uint16 parm2, int codec) {
- const int mx = pitch; // framebuffer width
- const int my = height; // framebuffer height
-
- // Generate dither tile table if palette base changed
- if (s_ra1C4Param != param) {
- ra1Codec4GenTiles(param);
- s_ra1C4Param = param;
- }
-
- // Load frame-specific tiles from data stream (4bpp nibble-split)
- int remaining = dataSize;
- const byte *data = src;
- if (parm2 > 0) {
- if (!ra1Codec4LoadTiles(data, remaining, parm2, param)) {
- warning("smushDecodeRA1Block: not enough data for tile load (parm2=%d)", parm2);
- return;
- }
- }
-
- // Decode blocks: iterate columns by 4, then rows by 4 (column-major order)
- for (int j = 0; j < width; j += 4) {
- byte mask = 0, bits = 0;
- int x = left + j;
- for (int i = 0; i < height; i += 4) {
- int y = top + i;
- int bit = 0;
-
- if (parm2 > 0) {
- if (bits == 0) {
- if (remaining < 1)
- return;
- mask = *data++;
- remaining--;
- bits = 8;
- }
- bit = !!(mask & 0x80);
- mask <<= 1;
- bits--;
- }
-
- if (remaining < 1)
- return;
- byte idx = *data++;
- remaining--;
-
- // Codec 4: index 0x80 = skip (delta). Codec 5: no skip.
- if (bit == 0 && idx == 0x80 && codec != 5)
- continue;
- if (y >= my || (y + 4) < 0 || (x + 4) < 0 || x >= mx)
- continue;
-
- const byte *gs = &s_ra1C4Tbl[bit][idx][0];
- if (y >= 0 && x >= 0 && (y + 4) <= my && (x + 4) <= mx) {
- // Fast path: fully within bounds
- for (int k = 0; k < 4; k++, gs += 4)
- memcpy(dst + x + (y + k) * pitch, gs, 4);
- } else {
- // Slow path: clipping
- for (int k = 0; k < 4; k++) {
- for (int l = 0; l < 4; l++, gs++) {
- int yo = y + k, xo = x + l;
- if (yo >= 0 && yo < my && xo >= 0 && xo < mx)
- *(dst + yo * pitch + xo) = *gs;
- }
- }
- }
- }
- }
-}
-
void SmushPlayer::decodeFrameObject(int codec, const uint8 *src, int left, int top, int width, int height, int dataSize, uint8 ra1Param, uint16 ra1Parm2) {
if ((height == 242) && (width == 384)) {
int bufSize = 242 * 384;
diff --git a/engines/scumm/smush/smush_player_ra1.cpp b/engines/scumm/smush/smush_player_ra1.cpp
index 4846ad016f0..e25d29c267b 100644
--- a/engines/scumm/smush/smush_player_ra1.cpp
+++ b/engines/scumm/smush/smush_player_ra1.cpp
@@ -255,13 +255,155 @@ void SmushPlayerRebel1::handleGameParseNextFrame() {
}
// Forward declarations for RA1 codec functions (defined in smush_player.cpp and codec1.cpp)
-void smushDecodeRA1Transparent(byte *dst, const byte *src, int left, int top, int width, int height, int pitch);
+/**
+ * RA1 codec 1: RLE with transparency on pixel 0.
+ * Same BOMP encoding as smushDecodeRLE but pixel value 0 is not written,
+ * allowing the background (restored via FTCH) to show through.
+ */
+void smushDecodeRA1Transparent(byte *dst, const byte *src, int left, int top, int width, int height, int pitch) {
+ dst += top * pitch;
+ do {
+ byte *rowDst = dst + left;
+ const byte *lineData = src + 2;
+ int remaining = width;
+
+ while (remaining > 0) {
+ byte code = *lineData++;
+ byte num = (code >> 1) + 1;
+ if (num > remaining)
+ num = remaining;
+ if (code & 1) {
+ byte color = *lineData++;
+ if (color != 0)
+ memset(rowDst, color, num);
+ } else {
+ for (int j = 0; j < num; j++) {
+ byte c = lineData[j];
+ if (c != 0)
+ rowDst[j] = c;
+ }
+ lineData += num;
+ }
+ rowDst += num;
+ remaining -= num;
+ }
+
+ src += READ_LE_UINT16(src) + 2;
+ dst += pitch;
+ } while (--height);
+}
void smushDecodeRLEOpaque(byte *dst, const byte *src, int left, int top, int width, int height, int pitch);
void smushDecodeRA1SkipCopy(byte *dst, const byte *src, int left, int top, int width, int height, int pitch);
void smushDecodeRA1AdditiveLineUpdate(byte *dst, const byte *src, int left, int top, int width, int height,
int pitch, int bufWidth, int bufHeight, uint8 param);
void smushDecodeRA1Scatter(byte *dst, const byte *src, int left, int top, int bufWidth, int bufHeight, int pitch, int dataSize);
-void smushDecodeRA1Block(byte *dst, const byte *src, int left, int top, int width, int height, int pitch, int dataSize, uint8 param, uint16 parm2, int codec);
+// RA1 codec 4/5: block-based dithered codec with 4x4 tile lookup tables
+static uint8 s_ra1C4Tbl[2][256][16];
+static uint16 s_ra1C4Param = 0xFFFF;
+
+static void ra1Codec4GenTiles(uint16 param1) {
+ uint8 *dst = &s_ra1C4Tbl[0][0][0];
+ for (int i = 1; i < 16; i += 2) {
+ for (int k = 0; k < 16; k++) {
+ int j = i + param1, l = k + param1;
+ int m = (j + l) / 2, n = (j + m) / 2, o = (l + m) / 2;
+ if (j == m || l == m) {
+ *dst++ = l; *dst++ = j; *dst++ = l; *dst++ = j;
+ *dst++ = j; *dst++ = l; *dst++ = j; *dst++ = j;
+ *dst++ = l; *dst++ = j; *dst++ = l; *dst++ = j;
+ *dst++ = l; *dst++ = l; *dst++ = j; *dst++ = l;
+ } else {
+ *dst++ = m; *dst++ = m; *dst++ = n; *dst++ = j;
+ *dst++ = m; *dst++ = m; *dst++ = n; *dst++ = j;
+ *dst++ = o; *dst++ = o; *dst++ = m; *dst++ = n;
+ *dst++ = l; *dst++ = l; *dst++ = o; *dst++ = m;
+ }
+ }
+ }
+ for (int i = 0; i < 16; i += 2) {
+ for (int k = 0; k < 16; k++) {
+ int j = i + param1, l = k + param1;
+ int m = (j + l) / 2, n = (j + m) / 2, o = (l + m) / 2;
+ if (m == j || m == l) {
+ *dst++ = j; *dst++ = j; *dst++ = l; *dst++ = j;
+ *dst++ = j; *dst++ = j; *dst++ = j; *dst++ = l;
+ *dst++ = l; *dst++ = j; *dst++ = l; *dst++ = l;
+ *dst++ = j; *dst++ = l; *dst++ = j; *dst++ = l;
+ } else {
+ *dst++ = j; *dst++ = j; *dst++ = n; *dst++ = m;
+ *dst++ = j; *dst++ = j; *dst++ = n; *dst++ = m;
+ *dst++ = n; *dst++ = n; *dst++ = m; *dst++ = o;
+ *dst++ = m; *dst++ = m; *dst++ = o; *dst++ = l;
+ }
+ }
+ }
+}
+
+static bool ra1Codec4LoadTiles(const byte *&src, int &remaining, uint16 param2, uint8 clr) {
+ uint8 *dst = &s_ra1C4Tbl[1][0][0];
+ int loop = param2 * 8;
+ if (param2 > 256 || remaining < loop)
+ return false;
+ for (int i = 0; i < loop; i++) {
+ byte c = *src++;
+ remaining--;
+ *dst++ = (c >> 4) + clr;
+ *dst++ = (c & 0xF) + clr;
+ }
+ return true;
+}
+
+void smushDecodeRA1Block(byte *dst, const byte *src, int left, int top, int width, int height,
+ int pitch, int dataSize, uint8 param, uint16 parm2, int codec) {
+ const int mx = pitch;
+ const int my = height;
+ if (s_ra1C4Param != param) {
+ ra1Codec4GenTiles(param);
+ s_ra1C4Param = param;
+ }
+ int remaining = dataSize;
+ const byte *data = src;
+ if (parm2 > 0) {
+ if (!ra1Codec4LoadTiles(data, remaining, parm2, param)) {
+ warning("smushDecodeRA1Block: not enough data for tile load (parm2=%d)", parm2);
+ return;
+ }
+ }
+ for (int j = 0; j < width; j += 4) {
+ byte mask = 0, bits = 0;
+ int x = left + j;
+ for (int i = 0; i < height; i += 4) {
+ int y = top + i;
+ int bit = 0;
+ if (parm2 > 0) {
+ if (bits == 0) {
+ if (remaining < 1) return;
+ mask = *data++; remaining--; bits = 8;
+ }
+ bit = !!(mask & 0x80);
+ mask <<= 1; bits--;
+ }
+ if (remaining < 1) return;
+ byte idx = *data++; remaining--;
+ if (bit == 0 && idx == 0x80 && codec != 5)
+ continue;
+ if (y >= my || (y + 4) < 0 || (x + 4) < 0 || x >= mx)
+ continue;
+ const byte *gs = &s_ra1C4Tbl[bit][idx][0];
+ if (y >= 0 && x >= 0 && (y + 4) <= my && (x + 4) <= mx) {
+ for (int k = 0; k < 4; k++, gs += 4)
+ memcpy(dst + x + (y + k) * pitch, gs, 4);
+ } else {
+ for (int k = 0; k < 4; k++)
+ for (int l = 0; l < 4; l++, gs++) {
+ int yo = y + k, xo = x + l;
+ if (yo >= 0 && yo < my && xo >= 0 && xo < mx)
+ *(dst + yo * pitch + xo) = *gs;
+ }
+ }
+ }
+ }
+}
bool SmushPlayerRebel1::handleGameFrameBufferSelect(int codec, int width, int height) {
// RA1 sub-fullscreen frames render into _specialBuffer at their (left, top) offset position.
Commit: f854095da73d2b7f6579b8486f1b615251ee12a1
https://github.com/scummvm/scummvm/commit/f854095da73d2b7f6579b8486f1b615251ee12a1
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:47+02:00
Commit Message:
SCUMM: RA: Move RA1 auxiliary decoders into RA player
Changed paths:
engines/scumm/smush/smush_player.cpp
engines/scumm/smush/smush_player_ra1.cpp
diff --git a/engines/scumm/smush/smush_player.cpp b/engines/scumm/smush/smush_player.cpp
index 0ed39b3ae9d..07d97275f32 100644
--- a/engines/scumm/smush/smush_player.cpp
+++ b/engines/scumm/smush/smush_player.cpp
@@ -44,8 +44,6 @@
#include "scumm/smush/smush_player.h"
#include "scumm/insane/insane.h"
-#include "scumm/insane/insane_rebel2.h"
-#include "scumm/insane/insane_rebel1.h"
#include "audio/audiostream.h"
#include "audio/mixer.h"
@@ -893,149 +891,6 @@ byte *SmushPlayer::getVideoPalette() {
void smushDecodeRLE(byte *dst, const byte *src, int left, int top, int width, int height, int pitch);
void smushDecodeUncompressed(byte *dst, const byte *src, int left, int top, int width, int height, int pitch);
-/**
- * RA1 codec 21: Skip/copy line codec (FUN_10D41).
- *
- * Each line: [u16 lineSize] then alternating [u16 skip] [u16 copyLen]
- * followed by (copyLen+1) literal bytes. Destination advances by pitch
- * per line, source advances by lineSize+2 per line.
- */
-void smushDecodeRA1SkipCopy(byte *dst, const byte *src, int left, int top, int width, int height, int pitch) {
- dst += top * pitch + left;
-
- for (int row = 0; row < height; row++) {
- uint16 lineSize = READ_LE_UINT16(src);
- const byte *lineData = src + 2;
- const byte *lineEnd = lineData + lineSize;
- byte *dstRow = dst;
- int remaining = width;
-
- while (remaining > 0 && lineData < lineEnd) {
- // Read skip distance
- if (lineData + 2 > lineEnd)
- break;
- uint16 skip = READ_LE_UINT16(lineData);
- lineData += 2;
- dstRow += skip;
- remaining -= skip;
- if (remaining <= 0)
- break;
-
- // Read copy count (+1)
- if (lineData + 2 > lineEnd)
- break;
- uint16 copyLen = READ_LE_UINT16(lineData) + 1;
- lineData += 2;
-
- int toCopy = MIN<int>(copyLen, remaining);
- if (lineData + toCopy > lineEnd)
- toCopy = (int)(lineEnd - lineData);
- if (toCopy > 0) {
- memcpy(dstRow, lineData, toCopy);
- lineData += toCopy;
- dstRow += toCopy;
- remaining -= toCopy;
- }
- // If copyLen was clamped by remaining, skip rest of source
- if (copyLen > toCopy)
- lineData += (copyLen - toCopy);
- }
-
- src += lineSize + 2;
- dst += pitch;
- }
-}
-
-/**
- * RA1 codec 23: Additive line-update overlay (FUN_10B40).
- *
- * Each line starts with a 16-bit row payload size followed by repeated
- * [skip:u8][run:u8] pairs. Unlike codec 21, the run length is stored directly,
- * not minus one. Instead of copying bytes from the stream, the decoder adds
- * `(paletteBase - 0x30)` to the destination pixels already present in the
- * framebuffer.
- */
-void smushDecodeRA1AdditiveLineUpdate(byte *dst, const byte *src, int left, int top, int width, int height,
- int pitch, int bufWidth, int bufHeight, uint8 paletteBase) {
- const uint8 colorDelta = (uint8)(paletteBase - 0x30);
-
- for (int row = 0; row < height; row++) {
- const uint16 lineSize = READ_LE_UINT16(src);
- const byte *lineData = src + 2;
- const byte *lineEnd = lineData + lineSize;
- const int dstY = top + row;
- int srcX = 0;
-
- while (srcX < width && lineData < lineEnd) {
- const int skip = *lineData++;
- srcX += skip;
- if (srcX >= width || lineData >= lineEnd)
- break;
-
- const int runLength = (int)(*lineData++);
- const int dstStartX = left + srcX;
- const int dstEndX = dstStartX + runLength;
-
- if (dstY >= 0 && dstY < bufHeight) {
- const int clippedStartX = MAX(dstStartX, 0);
- const int clippedEndX = MIN(dstEndX, bufWidth);
- if (clippedStartX < clippedEndX) {
- byte *dstPixel = dst + dstY * pitch + clippedStartX;
- for (int x = clippedStartX; x < clippedEndX; x++, dstPixel++)
- *dstPixel = (byte)(*dstPixel + colorDelta);
- }
- }
-
- srcX += runLength;
- }
-
- src += lineSize + 2;
- }
-}
-
-/**
- * RA1 codec 2: Scatter/point draw (FUN_110D7).
- *
- * Draws individual pixels at accumulated offsets â used for starfield
- * backgrounds. Each 4-byte entry: [dx:int16_le, dy:uint8, pixel:uint8].
- * Position starts at (left, top) and accumulates (dx, dy) per entry.
- * Pixel is drawn if position is within buffer bounds.
- */
-void smushDecodeRA1Scatter(byte *dst, const byte *src, int left, int top, int bufWidth, int bufHeight, int pitch, int dataSize) {
- int curX = left;
- int curY = top;
-
- while (dataSize >= 4) {
- int16 dx = (int16)READ_LE_UINT16(src);
- uint8 dy = src[2];
- uint8 pixel = src[3];
- src += 4;
- dataSize -= 4;
-
- curX += dx;
- curY += dy;
-
- if (curX >= 0 && curY >= 0 && curX < bufWidth && curY < bufHeight) {
- dst[curY * pitch + curX] = pixel;
- }
- }
-}
-
-/**
- * RA1 codec 4/5: Block-based frame decoder.
- * Adapted from FFmpeg's libavcodec/sanm.c old_codec4() (LGPL 2.1+).
- *
- * Two tile tables (c4tbl[2][256][16]):
- * Table 0: Generated dither patterns from 16-color sub-palette
- * Table 1: Loaded from frame data (4bpp nibble-split tiles)
- *
- * Each 4x4 block is decoded by reading a bit flag (from mask bytes)
- * and an index byte. c4tbl[bit][index] gives the 16-pixel pattern.
- * Codec 4: index 0x80 means "skip block" (delta frame).
- * Codec 5: all indices are drawn (keyframe).
- */
-
-// Persistent tile table state for RA1 codec 4/5
void SmushPlayer::decodeFrameObject(int codec, const uint8 *src, int left, int top, int width, int height, int dataSize, uint8 ra1Param, uint16 ra1Parm2) {
if ((height == 242) && (width == 384)) {
int bufSize = 242 * 384;
diff --git a/engines/scumm/smush/smush_player_ra1.cpp b/engines/scumm/smush/smush_player_ra1.cpp
index e25d29c267b..7c43346d315 100644
--- a/engines/scumm/smush/smush_player_ra1.cpp
+++ b/engines/scumm/smush/smush_player_ra1.cpp
@@ -293,10 +293,85 @@ void smushDecodeRA1Transparent(byte *dst, const byte *src, int left, int top, in
} while (--height);
}
void smushDecodeRLEOpaque(byte *dst, const byte *src, int left, int top, int width, int height, int pitch);
-void smushDecodeRA1SkipCopy(byte *dst, const byte *src, int left, int top, int width, int height, int pitch);
+
+/**
+ * RA1 codec 21: Skip/copy line codec (FUN_10D41).
+ */
+void smushDecodeRA1SkipCopy(byte *dst, const byte *src, int left, int top, int width, int height, int pitch) {
+ dst += top * pitch + left;
+ for (int row = 0; row < height; row++) {
+ uint16 lineSize = READ_LE_UINT16(src);
+ const byte *lineData = src + 2;
+ const byte *lineEnd = lineData + lineSize;
+ byte *dstRow = dst;
+ int remaining = width;
+ while (remaining > 0 && lineData < lineEnd) {
+ if (lineData + 2 > lineEnd) break;
+ uint16 skip = READ_LE_UINT16(lineData); lineData += 2;
+ dstRow += skip; remaining -= skip;
+ if (remaining <= 0) break;
+ if (lineData + 2 > lineEnd) break;
+ uint16 copyLen = READ_LE_UINT16(lineData) + 1; lineData += 2;
+ int toCopy = MIN<int>(copyLen, remaining);
+ if (lineData + toCopy > lineEnd) toCopy = (int)(lineEnd - lineData);
+ if (toCopy > 0) { memcpy(dstRow, lineData, toCopy); lineData += toCopy; dstRow += toCopy; remaining -= toCopy; }
+ if (copyLen > toCopy) lineData += (copyLen - toCopy);
+ }
+ src += lineSize + 2;
+ dst += pitch;
+ }
+}
+
+/**
+ * RA1 codec 23: Additive line-update overlay (FUN_10B40).
+ */
void smushDecodeRA1AdditiveLineUpdate(byte *dst, const byte *src, int left, int top, int width, int height,
- int pitch, int bufWidth, int bufHeight, uint8 param);
-void smushDecodeRA1Scatter(byte *dst, const byte *src, int left, int top, int bufWidth, int bufHeight, int pitch, int dataSize);
+ int pitch, int bufWidth, int bufHeight, uint8 paletteBase) {
+ const uint8 colorDelta = (uint8)(paletteBase - 0x30);
+ for (int row = 0; row < height; row++) {
+ const uint16 lineSize = READ_LE_UINT16(src);
+ const byte *lineData = src + 2;
+ const byte *lineEnd = lineData + lineSize;
+ const int dstY = top + row;
+ int srcX = 0;
+ while (srcX < width && lineData < lineEnd) {
+ const int skip = *lineData++; srcX += skip;
+ if (srcX >= width || lineData >= lineEnd) break;
+ const int runLength = (int)(*lineData++);
+ const int dstStartX = left + srcX;
+ const int dstEndX = dstStartX + runLength;
+ if (dstY >= 0 && dstY < bufHeight) {
+ const int clippedStartX = MAX(dstStartX, 0);
+ const int clippedEndX = MIN(dstEndX, bufWidth);
+ if (clippedStartX < clippedEndX) {
+ byte *dstPixel = dst + dstY * pitch + clippedStartX;
+ for (int x = clippedStartX; x < clippedEndX; x++, dstPixel++)
+ *dstPixel = (byte)(*dstPixel + colorDelta);
+ }
+ }
+ srcX += runLength;
+ }
+ src += lineSize + 2;
+ }
+}
+
+/**
+ * RA1 codec 2: Scatter/point draw (FUN_110D7).
+ */
+void smushDecodeRA1Scatter(byte *dst, const byte *src, int left, int top, int bufWidth, int bufHeight, int pitch, int dataSize) {
+ int curX = left;
+ int curY = top;
+ while (dataSize >= 4) {
+ int16 dx = (int16)READ_LE_UINT16(src);
+ uint8 dy = src[2];
+ uint8 pixel = src[3];
+ src += 4; dataSize -= 4;
+ curX += dx; curY += dy;
+ if (curX >= 0 && curY >= 0 && curX < bufWidth && curY < bufHeight)
+ dst[curY * pitch + curX] = pixel;
+ }
+}
+
// RA1 codec 4/5: block-based dithered codec with 4x4 tile lookup tables
static uint8 s_ra1C4Tbl[2][256][16];
static uint16 s_ra1C4Param = 0xFFFF;
Commit: 31d2c9a5730d30851d078f3460f10ee7c73bf05d
https://github.com/scummvm/scummvm/commit/31d2c9a5730d30851d078f3460f10ee7c73bf05d
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:48+02:00
Commit Message:
SCUMM: RA: Move specific class definitions out of smush_player.h
Changed paths:
A engines/scumm/smush/smush_player_ra1.h
A engines/scumm/smush/smush_player_ra2.h
engines/scumm/insane/insane_rebel1.h
engines/scumm/insane/insane_rebel1_render.cpp
engines/scumm/scumm.cpp
engines/scumm/smush/smush_player.h
engines/scumm/smush/smush_player_ra1.cpp
engines/scumm/smush/smush_player_ra2.cpp
diff --git a/engines/scumm/insane/insane_rebel1.h b/engines/scumm/insane/insane_rebel1.h
index db6f84a3890..24ebbced021 100644
--- a/engines/scumm/insane/insane_rebel1.h
+++ b/engines/scumm/insane/insane_rebel1.h
@@ -26,11 +26,11 @@
#include "audio/mixer.h"
#include "common/events.h"
#include "scumm/insane/insane.h"
+#include "scumm/smush/smush_player_ra1.h"
namespace Scumm {
class ScummEngine_v7;
-class SmushPlayer;
class SmushFont;
// Simple sprite bank for RA1 NUT files (ANIM v1 with odd-alignment padding).
@@ -67,8 +67,6 @@ static const int16 kRA1FocalY = 25;
* Star Wars: Rebel Assault (RA1) game logic.
* Adapts RA2 Handler 7 (ship flight) physics for RA1's 384x242 resolution.
*/
-class SmushPlayerRebel1;
-
class InsaneRebel1 : public Insane, public Common::EventObserver {
public:
InsaneRebel1(ScummEngine_v7 *scumm);
diff --git a/engines/scumm/insane/insane_rebel1_render.cpp b/engines/scumm/insane/insane_rebel1_render.cpp
index 93fdc31dc07..dd34b22c87f 100644
--- a/engines/scumm/insane/insane_rebel1_render.cpp
+++ b/engines/scumm/insane/insane_rebel1_render.cpp
@@ -22,7 +22,7 @@
#include "common/system.h"
#include "scumm/scumm_v7.h"
-#include "scumm/smush/smush_player.h"
+#include "scumm/smush/smush_player_ra1.h"
#include "scumm/insane/insane_rebel1.h"
namespace Scumm {
diff --git a/engines/scumm/scumm.cpp b/engines/scumm/scumm.cpp
index 8363ec50108..3a89da5bf87 100644
--- a/engines/scumm/scumm.cpp
+++ b/engines/scumm/scumm.cpp
@@ -50,6 +50,8 @@
#include "scumm/imuse/imuse.h"
#include "scumm/imuse_digi/dimuse_engine.h"
#include "scumm/smush/smush_player.h"
+#include "scumm/smush/smush_player_ra1.h"
+#include "scumm/smush/smush_player_ra2.h"
#include "scumm/players/player_towns.h"
#include "scumm/insane/insane.h"
#include "scumm/insane/insane_rebel2.h"
diff --git a/engines/scumm/smush/smush_player.h b/engines/scumm/smush/smush_player.h
index 22f2665069e..980cfa26fa7 100644
--- a/engines/scumm/smush/smush_player.h
+++ b/engines/scumm/smush/smush_player.h
@@ -365,117 +365,6 @@ private:
void timerCallback();
};
-class SmushPlayerRebel1 : public SmushPlayer {
- friend class InsaneRebel1;
-public:
- SmushPlayerRebel1(ScummEngine_v7 *scumm, IMuseDigital *imuseDigital, Insane *insane);
- ~SmushPlayerRebel1() override;
-
-protected:
- void initGamePlayerFields() override;
- void destroyGamePlayerFields() override;
- void resetGameVideoState() override;
- void releaseGameVideoState() override;
- bool handleGameFetch(int32 subSize, Common::SeekableReadStream &b) override;
- bool handleGameTextResource(uint32 subType, int32 subSize, Common::SeekableReadStream &b) override;
- SmushFont *getGameFont(int font) override;
- void adjustGamePalette() override;
- bool handleGameAnimHeader(byte *headerContent) override;
- void handleGameParseNextFrame() override;
- bool handleGameFrameBufferSelect(int codec, int width, int height) override;
- bool handleGameDimensionOverride(int codec, int width, int height) override;
- bool handleGameAdjustCoords(int codec, int &left, int &top, int &width, int &height, int pitch, int *srcSkipY) override;
- bool handleGameCodecDecode(int codec, const uint8 *src, int left, int top, int width, int height, int pitch, int dataSize) override;
- bool handleGameStoreFrame() override;
- void handleGameFrameObjectPre(int codec, int left, int top, int width, int height, int dataSize) override;
- void handleGameFrameObjectPost(int codec, const byte *data, int32 dataSize, int left, int top, int width, int height) override;
- void handleGameFrameStart() override;
- void handleGameGost(int32 subSize, Common::SeekableReadStream &b) override;
- void handleGameProcessAudio(int16 feedSize) override;
- bool isInsaneGame() const override { return true; }
- void handleFrameObject(int32 subSize, Common::SeekableReadStream &b) override;
- void handleFrame(int32 frameSize, Common::SeekableReadStream &b) override;
- void handleGameUpdateScreen(const byte *src, int srcPitch, int width, int height) override;
-
-private:
- void ra1HandleGost(int32 subSize, Common::SeekableReadStream &b);
- SmushFont *ra1GetFont(int font);
- void ra1HandleText(int32 subSize, Common::SeekableReadStream &b);
-
- // RA1 clean frame buffer for delta source restoration
- byte *_ra1CleanFrame;
- int32 _ra1CleanFrameSize;
- bool _ra1HasCleanFrame;
-
- // RA1 OBJ overlay FOBJ â cockpit drawn once frame 0, re-rendered every frame
- byte *_ra1ObjOverlayData;
- int32 _ra1ObjOverlayDataSize;
- int _ra1ObjOverlayCodec;
- int _ra1ObjOverlayLeft;
- int _ra1ObjOverlayTop;
- int _ra1ObjOverlayWidth;
- int _ra1ObjOverlayHeight;
-
- // RA1 viewport scroll offset for interactive gameplay
- int _ra1ViewportOffsetX;
- int _ra1ViewportOffsetY;
-};
-
-class SmushPlayerRebel2 : public SmushPlayer {
-public:
- SmushPlayerRebel2(ScummEngine_v7 *scumm, IMuseDigital *imuseDigital, Insane *insane);
- ~SmushPlayerRebel2() override;
-
-protected:
- void initGamePlayerFields() override;
- void destroyGamePlayerFields() override;
- void initGameVideoState() override;
- void releaseGameVideoState() override;
- bool shouldPreserveFrameBuffer() const override { return true; }
- bool handleGameFetch(int32 subSize, Common::SeekableReadStream &b) override;
- bool handleGameTextRendering(const char *str, int fontId, int color, int pos_x, int pos_y, int left, int top, int width, int height, TextStyleFlags flg) override;
- bool shouldAlwaysShowSubtitles() const override { return true; }
- SmushFont *getGameFont(int font) override;
- void adjustGamePalette() override;
- bool handleGameAnimHeader(byte *headerContent) override;
- const char *getGameStringResource() const override { return "SYSTM/GAME.TRS"; }
- void handleGameParseNextFrame() override;
- bool shouldRouteAllIACTs() const override { return true; }
- bool handleGameFrameBufferSelect(int codec, int width, int height) override;
- bool handleGameDimensionOverride(int codec, int width, int height) override;
- bool handleGameAdjustCoords(int codec, int &left, int &top, int &width, int &height, int pitch, int *srcSkipY) override;
- bool handleGameCodecDecode(int codec, const uint8 *src, int left, int top, int width, int height, int pitch, int dataSize) override;
- bool handleGameStoreFrame() override;
- void handleGameFrameObjectPre(int codec, int left, int top, int width, int height, int dataSize) override;
- void handleGameFrameObjectPost(int codec, const byte *data, int32 dataSize, int left, int top, int width, int height) override;
- void handleGameFrameStart() override;
- bool handleGameSkipChunk(uint32 subType, int32 subSize, Common::SeekableReadStream &b) override;
- void handleGameGost(int32 subSize, Common::SeekableReadStream &b) override;
- void handleGameProcessAudio(int16 feedSize) override;
- bool isInsaneGame() const override { return true; }
- void handleGameLoad(int32 subSize, Common::SeekableReadStream &b) override;
-
-private:
- void handleLoad(int32 subSize, Common::SeekableReadStream &b);
- void ra2HandleTextResource(const char *str, int fontId, int color,
- int pos_x, int pos_y, int left, int top,
- int width, int height, TextStyleFlags flg);
- void ra2SelectFrameBuffer(int width, int height);
- bool ra2DecodeCodec(int codec, const uint8 *src, int left, int top,
- int width, int height, int pitch, int dataSize);
- void ra2StoreFobjData(int codec, const byte *data, int32 dataSize,
- int left, int top, int width, int height);
- void ra2HandleGost(int32 subSize, Common::SeekableReadStream &b);
-
- // LOAD chunk streaming buffer (embedded resource data)
- byte *_loadBuffer;
- int32 _loadBufferSize;
- int32 _loadBufferOffset;
- int32 _loadReadOffset;
- int16 _lastLoadChunkIdx;
- int16 _totalLoadChunks;
-};
-
} // End of namespace Scumm
#endif
diff --git a/engines/scumm/smush/smush_player_ra1.cpp b/engines/scumm/smush/smush_player_ra1.cpp
index 7c43346d315..00bd2535d11 100644
--- a/engines/scumm/smush/smush_player_ra1.cpp
+++ b/engines/scumm/smush/smush_player_ra1.cpp
@@ -30,7 +30,7 @@
#include "scumm/file.h"
#include "scumm/scumm_v7.h"
#include "scumm/smush/smush_font.h"
-#include "scumm/smush/smush_player.h"
+#include "scumm/smush/smush_player_ra1.h"
#include "scumm/insane/insane_rebel1.h"
diff --git a/engines/scumm/smush/smush_player_ra1.h b/engines/scumm/smush/smush_player_ra1.h
new file mode 100644
index 00000000000..cd072afcabf
--- /dev/null
+++ b/engines/scumm/smush/smush_player_ra1.h
@@ -0,0 +1,87 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#ifndef SCUMM_SMUSH_PLAYER_RA1_H
+#define SCUMM_SMUSH_PLAYER_RA1_H
+
+#include "scumm/smush/smush_player.h"
+
+namespace Scumm {
+
+class SmushPlayerRebel1 : public SmushPlayer {
+ friend class InsaneRebel1;
+public:
+ SmushPlayerRebel1(ScummEngine_v7 *scumm, IMuseDigital *imuseDigital, Insane *insane);
+ ~SmushPlayerRebel1() override;
+
+protected:
+ void initGamePlayerFields() override;
+ void destroyGamePlayerFields() override;
+ void resetGameVideoState() override;
+ void releaseGameVideoState() override;
+ bool handleGameFetch(int32 subSize, Common::SeekableReadStream &b) override;
+ bool handleGameTextResource(uint32 subType, int32 subSize, Common::SeekableReadStream &b) override;
+ SmushFont *getGameFont(int font) override;
+ void adjustGamePalette() override;
+ bool handleGameAnimHeader(byte *headerContent) override;
+ void handleGameParseNextFrame() override;
+ bool handleGameFrameBufferSelect(int codec, int width, int height) override;
+ bool handleGameDimensionOverride(int codec, int width, int height) override;
+ bool handleGameAdjustCoords(int codec, int &left, int &top, int &width, int &height, int pitch, int *srcSkipY) override;
+ bool handleGameCodecDecode(int codec, const uint8 *src, int left, int top, int width, int height, int pitch, int dataSize) override;
+ bool handleGameStoreFrame() override;
+ void handleGameFrameObjectPre(int codec, int left, int top, int width, int height, int dataSize) override;
+ void handleGameFrameObjectPost(int codec, const byte *data, int32 dataSize, int left, int top, int width, int height) override;
+ void handleGameFrameStart() override;
+ void handleGameGost(int32 subSize, Common::SeekableReadStream &b) override;
+ void handleGameProcessAudio(int16 feedSize) override;
+ bool isInsaneGame() const override { return true; }
+ void handleFrameObject(int32 subSize, Common::SeekableReadStream &b) override;
+ void handleFrame(int32 frameSize, Common::SeekableReadStream &b) override;
+ void handleGameUpdateScreen(const byte *src, int srcPitch, int width, int height) override;
+
+private:
+ void ra1HandleGost(int32 subSize, Common::SeekableReadStream &b);
+ SmushFont *ra1GetFont(int font);
+ void ra1HandleText(int32 subSize, Common::SeekableReadStream &b);
+
+ // RA1 clean frame buffer for delta source restoration
+ byte *_ra1CleanFrame;
+ int32 _ra1CleanFrameSize;
+ bool _ra1HasCleanFrame;
+
+ // RA1 OBJ overlay FOBJ â cockpit drawn once frame 0, re-rendered every frame
+ byte *_ra1ObjOverlayData;
+ int32 _ra1ObjOverlayDataSize;
+ int _ra1ObjOverlayCodec;
+ int _ra1ObjOverlayLeft;
+ int _ra1ObjOverlayTop;
+ int _ra1ObjOverlayWidth;
+ int _ra1ObjOverlayHeight;
+
+ // RA1 viewport scroll offset for interactive gameplay
+ int _ra1ViewportOffsetX;
+ int _ra1ViewportOffsetY;
+};
+
+} // End of namespace Scumm
+
+#endif
diff --git a/engines/scumm/smush/smush_player_ra2.cpp b/engines/scumm/smush/smush_player_ra2.cpp
index e59e6831254..2f5a47b25d1 100644
--- a/engines/scumm/smush/smush_player_ra2.cpp
+++ b/engines/scumm/smush/smush_player_ra2.cpp
@@ -32,7 +32,7 @@
#include "scumm/scumm_v7.h"
#include "scumm/smush/smush_font.h"
#include "scumm/smush/smush_multi_font.h"
-#include "scumm/smush/smush_player.h"
+#include "scumm/smush/smush_player_ra2.h"
#include "scumm/insane/insane.h"
#include "scumm/insane/insane_rebel2.h"
diff --git a/engines/scumm/smush/smush_player_ra2.h b/engines/scumm/smush/smush_player_ra2.h
new file mode 100644
index 00000000000..9e180a852a0
--- /dev/null
+++ b/engines/scumm/smush/smush_player_ra2.h
@@ -0,0 +1,86 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#ifndef SCUMM_SMUSH_PLAYER_RA2_H
+#define SCUMM_SMUSH_PLAYER_RA2_H
+
+#include "scumm/smush/smush_player.h"
+
+namespace Scumm {
+
+class SmushPlayerRebel2 : public SmushPlayer {
+public:
+ SmushPlayerRebel2(ScummEngine_v7 *scumm, IMuseDigital *imuseDigital, Insane *insane);
+ ~SmushPlayerRebel2() override;
+
+protected:
+ void initGamePlayerFields() override;
+ void destroyGamePlayerFields() override;
+ void initGameVideoState() override;
+ void releaseGameVideoState() override;
+ bool shouldPreserveFrameBuffer() const override { return true; }
+ bool handleGameFetch(int32 subSize, Common::SeekableReadStream &b) override;
+ bool handleGameTextRendering(const char *str, int fontId, int color, int pos_x, int pos_y, int left, int top, int width, int height, TextStyleFlags flg) override;
+ bool shouldAlwaysShowSubtitles() const override { return true; }
+ SmushFont *getGameFont(int font) override;
+ void adjustGamePalette() override;
+ bool handleGameAnimHeader(byte *headerContent) override;
+ const char *getGameStringResource() const override { return "SYSTM/GAME.TRS"; }
+ void handleGameParseNextFrame() override;
+ bool shouldRouteAllIACTs() const override { return true; }
+ bool handleGameFrameBufferSelect(int codec, int width, int height) override;
+ bool handleGameDimensionOverride(int codec, int width, int height) override;
+ bool handleGameAdjustCoords(int codec, int &left, int &top, int &width, int &height, int pitch, int *srcSkipY) override;
+ bool handleGameCodecDecode(int codec, const uint8 *src, int left, int top, int width, int height, int pitch, int dataSize) override;
+ bool handleGameStoreFrame() override;
+ void handleGameFrameObjectPre(int codec, int left, int top, int width, int height, int dataSize) override;
+ void handleGameFrameObjectPost(int codec, const byte *data, int32 dataSize, int left, int top, int width, int height) override;
+ void handleGameFrameStart() override;
+ bool handleGameSkipChunk(uint32 subType, int32 subSize, Common::SeekableReadStream &b) override;
+ void handleGameGost(int32 subSize, Common::SeekableReadStream &b) override;
+ void handleGameProcessAudio(int16 feedSize) override;
+ bool isInsaneGame() const override { return true; }
+ void handleGameLoad(int32 subSize, Common::SeekableReadStream &b) override;
+
+private:
+ void handleLoad(int32 subSize, Common::SeekableReadStream &b);
+ void ra2HandleTextResource(const char *str, int fontId, int color,
+ int pos_x, int pos_y, int left, int top,
+ int width, int height, TextStyleFlags flg);
+ void ra2SelectFrameBuffer(int width, int height);
+ bool ra2DecodeCodec(int codec, const uint8 *src, int left, int top,
+ int width, int height, int pitch, int dataSize);
+ void ra2StoreFobjData(int codec, const byte *data, int32 dataSize,
+ int left, int top, int width, int height);
+ void ra2HandleGost(int32 subSize, Common::SeekableReadStream &b);
+
+ // LOAD chunk streaming buffer (embedded resource data)
+ byte *_loadBuffer;
+ int32 _loadBufferSize;
+ int32 _loadBufferOffset;
+ int32 _loadReadOffset;
+ int16 _lastLoadChunkIdx;
+ int16 _totalLoadChunks;
+};
+
+} // End of namespace Scumm
+
+#endif
Commit: 3dbe2d7eb7988fca2d5b00cf10dc7e8246b822c5
https://github.com/scummvm/scummvm/commit/3dbe2d7eb7988fca2d5b00cf10dc7e8246b822c5
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:48+02:00
Commit Message:
SCUMM: RA2: Remove redundant code
Changed paths:
engines/scumm/insane/insane_rebel2.cpp
engines/scumm/insane/insane_rebel2.h
engines/scumm/insane/insane_rebel2_menu.cpp
engines/scumm/insane/insane_rebel2_render.cpp
diff --git a/engines/scumm/insane/insane_rebel2.cpp b/engines/scumm/insane/insane_rebel2.cpp
index ad54fabdcc7..458c0b75437 100644
--- a/engines/scumm/insane/insane_rebel2.cpp
+++ b/engines/scumm/insane/insane_rebel2.cpp
@@ -98,10 +98,6 @@ InsaneRebel2::InsaneRebel2(ScummEngine_v7 *scumm) {
_rebelDetailMode = 1;
_smush_cockpitNut = new NutRenderer(_vm, "SYSTM/DISPFONT.NUT");
- // Load SMALFONT.NUT for HUD score/lives rendering (DAT_00482200 equivalent)
- // This is used by FUN_0041c012 to render the score in the status bar
- _smush_dispfontNut = new NutRenderer(_vm, "SYSTM/SMALFONT.NUT");
-
// Load DIHIFONT.NUT for in-video messages/subtitles (Opcode 9)
_rebelMsgFont = new SmushFont(_vm, "SYSTM/DIHIFONT.NUT", true);
@@ -115,16 +111,9 @@ InsaneRebel2::InsaneRebel2(ScummEngine_v7 *scumm) {
_smush_smalfontNut = new NutRenderer(_vm, "SYSTM/SMALFONT.NUT");
_smush_titlefontNut = new NutRenderer(_vm, "SYSTM/TITLFONT.NUT");
- // SmushFont for menu text rendering - uses SMALFONT with proper drawString support
- _menuFont = new SmushFont(_vm, "SYSTM/SMALFONT.NUT", true);
_pauseOverlayActive = false;
memset(_savedPausePalette, 0, sizeof(_savedPausePalette));
- // MSTOVER.NUT - Mouse Over background overlay (NOT a cursor!)
- // This is loaded into DAT_0047aba8 and used as a background overlay via FUN_004236e0
- // The original game uses the standard Windows arrow cursor (IDC_ARROW via LoadCursorA)
- _smush_mouseoverNut = new NutRenderer(_vm, "SYSTM/MSTOVER.NUT");
-
_enemies.clear();
_rebelHandler = 0; // Not set yet - will be set by IACT opcode 6
_rebelLevelType = 0; // Level type from Opcode 6 par3, determines HUD sprite variant
@@ -503,12 +492,9 @@ InsaneRebel2::~InsaneRebel2() {
terminateAudio();
freeSfx();
delete _rebelMsgFont;
- delete _menuFont;
- delete _smush_dispfontNut;
delete _smush_talkfontNut;
delete _smush_smalfontNut;
delete _smush_titlefontNut;
- delete _smush_mouseoverNut;
// Clean up Handler 8 ship sprites
delete _shipSprite;
diff --git a/engines/scumm/insane/insane_rebel2.h b/engines/scumm/insane/insane_rebel2.h
index bdc246cb85a..90b8c162d29 100644
--- a/engines/scumm/insane/insane_rebel2.h
+++ b/engines/scumm/insane/insane_rebel2.h
@@ -102,6 +102,10 @@ public:
int processMenuInput();
// Format-code-aware string rendering (^fNN=font, ^cNNN=color)
+ // parseFormatCode: advances str past ^fNN/^cNNN/^^/^l codes.
+ // Returns: fontIdx (>=0) on font change, -2 on color/newline, -1 on no match.
+ static int parseFormatCode(const char *&str, int &outColor);
+
int getMenuStringWidth(const char *str) const;
void drawMenuString(byte *renderBitmap, const char *str, int x, int y, int defaultColor = 1);
void drawMenuStringCentered(byte *renderBitmap, const char *str, int cx, int y, int defaultColor = 1);
@@ -455,10 +459,6 @@ public:
NutRenderer *_smush_cockpitNut;
- // Font used for HUD score/lives/damage display (SMALFONT.NUT)
- // DAT_00482200 equivalent - used by FUN_0041c012 for status bar rendering
- NutRenderer *_smush_dispfontNut;
-
// Font used for opcode 9 text/subtitle rendering (DIHIFONT / TALKFONT)
SmushFont *_rebelMsgFont;
@@ -472,17 +472,10 @@ public:
NutRenderer *_smush_smalfontNut; // Font 1 - small font for ^f01 switching
NutRenderer *_smush_titlefontNut; // Font 2 - title font
- // SmushFont for menu text rendering (uses SMALFONT.NUT with proper string drawing)
- SmushFont *_menuFont;
-
// Saved palette for pause overlay restoration (FUN_405A21)
byte _savedPausePalette[768];
bool _pauseOverlayActive;
- // MSTOVER.NUT - Mouse Over background overlay (NOT a cursor!)
- // Loaded into DAT_0047aba8 and rendered via FUN_004236e0 as background
- NutRenderer *_smush_mouseoverNut;
-
bool _introCursorPushed; // true when we've pushed an invisible cursor for intro
diff --git a/engines/scumm/insane/insane_rebel2_menu.cpp b/engines/scumm/insane/insane_rebel2_menu.cpp
index 15151d30504..696589e9133 100644
--- a/engines/scumm/insane/insane_rebel2_menu.cpp
+++ b/engines/scumm/insane/insane_rebel2_menu.cpp
@@ -235,122 +235,20 @@ void InsaneRebel2::drawMenuItems(byte *renderBitmap, int pitch, int width, int h
const int itemBaseY = numItems * -5 + 0x68;
const int itemSpacing = 10;
- // -------------------------------------------------------------------
- // Font system - Emulates linked list from FUN_00403bd0
- // -------------------------------------------------------------------
- // Font 0 (^f00): TALKFONT.NUT
- // Font 1 (^f01): SMALFONT.NUT (menu items)
- // Font 2 (^f02): TITLFONT.NUT (title)
- NutRenderer *fonts[3] = {
- _smush_talkfontNut,
- _smush_smalfontNut,
- _smush_titlefontNut
- };
-
+ NutRenderer *fonts[3] = { _smush_talkfontNut, _smush_smalfontNut, _smush_titlefontNut };
NutRenderer *defaultFont = fonts[0] ? fonts[0] : _smush_smalfontNut;
- if (!defaultFont) {
- debug(1, "drawMenuItems: no fonts available!");
+ if (!defaultFont)
return;
- }
Common::Rect clipRect(0, 0, _vm->_screenWidth, _vm->_screenHeight);
int actualPitch = _vm->_screenWidth;
- // -------------------------------------------------------------------
- // Format code parser - Emulates FUN_00434d10 / FUN_00433da0
- // -------------------------------------------------------------------
- // ^^ = literal ^, ^fNN = font switch, ^cNNN = color code, ^l = newline
- // Fixed-width format codes: ^fNN (2-digit font), ^cNNN (3-digit color)
- auto parseFormatCode = [&](const char *&str, int &outColor) -> int {
- if (*str != '^')
- return -1;
-
- const char *p = str + 1;
- if (*p == '^') {
- str = p;
- return -1;
- }
- if (*p == 'f') {
- p++;
- int fontIdx = (*p >= '0' && *p <= '9') ? (*p++ - '0') : 0;
- fontIdx = fontIdx * 10 + ((*p >= '0' && *p <= '9') ? (*p++ - '0') : 0);
- str = p;
- return (fontIdx >= 0 && fontIdx < 3) ? fontIdx : 0;
- }
- if (*p == 'c') {
- p++;
- int color = 0;
- for (int d = 0; d < 3 && *p >= '0' && *p <= '9'; d++)
- color = color * 10 + (*p++ - '0');
- str = p;
- outColor = color;
- return -2;
- }
- if (*p == 'l') {
- str = p + 1;
- return -2;
- }
- return -1;
- };
-
- // String width calculation - Emulates FUN_00433da0
auto getStringWidth = [&](const char *str) -> int {
- int w = 0;
- NutRenderer *curFont = defaultFont;
- int curColor = -1;
-
- while (*str) {
- int fontChange = parseFormatCode(str, curColor);
- if (fontChange >= 0) {
- curFont = fonts[fontChange] ? fonts[fontChange] : defaultFont;
- continue;
- }
- if (fontChange == -2)
- continue;
-
- byte c = (byte)*str++;
- if (c >= 'a' && c <= 'z')
- c = c - 'a' + 'A';
- if (curFont && c < curFont->getNumChars()) {
- w += curFont->getCharWidth(c);
- }
- }
- return w;
+ return getMenuStringWidth(str);
};
- // String rendering - Emulates FUN_00434d10
- // Codec 44 color substitution: font pixels with value 1 â ^cNNN color
auto drawString = [&](const char *str, int x, int y) {
- NutRenderer *curFont = defaultFont;
- int curColor = 1;
-
- while (*str) {
- int fontChange = parseFormatCode(str, curColor);
- if (fontChange >= 0) {
- curFont = fonts[fontChange] ? fonts[fontChange] : defaultFont;
- continue;
- }
- if (fontChange == -2)
- continue;
-
- byte c = (byte)*str++;
- if (c >= 'a' && c <= 'z')
- c = c - 'a' + 'A';
-
- if (!curFont)
- continue;
- int numChars = curFont->getNumChars();
- if (c >= numChars)
- continue;
-
- int charW = curFont->getCharWidth(c);
-
- if (x >= 0 && y >= 0 && charW > 0) {
- curFont->drawCharV7(renderBitmap, clipRect, x, y, actualPitch, curColor,
- kStyleAlignLeft, c, false, false);
- }
- x += charW;
- }
+ drawMenuString(renderBitmap, str, x, y, 1);
};
// -------------------------------------------------------------------
@@ -424,7 +322,33 @@ void InsaneRebel2::drawMenuItems(byte *renderBitmap, int pitch, int width, int h
}
}
-// getMenuStringWidth -- Format-code-aware string width (^fNN, ^cNNN, ^^).
+// parseFormatCode -- Shared ^fNN/^cNNN/^^/^l parser.
+// Returns: fontIdx (>=0) on font change, -2 on color/newline, -1 on no match.
+int InsaneRebel2::parseFormatCode(const char *&str, int &outColor) {
+ if (*str != '^')
+ return -1;
+ const char *p = str + 1;
+ if (*p == '^') { str = p; return -1; }
+ if (*p == 'f') {
+ p++;
+ int idx = 0;
+ while (*p >= '0' && *p <= '9') { idx = idx * 10 + (*p - '0'); p++; }
+ str = p;
+ return (idx >= 0 && idx < 3) ? idx : 0;
+ }
+ if (*p == 'c') {
+ p++;
+ int color = 0;
+ while (*p >= '0' && *p <= '9') { color = color * 10 + (*p - '0'); p++; }
+ str = p;
+ outColor = color;
+ return -2;
+ }
+ if (*p == 'l') { str = p + 1; return -2; }
+ return -1;
+}
+
+// getMenuStringWidth -- Format-code-aware string width.
int InsaneRebel2::getMenuStringWidth(const char *str) const {
NutRenderer *fonts[3] = { _smush_talkfontNut, _smush_smalfontNut, _smush_titlefontNut };
NutRenderer *defaultFont = fonts[0] ? fonts[0] : _smush_smalfontNut;
@@ -433,36 +357,20 @@ int InsaneRebel2::getMenuStringWidth(const char *str) const {
int w = 0;
NutRenderer *curFont = defaultFont;
+ int dummyColor = 0;
while (*str) {
- if (*str == '^') {
- const char *p = str + 1;
- if (*p == '^') { str = p + 1; continue; }
- if (*p == 'f') {
- p++;
- int idx = (*p >= '0' && *p <= '9') ? (*p++ - '0') : 0;
- idx = idx * 10 + ((*p >= '0' && *p <= '9') ? (*p++ - '0') : 0);
- curFont = (idx >= 0 && idx < 3 && fonts[idx]) ? fonts[idx] : defaultFont;
- str = p;
- continue;
- }
- if (*p == 'c') {
- p++;
- for (int d = 0; d < 3 && *p >= '0' && *p <= '9'; d++) p++;
- str = p;
- continue;
- }
- if (*p == 'l') { str = p + 1; continue; }
- }
+ int fc = parseFormatCode(str, dummyColor);
+ if (fc >= 0) { curFont = (fonts[fc] ? fonts[fc] : defaultFont); continue; }
+ if (fc == -2) continue;
byte c = (byte)*str++;
- if (c >= 'a' && c <= 'z')
- c = c - 'a' + 'A';
+ if (c >= 'a' && c <= 'z') c = c - 'a' + 'A';
if (curFont && c < curFont->getNumChars())
w += curFont->getCharWidth(c);
}
return w;
}
-// Format-code-aware string rendering at (x, y)
+// Format-code-aware string rendering at (x, y).
void InsaneRebel2::drawMenuString(byte *renderBitmap, const char *str, int x, int y, int defaultColor) {
NutRenderer *fonts[3] = { _smush_talkfontNut, _smush_smalfontNut, _smush_titlefontNut };
NutRenderer *defaultFont = fonts[0] ? fonts[0] : _smush_smalfontNut;
@@ -475,35 +383,12 @@ void InsaneRebel2::drawMenuString(byte *renderBitmap, const char *str, int x, in
NutRenderer *curFont = defaultFont;
int curColor = defaultColor;
while (*str) {
- if (*str == '^') {
- const char *p = str + 1;
- if (*p == '^') { str = p + 1; continue; }
- if (*p == 'f') {
- p++;
- int idx = (*p >= '0' && *p <= '9') ? (*p++ - '0') : 0;
- idx = idx * 10 + ((*p >= '0' && *p <= '9') ? (*p++ - '0') : 0);
- curFont = (idx >= 0 && idx < 3 && fonts[idx]) ? fonts[idx] : defaultFont;
- str = p;
- continue;
- }
- if (*p == 'c') {
- p++;
- int color = 0;
- for (int d = 0; d < 3 && *p >= '0' && *p <= '9'; d++)
- color = color * 10 + (*p++ - '0');
- curColor = color;
- str = p;
- continue;
- }
- if (*p == 'l') { str = p + 1; continue; }
- }
+ int fc = parseFormatCode(str, curColor);
+ if (fc >= 0) { curFont = (fonts[fc] ? fonts[fc] : defaultFont); continue; }
+ if (fc == -2) continue;
byte c = (byte)*str++;
- if (c >= 'a' && c <= 'z')
- c = c - 'a' + 'A';
- if (!curFont)
- continue;
- if (c >= curFont->getNumChars())
- continue;
+ if (c >= 'a' && c <= 'z') c = c - 'a' + 'A';
+ if (!curFont || c >= curFont->getNumChars()) continue;
int charW = curFont->getCharWidth(c);
if (x >= 0 && y >= 0 && charW > 0)
curFont->drawCharV7(renderBitmap, clipRect, x, y, pitch, curColor,
diff --git a/engines/scumm/insane/insane_rebel2_render.cpp b/engines/scumm/insane/insane_rebel2_render.cpp
index 72ad585eed4..3f3dbd37963 100644
--- a/engines/scumm/insane/insane_rebel2_render.cpp
+++ b/engines/scumm/insane/insane_rebel2_render.cpp
@@ -2700,29 +2700,11 @@ void InsaneRebel2::renderTextOverlay(byte *renderBitmap, int pitch, int width, i
Common::Rect clipRect(0, 0, width, height);
- // Format code parser (same as drawMenuItems / FUN_00434d10)
+ // Wrapper around shared parseFormatCode that also updates curFont
auto parseFormat = [&](const char *&s, NutRenderer *&curFont, int &curColor) {
- if (*s != '^')
- return false;
- const char *p = s + 1;
- if (*p == '^') { s = p; return false; }
- if (*p == 'f') {
- p++;
- int idx = 0;
- while (*p >= '0' && *p <= '9') { idx = idx * 10 + (*p - '0'); p++; }
- s = p;
- curFont = (idx >= 0 && idx < 3 && fonts[idx]) ? fonts[idx] : defaultFont;
- return true;
- }
- if (*p == 'c') {
- p++;
- int col = 0;
- while (*p >= '0' && *p <= '9') { col = col * 10 + (*p - '0'); p++; }
- s = p;
- curColor = col;
- return true;
- }
- return false;
+ int fc = parseFormatCode(s, curColor);
+ if (fc >= 0) { curFont = (fonts[fc] ? fonts[fc] : defaultFont); return true; }
+ return fc == -2;
};
// The TRS parser joins multi-line strings with spaces (stripping \n//),
Commit: 3350b7b032ee4cb74def671c2581c2c26971f3b1
https://github.com/scummvm/scummvm/commit/3350b7b032ee4cb74def671c2581c2c26971f3b1
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:48+02:00
Commit Message:
SCUMM: RA2: Reduce StringResource change impact
Changed paths:
engines/scumm/smush/smush_player.cpp
engines/scumm/smush/smush_player.h
engines/scumm/smush/smush_player_ra2.cpp
engines/scumm/smush/smush_player_ra2.h
engines/scumm/string_v7.cpp
diff --git a/engines/scumm/smush/smush_player.cpp b/engines/scumm/smush/smush_player.cpp
index 07d97275f32..cd71a780ff2 100644
--- a/engines/scumm/smush/smush_player.cpp
+++ b/engines/scumm/smush/smush_player.cpp
@@ -56,10 +56,10 @@
namespace Scumm {
-static const int MAX_STRINGS = 800; // RA2 has ~658 strings
+static const int MAX_STRINGS = 200;
static const int ETRS_HEADER_LENGTH = 16;
-class StringResource {
+class StringResourceImpl : public StringResource {
private:
struct {
@@ -70,28 +70,24 @@ private:
int _nbStrings;
int _lastId;
const char *_lastString;
- bool _preserveNewlines;
public:
- StringResource() :
+ StringResourceImpl() :
_nbStrings(0),
_lastId(-1),
- _lastString(nullptr),
- _preserveNewlines(false) {
+ _lastString(nullptr) {
for (int i = 0; i < MAX_STRINGS; i++) {
_strings[i].id = 0;
_strings[i].string = nullptr;
}
}
- ~StringResource() {
+ ~StringResourceImpl() override {
for (int32 i = 0; i < _nbStrings; i++) {
delete[] _strings[i].string;
}
}
- void setPreserveNewlines(bool preserve) { _preserveNewlines = preserve; }
-
bool init(char *buffer, int32 length) {
char *def_start = strchr(buffer, '#');
while (def_start != nullptr) {
@@ -142,24 +138,7 @@ public:
}
data_end -= 2;
- // Handle empty entries (e.g., RA2 TRS files have entries with no content)
- if (data_end <= data_start) {
- // Skip this entry - no content
- def_start = strchr(def_end + 1, '#');
- continue;
- }
-
- // Strip leading // from first line (RA2 format uses // prefix for content)
- if (data_start[0] == '/' && data_start[1] == '/') {
- data_start += 2;
- }
-
- // Recalculate length after stripping
- if (data_end <= data_start) {
- def_start = strchr(def_end + 1, '#');
- continue;
- }
-
+ assert(data_end > data_start);
char *value = new char[data_end - data_start + 1];
assert(value);
memcpy(value, data_start, data_end - data_start);
@@ -171,20 +150,11 @@ public:
line_start = line_end+1;
if (line_start[0] == '/' && line_start[1] == '/') {
line_start += 2;
- // RA2: preserve newlines for multi-line TRES text
- // (credits, cast lists). Other games join with spaces.
- if (_preserveNewlines) {
- if (line_end[-1] == '\r')
- line_end[-1] = '\n';
- // else line_end already points to '\n'
- memmove(line_end + 1, line_start, strlen(line_start)+1);
- } else {
- if (line_end[-1] == '\r')
- line_end[-1] = ' ';
- else
- *line_end++ = ' ';
- memmove(line_end, line_start, strlen(line_start)+1);
- }
+ if (line_end[-1] == '\r')
+ line_end[-1] = ' ';
+ else
+ *line_end++ = ' ';
+ memmove(line_end, line_start, strlen(line_start)+1);
}
}
_strings[_nbStrings].id = id;
@@ -195,7 +165,7 @@ public:
return true;
}
- const char *get(int id) {
+ const char *get(int id) override {
if (id == _lastId) {
return _lastString;
}
@@ -215,16 +185,14 @@ public:
};
static StringResource *getStrings(ScummEngine *vm, const char *file, bool is_encoded) {
- debugC(DEBUG_SMUSH, "getStrings: trying to read text resources from %s", file);
+ debugC(DEBUG_SMUSH, "trying to read text resources from %s", file);
ScummFile *theFile = vm->instantiateScummFile();
vm->openFile(*theFile, file);
if (!theFile->isOpen()) {
- debugC(DEBUG_SMUSH, "getStrings: Failed to open %s", file);
delete theFile;
return 0;
}
- debugC(DEBUG_SMUSH, "getStrings: Successfully opened %s", file);
int32 length = theFile->size();
char *filebuffer = new char [length + 1];
assert(filebuffer);
@@ -241,9 +209,8 @@ static StringResource *getStrings(ScummEngine *vm, const char *file, bool is_enc
}
filebuffer[length] = '\0';
}
- StringResource *sr = new StringResource;
+ StringResourceImpl *sr = new StringResourceImpl;
assert(sr);
- sr->setPreserveNewlines(vm->_game.id == GID_REBEL2);
sr->init(filebuffer, length);
delete[] filebuffer;
return sr;
@@ -1202,11 +1169,9 @@ void SmushPlayer::handleAnimHeader(int32 subSize, Common::SeekableReadStream &b)
void SmushPlayer::setupAnim(const char *file) {
if (_insanity) {
- const char *gameStringResource = getGameStringResource();
- if (gameStringResource) {
- _strings = getStrings(_vm, gameStringResource, true);
- } else if (!((_vm->_game.features & GF_DEMO) && (_vm->_game.platform == Common::kPlatformDOS))) {
- readString("mineroad.trs");
+ if (!handleGameSetupStrings()) {
+ if (!((_vm->_game.features & GF_DEMO) && (_vm->_game.platform == Common::kPlatformDOS)))
+ readString("mineroad.trs");
}
} else
readString(file);
diff --git a/engines/scumm/smush/smush_player.h b/engines/scumm/smush/smush_player.h
index 980cfa26fa7..b84d1880afb 100644
--- a/engines/scumm/smush/smush_player.h
+++ b/engines/scumm/smush/smush_player.h
@@ -93,7 +93,11 @@ class ScummEngine_v7;
class SmushFont;
class SmushMultiFont;
class SmushMixer;
-class StringResource;
+class StringResource {
+public:
+ virtual ~StringResource() {}
+ virtual const char *get(int id) = 0;
+};
class SmushDeltaBlocksDecoder;
class SmushDeltaGlyphsDecoder;
class IMuseDigital;
@@ -304,7 +308,7 @@ protected:
virtual SmushFont *getGameFont(int font) { return nullptr; }
virtual void adjustGamePalette() {}
virtual bool handleGameAnimHeader(byte *headerContent) { return false; }
- virtual const char *getGameStringResource() const { return nullptr; }
+ virtual bool handleGameSetupStrings() { return false; }
virtual void handleGameParseNextFrame() {}
virtual bool shouldRouteAllIACTs() const { return false; }
virtual bool handleGameFrameBufferSelect(int codec, int width, int height) { return false; }
diff --git a/engines/scumm/smush/smush_player_ra2.cpp b/engines/scumm/smush/smush_player_ra2.cpp
index 2f5a47b25d1..c256493fec5 100644
--- a/engines/scumm/smush/smush_player_ra2.cpp
+++ b/engines/scumm/smush/smush_player_ra2.cpp
@@ -226,6 +226,160 @@ SmushFont *SmushPlayerRebel2::getGameFont(int font) {
return _sf[font];
}
+// ---------------------------------------------------------------------------
+// RA2 string resource loading â separate from shared StringResource
+// ---------------------------------------------------------------------------
+
+// RA2 TRS format differences from standard SCUMM:
+// - Up to ~658 entries (standard only supports 200)
+// - Entries can be empty (no content between header and next #)
+// - Content lines prefixed with //
+// - Multi-line entries preserve newlines (credits, cast lists)
+
+static const int RA2_MAX_STRINGS = 800;
+static const int RA2_ETRS_HEADER_LENGTH = 16;
+
+struct RA2StringEntry {
+ int id;
+ char *string;
+};
+
+class StringResourceRA2 : public StringResource {
+ RA2StringEntry _ra2Strings[RA2_MAX_STRINGS];
+ int _ra2NbStrings;
+ int _ra2LastId;
+ const char *_ra2LastString;
+
+public:
+ StringResourceRA2() : _ra2NbStrings(0), _ra2LastId(-1), _ra2LastString(nullptr) {
+ for (int i = 0; i < RA2_MAX_STRINGS; i++) {
+ _ra2Strings[i].id = 0;
+ _ra2Strings[i].string = nullptr;
+ }
+ }
+
+ ~StringResourceRA2() override {
+ for (int i = 0; i < _ra2NbStrings; i++)
+ delete[] _ra2Strings[i].string;
+ }
+
+ bool init(char *buffer, int32 length) {
+ char *def_start = strchr(buffer, '#');
+ while (def_start != nullptr) {
+ char *def_end = strchr(def_start, '\n');
+ if (!def_end) break;
+
+ char *id_end = def_end;
+ while (id_end >= def_start && !Common::isDigit(*(id_end-1)))
+ id_end--;
+ if (id_end <= def_start) { def_start = strchr(def_end + 1, '#'); continue; }
+
+ char *id_start = id_end;
+ while (Common::isDigit(*(id_start - 1)))
+ id_start--;
+
+ char idstring[32];
+ memcpy(idstring, id_start, id_end - id_start);
+ idstring[id_end - id_start] = 0;
+ int32 id = atoi(idstring);
+
+ char *data_start = def_end;
+ while (*data_start == '\n' || *data_start == '\r')
+ data_start++;
+
+ char *data_end = data_start;
+ while (1) {
+ if (data_end[-2] == '\r' && data_end[-1] == '\n' && data_end[0] == '\r' && data_end[1] == '\n') break;
+ if (data_end[-2] == '\n' && data_end[-1] == '\n') break;
+ if (data_end[-2] == '\r' && data_end[-1] == '\n' && data_end[0] == '#') break;
+ data_end++;
+ if (data_end >= buffer + length) { data_end = buffer + length; break; }
+ }
+ data_end -= 2;
+
+ // Skip empty entries
+ if (data_end <= data_start) { def_start = strchr(def_end + 1, '#'); continue; }
+
+ // Strip leading // prefix
+ if (data_start[0] == '/' && data_start[1] == '/')
+ data_start += 2;
+ if (data_end <= data_start) { def_start = strchr(def_end + 1, '#'); continue; }
+
+ char *value = new char[data_end - data_start + 1];
+ memcpy(value, data_start, data_end - data_start);
+ value[data_end - data_start] = 0;
+
+ // Preserve newlines for multi-line TRES text (credits, cast lists)
+ char *line_start = value;
+ char *line_end;
+ while ((line_end = strchr(line_start, '\n'))) {
+ line_start = line_end + 1;
+ if (line_start[0] == '/' && line_start[1] == '/') {
+ line_start += 2;
+ if (line_end[-1] == '\r')
+ line_end[-1] = '\n';
+ memmove(line_end + 1, line_start, strlen(line_start) + 1);
+ }
+ }
+
+ if (_ra2NbStrings < RA2_MAX_STRINGS) {
+ _ra2Strings[_ra2NbStrings].id = id;
+ _ra2Strings[_ra2NbStrings].string = value;
+ _ra2NbStrings++;
+ } else {
+ delete[] value;
+ }
+ def_start = strchr(data_end + 2, '#');
+ }
+ return true;
+ }
+
+ const char *get(int id) override {
+ if (id == _ra2LastId)
+ return _ra2LastString;
+ for (int i = 0; i < _ra2NbStrings; i++) {
+ if (_ra2Strings[i].id == id) {
+ _ra2LastId = id;
+ _ra2LastString = _ra2Strings[i].string;
+ return _ra2LastString;
+ }
+ }
+ warning("StringResourceRA2: invalid string id : %d", id);
+ _ra2LastId = -1;
+ _ra2LastString = "unknown string";
+ return _ra2LastString;
+ }
+};
+
+bool SmushPlayerRebel2::handleGameSetupStrings() {
+ ScummFile *theFile = _vm->instantiateScummFile();
+ _vm->openFile(*theFile, "SYSTM/GAME.TRS");
+ if (!theFile->isOpen()) {
+ delete theFile;
+ return true; // handled (no strings available)
+ }
+ int32 length = theFile->size();
+ char *filebuffer = new char[length + 1];
+ theFile->read(filebuffer, length);
+ filebuffer[length] = 0;
+ theFile->close();
+ delete theFile;
+
+ if (READ_BE_UINT32(filebuffer) == MKTAG('E','T','R','S')) {
+ assert(length > RA2_ETRS_HEADER_LENGTH);
+ length -= RA2_ETRS_HEADER_LENGTH;
+ for (int i = 0; i < length; ++i)
+ filebuffer[i] = filebuffer[i + RA2_ETRS_HEADER_LENGTH] ^ 0xCC;
+ filebuffer[length] = '\0';
+ }
+
+ StringResourceRA2 *sr = new StringResourceRA2;
+ sr->init(filebuffer, length);
+ delete[] filebuffer;
+ _strings = sr;
+ return true;
+}
+
/**
* Reset XPAL delta palette from the current base palette.
* Prevents stale delta values from a previous video corrupting the palette.
diff --git a/engines/scumm/smush/smush_player_ra2.h b/engines/scumm/smush/smush_player_ra2.h
index 9e180a852a0..e4680866e58 100644
--- a/engines/scumm/smush/smush_player_ra2.h
+++ b/engines/scumm/smush/smush_player_ra2.h
@@ -43,7 +43,7 @@ protected:
SmushFont *getGameFont(int font) override;
void adjustGamePalette() override;
bool handleGameAnimHeader(byte *headerContent) override;
- const char *getGameStringResource() const override { return "SYSTM/GAME.TRS"; }
+ bool handleGameSetupStrings() override;
void handleGameParseNextFrame() override;
bool shouldRouteAllIACTs() const override { return true; }
bool handleGameFrameBufferSelect(int codec, int width, int height) override;
diff --git a/engines/scumm/string_v7.cpp b/engines/scumm/string_v7.cpp
index a9fe0c80ab9..9088be0728d 100644
--- a/engines/scumm/string_v7.cpp
+++ b/engines/scumm/string_v7.cpp
@@ -129,10 +129,7 @@ int TextRenderer_v7::getStringHeight(const char *str, uint numBytesMax) {
}
if (*str == '\n') {
- int lh = lineHeight ? lineHeight : _gr->getFontHeight();
- // RA2: add extra inter-line spacing for larger fonts (credits text)
- int gap = (_gameId == GID_REBEL2 && lh > 8) ? lh / 2 : 1;
- totalHeight += lh + gap;
+ totalHeight += (lineHeight ? lineHeight : _gr->getFontHeight()) + 1;
lineHeight = 0;
} else if (*str != '\r' && *str != _lineBreakMarker) {
lineHeight = MAX<int>(lineHeight, _gr->getCharHeight(*str));
Commit: 58b9cc95c4cf69ab16d70cbfeb4ed919c95cb4f2
https://github.com/scummvm/scummvm/commit/58b9cc95c4cf69ab16d70cbfeb4ed919c95cb4f2
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:49+02:00
Commit Message:
SCUMM: RA: Reduce insane.h change impact
Changed paths:
engines/scumm/insane/insane.h
diff --git a/engines/scumm/insane/insane.h b/engines/scumm/insane/insane.h
index d8dddfeb305..b610ccdc7b2 100644
--- a/engines/scumm/insane/insane.h
+++ b/engines/scumm/insane/insane.h
@@ -26,9 +26,6 @@
#include "scumm/smush/smush_player.h"
-#include "common/list.h"
-#include "common/rect.h"
-
namespace Scumm {
#define INV_CHAIN 0
@@ -53,8 +50,8 @@ namespace Scumm {
class Insane {
public:
- Insane() {};
- Insane(ScummEngine_v7 *scumm);
+ Insane() {}
+ Insane(ScummEngine_v7 *scumm);
virtual ~Insane();
void setSmushParams(int speed);
@@ -62,9 +59,9 @@ public:
void runScene(int arraynum);
virtual void procPreRendering(byte *renderBitmap);
- void virtual procPostRendering(byte *renderBitmap, int32 codecparam, int32 setupsan12,
+ virtual void procPostRendering(byte *renderBitmap, int32 codecparam, int32 setupsan12,
int32 setupsan13, int32 curFrame, int32 maxFrame);
- void virtual procIACT(byte *renderBitmap, int32 codecparam, int32 setupsan12,
+ virtual void procIACT(byte *renderBitmap, int32 codecparam, int32 setupsan12,
int32 setupsan13, Common::SeekableReadStream &b, int32 size, int32 flags, int16 par1,
int16 par2, int16 par3, int16 par4);
virtual void procSKIP(int32 subSize, Common::SeekableReadStream &b);
@@ -425,7 +422,7 @@ public:
void actor10Reaction(int32 buttons);
int32 actionEnemy();
int32 processKeyboard();
- int32 virtual processMouse();
+ virtual int32 processMouse();
void setEnemyAnimation(int32 actornum, int anim);
void chooseEnemyWeaponAnim(int32 buttons);
void switchEnemyWeapon();
@@ -451,8 +448,8 @@ public:
void iactScene21(byte *renderBitmap, int32 codecparam, int32 setupsan12,
int32 setupsan13, Common::SeekableReadStream &b, int32 size, int32 flags,
int16 par1, int16 par2, int16 par3, int16 par4);
- bool virtual isBitSet(int n);
- void virtual setBit(int n);
+ virtual bool isBitSet(int n);
+ virtual void setBit(int n);
void clearBit(int n);
void chooseEnemy();
void removeEmptyEnemies();
@@ -460,7 +457,7 @@ public:
public:
- void virtual loadEmbeddedSan(int userId, byte *animData, int32 size, byte *renderBitmap) {
+ virtual void loadEmbeddedSan(int userId, byte *animData, int32 size, byte *renderBitmap) {
// Nothing by default
};
};
Commit: 091dcd30183a519518edec3849dffa087c8d08b6
https://github.com/scummvm/scummvm/commit/091dcd30183a519518edec3849dffa087c8d08b6
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:49+02:00
Commit Message:
SCUMM: RA: Reduce NutRenderer change impact
Changed paths:
engines/scumm/insane/insane_rebel2_iact.cpp
engines/scumm/nut_renderer.cpp
engines/scumm/nut_renderer.h
diff --git a/engines/scumm/insane/insane_rebel2_iact.cpp b/engines/scumm/insane/insane_rebel2_iact.cpp
index 161c89192f3..3a237a9ad16 100644
--- a/engines/scumm/insane/insane_rebel2_iact.cpp
+++ b/engines/scumm/insane/insane_rebel2_iact.cpp
@@ -1895,8 +1895,9 @@ bool InsaneRebel2::loadHandler7FlySprites(Common::SeekableReadStream &b, int64 r
}
// Load as NUT
- NutRenderer *newNut = new NutRenderer(_vm, nutData, bytesRead);
- if (!newNut || newNut->getNumChars() <= 0) {
+ NutRenderer *newNut = new NutRenderer(_vm);
+ newNut->loadFontFromData(nutData, bytesRead);
+ if (newNut->getNumChars() <= 0) {
debug("Rebel2 loadHandler7FlySprites: NUT load failed for par4=%d", par4);
delete newNut;
free(nutData);
@@ -1959,7 +1960,8 @@ bool InsaneRebel2::loadTurretHudOverlay(byte *animData, int32 size, int16 par3)
return false; // Not a turret HUD slot
}
- NutRenderer *newNut = new NutRenderer(_vm, animData, size);
+ NutRenderer *newNut = new NutRenderer(_vm);
+ newNut->loadFontFromData(animData, size);
if (!newNut || newNut->getNumChars() <= 0) {
debug("Rebel2 loadTurretHudOverlay: NUT load failed for par3=%d", par3);
delete newNut;
@@ -2000,7 +2002,8 @@ bool InsaneRebel2::loadHandler8ShipSprites(byte *animData, int32 size, int16 par
return false;
}
- NutRenderer *newNut = new NutRenderer(_vm, animData, size);
+ NutRenderer *newNut = new NutRenderer(_vm);
+ newNut->loadFontFromData(animData, size);
if (!newNut || newNut->getNumChars() <= 0) {
debug("Rebel2 loadHandler8ShipSprites: NUT load failed for par4=%d", par4);
delete newNut;
@@ -2051,7 +2054,8 @@ bool InsaneRebel2::loadHandler25GrdSprites(byte *animData, int32 size, int16 par
return false;
}
- NutRenderer *newNut = new NutRenderer(_vm, animData, size);
+ NutRenderer *newNut = new NutRenderer(_vm);
+ newNut->loadFontFromData(animData, size);
if (!newNut || newNut->getNumChars() <= 0) {
debug("Rebel2 loadHandler25GrdSprites: NUT load failed for par4=%d", par4);
delete newNut;
diff --git a/engines/scumm/nut_renderer.cpp b/engines/scumm/nut_renderer.cpp
index 8b16fbd99c2..b84990ba1f5 100644
--- a/engines/scumm/nut_renderer.cpp
+++ b/engines/scumm/nut_renderer.cpp
@@ -47,30 +47,8 @@ NutRenderer::NutRenderer(ScummEngine *vm, const char *filename) :
memset(_2byteColorTable, 0, _2byteSteps);
_2byteMainColor = &_2byteColorTable[_2byteSteps - 1];
memset(_chars, 0, sizeof(_chars));
- loadFont(filename);
-}
-
-NutRenderer::NutRenderer(ScummEngine *vm, const byte *data, int32 dataSize) :
- _vm(vm),
- _numChars(0),
- _fontHeight(0),
- _decodedData(0),
- _2byteColorTable(0),
- _2byteShadowXOffsetTable(0),
- _2byteShadowYOffsetTable(0),
- _2byteMainColor(0),
- _spacing(vm->_useCJKMode && vm->_language != Common::JA_JPN ? 1 : 0),
- _2byteSteps(vm->_game.version == 8 ? 4 : 2),
- _direction(vm->_language == Common::HE_ISR ? -1 : 1) {
- static const int8 cjkShadowOffsetsX[4] = { -1, 0, 1, 0 };
- static const int8 cjkShadowOffsetsY[4] = { 0, 1, 0, 0 };
- _2byteShadowXOffsetTable = &cjkShadowOffsetsX[ARRAYSIZE(cjkShadowOffsetsX) - _2byteSteps];
- _2byteShadowYOffsetTable = &cjkShadowOffsetsY[ARRAYSIZE(cjkShadowOffsetsY) - _2byteSteps];
- _2byteColorTable = new uint8[_2byteSteps];
- memset(_2byteColorTable, 0, _2byteSteps);
- _2byteMainColor = &_2byteColorTable[_2byteSteps - 1];
- memset(_chars, 0, sizeof(_chars));
- loadFontFromData(data, dataSize);
+ if (filename)
+ loadFont(filename);
}
NutRenderer::~NutRenderer() {
diff --git a/engines/scumm/nut_renderer.h b/engines/scumm/nut_renderer.h
index c83ac830400..7dc15ebec76 100644
--- a/engines/scumm/nut_renderer.h
+++ b/engines/scumm/nut_renderer.h
@@ -64,12 +64,11 @@ protected:
void codec21(byte *dst, const byte *src, int width, int height, int pitch);
void loadFont(const char *filename);
- void loadFontFromData(const byte *data, int32 dataSize);
public:
- NutRenderer(ScummEngine *vm, const char *filename);
- NutRenderer(ScummEngine *vm, const byte *data, int32 dataSize);
+ NutRenderer(ScummEngine *vm, const char *filename = nullptr);
virtual ~NutRenderer();
+ void loadFontFromData(const byte *data, int32 dataSize);
int getNumChars() const { return _numChars; }
void drawFrame(byte *dst, int c, int x, int y, int pitch = -1);
Commit: 90cfb6851df30e3442fd849fa5164ecc743fbfc8
https://github.com/scummvm/scummvm/commit/90cfb6851df30e3442fd849fa5164ecc743fbfc8
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:49+02:00
Commit Message:
SCUMM: RA: Move Rebel Assault code into rebel directories
Changed paths:
A engines/scumm/insane/rebel1/audio.cpp
A engines/scumm/insane/rebel1/iact.cpp
A engines/scumm/insane/rebel1/levels.cpp
A engines/scumm/insane/rebel1/menu.cpp
A engines/scumm/insane/rebel1/rebel.cpp
A engines/scumm/insane/rebel1/rebel.h
A engines/scumm/insane/rebel1/render.cpp
A engines/scumm/insane/rebel1/runlevels.cpp
A engines/scumm/insane/rebel2/audio.cpp
A engines/scumm/insane/rebel2/iact.cpp
A engines/scumm/insane/rebel2/levels.cpp
A engines/scumm/insane/rebel2/menu.cpp
A engines/scumm/insane/rebel2/rebel.cpp
A engines/scumm/insane/rebel2/rebel.h
A engines/scumm/insane/rebel2/render.cpp
A engines/scumm/insane/rebel2/runlevels.cpp
A engines/scumm/smush/rebel/codec_ra2.cpp
A engines/scumm/smush/rebel/smush_multi_font.cpp
A engines/scumm/smush/rebel/smush_multi_font.h
A engines/scumm/smush/rebel/smush_player_ra1.cpp
A engines/scumm/smush/rebel/smush_player_ra1.h
A engines/scumm/smush/rebel/smush_player_ra2.cpp
A engines/scumm/smush/rebel/smush_player_ra2.h
R engines/scumm/insane/insane_rebel1.cpp
R engines/scumm/insane/insane_rebel1.h
R engines/scumm/insane/insane_rebel1_audio.cpp
R engines/scumm/insane/insane_rebel1_iact.cpp
R engines/scumm/insane/insane_rebel1_levels.cpp
R engines/scumm/insane/insane_rebel1_menu.cpp
R engines/scumm/insane/insane_rebel1_render.cpp
R engines/scumm/insane/insane_rebel1_runlevels.cpp
R engines/scumm/insane/insane_rebel2.cpp
R engines/scumm/insane/insane_rebel2.h
R engines/scumm/insane/insane_rebel2_audio.cpp
R engines/scumm/insane/insane_rebel2_iact.cpp
R engines/scumm/insane/insane_rebel2_levels.cpp
R engines/scumm/insane/insane_rebel2_menu.cpp
R engines/scumm/insane/insane_rebel2_render.cpp
R engines/scumm/insane/insane_rebel2_runlevels.cpp
R engines/scumm/smush/codec_ra2.cpp
R engines/scumm/smush/smush_multi_font.cpp
R engines/scumm/smush/smush_multi_font.h
R engines/scumm/smush/smush_player_ra1.cpp
R engines/scumm/smush/smush_player_ra1.h
R engines/scumm/smush/smush_player_ra2.cpp
R engines/scumm/smush/smush_player_ra2.h
engines/scumm/module.mk
engines/scumm/scumm.cpp
engines/scumm/smush/smush_player.cpp
diff --git a/engines/scumm/insane/insane_rebel1_audio.cpp b/engines/scumm/insane/rebel1/audio.cpp
similarity index 99%
rename from engines/scumm/insane/insane_rebel1_audio.cpp
rename to engines/scumm/insane/rebel1/audio.cpp
index 5801d0dce6f..35529463d17 100644
--- a/engines/scumm/insane/insane_rebel1_audio.cpp
+++ b/engines/scumm/insane/rebel1/audio.cpp
@@ -28,7 +28,7 @@
#include "audio/mixer.h"
#include "scumm/scumm_v7.h"
-#include "scumm/insane/insane_rebel1.h"
+#include "scumm/insane/rebel1/rebel.h"
namespace Scumm {
diff --git a/engines/scumm/insane/insane_rebel1_iact.cpp b/engines/scumm/insane/rebel1/iact.cpp
similarity index 99%
rename from engines/scumm/insane/insane_rebel1_iact.cpp
rename to engines/scumm/insane/rebel1/iact.cpp
index 4a900b5dd5b..f7da03c2091 100644
--- a/engines/scumm/insane/insane_rebel1_iact.cpp
+++ b/engines/scumm/insane/rebel1/iact.cpp
@@ -24,7 +24,7 @@
#include "common/endian.h"
#include "scumm/scumm_v7.h"
-#include "scumm/insane/insane_rebel1.h"
+#include "scumm/insane/rebel1/rebel.h"
namespace Scumm {
diff --git a/engines/scumm/insane/insane_rebel1_levels.cpp b/engines/scumm/insane/rebel1/levels.cpp
similarity index 99%
rename from engines/scumm/insane/insane_rebel1_levels.cpp
rename to engines/scumm/insane/rebel1/levels.cpp
index c51577b751c..d4681b2d47d 100644
--- a/engines/scumm/insane/insane_rebel1_levels.cpp
+++ b/engines/scumm/insane/rebel1/levels.cpp
@@ -24,7 +24,7 @@
#include "scumm/scumm_v7.h"
#include "scumm/file.h"
-#include "scumm/insane/insane_rebel1.h"
+#include "scumm/insane/rebel1/rebel.h"
namespace Scumm {
diff --git a/engines/scumm/insane/insane_rebel1_menu.cpp b/engines/scumm/insane/rebel1/menu.cpp
similarity index 99%
rename from engines/scumm/insane/insane_rebel1_menu.cpp
rename to engines/scumm/insane/rebel1/menu.cpp
index 7d3857c673c..0bd204db2f9 100644
--- a/engines/scumm/insane/insane_rebel1_menu.cpp
+++ b/engines/scumm/insane/rebel1/menu.cpp
@@ -28,7 +28,7 @@
#include "scumm/scumm_v7.h"
#include "scumm/smush/smush_player.h"
-#include "scumm/insane/insane_rebel1.h"
+#include "scumm/insane/rebel1/rebel.h"
namespace Scumm {
diff --git a/engines/scumm/insane/insane_rebel1.cpp b/engines/scumm/insane/rebel1/rebel.cpp
similarity index 99%
rename from engines/scumm/insane/insane_rebel1.cpp
rename to engines/scumm/insane/rebel1/rebel.cpp
index 0610f978d45..544cb026cab 100644
--- a/engines/scumm/insane/insane_rebel1.cpp
+++ b/engines/scumm/insane/rebel1/rebel.cpp
@@ -32,7 +32,7 @@
#include "scumm/scumm_v7.h"
#include "scumm/scumm.h"
#include "scumm/smush/smush_player.h"
-#include "scumm/insane/insane_rebel1.h"
+#include "scumm/insane/rebel1/rebel.h"
namespace Scumm {
diff --git a/engines/scumm/insane/insane_rebel1.h b/engines/scumm/insane/rebel1/rebel.h
similarity index 99%
rename from engines/scumm/insane/insane_rebel1.h
rename to engines/scumm/insane/rebel1/rebel.h
index 24ebbced021..c230a44aae3 100644
--- a/engines/scumm/insane/insane_rebel1.h
+++ b/engines/scumm/insane/rebel1/rebel.h
@@ -26,7 +26,7 @@
#include "audio/mixer.h"
#include "common/events.h"
#include "scumm/insane/insane.h"
-#include "scumm/smush/smush_player_ra1.h"
+#include "scumm/smush/rebel/smush_player_ra1.h"
namespace Scumm {
diff --git a/engines/scumm/insane/insane_rebel1_render.cpp b/engines/scumm/insane/rebel1/render.cpp
similarity index 99%
rename from engines/scumm/insane/insane_rebel1_render.cpp
rename to engines/scumm/insane/rebel1/render.cpp
index dd34b22c87f..2b6719f5a40 100644
--- a/engines/scumm/insane/insane_rebel1_render.cpp
+++ b/engines/scumm/insane/rebel1/render.cpp
@@ -22,8 +22,8 @@
#include "common/system.h"
#include "scumm/scumm_v7.h"
-#include "scumm/smush/smush_player_ra1.h"
-#include "scumm/insane/insane_rebel1.h"
+#include "scumm/smush/rebel/smush_player_ra1.h"
+#include "scumm/insane/rebel1/rebel.h"
namespace Scumm {
diff --git a/engines/scumm/insane/insane_rebel1_runlevels.cpp b/engines/scumm/insane/rebel1/runlevels.cpp
similarity index 99%
rename from engines/scumm/insane/insane_rebel1_runlevels.cpp
rename to engines/scumm/insane/rebel1/runlevels.cpp
index 04392541bff..1052fef9ad2 100644
--- a/engines/scumm/insane/insane_rebel1_runlevels.cpp
+++ b/engines/scumm/insane/rebel1/runlevels.cpp
@@ -27,7 +27,7 @@
#include "scumm/file.h"
#include "scumm/scumm_v7.h"
#include "scumm/smush/smush_player.h"
-#include "scumm/insane/insane_rebel1.h"
+#include "scumm/insane/rebel1/rebel.h"
namespace Scumm {
diff --git a/engines/scumm/insane/insane_rebel2_audio.cpp b/engines/scumm/insane/rebel2/audio.cpp
similarity index 99%
rename from engines/scumm/insane/insane_rebel2_audio.cpp
rename to engines/scumm/insane/rebel2/audio.cpp
index 3341bc18f72..f679b70f47d 100644
--- a/engines/scumm/insane/insane_rebel2_audio.cpp
+++ b/engines/scumm/insane/rebel2/audio.cpp
@@ -26,7 +26,7 @@
#include "scumm/smush/smush_player.h"
-#include "scumm/insane/insane_rebel2.h"
+#include "scumm/insane/rebel2/rebel.h"
#include "audio/audiostream.h"
#include "audio/decoders/raw.h"
diff --git a/engines/scumm/insane/insane_rebel2_iact.cpp b/engines/scumm/insane/rebel2/iact.cpp
similarity index 99%
rename from engines/scumm/insane/insane_rebel2_iact.cpp
rename to engines/scumm/insane/rebel2/iact.cpp
index 3a237a9ad16..2d653a2cf16 100644
--- a/engines/scumm/insane/insane_rebel2_iact.cpp
+++ b/engines/scumm/insane/rebel2/iact.cpp
@@ -28,7 +28,7 @@
#include "scumm/smush/smush_player.h"
#include "scumm/smush/smush_font.h"
-#include "scumm/insane/insane_rebel2.h"
+#include "scumm/insane/rebel2/rebel.h"
namespace Scumm {
diff --git a/engines/scumm/insane/insane_rebel2_levels.cpp b/engines/scumm/insane/rebel2/levels.cpp
similarity index 99%
rename from engines/scumm/insane/insane_rebel2_levels.cpp
rename to engines/scumm/insane/rebel2/levels.cpp
index 7b6907386e0..01c49a03452 100644
--- a/engines/scumm/insane/insane_rebel2_levels.cpp
+++ b/engines/scumm/insane/rebel2/levels.cpp
@@ -27,7 +27,7 @@
#include "scumm/smush/smush_player.h"
-#include "scumm/insane/insane_rebel2.h"
+#include "scumm/insane/rebel2/rebel.h"
namespace Scumm {
diff --git a/engines/scumm/insane/insane_rebel2_menu.cpp b/engines/scumm/insane/rebel2/menu.cpp
similarity index 99%
rename from engines/scumm/insane/insane_rebel2_menu.cpp
rename to engines/scumm/insane/rebel2/menu.cpp
index 696589e9133..f631909a4f6 100644
--- a/engines/scumm/insane/insane_rebel2_menu.cpp
+++ b/engines/scumm/insane/rebel2/menu.cpp
@@ -33,9 +33,9 @@
#include "scumm/smush/smush_player.h"
#include "scumm/smush/smush_font.h"
-#include "scumm/smush/smush_multi_font.h"
+#include "scumm/smush/rebel/smush_multi_font.h"
-#include "scumm/insane/insane_rebel2.h"
+#include "scumm/insane/rebel2/rebel.h"
namespace Scumm {
diff --git a/engines/scumm/insane/insane_rebel2.cpp b/engines/scumm/insane/rebel2/rebel.cpp
similarity index 99%
rename from engines/scumm/insane/insane_rebel2.cpp
rename to engines/scumm/insane/rebel2/rebel.cpp
index 458c0b75437..7231de9997a 100644
--- a/engines/scumm/insane/insane_rebel2.cpp
+++ b/engines/scumm/insane/rebel2/rebel.cpp
@@ -43,7 +43,7 @@
#include "scumm/smush/smush_player.h"
#include "scumm/smush/smush_font.h"
-#include "scumm/insane/insane_rebel2.h"
+#include "scumm/insane/rebel2/rebel.h"
#include "common/config-manager.h"
#include "audio/audiostream.h"
diff --git a/engines/scumm/insane/insane_rebel2.h b/engines/scumm/insane/rebel2/rebel.h
similarity index 100%
rename from engines/scumm/insane/insane_rebel2.h
rename to engines/scumm/insane/rebel2/rebel.h
diff --git a/engines/scumm/insane/insane_rebel2_render.cpp b/engines/scumm/insane/rebel2/render.cpp
similarity index 99%
rename from engines/scumm/insane/insane_rebel2_render.cpp
rename to engines/scumm/insane/rebel2/render.cpp
index 3f3dbd37963..ef0c79af23c 100644
--- a/engines/scumm/insane/insane_rebel2_render.cpp
+++ b/engines/scumm/insane/rebel2/render.cpp
@@ -31,7 +31,7 @@
#include "scumm/smush/smush_player.h"
#include "scumm/smush/smush_font.h"
-#include "scumm/insane/insane_rebel2.h"
+#include "scumm/insane/rebel2/rebel.h"
namespace Scumm {
diff --git a/engines/scumm/insane/insane_rebel2_runlevels.cpp b/engines/scumm/insane/rebel2/runlevels.cpp
similarity index 99%
rename from engines/scumm/insane/insane_rebel2_runlevels.cpp
rename to engines/scumm/insane/rebel2/runlevels.cpp
index a48dbc5b8c7..75da1488559 100644
--- a/engines/scumm/insane/insane_rebel2_runlevels.cpp
+++ b/engines/scumm/insane/rebel2/runlevels.cpp
@@ -25,7 +25,7 @@
#include "scumm/smush/smush_player.h"
-#include "scumm/insane/insane_rebel2.h"
+#include "scumm/insane/rebel2/rebel.h"
namespace Scumm {
diff --git a/engines/scumm/module.mk b/engines/scumm/module.mk
index b7fe661ee74..ea6cc308dc7 100644
--- a/engines/scumm/module.mk
+++ b/engines/scumm/module.mk
@@ -133,29 +133,29 @@ MODULE_OBJS += \
insane/insane_enemy.o \
insane/insane_scenes.o \
insane/insane_iact.o \
- insane/insane_rebel1.o \
- insane/insane_rebel1_audio.o \
- insane/insane_rebel1_iact.o \
- insane/insane_rebel1_levels.o \
- insane/insane_rebel1_menu.o \
- insane/insane_rebel1_render.o \
- insane/insane_rebel1_runlevels.o \
- insane/insane_rebel2.o \
- insane/insane_rebel2_audio.o \
- insane/insane_rebel2_iact.o \
- insane/insane_rebel2_levels.o \
- insane/insane_rebel2_menu.o \
- insane/insane_rebel2_render.o \
- insane/insane_rebel2_runlevels.o \
+ insane/rebel1/rebel.o \
+ insane/rebel1/audio.o \
+ insane/rebel1/iact.o \
+ insane/rebel1/levels.o \
+ insane/rebel1/menu.o \
+ insane/rebel1/render.o \
+ insane/rebel1/runlevels.o \
+ insane/rebel2/rebel.o \
+ insane/rebel2/audio.o \
+ insane/rebel2/iact.o \
+ insane/rebel2/levels.o \
+ insane/rebel2/menu.o \
+ insane/rebel2/render.o \
+ insane/rebel2/runlevels.o \
smush/codec1.o \
smush/codec20.o \
smush/codec37.o \
smush/codec47.o \
- smush/codec_ra2.o \
- smush/smush_multi_font.o \
smush/smush_player.o \
- smush/smush_player_ra1.o \
- smush/smush_player_ra2.o
+ smush/rebel/codec_ra2.o \
+ smush/rebel/smush_multi_font.o \
+ smush/rebel/smush_player_ra1.o \
+ smush/rebel/smush_player_ra2.o
ifdef USE_ARM_SMUSH_ASM
MODULE_OBJS += \
diff --git a/engines/scumm/scumm.cpp b/engines/scumm/scumm.cpp
index 3a89da5bf87..7d19565b732 100644
--- a/engines/scumm/scumm.cpp
+++ b/engines/scumm/scumm.cpp
@@ -50,12 +50,12 @@
#include "scumm/imuse/imuse.h"
#include "scumm/imuse_digi/dimuse_engine.h"
#include "scumm/smush/smush_player.h"
-#include "scumm/smush/smush_player_ra1.h"
-#include "scumm/smush/smush_player_ra2.h"
+#include "scumm/smush/rebel/smush_player_ra1.h"
+#include "scumm/smush/rebel/smush_player_ra2.h"
#include "scumm/players/player_towns.h"
#include "scumm/insane/insane.h"
-#include "scumm/insane/insane_rebel2.h"
-#include "scumm/insane/insane_rebel1.h"
+#include "scumm/insane/rebel2/rebel.h"
+#include "scumm/insane/rebel1/rebel.h"
#include "scumm/he/animation_he.h"
#include "scumm/he/font_he.h"
#include "scumm/he/intern_he.h"
diff --git a/engines/scumm/smush/codec_ra2.cpp b/engines/scumm/smush/rebel/codec_ra2.cpp
similarity index 100%
rename from engines/scumm/smush/codec_ra2.cpp
rename to engines/scumm/smush/rebel/codec_ra2.cpp
diff --git a/engines/scumm/smush/smush_multi_font.cpp b/engines/scumm/smush/rebel/smush_multi_font.cpp
similarity index 98%
rename from engines/scumm/smush/smush_multi_font.cpp
rename to engines/scumm/smush/rebel/smush_multi_font.cpp
index e68871d6f33..9c4205b7a65 100644
--- a/engines/scumm/smush/smush_multi_font.cpp
+++ b/engines/scumm/smush/rebel/smush_multi_font.cpp
@@ -19,7 +19,7 @@
*
*/
-#include "scumm/smush/smush_multi_font.h"
+#include "scumm/smush/rebel/smush_multi_font.h"
#include "scumm/smush/smush_font.h"
#include "scumm/smush/smush_player.h"
#include "scumm/scumm.h"
diff --git a/engines/scumm/smush/smush_multi_font.h b/engines/scumm/smush/rebel/smush_multi_font.h
similarity index 100%
rename from engines/scumm/smush/smush_multi_font.h
rename to engines/scumm/smush/rebel/smush_multi_font.h
diff --git a/engines/scumm/smush/smush_player_ra1.cpp b/engines/scumm/smush/rebel/smush_player_ra1.cpp
similarity index 99%
rename from engines/scumm/smush/smush_player_ra1.cpp
rename to engines/scumm/smush/rebel/smush_player_ra1.cpp
index 00bd2535d11..6fc23c1ae22 100644
--- a/engines/scumm/smush/smush_player_ra1.cpp
+++ b/engines/scumm/smush/rebel/smush_player_ra1.cpp
@@ -30,9 +30,9 @@
#include "scumm/file.h"
#include "scumm/scumm_v7.h"
#include "scumm/smush/smush_font.h"
-#include "scumm/smush/smush_player_ra1.h"
+#include "scumm/smush/rebel/smush_player_ra1.h"
-#include "scumm/insane/insane_rebel1.h"
+#include "scumm/insane/rebel1/rebel.h"
namespace Scumm {
diff --git a/engines/scumm/smush/smush_player_ra1.h b/engines/scumm/smush/rebel/smush_player_ra1.h
similarity index 100%
rename from engines/scumm/smush/smush_player_ra1.h
rename to engines/scumm/smush/rebel/smush_player_ra1.h
diff --git a/engines/scumm/smush/smush_player_ra2.cpp b/engines/scumm/smush/rebel/smush_player_ra2.cpp
similarity index 99%
rename from engines/scumm/smush/smush_player_ra2.cpp
rename to engines/scumm/smush/rebel/smush_player_ra2.cpp
index c256493fec5..c215c4cd465 100644
--- a/engines/scumm/smush/smush_player_ra2.cpp
+++ b/engines/scumm/smush/rebel/smush_player_ra2.cpp
@@ -31,11 +31,11 @@
#include "scumm/scumm.h"
#include "scumm/scumm_v7.h"
#include "scumm/smush/smush_font.h"
-#include "scumm/smush/smush_multi_font.h"
-#include "scumm/smush/smush_player_ra2.h"
+#include "scumm/smush/rebel/smush_multi_font.h"
+#include "scumm/smush/rebel/smush_player_ra2.h"
#include "scumm/insane/insane.h"
-#include "scumm/insane/insane_rebel2.h"
+#include "scumm/insane/rebel2/rebel.h"
namespace Scumm {
diff --git a/engines/scumm/smush/smush_player_ra2.h b/engines/scumm/smush/rebel/smush_player_ra2.h
similarity index 100%
rename from engines/scumm/smush/smush_player_ra2.h
rename to engines/scumm/smush/rebel/smush_player_ra2.h
diff --git a/engines/scumm/smush/smush_player.cpp b/engines/scumm/smush/smush_player.cpp
index cd71a780ff2..35431cd6da1 100644
--- a/engines/scumm/smush/smush_player.cpp
+++ b/engines/scumm/smush/smush_player.cpp
@@ -40,7 +40,7 @@
#include "scumm/smush/codec37.h"
#include "scumm/smush/codec47.h"
#include "scumm/smush/smush_font.h"
-#include "scumm/smush/smush_multi_font.h"
+#include "scumm/smush/rebel/smush_multi_font.h"
#include "scumm/smush/smush_player.h"
#include "scumm/insane/insane.h"
Commit: dafba02a3b9020e8c94d82e068d229245dd5de35
https://github.com/scummvm/scummvm/commit/dafba02a3b9020e8c94d82e068d229245dd5de35
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:50+02:00
Commit Message:
SCUMM: RA1: Fix regression in RA1 block decoder
Changed paths:
engines/scumm/smush/rebel/smush_player_ra1.cpp
engines/scumm/smush/rebel/smush_player_ra1.h
engines/scumm/smush/rebel/smush_player_ra2.cpp
engines/scumm/smush/rebel/smush_player_ra2.h
engines/scumm/smush/smush_player.cpp
engines/scumm/smush/smush_player.h
diff --git a/engines/scumm/smush/rebel/smush_player_ra1.cpp b/engines/scumm/smush/rebel/smush_player_ra1.cpp
index 6fc23c1ae22..5e8b3da8cf9 100644
--- a/engines/scumm/smush/rebel/smush_player_ra1.cpp
+++ b/engines/scumm/smush/rebel/smush_player_ra1.cpp
@@ -512,11 +512,7 @@ bool SmushPlayerRebel1::handleGameAdjustCoords(int codec, int &left, int &top, i
return true;
}
-bool SmushPlayerRebel1::handleGameCodecDecode(int codec, const uint8 *src, int left, int top, int width, int height, int pitch, int dataSize) {
- // The base class passes clipped coords. For additive codec and scatter, we need original coords
- // which are stored in the origLeft/origTop locals of decodeFrameObject. Since we can't access those
- // from the override, the additive codec and scatter draw are special-cased.
- // For now, handle the codecs that have RA1-specific behavior.
+bool SmushPlayerRebel1::handleGameCodecDecode(int codec, const uint8 *src, int left, int top, int width, int height, int pitch, int dataSize, uint8 param, uint16 parm2) {
switch (codec) {
case SMUSH_CODEC_RLE:
smushDecodeRA1Transparent(_dst, src, left, top, width, height, pitch);
@@ -529,7 +525,7 @@ bool SmushPlayerRebel1::handleGameCodecDecode(int codec, const uint8 *src, int l
return true;
case SMUSH_CODEC_RA1_DELTA:
case SMUSH_CODEC_RA1_BLOCK:
- smushDecodeRA1Block(_dst, src, left, top, width, height, pitch, dataSize, 0, 0, codec);
+ smushDecodeRA1Block(_dst, src, left, top, width, height, pitch, dataSize, param, parm2, codec);
return true;
case SMUSH_CODEC_LINE_UPDATE:
smushDecodeRA1SkipCopy(_dst, src, left, top, width, height, pitch);
diff --git a/engines/scumm/smush/rebel/smush_player_ra1.h b/engines/scumm/smush/rebel/smush_player_ra1.h
index cd072afcabf..407fbb309e2 100644
--- a/engines/scumm/smush/rebel/smush_player_ra1.h
+++ b/engines/scumm/smush/rebel/smush_player_ra1.h
@@ -46,7 +46,7 @@ protected:
bool handleGameFrameBufferSelect(int codec, int width, int height) override;
bool handleGameDimensionOverride(int codec, int width, int height) override;
bool handleGameAdjustCoords(int codec, int &left, int &top, int &width, int &height, int pitch, int *srcSkipY) override;
- bool handleGameCodecDecode(int codec, const uint8 *src, int left, int top, int width, int height, int pitch, int dataSize) override;
+ bool handleGameCodecDecode(int codec, const uint8 *src, int left, int top, int width, int height, int pitch, int dataSize, uint8 param = 0, uint16 parm2 = 0) override;
bool handleGameStoreFrame() override;
void handleGameFrameObjectPre(int codec, int left, int top, int width, int height, int dataSize) override;
void handleGameFrameObjectPost(int codec, const byte *data, int32 dataSize, int left, int top, int width, int height) override;
diff --git a/engines/scumm/smush/rebel/smush_player_ra2.cpp b/engines/scumm/smush/rebel/smush_player_ra2.cpp
index c215c4cd465..bdf5502ed92 100644
--- a/engines/scumm/smush/rebel/smush_player_ra2.cpp
+++ b/engines/scumm/smush/rebel/smush_player_ra2.cpp
@@ -739,7 +739,7 @@ bool SmushPlayerRebel2::handleGameAdjustCoords(int codec, int &left, int &top, i
return true;
}
-bool SmushPlayerRebel2::handleGameCodecDecode(int codec, const uint8 *src, int left, int top, int width, int height, int pitch, int dataSize) {
+bool SmushPlayerRebel2::handleGameCodecDecode(int codec, const uint8 *src, int left, int top, int width, int height, int pitch, int dataSize, uint8 param, uint16 parm2) {
// Handle RA2-specific codecs (21, 23, 44, 45); return false for standard
// codecs (RLE, uncompressed, codec 37/47) so the base class decodes them.
return ra2DecodeCodec(codec, src, left, top, width, height, pitch, dataSize);
diff --git a/engines/scumm/smush/rebel/smush_player_ra2.h b/engines/scumm/smush/rebel/smush_player_ra2.h
index e4680866e58..375d2a6a015 100644
--- a/engines/scumm/smush/rebel/smush_player_ra2.h
+++ b/engines/scumm/smush/rebel/smush_player_ra2.h
@@ -49,7 +49,7 @@ protected:
bool handleGameFrameBufferSelect(int codec, int width, int height) override;
bool handleGameDimensionOverride(int codec, int width, int height) override;
bool handleGameAdjustCoords(int codec, int &left, int &top, int &width, int &height, int pitch, int *srcSkipY) override;
- bool handleGameCodecDecode(int codec, const uint8 *src, int left, int top, int width, int height, int pitch, int dataSize) override;
+ bool handleGameCodecDecode(int codec, const uint8 *src, int left, int top, int width, int height, int pitch, int dataSize, uint8 param = 0, uint16 parm2 = 0) override;
bool handleGameStoreFrame() override;
void handleGameFrameObjectPre(int codec, int left, int top, int width, int height, int dataSize) override;
void handleGameFrameObjectPost(int codec, const byte *data, int32 dataSize, int left, int top, int width, int height) override;
diff --git a/engines/scumm/smush/smush_player.cpp b/engines/scumm/smush/smush_player.cpp
index 35431cd6da1..0da86094c01 100644
--- a/engines/scumm/smush/smush_player.cpp
+++ b/engines/scumm/smush/smush_player.cpp
@@ -911,7 +911,7 @@ void SmushPlayer::decodeFrameObject(int codec, const uint8 *src, int left, int t
}
// Try game-specific codec handler first (pass adjustedSrc for row-skip-aware codecs)
- if (!handleGameCodecDecode(codec, adjustedSrc, left, top, width, height, pitch, dataSize)) {
+ if (!handleGameCodecDecode(codec, adjustedSrc, left, top, width, height, pitch, dataSize, ra1Param, ra1Parm2)) {
// Standard SCUMM codec dispatch
switch (codec) {
case SMUSH_CODEC_RLE:
diff --git a/engines/scumm/smush/smush_player.h b/engines/scumm/smush/smush_player.h
index b84d1880afb..0ec4bd9311e 100644
--- a/engines/scumm/smush/smush_player.h
+++ b/engines/scumm/smush/smush_player.h
@@ -314,7 +314,7 @@ protected:
virtual bool handleGameFrameBufferSelect(int codec, int width, int height) { return false; }
virtual bool handleGameDimensionOverride(int codec, int width, int height) { return false; }
virtual bool handleGameAdjustCoords(int codec, int &left, int &top, int &width, int &height, int pitch, int *srcSkipY) { return false; }
- virtual bool handleGameCodecDecode(int codec, const uint8 *src, int left, int top, int width, int height, int pitch, int dataSize) { return false; }
+ virtual bool handleGameCodecDecode(int codec, const uint8 *src, int left, int top, int width, int height, int pitch, int dataSize, uint8 param = 0, uint16 parm2 = 0) { return false; }
virtual bool handleGameStoreFrame() { return false; }
virtual void handleGameFrameObjectPre(int codec, int left, int top, int width, int height, int dataSize) {}
virtual void handleGameFrameObjectPost(int codec, const byte *data, int32 dataSize, int left, int top, int width, int height) {}
Commit: d208eb1f6bd90909198bae7a79900e4907e98d37
https://github.com/scummvm/scummvm/commit/d208eb1f6bd90909198bae7a79900e4907e98d37
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:50+02:00
Commit Message:
SCUMM: RA1: Fix RA1 block decoder edge cases
Changed paths:
engines/scumm/smush/rebel/smush_player_ra1.cpp
diff --git a/engines/scumm/smush/rebel/smush_player_ra1.cpp b/engines/scumm/smush/rebel/smush_player_ra1.cpp
index 5e8b3da8cf9..ad3ea174974 100644
--- a/engines/scumm/smush/rebel/smush_player_ra1.cpp
+++ b/engines/scumm/smush/rebel/smush_player_ra1.cpp
@@ -429,9 +429,9 @@ static bool ra1Codec4LoadTiles(const byte *&src, int &remaining, uint16 param2,
}
void smushDecodeRA1Block(byte *dst, const byte *src, int left, int top, int width, int height,
- int pitch, int dataSize, uint8 param, uint16 parm2, int codec) {
+ int pitch, int bufHeight, int dataSize, uint8 param, uint16 parm2, int codec) {
const int mx = pitch;
- const int my = height;
+ const int my = bufHeight;
if (s_ra1C4Param != param) {
ra1Codec4GenTiles(param);
s_ra1C4Param = param;
@@ -505,8 +505,9 @@ bool SmushPlayerRebel1::handleGameDimensionOverride(int codec, int width, int he
}
bool SmushPlayerRebel1::handleGameAdjustCoords(int codec, int &left, int &top, int &width, int &height, int pitch, int *srcSkipY) {
- // RA1 additive codec (SKIP_RLE) uses original coords, not adjusted
- if (codec == SMUSH_CODEC_SKIP_RLE)
+ // RA1 additive codec (SKIP_RLE) and scatter (RA1_SCATTER) use absolute
+ // positions â they must NOT be clipped/adjusted.
+ if (codec == SMUSH_CODEC_SKIP_RLE || codec == SMUSH_CODEC_RA1_SCATTER)
return false;
adjustFrameCoords(left, top, width, height, pitch, srcSkipY);
return true;
@@ -525,7 +526,9 @@ bool SmushPlayerRebel1::handleGameCodecDecode(int codec, const uint8 *src, int l
return true;
case SMUSH_CODEC_RA1_DELTA:
case SMUSH_CODEC_RA1_BLOCK:
- smushDecodeRA1Block(_dst, src, left, top, width, height, pitch, dataSize, param, parm2, codec);
+ smushDecodeRA1Block(_dst, src, left, top, width, height, pitch,
+ (_dst == _specialBuffer) ? _height : _vm->_screenHeight,
+ dataSize, param, parm2, codec);
return true;
case SMUSH_CODEC_LINE_UPDATE:
smushDecodeRA1SkipCopy(_dst, src, left, top, width, height, pitch);
Commit: f86c294c7821d3b3e810175150b00d64eef59f58
https://github.com/scummvm/scummvm/commit/f86c294c7821d3b3e810175150b00d64eef59f58
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:50+02:00
Commit Message:
SCUMM: RA: Remove useless static qualifiers
Changed paths:
engines/scumm/insane/rebel1/audio.cpp
engines/scumm/insane/rebel1/iact.cpp
engines/scumm/insane/rebel1/levels.cpp
engines/scumm/insane/rebel1/menu.cpp
engines/scumm/insane/rebel1/rebel.cpp
engines/scumm/insane/rebel1/render.cpp
engines/scumm/insane/rebel1/runlevels.cpp
engines/scumm/insane/rebel2/audio.cpp
engines/scumm/insane/rebel2/levels.cpp
engines/scumm/insane/rebel2/menu.cpp
engines/scumm/insane/rebel2/rebel.cpp
engines/scumm/insane/rebel2/render.cpp
engines/scumm/insane/rebel2/runlevels.cpp
diff --git a/engines/scumm/insane/rebel1/audio.cpp b/engines/scumm/insane/rebel1/audio.cpp
index 35529463d17..6c1540aa368 100644
--- a/engines/scumm/insane/rebel1/audio.cpp
+++ b/engines/scumm/insane/rebel1/audio.cpp
@@ -32,7 +32,7 @@
namespace Scumm {
-static const char *const kRA1SfxFiles[8] = {
+const char *const kRA1SfxFiles[8] = {
"SYS/LASRSHOT.SAD",
"SYS/EXPLODE.SAD",
"SYS/BOOM.SAD",
diff --git a/engines/scumm/insane/rebel1/iact.cpp b/engines/scumm/insane/rebel1/iact.cpp
index f7da03c2091..b66c6d91bf1 100644
--- a/engines/scumm/insane/rebel1/iact.cpp
+++ b/engines/scumm/insane/rebel1/iact.cpp
@@ -28,12 +28,12 @@
namespace Scumm {
-static inline int16 applyRebel1AnalogDeadzone(int16 axisValue) {
+inline int16 applyRebel1AnalogDeadzone(int16 axisValue) {
const int deadZone = MAX(0, ConfMan.getInt("joystick_deadzone")) * 1000;
return (ABS(axisValue) <= deadZone) ? 0 : axisValue;
}
-static inline int16 smoothRebel1Op0BAnalogInput(int16 inputValue, int16 &filteredValue, int16 axisMax) {
+inline int16 smoothRebel1Op0BAnalogInput(int16 inputValue, int16 &filteredValue, int16 axisMax) {
const int delta = (int)inputValue - (int)filteredValue;
int step = delta / 10;
@@ -44,13 +44,13 @@ static inline int16 smoothRebel1Op0BAnalogInput(int16 inputValue, int16 &filtere
return filteredValue;
}
-static const int16 kRA1Op09AimXScale[5] = { 0, 44, 88, 128, 165 };
-static const int16 kRA1Op09AimYScale[5] = { 256, 252, 240, 221, 196 };
+const int16 kRA1Op09AimXScale[5] = { 0, 44, 88, 128, 165 };
+const int16 kRA1Op09AimYScale[5] = { 256, 252, 240, 221, 196 };
// LVL1 stage-2 0x5D damage/event codes. The gameplay stream exposes low record ids
// (6..18), while the recovered outer loop compares the post-latch state against the
// later translated values seen in the executable. Accept both representations.
-static inline bool isL1Stage2DamageLatch(uint16 code) {
+inline bool isL1Stage2DamageLatch(uint16 code) {
switch (code) {
case 0x0006:
case 0x0007:
@@ -81,7 +81,7 @@ static inline bool isL1Stage2DamageLatch(uint16 code) {
}
}
-static inline bool isL1Stage2SweepDamage(uint16 frameCounter, int16 perspectiveX) {
+inline bool isL1Stage2SweepDamage(uint16 frameCounter, int16 perspectiveX) {
switch (frameCounter) {
case 0x0034:
case 0x00ED:
@@ -96,7 +96,7 @@ static inline bool isL1Stage2SweepDamage(uint16 frameCounter, int16 perspectiveX
}
}
-static const int16 kLevel7BranchFrames[6][6] = {
+const int16 kLevel7BranchFrames[6][6] = {
{ -1, 78, 267, 398, 556, 630 },
{ -1, 187, 376, 507, 665, 739 },
{ -1, 187, 376, 507, 665, 739 },
@@ -105,21 +105,21 @@ static const int16 kLevel7BranchFrames[6][6] = {
{ -1, 112, 301, 432, 590, 664 }
};
-static const int16 kLevel7BranchDir[6] = {
+const int16 kLevel7BranchDir[6] = {
0, 1, 1, -1, 1, 1
};
-static const int16 kLevel7BranchThreshold[6] = {
+const int16 kLevel7BranchThreshold[6] = {
0, 170, 170, 160, 160, 160
};
-static const int16 kLevel8BranchFrames[3][3] = {
+const int16 kLevel8BranchFrames[3][3] = {
{ 2588, 1709, 262 },
{ 2323, 1444, -2 },
{ 877, -2, -2 }
};
-static inline bool isLevel4DamageLatch(uint16 code) {
+inline bool isLevel4DamageLatch(uint16 code) {
switch (code) {
case 0x0008:
case 0x000A:
@@ -138,7 +138,7 @@ static inline bool isLevel4DamageLatch(uint16 code) {
}
}
-static inline bool isLevel6DamageLatch(uint16 code) {
+inline bool isLevel6DamageLatch(uint16 code) {
switch (code) {
case 0x0003:
case 0x0008:
@@ -152,7 +152,7 @@ static inline bool isLevel6DamageLatch(uint16 code) {
}
}
-static inline bool isLevel10DamageLatch(uint16 code) {
+inline bool isLevel10DamageLatch(uint16 code) {
if (code < 0x7F) {
if (code > 0x3D) {
if (code > 0x3E) {
@@ -283,7 +283,7 @@ static inline bool isLevel10DamageLatch(uint16 code) {
return true;
}
-static inline bool hasLevel6PerspectiveHazard(uint16 frame, int16 perspectiveX, int16 perspectiveY) {
+inline bool hasLevel6PerspectiveHazard(uint16 frame, int16 perspectiveX, int16 perspectiveY) {
switch (frame) {
case 0x006A:
case 0x00FD:
@@ -321,7 +321,7 @@ static inline bool hasLevel6PerspectiveHazard(uint16 frame, int16 perspectiveX,
}
}
-static inline bool hasLevel8PerspectiveHazardRoute0(uint16 frame, int16 perspectiveX, int16 perspectiveY) {
+inline bool hasLevel8PerspectiveHazardRoute0(uint16 frame, int16 perspectiveX, int16 perspectiveY) {
switch (frame) {
case 0x00CD:
return perspectiveX < 0x29;
@@ -342,7 +342,7 @@ static inline bool hasLevel8PerspectiveHazardRoute0(uint16 frame, int16 perspect
}
}
-static inline bool hasLevel8PerspectiveHazardRoute1(uint16 frame, int16 perspectiveX, int16 perspectiveY) {
+inline bool hasLevel8PerspectiveHazardRoute1(uint16 frame, int16 perspectiveX, int16 perspectiveY) {
switch (frame) {
case 0x0189:
return perspectiveY < 0x0F;
@@ -360,7 +360,7 @@ static inline bool hasLevel8PerspectiveHazardRoute1(uint16 frame, int16 perspect
}
}
-static inline bool hasLevel8PerspectiveHazardRoute2(uint16 frame, int16 perspectiveX, int16 perspectiveY) {
+inline bool hasLevel8PerspectiveHazardRoute2(uint16 frame, int16 perspectiveX, int16 perspectiveY) {
switch (frame) {
case 0x00BB:
return perspectiveX < 0x29 && perspectiveY < 0x20;
@@ -1431,8 +1431,8 @@ void InsaneRebel1::updateAsteroidPhysics() {
// g_perspectiveX/Y = crosshair center (on-foot targeting)
// Our _perspectiveX/_perspectiveY maps to the camera offset (DAT_000041a0/41a2).
// The crosshair center (0xA3, 0x82) is a separate constant for on-foot mode.
-static const int16 kOnFootCenterX = 0xA3; // g_perspectiveX in HandleGameOp19
-static const int16 kOnFootCenterY = 0x82; // g_perspectiveY in HandleGameOp19
+const int16 kOnFootCenterX = 0xA3; // g_perspectiveX in HandleGameOp19
+const int16 kOnFootCenterY = 0x82; // g_perspectiveY in HandleGameOp19
void InsaneRebel1::updateOnFootPhysics() {
// --- First-frame initialization (0x19 counter==0) ---
diff --git a/engines/scumm/insane/rebel1/levels.cpp b/engines/scumm/insane/rebel1/levels.cpp
index d4681b2d47d..cbe0b1afda2 100644
--- a/engines/scumm/insane/rebel1/levels.cpp
+++ b/engines/scumm/insane/rebel1/levels.cpp
@@ -31,7 +31,7 @@ namespace Scumm {
// From smush/codec1.cpp
void smushDecodeRA1Transparent(byte *dst, const byte *src, int left, int top, int width, int height, int pitch);
-static void decodeBomp(byte *dst, const byte *src, int width, int height, int pitch) {
+void decodeBomp(byte *dst, const byte *src, int width, int height, int pitch) {
while (height--) {
byte *dstNext = dst + pitch;
const byte *srcNext = src + 2 + READ_LE_UINT16(src);
@@ -57,7 +57,7 @@ static void decodeBomp(byte *dst, const byte *src, int width, int height, int pi
}
}
-static void resetSpriteBank(RA1SpriteBank &bank) {
+void resetSpriteBank(RA1SpriteBank &bank) {
delete[] bank.sprites;
bank.sprites = nullptr;
free(bank.decodedData);
diff --git a/engines/scumm/insane/rebel1/menu.cpp b/engines/scumm/insane/rebel1/menu.cpp
index 0bd204db2f9..4f7ef736b5d 100644
--- a/engines/scumm/insane/rebel1/menu.cpp
+++ b/engines/scumm/insane/rebel1/menu.cpp
@@ -32,9 +32,9 @@
namespace Scumm {
-static const int kRA1LevelSelectItemCount = 16; // 15 levels + BACK
-static const int kRA1LevelSelectRowsPerCol = 8;
-static const int kRA1NumLevels = 15;
+const int kRA1LevelSelectItemCount = 16; // 15 levels + BACK
+const int kRA1LevelSelectRowsPerCol = 8;
+const int kRA1NumLevels = 15;
bool InsaneRebel1::notifyEvent(const Common::Event &event) {
if (event.type == Common::EVENT_MOUSEMOVE && !_mouseRecentering) {
@@ -400,7 +400,7 @@ void InsaneRebel1::renderMainMenuOverlay(byte *dst, int pitch, int width, int he
if (_optionsActive) {
// --- Options submenu (matching original RunGameOptionsMenu) ---
- static const char *kDiffNames[3] = { "EASY", "NORMAL", "HARD" };
+ const char *kDiffNames[3] = { "EASY", "NORMAL", "HARD" };
const int titleW = getTalkTextWidth("GAME OPTIONS");
drawTalkText((width - titleW) / 2, 30, "GAME OPTIONS");
@@ -456,7 +456,7 @@ void InsaneRebel1::renderMainMenuOverlay(byte *dst, int pitch, int width, int he
const int titleW = getTalkTextWidth("LEVEL SELECT");
drawTalkText((width - titleW) / 2, 30, "LEVEL SELECT");
- static const char *kLevelItems[kRA1LevelSelectItemCount] = {
+ const char *kLevelItems[kRA1LevelSelectItemCount] = {
" 1 TRAINING",
" 2 ASTEROIDS",
" 3 KOLAADOR",
@@ -510,7 +510,7 @@ void InsaneRebel1::renderMainMenuOverlay(byte *dst, int pitch, int width, int he
}
// --- Main menu ---
- static const char *kMenuItems[5] = {
+ const char *kMenuItems[5] = {
"START NEW GAME",
"GAME OPTIONS",
"LEVEL SELECT",
diff --git a/engines/scumm/insane/rebel1/rebel.cpp b/engines/scumm/insane/rebel1/rebel.cpp
index 544cb026cab..e7ac7a13a0a 100644
--- a/engines/scumm/insane/rebel1/rebel.cpp
+++ b/engines/scumm/insane/rebel1/rebel.cpp
@@ -39,7 +39,7 @@ namespace Scumm {
// Per-difficulty tuning tables from assault_data_3.bin (also loadable from C:\rebltune.txt)
// 21 sub-levels x 3 difficulties x 13 fields
// Fields: roll, lift, slide, drift, snap, miss, wham, shot, kill, time, levelPts, bonus, flags
-static const int16 kTuningTable[21][3][13] = {
+const int16 kTuningTable[21][3][13] = {
// Sub-level 0: "1A" (Flight Training - canyon flight)
{
{ 100, 100, 60, 110, 0, 0, 15, 0, 0, 5, 500, 100, 2048 }, // Easy
@@ -167,7 +167,7 @@ static const int16 kTuningTable[21][3][13] = {
{ 0, 0, 0, 0, 2, 22, 35, 4, 75, 10, 1500, 500, 2050 }, // Hard
},
};
-static const int kNumTunedLevels = 21;
+const int kNumTunedLevels = 21;
void InsaneRebel1::loadTuningForLevel(int level) {
@@ -319,7 +319,7 @@ InsaneRebel1::InsaneRebel1(ScummEngine_v7 *scumm) : Insane(), _vm(scumm) {
_optVolume = _vm->_mixer->getVolumeForSoundType(Audio::Mixer::kPlainSoundType) * 127 / Audio::Mixer::kMaxChannelVolume;
// Default high scores â from DS:0x1D0/0x298/0x2C0
- static const struct { const char *name; int32 score; byte difficulty; } kDefaultScores[kHighScoreCount] = {
+ const struct { const char *name; int32 score; byte difficulty; } kDefaultScores[kHighScoreCount] = {
{"Vince", 10000, 2}, {"Tamlynn", 9000, 2}, {"Chip", 8000, 2},
{"Brett", 7000, 1}, {"Casey", 6000, 1}, {"Justin", 5000, 1},
{"Bill", 4000, 0}, {"Aaron", 3000, 0}, {"Mary", 2000, 0},
diff --git a/engines/scumm/insane/rebel1/render.cpp b/engines/scumm/insane/rebel1/render.cpp
index 2b6719f5a40..c54c7580478 100644
--- a/engines/scumm/insane/rebel1/render.cpp
+++ b/engines/scumm/insane/rebel1/render.cpp
@@ -27,7 +27,7 @@
namespace Scumm {
-static inline int ra1OverlayViewOffsetX(const InsaneRebel1 *rebel1) {
+inline int ra1OverlayViewOffsetX(const InsaneRebel1 *rebel1) {
if (!rebel1 || !rebel1->isInteractiveVideoActive())
return 0;
@@ -37,14 +37,14 @@ static inline int ra1OverlayViewOffsetX(const InsaneRebel1 *rebel1) {
return (rebel1->getEffectiveGameOpcode() == 0x0B) ? rebel1->getPerspectiveX() : 0;
}
-static inline int ra1OverlayViewOffsetY(const InsaneRebel1 *rebel1) {
+inline int ra1OverlayViewOffsetY(const InsaneRebel1 *rebel1) {
if (!rebel1 || !rebel1->isInteractiveVideoActive())
return 0;
return (rebel1->getEffectiveGameOpcode() == 0x0B) ? rebel1->getPerspectiveY() : 0;
}
-static void drawBankString(const RA1SpriteBank &bank, byte *dst, int pitch, int width, int height,
+void drawBankString(const RA1SpriteBank &bank, byte *dst, int pitch, int width, int height,
int x, int y, const char *text) {
if (!dst || !text || bank.numSprites <= 0)
return;
@@ -107,7 +107,7 @@ static void drawBankString(const RA1SpriteBank &bank, byte *dst, int pitch, int
}
}
-static const RA1Sprite *lookupBankGlyph(const RA1SpriteBank &bank, char ch) {
+const RA1Sprite *lookupBankGlyph(const RA1SpriteBank &bank, char ch) {
if (bank.numSprites <= 0)
return nullptr;
if ((byte)ch < 0x21)
@@ -127,7 +127,7 @@ static const RA1Sprite *lookupBankGlyph(const RA1SpriteBank &bank, char ch) {
// Glyph markers in FUN_1C940/FUN_1CB22 go through DrawStringEx(..., flags=3),
// which centers the glyph and ignores the NUT x/y offsets. Use the same anchor
// rules here instead of the generic left-anchored text path.
-static void drawCenteredBankGlyph(const RA1SpriteBank &bank, byte *dst, int pitch, int width, int height,
+void drawCenteredBankGlyph(const RA1SpriteBank &bank, byte *dst, int pitch, int width, int height,
int centerX, int centerY, char ch) {
char glyphStr[2] = { ch, '\0' };
const RA1Sprite *glyph = lookupBankGlyph(bank, ch);
@@ -141,7 +141,7 @@ static void drawCenteredBankGlyph(const RA1SpriteBank &bank, byte *dst, int pitc
drawBankString(bank, dst, pitch, width, height, drawX, drawY, glyphStr);
}
-static int getBankStringWidth(const RA1SpriteBank &bank, const char *text) {
+int getBankStringWidth(const RA1SpriteBank &bank, const char *text) {
if (!text || bank.numSprites <= 0)
return 0;
@@ -169,14 +169,14 @@ static int getBankStringWidth(const RA1SpriteBank &bank, const char *text) {
// Approximate FUN_221B7/FUN_20BD3 space-advance behavior from available NUT glyphs.
// The original reads per-font space width from metadata tables and caps it to 8.
-static int getBankSpaceAdvance(const RA1SpriteBank &bank) {
+int getBankSpaceAdvance(const RA1SpriteBank &bank) {
const int exclWidth = getBankStringWidth(bank, "!");
if (exclWidth <= 0)
return 6;
return MIN(exclWidth, 8);
}
-static const RA1SpriteBank &selectLayerBank(const RA1SpriteBank &titleBank,
+const RA1SpriteBank &selectLayerBank(const RA1SpriteBank &titleBank,
const RA1SpriteBank &hudBank, const RA1SpriteBank &techBank, int layer) {
const bool techLayer = (layer >= 2);
const bool talkLayer = (layer == 1);
@@ -187,7 +187,7 @@ static const RA1SpriteBank &selectLayerBank(const RA1SpriteBank &titleBank,
return (titleBank.numSprites > 0) ? titleBank : hudBank;
}
-static int getBankSpaceHeight(const RA1SpriteBank &bank) {
+int getBankSpaceHeight(const RA1SpriteBank &bank) {
// In FUN_221B7 line advance is derived from the layer's space-glyph height (+4).
// With current NUT decoding we approximate that using the '!' glyph (index 0).
if (bank.numSprites > 0) {
@@ -199,7 +199,7 @@ static int getBankSpaceHeight(const RA1SpriteBank &bank) {
}
// FUN_1C794: direction bucket in range -4..4 from two points.
-static int ra1ShotDirection(int16 x1, int16 y1, int16 x2, int16 y2) {
+int ra1ShotDirection(int16 x1, int16 y1, int16 x2, int16 y2) {
int dx = x2 - x1;
int dy = y1 - y2;
if (dy < 0) {
@@ -232,7 +232,7 @@ static int ra1ShotDirection(int16 x1, int16 y1, int16 x2, int16 y2) {
}
// FUN_1CDA7 maps abs(FUN_1C794) to sprite base index: <=1 -> 0, ==2 -> 5, else -> 10.
-static int ra1ShotDirectionBucket(int dir) {
+int ra1ShotDirectionBucket(int dir) {
const int absDir = ABS(dir);
if (absDir <= 1)
return 0;
@@ -249,7 +249,7 @@ struct RA1ShotEmitterPair {
};
// DAT_244A and DAT_251A in ASSAULT.EXE data section, used by FUN_1D79C.
-static const RA1ShotEmitterPair kRA1ShotEmitters244A[27] = {
+const RA1ShotEmitterPair kRA1ShotEmitters244A[27] = {
{ 11, -11, -11, 0 }, { 16, -9, -16, -1 }, { 20, -6, -19, -3 }, { 20, -5, -21, -4 }, { -20, -6, 20, -5 },
{ -18, -9, 16, -1 }, { -13, -11, 13, 0 }, { -7, -13, 8, 2 }, { 1, -10, 3, 2 }, { 11, -16, -11, 4 },
{ 16, -14, -15, 1 }, { 19, -10, -19, -2 }, { 20, -5, -20, -4 }, { -20, -8, 19, -2 }, { -17, -11, 17, 1 },
@@ -258,7 +258,7 @@ static const RA1ShotEmitterPair kRA1ShotEmitters244A[27] = {
{ -5, -18, 9, 6 }, { -1, -11, -3, -6 }
};
-static const RA1ShotEmitterPair kRA1ShotEmitters251A[27] = {
+const RA1ShotEmitterPair kRA1ShotEmitters251A[27] = {
{ -1, -11, -3, -6 }, { 7, -12, -8, 1 }, { 14, -11, -12, 0 }, { 18, -9, -17, -1 }, { 21, -7, -19, -4 },
{ -20, -6, 21, -5 }, { -18, -8, 19, -2 }, { -16, -10, 16, -1 }, { -11, -12, 11, 0 }, { 1, -18, -2, -1 },
{ 8, -17, -5, 1 }, { 13, -15, -12, 2 }, { 17, -13, -15, 0 }, { 21, -8, -19, -2 }, { -19, -6, 21, -4 },
@@ -269,7 +269,7 @@ static const RA1ShotEmitterPair kRA1ShotEmitters251A[27] = {
// DAT_25EC/DAT_25F0 and DAT_28BC in ASSAULT.EXE. GAME opcode 0x09 uses these
// emitter offsets instead of the generic edge-beam fallback.
-static const RA1ShotEmitterPair kRA1FlightShotEmitters25EC[45] = {
+const RA1ShotEmitterPair kRA1FlightShotEmitters25EC[45] = {
{ -38, -14, 37, 6 }, { 37, -14, 37, 7 }, { 42, -11, -40, 11 }, { -37, -6, 38, 14 }, { -37, -5, 38, 15 },
{ -35, -19, 36, 11 }, { -35, -18, 35, 12 }, { -37, -15, 36, 16 }, { -37, -11, 34, 19 }, { -37, -10, 34, 20 },
{ -31, -24, 33, 16 }, { -31, -23, 33, 17 }, { -32, -19, 33, 22 }, { -34, -17, 29, 24 }, { -35, -15, 28, 25 },
@@ -281,7 +281,7 @@ static const RA1ShotEmitterPair kRA1FlightShotEmitters25EC[45] = {
{ 18, -32, -26, 25 }, { 19, -31, -25, 26 }, { 22, -28, -22, 29 }, { 25, -24, -19, 31 }, { 25, -24, -17, 31 }
};
-static const RA1ShotEmitterPair kRA1FlightShotEmitters25F0[45] = {
+const RA1ShotEmitterPair kRA1FlightShotEmitters25F0[45] = {
{ 37, -14, -37, 6 }, { -38, -12, -36, 7 }, { -41, -10, 41, 10 }, { 39, -6, -36, 13 }, { 38, -5, -36, 15 },
{ 41, -8, -39, 1 }, { 40, -7, -38, 1 }, { 41, -4, -40, 5 }, { 39, -1, -40, 9 }, { 39, 1, -40, 10 },
{ -38, -5, 42, -3 }, { -39, -4, 42, -1 }, { -43, 0, 40, 2 }, { -41, 2, 39, 5 }, { -42, 4, 37, 6 },
@@ -293,7 +293,7 @@ static const RA1ShotEmitterPair kRA1FlightShotEmitters25F0[45] = {
{ 32, -15, -42, 10 }, { 34, -14, -40, 11 }, { 36, -12, -38, 13 }, { 39, -10, -35, 17 }, { 41, -9, -33, 17 }
};
-static const RA1ShotEmitterPair kRA1FlightShotEmitters28BC[45] = {
+const RA1ShotEmitterPair kRA1FlightShotEmitters28BC[45] = {
{ -18, 0, 18, 0 }, { -18, -1, 18, 0 }, { -18, 0, 18, 0 }, { -18, 0, 17, 0 }, { -18, -1, 18, -1 },
{ -14, -3, 19, 3 }, { -15, -5, 20, 0 }, { -17, -4, 18, 1 }, { -15, -4, 18, 1 }, { -19, -4, 19, 2 },
{ -13, -9, 20, 2 }, { -16, -8, 19, 3 }, { -15, -9, 21, 3 }, { -14, -4, 18, 5 }, { -14, -2, 17, 6 },
@@ -306,7 +306,7 @@ static const RA1ShotEmitterPair kRA1FlightShotEmitters28BC[45] = {
};
// Small subset of FUN_20D43 draw flags used by RA1 shot sprites.
-static void renderSpriteWithFlags(byte *dst, int pitch, int width, int height,
+void renderSpriteWithFlags(byte *dst, int pitch, int width, int height,
int x, int y, const RA1Sprite &spr, uint32 flags) {
if (!spr.data || spr.width <= 0 || spr.height <= 0)
return;
@@ -604,7 +604,7 @@ void InsaneRebel1::renderTargetBoxes(byte *dst, int pitch, int width, int height
// The original does not draw a hardcoded pixel cross; it renders glyph markers
// whose state depends on _targetProximity.
void InsaneRebel1::renderTargeting(byte *dst, int pitch, int width, int height) {
- static const char kRA1TorpedoIndicator[] = "<d";
+ const char kRA1TorpedoIndicator[] = "<d";
const RA1SpriteBank &markerBank = (_techFontBank.numSprites > 0) ? _techFontBank : _hudFontBank;
const int overlayX = ra1OverlayViewOffsetX(this);
const int overlayY = ra1OverlayViewOffsetY(this);
@@ -725,8 +725,8 @@ void InsaneRebel1::renderGostSlots(byte *dst, int pitch, int width, int height)
// renderLaserShots â FUN_1CDA7/FUN_1D79C/HandleGameOp1A shot visual path.
void InsaneRebel1::renderLaserShots(byte *dst, int pitch, int width, int height) {
- static const char kRA1TorpedoTrailLeft[] = "<<&";
- static const char kRA1TorpedoTrailRight[] = "<<'";
+ const char kRA1TorpedoTrailLeft[] = "<<&";
+ const char kRA1TorpedoTrailRight[] = "<<'";
const bool torpedoMode = (_gameplayFlags75ff & 0x2) != 0;
if (_laserBank.numSprites <= 0 && !torpedoMode)
@@ -734,16 +734,16 @@ void InsaneRebel1::renderLaserShots(byte *dst, int pitch, int width, int height)
// DAT_2407 lookup used by FUN_1CDA7/FUN_1D79C for timer 1..5 interpolation.
// Entry 0 unused.
- static const int kShotLerpByTimer[6] = { 0, 8, 7, 6, 4, 0 };
+ const int kShotLerpByTimer[6] = { 0, 8, 7, 6, 4, 0 };
// DAT_2413: on-foot lerp table (timer 5 = 1, not 0 like flight mode).
- static const int kOnFootShotLerp[6] = { 0, 8, 7, 6, 4, 1 };
+ const int kOnFootShotLerp[6] = { 0, 8, 7, 6, 4, 1 };
// DAT_240e: gun barrel X offset indexed by shipDirIndex (for timer==5 first frame).
- static const int16 kOnFootGunBarrelX[20] = {
+ const int16 kOnFootGunBarrelX[20] = {
4, 0, 8, 8, 7, 6, 4, 1, 0, 0,
0, -56, -47, -23, -13, 0, 13, 30, 54, 59
};
// DAT_2420: gun barrel Y offset indexed by shipDirIndex (for timer==5 first frame).
- static const int16 kOnFootGunBarrelY[20] = {
+ const int16 kOnFootGunBarrelY[20] = {
0, 0, -56, -47, -23, -13, 0, 13, 30, 54,
59, -3, -19, -24, -30, -28, -30, -29, -20, -5
};
@@ -947,7 +947,7 @@ void InsaneRebel1::renderLaserShots(byte *dst, int pitch, int width, int height)
// Level intro title data (from RunTwoLineTextSplash calls in original binary).
// titleText is drawn at y=10, subtitleText at y=25 (matching original DrawUiString positions).
// revealStartFrame/revealEndFrame control the frame range during which text is visible.
-static const struct {
+const struct {
const char *titleText; // Top line (chapter number)
const char *subtitleText; // Bottom line (level name)
int16 revealStartFrame; // Frame at which text begins appearing
diff --git a/engines/scumm/insane/rebel1/runlevels.cpp b/engines/scumm/insane/rebel1/runlevels.cpp
index 1052fef9ad2..68d9c2f404a 100644
--- a/engines/scumm/insane/rebel1/runlevels.cpp
+++ b/engines/scumm/insane/rebel1/runlevels.cpp
@@ -37,7 +37,7 @@ struct RA1Level7ResumeSegment {
int16 localStart;
};
-static const RA1Level7ResumeSegment kLevel7ResumeSegments[6][4] = {
+const RA1Level7ResumeSegment kLevel7ResumeSegments[6][4] = {
{
{ 0, 638, 0 },
{ 1416, 1468, 639 },
@@ -76,7 +76,7 @@ static const RA1Level7ResumeSegment kLevel7ResumeSegments[6][4] = {
}
};
-static int32 mapLevel7TimelineFrameToLocal(int route, int32 timelineFrame) {
+int32 mapLevel7TimelineFrameToLocal(int route, int32 timelineFrame) {
if (timelineFrame <= 0)
return 0;
@@ -102,7 +102,7 @@ static int32 mapLevel7TimelineFrameToLocal(int route, int32 timelineFrame) {
return 0;
}
-static int32 findAnimFrameChunkOffset(ScummEngine_v7 *vm, const char *filename, int32 targetFrame) {
+int32 findAnimFrameChunkOffset(ScummEngine_v7 *vm, const char *filename, int32 targetFrame) {
if (targetFrame <= 0)
return 0;
@@ -734,7 +734,7 @@ bool InsaneRebel1::runLevel6() {
bool InsaneRebel1::runLevel7() {
debug(1, "InsaneRebel1: Running level 7");
- static const char *const kLevel7Segments[] = {
+ const char *const kLevel7Segments[] = {
"LVL7/L7PLAY1.ANM",
"LVL7/L7PLAY2.ANM",
"LVL7/L7PLAY3.ANM",
@@ -842,7 +842,7 @@ bool InsaneRebel1::runLevel7() {
bool InsaneRebel1::runLevel8() {
debug(1, "InsaneRebel1: Running level 8");
- static const char *const kLevel8Routes[] = {
+ const char *const kLevel8Routes[] = {
"LVL8/L8PLAY.ANM",
"LVL8/L8PLAY2.ANM",
"LVL8/L8PLAY3.ANM"
@@ -1655,7 +1655,7 @@ bool InsaneRebel1::runLevel15() {
// Matches original flow at 0x15597: intro â menu â level.
void InsaneRebel1::runGame() {
typedef bool (InsaneRebel1::*RunLevelMethod)();
- static const RunLevelMethod kLevelRunners[] = {
+ const RunLevelMethod kLevelRunners[] = {
&InsaneRebel1::runLevel1,
&InsaneRebel1::runLevel2,
&InsaneRebel1::runLevel3,
diff --git a/engines/scumm/insane/rebel2/audio.cpp b/engines/scumm/insane/rebel2/audio.cpp
index f679b70f47d..14ace0f131a 100644
--- a/engines/scumm/insane/rebel2/audio.cpp
+++ b/engines/scumm/insane/rebel2/audio.cpp
@@ -275,7 +275,7 @@ void InsaneRebel2::processAudioFrame(int16 feedSize) {
// Standalone SAUD files from SYSTM/ loaded at init for one-shot SFX.
// Original: FUN_0042a3b0 loads into DAT_00456888[0..7].
-static const char *const kRA2SfxFiles[InsaneRebel2::kRA2NumSfx] = {
+const char *const kRA2SfxFiles[InsaneRebel2::kRA2NumSfx] = {
"SYSTM/BLAST.SAD", // 0 - Player laser fire
"SYSTM/CRASH.SAD", // 1 - Corridor/wall collision
"SYSTM/EXPLODE.SAD", // 2 - Enemy explosion
diff --git a/engines/scumm/insane/rebel2/levels.cpp b/engines/scumm/insane/rebel2/levels.cpp
index 01c49a03452..97eb5bf40d8 100644
--- a/engines/scumm/insane/rebel2/levels.cpp
+++ b/engines/scumm/insane/rebel2/levels.cpp
@@ -218,7 +218,7 @@ void InsaneRebel2::playLevelBegin(int levelId) {
// Table of per-level text overlay parameters
// All levels use FUN_004171c5 (verified against decompiled level handlers)
// Text IDs are sequential: 0xAA (level 1) through 0xB8 (level 15)
- static const TextOverlayParams levelTextParams[16] = {
+ const TextOverlayParams levelTextParams[16] = {
{ -1, 0, 0, 0, 0}, // Level 0 (unused)
{0xAA, 0xA0, 10, 5, 0x4B}, // Level 1: FUN_00417E53
{0xAB, 0xA0, 10, 2, 0x46}, // Level 2: FUN_00418063
@@ -605,7 +605,7 @@ int InsaneRebel2::runLevel(int levelId) {
// Levels 1-6 use types 0-5, but Level 6 also uses type 6 mid-level.
// Levels 7-15 use types 7-15 (gap at type 6 which is Level 6 phase 2).
// Level 15 also switches to type 16 mid-level at frame 0x21e.
- static const int kLevelTypeMap[16] = {
+ const int kLevelTypeMap[16] = {
-1, 0, 1, 2, 3, 4, 5, 7, 8, 9, 10, 11, 12, 13, 14, 15
};
_rebelLevelType = kLevelTypeMap[levelId];
diff --git a/engines/scumm/insane/rebel2/menu.cpp b/engines/scumm/insane/rebel2/menu.cpp
index f631909a4f6..d95d3ba7cf5 100644
--- a/engines/scumm/insane/rebel2/menu.cpp
+++ b/engines/scumm/insane/rebel2/menu.cpp
@@ -284,9 +284,7 @@ void InsaneRebel2::drawMenuItems(byte *renderBitmap, int pitch, int width, int h
// Flash color: (-((DAT_0047a7e4 & 1) == 0) & 8U) - 0x10
// bit0==0: 8-16=248(0xF8), bit0==1: 0-16=240(0xF0)
- static int frameCounter = 0;
- frameCounter++;
- byte highlightColor = ((frameCounter / 8) & 1) ? 248 : 240;
+ byte highlightColor = ((_vm->_system->getMillis() / 133) & 1) ? 248 : 240;
// Box position: Y = itemY - 1 (0x67 vs 0x68)
int leftX = leftAligned ? 20 : (centerX - bracketWidth / 2);
@@ -447,7 +445,7 @@ void InsaneRebel2::drawMenuOverlay(byte *renderBitmap, int pitch, int width, int
// ---------------------------------------------------------------------------
// pauseFillRect -- Helper to fill a rectangle in the frame buffer with bounds checking.
-static void pauseFillRect(byte *buf, int bufW, int bufH, int x, int y, int w, int h, byte color) {
+void pauseFillRect(byte *buf, int bufW, int bufH, int x, int y, int w, int h, byte color) {
if (x < 0) { w += x; x = 0; }
if (y < 0) { h += y; y = 0; }
if (x + w > bufW) w = bufW - x;
@@ -1128,7 +1126,7 @@ Common::String InsaneRebel2::getRankString(int rating) {
// Password table lookup - emulates FUN_0041BCE0
// 90 entries: 15 levels à 6 difficulty slots, extracted from RA2WIN95.EXE at 0x481AF0
// Index formula: difficulty + (level * 3 - 3) * 2, level is 1-based (1-15), difficulty 0-5
-static const char *const kPasswordTable[90] = {
+const char *const kPasswordTable[90] = {
// Level 1: diff 0-5
"JABBA", "EWOKS", "BANTHA", "ANAKIN", "WOOKIEE", "WOOKIEE",
// Level 2: diff 0-5
@@ -1205,9 +1203,7 @@ void InsaneRebel2::drawChapterInfoLine(byte *renderBitmap, int pitch, int width,
if (!lockStr || !lockStr[0])
lockStr = "^f01^c248UNREGISTERED - PASSCODE REQUIRED";
- static int cursorCounter = 0;
- cursorCounter++;
- char cursor = ((cursorCounter / 8) & 1) ? '_' : ' ';
+ char cursor = ((_vm->_system->getMillis() / 133) & 1) ? '_' : ' ';
Common::String displayStr = Common::String::format("%s ^c005%s%c",
lockStr, _passwordInput.c_str(), cursor);
@@ -1985,7 +1981,7 @@ void InsaneRebel2::drawOptionsOverlay(byte *renderBitmap, int pitch, int width,
items[6] = _optRapidFire ? "^f01^c005Rapid Fire On" : "^f01^c005Rapid Fire Off";
// [7] Volume Level (slider) â TRS 103 = "^f01^c005Volume Level: %hd%%"
- static char volumeBuf[64];
+ char volumeBuf[64];
const char *volFmt = splayer->getString(103);
if (volFmt && volFmt[0])
Common::sprintf_s(volumeBuf, volFmt, (short)(_optVolumeLevel * 100 / 127));
diff --git a/engines/scumm/insane/rebel2/rebel.cpp b/engines/scumm/insane/rebel2/rebel.cpp
index 7231de9997a..17e712ec3a1 100644
--- a/engines/scumm/insane/rebel2/rebel.cpp
+++ b/engines/scumm/insane/rebel2/rebel.cpp
@@ -951,8 +951,8 @@ void InsaneRebel2::renderScoreHUD(byte *renderBitmap, int pitch, int width, int
// Save/load pilot profiles using ScummVM's save file system.
// Original: FUN_00411980 (load) / FUN_00411A5D (save).
-static const uint32 kPilotSaveMagic = MKTAG('R', 'A', '2', 'P');
-static const uint16 kPilotSaveVersion = 2;
+const uint32 kPilotSaveMagic = MKTAG('R', 'A', '2', 'P');
+const uint16 kPilotSaveVersion = 2;
// loadPilots -- Load all pilot profiles from save files (FUN_00411980).
bool InsaneRebel2::loadPilots() {
diff --git a/engines/scumm/insane/rebel2/render.cpp b/engines/scumm/insane/rebel2/render.cpp
index ef0c79af23c..43fa7090935 100644
--- a/engines/scumm/insane/rebel2/render.cpp
+++ b/engines/scumm/insane/rebel2/render.cpp
@@ -728,7 +728,7 @@ void InsaneRebel2::drawTexturedLine(byte *dst, int pitch, int width, int height,
}
// drawTexturedSegment -- Textured segment between two points (FUN_00429360 port).
-static void drawTexturedSegment(byte *dst, int pitch, int width, int height,
+void drawTexturedSegment(byte *dst, int pitch, int width, int height,
int param_3, int param_4, int param_5, int param_6, int param_7, const byte *param_8,
int clipLeft, int clipTop, int clipRight, int clipBottom) {
// Near-direct port of FUN_00429360.
@@ -2093,7 +2093,7 @@ void InsaneRebel2::renderNutSprite(byte *dst, int pitch, int width, int height,
}
// renderNutSpriteClipped -- Draw a NUT sprite with explicit clip rectangle.
-static void renderNutSpriteClipped(byte *dst, int pitch, int dstH,
+void renderNutSpriteClipped(byte *dst, int pitch, int dstH,
int clipLeft, int clipTop, int clipRight, int clipBottom,
int x, int y, NutRenderer *nut, int spriteIdx) {
if (!nut || spriteIdx < 0 || spriteIdx >= nut->getNumChars())
@@ -4234,10 +4234,7 @@ void InsaneRebel2::renderCrosshair(byte *renderBitmap, int pitch, int width, int
reticleIndex = 47; // 0x2F
break;
case 0x26: { // Turret/Cockpit - animated crosshair
- static int turretAnimCounter = 0;
- turretAnimCounter++;
-
- int animOffset = (_targetLockTimer == 0) ? 0 : 3 - (turretAnimCounter & 3);
+ int animOffset = (_targetLockTimer == 0) ? 0 : 3 - ((_vm->_system->getMillis() / 33) & 3);
if (_rebelLevelType == 5) {
reticleIndex = 0x30 + animOffset;
diff --git a/engines/scumm/insane/rebel2/runlevels.cpp b/engines/scumm/insane/rebel2/runlevels.cpp
index 75da1488559..44bab3adfd0 100644
--- a/engines/scumm/insane/rebel2/runlevels.cpp
+++ b/engines/scumm/insane/rebel2/runlevels.cpp
@@ -215,7 +215,7 @@ int InsaneRebel2::runLevel2() {
// Each phase gets a budget = tableBase + random(3). processWaveEnd() uses
// this budget to randomly redistribute kill credits, creating non-deterministic
// wave progression. Using calibrated defaults until exact table values extracted.
- static const int16 kLevel2BudgetBase[3] = { 3, 3, 3 }; // Phase 1, 2, 3
+ const int16 kLevel2BudgetBase[3] = { 3, 3, 3 }; // Phase 1, 2, 3
SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
int bonusCount = 0; // local_1c: tracks bonus events (DAT_0047ab9c & 0x10)
@@ -1218,7 +1218,7 @@ int InsaneRebel2::runLevel11() {
int prevPhaseState = 0;
// Kill credit budget bases per phase (from level data table DAT_0047e0e8)
- static const int16 kLevel11BudgetBase[4] = { 3, 3, 3, 3 };
+ const int16 kLevel11BudgetBase[4] = { 3, 3, 3, 3 };
// Play cutscene (11CUT.SAN)
playCinematic("LEV11/11CUT.SAN");
@@ -1607,7 +1607,7 @@ int InsaneRebel2::runLevel12() {
SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
// Kill credit budget bases per phase
- static const int16 kLevel12BudgetBase[4] = { 3, 4, 4, 4 };
+ const int16 kLevel12BudgetBase[4] = { 3, 4, 4, 4 };
// Play cutscene (12CUT.SAN)
playCinematic("LEV12/12CUT.SAN");
Commit: 8b81bcfb80e42cd41668bdd327a3ecb07de56c78
https://github.com/scummvm/scummvm/commit/8b81bcfb80e42cd41668bdd327a3ecb07de56c78
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:51+02:00
Commit Message:
SCUMM: RA2: Fix mixed indentation
Changed paths:
engines/scumm/insane/rebel2/iact.cpp
diff --git a/engines/scumm/insane/rebel2/iact.cpp b/engines/scumm/insane/rebel2/iact.cpp
index 2d653a2cf16..77de59759ad 100644
--- a/engines/scumm/insane/rebel2/iact.cpp
+++ b/engines/scumm/insane/rebel2/iact.cpp
@@ -392,7 +392,7 @@ void InsaneRebel2::iactRebel2Opcode2(Common::SeekableReadStream &b, int16 par2,
}
} else if (value > 0x3ff) { // Bitmask case: value > 0x3FF
- for (int slot = 1; slot <= 9; ++slot) {
+ for (int slot = 1; slot <= 9; ++slot) {
if ((value & (1 << (slot - 1))) != 0) {
if (!isBitSet(targetId)) {
_rebelMaskCounters[slot]++;
Commit: 41a8508ee0c5398814b3616f4e38f9d2202ae353
https://github.com/scummvm/scummvm/commit/41a8508ee0c5398814b3616f4e38f9d2202ae353
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:51+02:00
Commit Message:
SCUMM: RA: Clean up spacing and indentation
Changed paths:
engines/scumm/insane/rebel1/rebel.cpp
engines/scumm/insane/rebel1/rebel.h
engines/scumm/insane/rebel2/iact.cpp
engines/scumm/insane/rebel2/rebel.cpp
engines/scumm/insane/rebel2/rebel.h
engines/scumm/insane/rebel2/render.cpp
engines/scumm/metaengine.cpp
diff --git a/engines/scumm/insane/rebel1/rebel.cpp b/engines/scumm/insane/rebel1/rebel.cpp
index e7ac7a13a0a..f108f223686 100644
--- a/engines/scumm/insane/rebel1/rebel.cpp
+++ b/engines/scumm/insane/rebel1/rebel.cpp
@@ -287,16 +287,16 @@ InsaneRebel1::InsaneRebel1(ScummEngine_v7 *scumm) : Insane(), _vm(scumm) {
_maxChapterUnlocked = 0;
_interactiveVideoActive = false;
_gameCounter = 0;
- _pathBranchEnabled = false;
- _rightPathSelected = false;
- _levelRouteIndex = -1;
- _pendingRouteIndex = -1;
- _pendingRouteStartFrame = 0;
- _pendingRouteCutoverFrame = -1;
- _levelRouteChoice = 0;
- _levelGameplayPhase = 0;
- _level5SuccessFramesRemaining = 0;
- _menuActive = false;
+ _pathBranchEnabled = false;
+ _rightPathSelected = false;
+ _levelRouteIndex = -1;
+ _pendingRouteIndex = -1;
+ _pendingRouteStartFrame = 0;
+ _pendingRouteCutoverFrame = -1;
+ _levelRouteChoice = 0;
+ _levelGameplayPhase = 0;
+ _level5SuccessFramesRemaining = 0;
+ _menuActive = false;
_introTextActive = false;
_introTextStartFrame = 0;
_introTextEndFrame = 0;
diff --git a/engines/scumm/insane/rebel1/rebel.h b/engines/scumm/insane/rebel1/rebel.h
index c230a44aae3..5a85489b373 100644
--- a/engines/scumm/insane/rebel1/rebel.h
+++ b/engines/scumm/insane/rebel1/rebel.h
@@ -251,24 +251,24 @@ private:
int16 _mouseOffsetX; // 0x9762-style accumulated recenter offset in DOS 640-space
int16 _mouseOffsetY; // 0x9760-style accumulated recenter offset in DOS 200-space
int16 _mouseBiasX; // 0x9774: current preprocessed horizontal bias
- int16 _mouseBiasY; // 0x9772: current preprocessed vertical bias
- int16 _mousePrevBiasX; // 0x9770: previous-frame biasX
- int16 _mousePrevBiasY; // 0x976E: previous-frame biasY
- bool _mouseBiasLatch; // 0x4486: one-frame large-jump latch
- bool _mouseRecentering; // 0x976D: suppress recursive updates during warp
- int16 _joystickAxisX; // Rebel-specific left-stick X captured from keymapper axis events
- int16 _joystickAxisY; // Rebel-specific left-stick Y captured from keymapper axis events
- int16 _level2JoystickFilteredX; // Smoothed Level 2 analog X input
- int16 _level2JoystickFilteredY; // Smoothed Level 2 analog Y input
- enum InputSource {
- kInputSourceMouse,
- kInputSourceJoystickAnalog,
- kInputSourceJoystickDigital
- };
- InputSource _activeInputSource;
-
- // 0x0B handler physics update (asteroid/surface levels)
- void updateAsteroidPhysics();
+ int16 _mouseBiasY; // 0x9772: current preprocessed vertical bias
+ int16 _mousePrevBiasX; // 0x9770: previous-frame biasX
+ int16 _mousePrevBiasY; // 0x976E: previous-frame biasY
+ bool _mouseBiasLatch; // 0x4486: one-frame large-jump latch
+ bool _mouseRecentering; // 0x976D: suppress recursive updates during warp
+ int16 _joystickAxisX; // Rebel-specific left-stick X captured from keymapper axis events
+ int16 _joystickAxisY; // Rebel-specific left-stick Y captured from keymapper axis events
+ int16 _level2JoystickFilteredX; // Smoothed Level 2 analog X input
+ int16 _level2JoystickFilteredY; // Smoothed Level 2 analog Y input
+ enum InputSource {
+ kInputSourceMouse,
+ kInputSourceJoystickAnalog,
+ kInputSourceJoystickDigital
+ };
+ InputSource _activeInputSource;
+
+ // 0x0B handler physics update (asteroid/surface levels)
+ void updateAsteroidPhysics();
// 0x19/0x1A on-foot handler (Level 9 Stormtroopers)
void updateOnFootPhysics();
@@ -377,16 +377,16 @@ private:
// Path branching for levels with left/right alternative videos.
// Original sets nextSceneA/nextSceneB when GAME 0x07 counter == 394 (0x18A).
// We check ship position at that counter value to decide left vs right path.
- static const int32 kPathBranchCounter = 394; // GAME 0x07 field1 value
- int32 _gameCounter; // GAME 0x07 field1 â the original's _DAT_7740
- bool _pathBranchEnabled; // True when branching is active for this video
- bool _rightPathSelected; // True if player chose the right/easy path
- int _levelRouteIndex; // Current mid-level route/segment for branching levels
- int _pendingRouteIndex; // Next route requested by original frame-branch logic
- int32 _pendingRouteStartFrame; // Resume frame for branch-driven route switches
- int32 _pendingRouteCutoverFrame; // Delayed inline route splice frame (Level 7 uses branchFrame + 7)
- int _levelRouteChoice; // Level-local pending branch choice (0=none, 1=left, 2=right)
- int _levelGameplayPhase; // Level-local interactive phase (e.g. LVL4 PLAY1 vs PLAY2)
+ static const int32 kPathBranchCounter = 394; // GAME 0x07 field1 value
+ int32 _gameCounter; // GAME 0x07 field1 â the original's _DAT_7740
+ bool _pathBranchEnabled; // True when branching is active for this video
+ bool _rightPathSelected; // True if player chose the right/easy path
+ int _levelRouteIndex; // Current mid-level route/segment for branching levels
+ int _pendingRouteIndex; // Next route requested by original frame-branch logic
+ int32 _pendingRouteStartFrame; // Resume frame for branch-driven route switches
+ int32 _pendingRouteCutoverFrame; // Delayed inline route splice frame (Level 7 uses branchFrame + 7)
+ int _levelRouteChoice; // Level-local pending branch choice (0=none, 1=left, 2=right)
+ int _levelGameplayPhase; // Level-local interactive phase (e.g. LVL4 PLAY1 vs PLAY2)
// Main menu / options state
void runOptionsMenu();
diff --git a/engines/scumm/insane/rebel2/iact.cpp b/engines/scumm/insane/rebel2/iact.cpp
index 77de59759ad..f05bb063e1e 100644
--- a/engines/scumm/insane/rebel2/iact.cpp
+++ b/engines/scumm/insane/rebel2/iact.cpp
@@ -177,7 +177,7 @@ void InsaneRebel2::iactRebel2Scene1(byte *renderBitmap, int32 codecparam, int32
// par4 is word at offset +6
//
// Based on disassembly of FUN_4028C5 and FUN_4033CF:
- //
+ //
// For IACT opcode 4 (enemy position update), the structure is:
// Offset +0x06: Type/SubType (par3)
// Offset +0x08: Enemy ID
diff --git a/engines/scumm/insane/rebel2/rebel.cpp b/engines/scumm/insane/rebel2/rebel.cpp
index 17e712ec3a1..c9fb01a370c 100644
--- a/engines/scumm/insane/rebel2/rebel.cpp
+++ b/engines/scumm/insane/rebel2/rebel.cpp
@@ -239,7 +239,7 @@ InsaneRebel2::InsaneRebel2(ScummEngine_v7 *scumm) {
_iactSceneId2 = 0;
int i, j;
-
+
for (i = 0; i < 12; i++)
_metEnemiesList[i] = 0;
diff --git a/engines/scumm/insane/rebel2/rebel.h b/engines/scumm/insane/rebel2/rebel.h
index 90b8c162d29..ea3f31dbe69 100644
--- a/engines/scumm/insane/rebel2/rebel.h
+++ b/engines/scumm/insane/rebel2/rebel.h
@@ -477,7 +477,7 @@ public:
bool _pauseOverlayActive;
bool _introCursorPushed; // true when we've pushed an invisible cursor for intro
-
+
int32 processMouse() override;
Common::Point getGameplayAimPoint();
@@ -672,15 +672,15 @@ public:
void enemyUpdate(byte *renderBitmap, Common::SeekableReadStream &b, int16 par2, int16 par3, int16 par4);
Common::List<enemy> _enemies;
-
+
// Current handler type for Rebel Assault 2 (determines crosshair sprite)
// Handler 0: Background only
// Handler 7: Third-Person Ship - uses crosshair sprite 0x2F (47)
- // Handler 8: Third-Person On Foot - uses crosshair sprite 0x2E (46)
+ // Handler 8: Third-Person On Foot - uses crosshair sprite 0x2E (46)
// Handler 0x19: FPS/Mixed view - uses crosshair sprite 0x2F (47)
// Handler 0x26: Turret/Cockpit - crosshair varies by level type
int _rebelHandler;
-
+
// Level type from IACT opcode 6 par3 (corresponds to DAT_004436de)
// Determines crosshair variant for turret mode:
// - levelType == 5: Use sprites 0x30+ (48+) for crosshair
@@ -702,7 +702,7 @@ public:
int renderY; // Y position to render
bool valid; // True if this slot has valid data
};
-
+
EmbeddedSanFrame _rebelEmbeddedHud[16]; // HUD overlay slots (userId 0-15)
// Load and decode an embedded SAN animation from IACT chunk data
@@ -744,7 +744,7 @@ public:
int scale; // Determines sprite set (small/med/large)
bool active;
};
-
+
Explosion _explosions[5];
void spawnExplosion(int x, int y, int objectHalfWidth);
diff --git a/engines/scumm/insane/rebel2/render.cpp b/engines/scumm/insane/rebel2/render.cpp
index 43fa7090935..6c256f77c64 100644
--- a/engines/scumm/insane/rebel2/render.cpp
+++ b/engines/scumm/insane/rebel2/render.cpp
@@ -310,9 +310,9 @@ void InsaneRebel2::loadEmbeddedSan(int userId, byte *animData, int32 size, byte
debug("Rebel2: Invalid embedded SAN: userId=%d, size=%d", userId, size);
return;
}
-
+
Common::MemoryReadStream stream(animData, size);
-
+
// Read ANIM header
uint32 animTag = stream.readUint32BE();
if (animTag != MKTAG('A','N','I','M')) {
@@ -321,7 +321,7 @@ void InsaneRebel2::loadEmbeddedSan(int userId, byte *animData, int32 size, byte
}
uint32 animSize = stream.readUint32BE();
debug("Rebel2: Parsing embedded ANIM: userId=%d, reported size=%u, actual=%d", userId, animSize, size - 8);
-
+
// Iterate through chunks to find FRME -> FOBJ
while (!stream.eos() && stream.pos() < size) {
uint32 tag = stream.readUint32BE();
@@ -389,7 +389,7 @@ void InsaneRebel2::loadEmbeddedSan(int userId, byte *animData, int32 size, byte
// Update render position from FOBJ header
frame.renderX = left;
frame.renderY = top;
-
+
// Read the raw FOBJ data
int32 dataSize = subSize - 14;
if (dataSize > 0) {
@@ -443,7 +443,7 @@ void InsaneRebel2::loadEmbeddedSan(int userId, byte *animData, int32 size, byte
free(fobjData);
}
}
-
+
// Done with FOBJ - assume only one relevant frame per embedded SAN
stream.seek(nextChunkPos);
goto end_parsing;
@@ -461,7 +461,7 @@ void InsaneRebel2::loadEmbeddedSan(int userId, byte *animData, int32 size, byte
stream.skip(1);
}
}
-
+
debug("Rebel2: No FOBJ found in embedded SAN userId=%d", userId);
end_parsing:;
@@ -683,7 +683,7 @@ void InsaneRebel2::drawTexturedLine(byte *dst, int pitch, int width, int height,
const byte *srcData = nut->getCharData(spriteIdx);
int texW = nut->getCharWidth(spriteIdx);
int texH = nut->getCharHeight(spriteIdx);
-
+
if (!srcData || texW <= 0 || texH <= 0)
return;
if (v < 0)
@@ -694,12 +694,12 @@ void InsaneRebel2::drawTexturedLine(byte *dst, int pitch, int width, int height,
int dx = abs(x1 - x0), sx = x0 < x1 ? 1 : -1;
int dy = -abs(y1 - y0), sy = y0 < y1 ? 1 : -1;
int err = dx + dy, e2;
-
+
// Total length approximation for UV mapping
int totalDist = (abs(dx) > abs(dy)) ? abs(dx) : abs(dy);
if (totalDist == 0)
totalDist = 1;
-
+
int currentDist = 0;
for (;;) {
@@ -708,21 +708,21 @@ void InsaneRebel2::drawTexturedLine(byte *dst, int pitch, int width, int height,
int u = (currentDist * texW) / totalDist;
if (u >= texW)
u = texW - 1;
-
+
byte color = srcData[v * texW + u];
-
+
// Check for transparency (0 and optionally 231)
- if (color != 0 && (!mask231 || color != 231)) {
+ if (color != 0 && (!mask231 || color != 231)) {
dst[y0 * pitch + x0] = color;
}
}
-
+
if (x0 == x1 && y0 == y1)
break;
e2 = 2 * err;
if (e2 >= dy) { err += dy; x0 += sx; }
if (e2 <= dx) { err += dx; y0 += sy; }
-
+
currentDist++;
}
}
@@ -2227,17 +2227,17 @@ void InsaneRebel2::procPostRendering(byte *renderBitmap, int32 codecparam, int32
// Map mouse Y (0-200) to Scroll Y (0-60)
int maxScrollX = width - _vm->_screenWidth;
int maxScrollY = height - _vm->_screenHeight;
-
+
if (maxScrollX < 0)
maxScrollX = 0;
if (maxScrollY < 0)
maxScrollY = 0;
-
+
// Simple linear mapping: Center of screen corresponds to center of buffer
Common::Point aimPos = getGameplayAimPoint();
_viewX = (aimPos.x * maxScrollX) / _vm->_screenWidth;
_viewY = (aimPos.y * maxScrollY) / _vm->_screenHeight;
-
+
_player->setScrollOffset(_viewX, _viewY);
// Death check: original game (FUN_417E53 line 25) exits video playback
@@ -2263,10 +2263,10 @@ void InsaneRebel2::procPostRendering(byte *renderBitmap, int32 codecparam, int32
// - DISPFONT.NUT (DAT_00482200) sprites 1-7 contain the status bar elements
//
// We draw directly to screen at Y=180
-
+
// Use video content coordinates, NOT buffer coordinates
const int videoWidth = 320; // Native video width
- const int videoHeight = 200; // Native video height
+ const int videoHeight = 200; // Native video height
const int statusBarY = 180; // 0xb4 - status bar starts at Y=180 in video coords
// Hide HUD/status bar during intro videos (marked by SmushPlayer video flag 0x20)
diff --git a/engines/scumm/metaengine.cpp b/engines/scumm/metaengine.cpp
index 5cb8d9c1b88..bb952987007 100644
--- a/engines/scumm/metaengine.cpp
+++ b/engines/scumm/metaengine.cpp
@@ -1120,35 +1120,35 @@ Common::KeymapArray ScummMetaEngine::initKeymaps(const char *target) const {
act->addDefaultInputMapping("JOY_LEFT");
rebel1Keymap->addAction(act);
- act = new Action("RA1RIGHT", _("Aim right / menu right"));
- act->setCustomEngineActionEvent(kScummActionInsaneRight);
- act->addDefaultInputMapping("JOY_RIGHT");
- rebel1Keymap->addAction(act);
-
- act = new Action("RA1STICKUP", _("Stick up"));
- act->setCustomBackendActionAxisEvent(kScummBackendActionRebel1AxisUp);
- act->addDefaultInputMapping("JOY_LEFT_STICK_Y-");
- rebel1Keymap->addAction(act);
-
- act = new Action("RA1STICKDOWN", _("Stick down"));
- act->setCustomBackendActionAxisEvent(kScummBackendActionRebel1AxisDown);
- act->addDefaultInputMapping("JOY_LEFT_STICK_Y+");
- rebel1Keymap->addAction(act);
-
- act = new Action("RA1STICKLEFT", _("Stick left"));
- act->setCustomBackendActionAxisEvent(kScummBackendActionRebel1AxisLeft);
- act->addDefaultInputMapping("JOY_LEFT_STICK_X-");
- rebel1Keymap->addAction(act);
-
- act = new Action("RA1STICKRIGHT", _("Stick right"));
- act->setCustomBackendActionAxisEvent(kScummBackendActionRebel1AxisRight);
- act->addDefaultInputMapping("JOY_LEFT_STICK_X+");
- rebel1Keymap->addAction(act);
-
- act = new Action("RA1FIRE", _("Fire / select"));
- act->setCustomEngineActionEvent(kScummActionInsaneAttack);
- act->addDefaultInputMapping("JOY_A");
- rebel1Keymap->addAction(act);
+ act = new Action("RA1RIGHT", _("Aim right / menu right"));
+ act->setCustomEngineActionEvent(kScummActionInsaneRight);
+ act->addDefaultInputMapping("JOY_RIGHT");
+ rebel1Keymap->addAction(act);
+
+ act = new Action("RA1STICKUP", _("Stick up"));
+ act->setCustomBackendActionAxisEvent(kScummBackendActionRebel1AxisUp);
+ act->addDefaultInputMapping("JOY_LEFT_STICK_Y-");
+ rebel1Keymap->addAction(act);
+
+ act = new Action("RA1STICKDOWN", _("Stick down"));
+ act->setCustomBackendActionAxisEvent(kScummBackendActionRebel1AxisDown);
+ act->addDefaultInputMapping("JOY_LEFT_STICK_Y+");
+ rebel1Keymap->addAction(act);
+
+ act = new Action("RA1STICKLEFT", _("Stick left"));
+ act->setCustomBackendActionAxisEvent(kScummBackendActionRebel1AxisLeft);
+ act->addDefaultInputMapping("JOY_LEFT_STICK_X-");
+ rebel1Keymap->addAction(act);
+
+ act = new Action("RA1STICKRIGHT", _("Stick right"));
+ act->setCustomBackendActionAxisEvent(kScummBackendActionRebel1AxisRight);
+ act->addDefaultInputMapping("JOY_LEFT_STICK_X+");
+ rebel1Keymap->addAction(act);
+
+ act = new Action("RA1FIRE", _("Fire / select"));
+ act->setCustomEngineActionEvent(kScummActionInsaneAttack);
+ act->addDefaultInputMapping("JOY_A");
+ rebel1Keymap->addAction(act);
act = new Action("RA1BACK", _("Back / skip"));
act->setKeyEvent(KeyState(KEYCODE_ESCAPE, ASCII_ESCAPE));
Commit: 10d5865288773b160b821ed28790dad04138babf
https://github.com/scummvm/scummvm/commit/10d5865288773b160b821ed28790dad04138babf
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:51+02:00
Commit Message:
SCUMM: RA: Improve descriptions and error messages
Changed paths:
engines/scumm/metaengine.cpp
engines/scumm/nut_renderer.cpp
diff --git a/engines/scumm/metaengine.cpp b/engines/scumm/metaengine.cpp
index bb952987007..4cc3fa5cb8b 100644
--- a/engines/scumm/metaengine.cpp
+++ b/engines/scumm/metaengine.cpp
@@ -880,7 +880,7 @@ static const ExtraGuiOption enableTTS = {
static const ExtraGuiOption enableRebel2HiRes = {
_s("High resolution mode"),
- _s("Run the game in 640x400 high resolution mode instead of 320x200."),
+ _s("Run the game in 640x400 high resolution mode instead of 320x200"),
"rebel2_hires",
true,
0,
@@ -889,7 +889,7 @@ static const ExtraGuiOption enableRebel2HiRes = {
static const ExtraGuiOption enableRebel2UnlockAll = {
_s("Unlock all levels"),
- _s("All levels will be available without requiring passwords."),
+ _s("All levels will be available without requiring passwords"),
"rebel2_unlock_all",
false,
0,
diff --git a/engines/scumm/nut_renderer.cpp b/engines/scumm/nut_renderer.cpp
index b84990ba1f5..ed45e6ad886 100644
--- a/engines/scumm/nut_renderer.cpp
+++ b/engines/scumm/nut_renderer.cpp
@@ -368,7 +368,7 @@ int NutRenderer::getCharHeight(byte c) const {
const byte *NutRenderer::getCharData(byte c) {
if (c >= _numChars)
- error("invalid character in NutRenderer::getCharData : %d (%d)", c, _numChars);
+ error("invalid character in NutRenderer::getCharData: %d (%d)", c, _numChars);
return _chars[c].src;
}
Commit: 96ad3cc3b9cee58c929ee7622e07d50e25460f05
https://github.com/scummvm/scummvm/commit/96ad3cc3b9cee58c929ee7622e07d50e25460f05
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:52+02:00
Commit Message:
SCUMM: RA: Update hashes in scumm-md5.txt
Changed paths:
devtools/scumm-md5.txt
engines/scumm/detection_tables.h
engines/scumm/scumm-md5.h
diff --git a/devtools/scumm-md5.txt b/devtools/scumm-md5.txt
index 3a9ed9599a2..11029af684a 100644
--- a/devtools/scumm-md5.txt
+++ b/devtools/scumm-md5.txt
@@ -447,6 +447,14 @@ ft Full Throttle
9b7452b5cd6d3ffb2b2f5118010af84f 116463537 en Mac Demo Demo Mac bundle Fingolfin, Joachim Eberhard
8ead2c01c48c2e3d15d6a89b86cd84ab 19697 he DOS - - - Hebrew fan translation
+rebel1 Star Wars: Rebel Assault
+ b9ac90e4411da1b6f7dd075cc4b03c2a 214435 en DOS - - - -
+
+rebel2 Star Wars: Rebel Assault II: The Hidden Empire
+ 6b73a08c535c0544785d73ff812908a0 9430 en DOS - - - -
+
+ e977ed046b88e75b645491caeff37b0c 313689 en DOS Demo Demo - -
+
dig The Dig
d8323015ecb8b10bf53474f6e6b0ae33 16304 All All - - - Fingolfin
aad201302286c1cfee92321cd406e427 811008 All Windows Steam Steam Steam Version Ben Castricum, Filippos Karapetis
diff --git a/engines/scumm/detection_tables.h b/engines/scumm/detection_tables.h
index d06d0de36c4..b1bcb6e8b46 100644
--- a/engines/scumm/detection_tables.h
+++ b/engines/scumm/detection_tables.h
@@ -49,7 +49,6 @@ static const char *const directoryGlobs[] = {
"Contents", // Mac Steam versions
"MacOS", // Mac Steam versions
"Resources", // Mac SE/Remastered versions
- "OPEN", // RA2 demo detection (O_DEMO.SAN)
0
};
@@ -504,8 +503,8 @@ static const GameFilenamePattern gameFilenamesTable[] = {
{ "rebel1", "ASSAULT.EXE", kGenUnchanged, UNK_LANG, Common::kPlatformDOS, "" },
- { "rebel2", "RA2START.EXE", kGenUnchanged, UNK_LANG, Common::kPlatformDOS, "" },
- { "rebel2", "O_DEMO.SAN", kGenUnchanged, UNK_LANG, Common::kPlatformDOS, "Demo" },
+ { "rebel2", "REBEL2.EXE", kGenUnchanged, UNK_LANG, Common::kPlatformDOS, "" },
+ { "rebel2", "REBEL2.EXE", kGenUnchanged, UNK_LANG, Common::kPlatformDOS, "Demo" },
{ "comi", "comi.la%d", kGenDiskNum, UNK_LANG, UNK, 0 },
diff --git a/engines/scumm/scumm-md5.h b/engines/scumm/scumm-md5.h
index 72d64bcb65c..38d31631068 100644
--- a/engines/scumm/scumm-md5.h
+++ b/engines/scumm/scumm-md5.h
@@ -1,5 +1,5 @@
/*
- This file was generated by the md5table tool on Wed Mar 4 16:30:02 2026
+ This file was generated by the md5table tool on Thu Apr 30 19:26:15 2026
DO NOT EDIT MANUALLY!
*/
@@ -373,7 +373,7 @@ static const MD5Table md5table[] = {
{ "6b27dbcd8d5697d5c918eeca0f68ef6a", "puttrace", "HE CUP", "Preview", 3901484, Common::UNK_LANG, Common::kPlatformUnknown },
{ "6b3ec67da214f558dc5ceaa2acd47453", "indy3", "EGA", "EGA", 5361, Common::EN_ANY, Common::kPlatformDOS },
{ "6b5a3fef241e90d4b2e77f1e222773ee", "maniac", "NES", "", 2082, Common::SV_SWE, Common::kPlatformNES },
- { "TODO", "rebel2", "VGA", "VGA", 9430, Common::EN_ANY, Common::kPlatformDOS },
+ { "6b73a08c535c0544785d73ff812908a0", "rebel2", "", "", 9430, Common::EN_ANY, Common::kPlatformDOS },
{ "6bca7a1a96d16e52b8f3c42b50dbdca3", "fbear", "HE 62", "", -1, Common::JA_JPN, Common::kPlatform3DO },
{ "6bf70eee5de3d24d2403e0dd3d267e8a", "spyfox", "", "", 49221, Common::EN_USA, Common::kPlatformWindows },
{ "6c2bff0e327f2962e809c2e1a82d7309", "monkey", "VGA", "", 8347, Common::EN_ANY, Common::kPlatformAmiga },
Commit: 5a64207352e9dc70bc7483be6a39790e773ef6f5
https://github.com/scummvm/scummvm/commit/5a64207352e9dc70bc7483be6a39790e773ef6f5
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:52+02:00
Commit Message:
SCUMM: RA2: Make codec 45 line-size and probe fields signed
Changed paths:
engines/scumm/insane/rebel2/render.cpp
engines/scumm/smush/rebel/codec_ra2.cpp
diff --git a/engines/scumm/insane/rebel2/render.cpp b/engines/scumm/insane/rebel2/render.cpp
index 6c256f77c64..32a4463f66e 100644
--- a/engines/scumm/insane/rebel2/render.cpp
+++ b/engines/scumm/insane/rebel2/render.cpp
@@ -148,7 +148,7 @@ void InsaneRebel2::decodeCodec45(byte *dst, const byte *src, int width, int heig
// If no known header found, probe offsets 0, 2, 4, 6 to find valid RLE start
if (!foundValidOffset) {
for (int testOffset = 0; testOffset <= 6 && testOffset + 2 <= dataSize; testOffset += 2) {
- int testLineSize = READ_LE_UINT16(src + testOffset);
+ int testLineSize = READ_LE_INT16(src + testOffset);
// A valid first line size should be: > 0, <= width*2
if (testLineSize > 0 && testLineSize <= width * 2 && testLineSize < dataSize - testOffset) {
// Validate line-size sequence
@@ -157,7 +157,7 @@ void InsaneRebel2::decodeCodec45(byte *dst, const byte *src, int width, int heig
bool validSum = true;
while (linesTest < height && testPtr + 2 <= src + dataSize) {
- int ls = READ_LE_UINT16(testPtr);
+ int ls = READ_LE_INT16(testPtr);
if (ls <= 0 || ls > width * 2) {
validSum = false;
break;
@@ -185,13 +185,13 @@ void InsaneRebel2::decodeCodec45(byte *dst, const byte *src, int width, int heig
const byte *dataEnd = src + dataSize;
// Check if this is per-line RLE or continuous RLE
- int firstVal = READ_LE_UINT16(srcPtr);
+ int firstVal = READ_LE_INT16(srcPtr);
bool perLineMode = (firstVal > 0 && firstVal <= width * 2);
if (perLineMode) {
debug("Rebel2: Codec 45 using per-line RLE (firstLineSize=%d)", firstVal);
for (int row = 0; row < height && srcPtr < dataEnd; row++) {
- int lineSize = READ_LE_UINT16(srcPtr);
+ int lineSize = READ_LE_INT16(srcPtr);
srcPtr += 2;
if (lineSize <= 0 || lineSize > (int)(dataEnd - srcPtr))
break;
diff --git a/engines/scumm/smush/rebel/codec_ra2.cpp b/engines/scumm/smush/rebel/codec_ra2.cpp
index 7b9309f8d51..d9914ab9e4d 100644
--- a/engines/scumm/smush/rebel/codec_ra2.cpp
+++ b/engines/scumm/smush/rebel/codec_ra2.cpp
@@ -170,7 +170,7 @@ void smushDecodeRA2Bomp(byte *dst, const byte *src, int left, int top, int width
// Probe offsets to find valid RLE start
// Valid start should have reasonable line size values
for (int testOffset = 0; testOffset <= 6 && testOffset + 2 <= dataSize; testOffset += 2) {
- int testLineSize = READ_LE_UINT16(src + testOffset);
+ int testLineSize = READ_LE_INT16(src + testOffset);
// A valid line size should be positive and reasonable for the width
if (testLineSize > 0 && testLineSize <= width * 2 && testLineSize < dataSize - testOffset) {
// Further validation: try to count valid line sizes
@@ -179,7 +179,7 @@ void smushDecodeRA2Bomp(byte *dst, const byte *src, int left, int top, int width
bool validSum = true;
while (linesTest < height && testPtr + 2 <= src + dataSize) {
- int ls = READ_LE_UINT16(testPtr);
+ int ls = READ_LE_INT16(testPtr);
if (ls <= 0 || ls > width * 2) {
validSum = false;
break;
@@ -200,13 +200,13 @@ void smushDecodeRA2Bomp(byte *dst, const byte *src, int left, int top, int width
const byte *dataEnd = src + (dataSize - headerSkip);
// Check first value to determine per-line vs continuous mode
- int firstVal = (src + 2 <= dataEnd) ? READ_LE_UINT16(src) : 0;
+ int firstVal = (src + 2 <= dataEnd) ? READ_LE_INT16(src) : 0;
bool perLineMode = (firstVal > 0 && firstVal <= width * 2);
if (perLineMode) {
// Per-line RLE with 2-byte size headers
for (int row = 0; row < height && src < dataEnd; row++) {
- int lineSize = READ_LE_UINT16(src);
+ int lineSize = READ_LE_INT16(src);
src += 2;
if (lineSize <= 0 || lineSize > (int)(dataEnd - src))
break;
Commit: 804223f4226fcc02e967570391d483d408b44e29
https://github.com/scummvm/scummvm/commit/804223f4226fcc02e967570391d483d408b44e29
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:52+02:00
Commit Message:
SCUMM: RA2: Clean up codec 45 debug statements
Changed paths:
engines/scumm/insane/rebel2/render.cpp
diff --git a/engines/scumm/insane/rebel2/render.cpp b/engines/scumm/insane/rebel2/render.cpp
index 32a4463f66e..181a32786ea 100644
--- a/engines/scumm/insane/rebel2/render.cpp
+++ b/engines/scumm/insane/rebel2/render.cpp
@@ -129,11 +129,6 @@ void InsaneRebel2::decodeCodec45(byte *dst, const byte *src, int width, int heig
// Codec 45: RA2-specific BOMP RLE with variable header (FUN_0042B5F0)
// May have a 6-byte sub-header starting with "01 FE"
- debug("Rebel2: Codec 45 first 20 bytes: %02X %02X %02X %02X %02X %02X | %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X",
- src[0], src[1], src[2], src[3], src[4], src[5], src[6], src[7],
- src[8], src[9], src[10], src[11], src[12], src[13], src[14], src[15],
- src[16], src[17], src[18], src[19]);
-
// Probe for header offset
int headerSkip = 0;
bool foundValidOffset = false;
@@ -141,7 +136,6 @@ void InsaneRebel2::decodeCodec45(byte *dst, const byte *src, int width, int heig
// Check for known 6-byte header pattern: 01 FE XX XX XX XX
if (dataSize > 6 && src[0] == 0x01 && src[1] == 0xFE) {
headerSkip = 6;
- debug("Rebel2: Codec 45 found 01 FE header, skipping 6 bytes");
foundValidOffset = true;
}
@@ -170,7 +164,6 @@ void InsaneRebel2::decodeCodec45(byte *dst, const byte *src, int width, int heig
if (validSum && linesTest >= height - 1) {
headerSkip = testOffset;
foundValidOffset = true;
- debug("Rebel2: Codec 45 found valid RLE at offset %d (tested %d lines)", testOffset, linesTest);
break;
}
}
@@ -178,7 +171,7 @@ void InsaneRebel2::decodeCodec45(byte *dst, const byte *src, int width, int heig
}
if (!foundValidOffset) {
- debug("Rebel2: Codec 45 couldn't find valid RLE offset, using offset 0");
+ warning("Rebel2: Codec 45 couldn't find valid RLE offset, using offset 0");
}
const byte *srcPtr = src + headerSkip;
@@ -189,7 +182,6 @@ void InsaneRebel2::decodeCodec45(byte *dst, const byte *src, int width, int heig
bool perLineMode = (firstVal > 0 && firstVal <= width * 2);
if (perLineMode) {
- debug("Rebel2: Codec 45 using per-line RLE (firstLineSize=%d)", firstVal);
for (int row = 0; row < height && srcPtr < dataEnd; row++) {
int lineSize = READ_LE_INT16(srcPtr);
srcPtr += 2;
@@ -216,7 +208,6 @@ void InsaneRebel2::decodeCodec45(byte *dst, const byte *src, int width, int heig
}
} else {
// Continuous BOMP RLE (no per-line headers)
- debug("Rebel2: Codec 45 using continuous BOMP RLE");
for (int row = 0; row < height && srcPtr < dataEnd; row++) {
byte *rowDst = dst + row * width;
int x = 0;
@@ -240,15 +231,6 @@ void InsaneRebel2::decodeCodec45(byte *dst, const byte *src, int width, int heig
}
}
}
-
- // Count non-zero pixels for debug
- int nonZero = 0;
- for (int i = 0; i < width * height; i++) {
- if (dst[i] != 0)
- nonZero++;
- }
- debug("Rebel2: Decoded codec 45: %dx%d, %d non-zero (%d%%)",
- width, height, nonZero, (nonZero * 100) / (width * height));
}
// renderEmbeddedFrame -- Blit a decoded embedded frame to the video buffer.
Commit: 865d55c54b5d22720b22b651e503fad4df640da5
https://github.com/scummvm/scummvm/commit/865d55c54b5d22720b22b651e503fad4df640da5
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:53+02:00
Commit Message:
SCUMM: RA2: Remove goto and redundant seek
Changed paths:
engines/scumm/insane/rebel2/render.cpp
diff --git a/engines/scumm/insane/rebel2/render.cpp b/engines/scumm/insane/rebel2/render.cpp
index 181a32786ea..e96a7b91234 100644
--- a/engines/scumm/insane/rebel2/render.cpp
+++ b/engines/scumm/insane/rebel2/render.cpp
@@ -427,8 +427,7 @@ void InsaneRebel2::loadEmbeddedSan(int userId, byte *animData, int32 size, byte
}
// Done with FOBJ - assume only one relevant frame per embedded SAN
- stream.seek(nextChunkPos);
- goto end_parsing;
+ return;
} else {
// Skip other sub-chunks (AHDR inside FRME?) or padding
stream.seek(nextSubPos);
@@ -445,8 +444,6 @@ void InsaneRebel2::loadEmbeddedSan(int userId, byte *animData, int32 size, byte
}
debug("Rebel2: No FOBJ found in embedded SAN userId=%d", userId);
-
-end_parsing:;
}
// Spawn explosion into the shared 5-slot system.
Commit: 2e39caffe5665da7efec9cef50d965d14ead1ece
https://github.com/scummvm/scummvm/commit/2e39caffe5665da7efec9cef50d965d14ead1ece
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:53+02:00
Commit Message:
SCUMM: Remove extra blank lines from NutRenderer
Changed paths:
engines/scumm/nut_renderer.cpp
diff --git a/engines/scumm/nut_renderer.cpp b/engines/scumm/nut_renderer.cpp
index ed45e6ad886..5fa5819a613 100644
--- a/engines/scumm/nut_renderer.cpp
+++ b/engines/scumm/nut_renderer.cpp
@@ -414,8 +414,6 @@ int NutRenderer::drawCharV7(byte *buffer, Common::Rect &clipRect, int x, int y,
if (chr >= _numChars)
return 0;
-
-
if (_direction < 0)
x -= _chars[chr].width;
Commit: e4ee27d61fc2b9401c774f74d2e88b932e2f51d9
https://github.com/scummvm/scummvm/commit/e4ee27d61fc2b9401c774f74d2e88b932e2f51d9
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:53+02:00
Commit Message:
SCUMM: RA2: Remove codec duplication
Changed paths:
engines/scumm/insane/rebel2/iact.cpp
engines/scumm/insane/rebel2/rebel.h
engines/scumm/insane/rebel2/render.cpp
engines/scumm/smush/rebel/codec_ra2.cpp
engines/scumm/smush/rebel/smush_player_ra1.cpp
engines/scumm/smush/rebel/smush_player_ra2.cpp
diff --git a/engines/scumm/insane/rebel2/iact.cpp b/engines/scumm/insane/rebel2/iact.cpp
index f05bb063e1e..1f676bf0733 100644
--- a/engines/scumm/insane/rebel2/iact.cpp
+++ b/engines/scumm/insane/rebel2/iact.cpp
@@ -27,14 +27,12 @@
#include "scumm/smush/smush_player.h"
#include "scumm/smush/smush_font.h"
+#include "scumm/smush/rebel/codec_ra2.h"
#include "scumm/insane/rebel2/rebel.h"
namespace Scumm {
-// External codec functions from codec1.cpp
-extern void smushDecodeRLEOpaque(byte *dst, const byte *src, int left, int top, int width, int height, int pitch);
-
//
// procPreRendering -- Pre-frame setup: background restore and corridor overlays.
//
diff --git a/engines/scumm/insane/rebel2/rebel.h b/engines/scumm/insane/rebel2/rebel.h
index ea3f31dbe69..650613168e1 100644
--- a/engines/scumm/insane/rebel2/rebel.h
+++ b/engines/scumm/insane/rebel2/rebel.h
@@ -709,30 +709,10 @@ public:
// userId: HUD slot (1-4), animData: raw ANIM data, size: data size, renderBitmap: current frame buffer
void loadEmbeddedSan(int userId, byte *animData, int32 size, byte *renderBitmap) override;
- // ---------------------------------------------------------------------------
- // Embedded Frame Codec Decoders
- // ---------------------------------------------------------------------------
- // Decode different codec formats used in embedded ANIM/FOBJ data.
- // Based on retail FUN_0042C590 (codec 1), FUN_0042BD60 (codec 21), etc.
-
- // Decode codec 21/44 (Line Update) - skip/copy pairs per line
- // Used for fonts and some HUD frames (FUN_0042BD60)
- void decodeCodec21(byte *dst, const byte *src, int width, int height);
-
- // Decode codec 23 (Skip/Copy with embedded RLE) - hybrid format
- // Used for embedded HUD frames with transparency (FUN_0042BBF0)
- void decodeCodec23(byte *dst, const byte *src, int width, int height, int dataSize);
-
- // Decode codec 45 (RA2-specific BOMP RLE) - variable header format
- // Used for small animation elements and HUD pieces (FUN_0042B5F0)
- void decodeCodec45(byte *dst, const byte *src, int width, int height, int dataSize);
-
// Render a decoded embedded frame to the video buffer
// Handles transparency (color 0 and 231) and boundary checks
void renderEmbeddedFrame(byte *renderBitmap, const EmbeddedSanFrame &frame, int userId);
-
-
int16 _rebelLinks[512][3]; // Dependency links: Slot 0 (Disable on death), Slot 1/2 (Enable on death)
void clearBit(int n);
bool isShootingAllowed(); // FUN_0040d836/FUN_00401CCF: Check control mode before spawning shots
diff --git a/engines/scumm/insane/rebel2/render.cpp b/engines/scumm/insane/rebel2/render.cpp
index e96a7b91234..d01ecd6dbd2 100644
--- a/engines/scumm/insane/rebel2/render.cpp
+++ b/engines/scumm/insane/rebel2/render.cpp
@@ -30,209 +30,16 @@
#include "scumm/smush/smush_player.h"
#include "scumm/smush/smush_font.h"
+#include "scumm/smush/rebel/codec_ra2.h"
#include "scumm/insane/rebel2/rebel.h"
namespace Scumm {
-// External codec functions from codec1.cpp / codec_ra2.cpp
+// External codec functions from codec1.cpp
extern void smushDecodeRLE(byte *dst, const byte *src, int left, int top, int width, int height, int pitch);
-extern void smushDecodeRLEOpaque(byte *dst, const byte *src, int left, int top, int width, int height, int pitch);
extern void smushDecodeUncompressed(byte *dst, const byte *src, int left, int top, int width, int height, int pitch);
-
-// ---------------------------------------------------------------------------
-// Embedded Frame Codec Decoders
-// ---------------------------------------------------------------------------
-// Retail codec functions FUN_0042BD60, FUN_0042BBF0, FUN_0042B5F0.
-
-// decodeCodec21 -- Codec 21/44: line update codec (FUN_0042BD60).
-void InsaneRebel2::decodeCodec21(byte *dst, const byte *src, int width, int height) {
- // Codec 21/44: Line Update codec (FUN_0042BD60)
- // Format: each line has 2-byte size header, then pairs of (skip, count+1, literal_bytes)
- for (int row = 0; row < height; row++) {
- int lineDataSize = READ_LE_UINT16(src);
- src += 2;
- const byte *lineEnd = src + lineDataSize;
- byte *lineDst = dst + row * width;
- int x = 0;
-
- while (src < lineEnd && x < width) {
- int skip = READ_LE_UINT16(src);
- src += 2;
- x += skip;
- if (src >= lineEnd)
- break;
-
- int count = READ_LE_UINT16(src) + 1;
- src += 2;
- while (count-- > 0 && x < width && src < lineEnd) {
- lineDst[x++] = *src++;
- }
- }
- src = lineEnd;
- }
-}
-
-// decodeCodec23 -- Codec 23: skip/copy with embedded RLE (FUN_0042BBF0).
-void InsaneRebel2::decodeCodec23(byte *dst, const byte *src, int width, int height, int dataSize) {
- // Codec 23: Skip/Copy with embedded RLE (FUN_0042BBF0)
- // Format: each line has 2-byte size, then pairs of (skip, runSize, RLE_data)
- const byte *dataEnd = src + dataSize;
-
- for (int row = 0; row < height && src < dataEnd; row++) {
- int lineDataSize = READ_LE_UINT16(src);
- src += 2;
- const byte *lineEnd = src + lineDataSize;
- byte *lineDst = dst + row * width;
- int x = 0;
-
- while (src < lineEnd && x < width) {
- int skip = READ_LE_UINT16(src);
- src += 2;
- x += skip;
- if (src >= lineEnd || x >= width)
- break;
-
- int runSize = READ_LE_UINT16(src);
- src += 2;
-
- // Decode RLE within this run
- const byte *runEnd = src + runSize;
- while (src < runEnd && x < width) {
- byte code = *src++;
- int num = (code >> 1) + 1;
- if (num > width - x)
- num = width - x;
-
- if (code & 1) {
- // RLE run
- byte color = (src < runEnd) ? *src++ : 0;
- for (int i = 0; i < num && x < width; i++) {
- lineDst[x++] = color;
- }
- } else {
- // Literal run
- for (int i = 0; i < num && x < width && src < runEnd; i++) {
- lineDst[x++] = *src++;
- }
- }
- }
- src = runEnd;
- }
- src = lineEnd;
- }
-}
-
-// decodeCodec45 -- Codec 45: RA2-specific BOMP RLE with variable header (FUN_0042B5F0).
-void InsaneRebel2::decodeCodec45(byte *dst, const byte *src, int width, int height, int dataSize) {
- // Codec 45: RA2-specific BOMP RLE with variable header (FUN_0042B5F0)
- // May have a 6-byte sub-header starting with "01 FE"
-
- // Probe for header offset
- int headerSkip = 0;
- bool foundValidOffset = false;
-
- // Check for known 6-byte header pattern: 01 FE XX XX XX XX
- if (dataSize > 6 && src[0] == 0x01 && src[1] == 0xFE) {
- headerSkip = 6;
- foundValidOffset = true;
- }
-
- // If no known header found, probe offsets 0, 2, 4, 6 to find valid RLE start
- if (!foundValidOffset) {
- for (int testOffset = 0; testOffset <= 6 && testOffset + 2 <= dataSize; testOffset += 2) {
- int testLineSize = READ_LE_INT16(src + testOffset);
- // A valid first line size should be: > 0, <= width*2
- if (testLineSize > 0 && testLineSize <= width * 2 && testLineSize < dataSize - testOffset) {
- // Validate line-size sequence
- int linesTest = 0;
- const byte *testPtr = src + testOffset;
- bool validSum = true;
-
- while (linesTest < height && testPtr + 2 <= src + dataSize) {
- int ls = READ_LE_INT16(testPtr);
- if (ls <= 0 || ls > width * 2) {
- validSum = false;
- break;
- }
- testPtr += ls + 2;
- linesTest++;
- }
-
- // Accept if we got close to expected number of lines
- if (validSum && linesTest >= height - 1) {
- headerSkip = testOffset;
- foundValidOffset = true;
- break;
- }
- }
- }
- }
-
- if (!foundValidOffset) {
- warning("Rebel2: Codec 45 couldn't find valid RLE offset, using offset 0");
- }
-
- const byte *srcPtr = src + headerSkip;
- const byte *dataEnd = src + dataSize;
-
- // Check if this is per-line RLE or continuous RLE
- int firstVal = READ_LE_INT16(srcPtr);
- bool perLineMode = (firstVal > 0 && firstVal <= width * 2);
-
- if (perLineMode) {
- for (int row = 0; row < height && srcPtr < dataEnd; row++) {
- int lineSize = READ_LE_INT16(srcPtr);
- srcPtr += 2;
- if (lineSize <= 0 || lineSize > (int)(dataEnd - srcPtr))
- break;
-
- const byte *lineEnd = srcPtr + lineSize;
- byte *rowDst = dst + row * width;
- int x = 0;
-
- while (srcPtr < lineEnd && x < width) {
- byte ctrl = *srcPtr++;
- int count = (ctrl >> 1) + 1;
- if (ctrl & 1) {
- byte color = (srcPtr < lineEnd) ? *srcPtr++ : 0;
- for (int i = 0; i < count && x < width; i++)
- rowDst[x++] = color;
- } else {
- for (int i = 0; i < count && x < width && srcPtr < lineEnd; i++)
- rowDst[x++] = *srcPtr++;
- }
- }
- srcPtr = lineEnd;
- }
- } else {
- // Continuous BOMP RLE (no per-line headers)
- for (int row = 0; row < height && srcPtr < dataEnd; row++) {
- byte *rowDst = dst + row * width;
- int x = 0;
-
- while (x < width && srcPtr < dataEnd) {
- byte ctrl = *srcPtr++;
- int count = (ctrl >> 1) + 1;
-
- if (ctrl & 1) {
- // RLE fill
- byte color = (srcPtr < dataEnd) ? *srcPtr++ : 0;
- for (int i = 0; i < count && x < width; i++) {
- rowDst[x++] = color;
- }
- } else {
- // Literal copy
- for (int i = 0; i < count && x < width && srcPtr < dataEnd; i++) {
- rowDst[x++] = *srcPtr++;
- }
- }
- }
- }
- }
-}
-
// renderEmbeddedFrame -- Blit a decoded embedded frame to the video buffer.
void InsaneRebel2::renderEmbeddedFrame(byte *renderBitmap, const EmbeddedSanFrame &frame, int userId) {
// Render the decoded embedded frame to the video buffer
@@ -391,16 +198,16 @@ void InsaneRebel2::loadEmbeddedSan(int userId, byte *animData, int32 size, byte
debug("Rebel2: Decoded embedded HUD (codec 20/raw): %dx%d", width, height);
} else if (codec == 21 || codec == 44) {
// Codec 21/44: Line update (FUN_0042BD60)
- decodeCodec21(frame.pixels, fobjData, width, height);
+ smushDecodeLineUpdate(frame.pixels, fobjData, 0, 0, width, height, width);
frame.valid = true;
debug("Rebel2: Decoded embedded HUD (codec %d/line update): %dx%d", codec, width, height);
} else if (codec == 45) {
// Codec 45: RA2-specific BOMP RLE (FUN_0042B5F0)
- decodeCodec45(frame.pixels, fobjData, width, height, dataSize);
+ smushDecodeRA2Bomp(frame.pixels, fobjData, 0, 0, width, height, width, dataSize);
frame.valid = true;
} else if (codec == 23) {
// Codec 23: Skip/copy with embedded RLE (FUN_0042BBF0)
- decodeCodec23(frame.pixels, fobjData, width, height, dataSize);
+ smushDecodeSkipRLE(frame.pixels, fobjData, 0, 0, width, height, width, dataSize);
frame.valid = true;
debug("Rebel2: Decoded embedded HUD (codec 23/skip-RLE): %dx%d", width, height);
} else {
diff --git a/engines/scumm/smush/rebel/codec_ra2.cpp b/engines/scumm/smush/rebel/codec_ra2.cpp
index d9914ab9e4d..4251a4342e1 100644
--- a/engines/scumm/smush/rebel/codec_ra2.cpp
+++ b/engines/scumm/smush/rebel/codec_ra2.cpp
@@ -21,7 +21,10 @@
// Rebel Assault 2 SMUSH video codecs
+#include "scumm/smush/rebel/codec_ra2.h"
+
#include "common/endian.h"
+#include "common/textconsole.h"
#include "scumm/bomp.h"
@@ -162,10 +165,12 @@ void smushDecodeRA2Bomp(byte *dst, const byte *src, int left, int top, int width
// Detect header pattern and find RLE data start
int headerSkip = 0;
+ bool foundValidOffset = false;
// Check for common 6-byte header pattern: 01 FE XX XX XX XX
if (dataSize > 6 && src[0] == 0x01 && src[1] == 0xFE) {
headerSkip = 6;
+ foundValidOffset = true;
} else {
// Probe offsets to find valid RLE start
// Valid start should have reasonable line size values
@@ -190,12 +195,17 @@ void smushDecodeRA2Bomp(byte *dst, const byte *src, int left, int top, int width
if (validSum && linesTest >= height - 1) {
headerSkip = testOffset;
+ foundValidOffset = true;
break;
}
}
}
}
+ if (!foundValidOffset) {
+ warning("Rebel2: Codec 45 couldn't find valid RLE offset, using offset 0");
+ }
+
src += headerSkip;
const byte *dataEnd = src + (dataSize - headerSkip);
diff --git a/engines/scumm/smush/rebel/smush_player_ra1.cpp b/engines/scumm/smush/rebel/smush_player_ra1.cpp
index ad3ea174974..9c7dcfd685c 100644
--- a/engines/scumm/smush/rebel/smush_player_ra1.cpp
+++ b/engines/scumm/smush/rebel/smush_player_ra1.cpp
@@ -30,6 +30,7 @@
#include "scumm/file.h"
#include "scumm/scumm_v7.h"
#include "scumm/smush/smush_font.h"
+#include "scumm/smush/rebel/codec_ra2.h"
#include "scumm/smush/rebel/smush_player_ra1.h"
#include "scumm/insane/rebel1/rebel.h"
@@ -292,8 +293,6 @@ void smushDecodeRA1Transparent(byte *dst, const byte *src, int left, int top, in
dst += pitch;
} while (--height);
}
-void smushDecodeRLEOpaque(byte *dst, const byte *src, int left, int top, int width, int height, int pitch);
-
/**
* RA1 codec 21: Skip/copy line codec (FUN_10D41).
*/
diff --git a/engines/scumm/smush/rebel/smush_player_ra2.cpp b/engines/scumm/smush/rebel/smush_player_ra2.cpp
index bdf5502ed92..7dd4a90e0c1 100644
--- a/engines/scumm/smush/rebel/smush_player_ra2.cpp
+++ b/engines/scumm/smush/rebel/smush_player_ra2.cpp
@@ -32,6 +32,7 @@
#include "scumm/scumm_v7.h"
#include "scumm/smush/smush_font.h"
#include "scumm/smush/rebel/smush_multi_font.h"
+#include "scumm/smush/rebel/codec_ra2.h"
#include "scumm/smush/rebel/smush_player_ra2.h"
#include "scumm/insane/insane.h"
@@ -39,11 +40,6 @@
namespace Scumm {
-// Forward declarations for RA2 codec functions (defined in codec_ra2.cpp)
-void smushDecodeLineUpdate(byte *dst, const byte *src, int left, int top, int width, int height, int pitch);
-void smushDecodeSkipRLE(byte *dst, const byte *src, int left, int top, int width, int height, int pitch, int dataSize);
-void smushDecodeRA2Bomp(byte *dst, const byte *src, int left, int top, int width, int height, int pitch, int dataSize);
-
// ---------------------------------------------------------------------------
// SmushPlayerRebel2 â construction / destruction
// ---------------------------------------------------------------------------
Commit: 8b11f84c8d15a829ac3b686197cb1ed79f2af030
https://github.com/scummvm/scummvm/commit/8b11f84c8d15a829ac3b686197cb1ed79f2af030
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:54+02:00
Commit Message:
SCUMM: RA2: Add missing header
Changed paths:
A engines/scumm/smush/rebel/codec_ra2.h
diff --git a/engines/scumm/smush/rebel/codec_ra2.h b/engines/scumm/smush/rebel/codec_ra2.h
new file mode 100644
index 00000000000..f9b0191623b
--- /dev/null
+++ b/engines/scumm/smush/rebel/codec_ra2.h
@@ -0,0 +1,36 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#ifndef SCUMM_SMUSH_REBEL_CODEC_RA2_H
+#define SCUMM_SMUSH_REBEL_CODEC_RA2_H
+
+#include "common/scummsys.h"
+
+namespace Scumm {
+
+void smushDecodeRLEOpaque(byte *dst, const byte *src, int left, int top, int width, int height, int pitch);
+void smushDecodeLineUpdate(byte *dst, const byte *src, int left, int top, int width, int height, int pitch);
+void smushDecodeSkipRLE(byte *dst, const byte *src, int left, int top, int width, int height, int pitch, int dataSize);
+void smushDecodeRA2Bomp(byte *dst, const byte *src, int left, int top, int width, int height, int pitch, int dataSize);
+
+} // End of namespace Scumm
+
+#endif
Commit: 695edd8a7938ccd4f256fe8a5aae979765465429
https://github.com/scummvm/scummvm/commit/695edd8a7938ccd4f256fe8a5aae979765465429
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:54+02:00
Commit Message:
SCUMM: RA2: Refactor embedded frame region blitting
Changed paths:
engines/scumm/insane/rebel2/render.cpp
diff --git a/engines/scumm/insane/rebel2/render.cpp b/engines/scumm/insane/rebel2/render.cpp
index d01ecd6dbd2..0023a910441 100644
--- a/engines/scumm/insane/rebel2/render.cpp
+++ b/engines/scumm/insane/rebel2/render.cpp
@@ -40,6 +40,55 @@ namespace Scumm {
extern void smushDecodeRLE(byte *dst, const byte *src, int left, int top, int width, int height, int pitch);
extern void smushDecodeUncompressed(byte *dst, const byte *src, int left, int top, int width, int height, int pitch);
+static bool isValidEmbeddedFrame(const InsaneRebel2::EmbeddedSanFrame &frame) {
+ return frame.valid && frame.pixels && frame.width > 0 && frame.height > 0;
+}
+
+static int countEmbeddedFramePixels(const InsaneRebel2::EmbeddedSanFrame &frame) {
+ if (!isValidEmbeddedFrame(frame))
+ return 0;
+
+ int count = 0;
+ for (int i = 0; i < frame.width * frame.height; i++) {
+ if (frame.pixels[i] != 0)
+ count++;
+ }
+
+ return count;
+}
+
+static void blitEmbeddedFrameRegion(byte *renderBitmap, int pitch, int clipWidth, int clipHeight,
+ const InsaneRebel2::EmbeddedSanFrame &frame, int destX, int destY,
+ int srcX, int srcY, int drawWidth, int drawHeight) {
+ if (!renderBitmap || !isValidEmbeddedFrame(frame) || drawWidth <= 0 || drawHeight <= 0)
+ return;
+ if (srcX < 0 || srcY < 0)
+ return;
+
+ drawWidth = MIN(drawWidth, frame.width - srcX);
+ drawHeight = MIN(drawHeight, frame.height - srcY);
+ if (drawWidth <= 0 || drawHeight <= 0)
+ return;
+
+ for (int y = 0; y < drawHeight; y++) {
+ int dy = destY + y;
+ if (dy < 0 || dy >= clipHeight)
+ continue;
+
+ const byte *srcRow = frame.pixels + (srcY + y) * frame.width + srcX;
+ byte *dstRow = renderBitmap + dy * pitch;
+ for (int x = 0; x < drawWidth; x++) {
+ int dx = destX + x;
+ if (dx < 0 || dx >= clipWidth)
+ continue;
+
+ byte pixel = srcRow[x];
+ if (pixel != 0 && pixel != 231)
+ dstRow[dx] = pixel;
+ }
+ }
+}
+
// renderEmbeddedFrame -- Blit a decoded embedded frame to the video buffer.
void InsaneRebel2::renderEmbeddedFrame(byte *renderBitmap, const EmbeddedSanFrame &frame, int userId) {
// Render the decoded embedded frame to the video buffer
@@ -72,18 +121,8 @@ void InsaneRebel2::renderEmbeddedFrame(byte *renderBitmap, const EmbeddedSanFram
int pitch = (_player && _player->_width > 0) ? _player->_width : 320;
int bufHeight = (_player && _player->_height > 0) ? _player->_height : 200;
- for (int y = 0; y < frame.height && (frame.renderY + y) < bufHeight; y++) {
- for (int x = 0; x < frame.width && (frame.renderX + x) < pitch; x++) {
- byte pixel = frame.pixels[y * frame.width + x];
- if (pixel != 0 && pixel != 231) { // 0 and 231 = transparent
- int destX = frame.renderX + x;
- int destY = frame.renderY + y;
- if (destX >= 0 && destY >= 0) {
- renderBitmap[destY * pitch + destX] = pixel;
- }
- }
- }
- }
+ blitEmbeddedFrameRegion(renderBitmap, pitch, pitch, bufHeight, frame,
+ frame.renderX, frame.renderY, 0, 0, frame.width, frame.height);
debug("Rebel2: Rendered embedded HUD %d at (%d,%d)", userId, frame.renderX, frame.renderY);
}
@@ -217,11 +256,7 @@ void InsaneRebel2::loadEmbeddedSan(int userId, byte *animData, int32 size, byte
// Count non-zero pixels to verify frame has content
if (frame.valid) {
- int nonZeroPixels = 0;
- for (int i = 0; i < width * height; i++) {
- if (frame.pixels[i] != 0)
- nonZeroPixels++;
- }
+ int nonZeroPixels = countEmbeddedFramePixels(frame);
debug("Rebel2: Frame userId=%d has %d non-zero pixels (%d%%)",
userId, nonZeroPixels, (nonZeroPixels * 100) / (width * height));
}
@@ -2672,7 +2707,7 @@ void InsaneRebel2::renderEmbeddedHudOverlays(byte *renderBitmap, int pitch, int
for (int hudSlot = 1; hudSlot < 16; hudSlot++) {
EmbeddedSanFrame &frame = _rebelEmbeddedHud[hudSlot];
- if (!frame.valid || !frame.pixels || frame.width <= 0 || frame.height <= 0)
+ if (!isValidEmbeddedFrame(frame))
continue;
// Handler 25: Skip slot 4 (corridor overlay) in post-rendering.
@@ -2707,23 +2742,11 @@ void InsaneRebel2::renderEmbeddedHudOverlays(byte *renderBitmap, int pitch, int
int selectedOffset = _shipDirectionIndex % groupCount;
int selectedId = groupMembers[selectedOffset];
- // Verify selected frame has pixels
EmbeddedSanFrame &selectedFrame = _rebelEmbeddedHud[selectedId];
- int nonZero = 0;
- for (int i = 0; i < selectedFrame.width * selectedFrame.height; i++) {
- if (selectedFrame.pixels[i] != 0)
- nonZero++;
- }
-
- if (nonZero == 0) {
+ if (countEmbeddedFramePixels(selectedFrame) == 0) {
for (int i = 0; i < groupCount; i++) {
EmbeddedSanFrame &altFrame = _rebelEmbeddedHud[groupMembers[i]];
- int altNonZero = 0;
- for (int j = 0; j < altFrame.width * altFrame.height; j++) {
- if (altFrame.pixels[j] != 0)
- altNonZero++;
- }
- if (altNonZero > 0) {
+ if (countEmbeddedFramePixels(altFrame) > 0) {
selectedId = groupMembers[i];
break;
}
@@ -2762,19 +2785,8 @@ void InsaneRebel2::renderEmbeddedHudOverlays(byte *renderBitmap, int pitch, int
debug(3, "Rebel2: Rendering embedded HUD slot=%d size=%dx%d at (%d,%d)",
hudSlot, frame.width, frame.height, destX, destY);
- // Draw frame with transparency (pixel 0 and 231 = transparent)
- for (int y = 0; y < frame.height && (destY + y) < height; y++) {
- for (int x = 0; x < frame.width && (destX + x) < pitch; x++) {
- byte pixel = frame.pixels[y * frame.width + x];
- if (pixel != 0 && pixel != 231) {
- int fx = destX + x;
- int fy = destY + y;
- if (fx >= 0 && fy >= 0) {
- renderBitmap[fy * pitch + fx] = pixel;
- }
- }
- }
- }
+ blitEmbeddedFrameRegion(renderBitmap, pitch, pitch, height, frame,
+ destX, destY, 0, 0, frame.width, frame.height);
}
}
@@ -3336,7 +3348,7 @@ void InsaneRebel2::renderFallbackShip(byte *renderBitmap, int pitch, int width,
return;
EmbeddedSanFrame &shipFrame = _rebelEmbeddedHud[11];
- if (!shipFrame.valid || !shipFrame.pixels || shipFrame.width <= 0 || shipFrame.height <= 0)
+ if (!isValidEmbeddedFrame(shipFrame))
return;
// Calculate display offset
@@ -3368,21 +3380,8 @@ void InsaneRebel2::renderFallbackShip(byte *renderBitmap, int pitch, int width,
int drawX = shipScreenX - spriteW / 2 + _viewX;
int drawY = shipScreenY - spriteH / 2 + _viewY;
- // Blit from embedded HUD
- for (int y = 0; y < spriteH && (drawY + y) < height; y++) {
- if (drawY + y < 0)
- continue;
- for (int x = 0; x < spriteW && (drawX + x) < width; x++) {
- if (drawX + x < 0)
- continue;
- int srcIdx = (srcY + y) * shipFrame.width + (srcX + x);
- byte pixel = shipFrame.pixels[srcIdx];
- if (pixel != 0 && pixel != 231) {
- int dstIdx = (drawY + y) * pitch + (drawX + x);
- renderBitmap[dstIdx] = pixel;
- }
- }
- }
+ blitEmbeddedFrameRegion(renderBitmap, pitch, width, height, shipFrame,
+ drawX, drawY, srcX, srcY, spriteW, spriteH);
debug("Rebel2: Ship (fallback) at (%d,%d) strip=(%d,%d) of (%dx%d) dir=(%d,%d)",
drawX, drawY, srcX, srcY, numHorizontal, numVertical, _shipDirectionH, _shipDirectionV);
Commit: 5568808962e2bf964c997d4dbc04214037986e2d
https://github.com/scummvm/scummvm/commit/5568808962e2bf964c997d4dbc04214037986e2d
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:54+02:00
Commit Message:
SCUMM: RA2: Harden embedded SAN loading
Changed paths:
engines/scumm/insane/rebel2/render.cpp
diff --git a/engines/scumm/insane/rebel2/render.cpp b/engines/scumm/insane/rebel2/render.cpp
index 0023a910441..82eb8f85b63 100644
--- a/engines/scumm/insane/rebel2/render.cpp
+++ b/engines/scumm/insane/rebel2/render.cpp
@@ -89,6 +89,30 @@ static void blitEmbeddedFrameRegion(byte *renderBitmap, int pitch, int clipWidth
}
}
+static bool readEmbeddedSanChunkHeader(Common::SeekableReadStream &stream, int64 containerEnd, const char *context,
+ uint32 &tag, uint32 &chunkSize, int64 &dataEnd, int64 &nextChunkPos) {
+ const int64 headerPos = stream.pos();
+ if (headerPos < 0 || headerPos + 8 > containerEnd)
+ return false;
+
+ tag = stream.readUint32BE();
+ chunkSize = stream.readUint32BE();
+
+ const int64 dataStart = stream.pos();
+ if ((int64)chunkSize > containerEnd - dataStart) {
+ debug("Rebel2: Truncated embedded SAN %s chunk 0x%08X at %lld: size=%u, remaining=%lld",
+ context, tag, headerPos, chunkSize, containerEnd - dataStart);
+ return false;
+ }
+
+ dataEnd = dataStart + chunkSize;
+ nextChunkPos = dataEnd + (chunkSize & 1);
+ if (nextChunkPos > containerEnd)
+ nextChunkPos = dataEnd;
+
+ return true;
+}
+
// renderEmbeddedFrame -- Blit a decoded embedded frame to the video buffer.
void InsaneRebel2::renderEmbeddedFrame(byte *renderBitmap, const EmbeddedSanFrame &frame, int userId) {
// Render the decoded embedded frame to the video buffer
@@ -134,12 +158,13 @@ void InsaneRebel2::renderEmbeddedFrame(byte *renderBitmap, const EmbeddedSanFram
//
void InsaneRebel2::loadEmbeddedSan(int userId, byte *animData, int32 size, byte *renderBitmap) {
// Validate userId - Level 3 uses slots 0-11, allow up to 15 for safety
- if (userId < 0 || userId > 15 || !animData || size < 32) {
+ if (userId < 0 || userId > 15 || !animData || size < 8) {
debug("Rebel2: Invalid embedded SAN: userId=%d, size=%d", userId, size);
return;
}
Common::MemoryReadStream stream(animData, size);
+ const int64 streamEnd = stream.size();
// Read ANIM header
uint32 animTag = stream.readUint32BE();
@@ -148,42 +173,44 @@ void InsaneRebel2::loadEmbeddedSan(int userId, byte *animData, int32 size, byte
return;
}
uint32 animSize = stream.readUint32BE();
- debug("Rebel2: Parsing embedded ANIM: userId=%d, reported size=%u, actual=%d", userId, animSize, size - 8);
+ int64 animEnd = streamEnd;
+ if ((int64)animSize <= streamEnd - 8) {
+ animEnd = 8 + (int64)animSize;
+ } else {
+ debug("Rebel2: Embedded ANIM truncated: reported size=%u, actual=%lld", animSize, streamEnd - 8);
+ }
+ debug("Rebel2: Parsing embedded ANIM: userId=%d, reported size=%u, actual=%lld", userId, animSize, streamEnd - 8);
// Iterate through chunks to find FRME -> FOBJ
- while (!stream.eos() && stream.pos() < size) {
- uint32 tag = stream.readUint32BE();
- uint32 chunkSize = stream.readUint32BE();
- int32 nextChunkPos = stream.pos() + chunkSize;
+ while (!stream.eos() && stream.pos() + 8 <= animEnd) {
+ uint32 tag;
+ uint32 chunkSize;
+ int64 chunkDataEnd;
+ int64 nextChunkPos;
+ if (!readEmbeddedSanChunkHeader(stream, animEnd, "top-level", tag, chunkSize, chunkDataEnd, nextChunkPos))
+ break;
if (tag == MKTAG('F','R','M','E')) {
// Iterate sub-chunks in FRME
- while (stream.pos() < nextChunkPos && !stream.eos()) {
- uint32 subTag = stream.readUint32BE();
- uint32 subSize = stream.readUint32BE();
- int32 nextSubPos = stream.pos() + subSize;
+ while (stream.pos() + 8 <= chunkDataEnd && !stream.eos()) {
+ uint32 subTag;
+ uint32 subSize;
+ int64 subDataEnd;
+ int64 nextSubPos;
+ if (!readEmbeddedSanChunkHeader(stream, chunkDataEnd, "FRME", subTag, subSize, subDataEnd, nextSubPos))
+ break;
if (subTag == MKTAG('F','O','B','J')) {
- // Found FOBJ - Embedded HUD Frame
- // Dump raw FOBJ bytes for analysis
- int32 fobjStart = stream.pos();
- byte rawHeader[20];
- int headerBytesToRead = MIN((int)subSize, 20);
- stream.read(rawHeader, headerBytesToRead);
- stream.seek(fobjStart); // Reset to read normally
-
- debug("Rebel2: Raw FOBJ header (%d bytes): %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X",
- headerBytesToRead,
- rawHeader[0], rawHeader[1], rawHeader[2], rawHeader[3],
- rawHeader[4], rawHeader[5], rawHeader[6], rawHeader[7],
- rawHeader[8], rawHeader[9], rawHeader[10], rawHeader[11],
- rawHeader[12], rawHeader[13], rawHeader[14], rawHeader[15],
- rawHeader[16], rawHeader[17], rawHeader[18], rawHeader[19]);
+ if (subSize < 14) {
+ debug("Rebel2: Embedded FOBJ too small: userId=%d, size=%u", userId, subSize);
+ stream.seek(nextSubPos);
+ continue;
+ }
// Read FOBJ header
int codec = stream.readUint16LE();
- int left = stream.readUint16LE();
- int top = stream.readUint16LE();
+ int left = stream.readSint16LE();
+ int top = stream.readSint16LE();
int width = stream.readUint16LE();
int height = stream.readUint16LE();
stream.readUint16LE(); // unknown
@@ -203,11 +230,16 @@ void InsaneRebel2::loadEmbeddedSan(int userId, byte *animData, int32 size, byte
// Allocate storage for the decoded frame
EmbeddedSanFrame &frame = _rebelEmbeddedHud[userId];
+ frame.valid = false;
if (width > 0 && height > 0 && width <= 800 && height <= 480) {
if (frame.width != width || frame.height != height || !frame.pixels) {
free(frame.pixels);
frame.pixels = (byte *)malloc(width * height);
+ if (!frame.pixels) {
+ warning("Rebel2: Failed to allocate embedded HUD frame: %dx%d", width, height);
+ return;
+ }
frame.width = width;
frame.height = height;
}
@@ -219,10 +251,19 @@ void InsaneRebel2::loadEmbeddedSan(int userId, byte *animData, int32 size, byte
frame.renderY = top;
// Read the raw FOBJ data
- int32 dataSize = subSize - 14;
+ int32 dataSize = (int32)(subDataEnd - stream.pos());
if (dataSize > 0) {
byte *fobjData = (byte *)malloc(dataSize);
- stream.read(fobjData, dataSize);
+ if (!fobjData) {
+ warning("Rebel2: Failed to allocate embedded FOBJ data: %d bytes", dataSize);
+ return;
+ }
+ uint32 bytesRead = stream.read(fobjData, dataSize);
+ if (bytesRead != (uint32)dataSize) {
+ debug("Rebel2: Short embedded FOBJ read: got %u of %d bytes", bytesRead, dataSize);
+ free(fobjData);
+ return;
+ }
// Decode based on codec - use extracted helper functions (FUN_0042BD60, etc.)
if (codec == 1 || codec == 3) {
@@ -273,15 +314,12 @@ void InsaneRebel2::loadEmbeddedSan(int userId, byte *animData, int32 size, byte
} else {
// Skip other sub-chunks (AHDR inside FRME?) or padding
stream.seek(nextSubPos);
- if (subSize & 1)
- stream.skip(1);
}
}
+ stream.seek(nextChunkPos);
} else {
// Skip non-FRME chunks (AHDR, etc at top level)
stream.seek(nextChunkPos);
- if (chunkSize & 1)
- stream.skip(1);
}
}
Commit: b1fe2f8fd0acec06be33544a0b04b42066fb0f1c
https://github.com/scummvm/scummvm/commit/b1fe2f8fd0acec06be33544a0b04b42066fb0f1c
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:55+02:00
Commit Message:
SCUMM: RA2: Refactor level 2 background loading
Changed paths:
engines/scumm/insane/rebel2/iact.cpp
diff --git a/engines/scumm/insane/rebel2/iact.cpp b/engines/scumm/insane/rebel2/iact.cpp
index 1f676bf0733..b9c81478028 100644
--- a/engines/scumm/insane/rebel2/iact.cpp
+++ b/engines/scumm/insane/rebel2/iact.cpp
@@ -33,6 +33,30 @@
namespace Scumm {
+static bool readLevel2BackgroundChunkHeader(Common::SeekableReadStream &stream, int64 containerEnd, const char *context,
+ uint32 &tag, uint32 &chunkSize, int64 &dataEnd, int64 &nextChunkPos) {
+ const int64 headerPos = stream.pos();
+ if (headerPos < 0 || headerPos + 8 > containerEnd)
+ return false;
+
+ tag = stream.readUint32BE();
+ chunkSize = stream.readUint32BE();
+
+ const int64 dataStart = stream.pos();
+ if ((int64)chunkSize > containerEnd - dataStart) {
+ debug("Rebel2 loadLevel2Background: Truncated %s chunk 0x%08X at %lld: size=%u, remaining=%lld",
+ context, tag, headerPos, chunkSize, containerEnd - dataStart);
+ return false;
+ }
+
+ dataEnd = dataStart + chunkSize;
+ nextChunkPos = dataEnd + (chunkSize & 1);
+ if (nextChunkPos > containerEnd)
+ nextChunkPos = dataEnd;
+
+ return true;
+}
+
//
// procPreRendering -- Pre-frame setup: background restore and corridor overlays.
//
@@ -2103,77 +2127,104 @@ bool InsaneRebel2::loadLevel2Background(byte *animData, int32 size, byte *render
memset(_level2Background, 0, 320 * 200);
}
- // Parse embedded ANIM to find FOBJ
- // Structure: ANIM tag at offset 0, AHDR, then FRME with FOBJ
- int animOffset = 0;
- if (READ_BE_UINT32(animData) == MKTAG('A','N','I','M')) {
- uint32 animSize = READ_BE_UINT32(animData + 4);
- debug("Rebel2 loadLevel2Background: Found ANIM tag, size=%u", animSize);
+ Common::MemoryReadStream stream(animData, size);
+ const int64 streamEnd = stream.size();
- // Skip ANIM header (8 bytes) + AHDR chunk
- if (size >= 16 && READ_BE_UINT32(animData + 8) == MKTAG('A','H','D','R')) {
- uint32 ahdrSize = READ_BE_UINT32(animData + 12);
- animOffset = 8 + 8 + ahdrSize; // After ANIM tag + AHDR
- debug("Rebel2 loadLevel2Background: AHDR size=%u, FRME expected at offset %d", ahdrSize, animOffset);
- }
+ uint32 animTag = stream.readUint32BE();
+ if (animTag != MKTAG('A','N','I','M')) {
+ debug("Rebel2 loadLevel2Background: Missing ANIM tag, got 0x%08X", animTag);
+ return false;
+ }
+
+ uint32 animSize = stream.readUint32BE();
+ int64 animEnd = streamEnd;
+ if ((int64)animSize <= streamEnd - 8) {
+ animEnd = 8 + (int64)animSize;
+ } else {
+ debug("Rebel2 loadLevel2Background: ANIM truncated: reported size=%u, actual=%lld",
+ animSize, streamEnd - 8);
}
+ debug("Rebel2 loadLevel2Background: Found ANIM tag, size=%u", animSize);
- // Look for FRME containing FOBJ
bool foundBackground = false;
- for (int scanPos = animOffset; scanPos + 16 < size && !foundBackground; scanPos++) {
- if (READ_BE_UINT32(animData + scanPos) == MKTAG('F','R','M','E')) {
- int frmeSize = READ_BE_UINT32(animData + scanPos + 4);
- debug("Rebel2 loadLevel2Background: Found FRME at %d, size=%d", scanPos, frmeSize);
-
- for (int fobjPos = scanPos + 8; fobjPos + 18 < scanPos + 8 + frmeSize && fobjPos + 18 < size; fobjPos++) {
- if (READ_BE_UINT32(animData + fobjPos) == MKTAG('F','O','B','J')) {
- byte *fobjData = animData + fobjPos + 8;
-
- // FOBJ header: codec(2), x(2), y(2), w(2), h(2)
- int16 codec = READ_LE_INT16(fobjData);
- int16 fobjX = READ_LE_INT16(fobjData + 2);
- int16 fobjY = READ_LE_INT16(fobjData + 4);
- int16 fobjW = READ_LE_INT16(fobjData + 6);
- int16 fobjH = READ_LE_INT16(fobjData + 8);
-
- debug("Rebel2 loadLevel2Background: Found FOBJ: codec=%d pos=(%d,%d) size=%dx%d",
- codec, fobjX, fobjY, fobjW, fobjH);
-
- // Decode codec 3 (RLE) into background buffer
- // Use smushDecodeRLEOpaque to write ALL colors including color 0 (black).
- // The standard smushDecodeRLE treats color 0 as transparent, which causes
- // the background to appear as a "sketch" with black pixels missing.
- if (codec == 3 && fobjW > 0 && fobjH > 0 && fobjW <= 320 && fobjH <= 200) {
- byte *rleData = fobjData + 14; // Skip full 14-byte FOBJ header
- smushDecodeRLEOpaque(_level2Background, rleData, fobjX, fobjY, fobjW, fobjH, 320);
-
- debug("Rebel2 loadLevel2Background: Decoded Level 2 background (%dx%d at %d,%d)",
- fobjW, fobjH, fobjX, fobjY);
- _level2BackgroundLoaded = true;
- foundBackground = true;
-
- // Copy to render bitmap immediately if provided.
- // Only copy when render buffer pitch is 320 (standard screen size).
- // For oversized buffers (e.g., Level 12's 640x260 corridor),
- // the FOBJ/FETCH system handles background rendering and copying
- // 320-wide data into a wider buffer would corrupt the corridor.
- if (renderBitmap) {
- int bufferPitch = (_player && _player->_width > 0) ? _player->_width : 320;
- if (bufferPitch == 320) {
- for (int by = 0; by < 200; by++) {
- memcpy(renderBitmap + by * 320, _level2Background + by * 320, 320);
- }
- debug("Rebel2 loadLevel2Background: Copied to renderBitmap (pitch=%d)", bufferPitch);
- } else {
- debug("Rebel2 loadLevel2Background: Skipping renderBitmap copy (pitch=%d != 320)", bufferPitch);
- }
+ while (!stream.eos() && stream.pos() + 8 <= animEnd && !foundBackground) {
+ uint32 tag;
+ uint32 chunkSize;
+ int64 chunkDataEnd;
+ int64 nextChunkPos;
+ if (!readLevel2BackgroundChunkHeader(stream, animEnd, "ANIM", tag, chunkSize, chunkDataEnd, nextChunkPos))
+ break;
+
+ if (tag != MKTAG('F','R','M','E')) {
+ stream.seek(nextChunkPos);
+ continue;
+ }
+
+ debug("Rebel2 loadLevel2Background: Found FRME at %lld, size=%u", stream.pos() - 8, chunkSize);
+
+ while (stream.pos() + 8 <= chunkDataEnd && !stream.eos() && !foundBackground) {
+ uint32 subTag;
+ uint32 subSize;
+ int64 subDataEnd;
+ int64 nextSubPos;
+ if (!readLevel2BackgroundChunkHeader(stream, chunkDataEnd, "FRME", subTag, subSize, subDataEnd, nextSubPos))
+ break;
+
+ if (subTag != MKTAG('F','O','B','J')) {
+ stream.seek(nextSubPos);
+ continue;
+ }
+
+ if (subSize < 14) {
+ debug("Rebel2 loadLevel2Background: FOBJ too small: size=%u", subSize);
+ stream.seek(nextSubPos);
+ continue;
+ }
+
+ // FOBJ header: codec(2), x(2), y(2), w(2), h(2)
+ int codec = stream.readUint16LE();
+ int fobjX = stream.readSint16LE();
+ int fobjY = stream.readSint16LE();
+ int fobjW = stream.readSint16LE();
+ int fobjH = stream.readSint16LE();
+ stream.readUint16LE(); // unknown
+ stream.readUint16LE(); // unknown
+
+ debug("Rebel2 loadLevel2Background: Found FOBJ: codec=%d pos=(%d,%d) size=%dx%d",
+ codec, fobjX, fobjY, fobjW, fobjH);
+
+ // Decode codec 3 (RLE) into background buffer.
+ // Use smushDecodeRLEOpaque to write ALL colors including color 0 (black).
+ if (codec == 3 && fobjX >= 0 && fobjY >= 0 && fobjW > 0 && fobjH > 0 &&
+ fobjX + fobjW <= 320 && fobjY + fobjH <= 200 && stream.pos() < subDataEnd) {
+ const byte *rleData = animData + stream.pos();
+ smushDecodeRLEOpaque(_level2Background, rleData, fobjX, fobjY, fobjW, fobjH, 320);
+
+ debug("Rebel2 loadLevel2Background: Decoded Level 2 background (%dx%d at %d,%d)",
+ fobjW, fobjH, fobjX, fobjY);
+ _level2BackgroundLoaded = true;
+ foundBackground = true;
+
+ // Copy to render bitmap immediately if provided.
+ // Only copy when render buffer pitch is 320 (standard screen size).
+ // For oversized buffers, the FOBJ/FETCH system handles background rendering.
+ if (renderBitmap) {
+ int bufferPitch = (_player && _player->_width > 0) ? _player->_width : 320;
+ if (bufferPitch == 320) {
+ for (int by = 0; by < 200; by++) {
+ memcpy(renderBitmap + by * 320, _level2Background + by * 320, 320);
}
+ debug("Rebel2 loadLevel2Background: Copied to renderBitmap (pitch=%d)", bufferPitch);
+ } else {
+ debug("Rebel2 loadLevel2Background: Skipping renderBitmap copy (pitch=%d != 320)", bufferPitch);
}
- break;
}
}
- break;
+
+ stream.seek(nextSubPos);
}
+
+ stream.seek(nextChunkPos);
}
if (!foundBackground) {
Commit: 3881b4371b00bf85cda374ceedeb7296abe195b3
https://github.com/scummvm/scummvm/commit/3881b4371b00bf85cda374ceedeb7296abe195b3
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:55+02:00
Commit Message:
SCUMM: RA: Pass data size to line update decoder
Changed paths:
engines/scumm/insane/rebel2/iact.cpp
engines/scumm/insane/rebel2/render.cpp
engines/scumm/smush/rebel/codec_ra2.cpp
engines/scumm/smush/rebel/codec_ra2.h
engines/scumm/smush/rebel/smush_player_ra1.cpp
engines/scumm/smush/rebel/smush_player_ra1.h
engines/scumm/smush/rebel/smush_player_ra2.cpp
engines/scumm/smush/rebel/smush_player_ra2.h
diff --git a/engines/scumm/insane/rebel2/iact.cpp b/engines/scumm/insane/rebel2/iact.cpp
index b9c81478028..e0903fc0d27 100644
--- a/engines/scumm/insane/rebel2/iact.cpp
+++ b/engines/scumm/insane/rebel2/iact.cpp
@@ -2198,7 +2198,8 @@ bool InsaneRebel2::loadLevel2Background(byte *animData, int32 size, byte *render
if (codec == 3 && fobjX >= 0 && fobjY >= 0 && fobjW > 0 && fobjH > 0 &&
fobjX + fobjW <= 320 && fobjY + fobjH <= 200 && stream.pos() < subDataEnd) {
const byte *rleData = animData + stream.pos();
- smushDecodeRLEOpaque(_level2Background, rleData, fobjX, fobjY, fobjW, fobjH, 320);
+ smushDecodeRLEOpaque(_level2Background, rleData, fobjX, fobjY, fobjW, fobjH, 320,
+ (int)(subDataEnd - stream.pos()));
debug("Rebel2 loadLevel2Background: Decoded Level 2 background (%dx%d at %d,%d)",
fobjW, fobjH, fobjX, fobjY);
diff --git a/engines/scumm/insane/rebel2/render.cpp b/engines/scumm/insane/rebel2/render.cpp
index 82eb8f85b63..5a2b14241a9 100644
--- a/engines/scumm/insane/rebel2/render.cpp
+++ b/engines/scumm/insane/rebel2/render.cpp
@@ -278,7 +278,7 @@ void InsaneRebel2::loadEmbeddedSan(int userId, byte *animData, int32 size, byte
debug("Rebel2: Decoded embedded HUD (codec 20/raw): %dx%d", width, height);
} else if (codec == 21 || codec == 44) {
// Codec 21/44: Line update (FUN_0042BD60)
- smushDecodeLineUpdate(frame.pixels, fobjData, 0, 0, width, height, width);
+ smushDecodeLineUpdate(frame.pixels, fobjData, 0, 0, width, height, width, dataSize);
frame.valid = true;
debug("Rebel2: Decoded embedded HUD (codec %d/line update): %dx%d", codec, width, height);
} else if (codec == 45) {
diff --git a/engines/scumm/smush/rebel/codec_ra2.cpp b/engines/scumm/smush/rebel/codec_ra2.cpp
index 4251a4342e1..7ca7dd2e0d1 100644
--- a/engines/scumm/smush/rebel/codec_ra2.cpp
+++ b/engines/scumm/smush/rebel/codec_ra2.cpp
@@ -26,10 +26,57 @@
#include "common/endian.h"
#include "common/textconsole.h"
-#include "scumm/bomp.h"
-
namespace Scumm {
+static void bompDecodeLineOpaqueBounded(byte *dst, const byte *src, const byte *srcEnd, int len) {
+ while (len > 0 && src < srcEnd) {
+ byte code = *src++;
+ int num = (code >> 1) + 1;
+ if (num > len)
+ num = len;
+
+ if (code & 1) {
+ if (src >= srcEnd)
+ break;
+ memset(dst, *src++, num);
+ dst += num;
+ len -= num;
+ } else {
+ int toCopy = num;
+ if (toCopy > (int)(srcEnd - src))
+ toCopy = (int)(srcEnd - src);
+ memcpy(dst, src, toCopy);
+ src += toCopy;
+ dst += toCopy;
+ len -= toCopy;
+ if (toCopy < num)
+ break;
+ }
+ }
+}
+
+const byte *smushSkipRLELines(const byte *src, int &dataSize, int lines) {
+ for (int i = 0; i < lines; i++) {
+ if (dataSize < 2) {
+ src += dataSize;
+ dataSize = 0;
+ break;
+ }
+
+ int rowSize = READ_LE_UINT16(src) + 2;
+ if (rowSize > dataSize) {
+ src += dataSize;
+ dataSize = 0;
+ break;
+ }
+
+ src += rowSize;
+ dataSize -= rowSize;
+ }
+
+ return src;
+}
+
/**
* Codec 3 RLE decoder that writes ALL colors including color 0 (black).
* Use this for background images where color 0 should NOT be treated as transparent.
@@ -38,14 +85,24 @@ namespace Scumm {
*
* Used by: Rebel Assault 2 Level 2 background loading (IACT opcode 8, par4=5)
*/
-void smushDecodeRLEOpaque(byte *dst, const byte *src, int left, int top, int width, int height, int pitch) {
+void smushDecodeRLEOpaque(byte *dst, const byte *src, int left, int top, int width, int height, int pitch, int dataSize) {
+ if (dataSize <= 0)
+ return;
+
+ const byte *srcEnd = src + dataSize;
dst += top * pitch;
- do {
+ while (height-- && srcEnd - src >= 2) {
+ int lineSize = READ_LE_UINT16(src);
+ src += 2;
+ if (lineSize > srcEnd - src)
+ lineSize = (int)(srcEnd - src);
+ const byte *lineEnd = src + lineSize;
+
dst += left;
- bompDecodeLine(dst, src + 2, width, true); // setZero = TRUE to write all colors
- src += READ_LE_UINT16(src) + 2;
+ bompDecodeLineOpaqueBounded(dst, src, lineEnd, width);
+ src = lineEnd;
dst += pitch - left;
- } while (--height);
+ }
}
/**
@@ -55,39 +112,53 @@ void smushDecodeRLEOpaque(byte *dst, const byte *src, int left, int top, int wid
* The count value needs +1 to get the actual number of pixels to copy.
* Note: Skip regions preserve previous frame content (delta compression).
*/
-void smushDecodeLineUpdate(byte *dst, const byte *src, int left, int top, int width, int height, int pitch) {
+void smushDecodeLineUpdate(byte *dst, const byte *src, int left, int top, int width, int height, int pitch, int dataSize) {
+ if (dataSize <= 0)
+ return;
+
+ const byte *srcEnd = src + dataSize;
dst += top * pitch + left;
- while (height--) {
+ while (height-- && srcEnd - src >= 2) {
byte *dstPtrNext = dst + pitch;
- const byte *srcPtrNext = src + 2 + READ_LE_UINT16(src);
- src += 2; // Skip line size header
+ int lineDataSize = READ_LE_UINT16(src);
+ src += 2;
+ if (lineDataSize > srcEnd - src)
+ lineDataSize = (int)(srcEnd - src);
+ const byte *lineEnd = src + lineDataSize;
int len = width;
byte *lineDst = dst;
- while (len > 0) {
+ while (len > 0 && lineEnd - src >= 2) {
// Read 2-byte LE skip value
int skip = READ_LE_UINT16(src);
src += 2;
+ if (skip >= len)
+ break;
lineDst += skip;
len -= skip;
- if (len <= 0)
- break;
// Read 2-byte LE copy count (+1 for actual count)
+ if (lineEnd - src < 2)
+ break;
int count = READ_LE_UINT16(src) + 1;
src += 2;
if (count > len)
count = len;
- len -= count;
// Copy literal pixels
- memcpy(lineDst, src, count);
- lineDst += count;
- src += count;
+ int toCopy = count;
+ if (toCopy > (int)(lineEnd - src))
+ toCopy = (int)(lineEnd - src);
+ memcpy(lineDst, src, toCopy);
+ lineDst += toCopy;
+ src += toCopy;
+ len -= toCopy;
+ if (toCopy < count)
+ break;
}
dst = dstPtrNext;
- src = srcPtrNext;
+ src = lineEnd;
}
}
diff --git a/engines/scumm/smush/rebel/codec_ra2.h b/engines/scumm/smush/rebel/codec_ra2.h
index f9b0191623b..1aa8d2e86d0 100644
--- a/engines/scumm/smush/rebel/codec_ra2.h
+++ b/engines/scumm/smush/rebel/codec_ra2.h
@@ -26,10 +26,11 @@
namespace Scumm {
-void smushDecodeRLEOpaque(byte *dst, const byte *src, int left, int top, int width, int height, int pitch);
-void smushDecodeLineUpdate(byte *dst, const byte *src, int left, int top, int width, int height, int pitch);
+void smushDecodeRLEOpaque(byte *dst, const byte *src, int left, int top, int width, int height, int pitch, int dataSize);
+void smushDecodeLineUpdate(byte *dst, const byte *src, int left, int top, int width, int height, int pitch, int dataSize);
void smushDecodeSkipRLE(byte *dst, const byte *src, int left, int top, int width, int height, int pitch, int dataSize);
void smushDecodeRA2Bomp(byte *dst, const byte *src, int left, int top, int width, int height, int pitch, int dataSize);
+const byte *smushSkipRLELines(const byte *src, int &dataSize, int lines);
} // End of namespace Scumm
diff --git a/engines/scumm/smush/rebel/smush_player_ra1.cpp b/engines/scumm/smush/rebel/smush_player_ra1.cpp
index 9c7dcfd685c..3a2df42c5e4 100644
--- a/engines/scumm/smush/rebel/smush_player_ra1.cpp
+++ b/engines/scumm/smush/rebel/smush_player_ra1.cpp
@@ -75,6 +75,7 @@ void SmushPlayerRebel1::initGamePlayerFields() {
_ra1ObjOverlayHeight = 0;
_ra1ViewportOffsetX = 0;
_ra1ViewportOffsetY = 0;
+ _ra1FrameSourceSkipY = 0;
}
void SmushPlayerRebel1::destroyGamePlayerFields() {
@@ -508,7 +509,16 @@ bool SmushPlayerRebel1::handleGameAdjustCoords(int codec, int &left, int &top, i
// positions â they must NOT be clipped/adjusted.
if (codec == SMUSH_CODEC_SKIP_RLE || codec == SMUSH_CODEC_RA1_SCATTER)
return false;
- adjustFrameCoords(left, top, width, height, pitch, srcSkipY);
+ int sourceSkipY = 0;
+ _ra1FrameSourceSkipY = 0;
+ adjustFrameCoords(left, top, width, height, pitch, &sourceSkipY);
+ if (codec == SMUSH_CODEC_RLE_ALT) {
+ _ra1FrameSourceSkipY = sourceSkipY;
+ if (srcSkipY)
+ *srcSkipY = 0;
+ } else if (srcSkipY) {
+ *srcSkipY = sourceSkipY;
+ }
return true;
}
@@ -518,7 +528,8 @@ bool SmushPlayerRebel1::handleGameCodecDecode(int codec, const uint8 *src, int l
smushDecodeRA1Transparent(_dst, src, left, top, width, height, pitch);
return true;
case SMUSH_CODEC_RLE_ALT:
- smushDecodeRLEOpaque(_dst, src, left, top, width, height, pitch);
+ src = smushSkipRLELines(src, dataSize, _ra1FrameSourceSkipY);
+ smushDecodeRLEOpaque(_dst, src, left, top, width, height, pitch, dataSize);
return true;
case SMUSH_CODEC_RA1_SCATTER:
smushDecodeRA1Scatter(_dst, src, left, top, _width, _height, pitch, dataSize);
diff --git a/engines/scumm/smush/rebel/smush_player_ra1.h b/engines/scumm/smush/rebel/smush_player_ra1.h
index 407fbb309e2..e53fbea39a1 100644
--- a/engines/scumm/smush/rebel/smush_player_ra1.h
+++ b/engines/scumm/smush/rebel/smush_player_ra1.h
@@ -80,6 +80,7 @@ private:
// RA1 viewport scroll offset for interactive gameplay
int _ra1ViewportOffsetX;
int _ra1ViewportOffsetY;
+ int _ra1FrameSourceSkipY;
};
} // End of namespace Scumm
diff --git a/engines/scumm/smush/rebel/smush_player_ra2.cpp b/engines/scumm/smush/rebel/smush_player_ra2.cpp
index 7dd4a90e0c1..311b34df0fe 100644
--- a/engines/scumm/smush/rebel/smush_player_ra2.cpp
+++ b/engines/scumm/smush/rebel/smush_player_ra2.cpp
@@ -85,6 +85,7 @@ void SmushPlayerRebel2::initGamePlayerFields() {
_loadReadOffset = 8; // Original starts reading at offset 8 (skips header)
_lastLoadChunkIdx = -1;
_totalLoadChunks = 0;
+ _ra2FrameSourceSkipY = 0;
_scrollX = 0;
_scrollY = 0;
}
@@ -593,15 +594,21 @@ bool SmushPlayerRebel2::ra2DecodeCodec(int codec, const uint8 *src, int left, in
int width, int height, int pitch, int dataSize) {
switch (codec) {
case SMUSH_CODEC_LINE_UPDATE:
- case SMUSH_CODEC_LINE_UPDATE2:
- smushDecodeLineUpdate(_dst, src, left, top, width, height, pitch);
+ case SMUSH_CODEC_LINE_UPDATE2: {
+ const uint8 *adjustedSrc = smushSkipRLELines(src, dataSize, _ra2FrameSourceSkipY);
+ smushDecodeLineUpdate(_dst, adjustedSrc, left, top, width, height, pitch, dataSize);
return true;
- case SMUSH_CODEC_SKIP_RLE:
- smushDecodeSkipRLE(_dst, src, left, top, width, height, pitch, dataSize);
+ }
+ case SMUSH_CODEC_SKIP_RLE: {
+ const uint8 *adjustedSrc = smushSkipRLELines(src, dataSize, _ra2FrameSourceSkipY);
+ smushDecodeSkipRLE(_dst, adjustedSrc, left, top, width, height, pitch, dataSize);
return true;
- case SMUSH_CODEC_RA2_BOMP:
- smushDecodeRA2Bomp(_dst, src, left, top, width, height, pitch, dataSize);
+ }
+ case SMUSH_CODEC_RA2_BOMP: {
+ const uint8 *adjustedSrc = smushSkipRLELines(src, dataSize, _ra2FrameSourceSkipY);
+ smushDecodeRA2Bomp(_dst, adjustedSrc, left, top, width, height, pitch, dataSize);
return true;
+ }
default:
return false;
}
@@ -731,7 +738,17 @@ bool SmushPlayerRebel2::handleGameDimensionOverride(int codec, int width, int he
}
bool SmushPlayerRebel2::handleGameAdjustCoords(int codec, int &left, int &top, int &width, int &height, int pitch, int *srcSkipY) {
- adjustFrameCoords(left, top, width, height, pitch, srcSkipY);
+ int sourceSkipY = 0;
+ _ra2FrameSourceSkipY = 0;
+ adjustFrameCoords(left, top, width, height, pitch, &sourceSkipY);
+ if (codec == SMUSH_CODEC_LINE_UPDATE || codec == SMUSH_CODEC_LINE_UPDATE2 ||
+ codec == SMUSH_CODEC_SKIP_RLE || codec == SMUSH_CODEC_RA2_BOMP) {
+ _ra2FrameSourceSkipY = sourceSkipY;
+ if (srcSkipY)
+ *srcSkipY = 0;
+ } else if (srcSkipY) {
+ *srcSkipY = sourceSkipY;
+ }
return true;
}
diff --git a/engines/scumm/smush/rebel/smush_player_ra2.h b/engines/scumm/smush/rebel/smush_player_ra2.h
index 375d2a6a015..7a1004df06c 100644
--- a/engines/scumm/smush/rebel/smush_player_ra2.h
+++ b/engines/scumm/smush/rebel/smush_player_ra2.h
@@ -79,6 +79,7 @@ private:
int32 _loadReadOffset;
int16 _lastLoadChunkIdx;
int16 _totalLoadChunks;
+ int _ra2FrameSourceSkipY;
};
} // End of namespace Scumm
Commit: 0bf0542235af92ee644556fcae0f99e507760347
https://github.com/scummvm/scummvm/commit/0bf0542235af92ee644556fcae0f99e507760347
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:55+02:00
Commit Message:
SCUMM: RA2: Remove unused code and old TODOs
Changed paths:
engines/scumm/insane/rebel2/iact.cpp
engines/scumm/insane/rebel2/levels.cpp
engines/scumm/insane/rebel2/menu.cpp
engines/scumm/insane/rebel2/rebel.cpp
engines/scumm/insane/rebel2/rebel.h
engines/scumm/insane/rebel2/render.cpp
engines/scumm/insane/rebel2/runlevels.cpp
engines/scumm/smush/rebel/smush_player_ra2.cpp
diff --git a/engines/scumm/insane/rebel2/iact.cpp b/engines/scumm/insane/rebel2/iact.cpp
index e0903fc0d27..5d1423ca472 100644
--- a/engines/scumm/insane/rebel2/iact.cpp
+++ b/engines/scumm/insane/rebel2/iact.cpp
@@ -1176,7 +1176,6 @@ void InsaneRebel2::iactRebel2Opcode6(byte *renderBitmap, Common::SeekableReadStr
if (_grdSpriteMode == 3) {
if (_rebelDamageLevel == 5) {
// At max damage, check for direction change input
- // For now, use mouse X position to determine direction
int16 mouseX = getGameplayAimPoint().x;
if (_player && _player->_width > 320) {
mouseX = (mouseX * 320) / _player->_width;
diff --git a/engines/scumm/insane/rebel2/levels.cpp b/engines/scumm/insane/rebel2/levels.cpp
index 97eb5bf40d8..8d6e118e5bf 100644
--- a/engines/scumm/insane/rebel2/levels.cpp
+++ b/engines/scumm/insane/rebel2/levels.cpp
@@ -252,195 +252,6 @@ void InsaneRebel2::playLevelBegin(int levelId) {
}
}
-//
-// playLevelGameplay -- Main gameplay video(s) for a level
-//
-// Returns true if level completed (shield > 0), false if died.
-// Structures vary by level: single SAN, multi-part subdirs, or two phases.
-//
-bool InsaneRebel2::playLevelGameplay(int levelId) {
-
- Common::String dir = getLevelDir(levelId);
- Common::String prefix = getLevelPrefix(levelId);
- Common::String filename;
-
- SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
-
- // Set gameplay flags (interactive with HUD)
- splayer->setCurVideoFlags(0x28);
-
- // Reset damage/shield for this level
- _playerShield = 255;
- _rebelHandler = 0;
- _rebelStatusBarSprite = 0; // Will be set by IACT opcode 6 if par4==1
-
- debug("Rebel2: Starting gameplay for level %d", levelId);
-
- switch (levelId) {
- case 1:
- // Level 1: Single gameplay file (01P01.SAN)
- // Level 1 uses Handler 0x26 (turret mode) - set before gameplay
- _rebelHandler = 0x26;
- filename = Common::String::format("%s/%sP01.SAN", dir.c_str(), prefix.c_str());
- debug("Rebel2: Playing %s", filename.c_str());
- splayer->play(filename.c_str(), 12);
- break;
-
- case 2:
- // Level 2: Has cutscene first, then multiple parts
- // Level 2 uses Handler 8 (third-person on foot mode) - set before gameplay
- _rebelHandler = 8;
- // First play the cutscene
- filename = Common::String::format("%s/%sCUT.SAN", dir.c_str(), prefix.c_str());
- debug("Rebel2: Playing cutscene %s", filename.c_str());
- splayer->setCurVideoFlags(0x28);
- splayer->play(filename.c_str(), 12);
-
- if (_vm->shouldQuit() || _playerShield == 0)
- return false;
-
- // Part 1 (multiple variations - play A for now)
- splayer->setCurVideoFlags(0x28);
- filename = Common::String::format("%s/P1/%sP01_A.SAN", dir.c_str(), prefix.c_str());
- debug("Rebel2: Playing %s", filename.c_str());
- splayer->play(filename.c_str(), 12);
-
- if (_vm->shouldQuit() || _playerShield == 0)
- return false;
-
- // Post segment 1
- _rebelHandler = 0;
- _rebelStatusBarSprite = 0;
- filename = Common::String::format("%s/%sPST1.SAN", dir.c_str(), prefix.c_str());
- debug("Rebel2: Playing %s", filename.c_str());
- splayer->setCurVideoFlags(0x28);
- splayer->play(filename.c_str(), 12);
-
- if (_vm->shouldQuit() || _playerShield == 0)
- return false;
-
- // Part 2
- _rebelHandler = 8;
- splayer->setCurVideoFlags(0x28);
- filename = Common::String::format("%s/P2/%sP02_A.SAN", dir.c_str(), prefix.c_str());
- debug("Rebel2: Playing %s", filename.c_str());
- splayer->play(filename.c_str(), 12);
-
- if (_vm->shouldQuit() || _playerShield == 0)
- return false;
-
- // Post segment 2
- _rebelHandler = 0;
- _rebelStatusBarSprite = 0;
- filename = Common::String::format("%s/%sPST2.SAN", dir.c_str(), prefix.c_str());
- debug("Rebel2: Playing %s", filename.c_str());
- splayer->setCurVideoFlags(0x28);
- splayer->play(filename.c_str(), 12);
-
- if (_vm->shouldQuit() || _playerShield == 0)
- return false;
-
- // Part 3
- _rebelHandler = 8;
- splayer->setCurVideoFlags(0x28);
- filename = Common::String::format("%s/P3/%sP03_A.SAN", dir.c_str(), prefix.c_str());
- debug("Rebel2: Playing %s", filename.c_str());
- splayer->play(filename.c_str(), 12);
- break;
-
- case 3:
- // Level 3: Two gameplay phases (third-person ship)
- // Level 3 uses Handler 7 (third-person ship mode) - FUN_0040d836/FUN_0040c3cc
- _rebelHandler = 7;
- filename = Common::String::format("%s/%sPLAY1.SAN", dir.c_str(), prefix.c_str());
- debug("Rebel2: Playing %s", filename.c_str());
- splayer->play(filename.c_str(), 12);
-
- if (_vm->shouldQuit() || _playerShield == 0)
- return false;
-
- // Post segment
- _rebelHandler = 0;
- _rebelStatusBarSprite = 0;
- filename = Common::String::format("%s/%sPOST1.SAN", dir.c_str(), prefix.c_str());
- debug("Rebel2: Playing %s", filename.c_str());
- splayer->setCurVideoFlags(0x28);
- splayer->play(filename.c_str(), 12);
-
- if (_vm->shouldQuit() || _playerShield == 0)
- return false;
-
- // Phase 2 â handler will be re-set by IACT opcode 6
- splayer->setCurVideoFlags(0x28);
- filename = Common::String::format("%s/%sPLAY2.SAN", dir.c_str(), prefix.c_str());
- debug("Rebel2: Playing %s", filename.c_str());
- splayer->play(filename.c_str(), 12);
- break;
-
- case 4:
- _rebelHandler = 0x26;
- // Level 4: Has cutscene, then single gameplay
- filename = Common::String::format("%s/%sCUT.SAN", dir.c_str(), prefix.c_str());
- debug("Rebel2: Playing cutscene %s", filename.c_str());
- splayer->setCurVideoFlags(0x28);
- splayer->play(filename.c_str(), 12);
-
- if (_vm->shouldQuit())
- return false;
-
- splayer->setCurVideoFlags(0x28);
- filename = Common::String::format("%s/%sPLAY.SAN", dir.c_str(), prefix.c_str());
- debug("Rebel2: Playing %s", filename.c_str());
- splayer->play(filename.c_str(), 12);
- break;
-
- case 5:
- // Level 5: Single gameplay file
- filename = Common::String::format("%s/%sPLAY.SAN", dir.c_str(), prefix.c_str());
- debug("Rebel2: Playing %s", filename.c_str());
- splayer->play(filename.c_str(), 12);
- break;
-
- case 6:
- // Level 6: Two gameplay phases
- filename = Common::String::format("%s/%sPLAY1.SAN", dir.c_str(), prefix.c_str());
- debug("Rebel2: Playing %s", filename.c_str());
- splayer->play(filename.c_str(), 12);
-
- if (_vm->shouldQuit() || _playerShield == 0)
- return false;
-
- // Post segment
- _rebelHandler = 0;
- _rebelStatusBarSprite = 0;
- filename = Common::String::format("%s/%sPOST1.SAN", dir.c_str(), prefix.c_str());
- debug("Rebel2: Playing %s", filename.c_str());
- splayer->setCurVideoFlags(0x28);
- splayer->play(filename.c_str(), 12);
-
- if (_vm->shouldQuit() || _playerShield == 0)
- return false;
-
- // Phase 2 â handler will be re-set by IACT opcode 6
- splayer->setCurVideoFlags(0x28);
- filename = Common::String::format("%s/%sPLAY2.SAN", dir.c_str(), prefix.c_str());
- debug("Rebel2: Playing %s", filename.c_str());
- splayer->play(filename.c_str(), 12);
- break;
-
- default:
- // For levels 7-15 (not in demo), try common patterns
- // First try XXPLAY.SAN
- filename = Common::String::format("%s/%sPLAY.SAN", dir.c_str(), prefix.c_str());
- debug("Rebel2: Trying %s", filename.c_str());
- splayer->play(filename.c_str(), 12);
- break;
- }
-
- // Return true if player survived (shield > 0), false if died
- return (_playerShield > 0);
-}
-
// playLevelEnd -- Level completion video (FUN_00417327).
void InsaneRebel2::playLevelEnd(int levelId) {
@@ -459,31 +270,6 @@ void InsaneRebel2::playLevelEnd(int levelId) {
splayer->play(filename.c_str(), 12);
}
-// playLevelDeath -- Death video (LEVXX/XXDIE_X.SAN, FUN_00417168).
-void InsaneRebel2::playLevelDeath(int levelId) {
-
- _rebelHandler = 0;
- _rebelStatusBarSprite = 0; // No status bar during death cinematic
-
- Common::String dir = getLevelDir(levelId);
- Common::String prefix = getLevelPrefix(levelId);
-
- // Most levels have DIE_A, some have just DIE
- Common::String filename;
- if (levelId == 2 || levelId == 4 || levelId == 10 || levelId == 12 || levelId == 14) {
- filename = Common::String::format("%s/%sDIE.SAN", dir.c_str(), prefix.c_str());
- } else {
- filename = Common::String::format("%s/%sDIE_A.SAN", dir.c_str(), prefix.c_str());
- }
-
- debug("Rebel2: Playing level %d death: %s", levelId, filename.c_str());
-
- SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
- // Original: FUN_00417168 adds | 8, so flags = 0x20 | 0x08 = 0x28
- splayer->setCurVideoFlags(0x28);
- splayer->play(filename.c_str(), 12);
-}
-
// playLevelRetry -- Retry prompt video (LEVXX/XXRETRY.SAN, FUN_00417168).
void InsaneRebel2::playLevelRetry(int levelId) {
@@ -819,8 +605,8 @@ Common::String InsaneRebel2::selectDeathVideoVariant(int levelId, int phase, int
case 9:
// Level 9 (FUN_00419B86): Based on DAT_0047ab94 (death cause)
- // 0âA, 1âC, elseâB. Use phase as proxy.
- return "A"; // Default; exact tracking of DAT_0047ab94 deferred
+ // 0âA, 1âC, elseâB. DAT_0047ab94 is not tracked yet.
+ return "A";
case 10:
// Level 10 (FUN_00419E0A): Single death video (no variant suffix)
@@ -888,7 +674,7 @@ void InsaneRebel2::playLevelDeathVariant(int levelId, int phase, int frame) {
Common::String filename;
if (variant.empty()) {
- // No variant suffix (Level 2, 4)
+ // No variant suffix.
filename = Common::String::format("%s/%sDIE.SAN", dir.c_str(), prefix.c_str());
} else {
filename = Common::String::format("%s/%sDIE_%s.SAN", dir.c_str(), prefix.c_str(), variant.c_str());
diff --git a/engines/scumm/insane/rebel2/menu.cpp b/engines/scumm/insane/rebel2/menu.cpp
index d95d3ba7cf5..b03b822305a 100644
--- a/engines/scumm/insane/rebel2/menu.cpp
+++ b/engines/scumm/insane/rebel2/menu.cpp
@@ -82,7 +82,7 @@ Common::String InsaneRebel2::getRandomMenuVideo() {
//
// processMenuInput -- Menu input handling (FUN_0041f5ae)
//
-// Returns -1 (no action) or 0-4 (menu item selected).
+// Returns -1 (no action) or a 0-based selected menu item.
// Events captured by notifyEvent() before ScummEngine consumes them.
// Keyboard: Up=0x148, Down=0x150, Enter=0x0d, ESC=0x1b.
// Mouse mode (DAT_0047a806 == 1): Y position maps to selection.
@@ -93,10 +93,9 @@ int InsaneRebel2::processMenuInput() {
// Menu item Y positions (low-res 320x200 mode):
// From FUN_0041f5ae: baseY = numItems * -5 + 0x68
- // With 8 total items (title + 7 options): 8 * -5 + 104 = 64
- // Items at Y = 64, 74, 84, 94, 104, 114, 124 with spacing of 10
- const int numItemsTotal = 8; // Title + 7 selectable items (matching assembly)
- const int baseY = numItemsTotal * -5 + 0x68; // = 64
+ // With 7 selectable items: 7 * -5 + 104 = 69
+ // Items at Y = 69, 79, 89, 99, 109, 119, 129 with spacing of 10
+ const int baseY = _menuItemCount * -5 + 0x68;
const int itemSpacing = 10;
// Process events from the queue (populated by notifyEvent)
@@ -138,7 +137,7 @@ int InsaneRebel2::processMenuInput() {
break;
case Common::KEYCODE_ESCAPE:
- // ESC - Quit (index 4 = last item) - emulates key code 0x1b
+ // ESC - Quit (last item) - emulates key code 0x1b
result = _menuItemCount - 1; // Select quit option
debug("Menu: ESC pressed - selecting quit (item %d)", result);
break;
@@ -150,32 +149,30 @@ int InsaneRebel2::processMenuInput() {
case Common::EVENT_LBUTTONDOWN:
_menuInactivityTimer = 0;
- // TODO: Re-enable click-to-confirm (currently disabled for easier testing)
- // Original behavior: clicking on a menu item both highlights and confirms it.
+ _vm->_mouse.x = event.mouse.x;
+ _vm->_mouse.y = event.mouse.y;
+ for (int i = 0; i < _menuItemCount; i++) {
+ int itemY = baseY + i * itemSpacing;
+ if (event.mouse.y >= itemY - 1 && event.mouse.y < itemY + 9) {
+ _menuSelection = i;
+ result = _menuSelection;
+ debug("Menu: Item %d selected (mouse)", _menuSelection);
+ break;
+ }
+ }
break;
case Common::EVENT_MOUSEMOVE:
- // Update hover selection based on Y position
- // This emulates FUN_0041f5ae mouse mode behavior (DAT_0047a806 == 1)
{
int mouseY = event.mouse.y;
- // Calculate selection from mouse Y position
- // From assembly: DAT_00459988 = ((mouseY + 100) - (param_3 * -5 + 0x67)) / 10
- int newSelection = (mouseY + 100 - (numItemsTotal * -5 + 0x67)) / 10;
-
- // Clamp to valid range
- if (newSelection < 0)
- newSelection = 0;
- if (newSelection >= _menuItemCount)
- newSelection = _menuItemCount - 1;
-
- // Only update if within menu area (not too far above/below)
- int topY = baseY - 5;
- int bottomY = baseY + (_menuItemCount - 1) * itemSpacing + 10;
- if (mouseY >= topY && mouseY <= bottomY) {
- if (newSelection != _menuSelection) {
- _menuSelection = newSelection;
- debug(5, "Menu: Hover selection changed to %d (mouseY=%d)", _menuSelection, mouseY);
+ for (int i = 0; i < _menuItemCount; i++) {
+ int itemY = baseY + i * itemSpacing;
+ if (mouseY >= itemY - 4 && mouseY < itemY + 6) {
+ if (i != _menuSelection) {
+ _menuSelection = i;
+ debug(5, "Menu: Hover selection changed to %d (mouseY=%d)", _menuSelection, mouseY);
+ }
+ break;
}
}
}
@@ -890,8 +887,21 @@ int InsaneRebel2::processChapterSelectInput() {
break;
case Common::EVENT_LBUTTONDOWN:
- // TODO: Re-enable click-to-confirm (currently disabled for easier testing)
- // Original behavior: any click confirms current selection (DAT_0047a7e4 & 1)
+ _vm->_mouse.x = event.mouse.x;
+ _vm->_mouse.y = event.mouse.y;
+ {
+ int baseY = _chapterItemCount * -5 + 0x68;
+ for (int i = 0; i < _chapterItemCount; i++) {
+ int itemY = baseY + i * 10;
+ if (event.mouse.y >= itemY - 1 && event.mouse.y < itemY + 9) {
+ _chapterSelection = i;
+ _previewOffsetY = _chapterSelection * -50 + 75;
+ result = _chapterSelection;
+ debug("ChapterSelect: Item %d selected (mouse)", _chapterSelection);
+ break;
+ }
+ }
+ }
break;
case Common::EVENT_MOUSEMOVE:
@@ -1474,6 +1484,8 @@ int InsaneRebel2::processLevelSelectInput() {
bool isDifficultyMode = (_gameState == kStateDifficultySelect);
int &selection = isDifficultyMode ? _difficultySelection : _levelSelection;
int itemCount = isDifficultyMode ? 6 : _levelItemCount;
+ const int itemBaseY = itemCount * -5 + 0x68;
+ const int itemSpacing = 10;
while (!_menuEventQueue.empty()) {
Common::Event event = _menuEventQueue.pop();
@@ -1515,12 +1527,28 @@ int InsaneRebel2::processLevelSelectInput() {
break;
case Common::EVENT_LBUTTONDOWN:
- // TODO: Re-enable click-to-confirm (currently disabled for easier testing)
+ _vm->_mouse.x = event.mouse.x;
+ _vm->_mouse.y = event.mouse.y;
+ for (int i = 0; i < itemCount; i++) {
+ int itemY = itemBaseY + i * itemSpacing;
+ if (event.mouse.y >= itemY - 1 && event.mouse.y < itemY + 9) {
+ selection = i;
+ result = selection;
+ break;
+ }
+ }
break;
case Common::EVENT_MOUSEMOVE:
_vm->_mouse.x = event.mouse.x;
_vm->_mouse.y = event.mouse.y;
+ for (int i = 0; i < itemCount; i++) {
+ int itemY = itemBaseY + i * itemSpacing;
+ if (event.mouse.y >= itemY - 4 && event.mouse.y < itemY + 6) {
+ selection = i;
+ break;
+ }
+ }
break;
case Common::EVENT_QUIT:
diff --git a/engines/scumm/insane/rebel2/rebel.cpp b/engines/scumm/insane/rebel2/rebel.cpp
index c9fb01a370c..3c576b408f6 100644
--- a/engines/scumm/insane/rebel2/rebel.cpp
+++ b/engines/scumm/insane/rebel2/rebel.cpp
@@ -73,7 +73,7 @@ InsaneRebel2::InsaneRebel2(ScummEngine_v7 *scumm) {
// Rebel Assault 2: Load cockpit sprites NUT which contains crosshairs, explosions, status bar
// CPITIMAG.NUT = low-res (320x200), CPITIMHI.NUT = high-res (640x480)
- // For now, use CPITIMAG since the game runs at 320x200
+ // The current renderer runs at 320x200, so use the low-res assets.
_smush_iconsNut = new NutRenderer(_vm, "SYSTM/CPITIMAG.NUT");
_smush_icons2Nut = nullptr; // Not used for Rebel2
@@ -169,7 +169,7 @@ InsaneRebel2::InsaneRebel2(ScummEngine_v7 *scumm) {
}
_rebelLastCounter = 0;
- _difficulty = 1; // Default to Medium (1). TODO: Read from game config
+ _difficulty = 1; // Default to Medium.
_targetLockTimer = 0; // DAT_00443676 equivalent
_speed = 12;
@@ -401,8 +401,7 @@ InsaneRebel2::InsaneRebel2(ScummEngine_v7 *scumm) {
// Main menu has 7 selectable items (0-6) matching GAME.TRS indices 11-17:
// 0: Start Game, 1: Options, 2: Calibrate Joystick, 3: Continue Intro,
// 4: Show Top Pilots, 5: Show Credits, 6: Return to Launcher
- // Note: The coordinate formula uses numItemsTotal = 8 (includes title) for Y position calculation
- // Formula from FUN_0041f5ae: (DAT_0047a806 == 0) + 6 = 7 items for keyboard mode
+ // FUN_0041f5ae uses the selectable item count for Y position calculation.
_menuItemCount = 7;
_menuInactivityTimer = 0;
_lastMenuVariant = -1; // No previous menu video
diff --git a/engines/scumm/insane/rebel2/rebel.h b/engines/scumm/insane/rebel2/rebel.h
index 650613168e1..a16874082ce 100644
--- a/engines/scumm/insane/rebel2/rebel.h
+++ b/engines/scumm/insane/rebel2/rebel.h
@@ -359,16 +359,9 @@ public:
// Play level beginning cinematic (LEVXX/XXBEG.SAN)
void playLevelBegin(int levelId);
- // Play main gameplay video(s) for a level
- // Returns true if level completed, false if player died
- bool playLevelGameplay(int levelId);
-
// Play level completion video (LEVXX/XXEND.SAN)
void playLevelEnd(int levelId);
- // Play death video (LEVXX/XXDIE_X.SAN) - variant based on frame/location
- void playLevelDeath(int levelId);
-
// Play retry prompt video (LEVXX/XXRETRY.SAN)
void playLevelRetry(int levelId);
diff --git a/engines/scumm/insane/rebel2/render.cpp b/engines/scumm/insane/rebel2/render.cpp
index 5a2b14241a9..950256d543f 100644
--- a/engines/scumm/insane/rebel2/render.cpp
+++ b/engines/scumm/insane/rebel2/render.cpp
@@ -291,7 +291,7 @@ void InsaneRebel2::loadEmbeddedSan(int userId, byte *animData, int32 size, byte
frame.valid = true;
debug("Rebel2: Decoded embedded HUD (codec 23/skip-RLE): %dx%d", width, height);
} else {
- debug("Rebel2: TODO: Decode codec %d for embedded HUD", codec);
+ debug("Rebel2: Unsupported embedded HUD codec %d", codec);
frame.valid = false;
}
@@ -519,7 +519,7 @@ void InsaneRebel2::spawnSpaceShot(int x, int y) {
// Calculate gun positions from direction-based lookup tables
// In the original, these come from tables indexed by _shipDirectionIndex
// DAT_004437c2/DAT_00443808 for left gun, DAT_0044384e/DAT_00443894 for right gun
- // For now, use simplified positions relative to ship
+ // Use simplified positions relative to the ship.
int shipScreenX = 160 + ((_shipPosX - 160) >> 3);
int shipScreenY = 105 + ((_shipPosY - 40) >> 2);
diff --git a/engines/scumm/insane/rebel2/runlevels.cpp b/engines/scumm/insane/rebel2/runlevels.cpp
index 44bab3adfd0..bb32a844149 100644
--- a/engines/scumm/insane/rebel2/runlevels.cpp
+++ b/engines/scumm/insane/rebel2/runlevels.cpp
@@ -125,8 +125,8 @@ uint16 InsaneRebel2::processWaveEnd(int16 mask, int16 *budget, int16 threshold,
// The SmushPlayer::play() call already blocks until video ends, so this step
// is handled implicitly. The early-exit logic (threshold > 0: if frame > 50
// AND all required enemy type bits are set, count up and break when > threshold)
- // would need per-frame callbacks to work precisely. For now, the primary effect
- // is covered by the video playing to completion and accumulating state.
+ // requires per-frame callbacks to work precisely. The current implementation
+ // covers the primary effect by playing the full video and accumulating state.
// TODO: Implement per-frame early exit callback for threshold-based wave termination.
// Step 2: Copy wave state to phase state (line 33)
@@ -230,7 +230,6 @@ int InsaneRebel2::runLevel2() {
// Play level beginning cinematic (02BEG.SAN)
// Original: FUN_004171c5("LEV02/02BEG.SAN", 0x20, 0xab, 0xa0, 10, 2, 0x46)
- // Includes text overlay from GAME.TRS â deferred until text rendering is ready.
playLevelBegin(2);
if (_vm->shouldQuit())
return kLevelQuit;
@@ -498,7 +497,7 @@ int InsaneRebel2::runLevel2() {
// Level completed! Calculate accuracy score.
// Original: FUN_00417327 with score thresholds and medal ranks
- // Score presentation deferred until GAME.TRS text rendering is implemented.
+ // Score presentation remains to be implemented.
{
totalMisses += _rebelHitCounter;
int accuracy = 0;
@@ -522,9 +521,8 @@ int InsaneRebel2::runLevel2() {
if (_vm->shouldQuit())
return kLevelQuit;
- // Original: if (DAT_0047ab5c != 0) DAT_0047a7ee++ (bonus life award)
- // DAT_0047ab5c is set when player earns a bonus life (e.g., score threshold).
- // Currently not tracked â will be wired when bonus life system is implemented.
+ // Original: if (DAT_0047ab5c != 0) DAT_0047a7ee++ (bonus life award).
+ // DAT_0047ab5c is set when the player earns a bonus life.
_playerLives--;
if (_playerLives <= 0) {
// Original: FUN_00417ab2("LEV02/02OVER.SAN", 0x20, 2)
@@ -617,6 +615,7 @@ int InsaneRebel2::runLevel3() {
while (!_vm->shouldQuit()) {
_playerShield = 255;
_playerDamage = 0;
+ _playerScore = phase1Score;
// Reset bit table before gameplay starts
clearBit(0);
@@ -800,7 +799,7 @@ int InsaneRebel2::runLevel6() {
// phase 2 retries + death handling. Phase 1 death breaks inner â RETRY at outer bottom.
// Phase 2 death â RETRYB â re-enters phase 2 within inner loop.
SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
- int totalScore = 0;
+ int phase1Score = 0;
// Play level beginning cinematic (06BEG.SAN)
// Original: FUN_004171c5(s_LEV06_06BEG_SAN, 0x20, 0xaf, 0xa0, 10, 5, 0x4b)
@@ -852,8 +851,8 @@ int InsaneRebel2::runLevel6() {
continue;
}
- // Phase 1 survived â save score, play POST1
- totalScore = _playerScore; // variantIdx = DAT_0047ab84
+ // Phase 1 survived â preserve this score across phase 2 retries.
+ phase1Score = _playerScore; // variantIdx = DAT_0047ab84
_rebelHandler = 0;
_rebelStatusBarSprite = 0;
@@ -866,6 +865,7 @@ int InsaneRebel2::runLevel6() {
while (!_vm->shouldQuit()) {
_rebelLevelType = 6; // DAT_0047a7f8 = 6
_currentPhase = 2;
+ _playerScore = phase1Score;
clearBit(0); // FUN_00407d10
debug("Rebel2: Level 6 Phase 2");
@@ -876,9 +876,6 @@ int InsaneRebel2::runLevel6() {
if (_vm->shouldQuit())
return kLevelQuit;
- // Accumulate score: variantIdx = DAT_0047ab84 + variantIdx
- totalScore += _playerScore;
-
if (_playerShield > 0) {
// Level completed!
debug("Rebel2: Level 6 completed!");
@@ -947,8 +944,8 @@ int InsaneRebel2::runLevel7() {
// Original: FUN_0041f4d0("07PLAY.SAN", 0x28, -1, -1, 0)
// At frame 0x638 (1592), if DAT_0047ab8c != 0: play 07PLAYB.SAN (0x468)
// TODO: Mid-level fork at frame 1592 requires per-frame callback.
- // For now, play the main video. The fork video (07PLAYB) would be triggered
- // by IACT callbacks setting a state flag during gameplay.
+ // The fork video (07PLAYB) should be triggered by IACT callbacks setting
+ // a state flag during gameplay.
splayer->setCurVideoFlags(0x28);
splayer->play("LEV07/07PLAY.SAN", 12);
_deathFrame = splayer->_frame;
@@ -1461,7 +1458,7 @@ int InsaneRebel2::runLevel11() {
if (!allBasicKilled) {
// Normal bridge drop cinematic
playCinematic("LEV11/11POST3.SAN");
- // Bonus checks (FUN_0042aa70) â deferred, play standard path
+ // Bonus checks (FUN_0042aa70) are not implemented; play the standard path.
// Original checks 0x77 and 0x62 for special POST3C cinematic
} else {
// All enemy types killed â bridge dropped successfully
@@ -1969,8 +1966,8 @@ int InsaneRebel2::runLevel13() {
// If alive after Phase A, play Phase B (reactor destruction loop)
// Original: at frame == maxFrame-10, play 13PLAY_B.SAN (0x468)
// Then loop while (DAT_0047ab90 != 0 || DAT_0047ab7c != 0)
- // For now, play B as a sequential video. The IACT callbacks will manage
- // the reactor target state through opcode interactions.
+ // Play B as a sequential video. The IACT callbacks manage the reactor
+ // target state through opcode interactions.
if (_playerShield > 0) {
splayer->setCurVideoFlags(0x468);
splayer->play("LEV13/13PLAY_B.SAN", 12);
diff --git a/engines/scumm/smush/rebel/smush_player_ra2.cpp b/engines/scumm/smush/rebel/smush_player_ra2.cpp
index 311b34df0fe..2610822cdee 100644
--- a/engines/scumm/smush/rebel/smush_player_ra2.cpp
+++ b/engines/scumm/smush/rebel/smush_player_ra2.cpp
@@ -408,7 +408,7 @@ bool SmushPlayerRebel2::handleGameAnimHeader(byte *headerContent) {
}
// ---------------------------------------------------------------------------
-// RA2 helper methods (still on SmushPlayer for now, used by base class)
+// RA2 helper methods used by the base SmushPlayer pipeline.
// ---------------------------------------------------------------------------
void SmushPlayerRebel2::handleGameLoad(int32 subSize, Common::SeekableReadStream &b) {
Commit: 4ab5b29537b1c3e0903c031c99b6485a71d0c34f
https://github.com/scummvm/scummvm/commit/4ab5b29537b1c3e0903c031c99b6485a71d0c34f
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:56+02:00
Commit Message:
SCUMM: RA2: Remove remaining dead code
Changed paths:
engines/scumm/insane/rebel2/levels.cpp
engines/scumm/insane/rebel2/menu.cpp
engines/scumm/insane/rebel2/rebel.cpp
engines/scumm/insane/rebel2/rebel.h
engines/scumm/insane/rebel2/render.cpp
diff --git a/engines/scumm/insane/rebel2/levels.cpp b/engines/scumm/insane/rebel2/levels.cpp
index 8d6e118e5bf..d0d2368c7d7 100644
--- a/engines/scumm/insane/rebel2/levels.cpp
+++ b/engines/scumm/insane/rebel2/levels.cpp
@@ -413,8 +413,6 @@ int InsaneRebel2::runLevel(int levelId) {
_damageHighFlashCounter = 0;
_damageShakeCounter = 0;
_currentPhase = 1;
- _phaseScore = 0;
- _phaseMisses = 0;
_skipSectionRequested = false;
// Dispatch to per-level handler
diff --git a/engines/scumm/insane/rebel2/menu.cpp b/engines/scumm/insane/rebel2/menu.cpp
index b03b822305a..ffe9ccdb0cf 100644
--- a/engines/scumm/insane/rebel2/menu.cpp
+++ b/engines/scumm/insane/rebel2/menu.cpp
@@ -1003,125 +1003,6 @@ void InsaneRebel2::drawPreviewBox(byte *renderBitmap, int pitch, int width, int
}
}
-// Draw preview thumbnail content - emulates FUN_00428a10 + FUN_00429b40
-// Based on FUN_00415CF8 assembly analysis:
-//
-// The original uses O_LEVEL.SAN (640x400) with chapter previews stacked vertically.
-// Video offset (FUN_00425170) shifts which preview is visible:
-// X offset = -90 (0xffa6)
-// Y offset = chapter * -50 + 75
-//
-// For 320x200 mode, O_MENU_X.SAN doesn't contain chapter-specific preview images.
-// Those are only in O_LEVEL.SAN (640x400). We display a styled placeholder instead
-// with the chapter number and visual styling to match the original UI appearance.
-void InsaneRebel2::drawPreviewThumbnail(byte *renderBitmap, int pitch, int width, int height, int chapter) {
- // Preview destination area coordinates (inside the inner border)
- // From assembly: Inner box at X=230, Y=75, W=80, H=50
- const int destX = 230;
- const int destY = 75;
- const int thumbW = 80; // 0x50
- const int thumbH = 50; // 0x32
-
- // Fill preview area with a dark blue gradient background
- // This creates a styled placeholder since O_MENU_X.SAN doesn't have previews
- for (int py = 0; py < thumbH; py++) {
- int dy = destY + py;
- if (dy < 0 || dy >= height)
- continue;
-
- // Create vertical gradient: darker at top (0x10), lighter at bottom (0x18)
- byte bgColor = 0x10 + (py * 8 / thumbH);
-
- for (int px = 0; px < thumbW; px++) {
- int dx = destX + px;
- if (dx < 0 || dx >= width)
- continue;
- renderBitmap[dy * pitch + dx] = bgColor;
- }
- }
-
- // Draw chapter number overlay in the center of the preview
- NutRenderer *font = _smush_smalfontNut;
- if (!font)
- return;
-
- char chapterStr[16];
- if (chapter < 15) {
- snprintf(chapterStr, sizeof(chapterStr), "CH.%d", chapter + 1);
- } else {
- snprintf(chapterStr, sizeof(chapterStr), "FINALE");
- }
-
- // Calculate text width for centering
- int textWidth = 0;
- int numChars = font->getNumChars();
- for (const char *c = chapterStr; *c; c++) {
- int charIdx = (unsigned char)*c;
- if (charIdx < numChars) {
- textWidth += font->getCharWidth(charIdx);
- }
- }
-
- // Center the text in the preview area
- int textX = destX + (thumbW - textWidth) / 2;
- int textY = destY + thumbH / 2 - 4;
-
- Common::Rect clipRect(0, 0, width, height);
-
- // Draw text shadow (offset by 1,1)
- int curX = textX + 1;
- for (const char *c = chapterStr; *c; c++) {
- int charIdx = (unsigned char)*c;
- if (charIdx < numChars) {
- int charWidth = font->getCharWidth(charIdx);
- if (curX >= 0 && curX + charWidth <= width && textY + 1 >= 0 && textY + 1 < height) {
- font->drawCharV7(renderBitmap, clipRect, curX, textY + 1, pitch, 0,
- kStyleAlignLeft, charIdx, true, true);
- }
- curX += charWidth;
- }
- }
-
- // Draw main text (bright)
- curX = textX;
- for (const char *c = chapterStr; *c; c++) {
- int charIdx = (unsigned char)*c;
- if (charIdx < numChars) {
- int charWidth = font->getCharWidth(charIdx);
- if (curX >= 0 && curX + charWidth <= width && textY >= 0 && textY < height) {
- font->drawCharV7(renderBitmap, clipRect, curX, textY, pitch, -1,
- kStyleAlignLeft, charIdx, true, true);
- }
- curX += charWidth;
- }
- }
-
- // Draw lock icon for locked chapters
- if (!_chapterUnlocked[chapter]) {
- byte lockColor = 0xF8;
- int lockX = destX + thumbW - 15;
- int lockY = destY + 5;
-
- // Draw padlock shape
- for (int i = 2; i < 6; i++) {
- if (lockX + i < width && lockY < height && lockY >= 0)
- renderBitmap[lockY * pitch + lockX + i] = lockColor;
- }
- for (int i = 1; i < 4; i++) {
- if (lockX + 2 < width && lockY + i < height && lockY + i >= 0)
- renderBitmap[(lockY + i) * pitch + lockX + 2] = lockColor;
- if (lockX + 5 < width && lockY + i < height && lockY + i >= 0)
- renderBitmap[(lockY + i) * pitch + lockX + 5] = lockColor;
- }
- for (int y = 0; y < 4; y++) {
- for (int x = 1; x < 7; x++) {
- if (lockX + x < width && lockY + 4 + y < height && lockY + 4 + y >= 0)
- renderBitmap[(lockY + 4 + y) * pitch + lockX + x] = lockColor;
- }
- }
- }
-}
-
// Rating to medal string (FUN_0042001f): TALKFONT glyphs 3=big, 2=medium, 1=small
Common::String InsaneRebel2::getRankString(int rating) {
if (rating > 50)
diff --git a/engines/scumm/insane/rebel2/rebel.cpp b/engines/scumm/insane/rebel2/rebel.cpp
index 3c576b408f6..42fe279ad97 100644
--- a/engines/scumm/insane/rebel2/rebel.cpp
+++ b/engines/scumm/insane/rebel2/rebel.cpp
@@ -276,12 +276,6 @@ InsaneRebel2::InsaneRebel2(ScummEngine_v7 *scumm) {
_corridorBottomY = 0x104; // 260 â full game buffer height
_hitCooldown = 0;
- // Initialize legacy shot system (backwards compatibility)
- for (i = 0; i < 2; i++) {
- _shots[i].active = false;
- _shots[i].counter = 0;
- }
-
// Initialize Handler 0x26 Turret shot system (FUN_40AD63)
for (i = 0; i < 2; i++) {
_turretShots[i].counter = 0;
@@ -475,8 +469,6 @@ InsaneRebel2::InsaneRebel2(ScummEngine_v7 *scumm) {
// Initialize level state tracking for multi-phase levels
_currentPhase = 1;
_deathFrame = 0;
- _phaseScore = 0;
- _phaseMisses = 0;
_skipSectionRequested = false;
// Register as EventObserver to capture input events before ScummEngine consumes them
@@ -1106,21 +1098,6 @@ void InsaneRebel2::updatePilotProgress(int levelIndex, int32 score, int32 lives,
savePilots();
}
-int InsaneRebel2::getPilotHighestLevel() const {
- if (_activePilot < 0 || _activePilot >= _numPilots)
- return 0;
-
- const PilotData &pilot = _pilots[_activePilot];
- int highest = 0;
- for (int i = kNumLevels - 1; i >= 0; i--) {
- if (pilot.damage[i] < 0xFF) {
- highest = i;
- break;
- }
- }
- return highest;
-}
-
// processMouse -- Mouse input with edge detection for buttons.
int32 InsaneRebel2::processMouse() {
int32 buttons = 0;
diff --git a/engines/scumm/insane/rebel2/rebel.h b/engines/scumm/insane/rebel2/rebel.h
index a16874082ce..9325a802fe5 100644
--- a/engines/scumm/insane/rebel2/rebel.h
+++ b/engines/scumm/insane/rebel2/rebel.h
@@ -160,9 +160,6 @@ public:
// Draw the preview thumbnail box - emulates FUN_004292D0 calls in FUN_00415CF8
void drawPreviewBox(byte *renderBitmap, int pitch, int width, int height);
- // Draw the preview thumbnail content - shows chapter number/status in preview area
- void drawPreviewThumbnail(byte *renderBitmap, int pitch, int width, int height, int chapter);
-
// View offset for chapter preview scrolling (DAT_0047abe2/DAT_0047abe4)
int16 _previewOffsetX; // X offset = -90 for chapter select
int16 _previewOffsetY; // Y offset = chapter * -50 + 75
@@ -283,9 +280,6 @@ public:
// Update pilot progress after level completion
void updatePilotProgress(int levelIndex, int32 score, int32 lives, int32 damage);
- // Get highest unlocked level for active pilot (checks damage[] < 0xFF)
- int getPilotHighestLevel() const;
-
// ---------------------------------------------------------------------------
// Pilot Selection Menu (FUN_00414A41)
// ---------------------------------------------------------------------------
@@ -301,9 +295,7 @@ public:
enum PilotMenuMode {
kPilotModeSelect = 0, // Normal pilot list selection
kPilotModeNameInput = 1, // Typing a new pilot name
- kPilotModeDifficulty = 2, // Difficulty submenu
- kPilotModeDeleteConfirm = 3, // Delete confirmation
- kPilotModeCopySelect = 4 // Copy source selection
+ kPilotModeDifficulty = 2 // Difficulty submenu
};
PilotMenuMode _pilotMenuMode;
Common::String _pilotNameInput; // Current name being typed
@@ -442,8 +434,6 @@ public:
// Level state tracking for multi-phase levels
int _currentPhase; // Current gameplay phase (1, 2, 3 for Level 2; 1, 2 for Level 3/6)
int _deathFrame; // Frame number where player died (for death video selection)
- int _phaseScore; // Accumulated score from previous phases (preserved on phase retry)
- int _phaseMisses; // Accumulated misses from previous phases
bool _skipSectionRequested; // Debug shortcut (Shift+S): force current gameplay section to end
// ---------------------------------------------------------------------------
@@ -541,20 +531,6 @@ public:
// Reset enemy active flags and collision zones at frame end
void frameEndCleanup();
- // ---------------------------------------------------------------------------
- // Opcode 6 Helper Functions
- // ---------------------------------------------------------------------------
- // Handler-specific setup extracted from iactRebel2Opcode6.
-
- // Handler 8 (third-person on foot) setup - FUN_00401234 case 4
- void opcode6Handler8Setup(int16 par3, int16 par4);
-
- // Handler 7 (third-person ship) setup - FUN_0040c3cc case 4
- void opcode6Handler7Setup(int16 par3, int16 par4);
-
- // Calculate view offsets based on level type (lines 182-213)
- void opcode6CalcViewOffsets();
-
// ---------------------------------------------------------------------------
// Opcode 8 Helper Functions
// ---------------------------------------------------------------------------
@@ -618,7 +594,6 @@ public:
// _rebelDetailMode (DAT_0047a7fc): Controls whether edge highlights are drawn.
// Set from IACT opcode 6. When >= 0, edge highlights are enabled.
byte _edgeTable[256 * 256]; // DAT_0046a7d0 - primary edge blend table
- byte _edgeTableAlt[256 * 256]; // DAT_00443fb0 - secondary blend table (hi-res mode)
int16 _rebelDetailMode; // DAT_0047a7fc - edge highlight enable flag
// Initialize edge blend table (FUN_410510)
@@ -914,14 +889,6 @@ public:
SpaceShot _spaceShots[2];
int16 _spaceShotDirection; // DAT_0044374e - ship direction for gun lookup
- // Legacy struct for backwards compatibility
- struct Shot {
- bool active;
- int counter;
- int x, y; // Target position
- };
- Shot _shots[2];
-
// Handler-specific shot spawning
void spawnTurretShot(int x, int y); // Handler 0x26
void spawnVehicleShot(int x, int y); // Handler 8
@@ -1018,9 +985,6 @@ public:
int16 _shipDirectionH; // Horizontal direction (0-4, center=2)
int16 _shipDirectionV; // Vertical direction (0-6, center=3)
- // Helper to load a NUT file from IACT chunk data
- NutRenderer *loadNutFromIact(Common::SeekableReadStream &b, int dataSize);
-
// ---------------------------------------------------------------------------
// Handler 7 FLY Ship System
// ---------------------------------------------------------------------------
diff --git a/engines/scumm/insane/rebel2/render.cpp b/engines/scumm/insane/rebel2/render.cpp
index 950256d543f..08a0b731211 100644
--- a/engines/scumm/insane/rebel2/render.cpp
+++ b/engines/scumm/insane/rebel2/render.cpp
@@ -372,16 +372,6 @@ void InsaneRebel2::spawnShot(int x, int y) {
spawnHandler25Shot(x, y);
break;
default:
- // Legacy fallback
- for (int i = 0; i < 2; i++) {
- if (!_shots[i].active) {
- _shots[i].active = true;
- _shots[i].counter = getShotMaxDuration();
- _shots[i].x = x + _viewX;
- _shots[i].y = y + _viewY;
- break;
- }
- }
break;
}
}
@@ -394,14 +384,14 @@ void InsaneRebel2::spawnTurretShot(int x, int y) {
// levelType 5: BLAST.SAD (slot 0), otherwise: TBLAST.SAD (slot 7)
playSfx((_rebelLevelType == 5) ? 0 : 7, 127, 0);
- _turretShots[i].counter = getShotMaxDuration();
- _turretShots[i].seqNum = _turretShotSeqCounter;
- _turretShotSeqCounter++;
- _turretShots[i].targetX = x + _viewX; // DAT_0044366e in original
- _turretShots[i].targetY = y + _viewY; // DAT_00443670 in original
- break;
- }
+ _turretShots[i].counter = getShotMaxDuration();
+ _turretShots[i].seqNum = _turretShotSeqCounter;
+ _turretShotSeqCounter++;
+ _turretShots[i].targetX = x + _viewX; // DAT_0044366e in original
+ _turretShots[i].targetY = y + _viewY; // DAT_00443670 in original
+ break;
}
+ }
}
// spawnVehicleShot -- Handler 8 vehicle shot spawn (FUN_401CCF).
@@ -1025,11 +1015,10 @@ void InsaneRebel2::freeLaserTexture() {
}
//
-// initEdgeTable -- Initialize edge blend tables (FUN_410510).
+// initEdgeTable -- Initialize edge blend table (FUN_410510).
//
-// When data is nullptr, fills with default tables:
+// When data is nullptr, fills with the default table:
// _edgeTable[a*256+b] = min(a,b) (symmetric identity blend)
-// _edgeTableAlt[a*256+b] = special blend for hi-res mode
// When data is non-null, loads the primary table from data+8 (upper triangle, symmetric).
//
void InsaneRebel2::initEdgeTable(const byte *data) {
@@ -1040,27 +1029,12 @@ void InsaneRebel2::initEdgeTable(const byte *data) {
// Primary table: table[a][b] = a (i.e. min(a,b) since b >= a)
_edgeTable[a + b * 256] = (byte)a;
_edgeTable[b + a * 256] = (byte)a;
-
- // Secondary table: special blend rules (FUN_410510 lines 17-31)
- if (a < 0x10 || b > 0x4f) {
- // Outside blend range: use b if b==0, or (0xf < b && b < 0x50), or b==4
- if (b == 0 || (b > 0xf && b < 0x50) || b == 4) {
- _edgeTableAlt[a + b * 256] = (byte)b;
- } else {
- _edgeTableAlt[a + b * 256] = (byte)a;
- }
- } else {
- // Blend range [0x10..0x4f]: average of a and b
- _edgeTableAlt[a + b * 256] = (byte)((a + b) / 2);
- }
- _edgeTableAlt[b + a * 256] = _edgeTableAlt[a + b * 256];
}
}
// Special entries (FUN_410510 lines 33-36)
_edgeTable[0x42 * 256 + 0xf1] = 0x42; // DAT_00447ff1
_edgeTable[0x42 + 0xf0 * 256] = 0x42; // DAT_004480f0 (symmetric)
_edgeTable[0x41 * 256 + 0xb0] = 0x41; // DAT_00447fb0
- _edgeTableAlt[0x41 * 256 + 0xf0] = 0x41; // DAT_00443ff0
} else {
// Load table from IACT data (FUN_410510 non-NULL path, lines 39-47)
// Data format: 8-byte header + upper triangle of 256x256 symmetric table
Commit: 14f6c845da9687af84e8fa71ffd59feac957db7c
https://github.com/scummvm/scummvm/commit/14f6c845da9687af84e8fa71ffd59feac957db7c
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:56+02:00
Commit Message:
SCUMM: RA2: Use playLevelSegment in runlevels
Changed paths:
engines/scumm/insane/rebel2/rebel.h
engines/scumm/insane/rebel2/runlevels.cpp
diff --git a/engines/scumm/insane/rebel2/rebel.h b/engines/scumm/insane/rebel2/rebel.h
index 9325a802fe5..4beb48494ac 100644
--- a/engines/scumm/insane/rebel2/rebel.h
+++ b/engines/scumm/insane/rebel2/rebel.h
@@ -399,6 +399,13 @@ public:
// flags: bit 1 = add random unkilled types, bit 0 = limit credits to 2 (else 8)
uint16 processWaveEnd(int16 mask, int16 *budget, int16 threshold, uint16 flags);
+ // Play a raw SAN segment from a scripted level handler.
+ // Retail reaches these call sites through different wrappers/direct paths; this
+ // only collapses ScummVM's shared dispatch step. Callers still choose the original
+ // flags and when to call processWaveEnd(). recordFrame preserves the original
+ // split between gameplay/wave calls and transition/init-only segments.
+ bool playLevelSegment(const char *filename, uint16 flags, bool recordFrame = true);
+
// Random number helper (emulates FUN_004233a0)
int getRandomVariant(int max);
diff --git a/engines/scumm/insane/rebel2/runlevels.cpp b/engines/scumm/insane/rebel2/runlevels.cpp
index bb32a844149..ac2c2ccefa2 100644
--- a/engines/scumm/insane/rebel2/runlevels.cpp
+++ b/engines/scumm/insane/rebel2/runlevels.cpp
@@ -35,8 +35,6 @@ namespace Scumm {
// ---------------------------------------------------------------------------
int InsaneRebel2::runLevel1() {
- SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
-
// Play level beginning cinematic (01BEG.SAN)
playLevelBegin(1);
if (_vm->shouldQuit())
@@ -54,13 +52,7 @@ int InsaneRebel2::runLevel1() {
clearBit(0);
// Play gameplay (01P01.SAN with 0x28 flags)
- splayer->setCurVideoFlags(0x28);
- splayer->play("LEV01/01P01.SAN", 12);
-
- // Store death frame for video selection
- _deathFrame = splayer->_frame;
-
- if (_vm->shouldQuit())
+ if (!playLevelSegment("LEV01/01P01.SAN", 0x28))
return kLevelQuit;
if (_playerShield > 0) {
@@ -197,6 +189,15 @@ uint16 InsaneRebel2::processWaveEnd(int16 mask, int16 *budget, int16 threshold,
return result;
}
+bool InsaneRebel2::playLevelSegment(const char *filename, uint16 flags, bool recordFrame) {
+ SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
+ splayer->setCurVideoFlags(flags);
+ splayer->play(filename, 12);
+ if (recordFrame)
+ _deathFrame = splayer->_frame;
+ return !_vm->shouldQuit();
+}
+
// ---------------------------------------------------------------------------
// Level 2 Handler - FUN_00418063
// Multiple parts with P1/P2/P3 subdirectories
@@ -217,7 +218,6 @@ int InsaneRebel2::runLevel2() {
// wave progression. Using calibrated defaults until exact table values extracted.
const int16 kLevel2BudgetBase[3] = { 3, 3, 3 }; // Phase 1, 2, 3
- SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
int bonusCount = 0; // local_1c: tracks bonus events (DAT_0047ab9c & 0x10)
int totalKills = 0; // local_c: accumulated kill count across phases
int totalMisses = 0; // Accumulated misses (sVar1 + sVar2 from hit counters)
@@ -273,11 +273,7 @@ int InsaneRebel2::runLevel2() {
// Play A.SAN (background loader) â flags 0x28 (preserve buffer, gameplay mode)
debug("Rebel2: Level 2 Phase 1 - playing 02P01_A.SAN (background) budget=%d", budget);
- splayer->setCurVideoFlags(0x28);
- splayer->play("LEV02/P1/02P01_A.SAN", 12);
- _deathFrame = splayer->_frame;
-
- if (_vm->shouldQuit())
+ if (!playLevelSegment("LEV02/P1/02P01_A.SAN", 0x28))
return kLevelQuit;
// processWaveEnd after A.SAN (threshold=0, no early exit for background loader)
@@ -298,9 +294,8 @@ int InsaneRebel2::runLevel2() {
};
debug("Rebel2: Phase 1 wave - playing %s (state=0x%x budget=%d)", variants[variant], _rebelPhaseState, budget);
// Wave videos use flags 0x428 (original: FUN_0041f4d0 param_2=0x428)
- splayer->setCurVideoFlags(0x428);
- splayer->play(variants[variant], 12);
- _deathFrame = splayer->_frame;
+ if (!playLevelSegment(variants[variant], 0x428))
+ return kLevelQuit;
// processWaveEnd with threshold=0x14 (20) â enables early exit when enemies killed
processWaveEnd(0x36, &budget, 0x14, 0);
@@ -322,9 +317,7 @@ int InsaneRebel2::runLevel2() {
// Reset handler to 0 so procPostRendering skips HUD/sprite drawing during cinematic
_rebelHandler = 0;
_rebelStatusBarSprite = 0;
- splayer->setCurVideoFlags(0x28);
- splayer->play("LEV02/02PST1.SAN", 12);
- if (_vm->shouldQuit())
+ if (!playLevelSegment("LEV02/02PST1.SAN", 0x28, false))
return kLevelQuit;
totalKills += _rebelKillCounter;
@@ -346,11 +339,7 @@ int InsaneRebel2::runLevel2() {
// Play A.SAN (background loader)
debug("Rebel2: Level 2 Phase 2 - playing 02P02_A.SAN (background) budget=%d", budget);
- splayer->setCurVideoFlags(0x28);
- splayer->play("LEV02/P2/02P02_A.SAN", 12);
- _deathFrame = splayer->_frame;
-
- if (_vm->shouldQuit())
+ if (!playLevelSegment("LEV02/P2/02P02_A.SAN", 0x28))
return kLevelQuit;
// Phase 2 wave loop: processWaveEnd at TOP of loop (matches assembly structure)
@@ -386,9 +375,8 @@ int InsaneRebel2::runLevel2() {
}
debug("Rebel2: Phase 2 wave - playing %s (state=0x%x sel=0x%x budget=%d)", filename, _rebelPhaseState, waveSelect, budget);
- splayer->setCurVideoFlags(0x428);
- splayer->play(filename, 12);
- _deathFrame = splayer->_frame;
+ if (!playLevelSegment(filename, 0x428))
+ return kLevelQuit;
}
if ((_rebelPhaseState & 0x10) != 0)
@@ -404,9 +392,7 @@ int InsaneRebel2::runLevel2() {
// Reset handler to 0 so procPostRendering skips HUD/sprite drawing during cinematic
_rebelHandler = 0;
_rebelStatusBarSprite = 0;
- splayer->setCurVideoFlags(0x28);
- splayer->play("LEV02/02PST2.SAN", 12);
- if (_vm->shouldQuit())
+ if (!playLevelSegment("LEV02/02PST2.SAN", 0x28, false))
return kLevelQuit;
totalKills += _rebelKillCounter;
@@ -429,11 +415,7 @@ int InsaneRebel2::runLevel2() {
// Play A.SAN (background loader)
debug("Rebel2: Level 2 Phase 3 - playing 02P03_A.SAN (background) budget=%d", budget);
- splayer->setCurVideoFlags(0x28);
- splayer->play("LEV02/P3/02P03_A.SAN", 12);
- _deathFrame = splayer->_frame;
-
- if (_vm->shouldQuit())
+ if (!playLevelSegment("LEV02/P3/02P03_A.SAN", 0x28))
return kLevelQuit;
// Phase 3: processWaveEnd at BOTTOM (like Phase 1), waveSelect carried across iterations
@@ -476,9 +458,8 @@ int InsaneRebel2::runLevel2() {
}
debug("Rebel2: Phase 3 wave - playing %s (state=0x%x sel=0x%x budget=%d)", filename, _rebelPhaseState, waveSelect, budget);
- splayer->setCurVideoFlags(0x428);
- splayer->play(filename, 12);
- _deathFrame = splayer->_frame;
+ if (!playLevelSegment(filename, 0x428))
+ return kLevelQuit;
// processWaveEnd at BOTTOM with threshold=0x14
waveSelect = processWaveEnd(0x3e, &budget, 0x14, 0);
@@ -546,7 +527,6 @@ int InsaneRebel2::runLevel2() {
// ---------------------------------------------------------------------------
int InsaneRebel2::runLevel3() {
- SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
int phase1Score = 0; // Score preserved across phase 2 retries
// Play level beginning cinematic (03BEG.SAN)
@@ -565,11 +545,7 @@ int InsaneRebel2::runLevel3() {
// Play phase 1 gameplay (03PLAY1.SAN)
debug("Rebel2: Level 3 Phase 1");
- splayer->setCurVideoFlags(0x28);
- splayer->play("LEV03/03PLAY1.SAN", 12);
- _deathFrame = splayer->_frame;
-
- if (_vm->shouldQuit())
+ if (!playLevelSegment("LEV03/03PLAY1.SAN", 0x28))
return kLevelQuit;
if (_playerShield > 0) {
@@ -604,9 +580,7 @@ int InsaneRebel2::runLevel3() {
// Reset handler to 0 so procPostRendering skips HUD/sprite drawing during cinematic
_rebelHandler = 0;
_rebelStatusBarSprite = 0;
- splayer->setCurVideoFlags(0x28);
- splayer->play("LEV03/03POST1.SAN", 12);
- if (_vm->shouldQuit())
+ if (!playLevelSegment("LEV03/03POST1.SAN", 0x28, false))
return kLevelQuit;
// ----- PHASE 2 retry loop (preserves phase 1 score) -----
@@ -622,11 +596,7 @@ int InsaneRebel2::runLevel3() {
// Play phase 2 gameplay (03PLAY2.SAN)
debug("Rebel2: Level 3 Phase 2");
- splayer->setCurVideoFlags(0x28);
- splayer->play("LEV03/03PLAY2.SAN", 12);
- _deathFrame = splayer->_frame;
-
- if (_vm->shouldQuit())
+ if (!playLevelSegment("LEV03/03PLAY2.SAN", 0x28))
return kLevelQuit;
if (_playerShield > 0) {
@@ -665,13 +635,9 @@ int InsaneRebel2::runLevel3() {
// ---------------------------------------------------------------------------
int InsaneRebel2::runLevel4() {
- SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
-
// Play cutscene (04CUT.SAN)
// Original: FUN_00417168 adds | 8, so flags = 0x20 | 0x08 = 0x28
- splayer->setCurVideoFlags(0x28);
- splayer->play("LEV04/04CUT.SAN", 12);
- if (_vm->shouldQuit())
+ if (!playLevelSegment("LEV04/04CUT.SAN", 0x28, false))
return kLevelQuit;
// Play level beginning cinematic (04BEG.SAN)
@@ -690,11 +656,7 @@ int InsaneRebel2::runLevel4() {
// Play gameplay (04PLAY.SAN)
debug("Rebel2: Level 4 gameplay");
- splayer->setCurVideoFlags(0x28);
- splayer->play("LEV04/04PLAY.SAN", 12);
- _deathFrame = splayer->_frame;
-
- if (_vm->shouldQuit())
+ if (!playLevelSegment("LEV04/04PLAY.SAN", 0x28))
return kLevelQuit;
if (_playerShield > 0) {
@@ -732,8 +694,6 @@ int InsaneRebel2::runLevel4() {
// ---------------------------------------------------------------------------
int InsaneRebel2::runLevel5() {
- SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
-
// Play level beginning cinematic (05BEG.SAN)
playLevelBegin(5);
if (_vm->shouldQuit())
@@ -750,11 +710,7 @@ int InsaneRebel2::runLevel5() {
// Play gameplay (05PLAY.SAN)
debug("Rebel2: Level 5 gameplay");
- splayer->setCurVideoFlags(0x28);
- splayer->play("LEV05/05PLAY.SAN", 12);
- _deathFrame = splayer->_frame;
-
- if (_vm->shouldQuit())
+ if (!playLevelSegment("LEV05/05PLAY.SAN", 0x28))
return kLevelQuit;
if (_playerShield > 0) {
@@ -798,7 +754,6 @@ int InsaneRebel2::runLevel6() {
// Original structure: outer do-while for phase 1 retries, inner while(true) for
// phase 2 retries + death handling. Phase 1 death breaks inner â RETRY at outer bottom.
// Phase 2 death â RETRYB â re-enters phase 2 within inner loop.
- SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
int phase1Score = 0;
// Play level beginning cinematic (06BEG.SAN)
@@ -822,14 +777,10 @@ int InsaneRebel2::runLevel6() {
_currentPhase = 1;
debug("Rebel2: Level 6 Phase 1");
- splayer->setCurVideoFlags(0x28);
- splayer->play("LEV06/06PLAY1.SAN", 12);
+ if (!playLevelSegment("LEV06/06PLAY1.SAN", 0x28))
+ return kLevelQuit;
// TODO: Mid-level switch at frame 0x2a8 to 06PLAY1B.SAN (flags 0x468)
// + score checkpoint (FUN_00407f55) â needs per-frame callback
- _deathFrame = splayer->_frame;
-
- if (_vm->shouldQuit())
- return kLevelQuit;
if (_playerShield <= 0) {
// Died in phase 1
@@ -856,9 +807,7 @@ int InsaneRebel2::runLevel6() {
_rebelHandler = 0;
_rebelStatusBarSprite = 0;
- splayer->setCurVideoFlags(0x28);
- splayer->play("LEV06/06POST1.SAN", 12);
- if (_vm->shouldQuit())
+ if (!playLevelSegment("LEV06/06POST1.SAN", 0x28, false))
return kLevelQuit;
// ----- PHASE 2 retry loop (inner while(true) in original) -----
@@ -869,11 +818,7 @@ int InsaneRebel2::runLevel6() {
clearBit(0); // FUN_00407d10
debug("Rebel2: Level 6 Phase 2");
- splayer->setCurVideoFlags(0x28);
- splayer->play("LEV06/06PLAY2.SAN", 12);
- _deathFrame = splayer->_frame;
-
- if (_vm->shouldQuit())
+ if (!playLevelSegment("LEV06/06PLAY2.SAN", 0x28))
return kLevelQuit;
if (_playerShield > 0) {
@@ -916,7 +861,6 @@ int InsaneRebel2::runLevel6() {
// ---------------------------------------------------------------------------
int InsaneRebel2::runLevel7() {
- SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
bool reachedFork = false; // DAT_0047ab8c equivalent â tracks if 07PLAYB was played
// Play cutscene (07CUT.SAN)
@@ -946,11 +890,7 @@ int InsaneRebel2::runLevel7() {
// TODO: Mid-level fork at frame 1592 requires per-frame callback.
// The fork video (07PLAYB) should be triggered by IACT callbacks setting
// a state flag during gameplay.
- splayer->setCurVideoFlags(0x28);
- splayer->play("LEV07/07PLAY.SAN", 12);
- _deathFrame = splayer->_frame;
-
- if (_vm->shouldQuit())
+ if (!playLevelSegment("LEV07/07PLAY.SAN", 0x28))
return kLevelQuit;
if (_playerShield > 0) {
@@ -993,8 +933,6 @@ int InsaneRebel2::runLevel7() {
// ---------------------------------------------------------------------------
int InsaneRebel2::runLevel8() {
- SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
-
// No cutscene â starts directly with BEG
// Original: FUN_004171c5("08BEG.SAN", 0x20, 0xb1, 0xa0, 10, 5, 0x4b)
playLevelBegin(8);
@@ -1013,11 +951,7 @@ int InsaneRebel2::runLevel8() {
// Play gameplay (08PLAY.SAN)
// Original: FUN_0041f4d0("08PLAY.SAN", 8, -1, -1, 0) â note flags=0x08
- splayer->setCurVideoFlags(0x08);
- splayer->play("LEV08/08PLAY.SAN", 12);
- _deathFrame = splayer->_frame;
-
- if (_vm->shouldQuit())
+ if (!playLevelSegment("LEV08/08PLAY.SAN", 0x08))
return kLevelQuit;
if (_playerShield > 0) {
@@ -1061,8 +995,6 @@ int InsaneRebel2::runLevel8() {
// ---------------------------------------------------------------------------
int InsaneRebel2::runLevel9() {
- SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
-
// No cutscene â starts directly with BEG
// Original: FUN_004171c5("09BEG.SAN", 0x20, 0xb2, 0xa0, 10, 200, 0x10e)
playLevelBegin(9);
@@ -1086,11 +1018,7 @@ int InsaneRebel2::runLevel9() {
// Original: FUN_0041f4d0("09PLAY.SAN", 0x28, -1, -1, 0)
// Mid-events at frames 415 and 850: FUN_00407f55 (score save)
// These are handled implicitly â the IACT callbacks manage scoring.
- splayer->setCurVideoFlags(0x28);
- splayer->play("LEV09/09PLAY.SAN", 12);
- _deathFrame = splayer->_frame;
-
- if (_vm->shouldQuit())
+ if (!playLevelSegment("LEV09/09PLAY.SAN", 0x28))
return kLevelQuit;
if (_playerShield > 0) {
@@ -1133,8 +1061,6 @@ int InsaneRebel2::runLevel9() {
// ---------------------------------------------------------------------------
int InsaneRebel2::runLevel10() {
- SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
-
// Play cutscene (10CUT.SAN)
playCinematic("LEV10/10CUT.SAN");
if (_vm->shouldQuit())
@@ -1158,11 +1084,7 @@ int InsaneRebel2::runLevel10() {
// Play gameplay (10PLAY.SAN)
// Original: FUN_0041f4d0("10PLAY.SAN", 0x28, -1, -1, 0)
- splayer->setCurVideoFlags(0x28);
- splayer->play("LEV10/10PLAY.SAN", 12);
- _deathFrame = splayer->_frame;
-
- if (_vm->shouldQuit())
+ if (!playLevelSegment("LEV10/10PLAY.SAN", 0x28))
return kLevelQuit;
if (_playerShield > 0) {
@@ -1209,7 +1131,6 @@ int InsaneRebel2::runLevel10() {
// ---------------------------------------------------------------------------
int InsaneRebel2::runLevel11() {
- SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
int totalKills = 0;
int totalMisses = 0;
int prevPhaseState = 0;
@@ -1262,11 +1183,7 @@ int InsaneRebel2::runLevel11() {
// Play A.SAN (background loader)
debug("Rebel2: Level 11 Phase 1 - playing 11P01_A.SAN budget=%d", budget);
- splayer->setCurVideoFlags(0x28);
- splayer->play("LEV11/P1/11P01_A.SAN", 12);
- _deathFrame = splayer->_frame;
-
- if (_vm->shouldQuit())
+ if (!playLevelSegment("LEV11/P1/11P01_A.SAN", 0x28))
return kLevelQuit;
{
@@ -1294,9 +1211,8 @@ int InsaneRebel2::runLevel11() {
}
debug("Rebel2: Level 11 Phase 1 wave - %s (state=0x%x sel=%d)", filename, _rebelPhaseState, sel);
- splayer->setCurVideoFlags(0x428);
- splayer->play(filename, 12);
- _deathFrame = splayer->_frame;
+ if (!playLevelSegment(filename, 0x428))
+ return kLevelQuit;
waveSelect = processWaveEnd(0x0e, &budget, 0x14, 0);
}
@@ -1310,9 +1226,7 @@ int InsaneRebel2::runLevel11() {
// Post segment 1 (11POST1.SAN)
_rebelHandler = 0;
_rebelStatusBarSprite = 0;
- splayer->setCurVideoFlags(0x28);
- splayer->play("LEV11/11POST1.SAN", 12);
- if (_vm->shouldQuit())
+ if (!playLevelSegment("LEV11/11POST1.SAN", 0x28, false))
return kLevelQuit;
totalKills += _rebelKillCounter;
@@ -1331,11 +1245,7 @@ int InsaneRebel2::runLevel11() {
// Play A.SAN (background loader)
debug("Rebel2: Level 11 Phase 2 - playing 11P02_A.SAN budget=%d", budget);
- splayer->setCurVideoFlags(0x28);
- splayer->play("LEV11/P2/11P02_A.SAN", 12);
- _deathFrame = splayer->_frame;
-
- if (_vm->shouldQuit())
+ if (!playLevelSegment("LEV11/P2/11P02_A.SAN", 0x28))
return kLevelQuit;
{
@@ -1357,9 +1267,8 @@ int InsaneRebel2::runLevel11() {
}
debug("Rebel2: Level 11 Phase 2 wave - %s (state=0x%x)", filename, _rebelPhaseState);
- splayer->setCurVideoFlags(0x428);
- splayer->play(filename, 12);
- _deathFrame = splayer->_frame;
+ if (!playLevelSegment(filename, 0x428))
+ return kLevelQuit;
waveSelect = processWaveEnd(0x0e, &budget, 0x14, 3);
}
@@ -1373,9 +1282,7 @@ int InsaneRebel2::runLevel11() {
// Post segment 2 (11POST2.SAN)
_rebelHandler = 0;
_rebelStatusBarSprite = 0;
- splayer->setCurVideoFlags(0x28);
- splayer->play("LEV11/11POST2.SAN", 12);
- if (_vm->shouldQuit())
+ if (!playLevelSegment("LEV11/11POST2.SAN", 0x28, false))
return kLevelQuit;
totalKills += _rebelKillCounter;
@@ -1395,11 +1302,7 @@ int InsaneRebel2::runLevel11() {
_rebelHandler = 8;
debug("Rebel2: Level 11 Phase 3 first half - playing 11P03_A.SAN budget=%d", budget);
- splayer->setCurVideoFlags(0x28);
- splayer->play("LEV11/P3/11P03_A.SAN", 12);
- _deathFrame = splayer->_frame;
-
- if (_vm->shouldQuit())
+ if (!playLevelSegment("LEV11/P3/11P03_A.SAN", 0x28))
return kLevelQuit;
{
@@ -1437,9 +1340,8 @@ int InsaneRebel2::runLevel11() {
}
debug("Rebel2: Level 11 Phase 3a wave - %s (state=0x%x variantIdx=%d)", filename, _rebelPhaseState, variantIdx);
- splayer->setCurVideoFlags(0x428);
- splayer->play(filename, 12);
- _deathFrame = splayer->_frame;
+ if (!playLevelSegment(filename, 0x428))
+ return kLevelQuit;
// Threshold only for higher variants (original: (2 < variantIdx) - 1 & 0x14)
int16 threshold = (variantIdx > 2) ? 0x14 : 0;
@@ -1486,11 +1388,7 @@ int InsaneRebel2::runLevel11() {
// Play G.SAN (background loader for second half)
debug("Rebel2: Level 11 Phase 3 second half - playing 11P03_G.SAN budget=%d", budget);
- splayer->setCurVideoFlags(0x28);
- splayer->play("LEV11/P3/11P03_G.SAN", 12);
- _deathFrame = splayer->_frame;
-
- if (_vm->shouldQuit())
+ if (!playLevelSegment("LEV11/P3/11P03_G.SAN", 0x28))
return kLevelQuit;
// Only enter wave loop if not all basic types killed already
@@ -1523,9 +1421,8 @@ int InsaneRebel2::runLevel11() {
}
debug("Rebel2: Level 11 Phase 3b wave - %s (state=0x%x variantIdx=%d)", filename, _rebelPhaseState, variantIdx);
- splayer->setCurVideoFlags(0x428);
- splayer->play(filename, 12);
- _deathFrame = splayer->_frame;
+ if (!playLevelSegment(filename, 0x428))
+ return kLevelQuit;
int16 threshold = (variantIdx > 2) ? 0x14 : 0;
waveSelect = processWaveEnd(0x0e, &budget, threshold, 0);
@@ -1601,8 +1498,6 @@ int InsaneRebel2::runLevel11() {
// ---------------------------------------------------------------------------
int InsaneRebel2::runLevel12() {
- SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
-
// Kill credit budget bases per phase
const int16 kLevel12BudgetBase[4] = { 3, 4, 4, 4 };
@@ -1648,17 +1543,12 @@ int InsaneRebel2::runLevel12() {
// Initialization video (12P05.SAN)
debug("Rebel2: Level 12 Phase 1 - init 12P05.SAN budget=%d", budget);
- splayer->setCurVideoFlags(0x28);
- splayer->play("LEV12/12P05.SAN", 12);
- if (_vm->shouldQuit())
+ if (!playLevelSegment("LEV12/12P05.SAN", 0x28, false))
return kLevelQuit;
processWaveEnd(1, &budget, 0, 0);
// First wave (P1/12P01_A.SAN)
- splayer->setCurVideoFlags(0x428);
- splayer->play("LEV12/P1/12P01_A.SAN", 12);
- _deathFrame = splayer->_frame;
- if (_vm->shouldQuit())
+ if (!playLevelSegment("LEV12/P1/12P01_A.SAN", 0x428))
return kLevelQuit;
{
@@ -1679,9 +1569,8 @@ int InsaneRebel2::runLevel12() {
}
debug("Rebel2: Level 12 Phase 1 wave - %s (state=0x%x sel=%d)", filename, _rebelPhaseState, sel);
- splayer->setCurVideoFlags(0x428);
- splayer->play(filename, 12);
- _deathFrame = splayer->_frame;
+ if (!playLevelSegment(filename, 0x428))
+ return kLevelQuit;
waveSelect = processWaveEnd(6, &budget, 0x14, 0);
}
@@ -1701,17 +1590,12 @@ int InsaneRebel2::runLevel12() {
// Initialization video (12P06.SAN)
debug("Rebel2: Level 12 Phase 2 - init 12P06.SAN budget=%d", budget);
- splayer->setCurVideoFlags(0x428);
- splayer->play("LEV12/12P06.SAN", 12);
- if (_vm->shouldQuit())
+ if (!playLevelSegment("LEV12/12P06.SAN", 0x428, false))
return kLevelQuit;
processWaveEnd(1, &budget, 0, 0);
// First wave (P2/12P02_A.SAN)
- splayer->setCurVideoFlags(0x428);
- splayer->play("LEV12/P2/12P02_A.SAN", 12);
- _deathFrame = splayer->_frame;
- if (_vm->shouldQuit())
+ if (!playLevelSegment("LEV12/P2/12P02_A.SAN", 0x428))
return kLevelQuit;
{
@@ -1740,9 +1624,8 @@ int InsaneRebel2::runLevel12() {
}
debug("Rebel2: Level 12 Phase 2 wave - %s (state=0x%x variantIdx=%d)", filename, _rebelPhaseState, variantIdx);
- splayer->setCurVideoFlags(0x428);
- splayer->play(filename, 12);
- _deathFrame = splayer->_frame;
+ if (!playLevelSegment(filename, 0x428))
+ return kLevelQuit;
// Variants E(2) and F(5) reset threshold to 0
int16 threshold = (variantIdx == 2 || variantIdx == 5) ? 0 : 0x14;
@@ -1764,17 +1647,12 @@ int InsaneRebel2::runLevel12() {
// Initialization video (12P07.SAN)
debug("Rebel2: Level 12 Phase 3 - init 12P07.SAN budget=%d", budget);
- splayer->setCurVideoFlags(0x428);
- splayer->play("LEV12/12P07.SAN", 12);
- if (_vm->shouldQuit())
+ if (!playLevelSegment("LEV12/12P07.SAN", 0x428, false))
return kLevelQuit;
processWaveEnd(1, &budget, 0, 0);
// First wave (P3/12P03_A.SAN)
- splayer->setCurVideoFlags(0x428);
- splayer->play("LEV12/P3/12P03_A.SAN", 12);
- _deathFrame = splayer->_frame;
- if (_vm->shouldQuit())
+ if (!playLevelSegment("LEV12/P3/12P03_A.SAN", 0x428))
return kLevelQuit;
{
@@ -1803,9 +1681,8 @@ int InsaneRebel2::runLevel12() {
}
debug("Rebel2: Level 12 Phase 3 wave - %s (state=0x%x variantIdx=%d)", filename, _rebelPhaseState, variantIdx);
- splayer->setCurVideoFlags(0x428);
- splayer->play(filename, 12);
- _deathFrame = splayer->_frame;
+ if (!playLevelSegment(filename, 0x428))
+ return kLevelQuit;
waveSelect = processWaveEnd(6, &budget, 0x14, 0);
}
@@ -1825,17 +1702,12 @@ int InsaneRebel2::runLevel12() {
// Initialization video (12P08.SAN)
debug("Rebel2: Level 12 Phase 4 - init 12P08.SAN budget=%d", budget);
- splayer->setCurVideoFlags(0x428);
- splayer->play("LEV12/12P08.SAN", 12);
- if (_vm->shouldQuit())
+ if (!playLevelSegment("LEV12/12P08.SAN", 0x428, false))
return kLevelQuit;
processWaveEnd(1, &budget, 0, 0);
// First wave (P4/12P04_A.SAN)
- splayer->setCurVideoFlags(0x428);
- splayer->play("LEV12/P4/12P04_A.SAN", 12);
- _deathFrame = splayer->_frame;
- if (_vm->shouldQuit())
+ if (!playLevelSegment("LEV12/P4/12P04_A.SAN", 0x428))
return kLevelQuit;
{
@@ -1863,9 +1735,8 @@ int InsaneRebel2::runLevel12() {
}
debug("Rebel2: Level 12 Phase 4 wave - %s (state=0x%x variantIdx=%d)", filename, _rebelPhaseState, variantIdx);
- splayer->setCurVideoFlags(0x428);
- splayer->play(filename, 12);
- _deathFrame = splayer->_frame;
+ if (!playLevelSegment(filename, 0x428))
+ return kLevelQuit;
waveSelect = processWaveEnd(0x0e, &budget, 0x14, 0);
}
@@ -1877,9 +1748,7 @@ int InsaneRebel2::runLevel12() {
return kLevelQuit;
// ----- CLOSING: 12P09.SAN -----
- splayer->setCurVideoFlags(0x428);
- splayer->play("LEV12/12P09.SAN", 12);
- if (_vm->shouldQuit())
+ if (!playLevelSegment("LEV12/12P09.SAN", 0x428, false))
return kLevelQuit;
processWaveEnd(1, &budget, 0, 0);
@@ -1933,8 +1802,6 @@ int InsaneRebel2::runLevel12() {
// ---------------------------------------------------------------------------
int InsaneRebel2::runLevel13() {
- SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
-
// No cutscene â starts directly with BEG
// Original: FUN_004171c5("13BEG.SAN", 0x20, 0xb6, 0xa0, 10, 2, 0x46)
playLevelBegin(13);
@@ -1956,11 +1823,7 @@ int InsaneRebel2::runLevel13() {
// First inner loop runs until frame reaches maxFrame-10
// Then Phase B (13PLAY_B.SAN, flags 0x468) plays at that exact frame
// The 0x468 flags indicate seamless mid-video transition.
- splayer->setCurVideoFlags(0x28);
- splayer->play("LEV13/13PLAY_A.SAN", 12);
- _deathFrame = splayer->_frame;
-
- if (_vm->shouldQuit())
+ if (!playLevelSegment("LEV13/13PLAY_A.SAN", 0x28))
return kLevelQuit;
// If alive after Phase A, play Phase B (reactor destruction loop)
@@ -1969,14 +1832,10 @@ int InsaneRebel2::runLevel13() {
// Play B as a sequential video. The IACT callbacks manage the reactor
// target state through opcode interactions.
if (_playerShield > 0) {
- splayer->setCurVideoFlags(0x468);
- splayer->play("LEV13/13PLAY_B.SAN", 12);
- _deathFrame = splayer->_frame;
+ if (!playLevelSegment("LEV13/13PLAY_B.SAN", 0x468))
+ return kLevelQuit;
}
- if (_vm->shouldQuit())
- return kLevelQuit;
-
if (_playerShield > 0) {
int accuracy = 0;
if (_rebelKillCounter > 0) {
@@ -2015,8 +1874,6 @@ int InsaneRebel2::runLevel13() {
// ---------------------------------------------------------------------------
int InsaneRebel2::runLevel14() {
- SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
-
// No cutscene â starts directly with BEG
// Original: FUN_004171c5("14BEG.SAN", 0x20, 0xb7, 0xa0, 10, 2, 0x46)
playLevelBegin(14);
@@ -2035,11 +1892,7 @@ int InsaneRebel2::runLevel14() {
// Play gameplay (14PLAY.SAN)
// Original: FUN_0041f4d0("14PLAY.SAN", 0x28, -1, -1, 0)
- splayer->setCurVideoFlags(0x28);
- splayer->play("LEV14/14PLAY.SAN", 12);
- _deathFrame = splayer->_frame;
-
- if (_vm->shouldQuit())
+ if (!playLevelSegment("LEV14/14PLAY.SAN", 0x28))
return kLevelQuit;
if (_playerShield > 0) {
@@ -2083,8 +1936,6 @@ int InsaneRebel2::runLevel14() {
// ---------------------------------------------------------------------------
int InsaneRebel2::runLevel15() {
- SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
-
// Play cutscene (15CUT.SAN)
playCinematic("LEV15/15CUT.SAN");
if (_vm->shouldQuit())
@@ -2116,11 +1967,7 @@ int InsaneRebel2::runLevel15() {
// Play gameplay (15PLAY.SAN)
// Original: FUN_0041f4d0("15PLAY.SAN", 0x28, -1, -1, 0)
- splayer->setCurVideoFlags(0x28);
- splayer->play("LEV15/15PLAY.SAN", 12);
- _deathFrame = splayer->_frame;
-
- if (_vm->shouldQuit())
+ if (!playLevelSegment("LEV15/15PLAY.SAN", 0x28))
return kLevelQuit;
if (_playerShield > 0) {
Commit: 6fae78c49d0ca16743b9536a6dd7062c75a62dcf
https://github.com/scummvm/scummvm/commit/6fae78c49d0ca16743b9536a6dd7062c75a62dcf
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:56+02:00
Commit Message:
SCUMM: RA2: Use calculateAccuracy in runlevels
Changed paths:
engines/scumm/insane/rebel2/rebel.h
engines/scumm/insane/rebel2/runlevels.cpp
diff --git a/engines/scumm/insane/rebel2/rebel.h b/engines/scumm/insane/rebel2/rebel.h
index 4beb48494ac..62518613619 100644
--- a/engines/scumm/insane/rebel2/rebel.h
+++ b/engines/scumm/insane/rebel2/rebel.h
@@ -406,6 +406,8 @@ public:
// split between gameplay/wave calls and transition/init-only segments.
bool playLevelSegment(const char *filename, uint16 flags, bool recordFrame = true);
+ int calculateAccuracy(int kills, int misses) const;
+
// Random number helper (emulates FUN_004233a0)
int getRandomVariant(int max);
diff --git a/engines/scumm/insane/rebel2/runlevels.cpp b/engines/scumm/insane/rebel2/runlevels.cpp
index ac2c2ccefa2..dfd1ce18e85 100644
--- a/engines/scumm/insane/rebel2/runlevels.cpp
+++ b/engines/scumm/insane/rebel2/runlevels.cpp
@@ -198,6 +198,14 @@ bool InsaneRebel2::playLevelSegment(const char *filename, uint16 flags, bool rec
return !_vm->shouldQuit();
}
+int InsaneRebel2::calculateAccuracy(int kills, int misses) const {
+ const int totalShots = kills + misses;
+ if (kills <= 0 || totalShots <= 0)
+ return 0;
+
+ return (kills * 100) / totalShots;
+}
+
// ---------------------------------------------------------------------------
// Level 2 Handler - FUN_00418063
// Multiple parts with P1/P2/P3 subdirectories
@@ -481,11 +489,7 @@ int InsaneRebel2::runLevel2() {
// Score presentation remains to be implemented.
{
totalMisses += _rebelHitCounter;
- int accuracy = 0;
- int totalShots = totalKills + totalMisses;
- if (totalKills > 0 && totalShots > 0) {
- accuracy = (totalKills * 100) / totalShots;
- }
+ int accuracy = calculateAccuracy(totalKills, totalMisses);
debug("Rebel2: Level 2 completed! kills=%d misses=%d accuracy=%d%% bonus=%d",
totalKills, totalMisses, accuracy, bonusCount);
}
@@ -955,10 +959,7 @@ int InsaneRebel2::runLevel8() {
return kLevelQuit;
if (_playerShield > 0) {
- int accuracy = 0;
- if (_rebelKillCounter > 0) {
- accuracy = (_rebelKillCounter * 100) / (_rebelHitCounter + _rebelKillCounter);
- }
+ int accuracy = calculateAccuracy(_rebelKillCounter, _rebelHitCounter);
debug("Rebel2: Level 8 completed! accuracy=%d%%", accuracy);
playLevelEnd(8);
_levelUnlocked[8] = true;
@@ -1022,10 +1023,7 @@ int InsaneRebel2::runLevel9() {
return kLevelQuit;
if (_playerShield > 0) {
- int accuracy = 0;
- if (_rebelKillCounter > 0) {
- accuracy = (_rebelKillCounter * 100) / (_rebelHitCounter + _rebelKillCounter);
- }
+ int accuracy = calculateAccuracy(_rebelKillCounter, _rebelHitCounter);
debug("Rebel2: Level 9 completed! accuracy=%d%%", accuracy);
playLevelEnd(9);
_levelUnlocked[9] = true;
@@ -1088,10 +1086,7 @@ int InsaneRebel2::runLevel10() {
return kLevelQuit;
if (_playerShield > 0) {
- int accuracy = 0;
- if (_rebelKillCounter > 0) {
- accuracy = (_rebelKillCounter * 100) / (_rebelHitCounter + _rebelKillCounter);
- }
+ int accuracy = calculateAccuracy(_rebelKillCounter, _rebelHitCounter);
debug("Rebel2: Level 10 completed! accuracy=%d%%", accuracy);
playLevelEnd(10);
_levelUnlocked[10] = true;
@@ -1439,11 +1434,7 @@ int InsaneRebel2::runLevel11() {
// ----- LEVEL COMPLETED -----
{
totalMisses += _rebelHitCounter;
- int accuracy = 0;
- int totalShots = totalKills + totalMisses;
- if (totalKills > 0 && totalShots > 0) {
- accuracy = (totalKills * 100) / totalShots;
- }
+ int accuracy = calculateAccuracy(totalKills, totalMisses);
debug("Rebel2: Level 11 completed! kills=%d misses=%d accuracy=%d%%",
totalKills, totalMisses, accuracy);
}
@@ -1754,10 +1745,7 @@ int InsaneRebel2::runLevel12() {
// ----- LEVEL COMPLETED -----
{
- int accuracy = 0;
- if (_rebelKillCounter > 0) {
- accuracy = (_rebelKillCounter * 100) / (_rebelHitCounter + _rebelKillCounter);
- }
+ int accuracy = calculateAccuracy(_rebelKillCounter, _rebelHitCounter);
debug("Rebel2: Level 12 completed! kills=%d misses=%d accuracy=%d%%",
_rebelKillCounter, _rebelHitCounter, accuracy);
}
@@ -1837,10 +1825,7 @@ int InsaneRebel2::runLevel13() {
}
if (_playerShield > 0) {
- int accuracy = 0;
- if (_rebelKillCounter > 0) {
- accuracy = (_rebelKillCounter * 100) / (_rebelHitCounter + _rebelKillCounter);
- }
+ int accuracy = calculateAccuracy(_rebelKillCounter, _rebelHitCounter);
debug("Rebel2: Level 13 completed! accuracy=%d%%", accuracy);
playLevelEnd(13);
_levelUnlocked[13] = true;
@@ -1896,10 +1881,7 @@ int InsaneRebel2::runLevel14() {
return kLevelQuit;
if (_playerShield > 0) {
- int accuracy = 0;
- if (_rebelKillCounter > 0) {
- accuracy = (_rebelKillCounter * 100) / (_rebelHitCounter + _rebelKillCounter);
- }
+ int accuracy = calculateAccuracy(_rebelKillCounter, _rebelHitCounter);
debug("Rebel2: Level 14 completed! accuracy=%d%%", accuracy);
playLevelEnd(14);
_levelUnlocked[14] = true;
@@ -1971,10 +1953,7 @@ int InsaneRebel2::runLevel15() {
return kLevelQuit;
if (_playerShield > 0) {
- int accuracy = 0;
- if (_rebelKillCounter > 0) {
- accuracy = (_rebelKillCounter * 100) / (_rebelHitCounter + _rebelKillCounter);
- }
+ int accuracy = calculateAccuracy(_rebelKillCounter, _rebelHitCounter);
debug("Rebel2: Level 15 completed! accuracy=%d%%", accuracy);
playLevelEnd(15);
_levelUnlocked[15] = true;
Commit: de00bf9dd4afe466ae186a9deab1041e276dcfad
https://github.com/scummvm/scummvm/commit/de00bf9dd4afe466ae186a9deab1041e276dcfad
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:57+02:00
Commit Message:
SCUMM: RA2: Refactor explosion rendering
Changed paths:
engines/scumm/insane/rebel2/rebel.h
engines/scumm/insane/rebel2/render.cpp
diff --git a/engines/scumm/insane/rebel2/rebel.h b/engines/scumm/insane/rebel2/rebel.h
index 62518613619..0ed42c2b99d 100644
--- a/engines/scumm/insane/rebel2/rebel.h
+++ b/engines/scumm/insane/rebel2/rebel.h
@@ -702,6 +702,11 @@ public:
bool active;
};
+ enum ExplosionFrameAdvance {
+ kExplosionAdvanceAfterDraw,
+ kExplosionAdvanceBeforeDraw
+ };
+
Explosion _explosions[5];
void spawnExplosion(int x, int y, int objectHalfWidth);
@@ -906,6 +911,8 @@ public:
void spawnShot(int x, int y); // Dispatcher based on current handler
// Handler-specific explosion rendering
+ void renderExplosionFrame(byte *renderBitmap, int pitch, int width, int height,
+ Explosion &explosion, int screenX, int screenY, ExplosionFrameAdvance advance);
void renderTurretExplosions(byte *renderBitmap, int pitch, int width, int height); // FUN_409FBC (Handler 0x26)
void renderVehicleExplosions(byte *renderBitmap, int pitch, int width, int height); // FUN_402696 (Handler 8)
void renderSpaceExplosions(byte *renderBitmap, int pitch, int width, int height); // FUN_40F1C5 (Handler 7)
diff --git a/engines/scumm/insane/rebel2/render.cpp b/engines/scumm/insane/rebel2/render.cpp
index 08a0b731211..d0db992e1ff 100644
--- a/engines/scumm/insane/rebel2/render.cpp
+++ b/engines/scumm/insane/rebel2/render.cpp
@@ -3506,6 +3506,46 @@ void InsaneRebel2::renderExplosions(byte *renderBitmap, int pitch, int width, in
}
}
+// renderExplosionFrame -- Shared low-res explosion sprite path.
+// The original handlers reach this through separate retail functions. In the
+// 320x200 path used here, they share the same scale buckets and centered NUT
+// draw; callers keep their coordinate transforms and frame timing explicit.
+void InsaneRebel2::renderExplosionFrame(byte *renderBitmap, int pitch, int width, int height,
+ InsaneRebel2::Explosion &explosion, int screenX, int screenY, ExplosionFrameAdvance advance) {
+ if (!explosion.active)
+ return;
+
+ if (explosion.counter <= 0) {
+ explosion.active = false;
+ return;
+ }
+
+ if (advance == kExplosionAdvanceBeforeDraw)
+ explosion.counter--;
+
+ // Fixed low-res thresholds (0x0b=11, 0x15=21).
+ int baseIndex;
+ if (explosion.scale < 11) {
+ baseIndex = 9;
+ } else if (explosion.scale < 21) {
+ baseIndex = 19;
+ } else {
+ baseIndex = 29;
+ }
+
+ int spriteIndex = baseIndex + (12 - explosion.counter);
+
+ if (_smush_iconsNut->getNumChars() > spriteIndex) {
+ int ew = _smush_iconsNut->getCharWidth(spriteIndex);
+ int eh = _smush_iconsNut->getCharHeight(spriteIndex);
+ renderNutSprite(renderBitmap, pitch, width, height,
+ screenX - ew / 2, screenY - eh / 2, _smush_iconsNut, spriteIndex);
+ }
+
+ if (advance == kExplosionAdvanceAfterDraw)
+ explosion.counter--;
+}
+
// renderTurretExplosions -- Handler 0x26 turret explosion rendering (FUN_409FBC).
// Position: FUN_0041c720 3D->2D projection (identity at low-res).
// Scale thresholds: <11, <21. Secondary NUT: DAT_0047fe80 (if DAT_0047a7fc >= 0).
@@ -3517,36 +3557,12 @@ void InsaneRebel2::renderTurretExplosions(byte *renderBitmap, int pitch, int wid
if (!_explosions[i].active)
continue;
- if (_explosions[i].counter <= 0) {
- _explosions[i].active = false;
- continue;
- }
-
- // FUN_409FBC: Fixed thresholds (0x0b=11, 0x15=21)
- int baseIndex;
- if (_explosions[i].scale < 11) {
- baseIndex = 9; // Small (sprites 11-20)
- } else if (_explosions[i].scale < 21) {
- baseIndex = 19; // Medium (sprites 21-30)
- } else {
- baseIndex = 29; // Large (sprites 31-40)
- }
-
- int spriteIndex = baseIndex + (12 - _explosions[i].counter);
-
// Position: world coords passed through FUN_0041c720 (3Dâ2D projection).
// At 320x200 low-res turret view, projection is effectively identity.
int screenX = _explosions[i].x;
int screenY = _explosions[i].y;
-
- if (_smush_iconsNut->getNumChars() > spriteIndex) {
- int ew = _smush_iconsNut->getCharWidth(spriteIndex);
- int eh = _smush_iconsNut->getCharHeight(spriteIndex);
- renderNutSprite(renderBitmap, pitch, width, height,
- screenX - ew / 2, screenY - eh / 2, _smush_iconsNut, spriteIndex);
- }
-
- _explosions[i].counter--;
+ renderExplosionFrame(renderBitmap, pitch, width, height, _explosions[i],
+ screenX, screenY, kExplosionAdvanceAfterDraw);
}
}
@@ -3561,41 +3577,17 @@ void InsaneRebel2::renderVehicleExplosions(byte *renderBitmap, int pitch, int wi
if (!_explosions[i].active)
continue;
- if (_explosions[i].counter <= 0) {
- _explosions[i].active = false;
- continue;
- }
-
- // FUN_402696: Fixed thresholds (0x0b=11, 0x15=21)
- int baseIndex;
- if (_explosions[i].scale < 11) {
- baseIndex = 9;
- } else if (_explosions[i].scale < 21) {
- baseIndex = 19;
- } else {
- baseIndex = 29;
- }
-
- int spriteIndex = baseIndex + (12 - _explosions[i].counter);
-
// FUN_402696 line 22-23: screenX = worldX - DAT_0043e006, screenY = worldY - DAT_0043e008
int screenX = _explosions[i].x - _viewX;
int screenY = _explosions[i].y - _viewY;
-
- if (_smush_iconsNut->getNumChars() > spriteIndex) {
- int ew = _smush_iconsNut->getCharWidth(spriteIndex);
- int eh = _smush_iconsNut->getCharHeight(spriteIndex);
- renderNutSprite(renderBitmap, pitch, width, height,
- screenX - ew / 2, screenY - eh / 2, _smush_iconsNut, spriteIndex);
- }
-
- _explosions[i].counter--;
+ renderExplosionFrame(renderBitmap, pitch, width, height, _explosions[i],
+ screenX, screenY, kExplosionAdvanceAfterDraw);
}
}
// renderSpaceExplosions -- Handler 7 space explosion rendering (FUN_40F1C5).
// Position: FUN_0041c720 3D->2D projection.
-// Scale thresholds: resolution-dependent (low-res: <11/<21, hi-res: <21/<41).
+// Original scale thresholds are resolution-dependent; current low-res path uses <11/<21.
// Secondary NUT: DAT_0047ff00 (FLY004, if DAT_0047a7fc >= 0).
void InsaneRebel2::renderSpaceExplosions(byte *renderBitmap, int pitch, int width, int height) {
if (!_smush_iconsNut)
@@ -3606,39 +3598,12 @@ void InsaneRebel2::renderSpaceExplosions(byte *renderBitmap, int pitch, int widt
if (!_explosions[i].active)
continue;
- if (_explosions[i].counter <= 0) {
- _explosions[i].active = false;
- continue;
- }
-
- // FUN_40F1C5 lines 41-51: Resolution-dependent thresholds.
- // Low-res (DAT_0047a808 < 2): thresholds 20, 10
- // High-res: thresholds 40, 20
- // We run at low-res (320x200), so use 10/20 (same as fixed handlers).
- int baseIndex;
- if (_explosions[i].scale < 11) {
- baseIndex = 9;
- } else if (_explosions[i].scale < 21) {
- baseIndex = 19;
- } else {
- baseIndex = 29;
- }
-
- int spriteIndex = baseIndex + (12 - _explosions[i].counter);
-
// Position: world coords through FUN_0041c720 (3Dâ2D projection).
// At low-res, this is close to identity for the ship view.
int screenX = _explosions[i].x;
int screenY = _explosions[i].y;
-
- if (_smush_iconsNut->getNumChars() > spriteIndex) {
- int ew = _smush_iconsNut->getCharWidth(spriteIndex);
- int eh = _smush_iconsNut->getCharHeight(spriteIndex);
- renderNutSprite(renderBitmap, pitch, width, height,
- screenX - ew / 2, screenY - eh / 2, _smush_iconsNut, spriteIndex);
- }
-
- _explosions[i].counter--;
+ renderExplosionFrame(renderBitmap, pitch, width, height, _explosions[i],
+ screenX, screenY, kExplosionAdvanceAfterDraw);
}
// --- Part 2: Corridor/zone hit explosion (FUN_40F1C5 lines 61-85) ---
@@ -3697,7 +3662,7 @@ void InsaneRebel2::renderSpaceExplosions(byte *renderBitmap, int pitch, int widt
// renderHandler25Explosions -- Handler 25 FPS explosion rendering (FUN_41F29A).
// Position: world coords + view offset (DAT_0045790c/0e = _rebelViewOffsetX/Y).
-// Scale thresholds: resolution-dependent (same as Handler 7). No sound panning.
+// Original scale thresholds follow Handler 7; current low-res path uses <11/<21. No sound panning.
void InsaneRebel2::renderHandler25Explosions(byte *renderBitmap, int pitch, int width, int height) {
if (!_smush_iconsNut)
return;
@@ -3706,36 +3671,11 @@ void InsaneRebel2::renderHandler25Explosions(byte *renderBitmap, int pitch, int
if (!_explosions[i].active)
continue;
- if (_explosions[i].counter <= 0) {
- _explosions[i].active = false;
- continue;
- }
-
- // Match FUN_41F29A exactly: decrement first, then select frame.
- _explosions[i].counter--;
-
- // FUN_41F29A lines 27-37: Resolution-dependent thresholds (same as Handler 7).
- int baseIndex;
- if (_explosions[i].scale < 11) {
- baseIndex = 9;
- } else if (_explosions[i].scale < 21) {
- baseIndex = 19;
- } else {
- baseIndex = 29;
- }
-
- int spriteIndex = baseIndex + (12 - _explosions[i].counter);
-
// FUN_41F29A line 22-23: screenX = worldX + DAT_0045790c, screenY = worldY + DAT_0045790e
int screenX = _explosions[i].x + _rebelViewOffsetX;
int screenY = _explosions[i].y + _rebelViewOffsetY;
-
- if (_smush_iconsNut->getNumChars() > spriteIndex) {
- int ew = _smush_iconsNut->getCharWidth(spriteIndex);
- int eh = _smush_iconsNut->getCharHeight(spriteIndex);
- renderNutSprite(renderBitmap, pitch, width, height,
- screenX - ew / 2, screenY - eh / 2, _smush_iconsNut, spriteIndex);
- }
+ renderExplosionFrame(renderBitmap, pitch, width, height, _explosions[i],
+ screenX, screenY, kExplosionAdvanceBeforeDraw);
}
}
Commit: c6e257251eb84bf925afc0c080f9c4678cfe15e7
https://github.com/scummvm/scummvm/commit/c6e257251eb84bf925afc0c080f9c4678cfe15e7
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:57+02:00
Commit Message:
SCUMM: RA2: Remove remaining gotos
Changed paths:
engines/scumm/insane/rebel2/rebel.h
engines/scumm/insane/rebel2/runlevels.cpp
diff --git a/engines/scumm/insane/rebel2/rebel.h b/engines/scumm/insane/rebel2/rebel.h
index 0ed42c2b99d..812038eff10 100644
--- a/engines/scumm/insane/rebel2/rebel.h
+++ b/engines/scumm/insane/rebel2/rebel.h
@@ -408,6 +408,9 @@ public:
int calculateAccuracy(int kills, int misses) const;
+ // Play DIE/OVER/RETRY tail. Returns true when the caller should restart its retry loop.
+ bool handleLevelDeath(int levelId, int phase, const char *deathVideo, const char *retryVideo, int &levelResult);
+
// Random number helper (emulates FUN_004233a0)
int getRandomVariant(int max);
diff --git a/engines/scumm/insane/rebel2/runlevels.cpp b/engines/scumm/insane/rebel2/runlevels.cpp
index dfd1ce18e85..f7d7a8c0ff8 100644
--- a/engines/scumm/insane/rebel2/runlevels.cpp
+++ b/engines/scumm/insane/rebel2/runlevels.cpp
@@ -206,6 +206,32 @@ int InsaneRebel2::calculateAccuracy(int kills, int misses) const {
return (kills * 100) / totalShots;
}
+bool InsaneRebel2::handleLevelDeath(int levelId, int phase,
+ const char *deathVideo, const char *retryVideo, int &levelResult) {
+ debug("Rebel2: Level %d Phase %d death", levelId, phase);
+ playCinematic(deathVideo);
+ if (_vm->shouldQuit()) {
+ levelResult = kLevelQuit;
+ return false;
+ }
+
+ _playerLives--;
+ if (_playerLives <= 0) {
+ playLevelGameOver(levelId);
+ levelResult = kLevelGameOver;
+ return false;
+ }
+
+ playCinematic(retryVideo);
+ _playerDamage = 0;
+ if (_vm->shouldQuit()) {
+ levelResult = kLevelQuit;
+ return false;
+ }
+
+ return true;
+}
+
// ---------------------------------------------------------------------------
// Level 2 Handler - FUN_00418063
// Multiple parts with P1/P2/P3 subdirectories
@@ -314,8 +340,12 @@ int InsaneRebel2::runLevel2() {
if ((_rebelPhaseState & 0x10) != 0)
bonusCount++;
- if (_playerDamage >= 255)
- goto level2_death;
+ if (_playerDamage >= 255) {
+ int levelResult;
+ if (handleLevelDeath(2, _currentPhase, "LEV02/02DIE.SAN", "LEV02/02RETRY.SAN", levelResult))
+ continue;
+ return levelResult;
+ }
if (_vm->shouldQuit())
return kLevelQuit;
@@ -390,8 +420,12 @@ int InsaneRebel2::runLevel2() {
if ((_rebelPhaseState & 0x10) != 0)
bonusCount++;
- if (_playerDamage >= 255)
- goto level2_death;
+ if (_playerDamage >= 255) {
+ int levelResult;
+ if (handleLevelDeath(2, _currentPhase, "LEV02/02DIE.SAN", "LEV02/02RETRY.SAN", levelResult))
+ continue;
+ return levelResult;
+ }
if (_vm->shouldQuit())
return kLevelQuit;
@@ -479,8 +513,12 @@ int InsaneRebel2::runLevel2() {
bonusCount++;
totalKills += _rebelKillCounter;
- if (_playerDamage >= 255)
- goto level2_death;
+ if (_playerDamage >= 255) {
+ int levelResult;
+ if (handleLevelDeath(2, _currentPhase, "LEV02/02DIE.SAN", "LEV02/02RETRY.SAN", levelResult))
+ continue;
+ return levelResult;
+ }
if (_vm->shouldQuit())
return kLevelQuit;
@@ -497,28 +535,6 @@ int InsaneRebel2::runLevel2() {
playLevelEnd(2);
_levelUnlocked[2] = true; // Unlock level 3
return kLevelNextLevel;
-
- level2_death:
- // Player died â play death sequence and retry or game over
- // Original: FUN_00417168("LEV02/02DIE.SAN", 0x20)
- debug("Rebel2: Level 2 Phase %d death", _currentPhase);
- playCinematic("LEV02/02DIE.SAN");
- if (_vm->shouldQuit())
- return kLevelQuit;
-
- // Original: if (DAT_0047ab5c != 0) DAT_0047a7ee++ (bonus life award).
- // DAT_0047ab5c is set when the player earns a bonus life.
- _playerLives--;
- if (_playerLives <= 0) {
- // Original: FUN_00417ab2("LEV02/02OVER.SAN", 0x20, 2)
- playLevelGameOver(2);
- return kLevelGameOver;
- }
- playCinematic("LEV02/02RETRY.SAN");
- _playerDamage = 0;
- if (_vm->shouldQuit())
- return kLevelQuit;
- continue; // Restart from beginning
}
return kLevelQuit;
@@ -1213,8 +1229,12 @@ int InsaneRebel2::runLevel11() {
}
}
- if (_playerDamage >= 255)
- goto level11_death_phase1;
+ if (_playerDamage >= 255) {
+ int levelResult;
+ if (handleLevelDeath(11, 1, "LEV11/11DIE_A.SAN", "LEV11/11RETRY.SAN", levelResult))
+ continue;
+ return levelResult;
+ }
if (_vm->shouldQuit())
return kLevelQuit;
@@ -1269,8 +1289,12 @@ int InsaneRebel2::runLevel11() {
}
}
- if (_playerDamage >= 255)
- goto level11_death_phase2;
+ if (_playerDamage >= 255) {
+ int levelResult;
+ if (handleLevelDeath(11, 2, "LEV11/11DIE_B.SAN", "LEV11/11RETRY.SAN", levelResult))
+ continue;
+ return levelResult;
+ }
if (_vm->shouldQuit())
return kLevelQuit;
@@ -1344,8 +1368,12 @@ int InsaneRebel2::runLevel11() {
}
}
- if (_playerDamage >= 255)
- goto level11_death_phase3;
+ if (_playerDamage >= 255) {
+ int levelResult;
+ if (handleLevelDeath(11, 3, "LEV11/11DIE_C.SAN", "LEV11/11RETRY.SAN", levelResult))
+ continue;
+ return levelResult;
+ }
if (_vm->shouldQuit())
return kLevelQuit;
@@ -1426,8 +1454,12 @@ int InsaneRebel2::runLevel11() {
totalKills += _rebelKillCounter;
- if (_playerDamage >= 255)
- goto level11_death_phase3;
+ if (_playerDamage >= 255) {
+ int levelResult;
+ if (handleLevelDeath(11, 3, "LEV11/11DIE_C.SAN", "LEV11/11RETRY.SAN", levelResult))
+ continue;
+ return levelResult;
+ }
if (_vm->shouldQuit())
return kLevelQuit;
@@ -1442,35 +1474,6 @@ int InsaneRebel2::runLevel11() {
playLevelEnd(11);
_levelUnlocked[11] = true; // Unlock level 12
return kLevelNextLevel;
-
- level11_death_phase1:
- debug("Rebel2: Level 11 Phase 1 death");
- playCinematic("LEV11/11DIE_A.SAN");
- goto level11_retry;
-
- level11_death_phase2:
- debug("Rebel2: Level 11 Phase 2 death");
- playCinematic("LEV11/11DIE_B.SAN");
- goto level11_retry;
-
- level11_death_phase3:
- debug("Rebel2: Level 11 Phase 3 death");
- playCinematic("LEV11/11DIE_C.SAN");
- goto level11_retry;
-
- level11_retry:
- if (_vm->shouldQuit())
- return kLevelQuit;
- _playerLives--;
- if (_playerLives <= 0) {
- playLevelGameOver(11);
- return kLevelGameOver;
- }
- playCinematic("LEV11/11RETRY.SAN");
- _playerDamage = 0;
- if (_vm->shouldQuit())
- return kLevelQuit;
- continue; // Restart from Phase 1
}
return kLevelQuit;
@@ -1567,8 +1570,12 @@ int InsaneRebel2::runLevel12() {
}
}
- if (_playerDamage >= 255)
- goto level12_death;
+ if (_playerDamage >= 255) {
+ int levelResult;
+ if (handleLevelDeath(12, _currentPhase, "LEV12/12DIE.SAN", "LEV12/12RETRY.SAN", levelResult))
+ continue;
+ return levelResult;
+ }
if (_vm->shouldQuit())
return kLevelQuit;
@@ -1624,8 +1631,12 @@ int InsaneRebel2::runLevel12() {
}
}
- if (_playerDamage >= 255)
- goto level12_death;
+ if (_playerDamage >= 255) {
+ int levelResult;
+ if (handleLevelDeath(12, _currentPhase, "LEV12/12DIE.SAN", "LEV12/12RETRY.SAN", levelResult))
+ continue;
+ return levelResult;
+ }
if (_vm->shouldQuit())
return kLevelQuit;
@@ -1679,8 +1690,12 @@ int InsaneRebel2::runLevel12() {
}
}
- if (_playerDamage >= 255)
- goto level12_death;
+ if (_playerDamage >= 255) {
+ int levelResult;
+ if (handleLevelDeath(12, _currentPhase, "LEV12/12DIE.SAN", "LEV12/12RETRY.SAN", levelResult))
+ continue;
+ return levelResult;
+ }
if (_vm->shouldQuit())
return kLevelQuit;
@@ -1733,8 +1748,12 @@ int InsaneRebel2::runLevel12() {
}
}
- if (_playerDamage >= 255)
- goto level12_death;
+ if (_playerDamage >= 255) {
+ int levelResult;
+ if (handleLevelDeath(12, _currentPhase, "LEV12/12DIE.SAN", "LEV12/12RETRY.SAN", levelResult))
+ continue;
+ return levelResult;
+ }
if (_vm->shouldQuit())
return kLevelQuit;
@@ -1757,24 +1776,6 @@ int InsaneRebel2::runLevel12() {
playLevelEnd(12);
_levelUnlocked[12] = true; // Unlock level 13
return kLevelNextLevel;
-
- level12_death:
- // Single death video for all phases
- debug("Rebel2: Level 12 Phase %d death", _currentPhase);
- playCinematic("LEV12/12DIE.SAN");
-
- if (_vm->shouldQuit())
- return kLevelQuit;
- _playerLives--;
- if (_playerLives <= 0) {
- playLevelGameOver(12);
- return kLevelGameOver;
- }
- playCinematic("LEV12/12RETRY.SAN");
- _playerDamage = 0;
- if (_vm->shouldQuit())
- return kLevelQuit;
- continue; // Restart from Phase 1
}
return kLevelQuit;
Commit: ae5c073de3ac1089059be0cee52cc91c305de119
https://github.com/scummvm/scummvm/commit/ae5c073de3ac1089059be0cee52cc91c305de119
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:57+02:00
Commit Message:
SCUMM: RA2: Deduplicate runlevel helpers
Changed paths:
engines/scumm/insane/rebel2/rebel.h
engines/scumm/insane/rebel2/runlevels.cpp
diff --git a/engines/scumm/insane/rebel2/rebel.h b/engines/scumm/insane/rebel2/rebel.h
index 812038eff10..2d2210494cc 100644
--- a/engines/scumm/insane/rebel2/rebel.h
+++ b/engines/scumm/insane/rebel2/rebel.h
@@ -410,6 +410,9 @@ public:
// Play DIE/OVER/RETRY tail. Returns true when the caller should restart its retry loop.
bool handleLevelDeath(int levelId, int phase, const char *deathVideo, const char *retryVideo, int &levelResult);
+ void resetLevelAttemptState(int initialPhase);
+ void resetLevelPhaseState(bool clearEnemies);
+ void resetLevelWaveState();
// Random number helper (emulates FUN_004233a0)
int getRandomVariant(int max);
diff --git a/engines/scumm/insane/rebel2/runlevels.cpp b/engines/scumm/insane/rebel2/runlevels.cpp
index f7d7a8c0ff8..89ac7d43442 100644
--- a/engines/scumm/insane/rebel2/runlevels.cpp
+++ b/engines/scumm/insane/rebel2/runlevels.cpp
@@ -232,6 +232,37 @@ bool InsaneRebel2::handleLevelDeath(int levelId, int phase,
return true;
}
+void InsaneRebel2::resetLevelAttemptState(int initialPhase) {
+ _playerShield = 255;
+ _playerDamage = 0;
+ _currentPhase = initialPhase;
+
+ _rebelAutopilot = 0;
+ _rebelDamageLevel = 0;
+ _rebelControlMode = 0;
+
+ _enemies.clear();
+ for (int i = 0; i < 512; i++) {
+ _rebelLinks[i][0] = 0;
+ _rebelLinks[i][1] = 0;
+ _rebelLinks[i][2] = 0;
+ }
+}
+
+void InsaneRebel2::resetLevelPhaseState(bool clearEnemies) {
+ _rebelKillCounter = 0;
+ _rebelHitCounter = 0;
+ resetLevelWaveState();
+
+ if (clearEnemies)
+ _enemies.clear();
+}
+
+void InsaneRebel2::resetLevelWaveState() {
+ _rebelPhaseState = 0;
+ _rebelWaveState = 0;
+}
+
// ---------------------------------------------------------------------------
// Level 2 Handler - FUN_00418063
// Multiple parts with P1/P2/P3 subdirectories
@@ -273,33 +304,14 @@ int InsaneRebel2::runLevel2() {
// Main gameplay retry loop (restarts from beginning on death)
while (!_vm->shouldQuit()) {
- _playerShield = 255;
- _playerDamage = 0;
- _currentPhase = 1;
+ resetLevelAttemptState(1);
bonusCount = 0;
totalKills = 0;
totalMisses = 0;
- // Reset Handler 25 cover state â player starts uncovered at level start
- // DAT_00457904 and DAT_0045790a are zero-initialized globals in the original
- _rebelAutopilot = 0;
- _rebelDamageLevel = 0;
- _rebelControlMode = 0;
-
- // FUN_0041c7d0: Reset per-attempt state
- _enemies.clear();
- for (int i = 0; i < 512; i++) {
- _rebelLinks[i][0] = 0;
- _rebelLinks[i][1] = 0;
- _rebelLinks[i][2] = 0;
- }
-
// ----- PHASE 1: P1/02P01_X.SAN -----
// FUN_0041c7d0: Reset per-phase counters
- _rebelKillCounter = 0;
- _rebelHitCounter = 0;
- _rebelPhaseState = 0;
- _rebelWaveState = 0;
+ resetLevelPhaseState(false);
// Initialize kill budget from level data table + random(3)
// Original: sVar4 = levelData[phase1Offset]; local_14[0] = sVar4 + random(3)
@@ -363,11 +375,7 @@ int InsaneRebel2::runLevel2() {
// ----- PHASE 2: P2/02P02_X.SAN -----
_currentPhase = 2;
- _rebelKillCounter = 0;
- _rebelHitCounter = 0;
- _rebelPhaseState = 0;
- _rebelWaveState = 0;
- _enemies.clear();
+ resetLevelPhaseState(true);
// Initialize Phase 2 budget
budget = kLevel2BudgetBase[1] + _vm->_rnd.getRandomNumber(2);
@@ -442,11 +450,7 @@ int InsaneRebel2::runLevel2() {
// ----- PHASE 3: P3/02P03_X.SAN -----
_currentPhase = 3;
- _rebelKillCounter = 0;
- _rebelHitCounter = 0;
- _rebelPhaseState = 0;
- _rebelWaveState = 0;
- _enemies.clear();
+ resetLevelPhaseState(true);
prevWaveState = 0;
// Initialize Phase 3 budget
@@ -1164,31 +1168,13 @@ int InsaneRebel2::runLevel11() {
// Main gameplay retry loop (restarts from Phase 1 on death)
while (!_vm->shouldQuit()) {
- _playerShield = 255;
- _playerDamage = 0;
- _currentPhase = 1;
+ resetLevelAttemptState(1);
totalKills = 0;
totalMisses = 0;
prevPhaseState = 0;
- // Reset Handler 8 cover state
- _rebelAutopilot = 0;
- _rebelDamageLevel = 0;
- _rebelControlMode = 0;
-
- // FUN_0041c7d0: Reset per-attempt state
- _enemies.clear();
- for (int i = 0; i < 512; i++) {
- _rebelLinks[i][0] = 0;
- _rebelLinks[i][1] = 0;
- _rebelLinks[i][2] = 0;
- }
-
// ----- PHASE 1: P1/11P01_X.SAN -----
- _rebelKillCounter = 0;
- _rebelHitCounter = 0;
- _rebelPhaseState = 0;
- _rebelWaveState = 0;
+ resetLevelPhaseState(false);
int16 budget = kLevel11BudgetBase[0] + _vm->_rnd.getRandomNumber(2);
@@ -1249,11 +1235,7 @@ int InsaneRebel2::runLevel11() {
// ----- PHASE 2: P2/11P02_X.SAN -----
_currentPhase = 2;
- _rebelKillCounter = 0;
- _rebelHitCounter = 0;
- _rebelPhaseState = 0;
- _rebelWaveState = 0;
- _enemies.clear();
+ resetLevelPhaseState(true);
budget = kLevel11BudgetBase[1] + _vm->_rnd.getRandomNumber(2);
_rebelHandler = 8;
@@ -1310,11 +1292,7 @@ int InsaneRebel2::runLevel11() {
// ----- PHASE 3 FIRST HALF: P3/11P03_X (A-F) -----
// Bridge puzzle â exit when (phaseState & 0x70) == 0x70
_currentPhase = 3;
- _rebelKillCounter = 0;
- _rebelHitCounter = 0;
- _rebelPhaseState = 0;
- _rebelWaveState = 0;
- _enemies.clear();
+ resetLevelPhaseState(true);
prevPhaseState = 0;
budget = kLevel11BudgetBase[2] + _vm->_rnd.getRandomNumber(2);
@@ -1510,28 +1488,11 @@ int InsaneRebel2::runLevel12() {
// Main gameplay retry loop (restarts from Phase 1 on death)
while (!_vm->shouldQuit()) {
- _playerShield = 255;
- _playerDamage = 0;
- _currentPhase = 1;
-
- // Reset state
- _rebelAutopilot = 0;
- _rebelDamageLevel = 0;
- _rebelControlMode = 0;
-
- _enemies.clear();
- for (int i = 0; i < 512; i++) {
- _rebelLinks[i][0] = 0;
- _rebelLinks[i][1] = 0;
- _rebelLinks[i][2] = 0;
- }
+ resetLevelAttemptState(1);
// ----- PHASE 1: 12P05 â P1/12P01_X -----
// FUN_00401000: Reset at top of each retry
- _rebelKillCounter = 0;
- _rebelHitCounter = 0;
- _rebelPhaseState = 0;
- _rebelWaveState = 0;
+ resetLevelPhaseState(false);
int16 budget = kLevel12BudgetBase[0] + _vm->_rnd.getRandomNumber(2);
@@ -1581,8 +1542,7 @@ int InsaneRebel2::runLevel12() {
// ----- PHASE 2: 12P06 â P2/12P02_X -----
_currentPhase = 2;
- _rebelPhaseState = 0;
- _rebelWaveState = 0;
+ resetLevelWaveState();
budget = kLevel12BudgetBase[1] + _vm->_rnd.getRandomNumber(3);
@@ -1642,8 +1602,7 @@ int InsaneRebel2::runLevel12() {
// ----- PHASE 3: 12P07 â P3/12P03_X -----
_currentPhase = 3;
- _rebelPhaseState = 0;
- _rebelWaveState = 0;
+ resetLevelWaveState();
budget = kLevel12BudgetBase[2] + _vm->_rnd.getRandomNumber(3);
@@ -1701,8 +1660,7 @@ int InsaneRebel2::runLevel12() {
// ----- PHASE 4: 12P08 â P4/12P04_X -----
_currentPhase = 4;
- _rebelPhaseState = 0;
- _rebelWaveState = 0;
+ resetLevelWaveState();
budget = kLevel12BudgetBase[3] + _vm->_rnd.getRandomNumber(3);
Commit: 4879d743c66bdee6ebefdbf9e6f94847a100d462
https://github.com/scummvm/scummvm/commit/4879d743c66bdee6ebefdbf9e6f94847a100d462
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:58+02:00
Commit Message:
SCUMM: RA2: Refactor wave handling
Changed paths:
engines/scumm/insane/rebel2/rebel.h
engines/scumm/insane/rebel2/runlevels.cpp
diff --git a/engines/scumm/insane/rebel2/rebel.h b/engines/scumm/insane/rebel2/rebel.h
index 2d2210494cc..60a06d265e4 100644
--- a/engines/scumm/insane/rebel2/rebel.h
+++ b/engines/scumm/insane/rebel2/rebel.h
@@ -392,12 +392,24 @@ public:
// Wave state management (FUN_00417b61)
// Waits for current video to finish, accumulates kill state, redistributes
- // kill credits from the budget. Returns credited kill bits, or 0xFFFF on death/quit.
+ // kill credits from the budget.
// mask: required enemy bits (0x36 for Phase 1, 0x3e for Phases 2/3)
// budget: kill credit budget counter (decremented per credit transfer)
// threshold: early-exit frame threshold (0=disabled, 0x14=20 for wave loops)
// flags: bit 1 = add random unkilled types, bit 0 = limit credits to 2 (else 8)
- uint16 processWaveEnd(int16 mask, int16 *budget, int16 threshold, uint16 flags);
+ struct WaveEndResult {
+ WaveEndResult() : creditedBits(0), died(false), quit(false), completed(false), skipped(false) {}
+
+ bool shouldStop() const { return died || quit || completed || skipped; }
+
+ uint16 creditedBits;
+ bool died;
+ bool quit;
+ bool completed;
+ bool skipped;
+ };
+
+ WaveEndResult processWaveEnd(int16 mask, int16 *budget, int16 threshold, uint16 flags);
// Play a raw SAN segment from a scripted level handler.
// Retail reaches these call sites through different wrappers/direct paths; this
diff --git a/engines/scumm/insane/rebel2/runlevels.cpp b/engines/scumm/insane/rebel2/runlevels.cpp
index 89ac7d43442..93a763db401 100644
--- a/engines/scumm/insane/rebel2/runlevels.cpp
+++ b/engines/scumm/insane/rebel2/runlevels.cpp
@@ -88,28 +88,27 @@ int InsaneRebel2::runLevel1() {
// ---------------------------------------------------------------------------
// Wave State Management - FUN_00417b61
// Waits for video completion, accumulates kill state, redistributes kill credits.
-// Used by all multi-wave levels (Level 2, 3, 6, etc.) as the core wave loop primitive.
+// Used by randomized multi-wave level handlers as the core wave loop primitive.
// ---------------------------------------------------------------------------
-uint16 InsaneRebel2::processWaveEnd(int16 mask, int16 *budget, int16 threshold, uint16 flags) {
+InsaneRebel2::WaveEndResult InsaneRebel2::processWaveEnd(int16 mask, int16 *budget, int16 threshold, uint16 flags) {
// FUN_00417b61: Core wave management function
// Called after each wave video plays. Handles:
// 1. Waiting for video to finish (with early exit on enemy completion)
// 2. Copying wave state to accumulated phase state
// 3. Redistributing kill credits from the budget
- //
- // Returns: kill bits credited this wave, or 0xFFFF on death/quit/completion
- uint16 result = 0;
+ WaveEndResult result;
// Debug shortcut path: force-end current section when requested via Shift+S.
- // This returns the same sentinel (0xFFFF) used for section completion/death/quit.
if (_skipSectionRequested) {
_skipSectionRequested = false;
_rebelPhaseState = mask;
_rebelWaveState = mask;
debug("Rebel2 processWaveEnd: Shift+S skip consumed (mask=0x%x)", (uint16)mask);
- return 0xFFFF;
+ result.completed = true;
+ result.skipped = true;
+ return result;
}
// Step 1: Wait for video to finish (lines 21-32)
@@ -167,7 +166,7 @@ uint16 InsaneRebel2::processWaveEnd(int16 mask, int16 *budget, int16 threshold,
while (creditCount < maxCredits && numKilled > 0 && budget && *budget > 0) {
int idx = _vm->_rnd.getRandomNumber(numKilled - 1);
_rebelPhaseState -= killed[idx]; // Remove from accumulated state
- result |= killed[idx]; // Credit to return value
+ result.creditedBits |= killed[idx]; // Credit to return value
(*budget)--;
// Remove from array (shift remaining elements)
@@ -179,13 +178,13 @@ uint16 InsaneRebel2::processWaveEnd(int16 mask, int16 *budget, int16 threshold,
}
debug("Rebel2 processWaveEnd: result=0x%x phaseState=0x%x (after redistribution) budget=%d",
- result, _rebelPhaseState, budget ? *budget : -1);
+ result.creditedBits, _rebelPhaseState, budget ? *budget : -1);
+
+ // Step 5: Stop conditions (lines 74-78)
+ result.died = (_playerDamage >= 255);
+ result.completed = ((int16)_rebelPhaseState >= mask);
+ result.quit = _vm->shouldQuit();
- // Step 5: Return value (lines 74-78)
- // Return 0xFFFF if: dead, phase complete, or quit
- if (_playerDamage >= 255 || (int16)_rebelPhaseState >= mask || _vm->shouldQuit()) {
- return 0xFFFF;
- }
return result;
}
@@ -391,8 +390,9 @@ int InsaneRebel2::runLevel2() {
// Phase 2 wave loop: processWaveEnd at TOP of loop (matches assembly structure)
// Original: local_10 = FUN_00417b61(0x3e, local_14, 0, 0); then switch(local_10)
while (true) {
- uint16 waveSelect = processWaveEnd(0x3e, &budget, 0, 0);
- if (waveSelect == 0xFFFF || (_rebelPhaseState & 0x0e) == 0x0e)
+ WaveEndResult waveEnd = processWaveEnd(0x3e, &budget, 0, 0);
+ uint16 waveSelect = waveEnd.creditedBits;
+ if (waveEnd.shouldStop() || (_rebelPhaseState & 0x0e) == 0x0e)
break;
if (_vm->shouldQuit())
return kLevelQuit;
@@ -467,12 +467,14 @@ int InsaneRebel2::runLevel2() {
// Phase 3: processWaveEnd at BOTTOM (like Phase 1), waveSelect carried across iterations
// Original: local_10 = FUN_00417b61(0x3e, local_14, 0, 0); while (loop) { ...; local_10 = FUN_00417b61(0x3e, local_14, 0x14, 0); }
{
- uint16 waveSelect = processWaveEnd(0x3e, &budget, 0, 0);
+ WaveEndResult waveEnd = processWaveEnd(0x3e, &budget, 0, 0);
- while (waveSelect != 0xFFFF && (_rebelPhaseState & 0x0e) != 0x0e) {
+ while (!waveEnd.shouldStop() && (_rebelPhaseState & 0x0e) != 0x0e) {
if (_vm->shouldQuit())
return kLevelQuit;
+ uint16 waveSelect = waveEnd.creditedBits;
+
// Phase 3 randomization (original lines 113-115):
// If previous wave state bit 0 was clear AND random(8)==0, set bit 0
if (((prevWaveState & 1) == 0) && (_vm->_rnd.getRandomNumber(7) == 0)) {
@@ -508,7 +510,7 @@ int InsaneRebel2::runLevel2() {
return kLevelQuit;
// processWaveEnd at BOTTOM with threshold=0x14
- waveSelect = processWaveEnd(0x3e, &budget, 0x14, 0);
+ waveEnd = processWaveEnd(0x3e, &budget, 0x14, 0);
debug("Rebel2: Phase 3 wave done - state=0x%x (need 0x0e) budget=%d", _rebelPhaseState, budget);
}
}
@@ -1184,14 +1186,16 @@ int InsaneRebel2::runLevel11() {
return kLevelQuit;
{
- uint16 waveSelect = processWaveEnd(0x0e, &budget, 0, 0);
+ WaveEndResult waveEnd = processWaveEnd(0x0e, &budget, 0, 0);
// Phase 1 wave loop: random(2) | (waveSelect & 8) â variants
// 0âD, 1âC, 8âB, 9âA
- while (waveSelect != 0xFFFF) {
+ while (!waveEnd.shouldStop()) {
if (_vm->shouldQuit())
return kLevelQuit;
+ uint16 waveSelect = waveEnd.creditedBits;
+
// Bonus sound check
if ((_rebelPhaseState & 0x10) != 0 && (prevPhaseState & 0x10) == 0) {
// FUN_00411931 bonus sound â not yet implemented
@@ -1211,7 +1215,7 @@ int InsaneRebel2::runLevel11() {
if (!playLevelSegment(filename, 0x428))
return kLevelQuit;
- waveSelect = processWaveEnd(0x0e, &budget, 0x14, 0);
+ waveEnd = processWaveEnd(0x0e, &budget, 0x14, 0);
}
}
@@ -1247,10 +1251,10 @@ int InsaneRebel2::runLevel11() {
{
// Phase 2: flags=3 (maxCredits=2, redistribution ON)
- uint16 waveSelect = processWaveEnd(0x0e, &budget, 0, 3);
+ WaveEndResult waveEnd = processWaveEnd(0x0e, &budget, 0, 3);
// Random(4) for variant selection: A, B, C, D
- while (waveSelect != 0xFFFF) {
+ while (!waveEnd.shouldStop()) {
if (_vm->shouldQuit())
return kLevelQuit;
@@ -1267,7 +1271,7 @@ int InsaneRebel2::runLevel11() {
if (!playLevelSegment(filename, 0x428))
return kLevelQuit;
- waveSelect = processWaveEnd(0x0e, &budget, 0x14, 3);
+ waveEnd = processWaveEnd(0x0e, &budget, 0x14, 3);
}
}
@@ -1303,11 +1307,11 @@ int InsaneRebel2::runLevel11() {
return kLevelQuit;
{
- uint16 waveSelect = processWaveEnd(0x7e, &budget, 0, 0);
+ WaveEndResult waveEnd = processWaveEnd(0x7e, &budget, 0, 0);
int variantIdx = 0; // Tracks variant for randomization threshold
// Loop until (phaseState & 0x70) == 0x70 (bridge targets destroyed)
- while (waveSelect != 0xFFFF && (_rebelPhaseState & 0x70) != 0x70) {
+ while (!waveEnd.shouldStop() && (_rebelPhaseState & 0x70) != 0x70) {
if (_vm->shouldQuit())
return kLevelQuit;
@@ -1342,7 +1346,7 @@ int InsaneRebel2::runLevel11() {
// Threshold only for higher variants (original: (2 < variantIdx) - 1 & 0x14)
int16 threshold = (variantIdx > 2) ? 0x14 : 0;
- waveSelect = processWaveEnd(0x7e, &budget, threshold, 0);
+ waveEnd = processWaveEnd(0x7e, &budget, threshold, 0);
}
}
@@ -1395,9 +1399,9 @@ int InsaneRebel2::runLevel11() {
// Only enter wave loop if not all basic types killed already
if ((_rebelPhaseState & 0x0e) < 0x0e) {
int variantIdx = 0;
- uint16 waveSelect = processWaveEnd(0x0e, &budget, 0, 0);
+ WaveEndResult waveEnd = processWaveEnd(0x0e, &budget, 0, 0);
- while (waveSelect != 0xFFFF) {
+ while (!waveEnd.shouldStop()) {
if (_vm->shouldQuit())
return kLevelQuit;
@@ -1426,7 +1430,7 @@ int InsaneRebel2::runLevel11() {
return kLevelQuit;
int16 threshold = (variantIdx > 2) ? 0x14 : 0;
- waveSelect = processWaveEnd(0x0e, &budget, threshold, 0);
+ waveEnd = processWaveEnd(0x0e, &budget, threshold, 0);
}
}
@@ -1507,13 +1511,14 @@ int InsaneRebel2::runLevel12() {
return kLevelQuit;
{
- uint16 waveSelect = processWaveEnd(6, &budget, 0x14, 0);
+ WaveEndResult waveEnd = processWaveEnd(6, &budget, 0x14, 0);
// Wave loop: random(2) | (waveSelect & 2) â 0:C, 1:D, 2:A, 3:B
- while (waveSelect != 0xFFFF) {
+ while (!waveEnd.shouldStop()) {
if (_vm->shouldQuit())
return kLevelQuit;
+ uint16 waveSelect = waveEnd.creditedBits;
int sel = _vm->_rnd.getRandomNumber(1) | (waveSelect & 2);
const char *filename;
switch (sel) {
@@ -1527,7 +1532,7 @@ int InsaneRebel2::runLevel12() {
if (!playLevelSegment(filename, 0x428))
return kLevelQuit;
- waveSelect = processWaveEnd(6, &budget, 0x14, 0);
+ waveEnd = processWaveEnd(6, &budget, 0x14, 0);
}
}
@@ -1557,12 +1562,14 @@ int InsaneRebel2::runLevel12() {
return kLevelQuit;
{
- uint16 waveSelect = processWaveEnd(6, &budget, 0x14, 0);
+ WaveEndResult waveEnd = processWaveEnd(6, &budget, 0x14, 0);
- while (waveSelect != 0xFFFF) {
+ while (!waveEnd.shouldStop()) {
if (_vm->shouldQuit())
return kLevelQuit;
+ uint16 waveSelect = waveEnd.creditedBits;
+
// Variant selection: (waveSelect & 2) controls which set
int variantIdx;
if ((waveSelect & 2) == 0) {
@@ -1587,7 +1594,7 @@ int InsaneRebel2::runLevel12() {
// Variants E(2) and F(5) reset threshold to 0
int16 threshold = (variantIdx == 2 || variantIdx == 5) ? 0 : 0x14;
- waveSelect = processWaveEnd(6, &budget, threshold, 0);
+ waveEnd = processWaveEnd(6, &budget, threshold, 0);
}
}
@@ -1618,9 +1625,9 @@ int InsaneRebel2::runLevel12() {
{
int variantIdx = 0;
- uint16 waveSelect = processWaveEnd(6, &budget, 0x14, 0);
+ WaveEndResult waveEnd = processWaveEnd(6, &budget, 0x14, 0);
- while (waveSelect != 0xFFFF) {
+ while (!waveEnd.shouldStop()) {
if (_vm->shouldQuit())
return kLevelQuit;
@@ -1645,7 +1652,7 @@ int InsaneRebel2::runLevel12() {
if (!playLevelSegment(filename, 0x428))
return kLevelQuit;
- waveSelect = processWaveEnd(6, &budget, 0x14, 0);
+ waveEnd = processWaveEnd(6, &budget, 0x14, 0);
}
}
@@ -1676,9 +1683,9 @@ int InsaneRebel2::runLevel12() {
{
int variantIdx = 0;
- uint16 waveSelect = processWaveEnd(0x0e, &budget, 0x14, 0);
+ WaveEndResult waveEnd = processWaveEnd(0x0e, &budget, 0x14, 0);
- while (waveSelect != 0xFFFF) {
+ while (!waveEnd.shouldStop()) {
if (_vm->shouldQuit())
return kLevelQuit;
@@ -1702,7 +1709,7 @@ int InsaneRebel2::runLevel12() {
if (!playLevelSegment(filename, 0x428))
return kLevelQuit;
- waveSelect = processWaveEnd(0x0e, &budget, 0x14, 0);
+ waveEnd = processWaveEnd(0x0e, &budget, 0x14, 0);
}
}
Commit: 6eab30feedda5009d334ed8b6d305092caf964a5
https://github.com/scummvm/scummvm/commit/6eab30feedda5009d334ed8b6d305092caf964a5
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:58+02:00
Commit Message:
SCUMM: RA2: Split opcode 6 IACT handling
Changed paths:
engines/scumm/insane/rebel2/iact.cpp
engines/scumm/insane/rebel2/rebel.h
diff --git a/engines/scumm/insane/rebel2/iact.cpp b/engines/scumm/insane/rebel2/iact.cpp
index 5d1423ca472..7c088bf7026 100644
--- a/engines/scumm/insane/rebel2/iact.cpp
+++ b/engines/scumm/insane/rebel2/iact.cpp
@@ -583,29 +583,9 @@ void InsaneRebel2::iactRebel2Opcode3(Common::SeekableReadStream &b, int16 par2,
}
}
-//
-// iactRebel2Opcode6 -- Level setup / mode switch (FUN_41CADB case 4)
-//
-// Per-wave initialization: clears bit table, resets link tables, configures
-// handler mode (ship/turret/corridor), and loads collision zones. Called once
-// per wave video on frame 0.
-//
-void InsaneRebel2::iactRebel2Opcode6(byte *renderBitmap, Common::SeekableReadStream &b, int32 chunkSize, int16 par2, int16 par3, int16 par4) {
- // Opcode 6: Level setup / mode switch
- // Based on FUN_41CADB case 4 (switch on *local_14 - 2 == 4, meaning opcode 6)
- //
- // For Handler 8 (third-person on foot) - FUN_00401234 case 4:
- // - par3 sets ship level mode (DAT_0043e000)
- // - par4 == 1 triggers status bar display and state reset
- // - Updates ship position based on mouse input
- //
- // For Handler 0x26/0x19 (turret/FPS):
- // - Same par4 == 1 behavior
- // - Different view offset calculations
-
- debug("Rebel2 IACT Opcode 6: par2=%d par3=%d par4=%d", par2, par3, par4);
-
- // Update handler type if par2 is a known handler value (from FUN_4033CF case 6)
+// ScummVM refactor helper for opcode 6, not a separate retail function.
+void InsaneRebel2::updateOpcode6Handler(int16 par2) {
+ // Update handler type if par2 is a known handler value (from FUN_4033CF case 6).
if (par2 == 7 || par2 == 8 || par2 == 0x19 || par2 == 0x26) {
// Reset Level 2 background flag when transitioning away from Handler 8
if (_rebelHandler == 8 && par2 != 8) {
@@ -614,725 +594,728 @@ void InsaneRebel2::iactRebel2Opcode6(byte *renderBitmap, Common::SeekableReadStr
_rebelHandler = par2;
debug("Rebel2 Opcode 6: Setting handler=%d", par2);
}
+}
- // Handler 8 specific logic (third-person on foot) - FUN_00401234 case 4
- if (_rebelHandler == 8) {
- // Set ship level mode (DAT_0043e000 = par3)
- _shipLevelMode = par3;
-
- // If par4 == 1, enable status bar and re-render laser texture (FUN_0040bb87)
- if (par4 == 1) {
- _rebelStatusBarSprite = 5;
- if (_smush_iconsNut && _smush_iconsNut->getNumChars() > 5) {
- initLaserTexture(_smush_iconsNut, 5);
- }
- }
+// ScummVM refactor helper for opcode 6 Handler 8, not a separate retail function.
+void InsaneRebel2::handleOpcode6Handler8(int16 par3, int16 par4) {
+ // Handler 8 specific logic (third-person on foot) - FUN_00401234 case 4.
+ // Set ship level mode (DAT_0043e000 = par3)
+ _shipLevelMode = par3;
- // Reset state when shipLevelMode != 0 && par4 == 1 (FUN_401234 lines 97-103)
- // Guard with _rebelOp6Initialized: runs once per wave video, not per frame.
- if (_shipLevelMode != 0 && par4 == 1 && !_rebelOp6Initialized) {
- clearBit(0);
- for (int i = 0; i < 512; i++) {
- _rebelLinks[i][0] = 0;
- _rebelLinks[i][1] = 0;
- _rebelLinks[i][2] = 0;
- }
- _rebelWaveState = _rebelPhaseState;
- _rebelOp6Initialized = true;
- debug("Rebel2 Opcode 6 (Handler 8): Wave init, wave=0x%x", _rebelWaveState);
- }
-
- // Skip position calculation for special modes 4 and 5
- if (_shipLevelMode != 4 && _shipLevelMode != 5) {
- // ----- Movement Range Transition (Covered vs Shooting) -----
- // Based on FUN_00401234 lines 85-120:
- // Mode 2 = "Covered" state - contract movement range to 41 (0x29)
- // Other modes = "Shooting" state - expand movement range to 127 (0x7f)
- // Transition happens gradually at ±10 per frame for smooth animation
- if (_shipLevelMode == 2) {
- // Covered state - contract movement range
- if (_movementRangeLimit > 41) {
- _movementRangeLimit -= 10;
- }
- if (_movementRangeLimit < 41) {
- _movementRangeLimit = 41;
- }
- } else {
- // Shooting state - expand movement range
- if (_movementRangeLimit < 127) {
- _movementRangeLimit += 10;
- }
- if (_movementRangeLimit > 127) {
- _movementRangeLimit = 127;
- }
- }
+ // If par4 == 1, enable status bar and re-render laser texture (FUN_0040bb87)
+ if (par4 == 1) {
+ _rebelStatusBarSprite = 5;
+ if (_smush_iconsNut && _smush_iconsNut->getNumChars() > 5) {
+ initLaserTexture(_smush_iconsNut, 5);
+ }
+ }
- // Calculate target position from mouse input
- // Mouse X maps to ship horizontal tilt, Mouse Y to vertical tilt
- // Based on FUN_00401234 lines 151-166:
- // local_18 = ((DAT_0047a7e0 * 5 + 0x27b) * 0x40) / 0xfe
- // local_1c = ((DAT_0047a7e2 * 5 + 0x27b) * 0x10) / 0xfe
-
- // Map the effective aim position (-127 to 127 range) to the ship target.
- Common::Point aimPos = getGameplayAimPoint();
- int16 mouseOffsetX = (int16)((aimPos.x - 160) * 127 / 160);
- int16 mouseOffsetY = (int16)((aimPos.y - 100) * 127 / 100);
-
- // Clamp X offset to movement range limit (covered/shooting state)
- // Based on FUN_00401234 lines 119-136
- if (mouseOffsetX > _movementRangeLimit)
- mouseOffsetX = _movementRangeLimit;
- if (mouseOffsetX < -_movementRangeLimit)
- mouseOffsetX = -_movementRangeLimit;
- // Y offset always uses full range (±127)
- if (mouseOffsetY > 127)
- mouseOffsetY = 127;
- if (mouseOffsetY < -127)
- mouseOffsetY = -127;
-
- // Calculate target positions using the original formula
- // Original FUN_00401234 lines 151-166:
- // local_18 = ((mouseX * 5 + 0x27b) * 0x40) / 0xfe -> X target
- // local_1c = ((mouseY * 5 + 0x27b) * 0x10) / 0xfe -> Y target
- // _DAT_0043e004 = -local_1c (stored negated for cursor display)
- // The interpolation (lines 181-193) uses local_1c (positive), NOT _DAT_0043e004.
- // So the interpolation target must be the positive formula result.
- _shipTargetX = (int16)(((mouseOffsetX * 5 + 0x27b) * 0x40) / 0xfe);
- _shipTargetY = (int16)(((mouseOffsetY * 5 + 0x27b) * 0x10) / 0xfe);
-
- // Smooth interpolation toward target (max 50 pixels per frame)
- const int16 maxStep = 50; // 0x32 in hex
- if (_shipPosX < _shipTargetX) {
- int16 newX = _shipPosX + maxStep;
- _shipPosX = (newX > _shipTargetX) ? _shipTargetX : newX;
- } else if (_shipPosX > _shipTargetX) {
- int16 newX = _shipPosX - maxStep;
- _shipPosX = (newX < _shipTargetX) ? _shipTargetX : newX;
+ // Reset state when shipLevelMode != 0 && par4 == 1 (FUN_401234 lines 97-103)
+ // Guard with _rebelOp6Initialized: runs once per wave video, not per frame.
+ if (_shipLevelMode != 0 && par4 == 1 && !_rebelOp6Initialized) {
+ clearBit(0);
+ for (int i = 0; i < 512; i++) {
+ _rebelLinks[i][0] = 0;
+ _rebelLinks[i][1] = 0;
+ _rebelLinks[i][2] = 0;
+ }
+ _rebelWaveState = _rebelPhaseState;
+ _rebelOp6Initialized = true;
+ debug("Rebel2 Opcode 6 (Handler 8): Wave init, wave=0x%x", _rebelWaveState);
+ }
+
+ // Skip position calculation for special modes 4 and 5
+ if (_shipLevelMode != 4 && _shipLevelMode != 5) {
+ // ----- Movement Range Transition (Covered vs Shooting) -----
+ // Based on FUN_00401234 lines 85-120:
+ // Mode 2 = "Covered" state - contract movement range to 41 (0x29)
+ // Other modes = "Shooting" state - expand movement range to 127 (0x7f)
+ // Transition happens gradually at +/-10 per frame for smooth animation
+ if (_shipLevelMode == 2) {
+ // Covered state - contract movement range
+ if (_movementRangeLimit > 41) {
+ _movementRangeLimit -= 10;
}
-
- if (_shipPosY < _shipTargetY) {
- int16 newY = _shipPosY + maxStep;
- _shipPosY = (newY > _shipTargetY) ? _shipTargetY : newY;
- } else if (_shipPosY > _shipTargetY) {
- int16 newY = _shipPosY - maxStep;
- _shipPosY = (newY < _shipTargetY) ? _shipTargetY : newY;
+ if (_movementRangeLimit < 41) {
+ _movementRangeLimit = 41;
}
-
- // Calculate ship direction indices for sprite selection
- // Map mouse position to 5x7 direction grid (like Handler 7)
- int16 mouseX = aimPos.x;
- int16 mouseY = aimPos.y;
-
- // Scale mouse if video is larger than 320x200
- if (_player && _player->_width > 320) {
- mouseX = (mouseX * 320) / _player->_width;
+ } else {
+ // Shooting state - expand movement range
+ if (_movementRangeLimit < 127) {
+ _movementRangeLimit += 10;
}
- if (_player && _player->_height > 200) {
- mouseY = (mouseY * 200) / _player->_height;
+ if (_movementRangeLimit > 127) {
+ _movementRangeLimit = 127;
}
-
- // Horizontal: 5 zones (0=far left, 2=center, 4=far right)
- if (mouseX < 64)
- _shipDirectionH = 0;
- else if (mouseX < 128)
- _shipDirectionH = 1;
- else if (mouseX < 192)
- _shipDirectionH = 2;
- else if (mouseX < 256)
- _shipDirectionH = 3;
- else
- _shipDirectionH = 4;
-
- // Vertical: 7 zones (0=far up, 3=center, 6=far down)
- if (mouseY < 28)
- _shipDirectionV = 0;
- else if (mouseY < 57)
- _shipDirectionV = 1;
- else if (mouseY < 86)
- _shipDirectionV = 2;
- else if (mouseY < 114)
- _shipDirectionV = 3;
- else if (mouseY < 143)
- _shipDirectionV = 4;
- else if (mouseY < 171)
- _shipDirectionV = 5;
- else
- _shipDirectionV = 6;
-
- _shipDirectionIndex = _shipDirectionH * 7 + _shipDirectionV;
- }
-
- // Update firing state from mouse button or joystick fire action
- // Mode 4 (autopilot) disables shooting - FUN_00401CCF line 82-84
- if (_shipLevelMode == 4) {
- _shipFiring = false;
- } else {
- _shipFiring = (_vm->VAR(_vm->VAR_LEFTBTN_HOLD) != 0) ||
- _vm->getActionState(kScummActionInsaneAttack);
}
- debug("Rebel2 Opcode 6 (Handler 8): mode=%d range=%d shipPos=(%d,%d) target=(%d,%d) firing=%d dir=(%d,%d,%d)",
- _shipLevelMode, _movementRangeLimit, _shipPosX, _shipPosY, _shipTargetX, _shipTargetY, _shipFiring,
- _shipDirectionH, _shipDirectionV, _shipDirectionIndex);
+ // Calculate target position from mouse input
+ // Mouse X maps to ship horizontal tilt, Mouse Y to vertical tilt
+ // Based on FUN_00401234 lines 151-166:
+ // local_18 = ((DAT_0047a7e0 * 5 + 0x27b) * 0x40) / 0xfe
+ // local_1c = ((DAT_0047a7e2 * 5 + 0x27b) * 0x10) / 0xfe
- // Handler 8 doesn't use the same view offset logic as other handlers
- // Skip the rest of the function for Handler 8
- return;
+ // Map the effective aim position (-127 to 127 range) to the ship target.
+ Common::Point aimPos = getGameplayAimPoint();
+ int16 mouseOffsetX = (int16)((aimPos.x - 160) * 127 / 160);
+ int16 mouseOffsetY = (int16)((aimPos.y - 100) * 127 / 100);
+
+ // Clamp X offset to movement range limit (covered/shooting state)
+ // Based on FUN_00401234 lines 119-136
+ if (mouseOffsetX > _movementRangeLimit)
+ mouseOffsetX = _movementRangeLimit;
+ if (mouseOffsetX < -_movementRangeLimit)
+ mouseOffsetX = -_movementRangeLimit;
+ // Y offset always uses full range (+/-127)
+ if (mouseOffsetY > 127)
+ mouseOffsetY = 127;
+ if (mouseOffsetY < -127)
+ mouseOffsetY = -127;
+
+ // Calculate target positions using the original formula
+ // Original FUN_00401234 lines 151-166:
+ // local_18 = ((mouseX * 5 + 0x27b) * 0x40) / 0xfe -> X target
+ // local_1c = ((mouseY * 5 + 0x27b) * 0x10) / 0xfe -> Y target
+ // _DAT_0043e004 = -local_1c (stored negated for cursor display)
+ // The interpolation (lines 181-193) uses local_1c (positive), NOT _DAT_0043e004.
+ // So the interpolation target must be the positive formula result.
+ _shipTargetX = (int16)(((mouseOffsetX * 5 + 0x27b) * 0x40) / 0xfe);
+ _shipTargetY = (int16)(((mouseOffsetY * 5 + 0x27b) * 0x10) / 0xfe);
+
+ // Smooth interpolation toward target (max 50 pixels per frame)
+ const int16 maxStep = 50; // 0x32 in hex
+ if (_shipPosX < _shipTargetX) {
+ int16 newX = _shipPosX + maxStep;
+ _shipPosX = (newX > _shipTargetX) ? _shipTargetX : newX;
+ } else if (_shipPosX > _shipTargetX) {
+ int16 newX = _shipPosX - maxStep;
+ _shipPosX = (newX < _shipTargetX) ? _shipTargetX : newX;
+ }
+
+ if (_shipPosY < _shipTargetY) {
+ int16 newY = _shipPosY + maxStep;
+ _shipPosY = (newY > _shipTargetY) ? _shipTargetY : newY;
+ } else if (_shipPosY > _shipTargetY) {
+ int16 newY = _shipPosY - maxStep;
+ _shipPosY = (newY < _shipTargetY) ? _shipTargetY : newY;
+ }
+
+ // Calculate ship direction indices for sprite selection
+ // Map mouse position to 5x7 direction grid (like Handler 7)
+ int16 mouseX = aimPos.x;
+ int16 mouseY = aimPos.y;
+
+ // Scale mouse if video is larger than 320x200
+ if (_player && _player->_width > 320) {
+ mouseX = (mouseX * 320) / _player->_width;
+ }
+ if (_player && _player->_height > 200) {
+ mouseY = (mouseY * 200) / _player->_height;
+ }
+
+ // Horizontal: 5 zones (0=far left, 2=center, 4=far right)
+ if (mouseX < 64)
+ _shipDirectionH = 0;
+ else if (mouseX < 128)
+ _shipDirectionH = 1;
+ else if (mouseX < 192)
+ _shipDirectionH = 2;
+ else if (mouseX < 256)
+ _shipDirectionH = 3;
+ else
+ _shipDirectionH = 4;
+
+ // Vertical: 7 zones (0=far up, 3=center, 6=far down)
+ if (mouseY < 28)
+ _shipDirectionV = 0;
+ else if (mouseY < 57)
+ _shipDirectionV = 1;
+ else if (mouseY < 86)
+ _shipDirectionV = 2;
+ else if (mouseY < 114)
+ _shipDirectionV = 3;
+ else if (mouseY < 143)
+ _shipDirectionV = 4;
+ else if (mouseY < 171)
+ _shipDirectionV = 5;
+ else
+ _shipDirectionV = 6;
+
+ _shipDirectionIndex = _shipDirectionH * 7 + _shipDirectionV;
+ }
+
+ // Update firing state from mouse button or joystick fire action
+ // Mode 4 (autopilot) disables shooting - FUN_00401CCF line 82-84
+ if (_shipLevelMode == 4) {
+ _shipFiring = false;
+ } else {
+ _shipFiring = (_vm->VAR(_vm->VAR_LEFTBTN_HOLD) != 0) ||
+ _vm->getActionState(kScummActionInsaneAttack);
}
+ debug("Rebel2 Opcode 6 (Handler 8): mode=%d range=%d shipPos=(%d,%d) target=(%d,%d) firing=%d dir=(%d,%d,%d)",
+ _shipLevelMode, _movementRangeLimit, _shipPosX, _shipPosY, _shipTargetX, _shipTargetY, _shipFiring,
+ _shipDirectionH, _shipDirectionV, _shipDirectionIndex);
+}
+
+// ScummVM refactor helper for opcode 6 Handler 7, not a separate retail function.
+void InsaneRebel2::handleOpcode6Handler7(Common::SeekableReadStream &b, int16 par4) {
// Handler 7 specific logic (third-person ship) - FUN_0040d836 / FUN_0040c3cc
- // Used for Level 3 and similar space combat levels
- if (_rebelHandler == 7) {
- // Set control mode: DAT_004437c0 = param_5[3] = par4 in FUN_40C3CC case 4.
- // This determines collision mode and shooting capability:
- // Mode 0: Obstacle avoidance â SECONDARY zones, corridor boundaries
- // Mode 1: Tunnel flight â PRIMARY zones, per-edge push-back (hMargin=0x28)
- // Mode 2: Combat mode â shooting ENABLED, SECONDARY zones
- // Mode 3: Tunnel flight â PRIMARY zones, per-edge push-back (hMargin=0x0f)
- _flyControlMode = par4;
- debug("Rebel2 Opcode 6 (Handler 7): Control mode set to %d (shooting %s)",
- par4, (par4 == 2) ? "ENABLED" : "DISABLED");
-
- // Status bar: param_5[4] == 1 in original (first body word, 5th IACT word)
- // In our parsing, par3 maps to param_5[2] and the body follows par4.
- // FUN_40C3CC: if (param_5[4] == 1) FUN_0040bb87(DAT_0047a828,5);
- // par3 is param_5[2], which the original doesn't use here.
- // The body word for status bar is read separately below.
- int16 bodyStatusFlag = 0;
- if (b.size() - b.pos() >= 2) {
- bodyStatusFlag = b.readSint16LE();
- }
- if (bodyStatusFlag == 1) {
- _rebelStatusBarSprite = 5;
- if (_smush_iconsNut && _smush_iconsNut->getNumChars() > 5) {
- initLaserTexture(_smush_iconsNut, 5);
- }
- debug("Rebel2 Opcode 6 (Handler 7): Status bar enabled (body flag=%d)", bodyStatusFlag);
+ // Used for Level 3 and similar space combat levels.
+
+ // Set control mode: DAT_004437c0 = param_5[3] = par4 in FUN_40C3CC case 4.
+ // This determines collision mode and shooting capability:
+ // Mode 0: Obstacle avoidance - SECONDARY zones, corridor boundaries
+ // Mode 1: Tunnel flight - PRIMARY zones, per-edge push-back (hMargin=0x28)
+ // Mode 2: Combat mode - shooting ENABLED, SECONDARY zones
+ // Mode 3: Tunnel flight - PRIMARY zones, per-edge push-back (hMargin=0x0f)
+ _flyControlMode = par4;
+ debug("Rebel2 Opcode 6 (Handler 7): Control mode set to %d (shooting %s)",
+ par4, (par4 == 2) ? "ENABLED" : "DISABLED");
+
+ // Status bar: param_5[4] == 1 in original (first body word, 5th IACT word)
+ // In our parsing, par3 maps to param_5[2] and the body follows par4.
+ // FUN_40C3CC: if (param_5[4] == 1) FUN_0040bb87(DAT_0047a828,5);
+ // par3 is param_5[2], which the original doesn't use here.
+ // The body word for status bar is read separately below.
+ int16 bodyStatusFlag = 0;
+ if (b.size() - b.pos() >= 2) {
+ bodyStatusFlag = b.readSint16LE();
+ }
+ if (bodyStatusFlag == 1) {
+ _rebelStatusBarSprite = 5;
+ if (_smush_iconsNut && _smush_iconsNut->getNumChars() > 5) {
+ initLaserTexture(_smush_iconsNut, 5);
}
+ debug("Rebel2 Opcode 6 (Handler 7): Status bar enabled (body flag=%d)", bodyStatusFlag);
+ }
- // ------------------------------------------------------------
- // Ship position update â FUN_40C3CC case 4, lines 49-327
- // ------------------------------------------------------------
- // Velocity-based physics with momentum/inertia:
- // Mouse offset from center â scaled input [-127,127]
- // â velocity history averaging â physics delta (clamped ±12/frame)
- // â position clamping â corridor collision â perspective offsets
- //
- // Level data table (DAT_0047e0e8 + level*0x242 + difficulty*0x22):
- // offset 0: smoothing param (>>4 +1 = window size)
- // offset 2: Y speed offset 4: X speed (levelSpeed)
- // offset 6: wind multiplier offset 14: corridor damage
- // We don't have the actual level data, so we use calibrated defaults.
-
- // --- Step 1: Mouse input as offset from screen center ---
- // DAT_0047a7e0 = mouseX - 160, DAT_0047a7e2 = mouseY - 100
- // _vm->_mouse.x/y are in virtual screen coords (0-319, 0-199)
- // consistent with handler 8 which uses _vm->_mouse.x directly.
- Common::Point aimPos = getGameplayAimPoint();
- int16 inputX = (int16)(aimPos.x - 160); // DAT_0047a7e0
- int16 inputY = (int16)(aimPos.y - 100); // DAT_0047a7e2
-
- // Clamp: mouse mode uses [-160, 160] for X, [-127, 127] for Y (lines 55-70)
- if (inputX > 160)
- inputX = 160;
- if (inputX < -160)
- inputX = -160;
- if (inputY > 127)
- inputY = 127;
- if (inputY < -127)
- inputY = -127;
-
- // --- Step 2: Scale to [-127, 127] (lines 82-84) ---
- // Mouse mode: scaledInputX = (DAT_0047a7e0 * 0x7f) / 0xa0
- int16 scaledInputX = (int16)((inputX * 127) / 160);
- int16 scaledInputY = inputY; // Y already in [-127, 127]
-
- // --- Step 3: Velocity history + smoothed average (lines 141-157) ---
- for (int i = 24; i > 0; i--) {
- _velocityHistory[i] = _velocityHistory[i - 1];
- }
- _velocityHistory[0] = scaledInputX;
-
- // Window size = (levelData[0] >> 4) + 1. Calibrated default: 5.
- const int smoothWindow = 5;
- int velSum = 0;
- for (int i = 0; i < smoothWindow; i++) {
- velSum += _velocityHistory[i];
- }
- _smoothedVelocity = (int16)(velSum / smoothWindow); // DAT_0044370c
-
- // --- Step 4: Wind history (lines 158-173) ---
- // Wind multiplier comes from level data[6]. Without data, use 0 (no wind).
- const int16 windMult = 0;
- int windSumX = 0, windSumY = 0;
- for (int i = 14; i > 0; i--) {
- _windHistoryX[i] = _windHistoryX[i - 1];
- windSumX += _windHistoryX[i];
- }
- _windHistoryX[0] = _windParamX;
- int16 windEffectX = (int16)((windMult * (windSumX + _windParamX)) / 15);
-
- for (int i = 14; i > 0; i--) {
- _windHistoryY[i] = _windHistoryY[i - 1];
- windSumY += _windHistoryY[i];
- }
- _windHistoryY[0] = _windParamY;
- int16 windEffectY = (int16)((windMult * (windSumY + _windParamY)) / 15);
-
- // --- Step 5: Position delta (lines 174-242) ---
- // levelSpeed (offset 4): calibrated so max velocity (127) â delta 12.
- // 8 = (speed * 127) >> 9 â speed â 32
- // levelYSpeed (offset 2): calibrated so max input (127) â delta ~6.
- // 6 = (speed * 127) >> 10 â speed â 48
- const int16 levelSpeed = 32;
- const int16 levelYSpeed = 48;
- int16 absSmoothVel = ABS(_smoothedVelocity);
- int16 positionDeltaX;
-
- if (_flyControlMode == 1) {
- // Mode 1: Full cross-axis coupling (lines 174-186)
- // Banking: vertical input deflects horizontal movement
- if (scaledInputX < 1) {
- positionDeltaX = (int16)((levelSpeed * _smoothedVelocity - absSmoothVel * scaledInputY - windEffectX) >> 9);
- } else {
- positionDeltaX = (int16)((levelSpeed * _smoothedVelocity + absSmoothVel * scaledInputY - windEffectX) >> 9);
- }
+ // Ship position update - FUN_40C3CC case 4, lines 49-327.
+ // Velocity-based physics with momentum/inertia:
+ // Mouse offset from center -> scaled input [-127,127]
+ // -> velocity history averaging -> physics delta (clamped +/-12/frame)
+ // -> position clamping -> corridor collision -> perspective offsets
+ //
+ // Level data table (DAT_0047e0e8 + level*0x242 + difficulty*0x22):
+ // offset 0: smoothing param (>>4 +1 = window size)
+ // offset 2: Y speed offset 4: X speed (levelSpeed)
+ // offset 6: wind multiplier offset 14: corridor damage
+ // We don't have the actual level data, so we use calibrated defaults.
+
+ // Step 1: Mouse input as offset from screen center.
+ // DAT_0047a7e0 = mouseX - 160, DAT_0047a7e2 = mouseY - 100.
+ // _vm->_mouse.x/y are in virtual screen coords (0-319, 0-199)
+ // consistent with handler 8 which uses _vm->_mouse.x directly.
+ Common::Point aimPos = getGameplayAimPoint();
+ int16 inputX = (int16)(aimPos.x - 160); // DAT_0047a7e0
+ int16 inputY = (int16)(aimPos.y - 100); // DAT_0047a7e2
+
+ // Clamp: mouse mode uses [-160, 160] for X, [-127, 127] for Y (lines 55-70).
+ if (inputX > 160)
+ inputX = 160;
+ if (inputX < -160)
+ inputX = -160;
+ if (inputY > 127)
+ inputY = 127;
+ if (inputY < -127)
+ inputY = -127;
+
+ // Step 2: Scale to [-127, 127] (lines 82-84).
+ // Mouse mode: scaledInputX = (DAT_0047a7e0 * 0x7f) / 0xa0.
+ int16 scaledInputX = (int16)((inputX * 127) / 160);
+ int16 scaledInputY = inputY; // Y already in [-127, 127]
+
+ // Step 3: Velocity history + smoothed average (lines 141-157).
+ for (int i = 24; i > 0; i--) {
+ _velocityHistory[i] = _velocityHistory[i - 1];
+ }
+ _velocityHistory[0] = scaledInputX;
+
+ // Window size = (levelData[0] >> 4) + 1. Calibrated default: 5.
+ const int smoothWindow = 5;
+ int velSum = 0;
+ for (int i = 0; i < smoothWindow; i++) {
+ velSum += _velocityHistory[i];
+ }
+ _smoothedVelocity = (int16)(velSum / smoothWindow); // DAT_0044370c
+
+ // Step 4: Wind history (lines 158-173).
+ // Wind multiplier comes from level data[6]. Without data, use 0 (no wind).
+ const int16 windMult = 0;
+ int windSumX = 0, windSumY = 0;
+ for (int i = 14; i > 0; i--) {
+ _windHistoryX[i] = _windHistoryX[i - 1];
+ windSumX += _windHistoryX[i];
+ }
+ _windHistoryX[0] = _windParamX;
+ int16 windEffectX = (int16)((windMult * (windSumX + _windParamX)) / 15);
+
+ for (int i = 14; i > 0; i--) {
+ _windHistoryY[i] = _windHistoryY[i - 1];
+ windSumY += _windHistoryY[i];
+ }
+ _windHistoryY[0] = _windParamY;
+ int16 windEffectY = (int16)((windMult * (windSumY + _windParamY)) / 15);
+
+ // Step 5: Position delta (lines 174-242).
+ // levelSpeed (offset 4): calibrated so max velocity (127) -> delta 12.
+ // 8 = (speed * 127) >> 9 -> speed around 32
+ // levelYSpeed (offset 2): calibrated so max input (127) -> delta ~6.
+ // 6 = (speed * 127) >> 10 -> speed around 48
+ const int16 levelSpeed = 32;
+ const int16 levelYSpeed = 48;
+ int16 absSmoothVel = ABS(_smoothedVelocity);
+ int16 positionDeltaX;
+
+ if (_flyControlMode == 1) {
+ // Mode 1: Full cross-axis coupling (lines 174-186).
+ // Banking: vertical input deflects horizontal movement.
+ if (scaledInputX < 1) {
+ positionDeltaX = (int16)((levelSpeed * _smoothedVelocity - absSmoothVel * scaledInputY - windEffectX) >> 9);
} else {
- // Mode 0/2/3: Reduced cross-axis coupling (lines 218-230)
- if (scaledInputX < 1) {
- positionDeltaX = (int16)((levelSpeed * _smoothedVelocity - (absSmoothVel * scaledInputY >> 2) - windEffectX) >> 9);
- } else {
- positionDeltaX = (int16)((levelSpeed * _smoothedVelocity + (absSmoothVel * scaledInputY >> 2) - windEffectX) >> 9);
- }
+ positionDeltaX = (int16)((levelSpeed * _smoothedVelocity + absSmoothVel * scaledInputY - windEffectX) >> 9);
}
-
- // Clamp X delta to ±12 per frame (lines 187-192 / 231-236)
- if (positionDeltaX < -11)
- positionDeltaX = -12;
- if (positionDeltaX > 11)
- positionDeltaX = 12;
-
- // Apply X delta (line 193 / 237)
- _flyShipScreenX += positionDeltaX;
-
- // Y delta
- if (_flyControlMode == 1) {
- // Mode 1: clamped to ±12 with wind (lines 194-216)
- int yCalc = levelYSpeed * scaledInputY - (windEffectY >> 1);
- int yDelta = yCalc >> 10;
- if (yDelta < -12)
- yDelta = -12;
- if (yDelta > 12)
- yDelta = 12;
- _flyShipScreenY -= (int16)yDelta;
+ } else {
+ // Mode 0/2/3: Reduced cross-axis coupling (lines 218-230).
+ if (scaledInputX < 1) {
+ positionDeltaX = (int16)((levelSpeed * _smoothedVelocity - (absSmoothVel * scaledInputY >> 2) - windEffectX) >> 9);
} else {
- // Mode 0/2/3: unclamped (lines 238-241)
- _flyShipScreenY -= (int16)((levelYSpeed * scaledInputY) >> 10);
- }
-
- // Store vertical input for direction sprite (line 243)
- _verticalInput = scaledInputY; // DAT_0044370e
-
- // Ship facing direction (line 244)
- _facingRight = (0xd4 < _smoothedVelocity + _flyShipScreenX);
-
- // --- Step 6: Position clamping (lines 245-256) ---
- if (_flyShipScreenX > 0x194)
- _flyShipScreenX = 0x194; // 404
- if (_flyShipScreenY > 0xF0)
- _flyShipScreenY = 0xF0; // 240
- if (_flyShipScreenX < 0x14)
- _flyShipScreenX = 0x14; // 20
- if (_flyShipScreenY < 0x14)
- _flyShipScreenY = 0x14; // 20
-
- // --- Step 7: Corridor collision â mode 0/2 only (lines 257-292) ---
- if (_flyControlMode == 0 || _flyControlMode == 2) {
- LevelDifficultyParams wallParams = getDifficultyParams();
- int corridorWallDmg = (wallParams.dodgeDamage >= 0) ? wallParams.dodgeDamage : 0;
-
- // Right boundary (lines 258-270)
- // Original: position is ALWAYS clamped; damage/bounce only when cooldown < 5
- if (_corridorRightX < _flyShipScreenX) {
- _flyShipScreenX = _corridorRightX;
- if (_hitCooldown < 5) {
- for (int i = 0; i < 25; i++)
- _velocityHistory[i] = -127;
- _hitCooldown = 10;
- _spaceShotDirection = 1;
- initDamageFlash();
- if (!_rebelInvulnerable) {
- _playerDamage += corridorWallDmg;
- if (_playerDamage > 255)
- _playerDamage = 255;
- }
- _rebelHitCounter++;
- playSfx(1, 127, 100); // CRASH.SAD, right wall â pan right
- }
- }
- // Left boundary (lines 271-283)
- if (_flyShipScreenX < _corridorLeftX) {
- _flyShipScreenX = _corridorLeftX;
- if (_hitCooldown < 5) {
- for (int i = 0; i < 25; i++)
- _velocityHistory[i] = 127;
- _hitCooldown = 10;
- _spaceShotDirection = 0;
- initDamageFlash();
- if (!_rebelInvulnerable) {
- _playerDamage += corridorWallDmg;
- if (_playerDamage > 255)
- _playerDamage = 255;
- }
- _rebelHitCounter++;
- playSfx(1, 127, -100); // CRASH.SAD, left wall â pan left
- }
- }
- // Y boundary clamping â no damage (lines 285-292)
- if (_corridorBottomY < _flyShipScreenY) {
- _flyShipScreenY = _corridorBottomY;
- }
- if (_flyShipScreenY < _corridorTopY) {
- _flyShipScreenY = _corridorTopY;
- }
+ positionDeltaX = (int16)((levelSpeed * _smoothedVelocity + (absSmoothVel * scaledInputY >> 2) - windEffectX) >> 9);
}
+ }
- // --- Step 8: Perspective offsets (lines 293-316) ---
- // f(x) = (focal * center * |offset|) / ((center - focal) * |offset| + focal * center)
- // Close view (DAT_0047a7fc < 1): focalX=0x34, focalY=0x2d
- // Far view (DAT_0047a7fc >= 1): focalX=0x2b, focalY=0x19
- {
- int absOffX = ABS(_flyShipScreenX - 0xd4);
- int16 focalX = 0x2b; // Far view default for Level 3
- if (absOffX > 0) {
- _perspectiveX = (int16)((focalX * 0xd4 * absOffX) /
- ((0xd4 - focalX) * absOffX + focalX * 0xd4));
- } else {
- _perspectiveX = 0;
- }
- if (_flyShipScreenX < 0xd5)
- _perspectiveX = -_perspectiveX;
-
- int absOffY = ABS(_flyShipScreenY - 0x82);
- int16 focalY = 0x19; // Far view default for Level 3
- if (absOffY > 0) {
- _perspectiveY = (int16)((focalY * 0x82 * absOffY) /
- ((0x82 - focalY) * absOffY + focalY * 0x82));
- } else {
- _perspectiveY = 0;
- }
- if (_flyShipScreenY < 0x83)
- _perspectiveY = -_perspectiveY;
- }
-
- // View shift = clamped smoothed velocity (FUN_0040d836 lines 68-74)
- _viewShift = _smoothedVelocity;
- if (_viewShift > 127)
- _viewShift = 127;
- if (_viewShift < -127)
- _viewShift = -127;
-
- // --- Step 9: Direction sprite (FUN_0040d836 lines 88-106) ---
- // 5x7 grid: vDir(0-4) * 7 + hDir(0-6) = sprite index (0-34)
- // vDir from vertical input: (0xa0 - verticalInput) >> 6
- int16 vDir = (int16)(((int)(0xa0 - _verticalInput) + ((0xa0 - _verticalInput) < 0 ? 63 : 0)) >> 6);
- if (vDir < 0)
- vDir = 0;
- if (vDir > 4)
- vDir = 4;
-
- // hDir from smoothed velocity: (0x95 - smoothedVelocity) / 0x2b
- int16 hDir = (int16)((0x95 - _smoothedVelocity) / 0x2b);
- if (hDir < 0)
- hDir = 0;
- if (hDir > 6)
- hDir = 6;
-
- // Hysteresis at center (lines 90-97, 98-105)
- if (hDir == 3 && ABS(_smoothedVelocity) > 10) {
- hDir = (_smoothedVelocity < 1) ? 4 : 2;
- }
- if (vDir == 2 && ABS(_verticalInput) > 15) {
- vDir = (_verticalInput < 1) ? 3 : 1;
- }
-
- _shipDirectionIndex = vDir * 7 + hDir;
- if (_shipDirectionIndex < 0)
- _shipDirectionIndex = 0;
- if (_shipDirectionIndex > 34)
- _shipDirectionIndex = 34;
-
- _shipFiring = (_flyControlMode == 2) &&
- ((_vm->VAR(_vm->VAR_LEFTBTN_HOLD) != 0) ||
- _vm->getActionState(kScummActionInsaneAttack));
-
- debug("Rebel2 H7: pos=(%d,%d) vel=%d vIn=%d dx=%d dir=%d mode=%d",
- _flyShipScreenX, _flyShipScreenY, _smoothedVelocity,
- _verticalInput, positionDeltaX, _shipDirectionIndex, _flyControlMode);
+ // Clamp X delta to +/-12 per frame (lines 187-192 / 231-236).
+ if (positionDeltaX < -11)
+ positionDeltaX = -12;
+ if (positionDeltaX > 11)
+ positionDeltaX = 12;
- return;
- }
+ // Apply X delta (line 193 / 237).
+ _flyShipScreenX += positionDeltaX;
- // Handler 25 (0x19) specific logic (mixed mode - speeder bike)
- // Based on FUN_0041cadb case 4 (opcode 6) lines 113-229
- if (_rebelHandler == 25) {
- // Read the reset flag from IACT data at offset 8-9 (local_14[4] in decompiled code)
- // The stream position should be at offset 8 after par4 was read
- // From FUN_0041cadb line 114: if (local_14[4] == 1) { ... reset ... }
- int16 par5 = 0;
- if (b.pos() + 2 <= b.size()) {
- int64 savedPos = b.pos();
- par5 = b.readSint16LE();
- b.seek(savedPos); // Don't consume the stream
- }
-
- // If par5 == 1, enable status bar and reset state (lines 114-121)
- // Note: This is local_14[4] in the decompiled code, NOT local_14[3] (par4)
- if (par5 == 1) {
- _rebelStatusBarSprite = 5;
- if (_smush_iconsNut && _smush_iconsNut->getNumChars() > 5) {
- initLaserTexture(_smush_iconsNut, 5);
- }
- // Guard with _rebelOp6Initialized: runs once per wave video, not per frame.
- if (!_rebelOp6Initialized) {
- clearBit(0);
- for (int i = 0; i < 512; i++) {
- _rebelLinks[i][0] = 0;
- _rebelLinks[i][1] = 0;
- _rebelLinks[i][2] = 0;
+ // Y delta.
+ if (_flyControlMode == 1) {
+ // Mode 1: clamped to +/-12 with wind (lines 194-216).
+ int yCalc = levelYSpeed * scaledInputY - (windEffectY >> 1);
+ int yDelta = yCalc >> 10;
+ if (yDelta < -12)
+ yDelta = -12;
+ if (yDelta > 12)
+ yDelta = 12;
+ _flyShipScreenY -= (int16)yDelta;
+ } else {
+ // Mode 0/2/3: unclamped (lines 238-241).
+ _flyShipScreenY -= (int16)((levelYSpeed * scaledInputY) >> 10);
+ }
+
+ // Store vertical input for direction sprite (line 243).
+ _verticalInput = scaledInputY; // DAT_0044370e
+
+ // Ship facing direction (line 244).
+ _facingRight = (0xd4 < _smoothedVelocity + _flyShipScreenX);
+
+ // Step 6: Position clamping (lines 245-256).
+ if (_flyShipScreenX > 0x194)
+ _flyShipScreenX = 0x194; // 404
+ if (_flyShipScreenY > 0xF0)
+ _flyShipScreenY = 0xF0; // 240
+ if (_flyShipScreenX < 0x14)
+ _flyShipScreenX = 0x14; // 20
+ if (_flyShipScreenY < 0x14)
+ _flyShipScreenY = 0x14; // 20
+
+ // Step 7: Corridor collision - mode 0/2 only (lines 257-292).
+ if (_flyControlMode == 0 || _flyControlMode == 2) {
+ LevelDifficultyParams wallParams = getDifficultyParams();
+ int corridorWallDmg = (wallParams.dodgeDamage >= 0) ? wallParams.dodgeDamage : 0;
+
+ // Right boundary (lines 258-270).
+ // Original: position is ALWAYS clamped; damage/bounce only when cooldown < 5.
+ if (_corridorRightX < _flyShipScreenX) {
+ _flyShipScreenX = _corridorRightX;
+ if (_hitCooldown < 5) {
+ for (int i = 0; i < 25; i++)
+ _velocityHistory[i] = -127;
+ _hitCooldown = 10;
+ _spaceShotDirection = 1;
+ initDamageFlash();
+ if (!_rebelInvulnerable) {
+ _playerDamage += corridorWallDmg;
+ if (_playerDamage > 255)
+ _playerDamage = 255;
}
- _rebelWaveState = _rebelPhaseState;
- _rebelOp6Initialized = true;
- debug("Rebel2 Opcode 6 (Handler 25): Wave init, wave=0x%x autopilot=%d damageLevel=%d",
- _rebelWaveState, _rebelAutopilot, _rebelDamageLevel);
+ _rebelHitCounter++;
+ playSfx(1, 127, 100); // CRASH.SAD, right wall, pan right
}
}
- // Set sprite mode (DAT_00457900 = local_14[3]) - controls which GRD sprite to render
- // From FUN_0041cadb line 122: DAT_00457900 = local_14[3];
- // In ScummVM's IACT parsing: local_14[3] = offset 6-7 = par4
- // Mode 1: Uncovered, shooting position - sprite on left
- // Mode 2: Covered, vertical shift
- // Mode 3: Transition between covered/uncovered - sprite position depends on direction
- // Mode 4: Alternative uncovered position - sprite on right
- _grdSpriteMode = par4; // local_14[3] maps to par4 (offset 6-7)
-
- debug("Rebel2 Handler25 Opcode6: par2=%d par3=%d par4=%d(mode) par5=%d(reset) autopilot=%d damageLevel=%d controlMode=%d",
- par2, par3, par4, par5, _rebelAutopilot, _rebelDamageLevel, _rebelControlMode);
-
- // Autopilot logic (lines 123-146)
- // From original FUN_0041cadb - NO damageLevel check, toggle happens immediately
- // The damage level counter provides the smooth visual transition
- if (!_rebelInvulnerable) {
- if (_rebelAutopilot == 0) {
- // Uncovered: RIGHT button enters cover
- if ((_rebelControlMode & 2) != 0) {
- _rebelAutopilot = 1;
- debug("Rebel2 Handler25: Entering cover (right click), controlMode=%d", _rebelControlMode);
- }
- } else {
- // Covered: ANY button exits cover
- if (_rebelControlMode != 0) {
- _rebelAutopilot = 0;
- debug("Rebel2 Handler25: Exiting cover (button click), controlMode=%d", _rebelControlMode);
+ // Left boundary (lines 271-283).
+ if (_flyShipScreenX < _corridorLeftX) {
+ _flyShipScreenX = _corridorLeftX;
+ if (_hitCooldown < 5) {
+ for (int i = 0; i < 25; i++)
+ _velocityHistory[i] = 127;
+ _hitCooldown = 10;
+ _spaceShotDirection = 0;
+ initDamageFlash();
+ if (!_rebelInvulnerable) {
+ _playerDamage += corridorWallDmg;
+ if (_playerDamage > 255)
+ _playerDamage = 255;
}
+ _rebelHitCounter++;
+ playSfx(1, 127, -100); // CRASH.SAD, left wall, pan left
}
- // Clear control mode after processing (sticky flags consumed)
- _rebelControlMode = 0;
+ }
+
+ // Y boundary clamping - no damage (lines 285-292).
+ if (_corridorBottomY < _flyShipScreenY) {
+ _flyShipScreenY = _corridorBottomY;
+ }
+ if (_flyShipScreenY < _corridorTopY) {
+ _flyShipScreenY = _corridorTopY;
+ }
+ }
+
+ // Step 8: Perspective offsets (lines 293-316).
+ // f(x) = (focal * center * |offset|) / ((center - focal) * |offset| + focal * center)
+ // Close view (DAT_0047a7fc < 1): focalX=0x34, focalY=0x2d.
+ // Far view (DAT_0047a7fc >= 1): focalX=0x2b, focalY=0x19.
+ {
+ int absOffX = ABS(_flyShipScreenX - 0xd4);
+ int16 focalX = 0x2b; // Far view default for Level 3
+ if (absOffX > 0) {
+ _perspectiveX = (int16)((focalX * 0xd4 * absOffX) /
+ ((0xd4 - focalX) * absOffX + focalX * 0xd4));
} else {
- // Invulnerable mode: random autopilot changes
- if (_rebelAutopilot == 0) {
- if (_vm->_rnd.getRandomNumber(100) == 0) {
- _rebelAutopilot = 1;
- }
- } else {
- if (_vm->_rnd.getRandomNumber(15) == 0) {
- _rebelAutopilot = 0;
- _rebelFlightDir = _vm->_rnd.getRandomNumber(2);
- }
+ _perspectiveX = 0;
+ }
+ if (_flyShipScreenX < 0xd5)
+ _perspectiveX = -_perspectiveX;
+
+ int absOffY = ABS(_flyShipScreenY - 0x82);
+ int16 focalY = 0x19; // Far view default for Level 3
+ if (absOffY > 0) {
+ _perspectiveY = (int16)((focalY * 0x82 * absOffY) /
+ ((0x82 - focalY) * absOffY + focalY * 0x82));
+ } else {
+ _perspectiveY = 0;
+ }
+ if (_flyShipScreenY < 0x83)
+ _perspectiveY = -_perspectiveY;
+ }
+
+ // View shift = clamped smoothed velocity (FUN_0040d836 lines 68-74).
+ _viewShift = _smoothedVelocity;
+ if (_viewShift > 127)
+ _viewShift = 127;
+ if (_viewShift < -127)
+ _viewShift = -127;
+
+ // Step 9: Direction sprite (FUN_0040d836 lines 88-106).
+ // 5x7 grid: vDir(0-4) * 7 + hDir(0-6) = sprite index (0-34).
+ // vDir from vertical input: (0xa0 - verticalInput) >> 6.
+ int16 vDir = (int16)(((int)(0xa0 - _verticalInput) + ((0xa0 - _verticalInput) < 0 ? 63 : 0)) >> 6);
+ if (vDir < 0)
+ vDir = 0;
+ if (vDir > 4)
+ vDir = 4;
+
+ // hDir from smoothed velocity: (0x95 - smoothedVelocity) / 0x2b.
+ int16 hDir = (int16)((0x95 - _smoothedVelocity) / 0x2b);
+ if (hDir < 0)
+ hDir = 0;
+ if (hDir > 6)
+ hDir = 6;
+
+ // Hysteresis at center (lines 90-97, 98-105).
+ if (hDir == 3 && ABS(_smoothedVelocity) > 10) {
+ hDir = (_smoothedVelocity < 1) ? 4 : 2;
+ }
+ if (vDir == 2 && ABS(_verticalInput) > 15) {
+ vDir = (_verticalInput < 1) ? 3 : 1;
+ }
+
+ _shipDirectionIndex = vDir * 7 + hDir;
+ if (_shipDirectionIndex < 0)
+ _shipDirectionIndex = 0;
+ if (_shipDirectionIndex > 34)
+ _shipDirectionIndex = 34;
+
+ _shipFiring = (_flyControlMode == 2) &&
+ ((_vm->VAR(_vm->VAR_LEFTBTN_HOLD) != 0) ||
+ _vm->getActionState(kScummActionInsaneAttack));
+
+ debug("Rebel2 H7: pos=(%d,%d) vel=%d vIn=%d dx=%d dir=%d mode=%d",
+ _flyShipScreenX, _flyShipScreenY, _smoothedVelocity,
+ _verticalInput, positionDeltaX, _shipDirectionIndex, _flyControlMode);
+}
+
+// ScummVM refactor helper for opcode 6 Handler 25, not a separate retail function.
+void InsaneRebel2::handleOpcode6Handler25(byte *renderBitmap, Common::SeekableReadStream &b, int16 par2, int16 par3, int16 par4) {
+ // Handler 25 (0x19) specific logic (mixed mode - speeder bike).
+ // Based on FUN_0041cadb case 4 (opcode 6) lines 113-229.
+
+ // Read the reset flag from IACT data at offset 8-9 (local_14[4] in decompiled code).
+ // The stream position should be at offset 8 after par4 was read.
+ // From FUN_0041cadb line 114: if (local_14[4] == 1) { ... reset ... }
+ int16 par5 = 0;
+ if (b.pos() + 2 <= b.size()) {
+ int64 savedPos = b.pos();
+ par5 = b.readSint16LE();
+ b.seek(savedPos); // Don't consume the stream
+ }
+
+ // If par5 == 1, enable status bar and reset state (lines 114-121).
+ // Note: This is local_14[4] in the decompiled code, NOT local_14[3] (par4).
+ if (par5 == 1) {
+ _rebelStatusBarSprite = 5;
+ if (_smush_iconsNut && _smush_iconsNut->getNumChars() > 5) {
+ initLaserTexture(_smush_iconsNut, 5);
+ }
+
+ // Guard with _rebelOp6Initialized: runs once per wave video, not per frame.
+ if (!_rebelOp6Initialized) {
+ clearBit(0);
+ for (int i = 0; i < 512; i++) {
+ _rebelLinks[i][0] = 0;
+ _rebelLinks[i][1] = 0;
+ _rebelLinks[i][2] = 0;
}
+ _rebelWaveState = _rebelPhaseState;
+ _rebelOp6Initialized = true;
+ debug("Rebel2 Opcode 6 (Handler 25): Wave init, wave=0x%x autopilot=%d damageLevel=%d",
+ _rebelWaveState, _rebelAutopilot, _rebelDamageLevel);
}
+ }
- // Update damage level counter (lines 147-154)
- // This provides the smooth transition animation between covered/uncovered states
- int prevDamageLevel = _rebelDamageLevel;
+ // Set sprite mode (DAT_00457900 = local_14[3]) - controls which GRD sprite to render.
+ // From FUN_0041cadb line 122: DAT_00457900 = local_14[3].
+ // In ScummVM's IACT parsing: local_14[3] = offset 6-7 = par4.
+ // Mode 1: Uncovered, shooting position - sprite on left
+ // Mode 2: Covered, vertical shift
+ // Mode 3: Transition between covered/uncovered - sprite position depends on direction
+ // Mode 4: Alternative uncovered position - sprite on right
+ _grdSpriteMode = par4; // local_14[3] maps to par4 (offset 6-7)
+
+ debug("Rebel2 Handler25 Opcode6: par2=%d par3=%d par4=%d(mode) par5=%d(reset) autopilot=%d damageLevel=%d controlMode=%d",
+ par2, par3, par4, par5, _rebelAutopilot, _rebelDamageLevel, _rebelControlMode);
+
+ // Autopilot logic (lines 123-146).
+ // From original FUN_0041cadb - NO damageLevel check, toggle happens immediately.
+ // The damage level counter provides the smooth visual transition.
+ if (!_rebelInvulnerable) {
if (_rebelAutopilot == 0) {
- // Uncovered: decrement damage level towards 0
- if (_rebelDamageLevel > 0) {
- _rebelDamageLevel--;
+ // Uncovered: RIGHT button enters cover.
+ if ((_rebelControlMode & 2) != 0) {
+ _rebelAutopilot = 1;
+ debug("Rebel2 Handler25: Entering cover (right click), controlMode=%d", _rebelControlMode);
}
} else {
- // Covered: increment damage level towards 5
- if (_rebelDamageLevel < 5) {
- _rebelDamageLevel++;
+ // Covered: ANY button exits cover.
+ if (_rebelControlMode != 0) {
+ _rebelAutopilot = 0;
+ debug("Rebel2 Handler25: Exiting cover (button click), controlMode=%d", _rebelControlMode);
}
}
- if (_rebelDamageLevel != prevDamageLevel) {
- debug("Rebel2 Handler25: damageLevel transition %d -> %d (autopilot=%d)",
- prevDamageLevel, _rebelDamageLevel, _rebelAutopilot);
- }
- // Flight direction logic for mode 3 (lines 155-177)
- if (_grdSpriteMode == 3) {
- if (_rebelDamageLevel == 5) {
- // At max damage, check for direction change input
- int16 mouseX = getGameplayAimPoint().x;
- if (_player && _player->_width > 320) {
- mouseX = (mouseX * 320) / _player->_width;
- }
- if (mouseX > 235) { // 0x4b + 160 = 235
- _rebelFlightDir = 1;
- }
- if (mouseX < 85) { // 160 - 0x4b = 85
- _rebelFlightDir = 0;
- }
+ // Clear control mode after processing (sticky flags consumed).
+ _rebelControlMode = 0;
+ } else {
+ // Invulnerable mode: random autopilot changes.
+ if (_rebelAutopilot == 0) {
+ if (_vm->_rnd.getRandomNumber(100) == 0) {
+ _rebelAutopilot = 1;
}
} else {
- _rebelFlightDir = 0;
- }
-
- // Calculate sprite and view offset positions based on mode (lines 182-213)
- // DAT_0045790c = view offset X (for corridor overlay)
- // DAT_0045790e = view offset Y (for corridor overlay)
- // DAT_00457910 = sprite position X (relative to center)
- // DAT_00457912 = sprite position Y (relative to center)
- if (_grdSpriteMode == 1) {
- // Mode 1: Uncovered, shooting - sprite shifts left as damage increases
- _rebelViewMode1 = 0x0e;
- _rebelViewMode2 = 0;
- _rebelViewOffsetX = _rebelDamageLevel * -5 + -14; // DAT_0045790c
- _rebelViewOffset2X = _rebelDamageLevel * -22; // DAT_00457910
- _rebelViewOffsetY = 0; // DAT_0045790e
- _rebelViewOffset2Y = 0; // DAT_00457912
- } else if (_grdSpriteMode == 4) {
- // Mode 4: Alternative uncovered - sprite shifts right
- _rebelViewMode1 = 0x22;
- _rebelViewMode2 = 0;
- _rebelViewOffsetX = _rebelDamageLevel * 10 + -16; // DAT_0045790c
- _rebelViewOffset2X = _rebelDamageLevel * 17 + -85; // DAT_00457910 (0x11 = 17, -0x55 = -85)
- _rebelViewOffsetY = 0;
- _rebelViewOffset2Y = 0;
- } else if (_grdSpriteMode == 2) {
- // Mode 2: Covered - vertical shift
- _rebelViewMode1 = 0;
- _rebelViewMode2 = 0x0e;
- _rebelViewOffsetY = _rebelDamageLevel * -5 + -14; // DAT_0045790e
- _rebelViewOffset2Y = (5 - _rebelDamageLevel) * 15 + -60; // DAT_00457912 (0xf = 15, -0x3c = -60)
- _rebelViewOffsetX = 0;
- _rebelViewOffset2X = 0;
- } else if (_grdSpriteMode == 3) {
- // Mode 3: Transition - direction-dependent horizontal shift
- _rebelViewMode1 = 0x0f;
- _rebelViewMode2 = 0;
- // (-(DAT_00457902 == 0) & 6) - 3 = if dir==0: 6-3=3, else 0-3=-3
- int16 dirMultX = (_rebelFlightDir == 0) ? 3 : -3;
- // (-(DAT_00457902 == 0) & 0x28) - 0x14 = if dir==0: 40-20=20, else 0-20=-20
- int16 dirMultX2 = (_rebelFlightDir == 0) ? 20 : -20;
- _rebelViewOffsetX = dirMultX * (5 - _rebelDamageLevel) + -15; // DAT_0045790c
- _rebelViewOffset2X = dirMultX2 * (5 - _rebelDamageLevel); // DAT_00457910
- _rebelViewOffsetY = 0;
- _rebelViewOffset2Y = 0;
- } else {
- // Mode 0 or unknown: use Mode 1 defaults as fallback
- _rebelViewMode1 = 0x0e;
- _rebelViewMode2 = 0;
- _rebelViewOffsetX = _rebelDamageLevel * -5 + -14;
- _rebelViewOffset2X = _rebelDamageLevel * -22;
- _rebelViewOffsetY = 0;
- _rebelViewOffset2Y = 0;
- debug("Rebel2 Opcode 6 (Handler 25): Unknown mode %d, using Mode 1 fallback", _grdSpriteMode);
+ if (_vm->_rnd.getRandomNumber(15) == 0) {
+ _rebelAutopilot = 0;
+ _rebelFlightDir = _vm->_rnd.getRandomNumber(2);
+ }
}
+ }
- debug("Rebel2 Opcode 6 (Handler 25): mode=%d damage=%d dir=%d autopilot=%d viewOff=(%d,%d) spritePos=(%d,%d)",
- _grdSpriteMode, _rebelDamageLevel, _rebelFlightDir, _rebelAutopilot,
- _rebelViewOffsetX, _rebelViewOffsetY, _rebelViewOffset2X, _rebelViewOffset2Y);
+ // Update damage level counter (lines 147-154).
+ // This provides the smooth transition animation between covered/uncovered states.
+ int prevDamageLevel = _rebelDamageLevel;
+ if (_rebelAutopilot == 0) {
+ // Uncovered: decrement damage level towards 0.
+ if (_rebelDamageLevel > 0) {
+ _rebelDamageLevel--;
+ }
+ } else {
+ // Covered: increment damage level towards 5.
+ if (_rebelDamageLevel < 5) {
+ _rebelDamageLevel++;
+ }
+ }
+ if (_rebelDamageLevel != prevDamageLevel) {
+ debug("Rebel2 Handler25: damageLevel transition %d -> %d (autopilot=%d)",
+ prevDamageLevel, _rebelDamageLevel, _rebelAutopilot);
+ }
- // Set FOBJ position offsets (FUN_00424510 in original, line 214)
- // All subsequent FOBJs in this frame will be shifted by these offsets
- if (_player) {
- _player->_fobjOffsetX = _rebelViewOffsetX;
- _player->_fobjOffsetY = _rebelViewOffsetY;
+ // Flight direction logic for mode 3 (lines 155-177).
+ if (_grdSpriteMode == 3) {
+ if (_rebelDamageLevel == 5) {
+ // At max damage, check for direction change input.
+ int16 mouseX = getGameplayAimPoint().x;
+ if (_player && _player->_width > 320) {
+ mouseX = (mouseX * 320) / _player->_width;
+ }
+ if (mouseX > 235) { // 0x4b + 160 = 235
+ _rebelFlightDir = 1;
+ }
+ if (mouseX < 85) { // 160 - 0x4b = 85
+ _rebelFlightDir = 0;
+ }
}
+ } else {
+ _rebelFlightDir = 0;
+ }
- // Draw corridor overlay OPAQUELY (FUN_00428A10 in original, line 216)
- // This wipes previous frame content so codec 23 delta skip regions show clean corridor
- if (renderBitmap) {
- EmbeddedSanFrame &corridorOverlay = _rebelEmbeddedHud[4];
- if (corridorOverlay.valid && corridorOverlay.pixels) {
- int pitch = (_player && _player->_width > 0) ? _player->_width : 320;
- int bufHeight = (_player && _player->_height > 0) ? _player->_height : 200;
-
- int srcOffsetX = 0;
- int srcOffsetY = 0;
- int destX = _rebelViewOffsetX;
- int destY = _rebelViewOffsetY;
- int drawWidth = corridorOverlay.width;
- int drawHeight = corridorOverlay.height;
-
- if (destX < 0) { srcOffsetX = -destX; drawWidth -= srcOffsetX; destX = 0; }
- if (destY < 0) { srcOffsetY = -destY; drawHeight -= srcOffsetY; destY = 0; }
- if (destX + drawWidth > pitch)
- drawWidth = pitch - destX;
- if (destY + drawHeight > bufHeight)
- drawHeight = bufHeight - destY;
- if (drawWidth > corridorOverlay.width - srcOffsetX)
- drawWidth = corridorOverlay.width - srcOffsetX;
- if (drawHeight > corridorOverlay.height - srcOffsetY)
- drawHeight = corridorOverlay.height - srcOffsetY;
-
- if (drawWidth > 0 && drawHeight > 0) {
- for (int y = 0; y < drawHeight; y++) {
- memcpy(renderBitmap + (destY + y) * pitch + destX,
- corridorOverlay.pixels + (srcOffsetY + y) * corridorOverlay.width + srcOffsetX,
- drawWidth);
- }
+ // Calculate sprite and view offset positions based on mode (lines 182-213).
+ // DAT_0045790c = view offset X (for corridor overlay)
+ // DAT_0045790e = view offset Y (for corridor overlay)
+ // DAT_00457910 = sprite position X (relative to center)
+ // DAT_00457912 = sprite position Y (relative to center)
+ if (_grdSpriteMode == 1) {
+ // Mode 1: Uncovered, shooting - sprite shifts left as damage increases.
+ _rebelViewMode1 = 0x0e;
+ _rebelViewMode2 = 0;
+ _rebelViewOffsetX = _rebelDamageLevel * -5 + -14; // DAT_0045790c
+ _rebelViewOffset2X = _rebelDamageLevel * -22; // DAT_00457910
+ _rebelViewOffsetY = 0; // DAT_0045790e
+ _rebelViewOffset2Y = 0; // DAT_00457912
+ } else if (_grdSpriteMode == 4) {
+ // Mode 4: Alternative uncovered - sprite shifts right.
+ _rebelViewMode1 = 0x22;
+ _rebelViewMode2 = 0;
+ _rebelViewOffsetX = _rebelDamageLevel * 10 + -16; // DAT_0045790c
+ _rebelViewOffset2X = _rebelDamageLevel * 17 + -85; // DAT_00457910 (0x11 = 17, -0x55 = -85)
+ _rebelViewOffsetY = 0;
+ _rebelViewOffset2Y = 0;
+ } else if (_grdSpriteMode == 2) {
+ // Mode 2: Covered - vertical shift.
+ _rebelViewMode1 = 0;
+ _rebelViewMode2 = 0x0e;
+ _rebelViewOffsetY = _rebelDamageLevel * -5 + -14; // DAT_0045790e
+ _rebelViewOffset2Y = (5 - _rebelDamageLevel) * 15 + -60; // DAT_00457912 (0xf = 15, -0x3c = -60)
+ _rebelViewOffsetX = 0;
+ _rebelViewOffset2X = 0;
+ } else if (_grdSpriteMode == 3) {
+ // Mode 3: Transition - direction-dependent horizontal shift.
+ _rebelViewMode1 = 0x0f;
+ _rebelViewMode2 = 0;
+ // (-(DAT_00457902 == 0) & 6) - 3 = if dir==0: 6-3=3, else 0-3=-3
+ int16 dirMultX = (_rebelFlightDir == 0) ? 3 : -3;
+ // (-(DAT_00457902 == 0) & 0x28) - 0x14 = if dir==0: 40-20=20, else 0-20=-20
+ int16 dirMultX2 = (_rebelFlightDir == 0) ? 20 : -20;
+ _rebelViewOffsetX = dirMultX * (5 - _rebelDamageLevel) + -15; // DAT_0045790c
+ _rebelViewOffset2X = dirMultX2 * (5 - _rebelDamageLevel); // DAT_00457910
+ _rebelViewOffsetY = 0;
+ _rebelViewOffset2Y = 0;
+ } else {
+ // Mode 0 or unknown: use Mode 1 defaults as fallback.
+ _rebelViewMode1 = 0x0e;
+ _rebelViewMode2 = 0;
+ _rebelViewOffsetX = _rebelDamageLevel * -5 + -14;
+ _rebelViewOffset2X = _rebelDamageLevel * -22;
+ _rebelViewOffsetY = 0;
+ _rebelViewOffset2Y = 0;
+ debug("Rebel2 Opcode 6 (Handler 25): Unknown mode %d, using Mode 1 fallback", _grdSpriteMode);
+ }
+
+ debug("Rebel2 Opcode 6 (Handler 25): mode=%d damage=%d dir=%d autopilot=%d viewOff=(%d,%d) spritePos=(%d,%d)",
+ _grdSpriteMode, _rebelDamageLevel, _rebelFlightDir, _rebelAutopilot,
+ _rebelViewOffsetX, _rebelViewOffsetY, _rebelViewOffset2X, _rebelViewOffset2Y);
+
+ // Set FOBJ position offsets (FUN_00424510 in original, line 214).
+ // All subsequent FOBJs in this frame will be shifted by these offsets.
+ if (_player) {
+ _player->_fobjOffsetX = _rebelViewOffsetX;
+ _player->_fobjOffsetY = _rebelViewOffsetY;
+ }
+
+ // Draw corridor overlay OPAQUELY (FUN_00428A10 in original, line 216).
+ // This wipes previous frame content so codec 23 delta skip regions show clean corridor.
+ if (renderBitmap) {
+ EmbeddedSanFrame &corridorOverlay = _rebelEmbeddedHud[4];
+ if (corridorOverlay.valid && corridorOverlay.pixels) {
+ int pitch = (_player && _player->_width > 0) ? _player->_width : 320;
+ int bufHeight = (_player && _player->_height > 0) ? _player->_height : 200;
+
+ int srcOffsetX = 0;
+ int srcOffsetY = 0;
+ int destX = _rebelViewOffsetX;
+ int destY = _rebelViewOffsetY;
+ int drawWidth = corridorOverlay.width;
+ int drawHeight = corridorOverlay.height;
+
+ if (destX < 0) { srcOffsetX = -destX; drawWidth -= srcOffsetX; destX = 0; }
+ if (destY < 0) { srcOffsetY = -destY; drawHeight -= srcOffsetY; destY = 0; }
+ if (destX + drawWidth > pitch)
+ drawWidth = pitch - destX;
+ if (destY + drawHeight > bufHeight)
+ drawHeight = bufHeight - destY;
+ if (drawWidth > corridorOverlay.width - srcOffsetX)
+ drawWidth = corridorOverlay.width - srcOffsetX;
+ if (drawHeight > corridorOverlay.height - srcOffsetY)
+ drawHeight = corridorOverlay.height - srcOffsetY;
+
+ if (drawWidth > 0 && drawHeight > 0) {
+ for (int y = 0; y < drawHeight; y++) {
+ memcpy(renderBitmap + (destY + y) * pitch + destX,
+ corridorOverlay.pixels + (srcOffsetY + y) * corridorOverlay.width + srcOffsetX,
+ drawWidth);
}
- debug("Rebel2 Opcode 6: Corridor overlay drawn at (%d,%d) size(%d,%d)",
- _rebelViewOffsetX, _rebelViewOffsetY, corridorOverlay.width, corridorOverlay.height);
}
+ debug("Rebel2 Opcode 6: Corridor overlay drawn at (%d,%d) size(%d,%d)",
+ _rebelViewOffsetX, _rebelViewOffsetY, corridorOverlay.width, corridorOverlay.height);
}
+ }
+}
- return;
+// ScummVM refactor helper for opcode 6 Handler 0x26, not a separate retail function.
+void InsaneRebel2::handleOpcode6Turret(Common::SeekableReadStream &b, int16 par4) {
+ // Handler 0x26: FUN_407FCB line 77-79 - set level type from par4, read par5 for init trigger.
+ // param_5[3] = par4 = levelType, param_5[4] = par5 = init flag.
+ _rebelLevelType = par4;
+
+ // Read par5 from IACT body (param_5[4]).
+ int16 par5 = 0;
+ if (b.pos() + 2 <= b.size()) {
+ int64 savedPos = b.pos();
+ par5 = b.readSint16LE();
+ b.seek(savedPos);
}
- // Handler 0x26: FUN_407FCB line 77-79 â set level type from par4, read par5 for init trigger
- // param_5[3] = par4 = levelType, param_5[4] = par5 = init flag
- if (_rebelHandler == 0x26) {
- _rebelLevelType = par4;
-
- // Read par5 from IACT body (param_5[4])
- int16 par5 = 0;
- if (b.pos() + 2 <= b.size()) {
- int64 savedPos = b.pos();
- par5 = b.readSint16LE();
- b.seek(savedPos);
- }
-
- if (par5 == 1) {
- // Re-render laser texture for this level (FUN_0040bb87)
- // levelType 5 uses sprite 53, all others use sprite 5
- _rebelStatusBarSprite = (_rebelLevelType == 5) ? 53 : 5;
- if (_smush_iconsNut && _smush_iconsNut->getNumChars() > _rebelStatusBarSprite) {
- initLaserTexture(_smush_iconsNut, _rebelStatusBarSprite);
- }
+ if (par5 == 1) {
+ // Re-render laser texture for this level (FUN_0040bb87).
+ // levelType 5 uses sprite 53, all others use sprite 5.
+ _rebelStatusBarSprite = (_rebelLevelType == 5) ? 53 : 5;
+ if (_smush_iconsNut && _smush_iconsNut->getNumChars() > _rebelStatusBarSprite) {
+ initLaserTexture(_smush_iconsNut, _rebelStatusBarSprite);
+ }
- if (!_rebelOp6Initialized) {
- clearBit(0);
- for (int i = 0; i < 512; i++) {
- _rebelLinks[i][0] = 0;
- _rebelLinks[i][1] = 0;
- _rebelLinks[i][2] = 0;
- }
- _rebelWaveState = _rebelPhaseState;
- _rebelHitCounter = 0;
- _rebelOp6Initialized = true;
- debug("Rebel2 Opcode 6 (Handler 0x26): Wave init, levelType=%d waveState=0x%x",
- _rebelLevelType, _rebelWaveState);
+ if (!_rebelOp6Initialized) {
+ clearBit(0);
+ for (int i = 0; i < 512; i++) {
+ _rebelLinks[i][0] = 0;
+ _rebelLinks[i][1] = 0;
+ _rebelLinks[i][2] = 0;
}
+ _rebelWaveState = _rebelPhaseState;
+ _rebelHitCounter = 0;
+ _rebelOp6Initialized = true;
+ debug("Rebel2 Opcode 6 (Handler 0x26): Wave init, levelType=%d waveState=0x%x",
+ _rebelLevelType, _rebelWaveState);
}
}
+}
- // Other handlers: par4 == 1 triggers init (NOT level type)
+// ScummVM refactor helper for opcode 6 generic init, not a separate retail function.
+void InsaneRebel2::handleOpcode6GenericInit(int16 par4) {
+ // Other handlers: par4 == 1 triggers init (NOT level type).
if (_rebelHandler != 0x26 && par4 == 1) {
_rebelStatusBarSprite = 5;
if (_smush_iconsNut && _smush_iconsNut->getNumChars() > 5) {
@@ -1352,11 +1335,14 @@ void InsaneRebel2::iactRebel2Opcode6(byte *renderBitmap, Common::SeekableReadStr
debug("Rebel2 Opcode 6: Wave init - cleared bits/links, waveState=0x%x", _rebelWaveState);
}
}
+}
+// ScummVM refactor helper for opcode 6 generic flight state, not a separate retail function.
+void InsaneRebel2::updateOpcode6GenericFlightState() {
// Step 3: Autopilot/control mode logic (lines 123-146)
- // This determines whether the ship flies on autopilot or manual control
+ // This determines whether the ship flies on autopilot or manual control.
if (!_rebelInvulnerable) {
- // Normal mode: check control mode flags
+ // Normal mode: check control mode flags.
if (_rebelAutopilot == 0) {
if ((_rebelControlMode & 2) != 0) {
_rebelAutopilot = 1;
@@ -1367,7 +1353,7 @@ void InsaneRebel2::iactRebel2Opcode6(byte *renderBitmap, Common::SeekableReadStr
}
}
} else {
- // Invulnerable mode: random autopilot changes
+ // Invulnerable mode: random autopilot changes.
if (_rebelAutopilot == 0) {
if (_vm->_rnd.getRandomNumber(100) == 0) {
_rebelAutopilot = 1;
@@ -1380,7 +1366,7 @@ void InsaneRebel2::iactRebel2Opcode6(byte *renderBitmap, Common::SeekableReadStr
}
}
- // Step 4: Update damage level counter (lines 147-154)
+ // Step 4: Update damage level counter (lines 147-154).
if (_rebelAutopilot == 0) {
if (_rebelDamageLevel > 0) {
_rebelDamageLevel--;
@@ -1391,11 +1377,11 @@ void InsaneRebel2::iactRebel2Opcode6(byte *renderBitmap, Common::SeekableReadStr
}
}
- // Handle level type 3 special direction logic (lines 155-181)
+ // Handle level type 3 special direction logic (lines 155-181).
if (_rebelLevelType == 3) {
if (_rebelDamageLevel == 5) {
- // Check for joystick/key input to change direction
- // Simplified: use mouse position
+ // Check for joystick/key input to change direction.
+ // Simplified: use mouse position.
int16 mouseX = getGameplayAimPoint().x;
if (mouseX > 235) {
_rebelFlightDir = 1;
@@ -1408,10 +1394,10 @@ void InsaneRebel2::iactRebel2Opcode6(byte *renderBitmap, Common::SeekableReadStr
_rebelFlightDir = 0;
}
- // Step 5: Calculate view offsets based on level type (lines 182-213)
+ // Step 5: Calculate view offsets based on level type (lines 182-213).
switch (_rebelLevelType) {
case 1:
- // Type 1: Vertical movement
+ // Type 1: Vertical movement.
_rebelViewMode1 = 0x0e;
_rebelViewMode2 = 0;
_rebelViewOffsetX = _rebelDamageLevel * -5 - 0x0e;
@@ -1421,7 +1407,7 @@ void InsaneRebel2::iactRebel2Opcode6(byte *renderBitmap, Common::SeekableReadStr
break;
case 4:
- // Type 4: Different vertical movement
+ // Type 4: Different vertical movement.
_rebelViewMode1 = 0x22;
_rebelViewMode2 = 0;
_rebelViewOffsetX = _rebelDamageLevel * 10 - 0x10;
@@ -1431,7 +1417,7 @@ void InsaneRebel2::iactRebel2Opcode6(byte *renderBitmap, Common::SeekableReadStr
break;
case 2:
- // Type 2: Horizontal movement
+ // Type 2: Horizontal movement.
_rebelViewMode1 = 0;
_rebelViewMode2 = 0x0e;
_rebelViewOffsetY = _rebelDamageLevel * -5 - 0x0e;
@@ -1441,7 +1427,7 @@ void InsaneRebel2::iactRebel2Opcode6(byte *renderBitmap, Common::SeekableReadStr
break;
case 3:
- // Type 3: Direction-based movement
+ // Type 3: Direction-based movement.
_rebelViewMode1 = 0x0f;
_rebelViewMode2 = 0;
{
@@ -1455,7 +1441,7 @@ void InsaneRebel2::iactRebel2Opcode6(byte *renderBitmap, Common::SeekableReadStr
break;
default:
- // Default: No special offsets
+ // Default: No special offsets.
_rebelViewMode1 = 0;
_rebelViewMode2 = 0;
_rebelViewOffsetX = 0;
@@ -1467,45 +1453,96 @@ void InsaneRebel2::iactRebel2Opcode6(byte *renderBitmap, Common::SeekableReadStr
debug("Rebel2 Opcode 6: levelType=%d autopilot=%d damageLevel=%d viewOffset=(%d,%d)",
_rebelLevelType, _rebelAutopilot, _rebelDamageLevel, _rebelViewOffsetX, _rebelViewOffsetY);
+}
- // Detect and load embedded ANIM (SAN) within the remaining IACT payload
- // Note: chunkSize is the remaining IACT payload size after par1-par4 header
- {
- int64 startPos = b.pos();
- // Use chunkSize (remaining IACT payload) rather than b.size() (entire FRME stream)
- int64 remaining = chunkSize;
- if (remaining > 0) {
- int scanSize = (int)MIN<int64>(remaining, 65536);
- byte *scanBuf = (byte *)malloc(scanSize);
- if (scanBuf) {
- int bytesRead = b.read(scanBuf, scanSize);
- for (int i = 0; i + 8 <= bytesRead; ++i) {
- if (READ_BE_UINT32(scanBuf + i) == MKTAG('A','N','I','M')) {
- int64 animStreamPos = startPos + i;
- uint32 animReportedSize = READ_BE_UINT32(scanBuf + i + 4);
- // Limit to remaining IACT payload (chunkSize - offset into payload)
- int32 toCopy = (int)MIN<int64>((int64)animReportedSize + 8, chunkSize - i);
- if (toCopy > 0) {
- byte *animData = (byte *)malloc(toCopy);
- if (animData) {
- b.seek(animStreamPos);
- b.read(animData, toCopy);
- loadEmbeddedSan(par4, animData, toCopy, renderBitmap);
- free(animData);
- }
+// ScummVM refactor helper for opcode 6 embedded ANIM scan, not a separate retail function.
+void InsaneRebel2::scanOpcode6EmbeddedAnim(byte *renderBitmap, Common::SeekableReadStream &b, int32 chunkSize, int16 par4) {
+ // Detect and load embedded ANIM (SAN) within the remaining IACT payload.
+ // Note: chunkSize is the remaining IACT payload size after par1-par4 header.
+ int64 startPos = b.pos();
+
+ // Use chunkSize (remaining IACT payload) rather than b.size() (entire FRME stream).
+ int64 remaining = chunkSize;
+ if (remaining > 0) {
+ int scanSize = (int)MIN<int64>(remaining, 65536);
+ byte *scanBuf = (byte *)malloc(scanSize);
+ if (scanBuf) {
+ int bytesRead = b.read(scanBuf, scanSize);
+ for (int i = 0; i + 8 <= bytesRead; ++i) {
+ if (READ_BE_UINT32(scanBuf + i) == MKTAG('A','N','I','M')) {
+ int64 animStreamPos = startPos + i;
+ uint32 animReportedSize = READ_BE_UINT32(scanBuf + i + 4);
+
+ // Limit to remaining IACT payload (chunkSize - offset into payload).
+ int32 toCopy = (int)MIN<int64>((int64)animReportedSize + 8, chunkSize - i);
+ if (toCopy > 0) {
+ byte *animData = (byte *)malloc(toCopy);
+ if (animData) {
+ b.seek(animStreamPos);
+ b.read(animData, toCopy);
+ loadEmbeddedSan(par4, animData, toCopy, renderBitmap);
+ free(animData);
}
- b.seek(startPos);
- free(scanBuf);
- return;
}
+ b.seek(startPos);
+ free(scanBuf);
+ return;
}
- b.seek(startPos);
- free(scanBuf);
}
+ b.seek(startPos);
+ free(scanBuf);
}
}
}
+//
+// iactRebel2Opcode6 -- Level setup / mode switch (FUN_41CADB case 4)
+//
+// Per-wave initialization: clears bit table, resets link tables, configures
+// handler mode (ship/turret/corridor), and loads collision zones. Called once
+// per wave video on frame 0.
+//
+void InsaneRebel2::iactRebel2Opcode6(byte *renderBitmap, Common::SeekableReadStream &b, int32 chunkSize, int16 par2, int16 par3, int16 par4) {
+ // Opcode 6: Level setup / mode switch
+ // Based on FUN_41CADB case 4 (switch on *local_14 - 2 == 4, meaning opcode 6)
+ //
+ // For Handler 8 (third-person on foot) - FUN_00401234 case 4:
+ // - par3 sets ship level mode (DAT_0043e000)
+ // - par4 == 1 triggers status bar display and state reset
+ // - Updates ship position based on mouse input
+ //
+ // For Handler 0x26/0x19 (turret/FPS):
+ // - Same par4 == 1 behavior
+ // - Different view offset calculations
+
+ debug("Rebel2 IACT Opcode 6: par2=%d par3=%d par4=%d", par2, par3, par4);
+
+ updateOpcode6Handler(par2);
+
+ if (_rebelHandler == 8) {
+ handleOpcode6Handler8(par3, par4);
+ return;
+ }
+
+ if (_rebelHandler == 7) {
+ handleOpcode6Handler7(b, par4);
+ return;
+ }
+
+ if (_rebelHandler == 25) {
+ handleOpcode6Handler25(renderBitmap, b, par2, par3, par4);
+ return;
+ }
+
+ if (_rebelHandler == 0x26) {
+ handleOpcode6Turret(b, par4);
+ }
+
+ handleOpcode6GenericInit(par4);
+ updateOpcode6GenericFlightState();
+ scanOpcode6EmbeddedAnim(renderBitmap, b, chunkSize, par4);
+}
+
//
// iactRebel2Opcode8 -- HUD/Ship resource loading (FUN_0040c3cc / FUN_00401234 / FUN_00407fcb)
//
diff --git a/engines/scumm/insane/rebel2/rebel.h b/engines/scumm/insane/rebel2/rebel.h
index 60a06d265e4..fe4813e0a62 100644
--- a/engines/scumm/insane/rebel2/rebel.h
+++ b/engines/scumm/insane/rebel2/rebel.h
@@ -505,6 +505,14 @@ public:
void iactRebel2Opcode2(Common::SeekableReadStream &b, int16 par2, int16 par3, int16 par4);
void iactRebel2Opcode3(Common::SeekableReadStream &b, int16 par2, int16 par3, int16 par4);
void iactRebel2Opcode6(byte *renderBitmap, Common::SeekableReadStream &b, int32 chunkSize, int16 par2, int16 par3, int16 par4);
+ void updateOpcode6Handler(int16 par2);
+ void handleOpcode6Handler8(int16 par3, int16 par4);
+ void handleOpcode6Handler7(Common::SeekableReadStream &b, int16 par4);
+ void handleOpcode6Handler25(byte *renderBitmap, Common::SeekableReadStream &b, int16 par2, int16 par3, int16 par4);
+ void handleOpcode6Turret(Common::SeekableReadStream &b, int16 par4);
+ void handleOpcode6GenericInit(int16 par4);
+ void updateOpcode6GenericFlightState();
+ void scanOpcode6EmbeddedAnim(byte *renderBitmap, Common::SeekableReadStream &b, int32 chunkSize, int16 par4);
void iactRebel2Opcode8(byte *renderBitmap, Common::SeekableReadStream &b, int32 chunkSize, int16 par2, int16 par3, int16 par4);
void iactRebel2Opcode9(byte *renderBitmap, Common::SeekableReadStream &b, int16 par2, int16 par3, int16 par4);
Commit: 9092da7f1609a61fa4ae31a62af7bcdb8ce055cd
https://github.com/scummvm/scummvm/commit/9092da7f1609a61fa4ae31a62af7bcdb8ce055cd
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:58+02:00
Commit Message:
SCUMM: RA2: Split opcode 8 handling
Changed paths:
engines/scumm/insane/rebel2/iact.cpp
engines/scumm/insane/rebel2/rebel.h
diff --git a/engines/scumm/insane/rebel2/iact.cpp b/engines/scumm/insane/rebel2/iact.cpp
index 7c088bf7026..2a050339962 100644
--- a/engines/scumm/insane/rebel2/iact.cpp
+++ b/engines/scumm/insane/rebel2/iact.cpp
@@ -1555,33 +1555,30 @@ void InsaneRebel2::iactRebel2Opcode6(byte *renderBitmap, Common::SeekableReadStr
// Handler 0x26 (turret): Turret HUD NUT via par3 (1-4)
// Handler 0x19: Mixed turret mode, similar to 0x26
//
-void InsaneRebel2::iactRebel2Opcode8(byte *renderBitmap, Common::SeekableReadStream &b, int32 chunkSize, int16 par2, int16 par3, int16 par4) {
- // Sound loading: par3 in range 21-47
-
- debug("Rebel2 IACT Opcode 8: handler=%d par2=%d par3=%d par4=%d (gameState=%d)",
- _rebelHandler, par2, par3, par4, _gameState);
-
- int64 startPos = b.pos();
- int64 remaining = (chunkSize > 0) ? chunkSize : (b.size() - startPos);
-
- // ----- Handler 7: FLY NUT Loading (Third-Person Ship) -----
- // FUN_0040c3cc case 6: par4 determines FLY sprite slot
+// ScummVM refactor helper for opcode 8 Handler 7 FLY loading, not a separate retail function.
+bool InsaneRebel2::loadOpcode8Handler7FlySprites(Common::SeekableReadStream &b, int64 startPos, int64 remaining, int16 par4) {
+ // Handler 7: FLY NUT Loading (Third-Person Ship)
+ // FUN_0040c3cc case 6: par4 determines FLY sprite slot.
bool isHandler7FLY = (_rebelHandler == 7 && (par4 == 1 || par4 == 2 || par4 == 3 || par4 == 11));
if (isHandler7FLY && remaining >= 14) {
if (loadHandler7FlySprites(b, remaining, par4)) {
b.seek(startPos);
- return;
+ return true;
}
b.seek(startPos);
}
- // ----- Edge Blend Table Loading (par4 == 1000) -----
+ return false;
+}
+
+// ScummVM refactor helper for opcode 8 edge table loading, not a separate retail function.
+bool InsaneRebel2::loadOpcode8EdgeTable(Common::SeekableReadStream &b, int64 startPos, int64 remaining, int16 par4) {
+ // Edge Blend Table Loading (par4 == 1000)
// FUN_405663: After all handler-specific opcode 8 processing, checks if par4==1000.
// If so, loads a per-level 256x256 color blend table from the IACT chunk data.
// This table controls the edge glow color of laser beams (e.g. red vs green).
// Data starts at byte offset 18 in the IACT chunk (in_stack_00000014 + 9 shorts).
if (par4 == 1000 && remaining >= 18 + 8 + 32896) {
- // Read the raw edge table data from the stream
// Layout: 18 bytes IACT header params already consumed by caller,
// but 'b' is positioned at startPos which is after par1..par4.
// The original code passes (param + 9 shorts) = data at byte offset 18 from chunk start.
@@ -1595,63 +1592,75 @@ void InsaneRebel2::iactRebel2Opcode8(byte *renderBitmap, Common::SeekableReadStr
debug("Rebel2 Opcode 8: Loaded per-level edge blend table (par4=1000)");
}
b.seek(startPos);
- return;
+ return true;
}
- // ----- Auxiliary Sound Buffer Loading (par4 20-47) -----
- // FUN_401234 case 6 (handler 8): par4 0x14-0x1b (20-27) â aux buffer 0
- // FUN_41CADB case 6 (handler 25): par4 0x15-0x1b (21-27) â aux buffer 0,
- // 0x1f-0x25 (31-37) â aux buffer 1, 0x28 (40) â aux buffer 3,
- // 0x29-0x2f (41-47) â aux buffer 2
+ return false;
+}
+
+// ScummVM refactor helper for opcode 8 aux SFX loading, not a separate retail function.
+bool InsaneRebel2::loadOpcode8AuxSfx(Common::SeekableReadStream &b, int64 startPos, int64 remaining, int16 par4) {
+ // Auxiliary Sound Buffer Loading (par4 20-47)
+ // FUN_401234 case 6 (handler 8): par4 0x14-0x1b (20-27) -> aux buffer 0
+ // FUN_41CADB case 6 (handler 25): par4 0x15-0x1b (21-27) -> aux buffer 0,
+ // 0x1f-0x25 (31-37) -> aux buffer 1, 0x28 (40) -> aux buffer 3,
+ // 0x29-0x2f (41-47) -> aux buffer 2
// Data layout: offset 14 = uint32 data size, offset 18 = PCM data start.
// Stream is at offset 8 (after par1-par4), so data size at +6, PCM at +10.
- if (par4 >= 20 && par4 <= 47) {
- int auxBuffer = -1;
- if (par4 >= 20 && par4 <= 27) {
- auxBuffer = 0;
- } else if (par4 >= 31 && par4 <= 37) {
- auxBuffer = 1;
- } else if (par4 == 40) {
- auxBuffer = 3;
- } else if (par4 >= 41 && par4 <= 47) {
- auxBuffer = 2;
- }
-
- if (auxBuffer >= 0 && remaining >= 10) {
- b.seek(startPos + 6); // Skip to data size field (byte offset 14 from IACT start)
- uint32 dataSize = b.readUint32LE();
- if (dataSize > 0 && remaining >= (int64)(10 + dataSize)) {
- byte *soundData = (byte *)malloc(dataSize);
- if (soundData) {
- b.read(soundData, dataSize);
- loadAuxSfx(auxBuffer, soundData, dataSize);
- free(soundData);
- debug("Rebel2 Opcode 8: Loaded %d bytes into aux sound buffer %d (par4=%d)",
- dataSize, auxBuffer, par4);
- }
- } else {
- debug("Rebel2 Opcode 8: Aux sound par4=%d dataSize=%d exceeds remaining=%lld",
- par4, dataSize, (long long)remaining);
+ if (par4 < 20 || par4 > 47)
+ return false;
+
+ int auxBuffer = -1;
+ if (par4 >= 20 && par4 <= 27) {
+ auxBuffer = 0;
+ } else if (par4 >= 31 && par4 <= 37) {
+ auxBuffer = 1;
+ } else if (par4 == 40) {
+ auxBuffer = 3;
+ } else if (par4 >= 41 && par4 <= 47) {
+ auxBuffer = 2;
+ }
+
+ if (auxBuffer >= 0 && remaining >= 10) {
+ b.seek(startPos + 6); // Skip to data size field (byte offset 14 from IACT start)
+ uint32 dataSize = b.readUint32LE();
+ if (dataSize > 0 && remaining >= (int64)(10 + dataSize)) {
+ byte *soundData = (byte *)malloc(dataSize);
+ if (soundData) {
+ b.read(soundData, dataSize);
+ loadAuxSfx(auxBuffer, soundData, dataSize);
+ free(soundData);
+ debug("Rebel2 Opcode 8: Loaded %u bytes into aux sound buffer %d (par4=%d)",
+ dataSize, auxBuffer, par4);
}
+ } else {
+ debug("Rebel2 Opcode 8: Aux sound par4=%d dataSize=%u exceeds remaining=%lld",
+ par4, dataSize, (long long)remaining);
}
- b.seek(startPos);
- return;
}
+ b.seek(startPos);
+ return true;
+}
- // ----- Handler 25 (0x19): Shot-Origin Lookup Table (par4 == 8) -----
+// ScummVM refactor helper for opcode 8 Handler 25 shot-origin loading, not a separate retail function.
+bool InsaneRebel2::loadOpcode8ShotOriginTable(Common::SeekableReadStream &b, int64 startPos, int64 remaining, int16 par4) {
+ // Handler 25 (0x19): Shot-Origin Lookup Table (par4 == 8)
// FUN_0041CADB case 6 pushes 30 short pointers into sscanf with format at 0x482360:
// "%hd %hd %hd %hd ... %hd %hd" (15 X/Y pairs).
// Parsed values are written into DAT_004578a6 / DAT_004578c6 at indices 5..19.
if (_rebelHandler == 25 && par4 == 8) {
- bool loaded = loadHandler25ShotOriginTable(b, startPos, remaining);
- if (loaded) {
+ if (loadHandler25ShotOriginTable(b, startPos, remaining)) {
b.seek(startPos);
- return;
+ return true;
}
}
- // ----- Scan for embedded ANIM data -----
- // Remaining handlers require finding ANIM tag in the stream
+ return false;
+}
+
+// ScummVM refactor helper for opcode 8 embedded ANIM scanning, not a separate retail function.
+void InsaneRebel2::loadOpcode8EmbeddedAnim(byte *renderBitmap, Common::SeekableReadStream &b, int64 startPos, int64 remaining, int16 par3, int16 par4) {
+ // Remaining handlers require finding ANIM tag in the stream.
debug("Rebel2 Opcode 8: Scanning for ANIM tag (startPos=%lld remaining=%lld)",
(long long)startPos, (long long)remaining);
@@ -1668,7 +1677,6 @@ void InsaneRebel2::iactRebel2Opcode8(byte *renderBitmap, Common::SeekableReadStr
int bytesRead = b.read(scanBuf, scanSize);
debug("Rebel2 Opcode 8: Read %d bytes for ANIM scan", bytesRead);
- // Find ANIM tag
int animOffset = -1;
for (int i = 0; i + 8 <= bytesRead; ++i) {
if (READ_BE_UINT32(scanBuf + i) == MKTAG('A','N','I','M')) {
@@ -1685,7 +1693,6 @@ void InsaneRebel2::iactRebel2Opcode8(byte *renderBitmap, Common::SeekableReadStr
return;
}
- // Extract ANIM data
uint32 animReportedSize = READ_BE_UINT32(scanBuf + animOffset + 4);
int32 animDataSize = (int)MIN<int64>((int64)animReportedSize + 8, remaining - animOffset);
if (animDataSize <= 0) {
@@ -1703,33 +1710,38 @@ void InsaneRebel2::iactRebel2Opcode8(byte *renderBitmap, Common::SeekableReadStr
b.seek(startPos + animOffset);
b.read(animData, animDataSize);
+ handleOpcode8EmbeddedAnim(renderBitmap, animData, animDataSize, par3, par4);
+ free(animData);
+ free(scanBuf);
+ b.seek(startPos);
+}
+
+// ScummVM refactor helper for opcode 8 embedded ANIM routing, not a separate retail function.
+bool InsaneRebel2::handleOpcode8EmbeddedAnim(byte *renderBitmap, byte *animData, int32 animDataSize, int16 par3, int16 par4) {
bool handled = false;
- // ----- Handler 0x26/0x19: Turret HUD Overlays -----
- // FUN_00407fcb case 8: par3 1-4 for HUD NUT loading
+ // Handler 0x26/0x19: Turret HUD Overlays.
+ // FUN_00407fcb case 8: par3 1-4 for HUD NUT loading.
if (!handled && (_rebelHandler == 0x26 || _rebelHandler == 0x19)) {
if (par3 >= 1 && par3 <= 4) {
handled = loadTurretHudOverlay(animData, animDataSize, par3);
}
}
- // ----- Handler 8: POV Ship Sprites or Background -----
- // FUN_00401234 case 6: par4 selects POV NUT type (1,3,6,7) or background (5)
- // NOTE: par3 is always 0 for Handler 8; par4 contains the actual sprite type
+ // Handler 8: POV Ship Sprites or Background.
+ // FUN_00401234 case 6: par4 selects POV NUT type (1,3,6,7) or background (5).
+ // NOTE: par3 is always 0 for Handler 8; par4 contains the actual sprite type.
if (!handled && _rebelHandler == 8) {
- // Check for background loading first (par4=5)
if (par4 == 5) {
handled = loadLevel2Background(animData, animDataSize, renderBitmap);
- }
- // Check for POV NUT sprites (par4=1,3,6,7)
- else if (par4 == 1 || par4 == 3 || par4 == 6 || par4 == 7) {
+ } else if (par4 == 1 || par4 == 3 || par4 == 6 || par4 == 7) {
handled = loadHandler8ShipSprites(animData, animDataSize, par4);
}
}
- // ----- Handler 25 (0x19): Level 2 GRD Ship Sprites and Background -----
- // FUN_0041cadb case 6 (opcode 8): Uses PAR4 for switch selection
+ // Handler 25 (0x19): Level 2 GRD Ship Sprites and Background.
+ // FUN_0041cadb case 6 (opcode 8): Uses PAR4 for switch selection.
// par4=1: GRD001 - Primary ship sprite -> DAT_00482240 / _grd001Sprite
// par4=2: GRD002 - Secondary ship sprite -> DAT_00482238 / _grd002Sprite
// par4=4: 350x230 corridor overlay -> DAT_00482268, draws immediately
@@ -1738,38 +1750,33 @@ void InsaneRebel2::iactRebel2Opcode8(byte *renderBitmap, Common::SeekableReadStr
// par4=7: Overlay -> DAT_00482248, draws immediately
if (!handled && _rebelHandler == 25) {
if (par4 == 1 || par4 == 2) {
- // GRD ship sprites - load into NutRenderer for per-frame rendering
handled = loadHandler25GrdSprites(animData, animDataSize, par4);
} else if (par4 == 5) {
- // Background (320x200) - stored for per-frame restoration
handled = loadLevel2Background(animData, animDataSize, renderBitmap);
} else if (par4 == 4 || par4 == 6 || par4 == 7) {
- // Overlays - draw immediately to renderBitmap
- // These complete the visual scene along with the background
debug("Rebel2 Opcode 8: Handler 25 overlay par4=%d - drawing to screen", par4);
loadEmbeddedSan(par4, animData, animDataSize, renderBitmap);
handled = true;
}
}
- // ----- Fallback: Embedded SAN HUD overlays -----
- // For other cases, load as embedded SAN frame to HUD overlay slots
+ // Fallback: Embedded SAN HUD overlays.
+ // For other cases, load as embedded SAN frame to HUD overlay slots.
if (!handled) {
- // Skip high-res data (par3 == 2, 4)
+ // Skip high-res data (par3 == 2, 4).
if (par3 == 2 || par3 == 4) {
debug("Rebel2 Opcode 8: Skipping high-res HUD par3=%d", par3);
handled = true;
} else {
- // Determine userId: Handler 0x19 uses par3, others use par4
- // Heuristic: if par3 is valid GRD range (1-13) and par4 is invalid, prefer par3
- int userId;
+ // Determine userId: Handler 0x19 uses par3, others use par4.
+ // Heuristic: if par3 is valid GRD range (1-13) and par4 is invalid, prefer par3.
bool usePar3 = (_rebelHandler == 0x19);
if (!usePar3 && par3 >= 1 && par3 <= 13 && (par4 <= 0 || par4 >= 1000)) {
usePar3 = true;
}
- userId = usePar3 ? par3 : par4;
+ int userId = usePar3 ? par3 : par4;
- // Skip audio tracks (userId >= 1000)
+ // Skip audio tracks (userId >= 1000).
if (userId > 0 && userId < 1000) {
debug("Rebel2 Opcode 8: Loading embedded SAN HUD userId=%d (handler=%d par3=%d par4=%d)",
userId, _rebelHandler, par3, par4);
@@ -1783,9 +1790,29 @@ void InsaneRebel2::iactRebel2Opcode8(byte *renderBitmap, Common::SeekableReadStr
debug("Rebel2 Opcode 8: Unhandled case - handler=%d par3=%d par4=%d", _rebelHandler, par3, par4);
}
- free(animData);
- free(scanBuf);
- b.seek(startPos);
+ return handled;
+}
+
+void InsaneRebel2::iactRebel2Opcode8(byte *renderBitmap, Common::SeekableReadStream &b, int32 chunkSize, int16 par2, int16 par3, int16 par4) {
+ debug("Rebel2 IACT Opcode 8: handler=%d par2=%d par3=%d par4=%d (gameState=%d)",
+ _rebelHandler, par2, par3, par4, _gameState);
+
+ int64 startPos = b.pos();
+ int64 remaining = (chunkSize > 0) ? chunkSize : (b.size() - startPos);
+
+ if (loadOpcode8Handler7FlySprites(b, startPos, remaining, par4))
+ return;
+
+ if (loadOpcode8EdgeTable(b, startPos, remaining, par4))
+ return;
+
+ if (loadOpcode8AuxSfx(b, startPos, remaining, par4))
+ return;
+
+ if (loadOpcode8ShotOriginTable(b, startPos, remaining, par4))
+ return;
+
+ loadOpcode8EmbeddedAnim(renderBitmap, b, startPos, remaining, par3, par4);
}
// loadHandler25ShotOriginTable -- Parse shot origin coordinate pairs from IACT payload.
diff --git a/engines/scumm/insane/rebel2/rebel.h b/engines/scumm/insane/rebel2/rebel.h
index fe4813e0a62..add64ec8c4f 100644
--- a/engines/scumm/insane/rebel2/rebel.h
+++ b/engines/scumm/insane/rebel2/rebel.h
@@ -514,6 +514,12 @@ public:
void updateOpcode6GenericFlightState();
void scanOpcode6EmbeddedAnim(byte *renderBitmap, Common::SeekableReadStream &b, int32 chunkSize, int16 par4);
void iactRebel2Opcode8(byte *renderBitmap, Common::SeekableReadStream &b, int32 chunkSize, int16 par2, int16 par3, int16 par4);
+ bool loadOpcode8Handler7FlySprites(Common::SeekableReadStream &b, int64 startPos, int64 remaining, int16 par4);
+ bool loadOpcode8EdgeTable(Common::SeekableReadStream &b, int64 startPos, int64 remaining, int16 par4);
+ bool loadOpcode8AuxSfx(Common::SeekableReadStream &b, int64 startPos, int64 remaining, int16 par4);
+ bool loadOpcode8ShotOriginTable(Common::SeekableReadStream &b, int64 startPos, int64 remaining, int16 par4);
+ void loadOpcode8EmbeddedAnim(byte *renderBitmap, Common::SeekableReadStream &b, int64 startPos, int64 remaining, int16 par3, int16 par4);
+ bool handleOpcode8EmbeddedAnim(byte *renderBitmap, byte *animData, int32 animDataSize, int16 par3, int16 par4);
void iactRebel2Opcode9(byte *renderBitmap, Common::SeekableReadStream &b, int16 par2, int16 par3, int16 par4);
void procPostRendering(byte *renderBitmap, int32 codecparam, int32 setupsan12,
Commit: 79342da6bcd496c422f1ae65e523188841cf44df
https://github.com/scummvm/scummvm/commit/79342da6bcd496c422f1ae65e523188841cf44df
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:58+02:00
Commit Message:
SCUMM: RA2: Split post-render handling
Changed paths:
engines/scumm/insane/rebel2/rebel.h
engines/scumm/insane/rebel2/render.cpp
diff --git a/engines/scumm/insane/rebel2/rebel.h b/engines/scumm/insane/rebel2/rebel.h
index add64ec8c4f..6002a4e63c2 100644
--- a/engines/scumm/insane/rebel2/rebel.h
+++ b/engines/scumm/insane/rebel2/rebel.h
@@ -538,6 +538,16 @@ public:
void renderStatusBarBackground(byte *renderBitmap, int pitch, int width, int height,
int videoWidth, int videoHeight, int statusBarY);
+ void updatePostRenderScroll(int width, int height);
+ void updatePostRenderDeath();
+ void showPostRenderMenuCursor();
+ bool handlePostRenderMenuModes(byte *renderBitmap, int pitch, int width, int height, bool introPlaying);
+ bool handlePostRenderIntro(byte *renderBitmap, int pitch, int width, int height, int32 curFrame);
+ void renderGameplayPostFrame(byte *renderBitmap, int pitch, int width, int height,
+ int videoWidth, int videoHeight, int statusBarY, int32 curFrame);
+ void updateGameplayDamageEffects(byte *renderBitmap, int pitch, int width, int height);
+ void checkGameplayPostRenderCollisions(byte *renderBitmap, int pitch, int width, int height, int32 curFrame);
+
// Draw NUT-based HUD overlays for Handler 0x26/0x19 turret modes
void renderTurretHudOverlays(byte *renderBitmap, int pitch, int width, int height, int32 curFrame);
diff --git a/engines/scumm/insane/rebel2/render.cpp b/engines/scumm/insane/rebel2/render.cpp
index d0db992e1ff..1d5bb9147fe 100644
--- a/engines/scumm/insane/rebel2/render.cpp
+++ b/engines/scumm/insane/rebel2/render.cpp
@@ -2036,28 +2036,10 @@ void InsaneRebel2::renderNutSpriteMirrored(byte *dst, int pitch, int width, int
}
}
-//
-// procPostRendering -- Post-frame rendering: HUD, ships, enemies, effects, status bar.
-//
-// Called after FOBJ decoding. Dispatches to per-handler rendering functions
-// for ship sprites, laser shots, explosions, crosshair, and damage effects.
-//
-void InsaneRebel2::procPostRendering(byte *renderBitmap, int32 codecparam, int32 setupsan12,
- int32 setupsan13, int32 curFrame, int32 maxFrame) {
-
- // Determine correct pitch for the video buffer (usually 320 for Rebel2)
- int width = _player->_width;
- int height = _player->_height;
- if (width == 0)
- width = _vm->_screenWidth;
- if (height == 0)
- height = _vm->_screenHeight;
- int pitch = width;
-
- // Calculate View/Scroll Offsets
- // Rebel Assault 2 uses a buffer larger (424x260) than screen (320x200)
- // Map mouse X (0-320) to Scroll X (0-104)
- // Map mouse Y (0-200) to Scroll Y (0-60)
+// updatePostRenderScroll -- Set SmushPlayer scroll offsets for the current frame.
+void InsaneRebel2::updatePostRenderScroll(int width, int height) {
+ // Rebel Assault 2 uses a buffer larger (424x260) than screen (320x200).
+ // Map mouse X (0-320) to Scroll X (0-104), and Y (0-200) to Scroll Y (0-60).
int maxScrollX = width - _vm->_screenWidth;
int maxScrollY = height - _vm->_screenHeight;
@@ -2066,15 +2048,18 @@ void InsaneRebel2::procPostRendering(byte *renderBitmap, int32 codecparam, int32
if (maxScrollY < 0)
maxScrollY = 0;
- // Simple linear mapping: Center of screen corresponds to center of buffer
+ // Simple linear mapping: Center of screen corresponds to center of buffer.
Common::Point aimPos = getGameplayAimPoint();
_viewX = (aimPos.x * maxScrollX) / _vm->_screenWidth;
_viewY = (aimPos.y * maxScrollY) / _vm->_screenHeight;
_player->setScrollOffset(_viewX, _viewY);
+}
- // Death check: original game (FUN_417E53 line 25) exits video playback
- // when DAT_0047a7ec >= 0xff (damage accumulator reaches 255).
+// updatePostRenderDeath -- End gameplay playback when player damage reaches 255.
+void InsaneRebel2::updatePostRenderDeath() {
+ // Original game (FUN_417E53 line 25) exits video playback when
+ // DAT_0047a7ec >= 0xff (damage accumulator reaches 255).
// Sync _playerShield from _playerDamage and break out of video on death.
if (_rebelHandler != 0) {
_playerShield = 255 - _playerDamage;
@@ -2083,150 +2068,130 @@ void InsaneRebel2::procPostRendering(byte *renderBitmap, int32 codecparam, int32
_vm->_smushVideoShouldFinish = true;
}
}
+}
- // --- HUD Drawing Order (from FUN_004089ab assembly analysis) ---
- // Based on FUN_004089ab:
- // 1. Line 156: FUN_004288c0 fills status bar background at Y=0xb4 (180)
- // 2. Lines 171-226: Draw turret overlays, targeting reticle, crosshair
- // 3. Line 243: FUN_0041c012 draws status bar sprites LAST (on top)
- //
- // In FUN_0041c012:
- // - Sprites are drawn to buffer DAT_00482204 at position (0,0)
- // - Buffer is composited at Y=0xb4 (180) via FUN_0042f780
- // - DISPFONT.NUT (DAT_00482200) sprites 1-7 contain the status bar elements
- //
- // We draw directly to screen at Y=180
-
- // Use video content coordinates, NOT buffer coordinates
- const int videoWidth = 320; // Native video width
- const int videoHeight = 200; // Native video height
- const int statusBarY = 180; // 0xb4 - status bar starts at Y=180 in video coords
-
- // Hide HUD/status bar during intro videos (marked by SmushPlayer video flag 0x20)
- // The 0x20 flag indicates a non-interactive cutscene/intro sequence OR menu
- bool introPlaying = ((_player->_curVideoFlags & 0x20) != 0);
+// showPostRenderMenuCursor -- Restore the default cursor for menu videos.
+void InsaneRebel2::showPostRenderMenuCursor() {
+ Graphics::Cursor *cursor = Graphics::makeDefaultWinCursor();
+ CursorMan.replaceCursor(cursor);
+ delete cursor;
+ CursorMan.showMouse(true);
+}
- // Check if we're in menu mode (menu state + intro flag)
+// handlePostRenderMenuModes -- Process menu-like videos drawn during post-rendering.
+bool InsaneRebel2::handlePostRenderMenuModes(byte *renderBitmap, int pitch, int width, int height, bool introPlaying) {
+ // Check if we're in menu mode (menu state + intro flag).
bool menuMode = (introPlaying && _gameState == kStateMainMenu);
bool pilotSelectMode = (introPlaying && (_gameState == kStatePilotSelect || _gameState == kStateDifficultySelect));
bool chapterSelectMode = (introPlaying && _gameState == kStateChapterSelect);
- // Handle pilot selection input and rendering (FUN_00414A41)
- // This is the pilot/save slot selection screen with centered menu
+ // Handle pilot selection input and rendering (FUN_00414A41).
+ // This is the pilot/save slot selection screen with centered menu.
if (pilotSelectMode) {
- // Show the standard Windows arrow cursor
- Graphics::Cursor *cursor = Graphics::makeDefaultWinCursor();
- CursorMan.replaceCursor(cursor);
- delete cursor;
- CursorMan.showMouse(true);
+ showPostRenderMenuCursor();
- // Process pilot selection input - emulates FUN_00414A41 input handling
+ // Process pilot selection input - emulates FUN_00414A41 input handling.
int selection = processLevelSelectInput();
- // Draw pilot selection overlay - centered menu like main menu
+ // Draw pilot selection overlay - centered menu like main menu.
drawLevelSelectOverlay(renderBitmap, pitch, width, height);
- // If a selection was confirmed, signal video to stop
+ // If a selection was confirmed, signal video to stop.
if (selection >= 0) {
debug("Rebel2: Pilot selection confirmed: %d", selection);
_menuSelectionConfirmed = true;
_vm->_smushVideoShouldFinish = true;
}
- // Skip normal HUD rendering in pilot select mode
- return;
+ // Skip normal HUD rendering in pilot select mode.
+ return true;
}
- // Handle chapter selection input and rendering (FUN_00415CF8)
- // This is the actual level/chapter selection screen with preview and password
+ // Handle chapter selection input and rendering (FUN_00415CF8).
+ // This is the actual level/chapter selection screen with preview and password.
if (chapterSelectMode) {
- // Show the standard Windows arrow cursor (same as menu)
- Graphics::Cursor *cursor = Graphics::makeDefaultWinCursor();
- CursorMan.replaceCursor(cursor);
- delete cursor;
- CursorMan.showMouse(true);
+ showPostRenderMenuCursor();
// O_LEVEL.SAN provides the background with chapter preview thumbnails.
// The FOBJ offset system (set in procPreRendering) scrolls the correct preview
// into the preview box area. No black fill needed â video frame shows through.
- // Process chapter selection input - emulates FUN_00415CF8 input handling
+ // Process chapter selection input - emulates FUN_00415CF8 input handling.
int selection = processChapterSelectInput();
- // Draw chapter selection overlay - emulates FUN_00415CF8 rendering
+ // Draw chapter selection overlay - emulates FUN_00415CF8 rendering.
drawChapterSelectOverlay(renderBitmap, pitch, width, height);
- // If a selection was confirmed, signal video to stop
+ // If a selection was confirmed, signal video to stop.
if (selection >= 0) {
debug("Rebel2: Chapter selection confirmed: %d", selection);
_menuSelectionConfirmed = true;
_vm->_smushVideoShouldFinish = true;
}
- // Skip normal HUD rendering in chapter select mode
- return;
+ // Skip normal HUD rendering in chapter select mode.
+ return true;
}
- // Handle Top Pilots screen (FUN_00420116)
- bool topPilotsMode = (introPlaying && _gameState == kStateTopPilots);
- if (topPilotsMode) {
+ // Handle Top Pilots screen (FUN_00420116).
+ if (introPlaying && _gameState == kStateTopPilots) {
drawTopPilotsOverlay(renderBitmap, pitch, width, height);
- return;
+ return true;
}
- // Handle Options menu (FUN_004167A6)
- bool optionsMode = (introPlaying && _gameState == kStateOptions);
- if (optionsMode) {
+ // Handle Options menu (FUN_004167A6).
+ if (introPlaying && _gameState == kStateOptions) {
processOptionsInput();
drawOptionsOverlay(renderBitmap, pitch, width, height);
- return;
+ return true;
}
- // Handle menu input and rendering if in menu mode
if (menuMode) {
// The original game uses the standard Windows arrow cursor (IDC_ARROW)
- // loaded via LoadCursorA(NULL, 0x7f00) in FUN_420C70.decompiled.txt
- // MSTOVER.NUT is a background overlay, NOT a cursor
- Graphics::Cursor *cursor = Graphics::makeDefaultWinCursor();
- CursorMan.replaceCursor(cursor);
- delete cursor;
- CursorMan.showMouse(true);
-
- // Process menu input during each frame
+ // loaded via LoadCursorA(NULL, 0x7f00) in FUN_420C70.decompiled.txt.
+ // MSTOVER.NUT is a background overlay, NOT a cursor.
+ showPostRenderMenuCursor();
+
+ // Process menu input during each frame.
int selection = processMenuInput();
- // Update inactivity timer (only increments when no input is received)
- // Input resets timer in processMenuInput()
+ // Update inactivity timer (only increments when no input is received).
+ // Input resets timer in processMenuInput().
_menuInactivityTimer++;
- // Check for inactivity timeout
- // From FUN_004147b2: 300 frames of inactivity returns 0 (exit to intro/attract mode)
- // At 12fps video rate, 300 frames = ~25 seconds of inactivity
- // The original checks: if (local_8 > 299) return 0;
+ // Check for inactivity timeout.
+ // From FUN_004147b2: 300 frames of inactivity returns 0 (exit to intro/attract mode).
+ // At 12fps video rate, 300 frames = ~25 seconds of inactivity.
+ // The original checks: if (local_8 > 299) return 0.
if (_menuInactivityTimer > 300) {
debug("Rebel2: Menu inactivity timeout - ending video to loop");
- // Signal video to end so menu loop plays new video
+ // Signal video to end so menu loop plays new video.
// This emulates the attract mode behavior where a new random
- // menu video is selected after inactivity
+ // menu video is selected after inactivity.
_menuInactivityTimer = 0;
- // Don't set _smushVideoShouldFinish here - let video end naturally
- // This will cause runMainMenu to loop and play a new random video
+ // Don't set _smushVideoShouldFinish here - let video end naturally.
+ // This will cause runMainMenu to loop and play a new random video.
}
- // Draw menu selection overlay
+ // Draw menu selection overlay.
drawMenuOverlay(renderBitmap, pitch, width, height);
- // If a selection was confirmed, signal video to stop
+ // If a selection was confirmed, signal video to stop.
if (selection >= 0) {
debug("Rebel2: Menu selection confirmed: %d", selection);
_menuSelectionConfirmed = true;
_vm->_smushVideoShouldFinish = true;
}
- // Skip normal HUD rendering in menu mode
- return;
+ // Skip normal HUD rendering in menu mode.
+ return true;
}
+ return false;
+}
+
+// handlePostRenderIntro -- Hide gameplay HUD for intro/cinematic videos.
+bool InsaneRebel2::handlePostRenderIntro(byte *renderBitmap, int pitch, int width, int height, int32 curFrame) {
// During intro/cinematic sequences:
// - Hide the mouse cursor (original: ShowCursor(0) at startup in FUN_00420c70)
// - Skip all HUD/status bar/crosshair rendering
@@ -2238,34 +2203,77 @@ void InsaneRebel2::procPostRendering(byte *renderBitmap, int32 codecparam, int32
// - Cinematics/intros don't have opcode 6, so handler stays 0
// - We use _rebelHandler == 0 as the primary indicator for intro/cinematic mode
if (_rebelHandler == 0) {
- // Hide mouse cursor during intro - no crosshair, no clicking
+ // Hide mouse cursor during intro - no crosshair, no clicking.
CursorMan.showMouse(false);
- // Track state transition for debugging
+ // Track state transition for debugging.
if (!_introCursorPushed) {
_introCursorPushed = true;
debug("Rebel2: Intro/cinematic mode (handler=0, flags=0x%x, state=%d) - HUD disabled, mouse hidden",
_player->_curVideoFlags, _gameState);
}
- // Chapter title text overlay (FUN_004171c5)
+ // Chapter title text overlay (FUN_004171c5).
if (_textOverlayActive)
renderTextOverlay(renderBitmap, pitch, width, height, curFrame);
- // Skip all HUD rendering during intro - subtitles are rendered via opcode 9
- return;
- } else {
- // Gameplay mode - handler was set by IACT opcode 6
- if (_introCursorPushed) {
- _introCursorPushed = false;
- debug("Rebel2: Gameplay mode (handler=%d, flags=0x%x, state=%d) - HUD enabled",
- _rebelHandler, _player->_curVideoFlags, _gameState);
+ // Skip all HUD rendering during intro - subtitles are rendered via opcode 9.
+ return true;
+ }
+
+ // Gameplay mode - handler was set by IACT opcode 6.
+ if (_introCursorPushed) {
+ _introCursorPushed = false;
+ debug("Rebel2: Gameplay mode (handler=%d, flags=0x%x, state=%d) - HUD enabled",
+ _rebelHandler, _player->_curVideoFlags, _gameState);
+ }
+
+ return false;
+}
+
+// updateGameplayDamageEffects -- Apply handler-specific damage visuals.
+void InsaneRebel2::updateGameplayDamageEffects(byte *renderBitmap, int pitch, int width, int height) {
+ // Damage visual effects - handler-specific per original architecture:
+ // Handler 8: FUN_401CCF line 119 -> FUN_00420754 (palette flash + screen shake)
+ // Handler 0x19: FUN_41DB5E line 192 -> FUN_00420562 (palette flash only, every frame)
+ // Handler 0x26: FUN_4092D9 lines 135/225/237 -> FUN_00420515 trigger + palette flash
+ // Handler 7: FUN_40E35E -> FUN_00420515 trigger + palette flash
+ if (_rebelHandler == 8) {
+ // Full damage effect: palette flash + screen shake.
+ // Suppressed during autopilot (mode 4) and cutscene (mode 5).
+ if (_shipLevelMode != 4 && _shipLevelMode != 5) {
+ updateDamageEffect(renderBitmap, pitch, width, height);
}
+ } else if (_rebelHandler == 0x19 || _rebelHandler == 0x26 || _rebelHandler == 7) {
+ // Palette flash only - no screen shake for turret/FPS/ship handlers.
+ updateDamageFlashPalette();
+ }
+}
+
+// checkGameplayPostRenderCollisions -- Run handler-specific collision checks.
+void InsaneRebel2::checkGameplayPostRenderCollisions(byte *renderBitmap, int pitch, int width, int height, int32 curFrame) {
+ // Per-frame collision checking against registered zones.
+ //
+ // Handler 0x26 (turret): FUN_4092D9 - aim position vs primary zones (centered coords)
+ // Zones with filterValue < 1000 tested via point-in-quad against mouse/aim position.
+ //
+ // Handler 7 (ship): FUN_40E35E - ship position vs zones per control mode:
+ // Mode 0/2: SECONDARY zones (0x0E) - obstacle collision (inside quad = hit)
+ // Mode 1/3: PRIMARY zones (0x0D) - wall/boundary per-edge with push-back
+ // Uses ship position in raw buffer coords, hit cooldown, directional damage.
+ if (_rebelHandler == 0x26) {
+ checkCollisionZones();
+ } else if (_rebelHandler == 7) {
+ checkHandler7CollisionZones(renderBitmap, pitch, width, height, curFrame);
}
+}
- // From here on, we're in gameplay mode (_rebelHandler != 0)
- // Process mouse input for shooting
- // Original: FUN_00403240 only runs handlers when DAT_0047a814 == 0
+// renderGameplayPostFrame -- Draw the gameplay post-render pipeline in original order.
+void InsaneRebel2::renderGameplayPostFrame(byte *renderBitmap, int pitch, int width, int height,
+ int videoWidth, int videoHeight, int statusBarY, int32 curFrame) {
+ // From here on, we're in gameplay mode (_rebelHandler != 0).
+ // Process mouse input for shooting.
+ // Original: FUN_00403240 only runs handlers when DAT_0047a814 == 0.
processMouse();
// NOTE: Level 2 background is drawn ONCE during IACT opcode 8 par4=5 processing
@@ -2285,10 +2293,22 @@ void InsaneRebel2::procPostRendering(byte *renderBitmap, int32 codecparam, int32
// The cockpit frame covers laser beam edges, giving the appearance
// that beams emerge from behind the cockpit.
- // STEP 0: Fill status bar background (FUN_004288c0)
+ // Based on FUN_004089ab:
+ // 1. Line 156: FUN_004288c0 fills status bar background at Y=0xb4 (180)
+ // 2. Lines 171-226: Draw turret overlays, targeting reticle, crosshair
+ // 3. Line 243: FUN_0041c012 draws status bar sprites LAST (on top)
+ //
+ // In FUN_0041c012:
+ // - Sprites are drawn to buffer DAT_00482204 at position (0,0)
+ // - Buffer is composited at Y=0xb4 (180) via FUN_0042f780
+ // - DISPFONT.NUT (DAT_00482200) sprites 1-7 contain the status bar elements
+ //
+ // We draw directly to screen at Y=180.
+
+ // STEP 0: Fill status bar background (FUN_004288c0).
renderStatusBarBackground(renderBitmap, pitch, width, height, videoWidth, videoHeight, statusBarY);
- // Ship rendering (FUN_00401ccf for Handler 8, FUN_0040d836 for Handler 7)
+ // Ship rendering (FUN_00401ccf for Handler 8, FUN_0040d836 for Handler 7).
debug("Rebel2 Ship Check: handler=%d shipSprite=%p flyShipSprite=%p shipLevelMode=%d numSprites=%d/%d",
_rebelHandler, (void*)_shipSprite, (void*)_flyShipSprite, _shipLevelMode,
_shipSprite ? _shipSprite->getNumChars() : 0,
@@ -2298,13 +2318,13 @@ void InsaneRebel2::procPostRendering(byte *renderBitmap, int32 codecparam, int32
renderHandler8Ship(renderBitmap, pitch, width, height);
renderFallbackShip(renderBitmap, pitch, width, height);
- // Enemy target indicators (handler-specific; sprite-based in turret mode)
+ // Enemy target indicators (handler-specific; sprite-based in turret mode).
renderEnemyOverlays(renderBitmap, pitch, width, height, videoWidth);
- // Explosion animations (FUN_409FBC) â drawn before lasers in original
+ // Explosion animations (FUN_409FBC) - drawn before lasers in original.
renderExplosions(renderBitmap, pitch, width, height);
- // Laser shot beams â drawn BEFORE cockpit/HUD overlays so cockpit covers beam edges
+ // Laser shot beams - drawn BEFORE cockpit/HUD overlays so cockpit covers beam edges.
renderLaserShots(renderBitmap, pitch, width, height);
// Handler 25 GRD sprites drawn AFTER enemies/explosions/lasers per original FUN_0041DB5E:
@@ -2316,63 +2336,73 @@ void InsaneRebel2::procPostRendering(byte *renderBitmap, int32 codecparam, int32
renderHandler25ShipPre(renderBitmap, pitch, width, height);
renderHandler25Ship(renderBitmap, pitch, width, height);
- // STEP 1A: Draw NUT-based HUD overlays for Handler 0x26/0x19 (FUN_004089ab lines 195-226)
- // These are cockpit frame, crosshair, and reticle â drawn ON TOP of laser beams
+ // STEP 1A: Draw NUT-based HUD overlays for Handler 0x26/0x19 (FUN_004089ab lines 195-226).
+ // These are cockpit frame, crosshair, and reticle - drawn ON TOP of laser beams.
renderTurretHudOverlays(renderBitmap, pitch, width, height, curFrame);
- // STEP 1B: Draw embedded SAN HUD overlays (from IACT chunks)
+ // STEP 1B: Draw embedded SAN HUD overlays (from IACT chunks).
renderEmbeddedHudOverlays(renderBitmap, pitch, width, height);
- // STEP 2: Draw DISPFONT.NUT status bar sprites (FUN_0041c012)
+ // STEP 2: Draw DISPFONT.NUT status bar sprites (FUN_0041c012).
renderStatusBarSprites(renderBitmap, pitch, width, height, statusBarY, curFrame);
- // Damage visual effects â handler-specific per original architecture:
- // Handler 8: FUN_401CCF line 119 â FUN_00420754 (palette flash + screen shake)
- // Handler 0x19: FUN_41DB5E line 192 â FUN_00420562 (palette flash only, every frame)
- // Handler 0x26: FUN_4092D9 lines 135/225/237 â FUN_00420515 trigger + palette flash
- // Handler 7: FUN_40E35E â FUN_00420515 trigger + palette flash
- if (_rebelHandler == 8) {
- // Full damage effect: palette flash + screen shake
- // Suppressed during autopilot (mode 4) and cutscene (mode 5)
- if (_shipLevelMode != 4 && _shipLevelMode != 5) {
- updateDamageEffect(renderBitmap, pitch, width, height);
- }
- } else if (_rebelHandler == 0x19 || _rebelHandler == 0x26 || _rebelHandler == 7) {
- // Palette flash only â no screen shake for turret/FPS/ship handlers
- updateDamageFlashPalette();
- }
-
- // Per-frame collision checking against registered zones.
- //
- // Handler 0x26 (turret): FUN_4092D9 â aim position vs primary zones (centered coords)
- // Zones with filterValue < 1000 tested via point-in-quad against mouse/aim position.
- //
- // Handler 7 (ship): FUN_40E35E â ship position vs zones per control mode:
- // Mode 0/2: SECONDARY zones (0x0E) â obstacle collision (inside quad = hit)
- // Mode 1/3: PRIMARY zones (0x0D) â wall/boundary per-edge with push-back
- // Uses ship position in raw buffer coords, hit cooldown, directional damage.
- if (_rebelHandler == 0x26) {
- checkCollisionZones();
- } else if (_rebelHandler == 7) {
- checkHandler7CollisionZones(renderBitmap, pitch, width, height, curFrame);
- }
+ updateGameplayDamageEffects(renderBitmap, pitch, width, height);
+ checkGameplayPostRenderCollisions(renderBitmap, pitch, width, height, curFrame);
- // Crosshair/reticle (FUN_004089ab, FUN_0040d836)
+ // Crosshair/reticle (FUN_004089ab, FUN_0040d836).
renderCrosshair(renderBitmap, pitch, width, height);
- // HUD score/lives rendering (FUN_0041c012)
+ // HUD score/lives rendering (FUN_0041c012).
renderScoreHUD(renderBitmap, pitch, width, height, statusBarY);
- // Reset FOBJ position offsets (FUN_00424510(0,0) in original FUN_0041DB5E line 271)
+ // Reset FOBJ position offsets (FUN_00424510(0,0) in original FUN_0041DB5E line 271).
if (_player) {
_player->_fobjOffsetX = 0;
_player->_fobjOffsetY = 0;
}
- // Frame end cleanup: reset enemy active flags and collision zones (FUN_403240)
+ // Frame end cleanup: reset enemy active flags and collision zones (FUN_403240).
frameEndCleanup();
}
+//
+// procPostRendering -- Post-frame rendering: HUD, ships, enemies, effects, status bar.
+//
+// Called after FOBJ decoding. Dispatches to per-handler rendering functions
+// for ship sprites, laser shots, explosions, crosshair, and damage effects.
+//
+void InsaneRebel2::procPostRendering(byte *renderBitmap, int32 codecparam, int32 setupsan12,
+ int32 setupsan13, int32 curFrame, int32 maxFrame) {
+
+ // Determine correct pitch for the video buffer (usually 320 for Rebel2)
+ int width = _player->_width;
+ int height = _player->_height;
+ if (width == 0)
+ width = _vm->_screenWidth;
+ if (height == 0)
+ height = _vm->_screenHeight;
+ int pitch = width;
+
+ updatePostRenderScroll(width, height);
+ updatePostRenderDeath();
+
+ // Use video content coordinates, NOT buffer coordinates
+ const int videoWidth = 320; // Native video width
+ const int videoHeight = 200; // Native video height
+ const int statusBarY = 180; // 0xb4 - status bar starts at Y=180 in video coords
+
+ // Hide HUD/status bar during intro videos (marked by SmushPlayer video flag 0x20)
+ // The 0x20 flag indicates a non-interactive cutscene/intro sequence OR menu
+ bool introPlaying = ((_player->_curVideoFlags & 0x20) != 0);
+
+ if (handlePostRenderMenuModes(renderBitmap, pitch, width, height, introPlaying))
+ return;
+
+ if (handlePostRenderIntro(renderBitmap, pitch, width, height, curFrame))
+ return;
+ renderGameplayPostFrame(renderBitmap, pitch, width, height, videoWidth, videoHeight, statusBarY, curFrame);
+}
+
// ---------------------------------------------------------------------------
// Damage Visual Effect Functions
// ---------------------------------------------------------------------------
Commit: d90e9a159ab0e624b6f4c94a96145c7a1bacb914
https://github.com/scummvm/scummvm/commit/d90e9a159ab0e624b6f4c94a96145c7a1bacb914
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:59+02:00
Commit Message:
SCUMM: RA2: Split handler 7 code
Changed paths:
engines/scumm/insane/rebel2/rebel.h
engines/scumm/insane/rebel2/render.cpp
diff --git a/engines/scumm/insane/rebel2/rebel.h b/engines/scumm/insane/rebel2/rebel.h
index 6002a4e63c2..3fb850bc78a 100644
--- a/engines/scumm/insane/rebel2/rebel.h
+++ b/engines/scumm/insane/rebel2/rebel.h
@@ -813,6 +813,18 @@ public:
// Uses ship position (_flyShipScreenX/_flyShipScreenY) in raw buffer coords
// and draws proximity shadow cues for nearby danger zones.
void checkHandler7CollisionZones(byte *renderBitmap, int pitch, int width, int height, int32 curFrame);
+ bool isHandler7ShipInsideObstacleZone(const CollisionZone &zone, int margin);
+ void applyHandler7ObstacleHit(const CollisionZone &zone, int zoneIndex);
+ void awardHandler7DodgeScore();
+ void checkHandler7ObstacleZones(uint16 &warningMask);
+ bool applyHandler7WallDamage(int wallDamage);
+ void resetHandler7HorizontalVelocity(int16 velocity);
+ void checkHandler7TopBoundary(const CollisionZone &zone, int16 vMargin, int wallDamage, uint16 &warningMask);
+ void checkHandler7BottomBoundary(const CollisionZone &zone, int16 vMargin, int wallDamage, uint16 &warningMask);
+ void checkHandler7LeftBoundary(const CollisionZone &zone, int16 hMargin, int wallDamage);
+ void checkHandler7RightBoundary(const CollisionZone &zone, int16 hMargin, int wallDamage);
+ void checkHandler7BoundaryZones(uint16 &warningMask);
+ void renderHandler7WarningCues(byte *renderBitmap, int pitch, int width, int height, int32 curFrame, uint16 warningMask);
int16 _playerDamage; // Legacy damage counter (kept for compatibility/telemetry)
int16 _playerShield; // Shields: 0..255 where 255 = full
diff --git a/engines/scumm/insane/rebel2/render.cpp b/engines/scumm/insane/rebel2/render.cpp
index 1d5bb9147fe..04ce01e7f48 100644
--- a/engines/scumm/insane/rebel2/render.cpp
+++ b/engines/scumm/insane/rebel2/render.cpp
@@ -1665,236 +1665,252 @@ void InsaneRebel2::checkCollisionZones() {
// Two modes: obstacle collision (secondary zones) and wall/boundary
// collision (primary zones with per-edge push-back).
//
-void InsaneRebel2::checkHandler7CollisionZones(byte *renderBitmap, int pitch, int width, int height, int32 curFrame) {
- // FUN_40E35E â Handler 7 per-frame collision system.
- // Uses ship position (_flyShipScreenX/_flyShipScreenY) in raw buffer coords.
- // Two modes depending on _flyControlMode:
- // Mode 0/2: Obstacle collision using SECONDARY zones (inside quad = hit)
- // Mode 1/3: Wall/boundary collision using PRIMARY zones (per-edge push-back)
+// The helpers in this block are ScummVM refactor helpers split out of
+// checkHandler7CollisionZones; they are not separate retail functions.
+//
+bool InsaneRebel2::isHandler7ShipInsideObstacleZone(const InsaneRebel2::CollisionZone &zone, int margin) {
+ int x1 = zone.x1, y1 = zone.y1;
+ int x2 = zone.x2, y2 = zone.y2;
+ int x3 = zone.x3, y3 = zone.y3;
+ int x4 = zone.x4, y4 = zone.y4;
- // Note: _hitCooldown is decremented in renderSpaceExplosions (FUN_40F1C5)
- // to match the original where the decrement happens during rendering.
- //
- // local_c in FUN_40E35E: proximity mask for nearby danger-zone shadow cues.
- // bit 0=left, bit 1=right, bit 2=top, bit 3=bottom
- uint16 warningMask = 0;
+ // Point-in-quad test (lines 75-89).
+ // Start assuming inside, clear if outside any edge (with margin).
+ bool inside = true;
- if (_flyControlMode == 0 || _flyControlMode == 2) {
- // ---- Mode 0/2: Obstacle collision using SECONDARY zones (FUN_403b5b) ----
- // Original lines 52-132: Point-in-quad test with 15px inward margin.
- // Inside the quad = collision with obstacle.
- const int margin = 15; // local_14 = 0x0f, local_20 = 0x0f
-
- for (int i = 0; i < _secondaryZoneCount; i++) {
- CollisionZone &zone = _secondaryZones[i];
- if (!zone.active)
- continue;
+ // Top edge: interpolate Y along v1->v2 at shipX, +15 margin.
+ if (x2 != x1) {
+ int interpY = (_flyShipScreenX - x1) * (y2 - y1) / (x2 - x1) + margin + y1;
+ if (_flyShipScreenY < interpY)
+ inside = false;
+ }
+ // Bottom edge: interpolate Y along v4->v3 at shipX, -15 margin.
+ if (inside && x3 != x4) {
+ int interpY = (_flyShipScreenX - x4) * (y3 - y4) / (x3 - x4) + y4 - margin;
+ if (interpY < _flyShipScreenY)
+ inside = false;
+ }
+ // Left edge: interpolate X along v1->v4 at shipY, +15 margin.
+ if (inside && y4 != y1) {
+ int interpX = (_flyShipScreenY - y1) * (x4 - x1) / (y4 - y1) + margin + x1;
+ if (_flyShipScreenX < interpX)
+ inside = false;
+ }
+ // Right edge: interpolate X along v2->v3 at shipY, -15 margin.
+ if (inside && y3 != y2) {
+ int interpX = (_flyShipScreenY - y2) * (x3 - x2) / (y3 - y2) + x2 - margin;
+ if (interpX < _flyShipScreenX)
+ inside = false;
+ }
- int x1 = zone.x1, y1 = zone.y1;
- int x2 = zone.x2, y2 = zone.y2;
- int x3 = zone.x3, y3 = zone.y3;
- int x4 = zone.x4, y4 = zone.y4;
+ return inside;
+}
- // Point-in-quad test (lines 75-89)
- // Start assuming inside, clear if outside any edge (with margin)
- bool inside = true;
+void InsaneRebel2::applyHandler7ObstacleHit(const InsaneRebel2::CollisionZone &zone, int zoneIndex) {
+ // Collision with obstacle - apply damage and break.
+ _hitCooldown = 10;
+ _spaceShotDirection = zone.filterValue + 2;
- // Top edge: interpolate Y along v1âv2 at shipX, +15 margin
- if (x2 != x1) {
- int interpY = (_flyShipScreenX - x1) * (y2 - y1) / (x2 - x1) + margin + y1;
- if (_flyShipScreenY < interpY)
- inside = false;
- }
- // Bottom edge: interpolate Y along v4âv3 at shipX, -15 margin
- if (inside && x3 != x4) {
- int interpY = (_flyShipScreenX - x4) * (y3 - y4) / (x3 - x4) + y4 - margin;
- if (interpY < _flyShipScreenY)
- inside = false;
- }
- // Left edge: interpolate X along v1âv4 at shipY, +15 margin
- if (inside && y4 != y1) {
- int interpX = (_flyShipScreenY - y1) * (x4 - x1) / (y4 - y1) + margin + x1;
- if (_flyShipScreenX < interpX)
- inside = false;
- }
- // Right edge: interpolate X along v2âv3 at shipY, -15 margin
- if (inside && y3 != y2) {
- int interpX = (_flyShipScreenY - y2) * (x3 - x2) / (y3 - y2) + x2 - margin;
- if (interpX < _flyShipScreenX)
- inside = false;
- }
+ LevelDifficultyParams params = getDifficultyParams();
+ int collisionDamage = (params.dodgeDamage >= 0) ? params.dodgeDamage : 0;
+ if (!_rebelInvulnerable) {
+ _playerDamage += collisionDamage;
+ if (_playerDamage > 255)
+ _playerDamage = 255;
+ }
+ _rebelHitCounter++;
+ initDamageFlash();
+ // Pan based on ship X position relative to screen center.
+ playSfx(1, 127, CLIP((_flyShipScreenX - 212) * 127 / 160, -127, 127));
+ debug("Rebel2: Handler7 Mode0/2 OBSTACLE HIT zone=%d ship=(%d,%d) damage=%d",
+ zoneIndex, _flyShipScreenX, _flyShipScreenY, collisionDamage);
+}
- // Frame match: field2 - 1 == field1 (line 90)
- if (zone.field2 - 1 == zone.field1) {
- if (inside) {
- // Collision with obstacle â apply damage and break
- _hitCooldown = 10;
- _spaceShotDirection = zone.filterValue + 2;
-
- LevelDifficultyParams params = getDifficultyParams();
- int collisionDamage = (params.dodgeDamage >= 0) ? params.dodgeDamage : 0;
- if (!_rebelInvulnerable) {
- _playerDamage += collisionDamage;
- if (_playerDamage > 255)
- _playerDamage = 255;
- }
- _rebelHitCounter++;
- initDamageFlash();
- // Pan based on ship X position relative to screen center
- playSfx(1, 127, CLIP((_flyShipScreenX - 212) * 127 / 160, -127, 127));
- debug("Rebel2: Handler7 Mode0/2 OBSTACLE HIT zone=%d ship=(%d,%d) damage=%d",
- i, _flyShipScreenX, _flyShipScreenY, collisionDamage);
- break; // Only one collision per frame (original breaks)
- } else {
- // Safely avoided obstacle â award score
- // Original: FUN_0041bf8d(DAT_0047e100[levelIdx])
- LevelDifficultyParams scoreParams = getDifficultyParams();
- if (scoreParams.dodgePoints > 0) {
- addScore(scoreParams.dodgePoints);
- }
- }
- }
+void InsaneRebel2::awardHandler7DodgeScore() {
+ // Safely avoided obstacle - award score.
+ // Original: FUN_0041bf8d(DAT_0047e100[levelIdx]).
+ LevelDifficultyParams scoreParams = getDifficultyParams();
+ if (scoreParams.dodgePoints > 0) {
+ addScore(scoreParams.dodgePoints);
+ }
+}
+
+void InsaneRebel2::checkHandler7ObstacleZones(uint16 &warningMask) {
+ // ---- Mode 0/2: Obstacle collision using SECONDARY zones (FUN_403b5b) ----
+ // Original lines 52-132: Point-in-quad test with 15px inward margin.
+ // Inside the quad = collision with obstacle.
+ const int margin = 15; // local_14 = 0x0f, local_20 = 0x0f
- // FUN_40E35E line 104: mark near-danger proximity for shadow cue rendering.
- // Uses the low byte of zone.filterValue (retail local_1c) to pick direction bits.
- if (zone.field2 - 13 < zone.field1) {
- uint32 bit = 4u << ((byte)zone.filterValue & 0x1f);
- warningMask = (uint16)(warningMask | (uint16)bit);
+ for (int i = 0; i < _secondaryZoneCount; i++) {
+ CollisionZone &zone = _secondaryZones[i];
+ if (!zone.active)
+ continue;
+
+ bool inside = isHandler7ShipInsideObstacleZone(zone, margin);
+
+ // Frame match: field2 - 1 == field1 (line 90).
+ if (zone.field2 - 1 == zone.field1) {
+ if (inside) {
+ applyHandler7ObstacleHit(zone, i);
+ break; // Only one collision per frame (original breaks).
+ } else {
+ awardHandler7DodgeScore();
}
}
- // Corridor side proximity (FUN_40E35E lines 127-131)
- if (_flyShipScreenX < _corridorLeftX + 0x28)
- warningMask |= 1;
- if (_corridorRightX - 0x28 < _flyShipScreenX)
- warningMask |= 2;
+ // FUN_40E35E line 104: mark near-danger proximity for shadow cue rendering.
+ // Uses the low byte of zone.filterValue (retail local_1c) to pick direction bits.
+ if (zone.field2 - 13 < zone.field1) {
+ uint32 bit = 4u << ((byte)zone.filterValue & 0x1f);
+ warningMask = (uint16)(warningMask | (uint16)bit);
+ }
+ }
- } else {
- // ---- Mode 1/3: Wall/boundary collision using PRIMARY zones (FUN_403b34) ----
- // Original lines 133-235: Per-edge interpolation with push-back.
- // Ship position is clamped to wall boundaries when hitting.
- int16 hMargin = (_flyControlMode == 1) ? 0x28 : 0x0f; // local_14
- const int16 vMargin = 0x0f; // local_20
- LevelDifficultyParams wallParams = getDifficultyParams();
- int wallDamage = (wallParams.dodgeDamage >= 0) ? wallParams.dodgeDamage : 0;
-
- for (int i = 0; i < _primaryZoneCount; i++) {
- CollisionZone &zone = _primaryZones[i];
- if (!zone.active)
- continue;
+ // Corridor side proximity (FUN_40E35E lines 127-131).
+ if (_flyShipScreenX < _corridorLeftX + 0x28)
+ warningMask |= 1;
+ if (_corridorRightX - 0x28 < _flyShipScreenX)
+ warningMask |= 2;
+}
- int x1 = zone.x1, y1 = zone.y1;
- int x2 = zone.x2, y2 = zone.y2;
- int x3 = zone.x3, y3 = zone.y3;
- int x4 = zone.x4, y4 = zone.y4;
-
- // Top edge: interpolate Y along v1âv2 at shipX (lines 152-166)
- if (x2 != x1) {
- int16 edgeY = (int16)((_flyShipScreenX - x1) * (y2 - y1) / (x2 - x1) + y1 + vMargin);
- if (_flyShipScreenY < edgeY) {
- // Ship above top wall â push down
- if (_hitCooldown < 5 && !_rebelInvulnerable) {
- int damage = wallDamage;
- _playerDamage += damage;
- if (_playerDamage > 255)
- _playerDamage = 255;
- _rebelHitCounter++;
- _hitCooldown = 10;
- debug("Rebel2: Handler7 Mode1/3 TOP WALL ship=(%d,%d) edgeY=%d damage=%d",
- _flyShipScreenX, _flyShipScreenY, edgeY, damage);
- }
- _spaceShotDirection = 2; // Direction: pushed down
- _flyShipScreenY = edgeY; // Push-back
- playSfx(1, 127, 0); // CRASH.SAD, top wall â center pan (always)
- initDamageFlash();
- } else if (_flyShipScreenY < edgeY + 0x28) {
- warningMask |= 4;
- }
+bool InsaneRebel2::applyHandler7WallDamage(int wallDamage) {
+ if (_hitCooldown < 5 && !_rebelInvulnerable) {
+ _playerDamage += wallDamage;
+ if (_playerDamage > 255)
+ _playerDamage = 255;
+ _rebelHitCounter++;
+ _hitCooldown = 10;
+ return true;
+ }
+
+ return false;
+}
+
+void InsaneRebel2::resetHandler7HorizontalVelocity(int16 velocity) {
+ for (int j = 0; j < ARRAYSIZE(_velocityHistory); j++) {
+ _velocityHistory[j] = velocity;
+ }
+}
+
+void InsaneRebel2::checkHandler7TopBoundary(const InsaneRebel2::CollisionZone &zone, int16 vMargin, int wallDamage, uint16 &warningMask) {
+ int x1 = zone.x1, y1 = zone.y1;
+ int x2 = zone.x2, y2 = zone.y2;
+
+ // Top edge: interpolate Y along v1->v2 at shipX (lines 152-166).
+ if (x2 != x1) {
+ int16 edgeY = (int16)((_flyShipScreenX - x1) * (y2 - y1) / (x2 - x1) + y1 + vMargin);
+ if (_flyShipScreenY < edgeY) {
+ // Ship above top wall - push down.
+ if (applyHandler7WallDamage(wallDamage)) {
+ debug("Rebel2: Handler7 Mode1/3 TOP WALL ship=(%d,%d) edgeY=%d damage=%d",
+ _flyShipScreenX, _flyShipScreenY, edgeY, wallDamage);
}
+ _spaceShotDirection = 2; // Direction: pushed down
+ _flyShipScreenY = edgeY; // Push-back
+ playSfx(1, 127, 0); // CRASH.SAD, top wall -> center pan (always)
+ initDamageFlash();
+ } else if (_flyShipScreenY < edgeY + 0x28) {
+ warningMask |= 4;
+ }
+ }
+}
- // Bottom edge: interpolate Y along v4âv3 at shipX (lines 167-183)
- if (x3 != x4) {
- int16 edgeY = (int16)((_flyShipScreenX - x4) * (y3 - y4) / (x3 - x4) + y4 - vMargin);
- _corridorBottomY = vMargin + edgeY; // DAT_00443b10 update
- if (edgeY < _flyShipScreenY) {
- // Ship below bottom wall â push up
- if (_hitCooldown < 5 && !_rebelInvulnerable) {
- int damage = wallDamage;
- _playerDamage += damage;
- if (_playerDamage > 255)
- _playerDamage = 255;
- _rebelHitCounter++;
- _hitCooldown = 10;
- debug("Rebel2: Handler7 Mode1/3 BOTTOM WALL ship=(%d,%d) edgeY=%d damage=%d",
- _flyShipScreenX, _flyShipScreenY, edgeY, damage);
- }
- _spaceShotDirection = 3; // Direction: pushed up
- _flyShipScreenY = edgeY; // Push-back
- playSfx(1, 127, 0); // CRASH.SAD, bottom wall â center pan (always)
- initDamageFlash();
- } else if (edgeY - 0x28 < _flyShipScreenY) {
- warningMask |= 8;
- }
+void InsaneRebel2::checkHandler7BottomBoundary(const InsaneRebel2::CollisionZone &zone, int16 vMargin, int wallDamage, uint16 &warningMask) {
+ int x3 = zone.x3, y3 = zone.y3;
+ int x4 = zone.x4, y4 = zone.y4;
+
+ // Bottom edge: interpolate Y along v4->v3 at shipX (lines 167-183).
+ if (x3 != x4) {
+ int16 edgeY = (int16)((_flyShipScreenX - x4) * (y3 - y4) / (x3 - x4) + y4 - vMargin);
+ _corridorBottomY = vMargin + edgeY; // DAT_00443b10 update
+ if (edgeY < _flyShipScreenY) {
+ // Ship below bottom wall - push up.
+ if (applyHandler7WallDamage(wallDamage)) {
+ debug("Rebel2: Handler7 Mode1/3 BOTTOM WALL ship=(%d,%d) edgeY=%d damage=%d",
+ _flyShipScreenX, _flyShipScreenY, edgeY, wallDamage);
}
+ _spaceShotDirection = 3; // Direction: pushed up
+ _flyShipScreenY = edgeY; // Push-back
+ playSfx(1, 127, 0); // CRASH.SAD, bottom wall -> center pan (always)
+ initDamageFlash();
+ } else if (edgeY - 0x28 < _flyShipScreenY) {
+ warningMask |= 8;
+ }
+ }
+}
- // Left edge: interpolate X along v1âv4 at shipY (lines 184-199)
- if (y4 != y1) {
- int16 edgeX = (int16)((_flyShipScreenY - y1) * (x4 - x1) / (y4 - y1) + x1 + hMargin);
- if (_flyShipScreenX < edgeX) {
- // Ship left of left wall â push right
- _flyShipScreenX = edgeX; // Push-back
+void InsaneRebel2::checkHandler7LeftBoundary(const InsaneRebel2::CollisionZone &zone, int16 hMargin, int wallDamage) {
+ int x1 = zone.x1, y1 = zone.y1;
+ int x4 = zone.x4, y4 = zone.y4;
- // FUN_40E35E resets horizontal history to force immediate rightward correction.
- for (int j = 0; j < ARRAYSIZE(_velocityHistory); j++) {
- _velocityHistory[j] = 127;
- }
+ // Left edge: interpolate X along v1->v4 at shipY (lines 184-199).
+ if (y4 != y1) {
+ int16 edgeX = (int16)((_flyShipScreenY - y1) * (x4 - x1) / (y4 - y1) + x1 + hMargin);
+ if (_flyShipScreenX < edgeX) {
+ // Ship left of left wall - push right.
+ _flyShipScreenX = edgeX; // Push-back
- if (_hitCooldown < 5 && !_rebelInvulnerable) {
- int damage = wallDamage;
- _playerDamage += damage;
- if (_playerDamage > 255)
- _playerDamage = 255;
- _rebelHitCounter++;
- _hitCooldown = 10;
- debug("Rebel2: Handler7 Mode1/3 LEFT WALL ship=(%d,%d) edgeX=%d damage=%d",
- _flyShipScreenX, _flyShipScreenY, edgeX, damage);
- }
- _spaceShotDirection = 0; // Direction: pushed right
- playSfx(1, 127, -100); // CRASH.SAD, left wall â pan left (always)
- initDamageFlash();
- }
+ // FUN_40E35E resets horizontal history to force immediate rightward correction.
+ resetHandler7HorizontalVelocity(127);
+
+ if (applyHandler7WallDamage(wallDamage)) {
+ debug("Rebel2: Handler7 Mode1/3 LEFT WALL ship=(%d,%d) edgeX=%d damage=%d",
+ _flyShipScreenX, _flyShipScreenY, edgeX, wallDamage);
}
+ _spaceShotDirection = 0; // Direction: pushed right
+ playSfx(1, 127, -100); // CRASH.SAD, left wall -> pan left (always)
+ initDamageFlash();
+ }
+ }
+}
- // Right edge: interpolate X along v2âv3 at shipY (lines 200-215)
- if (y3 != y2) {
- int16 edgeX = (int16)((_flyShipScreenY - y2) * (x3 - x2) / (y3 - y2) + x2 - hMargin);
- if (edgeX < _flyShipScreenX) {
- // Ship right of right wall â push left
- _flyShipScreenX = edgeX; // Push-back
+void InsaneRebel2::checkHandler7RightBoundary(const InsaneRebel2::CollisionZone &zone, int16 hMargin, int wallDamage) {
+ int x2 = zone.x2, y2 = zone.y2;
+ int x3 = zone.x3, y3 = zone.y3;
- // FUN_40E35E resets horizontal history to force immediate leftward correction.
- for (int j = 0; j < ARRAYSIZE(_velocityHistory); j++) {
- _velocityHistory[j] = -127;
- }
+ // Right edge: interpolate X along v2->v3 at shipY (lines 200-215).
+ if (y3 != y2) {
+ int16 edgeX = (int16)((_flyShipScreenY - y2) * (x3 - x2) / (y3 - y2) + x2 - hMargin);
+ if (edgeX < _flyShipScreenX) {
+ // Ship right of right wall - push left.
+ _flyShipScreenX = edgeX; // Push-back
- if (_hitCooldown < 5 && !_rebelInvulnerable) {
- int damage = wallDamage;
- _playerDamage += damage;
- if (_playerDamage > 255)
- _playerDamage = 255;
- _rebelHitCounter++;
- _hitCooldown = 10;
- debug("Rebel2: Handler7 Mode1/3 RIGHT WALL ship=(%d,%d) edgeX=%d damage=%d",
- _flyShipScreenX, _flyShipScreenY, edgeX, damage);
- }
- _spaceShotDirection = 1; // Direction: pushed left
- playSfx(1, 127, 100); // CRASH.SAD, right wall â pan right (always)
- initDamageFlash();
- }
+ // FUN_40E35E resets horizontal history to force immediate leftward correction.
+ resetHandler7HorizontalVelocity(-127);
+
+ if (applyHandler7WallDamage(wallDamage)) {
+ debug("Rebel2: Handler7 Mode1/3 RIGHT WALL ship=(%d,%d) edgeX=%d damage=%d",
+ _flyShipScreenX, _flyShipScreenY, edgeX, wallDamage);
}
+ _spaceShotDirection = 1; // Direction: pushed left
+ playSfx(1, 127, 100); // CRASH.SAD, right wall -> pan right (always)
+ initDamageFlash();
}
}
+}
+
+void InsaneRebel2::checkHandler7BoundaryZones(uint16 &warningMask) {
+ // ---- Mode 1/3: Wall/boundary collision using PRIMARY zones (FUN_403b34) ----
+ // Original lines 133-235: Per-edge interpolation with push-back.
+ // Ship position is clamped to wall boundaries when hitting.
+ int16 hMargin = (_flyControlMode == 1) ? 0x28 : 0x0f; // local_14
+ const int16 vMargin = 0x0f; // local_20
+ LevelDifficultyParams wallParams = getDifficultyParams();
+ int wallDamage = (wallParams.dodgeDamage >= 0) ? wallParams.dodgeDamage : 0;
+ for (int i = 0; i < _primaryZoneCount; i++) {
+ CollisionZone &zone = _primaryZones[i];
+ if (!zone.active)
+ continue;
+
+ checkHandler7TopBoundary(zone, vMargin, wallDamage, warningMask);
+ checkHandler7BottomBoundary(zone, vMargin, wallDamage, warningMask);
+ checkHandler7LeftBoundary(zone, hMargin, wallDamage);
+ checkHandler7RightBoundary(zone, hMargin, wallDamage);
+ }
+}
+
+void InsaneRebel2::renderHandler7WarningCues(byte *renderBitmap, int pitch, int width, int height, int32 curFrame, uint16 warningMask) {
// FUN_40E35E tail: draw proximity danger shadow cues when enabled by frame/flags.
// Note: These are cue sprites (often perceived as "shadows"), not the aiming reticle.
LevelDifficultyParams dparams = getDifficultyParams();
@@ -1920,6 +1936,29 @@ void InsaneRebel2::checkHandler7CollisionZones(byte *renderBitmap, int pitch, in
}
}
+void InsaneRebel2::checkHandler7CollisionZones(byte *renderBitmap, int pitch, int width, int height, int32 curFrame) {
+ // FUN_40E35E - Handler 7 per-frame collision system.
+ // Uses ship position (_flyShipScreenX/_flyShipScreenY) in raw buffer coords.
+ // Two modes depending on _flyControlMode:
+ // Mode 0/2: Obstacle collision using SECONDARY zones (inside quad = hit)
+ // Mode 1/3: Wall/boundary collision using PRIMARY zones (per-edge push-back)
+
+ // Note: _hitCooldown is decremented in renderSpaceExplosions (FUN_40F1C5)
+ // to match the original where the decrement happens during rendering.
+ //
+ // local_c in FUN_40E35E: proximity mask for nearby danger-zone shadow cues.
+ // bit 0=left, bit 1=right, bit 2=top, bit 3=bottom
+ uint16 warningMask = 0;
+
+ if (_flyControlMode == 0 || _flyControlMode == 2) {
+ checkHandler7ObstacleZones(warningMask);
+ } else {
+ checkHandler7BoundaryZones(warningMask);
+ }
+
+ renderHandler7WarningCues(renderBitmap, pitch, width, height, curFrame, warningMask);
+}
+
// renderNutSprite -- Draw a NUT sprite with transparency.
void InsaneRebel2::renderNutSprite(byte *dst, int pitch, int width, int height, int x, int y, NutRenderer *nut, int spriteIdx) {
renderNutSpriteMirrored(dst, pitch, width, height, x, y, nut, spriteIdx, false);
Commit: 1da3f8d5805fc65ce0c145468135cc7cf7d35be9
https://github.com/scummvm/scummvm/commit/1da3f8d5805fc65ce0c145468135cc7cf7d35be9
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:59+02:00
Commit Message:
SCUMM: RA2: Use original cursor instead of generic one
Changed paths:
engines/scumm/insane/rebel2/rebel.h
engines/scumm/insane/rebel2/render.cpp
diff --git a/engines/scumm/insane/rebel2/rebel.h b/engines/scumm/insane/rebel2/rebel.h
index 3fb850bc78a..8b4d436768d 100644
--- a/engines/scumm/insane/rebel2/rebel.h
+++ b/engines/scumm/insane/rebel2/rebel.h
@@ -540,7 +540,7 @@ public:
void updatePostRenderScroll(int width, int height);
void updatePostRenderDeath();
- void showPostRenderMenuCursor();
+ void renderPostRenderMenuCursor(byte *renderBitmap, int pitch, int width, int height);
bool handlePostRenderMenuModes(byte *renderBitmap, int pitch, int width, int height, bool introPlaying);
bool handlePostRenderIntro(byte *renderBitmap, int pitch, int width, int height, int32 curFrame);
void renderGameplayPostFrame(byte *renderBitmap, int pitch, int width, int height,
diff --git a/engines/scumm/insane/rebel2/render.cpp b/engines/scumm/insane/rebel2/render.cpp
index 04ce01e7f48..060ca42f8fb 100644
--- a/engines/scumm/insane/rebel2/render.cpp
+++ b/engines/scumm/insane/rebel2/render.cpp
@@ -24,7 +24,6 @@
#include "common/util.h"
#include "graphics/cursorman.h"
-#include "graphics/wincursor.h"
#include "scumm/scumm_v7.h"
@@ -2109,12 +2108,47 @@ void InsaneRebel2::updatePostRenderDeath() {
}
}
-// showPostRenderMenuCursor -- Restore the default cursor for menu videos.
-void InsaneRebel2::showPostRenderMenuCursor() {
- Graphics::Cursor *cursor = Graphics::makeDefaultWinCursor();
- CursorMan.replaceCursor(cursor);
- delete cursor;
- CursorMan.showMouse(true);
+// renderPostRenderMenuCursor -- Draw RA2's software cursor for menu videos.
+void InsaneRebel2::renderPostRenderMenuCursor(byte *renderBitmap, int pitch, int width, int height) {
+ // Original menu stages call FUN_0042a6d0() to show the game's software
+ // cursor object. Slot 0 is initialized by FUN_0042a660() from the cursor
+ // table embedded in RA2WIN95.EXE at VA 0x482f30 (7x10, hotspot 0,0).
+ // FUN_0042a660() passes 1 as the transparent color to FUN_00430380().
+ static const byte kRa2MenuCursor[] = {
+ 0, 0, 1, 1, 1, 1, 1,
+ 0, 15, 0, 1, 1, 1, 1,
+ 0, 15, 15, 0, 1, 1, 1,
+ 0, 15, 15, 15, 0, 1, 1,
+ 0, 15, 15, 15, 15, 0, 1,
+ 0, 15, 15, 15, 15, 15, 0,
+ 0, 15, 15, 15, 0, 0, 0,
+ 0, 15, 0, 15, 15, 1, 1,
+ 0, 0, 0, 0, 15, 0, 1,
+ 1, 1, 1, 0, 0, 0, 1
+ };
+ const int cursorWidth = 7;
+ const int cursorHeight = 10;
+
+ CursorMan.showMouse(false);
+ _vm->_system->showMouse(false);
+
+ const int cursorX = _vm->_mouse.x;
+ const int cursorY = _vm->_mouse.y;
+ for (int y = 0; y < cursorHeight; y++) {
+ int dstY = cursorY + y;
+ if (dstY < 0 || dstY >= height)
+ continue;
+
+ for (int x = 0; x < cursorWidth; x++) {
+ int dstX = cursorX + x;
+ if (dstX < 0 || dstX >= width)
+ continue;
+
+ byte color = kRa2MenuCursor[y * cursorWidth + x];
+ if (color != 1)
+ renderBitmap[dstY * pitch + dstX] = color;
+ }
+ }
}
// handlePostRenderMenuModes -- Process menu-like videos drawn during post-rendering.
@@ -2127,13 +2161,12 @@ bool InsaneRebel2::handlePostRenderMenuModes(byte *renderBitmap, int pitch, int
// Handle pilot selection input and rendering (FUN_00414A41).
// This is the pilot/save slot selection screen with centered menu.
if (pilotSelectMode) {
- showPostRenderMenuCursor();
-
// Process pilot selection input - emulates FUN_00414A41 input handling.
int selection = processLevelSelectInput();
// Draw pilot selection overlay - centered menu like main menu.
drawLevelSelectOverlay(renderBitmap, pitch, width, height);
+ renderPostRenderMenuCursor(renderBitmap, pitch, width, height);
// If a selection was confirmed, signal video to stop.
if (selection >= 0) {
@@ -2149,8 +2182,6 @@ bool InsaneRebel2::handlePostRenderMenuModes(byte *renderBitmap, int pitch, int
// Handle chapter selection input and rendering (FUN_00415CF8).
// This is the actual level/chapter selection screen with preview and password.
if (chapterSelectMode) {
- showPostRenderMenuCursor();
-
// O_LEVEL.SAN provides the background with chapter preview thumbnails.
// The FOBJ offset system (set in procPreRendering) scrolls the correct preview
// into the preview box area. No black fill needed â video frame shows through.
@@ -2160,6 +2191,7 @@ bool InsaneRebel2::handlePostRenderMenuModes(byte *renderBitmap, int pitch, int
// Draw chapter selection overlay - emulates FUN_00415CF8 rendering.
drawChapterSelectOverlay(renderBitmap, pitch, width, height);
+ renderPostRenderMenuCursor(renderBitmap, pitch, width, height);
// If a selection was confirmed, signal video to stop.
if (selection >= 0) {
@@ -2182,15 +2214,11 @@ bool InsaneRebel2::handlePostRenderMenuModes(byte *renderBitmap, int pitch, int
if (introPlaying && _gameState == kStateOptions) {
processOptionsInput();
drawOptionsOverlay(renderBitmap, pitch, width, height);
+ renderPostRenderMenuCursor(renderBitmap, pitch, width, height);
return true;
}
if (menuMode) {
- // The original game uses the standard Windows arrow cursor (IDC_ARROW)
- // loaded via LoadCursorA(NULL, 0x7f00) in FUN_420C70.decompiled.txt.
- // MSTOVER.NUT is a background overlay, NOT a cursor.
- showPostRenderMenuCursor();
-
// Process menu input during each frame.
int selection = processMenuInput();
@@ -2214,6 +2242,7 @@ bool InsaneRebel2::handlePostRenderMenuModes(byte *renderBitmap, int pitch, int
// Draw menu selection overlay.
drawMenuOverlay(renderBitmap, pitch, width, height);
+ renderPostRenderMenuCursor(renderBitmap, pitch, width, height);
// If a selection was confirmed, signal video to stop.
if (selection >= 0) {
Commit: d027a4158db545ab2fe5c1a72e353dad9eea8b25
https://github.com/scummvm/scummvm/commit/d027a4158db545ab2fe5c1a72e353dad9eea8b25
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:59+02:00
Commit Message:
SCUMM: RA2: Render full shooting rays
Changed paths:
engines/scumm/insane/rebel2/iact.cpp
engines/scumm/insane/rebel2/render.cpp
diff --git a/engines/scumm/insane/rebel2/iact.cpp b/engines/scumm/insane/rebel2/iact.cpp
index 2a050339962..fd69da34581 100644
--- a/engines/scumm/insane/rebel2/iact.cpp
+++ b/engines/scumm/insane/rebel2/iact.cpp
@@ -1578,14 +1578,11 @@ bool InsaneRebel2::loadOpcode8EdgeTable(Common::SeekableReadStream &b, int64 sta
// If so, loads a per-level 256x256 color blend table from the IACT chunk data.
// This table controls the edge glow color of laser beams (e.g. red vs green).
// Data starts at byte offset 18 in the IACT chunk (in_stack_00000014 + 9 shorts).
- if (par4 == 1000 && remaining >= 18 + 8 + 32896) {
- // Layout: 18 bytes IACT header params already consumed by caller,
- // but 'b' is positioned at startPos which is after par1..par4.
- // The original code passes (param + 9 shorts) = data at byte offset 18 from chunk start.
- // Since our stream starts after the 6 par shorts (12 bytes), the data is at offset 6 from startPos.
+ // The stream is positioned after par1..par4 (8 bytes), so retail's +18 is startPos + 10.
+ if (par4 == 1000 && remaining >= 10 + 8 + 32896) {
byte *edgeData = (byte *)malloc(8 + 32896);
if (edgeData) {
- b.seek(startPos + 6); // Skip 3 remaining shorts (par2, par3, par4 already read; 3 more padding shorts)
+ b.seek(startPos + 10);
b.read(edgeData, 8 + 32896);
initEdgeTable(edgeData);
free(edgeData);
diff --git a/engines/scumm/insane/rebel2/render.cpp b/engines/scumm/insane/rebel2/render.cpp
index 060ca42f8fb..5746004151c 100644
--- a/engines/scumm/insane/rebel2/render.cpp
+++ b/engines/scumm/insane/rebel2/render.cpp
@@ -1326,13 +1326,9 @@ void InsaneRebel2::drawLaserBeam(byte *dst, int pitch, int width, int height,
byte *texPixels = _laserTexture.pixels;
// FUN_0040BBF6 line 23: sVar7 = (thickness * animFrame * 16) / maxFrames
- // Tuned beam segment spacing: 60% of original.
- constexpr int kBeamAnimScaleNumerator = 48; // 16 * 0.6 * 5
- constexpr int kBeamAnimScaleDenominator = 5;
if (maxFrames == 0)
maxFrames = 1;
- int16 sVar7 = (int16)(((int)thickness * (int)animFrame * kBeamAnimScaleNumerator) /
- ((int)maxFrames * kBeamAnimScaleDenominator));
+ int16 sVar7 = (int16)(((int)thickness * (int)animFrame * 16) / (int)maxFrames);
// FUN_0040BBF6 lines 24-25: Calculate delta with scaling
int16 dx = targetX - gunX;
Commit: 2771d282b951bc1492feaf3b7ef9c1e6af8f5767
https://github.com/scummvm/scummvm/commit/2771d282b951bc1492feaf3b7ef9c1e6af8f5767
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:10:00+02:00
Commit Message:
SCUMM: RA1: Improve level 8 UI rendering
Changed paths:
engines/scumm/insane/rebel1/iact.cpp
engines/scumm/insane/rebel1/rebel.cpp
engines/scumm/insane/rebel1/rebel.h
engines/scumm/insane/rebel1/render.cpp
engines/scumm/insane/rebel1/runlevels.cpp
engines/scumm/smush/rebel/smush_player_ra1.cpp
engines/scumm/smush/rebel/smush_player_ra1.h
diff --git a/engines/scumm/insane/rebel1/iact.cpp b/engines/scumm/insane/rebel1/iact.cpp
index b66c6d91bf1..6d8233ab8ea 100644
--- a/engines/scumm/insane/rebel1/iact.cpp
+++ b/engines/scumm/insane/rebel1/iact.cpp
@@ -113,12 +113,6 @@ const int16 kLevel7BranchThreshold[6] = {
0, 170, 170, 160, 160, 160
};
-const int16 kLevel8BranchFrames[3][3] = {
- { 2588, 1709, 262 },
- { 2323, 1444, -2 },
- { 877, -2, -2 }
-};
-
inline bool isLevel4DamageLatch(uint16 code) {
switch (code) {
case 0x0008:
@@ -501,14 +495,15 @@ void InsaneRebel1::checkDynamicLevelBranch() {
if (!_interactiveVideoActive || _levelRouteIndex < 0)
return;
- if (_currentLevel == 6 && _pendingRouteIndex >= 0) {
+ if ((_currentLevel == 6 || _currentLevel == 7) && _pendingRouteIndex >= 0) {
+ const uint32 routeFrame = (_currentLevel == 7) ? (uint32)_gameCounter : _frameCounter;
if (!_vm->_smushVideoShouldFinish &&
_pendingRouteCutoverFrame >= 0 &&
- _frameCounter >= (uint32)_pendingRouteCutoverFrame) {
+ routeFrame >= (uint32)_pendingRouteCutoverFrame) {
_vm->_smushVideoShouldFinish = true;
- debug(1, "RA1 L7 cutover: route=%d -> %d at frame=%u (resumeTimelineFrame=%d)",
- _levelRouteIndex, _pendingRouteIndex, (unsigned)_frameCounter,
- (int)_pendingRouteStartFrame);
+ debug(1, "RA1 L%d cutover: route=%d -> %d at frame=%u (resumeTimelineFrame=%d)",
+ _currentLevel + 1, _levelRouteIndex, _pendingRouteIndex,
+ (unsigned)routeFrame, (int)_pendingRouteStartFrame);
}
return;
}
@@ -539,45 +534,9 @@ void InsaneRebel1::checkDynamicLevelBranch() {
}
}
- if (_currentLevel == 7) {
- const int route = CLIP<int>(_levelRouteIndex, 0, 2);
- const int frame = (int)_frameCounter;
- const int leftBlockedFrame = kLevel8BranchFrames[route][2];
- const int rightBlockedFrame = kLevel8BranchFrames[route][1];
- const bool shotEdge = _playerFired && _fireCooldown == 0;
- int nextRoute = -1;
-
- for (int i = 0; i < 3; ++i) {
- const int triggerFrame = kLevel8BranchFrames[route][i];
- if (triggerFrame < 0)
- continue;
-
- if (shotEdge && frame > triggerFrame - 0x32 && frame <= triggerFrame)
- _levelRouteChoice = (_shipPosX < kRA1CenterX) ? 1 : 2;
-
- if (frame != triggerFrame)
- continue;
-
- const bool chooseLeft = (_levelRouteChoice == 1) ||
- ((_shipPosX < kRA1CenterX) && (_levelRouteChoice != 2));
- if (chooseLeft) {
- if (frame != leftBlockedFrame)
- nextRoute = 1;
- } else {
- if (frame != rightBlockedFrame)
- nextRoute = 2;
- }
- _levelRouteChoice = 0;
- break;
- }
-
- if (nextRoute >= 0 && nextRoute != route) {
- _pendingRouteIndex = nextRoute;
- _vm->_smushVideoShouldFinish = true;
- debug(1, "RA1 L8 branch: route=%d -> %d at frame=%u shipX=%d",
- route, nextRoute, (unsigned)_frameCounter, _shipPosX);
- }
- }
+ // Level 8 owns its branch choice in updateLevel8WalkerState(), where the
+ // original RunLevel8Flow also draws the timer/arrows and updates the local
+ // choice variable. This function only performs the delayed route cutover.
}
void InsaneRebel1::projectGameplayPoint(int16 &x, int16 &y) const {
@@ -1208,16 +1167,17 @@ void InsaneRebel1::updateAsteroidPhysics() {
_damageFlags |= 0x20;
if (_currentLevel == 7) {
+ const uint16 walkerFrame = (uint16)_gameCounter;
bool walkerHazard = false;
switch (CLIP<int>(_levelRouteIndex, 0, 2)) {
case 0:
- walkerHazard = hasLevel8PerspectiveHazardRoute0((uint16)_frameCounter, _perspectiveX, _perspectiveY);
+ walkerHazard = hasLevel8PerspectiveHazardRoute0(walkerFrame, _perspectiveX, _perspectiveY);
break;
case 1:
- walkerHazard = hasLevel8PerspectiveHazardRoute1((uint16)_frameCounter, _perspectiveX, _perspectiveY);
+ walkerHazard = hasLevel8PerspectiveHazardRoute1(walkerFrame, _perspectiveX, _perspectiveY);
break;
case 2:
- walkerHazard = hasLevel8PerspectiveHazardRoute2((uint16)_frameCounter, _perspectiveX, _perspectiveY);
+ walkerHazard = hasLevel8PerspectiveHazardRoute2(walkerFrame, _perspectiveX, _perspectiveY);
break;
}
@@ -1301,6 +1261,7 @@ void InsaneRebel1::updateAsteroidPhysics() {
_level2JoystickFilteredX = 0;
_level2JoystickFilteredY = 0;
}
+ _inputAxisDeltaX = inputX;
debug("RA1 asteroid input: source=%s axis=(%d,%d) mouse=(%d,%d) actions(L,R,U,D)=(%d,%d,%d,%d) raw=(%d,%d) final=(%d,%d) level=%d opcode=0x%X",
inputSourceName,
@@ -1588,6 +1549,7 @@ void InsaneRebel1::handleGameChunk(int32 subSize, Common::SeekableReadStream &b)
memset(_inputHistoryY, 0, sizeof(_inputHistoryY));
memset(_viewHistoryX, 0, sizeof(_viewHistoryX));
memset(_viewHistoryY, 0, sizeof(_viewHistoryY));
+ _inputAxisDeltaX = 0;
_avgInputX = 0;
_avgInputY = 0;
_mouseOffsetX = 0;
@@ -1622,6 +1584,8 @@ void InsaneRebel1::handleGameChunk(int32 subSize, Common::SeekableReadStream &b)
// Field1 == 0 corresponds to baseline recenter behavior in the original.
if ((int32)param1 == 0) {
+ _perspectiveX = 0x20;
+ _perspectiveY = 0x17;
_shipPosX = kRA1CenterX;
_shipPosY = kRA1CenterY;
}
@@ -1630,9 +1594,12 @@ void InsaneRebel1::handleGameChunk(int32 subSize, Common::SeekableReadStream &b)
_frameGameOpcodeMask = 0;
_frameDispatchFlags = 0;
resetProjectionTable();
+ if ((int32)param1 == 0)
+ syncViewportOffset(true);
- // RunLevel8Flow seeds the walker armor mask after the interactive segment
- // has entered its runtime reset path, not before playback begins.
+ // Original RunLevel8Flow initializes its separate g_level8HitboxBuffer
+ // after the first L8PLAY runtime reset. We fold that mask into the
+ // secondary half of _frameObjectState; route resumes preserve it.
if (_currentLevel == 7)
memset(_frameObjectState + 150, 0xFF, 150);
@@ -1792,6 +1759,11 @@ void InsaneRebel1::handleGameChunk(int32 subSize, Common::SeekableReadStream &b)
}
}
debug(7, "RA1 GAME 0x0B: counter=%d", _gameCounter);
+ if (!_asteroidPhysicsUpdatedThisFrame) {
+ updateAsteroidPhysics();
+ _asteroidPhysicsUpdatedThisFrame = true;
+ syncViewportOffset(true);
+ }
break;
case 0x5A:
diff --git a/engines/scumm/insane/rebel1/rebel.cpp b/engines/scumm/insane/rebel1/rebel.cpp
index f108f223686..9dc75ef8307 100644
--- a/engines/scumm/insane/rebel1/rebel.cpp
+++ b/engines/scumm/insane/rebel1/rebel.cpp
@@ -242,6 +242,7 @@ InsaneRebel1::InsaneRebel1(ScummEngine_v7 *scumm) : Insane(), _vm(scumm) {
memset(_inputHistoryY, 0, sizeof(_inputHistoryY));
memset(_viewHistoryX, 0, sizeof(_viewHistoryX));
memset(_viewHistoryY, 0, sizeof(_viewHistoryY));
+ _inputAxisDeltaX = 0;
_avgInputX = 0;
_avgInputY = 0;
_mouseOffsetX = 0;
@@ -267,6 +268,7 @@ InsaneRebel1::InsaneRebel1(ScummEngine_v7 *scumm) : Insane(), _vm(scumm) {
_activeGameOpcode = 0;
_frameGameOpcodeMask = 0;
_frameDispatchFlags = 0;
+ _asteroidPhysicsUpdatedThisFrame = false;
_health = kMaxHealth;
_lives = 3;
@@ -293,7 +295,6 @@ InsaneRebel1::InsaneRebel1(ScummEngine_v7 *scumm) : Insane(), _vm(scumm) {
_pendingRouteIndex = -1;
_pendingRouteStartFrame = 0;
_pendingRouteCutoverFrame = -1;
- _levelRouteChoice = 0;
_levelGameplayPhase = 0;
_level5SuccessFramesRemaining = 0;
_menuActive = false;
diff --git a/engines/scumm/insane/rebel1/rebel.h b/engines/scumm/insane/rebel1/rebel.h
index 5a85489b373..53e7bb55573 100644
--- a/engines/scumm/insane/rebel1/rebel.h
+++ b/engines/scumm/insane/rebel1/rebel.h
@@ -161,8 +161,10 @@ private:
void renderGostScorePopup(byte *dst, int pitch, int width, int height,
int16 centerX, int16 centerY, int16 frame);
void renderLaserShots(byte *dst, int pitch, int width, int height);
- void renderLevel8Overlay(byte *dst, int pitch, int width, int height);
+ void renderLevel8Overlay(byte *dst, int pitch, int width, int height,
+ int viewportX, int viewportY);
void updateLevel8WalkerState();
+ void syncViewportOffset(bool usePerspectiveViewport);
void renderSprite(byte *dst, int pitch, int width, int height,
int x, int y, const RA1Sprite &sprite);
void updateGostSlotPosition(int16 targetIdx, int16 left, int16 top, int16 right, int16 bottom);
@@ -246,6 +248,7 @@ private:
int16 _inputHistoryY[kInputHistorySize]; // 0x7594: vertical input history
int16 _viewHistoryX[kInputHistorySize]; // 0x75A8: viewport horizontal history
int16 _viewHistoryY[kInputHistorySize]; // 0x75BC: viewport vertical history
+ int16 _inputAxisDeltaX; // Current 0x0B horizontal input sample, before history averaging
int16 _avgInputX; // smoothed horizontal input (clamped to [-0xA0, 0xA0])
int16 _avgInputY; // smoothed vertical input (clamped to [-0x46, 0x41])
int16 _mouseOffsetX; // 0x9762-style accumulated recenter offset in DOS 640-space
@@ -300,6 +303,7 @@ private:
uint16 _activeGameOpcode;
uint32 _frameGameOpcodeMask;
uint16 _frameDispatchFlags;
+ bool _asteroidPhysicsUpdatedThisFrame;
// Difficulty (0=easy, 1=normal, 2=hard) â matches original DAT_22BC
int _difficulty;
@@ -384,8 +388,7 @@ private:
int _levelRouteIndex; // Current mid-level route/segment for branching levels
int _pendingRouteIndex; // Next route requested by original frame-branch logic
int32 _pendingRouteStartFrame; // Resume frame for branch-driven route switches
- int32 _pendingRouteCutoverFrame; // Delayed inline route splice frame (Level 7 uses branchFrame + 7)
- int _levelRouteChoice; // Level-local pending branch choice (0=none, 1=left, 2=right)
+ int32 _pendingRouteCutoverFrame; // Delayed inline route splice frame (branchFrame + 7)
int _levelGameplayPhase; // Level-local interactive phase (e.g. LVL4 PLAY1 vs PLAY2)
// Main menu / options state
@@ -494,19 +497,6 @@ private:
static const int16 kWalkerAttackWindow2[3];
static const int16 kWalkerAttackWindow3[3];
- // Per-route damage frame tables â FUN_12fe1/FUN_130c9/FUN_13195
- // Each entry: {frameNumber, hitboxType} where hitboxType determines the check
- struct WalkerDamageFrame {
- int16 frame;
- int16 type; // 0=proximity(41,32), 1=offsetY(15), 2=offsetX(24), 3=proximity(40,31), 4=offsetY(31)
- };
- static const WalkerDamageFrame kWalkerDamageRoute0[];
- static const WalkerDamageFrame kWalkerDamageRoute1[];
- static const WalkerDamageFrame kWalkerDamageRoute2[];
- static const int kWalkerDamageRoute0Count;
- static const int kWalkerDamageRoute1Count;
- static const int kWalkerDamageRoute2Count;
-
static const int kFrameObjectStateBytes = 300;
byte _frameObjectState[kFrameObjectStateBytes];
};
diff --git a/engines/scumm/insane/rebel1/render.cpp b/engines/scumm/insane/rebel1/render.cpp
index c54c7580478..183ec966e64 100644
--- a/engines/scumm/insane/rebel1/render.cpp
+++ b/engines/scumm/insane/rebel1/render.cpp
@@ -365,6 +365,7 @@ void renderSpriteWithFlags(byte *dst, int pitch, int width, int height,
void InsaneRebel1::procPreRendering(byte *renderBitmap) {
_frameGameOpcodeMask = 0;
_frameDispatchFlags = 0;
+ _asteroidPhysicsUpdatedThisFrame = false;
if (_interactiveVideoActive && _player) {
const bool usePerspectiveViewport =
@@ -387,6 +388,28 @@ void InsaneRebel1::procPreRendering(byte *renderBitmap) {
}
}
+void InsaneRebel1::syncViewportOffset(bool usePerspectiveViewport) {
+ if (!_player)
+ return;
+
+ if (!usePerspectiveViewport) {
+ ra1Player()->_ra1ViewportOffsetX = 0;
+ ra1Player()->_ra1ViewportOffsetY = 0;
+ return;
+ }
+
+ ra1Player()->_ra1ViewportOffsetX = _perspectiveX;
+ ra1Player()->_ra1ViewportOffsetY = _perspectiveY;
+
+ // SetCameraOffset() applies random shake to the camera value used by the
+ // visible source-window origin. Store it once so FTCH restore, overlays, and
+ // the final crop share one viewport origin for this frontend frame.
+ if (_screenFlash > 0) {
+ ra1Player()->_ra1ViewportOffsetX += (int16)(_vm->_rnd.getRandomNumber(4) - 2);
+ ra1Player()->_ra1ViewportOffsetY += (int16)(_vm->_rnd.getRandomNumber(4) - 2);
+ }
+}
+
void InsaneRebel1::procPostRendering(byte *renderBitmap, int32 codecparam, int32 setupsan12,
int32 setupsan13, int32 curFrame, int32 maxFrame) {
if (_menuActive && renderBitmap) {
@@ -432,7 +455,11 @@ void InsaneRebel1::procPostRendering(byte *renderBitmap, int32 codecparam, int32
(_activeGameOpcode == 0x07 || _activeGameOpcode == 0x09));
if (asteroidMode) {
// First-person asteroid/surface handler â opcode 0x0B (FUN_1CDA7).
- updateAsteroidPhysics();
+ if (!_asteroidPhysicsUpdatedThisFrame) {
+ updateAsteroidPhysics();
+ _asteroidPhysicsUpdatedThisFrame = true;
+ syncViewportOffset(true);
+ }
// DOS 0x0B loops test health after each frontend frame and leave the
// interactive movie as soon as it drops below zero. Mirror that here so
@@ -487,22 +514,13 @@ void InsaneRebel1::procPostRendering(byte *renderBitmap, int32 codecparam, int32
// GAME handlers in the original update FUN_224FD during the same frame that
// the new control state is computed. Sync the current frame's viewport window
- // before HUD/screen copy so 0x0B doesn't lag one frame behind the mouse.
+ // before HUD/screen copy so overlays and final crop observe the same camera.
// On-foot mode uses SetCameraOffset(0,0) â no viewport crop.
if (_player) {
if (onFootMode || (!asteroidMode && !turretMode && !flightMode)) {
- ra1Player()->_ra1ViewportOffsetX = 0;
- ra1Player()->_ra1ViewportOffsetY = 0;
- } else {
- ra1Player()->_ra1ViewportOffsetX = _perspectiveX;
- ra1Player()->_ra1ViewportOffsetY = _perspectiveY;
- }
-
- // Screen shake â SetCameraOffset (0x224FD): random [-2,+2] jitter when
- // _screenFlash > 0. Original uses RandScaleByte(5) - 2 for each axis.
- if (_screenFlash > 0) {
- ra1Player()->_ra1ViewportOffsetX += (int16)(_vm->_rnd.getRandomNumber(4) - 2);
- ra1Player()->_ra1ViewportOffsetY += (int16)(_vm->_rnd.getRandomNumber(4) - 2);
+ syncViewportOffset(false);
+ } else if (!asteroidMode) {
+ syncViewportOffset(true);
}
}
@@ -563,7 +581,9 @@ void InsaneRebel1::procPostRendering(byte *renderBitmap, int32 codecparam, int32
// game loop. We call it from procPostRendering when _currentLevel == 7.
if (_currentLevel == 7) {
updateLevel8WalkerState();
- renderLevel8Overlay(renderBitmap, pitch, width, height);
+ const int viewportX = _player ? ra1Player()->_ra1ViewportOffsetX : 0;
+ const int viewportY = _player ? ra1Player()->_ra1ViewportOffsetY : 0;
+ renderLevel8Overlay(renderBitmap, pitch, width, height, viewportX, viewportY);
}
renderHUD(renderBitmap, pitch, width, height);
@@ -1331,32 +1351,13 @@ const int16 InsaneRebel1::kWalkerAttackWindow1[3] = { 2588, 2323, 877 };
const int16 InsaneRebel1::kWalkerAttackWindow2[3] = { 1709, 1444, -2 };
const int16 InsaneRebel1::kWalkerAttackWindow3[3] = { 262, -2, -2 };
-// Per-route walker damage frame tables â FUN_12fe1/FUN_130c9/FUN_13195.
-// type: 0=proximity(41,32), 1=offsetY(15), 2=offsetX(24), 3=proximity(40,31), 4=offsetY(31)
-const InsaneRebel1::WalkerDamageFrame InsaneRebel1::kWalkerDamageRoute0[] = {
- {0x00CD, 2}, {0x00EF, 2}, {0x0294, 1}, {0x03A2, 2},
- {0x04BE, 0}, {0x05C9, 2}, {0x076C, 0}, {0x085A, 4}, {0x096F, 2}
-};
-const int InsaneRebel1::kWalkerDamageRoute0Count = ARRAYSIZE(kWalkerDamageRoute0);
-
-const InsaneRebel1::WalkerDamageFrame InsaneRebel1::kWalkerDamageRoute1[] = {
- {0x0189, 1}, {0x0297, 2}, {0x03B3, 0}, {0x04BE, 4},
- {0x0661, 0}, {0x074F, 4}, {0x0864, 4}
-};
-const int InsaneRebel1::kWalkerDamageRoute1Count = ARRAYSIZE(kWalkerDamageRoute1);
-
-const InsaneRebel1::WalkerDamageFrame InsaneRebel1::kWalkerDamageRoute2[] = {
- {0x00BB, 0}, {0x01A9, 4}, {0x02BE, 4}
-};
-const int InsaneRebel1::kWalkerDamageRoute2Count = ARRAYSIZE(kWalkerDamageRoute2);
-
-// updateLevel8WalkerState â Per-frame walker health + attack window + damage logic.
+// updateLevel8WalkerState â Per-frame walker health + attack window logic.
// Called from procPostRendering when _currentLevel == 7.
void InsaneRebel1::updateLevel8WalkerState() {
// Walker health computation â RunLevel8Flow (0x18634-0x18655)
if (_walkerHealth >= 11) {
_walkerHealth = (int16)(100 - (_killCount + (_killCount >> 2)));
- } else if (_walkerHealth > 0 && (_frameCounter & 3) == 0) {
+ } else if (_walkerHealth > 0 && (_gameCounter & 3) == 0) {
_walkerHealth--;
}
@@ -1366,40 +1367,10 @@ void InsaneRebel1::updateLevel8WalkerState() {
return;
}
- // Per-route damage check â FUN_12fe1/FUN_130c9/FUN_13195
+ // FUN_12fe1/FUN_130c9/FUN_13195 walker collision damage is applied from
+ // updateAsteroidPhysics(), where the 0x0B damage flags are consumed.
int route = CLIP(_levelRouteIndex, 0, 2);
- const WalkerDamageFrame *table;
- int tableCount;
- switch (route) {
- case 0: table = kWalkerDamageRoute0; tableCount = kWalkerDamageRoute0Count; break;
- case 1: table = kWalkerDamageRoute1; tableCount = kWalkerDamageRoute1Count; break;
- default: table = kWalkerDamageRoute2; tableCount = kWalkerDamageRoute2Count; break;
- }
-
- uint16 fc = (uint16)_frameCounter;
- for (int i = 0; i < tableCount; i++) {
- if (fc != (uint16)table[i].frame)
- continue;
-
- int16 dx = _shipPosX - kRA1CenterX; // distance from center
- int16 dy = _shipPosY - kRA1CenterY;
- if (dx < 0) dx = -dx;
- if (dy < 0) dy = -dy;
-
- bool hit = false;
- switch (table[i].type) {
- case 0: hit = (dx < 0x29 && dy < 0x20); break; // proximity(41,32)
- case 1: hit = (dy < 0x0F); break; // offsetY(15)
- case 2: hit = (dx < 0x18); break; // offsetX(24)
- case 3: hit = (dx < 0x28 && dy < 0x1F); break; // proximity(40,31)
- case 4: hit = (dy < 0x1F); break; // offsetY(31)
- default: break;
- }
-
- if (hit)
- _damageFlags |= 0x20;
- break;
- }
+ uint16 fc = (uint16)_gameCounter;
// Attack window logic â RunLevel8Flow (0x18778-0x18B4A)
const int16 *windows[3] = {
@@ -1440,12 +1411,12 @@ void InsaneRebel1::updateLevel8WalkerState() {
if (inDirectionalPhase) {
// Player can choose direction during last 50 frames
- if (_playerFired && _avgInputX == 0) {
+ if (_playerFired && _inputAxisDeltaX == 0) {
_walkerBranchChoice = (_shipPosX < 0xA0) ? 1 : 2;
}
} else {
// Torpedo sound every 8 frames during targeting phase
- if ((_frameCounter & 7) == 0)
+ if ((_gameCounter & 7) == 0)
playSfx(kSfxLockOn, 127, 0);
}
}
@@ -1471,9 +1442,13 @@ void InsaneRebel1::updateLevel8WalkerState() {
newRoute = 2;
}
- if (newRoute != 0) {
+ if (newRoute != 0 && newRoute != route) {
_pendingRouteIndex = newRoute;
- _vm->_smushVideoShouldFinish = true;
+ _pendingRouteCutoverFrame = _gameCounter + 7;
+ _pendingRouteStartFrame = _pendingRouteCutoverFrame;
+ debug(1, "RA1 L8 branch: route=%d -> %d at frame=%u shipX=%d resumeTimelineFrame=%d cutoverFrame=%d",
+ route, newRoute, (unsigned)_gameCounter, _shipPosX,
+ (int)_pendingRouteStartFrame, (int)_pendingRouteCutoverFrame);
}
_walkerBranchChoice = 0;
break;
@@ -1482,28 +1457,27 @@ void InsaneRebel1::updateLevel8WalkerState() {
// renderLevel8Overlay â Walker-specific UI from RunLevel8Flow (0x18660-0x18A7E).
// Draws walker health %, attack timer, directional arrows, and target reticle.
-// Original draws via DrawStringEx/FormatAndDrawText in screen-space (within viewport).
-// We draw into 384x242 buffer, so add viewport offset to convert screenâbuffer coords.
-void InsaneRebel1::renderLevel8Overlay(byte *dst, int pitch, int width, int height) {
+// Original RunLevel8Flow projects these fixed cockpit-panel points and then uses
+// 1/4 parallax compensation. We draw into the 384x242 SMUSH buffer, so add the
+// viewport offset to convert the original screen-space result back to buffer
+// coordinates before the final RA1 crop.
+void InsaneRebel1::renderLevel8Overlay(byte *dst, int pitch, int width, int height,
+ int viewportX, int viewportY) {
if (_currentLevel != 7)
return;
- // Viewport offset: screen-space â buffer-space (same approach as renderHUD)
- int viewX = _player ? ra1Player()->_ra1ViewportOffsetX : _perspectiveX;
- int viewY = _player ? ra1Player()->_ra1ViewportOffsetY : _perspectiveY;
-
- // Walker health display â "<<WALKER %d%%" at projected (0x61, 0x8D)
- // Blinks when health < 16: only drawn when (frameCounter & 2) != 0
- if (_walkerHealth > 0 && (_walkerHealth >= 16 || (_frameCounter & 2) != 0)) {
+ // Walker health display â "<<WALKER %d%%" at projected cockpit panel point (0x61, 0x8D).
+ // Blinks when health < 16: only drawn when (GAME counter & 2) != 0.
+ if (_walkerHealth > 0 && (_walkerHealth >= 16 || (_gameCounter & 2) != 0)) {
int16 projX = 0x61, projY = 0x8D;
projectGameplayPoint(projX, projY);
- // Apply 1/4 parallax compensation (original: 0x61 - (projX - 0x61) >> 2)
projX = (int16)(0x61 - ((projX - 0x61) >> 2));
projY = (int16)(0x8D - ((projY - 0x8D) >> 2));
char walkerStr[24];
- Common::sprintf_s(walkerStr, "<<WALKER %d%%%%", (int)_walkerHealth);
- drawFontBankString(dst, pitch, width, height, viewX + projX, viewY + projY, walkerStr);
+ Common::sprintf_s(walkerStr, "<<WALKER %d%%", (int)_walkerHealth);
+ drawFontBankString(dst, pitch, width, height,
+ viewportX + projX, viewportY + projY, walkerStr);
}
// Attack window overlay (timer + arrows/reticle)
@@ -1513,7 +1487,7 @@ void InsaneRebel1::renderLevel8Overlay(byte *dst, int pitch, int width, int heig
&kWalkerAttackWindow2[route],
&kWalkerAttackWindow3[route]
};
- int16 frameNum = (int16)(uint16)_frameCounter;
+ int16 frameNum = (int16)(uint16)_gameCounter;
bool inWindow = false;
bool inDirectionalPhase = false;
@@ -1531,7 +1505,7 @@ void InsaneRebel1::renderLevel8Overlay(byte *dst, int pitch, int width, int heig
if (!inWindow || _walkerBranchChoice != 0)
return;
- // Timer countdown â "<<TIME %d" at projected (0x62, 0x9C)
+ // Timer countdown â "<<TIME %d" at projected cockpit panel point (0x62, 0x9C).
{
int16 projX = 0x62, projY = 0x9C;
projectGameplayPoint(projX, projY);
@@ -1540,7 +1514,8 @@ void InsaneRebel1::renderLevel8Overlay(byte *dst, int pitch, int width, int heig
char timerStr[16];
Common::sprintf_s(timerStr, "<<TIME %d", (int)_walkerTimer);
- drawFontBankString(dst, pitch, width, height, viewX + projX, viewY + projY, timerStr);
+ drawFontBankString(dst, pitch, width, height,
+ viewportX + projX, viewportY + projY, timerStr);
}
if (inDirectionalPhase) {
@@ -1553,21 +1528,21 @@ void InsaneRebel1::renderLevel8Overlay(byte *dst, int pitch, int width, int heig
if (_shipPosX < 0xA0) {
// Left arrow "<<v"
drawFontBankString(dst, pitch, width, height,
- viewX + 0xA6 + px, viewY + 0x92 + py, "<<v");
+ viewportX + 0xA6 + px, viewportY + 0x92 + py, "<<v");
} else {
// Right arrow "<<u"
drawFontBankString(dst, pitch, width, height,
- viewX + 0xA8 - px, viewY + 0x93 - py, "<<u");
+ viewportX + 0xA8 - px, viewportY + 0x93 - py, "<<u");
}
} else {
// Target reticle â "<<w" at projected (0xA9, 0x9A), blinks on (frame & 4)
- if ((_frameCounter & 4) == 0) {
+ if ((_gameCounter & 4) == 0) {
int16 projX = 0, projY = 0;
projectGameplayPoint(projX, projY);
int16 drawX = (int16)(0xA9 - (projX >> 2));
int16 drawY = (int16)(0x9A - (projY >> 2));
drawFontBankString(dst, pitch, width, height,
- viewX + drawX, viewY + drawY, "<<w");
+ viewportX + drawX, viewportY + drawY, "<<w");
}
}
}
diff --git a/engines/scumm/insane/rebel1/runlevels.cpp b/engines/scumm/insane/rebel1/runlevels.cpp
index 68d9c2f404a..7486d22755d 100644
--- a/engines/scumm/insane/rebel1/runlevels.cpp
+++ b/engines/scumm/insane/rebel1/runlevels.cpp
@@ -143,6 +143,71 @@ int32 findAnimFrameChunkOffset(ScummEngine_v7 *vm, const char *filename, int32 t
return result;
}
+int32 findAnimFrameChunkOffsetByGameCounter(ScummEngine_v7 *vm, const char *filename, int32 targetCounter, int32 &localFrame) {
+ localFrame = 0;
+ if (targetCounter <= 0)
+ return 0;
+
+ ScummFile *file = vm->instantiateScummFile();
+ if (!vm->openFile(*file, Common::Path(filename))) {
+ delete file;
+ return -1;
+ }
+
+ int32 result = -1;
+ if (file->size() >= 8) {
+ file->readUint32BE();
+ file->readUint32BE();
+
+ int32 frameIndex = 0;
+ while (file->pos() + 8 <= file->size()) {
+ const int32 chunkOffset = (int32)file->pos();
+ const uint32 chunkTag = file->readUint32BE();
+ const int32 chunkSize = (int32)file->readUint32BE();
+ const int32 chunkDataOffset = (int32)file->pos();
+ const int32 nextChunkOffset = chunkOffset + 8 + chunkSize + ((chunkSize & 1) ? 1 : 0);
+ if (nextChunkOffset < chunkOffset || nextChunkOffset > file->size())
+ break;
+
+ if (chunkTag == MKTAG('F', 'R', 'M', 'E')) {
+ const int32 frameEnd = chunkDataOffset + chunkSize;
+ while (file->pos() + 8 <= frameEnd) {
+ const int32 subChunkOffset = (int32)file->pos();
+ const uint32 subChunkTag = file->readUint32BE();
+ const int32 subChunkSize = (int32)file->readUint32BE();
+ const int32 subChunkDataOffset = (int32)file->pos();
+ const int32 nextSubChunkOffset = subChunkDataOffset + subChunkSize + ((subChunkSize & 1) ? 1 : 0);
+ if (nextSubChunkOffset < subChunkOffset || nextSubChunkOffset > frameEnd)
+ break;
+
+ if (subChunkTag == MKTAG('G', 'A', 'M', 'E') && subChunkSize >= 8) {
+ const uint32 opcode = file->readUint32BE();
+ const int32 counter = (int32)file->readUint32BE();
+ if (opcode == 0x0B && counter >= targetCounter) {
+ localFrame = frameIndex;
+ result = chunkOffset;
+ break;
+ }
+ }
+
+ file->seek(nextSubChunkOffset, SEEK_SET);
+ }
+
+ if (result >= 0)
+ break;
+
+ frameIndex++;
+ }
+
+ file->seek(nextChunkOffset, SEEK_SET);
+ }
+ }
+
+ file->close();
+ delete file;
+ return result;
+}
+
// ---------------------------------------------------------------------------
// Game flow (matching original at 0x15597)
// ---------------------------------------------------------------------------
@@ -891,6 +956,7 @@ bool InsaneRebel1::runLevel8() {
memset(_inputHistoryY, 0, sizeof(_inputHistoryY));
memset(_viewHistoryX, 0, sizeof(_viewHistoryX));
memset(_viewHistoryY, 0, sizeof(_viewHistoryY));
+ _inputAxisDeltaX = 0;
_avgInputX = 0;
_avgInputY = 0;
@@ -900,24 +966,37 @@ bool InsaneRebel1::runLevel8() {
_walkerBranchChoice = 0;
int route = 0;
+ int32 routeStartFrame = 0;
while (!_vm->shouldQuit()) {
_levelRouteIndex = route;
_pendingRouteIndex = -1;
- playInteractiveVideo(kLevel8Routes[route]);
+ _pendingRouteStartFrame = routeStartFrame;
+ _pendingRouteCutoverFrame = -1;
+ playInteractiveVideo(kLevel8Routes[route], routeStartFrame);
if (_vm->shouldQuit())
return false;
if (_health < 0)
break;
+ if (_walkerHealth <= 0)
+ break;
+
if (_pendingRouteIndex < 0 || _pendingRouteIndex == route)
break;
+ // RunLevel8Flow uses PlayAnmFile(..., g_frameCounter, 1, -1)
+ // when it branches from one walker route to another. That wrapper
+ // keeps the current ANM alive for seven more gameplay frames, then
+ // opens the destination route while preserving the active state.
+ routeStartFrame = _pendingRouteStartFrame;
route = _pendingRouteIndex;
}
_levelRouteIndex = -1;
_pendingRouteIndex = -1;
+ _pendingRouteStartFrame = 0;
+ _pendingRouteCutoverFrame = -1;
if (_health >= 0) {
playCinematic("LVL8/L8END.ANM");
@@ -1747,7 +1826,6 @@ void InsaneRebel1::playInteractiveVideo(const char *filename, int32 startFrame)
if (!resumingRoute)
clearBit(0);
_interactiveVideoActive = true;
- _levelRouteChoice = 0;
if (!resumingRoute) {
_onFootInitialized = false; // Reset so each segment triggers counter==0 init
resetFrameObjectState();
@@ -1772,6 +1850,23 @@ void InsaneRebel1::playInteractiveVideo(const char *filename, int32 startFrame)
debug(1, "RA1 L7 resume: route=%d timelineFrame=%d -> localFrame=%d offset=0x%x",
_levelRouteIndex, (int)startFrame, (int)videoStartFrame, (unsigned)videoOffset);
}
+ } else if (_currentLevel == 7 && resumingRoute) {
+ videoOffset = findAnimFrameChunkOffsetByGameCounter(_vm, filename, startFrame, videoStartFrame);
+ if (videoOffset < 0) {
+ debug(1, "RA1 L8 resume: route=%d timelineFrame=%d GAME counter lookup failed",
+ _levelRouteIndex, (int)startFrame);
+ videoStartFrame = startFrame;
+ videoOffset = findAnimFrameChunkOffset(_vm, filename, videoStartFrame);
+ }
+ if (videoOffset < 0) {
+ debug(1, "RA1 L8 resume: route=%d timelineFrame=%d localFrame=%d offset lookup failed",
+ _levelRouteIndex, (int)startFrame, (int)videoStartFrame);
+ videoStartFrame = 0;
+ videoOffset = 0;
+ } else {
+ debug(1, "RA1 L8 resume: route=%d timelineFrame=%d -> localFrame=%d offset=0x%x",
+ _levelRouteIndex, (int)startFrame, (int)videoStartFrame, (unsigned)videoOffset);
+ }
}
// Center mouse, hide system cursor (we draw our own), lock mouse to window
diff --git a/engines/scumm/smush/rebel/smush_player_ra1.cpp b/engines/scumm/smush/rebel/smush_player_ra1.cpp
index 3a2df42c5e4..451b3a0506b 100644
--- a/engines/scumm/smush/rebel/smush_player_ra1.cpp
+++ b/engines/scumm/smush/rebel/smush_player_ra1.cpp
@@ -37,6 +37,14 @@
namespace Scumm {
+enum {
+ kRA1PresentationBorder = 4,
+ kRA1PresentationScreenWidth = 320,
+ kRA1PresentationScreenHeight = 200,
+ kRA1PresentationWidth = kRA1PresentationScreenWidth - kRA1PresentationBorder * 2,
+ kRA1PresentationHeight = kRA1PresentationScreenHeight - kRA1PresentationBorder * 2
+};
+
static void ra1ApplyCenteredFetchPlacement(InsaneRebel1 *rebel1, int width, int height, int &left, int &top) {
int16 centerX = (int16)(left + (width >> 1));
int16 centerY = (int16)(top + (height >> 1));
@@ -66,6 +74,8 @@ void SmushPlayerRebel1::initGamePlayerFields() {
_ra1CleanFrame = nullptr;
_ra1CleanFrameSize = 0;
_ra1HasCleanFrame = false;
+ _ra1PresentationBuffer = nullptr;
+ _ra1PresentationBufferSize = 0;
_ra1ObjOverlayData = nullptr;
_ra1ObjOverlayDataSize = 0;
_ra1ObjOverlayCodec = 0;
@@ -85,6 +95,9 @@ void SmushPlayerRebel1::destroyGamePlayerFields() {
free(_ra1CleanFrame);
_ra1CleanFrame = nullptr;
_ra1CleanFrameSize = 0;
+ free(_ra1PresentationBuffer);
+ _ra1PresentationBuffer = nullptr;
+ _ra1PresentationBufferSize = 0;
}
void SmushPlayerRebel1::resetGameVideoState() {
@@ -120,6 +133,10 @@ void SmushPlayerRebel1::releaseGameVideoState() {
_ra1CleanFrame = nullptr;
_ra1CleanFrameSize = 0;
_ra1HasCleanFrame = false;
+
+ free(_ra1PresentationBuffer);
+ _ra1PresentationBuffer = nullptr;
+ _ra1PresentationBufferSize = 0;
}
bool SmushPlayerRebel1::handleGameFetch(int32 subSize, Common::SeekableReadStream &b) {
@@ -295,30 +312,51 @@ void smushDecodeRA1Transparent(byte *dst, const byte *src, int left, int top, in
} while (--height);
}
/**
- * RA1 codec 21: Skip/copy line codec (FUN_10D41).
+ * RA1 codec 21: Skip/copy line codec (FUN_10D41). Clip copy runs without
+ * changing source X; stored cockpit patches can legitimately start offscreen.
*/
-void smushDecodeRA1SkipCopy(byte *dst, const byte *src, int left, int top, int width, int height, int pitch) {
- dst += top * pitch + left;
+void smushDecodeRA1SkipCopy(byte *dst, const byte *src, int left, int top, int width, int height,
+ int pitch, int bufWidth, int bufHeight) {
for (int row = 0; row < height; row++) {
- uint16 lineSize = READ_LE_UINT16(src);
+ const uint16 lineSize = READ_LE_UINT16(src);
const byte *lineData = src + 2;
const byte *lineEnd = lineData + lineSize;
- byte *dstRow = dst;
- int remaining = width;
- while (remaining > 0 && lineData < lineEnd) {
- if (lineData + 2 > lineEnd) break;
- uint16 skip = READ_LE_UINT16(lineData); lineData += 2;
- dstRow += skip; remaining -= skip;
- if (remaining <= 0) break;
- if (lineData + 2 > lineEnd) break;
- uint16 copyLen = READ_LE_UINT16(lineData) + 1; lineData += 2;
- int toCopy = MIN<int>(copyLen, remaining);
- if (lineData + toCopy > lineEnd) toCopy = (int)(lineEnd - lineData);
- if (toCopy > 0) { memcpy(dstRow, lineData, toCopy); lineData += toCopy; dstRow += toCopy; remaining -= toCopy; }
- if (copyLen > toCopy) lineData += (copyLen - toCopy);
+ const int dstY = top + row;
+ int srcX = 0;
+
+ while (srcX < width && lineData < lineEnd) {
+ if (lineData + 2 > lineEnd)
+ break;
+ const uint16 skip = READ_LE_UINT16(lineData);
+ lineData += 2;
+ srcX += skip;
+ if (srcX >= width)
+ break;
+
+ if (lineData + 2 > lineEnd)
+ break;
+ const int copyLen = READ_LE_UINT16(lineData) + 1;
+ lineData += 2;
+
+ const int readableLen = MIN<int>(copyLen, (int)(lineEnd - lineData));
+ const int dstStartX = left + srcX;
+ const int dstEndX = dstStartX + readableLen;
+ if (readableLen > 0 && dstY >= 0 && dstY < bufHeight) {
+ const int clippedStartX = MAX(dstStartX, 0);
+ const int clippedEndX = MIN(dstEndX, bufWidth);
+ if (clippedStartX < clippedEndX) {
+ const int srcSkipX = clippedStartX - dstStartX;
+ memcpy(dst + dstY * pitch + clippedStartX,
+ lineData + srcSkipX, clippedEndX - clippedStartX);
+ }
+ }
+
+ lineData += readableLen;
+ srcX += copyLen;
+ if (readableLen < copyLen)
+ break;
}
src += lineSize + 2;
- dst += pitch;
}
}
@@ -505,12 +543,23 @@ bool SmushPlayerRebel1::handleGameDimensionOverride(int codec, int width, int he
}
bool SmushPlayerRebel1::handleGameAdjustCoords(int codec, int &left, int &top, int &width, int &height, int pitch, int *srcSkipY) {
+ _ra1FrameSourceSkipY = 0;
+
// RA1 additive codec (SKIP_RLE) and scatter (RA1_SCATTER) use absolute
// positions â they must NOT be clipped/adjusted.
if (codec == SMUSH_CODEC_SKIP_RLE || codec == SMUSH_CODEC_RA1_SCATTER)
return false;
+
+ // RA1 codec 21 is source-X sensitive: generic left clipping would reduce
+ // the destination width without skipping the corresponding source columns.
+ // Keep only the global FOBJ offset here and let the codec clip each run.
+ if (codec == SMUSH_CODEC_LINE_UPDATE) {
+ left += _fobjOffsetX;
+ top += _fobjOffsetY;
+ return false;
+ }
+
int sourceSkipY = 0;
- _ra1FrameSourceSkipY = 0;
adjustFrameCoords(left, top, width, height, pitch, &sourceSkipY);
if (codec == SMUSH_CODEC_RLE_ALT) {
_ra1FrameSourceSkipY = sourceSkipY;
@@ -541,7 +590,8 @@ bool SmushPlayerRebel1::handleGameCodecDecode(int codec, const uint8 *src, int l
dataSize, param, parm2, codec);
return true;
case SMUSH_CODEC_LINE_UPDATE:
- smushDecodeRA1SkipCopy(_dst, src, left, top, width, height, pitch);
+ smushDecodeRA1SkipCopy(_dst, src, left, top, width, height, pitch,
+ pitch, (_dst == _specialBuffer) ? _height : _vm->_screenHeight);
return true;
case SMUSH_CODEC_SKIP_RLE: {
const int bufWidth = pitch;
@@ -939,20 +989,46 @@ void SmushPlayerRebel1::handleGameUpdateScreen(const byte *src, int srcPitch, in
if (_dst == nullptr || _width <= 0 || _height <= 0)
return;
+ if (!_insane || !static_cast<InsaneRebel1 *>(_insane)->isInteractiveVideoActive() ||
+ _vm->_screenWidth != kRA1PresentationScreenWidth ||
+ _vm->_screenHeight != kRA1PresentationScreenHeight) {
+ SmushPlayer::handleGameUpdateScreen(src, srcPitch, width, height);
+ return;
+ }
+
int ra1ViewX = _ra1ViewportOffsetX;
int ra1ViewY = _ra1ViewportOffsetY;
- const int srcX = CLIP(_scrollX + ra1ViewX, 0, _width - 1);
- const int srcY = CLIP(_scrollY + ra1ViewY, 0, _height - 1);
+ const int srcX = CLIP(_scrollX + ra1ViewX + kRA1PresentationBorder, 0, _width - 1);
+ const int srcY = CLIP(_scrollY + ra1ViewY + kRA1PresentationBorder, 0, _height - 1);
- int frameWidth = MIN(_width - srcX, _vm->_screenWidth);
- int frameHeight = MIN(_height - srcY, _vm->_screenHeight);
+ int frameWidth = MIN<int>(_width - srcX, kRA1PresentationWidth);
+ int frameHeight = MIN<int>(_height - srcY, kRA1PresentationHeight);
if (frameWidth <= 0 || frameHeight <= 0)
return;
+ const int presentationSize = kRA1PresentationScreenWidth * kRA1PresentationScreenHeight;
+ if (_ra1PresentationBuffer == nullptr || _ra1PresentationBufferSize < presentationSize) {
+ byte *newPresentationBuffer = (byte *)realloc(_ra1PresentationBuffer, presentationSize);
+ if (newPresentationBuffer == nullptr)
+ return;
+ _ra1PresentationBuffer = newPresentationBuffer;
+ _ra1PresentationBufferSize = presentationSize;
+ }
+ memset(_ra1PresentationBuffer, 0, presentationSize);
+
+ // ResetPlaybackViewport() (0x20A53) initializes the interactive draw window
+ // to (4,4,312,192), leaving a black presentation frame around cockpit scenes.
const byte *dst = _dst + srcY * _width + srcX;
+ byte *presentationDst = _ra1PresentationBuffer +
+ kRA1PresentationBorder * kRA1PresentationScreenWidth + kRA1PresentationBorder;
+ for (int y = 0; y < frameHeight; y++) {
+ memcpy(presentationDst + y * kRA1PresentationScreenWidth,
+ dst + y * _width, frameWidth);
+ }
- SmushPlayer::handleGameUpdateScreen(dst, _width, frameWidth, frameHeight);
+ SmushPlayer::handleGameUpdateScreen(_ra1PresentationBuffer,
+ kRA1PresentationScreenWidth, kRA1PresentationScreenWidth, kRA1PresentationScreenHeight);
}
SmushFont *SmushPlayerRebel1::ra1GetFont(int font) {
diff --git a/engines/scumm/smush/rebel/smush_player_ra1.h b/engines/scumm/smush/rebel/smush_player_ra1.h
index e53fbea39a1..3cd067fc794 100644
--- a/engines/scumm/smush/rebel/smush_player_ra1.h
+++ b/engines/scumm/smush/rebel/smush_player_ra1.h
@@ -68,6 +68,10 @@ private:
int32 _ra1CleanFrameSize;
bool _ra1HasCleanFrame;
+ // RA1 interactive movies present a 312x192 viewport inside a black 320x200 frame.
+ byte *_ra1PresentationBuffer;
+ int32 _ra1PresentationBufferSize;
+
// RA1 OBJ overlay FOBJ â cockpit drawn once frame 0, re-rendered every frame
byte *_ra1ObjOverlayData;
int32 _ra1ObjOverlayDataSize;
Commit: e11c594c4f08c09d9c2ed37d26156d0bd6c5eb1d
https://github.com/scummvm/scummvm/commit/e11c594c4f08c09d9c2ed37d26156d0bd6c5eb1d
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:10:00+02:00
Commit Message:
SCUMM: RA1: Fix invisible cockpit in level 8 sections
Changed paths:
engines/scumm/insane/rebel1/iact.cpp
engines/scumm/insane/rebel1/render.cpp
engines/scumm/smush/smush_player.cpp
engines/scumm/smush/smush_player.h
diff --git a/engines/scumm/insane/rebel1/iact.cpp b/engines/scumm/insane/rebel1/iact.cpp
index 6d8233ab8ea..3a8f20ab72a 100644
--- a/engines/scumm/insane/rebel1/iact.cpp
+++ b/engines/scumm/insane/rebel1/iact.cpp
@@ -500,6 +500,8 @@ void InsaneRebel1::checkDynamicLevelBranch() {
if (!_vm->_smushVideoShouldFinish &&
_pendingRouteCutoverFrame >= 0 &&
routeFrame >= (uint32)_pendingRouteCutoverFrame) {
+ if (_player)
+ _player->setPreserveGameVideoStateOnRelease(true);
_vm->_smushVideoShouldFinish = true;
debug(1, "RA1 L%d cutover: route=%d -> %d at frame=%u (resumeTimelineFrame=%d)",
_currentLevel + 1, _levelRouteIndex, _pendingRouteIndex,
diff --git a/engines/scumm/insane/rebel1/render.cpp b/engines/scumm/insane/rebel1/render.cpp
index 183ec966e64..f2597c684de 100644
--- a/engines/scumm/insane/rebel1/render.cpp
+++ b/engines/scumm/insane/rebel1/render.cpp
@@ -1519,20 +1519,22 @@ void InsaneRebel1::renderLevel8Overlay(byte *dst, int pitch, int width, int heig
}
if (inDirectionalPhase) {
- // Directional arrows â projected from (0,0) with parallax compensation
+ // Directional arrows â DrawStringEx(..., flags=0x81) at:
+ // left: (0xA6 - projX / 4, 0x92 - projY / 4)
+ // right: (0xA8 - projX / 4, 0x93 - projY / 4)
int16 projX = 0, projY = 0;
projectGameplayPoint(projX, projY);
- int16 px = (int16)(-(projX >> 2));
- int16 py = (int16)(-(projY >> 2));
+ const int16 parallaxX = (int16)(projX >> 2);
+ const int16 parallaxY = (int16)(projY >> 2);
if (_shipPosX < 0xA0) {
// Left arrow "<<v"
drawFontBankString(dst, pitch, width, height,
- viewportX + 0xA6 + px, viewportY + 0x92 + py, "<<v");
+ viewportX + 0xA6 - parallaxX, viewportY + 0x92 - parallaxY, "<<v");
} else {
// Right arrow "<<u"
drawFontBankString(dst, pitch, width, height,
- viewportX + 0xA8 - px, viewportY + 0x93 - py, "<<u");
+ viewportX + 0xA8 - parallaxX, viewportY + 0x93 - parallaxY, "<<u");
}
} else {
// Target reticle â "<<w" at projected (0xA9, 0x9A), blinks on (frame & 4)
diff --git a/engines/scumm/smush/smush_player.cpp b/engines/scumm/smush/smush_player.cpp
index 0da86094c01..795a90be08f 100644
--- a/engines/scumm/smush/smush_player.cpp
+++ b/engines/scumm/smush/smush_player.cpp
@@ -270,6 +270,7 @@ SmushPlayer::SmushPlayer(ScummEngine_v7 *scumm, IMuseDigital *imuseDigital, Insa
_fastForwardFromFrame = 0;
_fastForwardToFrame = 0;
_preserveVideoStateOnNextPlay = false;
+ _preserveGameVideoStateOnRelease = false;
_IACTpos = 0;
_speed = -1;
@@ -391,7 +392,10 @@ void SmushPlayer::release() {
free(_specialBuffer);
_specialBuffer = nullptr;
- releaseGameVideoState();
+ const bool preserveGameVideoState = _preserveGameVideoStateOnRelease;
+ _preserveGameVideoStateOnRelease = false;
+ if (!preserveGameVideoState)
+ releaseGameVideoState();
if (!shouldPreserveFrameBuffer()) {
free(_frameBuffer);
_frameBuffer = nullptr;
diff --git a/engines/scumm/smush/smush_player.h b/engines/scumm/smush/smush_player.h
index 0ec4bd9311e..d0bdd39ff8b 100644
--- a/engines/scumm/smush/smush_player.h
+++ b/engines/scumm/smush/smush_player.h
@@ -202,6 +202,7 @@ private:
uint32 _fastForwardFromFrame;
uint32 _fastForwardToFrame;
bool _preserveVideoStateOnNextPlay;
+ bool _preserveGameVideoStateOnRelease;
Audio::SoundHandle *_IACTchannel;
Audio::QueuingAudioStream *_IACTstream;
@@ -265,6 +266,7 @@ public:
void ensureMultiFont();
bool isFastForwardingCurrentFrame() const;
void setPreserveVideoStateOnNextPlay(bool preserve) { _preserveVideoStateOnNextPlay = preserve; }
+ void setPreserveGameVideoStateOnRelease(bool preserve) { _preserveGameVideoStateOnRelease = preserve; }
void setFastForwardFromFrame(uint32 frame) { _fastForwardFromFrame = frame; }
void setFastForwardToFrame(uint32 frame) { _fastForwardToFrame = frame; }
Commit: 6e7020314e6f1c1caba25813d9c16d3776116d67
https://github.com/scummvm/scummvm/commit/6e7020314e6f1c1caba25813d9c16d3776116d67
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:10:00+02:00
Commit Message:
SCUMM: RA1: Trigger damage in level 8
Changed paths:
engines/scumm/insane/rebel1/iact.cpp
engines/scumm/insane/rebel1/rebel.cpp
engines/scumm/insane/rebel1/rebel.h
engines/scumm/insane/rebel1/render.cpp
diff --git a/engines/scumm/insane/rebel1/iact.cpp b/engines/scumm/insane/rebel1/iact.cpp
index 3a8f20ab72a..8147bed73d6 100644
--- a/engines/scumm/insane/rebel1/iact.cpp
+++ b/engines/scumm/insane/rebel1/iact.cpp
@@ -315,52 +315,67 @@ inline bool hasLevel6PerspectiveHazard(uint16 frame, int16 perspectiveX, int16 p
}
}
-inline bool hasLevel8PerspectiveHazardRoute0(uint16 frame, int16 perspectiveX, int16 perspectiveY) {
+// Named translations of the original Level 8 route collision helpers
+// FUN_12FE1/FUN_130C9/FUN_13195. The names are new to the port.
+inline bool hasLevel8WalkerHazardRoute0(uint16 frame, int16 viewX, int16 viewY) {
switch (frame) {
case 0x00CD:
- return perspectiveX < 0x29;
case 0x00EF:
- return perspectiveY < 0x0F;
+ return viewX <= 0x28;
case 0x0294:
+ return viewY >= 0x0F;
+ case 0x03A2:
+ return viewX >= 0x18;
case 0x04BE:
case 0x076C:
- return perspectiveX < 0x29 && perspectiveY < 0x20;
- case 0x03A2:
- return perspectiveX < 0x18;
+ return viewX <= 0x28 && viewY <= 0x1F;
case 0x05C9:
case 0x085A:
case 0x096F:
- return perspectiveY < 0x20;
+ return viewY <= 0x1F;
default:
return false;
}
}
-inline bool hasLevel8PerspectiveHazardRoute1(uint16 frame, int16 perspectiveX, int16 perspectiveY) {
+inline bool hasLevel8WalkerHazardRoute1(uint16 frame, int16 viewX, int16 viewY) {
switch (frame) {
case 0x0189:
- return perspectiveY < 0x0F;
+ return viewY >= 0x0F;
case 0x0297:
- return perspectiveX < 0x18;
+ return viewX >= 0x18;
case 0x03B3:
case 0x0661:
- return perspectiveX < 0x29 && perspectiveY < 0x20;
+ return viewX <= 0x28 && viewY <= 0x1F;
case 0x04BE:
case 0x074F:
case 0x0864:
- return perspectiveY < 0x20;
+ return viewY <= 0x1F;
default:
return false;
}
}
-inline bool hasLevel8PerspectiveHazardRoute2(uint16 frame, int16 perspectiveX, int16 perspectiveY) {
+inline bool hasLevel8WalkerHazardRoute2(uint16 frame, int16 viewX, int16 viewY) {
switch (frame) {
case 0x00BB:
- return perspectiveX < 0x29 && perspectiveY < 0x20;
+ return viewX <= 0x28 && viewY <= 0x1F;
case 0x01A9:
case 0x02BE:
- return perspectiveY < 0x20;
+ return viewY <= 0x1F;
+ default:
+ return false;
+ }
+}
+
+inline bool hasLevel8WalkerPlayerHit(int route, uint16 frame, int16 viewX, int16 viewY) {
+ switch (CLIP<int>(route, 0, 2)) {
+ case 0:
+ return hasLevel8WalkerHazardRoute0(frame, viewX, viewY);
+ case 1:
+ return hasLevel8WalkerHazardRoute1(frame, viewX, viewY);
+ case 2:
+ return hasLevel8WalkerHazardRoute2(frame, viewX, viewY);
default:
return false;
}
@@ -1142,14 +1157,47 @@ void InsaneRebel1::updateTurretPhysics() {
_damageFlags = 0;
}
-// updateAsteroidPhysics â FUN_1CDA7 (0x1CDA7). Opcode 0x0B handler.
+// New port helpers for the palette-flash block that is inline in FUN_1CDA7.
+void InsaneRebel1::restoreScreenFlashPalette() {
+ if (!_screenFlashBasePaletteValid)
+ return;
+
+ if (_player)
+ _player->setPalette(_screenFlashBasePalette);
+ _screenFlashBasePaletteValid = false;
+}
+
+void InsaneRebel1::updateScreenFlashPalette() {
+ if (!_player) {
+ _screenFlashBasePaletteValid = false;
+ return;
+ }
+
+ if (_screenFlash <= 0) {
+ restoreScreenFlashPalette();
+ return;
+ }
+
+ if (!_screenFlashBasePaletteValid) {
+ memcpy(_screenFlashBasePalette, _player->getVideoPalette(), sizeof(_screenFlashBasePalette));
+ _screenFlashBasePaletteValid = true;
+ }
+
+ byte flashPalette[0x300];
+ const int blend = CLIP<int>(8 - _screenFlash, 0, 8);
+ for (int i = 0; i < 0x300; i++)
+ flashPalette[i] = (byte)(0xFF - (((0xFF - _screenFlashBasePalette[i]) * blend) >> 3));
+ _player->setPalette(flashPalette);
+}
+
+// updateGameOp0BPhysics â FUN_1CDA7 (0x1CDA7). GAME opcode 0x0B handler.
// Uses 10-frame input history averaging instead of accumulators.
// Ship position = averaged input + center offset.
// Viewport = second history buffer for smooth camera scrolling.
-void InsaneRebel1::updateAsteroidPhysics() {
+void InsaneRebel1::updateGameOp0BPhysics() {
// Control feel tweak: original uses full 10-sample average in FUN_1CDA7.
// We keep the same pipeline but average over fewer samples for responsiveness.
- const int kAsteroidSmoothWindow = 2;
+ const int kGameOp0BSmoothWindow = 2;
// RA1 FUN_1B297-style per-frame latches for 0x0B sections:
// 0x5D latch 0xFFFF -> bit 0x40 (scripted obstacle/contact)
@@ -1168,22 +1216,12 @@ void InsaneRebel1::updateAsteroidPhysics() {
if (_currentLevel == 5 && hasLevel6PerspectiveHazard((uint16)_frameCounter, _perspectiveX, _perspectiveY))
_damageFlags |= 0x20;
+ bool level8WalkerPlayerHit = false;
if (_currentLevel == 7) {
const uint16 walkerFrame = (uint16)_gameCounter;
- bool walkerHazard = false;
- switch (CLIP<int>(_levelRouteIndex, 0, 2)) {
- case 0:
- walkerHazard = hasLevel8PerspectiveHazardRoute0(walkerFrame, _perspectiveX, _perspectiveY);
- break;
- case 1:
- walkerHazard = hasLevel8PerspectiveHazardRoute1(walkerFrame, _perspectiveX, _perspectiveY);
- break;
- case 2:
- walkerHazard = hasLevel8PerspectiveHazardRoute2(walkerFrame, _perspectiveX, _perspectiveY);
- break;
- }
-
- if (walkerHazard)
+ level8WalkerPlayerHit = hasLevel8WalkerPlayerHit(_levelRouteIndex, walkerFrame,
+ _perspectiveX, _perspectiveY);
+ if (level8WalkerPlayerHit)
_damageFlags |= 0x20;
}
@@ -1193,15 +1231,18 @@ void InsaneRebel1::updateAsteroidPhysics() {
}
// Damage application (FUN_1CDA7 lines 20-41)
+ // Original 0x0B mapping: 0x80 -> +0x0F, 0x40 -> +0x11, 0x20 -> +0x13.
// No cooldown â all three damage types can stack each frame
if (_damageFlags != 0 && _health >= 0 && _deathTimer < 1) {
+ const int16 oldHealth = _health;
+ const byte appliedDamageFlags = _damageFlags;
_screenFlash = 5;
if (_damageFlags & 0x80)
- _health -= _tuning.shot;
- if (_damageFlags & 0x40)
_health -= _tuning.miss;
- if (_damageFlags & 0x20)
+ if (_damageFlags & 0x40)
_health -= _tuning.wham;
+ if (_damageFlags & 0x20)
+ _health -= _tuning.shot;
if (_health < 0) {
_deathTimer = 15; // 0x0F â shorter than Level 1's 30
if (_damageFlags & 0x80)
@@ -1209,7 +1250,14 @@ void InsaneRebel1::updateAsteroidPhysics() {
else
_deathCauseIndicator = 1;
}
- _prevDamageFlags = _damageFlags;
+ // FUN_1CDA7 dispatches g_sfxDamageHit, initialized from SYS/BOOM.SAD.
+ playSfx(kSfxBoom, 127, 0);
+ if (level8WalkerPlayerHit) {
+ debug(1, "RA1 L8 player hit by walker: route=%d frame=%u view=(%d,%d) flags=0x%02x health=%d->%d",
+ CLIP<int>(_levelRouteIndex, 0, 2), (unsigned)(uint16)_gameCounter,
+ _perspectiveX, _perspectiveY, appliedDamageFlags, oldHealth, _health);
+ }
+ _prevDamageFlags = appliedDamageFlags;
_damageFlags = 0;
}
@@ -1227,6 +1275,7 @@ void InsaneRebel1::updateAsteroidPhysics() {
_screenFlash--;
_screenShakeEnabled = (_screenFlash > 0);
}
+ updateScreenFlashPalette();
// --- Cursor and perspective smoothing (FUN_1CDA7) ---
// _inputHistory* maps to 0x7580/0x7594, _viewHistory* to 0x75A8/0x75BC.
@@ -1265,8 +1314,11 @@ void InsaneRebel1::updateAsteroidPhysics() {
}
_inputAxisDeltaX = inputX;
- debug("RA1 asteroid input: source=%s axis=(%d,%d) mouse=(%d,%d) actions(L,R,U,D)=(%d,%d,%d,%d) raw=(%d,%d) final=(%d,%d) level=%d opcode=0x%X",
+ debug("RA1 GAME 0x0B input: frame=%d source=%s view=(%d,%d) health=%d prevFlags=0x%02x axis=(%d,%d) mouse=(%d,%d) actions(L,R,U,D)=(%d,%d,%d,%d) raw=(%d,%d) final=(%d,%d) level=%d opcode=0x%X",
+ _gameCounter,
inputSourceName,
+ _perspectiveX, _perspectiveY,
+ _health, _prevDamageFlags,
_joystickAxisX, _joystickAxisY,
_vm->_mouse.x, _vm->_mouse.y,
_vm->getActionState(kScummActionInsaneLeft),
@@ -1285,13 +1337,13 @@ void InsaneRebel1::updateAsteroidPhysics() {
int sumInputX = 0;
int sumInputY = 0;
- for (int i = 0; i < kAsteroidSmoothWindow; i++) {
+ for (int i = 0; i < kGameOp0BSmoothWindow; i++) {
sumInputX += _inputHistoryX[i];
sumInputY += _inputHistoryY[i];
}
- _avgInputX = (int16)(sumInputX / kAsteroidSmoothWindow);
- _avgInputY = (int16)(-sumInputY / kAsteroidSmoothWindow);
+ _avgInputX = (int16)(sumInputX / kGameOp0BSmoothWindow);
+ _avgInputY = (int16)(-sumInputY / kGameOp0BSmoothWindow);
_avgInputX = CLIP<int16>(_avgInputX, -0xA0, 0xA0);
_avgInputY = CLIP<int16>(_avgInputY, -0x46, 0x41);
@@ -1307,13 +1359,13 @@ void InsaneRebel1::updateAsteroidPhysics() {
int sumViewX = 0;
int sumViewY = 0;
- for (int i = 0; i < kAsteroidSmoothWindow; i++) {
+ for (int i = 0; i < kGameOp0BSmoothWindow; i++) {
sumViewX += _viewHistoryX[i];
sumViewY += _viewHistoryY[i];
}
- int16 avgViewX = (int16)(sumViewX / kAsteroidSmoothWindow);
- int16 avgViewY = (int16)(sumViewY / kAsteroidSmoothWindow);
+ int16 avgViewX = (int16)(sumViewX / kGameOp0BSmoothWindow);
+ int16 avgViewY = (int16)(sumViewY / kGameOp0BSmoothWindow);
_perspectiveX = CLIP<int16>((int16)((avgViewX >> 1) + 0x20), 0, 0x40);
_perspectiveY = CLIP<int16>((int16)((avgViewY >> 1) + 0x17), 0, 0x2E);
resetProjectionTable();
@@ -1380,7 +1432,7 @@ void InsaneRebel1::updateAsteroidPhysics() {
checkDynamicLevelBranch();
- debug(7, "RA1 asteroid: pos=(%d,%d) avg=(%d,%d) view=(%d,%d) health=%d flash=%d",
+ debug(7, "RA1 GAME 0x0B: pos=(%d,%d) avg=(%d,%d) view=(%d,%d) health=%d flash=%d",
_shipPosX, _shipPosY, _avgInputX, _avgInputY,
_perspectiveX, _perspectiveY, _health, _screenFlash);
}
@@ -1744,7 +1796,7 @@ void InsaneRebel1::handleGameChunk(int32 subSize, Common::SeekableReadStream &b)
case 0x0B:
_activeGameOpcode = 0x0B;
_frameGameOpcodeMask |= (1u << 0x0B);
- // Asteroid/surface per-frame handler (FUN_1CDA7).
+ // GAME 0x0B per-frame handler (FUN_1CDA7).
// field1 = frame counter, field2 = max frames
_gameCounter = param1;
if (subSize >= 20) {
@@ -1761,9 +1813,9 @@ void InsaneRebel1::handleGameChunk(int32 subSize, Common::SeekableReadStream &b)
}
}
debug(7, "RA1 GAME 0x0B: counter=%d", _gameCounter);
- if (!_asteroidPhysicsUpdatedThisFrame) {
- updateAsteroidPhysics();
- _asteroidPhysicsUpdatedThisFrame = true;
+ if (!_gameOp0BPhysicsUpdatedThisFrame) {
+ updateGameOp0BPhysics();
+ _gameOp0BPhysicsUpdatedThisFrame = true;
syncViewportOffset(true);
}
break;
diff --git a/engines/scumm/insane/rebel1/rebel.cpp b/engines/scumm/insane/rebel1/rebel.cpp
index 9dc75ef8307..9d7dd390fb6 100644
--- a/engines/scumm/insane/rebel1/rebel.cpp
+++ b/engines/scumm/insane/rebel1/rebel.cpp
@@ -206,6 +206,8 @@ void InsaneRebel1::resetGameplayFlagsFromTuning() {
}
InsaneRebel1::InsaneRebel1(ScummEngine_v7 *scumm) : Insane(), _vm(scumm) {
+ Insane::_vm = scumm;
+
_screenWidth = 384;
_screenHeight = 242;
@@ -226,7 +228,8 @@ InsaneRebel1::InsaneRebel1(ScummEngine_v7 *scumm) : Insane(), _vm(scumm) {
_posAccumY = 0;
_driftParam = 0;
- _difficulty = 0; // Easy by default
+ // Original startup initializes the options difficulty/tuning index to 1.
+ _difficulty = 1;
loadTuningForLevel(0);
_perspectiveX = 0;
@@ -268,7 +271,7 @@ InsaneRebel1::InsaneRebel1(ScummEngine_v7 *scumm) : Insane(), _vm(scumm) {
_activeGameOpcode = 0;
_frameGameOpcodeMask = 0;
_frameDispatchFlags = 0;
- _asteroidPhysicsUpdatedThisFrame = false;
+ _gameOp0BPhysicsUpdatedThisFrame = false;
_health = kMaxHealth;
_lives = 3;
@@ -283,6 +286,8 @@ InsaneRebel1::InsaneRebel1(ScummEngine_v7 *scumm) : Insane(), _vm(scumm) {
_screenFlash = 0;
_frameCounter = 0;
_screenShakeEnabled = false;
+ memset(_screenFlashBasePalette, 0, sizeof(_screenFlashBasePalette));
+ _screenFlashBasePaletteValid = false;
_deathCauseIndicator = 0;
_hudRenderFlag = 0;
_hudDirtyFlag = 0;
diff --git a/engines/scumm/insane/rebel1/rebel.h b/engines/scumm/insane/rebel1/rebel.h
index 53e7bb55573..6c8117ee2e3 100644
--- a/engines/scumm/insane/rebel1/rebel.h
+++ b/engines/scumm/insane/rebel1/rebel.h
@@ -270,8 +270,10 @@ private:
};
InputSource _activeInputSource;
- // 0x0B handler physics update (asteroid/surface levels)
- void updateAsteroidPhysics();
+ // GAME opcode 0x0B physics update (scrolling cockpit/surface levels)
+ void updateGameOp0BPhysics();
+ void updateScreenFlashPalette();
+ void restoreScreenFlashPalette();
// 0x19/0x1A on-foot handler (Level 9 Stormtroopers)
void updateOnFootPhysics();
@@ -303,7 +305,7 @@ private:
uint16 _activeGameOpcode;
uint32 _frameGameOpcodeMask;
uint16 _frameDispatchFlags;
- bool _asteroidPhysicsUpdatedThisFrame;
+ bool _gameOp0BPhysicsUpdatedThisFrame;
// Difficulty (0=easy, 1=normal, 2=hard) â matches original DAT_22BC
int _difficulty;
@@ -317,9 +319,9 @@ private:
int16 slide; // +0x09: cross-axis coupling
int16 drift; // +0x0B: drift/turbulence multiplier
int16 snap; // +0x0D: hit radius for shooting targets
- int16 miss; // +0x0F: obstacle collision damage (0x0B bit 0x40)
- int16 wham; // +0x11: light/wall damage
- int16 shot; // +0x13: heavy/projectile damage
+ int16 miss; // +0x0F: first damage value; FUN_1CDA7 uses it for bit 0x80
+ int16 wham; // +0x11: second damage value; FUN_1CDA7 uses it for bit 0x40
+ int16 shot; // +0x13: third damage value; FUN_1CDA7 uses it for bit 0x20
int16 kill; // +0x15: score per target kill
int16 time; // +0x17: survival bonus (added every 32 frames)
int16 levelPts; // +0x19: chapter completion bonus (RunChapterCompleteSummaryScreen)
@@ -345,6 +347,8 @@ private:
int16 _screenFlash; // 0x7736: screen flash timer on hit
uint32 _frameCounter; // 0x7740: global frame counter
bool _screenShakeEnabled; // 0x41AC: when true, SetCameraOffset adds ±2 random jitter
+ byte _screenFlashBasePalette[0x300];
+ bool _screenFlashBasePaletteValid;
byte _deathCauseIndicator; // 0x772E: non-zero = player died; selects death animation variant
byte _hudRenderFlag; // 0x7600: 0xFF when HUD should render (set by combat mode handlers)
byte _hudDirtyFlag; // 0x7601: 0xFF after HUD redraw (set by renderHUD)
diff --git a/engines/scumm/insane/rebel1/render.cpp b/engines/scumm/insane/rebel1/render.cpp
index f2597c684de..9433b49b761 100644
--- a/engines/scumm/insane/rebel1/render.cpp
+++ b/engines/scumm/insane/rebel1/render.cpp
@@ -365,7 +365,7 @@ void renderSpriteWithFlags(byte *dst, int pitch, int width, int height,
void InsaneRebel1::procPreRendering(byte *renderBitmap) {
_frameGameOpcodeMask = 0;
_frameDispatchFlags = 0;
- _asteroidPhysicsUpdatedThisFrame = false;
+ _gameOp0BPhysicsUpdatedThisFrame = false;
if (_interactiveVideoActive && _player) {
const bool usePerspectiveViewport =
@@ -443,7 +443,7 @@ void InsaneRebel1::procPostRendering(byte *renderBitmap, int32 codecparam, int32
_hudRenderFlag = 0xFF;
const bool haveFrameGameOpcodes = (_frameGameOpcodeMask != 0);
- const bool asteroidMode = hasFrameGameOpcode(0x0B) ||
+ const bool gameOp0BMode = hasFrameGameOpcode(0x0B) ||
(!haveFrameGameOpcodes && _activeGameOpcode == 0x0B);
const bool onFootMode = hasFrameGameOpcode(0x19) || hasFrameGameOpcode(0x1A) ||
(!haveFrameGameOpcodes &&
@@ -453,17 +453,17 @@ void InsaneRebel1::procPostRendering(byte *renderBitmap, int32 codecparam, int32
const bool flightMode = hasFrameGameOpcode(0x07) || hasFrameGameOpcode(0x09) ||
(!haveFrameGameOpcodes &&
(_activeGameOpcode == 0x07 || _activeGameOpcode == 0x09));
- if (asteroidMode) {
- // First-person asteroid/surface handler â opcode 0x0B (FUN_1CDA7).
- if (!_asteroidPhysicsUpdatedThisFrame) {
- updateAsteroidPhysics();
- _asteroidPhysicsUpdatedThisFrame = true;
+ if (gameOp0BMode) {
+ // GAME 0x0B scrolling cockpit/surface handler â FUN_1CDA7.
+ if (!_gameOp0BPhysicsUpdatedThisFrame) {
+ updateGameOp0BPhysics();
+ _gameOp0BPhysicsUpdatedThisFrame = true;
syncViewportOffset(true);
}
// DOS 0x0B loops test health after each frontend frame and leave the
// interactive movie as soon as it drops below zero. Mirror that here so
- // asteroid/surface chapters transition to their retry/death clips like
+ // GAME 0x0B chapters transition to their retry/death clips like
// the other gameplay families do.
if (_health < 0) {
_fireCooldown = _playerFired ? 1 : 0;
@@ -517,9 +517,9 @@ void InsaneRebel1::procPostRendering(byte *renderBitmap, int32 codecparam, int32
// before HUD/screen copy so overlays and final crop observe the same camera.
// On-foot mode uses SetCameraOffset(0,0) â no viewport crop.
if (_player) {
- if (onFootMode || (!asteroidMode && !turretMode && !flightMode)) {
+ if (onFootMode || (!gameOp0BMode && !turretMode && !flightMode)) {
syncViewportOffset(false);
- } else if (!asteroidMode) {
+ } else if (!gameOp0BMode) {
syncViewportOffset(true);
}
}
@@ -1367,8 +1367,10 @@ void InsaneRebel1::updateLevel8WalkerState() {
return;
}
- // FUN_12fe1/FUN_130c9/FUN_13195 walker collision damage is applied from
- // updateAsteroidPhysics(), where the 0x0B damage flags are consumed.
+ // FUN_12FE1/FUN_130C9/FUN_13195 test whether the walker shot hits the
+ // player. The port synthesizes that player-damage flag in updateGameOp0BPhysics(),
+ // where the 0x0B damage flags are consumed. This is unrelated to _walkerHealth,
+ // which is the boss health displayed by the Level 8 overlay.
int route = CLIP(_levelRouteIndex, 0, 2);
uint16 fc = (uint16)_gameCounter;
Commit: 47e86512bc8dcba7157bd4749c591fcbec00d686
https://github.com/scummvm/scummvm/commit/47e86512bc8dcba7157bd4749c591fcbec00d686
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:10:01+02:00
Commit Message:
SCUMM: RA1: Improve level 8 comments
Changed paths:
engines/scumm/insane/rebel1/iact.cpp
engines/scumm/insane/rebel1/rebel.h
engines/scumm/insane/rebel1/render.cpp
engines/scumm/insane/rebel1/runlevels.cpp
diff --git a/engines/scumm/insane/rebel1/iact.cpp b/engines/scumm/insane/rebel1/iact.cpp
index 8147bed73d6..88d30ac5298 100644
--- a/engines/scumm/insane/rebel1/iact.cpp
+++ b/engines/scumm/insane/rebel1/iact.cpp
@@ -1221,6 +1221,8 @@ void InsaneRebel1::updateGameOp0BPhysics() {
const uint16 walkerFrame = (uint16)_gameCounter;
level8WalkerPlayerHit = hasLevel8WalkerPlayerHit(_levelRouteIndex, walkerFrame,
_perspectiveX, _perspectiveY);
+ // RunLevel8Flow sets damage flag 0x20 when the AT-AT route contact
+ // helper hits the player; boss damage uses _walkerHealth separately.
if (level8WalkerPlayerHit)
_damageFlags |= 0x20;
}
@@ -1231,18 +1233,18 @@ void InsaneRebel1::updateGameOp0BPhysics() {
}
// Damage application (FUN_1CDA7 lines 20-41)
- // Original 0x0B mapping: 0x80 -> +0x0F, 0x40 -> +0x11, 0x20 -> +0x13.
+ // Original 0x0B mapping: 0x80 -> +0x13, 0x40 -> +0x0F, 0x20 -> +0x11.
// No cooldown â all three damage types can stack each frame
if (_damageFlags != 0 && _health >= 0 && _deathTimer < 1) {
const int16 oldHealth = _health;
const byte appliedDamageFlags = _damageFlags;
_screenFlash = 5;
if (_damageFlags & 0x80)
- _health -= _tuning.miss;
+ _health -= _tuning.shot;
if (_damageFlags & 0x40)
- _health -= _tuning.wham;
+ _health -= _tuning.miss;
if (_damageFlags & 0x20)
- _health -= _tuning.shot;
+ _health -= _tuning.wham;
if (_health < 0) {
_deathTimer = 15; // 0x0F â shorter than Level 1's 30
if (_damageFlags & 0x80)
diff --git a/engines/scumm/insane/rebel1/rebel.h b/engines/scumm/insane/rebel1/rebel.h
index 6c8117ee2e3..74e99fbee89 100644
--- a/engines/scumm/insane/rebel1/rebel.h
+++ b/engines/scumm/insane/rebel1/rebel.h
@@ -319,9 +319,9 @@ private:
int16 slide; // +0x09: cross-axis coupling
int16 drift; // +0x0B: drift/turbulence multiplier
int16 snap; // +0x0D: hit radius for shooting targets
- int16 miss; // +0x0F: first damage value; FUN_1CDA7 uses it for bit 0x80
- int16 wham; // +0x11: second damage value; FUN_1CDA7 uses it for bit 0x40
- int16 shot; // +0x13: third damage value; FUN_1CDA7 uses it for bit 0x20
+ int16 miss; // +0x0F: light/scripted damage; FUN_1CDA7 uses it for bit 0x40
+ int16 wham; // +0x11: medium/contact damage; FUN_1CDA7 uses it for bit 0x20
+ int16 shot; // +0x13: heavy/projectile damage; FUN_1CDA7 uses it for bit 0x80
int16 kill; // +0x15: score per target kill
int16 time; // +0x17: survival bonus (added every 32 frames)
int16 levelPts; // +0x19: chapter completion bonus (RunChapterCompleteSummaryScreen)
diff --git a/engines/scumm/insane/rebel1/render.cpp b/engines/scumm/insane/rebel1/render.cpp
index 9433b49b761..57e3ee12c63 100644
--- a/engines/scumm/insane/rebel1/render.cpp
+++ b/engines/scumm/insane/rebel1/render.cpp
@@ -1367,8 +1367,8 @@ void InsaneRebel1::updateLevel8WalkerState() {
return;
}
- // FUN_12FE1/FUN_130C9/FUN_13195 test whether the walker shot hits the
- // player. The port synthesizes that player-damage flag in updateGameOp0BPhysics(),
+ // FUN_12FE1/FUN_130C9/FUN_13195 test whether the route-specific walker
+ // contact hazards hit the player. The port synthesizes that damage flag in updateGameOp0BPhysics(),
// where the 0x0B damage flags are consumed. This is unrelated to _walkerHealth,
// which is the boss health displayed by the Level 8 overlay.
int route = CLIP(_levelRouteIndex, 0, 2);
diff --git a/engines/scumm/insane/rebel1/runlevels.cpp b/engines/scumm/insane/rebel1/runlevels.cpp
index 7486d22755d..240497668d1 100644
--- a/engines/scumm/insane/rebel1/runlevels.cpp
+++ b/engines/scumm/insane/rebel1/runlevels.cpp
@@ -915,7 +915,10 @@ bool InsaneRebel1::runLevel8() {
_currentLevel = 7;
loadLevelSprites(8);
- loadTuningForLevel(7);
+ // RunLevel8Flow starts L8PLAY.ANM with initLevelFlag=10, so the walker
+ // chapter uses the original "8" tuning row while still keeping chapter
+ // index 7 for assets and Level 8-specific runtime logic.
+ loadTuningForLevel(10);
beginLevelTitleOverlay(7);
playCinematic("LVL8/L8INTRO.ANM");
Commit: 357f9ca51e0e7d4b54f890afd27a5afd36d22413
https://github.com/scummvm/scummvm/commit/357f9ca51e0e7d4b54f890afd27a5afd36d22413
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:10:01+02:00
Commit Message:
SCUMM: RA1: Render SMUSH_CODEC_SKIP_RLE correctly
Changed paths:
engines/scumm/smush/rebel/smush_player_ra1.cpp
diff --git a/engines/scumm/smush/rebel/smush_player_ra1.cpp b/engines/scumm/smush/rebel/smush_player_ra1.cpp
index 451b3a0506b..7754e319101 100644
--- a/engines/scumm/smush/rebel/smush_player_ra1.cpp
+++ b/engines/scumm/smush/rebel/smush_player_ra1.cpp
@@ -596,8 +596,12 @@ bool SmushPlayerRebel1::handleGameCodecDecode(int codec, const uint8 *src, int l
case SMUSH_CODEC_SKIP_RLE: {
const int bufWidth = pitch;
const int bufHeight = (_dst == _specialBuffer) ? _height : _vm->_screenHeight;
+ // Codec 23 uses the high byte of the FOBJ codec word as the palette
+ // band for its additive delta. The event-mask path may subtract 0x10
+ // from this value before decoding, which Level 8 uses for the walker
+ // armor layers.
smushDecodeRA1AdditiveLineUpdate(_dst, src, left, top, width, height,
- pitch, bufWidth, bufHeight, 0);
+ pitch, bufWidth, bufHeight, param);
return true;
}
default:
Commit: 876fcfa145943c996cb6f6edcfed51017b2ebf26
https://github.com/scummvm/scummvm/commit/876fcfa145943c996cb6f6edcfed51017b2ebf26
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:10:01+02:00
Commit Message:
SCUMM: RA1: Avoid trapping ScummVM menu during gameplay
Changed paths:
engines/scumm/insane/rebel1/menu.cpp
diff --git a/engines/scumm/insane/rebel1/menu.cpp b/engines/scumm/insane/rebel1/menu.cpp
index 4f7ef736b5d..eb9e7f17472 100644
--- a/engines/scumm/insane/rebel1/menu.cpp
+++ b/engines/scumm/insane/rebel1/menu.cpp
@@ -37,6 +37,12 @@ const int kRA1LevelSelectRowsPerCol = 8;
const int kRA1NumLevels = 15;
bool InsaneRebel1::notifyEvent(const Common::Event &event) {
+ // Global ScummVM dialogs pause the engine while their modal event loop runs.
+ // Do not consume those mouse/key events as RA1 gameplay/menu input, or the
+ // dialog buttons cannot receive clicks while an interactive video is active.
+ if (_vm->isPaused())
+ return false;
+
if (event.type == Common::EVENT_MOUSEMOVE && !_mouseRecentering) {
_activeInputSource = kInputSourceMouse;
}
Commit: a264d2921ca2c8aa1eeea783d32b3e7556dac6ff
https://github.com/scummvm/scummvm/commit/a264d2921ca2c8aa1eeea783d32b3e7556dac6ff
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:10:02+02:00
Commit Message:
SCUMM: RA1: Implement movement and damage for level 1 part 2
Changed paths:
engines/scumm/insane/rebel1/iact.cpp
engines/scumm/insane/rebel1/rebel.cpp
engines/scumm/insane/rebel1/rebel.h
engines/scumm/insane/rebel1/render.cpp
diff --git a/engines/scumm/insane/rebel1/iact.cpp b/engines/scumm/insane/rebel1/iact.cpp
index 88d30ac5298..d64f8e02d4a 100644
--- a/engines/scumm/insane/rebel1/iact.cpp
+++ b/engines/scumm/insane/rebel1/iact.cpp
@@ -47,24 +47,11 @@ inline int16 smoothRebel1Op0BAnalogInput(int16 inputValue, int16 &filteredValue,
const int16 kRA1Op09AimXScale[5] = { 0, 44, 88, 128, 165 };
const int16 kRA1Op09AimYScale[5] = { 256, 252, 240, 221, 196 };
-// LVL1 stage-2 0x5D damage/event codes. The gameplay stream exposes low record ids
-// (6..18), while the recovered outer loop compares the post-latch state against the
-// later translated values seen in the executable. Accept both representations.
-inline bool isL1Stage2DamageLatch(uint16 code) {
+// Level 15 final approach 0x5D damage/event codes consumed by
+// RunLevel1GameLoop. The latch stores the raw GAME parameter; no translation is
+// performed by HandleGameOp5D_SegmentLinkLatch.
+inline bool isLevel15FinalDamageLatch(uint16 code) {
switch (code) {
- case 0x0006:
- case 0x0007:
- case 0x0008:
- case 0x0009:
- case 0x000A:
- case 0x000B:
- case 0x000C:
- case 0x000D:
- case 0x000E:
- case 0x000F:
- case 0x0010:
- case 0x0011:
- case 0x0012:
case 0x0049:
case 0x004B:
case 0x004E:
@@ -81,7 +68,7 @@ inline bool isL1Stage2DamageLatch(uint16 code) {
}
}
-inline bool isL1Stage2SweepDamage(uint16 frameCounter, int16 perspectiveX) {
+inline bool hasLevel15FinalSweepDamage(uint16 frameCounter, int16 perspectiveX) {
switch (frameCounter) {
case 0x0034:
case 0x00ED:
@@ -910,8 +897,7 @@ void InsaneRebel1::updateShipPhysics() {
// RA1 FUN_1B297-style latches from GAME opcodes:
// 0x5D latch 0xFFFF -> bit 0x40 (obstacle/contact)
// 0x5F non-zero + RNG -> bit 0x80 (projectile-like hit)
- if (_gameLatch5D == 0xFFFF || (_currentLevel == 0 && _flyControlMode == 2 &&
- isL1Stage2DamageLatch(_gameLatch5D)))
+ if (_gameLatch5D == 0xFFFF)
_damageFlags |= 0x40;
if (_gameLatch5F != 0 && _vm->_rnd.getRandomNumber((uint16)(_gameLatch5F - 1)) == 0)
_damageFlags |= 0x80;
@@ -990,6 +976,67 @@ void InsaneRebel1::updateShipPhysics() {
_corridorLeftX, _corridorTopY, _corridorRightX, _corridorBottomY);
}
+// Port helper: FUN_1E6A7 computes this direction bucket inline before applying
+// the current frame's movement update.
+void InsaneRebel1::updateTurretShipDirection(int16 offsetY) {
+ int dir = 0;
+ if (_flyControlMode == 2) {
+ if (_rollAccum > 0x380) dir = 4;
+ else if (_rollAccum > 0x280) dir = 3;
+ else if (_rollAccum > 0x180) dir = 2;
+ else if (_rollAccum > 0x80) dir = 1;
+ else if (_rollAccum > -0x80) dir = 0;
+ else if (_rollAccum > -0x180) dir = 5;
+ else if (_rollAccum > -0x280) dir = 6;
+ else if (_rollAccum > -0x380) dir = 7;
+ else dir = 8;
+ } else {
+ if (_rollAccum > 0x380) dir = 8;
+ else if (_rollAccum > 0x280) dir = 7;
+ else if (_rollAccum > 0x180) dir = 6;
+ else if (_rollAccum > 0x80) dir = 5;
+ else if (_rollAccum > -0x80) dir = 4;
+ else if (_rollAccum > -0x180) dir = 3;
+ else if (_rollAccum > -0x280) dir = 2;
+ else if (_rollAccum > -0x380) dir = 1;
+ else dir = 0;
+
+ if (offsetY < -0x1E)
+ dir += 0x12;
+ else if (offsetY < 0x1E)
+ dir += 9;
+ }
+
+ const RA1SpriteBank *shipBank = &_shipBank;
+ if (_currentLevel == 0 && _flyControlMode == 2 && _shipBankAlt.numSprites > 0)
+ shipBank = &_shipBankAlt;
+ if (shipBank->numSprites > 0)
+ _shipDirIndex = CLIP<int16>((int16)dir, 0, shipBank->numSprites - 1);
+}
+
+void InsaneRebel1::getCollisionShipCenter(int16 &x, int16 &y) const {
+ // Original 0x0D/0x0E collision compares projected script zones against the
+ // drawn ship center (base center + g_shipOffset). This port draws into a
+ // 384x242 source buffer and later crops by the camera offset, so convert that
+ // source-buffer anchor to the visible screen-space point used by the projected
+ // zones.
+ //
+ // In Level 1 part 2, HandleGameOp0A_TurretVariant reuses _shipPos for the
+ // targeting cursor, so collision must read the movement accumulator instead.
+ if (_currentLevel == 0 && _flyControlMode == 2) {
+ x = (int16)(kRA1CenterX + (int16)(_posAccumX >> 8));
+ y = (int16)(kRA1CenterY + (int16)(_posAccumY >> 8));
+ } else {
+ x = _shipPosX;
+ y = _shipPosY;
+ }
+
+ if (_interactiveVideoActive) {
+ x = (int16)(x - _perspectiveX);
+ y = (int16)(y - _perspectiveY);
+ }
+}
+
// updateTurretPhysics â FUN_1E6A7 (0x1E6A7), opcode 0x08 path.
// Stage-2 cockpit mode uses different smoothing/clamps than FUN_1DEB5.
void InsaneRebel1::updateTurretPhysics() {
@@ -1001,16 +1048,9 @@ void InsaneRebel1::updateTurretPhysics() {
const int32 counter = _gameCounter;
const uint16 modeFlags = _frameDispatchFlags;
- // RA1 latches consumed by handler family in FUN_1B297.
- if (_currentLevel == 0 && _flyControlMode == 2 && isL1Stage2SweepDamage((uint16)counter, _perspectiveX))
- _damageFlags |= 0x20;
- if (_gameLatch5D == 0xFFFF || (_currentLevel == 0 && _flyControlMode == 2 &&
- isL1Stage2DamageLatch(_gameLatch5D)))
+ if (_gameLatch5D == 0xFFFF)
_damageFlags |= 0x40;
- if (_gameLatch5F != 0 &&
- ((_currentLevel == 0 && _flyControlMode == 2)
- ? (_vm->_rnd.getRandomNumber(2) == 0)
- : (_vm->_rnd.getRandomNumber((uint16)(_gameLatch5F - 1)) == 0)))
+ if (_gameLatch5F != 0 && _vm->_rnd.getRandomNumber((uint16)(_gameLatch5F - 1)) == 0)
_damageFlags |= 0x80;
if (counter == 0) {
@@ -1023,6 +1063,19 @@ void InsaneRebel1::updateTurretPhysics() {
_damageCooldown = 0;
}
+ // GAME 0x0A appears before 0x08 in L1PLAY2.ANM. The original therefore
+ // draws shots/targeting and the ship from this pre-physics center, then
+ // updates the camera offset at the end of 0x08 for the final viewport copy.
+ const int16 preMoveOffsetX = (int16)(_posAccumX >> 8);
+ const int16 preMoveOffsetY = (int16)(_posAccumY >> 8);
+ if (_currentLevel == 0 && _flyControlMode == 2) {
+ _turretFrameShipOffsetX = preMoveOffsetX;
+ _turretFrameShipOffsetY = preMoveOffsetY;
+ _turretFrameShipCenterX = (int16)(kRA1CenterX + preMoveOffsetX);
+ _turretFrameShipCenterY = (int16)(kRA1CenterY + preMoveOffsetY);
+ _turretFrameShipCenterValid = true;
+ }
+
// Damage gate from FUN_1E6A7.
if (_damageFlags != 0 && _damageCooldown == 0 && _health >= 0 && _deathTimer <= 0) {
if (_damageFlags == 0x80)
@@ -1049,6 +1102,8 @@ void InsaneRebel1::updateTurretPhysics() {
if (_health < 0 && _deathTimer > 0)
_deathTimer--;
+ updateTurretShipDirection(preMoveOffsetY);
+
// FUN_1E6A7 movement gate: counter > 8 or flags bit 0x40.
if (counter > 8 || (modeFlags & 0x40)) {
// FUN_1E6A7 consumes DAT_756C/DAT_756E from the shared input bridge,
@@ -1105,40 +1160,6 @@ void InsaneRebel1::updateTurretPhysics() {
// main flight handler, derived directly from roll.
rebuildProjectionTable((int16)(-(_rollAccum >> 9)), 0x0D);
- // Direction bucket synthesis from FUN_1E6A7.
- int dir = 0;
- if (_flyControlMode == 2) {
- if (_rollAccum > 0x380) dir = 4;
- else if (_rollAccum > 0x280) dir = 3;
- else if (_rollAccum > 0x180) dir = 2;
- else if (_rollAccum > 0x80) dir = 1;
- else if (_rollAccum > -0x80) dir = 0;
- else if (_rollAccum > -0x180) dir = 5;
- else if (_rollAccum > -0x280) dir = 6;
- else if (_rollAccum > -0x380) dir = 7;
- else dir = 8;
- } else {
- if (_rollAccum > 0x380) dir = 8;
- else if (_rollAccum > 0x280) dir = 7;
- else if (_rollAccum > 0x180) dir = 6;
- else if (_rollAccum > 0x80) dir = 5;
- else if (_rollAccum > -0x80) dir = 4;
- else if (_rollAccum > -0x180) dir = 3;
- else if (_rollAccum > -0x280) dir = 2;
- else if (_rollAccum > -0x380) dir = 1;
- else dir = 0;
-
- if (offsetY < -0x1E)
- dir += 0x12;
- else if (offsetY < 0x1E)
- dir += 9;
- }
- const RA1SpriteBank *shipBank = &_shipBank;
- if (_currentLevel == 0 && _flyControlMode == 2 && _shipBankAlt.numSprites > 0)
- shipBank = &_shipBankAlt;
- if (shipBank->numSprites > 0)
- _shipDirIndex = CLIP<int16>((int16)dir, 0, shipBank->numSprites - 1);
-
// Regeneration + survival bonus via FUN_1BB0E call in this path.
if ((_frameCounter & 0x1F) == 0) {
if (_health >= 0 && _health < kMaxHealth)
@@ -1202,19 +1223,24 @@ void InsaneRebel1::updateGameOp0BPhysics() {
// RA1 FUN_1B297-style per-frame latches for 0x0B sections:
// 0x5D latch 0xFFFF -> bit 0x40 (scripted obstacle/contact)
// 0x5F non-zero + RNG -> bit 0x80 (scripted random hit)
+ const bool level15FinalPhase = (_currentLevel == 14 && _levelGameplayPhase == 2);
+
if (_gameLatch5D == 0xFFFF ||
(_currentLevel == 3 && isLevel4DamageLatch(_gameLatch5D)) ||
(_currentLevel == 5 && isLevel6DamageLatch(_gameLatch5D)) ||
- (_currentLevel == 9 && isLevel10DamageLatch(_gameLatch5D)))
+ (_currentLevel == 9 && isLevel10DamageLatch(_gameLatch5D)) ||
+ (level15FinalPhase && isLevel15FinalDamageLatch(_gameLatch5D)))
_damageFlags |= 0x40;
if (_gameLatch5F != 0 &&
- ((_currentLevel == 3 || _currentLevel == 9)
+ ((_currentLevel == 3 || _currentLevel == 9 || level15FinalPhase)
? (_vm->_rnd.getRandomNumber(2) == 0)
: (_vm->_rnd.getRandomNumber((uint16)(_gameLatch5F - 1)) == 0)))
_damageFlags |= 0x80;
if (_currentLevel == 5 && hasLevel6PerspectiveHazard((uint16)_frameCounter, _perspectiveX, _perspectiveY))
_damageFlags |= 0x20;
+ if (level15FinalPhase && hasLevel15FinalSweepDamage((uint16)_gameCounter, _perspectiveX))
+ _damageFlags |= 0x20;
bool level8WalkerPlayerHit = false;
if (_currentLevel == 7) {
@@ -1725,39 +1751,43 @@ void InsaneRebel1::handleGameChunk(int32 subSize, Common::SeekableReadStream &b)
// Apply 0x0D immediately so it sees the same pre-physics ship state as
// the other per-frame GAME latches. Deferring this until after 0x07/0x09
// movement made right-wall hits fire too early when steering into a wall.
- const bool suppressDirectionalDamage = (_damageFlags & 0x10) != 0;
+ int16 collisionShipX = _shipPosX;
+ int16 collisionShipY = _shipPosY;
+ getCollisionShipCenter(collisionShipX, collisionShipY);
+
+ const bool suppressDirectionalDamage = (_frameDispatchFlags & 0x10) != 0;
const byte oldDirectionalFlags = _damageFlags & 0x0F;
if (_health >= 0) {
- if (_shipPosX < _corridorLeftX) {
- _posAccumX = (int32)(_corridorLeftX - kRA1CenterX) * 0x100;
+ if (collisionShipX < _corridorLeftX) {
+ _posAccumX = (int32)(_corridorLeftX + _perspectiveX - kRA1CenterX) * 0x100;
if (!suppressDirectionalDamage) {
if (_rollAccum < 0x100)
_rollAccum = 0x100;
_damageFlags |= 0x04;
}
}
- if (_shipPosX > _corridorRightX) {
- _posAccumX = (int32)(_corridorRightX - kRA1CenterX) * 0x100;
+ if (collisionShipX > _corridorRightX) {
+ _posAccumX = (int32)(_corridorRightX + _perspectiveX - kRA1CenterX) * 0x100;
if (!suppressDirectionalDamage) {
if (_rollAccum > -0x100)
_rollAccum = -0x100;
_damageFlags |= 0x02;
}
}
- if (_shipPosY < _corridorTopY) {
- _posAccumY = (int32)(_corridorTopY - kRA1CenterY) * 0x100 + 0x100;
+ if (collisionShipY < _corridorTopY) {
+ _posAccumY = (int32)(_corridorTopY + _perspectiveY - kRA1CenterY) * 0x100 + 0x100;
if (!suppressDirectionalDamage)
_damageFlags |= 0x01;
}
- if (_shipPosY > _corridorBottomY) {
- _posAccumY = (int32)(_corridorBottomY - kRA1CenterY) * 0x100 - 0x100;
+ if (collisionShipY > _corridorBottomY) {
+ _posAccumY = (int32)(_corridorBottomY + _perspectiveY - kRA1CenterY) * 0x100 - 0x100;
if (!suppressDirectionalDamage)
_damageFlags |= 0x08;
}
}
if ((_damageFlags & 0x0F) != oldDirectionalFlags) {
debug(1, "RA1 0x0D hit: ship=(%d,%d) corridor=[%d,%d]-[%d,%d] flags=0x%02x zoneSuppressed=%d",
- _shipPosX, _shipPosY,
+ collisionShipX, collisionShipY,
_corridorLeftX, _corridorTopY, _corridorRightX, _corridorBottomY,
_damageFlags, suppressDirectionalDamage ? 1 : 0);
}
@@ -1777,6 +1807,8 @@ void InsaneRebel1::handleGameChunk(int32 subSize, Common::SeekableReadStream &b)
int16 zoneTop = (int16)b.readUint32BE();
int16 zoneWidth = (int16)b.readUint32BE();
int16 zoneHeight = (int16)b.readUint32BE();
+ const int16 rawZoneLeft = zoneLeft;
+ const int16 rawZoneTop = zoneTop;
int16 centerX = zoneLeft + zoneWidth / 2;
int16 centerY = zoneTop + zoneHeight / 2;
@@ -1786,12 +1818,22 @@ void InsaneRebel1::handleGameChunk(int32 subSize, Common::SeekableReadStream &b)
zoneTop = centerY - zoneHeight / 2;
int16 zoneRight = zoneLeft + zoneWidth;
int16 zoneBottom = zoneTop + zoneHeight;
- if (_shipPosX > zoneLeft && _shipPosX < zoneRight &&
- _shipPosY > zoneTop && _shipPosY < zoneBottom) {
+ int16 collisionShipX = _shipPosX;
+ int16 collisionShipY = _shipPosY;
+ getCollisionShipCenter(collisionShipX, collisionShipY);
+
+ if (_health >= 0 &&
+ collisionShipX > zoneLeft && collisionShipX < zoneRight &&
+ collisionShipY > zoneTop && collisionShipY < zoneBottom) {
_damageFlags |= 0x10;
+ debug(1, "RA1 0x0E hit: ship=(%d,%d) zone=[%d,%d]-[%d,%d] raw=[%d,%d]+(%d,%d) cam=(%d,%d) flags=0x%02x",
+ collisionShipX, collisionShipY, zoneLeft, zoneTop, zoneRight, zoneBottom,
+ rawZoneLeft, rawZoneTop, zoneWidth, zoneHeight,
+ _perspectiveX, _perspectiveY, _damageFlags);
}
- debug(7, "RA1 GAME 0x0E: zone=[%d,%d]-[%d,%d] cam=(%d,%d) flags=0x%02x",
- zoneLeft, zoneTop, zoneRight, zoneBottom, _perspectiveX, _perspectiveY, _damageFlags);
+ debug(7, "RA1 GAME 0x0E: ship=(%d,%d) zone=[%d,%d]-[%d,%d] cam=(%d,%d) flags=0x%02x",
+ collisionShipX, collisionShipY, zoneLeft, zoneTop, zoneRight, zoneBottom,
+ _perspectiveX, _perspectiveY, _damageFlags);
}
break;
@@ -1914,7 +1956,7 @@ void InsaneRebel1::processShot() {
// Shot origin depends on game mode:
// On-foot: character position (g_shipOffsetX + g_perspectiveX)
- // Turret: perspective-adjusted center
+ // Turret: ship center
// Flight: cursor position
const bool turretMode = (effectiveOpcode == 0x08 || effectiveOpcode == 0x0A);
int16 originX, originY;
@@ -1922,8 +1964,7 @@ void InsaneRebel1::processShot() {
originX = _onFootCharX + kOnFootCenterX;
originY = _onFootCharY + kOnFootCenterY;
} else if (turretMode) {
- originX = (int16)(kRA1CenterX + (_perspectiveX - 0x20));
- originY = (int16)(kRA1CenterY + (_perspectiveY - 0x17));
+ getTurretShipCenter(originX, originY);
} else {
originX = _shipPosX;
originY = _shipPosY;
diff --git a/engines/scumm/insane/rebel1/rebel.cpp b/engines/scumm/insane/rebel1/rebel.cpp
index 9d7dd390fb6..b9599e1194c 100644
--- a/engines/scumm/insane/rebel1/rebel.cpp
+++ b/engines/scumm/insane/rebel1/rebel.cpp
@@ -226,6 +226,11 @@ InsaneRebel1::InsaneRebel1(ScummEngine_v7 *scumm) : Insane(), _vm(scumm) {
_liftSmooth = 0;
_posAccumX = 0;
_posAccumY = 0;
+ _turretFrameShipOffsetX = 0;
+ _turretFrameShipOffsetY = 0;
+ _turretFrameShipCenterX = kRA1CenterX;
+ _turretFrameShipCenterY = kRA1CenterY;
+ _turretFrameShipCenterValid = false;
_driftParam = 0;
// Original startup initializes the options difficulty/tuning index to 1.
diff --git a/engines/scumm/insane/rebel1/rebel.h b/engines/scumm/insane/rebel1/rebel.h
index 74e99fbee89..0e874041d17 100644
--- a/engines/scumm/insane/rebel1/rebel.h
+++ b/engines/scumm/insane/rebel1/rebel.h
@@ -147,6 +147,8 @@ private:
void loadLevelSprites(int level);
void updateShipPhysics();
void updateTurretPhysics();
+ void updateTurretShipDirection(int16 offsetY);
+ void getCollisionShipCenter(int16 &x, int16 &y) const;
void preprocessMouseAxes(int16 &inputX, int16 &inputY, bool *usedJoystick = nullptr);
void rebuildProjectionTable(int16 curveStep, int16 curveExtent);
void resetProjectionTable();
@@ -161,6 +163,7 @@ private:
void renderGostScorePopup(byte *dst, int pitch, int width, int height,
int16 centerX, int16 centerY, int16 frame);
void renderLaserShots(byte *dst, int pitch, int width, int height);
+ void getTurretShipCenter(int16 &x, int16 &y) const;
void renderLevel8Overlay(byte *dst, int pitch, int width, int height,
int viewportX, int viewportY);
void updateLevel8WalkerState();
@@ -231,6 +234,11 @@ private:
// _74C2/_74C6: position accumulators (32-bit), pixel offset = accum >> 8
int32 _posAccumX;
int32 _posAccumY;
+ int16 _turretFrameShipOffsetX;
+ int16 _turretFrameShipOffsetY;
+ int16 _turretFrameShipCenterX;
+ int16 _turretFrameShipCenterY;
+ bool _turretFrameShipCenterValid;
// Per-frame drift bias from GAME 0x07 field3
int16 _driftParam;
diff --git a/engines/scumm/insane/rebel1/render.cpp b/engines/scumm/insane/rebel1/render.cpp
index 57e3ee12c63..f56592dafa1 100644
--- a/engines/scumm/insane/rebel1/render.cpp
+++ b/engines/scumm/insane/rebel1/render.cpp
@@ -359,6 +359,27 @@ void renderSpriteWithFlags(byte *dst, int pitch, int width, int height,
}
}
+void InsaneRebel1::getTurretShipCenter(int16 &x, int16 &y) const {
+ if (_currentLevel == 0 && _flyControlMode == 2) {
+ // Port helper: original Level 1 part 2 keeps g_perspectiveX/Y at the
+ // screen center and draws the overhead ship at g_shipOffsetX/Y from
+ // FUN_1E6A7. In this port _perspectiveX/Y stores the clamped camera
+ // offset passed to SetCameraOffset(), so recover the ship center from
+ // the movement accumulators instead.
+ if (_turretFrameShipCenterValid) {
+ x = _turretFrameShipCenterX;
+ y = _turretFrameShipCenterY;
+ } else {
+ x = (int16)(kRA1CenterX + (int16)(_posAccumX >> 8));
+ y = (int16)(kRA1CenterY + (int16)(_posAccumY >> 8));
+ }
+ return;
+ }
+
+ x = (int16)(kRA1CenterX + (_perspectiveX - 0x20));
+ y = (int16)(kRA1CenterY + (_perspectiveY - 0x17));
+}
+
// procPreRendering â Sets viewport window offset (FUN_224FD at 0x224FD).
// RA1 decodes FOBJs at chunk coordinates, then displays a scrolled 320x200
// window inside the 384x242 framebuffer.
@@ -366,6 +387,7 @@ void InsaneRebel1::procPreRendering(byte *renderBitmap) {
_frameGameOpcodeMask = 0;
_frameDispatchFlags = 0;
_gameOp0BPhysicsUpdatedThisFrame = false;
+ _turretFrameShipCenterValid = false;
if (_interactiveVideoActive && _player) {
const bool usePerspectiveViewport =
@@ -555,10 +577,16 @@ void InsaneRebel1::procPostRendering(byte *renderBitmap, int32 codecparam, int32
// FUN_1D79C (GAME 0x0A) owns the cursor center in stage-2 turret mode.
// The preceding overlay/shot pass uses the previous frame's cursor; the
// handler then publishes the next cursor position from the current
- // ship-offset and camera state.
+ // pre-physics ship offset. In L1PLAY2 the following 0x08 handler updates
+ // the camera afterward, so source-space anchors and the final viewport
+ // crop intentionally observe different moments in the frame.
if (turretTargetingMode) {
- const int16 shipOffsetX = (int16)(_posAccumX >> 8);
- const int16 shipOffsetY = (int16)(_posAccumY >> 8);
+ const bool useTurretFrameCenter =
+ (_currentLevel == 0 && _flyControlMode == 2 && _turretFrameShipCenterValid);
+ const int16 shipOffsetX = useTurretFrameCenter ?
+ _turretFrameShipOffsetX : (int16)(_posAccumX >> 8);
+ const int16 shipOffsetY = useTurretFrameCenter ?
+ _turretFrameShipOffsetY : (int16)(_posAccumY >> 8);
_shipPosX = (int16)(kRA1CenterX + shipOffsetX);
_shipPosY = (int16)((kRA1CenterY + shipOffsetY - 0x23) - (shipOffsetY >> 3));
} else if (flightVariantTargetingMode) {
@@ -776,9 +804,14 @@ void InsaneRebel1::renderLaserShots(byte *dst, int pitch, int width, int height)
const bool onFootMode = (effectiveOpcode == 0x19 || effectiveOpcode == 0x1A);
const bool turretMode = (effectiveOpcode == 0x08 || effectiveOpcode == 0x0A);
const bool flightVariantMode = (effectiveOpcode == 0x09);
- const int shipBaseX = turretMode ? (kRA1CenterX + (_perspectiveX - 0x20)) : _shipPosX;
- const int shipBaseY = turretMode ? (kRA1CenterY + (_perspectiveY - 0x17))
- : (flightVariantMode ? _shipPosY : (overlayY + _shipPosY));
+ int shipBaseX = _shipPosX;
+ int shipBaseY = flightVariantMode ? _shipPosY : (overlayY + _shipPosY);
+ if (turretMode) {
+ int16 centerX, centerY;
+ getTurretShipCenter(centerX, centerY);
+ shipBaseX = centerX;
+ shipBaseY = centerY;
+ }
for (int i = 0; i < kMaxShotSlots; i++) {
if (_shotSlots[i].timer > 0 && _shotSlots[i].timer <= spritesPerSet) {
@@ -1142,15 +1175,16 @@ void InsaneRebel1::renderShip(byte *dst, int pitch, int width, int height) {
const RA1Sprite &spr = shipBank->sprites[_shipDirIndex];
- // In 0x08/0x0A turret handlers, _shipPos holds targeting/cursor state, while
- // the ship sprite is still anchored from camera perspective + ship drift
- // offsets. Flight handlers already store the ship sprite center in _shipPos.
+ // In 0x08/0x0A turret handlers, _shipPos holds targeting/cursor state.
+ // Flight handlers already store the ship sprite center in _shipPos.
int shipScreenX = _shipPosX;
int shipScreenY = _shipPosY;
const uint16 effectiveOpcode = getEffectiveGameOpcode();
if (effectiveOpcode == 0x08 || effectiveOpcode == 0x0A) {
- shipScreenX = kRA1CenterX + (_perspectiveX - 0x20);
- shipScreenY = kRA1CenterY + (_perspectiveY - 0x17);
+ int16 centerX, centerY;
+ getTurretShipCenter(centerX, centerY);
+ shipScreenX = centerX;
+ shipScreenY = centerY;
}
int drawX = shipScreenX - spr.width / 2;
@@ -1174,8 +1208,10 @@ void InsaneRebel1::renderExplosions(byte *dst, int pitch, int width, int height)
int shipScreenY = overlayY + _shipPosY;
const uint16 effectiveOpcode = getEffectiveGameOpcode();
if (effectiveOpcode == 0x08 || effectiveOpcode == 0x0A) {
- shipScreenX = kRA1CenterX + (_perspectiveX - 0x20);
- shipScreenY = kRA1CenterY + (_perspectiveY - 0x17);
+ int16 centerX, centerY;
+ getTurretShipCenter(centerX, centerY);
+ shipScreenX = centerX;
+ shipScreenY = centerY;
}
// --- Death shake explosions (FUN_1DEB5 LAB_1e0e3) ---
Commit: b45fc2804caa742cda17130b54b94cb33a1f73a9
https://github.com/scummvm/scummvm/commit/b45fc2804caa742cda17130b54b94cb33a1f73a9
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:10:02+02:00
Commit Message:
SCUMM: RA1: Allow destroying targets in level 1 part 2
Changed paths:
engines/scumm/insane/rebel1/iact.cpp
engines/scumm/insane/rebel1/runlevels.cpp
diff --git a/engines/scumm/insane/rebel1/iact.cpp b/engines/scumm/insane/rebel1/iact.cpp
index d64f8e02d4a..189fcda8345 100644
--- a/engines/scumm/insane/rebel1/iact.cpp
+++ b/engines/scumm/insane/rebel1/iact.cpp
@@ -1992,13 +1992,18 @@ void InsaneRebel1::processShot() {
}
// checkTargetHit â FUN_1C0EF (0x1C0EF). AABB target detection with snap tolerance.
-// The original compares target bounds against the cursor after
-// UnprojectScreenPoint(), then reprojects the snapped cursor center after a hit.
+// The original compares target bounds against the cursor after UnprojectScreenPoint()
+// because g_shipPos is a screen-space cursor. Most handlers in this port draw the
+// cursor into the 384x242 source buffer before the viewport crop, so their cursor is
+// already in the same target space as raw FOBJ bounds. Opcode 0x0B remains screen-space
+// and still needs the original project/unproject conversion.
void InsaneRebel1::checkTargetHit(int16 targetIdx, int16 left, int16 top, int16 right, int16 bottom) {
int16 snap = _tuning.snap;
+ const bool screenSpaceCursor = (getEffectiveGameOpcode() == 0x0B);
int16 curX = getGameplayCursorX();
int16 curY = getGameplayCursorY();
- unprojectGameplayPoint(curX, curY);
+ if (screenSpaceCursor)
+ unprojectGameplayPoint(curX, curY);
const int slot = _targetCount;
if (slot < kMaxTargetBoxes) {
@@ -2028,6 +2033,13 @@ void InsaneRebel1::checkTargetHit(int16 targetIdx, int16 left, int16 top, int16
if (curX > left - snap && curX < right + snap &&
curY > top - snap && curY < bottom + snap) {
_targetProximity = 2; // On-target
+ if (snap > 0) {
+ int16 snappedX = (left + right) / 2;
+ int16 snappedY = (top + bottom) / 2;
+ if (screenSpaceCursor)
+ projectGameplayPoint(snappedX, snappedY);
+ setGameplayCursor(snappedX, snappedY);
+ }
// DOS uses g_recentKillObjectIdPlus1 as a frame-wide latch. Once one
// target is hit this frame, overlapping FOBJ layers must not consume the
@@ -2079,15 +2091,6 @@ void InsaneRebel1::checkTargetHit(int16 targetIdx, int16 left, int16 top, int16
const int sfxPan = CLIP((hitCenterX - kRA1CenterX) * 127 / kRA1CenterX, -127, 127);
playSfx(kSfxExplode, 127, sfxPan);
- // Match FUN_1C0EF: snap in unprojected space, then project back
- // into the current gameplay window before rendering the pointer.
- if (snap > 0) {
- int16 snappedX = (left + right) / 2;
- int16 snappedY = (top + bottom) / 2;
- projectGameplayPoint(snappedX, snappedY);
- setGameplayCursor(snappedX, snappedY);
- }
-
debug(3, "RA1 HIT: target=%d gost=%d pos=(%d,%d) score=%d kills=%d bangSprites=%d",
targetIdx, gi, _gostSlots[gi].posX, _gostSlots[gi].posY,
_score, _killCount, _bangBank.numSprites);
diff --git a/engines/scumm/insane/rebel1/runlevels.cpp b/engines/scumm/insane/rebel1/runlevels.cpp
index 240497668d1..d6ce21e3f13 100644
--- a/engines/scumm/insane/rebel1/runlevels.cpp
+++ b/engines/scumm/insane/rebel1/runlevels.cpp
@@ -334,6 +334,7 @@ bool InsaneRebel1::runLevel1() {
_pathBranchEnabled = true;
_rightPathSelected = false;
_flyControlMode = 1;
+ loadTuningForLevel(0);
playInteractiveVideo("LVL1/L1PLAY1L.ANM");
if (_vm->shouldQuit())
@@ -354,6 +355,10 @@ bool InsaneRebel1::runLevel1() {
return false;
while (!_vm->shouldQuit()) {
+ // RunLevel1Flow calls L1PLAY2 with gameplay selector 1. This is
+ // the "1B" tuning row: snap/kill values are enabled and the
+ // lock/fire text overlay is suppressed.
+ loadTuningForLevel(1);
_flyControlMode = 2;
_turretEmitterLeftX = 10;
_turretEmitterLeftY = -5;
Commit: 361d886ac75b54b9babbad365de8dcaecdd59e31
https://github.com/scummvm/scummvm/commit/361d886ac75b54b9babbad365de8dcaecdd59e31
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:10:02+02:00
Commit Message:
SCUMM: RA1: Implement damage in level 2
Changed paths:
engines/scumm/insane/rebel1/iact.cpp
diff --git a/engines/scumm/insane/rebel1/iact.cpp b/engines/scumm/insane/rebel1/iact.cpp
index 189fcda8345..ba14a752bbf 100644
--- a/engines/scumm/insane/rebel1/iact.cpp
+++ b/engines/scumm/insane/rebel1/iact.cpp
@@ -83,6 +83,38 @@ inline bool hasLevel15FinalSweepDamage(uint16 frameCounter, int16 perspectiveX)
}
}
+inline bool isLevel2DamageLatch(uint16 code) {
+ switch (code) {
+ case 0x0003:
+ case 0x0009:
+ case 0x000A:
+ case 0x000D:
+ case 0x0012:
+ case 0x0015:
+ return true;
+ default:
+ return false;
+ }
+}
+
+// Level 2 asteroid-contact helper from FUN_00012d70. RunLevel2Flow calls it
+// once per frontend frame and raises damage flag 0x20 when the current asteroid
+// pass intersects the camera position.
+inline bool hasLevel2AsteroidImpact(uint16 frameCounter, int16 perspectiveX, int16 perspectiveY) {
+ switch (frameCounter) {
+ case 0x0071:
+ case 0x0271:
+ return perspectiveX >= 0x28;
+ case 0x011E:
+ return perspectiveY >= 0x1F;
+ case 0x01E1:
+ case 0x02ED:
+ return perspectiveX > 0x17;
+ default:
+ return false;
+ }
+}
+
const int16 kLevel7BranchFrames[6][6] = {
{ -1, 78, 267, 398, 556, 630 },
{ -1, 187, 376, 507, 665, 739 },
@@ -1224,8 +1256,10 @@ void InsaneRebel1::updateGameOp0BPhysics() {
// 0x5D latch 0xFFFF -> bit 0x40 (scripted obstacle/contact)
// 0x5F non-zero + RNG -> bit 0x80 (scripted random hit)
const bool level15FinalPhase = (_currentLevel == 14 && _levelGameplayPhase == 2);
+ bool level2AsteroidHit = false;
if (_gameLatch5D == 0xFFFF ||
+ (_currentLevel == 1 && isLevel2DamageLatch(_gameLatch5D)) ||
(_currentLevel == 3 && isLevel4DamageLatch(_gameLatch5D)) ||
(_currentLevel == 5 && isLevel6DamageLatch(_gameLatch5D)) ||
(_currentLevel == 9 && isLevel10DamageLatch(_gameLatch5D)) ||
@@ -1242,6 +1276,12 @@ void InsaneRebel1::updateGameOp0BPhysics() {
if (level15FinalPhase && hasLevel15FinalSweepDamage((uint16)_gameCounter, _perspectiveX))
_damageFlags |= 0x20;
+ if (_currentLevel == 1) {
+ level2AsteroidHit = hasLevel2AsteroidImpact((uint16)_gameCounter, _perspectiveX, _perspectiveY);
+ if (level2AsteroidHit)
+ _damageFlags |= 0x20;
+ }
+
bool level8WalkerPlayerHit = false;
if (_currentLevel == 7) {
const uint16 walkerFrame = (uint16)_gameCounter;
@@ -1280,6 +1320,12 @@ void InsaneRebel1::updateGameOp0BPhysics() {
}
// FUN_1CDA7 dispatches g_sfxDamageHit, initialized from SYS/BOOM.SAD.
playSfx(kSfxBoom, 127, 0);
+ if (_currentLevel == 1) {
+ debug(1, "RA1 L2 player hit: frame=%u view=(%d,%d) latch=%u asteroid=%d flags=0x%02x health=%d->%d",
+ (unsigned)(uint16)_gameCounter, _perspectiveX, _perspectiveY,
+ (unsigned)_gameLatch5D, level2AsteroidHit ? 1 : 0,
+ appliedDamageFlags, oldHealth, _health);
+ }
if (level8WalkerPlayerHit) {
debug(1, "RA1 L8 player hit by walker: route=%d frame=%u view=(%d,%d) flags=0x%02x health=%d->%d",
CLIP<int>(_levelRouteIndex, 0, 2), (unsigned)(uint16)_gameCounter,
Commit: 03b63a2b5b31e15af93154469c6d7e8debe093a1
https://github.com/scummvm/scummvm/commit/03b63a2b5b31e15af93154469c6d7e8debe093a1
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:10:03+02:00
Commit Message:
SCUMM: RA1: Add missing damage sounds
Changed paths:
engines/scumm/insane/rebel1/iact.cpp
diff --git a/engines/scumm/insane/rebel1/iact.cpp b/engines/scumm/insane/rebel1/iact.cpp
index ba14a752bbf..900f59e558f 100644
--- a/engines/scumm/insane/rebel1/iact.cpp
+++ b/engines/scumm/insane/rebel1/iact.cpp
@@ -956,6 +956,8 @@ void InsaneRebel1::updateShipPhysics() {
_prevDamageFlags = _damageFlags;
_damageCooldown = kDamageCooldownInit;
+ // HandleGameOp07_ShipFlight dispatches g_sfxDamageHit here.
+ playSfx(kSfxBoom, 127, 0);
_screenFlash = 3;
}
@@ -1125,6 +1127,8 @@ void InsaneRebel1::updateTurretPhysics() {
_prevDamageFlags = _damageFlags;
_damageCooldown = kDamageCooldownInit;
+ // HandleGameOp08_TurretFlight dispatches g_sfxDamageHit here.
+ playSfx(kSfxBoom, 127, 0);
_screenFlash = 3;
}
@@ -1611,6 +1615,7 @@ void InsaneRebel1::updateOnFootPhysics() {
}
_prevDamageFlags = _damageFlags;
_damageCooldown = 3;
+ playSfx(kSfxBoom, 127, 0);
_screenFlash = 5;
}
@@ -1634,6 +1639,7 @@ void InsaneRebel1::updateOnFootPhysics() {
_screenFlash--;
_screenShakeEnabled = (_screenFlash > 0);
}
+ updateScreenFlashPalette();
_damageFlags = 0;
_frameCounter++;
Commit: b31651a76c4e48c5a88e5c27a7bbe8fb6259eac8
https://github.com/scummvm/scummvm/commit/b31651a76c4e48c5a88e5c27a7bbe8fb6259eac8
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:10:03+02:00
Commit Message:
SCUMM: RA1: Implement level 13
Changed paths:
engines/scumm/insane/rebel1/rebel.cpp
engines/scumm/insane/rebel1/rebel.h
engines/scumm/insane/rebel1/render.cpp
engines/scumm/insane/rebel1/runlevels.cpp
diff --git a/engines/scumm/insane/rebel1/rebel.cpp b/engines/scumm/insane/rebel1/rebel.cpp
index b9599e1194c..0412b1a8a9b 100644
--- a/engines/scumm/insane/rebel1/rebel.cpp
+++ b/engines/scumm/insane/rebel1/rebel.cpp
@@ -363,6 +363,7 @@ InsaneRebel1::InsaneRebel1(ScummEngine_v7 *scumm) : Insane(), _vm(scumm) {
_gostSlotIdx = 0;
_killCount = 0;
_lastHitTarget = 0;
+ resetEnemyShotSlots();
_protectedTargetA = 0;
_protectedTargetB = 0;
_shieldGenHitsA = 0;
diff --git a/engines/scumm/insane/rebel1/rebel.h b/engines/scumm/insane/rebel1/rebel.h
index 0e874041d17..1da9652da5a 100644
--- a/engines/scumm/insane/rebel1/rebel.h
+++ b/engines/scumm/insane/rebel1/rebel.h
@@ -163,6 +163,8 @@ private:
void renderGostScorePopup(byte *dst, int pitch, int width, int height,
int16 centerX, int16 centerY, int16 frame);
void renderLaserShots(byte *dst, int pitch, int width, int height);
+ void resetEnemyShotSlots();
+ void renderLevel13EnemyShots(byte *dst, int pitch, int width, int height);
void getTurretShipCenter(int16 &x, int16 &y) const;
void renderLevel8Overlay(byte *dst, int pitch, int width, int height,
int viewportX, int viewportY);
@@ -203,6 +205,7 @@ private:
RA1SpriteBank _techFontBank; // SYS/TECHFONT.NUT â targeting glyph layer ("<<" markers)
RA1SpriteBank _bangBank; // LxBANG.NUT â impact/explosion sprites (10 frames)
RA1SpriteBank _laserBank; // LxLASER.NUT â laser/shot effect sprites
+ RA1SpriteBank _enemyLaserBank; // LxLASR2.NUT â incoming projectile sprites
SmushFont *_menuFont; // Use engine text renderer for correct TALKFONT character mapping
// RA1 screen dimensions (384x242)
@@ -482,6 +485,19 @@ private:
int16 _killCount; // 0x75D0: targets destroyed this stage
int16 _lastHitTarget; // 0x75D6: recent-kill latch, allows at most one hit per frame
+ // Incoming enemy projectile slots used by Level 13 RunLevel13Flow.
+ static const int kMaxEnemyShotSlots = 5;
+ struct EnemyShotSlot {
+ int16 timer;
+ int16 startX;
+ int16 startY;
+ int16 targetX;
+ int16 targetY;
+ int16 direction;
+ uint16 flags;
+ };
+ EnemyShotSlot _enemyShotSlots[kMaxEnemyShotSlots];
+
// Protected target IDs â 0x7732/0x7734 in original
// Targets listed here can be hit repeatedly (no event mask toggle).
// Used by Level 4 (shield generators) and Level 15 (torpedo targets).
diff --git a/engines/scumm/insane/rebel1/render.cpp b/engines/scumm/insane/rebel1/render.cpp
index f56592dafa1..039e4c10f17 100644
--- a/engines/scumm/insane/rebel1/render.cpp
+++ b/engines/scumm/insane/rebel1/render.cpp
@@ -241,6 +241,41 @@ int ra1ShotDirectionBucket(int dir) {
return 10;
}
+// QuantizeDirectionWithAxisFlags from RunLevel13Flow: returns one of nine
+// direction sprites and publishes DrawFobjGlyph flip flags.
+int ra1ProjectileDirectionWithFlags(int16 srcX, int16 srcY, int16 dstX, int16 dstY, uint16 &flags) {
+ int dx = srcX - dstX;
+ int dy = dstY - srcY;
+ flags = 0;
+
+ if (dy < 0) {
+ dy = -dy;
+ flags |= 0x4000;
+ }
+ if (dx < 0) {
+ dx = -dx;
+ flags |= 0x2000;
+ }
+
+ if (dx * 10 < dy)
+ return 0;
+ if (dx * 10 < dy * 3)
+ return 1;
+ if (dx * 2 < dy)
+ return 2;
+ if (dx * 5 < dy * 4)
+ return 3;
+ if (dx * 4 < dy * 5)
+ return 4;
+ if (dx < dy * 2)
+ return 5;
+ if (dx * 3 < dy * 10)
+ return 6;
+ if (dx < dy * 10)
+ return 7;
+ return 8;
+}
+
struct RA1ShotEmitterPair {
int16 x1;
int16 y1;
@@ -602,6 +637,9 @@ void InsaneRebel1::procPostRendering(byte *renderBitmap, int32 codecparam, int32
_lastHitTarget = 0;
}
+ if (_currentLevel == 12)
+ renderLevel13EnemyShots(renderBitmap, pitch, width, height);
+
renderExplosions(renderBitmap, pitch, width, height);
// Level 8 (Imperial Walkers) â walker-specific state update + UI overlay.
@@ -771,6 +809,80 @@ void InsaneRebel1::renderGostSlots(byte *dst, int pitch, int width, int height)
}
}
+void InsaneRebel1::resetEnemyShotSlots() {
+ memset(_enemyShotSlots, 0, sizeof(_enemyShotSlots));
+}
+
+// Port helper for Level 13 RunLevel13Flow. The original stores this state in
+// five local stack arrays around the L13PLAY frontend loop, not in a named function.
+void InsaneRebel1::renderLevel13EnemyShots(byte *dst, int pitch, int width, int height) {
+ if (_currentLevel != 12 || !_interactiveVideoActive || _health < 0)
+ return;
+
+ const int targetCount = CLIP<int>(_prevTargetCount, 0, kMaxTargetBoxes);
+ const uint16 effectiveOpcode = getEffectiveGameOpcode();
+
+ int16 playerX = getGameplayCursorX();
+ int16 playerY = getGameplayCursorY();
+ if (effectiveOpcode == 0x08 || effectiveOpcode == 0x0A) {
+ getTurretShipCenter(playerX, playerY);
+ } else if (effectiveOpcode == 0x0B) {
+ unprojectGameplayPoint(playerX, playerY);
+ }
+
+ for (int i = 0; i < targetCount; i++) {
+ if (_targetBoxX[i] - 7 < playerX && playerX < _targetBoxX[i] + 7 &&
+ _targetBoxY[i] - 7 < playerY && playerY < _targetBoxY[i] + 7) {
+ _damageFlags |= 0x10;
+ }
+ }
+
+ if (targetCount > 0 && _vm->_rnd.getRandomNumber(19) == 0) {
+ for (int i = 0; i < kMaxEnemyShotSlots; i++) {
+ EnemyShotSlot &slot = _enemyShotSlots[i];
+ if (slot.timer != 0)
+ continue;
+
+ const int targetIdx = _vm->_rnd.getRandomNumber(targetCount - 1);
+ slot.startX = _targetBoxX[targetIdx];
+ slot.startY = _targetBoxY[targetIdx];
+ slot.targetX = playerX;
+ slot.targetY = playerY;
+ slot.timer = 0x0F;
+ slot.direction = ra1ProjectileDirectionWithFlags(slot.startX, slot.startY,
+ slot.targetX, slot.targetY, slot.flags);
+ playSfx(kSfxBlast, 127, 0);
+ break;
+ }
+ }
+
+ for (int i = 0; i < kMaxEnemyShotSlots; i++) {
+ EnemyShotSlot &slot = _enemyShotSlots[i];
+ if (slot.timer == 0)
+ continue;
+
+ const int progress = 0x0F - slot.timer;
+ const int drawX = slot.startX + ((slot.targetX - slot.startX) * progress) / 10;
+ const int drawY = slot.startY + ((slot.targetY - slot.startY) * progress) / 10;
+
+ if (_enemyLaserBank.numSprites > 0) {
+ const int baseSprite = (drawY < 0x50) ? 0x12 : ((drawY < 0xA0) ? 9 : 0);
+ const int spriteIdx = baseSprite + slot.direction;
+ if (spriteIdx >= 0 && spriteIdx < _enemyLaserBank.numSprites) {
+ renderSpriteWithFlags(dst, pitch, width, height, drawX, drawY,
+ _enemyLaserBank.sprites[spriteIdx], slot.flags | 0x3);
+ }
+ }
+
+ slot.timer--;
+ if (drawX - 0x0F < playerX && playerX < drawX + 0x0F &&
+ drawY - 7 < playerY && playerY < drawY + 7) {
+ _damageFlags |= 0x80;
+ slot.timer = 0;
+ }
+ }
+}
+
// renderLaserShots â FUN_1CDA7/FUN_1D79C/HandleGameOp1A shot visual path.
void InsaneRebel1::renderLaserShots(byte *dst, int pitch, int width, int height) {
const char kRA1TorpedoTrailLeft[] = "<<&";
diff --git a/engines/scumm/insane/rebel1/runlevels.cpp b/engines/scumm/insane/rebel1/runlevels.cpp
index d6ce21e3f13..bf826e05f9a 100644
--- a/engines/scumm/insane/rebel1/runlevels.cpp
+++ b/engines/scumm/insane/rebel1/runlevels.cpp
@@ -1471,6 +1471,7 @@ bool InsaneRebel1::runLevel13() {
_currentLevel = 12;
loadLevelSprites(13);
+ loadRA1Nut("LVL13/L13LASR2.NUT", _enemyLaserBank);
loadTuningForLevel(12);
beginLevelTitleOverlay(12);
@@ -1514,6 +1515,7 @@ bool InsaneRebel1::runLevel13() {
memset(_viewHistoryY, 0, sizeof(_viewHistoryY));
_avgInputX = 0;
_avgInputY = 0;
+ resetEnemyShotSlots();
playInteractiveVideo("LVL13/L13PLAY.ANM");
if (_vm->shouldQuit())
Commit: dfeb60086737f9b54068429fa39f62a7bbb5d6ae
https://github.com/scummvm/scummvm/commit/dfeb60086737f9b54068429fa39f62a7bbb5d6ae
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:10:03+02:00
Commit Message:
SCUMM: RA1: Implement level 12
Changed paths:
engines/scumm/insane/rebel1/iact.cpp
engines/scumm/insane/rebel1/runlevels.cpp
diff --git a/engines/scumm/insane/rebel1/iact.cpp b/engines/scumm/insane/rebel1/iact.cpp
index 900f59e558f..956abfb3a1b 100644
--- a/engines/scumm/insane/rebel1/iact.cpp
+++ b/engines/scumm/insane/rebel1/iact.cpp
@@ -296,6 +296,25 @@ inline bool isLevel10DamageLatch(uint16 code) {
return true;
}
+inline bool isLevel12DamageLatch(uint16 code) {
+ // RunLevel12Flow treats these 0x5D latch values as safe; every other value
+ // raises damage flag 0x40. This helper is intentionally the inverse of the
+ // original branch shape so the update path reads as "is damage".
+ switch (code) {
+ case 0x0000:
+ case 0x0037:
+ case 0x003E:
+ case 0x0059:
+ case 0x00AC:
+ case 0x00C3:
+ case 0x00C5:
+ case 0x00C7:
+ return false;
+ default:
+ return true;
+ }
+}
+
inline bool hasLevel6PerspectiveHazard(uint16 frame, int16 perspectiveX, int16 perspectiveY) {
switch (frame) {
case 0x006A:
@@ -1267,10 +1286,11 @@ void InsaneRebel1::updateGameOp0BPhysics() {
(_currentLevel == 3 && isLevel4DamageLatch(_gameLatch5D)) ||
(_currentLevel == 5 && isLevel6DamageLatch(_gameLatch5D)) ||
(_currentLevel == 9 && isLevel10DamageLatch(_gameLatch5D)) ||
+ (_currentLevel == 11 && isLevel12DamageLatch(_gameLatch5D)) ||
(level15FinalPhase && isLevel15FinalDamageLatch(_gameLatch5D)))
_damageFlags |= 0x40;
if (_gameLatch5F != 0 &&
- ((_currentLevel == 3 || _currentLevel == 9 || level15FinalPhase)
+ ((_currentLevel == 3 || _currentLevel == 9 || _currentLevel == 11 || level15FinalPhase)
? (_vm->_rnd.getRandomNumber(2) == 0)
: (_vm->_rnd.getRandomNumber((uint16)(_gameLatch5F - 1)) == 0)))
_damageFlags |= 0x80;
@@ -1450,6 +1470,29 @@ void InsaneRebel1::updateGameOp0BPhysics() {
_frameCounter++;
+ if (_currentLevel == 11) {
+ // RunLevel12Flow checks DAT_761A bits 0x20, 0x08, and 0x02 at frame
+ // 0x550. These map to destroyed object IDs 211, 213, and 215 in the
+ // port's one-based frame-object helper. On failure the original keeps
+ // pumping L12PLAY until frame 0x564, plays L12RETRY, then restarts
+ // L12PLAY without resetting health.
+ if (_levelGameplayPhase == 0 && _frameCounter == 0x550) {
+ const bool target211Destroyed = isFrameObjectPrimarySet(211);
+ const bool target213Destroyed = isFrameObjectPrimarySet(213);
+ const bool target215Destroyed = isFrameObjectPrimarySet(215);
+ if (!target211Destroyed || !target213Destroyed || !target215Destroyed) {
+ _levelGameplayPhase = 1;
+ debug(1, "RA1 L12 retry armed: frame=0x%04x targets=(%d,%d,%d)",
+ _frameCounter,
+ target211Destroyed ? 1 : 0,
+ target213Destroyed ? 1 : 0,
+ target215Destroyed ? 1 : 0);
+ }
+ }
+ if (_levelGameplayPhase == 1 && _frameCounter >= 0x564)
+ _vm->_smushVideoShouldFinish = true;
+ }
+
// Level 4 Phase 2: enable torpedo mode at frame 0x3E and finish as
// soon as the torpedo registers a hit. The DOS loop exits on killCount.
if (_currentLevel == 3 && _levelGameplayPhase == 2) {
diff --git a/engines/scumm/insane/rebel1/runlevels.cpp b/engines/scumm/insane/rebel1/runlevels.cpp
index bf826e05f9a..b652de4a118 100644
--- a/engines/scumm/insane/rebel1/runlevels.cpp
+++ b/engines/scumm/insane/rebel1/runlevels.cpp
@@ -1411,38 +1411,52 @@ bool InsaneRebel1::runLevel12() {
_screenFlash = 0;
_screenShakeEnabled = false;
_deathCauseIndicator = 0;
- _frameCounter = 0;
- _gameCounter = 0;
- _activeGameOpcode = 0;
- _gameLatch5D = 0;
- _gameLatch5F = 0;
- resetGameplayFlagsFromTuning();
_killCount = 0;
- _targetCount = 0;
- _prevTargetCount = 0;
- _lastHitTarget = 0;
- _shipPosX = kRA1CenterX;
- _shipPosY = kRA1CenterY;
- _shipDirIndex = 17;
- _rollAccum = 0;
- _liftSmooth = 0;
- _posAccumX = 0;
- _posAccumY = 0;
- _perspectiveX = 0;
- _perspectiveY = 0;
- _levelGameplayPhase = 0;
- memset(_inputHistoryX, 0, sizeof(_inputHistoryX));
- memset(_inputHistoryY, 0, sizeof(_inputHistoryY));
- memset(_viewHistoryX, 0, sizeof(_viewHistoryX));
- memset(_viewHistoryY, 0, sizeof(_viewHistoryY));
- _avgInputX = 0;
- _avgInputY = 0;
- playInteractiveVideo("LVL12/L12PLAY.ANM");
- if (_vm->shouldQuit())
- return false;
+ while (!_vm->shouldQuit()) {
+ _frameCounter = 0;
+ _gameCounter = 0;
+ _activeGameOpcode = 0;
+ _gameLatch5D = 0;
+ _gameLatch5F = 0;
+ _damageFlags = 0;
+ resetGameplayFlagsFromTuning();
+ _targetCount = 0;
+ _prevTargetCount = 0;
+ _lastHitTarget = 0;
+ _shipPosX = kRA1CenterX;
+ _shipPosY = kRA1CenterY;
+ _shipDirIndex = 17;
+ _rollAccum = 0;
+ _liftSmooth = 0;
+ _posAccumX = 0;
+ _posAccumY = 0;
+ _perspectiveX = 0;
+ _perspectiveY = 0;
+ _levelGameplayPhase = 0;
+ memset(_inputHistoryX, 0, sizeof(_inputHistoryX));
+ memset(_inputHistoryY, 0, sizeof(_inputHistoryY));
+ memset(_viewHistoryX, 0, sizeof(_viewHistoryX));
+ memset(_viewHistoryY, 0, sizeof(_viewHistoryY));
+ _avgInputX = 0;
+ _avgInputY = 0;
+
+ playInteractiveVideo("LVL12/L12PLAY.ANM");
+ if (_vm->shouldQuit())
+ return false;
+
+ if (_levelGameplayPhase == 1) {
+ playCinematic("LVL12/L12RETRY.ANM");
+ if (_vm->shouldQuit())
+ return false;
+ if (_health < 0)
+ break;
+ continue;
+ }
+
+ if (_health < 0)
+ break;
- if (_health >= 0) {
playCinematic("LVL12/L12END.ANM");
_maxChapterUnlocked = MAX(_maxChapterUnlocked, (int16)12);
return !_vm->shouldQuit();
Commit: c079896afde4b2887c3e14cb8d985e7ed800e0bd
https://github.com/scummvm/scummvm/commit/c079896afde4b2887c3e14cb8d985e7ed800e0bd
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:10:04+02:00
Commit Message:
SCUMM: RA1: Implement level 14
Changed paths:
engines/scumm/insane/rebel1/iact.cpp
engines/scumm/insane/rebel1/rebel.cpp
engines/scumm/insane/rebel1/rebel.h
engines/scumm/insane/rebel1/runlevels.cpp
diff --git a/engines/scumm/insane/rebel1/iact.cpp b/engines/scumm/insane/rebel1/iact.cpp
index 956abfb3a1b..9b2f80536c4 100644
--- a/engines/scumm/insane/rebel1/iact.cpp
+++ b/engines/scumm/insane/rebel1/iact.cpp
@@ -315,6 +315,18 @@ inline bool isLevel12DamageLatch(uint16 code) {
}
}
+inline bool isLevel14Phase2DamageLatch(uint16 code) {
+ switch (code) {
+ case 0x0008:
+ case 0x000E:
+ case 0x0010:
+ case 0x0012:
+ return true;
+ default:
+ return false;
+ }
+}
+
inline bool hasLevel6PerspectiveHazard(uint16 frame, int16 perspectiveX, int16 perspectiveY) {
switch (frame) {
case 0x006A:
@@ -473,6 +485,42 @@ bool InsaneRebel1::isFrameObjectPrimarySet(int16 objectId) const {
return (_frameObjectState[byteIndex] & bit) != 0;
}
+// Port helpers for RunLevel14Flow. The original checks DAT_7614..7616 and
+// DAT_7605..7606 inline, then increments a local 60-frame completion counter.
+bool InsaneRebel1::areLevel14Phase1TargetsDestroyed() const {
+ return isFrameObjectPrimarySet(168) &&
+ isFrameObjectPrimarySet(169) &&
+ isFrameObjectPrimarySet(170) &&
+ isFrameObjectPrimarySet(171) &&
+ isFrameObjectPrimarySet(172) &&
+ isFrameObjectPrimarySet(173) &&
+ isFrameObjectPrimarySet(174) &&
+ isFrameObjectPrimarySet(175) &&
+ isFrameObjectPrimarySet(176) &&
+ isFrameObjectPrimarySet(177) &&
+ isFrameObjectPrimarySet(178) &&
+ isFrameObjectPrimarySet(179) &&
+ isFrameObjectPrimarySet(180) &&
+ isFrameObjectPrimarySet(181) &&
+ isFrameObjectPrimarySet(182) &&
+ isFrameObjectPrimarySet(183);
+}
+
+bool InsaneRebel1::areLevel14Phase2TargetsDestroyed() const {
+ return isFrameObjectPrimarySet(45) &&
+ isFrameObjectPrimarySet(46) &&
+ isFrameObjectPrimarySet(47) &&
+ isFrameObjectPrimarySet(48) &&
+ isFrameObjectPrimarySet(49) &&
+ isFrameObjectPrimarySet(50) &&
+ isFrameObjectPrimarySet(51) &&
+ isFrameObjectPrimarySet(52) &&
+ isFrameObjectPrimarySet(53) &&
+ isFrameObjectPrimarySet(54) &&
+ isFrameObjectPrimarySet(55) &&
+ isFrameObjectPrimarySet(56);
+}
+
bool InsaneRebel1::handleFrameObjectTarget(int16 objectId, int16 left, int16 top, int16 width, int16 height,
int codec, uint8 &ra1Param) {
if (!_interactiveVideoActive)
@@ -1279,6 +1327,8 @@ void InsaneRebel1::updateGameOp0BPhysics() {
// 0x5D latch 0xFFFF -> bit 0x40 (scripted obstacle/contact)
// 0x5F non-zero + RNG -> bit 0x80 (scripted random hit)
const bool level15FinalPhase = (_currentLevel == 14 && _levelGameplayPhase == 2);
+ const bool level14Phase1 = (_currentLevel == 13 && _levelGameplayPhase == 1);
+ const bool level14Phase2 = (_currentLevel == 13 && _levelGameplayPhase == 2);
bool level2AsteroidHit = false;
if (_gameLatch5D == 0xFFFF ||
@@ -1287,13 +1337,21 @@ void InsaneRebel1::updateGameOp0BPhysics() {
(_currentLevel == 5 && isLevel6DamageLatch(_gameLatch5D)) ||
(_currentLevel == 9 && isLevel10DamageLatch(_gameLatch5D)) ||
(_currentLevel == 11 && isLevel12DamageLatch(_gameLatch5D)) ||
+ (level14Phase2 && isLevel14Phase2DamageLatch(_gameLatch5D)) ||
(level15FinalPhase && isLevel15FinalDamageLatch(_gameLatch5D)))
_damageFlags |= 0x40;
- if (_gameLatch5F != 0 &&
- ((_currentLevel == 3 || _currentLevel == 9 || _currentLevel == 11 || level15FinalPhase)
- ? (_vm->_rnd.getRandomNumber(2) == 0)
- : (_vm->_rnd.getRandomNumber((uint16)(_gameLatch5F - 1)) == 0)))
- _damageFlags |= 0x80;
+ if (_gameLatch5F != 0 && !level14Phase2) {
+ bool randomProjectileHit = false;
+ if (level14Phase1)
+ randomProjectileHit = (_vm->_rnd.getRandomNumber(3) == 0);
+ else if (_currentLevel == 3 || _currentLevel == 9 || _currentLevel == 11 || level15FinalPhase)
+ randomProjectileHit = (_vm->_rnd.getRandomNumber(2) == 0);
+ else
+ randomProjectileHit = (_vm->_rnd.getRandomNumber((uint16)(_gameLatch5F - 1)) == 0);
+
+ if (randomProjectileHit)
+ _damageFlags |= 0x80;
+ }
if (_currentLevel == 5 && hasLevel6PerspectiveHazard((uint16)_frameCounter, _perspectiveX, _perspectiveY))
_damageFlags |= 0x20;
@@ -1493,6 +1551,15 @@ void InsaneRebel1::updateGameOp0BPhysics() {
_vm->_smushVideoShouldFinish = true;
}
+ if (_currentLevel == 13 && (_levelGameplayPhase == 1 || _levelGameplayPhase == 2)) {
+ const bool targetsDestroyed = (_levelGameplayPhase == 1) ?
+ areLevel14Phase1TargetsDestroyed() : areLevel14Phase2TargetsDestroyed();
+ if (targetsDestroyed && _level14SuccessFrames < 0x3C)
+ _level14SuccessFrames++;
+ if (_level14SuccessFrames >= 0x3C)
+ _vm->_smushVideoShouldFinish = true;
+ }
+
// Level 4 Phase 2: enable torpedo mode at frame 0x3E and finish as
// soon as the torpedo registers a hit. The DOS loop exits on killCount.
if (_currentLevel == 3 && _levelGameplayPhase == 2) {
diff --git a/engines/scumm/insane/rebel1/rebel.cpp b/engines/scumm/insane/rebel1/rebel.cpp
index 0412b1a8a9b..ebb258dc15b 100644
--- a/engines/scumm/insane/rebel1/rebel.cpp
+++ b/engines/scumm/insane/rebel1/rebel.cpp
@@ -307,6 +307,7 @@ InsaneRebel1::InsaneRebel1(ScummEngine_v7 *scumm) : Insane(), _vm(scumm) {
_pendingRouteCutoverFrame = -1;
_levelGameplayPhase = 0;
_level5SuccessFramesRemaining = 0;
+ _level14SuccessFrames = 0;
_menuActive = false;
_introTextActive = false;
_introTextStartFrame = 0;
diff --git a/engines/scumm/insane/rebel1/rebel.h b/engines/scumm/insane/rebel1/rebel.h
index 1da9652da5a..2fb95157f02 100644
--- a/engines/scumm/insane/rebel1/rebel.h
+++ b/engines/scumm/insane/rebel1/rebel.h
@@ -175,6 +175,8 @@ private:
void updateGostSlotPosition(int16 targetIdx, int16 left, int16 top, int16 right, int16 bottom);
void applyFrameObjectHitState(int16 targetIdx);
bool isFrameObjectPrimarySet(int16 objectId) const;
+ bool areLevel14Phase1TargetsDestroyed() const;
+ bool areLevel14Phase2TargetsDestroyed() const;
// Shooting pipeline â FUN_1CCA0 (0x1CCA0) shot spawner,
// FUN_1C0EF (0x1C0EF) target detection, FUN_1C940 (0x1C940) shot processing
@@ -508,6 +510,7 @@ private:
int16 _shieldGenHitsA; // Hits on _protectedTargetA
int16 _shieldGenHitsB; // Hits on _protectedTargetB
int16 _level5SuccessFramesRemaining; // DOS RunLevel5Flow: 20-frame hold after the third kill
+ int16 _level14SuccessFrames; // RunLevel14Flow: 60-frame hold after required targets are destroyed
// Level 15 torpedo success latch. The original derives this from
// g_gameplayPhaseFlags bit 1, which is the primary object-state bit for object 7.
diff --git a/engines/scumm/insane/rebel1/runlevels.cpp b/engines/scumm/insane/rebel1/runlevels.cpp
index b652de4a118..46aa308e005 100644
--- a/engines/scumm/insane/rebel1/runlevels.cpp
+++ b/engines/scumm/insane/rebel1/runlevels.cpp
@@ -1600,7 +1600,8 @@ bool InsaneRebel1::runLevel14() {
_posAccumY = 0;
_perspectiveX = 0;
_perspectiveY = 0;
- _levelGameplayPhase = 0;
+ _levelGameplayPhase = 1;
+ _level14SuccessFrames = 0;
memset(_inputHistoryX, 0, sizeof(_inputHistoryX));
memset(_inputHistoryY, 0, sizeof(_inputHistoryY));
memset(_viewHistoryX, 0, sizeof(_viewHistoryX));
@@ -1620,6 +1621,8 @@ bool InsaneRebel1::runLevel14() {
_gameLatch5F = 0;
resetGameplayFlagsFromTuning();
_killCount = 0;
+ _levelGameplayPhase = 2;
+ _level14SuccessFrames = 0;
playInteractiveVideo("LVL14/L14PLAY2.ANM");
if (_vm->shouldQuit())
Commit: e94cfd3f51ec3546e9ca4f071f4f5bf342a9cf28
https://github.com/scummvm/scummvm/commit/e94cfd3f51ec3546e9ca4f071f4f5bf342a9cf28
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:10:04+02:00
Commit Message:
SCUMM: RA1: Implement level 11 hits overlay
Changed paths:
engines/scumm/insane/rebel1/iact.cpp
engines/scumm/insane/rebel1/rebel.h
engines/scumm/insane/rebel1/render.cpp
diff --git a/engines/scumm/insane/rebel1/iact.cpp b/engines/scumm/insane/rebel1/iact.cpp
index 9b2f80536c4..110fd12f947 100644
--- a/engines/scumm/insane/rebel1/iact.cpp
+++ b/engines/scumm/insane/rebel1/iact.cpp
@@ -1326,6 +1326,7 @@ void InsaneRebel1::updateGameOp0BPhysics() {
// RA1 FUN_1B297-style per-frame latches for 0x0B sections:
// 0x5D latch 0xFFFF -> bit 0x40 (scripted obstacle/contact)
// 0x5F non-zero + RNG -> bit 0x80 (scripted random hit)
+ const bool level15Phase1 = (_currentLevel == 14 && _levelGameplayPhase == 1);
const bool level15FinalPhase = (_currentLevel == 14 && _levelGameplayPhase == 2);
const bool level14Phase1 = (_currentLevel == 13 && _levelGameplayPhase == 1);
const bool level14Phase2 = (_currentLevel == 13 && _levelGameplayPhase == 2);
@@ -1342,9 +1343,12 @@ void InsaneRebel1::updateGameOp0BPhysics() {
_damageFlags |= 0x40;
if (_gameLatch5F != 0 && !level14Phase2) {
bool randomProjectileHit = false;
+ // Original level loops spell these fixed-probability cases separately.
+ // The shared 0x0B path collapses them into one branch.
if (level14Phase1)
randomProjectileHit = (_vm->_rnd.getRandomNumber(3) == 0);
- else if (_currentLevel == 3 || _currentLevel == 9 || _currentLevel == 11 || level15FinalPhase)
+ else if (_currentLevel == 3 || _currentLevel == 9 || _currentLevel == 11 ||
+ level15Phase1 || level15FinalPhase)
randomProjectileHit = (_vm->_rnd.getRandomNumber(2) == 0);
else
randomProjectileHit = (_vm->_rnd.getRandomNumber((uint16)(_gameLatch5F - 1)) == 0);
diff --git a/engines/scumm/insane/rebel1/rebel.h b/engines/scumm/insane/rebel1/rebel.h
index 2fb95157f02..4173a1da385 100644
--- a/engines/scumm/insane/rebel1/rebel.h
+++ b/engines/scumm/insane/rebel1/rebel.h
@@ -163,6 +163,7 @@ private:
void renderGostScorePopup(byte *dst, int pitch, int width, int height,
int16 centerX, int16 centerY, int16 frame);
void renderLaserShots(byte *dst, int pitch, int width, int height);
+ void renderLevel11HitsOverlay(byte *dst, int pitch, int width, int height);
void resetEnemyShotSlots();
void renderLevel13EnemyShots(byte *dst, int pitch, int width, int height);
void getTurretShipCenter(int16 &x, int16 &y) const;
diff --git a/engines/scumm/insane/rebel1/render.cpp b/engines/scumm/insane/rebel1/render.cpp
index 039e4c10f17..140751dab2c 100644
--- a/engines/scumm/insane/rebel1/render.cpp
+++ b/engines/scumm/insane/rebel1/render.cpp
@@ -642,6 +642,9 @@ void InsaneRebel1::procPostRendering(byte *renderBitmap, int32 codecparam, int32
renderExplosions(renderBitmap, pitch, width, height);
+ if (_currentLevel == 10)
+ renderLevel11HitsOverlay(renderBitmap, pitch, width, height);
+
// Level 8 (Imperial Walkers) â walker-specific state update + UI overlay.
// In the original, RunLevel8Flow runs the walker logic inline in the per-frame
// game loop. We call it from procPostRendering when _currentLevel == 7.
@@ -809,6 +812,18 @@ void InsaneRebel1::renderGostSlots(byte *dst, int pitch, int width, int height)
}
}
+// renderLevel11HitsOverlay â RunLevel11Flow (0x19F9F) calls FormatAndDrawText
+// each L11PLAY frame with "<<HITS %02d" at (0x119, 0x16). This helper is a
+// ScummVM-side extraction; the original keeps the draw call inline in the loop.
+void InsaneRebel1::renderLevel11HitsOverlay(byte *dst, int pitch, int width, int height) {
+ if (_hudFontBank.numSprites <= 0 && _techFontBank.numSprites <= 0)
+ return;
+
+ char hitsStr[16];
+ Common::sprintf_s(hitsStr, "<<HITS %02d", (int)_killCount);
+ drawFontBankString(dst, pitch, width, height, 0x119, 0x16, hitsStr);
+}
+
void InsaneRebel1::resetEnemyShotSlots() {
memset(_enemyShotSlots, 0, sizeof(_enemyShotSlots));
}
Commit: 2efc01e65543bfc4206ca5f30efed28f44b1bd8f
https://github.com/scummvm/scummvm/commit/2efc01e65543bfc4206ca5f30efed28f44b1bd8f
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:10:04+02:00
Commit Message:
SCUMM: RA1: Implement level 14 splice
Changed paths:
engines/scumm/insane/rebel1/rebel.cpp
engines/scumm/insane/rebel1/rebel.h
engines/scumm/insane/rebel1/render.cpp
engines/scumm/insane/rebel1/runlevels.cpp
diff --git a/engines/scumm/insane/rebel1/rebel.cpp b/engines/scumm/insane/rebel1/rebel.cpp
index ebb258dc15b..934de8c438e 100644
--- a/engines/scumm/insane/rebel1/rebel.cpp
+++ b/engines/scumm/insane/rebel1/rebel.cpp
@@ -306,6 +306,9 @@ InsaneRebel1::InsaneRebel1(ScummEngine_v7 *scumm) : Insane(), _vm(scumm) {
_pendingRouteStartFrame = 0;
_pendingRouteCutoverFrame = -1;
_levelGameplayPhase = 0;
+ _level14Play2BSplicePending = false;
+ _level14Play2BSpliced = false;
+ _level14Play2BSpliceFrame = 0;
_level5SuccessFramesRemaining = 0;
_level14SuccessFrames = 0;
_menuActive = false;
diff --git a/engines/scumm/insane/rebel1/rebel.h b/engines/scumm/insane/rebel1/rebel.h
index 4173a1da385..a9ced908ece 100644
--- a/engines/scumm/insane/rebel1/rebel.h
+++ b/engines/scumm/insane/rebel1/rebel.h
@@ -163,6 +163,7 @@ private:
void renderGostScorePopup(byte *dst, int pitch, int width, int height,
int16 centerX, int16 centerY, int16 frame);
void renderLaserShots(byte *dst, int pitch, int width, int height);
+ void handleLevel14Play2BSplice(int32 curFrame, int32 maxFrame);
void renderLevel11HitsOverlay(byte *dst, int pitch, int width, int height);
void resetEnemyShotSlots();
void renderLevel13EnemyShots(byte *dst, int pitch, int width, int height);
@@ -408,6 +409,10 @@ private:
int32 _pendingRouteStartFrame; // Resume frame for branch-driven route switches
int32 _pendingRouteCutoverFrame; // Delayed inline route splice frame (branchFrame + 7)
int _levelGameplayPhase; // Level-local interactive phase (e.g. LVL4 PLAY1 vs PLAY2)
+ // RunLevel14Flow queues L14PLY2B at L14PLAY2 maxFrame-0x0F.
+ bool _level14Play2BSplicePending;
+ bool _level14Play2BSpliced;
+ int32 _level14Play2BSpliceFrame;
// Main menu / options state
void runOptionsMenu();
diff --git a/engines/scumm/insane/rebel1/render.cpp b/engines/scumm/insane/rebel1/render.cpp
index 140751dab2c..622e92b3cf6 100644
--- a/engines/scumm/insane/rebel1/render.cpp
+++ b/engines/scumm/insane/rebel1/render.cpp
@@ -641,6 +641,7 @@ void InsaneRebel1::procPostRendering(byte *renderBitmap, int32 codecparam, int32
renderLevel13EnemyShots(renderBitmap, pitch, width, height);
renderExplosions(renderBitmap, pitch, width, height);
+ handleLevel14Play2BSplice(curFrame, maxFrame);
if (_currentLevel == 10)
renderLevel11HitsOverlay(renderBitmap, pitch, width, height);
@@ -741,6 +742,31 @@ void InsaneRebel1::renderTargeting(byte *dst, int pitch, int width, int height)
_lastHitTarget = 0;
}
+// handleLevel14Play2BSplice â RunLevel14Flow (0x1ACD1) queues L14PLY2B.ANM
+// from inside the L14PLAY2 loop when the current clip reaches maxFrame - 0x0F.
+// This helper is a ScummVM-side extraction; the original keeps the PlayAnmFile
+// call inline and passes the old L14PLAY2 timeline frame to the ANM frame gate.
+void InsaneRebel1::handleLevel14Play2BSplice(int32 curFrame, int32 maxFrame) {
+ if (_currentLevel != 13 || _levelGameplayPhase != 2 || _level14Play2BSpliced ||
+ _level14Play2BSplicePending || maxFrame < 0x0F)
+ return;
+
+ if (curFrame != maxFrame - 0x0F)
+ return;
+
+ _level14Play2BSpliced = true;
+ _level14Play2BSplicePending = true;
+ _level14Play2BSpliceFrame = curFrame;
+
+ // Original after PlayAnmFile("LVL14/L14PLY2B.ANM", 0x860, maxFrame-0x0F, 1, -1):
+ // g_extendedPhaseFlags &= 0xFA;
+ // DAT_7604 &= 0xBF;
+ // In the port's object-state storage those are the first and third primary bytes.
+ _frameObjectState[0] &= 0xFA;
+ _frameObjectState[2] &= 0xBF;
+ _vm->_smushVideoShouldFinish = true;
+}
+
// renderGostScorePopup â Per-kill score glyph from RenderGostOverlaySlots (0x1C9CD).
// Maps kill score to tech-font glyph and draws it rising upward from the kill position.
void InsaneRebel1::renderGostScorePopup(byte *dst, int pitch, int width, int height,
diff --git a/engines/scumm/insane/rebel1/runlevels.cpp b/engines/scumm/insane/rebel1/runlevels.cpp
index 46aa308e005..8f3ea478862 100644
--- a/engines/scumm/insane/rebel1/runlevels.cpp
+++ b/engines/scumm/insane/rebel1/runlevels.cpp
@@ -1558,7 +1558,8 @@ bool InsaneRebel1::runLevel13() {
// Level 14 flow (RunLevel14Flow, 0x1ACB0): Surface Cannon
// Two interactive phases: L14PLAY (targeting cannons) + L14PLAY2 (exhaust port approach).
-// Original: L14INTRO â L14PLAY â L14PLAY2 â L14END/L14NEW/L14DEATH
+// Original: L14INTRO â L14PLAY â L14PLAY2 â optional L14PLY2B splice
+// â L14END/L14NEW/L14DEATH
bool InsaneRebel1::runLevel14() {
debug(1, "InsaneRebel1: Running level 14");
@@ -1623,10 +1624,26 @@ bool InsaneRebel1::runLevel14() {
_killCount = 0;
_levelGameplayPhase = 2;
_level14SuccessFrames = 0;
+ _level14Play2BSplicePending = false;
+ _level14Play2BSpliced = false;
+ _level14Play2BSpliceFrame = 0;
playInteractiveVideo("LVL14/L14PLAY2.ANM");
if (_vm->shouldQuit())
return false;
+
+ if (_health >= 0 && _level14Play2BSplicePending) {
+ const int32 spliceFrame = _level14Play2BSpliceFrame;
+ _level14Play2BSplicePending = false;
+
+ // DOS queues L14PLY2B from the L14PLAY2 loop with startFrame
+ // equal to L14PLAY2's old timeline frame. L14PLY2B itself is the
+ // continuation clip, so the port starts it from frame 0 but uses
+ // the non-zero frame argument to preserve gameplay/video state.
+ playInteractiveVideo("LVL14/L14PLY2B.ANM", spliceFrame);
+ if (_vm->shouldQuit())
+ return false;
+ }
}
if (_health >= 0) {
@@ -1894,6 +1911,13 @@ void InsaneRebel1::playInteractiveVideo(const char *filename, int32 startFrame)
debug(1, "RA1 L8 resume: route=%d timelineFrame=%d -> localFrame=%d offset=0x%x",
_levelRouteIndex, (int)startFrame, (int)videoStartFrame, (unsigned)videoOffset);
}
+ } else if (_currentLevel == 13 && resumingRoute) {
+ // RunLevel14Flow calls PlayAnmFile("LVL14/L14PLY2B.ANM", 0x860,
+ // oldMaxFrame-0x0F, 1, -1). That frame number belongs to L14PLAY2's
+ // timeline; L14PLY2B is already the continuation clip and starts at its
+ // matching lead-in frame. Preserve the current state, but do not seek.
+ debug(1, "RA1 L14 splice: L14PLAY2 timelineFrame=%d -> L14PLY2B frame 0",
+ (int)startFrame);
}
// Center mouse, hide system cursor (we draw our own), lock mouse to window
Commit: 6f800ee60d3ffca852940061bb2f6e69ae5f50e4
https://github.com/scummvm/scummvm/commit/6f800ee60d3ffca852940061bb2f6e69ae5f50e4
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:10:05+02:00
Commit Message:
SCUMM: RA1: Implement level 15 summary and scoring
Changed paths:
engines/scumm/insane/rebel1/rebel.cpp
engines/scumm/insane/rebel1/rebel.h
engines/scumm/insane/rebel1/render.cpp
engines/scumm/insane/rebel1/runlevels.cpp
diff --git a/engines/scumm/insane/rebel1/rebel.cpp b/engines/scumm/insane/rebel1/rebel.cpp
index 934de8c438e..c831798568f 100644
--- a/engines/scumm/insane/rebel1/rebel.cpp
+++ b/engines/scumm/insane/rebel1/rebel.cpp
@@ -316,6 +316,8 @@ InsaneRebel1::InsaneRebel1(ScummEngine_v7 *scumm) : Insane(), _vm(scumm) {
_introTextStartFrame = 0;
_introTextEndFrame = 0;
_introTextLevel = 0;
+ _level15SummaryActive = false;
+ _level15SummaryTargetBonus = 0;
_menuConfirmed = false;
_menuSelection = 0;
_menuFrameCounter = 0;
diff --git a/engines/scumm/insane/rebel1/rebel.h b/engines/scumm/insane/rebel1/rebel.h
index a9ced908ece..b931a413d56 100644
--- a/engines/scumm/insane/rebel1/rebel.h
+++ b/engines/scumm/insane/rebel1/rebel.h
@@ -307,6 +307,10 @@ private:
int _introTextLevel; // index into kLevelTitles
void beginLevelTitleOverlay(int level);
void drawLevelTitleOverlay(byte *dst, int pitch, int width, int height, int32 curFrame, int32 maxFrame);
+ bool _level15SummaryActive;
+ int _level15SummaryTargetBonus;
+ void beginLevel15SummaryOverlay(int targetBonus);
+ void drawLevel15SummaryOverlay(byte *dst, int pitch, int width, int height, int32 curFrame, int32 maxFrame);
// Control mode (from GAME opcode 0x5E)
int16 _flyControlMode;
diff --git a/engines/scumm/insane/rebel1/render.cpp b/engines/scumm/insane/rebel1/render.cpp
index 622e92b3cf6..fd2400b0f39 100644
--- a/engines/scumm/insane/rebel1/render.cpp
+++ b/engines/scumm/insane/rebel1/render.cpp
@@ -187,6 +187,30 @@ const RA1SpriteBank &selectLayerBank(const RA1SpriteBank &titleBank,
return (titleBank.numSprites > 0) ? titleBank : hudBank;
}
+void drawCenteredRebel1String(InsaneRebel1 *rebel1, byte *dst, int pitch, int width, int height,
+ int centerX, int y, const char *text, int maxChars = -1) {
+ if (!rebel1 || !text)
+ return;
+
+ const char *drawText = text;
+ char clipped[96];
+ if (maxChars >= 0) {
+ int len = 0;
+ while (text[len] != '\0' && len < maxChars && len < (int)sizeof(clipped) - 1) {
+ clipped[len] = text[len];
+ len++;
+ }
+ clipped[len] = '\0';
+ drawText = clipped;
+ }
+
+ if (drawText[0] == '\0')
+ return;
+
+ const int textW = rebel1->getFontBankStringWidth(drawText);
+ rebel1->drawFontBankString(dst, pitch, width, height, centerX - textW / 2, y, drawText);
+}
+
int getBankSpaceHeight(const RA1SpriteBank &bank) {
// In FUN_221B7 line advance is derived from the layer's space-glyph height (+4).
// With current NUT decoding we approximate that using the '!' glyph (index 0).
@@ -487,6 +511,14 @@ void InsaneRebel1::procPostRendering(byte *renderBitmap, int32 codecparam, int32
drawLevelTitleOverlay(renderBitmap, w, w, h, curFrame, maxFrame);
}
+ if (_level15SummaryActive && renderBitmap) {
+ int w = _player ? _player->_width : _screenWidth;
+ int h = _player ? _player->_height : _screenHeight;
+ if (w == 0) w = _screenWidth;
+ if (h == 0) h = _screenHeight;
+ drawLevel15SummaryOverlay(renderBitmap, w, w, h, curFrame, maxFrame);
+ }
+
if (!_interactiveVideoActive || !renderBitmap)
return;
@@ -1217,6 +1249,53 @@ void InsaneRebel1::drawLevelTitleOverlay(byte *dst, int pitch, int width, int he
drawFontBankString(dst, pitch, width, height, centerX - subtitleW / 2, 25, subtitle);
}
+void InsaneRebel1::beginLevel15SummaryOverlay(int targetBonus) {
+ _level15SummaryActive = true;
+ _level15SummaryTargetBonus = targetBonus;
+}
+
+// drawLevel15SummaryOverlay â RunChapterCompleteSummaryScreen (0x15E42), used by
+// RunLevel1GameLoop's L15END1 path. This is a ScummVM-side extraction of the
+// original summary draw loop; the score update is done by runLevel15().
+void InsaneRebel1::drawLevel15SummaryOverlay(byte *dst, int pitch, int width, int height,
+ int32 curFrame, int32 maxFrame) {
+ if (!_level15SummaryActive || !dst || maxFrame <= 0)
+ return;
+
+ const int32 revealBaseFrame = maxFrame - 0x122;
+ const int32 stopFrame = maxFrame - 0xA5;
+ if (curFrame < revealBaseFrame || curFrame >= stopFrame)
+ return;
+
+ const int centerX = width / 2;
+ const int titleChars = MAX<int>(0, (int)(curFrame - revealBaseFrame));
+ drawCenteredRebel1String(this, dst, pitch, width, height,
+ centerX, 5, "Chapter Complete", titleChars);
+
+ if (revealBaseFrame + 0x0F < curFrame) {
+ char completionText[40];
+ Common::sprintf_s(completionText, "Completion bonus: %d", (int)_tuning.levelPts);
+ drawCenteredRebel1String(this, dst, pitch, width, height, centerX, 0x19, completionText);
+ }
+
+ if (revealBaseFrame + 0x28 < curFrame && curFrame < revealBaseFrame + 0x46) {
+ char accuracyText[48];
+ char bonusText[32];
+ Common::sprintf_s(accuracyText, "Target Accuracy: %d percent",
+ ((int)_killCount * 100) / 0x58);
+ Common::sprintf_s(bonusText, "Bonus: %d", _level15SummaryTargetBonus);
+ drawCenteredRebel1String(this, dst, pitch, width, height, centerX, 0x32, "Part I");
+ drawCenteredRebel1String(this, dst, pitch, width, height, centerX, 0x46, accuracyText);
+ drawCenteredRebel1String(this, dst, pitch, width, height, centerX, 0x5A, bonusText);
+ }
+
+ if (revealBaseFrame + 0x55 < curFrame && curFrame < revealBaseFrame + 0x73) {
+ drawCenteredRebel1String(this, dst, pitch, width, height, centerX, 0x32, "Part II");
+ drawCenteredRebel1String(this, dst, pitch, width, height, centerX, 0x46, "Torpedo on mark");
+ drawCenteredRebel1String(this, dst, pitch, width, height, centerX, 0x5A, "Bonus: 10000");
+ }
+}
+
// drawFontBankString â FUN_221B7 (0x221B7), partial parity:
// supports '<'/'>' layer markup and layer-2 space handling used by RA1 HUD/targeting strings.
void InsaneRebel1::drawFontBankString(byte *dst, int pitch, int width, int height, int x, int y, const char *text) {
diff --git a/engines/scumm/insane/rebel1/runlevels.cpp b/engines/scumm/insane/rebel1/runlevels.cpp
index 8f3ea478862..c3480e719d7 100644
--- a/engines/scumm/insane/rebel1/runlevels.cpp
+++ b/engines/scumm/insane/rebel1/runlevels.cpp
@@ -1754,7 +1754,16 @@ bool InsaneRebel1::runLevel15() {
}
if (_health >= 0) {
+ int targetBonus = 0;
+ if (_killCount > 0x57)
+ targetBonus = 1000;
+ if (_killCount > 0x14)
+ targetBonus += (_killCount - 0x14) * _tuning.bonus;
+
+ beginLevel15SummaryOverlay(targetBonus);
playCinematic("LVL15/L15END1.ANM");
+ _level15SummaryActive = false;
+ _score += _tuning.levelPts + targetBonus + 10000;
_maxChapterUnlocked = MAX(_maxChapterUnlocked, (int16)15);
return !_vm->shouldQuit();
}
Commit: 988fc5be0addabf8a3ad4349a926d846a8747238
https://github.com/scummvm/scummvm/commit/988fc5be0addabf8a3ad4349a926d846a8747238
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:10:05+02:00
Commit Message:
SCUMM: RA1: Implement generic level summary and scoring
Changed paths:
engines/scumm/insane/rebel1/iact.cpp
engines/scumm/insane/rebel1/rebel.cpp
engines/scumm/insane/rebel1/rebel.h
engines/scumm/insane/rebel1/render.cpp
engines/scumm/insane/rebel1/runlevels.cpp
diff --git a/engines/scumm/insane/rebel1/iact.cpp b/engines/scumm/insane/rebel1/iact.cpp
index 110fd12f947..f4e779b0b2d 100644
--- a/engines/scumm/insane/rebel1/iact.cpp
+++ b/engines/scumm/insane/rebel1/iact.cpp
@@ -1056,7 +1056,7 @@ void InsaneRebel1::updateShipPhysics() {
// --- Path branching detection ---
// Original (FUN_1B297): at GAME counter 394 (0x18A), sets nextSceneA=0x67/nextSceneB=0x69.
// After this point, drift goes strongly negative (pushing ship left for the hard path).
- // If ship is right of center, player chose the right/easy path â switch to L1PLAY1R.
+ // If ship is right of center, player chose the hard branch â switch to L1PLAY1R.
// Keep this as a one-shot decision: once threshold is reached, lock path.
if (_pathBranchEnabled && _gameCounter >= kPathBranchCounter) {
if (_shipPosX > kRA1CenterX) {
diff --git a/engines/scumm/insane/rebel1/rebel.cpp b/engines/scumm/insane/rebel1/rebel.cpp
index c831798568f..bec456bd16e 100644
--- a/engines/scumm/insane/rebel1/rebel.cpp
+++ b/engines/scumm/insane/rebel1/rebel.cpp
@@ -316,8 +316,7 @@ InsaneRebel1::InsaneRebel1(ScummEngine_v7 *scumm) : Insane(), _vm(scumm) {
_introTextStartFrame = 0;
_introTextEndFrame = 0;
_introTextLevel = 0;
- _level15SummaryActive = false;
- _level15SummaryTargetBonus = 0;
+ memset(&_chapterSummary, 0, sizeof(_chapterSummary));
_menuConfirmed = false;
_menuSelection = 0;
_menuFrameCounter = 0;
diff --git a/engines/scumm/insane/rebel1/rebel.h b/engines/scumm/insane/rebel1/rebel.h
index b931a413d56..77855815881 100644
--- a/engines/scumm/insane/rebel1/rebel.h
+++ b/engines/scumm/insane/rebel1/rebel.h
@@ -140,6 +140,11 @@ private:
// Play a passive cinematic (no game callback, skippable)
// startFrame > 0: fast-forward (decode without display) to that frame
void playCinematic(const char *filename, int32 startFrame = 0);
+ void playChapterCompleteCinematic(const char *filename, int16 unlockedChapter,
+ int revealOffsetFromEnd, int stopOffsetFromEnd,
+ const char *bonusLabel1 = nullptr, const char *detailText1 = nullptr, int bonusValue1 = 0,
+ const char *bonusLabel2 = nullptr, const char *detailText2 = nullptr, int bonusValue2 = 0,
+ int passwordIndex = 0);
// Play interactive gameplay video (with ship physics + HUD)
void playInteractiveVideo(const char *filename, int32 startFrame = 0);
@@ -307,10 +312,28 @@ private:
int _introTextLevel; // index into kLevelTitles
void beginLevelTitleOverlay(int level);
void drawLevelTitleOverlay(byte *dst, int pitch, int width, int height, int32 curFrame, int32 maxFrame);
- bool _level15SummaryActive;
- int _level15SummaryTargetBonus;
- void beginLevel15SummaryOverlay(int targetBonus);
- void drawLevel15SummaryOverlay(byte *dst, int pitch, int width, int height, int32 curFrame, int32 maxFrame);
+
+ static const int kChapterSummaryTextSize = 80;
+ struct ChapterSummaryState {
+ bool active;
+ int revealOffsetFromEnd;
+ int stopOffsetFromEnd;
+ bool hasBonus1;
+ bool hasBonus2;
+ char bonusLabel1[kChapterSummaryTextSize];
+ char detailText1[kChapterSummaryTextSize];
+ int bonusValue1;
+ char bonusLabel2[kChapterSummaryTextSize];
+ char detailText2[kChapterSummaryTextSize];
+ int bonusValue2;
+ int passwordIndex;
+ };
+ ChapterSummaryState _chapterSummary;
+ void beginChapterSummaryOverlay(int revealOffsetFromEnd, int stopOffsetFromEnd,
+ const char *bonusLabel1, const char *detailText1, int bonusValue1,
+ const char *bonusLabel2, const char *detailText2, int bonusValue2,
+ int passwordIndex);
+ void drawChapterSummaryOverlay(byte *dst, int pitch, int width, int height, int32 curFrame, int32 maxFrame);
// Control mode (from GAME opcode 0x5E)
int16 _flyControlMode;
@@ -407,7 +430,7 @@ private:
static const int32 kPathBranchCounter = 394; // GAME 0x07 field1 value
int32 _gameCounter; // GAME 0x07 field1 â the original's _DAT_7740
bool _pathBranchEnabled; // True when branching is active for this video
- bool _rightPathSelected; // True if player chose the right/easy path
+ bool _rightPathSelected; // True if player branched into L1PLAY1R
int _levelRouteIndex; // Current mid-level route/segment for branching levels
int _pendingRouteIndex; // Next route requested by original frame-branch logic
int32 _pendingRouteStartFrame; // Resume frame for branch-driven route switches
diff --git a/engines/scumm/insane/rebel1/render.cpp b/engines/scumm/insane/rebel1/render.cpp
index fd2400b0f39..9cb9c810bfb 100644
--- a/engines/scumm/insane/rebel1/render.cpp
+++ b/engines/scumm/insane/rebel1/render.cpp
@@ -511,12 +511,12 @@ void InsaneRebel1::procPostRendering(byte *renderBitmap, int32 codecparam, int32
drawLevelTitleOverlay(renderBitmap, w, w, h, curFrame, maxFrame);
}
- if (_level15SummaryActive && renderBitmap) {
+ if (_chapterSummary.active && renderBitmap) {
int w = _player ? _player->_width : _screenWidth;
int h = _player ? _player->_height : _screenHeight;
if (w == 0) w = _screenWidth;
if (h == 0) h = _screenHeight;
- drawLevel15SummaryOverlay(renderBitmap, w, w, h, curFrame, maxFrame);
+ drawChapterSummaryOverlay(renderBitmap, w, w, h, curFrame, maxFrame);
}
if (!_interactiveVideoActive || !renderBitmap)
@@ -1249,21 +1249,48 @@ void InsaneRebel1::drawLevelTitleOverlay(byte *dst, int pitch, int width, int he
drawFontBankString(dst, pitch, width, height, centerX - subtitleW / 2, 25, subtitle);
}
-void InsaneRebel1::beginLevel15SummaryOverlay(int targetBonus) {
- _level15SummaryActive = true;
- _level15SummaryTargetBonus = targetBonus;
+void InsaneRebel1::beginChapterSummaryOverlay(int revealOffsetFromEnd, int stopOffsetFromEnd,
+ const char *bonusLabel1, const char *detailText1, int bonusValue1,
+ const char *bonusLabel2, const char *detailText2, int bonusValue2,
+ int passwordIndex) {
+ memset(&_chapterSummary, 0, sizeof(_chapterSummary));
+ _chapterSummary.active = true;
+ _chapterSummary.revealOffsetFromEnd = revealOffsetFromEnd;
+ _chapterSummary.stopOffsetFromEnd = stopOffsetFromEnd;
+ _chapterSummary.bonusValue1 = bonusValue1;
+ _chapterSummary.bonusValue2 = bonusValue2;
+ _chapterSummary.passwordIndex = passwordIndex;
+
+ if (bonusLabel1 && detailText1) {
+ _chapterSummary.hasBonus1 = true;
+ Common::strlcpy(_chapterSummary.bonusLabel1, bonusLabel1, sizeof(_chapterSummary.bonusLabel1));
+ Common::strlcpy(_chapterSummary.detailText1, detailText1, sizeof(_chapterSummary.detailText1));
+ }
+ if (bonusLabel2 && detailText2) {
+ _chapterSummary.hasBonus2 = true;
+ Common::strlcpy(_chapterSummary.bonusLabel2, bonusLabel2, sizeof(_chapterSummary.bonusLabel2));
+ Common::strlcpy(_chapterSummary.detailText2, detailText2, sizeof(_chapterSummary.detailText2));
+ }
}
-// drawLevel15SummaryOverlay â RunChapterCompleteSummaryScreen (0x15E42), used by
-// RunLevel1GameLoop's L15END1 path. This is a ScummVM-side extraction of the
-// original summary draw loop; the score update is done by runLevel15().
-void InsaneRebel1::drawLevel15SummaryOverlay(byte *dst, int pitch, int width, int height,
+// The DOS table is stored as 15 XOR-0xAA encoded, 20-byte passcode slots at
+// DS:0x00A4. RunChapterCompleteSummaryScreen indexes it with passwordIndex-1.
+static const char *const kChapterCompletePasswords[] = {
+ "FALCON", "BIGGS", "ACKBAR", "ANOAT", "KAIBURR",
+ "FORNAX", "YUZZEM", "MYNOCK", "BESPIN", "BRIGIA",
+ "DAGOBAH", "KESSEL", "GREEDO", "MIMBAN", "ORGANA"
+};
+
+// drawChapterSummaryOverlay â RunChapterCompleteSummaryScreen (0x15E42), shared by
+// the RA1 runlevel flows that call it. This helper is a ScummVM-side extraction;
+// the original pumps frontend frames from the runlevel after queueing the END ANM.
+void InsaneRebel1::drawChapterSummaryOverlay(byte *dst, int pitch, int width, int height,
int32 curFrame, int32 maxFrame) {
- if (!_level15SummaryActive || !dst || maxFrame <= 0)
+ if (!_chapterSummary.active || !dst || maxFrame <= 0)
return;
- const int32 revealBaseFrame = maxFrame - 0x122;
- const int32 stopFrame = maxFrame - 0xA5;
+ const int32 revealBaseFrame = maxFrame - _chapterSummary.revealOffsetFromEnd;
+ const int32 stopFrame = maxFrame - _chapterSummary.stopOffsetFromEnd;
if (curFrame < revealBaseFrame || curFrame >= stopFrame)
return;
@@ -1278,21 +1305,31 @@ void InsaneRebel1::drawLevel15SummaryOverlay(byte *dst, int pitch, int width, in
drawCenteredRebel1String(this, dst, pitch, width, height, centerX, 0x19, completionText);
}
- if (revealBaseFrame + 0x28 < curFrame && curFrame < revealBaseFrame + 0x46) {
- char accuracyText[48];
+ if (_chapterSummary.hasBonus1 &&
+ revealBaseFrame + 0x28 < curFrame && curFrame < revealBaseFrame + 0x46) {
+ char bonusText[32];
+ Common::sprintf_s(bonusText, "Bonus: %d", _chapterSummary.bonusValue1);
+ drawCenteredRebel1String(this, dst, pitch, width, height, centerX, 0x32, _chapterSummary.bonusLabel1);
+ drawCenteredRebel1String(this, dst, pitch, width, height, centerX, 0x46, _chapterSummary.detailText1);
+ drawCenteredRebel1String(this, dst, pitch, width, height, centerX, 0x5A, bonusText);
+ }
+
+ if (_chapterSummary.hasBonus2 &&
+ revealBaseFrame + 0x55 < curFrame && curFrame < revealBaseFrame + 0x73) {
char bonusText[32];
- Common::sprintf_s(accuracyText, "Target Accuracy: %d percent",
- ((int)_killCount * 100) / 0x58);
- Common::sprintf_s(bonusText, "Bonus: %d", _level15SummaryTargetBonus);
- drawCenteredRebel1String(this, dst, pitch, width, height, centerX, 0x32, "Part I");
- drawCenteredRebel1String(this, dst, pitch, width, height, centerX, 0x46, accuracyText);
+ Common::sprintf_s(bonusText, "Bonus: %d", _chapterSummary.bonusValue2);
+ drawCenteredRebel1String(this, dst, pitch, width, height, centerX, 0x32, _chapterSummary.bonusLabel2);
+ drawCenteredRebel1String(this, dst, pitch, width, height, centerX, 0x46, _chapterSummary.detailText2);
drawCenteredRebel1String(this, dst, pitch, width, height, centerX, 0x5A, bonusText);
}
- if (revealBaseFrame + 0x55 < curFrame && curFrame < revealBaseFrame + 0x73) {
- drawCenteredRebel1String(this, dst, pitch, width, height, centerX, 0x32, "Part II");
- drawCenteredRebel1String(this, dst, pitch, width, height, centerX, 0x46, "Torpedo on mark");
- drawCenteredRebel1String(this, dst, pitch, width, height, centerX, 0x5A, "Bonus: 10000");
+ if (_chapterSummary.passwordIndex > 0 &&
+ _chapterSummary.passwordIndex <= ARRAYSIZE(kChapterCompletePasswords) &&
+ revealBaseFrame + 10 < curFrame) {
+ char passwordText[40];
+ Common::sprintf_s(passwordText, "Password: %s",
+ kChapterCompletePasswords[_chapterSummary.passwordIndex - 1]);
+ drawCenteredRebel1String(this, dst, pitch, width, height, centerX, 0x73, passwordText);
}
}
diff --git a/engines/scumm/insane/rebel1/runlevels.cpp b/engines/scumm/insane/rebel1/runlevels.cpp
index c3480e719d7..f42e283fff3 100644
--- a/engines/scumm/insane/rebel1/runlevels.cpp
+++ b/engines/scumm/insane/rebel1/runlevels.cpp
@@ -208,6 +208,22 @@ int32 findAnimFrameChunkOffsetByGameCounter(ScummEngine_v7 *vm, const char *file
return result;
}
+static void formatTargetAccuracy(char *dst, size_t dstSize, int kills, int targetCount, bool perfectText) {
+ if (perfectText && kills >= targetCount)
+ Common::sprintf_s(dst, dstSize, "Target Accuracy: Perfect");
+ else
+ Common::sprintf_s(dst, dstSize, "Target Accuracy: %d percent", (kills * 100) / targetCount);
+}
+
+static int calculateThresholdBonus(int kills, int perfectThreshold, int perKillThreshold, int perKillBonus) {
+ int bonus = 0;
+ if (kills > perfectThreshold)
+ bonus += 1000;
+ if (kills > perKillThreshold)
+ bonus += (kills - perKillThreshold) * perKillBonus;
+ return bonus;
+}
+
// ---------------------------------------------------------------------------
// Game flow (matching original at 0x15597)
// ---------------------------------------------------------------------------
@@ -232,6 +248,23 @@ void InsaneRebel1::playCinematic(const char *filename, int32 startFrame) {
_introTextActive = false;
}
+// The original runlevel flows repeat this pattern around RunChapterCompleteSummaryScreen:
+// queue the END ANM, draw the summary while the frontend pumps, then award points/unlock.
+void InsaneRebel1::playChapterCompleteCinematic(const char *filename, int16 unlockedChapter,
+ int revealOffsetFromEnd, int stopOffsetFromEnd,
+ const char *bonusLabel1, const char *detailText1, int bonusValue1,
+ const char *bonusLabel2, const char *detailText2, int bonusValue2,
+ int passwordIndex) {
+ beginChapterSummaryOverlay(revealOffsetFromEnd, stopOffsetFromEnd,
+ bonusLabel1, detailText1, bonusValue1,
+ bonusLabel2, detailText2, bonusValue2,
+ passwordIndex);
+ playCinematic(filename);
+ _chapterSummary.active = false;
+ _score += _tuning.levelPts + bonusValue1 + bonusValue2;
+ _maxChapterUnlocked = MAX(_maxChapterUnlocked, unlockedChapter);
+}
+
void InsaneRebel1::clearVideoBuffer() {
if (_vm->_screenWidth <= 0 || _vm->_screenHeight <= 0)
return;
@@ -277,7 +310,7 @@ void InsaneRebel1::playIntroSequence() {
// 2. L1HANGAR.ANM â Full hangar departure cutscene (782 frames, flags 0x0420)
// 3. L1CU1.ANM â Pre-flight cutscene (flags 0x0400)
// 4. L1PLAY1L.ANM â Stage 1 flight, hard/left path (788 frames)
-// At frame 394, if player steers right â L1PLAY1R (easy path, 396 frames)
+// At frame 394, if player steers right â L1PLAY1R (hard path, 396 frames)
// 5. L1CU2.ANM â Mid-level cutscene
// 6. L1PLAY2.ANM â Stage 2 turret
// If score < 5 (0x75D0): L1RETRY â retry Stage 2
@@ -378,8 +411,14 @@ bool InsaneRebel1::runLevel1() {
break;
if (_killCount > 4) {
- playCinematic("LVL1/L1END.ANM");
- _maxChapterUnlocked = MAX(_maxChapterUnlocked, (int16)1);
+ const char *pathText = _rightPathSelected ? "Path Taken: Hard" : "Path Taken: Easy";
+ const int pathBonus = _rightPathSelected ? _tuning.bonus * 3 : 0;
+ char accuracyText[80];
+ formatTargetAccuracy(accuracyText, sizeof(accuracyText), _killCount, 0x0E, true);
+ const int targetBonus = calculateThresholdBonus(_killCount, 0x0D, 5, _tuning.bonus);
+ playChapterCompleteCinematic("LVL1/L1END.ANM", 1, 0x78, 5,
+ "Part I", pathText, pathBonus,
+ "Part II", accuracyText, targetBonus);
return !_vm->shouldQuit();
}
@@ -451,8 +490,10 @@ bool InsaneRebel1::runLevel2() {
return false;
if (_health >= 0) {
- playCinematic("LVL2/L2END.ANM");
- _maxChapterUnlocked = MAX(_maxChapterUnlocked, (int16)2);
+ char accuracyText[80];
+ formatTargetAccuracy(accuracyText, sizeof(accuracyText), _killCount, 0x18, true);
+ const int bonus = calculateThresholdBonus(_killCount, 0x17, 0x0C, _tuning.bonus);
+ playChapterCompleteCinematic("LVL2/L2END.ANM", 2, 0x69, 10, " ", accuracyText, bonus);
return !_vm->shouldQuit();
}
@@ -514,8 +555,8 @@ bool InsaneRebel1::runLevel3() {
return false;
if (_health >= 0) {
- playCinematic("LVL3/L3END.ANM");
- _maxChapterUnlocked = MAX(_maxChapterUnlocked, (int16)3);
+ playChapterCompleteCinematic("LVL3/L3END.ANM", 3, 0x69, 5,
+ nullptr, nullptr, 0, nullptr, nullptr, 0, 3);
return !_vm->shouldQuit();
}
@@ -597,8 +638,10 @@ bool InsaneRebel1::runLevel4() {
if (_health >= 0) {
// L4END1 = torpedo hit, L4END2 = torpedo missed
- playCinematic((_killCount != 0) ? "LVL4/L4END1.ANM" : "LVL4/L4END2.ANM");
- _maxChapterUnlocked = MAX(_maxChapterUnlocked, (int16)4);
+ const bool torpedoHit = (_killCount != 0);
+ playChapterCompleteCinematic(torpedoHit ? "LVL4/L4END1.ANM" : "LVL4/L4END2.ANM",
+ 4, 0x69, 5, " ", torpedoHit ? "Torpedo Hit" : "Torpedo Missed",
+ torpedoHit ? _tuning.bonus : 0);
return !_vm->shouldQuit();
}
@@ -717,8 +760,16 @@ bool InsaneRebel1::runLevel5() {
return false;
if (_health >= 0) {
- playCinematic("LVL5/L5END.ANM");
- _maxChapterUnlocked = MAX(_maxChapterUnlocked, (int16)5);
+ char accuracyText[80];
+ formatTargetAccuracy(accuracyText, sizeof(accuracyText), _killCount, 100, false);
+ int bonus = 0;
+ if (_killCount >= 0x1A)
+ bonus += (_killCount - 0x19) * _tuning.bonus;
+ if (_killCount >= 0x33)
+ bonus += (_killCount - 0x32) * _tuning.bonus;
+ if (_killCount >= 0x4C)
+ bonus += (_killCount - 0x4B) * _tuning.bonus;
+ playChapterCompleteCinematic("LVL5/L5END.ANM", 5, 0x69, 5, " ", accuracyText, bonus);
return !_vm->shouldQuit();
}
@@ -781,8 +832,11 @@ bool InsaneRebel1::runLevel6() {
return false;
if (_health >= 0) {
- playCinematic("LVL6/L6END.ANM");
- _maxChapterUnlocked = MAX(_maxChapterUnlocked, (int16)6);
+ char accuracyText[80];
+ formatTargetAccuracy(accuracyText, sizeof(accuracyText), _killCount, 0x27, true);
+ const int bonus = calculateThresholdBonus(_killCount, 0x26, 0x0C, _tuning.bonus);
+ playChapterCompleteCinematic("LVL6/L6END.ANM", 6, 0x4B, 5,
+ " ", accuracyText, bonus, nullptr, nullptr, 0, 6);
return !_vm->shouldQuit();
}
@@ -889,8 +943,10 @@ bool InsaneRebel1::runLevel7() {
_pendingRouteCutoverFrame = -1;
if (_health >= 0) {
- playCinematic("LVL7/L7END.ANM");
- _maxChapterUnlocked = MAX(_maxChapterUnlocked, (int16)7);
+ char accuracyText[80];
+ formatTargetAccuracy(accuracyText, sizeof(accuracyText), _killCount, 0x33, true);
+ const int bonus = calculateThresholdBonus(_killCount, 0x32, 0x14, _tuning.bonus);
+ playChapterCompleteCinematic("LVL7/L7END.ANM", 7, 0x69, 5, " ", accuracyText, bonus);
return !_vm->shouldQuit();
}
@@ -1007,8 +1063,7 @@ bool InsaneRebel1::runLevel8() {
_pendingRouteCutoverFrame = -1;
if (_health >= 0) {
- playCinematic("LVL8/L8END.ANM");
- _maxChapterUnlocked = MAX(_maxChapterUnlocked, (int16)8);
+ playChapterCompleteCinematic("LVL8/L8END.ANM", 8, 0x5F, 5);
return !_vm->shouldQuit();
}
@@ -1199,8 +1254,7 @@ bool InsaneRebel1::runLevel9() {
if (_health < 0)
break;
- playCinematic("LVL9/L9END.ANM");
- _maxChapterUnlocked = MAX(_maxChapterUnlocked, (int16)9);
+ playChapterCompleteCinematic("LVL9/L9END.ANM", 9, 0x69, 5);
return !_vm->shouldQuit();
}
@@ -1273,8 +1327,11 @@ bool InsaneRebel1::runLevel10() {
return false;
if (_health >= 0) {
- playCinematic("LVL10/L10END.ANM");
- _maxChapterUnlocked = MAX(_maxChapterUnlocked, (int16)10);
+ char accuracyText[80];
+ formatTargetAccuracy(accuracyText, sizeof(accuracyText), _killCount, 0x3E, true);
+ const int bonus = calculateThresholdBonus(_killCount, 0x3D, 0x32, _tuning.bonus);
+ playChapterCompleteCinematic("LVL10/L10END.ANM", 10, 0x4B, 5,
+ " ", accuracyText, bonus, nullptr, nullptr, 0, 10);
return !_vm->shouldQuit();
}
@@ -1366,8 +1423,10 @@ bool InsaneRebel1::runLevel11() {
}
if (_health >= 0) {
- playCinematic("LVL11/L11END.ANM");
- _maxChapterUnlocked = MAX(_maxChapterUnlocked, (int16)11);
+ char accuracyText[80];
+ formatTargetAccuracy(accuracyText, sizeof(accuracyText), _killCount, 0x60, true);
+ const int bonus = calculateThresholdBonus(_killCount, 0x5F, 0x0F, _tuning.bonus);
+ playChapterCompleteCinematic("LVL11/L11END.ANM", 11, 0x69, 5, " ", accuracyText, bonus);
return !_vm->shouldQuit();
}
@@ -1457,8 +1516,10 @@ bool InsaneRebel1::runLevel12() {
if (_health < 0)
break;
- playCinematic("LVL12/L12END.ANM");
- _maxChapterUnlocked = MAX(_maxChapterUnlocked, (int16)12);
+ char accuracyText[80];
+ formatTargetAccuracy(accuracyText, sizeof(accuracyText), _killCount, 0x58, true);
+ const int bonus = calculateThresholdBonus(_killCount, 0x57, 0x46, _tuning.bonus);
+ playChapterCompleteCinematic("LVL12/L12END.ANM", 12, 0x69, 5, " ", accuracyText, bonus);
return !_vm->shouldQuit();
}
@@ -1536,8 +1597,10 @@ bool InsaneRebel1::runLevel13() {
return false;
if (_health >= 0) {
- playCinematic("LVL13/L13END.ANM");
- _maxChapterUnlocked = MAX(_maxChapterUnlocked, (int16)13);
+ char accuracyText[80];
+ formatTargetAccuracy(accuracyText, sizeof(accuracyText), _killCount, 0x146, true);
+ const int bonus = calculateThresholdBonus(_killCount, 0x145, 0x14, _tuning.bonus);
+ playChapterCompleteCinematic("LVL13/L13END.ANM", 13, 0x69, 5, " ", accuracyText, bonus);
return !_vm->shouldQuit();
}
@@ -1647,8 +1710,8 @@ bool InsaneRebel1::runLevel14() {
}
if (_health >= 0) {
- playCinematic("LVL14/L14END.ANM");
- _maxChapterUnlocked = MAX(_maxChapterUnlocked, (int16)14);
+ playChapterCompleteCinematic("LVL14/L14END.ANM", 14, 0x69, 5,
+ nullptr, nullptr, 0, nullptr, nullptr, 0, 14);
return !_vm->shouldQuit();
}
@@ -1760,11 +1823,11 @@ bool InsaneRebel1::runLevel15() {
if (_killCount > 0x14)
targetBonus += (_killCount - 0x14) * _tuning.bonus;
- beginLevel15SummaryOverlay(targetBonus);
- playCinematic("LVL15/L15END1.ANM");
- _level15SummaryActive = false;
- _score += _tuning.levelPts + targetBonus + 10000;
- _maxChapterUnlocked = MAX(_maxChapterUnlocked, (int16)15);
+ char accuracyText[80];
+ formatTargetAccuracy(accuracyText, sizeof(accuracyText), _killCount, 0x58, false);
+ playChapterCompleteCinematic("LVL15/L15END1.ANM", 15, 0x122, 0xA5,
+ "Part I", accuracyText, targetBonus,
+ "Part II", "Torpedo on mark", 10000, 15);
return !_vm->shouldQuit();
}
Commit: 2b08e14c07cd682e0725633de60cbc2634253588
https://github.com/scummvm/scummvm/commit/2b08e14c07cd682e0725633de60cbc2634253588
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:10:05+02:00
Commit Message:
SCUMM: RA1: Add initial joystick support
Changed paths:
engines/scumm/insane/rebel1/iact.cpp
engines/scumm/insane/rebel1/menu.cpp
engines/scumm/insane/rebel1/rebel.cpp
engines/scumm/insane/rebel1/rebel.h
engines/scumm/metaengine.cpp
diff --git a/engines/scumm/insane/rebel1/iact.cpp b/engines/scumm/insane/rebel1/iact.cpp
index f4e779b0b2d..078bd883f7d 100644
--- a/engines/scumm/insane/rebel1/iact.cpp
+++ b/engines/scumm/insane/rebel1/iact.cpp
@@ -30,7 +30,8 @@ namespace Scumm {
inline int16 applyRebel1AnalogDeadzone(int16 axisValue) {
const int deadZone = MAX(0, ConfMan.getInt("joystick_deadzone")) * 1000;
- return (ABS(axisValue) <= deadZone) ? 0 : axisValue;
+ const int axis = axisValue;
+ return (ABS(axis) <= deadZone) ? 0 : axisValue;
}
inline int16 smoothRebel1Op0BAnalogInput(int16 inputValue, int16 &filteredValue, int16 axisMax) {
@@ -746,6 +747,21 @@ void InsaneRebel1::preprocessMouseAxes(int16 &inputX, int16 &inputY, bool *usedJ
(_vm->getActionState(kScummActionInsaneUp) ? 1 : 0) -
(_vm->getActionState(kScummActionInsaneDown) ? 1 : 0);
+ if (joyX != 0 || joyY != 0) {
+ _activeInputSource = kInputSourceJoystickDigital;
+
+ if (usedJoystick)
+ *usedJoystick = true;
+
+ inputX = joyX * 127;
+ inputY = joyY * 127;
+
+ if (_optControlsYFlip)
+ inputY = -inputY;
+
+ return;
+ }
+
if (_activeInputSource == kInputSourceJoystickAnalog) {
if (usedJoystick)
*usedJoystick = true;
@@ -764,12 +780,12 @@ void InsaneRebel1::preprocessMouseAxes(int16 &inputX, int16 &inputY, bool *usedJ
return;
}
- if (_activeInputSource == kInputSourceJoystickDigital || joyX != 0 || joyY != 0) {
+ if (_activeInputSource == kInputSourceJoystickDigital) {
if (usedJoystick)
*usedJoystick = true;
- inputX = joyX * 127;
- inputY = joyY * 127;
+ inputX = 0;
+ inputY = 0;
if (_optControlsYFlip)
inputY = -inputY;
@@ -901,9 +917,17 @@ void InsaneRebel1::updateShipPhysics() {
// not raw mouse coordinates. Reuse the same centered-axis law here.
int16 inputX = 0;
int16 inputY = 0;
- preprocessMouseAxes(inputX, inputY);
+ bool usedJoystick = false;
+ preprocessMouseAxes(inputX, inputY, &usedJoystick);
+ const int16 rawInputX = inputX;
+ const int16 rawInputY = inputY;
inputX = CLIP<int16>(inputX, -127, 127);
inputY = CLIP<int16>(inputY, -127, 127);
+ const char *inputSourceName = "mouse";
+ if (_activeInputSource == kInputSourceJoystickAnalog)
+ inputSourceName = "joystick-analog";
+ else if (_activeInputSource == kInputSourceJoystickDigital)
+ inputSourceName = "joystick-dpad";
// --- Step 2: Roll accumulator (_74CA) ---
// Normal mode: accumulate; mode 0x10: snap to input
@@ -992,6 +1016,18 @@ void InsaneRebel1::updateShipPhysics() {
if (_shipBank.numSprites > 0)
_shipDirIndex = CLIP<int16>((int16)(vComponent + hComponent), 0, _shipBank.numSprites - 1);
+ debug(1, "RA1 ship input: frame=%d source=%s usedJoystick=%d raw=(%d,%d) clipped=(%d,%d) storedAxis=(%d,%d) actionState(L,R,U,D)=(%d,%d,%d,%d) roll=%d lift=%d pos=(%d,%d) view=(%d,%d) dir=%d level=%d mode=%d opcode=0x%X",
+ _gameCounter, inputSourceName, usedJoystick,
+ rawInputX, rawInputY, inputX, inputY,
+ _joystickAxisX, _joystickAxisY,
+ _vm->getActionState(kScummActionInsaneLeft),
+ _vm->getActionState(kScummActionInsaneRight),
+ _vm->getActionState(kScummActionInsaneUp),
+ _vm->getActionState(kScummActionInsaneDown),
+ _rollAccum, _liftSmooth,
+ _shipPosX, _shipPosY, _perspectiveX, _perspectiveY, _shipDirIndex,
+ _currentLevel, _flyControlMode, _activeGameOpcode);
+
// --- Step 9: Damage/event bit synthesis + damage processing ---
// RA1 FUN_1B297-style latches from GAME opcodes:
// 0x5D latch 0xFFFF -> bit 0x40 (obstacle/contact)
diff --git a/engines/scumm/insane/rebel1/menu.cpp b/engines/scumm/insane/rebel1/menu.cpp
index eb9e7f17472..fd46706176d 100644
--- a/engines/scumm/insane/rebel1/menu.cpp
+++ b/engines/scumm/insane/rebel1/menu.cpp
@@ -35,6 +35,161 @@ namespace Scumm {
const int kRA1LevelSelectItemCount = 16; // 15 levels + BACK
const int kRA1LevelSelectRowsPerCol = 8;
const int kRA1NumLevels = 15;
+const int kRA1MenuAxisThreshold = Common::JOYAXIS_MAX / 2;
+const uint32 kRA1JoystickAxisEscGuardMs = 250;
+
+static int getRebel1MenuAxisDirection(int16 axisValue) {
+ if (axisValue >= kRA1MenuAxisThreshold)
+ return 1;
+ if (axisValue <= -kRA1MenuAxisThreshold)
+ return -1;
+ return 0;
+}
+
+static void setRebel1Volume(ScummEngine_v7 *vm, int &volume, int delta) {
+ volume = CLIP<int>(volume + delta, 0, 127);
+ vm->_mixer->setVolumeForSoundType(Audio::Mixer::kPlainSoundType,
+ (volume * Audio::Mixer::kMaxChannelVolume) / 127);
+ ConfMan.setInt("music_volume", (volume * 256) / 127);
+ ConfMan.setInt("sfx_volume", (volume * 256) / 127);
+ ConfMan.setInt("speech_volume", (volume * 256) / 127);
+}
+
+static const char *getRebel1ActionName(Common::CustomEventType customType) {
+ switch (customType) {
+ case kScummActionInsaneUp:
+ return "up";
+ case kScummActionInsaneDown:
+ return "down";
+ case kScummActionInsaneLeft:
+ return "left";
+ case kScummActionInsaneRight:
+ return "right";
+ case kScummActionInsaneAttack:
+ return "attack";
+ case kScummActionInsaneSwitch:
+ return "switch";
+ default:
+ return "other";
+ }
+}
+
+static const char *getRebel1BackendAxisName(Common::CustomEventType customType) {
+ switch (customType) {
+ case kScummBackendActionRebel1AxisUp:
+ return "stick-up";
+ case kScummBackendActionRebel1AxisDown:
+ return "stick-down";
+ case kScummBackendActionRebel1AxisLeft:
+ return "stick-left";
+ case kScummBackendActionRebel1AxisRight:
+ return "stick-right";
+ default:
+ return "other-axis";
+ }
+}
+
+static int16 normalizeRebel1MappedAxisPosition(int16 axisPosition) {
+ // Custom backend axis events are half-axis magnitudes. Keymapper takes
+ // ABS(rawPosition), but int16 -32768 cannot be represented as +32768.
+ return axisPosition == Common::JOYAXIS_MIN ? Common::JOYAXIS_MAX : axisPosition;
+}
+
+bool InsaneRebel1::handleControllerMenuAction(ScummAction action) {
+ if (!_menuActive || _highScoresActive)
+ return false;
+
+ if (_levelSelectActive) {
+ int col = _levelSelectSel / kRA1LevelSelectRowsPerCol;
+ int row = _levelSelectSel % kRA1LevelSelectRowsPerCol;
+
+ switch (action) {
+ case kScummActionInsaneUp:
+ row = (row + kRA1LevelSelectRowsPerCol - 1) % kRA1LevelSelectRowsPerCol;
+ _levelSelectSel = col * kRA1LevelSelectRowsPerCol + row;
+ return true;
+ case kScummActionInsaneDown:
+ row = (row + 1) % kRA1LevelSelectRowsPerCol;
+ _levelSelectSel = col * kRA1LevelSelectRowsPerCol + row;
+ return true;
+ case kScummActionInsaneLeft:
+ if (col > 0)
+ _levelSelectSel -= kRA1LevelSelectRowsPerCol;
+ return true;
+ case kScummActionInsaneRight:
+ if (col < 1)
+ _levelSelectSel += kRA1LevelSelectRowsPerCol;
+ return true;
+ case kScummActionInsaneAttack:
+ _menuConfirmed = true;
+ _vm->_smushVideoShouldFinish = true;
+ return true;
+ default:
+ return false;
+ }
+ }
+
+ if (_optionsActive) {
+ switch (action) {
+ case kScummActionInsaneUp:
+ _optionsSel = (_optionsSel + kOptionsItemCount - 1) % kOptionsItemCount;
+ return true;
+ case kScummActionInsaneDown:
+ _optionsSel = (_optionsSel + 1) % kOptionsItemCount;
+ return true;
+ case kScummActionInsaneLeft:
+ if (_optionsSel == 6)
+ setRebel1Volume(_vm, _optVolume, -5);
+ return true;
+ case kScummActionInsaneRight:
+ if (_optionsSel == 6)
+ setRebel1Volume(_vm, _optVolume, 5);
+ return true;
+ case kScummActionInsaneAttack:
+ _menuConfirmed = true;
+ _vm->_smushVideoShouldFinish = true;
+ return true;
+ default:
+ return false;
+ }
+ }
+
+ switch (action) {
+ case kScummActionInsaneUp:
+ _menuSelection = (_menuSelection + 4) % 5;
+ return true;
+ case kScummActionInsaneDown:
+ _menuSelection = (_menuSelection + 1) % 5;
+ return true;
+ case kScummActionInsaneAttack:
+ _menuConfirmed = true;
+ _vm->_smushVideoShouldFinish = true;
+ return true;
+ default:
+ return false;
+ }
+}
+
+bool InsaneRebel1::handleControllerMenuAxis(int16 oldAxisX, int16 oldAxisY) {
+ if (!_menuActive || _highScoresActive)
+ return false;
+
+ // RA1 maps stick axes to backend axis events for analog gameplay.
+ // Menus still need edge-triggered digital navigation from those same axes.
+ // Match the original raw-input convention: positive Y is stick-down in
+ // menus, and the Y-flip option reverses that interpretation.
+ const int oldX = getRebel1MenuAxisDirection(oldAxisX);
+ const int oldY = getRebel1MenuAxisDirection(oldAxisY);
+ const int newX = getRebel1MenuAxisDirection(_joystickAxisX);
+ const int newY = getRebel1MenuAxisDirection(_joystickAxisY);
+
+ if (newY != oldY && newY != 0)
+ return handleControllerMenuAction((newY > 0) != _optControlsYFlip ? kScummActionInsaneDown : kScummActionInsaneUp);
+ if (newX != oldX && newX != 0)
+ return handleControllerMenuAction(newX > 0 ? kScummActionInsaneRight : kScummActionInsaneLeft);
+
+ return false;
+}
bool InsaneRebel1::notifyEvent(const Common::Event &event) {
// Global ScummVM dialogs pause the engine while their modal event loop runs.
@@ -47,29 +202,84 @@ bool InsaneRebel1::notifyEvent(const Common::Event &event) {
_activeInputSource = kInputSourceMouse;
}
+ if (event.type == Common::EVENT_JOYAXIS_MOTION) {
+ _lastJoystickAxisEventTime = _vm->_system->getMillis();
+ debug(1, "RA1 input raw-joy-axis: axis=%d pos=%d menu=%d gameplay=%d storedAxis=(%d,%d)",
+ event.joystick.axis, event.joystick.position,
+ _menuActive, _interactiveVideoActive && !_menuActive,
+ _joystickAxisX, _joystickAxisY);
+ }
+
+ if (event.type == Common::EVENT_JOYBUTTON_DOWN || event.type == Common::EVENT_JOYBUTTON_UP) {
+ debug(1, "RA1 input raw-joy-button: button=%d pressed=%d menu=%d gameplay=%d storedAxis=(%d,%d)",
+ event.joystick.button, event.type == Common::EVENT_JOYBUTTON_DOWN,
+ _menuActive, _interactiveVideoActive && !_menuActive,
+ _joystickAxisX, _joystickAxisY);
+ }
+
if (event.type == Common::EVENT_CUSTOM_BACKEND_ACTION_AXIS) {
_activeInputSource = kInputSourceJoystickAnalog;
+ _lastJoystickAxisEventTime = _vm->_system->getMillis();
+ const int16 oldAxisX = _joystickAxisX;
+ const int16 oldAxisY = _joystickAxisY;
+ const int16 axisPosition = normalizeRebel1MappedAxisPosition(event.joystick.position);
switch (event.customType) {
case kScummBackendActionRebel1AxisUp:
- if (event.joystick.position == 0 && _joystickAxisY < 0)
+ if (event.joystick.position == 0 && _joystickAxisY > 0) {
+ debug(1, "RA1 input mapped-axis ignored-reset: %s pos=0 current=(%d,%d)",
+ getRebel1BackendAxisName(event.customType), _joystickAxisX, _joystickAxisY);
+ return true;
+ }
+ _joystickAxisY = -axisPosition;
+ debug(1, "RA1 input mapped-axis: %s pos=%d rawPos=%d old=(%d,%d) new=(%d,%d) menu=%d gameplay=%d",
+ getRebel1BackendAxisName(event.customType), axisPosition, event.joystick.position,
+ oldAxisX, oldAxisY, _joystickAxisX, _joystickAxisY,
+ _menuActive, _interactiveVideoActive && !_menuActive);
+ if (handleControllerMenuAxis(oldAxisX, oldAxisY))
return true;
- _joystickAxisY = event.joystick.position;
return true;
case kScummBackendActionRebel1AxisDown:
- if (event.joystick.position == 0 && _joystickAxisY > 0)
+ if (event.joystick.position == 0 && _joystickAxisY < 0) {
+ debug(1, "RA1 input mapped-axis ignored-reset: %s pos=0 current=(%d,%d)",
+ getRebel1BackendAxisName(event.customType), _joystickAxisX, _joystickAxisY);
+ return true;
+ }
+ _joystickAxisY = axisPosition;
+ debug(1, "RA1 input mapped-axis: %s pos=%d rawPos=%d old=(%d,%d) new=(%d,%d) menu=%d gameplay=%d",
+ getRebel1BackendAxisName(event.customType), axisPosition, event.joystick.position,
+ oldAxisX, oldAxisY, _joystickAxisX, _joystickAxisY,
+ _menuActive, _interactiveVideoActive && !_menuActive);
+ if (handleControllerMenuAxis(oldAxisX, oldAxisY))
return true;
- _joystickAxisY = -event.joystick.position;
return true;
case kScummBackendActionRebel1AxisLeft:
- if (event.joystick.position == 0 && _joystickAxisX > 0)
+ if (event.joystick.position == 0 && _joystickAxisX > 0) {
+ debug(1, "RA1 input mapped-axis ignored-reset: %s pos=0 current=(%d,%d)",
+ getRebel1BackendAxisName(event.customType), _joystickAxisX, _joystickAxisY);
+ return true;
+ }
+ _joystickAxisX = -axisPosition;
+ debug(1, "RA1 input mapped-axis: %s pos=%d rawPos=%d old=(%d,%d) new=(%d,%d) menu=%d gameplay=%d",
+ getRebel1BackendAxisName(event.customType), axisPosition, event.joystick.position,
+ oldAxisX, oldAxisY, _joystickAxisX, _joystickAxisY,
+ _menuActive, _interactiveVideoActive && !_menuActive);
+ if (handleControllerMenuAxis(oldAxisX, oldAxisY))
return true;
- _joystickAxisX = -event.joystick.position;
return true;
case kScummBackendActionRebel1AxisRight:
- if (event.joystick.position == 0 && _joystickAxisX < 0)
+ if (event.joystick.position == 0 && _joystickAxisX < 0) {
+ debug(1, "RA1 input mapped-axis ignored-reset: %s pos=0 current=(%d,%d)",
+ getRebel1BackendAxisName(event.customType), _joystickAxisX, _joystickAxisY);
+ return true;
+ }
+ _joystickAxisX = axisPosition;
+ debug(1, "RA1 input mapped-axis: %s pos=%d rawPos=%d old=(%d,%d) new=(%d,%d) menu=%d gameplay=%d",
+ getRebel1BackendAxisName(event.customType), axisPosition, event.joystick.position,
+ oldAxisX, oldAxisY, _joystickAxisX, _joystickAxisY,
+ _menuActive, _interactiveVideoActive && !_menuActive);
+ if (handleControllerMenuAxis(oldAxisX, oldAxisY))
return true;
- _joystickAxisX = event.joystick.position;
return true;
default:
break;
@@ -80,6 +290,15 @@ bool InsaneRebel1::notifyEvent(const Common::Event &event) {
event.type == Common::EVENT_CUSTOM_ENGINE_ACTION_END) {
const bool pressed = (event.type == Common::EVENT_CUSTOM_ENGINE_ACTION_START);
+ debug(1, "RA1 input mapped-action: action=%s custom=%u pressed=%d menu=%d gameplay=%d storedAxis=(%d,%d) actionState(L,R,U,D)=(%d,%d,%d,%d)",
+ getRebel1ActionName(event.customType), event.customType, pressed,
+ _menuActive, _interactiveVideoActive && !_menuActive,
+ _joystickAxisX, _joystickAxisY,
+ _vm->getActionState(kScummActionInsaneLeft),
+ _vm->getActionState(kScummActionInsaneRight),
+ _vm->getActionState(kScummActionInsaneUp),
+ _vm->getActionState(kScummActionInsaneDown));
+
if (pressed &&
(event.customType == kScummActionInsaneUp ||
event.customType == kScummActionInsaneDown ||
@@ -93,91 +312,8 @@ bool InsaneRebel1::notifyEvent(const Common::Event &event) {
return true;
}
- if (_menuActive && !_highScoresActive && pressed) {
- if (_levelSelectActive) {
- int col = _levelSelectSel / kRA1LevelSelectRowsPerCol;
- int row = _levelSelectSel % kRA1LevelSelectRowsPerCol;
-
- switch (event.customType) {
- case kScummActionInsaneUp:
- row = (row + kRA1LevelSelectRowsPerCol - 1) % kRA1LevelSelectRowsPerCol;
- _levelSelectSel = col * kRA1LevelSelectRowsPerCol + row;
- return true;
- case kScummActionInsaneDown:
- row = (row + 1) % kRA1LevelSelectRowsPerCol;
- _levelSelectSel = col * kRA1LevelSelectRowsPerCol + row;
- return true;
- case kScummActionInsaneLeft:
- if (col > 0)
- _levelSelectSel -= kRA1LevelSelectRowsPerCol;
- return true;
- case kScummActionInsaneRight:
- if (col < 1)
- _levelSelectSel += kRA1LevelSelectRowsPerCol;
- return true;
- case kScummActionInsaneAttack:
- _menuConfirmed = true;
- _vm->_smushVideoShouldFinish = true;
- return true;
- default:
- break;
- }
- }
-
- if (_optionsActive) {
- switch (event.customType) {
- case kScummActionInsaneUp:
- _optionsSel = (_optionsSel + kOptionsItemCount - 1) % kOptionsItemCount;
- return true;
- case kScummActionInsaneDown:
- _optionsSel = (_optionsSel + 1) % kOptionsItemCount;
- return true;
- case kScummActionInsaneLeft:
- if (_optionsSel == 6) {
- _optVolume = MAX(0, _optVolume - 5);
- _vm->_mixer->setVolumeForSoundType(Audio::Mixer::kPlainSoundType,
- (_optVolume * Audio::Mixer::kMaxChannelVolume) / 127);
- ConfMan.setInt("music_volume", (_optVolume * 256) / 127);
- ConfMan.setInt("sfx_volume", (_optVolume * 256) / 127);
- ConfMan.setInt("speech_volume", (_optVolume * 256) / 127);
- }
- return true;
- case kScummActionInsaneRight:
- if (_optionsSel == 6) {
- _optVolume = MIN(127, _optVolume + 5);
- _vm->_mixer->setVolumeForSoundType(Audio::Mixer::kPlainSoundType,
- (_optVolume * Audio::Mixer::kMaxChannelVolume) / 127);
- ConfMan.setInt("music_volume", (_optVolume * 256) / 127);
- ConfMan.setInt("sfx_volume", (_optVolume * 256) / 127);
- ConfMan.setInt("speech_volume", (_optVolume * 256) / 127);
- }
- return true;
- case kScummActionInsaneAttack:
- _menuConfirmed = true;
- _vm->_smushVideoShouldFinish = true;
- return true;
- default:
- break;
- }
- }
-
- if (!_optionsActive && !_levelSelectActive) {
- switch (event.customType) {
- case kScummActionInsaneUp:
- _menuSelection = (_menuSelection + 4) % 5;
- return true;
- case kScummActionInsaneDown:
- _menuSelection = (_menuSelection + 1) % 5;
- return true;
- case kScummActionInsaneAttack:
- _menuConfirmed = true;
- _vm->_smushVideoShouldFinish = true;
- return true;
- default:
- break;
- }
- }
- }
+ if (pressed && handleControllerMenuAction((ScummAction)event.customType))
+ return true;
if (_interactiveVideoActive && !_menuActive && event.customType == kScummActionInsaneAttack) {
_playerFired = pressed;
@@ -238,26 +374,14 @@ bool InsaneRebel1::notifyEvent(const Common::Event &event) {
case Common::KEYCODE_LEFT:
case Common::KEYCODE_a:
// Volume down when on volume row (row 6)
- if (_optionsSel == 6) {
- _optVolume = MAX(0, _optVolume - 5);
- _vm->_mixer->setVolumeForSoundType(Audio::Mixer::kPlainSoundType,
- (_optVolume * Audio::Mixer::kMaxChannelVolume) / 127);
- ConfMan.setInt("music_volume", (_optVolume * 256) / 127);
- ConfMan.setInt("sfx_volume", (_optVolume * 256) / 127);
- ConfMan.setInt("speech_volume", (_optVolume * 256) / 127);
- }
+ if (_optionsSel == 6)
+ setRebel1Volume(_vm, _optVolume, -5);
return true;
case Common::KEYCODE_RIGHT:
case Common::KEYCODE_d:
// Volume up when on volume row (row 6)
- if (_optionsSel == 6) {
- _optVolume = MIN(127, _optVolume + 5);
- _vm->_mixer->setVolumeForSoundType(Audio::Mixer::kPlainSoundType,
- (_optVolume * Audio::Mixer::kMaxChannelVolume) / 127);
- ConfMan.setInt("music_volume", (_optVolume * 256) / 127);
- ConfMan.setInt("sfx_volume", (_optVolume * 256) / 127);
- ConfMan.setInt("speech_volume", (_optVolume * 256) / 127);
- }
+ if (_optionsSel == 6)
+ setRebel1Volume(_vm, _optVolume, 5);
return true;
case Common::KEYCODE_RETURN:
case Common::KEYCODE_KP_ENTER:
@@ -329,9 +453,30 @@ bool InsaneRebel1::notifyEvent(const Common::Event &event) {
return true;
}
+ if (event.type == Common::EVENT_MAINMENU && _interactiveVideoActive && !_menuActive) {
+ const uint32 now = _vm->_system->getMillis();
+ const uint32 elapsedSinceAxis = _lastJoystickAxisEventTime ? now - _lastJoystickAxisEventTime : 0xffffffffu;
+ debug(1, "RA1 input mainmenu-event: gameplay=1 elapsedSinceAxis=%u storedAxis=(%d,%d)",
+ elapsedSinceAxis, _joystickAxisX, _joystickAxisY);
+ if (elapsedSinceAxis <= kRA1JoystickAxisEscGuardMs) {
+ debug(1, "RA1 input ignored mainmenu event after recent joystick axis movement (%u ms)", elapsedSinceAxis);
+ return true;
+ }
+ }
+
if (event.type == Common::EVENT_KEYDOWN && _player) {
if (_interactiveVideoActive && !_menuActive &&
event.kbd.keycode == Common::KEYCODE_ESCAPE) {
+ const uint32 now = _vm->_system->getMillis();
+ const uint32 elapsedSinceAxis = _lastJoystickAxisEventTime ? now - _lastJoystickAxisEventTime : 0xffffffffu;
+ debug(1, "RA1 input keydown-escape: gameplay=1 ascii=%d flags=0x%x repeat=%d elapsedSinceAxis=%u storedAxis=(%d,%d)",
+ event.kbd.ascii, event.kbd.flags, event.kbdRepeat,
+ elapsedSinceAxis, _joystickAxisX, _joystickAxisY);
+ if (elapsedSinceAxis <= kRA1JoystickAxisEscGuardMs) {
+ debug(1, "RA1 input ignored ESC after recent joystick axis movement (%u ms)", elapsedSinceAxis);
+ return true;
+ }
+
debug("Rebel1: ESC pressed during gameplay - opening ScummVM menu");
const bool wasPaused = _player->_paused;
if (!wasPaused)
diff --git a/engines/scumm/insane/rebel1/rebel.cpp b/engines/scumm/insane/rebel1/rebel.cpp
index bec456bd16e..28d4efc4e49 100644
--- a/engines/scumm/insane/rebel1/rebel.cpp
+++ b/engines/scumm/insane/rebel1/rebel.cpp
@@ -263,6 +263,7 @@ InsaneRebel1::InsaneRebel1(ScummEngine_v7 *scumm) : Insane(), _vm(scumm) {
_mouseRecentering = false;
_joystickAxisX = 0;
_joystickAxisY = 0;
+ _lastJoystickAxisEventTime = 0;
_level2JoystickFilteredX = 0;
_level2JoystickFilteredY = 0;
_activeInputSource = kInputSourceMouse;
diff --git a/engines/scumm/insane/rebel1/rebel.h b/engines/scumm/insane/rebel1/rebel.h
index 77855815881..06803f81abb 100644
--- a/engines/scumm/insane/rebel1/rebel.h
+++ b/engines/scumm/insane/rebel1/rebel.h
@@ -281,6 +281,7 @@ private:
bool _mouseRecentering; // 0x976D: suppress recursive updates during warp
int16 _joystickAxisX; // Rebel-specific left-stick X captured from keymapper axis events
int16 _joystickAxisY; // Rebel-specific left-stick Y captured from keymapper axis events
+ uint32 _lastJoystickAxisEventTime;
int16 _level2JoystickFilteredX; // Smoothed Level 2 analog X input
int16 _level2JoystickFilteredY; // Smoothed Level 2 analog Y input
enum InputSource {
@@ -443,6 +444,8 @@ private:
// Main menu / options state
void runOptionsMenu();
+ bool handleControllerMenuAction(ScummAction action);
+ bool handleControllerMenuAxis(int16 oldAxisX, int16 oldAxisY);
bool _menuActive;
bool _menuConfirmed;
int _menuSelection; // 0..4 maps to return values 1..5
diff --git a/engines/scumm/metaengine.cpp b/engines/scumm/metaengine.cpp
index 4cc3fa5cb8b..5c8c0025a02 100644
--- a/engines/scumm/metaengine.cpp
+++ b/engines/scumm/metaengine.cpp
@@ -1128,21 +1128,25 @@ Common::KeymapArray ScummMetaEngine::initKeymaps(const char *target) const {
act = new Action("RA1STICKUP", _("Stick up"));
act->setCustomBackendActionAxisEvent(kScummBackendActionRebel1AxisUp);
act->addDefaultInputMapping("JOY_LEFT_STICK_Y-");
+ act->addDefaultInputMapping("JOY_RIGHT_STICK_Y-");
rebel1Keymap->addAction(act);
act = new Action("RA1STICKDOWN", _("Stick down"));
act->setCustomBackendActionAxisEvent(kScummBackendActionRebel1AxisDown);
act->addDefaultInputMapping("JOY_LEFT_STICK_Y+");
+ act->addDefaultInputMapping("JOY_RIGHT_STICK_Y+");
rebel1Keymap->addAction(act);
act = new Action("RA1STICKLEFT", _("Stick left"));
act->setCustomBackendActionAxisEvent(kScummBackendActionRebel1AxisLeft);
act->addDefaultInputMapping("JOY_LEFT_STICK_X-");
+ act->addDefaultInputMapping("JOY_RIGHT_STICK_X-");
rebel1Keymap->addAction(act);
act = new Action("RA1STICKRIGHT", _("Stick right"));
act->setCustomBackendActionAxisEvent(kScummBackendActionRebel1AxisRight);
act->addDefaultInputMapping("JOY_LEFT_STICK_X+");
+ act->addDefaultInputMapping("JOY_RIGHT_STICK_X+");
rebel1Keymap->addAction(act);
act = new Action("RA1FIRE", _("Fire / select"));
Commit: d0cb0eea1f2971419806453a68a58b41d586d08d
https://github.com/scummvm/scummvm/commit/d0cb0eea1f2971419806453a68a58b41d586d08d
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:10:05+02:00
Commit Message:
SCUMM: RA1: Improve menu code
Changed paths:
engines/scumm/insane/rebel1/menu.cpp
engines/scumm/insane/rebel1/rebel.cpp
engines/scumm/insane/rebel1/rebel.h
diff --git a/engines/scumm/insane/rebel1/menu.cpp b/engines/scumm/insane/rebel1/menu.cpp
index fd46706176d..22e5d5e0fcc 100644
--- a/engines/scumm/insane/rebel1/menu.cpp
+++ b/engines/scumm/insane/rebel1/menu.cpp
@@ -36,6 +36,12 @@ const int kRA1LevelSelectItemCount = 16; // 15 levels + BACK
const int kRA1LevelSelectRowsPerCol = 8;
const int kRA1NumLevels = 15;
const int kRA1MenuAxisThreshold = Common::JOYAXIS_MAX / 2;
+const int kRA1MenuLogicalWidth = 0x140;
+const int kRA1MenuFrameX = 0x32;
+const int kRA1MenuFrameW = 0xdc;
+const int kRA1MenuFrameH = 0x0f;
+const int kRA1MenuRowH = 0x0f;
+const byte kRA1MenuFrameColor = 0xdf;
const uint32 kRA1JoystickAxisEscGuardMs = 250;
static int getRebel1MenuAxisDirection(int16 axisValue) {
@@ -95,6 +101,29 @@ static int16 normalizeRebel1MappedAxisPosition(int16 axisPosition) {
return axisPosition == Common::JOYAXIS_MIN ? Common::JOYAXIS_MAX : axisPosition;
}
+static int getRebel1MenuCenteredX(int textWidth) {
+ return (kRA1MenuLogicalWidth - textWidth) / 2;
+}
+
+static void drawRebel1MenuFrame(byte *dst, int pitch, int width, int height, int x, int y, int w) {
+ if (!dst || width <= 0 || height <= 0)
+ return;
+
+ const int leftX = CLIP(x, 0, width - 1);
+ const int rightX = CLIP(x + w - 1, 0, width - 1);
+ const int topY = CLIP(y, 0, height - 1);
+ const int bottomY = CLIP(y + kRA1MenuFrameH - 1, 0, height - 1);
+
+ for (int px = leftX; px <= rightX; px++) {
+ dst[topY * pitch + px] = kRA1MenuFrameColor;
+ dst[bottomY * pitch + px] = kRA1MenuFrameColor;
+ }
+ for (int py = topY; py <= bottomY; py++) {
+ dst[py * pitch + leftX] = kRA1MenuFrameColor;
+ dst[py * pitch + rightX] = kRA1MenuFrameColor;
+ }
+}
+
bool InsaneRebel1::handleControllerMenuAction(ScummAction action) {
if (!_menuActive || _highScoresActive)
return false;
@@ -390,7 +419,7 @@ bool InsaneRebel1::notifyEvent(const Common::Event &event) {
_vm->_smushVideoShouldFinish = true;
return true;
case Common::KEYCODE_ESCAPE:
- _optionsSel = kOptionsItemCount - 1; // Back
+ _optionsSel = 0;
_menuConfirmed = true;
_vm->_smushVideoShouldFinish = true;
return true;
@@ -532,7 +561,7 @@ void InsaneRebel1::renderMainMenuOverlay(byte *dst, int pitch, int width, int he
// Original renders over O1SCORE.ANM. Title appears after frame 20,
// entries fade in one per frame. We show all immediately.
const int titleW = getTalkTextWidth("TOP PILOTS");
- drawTalkText((width - titleW) / 2, 10, "TOP PILOTS");
+ drawTalkText(getRebel1MenuCenteredX(titleW), 10, "TOP PILOTS");
for (int i = 0; i < kHighScoreCount; i++) {
const int y = 25 + i * 14;
@@ -553,8 +582,8 @@ void InsaneRebel1::renderMainMenuOverlay(byte *dst, int pitch, int width, int he
// --- Options submenu (matching original RunGameOptionsMenu) ---
const char *kDiffNames[3] = { "EASY", "NORMAL", "HARD" };
- const int titleW = getTalkTextWidth("GAME OPTIONS");
- drawTalkText((width - titleW) / 2, 30, "GAME OPTIONS");
+ const int titleW = getTitleTextWidth("GAME OPTIONS");
+ drawTitleText(getRebel1MenuCenteredX(titleW), 15, "GAME OPTIONS");
// Build dynamic option strings for each row
char diffLine[64], volLine[64];
@@ -563,49 +592,32 @@ void InsaneRebel1::renderMainMenuOverlay(byte *dst, int pitch, int width, int he
const char *optItems[kOptionsItemCount] = {
"EXIT MENU",
+ _optRookieOneFemale ? "ROOKIE1 IS FEMALE" : "ROOKIE1 IS MALE",
_optMusicEnabled ? "MUSIC IS ON" : "MUSIC IS OFF",
_optSfxEnabled ? "SFX AND VOICE ARE ON" : "SFX AND VOICE ARE OFF",
_optTextEnabled ? "DIALOGUE TEXT IS ON" : "DIALOGUE TEXT IS OFF",
_optControlsYFlip ? "CONTROLS ARE Y-FLIPPED" : "CONTROLS ARE NORMAL",
- _turbulenceEnabled ? "TURBULENCE IS ON" : "TURBULENCE IS OFF",
volLine,
- diffLine,
- "BACK"
+ diffLine
};
- const int menuY = 44;
- const int rowH = 14;
-
for (int i = 0; i < kOptionsItemCount; i++) {
const int textW = getTalkTextWidth(optItems[i]);
- const int textX = (width - textW) / 2;
- const int y = menuY + i * rowH;
- drawTalkText(textX, y + 1, optItems[i]);
-
- if (i == _optionsSel) {
- byte highlightColor = ((_menuFrameCounter / 8) & 1) ? 248 : 240;
- int bracketWidth = textW + 12;
- int leftX = CLIP(textX - 6, 0, width - 1);
- int rightX = CLIP(leftX + bracketWidth, 0, width - 1);
- int topY = CLIP(y - 1, 0, height - 1);
- int bottomY = CLIP(y + rowH - 2, 0, height - 1);
- for (int x = leftX; x <= rightX; x++) {
- dst[topY * pitch + x] = highlightColor;
- dst[bottomY * pitch + x] = highlightColor;
- }
- for (int py = topY; py <= bottomY; py++) {
- dst[py * pitch + leftX] = highlightColor;
- dst[py * pitch + rightX] = highlightColor;
- }
- }
+ const int textX = getRebel1MenuCenteredX(textW);
+ const int y = 0x2d + i * kRA1MenuRowH;
+ drawTalkText(textX, y, optItems[i]);
+
+ if (i == _optionsSel)
+ drawRebel1MenuFrame(dst, pitch, width, height,
+ kRA1MenuFrameX, (i + 1) * kRA1MenuRowH + 0x1d, kRA1MenuFrameW);
}
return;
}
if (_levelSelectActive) {
- // --- Level select submenu (two-column layout) ---
- const int titleW = getTalkTextWidth("LEVEL SELECT");
- drawTalkText((width - titleW) / 2, 30, "LEVEL SELECT");
+ // --- ScummVM level select submenu, styled like the original frontend menus ---
+ const int titleW = getTitleTextWidth("LEVEL SELECT");
+ drawTitleText(getRebel1MenuCenteredX(titleW), 15, "LEVEL SELECT");
const char *kLevelItems[kRA1LevelSelectItemCount] = {
" 1 TRAINING",
@@ -626,36 +638,24 @@ void InsaneRebel1::renderMainMenuOverlay(byte *dst, int pitch, int width, int he
"BACK"
};
- const int menuY = 50;
- const int rowH = 14;
- const int leftColX = 8;
- const int rightColX = width / 2 + 4;
+ const int menuY = 0x2d;
+ const int leftFrameX = 20;
+ const int rightFrameX = 170;
+ const int columnW = 130;
for (int i = 0; i < kRA1LevelSelectItemCount; i++) {
const int col = i / kRA1LevelSelectRowsPerCol;
const int row = i % kRA1LevelSelectRowsPerCol;
- const int textX = (col == 0) ? leftColX : rightColX;
- const int y = menuY + row * rowH;
+ const int frameX = (col == 0) ? leftFrameX : rightFrameX;
+ const int y = menuY + row * kRA1MenuRowH;
const int textW = getTalkTextWidth(kLevelItems[i]);
+ const int textX = frameX + (columnW - textW) / 2;
- drawTalkText(textX, y + 1, kLevelItems[i]);
-
- if (i == _levelSelectSel) {
- byte highlightColor = ((_menuFrameCounter / 8) & 1) ? 248 : 240;
- int bracketWidth = textW + 12;
- int leftX = CLIP(textX - 6, 0, width - 1);
- int rightX = CLIP(leftX + bracketWidth, 0, width - 1);
- int topY = CLIP(y - 1, 0, height - 1);
- int bottomY = CLIP(y + rowH - 2, 0, height - 1);
- for (int x = leftX; x <= rightX; x++) {
- dst[topY * pitch + x] = highlightColor;
- dst[bottomY * pitch + x] = highlightColor;
- }
- for (int py = topY; py <= bottomY; py++) {
- dst[py * pitch + leftX] = highlightColor;
- dst[py * pitch + rightX] = highlightColor;
- }
- }
+ drawTalkText(textX, y, kLevelItems[i]);
+
+ if (i == _levelSelectSel)
+ drawRebel1MenuFrame(dst, pitch, width, height,
+ frameX, row * kRA1MenuRowH + 0x2c, columnW);
}
return;
}
@@ -671,52 +671,20 @@ void InsaneRebel1::renderMainMenuOverlay(byte *dst, int pitch, int width, int he
// Center title
const int titleW = getTitleTextWidth("MAIN MENU");
- const int titleX = (width - titleW) / 2;
- drawTitleText(titleX, 36, "MAIN MENU");
+ const int titleX = getRebel1MenuCenteredX(titleW);
+ drawTitleText(titleX, 30, "MAIN MENU");
// Draw menu items centered horizontally
- const int menuY = 60;
- const int rowH = 16;
-
for (int i = 0; i < 5; i++) {
const int textW = getTalkTextWidth(kMenuItems[i]);
- const int textX = (width - textW) / 2;
- const int y = menuY + i * rowH;
-
- drawTalkText(textX, y + 1, kMenuItems[i]);
-
- // Selection highlight box â flashing border (FUN_004292d0 pattern from RA2)
- if (i == _menuSelection) {
- // Flash between two palette colors every 8 frames
- byte highlightColor = ((_menuFrameCounter / 8) & 1) ? 248 : 240;
-
- int bracketWidth = textW + 12;
- int bracketHeight = rowH;
- int leftX = textX - 6;
- int rightX = leftX + bracketWidth;
- int topY = y - 1;
- int bottomY = y + bracketHeight - 2;
-
- // Clamp
- if (leftX < 0) leftX = 0;
- if (rightX >= width) rightX = width - 1;
- if (topY < 0) topY = 0;
- if (bottomY >= height) bottomY = height - 1;
-
- // Draw rectangle border (4 lines)
- for (int x = leftX; x <= rightX && x < width; x++) {
- if (topY >= 0 && topY < height)
- dst[topY * pitch + x] = highlightColor;
- if (bottomY >= 0 && bottomY < height)
- dst[bottomY * pitch + x] = highlightColor;
- }
- for (int py = topY; py <= bottomY && py < height; py++) {
- if (leftX >= 0 && leftX < width)
- dst[py * pitch + leftX] = highlightColor;
- if (rightX >= 0 && rightX < width)
- dst[py * pitch + rightX] = highlightColor;
- }
- }
+ const int textX = getRebel1MenuCenteredX(textW);
+ const int y = 0x3c + i * kRA1MenuRowH;
+
+ drawTalkText(textX, y, kMenuItems[i]);
+
+ if (i == _menuSelection)
+ drawRebel1MenuFrame(dst, pitch, width, height,
+ kRA1MenuFrameX, (i + 1) * kRA1MenuRowH + 0x2c, kRA1MenuFrameW);
}
}
@@ -759,37 +727,34 @@ void InsaneRebel1::runOptionsMenu() {
if (_menuConfirmed) {
switch (_optionsSel) {
- case 0: // EXIT MENU (same as BACK)
+ case 0: // EXIT MENU
_optionsActive = false;
return;
- case 1: // Toggle music
+ case 1: // Toggle Rookie One gender
+ _optRookieOneFemale = !_optRookieOneFemale;
+ break;
+ case 2: // Toggle music
_optMusicEnabled = !_optMusicEnabled;
_vm->_mixer->muteSoundType(Audio::Mixer::kMusicSoundType, !_optMusicEnabled);
break;
- case 2: // Toggle SFX + Voice
+ case 3: // Toggle SFX + Voice
_optSfxEnabled = !_optSfxEnabled;
_vm->_mixer->muteSoundType(Audio::Mixer::kSFXSoundType, !_optSfxEnabled);
_vm->_mixer->muteSoundType(Audio::Mixer::kSpeechSoundType, !_optSfxEnabled);
break;
- case 3: // Toggle dialogue text
+ case 4: // Toggle dialogue text
_optTextEnabled = !_optTextEnabled;
ConfMan.setBool("subtitles", _optTextEnabled);
break;
- case 4: // Toggle Y-flip controls
+ case 5: // Toggle Y-flip controls
_optControlsYFlip = !_optControlsYFlip;
break;
- case 5: // Toggle turbulence
- _turbulenceEnabled = !_turbulenceEnabled;
- break;
case 6: // Volume â adjusted via left/right in notifyEvent
break;
case 7: // Cycle difficulty
_difficulty = (_difficulty + 1) % 3;
loadTuningForLevel(0);
break;
- case 8: // BACK
- _optionsActive = false;
- return;
}
}
}
diff --git a/engines/scumm/insane/rebel1/rebel.cpp b/engines/scumm/insane/rebel1/rebel.cpp
index 28d4efc4e49..2815119bd10 100644
--- a/engines/scumm/insane/rebel1/rebel.cpp
+++ b/engines/scumm/insane/rebel1/rebel.cpp
@@ -326,9 +326,10 @@ InsaneRebel1::InsaneRebel1(ScummEngine_v7 *scumm) : Insane(), _vm(scumm) {
_levelSelectActive = false;
_levelSelectSel = 0;
_startLevel = 1;
- _turbulenceEnabled = false;
+ _turbulenceEnabled = true;
// Options â read initial state from ScummVM mixer
+ _optRookieOneFemale = false;
_optMusicEnabled = !_vm->_mixer->isSoundTypeMuted(Audio::Mixer::kMusicSoundType);
_optSfxEnabled = !_vm->_mixer->isSoundTypeMuted(Audio::Mixer::kSFXSoundType);
_optTextEnabled = true;
diff --git a/engines/scumm/insane/rebel1/rebel.h b/engines/scumm/insane/rebel1/rebel.h
index 06803f81abb..7a5c1a7cbc3 100644
--- a/engines/scumm/insane/rebel1/rebel.h
+++ b/engines/scumm/insane/rebel1/rebel.h
@@ -452,14 +452,15 @@ private:
int _menuFrameCounter;
// Options submenu state â RunGameOptionsMenu (0x14B42)
- static const int kOptionsItemCount = 9; // 8 options + BACK
+ static const int kOptionsItemCount = 8;
bool _optionsActive; // True when showing options instead of main menu
- int _optionsSel; // 0..8 selected option row
+ int _optionsSel; // 0..7 selected option row
bool _levelSelectActive; // True when showing level-select submenu
int _levelSelectSel; // 0=Level1 ... N-1=Back
int _startLevel; // 1-based start level for "Start New Game"
// Per-option state (matching original RunGameOptionsMenu globals)
+ bool _optRookieOneFemale; // DAT_22c3: Rookie One gender
bool _optMusicEnabled; // DAT_22b7: music on/off
bool _optSfxEnabled; // DAT_22b8: sfx+voice on/off
bool _optTextEnabled; // DAT_22b9: dialogue text on/off
Commit: b96a94011eaaf281921030f28576d22a2fc69e44
https://github.com/scummvm/scummvm/commit/b96a94011eaaf281921030f28576d22a2fc69e44
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:10:06+02:00
Commit Message:
SCUMM: RA1: Allow switching back to original input style
Changed paths:
engines/scumm/insane/rebel1/iact.cpp
engines/scumm/insane/rebel1/menu.cpp
engines/scumm/insane/rebel1/rebel.cpp
engines/scumm/insane/rebel1/rebel.h
engines/scumm/insane/rebel1/runlevels.cpp
diff --git a/engines/scumm/insane/rebel1/iact.cpp b/engines/scumm/insane/rebel1/iact.cpp
index 078bd883f7d..795917c5231 100644
--- a/engines/scumm/insane/rebel1/iact.cpp
+++ b/engines/scumm/insane/rebel1/iact.cpp
@@ -47,6 +47,12 @@ inline int16 smoothRebel1Op0BAnalogInput(int16 inputValue, int16 &filteredValue,
const int16 kRA1Op09AimXScale[5] = { 0, 44, 88, 128, 165 };
const int16 kRA1Op09AimYScale[5] = { 256, 252, 240, 221, 196 };
+const int kRA1DosMouseCenterX = 0x140;
+const int kRA1DosMouseCenterY = 100;
+const int kRA1DosMouseSafeLeft = 0x0AA;
+const int kRA1DosMouseSafeRight = 0x1D6;
+const int kRA1DosMouseSafeTop = 0x32;
+const int kRA1DosMouseSafeBottom = 0x96;
// Level 15 final approach 0x5D damage/event codes consumed by
// RunLevel1GameLoop. The latch stores the raw GAME parameter; no translation is
@@ -729,8 +735,9 @@ void InsaneRebel1::updateFlightVariantCursor() {
// preprocessMouseAxes â FUN_231BE (0x231BE) centered-axis output law, adapted to
// ScummVM's absolute 320x200 mouse space.
// Preserve the DOS bias/offset persistence and one-frame jump latch from
-// FUN_231BE, but avoid hard recentring the host mouse into the DOS safe window.
-// The actual frame-averaging behavior stays untouched.
+// FUN_231BE. Original-input mode also emulates FUN_23115's DOS mouse recenter;
+// gameplay hides and locks the cursor, so this follows the original without
+// exposing a visible pointer jump.
void InsaneRebel1::preprocessMouseAxes(int16 &inputX, int16 &inputY, bool *usedJoystick) {
if (usedJoystick)
*usedJoystick = false;
@@ -759,6 +766,7 @@ void InsaneRebel1::preprocessMouseAxes(int16 &inputX, int16 &inputY, bool *usedJ
if (_optControlsYFlip)
inputY = -inputY;
+ _mouseVirtualValid = false;
return;
}
@@ -777,6 +785,7 @@ void InsaneRebel1::preprocessMouseAxes(int16 &inputX, int16 &inputY, bool *usedJ
if (_optControlsYFlip)
inputY = -inputY;
+ _mouseVirtualValid = false;
return;
}
@@ -790,19 +799,60 @@ void InsaneRebel1::preprocessMouseAxes(int16 &inputX, int16 &inputY, bool *usedJ
if (_optControlsYFlip)
inputY = -inputY;
+ _mouseVirtualValid = false;
return;
}
int16 logicalX = (int16)CLIP<int>(_vm->_mouse.x, 0, 319);
int16 logicalY = (int16)CLIP<int>(_vm->_mouse.y, 0, 199);
- const int16 rawX = (int16)(logicalX << 1);
- const int16 rawY = logicalY;
- const int16 deltaX = (int16)(logicalX - kRA1CenterX);
- const int16 deltaY = (int16)(logicalY - kRA1CenterY);
- const int16 normX = (int16)(((int32)deltaX * 127) / 160);
- const int16 normY = (int16)(((int32)deltaY * 127) / 100);
- int16 biasX = (int16)((rawX + _mouseOffsetX - 0x140) >> 2);
- int16 biasY = (int16)((rawY + _mouseOffsetY - 100) >> 1);
+ int16 rawX = (int16)(logicalX << 1);
+ int16 rawY = logicalY;
+
+ if (!_optEnhancedControls) {
+ if (!_mouseVirtualValid) {
+ _mouseVirtualRawX = rawX;
+ _mouseVirtualRawY = rawY;
+ _mouseVirtualValid = true;
+ } else {
+ _mouseVirtualRawX = (int16)CLIP<int>(
+ _mouseVirtualRawX + ((logicalX - _mouseVirtualPrevLogicalX) << 1),
+ -32768, 32767);
+ _mouseVirtualRawY = (int16)CLIP<int>(
+ _mouseVirtualRawY + (logicalY - _mouseVirtualPrevLogicalY),
+ -32768, 32767);
+ }
+ _mouseVirtualPrevLogicalX = logicalX;
+ _mouseVirtualPrevLogicalY = logicalY;
+
+ rawX = _mouseVirtualRawX;
+ rawY = _mouseVirtualRawY;
+
+ if (rawX < kRA1DosMouseSafeLeft || rawX > kRA1DosMouseSafeRight ||
+ rawY < kRA1DosMouseSafeTop || rawY > kRA1DosMouseSafeBottom) {
+ _mouseOffsetX = (int16)CLIP<int>(
+ (int)_mouseOffsetX + rawX - kRA1DosMouseCenterX, -32768, 32767);
+ _mouseOffsetY = (int16)CLIP<int>(
+ (int)_mouseOffsetY + rawY - kRA1DosMouseCenterY, -32768, 32767);
+ rawX = kRA1DosMouseCenterX;
+ rawY = kRA1DosMouseCenterY;
+ _mouseVirtualRawX = rawX;
+ _mouseVirtualRawY = rawY;
+ _mouseVirtualPrevLogicalX = kRA1CenterX;
+ _mouseVirtualPrevLogicalY = kRA1CenterY;
+ _vm->_mouse.x = kRA1CenterX;
+ _vm->_mouse.y = kRA1CenterY;
+ smush_warpMouse(kRA1CenterX, kRA1CenterY, -1);
+ debug(2, "RA1 original input virtual recenter: offset=(%d,%d) mouse=(%d,%d)",
+ _mouseOffsetX, _mouseOffsetY, logicalX, logicalY);
+ }
+ } else {
+ _mouseVirtualValid = false;
+ }
+
+ const int16 normX = (int16)(((int32)(logicalX - kRA1CenterX) * 127) / 160);
+ const int16 normY = (int16)(((int32)(logicalY - kRA1CenterY) * 127) / 100);
+ int16 biasX = (int16)((rawX + _mouseOffsetX - kRA1DosMouseCenterX) >> 2);
+ int16 biasY = (int16)((rawY + _mouseOffsetY - kRA1DosMouseCenterY) >> 1);
if (biasY < 0x65) {
const bool largeJump =
@@ -825,8 +875,11 @@ void InsaneRebel1::preprocessMouseAxes(int16 &inputX, int16 &inputY, bool *usedJ
_mouseBiasLatch = true;
}
- const int16 scaledX = (int16)(normX + biasX);
- const int16 scaledY = (int16)(normY + biasY);
+ // In original mouse mode, FUN_2309C centers the joystick contribution when
+ // joystick is disabled, so FUN_231BE's mouse output is just the bias term.
+ // Enhanced mode keeps ScummVM's pre-existing centered mouse axis plus bias.
+ const int16 scaledX = _optEnhancedControls ? (int16)(normX + biasX) : biasX;
+ const int16 scaledY = _optEnhancedControls ? (int16)(normY + biasY) : biasY;
_mouseBiasX = biasX;
_mouseBiasY = biasY;
@@ -941,7 +994,8 @@ void InsaneRebel1::updateShipPhysics() {
// --- Step 4: Position accumulator deltas ---
// X delta: drift + slide coupling - cross-coupling
- int32 rng = _turbulenceEnabled ? (int32)_vm->_rnd.getRandomNumber(199) : 100; // 0-199, centered at 100
+ const bool originalTurbulence = !_optEnhancedControls;
+ int32 rng = originalTurbulence ? (int32)_vm->_rnd.getRandomNumber(199) : 100; // RandScaleByte(200), centered at 100
int32 crossTermX;
if (_liftSmooth < 0)
crossTermX = ((int32)_tuning.lift * _liftSmooth * _rollAccum) >> 11;
@@ -1016,8 +1070,9 @@ void InsaneRebel1::updateShipPhysics() {
if (_shipBank.numSprites > 0)
_shipDirIndex = CLIP<int16>((int16)(vComponent + hComponent), 0, _shipBank.numSprites - 1);
- debug(1, "RA1 ship input: frame=%d source=%s usedJoystick=%d raw=(%d,%d) clipped=(%d,%d) storedAxis=(%d,%d) actionState(L,R,U,D)=(%d,%d,%d,%d) roll=%d lift=%d pos=(%d,%d) view=(%d,%d) dir=%d level=%d mode=%d opcode=0x%X",
- _gameCounter, inputSourceName, usedJoystick,
+ debug(1, "RA1 ship input: frame=%d source=%s controls=%s turbulence=%d usedJoystick=%d raw=(%d,%d) clipped=(%d,%d) storedAxis=(%d,%d) actionState(L,R,U,D)=(%d,%d,%d,%d) roll=%d lift=%d pos=(%d,%d) view=(%d,%d) dir=%d level=%d mode=%d opcode=0x%X",
+ _gameCounter, inputSourceName,
+ _optEnhancedControls ? "enhanced" : "original", originalTurbulence, usedJoystick,
rawInputX, rawInputY, inputX, inputY,
_joystickAxisX, _joystickAxisY,
_vm->getActionState(kScummActionInsaneLeft),
@@ -1256,15 +1311,16 @@ void InsaneRebel1::updateTurretPhysics() {
const int16 rawInputX = inputX;
const int16 rawInputY = inputY;
- if (usedJoystick) {
+ if (usedJoystick && _optEnhancedControls) {
// First-person turret/cockpit stages are noticeably more sensitive on
// joystick than on mouse, so damp only the joystick-driven input here.
inputX /= 2;
inputY /= 2;
}
- debug("RA1 turret input: source=%s mouse=(%d,%d) actions(L,R,U,D)=(%d,%d,%d,%d) raw=(%d,%d) final=(%d,%d) level=%d mode=%d opcode=0x%X",
+ debug("RA1 turret input: source=%s controls=%s mouse=(%d,%d) actions(L,R,U,D)=(%d,%d,%d,%d) raw=(%d,%d) final=(%d,%d) level=%d mode=%d opcode=0x%X",
usedJoystick ? "joystick-actions" : "mouse-path",
+ _optEnhancedControls ? "enhanced" : "original",
_vm->_mouse.x, _vm->_mouse.y,
_vm->getActionState(kScummActionInsaneLeft),
_vm->getActionState(kScummActionInsaneRight),
@@ -1355,9 +1411,9 @@ void InsaneRebel1::updateScreenFlashPalette() {
// Ship position = averaged input + center offset.
// Viewport = second history buffer for smooth camera scrolling.
void InsaneRebel1::updateGameOp0BPhysics() {
- // Control feel tweak: original uses full 10-sample average in FUN_1CDA7.
- // We keep the same pipeline but average over fewer samples for responsiveness.
- const int kGameOp0BSmoothWindow = 2;
+ // Original FUN_1CDA7 uses the full 10-sample history. Enhanced controls keep
+ // the same pipeline but average fewer samples for responsiveness.
+ const int gameOp0BSmoothWindow = _optEnhancedControls ? 2 : kInputHistorySize;
// RA1 FUN_1B297-style per-frame latches for 0x0B sections:
// 0x5D latch 0xFFFF -> bit 0x40 (scripted obstacle/contact)
@@ -1491,7 +1547,7 @@ void InsaneRebel1::updateGameOp0BPhysics() {
else if (_activeInputSource == kInputSourceJoystickDigital)
inputSourceName = "joystick-dpad";
- if (usedJoystick) {
+ if (usedJoystick && _optEnhancedControls) {
// The 0x0B first-person handler is shared by multiple RA1 stages. Smooth
// analog stick input over time so these sections keep full reach without
// feeling hyper-sensitive, while leaving mouse behavior untouched.
@@ -1510,9 +1566,11 @@ void InsaneRebel1::updateGameOp0BPhysics() {
}
_inputAxisDeltaX = inputX;
- debug("RA1 GAME 0x0B input: frame=%d source=%s view=(%d,%d) health=%d prevFlags=0x%02x axis=(%d,%d) mouse=(%d,%d) actions(L,R,U,D)=(%d,%d,%d,%d) raw=(%d,%d) final=(%d,%d) level=%d opcode=0x%X",
+ debug("RA1 GAME 0x0B input: frame=%d source=%s controls=%s window=%d view=(%d,%d) health=%d prevFlags=0x%02x axis=(%d,%d) mouse=(%d,%d) actions(L,R,U,D)=(%d,%d,%d,%d) raw=(%d,%d) final=(%d,%d) level=%d opcode=0x%X",
_gameCounter,
inputSourceName,
+ _optEnhancedControls ? "enhanced" : "original",
+ gameOp0BSmoothWindow,
_perspectiveX, _perspectiveY,
_health, _prevDamageFlags,
_joystickAxisX, _joystickAxisY,
@@ -1533,13 +1591,13 @@ void InsaneRebel1::updateGameOp0BPhysics() {
int sumInputX = 0;
int sumInputY = 0;
- for (int i = 0; i < kGameOp0BSmoothWindow; i++) {
+ for (int i = 0; i < gameOp0BSmoothWindow; i++) {
sumInputX += _inputHistoryX[i];
sumInputY += _inputHistoryY[i];
}
- _avgInputX = (int16)(sumInputX / kGameOp0BSmoothWindow);
- _avgInputY = (int16)(-sumInputY / kGameOp0BSmoothWindow);
+ _avgInputX = (int16)(sumInputX / gameOp0BSmoothWindow);
+ _avgInputY = (int16)(-sumInputY / gameOp0BSmoothWindow);
_avgInputX = CLIP<int16>(_avgInputX, -0xA0, 0xA0);
_avgInputY = CLIP<int16>(_avgInputY, -0x46, 0x41);
@@ -1555,13 +1613,13 @@ void InsaneRebel1::updateGameOp0BPhysics() {
int sumViewX = 0;
int sumViewY = 0;
- for (int i = 0; i < kGameOp0BSmoothWindow; i++) {
+ for (int i = 0; i < gameOp0BSmoothWindow; i++) {
sumViewX += _viewHistoryX[i];
sumViewY += _viewHistoryY[i];
}
- int16 avgViewX = (int16)(sumViewX / kGameOp0BSmoothWindow);
- int16 avgViewY = (int16)(sumViewY / kGameOp0BSmoothWindow);
+ int16 avgViewX = (int16)(sumViewX / gameOp0BSmoothWindow);
+ int16 avgViewY = (int16)(sumViewY / gameOp0BSmoothWindow);
_perspectiveX = CLIP<int16>((int16)((avgViewX >> 1) + 0x20), 0, 0x40);
_perspectiveY = CLIP<int16>((int16)((avgViewY >> 1) + 0x17), 0, 0x2E);
resetProjectionTable();
@@ -1844,6 +1902,11 @@ void InsaneRebel1::handleGameChunk(int32 subSize, Common::SeekableReadStream &b)
_mousePrevBiasY = 0;
_mouseBiasLatch = false;
_mouseRecentering = false;
+ _mouseVirtualRawX = kRA1DosMouseCenterX;
+ _mouseVirtualRawY = kRA1DosMouseCenterY;
+ _mouseVirtualPrevLogicalX = kRA1CenterX;
+ _mouseVirtualPrevLogicalY = kRA1CenterY;
+ _mouseVirtualValid = false;
_level2JoystickFilteredX = 0;
_level2JoystickFilteredY = 0;
diff --git a/engines/scumm/insane/rebel1/menu.cpp b/engines/scumm/insane/rebel1/menu.cpp
index 22e5d5e0fcc..d491e849b2b 100644
--- a/engines/scumm/insane/rebel1/menu.cpp
+++ b/engines/scumm/insane/rebel1/menu.cpp
@@ -167,11 +167,11 @@ bool InsaneRebel1::handleControllerMenuAction(ScummAction action) {
_optionsSel = (_optionsSel + 1) % kOptionsItemCount;
return true;
case kScummActionInsaneLeft:
- if (_optionsSel == 6)
+ if (_optionsSel == 7)
setRebel1Volume(_vm, _optVolume, -5);
return true;
case kScummActionInsaneRight:
- if (_optionsSel == 6)
+ if (_optionsSel == 7)
setRebel1Volume(_vm, _optVolume, 5);
return true;
case kScummActionInsaneAttack:
@@ -402,14 +402,14 @@ bool InsaneRebel1::notifyEvent(const Common::Event &event) {
return true;
case Common::KEYCODE_LEFT:
case Common::KEYCODE_a:
- // Volume down when on volume row (row 6)
- if (_optionsSel == 6)
+ // Volume down when on volume row (row 7)
+ if (_optionsSel == 7)
setRebel1Volume(_vm, _optVolume, -5);
return true;
case Common::KEYCODE_RIGHT:
case Common::KEYCODE_d:
- // Volume up when on volume row (row 6)
- if (_optionsSel == 6)
+ // Volume up when on volume row (row 7)
+ if (_optionsSel == 7)
setRebel1Volume(_vm, _optVolume, 5);
return true;
case Common::KEYCODE_RETURN:
@@ -596,7 +596,8 @@ void InsaneRebel1::renderMainMenuOverlay(byte *dst, int pitch, int width, int he
_optMusicEnabled ? "MUSIC IS ON" : "MUSIC IS OFF",
_optSfxEnabled ? "SFX AND VOICE ARE ON" : "SFX AND VOICE ARE OFF",
_optTextEnabled ? "DIALOGUE TEXT IS ON" : "DIALOGUE TEXT IS OFF",
- _optControlsYFlip ? "CONTROLS ARE Y-FLIPPED" : "CONTROLS ARE NORMAL",
+ _optEnhancedControls ? "INPUT STYLE ENHANCED" : "INPUT STYLE ORIGINAL",
+ _optControlsYFlip ? "Y AXIS IS INVERTED" : "Y AXIS IS NORMAL",
volLine,
diffLine
};
@@ -746,12 +747,15 @@ void InsaneRebel1::runOptionsMenu() {
_optTextEnabled = !_optTextEnabled;
ConfMan.setBool("subtitles", _optTextEnabled);
break;
- case 5: // Toggle Y-flip controls
+ case 5: // Toggle enhanced/original input style
+ _optEnhancedControls = !_optEnhancedControls;
+ break;
+ case 6: // Toggle Y-flip controls
_optControlsYFlip = !_optControlsYFlip;
break;
- case 6: // Volume â adjusted via left/right in notifyEvent
+ case 7: // Volume â adjusted via left/right in notifyEvent
break;
- case 7: // Cycle difficulty
+ case 8: // Cycle difficulty
_difficulty = (_difficulty + 1) % 3;
loadTuningForLevel(0);
break;
diff --git a/engines/scumm/insane/rebel1/rebel.cpp b/engines/scumm/insane/rebel1/rebel.cpp
index 2815119bd10..39861afee1f 100644
--- a/engines/scumm/insane/rebel1/rebel.cpp
+++ b/engines/scumm/insane/rebel1/rebel.cpp
@@ -261,6 +261,11 @@ InsaneRebel1::InsaneRebel1(ScummEngine_v7 *scumm) : Insane(), _vm(scumm) {
_mousePrevBiasY = 0;
_mouseBiasLatch = false;
_mouseRecentering = false;
+ _mouseVirtualRawX = 0x140;
+ _mouseVirtualRawY = 100;
+ _mouseVirtualPrevLogicalX = kRA1CenterX;
+ _mouseVirtualPrevLogicalY = kRA1CenterY;
+ _mouseVirtualValid = false;
_joystickAxisX = 0;
_joystickAxisY = 0;
_lastJoystickAxisEventTime = 0;
@@ -326,13 +331,13 @@ InsaneRebel1::InsaneRebel1(ScummEngine_v7 *scumm) : Insane(), _vm(scumm) {
_levelSelectActive = false;
_levelSelectSel = 0;
_startLevel = 1;
- _turbulenceEnabled = true;
// Options â read initial state from ScummVM mixer
_optRookieOneFemale = false;
_optMusicEnabled = !_vm->_mixer->isSoundTypeMuted(Audio::Mixer::kMusicSoundType);
_optSfxEnabled = !_vm->_mixer->isSoundTypeMuted(Audio::Mixer::kSFXSoundType);
_optTextEnabled = true;
+ _optEnhancedControls = true;
_optControlsYFlip = false;
_optVolume = _vm->_mixer->getVolumeForSoundType(Audio::Mixer::kPlainSoundType) * 127 / Audio::Mixer::kMaxChannelVolume;
diff --git a/engines/scumm/insane/rebel1/rebel.h b/engines/scumm/insane/rebel1/rebel.h
index 7a5c1a7cbc3..c324a590dbf 100644
--- a/engines/scumm/insane/rebel1/rebel.h
+++ b/engines/scumm/insane/rebel1/rebel.h
@@ -279,6 +279,11 @@ private:
int16 _mousePrevBiasY; // 0x976E: previous-frame biasY
bool _mouseBiasLatch; // 0x4486: one-frame large-jump latch
bool _mouseRecentering; // 0x976D: suppress recursive updates during warp
+ int16 _mouseVirtualRawX; // Virtual DOS mouse X used by original-input recentering
+ int16 _mouseVirtualRawY; // Virtual DOS mouse Y used by original-input recentering
+ int16 _mouseVirtualPrevLogicalX;
+ int16 _mouseVirtualPrevLogicalY;
+ bool _mouseVirtualValid;
int16 _joystickAxisX; // Rebel-specific left-stick X captured from keymapper axis events
int16 _joystickAxisY; // Rebel-specific left-stick Y captured from keymapper axis events
uint32 _lastJoystickAxisEventTime;
@@ -452,9 +457,9 @@ private:
int _menuFrameCounter;
// Options submenu state â RunGameOptionsMenu (0x14B42)
- static const int kOptionsItemCount = 8;
+ static const int kOptionsItemCount = 9;
bool _optionsActive; // True when showing options instead of main menu
- int _optionsSel; // 0..7 selected option row
+ int _optionsSel; // 0..8 selected option row
bool _levelSelectActive; // True when showing level-select submenu
int _levelSelectSel; // 0=Level1 ... N-1=Back
int _startLevel; // 1-based start level for "Start New Game"
@@ -464,9 +469,9 @@ private:
bool _optMusicEnabled; // DAT_22b7: music on/off
bool _optSfxEnabled; // DAT_22b8: sfx+voice on/off
bool _optTextEnabled; // DAT_22b9: dialogue text on/off
+ bool _optEnhancedControls; // ScummVM option: current responsive controls vs original control law
bool _optControlsYFlip; // DAT_22be: Y-axis inversion
int _optVolume; // DAT_22c1: master volume 0..127
- bool _turbulenceEnabled; // Random per-frame jitter in deltaX (original has it on)
// High scores / TOP PILOTS display â data at DS:0x1D0
static const int kHighScoreCount = 10;
diff --git a/engines/scumm/insane/rebel1/runlevels.cpp b/engines/scumm/insane/rebel1/runlevels.cpp
index f42e283fff3..6f091cf8a9f 100644
--- a/engines/scumm/insane/rebel1/runlevels.cpp
+++ b/engines/scumm/insane/rebel1/runlevels.cpp
@@ -1994,6 +1994,11 @@ void InsaneRebel1::playInteractiveVideo(const char *filename, int32 startFrame)
// Center mouse, hide system cursor (we draw our own), lock mouse to window
smush_warpMouse(160, 100, -1);
+ _mouseVirtualRawX = 0x140;
+ _mouseVirtualRawY = 100;
+ _mouseVirtualPrevLogicalX = kRA1CenterX;
+ _mouseVirtualPrevLogicalY = kRA1CenterY;
+ _mouseVirtualValid = false;
CursorMan.showMouse(false);
g_system->lockMouse(true);
Commit: 559b588ebf7d9407479f23127c4020ff16e31b44
https://github.com/scummvm/scummvm/commit/559b588ebf7d9407479f23127c4020ff16e31b44
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:10:06+02:00
Commit Message:
SCUMM: RA1: Implement missing chunks
Changed paths:
engines/scumm/smush/rebel/smush_player_ra1.cpp
engines/scumm/smush/rebel/smush_player_ra1.h
diff --git a/engines/scumm/smush/rebel/smush_player_ra1.cpp b/engines/scumm/smush/rebel/smush_player_ra1.cpp
index 7754e319101..4d828c8356c 100644
--- a/engines/scumm/smush/rebel/smush_player_ra1.cpp
+++ b/engines/scumm/smush/rebel/smush_player_ra1.cpp
@@ -61,6 +61,64 @@ static void ra1ApplyCenteredFetchPlacement(InsaneRebel1 *rebel1, int width, int
top -= ((projectedTop - top) >> 2);
}
+static bool ra1EnsureBuffer(byte *&buffer, int32 &bufferSize, int32 neededSize) {
+ if (neededSize <= 0)
+ return false;
+ if (buffer != nullptr && bufferSize >= neededSize)
+ return true;
+
+ byte *newBuffer = (byte *)realloc(buffer, neededSize);
+ if (newBuffer == nullptr)
+ return false;
+
+ buffer = newBuffer;
+ bufferSize = neededSize;
+ return true;
+}
+
+static void ra1CopyFadeRun(byte *dst, const byte *src, int srcPitch, int width, int height,
+ int dstPos, int srcPos, int count) {
+ const int frameSize = width * height;
+ if (dstPos < 0 || srcPos < 0 || count <= 0 || dstPos >= frameSize || srcPos >= frameSize)
+ return;
+
+ count = MIN(count, frameSize - dstPos);
+ count = MIN(count, frameSize - srcPos);
+ while (count > 0) {
+ const int srcX = srcPos % width;
+ const int dstX = dstPos % width;
+ const int run = MIN(count, width - srcX);
+ const int rowRun = MIN(run, width - dstX);
+ memcpy(dst + dstPos, src + (srcPos / width) * srcPitch + srcX, rowRun);
+ srcPos += rowRun;
+ dstPos += rowRun;
+ count -= rowRun;
+ }
+}
+
+static void ra1RememberDisplayedFrame(byte *&buffer, int32 &bufferSize, int &storedWidth,
+ int &storedHeight, bool &valid, const byte *src, int pitch, int width, int height) {
+ if (src == nullptr || width <= 0 || height <= 0)
+ return;
+
+ const int32 neededSize = width * height;
+ if (!ra1EnsureBuffer(buffer, bufferSize, neededSize))
+ return;
+
+ if (buffer == src && pitch == width) {
+ // Already displaying the retained FADE buffer.
+ } else if (pitch == width) {
+ memcpy(buffer, src, neededSize);
+ } else {
+ for (int y = 0; y < height; ++y)
+ memcpy(buffer + y * width, src + y * pitch, width);
+ }
+
+ storedWidth = width;
+ storedHeight = height;
+ valid = true;
+}
+
SmushPlayerRebel1::SmushPlayerRebel1(ScummEngine_v7 *scumm, IMuseDigital *imuseDigital, Insane *insane)
: SmushPlayer(scumm, imuseDigital, insane) {
initGamePlayerFields();
@@ -86,6 +144,12 @@ void SmushPlayerRebel1::initGamePlayerFields() {
_ra1ViewportOffsetX = 0;
_ra1ViewportOffsetY = 0;
_ra1FrameSourceSkipY = 0;
+ _ra1FadeFrame = nullptr;
+ _ra1FadeFrameSize = 0;
+ _ra1FadeFrameWidth = 0;
+ _ra1FadeFrameHeight = 0;
+ _ra1FadeFrameValid = false;
+ _ra1UseFadeFrame = false;
}
void SmushPlayerRebel1::destroyGamePlayerFields() {
@@ -98,6 +162,9 @@ void SmushPlayerRebel1::destroyGamePlayerFields() {
free(_ra1PresentationBuffer);
_ra1PresentationBuffer = nullptr;
_ra1PresentationBufferSize = 0;
+ free(_ra1FadeFrame);
+ _ra1FadeFrame = nullptr;
+ _ra1FadeFrameSize = 0;
}
void SmushPlayerRebel1::resetGameVideoState() {
@@ -112,6 +179,7 @@ void SmushPlayerRebel1::resetGameVideoState() {
_ra1ObjOverlayHeight = 0;
_ra1ViewportOffsetX = 0;
_ra1ViewportOffsetY = 0;
+ _ra1UseFadeFrame = false;
}
void SmushPlayerRebel1::releaseGameVideoState() {
@@ -137,6 +205,14 @@ void SmushPlayerRebel1::releaseGameVideoState() {
free(_ra1PresentationBuffer);
_ra1PresentationBuffer = nullptr;
_ra1PresentationBufferSize = 0;
+
+ free(_ra1FadeFrame);
+ _ra1FadeFrame = nullptr;
+ _ra1FadeFrameSize = 0;
+ _ra1FadeFrameWidth = 0;
+ _ra1FadeFrameHeight = 0;
+ _ra1FadeFrameValid = false;
+ _ra1UseFadeFrame = false;
}
bool SmushPlayerRebel1::handleGameFetch(int32 subSize, Common::SeekableReadStream &b) {
@@ -250,7 +326,140 @@ SmushFont *SmushPlayerRebel1::getGameFont(int font) {
}
void SmushPlayerRebel1::adjustGamePalette() {
+ for (int i = 0; i < ARRAYSIZE(_pal); ++i)
+ _shiftedDeltaPal[i] = _pal[i] << 7;
+ memset(_deltaPal, 0, sizeof(_deltaPal));
+ _pal[0] = _pal[1] = _pal[2] = 0;
+}
+
+void SmushPlayerRebel1::ra1HandleDeltaPalette(int32 subSize, Common::SeekableReadStream &b) {
+ if (subSize < 4) {
+ b.skip(subSize);
+ return;
+ }
+
+ const uint32 command = b.readUint32BE();
+ const int32 payloadBytes = subSize - 4;
+
+ if (command == 0 || command == 2) {
+ _deltaPal[0] = 0;
+ _shiftedDeltaPal[0] = 0;
+ int32 remaining = payloadBytes;
+ if (remaining >= 2) {
+ // The original loop starts at palette component 1, leaving component
+ // 0 black and ignoring the first delta word in the XPAL payload.
+ b.skip(2);
+ remaining -= 2;
+ }
+
+ for (int i = 1; i < ARRAYSIZE(_pal); ++i) {
+ _shiftedDeltaPal[i] = _pal[i] << 7;
+ if (remaining >= 2) {
+ _deltaPal[i] = b.readSint16LE();
+ remaining -= 2;
+ } else {
+ _deltaPal[i] = 0;
+ }
+ }
+
+ if (remaining > 0)
+ b.skip(remaining);
+
+ // Command 2 in the DOS dispatcher first restores the palette state before
+ // loading a new delta table. ScummVM keeps the active palette in _pal, so
+ // marking it dirty is the corresponding visible-side effect.
+ if (command == 2)
+ setDirtyColors(0, 255);
+ return;
+ }
+
+ if (payloadBytes > 0)
+ b.skip(payloadBytes);
+
+ for (int i = 1; i < ARRAYSIZE(_pal); ++i) {
+ _shiftedDeltaPal[i] += _deltaPal[i];
+ _pal[i] = CLIP<int32>(_shiftedDeltaPal[i] >> 7, 0, 255);
+ }
_pal[0] = _pal[1] = _pal[2] = 0;
+ setDirtyColors(0, 255);
+}
+
+void SmushPlayerRebel1::ra1HandleFade(int32 subSize, Common::SeekableReadStream &b) {
+ if (subSize <= 24 || _dst == nullptr || _width <= 0 || _height <= 0) {
+ b.skip(subSize);
+ return;
+ }
+
+ byte *fadeData = (byte *)malloc(subSize);
+ if (fadeData == nullptr) {
+ b.skip(subSize);
+ return;
+ }
+ b.read(fadeData, subSize);
+
+ int fadeWidth = kRA1PresentationScreenWidth;
+ int fadeHeight = kRA1PresentationScreenHeight;
+ if (subSize >= 16 && READ_BE_UINT32(fadeData) == MKTAG('F','D','H','D')) {
+ const int headerWidth = READ_LE_UINT16(fadeData + 12);
+ const int headerHeight = READ_LE_UINT16(fadeData + 14);
+ if (headerWidth > 0 && headerHeight > 0) {
+ fadeWidth = headerWidth;
+ fadeHeight = headerHeight;
+ }
+ }
+
+ fadeWidth = MIN(fadeWidth, MIN(_vm->_screenWidth, _width - _scrollX));
+ fadeHeight = MIN(fadeHeight, MIN(_vm->_screenHeight, _height - _scrollY));
+ if (fadeWidth <= 0 || fadeHeight <= 0) {
+ free(fadeData);
+ return;
+ }
+
+ const int32 fadeFrameSize = fadeWidth * fadeHeight;
+ if (!ra1EnsureBuffer(_ra1FadeFrame, _ra1FadeFrameSize, fadeFrameSize)) {
+ free(fadeData);
+ return;
+ }
+
+ if (!_ra1FadeFrameValid ||
+ _ra1FadeFrameWidth != fadeWidth || _ra1FadeFrameHeight != fadeHeight) {
+ memset(_ra1FadeFrame, 0, fadeFrameSize);
+ _ra1FadeFrameValid = true;
+ }
+ _ra1FadeFrameWidth = fadeWidth;
+ _ra1FadeFrameHeight = fadeHeight;
+
+ const byte *control = fadeData + 24;
+ int32 remaining = subSize - 24;
+ const byte *src = _dst + _scrollY * _width + _scrollX;
+ int srcPos = 0;
+ int dstPos = 0;
+
+ while (remaining > 0 && dstPos < fadeFrameSize && srcPos < fadeFrameSize) {
+ byte op = *control++;
+ remaining--;
+
+ int count = op & 0x7F;
+ if (count == 0) {
+ if (remaining < 2)
+ break;
+ count = READ_LE_UINT16(control);
+ control += 2;
+ remaining -= 2;
+ }
+
+ if (op & 0x80) {
+ srcPos += count;
+ dstPos += count;
+ } else {
+ ra1CopyFadeRun(_ra1FadeFrame, src, _width, fadeWidth, fadeHeight, dstPos, srcPos, count);
+ srcPos += count;
+ dstPos += count;
+ }
+ }
+
+ _ra1UseFadeFrame = true;
+ free(fadeData);
}
bool SmushPlayerRebel1::handleGameAnimHeader(byte *headerContent) {
@@ -734,7 +943,8 @@ static bool ra1FrameHasGameChunk(Common::SeekableReadStream &b, int32 frameSize)
if (subType == MKTAG('F', 'R', 'M', 'E'))
break;
- if (subType == MKTAG('G', 'A', 'M', 'E')) {
+ if (subType == MKTAG('G', 'A', 'M', 'E') ||
+ subType == MKTAG('G', 'A', 'M', '2')) {
b.seek(frameStart, SEEK_SET);
return true;
}
@@ -843,7 +1053,7 @@ void SmushPlayerRebel1::handleFrame(int32 frameSize, Common::SeekableReadStream
handleTextResource(subType, subSize, b);
break;
case MKTAG('X','P','A','L'):
- handleDeltaPalette(subSize, b);
+ ra1HandleDeltaPalette(subSize, b);
break;
case MKTAG('I','A','C','T'):
handleIACT(subSize, b);
@@ -860,7 +1070,8 @@ void SmushPlayerRebel1::handleFrame(int32 frameSize, Common::SeekableReadStream
case MKTAG('G','O','S','T'):
handleGameGost(subSize, b);
break;
- case MKTAG('G','A','M','E'): {
+ case MKTAG('G','A','M','E'):
+ case MKTAG('G','A','M','2'): {
InsaneRebel1 *rebel1 = (InsaneRebel1 *)_insane;
rebel1->handleGameChunk(subSize, b);
break;
@@ -880,6 +1091,7 @@ void SmushPlayerRebel1::handleFrame(int32 frameSize, Common::SeekableReadStream
bool recognized = (embTag == MKTAG('F','O','B','J') ||
embTag == MKTAG('G','A','M','E') ||
+ embTag == MKTAG('G','A','M','2') ||
embTag == MKTAG('P','S','A','D'));
if (!recognized || embSize > (uint32)embRemaining) {
@@ -903,7 +1115,7 @@ void SmushPlayerRebel1::handleFrame(int32 frameSize, Common::SeekableReadStream
_ra1ObjOverlayWidth = READ_LE_UINT16(objBuf + objPos + 14);
_ra1ObjOverlayHeight = READ_LE_UINT16(objBuf + objPos + 16);
}
- } else if (embTag == MKTAG('G','A','M','E')) {
+ } else if (embTag == MKTAG('G','A','M','E') || embTag == MKTAG('G','A','M','2')) {
Common::MemoryReadStream embStream(objBuf + objPos + 8, embSize);
InsaneRebel1 *rebel1 = (InsaneRebel1 *)_insane;
rebel1->handleGameChunk(embSize, embStream);
@@ -925,8 +1137,9 @@ void SmushPlayerRebel1::handleFrame(int32 frameSize, Common::SeekableReadStream
frameSize = 0;
continue;
}
- case MKTAG('G','A','M','2'):
case MKTAG('F','A','D','E'):
+ ra1HandleFade(subSize, b);
+ break;
case MKTAG('S','E','G','A'):
case MKTAG('A','D','L',' '):
case MKTAG('A','D','L','2'):
@@ -993,21 +1206,37 @@ void SmushPlayerRebel1::handleGameUpdateScreen(const byte *src, int srcPitch, in
if (_dst == nullptr || _width <= 0 || _height <= 0)
return;
+ const bool useFadeFrame = _ra1UseFadeFrame && _ra1FadeFrameValid && _ra1FadeFrame != nullptr;
+ if (useFadeFrame) {
+ src = _ra1FadeFrame;
+ srcPitch = _ra1FadeFrameWidth;
+ width = MIN(width, _ra1FadeFrameWidth);
+ height = MIN(height, _ra1FadeFrameHeight);
+ }
+
if (!_insane || !static_cast<InsaneRebel1 *>(_insane)->isInteractiveVideoActive() ||
_vm->_screenWidth != kRA1PresentationScreenWidth ||
_vm->_screenHeight != kRA1PresentationScreenHeight) {
SmushPlayer::handleGameUpdateScreen(src, srcPitch, width, height);
+ ra1RememberDisplayedFrame(_ra1FadeFrame, _ra1FadeFrameSize,
+ _ra1FadeFrameWidth, _ra1FadeFrameHeight, _ra1FadeFrameValid,
+ src, srcPitch, width, height);
+ _ra1UseFadeFrame = false;
return;
}
int ra1ViewX = _ra1ViewportOffsetX;
int ra1ViewY = _ra1ViewportOffsetY;
- const int srcX = CLIP(_scrollX + ra1ViewX + kRA1PresentationBorder, 0, _width - 1);
- const int srcY = CLIP(_scrollY + ra1ViewY + kRA1PresentationBorder, 0, _height - 1);
+ const byte *sourceBase = useFadeFrame ? src : _dst;
+ const int sourcePitch = useFadeFrame ? srcPitch : _width;
+ const int sourceWidth = useFadeFrame ? width : _width;
+ const int sourceHeight = useFadeFrame ? height : _height;
+ const int srcX = useFadeFrame ? 0 : CLIP(_scrollX + ra1ViewX + kRA1PresentationBorder, 0, sourceWidth - 1);
+ const int srcY = useFadeFrame ? 0 : CLIP(_scrollY + ra1ViewY + kRA1PresentationBorder, 0, sourceHeight - 1);
- int frameWidth = MIN<int>(_width - srcX, kRA1PresentationWidth);
- int frameHeight = MIN<int>(_height - srcY, kRA1PresentationHeight);
+ int frameWidth = MIN<int>(sourceWidth - srcX, kRA1PresentationWidth);
+ int frameHeight = MIN<int>(sourceHeight - srcY, kRA1PresentationHeight);
if (frameWidth <= 0 || frameHeight <= 0)
return;
@@ -1023,16 +1252,21 @@ void SmushPlayerRebel1::handleGameUpdateScreen(const byte *src, int srcPitch, in
// ResetPlaybackViewport() (0x20A53) initializes the interactive draw window
// to (4,4,312,192), leaving a black presentation frame around cockpit scenes.
- const byte *dst = _dst + srcY * _width + srcX;
+ const byte *dst = sourceBase + srcY * sourcePitch + srcX;
byte *presentationDst = _ra1PresentationBuffer +
kRA1PresentationBorder * kRA1PresentationScreenWidth + kRA1PresentationBorder;
for (int y = 0; y < frameHeight; y++) {
memcpy(presentationDst + y * kRA1PresentationScreenWidth,
- dst + y * _width, frameWidth);
+ dst + y * sourcePitch, frameWidth);
}
SmushPlayer::handleGameUpdateScreen(_ra1PresentationBuffer,
kRA1PresentationScreenWidth, kRA1PresentationScreenWidth, kRA1PresentationScreenHeight);
+ ra1RememberDisplayedFrame(_ra1FadeFrame, _ra1FadeFrameSize,
+ _ra1FadeFrameWidth, _ra1FadeFrameHeight, _ra1FadeFrameValid,
+ _ra1PresentationBuffer, kRA1PresentationScreenWidth,
+ kRA1PresentationScreenWidth, kRA1PresentationScreenHeight);
+ _ra1UseFadeFrame = false;
}
SmushFont *SmushPlayerRebel1::ra1GetFont(int font) {
diff --git a/engines/scumm/smush/rebel/smush_player_ra1.h b/engines/scumm/smush/rebel/smush_player_ra1.h
index 3cd067fc794..5d54cfd0b1a 100644
--- a/engines/scumm/smush/rebel/smush_player_ra1.h
+++ b/engines/scumm/smush/rebel/smush_player_ra1.h
@@ -60,6 +60,8 @@ protected:
private:
void ra1HandleGost(int32 subSize, Common::SeekableReadStream &b);
+ void ra1HandleDeltaPalette(int32 subSize, Common::SeekableReadStream &b);
+ void ra1HandleFade(int32 subSize, Common::SeekableReadStream &b);
SmushFont *ra1GetFont(int font);
void ra1HandleText(int32 subSize, Common::SeekableReadStream &b);
@@ -85,6 +87,15 @@ private:
int _ra1ViewportOffsetX;
int _ra1ViewportOffsetY;
int _ra1FrameSourceSkipY;
+
+ // RA1 FADE chunks update the visible 320x200 screen through a sparse
+ // copy mask, separate from the decoded frame buffer.
+ byte *_ra1FadeFrame;
+ int32 _ra1FadeFrameSize;
+ int _ra1FadeFrameWidth;
+ int _ra1FadeFrameHeight;
+ bool _ra1FadeFrameValid;
+ bool _ra1UseFadeFrame;
};
} // End of namespace Scumm
Commit: c47a0c95a4998167a1feed43024f286d8b8f1ca5
https://github.com/scummvm/scummvm/commit/c47a0c95a4998167a1feed43024f286d8b8f1ca5
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:10:06+02:00
Commit Message:
SCUMM: RA1: Improve HandleGameOp19 accuracy
Changed paths:
engines/scumm/insane/rebel1/iact.cpp
engines/scumm/insane/rebel1/rebel.h
diff --git a/engines/scumm/insane/rebel1/iact.cpp b/engines/scumm/insane/rebel1/iact.cpp
index 795917c5231..91c31357ed1 100644
--- a/engines/scumm/insane/rebel1/iact.cpp
+++ b/engines/scumm/insane/rebel1/iact.cpp
@@ -90,6 +90,18 @@ inline bool hasLevel15FinalSweepDamage(uint16 frameCounter, int16 perspectiveX)
}
}
+inline bool ra1DispatcherHudOnlyWhenDisabled(uint32 opcode) {
+ switch (opcode) {
+ case 0x07:
+ case 0x08:
+ case 0x0B:
+ case 0x1A:
+ return true;
+ default:
+ return false;
+ }
+}
+
inline bool isLevel2DamageLatch(uint16 code) {
switch (code) {
case 0x0003:
@@ -1724,7 +1736,8 @@ void InsaneRebel1::updateGameOp0BPhysics() {
}
-// updateOnFootPhysics â HandleGameOp19_OnFootSequence (0x19) + HandleGameOp1A_OnFootVariant (0x1A).
+// updateOnFootPhysics â ScummVM-side glue for original on-foot GAME handlers
+// HandleGameOp19_OnFootSequence (0x19) and HandleGameOp1A_OnFootVariant (0x1A).
// On-foot handler for Level 9 (Stormtroopers). Character walks left/right, crosshair tracks mouse.
//
// Original has TWO separate variable pairs:
@@ -1735,7 +1748,9 @@ void InsaneRebel1::updateGameOp0BPhysics() {
const int16 kOnFootCenterX = 0xA3; // g_perspectiveX in HandleGameOp19
const int16 kOnFootCenterY = 0x82; // g_perspectiveY in HandleGameOp19
-void InsaneRebel1::updateOnFootPhysics() {
+// Port split matching HandleGameOp19_OnFootSequence. The helper name is new to
+// this implementation; the original code dispatches the opcode handler directly.
+void InsaneRebel1::updateOnFootSequence() {
// --- First-frame initialization (0x19 counter==0) ---
if (!_onFootInitialized) {
_onFootInitialized = true;
@@ -1796,15 +1811,6 @@ void InsaneRebel1::updateOnFootPhysics() {
_shipDirIndex = 4; // Walk left
}
- // --- 0x1A: Crosshair positioning (HandleGameOp1A_OnFootVariant) ---
- // shipPosX/Y = mouse_input + crosshair_center + character_offset
- int16 inputX = 0, inputY = 0;
- preprocessMouseAxes(inputX, inputY);
- inputX = CLIP<int16>(inputX, -100, 100);
- int16 inputYNeg = CLIP<int16>((int16)(-inputY), -0x4B, 0x0F);
- _shipPosX = inputX + kOnFootCenterX + _onFootCharX;
- _shipPosY = inputYNeg + kOnFootCenterY + _onFootCharY - 0x32;
-
// --- Scripted damage latches â damageFlags (matching FUN_1B297 pattern) ---
// GAME 0x5D/0x5F set latches; convert to damage flags before the check.
if (_gameLatch5D == 0xFFFF)
@@ -1814,9 +1820,10 @@ void InsaneRebel1::updateOnFootPhysics() {
_damageFlags |= 0x80;
// --- Damage handling (from HandleGameOp19_OnFootSequence) ---
- // On-foot uses single tuning value (DAT_00001b29 offset = miss) for all damage types.
+ // On-foot damage uses the same heavy-damage tuning byte as ship shot/collision
+ // damage in the original, not the miss penalty.
if (_damageFlags != 0 && _damageCooldown == 0 && _health >= 0 && _deathTimer < 1) {
- _health -= _tuning.miss;
+ _health -= _tuning.shot;
if (_health < 0) {
_deathTimer = 15;
_deathCauseIndicator = (_damageFlags & 0x80) ? 2 : 1;
@@ -1826,6 +1833,32 @@ void InsaneRebel1::updateOnFootPhysics() {
playSfx(kSfxBoom, 127, 0);
_screenFlash = 5;
}
+}
+
+// Port split matching HandleGameOp1A_OnFootVariant. The helper name is new to
+// this implementation; the original code dispatches the opcode handler directly.
+void InsaneRebel1::updateOnFootAimVariant() {
+ // --- 0x1A: Crosshair positioning (HandleGameOp1A_OnFootVariant) ---
+ // shipPosX/Y = mouse_input + crosshair_center + character_offset
+ int16 inputX = 0, inputY = 0;
+ preprocessMouseAxes(inputX, inputY);
+ inputX = CLIP<int16>(inputX, -100, 100);
+ int16 inputYNeg = CLIP<int16>((int16)(-inputY), -0x4B, 0x0F);
+ _shipPosX = inputX + kOnFootCenterX + _onFootCharX;
+ _shipPosY = inputYNeg + kOnFootCenterY + _onFootCharY - 0x32;
+}
+
+void InsaneRebel1::updateOnFootPhysics() {
+ const bool haveFrameGameOpcodes = (_frameGameOpcodeMask != 0);
+ const bool sequenceOpcode =
+ hasFrameGameOpcode(0x19) || (!haveFrameGameOpcodes && _activeGameOpcode == 0x19);
+ const bool aimOpcode =
+ hasFrameGameOpcode(0x1A) || (!haveFrameGameOpcodes && _activeGameOpcode == 0x1A);
+
+ if (sequenceOpcode)
+ updateOnFootSequence();
+ if (aimOpcode)
+ updateOnFootAimVariant();
if (_damageCooldown > 0)
_damageCooldown--;
@@ -1870,10 +1903,31 @@ void InsaneRebel1::handleGameChunk(int32 subSize, Common::SeekableReadStream &b)
uint32 opcode = b.readUint32BE();
uint32 param1 = b.readUint32BE();
+ // FUN_1BE1B applies two global gates before the opcode switch. Bit 0 of
+ // g_combatModeFlags skips gameplay dispatch entirely; bit 5 of g_hudDisableFlags
+ // suppresses the handlers while still requesting HUD refresh for a few opcodes.
+ if (_gameplayFlags75ff & 1) {
+ debug(7, "RA1 GAME 0x%02x: skipped by combat mode flags=0x%02x",
+ opcode, _gameplayFlags75ff);
+ return;
+ }
+ if (_gameplayFlags75fe & 0x20) {
+ if (ra1DispatcherHudOnlyWhenDisabled(opcode))
+ _hudRenderFlag = 0xFF;
+ debug(7, "RA1 GAME 0x%02x: skipped by HUD disable flags=0x%02x",
+ opcode, _gameplayFlags75fe);
+ return;
+ }
+
switch (opcode) {
case 0x5E:
// RA1 dispatcher inline reset/init path (FUN_1BE1B case 0x5E).
// This is not a pure control-mode assignment.
+ if (_frameDispatchFlags & 0x40) {
+ debug(7, "RA1 GAME 0x5E: reset suppressed by dispatch flags=0x%02x",
+ _frameDispatchFlags);
+ break;
+ }
_damageFlags = 0;
_prevDamageFlags = 0;
_damageCooldown = 0;
diff --git a/engines/scumm/insane/rebel1/rebel.h b/engines/scumm/insane/rebel1/rebel.h
index c324a590dbf..d285f398e2d 100644
--- a/engines/scumm/insane/rebel1/rebel.h
+++ b/engines/scumm/insane/rebel1/rebel.h
@@ -303,6 +303,8 @@ private:
// 0x19/0x1A on-foot handler (Level 9 Stormtroopers)
void updateOnFootPhysics();
+ void updateOnFootSequence();
+ void updateOnFootAimVariant();
int16 _onFootCharX; // Character draw X (g_shipOffsetX in original)
int16 _onFootCharY; // Character draw Y (g_shipOffsetY in original)
int16 _onFootAnimCounter; // DAT_0000828a: fire animation counter
Commit: e62919da5be6076fb748291ed2fa3907326216f3
https://github.com/scummvm/scummvm/commit/e62919da5be6076fb748291ed2fa3907326216f3
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:10:07+02:00
Commit Message:
SCUMM: RA1: Refactor laser rendering
Changed paths:
engines/scumm/insane/rebel1/render.cpp
diff --git a/engines/scumm/insane/rebel1/render.cpp b/engines/scumm/insane/rebel1/render.cpp
index 9cb9c810bfb..7b552325774 100644
--- a/engines/scumm/insane/rebel1/render.cpp
+++ b/engines/scumm/insane/rebel1/render.cpp
@@ -418,6 +418,54 @@ void renderSpriteWithFlags(byte *dst, int pitch, int width, int height,
}
}
+// ScummVM-side helper only: the original keeps this shot-sprite math inline in
+// several GAME handlers. It is collapsed here because the direction/lerp/render
+// sequence is identical for one-beam shot sprites.
+void renderAimedShotSprite(byte *dst, int pitch, int width, int height,
+ const RA1SpriteBank &laserBank, int startX, int startY, int targetX, int targetY,
+ int lerp) {
+ if (laserBank.numSprites <= 0)
+ return;
+
+ const int dir = ra1ShotDirection((int16)startX, (int16)startY, (int16)targetX, (int16)targetY);
+ const int sprIdx = MIN<int>(ABS(dir), laserBank.numSprites - 1);
+ const uint32 flags = 0x83 | ((dir < 0) ? 0x2000 : 0);
+ const int interpX = startX + (((targetX - startX) * lerp) >> 3);
+ const int interpY = startY + (((targetY - startY) * lerp) >> 3);
+
+ renderSpriteWithFlags(dst, pitch, width, height,
+ interpX, interpY, laserBank.sprites[sprIdx], flags);
+}
+
+void renderAimedShotPair(byte *dst, int pitch, int width, int height,
+ const RA1SpriteBank &laserBank, int start1X, int start1Y, int start2X, int start2Y,
+ int targetX, int targetY, int lerp) {
+ renderAimedShotSprite(dst, pitch, width, height, laserBank,
+ start1X, start1Y, targetX, targetY, lerp);
+ renderAimedShotSprite(dst, pitch, width, height, laserBank,
+ start2X, start2Y, targetX, targetY, lerp);
+}
+
+// ScummVM-side helper for the 0x0B fallback edge-beam path. The original keeps
+// the bucket lookup inline with the renderer, but both left/right beams share it.
+void renderBucketedShotSprite(byte *dst, int pitch, int width, int height,
+ const RA1SpriteBank &laserBank, int startX, int startY, int targetX, int targetY,
+ int lerp, int frame, uint32 flags) {
+ if (laserBank.numSprites <= 0)
+ return;
+
+ const int dir = ra1ShotDirection((int16)startX, (int16)startY, (int16)targetX, (int16)targetY);
+ const int sprIdx = frame + ra1ShotDirectionBucket(dir);
+ if (sprIdx < 0 || sprIdx >= laserBank.numSprites)
+ return;
+
+ const int interpX = startX + (((targetX - startX) * lerp) >> 3);
+ const int interpY = startY + (((targetY - startY) * lerp) >> 3);
+
+ renderSpriteWithFlags(dst, pitch, width, height,
+ interpX, interpY, laserBank.sprites[sprIdx], flags);
+}
+
void InsaneRebel1::getTurretShipCenter(int16 &x, int16 &y) const {
if (_currentLevel == 0 && _flyControlMode == 2) {
// Port helper: original Level 1 part 2 keeps g_perspectiveX/Y at the
@@ -1021,16 +1069,10 @@ void InsaneRebel1::renderLaserShots(byte *dst, int pitch, int width, int height)
const int srcY = _shotSlots[i].centerY;
const int dstX = _shotSlots[i].posX;
const int dstY = _shotSlots[i].posY;
- const int dir = ra1ShotDirection((int16)srcX, (int16)srcY,
- (int16)dstX, (int16)dstY);
- const int sprIdx = MIN<int>(ABS(dir), _laserBank.numSprites - 1);
- const uint32 flags = 0x83 | ((dir < 0) ? 0x2000 : 0);
const int onFootLerp = kOnFootShotLerp[timer];
- const int interpX = srcX + (((dstX - srcX) * onFootLerp) >> 3);
- const int interpY = srcY + (((dstY - srcY) * onFootLerp) >> 3);
- renderSpriteWithFlags(dst, pitch, width, height,
- interpX, interpY, _laserBank.sprites[sprIdx], flags);
+ renderAimedShotSprite(dst, pitch, width, height, _laserBank,
+ srcX, srcY, dstX, dstY, onFootLerp);
continue;
}
@@ -1065,21 +1107,8 @@ void InsaneRebel1::renderLaserShots(byte *dst, int pitch, int width, int height)
if (!haveEmitters)
continue;
- const int dir1 = ra1ShotDirection((int16)start1X, (int16)start1Y, (int16)targetX, (int16)targetY);
- const int dir2 = ra1ShotDirection((int16)start2X, (int16)start2Y, (int16)targetX, (int16)targetY);
- const int sprIdx1 = MIN<int>(ABS(dir1), _laserBank.numSprites - 1);
- const int sprIdx2 = MIN<int>(ABS(dir2), _laserBank.numSprites - 1);
- const uint32 flags1 = 0x83 | ((dir1 < 0) ? 0x2000 : 0);
- const uint32 flags2 = 0x83 | ((dir2 < 0) ? 0x2000 : 0);
- const int interp1X = start1X + (((targetX - start1X) * lerp) >> 3);
- const int interp1Y = start1Y + (((targetY - start1Y) * lerp) >> 3);
- const int interp2X = start2X + (((targetX - start2X) * lerp) >> 3);
- const int interp2Y = start2Y + (((targetY - start2Y) * lerp) >> 3);
-
- renderSpriteWithFlags(dst, pitch, width, height,
- interp1X, interp1Y, _laserBank.sprites[sprIdx1], flags1);
- renderSpriteWithFlags(dst, pitch, width, height,
- interp2X, interp2Y, _laserBank.sprites[sprIdx2], flags2);
+ renderAimedShotPair(dst, pitch, width, height, _laserBank,
+ start1X, start1Y, start2X, start2Y, targetX, targetY, lerp);
continue;
}
@@ -1099,17 +1128,6 @@ void InsaneRebel1::renderLaserShots(byte *dst, int pitch, int width, int height)
const int start1Y = shipBaseY + emit.y1;
const int start2X = shipBaseX + emit.x2;
const int start2Y = shipBaseY + emit.y2;
- const int dir1 = ra1ShotDirection((int16)start1X, (int16)start1Y, (int16)targetX, (int16)targetY);
- const int dir2 = ra1ShotDirection((int16)start2X, (int16)start2Y, (int16)targetX, (int16)targetY);
- const int sprIdx1 = MIN<int>(ABS(dir1), _laserBank.numSprites - 1);
- const int sprIdx2 = MIN<int>(ABS(dir2), _laserBank.numSprites - 1);
- const uint32 flags1 = 0x83 | ((dir1 < 0) ? 0x2000 : 0);
- const uint32 flags2 = 0x83 | ((dir2 < 0) ? 0x2000 : 0);
- const int interp1X = start1X + (((targetX - start1X) * lerp) >> 3);
- const int interp1Y = start1Y + (((targetY - start1Y) * lerp) >> 3);
- const int interp2X = start2X + (((targetX - start2X) * lerp) >> 3);
- const int interp2Y = start2Y + (((targetY - start2Y) * lerp) >> 3);
-
if (_currentLevel == 4) {
debug(1, "RA1 op09 shotRender: frame=%d timer=%d shipBase=(%d,%d) target=(%d,%d) emit1=(%d,%d) emit2=(%d,%d) dir=%d variant=%d mode=%d",
_gameCounter, timer, shipBaseX, shipBaseY, targetX, targetY,
@@ -1117,10 +1135,8 @@ void InsaneRebel1::renderLaserShots(byte *dst, int pitch, int width, int height)
_shotSlots[i].variant, _flyControlMode);
}
- renderSpriteWithFlags(dst, pitch, width, height,
- interp1X, interp1Y, _laserBank.sprites[sprIdx1], flags1);
- renderSpriteWithFlags(dst, pitch, width, height,
- interp2X, interp2Y, _laserBank.sprites[sprIdx2], flags2);
+ renderAimedShotPair(dst, pitch, width, height, _laserBank,
+ start1X, start1Y, start2X, start2Y, targetX, targetY, lerp);
continue;
}
@@ -1159,25 +1175,10 @@ void InsaneRebel1::renderLaserShots(byte *dst, int pitch, int width, int height)
const int startLeftX = overlayX + leftStartX;
const int startRightX = overlayX + rightStartX;
- const int dirLeft = ra1ShotDirection((int16)startLeftX, (int16)leftStartY, (int16)targetX, (int16)targetY);
- const int dirRight = ra1ShotDirection((int16)startRightX, (int16)rightStartY, (int16)targetX, (int16)targetY);
- const int bucketLeft = ra1ShotDirectionBucket(dirLeft);
- const int bucketRight = ra1ShotDirectionBucket(dirRight);
- const int sprIdxLeft = frame + bucketLeft;
- const int sprIdxRight = frame + bucketRight;
- const int interpLeftX = startLeftX + (((targetX - startLeftX) * lerp) >> 3);
- const int interpLeftY = leftStartY + (((targetY - leftStartY) * lerp) >> 3);
- const int interpRightX = startRightX + (((targetX - startRightX) * lerp) >> 3);
- const int interpRightY = rightStartY + (((targetY - rightStartY) * lerp) >> 3);
-
- if (sprIdxLeft >= 0 && sprIdxLeft < _laserBank.numSprites) {
- renderSpriteWithFlags(dst, pitch, width, height,
- interpLeftX, interpLeftY, _laserBank.sprites[sprIdxLeft], leftFlags);
- }
- if (sprIdxRight >= 0 && sprIdxRight < _laserBank.numSprites) {
- renderSpriteWithFlags(dst, pitch, width, height,
- interpRightX, interpRightY, _laserBank.sprites[sprIdxRight], rightFlags);
- }
+ renderBucketedShotSprite(dst, pitch, width, height, _laserBank,
+ startLeftX, leftStartY, targetX, targetY, lerp, frame, leftFlags);
+ renderBucketedShotSprite(dst, pitch, width, height, _laserBank,
+ startRightX, rightStartY, targetX, targetY, lerp, frame, rightFlags);
}
}
}
Commit: 7c868c8408efbceb325954f075364699dce27dc5
https://github.com/scummvm/scummvm/commit/7c868c8408efbceb325954f075364699dce27dc5
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:10:07+02:00
Commit Message:
SCUMM: RA1: Implement password selection correctly
Changed paths:
engines/scumm/insane/rebel1/menu.cpp
engines/scumm/insane/rebel1/rebel.cpp
engines/scumm/insane/rebel1/rebel.h
engines/scumm/insane/rebel1/render.cpp
engines/scumm/insane/rebel1/runlevels.cpp
diff --git a/engines/scumm/insane/rebel1/menu.cpp b/engines/scumm/insane/rebel1/menu.cpp
index d491e849b2b..1d6d033016a 100644
--- a/engines/scumm/insane/rebel1/menu.cpp
+++ b/engines/scumm/insane/rebel1/menu.cpp
@@ -32,6 +32,7 @@
namespace Scumm {
+const int kRA1MainMenuItemCount = 6;
const int kRA1LevelSelectItemCount = 16; // 15 levels + BACK
const int kRA1LevelSelectRowsPerCol = 8;
const int kRA1NumLevels = 15;
@@ -43,6 +44,11 @@ const int kRA1MenuFrameH = 0x0f;
const int kRA1MenuRowH = 0x0f;
const byte kRA1MenuFrameColor = 0xdf;
const uint32 kRA1JoystickAxisEscGuardMs = 250;
+// Original picker traversal uses fixed max indices, not strlen(): passcodes
+// stop at 0x1d and high-score names stop at 0x37.
+const char kRA1TextEntryPickerChars[] = "^`_ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
+const int kRA1PasscodePickerCount = 0x1d + 1;
+const int kRA1NamePickerCount = 0x37 + 1;
static int getRebel1MenuAxisDirection(int16 axisValue) {
if (axisValue >= kRA1MenuAxisThreshold)
@@ -105,6 +111,68 @@ static int getRebel1MenuCenteredX(int textWidth) {
return (kRA1MenuLogicalWidth - textWidth) / 2;
}
+static const char *getRebel1TextEntryPickerChars(bool) {
+ return kRA1TextEntryPickerChars;
+}
+
+static int getRebel1TextEntryPickerCount(bool passcodeMode) {
+ return passcodeMode ? kRA1PasscodePickerCount : kRA1NamePickerCount;
+}
+
+static int getRebel1PasscodeDifficulty(int passwordIndex) {
+ return (passwordIndex - 1) % 3;
+}
+
+static int getRebel1PasscodeStartLevel(int passwordIndex) {
+ // Original main-menu control flow groups passcodes by difficulty:
+ // 1-3 resume after chapter 3, 4-6 after chapter 6, 7-9 after chapter 10,
+ // 10-12 after chapter 14, and 13-15 jump to the final ending sequence.
+ switch ((passwordIndex - 1) / 3) {
+ case 0:
+ return 4;
+ case 1:
+ return 7;
+ case 2:
+ return 11;
+ case 3:
+ return 15;
+ case 4:
+ return kRA1NumLevels + 1;
+ default:
+ return 0;
+ }
+}
+
+static char normalizeRebel1PasscodeChar(char c) {
+ if (c >= 'a' && c <= 'z')
+ return c - ('a' - 'A');
+ return c;
+}
+
+static bool isRebel1TextEntryChar(bool passcodeMode, char c) {
+ if (passcodeMode) {
+ c = normalizeRebel1PasscodeChar(c);
+ return c >= 'A' && c <= 'Z';
+ }
+
+ return c == ' ' || c == '_' ||
+ (c >= 'A' && c <= 'Z') ||
+ (c >= 'a' && c <= 'z');
+}
+
+static void appendRebel1TextEntryChar(char *buffer, int bufferSize, int maxChars, bool passcodeMode, char c) {
+ if (!buffer || bufferSize <= 0)
+ return;
+
+ c = passcodeMode ? normalizeRebel1PasscodeChar(c) : c;
+ const int len = strlen(buffer);
+ if (len >= maxChars || len >= bufferSize - 1)
+ return;
+
+ buffer[len] = c;
+ buffer[len + 1] = '\0';
+}
+
static void drawRebel1MenuFrame(byte *dst, int pitch, int width, int height, int x, int y, int w) {
if (!dst || width <= 0 || height <= 0)
return;
@@ -124,10 +192,135 @@ static void drawRebel1MenuFrame(byte *dst, int pitch, int width, int height, int
}
}
+void InsaneRebel1::beginTextEntry(bool passcodeMode) {
+ _textEntryActive = true;
+ _textEntryPasscodeMode = passcodeMode;
+ _textEntryDone = false;
+ _textEntryCanceled = false;
+ _textEntryPickerIndex = 3; // First letter in the original picker string.
+ _textEntryPickerOffsetX = 0;
+ // The DOS buffers include a leading '<' font marker and cap strlen() at 8.
+ _textEntryMaxChars = 8;
+ _textEntryBuffer[0] = '\0';
+
+ if (passcodeMode && _maxChapterUnlocked > 0) {
+ const char *password = getChapterCompletePassword(_maxChapterUnlocked);
+ if (password) {
+ Common::strlcpy(_textEntryBuffer, password, sizeof(_textEntryBuffer));
+ _textEntryPickerIndex = 1;
+ }
+ }
+}
+
+void InsaneRebel1::finishTextEntry(bool canceled) {
+ _textEntryCanceled = canceled;
+ if (!canceled)
+ _textEntryDone = true;
+ _textEntryActive = false;
+ _vm->_smushVideoShouldFinish = true;
+}
+
+void InsaneRebel1::selectTextEntryChar() {
+ if (!_textEntryActive)
+ return;
+
+ const char *pickerChars = getRebel1TextEntryPickerChars(_textEntryPasscodeMode);
+ const int pickerCount = getRebel1TextEntryPickerCount(_textEntryPasscodeMode);
+ if (_textEntryPickerIndex < 0 || _textEntryPickerIndex >= pickerCount)
+ return;
+
+ const char ch = pickerChars[_textEntryPickerIndex];
+ if (ch == '^') {
+ const int len = strlen(_textEntryBuffer);
+ if (len > 0)
+ _textEntryBuffer[len - 1] = '\0';
+ } else if (ch == '`') {
+ finishTextEntry(false);
+ } else if (ch == '_') {
+ appendRebel1TextEntryChar(_textEntryBuffer, sizeof(_textEntryBuffer),
+ _textEntryMaxChars, _textEntryPasscodeMode, ' ');
+ } else {
+ appendRebel1TextEntryChar(_textEntryBuffer, sizeof(_textEntryBuffer),
+ _textEntryMaxChars, _textEntryPasscodeMode, ch);
+ }
+}
+
+bool InsaneRebel1::handleTextEntryAction(ScummAction action) {
+ if (!_textEntryActive)
+ return false;
+
+ const int pickerCount = getRebel1TextEntryPickerCount(_textEntryPasscodeMode);
+ switch (action) {
+ case kScummActionInsaneLeft:
+ _textEntryPickerIndex = (_textEntryPickerIndex + pickerCount - 1) % pickerCount;
+ _textEntryPickerOffsetX = -7;
+ return true;
+ case kScummActionInsaneRight:
+ _textEntryPickerIndex = (_textEntryPickerIndex + 1) % pickerCount;
+ _textEntryPickerOffsetX = 7;
+ return true;
+ case kScummActionInsaneAttack:
+ selectTextEntryChar();
+ return true;
+ default:
+ return false;
+ }
+}
+
+bool InsaneRebel1::handleTextEntryKey(const Common::Event &event) {
+ if (!_textEntryActive || event.type != Common::EVENT_KEYDOWN)
+ return false;
+
+ const int pickerCount = getRebel1TextEntryPickerCount(_textEntryPasscodeMode);
+ switch (event.kbd.keycode) {
+ case Common::KEYCODE_LEFT:
+ _textEntryPickerIndex = (_textEntryPickerIndex + pickerCount - 1) % pickerCount;
+ _textEntryPickerOffsetX = -7;
+ return true;
+ case Common::KEYCODE_RIGHT:
+ _textEntryPickerIndex = (_textEntryPickerIndex + 1) % pickerCount;
+ _textEntryPickerOffsetX = 7;
+ return true;
+ case Common::KEYCODE_RETURN:
+ case Common::KEYCODE_KP_ENTER:
+ finishTextEntry(false);
+ return true;
+ case Common::KEYCODE_ESCAPE:
+ finishTextEntry(true);
+ return true;
+ case Common::KEYCODE_BACKSPACE: {
+ const int len = strlen(_textEntryBuffer);
+ if (len > 0)
+ _textEntryBuffer[len - 1] = '\0';
+ return true;
+ }
+ case Common::KEYCODE_SPACE:
+ appendRebel1TextEntryChar(_textEntryBuffer, sizeof(_textEntryBuffer),
+ _textEntryMaxChars, _textEntryPasscodeMode, ' ');
+ return true;
+ default:
+ break;
+ }
+
+ char ch = (char)event.kbd.ascii;
+ if (ch == '\0')
+ ch = (char)event.kbd.keycode;
+ if (isRebel1TextEntryChar(_textEntryPasscodeMode, ch)) {
+ appendRebel1TextEntryChar(_textEntryBuffer, sizeof(_textEntryBuffer),
+ _textEntryMaxChars, _textEntryPasscodeMode, ch);
+ return true;
+ }
+
+ return true;
+}
+
bool InsaneRebel1::handleControllerMenuAction(ScummAction action) {
if (!_menuActive || _highScoresActive)
return false;
+ if (_textEntryActive)
+ return handleTextEntryAction(action);
+
if (_levelSelectActive) {
int col = _levelSelectSel / kRA1LevelSelectRowsPerCol;
int row = _levelSelectSel % kRA1LevelSelectRowsPerCol;
@@ -185,10 +378,10 @@ bool InsaneRebel1::handleControllerMenuAction(ScummAction action) {
switch (action) {
case kScummActionInsaneUp:
- _menuSelection = (_menuSelection + 4) % 5;
+ _menuSelection = (_menuSelection + kRA1MainMenuItemCount - 1) % kRA1MainMenuItemCount;
return true;
case kScummActionInsaneDown:
- _menuSelection = (_menuSelection + 1) % 5;
+ _menuSelection = (_menuSelection + 1) % kRA1MainMenuItemCount;
return true;
case kScummActionInsaneAttack:
_menuConfirmed = true;
@@ -350,6 +543,9 @@ bool InsaneRebel1::notifyEvent(const Common::Event &event) {
}
}
+ if (_menuActive && _textEntryActive && event.type == Common::EVENT_KEYDOWN)
+ return handleTextEntryKey(event);
+
if (_menuActive && _levelSelectActive && event.type == Common::EVENT_KEYDOWN) {
int col = _levelSelectSel / kRA1LevelSelectRowsPerCol;
int row = _levelSelectSel % kRA1LevelSelectRowsPerCol;
@@ -432,11 +628,11 @@ bool InsaneRebel1::notifyEvent(const Common::Event &event) {
switch (event.kbd.keycode) {
case Common::KEYCODE_UP:
case Common::KEYCODE_w:
- _menuSelection = (_menuSelection + 4) % 5;
+ _menuSelection = (_menuSelection + kRA1MainMenuItemCount - 1) % kRA1MainMenuItemCount;
return true;
case Common::KEYCODE_DOWN:
case Common::KEYCODE_s:
- _menuSelection = (_menuSelection + 1) % 5;
+ _menuSelection = (_menuSelection + 1) % kRA1MainMenuItemCount;
return true;
case Common::KEYCODE_RETURN:
case Common::KEYCODE_KP_ENTER:
@@ -449,12 +645,13 @@ bool InsaneRebel1::notifyEvent(const Common::Event &event) {
case Common::KEYCODE_3:
case Common::KEYCODE_4:
case Common::KEYCODE_5:
+ case Common::KEYCODE_6:
_menuSelection = event.kbd.keycode - Common::KEYCODE_1;
_menuConfirmed = true;
_vm->_smushVideoShouldFinish = true;
return true;
case Common::KEYCODE_ESCAPE:
- _menuSelection = 4;
+ _menuSelection = kRA1MainMenuItemCount - 1;
_menuConfirmed = true;
_vm->_smushVideoShouldFinish = true;
return true;
@@ -556,6 +753,11 @@ void InsaneRebel1::renderMainMenuOverlay(byte *dst, int pitch, int width, int he
drawFontBankString(dst, pitch, width, height, x, y, text);
};
+ if (_textEntryActive) {
+ renderTextEntryOverlay(dst, pitch, width, height);
+ return;
+ }
+
if (_highScoresActive) {
// --- TOP PILOTS high score display ---
// Original renders over O1SCORE.ANM. Title appears after frame 20,
@@ -662,9 +864,10 @@ void InsaneRebel1::renderMainMenuOverlay(byte *dst, int pitch, int width, int he
}
// --- Main menu ---
- const char *kMenuItems[5] = {
+ const char *kMenuItems[kRA1MainMenuItemCount] = {
"START NEW GAME",
"GAME OPTIONS",
+ "ENTER PASSCODE",
"LEVEL SELECT",
"CONTINUE DEMO",
"EXIT TO DOS"
@@ -676,7 +879,7 @@ void InsaneRebel1::renderMainMenuOverlay(byte *dst, int pitch, int width, int he
drawTitleText(titleX, 30, "MAIN MENU");
// Draw menu items centered horizontally
- for (int i = 0; i < 5; i++) {
+ for (int i = 0; i < kRA1MainMenuItemCount; i++) {
const int textW = getTalkTextWidth(kMenuItems[i]);
const int textX = getRebel1MenuCenteredX(textW);
const int y = 0x3c + i * kRA1MenuRowH;
@@ -689,6 +892,45 @@ void InsaneRebel1::renderMainMenuOverlay(byte *dst, int pitch, int width, int he
}
}
+void InsaneRebel1::renderTextEntryOverlay(byte *dst, int pitch, int width, int height) {
+ auto makeTalkText = [](const char *text) {
+ Common::String out("<");
+ out += text;
+ return out;
+ };
+ auto drawCenteredTalkText = [&](int y, const char *text) {
+ Common::String styled = makeTalkText(text);
+ const int textW = getFontBankStringWidth(styled.c_str());
+ drawFontBankString(dst, pitch, width, height,
+ getRebel1MenuCenteredX(textW), y, styled.c_str());
+ };
+ auto drawCenteredRawChar = [&](int centerX, int y, char ch) {
+ char text[2] = { ch, '\0' };
+ const int textW = getFontBankStringWidth(text);
+ drawFontBankString(dst, pitch, width, height, centerX - textW / 2, y, text);
+ };
+
+ if (_textEntryPasscodeMode) {
+ drawCenteredTalkText(0x4b, "ENTER PASSCODE");
+ drawCenteredTalkText(0x5f, _textEntryBuffer);
+ } else {
+ char scoreText[40];
+ Common::sprintf_s(scoreText, "SCORE: %ld", (long)_score);
+ drawCenteredTalkText(0x37, scoreText);
+ drawCenteredTalkText(0x4b, "NEW HIGH SCORE");
+ drawCenteredTalkText(0x5f, _textEntryBuffer);
+ }
+
+ const char *pickerChars = getRebel1TextEntryPickerChars(_textEntryPasscodeMode);
+ const int pickerCount = getRebel1TextEntryPickerCount(_textEntryPasscodeMode);
+ const int prevIndex = (_textEntryPickerIndex + pickerCount - 1) % pickerCount;
+ const int nextIndex = (_textEntryPickerIndex + 1) % pickerCount;
+ drawCenteredRawChar(0x91 + _textEntryPickerOffsetX, 0x6e, pickerChars[prevIndex]);
+ drawCenteredRawChar(0xa0 + _textEntryPickerOffsetX, 0x6e, pickerChars[_textEntryPickerIndex]);
+ drawCenteredRawChar(0xaf + _textEntryPickerOffsetX, 0x6e, pickerChars[nextIndex]);
+ _textEntryPickerOffsetX = 0;
+}
+
int InsaneRebel1::runMainMenu() {
debug(1, "InsaneRebel1: Main menu");
@@ -702,13 +944,84 @@ int InsaneRebel1::runMainMenu() {
_menuActive = false;
if (_vm->shouldQuit())
- return 5;
+ return kRA1MainMenuItemCount;
if (_menuConfirmed)
return _menuSelection + 1;
}
- return 5;
+ return kRA1MainMenuItemCount;
+}
+
+int InsaneRebel1::runPasscodeEntryDialog() {
+ beginTextEntry(true);
+
+ while (!_vm->shouldQuit() && !_textEntryDone && !_textEntryCanceled) {
+ _menuActive = true;
+ _menuConfirmed = false;
+ _menuFrameCounter = 0;
+ clearVideoBuffer();
+ playCinematic("OPEN/O1OPTION.ANM");
+ _menuActive = false;
+ }
+
+ if (_vm->shouldQuit() || _textEntryCanceled)
+ return 0;
+
+ for (int i = 1; i <= kRA1NumLevels; i++) {
+ const char *password = getChapterCompletePassword(i);
+ if (password && !scumm_stricmp(_textEntryBuffer, password)) {
+ const int targetLevel = getRebel1PasscodeStartLevel(i);
+ if (targetLevel == 0)
+ return 0;
+
+ _difficulty = getRebel1PasscodeDifficulty(i);
+ _maxChapterUnlocked = MAX<int16>(_maxChapterUnlocked, i);
+ if (targetLevel <= kRA1NumLevels)
+ _startLevel = targetLevel;
+ debug(1, "RA1 passcode accepted: slot=%d password=%s difficulty=%d target=%d",
+ i, password, _difficulty, targetLevel);
+ return targetLevel;
+ }
+ }
+
+ debug(1, "RA1 passcode rejected: '%s'", _textEntryBuffer);
+ return 0;
+}
+
+bool InsaneRebel1::runHighScoreNameEntry() {
+ int slot = 0;
+ while (slot < kHighScoreCount && _highScores[slot].score >= _score)
+ slot++;
+ if (slot >= kHighScoreCount)
+ return false;
+
+ for (int i = kHighScoreCount - 1; i > slot; i--)
+ _highScores[i] = _highScores[i - 1];
+
+ _highScores[slot].score = _score;
+ _highScores[slot].difficulty = _difficulty;
+ Common::strlcpy(_highScores[slot].name, "<", sizeof(_highScores[slot].name));
+ _highScoreEntryIndex = slot;
+
+ beginTextEntry(false);
+ while (!_vm->shouldQuit() && !_textEntryDone && !_textEntryCanceled) {
+ _menuActive = true;
+ _menuConfirmed = false;
+ _menuFrameCounter = 0;
+ clearVideoBuffer();
+ playCinematic("OPEN/O1OPTION.ANM");
+ _menuActive = false;
+ }
+
+ Common::String storedName("<");
+ storedName += _textEntryBuffer;
+ Common::strlcpy(_highScores[slot].name, storedName.c_str(), sizeof(_highScores[slot].name));
+ _highScores[slot].difficulty = _difficulty;
+ _highScoreEntryIndex = -1;
+ debug(1, "RA1 high score inserted: slot=%d name=%s score=%ld difficulty=%d",
+ slot, _highScores[slot].name, (long)_highScores[slot].score, _highScores[slot].difficulty);
+ return true;
}
void InsaneRebel1::runOptionsMenu() {
diff --git a/engines/scumm/insane/rebel1/rebel.cpp b/engines/scumm/insane/rebel1/rebel.cpp
index 39861afee1f..efa8e64e700 100644
--- a/engines/scumm/insane/rebel1/rebel.cpp
+++ b/engines/scumm/insane/rebel1/rebel.cpp
@@ -354,6 +354,15 @@ InsaneRebel1::InsaneRebel1(ScummEngine_v7 *scumm) : Insane(), _vm(scumm) {
_highScores[i].difficulty = kDefaultScores[i].difficulty;
}
_highScoresActive = false;
+ _textEntryActive = false;
+ _textEntryPasscodeMode = false;
+ _textEntryDone = false;
+ _textEntryCanceled = false;
+ _textEntryPickerIndex = 0;
+ _textEntryPickerOffsetX = 0;
+ _textEntryMaxChars = 0;
+ _highScoreEntryIndex = -1;
+ memset(_textEntryBuffer, 0, sizeof(_textEntryBuffer));
// Shooting/targeting state
_playerFired = false;
diff --git a/engines/scumm/insane/rebel1/rebel.h b/engines/scumm/insane/rebel1/rebel.h
index d285f398e2d..05f88d954c4 100644
--- a/engines/scumm/insane/rebel1/rebel.h
+++ b/engines/scumm/insane/rebel1/rebel.h
@@ -113,7 +113,8 @@ private:
void clearVideoBuffer();
// Main menu loop on O1OPTION.ANM background (0x15968)
- // Returns: 1=Start New Game, 2=Game Options, 3=Level Select, 4=Continue Demo, 5=Exit
+ // Returns: 1=Start New Game, 2=Game Options, 3=Enter Passcode,
+ // 4=Level Select, 5=Continue Demo, 6=Exit
int runMainMenu();
int runLevelSelectMenu();
@@ -161,6 +162,7 @@ private:
void renderShip(byte *dst, int pitch, int width, int height);
void renderHUD(byte *dst, int pitch, int width, int height);
void renderMainMenuOverlay(byte *dst, int pitch, int width, int height);
+ void renderTextEntryOverlay(byte *dst, int pitch, int width, int height);
void renderExplosions(byte *dst, int pitch, int width, int height);
void renderTargetBoxes(byte *dst, int pitch, int width, int height);
void renderTargeting(byte *dst, int pitch, int width, int height);
@@ -402,7 +404,7 @@ private:
byte _deathCauseIndicator; // 0x772E: non-zero = player died; selects death animation variant
byte _hudRenderFlag; // 0x7600: 0xFF when HUD should render (set by combat mode handlers)
byte _hudDirtyFlag; // 0x7601: 0xFF after HUD redraw (set by renderHUD)
- int16 _maxChapterUnlocked; // 0x7730: highest chapter with valid passcode (0=none, set on completion)
+ int16 _maxChapterUnlocked; // 0x7730: highest unlocked passcode slot (0=none)
static const int16 kMaxHealth = 98;
static const int16 kDeathTimerInit = 30;
@@ -451,11 +453,19 @@ private:
// Main menu / options state
void runOptionsMenu();
+ int runPasscodeEntryDialog();
+ bool runHighScoreNameEntry();
bool handleControllerMenuAction(ScummAction action);
bool handleControllerMenuAxis(int16 oldAxisX, int16 oldAxisY);
+ bool handleTextEntryAction(ScummAction action);
+ bool handleTextEntryKey(const Common::Event &event);
+ void beginTextEntry(bool passcodeMode);
+ void finishTextEntry(bool canceled);
+ void selectTextEntryChar();
+ const char *getChapterCompletePassword(int passwordIndex) const;
bool _menuActive;
bool _menuConfirmed;
- int _menuSelection; // 0..4 maps to return values 1..5
+ int _menuSelection; // 0..5 maps to return values 1..6
int _menuFrameCounter;
// Options submenu state â RunGameOptionsMenu (0x14B42)
@@ -486,6 +496,18 @@ private:
bool _highScoresActive; // True when showing TOP PILOTS overlay
void showHighScores();
+ // Character picker shared by RunPasscodeEntryDialog and RunHighScoreNameEntry.
+ static const int kTextEntryBufferSize = 20;
+ bool _textEntryActive;
+ bool _textEntryPasscodeMode;
+ bool _textEntryDone;
+ bool _textEntryCanceled;
+ int _textEntryPickerIndex;
+ int _textEntryPickerOffsetX;
+ int _textEntryMaxChars;
+ int _highScoreEntryIndex;
+ char _textEntryBuffer[kTextEntryBufferSize];
+
// Shooting state â FUN_1CCA0 (0x1CCA0)
bool _playerFired; // 0x7570: current fire-button state
int16 _fireCooldown; // 0x757C: previous-frame fire-button state (edge gate)
diff --git a/engines/scumm/insane/rebel1/render.cpp b/engines/scumm/insane/rebel1/render.cpp
index 7b552325774..b051339717b 100644
--- a/engines/scumm/insane/rebel1/render.cpp
+++ b/engines/scumm/insane/rebel1/render.cpp
@@ -1274,14 +1274,21 @@ void InsaneRebel1::beginChapterSummaryOverlay(int revealOffsetFromEnd, int stopO
}
}
-// The DOS table is stored as 15 XOR-0xAA encoded, 20-byte passcode slots at
-// DS:0x00A4. RunChapterCompleteSummaryScreen indexes it with passwordIndex-1.
+// ScummVM keeps the passcodes in clear text. The DOS table is XOR-0xAA
+// encoded in 15 20-byte slots at DS:0x00A4.
static const char *const kChapterCompletePasswords[] = {
"FALCON", "BIGGS", "ACKBAR", "ANOAT", "KAIBURR",
"FORNAX", "YUZZEM", "MYNOCK", "BESPIN", "BRIGIA",
"DAGOBAH", "KESSEL", "GREEDO", "MIMBAN", "ORGANA"
};
+const char *InsaneRebel1::getChapterCompletePassword(int passwordIndex) const {
+ if (passwordIndex < 1 || passwordIndex > (int)ARRAYSIZE(kChapterCompletePasswords))
+ return nullptr;
+
+ return kChapterCompletePasswords[passwordIndex - 1];
+}
+
// drawChapterSummaryOverlay â RunChapterCompleteSummaryScreen (0x15E42), shared by
// the RA1 runlevel flows that call it. This helper is a ScummVM-side extraction;
// the original pumps frontend frames from the runlevel after queueing the END ANM.
@@ -1325,12 +1332,13 @@ void InsaneRebel1::drawChapterSummaryOverlay(byte *dst, int pitch, int width, in
}
if (_chapterSummary.passwordIndex > 0 &&
- _chapterSummary.passwordIndex <= ARRAYSIZE(kChapterCompletePasswords) &&
revealBaseFrame + 10 < curFrame) {
- char passwordText[40];
- Common::sprintf_s(passwordText, "Password: %s",
- kChapterCompletePasswords[_chapterSummary.passwordIndex - 1]);
- drawCenteredRebel1String(this, dst, pitch, width, height, centerX, 0x73, passwordText);
+ const char *password = getChapterCompletePassword(_chapterSummary.passwordIndex);
+ if (password) {
+ char passwordText[40];
+ Common::sprintf_s(passwordText, "Password: %s", password);
+ drawCenteredRebel1String(this, dst, pitch, width, height, centerX, 0x73, passwordText);
+ }
}
}
diff --git a/engines/scumm/insane/rebel1/runlevels.cpp b/engines/scumm/insane/rebel1/runlevels.cpp
index 6f091cf8a9f..08b2d4991e5 100644
--- a/engines/scumm/insane/rebel1/runlevels.cpp
+++ b/engines/scumm/insane/rebel1/runlevels.cpp
@@ -262,7 +262,8 @@ void InsaneRebel1::playChapterCompleteCinematic(const char *filename, int16 unlo
playCinematic(filename);
_chapterSummary.active = false;
_score += _tuning.levelPts + bonusValue1 + bonusValue2;
- _maxChapterUnlocked = MAX(_maxChapterUnlocked, unlockedChapter);
+ if (passwordIndex != 0)
+ _maxChapterUnlocked = MAX<int16>(_maxChapterUnlocked, passwordIndex);
}
void InsaneRebel1::clearVideoBuffer() {
@@ -556,7 +557,7 @@ bool InsaneRebel1::runLevel3() {
if (_health >= 0) {
playChapterCompleteCinematic("LVL3/L3END.ANM", 3, 0x69, 5,
- nullptr, nullptr, 0, nullptr, nullptr, 0, 3);
+ nullptr, nullptr, 0, nullptr, nullptr, 0, _difficulty + 1);
return !_vm->shouldQuit();
}
@@ -836,7 +837,7 @@ bool InsaneRebel1::runLevel6() {
formatTargetAccuracy(accuracyText, sizeof(accuracyText), _killCount, 0x27, true);
const int bonus = calculateThresholdBonus(_killCount, 0x26, 0x0C, _tuning.bonus);
playChapterCompleteCinematic("LVL6/L6END.ANM", 6, 0x4B, 5,
- " ", accuracyText, bonus, nullptr, nullptr, 0, 6);
+ " ", accuracyText, bonus, nullptr, nullptr, 0, _difficulty + 4);
return !_vm->shouldQuit();
}
@@ -1331,7 +1332,7 @@ bool InsaneRebel1::runLevel10() {
formatTargetAccuracy(accuracyText, sizeof(accuracyText), _killCount, 0x3E, true);
const int bonus = calculateThresholdBonus(_killCount, 0x3D, 0x32, _tuning.bonus);
playChapterCompleteCinematic("LVL10/L10END.ANM", 10, 0x4B, 5,
- " ", accuracyText, bonus, nullptr, nullptr, 0, 10);
+ " ", accuracyText, bonus, nullptr, nullptr, 0, _difficulty + 7);
return !_vm->shouldQuit();
}
@@ -1711,7 +1712,7 @@ bool InsaneRebel1::runLevel14() {
if (_health >= 0) {
playChapterCompleteCinematic("LVL14/L14END.ANM", 14, 0x69, 5,
- nullptr, nullptr, 0, nullptr, nullptr, 0, 14);
+ nullptr, nullptr, 0, nullptr, nullptr, 0, _difficulty + 10);
return !_vm->shouldQuit();
}
@@ -1827,7 +1828,7 @@ bool InsaneRebel1::runLevel15() {
formatTargetAccuracy(accuracyText, sizeof(accuracyText), _killCount, 0x58, false);
playChapterCompleteCinematic("LVL15/L15END1.ANM", 15, 0x122, 0xA5,
"Part I", accuracyText, targetBonus,
- "Part II", "Torpedo on mark", 10000, 15);
+ "Part II", "Torpedo on mark", 10000, _difficulty + 13);
return !_vm->shouldQuit();
}
@@ -1867,6 +1868,27 @@ void InsaneRebel1::runGame() {
&InsaneRebel1::runLevel14,
&InsaneRebel1::runLevel15
};
+ const int numLevels = (int)(sizeof(kLevelRunners) / sizeof(kLevelRunners[0]));
+ auto runLevelsFrom = [&](int startLevel) {
+ const int firstLevel = CLIP<int>(startLevel, 1, numLevels);
+ bool completed = true;
+ _health = kMaxHealth;
+ _lives = 3;
+ _score = 0;
+ _prevScore = 0;
+
+ for (int level = firstLevel;
+ level <= numLevels && completed && !_vm->shouldQuit();
+ ++level) {
+ completed = (this->*kLevelRunners[level - 1])();
+ if (completed && level < numLevels)
+ _startLevel = level + 1;
+ }
+
+ if (!_vm->shouldQuit())
+ runHighScoreNameEntry();
+ _currentLevel = 0;
+ };
// Play intro sequence (logo + opening)
playIntroSequence();
@@ -1882,18 +1904,7 @@ void InsaneRebel1::runGame() {
switch (menuResult) {
case 1: {
// START NEW GAME â sequential play from _startLevel
- const int numLevels = (int)(sizeof(kLevelRunners) / sizeof(kLevelRunners[0]));
- const int startLevel = CLIP<int>(_startLevel, 1, numLevels);
- bool completed = true;
-
- for (int level = startLevel;
- level <= numLevels && completed && !_vm->shouldQuit();
- ++level) {
- completed = (this->*kLevelRunners[level - 1])();
- if (completed && level < numLevels)
- _startLevel = level + 1;
- }
- _currentLevel = 0;
+ runLevelsFrom(_startLevel);
break;
}
case 2:
@@ -1901,23 +1912,48 @@ void InsaneRebel1::runGame() {
runOptionsMenu();
break;
case 3: {
- // Level Select â launch directly
+ // Enter Passcode â original RunPasscodeEntryDialog starts the
+ // game from the decoded chapter when a valid passcode is entered.
+ const int passcodeLevel = runPasscodeEntryDialog();
+ if (passcodeLevel >= 1 && passcodeLevel <= numLevels)
+ runLevelsFrom(passcodeLevel);
+ else if (passcodeLevel == numLevels + 1) {
+ _health = kMaxHealth;
+ _lives = 3;
+ _score = 0;
+ _prevScore = 0;
+ playCinematic("FIN/FNFINAL.ANM");
+ if (!_vm->shouldQuit())
+ runHighScoreNameEntry();
+ _currentLevel = 0;
+ }
+ break;
+ }
+ case 4: {
+ // Level Select â ScummVM-only direct launcher, kept separate from
+ // original passcode-start flow so it does not force sequential play.
int selectedLevel = runLevelSelectMenu();
- if (selectedLevel >= 1 && selectedLevel <= (int)(sizeof(kLevelRunners) / sizeof(kLevelRunners[0]))) {
+ if (selectedLevel >= 1 && selectedLevel <= numLevels) {
_startLevel = selectedLevel;
+ _health = kMaxHealth;
+ _lives = 3;
+ _score = 0;
+ _prevScore = 0;
(this->*kLevelRunners[selectedLevel - 1])();
+ if (!_vm->shouldQuit())
+ runHighScoreNameEntry();
_currentLevel = 0;
}
break;
}
- case 4:
+ case 5:
// CONTINUE DEMO â attract mode.
// Original shows TOP PILOTS (O1SCORE.ANM) then loops O1OPEN.ANM.
showHighScores();
if (!_vm->shouldQuit())
playCinematic("OPEN/O1OPEN.ANM");
break;
- case 5:
+ case 6:
// Exit
return;
default:
Commit: d9df04cc213130d49b54d77cecaa4ac7244bc9d4
https://github.com/scummvm/scummvm/commit/d9df04cc213130d49b54d77cecaa4ac7244bc9d4
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:10:07+02:00
Commit Message:
SCUMM: RA1: Add missing cutscenes
Changed paths:
engines/scumm/insane/rebel1/rebel.h
engines/scumm/insane/rebel1/runlevels.cpp
diff --git a/engines/scumm/insane/rebel1/rebel.h b/engines/scumm/insane/rebel1/rebel.h
index 05f88d954c4..66bcb96ef76 100644
--- a/engines/scumm/insane/rebel1/rebel.h
+++ b/engines/scumm/insane/rebel1/rebel.h
@@ -141,6 +141,7 @@ private:
// Play a passive cinematic (no game callback, skippable)
// startFrame > 0: fast-forward (decode without display) to that frame
void playCinematic(const char *filename, int32 startFrame = 0);
+ void playLevelTransitionCutscene(int level);
void playChapterCompleteCinematic(const char *filename, int16 unlockedChapter,
int revealOffsetFromEnd, int stopOffsetFromEnd,
const char *bonusLabel1 = nullptr, const char *detailText1 = nullptr, int bonusValue1 = 0,
diff --git a/engines/scumm/insane/rebel1/runlevels.cpp b/engines/scumm/insane/rebel1/runlevels.cpp
index 08b2d4991e5..fc67a0ec0d6 100644
--- a/engines/scumm/insane/rebel1/runlevels.cpp
+++ b/engines/scumm/insane/rebel1/runlevels.cpp
@@ -266,6 +266,38 @@ void InsaneRebel1::playChapterCompleteCinematic(const char *filename, int16 unlo
_maxChapterUnlocked = MAX<int16>(_maxChapterUnlocked, passwordIndex);
}
+void InsaneRebel1::playLevelTransitionCutscene(int level) {
+ switch (level) {
+ case 4:
+ // Original successor path 0x6d7d, reached after chapter 3 and by the
+ // FALCON/BIGGS/WEDGE passcode group. This is separate from RunLevel4Flow
+ // (0x6ee4), which starts with LVL4/L4INTRO.ANM.
+ playCinematic("CUT1/C1BLOCK.ANM");
+ if (_vm->shouldQuit())
+ break;
+ playCinematic("CUT1/C1DARTH1.ANM");
+ if (_vm->shouldQuit())
+ break;
+ playCinematic("CUT1/C1C3PO.ANM");
+ if (_vm->shouldQuit())
+ break;
+ playCinematic("CUT1/C1DARTH2.ANM");
+ break;
+ case 7:
+ // Original successor path 0x7d52, reached after chapter 6 and by the
+ // chapter-6 passcode group, before RunLevel7Flow (0x7dcc).
+ playCinematic("CUT2/C2CUT2.ANM");
+ break;
+ case 11:
+ // Original successor path 0x9f3a, reached after chapter 10 and by the
+ // chapter-10 passcode group, before RunLevel11Flow (0x9f9f).
+ playCinematic("CUT3/C3BOOM.ANM");
+ break;
+ default:
+ break;
+ }
+}
+
void InsaneRebel1::clearVideoBuffer() {
if (_vm->_screenWidth <= 0 || _vm->_screenHeight <= 0)
return;
@@ -1872,6 +1904,7 @@ void InsaneRebel1::runGame() {
auto runLevelsFrom = [&](int startLevel) {
const int firstLevel = CLIP<int>(startLevel, 1, numLevels);
bool completed = true;
+ int lastCompletedLevel = 0;
_health = kMaxHealth;
_lives = 3;
_score = 0;
@@ -1880,11 +1913,20 @@ void InsaneRebel1::runGame() {
for (int level = firstLevel;
level <= numLevels && completed && !_vm->shouldQuit();
++level) {
+ playLevelTransitionCutscene(level);
+ if (_vm->shouldQuit())
+ break;
+
completed = (this->*kLevelRunners[level - 1])();
- if (completed && level < numLevels)
- _startLevel = level + 1;
+ if (completed) {
+ lastCompletedLevel = level;
+ if (level < numLevels)
+ _startLevel = level + 1;
+ }
}
+ if (!_vm->shouldQuit() && completed && lastCompletedLevel == numLevels)
+ playCinematic("FIN/FNFINAL.ANM");
if (!_vm->shouldQuit())
runHighScoreNameEntry();
_currentLevel = 0;
@@ -1930,20 +1972,11 @@ void InsaneRebel1::runGame() {
break;
}
case 4: {
- // Level Select â ScummVM-only direct launcher, kept separate from
- // original passcode-start flow so it does not force sequential play.
+ // Level Select â ScummVM-only start point. Continue through the
+ // original successor flow so post-level cinematics still play.
int selectedLevel = runLevelSelectMenu();
- if (selectedLevel >= 1 && selectedLevel <= numLevels) {
- _startLevel = selectedLevel;
- _health = kMaxHealth;
- _lives = 3;
- _score = 0;
- _prevScore = 0;
- (this->*kLevelRunners[selectedLevel - 1])();
- if (!_vm->shouldQuit())
- runHighScoreNameEntry();
- _currentLevel = 0;
- }
+ if (selectedLevel >= 1 && selectedLevel <= numLevels)
+ runLevelsFrom(selectedLevel);
break;
}
case 5:
Commit: b55c73fea517249fc047f3b3c810863957457f61
https://github.com/scummvm/scummvm/commit/b55c73fea517249fc047f3b3c810863957457f61
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:10:08+02:00
Commit Message:
SCUMM: RA1: Avoid crash in handleGameAdjustCoords
Changed paths:
engines/scumm/smush/rebel/smush_player_ra1.cpp
diff --git a/engines/scumm/smush/rebel/smush_player_ra1.cpp b/engines/scumm/smush/rebel/smush_player_ra1.cpp
index 4d828c8356c..acca9e27cc2 100644
--- a/engines/scumm/smush/rebel/smush_player_ra1.cpp
+++ b/engines/scumm/smush/rebel/smush_player_ra1.cpp
@@ -759,6 +759,15 @@ bool SmushPlayerRebel1::handleGameAdjustCoords(int codec, int &left, int &top, i
if (codec == SMUSH_CODEC_SKIP_RLE || codec == SMUSH_CODEC_RA1_SCATTER)
return false;
+ // RA1 block codecs are column-major tile streams, not row-prefixed RLE
+ // streams. Preserve the source data and let smushDecodeRA1Block() consume
+ // offscreen tiles while clipping destination pixels.
+ if (codec == SMUSH_CODEC_RA1_DELTA || codec == SMUSH_CODEC_RA1_BLOCK) {
+ left += _fobjOffsetX;
+ top += _fobjOffsetY;
+ return false;
+ }
+
// RA1 codec 21 is source-X sensitive: generic left clipping would reduce
// the destination width without skipping the corresponding source columns.
// Keep only the global FOBJ offset here and let the codec clip each run.
Commit: c29ef27990f72892388ee0242b41f1348fbb1a2f
https://github.com/scummvm/scummvm/commit/c29ef27990f72892388ee0242b41f1348fbb1a2f
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:10:08+02:00
Commit Message:
SCUMM: RA1: Fix level 14 frame object state checks
Changed paths:
engines/scumm/insane/rebel1/iact.cpp
engines/scumm/insane/rebel1/rebel.h
engines/scumm/insane/rebel1/render.cpp
diff --git a/engines/scumm/insane/rebel1/iact.cpp b/engines/scumm/insane/rebel1/iact.cpp
index 91c31357ed1..7d8ac754669 100644
--- a/engines/scumm/insane/rebel1/iact.cpp
+++ b/engines/scumm/insane/rebel1/iact.cpp
@@ -504,40 +504,33 @@ bool InsaneRebel1::isFrameObjectPrimarySet(int16 objectId) const {
return (_frameObjectState[byteIndex] & bit) != 0;
}
+bool InsaneRebel1::areFrameObjectPrimaryBitsSet(int byteIndex, byte mask) const {
+ if (byteIndex < 0 || byteIndex >= 0x96 || byteIndex >= kFrameObjectStateBytes)
+ return false;
+
+ return (_frameObjectState[byteIndex] & mask) == mask;
+}
+
+void InsaneRebel1::clearFrameObjectPrimaryBits(int byteIndex, byte mask) {
+ if (byteIndex < 0 || byteIndex >= 0x96 || byteIndex >= kFrameObjectStateBytes)
+ return;
+
+ _frameObjectState[byteIndex] &= ~mask;
+}
+
// Port helpers for RunLevel14Flow. The original checks DAT_7614..7616 and
// DAT_7605..7606 inline, then increments a local 60-frame completion counter.
bool InsaneRebel1::areLevel14Phase1TargetsDestroyed() const {
- return isFrameObjectPrimarySet(168) &&
- isFrameObjectPrimarySet(169) &&
- isFrameObjectPrimarySet(170) &&
- isFrameObjectPrimarySet(171) &&
- isFrameObjectPrimarySet(172) &&
- isFrameObjectPrimarySet(173) &&
- isFrameObjectPrimarySet(174) &&
- isFrameObjectPrimarySet(175) &&
- isFrameObjectPrimarySet(176) &&
- isFrameObjectPrimarySet(177) &&
- isFrameObjectPrimarySet(178) &&
- isFrameObjectPrimarySet(179) &&
- isFrameObjectPrimarySet(180) &&
- isFrameObjectPrimarySet(181) &&
- isFrameObjectPrimarySet(182) &&
- isFrameObjectPrimarySet(183);
+ // g_gameplayPhaseFlags starts at primary byte 0; DAT_7614 is byte 0x12.
+ return areFrameObjectPrimaryBitsSet(0x12, 0x01) &&
+ areFrameObjectPrimaryBitsSet(0x13, 0xFF) &&
+ areFrameObjectPrimaryBitsSet(0x14, 0xFE);
}
bool InsaneRebel1::areLevel14Phase2TargetsDestroyed() const {
- return isFrameObjectPrimarySet(45) &&
- isFrameObjectPrimarySet(46) &&
- isFrameObjectPrimarySet(47) &&
- isFrameObjectPrimarySet(48) &&
- isFrameObjectPrimarySet(49) &&
- isFrameObjectPrimarySet(50) &&
- isFrameObjectPrimarySet(51) &&
- isFrameObjectPrimarySet(52) &&
- isFrameObjectPrimarySet(53) &&
- isFrameObjectPrimarySet(54) &&
- isFrameObjectPrimarySet(55) &&
- isFrameObjectPrimarySet(56);
+ // Phase 2 checks DAT_7605 low nibble and all of DAT_7606.
+ return areFrameObjectPrimaryBitsSet(0x03, 0x0F) &&
+ areFrameObjectPrimaryBitsSet(0x04, 0xFF);
}
bool InsaneRebel1::handleFrameObjectTarget(int16 objectId, int16 left, int16 top, int16 width, int16 height,
@@ -1714,15 +1707,15 @@ void InsaneRebel1::updateGameOp0BPhysics() {
}
// Level 15 Phase 2: enable torpedo at frame 0x18A, expose the protected
- // target IDs used by the original flow, and finish when object-state bit
- // 0x7602 & 2 becomes set.
+ // target IDs used by the original flow, and finish when
+ // g_gameplayPhaseFlags & 2 becomes set.
if (_currentLevel == 14 && _levelGameplayPhase == 2) {
if (_frameCounter == 0x18A) {
_gameplayFlags75ff |= 2;
_protectedTargetA = 0x67;
_protectedTargetB = 0x69;
}
- if (isFrameObjectPrimarySet(7)) {
+ if (areFrameObjectPrimaryBitsSet(0, 0x02)) {
_torpedoFired = true;
_vm->_smushVideoShouldFinish = true;
}
diff --git a/engines/scumm/insane/rebel1/rebel.h b/engines/scumm/insane/rebel1/rebel.h
index 66bcb96ef76..600a8567a28 100644
--- a/engines/scumm/insane/rebel1/rebel.h
+++ b/engines/scumm/insane/rebel1/rebel.h
@@ -185,6 +185,8 @@ private:
void updateGostSlotPosition(int16 targetIdx, int16 left, int16 top, int16 right, int16 bottom);
void applyFrameObjectHitState(int16 targetIdx);
bool isFrameObjectPrimarySet(int16 objectId) const;
+ bool areFrameObjectPrimaryBitsSet(int byteIndex, byte mask) const;
+ void clearFrameObjectPrimaryBits(int byteIndex, byte mask);
bool areLevel14Phase1TargetsDestroyed() const;
bool areLevel14Phase2TargetsDestroyed() const;
@@ -580,7 +582,7 @@ private:
int16 _level14SuccessFrames; // RunLevel14Flow: 60-frame hold after required targets are destroyed
// Level 15 torpedo success latch. The original derives this from
- // g_gameplayPhaseFlags bit 1, which is the primary object-state bit for object 7.
+ // g_gameplayPhaseFlags bit 1, which is _frameObjectState primary byte 0 bit 0x02.
bool _torpedoFired;
// Level 8 walker-specific state â RunLevel8Flow (0x18546)
diff --git a/engines/scumm/insane/rebel1/render.cpp b/engines/scumm/insane/rebel1/render.cpp
index b051339717b..5943b1c6b91 100644
--- a/engines/scumm/insane/rebel1/render.cpp
+++ b/engines/scumm/insane/rebel1/render.cpp
@@ -841,9 +841,10 @@ void InsaneRebel1::handleLevel14Play2BSplice(int32 curFrame, int32 maxFrame) {
// Original after PlayAnmFile("LVL14/L14PLY2B.ANM", 0x860, maxFrame-0x0F, 1, -1):
// g_extendedPhaseFlags &= 0xFA;
// DAT_7604 &= 0xBF;
- // In the port's object-state storage those are the first and third primary bytes.
- _frameObjectState[0] &= 0xFA;
- _frameObjectState[2] &= 0xBF;
+ // g_extendedPhaseFlags is primary byte 1 because g_gameplayPhaseFlags starts
+ // at byte 0 of the original frame-object state array.
+ clearFrameObjectPrimaryBits(1, 0x05);
+ clearFrameObjectPrimaryBits(2, 0x40);
_vm->_smushVideoShouldFinish = true;
}
Commit: e5324c60805d9b798ae568e76726c25fb6f465d0
https://github.com/scummvm/scummvm/commit/e5324c60805d9b798ae568e76726c25fb6f465d0
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:10:08+02:00
Commit Message:
SCUMM: RA1: Correct level names and tuning selectors
Changed paths:
engines/scumm/insane/rebel1/rebel.cpp
engines/scumm/insane/rebel1/runlevels.cpp
diff --git a/engines/scumm/insane/rebel1/rebel.cpp b/engines/scumm/insane/rebel1/rebel.cpp
index efa8e64e700..90cd418cf80 100644
--- a/engines/scumm/insane/rebel1/rebel.cpp
+++ b/engines/scumm/insane/rebel1/rebel.cpp
@@ -40,115 +40,115 @@ namespace Scumm {
// 21 sub-levels x 3 difficulties x 13 fields
// Fields: roll, lift, slide, drift, snap, miss, wham, shot, kill, time, levelPts, bonus, flags
const int16 kTuningTable[21][3][13] = {
- // Sub-level 0: "1A" (Flight Training - canyon flight)
+ // Sub-level 0: "1A" (Flight Training part 1)
{
{ 100, 100, 60, 110, 0, 0, 15, 0, 0, 5, 500, 100, 2048 }, // Easy
{ 100, 105, 60, 115, 0, 0, 25, 0, 0, 5, 1000, 200, 2048 }, // Normal
{ 105, 110, 65, 120, 0, 0, 30, 0, 0, 10, 1500, 500, 2050 }, // Hard
},
- // Sub-level 1: "1B" (Flight Training - asteroid flight)
+ // Sub-level 1: "1B" (Flight Training part 2)
{
{ 100, 16, 120, 0, 7, 0, 15, 0, 25, 5, 500, 100, 3072 }, // Easy
{ 100, 18, 120, 0, 5, 0, 20, 0, 50, 5, 1000, 200, 3072 }, // Normal
{ 100, 20, 150, 0, 1, 0, 25, 0, 75, 10, 1500, 500, 3074 }, // Hard
},
- // Sub-level 2: "2" (Planet Kolaador)
+ // Sub-level 2: "2" (Asteroid Field Training)
{
{ 0, 0, 0, 0, 4, 15, 25, 0, 25, 10, 500, 100, 2048 }, // Easy
{ 0, 0, 0, 0, 2, 18, 30, 0, 50, 10, 1000, 200, 2048 }, // Normal
{ 0, 0, 0, 0, 0, 20, 35, 0, 75, 10, 1500, 500, 2050 }, // Hard
},
- // Sub-level 3: "3" (Star Destroyer Attack)
+ // Sub-level 3: "3" (Planet Kolaador)
{
{ 70, 100, 150, 90, 0, 0, 20, 0, 0, 5, 1000, 100, 2048 }, // Easy
{ 72, 105, 155, 105, 0, 0, 25, 0, 0, 5, 2000, 200, 2048 }, // Normal
{ 75, 110, 160, 110, 0, 0, 28, 0, 0, 10, 3000, 500, 2050 }, // Hard
},
- // Sub-level 4: "4A" (Tatooine Attack)
+ // Sub-level 4: "4A" (Star Destroyer Attack part 1)
{
{ 0, 0, 0, 0, 2, 11, 0, 4, 25, 5, 500, 750, 2048 }, // Easy
{ 0, 0, 0, 0, 1, 25, 0, 6, 50, 5, 1000, 1500, 2048 }, // Normal
{ 0, 0, 0, 0, 1, 28, 0, 6, 75, 10, 1500, 2000, 2050 }, // Hard
},
- // Sub-level 5: "4B" (Tatooine Attack part 2)
+ // Sub-level 5: "4B" (Star Destroyer Attack part 2)
{
{ 0, 0, 0, 0, 3, 20, 0, 2, 50, 5, 500, 750, 2064 }, // Easy
{ 0, 0, 0, 0, 1, 25, 0, 5, 100, 5, 1000, 1500, 2064 }, // Normal
{ 0, 0, 0, 0, 1, 28, 0, 6, 200, 10, 1500, 2000, 2064 }, // Hard
},
- // Sub-level 6: "5A" (Imperial Probe Droids - speeder)
+ // Sub-level 6: "5A" (Tatooine Attack part 1)
{
{ 70, 150, 50, 25, 10, 0, 20, 0, 25, 5, 500, 15, 3072 }, // Easy
{ 72, 165, 155, 30, 8, 0, 30, 0, 50, 5, 1000, 30, 3072 }, // Normal
{ 110, 190, 55, 65, 3, 0, 33, 0, 75, 10, 1500, 75, 3074 }, // Hard
},
- // Sub-level 7: "5B" (Imperial Walkers)
+ // Sub-level 7: "5B" (Tatooine Attack part 2)
{
{ 0, 0, 0, 0, 5, 0, 0, 2, 25, 0, 500, 15, 2048 }, // Easy
{ 0, 0, 0, 0, 3, 0, 0, 5, 50, 5, 1000, 30, 2048 }, // Normal
{ 0, 0, 0, 0, 1, 0, 0, 6, 75, 10, 1500, 75, 2050 }, // Hard
},
- // Sub-level 8: "6" (Stormtroopers)
+ // Sub-level 8: "6" (Asteroid Field Chase)
{
{ 0, 0, 0, 0, 2, 20, 20, 0, 25, 5, 500, 100, 2048 }, // Easy
{ 0, 0, 0, 0, 1, 25, 30, 0, 50, 5, 1000, 200, 2048 }, // Normal
{ 0, 0, 0, 0, 0, 28, 33, 0, 75, 10, 1500, 500, 2050 }, // Hard
},
- // Sub-level 9: "7" (Protect Rebel Transport)
+ // Sub-level 9: "7" (Imperial Probe Droids)
{
{ 100, 150, 150, 25, 7, 0, 12, 2, 50, 5, 500, 100, 3072 }, // Easy
{ 100, 160, 200, 35, 4, 0, 30, 4, 100, 5, 1000, 200, 3072 }, // Normal
{ 100, 180, 250, 50, 3, 0, 33, 5, 100, 10, 1500, 500, 3074 }, // Hard
},
- // Sub-level 10: "8" (Death Star surface)
+ // Sub-level 10: "8" (Imperial Walkers)
{
{ 0, 0, 0, 0, 0, 0, 30, 0, 25, 0, 1000, 100, 3074 }, // Easy
{ 0, 0, 0, 0, 0, 0, 36, 0, 50, 0, 2000, 200, 3074 }, // Normal
{ 0, 0, 0, 0, 0, 0, 39, 0, 75, 0, 3000, 500, 3074 }, // Hard
},
- // Sub-level 11: "9A" (Death Star turrets part 1)
+ // Sub-level 11: "9A" (Stormtroopers part 1)
{
{ 0, 0, 0, 0, 4, 0, 0, 15, 25, 0, 1000, 100, 3074 }, // Easy
{ 0, 0, 0, 0, 2, 0, 0, 25, 50, 0, 2000, 200, 3078 }, // Normal
{ 0, 0, 0, 0, 0, 0, 0, 30, 75, 0, 3000, 500, 3078 }, // Hard
},
- // Sub-level 12: "9B" (Death Star turrets part 2)
+ // Sub-level 12: "9B" (Stormtroopers part 2)
{
{ 0, 0, 0, 0, 0, 0, 0, 15, 25, 0, 1000, 100, 3098 }, // Easy
{ 0, 0, 0, 0, 0, 0, 0, 25, 50, 0, 2000, 200, 3098 }, // Normal
{ 0, 0, 0, 0, 0, 0, 0, 30, 75, 0, 3000, 500, 3098 }, // Hard
},
- // Sub-level 13: "10" (Death Star trench approach)
+ // Sub-level 13: "10" (Protect Rebel Transport)
{
{ 0, 0, 0, 0, 3, 10, 0, 5, 25, 5, 500, 200, 2048 }, // Easy
{ 0, 0, 0, 0, 1, 16, 0, 5, 50, 5, 1000, 400, 2048 }, // Normal
{ 0, 0, 0, 0, 0, 18, 0, 7, 75, 10, 1500, 1000, 2050 }, // Hard
},
- // Sub-level 14: "11" (Death Star trench - speeder)
+ // Sub-level 14: "11" (Yavin Training)
{
{ 70, 150, 150, 25, 12, 0, 30, 0, 50, 5, 500, 200, 3072 }, // Easy
{ 72, 165, 155, 30, 7, 0, 36, 0, 50, 5, 1000, 400, 3072 }, // Normal
{ 75, 170, 160, 33, 3, 0, 39, 0, 75, 10, 1500, 1000, 3074 }, // Hard
},
- // Sub-level 15: "12" (Death Star trench run)
+ // Sub-level 15: "12" (TIE Attack)
{
{ 0, 0, 0, 0, 4, 13, 0, 5, 25, 5, 500, 100, 2048 }, // Easy
{ 0, 0, 0, 0, 2, 20, 0, 5, 50, 5, 1000, 200, 2048 }, // Normal
{ 0, 0, 0, 0, 0, 23, 0, 5, 75, 10, 1500, 500, 2050 }, // Hard
},
- // Sub-level 16: "13" (Asteroid belt chase)
+ // Sub-level 16: "13" (Death Star Surface)
{
{ 100, 16, 120, 0, 20, 0, 35, 8, 75, 5, 500, 100, 3072 }, // Easy
{ 100, 18, 120, 0, 18, 0, 36, 10, 100, 5, 1000, 200, 3072 }, // Normal
{ 100, 20, 150, 0, 15, 0, 39, 12, 200, 10, 1500, 500, 3074 }, // Hard
},
- // Sub-level 17: "14A" (Star Destroyer attack 2 part 1)
+ // Sub-level 17: "14A" (Surface Cannon part 1)
{
{ 0, 0, 0, 0, 0, 20, 35, 8, 25, 0, 1000, 100, 2048 }, // Easy
{ 0, 0, 0, 0, 0, 27, 36, 12, 50, 0, 2000, 200, 2048 }, // Normal
{ 0, 0, 0, 0, 0, 28, 39, 12, 75, 0, 3000, 500, 2050 }, // Hard
},
- // Sub-level 18: "14B" (Star Destroyer attack 2 part 2)
+ // Sub-level 18: "14B" (Surface Cannon part 2)
{
{ 0, 0, 0, 0, 10, 20, 35, 8, 25, 0, 1000, 100, 2048 }, // Easy
{ 0, 0, 0, 0, 5, 25, 36, 10, 50, 0, 2000, 200, 2048 }, // Normal
diff --git a/engines/scumm/insane/rebel1/runlevels.cpp b/engines/scumm/insane/rebel1/runlevels.cpp
index fc67a0ec0d6..2b371a8ae1d 100644
--- a/engines/scumm/insane/rebel1/runlevels.cpp
+++ b/engines/scumm/insane/rebel1/runlevels.cpp
@@ -613,7 +613,7 @@ bool InsaneRebel1::runLevel4() {
_currentLevel = 3;
loadLevelSprites(4);
- // DOS RunLevel4Flow launches L4PLAY1/2.ANM with gameplay selector 4.
+ // DOS RunLevel4Flow launches L4PLAY1.ANM with selector 4 and L4PLAY2.ANM with selector 5.
loadTuningForLevel(4);
beginLevelTitleOverlay(3);
@@ -622,6 +622,7 @@ bool InsaneRebel1::runLevel4() {
return false;
while (!_vm->shouldQuit()) {
+ loadTuningForLevel(4);
_flyControlMode = 1;
_health = kMaxHealth;
_damageFlags = 0;
@@ -661,6 +662,7 @@ bool InsaneRebel1::runLevel4() {
_activeGameOpcode = 0;
_gameLatch5D = 0;
_gameLatch5F = 0;
+ loadTuningForLevel(5);
resetGameplayFlagsFromTuning();
_killCount = 0;
_levelGameplayPhase = 2;
@@ -902,7 +904,8 @@ bool InsaneRebel1::runLevel7() {
_currentLevel = 6;
loadLevelSprites(7);
- loadTuningForLevel(6);
+ // DOS RunLevel7Flow starts L7PLAY1.ANM with initLevelFlag=9.
+ loadTuningForLevel(9);
beginLevelTitleOverlay(6);
playCinematic("LVL7/L7INTRO.ANM");
@@ -1124,7 +1127,8 @@ bool InsaneRebel1::runLevel9() {
_currentLevel = 8;
loadLevelSprites(9);
- loadTuningForLevel(8);
+ // DOS RunLevel9Flow alternates selectors 0x0B and 0x0C across its playable routes.
+ loadTuningForLevel(0x0B);
beginLevelTitleOverlay(8);
playCinematic("LVL9/L9INTRO.ANM");
@@ -1132,6 +1136,7 @@ bool InsaneRebel1::runLevel9() {
return false;
while (!_vm->shouldQuit()) {
+ loadTuningForLevel(0x0B);
_flyControlMode = 0;
_health = kMaxHealth;
_damageFlags = 0;
@@ -1173,6 +1178,7 @@ bool InsaneRebel1::runLevel9() {
_avgInputY = 0;
while (!_vm->shouldQuit()) {
+ loadTuningForLevel(0x0B);
playInteractiveVideo("LVL9/L9PLAY1.ANM");
if (_vm->shouldQuit())
return false;
@@ -1185,6 +1191,7 @@ bool InsaneRebel1::runLevel9() {
_shipPosX = kRA1CenterX;
_posAccumX = 0;
+ loadTuningForLevel(0x0C);
playInteractiveVideo("LVL9/L9PLAY2.ANM");
if (_vm->shouldQuit())
return false;
@@ -1208,6 +1215,7 @@ bool InsaneRebel1::runLevel9() {
if (_vm->shouldQuit())
return false;
+ loadTuningForLevel(0x0B);
playInteractiveVideo("LVL9/L9PLAY3A.ANM");
if (_vm->shouldQuit())
return false;
@@ -1228,6 +1236,7 @@ bool InsaneRebel1::runLevel9() {
_shipPosX = kRA1CenterX;
_posAccumX = 0;
+ loadTuningForLevel(0x0C);
playInteractiveVideo("LVL9/L9PLAY4.ANM");
if (_vm->shouldQuit())
return false;
@@ -1245,6 +1254,7 @@ bool InsaneRebel1::runLevel9() {
if (_vm->shouldQuit())
return false;
+ loadTuningForLevel(0x0B);
playInteractiveVideo("LVL9/L9PLAY5.ANM");
if (_vm->shouldQuit())
return false;
@@ -1257,6 +1267,7 @@ bool InsaneRebel1::runLevel9() {
_shipPosX = kRA1CenterX;
_posAccumX = 0;
+ loadTuningForLevel(0x0C);
playInteractiveVideo("LVL9/L9PLAY6.ANM");
if (_vm->shouldQuit())
return false;
@@ -1281,6 +1292,7 @@ bool InsaneRebel1::runLevel9() {
return false;
}
+ loadTuningForLevel(0x0B);
playInteractiveVideo("LVL9/L9PLAY7.ANM");
if (_vm->shouldQuit())
return false;
@@ -1311,7 +1323,8 @@ bool InsaneRebel1::runLevel10() {
_currentLevel = 9;
loadLevelSprites(10);
- loadTuningForLevel(9);
+ // DOS RunLevel10Flow starts L10PLAY.ANM with initLevelFlag=0x0D.
+ loadTuningForLevel(0x0D);
beginLevelTitleOverlay(9);
playCinematic("LVL10/L10INTRO.ANM");
@@ -1391,7 +1404,9 @@ bool InsaneRebel1::runLevel11() {
_currentLevel = 10;
loadLevelSprites(11);
- loadTuningForLevel(10);
+ // DOS RunLevel11Flow starts L11PLAY.ANM with initLevelFlag=0x0E.
+ // Row 10 has zero roll/slide tuning, which prevents horizontal aiming.
+ loadTuningForLevel(0x0E);
beginLevelTitleOverlay(10);
playCinematic("LVL11/L11INTRO.ANM");
@@ -1486,7 +1501,8 @@ bool InsaneRebel1::runLevel12() {
_currentLevel = 11;
loadLevelSprites(12);
- loadTuningForLevel(11);
+ // DOS RunLevel12Flow starts L12PLAY.ANM with initLevelFlag=0x0F.
+ loadTuningForLevel(0x0F);
beginLevelTitleOverlay(11);
playCinematic("LVL12/L12INTRO.ANM");
@@ -1506,6 +1522,7 @@ bool InsaneRebel1::runLevel12() {
_killCount = 0;
while (!_vm->shouldQuit()) {
+ loadTuningForLevel(0x0F);
_frameCounter = 0;
_gameCounter = 0;
_activeGameOpcode = 0;
@@ -1580,7 +1597,8 @@ bool InsaneRebel1::runLevel13() {
_currentLevel = 12;
loadLevelSprites(13);
loadRA1Nut("LVL13/L13LASR2.NUT", _enemyLaserBank);
- loadTuningForLevel(12);
+ // DOS RunLevel13Flow starts L13PLAY.ANM with initLevelFlag=0x10.
+ loadTuningForLevel(0x10);
beginLevelTitleOverlay(12);
playCinematic("LVL13/L13INTRO.ANM");
@@ -1661,7 +1679,8 @@ bool InsaneRebel1::runLevel14() {
_currentLevel = 13;
loadLevelSprites(14);
- loadTuningForLevel(13);
+ // DOS RunLevel14Flow uses selector 0x11 for L14PLAY and 0x12 for L14PLAY2.
+ loadTuningForLevel(0x11);
beginLevelTitleOverlay(13);
playCinematic("LVL14/L14INTRO.ANM");
@@ -1669,6 +1688,7 @@ bool InsaneRebel1::runLevel14() {
return false;
while (!_vm->shouldQuit()) {
+ loadTuningForLevel(0x11);
_flyControlMode = 1;
_health = kMaxHealth;
_damageFlags = 0;
@@ -1716,6 +1736,7 @@ bool InsaneRebel1::runLevel14() {
_activeGameOpcode = 0;
_gameLatch5D = 0;
_gameLatch5F = 0;
+ loadTuningForLevel(0x12);
resetGameplayFlagsFromTuning();
_killCount = 0;
_levelGameplayPhase = 2;
@@ -1772,7 +1793,8 @@ bool InsaneRebel1::runLevel15() {
_currentLevel = 14;
loadLevelSprites(15);
- loadTuningForLevel(14);
+ // DOS RunLevel1GameLoop uses selector 0x13 for L15PLAY1 and 0x14 for L15PLAY2.
+ loadTuningForLevel(0x13);
beginLevelTitleOverlay(14);
playCinematic("LVL15/L15INTRO.ANM");
@@ -1780,6 +1802,7 @@ bool InsaneRebel1::runLevel15() {
return false;
while (!_vm->shouldQuit()) {
+ loadTuningForLevel(0x13);
_flyControlMode = 1;
_health = kMaxHealth;
_damageFlags = 0;
@@ -1834,6 +1857,7 @@ bool InsaneRebel1::runLevel15() {
_activeGameOpcode = 0;
_gameLatch5D = 0;
_gameLatch5F = 0;
+ loadTuningForLevel(0x14);
resetGameplayFlagsFromTuning();
_killCount = 0;
_torpedoFired = false;
Commit: 571e6bbc83c05d1a84e97c3d789d5ba6d6508858
https://github.com/scummvm/scummvm/commit/571e6bbc83c05d1a84e97c3d789d5ba6d6508858
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:10:09+02:00
Commit Message:
SCUMM: RA1: Improve level 13 controls
Changed paths:
engines/scumm/insane/rebel1/iact.cpp
engines/scumm/insane/rebel1/render.cpp
diff --git a/engines/scumm/insane/rebel1/iact.cpp b/engines/scumm/insane/rebel1/iact.cpp
index 7d8ac754669..9371c359031 100644
--- a/engines/scumm/insane/rebel1/iact.cpp
+++ b/engines/scumm/insane/rebel1/iact.cpp
@@ -739,10 +739,10 @@ void InsaneRebel1::updateFlightVariantCursor() {
// preprocessMouseAxes â FUN_231BE (0x231BE) centered-axis output law, adapted to
// ScummVM's absolute 320x200 mouse space.
-// Preserve the DOS bias/offset persistence and one-frame jump latch from
-// FUN_231BE. Original-input mode also emulates FUN_23115's DOS mouse recenter;
-// gameplay hides and locks the cursor, so this follows the original without
-// exposing a visible pointer jump.
+// Original-input mode preserves the DOS bias/offset persistence and one-frame
+// jump latch from FUN_231BE, plus FUN_23115's DOS mouse recenter behavior.
+// Enhanced controls are ScummVM-only: they bypass the original recentering state
+// and expose a stable absolute centered mouse axis instead.
void InsaneRebel1::preprocessMouseAxes(int16 &inputX, int16 &inputY, bool *usedJoystick) {
if (usedJoystick)
*usedJoystick = false;
@@ -810,52 +810,64 @@ void InsaneRebel1::preprocessMouseAxes(int16 &inputX, int16 &inputY, bool *usedJ
int16 logicalX = (int16)CLIP<int>(_vm->_mouse.x, 0, 319);
int16 logicalY = (int16)CLIP<int>(_vm->_mouse.y, 0, 199);
+
+ if (_optEnhancedControls) {
+ _mouseVirtualValid = false;
+ _mouseBiasLatch = false;
+ _mouseBiasX = 0;
+ _mouseBiasY = 0;
+ _mousePrevBiasX = 0;
+ _mousePrevBiasY = 0;
+
+ inputX = (int16)CLIP<int32>(((int32)(logicalX - kRA1CenterX) * 127) / kRA1CenterX, -127, 127);
+ inputY = (int16)CLIP<int32>(((int32)(logicalY - kRA1CenterY) * 127) / kRA1CenterY, -127, 127);
+
+ if (_optControlsYFlip)
+ inputY = -inputY;
+
+ return;
+ }
+
int16 rawX = (int16)(logicalX << 1);
int16 rawY = logicalY;
- if (!_optEnhancedControls) {
- if (!_mouseVirtualValid) {
- _mouseVirtualRawX = rawX;
- _mouseVirtualRawY = rawY;
- _mouseVirtualValid = true;
- } else {
- _mouseVirtualRawX = (int16)CLIP<int>(
- _mouseVirtualRawX + ((logicalX - _mouseVirtualPrevLogicalX) << 1),
- -32768, 32767);
- _mouseVirtualRawY = (int16)CLIP<int>(
- _mouseVirtualRawY + (logicalY - _mouseVirtualPrevLogicalY),
- -32768, 32767);
- }
- _mouseVirtualPrevLogicalX = logicalX;
- _mouseVirtualPrevLogicalY = logicalY;
-
- rawX = _mouseVirtualRawX;
- rawY = _mouseVirtualRawY;
-
- if (rawX < kRA1DosMouseSafeLeft || rawX > kRA1DosMouseSafeRight ||
- rawY < kRA1DosMouseSafeTop || rawY > kRA1DosMouseSafeBottom) {
- _mouseOffsetX = (int16)CLIP<int>(
- (int)_mouseOffsetX + rawX - kRA1DosMouseCenterX, -32768, 32767);
- _mouseOffsetY = (int16)CLIP<int>(
- (int)_mouseOffsetY + rawY - kRA1DosMouseCenterY, -32768, 32767);
- rawX = kRA1DosMouseCenterX;
- rawY = kRA1DosMouseCenterY;
- _mouseVirtualRawX = rawX;
- _mouseVirtualRawY = rawY;
- _mouseVirtualPrevLogicalX = kRA1CenterX;
- _mouseVirtualPrevLogicalY = kRA1CenterY;
- _vm->_mouse.x = kRA1CenterX;
- _vm->_mouse.y = kRA1CenterY;
- smush_warpMouse(kRA1CenterX, kRA1CenterY, -1);
- debug(2, "RA1 original input virtual recenter: offset=(%d,%d) mouse=(%d,%d)",
- _mouseOffsetX, _mouseOffsetY, logicalX, logicalY);
- }
+ if (!_mouseVirtualValid) {
+ _mouseVirtualRawX = rawX;
+ _mouseVirtualRawY = rawY;
+ _mouseVirtualValid = true;
} else {
- _mouseVirtualValid = false;
+ _mouseVirtualRawX = (int16)CLIP<int>(
+ _mouseVirtualRawX + ((logicalX - _mouseVirtualPrevLogicalX) << 1),
+ -32768, 32767);
+ _mouseVirtualRawY = (int16)CLIP<int>(
+ _mouseVirtualRawY + (logicalY - _mouseVirtualPrevLogicalY),
+ -32768, 32767);
+ }
+ _mouseVirtualPrevLogicalX = logicalX;
+ _mouseVirtualPrevLogicalY = logicalY;
+
+ rawX = _mouseVirtualRawX;
+ rawY = _mouseVirtualRawY;
+
+ if (rawX < kRA1DosMouseSafeLeft || rawX > kRA1DosMouseSafeRight ||
+ rawY < kRA1DosMouseSafeTop || rawY > kRA1DosMouseSafeBottom) {
+ _mouseOffsetX = (int16)CLIP<int>(
+ (int)_mouseOffsetX + rawX - kRA1DosMouseCenterX, -32768, 32767);
+ _mouseOffsetY = (int16)CLIP<int>(
+ (int)_mouseOffsetY + rawY - kRA1DosMouseCenterY, -32768, 32767);
+ rawX = kRA1DosMouseCenterX;
+ rawY = kRA1DosMouseCenterY;
+ _mouseVirtualRawX = rawX;
+ _mouseVirtualRawY = rawY;
+ _mouseVirtualPrevLogicalX = kRA1CenterX;
+ _mouseVirtualPrevLogicalY = kRA1CenterY;
+ _vm->_mouse.x = kRA1CenterX;
+ _vm->_mouse.y = kRA1CenterY;
+ smush_warpMouse(kRA1CenterX, kRA1CenterY, -1);
+ debug(2, "RA1 original input virtual recenter: offset=(%d,%d) mouse=(%d,%d)",
+ _mouseOffsetX, _mouseOffsetY, logicalX, logicalY);
}
- const int16 normX = (int16)(((int32)(logicalX - kRA1CenterX) * 127) / 160);
- const int16 normY = (int16)(((int32)(logicalY - kRA1CenterY) * 127) / 100);
int16 biasX = (int16)((rawX + _mouseOffsetX - kRA1DosMouseCenterX) >> 2);
int16 biasY = (int16)((rawY + _mouseOffsetY - kRA1DosMouseCenterY) >> 1);
@@ -880,12 +892,6 @@ void InsaneRebel1::preprocessMouseAxes(int16 &inputX, int16 &inputY, bool *usedJ
_mouseBiasLatch = true;
}
- // In original mouse mode, FUN_2309C centers the joystick contribution when
- // joystick is disabled, so FUN_231BE's mouse output is just the bias term.
- // Enhanced mode keeps ScummVM's pre-existing centered mouse axis plus bias.
- const int16 scaledX = _optEnhancedControls ? (int16)(normX + biasX) : biasX;
- const int16 scaledY = _optEnhancedControls ? (int16)(normY + biasY) : biasY;
-
_mouseBiasX = biasX;
_mouseBiasY = biasY;
_mousePrevBiasX = biasX;
@@ -931,8 +937,8 @@ void InsaneRebel1::preprocessMouseAxes(int16 &inputX, int16 &inputY, bool *usedJ
_mouseOffsetY -= 4;
}
- inputX = CLIP<int16>(scaledX, -0xA0, 0xA0);
- inputY = CLIP<int16>(scaledY, -127, 127);
+ inputX = CLIP<int16>(biasX, -0xA0, 0xA0);
+ inputY = CLIP<int16>(biasY, -127, 127);
// Controls Y-flip option (DAT_22be in original)
if (_optControlsYFlip)
@@ -1260,18 +1266,16 @@ void InsaneRebel1::updateTurretPhysics() {
_damageCooldown = 0;
}
- // GAME 0x0A appears before 0x08 in L1PLAY2.ANM. The original therefore
+ // GAME 0x0A appears before 0x08 in turret/combat ANMs. The original
// draws shots/targeting and the ship from this pre-physics center, then
// updates the camera offset at the end of 0x08 for the final viewport copy.
const int16 preMoveOffsetX = (int16)(_posAccumX >> 8);
const int16 preMoveOffsetY = (int16)(_posAccumY >> 8);
- if (_currentLevel == 0 && _flyControlMode == 2) {
- _turretFrameShipOffsetX = preMoveOffsetX;
- _turretFrameShipOffsetY = preMoveOffsetY;
- _turretFrameShipCenterX = (int16)(kRA1CenterX + preMoveOffsetX);
- _turretFrameShipCenterY = (int16)(kRA1CenterY + preMoveOffsetY);
- _turretFrameShipCenterValid = true;
- }
+ _turretFrameShipOffsetX = preMoveOffsetX;
+ _turretFrameShipOffsetY = preMoveOffsetY;
+ _turretFrameShipCenterX = (int16)(kRA1CenterX + preMoveOffsetX);
+ _turretFrameShipCenterY = (int16)(kRA1CenterY + preMoveOffsetY);
+ _turretFrameShipCenterValid = true;
// Damage gate from FUN_1E6A7.
if (_damageFlags != 0 && _damageCooldown == 0 && _health >= 0 && _deathTimer <= 0) {
@@ -1316,9 +1320,9 @@ void InsaneRebel1::updateTurretPhysics() {
const int16 rawInputX = inputX;
const int16 rawInputY = inputY;
- if (usedJoystick && _optEnhancedControls) {
- // First-person turret/cockpit stages are noticeably more sensitive on
- // joystick than on mouse, so damp only the joystick-driven input here.
+ if (usedJoystick && _optEnhancedControls && _flyControlMode == 2) {
+ // ScummVM-only concession for Level 1 part 2. The original 0x08 handler
+ // uses raw axes directly; do not damp Level 13's surface controls.
inputX /= 2;
inputY /= 2;
}
diff --git a/engines/scumm/insane/rebel1/render.cpp b/engines/scumm/insane/rebel1/render.cpp
index 5943b1c6b91..54b5b2b96cf 100644
--- a/engines/scumm/insane/rebel1/render.cpp
+++ b/engines/scumm/insane/rebel1/render.cpp
@@ -467,24 +467,18 @@ void renderBucketedShotSprite(byte *dst, int pitch, int width, int height,
}
void InsaneRebel1::getTurretShipCenter(int16 &x, int16 &y) const {
- if (_currentLevel == 0 && _flyControlMode == 2) {
- // Port helper: original Level 1 part 2 keeps g_perspectiveX/Y at the
- // screen center and draws the overhead ship at g_shipOffsetX/Y from
- // FUN_1E6A7. In this port _perspectiveX/Y stores the clamped camera
- // offset passed to SetCameraOffset(), so recover the ship center from
- // the movement accumulators instead.
- if (_turretFrameShipCenterValid) {
- x = _turretFrameShipCenterX;
- y = _turretFrameShipCenterY;
- } else {
- x = (int16)(kRA1CenterX + (int16)(_posAccumX >> 8));
- y = (int16)(kRA1CenterY + (int16)(_posAccumY >> 8));
- }
+ // Port helper: original FUN_1E6A7 keeps g_perspectiveX/Y at screen center
+ // and draws the ship at g_perspective + g_shipOffset. In this port
+ // _perspectiveX/Y stores the clamped camera offset passed to SetCameraOffset(),
+ // so recover the ship center from the movement accumulators instead.
+ if (_turretFrameShipCenterValid) {
+ x = _turretFrameShipCenterX;
+ y = _turretFrameShipCenterY;
return;
}
- x = (int16)(kRA1CenterX + (_perspectiveX - 0x20));
- y = (int16)(kRA1CenterY + (_perspectiveY - 0x17));
+ x = (int16)(kRA1CenterX + (int16)(_posAccumX >> 8));
+ y = (int16)(kRA1CenterY + (int16)(_posAccumY >> 8));
}
// procPreRendering â Sets viewport window offset (FUN_224FD at 0x224FD).
@@ -689,18 +683,16 @@ void InsaneRebel1::procPostRendering(byte *renderBitmap, int32 codecparam, int32
renderGostSlots(renderBitmap, pitch, width, height);
renderTargeting(renderBitmap, pitch, width, height);
- // FUN_1D79C (GAME 0x0A) owns the cursor center in stage-2 turret mode.
+ // FUN_1D79C (GAME 0x0A) owns the cursor center in turret/combat mode.
// The preceding overlay/shot pass uses the previous frame's cursor; the
// handler then publishes the next cursor position from the current
- // pre-physics ship offset. In L1PLAY2 the following 0x08 handler updates
- // the camera afterward, so source-space anchors and the final viewport
- // crop intentionally observe different moments in the frame.
+ // pre-physics ship offset. The following 0x08 handler updates the camera
+ // afterward, so source-space anchors and the final viewport crop
+ // intentionally observe different moments in the frame.
if (turretTargetingMode) {
- const bool useTurretFrameCenter =
- (_currentLevel == 0 && _flyControlMode == 2 && _turretFrameShipCenterValid);
- const int16 shipOffsetX = useTurretFrameCenter ?
+ const int16 shipOffsetX = _turretFrameShipCenterValid ?
_turretFrameShipOffsetX : (int16)(_posAccumX >> 8);
- const int16 shipOffsetY = useTurretFrameCenter ?
+ const int16 shipOffsetY = _turretFrameShipCenterValid ?
_turretFrameShipOffsetY : (int16)(_posAccumY >> 8);
_shipPosX = (int16)(kRA1CenterX + shipOffsetX);
_shipPosY = (int16)((kRA1CenterY + shipOffsetY - 0x23) - (shipOffsetY >> 3));
Commit: 298177ad654608568c5c3ebcd1bfb5471a1cc945
https://github.com/scummvm/scummvm/commit/298177ad654608568c5c3ebcd1bfb5471a1cc945
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:10:09+02:00
Commit Message:
SCUMM: RA1: Render HUD only during active gameplay
Changed paths:
engines/scumm/insane/rebel1/iact.cpp
engines/scumm/insane/rebel1/rebel.cpp
engines/scumm/insane/rebel1/rebel.h
engines/scumm/insane/rebel1/render.cpp
engines/scumm/insane/rebel1/runlevels.cpp
engines/scumm/smush/rebel/smush_player_ra1.cpp
diff --git a/engines/scumm/insane/rebel1/iact.cpp b/engines/scumm/insane/rebel1/iact.cpp
index 9371c359031..e38cc3f1625 100644
--- a/engines/scumm/insane/rebel1/iact.cpp
+++ b/engines/scumm/insane/rebel1/iact.cpp
@@ -1899,6 +1899,7 @@ void InsaneRebel1::handleGameChunk(int32 subSize, Common::SeekableReadStream &b)
uint32 opcode = b.readUint32BE();
uint32 param1 = b.readUint32BE();
+ _frameHasGameChunk = true;
// FUN_1BE1B applies two global gates before the opcode switch. Bit 0 of
// g_combatModeFlags skips gameplay dispatch entirely; bit 5 of g_hudDisableFlags
diff --git a/engines/scumm/insane/rebel1/rebel.cpp b/engines/scumm/insane/rebel1/rebel.cpp
index 90cd418cf80..3e60a5271e1 100644
--- a/engines/scumm/insane/rebel1/rebel.cpp
+++ b/engines/scumm/insane/rebel1/rebel.cpp
@@ -281,6 +281,7 @@ InsaneRebel1::InsaneRebel1(ScummEngine_v7 *scumm) : Insane(), _vm(scumm) {
_turretEmitterRightY = 0;
_activeGameOpcode = 0;
_frameGameOpcodeMask = 0;
+ _frameHasGameChunk = false;
_frameDispatchFlags = 0;
_gameOp0BPhysicsUpdatedThisFrame = false;
diff --git a/engines/scumm/insane/rebel1/rebel.h b/engines/scumm/insane/rebel1/rebel.h
index 600a8567a28..05a57f7ea22 100644
--- a/engines/scumm/insane/rebel1/rebel.h
+++ b/engines/scumm/insane/rebel1/rebel.h
@@ -86,6 +86,7 @@ public:
void handleGameChunk(int32 subSize, Common::SeekableReadStream &b);
bool isInteractiveVideoActive() const { return _interactiveVideoActive; }
+ void setFrameHasGameChunk(bool hasGameChunk) { _frameHasGameChunk = hasGameChunk; }
int getCurrentLevel() const { return _currentLevel; }
uint16 getActiveGameOpcode() const { return _activeGameOpcode; }
uint16 getEffectiveGameOpcode() const;
@@ -359,6 +360,7 @@ private:
// Kept for legacy call sites; frame-accurate dispatch uses _frameGameOpcodeMask.
uint16 _activeGameOpcode;
uint32 _frameGameOpcodeMask;
+ bool _frameHasGameChunk;
uint16 _frameDispatchFlags;
bool _gameOp0BPhysicsUpdatedThisFrame;
diff --git a/engines/scumm/insane/rebel1/render.cpp b/engines/scumm/insane/rebel1/render.cpp
index 54b5b2b96cf..bf043391e19 100644
--- a/engines/scumm/insane/rebel1/render.cpp
+++ b/engines/scumm/insane/rebel1/render.cpp
@@ -487,14 +487,16 @@ void InsaneRebel1::getTurretShipCenter(int16 &x, int16 &y) const {
void InsaneRebel1::procPreRendering(byte *renderBitmap) {
_frameGameOpcodeMask = 0;
_frameDispatchFlags = 0;
+ _hudRenderFlag = 0;
_gameOp0BPhysicsUpdatedThisFrame = false;
_turretFrameShipCenterValid = false;
if (_interactiveVideoActive && _player) {
const bool usePerspectiveViewport =
- _activeGameOpcode == 0x07 || _activeGameOpcode == 0x08 ||
- _activeGameOpcode == 0x09 || _activeGameOpcode == 0x0A ||
- _activeGameOpcode == 0x0B;
+ _frameHasGameChunk &&
+ (_activeGameOpcode == 0x07 || _activeGameOpcode == 0x08 ||
+ _activeGameOpcode == 0x09 || _activeGameOpcode == 0x0A ||
+ _activeGameOpcode == 0x0B);
// Only gameplay handlers that actually execute FUN_224FD own the scrolling
// 320x200 window inside the 384x242 buffer. Interactive movies with no
// GAME stream (for example LVL4/L4PLAY2.ANM) keep a static camera.
@@ -570,19 +572,23 @@ void InsaneRebel1::procPostRendering(byte *renderBitmap, int32 codecparam, int32
if (height == 0) height = _screenHeight;
int pitch = width;
- // Set HUD render flag when interactive gameplay is active (0x7600)
- _hudRenderFlag = 0xFF;
+ if (!_frameHasGameChunk) {
+ syncViewportOffset(false);
+ _fireCooldown = _playerFired ? 1 : 0;
+ return;
+ }
const bool haveFrameGameOpcodes = (_frameGameOpcodeMask != 0);
+ const bool allowImplicitGameplayMode = _frameHasGameChunk && !haveFrameGameOpcodes;
const bool gameOp0BMode = hasFrameGameOpcode(0x0B) ||
- (!haveFrameGameOpcodes && _activeGameOpcode == 0x0B);
+ (allowImplicitGameplayMode && _activeGameOpcode == 0x0B);
const bool onFootMode = hasFrameGameOpcode(0x19) || hasFrameGameOpcode(0x1A) ||
- (!haveFrameGameOpcodes &&
+ (allowImplicitGameplayMode &&
(_activeGameOpcode == 0x19 || _activeGameOpcode == 0x1A));
const bool turretMode = hasFrameGameOpcode(0x08) || hasFrameGameOpcode(0x0A) ||
- (!haveFrameGameOpcodes && (_activeGameOpcode == 0x08 || _activeGameOpcode == 0x0A));
+ (allowImplicitGameplayMode && (_activeGameOpcode == 0x08 || _activeGameOpcode == 0x0A));
const bool flightMode = hasFrameGameOpcode(0x07) || hasFrameGameOpcode(0x09) ||
- (!haveFrameGameOpcodes &&
+ (allowImplicitGameplayMode &&
(_activeGameOpcode == 0x07 || _activeGameOpcode == 0x09));
if (gameOp0BMode) {
// GAME 0x0B scrolling cockpit/surface handler â FUN_1CDA7.
@@ -661,16 +667,16 @@ void InsaneRebel1::procPostRendering(byte *renderBitmap, int32 codecparam, int32
const bool hasTargetingPipeline =
hasFrameGameOpcode(0x09) || hasFrameGameOpcode(0x0A) ||
hasFrameGameOpcode(0x0B) || hasFrameGameOpcode(0x1A) ||
- (!haveFrameGameOpcodes &&
+ (allowImplicitGameplayMode &&
(_activeGameOpcode == 0x09 || _activeGameOpcode == 0x0A ||
_activeGameOpcode == 0x0B || _activeGameOpcode == 0x1A));
if (hasTargetingPipeline) {
const bool flightVariantTargetingMode =
hasFrameGameOpcode(0x09) ||
- (!haveFrameGameOpcodes && _activeGameOpcode == 0x09);
+ (allowImplicitGameplayMode && _activeGameOpcode == 0x09);
const bool turretTargetingMode =
hasFrameGameOpcode(0x0A) ||
- (!haveFrameGameOpcodes && _activeGameOpcode == 0x0A);
+ (allowImplicitGameplayMode && _activeGameOpcode == 0x0A);
renderTargetBoxes(renderBitmap, pitch, width, height);
processShot();
renderLaserShots(renderBitmap, pitch, width, height);
@@ -728,7 +734,8 @@ void InsaneRebel1::procPostRendering(byte *renderBitmap, int32 codecparam, int32
renderLevel8Overlay(renderBitmap, pitch, width, height, viewportX, viewportY);
}
- renderHUD(renderBitmap, pitch, width, height);
+ if (gameOp0BMode || onFootMode || turretMode || flightMode || _hudRenderFlag != 0)
+ renderHUD(renderBitmap, pitch, width, height);
_fireCooldown = _playerFired ? 1 : 0;
}
@@ -830,6 +837,11 @@ void InsaneRebel1::handleLevel14Play2BSplice(int32 curFrame, int32 maxFrame) {
_level14Play2BSplicePending = true;
_level14Play2BSpliceFrame = curFrame;
+ // DOS queues the continuation from inside the active playback loop, so the
+ // STOR/FTCH video state remains live across the L14PLAY2 -> L14PLY2B jump.
+ if (_player)
+ _player->setPreserveGameVideoStateOnRelease(true);
+
// Original after PlayAnmFile("LVL14/L14PLY2B.ANM", 0x860, maxFrame-0x0F, 1, -1):
// g_extendedPhaseFlags &= 0xFA;
// DAT_7604 &= 0xBF;
diff --git a/engines/scumm/insane/rebel1/runlevels.cpp b/engines/scumm/insane/rebel1/runlevels.cpp
index 2b371a8ae1d..29bec475b90 100644
--- a/engines/scumm/insane/rebel1/runlevels.cpp
+++ b/engines/scumm/insane/rebel1/runlevels.cpp
@@ -235,6 +235,7 @@ void InsaneRebel1::playCinematic(const char *filename, int32 startFrame) {
debug(1, "InsaneRebel1::playCinematic('%s', startFrame=%d)", filename, startFrame);
SmushPlayer *splayer = _vm->_splayer;
_player = splayer;
+ restoreScreenFlashPalette();
_interactiveVideoActive = false;
_vm->_smushVideoShouldFinish = false;
splayer->setCurVideoFlags(0x28); // Cinematic mode + buffer preserve
@@ -2032,6 +2033,7 @@ void InsaneRebel1::playInteractiveVideo(const char *filename, int32 startFrame)
SmushPlayer *splayer = _vm->_splayer;
_player = splayer;
+ restoreScreenFlashPalette();
if (!resumingRoute)
clearBit(0);
_interactiveVideoActive = true;
diff --git a/engines/scumm/smush/rebel/smush_player_ra1.cpp b/engines/scumm/smush/rebel/smush_player_ra1.cpp
index acca9e27cc2..ef27996d58f 100644
--- a/engines/scumm/smush/rebel/smush_player_ra1.cpp
+++ b/engines/scumm/smush/rebel/smush_player_ra1.cpp
@@ -242,7 +242,8 @@ bool SmushPlayerRebel1::handleGameFetch(int32 subSize, Common::SeekableReadStrea
// directly â no projection-based FTCH placement. Level 4 phase 2
// also stores a full-width screen-space patch (320x180) that DOS
// restores without the centered 1/4 projection warp.
- if (fullWidthStoredPatch || gameOp == 0x19 || gameOp == 0x1A) {
+ if (fullWidthStoredPatch || gameOp == 0x0B ||
+ gameOp == 0x19 || gameOp == 0x1A) {
left += _ra1ViewportOffsetX;
top += _ra1ViewportOffsetY;
} else {
@@ -976,13 +977,14 @@ void SmushPlayerRebel1::handleFrame(int32 frameSize, Common::SeekableReadStream
if (_insane) {
InsaneRebel1 *rebel1 = static_cast<InsaneRebel1 *>(_insane);
bool interactive = rebel1->isInteractiveVideoActive();
+ const bool frameHasGameChunk = interactive && ra1FrameHasGameChunk(b, frameSize);
+ rebel1->setFrameHasGameChunk(frameHasGameChunk);
const uint16 activeOpcode = rebel1->getActiveGameOpcode();
bool forceClear = interactive &&
(activeOpcode == 0x0B ||
(activeOpcode == 0 && rebel1->getCurrentLevel() == 1));
- preserveFrameHistory = interactive && !forceClear &&
- ra1FrameHasGameChunk(b, frameSize);
+ preserveFrameHistory = interactive && !forceClear && frameHasGameChunk;
}
// Restore clean frame for delta source
Commit: 24f60c962aba4f57035f15deb87d4b61d240a509
https://github.com/scummvm/scummvm/commit/24f60c962aba4f57035f15deb87d4b61d240a509
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:10:09+02:00
Commit Message:
SCUMM: RA1: Improve torpedo mode rendering
Changed paths:
engines/scumm/insane/rebel1/iact.cpp
engines/scumm/insane/rebel1/rebel.h
engines/scumm/insane/rebel1/render.cpp
diff --git a/engines/scumm/insane/rebel1/iact.cpp b/engines/scumm/insane/rebel1/iact.cpp
index e38cc3f1625..90022a4cafa 100644
--- a/engines/scumm/insane/rebel1/iact.cpp
+++ b/engines/scumm/insane/rebel1/iact.cpp
@@ -1715,6 +1715,10 @@ void InsaneRebel1::updateGameOp0BPhysics() {
// g_gameplayPhaseFlags & 2 becomes set.
if (_currentLevel == 14 && _levelGameplayPhase == 2) {
if (_frameCounter == 0x18A) {
+ // Original writes the 16-bit flags word: g_hudDisableFlags |= 0x210.
+ // Low byte bit 0x10 suppresses normal hit feedback in the torpedo phase;
+ // high byte bit 0x02 switches targeting/shot rendering to torpedoes.
+ _gameplayFlags75fe |= 0x10;
_gameplayFlags75ff |= 2;
_protectedTargetA = 0x67;
_protectedTargetB = 0x69;
@@ -1732,6 +1736,18 @@ void InsaneRebel1::updateGameOp0BPhysics() {
_perspectiveX, _perspectiveY, _health, _screenFlash);
}
+bool InsaneRebel1::isTorpedoModeActive() const {
+ if ((_gameplayFlags75ff & 0x2) == 0)
+ return false;
+
+ // The original high-byte flag is only intentionally armed by the level 4
+ // torpedo run and the level 15 exhaust-port run. Gate the port's rendering
+ // and shot behavior to those phases so stale route/retry state cannot turn
+ // ordinary laser sections into torpedo mode.
+ return (_currentLevel == 3 && _levelGameplayPhase == 2) ||
+ (_currentLevel == 14 && _levelGameplayPhase == 2);
+}
+
// updateOnFootPhysics â ScummVM-side glue for original on-foot GAME handlers
// HandleGameOp19_OnFootSequence (0x19) and HandleGameOp1A_OnFootVariant (0x1A).
@@ -2289,14 +2305,15 @@ void InsaneRebel1::processShot() {
const int16 cursorX = getGameplayCursorX();
const int16 cursorY = getGameplayCursorY();
- _shotSlots[slot].timer = (_gameplayFlags75ff & 0x2) ? 2 : 5;
+ const bool torpedoMode = isTorpedoModeActive();
+ _shotSlots[slot].timer = torpedoMode ? 2 : 5;
_shotSlots[slot].posX = cursorX;
_shotSlots[slot].posY = cursorY;
_shotSlots[slot].centerX = originX;
_shotSlots[slot].centerY = originY;
_shotSlots[slot].variant = _shotAlternator;
_shotAlternator = 1 - _shotAlternator;
- playSfx((_gameplayFlags75ff & 0x2) ? kSfxAlert : kSfxLaserShot, 127, 0);
+ playSfx(torpedoMode ? kSfxAlert : kSfxLaserShot, 127, 0);
if (effectiveOpcode == 0x09 || _currentLevel == 4) {
debug(1, "RA1 shot: opcode=0x%02x frame=%d slot=%d cursor=(%d,%d) origin=(%d,%d) dir=%d mode=%d",
@@ -2406,7 +2423,8 @@ void InsaneRebel1::checkTargetHit(int16 targetIdx, int16 left, int16 top, int16
int16 hitCenterY = (top + bottom) / 2;
projectGameplayPoint(hitCenterX, hitCenterY);
const int sfxPan = CLIP((hitCenterX - kRA1CenterX) * 127 / kRA1CenterX, -127, 127);
- playSfx(kSfxExplode, 127, sfxPan);
+ if ((_gameplayFlags75fe & 0x10) == 0)
+ playSfx(kSfxExplode, 127, sfxPan);
debug(3, "RA1 HIT: target=%d gost=%d pos=(%d,%d) score=%d kills=%d bangSprites=%d",
targetIdx, gi, _gostSlots[gi].posX, _gostSlots[gi].posY,
diff --git a/engines/scumm/insane/rebel1/rebel.h b/engines/scumm/insane/rebel1/rebel.h
index 05a57f7ea22..8ba2f6576ad 100644
--- a/engines/scumm/insane/rebel1/rebel.h
+++ b/engines/scumm/insane/rebel1/rebel.h
@@ -193,6 +193,7 @@ private:
// Shooting pipeline â FUN_1CCA0 (0x1CCA0) shot spawner,
// FUN_1C0EF (0x1C0EF) target detection, FUN_1C940 (0x1C940) shot processing
+ bool isTorpedoModeActive() const;
void processShot();
void checkTargetHit(int16 targetIdx, int16 left, int16 top, int16 right, int16 bottom);
diff --git a/engines/scumm/insane/rebel1/render.cpp b/engines/scumm/insane/rebel1/render.cpp
index bf043391e19..f02ad7fb121 100644
--- a/engines/scumm/insane/rebel1/render.cpp
+++ b/engines/scumm/insane/rebel1/render.cpp
@@ -773,14 +773,14 @@ void InsaneRebel1::renderTargetBoxes(byte *dst, int pitch, int width, int height
// The original does not draw a hardcoded pixel cross; it renders glyph markers
// whose state depends on _targetProximity.
void InsaneRebel1::renderTargeting(byte *dst, int pitch, int width, int height) {
- const char kRA1TorpedoIndicator[] = "<d";
+ const char kRA1TorpedoIndicator[] = "<<d";
const RA1SpriteBank &markerBank = (_techFontBank.numSprites > 0) ? _techFontBank : _hudFontBank;
const int overlayX = ra1OverlayViewOffsetX(this);
const int overlayY = ra1OverlayViewOffsetY(this);
if (markerBank.numSprites > 0) {
// FUN_1CB22 can switch marker sets via DAT_75FF bit 1.
// Baseline RA1 targeting uses '^' and animation e..h.
- const bool altMarkerSet = (_gameplayFlags75ff & 0x2) != 0;
+ const bool altMarkerSet = isTorpedoModeActive();
// DAT_75FF bit 2 suppresses the fixed lock/readiness overlay.
if ((_gameplayFlags75ff & 0x4) == 0) {
@@ -879,6 +879,9 @@ void InsaneRebel1::renderGostScorePopup(byte *dst, int pitch, int width, int hei
// renderGostSlots â FUN_1C9CD (0x1C9CD). Hit explosion animations at target positions.
// Renders explosion sprites from bangBank + per-kill score popup glyphs.
void InsaneRebel1::renderGostSlots(byte *dst, int pitch, int width, int height) {
+ if ((_gameplayFlags75fe & 0x10) != 0)
+ return;
+
if (_bangBank.numSprites <= 0) {
// Warn if there are active GOST slots but no bang sprites to render
for (int i = 0; i < kMaxGostSlots; i++) {
@@ -1013,7 +1016,7 @@ void InsaneRebel1::renderLevel13EnemyShots(byte *dst, int pitch, int width, int
void InsaneRebel1::renderLaserShots(byte *dst, int pitch, int width, int height) {
const char kRA1TorpedoTrailLeft[] = "<<&";
const char kRA1TorpedoTrailRight[] = "<<'";
- const bool torpedoMode = (_gameplayFlags75ff & 0x2) != 0;
+ const bool torpedoMode = isTorpedoModeActive();
if (_laserBank.numSprites <= 0 && !torpedoMode)
return;
Commit: ec4cff3a96833e1e9fad5d5bf443a2815708f79f
https://github.com/scummvm/scummvm/commit/ec4cff3a96833e1e9fad5d5bf443a2815708f79f
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:10:10+02:00
Commit Message:
SCUMM: RA1: Fix level 9 on-foot targeting
Changed paths:
engines/scumm/insane/rebel1/iact.cpp
engines/scumm/insane/rebel1/rebel.cpp
engines/scumm/insane/rebel1/rebel.h
engines/scumm/insane/rebel1/render.cpp
engines/scumm/insane/rebel1/runlevels.cpp
diff --git a/engines/scumm/insane/rebel1/iact.cpp b/engines/scumm/insane/rebel1/iact.cpp
index 90022a4cafa..facc373470e 100644
--- a/engines/scumm/insane/rebel1/iact.cpp
+++ b/engines/scumm/insane/rebel1/iact.cpp
@@ -549,16 +549,33 @@ bool InsaneRebel1::handleFrameObjectTarget(int16 objectId, int16 left, int16 top
const byte bit = (byte)(0x80 >> (bitIndex & 7));
const int altIndex = byteIndex + 0x96;
- const bool primarySet = (_frameObjectState[byteIndex] & bit) != 0;
- const bool secondarySet = (altIndex < kFrameObjectStateBytes) && ((_frameObjectState[altIndex] & bit) != 0);
const int16 right = left + width;
const int16 bottom = top + height;
+ if (objectId >= 0x280 && _frameObjectHitRevealPending) {
+ // DispatchSmushFrameChunks keeps a one-object latch after a target hit.
+ // The following high-id FOBJ clears its hidden bit and becomes the
+ // replacement/death frame. This is used heavily by Level 9 troopers.
+ _frameObjectState[byteIndex] &= ~bit;
+ _frameObjectHitRevealPending = false;
+ debug(5, "RA1 FOBJ reveal: object=%d frameObjectByte=%d bit=0x%02x",
+ objectId, byteIndex, bit);
+ }
+
+ const bool primarySet = (_frameObjectState[byteIndex] & bit) != 0;
+ const bool secondarySet = (altIndex < kFrameObjectStateBytes) && ((_frameObjectState[altIndex] & bit) != 0);
+
if (objectId > 0 && objectId < 0x280) {
- if (!primarySet || secondarySet)
+ if (!primarySet || secondarySet) {
+ const int16 previousKillCount = _killCount;
checkTargetHit(objectId - 1, left, top, right, bottom);
- else
+ const bool updatedPrimarySet = (_frameObjectState[byteIndex] & bit) != 0;
+ _frameObjectHitRevealPending =
+ (_killCount != previousKillCount) && !primarySet && updatedPrimarySet;
+ } else {
+ _frameObjectHitRevealPending = false;
updateGostSlotPosition(objectId - 1, left, top, right, bottom);
+ }
}
const bool updatedPrimarySet = (_frameObjectState[byteIndex] & bit) != 0;
@@ -1749,7 +1766,7 @@ bool InsaneRebel1::isTorpedoModeActive() const {
}
-// updateOnFootPhysics â ScummVM-side glue for original on-foot GAME handlers
+// ScummVM-side splits for the original on-foot GAME handlers:
// HandleGameOp19_OnFootSequence (0x19) and HandleGameOp1A_OnFootVariant (0x1A).
// On-foot handler for Level 9 (Stormtroopers). Character walks left/right, crosshair tracks mouse.
//
@@ -1763,7 +1780,7 @@ const int16 kOnFootCenterY = 0x82; // g_perspectiveY in HandleGameOp19
// Port split matching HandleGameOp19_OnFootSequence. The helper name is new to
// this implementation; the original code dispatches the opcode handler directly.
-void InsaneRebel1::updateOnFootSequence() {
+void InsaneRebel1::initOnFootSequence() {
// --- First-frame initialization (0x19 counter==0) ---
if (!_onFootInitialized) {
_onFootInitialized = true;
@@ -1780,8 +1797,14 @@ void InsaneRebel1::updateOnFootSequence() {
_perspectiveY = 0;
resetProjectionTable();
}
+}
- // --- 0x19: Character walk animation + damage ---
+// Port split matching HandleGameOp19_OnFootSequence. The helper name is new to
+// this implementation; the original code dispatches the opcode handler directly.
+void InsaneRebel1::updateOnFootSequence() {
+ initOnFootSequence();
+
+ // --- 0x19: Post-draw character walk animation + damage ---
// Track fire button for animation
if (!_playerFired)
_onFootAnimCounter = 0;
@@ -1801,19 +1824,16 @@ void InsaneRebel1::updateOnFootSequence() {
// Right edge: snap to center, step character right
_shipDirIndex = 15;
_onFootCharX += 0x3A;
- } else if (_onFootAnimCounter < 5 && !_playerFired) {
- // Aim direction from crosshair toward character (QuantizeDirection8Way)
- int16 dx = _shipPosX - (_onFootCharX + kOnFootCenterX);
- int16 aimDir = 0;
- if (dx > 30)
- aimDir = 4;
- else if (dx > 10)
- aimDir = 2;
- else if (dx < -30)
- aimDir = -4;
- else if (dx < -10)
- aimDir = -2;
- _shipDirIndex = CLIP<int16>(aimDir + 15, 11, 19);
+ } else if (_onFootAnimCounter < 5 && !(_playerFired && _fireCooldown == 0)) {
+ // Original calls QuantizeDirection8Way with the cursor and character
+ // center, but the DOS on-foot axis is mirrored relative to the screen
+ // coordinates used by this port. Use the visual screen-space vector so
+ // L9PILOT.NUT poses 11..14 aim left and 16..19 aim right.
+ const int16 centerX = _onFootCharX + kOnFootCenterX;
+ const int16 centerY = _onFootCharY + kOnFootCenterY;
+ const int16 aimDir = CLIP<int16>(
+ (int16)ra1ShotDirection(centerX, centerY, _shipPosX, _shipPosY), -4, 4);
+ _shipDirIndex = aimDir + 15;
} else {
// Walking based on mouse input direction
int16 inputX = 0, inputY = 0;
@@ -1861,18 +1881,7 @@ void InsaneRebel1::updateOnFootAimVariant() {
_shipPosY = inputYNeg + kOnFootCenterY + _onFootCharY - 0x32;
}
-void InsaneRebel1::updateOnFootPhysics() {
- const bool haveFrameGameOpcodes = (_frameGameOpcodeMask != 0);
- const bool sequenceOpcode =
- hasFrameGameOpcode(0x19) || (!haveFrameGameOpcodes && _activeGameOpcode == 0x19);
- const bool aimOpcode =
- hasFrameGameOpcode(0x1A) || (!haveFrameGameOpcodes && _activeGameOpcode == 0x1A);
-
- if (sequenceOpcode)
- updateOnFootSequence();
- if (aimOpcode)
- updateOnFootAimVariant();
-
+void InsaneRebel1::finishOnFootFrame() {
if (_damageCooldown > 0)
_damageCooldown--;
diff --git a/engines/scumm/insane/rebel1/rebel.cpp b/engines/scumm/insane/rebel1/rebel.cpp
index 3e60a5271e1..b90af71f368 100644
--- a/engines/scumm/insane/rebel1/rebel.cpp
+++ b/engines/scumm/insane/rebel1/rebel.cpp
@@ -385,6 +385,7 @@ InsaneRebel1::InsaneRebel1(ScummEngine_v7 *scumm) : Insane(), _vm(scumm) {
_gostSlotIdx = 0;
_killCount = 0;
_lastHitTarget = 0;
+ _frameObjectHitRevealPending = false;
resetEnemyShotSlots();
_protectedTargetA = 0;
_protectedTargetB = 0;
diff --git a/engines/scumm/insane/rebel1/rebel.h b/engines/scumm/insane/rebel1/rebel.h
index 8ba2f6576ad..83fbb415459 100644
--- a/engines/scumm/insane/rebel1/rebel.h
+++ b/engines/scumm/insane/rebel1/rebel.h
@@ -63,6 +63,8 @@ static const int16 kRA1MaxY = 180;
static const int16 kRA1FocalX = 43;
static const int16 kRA1FocalY = 25;
+int ra1ShotDirection(int16 x1, int16 y1, int16 x2, int16 y2);
+
/**
* Star Wars: Rebel Assault (RA1) game logic.
* Adapts RA2 Handler 7 (ship flight) physics for RA1's 384x242 resolution.
@@ -172,6 +174,8 @@ private:
void renderGostScorePopup(byte *dst, int pitch, int width, int height,
int16 centerX, int16 centerY, int16 frame);
void renderLaserShots(byte *dst, int pitch, int width, int height);
+ void renderShotOverlayPipeline(byte *dst, int pitch, int width, int height,
+ bool drawTargetBoxes);
void handleLevel14Play2BSplice(int32 curFrame, int32 maxFrame);
void renderLevel11HitsOverlay(byte *dst, int pitch, int width, int height);
void resetEnemyShotSlots();
@@ -309,9 +313,10 @@ private:
void restoreScreenFlashPalette();
// 0x19/0x1A on-foot handler (Level 9 Stormtroopers)
- void updateOnFootPhysics();
+ void initOnFootSequence();
void updateOnFootSequence();
void updateOnFootAimVariant();
+ void finishOnFootFrame();
int16 _onFootCharX; // Character draw X (g_shipOffsetX in original)
int16 _onFootCharY; // Character draw Y (g_shipOffsetY in original)
int16 _onFootAnimCounter; // DAT_0000828a: fire animation counter
@@ -558,6 +563,7 @@ private:
int16 _killCount; // 0x75D0: targets destroyed this stage
int16 _lastHitTarget; // 0x75D6: recent-kill latch, allows at most one hit per frame
+ bool _frameObjectHitRevealPending; // DispatchSmushFrameChunks local_14 high-id reveal latch
// Incoming enemy projectile slots used by Level 13 RunLevel13Flow.
static const int kMaxEnemyShotSlots = 5;
diff --git a/engines/scumm/insane/rebel1/render.cpp b/engines/scumm/insane/rebel1/render.cpp
index f02ad7fb121..3702b7029b3 100644
--- a/engines/scumm/insane/rebel1/render.cpp
+++ b/engines/scumm/insane/rebel1/render.cpp
@@ -490,6 +490,7 @@ void InsaneRebel1::procPreRendering(byte *renderBitmap) {
_hudRenderFlag = 0;
_gameOp0BPhysicsUpdatedThisFrame = false;
_turretFrameShipCenterValid = false;
+ _frameObjectHitRevealPending = false;
if (_interactiveVideoActive && _player) {
const bool usePerspectiveViewport =
@@ -582,14 +583,17 @@ void InsaneRebel1::procPostRendering(byte *renderBitmap, int32 codecparam, int32
const bool allowImplicitGameplayMode = _frameHasGameChunk && !haveFrameGameOpcodes;
const bool gameOp0BMode = hasFrameGameOpcode(0x0B) ||
(allowImplicitGameplayMode && _activeGameOpcode == 0x0B);
- const bool onFootMode = hasFrameGameOpcode(0x19) || hasFrameGameOpcode(0x1A) ||
- (allowImplicitGameplayMode &&
- (_activeGameOpcode == 0x19 || _activeGameOpcode == 0x1A));
+ const bool onFootSequenceMode = hasFrameGameOpcode(0x19) ||
+ (allowImplicitGameplayMode && _activeGameOpcode == 0x19);
+ const bool onFootAimMode = hasFrameGameOpcode(0x1A) ||
+ (allowImplicitGameplayMode && _activeGameOpcode == 0x1A);
+ const bool onFootMode = onFootSequenceMode || onFootAimMode;
const bool turretMode = hasFrameGameOpcode(0x08) || hasFrameGameOpcode(0x0A) ||
(allowImplicitGameplayMode && (_activeGameOpcode == 0x08 || _activeGameOpcode == 0x0A));
const bool flightMode = hasFrameGameOpcode(0x07) || hasFrameGameOpcode(0x09) ||
(allowImplicitGameplayMode &&
(_activeGameOpcode == 0x07 || _activeGameOpcode == 0x09));
+ bool shotOverlayHandled = false;
if (gameOp0BMode) {
// GAME 0x0B scrolling cockpit/surface handler â FUN_1CDA7.
if (!_gameOp0BPhysicsUpdatedThisFrame) {
@@ -609,22 +613,59 @@ void InsaneRebel1::procPostRendering(byte *renderBitmap, int32 codecparam, int32
}
} else if (onFootMode) {
// On-foot handler â opcodes 0x19/0x1A (Level 9 Stormtroopers)
- updateOnFootPhysics();
-
- if (_health < 0 && _deathTimer < 2) {
+ if (_currentLevel == 8 && onFootAimMode && !onFootSequenceMode && _killCount > 0) {
_fireCooldown = _playerFired ? 1 : 0;
_vm->_smushVideoShouldFinish = true;
return;
}
- // Draw character sprite at on-foot position (DrawFobjGlyph with flag 0x80)
- if (_shipBank.numSprites > 0 && _shipDirIndex >= 0 &&
+ if (onFootSequenceMode)
+ initOnFootSequence();
+
+ // Original LVL9 frames dispatch GAME 0x1A before 0x19. That means the
+ // shot overlay uses the current crosshair/pose and is then covered by the
+ // character DrawFobjGlyph from 0x19; the 0x19 pose update is for the next
+ // frame. Keep that ordering explicit here.
+ if (onFootAimMode) {
+ shotOverlayHandled = true;
+ if (_health >= 0) {
+ // HandleGameOp5A_ObjectOrSceneTrigger can snap g_shipPos to the
+ // target center before the shot overlay runs. The ScummVM player
+ // defers GAME 0x1A until after FOBJ dispatch, so preserve that
+ // snap instead of immediately replacing it with raw input again.
+ const bool preserveTargetSnap = (_targetProximity == 2 && _tuning.snap > 0);
+ const int16 snappedTargetX = _shipPosX;
+ const int16 snappedTargetY = _shipPosY;
+
+ updateOnFootAimVariant();
+ if (preserveTargetSnap) {
+ _shipPosX = snappedTargetX;
+ _shipPosY = snappedTargetY;
+ }
+ renderShotOverlayPipeline(renderBitmap, pitch, width, height, false);
+ }
+ }
+
+ // Draw character sprite at on-foot position (DrawFobjGlyph with flag 0x80).
+ // GAME 0x1A-only selector clips reuse the targeting handler but do not
+ // draw the walking character.
+ if (onFootSequenceMode && _shipBank.numSprites > 0 && _shipDirIndex >= 0 &&
_shipDirIndex < _shipBank.numSprites) {
const RA1Sprite &spr = _shipBank.sprites[_shipDirIndex];
int drawX = _onFootCharX + spr.xoffs;
int drawY = _onFootCharY + spr.yoffs;
renderSprite(renderBitmap, pitch, width, height, drawX, drawY, spr);
}
+
+ if (onFootSequenceMode)
+ updateOnFootSequence();
+ finishOnFootFrame();
+
+ if (_health < 0 && _deathTimer < 2) {
+ _fireCooldown = _playerFired ? 1 : 0;
+ _vm->_smushVideoShouldFinish = true;
+ return;
+ }
} else {
// Dispatch movement path by GAME handler family:
// 0x08/0x0A -> FUN_1E6A7/FUN_1D79C (turret/cockpit)
@@ -665,11 +706,12 @@ void InsaneRebel1::procPostRendering(byte *renderBitmap, int32 codecparam, int32
// in handlers 0x09/0x0A/0x0B/0x1A. LVL1 stage-2 works because the stream emits
// both 0x0A and 0x08 in the same frame, not because 0x08 owns the overlay path.
const bool hasTargetingPipeline =
- hasFrameGameOpcode(0x09) || hasFrameGameOpcode(0x0A) ||
- hasFrameGameOpcode(0x0B) || hasFrameGameOpcode(0x1A) ||
- (allowImplicitGameplayMode &&
+ !shotOverlayHandled &&
+ (hasFrameGameOpcode(0x09) || hasFrameGameOpcode(0x0A) ||
+ hasFrameGameOpcode(0x0B) || hasFrameGameOpcode(0x1A) ||
+ (allowImplicitGameplayMode &&
(_activeGameOpcode == 0x09 || _activeGameOpcode == 0x0A ||
- _activeGameOpcode == 0x0B || _activeGameOpcode == 0x1A));
+ _activeGameOpcode == 0x0B || _activeGameOpcode == 0x1A)));
if (hasTargetingPipeline) {
const bool flightVariantTargetingMode =
hasFrameGameOpcode(0x09) ||
@@ -677,17 +719,7 @@ void InsaneRebel1::procPostRendering(byte *renderBitmap, int32 codecparam, int32
const bool turretTargetingMode =
hasFrameGameOpcode(0x0A) ||
(allowImplicitGameplayMode && _activeGameOpcode == 0x0A);
- renderTargetBoxes(renderBitmap, pitch, width, height);
- processShot();
- renderLaserShots(renderBitmap, pitch, width, height);
- // Timer decrement AFTER rendering (original decrements inside the render loop).
- // This ensures timer==5 first frame is rendered with gun barrel offset and lerp=1.
- for (int i = 0; i < kMaxShotSlots; i++) {
- if (_shotSlots[i].timer > 0)
- _shotSlots[i].timer--;
- }
- renderGostSlots(renderBitmap, pitch, width, height);
- renderTargeting(renderBitmap, pitch, width, height);
+ renderShotOverlayPipeline(renderBitmap, pitch, width, height, true);
// FUN_1D79C (GAME 0x0A) owns the cursor center in turret/combat mode.
// The preceding overlay/shot pass uses the previous frame's cursor; the
@@ -705,7 +737,7 @@ void InsaneRebel1::procPostRendering(byte *renderBitmap, int32 codecparam, int32
} else if (flightVariantTargetingMode) {
updateFlightVariantCursor();
}
- } else {
+ } else if (!shotOverlayHandled) {
// Keep lock/target accumulators quiescent when current handler doesn't
// execute FUN_1C940/FUN_1CCA0/FUN_1C9CD/FUN_1CB22.
_targetProximity = 0;
@@ -739,6 +771,30 @@ void InsaneRebel1::procPostRendering(byte *renderBitmap, int32 codecparam, int32
_fireCooldown = _playerFired ? 1 : 0;
}
+// ScummVM helper that groups the common shot/lock overlay calls used by the
+// original GAME handlers. GAME 0x1A uses the same pipeline without the target
+// box draw; 0x09/0x0A/0x0B include DrawTargetIndicators first.
+void InsaneRebel1::renderShotOverlayPipeline(byte *dst, int pitch, int width, int height,
+ bool drawTargetBoxes) {
+ if (drawTargetBoxes) {
+ renderTargetBoxes(dst, pitch, width, height);
+ } else {
+ _prevTargetCount = _targetCount;
+ _targetCount = 0;
+ }
+
+ processShot();
+ renderLaserShots(dst, pitch, width, height);
+ // Timer decrement AFTER rendering (original decrements inside the render loop).
+ // This ensures timer==5 first frame is rendered with gun barrel offset and lerp=1.
+ for (int i = 0; i < kMaxShotSlots; i++) {
+ if (_shotSlots[i].timer > 0)
+ _shotSlots[i].timer--;
+ }
+ renderGostSlots(dst, pitch, width, height);
+ renderTargeting(dst, pitch, width, height);
+}
+
// renderTargetBoxes â FUN_1C940 (0x1C940). Per-target green box overlays.
// Original gates on g_hudDisableFlags (0x75FE) bit 1: skip when set (Hard difficulty).
void InsaneRebel1::renderTargetBoxes(byte *dst, int pitch, int width, int height) {
@@ -882,35 +938,25 @@ void InsaneRebel1::renderGostSlots(byte *dst, int pitch, int width, int height)
if ((_gameplayFlags75fe & 0x10) != 0)
return;
- if (_bangBank.numSprites <= 0) {
- // Warn if there are active GOST slots but no bang sprites to render
- for (int i = 0; i < kMaxGostSlots; i++) {
- if (_gostSlots[i].targetId != 0 && _gostSlots[i].frame < 10) {
- debug(1, "RA1 renderGostSlots: bangBank empty but gost slot %d active (target=%d frame=%d)",
- i, _gostSlots[i].targetId, _gostSlots[i].frame);
- _gostSlots[i].targetId = 0; // Clear stale slot
- }
- }
- return;
- }
-
const int overlayX = ra1OverlayViewOffsetX(this);
const bool projectGostMarkers = (getEffectiveGameOpcode() == 0x0B);
for (int i = 0; i < kMaxGostSlots; i++) {
if (_gostSlots[i].targetId != 0 && _gostSlots[i].frame < 10) {
- int sprIdx = _gostSlots[i].frame;
- if (sprIdx >= _bangBank.numSprites)
- sprIdx = _bangBank.numSprites - 1;
-
- const RA1Sprite &spr = _bangBank.sprites[sprIdx];
int16 centerX = _gostSlots[i].posX;
int16 centerY = _gostSlots[i].posY;
if (projectGostMarkers)
centerX = (int16)(centerX - _perspectiveX);
- int drawX = overlayX + centerX - spr.width / 2;
- int drawY = centerY - spr.height / 2;
- renderSprite(dst, pitch, width, height, drawX, drawY, spr);
+ if (_bangBank.numSprites > 0) {
+ int sprIdx = _gostSlots[i].frame;
+ if (sprIdx >= _bangBank.numSprites)
+ sprIdx = _bangBank.numSprites - 1;
+
+ const RA1Sprite &spr = _bangBank.sprites[sprIdx];
+ int drawX = overlayX + centerX - spr.width / 2;
+ int drawY = centerY - spr.height / 2;
+ renderSprite(dst, pitch, width, height, drawX, drawY, spr);
+ }
// Per-kill score popup glyph â RenderGostOverlaySlots (0x1CA35)
// Suppressed when DAT_75FF bit 3 is set.
@@ -920,8 +966,6 @@ void InsaneRebel1::renderGostSlots(byte *dst, int pitch, int width, int height)
}
_gostSlots[i].frame++;
- if (_gostSlots[i].frame >= 10)
- _gostSlots[i].targetId = 0; // Animation complete
}
}
}
@@ -1027,14 +1071,15 @@ void InsaneRebel1::renderLaserShots(byte *dst, int pitch, int width, int height)
// DAT_2413: on-foot lerp table (timer 5 = 1, not 0 like flight mode).
const int kOnFootShotLerp[6] = { 0, 8, 7, 6, 4, 1 };
// DAT_240e: gun barrel X offset indexed by shipDirIndex (for timer==5 first frame).
+ // Indices 11..19 are the active firing poses from L9PILOT.NUT.
const int16 kOnFootGunBarrelX[20] = {
- 4, 0, 8, 8, 7, 6, 4, 1, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, -56, -47, -23, -13, 0, 13, 30, 54, 59
};
// DAT_2420: gun barrel Y offset indexed by shipDirIndex (for timer==5 first frame).
const int16 kOnFootGunBarrelY[20] = {
- 0, 0, -56, -47, -23, -13, 0, 13, 30, 54,
- 59, -3, -19, -24, -30, -28, -30, -29, -20, -5
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, -3, -19, -24, -30, -28, -30, -29, -20, -5
};
const int spritesPerSet = 5;
const int overlayX = ra1OverlayViewOffsetX(this);
diff --git a/engines/scumm/insane/rebel1/runlevels.cpp b/engines/scumm/insane/rebel1/runlevels.cpp
index 29bec475b90..ba0978cad89 100644
--- a/engines/scumm/insane/rebel1/runlevels.cpp
+++ b/engines/scumm/insane/rebel1/runlevels.cpp
@@ -1125,6 +1125,30 @@ bool InsaneRebel1::runLevel9() {
const int randPath1 = _vm->_rnd.getRandomNumber(1);
const int randPath2 = _vm->_rnd.getRandomNumber(1);
const int randPath3 = _vm->_rnd.getRandomNumber(1);
+ auto playLevel9PathSelector = [&](const char *filename) {
+ while (!_vm->shouldQuit()) {
+ // DOS zeros g_shipOffsetX around the 0x1A-only selector clips.
+ // Keep the selector cursor centered instead of inheriting the last
+ // walking offset from the previous stormtrooper segment.
+ _onFootCharX = 0;
+ _onFootCharY = 0;
+ _shipPosX = kRA1CenterX;
+ _shipPosY = kRA1CenterY;
+ _posAccumX = 0;
+ _posAccumY = 0;
+ _killCount = 0;
+ _lastHitTarget = 0;
+
+ playInteractiveVideo(filename);
+ if (_vm->shouldQuit() || _health < 0)
+ return -1;
+ if (_killCount > 0)
+ return (_shipPosX < kRA1CenterX) ? 0 : 1;
+
+ debug(1, "RA1 L9 selector '%s' ended without target hit; replaying", filename);
+ }
+ return -1;
+ };
_currentLevel = 8;
loadLevelSprites(9);
@@ -1190,17 +1214,16 @@ bool InsaneRebel1::runLevel9() {
if (_vm->shouldQuit())
return false;
- _shipPosX = kRA1CenterX;
- _posAccumX = 0;
loadTuningForLevel(0x0C);
- playInteractiveVideo("LVL9/L9PLAY2.ANM");
+ const int side1 = playLevel9PathSelector("LVL9/L9PLAY2.ANM");
if (_vm->shouldQuit())
return false;
if (_health < 0)
break;
+ if (side1 < 0)
+ return false;
_gameplayFlags75fe |= 4;
- const int side1 = (_shipPosX < kRA1CenterX) ? 0 : 1;
playCinematic(side1 == 0 ? "LVL9/L9PLAY2A.ANM" : "LVL9/L9PLAY2B.ANM");
if (_vm->shouldQuit())
return false;
@@ -1235,17 +1258,16 @@ bool InsaneRebel1::runLevel9() {
if (_vm->shouldQuit())
return false;
- _shipPosX = kRA1CenterX;
- _posAccumX = 0;
loadTuningForLevel(0x0C);
- playInteractiveVideo("LVL9/L9PLAY4.ANM");
+ const int side2 = playLevel9PathSelector("LVL9/L9PLAY4.ANM");
if (_vm->shouldQuit())
return false;
if (_health < 0)
break;
+ if (side2 < 0)
+ return false;
_gameplayFlags75fe |= 4;
- const int side2 = (_shipPosX < kRA1CenterX) ? 0 : 1;
playCinematic(side2 == 0 ? "LVL9/L9PLAY4A.ANM" : "LVL9/L9PLAY4B.ANM");
if (_vm->shouldQuit())
return false;
@@ -1266,17 +1288,16 @@ bool InsaneRebel1::runLevel9() {
if (_vm->shouldQuit())
return false;
- _shipPosX = kRA1CenterX;
- _posAccumX = 0;
loadTuningForLevel(0x0C);
- playInteractiveVideo("LVL9/L9PLAY6.ANM");
+ const int side3 = playLevel9PathSelector("LVL9/L9PLAY6.ANM");
if (_vm->shouldQuit())
return false;
if (_health < 0)
break;
+ if (side3 < 0)
+ return false;
_gameplayFlags75fe |= 4;
- const int side3 = (_shipPosX < kRA1CenterX) ? 0 : 1;
if (side3 == randPath3) {
playCinematic("LVL9/L9CUT6A.ANM");
if (_vm->shouldQuit())
Commit: 8565fdbda2b4c89a1ca897a1f8292aa131f6ac16
https://github.com/scummvm/scummvm/commit/8565fdbda2b4c89a1ca897a1f8292aa131f6ac16
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:10:10+02:00
Commit Message:
SCUMM: RA1: Seed random path selection deterministically
Changed paths:
engines/scumm/insane/rebel1/runlevels.cpp
diff --git a/engines/scumm/insane/rebel1/runlevels.cpp b/engines/scumm/insane/rebel1/runlevels.cpp
index ba0978cad89..2c20befca89 100644
--- a/engines/scumm/insane/rebel1/runlevels.cpp
+++ b/engines/scumm/insane/rebel1/runlevels.cpp
@@ -1122,9 +1122,18 @@ bool InsaneRebel1::runLevel8() {
bool InsaneRebel1::runLevel9() {
debug(1, "InsaneRebel1: Running level 9");
- const int randPath1 = _vm->_rnd.getRandomNumber(1);
- const int randPath2 = _vm->_rnd.getRandomNumber(1);
- const int randPath3 = _vm->_rnd.getRandomNumber(1);
+ // DOS RunLevel9Flow calls RandScaleByte(2) three times before the intro.
+ // That helper advances a byte seed with seed = seed * 9 + 0x35 and returns
+ // (2 * seed) >> 8. Do not use ScummVM's session RNG here: it can turn the
+ // original right-side route into the capture/restart branch.
+ uint8 originalRouteSeed = 0;
+ auto getOriginalRouteBit = [&originalRouteSeed]() {
+ originalRouteSeed = (uint8)(originalRouteSeed * 9 + 0x35);
+ return (2 * originalRouteSeed) >> 8;
+ };
+ const int randPath1 = getOriginalRouteBit();
+ const int randPath2 = getOriginalRouteBit();
+ const int randPath3 = getOriginalRouteBit();
auto playLevel9PathSelector = [&](const char *filename) {
while (!_vm->shouldQuit()) {
// DOS zeros g_shipOffsetX around the 0x1A-only selector clips.
Commit: aa24c708399928fc64219b1a32b58cb47c6a09c6
https://github.com/scummvm/scummvm/commit/aa24c708399928fc64219b1a32b58cb47c6a09c6
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:10:10+02:00
Commit Message:
SCUMM: RA1: Allow navigation in level 7
Changed paths:
engines/scumm/insane/rebel1/iact.cpp
engines/scumm/insane/rebel1/rebel.cpp
engines/scumm/insane/rebel1/rebel.h
engines/scumm/insane/rebel1/render.cpp
engines/scumm/insane/rebel1/runlevels.cpp
engines/scumm/smush/rebel/smush_player_ra1.cpp
diff --git a/engines/scumm/insane/rebel1/iact.cpp b/engines/scumm/insane/rebel1/iact.cpp
index facc373470e..9df570a6031 100644
--- a/engines/scumm/insane/rebel1/iact.cpp
+++ b/engines/scumm/insane/rebel1/iact.cpp
@@ -621,21 +621,28 @@ void InsaneRebel1::resetProjectionTable() {
rebuildProjectionTable(0, 1);
}
-void InsaneRebel1::checkDynamicLevelBranch() {
+void InsaneRebel1::checkDynamicLevelBranch(int32 curFrame) {
if (!_interactiveVideoActive || _levelRouteIndex < 0)
return;
if ((_currentLevel == 6 || _currentLevel == 7) && _pendingRouteIndex >= 0) {
- const uint32 routeFrame = (_currentLevel == 7) ? (uint32)_gameCounter : _frameCounter;
+ const uint32 routeFrame = (_currentLevel == 6 && curFrame >= 0) ?
+ (uint32)curFrame : (uint32)_gameCounter;
if (!_vm->_smushVideoShouldFinish &&
_pendingRouteCutoverFrame >= 0 &&
routeFrame >= (uint32)_pendingRouteCutoverFrame) {
- if (_player)
+ // Level 7 route switches open the destination ANM as a fresh file.
+ // Keep Rebel runtime state, but do not carry SMUSH decoder state
+ // from the previous route into the new file.
+ if (_player && _currentLevel != 6)
_player->setPreserveGameVideoStateOnRelease(true);
_vm->_smushVideoShouldFinish = true;
- debug(1, "RA1 L%d cutover: route=%d -> %d at frame=%u (resumeTimelineFrame=%d)",
+ const int32 resumeFrame = (_currentLevel == 6 && _pendingRouteStartFrame < 0) ?
+ 0 : _pendingRouteStartFrame;
+ debug(1, "RA1 L%d cutover: route=%d -> %d at %s=%u (resumeFrame=%d)",
_currentLevel + 1, _levelRouteIndex, _pendingRouteIndex,
- (unsigned)routeFrame, (int)_pendingRouteStartFrame);
+ _currentLevel == 6 ? "localFrame" : "frame",
+ (unsigned)routeFrame, (int)resumeFrame);
}
return;
}
@@ -644,23 +651,48 @@ void InsaneRebel1::checkDynamicLevelBranch() {
return;
if (_currentLevel == 6) {
+ // RunLevel7Flow compares the branch table against g_frameCounter. The
+ // playback callback writes that value from the ANM-local frame index,
+ // not from the decoded GAME counter embedded in these non-linear files.
+ if (curFrame < 0)
+ return;
+ const uint32 routeFrame = (uint32)curFrame;
+ // GAME 0x09 publishes its branch-tested position in g_shipPosX.
+ // ScummVM keeps the drawn ship center and the 0x09 aim cursor split,
+ // so compare the effective gameplay cursor here.
+ const int16 branchX = getGameplayCursorX();
const int route = CLIP<int>(_levelRouteIndex, 0, 5);
for (int nextRoute = 1; nextRoute < 6; ++nextRoute) {
const int triggerFrame = kLevel7BranchFrames[route][nextRoute];
- if (triggerFrame <= 0 || nextRoute == route || _frameCounter != (uint32)(triggerFrame - 1))
+ if (triggerFrame <= 0)
+ continue;
+
+ const uint32 decisionFrame = (uint32)(triggerFrame - 1);
+ if (routeFrame + 0x1E == decisionFrame) {
+ _level7WarningFrames = 0x1E;
+ _level7WarningThreshold = kLevel7BranchThreshold[nextRoute];
+ }
+
+ if (routeFrame != decisionFrame)
continue;
const bool takeBranch = (kLevel7BranchDir[nextRoute] > 0)
- ? (_shipPosX > kLevel7BranchThreshold[nextRoute])
- : (_shipPosX < kLevel7BranchThreshold[nextRoute]);
- if (!takeBranch)
+ ? (branchX > kLevel7BranchThreshold[nextRoute])
+ : (branchX < kLevel7BranchThreshold[nextRoute]);
+ if (!takeBranch) {
+ if (routeFrame == decisionFrame)
+ debug(1, "RA1 L7 branch miss: route=%d candidate=%d localFrame=%u gameFrame=%d shipX=%d dir=%d threshold=%d",
+ route, nextRoute, (unsigned)routeFrame, (int)_gameCounter, branchX,
+ kLevel7BranchDir[nextRoute], kLevel7BranchThreshold[nextRoute]);
continue;
+ }
_pendingRouteIndex = nextRoute;
- _pendingRouteCutoverFrame = (int32)_frameCounter + 7;
- _pendingRouteStartFrame = _pendingRouteCutoverFrame;
- debug(1, "RA1 L7 branch: route=%d -> %d at frame=%u shipX=%d resumeTimelineFrame=%d cutoverFrame=%d",
- route, nextRoute, (unsigned)_frameCounter, _shipPosX,
+ _pendingRouteCutoverFrame = (int32)routeFrame + 7;
+ _pendingRouteStartFrame = (int32)routeFrame;
+ _level7WarningFrames = 0;
+ debug(1, "RA1 L7 branch: route=%d -> %d at localFrame=%u gameFrame=%d decisionFrame=%u shipX=%d resumeSourceFrame=%d cutoverFrame=%d",
+ route, nextRoute, (unsigned)routeFrame, (int)_gameCounter, (unsigned)decisionFrame, branchX,
(int)_pendingRouteStartFrame, (int)_pendingRouteCutoverFrame);
return;
}
@@ -1188,7 +1220,8 @@ void InsaneRebel1::updateShipPhysics() {
_pathBranchEnabled = false;
}
- checkDynamicLevelBranch();
+ if (_currentLevel != 6)
+ checkDynamicLevelBranch();
debug(7, "RA1 ship: pos=(%d,%d) roll=%d lift=%d accX=%d accY=%d dir=%d health=%d corridor=[%d,%d]-[%d,%d]",
_shipPosX, _shipPosY, _rollAccum, _liftSmooth,
@@ -1235,11 +1268,10 @@ void InsaneRebel1::updateTurretShipDirection(int16 offsetY) {
}
void InsaneRebel1::getCollisionShipCenter(int16 &x, int16 &y) const {
- // Original 0x0D/0x0E collision compares projected script zones against the
- // drawn ship center (base center + g_shipOffset). This port draws into a
- // 384x242 source buffer and later crops by the camera offset, so convert that
- // source-buffer anchor to the visible screen-space point used by the projected
- // zones.
+ // Original 0x0D/0x0E collision compares script zones transformed by
+ // FUN_2248C against the source-buffer ship center (base center +
+ // g_shipOffset). Keep this in the same 384x242 space; the final viewport
+ // crop is only a presentation step.
//
// In Level 1 part 2, HandleGameOp0A_TurretVariant reuses _shipPos for the
// targeting cursor, so collision must read the movement accumulator instead.
@@ -1250,11 +1282,6 @@ void InsaneRebel1::getCollisionShipCenter(int16 &x, int16 &y) const {
x = _shipPosX;
y = _shipPosY;
}
-
- if (_interactiveVideoActive) {
- x = (int16)(x - _perspectiveX);
- y = (int16)(y - _perspectiveY);
- }
}
// updateTurretPhysics â FUN_1E6A7 (0x1E6A7), opcode 0x08 path.
@@ -2083,7 +2110,10 @@ void InsaneRebel1::handleGameChunk(int32 subSize, Common::SeekableReadStream &b)
int16 centerX = corridorLeft + corridorWidth / 2;
int16 centerY = corridorTop + corridorHeight / 2;
- projectGameplayPoint(centerX, centerY);
+ // DOS FUN_1C54D calls FUN_2248C here, which adds the current
+ // camera offset to the scripted rectangle center before testing it
+ // against the source-buffer ship center.
+ unprojectGameplayPoint(centerX, centerY);
_corridorLeftX = centerX - corridorWidth / 2;
_corridorTopY = centerY - corridorHeight / 2;
@@ -2101,7 +2131,7 @@ void InsaneRebel1::handleGameChunk(int32 subSize, Common::SeekableReadStream &b)
const byte oldDirectionalFlags = _damageFlags & 0x0F;
if (_health >= 0) {
if (collisionShipX < _corridorLeftX) {
- _posAccumX = (int32)(_corridorLeftX + _perspectiveX - kRA1CenterX) * 0x100;
+ _posAccumX = (int32)(_corridorLeftX - kRA1CenterX) * 0x100;
if (!suppressDirectionalDamage) {
if (_rollAccum < 0x100)
_rollAccum = 0x100;
@@ -2109,7 +2139,7 @@ void InsaneRebel1::handleGameChunk(int32 subSize, Common::SeekableReadStream &b)
}
}
if (collisionShipX > _corridorRightX) {
- _posAccumX = (int32)(_corridorRightX + _perspectiveX - kRA1CenterX) * 0x100;
+ _posAccumX = (int32)(_corridorRightX - kRA1CenterX) * 0x100;
if (!suppressDirectionalDamage) {
if (_rollAccum > -0x100)
_rollAccum = -0x100;
@@ -2117,12 +2147,12 @@ void InsaneRebel1::handleGameChunk(int32 subSize, Common::SeekableReadStream &b)
}
}
if (collisionShipY < _corridorTopY) {
- _posAccumY = (int32)(_corridorTopY + _perspectiveY - kRA1CenterY) * 0x100 + 0x100;
+ _posAccumY = (int32)(_corridorTopY - kRA1CenterY) * 0x100 + 0x100;
if (!suppressDirectionalDamage)
_damageFlags |= 0x01;
}
if (collisionShipY > _corridorBottomY) {
- _posAccumY = (int32)(_corridorBottomY + _perspectiveY - kRA1CenterY) * 0x100 - 0x100;
+ _posAccumY = (int32)(_corridorBottomY - kRA1CenterY) * 0x100 - 0x100;
if (!suppressDirectionalDamage)
_damageFlags |= 0x08;
}
@@ -2154,7 +2184,8 @@ void InsaneRebel1::handleGameChunk(int32 subSize, Common::SeekableReadStream &b)
int16 centerX = zoneLeft + zoneWidth / 2;
int16 centerY = zoneTop + zoneHeight / 2;
- projectGameplayPoint(centerX, centerY);
+ // Same transform as opcode 0x0D/FUN_1C54D.
+ unprojectGameplayPoint(centerX, centerY);
zoneLeft = centerX - zoneWidth / 2;
zoneTop = centerY - zoneHeight / 2;
diff --git a/engines/scumm/insane/rebel1/rebel.cpp b/engines/scumm/insane/rebel1/rebel.cpp
index b90af71f368..f3d856b65d8 100644
--- a/engines/scumm/insane/rebel1/rebel.cpp
+++ b/engines/scumm/insane/rebel1/rebel.cpp
@@ -312,6 +312,8 @@ InsaneRebel1::InsaneRebel1(ScummEngine_v7 *scumm) : Insane(), _vm(scumm) {
_pendingRouteIndex = -1;
_pendingRouteStartFrame = 0;
_pendingRouteCutoverFrame = -1;
+ _level7WarningFrames = 0;
+ _level7WarningThreshold = 0;
_levelGameplayPhase = 0;
_level14Play2BSplicePending = false;
_level14Play2BSpliced = false;
diff --git a/engines/scumm/insane/rebel1/rebel.h b/engines/scumm/insane/rebel1/rebel.h
index 83fbb415459..c7dc42932ae 100644
--- a/engines/scumm/insane/rebel1/rebel.h
+++ b/engines/scumm/insane/rebel1/rebel.h
@@ -162,7 +162,7 @@ private:
void preprocessMouseAxes(int16 &inputX, int16 &inputY, bool *usedJoystick = nullptr);
void rebuildProjectionTable(int16 curveStep, int16 curveExtent);
void resetProjectionTable();
- void checkDynamicLevelBranch();
+ void checkDynamicLevelBranch(int32 curFrame = -1);
void renderShip(byte *dst, int pitch, int width, int height);
void renderHUD(byte *dst, int pitch, int width, int height);
void renderMainMenuOverlay(byte *dst, int pitch, int width, int height);
@@ -177,6 +177,7 @@ private:
void renderShotOverlayPipeline(byte *dst, int pitch, int width, int height,
bool drawTargetBoxes);
void handleLevel14Play2BSplice(int32 curFrame, int32 maxFrame);
+ void renderLevel7RouteOverlays(byte *dst, int pitch, int width, int height);
void renderLevel11HitsOverlay(byte *dst, int pitch, int width, int height);
void resetEnemyShotSlots();
void renderLevel13EnemyShots(byte *dst, int pitch, int width, int height);
@@ -408,7 +409,7 @@ private:
int16 _damageCooldown; // 0x74D8: invulnerability timer (10 frames after hit)
int16 _deathTimer; // 0x756A: death animation countdown (30 on death)
int16 _screenFlash; // 0x7736: screen flash timer on hit
- uint32 _frameCounter; // 0x7740: global frame counter
+ uint32 _frameCounter; // Gameplay handler frame accumulator
bool _screenShakeEnabled; // 0x41AC: when true, SetCameraOffset adds ±2 random jitter
byte _screenFlashBasePalette[0x300];
bool _screenFlashBasePaletteValid;
@@ -449,13 +450,15 @@ private:
// Original sets nextSceneA/nextSceneB when GAME 0x07 counter == 394 (0x18A).
// We check ship position at that counter value to decide left vs right path.
static const int32 kPathBranchCounter = 394; // GAME 0x07 field1 value
- int32 _gameCounter; // GAME 0x07 field1 â the original's _DAT_7740
+ int32 _gameCounter; // GAME chunk field1/logical counter
bool _pathBranchEnabled; // True when branching is active for this video
bool _rightPathSelected; // True if player branched into L1PLAY1R
int _levelRouteIndex; // Current mid-level route/segment for branching levels
int _pendingRouteIndex; // Next route requested by original frame-branch logic
- int32 _pendingRouteStartFrame; // Resume frame for branch-driven route switches
+ int32 _pendingRouteStartFrame; // Resume/frame-gate target for branch-driven route switches
int32 _pendingRouteCutoverFrame; // Delayed inline route splice frame (branchFrame + 7)
+ int16 _level7WarningFrames; // RunLevel7Flow local_38: 30-frame branch warning countdown
+ int16 _level7WarningThreshold; // Collapses RunLevel7Flow local_40 into its DAT_2361 threshold
int _levelGameplayPhase; // Level-local interactive phase (e.g. LVL4 PLAY1 vs PLAY2)
// RunLevel14Flow queues L14PLY2B at L14PLAY2 maxFrame-0x0F.
bool _level14Play2BSplicePending;
diff --git a/engines/scumm/insane/rebel1/render.cpp b/engines/scumm/insane/rebel1/render.cpp
index 3702b7029b3..a390fc6006f 100644
--- a/engines/scumm/insane/rebel1/render.cpp
+++ b/engines/scumm/insane/rebel1/render.cpp
@@ -581,6 +581,7 @@ void InsaneRebel1::procPostRendering(byte *renderBitmap, int32 codecparam, int32
const bool haveFrameGameOpcodes = (_frameGameOpcodeMask != 0);
const bool allowImplicitGameplayMode = _frameHasGameChunk && !haveFrameGameOpcodes;
+
const bool gameOp0BMode = hasFrameGameOpcode(0x0B) ||
(allowImplicitGameplayMode && _activeGameOpcode == 0x0B);
const bool onFootSequenceMode = hasFrameGameOpcode(0x19) ||
@@ -747,9 +748,17 @@ void InsaneRebel1::procPostRendering(byte *renderBitmap, int32 codecparam, int32
_lastHitTarget = 0;
}
+ // RunLevel7Flow tests route branches after PumpFrontendFrame, using the
+ // decoded logical route timeline and updated GAME 0x09 cursor state.
+ if (_currentLevel == 6)
+ checkDynamicLevelBranch(curFrame);
+
if (_currentLevel == 12)
renderLevel13EnemyShots(renderBitmap, pitch, width, height);
+ if (_currentLevel == 6)
+ renderLevel7RouteOverlays(renderBitmap, pitch, width, height);
+
renderExplosions(renderBitmap, pitch, width, height);
handleLevel14Play2BSplice(curFrame, maxFrame);
@@ -986,6 +995,70 @@ void InsaneRebel1::resetEnemyShotSlots() {
memset(_enemyShotSlots, 0, sizeof(_enemyShotSlots));
}
+// Port helper for Level 7 RunLevel7Flow. The original keeps this warning and
+// single incoming projectile slot inline in the L7PLAY frontend loop.
+void InsaneRebel1::renderLevel7RouteOverlays(byte *dst, int pitch, int width, int height) {
+ if (_currentLevel != 6 || !_interactiveVideoActive || _health < 0)
+ return;
+
+ if (_level7WarningFrames != 0) {
+ const int16 oldWarningFrames = _level7WarningFrames;
+ _level7WarningFrames--;
+
+ if ((oldWarningFrames & 7) == 0)
+ playSfx(kSfxLockOn, 127, 0);
+
+ const char *warningText = nullptr;
+ const int16 warningX = getGameplayCursorX();
+ if (_level7WarningFrames < 0x0B) {
+ warningText = (_level7WarningThreshold < warningX) ? "<<_" : "<<`";
+ } else if ((_level7WarningFrames & 4) != 0) {
+ warningText = "<<`_";
+ }
+
+ if (warningText != nullptr)
+ drawFontBankString(dst, pitch, width, height, 0x11C, 0x16, warningText);
+ }
+
+ const int targetCount = CLIP<int>(_prevTargetCount, 0, kMaxTargetBoxes);
+ EnemyShotSlot &slot = _enemyShotSlots[0];
+
+ int16 playerX = (int16)(_shipPosX - kRA1CenterX + _perspectiveX);
+ int16 playerY = (int16)(_shipPosY - kRA1CenterY + _perspectiveY);
+ unprojectGameplayPoint(playerX, playerY);
+
+ if (slot.timer == 0 && targetCount > 0 && _vm->_rnd.getRandomNumber(14) == 0) {
+ const int targetIdx = _vm->_rnd.getRandomNumber(targetCount - 1);
+ slot.startX = _targetBoxX[targetIdx];
+ slot.startY = _targetBoxY[targetIdx];
+ slot.targetX = playerX;
+ slot.targetY = playerY;
+ slot.timer = 7;
+ slot.direction = ra1ProjectileDirectionWithFlags(slot.startX, slot.startY,
+ slot.targetX, slot.targetY, slot.flags);
+ playSfx(kSfxBlast, 127, 0);
+ }
+
+ if (slot.timer == 0)
+ return;
+
+ const int progress = 9 - slot.timer;
+ const int drawX = slot.startX + (((slot.targetX - slot.startX) * progress) >> 2);
+ const int drawY = slot.startY + (((slot.targetY - slot.startY) * progress) >> 2);
+ const RA1SpriteBank &laserBank = (_enemyLaserBank.numSprites > 0) ? _enemyLaserBank : _laserBank;
+ if (slot.direction >= 0 && slot.direction < laserBank.numSprites) {
+ renderSpriteWithFlags(dst, pitch, width, height, drawX, drawY,
+ laserBank.sprites[slot.direction], slot.flags | 0x3);
+ }
+
+ slot.timer--;
+ if (drawX - 0x14 < playerX && playerX < drawX + 0x14 &&
+ drawY - 0x1E < playerY && playerY < drawY + 0x1E) {
+ _damageFlags |= 0x80;
+ slot.timer = 0;
+ }
+}
+
// Port helper for Level 13 RunLevel13Flow. The original stores this state in
// five local stack arrays around the L13PLAY frontend loop, not in a named function.
void InsaneRebel1::renderLevel13EnemyShots(byte *dst, int pitch, int width, int height) {
diff --git a/engines/scumm/insane/rebel1/runlevels.cpp b/engines/scumm/insane/rebel1/runlevels.cpp
index 2c20befca89..d85c5a75ea2 100644
--- a/engines/scumm/insane/rebel1/runlevels.cpp
+++ b/engines/scumm/insane/rebel1/runlevels.cpp
@@ -31,77 +31,6 @@
namespace Scumm {
-struct RA1Level7ResumeSegment {
- int16 timelineStart;
- int16 timelineEnd;
- int16 localStart;
-};
-
-const RA1Level7ResumeSegment kLevel7ResumeSegments[6][4] = {
- {
- { 0, 638, 0 },
- { 1416, 1468, 639 },
- { -1, -1, -1 },
- { -1, -1, -1 }
- },
- {
- { 80, 639, 189 },
- { 691, 879, 0 },
- { 1416, 1468, 749 },
- { -1, -1, -1 }
- },
- {
- { 80, 639, 189 },
- { 777, 879, 86 },
- { 880, 965, 0 },
- { 1416, 1468, 749 }
- },
- {
- { 398, 639, 284 },
- { 1132, 1415, 0 },
- { 1416, 1468, 526 },
- { -1, -1, -1 }
- },
- {
- { 398, 639, 143 },
- { 966, 1076, 0 },
- { 1384, 1415, 111 },
- { 1416, 1468, 385 }
- },
- {
- { 80, 639, 114 },
- { 821, 879, 55 },
- { 1077, 1131, 0 },
- { 1416, 1468, 674 }
- }
-};
-
-int32 mapLevel7TimelineFrameToLocal(int route, int32 timelineFrame) {
- if (timelineFrame <= 0)
- return 0;
-
- const int clampedRoute = CLIP<int>(route, 0, ARRAYSIZE(kLevel7ResumeSegments) - 1);
- const RA1Level7ResumeSegment *segments = kLevel7ResumeSegments[clampedRoute];
-
- for (int i = 0; i < ARRAYSIZE(kLevel7ResumeSegments[0]); ++i) {
- const RA1Level7ResumeSegment &segment = segments[i];
- if (segment.timelineStart < 0)
- break;
- if (timelineFrame < segment.timelineStart)
- return segment.localStart;
- if (timelineFrame <= segment.timelineEnd)
- return segment.localStart + (timelineFrame - segment.timelineStart);
- }
-
- for (int i = ARRAYSIZE(kLevel7ResumeSegments[0]) - 1; i >= 0; --i) {
- const RA1Level7ResumeSegment &segment = segments[i];
- if (segment.timelineStart >= 0)
- return segment.localStart;
- }
-
- return 0;
-}
-
int32 findAnimFrameChunkOffset(ScummEngine_v7 *vm, const char *filename, int32 targetFrame) {
if (targetFrame <= 0)
return 0;
@@ -905,6 +834,7 @@ bool InsaneRebel1::runLevel7() {
_currentLevel = 6;
loadLevelSprites(7);
+ loadRA1Nut("LVL7/L7LASER2.NUT", _enemyLaserBank);
// DOS RunLevel7Flow starts L7PLAY1.ANM with initLevelFlag=9.
loadTuningForLevel(9);
@@ -929,6 +859,7 @@ bool InsaneRebel1::runLevel7() {
_gameLatch5D = 0;
_gameLatch5F = 0;
resetGameplayFlagsFromTuning();
+ _driftParam = 0x19;
_killCount = 0;
_targetCount = 0;
_prevTargetCount = 0;
@@ -949,13 +880,17 @@ bool InsaneRebel1::runLevel7() {
memset(_viewHistoryY, 0, sizeof(_viewHistoryY));
_avgInputX = 0;
_avgInputY = 0;
+ resetEnemyShotSlots();
+ _level7WarningFrames = 0;
+ _level7WarningThreshold = 0;
int route = 0;
int32 routeStartFrame = 0;
+ int32 routeSourceFrame = 0;
while (!_vm->shouldQuit()) {
_levelRouteIndex = route;
_pendingRouteIndex = -1;
- _pendingRouteStartFrame = routeStartFrame;
+ _pendingRouteStartFrame = routeSourceFrame;
_pendingRouteCutoverFrame = -1;
playInteractiveVideo(kLevel7Segments[route], routeStartFrame);
if (_vm->shouldQuit())
@@ -964,20 +899,23 @@ bool InsaneRebel1::runLevel7() {
if (_health < 0)
break;
- if (_pendingRouteIndex < 0 || _pendingRouteIndex == route)
+ if (_pendingRouteIndex < 0)
break;
- // RunLevel7Flow arms the next route inline, lets the current route run for
- // seven more gameplay frames, then opens the destination ANM while keeping
- // the existing video state alive.
- routeStartFrame = _pendingRouteStartFrame;
route = _pendingRouteIndex;
+ routeSourceFrame = _pendingRouteStartFrame;
+ // DOS does not seek the destination route ANM here. The ANM-local
+ // decision frame is used by the playback gate/cutoff, while
+ // L7PLAY2..6 open from their first frame.
+ routeStartFrame = 0;
}
_levelRouteIndex = -1;
_pendingRouteIndex = -1;
_pendingRouteStartFrame = 0;
_pendingRouteCutoverFrame = -1;
+ _level7WarningFrames = 0;
+ _level7WarningThreshold = 0;
if (_health >= 0) {
char accuracyText[80];
@@ -2053,7 +1991,10 @@ void InsaneRebel1::runGame() {
// Play interactive gameplay video (with ship physics + HUD).
void InsaneRebel1::playInteractiveVideo(const char *filename, int32 startFrame) {
debug(1, "InsaneRebel1::playInteractiveVideo('%s', startFrame=%d)", filename, startFrame);
- const bool resumingRoute = (startFrame > 0);
+ const bool level7RouteSplice = (_currentLevel == 6 && _levelRouteIndex > 0);
+ const bool resumingRoute = startFrame > 0;
+ const bool preserveRuntimeState = resumingRoute || level7RouteSplice;
+ const bool preserveVideoState = resumingRoute && !level7RouteSplice;
int32 videoStartFrame = 0;
int32 videoOffset = 0;
@@ -2064,10 +2005,10 @@ void InsaneRebel1::playInteractiveVideo(const char *filename, int32 startFrame)
SmushPlayer *splayer = _vm->_splayer;
_player = splayer;
restoreScreenFlashPalette();
- if (!resumingRoute)
+ if (!preserveRuntimeState)
clearBit(0);
_interactiveVideoActive = true;
- if (!resumingRoute) {
+ if (!preserveRuntimeState) {
_onFootInitialized = false; // Reset so each segment triggers counter==0 init
resetFrameObjectState();
}
@@ -2075,21 +2016,24 @@ void InsaneRebel1::playInteractiveVideo(const char *filename, int32 startFrame)
// Route resumes stay in the same gameplay flow in the original executable.
// Preserve the previous video/runtime state, but keep the destination clip
// fully interactive from its first visible frame.
- splayer->setPreserveVideoStateOnNextPlay(resumingRoute);
+ splayer->setPreserveVideoStateOnNextPlay(preserveVideoState);
splayer->setCurVideoFlags(0x28);
splayer->setFastForwardFromFrame(0);
splayer->setFastForwardToFrame(0);
- if (_currentLevel == 6 && resumingRoute) {
- videoStartFrame = mapLevel7TimelineFrameToLocal(_levelRouteIndex, startFrame);
+ if (_currentLevel == 6 && level7RouteSplice) {
+ // DOS opens the route ANM from the beginning, then the armed frame gate
+ // suppresses destination local frame 0 because the L7 arm-frame table is 1.
+ videoStartFrame = 1;
videoOffset = findAnimFrameChunkOffset(_vm, filename, videoStartFrame);
if (videoOffset < 0) {
- debug(1, "RA1 L7 resume: route=%d timelineFrame=%d localFrame=%d offset lookup failed",
- _levelRouteIndex, (int)startFrame, (int)videoStartFrame);
+ debug(1, "RA1 L7 route switch: route=%d gateFrame=%d offset lookup failed",
+ _levelRouteIndex, (int)videoStartFrame);
videoStartFrame = 0;
videoOffset = 0;
} else {
- debug(1, "RA1 L7 resume: route=%d timelineFrame=%d -> localFrame=%d offset=0x%x",
- _levelRouteIndex, (int)startFrame, (int)videoStartFrame, (unsigned)videoOffset);
+ debug(1, "RA1 L7 route switch: route=%d decisionLocalFrame=%d opens destination at gateFrame=%d offset=0x%x",
+ _levelRouteIndex, (int)_pendingRouteStartFrame,
+ (int)videoStartFrame, (unsigned)videoOffset);
}
} else if (_currentLevel == 7 && resumingRoute) {
videoOffset = findAnimFrameChunkOffsetByGameCounter(_vm, filename, startFrame, videoStartFrame);
@@ -2117,13 +2061,17 @@ void InsaneRebel1::playInteractiveVideo(const char *filename, int32 startFrame)
(int)startFrame);
}
- // Center mouse, hide system cursor (we draw our own), lock mouse to window
- smush_warpMouse(160, 100, -1);
- _mouseVirtualRawX = 0x140;
- _mouseVirtualRawY = 100;
- _mouseVirtualPrevLogicalX = kRA1CenterX;
- _mouseVirtualPrevLogicalY = kRA1CenterY;
- _mouseVirtualValid = false;
+ // Center mouse, hide system cursor (we draw our own), lock mouse to window.
+ // Level 7 route splices happen inside one original gameplay loop, so keep
+ // the current input state instead of recentering between route clips.
+ if (!level7RouteSplice) {
+ smush_warpMouse(160, 100, -1);
+ _mouseVirtualRawX = 0x140;
+ _mouseVirtualRawY = 100;
+ _mouseVirtualPrevLogicalX = kRA1CenterX;
+ _mouseVirtualPrevLogicalY = kRA1CenterY;
+ _mouseVirtualValid = false;
+ }
CursorMan.showMouse(false);
g_system->lockMouse(true);
diff --git a/engines/scumm/smush/rebel/smush_player_ra1.cpp b/engines/scumm/smush/rebel/smush_player_ra1.cpp
index ef27996d58f..3c9c4c71e96 100644
--- a/engines/scumm/smush/rebel/smush_player_ra1.cpp
+++ b/engines/scumm/smush/rebel/smush_player_ra1.cpp
@@ -972,6 +972,7 @@ void SmushPlayerRebel1::handleFrame(int32 frameSize, Common::SeekableReadStream
uint8 *audioChunk = nullptr;
_skipNext = false;
handleGameFrameStart();
+ const bool fastForwarding = isFastForwardingCurrentFrame();
bool preserveFrameHistory = false;
if (_insane) {
@@ -1083,8 +1084,10 @@ void SmushPlayerRebel1::handleFrame(int32 frameSize, Common::SeekableReadStream
break;
case MKTAG('G','A','M','E'):
case MKTAG('G','A','M','2'): {
- InsaneRebel1 *rebel1 = (InsaneRebel1 *)_insane;
- rebel1->handleGameChunk(subSize, b);
+ if (!fastForwarding) {
+ InsaneRebel1 *rebel1 = (InsaneRebel1 *)_insane;
+ rebel1->handleGameChunk(subSize, b);
+ }
break;
}
case MKTAG('O','B','J','\0'): {
@@ -1127,9 +1130,11 @@ void SmushPlayerRebel1::handleFrame(int32 frameSize, Common::SeekableReadStream
_ra1ObjOverlayHeight = READ_LE_UINT16(objBuf + objPos + 16);
}
} else if (embTag == MKTAG('G','A','M','E') || embTag == MKTAG('G','A','M','2')) {
- Common::MemoryReadStream embStream(objBuf + objPos + 8, embSize);
- InsaneRebel1 *rebel1 = (InsaneRebel1 *)_insane;
- rebel1->handleGameChunk(embSize, embStream);
+ if (!fastForwarding) {
+ Common::MemoryReadStream embStream(objBuf + objPos + 8, embSize);
+ InsaneRebel1 *rebel1 = (InsaneRebel1 *)_insane;
+ rebel1->handleGameChunk(embSize, embStream);
+ }
} else if (embTag == MKTAG('P','S','A','D')) {
if (!_compressedFileMode && !isFastForwardingCurrentFrame()) {
uint8 *audioBuf = (uint8 *)malloc(embSize + 8);
@@ -1200,10 +1205,10 @@ void SmushPlayerRebel1::handleFrame(int32 frameSize, Common::SeekableReadStream
_ra1HasCleanFrame = false;
}
- if (_insanity)
+ if (_insanity && !fastForwarding)
_insane->procPostRendering(_dst, 0, 0, 0, _frame, _nbframes-1);
- if (_width != 0 && _height != 0)
+ if (_width != 0 && _height != 0 && !fastForwarding)
updateScreen();
_frame++;
Commit: 10feabc9dab1f70402e9b6a9d2a8e4240e47f7ca
https://github.com/scummvm/scummvm/commit/10feabc9dab1f70402e9b6a9d2a8e4240e47f7ca
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:10:11+02:00
Commit Message:
SCUMM: RA1: Refine navigation in level 7
Changed paths:
engines/scumm/insane/rebel1/iact.cpp
engines/scumm/insane/rebel1/rebel.cpp
engines/scumm/insane/rebel1/rebel.h
engines/scumm/insane/rebel1/runlevels.cpp
diff --git a/engines/scumm/insane/rebel1/iact.cpp b/engines/scumm/insane/rebel1/iact.cpp
index 9df570a6031..98ed443d600 100644
--- a/engines/scumm/insane/rebel1/iact.cpp
+++ b/engines/scumm/insane/rebel1/iact.cpp
@@ -690,10 +690,12 @@ void InsaneRebel1::checkDynamicLevelBranch(int32 curFrame) {
_pendingRouteIndex = nextRoute;
_pendingRouteCutoverFrame = (int32)routeFrame + 7;
_pendingRouteStartFrame = (int32)routeFrame;
+ _pendingRouteVideoStartFrame = 1 + (_pendingRouteCutoverFrame - _pendingRouteStartFrame);
_level7WarningFrames = 0;
- debug(1, "RA1 L7 branch: route=%d -> %d at localFrame=%u gameFrame=%d decisionFrame=%u shipX=%d resumeSourceFrame=%d cutoverFrame=%d",
+ debug(1, "RA1 L7 branch: route=%d -> %d at localFrame=%u gameFrame=%d decisionFrame=%u shipX=%d resumeSourceFrame=%d cutoverFrame=%d destFrame=%d",
route, nextRoute, (unsigned)routeFrame, (int)_gameCounter, (unsigned)decisionFrame, branchX,
- (int)_pendingRouteStartFrame, (int)_pendingRouteCutoverFrame);
+ (int)_pendingRouteStartFrame, (int)_pendingRouteCutoverFrame,
+ (int)_pendingRouteVideoStartFrame);
return;
}
}
diff --git a/engines/scumm/insane/rebel1/rebel.cpp b/engines/scumm/insane/rebel1/rebel.cpp
index f3d856b65d8..c6686439014 100644
--- a/engines/scumm/insane/rebel1/rebel.cpp
+++ b/engines/scumm/insane/rebel1/rebel.cpp
@@ -312,6 +312,7 @@ InsaneRebel1::InsaneRebel1(ScummEngine_v7 *scumm) : Insane(), _vm(scumm) {
_pendingRouteIndex = -1;
_pendingRouteStartFrame = 0;
_pendingRouteCutoverFrame = -1;
+ _pendingRouteVideoStartFrame = 0;
_level7WarningFrames = 0;
_level7WarningThreshold = 0;
_levelGameplayPhase = 0;
diff --git a/engines/scumm/insane/rebel1/rebel.h b/engines/scumm/insane/rebel1/rebel.h
index c7dc42932ae..d714c77588b 100644
--- a/engines/scumm/insane/rebel1/rebel.h
+++ b/engines/scumm/insane/rebel1/rebel.h
@@ -457,6 +457,7 @@ private:
int _pendingRouteIndex; // Next route requested by original frame-branch logic
int32 _pendingRouteStartFrame; // Resume/frame-gate target for branch-driven route switches
int32 _pendingRouteCutoverFrame; // Delayed inline route splice frame (branchFrame + 7)
+ int32 _pendingRouteVideoStartFrame; // L7 destination ANM-local frame after gate adjustment
int16 _level7WarningFrames; // RunLevel7Flow local_38: 30-frame branch warning countdown
int16 _level7WarningThreshold; // Collapses RunLevel7Flow local_40 into its DAT_2361 threshold
int _levelGameplayPhase; // Level-local interactive phase (e.g. LVL4 PLAY1 vs PLAY2)
diff --git a/engines/scumm/insane/rebel1/runlevels.cpp b/engines/scumm/insane/rebel1/runlevels.cpp
index d85c5a75ea2..b5a95396492 100644
--- a/engines/scumm/insane/rebel1/runlevels.cpp
+++ b/engines/scumm/insane/rebel1/runlevels.cpp
@@ -887,11 +887,13 @@ bool InsaneRebel1::runLevel7() {
int route = 0;
int32 routeStartFrame = 0;
int32 routeSourceFrame = 0;
+ int32 routeVideoStartFrame = 0;
while (!_vm->shouldQuit()) {
_levelRouteIndex = route;
_pendingRouteIndex = -1;
_pendingRouteStartFrame = routeSourceFrame;
_pendingRouteCutoverFrame = -1;
+ _pendingRouteVideoStartFrame = routeVideoStartFrame;
playInteractiveVideo(kLevel7Segments[route], routeStartFrame);
if (_vm->shouldQuit())
return false;
@@ -904,9 +906,10 @@ bool InsaneRebel1::runLevel7() {
route = _pendingRouteIndex;
routeSourceFrame = _pendingRouteStartFrame;
+ routeVideoStartFrame = _pendingRouteVideoStartFrame;
// DOS does not seek the destination route ANM here. The ANM-local
- // decision frame is used by the playback gate/cutoff, while
- // L7PLAY2..6 open from their first frame.
+ // decision frame is used by the playback gate/cutoff; ScummVM
+ // starts at the already-advanced gate target after the cutover.
routeStartFrame = 0;
}
@@ -914,6 +917,7 @@ bool InsaneRebel1::runLevel7() {
_pendingRouteIndex = -1;
_pendingRouteStartFrame = 0;
_pendingRouteCutoverFrame = -1;
+ _pendingRouteVideoStartFrame = 0;
_level7WarningFrames = 0;
_level7WarningThreshold = 0;
@@ -2022,16 +2026,18 @@ void InsaneRebel1::playInteractiveVideo(const char *filename, int32 startFrame)
splayer->setFastForwardToFrame(0);
if (_currentLevel == 6 && level7RouteSplice) {
// DOS opens the route ANM from the beginning, then the armed frame gate
- // suppresses destination local frame 0 because the L7 arm-frame table is 1.
- videoStartFrame = 1;
+ // suppresses until the adjusted target. With ScummVM's delayed cutover,
+ // the destination must advance by the source tail already displayed.
+ videoStartFrame = (_pendingRouteVideoStartFrame > 0) ?
+ _pendingRouteVideoStartFrame : 1;
videoOffset = findAnimFrameChunkOffset(_vm, filename, videoStartFrame);
if (videoOffset < 0) {
- debug(1, "RA1 L7 route switch: route=%d gateFrame=%d offset lookup failed",
+ debug(1, "RA1 L7 route switch: route=%d destinationFrame=%d offset lookup failed",
_levelRouteIndex, (int)videoStartFrame);
videoStartFrame = 0;
videoOffset = 0;
} else {
- debug(1, "RA1 L7 route switch: route=%d decisionLocalFrame=%d opens destination at gateFrame=%d offset=0x%x",
+ debug(1, "RA1 L7 route switch: route=%d decisionLocalFrame=%d opens destination at localFrame=%d offset=0x%x",
_levelRouteIndex, (int)_pendingRouteStartFrame,
(int)videoStartFrame, (unsigned)videoOffset);
}
Commit: aacaf1cdd711a45fdc7d21f4d3bd157ee25d28d6
https://github.com/scummvm/scummvm/commit/aacaf1cdd711a45fdc7d21f4d3bd157ee25d28d6
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:10:11+02:00
Commit Message:
SCUMM: RA1: Deduplicate runLevel handlers
Changed paths:
engines/scumm/insane/rebel1/rebel.h
engines/scumm/insane/rebel1/runlevels.cpp
diff --git a/engines/scumm/insane/rebel1/rebel.h b/engines/scumm/insane/rebel1/rebel.h
index d714c77588b..b8b1678ffc0 100644
--- a/engines/scumm/insane/rebel1/rebel.h
+++ b/engines/scumm/insane/rebel1/rebel.h
@@ -150,6 +150,14 @@ private:
const char *bonusLabel1 = nullptr, const char *detailText1 = nullptr, int bonusValue1 = 0,
const char *bonusLabel2 = nullptr, const char *detailText2 = nullptr, int bonusValue2 = 0,
int passwordIndex = 0);
+ bool playDeathOrRetry(const char *retryVideo, const char *gameOverVideo);
+ void resetLevelDamageState();
+ void resetLevelFrameState();
+ void resetLevelTargetingState(bool resetKillCount = true);
+ void resetLevelFlightState(int16 shipDirIndex = 17);
+ void resetLevelInputHistory(bool resetAxisDeltaX = false);
+ void resetLevelAttemptState(int16 flyControlMode, int16 gameplayPhase,
+ int16 shipDirIndex = 17, bool resetAxisDeltaX = false);
// Play interactive gameplay video (with ship physics + HUD)
void playInteractiveVideo(const char *filename, int32 startFrame = 0);
diff --git a/engines/scumm/insane/rebel1/runlevels.cpp b/engines/scumm/insane/rebel1/runlevels.cpp
index b5a95396492..80da8bfcbfa 100644
--- a/engines/scumm/insane/rebel1/runlevels.cpp
+++ b/engines/scumm/insane/rebel1/runlevels.cpp
@@ -196,6 +196,81 @@ void InsaneRebel1::playChapterCompleteCinematic(const char *filename, int16 unlo
_maxChapterUnlocked = MAX<int16>(_maxChapterUnlocked, passwordIndex);
}
+bool InsaneRebel1::playDeathOrRetry(const char *retryVideo, const char *gameOverVideo) {
+ if (_lives > 0) {
+ playCinematic(retryVideo);
+ if (_vm->shouldQuit())
+ return false;
+ _lives--;
+ return true;
+ }
+
+ playCinematic(gameOverVideo);
+ return false;
+}
+
+void InsaneRebel1::resetLevelDamageState() {
+ _health = kMaxHealth;
+ _damageFlags = 0;
+ _prevDamageFlags = 0;
+ _damageCooldown = 0;
+ _deathTimer = 0;
+ _screenFlash = 0;
+ _screenShakeEnabled = false;
+ _deathCauseIndicator = 0;
+}
+
+void InsaneRebel1::resetLevelFrameState() {
+ _frameCounter = 0;
+ _gameCounter = 0;
+ _activeGameOpcode = 0;
+ _gameLatch5D = 0;
+ _gameLatch5F = 0;
+}
+
+void InsaneRebel1::resetLevelTargetingState(bool resetKillCount) {
+ if (resetKillCount)
+ _killCount = 0;
+ _targetCount = 0;
+ _prevTargetCount = 0;
+ _lastHitTarget = 0;
+}
+
+void InsaneRebel1::resetLevelFlightState(int16 shipDirIndex) {
+ _shipPosX = kRA1CenterX;
+ _shipPosY = kRA1CenterY;
+ _shipDirIndex = shipDirIndex;
+ _rollAccum = 0;
+ _liftSmooth = 0;
+ _posAccumX = 0;
+ _posAccumY = 0;
+ _perspectiveX = 0;
+ _perspectiveY = 0;
+}
+
+void InsaneRebel1::resetLevelInputHistory(bool resetAxisDeltaX) {
+ memset(_inputHistoryX, 0, sizeof(_inputHistoryX));
+ memset(_inputHistoryY, 0, sizeof(_inputHistoryY));
+ memset(_viewHistoryX, 0, sizeof(_viewHistoryX));
+ memset(_viewHistoryY, 0, sizeof(_viewHistoryY));
+ if (resetAxisDeltaX)
+ _inputAxisDeltaX = 0;
+ _avgInputX = 0;
+ _avgInputY = 0;
+}
+
+void InsaneRebel1::resetLevelAttemptState(int16 flyControlMode, int16 gameplayPhase,
+ int16 shipDirIndex, bool resetAxisDeltaX) {
+ _flyControlMode = flyControlMode;
+ resetLevelDamageState();
+ resetLevelFrameState();
+ resetGameplayFlagsFromTuning();
+ resetLevelTargetingState();
+ resetLevelFlightState(shipDirIndex);
+ _levelGameplayPhase = gameplayPhase;
+ resetLevelInputHistory(resetAxisDeltaX);
+}
+
void InsaneRebel1::playLevelTransitionCutscene(int level) {
switch (level) {
case 4:
@@ -301,32 +376,10 @@ bool InsaneRebel1::runLevel1() {
while (!_vm->shouldQuit()) {
bool stage2Started = false;
- _health = kMaxHealth;
- _damageFlags = 0;
- _prevDamageFlags = 0;
- _damageCooldown = 0;
- _deathTimer = 0;
- _screenFlash = 0;
- _screenShakeEnabled = false;
- _deathCauseIndicator = 0;
- _frameCounter = 0;
- _gameCounter = 0;
- _activeGameOpcode = 0;
- _gameLatch5D = 0;
- _gameLatch5F = 0;
- _killCount = 0;
- _targetCount = 0;
- _prevTargetCount = 0;
- _lastHitTarget = 0;
- _shipPosX = kRA1CenterX;
- _shipPosY = kRA1CenterY;
- _shipDirIndex = 17;
- _rollAccum = 0;
- _liftSmooth = 0;
- _posAccumX = 0;
- _posAccumY = 0;
- _perspectiveX = 0;
- _perspectiveY = 0;
+ resetLevelDamageState();
+ resetLevelFrameState();
+ resetLevelTargetingState();
+ resetLevelFlightState();
_pathBranchEnabled = true;
_rightPathSelected = false;
_flyControlMode = 1;
@@ -398,15 +451,8 @@ bool InsaneRebel1::runLevel1() {
if (_vm->shouldQuit())
return false;
- if (_lives <= 0) {
- playCinematic("LVL1/L1DEATH.ANM");
+ if (!playDeathOrRetry("LVL1/L1NEW.ANM", "LVL1/L1DEATH.ANM"))
return false;
- }
-
- playCinematic("LVL1/L1NEW.ANM");
- if (_vm->shouldQuit())
- return false;
- _lives--;
}
return false;
@@ -427,26 +473,10 @@ bool InsaneRebel1::runLevel2() {
while (!_vm->shouldQuit()) {
_flyControlMode = 0;
- _health = kMaxHealth;
- _damageFlags = 0;
- _prevDamageFlags = 0;
- _damageCooldown = 0;
- _deathTimer = 0;
- _screenFlash = 0;
- _screenShakeEnabled = false;
- _deathCauseIndicator = 0;
- _frameCounter = 0;
- _gameCounter = 0;
- memset(_inputHistoryX, 0, sizeof(_inputHistoryX));
- memset(_inputHistoryY, 0, sizeof(_inputHistoryY));
- memset(_viewHistoryX, 0, sizeof(_viewHistoryX));
- memset(_viewHistoryY, 0, sizeof(_viewHistoryY));
- _avgInputX = 0;
- _avgInputY = 0;
+ resetLevelDamageState();
+ resetLevelFrameState();
+ resetLevelInputHistory();
_killCount = 0;
- _activeGameOpcode = 0;
- _gameLatch5D = 0;
- _gameLatch5F = 0;
playInteractiveVideo("LVL2/L2PLAY.ANM");
if (_vm->shouldQuit())
@@ -460,15 +490,9 @@ bool InsaneRebel1::runLevel2() {
return !_vm->shouldQuit();
}
- if (_lives > 0) {
- playCinematic("LVL2/L2NEW.ANM");
- if (_vm->shouldQuit())
- return false;
- _lives--;
+ if (playDeathOrRetry("LVL2/L2NEW.ANM", "LVL2/L2DEATH.ANM"))
continue;
- }
- playCinematic("LVL2/L2DEATH.ANM");
return false;
}
@@ -490,28 +514,12 @@ bool InsaneRebel1::runLevel3() {
while (!_vm->shouldQuit()) {
_flyControlMode = 1;
- _health = kMaxHealth;
- _damageFlags = 0;
- _prevDamageFlags = 0;
- _damageCooldown = 0;
- _deathTimer = 0;
- _screenFlash = 0;
- _screenShakeEnabled = false;
- _deathCauseIndicator = 0;
- _frameCounter = 0;
- _gameCounter = 0;
- _activeGameOpcode = 0;
- _gameLatch5D = 0;
- _gameLatch5F = 0;
+ resetLevelDamageState();
+ resetLevelFrameState();
resetGameplayFlagsFromTuning();
_killCount = 0;
_levelGameplayPhase = 0;
- memset(_inputHistoryX, 0, sizeof(_inputHistoryX));
- memset(_inputHistoryY, 0, sizeof(_inputHistoryY));
- memset(_viewHistoryX, 0, sizeof(_viewHistoryX));
- memset(_viewHistoryY, 0, sizeof(_viewHistoryY));
- _avgInputX = 0;
- _avgInputY = 0;
+ resetLevelInputHistory();
playInteractiveVideo("LVL3/L3PLAY.ANM");
if (_vm->shouldQuit())
@@ -523,15 +531,9 @@ bool InsaneRebel1::runLevel3() {
return !_vm->shouldQuit();
}
- if (_lives > 0) {
- playCinematic("LVL3/L3NEW.ANM");
- if (_vm->shouldQuit())
- return false;
- _lives--;
+ if (playDeathOrRetry("LVL3/L3NEW.ANM", "LVL3/L3DEATH.ANM"))
continue;
- }
- playCinematic("LVL3/L3DEATH.ANM");
return false;
}
@@ -554,19 +556,8 @@ bool InsaneRebel1::runLevel4() {
while (!_vm->shouldQuit()) {
loadTuningForLevel(4);
_flyControlMode = 1;
- _health = kMaxHealth;
- _damageFlags = 0;
- _prevDamageFlags = 0;
- _damageCooldown = 0;
- _deathTimer = 0;
- _screenFlash = 0;
- _screenShakeEnabled = false;
- _deathCauseIndicator = 0;
- _frameCounter = 0;
- _gameCounter = 0;
- _activeGameOpcode = 0;
- _gameLatch5D = 0;
- _gameLatch5F = 0;
+ resetLevelDamageState();
+ resetLevelFrameState();
_pendingRouteStartFrame = 0;
resetGameplayFlagsFromTuning();
_killCount = 0;
@@ -610,15 +601,9 @@ bool InsaneRebel1::runLevel4() {
return !_vm->shouldQuit();
}
- if (_lives > 0) {
- playCinematic("LVL4/L4NEW.ANM");
- if (_vm->shouldQuit())
- return false;
- _lives--;
+ if (playDeathOrRetry("LVL4/L4NEW.ANM", "LVL4/L4DEATH.ANM"))
continue;
- }
- playCinematic("LVL4/L4DEATH.ANM");
return false;
}
@@ -641,57 +626,17 @@ bool InsaneRebel1::runLevel5() {
while (!_vm->shouldQuit()) {
loadRA1Nut("LVL5/L5LASER.NUT", _laserBank);
loadTuningForLevel(6);
- _flyControlMode = 1;
- _health = kMaxHealth;
- _damageFlags = 0;
- _prevDamageFlags = 0;
- _damageCooldown = 0;
- _deathTimer = 0;
- _screenFlash = 0;
- _screenShakeEnabled = false;
- _deathCauseIndicator = 0;
- _frameCounter = 0;
- _gameCounter = 0;
- _activeGameOpcode = 0;
- _gameLatch5D = 0;
- _gameLatch5F = 0;
- resetGameplayFlagsFromTuning();
- _killCount = 0;
- _targetCount = 0;
- _prevTargetCount = 0;
- _lastHitTarget = 0;
- _shipPosX = kRA1CenterX;
- _shipPosY = kRA1CenterY;
- _shipDirIndex = 17;
- _rollAccum = 0;
- _liftSmooth = 0;
- _posAccumX = 0;
- _posAccumY = 0;
- _perspectiveX = 0;
- _perspectiveY = 0;
- _levelGameplayPhase = 1;
+ resetLevelAttemptState(1, 1);
_level5SuccessFramesRemaining = 0x14;
- memset(_inputHistoryX, 0, sizeof(_inputHistoryX));
- memset(_inputHistoryY, 0, sizeof(_inputHistoryY));
- memset(_viewHistoryX, 0, sizeof(_viewHistoryX));
- memset(_viewHistoryY, 0, sizeof(_viewHistoryY));
- _avgInputX = 0;
- _avgInputY = 0;
playInteractiveVideo("LVL5/L5PLAY.ANM");
if (_vm->shouldQuit())
return false;
if (_health < 0) {
- if (_lives > 0) {
- playCinematic("LVL5/L5NEW.ANM");
- if (_vm->shouldQuit())
- return false;
- _lives--;
+ if (playDeathOrRetry("LVL5/L5NEW.ANM", "LVL5/L5DEATH.ANM"))
continue;
- }
- playCinematic("LVL5/L5DEATH.ANM");
return false;
}
@@ -738,15 +683,9 @@ bool InsaneRebel1::runLevel5() {
return !_vm->shouldQuit();
}
- if (_lives > 0) {
- playCinematic("LVL5/L5NEW.ANM");
- if (_vm->shouldQuit())
- return false;
- _lives--;
+ if (playDeathOrRetry("LVL5/L5NEW.ANM", "LVL5/L5DEATH.ANM"))
continue;
- }
- playCinematic("LVL5/L5DEATH.ANM");
return false;
}
@@ -769,28 +708,12 @@ bool InsaneRebel1::runLevel6() {
while (!_vm->shouldQuit()) {
_flyControlMode = 1;
- _health = kMaxHealth;
- _damageFlags = 0;
- _prevDamageFlags = 0;
- _damageCooldown = 0;
- _deathTimer = 0;
- _screenFlash = 0;
- _screenShakeEnabled = false;
- _deathCauseIndicator = 0;
- _frameCounter = 0;
- _gameCounter = 0;
- _activeGameOpcode = 0;
- _gameLatch5D = 0;
- _gameLatch5F = 0;
+ resetLevelDamageState();
+ resetLevelFrameState();
resetGameplayFlagsFromTuning();
_killCount = 0;
_levelGameplayPhase = 0;
- memset(_inputHistoryX, 0, sizeof(_inputHistoryX));
- memset(_inputHistoryY, 0, sizeof(_inputHistoryY));
- memset(_viewHistoryX, 0, sizeof(_viewHistoryX));
- memset(_viewHistoryY, 0, sizeof(_viewHistoryY));
- _avgInputX = 0;
- _avgInputY = 0;
+ resetLevelInputHistory();
playInteractiveVideo("LVL6/L6PLAY.ANM");
if (_vm->shouldQuit())
@@ -805,15 +728,9 @@ bool InsaneRebel1::runLevel6() {
return !_vm->shouldQuit();
}
- if (_lives > 0) {
- playCinematic("LVL6/L6NEW.ANM");
- if (_vm->shouldQuit())
- return false;
- _lives--;
+ if (playDeathOrRetry("LVL6/L6NEW.ANM", "LVL6/L6DEATH.ANM"))
continue;
- }
- playCinematic("LVL6/L6DEATH.ANM");
return false;
}
@@ -844,42 +761,8 @@ bool InsaneRebel1::runLevel7() {
return false;
while (!_vm->shouldQuit()) {
- _flyControlMode = 3;
- _health = kMaxHealth;
- _damageFlags = 0;
- _prevDamageFlags = 0;
- _damageCooldown = 0;
- _deathTimer = 0;
- _screenFlash = 0;
- _screenShakeEnabled = false;
- _deathCauseIndicator = 0;
- _frameCounter = 0;
- _gameCounter = 0;
- _activeGameOpcode = 0;
- _gameLatch5D = 0;
- _gameLatch5F = 0;
- resetGameplayFlagsFromTuning();
+ resetLevelAttemptState(3, 0);
_driftParam = 0x19;
- _killCount = 0;
- _targetCount = 0;
- _prevTargetCount = 0;
- _lastHitTarget = 0;
- _shipPosX = kRA1CenterX;
- _shipPosY = kRA1CenterY;
- _shipDirIndex = 17;
- _rollAccum = 0;
- _liftSmooth = 0;
- _posAccumX = 0;
- _posAccumY = 0;
- _perspectiveX = 0;
- _perspectiveY = 0;
- _levelGameplayPhase = 0;
- memset(_inputHistoryX, 0, sizeof(_inputHistoryX));
- memset(_inputHistoryY, 0, sizeof(_inputHistoryY));
- memset(_viewHistoryX, 0, sizeof(_viewHistoryX));
- memset(_viewHistoryY, 0, sizeof(_viewHistoryY));
- _avgInputX = 0;
- _avgInputY = 0;
resetEnemyShotSlots();
_level7WarningFrames = 0;
_level7WarningThreshold = 0;
@@ -929,15 +812,9 @@ bool InsaneRebel1::runLevel7() {
return !_vm->shouldQuit();
}
- if (_lives > 0) {
- playCinematic("LVL7/L7NEW.ANM");
- if (_vm->shouldQuit())
- return false;
- _lives--;
+ if (playDeathOrRetry("LVL7/L7NEW.ANM", "LVL7/L7DEATH.ANM"))
continue;
- }
- playCinematic("LVL7/L7DEATH.ANM");
return false;
}
@@ -966,42 +843,7 @@ bool InsaneRebel1::runLevel8() {
return false;
while (!_vm->shouldQuit()) {
- _flyControlMode = 3;
- _health = kMaxHealth;
- _damageFlags = 0;
- _prevDamageFlags = 0;
- _damageCooldown = 0;
- _deathTimer = 0;
- _screenFlash = 0;
- _screenShakeEnabled = false;
- _deathCauseIndicator = 0;
- _frameCounter = 0;
- _gameCounter = 0;
- _activeGameOpcode = 0;
- _gameLatch5D = 0;
- _gameLatch5F = 0;
- resetGameplayFlagsFromTuning();
- _killCount = 0;
- _targetCount = 0;
- _prevTargetCount = 0;
- _lastHitTarget = 0;
- _shipPosX = kRA1CenterX;
- _shipPosY = kRA1CenterY;
- _shipDirIndex = 17;
- _rollAccum = 0;
- _liftSmooth = 0;
- _posAccumX = 0;
- _posAccumY = 0;
- _perspectiveX = 0;
- _perspectiveY = 0;
- _levelGameplayPhase = 0;
- memset(_inputHistoryX, 0, sizeof(_inputHistoryX));
- memset(_inputHistoryY, 0, sizeof(_inputHistoryY));
- memset(_viewHistoryX, 0, sizeof(_viewHistoryX));
- memset(_viewHistoryY, 0, sizeof(_viewHistoryY));
- _inputAxisDeltaX = 0;
- _avgInputX = 0;
- _avgInputY = 0;
+ resetLevelAttemptState(3, 0, 17, true);
// Walker-specific state â RunLevel8Flow (0x18546)
_walkerHealth = 100;
@@ -1046,15 +888,9 @@ bool InsaneRebel1::runLevel8() {
return !_vm->shouldQuit();
}
- if (_lives > 0) {
- playCinematic("LVL8/L8NEW.ANM");
- if (_vm->shouldQuit())
- return false;
- _lives--;
+ if (playDeathOrRetry("LVL8/L8NEW.ANM", "LVL8/L8DEATH.ANM"))
continue;
- }
- playCinematic("LVL8/L8DEATH.ANM");
return false;
}
@@ -1113,45 +949,11 @@ bool InsaneRebel1::runLevel9() {
while (!_vm->shouldQuit()) {
loadTuningForLevel(0x0B);
- _flyControlMode = 0;
- _health = kMaxHealth;
- _damageFlags = 0;
- _prevDamageFlags = 0;
- _damageCooldown = 0;
- _deathTimer = 0;
- _screenFlash = 0;
- _screenShakeEnabled = false;
- _deathCauseIndicator = 0;
- _frameCounter = 0;
- _gameCounter = 0;
- _activeGameOpcode = 0;
- _gameLatch5D = 0;
- _gameLatch5F = 0;
- resetGameplayFlagsFromTuning();
- _killCount = 0;
- _targetCount = 0;
- _prevTargetCount = 0;
- _lastHitTarget = 0;
- _shipPosX = kRA1CenterX;
- _shipPosY = kRA1CenterY;
- _shipDirIndex = 15; // On-foot center direction
- _rollAccum = 0;
- _liftSmooth = 0;
- _posAccumX = 0;
- _posAccumY = 0;
- _perspectiveX = 0;
- _perspectiveY = 0;
- _levelGameplayPhase = 0;
+ resetLevelAttemptState(0, 0, 15); // On-foot center direction
_onFootCharX = 0;
_onFootCharY = 0;
_onFootAnimCounter = 0;
_onFootInitialized = false;
- memset(_inputHistoryX, 0, sizeof(_inputHistoryX));
- memset(_inputHistoryY, 0, sizeof(_inputHistoryY));
- memset(_viewHistoryX, 0, sizeof(_viewHistoryX));
- memset(_viewHistoryY, 0, sizeof(_viewHistoryY));
- _avgInputX = 0;
- _avgInputY = 0;
while (!_vm->shouldQuit()) {
loadTuningForLevel(0x0B);
@@ -1276,15 +1078,9 @@ bool InsaneRebel1::runLevel9() {
return !_vm->shouldQuit();
}
- if (_lives > 0) {
- playCinematic("LVL9/L9NEW.ANM");
- if (_vm->shouldQuit())
- return false;
- _lives--;
+ if (playDeathOrRetry("LVL9/L9NEW.ANM", "LVL9/L9DEATH.ANM"))
continue;
- }
- playCinematic("LVL9/L9DEATH.ANM");
return false;
}
@@ -1305,41 +1101,7 @@ bool InsaneRebel1::runLevel10() {
return false;
while (!_vm->shouldQuit()) {
- _flyControlMode = 1;
- _health = kMaxHealth;
- _damageFlags = 0;
- _prevDamageFlags = 0;
- _damageCooldown = 0;
- _deathTimer = 0;
- _screenFlash = 0;
- _screenShakeEnabled = false;
- _deathCauseIndicator = 0;
- _frameCounter = 0;
- _gameCounter = 0;
- _activeGameOpcode = 0;
- _gameLatch5D = 0;
- _gameLatch5F = 0;
- resetGameplayFlagsFromTuning();
- _killCount = 0;
- _targetCount = 0;
- _prevTargetCount = 0;
- _lastHitTarget = 0;
- _shipPosX = kRA1CenterX;
- _shipPosY = kRA1CenterY;
- _shipDirIndex = 17;
- _rollAccum = 0;
- _liftSmooth = 0;
- _posAccumX = 0;
- _posAccumY = 0;
- _perspectiveX = 0;
- _perspectiveY = 0;
- _levelGameplayPhase = 0;
- memset(_inputHistoryX, 0, sizeof(_inputHistoryX));
- memset(_inputHistoryY, 0, sizeof(_inputHistoryY));
- memset(_viewHistoryX, 0, sizeof(_viewHistoryX));
- memset(_viewHistoryY, 0, sizeof(_viewHistoryY));
- _avgInputX = 0;
- _avgInputY = 0;
+ resetLevelAttemptState(1, 0);
playInteractiveVideo("LVL10/L10PLAY.ANM");
if (_vm->shouldQuit())
@@ -1354,15 +1116,9 @@ bool InsaneRebel1::runLevel10() {
return !_vm->shouldQuit();
}
- if (_lives > 0) {
- playCinematic("LVL10/L10NEW.ANM");
- if (_vm->shouldQuit())
- return false;
- _lives--;
+ if (playDeathOrRetry("LVL10/L10NEW.ANM", "LVL10/L10DEATH.ANM"))
continue;
- }
- playCinematic("LVL10/L10DEATH.ANM");
return false;
}
@@ -1387,41 +1143,7 @@ bool InsaneRebel1::runLevel11() {
return false;
while (!_vm->shouldQuit()) {
- _flyControlMode = 1;
- _health = kMaxHealth;
- _damageFlags = 0;
- _prevDamageFlags = 0;
- _damageCooldown = 0;
- _deathTimer = 0;
- _screenFlash = 0;
- _screenShakeEnabled = false;
- _deathCauseIndicator = 0;
- _frameCounter = 0;
- _gameCounter = 0;
- _activeGameOpcode = 0;
- _gameLatch5D = 0;
- _gameLatch5F = 0;
- resetGameplayFlagsFromTuning();
- _killCount = 0;
- _targetCount = 0;
- _prevTargetCount = 0;
- _lastHitTarget = 0;
- _shipPosX = kRA1CenterX;
- _shipPosY = kRA1CenterY;
- _shipDirIndex = 17;
- _rollAccum = 0;
- _liftSmooth = 0;
- _posAccumX = 0;
- _posAccumY = 0;
- _perspectiveX = 0;
- _perspectiveY = 0;
- _levelGameplayPhase = 0;
- memset(_inputHistoryX, 0, sizeof(_inputHistoryX));
- memset(_inputHistoryY, 0, sizeof(_inputHistoryY));
- memset(_viewHistoryX, 0, sizeof(_viewHistoryX));
- memset(_viewHistoryY, 0, sizeof(_viewHistoryY));
- _avgInputX = 0;
- _avgInputY = 0;
+ resetLevelAttemptState(1, 0);
_turretEmitterLeftX = 25;
_turretEmitterLeftY = 15;
@@ -1451,15 +1173,9 @@ bool InsaneRebel1::runLevel11() {
return !_vm->shouldQuit();
}
- if (_lives > 0) {
- playCinematic("LVL11/L11NEW.ANM");
- if (_vm->shouldQuit())
- return false;
- _lives--;
+ if (playDeathOrRetry("LVL11/L11NEW.ANM", "LVL11/L11DEATH.ANM"))
continue;
- }
- playCinematic("LVL11/L11DEATH.ANM");
return false;
}
@@ -1484,44 +1200,18 @@ bool InsaneRebel1::runLevel12() {
while (!_vm->shouldQuit()) {
_flyControlMode = 1;
- _health = kMaxHealth;
- _damageFlags = 0;
- _prevDamageFlags = 0;
- _damageCooldown = 0;
- _deathTimer = 0;
- _screenFlash = 0;
- _screenShakeEnabled = false;
- _deathCauseIndicator = 0;
+ resetLevelDamageState();
_killCount = 0;
while (!_vm->shouldQuit()) {
loadTuningForLevel(0x0F);
- _frameCounter = 0;
- _gameCounter = 0;
- _activeGameOpcode = 0;
- _gameLatch5D = 0;
- _gameLatch5F = 0;
+ resetLevelFrameState();
_damageFlags = 0;
resetGameplayFlagsFromTuning();
- _targetCount = 0;
- _prevTargetCount = 0;
- _lastHitTarget = 0;
- _shipPosX = kRA1CenterX;
- _shipPosY = kRA1CenterY;
- _shipDirIndex = 17;
- _rollAccum = 0;
- _liftSmooth = 0;
- _posAccumX = 0;
- _posAccumY = 0;
- _perspectiveX = 0;
- _perspectiveY = 0;
+ resetLevelTargetingState(false);
+ resetLevelFlightState();
_levelGameplayPhase = 0;
- memset(_inputHistoryX, 0, sizeof(_inputHistoryX));
- memset(_inputHistoryY, 0, sizeof(_inputHistoryY));
- memset(_viewHistoryX, 0, sizeof(_viewHistoryX));
- memset(_viewHistoryY, 0, sizeof(_viewHistoryY));
- _avgInputX = 0;
- _avgInputY = 0;
+ resetLevelInputHistory();
playInteractiveVideo("LVL12/L12PLAY.ANM");
if (_vm->shouldQuit())
@@ -1546,15 +1236,9 @@ bool InsaneRebel1::runLevel12() {
return !_vm->shouldQuit();
}
- if (_lives > 0) {
- playCinematic("LVL12/L12NEW.ANM");
- if (_vm->shouldQuit())
- return false;
- _lives--;
+ if (playDeathOrRetry("LVL12/L12NEW.ANM", "LVL12/L12DEATH.ANM"))
continue;
- }
- playCinematic("LVL12/L12DEATH.ANM");
return false;
}
@@ -1579,41 +1263,7 @@ bool InsaneRebel1::runLevel13() {
return false;
while (!_vm->shouldQuit()) {
- _flyControlMode = 1;
- _health = kMaxHealth;
- _damageFlags = 0;
- _prevDamageFlags = 0;
- _damageCooldown = 0;
- _deathTimer = 0;
- _screenFlash = 0;
- _screenShakeEnabled = false;
- _deathCauseIndicator = 0;
- _frameCounter = 0;
- _gameCounter = 0;
- _activeGameOpcode = 0;
- _gameLatch5D = 0;
- _gameLatch5F = 0;
- resetGameplayFlagsFromTuning();
- _killCount = 0;
- _targetCount = 0;
- _prevTargetCount = 0;
- _lastHitTarget = 0;
- _shipPosX = kRA1CenterX;
- _shipPosY = kRA1CenterY;
- _shipDirIndex = 17;
- _rollAccum = 0;
- _liftSmooth = 0;
- _posAccumX = 0;
- _posAccumY = 0;
- _perspectiveX = 0;
- _perspectiveY = 0;
- _levelGameplayPhase = 0;
- memset(_inputHistoryX, 0, sizeof(_inputHistoryX));
- memset(_inputHistoryY, 0, sizeof(_inputHistoryY));
- memset(_viewHistoryX, 0, sizeof(_viewHistoryX));
- memset(_viewHistoryY, 0, sizeof(_viewHistoryY));
- _avgInputX = 0;
- _avgInputY = 0;
+ resetLevelAttemptState(1, 0);
resetEnemyShotSlots();
playInteractiveVideo("LVL13/L13PLAY.ANM");
@@ -1628,15 +1278,9 @@ bool InsaneRebel1::runLevel13() {
return !_vm->shouldQuit();
}
- if (_lives > 0) {
- playCinematic("LVL13/L13NEW.ANM");
- if (_vm->shouldQuit())
- return false;
- _lives--;
+ if (playDeathOrRetry("LVL13/L13NEW.ANM", "LVL13/L13DEATH.ANM"))
continue;
- }
- playCinematic("LVL13/L13DEATH.ANM");
return false;
}
@@ -1662,42 +1306,8 @@ bool InsaneRebel1::runLevel14() {
while (!_vm->shouldQuit()) {
loadTuningForLevel(0x11);
- _flyControlMode = 1;
- _health = kMaxHealth;
- _damageFlags = 0;
- _prevDamageFlags = 0;
- _damageCooldown = 0;
- _deathTimer = 0;
- _screenFlash = 0;
- _screenShakeEnabled = false;
- _deathCauseIndicator = 0;
- _frameCounter = 0;
- _gameCounter = 0;
- _activeGameOpcode = 0;
- _gameLatch5D = 0;
- _gameLatch5F = 0;
- resetGameplayFlagsFromTuning();
- _killCount = 0;
- _targetCount = 0;
- _prevTargetCount = 0;
- _lastHitTarget = 0;
- _shipPosX = kRA1CenterX;
- _shipPosY = kRA1CenterY;
- _shipDirIndex = 17;
- _rollAccum = 0;
- _liftSmooth = 0;
- _posAccumX = 0;
- _posAccumY = 0;
- _perspectiveX = 0;
- _perspectiveY = 0;
- _levelGameplayPhase = 1;
+ resetLevelAttemptState(1, 1);
_level14SuccessFrames = 0;
- memset(_inputHistoryX, 0, sizeof(_inputHistoryX));
- memset(_inputHistoryY, 0, sizeof(_inputHistoryY));
- memset(_viewHistoryX, 0, sizeof(_viewHistoryX));
- memset(_viewHistoryY, 0, sizeof(_viewHistoryY));
- _avgInputX = 0;
- _avgInputY = 0;
// Phase 1: targeting surface cannons
playInteractiveVideo("LVL14/L14PLAY.ANM");
@@ -1742,15 +1352,9 @@ bool InsaneRebel1::runLevel14() {
return !_vm->shouldQuit();
}
- if (_lives > 0) {
- playCinematic("LVL14/L14NEW.ANM");
- if (_vm->shouldQuit())
- return false;
- _lives--;
+ if (playDeathOrRetry("LVL14/L14NEW.ANM", "LVL14/L14DEATH.ANM"))
continue;
- }
- playCinematic("LVL14/L14DEATH.ANM");
return false;
}
@@ -1776,41 +1380,7 @@ bool InsaneRebel1::runLevel15() {
while (!_vm->shouldQuit()) {
loadTuningForLevel(0x13);
- _flyControlMode = 1;
- _health = kMaxHealth;
- _damageFlags = 0;
- _prevDamageFlags = 0;
- _damageCooldown = 0;
- _deathTimer = 0;
- _screenFlash = 0;
- _screenShakeEnabled = false;
- _deathCauseIndicator = 0;
- _frameCounter = 0;
- _gameCounter = 0;
- _activeGameOpcode = 0;
- _gameLatch5D = 0;
- _gameLatch5F = 0;
- resetGameplayFlagsFromTuning();
- _killCount = 0;
- _targetCount = 0;
- _prevTargetCount = 0;
- _lastHitTarget = 0;
- _shipPosX = kRA1CenterX;
- _shipPosY = kRA1CenterY;
- _shipDirIndex = 17;
- _rollAccum = 0;
- _liftSmooth = 0;
- _posAccumX = 0;
- _posAccumY = 0;
- _perspectiveX = 0;
- _perspectiveY = 0;
- _levelGameplayPhase = 0;
- memset(_inputHistoryX, 0, sizeof(_inputHistoryX));
- memset(_inputHistoryY, 0, sizeof(_inputHistoryY));
- memset(_viewHistoryX, 0, sizeof(_viewHistoryX));
- memset(_viewHistoryY, 0, sizeof(_viewHistoryY));
- _avgInputX = 0;
- _avgInputY = 0;
+ resetLevelAttemptState(1, 0);
// Phase 1: trench run
_levelGameplayPhase = 1;
@@ -1861,15 +1431,9 @@ bool InsaneRebel1::runLevel15() {
return !_vm->shouldQuit();
}
- if (_lives > 0) {
- playCinematic("LVL15/L15NEW.ANM");
- if (_vm->shouldQuit())
- return false;
- _lives--;
+ if (playDeathOrRetry("LVL15/L15NEW.ANM", "LVL15/L15DEATH.ANM"))
continue;
- }
- playCinematic("LVL15/L15DEATH.ANM");
return false;
}
Commit: 625f0e9324f088731fac3122d01cf0aedc2763e4
https://github.com/scummvm/scummvm/commit/625f0e9324f088731fac3122d01cf0aedc2763e4
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:10:11+02:00
Commit Message:
SCUMM: RA1: Extract codecs
Changed paths:
A engines/scumm/smush/rebel/codec_ra1.cpp
A engines/scumm/smush/rebel/codec_ra1.h
engines/scumm/insane/rebel1/levels.cpp
engines/scumm/module.mk
engines/scumm/smush/rebel/smush_player_ra1.cpp
diff --git a/engines/scumm/insane/rebel1/levels.cpp b/engines/scumm/insane/rebel1/levels.cpp
index cbe0b1afda2..705cf871353 100644
--- a/engines/scumm/insane/rebel1/levels.cpp
+++ b/engines/scumm/insane/rebel1/levels.cpp
@@ -25,38 +25,10 @@
#include "scumm/scumm_v7.h"
#include "scumm/file.h"
#include "scumm/insane/rebel1/rebel.h"
+#include "scumm/smush/rebel/codec_ra1.h"
namespace Scumm {
-// From smush/codec1.cpp
-void smushDecodeRA1Transparent(byte *dst, const byte *src, int left, int top, int width, int height, int pitch);
-
-void decodeBomp(byte *dst, const byte *src, int width, int height, int pitch) {
- while (height--) {
- byte *dstNext = dst + pitch;
- const byte *srcNext = src + 2 + READ_LE_UINT16(src);
- src += 2;
- int len = width;
- byte *d = dst;
- do {
- int offs = READ_LE_UINT16(src); src += 2;
- d += offs;
- len -= offs;
- if (len <= 0)
- break;
- int w = READ_LE_UINT16(src) + 1; src += 2;
- len -= w;
- if (len < 0)
- w += len;
- memcpy(d, src, w);
- src += w;
- d += w;
- } while (len > 0);
- dst = dstNext;
- src = srcNext;
- }
-}
-
void resetSpriteBank(RA1SpriteBank &bank) {
delete[] bank.sprites;
bank.sprites = nullptr;
@@ -175,17 +147,21 @@ bool InsaneRebel1::loadRA1Nut(const char *filename, RA1SpriteBank &bank) {
int pixelCount = bank.sprites[i].width * bank.sprites[i].height;
const byte *fobjData = data + fobjOffset + 22;
+ const uint32 fobjSize = READ_BE_UINT32(data + fobjOffset + 4);
+ const int fobjDataSize = (fobjSize >= 14) ? (int)(fobjSize - 14) : 0;
if (codec == 21) {
bank.sprites[i].data = decPtr;
- decodeBomp(decPtr, fobjData, bank.sprites[i].width,
- bank.sprites[i].height, bank.sprites[i].width);
+ smushDecodeRA1SkipCopy(decPtr, fobjData, 0, 0, bank.sprites[i].width,
+ bank.sprites[i].height, bank.sprites[i].width, bank.sprites[i].width,
+ bank.sprites[i].height, fobjDataSize);
} else if (codec == 1) {
// RA1 codec 1 in NUTs (e.g. LVL2/L2LASER.NUT): RLE where color 0 is transparent.
// Decode into a zero-cleared sprite buffer so skipped pixels stay transparent.
bank.sprites[i].data = decPtr;
smushDecodeRA1Transparent(decPtr, fobjData, 0, 0,
- bank.sprites[i].width, bank.sprites[i].height, bank.sprites[i].width);
+ bank.sprites[i].width, bank.sprites[i].height, bank.sprites[i].width,
+ fobjDataSize);
} else {
bank.sprites[i].width = 0;
bank.sprites[i].height = 0;
diff --git a/engines/scumm/module.mk b/engines/scumm/module.mk
index ea6cc308dc7..fedb366f729 100644
--- a/engines/scumm/module.mk
+++ b/engines/scumm/module.mk
@@ -152,6 +152,7 @@ MODULE_OBJS += \
smush/codec37.o \
smush/codec47.o \
smush/smush_player.o \
+ smush/rebel/codec_ra1.o \
smush/rebel/codec_ra2.o \
smush/rebel/smush_multi_font.o \
smush/rebel/smush_player_ra1.o \
diff --git a/engines/scumm/smush/rebel/codec_ra1.cpp b/engines/scumm/smush/rebel/codec_ra1.cpp
new file mode 100644
index 00000000000..e9c0a0ba9e9
--- /dev/null
+++ b/engines/scumm/smush/rebel/codec_ra1.cpp
@@ -0,0 +1,324 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+// Rebel Assault 1 SMUSH video codecs
+
+#include "scumm/smush/rebel/codec_ra1.h"
+
+#include "common/endian.h"
+#include "common/textconsole.h"
+#include "common/util.h"
+
+namespace Scumm {
+
+/**
+ * RA1 codec 1: RLE with transparency on pixel 0.
+ * Same BOMP encoding as smushDecodeRLE but pixel value 0 is not written,
+ * allowing the background (restored via FTCH) to show through.
+ */
+void smushDecodeRA1Transparent(byte *dst, const byte *src, int left, int top, int width, int height, int pitch, int dataSize) {
+ if (dst == nullptr || src == nullptr || width <= 0 || height <= 0 || pitch <= 0 || dataSize <= 0)
+ return;
+
+ const byte *srcEnd = src + dataSize;
+ dst += top * pitch;
+ while (height-- > 0 && srcEnd - src >= 2) {
+ const int lineSize = READ_LE_UINT16(src);
+ const byte *lineData = src + 2;
+ const byte *lineEnd = lineData + MIN<int>(lineSize, srcEnd - lineData);
+ byte *rowDst = dst + left;
+ int remaining = width;
+
+ while (remaining > 0 && lineData < lineEnd) {
+ byte code = *lineData++;
+ int num = (code >> 1) + 1;
+ if (num > remaining)
+ num = remaining;
+
+ if (code & 1) {
+ if (lineData >= lineEnd)
+ break;
+ byte color = *lineData++;
+ if (color != 0)
+ memset(rowDst, color, num);
+ } else {
+ const int readable = MIN<int>(num, lineEnd - lineData);
+ for (int j = 0; j < readable; j++) {
+ byte c = lineData[j];
+ if (c != 0)
+ rowDst[j] = c;
+ }
+ lineData += readable;
+ if (readable < num)
+ break;
+ }
+ rowDst += num;
+ remaining -= num;
+ }
+
+ const int rowSize = 2 + lineSize;
+ if (rowSize > srcEnd - src)
+ break;
+ src += rowSize;
+ dst += pitch;
+ }
+}
+
+/**
+ * RA1 codec 21: Skip/copy line codec (FUN_10D41). Clip copy runs without
+ * changing source X; stored cockpit patches can legitimately start offscreen.
+ */
+void smushDecodeRA1SkipCopy(byte *dst, const byte *src, int left, int top, int width, int height,
+ int pitch, int bufWidth, int bufHeight, int dataSize) {
+ if (dst == nullptr || src == nullptr || width <= 0 || height <= 0 || pitch <= 0 || dataSize <= 0)
+ return;
+
+ const byte *srcEnd = src + dataSize;
+ for (int row = 0; row < height && srcEnd - src >= 2; row++) {
+ const uint16 lineSize = READ_LE_UINT16(src);
+ const byte *lineData = src + 2;
+ const byte *lineEnd = lineData + MIN<int>(lineSize, srcEnd - lineData);
+ const int dstY = top + row;
+ int srcX = 0;
+
+ while (srcX < width && lineData < lineEnd) {
+ if (lineData + 2 > lineEnd)
+ break;
+ const uint16 skip = READ_LE_UINT16(lineData);
+ lineData += 2;
+ srcX += skip;
+ if (srcX >= width)
+ break;
+
+ if (lineData + 2 > lineEnd)
+ break;
+ const int copyLen = READ_LE_UINT16(lineData) + 1;
+ lineData += 2;
+
+ const int readableLen = MIN<int>(copyLen, (int)(lineEnd - lineData));
+ const int dstStartX = left + srcX;
+ const int dstEndX = dstStartX + readableLen;
+ if (readableLen > 0 && dstY >= 0 && dstY < bufHeight) {
+ const int clippedStartX = MAX(dstStartX, 0);
+ const int clippedEndX = MIN(dstEndX, bufWidth);
+ if (clippedStartX < clippedEndX) {
+ const int srcSkipX = clippedStartX - dstStartX;
+ memcpy(dst + dstY * pitch + clippedStartX,
+ lineData + srcSkipX, clippedEndX - clippedStartX);
+ }
+ }
+
+ lineData += readableLen;
+ srcX += copyLen;
+ if (readableLen < copyLen)
+ break;
+ }
+
+ const int rowSize = 2 + lineSize;
+ if (rowSize > srcEnd - src)
+ break;
+ src += rowSize;
+ }
+}
+
+/**
+ * RA1 codec 23: Additive line-update overlay (FUN_10B40).
+ */
+void smushDecodeRA1AdditiveLineUpdate(byte *dst, const byte *src, int left, int top, int width, int height,
+ int pitch, int bufWidth, int bufHeight, uint8 paletteBase, int dataSize) {
+ if (dst == nullptr || src == nullptr || width <= 0 || height <= 0 || pitch <= 0 || dataSize <= 0)
+ return;
+
+ const uint8 colorDelta = (uint8)(paletteBase - 0x30);
+ const byte *srcEnd = src + dataSize;
+ for (int row = 0; row < height && srcEnd - src >= 2; row++) {
+ const uint16 lineSize = READ_LE_UINT16(src);
+ const byte *lineData = src + 2;
+ const byte *lineEnd = lineData + MIN<int>(lineSize, srcEnd - lineData);
+ const int dstY = top + row;
+ int srcX = 0;
+ while (srcX < width && lineData < lineEnd) {
+ const int skip = *lineData++;
+ srcX += skip;
+ if (srcX >= width || lineData >= lineEnd)
+ break;
+ const int runLength = (int)(*lineData++);
+ const int dstStartX = left + srcX;
+ const int dstEndX = dstStartX + runLength;
+ if (dstY >= 0 && dstY < bufHeight) {
+ const int clippedStartX = MAX(dstStartX, 0);
+ const int clippedEndX = MIN(dstEndX, bufWidth);
+ if (clippedStartX < clippedEndX) {
+ byte *dstPixel = dst + dstY * pitch + clippedStartX;
+ for (int x = clippedStartX; x < clippedEndX; x++, dstPixel++)
+ *dstPixel = (byte)(*dstPixel + colorDelta);
+ }
+ }
+ srcX += runLength;
+ }
+
+ const int rowSize = 2 + lineSize;
+ if (rowSize > srcEnd - src)
+ break;
+ src += rowSize;
+ }
+}
+
+/**
+ * RA1 codec 2: Scatter/point draw (FUN_110D7).
+ */
+void smushDecodeRA1Scatter(byte *dst, const byte *src, int left, int top, int bufWidth, int bufHeight, int pitch, int dataSize) {
+ if (dst == nullptr || src == nullptr || pitch <= 0 || dataSize <= 0)
+ return;
+
+ int curX = left;
+ int curY = top;
+ while (dataSize >= 4) {
+ int16 dx = (int16)READ_LE_UINT16(src);
+ uint8 dy = src[2];
+ uint8 pixel = src[3];
+ src += 4;
+ dataSize -= 4;
+ curX += dx;
+ curY += dy;
+ if (curX >= 0 && curY >= 0 && curX < bufWidth && curY < bufHeight)
+ dst[curY * pitch + curX] = pixel;
+ }
+}
+
+// RA1 codec 4/5: block-based dithered codec with 4x4 tile lookup tables
+static uint8 s_ra1C4Tbl[2][256][16];
+static uint16 s_ra1C4Param = 0xFFFF;
+
+static void ra1Codec4GenTiles(uint16 param1) {
+ uint8 *dst = &s_ra1C4Tbl[0][0][0];
+ for (int i = 1; i < 16; i += 2) {
+ for (int k = 0; k < 16; k++) {
+ int j = i + param1, l = k + param1;
+ int m = (j + l) / 2, n = (j + m) / 2, o = (l + m) / 2;
+ if (j == m || l == m) {
+ *dst++ = l; *dst++ = j; *dst++ = l; *dst++ = j;
+ *dst++ = j; *dst++ = l; *dst++ = j; *dst++ = j;
+ *dst++ = l; *dst++ = j; *dst++ = l; *dst++ = j;
+ *dst++ = l; *dst++ = l; *dst++ = j; *dst++ = l;
+ } else {
+ *dst++ = m; *dst++ = m; *dst++ = n; *dst++ = j;
+ *dst++ = m; *dst++ = m; *dst++ = n; *dst++ = j;
+ *dst++ = o; *dst++ = o; *dst++ = m; *dst++ = n;
+ *dst++ = l; *dst++ = l; *dst++ = o; *dst++ = m;
+ }
+ }
+ }
+ for (int i = 0; i < 16; i += 2) {
+ for (int k = 0; k < 16; k++) {
+ int j = i + param1, l = k + param1;
+ int m = (j + l) / 2, n = (j + m) / 2, o = (l + m) / 2;
+ if (m == j || m == l) {
+ *dst++ = j; *dst++ = j; *dst++ = l; *dst++ = j;
+ *dst++ = j; *dst++ = j; *dst++ = j; *dst++ = l;
+ *dst++ = l; *dst++ = j; *dst++ = l; *dst++ = l;
+ *dst++ = j; *dst++ = l; *dst++ = j; *dst++ = l;
+ } else {
+ *dst++ = j; *dst++ = j; *dst++ = n; *dst++ = m;
+ *dst++ = j; *dst++ = j; *dst++ = n; *dst++ = m;
+ *dst++ = n; *dst++ = n; *dst++ = m; *dst++ = o;
+ *dst++ = m; *dst++ = m; *dst++ = o; *dst++ = l;
+ }
+ }
+ }
+}
+
+static bool ra1Codec4LoadTiles(const byte *&src, int &remaining, uint16 param2, uint8 clr) {
+ uint8 *dst = &s_ra1C4Tbl[1][0][0];
+ int loop = param2 * 8;
+ if (param2 > 256 || remaining < loop)
+ return false;
+ for (int i = 0; i < loop; i++) {
+ byte c = *src++;
+ remaining--;
+ *dst++ = (c >> 4) + clr;
+ *dst++ = (c & 0xF) + clr;
+ }
+ return true;
+}
+
+void smushDecodeRA1Block(byte *dst, const byte *src, int left, int top, int width, int height,
+ int pitch, int bufHeight, int dataSize, uint8 param, uint16 parm2, int codec) {
+ if (dst == nullptr || src == nullptr || width <= 0 || height <= 0 || pitch <= 0 || bufHeight <= 0 || dataSize <= 0)
+ return;
+
+ const int mx = pitch;
+ const int my = bufHeight;
+ if (s_ra1C4Param != param) {
+ ra1Codec4GenTiles(param);
+ s_ra1C4Param = param;
+ }
+ int remaining = dataSize;
+ const byte *data = src;
+ if (parm2 > 0) {
+ if (!ra1Codec4LoadTiles(data, remaining, parm2, param)) {
+ warning("smushDecodeRA1Block: not enough data for tile load (parm2=%d)", parm2);
+ return;
+ }
+ }
+ for (int j = 0; j < width; j += 4) {
+ byte mask = 0, bits = 0;
+ int x = left + j;
+ for (int i = 0; i < height; i += 4) {
+ int y = top + i;
+ int bit = 0;
+ if (parm2 > 0) {
+ if (bits == 0) {
+ if (remaining < 1)
+ return;
+ mask = *data++;
+ remaining--;
+ bits = 8;
+ }
+ bit = !!(mask & 0x80);
+ mask <<= 1;
+ bits--;
+ }
+ if (remaining < 1)
+ return;
+ byte idx = *data++;
+ remaining--;
+ if (bit == 0 && idx == 0x80 && codec != 5)
+ continue;
+ if (y >= my || (y + 4) < 0 || (x + 4) < 0 || x >= mx)
+ continue;
+ const byte *gs = &s_ra1C4Tbl[bit][idx][0];
+ if (y >= 0 && x >= 0 && (y + 4) <= my && (x + 4) <= mx) {
+ for (int k = 0; k < 4; k++, gs += 4)
+ memcpy(dst + x + (y + k) * pitch, gs, 4);
+ } else {
+ for (int k = 0; k < 4; k++)
+ for (int l = 0; l < 4; l++, gs++) {
+ int yo = y + k, xo = x + l;
+ if (yo >= 0 && yo < my && xo >= 0 && xo < mx)
+ *(dst + yo * pitch + xo) = *gs;
+ }
+ }
+ }
+ }
+}
+
+} // End of namespace Scumm
diff --git a/engines/scumm/smush/rebel/codec_ra1.h b/engines/scumm/smush/rebel/codec_ra1.h
new file mode 100644
index 00000000000..b509120323b
--- /dev/null
+++ b/engines/scumm/smush/rebel/codec_ra1.h
@@ -0,0 +1,40 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#ifndef SCUMM_SMUSH_REBEL_CODEC_RA1_H
+#define SCUMM_SMUSH_REBEL_CODEC_RA1_H
+
+#include "common/scummsys.h"
+
+namespace Scumm {
+
+void smushDecodeRA1Transparent(byte *dst, const byte *src, int left, int top, int width, int height, int pitch, int dataSize);
+void smushDecodeRA1SkipCopy(byte *dst, const byte *src, int left, int top, int width, int height,
+ int pitch, int bufWidth, int bufHeight, int dataSize);
+void smushDecodeRA1AdditiveLineUpdate(byte *dst, const byte *src, int left, int top, int width, int height,
+ int pitch, int bufWidth, int bufHeight, uint8 paletteBase, int dataSize);
+void smushDecodeRA1Scatter(byte *dst, const byte *src, int left, int top, int bufWidth, int bufHeight, int pitch, int dataSize);
+void smushDecodeRA1Block(byte *dst, const byte *src, int left, int top, int width, int height,
+ int pitch, int bufHeight, int dataSize, uint8 param, uint16 parm2, int codec);
+
+} // End of namespace Scumm
+
+#endif
diff --git a/engines/scumm/smush/rebel/smush_player_ra1.cpp b/engines/scumm/smush/rebel/smush_player_ra1.cpp
index 3c9c4c71e96..bb5527bff00 100644
--- a/engines/scumm/smush/rebel/smush_player_ra1.cpp
+++ b/engines/scumm/smush/rebel/smush_player_ra1.cpp
@@ -29,6 +29,7 @@
#include "scumm/file.h"
#include "scumm/scumm_v7.h"
+#include "scumm/smush/rebel/codec_ra1.h"
#include "scumm/smush/smush_font.h"
#include "scumm/smush/rebel/codec_ra2.h"
#include "scumm/smush/rebel/smush_player_ra1.h"
@@ -483,251 +484,6 @@ void SmushPlayerRebel1::handleGameParseNextFrame() {
processDispatches(_smushAudioSampleRate / _speed);
}
-// Forward declarations for RA1 codec functions (defined in smush_player.cpp and codec1.cpp)
-/**
- * RA1 codec 1: RLE with transparency on pixel 0.
- * Same BOMP encoding as smushDecodeRLE but pixel value 0 is not written,
- * allowing the background (restored via FTCH) to show through.
- */
-void smushDecodeRA1Transparent(byte *dst, const byte *src, int left, int top, int width, int height, int pitch) {
- dst += top * pitch;
- do {
- byte *rowDst = dst + left;
- const byte *lineData = src + 2;
- int remaining = width;
-
- while (remaining > 0) {
- byte code = *lineData++;
- byte num = (code >> 1) + 1;
- if (num > remaining)
- num = remaining;
- if (code & 1) {
- byte color = *lineData++;
- if (color != 0)
- memset(rowDst, color, num);
- } else {
- for (int j = 0; j < num; j++) {
- byte c = lineData[j];
- if (c != 0)
- rowDst[j] = c;
- }
- lineData += num;
- }
- rowDst += num;
- remaining -= num;
- }
-
- src += READ_LE_UINT16(src) + 2;
- dst += pitch;
- } while (--height);
-}
-/**
- * RA1 codec 21: Skip/copy line codec (FUN_10D41). Clip copy runs without
- * changing source X; stored cockpit patches can legitimately start offscreen.
- */
-void smushDecodeRA1SkipCopy(byte *dst, const byte *src, int left, int top, int width, int height,
- int pitch, int bufWidth, int bufHeight) {
- for (int row = 0; row < height; row++) {
- const uint16 lineSize = READ_LE_UINT16(src);
- const byte *lineData = src + 2;
- const byte *lineEnd = lineData + lineSize;
- const int dstY = top + row;
- int srcX = 0;
-
- while (srcX < width && lineData < lineEnd) {
- if (lineData + 2 > lineEnd)
- break;
- const uint16 skip = READ_LE_UINT16(lineData);
- lineData += 2;
- srcX += skip;
- if (srcX >= width)
- break;
-
- if (lineData + 2 > lineEnd)
- break;
- const int copyLen = READ_LE_UINT16(lineData) + 1;
- lineData += 2;
-
- const int readableLen = MIN<int>(copyLen, (int)(lineEnd - lineData));
- const int dstStartX = left + srcX;
- const int dstEndX = dstStartX + readableLen;
- if (readableLen > 0 && dstY >= 0 && dstY < bufHeight) {
- const int clippedStartX = MAX(dstStartX, 0);
- const int clippedEndX = MIN(dstEndX, bufWidth);
- if (clippedStartX < clippedEndX) {
- const int srcSkipX = clippedStartX - dstStartX;
- memcpy(dst + dstY * pitch + clippedStartX,
- lineData + srcSkipX, clippedEndX - clippedStartX);
- }
- }
-
- lineData += readableLen;
- srcX += copyLen;
- if (readableLen < copyLen)
- break;
- }
- src += lineSize + 2;
- }
-}
-
-/**
- * RA1 codec 23: Additive line-update overlay (FUN_10B40).
- */
-void smushDecodeRA1AdditiveLineUpdate(byte *dst, const byte *src, int left, int top, int width, int height,
- int pitch, int bufWidth, int bufHeight, uint8 paletteBase) {
- const uint8 colorDelta = (uint8)(paletteBase - 0x30);
- for (int row = 0; row < height; row++) {
- const uint16 lineSize = READ_LE_UINT16(src);
- const byte *lineData = src + 2;
- const byte *lineEnd = lineData + lineSize;
- const int dstY = top + row;
- int srcX = 0;
- while (srcX < width && lineData < lineEnd) {
- const int skip = *lineData++; srcX += skip;
- if (srcX >= width || lineData >= lineEnd) break;
- const int runLength = (int)(*lineData++);
- const int dstStartX = left + srcX;
- const int dstEndX = dstStartX + runLength;
- if (dstY >= 0 && dstY < bufHeight) {
- const int clippedStartX = MAX(dstStartX, 0);
- const int clippedEndX = MIN(dstEndX, bufWidth);
- if (clippedStartX < clippedEndX) {
- byte *dstPixel = dst + dstY * pitch + clippedStartX;
- for (int x = clippedStartX; x < clippedEndX; x++, dstPixel++)
- *dstPixel = (byte)(*dstPixel + colorDelta);
- }
- }
- srcX += runLength;
- }
- src += lineSize + 2;
- }
-}
-
-/**
- * RA1 codec 2: Scatter/point draw (FUN_110D7).
- */
-void smushDecodeRA1Scatter(byte *dst, const byte *src, int left, int top, int bufWidth, int bufHeight, int pitch, int dataSize) {
- int curX = left;
- int curY = top;
- while (dataSize >= 4) {
- int16 dx = (int16)READ_LE_UINT16(src);
- uint8 dy = src[2];
- uint8 pixel = src[3];
- src += 4; dataSize -= 4;
- curX += dx; curY += dy;
- if (curX >= 0 && curY >= 0 && curX < bufWidth && curY < bufHeight)
- dst[curY * pitch + curX] = pixel;
- }
-}
-
-// RA1 codec 4/5: block-based dithered codec with 4x4 tile lookup tables
-static uint8 s_ra1C4Tbl[2][256][16];
-static uint16 s_ra1C4Param = 0xFFFF;
-
-static void ra1Codec4GenTiles(uint16 param1) {
- uint8 *dst = &s_ra1C4Tbl[0][0][0];
- for (int i = 1; i < 16; i += 2) {
- for (int k = 0; k < 16; k++) {
- int j = i + param1, l = k + param1;
- int m = (j + l) / 2, n = (j + m) / 2, o = (l + m) / 2;
- if (j == m || l == m) {
- *dst++ = l; *dst++ = j; *dst++ = l; *dst++ = j;
- *dst++ = j; *dst++ = l; *dst++ = j; *dst++ = j;
- *dst++ = l; *dst++ = j; *dst++ = l; *dst++ = j;
- *dst++ = l; *dst++ = l; *dst++ = j; *dst++ = l;
- } else {
- *dst++ = m; *dst++ = m; *dst++ = n; *dst++ = j;
- *dst++ = m; *dst++ = m; *dst++ = n; *dst++ = j;
- *dst++ = o; *dst++ = o; *dst++ = m; *dst++ = n;
- *dst++ = l; *dst++ = l; *dst++ = o; *dst++ = m;
- }
- }
- }
- for (int i = 0; i < 16; i += 2) {
- for (int k = 0; k < 16; k++) {
- int j = i + param1, l = k + param1;
- int m = (j + l) / 2, n = (j + m) / 2, o = (l + m) / 2;
- if (m == j || m == l) {
- *dst++ = j; *dst++ = j; *dst++ = l; *dst++ = j;
- *dst++ = j; *dst++ = j; *dst++ = j; *dst++ = l;
- *dst++ = l; *dst++ = j; *dst++ = l; *dst++ = l;
- *dst++ = j; *dst++ = l; *dst++ = j; *dst++ = l;
- } else {
- *dst++ = j; *dst++ = j; *dst++ = n; *dst++ = m;
- *dst++ = j; *dst++ = j; *dst++ = n; *dst++ = m;
- *dst++ = n; *dst++ = n; *dst++ = m; *dst++ = o;
- *dst++ = m; *dst++ = m; *dst++ = o; *dst++ = l;
- }
- }
- }
-}
-
-static bool ra1Codec4LoadTiles(const byte *&src, int &remaining, uint16 param2, uint8 clr) {
- uint8 *dst = &s_ra1C4Tbl[1][0][0];
- int loop = param2 * 8;
- if (param2 > 256 || remaining < loop)
- return false;
- for (int i = 0; i < loop; i++) {
- byte c = *src++;
- remaining--;
- *dst++ = (c >> 4) + clr;
- *dst++ = (c & 0xF) + clr;
- }
- return true;
-}
-
-void smushDecodeRA1Block(byte *dst, const byte *src, int left, int top, int width, int height,
- int pitch, int bufHeight, int dataSize, uint8 param, uint16 parm2, int codec) {
- const int mx = pitch;
- const int my = bufHeight;
- if (s_ra1C4Param != param) {
- ra1Codec4GenTiles(param);
- s_ra1C4Param = param;
- }
- int remaining = dataSize;
- const byte *data = src;
- if (parm2 > 0) {
- if (!ra1Codec4LoadTiles(data, remaining, parm2, param)) {
- warning("smushDecodeRA1Block: not enough data for tile load (parm2=%d)", parm2);
- return;
- }
- }
- for (int j = 0; j < width; j += 4) {
- byte mask = 0, bits = 0;
- int x = left + j;
- for (int i = 0; i < height; i += 4) {
- int y = top + i;
- int bit = 0;
- if (parm2 > 0) {
- if (bits == 0) {
- if (remaining < 1) return;
- mask = *data++; remaining--; bits = 8;
- }
- bit = !!(mask & 0x80);
- mask <<= 1; bits--;
- }
- if (remaining < 1) return;
- byte idx = *data++; remaining--;
- if (bit == 0 && idx == 0x80 && codec != 5)
- continue;
- if (y >= my || (y + 4) < 0 || (x + 4) < 0 || x >= mx)
- continue;
- const byte *gs = &s_ra1C4Tbl[bit][idx][0];
- if (y >= 0 && x >= 0 && (y + 4) <= my && (x + 4) <= mx) {
- for (int k = 0; k < 4; k++, gs += 4)
- memcpy(dst + x + (y + k) * pitch, gs, 4);
- } else {
- for (int k = 0; k < 4; k++)
- for (int l = 0; l < 4; l++, gs++) {
- int yo = y + k, xo = x + l;
- if (yo >= 0 && yo < my && xo >= 0 && xo < mx)
- *(dst + yo * pitch + xo) = *gs;
- }
- }
- }
- }
-}
-
bool SmushPlayerRebel1::handleGameFrameBufferSelect(int codec, int width, int height) {
// RA1 sub-fullscreen frames render into _specialBuffer at their (left, top) offset position.
int bufSize = 384 * 242;
@@ -793,7 +549,7 @@ bool SmushPlayerRebel1::handleGameAdjustCoords(int codec, int &left, int &top, i
bool SmushPlayerRebel1::handleGameCodecDecode(int codec, const uint8 *src, int left, int top, int width, int height, int pitch, int dataSize, uint8 param, uint16 parm2) {
switch (codec) {
case SMUSH_CODEC_RLE:
- smushDecodeRA1Transparent(_dst, src, left, top, width, height, pitch);
+ smushDecodeRA1Transparent(_dst, src, left, top, width, height, pitch, dataSize);
return true;
case SMUSH_CODEC_RLE_ALT:
src = smushSkipRLELines(src, dataSize, _ra1FrameSourceSkipY);
@@ -810,7 +566,7 @@ bool SmushPlayerRebel1::handleGameCodecDecode(int codec, const uint8 *src, int l
return true;
case SMUSH_CODEC_LINE_UPDATE:
smushDecodeRA1SkipCopy(_dst, src, left, top, width, height, pitch,
- pitch, (_dst == _specialBuffer) ? _height : _vm->_screenHeight);
+ pitch, (_dst == _specialBuffer) ? _height : _vm->_screenHeight, dataSize);
return true;
case SMUSH_CODEC_SKIP_RLE: {
const int bufWidth = pitch;
@@ -820,7 +576,7 @@ bool SmushPlayerRebel1::handleGameCodecDecode(int codec, const uint8 *src, int l
// from this value before decoding, which Level 8 uses for the walker
// armor layers.
smushDecodeRA1AdditiveLineUpdate(_dst, src, left, top, width, height,
- pitch, bufWidth, bufHeight, param);
+ pitch, bufWidth, bufHeight, param, dataSize);
return true;
}
default:
Commit: 0e4cfe0eed25a91c0e26cf4641aa2b257f98d476
https://github.com/scummvm/scummvm/commit/0e4cfe0eed25a91c0e26cf4641aa2b257f98d476
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:10:12+02:00
Commit Message:
SCUMM: RA1: Split handleFrame
Changed paths:
engines/scumm/smush/rebel/smush_player_ra1.cpp
engines/scumm/smush/rebel/smush_player_ra1.h
diff --git a/engines/scumm/smush/rebel/smush_player_ra1.cpp b/engines/scumm/smush/rebel/smush_player_ra1.cpp
index bb5527bff00..e33221d76ee 100644
--- a/engines/scumm/smush/rebel/smush_player_ra1.cpp
+++ b/engines/scumm/smush/rebel/smush_player_ra1.cpp
@@ -723,9 +723,175 @@ static bool ra1FrameHasGameChunk(Common::SeekableReadStream &b, int32 frameSize)
return false;
}
+void SmushPlayerRebel1::ra1HandleFrameAudioChunk(int32 subSize, Common::SeekableReadStream &b) {
+ if (_compressedFileMode || isFastForwardingCurrentFrame())
+ return;
+
+ uint8 *audioChunk = (uint8 *)malloc(subSize + 8);
+ if (audioChunk == nullptr)
+ return;
+
+ b.seek(-8, SEEK_CUR);
+ b.read(audioChunk, subSize + 8);
+ feedAudio(audioChunk, 0, 127, 0, 0);
+ free(audioChunk);
+}
+
+void SmushPlayerRebel1::ra1HandleGameFrameChunk(int32 subSize, Common::SeekableReadStream &b, bool fastForwarding) {
+ if (!fastForwarding && _insane) {
+ InsaneRebel1 *rebel1 = (InsaneRebel1 *)_insane;
+ rebel1->handleGameChunk(subSize, b);
+ }
+}
+
+void SmushPlayerRebel1::ra1HandleObjOverlayFrameChunk(int32 objDataSize, Common::SeekableReadStream &b, bool fastForwarding) {
+ if (objDataSize <= 0)
+ return;
+
+ byte *objBuf = (byte *)malloc(objDataSize);
+ if (objBuf == nullptr)
+ return;
+
+ b.read(objBuf, objDataSize);
+
+ int32 objPos = 0;
+ while (objPos + 8 < objDataSize) {
+ uint32 embTag = READ_BE_UINT32(objBuf + objPos);
+ uint32 embSize = READ_BE_UINT32(objBuf + objPos + 4);
+ int32 embRemaining = objDataSize - objPos - 8;
+
+ bool recognized = (embTag == MKTAG('F','O','B','J') ||
+ embTag == MKTAG('G','A','M','E') ||
+ embTag == MKTAG('G','A','M','2') ||
+ embTag == MKTAG('P','S','A','D'));
+
+ if (!recognized || embSize > (uint32)embRemaining) {
+ objPos++;
+ continue;
+ }
+
+ if (embTag == MKTAG('F','O','B','J') && embSize >= 14) {
+ Common::MemoryReadStream embStream(objBuf + objPos + 8, embSize);
+ handleFrameObject(embSize, embStream);
+
+ if (_ra1ObjOverlayData == nullptr ||
+ (int32)embSize > _ra1ObjOverlayDataSize) {
+ free(_ra1ObjOverlayData);
+ _ra1ObjOverlayDataSize = embSize;
+ _ra1ObjOverlayData = (byte *)malloc(embSize);
+ memcpy(_ra1ObjOverlayData, objBuf + objPos + 8, embSize);
+ _ra1ObjOverlayCodec = objBuf[objPos + 8] & 0xFF;
+ _ra1ObjOverlayLeft = (int16)READ_LE_UINT16(objBuf + objPos + 10);
+ _ra1ObjOverlayTop = (int16)READ_LE_UINT16(objBuf + objPos + 12);
+ _ra1ObjOverlayWidth = READ_LE_UINT16(objBuf + objPos + 14);
+ _ra1ObjOverlayHeight = READ_LE_UINT16(objBuf + objPos + 16);
+ }
+ } else if (embTag == MKTAG('G','A','M','E') || embTag == MKTAG('G','A','M','2')) {
+ Common::MemoryReadStream embStream(objBuf + objPos + 8, embSize);
+ ra1HandleGameFrameChunk(embSize, embStream, fastForwarding);
+ } else if (embTag == MKTAG('P','S','A','D')) {
+ if (!_compressedFileMode && !isFastForwardingCurrentFrame()) {
+ uint8 *audioBuf = (uint8 *)malloc(embSize + 8);
+ if (audioBuf == nullptr)
+ break;
+ memcpy(audioBuf, objBuf + objPos, embSize + 8);
+ feedAudio(audioBuf, 0, 127, 0, 0);
+ free(audioBuf);
+ }
+ }
+
+ objPos += 8 + embSize;
+ if (embSize & 1)
+ objPos++;
+ }
+
+ free(objBuf);
+}
+
+bool SmushPlayerRebel1::ra1HandleUnknownFrameChunk(uint32 subType, int32 subSize) {
+ // Original FUN_1FDBC: unknown uppercase tag -> silently stop
+ byte tb0 = (subType >> 24) & 0xFF, tb1 = (subType >> 16) & 0xFF;
+ byte tb2 = (subType >> 8) & 0xFF, tb3 = subType & 0xFF;
+ if (tb0 > 0x40 && tb0 < 0x5B && tb1 > 0x40 && tb1 < 0x5B &&
+ tb2 > 0x40 && tb2 < 0x5B && tb3 > 0x40 && tb3 < 0x5B) {
+ debug(5, "RA1: unknown uppercase tag %s at frame %d, stopping frame parse", tag2str(subType), _frame);
+ return true;
+ }
+
+ error("Unknown frame subChunk found : %s, %d", tag2str(subType), subSize);
+ return false;
+}
+
+bool SmushPlayerRebel1::ra1DispatchFrameChunk(uint32 subType, int32 subSize, int32 &frameSize,
+ Common::SeekableReadStream &b, bool fastForwarding) {
+ switch (subType) {
+ case MKTAG('N','P','A','L'):
+ handleNewPalette(subSize, b);
+ break;
+ case MKTAG('F','O','B','J'):
+ handleFrameObject(subSize, b);
+ break;
+ case MKTAG('Z','F','O','B'):
+ handleZlibFrameObject(subSize, b);
+ break;
+ case MKTAG('P','S','A','D'):
+ case MKTAG('P','V','O','C'):
+ ra1HandleFrameAudioChunk(subSize, b);
+ break;
+ case MKTAG('T','R','E','S'):
+ case MKTAG('T','E','X','T'):
+ handleTextResource(subType, subSize, b);
+ break;
+ case MKTAG('X','P','A','L'):
+ ra1HandleDeltaPalette(subSize, b);
+ break;
+ case MKTAG('I','A','C','T'):
+ handleIACT(subSize, b);
+ break;
+ case MKTAG('S','T','O','R'):
+ handleStore(subSize, b);
+ break;
+ case MKTAG('F','T','C','H'):
+ handleFetch(subSize, b);
+ break;
+ case MKTAG('S','K','I','P'):
+ _insane->procSKIP(subSize, b);
+ break;
+ case MKTAG('G','O','S','T'):
+ handleGameGost(subSize, b);
+ break;
+ case MKTAG('G','A','M','E'):
+ case MKTAG('G','A','M','2'):
+ ra1HandleGameFrameChunk(subSize, b, fastForwarding);
+ break;
+ case MKTAG('O','B','J','\0'):
+ ra1HandleObjOverlayFrameChunk(frameSize - 8, b, fastForwarding);
+ frameSize = 0;
+ return true;
+ case MKTAG('F','A','D','E'):
+ ra1HandleFade(subSize, b);
+ break;
+ case MKTAG('S','E','G','A'):
+ case MKTAG('A','D','L',' '):
+ case MKTAG('A','D','L','2'):
+ case MKTAG('S','B','L',' '):
+ case MKTAG('S','B','L','2'):
+ case MKTAG('P','S','D','2'):
+ debugC(DEBUG_SMUSH, "SmushPlayerRebel1::handleFrame: skipping chunk %s (%d bytes)", tag2str(subType), subSize);
+ break;
+ default:
+ if (ra1HandleUnknownFrameChunk(subType, subSize)) {
+ frameSize = 0;
+ return true;
+ }
+ break;
+ }
+
+ return false;
+}
+
void SmushPlayerRebel1::handleFrame(int32 frameSize, Common::SeekableReadStream &b) {
debugC(DEBUG_SMUSH, "SmushPlayerRebel1::handleFrame(%d)", _frame);
- uint8 *audioChunk = nullptr;
_skipNext = false;
handleGameFrameStart();
const bool fastForwarding = isFastForwardingCurrentFrame();
@@ -795,144 +961,8 @@ void SmushPlayerRebel1::handleFrame(int32 frameSize, Common::SeekableReadStream
break;
}
- switch (subType) {
- case MKTAG('N','P','A','L'):
- handleNewPalette(subSize, b);
- break;
- case MKTAG('F','O','B','J'):
- handleFrameObject(subSize, b);
- break;
- case MKTAG('Z','F','O','B'):
- handleZlibFrameObject(subSize, b);
- break;
- case MKTAG('P','S','A','D'):
- case MKTAG('P','V','O','C'):
- if (!_compressedFileMode && !isFastForwardingCurrentFrame()) {
- audioChunk = (uint8 *)malloc(subSize + 8);
- b.seek(-8, SEEK_CUR);
- b.read(audioChunk, subSize + 8);
- feedAudio(audioChunk, 0, 127, 0, 0);
- free(audioChunk);
- audioChunk = nullptr;
- }
- break;
- case MKTAG('T','R','E','S'):
- case MKTAG('T','E','X','T'):
- handleTextResource(subType, subSize, b);
- break;
- case MKTAG('X','P','A','L'):
- ra1HandleDeltaPalette(subSize, b);
- break;
- case MKTAG('I','A','C','T'):
- handleIACT(subSize, b);
- break;
- case MKTAG('S','T','O','R'):
- handleStore(subSize, b);
- break;
- case MKTAG('F','T','C','H'):
- handleFetch(subSize, b);
- break;
- case MKTAG('S','K','I','P'):
- _insane->procSKIP(subSize, b);
- break;
- case MKTAG('G','O','S','T'):
- handleGameGost(subSize, b);
- break;
- case MKTAG('G','A','M','E'):
- case MKTAG('G','A','M','2'): {
- if (!fastForwarding) {
- InsaneRebel1 *rebel1 = (InsaneRebel1 *)_insane;
- rebel1->handleGameChunk(subSize, b);
- }
- break;
- }
- case MKTAG('O','B','J','\0'): {
- // RA1 object overlay chunk: variable-size header + embedded FOBJ/GAME/PSAD.
- int32 objDataSize = frameSize - 8;
- if (objDataSize > 0) {
- byte *objBuf = (byte *)malloc(objDataSize);
- b.read(objBuf, objDataSize);
-
- int32 objPos = 0;
- while (objPos + 8 < objDataSize) {
- uint32 embTag = READ_BE_UINT32(objBuf + objPos);
- uint32 embSize = READ_BE_UINT32(objBuf + objPos + 4);
- int32 embRemaining = objDataSize - objPos - 8;
-
- bool recognized = (embTag == MKTAG('F','O','B','J') ||
- embTag == MKTAG('G','A','M','E') ||
- embTag == MKTAG('G','A','M','2') ||
- embTag == MKTAG('P','S','A','D'));
-
- if (!recognized || embSize > (uint32)embRemaining) {
- objPos++;
- continue;
- }
-
- if (embTag == MKTAG('F','O','B','J') && embSize >= 14) {
- Common::MemoryReadStream embStream(objBuf + objPos + 8, embSize);
- handleFrameObject(embSize, embStream);
-
- if (_ra1ObjOverlayData == nullptr ||
- (int32)embSize > _ra1ObjOverlayDataSize) {
- free(_ra1ObjOverlayData);
- _ra1ObjOverlayDataSize = embSize;
- _ra1ObjOverlayData = (byte *)malloc(embSize);
- memcpy(_ra1ObjOverlayData, objBuf + objPos + 8, embSize);
- _ra1ObjOverlayCodec = objBuf[objPos + 8] & 0xFF;
- _ra1ObjOverlayLeft = (int16)READ_LE_UINT16(objBuf + objPos + 10);
- _ra1ObjOverlayTop = (int16)READ_LE_UINT16(objBuf + objPos + 12);
- _ra1ObjOverlayWidth = READ_LE_UINT16(objBuf + objPos + 14);
- _ra1ObjOverlayHeight = READ_LE_UINT16(objBuf + objPos + 16);
- }
- } else if (embTag == MKTAG('G','A','M','E') || embTag == MKTAG('G','A','M','2')) {
- if (!fastForwarding) {
- Common::MemoryReadStream embStream(objBuf + objPos + 8, embSize);
- InsaneRebel1 *rebel1 = (InsaneRebel1 *)_insane;
- rebel1->handleGameChunk(embSize, embStream);
- }
- } else if (embTag == MKTAG('P','S','A','D')) {
- if (!_compressedFileMode && !isFastForwardingCurrentFrame()) {
- uint8 *audioBuf = (uint8 *)malloc(embSize + 8);
- memcpy(audioBuf, objBuf + objPos, embSize + 8);
- feedAudio(audioBuf, 0, 127, 0, 0);
- free(audioBuf);
- }
- }
-
- objPos += 8 + embSize;
- if (embSize & 1)
- objPos++;
- }
- free(objBuf);
- }
- frameSize = 0;
+ if (ra1DispatchFrameChunk(subType, subSize, frameSize, b, fastForwarding))
continue;
- }
- case MKTAG('F','A','D','E'):
- ra1HandleFade(subSize, b);
- break;
- case MKTAG('S','E','G','A'):
- case MKTAG('A','D','L',' '):
- case MKTAG('A','D','L','2'):
- case MKTAG('S','B','L',' '):
- case MKTAG('S','B','L','2'):
- case MKTAG('P','S','D','2'):
- debugC(DEBUG_SMUSH, "SmushPlayerRebel1::handleFrame: skipping chunk %s (%d bytes)", tag2str(subType), subSize);
- break;
- default: {
- // Original FUN_1FDBC: unknown uppercase tag â silently stop
- byte tb0 = (subType >> 24) & 0xFF, tb1 = (subType >> 16) & 0xFF;
- byte tb2 = (subType >> 8) & 0xFF, tb3 = subType & 0xFF;
- if (tb0 > 0x40 && tb0 < 0x5B && tb1 > 0x40 && tb1 < 0x5B &&
- tb2 > 0x40 && tb2 < 0x5B && tb3 > 0x40 && tb3 < 0x5B) {
- debug(5, "RA1: unknown uppercase tag %s at frame %d, stopping frame parse", tag2str(subType), _frame);
- frameSize = 0;
- continue;
- }
- error("Unknown frame subChunk found : %s, %d", tag2str(subType), subSize);
- }
- }
frameSize -= subSize + 8;
b.seek(subOffset + subSize, SEEK_SET);
diff --git a/engines/scumm/smush/rebel/smush_player_ra1.h b/engines/scumm/smush/rebel/smush_player_ra1.h
index 5d54cfd0b1a..ff1dc1befdc 100644
--- a/engines/scumm/smush/rebel/smush_player_ra1.h
+++ b/engines/scumm/smush/rebel/smush_player_ra1.h
@@ -64,6 +64,12 @@ private:
void ra1HandleFade(int32 subSize, Common::SeekableReadStream &b);
SmushFont *ra1GetFont(int font);
void ra1HandleText(int32 subSize, Common::SeekableReadStream &b);
+ void ra1HandleFrameAudioChunk(int32 subSize, Common::SeekableReadStream &b);
+ void ra1HandleGameFrameChunk(int32 subSize, Common::SeekableReadStream &b, bool fastForwarding);
+ void ra1HandleObjOverlayFrameChunk(int32 objDataSize, Common::SeekableReadStream &b, bool fastForwarding);
+ bool ra1HandleUnknownFrameChunk(uint32 subType, int32 subSize);
+ bool ra1DispatchFrameChunk(uint32 subType, int32 subSize, int32 &frameSize,
+ Common::SeekableReadStream &b, bool fastForwarding);
// RA1 clean frame buffer for delta source restoration
byte *_ra1CleanFrame;
Commit: 1c4b90088378d95b36f3d6d3b2ab28a91bdd7ffd
https://github.com/scummvm/scummvm/commit/1c4b90088378d95b36f3d6d3b2ab28a91bdd7ffd
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:10:12+02:00
Commit Message:
SCUMM: RA1: Split playInteractiveVideo
Changed paths:
engines/scumm/insane/rebel1/rebel.h
engines/scumm/insane/rebel1/runlevels.cpp
diff --git a/engines/scumm/insane/rebel1/rebel.h b/engines/scumm/insane/rebel1/rebel.h
index b8b1678ffc0..4c7baa881d4 100644
--- a/engines/scumm/insane/rebel1/rebel.h
+++ b/engines/scumm/insane/rebel1/rebel.h
@@ -161,6 +161,12 @@ private:
// Play interactive gameplay video (with ship physics + HUD)
void playInteractiveVideo(const char *filename, int32 startFrame = 0);
+ void resetInteractiveVideoAudio();
+ void setupInteractiveVideoState(int32 startFrame);
+ void resolveSeek(const char *filename, int32 startFrame, int32 &videoOffset, int32 &videoStartFrame);
+ void captureInteractiveVideoInput();
+ void releaseInteractiveVideoInput();
+ void playInteractiveVideoFile(const char *filename, int32 videoOffset, int32 videoStartFrame);
bool loadRA1Nut(const char *filename, RA1SpriteBank &bank);
void loadLevelSprites(int level);
void updateShipPhysics();
diff --git a/engines/scumm/insane/rebel1/runlevels.cpp b/engines/scumm/insane/rebel1/runlevels.cpp
index 80da8bfcbfa..2e6334daca2 100644
--- a/engines/scumm/insane/rebel1/runlevels.cpp
+++ b/engines/scumm/insane/rebel1/runlevels.cpp
@@ -1556,19 +1556,16 @@ void InsaneRebel1::runGame() {
}
}
-// Play interactive gameplay video (with ship physics + HUD).
-void InsaneRebel1::playInteractiveVideo(const char *filename, int32 startFrame) {
- debug(1, "InsaneRebel1::playInteractiveVideo('%s', startFrame=%d)", filename, startFrame);
+void InsaneRebel1::resetInteractiveVideoAudio() {
+ terminateAudio();
+ initAudio(_audioSampleRate);
+}
+
+void InsaneRebel1::setupInteractiveVideoState(int32 startFrame) {
const bool level7RouteSplice = (_currentLevel == 6 && _levelRouteIndex > 0);
const bool resumingRoute = startFrame > 0;
const bool preserveRuntimeState = resumingRoute || level7RouteSplice;
const bool preserveVideoState = resumingRoute && !level7RouteSplice;
- int32 videoStartFrame = 0;
- int32 videoOffset = 0;
-
- // Stop any leftover audio from previous video
- terminateAudio();
- initAudio(_audioSampleRate);
SmushPlayer *splayer = _vm->_splayer;
_player = splayer;
@@ -1588,6 +1585,15 @@ void InsaneRebel1::playInteractiveVideo(const char *filename, int32 startFrame)
splayer->setCurVideoFlags(0x28);
splayer->setFastForwardFromFrame(0);
splayer->setFastForwardToFrame(0);
+}
+
+void InsaneRebel1::resolveSeek(const char *filename, int32 startFrame, int32 &videoOffset, int32 &videoStartFrame) {
+ const bool level7RouteSplice = (_currentLevel == 6 && _levelRouteIndex > 0);
+ const bool resumingRoute = startFrame > 0;
+
+ videoStartFrame = 0;
+ videoOffset = 0;
+
if (_currentLevel == 6 && level7RouteSplice) {
// DOS opens the route ANM from the beginning, then the armed frame gate
// suppresses until the adjusted target. With ScummVM's delayed cutover,
@@ -1630,6 +1636,10 @@ void InsaneRebel1::playInteractiveVideo(const char *filename, int32 startFrame)
debug(1, "RA1 L14 splice: L14PLAY2 timelineFrame=%d -> L14PLY2B frame 0",
(int)startFrame);
}
+}
+
+void InsaneRebel1::captureInteractiveVideoInput() {
+ const bool level7RouteSplice = (_currentLevel == 6 && _levelRouteIndex > 0);
// Center mouse, hide system cursor (we draw our own), lock mouse to window.
// Level 7 route splices happen inside one original gameplay loop, so keep
@@ -1644,11 +1654,29 @@ void InsaneRebel1::playInteractiveVideo(const char *filename, int32 startFrame)
}
CursorMan.showMouse(false);
g_system->lockMouse(true);
+}
+
+void InsaneRebel1::releaseInteractiveVideoInput() {
+ g_system->lockMouse(false);
+}
- splayer->play(filename, 12, videoOffset, videoStartFrame);
+void InsaneRebel1::playInteractiveVideoFile(const char *filename, int32 videoOffset, int32 videoStartFrame) {
+ _vm->_splayer->play(filename, 12, videoOffset, videoStartFrame);
_interactiveVideoActive = false;
+}
- g_system->lockMouse(false);
+// Play interactive gameplay video (with ship physics + HUD).
+void InsaneRebel1::playInteractiveVideo(const char *filename, int32 startFrame) {
+ debug(1, "InsaneRebel1::playInteractiveVideo('%s', startFrame=%d)", filename, startFrame);
+ int32 videoStartFrame = 0;
+ int32 videoOffset = 0;
+
+ resetInteractiveVideoAudio();
+ setupInteractiveVideoState(startFrame);
+ resolveSeek(filename, startFrame, videoOffset, videoStartFrame);
+ captureInteractiveVideoInput();
+ playInteractiveVideoFile(filename, videoOffset, videoStartFrame);
+ releaseInteractiveVideoInput();
}
} // End of namespace Scumm
Commit: 3fc5813aa35acd8579e551c4d209062e6de7e97a
https://github.com/scummvm/scummvm/commit/3fc5813aa35acd8579e551c4d209062e6de7e97a
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:10:12+02:00
Commit Message:
SCUMM: RA1: Refactor menu handling and rendering
Changed paths:
engines/scumm/insane/rebel1/menu.cpp
engines/scumm/insane/rebel1/rebel.h
diff --git a/engines/scumm/insane/rebel1/menu.cpp b/engines/scumm/insane/rebel1/menu.cpp
index 1d6d033016a..c7628ea5fea 100644
--- a/engines/scumm/insane/rebel1/menu.cpp
+++ b/engines/scumm/insane/rebel1/menu.cpp
@@ -149,6 +149,60 @@ static char normalizeRebel1PasscodeChar(char c) {
return c;
}
+static RA1MenuCommand getRebel1MenuCommandFromAction(ScummAction action) {
+ switch (action) {
+ case kScummActionInsaneUp:
+ return kRA1MenuCommandUp;
+ case kScummActionInsaneDown:
+ return kRA1MenuCommandDown;
+ case kScummActionInsaneLeft:
+ return kRA1MenuCommandLeft;
+ case kScummActionInsaneRight:
+ return kRA1MenuCommandRight;
+ case kScummActionInsaneAttack:
+ return kRA1MenuCommandAccept;
+ default:
+ return kRA1MenuCommandNone;
+ }
+}
+
+static RA1MenuCommand getRebel1MenuCommandFromKey(const Common::KeyState &kbd) {
+ switch (kbd.keycode) {
+ case Common::KEYCODE_UP:
+ case Common::KEYCODE_w:
+ return kRA1MenuCommandUp;
+ case Common::KEYCODE_DOWN:
+ case Common::KEYCODE_s:
+ return kRA1MenuCommandDown;
+ case Common::KEYCODE_LEFT:
+ case Common::KEYCODE_a:
+ return kRA1MenuCommandLeft;
+ case Common::KEYCODE_RIGHT:
+ case Common::KEYCODE_d:
+ return kRA1MenuCommandRight;
+ case Common::KEYCODE_RETURN:
+ case Common::KEYCODE_KP_ENTER:
+ case Common::KEYCODE_SPACE:
+ return kRA1MenuCommandAccept;
+ case Common::KEYCODE_ESCAPE:
+ return kRA1MenuCommandCancel;
+ case Common::KEYCODE_1:
+ return kRA1MenuCommandSelect1;
+ case Common::KEYCODE_2:
+ return kRA1MenuCommandSelect2;
+ case Common::KEYCODE_3:
+ return kRA1MenuCommandSelect3;
+ case Common::KEYCODE_4:
+ return kRA1MenuCommandSelect4;
+ case Common::KEYCODE_5:
+ return kRA1MenuCommandSelect5;
+ case Common::KEYCODE_6:
+ return kRA1MenuCommandSelect6;
+ default:
+ return kRA1MenuCommandNone;
+ }
+}
+
static bool isRebel1TextEntryChar(bool passcodeMode, char c) {
if (passcodeMode) {
c = normalizeRebel1PasscodeChar(c);
@@ -314,35 +368,38 @@ bool InsaneRebel1::handleTextEntryKey(const Common::Event &event) {
return true;
}
-bool InsaneRebel1::handleControllerMenuAction(ScummAction action) {
- if (!_menuActive || _highScoresActive)
+bool InsaneRebel1::handleMenuCommand(RA1MenuCommand command) {
+ if (!_menuActive || _highScoresActive || command == kRA1MenuCommandNone)
return false;
if (_textEntryActive)
- return handleTextEntryAction(action);
+ return false;
if (_levelSelectActive) {
int col = _levelSelectSel / kRA1LevelSelectRowsPerCol;
int row = _levelSelectSel % kRA1LevelSelectRowsPerCol;
- switch (action) {
- case kScummActionInsaneUp:
+ switch (command) {
+ case kRA1MenuCommandUp:
row = (row + kRA1LevelSelectRowsPerCol - 1) % kRA1LevelSelectRowsPerCol;
_levelSelectSel = col * kRA1LevelSelectRowsPerCol + row;
return true;
- case kScummActionInsaneDown:
+ case kRA1MenuCommandDown:
row = (row + 1) % kRA1LevelSelectRowsPerCol;
_levelSelectSel = col * kRA1LevelSelectRowsPerCol + row;
return true;
- case kScummActionInsaneLeft:
+ case kRA1MenuCommandLeft:
if (col > 0)
_levelSelectSel -= kRA1LevelSelectRowsPerCol;
return true;
- case kScummActionInsaneRight:
+ case kRA1MenuCommandRight:
if (col < 1)
_levelSelectSel += kRA1LevelSelectRowsPerCol;
return true;
- case kScummActionInsaneAttack:
+ case kRA1MenuCommandCancel:
+ _levelSelectSel = kRA1LevelSelectItemCount - 1; // Back
+ // fall through
+ case kRA1MenuCommandAccept:
_menuConfirmed = true;
_vm->_smushVideoShouldFinish = true;
return true;
@@ -352,22 +409,25 @@ bool InsaneRebel1::handleControllerMenuAction(ScummAction action) {
}
if (_optionsActive) {
- switch (action) {
- case kScummActionInsaneUp:
+ switch (command) {
+ case kRA1MenuCommandUp:
_optionsSel = (_optionsSel + kOptionsItemCount - 1) % kOptionsItemCount;
return true;
- case kScummActionInsaneDown:
+ case kRA1MenuCommandDown:
_optionsSel = (_optionsSel + 1) % kOptionsItemCount;
return true;
- case kScummActionInsaneLeft:
+ case kRA1MenuCommandLeft:
if (_optionsSel == 7)
setRebel1Volume(_vm, _optVolume, -5);
return true;
- case kScummActionInsaneRight:
+ case kRA1MenuCommandRight:
if (_optionsSel == 7)
setRebel1Volume(_vm, _optVolume, 5);
return true;
- case kScummActionInsaneAttack:
+ case kRA1MenuCommandCancel:
+ _optionsSel = 0;
+ // fall through
+ case kRA1MenuCommandAccept:
_menuConfirmed = true;
_vm->_smushVideoShouldFinish = true;
return true;
@@ -376,14 +436,27 @@ bool InsaneRebel1::handleControllerMenuAction(ScummAction action) {
}
}
- switch (action) {
- case kScummActionInsaneUp:
+ switch (command) {
+ case kRA1MenuCommandUp:
_menuSelection = (_menuSelection + kRA1MainMenuItemCount - 1) % kRA1MainMenuItemCount;
return true;
- case kScummActionInsaneDown:
+ case kRA1MenuCommandDown:
_menuSelection = (_menuSelection + 1) % kRA1MainMenuItemCount;
return true;
- case kScummActionInsaneAttack:
+ case kRA1MenuCommandCancel:
+ _menuSelection = kRA1MainMenuItemCount - 1;
+ // fall through
+ case kRA1MenuCommandAccept:
+ _menuConfirmed = true;
+ _vm->_smushVideoShouldFinish = true;
+ return true;
+ case kRA1MenuCommandSelect1:
+ case kRA1MenuCommandSelect2:
+ case kRA1MenuCommandSelect3:
+ case kRA1MenuCommandSelect4:
+ case kRA1MenuCommandSelect5:
+ case kRA1MenuCommandSelect6:
+ _menuSelection = command - kRA1MenuCommandSelect1;
_menuConfirmed = true;
_vm->_smushVideoShouldFinish = true;
return true;
@@ -392,6 +465,16 @@ bool InsaneRebel1::handleControllerMenuAction(ScummAction action) {
}
}
+bool InsaneRebel1::handleControllerMenuAction(ScummAction action) {
+ if (!_menuActive || _highScoresActive)
+ return false;
+
+ if (_textEntryActive)
+ return handleTextEntryAction(action);
+
+ return handleMenuCommand(getRebel1MenuCommandFromAction(action));
+}
+
bool InsaneRebel1::handleControllerMenuAxis(int16 oldAxisX, int16 oldAxisY) {
if (!_menuActive || _highScoresActive)
return false;
@@ -406,9 +489,9 @@ bool InsaneRebel1::handleControllerMenuAxis(int16 oldAxisX, int16 oldAxisY) {
const int newY = getRebel1MenuAxisDirection(_joystickAxisY);
if (newY != oldY && newY != 0)
- return handleControllerMenuAction((newY > 0) != _optControlsYFlip ? kScummActionInsaneDown : kScummActionInsaneUp);
+ return handleMenuCommand((newY > 0) != _optControlsYFlip ? kRA1MenuCommandDown : kRA1MenuCommandUp);
if (newX != oldX && newX != 0)
- return handleControllerMenuAction(newX > 0 ? kScummActionInsaneRight : kScummActionInsaneLeft);
+ return handleMenuCommand(newX > 0 ? kRA1MenuCommandRight : kRA1MenuCommandLeft);
return false;
}
@@ -546,119 +629,9 @@ bool InsaneRebel1::notifyEvent(const Common::Event &event) {
if (_menuActive && _textEntryActive && event.type == Common::EVENT_KEYDOWN)
return handleTextEntryKey(event);
- if (_menuActive && _levelSelectActive && event.type == Common::EVENT_KEYDOWN) {
- int col = _levelSelectSel / kRA1LevelSelectRowsPerCol;
- int row = _levelSelectSel % kRA1LevelSelectRowsPerCol;
- switch (event.kbd.keycode) {
- case Common::KEYCODE_UP:
- case Common::KEYCODE_w:
- row = (row + kRA1LevelSelectRowsPerCol - 1) % kRA1LevelSelectRowsPerCol;
- _levelSelectSel = col * kRA1LevelSelectRowsPerCol + row;
- return true;
- case Common::KEYCODE_DOWN:
- case Common::KEYCODE_s:
- row = (row + 1) % kRA1LevelSelectRowsPerCol;
- _levelSelectSel = col * kRA1LevelSelectRowsPerCol + row;
- return true;
- case Common::KEYCODE_LEFT:
- case Common::KEYCODE_a:
- if (col > 0)
- _levelSelectSel -= kRA1LevelSelectRowsPerCol;
- return true;
- case Common::KEYCODE_RIGHT:
- case Common::KEYCODE_d:
- if (col < 1)
- _levelSelectSel += kRA1LevelSelectRowsPerCol;
- return true;
- case Common::KEYCODE_RETURN:
- case Common::KEYCODE_KP_ENTER:
- case Common::KEYCODE_SPACE:
- _menuConfirmed = true;
- _vm->_smushVideoShouldFinish = true;
- return true;
- case Common::KEYCODE_ESCAPE:
- _levelSelectSel = kRA1LevelSelectItemCount - 1; // Back
- _menuConfirmed = true;
- _vm->_smushVideoShouldFinish = true;
- return true;
- default:
- break;
- }
- }
-
- if (_menuActive && _optionsActive && event.type == Common::EVENT_KEYDOWN) {
- switch (event.kbd.keycode) {
- case Common::KEYCODE_UP:
- case Common::KEYCODE_w:
- _optionsSel = (_optionsSel + kOptionsItemCount - 1) % kOptionsItemCount;
- return true;
- case Common::KEYCODE_DOWN:
- case Common::KEYCODE_s:
- _optionsSel = (_optionsSel + 1) % kOptionsItemCount;
- return true;
- case Common::KEYCODE_LEFT:
- case Common::KEYCODE_a:
- // Volume down when on volume row (row 7)
- if (_optionsSel == 7)
- setRebel1Volume(_vm, _optVolume, -5);
- return true;
- case Common::KEYCODE_RIGHT:
- case Common::KEYCODE_d:
- // Volume up when on volume row (row 7)
- if (_optionsSel == 7)
- setRebel1Volume(_vm, _optVolume, 5);
- return true;
- case Common::KEYCODE_RETURN:
- case Common::KEYCODE_KP_ENTER:
- case Common::KEYCODE_SPACE:
- _menuConfirmed = true;
- _vm->_smushVideoShouldFinish = true;
- return true;
- case Common::KEYCODE_ESCAPE:
- _optionsSel = 0;
- _menuConfirmed = true;
- _vm->_smushVideoShouldFinish = true;
- return true;
- default:
- break;
- }
- }
-
- if (_menuActive && !_optionsActive && !_levelSelectActive && event.type == Common::EVENT_KEYDOWN) {
- switch (event.kbd.keycode) {
- case Common::KEYCODE_UP:
- case Common::KEYCODE_w:
- _menuSelection = (_menuSelection + kRA1MainMenuItemCount - 1) % kRA1MainMenuItemCount;
- return true;
- case Common::KEYCODE_DOWN:
- case Common::KEYCODE_s:
- _menuSelection = (_menuSelection + 1) % kRA1MainMenuItemCount;
- return true;
- case Common::KEYCODE_RETURN:
- case Common::KEYCODE_KP_ENTER:
- case Common::KEYCODE_SPACE:
- _menuConfirmed = true;
- _vm->_smushVideoShouldFinish = true;
- return true;
- case Common::KEYCODE_1:
- case Common::KEYCODE_2:
- case Common::KEYCODE_3:
- case Common::KEYCODE_4:
- case Common::KEYCODE_5:
- case Common::KEYCODE_6:
- _menuSelection = event.kbd.keycode - Common::KEYCODE_1;
- _menuConfirmed = true;
- _vm->_smushVideoShouldFinish = true;
- return true;
- case Common::KEYCODE_ESCAPE:
- _menuSelection = kRA1MainMenuItemCount - 1;
- _menuConfirmed = true;
- _vm->_smushVideoShouldFinish = true;
- return true;
- default:
- break;
- }
- }
+ if (_menuActive && event.type == Common::EVENT_KEYDOWN &&
+ handleMenuCommand(getRebel1MenuCommandFromKey(event.kbd)))
+ return true;
// Shooting: mouse button during interactive gameplay â FUN_1CCA0 (0x1CCA0)
if (_interactiveVideoActive && !_menuActive) {
@@ -731,138 +704,127 @@ bool InsaneRebel1::notifyEvent(const Common::Event &event) {
return false;
}
-void InsaneRebel1::renderMainMenuOverlay(byte *dst, int pitch, int width, int height) {
- _menuFrameCounter++;
- auto makeTalkText = [](const char *text) {
- Common::String out("<");
- out += text;
- return out;
- };
- auto getTalkTextWidth = [&](const char *text) {
- Common::String styled = makeTalkText(text);
- return getFontBankStringWidth(styled.c_str());
- };
- auto drawTalkText = [&](int x, int y, const char *text) {
- Common::String styled = makeTalkText(text);
- drawFontBankString(dst, pitch, width, height, x, y, styled.c_str());
- };
- auto getTitleTextWidth = [&](const char *text) {
- return getFontBankStringWidth(text);
- };
- auto drawTitleText = [&](int x, int y, const char *text) {
- drawFontBankString(dst, pitch, width, height, x, y, text);
- };
+int InsaneRebel1::getMenuTalkTextWidth(const char *text) {
+ Common::String styled("<");
+ styled += text;
+ return getFontBankStringWidth(styled.c_str());
+}
- if (_textEntryActive) {
- renderTextEntryOverlay(dst, pitch, width, height);
- return;
- }
+void InsaneRebel1::drawMenuTalkText(byte *dst, int pitch, int width, int height,
+ int x, int y, const char *text) {
+ Common::String styled("<");
+ styled += text;
+ drawFontBankString(dst, pitch, width, height, x, y, styled.c_str());
+}
- if (_highScoresActive) {
- // --- TOP PILOTS high score display ---
- // Original renders over O1SCORE.ANM. Title appears after frame 20,
- // entries fade in one per frame. We show all immediately.
- const int titleW = getTalkTextWidth("TOP PILOTS");
- drawTalkText(getRebel1MenuCenteredX(titleW), 10, "TOP PILOTS");
-
- for (int i = 0; i < kHighScoreCount; i++) {
- const int y = 25 + i * 14;
- // Name (left side)
- drawFontBankString(dst, pitch, width, height, 40, y, _highScores[i].name);
- // Score + difficulty glyph (right side) â original format "<%ld %c"
- // Difficulty byte 0/1/2 + 0x7B = '{','|','}' tech font glyphs (easy/normal/hard)
- char scoreLine[32];
- Common::sprintf_s(scoreLine, "<%ld %c",
- (long)_highScores[i].score,
- (char)(_highScores[i].difficulty + 0x7B));
- drawFontBankString(dst, pitch, width, height, 220, y, scoreLine);
- }
- return;
+void InsaneRebel1::drawMenuTitleText(byte *dst, int pitch, int width, int height,
+ int x, int y, const char *text) {
+ drawFontBankString(dst, pitch, width, height, x, y, text);
+}
+
+void InsaneRebel1::renderHighScoresOverlay(byte *dst, int pitch, int width, int height) {
+ // --- TOP PILOTS high score display ---
+ // Original renders over O1SCORE.ANM. Title appears after frame 20,
+ // entries fade in one per frame. We show all immediately.
+ const int titleW = getMenuTalkTextWidth("TOP PILOTS");
+ drawMenuTalkText(dst, pitch, width, height, getRebel1MenuCenteredX(titleW), 10, "TOP PILOTS");
+
+ for (int i = 0; i < kHighScoreCount; i++) {
+ const int y = 25 + i * 14;
+ // Name (left side)
+ drawFontBankString(dst, pitch, width, height, 40, y, _highScores[i].name);
+ // Score + difficulty glyph (right side) â original format "<%ld %c"
+ // Difficulty byte 0/1/2 + 0x7B = '{','|','}' tech font glyphs (easy/normal/hard)
+ char scoreLine[32];
+ Common::sprintf_s(scoreLine, "<%ld %c",
+ (long)_highScores[i].score,
+ (char)(_highScores[i].difficulty + 0x7B));
+ drawFontBankString(dst, pitch, width, height, 220, y, scoreLine);
}
+}
- if (_optionsActive) {
- // --- Options submenu (matching original RunGameOptionsMenu) ---
- const char *kDiffNames[3] = { "EASY", "NORMAL", "HARD" };
-
- const int titleW = getTitleTextWidth("GAME OPTIONS");
- drawTitleText(getRebel1MenuCenteredX(titleW), 15, "GAME OPTIONS");
-
- // Build dynamic option strings for each row
- char diffLine[64], volLine[64];
- Common::sprintf_s(diffLine, "DIFFICULTY IS %s", kDiffNames[CLIP(_difficulty, 0, 2)]);
- Common::sprintf_s(volLine, "VOLUME AT %d PERCENT", (_optVolume * 100) / 127);
-
- const char *optItems[kOptionsItemCount] = {
- "EXIT MENU",
- _optRookieOneFemale ? "ROOKIE1 IS FEMALE" : "ROOKIE1 IS MALE",
- _optMusicEnabled ? "MUSIC IS ON" : "MUSIC IS OFF",
- _optSfxEnabled ? "SFX AND VOICE ARE ON" : "SFX AND VOICE ARE OFF",
- _optTextEnabled ? "DIALOGUE TEXT IS ON" : "DIALOGUE TEXT IS OFF",
- _optEnhancedControls ? "INPUT STYLE ENHANCED" : "INPUT STYLE ORIGINAL",
- _optControlsYFlip ? "Y AXIS IS INVERTED" : "Y AXIS IS NORMAL",
- volLine,
- diffLine
- };
-
- for (int i = 0; i < kOptionsItemCount; i++) {
- const int textW = getTalkTextWidth(optItems[i]);
- const int textX = getRebel1MenuCenteredX(textW);
- const int y = 0x2d + i * kRA1MenuRowH;
- drawTalkText(textX, y, optItems[i]);
-
- if (i == _optionsSel)
- drawRebel1MenuFrame(dst, pitch, width, height,
- kRA1MenuFrameX, (i + 1) * kRA1MenuRowH + 0x1d, kRA1MenuFrameW);
- }
- return;
+void InsaneRebel1::renderOptionsOverlay(byte *dst, int pitch, int width, int height) {
+ // --- Options submenu (matching original RunGameOptionsMenu) ---
+ const char *kDiffNames[3] = { "EASY", "NORMAL", "HARD" };
+
+ const int titleW = getFontBankStringWidth("GAME OPTIONS");
+ drawMenuTitleText(dst, pitch, width, height, getRebel1MenuCenteredX(titleW), 15, "GAME OPTIONS");
+
+ // Build dynamic option strings for each row
+ char diffLine[64], volLine[64];
+ Common::sprintf_s(diffLine, "DIFFICULTY IS %s", kDiffNames[CLIP(_difficulty, 0, 2)]);
+ Common::sprintf_s(volLine, "VOLUME AT %d PERCENT", (_optVolume * 100) / 127);
+
+ const char *optItems[kOptionsItemCount] = {
+ "EXIT MENU",
+ _optRookieOneFemale ? "ROOKIE1 IS FEMALE" : "ROOKIE1 IS MALE",
+ _optMusicEnabled ? "MUSIC IS ON" : "MUSIC IS OFF",
+ _optSfxEnabled ? "SFX AND VOICE ARE ON" : "SFX AND VOICE ARE OFF",
+ _optTextEnabled ? "DIALOGUE TEXT IS ON" : "DIALOGUE TEXT IS OFF",
+ _optEnhancedControls ? "INPUT STYLE ENHANCED" : "INPUT STYLE ORIGINAL",
+ _optControlsYFlip ? "Y AXIS IS INVERTED" : "Y AXIS IS NORMAL",
+ volLine,
+ diffLine
+ };
+
+ for (int i = 0; i < kOptionsItemCount; i++) {
+ const int textW = getMenuTalkTextWidth(optItems[i]);
+ const int textX = getRebel1MenuCenteredX(textW);
+ const int y = 0x2d + i * kRA1MenuRowH;
+ drawMenuTalkText(dst, pitch, width, height, textX, y, optItems[i]);
+
+ if (i == _optionsSel)
+ drawRebel1MenuFrame(dst, pitch, width, height,
+ kRA1MenuFrameX, (i + 1) * kRA1MenuRowH + 0x1d, kRA1MenuFrameW);
}
+}
- if (_levelSelectActive) {
- // --- ScummVM level select submenu, styled like the original frontend menus ---
- const int titleW = getTitleTextWidth("LEVEL SELECT");
- drawTitleText(getRebel1MenuCenteredX(titleW), 15, "LEVEL SELECT");
-
- const char *kLevelItems[kRA1LevelSelectItemCount] = {
- " 1 TRAINING",
- " 2 ASTEROIDS",
- " 3 KOLAADOR",
- " 4 STAR DESTR",
- " 5 TATOOINE",
- " 6 AST CHASE",
- " 7 PROBES",
- " 8 WALKERS",
- " 9 TROOPERS",
- "10 TRANSPORT",
- "11 YAVIN",
- "12 TIE ATK",
- "13 DS SURFACE",
- "14 CANNON",
- "15 DS TRENCH",
- "BACK"
- };
-
- const int menuY = 0x2d;
- const int leftFrameX = 20;
- const int rightFrameX = 170;
- const int columnW = 130;
-
- for (int i = 0; i < kRA1LevelSelectItemCount; i++) {
- const int col = i / kRA1LevelSelectRowsPerCol;
- const int row = i % kRA1LevelSelectRowsPerCol;
- const int frameX = (col == 0) ? leftFrameX : rightFrameX;
- const int y = menuY + row * kRA1MenuRowH;
- const int textW = getTalkTextWidth(kLevelItems[i]);
- const int textX = frameX + (columnW - textW) / 2;
-
- drawTalkText(textX, y, kLevelItems[i]);
-
- if (i == _levelSelectSel)
- drawRebel1MenuFrame(dst, pitch, width, height,
- frameX, row * kRA1MenuRowH + 0x2c, columnW);
- }
- return;
+void InsaneRebel1::renderLevelSelectOverlay(byte *dst, int pitch, int width, int height) {
+ // --- ScummVM level select submenu, styled like the original frontend menus ---
+ const int titleW = getFontBankStringWidth("LEVEL SELECT");
+ drawMenuTitleText(dst, pitch, width, height, getRebel1MenuCenteredX(titleW), 15, "LEVEL SELECT");
+
+ const char *kLevelItems[kRA1LevelSelectItemCount] = {
+ " 1 TRAINING",
+ " 2 ASTEROIDS",
+ " 3 KOLAADOR",
+ " 4 STAR DESTR",
+ " 5 TATOOINE",
+ " 6 AST CHASE",
+ " 7 PROBES",
+ " 8 WALKERS",
+ " 9 TROOPERS",
+ "10 TRANSPORT",
+ "11 YAVIN",
+ "12 TIE ATK",
+ "13 DS SURFACE",
+ "14 CANNON",
+ "15 DS TRENCH",
+ "BACK"
+ };
+
+ const int menuY = 0x2d;
+ const int leftFrameX = 20;
+ const int rightFrameX = 170;
+ const int columnW = 130;
+
+ for (int i = 0; i < kRA1LevelSelectItemCount; i++) {
+ const int col = i / kRA1LevelSelectRowsPerCol;
+ const int row = i % kRA1LevelSelectRowsPerCol;
+ const int frameX = (col == 0) ? leftFrameX : rightFrameX;
+ const int y = menuY + row * kRA1MenuRowH;
+ const int textW = getMenuTalkTextWidth(kLevelItems[i]);
+ const int textX = frameX + (columnW - textW) / 2;
+
+ drawMenuTalkText(dst, pitch, width, height, textX, y, kLevelItems[i]);
+
+ if (i == _levelSelectSel)
+ drawRebel1MenuFrame(dst, pitch, width, height,
+ frameX, row * kRA1MenuRowH + 0x2c, columnW);
}
+}
+void InsaneRebel1::renderMainMenuItems(byte *dst, int pitch, int width, int height) {
// --- Main menu ---
const char *kMenuItems[kRA1MainMenuItemCount] = {
"START NEW GAME",
@@ -874,17 +836,17 @@ void InsaneRebel1::renderMainMenuOverlay(byte *dst, int pitch, int width, int he
};
// Center title
- const int titleW = getTitleTextWidth("MAIN MENU");
+ const int titleW = getFontBankStringWidth("MAIN MENU");
const int titleX = getRebel1MenuCenteredX(titleW);
- drawTitleText(titleX, 30, "MAIN MENU");
+ drawMenuTitleText(dst, pitch, width, height, titleX, 30, "MAIN MENU");
// Draw menu items centered horizontally
for (int i = 0; i < kRA1MainMenuItemCount; i++) {
- const int textW = getTalkTextWidth(kMenuItems[i]);
+ const int textW = getMenuTalkTextWidth(kMenuItems[i]);
const int textX = getRebel1MenuCenteredX(textW);
const int y = 0x3c + i * kRA1MenuRowH;
- drawTalkText(textX, y, kMenuItems[i]);
+ drawMenuTalkText(dst, pitch, width, height, textX, y, kMenuItems[i]);
if (i == _menuSelection)
drawRebel1MenuFrame(dst, pitch, width, height,
@@ -892,17 +854,36 @@ void InsaneRebel1::renderMainMenuOverlay(byte *dst, int pitch, int width, int he
}
}
+void InsaneRebel1::renderMainMenuOverlay(byte *dst, int pitch, int width, int height) {
+ _menuFrameCounter++;
+
+ if (_textEntryActive) {
+ renderTextEntryOverlay(dst, pitch, width, height);
+ return;
+ }
+
+ if (_highScoresActive) {
+ renderHighScoresOverlay(dst, pitch, width, height);
+ return;
+ }
+
+ if (_optionsActive) {
+ renderOptionsOverlay(dst, pitch, width, height);
+ return;
+ }
+
+ if (_levelSelectActive) {
+ renderLevelSelectOverlay(dst, pitch, width, height);
+ return;
+ }
+
+ renderMainMenuItems(dst, pitch, width, height);
+}
+
void InsaneRebel1::renderTextEntryOverlay(byte *dst, int pitch, int width, int height) {
- auto makeTalkText = [](const char *text) {
- Common::String out("<");
- out += text;
- return out;
- };
auto drawCenteredTalkText = [&](int y, const char *text) {
- Common::String styled = makeTalkText(text);
- const int textW = getFontBankStringWidth(styled.c_str());
- drawFontBankString(dst, pitch, width, height,
- getRebel1MenuCenteredX(textW), y, styled.c_str());
+ const int textW = getMenuTalkTextWidth(text);
+ drawMenuTalkText(dst, pitch, width, height, getRebel1MenuCenteredX(textW), y, text);
};
auto drawCenteredRawChar = [&](int centerX, int y, char ch) {
char text[2] = { ch, '\0' };
@@ -931,17 +912,28 @@ void InsaneRebel1::renderTextEntryOverlay(byte *dst, int pitch, int width, int h
_textEntryPickerOffsetX = 0;
}
+void InsaneRebel1::playMenuBackground() {
+ _menuActive = true;
+ _menuConfirmed = false;
+ _menuFrameCounter = 0;
+ clearVideoBuffer();
+ playCinematic("OPEN/O1OPTION.ANM");
+ _menuActive = false;
+}
+
+bool InsaneRebel1::runTextEntryMenuLoop() {
+ while (!_vm->shouldQuit() && !_textEntryDone && !_textEntryCanceled)
+ playMenuBackground();
+
+ return !_vm->shouldQuit() && !_textEntryCanceled;
+}
+
int InsaneRebel1::runMainMenu() {
debug(1, "InsaneRebel1: Main menu");
_menuSelection = 0;
while (!_vm->shouldQuit()) {
- _menuActive = true;
- _menuConfirmed = false;
- _menuFrameCounter = 0;
- clearVideoBuffer();
- playCinematic("OPEN/O1OPTION.ANM");
- _menuActive = false;
+ playMenuBackground();
if (_vm->shouldQuit())
return kRA1MainMenuItemCount;
@@ -956,16 +948,7 @@ int InsaneRebel1::runMainMenu() {
int InsaneRebel1::runPasscodeEntryDialog() {
beginTextEntry(true);
- while (!_vm->shouldQuit() && !_textEntryDone && !_textEntryCanceled) {
- _menuActive = true;
- _menuConfirmed = false;
- _menuFrameCounter = 0;
- clearVideoBuffer();
- playCinematic("OPEN/O1OPTION.ANM");
- _menuActive = false;
- }
-
- if (_vm->shouldQuit() || _textEntryCanceled)
+ if (!runTextEntryMenuLoop())
return 0;
for (int i = 1; i <= kRA1NumLevels; i++) {
@@ -1005,14 +988,7 @@ bool InsaneRebel1::runHighScoreNameEntry() {
_highScoreEntryIndex = slot;
beginTextEntry(false);
- while (!_vm->shouldQuit() && !_textEntryDone && !_textEntryCanceled) {
- _menuActive = true;
- _menuConfirmed = false;
- _menuFrameCounter = 0;
- clearVideoBuffer();
- playCinematic("OPEN/O1OPTION.ANM");
- _menuActive = false;
- }
+ runTextEntryMenuLoop();
Common::String storedName("<");
storedName += _textEntryBuffer;
@@ -1029,12 +1005,7 @@ void InsaneRebel1::runOptionsMenu() {
_optionsActive = true;
while (!_vm->shouldQuit()) {
- _menuActive = true;
- _menuConfirmed = false;
- _menuFrameCounter = 0;
- clearVideoBuffer();
- playCinematic("OPEN/O1OPTION.ANM");
- _menuActive = false;
+ playMenuBackground();
if (_vm->shouldQuit())
break;
@@ -1083,12 +1054,7 @@ int InsaneRebel1::runLevelSelectMenu() {
_levelSelectActive = true;
while (!_vm->shouldQuit()) {
- _menuActive = true;
- _menuConfirmed = false;
- _menuFrameCounter = 0;
- clearVideoBuffer();
- playCinematic("OPEN/O1OPTION.ANM");
- _menuActive = false;
+ playMenuBackground();
if (_vm->shouldQuit())
break;
diff --git a/engines/scumm/insane/rebel1/rebel.h b/engines/scumm/insane/rebel1/rebel.h
index 4c7baa881d4..fde8876fd59 100644
--- a/engines/scumm/insane/rebel1/rebel.h
+++ b/engines/scumm/insane/rebel1/rebel.h
@@ -33,6 +33,22 @@ namespace Scumm {
class ScummEngine_v7;
class SmushFont;
+enum RA1MenuCommand {
+ kRA1MenuCommandNone = 0,
+ kRA1MenuCommandUp,
+ kRA1MenuCommandDown,
+ kRA1MenuCommandLeft,
+ kRA1MenuCommandRight,
+ kRA1MenuCommandAccept,
+ kRA1MenuCommandCancel,
+ kRA1MenuCommandSelect1,
+ kRA1MenuCommandSelect2,
+ kRA1MenuCommandSelect3,
+ kRA1MenuCommandSelect4,
+ kRA1MenuCommandSelect5,
+ kRA1MenuCommandSelect6
+};
+
// Simple sprite bank for RA1 NUT files (ANIM v1 with odd-alignment padding).
// Separate from NutRenderer to avoid modifying shared NUT parsing code.
struct RA1Sprite {
@@ -484,10 +500,13 @@ private:
void runOptionsMenu();
int runPasscodeEntryDialog();
bool runHighScoreNameEntry();
+ bool handleMenuCommand(RA1MenuCommand command);
bool handleControllerMenuAction(ScummAction action);
bool handleControllerMenuAxis(int16 oldAxisX, int16 oldAxisY);
bool handleTextEntryAction(ScummAction action);
bool handleTextEntryKey(const Common::Event &event);
+ void playMenuBackground();
+ bool runTextEntryMenuLoop();
void beginTextEntry(bool passcodeMode);
void finishTextEntry(bool canceled);
void selectTextEntryChar();
@@ -524,6 +543,13 @@ private:
HighScoreEntry _highScores[kHighScoreCount];
bool _highScoresActive; // True when showing TOP PILOTS overlay
void showHighScores();
+ int getMenuTalkTextWidth(const char *text);
+ void drawMenuTalkText(byte *dst, int pitch, int width, int height, int x, int y, const char *text);
+ void drawMenuTitleText(byte *dst, int pitch, int width, int height, int x, int y, const char *text);
+ void renderHighScoresOverlay(byte *dst, int pitch, int width, int height);
+ void renderOptionsOverlay(byte *dst, int pitch, int width, int height);
+ void renderLevelSelectOverlay(byte *dst, int pitch, int width, int height);
+ void renderMainMenuItems(byte *dst, int pitch, int width, int height);
// Character picker shared by RunPasscodeEntryDialog and RunHighScoreNameEntry.
static const int kTextEntryBufferSize = 20;
Commit: 9abca5775a71d67b7c12b4822bee474702d547e0
https://github.com/scummvm/scummvm/commit/9abca5775a71d67b7c12b4822bee474702d547e0
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:10:13+02:00
Commit Message:
SCUMM: RA: Extract common audio helpers
Changed paths:
A engines/scumm/insane/rebel/rebel_audio.cpp
A engines/scumm/insane/rebel/rebel_audio.h
A engines/scumm/smush/rebel/smush_player_rebel.cpp
engines/scumm/insane/rebel1/audio.cpp
engines/scumm/insane/rebel1/rebel.h
engines/scumm/insane/rebel1/runlevels.cpp
engines/scumm/insane/rebel2/audio.cpp
engines/scumm/insane/rebel2/rebel.cpp
engines/scumm/insane/rebel2/rebel.h
engines/scumm/module.mk
engines/scumm/smush/rebel/smush_player_ra2.cpp
engines/scumm/smush/smush_player.h
diff --git a/engines/scumm/insane/rebel/rebel_audio.cpp b/engines/scumm/insane/rebel/rebel_audio.cpp
new file mode 100644
index 00000000000..b06a58fd884
--- /dev/null
+++ b/engines/scumm/insane/rebel/rebel_audio.cpp
@@ -0,0 +1,209 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include "audio/audiostream.h"
+#include "audio/decoders/raw.h"
+#include "audio/mixer.h"
+
+#include "scumm/scumm_v7.h"
+#include "scumm/smush/smush_player.h"
+#include "scumm/insane/rebel/rebel_audio.h"
+
+namespace Scumm {
+
+RebelAudio::RebelAudio() : _vm(nullptr), _sampleRate(11025) {
+ for (int i = 0; i < kMaxTracks; i++) {
+ _streams[i] = nullptr;
+ _trackActive[i] = false;
+ }
+}
+
+void RebelAudio::init(ScummEngine_v7 *vm, int sampleRate) {
+ _vm = vm;
+ _sampleRate = sampleRate;
+ for (int i = 0; i < kMaxTracks; i++) {
+ _streams[i] = nullptr;
+ _trackActive[i] = false;
+ }
+}
+
+void RebelAudio::terminate() {
+ if (!_vm)
+ return;
+
+ for (int i = 0; i < kMaxTracks; i++) {
+ if (_trackActive[i]) {
+ _vm->_mixer->stopHandle(_handles[i]);
+ _trackActive[i] = false;
+ }
+ if (_streams[i]) {
+ _streams[i]->finish();
+ _streams[i] = nullptr;
+ }
+ }
+}
+
+void RebelAudio::queueData(int trackIdx, const uint8 *data, int32 size, int volume, int pan) {
+ if (!_vm || trackIdx < 0 || trackIdx >= kMaxTracks || size <= 0 || !data)
+ return;
+
+ if (!_streams[trackIdx]) {
+ debug(1, "RebelAudio: Creating audio stream for track %d at %d Hz", trackIdx, _sampleRate);
+ _streams[trackIdx] = Audio::makeQueuingAudioStream(_sampleRate, false);
+ _trackActive[trackIdx] = true;
+ _vm->_mixer->playStream(Audio::Mixer::kSFXSoundType, &_handles[trackIdx],
+ _streams[trackIdx], -1, Audio::Mixer::kMaxChannelVolume, 0,
+ DisposeAfterUse::NO);
+ }
+
+ byte *audioCopy = (byte *)malloc(size);
+ if (!audioCopy)
+ return;
+ memcpy(audioCopy, data, size);
+
+ _streams[trackIdx]->queueBuffer(audioCopy, size, DisposeAfterUse::YES, Audio::FLAG_UNSIGNED);
+
+ const int scaledVolume = (volume * Audio::Mixer::kMaxChannelVolume) / 127;
+ const int scaledPan = (pan * 127) / 128;
+ _vm->_mixer->setChannelVolume(_handles[trackIdx], scaledVolume);
+ _vm->_mixer->setChannelBalance(_handles[trackIdx], scaledPan);
+}
+
+void RebelAudio::processFrame(SmushPlayer *player, int16 feedSize) {
+ if (!player)
+ return;
+
+ if (player->_smushTracksNeedInit) {
+ player->_smushTracksNeedInit = false;
+ for (int i = 0; i < SMUSH_MAX_TRACKS; i++) {
+ player->_smushDispatch[i].fadeRemaining = 0;
+ player->_smushDispatch[i].fadeVolume = 0;
+ player->_smushDispatch[i].fadeSampleRate = 0;
+ player->_smushDispatch[i].elapsedAudio = 0;
+ player->_smushDispatch[i].audioLength = 0;
+ }
+ }
+
+ for (int i = 0; i < player->_smushNumTracks; i++) {
+ SmushPlayer::SmushAudioTrack &track = player->_smushTracks[i];
+ SmushPlayer::SmushAudioDispatch &dispatch = player->_smushDispatch[i];
+
+ if (track.state == TRK_STATE_INACTIVE || !track.blockPtr)
+ continue;
+
+ bool isPlayableTrack =
+ ((track.flags & TRK_TYPE_MASK) == IS_SPEECH && player->isChanActive(CHN_SPEECH)) ||
+ ((track.flags & TRK_TYPE_MASK) == IS_BKG_MUSIC && player->isChanActive(CHN_BKGMUS)) ||
+ ((track.flags & TRK_TYPE_MASK) == IS_SFX && player->isChanActive(CHN_OTHER));
+
+ if (!isPlayableTrack)
+ continue;
+
+ int baseVolume;
+ switch (track.flags & TRK_TYPE_MASK) {
+ case IS_SFX:
+ baseVolume = (player->_smushTrackVols[1] * track.volume) >> 7;
+ break;
+ case IS_BKG_MUSIC:
+ baseVolume = (player->_smushTrackVols[3] * track.volume) >> 7;
+ break;
+ case IS_SPEECH:
+ baseVolume = (player->_smushTrackVols[2] * track.volume) >> 7;
+ break;
+ default:
+ baseVolume = track.volume;
+ break;
+ }
+ int mixVolume = baseVolume * player->_smushTrackVols[0] / 127;
+
+ if (track.state == TRK_STATE_FADING) {
+ dispatch.headerPtr = track.dataBuf;
+ dispatch.dataBuf = track.subChunkPtr;
+ dispatch.dataSize = track.dataSize;
+ dispatch.currentOffset = 0;
+ dispatch.audioLength = 0;
+ track.state = TRK_STATE_PLAYING;
+ }
+
+ if (track.state != TRK_STATE_INACTIVE) {
+ int32 tmpFeedSize = feedSize;
+
+ while (tmpFeedSize > 0) {
+ int32 mixInFrameCount = dispatch.currentOffset;
+
+ if (mixInFrameCount > 0 && dispatch.dataBuf && dispatch.dataSize > 0) {
+ if (dispatch.audioRemaining < 0)
+ dispatch.audioRemaining = 0;
+
+ int32 offset = dispatch.audioRemaining % dispatch.dataSize;
+
+ if (dispatch.sampleRate > 0 && player->_smushAudioSampleRate > 0) {
+ int32 maxFrames = dispatch.sampleRate * tmpFeedSize / player->_smushAudioSampleRate;
+ if (mixInFrameCount > maxFrames)
+ mixInFrameCount = maxFrames;
+ }
+
+ if (offset + mixInFrameCount > dispatch.dataSize)
+ mixInFrameCount = dispatch.dataSize - offset;
+
+ if (dispatch.audioRemaining + mixInFrameCount > track.availableSize) {
+ mixInFrameCount = track.availableSize - dispatch.audioRemaining;
+ if (mixInFrameCount <= 0) {
+ track.state = TRK_STATE_ENDING;
+ break;
+ }
+ }
+
+ if (mixInFrameCount > 0) {
+ if (!dispatch.dataBuf || offset < 0 || offset + mixInFrameCount > dispatch.dataSize)
+ break;
+
+ queueData(i, &dispatch.dataBuf[offset], mixInFrameCount, mixVolume, track.pan);
+
+ dispatch.currentOffset -= mixInFrameCount;
+ dispatch.audioRemaining += mixInFrameCount;
+
+ if (dispatch.sampleRate > 0) {
+ int32 consumedFeed = mixInFrameCount * player->_smushAudioSampleRate / dispatch.sampleRate;
+ tmpFeedSize -= consumedFeed;
+ } else {
+ tmpFeedSize -= mixInFrameCount;
+ }
+ }
+ }
+
+ if (dispatch.currentOffset <= 0) {
+ if (!player->processAudioCodes(i, tmpFeedSize, mixVolume))
+ break;
+ if (dispatch.currentOffset <= 0)
+ break;
+ } else if (tmpFeedSize <= 0) {
+ break;
+ }
+ }
+ }
+
+ track.audioRemaining = dispatch.audioRemaining;
+ dispatch.state = track.state;
+ }
+}
+
+} // End of namespace Scumm
diff --git a/engines/scumm/insane/rebel/rebel_audio.h b/engines/scumm/insane/rebel/rebel_audio.h
new file mode 100644
index 00000000000..c20dace1e5e
--- /dev/null
+++ b/engines/scumm/insane/rebel/rebel_audio.h
@@ -0,0 +1,60 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#ifndef SCUMM_INSANE_REBEL_AUDIO_H
+#define SCUMM_INSANE_REBEL_AUDIO_H
+
+#include "audio/mixer.h"
+#include "common/scummsys.h"
+
+namespace Audio {
+class QueuingAudioStream;
+}
+
+namespace Scumm {
+
+class ScummEngine_v7;
+class SmushPlayer;
+
+class RebelAudio {
+public:
+ RebelAudio();
+
+ void init(ScummEngine_v7 *vm, int sampleRate);
+ void terminate();
+ int sampleRate() const { return _sampleRate; }
+
+ void queueData(int trackIdx, const uint8 *data, int32 size, int volume, int pan);
+ void processFrame(SmushPlayer *player, int16 feedSize);
+
+private:
+ static const int kMaxTracks = 4;
+
+ ScummEngine_v7 *_vm;
+ Audio::QueuingAudioStream *_streams[kMaxTracks];
+ Audio::SoundHandle _handles[kMaxTracks];
+ bool _trackActive[kMaxTracks];
+ int _sampleRate;
+};
+
+} // End of namespace Scumm
+
+#endif
diff --git a/engines/scumm/insane/rebel1/audio.cpp b/engines/scumm/insane/rebel1/audio.cpp
index 6c1540aa368..bb807f9df2d 100644
--- a/engines/scumm/insane/rebel1/audio.cpp
+++ b/engines/scumm/insane/rebel1/audio.cpp
@@ -48,24 +48,11 @@ const char *const kRA1SfxFiles[8] = {
// ---------------------------------------------------------------------------
void InsaneRebel1::initAudio(int sampleRate) {
- _audioSampleRate = sampleRate;
- for (int i = 0; i < kMaxAudioTracks; i++) {
- _audioStreams[i] = nullptr;
- _audioTrackActive[i] = false;
- }
+ _audio.init(_vm, sampleRate);
}
void InsaneRebel1::terminateAudio() {
- for (int i = 0; i < kMaxAudioTracks; i++) {
- if (_audioTrackActive[i]) {
- _vm->_mixer->stopHandle(_audioHandles[i]);
- _audioTrackActive[i] = false;
- }
- if (_audioStreams[i]) {
- _audioStreams[i]->finish();
- _audioStreams[i] = nullptr;
- }
- }
+ _audio.terminate();
for (int i = 0; i < kNumSfx; i++) {
_vm->_mixer->stopHandle(_sfxHandles[i]);
@@ -73,150 +60,11 @@ void InsaneRebel1::terminateAudio() {
}
void InsaneRebel1::queueAudioData(int trackIdx, uint8 *data, int32 size, int volume, int pan) {
- if (trackIdx < 0 || trackIdx >= kMaxAudioTracks || size <= 0 || !data)
- return;
-
- if (!_audioStreams[trackIdx]) {
- debug(1, "InsaneRebel1: Creating audio stream for track %d at %d Hz", trackIdx, _audioSampleRate);
- _audioStreams[trackIdx] = Audio::makeQueuingAudioStream(_audioSampleRate, false);
- _audioTrackActive[trackIdx] = true;
- _vm->_mixer->playStream(Audio::Mixer::kSFXSoundType, &_audioHandles[trackIdx],
- _audioStreams[trackIdx], -1, Audio::Mixer::kMaxChannelVolume, 0,
- DisposeAfterUse::NO);
- }
-
- byte *audioCopy = (byte *)malloc(size);
- if (!audioCopy)
- return;
- memcpy(audioCopy, data, size);
-
- _audioStreams[trackIdx]->queueBuffer(audioCopy, size, DisposeAfterUse::YES, Audio::FLAG_UNSIGNED);
-
- int scaledVolume = (volume * Audio::Mixer::kMaxChannelVolume) / 127;
- int scaledPan = (pan * 127) / 128;
- _vm->_mixer->setChannelVolume(_audioHandles[trackIdx], scaledVolume);
- _vm->_mixer->setChannelBalance(_audioHandles[trackIdx], scaledPan);
+ _audio.queueData(trackIdx, data, size, volume, pan);
}
void InsaneRebel1::processAudioFrame(int16 feedSize) {
- if (!_player)
- return;
-
- SmushPlayer *sp = _player;
-
- if (sp->_smushTracksNeedInit) {
- sp->_smushTracksNeedInit = false;
- for (int i = 0; i < SMUSH_MAX_TRACKS; i++) {
- sp->_smushDispatch[i].fadeRemaining = 0;
- sp->_smushDispatch[i].fadeVolume = 0;
- sp->_smushDispatch[i].fadeSampleRate = 0;
- sp->_smushDispatch[i].elapsedAudio = 0;
- sp->_smushDispatch[i].audioLength = 0;
- }
- }
-
- for (int i = 0; i < sp->_smushNumTracks; i++) {
- SmushPlayer::SmushAudioTrack &track = sp->_smushTracks[i];
- SmushPlayer::SmushAudioDispatch &dispatch = sp->_smushDispatch[i];
-
- if (track.state == TRK_STATE_INACTIVE || !track.blockPtr)
- continue;
-
- bool isPlayableTrack = ((track.flags & TRK_TYPE_MASK) == IS_SPEECH && sp->isChanActive(CHN_SPEECH)) ||
- ((track.flags & TRK_TYPE_MASK) == IS_BKG_MUSIC && sp->isChanActive(CHN_BKGMUS)) ||
- ((track.flags & TRK_TYPE_MASK) == IS_SFX && sp->isChanActive(CHN_OTHER));
-
- if (!isPlayableTrack)
- continue;
-
- int baseVolume;
- switch (track.flags & TRK_TYPE_MASK) {
- case IS_SFX:
- baseVolume = (sp->_smushTrackVols[1] * track.volume) >> 7;
- break;
- case IS_BKG_MUSIC:
- baseVolume = (sp->_smushTrackVols[3] * track.volume) >> 7;
- break;
- case IS_SPEECH:
- baseVolume = (sp->_smushTrackVols[2] * track.volume) >> 7;
- break;
- default:
- baseVolume = track.volume;
- break;
- }
- int mixVolume = baseVolume * sp->_smushTrackVols[0] / 127;
-
- // Handle FADING -> PLAYING transition
- if (track.state == TRK_STATE_FADING) {
- dispatch.headerPtr = track.dataBuf;
- dispatch.dataBuf = track.subChunkPtr;
- dispatch.dataSize = track.dataSize;
- dispatch.currentOffset = 0;
- dispatch.audioLength = 0;
- track.state = TRK_STATE_PLAYING;
- }
-
- if (track.state != TRK_STATE_INACTIVE) {
- int32 tmpFeedSize = feedSize;
-
- while (tmpFeedSize > 0) {
- int32 mixInFrameCount = dispatch.currentOffset;
-
- if (mixInFrameCount > 0 && dispatch.dataBuf && dispatch.dataSize > 0) {
- if (dispatch.audioRemaining < 0)
- dispatch.audioRemaining = 0;
-
- int32 offset = dispatch.audioRemaining % dispatch.dataSize;
-
- if (dispatch.sampleRate > 0 && sp->_smushAudioSampleRate > 0) {
- int32 maxFrames = dispatch.sampleRate * tmpFeedSize / sp->_smushAudioSampleRate;
- if (mixInFrameCount > maxFrames)
- mixInFrameCount = maxFrames;
- }
-
- if (offset + mixInFrameCount > dispatch.dataSize)
- mixInFrameCount = dispatch.dataSize - offset;
-
- if (dispatch.audioRemaining + mixInFrameCount > track.availableSize) {
- mixInFrameCount = track.availableSize - dispatch.audioRemaining;
- if (mixInFrameCount <= 0) {
- track.state = TRK_STATE_ENDING;
- break;
- }
- }
-
- if (mixInFrameCount > 0) {
- if (!dispatch.dataBuf || offset < 0 || offset + mixInFrameCount > dispatch.dataSize)
- break;
-
- queueAudioData(i, &dispatch.dataBuf[offset], mixInFrameCount, mixVolume, track.pan);
-
- dispatch.currentOffset -= mixInFrameCount;
- dispatch.audioRemaining += mixInFrameCount;
-
- if (dispatch.sampleRate > 0) {
- int32 consumedFeed = mixInFrameCount * sp->_smushAudioSampleRate / dispatch.sampleRate;
- tmpFeedSize -= consumedFeed;
- } else {
- tmpFeedSize -= mixInFrameCount;
- }
- }
- }
-
- if (dispatch.currentOffset <= 0) {
- if (!sp->processAudioCodes(i, tmpFeedSize, mixVolume))
- break;
- if (dispatch.currentOffset <= 0)
- break;
- } else if (tmpFeedSize <= 0) {
- break;
- }
- }
- }
-
- track.audioRemaining = dispatch.audioRemaining;
- dispatch.state = track.state;
- }
+ _audio.processFrame(_player, feedSize);
}
void InsaneRebel1::loadSfx() {
diff --git a/engines/scumm/insane/rebel1/rebel.h b/engines/scumm/insane/rebel1/rebel.h
index fde8876fd59..d358202f8ef 100644
--- a/engines/scumm/insane/rebel1/rebel.h
+++ b/engines/scumm/insane/rebel1/rebel.h
@@ -26,6 +26,7 @@
#include "audio/mixer.h"
#include "common/events.h"
#include "scumm/insane/insane.h"
+#include "scumm/insane/rebel/rebel_audio.h"
#include "scumm/smush/rebel/smush_player_ra1.h"
namespace Scumm {
@@ -452,12 +453,8 @@ private:
static const int16 kDeathTimerInit = 30;
static const int16 kDamageCooldownInit = 10;
- // Audio state (same structure as RA2)
- static const int kMaxAudioTracks = 4;
- Audio::QueuingAudioStream *_audioStreams[kMaxAudioTracks];
- Audio::SoundHandle _audioHandles[kMaxAudioTracks];
- bool _audioTrackActive[kMaxAudioTracks];
- int _audioSampleRate;
+ // Streamed SMUSH audio
+ RebelAudio _audio;
static const int kNumSfx = 8;
enum SfxSlot {
kSfxLaserShot = 0,
diff --git a/engines/scumm/insane/rebel1/runlevels.cpp b/engines/scumm/insane/rebel1/runlevels.cpp
index 2e6334daca2..6ed8cbf89fb 100644
--- a/engines/scumm/insane/rebel1/runlevels.cpp
+++ b/engines/scumm/insane/rebel1/runlevels.cpp
@@ -1557,8 +1557,9 @@ void InsaneRebel1::runGame() {
}
void InsaneRebel1::resetInteractiveVideoAudio() {
+ const int sampleRate = _audio.sampleRate();
terminateAudio();
- initAudio(_audioSampleRate);
+ initAudio(sampleRate);
}
void InsaneRebel1::setupInteractiveVideoState(int32 startFrame) {
diff --git a/engines/scumm/insane/rebel2/audio.cpp b/engines/scumm/insane/rebel2/audio.cpp
index 14ace0f131a..cddd81a69c0 100644
--- a/engines/scumm/insane/rebel2/audio.cpp
+++ b/engines/scumm/insane/rebel2/audio.cpp
@@ -40,65 +40,18 @@ namespace Scumm {
// initAudio -- Initialize audio system for RA2.
void InsaneRebel2::initAudio(int sampleRate) {
- _audioSampleRate = sampleRate;
- for (int i = 0; i < kRA2MaxAudioTracks; i++) {
- _audioStreams[i] = nullptr;
- _audioTrackActive[i] = false;
- }
+ _audio.init(_vm, sampleRate);
}
// terminateAudio -- Stop all tracks and release audio streams.
void InsaneRebel2::terminateAudio() {
- for (int i = 0; i < kRA2MaxAudioTracks; i++) {
- if (_audioTrackActive[i]) {
- _vm->_mixer->stopHandle(_audioHandles[i]);
- _audioTrackActive[i] = false;
- }
- if (_audioStreams[i]) {
- _audioStreams[i]->finish();
- _audioStreams[i] = nullptr;
- }
- }
+ _audio.terminate();
}
// queueAudioData -- Queue raw PCM data for playback on a track.
// Creates the queuing stream on first use. RA2 audio is 8-bit unsigned mono.
void InsaneRebel2::queueAudioData(int trackIdx, uint8 *data, int32 size, int volume, int pan) {
- if (trackIdx < 0 || trackIdx >= kRA2MaxAudioTracks || size <= 0 || !data) {
- debug(5, "InsaneRebel2::queueAudioData: Invalid params trackIdx=%d size=%d data=%p", trackIdx, size, (void*)data);
- return;
- }
-
- debug(5, "InsaneRebel2::queueAudioData: trackIdx=%d size=%d volume=%d pan=%d", trackIdx, size, volume, pan);
-
- // Create audio stream if not already active
- if (!_audioStreams[trackIdx]) {
- // RA2 audio is 8-bit unsigned mono at the track's sample rate
- debug("InsaneRebel2: Creating audio stream for track %d at %d Hz", trackIdx, _audioSampleRate);
- _audioStreams[trackIdx] = Audio::makeQueuingAudioStream(_audioSampleRate, false);
- _audioTrackActive[trackIdx] = true;
- _vm->_mixer->playStream(Audio::Mixer::kSFXSoundType, &_audioHandles[trackIdx],
- _audioStreams[trackIdx], -1, Audio::Mixer::kMaxChannelVolume, 0,
- DisposeAfterUse::NO);
- }
-
- debug(6, "InsaneRebel2: Queueing %d bytes to track %d (vol=%d)", size, trackIdx, volume);
-
- // Copy the audio data since queueBuffer may need to own it
- byte *audioCopy = (byte *)malloc(size);
- if (!audioCopy) {
- return;
- }
- memcpy(audioCopy, data, size);
-
- // Queue the audio data - RA2 SMUSH audio is 8-bit unsigned mono
- _audioStreams[trackIdx]->queueBuffer(audioCopy, size, DisposeAfterUse::YES, Audio::FLAG_UNSIGNED);
-
- // Apply volume and pan to the channel
- int scaledVolume = (volume * Audio::Mixer::kMaxChannelVolume) / 127;
- int scaledPan = (pan * 127) / 128; // Convert -128..127 to -127..127
- _vm->_mixer->setChannelVolume(_audioHandles[trackIdx], scaledVolume);
- _vm->_mixer->setChannelBalance(_audioHandles[trackIdx], scaledPan);
+ _audio.queueData(trackIdx, data, size, volume, pan);
}
//
@@ -109,164 +62,7 @@ void InsaneRebel2::queueAudioData(int trackIdx, uint8 *data, int32 size, int vol
// iMUSE is null.
//
void InsaneRebel2::processAudioFrame(int16 feedSize) {
- if (!_player) {
- return;
- }
-
- // Initialize dispatch data if needed (normally done in processDispatches for iMUSE games)
- if (_player->_smushTracksNeedInit) {
- _player->_smushTracksNeedInit = false;
- for (int i = 0; i < SMUSH_MAX_TRACKS; i++) {
- _player->_smushDispatch[i].fadeRemaining = 0;
- _player->_smushDispatch[i].fadeVolume = 0;
- _player->_smushDispatch[i].fadeSampleRate = 0;
- _player->_smushDispatch[i].elapsedAudio = 0;
- _player->_smushDispatch[i].audioLength = 0;
- }
- }
-
- // Access SmushPlayer's audio track data (InsaneRebel2 is a friend class)
- // Only iterate over actually allocated tracks (not SMUSH_MAX_TRACKS)
- for (int i = 0; i < _player->_smushNumTracks; i++) {
- SmushPlayer::SmushAudioTrack &track = _player->_smushTracks[i];
- SmushPlayer::SmushAudioDispatch &dispatch = _player->_smushDispatch[i];
-
- if (track.state == TRK_STATE_INACTIVE) {
- continue;
- }
-
- // Skip tracks that don't have valid buffer pointers yet
- // Note: dispatch.dataBuf is set when transitioning from FADING to PLAYING,
- // so tracks in FADING state won't have it set yet - that's OK, they'll be
- // transitioned below and then processed
- if (!track.blockPtr) {
- debug(5, "InsaneRebel2: Skipping track %d - blockPtr=%p state=%d",
- i, (void*)track.blockPtr, track.state);
- continue;
- }
-
- // Check if this track type should be played
- bool isPlayableTrack = ((track.flags & TRK_TYPE_MASK) == IS_SPEECH && _player->isChanActive(CHN_SPEECH)) ||
- ((track.flags & TRK_TYPE_MASK) == IS_BKG_MUSIC && _player->isChanActive(CHN_BKGMUS)) ||
- ((track.flags & TRK_TYPE_MASK) == IS_SFX && _player->isChanActive(CHN_OTHER));
-
- if (!isPlayableTrack) {
- continue;
- }
-
- // Calculate base volume for this track type
- int baseVolume;
- switch (track.flags & TRK_TYPE_MASK) {
- case IS_SFX:
- baseVolume = (_player->_smushTrackVols[1] * track.volume) >> 7;
- break;
- case IS_BKG_MUSIC:
- baseVolume = (_player->_smushTrackVols[3] * track.volume) >> 7;
- break;
- case IS_SPEECH:
- baseVolume = (_player->_smushTrackVols[2] * track.volume) >> 7;
- break;
- default:
- baseVolume = track.volume;
- break;
- }
- int mixVolume = baseVolume * _player->_smushTrackVols[0] / 127;
-
- // Handle track state transitions: FADING -> PLAYING
- if (track.state == TRK_STATE_FADING) {
- dispatch.headerPtr = track.dataBuf;
- dispatch.dataBuf = track.subChunkPtr;
- dispatch.dataSize = track.dataSize;
- dispatch.currentOffset = 0;
- dispatch.audioLength = 0;
- track.state = TRK_STATE_PLAYING;
- }
-
- // Process audio for this track
- if (track.state != TRK_STATE_INACTIVE) {
- int32 tmpFeedSize = feedSize;
-
- while (tmpFeedSize > 0) {
- int32 mixInFrameCount = dispatch.currentOffset;
-
- // Use dispatch.dataBuf and dispatch.dataSize which are set consistently
- // when the track transitions from FADING to PLAYING, and audioRemaining
- // is calculated relative to these values by processAudioCodes
- if (mixInFrameCount > 0 && dispatch.dataBuf && dispatch.dataSize > 0) {
- // Ensure audioRemaining is non-negative for proper circular buffer access
- if (dispatch.audioRemaining < 0) {
- debug(5, "InsaneRebel2: Resetting negative audioRemaining=%d for track %d", dispatch.audioRemaining, i);
- dispatch.audioRemaining = 0;
- }
- int32 offset = dispatch.audioRemaining % dispatch.dataSize;
-
- // Limit to feed size proportional to sample rate
- if (dispatch.sampleRate > 0 && _player->_smushAudioSampleRate > 0) {
- int32 maxFrames = dispatch.sampleRate * tmpFeedSize / _player->_smushAudioSampleRate;
- if (mixInFrameCount > maxFrames) {
- mixInFrameCount = maxFrames;
- }
- }
-
- // Don't read past the buffer
- if (offset + mixInFrameCount > dispatch.dataSize) {
- mixInFrameCount = dispatch.dataSize - offset;
- }
-
- // Make sure we don't exceed available data
- if (dispatch.audioRemaining + mixInFrameCount > track.availableSize) {
- mixInFrameCount = track.availableSize - dispatch.audioRemaining;
- if (mixInFrameCount <= 0) {
- // Track is ending - no more data
- track.state = TRK_STATE_ENDING;
- break;
- }
- }
-
- if (mixInFrameCount > 0) {
- // Safety check: verify the pointer and offset are within bounds
- if (!dispatch.dataBuf || offset < 0 || offset + mixInFrameCount > dispatch.dataSize) {
- debug(1, "InsaneRebel2: Invalid audio buffer access track=%d dataBuf=%p offset=%d mixInFrameCount=%d dataSize=%d",
- i, (void*)dispatch.dataBuf, offset, mixInFrameCount, dispatch.dataSize);
- break;
- }
-
- // Queue audio data directly to our audio streams
- queueAudioData(i, &dispatch.dataBuf[offset], mixInFrameCount, mixVolume, track.pan);
-
- // Update dispatch state
- dispatch.currentOffset -= mixInFrameCount;
- dispatch.audioRemaining += mixInFrameCount;
-
- // Calculate how much feed time was consumed
- if (dispatch.sampleRate > 0) {
- int32 consumedFeed = mixInFrameCount * _player->_smushAudioSampleRate / dispatch.sampleRate;
- tmpFeedSize -= consumedFeed;
- } else {
- tmpFeedSize -= mixInFrameCount;
- }
- }
- }
-
- // If currentOffset is depleted, process audio codes to get more
- if (dispatch.currentOffset <= 0) {
- // processAudioCodes returns true if there's more audio, false if done
- if (!_player->processAudioCodes(i, tmpFeedSize, mixVolume)) {
- break;
- }
- // If still no offset after processing codes, we're done
- if (dispatch.currentOffset <= 0) {
- break;
- }
- } else if (tmpFeedSize <= 0) {
- break;
- }
- }
- }
-
- track.audioRemaining = dispatch.audioRemaining;
- dispatch.state = track.state;
- }
+ _audio.processFrame(_player, feedSize);
}
// ---------------------------------------------------------------------------
diff --git a/engines/scumm/insane/rebel2/rebel.cpp b/engines/scumm/insane/rebel2/rebel.cpp
index 42fe279ad97..ba9e1b5ed92 100644
--- a/engines/scumm/insane/rebel2/rebel.cpp
+++ b/engines/scumm/insane/rebel2/rebel.cpp
@@ -370,11 +370,7 @@ InsaneRebel2::InsaneRebel2(ScummEngine_v7 *scumm) {
_hudOverlay2Nut = nullptr; // DAT_0047fe80 - Secondary HUD overlay
// Initialize audio system for RA2 (since we don't use iMUSE)
- _audioSampleRate = 11025; // RA2 audio is 11025 Hz, not 22050 Hz
- for (i = 0; i < kRA2MaxAudioTracks; i++) {
- _audioStreams[i] = nullptr;
- _audioTrackActive[i] = false;
- }
+ initAudio(11025); // RA2 audio is 11025 Hz, not 22050 Hz
// Initialize and load sound effects (SYSTM/*.SAD files)
for (i = 0; i < kRA2NumSfx; i++) {
diff --git a/engines/scumm/insane/rebel2/rebel.h b/engines/scumm/insane/rebel2/rebel.h
index 8b4d436768d..bf19ea2af37 100644
--- a/engines/scumm/insane/rebel2/rebel.h
+++ b/engines/scumm/insane/rebel2/rebel.h
@@ -27,6 +27,7 @@
#include "scumm/smush/smush_player.h"
#include "scumm/insane/insane.h"
+#include "scumm/insane/rebel/rebel_audio.h"
#include "common/keyboard.h"
#include "common/list.h"
@@ -1222,12 +1223,7 @@ public:
// ---------------------------------------------------------------------------
// RA2 doesn't use iMUSE -- audio is handled directly through the mixer.
- static const int kRA2MaxAudioTracks = 4;
-
- Audio::QueuingAudioStream *_audioStreams[kRA2MaxAudioTracks];
- Audio::SoundHandle _audioHandles[kRA2MaxAudioTracks];
- bool _audioTrackActive[kRA2MaxAudioTracks];
- int _audioSampleRate;
+ RebelAudio _audio;
// Initialize audio system for RA2
void initAudio(int sampleRate);
diff --git a/engines/scumm/module.mk b/engines/scumm/module.mk
index fedb366f729..b77e4a876a9 100644
--- a/engines/scumm/module.mk
+++ b/engines/scumm/module.mk
@@ -133,6 +133,7 @@ MODULE_OBJS += \
insane/insane_enemy.o \
insane/insane_scenes.o \
insane/insane_iact.o \
+ insane/rebel/rebel_audio.o \
insane/rebel1/rebel.o \
insane/rebel1/audio.o \
insane/rebel1/iact.o \
@@ -155,6 +156,7 @@ MODULE_OBJS += \
smush/rebel/codec_ra1.o \
smush/rebel/codec_ra2.o \
smush/rebel/smush_multi_font.o \
+ smush/rebel/smush_player_rebel.o \
smush/rebel/smush_player_ra1.o \
smush/rebel/smush_player_ra2.o
diff --git a/engines/scumm/smush/rebel/smush_player_ra2.cpp b/engines/scumm/smush/rebel/smush_player_ra2.cpp
index 2610822cdee..fe8165bde42 100644
--- a/engines/scumm/smush/rebel/smush_player_ra2.cpp
+++ b/engines/scumm/smush/rebel/smush_player_ra2.cpp
@@ -488,12 +488,6 @@ void SmushPlayerRebel2::handleLoad(int32 subSize, Common::SeekableReadStream &b)
}
}
-void SmushPlayer::ensureMultiFont() {
- if (!_multiFont) {
- _multiFont = new SmushMultiFont(_vm, this, true);
- }
-}
-
/**
* RA2-specific text rendering using SmushMultiFont for inline font switching.
*/
@@ -557,35 +551,6 @@ void SmushPlayerRebel2::ra2SelectFrameBuffer(int width, int height) {
}
}
-/**
- * Apply RA2 FOBJ position offsets and clamp to buffer bounds.
- * Modifies left, top, width, height in place.
- * When srcSkipY is non-null, outputs the number of source rows to skip
- * when top is clipped from negative (for codecs with row-size prefixes).
- */
-void SmushPlayer::adjustFrameCoords(int &left, int &top, int &width, int &height, int pitch, int *srcSkipY) {
- left += _fobjOffsetX;
- top += _fobjOffsetY;
-
- int bufHeight = (_dst == _specialBuffer) ? _height : _vm->_screenHeight;
- if (top < 0) {
- if (srcSkipY)
- *srcSkipY = -top;
- height += top;
- top = 0;
- }
- if (left < 0) {
- width += left;
- left = 0;
- }
- if (top + height > bufHeight) {
- height = bufHeight - top;
- }
- if (left + width > pitch) {
- width = pitch - left;
- }
-}
-
/**
* Dispatch to RA2-specific codec functions.
* Returns true if the codec was handled, false for standard codecs.
@@ -632,37 +597,6 @@ void SmushPlayerRebel2::ra2StoreFobjData(int codec, const byte *data, int32 data
_storeFrame = false;
}
-/**
- * Cache the most recent frame FOBJ for GOST re-rendering.
- */
-void SmushPlayer::rememberLastFobj(int codec, const byte *data, int32 dataSize,
- int left, int top, int width, int height) {
- if (dataSize <= 0) {
- _hasFrameFobjForGost = false;
- return;
- }
-
- byte *newData = (byte *)realloc(_lastFobjData, dataSize);
- if (newData == nullptr) {
- warning("SmushPlayer::rememberLastFobj: Failed to allocate %d bytes", dataSize);
- free(_lastFobjData);
- _lastFobjData = nullptr;
- _lastFobjDataSize = 0;
- _hasFrameFobjForGost = false;
- return;
- }
-
- _lastFobjData = newData;
- memcpy(_lastFobjData, data, dataSize);
- _lastFobjDataSize = dataSize;
- _lastFobjCodec = codec;
- _lastFobjLeft = left;
- _lastFobjTop = top;
- _lastFobjWidth = width;
- _lastFobjHeight = height;
- _hasFrameFobjForGost = true;
-}
-
/**
* RA2 GOST chunk handler.
* Re-renders the most recent frame FOBJ at the supplied ghost position.
@@ -800,33 +734,4 @@ void SmushPlayerRebel2::handleGameProcessAudio(int16 feedSize) {
}
}
-// Masked region management â used by InsaneRebel2
-
-void SmushPlayer::addMaskedRegion(const Common::Rect &rect) {
- for (Common::List<Common::Rect>::iterator it = _maskedRegions.begin(); it != _maskedRegions.end(); ++it) {
- if (*it == rect) {
- return; // Already exists
- }
- }
- _maskedRegions.push_back(rect);
-}
-
-void SmushPlayer::removeMaskedRegion(const Common::Rect &rect) {
- for (Common::List<Common::Rect>::iterator it = _maskedRegions.begin(); it != _maskedRegions.end(); ++it) {
- if (*it == rect) {
- _maskedRegions.erase(it);
- return;
- }
- }
-}
-
-void SmushPlayer::clearMaskedRegions() {
- _maskedRegions.clear();
-}
-
-void SmushPlayer::setScrollOffset(int x, int y) {
- _scrollX = MAX(0, x);
- _scrollY = MAX(0, y);
-}
-
} // End of namespace Scumm
diff --git a/engines/scumm/smush/rebel/smush_player_rebel.cpp b/engines/scumm/smush/rebel/smush_player_rebel.cpp
new file mode 100644
index 00000000000..681c3e283cf
--- /dev/null
+++ b/engines/scumm/smush/rebel/smush_player_rebel.cpp
@@ -0,0 +1,110 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include "common/rect.h"
+
+#include "scumm/scumm_v7.h"
+#include "scumm/smush/rebel/smush_multi_font.h"
+#include "scumm/smush/smush_player.h"
+
+namespace Scumm {
+
+void SmushPlayer::ensureMultiFont() {
+ if (!_multiFont)
+ _multiFont = new SmushMultiFont(_vm, this, true);
+}
+
+void SmushPlayer::addMaskedRegion(const Common::Rect &rect) {
+ for (Common::List<Common::Rect>::iterator it = _maskedRegions.begin(); it != _maskedRegions.end(); ++it) {
+ if (*it == rect)
+ return;
+ }
+ _maskedRegions.push_back(rect);
+}
+
+void SmushPlayer::removeMaskedRegion(const Common::Rect &rect) {
+ for (Common::List<Common::Rect>::iterator it = _maskedRegions.begin(); it != _maskedRegions.end(); ++it) {
+ if (*it == rect) {
+ _maskedRegions.erase(it);
+ return;
+ }
+ }
+}
+
+void SmushPlayer::clearMaskedRegions() {
+ _maskedRegions.clear();
+}
+
+void SmushPlayer::setScrollOffset(int x, int y) {
+ _scrollX = MAX(0, x);
+ _scrollY = MAX(0, y);
+}
+
+void SmushPlayer::adjustFrameCoords(int &left, int &top, int &width, int &height, int pitch, int *srcSkipY) {
+ left += _fobjOffsetX;
+ top += _fobjOffsetY;
+
+ int bufHeight = (_dst == _specialBuffer) ? _height : _vm->_screenHeight;
+ if (top < 0) {
+ if (srcSkipY)
+ *srcSkipY = -top;
+ height += top;
+ top = 0;
+ }
+ if (left < 0) {
+ width += left;
+ left = 0;
+ }
+ if (top + height > bufHeight)
+ height = bufHeight - top;
+ if (left + width > pitch)
+ width = pitch - left;
+}
+
+void SmushPlayer::rememberLastFobj(int codec, const byte *data, int32 dataSize,
+ int left, int top, int width, int height) {
+ if (dataSize <= 0) {
+ _hasFrameFobjForGost = false;
+ return;
+ }
+
+ byte *newData = (byte *)realloc(_lastFobjData, dataSize);
+ if (newData == nullptr) {
+ warning("SmushPlayer::rememberLastFobj: Failed to allocate %d bytes", dataSize);
+ free(_lastFobjData);
+ _lastFobjData = nullptr;
+ _lastFobjDataSize = 0;
+ _hasFrameFobjForGost = false;
+ return;
+ }
+
+ _lastFobjData = newData;
+ memcpy(_lastFobjData, data, dataSize);
+ _lastFobjDataSize = dataSize;
+ _lastFobjCodec = codec;
+ _lastFobjLeft = left;
+ _lastFobjTop = top;
+ _lastFobjWidth = width;
+ _lastFobjHeight = height;
+ _hasFrameFobjForGost = true;
+}
+
+} // End of namespace Scumm
diff --git a/engines/scumm/smush/smush_player.h b/engines/scumm/smush/smush_player.h
index d0bdd39ff8b..4f967932c5a 100644
--- a/engines/scumm/smush/smush_player.h
+++ b/engines/scumm/smush/smush_player.h
@@ -102,6 +102,7 @@ class SmushDeltaBlocksDecoder;
class SmushDeltaGlyphsDecoder;
class IMuseDigital;
class Insane;
+class RebelAudio;
class SmushPlayerRebel1;
class SmushPlayerRebel2;
@@ -109,6 +110,7 @@ class SmushPlayer {
friend class Insane;
friend class InsaneRebel1;
friend class InsaneRebel2;
+ friend class RebelAudio;
friend class SmushPlayerRebel1;
friend class SmushPlayerRebel2;
friend class SmushMultiFont;
Commit: 088825f21788afc474d36f7adec8655ad6ac0b61
https://github.com/scummvm/scummvm/commit/088825f21788afc474d36f7adec8655ad6ac0b61
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:10:13+02:00
Commit Message:
SCUMM: RA1: Extract common iterator code
Changed paths:
A engines/scumm/smush/rebel/anim_ra1.h
engines/scumm/insane/rebel1/levels.cpp
engines/scumm/insane/rebel1/runlevels.cpp
engines/scumm/smush/rebel/smush_player_ra1.cpp
diff --git a/engines/scumm/insane/rebel1/levels.cpp b/engines/scumm/insane/rebel1/levels.cpp
index 705cf871353..f32b598aee8 100644
--- a/engines/scumm/insane/rebel1/levels.cpp
+++ b/engines/scumm/insane/rebel1/levels.cpp
@@ -25,6 +25,7 @@
#include "scumm/scumm_v7.h"
#include "scumm/file.h"
#include "scumm/insane/rebel1/rebel.h"
+#include "scumm/smush/rebel/anim_ra1.h"
#include "scumm/smush/rebel/codec_ra1.h"
namespace Scumm {
@@ -86,45 +87,25 @@ bool InsaneRebel1::loadRA1Nut(const char *filename, RA1SpriteBank &bank) {
// Pass 1: Parse ANIM chunks properly and collect FRME->FOBJ offsets in-order.
uint32 decodedSize = 0;
uint16 foundSprites = 0;
- uint32 chunkOffset = 0;
- while (chunkOffset + 8 <= animSize && foundSprites < expectedSprites) {
- uint32 chunkTag = READ_BE_UINT32(data + chunkOffset);
- uint32 chunkSize = READ_BE_UINT32(data + chunkOffset + 4);
- uint32 chunkDataOffset = chunkOffset + 8;
- uint32 chunkEnd = chunkDataOffset + chunkSize;
- if (chunkEnd > animSize)
- break;
-
- if (chunkTag == MKTAG('F','R','M','E')) {
- uint32 subOffset = chunkDataOffset;
- while (subOffset + 8 <= chunkEnd) {
- uint32 subTag = READ_BE_UINT32(data + subOffset);
- uint32 subSize = READ_BE_UINT32(data + subOffset + 4);
- uint32 subDataOffset = subOffset + 8;
- uint32 subEnd = subDataOffset + subSize;
- if (subEnd > chunkEnd)
- break;
-
- if (subTag == MKTAG('F','O','B','J') && subOffset + 22 <= animSize) {
- uint16 w = READ_LE_UINT16(data + subOffset + 14);
- uint16 h = READ_LE_UINT16(data + subOffset + 16);
+ RA1AnimChunkIterator chunks(data, animSize);
+ RA1AnimChunk chunk;
+ while (chunks.next(chunk) && foundSprites < expectedSprites) {
+ if (chunk.tag == MKTAG('F','R','M','E')) {
+ RA1AnimChunkIterator subChunks(data, (uint32)chunk.dataOffset, (uint32)chunk.endOffset);
+ RA1AnimChunk subChunk;
+ while (subChunks.next(subChunk)) {
+ if (subChunk.tag == MKTAG('F','O','B','J') && subChunk.size >= 14) {
+ uint16 w = READ_LE_UINT16(data + subChunk.dataOffset + 6);
+ uint16 h = READ_LE_UINT16(data + subChunk.dataOffset + 8);
decodedSize += (uint32)w * (uint32)h;
- fobjOffsets[foundSprites] = subOffset;
+ fobjOffsets[foundSprites] = (uint32)subChunk.offset;
break;
}
-
- subOffset = subEnd;
- if (subSize & 1)
- subOffset++;
}
// Always increment for every FRME to preserve char-to-glyph alignment.
// Empty FRMEs (no FOBJ) keep fobjOffsets[i] = 0, decoded as blank sprites.
foundSprites++;
}
-
- chunkOffset = chunkEnd;
- if (chunkSize & 1)
- chunkOffset++;
}
bank.decodedData = (byte *)calloc(decodedSize ? decodedSize : 1, 1);
diff --git a/engines/scumm/insane/rebel1/runlevels.cpp b/engines/scumm/insane/rebel1/runlevels.cpp
index 6ed8cbf89fb..6c1b16854ce 100644
--- a/engines/scumm/insane/rebel1/runlevels.cpp
+++ b/engines/scumm/insane/rebel1/runlevels.cpp
@@ -26,6 +26,7 @@
#include "scumm/file.h"
#include "scumm/scumm_v7.h"
+#include "scumm/smush/rebel/anim_ra1.h"
#include "scumm/smush/smush_player.h"
#include "scumm/insane/rebel1/rebel.h"
@@ -44,26 +45,22 @@ int32 findAnimFrameChunkOffset(ScummEngine_v7 *vm, const char *filename, int32 t
int32 result = -1;
if (file->size() >= 8) {
file->readUint32BE();
- file->readUint32BE();
+ const uint32 animSize = file->readUint32BE();
+ const int64 animEnd = MIN<int64>((int64)file->pos() + animSize, file->size());
int32 frameIndex = 0;
- while (file->pos() + 8 <= file->size()) {
- const int32 chunkOffset = (int32)file->pos();
- const uint32 chunkTag = file->readUint32BE();
- const int32 chunkSize = (int32)file->readUint32BE();
- const int32 nextChunkOffset = chunkOffset + 8 + chunkSize + ((chunkSize & 1) ? 1 : 0);
- if (nextChunkOffset < chunkOffset || nextChunkOffset > file->size())
- break;
-
- if (chunkTag == MKTAG('F', 'R', 'M', 'E')) {
+ RA1AnimStreamChunkIterator chunks(*file, animEnd);
+ RA1AnimChunk chunk;
+ while (chunks.next(chunk)) {
+ if (chunk.tag == MKTAG('F', 'R', 'M', 'E')) {
if (frameIndex == targetFrame) {
- result = chunkOffset;
+ result = (int32)chunk.offset;
break;
}
frameIndex++;
}
- file->seek(nextChunkOffset, SEEK_SET);
+ chunks.skip(chunk);
}
}
@@ -86,40 +83,28 @@ int32 findAnimFrameChunkOffsetByGameCounter(ScummEngine_v7 *vm, const char *file
int32 result = -1;
if (file->size() >= 8) {
file->readUint32BE();
- file->readUint32BE();
+ const uint32 animSize = file->readUint32BE();
+ const int64 animEnd = MIN<int64>((int64)file->pos() + animSize, file->size());
int32 frameIndex = 0;
- while (file->pos() + 8 <= file->size()) {
- const int32 chunkOffset = (int32)file->pos();
- const uint32 chunkTag = file->readUint32BE();
- const int32 chunkSize = (int32)file->readUint32BE();
- const int32 chunkDataOffset = (int32)file->pos();
- const int32 nextChunkOffset = chunkOffset + 8 + chunkSize + ((chunkSize & 1) ? 1 : 0);
- if (nextChunkOffset < chunkOffset || nextChunkOffset > file->size())
- break;
-
- if (chunkTag == MKTAG('F', 'R', 'M', 'E')) {
- const int32 frameEnd = chunkDataOffset + chunkSize;
- while (file->pos() + 8 <= frameEnd) {
- const int32 subChunkOffset = (int32)file->pos();
- const uint32 subChunkTag = file->readUint32BE();
- const int32 subChunkSize = (int32)file->readUint32BE();
- const int32 subChunkDataOffset = (int32)file->pos();
- const int32 nextSubChunkOffset = subChunkDataOffset + subChunkSize + ((subChunkSize & 1) ? 1 : 0);
- if (nextSubChunkOffset < subChunkOffset || nextSubChunkOffset > frameEnd)
- break;
-
- if (subChunkTag == MKTAG('G', 'A', 'M', 'E') && subChunkSize >= 8) {
+ RA1AnimStreamChunkIterator chunks(*file, animEnd);
+ RA1AnimChunk chunk;
+ while (chunks.next(chunk)) {
+ if (chunk.tag == MKTAG('F', 'R', 'M', 'E')) {
+ RA1AnimStreamChunkIterator subChunks(*file, chunk.endOffset);
+ RA1AnimChunk subChunk;
+ while (subChunks.next(subChunk)) {
+ if (subChunk.tag == MKTAG('G', 'A', 'M', 'E') && subChunk.size >= 8) {
const uint32 opcode = file->readUint32BE();
const int32 counter = (int32)file->readUint32BE();
if (opcode == 0x0B && counter >= targetCounter) {
localFrame = frameIndex;
- result = chunkOffset;
+ result = (int32)chunk.offset;
break;
}
}
- file->seek(nextSubChunkOffset, SEEK_SET);
+ subChunks.skip(subChunk);
}
if (result >= 0)
@@ -128,7 +113,7 @@ int32 findAnimFrameChunkOffsetByGameCounter(ScummEngine_v7 *vm, const char *file
frameIndex++;
}
- file->seek(nextChunkOffset, SEEK_SET);
+ chunks.skip(chunk);
}
}
diff --git a/engines/scumm/smush/rebel/anim_ra1.h b/engines/scumm/smush/rebel/anim_ra1.h
new file mode 100644
index 00000000000..fea359322cb
--- /dev/null
+++ b/engines/scumm/smush/rebel/anim_ra1.h
@@ -0,0 +1,177 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#ifndef SCUMM_SMUSH_REBEL_ANIM_RA1_H
+#define SCUMM_SMUSH_REBEL_ANIM_RA1_H
+
+#include "common/endian.h"
+#include "common/stream.h"
+
+namespace Scumm {
+
+struct RA1AnimChunk {
+ uint32 tag;
+ uint32 size;
+ int64 offset;
+ int64 dataOffset;
+ int64 endOffset;
+ int64 nextOffset;
+ const byte *data;
+};
+
+class RA1AnimChunkIterator {
+public:
+ RA1AnimChunkIterator(const byte *data, uint32 size)
+ : _data(data), _offset(0), _endOffset(size) {}
+
+ RA1AnimChunkIterator(const byte *data, uint32 startOffset, uint32 endOffset)
+ : _data(data), _offset(startOffset), _endOffset(endOffset) {
+ if (_offset > _endOffset)
+ _offset = _endOffset;
+ }
+
+ bool next(RA1AnimChunk &chunk) {
+ if (_data == nullptr || _offset + 8 > _endOffset)
+ return false;
+
+ const uint32 size = READ_BE_UINT32(_data + _offset + 4);
+ const uint32 dataOffset = _offset + 8;
+ if (size > _endOffset - dataOffset) {
+ _offset = _endOffset;
+ return false;
+ }
+
+ chunk.tag = READ_BE_UINT32(_data + _offset);
+ chunk.size = size;
+ chunk.offset = _offset;
+ chunk.dataOffset = dataOffset;
+ chunk.endOffset = dataOffset + size;
+ chunk.nextOffset = chunk.endOffset + (size & 1);
+ if (chunk.nextOffset > _endOffset)
+ chunk.nextOffset = _endOffset;
+ chunk.data = _data + dataOffset;
+
+ _offset = (uint32)chunk.nextOffset;
+ return true;
+ }
+
+private:
+ const byte *_data;
+ uint32 _offset;
+ uint32 _endOffset;
+};
+
+class RA1AnimStreamChunkIterator {
+public:
+ RA1AnimStreamChunkIterator(Common::SeekableReadStream &stream, int64 endOffset)
+ : _stream(stream), _endOffset(endOffset) {}
+
+ bool next(RA1AnimChunk &chunk) {
+ if (_stream.pos() + 8 > _endOffset)
+ return false;
+
+ const int64 chunkOffset = _stream.pos();
+ const uint32 tag = _stream.readUint32BE();
+ const uint32 size = _stream.readUint32BE();
+ const int64 dataOffset = _stream.pos();
+ const int64 endOffset = dataOffset + size;
+ if (endOffset < dataOffset || endOffset > _endOffset)
+ return false;
+
+ chunk.tag = tag;
+ chunk.size = size;
+ chunk.offset = chunkOffset;
+ chunk.dataOffset = dataOffset;
+ chunk.endOffset = endOffset;
+ chunk.nextOffset = endOffset + (size & 1);
+ if (chunk.nextOffset > _endOffset)
+ chunk.nextOffset = _endOffset;
+ chunk.data = nullptr;
+ return true;
+ }
+
+ void skip(const RA1AnimChunk &chunk) {
+ _stream.seek(chunk.nextOffset, SEEK_SET);
+ }
+
+private:
+ Common::SeekableReadStream &_stream;
+ int64 _endOffset;
+};
+
+class RA1FrameChunkIterator {
+public:
+ RA1FrameChunkIterator(Common::SeekableReadStream &stream, int32 &remaining)
+ : _stream(stream), _remaining(remaining) {}
+
+ bool next(RA1AnimChunk &chunk) {
+ while (_remaining > 1) {
+ if ((_stream.pos() & 1) && _remaining > 0) {
+ const byte pad = _stream.readByte();
+ if (pad == 0) {
+ _remaining--;
+ } else {
+ _stream.seek(-1, SEEK_CUR);
+ }
+ }
+
+ if (_remaining < 8) {
+ _stream.skip(_remaining);
+ _remaining = 0;
+ return false;
+ }
+
+ const int64 chunkOffset = _stream.pos();
+ chunk.tag = _stream.readUint32BE();
+ chunk.size = _stream.readUint32BE();
+ chunk.offset = chunkOffset;
+ chunk.dataOffset = _stream.pos();
+ chunk.endOffset = chunk.dataOffset + chunk.size;
+ chunk.nextOffset = chunk.endOffset;
+ chunk.data = nullptr;
+ return true;
+ }
+
+ if (_remaining == 1) {
+ _stream.skip(1);
+ _remaining = 0;
+ }
+ return false;
+ }
+
+ bool fits(const RA1AnimChunk &chunk) const {
+ return _remaining >= 8 && chunk.size <= (uint32)(_remaining - 8);
+ }
+
+ void skip(const RA1AnimChunk &chunk) {
+ const int32 consumed = fits(chunk) ? (int32)chunk.size + 8 : _remaining;
+ _remaining -= consumed;
+ _stream.seek(chunk.endOffset, SEEK_SET);
+ }
+
+private:
+ Common::SeekableReadStream &_stream;
+ int32 &_remaining;
+};
+
+} // End of namespace Scumm
+
+#endif
diff --git a/engines/scumm/smush/rebel/smush_player_ra1.cpp b/engines/scumm/smush/rebel/smush_player_ra1.cpp
index e33221d76ee..d04d673155f 100644
--- a/engines/scumm/smush/rebel/smush_player_ra1.cpp
+++ b/engines/scumm/smush/rebel/smush_player_ra1.cpp
@@ -29,6 +29,7 @@
#include "scumm/file.h"
#include "scumm/scumm_v7.h"
+#include "scumm/smush/rebel/anim_ra1.h"
#include "scumm/smush/rebel/codec_ra1.h"
#include "scumm/smush/smush_font.h"
#include "scumm/smush/rebel/codec_ra2.h"
@@ -690,33 +691,20 @@ void SmushPlayerRebel1::handleFrameObject(int32 subSize, Common::SeekableReadStr
static bool ra1FrameHasGameChunk(Common::SeekableReadStream &b, int32 frameSize) {
const int64 frameStart = b.pos();
int32 remaining = frameSize;
-
- while (remaining > 1) {
- if ((b.pos() & 1) && remaining > 0) {
- const byte pad = b.readByte();
- if (pad == 0) {
- remaining--;
- } else {
- b.seek(-1, SEEK_CUR);
- }
- }
- if (remaining < 8)
+ RA1FrameChunkIterator chunks(b, remaining);
+ RA1AnimChunk chunk;
+ while (chunks.next(chunk)) {
+ if (chunk.tag == MKTAG('F', 'R', 'M', 'E'))
break;
-
- const uint32 subType = b.readUint32BE();
- const int32 subSize = b.readUint32BE();
- const int64 subDataPos = b.pos();
-
- if (subType == MKTAG('F', 'R', 'M', 'E'))
- break;
- if (subType == MKTAG('G', 'A', 'M', 'E') ||
- subType == MKTAG('G', 'A', 'M', '2')) {
+ if (chunk.tag == MKTAG('G', 'A', 'M', 'E') ||
+ chunk.tag == MKTAG('G', 'A', 'M', '2')) {
b.seek(frameStart, SEEK_SET);
return true;
}
- remaining -= subSize + 8;
- b.seek(subDataPos + subSize, SEEK_SET);
+ if (!chunks.fits(chunk))
+ break;
+ chunks.skip(chunk);
}
b.seek(frameStart, SEEK_SET);
@@ -928,44 +916,21 @@ void SmushPlayerRebel1::handleFrame(int32 frameSize, Common::SeekableReadStream
memset(_dst, 0, _width * _height);
}
- while (frameSize > 0) {
- // RA1 exits when <=1 byte remains
- if (frameSize <= 1) {
- if (frameSize == 1)
- b.skip(1);
- break;
- }
-
- // RA1 top-of-loop alignment (FUN_1FDBC)
- if ((b.pos() & 1) && frameSize > 0) {
- byte peek = b.readByte();
- if (peek == 0) {
- frameSize--;
- } else {
- b.seek(-1, SEEK_CUR);
- }
- }
-
- if (frameSize < 8) {
- b.skip(frameSize);
- break;
- }
-
- uint32 subType = b.readUint32BE();
- int32 subSize = b.readUint32BE();
- int32 subOffset = b.pos();
+ RA1FrameChunkIterator chunks(b, frameSize);
+ RA1AnimChunk chunk;
+ while (chunks.next(chunk)) {
+ const int32 subSize = (int32)chunk.size;
// Guard against consuming next frame marker
- if (subType == MKTAG('F','R','M','E')) {
- b.seek(-8, SEEK_CUR);
+ if (chunk.tag == MKTAG('F','R','M','E')) {
+ b.seek(chunk.offset, SEEK_SET);
break;
}
- if (ra1DispatchFrameChunk(subType, subSize, frameSize, b, fastForwarding))
+ if (ra1DispatchFrameChunk(chunk.tag, subSize, frameSize, b, fastForwarding))
continue;
- frameSize -= subSize + 8;
- b.seek(subOffset + subSize, SEEK_SET);
+ chunks.skip(chunk);
// RA1 uses top-of-loop alignment, not bottom-of-loop padding
}
Commit: 915b9dab5f5df540c617ef674b2ca15ec2ab7224
https://github.com/scummvm/scummvm/commit/915b9dab5f5df540c617ef674b2ca15ec2ab7224
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:10:13+02:00
Commit Message:
SCUMM: RA1: Refactor handleGameOpcode
Changed paths:
engines/scumm/insane/rebel1/iact.cpp
engines/scumm/insane/rebel1/rebel.h
diff --git a/engines/scumm/insane/rebel1/iact.cpp b/engines/scumm/insane/rebel1/iact.cpp
index 98ed443d600..c89961c50a4 100644
--- a/engines/scumm/insane/rebel1/iact.cpp
+++ b/engines/scumm/insane/rebel1/iact.cpp
@@ -1945,6 +1945,331 @@ void InsaneRebel1::procIACT(byte *renderBitmap, int32 codecparam, int32 setupsan
void InsaneRebel1::procSKIP(int32 subSize, Common::SeekableReadStream &b) {
}
+void InsaneRebel1::handleGameOpcode5EReset(uint32 param1) {
+ // RA1 dispatcher inline reset/init path (FUN_1BE1B case 0x5E).
+ // This is not a pure control-mode assignment.
+ if (_frameDispatchFlags & 0x40) {
+ debug(7, "RA1 GAME 0x5E: reset suppressed by dispatch flags=0x%02x",
+ _frameDispatchFlags);
+ return;
+ }
+
+ _damageFlags = 0;
+ _prevDamageFlags = 0;
+ _damageCooldown = 0;
+ _deathTimer = 0;
+ _screenFlash = 0;
+ _screenShakeEnabled = false;
+ _gameLatch5D = 0;
+ _gameLatch5F = 0;
+ _driftParam = 0;
+ _rollAccum = 0;
+ _liftSmooth = 0;
+ _posAccumX = 0;
+ _posAccumY = 0;
+ memset(_inputHistoryX, 0, sizeof(_inputHistoryX));
+ memset(_inputHistoryY, 0, sizeof(_inputHistoryY));
+ memset(_viewHistoryX, 0, sizeof(_viewHistoryX));
+ memset(_viewHistoryY, 0, sizeof(_viewHistoryY));
+ _inputAxisDeltaX = 0;
+ _avgInputX = 0;
+ _avgInputY = 0;
+ _mouseOffsetX = 0;
+ _mouseOffsetY = 0;
+ _mouseBiasX = 0;
+ _mouseBiasY = 0;
+ _mousePrevBiasX = 0;
+ _mousePrevBiasY = 0;
+ _mouseBiasLatch = false;
+ _mouseRecentering = false;
+ _mouseVirtualRawX = kRA1DosMouseCenterX;
+ _mouseVirtualRawY = kRA1DosMouseCenterY;
+ _mouseVirtualPrevLogicalX = kRA1CenterX;
+ _mouseVirtualPrevLogicalY = kRA1CenterY;
+ _mouseVirtualValid = false;
+ _level2JoystickFilteredX = 0;
+ _level2JoystickFilteredY = 0;
+
+ _playerFired = false;
+ _fireCooldown = 0;
+ memset(_shotSlots, 0, sizeof(_shotSlots));
+ _shotAlternator = 0;
+ _shotSideToggle = false;
+ _targetProximity = 0;
+ _prevTargetProx = 0;
+ _targetAnimCounter = 0;
+ _targetCount = 0;
+ _prevTargetCount = 0;
+ memset(_targetBoxX, 0, sizeof(_targetBoxX));
+ memset(_targetBoxY, 0, sizeof(_targetBoxY));
+ memset(_targetBoxVariant, 0, sizeof(_targetBoxVariant));
+ memset(_gostSlots, 0, sizeof(_gostSlots));
+ _gostSlotIdx = 0;
+ _killCount = 0;
+ _lastHitTarget = 0;
+
+ // Field1 == 0 corresponds to baseline recenter behavior in the original.
+ if ((int32)param1 == 0) {
+ _perspectiveX = 0x20;
+ _perspectiveY = 0x17;
+ _shipPosX = kRA1CenterX;
+ _shipPosY = kRA1CenterY;
+ }
+
+ _activeGameOpcode = 0;
+ _frameGameOpcodeMask = 0;
+ _frameDispatchFlags = 0;
+ resetProjectionTable();
+ if ((int32)param1 == 0)
+ syncViewportOffset(true);
+
+ // Original RunLevel8Flow initializes its separate g_level8HitboxBuffer
+ // after the first L8PLAY runtime reset. We fold that mask into the
+ // secondary half of _frameObjectState; route resumes preserve it.
+ if (_currentLevel == 7)
+ memset(_frameObjectState + 150, 0xFF, 150);
+
+ debug(5, "RA1 GAME 0x5E: reset state field1=%d mode=%d", (int32)param1, (int)_flyControlMode);
+}
+
+void InsaneRebel1::handleGameOpcode5DLinkLatch(uint32 param1) {
+ if ((uint16)param1 == 0xFFFF) {
+ _gameLatch5D = 0xFFFF;
+ } else if (param1 > 0) {
+ const int bitIndex = (int)param1 - 1;
+ const int byteIndex = bitIndex >> 3;
+ if (byteIndex >= 0 && byteIndex < 0x96 &&
+ (_frameObjectState[byteIndex] & (byte)(0x80 >> (bitIndex & 7))) == 0) {
+ _gameLatch5D = (uint16)param1;
+ }
+ }
+
+ debug(5, "RA1 GAME 0x5D (link/event latch) param=%u", _gameLatch5D);
+}
+
+void InsaneRebel1::handleGameOpcode5FRandomHitLatch(uint32 param1) {
+ if (param1 > 0) {
+ const int bitIndex = (int)param1 - 1;
+ const int byteIndex = bitIndex >> 3;
+ if (byteIndex >= 0 && byteIndex < 0x96 &&
+ (_frameObjectState[byteIndex] & (byte)(0x80 >> (bitIndex & 7))) == 0) {
+ _gameLatch5F = (uint16)param1;
+ }
+ }
+
+ debug(5, "RA1 GAME 0x5F (random-hit latch) param=%u", _gameLatch5F);
+}
+
+void InsaneRebel1::handleGameOpcode07ShipFlight(int32 subSize, Common::SeekableReadStream &b, uint32 param1) {
+ _activeGameOpcode = 0x07;
+ _frameGameOpcodeMask |= (1u << 0x07);
+ // Per-frame corridor data: f1=frame counter, f2=max frames, f3=drift bias, f4=unused
+ // f1 is the original's _DAT_7740 (game frame counter)
+ // f3 is the drift/wind parameter combined with tuning table
+ _gameCounter = param1;
+ if (subSize >= 20) {
+ b.readUint32BE(); // f2 (max frames, unused in physics)
+ _driftParam = (int16)(int32)b.readUint32BE();
+ b.readUint32BE(); // f4 (unused in original assembly)
+ debug(7, "RA1 GAME 0x07: counter=%d driftParam=%d", _gameCounter, _driftParam);
+ }
+}
+
+void InsaneRebel1::handleGameOpcode0DCorridor(int32 subSize, Common::SeekableReadStream &b, uint32 param1) {
+ // Corridor boundaries: per-frame flight corridor
+ // Original params: left, top, WIDTH, HEIGHT (not right/bottom!)
+ // FUN_1C54D computes center = (left+width/2, top+height/2), transforms, then checks edges.
+ if (subSize < 20)
+ return;
+
+ int16 corridorLeft = (int16)param1;
+ int16 corridorTop = (int16)b.readUint32BE();
+ int16 corridorWidth = (int16)b.readUint32BE();
+ int16 corridorHeight = (int16)b.readUint32BE();
+
+ int16 centerX = corridorLeft + corridorWidth / 2;
+ int16 centerY = corridorTop + corridorHeight / 2;
+ // DOS FUN_1C54D calls FUN_2248C here, which adds the current
+ // camera offset to the scripted rectangle center before testing it
+ // against the source-buffer ship center.
+ unprojectGameplayPoint(centerX, centerY);
+
+ _corridorLeftX = centerX - corridorWidth / 2;
+ _corridorTopY = centerY - corridorHeight / 2;
+ _corridorRightX = _corridorLeftX + corridorWidth;
+ _corridorBottomY = _corridorTopY + corridorHeight;
+
+ // Apply 0x0D immediately so it sees the same pre-physics ship state as
+ // the other per-frame GAME latches. Deferring this until after 0x07/0x09
+ // movement made right-wall hits fire too early when steering into a wall.
+ int16 collisionShipX = _shipPosX;
+ int16 collisionShipY = _shipPosY;
+ getCollisionShipCenter(collisionShipX, collisionShipY);
+
+ const bool suppressDirectionalDamage = (_frameDispatchFlags & 0x10) != 0;
+ const byte oldDirectionalFlags = _damageFlags & 0x0F;
+ if (_health >= 0) {
+ if (collisionShipX < _corridorLeftX) {
+ _posAccumX = (int32)(_corridorLeftX - kRA1CenterX) * 0x100;
+ if (!suppressDirectionalDamage) {
+ if (_rollAccum < 0x100)
+ _rollAccum = 0x100;
+ _damageFlags |= 0x04;
+ }
+ }
+ if (collisionShipX > _corridorRightX) {
+ _posAccumX = (int32)(_corridorRightX - kRA1CenterX) * 0x100;
+ if (!suppressDirectionalDamage) {
+ if (_rollAccum > -0x100)
+ _rollAccum = -0x100;
+ _damageFlags |= 0x02;
+ }
+ }
+ if (collisionShipY < _corridorTopY) {
+ _posAccumY = (int32)(_corridorTopY - kRA1CenterY) * 0x100 + 0x100;
+ if (!suppressDirectionalDamage)
+ _damageFlags |= 0x01;
+ }
+ if (collisionShipY > _corridorBottomY) {
+ _posAccumY = (int32)(_corridorBottomY - kRA1CenterY) * 0x100 - 0x100;
+ if (!suppressDirectionalDamage)
+ _damageFlags |= 0x08;
+ }
+ }
+ if ((_damageFlags & 0x0F) != oldDirectionalFlags) {
+ debug(1, "RA1 0x0D hit: ship=(%d,%d) corridor=[%d,%d]-[%d,%d] flags=0x%02x zoneSuppressed=%d",
+ collisionShipX, collisionShipY,
+ _corridorLeftX, _corridorTopY, _corridorRightX, _corridorBottomY,
+ _damageFlags, suppressDirectionalDamage ? 1 : 0);
+ }
+
+ debug(5, "RA1 GAME 0x0D: raw=[%d,%d]+(%d,%d) cam=(%d,%d) transformed=[%d,%d]-[%d,%d]",
+ corridorLeft, corridorTop, corridorWidth, corridorHeight,
+ _perspectiveX, _perspectiveY,
+ _corridorLeftX, _corridorTopY, _corridorRightX, _corridorBottomY);
+}
+
+void InsaneRebel1::handleGameOpcode0EZone(int32 subSize, Common::SeekableReadStream &b, uint32 param1) {
+ // Secondary collision zone (FUN_1C6E9): AABB test, sets damageFlags bit 4 (0x10)
+ // Original params: left, top, WIDTH, HEIGHT (same as 0x0D)
+ if (subSize < 20)
+ return;
+
+ int16 zoneLeft = (int16)param1;
+ int16 zoneTop = (int16)b.readUint32BE();
+ int16 zoneWidth = (int16)b.readUint32BE();
+ int16 zoneHeight = (int16)b.readUint32BE();
+ const int16 rawZoneLeft = zoneLeft;
+ const int16 rawZoneTop = zoneTop;
+
+ int16 centerX = zoneLeft + zoneWidth / 2;
+ int16 centerY = zoneTop + zoneHeight / 2;
+ // Same transform as opcode 0x0D/FUN_1C54D.
+ unprojectGameplayPoint(centerX, centerY);
+
+ zoneLeft = centerX - zoneWidth / 2;
+ zoneTop = centerY - zoneHeight / 2;
+ int16 zoneRight = zoneLeft + zoneWidth;
+ int16 zoneBottom = zoneTop + zoneHeight;
+ int16 collisionShipX = _shipPosX;
+ int16 collisionShipY = _shipPosY;
+ getCollisionShipCenter(collisionShipX, collisionShipY);
+
+ if (_health >= 0 &&
+ collisionShipX > zoneLeft && collisionShipX < zoneRight &&
+ collisionShipY > zoneTop && collisionShipY < zoneBottom) {
+ _damageFlags |= 0x10;
+ debug(1, "RA1 0x0E hit: ship=(%d,%d) zone=[%d,%d]-[%d,%d] raw=[%d,%d]+(%d,%d) cam=(%d,%d) flags=0x%02x",
+ collisionShipX, collisionShipY, zoneLeft, zoneTop, zoneRight, zoneBottom,
+ rawZoneLeft, rawZoneTop, zoneWidth, zoneHeight,
+ _perspectiveX, _perspectiveY, _damageFlags);
+ }
+ debug(7, "RA1 GAME 0x0E: ship=(%d,%d) zone=[%d,%d]-[%d,%d] cam=(%d,%d) flags=0x%02x",
+ collisionShipX, collisionShipY, zoneLeft, zoneTop, zoneRight, zoneBottom,
+ _perspectiveX, _perspectiveY, _damageFlags);
+}
+
+void InsaneRebel1::handleGameOpcode0BFirstPerson(int32 subSize, Common::SeekableReadStream &b, uint32 param1) {
+ _activeGameOpcode = 0x0B;
+ _frameGameOpcodeMask |= (1u << 0x0B);
+ // GAME 0x0B per-frame handler (FUN_1CDA7).
+ // field1 = frame counter, field2 = max frames
+ _gameCounter = param1;
+ if (subSize >= 20) {
+ uint32 maxFrames = b.readUint32BE(); // field2 (max frames)
+ b.readUint32BE(); // field3
+ b.readUint32BE(); // field4
+
+ // RA1 scripts drive progression with GAME counters. Finish 0x0B-driven
+ // interactive videos once the script counter reaches the terminal frame.
+ if (_interactiveVideoActive && maxFrames > 0 &&
+ _gameCounter >= (int32)maxFrames - 1) {
+ _vm->_smushVideoShouldFinish = true;
+ debug(1, "RA1: finishing 0x0B interactive video at counter=%d/%u", _gameCounter, maxFrames);
+ }
+ }
+ debug(7, "RA1 GAME 0x0B: counter=%d", _gameCounter);
+ if (!_gameOp0BPhysicsUpdatedThisFrame) {
+ updateGameOp0BPhysics();
+ _gameOp0BPhysicsUpdatedThisFrame = true;
+ syncViewportOffset(true);
+ }
+}
+
+void InsaneRebel1::handleGameOpcode5ATarget(int32 subSize, Common::SeekableReadStream &b, uint32 param1) {
+ // Target detection â HandleGameOp5A (0x1C0EF). AABB from video stream.
+ // Original checks event mask: if target already killed, skip to GOST update.
+ if (subSize < 24)
+ return;
+
+ int16 targetIdx = (int16)param1;
+ int16 left = (int16)b.readUint32BE();
+ int16 top = (int16)b.readUint32BE();
+ int16 w = (int16)b.readUint32BE();
+ int16 h = (int16)b.readUint32BE();
+ int16 right = left + w;
+ int16 bottom = top + h;
+
+ if (targetIdx >= 0) {
+ const int byteIdx = targetIdx >> 3;
+ if (byteIdx >= 0 && byteIdx < 0x96 && byteIdx < kFrameObjectStateBytes) {
+ const byte bit = (byte)(0x80 >> (targetIdx & 7));
+ const int altIdx = byteIdx + 0x96;
+ const bool primarySet = (_frameObjectState[byteIdx] & bit) != 0;
+ const bool secondarySet = (altIdx < kFrameObjectStateBytes) &&
+ ((_frameObjectState[altIdx] & bit) != 0);
+
+ if (!primarySet || secondarySet) {
+ checkTargetHit(targetIdx, left, top, right, bottom);
+ } else {
+ updateGostSlotPosition(targetIdx, left, top, right, bottom);
+ }
+ } else {
+ checkTargetHit(targetIdx, left, top, right, bottom);
+ }
+ }
+ debug(5, "RA1 GAME 0x5A: target=%d rect=[%d,%d]-[%d,%d] prox=%d",
+ targetIdx, left, top, right, bottom, _targetProximity);
+}
+
+void InsaneRebel1::handleGameCounterOpcode(uint32 opcode, int32 subSize, Common::SeekableReadStream &b, uint32 param1) {
+ _activeGameOpcode = (uint16)opcode;
+ _frameGameOpcodeMask |= (1u << opcode);
+ _gameCounter = param1;
+ if (subSize >= 20) {
+ uint32 param2 = b.readUint32BE();
+ uint32 param3 = b.readUint32BE();
+ uint32 param4 = b.readUint32BE();
+ if (opcode == 0x09 && _currentLevel == 4) {
+ debug(1, "RA1 GAME 0x09: counter=%d params=(%d,%d,%d) opcodeMask=0x%08x",
+ _gameCounter, param2, param3, param4, _frameGameOpcodeMask);
+ } else {
+ debug(5, "RA1 GAME 0x%02x: counter=%d params=(%d,%d,%d)",
+ opcode, _gameCounter, param2, param3, param4);
+ }
+ }
+}
+
// handleGameChunk â FUN_1BE1B (0x1BE1B). Central GAME opcode dispatcher.
// Reads 7x32-bit BE integers from GAME chunk, routes to per-opcode handlers.
void InsaneRebel1::handleGameChunk(int32 subSize, Common::SeekableReadStream &b) {
@@ -1973,305 +2298,35 @@ void InsaneRebel1::handleGameChunk(int32 subSize, Common::SeekableReadStream &b)
switch (opcode) {
case 0x5E:
- // RA1 dispatcher inline reset/init path (FUN_1BE1B case 0x5E).
- // This is not a pure control-mode assignment.
- if (_frameDispatchFlags & 0x40) {
- debug(7, "RA1 GAME 0x5E: reset suppressed by dispatch flags=0x%02x",
- _frameDispatchFlags);
- break;
- }
- _damageFlags = 0;
- _prevDamageFlags = 0;
- _damageCooldown = 0;
- _deathTimer = 0;
- _screenFlash = 0;
- _screenShakeEnabled = false;
- _gameLatch5D = 0;
- _gameLatch5F = 0;
- _driftParam = 0;
- _rollAccum = 0;
- _liftSmooth = 0;
- _posAccumX = 0;
- _posAccumY = 0;
- memset(_inputHistoryX, 0, sizeof(_inputHistoryX));
- memset(_inputHistoryY, 0, sizeof(_inputHistoryY));
- memset(_viewHistoryX, 0, sizeof(_viewHistoryX));
- memset(_viewHistoryY, 0, sizeof(_viewHistoryY));
- _inputAxisDeltaX = 0;
- _avgInputX = 0;
- _avgInputY = 0;
- _mouseOffsetX = 0;
- _mouseOffsetY = 0;
- _mouseBiasX = 0;
- _mouseBiasY = 0;
- _mousePrevBiasX = 0;
- _mousePrevBiasY = 0;
- _mouseBiasLatch = false;
- _mouseRecentering = false;
- _mouseVirtualRawX = kRA1DosMouseCenterX;
- _mouseVirtualRawY = kRA1DosMouseCenterY;
- _mouseVirtualPrevLogicalX = kRA1CenterX;
- _mouseVirtualPrevLogicalY = kRA1CenterY;
- _mouseVirtualValid = false;
- _level2JoystickFilteredX = 0;
- _level2JoystickFilteredY = 0;
-
- // Shooting/targeting reset
- _playerFired = false;
- _fireCooldown = 0;
- memset(_shotSlots, 0, sizeof(_shotSlots));
- _shotAlternator = 0;
- _shotSideToggle = false;
- _targetProximity = 0;
- _prevTargetProx = 0;
- _targetAnimCounter = 0;
- _targetCount = 0;
- _prevTargetCount = 0;
- memset(_targetBoxX, 0, sizeof(_targetBoxX));
- memset(_targetBoxY, 0, sizeof(_targetBoxY));
- memset(_targetBoxVariant, 0, sizeof(_targetBoxVariant));
- memset(_gostSlots, 0, sizeof(_gostSlots));
- _gostSlotIdx = 0;
- _killCount = 0;
- _lastHitTarget = 0;
-
- // Field1 == 0 corresponds to baseline recenter behavior in the original.
- if ((int32)param1 == 0) {
- _perspectiveX = 0x20;
- _perspectiveY = 0x17;
- _shipPosX = kRA1CenterX;
- _shipPosY = kRA1CenterY;
- }
-
- _activeGameOpcode = 0;
- _frameGameOpcodeMask = 0;
- _frameDispatchFlags = 0;
- resetProjectionTable();
- if ((int32)param1 == 0)
- syncViewportOffset(true);
-
- // Original RunLevel8Flow initializes its separate g_level8HitboxBuffer
- // after the first L8PLAY runtime reset. We fold that mask into the
- // secondary half of _frameObjectState; route resumes preserve it.
- if (_currentLevel == 7)
- memset(_frameObjectState + 150, 0xFF, 150);
-
- debug(5, "RA1 GAME 0x5E: reset state field1=%d mode=%d", (int32)param1, (int)_flyControlMode);
+ handleGameOpcode5EReset(param1);
break;
case 0x5D:
- if ((uint16)param1 == 0xFFFF) {
- _gameLatch5D = 0xFFFF;
- } else if (param1 > 0) {
- const int bitIndex = (int)param1 - 1;
- const int byteIndex = bitIndex >> 3;
- if (byteIndex >= 0 && byteIndex < 0x96 &&
- (_frameObjectState[byteIndex] & (byte)(0x80 >> (bitIndex & 7))) == 0) {
- _gameLatch5D = (uint16)param1;
- }
- }
- debug(5, "RA1 GAME 0x5D (link/event latch) param=%u", _gameLatch5D);
+ handleGameOpcode5DLinkLatch(param1);
break;
case 0x5F:
- if (param1 > 0) {
- const int bitIndex = (int)param1 - 1;
- const int byteIndex = bitIndex >> 3;
- if (byteIndex >= 0 && byteIndex < 0x96 &&
- (_frameObjectState[byteIndex] & (byte)(0x80 >> (bitIndex & 7))) == 0) {
- _gameLatch5F = (uint16)param1;
- }
- }
- debug(5, "RA1 GAME 0x5F (random-hit latch) param=%u", _gameLatch5F);
+ handleGameOpcode5FRandomHitLatch(param1);
break;
case 0x07:
- _activeGameOpcode = 0x07;
- _frameGameOpcodeMask |= (1u << 0x07);
- // Per-frame corridor data: f1=frame counter, f2=max frames, f3=drift bias, f4=unused
- // f1 is the original's _DAT_7740 (game frame counter)
- // f3 is the drift/wind parameter combined with tuning table
- _gameCounter = param1;
- if (subSize >= 20) {
- b.readUint32BE(); // f2 (max frames, unused in physics)
- _driftParam = (int16)(int32)b.readUint32BE();
- b.readUint32BE(); // f4 (unused in original assembly)
- debug(7, "RA1 GAME 0x07: counter=%d driftParam=%d", _gameCounter, _driftParam);
- }
+ handleGameOpcode07ShipFlight(subSize, b, param1);
break;
case 0x0D:
- // Corridor boundaries: per-frame flight corridor
- // Original params: left, top, WIDTH, HEIGHT (not right/bottom!)
- // FUN_1C54D computes center = (left+width/2, top+height/2), transforms, then checks edges.
- if (subSize >= 20) {
- int16 corridorLeft = (int16)param1;
- int16 corridorTop = (int16)b.readUint32BE();
- int16 corridorWidth = (int16)b.readUint32BE();
- int16 corridorHeight = (int16)b.readUint32BE();
-
- int16 centerX = corridorLeft + corridorWidth / 2;
- int16 centerY = corridorTop + corridorHeight / 2;
- // DOS FUN_1C54D calls FUN_2248C here, which adds the current
- // camera offset to the scripted rectangle center before testing it
- // against the source-buffer ship center.
- unprojectGameplayPoint(centerX, centerY);
-
- _corridorLeftX = centerX - corridorWidth / 2;
- _corridorTopY = centerY - corridorHeight / 2;
- _corridorRightX = _corridorLeftX + corridorWidth;
- _corridorBottomY = _corridorTopY + corridorHeight;
-
- // Apply 0x0D immediately so it sees the same pre-physics ship state as
- // the other per-frame GAME latches. Deferring this until after 0x07/0x09
- // movement made right-wall hits fire too early when steering into a wall.
- int16 collisionShipX = _shipPosX;
- int16 collisionShipY = _shipPosY;
- getCollisionShipCenter(collisionShipX, collisionShipY);
-
- const bool suppressDirectionalDamage = (_frameDispatchFlags & 0x10) != 0;
- const byte oldDirectionalFlags = _damageFlags & 0x0F;
- if (_health >= 0) {
- if (collisionShipX < _corridorLeftX) {
- _posAccumX = (int32)(_corridorLeftX - kRA1CenterX) * 0x100;
- if (!suppressDirectionalDamage) {
- if (_rollAccum < 0x100)
- _rollAccum = 0x100;
- _damageFlags |= 0x04;
- }
- }
- if (collisionShipX > _corridorRightX) {
- _posAccumX = (int32)(_corridorRightX - kRA1CenterX) * 0x100;
- if (!suppressDirectionalDamage) {
- if (_rollAccum > -0x100)
- _rollAccum = -0x100;
- _damageFlags |= 0x02;
- }
- }
- if (collisionShipY < _corridorTopY) {
- _posAccumY = (int32)(_corridorTopY - kRA1CenterY) * 0x100 + 0x100;
- if (!suppressDirectionalDamage)
- _damageFlags |= 0x01;
- }
- if (collisionShipY > _corridorBottomY) {
- _posAccumY = (int32)(_corridorBottomY - kRA1CenterY) * 0x100 - 0x100;
- if (!suppressDirectionalDamage)
- _damageFlags |= 0x08;
- }
- }
- if ((_damageFlags & 0x0F) != oldDirectionalFlags) {
- debug(1, "RA1 0x0D hit: ship=(%d,%d) corridor=[%d,%d]-[%d,%d] flags=0x%02x zoneSuppressed=%d",
- collisionShipX, collisionShipY,
- _corridorLeftX, _corridorTopY, _corridorRightX, _corridorBottomY,
- _damageFlags, suppressDirectionalDamage ? 1 : 0);
- }
-
- debug(5, "RA1 GAME 0x0D: raw=[%d,%d]+(%d,%d) cam=(%d,%d) transformed=[%d,%d]-[%d,%d]",
- corridorLeft, corridorTop, corridorWidth, corridorHeight,
- _perspectiveX, _perspectiveY,
- _corridorLeftX, _corridorTopY, _corridorRightX, _corridorBottomY);
- }
+ handleGameOpcode0DCorridor(subSize, b, param1);
break;
case 0x0E:
- // Secondary collision zone (FUN_1C6E9): AABB test, sets damageFlags bit 4 (0x10)
- // Original params: left, top, WIDTH, HEIGHT (same as 0x0D)
- if (subSize >= 20) {
- int16 zoneLeft = (int16)param1;
- int16 zoneTop = (int16)b.readUint32BE();
- int16 zoneWidth = (int16)b.readUint32BE();
- int16 zoneHeight = (int16)b.readUint32BE();
- const int16 rawZoneLeft = zoneLeft;
- const int16 rawZoneTop = zoneTop;
-
- int16 centerX = zoneLeft + zoneWidth / 2;
- int16 centerY = zoneTop + zoneHeight / 2;
- // Same transform as opcode 0x0D/FUN_1C54D.
- unprojectGameplayPoint(centerX, centerY);
-
- zoneLeft = centerX - zoneWidth / 2;
- zoneTop = centerY - zoneHeight / 2;
- int16 zoneRight = zoneLeft + zoneWidth;
- int16 zoneBottom = zoneTop + zoneHeight;
- int16 collisionShipX = _shipPosX;
- int16 collisionShipY = _shipPosY;
- getCollisionShipCenter(collisionShipX, collisionShipY);
-
- if (_health >= 0 &&
- collisionShipX > zoneLeft && collisionShipX < zoneRight &&
- collisionShipY > zoneTop && collisionShipY < zoneBottom) {
- _damageFlags |= 0x10;
- debug(1, "RA1 0x0E hit: ship=(%d,%d) zone=[%d,%d]-[%d,%d] raw=[%d,%d]+(%d,%d) cam=(%d,%d) flags=0x%02x",
- collisionShipX, collisionShipY, zoneLeft, zoneTop, zoneRight, zoneBottom,
- rawZoneLeft, rawZoneTop, zoneWidth, zoneHeight,
- _perspectiveX, _perspectiveY, _damageFlags);
- }
- debug(7, "RA1 GAME 0x0E: ship=(%d,%d) zone=[%d,%d]-[%d,%d] cam=(%d,%d) flags=0x%02x",
- collisionShipX, collisionShipY, zoneLeft, zoneTop, zoneRight, zoneBottom,
- _perspectiveX, _perspectiveY, _damageFlags);
- }
+ handleGameOpcode0EZone(subSize, b, param1);
break;
case 0x0B:
- _activeGameOpcode = 0x0B;
- _frameGameOpcodeMask |= (1u << 0x0B);
- // GAME 0x0B per-frame handler (FUN_1CDA7).
- // field1 = frame counter, field2 = max frames
- _gameCounter = param1;
- if (subSize >= 20) {
- uint32 maxFrames = b.readUint32BE(); // field2 (max frames)
- b.readUint32BE(); // field3
- b.readUint32BE(); // field4
-
- // RA1 scripts drive progression with GAME counters. Finish 0x0B-driven
- // interactive videos once the script counter reaches the terminal frame.
- if (_interactiveVideoActive && maxFrames > 0 &&
- _gameCounter >= (int32)maxFrames - 1) {
- _vm->_smushVideoShouldFinish = true;
- debug(1, "RA1: finishing 0x0B interactive video at counter=%d/%u", _gameCounter, maxFrames);
- }
- }
- debug(7, "RA1 GAME 0x0B: counter=%d", _gameCounter);
- if (!_gameOp0BPhysicsUpdatedThisFrame) {
- updateGameOp0BPhysics();
- _gameOp0BPhysicsUpdatedThisFrame = true;
- syncViewportOffset(true);
- }
+ handleGameOpcode0BFirstPerson(subSize, b, param1);
break;
case 0x5A:
- // Target detection â HandleGameOp5A (0x1C0EF). AABB from video stream.
- // Original checks event mask: if target already killed, skip to GOST update.
- if (subSize >= 24) {
- int16 targetIdx = (int16)param1;
- int16 left = (int16)b.readUint32BE();
- int16 top = (int16)b.readUint32BE();
- int16 w = (int16)b.readUint32BE();
- int16 h = (int16)b.readUint32BE();
- int16 right = left + w;
- int16 bottom = top + h;
-
- if (targetIdx >= 0) {
- const int byteIdx = targetIdx >> 3;
- if (byteIdx >= 0 && byteIdx < 0x96 && byteIdx < kFrameObjectStateBytes) {
- const byte bit = (byte)(0x80 >> (targetIdx & 7));
- const int altIdx = byteIdx + 0x96;
- const bool primarySet = (_frameObjectState[byteIdx] & bit) != 0;
- const bool secondarySet = (altIdx < kFrameObjectStateBytes) &&
- ((_frameObjectState[altIdx] & bit) != 0);
-
- if (!primarySet || secondarySet) {
- checkTargetHit(targetIdx, left, top, right, bottom);
- } else {
- updateGostSlotPosition(targetIdx, left, top, right, bottom);
- }
- } else {
- checkTargetHit(targetIdx, left, top, right, bottom);
- }
- }
- debug(5, "RA1 GAME 0x5A: target=%d rect=[%d,%d]-[%d,%d] prox=%d",
- targetIdx, left, top, right, bottom, _targetProximity);
- }
+ handleGameOpcode5ATarget(subSize, b, param1);
break;
case 0x08:
@@ -2279,21 +2334,7 @@ void InsaneRebel1::handleGameChunk(int32 subSize, Common::SeekableReadStream &b)
case 0x0A:
case 0x19:
case 0x1A:
- _activeGameOpcode = (uint16)opcode;
- _frameGameOpcodeMask |= (1u << opcode);
- _gameCounter = param1;
- if (subSize >= 20) {
- uint32 param2 = b.readUint32BE();
- uint32 param3 = b.readUint32BE();
- uint32 param4 = b.readUint32BE();
- if (opcode == 0x09 && _currentLevel == 4) {
- debug(1, "RA1 GAME 0x09: counter=%d params=(%d,%d,%d) opcodeMask=0x%08x",
- _gameCounter, param2, param3, param4, _frameGameOpcodeMask);
- } else {
- debug(5, "RA1 GAME 0x%02x: counter=%d params=(%d,%d,%d)",
- opcode, _gameCounter, param2, param3, param4);
- }
- }
+ handleGameCounterOpcode(opcode, subSize, b, param1);
break;
default:
diff --git a/engines/scumm/insane/rebel1/rebel.h b/engines/scumm/insane/rebel1/rebel.h
index d358202f8ef..155dc39da4f 100644
--- a/engines/scumm/insane/rebel1/rebel.h
+++ b/engines/scumm/insane/rebel1/rebel.h
@@ -186,6 +186,15 @@ private:
void playInteractiveVideoFile(const char *filename, int32 videoOffset, int32 videoStartFrame);
bool loadRA1Nut(const char *filename, RA1SpriteBank &bank);
void loadLevelSprites(int level);
+ void handleGameOpcode5EReset(uint32 param1);
+ void handleGameOpcode5DLinkLatch(uint32 param1);
+ void handleGameOpcode5FRandomHitLatch(uint32 param1);
+ void handleGameOpcode07ShipFlight(int32 subSize, Common::SeekableReadStream &b, uint32 param1);
+ void handleGameOpcode0DCorridor(int32 subSize, Common::SeekableReadStream &b, uint32 param1);
+ void handleGameOpcode0EZone(int32 subSize, Common::SeekableReadStream &b, uint32 param1);
+ void handleGameOpcode0BFirstPerson(int32 subSize, Common::SeekableReadStream &b, uint32 param1);
+ void handleGameOpcode5ATarget(int32 subSize, Common::SeekableReadStream &b, uint32 param1);
+ void handleGameCounterOpcode(uint32 opcode, int32 subSize, Common::SeekableReadStream &b, uint32 param1);
void updateShipPhysics();
void updateTurretPhysics();
void updateTurretShipDirection(int16 offsetY);
Commit: 54fe61ff8d06de23e232c15b9784e0cffeef2cf3
https://github.com/scummvm/scummvm/commit/54fe61ff8d06de23e232c15b9784e0cffeef2cf3
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:10:14+02:00
Commit Message:
SCUMM: RA1: Render number of pilots left correctly
Changed paths:
engines/scumm/insane/rebel1/render.cpp
diff --git a/engines/scumm/insane/rebel1/render.cpp b/engines/scumm/insane/rebel1/render.cpp
index a390fc6006f..c5397079d76 100644
--- a/engines/scumm/insane/rebel1/render.cpp
+++ b/engines/scumm/insane/rebel1/render.cpp
@@ -1692,28 +1692,50 @@ void InsaneRebel1::renderHUD(byte *dst, int pitch, int width, int height) {
hudOriginY = ra1Player()->_ra1ViewportOffsetY;
}
- int hudX = hudOriginX + bar.xoffs;
- int hudY = hudOriginY + bar.yoffs;
+ const int hudX = hudOriginX + bar.xoffs;
+ const int hudY = hudOriginY + bar.yoffs;
+ // FUN_1BBCB draws DISPLAY.NUT at x=5 with a 4..315 clip rect. The HUD
+ // masks/text below use the unshifted screen-space coordinate origin.
+ const int hudPlateX = hudX + 5;
+ const int hudPlateY = hudY;
// DOS RA1 draws the HUD plate through DrawFobjGlyph(..., flags=0x181),
// which selects the opaque blit path. Keep zero-valued pixels black instead
// of treating them as transparent.
if (bar.data && bar.width > 0 && bar.height > 0) {
- int drawX = hudX, drawY = hudY, drawW = bar.width, drawH = bar.height;
+ int drawX = hudPlateX, drawY = hudPlateY, drawW = bar.width, drawH = bar.height;
int srcOffX = 0, srcOffY = 0;
+ const int clipLeft = hudX + 4;
+ const int clipTop = hudY;
+ const int clipRight = hudX + 315;
+ const int clipBottom = hudY + 19;
+ if (drawX < clipLeft) {
+ srcOffX = clipLeft - drawX;
+ drawW -= srcOffX;
+ drawX = clipLeft;
+ }
+ if (drawY < clipTop) {
+ srcOffY = clipTop - drawY;
+ drawH -= srcOffY;
+ drawY = clipTop;
+ }
+ if (drawX + drawW - 1 > clipRight) drawW = clipRight - drawX + 1;
+ if (drawY + drawH - 1 > clipBottom) drawH = clipBottom - drawY + 1;
if (drawX < 0) { srcOffX = -drawX; drawW += drawX; drawX = 0; }
if (drawY < 0) { srcOffY = -drawY; drawH += drawY; drawY = 0; }
if (drawX + drawW > width) drawW = width - drawX;
if (drawY + drawH > height) drawH = height - drawY;
- for (int iy = 0; iy < drawH; iy++) {
- const byte *s = bar.data + (srcOffY + iy) * bar.width + srcOffX;
- byte *d = dst + (drawY + iy) * pitch + drawX;
- memcpy(d, s, drawW);
+ if (drawW > 0 && drawH > 0) {
+ for (int iy = 0; iy < drawH; iy++) {
+ const byte *s = bar.data + (srcOffY + iy) * bar.width + srcOffX;
+ byte *d = dst + (drawY + iy) * pitch + drawX;
+ memcpy(d, s, drawW);
+ }
}
debug(5, "RA1 HUD: drawn at (%d,%d) size=%dx%d",
- hudX, hudY, bar.width, bar.height);
+ hudPlateX, hudPlateY, bar.width, bar.height);
}
// Draw health bar from FUN_1BBCB (0x1BBCB) + FUN_21D66 (0x21D66):
Commit: dee67a5319a51c39ad0e9141c260057047cbdd7b
https://github.com/scummvm/scummvm/commit/dee67a5319a51c39ad0e9141c260057047cbdd7b
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:10:14+02:00
Commit Message:
SCUMM: RA1: Add basic save/load support
Changed paths:
A engines/scumm/insane/rebel1/saveload.cpp
engines/scumm/insane/rebel1/menu.cpp
engines/scumm/insane/rebel1/rebel.cpp
engines/scumm/insane/rebel1/rebel.h
engines/scumm/insane/rebel1/runlevels.cpp
engines/scumm/module.mk
engines/scumm/saveload.cpp
diff --git a/engines/scumm/insane/rebel1/menu.cpp b/engines/scumm/insane/rebel1/menu.cpp
index c7628ea5fea..43f7c341a06 100644
--- a/engines/scumm/insane/rebel1/menu.cpp
+++ b/engines/scumm/insane/rebel1/menu.cpp
@@ -922,20 +922,20 @@ void InsaneRebel1::playMenuBackground() {
}
bool InsaneRebel1::runTextEntryMenuLoop() {
- while (!_vm->shouldQuit() && !_textEntryDone && !_textEntryCanceled)
+ while (!shouldAbortGameFlow() && !_textEntryDone && !_textEntryCanceled)
playMenuBackground();
- return !_vm->shouldQuit() && !_textEntryCanceled;
+ return !shouldAbortGameFlow() && !_textEntryCanceled;
}
int InsaneRebel1::runMainMenu() {
debug(1, "InsaneRebel1: Main menu");
_menuSelection = 0;
- while (!_vm->shouldQuit()) {
+ while (!shouldAbortGameFlow()) {
playMenuBackground();
- if (_vm->shouldQuit())
+ if (shouldAbortGameFlow())
return kRA1MainMenuItemCount;
if (_menuConfirmed)
@@ -1004,10 +1004,10 @@ void InsaneRebel1::runOptionsMenu() {
_optionsSel = 0;
_optionsActive = true;
- while (!_vm->shouldQuit()) {
+ while (!shouldAbortGameFlow()) {
playMenuBackground();
- if (_vm->shouldQuit())
+ if (shouldAbortGameFlow())
break;
if (_menuConfirmed) {
@@ -1053,10 +1053,10 @@ int InsaneRebel1::runLevelSelectMenu() {
_levelSelectSel = CLIP(_startLevel - 1, 0, kRA1NumLevels - 1);
_levelSelectActive = true;
- while (!_vm->shouldQuit()) {
+ while (!shouldAbortGameFlow()) {
playMenuBackground();
- if (_vm->shouldQuit())
+ if (shouldAbortGameFlow())
break;
if (_menuConfirmed) {
diff --git a/engines/scumm/insane/rebel1/rebel.cpp b/engines/scumm/insane/rebel1/rebel.cpp
index c6686439014..875a305541e 100644
--- a/engines/scumm/insane/rebel1/rebel.cpp
+++ b/engines/scumm/insane/rebel1/rebel.cpp
@@ -274,6 +274,9 @@ InsaneRebel1::InsaneRebel1(ScummEngine_v7 *scumm) : Insane(), _vm(scumm) {
_activeInputSource = kInputSourceMouse;
_currentLevel = 0;
+ _resumeLevel = 1;
+ _activeSaveSlot = -1;
+ _loadRequested = false;
_flyControlMode = 0;
_turretEmitterLeftX = 0;
_turretEmitterLeftY = 0;
diff --git a/engines/scumm/insane/rebel1/rebel.h b/engines/scumm/insane/rebel1/rebel.h
index 155dc39da4f..22f93bd3e49 100644
--- a/engines/scumm/insane/rebel1/rebel.h
+++ b/engines/scumm/insane/rebel1/rebel.h
@@ -126,6 +126,9 @@ public:
// Game flow (matching original at 0x15597)
void runGame();
+ Common::Error saveGameState(int slot, const Common::String &desc, bool isAutosave = false);
+ Common::Error loadGameState(int slot, bool startupLoad = false);
+ bool shouldAbortGameFlow() const { return _vm->shouldQuit() || _loadRequested; }
private:
// Intro sequence: O1LOGO â O1OPEN (0x155ef-0x158f8)
@@ -168,6 +171,19 @@ private:
const char *bonusLabel2 = nullptr, const char *detailText2 = nullptr, int bonusValue2 = 0,
int passwordIndex = 0);
bool playDeathOrRetry(const char *retryVideo, const char *gameOverVideo);
+ void autosaveProgress();
+ int getAutosaveTargetSlot() const;
+ int getCurrentSaveLevel() const;
+ struct SaveState {
+ int resumeLevel;
+ int lives;
+ int score;
+ int prevScore;
+ int difficulty;
+ int maxChapterUnlocked;
+ };
+ Common::Error writeSaveState(int slot, const Common::String &desc, const SaveState &state) const;
+ bool readSaveState(int slot, SaveState &state, Common::String *desc = nullptr) const;
void resetLevelDamageState();
void resetLevelFrameState();
void resetLevelTargetingState(bool resetKillCount = true);
@@ -365,6 +381,9 @@ private:
// Current level index (0-based: 0=LVL1, 1=LVL2, etc.)
int _currentLevel;
+ int _resumeLevel; // 1-based level used when saving/restoring RA1 progress
+ int _activeSaveSlot; // Last manually saved or loaded slot; -1 means use autosave slot
+ bool _loadRequested; // Runtime load requested while unwinding the current video flow
// Intro title overlay (RunTwoLineTextSplash from original)
bool _introTextActive;
@@ -461,6 +480,7 @@ private:
static const int16 kMaxHealth = 98;
static const int16 kDeathTimerInit = 30;
static const int16 kDamageCooldownInit = 10;
+ enum { kNumLevels = 15 };
// Streamed SMUSH audio
RebelAudio _audio;
diff --git a/engines/scumm/insane/rebel1/runlevels.cpp b/engines/scumm/insane/rebel1/runlevels.cpp
index 6c1b16854ce..8dffb1c5a27 100644
--- a/engines/scumm/insane/rebel1/runlevels.cpp
+++ b/engines/scumm/insane/rebel1/runlevels.cpp
@@ -19,6 +19,7 @@
*
*/
+#include "common/config-manager.h"
#include "common/system.h"
#include "graphics/cursorman.h"
@@ -147,6 +148,9 @@ static int calculateThresholdBonus(int kills, int perfectThreshold, int perKillT
// startFrame > 0: fast-forward (decode without display/audio) to that frame.
void InsaneRebel1::playCinematic(const char *filename, int32 startFrame) {
debug(1, "InsaneRebel1::playCinematic('%s', startFrame=%d)", filename, startFrame);
+ if (shouldAbortGameFlow())
+ return;
+
SmushPlayer *splayer = _vm->_splayer;
_player = splayer;
restoreScreenFlashPalette();
@@ -184,7 +188,7 @@ void InsaneRebel1::playChapterCompleteCinematic(const char *filename, int16 unlo
bool InsaneRebel1::playDeathOrRetry(const char *retryVideo, const char *gameOverVideo) {
if (_lives > 0) {
playCinematic(retryVideo);
- if (_vm->shouldQuit())
+ if (shouldAbortGameFlow())
return false;
_lives--;
return true;
@@ -263,13 +267,13 @@ void InsaneRebel1::playLevelTransitionCutscene(int level) {
// FALCON/BIGGS/WEDGE passcode group. This is separate from RunLevel4Flow
// (0x6ee4), which starts with LVL4/L4INTRO.ANM.
playCinematic("CUT1/C1BLOCK.ANM");
- if (_vm->shouldQuit())
+ if (shouldAbortGameFlow())
break;
playCinematic("CUT1/C1DARTH1.ANM");
- if (_vm->shouldQuit())
+ if (shouldAbortGameFlow())
break;
playCinematic("CUT1/C1C3PO.ANM");
- if (_vm->shouldQuit())
+ if (shouldAbortGameFlow())
break;
playCinematic("CUT1/C1DARTH2.ANM");
break;
@@ -315,7 +319,7 @@ void InsaneRebel1::playIntroSequence() {
// LucasArts logo (original: PUSH 0x57cc, CALL FUN_1BA32 with flags 0x0420)
playCinematic("OPEN/O1LOGO.ANM");
- if (_vm->shouldQuit())
+ if (shouldAbortGameFlow())
return;
clearVideoBuffer();
@@ -351,14 +355,14 @@ bool InsaneRebel1::runLevel1() {
beginLevelTitleOverlay(0);
playCinematic("LVL1/L1HANGAR.ANM");
- if (_vm->shouldQuit())
+ if (shouldAbortGameFlow())
return false;
playCinematic("LVL1/L1CU1.ANM");
- if (_vm->shouldQuit())
+ if (shouldAbortGameFlow())
return false;
- while (!_vm->shouldQuit()) {
+ while (!shouldAbortGameFlow()) {
bool stage2Started = false;
resetLevelDamageState();
@@ -371,24 +375,24 @@ bool InsaneRebel1::runLevel1() {
loadTuningForLevel(0);
playInteractiveVideo("LVL1/L1PLAY1L.ANM");
- if (_vm->shouldQuit())
+ if (shouldAbortGameFlow())
return false;
if (_rightPathSelected && _health >= 0) {
_pathBranchEnabled = false;
_flyControlMode = 1;
playInteractiveVideo("LVL1/L1PLAY1R.ANM", 0x187);
- if (_vm->shouldQuit())
+ if (shouldAbortGameFlow())
return false;
}
_pathBranchEnabled = false;
if (_health >= 0) {
playCinematic("LVL1/L1CU2.ANM");
- if (_vm->shouldQuit())
+ if (shouldAbortGameFlow())
return false;
- while (!_vm->shouldQuit()) {
+ while (!shouldAbortGameFlow()) {
// RunLevel1Flow calls L1PLAY2 with gameplay selector 1. This is
// the "1B" tuning row: snap/kill values are enabled and the
// lock/fire text overlay is suppressed.
@@ -405,7 +409,7 @@ bool InsaneRebel1::runLevel1() {
stage2Started = true;
playInteractiveVideo("LVL1/L1PLAY2.ANM");
- if (_vm->shouldQuit())
+ if (shouldAbortGameFlow())
return false;
if (_health < 0)
@@ -420,11 +424,11 @@ bool InsaneRebel1::runLevel1() {
playChapterCompleteCinematic("LVL1/L1END.ANM", 1, 0x78, 5,
"Part I", pathText, pathBonus,
"Part II", accuracyText, targetBonus);
- return !_vm->shouldQuit();
+ return !shouldAbortGameFlow();
}
playCinematic("LVL1/L1RETRY.ANM");
- if (_vm->shouldQuit())
+ if (shouldAbortGameFlow())
return false;
}
}
@@ -433,7 +437,7 @@ bool InsaneRebel1::runLevel1() {
playCinematic("LVL1/L1CRASHB.ANM");
else
playCinematic("LVL1/L1CRASHA.ANM");
- if (_vm->shouldQuit())
+ if (shouldAbortGameFlow())
return false;
if (!playDeathOrRetry("LVL1/L1NEW.ANM", "LVL1/L1DEATH.ANM"))
@@ -453,10 +457,10 @@ bool InsaneRebel1::runLevel2() {
beginLevelTitleOverlay(1);
playCinematic("LVL2/L2INTRO.ANM");
- if (_vm->shouldQuit())
+ if (shouldAbortGameFlow())
return false;
- while (!_vm->shouldQuit()) {
+ while (!shouldAbortGameFlow()) {
_flyControlMode = 0;
resetLevelDamageState();
resetLevelFrameState();
@@ -464,7 +468,7 @@ bool InsaneRebel1::runLevel2() {
_killCount = 0;
playInteractiveVideo("LVL2/L2PLAY.ANM");
- if (_vm->shouldQuit())
+ if (shouldAbortGameFlow())
return false;
if (_health >= 0) {
@@ -472,7 +476,7 @@ bool InsaneRebel1::runLevel2() {
formatTargetAccuracy(accuracyText, sizeof(accuracyText), _killCount, 0x18, true);
const int bonus = calculateThresholdBonus(_killCount, 0x17, 0x0C, _tuning.bonus);
playChapterCompleteCinematic("LVL2/L2END.ANM", 2, 0x69, 10, " ", accuracyText, bonus);
- return !_vm->shouldQuit();
+ return !shouldAbortGameFlow();
}
if (playDeathOrRetry("LVL2/L2NEW.ANM", "LVL2/L2DEATH.ANM"))
@@ -494,10 +498,10 @@ bool InsaneRebel1::runLevel3() {
beginLevelTitleOverlay(2);
playCinematic("LVL3/L3INTRO.ANM");
- if (_vm->shouldQuit())
+ if (shouldAbortGameFlow())
return false;
- while (!_vm->shouldQuit()) {
+ while (!shouldAbortGameFlow()) {
_flyControlMode = 1;
resetLevelDamageState();
resetLevelFrameState();
@@ -507,13 +511,13 @@ bool InsaneRebel1::runLevel3() {
resetLevelInputHistory();
playInteractiveVideo("LVL3/L3PLAY.ANM");
- if (_vm->shouldQuit())
+ if (shouldAbortGameFlow())
return false;
if (_health >= 0) {
playChapterCompleteCinematic("LVL3/L3END.ANM", 3, 0x69, 5,
nullptr, nullptr, 0, nullptr, nullptr, 0, _difficulty + 1);
- return !_vm->shouldQuit();
+ return !shouldAbortGameFlow();
}
if (playDeathOrRetry("LVL3/L3NEW.ANM", "LVL3/L3DEATH.ANM"))
@@ -535,10 +539,10 @@ bool InsaneRebel1::runLevel4() {
beginLevelTitleOverlay(3);
playCinematic("LVL4/L4INTRO.ANM");
- if (_vm->shouldQuit())
+ if (shouldAbortGameFlow())
return false;
- while (!_vm->shouldQuit()) {
+ while (!shouldAbortGameFlow()) {
loadTuningForLevel(4);
_flyControlMode = 1;
resetLevelDamageState();
@@ -559,7 +563,7 @@ bool InsaneRebel1::runLevel4() {
playInteractiveVideo("LVL4/L4PLAY1.ANM");
_protectedTargetA = 0;
_protectedTargetB = 0;
- if (_vm->shouldQuit())
+ if (shouldAbortGameFlow())
return false;
if (_health >= 0) {
@@ -573,7 +577,7 @@ bool InsaneRebel1::runLevel4() {
_killCount = 0;
_levelGameplayPhase = 2;
playInteractiveVideo("LVL4/L4PLAY2.ANM");
- if (_vm->shouldQuit())
+ if (shouldAbortGameFlow())
return false;
}
@@ -583,7 +587,7 @@ bool InsaneRebel1::runLevel4() {
playChapterCompleteCinematic(torpedoHit ? "LVL4/L4END1.ANM" : "LVL4/L4END2.ANM",
4, 0x69, 5, " ", torpedoHit ? "Torpedo Hit" : "Torpedo Missed",
torpedoHit ? _tuning.bonus : 0);
- return !_vm->shouldQuit();
+ return !shouldAbortGameFlow();
}
if (playDeathOrRetry("LVL4/L4NEW.ANM", "LVL4/L4DEATH.ANM"))
@@ -605,17 +609,17 @@ bool InsaneRebel1::runLevel5() {
beginLevelTitleOverlay(4);
playCinematic("LVL5/L5INTRO.ANM");
- if (_vm->shouldQuit())
+ if (shouldAbortGameFlow())
return false;
- while (!_vm->shouldQuit()) {
+ while (!shouldAbortGameFlow()) {
loadRA1Nut("LVL5/L5LASER.NUT", _laserBank);
loadTuningForLevel(6);
resetLevelAttemptState(1, 1);
_level5SuccessFramesRemaining = 0x14;
playInteractiveVideo("LVL5/L5PLAY.ANM");
- if (_vm->shouldQuit())
+ if (shouldAbortGameFlow())
return false;
if (_health < 0) {
@@ -629,7 +633,7 @@ bool InsaneRebel1::runLevel5() {
if (_lives > 0) {
_lives--;
playCinematic("LVL5/L5RETRY.ANM");
- if (_vm->shouldQuit())
+ if (shouldAbortGameFlow())
return false;
continue;
}
@@ -639,7 +643,7 @@ bool InsaneRebel1::runLevel5() {
}
playCinematic("LVL5/L5BINTRO.ANM");
- if (_vm->shouldQuit())
+ if (shouldAbortGameFlow())
return false;
loadRA1Nut("LVL5/L5LASER2.NUT", _laserBank);
@@ -651,7 +655,7 @@ bool InsaneRebel1::runLevel5() {
_levelGameplayPhase = 2;
_level5SuccessFramesRemaining = 0;
playInteractiveVideo("LVL5/L5PLAY2.ANM");
- if (_vm->shouldQuit())
+ if (shouldAbortGameFlow())
return false;
if (_health >= 0) {
@@ -665,7 +669,7 @@ bool InsaneRebel1::runLevel5() {
if (_killCount >= 0x4C)
bonus += (_killCount - 0x4B) * _tuning.bonus;
playChapterCompleteCinematic("LVL5/L5END.ANM", 5, 0x69, 5, " ", accuracyText, bonus);
- return !_vm->shouldQuit();
+ return !shouldAbortGameFlow();
}
if (playDeathOrRetry("LVL5/L5NEW.ANM", "LVL5/L5DEATH.ANM"))
@@ -688,10 +692,10 @@ bool InsaneRebel1::runLevel6() {
beginLevelTitleOverlay(5);
playCinematic("LVL6/L6INTRO.ANM");
- if (_vm->shouldQuit())
+ if (shouldAbortGameFlow())
return false;
- while (!_vm->shouldQuit()) {
+ while (!shouldAbortGameFlow()) {
_flyControlMode = 1;
resetLevelDamageState();
resetLevelFrameState();
@@ -701,7 +705,7 @@ bool InsaneRebel1::runLevel6() {
resetLevelInputHistory();
playInteractiveVideo("LVL6/L6PLAY.ANM");
- if (_vm->shouldQuit())
+ if (shouldAbortGameFlow())
return false;
if (_health >= 0) {
@@ -710,7 +714,7 @@ bool InsaneRebel1::runLevel6() {
const int bonus = calculateThresholdBonus(_killCount, 0x26, 0x0C, _tuning.bonus);
playChapterCompleteCinematic("LVL6/L6END.ANM", 6, 0x4B, 5,
" ", accuracyText, bonus, nullptr, nullptr, 0, _difficulty + 4);
- return !_vm->shouldQuit();
+ return !shouldAbortGameFlow();
}
if (playDeathOrRetry("LVL6/L6NEW.ANM", "LVL6/L6DEATH.ANM"))
@@ -742,10 +746,10 @@ bool InsaneRebel1::runLevel7() {
beginLevelTitleOverlay(6);
playCinematic("LVL7/L7INTRO.ANM");
- if (_vm->shouldQuit())
+ if (shouldAbortGameFlow())
return false;
- while (!_vm->shouldQuit()) {
+ while (!shouldAbortGameFlow()) {
resetLevelAttemptState(3, 0);
_driftParam = 0x19;
resetEnemyShotSlots();
@@ -756,14 +760,14 @@ bool InsaneRebel1::runLevel7() {
int32 routeStartFrame = 0;
int32 routeSourceFrame = 0;
int32 routeVideoStartFrame = 0;
- while (!_vm->shouldQuit()) {
+ while (!shouldAbortGameFlow()) {
_levelRouteIndex = route;
_pendingRouteIndex = -1;
_pendingRouteStartFrame = routeSourceFrame;
_pendingRouteCutoverFrame = -1;
_pendingRouteVideoStartFrame = routeVideoStartFrame;
playInteractiveVideo(kLevel7Segments[route], routeStartFrame);
- if (_vm->shouldQuit())
+ if (shouldAbortGameFlow())
return false;
if (_health < 0)
@@ -794,7 +798,7 @@ bool InsaneRebel1::runLevel7() {
formatTargetAccuracy(accuracyText, sizeof(accuracyText), _killCount, 0x33, true);
const int bonus = calculateThresholdBonus(_killCount, 0x32, 0x14, _tuning.bonus);
playChapterCompleteCinematic("LVL7/L7END.ANM", 7, 0x69, 5, " ", accuracyText, bonus);
- return !_vm->shouldQuit();
+ return !shouldAbortGameFlow();
}
if (playDeathOrRetry("LVL7/L7NEW.ANM", "LVL7/L7DEATH.ANM"))
@@ -824,10 +828,10 @@ bool InsaneRebel1::runLevel8() {
beginLevelTitleOverlay(7);
playCinematic("LVL8/L8INTRO.ANM");
- if (_vm->shouldQuit())
+ if (shouldAbortGameFlow())
return false;
- while (!_vm->shouldQuit()) {
+ while (!shouldAbortGameFlow()) {
resetLevelAttemptState(3, 0, 17, true);
// Walker-specific state â RunLevel8Flow (0x18546)
@@ -837,13 +841,13 @@ bool InsaneRebel1::runLevel8() {
int route = 0;
int32 routeStartFrame = 0;
- while (!_vm->shouldQuit()) {
+ while (!shouldAbortGameFlow()) {
_levelRouteIndex = route;
_pendingRouteIndex = -1;
_pendingRouteStartFrame = routeStartFrame;
_pendingRouteCutoverFrame = -1;
playInteractiveVideo(kLevel8Routes[route], routeStartFrame);
- if (_vm->shouldQuit())
+ if (shouldAbortGameFlow())
return false;
if (_health < 0)
@@ -870,7 +874,7 @@ bool InsaneRebel1::runLevel8() {
if (_health >= 0) {
playChapterCompleteCinematic("LVL8/L8END.ANM", 8, 0x5F, 5);
- return !_vm->shouldQuit();
+ return !shouldAbortGameFlow();
}
if (playDeathOrRetry("LVL8/L8NEW.ANM", "LVL8/L8DEATH.ANM"))
@@ -898,7 +902,7 @@ bool InsaneRebel1::runLevel9() {
const int randPath2 = getOriginalRouteBit();
const int randPath3 = getOriginalRouteBit();
auto playLevel9PathSelector = [&](const char *filename) {
- while (!_vm->shouldQuit()) {
+ while (!shouldAbortGameFlow()) {
// DOS zeros g_shipOffsetX around the 0x1A-only selector clips.
// Keep the selector cursor centered instead of inheriting the last
// walking offset from the previous stormtrooper segment.
@@ -912,7 +916,7 @@ bool InsaneRebel1::runLevel9() {
_lastHitTarget = 0;
playInteractiveVideo(filename);
- if (_vm->shouldQuit() || _health < 0)
+ if (shouldAbortGameFlow() || _health < 0)
return -1;
if (_killCount > 0)
return (_shipPosX < kRA1CenterX) ? 0 : 1;
@@ -929,10 +933,10 @@ bool InsaneRebel1::runLevel9() {
beginLevelTitleOverlay(8);
playCinematic("LVL9/L9INTRO.ANM");
- if (_vm->shouldQuit())
+ if (shouldAbortGameFlow())
return false;
- while (!_vm->shouldQuit()) {
+ while (!shouldAbortGameFlow()) {
loadTuningForLevel(0x0B);
resetLevelAttemptState(0, 0, 15); // On-foot center direction
_onFootCharX = 0;
@@ -940,21 +944,21 @@ bool InsaneRebel1::runLevel9() {
_onFootAnimCounter = 0;
_onFootInitialized = false;
- while (!_vm->shouldQuit()) {
+ while (!shouldAbortGameFlow()) {
loadTuningForLevel(0x0B);
playInteractiveVideo("LVL9/L9PLAY1.ANM");
- if (_vm->shouldQuit())
+ if (shouldAbortGameFlow())
return false;
if (_health < 0)
break;
playCinematic("LVL9/L9CUT1.ANM");
- if (_vm->shouldQuit())
+ if (shouldAbortGameFlow())
return false;
loadTuningForLevel(0x0C);
const int side1 = playLevel9PathSelector("LVL9/L9PLAY2.ANM");
- if (_vm->shouldQuit())
+ if (shouldAbortGameFlow())
return false;
if (_health < 0)
break;
@@ -963,42 +967,42 @@ bool InsaneRebel1::runLevel9() {
_gameplayFlags75fe |= 4;
playCinematic(side1 == 0 ? "LVL9/L9PLAY2A.ANM" : "LVL9/L9PLAY2B.ANM");
- if (_vm->shouldQuit())
+ if (shouldAbortGameFlow())
return false;
if (side1 == randPath1) {
playCinematic("LVL9/L9CUT2A.ANM");
- if (_vm->shouldQuit())
+ if (shouldAbortGameFlow())
return false;
continue;
}
playCinematic("LVL9/L9CUT2B.ANM");
- if (_vm->shouldQuit())
+ if (shouldAbortGameFlow())
return false;
loadTuningForLevel(0x0B);
playInteractiveVideo("LVL9/L9PLAY3A.ANM");
- if (_vm->shouldQuit())
+ if (shouldAbortGameFlow())
return false;
if (_health < 0)
break;
if (_killCount < 15) {
playInteractiveVideo("LVL9/L9PLAY3B.ANM");
- if (_vm->shouldQuit())
+ if (shouldAbortGameFlow())
return false;
if (_health < 0)
break;
}
playCinematic("LVL9/L9CUT3.ANM");
- if (_vm->shouldQuit())
+ if (shouldAbortGameFlow())
return false;
loadTuningForLevel(0x0C);
const int side2 = playLevel9PathSelector("LVL9/L9PLAY4.ANM");
- if (_vm->shouldQuit())
+ if (shouldAbortGameFlow())
return false;
if (_health < 0)
break;
@@ -1007,28 +1011,28 @@ bool InsaneRebel1::runLevel9() {
_gameplayFlags75fe |= 4;
playCinematic(side2 == 0 ? "LVL9/L9PLAY4A.ANM" : "LVL9/L9PLAY4B.ANM");
- if (_vm->shouldQuit())
+ if (shouldAbortGameFlow())
return false;
if (side2 == randPath2) {
playCinematic(side2 == 0 ? "LVL9/L9CUT4AX.ANM" : "LVL9/L9CUT4B.ANM");
- if (_vm->shouldQuit())
+ if (shouldAbortGameFlow())
return false;
loadTuningForLevel(0x0B);
playInteractiveVideo("LVL9/L9PLAY5.ANM");
- if (_vm->shouldQuit())
+ if (shouldAbortGameFlow())
return false;
if (_health < 0)
break;
playCinematic("LVL9/L9CUT5.ANM");
- if (_vm->shouldQuit())
+ if (shouldAbortGameFlow())
return false;
loadTuningForLevel(0x0C);
const int side3 = playLevel9PathSelector("LVL9/L9PLAY6.ANM");
- if (_vm->shouldQuit())
+ if (shouldAbortGameFlow())
return false;
if (_health < 0)
break;
@@ -1038,29 +1042,29 @@ bool InsaneRebel1::runLevel9() {
_gameplayFlags75fe |= 4;
if (side3 == randPath3) {
playCinematic("LVL9/L9CUT6A.ANM");
- if (_vm->shouldQuit())
+ if (shouldAbortGameFlow())
return false;
continue;
}
playCinematic("LVL9/L9CUT6B.ANM");
- if (_vm->shouldQuit())
+ if (shouldAbortGameFlow())
return false;
} else {
playCinematic(side2 == 0 ? "LVL9/L9CUT4A.ANM" : "LVL9/L9CUT4BX.ANM");
- if (_vm->shouldQuit())
+ if (shouldAbortGameFlow())
return false;
}
loadTuningForLevel(0x0B);
playInteractiveVideo("LVL9/L9PLAY7.ANM");
- if (_vm->shouldQuit())
+ if (shouldAbortGameFlow())
return false;
if (_health < 0)
break;
playChapterCompleteCinematic("LVL9/L9END.ANM", 9, 0x69, 5);
- return !_vm->shouldQuit();
+ return !shouldAbortGameFlow();
}
if (playDeathOrRetry("LVL9/L9NEW.ANM", "LVL9/L9DEATH.ANM"))
@@ -1082,14 +1086,14 @@ bool InsaneRebel1::runLevel10() {
beginLevelTitleOverlay(9);
playCinematic("LVL10/L10INTRO.ANM");
- if (_vm->shouldQuit())
+ if (shouldAbortGameFlow())
return false;
- while (!_vm->shouldQuit()) {
+ while (!shouldAbortGameFlow()) {
resetLevelAttemptState(1, 0);
playInteractiveVideo("LVL10/L10PLAY.ANM");
- if (_vm->shouldQuit())
+ if (shouldAbortGameFlow())
return false;
if (_health >= 0) {
@@ -1098,7 +1102,7 @@ bool InsaneRebel1::runLevel10() {
const int bonus = calculateThresholdBonus(_killCount, 0x3D, 0x32, _tuning.bonus);
playChapterCompleteCinematic("LVL10/L10END.ANM", 10, 0x4B, 5,
" ", accuracyText, bonus, nullptr, nullptr, 0, _difficulty + 7);
- return !_vm->shouldQuit();
+ return !shouldAbortGameFlow();
}
if (playDeathOrRetry("LVL10/L10NEW.ANM", "LVL10/L10DEATH.ANM"))
@@ -1124,17 +1128,17 @@ bool InsaneRebel1::runLevel11() {
beginLevelTitleOverlay(10);
playCinematic("LVL11/L11INTRO.ANM");
- if (_vm->shouldQuit())
+ if (shouldAbortGameFlow())
return false;
- while (!_vm->shouldQuit()) {
+ while (!shouldAbortGameFlow()) {
resetLevelAttemptState(1, 0);
_turretEmitterLeftX = 25;
_turretEmitterLeftY = 15;
- while (!_vm->shouldQuit()) {
+ while (!shouldAbortGameFlow()) {
playInteractiveVideo("LVL11/L11PLAY.ANM");
- if (_vm->shouldQuit())
+ if (shouldAbortGameFlow())
return false;
if (_health < 0)
@@ -1146,7 +1150,7 @@ bool InsaneRebel1::runLevel11() {
// Not enough kills â retry
playCinematic("LVL11/L11RETRY.ANM");
- if (_vm->shouldQuit())
+ if (shouldAbortGameFlow())
return false;
}
@@ -1155,7 +1159,7 @@ bool InsaneRebel1::runLevel11() {
formatTargetAccuracy(accuracyText, sizeof(accuracyText), _killCount, 0x60, true);
const int bonus = calculateThresholdBonus(_killCount, 0x5F, 0x0F, _tuning.bonus);
playChapterCompleteCinematic("LVL11/L11END.ANM", 11, 0x69, 5, " ", accuracyText, bonus);
- return !_vm->shouldQuit();
+ return !shouldAbortGameFlow();
}
if (playDeathOrRetry("LVL11/L11NEW.ANM", "LVL11/L11DEATH.ANM"))
@@ -1180,15 +1184,15 @@ bool InsaneRebel1::runLevel12() {
beginLevelTitleOverlay(11);
playCinematic("LVL12/L12INTRO.ANM");
- if (_vm->shouldQuit())
+ if (shouldAbortGameFlow())
return false;
- while (!_vm->shouldQuit()) {
+ while (!shouldAbortGameFlow()) {
_flyControlMode = 1;
resetLevelDamageState();
_killCount = 0;
- while (!_vm->shouldQuit()) {
+ while (!shouldAbortGameFlow()) {
loadTuningForLevel(0x0F);
resetLevelFrameState();
_damageFlags = 0;
@@ -1199,12 +1203,12 @@ bool InsaneRebel1::runLevel12() {
resetLevelInputHistory();
playInteractiveVideo("LVL12/L12PLAY.ANM");
- if (_vm->shouldQuit())
+ if (shouldAbortGameFlow())
return false;
if (_levelGameplayPhase == 1) {
playCinematic("LVL12/L12RETRY.ANM");
- if (_vm->shouldQuit())
+ if (shouldAbortGameFlow())
return false;
if (_health < 0)
break;
@@ -1218,7 +1222,7 @@ bool InsaneRebel1::runLevel12() {
formatTargetAccuracy(accuracyText, sizeof(accuracyText), _killCount, 0x58, true);
const int bonus = calculateThresholdBonus(_killCount, 0x57, 0x46, _tuning.bonus);
playChapterCompleteCinematic("LVL12/L12END.ANM", 12, 0x69, 5, " ", accuracyText, bonus);
- return !_vm->shouldQuit();
+ return !shouldAbortGameFlow();
}
if (playDeathOrRetry("LVL12/L12NEW.ANM", "LVL12/L12DEATH.ANM"))
@@ -1244,15 +1248,15 @@ bool InsaneRebel1::runLevel13() {
beginLevelTitleOverlay(12);
playCinematic("LVL13/L13INTRO.ANM");
- if (_vm->shouldQuit())
+ if (shouldAbortGameFlow())
return false;
- while (!_vm->shouldQuit()) {
+ while (!shouldAbortGameFlow()) {
resetLevelAttemptState(1, 0);
resetEnemyShotSlots();
playInteractiveVideo("LVL13/L13PLAY.ANM");
- if (_vm->shouldQuit())
+ if (shouldAbortGameFlow())
return false;
if (_health >= 0) {
@@ -1260,7 +1264,7 @@ bool InsaneRebel1::runLevel13() {
formatTargetAccuracy(accuracyText, sizeof(accuracyText), _killCount, 0x146, true);
const int bonus = calculateThresholdBonus(_killCount, 0x145, 0x14, _tuning.bonus);
playChapterCompleteCinematic("LVL13/L13END.ANM", 13, 0x69, 5, " ", accuracyText, bonus);
- return !_vm->shouldQuit();
+ return !shouldAbortGameFlow();
}
if (playDeathOrRetry("LVL13/L13NEW.ANM", "LVL13/L13DEATH.ANM"))
@@ -1286,17 +1290,17 @@ bool InsaneRebel1::runLevel14() {
beginLevelTitleOverlay(13);
playCinematic("LVL14/L14INTRO.ANM");
- if (_vm->shouldQuit())
+ if (shouldAbortGameFlow())
return false;
- while (!_vm->shouldQuit()) {
+ while (!shouldAbortGameFlow()) {
loadTuningForLevel(0x11);
resetLevelAttemptState(1, 1);
_level14SuccessFrames = 0;
// Phase 1: targeting surface cannons
playInteractiveVideo("LVL14/L14PLAY.ANM");
- if (_vm->shouldQuit())
+ if (shouldAbortGameFlow())
return false;
if (_health >= 0) {
@@ -1314,7 +1318,7 @@ bool InsaneRebel1::runLevel14() {
_level14Play2BSpliceFrame = 0;
playInteractiveVideo("LVL14/L14PLAY2.ANM");
- if (_vm->shouldQuit())
+ if (shouldAbortGameFlow())
return false;
if (_health >= 0 && _level14Play2BSplicePending) {
@@ -1326,7 +1330,7 @@ bool InsaneRebel1::runLevel14() {
// continuation clip, so the port starts it from frame 0 but uses
// the non-zero frame argument to preserve gameplay/video state.
playInteractiveVideo("LVL14/L14PLY2B.ANM", spliceFrame);
- if (_vm->shouldQuit())
+ if (shouldAbortGameFlow())
return false;
}
}
@@ -1334,7 +1338,7 @@ bool InsaneRebel1::runLevel14() {
if (_health >= 0) {
playChapterCompleteCinematic("LVL14/L14END.ANM", 14, 0x69, 5,
nullptr, nullptr, 0, nullptr, nullptr, 0, _difficulty + 10);
- return !_vm->shouldQuit();
+ return !shouldAbortGameFlow();
}
if (playDeathOrRetry("LVL14/L14NEW.ANM", "LVL14/L14DEATH.ANM"))
@@ -1360,23 +1364,23 @@ bool InsaneRebel1::runLevel15() {
beginLevelTitleOverlay(14);
playCinematic("LVL15/L15INTRO.ANM");
- if (_vm->shouldQuit())
+ if (shouldAbortGameFlow())
return false;
- while (!_vm->shouldQuit()) {
+ while (!shouldAbortGameFlow()) {
loadTuningForLevel(0x13);
resetLevelAttemptState(1, 0);
// Phase 1: trench run
_levelGameplayPhase = 1;
playInteractiveVideo("LVL15/L15PLAY1.ANM");
- if (_vm->shouldQuit())
+ if (shouldAbortGameFlow())
return false;
if (_health >= 0) {
// Torpedo lock cutscene
playCinematic("LVL15/L15INTR2.ANM");
- if (_vm->shouldQuit())
+ if (shouldAbortGameFlow())
return false;
// Phase 2: final approach and torpedo shot. The DOS flow enables
@@ -1392,7 +1396,7 @@ bool InsaneRebel1::runLevel15() {
_levelGameplayPhase = 2;
playInteractiveVideo("LVL15/L15PLAY2.ANM");
- if (_vm->shouldQuit())
+ if (shouldAbortGameFlow())
return false;
}
@@ -1413,7 +1417,7 @@ bool InsaneRebel1::runLevel15() {
playChapterCompleteCinematic("LVL15/L15END1.ANM", 15, 0x122, 0xA5,
"Part I", accuracyText, targetBonus,
"Part II", "Torpedo on mark", 10000, _difficulty + 13);
- return !_vm->shouldQuit();
+ return !shouldAbortGameFlow();
}
if (playDeathOrRetry("LVL15/L15NEW.ANM", "LVL15/L15DEATH.ANM"))
@@ -1447,52 +1451,85 @@ void InsaneRebel1::runGame() {
&InsaneRebel1::runLevel15
};
const int numLevels = (int)(sizeof(kLevelRunners) / sizeof(kLevelRunners[0]));
- auto runLevelsFrom = [&](int startLevel) {
- const int firstLevel = CLIP<int>(startLevel, 1, numLevels);
- bool completed = true;
- int lastCompletedLevel = 0;
- _health = kMaxHealth;
- _lives = 3;
- _score = 0;
- _prevScore = 0;
-
- for (int level = firstLevel;
- level <= numLevels && completed && !_vm->shouldQuit();
- ++level) {
- playLevelTransitionCutscene(level);
- if (_vm->shouldQuit())
- break;
+ auto runLevelsFrom = [&](int startLevel, bool resetRunState) {
+ int firstLevel = CLIP<int>(startLevel, 1, numLevels);
+
+ while (!_vm->shouldQuit()) {
+ _loadRequested = false;
+ bool completed = true;
+ int lastCompletedLevel = 0;
- completed = (this->*kLevelRunners[level - 1])();
- if (completed) {
- lastCompletedLevel = level;
- if (level < numLevels)
- _startLevel = level + 1;
+ _health = kMaxHealth;
+ if (resetRunState) {
+ _lives = 3;
+ _score = 0;
+ _prevScore = 0;
+ }
+ resetRunState = false;
+
+ for (int level = firstLevel;
+ level <= numLevels && completed && !shouldAbortGameFlow();
+ ++level) {
+ _resumeLevel = level;
+ playLevelTransitionCutscene(level);
+ if (shouldAbortGameFlow())
+ break;
+
+ completed = (this->*kLevelRunners[level - 1])();
+ if (completed) {
+ lastCompletedLevel = level;
+ if (level < numLevels) {
+ _startLevel = level + 1;
+ _resumeLevel = _startLevel;
+ autosaveProgress();
+ }
+ }
}
- }
- if (!_vm->shouldQuit() && completed && lastCompletedLevel == numLevels)
- playCinematic("FIN/FNFINAL.ANM");
- if (!_vm->shouldQuit())
- runHighScoreNameEntry();
- _currentLevel = 0;
+ if (_loadRequested) {
+ firstLevel = getCurrentSaveLevel();
+ continue;
+ }
+
+ if (!shouldAbortGameFlow() && completed && lastCompletedLevel == numLevels)
+ playCinematic("FIN/FNFINAL.ANM");
+ if (!shouldAbortGameFlow())
+ runHighScoreNameEntry();
+ _currentLevel = 0;
+ return;
+ }
};
- // Play intro sequence (logo + opening)
- playIntroSequence();
- if (_vm->shouldQuit())
- return;
+ bool loadedStartupSave = false;
+ if (ConfMan.hasKey("save_slot")) {
+ const int saveSlot = ConfMan.getInt("save_slot");
+ if (loadGameState(saveSlot, true).getCode() == Common::kNoError) {
+ loadedStartupSave = true;
+ runLevelsFrom(_resumeLevel, false);
+ }
+ }
+
+ if (!loadedStartupSave) {
+ // Play intro sequence (logo + opening)
+ playIntroSequence();
+ if (shouldAbortGameFlow())
+ return;
+ }
// Main menu â gameplay loop
while (!_vm->shouldQuit()) {
int menuResult = runMainMenu();
+ if (_loadRequested) {
+ runLevelsFrom(_resumeLevel, false);
+ continue;
+ }
if (_vm->shouldQuit())
return;
switch (menuResult) {
case 1: {
// START NEW GAME â sequential play from _startLevel
- runLevelsFrom(_startLevel);
+ runLevelsFrom(_startLevel, true);
break;
}
case 2:
@@ -1504,14 +1541,14 @@ void InsaneRebel1::runGame() {
// game from the decoded chapter when a valid passcode is entered.
const int passcodeLevel = runPasscodeEntryDialog();
if (passcodeLevel >= 1 && passcodeLevel <= numLevels)
- runLevelsFrom(passcodeLevel);
+ runLevelsFrom(passcodeLevel, true);
else if (passcodeLevel == numLevels + 1) {
_health = kMaxHealth;
_lives = 3;
_score = 0;
_prevScore = 0;
playCinematic("FIN/FNFINAL.ANM");
- if (!_vm->shouldQuit())
+ if (!shouldAbortGameFlow())
runHighScoreNameEntry();
_currentLevel = 0;
}
@@ -1522,14 +1559,14 @@ void InsaneRebel1::runGame() {
// original successor flow so post-level cinematics still play.
int selectedLevel = runLevelSelectMenu();
if (selectedLevel >= 1 && selectedLevel <= numLevels)
- runLevelsFrom(selectedLevel);
+ runLevelsFrom(selectedLevel, true);
break;
}
case 5:
// CONTINUE DEMO â attract mode.
// Original shows TOP PILOTS (O1SCORE.ANM) then loops O1OPEN.ANM.
showHighScores();
- if (!_vm->shouldQuit())
+ if (!shouldAbortGameFlow())
playCinematic("OPEN/O1OPEN.ANM");
break;
case 6:
@@ -1654,6 +1691,9 @@ void InsaneRebel1::playInteractiveVideoFile(const char *filename, int32 videoOff
// Play interactive gameplay video (with ship physics + HUD).
void InsaneRebel1::playInteractiveVideo(const char *filename, int32 startFrame) {
debug(1, "InsaneRebel1::playInteractiveVideo('%s', startFrame=%d)", filename, startFrame);
+ if (shouldAbortGameFlow())
+ return;
+
int32 videoStartFrame = 0;
int32 videoOffset = 0;
diff --git a/engines/scumm/insane/rebel1/saveload.cpp b/engines/scumm/insane/rebel1/saveload.cpp
new file mode 100644
index 00000000000..06849925a7f
--- /dev/null
+++ b/engines/scumm/insane/rebel1/saveload.cpp
@@ -0,0 +1,254 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include "common/ptr.h"
+#include "common/stream.h"
+#include "common/endian.h"
+
+#include "graphics/thumbnail.h"
+
+#include "scumm/scumm.h"
+#include "scumm/scumm_v7.h"
+#include "scumm/insane/rebel1/rebel.h"
+
+namespace Scumm {
+
+const uint32 kRA1SaveTag = MKTAG('R', 'A', '1', 'S');
+const uint32 kRA1SaveVersion = 1;
+const uint32 kRA1ScummSaveVersion = 124;
+
+struct Rebel1SavegameHeader {
+ uint32 type;
+ uint32 size;
+ uint32 version;
+ char name[32];
+};
+
+bool writeRebel1SavegameHeader(Common::WriteStream *out, const Common::String &desc) {
+ Rebel1SavegameHeader hdr;
+ hdr.type = MKTAG('S', 'C', 'V', 'M');
+ hdr.size = 0;
+ hdr.version = kRA1ScummSaveVersion;
+ memset(hdr.name, 0, sizeof(hdr.name));
+ Common::strlcpy(hdr.name, desc.c_str(), sizeof(hdr.name));
+
+ out->writeUint32BE(hdr.type);
+ out->writeUint32LE(hdr.size);
+ out->writeUint32LE(hdr.version);
+ out->write(hdr.name, sizeof(hdr.name));
+ return !out->err();
+}
+
+bool readRebel1SavegameHeader(Common::SeekableReadStream *in, Common::String *desc, uint32 *version) {
+ Rebel1SavegameHeader hdr;
+ hdr.type = in->readUint32BE();
+ hdr.size = in->readUint32LE();
+ hdr.version = in->readUint32LE();
+ in->read(hdr.name, sizeof(hdr.name));
+ if (in->err() || hdr.type != MKTAG('S', 'C', 'V', 'M'))
+ return false;
+
+ if (hdr.version > 0xFFFFFF)
+ hdr.version = SWAP_BYTES_32(hdr.version);
+ if (hdr.version < VER(52) || hdr.version > kRA1ScummSaveVersion)
+ return false;
+
+ hdr.name[sizeof(hdr.name) - 1] = 0;
+ if (desc)
+ *desc = hdr.name;
+ if (version)
+ *version = hdr.version;
+ return true;
+}
+
+int InsaneRebel1::getCurrentSaveLevel() const {
+ return CLIP<int>(_resumeLevel, 1, kNumLevels);
+}
+
+int InsaneRebel1::getAutosaveTargetSlot() const {
+ return _activeSaveSlot >= 0 ? _activeSaveSlot : 0;
+}
+
+Common::Error InsaneRebel1::writeSaveState(int slot, const Common::String &desc, const SaveState &state) const {
+ Common::String filename;
+ Common::SeekableWriteStream *out = _vm->openSaveFileForWriting(slot, false, filename);
+ if (!out)
+ return Common::kWritingFailed;
+
+ writeRebel1SavegameHeader(out, desc);
+ Graphics::saveThumbnail(*out);
+ _vm->saveInfos(out);
+
+ out->writeUint32BE(kRA1SaveTag);
+ out->writeUint32LE(kRA1SaveVersion);
+ out->writeSint16LE((int16)CLIP<int>(state.resumeLevel, 1, kNumLevels));
+ out->writeSint16LE((int16)MAX<int>(state.lives, 0));
+ out->writeSint32LE(state.score);
+ out->writeSint32LE(state.prevScore);
+ out->writeSint16LE((int16)CLIP<int>(state.difficulty, 0, 2));
+ out->writeSint16LE((int16)CLIP<int>(state.maxChapterUnlocked, 0, kNumLevels));
+
+ out->finalize();
+ const bool failed = out->err();
+ delete out;
+
+ if (failed) {
+ warning("RA1: failed to write save '%s'", filename.c_str());
+ return Common::kWritingFailed;
+ }
+
+ debug(1, "RA1: saved slot=%d level=%d lives=%d score=%d desc='%s'",
+ slot, state.resumeLevel, state.lives, state.score, desc.c_str());
+ return Common::kNoError;
+}
+
+bool InsaneRebel1::readSaveState(int slot, SaveState &state, Common::String *desc) const {
+ Common::String filename;
+ Common::ScopedPtr<Common::SeekableReadStream> in(_vm->openSaveFileForReading(slot, false, filename));
+ if (!in)
+ return false;
+
+ uint32 headerVersion = 0;
+ if (!readRebel1SavegameHeader(in.get(), desc, &headerVersion)) {
+ warning("RA1: invalid save header in '%s'", filename.c_str());
+ return false;
+ }
+
+ if (headerVersion >= VER(52) && !Graphics::skipThumbnail(*in)) {
+ warning("RA1: save thumbnail could not be skipped in '%s'", filename.c_str());
+ return false;
+ }
+
+ if (headerVersion >= VER(56)) {
+ SaveStateMetaInfos infos;
+ if (!_vm->loadInfos(in.get(), &infos)) {
+ warning("RA1: save info section could not be found in '%s'", filename.c_str());
+ return false;
+ }
+ _vm->setTotalPlayTime(infos.playtime * 1000);
+ } else {
+ _vm->setTotalPlayTime();
+ }
+
+ if (in->readUint32BE() != kRA1SaveTag) {
+ warning("RA1: missing RA1 save data in '%s'", filename.c_str());
+ return false;
+ }
+
+ const uint32 version = in->readUint32LE();
+ if (version == 0 || version > kRA1SaveVersion) {
+ warning("RA1: unsupported save version %u in '%s'", version, filename.c_str());
+ return false;
+ }
+
+ state.resumeLevel = CLIP<int>(in->readSint16LE(), 1, kNumLevels);
+ state.lives = MAX<int>(in->readSint16LE(), 0);
+ state.score = in->readSint32LE();
+ state.prevScore = in->readSint32LE();
+ state.difficulty = CLIP<int>(in->readSint16LE(), 0, 2);
+ state.maxChapterUnlocked = CLIP<int>(in->readSint16LE(), 0, kNumLevels);
+
+ if (in->err()) {
+ warning("RA1: truncated save data in '%s'", filename.c_str());
+ return false;
+ }
+
+ return true;
+}
+
+Common::Error InsaneRebel1::saveGameState(int slot, const Common::String &desc, bool isAutosave) {
+ if (isAutosave) {
+ autosaveProgress();
+ return Common::kNoError;
+ }
+
+ SaveState state;
+ state.resumeLevel = getCurrentSaveLevel();
+ state.lives = _lives;
+ state.score = _score;
+ state.prevScore = _prevScore;
+ state.difficulty = _difficulty;
+ state.maxChapterUnlocked = _maxChapterUnlocked;
+
+ Common::String saveDesc = desc;
+ if (saveDesc.empty())
+ saveDesc = Common::String::format("Level %d", state.resumeLevel);
+
+ Common::Error result = writeSaveState(slot, saveDesc, state);
+ if (result.getCode() == Common::kNoError)
+ _activeSaveSlot = slot;
+ return result;
+}
+
+Common::Error InsaneRebel1::loadGameState(int slot, bool startupLoad) {
+ SaveState state;
+ if (!readSaveState(slot, state))
+ return Common::kReadingFailed;
+
+ _resumeLevel = state.resumeLevel;
+ _startLevel = state.resumeLevel;
+ _lives = state.lives;
+ _score = state.score;
+ _prevScore = state.prevScore;
+ _difficulty = state.difficulty;
+ _maxChapterUnlocked = state.maxChapterUnlocked;
+ _health = kMaxHealth;
+ _activeSaveSlot = slot;
+
+ loadTuningForLevel(_resumeLevel - 1);
+ if (!startupLoad) {
+ _loadRequested = true;
+ _vm->_smushVideoShouldFinish = true;
+ }
+
+ debug(1, "RA1: loaded slot=%d level=%d lives=%d score=%d", slot, _resumeLevel, _lives, _score);
+ return Common::kNoError;
+}
+
+void InsaneRebel1::autosaveProgress() {
+ SaveState state;
+ state.resumeLevel = getCurrentSaveLevel();
+ state.lives = _lives;
+ state.score = _score;
+ state.prevScore = _prevScore;
+ state.difficulty = _difficulty;
+ state.maxChapterUnlocked = _maxChapterUnlocked;
+
+ const int slot = getAutosaveTargetSlot();
+ SaveState oldState;
+ Common::String oldDesc;
+ if (readSaveState(slot, oldState, &oldDesc)) {
+ if (oldState.resumeLevel > state.resumeLevel ||
+ (oldState.resumeLevel == state.resumeLevel && oldState.lives >= state.lives)) {
+ debug(1, "RA1: skipping autosave slot=%d level=%d lives=%d; existing level=%d lives=%d",
+ slot, state.resumeLevel, state.lives, oldState.resumeLevel, oldState.lives);
+ return;
+ }
+ }
+
+ Common::String desc = oldDesc;
+ if (slot == 0 || desc.empty())
+ desc = "Autosave";
+
+ (void)writeSaveState(slot, desc, state);
+}
+
+} // End of namespace Scumm
diff --git a/engines/scumm/module.mk b/engines/scumm/module.mk
index b77e4a876a9..e7fe13f0263 100644
--- a/engines/scumm/module.mk
+++ b/engines/scumm/module.mk
@@ -141,6 +141,7 @@ MODULE_OBJS += \
insane/rebel1/menu.o \
insane/rebel1/render.o \
insane/rebel1/runlevels.o \
+ insane/rebel1/saveload.o \
insane/rebel2/rebel.o \
insane/rebel2/audio.o \
insane/rebel2/iact.o \
diff --git a/engines/scumm/saveload.cpp b/engines/scumm/saveload.cpp
index 7d050a27045..55af040617a 100644
--- a/engines/scumm/saveload.cpp
+++ b/engines/scumm/saveload.cpp
@@ -42,6 +42,10 @@
#include "scumm/he/sprite_he.h"
#include "scumm/verbs.h"
+#ifdef ENABLE_SCUMM_7_8
+#include "scumm/insane/rebel1/rebel.h"
+#endif
+
#include "backends/audiocd/audiocd.h"
#include "graphics/thumbnail.h"
@@ -76,6 +80,14 @@ struct SaveInfoSection {
#pragma mark -
Common::Error ScummEngine::loadGameState(int slot) {
+#ifdef ENABLE_SCUMM_7_8
+ if (_game.id == GID_REBEL1) {
+ InsaneRebel1 *rebel = (InsaneRebel1 *)((ScummEngine_v7 *)this)->getInsane();
+ if (rebel)
+ return rebel->loadGameState(slot);
+ }
+#endif
+
requestLoad(slot);
return Common::kNoError;
}
@@ -84,6 +96,11 @@ bool ScummEngine::canLoadGameStateCurrently(Common::U32String *msg) {
if (!_setupIsComplete)
return false;
+#ifdef ENABLE_SCUMM_7_8
+ if (_game.id == GID_REBEL1)
+ return true;
+#endif
+
// FIXME: For now always allow loading in V0-V3 games
// FIXME: Actually, we might wish to support loading in more places.
// As long as we are sure it won't cause any problems... Are we
@@ -139,6 +156,14 @@ bool ScummEngine::canLoadGameStateCurrently(Common::U32String *msg) {
}
Common::Error ScummEngine::saveGameState(int slot, const Common::String &desc, bool isAutosave) {
+#ifdef ENABLE_SCUMM_7_8
+ if (_game.id == GID_REBEL1) {
+ InsaneRebel1 *rebel = (InsaneRebel1 *)((ScummEngine_v7 *)this)->getInsane();
+ if (rebel)
+ return rebel->saveGameState(slot, desc, isAutosave);
+ }
+#endif
+
requestSave(slot, desc);
return Common::kNoError;
}
@@ -147,6 +172,11 @@ bool ScummEngine::canSaveGameStateCurrently(Common::U32String *msg) {
if (!_setupIsComplete)
return false;
+#ifdef ENABLE_SCUMM_7_8
+ if (_game.id == GID_REBEL1)
+ return true;
+#endif
+
// Disallow saving in v0-v3 games when a 'prequel' to a cutscene is shown.
// This is a blank screen with text, and while this is shown, saving should
// be disabled, as no room is set.
Commit: 9e6422b20a72b3e6107217c7feba9b0227ecf7f9
https://github.com/scummvm/scummvm/commit/9e6422b20a72b3e6107217c7feba9b0227ecf7f9
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:10:14+02:00
Commit Message:
SCUMM: RA2: Avoid direct OSystem mouse call
Changed paths:
engines/scumm/insane/rebel2/render.cpp
diff --git a/engines/scumm/insane/rebel2/render.cpp b/engines/scumm/insane/rebel2/render.cpp
index 5746004151c..e9b35a408b6 100644
--- a/engines/scumm/insane/rebel2/render.cpp
+++ b/engines/scumm/insane/rebel2/render.cpp
@@ -2126,7 +2126,6 @@ void InsaneRebel2::renderPostRenderMenuCursor(byte *renderBitmap, int pitch, int
const int cursorHeight = 10;
CursorMan.showMouse(false);
- _vm->_system->showMouse(false);
const int cursorX = _vm->_mouse.x;
const int cursorY = _vm->_mouse.y;
More information about the Scummvm-git-logs
mailing list