diff --git a/CREDITS.md b/CREDITS.md index a5c854fdc..040311e5d 100644 --- a/CREDITS.md +++ b/CREDITS.md @@ -7,6 +7,7 @@ This page lists all the individual contributions to the project by their author. - **Belonit (Gluk-v48)**: - Check for Changelog/Documentation/Credits in Pull Requests. - Docs dark theme switcher. + - Porting the YR MP spawner from C to C++ and YR++, used as a base for the Vinifera spawner. - **CCHyper/tomsons26**: - Vinifera foundations: TS++, game.exe hooker, extension system and other core features - Implement `CurleyShuffle` for AircraftTypes @@ -130,6 +131,9 @@ This page lists all the individual contributions to the project by their author. - Implement various controls to customise target lasers line. - Implement various controls to show and customise NavCom queue lines. - Implement customizable mouse cursors and actions. + - Extend `BaseUnit` to accept a list of vehicles. +- **CnCNet Contributors**: + - Tiberian Sun TS-patches spawner, Yuri's Revenge CnCNet spawner that served as a base for Vinifera spawner. - **Kerbiter (Metadorius)**: - Initial documentation setup. - **MarkJFox**: @@ -163,6 +167,7 @@ This page lists all the individual contributions to the project by their author. - Implement the Torpedo logic from Red Alert 1 for BulletTypes. - Add `BuildTimeCost`. - Allow scenarios to have custom score screen bar colors. + - Add support for more than 2 sides' loading screens, sidebars and speeches. - **secsome**: - Add support for up to 32767 waypoints to be used in scenarios. - **ZivDero**: @@ -200,4 +205,18 @@ This page lists all the individual contributions to the project by their author. - Update and finalize custom mouse cursors and actions, add customizable weapon & EMP cursors. - Implement support for a Saved Games subdirectory. - Fix a bug where if the player loaded a saved game, the score screen timer would report the time since the saved game was loaded, instead of since when the scenario was first started. - + - Implement the multiplayer spawner. + - Extend `BaseUnit` to accept a list of vehicles. + - Allow `BuildConst`, `BuildRefinery`, `BuildWeapons` and `HarvesterUnit` to properly have multiple entries. + - Port Rampastring's trigger actions from TS-Patches. + - Allow manually aiming AA buildings. + - Add support for more than 2 sides' loading screens, sidebars and speeches. + - Disallow loading campaign saves from other playthoughs, as well as from skirmish. + - Allow customizing the options color per side. + - Fix a bug where units could gain veterancy by killing allies. + - Fix a bug where a trigger could delete itself, leading to a crash. + - Fix a bug where AI Triggers' `MultiSide` wouldn't correctly consider all houses. + - Fix a bug where newly created objects wouldn't reveal shroud for allies with `AllyReveal=yes`. + - Fix a bug where mission `Ambush` wouldn't work correctly. + - Add unit promotion sounds, EVA and flashing. + \ No newline at end of file diff --git a/docs/Bugfixes.md b/docs/Bugfixes.md index a187c75d7..2080b39a3 100644 --- a/docs/Bugfixes.md +++ b/docs/Bugfixes.md @@ -60,4 +60,8 @@ This page lists all vanilla bugs fixed by Vinifera. - Fix a bug where crew wouldn't exit from construction yards when they were sold or destroyed. - Fix a bug where you could sometimes get extra crew to exit a building that was being sold and was destroying/undeploying. - Fix a bug where if the player loaded a saved game, the score screen timer would report the time since the saved game was loaded, instead of since when the scenario was first started. -- Fix a bug where units could gain veterancy by killing allies. \ No newline at end of file +- Fix a bug where units could gain veterancy by killing allies. +- Fix a bug where a trigger could delete itself, leading to a crash. +- Fix a bug where AI Triggers' `MultiSide` wouldn't correctly consider all houses. +- Fix a bug where newly created objects wouldn't reveal shroud for allies with `AllyReveal=yes`. +- Fix a bug where mission `Ambush` wouldn't work correctly. diff --git a/docs/Mapping.md b/docs/Mapping.md index 2ab85c7cd..d43fdd1c6 100644 --- a/docs/Mapping.md +++ b/docs/Mapping.md @@ -11,14 +11,32 @@ This page describes all mapping-related additions and changes introduced by Vini ## Campaign Settings +### Campaign Side + +- `Side` can now be set for campaigns, allowing the customisation of which **HOUSE**'s loading screens this campaign should use. + +In `BATTLE.INI`: +```ini +[SOMECAMPAIGN] ; Campaign +Side=0 ; integer, the index of the house whose loading screens will be used for this campaign. +``` + +```{note} +To preserve compatibility, the campaign's `Side` defaults to `0` if its scenario names contains `GDI`, to `1` if it contains `NOD`, and to 0 otherwise. +``` + +```{note} +This setting only affects the loading screen graphics used. +``` + ### Intro Movie - `IntroMovie` can now be set for campaigns, allowing the customisation of the intro movie that plays before the campaign path starts. In `BATTLE.INI`: ```ini -[Campaign] -IntroMovie= ; string, the intro movie name (without the .VQA extension) to play at the start of the campaign. +[SOMECAMPAIGN] ; Campaign +IntroMovie= ; string, the intro movie name (without the .VQA extension) to play at the start of the campaign. ``` ### DebugOnly @@ -27,13 +45,38 @@ IntroMovie= ; string, the intro movie name (without the .VQA extension) t In `BATTLE.INI`: ```ini -[Campaign] -DebugOnly=no ; boolean, is this campaign only available in Developer mode? +[SOMECAMPAIGN] ; Campaign +DebugOnly=no ; boolean, is this campaign only available in Developer mode? ``` For testing/debugging versions of the Tiberian Sun and Firestorm campaigns, download [BATTLE_DEBUG_CAMPAIGN.INI](https://github.com/Vinifera-Developers/Vinifera-Files/blob/master/files/BATTLE_DEBUG_CAMPAIGN.INI) and place it in your game install directory. ## Scenario Settings +### AI Base Nodes in Skirmish/Multiplayer + +- Vinifera allows enabling base nodes for the AI outside of campaigns. + +In a scenario file: +```ini +[Basic] +UseMPAIBaseNodes=no ; boolean, should the AI use base nodes for base construction, like in campaign? +``` + +### Custom Loading Screen + +- The scenario file can now specify which loading screen to use. + +In a scenario file: +```ini +[Basic] +LoadingScreen400= ; string, the name of the loading screen to use with this resolution. +LoadingScreen480= ; string, the name of the loading screen to use with this resolution. +LoadingScreen600= ; string, the name of the loading screen to use with this resolution. +LoadingScreen400TextPos= ; Point2D, a custom offset for the loading screen text and bars. +LoadingScreen480TextPos= ; Point2D, a custom offset for the loading screen text and bars. +LoadingScreen600TextPos= ; Point2D, a custom offset for the loading screen text and bars. +``` + ### Ice Destruction - Ice destruction can now be disabled. @@ -60,3 +103,47 @@ ScoreEnemyColor=250,28,28 ; color in R,G,B, color of the enemy's score bars ## Script Actions ## Trigger Actions + +### `106` Give Credits + +- Give `P3` credits to House `P2`. + +### `107` Enable Short Game + +- Enable Short Game. + +### `108` Disable Short Game + +- Disable Short Game. + +### `109` Reserved + +- Does nothing. + +### `110` Blow Up House + +- Blow up all units and structures of House `P2`. + +### `111` Make Elite + +- Make all attached objects elite. + +### `112` Enable AllyReveal + +- Enable `AllyReveal`. + +### `113` Disable AllyReveal + +- Disable `AllyReveal`. + +### `114` Create Auto-Save + +- Schedule the creation of an auto-save at the end of this frame. Works in MP and SP. + +### `115` Delete Object + +- Silently delete all attached objects from the map. + +### `116` Assign Mission to All + +- Assign Mission `P2` to all attached objects. diff --git a/docs/Miscellaneous.md b/docs/Miscellaneous.md index 9fef6c9a9..adc4fccd6 100644 --- a/docs/Miscellaneous.md +++ b/docs/Miscellaneous.md @@ -11,6 +11,127 @@ This page describes every change in Vinifera that wasn't categorized into a prop - Harvesters used to drop their cargo as Tiberium Riparius on death. They will now drop the Tiberium types they are carrying, instead. - It is no longer required to list all Tiberiums in a map to override some Tiberium's properties. - `FreeUnit` or `PadAircraft` would in some cases affect the cost of a building. This functionality has been removed. +- `BaseUnit` now accepts a list of units. Players will be granted the first unit in the list that has their house listed under `Owners=`. +- The AI now correctly considers all entries of `BuildConst`, `BuildRefinery`, `BuildWeapons` and `HarvesterUnit`. + +## Spawner + +- Vinifera implements its own spawner, capable of starting a new singleplayer, skirmish or multiplayer game, as well as loading saved games. +- To start the game in spawner mode, the `-SPAWN` command line argument must be specified. +- The spawner's options can be configures in `SPAWN.INI`. + +In `SPAWN.INI`: +```ini +[Settings] +; Game Mode Options +Bases=yes ; boolean, do players start with MCVs/Construction Yards? +Credits=10000 ; integer, starting amount of credits for the players. +BridgeDestroy=yes ; boolean, can bridges be destroyed? +Crates=no ; boolean, are crates enabled? +ShortGame=no ; boolean, is short game enabled? +BuildOffAlly=no ; boolean, is building off ally bases allowed? +GameSpeed=0 ; integer, starting game speed. +MultiEngineer=no ; boolean, is multi-engineer enabled? +UnitCount=0 ; integer, starting unit count. +AIPlayers=0 ; integer, number of AI players. +AIDifficulty=1 ; integer, AI difficulty. +AlliesAllowed=no ; boolean, can players form and break alliances in-game? +HarvesterTruce=no ; boolean, are harvesters invulnerable? +FogOfWar=no ; boolean, is fog of war enabled? +MCVRedeploy=yes ; boolean, can MCVs be redeployed? + +; Savegame Options +LoadSaveGame=no ; boolean, should the spawner load a saved game, as opposed to starting a new scenario? +SaveGameName= ; string, name of the saved game to load. +AutoSaveInterval=7200 ; integer, interval in frames between auto-saves. +NextAutoSaveNumber=1 ; integer, the number of the next campaign auto-save to make. + +; Scenario Options +Seed=0 ; integer, random seed. +TechLevel=10 ; integer, maximum tech level. +IsCampaign=no ; boolean, is the game that is about to start campaign, as opposed to skirmish? +CampaignID=-1 ; integer, ID of the campaign (from BATTLE.INI) to start +CampaignModeHuman=1 ; DiffType, difficulty used by the human player in Campaign. +CompaignModeComputer=1 ; DiffType, difficulty used by the AI players in Campaign. +Tournament=0 ; integer, WOL Tournament Type +WOLGameID=3735928559 ; unsigned integer, WOL Game ID +ScenarioName=spawnmap.ini ; string, name of the scenario (map) to load. +MapHash= ; string, map hash, only used in statistics collection. +UIMapName= ; string, name of the map, only used in statistics collection. +PlayMoviesInMultiplayer=no ; boolean, should movies be played in multiplayer. + +; Network Options +Protocol=2 ; integer, network protocol to use. +FrameSendRate=4 ; integer, starting FrameSendRate value. +ReconnectTimeout=2400 ; integer, player reconnection timeout. +ConnTimeout=3600 ; integer, player connection timeout. +MaxAhead=-1 ; integer, starting MaxHead value. +PreCalcMaxAhead=0 ; integer, starting PrecalcMaxHead value. +MaxLatencyLevel=255 ; unsigned byte, maximum allowed Protocol 0 latency level. + +; Tunnel Options +TunnelId=0 ; integer, tunnel ID. +TunnelIp=0.0.0.0 ; string, tunnel IP. +TunnelPort=0 ; integer, tunnel port. +ListenPort=1234 ; integer, listen port. + +; Extra Options +Firestorm=yes ; boolean, should the game start with Firestorm enabled? +QuickMatch=no ; boolean, should the game start in Quick Match mode? +SkipScoreScreen=no ; boolean, should the score screen be skipped once the game is over? +WriteStatistics=no ; boolean, should statistics be sent? +AINamesByDifficulty=no ; boolean, should AI players have their difficulty in their name? +CoachMode=no ; boolean, should defeated players that have allies not have the entire map revealed to them upon death? +AutoSurrender=yes ; boolean, should players surrender on disconnection, as opposed to turning their base over to the AI? +AttackNeutralUnits=no ; boolean, should neutral units be targeted by the player's army automatically? +ScrapMetal=no ; boolean, should explosions use alternative animations from the `ScrapExplosion=` list? +ContinueWithoutHumans=yes ; boolean, should the game not end even if the only players left alive are AI? +DifficultyName= ; string, and override for the difficulty name printed at the start of the scenario. +``` + +- Information about the local player is read from the `Settings` section, for all other players - from `OtherX` sections, where `X` ranges from `1` to `7`. + +In `SPAWN.INI`: +```ini +[PLAYERSECTION] +IsHuman=no ; boolean, is this a human player? +Name= ; string, the player's name. +Color=-1 ; integer, the player's color. +House=-1 ; integer, the player's house. +Difficulty=-1 ; integer, the player's difficulty. +Ip=0.0.0.0 ; string, the player's IP address. +Port=-1 ; integer, the player's port. +``` + +- Additionally, AI players (always come after human players) have these options parsed from sections of the format `MultiX`, where `X` ranges from `1` to `8`. + +In `SPAWN.INI`: +```ini +[MULTISECTION] +Color=-1 ; integer, the player's color. +House=-1 ; integer, the player's house. +Difficulty=-1 ; integer, the player's difficulty. +``` + +- Additionally, the spawner reads configuration for each house. Player houses come first, in the order of their color (increasing), then AI houses. +- Alliances are read from sections of the format `MultiX_Alliances`, where `X` ranges from `1` to `8`. + +In `SPAWN.INI`: +```ini +[MULTISECTION] +IsSpectator=no ; boolean, is this house a spectator (observer)? +SpawnLocations=-2 ; integer, spawn location of this house. 90 and -1 mean spectator, -2 means random. + +[ALLIANCESSECTION] +HouseAllyOne=-1 ; integer, index of the house this house is allied to, -1 means none. +HouseAllyTwo=-1 ; integer, index of the house this house is allied to, -1 means none. +HouseAllyThree=-1 ; integer, index of the house this house is allied to, -1 means none. +HouseAllyFour=-1 ; integer, index of the house this house is allied to, -1 means none. +HouseAllyFive=-1 ; integer, index of the house this house is allied to, -1 means none. +HouseAllySix=-1 ; integer, index of the house this house is allied to, -1 means none. +HouseAllySeven=-1 ; integer, index of the house this house is allied to, -1 means none. +HouseAllyEight=-1 ; integer, index of the house this house is allied to, -1 means none. +``` ## Quality of Life @@ -39,6 +160,44 @@ PrePlacedConYards=no ; boolean, should pre-place construction yards instead of ; NOTE: This option has priority over AutoDeployMCV. ``` +## Auto-Saves + +- When playing campaigns, Vinifera will now make auto-saves for the player at equal intervals. The number of auto-saves to keep, as well as the interval, can be customized. + +In `SUN.INI`: +```ini +[Options] +AutoSaveCount=5 ; integer, the number of auto-saves to keep simultaneously. Setting to 0 will disable auto-saves. +AutoSaveInterval=7200 ; integer, the interval between auto-saves, in frames. +``` + +## Human Difficultiy + +- Vinifera adds to possibility to optionally use a different diffiulty level for the human player when their difficulty is set to `Normal`. The new difficulty must have its values be provided in the same manner as vanilla difficulties in a new section, `HumanNormal`. + +In `VINIFERA.INI`: +```ini +[Features] +HumanNormalDifficulty=no ; boolean, should the human player use a separate difficulty when on normal difficulty? +``` + +- Additionally, difficulty names can be customized. + +In `VINIFERA.INI`: +```ini +[Language] +DifficultyEasy=Easy +DifficultyNormal=Normal +DifficultyHard=Hard +DifficultyVeryEasy=Very Easy ; 2 extra difficulties used by the XNA Client (CnCNet) +DifficultyExtremelyEasy=Extremely Easy +DifficultyAIEasy=Hard +DifficultyAINormal=Normal +DifficultyAIHard=Easy +DifficultyAIVeryEasy=Brutal ; 2 extra difficulties used by the XNA Client (CnCNet) +DifficultyAIExtremelyEasy=Ultimate +``` + ## Multi-Engineer - Vinifera fixes `EngineerDamage` and `EngineerCaptureLevel` to be considered by the game, like they were in Tiberian Dawn and Red Alert. @@ -156,7 +315,10 @@ Due to the nature of its use, this feature is only available when Vinifera is ru ### Command Line Options -- Vinifera adds a number of command-line arguments allowing the user to skip the startup movies, or skip directly to a specific game mode and/or dialog. +- Vinifera adds a number of command-line arguments. + +- `-SPAWN` +Launch the game in spawner mode. - `-NO_STARTUP_VIDEO` Skips all startup movies. diff --git a/docs/New-Features-and-Enhancements.md b/docs/New-Features-and-Enhancements.md index 4919e3005..3197ac794 100644 --- a/docs/New-Features-and-Enhancements.md +++ b/docs/New-Features-and-Enhancements.md @@ -93,7 +93,7 @@ It is not recommended to set `EngineerChance=100`, as this may put the game into - In the original game, harvesters always prefer free refineries over occupied ones, even if the free refinery was much farther away than the occupied refinery. Vinifera fixes this so that harvesters now prefer queueing to occupied refineries if they are much closer than free refineries. The distance for this preference is customizable. In `RULES.INI`: -``` +```ini [General] ; When looking for refineries, harvesters will prefer a distant free ; refinery over a closer occupied refinery if the refineries' distance @@ -101,6 +101,31 @@ In `RULES.INI`: MaxFreeRefineryDistanceBias=16 ``` +## Houses + +- Loading screens can now be customized per house. + +In `RULES.INI`: +```ini +[SOMEHOUSE] ; HouseType +LoadingScreens400= ; list of strings, loading screens to be used by this house with a screen resolution of at least 400x600. +LoadingScreens480= ; list of strings, loading screens to be used by this house with a screen resolution of at least 480x600. +LoadingScreens600= ; list of strings, loading screens to be used by this house with a screen resolution of at least 600x800. +``` + +- The defaults for loading screens are as follows: +```ini +LoadingScreens000=LOAD000C,LOAD000D ; House 0 - GDI +LoadingScreens000=LOAD000A,LOAD000B ; House 1 - Nod +LoadingScreens000=LOAD000E,LOAD000F ; House 2 +``` + +- `000` is replaced with the loading screen's height, starting from house 2 letters are incremented (so house 2 uses `E` and `F`, house 3 uses letters `G` and `H`, etc.). After house 12 the letters loop around to `A` and `B`. + +```{note} +Loading screen names should not contain the `.PCX` extension. +``` + ## Ice - Ice strength can now be customized. @@ -185,9 +210,10 @@ SurvivorDivisor= ; integer, this side's survivor divisor. Defaults to `[General In `RULES.INI`: ```ini -[SOMESIDE] ; Side -UIColor=LightGold ; ColorScheme, the color to be used when drawing UI elements. -ToolTipColor=Green ; ColorScheme, the color to be used when drawing tooltips. +[SOMESIDE] ; Side +UIColor=LightGold ; ColorScheme, the color to be used when drawing UI elements. +ToolTipColor=Green ; ColorScheme, the color to be used when drawing tooltips. +OptionsColor=112,255,0 ; RGB Color, the color to be used by the options menu. ``` ![image](https://github.com/user-attachments/assets/f4219655-2d28-49d2-9537-25f2fe4ae102) diff --git a/docs/User-Interface.md b/docs/User-Interface.md index ac59a3600..63102c444 100644 --- a/docs/User-Interface.md +++ b/docs/User-Interface.md @@ -217,6 +217,19 @@ TextLabelOutline=yes ; boolean, should the text be drawn with a b TextLabelBackgroundTransparency=50 ; unsigned integer, the transparency of the text background fill. Ranged between 0 and 100. ``` +### Unit Promotion Indicators + +- In Red Alert 2, unit promotion is indicated by sounds, flashing and an EVA voiceline. Vinifera ports this behavior to Tiberian Sun. + +In `RULES.INI`: +```ini +[AudioVisual] +UpgradeVeteranSound= ; VocType, the sound played when a unit is promoited to veteran status. +UpgradeEliteSound= ; VocType, the sound played when a unit is promoted to elite status. +VoxUnitPromoted= ; VoxType, the EVA line played when a unit is promoted. +EliteFlashTimer=0 ; integer, the number of frames that a newly elite unit will flash for. +``` + ### Unit Health Bar - Vinifera allows customizing the position of the heath bar. diff --git a/docs/Whats-New.md b/docs/Whats-New.md index f5abf7479..99461b423 100644 --- a/docs/Whats-New.md +++ b/docs/Whats-New.md @@ -162,7 +162,14 @@ New: - Implement `WaterAlt` (by ZivDero) - Implement customizable mouse cursors and actions (by CCHyper/tomsons26, ZivDero) - Implement support for a Saved Games subdirectory (by ZivDero) - +- Implemented the multiplayer spawner (by ZivDero) +- Extend `BaseUnit` to accept a list of vehicles (by ZivDero/CCHyper) +- Allow `BuildConst`, `BuildRefinery`, `BuildWeapons` and `HarvesterUnit` to properly have multiple entries (by ZivDero) +- Port trigger actions from TS-Patches (by ZivDero, Rampastring) +- Allow manually aiming AA buildings (by ZivDero) +- Add support for more than 2 sides' loading screens, sidebars and speeches (by CCHyper/tomsons26, ZivDero) +- Disallow loading campaign saves from other playthoughs, as well as from skirmish (by ZivDero) +- Allow customizing the options color per side (by ZivDero) Vanilla fixes: - Fix HouseType `Nod` having the `Prefix=B` and `Side=GDI` in vanilla `rules.ini` by setting them to `N` and `Nod`, respectively (by CCHyper/tomsons26) @@ -206,7 +213,7 @@ Vanilla fixes: - Fix an issue where losers were not marked as defeated in multiplayer when using TACTION_WIN or TACTION_LOSE to end the game (by Rampastring) - Fix a bug where under some circumstances, the player could hear "New Construction Options", even though no new construction options were available (by ZivDero) - Fix a bug where attempting to start construction when low funds would put the queue on hold (by ZivDero) -- Port the fix for the (Whiteboy bug)[https://modenc.renegadeprojects.com/Whiteboy-Bug] (by ZivDero) +- Port the fix for the [Whiteboy bug](https://modenc.renegadeprojects.com/Whiteboy-Bug) (by ZivDero) - Fix a bug where the objects would sometimes receive a minimum of 1 damage even if MinDamage was set to 0 (by ZivDero) - Fix a bug where aircraft are unable to attack shrouded targets in campaign games and instead get stuck in mid-air (by Rampastring) - Fix a bug where the player was able to input keyboard commands while input was locked through a trigger action (by Rampastring) @@ -224,6 +231,11 @@ Vanilla fixes: - Fix a bug where you could sometimes get extra crew to exit a building that was being sold and was destroying/undeploying (by ZivDero) - Fix a bug where if the player loaded a saved game, the score screen timer would report the time since the saved game was loaded, instead of since when the scenario was first started (by ZivDero) - Fix a bug where units could gain veterancy by killing allies (by ZivDero) +- Fix a bug where a trigger could delete itself, leading to a crash (by ZivDero) +- Fix a bug where AI Triggers' `MultiSide` wouldn't correctly consider all houses (by ZivDero) +- Fix a bug where newly created objects wouldn't reveal shroud for allies with `AllyReveal=yes` (by ZivDero) +- Fix a bug where mission `Ambush` wouldn't work correctly (by ZivDero) +- Add unit promotion sounds, EVA and flashing (by ZivDero) diff --git a/src/cncnet/cncnet4/cncnet4.cpp b/src/cncnet/cncnet4/cncnet4.cpp deleted file mode 100644 index 9c6e57842..000000000 --- a/src/cncnet/cncnet4/cncnet4.cpp +++ /dev/null @@ -1,413 +0,0 @@ -/******************************************************************************* -/* O P E N S O U R C E -- V I N I F E R A ** -/******************************************************************************* - * - * @project Vinifera - * - * @file CNCNET4.CPP - * - * @author CCHyper (Based on work by Toni Spets) - * - * @brief CnCNet4 replacements for low level networking API. - * - * @license Vinifera 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. - * - * Vinifera 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 . - * - ******************************************************************************/ -#include "cncnet4.h" -#include "cncnet4_net.h" -#include "cncnet4_globals.h" -#include "rawfile.h" -#include "ini.h" -#include "debughandler.h" -#include "asserthandler.h" - -#include -#include -#include -#include -#include - - -/** - * Initialises the CnCNet4 system. - */ -bool __stdcall CnCNet4::Init() -{ - RawFileClass file("SUN.INI"); - INIClass ini; - - /** - * Load the CnCNet4 settings. - */ - ini.Load(file); - if (ini.Is_Present("CnCNet4")) { - CnCNet4::IsEnabled = ini.Get_Bool("CnCNet4", "Enabled", CnCNet4::IsEnabled); - ini.Get_String("CnCNet4", "Host", CnCNet4::Host, sizeof(CnCNet4::Host)); - CnCNet4::Peer2Peer = ini.Get_Bool("CnCNet4", "P2P", CnCNet4::Peer2Peer); - CnCNet4::UseUDP = ini.Get_Bool("CnCNet4", "UDP", CnCNet4::UseUDP); - CnCNet4::Port = ini.Get_Int("CnCNet4", "Port", CnCNet4::Port); - } - - if (!CnCNet4::IsEnabled) { - - /** - * Nothing went wrong, but the CnCNet4 interface is not enabled. - */ - return true; - } - - /** - * Check to make sure the CnCNet4 DLL is not present in the directory. - */ - if (RawFileClass("wsock32.dll").Is_Available()) { - MessageBox(nullptr, "File conflict detected!\nPlease remove WSOCK32.DLL from the game directory!", "Vinifera", MB_OK|MB_ICONEXCLAMATION); - return false; - } - if (RawFileClass("cncnet.dll").Is_Available()) { - MessageBox(nullptr, "File conflict detected!\nPlease remove CNCNET.DLL from the game directory!", "Vinifera", MB_OK|MB_ICONEXCLAMATION); - return false; - } - - int s = net_init(); - net_opt_reuse(); - - DEBUG_INFO("CnCNet4: Initialising...\n"); - - if (CnCNet4::Port < 1024 || CnCNet4::Port > 65534) { - CnCNet4::Port = 9001; - } - - DEBUG_INFO("CnCNet4: Broadcasting to \"%s:%d\".\n", CnCNet4::Host, CnCNet4::Port); - - CnCNet4::IsDedicated = true; - - if (!net_address(&CnCNet4::Server, CnCNet4::Host, CnCNet4::Port)) { - return false; - } - - if (CnCNet4::Peer2Peer) { - - /** - * Test P2P. - */ - bool pass = false; - - fd_set rfds; - struct timeval tv; - struct sockaddr_in to; - struct sockaddr_in from; - int start = time(nullptr); - - net_write_int8(CMD_TESTP2P); - net_write_int32(start); - - while (time(nullptr) < start + 5) { - - net_send_noflush(&to); - - FD_ZERO(&rfds); - FD_SET(s, &rfds); - tv.tv_sec = 1; - tv.tv_usec = 0; - - if (select(s + 1, &rfds, NULL, NULL, &tv) > -1) { - if (FD_ISSET(s, &rfds)) { - net_recv(&from); - - if (net_read_int8() == CMD_TESTP2P && net_read_int32() == start) { - pass = true; - break; - } - } - } - } - - /** - * Enable P2P if passed. - */ - if (pass) { - DEBUG_INFO("CnCNet4: Peer-to-peer test passed.\n"); - DEBUG_INFO("CnCNet4: Peer-to-peer is enabled.\n"); - net_bind("0.0.0.0", 8054); - - } else { - DEBUG_WARNING("CnCNet4: Peer-to-peer test failed!\n"); - //return false; - } - - } - - return true; -} - - - -/** - * Shutdown the CnCNet4 system. - */ -void __stdcall CnCNet4::Shutdown() -{ - net_free(); -} - - -SOCKET __stdcall CnCNet4::socket(int af, int type, int protocol) -{ -#ifndef NDEBUG - DEV_DEBUG_INFO("CnCNet4: socket(af=%08X, type=%08X, protocol=%08X)\n", af, type, protocol); -#endif - - if (af == AF_IPX) { - return net_socket; - } - - return ::socket(af, type, protocol); -} - - -int __stdcall CnCNet4::bind(SOCKET s, const struct sockaddr *name, int namelen) -{ -#ifndef NDEBUG - DEV_DEBUG_INFO("CnCNet4: bind(s=%d, name=%p, namelen=%d)\n", s, name, namelen); -#endif - - if (s == net_socket) { - return 0; - } - - return ::bind(s, name, namelen); -} - - -int __stdcall CnCNet4::recvfrom(SOCKET s, char *buf, int len, int flags, struct sockaddr *from, int *fromlen) -{ -#ifndef NDEBUG - //DEV_DEBUG_INFO("CnCNet4: recvfrom(s=%d, buf=%p, len=%d, flags=%08X, from=%p, fromlen=%p (%d))\n", s, buf, len, flags, from, fromlen, *fromlen); -#endif - - if (s == net_socket) { - int ret; - struct sockaddr_in from_in; - - ret = net_recv(&from_in); - - if (ret > 0) { - - if (CnCNet4::IsDedicated) { - - if (from_in.sin_addr.s_addr == CnCNet4::Server.sin_addr.s_addr && from_in.sin_port == CnCNet4::Server.sin_port) { - uint8_t cmd = net_read_int8(); - - /** - * Handle keepalive packets from server, very special case. - */ - if (cmd == CMD_PING) { - net_write_int8(CMD_PING); - net_write_int32(net_read_int32()); - net_send(&from_in); - - /** - * #FIXME: returning 0 means disconnected - */ - return 0; - } - - /** - * P2p flag. - */ - from_in.sin_zero[0] = cmd; - - from_in.sin_addr.s_addr = net_read_int32(); - from_in.sin_port = net_read_int16(); - - } else if (CnCNet4::Peer2Peer) { - /** - * P2p flag for direct packets. - */ - from_in.sin_zero[0] = 1; - - } else { - /** - * Discard p2p packets if not in p2p mode. - * #FIXME: returning 0 means disconnected - */ - return 0; - } - - /** - * Force p2p port. - */ - if (from_in.sin_zero[0]) { - from_in.sin_port = htons(8054); - } - - in2ipx(&from_in, (struct sockaddr_ipx *)from); - - } else { - in2ipx(&from_in, (struct sockaddr_ipx *)from); - } - - ret = net_read_data((void *)buf, len); - } - - return ret; - } - - return ::recvfrom(s, buf, len, flags, from, fromlen); -} - - -int __stdcall CnCNet4::sendto(SOCKET s, const char *buf, int len, int flags, const struct sockaddr *to, int tolen) -{ -#ifndef NDEBUG - //DEV_DEBUG_INFO("CnCNet4: sendto(s=%d, buf=%p, len=%d, flags=%08X, to=%p, tolen=%d)\n", s, buf, len, flags, to, tolen); -#endif - - if (to->sa_family == AF_IPX) { - struct sockaddr_in to_in; - - if (CnCNet4::IsDedicated) { - - if (is_ipx_broadcast((struct sockaddr_ipx *)to)) { - net_write_int8(CnCNet4::Peer2Peer ? 1 : 0); - net_write_int32(0xFFFFFFFF); - net_write_int16(0xFFFF); - net_write_data((void *)buf, len); - net_send(&CnCNet4::Server); - - } else { - - ipx2in((struct sockaddr_ipx *)to, &to_in); - - /** - * Use p2p only if both clients are in p2p mode. - */ - if (to_in.sin_zero[0] && CnCNet4::Peer2Peer) { - net_write_data((void *)buf, len); - net_send(&to_in); - - } else { - net_write_int8(CnCNet4::Peer2Peer ? 1 : 0); - net_write_int32(to_in.sin_addr.s_addr); - net_write_int16(to_in.sin_port); - net_write_data((void *)buf, len); - net_send(&CnCNet4::Server); - } - } - - return len; - } - - ipx2in((struct sockaddr_ipx *)to, &to_in); - net_write_data((void *)buf, len); - - /** - * Check if it's a broadcast. - */ - if (is_ipx_broadcast((struct sockaddr_ipx *)to)) { - net_send(&CnCNet4::Server); - return len; - - } else { - return net_send(&to_in); - } - } - - return ::sendto(s, buf, len, flags, to, tolen); -} - - -int __stdcall CnCNet4::getsockopt(SOCKET s, int level, int optname, char *optval, int *optlen) -{ -#ifndef NDEBUG - DEV_DEBUG_INFO("CnCNet4: getsockopt(s=%d, level=%08X, optname=%08X, optval=%p, optlen=%p (%d))\n", s, level, optname, optval, optlen, *optlen); -#endif - - if (level == 0x3E8) { - *optval = 1; - *optlen = 1; - return 0; - } - - if (level == 0xFFFF) { - *optval = 1; - *optlen = 1; - return 0; - } - - return ::getsockopt(s, level, optname, optval, optlen); -} - - -int __stdcall CnCNet4::setsockopt(SOCKET s, int level, int optname, const char *optval, int optlen) -{ -#ifndef NDEBUG - DEV_DEBUG_INFO("CnCNet4: setsockopt(s=%d, level=%08X, optname=%08X, optval=%p, optlen=%d)\n", s, level, optname, optval, optlen); -#endif - - if (level == 0x3E8) { - return 0; - } - if (level == 0xFFFF) { - return 0; - } - - return ::setsockopt(s, level, optname, optval, optlen); -} - - -int __stdcall CnCNet4::closesocket(SOCKET s) -{ -#ifndef NDEBUG - DEV_DEBUG_INFO("CnCNet4: closesocket(s=%d)\n", s); -#endif - - if (s == net_socket) { - if (CnCNet4::IsDedicated) { - net_write_int8(CMD_DISCONNECT); - net_send(&CnCNet4::Server); - } - return 0; - } - - return ::closesocket(s); -} - - -int __stdcall CnCNet4::getsockname(SOCKET s, struct sockaddr *name, int *namelen) -{ -#ifndef NDEBUG - DEV_DEBUG_INFO("CnCNet4: getsockname(s=%d, name=%p, namelen=%p (%d)\n", s, name, namelen, *namelen); -#endif - - if (s == net_socket) { - struct sockaddr_in name_in; - char hostname[256]; - struct hostent *he; - - gethostname(hostname, 256); - he = ::gethostbyname(hostname); - - DEBUG_INFO("getsockname: local hostname: %s\n", hostname); - - if (he) { - DEBUG_INFO("getsockname: local ip: %s\n", inet_ntoa(*(struct in_addr *)(he->h_addr_list[0]))); - name_in.sin_addr = *(struct in_addr *)(he->h_addr_list[0]); - in2ipx(&name_in, (struct sockaddr_ipx *)name); - } - } - - return ::getsockname(s, name, namelen);; -} diff --git a/src/cncnet/cncnet4/cncnet4.h b/src/cncnet/cncnet4/cncnet4.h deleted file mode 100644 index a88d95fea..000000000 --- a/src/cncnet/cncnet4/cncnet4.h +++ /dev/null @@ -1,48 +0,0 @@ -/******************************************************************************* -/* O P E N S O U R C E -- V I N I F E R A ** -/******************************************************************************* - * - * @project Vinifera - * - * @file CNCNET4.H - * - * @author CCHyper - * - * @brief CnCNet4 replacements for low level networking API. - * - * @license Vinifera 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. - * - * Vinifera 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 . - * - ******************************************************************************/ -#pragma once - -#include -#include - - -namespace CnCNet4 { - -bool __stdcall Init(); -void __stdcall Shutdown(); - -int __stdcall bind(SOCKET s, const struct sockaddr *name, int namelen); -SOCKET __stdcall socket(int af, int type, int protocol); -int __stdcall recvfrom(SOCKET s, char *buf, int len, int flags, struct sockaddr *from, int *fromlen); -int __stdcall sendto(SOCKET s, const char *buf, int len, int flags, const struct sockaddr *to, int tolen); -int __stdcall getsockopt(SOCKET s, int level, int optname, char *optval, int *optlen); -int __stdcall setsockopt(SOCKET s, int level, int optname, const char *optval, int optlen); -int __stdcall closesocket(SOCKET s); -int __stdcall getsockname(SOCKET s, struct sockaddr *name, int *namelen); - -}; // namespace CnCNet4 diff --git a/src/cncnet/cncnet4/cncnet4_hooks.cpp b/src/cncnet/cncnet4/cncnet4_hooks.cpp deleted file mode 100644 index b0a8e488b..000000000 --- a/src/cncnet/cncnet4/cncnet4_hooks.cpp +++ /dev/null @@ -1,185 +0,0 @@ -/******************************************************************************* -/* O P E N S O U R C E -- V I N I F E R A ** -/******************************************************************************* - * - * @project Vinifera - * - * @file CNCNET4_HOOKS.CPP - * - * @author CCHyper - * - * @brief Contains the hooks for the CnCNet4 system. - * - * @license Vinifera 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. - * - * Vinifera 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 . - * - ******************************************************************************/ -#include "cncnet4_hooks.h" -#include "cncnet4_globals.h" -#include "cncnet4.h" -#include "tibsun_globals.h" -#include "session.h" -#include "wspudp.h" -#include "wspipx.h" -#include "debughandler.h" - -#include "hooker.h" -#include "hooker_macros.h" - - -static int __stdcall bind_intercept(SOCKET s, const struct sockaddr *name, int namelen) -{ - if (CnCNet4::IsEnabled) { - return CnCNet4::bind(s, name, namelen); - } else { - return ::bind(s, name, namelen); - } -} - -static int __stdcall closesocket_intercept(SOCKET s) -{ - if (CnCNet4::IsEnabled) { - return CnCNet4::closesocket(s); - } else { - return ::closesocket(s); - } -} - -static int __stdcall getsockname_intercept(SOCKET s, struct sockaddr *name, int *namelen) -{ - if (CnCNet4::IsEnabled) { - return CnCNet4::getsockname(s, name, namelen); - } else { - return ::getsockname(s, name, namelen); - } -} - -static int __stdcall getsockopt_intercept(SOCKET s, int level, int optname, char *optval, int *optlen) -{ - if (CnCNet4::IsEnabled) { - return CnCNet4::getsockopt(s, level, optname, optval, optlen); - } else { - return ::getsockopt(s, level, optname, optval, optlen); - } -} - -static int __stdcall recvfrom_intercept(SOCKET s, char *buf, int len, int flags, struct sockaddr *from, int *fromlen) -{ - if (CnCNet4::IsEnabled) { - return CnCNet4::recvfrom(s, buf, len, flags, from, fromlen); - } else { - return ::recvfrom(s, buf, len, flags, from, fromlen); - } -} - -static int __stdcall sendto_intercept(SOCKET s, const char *buf, int len, int flags, const struct sockaddr *to, int tolen) -{ - if (CnCNet4::IsEnabled) { - return CnCNet4::sendto(s, buf, len, flags, to, tolen); - } else { - return ::sendto(s, buf, len, flags, to, tolen); - } -} - -static int __stdcall setsockopt_intercept(SOCKET s, int level, int optname, const char *optval, int optlen) -{ - if (CnCNet4::IsEnabled) { - return CnCNet4::setsockopt(s, level, optname, optval, optlen); - } else { - return ::setsockopt(s, level, optname, optval, optlen); - } -} - -static SOCKET __stdcall socket_intercept(int af, int type, int protocol) -{ - if (CnCNet4::IsEnabled) { - return CnCNet4::socket(af, type, protocol); - } else { - return ::socket(af, type, protocol); - } -} - - -/** - * #issue-504 - * - * Create the CnCNet4 UDP interface or standard UDP interface depending - * on if the CnCNet4 system has been enabled. - * - * @author: CCHyper - */ -static void CnCNet_Create_PacketTransport() -{ - bool created = false; - - if (CnCNet4::IsEnabled && CnCNet4::UseUDP) { - PacketTransport = new UDPInterfaceClass(); - if (PacketTransport) { - DEBUG_INFO("UDP PacketTransport for CnCNet4.\n"); - } - - created = (PacketTransport != nullptr); - } - - if (!created) { - PacketTransport = new IPXInterfaceClass(); - if (PacketTransport) { - DEBUG_INFO("IPX PacketTransport created.\n"); - } - } - - if (!PacketTransport) { - DEBUG_ERROR("Failed to create PacketTransport!\n"); - } -} - - -/** - * #issue-504 - * - * This patch replaces the call to the IPXInterfaceClass constructor when - * setting up the PacketTransport for network multiplayer games with - * conditional code that creates the UDPInterfaceClass when enabled. - * - * @author: CCHyper - */ -DECLARE_PATCH(_Select_Game_Network_Create_PacketTransport_Patch) -{ - Session.Type = GAME_IPX; - Session.CommProtocol = COMM_PROTOCOL_MULTI_E_COMP; - - if (!PacketTransport) { - CnCNet_Create_PacketTransport(); - } - - JMP(0x004E2698); -} - - -/** - * Main function for patching the hooks. - */ -void CnCNet4_Hooks() -{ - Patch_Jump(0x006B4D54, &bind_intercept); - Patch_Jump(0x006B4D4E, &closesocket_intercept); - //Patch_Jump(0x, &getsockname_intercept); - Patch_Jump(0x006B4D48, &getsockopt_intercept); - Patch_Jump(0x006B4D66, &recvfrom_intercept); - Patch_Jump(0x006B4D6C, &sendto_intercept); - Patch_Jump(0x006B4D60, &setsockopt_intercept); - Patch_Jump(0x006B4D5A, &socket_intercept); - - Patch_Jump(0x004E2656, &_Select_Game_Network_Create_PacketTransport_Patch); -} diff --git a/src/cncnet/cncnet4/cncnet4_net.cpp b/src/cncnet/cncnet4/cncnet4_net.cpp deleted file mode 100644 index c7b2f664e..000000000 --- a/src/cncnet/cncnet4/cncnet4_net.cpp +++ /dev/null @@ -1,310 +0,0 @@ -/******************************************************************************* -/* O P E N S O U R C E -- V I N I F E R A ** -/******************************************************************************* - * - * @project Vinifera - * - * @file CNCNET4_NET.CPP - * - * @author CCHyper (Based on work by Toni Spets) - * - * @brief Network utility functions. - * - * @license Vinifera 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. - * - * Vinifera 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 . - * - ******************************************************************************/ -#include -#include -#include - -#include "cncnet4_net.h" - -#include "debughandler.h" -#include "asserthandler.h" - - -static struct sockaddr_in net_local; -static uint8_t net_ibuf[NET_BUF_SIZE]; -static uint8_t net_obuf[NET_BUF_SIZE]; -static uint32_t net_ipos; -static uint32_t net_ilen; -static uint32_t net_opos; -int net_socket = 0; - - -void ipx2in(struct sockaddr_ipx *from, struct sockaddr_in *to) -{ - to->sin_family = AF_INET; - std::memcpy(&to->sin_addr.s_addr, from->sa_nodenum, 4); - std::memcpy(&to->sin_port, from->sa_nodenum + 4, 2); - to->sin_zero[0] = from->sa_netnum[1]; -} - - -void in2ipx(struct sockaddr_in *from, struct sockaddr_ipx *to) -{ - to->sa_family = AF_IPX; - *(DWORD *)&to->sa_netnum = 1; - to->sa_netnum[1] = from->sin_zero[0]; - std::memcpy(to->sa_nodenum, &from->sin_addr.s_addr, 4); - std::memcpy(to->sa_nodenum + 4, &from->sin_port, 2); -} - - -bool is_ipx_broadcast(struct sockaddr_ipx *addr) -{ - unsigned char ff[] = { 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF }; - if (std::memcmp(addr->sa_netnum, ff, 4) == 0 || std::memcmp(addr->sa_nodenum, ff, 6) == 0) { - return true; - } else { - return false; - } -} - - -int net_opt_reuse() -{ - int yes = 1; - ::setsockopt(net_socket, SOL_SOCKET, SO_REUSEADDR, (char *)&yes, sizeof(yes)); - return yes; -} - - -int net_opt_broadcast() -{ - int yes = 1; - ::setsockopt(net_socket, SOL_SOCKET, SO_BROADCAST, (char *)&yes, sizeof(yes)); - return yes; -} - - -int net_address(struct sockaddr_in *addr, const char *host, uint16_t port) -{ - struct hostent *hent; - hent = ::gethostbyname(host); - if (!hent) { - DEBUG_ERROR("CnCNet4: gethostbyname failed!\n"); - return 0; - } - - net_address_ex(addr, *(int *)hent->h_addr_list[0], port); - return 1; -} - - -void net_address_ex(struct sockaddr_in *addr, uint32_t ip, uint16_t port) -{ - int size = sizeof(struct sockaddr_in); - std::memset(addr, 0, size); - addr->sin_family = AF_INET; - addr->sin_addr.s_addr = ip; - addr->sin_port = htons(port); -} - - -int net_init() -{ - WSADATA wsaData; - - if (net_socket) { - net_free(); - } - - WSAStartup(0x0101, &wsaData); - - net_socket = ::socket(AF_INET, SOCK_DGRAM, 0); - net_ipos = 0; - net_opos = 0; - return net_socket; -} - - -void net_free() -{ - ::closesocket(net_socket); - WSACleanup(); - net_socket = 0; -} - - -int net_bind(const char *ip, int port) -{ - if (!net_socket) { - return 0; - } - - net_address(&net_local, ip, port); - - return ::bind(net_socket, (struct sockaddr *)&net_local, sizeof(net_local)); -} - - -uint32_t net_read_size() -{ - return net_ilen - net_ipos; -} - - -int8_t net_read_int8() -{ - int8_t tmp; - if (net_ipos + 1 > net_ilen) { - return 0; - } - std::memcpy(&tmp, net_ibuf + net_ipos, 1); - net_ipos += 1; - return tmp; -} - - -int16_t net_read_int16() -{ - int16_t tmp; - if (net_ipos + 2 > net_ilen) { - return 0; - } - std::memcpy(&tmp, net_ibuf + net_ipos, 2); - net_ipos += 2; - return tmp; -} - - -int32_t net_read_int32() -{ - int32_t tmp; - if (net_ipos + 4 > net_ilen) { - return 0; - } - std::memcpy(&tmp, net_ibuf + net_ipos, 4); - net_ipos += 4; - return tmp; -} - - -int net_read_data(void *ptr, size_t len) -{ - if (net_ipos + len > net_ilen) { - len = net_ilen - net_ipos; - } - - std::memcpy(ptr, net_ibuf + net_ipos, len); - net_ipos += len; - return len; -} - - -int net_read_string(char *str, size_t len) -{ - int i; - for (i = net_ipos; i < NET_BUF_SIZE; i++) { - if (net_ibuf[i] == '\0') { - break; - } - } - - if (len > i - net_ipos) { - len = i - net_ipos; - } - - std::memcpy(str, net_ibuf + net_ipos, len); - str[len] = '\0'; - net_ipos += len + 1; - return 0; -} - - -int net_write_int8(int8_t d) -{ - ASSERT(net_opos + 1 <= NET_BUF_SIZE); - std::memcpy(net_obuf + net_opos, &d, 1); - net_opos += 1; - return 1; -} - - -int net_write_int16(int16_t d) -{ - ASSERT(net_opos + 2 <= NET_BUF_SIZE); - int16_t tmp = d; - std::memcpy(net_obuf + net_opos, &tmp, 2); - net_opos += 2; - return 1; -} - - -int net_write_int32(int32_t d) -{ - ASSERT(net_opos + 4 <= NET_BUF_SIZE); - int32_t tmp = d; - std::memcpy(net_obuf + net_opos, &tmp, 4); - net_opos += 4; - return 1; -} - - -int net_write_data(void *ptr, size_t len) -{ - ASSERT(net_opos + len <= NET_BUF_SIZE); - std::memcpy(net_obuf + net_opos, ptr, len); - net_opos += len; - return 1; -} - - -int net_write_string(char *str) -{ - ASSERT(net_opos + strlen(str) + 1 <= NET_BUF_SIZE); - std::memcpy(net_obuf + net_opos, str, strlen(str) + 1); - net_opos += strlen(str) + 1; - return 1; -} - - -int net_write_string_int32(int32_t d) -{ - char str[32]; - std::snprintf(str, sizeof(str), "%d", d); - return net_write_string(str); -} - - -int net_recv(struct sockaddr_in *src) -{ - socklen_t l = sizeof(struct sockaddr_in); - net_ipos = 0; - net_ilen = ::recvfrom(net_socket, (char *)net_ibuf, NET_BUF_SIZE, 0, (struct sockaddr *)src, &l); - return net_ilen; -} - - -int net_send(struct sockaddr_in *dst) -{ - int ret = net_send_noflush(dst); - net_send_discard(); - return ret; -} - - -int net_send_noflush(struct sockaddr_in *dst) -{ - int ret = ::sendto(net_socket, (char *)net_obuf, net_opos, 0, (struct sockaddr *)dst, sizeof(struct sockaddr_in)); - return ret; -} - - -void net_send_discard() -{ - net_opos = 0; -} diff --git a/src/cncnet/cncnet4/cncnet4_net.h b/src/cncnet/cncnet4/cncnet4_net.h deleted file mode 100644 index 862de3391..000000000 --- a/src/cncnet/cncnet4/cncnet4_net.h +++ /dev/null @@ -1,84 +0,0 @@ -/******************************************************************************* -/* O P E N S O U R C E -- V I N I F E R A ** -/******************************************************************************* - * - * @project Vinifera - * - * @file CNCNET4_NET.H - * - * @author CCHyper (Based on work by Toni Spets) - * - * @brief Network utility functions. - * - * @license Vinifera 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. - * - * Vinifera 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 . - * - ******************************************************************************/ -#pragma once - -#include -#include -#include -#include - - -#define NET_BUF_SIZE 2048 - -typedef int socklen_t; - -enum { - CMD_TUNNEL, - CMD_P2P, - CMD_DISCONNECT, - CMD_PING, - CMD_QUERY, - CMD_TESTP2P -}; - - -extern int net_socket; - -void ipx2in(struct sockaddr_ipx *from, struct sockaddr_in *to); -void in2ipx(struct sockaddr_in *from, struct sockaddr_ipx *to); -bool is_ipx_broadcast(struct sockaddr_ipx *addr); - -int net_opt_reuse(); -int net_opt_broadcast(); - -int net_address(struct sockaddr_in *addr, const char *host, uint16_t port); -void net_address_ex(struct sockaddr_in *addr, uint32_t ip, uint16_t port); - -int net_init(); -void net_free(); - -int net_bind(const char *ip, int port); - -uint32_t net_read_size(); -int8_t net_read_int8(); -int16_t net_read_int16(); -int32_t net_read_int32(); -int net_read_data(void *, size_t); -int net_read_string(char *str, size_t len); - -int net_write_int8(int8_t); -int net_write_int16(int16_t); -int net_write_int32(int32_t); -int net_write_data(void *, size_t); -int net_write_string(char *str); -int net_write_string_int32(int32_t); - -int net_recv(struct sockaddr_in *); -int net_send(struct sockaddr_in *); -int net_send_noflush(struct sockaddr_in *dst); -void net_send_discard(); diff --git a/src/cncnet/cncnet5/cncnet5_hooks.cpp b/src/cncnet/cncnet5/cncnet5_hooks.cpp deleted file mode 100644 index e4556a5d1..000000000 --- a/src/cncnet/cncnet5/cncnet5_hooks.cpp +++ /dev/null @@ -1,99 +0,0 @@ -/******************************************************************************* -/* O P E N S O U R C E -- V I N I F E R A ** -/******************************************************************************* - * - * @project Vinifera - * - * @file CNCNET5_HOOKS.CPP - * - * @author CCHyper - * - * @brief Contains the hooks for implementing the CnCNet5 system. - * - * @license Vinifera 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. - * - * Vinifera 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 . - * - ******************************************************************************/ -#include "cncnet5_hooks.h" -#include "cncnet5_globals.h" -#include "cncnet5_wspudp.h" -#include "wsproto.h" -#include "wspipx.h" -#include "wspudp.h" -#include "tibsun_globals.h" -#include "session.h" -#include "debughandler.h" -#include "asserthandler.h" -#include "hooker.h" -#include "hooker_macros.h" - - -/** - * #issue-69 - * - * Create the CnCNet5 UDP interface or standard UDP interface depending - * on if the CnCNet5 system has been enabled. - * - * @author: CCHyper - */ -static void Create_PacketTransport() -{ - if (CnCNet5::IsActive && CnCNet5::TunnelInfo.Is_Valid()) { - PacketTransport = new CnCNet5UDPInterfaceClass( - CnCNet5::TunnelInfo.ID, - CnCNet5::TunnelInfo.IP, - CnCNet5::TunnelInfo.Port, - CnCNet5::TunnelInfo.PortHack); - if (!PacketTransport) { - DEBUG_ERROR("Failed to create PacketTransport for CnCNet5!\n"); - } - - } else { - PacketTransport = new UDPInterfaceClass(); - if (!PacketTransport) { - DEBUG_ERROR("Failed to create PacketTransport!\n"); - } - } -} - - -/** - * #issue-69 - * - * This patch replaces the call to the UDPInterfaceClass constructor when - * setting up the PacketTransport for network multiplayer games with - * conditional code that creates the CnCNet5 interface is enabled. - * - * @author: CCHyper - */ -DECLARE_PATCH(_Select_Game_Create_PacketTransport_Patch) -{ - Create_PacketTransport(); - - Session.CommProtocol = COMM_PROTOCOL_SINGLE_NO_COMP; - - _asm { mov eax, [0x0074C8D8] } // PacketProtocol - _asm { mov eax, [eax] } - - JMP_REG(edx, 0x004E2436); -} - - -/** - * Main function for patching the hooks. - */ -void CnCNet5_Hooks() -{ - Patch_Jump(0x004E2406, &_Select_Game_Create_PacketTransport_Patch); -} diff --git a/src/cncnet/cncnet5/cncnet5_globals.h b/src/extensions/aitriggertype/aitriggertypeext_hooks.cpp similarity index 52% rename from src/cncnet/cncnet5/cncnet5_globals.h rename to src/extensions/aitriggertype/aitriggertypeext_hooks.cpp index 7ad79c15e..c01898f50 100644 --- a/src/cncnet/cncnet5/cncnet5_globals.h +++ b/src/extensions/aitriggertype/aitriggertypeext_hooks.cpp @@ -4,11 +4,11 @@ * * @project Vinifera * - * @file CNCNET_GLOBALS.H + * @file AITRIGGERTYPEEXT_HOOKS.H * - * @author CCHyper + * @author ZivDero * - * @brief Global values and types used for the CnCNet5 system. + * @brief Contains the hooks for the extended AITriggerType class. * * @license Vinifera is free software: you can redistribute it and/or * modify it under the terms of the GNU General Public License @@ -25,39 +25,50 @@ * If not, see . * ******************************************************************************/ -#pragma once +#include "aitriggertypeext_hooks.h" -#include +#include "aitrigtype.h" +#include "debughandler.h" +#include "asserthandler.h" +#include "tibsun_globals.h" +#include "house.h" +#include "vector.h"; -namespace CnCNet5 -{ +#include "hooker.h" +#include "hooker_macros.h" +#include "scenario.h" +#include "vinifera_globals.h" -typedef struct TunnelInfoStruct -{ - unsigned long ID; - unsigned long IP; - unsigned short Port; - bool PortHack; - bool Is_Valid() const { return !(ID == -1 || IP == -1 || Port == -1); } +/** + * Patch that AITriggerTypes no longer assume that not-GDI means Nod and vice versa. + * + * @author: ZivDero + */ +DECLARE_PATCH(_AITriggerTypeClass_Process_MultiSide_Patch) +{ + GET_REGISTER_STATIC(AITriggerTypeClass*, trigtype, esi); + GET_REGISTER_STATIC(HouseClass*, house, ebp); -} TunnelInfoStruct; + _asm push ecx + if (trigtype->MultiSide != 0 && trigtype->MultiSide != house->ActLike + 1) + { + // return 0; + _asm pop ecx + JMP(0x00410A00); + } -/** - * Has the CnCNet5 system been activated? - */ -extern bool IsActive; + _asm pop ecx + JMP(0x00410A1F); +} -/** - * Is the tunnel system active (set when tunnel information has been provided)? - */ -extern bool IsTunnelActive; /** - * CnCNet5 UDP Tunnel info. + * Main function for patching the hooks. */ -extern TunnelInfoStruct TunnelInfo; - -}; +void AITriggerTypeClassExtension_Hooks() +{ + Patch_Jump(0x004109EF, &_AITriggerTypeClass_Process_MultiSide_Patch); +} diff --git a/src/extensions/aitriggertype/aitriggertypeext_hooks.h b/src/extensions/aitriggertype/aitriggertypeext_hooks.h new file mode 100644 index 000000000..0553fc0e1 --- /dev/null +++ b/src/extensions/aitriggertype/aitriggertypeext_hooks.h @@ -0,0 +1,31 @@ +/******************************************************************************* +/* O P E N S O U R C E -- V I N I F E R A ** +/******************************************************************************* + * + * @project Vinifera + * + * @file AITRIGGERTYPEEXT_HOOKS.H + * + * @author ZivDero + * + * @brief Contains the hooks for the extended AITriggerType class. + * + * @license Vinifera 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. + * + * Vinifera 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 . + * + ******************************************************************************/ +#pragma once + + +void AITriggerTypeClassExtension_Hooks(); diff --git a/src/extensions/building/buildingext_hooks.cpp b/src/extensions/building/buildingext_hooks.cpp index d813bfd91..67059e757 100644 --- a/src/extensions/building/buildingext_hooks.cpp +++ b/src/extensions/building/buildingext_hooks.cpp @@ -37,6 +37,7 @@ #include "buildingtypeext.h" #include "unit.h"; #include "unitext.h" +#include "factory.h" #include "technotype.h" #include "technotypeext.h" #include "aircraft.h" @@ -67,6 +68,9 @@ #include "hooker.h" #include "hooker_macros.h" +#include "rulesext.h" +#include "spawner.h" +#include "vinifera_globals.h" /** @@ -84,6 +88,7 @@ static class BuildingClassExt final : public BuildingClass const InfantryTypeClass* _Crew_Type() const; int _How_Many_Survivors() const; int _Shape_Number() const; + void _Draw_Overlays(Point2D& coord, Rect& rect); }; @@ -275,6 +280,116 @@ int BuildingClassExt::_Shape_Number() const } +/** + * Reimplements the BuildingClass::Draw_Overlays function. + * + * @author: ZivDero + */ +void BuildingClassExt::_Draw_Overlays(Point2D& coord, Rect& rect) +{ + if (BState != BSTATE_CONSTRUCTION) + { + /** + * Draw the repair animation. + */ + if (IsRepairing) + { + if (!Map.Is_Shrouded(Center_Coord()) && !(Scen->SpecialFlags.IsFogOfWar || IsFogged) && Visual_Character() != VISUAL_HIDDEN) + { + Point2D xy = coord; + if (!IsPowerOn) + xy -= Point2D(5, 5); + + int delay = Options.Normalize_Delay(14) / 4; + if (delay < 2) + delay = 2; + + CC_Draw_Shape(LogicSurface, MouseDrawer, WrenchShape, 6 * (Frame % delay) / (delay - 1), &xy, &rect, SHAPE_ALPHA | SHAPE_WIN_REL | SHAPE_CENTER); + } + } + + /** + * Draw the power off animation. + */ + if (!IsPowerOn && House->Is_Player_Control()) + { + if (!Map.Is_Shrouded(Center_Coord()) && !(Scen->SpecialFlags.IsFogOfWar || IsFogged)) + { + Point2D xy = coord; + if (IsRepairing) + xy += Point2D(10, 10); + + int delay = Options.Normalize_Delay(14) / 4; + if (delay < 2) + delay = 2; + + CC_Draw_Shape(LogicSurface, MouseDrawer, PowerOffShape, 6 * (Frame % delay) / (delay - 1), &xy, &rect, SHAPE_ALPHA | SHAPE_WIN_REL | SHAPE_CENTER); + } + } + + if (IsSelected) + { + /** + * Draw the primary factory pip. + */ + if (House->Is_Ally(PlayerPtr) || SpiedBy & (1 << (PlayerPtr->Class->House)) || (PlayerPtr == Vinifera_ObserverPtr)) + { + Point2D xy(coord.X - 10, coord.Y + 10); + Draw_Text_Overlay(xy, coord, rect); + } + + /** + * If this is a factory, and the player has spied its owner, draw the cameo of what it's currently producing. + */ + if (SpiedBy & (1 << (PlayerPtr->Class->House)) || (PlayerPtr == Vinifera_ObserverPtr)) + { + FactoryClass* factory = House->Is_Human_Control() ? House->Fetch_Factory(Class->ToBuild) : Factory; + if (factory != nullptr) + { + ObjectClass* obj = factory->Get_Object(); + if (obj != nullptr) + { + /** + * #issue-487 + * + * Adds support for PCX/PNG cameo icons. + * + * @author: CCHyper + */ + const auto technotypeext = Extension::Fetch(obj->Techno_Type_Class()); + if (technotypeext->CameoImageSurface) + { + /** + * Draw the cameo pcx image. + */ + Rect pcxrect; + pcxrect.X = rect.X + coord.X; + pcxrect.Y = rect.Y + coord.Y; + pcxrect.Width = technotypeext->CameoImageSurface->Get_Width(); + pcxrect.Height = technotypeext->CameoImageSurface->Get_Height(); + + SpriteCollection.Draw(pcxrect, *LogicSurface, *technotypeext->CameoImageSurface); + } + else + { + const ShapeFileStruct* shape = obj->Techno_Type_Class()->Get_Cameo_Data(); + + /** + * Draw the cameo shape. + * + * Original code used NormalDrawer, which is the old Red Alert shape + * drawer, so we need to use CameoDrawer here for the correct palette. + */ + CC_Draw_Shape(LogicSurface, CameoDrawer, shape, 0, &coord, &rect, SHAPE_ALPHA | SHAPE_WIN_REL | SHAPE_CENTER); + } + } + } + } + } + } +} + + /** * #issue-204 * @@ -771,62 +886,6 @@ DECLARE_PATCH(_BuildingClass_Explode_ShakeScreen_Division_BugFix_Patch) } -/** - * #issue-72 - * - * Fixes the bug where the wrong palette used to draw the cameo of the object - * being produced above a enemy spied factory building. - * - * @author: CCHyper - */ -DECLARE_PATCH(_BuildingClass_Draw_Spied_Cameo_Palette_Patch) -{ - GET_REGISTER_STATIC(TechnoClass *, factory_obj, eax); - GET_REGISTER_STATIC(Point2D *, pos_xy, edi); - GET_REGISTER_STATIC(Rect *, window_rect, ebp); - static TechnoTypeClass *technotype; - static TechnoTypeClassExtension *technotypeext; - static const ShapeFileStruct *cameo_shape; - static Surface *pcx_image; - static Rect pcxrect; - - technotype = factory_obj->Techno_Type_Class(); - - /** - * #issue-487 - * - * Adds support for PCX/PNG cameo icons. - * - * @author: CCHyper - */ - technotypeext = Extension::Fetch(technotype); - if (technotypeext->CameoImageSurface) { - - /** - * Draw the cameo pcx image. - */ - pcxrect.X = window_rect->X + pos_xy->X; - pcxrect.Y = window_rect->Y + pos_xy->Y; - pcxrect.Width = technotypeext->CameoImageSurface->Get_Width(); - pcxrect.Height = technotypeext->CameoImageSurface->Get_Height(); - - SpriteCollection.Draw(pcxrect, *LogicSurface, *technotypeext->CameoImageSurface); - - } else { - - cameo_shape = technotype->Get_Cameo_Data(); - - /** - * Draw the cameo shape. - * - * Original code used NormalDrawer, which is the old Red Alert shape - * drawer, so we need to use CameoDrawer here for the correct palette. - */ - CC_Draw_Shape(LogicSurface, CameoDrawer, cameo_shape, 0, pos_xy, window_rect, ShapeFlagsType(SHAPE_CENTER|SHAPE_WIN_REL|SHAPE_ALPHA|SHAPE_NORMAL)); - } - - JMP(0x00428B13); -} /** @@ -985,25 +1044,6 @@ DECLARE_PATCH(_BuildingClass_Mission_Deconstruction_Double_Survivors_Patch) } -/** - * Patch to not assign archive targets to buildings currently being sold. - * - * @author: ZivDero - */ -DECLARE_PATCH(_EventClass_Execute_Archive_Selling_Patch) -{ - GET_REGISTER_STATIC(TechnoClass*, techno, edi); - GET_REGISTER_STATIC(TARGET, target, eax); - - // Don't assign an archive target if currently selling - if (techno->Mission != MISSION_DECONSTRUCTION) { - techno->Assign_Archive_Target(target); - } - - JMP(0x00494372); -} - - /** * Patch in BuildingClass::Captured to not count captured DontScore buildings. * @@ -1015,7 +1055,7 @@ DECLARE_PATCH(_BuildingClass_Captured_DontScore_Patch) static BuildingTypeClassExtension* ext; ext = Extension::Fetch(this_ptr->Class); - if ((Session.Type == GAME_INTERNET || Session.Type == GAME_IPX) && !ext->IsDontScore) + if (!ext->IsDontScore) { JMP(0x0042F7A3); } @@ -1050,6 +1090,86 @@ DECLARE_PATCH(_BuildingClass_Grand_Opening_Assign_FreeUnit_LastDockedBuilding_Pa } +/** + * #issue-177 + * + * Patches the check for if a building is a Construction Yard to check the entire BuildConst list. + * + * @author: ZivDero + */ +DECLARE_PATCH(_BuildingClass_Unlimbo_BuildConst_Patch) +{ + GET_REGISTER_STATIC(BuildingClass*, this_ptr, esi); + + if (Rule->BuildConst.Is_Present(this_ptr->Class)) + { + JMP(0x0042AA8B); + } + + JMP(0x0042AACF); +} + + +/** + * #issue-177 + * + * Patches the check for if a building is a Construction Yard to check the entire BuildConst list. + * + * @author: ZivDero + */ +DECLARE_PATCH(_BuildingClass_Captured_BuildConst_Patch1) +{ + GET_REGISTER_STATIC(BuildingTypeClass*, buildingtype, ecx); + + if (Rule->BuildConst.Is_Present(buildingtype)) + { + JMP(0x0042F968); + } + + JMP(0x0042F9A2); +} + + +/** + * #issue-177 + * + * Patches the check for if you have a Construction Yard to check the entire BuildConst list. + * + * @author: ZivDero + */ +DECLARE_PATCH(_BuildingClass_Captured_BuildConst_Patch2) +{ + GET_REGISTER_STATIC(HouseClass*, house, ebx); + + if (house->Count_Owned(Rule->BuildConst)) + { + JMP(0x0042FAEF); + } + + JMP(0x0042FB10); +} + + +/** + * #issue-177 + * + * Patches the check for if a building is a Construction Yard to check the entire BuildConst list. + * + * @author: ZivDero + */ +DECLARE_PATCH(_BuildingClass_Captured_BuildConst_Patch3) +{ + GET_REGISTER_STATIC(BuildingClass*, this_ptr, esi); + + if (Rule->BuildConst.Is_Present(this_ptr->Class)) + { + JMP(0x0042FCB6); + } + + JMP(0x0042FCF8); +} + + /** * Main function for patching the hooks. */ @@ -1060,7 +1180,6 @@ void BuildingClassExtension_Hooks() */ BuildingClassExtension_Init(); - Patch_Jump(0x00428AD3, &_BuildingClass_Draw_Spied_Cameo_Palette_Patch); Patch_Jump(0x0042B250, &_BuildingClass_Explode_ShakeScreen_Division_BugFix_Patch); Patch_Jump(0x00433BB5, &_BuildingClass_Mission_Open_Gate_Open_Sound_Patch); Patch_Jump(0x00433C6F, &_BuildingClass_Mission_Open_Gate_Close_Sound_Patch); @@ -1080,9 +1199,15 @@ void BuildingClassExtension_Hooks() Patch_Jump(0x00430CC2, &_BuildingClass_Mission_Deconstruction_ConYard_Survivors_Patch); Patch_Jump(0x00430A01, &_BuildingClass_Mission_Deconstruction_ConYard_Unlimbo_Patch); Patch_Jump(0x00430F2B, &_BuildingClass_Mission_Deconstruction_Double_Survivors_Patch); - Patch_Jump(0x0049436A, &_EventClass_Execute_Archive_Selling_Patch); Patch_Jump(0x0042F799, &_BuildingClass_Captured_DontScore_Patch); Patch_Jump(0x0042E5F5, &_BuildingClass_Grand_Opening_Assign_FreeUnit_LastDockedBuilding_Patch); //Patch_Jump(0x00429220, &BuildingClassExt::_Shape_Number); // It's identical to vanilla, leaving it in in case it's ever needed Patch_Jump(0x0042E53C, 0x0042E56F); // Jump a check for the PurchasePrice of a building for spawning its FreeUnit in Grand_Opening + Patch_Jump(0x00428810, &BuildingClassExt::_Draw_Overlays); + Patch_Jump(0x0042AA76, &_BuildingClass_Unlimbo_BuildConst_Patch); + Patch_Jump(0x0042F958, &_BuildingClass_Captured_BuildConst_Patch1); + Patch_Jump(0x0042FACC, &_BuildingClass_Captured_BuildConst_Patch2); + Patch_Jump(0x0042FCA1, &_BuildingClass_Captured_BuildConst_Patch3); + + Patch_Jump(0x0042ED3C, 0x0042ED46); // Allow manually aiming buildings whose weapons are not anti-ground. } diff --git a/src/extensions/campaign/campaignext.cpp b/src/extensions/campaign/campaignext.cpp index 8071f358f..bb28cfc6e 100644 --- a/src/extensions/campaign/campaignext.cpp +++ b/src/extensions/campaign/campaignext.cpp @@ -41,7 +41,8 @@ CampaignClassExtension::CampaignClassExtension(const CampaignClass *this_ptr) : AbstractTypeClassExtension(this_ptr), IsDebugOnly(false), - IntroMovie() + IntroMovie(), + House(HOUSE_GDI) { //if (this_ptr) EXT_DEBUG_TRACE("CampaignClassExtension::CampaignClassExtension - Name: %s (0x%08X)\n", Name(), (uintptr_t)(This())); @@ -173,14 +174,31 @@ void CampaignClassExtension::Compute_CRC(WWCRCEngine &crc) const */ bool CampaignClassExtension::Read_INI(CCINIClass &ini) { + const char* ini_name = Name(); + + if (!IsInitialized) { + + /** + * Select vanilla campaigns's house based on their name + */ + HousesType side = HOUSE_GDI; + + if (std::strstr(This()->Scenario, "GDI")) { + side = HOUSE_GDI; + } + else if (std::strstr(This()->Scenario, "NOD")) { + side = HOUSE_NOD; + } + + House = side; + } + //EXT_DEBUG_TRACE("CampaignClassExtension::Read_INI - Name: %s (0x%08X)\n", Name(), (uintptr_t)(This())); if (!AbstractTypeClassExtension::Read_INI(ini)) { return false; } - const char *ini_name = Name(); - IsDebugOnly = ini.Get_Bool(ini_name, "DebugOnly", IsDebugOnly); /** @@ -194,5 +212,9 @@ bool CampaignClassExtension::Read_INI(CCINIClass &ini) ini.Get_String(ini_name, "IntroMovie", IntroMovie, sizeof(IntroMovie)); + House = static_cast(ini.Get_Int(ini_name, "Side", House)); + + IsInitialized = true; + return true; } diff --git a/src/extensions/campaign/campaignext.h b/src/extensions/campaign/campaignext.h index bbc0195b2..7e9170abf 100644 --- a/src/extensions/campaign/campaignext.h +++ b/src/extensions/campaign/campaignext.h @@ -71,4 +71,9 @@ CampaignClassExtension final : public AbstractTypeClassExtension * The movie to play at start of this campaign. */ char IntroMovie[64]; + + /** + * The HOUSE (not side!) this campaign is played as. + */ + HousesType House; }; diff --git a/src/extensions/cell/cellext_hooks.cpp b/src/extensions/cell/cellext_hooks.cpp index 7f6bc6c21..4f5d4dc76 100644 --- a/src/extensions/cell/cellext_hooks.cpp +++ b/src/extensions/cell/cellext_hooks.cpp @@ -31,16 +31,161 @@ #include "session.h" #include "rules.h" #include "iomap.h" +#include "rulesext.h" +#include "foot.h" +#include "unit.h" +#include "unittype.h" #include "techno.h" #include "technotype.h" +#include "house.h" +#include "session.h" #include "fatal.h" #include "debughandler.h" #include "asserthandler.h" +#include "buildingtype.h" +#include "vinifera_globals.h" #include "hooker.h" #include "hooker_macros.h" +/** + * #issue-177 + * + * Patches the check for if you own base units before giving you a crate MCV to use the new BaseUnit vector. + * + * @author: CCHyper + */ +DECLARE_PATCH(_CellClass_Goodie_Check_BaseUnit_Quantity_Patch) +{ + GET_REGISTER_STATIC(FootClass *, object, ebx); + static UnitTypeClass *unittype; + static HouseClass *objhouse; + static UnitType unit; + static int count; + + objhouse = object->House; + + /** + * Fetch the first buildable base unit from the new base unit entry + * and get the current count of that unit that this house owns. + */ + unittype = objhouse->Get_First_Ownable(RuleExtension->BaseUnit); + if (unittype) { + unit = static_cast(unittype->Get_Heap_ID()); + count = objhouse->UQuantity.Count_Of(unit); + } + + /** + * If no ownable base units were found, continue the force mcv check. + */ + if (!count) { + goto continue_check; + } + + /** + * Skip the check. + */ +skip_check: + JMP_REG(eax, 0x00457DCF); + + /** + * Continue check for setting "force mcv". + */ +continue_check: + JMP_REG(edi, 0x00457DB8); +} + + +/** + * #issue-177 + * + * Patches crates to give you a base unit from the new BaseUnit vector. + * + * @author: CCHyper, ZivDero + */ +DECLARE_PATCH(_CellClass_Goodie_Check_CRATE_UNIT_BaseUnit_Patch) +{ + GET_REGISTER_STATIC(FootClass *, object, ebx); + static UnitTypeClass *unittype; + static HouseClass *objhouse; + static UnitType unit; + + objhouse = object->House; + + /** + * Fetch the first buildable base unit from the new base unit entry. + */ + unittype = objhouse->Get_First_Ownable(RuleExtension->BaseUnit); + + if (unittype) + { + _asm mov eax, Rule + _asm mov eax, [eax] + _asm mov edi, unittype + JMP_REG(edx, 0x004581AA); + } + + _asm mov eax, Rule + _asm mov eax, [eax] + _asm mov edi, unittype + JMP_REG(edx, 0x00458148); +} + + +/** + * #issue-177 + * + * Patches crates to check if you have refineries and harvesters using the entire lists. + * + * @author: ZivDero + */ +DECLARE_PATCH(_CellClass_Goodie_Check_CRATE_UNIT_BuildRefinery_HarvesterUnit_Patch) +{ + GET_REGISTER_STATIC(FootClass*, object, ebx); + GET_REGISTER_STATIC(UnitTypeClass*, unittype, edi); + HouseClass* owner_house; + + owner_house = object->House; + + if (owner_house->Count_Owned(Rule->BuildRefinery) > 0 && owner_house->Count_Owned(Rule->HarvesterUnit) == 0) + { + // We can grant a harvester + unittype = owner_house->Get_First_Ownable(Rule->HarvesterUnit); + } + + _asm mov eax, Rule + _asm mov eax, [eax] + _asm mov edi, unittype + JMP_REG(edx, 0x004581AA); +} + + +/** + * #issue-177 + * + * Patches crates to check if a unit is a BaseUnit using the new list. + * + * @author: ZivDero + */ +DECLARE_PATCH(_CellClass_Goodie_Check_No_Buildings_Force_MCV_BaseUnit_Patch) +{ + GET_REGISTER_STATIC(UnitTypeClass *, unittype, edi); + static int i; + + /** + * Check if this is a BaseUnit. + * If so, continue the loop. + */ + if (RuleExtension->BaseUnit.Is_Present(unittype)) + { + JMP(0x004581BA); + } + + JMP(0x0045821B); +} + + /** * #issue-381 * @@ -50,41 +195,41 @@ */ DECLARE_PATCH(_CellClass_Draw_Shroud_Fog_Patch) { - static bool _shroud_one_time = false; - static const ShapeFileStruct *_shroud_shape; - static const ShapeFileStruct *_fog_shape; - - /** - * Stolen bytes/code. - */ - _asm { sub esp, 0x34 } - - /** - * Perform a one-time load of the shroud and fog shape data. - */ - if (!_shroud_one_time) { - _shroud_shape = (const ShapeFileStruct *)MFCC::Retrieve("SHROUD.SHP"); - _fog_shape = (const ShapeFileStruct *)MFCC::Retrieve("FOG.SHP"); - _shroud_one_time = true; - } - - /** - * If we are playing a multiplayer game, use the hardcoded shape data. - */ - if (!Session.Singleplayer_Game()) { - Cell_ShroudShape = (const ShapeFileStruct *)&ShroudShapeBinary; - Cell_FogShape = (const ShapeFileStruct *)&FogShapeBinary; - - } else { - Cell_ShroudShape = _shroud_shape; - Cell_FogShape = _fog_shape; - } - - /** - * Continues function flow. - */ + static bool _shroud_one_time = false; + static const ShapeFileStruct *_shroud_shape; + static const ShapeFileStruct *_fog_shape; + + /** + * Stolen bytes/code. + */ + _asm { sub esp, 0x34 } + + /** + * Perform a one-time load of the shroud and fog shape data. + */ + if (!_shroud_one_time) { + _shroud_shape = (const ShapeFileStruct *)MFCC::Retrieve("SHROUD.SHP"); + _fog_shape = (const ShapeFileStruct *)MFCC::Retrieve("FOG.SHP"); + _shroud_one_time = true; + } + + /** + * If we are playing a multiplayer game, use the hardcoded shape data. + */ + if (!Session.Singleplayer_Game()) { + Cell_ShroudShape = (const ShapeFileStruct *)&ShroudShapeBinary; + Cell_FogShape = (const ShapeFileStruct *)&FogShapeBinary; + + } else { + Cell_ShroudShape = _shroud_shape; + Cell_FogShape = _fog_shape; + } + + /** + * Continues function flow. + */ continue_function: - JMP(0x00454E91); + JMP(0x00454E91); } @@ -97,39 +242,39 @@ DECLARE_PATCH(_CellClass_Draw_Shroud_Fog_Patch) */ DECLARE_PATCH(_CellClass_Draw_Fog_Patch) { - static bool _fog_one_time = false; - static const ShapeFileStruct *_fog_shape; - - /** - * Stolen bytes/code. - */ - _asm { sub esp, 0x2C } - - /** - * Perform a one-time load of the fog shape data. - */ - if (!_fog_one_time) { - _fog_shape = (const ShapeFileStruct *)MFCC::Retrieve("FOG.SHP"); - _fog_one_time = true; - } - - /** - * If we are playing a multiplayer game, use the hardcoded shape data. - */ - if (!Session.Singleplayer_Game()) { - Cell_FixupFogShape = (const ShapeFileStruct *)&FogShapeBinary; - - } else { - Cell_FixupFogShape = _fog_shape; - } - - /** - * Continues function flow. - */ + static bool _fog_one_time = false; + static const ShapeFileStruct *_fog_shape; + + /** + * Stolen bytes/code. + */ + _asm { sub esp, 0x2C } + + /** + * Perform a one-time load of the fog shape data. + */ + if (!_fog_one_time) { + _fog_shape = (const ShapeFileStruct *)MFCC::Retrieve("FOG.SHP"); + _fog_one_time = true; + } + + /** + * If we are playing a multiplayer game, use the hardcoded shape data. + */ + if (!Session.Singleplayer_Game()) { + Cell_FixupFogShape = (const ShapeFileStruct *)&FogShapeBinary; + + } else { + Cell_FixupFogShape = _fog_shape; + } + + /** + * Continues function flow. + */ continue_function: - _asm { mov eax, Cell_FixupFogShape } - _asm { mov eax, [eax] } - JMP_REG(ecx, 0x00455159); + _asm { mov eax, Cell_FixupFogShape } + _asm { mov eax, [eax] } + JMP_REG(ecx, 0x00455159); } @@ -144,26 +289,26 @@ DECLARE_PATCH(_CellClass_Draw_Fog_Patch) */ DECLARE_PATCH(_CellClass_Goodie_Check_Crates_Disabled_Respawn_BugFix_Patch) { - /** - * Random crates are only thing in multiplayer. - */ - if (Session.Type != GAME_NORMAL) { - - /** - * Check to make sure crates are enabled for this game session. - * - * The original code was missing the Session "Goodies" check. - */ - if (Rule->IsMPCrates && Session.Options.Goodies) { - Map.Place_Random_Crate(); - } - } - - /** - * Continues function flow. - */ + /** + * Random crates are only thing in multiplayer. + */ + if (Session.Type != GAME_NORMAL) { + + /** + * Check to make sure crates are enabled for this game session. + * + * The original code was missing the Session "Goodies" check. + */ + if (Rule->IsMPCrates && Session.Options.Goodies) { + Map.Place_Random_Crate(); + } + } + + /** + * Continues function flow. + */ continue_function: - JMP_REG(ecx, 0x00457ECE); + JMP_REG(ecx, 0x00457ECE); } @@ -177,36 +322,36 @@ DECLARE_PATCH(_CellClass_Goodie_Check_Crates_Disabled_Respawn_BugFix_Patch) */ DECLARE_PATCH(_CellClass_Goodie_Check_Veterency_Trainable_BugFix_Patch) { - GET_REGISTER_STATIC(ObjectClass *, object, ecx); - static TechnoClass *techno; - static TechnoTypeClass *technotype; - - /** - * Make sure the ground layer object is a techno. - */ - if (!object->Is_Techno()) { - goto continue_loop; - } - - /** - * Is this object trainable? If so, grant it the bonus. - */ - techno = reinterpret_cast(object); - if (techno->Techno_Type_Class()->IsTrainable) { - goto passes_check; - } - - /** - * Continues the loop over the ground layer objects. - */ + GET_REGISTER_STATIC(ObjectClass *, object, ecx); + static TechnoClass *techno; + static TechnoTypeClass *technotype; + + /** + * Make sure the ground layer object is a techno. + */ + if (!object->Is_Techno()) { + goto continue_loop; + } + + /** + * Is this object trainable? If so, grant it the bonus. + */ + techno = reinterpret_cast(object); + if (techno->Techno_Type_Class()->IsTrainable) { + goto passes_check; + } + + /** + * Continues the loop over the ground layer objects. + */ continue_loop: - JMP(0x0045894E); + JMP(0x0045894E); - /** - * Continue to grant the veterancy bonus. - */ + /** + * Continue to grant the veterancy bonus. + */ passes_check: - JMP(0x00458839); + JMP(0x00458839); } @@ -215,8 +360,17 @@ DECLARE_PATCH(_CellClass_Goodie_Check_Veterency_Trainable_BugFix_Patch) */ void CellClassExtension_Hooks() { - Patch_Jump(0x0045882C, &_CellClass_Goodie_Check_Veterency_Trainable_BugFix_Patch); - Patch_Jump(0x00457EAB, &_CellClass_Goodie_Check_Crates_Disabled_Respawn_BugFix_Patch); - Patch_Jump(0x00454E60, &_CellClass_Draw_Shroud_Fog_Patch); - Patch_Jump(0x00455130, &_CellClass_Draw_Fog_Patch); + Patch_Jump(0x0045882C, &_CellClass_Goodie_Check_Veterency_Trainable_BugFix_Patch); + Patch_Jump(0x00457EAB, &_CellClass_Goodie_Check_Crates_Disabled_Respawn_BugFix_Patch); + Patch_Jump(0x00454E60, &_CellClass_Draw_Shroud_Fog_Patch); + Patch_Jump(0x00455130, &_CellClass_Draw_Fog_Patch); + Patch_Jump(0x00457D90, &_CellClass_Goodie_Check_BaseUnit_Quantity_Patch); + Patch_Jump(0x0045813E, &_CellClass_Goodie_Check_CRATE_UNIT_BaseUnit_Patch); + Patch_Jump(0x0045820E, &_CellClass_Goodie_Check_No_Buildings_Force_MCV_BaseUnit_Patch); + Patch_Jump(0x00458148, &_CellClass_Goodie_Check_CRATE_UNIT_BuildRefinery_HarvesterUnit_Patch); + + /** + * Patch away a check for GAME_INTERNET to enable statistics collection. + */ + Patch_Jump(0x00457E7A, 0x00457E83); // CellClass::Goodie_Check } diff --git a/src/extensions/command/commandext_hooks.cpp b/src/extensions/command/commandext_hooks.cpp index 4f9cd51de..e8885bd08 100644 --- a/src/extensions/command/commandext_hooks.cpp +++ b/src/extensions/command/commandext_hooks.cpp @@ -30,16 +30,110 @@ #include "tibsun_functions.h" #include "tibsun_globals.h" #include "session.h" +#include "rules.h" +#include "rulesext.h" #include "ccfile.h" #include "ccini.h" #include "object.h" #include "unit.h" #include "unittype.h" +#include "building.h" +#include "buildingtype.h" #include "asserthandler.h" #include "debughandler.h" +#include "display.h" +#include "mouse.h" #include "hooker.h" #include "hooker_macros.h" +#include "house.h" +#include "tactical.h" + + +/** + * A fake class for implementing new member functions which allow + * access to the "this" pointer of the intended class. + * + * @note: This must not contain a constructor or deconstructor! + * @note: All functions must be prefixed with "_" to prevent accidental virtualization. + */ +class CenterBaseCommandClassExt final : public CommandClass +{ +public: + bool _Process(); + +}; + + +/** + * #issue-177 + * + * Replaces CenterBaseCommandClass::Process to use the entire BuildConst list, + * as well as the new BaseUnit list. + * + * @author: ZivDero + */ +bool CenterBaseCommandClassExt::_Process() +{ + Coordinate conyard_coord = Coordinate(), anybuilding_coord = Coordinate(); + + if (PlayerPtr->CurBuildings) + { + for (int i = 0; i < Buildings.Count(); i++) + { + BuildingClass* building = Buildings[i]; + if (building && !building->IsInLimbo && building->House->Is_Player_Control()) + { + if (Rule->BuildConst.Is_Present(building->Class)) + { + conyard_coord = building->Center_Coord(); + if (building->IsLeader) + break; + } + else if (!anybuilding_coord) + { + anybuilding_coord = building->Center_Coord(); + } + } + } + } + + if (conyard_coord) + { + TacticalMap->Set_Tactical_Position(conyard_coord); + } + else + { + if (PlayerPtr->CurUnits) + { + for (int i = 0; i < Units.Count(); i++) + { + UnitClass* unit = Units[i]; + if (unit && !unit->IsInLimbo && unit->House->Is_Player_Control()) + { + conyard_coord = unit->Center_Coord(); + if (RuleExtension->BaseUnit.Is_Present(unit->Class)) + break; + } + + } + } + + if (conyard_coord) + TacticalMap->Set_Tactical_Position(conyard_coord); + else if (anybuilding_coord) + TacticalMap->Set_Tactical_Position(anybuilding_coord); + } + + if (Map.PendingObject) + Map.Set_Cursor_Pos(); + + Map.Follow_This(nullptr); + Map.Flag_To_Redraw(1); + + return true; +} + /** @@ -604,6 +698,8 @@ void CommandExtension_Hooks() Patch_Jump(0x004E95C2, &_GuardCommandClass_Process_Harvesters_Set_Mission_Patch); + Patch_Jump(0x004E97E0, &CenterBaseCommandClassExt::_Process); + /** * This can not be in client compatabile builds currently as the additional * commands added do not have runtime type information. diff --git a/src/extensions/display/displayext_hooks.cpp b/src/extensions/display/displayext_hooks.cpp index 8fcc7de80..17fab06d1 100644 --- a/src/extensions/display/displayext_hooks.cpp +++ b/src/extensions/display/displayext_hooks.cpp @@ -45,6 +45,7 @@ #include "mousetype.h" #include "actiontype.h" #include "extension.h" +#include "tactical.h" #include "fatal.h" #include "debughandler.h" #include "asserthandler.h" @@ -53,6 +54,71 @@ #include "hooker_macros.h" +/** + * A fake class for implementing new member functions which allow + * access to the "this" pointer of the intended class. + * + * @note: This must not contain a constructor or deconstructor! + * @note: All functions must be prefixed with "_" to prevent accidental virtualization. + */ +class DisplayClassExt final : public DisplayClass +{ +public: + void _Compute_Start_Pos(); +}; + + +/** + * Computes player's start pos from unit coords. + * + * @author: 02/28/1995 JLB - Red Alert Source COde + * 29/10/2024 ZivDero - Adjustments for Tiberian Sun + */ +void DisplayClassExt::_Compute_Start_Pos() +{ + /** + * Find the summation coordinate for all the player's units, infantry, + * and buildings. + */ + long num = 0; + + Coordinate coord = Coordinate(); + + for (int i = 0; i < Technos.Count(); i++) { + TechnoClass* technop = Technos[i]; + if (!technop->IsInLimbo && technop->IsOwnedByPlayer) { + coord += technop->Get_Coord(); + num++; + } + } + + /** + * Divide the coordinate by 'num' to compute the average value. + */ + coord.Z = 0; + if (num > 0) { + coord /= num; + } + /** + * If the player has no units (i. e. is an observer), use their house's center cell. + */ + else { + coord = PlayerPtr->Center; + } + + Scen->Views[3] = Coord_Cell(coord); + Scen->Views[2] = Scen->Views[3]; + Scen->Views[1] = Scen->Views[2]; + Scen->Views[0] = Scen->Views[1]; + Scen->AltHomeCell = Scen->HomeCell; + + if (TacticalMap) { + TacticalMap->Set_Tactical_Position(coord); + } +} + + + /** * Sets the mouse cursor based on the action. * @@ -370,4 +436,6 @@ void DisplayClassExtension_Hooks() * @author: ZivDero */ Patch_Jump(0x0047A856, &_DisplayClass_47A790_Patch); + + Patch_Jump(0x004793A0, &DisplayClassExt::_Compute_Start_Pos); } diff --git a/src/extensions/dropship/dropshipext_hooks.cpp b/src/extensions/dropship/dropshipext_hooks.cpp index ee07388d2..5f64d686b 100644 --- a/src/extensions/dropship/dropshipext_hooks.cpp +++ b/src/extensions/dropship/dropshipext_hooks.cpp @@ -65,53 +65,6 @@ DECLARE_PATCH(_Dropship_Draw_Info_Text_ArmorName_Patch) } -/** - * #issue-262 - * - * In certain cases, the mouse might not be shown on the Dropship Loadout menu. - * This patch fixes that by showing the mouse regardless of its current state. - * - * @author: CCHyper - */ -DECLARE_PATCH(_Start_Scenario_Dropship_Loadout_Show_Mouse_Patch) -{ - /** - * issue-284 - * - * Play a background theme during the loadout menu. - * - * @author: CCHyper - */ - if (!Theme.Still_Playing()) { - - /** - * If DSHPLOAD is defined in THEME.INI, play that, otherwise default - * to playing the TS Maps theme. - */ - ThemeType theme = Theme.From_Name("DSHPLOAD"); - if (theme == THEME_NONE) { - theme = Theme.From_Name("MAPS"); - } - - Theme.Play_Song(theme); - } - - WWMouse->Release_Mouse(); - WWMouse->Show_Mouse(); - - Dropship_Loadout(); - - WWMouse->Hide_Mouse(); - WWMouse->Capture_Mouse(); - - if (Theme.Still_Playing()) { - Theme.Stop(true); // Smoothly fade out the track. - } - - JMP(0x005DB3C0); -} - - /** * #issue-285 * @@ -168,6 +121,5 @@ DECLARE_PATCH(_Dropship_Loadout_Help_Text_Patch) void DropshipExtension_Hooks() { Patch_Jump(0x004868FB, &_Dropship_Loadout_Help_Text_Patch); - Patch_Jump(0x005DB3BB, &_Start_Scenario_Dropship_Loadout_Show_Mouse_Patch); Patch_Jump(0x0048706A, &_Dropship_Draw_Info_Text_ArmorName_Patch); } diff --git a/src/extensions/event/eventext_hooks.cpp b/src/extensions/event/eventext_hooks.cpp new file mode 100644 index 000000000..dda7a575c --- /dev/null +++ b/src/extensions/event/eventext_hooks.cpp @@ -0,0 +1,1065 @@ +/******************************************************************************* +/* O P E N S O U R C E -- V I N I F E R A ** +/******************************************************************************* + * + * @project Vinifera + * + * @file EVENTEXT_HOOKS.CPP + * + * @author ZivDero + * + * @brief Contains the hooks for the extended EventClass. + * + * @license Vinifera 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. + * + * Vinifera 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 . + * + ******************************************************************************/ + +#include "eventext_hooks.h" + +#include "hooker.h" +#include "hooker_macros.h" +#include "viniferaevent.h" +#include "techno.h" +#include "building.h" +#include "foot.h" +#include "team.h" +#include "mouse.h" +#include "cell.h" +#include "fetchres.h" +#include "language.h" +#include "rules.h" +#include "unit.h" +#include "session.h" +#include "anim.h" +#include "extension.h" +#include "protocolzero.h" +#include "spawner.h" +#include "technoext.h" +#include "spawnmanager.h" +#include "tibsun_functions.h" +#include "vinifera_globals.h" + + +/** + * A fake class for implementing new member functions which allow + * access to the "this" pointer of the intended class. + * + * @note: This must not contain a constructor or destructor! + * @note: All functions must be prefixed with "_" to prevent accidental virtualization. + */ +class EventClassExt final : public EventClass +{ +public: + void _Execute(); + + void _Event_PowerOn(); + void _Event_PowerOff(); + void _Event_Ally(); + void _Event_MegaMission(); + void _Event_Idle(); + void _Event_Scatter(); + void _Event_Destruct(); + void _Event_Deploy(); + void _Event_Place(); + void _Event_Options(); + void _Event_GameSpeed(); + void _Event_Produce(); + void _Event_Suspend(); + void _Event_Abandon(); + void _Event_Primary(); + void _Event_Special_Place(); + void _Event_Exit(); + void _Event_Animation(); + void _Event_Repair(); + void _Event_Sell(); + void _Event_SellCell(); + void _Event_Special(); + void _Event_Response_Time(); + void _Event_SaveGame(); + void _Event_Archive(); + void _Event_AddPlayer(); + void _Event_Timing(); + void _Event_Process_Time(); + void _Event_PageUser(); + void _Event_RemovePlayer(); + void _Event_LatencyFudge(); + +}; + + +void EventClassExt::_Event_PowerOn() +{ + BuildingClass* building = Data.Target.Whom.As_Building(); + if (building && building->IsActive && !building->IsInLimbo && !building->Class->IsFirestormWall) { + building->Turn_On(); + } +} + + +void EventClassExt::_Event_PowerOff() +{ + BuildingClass* building = Data.Target.Whom.As_Building(); + if (building && building->IsActive && !building->IsInLimbo && !building->Class->IsFirestormWall) { + building->Turn_Off(); + } +} + + +void EventClassExt::_Event_Ally() +{ + HouseClass* house = Houses[ID]; + if (house->Is_Ally(Houses[static_cast(Data.General.Value)])) { + house->Make_Enemy(static_cast(Data.General.Value)); + } + else { + house->Make_Ally(static_cast(Data.General.Value)); + } +} + + +void EventClassExt::_Event_MegaMission() +{ + TechnoClass* techno = Data.MegaMission.Whom.As_Techno(); + + if (techno != nullptr && techno->IsActive && techno->Strength > 0 && !techno->IsInLimbo) { + + /** + * Fetch a pointer to the object of the mission. If there is an error with + * this object, such as it is dead, then bail. + */ + ObjectClass* object = nullptr; + if (Data.MegaMission.Target.Is_Valid()) { + object = Data.MegaMission.Target.As_Object(); + if (object != nullptr && (!object->IsActive || object->Strength == 0 || object->IsInLimbo)) { + return; + } + } + + /** + * If the destination target is invalid because the object is dead, then + * bail from processing this mega mission. + */ + if (Data.MegaMission.Destination.Is_Valid()) { + ObjectClass* destination = Data.MegaMission.Destination.As_Object(); + if (destination != nullptr && (!destination->IsActive || destination->Strength == 0 || destination->IsInLimbo)) { + return; + } + } + + /** + * If the destination target is a building that we're already docking with, then + * bail from processing this mega mission. + */ + if (Data.MegaMission.Mission == MISSION_ENTER && techno->Get_Mission() == MISSION_ENTER && techno->Is_Foot()) { + AbstractClass* destination = Data.MegaMission.Destination.As_Abstract(); + if (destination != nullptr && destination->What_Am_I() == RTTI_BUILDING && techno->Contact_With_Whom() == destination) { + return; + } + } + + /** + * Break any existing tether or team contact, since it is now invalid. + */ + if (!techno->IsTethered) { + techno->Transmit_Message(RADIO_OVER_OUT); + } + else { + if (techno->In_Radio_Contact() && techno->Contact_With_Whom()->IsActive) { + BuildingClass* ref = dynamic_cast(techno->Contact_With_Whom()); + + if (ref && ref->Class->IsRefinery) { + techno->Transmit_Message(RADIO_OVER_OUT); + techno->IsTethered = false; + } + } + } + + if (techno->field_20C) { + techno->field_20C = nullptr; + } + + if (techno->Is_Foot()) { + if (static_cast(techno)->Team && Data.MegaMission.Mission != MISSION_UNLOAD) { + static_cast(techno)->Team->Remove(static_cast(techno)); + } + } + + if (object != nullptr) { + + if (PlayerPtr->Is_Ally(techno)) { + object->Clicked_As_Target(); + } + } + + /** + * Test to see if the navigation target should really be queued rather + * than assigned to the object. This would be the case if this is a + * special queued move mission and there is already a valid navigation + * target for this unit. + */ + const bool q = Data.MegaMission.Mission == MISSION_QMOVE; + + techno->Assign_Mission(Data.MegaMission.Mission); + if (techno->Is_Foot()) { + static_cast(techno)->SuspendedNavCom = nullptr; + } + techno->SuspendedTarCom = nullptr; + + /** + * Guard area mode is handled with care. The specified target is actually + * assigned as the location that should be guarded. In addition, the + * movement destination is immediately set to this new location. + */ + if (Data.MegaMission.Mission == MISSION_GUARD_AREA && techno->Is_Foot()) { + techno->Assign_Target(nullptr); + techno->Assign_Destination(Data.MegaMission.Target.As_Abstract()); + techno->Assign_Archive_Target(Data.MegaMission.Target.As_Abstract()); + } + else { + if (techno->Is_Foot()) { + techno->Assign_Archive_Target(nullptr); + } + + if (q && techno->Is_Foot()) { + static_cast(techno)->Queue_Navigation_List(Data.MegaMission.Destination.As_Abstract()); + } + else { + if (techno->Is_Foot()) { + ((FootClass*)techno)->Clear_Navigation_List(); + } + techno->Assign_Target(Data.MegaMission.Target.As_Abstract()); + techno->Assign_Destination(Data.MegaMission.Destination.As_Abstract()); + } + } + } +} + + +void EventClassExt::_Event_Idle() +{ + TechnoClass* techno = Data.Target.Whom.As_Techno(); + + if (techno != nullptr && techno->IsActive && !techno->IsInLimbo && !techno->IsTethered + && techno->Get_Mission() != MISSION_CONSTRUCTION && techno->Get_Mission() != MISSION_DECONSTRUCTION) { + + if (techno->IsOnBridge || Map[techno->Get_Coord()].Ramp || !techno->Is_On_Elevation()) { + + if (techno->Is_Foot()) { + static_cast(techno)->Clear_Navigation_List(); + static_cast(techno)->field_220 = -1; + static_cast(techno)->field_33E = 0; + static_cast(techno)->field_224 = Cell(); + } + + techno->Transmit_Message(RADIO_OVER_OUT); + techno->Assign_Destination(nullptr); + techno->Assign_Target(nullptr); + + const auto extension = Extension::Fetch(techno); + if (extension->SpawnManager) + extension->SpawnManager->Abandon_Target(); + + if (techno->What_Am_I() == RTTI_UNIT && (static_cast(techno)->Class->IsToHarvest || static_cast(techno)->Class->IsToVeinHarvest) && + (techno->Get_Mission() == MISSION_HARVEST || techno->Get_Mission() == MISSION_RETURN)) { + techno->Assign_Mission(MISSION_GUARD); + techno->Commence(); + } + } + } +} + + +void EventClassExt::_Event_Scatter() +{ + TechnoClass* techno = Data.Target.Whom.As_Techno(); + + if (techno != nullptr && techno->Is_Foot() && techno->IsActive && !techno->IsInLimbo && !techno->IsTethered) { + static_cast(techno)->IsScattering = true; + techno->Scatter(Coordinate(), true, false); + } +} + + +void EventClassExt::_Event_Destruct() +{ + Houses[ID]->Flag_To_Die(); +} + + +void EventClassExt::_Event_Deploy() +{ + TechnoClass* techno = Data.Target.Whom.As_Techno(); + + if (techno != nullptr && techno->IsActive && !techno->IsInLimbo && !techno->IsTethered && techno->EMPFramesRemaining == 0) { + + if (techno->IsOnBridge || Map[techno->Get_Coord()].Ramp || !techno->Is_On_Elevation()) { + + if (techno->Get_Mission() != MISSION_CONSTRUCTION && techno->Get_Mission() != MISSION_DECONSTRUCTION && techno->What_Am_I() != RTTI_AIRCRAFT) + { + Cell cell = techno->Get_Cell(); + if (cell == Cell() || Map[cell].Cell_Building() == nullptr || !Map[cell].Cell_Building()->Class->IsWeaponsFactory) + { + techno->Transmit_Message(RADIO_OVER_OUT); + techno->Assign_Destination(nullptr); + techno->Assign_Target(nullptr); + techno->Assign_Mission(MISSION_UNLOAD); + } + } + } + } +} + + +void EventClassExt::_Event_Place() +{ + Cell cell(Data.Place.Cell); + Houses[ID]->Place_Object(Data.Place.Type, cell); +} + + +void EventClassExt::_Event_Options() +{ + if (!Session.Play) { + SpecialDialog = SDLG_OPTIONS; + } +} + + +void EventClassExt::_Event_GameSpeed() +{ + char txt[256]; + + Options.GameSpeed = Data.General.Value; + + HouseClass* house = Houses[ID]; + if (house != PlayerPtr && house != nullptr) { + const char* text = Fetch_String(TXT_PLAYER_CHANGED_SPEED); + + if (text && std::strlen(text)) { + std::snprintf(txt, std::size(txt), text, house->IniName); + Session.Messages.Add_Message(nullptr, 0, txt, house->RemapColor, TPF_USE_GRAD_PAL | TPF_FULLSHADOW | TPF_6PT_GRAD, Rule->MessageDelay * TICKS_PER_MINUTE); + } + } +} + + +void EventClassExt::_Event_Produce() +{ + Houses[ID]->Begin_Production(Data.Specific.Type, Data.Specific.ID, false); +} + + +void EventClassExt::_Event_Suspend() +{ + Houses[ID]->Suspend_Production(Data.Specific.Type); +} + + +void EventClassExt::_Event_Abandon() +{ + Houses[ID]->Abandon_Production(Data.Specific.Type, Data.Specific.ID); +} + + +void EventClassExt::_Event_Primary() +{ + BuildingClass* building = Data.Target.Whom.As_Building(); + if (building && building->IsActive) { + building->Toggle_Primary(); + } +} + + +void EventClassExt::_Event_Special_Place() +{ + Houses[ID]->Place_Special_Blast(static_cast(Data.Special.ID), Data.Special.Cell); +} + + +void EventClassExt::_Event_Exit() +{ + PlayerAborts = true; +} + + +void EventClassExt::_Event_Animation() +{ + Coordinate coord = Coordinate(Data.Anim.Where.X, Data.Anim.Where.Y, 0); + coord.Z = Map.Get_Cell_Height(coord); + if (Map[coord].Bit2_16) { + coord.Z += BridgeCellHeight; + } + + if (Data.Anim.What != ANIM_NONE) { + new AnimClass(AnimTypes[Data.Anim.What], coord); + } + else { + new AnimClass(Rule->MoveFlash, coord); + } +} + + +void EventClassExt::_Event_Repair() +{ + TechnoClass* techno = Data.Target.Whom.As_Techno(); + + if (techno && techno->IsActive) { + techno->Repair(-1); + } +} + + +void EventClassExt::_Event_Sell() +{ + TechnoClass* techno = Data.Target.Whom.As_Techno(); + + if (techno && techno->IsActive && techno->House->Get_Heap_ID() == ID) { + if (techno->What_Am_I() == RTTI_BUILDING || ((techno->What_Am_I() == RTTI_UNIT || techno->What_Am_I() == RTTI_AIRCRAFT) && Map[techno->Center_Coord()].Cell_Building() != nullptr)) { + techno->Sell_Back(-1); + } + } +} + + +void EventClassExt::_Event_SellCell() +{ + Cell cell(Data.SellCell.Cell); + Houses[ID]->Sell_Wall(cell, false); +} + + +void EventClassExt::_Event_Special() +{ + char txt[256]; + HouseClass* house = Houses[ID]; + + if (house) { + Special = Data.Options.Data; + Scen->SpecialFlags = Data.Options.Data; + std::snprintf(txt, std::size(txt), Fetch_String(TXT_SPECIAL_WARNING), house->IniName); + Session.Messages.Add_Message(nullptr, 0, txt, house->RemapColor, TPF_6PT_GRAD | TPF_USE_GRAD_PAL | TPF_FULLSHADOW, 1200); + Map.Flag_To_Redraw(false); + } +} + + +void EventClassExt::_Event_Response_Time() +{ + Session.MaxAhead = Data.FrameInfo.Delay; +} + + +void EventClassExt::_Event_SaveGame() +{ + /** + * Mark that we'd like to save the game. + */ + Vinifera_DoSave = true; +} + + +void EventClassExt::_Event_Archive() +{ + TechnoClass* techno = Data.NavCom.Whom.As_Techno(); + if (techno && techno->IsActive && techno->Get_Mission() != MISSION_DECONSTRUCTION) { + techno->Assign_Archive_Target(Data.NavCom.Where.As_Abstract()); + } +} + + +void EventClassExt::_Event_AddPlayer() +{ + if (ID != PlayerPtr->Get_Heap_ID()) { + delete[] Data.Variable.Pointer; + } +} + + +void EventClassExt::_Event_Timing() +{ + if (!Vinifera_SpawnerActive || !ProtocolZero::Enable) + Data.Timing.MaxAhead -= Scen->SpecialFlags.IsFogOfWar ? 10 : 0; + + /** + * If MaxAhead is about to increase, we're vulnerable to a Packet- + * Received-Too-Late error, if any system generates an event after + * this TIMING event, but before it executes. So, record the + * period of vulnerability's frame start & end values, so we + * can reschedule these events to execute after it's over. + */ + if (Data.Timing.MaxAhead > Session.MaxAhead || Data.Timing.FrameSendRate > Session.FrameSendRate) { + NewMaxAheadFrame1 = Frame; + NewMaxAheadFrame2 = Data.Timing.FrameSendRate * ((Data.Timing.FrameSendRate + Data.Timing.MaxAhead + Frame - 1) / Data.Timing.FrameSendRate); + } + else { + NewMaxAheadFrame1 = 0; + NewMaxAheadFrame2 = 0; + } + + Session.DesiredFrameRate = Data.Timing.DesiredFrameRate; + Session.MaxAhead = Data.Timing.MaxAhead; + Session.MaxMaxAhead = std::max(Session.MaxMaxAhead, Session.MaxAhead); + Session.FrameSendRate = Data.Timing.FrameSendRate; +} + + +void EventClassExt::_Event_Process_Time() +{ + for (int i = 0; i < Session.Players.Count(); i++) { + if (ID == Session.Players[i]->Player.ID) { + Session.Players[i]->Player.ProcessTime = Data.ProcessTime.AverageTicks; + break; + } + } +} + + +void EventClassExt::_Event_PageUser() +{ + if (!Session.Play) { + SpecialDialog = SDLG_WOL_OPTIONS; + } +} + + +void EventClassExt::_Event_RemovePlayer() +{ + DEBUG_INFO("Executing REMOVEPLAYER event. Frame is %d\n", Frame); + HouseClass* house = Houses[Data.General.Value]; + + /** + * Turn off autosaves when a player disconnects. + */ + if (Vinifera_SpawnerActive) { + Vinifera_SpawnerConfig->AutoSaveInterval = 0; + } + + if ((Session.Type == GAME_INTERNET && PlanetWestwoodTournament) || (Vinifera_SpawnerActive && Session.Type == GAME_IPX && Vinifera_SpawnerConfig->AutoSurrender)) { + house->Flag_To_Die(); + } + else if (house->Is_Human_Control()) { + house->AI_Takeover(); + } +} + + +void EventClassExt::_Event_LatencyFudge() +{ + char txt[256]; + + DEBUG_INFO("Executing LATENCYFUDGE event. Frame is %d\n", Frame); + Session.LatencyFudge = Data.General.Value; + DEBUG_INFO("LatencyFudge is %d\n", Session.LatencyFudge); + + HouseClass* house = Houses[ID]; + if (house != PlayerPtr && house != nullptr) { + const char* text = Fetch_String(TXT_PLAYER_CHANGED_LATENCY); + + if (text && std::strlen(text)) { + std::snprintf(txt, std::size(txt), text, house->IniName); + Session.Messages.Add_Message(nullptr, 0, txt, house->RemapColor, TPF_USE_GRAD_PAL | TPF_FULLSHADOW | TPF_6PT_GRAD, Rule->MessageDelay * TICKS_PER_MINUTE); + } + } +} + + +void EventClassExt::_Execute() +{ + /** + * If it's one of our events, hand it over to our class to execute. + */ + if (ViniferaEventClass::Is_Vinifera_Event(static_cast(Type))) + { + reinterpret_cast(this)->Execute(); + return; + } + + switch (Type) { + + /** + * Turn a building's power on. + */ + case EVENT_POWERON: + _Event_PowerOn(); + break; + + /** + * Turn a building's power off. + */ + case EVENT_POWEROFF: + _Event_PowerOff(); + break; + + /** + * Make or break alliance. + */ + case EVENT_ALLY: + _Event_Ally(); + break; + + /** + * This is the general purpose mission control event. Most player + * action routes through this event. It sets a unit's mission, TarCom, + * and NavCom to the values specified. + */ + case EVENT_MEGAMISSION_F: + case EVENT_MEGAMISSION: + _Event_MegaMission(); + break; + + /** + * Request that the unit/infantry/aircraft go into idle mode. + */ + case EVENT_IDLE: + _Event_Idle(); + break; + + /** + * Request that the unit/infantry/aircraft scatter from its current location. + */ + case EVENT_SCATTER: + _Event_Scatter(); + break; + + /** + * Special self destruct action requested. This is active in the multiplayer mode. + */ + case EVENT_DESTRUCT: + _Event_Destruct(); + break; + + /** + * Request that the unit deploys. + */ + case EVENT_DEPLOY: + _Event_Deploy(); + break; + + /** + * This event will place the specified object at the specified location. + * The event is used to place newly constructed buildings down on the map. The + * object type is specified. From this object type, the house can determine the + * exact factory and real object pointer to use. + */ + case EVENT_PLACE: + _Event_Place(); + break; + + /** + * Process the options menu, unless we're playing back a recording. + */ + case EVENT_OPTIONS: + _Event_Options(); + break; + + /** + * Process the options Game Speed + */ + case EVENT_GAMESPEED: + _Event_GameSpeed(); + break; + + /** + * This event starts production of the specified object type. The house can + * determine from the type and ID value, what object to begin production on and + * what factory to use. + */ + case EVENT_PRODUCE: + _Event_Produce(); + break; + + /** + * This event is generated when the player puts production on hold. From the + * object type, the factory can be inferred. + */ + case EVENT_SUSPEND: + _Event_Suspend(); + break; + + /** + * This event is generated when the player cancels production of the specified + * object type. From the object type, the exact factory can be inferred. + */ + case EVENT_ABANDON: + _Event_Abandon(); + break; + + /** + * Toggles the primary factory state of the specified building. + */ + case EVENT_PRIMARY: + _Event_Primary(); + break; + + /** + * If we are placing down the ion cannon blast then lets take + * care of it. + */ + case EVENT_SPECIAL_PLACE: + _Event_Special_Place(); + break; + + /** + * Exit the game. + */ + case EVENT_EXIT: + _Event_Exit(); + break; + + /** + * This even is used to trigger an animation that is generated as a direct + * result of player intervention. + */ + case EVENT_ANIMATION: + _Event_Animation(); + break; + + /** + * Starts or stops repair on the specified object. This event is triggered by the + * player clicking the repair wrench on a building. + */ + case EVENT_REPAIR: + _Event_Repair(); + break; + + /** + * Tells a building/unit to sell. This event is triggered by the player clicking the + * sell animating cursor over the building or unit. + */ + case EVENT_SELL: + _Event_Sell(); + break; + + /** + * Tells the wall at the specified location to sell off. + */ + case EVENT_SELLCELL: + _Event_SellCell(); + break; + + /** + * Update the special control flags. This is necessary so that in a multiplay + * game, all machines will agree on the rules. If these options change during + * game play, then all players are informed that options have changed. + */ + case EVENT_SPECIAL: + _Event_Special(); + break; + + /** + * Adjust connection timing for multiplayer games + */ + case EVENT_RESPONSE_TIME: + _Event_Response_Time(); + break; + + /** + * Save a multiplayer game (this event is only generated in multiplayer mode) + */ + case EVENT_SAVEGAME: + _Event_SaveGame(); + break; + + /** + * Update the archive target for this building. + */ + case EVENT_ARCHIVE: + _Event_Archive(); + break; + + /** + * Add a new player to the game: + * - Form a network connection to him + * - Add his name, ID, House etc to our list of players + * - Re-sort the ID array + * - Place his units on the map + */ + case EVENT_ADDPLAYER: + _Event_AddPlayer(); + break; + + /** + * This event tells all systems to use new timing values. It's like + * RESPONSE_TIME, only it works. It's only used with the + * COMM_MULTI_E_COMP protocol. + */ + case EVENT_TIMING: + _Event_Timing(); + break; + + /** + * This event tells all systems what the other systems' process + * timing requirements are; it's used to compute a desired frame rate + * for the game. + */ + case EVENT_PROCESS_TIME: + _Event_Process_Time(); + break; + + /** + * Opens some WOL dialog, perhaps an in-game chat? + */ + case EVENT_PAGEUSER: + _Event_PageUser(); + break; + + /** + * Remove a play from the game. + */ + case EVENT_REMOVEPLAYER: + _Event_RemovePlayer(); + break; + + /** + * Change network latency fudge. + */ + case EVENT_LATENCYFUDGE: + _Event_LatencyFudge(); + break; + + /** + * Default: do nothing. + */ + case EVENT_FRAMESYNC: + case EVENT_MESSAGE: + case EVENT_FRAMEINFO: + default: + break; + } +} + + +/** + * Patch event length in Add_Compressed_Events. + * + * @author: ZivDero + */ +DECLARE_PATCH(_Add_Compressed_Events_ViniferaEvent_Length) +{ + GET_REGISTER_STATIC(unsigned char, eventtype, cl); + static unsigned char eventlength; + + _asm pushad + + if (ViniferaEventClass::Is_Vinifera_Event(static_cast(eventtype))) + { + eventlength = ViniferaEventClass::Event_Length(static_cast(eventtype)); + } + else + { + eventlength = EventClass::Event_Length(static_cast(eventtype)); + } + + if (eventtype == EVENT_ADDPLAYER) + { + _asm popad + _asm mov bl, eventlength + JMP_REG(esi, 0x005B45EA); + } + else + { + _asm popad + _asm mov bl, eventlength + JMP_REG(esi, 0x005B45F3); + } +} + + +/** + * Extract_Compressed_Events -- extracts events from a packet. + * + * @author: 11/21/1995 DRD - Created. + * ZivDero - Adjustments for Tiberian Sun. + */ +static int Vinifera_Extract_Compressed_Events(void* buf, int bufsize) +{ + int pos = 0; // current buffer parsing position + int leftover = bufsize; // # bytes left to process + EventClass* event; // event ptr for parsing buffer + int count = 0; // # events processed + int datasize = 0; // size of data to copy + EventClass eventdata; // stores Frame, ID, etc + unsigned char numunits = 0; // # units stored in compressed MegaMissions + + /** + * Clear work event structure. + */ + std::memset(&eventdata, 0, sizeof(EventClass)); + + /** + * Assume the first event is a FRAMEINFO event + * Init 'datasize' to the amount of data to copy, minus the EventType value + * For the 1st packet only, this will include all info before the Data + * union, plus the size of the FrameInfo structure, minus the EventType size. + */ + datasize = (offsetof(EventClass, Data) + sizeof(EventClass::Data.FrameInfo)) - sizeof(EventType); + event = reinterpret_cast(static_cast(buf) + pos); + + while (leftover >= datasize + (int)sizeof(EventType)) + { + /** + * Add event to the DoList, only if it's not a FRAMESYNC + * (but FRAMEINFO's do get added.) + */ + if (event->Type != EVENT_FRAMESYNC) + { + /** + * Initialize the common data from the FRAMEINFO event. + * keeping IsExecuted 0 + */ + if (event->Type == EVENT_FRAMEINFO) + { + eventdata.Frame = event->Frame; + eventdata.ID = event->ID; + + /** + * Adjust position past the common data. + */ + pos += offsetof(EventClass, Data) - sizeof(EventType); + leftover -= offsetof(EventClass, Data) - sizeof(EventType); + } + + /** + * If MEGAMISSION event get the number of units (events to generate). + */ + else if (event->Type == EVENT_MEGAMISSION) + { + numunits = *(static_cast(buf) + pos + sizeof(eventdata.Type)); + pos += sizeof(numunits); + leftover -= sizeof(numunits); + } + + /** + * Clear the union data portion of the event. + */ + memset(&eventdata.Data, 0, sizeof(eventdata.Data)); + eventdata.Type = event->Type; + datasize = ViniferaEventClass::Event_Length(eventdata.Type); + + switch (eventdata.Type) + { + case EVENT_RESPONSE_TIME: + memcpy(&eventdata.Data.FrameInfo.Delay, static_cast(buf) + pos + sizeof(EventType), datasize); + break; + + case EVENT_ADDPLAYER: + memcpy(&eventdata.Data.Variable.Size, static_cast(buf) + pos + sizeof(EventType), datasize); + + eventdata.Data.Variable.Pointer = new char[eventdata.Data.Variable.Size]; + memcpy(eventdata.Data.Variable.Pointer, static_cast(buf) + pos + sizeof(EventType) + datasize, eventdata.Data.Variable.Size); + + pos += eventdata.Data.Variable.Size; + leftover -= eventdata.Data.Variable.Size; + + break; + + case EVENT_MEGAMISSION: + memcpy(&eventdata.Data.MegaMission, static_cast(buf) + pos + sizeof(EventType), datasize); + + if (numunits > 1) + { + pos += datasize + sizeof(EventType); + leftover -= datasize + sizeof(EventType); + datasize = sizeof(eventdata.Data.MegaMission.Whom); + + while (numunits) + { + if (!DoList.Add(eventdata)) + return -1; + + /** + * Keep count of how many events we add to the queue. + */ + count++; + numunits--; + memcpy(&eventdata.Data.MegaMission.Whom, static_cast(buf) + pos, datasize); + + /** + * If one unit left fall thru to normal code. + */ + if (numunits == 1) + { + datasize -= sizeof(EventType); + break; + } + else + { + pos += datasize; + leftover -= datasize; + } + } + } + break; + + default: + memcpy(&eventdata.Data, static_cast(buf) + pos + sizeof(EventType), datasize); + break; + } + + if (!DoList.Add(eventdata)) + { + if (eventdata.Type == EVENT_ADDPLAYER) + delete[] eventdata.Data.Variable.Pointer; + + return -1; + } + + /** + * Keep count of how many events we add to the queue. + */ + count++; + + pos += datasize + sizeof(EventType); + leftover -= datasize + sizeof(EventType); + + if (leftover) + { + event = reinterpret_cast(static_cast(buf) + pos); + datasize = ViniferaEventClass::Event_Length(event->Type); + if (event->Type == EVENT_MEGAMISSION) + datasize += sizeof(numunits); + } + } + /** + * FRAMESYNC event: This >should< be the only event in the buffer, + * and it will be uncompressed. + */ + else + { + pos += datasize + sizeof(EventType); + leftover -= datasize + sizeof(EventType); + event = reinterpret_cast(static_cast(buf) + pos); + + /** + * Size of FRAMESYNC event - EventType size. + */ + datasize = offsetof(EventClass, Data) + sizeof(EventClass::Data.FrameInfo) - sizeof(EventType); + } + } + + return count; + +} + + +/** + * Main function for patching the hooks. + */ +void EventClassExtension_Hooks() +{ + Patch_Jump(0x00494280, &EventClassExt::_Execute); + Patch_Jump(0x005B45D5, &_Add_Compressed_Events_ViniferaEvent_Length); + Patch_Jump(0x005B4A40, &Vinifera_Extract_Compressed_Events); +} diff --git a/src/extensions/event/eventext_hooks.h b/src/extensions/event/eventext_hooks.h new file mode 100644 index 000000000..7a2d9b0d6 --- /dev/null +++ b/src/extensions/event/eventext_hooks.h @@ -0,0 +1,31 @@ +/******************************************************************************* +/* O P E N S O U R C E -- V I N I F E R A ** +/******************************************************************************* + * + * @project Vinifera + * + * @file EVENTEXT_HOOKS.H + * + * @author ZivDero + * + * @brief Contains the hooks for the extended EventClass. + * + * @license Vinifera 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. + * + * Vinifera 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 . + * + ******************************************************************************/ +#pragma once + + +void EventClassExtension_Hooks(); diff --git a/src/extensions/extension.cpp b/src/extensions/extension.cpp index 7925d5fdf..6684bfaa0 100644 --- a/src/extensions/extension.cpp +++ b/src/extensions/extension.cpp @@ -139,6 +139,7 @@ #include "warheadtypeext.h" #include "waveext.h" #include "weapontypeext.h" +#include "viniferaevent.h" #include "rulesext.h" #include "scenarioext.h" @@ -1144,8 +1145,8 @@ static bool Print_Event_List(FILE *fp, QueueClass &list) if (ev) { char ev_byte_format[4]; Wstring ev_data_buffer; - int ev_size = EventClass::Event_Length(ev->Type); - const char *ev_name = EventClass::Event_Name(ev->Type); + int ev_size = ViniferaEventClass::Event_Length(ev->Type); + const char *ev_name = ViniferaEventClass::Event_Name(ev->Type); for (int i = 0; i < ev_size; ++i) { std::snprintf(ev_byte_format, sizeof(ev_byte_format), "%02X", (unsigned char)ev->Data.Array.Byte[i]); // We use this union member so we can do array access. ev_data_buffer += ev_byte_format; diff --git a/src/extensions/extension_hooks.cpp b/src/extensions/extension_hooks.cpp index 361e2ff06..bf4c5cb2d 100644 --- a/src/extensions/extension_hooks.cpp +++ b/src/extensions/extension_hooks.cpp @@ -94,7 +94,7 @@ #include "tacticalext_hooks.h" #include "superext_hooks.h" //#include "aitriggerext_hooks.h" -//#include "aitriggertypeext_hooks.h" +#include "aitriggertypeext_hooks.h" //#include "neuronext_hooks.h" //#include "foggedobjectext_hooks.h" //#include "alphashapeext_hooks.h" @@ -153,6 +153,7 @@ #include "hooker.h" #include "hooker_macros.h" #include "spawnmanager_hooks.h" +#include "eventext_hooks.h" void Extension_Hooks() @@ -235,7 +236,7 @@ void Extension_Hooks() TacticalExtension_Hooks(); SuperClassExtension_Hooks(); //AITriggerClassExtension_Hooks(); // Not yet implemented - //AITriggerTypeClassExtension_Hooks(); // Not yet implemented + AITriggerTypeClassExtension_Hooks(); //NeuronClassExtension_Hooks(); // Not yet implemented //FoggedObjectClassExtension_Hooks(); // Not yet implemented //AlphaShapeClassExtension_Hooks(); // Not yet implemented @@ -282,6 +283,7 @@ void Extension_Hooks() MultiScoreExtension_Hooks(); ScoreClassExtension_Hooks(); MultiMissionExtension_Hooks(); + EventClassExtension_Hooks(); /** * Dialogs and associated code. diff --git a/src/extensions/foot/footext_hooks.cpp b/src/extensions/foot/footext_hooks.cpp index c36b5ce9e..4500148e6 100644 --- a/src/extensions/foot/footext_hooks.cpp +++ b/src/extensions/foot/footext_hooks.cpp @@ -681,6 +681,26 @@ void FootClassExt::_Death_Announcement(TechnoClass* source) const } +/** + * #issue-177 + * + * Patches the harvester counting to count all units listed under HarvesterUnit. + * + * @author: ZivDero + */ +DECLARE_PATCH(_FootClass_Search_For_Tiberium_Weighted_HarvesterUnit_Patch) +{ + GET_REGISTER_STATIC(FootClass *, this_ptr, edi); + + static int count; + + count = this_ptr->House->Count_Owned(Rule->HarvesterUnit); + + _asm mov eax, count + JMP_REG(esi, 0x004A7A65); +} + + /** * Main function for patching the hooks. */ @@ -694,4 +714,5 @@ void FootClassExtension_Hooks() Patch_Jump(0x004A6A40, &FootClassExt::_Draw_Action_Line); Patch_Jump(0x004A4D60, &FootClassExt::_Death_Announcement); Patch_Jump(0x004A76F0, &FootClassExt::_Search_For_Tiberium); + Patch_Jump(0x004A7A3F, &_FootClass_Search_For_Tiberium_Weighted_HarvesterUnit_Patch); } diff --git a/src/extensions/house/houseext.cpp b/src/extensions/house/houseext.cpp index 53015b25d..0ce8e4d8d 100644 --- a/src/extensions/house/houseext.cpp +++ b/src/extensions/house/houseext.cpp @@ -44,7 +44,8 @@ HouseClassExtension::HouseClassExtension(const HouseClass *this_ptr) : AbstractClassExtension(this_ptr), TiberiumStorage(Tiberiums.Count()), - WeedStorage(Tiberiums.Count()) + WeedStorage(Tiberiums.Count()), + IsObserver(false) { //if (this_ptr) EXT_DEBUG_TRACE("HouseClassExtension::HouseClassExtension - 0x%08X\n", (uintptr_t)(This())); @@ -56,8 +57,19 @@ HouseClassExtension::HouseClassExtension(const HouseClass *this_ptr) : if (this_ptr) { - new ((StorageClassExt*)&(this_ptr->Tiberium)) StorageClassExt(&TiberiumStorage); - new ((StorageClassExt*)&(this_ptr->Weed)) StorageClassExt(&WeedStorage); + new ((StorageClassExt*)&this_ptr->Tiberium) StorageClassExt(&TiberiumStorage); + new ((StorageClassExt*)&this_ptr->Weed) StorageClassExt(&WeedStorage); + + /** + * Vanilla hardcoded the ActLike default, unhardcode that. + * Uuuhh... the fact that this is const is annoying, but for now while + * this is not a massive issue, just const_cast it. + */ + if (Wstring(this_ptr->IniName) != "Neutral" && Wstring(this_ptr->IniName) != "Special") + const_cast(this_ptr)->ActLike = this_ptr->Class->House; + else + const_cast(this_ptr)->ActLike = HOUSE_NONE; + } HouseExtensions.Add(this); diff --git a/src/extensions/house/houseext.h b/src/extensions/house/houseext.h index 5c0e151d5..3f486387d 100644 --- a/src/extensions/house/houseext.h +++ b/src/extensions/house/houseext.h @@ -75,4 +75,9 @@ HouseClassExtension final : public AbstractClassExtension * Replacement Weed storage. */ VectorClass WeedStorage; + + /** + * Is this house an observer? + */ + bool IsObserver; }; diff --git a/src/extensions/house/houseext_hooks.cpp b/src/extensions/house/houseext_hooks.cpp index e63336456..fae576ae7 100644 --- a/src/extensions/house/houseext_hooks.cpp +++ b/src/extensions/house/houseext_hooks.cpp @@ -39,34 +39,52 @@ #include "unittype.h" #include "unittypeext.h" #include "mouse.h" +#include "unittype.h" +#include "rules.h" +#include "rulesext.h" +#include "unit.h" +#include "session.h" #include "fatal.h" #include "debughandler.h" #include "asserthandler.h" +#include "buildingext.h" #include "extension_globals.h" #include "sidebarext.h" #include "rules.h" #include "session.h" #include "ccini.h" +#include "fetchres.h" #include "sideext.h" #include "hooker.h" #include "hooker_macros.h" +#include "houseext.h" +#include "language.h" +#include "logic.h" +#include "scenarioext.h" +#include "spawner.h" #include "tibsun_functions.h" +#include "vox.h" /** - * A fake class for implementing new member functions which allow - * access to the "this" pointer of the intended class. - * - * @note: This must not contain a constructor or deconstructor! - * @note: All functions must be prefixed with "_" to prevent accidental virtualization. - */ -static class HouseClassExt final : public HouseClass + * A fake class for implementing new member functions which allow + * access to the "this" pointer of the intended class. + * + * @note: This must not contain a constructor or deconstructor! + * @note: All functions must be prefixed with "_" to prevent accidental virtualization. + */ +class HouseClassExt final : public HouseClass { public: ProdFailType _Begin_Production(RTTIType type, int id, bool resume); ProdFailType _Abandon_Production(RTTIType type, int id); + bool _Can_Make_Money(); + UrgencyType _Check_Raise_Money(); int _AI_Building(); + void _MPlayer_Defeated(); + DiffType _Assign_Handicap(DiffType handicap); + void _Make_Ally(HouseClass* house); }; @@ -267,28 +285,6 @@ int HouseClassExt::_AI_Building() BASE_DEFENSE = -1 }; - /** - * Unfortunately, ts-patches spawner has a hack here. - * Until we reimplement the spawner in Vinifera, this will have to do. - */ - static bool spawner_hack_init = false; - static bool spawner_hack_mpnodes = false; - - if (!spawner_hack_init) - { - RawFileClass file("SPAWN.INI"); - CCINIClass spawn_ini; - - if (file.Is_Available()) { - - spawn_ini.Load(file, false); - spawner_hack_mpnodes = spawn_ini.Get_Bool("Settings", "UseMPAIBaseNodes", spawner_hack_mpnodes); - } - - spawner_hack_init = true; - } - - if (BuildStructure != BUILDING_NONE) return TICKS_PER_SECOND; if (ConstructionYards.Count() == 0) return TICKS_PER_SECOND; @@ -339,7 +335,7 @@ int HouseClassExt::_AI_Building() * just proceed with building the base node. */ BuildingTypeClass* b = BuildingTypes[node->Type]; - if (Session.Type == GAME_NORMAL || spawner_hack_mpnodes || b->Drain + Drain <= Power - PowerSurplus || Rule->BuildConst.Is_Present(b) || b->Drain <= 0) { + if (Session.Type == GAME_NORMAL || ScenExtension->IsUseMPAIBaseNodes || b->Drain + Drain <= Power - PowerSurplus || Rule->BuildConst.Is_Present(b) || b->Drain <= 0) { /** * Check if this is a building upgrade if we can actually place the upgrade where it's scheduled to be placed. @@ -425,6 +421,550 @@ int HouseClassExt::_AI_Building() } +/** + * #issue-177 + * + * Checks if the AI house has the capability to make money. Adjusted to + * use the entire Build* and HarvesterUnit lists. + * + * @author: ZivDero + */ +bool HouseClassExt::_Can_Make_Money() +{ + const int credits = Available_Money(); + const int ref_cost = Get_First_Ownable(Rule->BuildRefinery)->Cost_Of(this); + const int harv_cost = Get_First_Ownable(Rule->HarvesterUnit)->Cost_Of(this); + + const int ref_count = Count_Owned(Rule->BuildRefinery); + const int harv_count = Count_Owned(Rule->HarvesterUnit); + + /** + * If we don't have any refineries, building one is a priority. + */ + if (ref_count == 0) + return credits > ref_cost; + + /** + * If we have a refinery and a harvester, all's well. + */ + if (harv_count) + return true; + + const bool has_factory = Count_Owned(Rule->BuildWeapons) > 0; + const int factory_cost = Get_First_Ownable(Rule->BuildWeapons)->Cost_Of(this); + + /** + * If we have a refinery, but not a harvester, see if + * we can build one if we have a factory. + */ + if (has_factory && credits >= harv_cost) + return true; + + /** + * And if we don't have a factory, see if we can build one. + */ + if (credits >= harv_cost + factory_cost) + return true; + + /** + * Worst case, see if we can build a new refinery to get a free harvester. + */ + if (credits >= ref_cost) + return true; + + return false; +} + + +/** + * #issue-177 + * + * Checks if the AI needs to urgently raise more money. + * Adjusted to use the entire Build* and HarvesterUnit lists. + * + * @author: ZivDero + */ +UrgencyType HouseClassExt::_Check_Raise_Money() +{ + UrgencyType urgency = URGENCY_NONE; + + /** + * Human players don't need AI to raise money for them. + */ + const bool human = Session.Type == GAME_NORMAL ? Is_Player_Control() : IsHuman; + if (human) + return urgency; + + /** + * If we can afford to have a harvester and a refinery, all is well. + */ + if (Can_Make_Money()) + return urgency; + + /** + * See if we have a refinery. + */ + if (Count_Owned(Rule->BuildRefinery)) + { + /** + * Iterate all the buildings and check if we have a refinery under construction. + * If so, we don't need raise money, since we'll get a free harvester. + */ + for (int i = 0; i < Buildings.Count(); i++) + { + BuildingClass* building = Buildings[i]; + if (building->House == this) + { + if (Rule->BuildRefinery.Is_Present(building->Class) && building->Get_Mission() == MISSION_CONSTRUCTION) + return urgency; + + urgency = URGENCY_NONE; + } + } + + /** + * Check if what we're currently building is a harvester. + * If it's not and we don't have enough money to build one, + * we've got minor issues. + */ + const UnitTypeClass* harvester = Get_First_Ownable(Rule->HarvesterUnit); + if (BuildUnit != harvester->Type) + { + if (Available_Money() < harvester->Cost_Of(this)) + urgency++; + + return urgency; + } + + /** + * Check all the factories and find which is building our harvester. + * If we haven't got enough money to complete contruction, we've got issues. + */ + for (int i = 0; i < Factories.Count(); i++) + { + const FactoryClass* factory = Factories[i]; + if (factory && factory->House == this) + { + ObjectClass* obj = factory->Get_Object(); + if (obj && obj->What_Am_I() == RTTI_UNIT + && Rule->HarvesterUnit.Is_Present(static_cast(obj->Techno_Type_Class()))) + { + if (Available_Money() < factory->Balance) + urgency++; + + return urgency; + + } + } + } + } + else + { + /** + * Check if what we're currently building is a refinery. + * If it's not and we don't have enough money to build one, + * we've got minor issues. + */ + const BuildingTypeClass* refinery = Get_First_Ownable(Rule->BuildRefinery); + if (BuildStructure != refinery->Type) + { + if (Available_Money() < refinery->Cost_Of(this)) + urgency++; + + return urgency; + } + + /** + * Check all the factories and find which is building our refinery. + * If we haven't got enough money to complete contruction, we've got issues. + */ + for (int i = 0; i < Factories.Count(); i++) + { + const FactoryClass* factory = Factories[i]; + if (factory && factory->House == this) + { + ObjectClass* obj = factory->Get_Object(); + if (obj && obj->What_Am_I() == RTTI_BUILDING + && Rule->BuildRefinery.Is_Present(static_cast(obj->Techno_Type_Class()))) + { + if (Available_Money() < factory->Balance) + urgency++; + + return urgency; + } + } + } + } + + /** + * Something weird has happened, it's surely not good. + */ + urgency++; + return urgency; +} + + +/** + * A house is defeated in multiplayer. + * + * @author: 05/25/1995 BRR - Created + * 29/10/2024 ZivDero - Adjustments for Tiberian Sun + */ +void HouseClassExt::_MPlayer_Defeated() +{ + char txt[80]; + int i, j; + unsigned char id; + HouseClass* hptr; + HouseClass* hptr2; + int num_alive; + int num_humans; + bool all_allies; + + /** + * Set the defeat flag for this house + */ + IsDefeated = true; + + /** + * If this is a computer controlled house, then all computer controlled + * houses become paranoid. + */ + if (IQ == Rule->MaxIQ && !(IsHuman || Session.Type == GAME_NORMAL && IsPlayerControl) && Rule->IsComputerParanoid) { + Computer_Paranoid(); + } + + /** + * Remove this house's flag & flag home cell + */ + if (Special.IsCaptureTheFlag) { + if (FlagLocation) { + Flag_Remove(FlagLocation, true); + } + else { + if (FlagHome != 0) { + Flag_Remove(&Map[FlagHome], true); + } + } + } + + /** + * If harvester truce is on, remove all of this player's harvesters. + */ + if (Session.Type != GAME_NORMAL && Scen->SpecialFlags.IsHarvesterImmune) { + for (int i = 0; i < Units.Count(); i++) { + if (Units[i]->Owning_House() == this && Units[i]->IsActive) { + Units[i]->Remove_This(); + } + } + } + + /** + * If this is me: + * - Set MPlayerObiWan, so I can only send messages to all players, and + * not just one (so I can't be obnoxiously omnipotent) + * - Reveal the map + * - Add my defeat message + */ + if (PlayerPtr == this) { + Session.ObiWan = true; + Map.Reveal_The_Map(); + HiddenSurface->Fill(0); + + if (Vinifera_ObserverPtr != this) { + /** + * Pop up a message showing that I was defeated + */ + std::snprintf(txt, std::size(txt), Fetch_String(TXT_PLAYER_DEFEATED), IniName); + Session.Messages.Add_Message(nullptr, 0, txt, static_cast(Session.ColorIdx), TPF_6PT_GRAD | TPF_USE_GRAD_PAL | TPF_FULLSHADOW, Rule->MessageDelay * TICKS_PER_MINUTE); + Speak(VOX_YOU_HAVE_LOST); + } + + Map.Flag_To_Redraw(0); + DEBUG_INFO("MPlayer_Defeated() - Player %s has been defeated (OBIWAN MODE)\n", IniName); + + } + else { + + /** + * If it wasn't me, find out who was defeated + */ + if (!Class->IsMultiplayPassive) { + if (Vinifera_ObserverPtr != this) { + std::snprintf(txt, std::size(txt), Fetch_String(TXT_PLAYER_DEFEATED), IniName); + Session.Messages.Add_Message(nullptr, 0, txt, RemapColor, TPF_6PT_GRAD | TPF_USE_GRAD_PAL | TPF_FULLSHADOW, Rule->MessageDelay * TICKS_PER_MINUTE); + Speak(VOX_PLAYER_DEFEATED); + + } + + Map.Flag_To_Redraw(0); + DEBUG_INFO("MPlayer_Defeated() - Opponent %s has been defeated\n", IniName); + } + } + + /** + * Find out how many players are left alive. + */ + num_alive = 0; + num_humans = 0; + for (i = 0; i < Houses.Count(); i++) { + hptr = Houses[i]; + if (hptr && !hptr->IsDefeated && !hptr->Class->IsMultiplayPassive) { + if (hptr->IsHuman || (Session.Type == GAME_NORMAL && hptr->IsPlayerControl)) { + num_humans++; + } + num_alive++; + } + } + DEBUG_INFO("MPlayer_Defeated() - Alive = %d, Humans = %d\n", num_alive, num_humans); + + /** + * If all the houses left alive are allied with each other, then in reality + * there's only one player left: + */ + all_allies = true; + for (i = 0; i < Houses.Count(); i++) { + + /** + * Get a pointer to this house + */ + hptr = Houses[i]; + if (!hptr || hptr->IsDefeated || hptr->Class->IsMultiplayPassive) + continue; + + /** + * Loop through all houses; if there's one left alive that this house + * isn't allied with, then all_allies will be false + */ + for (j = 0; j < Houses.Count(); j++) { + hptr2 = Houses[j]; + if (!hptr2) { + continue; + } + + if (!hptr2->IsDefeated && !hptr2->Class->IsMultiplayPassive && hptr != hptr2 && !hptr->Is_Ally(hptr2)) { + all_allies = false; + break; + } + } + if (!all_allies) { + break; + } + } + + /** + * If all houses left are allies, set 'num_alive' to 1; game over. + */ + if (all_allies) { + + Session.SawCompletion = true; + DEBUG_INFO("Saw game completion due to player defeat\n"); + DEBUG_INFO("MPlayer_Defeated() - All remaining players are allied\n"); + num_alive = 1; + } + + /** + * If there's only one human player left or no humans left, the game is over: + * - Determine whether this player wins or loses, based on the state of the + * player's IsDefeated flag + */ + HouseClassExtension* houseext = Extension::Fetch(this); + if (!houseext->IsObserver) { + if (num_alive == 1 || (num_humans == 0 && !(Vinifera_SpawnerActive && Vinifera_SpawnerConfig->ContinueWithoutHumans))) { + IsToDie = false; + + if (PlayerPtr->IsDefeated) { + DEBUG_INFO("MPlayer_Defeated() - Flag_To_Lose\n"); + Flag_To_Lose(false); + } + else { + DEBUG_INFO("MPlayer_Defeated() - Flag_To_Win\n"); + Flag_To_Win(false); + } + } + } + + +} + + +/** + * Assigns the specified handicap rating to the house. + * + * @author: 07/09/1996 JLB - Created + * 29/10/2024 ZivDero - Adjustments for Tiberian Sun + */ +DiffType HouseClassExt::_Assign_Handicap(DiffType handicap) +{ + DiffType old = Difficulty; + Difficulty = handicap; + + const DifficultyClass* diff = &RuleExtension->Diff[handicap]; + + if (Is_Human_Control() && handicap == DIFF_NORMAL && Vinifera_HumanNormalDifficulty) { + diff = &RuleExtension->DiffHuman; + } + + if (Session.Type != GAME_NORMAL) { + HouseTypeClass const* hptr = Class; + FirepowerBias = hptr->FirepowerBias * diff->FirepowerBias; + GroundspeedBias = hptr->GroundspeedBias * diff->GroundspeedBias * Rule->GameSpeedBias; + AirspeedBias = hptr->AirspeedBias * diff->AirspeedBias * Rule->GameSpeedBias; + ArmorBias = hptr->ArmorBias * diff->ArmorBias; + ROFBias = hptr->ROFBias * diff->ROFBias; + CostBias = hptr->CostBias * diff->CostBias; + RepairDelay = diff->RepairDelay; + BuildDelay = diff->BuildDelay; + BuildSpeedBias = hptr->BuildSpeedBias * diff->BuildSpeedBias * Rule->GameSpeedBias; + } + else { + FirepowerBias = diff->FirepowerBias; + GroundspeedBias = diff->GroundspeedBias * Rule->GameSpeedBias; + AirspeedBias = diff->AirspeedBias * Rule->GameSpeedBias; + ArmorBias = diff->ArmorBias; + ROFBias = diff->ROFBias; + CostBias = diff->CostBias; + RepairDelay = diff->RepairDelay; + BuildDelay = diff->BuildDelay; + BuildSpeedBias = diff->BuildSpeedBias * Rule->GameSpeedBias; + } + + TeamTime = 175 * ID + Rule->TeamDelays[handicap]; + + return old; +} + + +/** + * Make the specified house an ally. + * + * @author: 05/08/1995 JLB - Created + * 29/10/2024 ZivDero - Adjustments for Tiberian Sun + */ +void HouseClassExt::_Make_Ally(HouseClass* house) +{ + if (Is_Allowed_To_Ally(house)) { + + Allies |= (1L << house->ID); + + /** + * Don't consider the newfound ally to be an enemy -- of course. + */ + Recalc_Threat_Regions(); + Clear_Anger(house); + + if (Enemy == house->ID) { + Enemy = HOUSE_NONE; + } + + if (ScenarioInit) { + Control.Allies |= (1L << house->ID); + } + + if (Session.Type != GAME_NORMAL || !ScenarioInit) { + + if (!ScenarioInit) { + + /** + * An alliance with another human player will cause the computer + * players (if present) to become paranoid. + */ + if (Is_Human_Control() && Rule->IsComputerParanoid && !house->Class->IsMultiplayPassive) { + Computer_Paranoid(); + } + + /** + * Sweep through all techno objects and perform a cheeseball tarcom clear to ensure + * that fighting will most likely stop when the cease fire begins. + */ + for (int index = 0; index < Logic.Count(); index++) { + ObjectClass* object = Logic[index]; + + if (object != NULL && object->As_Techno() && !object->IsInLimbo && object->Owner() == Class->ID) { + TargetClass target = As_Target(static_cast(object)->TarCom); + if (target.Is_Valid() && target.As_Techno()) { + if (Is_Ally(target.As_Techno())) { + static_cast(object)->Assign_Target(nullptr); + } + } + } + } + + if (Is_Human_Control() && Session.Type != GAME_NORMAL && !house->Class->IsMultiplayPassive) { + + char buffer[80]; + std::snprintf(buffer, std::size(buffer), Fetch_String(TXT_HAS_ALLIED), IniName, house->IniName); + Session.Messages.Add_Message(nullptr, 0, buffer, RemapColor, TPF_6PT_GRAD | TPF_USE_GRAD_PAL | TPF_FULLSHADOW, TICKS_PER_MINUTE * Rule->MessageDelay); + } + + if (Is_Human_Control()) { + Speak(VOX_ALLIANCE_FORMED); + } + } + + /** + * Cause all technos to be revealed to the house that has been + * allied with. + */ + if (Rule->IsAllyReveal && house == PlayerPtr) { + for (int index = 0; index < Technos.Count(); index++) { + TechnoClass const* t = Technos[index]; + + if (!t->IsInLimbo && t->House == this) { + Map.Sight_From(t->Center_Coord(), t->Techno_Type_Class()->SightRange, PlayerPtr); + } + } + } + + Map.Flag_To_Redraw(); + } + } +} + + + +/** + * #issue-177 + * + * Allow the game to check BaseUnit for all pertinent entries for "Short Game". + * + * #NOTE: The code before this patch already checks if the house has + * any buildings first. + * + * @author: CCHyper, ZivDero + */ +DECLARE_PATCH(_HouseClass_AI_Short_Game_BaseUnit_Patch) +{ + GET_REGISTER_STATIC(HouseClass *, this_ptr, esi); + static UnitTypeClass *unittype; + static UnitType unit; + static int count; + + /** + * Count all MCVs we own to see if the player should explode. + */ + count = this_ptr->Count_Owned(RuleExtension->BaseUnit); + + if (count) { + goto continue_function; + } + + goto blowup_house; + + /** + * + */ +continue_function: + JMP_REG(eax, 0x004BCF6E); + + /** + * Blows up the house, marking the house as defeated. + */ +blowup_house: + JMP_REG(ecx, 0x004BCF60); +} + + /** * Patch for InstantSuperRechargeCommandClass * @@ -704,6 +1244,412 @@ DECLARE_PATCH(_HouseClass_Enable_SWs_Check_For_Building_Power) } +/** + * #issue-177 + * + * Patches the check for if a house owns a Construction Yard to check the entire BuildConst list. + * + * @author: ZivDero + */ +DECLARE_PATCH(_HouseClass_AI_BuildConst_Patch) +{ + GET_REGISTER_STATIC(HouseClass*, this_ptr, esi); + + if (this_ptr->Count_Owned(Rule->BuildConst) > 0) + { + JMP(0x004BCD85); + } + + JMP(0x004BCE0B); +} + + +/** + * #issue-177 + * + * Patches the check for if a house owns a harvester to check the entire HarvesterUnit list. + * + * @author: ZivDero + */ +DECLARE_PATCH(_HouseClass_AI_Count_HarvesterUnit_Patch) +{ + GET_REGISTER_STATIC(HouseClass*, this_ptr, esi); + static int harv_count; + + harv_count = this_ptr->Count_Owned(Rule->HarvesterUnit); + + _asm mov eax, harv_count + JMP_REG(ecx, 0x004BCF5A); +} + + +/** + * #issue-177 + * + * Patches the check for if a house is building a harvester to check the entire HarvesterUnit list. + * + * @author: ZivDero + */ +DECLARE_PATCH(_HouseClass_AI_Is_Building_Harvester_Unit_Patch) +{ + GET_REGISTER_STATIC(HouseClass*, this_ptr, esi); + + if (this_ptr->BuildUnit != -1 && Rule->HarvesterUnit.Is_Present(UnitTypes[this_ptr->BuildUnit])) + { + JMP(0x004BD0E5); + } + + JMP(0x004BD0D7); +} + + +/** + * #issue-177 + * + * Patches the AI to correctly consider all refineries, weapons factories and harvesters. + * + * @author: ZivDero + */ +DECLARE_PATCH(_HouseClass_AI_Raise_Money_HarvRef1) +{ + GET_REGISTER_STATIC(HouseClass*, this_ptr, esi); + + static bool build_harv; + static int object_cost; + + /** + * If we have a refinery and a weapons factory, build a harvester, otherwise - a refinery. + */ + if (this_ptr->Count_Owned(Rule->BuildRefinery) > 0 + && this_ptr->Count_Owned(Rule->BuildWeapons) > 0) + { + build_harv = true; + object_cost = this_ptr->Get_First_Ownable(Rule->HarvesterUnit)->Cost_Of(this_ptr); + } + else + { + build_harv = false; + object_cost = this_ptr->Get_First_Ownable(Rule->BuildRefinery)->Cost_Of(this_ptr); + } + + _asm mov al, build_harv + _asm mov [esp+0x13], al + _asm mov eax, object_cost + + JMP_REG(ebx, 0x004C0D94); +} + + +/** + * #issue-177 + * + * Patches the AI to correctly construct its own faction's harvester. + * + * @author: ZivDero + */ +DECLARE_PATCH(_HouseClass_AI_Raise_Money_HarvRef2) +{ + GET_REGISTER_STATIC(HouseClass*, this_ptr, esi); + + UnitType harv; + harv = this_ptr->Get_First_Ownable(Rule->HarvesterUnit)->Type; + + _asm mov eax, harv + JMP_REG(ecx, 0x004C0F72); +} + + +/** + * #issue-177 + * + * Patches the AI to correctly construct its own faction's refinery. + * + * @author: ZivDero + */ +DECLARE_PATCH(_HouseClass_AI_Raise_Money_HarvRef3) +{ + GET_REGISTER_STATIC(HouseClass*, this_ptr, esi); + + BuildingTypeClass* refinery_ptr; + BuildingTypeClass** refinery_ptr_ptr; + refinery_ptr = this_ptr->Get_First_Ownable(Rule->BuildRefinery); + refinery_ptr_ptr = &refinery_ptr; + + // The instructions here are messy, so we hijack when the game + // is accessing the vector and substitute our pointer + _asm mov edx, refinery_ptr_ptr + JMP(0x004C0FBB); +} + + +/** + * #issue-177 + * + * Patches the AI to correctly construct its own faction's refinery. + * + * @author: ZivDero + */ +DECLARE_PATCH(_HouseClass_AI_Raise_Money_HarvRef4) +{ + GET_REGISTER_STATIC(HouseClass*, this_ptr, esi); + BuildingTypeClass* refinery; + + refinery = this_ptr->Get_First_Ownable(Rule->BuildRefinery); + + _asm mov eax, refinery + JMP_REG(ecx, 0x004C105E); +} + + +/** + * #issue-177 + * + * Patches the AI to correctly count all harvesters and refineries. + * + * @author: ZivDero + */ +DECLARE_PATCH(_HouseClass_AI_Unit_HarvRef1) +{ + GET_REGISTER_STATIC(HouseClass*, this_ptr, ebp); + static int harv_count, ref_count; + + harv_count = this_ptr->Count_Owned(Rule->HarvesterUnit); + ref_count = this_ptr->Count_Owned(Rule->BuildRefinery); + + _asm mov esi, harv_count + _asm mov eax, ref_count + JMP_REG(ecx, 0x004C16AE); +} + + +/** + * #issue-177 + * + * Patches the AI to correctly building its own faction's harvester. + * + * @author: ZivDero + */ +DECLARE_PATCH(_HouseClass_AI_Unit_HarvRef2) +{ + GET_REGISTER_STATIC(HouseClass*, this_ptr, ebp); + static UnitTypeClass* harvester; + + harvester = this_ptr->Get_First_Ownable(Rule->HarvesterUnit); + + _asm mov eax, harvester + JMP_REG(edx, 0x004C1718); +} + + +/** + * #issue-177 + * + * Patches the AI to correctly consider all Construction Yards from the list in prerequisite checks. + * + * @author: ZivDero + */ +DECLARE_PATCH(_HouseClass_Has_Prerequisites_BuildConst) +{ + GET_REGISTER_STATIC(BuildingTypeClass*, building, ecx); + _asm pushad + + if (!Rule->BuildConst.Is_Present(building)) + { + _asm popad + JMP(0x004C5985); + } + + _asm popad + JMP(0x004C5B62); +} + + +/** + * #issue-177 + * + * Patches the AI to correctly consider all Construction Yards from the list. + * + * @author: ZivDero + */ +DECLARE_PATCH(_HouseClass_GenerateAIBuildList_4C5BB0_BuildConst) +{ + GET_STACK_STATIC(HouseClass*, this_ptr, esp, 0x14); + BuildingTypeClass* conyard; + + conyard = this_ptr->Get_First_Ownable(Rule->BuildConst); + + _asm mov esi, conyard; + JMP(0x004C5E28); +} + + +/** + * #issue-177 + * + * Patches the AI to correctly consider all Construction Yards from the list as targets for the Ion Cannon. + * + * @author: ZivDero + */ +DECLARE_PATCH(_HouseClass_AI_Use_Super_Ion_Cannon_BuildConst) +{ + GET_REGISTER_STATIC(UnitTypeClass*, unittype, ecx); + _asm push eax + + if (Rule->BuildConst.Is_Present(unittype->DeploysInto)) + { + _asm pop eax + JMP_REG(ecx, 0x004CA232); + } + + _asm pop eax + JMP_REG(edx, 0x004CA240); +} + + +/** + * #issue-177 + * + * Patches the AI to correctly consider all Construction Yards from the list when the AI takes over a player's house. + * + * @author: ZivDero + */ +DECLARE_PATCH(_HouseClass_AI_Takeover_BuildConst) +{ + GET_REGISTER_STATIC(BuildingTypeClass*, buildingtype, ecx); + _asm push eax + + if (Rule->BuildConst.Is_Present(buildingtype)) + { + _asm pop eax + JMP_REG(edi, 0x004CA9A9); + } + + _asm pop eax + JMP_REG(edi, 0x004CA9B7) +} + + +/** + * #issue-177 + * + * Fix a vanilla bug where vehicles thieves were able to target harvesters even when HarvesterTruce was on. + * + * @author: ZivDero + */ +DECLARE_PATCH(_InfantryClass_What_Action_Harvester_Thief) +{ + GET_REGISTER_STATIC(UnitClass*, target, esi); + + if (target->What_Am_I() == RTTI_UNIT && Rule->HarvesterUnit.Is_Present(target->Class)) + { + // return ACTION_SELECT; + JMP(0x004D7258); + } + + // return ACTION_CAPTURE; + JMP(0x004D72A8); +} + + +/** + * Patch to enable base nodes for the AI when UseMPAIBaseNodes=yes is set in the scenario. + * + * @author: ZivDero + */ +DECLARE_PATCH(_HouseClass_AI_Building_MP_AI_BaseNodes_Patch) +{ + _asm pushad + + /** + * Use base nodes in Campaign. + */ + if (Session.Type == GAME_NORMAL) + { + _asm popad + JMP_REG(ecx, 0x004C1554); + } + + /** + * Also use base nodes if it was requested by the client. + */ + if (Vinifera_SpawnerActive && ScenExtension->IsUseMPAIBaseNodes) + { + _asm popad + JMP_REG(ecx, 0x004C1554); + } + + /** + * Continue checks. + */ + _asm popad + JMP_REG(ecx, 0x004C129D); +} + + +/** + * Patch to enable base nodes for the AI when UseMPAIBaseNodes=yes is set in the scenario. + * + * @author: ZivDero + */ +DECLARE_PATCH(_HouseClass_Can_Build_Here_MP_AI_BaseNodes_Patch) +{ + // Stolen instructions + _asm push edi + _asm mov edi, ecx + + /** + * Ignore AIBaseSpacing in Campaign. + */ + if (Session.Type == GAME_NORMAL) + { + // return 1; + JMP(0x004CB9D2); + } + + /** + * Also ignore AIBaseSpacing if it was requested by the client. + */ + if (ScenExtension->IsUseMPAIBaseNodes) + { + // return 1; + JMP(0x004CB9D2); + } + + /** + * Continue with AIBaseSpacing. + */ + JMP_REG(ecx, 0x004CB9DE); +} + + +/** + * Patch to enable base nodes for the AI when UseMPAIBaseNodes=yes is set in the scenario. + * + * @author: ZivDero + */ +DECLARE_PATCH(_HouseClass_Expert_AI_MP_AI_BaseNodes_Patch) +{ + _asm push eax + + if (Session.Type == GAME_NORMAL || ScenExtension->IsUseMPAIBaseNodes) + { + /** + * Skip trying to raise money. + */ + _asm pop eax + JMP_REG(ecx, 0x004C09AF); + + + } + + /** + * Potentially try to raise money + */ + _asm pop eax + JMP_REG(ecx, 0x004C08D1); +} + + /** * Main function for patching the hooks. */ @@ -716,16 +1662,50 @@ void HouseClassExtension_Hooks() Patch_Jump(0x004BBD26, &_HouseClass_Can_Build_BuildCheat_Patch); Patch_Jump(0x004BD30B, &_HouseClass_Super_Weapon_Handler_InstantRecharge_Patch); + Patch_Jump(0x004BBD26, &_HouseClass_Can_Build_BuildCheat_Patch); + + Patch_Jump(0x004BCD5D, &_HouseClass_AI_BuildConst_Patch); + Patch_Jump(0x004BCEE7, &_HouseClass_AI_Short_Game_BaseUnit_Patch); + Patch_Jump(0x004BCF3A, &_HouseClass_AI_Count_HarvesterUnit_Patch); + Patch_Jump(0x004BD0BC, &_HouseClass_AI_Is_Building_Harvester_Unit_Patch); + Patch_Jump(0x004C0D0C, &_HouseClass_AI_Raise_Money_HarvRef1); + Patch_Jump(0x004C0F5F, &_HouseClass_AI_Raise_Money_HarvRef2); + Patch_Jump(0x004C0FAB, &_HouseClass_AI_Raise_Money_HarvRef3); + Patch_Jump(0x004C1051, &_HouseClass_AI_Raise_Money_HarvRef4); + Patch_Jump(0x004C166D, &_HouseClass_AI_Unit_HarvRef1); + Patch_Jump(0x004C1710, &_HouseClass_AI_Unit_HarvRef2); + Patch_Jump(0x004C5977, &_HouseClass_Has_Prerequisites_BuildConst); + Patch_Jump(0x004C5E20, &_HouseClass_GenerateAIBuildList_4C5BB0_BuildConst); + Patch_Jump(0x004CA222, &_HouseClass_AI_Use_Super_Ion_Cannon_BuildConst); + Patch_Jump(0x004CA9A1, &_HouseClass_AI_Takeover_BuildConst); + Patch_Jump(0x004D7284, &_InfantryClass_What_Action_Harvester_Thief); Patch_Jump(0x004BE200, &HouseClassExt::_Begin_Production); Patch_Jump(0x004BE6A0, &HouseClassExt::_Abandon_Production); + Patch_Jump(0x004BAED0, &HouseClassExt::_Can_Make_Money); + Patch_Jump(0x004C0A40, &HouseClassExt::_Check_Raise_Money); + Patch_Jump(0x004C10E0, &HouseClassExt::_AI_Building); + Patch_Jump(0x004BF4C0, &HouseClassExt::_MPlayer_Defeated); + Patch_Jump(0x004BB460, &HouseClassExt::_Assign_Handicap); + Patch_Jump(0x004BDB50, &HouseClassExt::_Make_Ally); Patch_Jump(0x004CB777, &_HouseClass_ShouldDisableCameo_BuildLimit_Fix); Patch_Jump(0x004BC187, &_HouseClass_Can_Build_BuildLimit_Handle_Vehicle_Transform); Patch_Jump(0x004CB6C1, &_HouseClass_Enable_SWs_Check_For_Building_Power); - Patch_Jump(0x004C10E0, &HouseClassExt::_AI_Building); - Patch_Jump(0x004BAC2C, 0x004BAC39); // Patch a jump in the constructor to always allocate unit trackers + Patch_Jump(0x004BC077, 0x004BC082); // HouseClass::Can_Build, always check for ConYard of required Owner + + /** + * Patch away a few checks for GAME_INTERNET to enable statistics collection. + */ + Patch_Jump(0x004C220B, 0x004C2218); // HouseClass::Add_Tracking + Patch_Jump(0x004C2255, 0x004C2262); // HouseClass::Add_Tracking + Patch_Jump(0x004C229F, 0x004C22A8); // HouseClass::Add_Tracking + Patch_Jump(0x004C22E5, 0x004C22EE); // HouseClass::Add_Tracking + + Patch_Jump(0x004C128F, &_HouseClass_AI_Building_MP_AI_BaseNodes_Patch); + Patch_Jump(0x004CB9CD, &_HouseClass_Can_Build_Here_MP_AI_BaseNodes_Patch); + Patch_Jump(0x004C08C5, &_HouseClass_Expert_AI_MP_AI_BaseNodes_Patch); } diff --git a/src/extensions/housetype/housetypeext.cpp b/src/extensions/housetype/housetypeext.cpp index 0bebae9ad..04fc2ec19 100644 --- a/src/extensions/housetype/housetypeext.cpp +++ b/src/extensions/housetype/housetypeext.cpp @@ -39,7 +39,8 @@ * @author: CCHyper */ HouseTypeClassExtension::HouseTypeClassExtension(const HouseTypeClass *this_ptr) : - AbstractTypeClassExtension(this_ptr) + AbstractTypeClassExtension(this_ptr), + LoadingScreens { } { //if (this_ptr) EXT_DEBUG_TRACE("HouseTypeClassExtension::HouseTypeClassExtension - Name: %s (0x%08X)\n", Name(), (uintptr_t)(This())); @@ -53,7 +54,8 @@ HouseTypeClassExtension::HouseTypeClassExtension(const HouseTypeClass *this_ptr) * @author: CCHyper */ HouseTypeClassExtension::HouseTypeClassExtension(const NoInitClass &noinit) : - AbstractTypeClassExtension(noinit) + AbstractTypeClassExtension(noinit), + LoadingScreens { } { //EXT_DEBUG_TRACE("HouseTypeClassExtension::HouseTypeClassExtension(NoInitClass) - Name: %s (0x%08X)\n", Name(), (uintptr_t)(This())); } @@ -100,12 +102,23 @@ HRESULT HouseTypeClassExtension::Load(IStream *pStm) { //EXT_DEBUG_TRACE("HouseTypeClassExtension::Load - Name: %s (0x%08X)\n", Name(), (uintptr_t)(This())); + LoadingScreens[0].Clear(); + LoadingScreens[1].Clear(); + LoadingScreens[2].Clear(); + HRESULT hr = AbstractTypeClassExtension::Load(pStm); if (FAILED(hr)) { return E_FAIL; } new (this) HouseTypeClassExtension(NoInitClass()); + + /** + * We don't need loading screens during the game so we don't bother saving and loading them. + */ + new (&LoadingScreens[0]) DynamicVectorClass(); + new (&LoadingScreens[1]) DynamicVectorClass(); + new (&LoadingScreens[2]) DynamicVectorClass(); return hr; } @@ -173,16 +186,54 @@ bool HouseTypeClassExtension::Read_INI(CCINIClass &ini) { //EXT_DEBUG_TRACE("HouseTypeClassExtension::Read_INI - Name: %s (0x%08X)\n", Name(), (uintptr_t)(This())); + const char* ini_name = Name(); + + if (!IsInitialized) { + + char buffer[12]; + + LoadingScreens[0].Clear(); + LoadingScreens[1].Clear(); + LoadingScreens[2].Clear(); + + for (int i = 0; i < 2; i++) + { + const char letter_count = 26; + char letter; + + if (This()->House == HOUSE_GDI) { + letter = 'C' + i; + } + else if (This()->House == HOUSE_NOD) { + letter = 'A' + i; + } + else { + letter = 'A' + ((static_cast(This()->House) * 2) % letter_count) + i; + } + + std::sprintf(buffer, "LOAD400%c", letter); + LoadingScreens[0].Add(buffer); + + std::sprintf(buffer, "LOAD480%c", letter); + LoadingScreens[1].Add(buffer); + + std::sprintf(buffer, "LOAD600%c", letter); + LoadingScreens[2].Add(buffer); + } + } + if (!AbstractTypeClassExtension::Read_INI(ini)) { return false; } - const char *ini_name = Name(); - if (!ini.Is_Present(ini_name)) { return false; } + LoadingScreens[0] = ini.Get_Strings(ini_name, "LoadingScreens400", LoadingScreens[0]); + LoadingScreens[1] = ini.Get_Strings(ini_name, "LoadingScreens480", LoadingScreens[1]); + LoadingScreens[2] = ini.Get_Strings(ini_name, "LoadingScreens600", LoadingScreens[2]); + IsInitialized = true; return true; diff --git a/src/extensions/housetype/housetypeext.h b/src/extensions/housetype/housetypeext.h index cd6883586..0e3119c13 100644 --- a/src/extensions/housetype/housetypeext.h +++ b/src/extensions/housetype/housetypeext.h @@ -29,37 +29,40 @@ #include "abstracttypeext.h" #include "housetype.h" +#include "wstring.h" class DECLSPEC_UUID(UUID_HOUSETYPE_EXTENSION) HouseTypeClassExtension final : public AbstractTypeClassExtension { - public: - /** - * IPersist - */ - IFACEMETHOD(GetClassID)(CLSID *pClassID); +public: + /** + * IPersist + */ + IFACEMETHOD(GetClassID)(CLSID *pClassID); - /** - * IPersistStream - */ - IFACEMETHOD(Load)(IStream *pStm); - IFACEMETHOD(Save)(IStream *pStm, BOOL fClearDirty); + /** + * IPersistStream + */ + IFACEMETHOD(Load)(IStream *pStm); + IFACEMETHOD(Save)(IStream *pStm, BOOL fClearDirty); - public: - HouseTypeClassExtension(const HouseTypeClass *this_ptr = nullptr); - HouseTypeClassExtension(const NoInitClass &noinit); - virtual ~HouseTypeClassExtension(); +public: + HouseTypeClassExtension(const HouseTypeClass *this_ptr = nullptr); + HouseTypeClassExtension(const NoInitClass &noinit); + virtual ~HouseTypeClassExtension(); - virtual int Size_Of() const override; - virtual void Detach(TARGET target, bool all = true) override; - virtual void Compute_CRC(WWCRCEngine &crc) const override; - - virtual HouseTypeClass *This() const override { return reinterpret_cast(AbstractTypeClassExtension::This()); } - virtual const HouseTypeClass *This_Const() const override { return reinterpret_cast(AbstractTypeClassExtension::This_Const()); } - virtual RTTIType What_Am_I() const override { return RTTI_HOUSETYPE; } + virtual int Size_Of() const override; + virtual void Detach(TARGET target, bool all = true) override; + virtual void Compute_CRC(WWCRCEngine &crc) const override; + + virtual HouseTypeClass *This() const override { return reinterpret_cast(AbstractTypeClassExtension::This()); } + virtual const HouseTypeClass *This_Const() const override { return reinterpret_cast(AbstractTypeClassExtension::This_Const()); } + virtual RTTIType What_Am_I() const override { return RTTI_HOUSETYPE; } - virtual bool Read_INI(CCINIClass &ini) override; + virtual bool Read_INI(CCINIClass &ini) override; - public: +public: + + DynamicVectorClass LoadingScreens[3]; }; diff --git a/src/extensions/init/initext_hooks.cpp b/src/extensions/init/initext_hooks.cpp index 5f9fde3ba..60e09f79a 100644 --- a/src/extensions/init/initext_hooks.cpp +++ b/src/extensions/init/initext_hooks.cpp @@ -40,6 +40,8 @@ #include "theme.h" #include "session.h" #include "iomap.h" +#include "house.h" +#include "housetype.h" #include "dsaudio.h" #include "vinifera_gitinfo.h" #include "tspp_gitinfo.h" @@ -49,8 +51,10 @@ #include #include +#include "extension.h" #include "hooker.h" #include "hooker_macros.h" +#include "sideext.h" extern HMODULE DLLInstance; @@ -63,6 +67,44 @@ extern HMODULE DLLInstance; #define TS_MAINCURSOR 104 +/** + * #issue-218 + * + * We abuse SessionClass::IsGDI in this patch to store the current players + * HouseType so it can be used to fetch the SideType from it for loading + * the assets. This also means this bugfix works without extending any of + * the games classes. + * + * @warning: This does mean we are limited to 255 unique houses (oh no!). + *) + * @author: CCHyper + */ +static void Set_Session_House() { reinterpret_cast(Session.IsGDI) = static_cast(Session.Players.Fetch_Head()->Player.House) & 0xFF; } +DECLARE_PATCH(_Select_Game_PreStart_SetPlayerHouse_Patch) +{ + /** + * This patch removes the code that sets the "IsGDI" member of SessionClass + * bool based on if the house name matched "GDI" or not and stores + * the player HouseType directly. + */ +#if 0 + /** + * Original game code. + */ + static HouseTypeClass *housetype; + housetype = HouseTypes[Session.Players.Fetch_Head()->Player.House & 0xFF]; + Session.IsGDI = strcmpi("GDI", housetype->Name()) == 0; +#endif + + /** + * Accessing unions trashes the stack, so this operation is wrapped. + */ + Set_Session_House(); + + JMP(0x004E2D13); +} + + /** * #issue-305 * @@ -492,13 +534,13 @@ void Vinifera_Create_Main_Window(HINSTANCE hInstance, int nCmdShow, int width, i */ bool Vinifera_Prep_For_Side(SideType side) { - DEBUG_INFO("Preparing Mixfiles for Side %02d.\n", side); + int sidenum = (side+1); // Logical side number. + + DEBUG_INFO("Preparing Mixfiles for Side %02d (logical %02d).\n", side, sidenum); MFCC *mix = nullptr; char buffer[16]; - int sidenum = (side+1); // Logical side number. - if (SideCachedMix) { DEBUG_INFO(" Releasing %s\n", SideCachedMix->Filename); delete SideCachedMix; @@ -556,6 +598,8 @@ bool Vinifera_Prep_For_Side(SideType side) return false; } DEBUG_INFO(" %s\n", buffer); + } else { + DEBUG_WARNING(" Failed to find %s!\n", buffer); } std::snprintf(buffer, sizeof(buffer), "SIDENC%02d.MIX", sidenum); @@ -567,6 +611,8 @@ bool Vinifera_Prep_For_Side(SideType side) //return false; // #issue-193: Unable to load side mix files is no longer a fatal error. } DEBUG_INFO(" %s\n", buffer); + } else { + DEBUG_WARNING(" Failed to find %s!\n", buffer); } if (Session.Type == GAME_NORMAL) { @@ -583,11 +629,21 @@ bool Vinifera_Prep_For_Side(SideType side) //return false; // #issue-193: Unable to load side mix files is no longer a fatal error. } DEBUG_INFO(" %s\n", buffer); + } else { + DEBUG_WARNING(" Failed to find %s!\n", buffer); } } Map.Init_For_House(); + /** + * Set the options menu color. + */ + static auto& OptionsColor = Make_Global(0x00808B7C); + const SideClassExtension* sideext = Extension::Fetch(Sides[static_cast(Scen->IsGDI)]); + const RGBStruct& options_rgb = sideext->OptionsColor; + OptionsColor = RGB(options_rgb.R, options_rgb.G, options_rgb.B); + return true; } @@ -1029,6 +1085,8 @@ void GameInit_Hooks() /** * TS Client file structure assumes Firestorm is always installed and enabled. */ - //Patch_Jump(0x00407050, &Vinifera_Detect_Addons); + Patch_Jump(0x00407050, &Vinifera_Detect_Addons); #endif + + Patch_Jump(0x004E2CE4, &_Select_Game_PreStart_SetPlayerHouse_Patch); } diff --git a/src/extensions/mainloop/mainloopext_hooks.cpp b/src/extensions/mainloop/mainloopext_hooks.cpp index f2ec9298b..35d25aa0c 100644 --- a/src/extensions/mainloop/mainloopext_hooks.cpp +++ b/src/extensions/mainloop/mainloopext_hooks.cpp @@ -40,10 +40,16 @@ #include "ccini.h" #include "fatal.h" #include "debughandler.h" -#include "asserthandler.h"6 +#include "asserthandler.h" +#include "extension_globals.h" +#include "session.h" #include "hooker.h" #include "hooker_macros.h" +#include "optionsext.h" +#include "saveload.h" +#include "spawner.h" +#include "textprint.h" /** @@ -80,6 +86,31 @@ static void Before_Main_Loop() } +/** + * Prints a message that there's an autosave happening. + * + * @author: ZivDero + */ +void Print_Saving_Game_Message() +{ + /** + * Calculate the message delay. + */ + const int message_delay = Rule->MessageDelay * TICKS_PER_MINUTE; + + /** + * Send the message. + */ + Session.Messages.Add_Message(nullptr, 0, "Saving game...", static_cast(4), TPF_6PT_GRAD | TPF_USE_GRAD_PAL | TPF_FULLSHADOW, message_delay); + + /** + * Force a redraw so that our message gets printed. + */ + Map.Flag_To_Redraw(2); + Map.Render(); +} + + static void After_Main_Loop() { /** @@ -182,6 +213,85 @@ static void After_Main_Loop() */ Vinifera_Developer_IsToReloadRules = false; } + + const bool do_campaign_autosaves = Session.Type == GAME_NORMAL && OptionsExtension->AutoSaveCount > 0 && OptionsExtension->AutoSaveInterval > 0; + const bool do_mp_autosaves = Vinifera_SpawnerActive && Session.Type == GAME_IPX && Vinifera_SpawnerConfig->AutoSaveInterval > 0; + + /** + * Schedule to make a save if it's time to autosave. + */ + if (do_campaign_autosaves || do_mp_autosaves) { + if (Frame == Vinifera_NextAutoSaveFrame) { + Vinifera_DoSave = true; + } + } + + if (Vinifera_DoSave) { + + Print_Saving_Game_Message(); + + /** + * Campaign autosave. + */ + if (Session.Type == GAME_NORMAL) { + + static char save_filename[32]; + static char save_description[32]; + + /** + * Prepare the save name and description. + */ + std::sprintf(save_filename, "AUTOSAVE%d.SAV", Vinifera_NextAutoSaveNumber + 1); + std::sprintf(save_description, "Mission Auto-Save (Slot %d)", Vinifera_NextAutoSaveNumber + 1); + + /** + * Pause the mission timer. + */ + Pause_Scenario_Timer(); + Call_Back(); + + /** + * Save! + */ + Save_Game(save_filename, save_description); + + /** + * Unpause the mission timer. + */ + Resume_Scenario_Timer(); + + /** + * Increment the autosave number. + */ + Vinifera_NextAutoSaveNumber = (Vinifera_NextAutoSaveNumber + 1) % OptionsExtension->AutoSaveCount; + + /** + * Schedule the next autosave. + */ + Vinifera_NextAutoSaveFrame = Frame + OptionsExtension->AutoSaveInterval; + } + else if (Session.Type == GAME_IPX) { + + /** + * We do it by ourselves here instead of letting original Westwood code save when + * the event is executed, because saving mid-frame before Remove_All_Inactive() + * has been called can lead to save corruption + * In other words, by doing it here we fix a Westwood bug/oversight + */ + + /** + * Save! + */ + Save_Game("SAVEGAME.NET", "Multiplayer Game"); + + /** + * Schedule the next autosave. + */ + Vinifera_NextAutoSaveFrame = Frame + Vinifera_SpawnerConfig->AutoSaveInterval; + } + + Vinifera_DoSave = false; + } } diff --git a/src/extensions/movie/playmovie_hooks.cpp b/src/extensions/movie/playmovie_hooks.cpp index 6c52bd0f0..6ecaf47a8 100644 --- a/src/extensions/movie/playmovie_hooks.cpp +++ b/src/extensions/movie/playmovie_hooks.cpp @@ -152,115 +152,6 @@ DECLARE_PATCH(_Play_Movie_Scale_By_Ratio_Patch) } -/** - * #issue-95 - * - * Patch for handling the campaign intro movies - * for "The First Decade" and "Freeware TS" installations. - * - * @author: CCHyper - */ -static bool Play_Intro_Movie(CampaignType campaign_id) -{ - /** - * Catch any cases where we might be starting a non-campaign scenario. - */ - if (campaign_id == CAMPAIGN_NONE) { - return false; - } - - if (Scen->Scenario != 1) { - return false; - } - - char movie_filename[32]; - VQType intro_vq = VQ_NONE; - - /** - * Fetch the campaign disk id. - */ - CampaignClass *campaign = Campaigns[campaign_id]; - DiskID cd_num = campaign->WhichCD; - - /** - * Check if the current campaign is an original GDI or NOD campaign. - */ - bool is_original_gdi = (cd_num == DISK_GDI && (Wstring(campaign->IniName) == "GDI1" || Wstring(campaign->IniName) == "GDI1A") && Wstring(campaign->Scenario) == "GDI1A.MAP"); - bool is_original_nod = (cd_num == DISK_NOD && (Wstring(campaign->IniName) == "NOD1" || Wstring(campaign->IniName) == "NOD1A") && Wstring(campaign->Scenario) == "NOD1A.MAP"); - - /** - * #issue-762 - * - * Fetch the campaign extension (if available) and get the custom intro movie. - * - * @author: CCHyper - */ - CampaignClassExtension *campaignext = Extension::Fetch(campaign); - if (campaignext->IntroMovie[0] != '\0') { - std::snprintf(movie_filename, sizeof(movie_filename), "%s.VQA", campaignext->IntroMovie); - DEBUG_INFO("About to play \"%s\".\n", movie_filename); - Play_Movie(movie_filename); - - /** - * If this is an original Tiberian Sun campaign, play the respective intro movie. - */ - } else if (is_original_gdi || is_original_nod) { - - /** - * "The First Decade" and "Freeware TS" installations reshuffle - * the movie files due to all mix files being local now and a - * primitive "no-cd" added; - * - * MOVIES01.MIX -> INTRO.VQA (GDI) is now INTR0.VQA - * MOVIES02.MIX -> INTRO.VQA (NOD) is now INTR1.VQA - * - * Build the movie filename based on the current campaigns desired CD (see DiskID enum). - */ - std::snprintf(movie_filename, sizeof(movie_filename), "INTR%d.VQA", cd_num); - - /** - * Now play the movie if it is found, falling back to original behavior otherwise. - */ - if (CCFileClass(movie_filename).Is_Available()) { - DEBUG_INFO("About to play \"%s\".\n", movie_filename); - Play_Movie(movie_filename); - - } else if (CCFileClass("INTRO.VQA").Is_Available()) { - DEBUG_INFO("About to play \"INTRO.VQA\".\n"); - Play_Movie("INTRO.VQA"); - - } else { - DEBUG_WARNING("Failed to find Intro movie!\n"); - return false; - } - - } else { - DEBUG_WARNING("No campaign intro movie defined.\n"); - } - - return true; -} - -DECLARE_PATCH(_Start_Scenario_Intro_Movie_Patch) -{ - GET_REGISTER_STATIC(CampaignType, campaign_id, ebx); - GET_REGISTER_STATIC(char *, name, ebp); - - Play_Intro_Movie(campaign_id); - -read_scenario: - //JMP(0x005DB319); - - /** - * The First Decade" and "Freeware TS" EXE's actually have patched code at - * the address 0x005DB319, so lets handle the debug log print ourself and - * jump back at a safe location. - */ - DEBUG_GAME("Reading scenario: %s\n", name); - JMP(0x005DB327); -} - - /** * #issue-95 * @@ -347,7 +238,6 @@ DECLARE_PATCH(_Select_Game_Intro_SneakPeak_Movies_Patch) */ void PlayMovieExtension_Hooks() { - Patch_Jump(0x005DB2DE, &_Start_Scenario_Intro_Movie_Patch); Patch_Jump(0x004E2796, &_Select_Game_Intro_SneakPeak_Movies_Patch); /** diff --git a/src/extensions/multiscore/multiscoreext_hooks.cpp b/src/extensions/multiscore/multiscoreext_hooks.cpp index b382a7946..d92b7a3e2 100644 --- a/src/extensions/multiscore/multiscoreext_hooks.cpp +++ b/src/extensions/multiscore/multiscoreext_hooks.cpp @@ -28,6 +28,7 @@ #include "multiscoreext_hooks.h" #include "debughandler.h" #include "asserthandler.h" +#include "extension.h" #include "tibsun_globals.h" #include "house.h" @@ -35,86 +36,231 @@ #include "hooker.h" #include "hooker_macros.h" +#include "houseext.h" +#include "housetype.h" +#include "multiscore.h" #include "scenario.h" +#include "session.h" #include "vinifera_globals.h" -static int MostCreditsSpent; - -static void _MultiScore_Tally_Score_Get_Largest_CreditsSpent_Score() +class HouseClassExtension; +/** + * A fake class for implementing new member functions which allow + * access to the "this" pointer of the intended class. + * + * @note: This must not contain a constructor or destructor! + * @note: All functions must be prefixed with "_" to prevent accidental virtualization. + */ +class MultiScoreExt final : public MultiScore { - MostCreditsSpent = 0; - - for (int i = 0; i < Houses.Count(); i++) { - if (Houses[i]->CreditsSpent > MostCreditsSpent) { - MostCreditsSpent = Houses[i]->CreditsSpent; - } - } -} +public: + void _Tally_Score(); +}; -/** - * #issue-544 - * - * Fixes the nonsensical economy stat in the score screen. - * Records the highest "credits spent" score from all players for - * later use in comparing players' economy scores. - * - * @author: Rampastring - */ -DECLARE_PATCH(_MultiScore_Tally_Score_Fetch_Largest_CreditsSpent_Score) +void MultiScoreExt::_Tally_Score() { - _MultiScore_Tally_Score_Get_Largest_CreditsSpent_Score(); + /** + * Reset the score entry count. + */ + Session.NumScores = 0; /** - * Stolen bytes / code. + * Find which player has spent the most credits. */ - _asm { mov ecx, dword ptr ds:0x007E1568 } - JMP(0x005687AF); -} + int most_credits_spent = 0; + for (HousesType house = HOUSE_FIRST; house < Houses.Count(); house++) { + HouseClass* hptr = Houses[house]; + const HouseClassExtension* hext = hptr ? Extension::Fetch(hptr) : nullptr; -/** - * #issue-544 - * - * Fixes the nonsensical economy stat in the score screen. - * Calculates a player's economy score based on their amount of - * credits spent. - * - * @author: Rampastring - */ -DECLARE_PATCH(_MultiScore_Tally_Score_Calculate_Economy_Score) -{ - GET_REGISTER_STATIC(HouseClass *, house, ebx); - static int economy_score; + /** + * Skip this house if it's multiplay passive, or is an observer. + */ + if (!hptr || hptr->Class->IsMultiplayPassive || hext->IsObserver) { + continue; + } + + if (Houses[house]->CreditsSpent > most_credits_spent) { + most_credits_spent = Houses[house]->CreditsSpent; + } + } - /* - * Calculate a percentage of how many credits this house has - * spent compared to the house that spent the highest - * amount of credits during the match. + /** + * Loop through all houses, tallying up each player's score. */ - if (MostCreditsSpent > 0) { - if (house->CreditsSpent >= MostCreditsSpent) { + for (HousesType house = HOUSE_FIRST; house < Houses.Count(); house++) { + HouseClass* hptr = Houses[house]; + const HouseClassExtension* hext = hptr ? Extension::Fetch(hptr) : nullptr; + + /** + * Skip this house if it's multiplay passive, or is an observer. + */ + if (!hptr || hptr->Class->IsMultiplayPassive || hext->IsObserver) { + continue; + } + + /** + * Now find out where this player is in the score array. + */ + const int score_index = Session.NumScores++; + + /** + * Initialize this score entry. + */ + Session.Score[score_index].Wins = 0; + std::strncpy(Session.Score[score_index].Name, hptr->IniName, std::size(Session.Score[score_index].Name) - 1); + + /** + * Init this player's statistics to 0 (-1 means he didn't play this round; + * 0 means he played but did nothing). + */ + Session.Score[score_index].Lost[0] = 0; + Session.Score[score_index].Kills[0] = 0; + Session.Score[score_index].Economy[0] = 0; + Session.Score[score_index].Score[0] = 0; + + /** + * Init this player's color to his last-used color index + */ + Session.Score[score_index].Color = static_cast(hptr->RemapColor); + + /** + * If this house was undefeated, it must have been the winner. + * (If no human houses are undefeated, the computer won.) + */ + if (!hptr->IsDefeated) { + Session.Score[score_index].Wins++; + Session.Winner = score_index; + /** - * For some reason the score screen presentation seems to - * lower this by some 1-2%, so we take that into account. - * TODO investigate and fix + * Calculate the average score for all other houses and use it as a basseline, I guess? Score inflation. */ - economy_score = 102; + int score = 0; + int count = 0; + + for (HousesType house2 = HOUSE_FIRST; house2 < Houses.Count(); house2++) { + const HouseClass* hptr2 = Houses[house2]; + const HouseClassExtension* hext2 = hptr2 ? Extension::Fetch(hptr2) : nullptr; + + /** + * Skip this house if it's the same house, is multiplay passive, or is an observer. + */ + if (!hptr2 || hptr2->Class->IsMultiplayPassive || hptr == hptr2 || hext2->IsObserver) { + continue; + } + + score += hptr->PointTotal; + count++; + } + + /** + * Average the scores. + */ + if (count > 0) { + score /= count; + } + + score = std::max(200, score); + Session.Score[score_index].Score[0] = score / 2; + } + + /** + * Tally up all kills for this player. + */ + unsigned total_kills = 0; + for (int i = 0; i < std::size(hptr->UnitsKilled); i++) { + total_kills += hptr->UnitsKilled[i]; + } + + for (int i = 0; i < std::size(hptr->BuildingsKilled); i++) { + total_kills += hptr->BuildingsKilled[i]; + } + + Session.Score[score_index].Kills[0] = total_kills; + + /** + * Tally up the losses for this player. + */ + const int total_losses = hptr->UnitsLost + hptr->BuildingsLost; + Session.Score[score_index].Lost[0] = total_losses; + + /** + * Calculate the kill to loss ratio. + */ + double kill_ratio = 0.0; + if (total_losses > 0) { + kill_ratio = static_cast(total_kills) / total_losses; + } + + /** + * Original economy score calculation. Ratio of currently owned objects to total built objects. + */ +#if 0 + int total_owned = hptr->ActiveBQuantity.Total(); + total_owned += hptr->ActiveUQuantity.Total(); + total_owned += hptr->ActiveIQuantity.Total(); + total_owned += hptr->ActiveAQuantity.Total(); + + double build_economy; + int total_built = total_owned + hptr->BuildingsLost + hptr->UnitsLost; + if (total_built <= 0) { + build_economy = 0.0; } else { - economy_score = (house->CreditsSpent * 100) / MostCreditsSpent; + build_economy = total_owned / total_built; } - } else { - economy_score = 0; - } + build_economy = std::max(0.0, build_economy); + Session.Score[score_index].Economy[0] = (build_economy * 100.0); +#endif + /* + * Calculate a percentage of how many credits this house has + * spent compared to the house that spent the highest + * amount of credits during the match. + * + * @author: Rampastring + */ + double build_economy; + if (most_credits_spent > 0) { + build_economy = static_cast(hptr->CreditsSpent) / most_credits_spent; + } + else { + build_economy = 0; + } + Session.Score[score_index].Economy[0] = (build_economy * 100.0); - _asm { mov eax, [economy_score] }; + /** + * A score of 100 prints as 99 for some reason, so we do this to make it print 100. + */ + if (Session.Score[score_index].Economy[0] == 100) { + Session.Score[score_index].Economy[0] = 102; + } - /** - * Assign economy score and continue score processing. - */ - JMP_REG(ecx, 0x005689E0); + /** + * Set the player's score. + */ + if (hptr->PointTotal > 0) { + Session.Score[score_index].Score[0] += hptr->PointTotal; + } + + /** + * Print if this player is a winner or a loser. + */ + const char* win_string = Session.Score[score_index].Wins > 0 ? "Winner" : "Loser"; + + DEBUG_INFO( + "%s: %s\n Scheme: %d\n Lost = %d\n Kills = %d\n Economy = %d\n Score = %d\n", + Session.Score[score_index].Name, + win_string, + Session.Score[score_index].Color, + Session.Score[score_index].Lost[0], + Session.Score[score_index].Kills[0], + Session.Score[score_index].Economy[0], + Session.Score[score_index].Score[0]); + + DEBUG_INFO(" KillRatio = %f\n BuildEconomy = %f\n", kill_ratio, build_economy); + } } @@ -139,17 +285,6 @@ DECLARE_PATCH(_MultiScore_568BE0_ElapsedTime_Patch) */ void MultiScoreExtension_Hooks() { - Patch_Jump(0x005687A9, &_MultiScore_Tally_Score_Fetch_Largest_CreditsSpent_Score); - Patch_Jump(0x005689D5, &_MultiScore_Tally_Score_Calculate_Economy_Score); Patch_Jump(0x00568D10, &_MultiScore_568BE0_ElapsedTime_Patch); - - /** - * #issue-187 - * - * Fixes incorrect spelling of "Loser" on the multiplayer score screen debug output. - * - * @author: CCHyper - */ - static const char *TEXT_LOSER = "Loser"; - Patch_Dword(0x00568A05+1, (uintptr_t)TEXT_LOSER); // +1 skips "mov eax," opcode + Patch_Jump(0x005687A0, &MultiScoreExt::_Tally_Score); } diff --git a/src/extensions/options/optionsext.cpp b/src/extensions/options/optionsext.cpp index 3f0248bbd..129a036fc 100644 --- a/src/extensions/options/optionsext.cpp +++ b/src/extensions/options/optionsext.cpp @@ -45,7 +45,9 @@ OptionsClassExtension::OptionsClassExtension(const OptionsClass *this_ptr) : GlobalExtensionClass(this_ptr), SortDefensesAsLast(true), - FilterBandBoxSelection(true) + FilterBandBoxSelection(true), + AutoSaveCount(5), + AutoSaveInterval(7200) { //EXT_DEBUG_TRACE("OptionsClassExtension::OptionsClassExtension - 0x%08X\n", (uintptr_t)(This())); } @@ -165,6 +167,8 @@ void OptionsClassExtension::Load_Settings() SortDefensesAsLast = sun_ini.Get_Bool("Options", "SortDefensesAsLast", SortDefensesAsLast); FilterBandBoxSelection = sun_ini.Get_Bool("Options", "FilterBandBoxSelection", FilterBandBoxSelection); + AutoSaveCount = sun_ini.Get_Int("Options", "AutoSaveCount", AutoSaveCount); + AutoSaveInterval = sun_ini.Get_Int("Options", "AutoSaveInterval", AutoSaveInterval); } /** diff --git a/src/extensions/options/optionsext.h b/src/extensions/options/optionsext.h index 3c478aabb..466f0568b 100644 --- a/src/extensions/options/optionsext.h +++ b/src/extensions/options/optionsext.h @@ -37,41 +37,51 @@ class CCINIClass; class OptionsClassExtension final : public GlobalExtensionClass { - public: - IFACEMETHOD(Load)(IStream *pStm); - IFACEMETHOD(Save)(IStream *pStm, BOOL fClearDirty); +public: + IFACEMETHOD(Load)(IStream *pStm); + IFACEMETHOD(Save)(IStream *pStm, BOOL fClearDirty); - public: - OptionsClassExtension(const OptionsClass *this_ptr); - OptionsClassExtension(const NoInitClass &noinit); - virtual ~OptionsClassExtension(); +public: + OptionsClassExtension(const OptionsClass *this_ptr); + OptionsClassExtension(const NoInitClass &noinit); + virtual ~OptionsClassExtension(); - /** - * OptionsClass extension does not require these to be used, but we - * implement them for completeness. - */ - virtual int Size_Of() const override; - virtual void Detach(TARGET target, bool all = true) override; - virtual void Compute_CRC(WWCRCEngine &crc) const override; + /** + * OptionsClass extension does not require these to be used, but we + * implement them for completeness. + */ + virtual int Size_Of() const override; + virtual void Detach(TARGET target, bool all = true) override; + virtual void Compute_CRC(WWCRCEngine &crc) const override; - virtual const char *Name() const override { return "Options"; } - virtual const char *Full_Name() const override { return "Options"; } + virtual const char *Name() const override { return "Options"; } + virtual const char *Full_Name() const override { return "Options"; } - void Load_Settings(); - void Load_Init_Settings(); - void Save_Settings(); + void Load_Settings(); + void Load_Init_Settings(); + void Save_Settings(); - void Set(); + void Set(); - public: +public: - /** - * Should cameos of defenses (including walls and gates) be sorted to the bottom of the sidebar? - */ - bool SortDefensesAsLast; + /** + * Should cameos of defenses (including walls and gates) be sorted to the bottom of the sidebar? + */ + bool SortDefensesAsLast; - /** - * Are harvesters and MCVs excluded from a band-box selection that includes combat units? - */ - bool FilterBandBoxSelection; + /** + * Are harvesters and MCVs excluded from a band-box selection that includes combat units? + */ + bool FilterBandBoxSelection; + + /** + * Number of autosaves to make in skirmish. + */ + int AutoSaveCount; + + /** + * The delay between autosaves in skirmish in frames. + */ + int AutoSaveInterval; }; diff --git a/src/extensions/rules/rulesext.cpp b/src/extensions/rules/rulesext.cpp index 5b8fa1212..0783d133f 100644 --- a/src/extensions/rules/rulesext.cpp +++ b/src/extensions/rules/rulesext.cpp @@ -82,7 +82,12 @@ RulesClassExtension::RulesClassExtension(const RulesClass *this_ptr) : IsShowSuperWeaponTimers(true), IceStrength(0), WeedPipIndex(1), - MaxFreeRefineryDistanceBias(16) + MaxFreeRefineryDistanceBias(16), + BaseUnit(), + UpgradeVeteranSound(VOC_NONE), + UpgradeEliteSound(VOC_NONE), + VoxUnitPromoted(VOX_NONE), + EliteFlashTimer(0) { //if (this_ptr) EXT_DEBUG_TRACE("RulesClassExtension::RulesClassExtension - 0x%08X\n", (uintptr_t)(ThisPtr)); @@ -114,7 +119,8 @@ RulesClassExtension::RulesClassExtension(const RulesClass *this_ptr) : */ RulesClassExtension::RulesClassExtension(const NoInitClass &noinit) : GlobalExtensionClass(noinit), - MaxPips(noinit) + MaxPips(noinit), + BaseUnit(noinit) { //EXT_DEBUG_TRACE("RulesClassExtension::RulesClassExtension(NoInitClass) - 0x%08X\n", (uintptr_t)(ThisPtr)); } @@ -141,6 +147,7 @@ HRESULT RulesClassExtension::Load(IStream *pStm) //EXT_DEBUG_TRACE("RulesClassExtension::Load - 0x%08X\n", (uintptr_t)(This())); MaxPips.Clear(); + BaseUnit.Clear(); HRESULT hr = GlobalExtensionClass::Load(pStm); if (FAILED(hr)) { @@ -150,6 +157,9 @@ HRESULT RulesClassExtension::Load(IStream *pStm) new (this) RulesClassExtension(NoInitClass()); MaxPips.Load(pStm); + BaseUnit.Load(pStm); + + VINIFERA_SWIZZLE_REQUEST_POINTER_REMAP_LIST(BaseUnit, "BaseUnit"); return hr; } @@ -170,6 +180,7 @@ HRESULT RulesClassExtension::Save(IStream *pStm, BOOL fClearDirty) } MaxPips.Save(pStm); + BaseUnit.Save(pStm); return hr; } @@ -196,6 +207,10 @@ int RulesClassExtension::Size_Of() const void RulesClassExtension::Detach(TARGET target, bool all) { //EXT_DEBUG_TRACE("RulesClassExtension::Detach - 0x%08X\n", (uintptr_t)(This())); + + if (target->What_Am_I() == RTTI_UNITTYPE) { + BaseUnit.Delete(reinterpret_cast(target)); + } } @@ -214,6 +229,7 @@ void RulesClassExtension::Compute_CRC(WWCRCEngine &crc) const crc(IsShowSuperWeaponTimers); crc(IceStrength); crc(MaxFreeRefineryDistanceBias); + crc(BaseUnit.Count()); } @@ -321,7 +337,8 @@ void RulesClassExtension::Process(CCINIClass &ini) */ Objects(ini); - This()->Difficulty(ini); + //This()->Difficulty(ini); + Difficulty(ini); This()->CrateRules(ini); This()->CombatDamage(ini); This()->AudioVisual(ini); @@ -612,6 +629,13 @@ bool RulesClassExtension::General(CCINIClass &ini) This()->EngineerDamage = ini.Get_Float(GENERAL, "EngineerDamage", This()->EngineerDamage); MaxFreeRefineryDistanceBias = ini.Get_Int(GENERAL, "MaxFreeRefineryDistanceBias", MaxFreeRefineryDistanceBias); + /** + * Reload the BaseUnit entry and store the value in the new class extension. + * This allows us to expand the original BaseUnit logic without impacting + * the original behaviour of BaseUnit. + */ + BaseUnit = ini.Get_Units(GENERAL, "BaseUnit", BaseUnit); + return true; } @@ -638,6 +662,11 @@ bool RulesClassExtension::AudioVisual(CCINIClass &ini) for (int i = 0; i < MaxPips.Count(); i++) DEBUG_INFO("%d", MaxPips[i]); + UpgradeVeteranSound = ini.Get_VocType(AUDIOVISUAL, "UpgradeVeteranSound", UpgradeVeteranSound); + UpgradeEliteSound = ini.Get_VocType(AUDIOVISUAL, "UpgradeEliteSound", UpgradeEliteSound); + VoxUnitPromoted = ini.Get_VoxType(AUDIOVISUAL, "VoxUnitPromoted", VoxUnitPromoted); + EliteFlashTimer = ini.Get_Int(AUDIOVISUAL, "EliteFlashTimer", EliteFlashTimer); + return true; } @@ -847,6 +876,24 @@ bool RulesClassExtension::Tiberiums(CCINIClass &ini) } +/** + * Reads the difficulty settings from the INI file. + * + * @author: ZivDero + */ +bool RulesClassExtension::Difficulty(CCINIClass& ini) +{ + Difficulty_Get(ini, Diff[DIFF_EASY], "Easy"); + Difficulty_Get(ini, Diff[DIFF_NORMAL], "Normal"); + Difficulty_Get(ini, Diff[DIFF_HARD], "Difficult"); + Difficulty_Get(ini, Diff[DIFF_VERY_EASY], "VeryEasy"); + Difficulty_Get(ini, Diff[DIFF_EXTREMELY_EASY], "ExtremelyEasy"); + Difficulty_Get(ini, DiffHuman, "HumanNormal"); + + return true; +} + + /** * Performs checks on rules data to ensure values are as expected. * @@ -949,9 +996,9 @@ void RulesClassExtension::Fixups(CCINIClass &ini) * Workaround because NOD has Side=GDI and Prefix=B in unmodded Tiberian Sun. * * Match criteria; - * - Are we currently processing RuleINI? + * - Are we currently processing one of the unmodified rule INI's? */ - if (is_ruleini) { + if (rule_unmodified || fsrule_unmodified) { /** * Ensure at least two HouseTypes are defined before performing this fixup case. diff --git a/src/extensions/rules/rulesext.h b/src/extensions/rules/rulesext.h index a82e9ec90..17fe3b1f5 100644 --- a/src/extensions/rules/rulesext.h +++ b/src/extensions/rules/rulesext.h @@ -32,6 +32,7 @@ #include "rules.h" #include "extension.h" #include "tpoint.h" +#include "typelist.h" class CCINIClass; @@ -39,82 +40,117 @@ class CCINIClass; class RulesClassExtension final : public GlobalExtensionClass { - public: - IFACEMETHOD(Load)(IStream *pStm); - IFACEMETHOD(Save)(IStream *pStm, BOOL fClearDirty); - - public: - RulesClassExtension(const RulesClass *this_ptr); - RulesClassExtension(const NoInitClass &noinit); - virtual ~RulesClassExtension(); - - virtual int Size_Of() const override; - virtual void Detach(TARGET target, bool all = true) override; - virtual void Compute_CRC(WWCRCEngine &crc) const override; - - virtual const char *Name() const override { return "Rule"; } - virtual const char *Full_Name() const override { return "Rule"; } - - void Process(CCINIClass &ini); - void Initialize(CCINIClass &ini); - - bool Objects(CCINIClass &ini); - - bool General(CCINIClass &ini); - bool MPlayer(CCINIClass &ini); - bool AudioVisual(CCINIClass &ini); - bool CombatDamage(CCINIClass &ini); - bool Weapons(CCINIClass &ini); - bool Armors(CCINIClass &ini); - bool Rockets(CCINIClass &ini); - bool Tiberiums(CCINIClass &ini); - - private: - void Check(); - void Fixups(CCINIClass &ini); - - public: - /** - * Should the MCV unit auto deploy on game start? - */ - bool IsMPAutoDeployMCV; - - /** - * Are construction yards pre-placed on the map rather than a MCV given to the player? - */ - bool IsMPPrePlacedConYards; - - /** - * Can players build their own structures adjacent to structures owned by their allies? - */ - bool IsBuildOffAlly; - - /** - * Should active super weapons show their recharge timer display - * on the tactical view? - */ - bool IsShowSuperWeaponTimers; - - /** - * Defines the strength of ice. Higher values make ice less likely - * to break from a shot. - */ - int IceStrength; - - /** - * Storage pip used for weeds. - */ - int WeedPipIndex; - - /** - * Customizable maximum counts for drawing different pips. - */ - TypeList MaxPips; - - /** - * When looking for refineries, harvesters will prefer a distant free - * refinery over a closer occupied refinery if the refineries' distance - * difference in cells is less than this. - */ - int MaxFreeRefineryDistanceBias; +public: + IFACEMETHOD(Load)(IStream *pStm); + IFACEMETHOD(Save)(IStream *pStm, BOOL fClearDirty); + +public: + RulesClassExtension(const RulesClass *this_ptr); + RulesClassExtension(const NoInitClass &noinit); + virtual ~RulesClassExtension(); + + virtual int Size_Of() const override; + virtual void Detach(TARGET target, bool all = true) override; + virtual void Compute_CRC(WWCRCEngine &crc) const override; + + virtual const char *Name() const override { return "Rule"; } + virtual const char *Full_Name() const override { return "Rule"; } + + void Process(CCINIClass &ini); + void Initialize(CCINIClass &ini); + + bool Objects(CCINIClass &ini); + + bool General(CCINIClass &ini); + bool MPlayer(CCINIClass &ini); + bool AudioVisual(CCINIClass &ini); + bool CombatDamage(CCINIClass &ini); + bool Weapons(CCINIClass &ini); + bool Armors(CCINIClass &ini); + bool Rockets(CCINIClass &ini); + bool Tiberiums(CCINIClass &ini); + bool Difficulty(CCINIClass &ini); + + void Fixups(CCINIClass &ini); + +private: + void Check(); + +public: + /** + * Should the MCV unit auto deploy on game start? + */ + bool IsMPAutoDeployMCV; + + /** + * Are construction yards pre-placed on the map rather than a MCV given to the player? + */ + bool IsMPPrePlacedConYards; + + /** + * Can players build their own structures adjacent to structures owned by their allies? + */ + bool IsBuildOffAlly; + + /** + * Should active super weapons show their recharge timer display + * on the tactical view? + */ + bool IsShowSuperWeaponTimers; + + /** + * Defines the strength of ice. Higher values make ice less likely + * to break from a shot. + */ + int IceStrength; + + /** + * Storage pip used for weeds. + */ + int WeedPipIndex; + + /** + * Customizable maximum counts for drawing different pips. + */ + TypeList MaxPips; + + /** + * When looking for refineries, harvesters will prefer a distant free + * refinery over a closer occupied refinery if the refineries' distance + * difference in cells is less than this. + */ + int MaxFreeRefineryDistanceBias; + + /** + * List of units to consider "home". + */ + TypeList BaseUnit; + + /** + * This array controls the difficulty affects on the game. There is one + * difficulty class object for each difficulty level. + */ + DifficultyClass Diff[VINIFERA_DIFF_COUNT]; + + /** + * A separate difficulty used by the human player if so chosen by the mod author, + * to allow customizing AI difficulties without affecting the human player. + */ + DifficultyClass DiffHuman; + + /** + * Sounds played when a unit is promoted. + */ + VocType UpgradeVeteranSound; + VocType UpgradeEliteSound; + + /** + * EVA announcement when a unit is promoted. + */ + VoxType VoxUnitPromoted; + + /** + * The number of frames that a newly elite unit will flash for. + */ + int EliteFlashTimer; }; diff --git a/src/extensions/rules/rulesext_hooks.cpp b/src/extensions/rules/rulesext_hooks.cpp index 80715dcef..f478d5e1c 100644 --- a/src/extensions/rules/rulesext_hooks.cpp +++ b/src/extensions/rules/rulesext_hooks.cpp @@ -230,7 +230,6 @@ void RulesClassExtension_Hooks() Patch_Jump(0x005C6710, &RulesClassExt::_Process); Patch_Call(0x0053E408, &RulesClassExt::_Initialize); - Patch_Call(0x005DD7D0, &RulesClassExt::_Initialize); Patch_Jump(0x004E138B, &_Init_Rules_Extended_Class_Patch); Patch_Jump(0x004E12EB, &_Init_Rules_Show_Rules_Select_Dialog_Patch); diff --git a/src/extensions/scenario/scenarioext.cpp b/src/extensions/scenario/scenarioext.cpp index 9553313e7..692580e11 100644 --- a/src/extensions/scenario/scenarioext.cpp +++ b/src/extensions/scenario/scenarioext.cpp @@ -26,6 +26,11 @@ * ******************************************************************************/ #include "scenarioext.h" + +#include "addon.h" +#include "aircraft.h" +#include "aitrigtype.h" +#include "armortype.h" #include "tibsun_globals.h" #include "tibsun_defines.h" #include "ccini.h" @@ -34,6 +39,7 @@ #include "unittype.h" #include "buildingtype.h" #include "infantrytype.h" +#include "rulesext.h" #include "house.h" #include "housetype.h" #include "rules.h" @@ -46,7 +52,36 @@ #include "swizzle.h" #include "vinifera_saveload.h" #include "asserthandler.h" +#include "campaign.h" +#include "cd.h" #include "debughandler.h" +#include "houseext.h" +#include "infantry.h" +#include "lightsource.h" +#include "overlay.h" +#include "radarevent.h" +#include "scenarioini.h" +#include "scripttype.h" +#include "smudge.h" +#include "spawner.h" +#include "tactical.h" +#include "tagtype.h" +#include "taskforce.h" +#include "teamtype.h" +#include "terrain.h" +#include "tiberium.h" +#include "tibsun_functions.h" +#include "tracker.h" +#include "triggertype.h" +#include "tube.h" +#include "veinholemonster.h" +#include "optionsext.h" +#include "theme.h" +#include "multimiss.h" +#include "playmovie.h" +#include "restate.h" +#include "wwmouse.h" +#include "campaignext.h" /** @@ -57,7 +92,10 @@ ScenarioClassExtension::ScenarioClassExtension(const ScenarioClass *this_ptr) : GlobalExtensionClass(this_ptr), Waypoint(NEW_WAYPOINT_COUNT), - IsIceDestruction(true) + IsIceDestruction(true), + SidebarSide(SIDE_NONE), + IsUseMPAIBaseNodes(false), + LoadingScreens{ { "", {} }, { "", {} } , { "", {} } } { //if (this_ptr) EXT_DEBUG_TRACE("ScenarioClassExtension::ScenarioClassExtension - 0x%08X\n", (uintptr_t)(ThisPtr)); @@ -189,6 +227,13 @@ void ScenarioClassExtension::Init_Clear() //EXT_DEBUG_TRACE("ScenarioClassExtension::Init_Clear - 0x%08X\n", (uintptr_t)(This())); + LoadingScreens[0].Filename = ""; + LoadingScreens[1].Filename = ""; + LoadingScreens[2].Filename = ""; + LoadingScreens[0].Position = TPoint2D(0,0); + LoadingScreens[1].Position = TPoint2D(0,0); + LoadingScreens[2].Position = TPoint2D(0,0); + { /** * Clear the any previously loaded tutorial messages in preperation for @@ -208,6 +253,14 @@ void ScenarioClassExtension::Init_Clear() * Clear all waypoint values, preparing for scenario loading. */ Clear_All_Waypoints(); + + enum { START_RANDOM = -2 }; + + for (int i = 0; i < MAX_PLAYERS; i++) { + + StartingPositions[i] = START_RANDOM; + StartingPositionCells[i] = Cell(); + } } @@ -225,6 +278,7 @@ bool ScenarioClassExtension::Read_INI(CCINIClass &ini) IsIceDestruction = ini.Get_Bool(BASIC, "IceDestructionEnabled", IsIceDestruction); ScorePlayerColor = ini.Get_RGB(BASIC, "ScorePlayerColor", ScorePlayerColor); ScoreEnemyColor = ini.Get_RGB(BASIC, "ScoreEnemyColor", ScoreEnemyColor); + IsUseMPAIBaseNodes = ini.Get_Bool(BASIC, "UseMPAIBaseNodes", IsUseMPAIBaseNodes); /** * #issue-123 @@ -237,6 +291,46 @@ bool ScenarioClassExtension::Read_INI(CCINIClass &ini) } +/** + * Read the loading screen overrides from the scenario INI. + * + * @author: CCHyper + */ +bool ScenarioClassExtension::Read_Loading_Screen_INI(const char *filename) +{ + //EXT_DEBUG_TRACE("ScenarioClassExtension::Read_Loading_Screen_INI - 0x%08X\n", (uintptr_t)(This())); + + static const char * const BASIC = "Basic"; + + CCFileClass file(filename); + CCINIClass ini(file); + + if (!ini.Is_Loaded()) { + return false; + } + + if (Session.Type == GAME_NORMAL) { + + char buffer[MAX_PATH]; + + ini.Get_String(BASIC, "LoadingScreen400", buffer, sizeof(buffer)); + ScenExtension->LoadingScreens[0].Filename = buffer; + + ini.Get_String(BASIC, "LoadingScreen480", buffer, sizeof(buffer)); + ScenExtension->LoadingScreens[1].Filename = buffer; + + ini.Get_String(BASIC, "LoadingScreen600", buffer, sizeof(buffer)); + ScenExtension->LoadingScreens[2].Filename = buffer; + + ScenExtension->LoadingScreens[0].Position = ini.Get_Point(BASIC, "LoadingScreen400TextPos", ScenExtension->LoadingScreens[0].Position); + ScenExtension->LoadingScreens[1].Position = ini.Get_Point(BASIC, "LoadingScreen460TextPos", ScenExtension->LoadingScreens[1].Position); + ScenExtension->LoadingScreens[2].Position = ini.Get_Point(BASIC, "LoadingScreen600TextPos", ScenExtension->LoadingScreens[2].Position); + } + + return true; +} + + /** * Load the tutorial messages section from the ini database. * @@ -502,85 +596,1265 @@ void ScenarioClassExtension::Read_Waypoint_INI(CCINIClass &ini) */ Waypoint[wp_num] = cell; -#if defined(TS_CLIENT) +#if defined(TS_CLIENT) + /** + * Also store original waypoint value for the CnCNet ts-patches spawner. + */ + if (wp_num < WAYPOINT_COUNT) { + Scen->Waypoint[wp_num] = cell; + } +#endif + + /** + * If the cell location is valid, flag the cell on the map as a waypoint holder. + */ + if (wp_num >= 0 && cell) { +#ifndef NDEBUG + //DEV_DEBUG_INFO("Scenario: Waypoint '%s', location '%d,%d' -> IsWaypoint = true.\n", ::Waypoint_As_String(cell), cell.X, cell.Y); +#endif + Map[cell].IsWaypoint = true; + } + + } + + if (valid_count > 0) DEV_DEBUG_INFO("Scenario: Read a total of '%d' waypoints.\n", valid_count); +} + + +/** + * Write the waypoint locations to the ini database. + * + * @author: CCHyper + */ +void ScenarioClassExtension::Write_Waypoint_INI(CCINIClass &ini) +{ + //EXT_DEBUG_TRACE("ScenarioClassExtension::Write_Waypoint_INI - 0x%08X\n", (uintptr_t)(This())); + + static char const * const WAYNAME = "Waypoints"; + + char entry[32]; + int valid_count = 0; + + /** + * Clear any existing section from the ini database. + */ + ini.Clear(WAYNAME); + + /** + * Save the Waypoint entries. + */ + for (WaypointType wp = WAYPOINT_FIRST; wp < Waypoint.Length(); ++wp) { + if (Is_Valid_Waypoint(wp)) { + std::snprintf(entry, sizeof(entry), "%d", wp); + int value = Waypoint[wp].X + 1000 * Waypoint[wp].Y; + ini.Put_Int(WAYNAME, entry, value); + ++valid_count; + } + } + + if (valid_count > 0) DEV_DEBUG_INFO("Scenario: Wrote a total of '%d' waypoints.\n", valid_count); +} + + +/** + * Returns the waypoint number as a string. + * + * @author: CCHyper + */ +const char * ScenarioClassExtension::Waypoint_As_String(WaypointType wp) const +{ + //EXT_DEBUG_TRACE("ScenarioClassExtension::Waypoint_As_String - 0x%08X\n", (uintptr_t)(This())); + + for (WaypointType wp = WAYPOINT_FIRST; wp < Waypoint.Length(); ++wp) { + if (Is_Valid_Waypoint(wp)) { + return ::Waypoint_As_String(wp); + } + } + + return ""; +} + + +/** + * Starts the scenario. + * + * @author: 07/04/1995 JLB : Red Alert Source Code + * 01/11/2024 ZivDero : Adjustments for Tiberian Sun + */ +bool ScenarioClassExtension::Start_Scenario(char* name, bool briefing, CampaignType campaignid) +{ + /** + * If there is no scenario name supplied, but we got a campaign id, fetch the scenario name from the campaign. + */ + if ((name == nullptr || std::strlen(name) == 0) && campaignid != CAMPAIGN_NONE) { + name = Campaigns[campaignid]->Scenario; + } + + /** + * Set the current campaign ID. + */ + Scen->CampaignID = campaignid; + + DEBUG_INFO("\n----- Starting scnenario: %s -----\n", name); + DEBUG_INFO("Player Count: %d\n", Session.Players.Count()); + + /** + * Set the scenario name. + */ + std::strcpy(Scen->ScenarioName, name); + _strupr(Scen->ScenarioName); + + Theme.Stop(); + + /** + * Play the winning movie and then start the next scenario. + */ + CD::Set_Required_CD(DISK_ANY); + + if (Session.Type == GAME_NORMAL) { + if (Scen->CampaignID != CAMPAIGN_NONE) { + DiskID disk = Campaigns[Scen->CampaignID]->WhichCD; + CD::Set_Required_CD(disk); + } + } + else if (Session.Options.ScenarioIndex != -1) { + MultiMission* mission = Session.Scenarios[Session.Options.ScenarioIndex]; + DiskID CurrentDisk = CD::Get_Volume_Index(); + if (!mission->Is_Available(CurrentDisk)) { + DiskID disk = mission->Get_Disk(); + CD::Set_Required_CD(disk); + } + } + + Session.Suspended++; + if (CD().Is_Available(CD::RequiredCD)) { + + Session.Suspended--; + if (briefing && campaignid != CAMPAIGN_NONE && Scen->Scenario == 1) { + + /** + * #issue-95 + * + * Patch for handling the campaign intro movies + * for "The First Decade" and "Freeware TS" installations. + * + * @author: CCHyper + */ + char movie_filename[32]; + + /** + * Fetch the campaign disk id. + */ + CampaignClass* campaign = Campaigns[campaignid]; + DiskID cd_num = campaign->WhichCD; + + /** + * Check if the current campaign is an original GDI or NOD campaign. + */ + bool is_original_gdi = (cd_num == DISK_GDI && (Wstring(campaign->IniName) == "GDI1" || Wstring(campaign->IniName) == "GDI1A") && Wstring(campaign->Scenario) == "GDI1A.MAP"); + bool is_original_nod = (cd_num == DISK_NOD && (Wstring(campaign->IniName) == "NOD1" || Wstring(campaign->IniName) == "NOD1A") && Wstring(campaign->Scenario) == "NOD1A.MAP"); + + /** + * #issue-762 + * + * Fetch the campaign extension (if available) and get the custom intro movie. + * + * @author: CCHyper + */ + CampaignClassExtension* campaignext = Extension::Fetch(campaign); + if (campaignext->IntroMovie[0] != '\0') { + std::snprintf(movie_filename, sizeof(movie_filename), "%s.VQA", campaignext->IntroMovie); + DEBUG_INFO("About to play \"%s\".\n", movie_filename); + Play_Movie(movie_filename); + } + /** + * If this is an original Tiberian Sun campaign, play the respective intro movie. + */ + else if (is_original_gdi || is_original_nod) { + + /** + * "The First Decade" and "Freeware TS" installations reshuffle + * the movie files due to all mix files being local now and a + * primitive "no-cd" added; + * + * MOVIES01.MIX -> INTRO.VQA (GDI) is now INTR0.VQA + * MOVIES02.MIX -> INTRO.VQA (NOD) is now INTR1.VQA + * + * Build the movie filename based on the current campaign's desired CD (see DiskID enum). + */ + std::snprintf(movie_filename, sizeof(movie_filename), "INTR%d.VQA", cd_num); + + /** + * Now play the movie if it is found, falling back to original behavior otherwise. + */ + if (CCFileClass(movie_filename).Is_Available()) { + DEBUG_INFO("About to play \"%s\".\n", movie_filename); + Play_Movie(movie_filename); + + } + else if (CCFileClass("INTRO.VQA").Is_Available()) { + DEBUG_INFO("About to play \"INTRO.VQA\".\n"); + Play_Movie("INTRO.VQA"); + + } + else { + DEBUG_WARNING("Failed to find Intro movie!\n"); + return false; + } + + } + else { + DEBUG_WARNING("No campaign intro movie defined.\n"); + } + } + + if (!Read_Scenario(name)) { + return false; + } + + Theme.Stop(); + + if (briefing) { + Play_Movie(Scen->IntroMovie, THEME_NONE); + Play_Movie(Scen->BriefMovie, THEME_NONE); + } + + /** + * If there's no briefing movie, restate the mission at the beginning. + */ + char buffer[32]; + if (Scen->BriefMovie != VQ_NONE) { + std::snprintf(buffer, std::size(buffer), "%s.VQA", Movies[Scen->BriefMovie]); + } + + if (Session.Type == GAME_NORMAL && (Scen->BriefMovie == VQ_NONE || !CCFileClass(buffer).Is_Available())) { + + /** + * Make sure the mouse is visible before showing the restatement. + */ + WWMouse->Release_Mouse(); + WWMouse->Show_Mouse(); + + Restate_Mission(Scen); + + WWMouse->Hide_Mouse(); + WWMouse->Capture_Mouse(); + } + + /** + * Show the dropship loadout screen if this mission has a dropship. + */ + if (Scen->StartingDropships > 0) { + + /** + * issue-284 + * + * Play a background theme during the loadout menu. + * + * @author: CCHyper + */ + if (!Theme.Still_Playing()) { + + /** + * If DSHPLOAD is defined in THEME.INI, play that, otherwise default + * to playing the TS Maps theme. + */ + ThemeType theme = Theme.From_Name("DSHPLOAD"); + if (theme == THEME_NONE) { + theme = Theme.From_Name("MAPS"); + } + + Theme.Play_Song(theme); + } + + WWMouse->Release_Mouse(); + WWMouse->Show_Mouse(); + + Dropship_Loadout(); + + WWMouse->Hide_Mouse(); + WWMouse->Capture_Mouse(); + + if (Theme.Still_Playing()) { + Theme.Stop(true); // Smoothly fade out the track. + } + } + + if (briefing) { + Play_Movie(Scen->ActionMovie, Scen->TransitTheme); + } + + if (Scen->ActionMovie != VQ_NONE || Scen->TransitTheme == THEME_NONE) { + Theme.Queue_Song(THEME_PICK_ANOTHER); + } + else { + Theme.Queue_Song(Scen->TransitTheme); + } + + /** + * Set the options values, since the palette has been initialized by Read_Scenario. + */ + Options.Set(); + + /** + * Black out the screen. + */ + HiddenSurface->Fill(0); + GScreenClass::Blit(true, HiddenSurface, 0); + + /** + * Toggle the display mode if mode toggling is allowed. + */ + if (Debug_AllowModeToggle && (ScreenRect.Width != Options.ScreenWidth || ScreenRect.Height != Options.ScreenHeight)) { + DEBUG_INFO("Toggle display mode to %d X %d\n", Options.ScreenWidth, Options.ScreenHeight); + Change_Video_Mode(Options.ScreenWidth, Options.ScreenHeight); + } + + /** + * Print a message stating the current difficulty level. + */ + char diff_name[128]; + std::snprintf(diff_name, std::size(diff_name), "Difficulty: %s", Vinifera_AIDifficultyNames[Scen->CDifficulty].Peek_Buffer()); + + if (Vinifera_SpawnerActive && std::strlen(Vinifera_SpawnerConfig->DifficultyName)) { + std::snprintf(diff_name, std::size(diff_name), "Difficulty: %s", Vinifera_SpawnerConfig->DifficultyName); + } + + Session.Messages.Add_Message(nullptr, 0, diff_name, static_cast(4), TPF_6PT_GRAD | TPF_USE_GRAD_PAL | TPF_FULLSHADOW, Rule->MessageDelay * TICKS_PER_MINUTE); + + /** + * Mark the game as having started. + */ + Scen->ElapsedTimer.Start(); + TacticalViewActive = true; + ScenarioStarted = true; + + return true; + } + + Session.Suspended--; + return false; +} + + + +/** + * Read specified scenario INI file. + * + * @author: ZivDero + */ +bool ScenarioClassExtension::Read_Scenario_INI(const char* root, bool) +{ + if (CD().Is_Available(CD::RequiredCD)) { + /** + * Reset the frame counter. + */ + Frame = 0; + + /** + * Set the time limit if the game is to be of a specified duration. + */ + /* + if (TournamentTime > 0) { + PlayLimitTimer = TournamentTime * 900; + } + */ + + CCINIClass scenario_ini; + CCFileClass scenario_file(root); + + DEBUG_INFO("Read_Scenario_INI - Filename is %s\n", root); + if (scenario_ini.Load(scenario_file, true, false)) { + std::strncpy(Scen->ScenarioName, root, sizeof(Scen->ScenarioName) - 1); + return Load_Scenario(scenario_ini, false); + } + else { + DEBUG_INFO("Scenario ini load failed!\n"); + return false; + } + } + + return false; +} + + +/** + * Process additions to the Rules data from the input file. + * + * @author: CCHyper + */ +static bool Rule_Addition(const char* fname, bool with_digest = false) +{ + CCFileClass file(fname); + if (!file.Is_Available()) { + return false; + } + + CCINIClass ini; + if (!ini.Load(file, with_digest)) { + return false; + } + + DEBUG_INFO("Calling Rule->Addition() with \"%s\" overrides.\n", fname); + + Rule->Addition(ini); + + return true; +} + + +/** + * Load the scenario from the specified INI file. + * + * @author: 10/07/1992 JLB - Red Alert source code. + * ZivDero - Adjustments for Tiberian Sun. + */ +bool ScenarioClassExtension::Load_Scenario(CCINIClass& ini, bool random) +{ + static const char* BASIC = "Basic"; + static const char* MAP = "Map"; + static const char* MISSION_INI = "MISSION.INI"; + char buffer[32]; + + ScenarioInit++; + + Clear_Scenario(); + + /** + * Set up difficulty and fog of war settings. + */ + if (Session.Type == GAME_NORMAL) { + if (Vinifera_SpawnerActive) { + Scen->Difficulty = static_cast(Vinifera_SpawnerConfig->CampaignDifficulty); + Scen->CDifficulty = static_cast(Vinifera_SpawnerConfig->CampaignCDifficulty); + } + else { + Scen->Difficulty = static_cast(Options.Difficulty); + Scen->CDifficulty = static_cast(DIFF_COUNT - Options.Difficulty - 1); + } + Scen->SpecialFlags.IsFogOfWar = false; + Special.IsFogOfWar = false; + } + else { + Scen->Difficulty = static_cast(Session.Options.AIDifficulty); + Scen->CDifficulty = static_cast(DIFF_COUNT - Scen->Difficulty - 1); + Scen->SpecialFlags.IsFogOfWar = Session.Options.FogOfWar; + Special.IsFogOfWar = Session.Options.FogOfWar; + } + + Scen->InitTime = ini.Get_Int(BASIC, "InitTime", 10000); + const bool official = ini.Get_Bool(BASIC, "Official", false); + + /** + * Set the unique playthrough ID. + */ + Vinifera_PlaythroughID = std::time(nullptr); + DEBUG_INFO("[Vinifera] Starting new scenario. Playthrough ID: %u.\n", Vinifera_PlaythroughID); + + /** + * Make sure we have, and then enable the required addon. + */ + if (Session.Type == GAME_NORMAL) { + + Disable_Addon(ADDON_ANY); + Scen->RequiredAddOn = static_cast(ini.Get_Bool(BASIC, "RequiredAddOn", ADDON_NONE)); + Set_Required_Addon(Scen->RequiredAddOn); + if (!Is_Addon_Available(Scen->RequiredAddOn)) { + return false; + } + Enable_Addon(Scen->RequiredAddOn); + } + else { + + Scen->RequiredAddOn = Get_Required_Addon(); + } + + Session.Loading_Callback(3); + + /** + * Reset the swizzle manager. + */ + SwizzleManager.Reset(); + + /** + * Recreate the tactical map. + */ + DEBUG_INFO("Creating new tactical map\n"); + delete TacticalMap; + TacticalMap = new Tactical(); + TacticalMap->Set_Tactical_Dimensions(TacticalRect); + + /** + * Initialize the theater. + */ + Scen->Theater = ini.Get_TheaterType(MAP, "Theater", THEATER_FIRST); + Init_Theater(Scen->Theater); + Session.Loading_Callback(8); + + /** + * Load the main rules file. + */ + DEBUG_INFO("Initializing Rules\n"); + RuleExtension->Initialize(*RuleINI); + Rule->Initialize(*RuleINI); + + Session.Loading_Callback(15); + Call_Back(); + + /** + * Read the rules into ScenarioClass. + */ + DEBUG_INFO("Calling Scen->Read_Global_INI(*RuleINI);\n"); + Scen->Read_Global_INI(*RuleINI); + + Call_Back(); + + /** + * #issue-#671 + * + * Add loading of MPLAYER.INI to override Rules data for multiplayer games. + * + * @author: CCHyper + */ + if (Session.Type != GAME_NORMAL && Session.Type != GAME_WDT) { + + /** + * Process the multiplayer ini overrides. + */ + Rule_Addition("MPLAYER.INI"); + if (Is_Addon_Enabled(ADDON_FIRESTORM)) { + Rule_Addition("MPLAYERFS.INI"); + } + + } + + Session.Loading_Callback(30); + + Call_Back(); + + /** + * Read scenario overrides into our Rules. + */ + DEBUG_INFO("Calling Rule->Addition() with scenario overrides\n"); + Rule->Addition(ini); + DEBUG_INFO("Finished Rule->Addition() with scenario overrides\n"); + + Session.Loading_Callback(45); + + /** + * Read in the specific information for each of the house types. This creates + * the houses of different types. + */ + if (Session.Type == GAME_NORMAL) { + DEBUG_INFO("Reading in scenario house types\n"); + HouseClass::Read_Scenario_INI(ini); + } + + /** + * Init the Scenario CRC value + */ + ScenarioCRC = 0; + + /** + * Read scenario data from the scenario INI. + */ + if (Scen->Read_INI(ini) && ScenExtension->Read_INI(ini)) { + + Session.Loading_Callback(50); + + /** + * Determine the player's side. + */ + if (Session.Type == GAME_NORMAL) { + ini.Get_String(BASIC, "Player", "GDI", buffer, 32); + /** + * Fetch the house's side and use this to decide which assets to load. + */ + const auto housetype = HouseTypeClass::As_Pointer(buffer); + + Scen->IsGDI = static_cast(housetype->Side & 0xFF); + Scen->SpeechSide = housetype->Side; + ScenExtension->SidebarSide = housetype->Side; + } + else { + Scen->IsGDI = static_cast(Session.IsGDI); + Scen->SpeechSide = static_cast(Session.IsGDI); + ScenExtension->SidebarSide = static_cast(Session.IsGDI); + } + + /** + * Init side-specific data. + */ + DEBUG_INFO("Calling Prep_For_Side()\n"); + if (Prep_For_Side(ScenExtension->SidebarSide)) { + + Call_Back(); + + /** + * Unfortunately, since we now load rules before prepping for side, + * we have to reload cameos for Technos, as they can be side-specific. + */ + + for (int index = 0; index < TechnoTypes.Count(); ++index) { + + TechnoTypeClass* ttype = TechnoTypes[index]; + std::snprintf(buffer, sizeof(buffer), "%s.SHP", ttype->CameoFilename); + + const ShapeFileStruct* cameodata = MFCC::RetrieveT(buffer); + + if (cameodata != nullptr) { + ttype->CameoData = cameodata; + } + } + + Call_Back(); + + /** + * In single player, the speech and sidebar side can be overridden by the scenario. + */ + if (Session.Type == GAME_NORMAL) { + Scen->SpeechSide = ini.Get_SideType("Basic", "SpeechSide", Scen->SpeechSide); + ScenExtension->SidebarSide = ini.Get_SideType("Basic", "SidebarSide", ScenExtension->SidebarSide); + } + + /** + * Init the speech for the side. + */ + DEBUG_INFO("Calling Prep_Speech_For_Side()\n"); + if (Prep_Speech_For_Side(Scen->SpeechSide)) { + + Session.Loading_Callback(58); + + /** + * Read in the map control values. This includes dimensions + * as well as theater information. + */ + Map.Read_INI(ini); + + /** + * Outside of campaign, assign houses their starting positions. + * This used to happen in Create_Units(), but needs to happen earlier + * so that we can handle Spawn houses. + */ + if (Session.Type != GAME_NORMAL) { + ScenExtension->Assign_Starting_Positions(official); + } + + /** + * Outside of campaign, whether the buildings are destructible can be controlled. + */ + if (Session.Type != GAME_NORMAL) { + Special.IsDestroyableBridges = Session.Options.BridgeDestruction; + } + + Call_Back(); + + /** + * Outside of campaign, the scenario may request that we read base nodes for + * Spawn houses. Do that if necessary. + */ + if (Session.Type != GAME_NORMAL && ScenExtension->IsUseMPAIBaseNodes) { + for (int i = 0; i < Session.Players.Count() + Session.Options.AIPlayers; i++) { + + /** + * Skip observers, they don't need base nodes. + */ + const auto houseext = Extension::Fetch(Houses[i]); + if (houseext->IsObserver) { + continue; + } + + /** + * Read base nodes for this house. + */ + std::snprintf(buffer, std::size(buffer), "Spawn%d", ScenExtension->StartingPositions[i]); + Houses[i]->Base.Read_INI(ini, buffer); + } + } + + /** + * Read in the team type data. The team types must be created before any + * triggers can be created. + */ + TeamTypeClass::Read_Scenario_INI(AIINI, true); + if (Is_Addon_Enabled(ADDON_FIRESTORM)) { + TeamTypeClass::Read_Scenario_INI(FSAIINI, true); + } + TeamTypeClass::Read_Scenario_INI(ini, false); + + /** + * Read in the script type data. + */ + ScriptTypeClass::Read_Scenario_INI(AIINI, true); + if (Is_Addon_Enabled(ADDON_FIRESTORM)) { + ScriptTypeClass::Read_Scenario_INI(FSAIINI, true); + } + ScriptTypeClass::Read_Scenario_INI(ini, false); + + /** + * Read in the task force data. + */ + TaskForceClass::Read_Scenario_INI(AIINI, true); + if (Is_Addon_Enabled(ADDON_FIRESTORM)) { + TaskForceClass::Read_Scenario_INI(FSAIINI, true); + } + TaskForceClass::Read_Scenario_INI(ini, false); + + /** + * Read in the trigger data. The triggers must be created before any other + * objects can be initialized. + */ + TriggerTypeClass::Read_Scenario_INI(ini); + + /** + * Read in the trigger tag data. + */ + TagTypeClass::Read_Scenario_INI(ini); + + /** + * Read in the AI trigger data. + */ + AITriggerTypeClass::Read_Scenario_INI(AIINI, true); + if (Is_Addon_Enabled(ADDON_FIRESTORM)) { + AITriggerTypeClass::Read_Scenario_INI(FSAIINI, true); + } + AITriggerTypeClass::Read_Scenario_INI(ini, 0); + + Session.Loading_Callback(60); + + Call_Back(); + + /** + * Read in the tunnel values. + */ + TubeClass::Read_Scenario_INI(ini); + + /** + * Buildings that convert into isometric tiles need to have + * pointers to those tiles fetched now. + */ + BuildingTypeClass::Fetch_ToTile_Types(); + + Map.Flag_To_Redraw(2); + + Session.Loading_Callback(70); + Call_Back(); + + /** + * Read in any normal overlay objects. + */ + OverlayClass::Read_INI(ini); + Call_Back(); + + /** + * Recalc the attributes of all cells of the map. + */ + Map.Iterator_Reset(); + for (CellClass* cell = Map.Iterator_Next_Cell(); cell; cell = Map.Iterator_Next_Cell()) { + cell->Recalc_Attributes(-1); + } + + /** + * Place veins onto the map. + */ + OverlayClass::Place_All_Veins(); + + /** + * Read in and place the 3D terrain objects. + */ + TerrainClass::Read_INI(ini); + Call_Back(); + + /** + * Place veinhole monsters onto the map. + */ + VeinholeMonsterClass::Place_Veinhole_Monsters(true); + + /** + * Initialize Tiberium. + */ + TiberiumClass::Growth_Init_Clear(); + TiberiumClass::Init_Cells(); + + Session.Loading_Callback(72); + + /** + * Do something with the radar. + */ + Map.Compute_Radar_Image(); + + /** + * Read in and place the units (all sides). + */ + UnitClass::Read_INI(ini); + Call_Back(); + Session.Loading_Callback(74); + + /** + * Read in and place the aircraft units (all sides). + */ + AircraftClass::Read_INI(ini); + Call_Back(); + + /** + * Read in and place the infantry units (all sides). + */ + InfantryClass::Read_INI(ini); + Call_Back(); + Session.Loading_Callback(76); + + /** + * Read in and place all the buildings on the map. + */ + LightSourceClass::UpdateAllowed = false; + BuildingClass::Read_INI(ini); + Call_Back(); + Session.Loading_Callback(78); + + LightSourceClass::UpdateAllowed = true; + Call_Back(); + + /** + * Read in any smudge overlays. + */ + SmudgeClass::Read_INI(ini); + Call_Back(); + + CCINIClass temp_ini; + CCFileClass temp_file; + + if (Session.Type == GAME_NORMAL) { + + /** + * Reload the rules with out scenario file again? Not sure why. + */ + _splitpath(Scen->ScenarioName, nullptr, nullptr, buffer, nullptr); + std::strncat(buffer, ".INI", std::size(buffer) - 1); + + temp_file.Set_Name(buffer); + if (temp_file.Is_Available(false)) { + temp_ini.Load(temp_file, false, false); + Rule->Addition(temp_ini); + } + temp_file.Close(); + + /** + * Read the name and briefing of the mission from the MISSION.INI file. + */ + const char* mission_file_name; + if (Scen->RequiredAddOn > ADDON_NONE) { + std::snprintf(buffer, std::size(buffer), "MISSION%1d.INI", Scen->RequiredAddOn); + mission_file_name = buffer; + } + else { + mission_file_name = MISSION_INI; + } + + temp_file.Set_Name(mission_file_name); + if (temp_file.Is_Available(false)) { + + temp_ini.Load(temp_file, false, false); + + if (temp_ini.Is_Present("Name")) { + temp_ini.Get_String(Scen->ScenarioName, "Name", "", Scen->Description, std::size(Scen->Description)); + } + + if (temp_ini.Is_Present("Briefing")) { + temp_ini.Get_String(Scen->ScenarioName, "Briefing", "", buffer, std::size(buffer)); + if (std::strlen(buffer)) { + temp_ini.Get_TextBlock(buffer, Scen->BriefingText, std::size(Scen->BriefingText)); + } + } + } + } + + /** + * WW's "TheTeam" cheat. + */ + if (Session.Type == GAME_SKIRMISH && Cheat_TheTeam) { + + temp_file.Close(); + temp_file.Set_Name("TMCJ4F.INI"); + + if (temp_file.Is_Available(false)) { + temp_ini.Load(temp_file, false, false); + Rule->Addition(temp_ini); + } + } + + Session.Loading_Callback(82); + Call_Back(); + + /** + * Do some last passes on some map stuff. + */ + Map.Overpass(); + + Session.Loading_Callback(86); + Call_Back(); + + Session.Loading_Callback(90); + Call_Back(); + + /** + * Multi-player last-minute fixups + */ + if (Session.Type != GAME_NORMAL && !random) { + Scenario_MP_Fixups(official); + } + + if (Session.Type != GAME_NORMAL) { + Init_Forced_Alliances(); + } + + Call_Back(); + + /** + * Reset the swizzle manager. + */ + SwizzleManager.Reset(); + + Session.Loading_Callback(96); + Call_Back(); + + /** + * Remove all inactive objects. + */ + Remove_All_Inactive(); + + /** + * Outside of campaign, the scenario's special flags are not used. + */ + if (Session.Type != GAME_NORMAL) { + Scen->SpecialFlags = Special; + } + + int save_init = ScenarioInit; + ScenarioInit = 0; + + /** + * Set up laser fences. + */ + BuildingClass::Init_Laser_Fences(); + + ScenarioInit = save_init; + ScenarioInit--; + + Session.Loading_Callback(98); + Call_Back(); + + Map.field_122C.Clear(); + + /** + * If we have FoW turned on, fog the entire map. + */ + if (Scen->SpecialFlags.IsFogOfWar) { + Map.Fog_Map(); + } + + /** + * Refresh the radar. + */ + RadarEventClass::Clear_All(); + Map.Total_Radar_Refresh(); + + /** + * Schedule the next autosave. + */ + Vinifera_NextAutoSaveFrame = Frame; + Vinifera_NextAutoSaveFrame += Vinifera_SpawnerActive && Session.Type == GAME_IPX ? Vinifera_SpawnerConfig->AutoSaveInterval : OptionsExtension->AutoSaveInterval; + + /** + * Set the skip score bool. + */ + if (Vinifera_SpawnerActive) { + Scen->IsSkipScore = Vinifera_SpawnerConfig->SkipScoreScreen; + } + + /** + * Return with flag saying that the scenario file was read. + */ + return true; + } + } + } + + /** + * Return with flag saying that the scenario file failed to be read. + */ + ScenarioInit--; + return false; +} + + +/** + * Creates alliances as dictated by the client. + * + * @author: ZivDero + */ +void ScenarioClassExtension::Init_Forced_Alliances() +{ + /** + * Process the clients's forced alliances. + */ + if (Vinifera_SpawnerActive) { + for (int i = 0; i < Session.Players.Count() + Session.Options.AIPlayers; i++) { + HouseClass* housep = Houses[i]; + + /** + * Multiplay passive houses don't get allies. + */ + if (housep->Class->IsMultiplayPassive) + continue; + + const auto house_config = &Vinifera_SpawnerConfig->Houses[i]; + for (int j = 0; j < std::size(house_config->Alliances); j++) + { + const int ally_index = house_config->Alliances[j]; + if (ally_index != -1) + housep->Make_Ally(static_cast(ally_index)); + } + } + } +} + + +/** + * Build a list of valid multiplayer starting waypoints. + * + * @author: CCHyper + */ +static DynamicVectorClass Build_Starting_Waypoint_List(bool official) +{ + DynamicVectorClass waypts; + + /** + * Find first valid player spawn waypoint. + */ + int min_waypts = 0; + for (int i = 0; i < 8; i++) { + if (!Scen->Is_Valid_Waypoint(i)) { + break; + } + min_waypts++; + } + + /** + * Calculate the number of waypoints (as a minimum) that will be lifted from the + * mission file. Bias this number so that only the first 4 waypoints are used + * if there are 4 or fewer players. Unofficial maps will pick from all the + * available waypoints. + */ + int look_for = std::max(min_waypts, Session.Players.Count() + Session.Options.AIPlayers); + if (!official) { + look_for = MAX_PLAYERS; + } + + if (Vinifera_SpawnerActive) { + for (int i = 0; i < Session.Players.Count() + Session.Options.AIPlayers; i++) { + if (Vinifera_SpawnerConfig->Houses[i].IsObserver) + look_for--; + } + } + + for (int waycount = 0; waycount < look_for; ++waycount) { + if (Scen->Is_Valid_Waypoint(waycount)) { + Cell waycell = Scen->Get_Waypoint_Location(waycount); + waypts.Add(waycell); + DEBUG_INFO("Multiplayer start waypoint found at cell %d,%d.\n", waycell.X, waycell.Y); + } + } + + /** + * If there are insufficient waypoints to account for all players, then randomly + * assign starting points until there is enough. + */ + int deficiency = look_for - waypts.Count(); + if (deficiency > 0) { + DEBUG_WARNING("Multiplayer start waypoint deficiency - looking for more start positions.\n"); + for (int index = 0; index < deficiency; ++index) { + + Cell trycell = XY_Cell(Map.MapCellX + Random_Pick(10, Map.MapCellWidth - 10), + Map.MapCellY + Random_Pick(0, Map.MapCellHeight - 10) + 10); + + trycell = Map.Nearby_Location(trycell, SPEED_TRACK, -1, MZONE_NORMAL, false, 8, 8); + if (trycell) { + waypts.Add(trycell); + DEBUG_INFO("Random multiplayer start waypoint added at cell %d,%d.\n", trycell.X, trycell.Y); + } + } + } + + return waypts; +} + + +/** + * Assigns starting positions to multiplayer houses. + * Split from Create_Units(). + * + * @author: ZivDero, CCHyper + */ +void ScenarioClassExtension::Assign_Starting_Positions(bool official) +{ + Cell centroid; // centroid of this house's stuff. + int numtaken = 0; + + /** + * Build a list of the valid waypoints. This normally shouldn't be + * necessary because the scenario level designer should have assigned + * valid locations to the first N waypoints, but just in case, this + * loop verifies that. + */ + const unsigned int MAX_STORED_WAYPOINTS = 26; + + bool taken[MAX_STORED_WAYPOINTS]; + std::memset(taken, '\0', sizeof(taken)); + + DynamicVectorClass waypts; + waypts = Build_Starting_Waypoint_List(official); + + DEV_DEBUG_INFO("Assigning starting positions to houses.\n"); + + /** + * If the spawner is active, assign the received starting positions to the houses. + */ + if (Vinifera_SpawnerActive) { + for (int house = 0; house < Session.Players.Count() + Session.Options.AIPlayers; house++) { + StartingPositions[house] = Vinifera_SpawnerConfig->Houses[house].SpawnLocation; + } + } + + for (int house = HOUSE_FIRST; house < Houses.Count(); house++) + { /** - * Also store original waypoint value for the CnCNet ts-patches spawner. + * Get a pointer to this house; if there is none, go to the next house. */ - if (wp_num < WAYPOINT_COUNT) { - Scen->Waypoint[wp_num] = cell; + HouseClass* hptr = Houses[house]; + if (hptr == nullptr) { + DEV_DEBUG_INFO("Invalid house %d!\n", house); + continue; } -#endif /** - * If the cell location is valid, flag the cell on the map as a waypoint holder. + * Skip passive houses. */ - if (wp_num >= 0 && cell) { -#ifndef NDEBUG - //DEV_DEBUG_INFO("Scenario: Waypoint '%s', location '%d,%d' -> IsWaypoint = true.\n", ::Waypoint_As_String(cell), cell.X, cell.Y); -#endif - Map[cell].IsWaypoint = true; + if (hptr->Class->IsMultiplayPassive) { + DEV_DEBUG_INFO("House %d (%s - \"%s\") is passive, skipping.\n", house, hptr->Class->Name(), hptr->IniName); + continue; } - } + bool pick_random = true; + if (Vinifera_SpawnerActive) { - if (valid_count > 0) DEV_DEBUG_INFO("Scenario: Read a total of '%d' waypoints.\n", valid_count); -} + const auto& houseconfig = Vinifera_SpawnerConfig->Houses[house]; + if (houseconfig.IsObserver) { + + /** + * Compute our x & y limits + */ + const int xmin = Map.MapCellX; + const int xmax = xmin + Map.MapCellWidth - 1; + const int ymin = Map.MapCellY; + const int ymax = ymin + Map.MapCellHeight - 1; + centroid = Cell(Random_Pick(xmin, xmax), Random_Pick(ymin, ymax)); + hptr->Center = Cell_Coord(centroid, true); + StartingPositionCells[house] = centroid; + StartingPositions[house] = -1; -/** - * Write the waypoint locations to the ini database. - * - * @author: CCHyper - */ -void ScenarioClassExtension::Write_Waypoint_INI(CCINIClass &ini) -{ - //EXT_DEBUG_TRACE("ScenarioClassExtension::Write_Waypoint_INI - 0x%08X\n", (uintptr_t)(This())); + DEBUG_INFO(" House %d (%s) observing at random cell (%d,%d)\n", house, hptr->IniName, centroid.X, centroid.Y); + continue; + } - static char const * const WAYNAME = "Waypoints"; + const int chosen_spawn = StartingPositions[house]; + if (chosen_spawn >= 0 && chosen_spawn < MAX_PLAYERS && !taken[chosen_spawn]) { + centroid = waypts[chosen_spawn]; + taken[chosen_spawn] = true; + pick_random = false; + numtaken++; + } + } - char entry[32]; - int valid_count = 0; + if (pick_random) { - /** - * Clear any existing section from the ini database. - */ - ini.Clear(WAYNAME); + /** + * Pick the starting location for this house. The first house just picks + * one of the valid locations at random. The other houses pick the furthest + * waypoint from the existing houses. + */ + if (numtaken == 0) { + int pick = Random_Pick(0, waypts.Count() - 1); + centroid = waypts[pick]; + taken[pick] = true; + numtaken++; + StartingPositions[house] = pick; - /** - * Save the Waypoint entries. - */ - for (WaypointType wp = WAYPOINT_FIRST; wp < Waypoint.Length(); ++wp) { - if (Is_Valid_Waypoint(wp)) { - std::snprintf(entry, sizeof(entry), "%d", wp); - int value = Waypoint[wp].X + 1000 * Waypoint[wp].Y; - ini.Put_Int(WAYNAME, entry, value); - ++valid_count; - } - } + } + else { - if (valid_count > 0) DEV_DEBUG_INFO("Scenario: Wrote a total of '%d' waypoints.\n", valid_count); -} + /** + * Set all waypoints to have a score of zero in preparation for giving + * a distance score to all waypoints. + */ + int score[MAX_STORED_WAYPOINTS]; + std::memset(score, '\0', sizeof(score)); + /** + * Scan through all waypoints and give a score as a value of the sum + * of the distances from this waypoint to all taken waypoints. + */ + for (int index = 0; index < waypts.Count(); index++) { -/** - * Returns the waypoint number as a string. - * - * @author: CCHyper - */ -const char * ScenarioClassExtension::Waypoint_As_String(WaypointType wp) const -{ - //EXT_DEBUG_TRACE("ScenarioClassExtension::Waypoint_As_String - 0x%08X\n", (uintptr_t)(This())); + /** + * If this waypoint has not already been taken, then accumulate the + * sum of the distance between this waypoint and all other taken + * waypoints. + */ + if (!taken[index]) { + for (int trypoint = 0; trypoint < waypts.Count(); trypoint++) { - for (WaypointType wp = WAYPOINT_FIRST; wp < Waypoint.Length(); ++wp) { - if (Is_Valid_Waypoint(wp)) { - return ::Waypoint_As_String(wp); + if (taken[trypoint]) { + score[index] += Distance(waypts[index], waypts[trypoint]); + } + } + } + } + + /** + * Now find the waypoint with the largest score. This waypoint is the one + * that is furthest from all other taken waypoints. + */ + int best = 0; + int bestvalue = 0; + for (int searchindex = 0; searchindex < waypts.Count(); searchindex++) { + if (score[searchindex] > bestvalue || bestvalue == 0) { + bestvalue = score[searchindex]; + best = searchindex; + } + } + + /** + * Assign this best position to the house. + */ + centroid = waypts[best]; + taken[best] = true; + numtaken++; + StartingPositions[house] = best; + } } - } - return ""; + /** + * Assign the center of this house to the waypoint location. + */ + hptr->Center = Cell_Coord(centroid, true); + StartingPositionCells[house] = centroid; + DEBUG_INFO(" House %d (%s) starting at waypoint %d (%d,%d)\n", house, hptr->IniName, StartingPositions[house], centroid.X, centroid.Y); + } } + /** * Assigns multiplayer houses to various players. * @@ -593,6 +1867,7 @@ void ScenarioClassExtension::Assign_Houses() bool color_used[MAX_PLAYERS]; // true = this color is in use. HouseClass *housep; + HouseClassExtension* houseext; HouseTypeClass *housetype; HousesType house; int lowest_color; @@ -651,9 +1926,10 @@ void ScenarioClassExtension::Assign_Houses() * in the HouseClass array. */ housep = new HouseClass(HouseTypes[node.Player.House]); + houseext = Extension::Fetch(housep); - std::memset((char *)housep->IniName, 0, MPLAYER_NAME_MAX); - std::strncpy((char *)housep->IniName, node.Name, MPLAYER_NAME_MAX-1); + std::memset(housep->IniName, 0, MPLAYER_NAME_MAX); + std::strncpy(housep->IniName, node.Name, MPLAYER_NAME_MAX-1); /** * Set the house's IsHuman, Credits, ActLike, and RemapTable. @@ -661,9 +1937,8 @@ void ScenarioClassExtension::Assign_Houses() housep->IsHuman = true; housep->Control.TechLevel = BuildLevel; - housep->Init_Data((PlayerColorType)node.Player.Color, - node.Player.House, Session.Options.Credits); - housep->RemapColor = Session.Player_Color_To_Scheme_Color((PlayerColorType)node.Player.Color); + housep->Init_Data(node.Player.Color, node.Player.House, Session.Options.Credits); + housep->RemapColor = Session.Player_Color_To_Scheme_Color(node.Player.Color); housep->Init_Remap_Color(); /** @@ -676,6 +1951,30 @@ void ScenarioClassExtension::Assign_Houses() housep->Assign_Handicap(DIFF_NORMAL); + /** + * Process spawner overrides. + */ + if (Vinifera_SpawnerActive) { + int house_index = Houses.Count() - 1; + const auto& houseconfig = Vinifera_SpawnerConfig->Houses[house_index]; + + /** + * Mark an observer accordingly. + */ + if (houseconfig.IsObserver) { + + houseext->IsObserver = true; + + /** + * If this ID is for myself, set up ObserverPtr. + */ + if (index == 0) { + Vinifera_ObserverPtr = housep; + } + } + + } + /** * Record where we placed this player. */ @@ -694,52 +1993,50 @@ void ScenarioClassExtension::Assign_Houses() */ for (int i = Session.Players.Count(); i < Session.Players.Count() + Session.Options.AIPlayers; ++i) { -#if 0 - if (Percent_Chance(50)) { - pref_house = HOUSE_GDI; - } else { - pref_house = HOUSE_NOD; - } -#endif - - /** - * #issue-7 - * - * Replaces code from above. - * - * Fixes a limitation where the AI would only be able to choose - * between the houses GDI (0) and NOD (1). Now, all houses that - * have "IsMultiplay" true will be considered for sellection. - */ - while (true) { - pref_house = (HousesType)Random_Pick(0, HouseTypes.Count()-1); - if (HouseTypes[pref_house]->IsMultiplay) { - break; + if (!Vinifera_SpawnerActive) + { + /** + * #issue-7 + * + * Fixes a limitation where the AI would only be able to choose + * between the houses GDI (0) and Nod (1). Now, all houses that + * have "IsMultiplay" true will be considered for sellection. + */ + while (true) { + pref_house = (HousesType)Random_Pick(0, HouseTypes.Count() - 1); + if (HouseTypes[pref_house]->IsMultiplay) { + break; + } } - } - /** - * Pick a color for this house; keep looping until we find one. - */ - while (true) { - color = Random_Pick(0, (MAX_PLAYERS-1)); - if (color_used[color] == false) { - break; + /** + * Pick a color for this house; keep looping until we find one. + */ + while (true) { + color = Random_Pick(0, (MAX_PLAYERS - 1)); + if (color_used[color] == false) { + break; + } } + color_used[color] = true; + } + else + { + color = Vinifera_SpawnerConfig->Players[i].Color; + pref_house = static_cast(Vinifera_SpawnerConfig->Players[i].House); } - color_used[color] = true; housep = new HouseClass(HouseTypes[pref_house]); + houseext = Extension::Fetch(housep); /** - * Set the house's IsHuman, Credits, ActLike, and RemapTable. + * Set the house's IsHuman, Credits, ActLike, and RemapColor. */ housep->IsHuman = false; - //housep->IsStarted = true; housep->Control.TechLevel = BuildLevel; - housep->Init_Data((PlayerColorType)color, pref_house, Session.Options.Credits); - housep->RemapColor = Session.Player_Color_To_Scheme_Color((PlayerColorType)color); + housep->Init_Data(static_cast(color), pref_house, Session.Options.Credits); + housep->RemapColor = Session.Player_Color_To_Scheme_Color(static_cast(color)); housep->Init_Remap_Color(); std::strcpy(housep->IniName, Text_String(TXT_COMPUTER)); @@ -751,10 +2048,28 @@ void ScenarioClassExtension::Assign_Houses() DiffType difficulty = Scen->CDifficulty; if (Session.Players.Count() > 1 && Rule->IsCompEasyBonus && difficulty > DIFF_EASY) { - difficulty = (DiffType)(difficulty - 1); + difficulty = static_cast(difficulty - 1); } housep->Assign_Handicap(difficulty); + /** + * Process spawner overrides. + */ + if (Vinifera_SpawnerActive) + { + const auto player_config = &Vinifera_SpawnerConfig->Players[i]; + + /** + * Set the difficulty and name for the AI (for AIs, player index == house index) + */ + if (player_config->Difficulty >= DIFF_FIRST && player_config->Difficulty < VINIFERA_DIFF_COUNT) { + housep->Assign_Handicap(static_cast(player_config->Difficulty)); + if (Vinifera_SpawnerConfig->AINamesByDifficulty && !housep->IsHuman) { + std::snprintf(housep->IniName, std::size(housep->IniName), "%s AI", Vinifera_AIDifficultyNames[player_config->Difficulty].Peek_Buffer()); + } + } + } + DEBUG_INFO(" Assigned computer house \"%s\" (ID: %d, Color: \"%s\") to slot %d.\n", housep->Class->Name(), housep->ID, ColorSchemes[housep->RemapColor]->Name, i); } @@ -1153,69 +2468,6 @@ static bool Place_Object(ObjectClass *obj, Cell cell, FacingType facing, int dis } -/** - * Build a list of valid multiplayer starting waypoints. - * - * @author: CCHyper - */ -static DynamicVectorClass Build_Starting_Waypoint_List(bool official) -{ - DynamicVectorClass waypts; - - /** - * Find first valid player spawn waypoint. - */ - int min_waypts = 0; - for (int i = 0; i < 8; i++) { - if (!Scen->Is_Valid_Waypoint(i)) { - break; - } - min_waypts++; - } - - /** - * Calculate the number of waypoints (as a minimum) that will be lifted from the - * mission file. Bias this number so that only the first 4 waypoints are used - * if there are 4 or fewer players. Unofficial maps will pick from all the - * available waypoints. - */ - int look_for = std::max(min_waypts, Session.Players.Count()+Session.Options.AIPlayers); - if (!official) { - look_for = MAX_PLAYERS; - } - - for (int waycount = 0; waycount < look_for; ++waycount) { - if (Scen->Is_Valid_Waypoint(waycount)) { - Cell waycell = Scen->Get_Waypoint_Location(waycount); - waypts.Add(waycell); - DEBUG_INFO("Multiplayer start waypoint found at cell %d,%d.\n", waycell.X, waycell.Y); - } - } - - /** - * If there are insufficient waypoints to account for all players, then randomly - * assign starting points until there is enough. - */ - int deficiency = look_for - waypts.Count(); - if (deficiency > 0) { - DEBUG_WARNING("Multiplayer start waypoint deficiency - looking for more start positions.\n"); - for (int index = 0; index < deficiency; ++index) { - - Cell trycell = XY_Cell(Map.MapCellX + Random_Pick(10, Map.MapCellWidth-10), - Map.MapCellY + Random_Pick(0, Map.MapCellHeight-10) + 10); - - trycell = Map.Nearby_Location(trycell, SPEED_TRACK, -1, MZONE_NORMAL, false, 8, 8); - if (trycell) { - waypts.Add(trycell); - DEBUG_INFO("Random multiplayer start waypoint added at cell %d,%d.\n", trycell.X, trycell.Y); - } - } - } - - return waypts; -} - - /** * New implementation of Create_Units() * @@ -1256,7 +2508,7 @@ void ScenarioClassExtension::Create_Units(bool official) for (int i = 0; i < UnitTypes.Count(); ++i) { UnitTypeClass *unittype = UnitTypes[i]; if (unittype && unittype->IsAllowedToStartInMultiplayer) { - if (Rule->BaseUnit->Fetch_ID() != unittype->Fetch_ID()) { + if (!RuleExtension->BaseUnit.Is_Present(unittype)) { ++tot_unit_count; } } @@ -1273,25 +2525,10 @@ void ScenarioClassExtension::Create_Units(bool official) DEBUG_WARNING("No starting units available!"); } - /** - * Build a list of the valid waypoints. This normally shouldn't be - * necessary because the scenario level designer should have assigned - * valid locations to the first N waypoints, but just in case, this - * loop verifies that. - */ - const unsigned int MAX_STORED_WAYPOINTS = 26; - - bool taken[MAX_STORED_WAYPOINTS]; - std::memset(taken, '\0', sizeof(taken)); - - DynamicVectorClass waypts; - waypts = Build_Starting_Waypoint_List(official); - /** * Loop through all houses. Computer-controlled houses, with Session.Options.Bases * ON, are treated as though bases are OFF (since we have no base-building AI logic.) */ - int numtaken = 0; for (HousesType house = HOUSE_FIRST; house < Houses.Count(); ++house) { /** @@ -1303,6 +2540,12 @@ void ScenarioClassExtension::Create_Units(bool official) continue; } + HouseClassExtension* hexptr = Extension::Fetch(hptr); + if (hexptr->IsObserver) { + DEV_DEBUG_INFO("House %d is an Observer, skipping.\n", house); + continue; + } + DynamicVectorClass available_infantry; DynamicVectorClass available_units; @@ -1314,6 +2557,11 @@ void ScenarioClassExtension::Create_Units(bool official) continue; } + /** + * Fetch the center cell for this house that we assigned earlier in Assign_Starting_Positions(). + */ + centroid = ScenExtension->StartingPositionCells[house]; + int owner_id = 1 << hptr->Class->ID; DEBUG_INFO("Generating units for house %d (Name: %s - \"%s\", Color: %s)...\n", @@ -1338,8 +2586,7 @@ void ScenarioClassExtension::Create_Units(bool official) * Check tech level and ownership. */ if (unittype->TechLevel <= hptr->Control.TechLevel && (owner_id & unittype->Ownable) != 0) { - - if (Rule->BaseUnit->Fetch_ID() != unittype->Fetch_ID()) { + if (!RuleExtension->BaseUnit.Is_Present(unittype)) { DEBUG_INFO(" Added %s\n", unittype->Name()); available_units.Add(unittype); } @@ -1372,74 +2619,6 @@ void ScenarioClassExtension::Create_Units(bool official) } } - /** - * Pick the starting location for this house. The first house just picks - * one of the valid locations at random. The other houses pick the furthest - * waypoint from the existing houses. - */ - if (numtaken == 0) { - int pick = Random_Pick(0, waypts.Count()-1); - centroid = waypts[pick]; - taken[pick] = true; - numtaken++; - - } else { - - /** - * Set all waypoints to have a score of zero in preparation for giving - * a distance score to all waypoints. - */ - int score[MAX_STORED_WAYPOINTS]; - std::memset(score, '\0', sizeof(score)); - - /** - * Scan through all waypoints and give a score as a value of the sum - * of the distances from this waypoint to all taken waypoints. - */ - for (int index = 0; index < waypts.Count(); index++) { - - /** - * If this waypoint has not already been taken, then accumulate the - * sum of the distance between this waypoint and all other taken - * waypoints. - */ - if (!taken[index]) { - for (int trypoint = 0; trypoint < waypts.Count(); trypoint++) { - - if (taken[trypoint]) { - score[index] += Distance(waypts[index], waypts[trypoint]); - } - } - } - } - - /** - * Now find the waypoint with the largest score. This waypoint is the one - * that is furthest from all other taken waypoints. - */ - int best = 0; - int bestvalue = 0; - for (int searchindex = 0; searchindex < waypts.Count(); searchindex++) { - if (score[searchindex] > bestvalue || bestvalue == 0) { - bestvalue = score[searchindex]; - best = searchindex; - } - } - - /** - * Assign this best position to the house. - */ - centroid = waypts[best]; - taken[best] = true; - numtaken++; - } - - /** - * Assign the center of this house to the waypoint location. - */ - hptr->Center = Cell_Coord(centroid, true); - DEBUG_INFO(" Setting house center to %d,%d\n", centroid.X, centroid.Y); - /** * If Bases are ON, place a base unit (MCV). */ @@ -1458,7 +2637,7 @@ void ScenarioClassExtension::Create_Units(bool official) /** * Create a construction yard (decided from the base unit). */ - obj = new BuildingClass(Rule->BaseUnit->DeploysInto, hptr); + obj = new BuildingClass(hptr->Get_First_Ownable(RuleExtension->BaseUnit)->DeploysInto, hptr); if (obj->Unlimbo(Cell_Coord(centroid, true), DIR_N) || Scan_Place_Object(obj, centroid)) { if (obj != nullptr) { DEBUG_INFO(" Construction yard %s placed at %d,%d.\n", @@ -1513,7 +2692,7 @@ void ScenarioClassExtension::Create_Units(bool official) * - Create an MCV * - Attach a flag to it for capture-the-flag mode. */ - obj = new UnitClass(Rule->BaseUnit, hptr); + obj = new UnitClass(hptr->Get_First_Ownable(RuleExtension->BaseUnit), hptr); if (obj->Unlimbo(Cell_Coord(centroid, true), DIR_N) || Scan_Place_Object(obj, centroid)) { if (obj != nullptr) { DEBUG_INFO(" Base unit %s placed at %d,%d.\n", diff --git a/src/extensions/scenario/scenarioext.h b/src/extensions/scenario/scenarioext.h index 315871237..6a21d3eef 100644 --- a/src/extensions/scenario/scenarioext.h +++ b/src/extensions/scenario/scenarioext.h @@ -30,68 +30,104 @@ #include "always.h" #include "extension.h" #include "scenario.h" +#include "wstring.h" class ScenarioClassExtension final : public GlobalExtensionClass { - public: - IFACEMETHOD(Load)(IStream *pStm); - IFACEMETHOD(Save)(IStream *pStm, BOOL fClearDirty); - - public: - ScenarioClassExtension(const ScenarioClass *this_ptr); - ScenarioClassExtension(const NoInitClass &noinit); - virtual ~ScenarioClassExtension(); - - virtual int Size_Of() const override; - virtual void Detach(TARGET target, bool all = true) override; - virtual void Compute_CRC(WWCRCEngine &crc) const override; - - virtual const char *Name() const override { return "Scenario"; } - virtual const char *Full_Name() const override { return "Scenario"; } - - void Init_Clear(); - bool Read_INI(CCINIClass &ini); - - bool Read_Tutorial_INI(CCINIClass &ini, bool log = false); - - Cell Get_Waypoint_Cell(WaypointType wp) const; - CellClass * Get_Waypoint_CellPtr(WaypointType wp) const; - Coordinate Get_Waypoint_Coord(WaypointType wp) const; - Coordinate Get_Waypoint_Coord_Height(WaypointType wp) const; - - void Set_Waypoint_Cell(WaypointType wp, Cell &cell); - void Set_Waypoint_Coord(WaypointType wp, Coordinate &coord); - - bool Is_Valid_Waypoint(WaypointType wp) const; - void Clear_Waypoint(WaypointType wp); - - void Clear_All_Waypoints(); - - void Read_Waypoint_INI(CCINIClass &ini); - void Write_Waypoint_INI(CCINIClass &ini); - - const char *Waypoint_As_String(WaypointType wp) const; - - static void Assign_Houses(); - static void Create_Units(bool official); - - public: - /** - * This is an vector of waypoints; each waypoint corresponds to a letter of - * the alphabet, and points to a cell position. - * - * The CellClass has a bit that tells if that cell has a waypoint attached to - * it; the only way to find which waypoint it is, is to scan this array. This - * shouldn't be needed often; usually, you know the waypoint & you want the "Cell". - */ - VectorClass Waypoint; - - /** - * Can ice get destroyed when hit by certain weapons? - */ - bool IsIceDestruction; - - RGBStruct ScorePlayerColor; - RGBStruct ScoreEnemyColor; +public: + IFACEMETHOD(Load)(IStream *pStm); + IFACEMETHOD(Save)(IStream *pStm, BOOL fClearDirty); + +public: + ScenarioClassExtension(const ScenarioClass *this_ptr); + ScenarioClassExtension(const NoInitClass &noinit); + virtual ~ScenarioClassExtension(); + + virtual int Size_Of() const override; + virtual void Detach(TARGET target, bool all = true) override; + virtual void Compute_CRC(WWCRCEngine &crc) const override; + + virtual const char *Name() const override { return "Scenario"; } + virtual const char *Full_Name() const override { return "Scenario"; } + + void Init_Clear(); + bool Read_INI(CCINIClass &ini); + + bool Read_Tutorial_INI(CCINIClass &ini, bool log = false); + + Cell Get_Waypoint_Cell(WaypointType wp) const; + CellClass * Get_Waypoint_CellPtr(WaypointType wp) const; + Coordinate Get_Waypoint_Coord(WaypointType wp) const; + Coordinate Get_Waypoint_Coord_Height(WaypointType wp) const; + + void Set_Waypoint_Cell(WaypointType wp, Cell &cell); + void Set_Waypoint_Coord(WaypointType wp, Coordinate &coord); + + bool Is_Valid_Waypoint(WaypointType wp) const; + void Clear_Waypoint(WaypointType wp); + + void Clear_All_Waypoints(); + + void Read_Waypoint_INI(CCINIClass &ini); + void Write_Waypoint_INI(CCINIClass &ini); + + const char *Waypoint_As_String(WaypointType wp) const; + + static bool Start_Scenario(char* name, bool briefing, CampaignType campaignid); + static bool Read_Scenario_INI(const char* root, bool); + static bool Load_Scenario(CCINIClass& ini, bool random = false); + static void Init_Forced_Alliances(); + + void Assign_Starting_Positions(bool official); + static void Assign_Houses(); + static void Create_Units(bool official); + bool Read_Loading_Screen_INI(const char* filename); + +public: + /** + * This is a vector of waypoints; each waypoint corresponds to a letter of + * the alphabet, and points to a cell position. + * + * The CellClass has a bit that tells if that cell has a waypoint attached to + * it; the only way to find which waypoint it is, is to scan this array. This + * shouldn't be needed often; usually, you know the waypoint & you want the "Cell". + */ + VectorClass Waypoint; + + /** + * Can ice get destroyed when hit by certain weapons? + */ + bool IsIceDestruction; + + RGBStruct ScorePlayerColor; + RGBStruct ScoreEnemyColor; + + /** + * The starting positions of the houses. + * StartingPositions[HousesType] = 0 .. 7 + */ + int StartingPositions[MAX_PLAYERS]; + Cell StartingPositionCells[MAX_PLAYERS]; + + /** + * The side to use for the sidebar assets (singleplayer only). + */ + SideType SidebarSide; + + /** + * Scenarios can override the loading screen with a custom variant, these + * define the filename to load and position overrides. + */ + struct LoadingScreenData { + Wstring Filename; + TPoint2D Position; + }; + + LoadingScreenData LoadingScreens[3]; + + /** + * Should the AI use base nodes outside of campaign, instead of skirmish AI base building logic. + */ + bool IsUseMPAIBaseNodes; }; diff --git a/src/extensions/scenario/scenarioext_hooks.cpp b/src/extensions/scenario/scenarioext_hooks.cpp index 16dc762ac..a921c6e4a 100644 --- a/src/extensions/scenario/scenarioext_hooks.cpp +++ b/src/extensions/scenario/scenarioext_hooks.cpp @@ -30,22 +30,46 @@ #include "scenarioext.h" #include "tibsun_functions.h" #include "tibsun_globals.h" +#include "tibsun_inline.h" +#include "house.h" +#include "housetype.h" +#include "unit.h" +#include "unittype.h" +#include "unittypeext.h" +#include "rules.h" +#include "rulesext.h" +#include "campaign.h" #include "multiscore.h" #include "scenario.h" +#include "scenarioext.h" #include "session.h" #include "rules.h" #include "ccfile.h" #include "ccini.h" #include "endgame.h" #include "addon.h" +#include "side.h" +#include "infantrytype.h" +#include "aircrafttype.h" +#include "buildingtype.h" +#include "progressscreen.h" +#include "language.h" +#include "wsproto.h" +#include "ownrdraw.h" +#include "spritecollection.h" #include "fatal.h" #include "debughandler.h" #include "asserthandler.h" +#include "campaignext.h" +#include "housetypeext.h" #include "hooker.h" #include "hooker_macros.h" #include "kamikazetracker.h" #include "mouse.h" +#include "reinf.h" +#include "spawner.h" +#include "teamtype.h" #include "vinifera_globals.h" @@ -98,6 +122,8 @@ DECLARE_PATCH(_Clear_Scenario_Patch) KamikazeTracker->Clear(); + Vinifera_ObserverPtr = nullptr; + JMP(0x005DC872); } @@ -204,63 +230,228 @@ int _Waypoint_From_Name(char* wp) } +enum { + LS_400, + LS_480, + LS_600, + LS_SIZE_COUNT, + LS_SIZE_FIRST = LS_400 +}; + + +struct LoadingScreenConfig +{ + int SizeIndex; + TPoint2D Size; + TPoint2D SPPosition; + TPoint2D MPPosition; +}; + + /** - * Process additions to the Rules data from the input file. - * - * @author: CCHyper + * This array contains the loading screen properties for different screen sizes. */ -static bool Rule_Addition(const char *fname, bool with_digest = false) +static LoadingScreenConfig LoadingScreenConfigs[] { - CCFileClass file(fname); - if (!file.Is_Available()) { - return false; - } - - CCINIClass ini; - if (!ini.Load(file, with_digest)) { - return false; - } + { LS_400, { 640, 400 }, { 436, 155 }, { 566, 152 } }, + { LS_480, { 640, 480 }, { 436, 186 }, { 566, 177 } }, + { LS_600, { 800, 600 }, { 546, 233 }, { 711, 227 } } +}; - DEBUG_INFO("Calling Rule->Addition() with \"%s\" overrides.\n", fname); - Rule->Addition(ini); +/** + * Gets the loading screen roperties for the user's screen size. + * + * @author: ZivDero + */ +static LoadingScreenConfig& Get_Loading_Screen_Config() +{ + for (int i = LS_SIZE_COUNT - 1; i > LS_SIZE_FIRST; i--) { + if (ScreenRect.Width >= LoadingScreenConfigs[i].Size.X && ScreenRect.Height >= LoadingScreenConfigs[i].Size.Y) { + return LoadingScreenConfigs[i]; + } + } - return true; + return LoadingScreenConfigs[LS_SIZE_FIRST]; } /** - * #issue-#671 - * - * Add loading of MPLAYER.INI to override Rules data for multiplayer games. + * Reimplements the loading screen setup routine. * - * @author: CCHyper + * @author: CCHyper, ZivDero */ -DECLARE_PATCH(_Read_Scenario_INI_MPlayer_INI_Patch) +static void Init_Loading_Screen(const char* filename) { - if (Session.Type != GAME_NORMAL && Session.Type != GAME_WDT) { + /** + * We need to read sides and houses now, because we need them to determine the player's + * side and loading screens. + */ + Rule->Sides(*RuleINI); + Rule->Houses(*RuleINI); + + for (int i = 0; i < HouseTypes.Count(); i++) + HouseTypes[i]->Read_INI(*RuleINI); + + for (int i = 0; i < HouseTypeExtensions.Count(); i++) + HouseTypeExtensions[i]->Read_INI(*RuleINI); + + /** + * #EDGE-CASE/#BUGFIX: + * + * We need to do the fixup even earlier now as we need to use the Side + * value from the players HouseType. + */ + { + CCFileClass file(filename); + CCINIClass ini(file); + RuleExtension->Fixups(ini); + } + + /** + * For the campaign, we check to see if the scenario name contains either + * "GDI" or "NOD", and then set the side to those respectively. + */ + HousesType house = HOUSE_GDI; + if (Session.Type == GAME_NORMAL) { + if (Scen->CampaignID != CAMPAIGN_NONE) { + + const auto campaign_ext = Extension::Fetch(Campaigns[Scen->CampaignID]); + house = campaign_ext->House; + } + } + else { /** - * Process the multiplayer ini overrides. + * The first player in the player array is always the local player, so + * fetch our player info and the house we are assigned as. */ - Rule_Addition("MPLAYER.INI"); - if (Is_Addon_Enabled(ADDON_FIRESTORM)) { - Rule_Addition("MPLAYERFS.INI"); + + HouseTypeClass* housetype = HouseTypes[Session.Players.Fetch_Head()->Player.House]; + house = housetype->House; + + /** + * Set the player's side. This would happen in Select_Game, but we + * do it here for the spawner, and to take advantage of fixups. + */ + reinterpret_cast(Session.IsGDI) = static_cast(housetype->Side) & 0xFF; + } + + /** + * Sanity check the side type. + */ + if (house == HOUSE_NONE || house >= HouseTypes.Count()) { + house = HOUSE_GDI; + } + + const char* loadname; + TPoint2D textpos; + + /** + * Fetch the loading screen properties for the user's screen size. + */ + LoadingScreenConfig& ls_config = Get_Loading_Screen_Config(); + + /** + * Fetch the according loading screen from the user's house's data. + */ + const auto housetype_ext = Extension::Fetch(HouseTypes[house]); + const auto& house_ls = housetype_ext->LoadingScreens[ls_config.SizeIndex]; + + loadname = house_ls[Sim_Random_Pick(0, house_ls.Count() - 1)].Peek_Buffer(); + textpos = Session.Singleplayer_Game() ? ls_config.SPPosition : ls_config.MPPosition; + + /** + * Adjust the text position for Nod. + */ + if (house == HOUSE_NOD) { + textpos.Y += 7; + } + + /** + * Fetch the loading screen override from the scenario. + */ + const auto& ls_override = ScenExtension->LoadingScreens[ls_config.SizeIndex]; + if (ls_override.Filename.Is_Not_Empty()) { + loadname = ls_override.Filename.Peek_Buffer(); + + if (ls_override.Position.Is_Valid()) { + textpos = ls_override.Position; } + } + /** + * Adjust the position of the text so it is correct for widescreen resolutions. + */ + textpos.X += (ScreenRect.Width - ls_config.Size.X) / 2; + textpos.Y += (ScreenRect.Height - ls_config.Size.Y) / 2; + + char loadfilename[PATH_MAX]; + std::snprintf(loadfilename, sizeof(loadfilename), "%s.PCX", loadname); + + /** + * The spawner can forcibly override the loading screen, and it already includes .PCX. + */ + if (Vinifera_SpawnerActive) { + + if (Wstring(Vinifera_SpawnerConfig->CustomLoadScreen).Is_Not_Empty()) { + std::snprintf(loadfilename, sizeof(loadfilename), "%s", Vinifera_SpawnerConfig->CustomLoadScreen); + + if (Vinifera_SpawnerConfig->CustomLoadScreenPos.Is_Valid()) { + textpos = Vinifera_SpawnerConfig->CustomLoadScreenPos; + } + } } + DEV_DEBUG_INFO("Loading Screen: \"%s\"\n", loadfilename); + /** - * Update the progress screen bars. + * If this is a tournament game, format the game id. */ - Session.Loading_Callback(42); + char gamenamebuffer[128]; + const char* gamename = nullptr; + + if (Session.Type == GAME_INTERNET && PlanetWestwoodTournament == WOL::TOURNAMENT_0) { + std::snprintf(gamenamebuffer, sizeof(gamenamebuffer), Text_String(TXT_GAME_ID), PlanetWestwoodGameID); + gamename = gamenamebuffer; + } /** - * Stolen bytes/code. + * Select the progress bar graphic depending on the game mode. + */ + const int player_count = Session.Singleplayer_Game() ? 1 : Session.Players.Count(); + const char* progress_name = player_count <= 1 ? "PROGBAR.SHP" : "PROGBARM.SHP"; + + /** + * Initialise the loading screen. */ - Call_Back(); + ProgressScreen.Init(100.0f, player_count); + + /** + * Forces the initial draw, Call_Back calls will update the redraw from here on. + */ + ProgressScreen.Draw_Graphics(progress_name, loadfilename, gamename, textpos.X, textpos.Y); + ProgressScreen.Draw_Bars_And_Text(); +} + + +/** + * Patch to intercept and replace the loading screen setup. + * + * @author: CCHyper + */ +DECLARE_PATCH(_Read_Scenario_Loading_Screen_Patch) +{ + LEA_STACK_STATIC(const char *, filename, esp, 0x50); + + ScenExtension->Read_Loading_Screen_INI(filename); + + Init_Loading_Screen(filename); - JMP(0x005DD8DA); + /** + * Jump to setting broadcast addresses. + */ + JMP(0x005DBD4A); } @@ -286,6 +477,7 @@ DECLARE_PATCH(_Do_Win_Skip_MPlayer_Score_Screen_Patch) JMP(0x005DC9DF); } + DECLARE_PATCH(_Do_Lose_Skip_MPlayer_Score_Screen_Patch) { /** @@ -301,41 +493,294 @@ DECLARE_PATCH(_Do_Lose_Skip_MPlayer_Score_Screen_Patch) } +#define SPAWN_HOUSE_OFFSET 50 + /** - * Main function for patching the hooks. + * Returns a house from a spawn house name. + * + * @author: ZivDero */ -void ScenarioClassExtension_Hooks() +HousesType Spawn_House_From_Name(const char* name) { + ASSERT(name != nullptr); + + int spawn_number; + /** - * Initialises the extended class. + * Try to read the house name as a spawn house name and extract its number. */ - ScenarioClassExtension_Init(); + if (std::sscanf(name, "Spawn%d", &spawn_number) == 1) { + + /** + * If we're successful, return a spawn house number. + */ + return static_cast(spawn_number - 1 + SPAWN_HOUSE_OFFSET); + } /** - * For compatibility with the TS Client we need to remove - * these two reimplementations as they conflict with the spawner. + * Fetch the house the normal way. */ -#if !defined(TS_CLIENT) + return HouseTypeClass::From_Name(name); +} + + +bool Is_Spawn_House(HousesType house) +{ + return house >= SPAWN_HOUSE_OFFSET && house < SPAWN_HOUSE_OFFSET + MAX_PLAYERS; +} + +/** + * Returns a house from a spawn house name or a normal house name. + * + * @author: ZivDero + */ +HousesType House_Or_Spawn_House_From_Name(const char* name) +{ /** - * Hooks in the new Assign_Houses() function. - * - * @author: CCHyper + * In campaigns, proceed as usual. */ - Patch_Call(0x005E08E3, &ScenarioClassExtension::Assign_Houses); + if (Session.Type == GAME_NORMAL) { + return HouseTypeClass::From_Name(name); + } /** - * #issue-338 - * - * Hooks in the new Create_Units() function. - * - * @author: CCHyper + * In skirmish/multiplayer, try to fetch a spawn house instead. */ - Patch_Call(0x005DD320, &ScenarioClassExtension::Create_Units); -#endif + return Spawn_House_From_Name(name); +} + + +/** + * Special unit version of House_Or_Spawn_House_From_Name that adds a + * null pointer to the unit vector if the house is not found. + * + * @author: ZivDero + */ +HousesType House_Or_Spawn_House_From_Name_Unit(const char* name) +{ + /** + * In campaigns, proceed as usual. + */ + if (Session.Type == GAME_NORMAL) { + return HouseTypeClass::From_Name(name); + } + + /** + * In skirmish/multiplayer, try to fetch a spawn house instead. + * If we couldn't find the spawn house, add a null pointer to the unit vector + * so that the "LinkedTo" numbers don't break. We'll remove these null pointers + * at the end. + */ + HousesType house = Spawn_House_From_Name(name); + if (house == HOUSE_NONE) { + Units.Add(nullptr); + } + + return house; +} + + +/** + * Returns a house pointer from a house type. + * + * @author: ZivDero + */ +HouseClass* HouseClass_As_Pointer(HousesType house) +{ + /** + * In campaigns, or if this isn't a spawn house, proceed as usual. + */ + if (Session.Type == GAME_NORMAL || !Is_Spawn_House(house)) { + + for (int i = 0; i < Houses.Count(); i++) { + if (Houses[i]->Class->House == house) { + return Houses[i]; + } + } + + return nullptr; + } + + /** + * For spawn houses, iterate all assigned starting positions and check if the one we want is present. + */ + for (int i = 0; i < Session.Players.Count() + Session.Options.AIPlayers; i++) { + + /** + * If it is, that's our desired house. + */ + if (ScenExtension->StartingPositions[i] == house - SPAWN_HOUSE_OFFSET) { + return Houses[i]; + } + } + + return nullptr; +} + + +/** + * Patch to fetch the spawn house for infantry during initial placement. + * + * @author: ZivDero + */ +DECLARE_PATCH(_InfantryClass_Read_INI_SpawnHouses_Patch) +{ + GET_REGISTER_STATIC(char*, house_name, eax); + + static HousesType house; + static HouseClass* hptr; + + house = House_Or_Spawn_House_From_Name(house_name); + + if (house != HOUSE_NONE) + { + hptr = HouseClass::As_Pointer(house); + + if (hptr) + { + _asm mov edi, hptr + JMP(0x004D7BD5); + } + } + + JMP(0x004D7F30); +} + + +/** + * Link units to their followers. + * + * @author: ZivDero + */ +static void Link_Units(DynamicVectorClass& link_vector) +{ + /** + * Links the followed and followed units, checking to make sure both actually exist. + */ + for (int i = 0; i < Units.Count(); ++i) + { + int follower_id = link_vector[i]; + UnitClass* unit = Units[i]; + + if (unit) { + + if (follower_id != -1 && follower_id < Units.Count() && Units[follower_id]) { + UnitClass* follower = Units[follower_id]; + unit->FollowingMe = follower; + follower->IsFollowing = true; + } + else { + unit->FollowingMe = nullptr; + } + } + } + + /** + * We need to remove the null pointers we added from the unit vector. + */ + for (int i = 0; i < Units.Count(); i++) { + if (!Units[i]) { + Units.Delete(i--); + } + } +} + + +/** + * Patch to link follower and followed units. + * + * @author: ZivDero + */ +DECLARE_PATCH(_UnitClass_Read_INI_Link_Units) +{ + LEA_STACK_STATIC(DynamicVectorClass*, link_vector, esp, 0xC); + + Link_Units(*link_vector); + + JMP(0x00658A10); +} + + +/** + * A fake class for implementing new member functions which allow + * access to the "this" pointer of the intended class. + * + * @note: This must not contain a constructor or destructor! + * @note: All functions must be prefixed with "_" to prevent accidental virtualization. + */ +static class CCINIClassExt final : public CCINIClass +{ +public: + HousesType _Get_HousesType(const char* section, const char* entry, const HousesType defvalue); +}; + + +/** + * A wrapper for CCINIClass::Get_HousesType to read SpawnX houses. + * + * @author: ZivDero + */ +HousesType CCINIClassExt::_Get_HousesType(const char* section, const char* entry, const HousesType defvalue) +{ + char buffer[128]; + + /** + * In campaigns, proceed as usual. + */ + if (Session.Type == GAME_NORMAL) { + return Get_HousesType(section, entry, defvalue); + } + + Get_String(section, entry, "", buffer, sizeof(buffer)); + + /** + * Try to fetch the spawn houses's index. + */ + return Spawn_House_From_Name(buffer); +} + + +/** + * A wrapper for Do_Reinforcements that checks if the team has a house. + * + * @author: ZivDero + */ +bool Do_Reinforcements_Wrapper(const TeamTypeClass* team, WaypointType wp = WAYPOINT_NONE) +{ + /** + * Since not all spawn houses are present, some teams may have null houses. Don't spawn these teams. + */ + if (team->House) { + return Do_Reinforcements(team, wp); + } + + return false; +} + + +/** + * Main function for patching the hooks. + */ +void ScenarioClassExtension_Hooks() +{ + /** + * Initialises the extended class. + */ + ScenarioClassExtension_Init(); + + /** + * Hooks for new scanario-related functions. + * + * @author: CCHyper, ZivDero + */ + Patch_Jump(0x005DB170, &ScenarioClassExtension::Start_Scenario); + //Patch_Jump(0x005DD100, &ScenarioClassExtension::Read_Scenario_INI); // Identical to vanilla for now, but missing a timer + Patch_Jump(0x005DD4C0, &ScenarioClassExtension::Load_Scenario); + Patch_Jump(0x005DE210, &ScenarioClassExtension::Assign_Houses); + Patch_Jump(0x005DE580, &ScenarioClassExtension::Create_Units); Patch_Jump(0x005DC9D4, &_Do_Win_Skip_MPlayer_Score_Screen_Patch); Patch_Jump(0x005DCD92, &_Do_Lose_Skip_MPlayer_Score_Screen_Patch); - Patch_Jump(0x005DD8D5, &_Read_Scenario_INI_MPlayer_INI_Patch); /** * #issue-71 @@ -360,4 +805,49 @@ void ScenarioClassExtension_Hooks() Patch_Jump(0x00673330, &_Waypoint_From_Name); Patch_Jump(0x006732B0, &_Waypoint_To_Name); // 0047A96C + + /** + * #issue-218 + * + * Changes the default value of ScenarioClass 0x1D91 (IsGDI) from "1" to "0". This is + * because we now use it as a HouseType index, and need it to default to the first index. + */ + Patch_Byte(0x005DAFD0+6, 0x00); // +6 skips the opcode. + + Patch_Jump(0x005DBA8B, &_Read_Scenario_Loading_Screen_Patch); + + /** + * Hooks for SpawnX houses. + */ + + /** + * Patch HouseClass::As_Pointer to return houses based on spawn positions for IDs 50-57. + */ + Patch_Jump(0x004C4730, &HouseClass_As_Pointer); + + /** + * Patch Unit, Building, Aircraft, Infatry and Team creation from the map to + * fetch Spawn houses by names correctly. + */ + Patch_Call(0x00658658, &House_Or_Spawn_House_From_Name_Unit); // UnitClass + Patch_Call(0x00434843, &House_Or_Spawn_House_From_Name); // BuildingClass + Patch_Call(0x0040E806, &House_Or_Spawn_House_From_Name); // AircraftClass + Patch_Jump(0x004D7B98, &_InfantryClass_Read_INI_SpawnHouses_Patch); // InfantryClass has As_Pointer inlined, so we have to do this instead + Patch_Call(0x00628600, &CCINIClassExt::_Get_HousesType); // TeamTypeClass + + /** + * Units have the followed mechanic, so we need to fix that up to account for potentially missing units. + */ + Patch_Jump(0x006589C8, &_UnitClass_Read_INI_Link_Units); + + /** + * Jump past check in BuildingClass::Read_INI() preventing multiplayer building spawning for players. + */ + Patch_Jump(0x0043485F, 0x00434874); + + /** + * Skip doing reinforcements if their receiver is non-existent. + */ + Patch_Call(0x0061C39A, &Do_Reinforcements_Wrapper); + Patch_Call(0x0061C3C1, &Do_Reinforcements_Wrapper); } diff --git a/src/extensions/scenario/scenarioext_init.cpp b/src/extensions/scenario/scenarioext_init.cpp index 335e8d314..60de9310e 100644 --- a/src/extensions/scenario/scenarioext_init.cpp +++ b/src/extensions/scenario/scenarioext_init.cpp @@ -29,6 +29,7 @@ #include "scenarioext.h" #include "scenario.h" #include "tibsun_globals.h" +#include "session.h" #include "vinifera_util.h" #include "extension.h" #include "extension_globals.h" @@ -135,30 +136,6 @@ DECLARE_PATCH(_ScenarioClass_Init_Clear_Patch) } -/** - * Patch for reading the extended class members from the ini instance. - * - * @warning: Do not touch this unless you know what you are doing! - * - * @author: CCHyper - */ -DECLARE_PATCH(_ScenarioClass_Read_INI_Patch) -{ - GET_REGISTER_STATIC(CCINIClass *, ini, ebp); - static bool retval; - - /** - * Stolen bytes/code. - */ - retval |= Scen->Read_INI(*ini); - - retval |= ScenExtension->Read_INI(*ini); - - _asm { mov al, retval } - JMP_REG(ecx, 0x005DD947); -} - - /** * Main function for patching the hooks. */ @@ -167,5 +144,4 @@ void ScenarioClassExtension_Init() Patch_Jump(0x005DADDE, &_ScenarioClass_Constructor_Patch); Patch_Jump(0x006023CC, &_ScenarioClass_Destructor_Patch); // Inlined in game shutdown. Patch_Jump(0x005DB166, &_ScenarioClass_Init_Clear_Patch); - Patch_Jump(0x005DD93B, &_ScenarioClass_Read_INI_Patch); } diff --git a/src/extensions/session/sessionext_hooks.cpp b/src/extensions/session/sessionext_hooks.cpp index bc63471ef..44fc54376 100644 --- a/src/extensions/session/sessionext_hooks.cpp +++ b/src/extensions/session/sessionext_hooks.cpp @@ -32,6 +32,9 @@ #include "debughandler.h" #include "asserthandler.h" +#include "hooker.h" +#include "hooker_macros.h" + /** * Main function for patching the hooks. @@ -42,4 +45,12 @@ void SessionClassExtension_Hooks() * Initialises the extended class. */ SessionClassExtension_Init(); + + /** + * #issue-218 + * + * Changes the default value of SessionClass 0x1D91 (IsGDI) from "1" to "0".. This is + * because we now use it as a HouseType index, and need it to default to the first index. + */ + Patch_Byte(0x005ED06B+1, 0x85); // changes "dl" (1) to "al" (0) } diff --git a/src/extensions/side/sideext.cpp b/src/extensions/side/sideext.cpp index 29d6ed824..6a7e39a70 100644 --- a/src/extensions/side/sideext.cpp +++ b/src/extensions/side/sideext.cpp @@ -46,6 +46,7 @@ SideClassExtension::SideClassExtension(const SideClass *this_ptr) : AbstractTypeClassExtension(this_ptr), UIColor(COLORSCHEME_NONE), ToolTipColor(COLORSCHEME_NONE), + OptionsColor{ 112, 255, 0 }, // 0x00FF70 Crew(nullptr), Engineer(nullptr), Technician(nullptr), @@ -235,6 +236,7 @@ bool SideClassExtension::Read_INI(CCINIClass &ini) UIColor = ini.Get_ColorSchemeType(ini_name, "UIColor", UIColor); ToolTipColor = ini.Get_ColorSchemeType(ini_name, "ToolTipColor", ToolTipColor); + OptionsColor = ini.Get_RGB(ini_name, "OptionsColor", OptionsColor); Crew = ini.Get_Infantry(ini_name, "Crew", Crew); Engineer = ini.Get_Infantry(ini_name, "Engineer", Engineer); diff --git a/src/extensions/side/sideext.h b/src/extensions/side/sideext.h index 203d98050..071ccb12e 100644 --- a/src/extensions/side/sideext.h +++ b/src/extensions/side/sideext.h @@ -90,6 +90,11 @@ SideClassExtension final : public AbstractTypeClassExtension */ ColorSchemeType ToolTipColor; + /** + * RGB color used by the options menu. + */ + RGBStruct OptionsColor; + /** * InfantryType used as this Side's crew. */ diff --git a/src/extensions/taction/tactionext_hooks.cpp b/src/extensions/taction/tactionext_hooks.cpp index aceee7520..6d2d905c6 100644 --- a/src/extensions/taction/tactionext_hooks.cpp +++ b/src/extensions/taction/tactionext_hooks.cpp @@ -40,257 +40,1365 @@ #include "house.h" #include "housetype.h" #include "session.h" +#include "object.h" +#include "tagtype.h" +#include "rules.h" #include "hooker.h" #include "hooker_macros.h" +#include "options.h" +#include "tag.h" +#include "techno.h" +#include "vinifera_globals.h" + + +/** + * Helper info for writing new actions. + * + * TActionClass::Data = First Param (PARAM1) + * TActionClass::Bounds.X = Second Param (PARAM2) + * TActionClass::Bounds.Y = Third Param (PARAM3) + * TActionClass::Bounds.W = Fourth Param (PARAM4) + * TActionClass::Bounds.H = Fifth Param (PARAM5) + * + * (PARAM6) (OPTIONAL) + * if TActionFormatType == 4 + * TActionClass::Data (overwrites PARAM1) + * else + * TActionClass::Location + * + * + * Example action line from a scenario file; + * + * [Actions] + * NAME = [Action Count], [TActionType], [TActionFormatType], [PARAM1], [PARAM2], [PARAM3], [PARAM4], [PARAM5], [PARAM6:OPTIONAL] + * + * To allow the use of TActionClass::Data (PARAM1), you must have the TActionFormatType set + * to "0", otherwise this param is ignored! + * + * + * For producing FinalSun [Action] entries; + * NOTE: For available ParamTypes, see the [ParamTypes] section in FSData.INI. + * NOTE: "DEF_PARAM1_VALUE" if negative (-ve), PARAM1 will be set to the absolute value of this number (filled in). + * + * [Actions] + * TActionType = [Name], [DEF_PARAM1_VALUE], [PARAM1_TYPE], [PARAM2_TYPE], [PARAM3_TYPE], [PARAM4_TYPE], [PARAM5_TYPE], [PARAM6_TYPE], [USE_WP], [USE_TAG], [Description], 1, 0, [TActionType] + */ /** * A fake class for implementing new member functions which allow * access to the "this" pointer of the intended class. * - * @note: This must not contain a constructor or destructor! - * @note: All functions must be prefixed with "_" to prevent accidental virtualization. + * @note: This must not contain a constructor or destructor! + * @note: All functions must be prefixed with "_" to prevent accidental virtualization. + */ +class TActionClassExt final : public TActionClass +{ +public: + enum { + TACTION_GIVE_CREDITS = TACTION_COUNT, + TACTION_ENABLE_SHORT_GAME, + TACTION_DISABLE_SHORT_GAME, + TACTION_UNUSED1, + TACTION_BLOWUP_HOUSE, + TACTION_MAKE_ELITE, + TACTION_ENABLE_ALLYREVEAL, + TACTION_DISABLE_ALLYREVEAL, + TACTION_CREATE_AUTOSAVE, + TACTION_DELETE_OBJECT, + TACTION_ALL_ASSIGN_MISSION, + + TACTION_NEW_COUNT + }; + +public: + bool _Function_Call_Operator(HouseClass* house, ObjectClass* object, TriggerClass* trig, Cell& cell); + + bool _TAction_Play_Sound_At_Random_Waypoint(HouseClass* house, ObjectClass* object, TriggerClass* trig, Cell& cell); + bool _TAction_Destroy_Trigger(HouseClass* house, ObjectClass* object, TriggerClass* trig, Cell& cell); + bool _TAction_Enable_Trigger(HouseClass* house, ObjectClass* object, TriggerClass* trig, Cell& cell); + bool _TAction_Win(HouseClass* house, ObjectClass* object, TriggerClass* trig, Cell& cell); + bool _TAction_Lose(HouseClass* house, ObjectClass* object, TriggerClass* trig, Cell& cell); + bool _TAction_Change_House(HouseClass* house, ObjectClass* object, TriggerClass* trig, Cell& cell); + bool _TAction_All_Change_House(HouseClass* house, ObjectClass* object, TriggerClass* trig, Cell& cell); + bool _TAction_Make_Ally(HouseClass* house, ObjectClass* object, TriggerClass* trig, Cell& cell); + bool _TAction_Make_Enemy(HouseClass* house, ObjectClass* object, TriggerClass* trig, Cell& cell); + bool _TAction_Begin_Production(HouseClass* house, ObjectClass* object, TriggerClass* trig, Cell& cell); + bool _TAction_Fire_Sale(HouseClass* house, ObjectClass* object, TriggerClass* trig, Cell& cell); + bool _TAction_Begin_Autocreate(HouseClass* house, ObjectClass* object, TriggerClass* trig, Cell& cell); + bool _TAction_All_Hunt(HouseClass* house, ObjectClass* object, TriggerClass* trig, Cell& cell); + bool _TAction_Set_AI_Triggers_Begin(HouseClass* house, ObjectClass* object, TriggerClass* trig, Cell& cell); + bool _TAction_Set_AI_Triggers_End(HouseClass* house, ObjectClass* object, TriggerClass* trig, Cell& cell); + + bool _TAction_Give_Credits(HouseClass* house, ObjectClass* object, TriggerClass* trig, Cell& cell); + bool _TAction_Enable_Short_Game(HouseClass* house, ObjectClass* object, TriggerClass* trig, Cell& cell); + bool _TAction_Disable_Short_Game(HouseClass* house, ObjectClass* object, TriggerClass* trig, Cell& cell); + bool _TAction_Blowup_House(HouseClass* house, ObjectClass* object, TriggerClass* trig, Cell& cell); + bool _TAction_Make_Elite(HouseClass* house, ObjectClass* object, TriggerClass* trig, Cell& cell); + bool _TAction_Enable_AllyReveal(HouseClass* house, ObjectClass* object, TriggerClass* trig, Cell& cell); + bool _TAction_Disable_AllyReveal(HouseClass* house, ObjectClass* object, TriggerClass* trig, Cell& cell); + bool _TAction_Create_AutoSave(HouseClass* house, ObjectClass* object, TriggerClass* trig, Cell& cell); + bool _TAction_Delete_Object(HouseClass* house, ObjectClass* object, TriggerClass* trig, Cell& cell); + bool _TAction_All_Assign_Mission(HouseClass* house, ObjectClass* object, TriggerClass* trig, Cell& cell); +}; + + +bool TActionClassExt::_Function_Call_Operator(HouseClass* house, ObjectClass* object, TriggerClass* trig, Cell& cell) +{ + /** + * Take an appropriate action. + */ + bool success = true; + + /** + * Ensure that the specified object is not actually dead. A dead object could + * be passed to this routine in the case of a multiple event trigger that + * had the first event kill the object. + */ + if (object && !object->IsActive) { + object = nullptr; + } + + switch (Action) { + + /** + * Flag the house specified as the winner. Really the house value + * is only used to determine if it is the player or the computer. + */ + case TACTION_WIN: + success = TAction_Win(house, object, trig, cell); + break; + + /** + * Flag the house specified as the loser. The house parameter is only + * used to determine if it refers to the player or the computer. + */ + case TACTION_LOSE: + success = TAction_Lose(house, object, trig, cell); + break; + + /** + * This will enable production to begin for the house specified. + */ + case TACTION_BEGIN_PRODUCTION: + success = TAction_Begin_Production(house, object, trig, cell); + break; + + /** + * Manually create the team specified. + */ + case TACTION_CREATE_TEAM: + success = TAction_Create_Team(house, object, trig, cell); + break; + + /** + * Destroy all teams of the type specified. + */ + case TACTION_DESTROY_TEAM: + success = TAction_Destroy_Team(house, object, trig, cell); + break; + + /** + * Force all units of the house specified to go into + * hunt mode. + */ + case TACTION_ALL_HUNT: + success = TAction_All_Hunt(house, object, trig, cell); + break; + + /** + * Create a reinforcement of the team specified. + */ + case TACTION_REINFORCEMENTS: + success = TAction_Reinforcements(house, object, trig, cell); + break; + + /** + * Place a smoke marker at the waypoint specified. + */ + case TACTION_DZ: + success = TAction_Drop_Zone_Flare(house, object, trig, cell); + break; + + /** + * Cause all buildings to be sold and all units to go into + * hunt mode. + */ + case TACTION_FIRE_SALE: + success = TAction_Fire_Sale(house, object, trig, cell); + break; + + /** + * Play a movie immediately. The game is temporarily + * suspended while the movie plays. + */ + case TACTION_PLAY_MOVIE: + success = TAction_Play_Movie(house, object, trig, cell); + break; + + /** + * Display a text message overlayed onto the tactical map. + */ + case TACTION_TEXT_TRIGGER: + success = TAction_Text_Trigger(house, object, trig, cell); + break; + + /** + * Destroying a trigger means that all triggers of that type will be destroyed. + */ + case TACTION_DESTROY_TRIGGER: + success = TAction_Destroy_Trigger(house, object, trig, cell); + break; + + /** + * Begin the team autocreate logic for the house specified. + */ + case TACTION_AUTOCREATE: + success = TAction_Begin_Autocreate(house, object, trig, cell);; + break; + + /** + * Change the house of the attached object. + */ + case TACTION_CHANGE_HOUSE: + success = TAction_Change_House(house, object, trig, cell); + break; + + /** + * Reveal the entire map. + */ + case TACTION_REVEAL_ALL: + success = TAction_Reveal_Map(house, object, trig, cell); + break; + + /** + * Reveal the map around the area specified. + */ + case TACTION_REVEAL_SOME: + success = TAction_Reveal_Area(house, object, trig, cell); + break; + + /** + * Reveal all cells of the zone that the specified waypoint is located + * in. This can be used to reveal whole islands or bodies of water + */ + case TACTION_REVEAL_ZONE: + success = TAction_Reveal_Zone(house, object, trig, cell); + break; + + /** + * Play a sound effect. + */ + case TACTION_PLAY_SOUND: + success = TAction_Play_Sound(house, object, trig, cell); + break; + + /** + * Play a musical theme. + */ + case TACTION_PLAY_MUSIC: + success = TAction_Play_Music(house, object, trig, cell); + break; + + /** + * Play the speech data specified. + */ + case TACTION_PLAY_SPEECH: + success = TAction_Play_Speech(house, object, trig, cell); + break; + + /** + * A forced trigger will force an existing trigger of that type or + * will create a trigger of that type and then force it to be sprung. + */ + case TACTION_FORCE_TRIGGER: + success = TAction_Force_Trigger(house, object, trig, cell); + break; + + /** + * Star the mission timer. + */ + case TACTION_START_TIMER: + success = TAction_Start_Timer(house, object, trig, cell); + break; + + /** + * Stop the mission timer. This will really just + * suspend the timer. + */ + case TACTION_STOP_TIMER: + success = TAction_Stop_Timer(house, object, trig, cell); + break; + + /** + * Add time to the mission timer. + */ + case TACTION_ADD_TIMER: + success = TAction_Add_Timer(house, object, trig, cell); + break; + + /** + * Remove time from the mission timer. + */ + case TACTION_SUB_TIMER: + success = TAction_Sub_Timer(house, object, trig, cell); + break; + + /** + * Set the mission timer to the value specified. + */ + case TACTION_SET_TIMER: + success = TAction_Set_Timer(house, object, trig, cell); + break; + + /** + * Set a scenario global. + */ + case TACTION_SET_GLOBAL: + success = TAction_Global_Set(house, object, trig, cell); + break; + + /** + * Clear a scenario global. + */ + case TACTION_GLOBAL_CLEAR: + success = TAction_Global_Clear(house, object, trig, cell); + break; + + /** + * Initiate (or disable) the computer AI. When active, the computer will + * build bases and units. + */ + case TACTION_BASE_BUILDING: + success = TAction_Base_Building(house, object, trig, cell); + break; + + /** + * Cause the shadow to creep back one step. + */ + case TACTION_CREEP_SHADOW: + success = TAction_Creep_Shadow(house, object, trig, cell); + break; + + /** + * This will destroy all objects that this trigger is + * attached to. + */ + case TACTION_DESTROY_OBJECT: + success = TAction_Destroy_Object(house, object, trig, cell); + break; + + /** + * Give a one-time special weapon to the house. + */ + case TACTION_1_SPECIAL: + success = TAction_One_Time_Special(house, object, trig, cell); + break; + + /** + * Give the special weapon to the house. + */ + case TACTION_FULL_SPECIAL: + success = TAction_Full_Special(house, object, trig, cell); + break; + + /** + * Set the preferred target for the house. + */ + case TACTION_PREFERRED_TARGET: + success = TAction_Preferred_Target(house, object, trig, cell); + break; + + case TACTION_ALL_CHANGE_HOUSE: + success = TAction_All_Change_House(house, object, trig, cell); + break; + + + case TACTION_MAKE_ALLY: + success = TAction_Make_Ally(house, object, trig, cell); + break; + + + case TACTION_MAKE_ENEMY: + success = TAction_Make_Enemy(house, object, trig, cell); + break; + + + case TACTION_CHANGE_ZOOM_LEVEL: + success = TAction_Change_Zoom_Level(house, object, trig, cell); + break; + + + case TACTION_RESIZE_VIEW: + success = TAction_Resize_Player_View(house, object, trig, cell); + break; + + + case TACTION_PLAY_ANIM: + success = TAction_Play_Anim_At(house, object, trig, cell); + break; + + + case TACTION_EXPLOSION: + success = TAction_Do_Explosion_At(house, object, trig, cell); + break; + + + case TACTION_METEOR_IMPACT: + success = TAction_Meteor_Impact_At(house, object, trig, cell); + break; + + + case TACTION_ION_STORM_START: + success = TAction_Ion_Storm_Start(house, object, trig, cell); + break; + + + case TACTION_ION_STORM_STOP: + success = TAction_Ion_Storm_End(house, object, trig, cell); + break; + + + case TACTION_LOCK_INPUT: + success = TAction_Lock_Input(house, object, trig, cell); + break; + + + case TACTION_UNLOCK_INPUT: + success = TAction_Unlock_Input(house, object, trig, cell); + break; + + + case TACTION_CENTER_CAMERA: + success = TAction_Center_Camera_At(house, object, trig, cell); + break; + + + case TACTION_ZOOM_IN: + success = TAction_Zoom_In(house, object, trig, cell); + break; + + + case TACTION_ZOOM_OUT: + success = TAction_Zoom_Out(house, object, trig, cell); + break; + + + case TACTION_RESHROUD_MAP: + success = TAction_Reshroud_Map(house, object, trig, cell); + break; + + + case TACTION_CHANGE_SPOTLIGHT_BEHAVIOR: + success = TAction_Change_Spotlight_Behavior(house, object, trig, cell); + break; + + + case TACTION_ENABLE_TRIGGER: + success = TAction_Enable_Trigger(house, object, trig, cell); + break; + + + case TACTION_DISABLE_TRIGGER: + success = TAction_Disable_Trigger(house, object, trig, cell); + break; + + + case TACTION_CREATE_RADAR_EVENT: + success = TAction_Create_Radar_Event(house, object, trig, cell); + break; + + + case TACTION_LOCAL_SET: + success = TAction_Local_Set(house, object, trig, cell); + break; + + + case TACTION_LOCAL_CLEAR: + success = TAction_Local_Clear(house, object, trig, cell); + break; + + + case TACTION_METEOR_SHOWER: + success = TAction_Meteor_Shower_At(house, object, trig, cell); + break; + + + case TACTION_REDUCE_TIBERIUM: + success = TAction_Reduce_Tiberium_At(house, object, trig, cell); + break; + + + case TACTION_SELL_BUILDING: + success = TAction_Sell_Building(house, object, trig, cell); + break; + + + case TACTION_TURN_OFF_BUILDING: + success = TAction_Turn_Off_Building(house, object, trig, cell); + break; + + + case TACTION_TURN_ON_BUILDING: + success = TAction_Turn_On_Building(house, object, trig, cell); + break; + + + case TACTION_APPLY_100_DAMAGE: + success = TAction_Apply_100_Damage(house, object, trig, cell); + break; + + + case TACTION_LIGHT_FLASH_SMALL: + success = TAction_Small_Light_Flash_At(house, object, trig, cell); + break; + + + case TACTION_LIGHT_FLASH_MEDIUM: + success = TAction_Medium_Light_Flash_At(house, object, trig, cell); + break; + + + case TACTION_LIGHT_FLASH_LARGE: + success = TAction_Large_Light_Flash_At(house, object, trig, cell); + break; + + + case TACTION_ANNOUNCE_WIN: + success = TAction_Annouce_Win(house, object, trig, cell); + break; + + + case TACTION_ANNOUNCE_LOSE: + success = TAction_Annouce_Lose(house, object, trig, cell); + break; + + + case TACTION_FORCE_END: + success = TAction_Force_End(house, object, trig, cell); + break; + + + case TACTION_DESTROY_TAG: + success = TAction_Destroy_Tag(house, object, trig, cell); + break; + + + case TACTION_SET_AMBIENT_STEP: + success = TAction_Set_Ambient_Step(house, object, trig, cell); + break; + + + case TACTION_SET_AMBIENT_RATE: + success = TAction_Set_Ambient_Rate(house, object, trig, cell); + break; + + + case TACTION_SET_AMBIENT_LIGHT: + success = TAction_Set_Ambient_Light(house, object, trig, cell); + break; + + + case TACTION_AI_TRIGGERS_BEGIN: + success = TAction_Set_AI_Triggers_Begin(house, object, trig, cell); + break; + + + case TACTION_AI_TRIGGERS_END: + success = TAction_Set_AI_Triggers_End(house, object, trig, cell); + break; + + + case TACTION_RATIO_AI_TRIGGER_TEAMS: + success = TAction_Set_Ratio_Of_AI_Trigger_Teams(house, object, trig, cell); + break; + + + case TACTION_SET_TEAM_AIRCRAFT_RATIO: + success = TAction_Set_Ratio_Of_Team_Aircraft(house, object, trig, cell); + break; + + + case TACTION_SET_TEAM_INFANTRY_RATIO: + success = TAction_Set_Ratio_Of_Team_Infantry(house, object, trig, cell); + break; + + + case TACTION_SET_TEAM_UNIT_RATIO: + success = TAction_Set_Ratio_Of_Team_Units(house, object, trig, cell); + break; + + + case TACTION_REINFORCEMENTS_AT: + success = TAction_Reinforcement_At(house, object, trig, cell); + break; + + + case TACTION_WAKEUP_SELF: + success = TAction_Wakeup_Self(house, object, trig, cell); + break; + + + case TACTION_WAKEUP_ALL_SLEEPERS: + success = TAction_Wakeup_Sleepers(house, object, trig, cell); + break; + + + case TACTION_WAKEUP_ALL_HARMLESS: + success = TAction_Wakeup_Harmless(house, object, trig, cell); + break; + + + case TACTION_WAKEUP_GROUP: + success = TAction_Wakeup_Group(house, object, trig, cell); + break; + + + case TACTION_VEIN_GROWTH: + success = TAction_Vein_Growth(house, object, trig, cell); + break; + + + case TACTION_TIBERIUM_GROWTH: + success = TAction_Tiberium_Growth(house, object, trig, cell); + break; + + + case TACTION_ICE_GROWTH: + success = TAction_Ice_Growth(house, object, trig, cell); + break; + + + case TACTION_PARTICLE_ANIM_AT: + success = TAction_Particle_Anim_At(house, object, trig, cell); + break; + + + case TACTION_REMOVE_PARTICLE_AT: + success = TAction_Remove_Particle_Anim_At(house, object, trig, cell); + break; + + + case TACTION_LIGHTENING_STRIKE: + success = TAction_Lightning_Strike_At(house, object, trig, cell); + break; + + + case TACTION_GO_BERZERK: + success = TAction_Go_Bezerk(house, object, trig, cell); + break; + + + case TACTION_ACTIVATE_FIRESTORM: + success = TAction_Activate_Firestorm_Defense(house, object, trig, cell); + break; + + + case TACTION_DEACTIVATE_FIRESTORM: + success = TAction_Deactivate_Firestorm_Defense(house, object, trig, cell); + break; + + + case TACTION_ION_CANNON_STRIKE: + success = TAction_Ion_Cannon_Strike(house, object, trig, cell); + break; + + + case TACTION_NUKE_STRIKE: + success = TAction_Nuke_Strike(house, object, trig, cell); + break; + + + case TACTION_CHEM_MISSILE_STRIKE: + success = TAction_Chemical_Missile_Strike(house, object, trig, cell); + break; + + + case TACTION_TOGGLE_TRAIN_CARGO: + success = TAction_Toggle_Train_Cargo(house, object, trig, cell); + break; + + + case TACTION_PLAY_RANDOM_SOUND_EFFECT: + success = TAction_Play_Sound_At_Random_Waypoint(house, object, trig, cell); + break; + + + case TACTION_PLAY_SOUND_EFFECT_AT: + success = TAction_Play_Sound_At(house, object, trig, cell); + break; + + + case TACTION_PLAY_INGAME_MOVIE: + success = TAction_Play_Ingame_Movie(house, object, trig, cell); + break; + + + case TACTION_FLASH_TEAM: + success = TAction_Flash_Team(house, object, trig, cell); + break; + + + case TACTION_DISABLE_SPEECH: + success = TAction_Disable_Speech(house, object, trig, cell); + break; + + + case TACTION_ENABLE_SPEECH: + success = TAction_Enable_Speech(house, object, trig, cell); + break; + + + case TACTION_SET_GROUP_ID: + success = TAction_Set_Group_ID(house, object, trig, cell); + break; + + + case TACTION_TALK_BUBBLE: + success = TAction_Talk_Bubble(house, object, trig, cell); + break; + + /** + * + * New Vinifera actions. + * + */ + + case TACTION_GIVE_CREDITS: + success = _TAction_Give_Credits(house, object, trig, cell); + break; + + + case TACTION_ENABLE_SHORT_GAME: + success = _TAction_Enable_Short_Game(house, object, trig, cell); + break; + + case TACTION_DISABLE_SHORT_GAME: + success = _TAction_Disable_Short_Game(house, object, trig, cell); + break; + + case TACTION_BLOWUP_HOUSE: + success = _TAction_Blowup_House(house, object, trig, cell); + break; + + case TACTION_MAKE_ELITE: + success = _TAction_Make_Elite(house, object, trig, cell); + break; + + case TACTION_ENABLE_ALLYREVEAL: + success = _TAction_Enable_AllyReveal(house, object, trig, cell); + break; + + case TACTION_DISABLE_ALLYREVEAL: + success = _TAction_Disable_AllyReveal(house, object, trig, cell); + break; + + case TACTION_CREATE_AUTOSAVE: + success = _TAction_Create_AutoSave(house, object, trig, cell); + break; + + case TACTION_DELETE_OBJECT: + success = _TAction_Delete_Object(house, object, trig, cell); + break; + + case TACTION_ALL_ASSIGN_MISSION: + success = _TAction_All_Assign_Mission(house, object, trig, cell); + break; + + /** + * Do no action at all. + */ + case TACTION_UNUSED1: + case TACTION_NONE: + default: + break; + } + + return success; +} + + +/** + * #issue-71 + * + * Reimplement Play_Sound_At_Random_Waypoint to support the new waypoint limit. + * + * @author: CCHyper + */ +bool TActionClassExt::_TAction_Play_Sound_At_Random_Waypoint(HouseClass* house, ObjectClass* object, TriggerClass* trig, Cell& cell) +{ + Cell cell_list[NEW_WAYPOINT_COUNT]; + int cell_list_count = 0; + + /** + * Make a list of all the valid waypoints in this scenario. + */ + for (WaypointType wp = WAYPOINT_FIRST; wp < NEW_WAYPOINT_COUNT; ++wp) { + if (ScenExtension->Is_Valid_Waypoint(wp)) { + cell_list[cell_list_count++] = ScenExtension->Get_Waypoint_Cell(wp); + if (cell_list_count >= std::size(cell_list)) { + break; + } + } + } + + /** + * Pick a random cell from the valid waypoint list and play the desired sound. + */ + Cell rnd_cell = cell_list[Random_Pick(0, std::size(cell_list) - 1)]; + + Sound_Effect(Data.Sound, Cell_Coord(rnd_cell, true)); + + return true; +} + + +/** + * Fixes a crash when a trigger deleted itself. + * + * @author: ZivDero + */ +bool TActionClassExt::_TAction_Destroy_Trigger(HouseClass* house, ObjectClass* object, TriggerClass* trig, Cell& cell) +{ + if (Trigger) { + for (int i = 0; i < Triggers.Count(); i++) { + auto trigger = Triggers[i]; + + /** + * Don't allow deleting itself. + */ + if (trigger && trigger != trig && trigger->Class == Trigger) { + + delete trigger; + i--; + } + } + } + + return true; +} + + +/** + * #issue-299 + * + * Fixes the issue with the current difficulty not being checked + * when enabling triggers. + * + * @see: TriggerClass and TriggerTypeClass for the other parts of this fix. + * + * @author: CCHyper + */ +bool TActionClassExt::_TAction_Enable_Trigger(HouseClass* house, ObjectClass* object, TriggerClass* trig, Cell& cell) +{ + if (Trigger) { + for (int i = 0; i < Triggers.Count(); i++) { + auto trigger = Triggers[i]; + + if (trigger->Class == Trigger) { + + /** + * Only enable the trigger if it's marked as enabled for this difficulty. + */ + if (Scen->Difficulty == DIFF_EASY && trigger->Class->Easy + || Scen->Difficulty == DIFF_NORMAL && trigger->Class->Normal + || Scen->Difficulty == DIFF_HARD && trigger->Class->Hard) { + + trigger->Enable(); + } + } + } + } + + return true; +} + + +/** + * #issue-965 + * + * Makes the "Winner is" trigger action set the IsDefeated flag on losing + * houses in multiplayer. + * + * @author: Rampastring */ -class TActionClassExt final : public TActionClass +bool TActionClassExt::_TAction_Win(HouseClass* house, ObjectClass* object, TriggerClass* trig, Cell& cell) { - public: - bool _Play_Sound_At_Random_Waypoint(HouseClass *house, ObjectClass *object, TriggerClass *trigger, Cell &cell); -}; + /** + * Flag the house specified as the loser. The house parameter is only + * used to determine if it refers to the player or the computer. + */ + if (Data.House == PlayerPtr->Class->House) { + PlayerPtr->Flag_To_Lose(false); + } + else { + PlayerPtr->Flag_To_Win(false); + } + + /** + * Outside of campaign, mark all losers as defeated. + */ + if (Session.Type != GAME_NORMAL) { + for (int i = 0; i < Houses.Count(); i++) { + HouseClass* house = Houses[i]; + + if (house->Class->House == Data.House) { + house->IsDefeated = true; + } + } + } + + return true; +} /** - * #issue-71 + * #issue-965 * - * Reimplement Play_Sound_At_Random_Waypoint to support the new waypoint limit. + * Makes the "Loser is" trigger action set the IsDefeated flag on the + * losing house in multiplayer. * - * @author: CCHyper + * @author: Rampastring */ -bool TActionClassExt::_Play_Sound_At_Random_Waypoint(HouseClass *house, ObjectClass *object, TriggerClass *trigger, Cell &cell) +bool TActionClassExt::_TAction_Lose(HouseClass* house, ObjectClass* object, TriggerClass* trig, Cell& cell) { - Cell cell_list[NEW_WAYPOINT_COUNT]; - int cell_list_count = 0; + /** + * Flag the house specified as the winner. Really the house value + * is only used to determine if it is the player or the computer. + */ + if (Data.House == PlayerPtr->Class->House) { + PlayerPtr->Flag_To_Win(false); + } + else { + PlayerPtr->Flag_To_Lose(false); + } /** - * Make a list of all the valid waypoints in this scenario. + * Outside of campaign, mark all houses other than the winner as defeated. */ - for (WaypointType wp = WAYPOINT_FIRST; wp < NEW_WAYPOINT_COUNT; ++wp) { - if (ScenExtension->Is_Valid_Waypoint(wp)) { - cell_list[cell_list_count++] = ScenExtension->Get_Waypoint_Cell(wp); - if (cell_list_count >= std::size(cell_list)) { - break; + if (Session.Type != GAME_NORMAL) { + for (int i = 0; i < Houses.Count(); i++) { + HouseClass* house = Houses[i]; + + if (house->Class->House != Data.House) { + house->IsDefeated = true; } } } + return true; +} + + +/** + * Replacement of TAction_Change_House to handle the case when the target house does not exist. + * + * @author: ZivDero + */ +bool TActionClassExt::_TAction_Change_House(HouseClass* house, ObjectClass* object, TriggerClass* trig, Cell& cell) +{ + bool success = false; + + HouseClass* newhouse = HouseClass::As_Pointer(Data.House); + /** - * Pick a random cell from the valid waypoint list and play the desired sound. + * Fix: check if the house exists, since a spawn house might not. */ - Cell rnd_cell = cell_list[Random_Pick(0, std::size(cell_list) - 1)]; + if (newhouse) { + for (int i = 0; i < Technos.Count(); i++) { + TechnoClass* techno = Technos[i]; - Sound_Effect(Data.Sound, Cell_Coord(rnd_cell, true)); + if (techno->IsActive && techno->IsDown && !techno->IsInLimbo) { + if (techno->Tag && techno->Tag->Is_Trigger_Attached(trig)) { - return true; + techno->Captured(newhouse); + success = true; + } + } + } + } + + return success; } /** - * #issue-71 - * - * Replace inlined instance of Play_Sound_At_Random_Waypoint. + * Replacement of TAction_All_Change_House to handle the case when the target house does not exist. * - * @author: CCHyper + * @author: ZivDero */ -DECLARE_PATCH(_TActionClass_Operator_Play_Sound_At_Random_Waypoint_Remove_Inline_Patch) +bool TActionClassExt::_TAction_All_Change_House(HouseClass* house, ObjectClass* object, TriggerClass* trig, Cell& cell) { - GET_REGISTER_STATIC(TActionClass *, this_ptr, esi); - GET_REGISTER_STATIC(ObjectClass *, object, ecx); - GET_STACK_STATIC(Cell *, cell, esp, 0x1D0); - GET_STACK_STATIC(TriggerClass *, trigger, esp, 0x1CC); - //GET_STACK_STATIC(ObjectClass *, object, esp, 0x1C8); // Use ECX instead. - GET_STACK_STATIC(HouseClass *, house, esp, 0x1C4); - static bool retval; + bool success = false; - retval = this_ptr->TAction_Play_Sound_At_Random_Waypoint(house, object, trigger, *cell); + HouseClass* newhouse = HouseClass::As_Pointer(Data.House); /** - * Function return. + * Fix: check if the house exists, since a spawn house might not. */ -return_true: - _asm { mov al, retval } - JMP_REG(ecx, 0x0061A9C5); + if (newhouse) { + for (int i = 0; i < Technos.Count(); i++) { + TechnoClass* techno = Technos[i]; + + if (techno->Owning_House() == house) { + techno->Captured(newhouse); + success = true; + } + } + } + + return success; } /** - * #issue-299 - * - * Fixes the issue with the current difficulty not being checked - * when enabling triggers. - * - * @see: TriggerClass and TriggerTypeClass for the other parts of this fix. - * - * @author: CCHyper + * Replacement of TAction_Make_Ally to handle the case when the target house does not exist. + * + * @author: ZivDero */ -DECLARE_PATCH(_TActionClass_Operator_Enable_Trigger_For_Difficulty_Patch) +bool TActionClassExt::_TAction_Make_Ally(HouseClass* house, ObjectClass* object, TriggerClass* trig, Cell& cell) { - GET_REGISTER_STATIC(int, trigger_index, edi); - static TriggerClass* trigger; + HouseClass* other = HouseClass::As_Pointer(Data.House); /** - * This is direct port of the code from Red Alert 2, which looks to fix this issue. + * Fix: check if the house exists, since a spawn house might not. */ + if (other) { + house->Make_Ally(other); + other->Make_Ally(house); + } + + return true; +} + + +/** + * Replacement of TAction_Make_Enemy to handle the case when the target house does not exist. + * + * @author: ZivDero + */ +bool TActionClassExt::_TAction_Make_Enemy(HouseClass* house, ObjectClass* object, TriggerClass* trig, Cell& cell) +{ + HouseClass* other = HouseClass::As_Pointer(Data.House); /** - * We need to re-fetch the trigger from the vector as the - * register is reused by this point. + * Fix: check if the house exists, since a spawn house might not. */ - trigger = Triggers[trigger_index]; - if (trigger) { + if (other) { + house->Make_Enemy(other); + other->Make_Enemy(house); + } - /** - * Set this trigger to be disabled if it is marked as disabled - * for this current mission difficulty. - */ - if (Scen->Difficulty == DIFF_EASY && !trigger->Class->Easy - || Scen->Difficulty == DIFF_NORMAL && !trigger->Class->Normal - || Scen->Difficulty == DIFF_HARD && !trigger->Class->Hard) { + return true; +} - trigger->Disable(); - } else { +/** + * Replacement of TAction_Begin_Production to handle the case when the target house does not exist. + * + * @author: ZivDero + */ +bool TActionClassExt::_TAction_Begin_Production(HouseClass* house, ObjectClass* object, TriggerClass* trig, Cell& cell) +{ + HouseClass* other = HouseClass::As_Pointer(Data.House); - trigger->Enable(); - } + /** + * Fix: check if the house exists, since a spawn house might not. + */ + if (other) { + other->IsStarted = true; } - JMP(0x0061A611); + return true; } + /** - * Helper function. Flags all houses aside from the winner - * as defeated. + * Replacement of TAction_Fire_Sale to handle the case when the target house does not exist. * - * @author: Rampastring + * @author: ZivDero */ -void TAction_Win_Flag_Houses(HousesType winner) +bool TActionClassExt::_TAction_Fire_Sale(HouseClass* house, ObjectClass* object, TriggerClass* trig, Cell& cell) { + HouseClass* other = HouseClass::As_Pointer(Data.House); + /** - * Flag the player as won or lost, like in the original code. + * Fix: check if the house exists, since a spawn house might not. */ - if (PlayerPtr->Class->House == winner) { - PlayerPtr->Flag_To_Win(false); - } else { - PlayerPtr->Flag_To_Lose(false); + if (other) { + other->State = STATE_ENDGAME; } + return true; +} + + +/** + * Replacement of TAction_Begin_Autocreate to handle the case when the target house does not exist. + * + * @author: ZivDero + */ +bool TActionClassExt::_TAction_Begin_Autocreate(HouseClass* house, ObjectClass* object, TriggerClass* trig, Cell& cell) +{ + HouseClass* other = HouseClass::As_Pointer(Data.House); + /** - * Mark all other houses than the winner as defeated. + * Fix: check if the house exists, since a spawn house might not. */ - for (int i = 0; i < Houses.Count(); i++) { - HouseClass *house = Houses[i]; - - if (house->Class->House != winner) { - house->IsDefeated = true; - } + if (other) { + other->IsAlerted = true; } + + return true; } /** - * #issue-965 + * Replacement of TAction_All_Hunt to handle the case when the target house does not exist. * - * Makes the "Winner is" trigger action set the IsDefeated flag on losing - * houses in multiplayer. + * @author: ZivDero + */ +bool TActionClassExt::_TAction_All_Hunt(HouseClass* house, ObjectClass* object, TriggerClass* trig, Cell& cell) +{ + HouseClass* other = HouseClass::As_Pointer(Data.House); + + /** + * Fix: check if the house exists, since a spawn house might not. + */ + if (other) { + other->All_To_Hunt(); + } + + return true; +} + + +/** + * Replacement of TAction_Set_AI_Triggers_Begin to handle the case when the target house does not exist. * - * @author: Rampastring + * @author: ZivDero */ -DECLARE_PATCH(_TAction_Win_FlagLosersAsDefeatedInMultiplayer) +bool TActionClassExt::_TAction_Set_AI_Triggers_Begin(HouseClass* house, ObjectClass* object, TriggerClass* trig, Cell& cell) { - GET_STACK_STATIC(HousesType, housestype, esi, 0x40); + HouseClass* other = HouseClass::As_Pointer(Data.House); - if (Session.Type == GAME_NORMAL) { - goto original_code; + /** + * Fix: check if the house exists, since a spawn house might not. + */ + if (other) { + other->IsAITriggersOn = true; } - TAction_Win_Flag_Houses(housestype); + return true; +} + + +/** + * Replacement of TAction_Set_AI_Triggers_End to handle the case when the target house does not exist. + * + * @author: ZivDero + */ +bool TActionClassExt::_TAction_Set_AI_Triggers_End(HouseClass* house, ObjectClass* object, TriggerClass* trig, Cell& cell) +{ + HouseClass* other = HouseClass::As_Pointer(Data.House); /** - * The action has done its job, return true. + * Fix: check if the house exists, since a spawn house might not. */ -return_true: - JMP_REG(ebx, 0x00619FF6); + if (other) { + other->IsAITriggersOn = false; + } + + return true; +} + + +/** + * Gives credits to the house specified as the argument. + * + * @author: ZivDero, Rampastring + */ +bool TActionClassExt::_TAction_Give_Credits(HouseClass* house, ObjectClass* object, TriggerClass* trig, Cell& cell) +{ + HouseClass* other = HouseClass::As_Pointer(Data.House); -original_code: /** - * Stolen bytes / code. + * Give credits to the house. */ - _asm { mov ecx, PlayerPtr } - _asm { mov ecx, [ecx] } - JMP(0x00619FE1); + if (other) { + + const int amount = Bounds.X; + if (amount >= 0) { + other->Refund_Money(amount); + } + else { + other->Spend_Money(-amount); + } + } + + return true; } /** - * Helper function. Flags the losers as defeated. + * Enables short game. * - * @author: Rampastring + * @author: ZivDero, Rampastring + */ +bool TActionClassExt::_TAction_Enable_Short_Game(HouseClass* house, ObjectClass* object, TriggerClass* trig, Cell& cell) +{ + Session.Options.ShortGame = true; + + return true; +} + + +/** + * Disables short game. + * + * @author: ZivDero, Rampastring + */ +bool TActionClassExt::_TAction_Disable_Short_Game(HouseClass* house, ObjectClass* object, TriggerClass* trig, Cell& cell) +{ + Session.Options.ShortGame = false; + + return true; +} + + +/** + * Blows up the specified house. + * + * @author: ZivDero, Rampastring */ -void TAction_Lose_Flag_Houses(HousesType loser) +bool TActionClassExt::_TAction_Blowup_House(HouseClass* house, ObjectClass* object, TriggerClass* trig, Cell& cell) { + HouseClass* other = HouseClass::As_Pointer(Data.House); + /** - * Flag the player as won or lost, like in the original code. + * Blow the house up and mark the player as defeated. */ - if (PlayerPtr->Class->House == loser) { - PlayerPtr->Flag_To_Lose(false); - } else { - PlayerPtr->Flag_To_Win(false); + if (other) { + other->Blowup_All(); + other->MPlayer_Defeated(); } + return true; +} + + +/** + * Makes all objects attached to the trigger elite. + * + * @author: ZivDero, Rampastring + */ +bool TActionClassExt::_TAction_Make_Elite(HouseClass* house, ObjectClass* object, TriggerClass* trig, Cell& cell) +{ /** - * Mark all losers as defeated. + * Iterate all technos, and if their tag is attached to this trigger, make them elite. */ - for (int i = 0; i < Houses.Count(); i++) { - HouseClass* house = Houses[i]; + for (int i = 0; i < Technos.Count(); i++) { + TechnoClass* techno = Technos[i]; - if (house->Class->House == loser) { - house->IsDefeated = true; + if (techno->IsActive && techno->IsDown && !techno->IsInLimbo) { + if (techno->Tag && techno->Tag->Is_Trigger_Attached(trig)) { + techno->Veterancy.Set_Elite(true); + } } } + + return true; } /** - * #issue-965 + * Enables ally reveal * - * Makes the "Loser is" trigger action set the IsDefeated flag on the - * losing house in multiplayer. + * @author: ZivDero, Rampastring + */ +bool TActionClassExt::_TAction_Enable_AllyReveal(HouseClass* house, ObjectClass* object, TriggerClass* trig, Cell& cell) +{ + Rule->IsAllyReveal = true; + + return true; +} + + +/** + * Disables ally reveal. * - * @author: Rampastring + * @author: ZivDero, Rampastring + */ +bool TActionClassExt::_TAction_Disable_AllyReveal(HouseClass* house, ObjectClass* object, TriggerClass* trig, Cell& cell) +{ + Rule->IsAllyReveal = false; + + return true; +} + + +/** + * Schedules the creation of an autosave the next frame. + * + * @author: ZivDero, Rampastring */ -DECLARE_PATCH(_TAction_Lose_FlagLoserAsLostInMultiplayer) +bool TActionClassExt::_TAction_Create_AutoSave(HouseClass* house, ObjectClass* object, TriggerClass* trig, Cell& cell) { - GET_STACK_STATIC(HousesType, housestype, esi, 0x40); + /** + * Schedule a save. + */ + Vinifera_DoSave = true; - if (Session.Type == GAME_NORMAL) { - goto original_code; - } + return true; +} - TAction_Lose_Flag_Houses(housestype); +/** + * Silently deletes all objects attached to this trigger from the map. + * + * @author: ZivDero, Rampastring + */ +bool TActionClassExt::_TAction_Delete_Object(HouseClass* house, ObjectClass* object, TriggerClass* trig, Cell& cell) +{ /** - * The action has done its job, return true. + * Iterate all technos, and if their tag is attached to this trigger, flag them for deletion. */ -return_true: - JMP_REG(ebx, 0x0061A020); + for (int i = 0; i < Technos.Count(); i++) { + TechnoClass* techno = Technos[i]; + + if (techno->IsActive && techno->IsDown && !techno->IsInLimbo) { + if (techno->Tag && techno->Tag->Is_Trigger_Attached(trig)) { + techno->Remove_This(); + } + } + } -original_code: + return true; +} + + +/** + * Assigns a mission to all units owned by the trigger owner. + * + * @author: ZivDero, Rampastring + */ +bool TActionClassExt::_TAction_All_Assign_Mission(HouseClass* house, ObjectClass* object, TriggerClass* trig, Cell& cell) +{ /** - * Stolen bytes / code. + * Iterate all units, and if they are owned by the trigger owner, assign the mission. */ - _asm { mov ecx, PlayerPtr } - _asm { mov ecx, [ecx] } - JMP(0x0061A00B); + for (int i = 0; i < Technos.Count(); i++) { + TechnoClass* techno = Technos[i]; + + if (techno->IsActive && techno->IsDown && !techno->IsInLimbo) { + if (techno->Owning_House() == house) { + techno->Assign_Mission(static_cast(Data.Value)); + } + } + } + + return true; } @@ -299,31 +1407,40 @@ DECLARE_PATCH(_TAction_Lose_FlagLoserAsLostInMultiplayer) */ void TActionClassExtension_Hooks() { + /** + * Replacement of TActionClass::operator(). + */ + Patch_Jump(0x00619110, &TActionClassExt::_Function_Call_Operator); + /** * #issue-674 - * + * * Fixes a bug where the game would crash when TACTION_WAKEUP_GROUP was * executed but the game was not able to match the Group to the triggers * group. This was because the game was searching the Foots vector with * the count of the Technos vector, and in cases where the Group did * not match, the game would crash trying to search out of bounds. - * + * * @author: CCHyper */ - Patch_Dword(0x00619552+2, (0x007E4820+4)); // Foot vector to Technos vector. - - Patch_Jump(0x0061A60C, &_TActionClass_Operator_Enable_Trigger_For_Difficulty_Patch); + Patch_Dword(0x0061AFB0 + 2, 0x007E4820 + 4); // Foot vector to Technos vector. /** - * #issue-71 - * - * Increases the amount of available waypoints (see ScenarioClassExtension for implementation). - * - * @author: CCHyper + * Replacement of various vanilla actions. */ - Patch_Jump(0x0061BF50, &TActionClassExt::_Play_Sound_At_Random_Waypoint); - Patch_Jump(0x00619E42, &_TActionClass_Operator_Play_Sound_At_Random_Waypoint_Remove_Inline_Patch); - - Patch_Jump(0x00619FDB, &_TAction_Win_FlagLosersAsDefeatedInMultiplayer); - Patch_Jump(0x0061A005, &_TAction_Lose_FlagLoserAsLostInMultiplayer); + Patch_Jump(0x0061BF50, &TActionClassExt::_TAction_Play_Sound_At_Random_Waypoint); + Patch_Jump(0x0061CDA0, &TActionClassExt::_TAction_Enable_Trigger); + Patch_Jump(0x0061CCB0, &TActionClassExt::_TAction_Destroy_Trigger); + Patch_Jump(0x0061C200, &TActionClassExt::_TAction_Win); + Patch_Jump(0x0061C230, &TActionClassExt::_TAction_Lose); + Patch_Jump(0x0061B630, &TActionClassExt::_TAction_Change_House); + Patch_Jump(0x0061B6E0, &TActionClassExt::_TAction_All_Change_House); + Patch_Jump(0x0061B820, &TActionClassExt::_TAction_Make_Ally); + Patch_Jump(0x0061B860, &TActionClassExt::_TAction_Make_Enemy); + Patch_Jump(0x0061C260, &TActionClassExt::_TAction_Begin_Production); + Patch_Jump(0x0061C280, &TActionClassExt::_TAction_Fire_Sale); + Patch_Jump(0x0061C2A0, &TActionClassExt::_TAction_Begin_Autocreate); + Patch_Jump(0x0061C3D0, &TActionClassExt::_TAction_All_Hunt); + Patch_Jump(0x0061D0E0, &TActionClassExt::_TAction_Set_AI_Triggers_Begin); + Patch_Jump(0x0061D100, &TActionClassExt::_TAction_Set_AI_Triggers_End); } diff --git a/src/extensions/techno/technoext.cpp b/src/extensions/techno/technoext.cpp index 16f035406..26dec0af3 100644 --- a/src/extensions/techno/technoext.cpp +++ b/src/extensions/techno/technoext.cpp @@ -55,7 +55,8 @@ TechnoClassExtension::TechnoClassExtension(const TechnoClass *this_ptr) : ElectricBolt(nullptr), Storage(Tiberiums.Count()), SpawnManager(nullptr), - SpawnOwner(nullptr) + SpawnOwner(nullptr), + LastVeterancy(VETERANCY_NONE) { //if (this_ptr) EXT_DEBUG_TRACE("TechnoClassExtension::TechnoClassExtension - Name: %s (0x%08X)\n", Name(), (uintptr_t)(This())); diff --git a/src/extensions/techno/technoext.h b/src/extensions/techno/technoext.h index 4292b8440..0f37e2e1f 100644 --- a/src/extensions/techno/technoext.h +++ b/src/extensions/techno/technoext.h @@ -92,4 +92,10 @@ class TechnoClassExtension : public RadioClassExtension * The object that spawned this object. */ TechnoClass* SpawnOwner; + + /** + * The veternacy rank of this unit last time it performed its AI() function. + * Used to determine when a unit has ranked up. + */ + VeterancyRankType LastVeterancy; }; diff --git a/src/extensions/techno/technoext_hooks.cpp b/src/extensions/techno/technoext_hooks.cpp index fdd6262f3..16b538125 100644 --- a/src/extensions/techno/technoext_hooks.cpp +++ b/src/extensions/techno/technoext_hooks.cpp @@ -86,7 +86,8 @@ #include "tibsun_functions.h" #include "utracker.h" #include "aircraft.h" - +#include "spawner.h" +#include "vox.h" /** @@ -121,6 +122,7 @@ class TechnoClassExt : public TechnoClass bool _Can_Player_Move() const; Coordinate _Fire_Coord(WeaponSlotType which) const; void _Record_The_Kill(TechnoClass* source); + bool _Revealed(HouseClass* house); }; @@ -591,8 +593,7 @@ void TechnoClassExt::_Stun() /** - * Wrapper function to patch the call in TechnoClass::AI to call - * SpawnManagerClass::AI. + * Wrapper function to insert new things into TechnoClass::AI. * * @author: ZivDero */ @@ -602,8 +603,55 @@ void TechnoClassExt::_Mission_AI() const auto extension = Extension::Fetch(this); + /** + * Execute SpawnManager AI. + */ if (extension->SpawnManager) extension->SpawnManager->AI(); + + /** + * Check if the unit has been promoted. + */ + if (extension->LastVeterancy != Veterancy.Get_Rank()) + { + if (extension->LastVeterancy != VETERANCY_NONE) + { + if (Veterancy.Get_Rank() == RANK_ELITE) + { + /** + * Play the promotion sound and voice line. + */ + if (House->Is_Player_Control()) + { + Sound_Effect(RuleExtension->UpgradeEliteSound, Coord); + Speak(RuleExtension->VoxUnitPromoted); + } + + /** + * Elite units also flash for a while. + */ + FlashCount = RuleExtension->EliteFlashTimer; + } + else if (Veterancy.Get_Rank() == RANK_VETERAN) + { + /** + * Play the promotion sound and voice line. + */ + if (House->Is_Player_Control()) + { + Sound_Effect(RuleExtension->UpgradeVeteranSound, Coord); + Speak(RuleExtension->VoxUnitPromoted); + } + } + + /** + * Force the unit to look in case its range has been upgraded. + */ + Look(); + } + + extension->LastVeterancy = Veterancy.Get_Rank(); + } } @@ -1245,7 +1293,7 @@ void TechnoClassExt::_Record_The_Kill(TechnoClass* source) } if (source) { - if ((Session.Type == GAME_INTERNET || Session.Type == GAME_IPX) && !typeext->IsDontScore) { + if (!typeext->IsDontScore) { source->House->DestroyedBuildings->Increment_Unit_Total(reinterpret_cast(this)->Class->Type); } source->House->BuildingsKilled[Owner()]++; @@ -1262,19 +1310,19 @@ void TechnoClassExt::_Record_The_Kill(TechnoClass* source) break; case RTTI_AIRCRAFT: - if (source && (Session.Type == GAME_INTERNET || Session.Type == GAME_IPX) && !typeext->IsDontScore) { + if (source && !typeext->IsDontScore) { source->House->DestroyedAircraft->Increment_Unit_Total(reinterpret_cast(this)->Class->Type); total_recorded++; } // Fall through..... case RTTI_INFANTRY: - if (source && !total_recorded && (Session.Type == GAME_INTERNET || Session.Type == GAME_IPX) && !typeext->IsDontScore) { + if (source && !total_recorded && !typeext->IsDontScore) { source->House->DestroyedInfantry->Increment_Unit_Total(reinterpret_cast(this)->Class->Type); total_recorded++; } // Fall through..... case RTTI_UNIT: - if (source && !total_recorded && (Session.Type == GAME_INTERNET || Session.Type == GAME_IPX) && !typeext->IsDontScore) { + if (source && !total_recorded && !typeext->IsDontScore) { source->House->DestroyedUnits->Increment_Unit_Total(reinterpret_cast(this)->Class->Type); } @@ -1296,6 +1344,83 @@ void TechnoClassExt::_Record_The_Kill(TechnoClass* source) } +/** + * Handles revealing an object to the house specified. + * + * @author: 06/02/1994 JLB - Created. + * ZivDero - Adjustments for Tiberian Sun. + */ +bool TechnoClassExt::_Revealed(HouseClass* house) +{ + if (house == PlayerPtr && IsDiscoveredByPlayer) { + return false; + } + + if (house != PlayerPtr) { + if (IsDiscoveredByComputer) return false; + IsDiscoveredByComputer = true; + } + + if (house == nullptr) { + return false; + } + + if (RadioClass::Revealed(house)) { + + /* + * An enemy object that is discovered will go into hunt mode if + * its current mission is to ambush. + */ + if (!House->Is_Human_Control() && Mission == MISSION_AMBUSH) { + Assign_Mission(MISSION_HUNT); + } + + if (house == PlayerPtr) { + + IsDiscoveredByPlayer = true; + House->field_56C = true; + House->field_56D = true; + + if (!IsOwnedByPlayer) { + + /** + * If there is a trigger event associated with this object, then process + * it for discovery purposes. + */ + if (!ScenarioInit && Tag) { + Tag->Spring(TEVENT_DISCOVERED, this); + } + + /** + * Alert the enemy house to presence of the friendly side. + */ + House->IsDiscovered = true; + } + else { + + /** + * A newly revealed object will always perform a look operation. + */ + Look(); + } + + /** + * Outside of campaign, reveal newly built allied objects with AllyReveal on. + */ + if (Session.Type != GAME_NORMAL && Rule->IsAllyReveal && House->Is_Ally(house)) { + Look(); + } + } + else { + IsDiscoveredByComputer = true; + } + + return true; + } + + return false; +} + /** * #issue-1087 @@ -1684,6 +1809,44 @@ DECLARE_PATCH(_TechnoClass_Evaluate_Object_PassiveAcquire_Armor_Patch) } +/** + * Wrapper for the patch below because doing this messes with the stack. + */ +static bool Can_Attack_Neutrals(TechnoClass* target) +{ + bool attack_neutrals = Vinifera_SpawnerActive && Vinifera_SpawnerConfig->AttackNeutralUnits; + bool unarmed_building = target->What_Am_I() == RTTI_BUILDING && (!target->Is_Weapon_Equipped() || target->Get_Weapon()->Weapon->Range == 0); + + return attack_neutrals && !unarmed_building; +}; + + +/** + * Patch to allow units to target neutral units if the spawner requests it. + * + * @author: ZivDero + */ +DECLARE_PATCH(_TechnoClass_Evaluate_Object_AttackNeutralUnits_Patch) +{ + GET_REGISTER_STATIC(TechnoClass*, target, esi); + + if (Session.Type != GAME_NORMAL && target->Owning_House()->Class->IsMultiplayPassive) + { + /** + * Allow attacking neutrals, but if it's a building, it must be armed. + */ + if (!Can_Attack_Neutrals(target)) + { + // return false; + JMP(0x0062D8C0); + } + } + + // Continue normally. + JMP(0x0062D4BA); +} + + /** * Replaces Verses (Modifier) of the Warhead with the one from the extension. * @@ -2599,4 +2762,6 @@ void TechnoClassExtension_Hooks() Patch_Jump(0x00631FF0, &TechnoClassExt::_Can_Player_Move); Patch_Jump(0x006336F0, &TechnoClassExt::_Record_The_Kill); //Patch_Jump(0x0062A3D0, &TechnoClassExt::_Fire_Coord); // Disabled because it's functionally identical to the vanilla function when there's no secondary coordinate + Patch_Jump(0x0062D49A, &_TechnoClass_Evaluate_Object_AttackNeutralUnits_Patch); + Patch_Jump(0x0062AAD0, &TechnoClassExt::_Revealed); } diff --git a/src/extensions/technotype/technotypeext.cpp b/src/extensions/technotype/technotypeext.cpp index cd944b582..a83c39585 100644 --- a/src/extensions/technotype/technotypeext.cpp +++ b/src/extensions/technotype/technotypeext.cpp @@ -39,6 +39,7 @@ #include "vinifera_saveload.h" #include "asserthandler.h" #include "debughandler.h" +#include "spawner.h" /** @@ -86,7 +87,8 @@ TechnoTypeClassExtension::TechnoTypeClassExtension(const TechnoTypeClass *this_p MaxRandomSpawnOffset(0), IsDontScore(false), IsSpawned(false), - BuildTimeCost(0) + BuildTimeCost(0), + ScrapExplosion() { //if (this_ptr) EXT_DEBUG_TRACE("TechnoTypeClassExtension::TechnoTypeClassExtension - Name: %s (0x%08X)\n", Name(), (uintptr_t)(This())); } @@ -98,7 +100,8 @@ TechnoTypeClassExtension::TechnoTypeClassExtension(const TechnoTypeClass *this_p * @author: CCHyper */ TechnoTypeClassExtension::TechnoTypeClassExtension(const NoInitClass &noinit) : - ObjectTypeClassExtension(noinit) + ObjectTypeClassExtension(noinit), + ScrapExplosion(noinit) { //EXT_DEBUG_TRACE("TechnoTypeClassExtension::TechnoTypeClassExtension(NoInitClass) - Name: %s (0x%08X)\n", Name(), (uintptr_t)(This())); } @@ -127,11 +130,16 @@ HRESULT TechnoTypeClassExtension::Load(IStream *pStm) { //EXT_DEBUG_TRACE("TechnoTypeClassExtension::Load - Name: %s (0x%08X)\n", Name(), (uintptr_t)(This())); + ScrapExplosion.Clear(); + HRESULT hr = ObjectTypeClassExtension::Load(pStm); if (FAILED(hr)) { return E_FAIL; } + ScrapExplosion.Load(pStm); + + VINIFERA_SWIZZLE_REQUEST_POINTER_REMAP_LIST(ScrapExplosion, "ScrapExplosion"); VINIFERA_SWIZZLE_REQUEST_POINTER_REMAP(UnloadingClass, "UnloadingClass"); VINIFERA_SWIZZLE_REQUEST_POINTER_REMAP(Spawns, "Spawns"); @@ -177,6 +185,8 @@ HRESULT TechnoTypeClassExtension::Save(IStream *pStm, BOOL fClearDirty) return hr; } + ScrapExplosion.Save(pStm); + return hr; } @@ -316,11 +326,20 @@ bool TechnoTypeClassExtension::Read_INI(CCINIClass &ini) SpawnLogicRate = ini.Get_Int(ini_name, "SpawnLogicRate", SpawnLogicRate); SpawnsNumber = ini.Get_Int(ini_name, "SpawnsNumber", SpawnsNumber); SecondSpawnOffset = ArtINI.Get_Point(graphic_name, "SecondSpawnOffset", SecondSpawnOffset); - MaxRandomSpawnOffset = ini.Get_Int(graphic_name, "MaxRandomSpawnOffset", MaxRandomSpawnOffset); + MaxRandomSpawnOffset = ini.Get_Int(ini_name, "MaxRandomSpawnOffset", MaxRandomSpawnOffset); IsDontScore = ini.Get_Bool(ini_name, "DontScore", IsDontScore); IsSpawned = ini.Get_Bool(ini_name, "Spawned", IsSpawned); BuildTimeCost = ini.Get_Int(ini_name, "BuildTimeCost", BuildTimeCost); + ScrapExplosion = ini.Get_Anims(ini_name, "ScrapExplosion", ScrapExplosion); + + /** + * If the spawner requested scrap explosions, replace the game's explosion vector with ours. + */ + if (Vinifera_SpawnerActive && Vinifera_SpawnerConfig->ScrapMetal) { + This()->Explosion = ScrapExplosion; + } + return true; } diff --git a/src/extensions/technotype/technotypeext.h b/src/extensions/technotype/technotypeext.h index 7e5d7891a..87a3e6c4e 100644 --- a/src/extensions/technotype/technotypeext.h +++ b/src/extensions/technotype/technotypeext.h @@ -250,4 +250,9 @@ class TechnoTypeClassExtension : public ObjectTypeClassExtension * Optional override for the cost that is used for determining the techno's build time. */ int BuildTimeCost; + + /** + * List of animations to be used as the explosion when scrap explosions are turned on. + */ + TypeList ScrapExplosion; }; diff --git a/src/extensions/unit/unitext_hooks.cpp b/src/extensions/unit/unitext_hooks.cpp index 6f1b07551..e7bdd6518 100644 --- a/src/extensions/unit/unitext_hooks.cpp +++ b/src/extensions/unit/unitext_hooks.cpp @@ -34,6 +34,7 @@ #include "tag.h" #include "technotype.h" #include "technotypeext.h" +#include "house.h" #include "warheadtype.h" #include "unit.h" #include "unitext.h" @@ -74,6 +75,7 @@ class UnitClassExt : public UnitClass public: void _Firing_AI(); void _Draw_Voxel(unsigned int frame, int key, Rect& rect, Point2D& point, const Matrix3D& other_matrix, int color, int flags); + int _Mission_Hunt(); }; @@ -256,6 +258,35 @@ DECLARE_PATCH(_UnitClass_Draw_Voxel_Patch) } +/** + * #issue-177 + * + * Reaplces UnitClass::MissionHunt to consider the entire BuildConst vector. + * + * @author: ZivDero + */ +int UnitClassExt::_Mission_Hunt() +{ + if (Class->DeploysInto && (Rule->BuildConst.Is_Present(Class->DeploysInto) || TarCom || House->Is_Human_Control())) + { + if (Status) + { + if (Status == 1 && !IsDeploying) + Status = 0; + } + else if (Goto_Clear_Spot()) + { + if (Try_To_Deploy()) + Status = 1; + } + + return Get_Current_Mission_Control().Rate * TICKS_PER_MINUTE + Random_Pick(0, 2); + } + + return FootClass::Mission_Hunt(); +} + + #if 0 /** * #issue-510 @@ -1337,6 +1368,68 @@ DECLARE_PATCH(_UnitClass_Mission_Harvest_FINDHOME_Find_Nearest_Refinery_Patch) } +/** + * #issue-177 + * + * Patches the AI to correctly consider all Construction Yards from the list. + * + * @author: ZivDero + */ +DECLARE_PATCH(_UnitClass_AI_BuildConst_Patch) +{ + GET_REGISTER_STATIC(UnitTypeClass*, unittype, edx); + + if (Rule->BuildConst.Is_Present(unittype->DeploysInto)) + { + JMP_REG(ecx, 0x0064E0EC); + } + + JMP_REG(eax, 0x0064E134); +} + + +/** + * #issue-177 + * + * Patches the AI to correctly consider all Construction Yards from the list. + * + * @author: ZivDero + */ +DECLARE_PATCH(_UnitClass_What_Action_BuildConst) +{ + GET_REGISTER_STATIC(BuildingTypeClass*, buildingtype, ebp); + _asm pushad + + if (Rule->BuildConst.Is_Present(buildingtype)) + { + _asm popad + JMP_REG(edx, 0x00656084); + } + + _asm popad + JMP_REG(edi, 0x006560A3); +} + + +/** + * #issue-177 + * + * Patches the AI to correctly consider all Construction Yards from the list. + * + * @author: ZivDero + */ +DECLARE_PATCH(_UnitClass_Mission_Guard_BuildConst) +{ + GET_REGISTER_STATIC(UnitClass*, unit, esi); + + if (Rule->BuildConst.Is_Present(unit->Class->DeploysInto)) + { + JMP(0x00656770); + } + + JMP(0x006567FD); +} + /** * Main function for patching the hooks. */ @@ -1363,6 +1456,10 @@ void UnitClassExtension_Hooks() Patch_Jump(0x0064E920, &UnitClassExt::_Firing_AI); Patch_Jump(0x006527B1, &_UnitClass_Draw_Voxel_Patch); Patch_Jump(0x00654EEE, &_UnitClass_Mission_Harvest_FINDHOME_Find_Nearest_Refinery_Patch); + Patch_Jump(0x0064E0D7, &_UnitClass_AI_BuildConst_Patch); + Patch_Jump(0x00655270, &UnitClassExt::_Mission_Hunt); + Patch_Jump(0x00656074, &_UnitClass_What_Action_BuildConst); + Patch_Jump(0x00656751, &_UnitClass_Mission_Guard_BuildConst); //Patch_Jump(0x0065054F, &_UnitClass_Enter_Idle_Mode_Block_Harvesting_On_Bridge_Patch); // Removed, keeping code for reference. //Patch_Jump(0x00654AB0, &_UnitClass_Mission_Harvest_Block_Harvesting_On_Bridge_Patch); // Removed, keeping code for reference. } diff --git a/src/hooker/setup_hooks.cpp b/src/hooker/setup_hooks.cpp index 2851cd6ec..81a650626 100644 --- a/src/hooker/setup_hooks.cpp +++ b/src/hooker/setup_hooks.cpp @@ -36,8 +36,6 @@ #include "vinifera_hooks.h" #include "newswizzle_hooks.h" #include "extension_hooks.h" -#include "cncnet4_hooks.h" -#include "cncnet5_hooks.h" #include "sidebarext_hooks.h" @@ -50,9 +48,6 @@ void Setup_Hooks() Vinifera_Hooks(); NewSwizzle_Hooks(); Extension_Hooks(); - - CnCNet4_Hooks(); - CnCNet5_Hooks(); } /** diff --git a/src/new/spawnmanager/spawnmanager_hooks.cpp b/src/new/spawnmanager/spawnmanager_hooks.cpp index 34001fad6..43de0830e 100644 --- a/src/new/spawnmanager/spawnmanager_hooks.cpp +++ b/src/new/spawnmanager/spawnmanager_hooks.cpp @@ -40,33 +40,6 @@ #include "vinifera_globals.h" -/** - * Patch to make the spawn manager abandon its target when ordered to idle. - * - * @author: ZivDero - */ -DECLARE_PATCH(_EventClass_Execute_IDLE_Spawn_Manager_Patch) -{ - GET_REGISTER_STATIC(TechnoClass*, techno, esi); - static TechnoClassExtension* extension; - - extension = Extension::Fetch(techno); - if (extension->SpawnManager) - extension->SpawnManager->Abandon_Target(); - - static RTTIType rtti = techno->Kind_Of(); - if (rtti == RTTI_UNIT) - { - JMP(0x00494AC5); - } - else - { - JMP(0x00495110); - } - -} - - /** * Patch to block vehicles from moving if they're currently preparing to spawn. * @@ -114,7 +87,6 @@ DECLARE_PATCH(_LogicClass_AI_Kamikaze_AI_Patch) */ void SpawnManager_Hooks() { - Patch_Jump(0x00494AB5, &_EventClass_Execute_IDLE_Spawn_Manager_Patch); Patch_Jump(0x0047FE2F, &_DriveLocomotionClass_Start_Of_Move_Spawn_Manager_Patch); Patch_Jump(0x00507000, &_LogicClass_AI_Kamikaze_AI_Patch); } diff --git a/src/new/viniferaevent/viniferaevent.cpp b/src/new/viniferaevent/viniferaevent.cpp new file mode 100644 index 000000000..4f9f3e3dd --- /dev/null +++ b/src/new/viniferaevent/viniferaevent.cpp @@ -0,0 +1,108 @@ +/******************************************************************************* +/* O P E N S O U R C E -- V I N I F E R A ** +/******************************************************************************* + * + * @project Vinifera + * + * @file VINIFERAEVENT.CPP + * + * @author ZivDero, Belonit + * + * @brief Class that mimics vanilla EventClass to allow the creation + * of new events in Vinifera. + * + * @license Vinifera 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. + * + * Vinifera 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 . + * + ******************************************************************************/ + +#include "viniferaevent.h" + +#include "asserthandler.h" +#include "protocolzero.h" + +/** + * Arrays with new event lengths and names. + */ +unsigned char ViniferaEventLength[VEVENT_COUNT - EVENT_COUNT] +{ + sizeof(ViniferaEventClass::Data.ResponseTime2) +}; + +const char* ViniferaEventNames[VEVENT_COUNT - EVENT_COUNT] +{ + "RESPONSE_TIME_2" +}; + + +/** + * Execute our new event. + * + * @author: ZivDero + */ +void ViniferaEventClass::Execute() +{ + switch (Type) + { + case VEVENT_RESPONSE_TIME_2: + ProtocolZero::Handle_Response_Time(this); + break; + + default: + break; + } +} + + +/** + * Get the length of this event. + * + * @author: ZivDero + */ +unsigned char ViniferaEventClass::Event_Length(ViniferaEventType type) +{ + ASSERT(type >= 0 && type < VEVENT_COUNT); + + if (type < EVENT_COUNT) + return EventClass::Event_Length(static_cast(type)); + + return ViniferaEventLength[type - EVENT_COUNT]; +} + + +/** + * Get the name of this event. + * + * @author: ZivDero + */ +const char* ViniferaEventClass::Event_Name(ViniferaEventType type) +{ + ASSERT(type >= 0 && type < VEVENT_COUNT); + + if (type < EVENT_COUNT) + return EventClass::Event_Name(static_cast(type)); + + return ViniferaEventNames[type - EVENT_COUNT]; +} + + +/** + * Check if this event was added by Vinifera. + * + * @author: ZivDero + */ +bool ViniferaEventClass::Is_Vinifera_Event(ViniferaEventType type) +{ + return (type >= VEVENT_FIRST && type < VEVENT_COUNT); +} diff --git a/src/new/viniferaevent/viniferaevent.h b/src/new/viniferaevent/viniferaevent.h new file mode 100644 index 000000000..8a5fdbd10 --- /dev/null +++ b/src/new/viniferaevent/viniferaevent.h @@ -0,0 +1,88 @@ +/******************************************************************************* +/* O P E N S O U R C E -- V I N I F E R A ** +/******************************************************************************* + * + * @project Vinifera + * + * @file VINIFERAEVENT.H + * + * @author ZivDero, Belonit + * + * @brief Class that mimics vanilla EventClass to allow the creation + * of new events in Vinifera. + * + * @license Vinifera 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. + * + * Vinifera 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 . + * + ******************************************************************************/ +#pragma once + +#include +#include + +#include "event.h" +#include "latencylevel.h" +#include "tibsun_defines.h" + +enum ViniferaEventType : unsigned char +{ + VEVENT_RESPONSE_TIME_2 = EVENT_COUNT, // Start after the last vanilla event + + VEVENT_COUNT, + VEVENT_FIRST = VEVENT_RESPONSE_TIME_2 +}; + + +extern unsigned char ViniferaEventLength[VEVENT_COUNT - EVENT_COUNT]; +extern const char* ViniferaEventNames[VEVENT_COUNT - EVENT_COUNT]; + + +class ViniferaEventClass +{ +#pragma pack(push, 1) +public: + ViniferaEventType Type; + unsigned Frame; + bool IsExecuted; + int ID; + + union + { + char DataBuffer[36]; + + struct ResponseTime2 + { + unsigned char MaxAhead; + LatencyLevelEnum LatencyLevel; + } ResponseTime2; + + } Data; + + void Execute(); + EventClass& As_Event() { return *reinterpret_cast(this); } + + static unsigned char Event_Length(ViniferaEventType type); + static unsigned char Event_Length(EventType type) { return Event_Length(static_cast(type)); } + + static const char* Event_Name(ViniferaEventType type); + static const char* Event_Name(EventType type) { return Event_Name(static_cast(type)); } + + static bool Is_Vinifera_Event(ViniferaEventType type); +#pragma pack(pop) +}; + + +static_assert(sizeof(ViniferaEventClass) == sizeof(EventClass), "ViniferaEventClass doesn't match EventClass in size!"); +static_assert(sizeof(ViniferaEventClass::Data) == sizeof(EventClass::Data), "ViniferaEventClass::Data doesn't match EventClass::Data in size!"); +static_assert(offsetof(ViniferaEventClass, Data) == offsetof(EventClass, Data), "ViniferaEventClass Data is misplaced!"); diff --git a/src/spawner/modules/observer_hooks.cpp b/src/spawner/modules/observer_hooks.cpp new file mode 100644 index 000000000..1743e6fb9 --- /dev/null +++ b/src/spawner/modules/observer_hooks.cpp @@ -0,0 +1,226 @@ +/******************************************************************************* +/* O P E N S O U R C E -- T S + + ** +/******************************************************************************* + * + * @project TS++ + * + * @file OBSERVER_HOOKS.CPP + * + * @authors ZivDero + * + * @brief Contains the hooks for observer mode. + * + * @license TS++ 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. + * + * TS++ 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 . + * + ******************************************************************************/ + +#include "observer_hooks.h" + +#include "display.h" +#include "extension.h" +#include "hooker.h" +#include "hooker_macros.h" +#include "house.h" +#include "houseext.h" +#include "housetype.h" +#include "session.h" +#include "spawner.h" +#include "mouse.h" + +/** + * A fake class for implementing new member functions which allow + * access to the "this" pointer of the intended class. + * + * @note: This must not contain a constructor or destructor. + * + * @note: All functions must not be virtual and must also be prefixed + * with "_" to prevent accidental virtualization. + */ +class HouseClassExt : public HouseClass +{ +public: + bool _Is_Ally_Or_Observer(const HouseClassExt* house) const; + void _Update_Radars(); + bool _Has_Player_Allies() const; +}; + + +/** + * Helper function that returns if the house is allied to the other house, or if the player's house is the observer. + * + * @author: ZivDero + */ +bool HouseClassExt::_Is_Ally_Or_Observer(const HouseClassExt* house) const +{ + return Is_Ally(house) || PlayerPtr == Vinifera_ObserverPtr; +} + + +/** + * Helper function that returns if the player has any allies. + * + * @author: ZivDero + */ +bool HouseClassExt::_Has_Player_Allies() const +{ + const char* SPECIAL = "Special"; + + unsigned int allies = Allies; + + // Special is allied to everyone, so we need to exclude it from the allies list to get the real picture + int special_house_id = As_Pointer(HouseTypeClass::From_Name(SPECIAL))->Get_Heap_ID(); + allies &= ~(1 << special_house_id); + + return allies != 0; +} + + +/** + * Enable the radar for observers. + * + * @author: ZivDero + */ +static bool observer_radar_enabled = false; +void HouseClassExt::_Update_Radars() +{ + Update_Radars(); + + if (this == Vinifera_ObserverPtr && !observer_radar_enabled) { + Map.IsRadarAvailable = true; + Map.RadarClass::Radar_Activate(1); + observer_radar_enabled = true; + } +} + + +/** + * A fake class for implementing new member functions which allow + * access to the "this" pointer of the intended class. + * + * @note: This must not contain a constructor or destructor. + * + * @note: All functions must not be virtual and must also be prefixed + * with "_" to prevent accidental virtualization. + */ +class DisplayClassExt : public DisplayClass +{ +public: + void _Encroach_Shadow_Observer(); + void _Encroach_Fog_Observer(); +}; + + +/** + * Don't encroach shadow for observers. + * + * @author: ZivDero + */ +void DisplayClassExt::_Encroach_Shadow_Observer() +{ + if (Vinifera_SpawnerActive && PlayerPtr == Vinifera_ObserverPtr) { + return; + } + + DisplayClass::Encroach_Shadow(); +} + + +/** + * Don't encroach fog for observers. + * + * @author: ZivDero + */ +void DisplayClassExt::_Encroach_Fog_Observer() +{ + if (Vinifera_SpawnerActive && PlayerPtr == Vinifera_ObserverPtr) { + return; + } + + DisplayClass::Encroach_Fog(); +} + + +/** + * A fake class for implementing new member functions which allow + * access to the "this" pointer of the intended class. + * + * @note: This must not contain a constructor or destructor. + * + * @note: All functions must not be virtual and must also be prefixed + * with "_" to prevent accidental virtualization. + */ +class MapClassExt : public MapClass +{ +public: + void _Reveal_The_Map(); +}; + + +/** + * Don't reveal the map in coach mode. + * + * @author: ZivDero + */ +void MapClassExt::_Reveal_The_Map() +{ + if (Vinifera_SpawnerActive && Vinifera_SpawnerConfig->CoachMode && static_cast(PlayerPtr)->_Has_Player_Allies()) { + return; + } + + MapClass::Reveal_The_Map(); +} + + +/** + * Don't process the radar for observers. + * + * @author: ZivDero + */ +DECLARE_PATCH(_HouseClass_Radar_Outage_Observers) +{ + GET_STACK_STATIC8(bool, tactical_availability, esp, 0x4); + GET_REGISTER_STATIC(HouseClassExt*, house, esi); + + if (house != Vinifera_ObserverPtr) { + Map.RadarClass::Toggle_Radar(tactical_availability); + } + + // Return + JMP(0x004C9693); +} + + +/** + * Main function for patching the hooks. + */ +void Observer_Hooks() +{ + Patch_Call(0x00506D7B, &DisplayClassExt::_Encroach_Shadow_Observer); + Patch_Call(0x00507291, &DisplayClassExt::_Encroach_Shadow_Observer); + Patch_Call(0x00619AE9, &DisplayClassExt::_Encroach_Shadow_Observer); + Patch_Call(0x0061B985, &DisplayClassExt::_Encroach_Shadow_Observer); + Patch_Call(0x00506DFC, &DisplayClassExt::_Encroach_Fog_Observer); + Patch_Call(0x00507309, &DisplayClassExt::_Encroach_Fog_Observer); + Patch_Jump(0x004C9684, &_HouseClass_Radar_Outage_Observers); + Patch_Call(0x0043852B, &HouseClassExt::_Is_Ally_Or_Observer); // BuildingClass::Visual_Character + Patch_Call(0x00438540, &HouseClassExt::_Is_Ally_Or_Observer); // BuildingClass::Visual_Character + Patch_Call(0x00633E85, &HouseClassExt::_Is_Ally_Or_Observer); // TechnoClass::Visual_Character + Patch_Call(0x00633E9F, &HouseClassExt::_Is_Ally_Or_Observer); // TechnoClass::Visual_Character + Patch_Call(0x0062C6CE, &HouseClassExt::_Is_Ally_Or_Observer); // TechnoClass::Draw_Health_Bar + Patch_Call(0x0062CA26, &HouseClassExt::_Is_Ally_Or_Observer); // TechnoClass::Draw_Health_Bar + Patch_Call(0x0047B0BB, &HouseClassExt::_Is_Ally_Or_Observer); // DisplayClass::ToolTip_Text + Patch_Call(0x004BC608, &HouseClassExt::_Update_Radars); + Patch_Call(0x004BF5D6, &MapClassExt::_Reveal_The_Map); +} diff --git a/src/cncnet/cncnet4/cncnet4_hooks.h b/src/spawner/modules/observer_hooks.h similarity index 89% rename from src/cncnet/cncnet4/cncnet4_hooks.h rename to src/spawner/modules/observer_hooks.h index 5dcc88fec..a961b6240 100644 --- a/src/cncnet/cncnet4/cncnet4_hooks.h +++ b/src/spawner/modules/observer_hooks.h @@ -4,11 +4,11 @@ * * @project Vinifera * - * @file CNCNET4_HOOKS.H + * @file OBSERVER_HOOKS.H * - * @author CCHyper + * @author ZivDero * - * @brief Contains the hooks for the CnCNet4 system. + * @brief Contains the hooks for observer mode. * * @license Vinifera is free software: you can redistribute it and/or * modify it under the terms of the GNU General Public License @@ -28,4 +28,4 @@ #pragma once -void CnCNet4_Hooks(); +void Observer_Hooks(); diff --git a/src/spawner/modules/quickmatch_hooks.cpp b/src/spawner/modules/quickmatch_hooks.cpp new file mode 100644 index 000000000..456c42121 --- /dev/null +++ b/src/spawner/modules/quickmatch_hooks.cpp @@ -0,0 +1,150 @@ +/******************************************************************************* +/* O P E N S O U R C E -- V I N I F E R A ** +/******************************************************************************* + * + * @project Vinifera + * + * @file QUICKMATCH_HOOKS.CPP + * + * @author ZivDero + * + * @brief Contains the hooks for the quick match mode. + * + * @license Vinifera 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. + * + * Vinifera 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 . + * + ******************************************************************************/ + +#include "quickmatch_hooks.h" + +#include "hooker.h" +#include "spawner.h" +#include "house.h" +#include "textprint.h" +#include "ipxmgr.h" +#include "session.h" + +#include "hooker_macros.h" + + +static const char* PLAYER = "Player"; + + +/** + * A fake class for implementing new member functions which allow + * access to the "this" pointer of the intended class. + * + * @note: This must not contain a constructor or destructor. + * + * @note: All functions must not be virtual and must also be prefixed + * with "_" to prevent accidental virtualization. + */ +class IPXManagerClassExt : public IPXManagerClass +{ +public: + char* _Connection_Name(int id); +}; + + +/** + * Hide the player names when the IPX manager is asked for it. + * + * @author: ZivDero + */ +char* IPXManagerClassExt::_Connection_Name(int id) +{ + if (Vinifera_SpawnerActive && Vinifera_SpawnerConfig->QuickMatch) + { + return const_cast(PLAYER); + } + else + { + return IPXManagerClass::Connection_Name(id); + } +} + + +/** + * Hide the player names in the in the radar. + * + * @author: ZivDero + */ +static int __cdecl sprintf_RadarClass_Draw_Names_Wrapper(char* buffer, const char* format, char* str) +{ + if (Vinifera_SpawnerActive && Vinifera_SpawnerConfig->QuickMatch) + { + return std::sprintf(buffer, "%s", PLAYER); + } + else + { + return std::sprintf(buffer, format, str); + } +} + + +/** + * Hide the player names in the on the progress screen. + * + * @author: ZivDero + */ +static Point2D Fancy_Text_Print_ProgressScreenClass_Draw_Graphics_Wrapper(const char* text, XSurface* surface, Rect* rect, Point2D* xy, ColorScheme* fore, unsigned back, TextPrintType flag) +{ + if (Vinifera_SpawnerActive && Vinifera_SpawnerConfig->QuickMatch) + { + return Fancy_Text_Print(PLAYER, surface, rect, xy, fore, back, flag); + } + else + { + return Fancy_Text_Print(text, surface, rect, xy, fore, back, flag); + } +} + + +/** + * Hide the player anmes in the Kick Player dialog. + * + * @author: ZivDero + */ +DECLARE_PATCH(_Kick_Player_Dialog_SendMessage_Hide_Name) +{ + GET_REGISTER_STATIC(HWND, hWnd, ebp); + GET_REGISTER_STATIC(int, index, esi); + + _asm pushad + + if (Vinifera_SpawnerActive && Vinifera_SpawnerConfig->QuickMatch) + { + SendMessageA(hWnd, WM_SETTEXT, 0, reinterpret_cast(PLAYER)); + } + else + { + SendMessageA(hWnd, WM_SETTEXT, 0, reinterpret_cast(Session.Players[index]->Name)); + } + + _asm popad + + JMP(0x005B4038); +} + + +/** + * Main function for patching the hooks. + */ +void QuickMatch_Hooks() +{ + Patch_Call(0x005B980E, &sprintf_RadarClass_Draw_Names_Wrapper); + Patch_Call(0x005ADC8F, &Fancy_Text_Print_ProgressScreenClass_Draw_Graphics_Wrapper); + Patch_Jump(0x005B4024, &_Kick_Player_Dialog_SendMessage_Hide_Name); + Patch_Call(0x00648EAE, &IPXManagerClassExt::_Connection_Name); +} diff --git a/src/cncnet/cncnet5/cncnet5_hooks.h b/src/spawner/modules/quickmatch_hooks.h similarity index 88% rename from src/cncnet/cncnet5/cncnet5_hooks.h rename to src/spawner/modules/quickmatch_hooks.h index 853ea9b0c..d63cf2ea0 100644 --- a/src/cncnet/cncnet5/cncnet5_hooks.h +++ b/src/spawner/modules/quickmatch_hooks.h @@ -4,11 +4,11 @@ * * @project Vinifera * - * @file CNCNET5_HOOKS.H + * @file QUICKMATCH_HOOKS.H * - * @author CCHyper + * @author ZivDero * - * @brief Contains the hooks for implementing the CnCNet5 system. + * @brief Contains the hooks for the quick match mode. * * @license Vinifera is free software: you can redistribute it and/or * modify it under the terms of the GNU General Public License @@ -28,4 +28,4 @@ #pragma once -void CnCNet5_Hooks(); +void QuickMatch_Hooks(); diff --git a/src/spawner/modules/statistics_hooks.cpp b/src/spawner/modules/statistics_hooks.cpp new file mode 100644 index 000000000..8090732c2 --- /dev/null +++ b/src/spawner/modules/statistics_hooks.cpp @@ -0,0 +1,317 @@ +/******************************************************************************* +/* O P E N S O U R C E -- V I N I F E R A ** +/******************************************************************************* + * + * @project Vinifera + * + * @file STATISTICS_HOOKS.CPP + * + * @author ZivDero + * + * @brief Contains the hooks for statistics collection. + * + * @license Vinifera 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. + * + * Vinifera 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 . + * + ******************************************************************************/ + +#include "statistics_hooks.h" + +#include "extension.h" +#include "hooker.h" +#include "hooker_macros.h" +#include "packet.h" +#include "scenario.h" +#include "session.h" +#include "spawner.h" +#include "field.h" +#include "house.h" +#include "houseext.h" +#include "housetype.h" +#include "scenarioext.h" +#include "tibsun_globals.h" + + +static bool Is_Spawner_Write_Statistics() +{ + if (Vinifera_SpawnerActive) + { + return Vinifera_SpawnerConfig->WriteStatistics + && Session.Type == GAME_IPX; + } + + return false; +} + + +static bool Is_Statistics_Enabled() +{ + return Is_Spawner_Write_Statistics() || Session.Type == GAME_INTERNET; +} + + +/** + * A fake class for implementing new member functions which allow + * access to the "this" pointer of the intended class. + * + * @note: This must not contain a constructor or destructor. + * + * @note: All functions must not be virtual and must also be prefixed + * with "_" to prevent accidental virtualization. + */ +class PacketClassExt : public PacketClass +{ +public: + char* _Create_Comms_Packet(int& size); + void _Add_Field_SCEN_ACCN_HASH(FieldClass* field); + void _Add_Field_Player_Data(FieldClass* field); +}; + + +/** + * Write statistics to a file for the client. + * + * @author: ZivDero + */ +char* PacketClassExt::_Create_Comms_Packet(int& size) +{ + char* result = Create_Comms_Packet(size); + + if (Is_Spawner_Write_Statistics()) + { + CCFileClass stats_file("stats.dmp"); + if (stats_file.Open(FILE_ACCESS_WRITE)) + { + stats_file.Write(result, size); + stats_file.Close(); + } + + GameStatisticsPacketSent = true; + } + + return result; +} + + +/** + * Add some scenario-related fields to the statistics packet. + * + * @author: ZivDero + */ +void PacketClassExt::_Add_Field_SCEN_ACCN_HASH(FieldClass* field) +{ + if (Is_Spawner_Write_Statistics()) + { + PacketClass::Add_Field(new FieldClass("SCEN", Vinifera_SpawnerConfig->UIMapName)); + PacketClass::Add_Field(new FieldClass("ACCN", PlayerPtr->IniName)); + PacketClass::Add_Field(new FieldClass("HASH", Vinifera_SpawnerConfig->MapHash)); + + return; + } + + PacketClass::Add_Field(field); +} + + +/** + * Add some player-related fields to the statistics packet. + * + * @author: ZivDero + */ +void PacketClassExt::_Add_Field_Player_Data(FieldClass* field) +{ + // This is the global string "NAM?" + // The game replaces "?" with the player's ID before this call, + // so we can grab it from there. + // It should be also be the house ID. + static auto& field_player_handle = Make_Global(0x0070FCF4); + + if (Is_Spawner_Write_Statistics()) + { + const char id = field_player_handle[3] - '0'; + + const HouseClass* house = Houses[id]; + const HouseClassExtension* house_ext = Extension::Fetch(house); + + if (house == PlayerPtr) + { + PacketClass::Add_Field(new FieldClass("MYID", static_cast(id))); + PacketClass::Add_Field(new FieldClass("NKEY", static_cast(0))); + PacketClass::Add_Field(new FieldClass("SKEY", static_cast(0))); + } + + static char field_player_allies[] = "ALY?"; + field_player_allies[3] = id; + PacketClass::Add_Field(new FieldClass(field_player_allies, static_cast(house->Allies))); + + static char field_player_spawn[] = "BSP?"; + field_player_spawn[3] = id; + PacketClass::Add_Field(new FieldClass(field_player_spawn, static_cast(ScenExtension->StartingPositions[id]))); + + static char field_player_observer[] = "SPC?"; + field_player_observer[3] = id; + PacketClass::Add_Field(new FieldClass(field_player_observer, static_cast(house_ext->IsObserver))); + } + + PacketClass::Add_Field(field); +} + + +/** + * Numerous patches to enable statistics collection. + * + * @author: ZivDero + */ +DECLARE_PATCH(_Print_MP_Stats_Check) +{ + if (Is_Statistics_Enabled()) + { + JMP(0x00463542); + } + + JMP(0x0046371F); +} + + +DECLARE_PATCH(_Kick_Player_Now_SendStatistics) +{ + if (Is_Statistics_Enabled()) + { + JMP(0x005B433C); + } + + JMP(0x005B439F); +} + + +DECLARE_PATCH(_Queue_AI_Multiplayer_SendStatistics) +{ + if (Is_Statistics_Enabled()) + { + JMP(0x005B1EA0); + } + + JMP(0x005B1F21); +} + + +DECLARE_PATCH(_Main_Loop_SendStatistics1) +{ + if (Is_Statistics_Enabled()) + { + JMP(0x00509229); + } + + JMP(0x0050924B); +} + + +DECLARE_PATCH(_Main_Loop_SendStatistics2) +{ + if (Is_Statistics_Enabled()) + { + JMP(0x00509283); + } + + JMP(0x005092A5); +} + + +DECLARE_PATCH(_Execute_DoList_SendStatistics1) +{ + if (Is_Statistics_Enabled()) + { + JMP(0x005B4FB9); + } + + JMP(0x005B500C); +} + + +DECLARE_PATCH(_Execute_DoList_SendStatistics2) +{ + if (Is_Statistics_Enabled()) + { + JMP(0x005B4FDE); + } + + JMP(0x005B500C); +} + + +DECLARE_PATCH(_Main_Game_Start_Timer) +{ + if (Is_Statistics_Enabled()) + { + JMP(0x00462A2F); + } + + JMP(0x00462A46); +} + + +/** + * Don't send statistics for observers. + * + * @author: ZivDero + */ +DECLARE_PATCH(_Send_Statistics_Packet_Send_AI_Dont_Send_Observers) +{ + GET_REGISTER_STATIC(HouseClass**, house, edx); + static HouseClassExtension* house_ext; + + _asm pushad + + if (Is_Spawner_Write_Statistics()) + { + house_ext = Extension::Fetch(*house); + if ((*house)->Class->IsMultiplayPassive || house_ext->IsObserver) + { + _asm popad + JMP_REG(ecx, 0x006098EC); + } + } + else // Vanilla condition + { + if (!(*house)->IsHuman) + { + _asm popad + JMP_REG(ecx, 0x006098EC); + } + } + + _asm popad + JMP_REG(ecx, 0x006098E6); +} + + +/** + * Main function for patching the hooks. + */ +void Statistics_Hooks() +{ + Patch_Call(0x0060A797, &PacketClassExt::_Create_Comms_Packet); + Patch_Jump(0x0046353C, &_Print_MP_Stats_Check); + Patch_Jump(0x005B4333, &_Kick_Player_Now_SendStatistics); + Patch_Jump(0x005B1E94, &_Queue_AI_Multiplayer_SendStatistics); + Patch_Jump(0x00509220, &_Main_Loop_SendStatistics1); + Patch_Jump(0x0050927A, &_Main_Loop_SendStatistics2); + Patch_Jump(0x005B4FAE, &_Execute_DoList_SendStatistics1); + Patch_Jump(0x005B4FD3, &_Execute_DoList_SendStatistics2); + Patch_Jump(0x00462A26, &_Main_Game_Start_Timer); + Patch_Call(0x0060982A, &PacketClassExt::_Add_Field_SCEN_ACCN_HASH); + Patch_Call(0x00609DA6, &PacketClassExt::_Add_Field_Player_Data); + //Patch_Jump(0x006098DA, &_Send_Statistics_Packet_Send_AI_Dont_Send_Observers); // Crashes. We write whether the player is the observer in the statistics anyway + Patch_Jump(0x0060A79C, 0x0060A7C6); // Skip call to some WOL utility to send the packet +} diff --git a/src/cncnet/cncnet5/cncnet5_globals.cpp b/src/spawner/modules/statistics_hooks.h similarity index 71% rename from src/cncnet/cncnet5/cncnet5_globals.cpp rename to src/spawner/modules/statistics_hooks.h index b30f9fe8a..95b8d8af5 100644 --- a/src/cncnet/cncnet5/cncnet5_globals.cpp +++ b/src/spawner/modules/statistics_hooks.h @@ -4,11 +4,11 @@ * * @project Vinifera * - * @file CNCNET_GLOBALS.CPP + * @file STATISTICS_HOOKS.H * - * @author CCHyper + * @author ZivDero * - * @brief Global values and types used for the CnCNet5 system. + * @brief Contains the hooks for statistics collection. * * @license Vinifera is free software: you can redistribute it and/or * modify it under the terms of the GNU General Public License @@ -25,20 +25,7 @@ * If not, see . * ******************************************************************************/ -#include "cncnet5_globals.h" +#pragma once -/** - * Has the CnCNet5 system been activated? - */ -bool CnCNet5::IsActive = false; - -/** - * Is the tunnel system active (set when tunnel information has been provided)? - */ -bool CnCNet5::IsTunnelActive = false; - -/** - * CnCNet5 UDP Tunnel info. - */ -CnCNet5::TunnelInfoStruct CnCNet5::TunnelInfo { -1, -1, -1, false }; +void Statistics_Hooks(); diff --git a/src/cncnet/cncnet5/cncnet5_wspudp.cpp b/src/spawner/net/cncnet5_wspudp.cpp similarity index 89% rename from src/cncnet/cncnet5/cncnet5_wspudp.cpp rename to src/spawner/net/cncnet5_wspudp.cpp index bc6873233..06a0d72ff 100644 --- a/src/cncnet/cncnet5/cncnet5_wspudp.cpp +++ b/src/spawner/net/cncnet5_wspudp.cpp @@ -36,7 +36,6 @@ */ CnCNet5UDPInterfaceClass::CnCNet5UDPInterfaceClass(unsigned short id, unsigned long ip, unsigned short port, bool port_hack) : UDPInterfaceClass(), - IsEnabled(false), AddressList(), TunnelID(id), TunnelIP(ip), @@ -55,14 +54,7 @@ CnCNet5UDPInterfaceClass::CnCNet5UDPInterfaceClass(unsigned short id, unsigned l */ LRESULT CnCNet5UDPInterfaceClass::Message_Handler(HWND hWnd, UINT uMsg, UINT wParam, LONG lParam) { - /** - * If the CnCNet interface has not been enabled, just use the standard UDP interface. - */ - if (!IsEnabled) { - return UDPInterfaceClass::Message_Handler(hWnd, uMsg, wParam, lParam); - } - - struct sockaddr_in addr; + sockaddr_in addr; int rc; int addr_len; WinsockBufferType *packet; @@ -94,9 +86,9 @@ LRESULT CnCNet5UDPInterfaceClass::Message_Handler(HWND hWnd, UINT uMsg, UINT wPa * Call the CnCNet tunnel Receive_From function to get the outstanding packet. */ addr_len = sizeof(addr); - rc = CnCNet5UDPInterfaceClass::Receive_From(Socket, (char*)ReceiveBuffer, sizeof(ReceiveBuffer), 0, (PSOCKADDR_IN)&addr, &addr_len); + rc = Receive_From(Socket, reinterpret_cast(ReceiveBuffer), sizeof(ReceiveBuffer), 0, &addr, &addr_len); if (rc == SOCKET_ERROR) { - DEBUG_WARNING("CnCNet5: Send_To returned %d!\n", WSAGetLastError()); + DEBUG_WARNING("CnCNet5: Receive_From returned %d!\n", WSAGetLastError()); Clear_Socket_Error(Socket); return 0; } @@ -143,15 +135,16 @@ LRESULT CnCNet5UDPInterfaceClass::Message_Handler(HWND hWnd, UINT uMsg, UINT wPa * Create a new buffer and store this packet in it. */ packet = Get_New_In_Buffer(); - packet->BufferLen = rc; - std::memcpy(packet->PacketData.Buffer, ReceiveBuffer, rc); + packet->BufferLen = rc - sizeof(packet->PacketData.CRC); + packet->PacketData.CRC = *reinterpret_cast(ReceiveBuffer); + std::memcpy(packet->PacketData.Buffer, ReceiveBuffer + sizeof(packet->PacketData.CRC), rc - sizeof(packet->PacketData.CRC)); if (!Passes_CRC_Check(packet)) { DEBUG_INFO("CnCNet5: Throwing away malformed packet!\n"); Delete_In_Buffer(packet); return 0; } std::memset(packet->Address, 0, sizeof (packet->Address)); - std::memcpy(packet->Address+4, &addr.sin_addr.s_addr, 4); + std::memcpy(packet->Address + 4, &addr.sin_addr.s_addr, 4); InBuffers.Add(packet); } return 0; @@ -185,7 +178,7 @@ LRESULT CnCNet5UDPInterfaceClass::Message_Handler(HWND hWnd, UINT uMsg, UINT wPa /** * (CnCNet) pull index. */ - int i = addr.sin_addr.s_addr - 1; + const int i = *reinterpret_cast(packet->Address + 4) - 1; /** * (CnCNet) validate index. @@ -207,8 +200,8 @@ LRESULT CnCNet5UDPInterfaceClass::Message_Handler(HWND hWnd, UINT uMsg, UINT wPa * at this time. In this case, we clear the socket error and just exit. Winsock will * send us another WRITE message when it is ready to receive more data. */ - rc = CnCNet5UDPInterfaceClass::Send_To(Socket, (const char *)&packet->PacketData, packet->BufferLen, 0, (PSOCKADDR_IN)&addr, sizeof (addr)); - if (rc == SOCKET_ERROR){ + rc = Send_To(Socket, reinterpret_cast(&packet->PacketData), packet->BufferLen + sizeof(packet->PacketData.CRC), 0, &addr, sizeof(addr)); + if (rc == SOCKET_ERROR) { if (WSAGetLastError() != WSAEWOULDBLOCK) { Clear_Socket_Error(Socket); return 0; @@ -241,7 +234,7 @@ int CnCNet5UDPInterfaceClass::Send_To(SOCKET s, const char *buf, int len, int fl /** * No processing if no tunnel. */ - if (TunnelPort == -1) { + if (TunnelPort == 0) { DEBUG_WARNING("CnCNet5: TunnelPort is invalid in Send_To!\n"); return sendto(s, buf, len, flags, (const sockaddr *)dest_addr, addrlen); } @@ -282,9 +275,9 @@ int CnCNet5UDPInterfaceClass::Receive_From(SOCKET s, char *buf, int len, int fla /** * No processing if no tunnel. */ - if (TunnelPort == -1) { + if (TunnelPort == 0) { DEBUG_WARNING("CnCNet5: TunnelPort is invalid in Recieve_From!\n"); - return recvfrom(s, buf, len, flags, (sockaddr *)src_addr, addrlen); + return recvfrom(s, buf, len, flags, reinterpret_cast(src_addr), addrlen); } #ifndef NDEBUG @@ -294,7 +287,7 @@ int CnCNet5UDPInterfaceClass::Receive_From(SOCKET s, char *buf, int len, int fla /** * Call recvfrom first to get the packet. */ - int ret = recvfrom(s, tempbuf, sizeof tempbuf, flags, (sockaddr *)src_addr, addrlen); + int ret = recvfrom(s, tempbuf, sizeof(tempbuf), flags, reinterpret_cast(src_addr), addrlen); /** * No processing if returning error or less than 5 bytes of data. diff --git a/src/cncnet/cncnet5/cncnet5_wspudp.h b/src/spawner/net/cncnet5_wspudp.h similarity index 87% rename from src/cncnet/cncnet5/cncnet5_wspudp.h rename to src/spawner/net/cncnet5_wspudp.h index d1e5349a8..38913038b 100644 --- a/src/cncnet/cncnet5/cncnet5_wspudp.h +++ b/src/spawner/net/cncnet5_wspudp.h @@ -31,11 +31,11 @@ #include "tibsun_defines.h" -typedef struct TunnelAddress +struct TunnelAddress { unsigned long IP; - unsigned long Port; -} TunnelAddress; + unsigned short Port; +}; /** @@ -49,7 +49,7 @@ class CnCNet5UDPInterfaceClass : public UDPInterfaceClass { public: CnCNet5UDPInterfaceClass(unsigned short id, unsigned long ip, unsigned short port, bool port_hack = false); - virtual ~CnCNet5UDPInterfaceClass() {} + virtual ~CnCNet5UDPInterfaceClass() override = default; virtual LRESULT Message_Handler(HWND hWnd, UINT uMsg, UINT wParam, LONG lParam) override; @@ -58,13 +58,6 @@ class CnCNet5UDPInterfaceClass : public UDPInterfaceClass int Receive_From(SOCKET s, char *buf, int len, int flags, sockaddr_in *src_addr, int *addrlen); public: - /** - * Should be CnCNet5 tunnel system interface be used over WinSock? - * - * @note: This should manually be set after instantiating the class. - */ - bool IsEnabled; - TunnelAddress AddressList[MAX_PLAYERS]; unsigned short TunnelID; diff --git a/src/spawner/net/latencylevel.cpp b/src/spawner/net/latencylevel.cpp new file mode 100644 index 000000000..d653526b9 --- /dev/null +++ b/src/spawner/net/latencylevel.cpp @@ -0,0 +1,141 @@ +/******************************************************************************* +/* O P E N S O U R C E -- V I N I F E R A ** +/******************************************************************************* + * + * @project Vinifera + * + * @file LATENCYLEVEL.CPP + * + * @author Belonit, ZivDero + * + * @brief Protocol zero latency level class. + * + * @license Vinifera 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. + * + * Vinifera 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 . + * + ******************************************************************************/ + +#include "latencylevel.h" + +#include "colorscheme.h" +#include "protocolzero.h" +#include "debughandler.h" +#include "house.h" +#include "session.h" +#include "rules.h" + + +LatencyLevelEnum LatencyLevel::CurentLatencyLevel = LATENCY_LEVEL_INITIAL; +unsigned char LatencyLevel::NewFrameSendRate = 3; + + +/** + * Sets the desired latency level. + * + * @author: Belonit + */ +void LatencyLevel::Apply(LatencyLevelEnum new_latency_level) +{ + if (new_latency_level > LATENCY_LEVEL_MAX) + new_latency_level = LATENCY_LEVEL_MAX; + + auto max_latency_level = static_cast(ProtocolZero::MaxLatencyLevel); + if (new_latency_level > max_latency_level) + new_latency_level = max_latency_level; + + if (new_latency_level <= CurentLatencyLevel) + return; + + DEBUG_INFO("[Spawner] Player %ls, LatencyMode (%d, %d) Frame = %d\n" + , PlayerPtr->IniName + , new_latency_level + , CurentLatencyLevel + , Frame + ); + + CurentLatencyLevel = new_latency_level; + NewFrameSendRate = static_cast(new_latency_level); + Session.PrecalcDesiredFrameRate = 60; + Session.PrecalcMaxAhead = Get_Max_Ahead(new_latency_level); + Session.Messages.Add_Message(nullptr, 0, Get_Latency_Message(new_latency_level), ColorScheme::From_Name("White"), TPF_USE_GRAD_PAL | TPF_FULLSHADOW | TPF_6PT_GRAD, static_cast(Rule->MessageDelay * TICKS_PER_MINUTE / 2)); +} + + +/** + * Gets the max ahead for the given latency level. + * + * @author: Belonit + */ +unsigned int LatencyLevel::Get_Max_Ahead(LatencyLevelEnum latency_level) +{ + const int maxAhead[] = + { + /* 0 */ 1, + + /* 1 */ 4, + /* 2 */ 6, + /* 3 */ 12, + /* 4 */ 16, + /* 5 */ 20, + /* 6 */ 24, + /* 7 */ 28, + /* 8 */ 32, + /* 9 */ 36 + }; + + return maxAhead[latency_level]; +} + + +/** + * Gets the chat message for the given latency level. + * + * @author: Belonit + */ +const char* LatencyLevel::Get_Latency_Message(LatencyLevelEnum latency_level) +{ + const char* message[] = + { + /* 0 */ "CnCNet: Latency mode set to: 0 - Initial", // Players should never see this, if they do, then it's a bug + + /* 1 */ "CnCNet: Latency mode set to: 1 - Best", + /* 2 */ "CnCNet: Latency mode set to: 2 - Super", + /* 3 */ "CnCNet: Latency mode set to: 3 - Excellent", + /* 4 */ "CnCNet: Latency mode set to: 4 - Very Good", + /* 5 */ "CnCNet: Latency mode set to: 5 - Good", + /* 6 */ "CnCNet: Latency mode set to: 6 - Good", + /* 7 */ "CnCNet: Latency mode set to: 7 - Default", + /* 8 */ "CnCNet: Latency mode set to: 8 - Default", + /* 9 */ "CnCNet: Latency mode set to: 9 - Default", + }; + + return message[latency_level]; +} + + +/** + * Gets the latency level for the given response time. + * + * @author: Belonit + */ +LatencyLevelEnum LatencyLevel::From_Response_Time(unsigned int response_time) +{ + for (char i = LATENCY_LEVEL_1; i < LATENCY_LEVEL_MAX; i++) + { + if (response_time <= Get_Max_Ahead(static_cast(i))) + return static_cast(i); + } + + return LATENCY_LEVEL_MAX; +} diff --git a/src/cncnet/cncnet4/cncnet4_globals.h b/src/spawner/net/latencylevel.h similarity index 50% rename from src/cncnet/cncnet4/cncnet4_globals.h rename to src/spawner/net/latencylevel.h index 098937913..4f4ac7e85 100644 --- a/src/cncnet/cncnet4/cncnet4_globals.h +++ b/src/spawner/net/latencylevel.h @@ -4,11 +4,11 @@ * * @project Vinifera * - * @file CNCNET4_GLOBALS.H + * @file LATENCYLEVEL.H * - * @author CCHyper + * @author Belonit, ZivDero * - * @brief CnCNet4 global values. + * @brief Protocol zero latency level class. * * @license Vinifera is free software: you can redistribute it and/or * modify it under the terms of the GNU General Public License @@ -27,30 +27,42 @@ ******************************************************************************/ #pragma once -#include -#include +enum LatencyLevelEnum : unsigned char +{ + LATENCY_LEVEL_INITIAL = 0, -namespace CnCNet4 { + LATENCY_LEVEL_1 = 1, + LATENCY_LEVEL_2 = 2, + LATENCY_LEVEL_3 = 3, + LATENCY_LEVEL_4 = 4, + LATENCY_LEVEL_5 = 5, + LATENCY_LEVEL_6 = 6, + LATENCY_LEVEL_7 = 7, + LATENCY_LEVEL_8 = 8, + LATENCY_LEVEL_9 = 9, -extern bool IsEnabled; + LATENCY_LEVEL_MAX = LATENCY_LEVEL_9, + LATENCY_SIZE = 1 + LATENCY_LEVEL_MAX +}; -extern char Host[256]; -extern unsigned Port; -extern bool Peer2Peer; -extern bool IsDedicated; -extern bool UseUDP; -extern struct sockaddr_in Server; - -}; // namespace CnCNet4 +/** + * LatencyLevel + * + * This class is contains methods for working with Protocol 0 latency levels. + */ +class LatencyLevel +{ +public: + LatencyLevel() = delete; + static LatencyLevelEnum CurentLatencyLevel; + static unsigned char NewFrameSendRate; -int __stdcall bind_intercept(SOCKET s, const struct sockaddr *name, int namelen); -int __stdcall closesocket_intercept(SOCKET s); -int __stdcall getsockname_intercept(SOCKET s, struct sockaddr *name, int *namelen); -int __stdcall getsockopt_intercept(SOCKET s, int level, int optname, char *optval, int *optlen); -int __stdcall recvfrom_intercept(SOCKET s, char *buf, int len, int flags, struct sockaddr *from, int *fromlen); -int __stdcall sendto_intercept(SOCKET s, const char *buf, int len, int flags, const struct sockaddr *to, int tolen); -int __stdcall setsockopt_intercept(SOCKET s, int level, int optname, const char *optval, int optlen); -SOCKET __stdcall socket_intercept(int af, int type, int protocol); + static void Apply(LatencyLevelEnum new_latency_level); + static void Apply(unsigned char new_latency_level) { Apply(static_cast(new_latency_level)); } + static unsigned int Get_Max_Ahead(LatencyLevelEnum latency_level); + static const char* Get_Latency_Message(LatencyLevelEnum latency_level); + static LatencyLevelEnum From_Response_Time(unsigned int response_time); +}; diff --git a/src/spawner/net/protocolzero.cpp b/src/spawner/net/protocolzero.cpp new file mode 100644 index 000000000..e223d3abf --- /dev/null +++ b/src/spawner/net/protocolzero.cpp @@ -0,0 +1,156 @@ +/******************************************************************************* +/* O P E N S O U R C E -- V I N I F E R A ** +/******************************************************************************* + * + * @project Vinifera + * + * @file PROTOCOLZERO.CPP + * + * @author Belonit, ZivDero + * + * @brief Protocol zero. + * + * @license Vinifera 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. + * + * Vinifera 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 . + * + ******************************************************************************/ + +#include "protocolzero.h" + +#include "latencylevel.h" +#include "spawner.h" +#include "viniferaevent/viniferaevent.h" +#include "house.h" +#include "session.h" +#include "ipxmgr.h" + +#include "debughandler.h" + + +bool ProtocolZero::Enable = false; +bool ProtocolZero::GetRealMaxAhead = false; +unsigned int ProtocolZero::WorstMaxAhead = 24; +unsigned char ProtocolZero::MaxLatencyLevel = 0xff; + + +/** + * Sends a Response Time event. + * + * @author: Belonit + */ +void ProtocolZero::Send_Response_Time() +{ + if (Enable == false || Session.Singleplayer_Game()) + return; + + static int NextSendFrame = 6 * SendResponseTimeInterval; + + /** + * It is not yet time to send a Response Time event. + */ + if (Frame <= NextSendFrame) + return; + + /** + * IPXManagerClass::Response_Time is patched to return ProtocolZero::MaxAhead, + * so to get the real MaxAhead we set this bool to true just for this call. + */ + GetRealMaxAhead = true; + const unsigned int ipxResponseTime = Ipx.Response_Time(); + GetRealMaxAhead = false; + + /** + * Create the event. + */ + ViniferaEventClass event; + event.Type = VEVENT_RESPONSE_TIME_2; + event.ID = PlayerPtr->Get_Heap_ID(); + event.Frame = Frame + Session.MaxAhead; + event.Data.ResponseTime2.MaxAhead = static_cast(ipxResponseTime + 1); + event.Data.ResponseTime2.LatencyLevel = LatencyLevel::From_Response_Time(ipxResponseTime); + + /** + * Send it! + */ + if (OutList.Add(event.As_Event())) + { + NextSendFrame = Frame + SendResponseTimeInterval; + DEBUG_INFO("[Spawner] Player %d sending response time of %u, LatencyMode = %d, Frame = %d\n" + , event.ID + , event.Data.ResponseTime2.MaxAhead + , event.Data.ResponseTime2.LatencyLevel + , Frame + ); + } + else + { + NextSendFrame++; + } +} + + +/** + * Executes a Response Time event. + * + * @author: Belonit + */ +void ProtocolZero::Handle_Response_Time(ViniferaEventClass* event) +{ + if (Enable == false || Session.Singleplayer_Game()) + return; + + if (event->Data.ResponseTime2.MaxAhead == 0) + { + DEBUG_INFO("[Spawner] Returning because event->MaxAhead == 0\n"); + return; + } + + static unsigned int PlayerMaxAheads[8] = { 0, 0, 0, 0, 0, 0, 0, 0 }; + static unsigned char PlayerLatencyMode[8] = { 0, 0, 0, 0, 0, 0, 0, 0 }; + static unsigned int PlayerLastTimingFrame[8] = { 0, 0, 0, 0, 0, 0, 0, 0 }; + + /** + * Save the info we got from the event. + */ + PlayerMaxAheads[event->ID] = event->Data.ResponseTime2.MaxAhead; + PlayerLatencyMode[event->ID] = event->Data.ResponseTime2.LatencyLevel; + PlayerLastTimingFrame[event->ID] = event->Frame; + + /** + * Now loop all the players and find the worst one latency-wise. + */ + unsigned char latency_mode = 0; + unsigned int max_ahead = 0; + + for (size_t i = 0; i < std::size(PlayerMaxAheads); i++) + { + if (PlayerLastTimingFrame[i] + SendResponseTimeInterval * 4 < Frame) + { + PlayerMaxAheads[i] = 0; + PlayerLatencyMode[i] = 0; + } + else + { + max_ahead = PlayerMaxAheads[i] > max_ahead ? PlayerMaxAheads[i] : max_ahead; + if (PlayerLatencyMode[i] > latency_mode) + latency_mode = PlayerLatencyMode[i]; + } + } + + /** + * The worst determines the settings for all the players. + */ + WorstMaxAhead = max_ahead; + LatencyLevel::Apply(latency_mode); +} diff --git a/src/spawner/net/protocolzero.h b/src/spawner/net/protocolzero.h new file mode 100644 index 000000000..ab5559326 --- /dev/null +++ b/src/spawner/net/protocolzero.h @@ -0,0 +1,51 @@ +/******************************************************************************* +/* O P E N S O U R C E -- V I N I F E R A ** +/******************************************************************************* + * + * @project Vinifera + * + * @file PROTOCOLZERO.H + * + * @author Belonit, ZivDero + * + * @brief Protocol zero. + * + * @license Vinifera 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. + * + * Vinifera 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 . + * + ******************************************************************************/ +#pragma once + +class ViniferaEventClass; + + +/** + * ProtocolZero + * + * This class is contains methods and the state of Protocol 0. + */ +class ProtocolZero +{ +private: + static constexpr int SendResponseTimeInterval = 30; + +public: + static bool Enable; + static bool GetRealMaxAhead; + static unsigned char MaxLatencyLevel; + static unsigned int WorstMaxAhead; + + static void Send_Response_Time(); + static void Handle_Response_Time(ViniferaEventClass* event); +}; diff --git a/src/spawner/net/protocolzero_hooks.cpp b/src/spawner/net/protocolzero_hooks.cpp new file mode 100644 index 000000000..e7a57330a --- /dev/null +++ b/src/spawner/net/protocolzero_hooks.cpp @@ -0,0 +1,295 @@ +/******************************************************************************* +/* O P E N S O U R C E -- V I N I F E R A ** +/******************************************************************************* + * + * @project Vinifera + * + * @file PROTOCOLZERO_HOOKS.CPP + * + * @author ZivDero + * + * @brief Contains the hooks for protocol zero. + * + * @license Vinifera 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. + * + * Vinifera 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 . + * + ******************************************************************************/ + +#include "protocolzero_hooks.h" + +#include "hooker.h" +#include "hooker_macros.h" +#include "latencylevel.h" +#include "protocolzero.h" +#include "tibsun_globals.h" +#include "session.h" +#include "viniferaevent/viniferaevent.h" +#include "ipxmgr.h" +#include "scenario.h" +#include "spawner.h" + +/** + * A fake class for implementing new member functions which allow + * access to the "this" pointer of the intended class. + * + * @note: This must not contain a constructor or destructor. + * + * @note: All functions must not be virtual and must also be prefixed + * with "_" to prevent accidental virtualization. + */ +class IPXManagerClassExt : public IPXManagerClass +{ +public: + void _Set_Timing(unsigned long retrydelta, unsigned long maxretries, unsigned long timeout, bool global = true); + unsigned long _Response_Time(); +}; + + +/** + * Patch to log network parameters. + * + * @author: ZivDero + */ +void IPXManagerClassExt::_Set_Timing(unsigned long retrydelta, unsigned long maxretries, unsigned long timeout, bool global) +{ + if (ProtocolZero::Enable) { + DEBUG_INFO("[Spawner] NewRetryDelta = %d, NewRetryTimeout = %d, FrameSendRate = %d, CurentLatencyLevel = %d\n" + , retrydelta + , maxretries + , Session.FrameSendRate + , LatencyLevel::CurentLatencyLevel + ); + } + + /** + * Vanilla function. + */ + DEBUG_INFO("RetryDelta = %d\n", retrydelta); + DEBUG_INFO("MaxAhead is %d\n", Session.MaxAhead); + + RetryDelta = retrydelta; + MaxRetries = maxretries; + Timeout = timeout; + + if (global) { + Set_External_Timing(RetryDelta, MaxRetries, Timeout); + } + + for (int i = 0; i < NumConnections; i++) { + Connection[i]->Set_Retry_Delta(RetryDelta); + Connection[i]->Set_Max_Retries(MaxRetries); + Connection[i]->Set_TimeOut(Timeout); + } +} + + +/** + * Patch IPXManagerClass to return our MaxAhead when Protocol 0 is active. + * + * @author: ZivDero + */ +unsigned long IPXManagerClassExt::_Response_Time() +{ + if (ProtocolZero::Enable && !ProtocolZero::GetRealMaxAhead) { + return ProtocolZero::WorstMaxAhead; + } + + // Vanilla function + unsigned long maxresp = 0; + + for (int i = 0; i < NumConnections; i++) { + unsigned long resp = Connection[i]->Queue->Avg_Response_Time(); + if (resp > maxresp) { + maxresp = resp; + } + } + + return maxresp; +} + + +/** + * A fake class for implementing new member functions which allow + * access to the "this" pointer of the intended class. + * + * @note: This must not contain a constructor or destructor. + * + * @note: All functions must not be virtual and must also be prefixed + * with "_" to prevent accidental virtualization. + */ +class MessageListClassExt : public MessageListClass +{ +public: + bool _Manage(); +}; + + +/** + * Convenient patch in Main_Loop to send a Response Time event. + * + * @author: ZivDero + */ +bool MessageListClassExt::_Manage() +{ + if (ProtocolZero::Enable) + ProtocolZero::Send_Response_Time(); + + return MessageListClass::Manage(); +} + + +/** + * A fake class for implementing new member functions which allow + * access to the "this" pointer of the intended class. + * + * @note: This must not contain a constructor or destructor. + * + * @note: All functions must not be virtual and must also be prefixed + * with "_" to prevent accidental virtualization. + */ +class EventClassExt : public EventClass +{ +public: + void _Execute_Timing(); +}; + + +/** + * Skip checking MySent, if Protocol 0 is active. + * + * @author: ZivDero + */ +static short& MySent = Make_Global(0x008099F0); +DECLARE_PATCH(_ProtocolZero_Queue_AI_Multiplayer_1) +{ + if (ProtocolZero::Enable || MySent >= 5) + { + JMP(0x005B1A3B); + } + + JMP(0x005B1C4C); +} + + +/** + * Adds the adjusted Timing event. + * + * @author: ZivDero + */ +static void Add_Timing_Event() +{ + DEBUG_INFO("[Spawner] Sending precalculated network timings on frame %d\n", Frame); + + EventClass ev; + ev.Type = EVENT_TIMING; + ev.Data.Timing.DesiredFrameRate = Session.PrecalcDesiredFrameRate; + ev.Data.Timing.MaxAhead = Session.PrecalcMaxAhead; + ev.Data.Timing.FrameSendRate = ProtocolZero::Enable ? LatencyLevel::NewFrameSendRate : + Session.PrecalcDesiredFrameRate > 30 ? 10 : 5; + + OutList.Add(ev); + Session.PrecalcMaxAhead = 0; + Session.PrecalcDesiredFrameRate = 0; +} + + +/** + * Patch adding the Timing event, taking Protocol 0 into consideration. + * + * @author: ZivDero + */ +DECLARE_PATCH(_ProtocolZero_Queue_AI_Multiplayer_2) +{ + Add_Timing_Event(); + JMP(0x005B1C4C); +} + + +/** + * Adds the adjusted Timing event. + * + * @author: ZivDero + */ +static void Add_Timing_Event_2(int max_ahead) +{ + EventClass ev; + ev.Type = EVENT_TIMING; + ev.Data.Timing.DesiredFrameRate = Session.DesiredFrameRate; + ev.Data.Timing.MaxAhead = ProtocolZero::Enable ? Session.MaxAhead : (max_ahead + Scen->SpecialFlags.IsFogOfWar ? 10 : 0); + ev.Data.Timing.FrameSendRate = Session.FrameSendRate; + + OutList.Add(ev); +} + + +/** + * Patch adding the Timing event, taking Protocol 0 into consideration. + * + * @author: ZivDero + */ +DECLARE_PATCH(_ProtocolZero_Queue_AI_Multiplayer_3) +{ + GET_REGISTER_STATIC(int, max_ahead, edi); + _asm push esi + + Add_Timing_Event_2(max_ahead); + + _asm pop esi + JMP(0x005B1BB9); +} + + +/** + * If Protocol 0 is enabled, allow some types of packets to come in late. + * + * @author: ZivDero + */ +DECLARE_PATCH(_ProtocolZero_ExecuteDoList) +{ + GET_REGISTER_STATIC(EventClass*, event, esi) + + if (ProtocolZero::Enable) + { + if (event->Type == EVENT_EMPTY) + goto continue_execution; + + if (event->Type == EVENT_PROCESS_TIME) + goto continue_execution; + + if (event->Type == VEVENT_RESPONSE_TIME_2) + goto continue_execution; + } + + _asm mov eax, Session + _asm mov eax, [eax] + JMP_REG(ecx, 0x005B4EAA); + + continue_execution: + JMP(0x005B4EB7); +} + + +/** + * Main function for patching the hooks. + */ +void ProtocolZero_Hooks() +{ + Patch_Call(0x005091A5, &MessageListClassExt::_Manage); + Patch_Jump(0x005B1A2D, &_ProtocolZero_Queue_AI_Multiplayer_1); + Patch_Jump(0x005B1BF1, &_ProtocolZero_Queue_AI_Multiplayer_2); + Patch_Jump(0x005B1B7A, &_ProtocolZero_Queue_AI_Multiplayer_3); + Patch_Jump(0x005B4EA5, &_ProtocolZero_ExecuteDoList); + Patch_Jump(0x004F05B0, &IPXManagerClassExt::_Set_Timing); + Patch_Jump(0x004F0F00, &IPXManagerClassExt::_Response_Time); +} diff --git a/src/spawner/net/protocolzero_hooks.h b/src/spawner/net/protocolzero_hooks.h new file mode 100644 index 000000000..d7fe78549 --- /dev/null +++ b/src/spawner/net/protocolzero_hooks.h @@ -0,0 +1,31 @@ +/******************************************************************************* +/* O P E N S O U R C E -- V I N I F E R A ** +/******************************************************************************* + * + * @project Vinifera + * + * @file PROTOCOLZERO_HOOKS.H + * + * @author ZivDero + * + * @brief Contains the hooks for protocol zero. + * + * @license Vinifera 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. + * + * Vinifera 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 . + * + ******************************************************************************/ +#pragma once + + +void ProtocolZero_Hooks(); diff --git a/src/spawner/spawner.cpp b/src/spawner/spawner.cpp new file mode 100644 index 000000000..42f0538cd --- /dev/null +++ b/src/spawner/spawner.cpp @@ -0,0 +1,509 @@ +/******************************************************************************* +/* O P E N S O U R C E -- V I N I F E R A ** +/******************************************************************************* + * + * @project Vinifera + * + * @file SPAWNER.CPP + * + * @author Belonit, ZivDero + * + * @brief Multiplayer spawner class. + * + * @license Vinifera 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. + * + * Vinifera 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 . + * + ******************************************************************************/ + +#include "spawner.h" +#include "protocolzero.h" +#include "latencylevel.h" + +#include "options.h" +#include "house.h" +#include "ipxmgr.h" +#include "loadoptions.h" +#include "scenario.h" +#include + +#include "addon.h" +#include "wspudp.h" +#include "wwmouse.h" +#include "ccini.h" +#include "cncnet5_wspudp.h" +#include "debughandler.h" +#include "extension_globals.h" +#include "gscreen.h" +#include "housetype.h" +#include "language.h" +#include "housetypeext.h" +#include "mouse.h" +#include "ownrdraw.h" +#include "saveload.h" +#include "tab.h" +#include "WinUser.h" +#include "sessionext.h" +#include "tibsun_functions.h" +#include "rules.h" +#include "vinifera_globals.h" + + +/** + * Initializes the Spawner. + * + * @author: ZivDero + */ +void Spawner::Init() +{ + Vinifera_SpawnerConfig = new SpawnerConfig; + + CCFileClass spawn_file("SPAWN.INI"); + CCINIClass spawn_ini; + + if (spawn_file.Is_Available()) { + + spawn_ini.Load(spawn_file, false); + Vinifera_SpawnerConfig->Read_INI(spawn_ini); + + } + else { + DEBUG_FATAL("SPAWN.INI not found!\n"); + } +} + + +/** + * Starts the game. + * + * @author: ZivDero + */ +bool Spawner::Start_Game() +{ + if (Vinifera_SpawnerActive) + return false; + + Vinifera_SpawnerActive = true; + GameActive = true; + + Init_UI(); + + const bool result = Start_Scenario(Vinifera_SpawnerConfig->ScenarioName); + + Prepare_Screen(); + + return result; +} + + +/** + * Starts a new scenario. + * + * @author: ZivDero + */ +bool Spawner::Start_Scenario(char* scenario_name) +{ + /** + * Can't read an unnamed file, bail. + */ + if (scenario_name[0] == 0 && !Vinifera_SpawnerConfig->LoadSaveGame) + { + DEBUG_INFO("[Spawner] Failed to read scenario [%s]\n", scenario_name); + MessageBox(MainWindow, Text_String(TXT_UNABLE_READ_SCENARIO), "Vinifera", MB_OK); + + return false; + } + + /** + * Turn Firestorm on, if requested. + */ + Disable_Addon(ADDON_ANY); + if (Vinifera_SpawnerConfig->Firestorm) + { + Enable_Addon(ADDON_FIRESTORM); + Set_Required_Addon(ADDON_FIRESTORM); + } + + /** + * Configure session options. + */ + strcpy_s(Session.ScenarioFileName, 0x200, scenario_name); + Session.Options.ScenarioIndex = -1; + Session.Options.Bases = Vinifera_SpawnerConfig->Bases; + Session.Options.Credits = Vinifera_SpawnerConfig->Credits; + Session.Options.BridgeDestruction = Vinifera_SpawnerConfig->BridgeDestroy; + Session.Options.Goodies = Vinifera_SpawnerConfig->Crates; + Session.Options.ShortGame = Vinifera_SpawnerConfig->ShortGame; + SessionExtension->ExtOptions.IsBuildOffAlly = Vinifera_SpawnerConfig->BuildOffAlly; + Session.Options.GameSpeed = Vinifera_SpawnerConfig->GameSpeed; + Session.Options.CrapEngineers = Vinifera_SpawnerConfig->MultiEngineer; + Session.Options.UnitCount = Vinifera_SpawnerConfig->UnitCount; + Session.Options.AIPlayers = Vinifera_SpawnerConfig->AIPlayers; + Session.Options.AIDifficulty = Vinifera_SpawnerConfig->AIDifficulty; + Session.Options.AlliesAllowed = Vinifera_SpawnerConfig->AlliesAllowed; + Session.Options.HarvesterTruce = Vinifera_SpawnerConfig->HarvesterTruce; + // Session.Options.CaptureTheFlag + Session.Options.FogOfWar = Vinifera_SpawnerConfig->FogOfWar; + Session.Options.RedeployMCV = Vinifera_SpawnerConfig->MCVRedeploy; + std::strcpy(Session.Options.ScenarioDescription, Vinifera_SpawnerConfig->UIMapName); + Session.ColorIdx = static_cast(Vinifera_SpawnerConfig->Players[0].Color); + Session.NumPlayers = Vinifera_SpawnerConfig->HumanPlayers; + + Seed = Vinifera_SpawnerConfig->Seed; + BuildLevel = Vinifera_SpawnerConfig->TechLevel; + Options.GameSpeed = Vinifera_SpawnerConfig->GameSpeed; + + Vinifera_NextAutoSaveNumber = Vinifera_SpawnerConfig->NextAutoSaveNumber; + + /** + * Create the player node for the local player. + */ + const auto nodename = new NodeNameType(); + Session.Players.Add(nodename); + + std::strcpy(nodename->Name, Vinifera_SpawnerConfig->Players[0].Name); + nodename->Player.House = static_cast(Vinifera_SpawnerConfig->Players[0].House); + nodename->Player.Color = static_cast(Vinifera_SpawnerConfig->Players[0].Color); + nodename->Player.ProcessTime = -1; + nodename->Game.LastTime = 1; + + /** + * Set session type. + */ + if (Vinifera_SpawnerConfig->IsCampaign) + Session.Type = GAME_NORMAL; + else if (Session.NumPlayers > 1) + Session.Type = GAME_INTERNET; // HACK: will be set to GAME_IPX later + else + Session.Type = GAME_SKIRMISH; + + + Init_Random(); + + /** + * Start the scenario. + */ + if (Session.Type == GAME_NORMAL) + { + Session.Options.Goodies = true; + + const bool result = Vinifera_SpawnerConfig->LoadSaveGame ? + Load_Game(Vinifera_SpawnerConfig->SaveGameName) : ::Start_Scenario(scenario_name, Vinifera_SpawnerConfig->PlayMoviesInMultiplayer, static_cast(Vinifera_SpawnerConfig->CampaignID)); + + return result; + } + else if (Session.Type == GAME_SKIRMISH) + { + const bool result = Vinifera_SpawnerConfig->LoadSaveGame ? + Load_Game(Vinifera_SpawnerConfig->SaveGameName) : ::Start_Scenario(scenario_name, false, CAMPAIGN_NONE); + + return result; + } + else + { + Init_Network(); + + bool result = Vinifera_SpawnerConfig->LoadSaveGame ? + Load_Game(Vinifera_SpawnerConfig->SaveGameName) : ::Start_Scenario(scenario_name, false, CAMPAIGN_NONE); + + if (!result) + return false; + + Session.Type = GAME_IPX; + + if (Vinifera_SpawnerConfig->LoadSaveGame && !Reconcile_Players()) + return false; + + if (!Session.Create_Connections()) + return false; + + return true; + } +} + + +/** + * Loads a saved game. + * + * @author: ZivDero + */ +bool Spawner::Load_Game(const char* file_name) +{ + if (!file_name[0] || !::Load_Game(file_name)) + { + DEBUG_INFO("[Spawner] Failed to load savegame [%s]\n", file_name); + MessageBox(MainWindow, Text_String(TXT_ERROR_LOADING_GAME), "Vinifera", MB_OK); + + return false; + } + + return true; +} + + +/** + * Initializes everything necessary for an MP game. + * + * @author: ZivDero + */ +void Spawner::Init_Network() +{ + const unsigned short tunnel_id = htons(Vinifera_SpawnerConfig->TunnelId); + const unsigned long tunnel_ip = inet_addr(Vinifera_SpawnerConfig->TunnelIp); + const unsigned short tunnel_port = htons(Vinifera_SpawnerConfig->TunnelPort); + + /** + * Create the UDP interface. + * This needs to happen before we set up player nodes, + * because it contains player connection data. + */ + const auto udp_interface = new CnCNet5UDPInterfaceClass(tunnel_id, tunnel_ip, tunnel_port, true); + PacketTransport = udp_interface; + + PlanetWestwoodPortNumber = tunnel_port ? 0 : Vinifera_SpawnerConfig->ListenPort; + + /** + * Set up the player nodes. + */ + const char max_players = std::size(Vinifera_SpawnerConfig->Players); + for (char player_index = 1; player_index < max_players; player_index++) + { + const auto player = &Vinifera_SpawnerConfig->Players[player_index]; + if (!player->IsHuman) + continue; + + const auto nodename = new NodeNameType(); + Session.Players.Add(nodename); + + std::strcpy(nodename->Name, player->Name); + nodename->Player.House = static_cast(player->House); + nodename->Player.Color = static_cast(player->Color); + nodename->Player.ProcessTime = -1; + nodename->Game.LastTime = 1; + + std::memset(&nodename->Address, 0, sizeof(nodename->Address)); + std::memcpy(&nodename->Address.NetworkNumber, &player_index, sizeof(player_index)); + std::memcpy(&nodename->Address.NodeAddress, &player_index, sizeof(player_index)); + + const auto ip = inet_addr(player->Ip); + const auto port = htons(player->Port); + udp_interface->AddressList[player_index - 1].IP = ip; + udp_interface->AddressList[player_index - 1].Port = port; + if (port != Vinifera_SpawnerConfig->ListenPort) + udp_interface->PortHack = false; + } + + /** + * Now set up the rest of the network stuff. + */ + PacketTransport->Init(); + PacketTransport->Open_Socket(0); + PacketTransport->Start_Listening(); + PacketTransport->Discard_In_Buffers(); + PacketTransport->Discard_Out_Buffers(); + Ipx.Set_Timing(60, -1, 600, true); + + PlanetWestwoodStartTime = time(nullptr); + GameFSSKU = 0x1C00; + GameSKU = 0x1D00; + + /** + * Set up protocol stuff. + */ + ProtocolZero::Enable = (Vinifera_SpawnerConfig->Protocol == 0); + if (ProtocolZero::Enable) + { + Session.FrameSendRate = 2; + Session.PrecalcMaxAhead = Vinifera_SpawnerConfig->PreCalcMaxAhead; + ProtocolZero::MaxLatencyLevel = std::clamp( + Vinifera_SpawnerConfig->MaxLatencyLevel, + static_cast(LATENCY_LEVEL_1), + static_cast(LATENCY_LEVEL_MAX)); + } + else + { + Session.FrameSendRate = Vinifera_SpawnerConfig->FrameSendRate; + } + + Session.MaxAhead = Vinifera_SpawnerConfig->MaxAhead == -1 + ? Session.FrameSendRate * 6 + : Vinifera_SpawnerConfig->MaxAhead; + + /** + * Miscellaneous network settings. + */ + Session.MaxMaxAhead = 0; + Session.CommProtocol = 2; + Session.LatencyFudge = 0; + Session.DesiredFrameRate = 60; + PlanetWestwoodTournament = static_cast(Vinifera_SpawnerConfig->Tournament); + PlanetWestwoodGameID = Vinifera_SpawnerConfig->WOLGameID; + FrameSyncSettings[GAME_IPX].Timeout = Vinifera_SpawnerConfig->ReconnectTimeout; + + /** + * For Quick Match, make sure MPDebug is off so that players can't cheat with it. + */ + if (Vinifera_SpawnerConfig->QuickMatch) + { + Session.MPlayerDebug = false; + } + + ::Init_Network(); +} + + +/** + * Reconciles loaded data with the "Players" vector. + * + * This function is for supporting loading a saved multiplayer game. + * When the game is loaded, we have to figure out which house goes with + * which entry in the Players vector. We also have to figure out if + * everyone who was originally in the game is still with us, and if not, + * turn their stuff over to the computer. + */ +bool Spawner::Reconcile_Players() +{ + int i; + bool found; + int house; + HouseClass* housep; + + /** + * If there are no players, there's nothing to do. + */ + if (Session.Players.Count() == 0) + return true; + + /** + * Make sure every name we're connected to can be found in a House. + */ + for (i = 0; i < Session.Players.Count(); i++) { + found = false; + for (house = 0; house < Session.Players.Count(); house++) { + housep = Houses[house]; + if (!housep) { + continue; + } + + if (!stricmp(Session.Players[i]->Name, housep->IniName)) { + found = true; + break; + } + } + if (!found) + return false; + } + + /** + * Loop through all Houses; if we find a human-owned house that we're + * not connected to, turn it over to the computer. + */ + for (house = 0; house < Session.Players.Count(); house++) { + housep = Houses[house]; + if (!housep) { + continue; + } + + /** + * Skip this house if it wasn't human to start with. + */ + if (!housep->IsHuman) { + continue; + } + + /** + * Try to find this name in the Players vector; if it's found, set + * its ID to this house. + */ + found = false; + for (i = 0; i < Session.Players.Count(); i++) { + if (!stricmp(Session.Players[i]->Name, housep->IniName)) { + found = true; + Session.Players[i]->Player.ID = static_cast(house); + break; + } + } + + /** + * If this name wasn't found, remove it + */ + if (!found) { + + /** + * Turn the player's house over to the computer's AI + */ + housep->IsHuman = false; + housep->IsStarted = true; + housep->IQ = Rule->MaxIQ; + + static char buffer[HOUSE_NAME_MAX + 1]; + std::snprintf(buffer, sizeof(buffer), "%s (AI)", housep->IniName); + std::strncpy(housep->IniName, buffer, sizeof(housep->IniName)); + //strcpy(housep->IniName, Fetch_String(TXT_COMPUTER)); + + Session.NumPlayers--; + } + } + + /** + * If all went well, our Session.NumPlayers value should now equal the value + * from the saved game, minus any players we removed. + */ + if (Session.NumPlayers == Session.Players.Count()) { + return true; + } + else { + return false; + } +} + + +/** + * Initializes some things for OwnerDraw UI. + * + * @author: ZivDero + */ +void Spawner::Init_UI() +{ + OwnerDraw::Init_UI_Color_Stuff_58F060(); + + if (!OwnerDraw::UIInitialized) + { + OwnerDraw::Init_Glow_Colors(); + OwnerDraw::Load_Graphics(); + OwnerDraw::UIInitialized = true; + } +} + + +/** + * Prepares the screen. + * + * @author: ZivDero + */ +void Spawner::Prepare_Screen() +{ + WWMouse->Hide_Mouse(); + + HiddenSurface->Fill(TBLACK); + GScreenClass::Blit(true, HiddenSurface); + LogicSurface = HiddenSurface; + + WWMouse->Show_Mouse(); + + Map.MouseClass::Set_Default_Mouse(MOUSE_NO_MOVE, false); + Map.MouseClass::Revert_Mouse_Shape(); + + Map.TabClass::Activate(1); + Map.SidebarClass::Flag_To_Redraw(); +} diff --git a/src/cncnet/cncnet4/cncnet4_globals.cpp b/src/spawner/spawner.h similarity index 65% rename from src/cncnet/cncnet4/cncnet4_globals.cpp rename to src/spawner/spawner.h index 3dca2d98d..5288b6b35 100644 --- a/src/cncnet/cncnet4/cncnet4_globals.cpp +++ b/src/spawner/spawner.h @@ -4,11 +4,11 @@ * * @project Vinifera * - * @file CNCNET4_GLOBALS.CPP + * @file SPAWNER.H * - * @author CCHyper + * @author Belonit, ZivDero * - * @brief CnCNet4 global values. + * @brief Multiplayer spawner class. * * @license Vinifera is free software: you can redistribute it and/or * modify it under the terms of the GNU General Public License @@ -25,31 +25,31 @@ * If not, see . * ******************************************************************************/ -#include "cncnet4_globals.h" -#include "cncnet4.h" +#pragma once -/** - * Is the CnCNet4 interface active? - */ -bool CnCNet4::IsEnabled = false; +#include "spawnerconfig.h" +#include "vinifera_globals.h" -/** - * The host name (Must be running a instance of the dedicated server). - */ -char CnCNet4::Host[256] = { "server.cncnet.org" }; -unsigned CnCNet4::Port = 9001; /** - * Clients connect to each other rather than the server? + * This class contains all logic for spawning players in-game (usually via the client). */ -bool CnCNet4::Peer2Peer = false; +class Spawner +{ +public: + Spawner() = delete; -bool CnCNet4::IsDedicated = false; + static void Init(); + static bool Start_Game(); -/** - * Use the UDP interface instead of IPX? - */ -bool CnCNet4::UseUDP = true; +private: + static bool Start_Scenario(char* scenario_name); + static bool Load_Game(const char* file_name); + + static void Init_Network(); + static bool Reconcile_Players(); -struct sockaddr_in CnCNet4::Server; + static void Init_UI(); + static void Prepare_Screen(); +}; diff --git a/src/spawner/spawner_hooks.cpp b/src/spawner/spawner_hooks.cpp new file mode 100644 index 000000000..1723aa983 --- /dev/null +++ b/src/spawner/spawner_hooks.cpp @@ -0,0 +1,222 @@ +/******************************************************************************* +/* O P E N S O U R C E -- V I N I F E R A ** +/******************************************************************************* + * + * @project Vinifera + * + * @file SPAWNER_HOOKS.CPP + * + * @author ZivDero + * + * @brief Contains the hooks for the multiplayer spawner class. + * + * @license Vinifera 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. + * + * Vinifera 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 . + * + ******************************************************************************/ + +#include "spawner_hooks.h" + +#include "hooker.h" +#include "hooker_macros.h" +#include "session.h" +#include "spawner.h" +#include "house.h" +#include "housetype.h" +#include "multiscore.h" +#include "protocolzero_hooks.h" +#include "quickmatch_hooks.h" +#include "observer_hooks.h" +#include "statistics_hooks.h" +#include "tibsun_functions.h" +#include "vinifera_globals.h" + + +/** + * A fake class for implementing new member functions which allow + * access to the "this" pointer of the intended class. + * + * @note: This must not contain a constructor or destructor. + * + * @note: All functions must not be virtual and must also be prefixed + * with "_" to prevent accidental virtualization. + */ +class SessionClassExt : public SessionClass +{ +public: + void _Read_Scenario_Descriptions(); +}; + + +/** + * Patches Read_Scenario_Descriptions to do nothing when the spawner is active. + * + * @author: ZivDero + */ +void SessionClassExt::_Read_Scenario_Descriptions() +{ + if (Vinifera_SpawnerActive) + return; + + SessionClass::Read_Scenario_Descriptions(); +} + + +/** + * Patches Expert AI not the consider allies as enemies. + * + * @author: ZivDero + */ +DECLARE_PATCH(_HouseClass_Expert_AI_Check_Allies) +{ + GET_REGISTER_STATIC(HouseClass*, this_ptr, edi); + GET_REGISTER_STATIC(HouseClass*, house, esi); + + if (house != this_ptr && !house->Class->IsMultiplayPassive && !house->IsDefeated && this_ptr->Is_Ally(house)) + { + JMP(0x004C06F7); + } + + JMP(0x004C0777); +} + + +/** + * Players skipping movies in multiplayer leads to disconnects. + * Prevent players from skipping movies in MP. + * + * @author: ZivDero, Rampastring + */ +DECLARE_PATCH(_Play_VQA_Forbid_Skipping_In_MP_Patch) +{ + GET_STACK_STATIC8(bool, cant_break_out, esp, 0x40); + + /** + * Don't skip the movie. + */ + if (cant_break_out || (Session.Type != GAME_NORMAL && Session.Type != GAME_SKIRMISH)) + { + JMP(0x0066BA30); + } + + /** + * Check if we want to skip the movie. + */ + JMP(0x0066BB61); +} + + +/** + * Hack VQA playback loop to do some network communication in MP + * so the tunnel server doesn't forget about us. + * + * @author: ZivDero, Rampastring + */ +DECLARE_PATCH(_Play_VQA_Network_Callback_Patch) +{ + if (Session.Type != GAME_NORMAL && Session.Type != GAME_SKIRMISH) { + static unsigned NextNetworkRefreshTime = UINT_MAX; + + if (timeGetTime() >= NextNetworkRefreshTime) { + Session.Loading_Callback(100); + Call_Back(); + } + + NextNetworkRefreshTime = timeGetTime() + 1000; + } + + // Stolen instructions + _asm + { + push ebx + push ebx + push ebx + lea edx, [esp + 0x28] + } + + JMP(0x0066BA5D); +} + + +/** + * Prevents AI Takeover if autosurrender is turned on. + * + * @author: ZivDero + */ +DECLARE_PATCH(_Destroy_Connection_AutoSurrender_Patch) +{ + GET_REGISTER_STATIC(HouseClass*, hptr, ebp); + + if ((Session.Type == GAME_INTERNET && PlanetWestwoodTournament) || (Vinifera_SpawnerActive && Vinifera_SpawnerConfig->AutoSurrender)) + { + hptr->Flag_To_Die(); + } + else + { + hptr->AI_Takeover(); + } + + JMP(0x0057526B); +} + + +/** + * Main function for patching the hooks. + */ +void Spawner_Hooks() +{ + Patch_Call(0x004629D1, &Spawner::Start_Game); // Main_Game + Patch_Call(0x00462B8B, &Spawner::Start_Game); // Main_Game + + /** + * The spawner allows player to jump right into a game, so no need to + * show the startup movies. + */ + Vinifera_SkipLogoMovies = true; + Vinifera_SkipStartupMovies = true; + + Patch_Dword(0x005DB794 + 1, Vinifera_SpawnerConfig->ConnTimeout); // Set ConnTimeout + + /** + * Remove calls to SessionClass::Read_Scenario_Descriptions() when the + * spawner is active. This will speed up the initialisation and loading + * process, as PKT and MPR files are not required when using the spawner. + */ + Patch_Call(0x004E8910, &SessionClassExt::_Read_Scenario_Descriptions); // New_Main_Menu + Patch_Call(0x00564BAE, &SessionClassExt::_Read_Scenario_Descriptions); // Select_MPlayer_Game + Patch_Call(0x0057FE2A, &SessionClassExt::_Read_Scenario_Descriptions); // NewMenuClass::Process_Game_Select + Patch_Call(0x0058037C, &SessionClassExt::_Read_Scenario_Descriptions); // NewMenuClass:: + Patch_Call(0x005ED477, &SessionClassExt::_Read_Scenario_Descriptions); // SessionClass::One_Time + + Patch_Jump(0x004C06EF, &_HouseClass_Expert_AI_Check_Allies); + + /** + * PlayMoviesInMultiplayer feature. + */ + Patch_Jump(0x0066BB57, &_Play_VQA_Forbid_Skipping_In_MP_Patch); + Patch_Jump(0x0066BA56, &_Play_VQA_Network_Callback_Patch); + + /** + * AutoSurrender feature. + */ + Patch_Jump(0x0057524A, &_Destroy_Connection_AutoSurrender_Patch); + + /** + * Hooks for various sub-modules. + */ + ProtocolZero_Hooks(); + Observer_Hooks(); + QuickMatch_Hooks(); + Statistics_Hooks(); +} diff --git a/src/spawner/spawner_hooks.h b/src/spawner/spawner_hooks.h new file mode 100644 index 000000000..a6db6004e --- /dev/null +++ b/src/spawner/spawner_hooks.h @@ -0,0 +1,31 @@ +/******************************************************************************* +/* O P E N S O U R C E -- V I N I F E R A ** +/******************************************************************************* + * + * @project Vinifera + * + * @file SPAWNER_HOOKS.CPP + * + * @author ZivDero + * + * @brief Contains the hooks for the multiplayer spawner class. + * + * @license Vinifera 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. + * + * Vinifera 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 . + * + ******************************************************************************/ +#pragma once + + +void Spawner_Hooks(); diff --git a/src/spawner/spawnerconfig.cpp b/src/spawner/spawnerconfig.cpp new file mode 100644 index 000000000..5cb7d8706 --- /dev/null +++ b/src/spawner/spawnerconfig.cpp @@ -0,0 +1,256 @@ +/******************************************************************************* +/* O P E N S O U R C E -- V I N I F E R A ** +/******************************************************************************* + * + * @project Vinifera + * + * @file SPAWNERCONFIG.CPP + * + * @author Belonit, ZivDero + * + * @brief Configuration of the multiplayer spawner. + * + * @license Vinifera 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. + * + * Vinifera 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 . + * + ******************************************************************************/ + +#include "spawnerconfig.h" + +#include "ccini.h" + + +/** + * Reads spawner config from the INI. + * + * @author: Belonit, ZivDero + */ +void SpawnerConfig::Read_INI(CCINIClass& spawn_ini) +{ + static char const* const SETTINGS = "Settings"; + static char const* const TUNNEL = "Tunnel"; + + /** + * Game Mode Options + */ + Bases = spawn_ini.Get_Bool(SETTINGS, "Bases", Bases); + Credits = spawn_ini.Get_Int(SETTINGS, "Credits", Credits); + BridgeDestroy = spawn_ini.Get_Bool(SETTINGS, "BridgeDestroy", BridgeDestroy); + Crates = spawn_ini.Get_Bool(SETTINGS, "Crates", Crates); + ShortGame = spawn_ini.Get_Bool(SETTINGS, "ShortGame", ShortGame); + BuildOffAlly = spawn_ini.Get_Bool(SETTINGS, "BuildOffAlly", BuildOffAlly); + GameSpeed = spawn_ini.Get_Int(SETTINGS, "GameSpeed", GameSpeed); + MultiEngineer = spawn_ini.Get_Bool(SETTINGS, "MultiEngineer", MultiEngineer); + UnitCount = spawn_ini.Get_Int(SETTINGS, "UnitCount", UnitCount); + AIPlayers = spawn_ini.Get_Int(SETTINGS, "AIPlayers", AIPlayers); + AIDifficulty = spawn_ini.Get_Int(SETTINGS, "AIDifficulty", AIDifficulty); + AlliesAllowed = spawn_ini.Get_Bool(SETTINGS, "AlliesAllowed", AlliesAllowed); + HarvesterTruce = spawn_ini.Get_Bool(SETTINGS, "HarvesterTruce", HarvesterTruce); + FogOfWar = spawn_ini.Get_Bool(SETTINGS, "FogOfWar", FogOfWar); + MCVRedeploy = spawn_ini.Get_Bool(SETTINGS, "MCVRedeploy", MCVRedeploy); + + /** + * Savegame Options + */ + LoadSaveGame = spawn_ini.Get_Bool(SETTINGS, "LoadSaveGame", LoadSaveGame); + /* SaveGameName */ spawn_ini.Get_String(SETTINGS, "SaveGameName", SaveGameName, SaveGameName, sizeof(SaveGameName)); + AutoSaveInterval = spawn_ini.Get_Int(SETTINGS, "AutoSaveGame", AutoSaveInterval); + NextAutoSaveNumber = spawn_ini.Get_Int(SETTINGS, "NextSPAutoSaveId", NextAutoSaveNumber + 1) - 1; // Subtract 1 since our autosaves are 0-based internally + + /** + * Scenario Options + */ + Seed = spawn_ini.Get_Int(SETTINGS, "Seed", Seed); + TechLevel = spawn_ini.Get_Int(SETTINGS, "TechLevel", TechLevel); + IsCampaign = spawn_ini.Get_Bool(SETTINGS, "IsSinglePlayer", IsCampaign); + CampaignID = spawn_ini.Get_Int(SETTINGS, "CampaignID", CampaignID); + CampaignDifficulty = spawn_ini.Get_Int(SETTINGS, "DifficultyModeHuman", CampaignDifficulty); + CampaignCDifficulty = spawn_ini.Get_Int(SETTINGS, "DifficultyModeComputer", CampaignCDifficulty); + Tournament = spawn_ini.Get_Int(SETTINGS, "Tournament", Tournament); + WOLGameID = spawn_ini.Get_Int(SETTINGS, "GameID", WOLGameID); + /* ScenarioName */ spawn_ini.Get_String(SETTINGS, "Scenario", ScenarioName, ScenarioName, sizeof(ScenarioName)); + /* MapHash */ spawn_ini.Get_String(SETTINGS, "MapHash", MapHash, MapHash, sizeof(MapHash)); + /* UIMapName */ spawn_ini.Get_String(SETTINGS, "UIMapName", UIMapName, UIMapName, sizeof(UIMapName)); + PlayMoviesInMultiplayer = spawn_ini.Get_Bool(SETTINGS, "PlayMoviesInMultiplayer", PlayMoviesInMultiplayer); + + /** + * Network Options + */ + Protocol = spawn_ini.Get_Int(SETTINGS, "Protocol", Protocol); + FrameSendRate = spawn_ini.Get_Int(SETTINGS, "FrameSendRate", FrameSendRate); + ReconnectTimeout = spawn_ini.Get_Int(SETTINGS, "ReconnectTimeout", ReconnectTimeout); + ConnTimeout = spawn_ini.Get_Int(SETTINGS, "ConnTimeout", ConnTimeout); + MaxAhead = spawn_ini.Get_Int(SETTINGS, "MaxAhead", MaxAhead); + PreCalcMaxAhead = spawn_ini.Get_Int(SETTINGS, "PreCalcMaxAhead", PreCalcMaxAhead); + MaxLatencyLevel = spawn_ini.Get_Int(SETTINGS, "MaxLatencyLevel", MaxLatencyLevel); + + /** + * Tunnel Options + */ + TunnelId = spawn_ini.Get_Int(SETTINGS, "Port", TunnelId); + ListenPort = spawn_ini.Get_Int(SETTINGS, "Port", ListenPort); + /* TunnelIp */ spawn_ini.Get_String(TUNNEL, "Ip", TunnelIp, TunnelIp, sizeof(TunnelIp)); + TunnelPort = spawn_ini.Get_Int(TUNNEL, "Port", TunnelPort); + + /** + * Player and House Options + */ + for (int i = 0; i < std::size(Players); ++i) + { + Players[i].Read_INI(spawn_ini, i); + if (Players[i].IsHuman) + HumanPlayers++; + + Houses[i].Read_INI(spawn_ini, i); + } + + /** + * Extended Options + */ + Firestorm = spawn_ini.Get_Bool(SETTINGS, "Firestorm", Firestorm); + QuickMatch = spawn_ini.Get_Bool(SETTINGS, "QuickMatch", QuickMatch); + SkipScoreScreen = spawn_ini.Get_Bool(SETTINGS, "SkipScoreScreen", SkipScoreScreen); + WriteStatistics = spawn_ini.Get_Bool(SETTINGS, "WriteStatistics", WriteStatistics); + AINamesByDifficulty = spawn_ini.Get_Bool(SETTINGS, "AINamesByDifficulty", AINamesByDifficulty); + CoachMode = spawn_ini.Get_Bool(SETTINGS, "CoachMode", CoachMode); + AutoSurrender = spawn_ini.Get_Bool(SETTINGS, "AutoSurrender", AutoSurrender); + AttackNeutralUnits = spawn_ini.Get_Bool(SETTINGS, "AttackNeutralUnits", AttackNeutralUnits); + ScrapMetal = spawn_ini.Get_Bool(SETTINGS, "ScrapMetal", ScrapMetal); + /* CustomLoadScreen */ spawn_ini.Get_String(SETTINGS, "CustomLoadScreen", CustomLoadScreen, sizeof(CustomLoadScreen)); + CustomLoadScreenPos = spawn_ini.Get_Point(SETTINGS, "CustomLoadScreenPos", CustomLoadScreenPos); + ContinueWithoutHumans = spawn_ini.Get_Bool(SETTINGS, "ContinueWithoutHumans", ContinueWithoutHumans); + /* DifficultyName */ spawn_ini.Get_String(SETTINGS, "DifficultyName", DifficultyName, sizeof(DifficultyName)); +} + + +static constexpr char* PlayerSectionArray[8] = { + "Settings", + "Other1", + "Other2", + "Other3", + "Other4", + "Other5", + "Other6", + "Other7" +}; + + +static constexpr char* MultiTagArray[8] = { + "Multi1", + "Multi2", + "Multi3", + "Multi4", + "Multi5", + "Multi6", + "Multi7", + "Multi8" +}; + + +static constexpr char* AlliancesSectionArray[8] = { + "Multi1_Alliances", + "Multi2_Alliances", + "Multi3_Alliances", + "Multi4_Alliances", + "Multi5_Alliances", + "Multi6_Alliances", + "Multi7_Alliances", + "Multi8_Alliances" +}; + + +static constexpr char* AlliancesTagArray[8] = { + "HouseAllyOne", + "HouseAllyTwo", + "HouseAllyThree", + "HouseAllyFour", + "HouseAllyFive", + "HouseAllySix", + "HouseAllySeven", + "HouseAllyEight" +}; + + +/** + * Reads player's config from the INI. + * + * @author: Belonit, ZivDero + */ +void SpawnerConfig::PlayerConfig::Read_INI(CCINIClass& spawn_ini, int index) +{ + if (index >= MAX_PLAYERS) + return; + + const char* SECTION = PlayerSectionArray[index]; + const char* MULTI_TAG = MultiTagArray[index]; + + if (spawn_ini.Is_Present(SECTION)) + { + IsHuman = true; + Difficulty = -1; + + spawn_ini.Get_String(SECTION, "Name", Name, Name, sizeof(Name)); + + Color = spawn_ini.Get_Int(SECTION, "Color", Color); + House = spawn_ini.Get_Int(SECTION, "Side", House); + + spawn_ini.Get_String(SECTION, "Ip", Ip, Ip, sizeof(Ip)); + Port = spawn_ini.Get_Int(SECTION, "Port", Port); + } + else if (!IsHuman) + { + Color = spawn_ini.Get_Int("HouseColors", MULTI_TAG, Color); + House = spawn_ini.Get_Int("HouseCountries", MULTI_TAG, House); + Difficulty = spawn_ini.Get_Int("HouseHandicaps", MULTI_TAG, Difficulty); + } +} + + +/** + * Reads house's config from the INI. + * + * @author: Belonit, ZivDero + */ +void SpawnerConfig::HouseConfig::Read_INI(CCINIClass& spawn_ini, int index) +{ + if (index >= MAX_PLAYERS) + return; + + const char* ALLIANCES = AlliancesSectionArray[index]; + const char* MULTI_TAG = MultiTagArray[index]; + + IsObserver = spawn_ini.Get_Bool("IsSpectator", MULTI_TAG, IsObserver); + SpawnLocation = spawn_ini.Get_Int("SpawnLocations", MULTI_TAG, SpawnLocation); + + /** + * The client might pass these to indicate that this is an observer. + */ + if (SpawnLocation == -1 || SpawnLocation == 90) + { + IsObserver = true; + SpawnLocation = -1; + } + + /** + * Reset any weird values we might receive as input. + */ + if (SpawnLocation < 0 || SpawnLocation > MAX_PLAYERS - 1) + SpawnLocation = -1; + + if (spawn_ini.Is_Present(ALLIANCES)) + { + for(int i = 0; i < 8; i++) + Alliances[i] = spawn_ini.Get_Int(ALLIANCES, AlliancesTagArray[i], Alliances[i]); + } +} diff --git a/src/spawner/spawnerconfig.h b/src/spawner/spawnerconfig.h new file mode 100644 index 000000000..c7b65a6eb --- /dev/null +++ b/src/spawner/spawnerconfig.h @@ -0,0 +1,266 @@ +/******************************************************************************* +/* O P E N S O U R C E -- V I N I F E R A ** +/******************************************************************************* + * + * @project Vinifera + * + * @file SPAWNERCONFIG.H + * + * @author Belonit, ZivDero + * + * @brief Configuration of the multiplayer spawner. + * + * @license Vinifera 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. + * + * Vinifera 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 . + * + ******************************************************************************/ +#pragma once + +#include "abstractext.h" + +class CCINIClass; + + +/** + * This class contains all the configuration for the spawner, usually read from SPAWN.INI. + */ +class SpawnerConfig +{ + /** + * Used to create NodeNameType + * The order of entries may differ from HouseConfig + */ + struct PlayerConfig + { + bool IsHuman; + char Name[20]; + int Color; + int House; + int Difficulty; + char Ip[0x20]; + int Port; + + PlayerConfig() + : IsHuman { false } + , Name { "" } + , Color { -1 } + , House { -1 } + , Difficulty { -1 } + , Ip { "0.0.0.0" } + , Port { -1 } + { } + + void Read_INI(CCINIClass& spawn_ini, int index); + }; + + /** + * Used to configure the generated HouseClass + * Must be sorted by respective player color + */ + struct HouseConfig + { + bool IsObserver; + int SpawnLocation; + int Alliances[8]; + + HouseConfig() + : IsObserver { false } + , SpawnLocation { -2 } + , Alliances { -1, -1, -1, -1, -1, -1, -1, -1 } + { } + + void Read_INI(CCINIClass& spawn_ini, int index); + }; + +public: + /** + * Game Mode Options + */ + bool Bases; + int Credits; + bool BridgeDestroy; + bool Crates; + bool ShortGame; + bool BuildOffAlly; + int GameSpeed; + bool MultiEngineer; + int UnitCount; + int AIPlayers; + int AIDifficulty; + bool AlliesAllowed; + bool HarvesterTruce; + bool FogOfWar; + bool MCVRedeploy; + + /** + * Savegame Options + */ + bool LoadSaveGame; + char SaveGameName[60]; + int AutoSaveInterval; + int NextAutoSaveNumber; + + /** + * Scenario Options + */ + int Seed; + int TechLevel; + bool IsCampaign; + int CampaignID; + int CampaignDifficulty; + int CampaignCDifficulty; + int Tournament; + unsigned int WOLGameID; + char ScenarioName[260]; + char MapHash[0xff]; + char UIMapName[44]; + bool PlayMoviesInMultiplayer; + + /** + * Network Options + */ + int Protocol; + int FrameSendRate; + int ReconnectTimeout; + int ConnTimeout; + int MaxAhead; + int PreCalcMaxAhead; + unsigned char MaxLatencyLevel; + + /** + * Tunnel Options + */ + int TunnelId; + char TunnelIp[0x20]; + int TunnelPort; + int ListenPort; + + /** + * Player Options + */ + PlayerConfig Players[8]; + int HumanPlayers; + + /** + * House Options + */ + HouseConfig Houses[8]; + + /** + * Extended Options + */ + bool Firestorm; + bool QuickMatch; + bool SkipScoreScreen; + bool WriteStatistics; + bool AINamesByDifficulty; + bool CoachMode; + bool AutoSurrender; + bool AttackNeutralUnits; + bool ScrapMetal; + char CustomLoadScreen[PATH_MAX]; + TPoint2D CustomLoadScreenPos; + bool ContinueWithoutHumans; + char DifficultyName[32]; + + SpawnerConfig() + : Bases { true } + , Credits { 10000 } + , BridgeDestroy { true } + , Crates { false } + , ShortGame { false } + , BuildOffAlly { false } + , GameSpeed { 0 } + , MultiEngineer { false } + , UnitCount { 0 } + , AIPlayers { 0 } + , AIDifficulty { 1 } + , AlliesAllowed { false } + , HarvesterTruce { false } + , FogOfWar { false } + , MCVRedeploy { true } + + , LoadSaveGame { false } + , SaveGameName { "" } + , AutoSaveInterval { 1 } + , NextAutoSaveNumber { 0 } + + , Seed { 0 } + , TechLevel { 10 } + , IsCampaign { false } + , CampaignID { -1 } + , CampaignDifficulty { 1 } + , CampaignCDifficulty { 1 } + , Tournament { 0 } + , WOLGameID { 0xDEADBEEF } + , ScenarioName { "spawnmap.ini" } + , MapHash { "" } + , UIMapName { "" } + , PlayMoviesInMultiplayer { false } + + , Protocol { 2 } + , FrameSendRate { 4 } + , ReconnectTimeout { 2400 } + , ConnTimeout { 3600 } + , MaxAhead { -1 } + , PreCalcMaxAhead { 0 } + , MaxLatencyLevel { 0xFF } + + , TunnelId { 0 } + , TunnelIp { "0.0.0.0" } + , TunnelPort { 0 } + , ListenPort { 1234 } + + , Players { + PlayerConfig(), + PlayerConfig(), + PlayerConfig(), + PlayerConfig(), + + PlayerConfig(), + PlayerConfig(), + PlayerConfig(), + PlayerConfig() + } + , HumanPlayers(0) + + , Houses { + HouseConfig(), + HouseConfig(), + HouseConfig(), + HouseConfig(), + + HouseConfig(), + HouseConfig(), + HouseConfig(), + HouseConfig() + } + + , Firestorm { true } + , QuickMatch { false } + , SkipScoreScreen { false } + , WriteStatistics { false } + , AINamesByDifficulty { false } + , CoachMode { false } + , AutoSurrender { true } + , AttackNeutralUnits{ false } + , ScrapMetal { false } + , CustomLoadScreen { "" } + , CustomLoadScreenPos { } + , ContinueWithoutHumans { false } + , DifficultyName { "" } + { } + + void Read_INI(CCINIClass& spawn_ini); +}; diff --git a/src/vinifera/vinifera_defines.h b/src/vinifera/vinifera_defines.h index 3c2a335a9..1505089d7 100644 --- a/src/vinifera/vinifera_defines.h +++ b/src/vinifera/vinifera_defines.h @@ -151,3 +151,12 @@ typedef enum ViniferaRTTIType VINIFERA_RTTI_COUNT }; DEFINE_ENUMERATION_OPERATORS(ViniferaRTTIType); + + +typedef enum ViniferaDiffType : int +{ + DIFF_VERY_EASY = DIFF_COUNT, + DIFF_EXTREMELY_EASY, + + VINIFERA_DIFF_COUNT +}; diff --git a/src/vinifera/vinifera_functions.cpp b/src/vinifera/vinifera_functions.cpp index eeef2a41a..7dee971c1 100644 --- a/src/vinifera/vinifera_functions.cpp +++ b/src/vinifera/vinifera_functions.cpp @@ -29,9 +29,6 @@ #include "vinifera_globals.h" #include "vinifera_newdel.h" #include "tibsun_globals.h" -#include "cncnet4.h" -#include "cncnet4_globals.h" -#include "cncnet5_globals.h" #include "rulesext.h" #include "ccfile.h" #include "ccini.h" @@ -60,6 +57,9 @@ #include "rocketlocomotion.h" #include "setup_hooks.h" +#include "spawner.h" +#include "spawner_hooks.h" +#include "spawnerconfig.h" static DynamicVectorClass ViniferaSearchPaths; @@ -136,12 +136,26 @@ bool Vinifera_Load_INI() #endif } - Vinifera_NewSidebar = ini.Get_Bool("Features", "NewSidebar", false); ini.Get_String("General", "SavedGamesDirectory", buffer, std::size(buffer)); if (std::strlen(buffer) > 0) { std::strncpy(Vinifera_SavedGamesDirectory, buffer, std::size(Vinifera_SavedGamesDirectory) - 1); } + Vinifera_NewSidebar = ini.Get_Bool("Features", "NewSidebar", Vinifera_NewSidebar); + Vinifera_HumanNormalDifficulty = ini.Get_Bool("Features", "HumanNormalDifficulty", Vinifera_HumanNormalDifficulty); + + ini.Get_String("Language", "DifficultyEasy", "Easy", Vinifera_DifficultyNames[DIFF_EASY]); + ini.Get_String("Language", "DifficultyNormal", "Normal", Vinifera_DifficultyNames[DIFF_NORMAL]); + ini.Get_String("Language", "DifficultyHard", "Hard", Vinifera_DifficultyNames[DIFF_HARD]); + ini.Get_String("Language", "DifficultyVeryEasy", "Very Easy", Vinifera_DifficultyNames[DIFF_VERY_EASY]); + ini.Get_String("Language", "DifficultyExtremelyEasy", "Extremely Easy", Vinifera_DifficultyNames[DIFF_EXTREMELY_EASY]); + + ini.Get_String("Language", "AIDifficultyEasy", "Hard", Vinifera_AIDifficultyNames[DIFF_EASY]); + ini.Get_String("Language", "AIDifficultyNormal", "Normal", Vinifera_AIDifficultyNames[DIFF_NORMAL]); + ini.Get_String("Language", "AIDifficultyHard", "Easy", Vinifera_AIDifficultyNames[DIFF_HARD]); + ini.Get_String("Language", "AIDifficultyVeryEasy", "Brutal", Vinifera_AIDifficultyNames[DIFF_VERY_EASY]); + ini.Get_String("Language", "AIDifficultyExtremelyEasy", "Ultimate", Vinifera_AIDifficultyNames[DIFF_EXTREMELY_EASY]); + return true; } @@ -297,6 +311,16 @@ bool Vinifera_Parse_Command_Line(int argc, char *argv[]) continue; } + /** + * Start in spawner mode. + */ + if (stricmp(string, "-SPAWN") == 0) { + DEBUG_INFO(" - Spawner enabled.\n"); + Spawner::Init(); + Spawner_Hooks(); + continue; + } + /** * Skip the startup videos. */ @@ -501,39 +525,11 @@ bool Vinifera_Startup() ViniferaSearchPaths.Add("TS3"); #endif - /** - * #issue-514: - * - * Adds various search paths for loading files locally for the TS-Client builds only. - * - * #NOTE: REMOVED: Additional paths must now be set via SearchPaths in VINIFERA.INI! - * - * @author: CCHyper - */ -#if 0 // #if defined(TS_CLIENT) - - // Only required for the TS Client builds as most projects will - // put VINIFERA.INI in this directory. - ViniferaSearchPaths.Add("INI"); - - // Required for startup mix files to be found. - ViniferaSearchPaths.Add("MIX"); -#endif - #if !defined(TS_CLIENT) // Required for startup movies to be found. ViniferaSearchPaths.Add("MOVIES"); #endif - // REMOVED: Paths are now set via SearchPaths in VINIFERA.INI -//#if defined(TS_CLIENT) -// ViniferaSearchPaths.Add("MUSIC"); -// ViniferaSearchPaths.Add("SOUNDS"); -// ViniferaSearchPaths.Add("MAPS"); -// ViniferaSearchPaths.Add("MAPS\\MULTIPLAYER"); -// ViniferaSearchPaths.Add("MAPS\\MISSION"); -//#endif - /** * Load Vinifera settings and overrides. */ @@ -614,32 +610,8 @@ bool Vinifera_Startup() return false; } -#if !defined(TS_CLIENT) - /** - * Initialise the CnCNet4 system. - */ - if (!CnCNet4::Init()) { - CnCNet4::IsEnabled = false; - DEBUG_WARNING("Failed to initialise CnCNet4, continuing without CnCNet4 support!\n"); - } - - /** - * Disable CnCNet4 if CnCNet5 is active, they can not co-exist. - */ - if (CnCNet4::IsEnabled && CnCNet5::IsActive) { - CnCNet4::Shutdown(); - CnCNet4::IsEnabled = false; - } -#else - /** - * Client builds can only use CnCNet5. - */ - CnCNet4::IsEnabled = false; - //CnCNet5::IsActive = true; // Enable when new Client system is implemented. -#endif - KamikazeTracker = new KamikazeTrackerClass; - + return true; } @@ -686,6 +658,9 @@ bool Vinifera_Shutdown() delete KamikazeTracker; KamikazeTracker = nullptr; + delete Vinifera_SpawnerConfig; + Vinifera_SpawnerConfig = nullptr; + DEV_DEBUG_INFO("Shutdown - New Count: %d, Delete Count: %d\n", Vinifera_New_Count, Vinifera_Delete_Count); return true; @@ -721,13 +696,6 @@ int Vinifera_Pre_Init_Game(int argc, char *argv[]) DEV_DEBUG_WARNING("UI.INI not found!\n"); } -#if defined(TS_CLIENT) - /** - * The TS Client allows player to jump right into a game, so no need to - * show the startup movies for these builds. - */ - Vinifera_SkipStartupMovies = true; -#endif /** * Read the mouse controls and overrides. diff --git a/src/vinifera/vinifera_globals.cpp b/src/vinifera/vinifera_globals.cpp index 6a43c18e6..d541c2173 100644 --- a/src/vinifera/vinifera_globals.cpp +++ b/src/vinifera/vinifera_globals.cpp @@ -67,10 +67,21 @@ bool Vinifera_NoTacticalVersionString = false; bool Vinifera_ShowSuperWeaponTimers = true; -/** - * The total play time from all previous sessions of the current game. - */ +SpawnerConfig* Vinifera_SpawnerConfig = nullptr; +bool Vinifera_SpawnerActive = false; + +HouseClass* Vinifera_ObserverPtr = nullptr; + +bool Vinifera_DoSave = false; +int Vinifera_NextAutoSaveFrame = -1; +int Vinifera_NextAutoSaveNumber = 0; + unsigned Vinifera_TotalPlayTime = 0; +unsigned Vinifera_PlaythroughID = 0; + +bool Vinifera_HumanNormalDifficulty = false; +Wstring Vinifera_DifficultyNames[5]; +Wstring Vinifera_AIDifficultyNames[5]; DynamicVectorClass ViniferaMapsMixes; DynamicVectorClass ViniferaMoviesMixes; diff --git a/src/vinifera/vinifera_globals.h b/src/vinifera/vinifera_globals.h index 29c888261..d4e17b72a 100644 --- a/src/vinifera/vinifera_globals.h +++ b/src/vinifera/vinifera_globals.h @@ -32,6 +32,7 @@ #include "ccfile.h" +class HouseClass; class KamikazeTrackerClass; class SpawnManagerClass; class EBoltClass; @@ -40,6 +41,7 @@ class ArmorTypeClass; class RocketTypeClass; class MouseTypeClass; class ActionTypeClass; +class SpawnerConfig; extern bool Vinifera_DeveloperMode; @@ -93,7 +95,21 @@ extern bool Vinifera_NoTacticalVersionString; extern bool Vinifera_ShowSuperWeaponTimers; +extern SpawnerConfig* Vinifera_SpawnerConfig; +extern bool Vinifera_SpawnerActive; + +extern HouseClass* Vinifera_ObserverPtr; + +extern bool Vinifera_DoSave; +extern int Vinifera_NextAutoSaveFrame; +extern int Vinifera_NextAutoSaveNumber; + extern unsigned Vinifera_TotalPlayTime; +extern unsigned Vinifera_PlaythroughID; + +extern bool Vinifera_HumanNormalDifficulty; +extern Wstring Vinifera_DifficultyNames[5]; +extern Wstring Vinifera_AIDifficultyNames[5]; extern DynamicVectorClass ViniferaMapsMixes; extern DynamicVectorClass ViniferaMoviesMixes; diff --git a/src/vinifera/vinifera_hooks.cpp b/src/vinifera/vinifera_hooks.cpp index 2d5360a95..104336e63 100644 --- a/src/vinifera/vinifera_hooks.cpp +++ b/src/vinifera/vinifera_hooks.cpp @@ -53,6 +53,7 @@ #include "spawnmanager.h" #include "armortype.h" #include "rockettype.h" +#include "spawner.h" /** @@ -420,6 +421,9 @@ DECLARE_PATCH(_Select_Game_Clear_Globals_Patch) */ Vinifera_ShowSuperWeaponTimers = true; Vinifera_TotalPlayTime = 0; + Vinifera_DoSave = false; + Vinifera_NextAutoSaveFrame = -1; + Vinifera_NextAutoSaveNumber = 0; /** * Stolen bytes/code. @@ -708,20 +712,6 @@ void Vinifera_Hooks() Patch_Byte(0x0070EEAB, num); Patch_Byte(0x0070EF0F, num); -#if defined(TS_CLIENT) - /** - * Remove calls to SessionClass::Read_Scenario_Descriptions() in TS Client - * compatable builds. This will speed up the initialisation and loading - * process, as the reason of PKT and MPR files are not required when using - * the Client. - */ - Patch_Byte_Range(0x004E8901, 0x90, 5); // NewMenu::Process - Patch_Byte_Range(0x004E8910, 0x90, 5); // ^ - Patch_Byte_Range(0x00564BA9, 0x90, 10); // Select_MPlayer_Game - Patch_Byte_Range(0x0057FE2A, 0x90, 10); // NewMenuClass::Process_Game_Select - Patch_Byte_Range(0x00580377, 0x90, 10); // NewMenuClass::Process_Game_Select -#endif - /** * Various patches to intercept the games object tracking and heap processing. */ diff --git a/src/vinifera/vinifera_saveload.cpp b/src/vinifera/vinifera_saveload.cpp index 942ba563f..a98b5521f 100644 --- a/src/vinifera/vinifera_saveload.cpp +++ b/src/vinifera/vinifera_saveload.cpp @@ -134,6 +134,12 @@ #include "savever.h" #include "vinifera_savever.h" #include "windialog.h" +#include "fetchres.h" +#include "optionsext.h" +#include "spawner.h" +#include "technoext.h" +#include "verses.h" +#include "scenarioext.h" /** @@ -289,6 +295,8 @@ bool Vinifera_Put_All(IStream *pStm, bool save_net) DEBUG_INFO("Saving Misc. Values...\n"); if (FAILED(Save_Misc_Values(pStm))) { return false; } + pStm->Write(&Vinifera_ObserverPtr, sizeof(Vinifera_ObserverPtr), nullptr); + /** * Save the Logic & Map layers. */ @@ -296,9 +304,7 @@ bool Vinifera_Put_All(IStream *pStm, bool save_net) if (FAILED(Logic.Save(pStm))) { return false; } DEBUG_INFO("Saving TacticalMap...\n"); - { - if (FAILED(OleSaveToStream(TacticalMap, pStm))) { return false; } - } + if (FAILED(OleSaveToStream(TacticalMap, pStm))) { return false; } /** * Save all game objects. This code saves every object that's stored in a DynamicVector class. @@ -459,17 +465,6 @@ bool Vinifera_Get_All(IStream *pStm, bool load_net) Enable_Addon(Scen->RequiredAddOn); - SideType side = Scen->IsGDI ? SIDE_GDI : SIDE_NOD; -#if defined(TS_CLIENT) - side = static_cast(Scen->IsGDI); -#endif - - DEBUG_INFO("About to call Prep_For_Side()...\n"); - if (!Prep_For_Side(side)) { - DEBUG_ERROR("Prep_For_Side() failed!\n"); - return false; - } - { Rect tactical_rect = Get_Tactical_Rect(); Rect composite_rect(0, 0, tactical_rect.Width, ScreenRect.Height); @@ -498,12 +493,6 @@ bool Vinifera_Get_All(IStream *pStm, bool load_net) DEBUG_INFO("Loading Rule...\n"); Rule->Load(pStm); - DEBUG_INFO("About to call Prep_Speech_For_Side()...\n"); - if (!Prep_Speech_For_Side(side)) { - DEBUG_ERROR("Prep_Speech_For_Side() failed!\n"); - return false; - } - if (FAILED(Vinifera_Load_Vector(pStm, AnimTypes, "AnimTypes"))) { return false; } /** @@ -520,6 +509,9 @@ bool Vinifera_Get_All(IStream *pStm, bool load_net) DEBUG_INFO("Loading Misc. Values...\n"); if (FAILED(Load_Misc_Values(pStm))) { return false; } + pStm->Read(&Vinifera_ObserverPtr, sizeof(Vinifera_ObserverPtr), nullptr); + VINIFERA_SWIZZLE_REQUEST_POINTER_REMAP(Vinifera_ObserverPtr, "Vinifera_ObserverPtr"); + DEBUG_INFO("About to call Map.Clear_SubZones()...\n"); Map.Clear_SubZones(); @@ -626,6 +618,51 @@ bool Vinifera_Get_All(IStream *pStm, bool load_net) return false; } + /** + * #issue-218 + * + * We now abuse ScenarioClass::IsGDI to store the player house so it can be used to + * fetch the SideType from it for loading the assets. This also means this + * bugfix works without extending any of the games classes, but this does mean we + * are limited to 255 unique houses! + */ + const HousesType house = static_cast(Scen->IsGDI); + ASSERT_FATAL(house != HOUSE_NONE & house < HouseTypes.Count()); + const HouseTypeClass* housetype = HouseTypes[house]; + ASSERT_FATAL(housetype != nullptr); + + /** + * Fetch the houses side type and use this to decide which assets to load. + */ + DEBUG_INFO("About to call Prep_For_Side()...\n"); + if (!Prep_For_Side(ScenExtension->SidebarSide)) { + DEBUG_WARNING("Prep_For_Side(%d) failed! Trying with side 0...\n", housetype->Side); + + /** + * Try once again but with the Side 0 (GDI) assets. + */ + if (!Prep_For_Side(SIDE_GDI)) { + DEBUG_ERROR("Prep_For_Side() failed!\n"); + return false; + } + } + + /** + * Fetch the houses side type and use this to decide which speech assets to load. + */ + DEBUG_INFO("About to call Prep_Speech_For_Side()...\n"); + if (!Prep_Speech_For_Side(housetype->Side)) { + DEBUG_WARNING("Prep_Speech_For_Side(%d) failed! Trying with side 0...\n", housetype->Side); + + /** + * Try once again but with the Side 0 (GDI) assets. + */ + if (!Prep_Speech_For_Side(SIDE_GDI)) { + DEBUG_ERROR("Prep_Speech_For_Side() failed!\n"); + return false; + } + } + Map.Flag_To_Redraw(2); //Vinifera_Remap_Extension_Pointers(); @@ -745,7 +782,7 @@ bool Vinifera_Save_Game(const char* file_name, const char* descr, bool) versioninfo.Set_Vinifera_Version(ViniferaGameVersion); versioninfo.Set_Vinifera_Commit_Hash(Vinifera_Git_Hash()); - versioninfo.Set_Session_ID(Session.UniqueID); + versioninfo.Set_Playthrough_ID(Vinifera_PlaythroughID); versioninfo.Set_Difficulty(Scen->Difficulty); versioninfo.Set_Total_Play_Time(Vinifera_TotalPlayTime + Scen->ElapsedTimer.Value()); @@ -855,6 +892,7 @@ bool Vinifera_Load_Game(const char* file_name) storage.Release(); Session.Type = static_cast(saveversion.Get_Game_Type()); Vinifera_TotalPlayTime = saveversion.Get_Total_Play_Time(); + Vinifera_PlaythroughID = saveversion.Get_Playthrough_ID(); SwizzleManager.Reset(); DEBUG_INFO("Opening DocFile\n"); @@ -914,6 +952,12 @@ bool Vinifera_Load_Game(const char* file_name) TacticalViewActive = true; ScenarioStarted = true; + /** + * Schedule the next autosave. + */ + Vinifera_NextAutoSaveFrame = Frame; + Vinifera_NextAutoSaveFrame += Vinifera_SpawnerActive && Session.Type == GAME_IPX ? Vinifera_SpawnerConfig->AutoSaveInterval : OptionsExtension->AutoSaveInterval; + DEBUG_INFO("LOADING GAME [%s] - Complete\n", formatted_file_name); return true; @@ -956,6 +1000,11 @@ bool LoadOptionsClassExt::_Load_File(const char* filename) TacticalViewActive = false; ScenarioStarted = false; + /** + * If the user has manually loaded a save game, the spawner isn't responsible for anything anymore. + */ + Vinifera_SpawnerActive = false; + _makepath(formatted_file_name, nullptr, Vinifera_SavedGamesDirectory, Filename_From_Path(filename), nullptr); const bool result = Load_Game(formatted_file_name); @@ -1037,6 +1086,23 @@ bool LoadOptionsClassExt::_Read_File(FileEntryClass* file, WIN32_FIND_DATA* file return false; } + /** + * Don't allow loading saves from other campaign playthroughs, or campaign saves in general if we're not in campaign + * (to facilitate the client's playthrough tracking). + */ + if (GameActive) { + + if (Session.Type == GAME_NORMAL && saveversion.Get_Playthrough_ID() != Vinifera_PlaythroughID) { + DEBUG_INFO("Save file \"%s\" belongs to a different playthough, skipping.\n", formatted_file_name); + return false; + } + + if (Session.Type != GAME_NORMAL && saveversion.Get_Game_Type() == GAME_NORMAL) { + DEBUG_INFO("Save file \"%s\" is a campaign save and the player is currently not in campaign, skipping.\n", formatted_file_name); + return false; + } + } + wsprintfA(file->Descr, "%s", saveversion.Get_Scenario_Description()); file->Old = false; file->Valid = true; diff --git a/src/vinifera/vinifera_saveload.h b/src/vinifera/vinifera_saveload.h index c5d713da1..90804f7a7 100644 --- a/src/vinifera/vinifera_saveload.h +++ b/src/vinifera/vinifera_saveload.h @@ -118,7 +118,7 @@ HRESULT Save_Primitive_Vector(LPSTREAM& pStm, VectorClass& list, const char* for (int index = 0; index < count; ++index) { - HRESULT hr = pStm->Write(&list[index], sizeof(list[index]), nullptr); + hr = pStm->Write(&list[index], sizeof(list[index]), nullptr); if (FAILED(hr)) { return hr; } @@ -149,7 +149,7 @@ HRESULT Load_Primitive_Vector(LPSTREAM& pStm, VectorClass& list, const char* for (int index = 0; index < count; ++index) { T obj; - HRESULT hr = pStm->Read(&obj, sizeof(obj), nullptr); + hr = pStm->Read(&obj, sizeof(obj), nullptr); if (FAILED(hr)) { return hr; } @@ -178,7 +178,7 @@ HRESULT Save_Primitive_Vector(LPSTREAM& pStm, DynamicVectorClass& list, const for (int index = 0; index < count; ++index) { - HRESULT hr = pStm->Write(&list[index], sizeof(list[index]), nullptr); + hr = pStm->Write(&list[index], sizeof(list[index]), nullptr); if (FAILED(hr)) { return hr; } @@ -209,7 +209,7 @@ HRESULT Load_Primitive_Vector(LPSTREAM& pStm, DynamicVectorClass& list, const for (int index = 0; index < count; ++index) { T obj; - HRESULT hr = pStm->Read(&obj, sizeof(obj), nullptr); + hr = pStm->Read(&obj, sizeof(obj), nullptr); if (FAILED(hr)) { return hr; } @@ -238,7 +238,7 @@ HRESULT Save_Primitive_Vector(LPSTREAM& pStm, std::vector& list, const char* for (int index = 0; index < count; ++index) { - HRESULT hr = pStm->Write(&list[index], sizeof(list[index]), nullptr); + hr = pStm->Write(&list[index], sizeof(list[index]), nullptr); if (FAILED(hr)) { return hr; } @@ -269,7 +269,7 @@ HRESULT Load_Primitive_Vector(LPSTREAM& pStm, std::vector& list, const char* for (int index = 0; index < count; ++index) { T obj; - HRESULT hr = pStm->Read(&obj, sizeof(obj), nullptr); + hr = pStm->Read(&obj, sizeof(obj), nullptr); if (FAILED(hr)) { return hr; } diff --git a/src/vinifera/vinifera_savever.cpp b/src/vinifera/vinifera_savever.cpp index a90339b38..f6c779688 100644 --- a/src/vinifera/vinifera_savever.cpp +++ b/src/vinifera/vinifera_savever.cpp @@ -52,7 +52,7 @@ ViniferaSaveVersionInfo::ViniferaSaveVersionInfo() : GameType(0), ViniferaVersion(0), ViniferaCommitHash{ "" }, - SessionID(0) + PlaythroughID(0) { StartTime.dwLowDateTime = 0; StartTime.dwHighDateTime = 0; @@ -406,9 +406,9 @@ const char* ViniferaSaveVersionInfo::Get_Vinifera_Commit_Hash() const * * @author: ZivDero */ -void ViniferaSaveVersionInfo::Set_Session_ID(int num) +void ViniferaSaveVersionInfo::Set_Playthrough_ID(int num) { - SessionID = num; + PlaythroughID = num; } @@ -417,9 +417,9 @@ void ViniferaSaveVersionInfo::Set_Session_ID(int num) * * @author: ZivDero */ -int ViniferaSaveVersionInfo::Get_Session_ID() const +int ViniferaSaveVersionInfo::Get_Playthrough_ID() const { - return SessionID; + return PlaythroughID; } @@ -565,7 +565,7 @@ HRESULT ViniferaSaveVersionInfo::Save(IStorage *storage) return res; } - res = Save_Int_Set(storageset, ID_SESSION_ID, SessionID); + res = Save_Int_Set(storageset, ID_PLAYTHROUGH_ID, PlaythroughID); if (FAILED(res)) { return res; } @@ -666,7 +666,7 @@ HRESULT ViniferaSaveVersionInfo::Save(IStorage *storage) return res; } - res = Save_Int(storage, ID_SESSION_ID, SessionID); + res = Save_Int(storage, ID_PLAYTHROUGH_ID, PlaythroughID); if (FAILED(res)) { return res; } @@ -790,7 +790,7 @@ HRESULT ViniferaSaveVersionInfo::Load(IStorage *storage) strcpy(ViniferaCommitHash, buffer); - res = Load_Int_Set(storageset, ID_SESSION_ID, &SessionID); + res = Load_Int_Set(storageset, ID_PLAYTHROUGH_ID, &PlaythroughID); if (FAILED(res)) { return res; } @@ -896,7 +896,7 @@ HRESULT ViniferaSaveVersionInfo::Load(IStorage *storage) strcpy(ViniferaCommitHash, buffer); - res = Load_Int(storage, ID_SESSION_ID, &SessionID); + res = Load_Int(storage, ID_PLAYTHROUGH_ID, &PlaythroughID); if (FAILED(res)) { return res; } @@ -1331,7 +1331,7 @@ const WCHAR *Vinifera_Stream_Name_From_ID(int id) { ViniferaSaveVersionInfo::ID_VINIFERA_VERSION, L"Vinifera Version" }, { ViniferaSaveVersionInfo::ID_VINIFERA_COMMIT_HASH, L"Vinifera Commit Hash" }, - { ViniferaSaveVersionInfo::ID_SESSION_ID, L"Session ID" }, + { ViniferaSaveVersionInfo::ID_PLAYTHROUGH_ID, L"Playthrough ID" }, { ViniferaSaveVersionInfo::ID_DIFFICULTY, L"Difficulty" }, { ViniferaSaveVersionInfo::ID_TOTAL_PLAY_TIME, L"Total Play Time" }, }; diff --git a/src/vinifera/vinifera_savever.h b/src/vinifera/vinifera_savever.h index 523995c30..b54d05fba 100644 --- a/src/vinifera/vinifera_savever.h +++ b/src/vinifera/vinifera_savever.h @@ -51,7 +51,7 @@ class ViniferaSaveVersionInfo ID_VINIFERA_VERSION = 105, ID_VINIFERA_COMMIT_HASH = 106, - ID_SESSION_ID = 107, + ID_PLAYTHROUGH_ID = 107, ID_DIFFICULTY = 108, ID_TOTAL_PLAY_TIME = 109, }; @@ -104,8 +104,8 @@ class ViniferaSaveVersionInfo void Set_Vinifera_Commit_Hash(const char* hash); const char* Get_Vinifera_Commit_Hash() const; - void Set_Session_ID(int num); - int Get_Session_ID() const; + void Set_Playthrough_ID(int num); + int Get_Playthrough_ID() const; void Set_Difficulty(int num); int Get_Difficulty() const; @@ -155,7 +155,7 @@ class ViniferaSaveVersionInfo */ int ViniferaVersion; char ViniferaCommitHash[40]; - int SessionID; + int PlaythroughID; int Difficulty; int TotalPlayTime; }; diff --git a/src/vinifera/vinifera_util.cpp b/src/vinifera/vinifera_util.cpp index b9ee29085..1b8f379ff 100644 --- a/src/vinifera/vinifera_util.cpp +++ b/src/vinifera/vinifera_util.cpp @@ -38,7 +38,6 @@ #include "spritecollection.h" #include "filepng.h" #include "filepcx.h" -#include "cncnet4_globals.h" #include "wwfont.h" #include "msgbox.h" #include "minidump.h" @@ -61,26 +60,16 @@ const char *Vinifera_Name_String() if (_buffer[0] == '\0') { - /** - * Append the CnCNet version if enabled. - */ - char *cncnet_mode = nullptr; - if (CnCNet4::IsEnabled) { - cncnet_mode = " (CnCNet4)"; - } - char *dev_mode = nullptr; if (Vinifera_DeveloperMode) { dev_mode = " (Dev)"; } - if (!dev_mode && !cncnet_mode) { + if (!dev_mode) { std::snprintf(_buffer, sizeof(_buffer), "Vinifera"); } else { - std::snprintf(_buffer, sizeof(_buffer), "Vinifera:%s%s", - cncnet_mode != nullptr ? cncnet_mode : "", - dev_mode != nullptr ? dev_mode : ""); + std::snprintf(_buffer, sizeof(_buffer), "Vinifera:%s", dev_mode != nullptr ? dev_mode : ""); } #if defined(TS_CLIENT) @@ -164,24 +153,14 @@ const char *Vinifera_Version_String() static char _buffer[512] { '\0' }; if (_buffer[0] == '\0') { - - /** - * Append the CnCNet version if enabled. - */ - char *cncnet_mode = nullptr; - if (CnCNet4::IsEnabled) { - cncnet_mode = " (CnCNet4)"; - } #ifndef RELEASE - std::snprintf(_buffer, sizeof(_buffer), "Vinifera:%s%s - %s %s %s%s %s", - cncnet_mode != nullptr ? cncnet_mode : "", + std::snprintf(_buffer, sizeof(_buffer), "Vinifera:%s - %s %s %s%s %s", Vinifera_DeveloperMode ? " (Dev)" : "", Vinifera_Git_Branch(), Vinifera_Git_Author(), Vinifera_Git_Uncommitted_Changes() ? "~" : "", Vinifera_Git_Hash_Short(), Vinifera_Git_DateTime()); #else - std::snprintf(_buffer, sizeof(_buffer), "Vinifera:%s%s - %s%s %s", - cncnet_mode != nullptr ? cncnet_mode : "", + std::snprintf(_buffer, sizeof(_buffer), "Vinifera:%s - %s%s %s", Vinifera_DeveloperMode ? " (Dev)" : "", Vinifera_Git_Uncommitted_Changes() ? "~" : "", Vinifera_Git_Hash_Short(), Vinifera_Git_DateTime()); #endif