diff --git a/README.md b/README.md index febaa89..f4a7b2a 100644 --- a/README.md +++ b/README.md @@ -35,6 +35,37 @@ To run CoDxe, you will need one of the following: | Raw map ents loader | :x: | :x: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | | Raw `.dds` texture loader | :x: | :x: | :x: | :white_check_mark: | :x: | :x: | :x: | +## Call of Duty 4 + +Loading single player maps is not stable so things may or may not work. + +| Name | Xbox 360 | Xenia | +| ------------------- | :------: | :---: | +| ac130.ff | 🟢 | 🟢 | +| aftermath.ff | 🟢 | 🟢 | +| airlift.ff | ❌ | 🟢 | +| airplane.ff | 🟢 | 🟢 | +| ambush.ff | ❌ | 🟢 | +| armada.ff | ❌ | ❌ | +| blackout.ff | ❌ | 🟢 | +| bog_a.ff | ❌ | 🟢 | +| bog_b.ff | ❌ | 🟢 | +| cargoship.ff | 🟢 | 🟢 | +| coup.ff | ❌ | 🟢 | +| hunted.ff | ❌ | 🟢 | +| icbm.ff | ❌ | 🟢 | +| jeepride.ff | ❌ | 🟢 | +| killhouse.ff | ❌ | 🟢 | +| launchfacility_a.ff | ❌ | 🟢 | +| launchfacility_b.ff | ❌ | 🟢 | +| scoutsniper.ff | ❌ | 🟢 | +| simplecredits.ff | ❌ | ❌ | +| sniperescape.ff | ❌ | 🟢 | +| village_assault.ff | ❌ | 🟢 | +| village_defend.ff | ❌ | 🟢 | + +NOTE: Xenia requires the patch [patch]() to increase memory installed. + ## Features ### GSC Loader diff --git a/codxe.vcxproj b/codxe.vcxproj index 8bf6e03..b554b8d 100644 --- a/codxe.vcxproj +++ b/codxe.vcxproj @@ -93,6 +93,7 @@ + diff --git a/resources/iw3/_codxe/internal/maps/ac130.d3dbsp.ents b/resources/iw3/_codxe/internal/maps/ac130.d3dbsp.ents new file mode 100644 index 0000000..ba49fd7 --- /dev/null +++ b/resources/iw3/_codxe/internal/maps/ac130.d3dbsp.ents @@ -0,0 +1,23 @@ +{ +"classname" "worldspawn" +} +{ +"origin" "5885.9 9506.2 125.7" +"angles" "0 0 0" +"classname" "mp_global_intermission" +} +{ +"origin" "5885.9 9506.2 125.7" +"angles" "0 0 0" +"classname" "mp_tdm_spawn_allies_start" +} +{ +"origin" "5885.9 9506.2 125.7" +"angles" "0 0 0" +"classname" "mp_tdm_spawn_axis_start" +} +{ +"origin" "5885.9 9506.2 125.7" +"angles" "0 0 0" +"classname" "mp_tdm_spawn" +} diff --git a/resources/iw3/_codxe/internal/maps/ac130.gsc b/resources/iw3/_codxe/internal/maps/ac130.gsc new file mode 100644 index 0000000..54dc4c5 --- /dev/null +++ b/resources/iw3/_codxe/internal/maps/ac130.gsc @@ -0,0 +1,3 @@ +main() +{ +} diff --git a/resources/iw3/_codxe/internal/maps/aftermath.d3dbsp.ents b/resources/iw3/_codxe/internal/maps/aftermath.d3dbsp.ents new file mode 100644 index 0000000..5a1baf1 --- /dev/null +++ b/resources/iw3/_codxe/internal/maps/aftermath.d3dbsp.ents @@ -0,0 +1,23 @@ +{ +"classname" "worldspawn" +} +{ +"origin" "-572 8502 662.3" +"angles" "0 0 0" +"classname" "mp_global_intermission" +} +{ +"origin" "-572 8502 662.3" +"angles" "0 0 0" +"classname" "mp_tdm_spawn_allies_start" +} +{ +"origin" "-572 8502 662.3" +"angles" "0 0 0" +"classname" "mp_tdm_spawn_axis_start" +} +{ +"origin" "-572 8502 662.3" +"angles" "0 0 0" +"classname" "mp_tdm_spawn" +} diff --git a/resources/iw3/_codxe/internal/maps/aftermath.gsc b/resources/iw3/_codxe/internal/maps/aftermath.gsc new file mode 100644 index 0000000..54dc4c5 --- /dev/null +++ b/resources/iw3/_codxe/internal/maps/aftermath.gsc @@ -0,0 +1,3 @@ +main() +{ +} diff --git a/resources/iw3/_codxe/internal/maps/airlift.d3dbsp.ents b/resources/iw3/_codxe/internal/maps/airlift.d3dbsp.ents new file mode 100644 index 0000000..0b2f97e --- /dev/null +++ b/resources/iw3/_codxe/internal/maps/airlift.d3dbsp.ents @@ -0,0 +1,24 @@ + +{ +"classname" "worldspawn" +} +{ +"origin" "-15355 787 600" +"angles" "0 203 0" +"classname" "mp_global_intermission" +} +{ +"origin" "-15355 787 600" +"angles" "0 270 0" +"classname" "mp_tdm_spawn_allies_start" +} +{ +"origin" "-15355 787 600" +"angles" "0 270 0" +"classname" "mp_tdm_spawn_axis_start" +} +{ +"origin" "-15355 787 600" +"angles" "0 270 0" +"classname" "mp_tdm_spawn" +} diff --git a/resources/iw3/_codxe/internal/maps/airlift.gsc b/resources/iw3/_codxe/internal/maps/airlift.gsc new file mode 100644 index 0000000..54dc4c5 --- /dev/null +++ b/resources/iw3/_codxe/internal/maps/airlift.gsc @@ -0,0 +1,3 @@ +main() +{ +} diff --git a/resources/iw3/_codxe/internal/maps/airplane.d3dbsp.ents b/resources/iw3/_codxe/internal/maps/airplane.d3dbsp.ents new file mode 100644 index 0000000..ddbfe78 --- /dev/null +++ b/resources/iw3/_codxe/internal/maps/airplane.d3dbsp.ents @@ -0,0 +1,23 @@ +{ +"classname" "worldspawn" +} +{ +"origin" "-1392 544 304" +"angles" "0 0 0" +"classname" "mp_global_intermission" +} +{ +"origin" "-1392 544 304" +"angles" "0 0 0" +"classname" "mp_tdm_spawn_allies_start" +} +{ +"origin" "-1392 544 304" +"angles" "0 0 0" +"classname" "mp_tdm_spawn_axis_start" +} +{ +"origin" "-1392 544 304" +"angles" "0 0 0" +"classname" "mp_tdm_spawn" +} diff --git a/resources/iw3/_codxe/internal/maps/airplane.gsc b/resources/iw3/_codxe/internal/maps/airplane.gsc new file mode 100644 index 0000000..54dc4c5 --- /dev/null +++ b/resources/iw3/_codxe/internal/maps/airplane.gsc @@ -0,0 +1,3 @@ +main() +{ +} diff --git a/resources/iw3/_codxe/internal/maps/ambush.d3dbsp.ents b/resources/iw3/_codxe/internal/maps/ambush.d3dbsp.ents new file mode 100644 index 0000000..06f16c9 --- /dev/null +++ b/resources/iw3/_codxe/internal/maps/ambush.d3dbsp.ents @@ -0,0 +1,23 @@ +{ +"classname" "worldspawn" +} +{ +"origin" "150.7 -2741.1 51.9" +"angles" "0 0 0" +"classname" "mp_global_intermission" +} +{ +"origin" "150.7 -2741.1 51.9" +"angles" "0 0 0" +"classname" "mp_tdm_spawn_allies_start" +} +{ +"origin" "150.7 -2741.1 51.9" +"angles" "0 0 0" +"classname" "mp_tdm_spawn_axis_start" +} +{ +"origin" "150.7 -2741.1 51.9" +"angles" "0 0 0" +"classname" "mp_tdm_spawn" +} diff --git a/resources/iw3/_codxe/internal/maps/ambush.gsc b/resources/iw3/_codxe/internal/maps/ambush.gsc new file mode 100644 index 0000000..54dc4c5 --- /dev/null +++ b/resources/iw3/_codxe/internal/maps/ambush.gsc @@ -0,0 +1,3 @@ +main() +{ +} diff --git a/resources/iw3/_codxe/internal/maps/armada.d3dbsp.ents b/resources/iw3/_codxe/internal/maps/armada.d3dbsp.ents new file mode 100644 index 0000000..a6c0300 --- /dev/null +++ b/resources/iw3/_codxe/internal/maps/armada.d3dbsp.ents @@ -0,0 +1,23 @@ +{ +"classname" "worldspawn" +} +{ +"origin" "685 31203.5 246" +"angles" "0 0 0" +"classname" "mp_global_intermission" +} +{ +"origin" "685 31203.5 246" +"angles" "0 0 0" +"classname" "mp_tdm_spawn_allies_start" +} +{ +"origin" "685 31203.5 246" +"angles" "0 0 0" +"classname" "mp_tdm_spawn_axis_start" +} +{ +"origin" "685 31203.5 246" +"angles" "0 0 0" +"classname" "mp_tdm_spawn" +} diff --git a/resources/iw3/_codxe/internal/maps/armada.gsc b/resources/iw3/_codxe/internal/maps/armada.gsc new file mode 100644 index 0000000..54dc4c5 --- /dev/null +++ b/resources/iw3/_codxe/internal/maps/armada.gsc @@ -0,0 +1,3 @@ +main() +{ +} diff --git a/resources/iw3/_codxe/internal/maps/blackout.d3dbsp.ents b/resources/iw3/_codxe/internal/maps/blackout.d3dbsp.ents new file mode 100644 index 0000000..9180656 --- /dev/null +++ b/resources/iw3/_codxe/internal/maps/blackout.d3dbsp.ents @@ -0,0 +1,23 @@ +{ +"classname" "worldspawn" +} +{ +"origin" "-6309 -1935 312" +"angles" "0 0 0" +"classname" "mp_global_intermission" +} +{ +"origin" "-6309 -1935 312" +"angles" "0 0 0" +"classname" "mp_tdm_spawn_allies_start" +} +{ +"origin" "-6309 -1935 312" +"angles" "0 0 0" +"classname" "mp_tdm_spawn_axis_start" +} +{ +"origin" "-6309 -1935 312" +"angles" "0 0 0" +"classname" "mp_tdm_spawn" +} diff --git a/resources/iw3/_codxe/internal/maps/blackout.gsc b/resources/iw3/_codxe/internal/maps/blackout.gsc new file mode 100644 index 0000000..54dc4c5 --- /dev/null +++ b/resources/iw3/_codxe/internal/maps/blackout.gsc @@ -0,0 +1,3 @@ +main() +{ +} diff --git a/resources/iw3/_codxe/internal/maps/bog_a.d3dbsp.ents b/resources/iw3/_codxe/internal/maps/bog_a.d3dbsp.ents new file mode 100644 index 0000000..40dbea1 --- /dev/null +++ b/resources/iw3/_codxe/internal/maps/bog_a.d3dbsp.ents @@ -0,0 +1,23 @@ +{ +"classname" "worldspawn" +} +{ +"origin" "12086.6 4372.7 202" +"angles" "0 0 0" +"classname" "mp_global_intermission" +} +{ +"origin" "12086.6 4372.7 202" +"angles" "0 0 0" +"classname" "mp_tdm_spawn_allies_start" +} +{ +"origin" "12086.6 4372.7 202" +"angles" "0 0 0" +"classname" "mp_tdm_spawn_axis_start" +} +{ +"origin" "12086.6 4372.7 202" +"angles" "0 0 0" +"classname" "mp_tdm_spawn" +} diff --git a/resources/iw3/_codxe/internal/maps/bog_a.gsc b/resources/iw3/_codxe/internal/maps/bog_a.gsc new file mode 100644 index 0000000..c2d7806 --- /dev/null +++ b/resources/iw3/_codxe/internal/maps/bog_a.gsc @@ -0,0 +1,3 @@ +main() +{ +} \ No newline at end of file diff --git a/resources/iw3/_codxe/internal/maps/bog_b.d3dbsp.ents b/resources/iw3/_codxe/internal/maps/bog_b.d3dbsp.ents new file mode 100644 index 0000000..65a8965 --- /dev/null +++ b/resources/iw3/_codxe/internal/maps/bog_b.d3dbsp.ents @@ -0,0 +1,23 @@ +{ +"classname" "worldspawn" +} +{ +"origin" "6008 1312 60.7" +"angles" "0 0 0" +"classname" "mp_global_intermission" +} +{ +"origin" "6008 1312 60.7" +"angles" "0 0 0" +"classname" "mp_tdm_spawn_allies_start" +} +{ +"origin" "6008 1312 60.7" +"angles" "0 0 0" +"classname" "mp_tdm_spawn_axis_start" +} +{ +"origin" "6008 1312 60.7" +"angles" "0 0 0" +"classname" "mp_tdm_spawn" +} diff --git a/resources/iw3/_codxe/internal/maps/bog_b.gsc b/resources/iw3/_codxe/internal/maps/bog_b.gsc new file mode 100644 index 0000000..c2d7806 --- /dev/null +++ b/resources/iw3/_codxe/internal/maps/bog_b.gsc @@ -0,0 +1,3 @@ +main() +{ +} \ No newline at end of file diff --git a/resources/iw3/_codxe/internal/maps/cargoship.d3dbsp.ents b/resources/iw3/_codxe/internal/maps/cargoship.d3dbsp.ents new file mode 100644 index 0000000..d7e9a49 --- /dev/null +++ b/resources/iw3/_codxe/internal/maps/cargoship.d3dbsp.ents @@ -0,0 +1,23 @@ +{ +"classname" "worldspawn" +} +{ +"origin" "-2048 208 -352" +"angles" "0 0 0" +"classname" "mp_global_intermission" +} +{ +"origin" "-2048 208 -352" +"angles" "0 0 0" +"classname" "mp_tdm_spawn_allies_start" +} +{ +"origin" "-2048 208 -352" +"angles" "0 0 0" +"classname" "mp_tdm_spawn_axis_start" +} +{ +"origin" "-2048 208 -352" +"angles" "0 0 0" +"classname" "mp_tdm_spawn" +} diff --git a/resources/iw3/_codxe/internal/maps/cargoship.gsc b/resources/iw3/_codxe/internal/maps/cargoship.gsc new file mode 100644 index 0000000..54dc4c5 --- /dev/null +++ b/resources/iw3/_codxe/internal/maps/cargoship.gsc @@ -0,0 +1,3 @@ +main() +{ +} diff --git a/resources/iw3/_codxe/internal/maps/coup.d3dbsp.ents b/resources/iw3/_codxe/internal/maps/coup.d3dbsp.ents new file mode 100644 index 0000000..fab20ef --- /dev/null +++ b/resources/iw3/_codxe/internal/maps/coup.d3dbsp.ents @@ -0,0 +1,23 @@ +{ +"classname" "worldspawn" +} +{ +"origin" "info_player_start" +"angles" "0 0 0" +"classname" "mp_global_intermission" +} +{ +"origin" "info_player_start" +"angles" "0 0 0" +"classname" "mp_tdm_spawn_allies_start" +} +{ +"origin" "info_player_start" +"angles" "0 0 0" +"classname" "mp_tdm_spawn_axis_start" +} +{ +"origin" "info_player_start" +"angles" "0 0 0" +"classname" "mp_tdm_spawn" +} diff --git a/resources/iw3/_codxe/internal/maps/coup.gsc b/resources/iw3/_codxe/internal/maps/coup.gsc new file mode 100644 index 0000000..54dc4c5 --- /dev/null +++ b/resources/iw3/_codxe/internal/maps/coup.gsc @@ -0,0 +1,3 @@ +main() +{ +} diff --git a/resources/iw3/_codxe/internal/maps/hunted.d3dbsp.ents b/resources/iw3/_codxe/internal/maps/hunted.d3dbsp.ents new file mode 100644 index 0000000..2d06a63 --- /dev/null +++ b/resources/iw3/_codxe/internal/maps/hunted.d3dbsp.ents @@ -0,0 +1,23 @@ +{ +"classname" "worldspawn" +} +{ +"origin" "1816 -8350 303.3" +"angles" "0 0 0" +"classname" "mp_global_intermission" +} +{ +"origin" "1816 -8350 303.3" +"angles" "0 0 0" +"classname" "mp_tdm_spawn_allies_start" +} +{ +"origin" "1816 -8350 303.3" +"angles" "0 0 0" +"classname" "mp_tdm_spawn_axis_start" +} +{ +"origin" "1816 -8350 303.3" +"angles" "0 0 0" +"classname" "mp_tdm_spawn" +} diff --git a/resources/iw3/_codxe/internal/maps/hunted.gsc b/resources/iw3/_codxe/internal/maps/hunted.gsc new file mode 100644 index 0000000..54dc4c5 --- /dev/null +++ b/resources/iw3/_codxe/internal/maps/hunted.gsc @@ -0,0 +1,3 @@ +main() +{ +} diff --git a/resources/iw3/_codxe/internal/maps/icbm.d3dbsp.ents b/resources/iw3/_codxe/internal/maps/icbm.d3dbsp.ents new file mode 100644 index 0000000..aa5c438 --- /dev/null +++ b/resources/iw3/_codxe/internal/maps/icbm.d3dbsp.ents @@ -0,0 +1,23 @@ +{ +"classname" "worldspawn" +} +{ +"origin" "9276.5 -21339.5 -620" +"angles" "0 0 0" +"classname" "mp_global_intermission" +} +{ +"origin" "9276.5 -21339.5 -620" +"angles" "0 0 0" +"classname" "mp_tdm_spawn_allies_start" +} +{ +"origin" "9276.5 -21339.5 -620" +"angles" "0 0 0" +"classname" "mp_tdm_spawn_axis_start" +} +{ +"origin" "9276.5 -21339.5 -620" +"angles" "0 0 0" +"classname" "mp_tdm_spawn" +} diff --git a/resources/iw3/_codxe/internal/maps/icbm.gsc b/resources/iw3/_codxe/internal/maps/icbm.gsc new file mode 100644 index 0000000..c2d7806 --- /dev/null +++ b/resources/iw3/_codxe/internal/maps/icbm.gsc @@ -0,0 +1,3 @@ +main() +{ +} \ No newline at end of file diff --git a/resources/iw3/_codxe/internal/maps/jeepride.d3dbsp.ents b/resources/iw3/_codxe/internal/maps/jeepride.d3dbsp.ents new file mode 100644 index 0000000..0eea551 --- /dev/null +++ b/resources/iw3/_codxe/internal/maps/jeepride.d3dbsp.ents @@ -0,0 +1,23 @@ +{ +"classname" "worldspawn" +} +{ +"origin" "-15355 787 600" +"angles" "0 203 0" +"classname" "mp_global_intermission" +} +{ +"origin" "-15355 787 600" +"angles" "0 270 0" +"classname" "mp_tdm_spawn_allies_start" +} +{ +"origin" "-15355 787 600" +"angles" "0 270 0" +"classname" "mp_tdm_spawn_axis_start" +} +{ +"origin" "-15355 787 600" +"angles" "0 270 0" +"classname" "mp_tdm_spawn" +} diff --git a/resources/iw3/_codxe/internal/maps/jeepride.gsc b/resources/iw3/_codxe/internal/maps/jeepride.gsc new file mode 100644 index 0000000..54dc4c5 --- /dev/null +++ b/resources/iw3/_codxe/internal/maps/jeepride.gsc @@ -0,0 +1,3 @@ +main() +{ +} diff --git a/resources/iw3/_codxe/internal/maps/killhouse.d3dbsp.ents b/resources/iw3/_codxe/internal/maps/killhouse.d3dbsp.ents new file mode 100644 index 0000000..e2ec590 --- /dev/null +++ b/resources/iw3/_codxe/internal/maps/killhouse.d3dbsp.ents @@ -0,0 +1,23 @@ +{ +"classname" "worldspawn" +} +{ +"origin" "3280 -2064 16" +"angles" "0 0 0" +"classname" "mp_global_intermission" +} +{ +"origin" "3280 -2064 16" +"angles" "0 0 0" +"classname" "mp_tdm_spawn_allies_start" +} +{ +"origin" "3280 -2064 16" +"angles" "0 0 0" +"classname" "mp_tdm_spawn_axis_start" +} +{ +"origin" "3280 -2064 16" +"angles" "0 0 0" +"classname" "mp_tdm_spawn" +} diff --git a/resources/iw3/_codxe/internal/maps/killhouse.gsc b/resources/iw3/_codxe/internal/maps/killhouse.gsc new file mode 100644 index 0000000..54dc4c5 --- /dev/null +++ b/resources/iw3/_codxe/internal/maps/killhouse.gsc @@ -0,0 +1,3 @@ +main() +{ +} diff --git a/resources/iw3/_codxe/internal/maps/launchfacility_a.d3dbsp.ents b/resources/iw3/_codxe/internal/maps/launchfacility_a.d3dbsp.ents new file mode 100644 index 0000000..332488a --- /dev/null +++ b/resources/iw3/_codxe/internal/maps/launchfacility_a.d3dbsp.ents @@ -0,0 +1,23 @@ +{ +"classname" "worldspawn" +} +{ +"origin" "-6734.6 -7824.9 -808.6" +"angles" "0 0 0" +"classname" "mp_global_intermission" +} +{ +"origin" "-6734.6 -7824.9 -808.6" +"angles" "0 0 0" +"classname" "mp_tdm_spawn_allies_start" +} +{ +"origin" "-6734.6 -7824.9 -808.6" +"angles" "0 0 0" +"classname" "mp_tdm_spawn_axis_start" +} +{ +"origin" "-6734.6 -7824.9 -808.6" +"angles" "0 0 0" +"classname" "mp_tdm_spawn" +} diff --git a/resources/iw3/_codxe/internal/maps/launchfacility_a.gsc b/resources/iw3/_codxe/internal/maps/launchfacility_a.gsc new file mode 100644 index 0000000..c2d7806 --- /dev/null +++ b/resources/iw3/_codxe/internal/maps/launchfacility_a.gsc @@ -0,0 +1,3 @@ +main() +{ +} \ No newline at end of file diff --git a/resources/iw3/_codxe/internal/maps/launchfacility_b.d3dbsp.ents b/resources/iw3/_codxe/internal/maps/launchfacility_b.d3dbsp.ents new file mode 100644 index 0000000..d198289 --- /dev/null +++ b/resources/iw3/_codxe/internal/maps/launchfacility_b.d3dbsp.ents @@ -0,0 +1,23 @@ +{ +"classname" "worldspawn" +} +{ +"origin" "881 -369.5 152" +"angles" "0 0 0" +"classname" "mp_global_intermission" +} +{ +"origin" "881 -369.5 152" +"angles" "0 0 0" +"classname" "mp_tdm_spawn_allies_start" +} +{ +"origin" "881 -369.5 152" +"angles" "0 0 0" +"classname" "mp_tdm_spawn_axis_start" +} +{ +"origin" "881 -369.5 152" +"angles" "0 0 0" +"classname" "mp_tdm_spawn" +} diff --git a/resources/iw3/_codxe/internal/maps/launchfacility_b.gsc b/resources/iw3/_codxe/internal/maps/launchfacility_b.gsc new file mode 100644 index 0000000..c2d7806 --- /dev/null +++ b/resources/iw3/_codxe/internal/maps/launchfacility_b.gsc @@ -0,0 +1,3 @@ +main() +{ +} \ No newline at end of file diff --git a/resources/iw3/_codxe/internal/maps/scoutsniper.d3dbsp.ents b/resources/iw3/_codxe/internal/maps/scoutsniper.d3dbsp.ents new file mode 100644 index 0000000..9180656 --- /dev/null +++ b/resources/iw3/_codxe/internal/maps/scoutsniper.d3dbsp.ents @@ -0,0 +1,23 @@ +{ +"classname" "worldspawn" +} +{ +"origin" "-6309 -1935 312" +"angles" "0 0 0" +"classname" "mp_global_intermission" +} +{ +"origin" "-6309 -1935 312" +"angles" "0 0 0" +"classname" "mp_tdm_spawn_allies_start" +} +{ +"origin" "-6309 -1935 312" +"angles" "0 0 0" +"classname" "mp_tdm_spawn_axis_start" +} +{ +"origin" "-6309 -1935 312" +"angles" "0 0 0" +"classname" "mp_tdm_spawn" +} diff --git a/resources/iw3/_codxe/internal/maps/scoutsniper.gsc b/resources/iw3/_codxe/internal/maps/scoutsniper.gsc new file mode 100644 index 0000000..c2d7806 --- /dev/null +++ b/resources/iw3/_codxe/internal/maps/scoutsniper.gsc @@ -0,0 +1,3 @@ +main() +{ +} \ No newline at end of file diff --git a/resources/iw3/_codxe/internal/maps/sniperescape.d3dbsp.ents b/resources/iw3/_codxe/internal/maps/sniperescape.d3dbsp.ents new file mode 100644 index 0000000..a2e71fa --- /dev/null +++ b/resources/iw3/_codxe/internal/maps/sniperescape.d3dbsp.ents @@ -0,0 +1,23 @@ +{ +"classname" "worldspawn" +} +{ +"origin" "796 -11425.6 998.5" +"angles" "0 0 0" +"classname" "mp_global_intermission" +} +{ +"origin" "796 -11425.6 998.5" +"angles" "0 0 0" +"classname" "mp_tdm_spawn_allies_start" +} +{ +"origin" "796 -11425.6 998.5" +"angles" "0 0 0" +"classname" "mp_tdm_spawn_axis_start" +} +{ +"origin" "796 -11425.6 998.5" +"angles" "0 0 0" +"classname" "mp_tdm_spawn" +} diff --git a/resources/iw3/_codxe/internal/maps/sniperescape.gsc b/resources/iw3/_codxe/internal/maps/sniperescape.gsc new file mode 100644 index 0000000..c2d7806 --- /dev/null +++ b/resources/iw3/_codxe/internal/maps/sniperescape.gsc @@ -0,0 +1,3 @@ +main() +{ +} \ No newline at end of file diff --git a/resources/iw3/_codxe/internal/maps/village_assault.d3dbsp.ents b/resources/iw3/_codxe/internal/maps/village_assault.d3dbsp.ents new file mode 100644 index 0000000..0be6d67 --- /dev/null +++ b/resources/iw3/_codxe/internal/maps/village_assault.d3dbsp.ents @@ -0,0 +1,23 @@ +{ +"classname" "worldspawn" +} +{ +"origin" "1523.1 -3573.2 -619.6" +"angles" "0 0 0" +"classname" "mp_global_intermission" +} +{ +"origin" "1523.1 -3573.2 -619.6" +"angles" "0 0 0" +"classname" "mp_tdm_spawn_allies_start" +} +{ +"origin" "1523.1 -3573.2 -619.6" +"angles" "0 0 0" +"classname" "mp_tdm_spawn_axis_start" +} +{ +"origin" "1523.1 -3573.2 -619.6" +"angles" "0 0 0" +"classname" "mp_tdm_spawn" +} diff --git a/resources/iw3/_codxe/internal/maps/village_assault.gsc b/resources/iw3/_codxe/internal/maps/village_assault.gsc new file mode 100644 index 0000000..c2d7806 --- /dev/null +++ b/resources/iw3/_codxe/internal/maps/village_assault.gsc @@ -0,0 +1,3 @@ +main() +{ +} \ No newline at end of file diff --git a/resources/iw3/_codxe/internal/maps/village_defend.d3dbsp.ents b/resources/iw3/_codxe/internal/maps/village_defend.d3dbsp.ents new file mode 100644 index 0000000..62ab424 --- /dev/null +++ b/resources/iw3/_codxe/internal/maps/village_defend.d3dbsp.ents @@ -0,0 +1,23 @@ +{ +"classname" "worldspawn" +} +{ +"origin" "-2201.5 1548.3 488" +"angles" "0 0 0" +"classname" "mp_global_intermission" +} +{ +"origin" "-2201.5 1548.3 488" +"angles" "0 0 0" +"classname" "mp_tdm_spawn_allies_start" +} +{ +"origin" "-2201.5 1548.3 488" +"angles" "0 0 0" +"classname" "mp_tdm_spawn_axis_start" +} +{ +"origin" "-2201.5 1548.3 488" +"angles" "0 0 0" +"classname" "mp_tdm_spawn" +} diff --git a/resources/iw3/_codxe/internal/maps/village_defend.gsc b/resources/iw3/_codxe/internal/maps/village_defend.gsc new file mode 100644 index 0000000..c2d7806 --- /dev/null +++ b/resources/iw3/_codxe/internal/maps/village_defend.gsc @@ -0,0 +1,3 @@ +main() +{ +} \ No newline at end of file diff --git a/resources/iw3/_codxe/mods/codjumper/maps/mp/gametypes/cj.gsc b/resources/iw3/_codxe/mods/codjumper/maps/mp/gametypes/cj.gsc index 4d70b02..cf245f8 100644 --- a/resources/iw3/_codxe/mods/codjumper/maps/mp/gametypes/cj.gsc +++ b/resources/iw3/_codxe/mods/codjumper/maps/mp/gametypes/cj.gsc @@ -19,6 +19,7 @@ init() level.THEMES = get_themes(); level.PLAYER_MODELS = get_player_models(); level.MAPS = get_maps(); + level.MAPS_SP = get_maps_sp(); level.SELECTED_PREFIX = "^2-->^7 "; @@ -1735,6 +1736,35 @@ get_maps() return maps; } +get_maps_sp() +{ + maps = []; + // Alphabetically sorted by value + maps["ac130"] = "ac130"; + maps["aftermath"] = "aftermath"; + maps["airlift"] = "airlift"; + maps["airplane"] = "airplane"; + maps["ambush"] = "ambush"; + // maps["armada"] = "armada"; + maps["blackout"] = "blackout"; + maps["bog_a"] = "bog_a"; + maps["bog_b"] = "bog_b"; + maps["cargoship"] = "cargoship"; + maps["coup"] = "coup"; + maps["hunted"] = "hunted"; + maps["icbm"] = "icbm"; + maps["jeepride"] = "jeepride"; + maps["killhouse"] = "killhouse"; + maps["launchfacility_a"] = "launchfacility_a"; + maps["launchfacility_b"] = "launchfacility_b"; + maps["scoutsniper"] = "scoutsniper"; + maps["sniperescape"] = "sniperescape"; + maps["village_assault"] = "village_assault"; + maps["village_defend"] = "village_defend"; + + return maps; +} + get_player_models() { mapname = getDvar("mapname"); @@ -2469,6 +2499,19 @@ generateMenuOptions() label = level.MAPS[mapname]; self addMenuOption("host_menu_maps", label, ::changeMap, mapname); } + + // Single player map selector + // Map selector + self addMenuOption("main", "Select map (SP)", ::menuAction, "CHANGE_MENU", "host_menu_maps_sp"); + self addMenu("host_menu_maps_sp", "main"); + maps_sp = getarraykeys(level.MAPS_SP); + // loop in reverse to display the maps in the order they are defined + for (i = maps_sp.size - 1; i >= 0; i--) + { + mapname = maps_sp[i]; + label = level.MAPS_sp[mapname]; + self addMenuOption("host_menu_maps_sp", label, ::changeMap, mapname); + } } self addMenuOption("main", "Global settings", ::menuAction, "CHANGE_MENU", "host_menu"); diff --git a/resources/xenia/patches/415607E6 - Call of Duty 4 Modern Warfare MP (TU4).patch.toml b/resources/xenia/patches/415607E6 - Call of Duty 4 Modern Warfare MP (TU4).patch.toml index 31d155d..8f72531 100644 --- a/resources/xenia/patches/415607E6 - Call of Duty 4 Modern Warfare MP (TU4).patch.toml +++ b/resources/xenia/patches/415607E6 - Call of Duty 4 Modern Warfare MP (TU4).patch.toml @@ -29,11 +29,11 @@ value = 0x0000 # 2 -> 0 # PMem_Init - Updates memory allocation size for initialization [[patch.be16]] address = 0x821CFD1A -value = 0x1E00 # Change 0x1970 → 0x1E00 (main allocation call) +value = 0x1ECA # Change 0x1970 → 0x1E00 (main allocation call) [[patch.be16]] address = 0x821CFD2A -value = 0x1E00 # Change 0x1970 → 0x1E00 (secondary reference) +value = 0x1ECA # Change 0x1970 → 0x1E00 (secondary reference) # https://github.com/AdrianCassar/Xenia-WebServices/blob/f0f74f24d47ed7f8370a940bf1c5b2f1617fb8af/patches/415607E6%20-%20Call%20of%20Duty%204%20Modern%20Warfare%20(TU%204).patch.toml [[patch]] diff --git a/src/game/iw3/mp/components/mpsp.cpp b/src/game/iw3/mp/components/mpsp.cpp new file mode 100644 index 0000000..3a5cd1f --- /dev/null +++ b/src/game/iw3/mp/components/mpsp.cpp @@ -0,0 +1,1039 @@ + +/** + * Load Singleplayer maps in Multiplayer. + * Inspiration + * https://github.com/xoxor4d/iw3xo-dev/blob/4eeba0bf63fbf991f44be6eae105ea9e60df0c3f/src/components/modules/_map.cpp + */ + +#include "pch.h" +#include "mpsp.h" +#include "unordered_map" + +namespace iw3 +{ +namespace mp +{ +bool mpsp::is_sp_map = false; + +namespace FS +{ + +/* + * Normalizes a file path by replacing forward slashes with backslashes, + * removing duplicate backslashes, and trimming trailing backslashes (except for root paths). + */ +std::string NormalizePath(const std::string &path) +{ + std::string result = path; + std::replace(result.begin(), result.end(), '/', '\\'); + + size_t pos = 0; + while ((pos = result.find("\\\\", pos)) != std::string::npos) + { + result.erase(pos, 1); + } + + if (!result.empty() && result[result.length() - 1] == '\\') + { + size_t root_end = result.find_first_of('\\'); + if (root_end != result.length() - 1) + { + result.erase(result.length() - 1); + } + } + + return result; +} + +bool WriteFile(const std::string &path, const char *data, int size) +{ + const std::string normalized = NormalizePath(path); + + // Create directories if they do not exist + size_t pos = 0; + while ((pos = normalized.find('\\', pos)) != std::string::npos) + { + std::string dir = normalized.substr(0, pos); + CreateDirectoryA(dir.c_str(), NULL); + pos++; + } + + std::ofstream file(normalized.c_str(), std::ios::binary); + if (!file.is_open()) + { + DbgPrint("[IO] Failed to open file for writing: %s\n", normalized.c_str()); + return false; + } + + file.write(reinterpret_cast(data), size); + return file.good(); +} + +std::string ReadTextFile(const std::string &path) +{ + const std::string normalized = NormalizePath(path); + std::ifstream file(normalized, std::ios::binary); + if (!file) + { + DbgPrint("ReadTextFile: Failed to open file: %s\n", normalized.c_str()); + return ""; + } + + // Get file size and pre-allocate string + file.seekg(0, std::ios::end); + size_t size = static_cast(file.tellg()); + file.seekg(0, std::ios::beg); + + std::string content(size, '\0'); + file.read(&content[0], size); + + return content; +} + +std::vector ReadBinaryFile(const std::string &path) +{ + const std::string normalized = NormalizePath(path); + std::ifstream file(normalized, std::ios::binary); + if (!file) + { + DbgPrint("[IO] ReadBinaryFile: Failed to open file: %s\n", normalized.c_str()); + return std::vector(); + } + + // Get file size and pre-allocate vector + file.seekg(0, std::ios::end); + size_t size = static_cast(file.tellg()); + file.seekg(0, std::ios::beg); + + std::vector buffer(size); + file.read(reinterpret_cast(buffer.data()), size); + + if (!file.good() && !file.eof()) + { + DbgPrint("[IO] ReadBinaryFile: Error reading file: %s\n", normalized.c_str()); + return std::vector(); + } + + return buffer; +} + +} // namespace FS + +namespace Asset +{ + +std::string get_load_dir() +{ + return "game:\\_codxe\\internal"; +} +namespace MapEnts_ +{ + +std::string mapents_buffer; + +void override_(MapEnts *asset) +{ + // _load\maps\airplane.d3dbsp.ents + const std::string filename = Asset::get_load_dir() + "\\" + asset->name + ".ents"; + const std::string buffer = FS::ReadTextFile(filename); + + if (buffer.empty()) + { + return; + } + + if (!mapents_buffer.empty()) + { + mapents_buffer.clear(); + } + + DbgPrint("Overriding map_ents '%s'\n", asset->name); + + mapents_buffer.assign(buffer); + + asset->entityString = const_cast(mapents_buffer.c_str()); + asset->numEntityChars = mapents_buffer.length(); +} + +} // namespace MapEnts_ + +namespace RawFile_ +{ +std::unordered_map> rawfile_buffers; + +void override_(RawFile *asset) +{ + if (!asset || !asset->name || asset->name[0] == '\0') + { + return; + } + + const std::string filename = Asset::get_load_dir() + "\\" + asset->name; + const std::string buffer = FS::ReadTextFile(filename); + + if (buffer.empty()) + { + return; + } + + DbgPrint("Overriding rawfile '%s'\n", asset->name); + + auto itr = rawfile_buffers.find(asset->name); + if (itr != rawfile_buffers.end()) + { + rawfile_buffers.erase(itr); + } + rawfile_buffers[asset->name] = make_unique(); + itr = rawfile_buffers.find(asset->name); + auto rawfile_buffer = itr->second.get(); + + rawfile_buffer->assign(buffer); + + asset->len = rawfile_buffer->length(); + asset->buffer = rawfile_buffer->data(); +} +} // namespace RawFile_ + +} // namespace Asset + +// Structs + +enum XBlockType : __int32 +{ + XFILE_BLOCK_TEMP = 0x0, + XFILE_BLOCK_RUNTIME_BEGIN = 0x1, + XFILE_BLOCK_RUNTIME = 0x1, + XFILE_BLOCK_LARGE_RUNTIME = 0x2, + XFILE_BLOCK_PHYSICAL_RUNTIME = 0x3, + XFILE_BLOCK_RUNTIME_END = 0x4, + XFILE_BLOCK_VIRTUAL = 0x4, + XFILE_BLOCK_LARGE = 0x5, + XFILE_BLOCK_PHYSICAL = 0x6, + MAX_XFILE_COUNT = 0x7, +}; + +struct XBlock +{ + unsigned __int8 *data; + unsigned int size; +}; + +struct internal_state; + +struct _OVERLAPPED +{ + unsigned int Internal; + unsigned int InternalHigh; + unsigned int Offset; + unsigned int OffsetHigh; + void *hEvent; +}; + +struct z_stream_s +{ + unsigned __int8 *next_in; + unsigned int avail_in; + unsigned int total_in; + unsigned __int8 *next_out; + unsigned int avail_out; + unsigned int total_out; + char *msg; + internal_state *state; + unsigned __int8 *(__fastcall *zalloc)(unsigned __int8 *, unsigned int, unsigned int); + void(__fastcall *zfree)(unsigned __int8 *, unsigned __int8 *); + unsigned __int8 *opaque; + int data_type; +}; +static_assert(sizeof(z_stream_s) == 48, ""); + +struct DB_LoadData +{ + void *f; + const char *filename; + XBlock *blocks; + int outstandingReads; + _OVERLAPPED overlapped; + z_stream_s stream; + unsigned __int8 *compressBufferStart; + unsigned __int8 *compressBufferEnd; + void(__fastcall *interrupt)(); + int allocType; +}; + +struct XAsset; + +struct ScriptStringList +{ + int count; + const char **strings; +}; + +struct XAssetList +{ + ScriptStringList stringList; + int assetCount; + XAsset *assets; +}; + +enum errorParm_t : __int32 +{ + ERR_FATAL = 0x0, + ERR_DROP = 0x1, + ERR_SERVERDISCONNECT = 0x2, + ERR_DISCONNECT = 0x3, + ERR_SCRIPT = 0x4, + ERR_SCRIPT_DROP = 0x5, + ERR_LOCALIZATION = 0x6, + ERR_MAPLOADERRORSUMMARY = 0x7, +}; + +struct XFile +{ + unsigned int size; + unsigned int externalSize; + unsigned int blockSize[7]; +}; + +struct XZoneName +{ + char name[64]; + int flags; +}; + +struct StreamDelayInfo +{ + const void *ptr; + int size; +}; + +// data pointers + +unsigned __int8 **g_streamPos = reinterpret_cast(0x826B91F4); +unsigned __int8 **g_streamPosArray = reinterpret_cast(0x82708C04); +// XBlock **g_streamBlocks = reinterpret_cast(0x826AD1EC); +// unsigned __int8 *g_streamPosIndex = reinterpret_cast(0x826BA3FC); +unsigned *g_streamDelayIndex = reinterpret_cast(0x82668D5C); +StreamDelayInfo *g_streamDelayArray = reinterpret_cast(0x8270C4F0); +// unsigned __int8 *g_streamPosStackIndex = reinterpret_cast(0x82668A34); +unsigned int *g_loadingAssets = reinterpret_cast(0x824754F4); +bool *g_anyFastFileLoaded = reinterpret_cast(0x82435AB5); +DB_LoadData *g_load = reinterpret_cast(0x82475508); +const char **g_block_mem_name = reinterpret_cast(0x823A42AC); +const char **g_assetNames = reinterpret_cast(0x823A42C8); +const char **g_defaultAssetName = reinterpret_cast(0x823A40F8); +const XZoneName *g_zoneNames = reinterpret_cast(0x8270BC28); +const unsigned int *g_zoneIndex = reinterpret_cast(0x82536C4C); +XAsset **varXAsset = reinterpret_cast(0x82475658); +XAssetHeader **varXAssetHeader = reinterpret_cast(0x824756E0); +XAssetList **varXAssetList = reinterpret_cast(0x824756F4); +void **DB_XAssetPool = reinterpret_cast(0x823A4070); +int *g_poolSize = reinterpret_cast(0x823A3E50); + +int *g_trackLoadProgress = reinterpret_cast(0x824754FC); +int *g_totalSize = reinterpret_cast(0x824754F8); +int *g_loadedSize = reinterpret_cast(0x82475500); +int *g_totalExternalBytes = reinterpret_cast(0x82475504); +int *g_loadedExternalBytes = reinterpret_cast(0x8246F4AC); + +// function pointers + +typedef int (*Com_sprintf_t)(char *dest, unsigned int size, const char *fmt, ...); +Com_sprintf_t Com_sprintf = reinterpret_cast(0x821CCED8); + +typedef void (*DB_SetXAssetName_t)(XAsset *asset, const char *name); +DB_SetXAssetName_t DB_SetXAssetName = reinterpret_cast(0x822B30D0); + +typedef const char *(*DB_GetXAssetName_t)(const XAsset *asset); +DB_GetXAssetName_t DB_GetXAssetName = reinterpret_cast(0x822B3490); + +typedef XAssetEntry *(*DB_LinkXAssetEntry_t)(XAssetEntry *newEntry, int allowOverride); +DB_LinkXAssetEntry_t DB_LinkXAssetEntry = reinterpret_cast(0x8229FC50); + +typedef void (*DB_LoadXFileData_t)(unsigned __int8 *pos, unsigned int size); +DB_LoadXFileData_t DB_LoadXFileData = reinterpret_cast(0x822B1FC8); + +typedef void (*DB_AllocXBlocks_t)(const unsigned int *blockSize, const char *filename, XBlock *blocks, + unsigned int allocType); +DB_AllocXBlocks_t DB_AllocXBlocks = reinterpret_cast(0x822A1DF0); + +typedef void (*Load_XAssetArrayCustom_t)(int count); +Load_XAssetArrayCustom_t Load_XAssetArrayCustom = reinterpret_cast(0x822B2140); + +typedef void (*Load_Stream_t)(bool atStreamStart, void *ptr, size_t size); +Load_Stream_t Load_Stream = reinterpret_cast(0x8229D148); + +typedef void (*Load_DelayStream_t)(); +Load_DelayStream_t Load_DelayStream = reinterpret_cast(0x8229D0F8); + +typedef void (*Load_XAssetHeader_t)(bool atStreamStart); +Load_XAssetHeader_t Load_XAssetHeader = reinterpret_cast(0x822B1838); + +struct snapshotEntityNumbers_t; + +typedef void (*SV_AddEntitiesVisibleFromPoint_t)(const float *org, int clientNum, snapshotEntityNumbers_t *eNums); +SV_AddEntitiesVisibleFromPoint_t SV_AddEntitiesVisibleFromPoint = + reinterpret_cast(0x821FB898); + +typedef void (*DB_LoadXFileInternal_t)(); +DB_LoadXFileInternal_t DB_LoadXFileInternal = reinterpret_cast(0x822B21B8); + +typedef void (*DB_ReadXFileStage_t)(); +DB_ReadXFileStage_t DB_ReadXFileStage = reinterpret_cast(0x822B1F60); + +typedef void (*DB_WaitXFileStage_t)(); +DB_WaitXFileStage_t DB_WaitXFileStage = reinterpret_cast(0x822B1DA0); + +typedef void (*Com_Error_t)(errorParm_t code, const char *fmt, ...); +Com_Error_t Com_Error = reinterpret_cast(0x82236640); + +typedef void (*R_ShowDirtyDiscError_t)(); +R_ShowDirtyDiscError_t R_ShowDirtyDiscError = reinterpret_cast(0x82155218); + +typedef int (*DB_AuthLoad_InflateInit_t)(z_stream_s *stream, bool isSecure, const char *filename); +DB_AuthLoad_InflateInit_t DB_AuthLoad_InflateInit = reinterpret_cast(0x822B2B90); + +typedef void (*Load_XAssetListCustom_t)(); +Load_XAssetListCustom_t Load_XAssetListCustom = reinterpret_cast(0x822B20E0); + +typedef void (*DB_PushStreamPos_t)(unsigned int index); +DB_PushStreamPos_t DB_PushStreamPos = reinterpret_cast(0x8229D410); + +typedef void (*DB_InitStreams_t)(XBlock *blocks); +DB_InitStreams_t DB_InitStreams = reinterpret_cast(0x8229D310); + +typedef void (*DB_PopStreamPos_t)(); +DB_PopStreamPos_t DB_PopStreamPos = reinterpret_cast(0x8229D390); + +typedef void (*DB_CancelLoadXFile_t)(); +DB_CancelLoadXFile_t DB_CancelLoadXFile = reinterpret_cast(0x822B1EC0); + +typedef int (*inflateInit2__t)(z_stream_s *z, int w, const char *version, int stream_size); +inflateInit2__t inflateInit2_ = reinterpret_cast(0x823527B8); + +typedef int (*DB_AuthLoad_Inflate_t)(z_stream_s *stream, int flush); +DB_AuthLoad_Inflate_t DB_AuthLoad_Inflate = reinterpret_cast(0x822B2F70); + +typedef int (*inflate_t)(z_stream_s *stream, int flush); +inflate_t inflate = reinterpret_cast(0x82352198); + +typedef int (*DB_GetXAssetTypeSize_t)(XAssetType type); +DB_GetXAssetTypeSize_t DB_GetXAssetTypeSize = reinterpret_cast(0x822B30B8); + +bool mpsp::is_sp_mapname(const std::string &name) +{ + static const char *kSpMaps[] = {"ac130", + "aftermath", + "airlift", + "airplane", + "ambush", + "armada", + "blackout", + "bog_a", + "bog_b", + "cargoship", + "coup", + "hunted", + "icbm", + "jeepride", + "killhouse", + "launchfacility_a", + "launchfacility_b", + "scoutsniper", + "simplecredits", + "sniperescape", + "village_assault", + "village_defend"}; + + for (size_t i = 0; i < ARRAYSIZE(kSpMaps); ++i) + { + if (name == kSpMaps[i]) + return true; + } + + return false; +} + +struct ZoneOverride +{ + std::string name; + unsigned int delayStreamStart; // = xfile.size - xfile.blockSize[2] + sizeof(XFile) + int assetCountOverride; + unsigned int blockSizeOverride[7]; +}; + +// This data was obtained semi-manually by dumping info at different intervals during asset loading +const ZoneOverride ZONE_OVERRIDES[] = { + {"airlift", + 83609292, + 672, + { + /* TEMP */ 0, + /* RUNTIME */ 0, + /* LARGE_RUNTIME */ 149794816, + /* PHYSICAL_RUNTIME */ 0, + /* VIRTUAL */ 0, + /* LARGE */ 0, // -+1024 calc + /* PHYSICAL */ 0, + }}, + {"ambush", + 78799549, + 918, + { + /* TEMP */ 0, + /* RUNTIME */ 0, + /* LARGE_RUNTIME */ 174501888, + /* PHYSICAL_RUNTIME */ 0, + /* VIRTUAL */ 65000000, + /* LARGE */ 0, + /* PHYSICAL */ 0, + }}, + // Still too large + // {"armada", + // 88246690, + // 876, + // { + // /* TEMP */ 0, + // /* RUNTIME */ 0, + // /* LARGE_RUNTIME */ 175124480, + // /* PHYSICAL_RUNTIME */ 0, + // /* VIRTUAL */ 0, + // /* LARGE */ 0, + // /* PHYSICAL */ 0, + // }}, + {"blackout", + 80876054, + 868, + { + /* TEMP */ 0, + /* RUNTIME */ 0, + /* LARGE_RUNTIME */ 174112768, + /* PHYSICAL_RUNTIME */ 0, + /* VIRTUAL */ 0, + /* LARGE */ 0, + /* PHYSICAL */ 0, + }}, + {"bog_a", + 76407093, + 837, + { + /* TEMP */ 0, + /* RUNTIME */ 0, + /* LARGE_RUNTIME */ 162222080, + /* PHYSICAL_RUNTIME */ 0, + /* VIRTUAL */ 0, + /* LARGE */ 0, + /* PHYSICAL */ 850344, + }}, + {"bog_b", + 73772025, + 847, + { + /* TEMP */ 0, + /* RUNTIME */ 0, + /* LARGE_RUNTIME */ 0, + /* PHYSICAL_RUNTIME */ 0, + /* VIRTUAL */ 0, + /* LARGE */ 0, + /* PHYSICAL */ 0, + }}, + {"cargoship", + 78836333, + 630, + { + /* TEMP */ 0, + /* RUNTIME */ 0, + /* LARGE_RUNTIME */ 97931264, + /* PHYSICAL_RUNTIME */ 0, + /* VIRTUAL */ 56533667, + /* LARGE */ 0, + /* PHYSICAL */ 364488, + }}, + {"coup", + 79849985, + 530, + { + /* TEMP */ 0, + /* RUNTIME */ 0, + /* LARGE_RUNTIME */ 155185152, + /* PHYSICAL_RUNTIME */ 0, + /* VIRTUAL */ 0, + /* LARGE */ 0, + /* PHYSICAL */ 0, + }}, + {"jeepride", + 77336509, + 586, + { + /* TEMP */ 0, + /* RUNTIME */ 0, + /* LARGE_RUNTIME */ 140935168, + /* PHYSICAL_RUNTIME */ 0, + /* VIRTUAL */ 0, + /* LARGE */ 0, + /* PHYSICAL */ 0, + }}, + {"hunted", + 82247981, + 739, + { + /* TEMP */ 0, + /* RUNTIME */ 0, + /* LARGE_RUNTIME */ 160907264, + /* PHYSICAL_RUNTIME */ 0, + /* VIRTUAL */ 0, + /* LARGE */ 0, + /* PHYSICAL */ 0, + }}, + {"icbm", + 86973506, + 783, + { + /* TEMP */ 0, + /* RUNTIME */ 0, + /* LARGE_RUNTIME */ 168857600, + /* PHYSICAL_RUNTIME */ 0, + /* VIRTUAL */ 0, + /* LARGE */ 0, + /* PHYSICAL */ 0, + }}, + {"killhouse", + 74240661, + 641, + { + /* TEMP */ 0, + /* RUNTIME */ 0, + /* LARGE_RUNTIME */ 170491904, + /* PHYSICAL_RUNTIME */ 0, + /* VIRTUAL */ 0, + /* LARGE */ 0, + /* PHYSICAL */ 0, + }}, + {"scoutsniper", + 88016466, + 617, + { + /* TEMP */ 0, + /* RUNTIME */ 0, + /* LARGE_RUNTIME */ 173142016, + /* PHYSICAL_RUNTIME */ 0, + /* VIRTUAL */ 0, + /* LARGE */ 0, + /* PHYSICAL */ 0, + }}, + {"sniperescape", + 87072605, + 698, + { + /* TEMP */ 0, + /* RUNTIME */ 0, + /* LARGE_RUNTIME */ 169299968, + /* PHYSICAL_RUNTIME */ 0, + /* VIRTUAL */ 0, + /* LARGE */ 0, + /* PHYSICAL */ 0, + }}, + {"village_assault", + 74069735, + 901, + { + /* TEMP */ 0, + /* RUNTIME */ 0, + /* LARGE_RUNTIME */ 176791552, + /* PHYSICAL_RUNTIME */ 0, + /* VIRTUAL */ 0, + /* LARGE */ 0, + /* PHYSICAL */ 0, + }}, + + {"village_defend", + 74943765, + 894, + { + /* TEMP */ 0, + /* RUNTIME */ 0, + /* LARGE_RUNTIME */ 172601344, + /* PHYSICAL_RUNTIME */ 0, + /* VIRTUAL */ 0, + /* LARGE */ 0, + /* PHYSICAL */ 0, + }}, +}; + +int g_zoneOverrideIndex = -1; + +Detour DB_LinkXAssetEntry_Detour; +XAssetEntry *DB_LinkXAssetEntry_Hook(XAssetEntry *newEntry, int allowOverride) +{ + XAsset xasset; + xasset.type = newEntry->asset.type; + xasset.header = newEntry->asset.header; + + if (mpsp::is_sp_map) + { + switch (newEntry->asset.type) + { + case ASSET_TYPE_MAP_ENTS: + { + Asset::MapEnts_::override_(newEntry->asset.header.mapEnts); + break; + } + case ASSET_TYPE_RAWFILE: + { + Asset::RawFile_::override_(newEntry->asset.header.rawfile); + break; + } + case ASSET_TYPE_GAMEWORLD_SP: + { + newEntry->asset.type = ASSET_TYPE_GAMEWORLD_MP; + break; + } + // Hijack the reference asset ',' mechanism to avoid reaching asset limits. + case ASSET_TYPE_WEAPON: + { + static const std::string weapon_default_reference_name = + std::string(",") + g_defaultAssetName[ASSET_TYPE_WEAPON]; + DB_SetXAssetName(&xasset, weapon_default_reference_name.c_str()); + break; + } + case ASSET_TYPE_FX: + { + static const std::string fx_default_reference_name = std::string(",") + g_defaultAssetName[ASSET_TYPE_FX]; + DB_SetXAssetName(&xasset, fx_default_reference_name.c_str()); + break; + } + } + } + + return DB_LinkXAssetEntry_Detour.GetOriginal()(newEntry, allowOverride); +} + +Detour Com_sprintf_Detour; +int Com_sprintf_Hook(char *dest, unsigned int size, const char *fmt...) +{ + // Do original logic + va_list va; + va_start(va, fmt); + int result = _vsnprintf(dest, size, fmt, va); + va_end(va); + + if (size > 0) + dest[size - 1] = '\0'; + + // Rewrites + + // [mpsp] + // Com_GetBspFilename has been inlined + const void *returnAddress = _ReturnAddress(); + if (returnAddress == (void *)0x82203164 // SV_SpawnServer + || returnAddress == (void *)0x822E7044 // CL_InitCGame + || returnAddress == (void *)0x822F9EE8 // CG_ParseServerInfo + // || returnAddress == (void *)0x823283C8 // + // || returnAddress == (void *)0x823283E8 // + + ) + { + // "maps/mp/%s.d3dbsp" + const char *mapname = dest + 8; // skip "maps/mp/" + const bool isMp = (strncmp(mapname, "mp_", 3) == 0); + + if (!isMp) + { + char *dst = dest + 5; // after "maps/" + const char *src = dest + 8; // after "maps/mp/" + memmove(dst, src, strlen(src) + 1); + } + } + + // [mpsp] + if (returnAddress == (void *)0x82265820) // GScr_LoadLevelScript + { + // "maps/mp/%s" + const char *mapname = dest + 8; // skip "maps/mp/" + const bool isMp = (strncmp(mapname, "mp_", 3) == 0); + if (!isMp) + { + // Rewrite to "maps/%s" + char *dst = dest + 5; // after "maps/" + const char *src = dest + 8; // after "maps/mp/" + memmove(dst, src, strlen(src) + 1); + } + } + + return result; +} + +Detour Load_XAssetArrayCustom_Detour; +void Load_XAssetArrayCustom_Stub(int count) +{ + // Load the entire XAsset array + // This ensures that the stream position is correct even if we are loading less assets later. + Load_Stream(1, *varXAsset, 8 * count); + + // Override the count if needed AFTER progressing the stream correctly on asset list + if (g_zoneOverrideIndex != -1) + { + count = ZONE_OVERRIDES[g_zoneOverrideIndex].assetCountOverride; + } + + XAsset *assetArray = *varXAsset; + + // Load each asset's header data + for (int i = 0; i < count; i++) + { + XAsset *asset = &assetArray[i]; + *varXAsset = asset; + Load_Stream(0, asset, 8); + + *varXAssetHeader = &asset->header; + Load_XAssetHeader(0); + } +} + +void DB_SkipXFileData(unsigned int bytesToSkip) +{ + // Small scratch buffer so we don't need to allocate huge blocks + static unsigned char scratch[0x4000]; + + while (bytesToSkip > 0) + { + unsigned int chunk = bytesToSkip; + if (chunk > sizeof(scratch)) + chunk = sizeof(scratch); + + // This will decompress `chunk` bytes and advance the zlib stream + DB_LoadXFileData(scratch, chunk); + + bytesToSkip -= chunk; + } +} + +Detour SV_AddEntitiesVisibleFromPoint_Detour; +void SV_AddEntitiesVisibleFromPoint_Hook(const float *org, int clientNum, snapshotEntityNumbers_t *eNums) +{ + if (mpsp::is_sp_map) + { + return; + } + + SV_AddEntitiesVisibleFromPoint_Detour.GetOriginal()(org, clientNum, eNums); +} + +/** + * Tracks if the currently loading fastfile has auth or not. + */ +bool g_isSecure = true; + +Detour DB_LoadXFileInternal_Detour; +void DB_LoadXFileInternal_Stub() +{ + DB_ReadXFileStage(); + if (!g_load->outstandingReads) + Com_Error(ERR_DROP, "Fastfile for zone '%s' is empty.", g_load->filename); + DB_WaitXFileStage(); + DB_ReadXFileStage(); + + mpsp::is_sp_map = mpsp::is_sp_mapname(g_load->filename); + + // [mpsp] Reset any previous override + g_zoneOverrideIndex = -1; + for (size_t i = 0; i < ARRAYSIZE(ZONE_OVERRIDES); i++) + { + if (ZONE_OVERRIDES[i].name == g_load->filename) + { + g_zoneOverrideIndex = i; + break; + } + } + + // Read magic + char magic[8]; + std::memcpy(magic, g_load->stream.next_in, sizeof(magic)); + g_load->stream.next_in += sizeof(magic); + g_load->stream.avail_in -= sizeof(magic); + + bool isSecure; + + if (std::memcmp(magic, "IWff0100", sizeof(magic)) == 0) + { + isSecure = true; + } + else if (std::memcmp(magic, "IWffu100", sizeof(magic)) == 0) + { + isSecure = false; + } + else + { + // Magic is neither the signed nor the unsigned form: treat as bad disc. + R_ShowDirtyDiscError(); + return; + } + + std::uint32_t fileVersion = 0; + std::memcpy(&fileVersion, g_load->stream.next_in, sizeof(fileVersion)); + g_load->stream.next_in += sizeof(fileVersion); + g_load->stream.avail_in -= sizeof(fileVersion); + + const std::uint32_t expectedFastfileVersion = 1; + + if (fileVersion != expectedFastfileVersion) + { + if (fileVersion < expectedFastfileVersion) + { + Com_Error(ERR_DROP, "Fastfile for zone '%s' is out of date (version %u, expecting %u)", g_load->filename, + fileVersion, expectedFastfileVersion); + } + else + { + Com_Error(ERR_DROP, "Fastfile for zone '%s' is newer than client executable (version %u, expecting %u)", + g_load->filename, fileVersion, expectedFastfileVersion); + } + } + + int err; + if (isSecure) + { + g_isSecure = true; + err = DB_AuthLoad_InflateInit(&g_load->stream, isSecure, g_load->filename); + } + else + { + g_isSecure = false; + err = inflateInit2_(&g_load->stream, 15, "1.1.4", sizeof(z_stream_s)); + } + + if (err != 0) + { + R_ShowDirtyDiscError(); + return; + } + + XFile xfile; + DB_LoadXFileData(reinterpret_cast(&xfile), sizeof(XFile)); + + // Handles the loading bar during map load + if (*g_trackLoadProgress) + { + int fileSize = GetFileSize(g_load->f, 0); + if (xfile.externalSize + fileSize >= 0x100000) + { + *g_totalSize = (fileSize + 0x3FFFF) / 0x40000 - *g_loadedSize; + *g_loadedSize = 0; + *g_totalExternalBytes = xfile.externalSize - *g_loadedExternalBytes; + *g_loadedExternalBytes = 0; + } + } + + // [mpsp] Override blocksize if we have any + if (g_zoneOverrideIndex != -1) + { + for (int i = 0; i < MAX_XFILE_COUNT; i++) + { + const int blockSizeOverride = ZONE_OVERRIDES[g_zoneOverrideIndex].blockSizeOverride[i]; + if (blockSizeOverride) + xfile.blockSize[i] = blockSizeOverride; + } + } + + DB_AllocXBlocks(xfile.blockSize, g_load->filename, g_load->blocks, g_load->allocType); + DB_InitStreams(g_load->blocks); + Load_XAssetListCustom(); + DB_PushStreamPos(4); + + if ((*varXAssetList)->assets) + { + const int assetCount = (*varXAssetList)->assetCount; + + unsigned int alignedPos = (reinterpret_cast(*g_streamPos) + 3u) & ~std::uintptr_t(3u); + *g_streamPos = reinterpret_cast(alignedPos); + *varXAsset = reinterpret_cast(*g_streamPos); + (*varXAssetList)->assets = *varXAsset; + Load_XAssetArrayCustom(assetCount); + } + + DB_PopStreamPos(); + --*g_loadingAssets; + + // [mpsp] + // Skipforward the stream so the delay load stream is loaded from the correct offset + if (g_zoneOverrideIndex != -1) + { + const int delayStreamStart = ZONE_OVERRIDES[g_zoneOverrideIndex].delayStreamStart; + DB_SkipXFileData(delayStreamStart - g_load->stream.total_out); + } + + Load_DelayStream(); + Com_Printf(CON_CHANNEL_FILES, "Loaded zone '%s'\n", g_load->filename); + *g_anyFastFileLoaded = 1; + DB_CancelLoadXFile(); +} + +Detour DB_AuthLoad_Inflate_Detour; +int DB_AuthLoad_Inflate_Hook(z_stream_s *stream, int flush) +{ + if (g_isSecure) + { + return DB_AuthLoad_Inflate_Detour.GetOriginal()(stream, flush); + } + else + { + return inflate(stream, 2); + } +} + +void DB_ReallocXAssetPool(XAssetType type, unsigned int newSize) +{ + void *pool_entry = malloc(newSize * DB_GetXAssetTypeSize(type)); + DB_XAssetPool[type] = pool_entry; + g_poolSize[type] = newSize; +} + +mpsp::mpsp() +{ + // The larger maps only load on xenia anyway + if (xbox::IsXenia()) + { + DB_ReallocXAssetPool(ASSET_TYPE_XMODEL, 10250); + DB_ReallocXAssetPool(ASSET_TYPE_MATERIAL, 2600); + } + + // BSP resolving + Com_sprintf_Detour = Detour(Com_sprintf, Com_sprintf_Hook); + Com_sprintf_Detour.Install(); + + // Rewritten to support loading unsigned zones and other patches + DB_LoadXFileInternal_Detour = Detour(DB_LoadXFileInternal, DB_LoadXFileInternal_Stub); + DB_LoadXFileInternal_Detour.Install(); + + DB_AuthLoad_Inflate_Detour = Detour(DB_AuthLoad_Inflate, DB_AuthLoad_Inflate_Hook); + DB_AuthLoad_Inflate_Detour.Install(); + + // Rewrite some assets before linking + DB_LinkXAssetEntry_Detour = Detour(DB_LinkXAssetEntry, DB_LinkXAssetEntry_Hook); + DB_LinkXAssetEntry_Detour.Install(); + + Load_XAssetArrayCustom_Detour = Detour(Load_XAssetArrayCustom, Load_XAssetArrayCustom_Stub); + Load_XAssetArrayCustom_Detour.Install(); + + // Without this it crashes when knifing the game world? TODO: investigate a less invasive patch + // This prevents players from seeing other players. + SV_AddEntitiesVisibleFromPoint_Detour = Detour(SV_AddEntitiesVisibleFromPoint, SV_AddEntitiesVisibleFromPoint_Hook); + SV_AddEntitiesVisibleFromPoint_Detour.Install(); +} + +mpsp::~mpsp() +{ + Com_sprintf_Detour.Remove(); + + DB_LoadXFileInternal_Detour.Remove(); + + DB_AuthLoad_Inflate_Detour.Remove(); + + DB_LinkXAssetEntry_Detour.Remove(); + + Load_XAssetArrayCustom_Detour.Remove(); + + SV_AddEntitiesVisibleFromPoint_Detour.Remove(); +} + +} // namespace mp +} // namespace iw3 diff --git a/src/game/iw3/mp/components/mpsp.h b/src/game/iw3/mp/components/mpsp.h new file mode 100644 index 0000000..1948211 --- /dev/null +++ b/src/game/iw3/mp/components/mpsp.h @@ -0,0 +1,22 @@ +#pragma once + +#include "pch.h" + +namespace iw3 +{ +namespace mp +{ +class mpsp : public Module +{ + public: + mpsp(); + ~mpsp(); + const char *get_name() override + { + return "mpsp"; + }; + static bool is_sp_map; + static bool is_sp_mapname(const std::string &name); +}; +} // namespace mp +} // namespace iw3 diff --git a/src/game/iw3/mp/main.cpp b/src/game/iw3/mp/main.cpp index d4da1fc..4090f39 100644 --- a/src/game/iw3/mp/main.cpp +++ b/src/game/iw3/mp/main.cpp @@ -6,6 +6,7 @@ #include "components/cmds.h" #include "components/g_client_fields.h" #include "components/g_scr_main.h" +#include "components/mpsp.h" #include "components/pm.h" #include "components/scr_parser.h" #include "components/scr_vm_functions.h" @@ -1529,10 +1530,27 @@ void Pmove_Hook(pmove_t *pm) } } +/** + * Patch out the signature checks used during fastfile authentication. + * Signature data must still be present in the fastfile structure, but the values themselves may be zeroed. + */ +void DisableFastfileAuth() +{ + // DBX_AuthLoad_ValidateHash + *(volatile uint32_t *)0x822B2994 = 0x60000000; + *(volatile uint32_t *)0x822B2A34 = 0x60000000; + *(volatile uint32_t *)0x822B2D2C = 0x60000000; + + // DBX_AuthLoad_ValidateSignature + *(volatile uint32_t *)0x822B2D44 = 0x60000000; +} + IW3_MP_Plugin::IW3_MP_Plugin() { DbgPrint("Initializing MP\n"); + DisableFastfileAuth(); + RegisterModule(new Config()); RegisterModule(new cg()); @@ -1542,6 +1560,7 @@ IW3_MP_Plugin::IW3_MP_Plugin() RegisterModule(new g_client_fields()); RegisterModule(new g_scr_main()); RegisterModule(new pm()); + RegisterModule(new mpsp()); RegisterModule(new scr_parser()); RegisterModule(new scr_vm_functions()); diff --git a/src/game/iw3/mp/structs.h b/src/game/iw3/mp/structs.h index f3d1fe8..84a3b2f 100644 --- a/src/game/iw3/mp/structs.h +++ b/src/game/iw3/mp/structs.h @@ -1296,7 +1296,7 @@ union XAssetHeader // ComWorld *comWorld; // GameWorldSp *gameWorldSp; // GameWorldMp *gameWorldMp; - // MapEnts *mapEnts; + MapEnts *mapEnts; // GfxWorld *gfxWorld; // GfxLightDef *lightDef; Font_s *font;