From 02a1fa974a5ca7dd84d4fb6e56fe1b4d79dbf709 Mon Sep 17 00:00:00 2001 From: pWn3d Date: Wed, 17 Jun 2026 20:48:52 +0200 Subject: [PATCH 1/3] add option to enable singleplayer chatwindow --- GeneralsMD/Code/GameEngine/Include/Common/GlobalData.h | 2 ++ GeneralsMD/Code/GameEngine/Source/Common/GlobalData.cpp | 3 +++ .../Source/GameClient/GUI/GUICallbacks/InGameChat.cpp | 8 ++++++-- .../Source/GameClient/MessageStream/CommandXlat.cpp | 8 ++++++-- 4 files changed, 17 insertions(+), 4 deletions(-) diff --git a/GeneralsMD/Code/GameEngine/Include/Common/GlobalData.h b/GeneralsMD/Code/GameEngine/Include/Common/GlobalData.h index 4c41309fce1..1296b8bc1be 100644 --- a/GeneralsMD/Code/GameEngine/Include/Common/GlobalData.h +++ b/GeneralsMD/Code/GameEngine/Include/Common/GlobalData.h @@ -324,6 +324,8 @@ class GlobalData : public SubsystemInterface // Latency insertion, packet loss for network debugging Int m_netMinPlayers; ///< Min players needed to start a net game + Bool m_enableSingleplayerChatWindow; ///< Allow the in-game chat window in singleplayer/skirmish (for chat commands) + UnsignedInt m_defaultIP; ///< preferred IP address for LAN UnsignedInt m_firewallBehavior; ///< Last detected firewall behavior Bool m_firewallSendDelay; ///< Use send delay for firewall connection negotiations diff --git a/GeneralsMD/Code/GameEngine/Source/Common/GlobalData.cpp b/GeneralsMD/Code/GameEngine/Source/Common/GlobalData.cpp index 8f35ce38912..ed7b223e3a4 100644 --- a/GeneralsMD/Code/GameEngine/Source/Common/GlobalData.cpp +++ b/GeneralsMD/Code/GameEngine/Source/Common/GlobalData.cpp @@ -97,6 +97,7 @@ GlobalData* GlobalData::m_theOriginal = nullptr; { "UseTrees", INI::parseBool, nullptr, offsetof( GlobalData, m_useTrees ) }, { "UseFPSLimit", INI::parseBool, nullptr, offsetof( GlobalData, m_useFpsLimit ) }, { "DumpAssetUsage", INI::parseBool, nullptr, offsetof( GlobalData, m_dumpAssetUsage ) }, + { "EnableSingleplayerChatwindow", INI::parseBool, nullptr, offsetof( GlobalData, m_enableSingleplayerChatWindow ) }, { "FramesPerSecondLimit", INI::parseInt, nullptr, offsetof( GlobalData, m_framesPerSecondLimit ) }, { "ChipsetType", INI::parseInt, nullptr, offsetof( GlobalData, m_chipSetType ) }, { "MaxShellScreens", INI::parseInt, nullptr, offsetof( GlobalData, m_maxShellScreens ) }, @@ -919,6 +920,8 @@ GlobalData::GlobalData() m_netMinPlayers = 1; // allowing sandbox mode + m_enableSingleplayerChatWindow = FALSE; + m_defaultIP = 0; m_BuildSpeed = 0.0f; diff --git a/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/InGameChat.cpp b/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/InGameChat.cpp index 1f21323ade1..a13c2064324 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/InGameChat.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/InGameChat.cpp @@ -199,7 +199,9 @@ void ToggleInGameChat( Bool immediate ) if (TheGameLogic->isInReplayGame()) return; - if (!TheGameInfo->isMultiPlayer() && TheGlobalData->m_netMinPlayers) + // Block the chat window in singleplayer/skirmish unless EnableSingleplayerChatwindow is set (for chat commands). + if (!TheGlobalData->m_enableSingleplayerChatWindow + && (!TheGameInfo || !TheGameInfo->isMultiPlayer()) && TheGlobalData->m_netMinPlayers) return; if (chatWindow) @@ -214,7 +216,9 @@ void ToggleInGameChat( Bool immediate ) // Send what is there, clear it out, and hide the window UnicodeString msg = GadgetTextEntryGetText( chatTextEntry ); msg.trim(); - if (!msg.isEmpty() && !handleInGameSlashCommands(msg)) + // Slash commands are handled above and work in singleplayer. + // The network broadcast below only applies to multiplayer; skip it when there is no network. + if (!msg.isEmpty() && !handleInGameSlashCommands(msg) && TheNetwork && TheGameInfo) { const Player *localPlayer = ThePlayerList->getLocalPlayer(); AsciiString playerName; diff --git a/GeneralsMD/Code/GameEngine/Source/GameClient/MessageStream/CommandXlat.cpp b/GeneralsMD/Code/GameEngine/Source/GameClient/MessageStream/CommandXlat.cpp index 6765f8121cb..fdccee91d6d 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameClient/MessageStream/CommandXlat.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameClient/MessageStream/CommandXlat.cpp @@ -3207,7 +3207,9 @@ GameMessageDisposition CommandTranslator::translateGameMessage(const GameMessage //----------------------------------------------------------------------------------------- case GameMessage::MSG_META_CHAT_ALLIES: - if (TheGameLogic->isInMultiplayerGame() && !TheGameLogic->isInReplayGame()) + // Chat is available in multiplayer, and in singleplayer/skirmish only when EnableSingleplayerChatwindow is set (for chat commands). + if (TheGameLogic->isInInteractiveGame() && !TheGameLogic->isInReplayGame() + && (TheGameLogic->isInMultiplayerGame() || TheGlobalData->m_enableSingleplayerChatWindow)) { Player *localPlayer = ThePlayerList->getLocalPlayer(); if ((localPlayer && localPlayer->isPlayerActive()) || !TheGlobalData->m_netMinPlayers) @@ -3221,7 +3223,9 @@ GameMessageDisposition CommandTranslator::translateGameMessage(const GameMessage //----------------------------------------------------------------------------------------- case GameMessage::MSG_META_CHAT_EVERYONE: - if (TheGameLogic->isInMultiplayerGame() && !TheGameLogic->isInReplayGame()) + // Chat is available in multiplayer, and in singleplayer/skirmish only when EnableSingleplayerChatwindow is set (for chat commands). + if (TheGameLogic->isInInteractiveGame() && !TheGameLogic->isInReplayGame() + && (TheGameLogic->isInMultiplayerGame() || TheGlobalData->m_enableSingleplayerChatWindow)) { Player *localPlayer = ThePlayerList->getLocalPlayer(); // TheSuperHackers @tweak skyaero 19/07/2025 Observers can now chat From e79a5fda428f16eb2c228cfebb71d1ed8b0ea39d Mon Sep 17 00:00:00 2001 From: pWn3d Date: Thu, 18 Jun 2026 17:57:56 +0200 Subject: [PATCH 2/3] add chat commands --- Core/GameEngine/Include/Common/INI.h | 1 + Core/GameEngine/Source/Common/INI/INI.cpp | 1 + GeneralsMD/Code/GameEngine/CMakeLists.txt | 2 + .../GameEngine/Include/Common/ChatCommand.h | 110 +++++ .../Code/GameEngine/Include/Common/Energy.h | 6 + .../Code/GameEngine/Include/Common/Player.h | 11 + .../Include/Common/ProductionPrerequisite.h | 5 +- .../GameEngine/Source/Common/ChatCommand.cpp | 403 ++++++++++++++++++ .../GameEngine/Source/Common/GameEngine.cpp | 3 + .../GameEngine/Source/Common/RTS/Energy.cpp | 24 +- .../GameEngine/Source/Common/RTS/Player.cpp | 13 +- .../Common/RTS/ProductionPrerequisite.cpp | 6 +- .../GUI/GUICallbacks/InGameChat.cpp | 13 + 13 files changed, 592 insertions(+), 6 deletions(-) create mode 100644 GeneralsMD/Code/GameEngine/Include/Common/ChatCommand.h create mode 100644 GeneralsMD/Code/GameEngine/Source/Common/ChatCommand.cpp diff --git a/Core/GameEngine/Include/Common/INI.h b/Core/GameEngine/Include/Common/INI.h index d5b1733248a..332ea285d0a 100644 --- a/Core/GameEngine/Include/Common/INI.h +++ b/Core/GameEngine/Include/Common/INI.h @@ -222,6 +222,7 @@ class INI static void parseMetaMapDefinition( INI *ini ); static void parseFXListDefinition( INI *ini ); static void parseBuffTemplateDefinition( INI* ini ); + static void parseChatCommandDefinition( INI* ini ); static void parseObjectCreationListDefinition( INI* ini ); static void parseMultiplayerSettingsDefinition( INI* ini ); static void parseMultiplayerColorDefinition( INI* ini ); diff --git a/Core/GameEngine/Source/Common/INI/INI.cpp b/Core/GameEngine/Source/Common/INI/INI.cpp index 31e1d16344e..03aa9aba0bd 100644 --- a/Core/GameEngine/Source/Common/INI/INI.cpp +++ b/Core/GameEngine/Source/Common/INI/INI.cpp @@ -91,6 +91,7 @@ static const BlockParse theTypeTable[] = { "BuffTemplate", INI::parseBuffTemplateDefinition }, { "Campaign", INI::parseCampaignDefinition }, { "ChallengeGenerals", INI::parseChallengeModeDefinition }, + { "ChatCommand", INI::parseChatCommandDefinition }, { "CommandButton", INI::parseCommandButtonDefinition }, { "CommandMap", INI::parseMetaMapDefinition }, { "CommandSet", INI::parseCommandSetDefinition }, diff --git a/GeneralsMD/Code/GameEngine/CMakeLists.txt b/GeneralsMD/Code/GameEngine/CMakeLists.txt index 78426594282..55ad63fdeac 100644 --- a/GeneralsMD/Code/GameEngine/CMakeLists.txt +++ b/GeneralsMD/Code/GameEngine/CMakeLists.txt @@ -42,6 +42,7 @@ set(GAMEENGINE_SRC # Include/Common/GameAudio.h # Include/Common/GameCommon.h # Include/Common/GameDefines.h + Include/Common/ChatCommand.h Include/Common/GameEngine.h Include/Common/GameLOD.h # Include/Common/GameMemory.h @@ -601,6 +602,7 @@ set(GAMEENGINE_SRC Source/Common/Bezier/BezFwdIterator.cpp Source/Common/Bezier/BezierSegment.cpp Source/Common/BitFlags.cpp + Source/Common/ChatCommand.cpp Source/Common/CommandLine.cpp # Source/Common/crc.cpp # Source/Common/CRCDebug.cpp diff --git a/GeneralsMD/Code/GameEngine/Include/Common/ChatCommand.h b/GeneralsMD/Code/GameEngine/Include/Common/ChatCommand.h new file mode 100644 index 00000000000..d2886f32348 --- /dev/null +++ b/GeneralsMD/Code/GameEngine/Include/Common/ChatCommand.h @@ -0,0 +1,110 @@ +/* +** Command & Conquer Generals Zero Hour(tm) +** Copyright 2025 Electronic Arts Inc. +** +** This program is free software: you can redistribute it and/or modify +** it under the terms of the GNU General Public License as published by +** the Free Software Foundation, either version 3 of the License, or +** (at your option) any later version. +** +** This program is distributed in the hope that it will be useful, +** but WITHOUT ANY WARRANTY; without even the implied warranty of +** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +** GNU General Public License for more details. +** +** You should have received a copy of the GNU General Public License +** along with this program. If not, see . +*/ + +// FILE: ChatCommand.h ////////////////////////////////////////////////////////////////////////// +// Desc: Parsing and storage for ChatCommand blocks defined in the optional ChatCommands.ini. +// Commands carry no behavior yet; key/value attributes and dispatch logic come later. +////////////////////////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#ifndef _ChatCommand_H_ +#define _ChatCommand_H_ + +// INCLUDES /////////////////////////////////////////////////////////////////////////////////////// +#include "Common/AsciiString.h" +#include "Common/SubsystemInterface.h" +#include + +// FORWARD REFERENCES ///////////////////////////////////////////////////////////////////////////// +class INI; +struct FieldParse; + +//------------------------------------------------------------------------------------------------- +/** A single chat command parsed from a "ChatCommand ... End" block. */ +//------------------------------------------------------------------------------------------------- +class ChatCommand +{ +public: + ChatCommand() {} + + const AsciiString& getName() const { return m_name; } + void setName( const AsciiString& name ) { m_name = name; } + + const FieldParse* getFieldParse() const { return s_fieldParseTable; } + + Int getMoney() const { return m_money; } + UnsignedInt getRank() const { return m_rank; } + Bool getReadyTimers() const { return m_readyTimers; } + const AsciiString& getSpawnUnit() const { return m_spawnUnit; } + Bool getToggleUnitRequirements() const { return m_toggleUnitRequirements; } + Bool getToggleInfiniteEnergy() const { return m_toggleInfiniteEnergy; } + Bool getGrantAllUpgrades() const { return m_grantAllUpgrades; } + Int getPromoteUnit() const { return m_promoteUnit; } + Int getGiveSalvage() const { return m_giveSalvage; } + + /** Run this command's effects. Inspects the parsed members and acts accordingly. */ + void execute() const; + +private: + AsciiString m_name; + Int m_money = 0; ///< "Money" attribute; signed amount, defaults to 0. + UnsignedInt m_rank = 0; ///< "Rank" attribute; ranks to grant, capped at the max rank. Defaults to 0. + Bool m_readyTimers = FALSE; ///< "ReadyTimers" attribute; when TRUE, set all of the player's special power timers to ready. + AsciiString m_spawnUnit; ///< "SpawnUnit" attribute; ObjectTemplate name to spawn for the local player at the mouse cursor. + Bool m_toggleUnitRequirements = FALSE; ///< "ToggleUnitRequirements" attribute; when TRUE, toggles ignoring unit/building build prereqs (science still applies). + Bool m_toggleInfiniteEnergy = FALSE; ///< "ToggleInfiniteEnergy" attribute; when TRUE, toggles infinite power for the local player. + Bool m_grantAllUpgrades = FALSE; ///< "GrantAllUpgrades" attribute; when TRUE, grants the local player all player-type upgrades. + Int m_promoteUnit = 0; ///< "PromoteUnit" attribute; promote selected units by this many veterancy levels (negative demotes), capped to the valid range. + Int m_giveSalvage = 0; ///< "GiveSalvage" attribute; change selected salvagers' crate-upgrade tier by this much (negative removes), capped 0..2. + + static const FieldParse s_fieldParseTable[]; +}; + +//------------------------------------------------------------------------------------------------- +/** The store that owns all ChatCommands parsed from ChatCommands.ini. */ +//------------------------------------------------------------------------------------------------- +class ChatCommandStore : public SubsystemInterface +{ +public: + ChatCommandStore() {} + virtual ~ChatCommandStore(); + + virtual void init() {} + virtual void reset(); + virtual void update() {} + + /** Return the command with the given name, or NULL if none exists. */ + const ChatCommand* findChatCommand( const AsciiString& name ) const; + + /** Number of parsed commands. */ + UnsignedInt getChatCommandCount() const { return (UnsignedInt)m_commands.size(); } + + // INI block parser, registered in INI's block table. + static void parseChatCommandDefinition( INI* ini ); + +private: + void clear(); + + std::vector m_commands; +}; + +// EXTERNALS ////////////////////////////////////////////////////////////////////////////////////// +extern ChatCommandStore* TheChatCommandStore; + +#endif // _ChatCommand_H_ diff --git a/GeneralsMD/Code/GameEngine/Include/Common/Energy.h b/GeneralsMD/Code/GameEngine/Include/Common/Energy.h index 0aea6f0c710..137db81857c 100644 --- a/GeneralsMD/Code/GameEngine/Include/Common/Energy.h +++ b/GeneralsMD/Code/GameEngine/Include/Common/Energy.h @@ -71,6 +71,7 @@ class Energy : public Snapshot m_energyProduction = 0; m_energyConsumption = 0; m_powerSabotagedTillFrame = 0; + m_infinitePower = FALSE; m_owner = owner; } @@ -98,6 +99,10 @@ class Energy : public Snapshot void setPowerSabotagedTillFrame( UnsignedInt frame ) { m_powerSabotagedTillFrame = frame; } UnsignedInt getPowerSabotagedTillFrame() const { return m_powerSabotagedTillFrame; } + /// when set, the player always has sufficient power (overrides production/consumption and sabotage). + void setInfinitePower( Bool enable ) { m_infinitePower = enable; } + Bool hasInfinitePower() const { return m_infinitePower; } + /** return the percentage of energy needed that we actually produce, as a 0.0 ... 1.0 fraction. */ @@ -118,5 +123,6 @@ class Energy : public Snapshot Int m_energyProduction; ///< level of energy production, in kw Int m_energyConsumption; ///< level of energy consumption, in kw UnsignedInt m_powerSabotagedTillFrame; ///< If power is sabotaged, the frame will be greater than now. + Bool m_infinitePower; ///< cheat: always have sufficient power Player *m_owner; ///< Tight pointer to the Player I am intrinsic to. }; diff --git a/GeneralsMD/Code/GameEngine/Include/Common/Player.h b/GeneralsMD/Code/GameEngine/Include/Common/Player.h index ef47af80734..c714c6c0748 100644 --- a/GeneralsMD/Code/GameEngine/Include/Common/Player.h +++ b/GeneralsMD/Code/GameEngine/Include/Common/Player.h @@ -247,6 +247,10 @@ class Player : public Snapshot void addPowerBonus(Object *obj) { m_energy.addPowerBonus(obj); } void removePowerBonus(Object *obj) { m_energy.removePowerBonus(obj); } + /// cheat: set/toggle infinite power, refreshing power-dependent objects to match. + void setInfinitePower(Bool enable); + void toggleInfinitePower() { setInfinitePower( !m_energy.hasInfinitePower() ); } + ResourceGatheringManager *getResourceGatheringManager(){ return m_resourceGatheringManager; } TunnelTracker* getTunnelSystem(){ return m_tunnelSystem; } @@ -348,6 +352,11 @@ class Player : public Snapshot Bool buildsInstantly() const { return m_DEMO_instantBuild; } #endif + /// When set, unit/building build prerequisites are ignored for this player (science prereqs still apply). + void toggleIgnoreUnitPrereqs() { m_ignoreUnitPrereqs = !m_ignoreUnitPrereqs; } + void setIgnoreUnitPrereqs(Bool enable) { m_ignoreUnitPrereqs = enable; } + Bool ignoresUnitPrereqs() const { return m_ignoreUnitPrereqs; } + ///< Power just changed at all. Didn't make two functions so you can't forget to undo something you didin one of them. ///< @todo Can't do edge trigger until after demo; make things check for power on creation void onPowerBrownOutChange( Bool brownOut ); @@ -842,6 +851,8 @@ class Player : public Snapshot Bool m_DEMO_instantBuild; ///< Can I build anything in one frame? #endif + Bool m_ignoreUnitPrereqs; ///< ignore unit/building build prereqs (science prereqs still apply) + ScoreKeeper m_scoreKeeper; ///< The local scorekeeper for this player // Production Cost modifier diff --git a/GeneralsMD/Code/GameEngine/Include/Common/ProductionPrerequisite.h b/GeneralsMD/Code/GameEngine/Include/Common/ProductionPrerequisite.h index ad9d1f833ba..7e13b18d5d9 100644 --- a/GeneralsMD/Code/GameEngine/Include/Common/ProductionPrerequisite.h +++ b/GeneralsMD/Code/GameEngine/Include/Common/ProductionPrerequisite.h @@ -79,8 +79,9 @@ class ProductionPrerequisite /// not satisfied yet UnicodeString getRequiresList(const Player *player) const; - /// return true iff the player satisfies our set of prerequisites - Bool isSatisfied(const Player *player) const; + /// return true iff the player satisfies our set of prerequisites. + /// if ignoreUnitPrereqs is set, only science prerequisites are enforced. + Bool isSatisfied(const Player *player, Bool ignoreUnitPrereqs = FALSE) const; /** return the BuildFacilityTemplate, if any. diff --git a/GeneralsMD/Code/GameEngine/Source/Common/ChatCommand.cpp b/GeneralsMD/Code/GameEngine/Source/Common/ChatCommand.cpp new file mode 100644 index 00000000000..9fcfc07f109 --- /dev/null +++ b/GeneralsMD/Code/GameEngine/Source/Common/ChatCommand.cpp @@ -0,0 +1,403 @@ +/* +** Command & Conquer Generals Zero Hour(tm) +** Copyright 2025 Electronic Arts Inc. +** +** This program is free software: you can redistribute it and/or modify +** it under the terms of the GNU General Public License as published by +** the Free Software Foundation, either version 3 of the License, or +** (at your option) any later version. +** +** This program is distributed in the hope that it will be useful, +** but WITHOUT ANY WARRANTY; without even the implied warranty of +** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +** GNU General Public License for more details. +** +** You should have received a copy of the GNU General Public License +** along with this program. If not, see . +*/ + +// FILE: ChatCommand.cpp //////////////////////////////////////////////////////////////////////// +// Desc: Parsing and storage for ChatCommand blocks defined in the optional ChatCommands.ini. +////////////////////////////////////////////////////////////////////////////////////////////////// + +// INCLUDES /////////////////////////////////////////////////////////////////////////////////////// +#include "PreRTS.h" // This must go first in EVERY cpp file in the GameEngine + +#include "Common/ChatCommand.h" +#include "Common/INI.h" +#include "Common/Money.h" +#include "Common/Player.h" +#include "Common/PlayerList.h" +#include "Common/Upgrade.h" +#include "Common/GameAudio.h" +#include "Common/MiscAudio.h" +#include "Common/Team.h" +#include "Common/ThingFactory.h" +#include "Common/ThingTemplate.h" +#include "Common/KindOf.h" +#include "Common/ModelState.h" +#include "GameClient/ControlBar.h" +#include "GameClient/InGameUI.h" +#include "GameClient/Mouse.h" +#include "GameClient/View.h" +#include "GameClient/Drawable.h" +#include "GameLogic/GameLogic.h" +#include "GameLogic/Object.h" +#include "GameLogic/ExperienceTracker.h" +#include "GameLogic/WeaponSetType.h" +#include "GameLogic/ArmorSet.h" +#include "GameLogic/Module/BehaviorModule.h" +#include "GameLogic/Module/SpecialPowerModule.h" + +//------------------------------------------------------------------------------------------------- +ChatCommandStore* TheChatCommandStore = nullptr; + +const FieldParse ChatCommand::s_fieldParseTable[] = +{ + { "Money", INI::parseInt, nullptr, offsetof( ChatCommand, m_money ) }, + { "Rank", INI::parseUnsignedInt, nullptr, offsetof( ChatCommand, m_rank ) }, + { "ReadyTimers", INI::parseBool, nullptr, offsetof( ChatCommand, m_readyTimers ) }, + { "SpawnUnit", INI::parseAsciiString, nullptr, offsetof( ChatCommand, m_spawnUnit ) }, + { "ToggleUnitRequirements", INI::parseBool, nullptr, offsetof( ChatCommand, m_toggleUnitRequirements ) }, + { "ToggleInfiniteEnergy", INI::parseBool, nullptr, offsetof( ChatCommand, m_toggleInfiniteEnergy ) }, + { "GrantAllUpgrades", INI::parseBool, nullptr, offsetof( ChatCommand, m_grantAllUpgrades ) }, + { "PromoteUnit", INI::parseInt, nullptr, offsetof( ChatCommand, m_promoteUnit ) }, + { "GiveSalvage", INI::parseInt, nullptr, offsetof( ChatCommand, m_giveSalvage ) }, + { NULL, NULL, 0, 0 } // keep this last +}; + +//------------------------------------------------------------------------------------------------- +// Set a weapon-salvager's crate-upgrade tier (0=none, 1=ONE, 2=TWO) to match newTier. +static void setWeaponSalvageTier( Object *obj, Int newTier ) +{ + obj->clearWeaponSetFlag( WEAPONSET_CRATEUPGRADE_ONE ); + obj->clearWeaponSetFlag( WEAPONSET_CRATEUPGRADE_TWO ); + if (newTier >= 2) + obj->setWeaponSetFlag( WEAPONSET_CRATEUPGRADE_TWO ); + else if (newTier == 1) + obj->setWeaponSetFlag( WEAPONSET_CRATEUPGRADE_ONE ); +} + +//------------------------------------------------------------------------------------------------- +// Set an armor-salvager's crate-upgrade tier (0=none, 1=ONE, 2=TWO), including model visuals. +static void setArmorSalvageTier( Object *obj, Int newTier ) +{ + obj->clearArmorSetFlag( ARMORSET_CRATE_UPGRADE_ONE ); + obj->clearArmorSetFlag( ARMORSET_CRATE_UPGRADE_TWO ); + obj->clearModelConditionState( MODELCONDITION_ARMORSET_CRATEUPGRADE_ONE ); + obj->clearModelConditionState( MODELCONDITION_ARMORSET_CRATEUPGRADE_TWO ); + if (newTier >= 2) + { + obj->setArmorSetFlag( ARMORSET_CRATE_UPGRADE_TWO ); + obj->setModelConditionState( MODELCONDITION_ARMORSET_CRATEUPGRADE_TWO ); + } + else if (newTier == 1) + { + obj->setArmorSetFlag( ARMORSET_CRATE_UPGRADE_ONE ); + obj->setModelConditionState( MODELCONDITION_ARMORSET_CRATEUPGRADE_ONE ); + } +} + +//------------------------------------------------------------------------------------------------- +void ChatCommand::execute() const +{ + // Money: positive grants cash, negative removes it. The local player's funds can never + // go below zero (withdraw caps at the current balance), so a large removal just empties it. + if (m_money != 0) + { + Player *player = ThePlayerList ? ThePlayerList->getLocalPlayer() : nullptr; + Money *money = player ? player->getMoney() : nullptr; + if (money) + { + // Suppress the built-in deposit/withdraw sound; play the cash-hack sound instead (below). + if (m_money > 0) + money->deposit( (UnsignedInt)m_money, FALSE ); + else + money->withdraw( (UnsignedInt)(-m_money), FALSE ); + + // Play the same sound the Cash Hack special power uses, for the local player. + if (TheAudio && TheAudio->getMiscAudio()) + { + AudioEventRTS sound = TheAudio->getMiscAudio()->m_moneyDepositSound; + sound.setPlayerIndex( player->getPlayerIndex() ); + TheAudio->addAudioEvent( &sound ); + } + } + } + + // Rank: grant additional rank levels. setRankLevel() clamps to the maximum rank, + // so over-granting just tops the player out. + if (m_rank > 0) + { + Player *player = ThePlayerList ? ThePlayerList->getLocalPlayer() : nullptr; + if (player) + player->setRankLevel( player->getRankLevel() + (Int)m_rank ); + } + + // ReadyTimers: set every special power timer the player owns to ready (available now). + if (m_readyTimers) + { + Player *player = ThePlayerList ? ThePlayerList->getLocalPlayer() : nullptr; + if (player) + { + const UnsignedInt now = TheGameLogic->getFrame(); + for (Player::PlayerTeamList::const_iterator pt = player->getPlayerTeams()->begin(); + pt != player->getPlayerTeams()->end(); ++pt) + { + for (DLINK_ITERATOR teamIt = (*pt)->iterate_TeamInstanceList(); !teamIt.done(); teamIt.advance()) + { + Team *team = teamIt.cur(); + if (!team) + continue; + + for (DLINK_ITERATOR objIt = team->iterate_TeamMemberList(); !objIt.done(); objIt.advance()) + { + Object *obj = objIt.cur(); + if (!obj) + continue; + + for (BehaviorModule **b = obj->getBehaviorModules(); b && *b; ++b) + { + SpecialPowerModuleInterface *sp = (*b)->getSpecialPower(); + if (sp) + { + // Ready the per-object timer (superweapons) and the player's shared + // timer (generals' shortcut powers read their readiness from there). + sp->setReadyFrame( now ); + const SpecialPowerTemplate *temp = sp->getSpecialPowerTemplate(); + if (temp) + player->expressSpecialPowerReadyFrame( temp, now ); + } + } + } + } + } + } + } + + // SpawnUnit: create one instance of the named ObjectTemplate for the local player, + // placed on the terrain under the mouse cursor. + if (!m_spawnUnit.isEmpty()) + { + const ThingTemplate *tmpl = TheThingFactory ? TheThingFactory->findTemplate( m_spawnUnit ) : nullptr; + Player *player = ThePlayerList ? ThePlayerList->getLocalPlayer() : nullptr; + Team *team = player ? player->getDefaultTeam() : nullptr; + if (tmpl && team && TheMouse && TheTacticalView) + { + Coord3D pos; + const MouseIO *mouseIO = TheMouse->getMouseStatus(); + TheTacticalView->screenToTerrain( &mouseIO->pos, &pos ); + + Object *obj = TheThingFactory->newObject( tmpl, team ); + if (obj) + { + obj->setPosition( &pos ); + obj->setOrientation( 0.0f ); + } + } + else if (!tmpl) + { + DEBUG_LOG((">>> CHAT COMMAND SpawnUnit: ThingTemplate '%s' not found.", m_spawnUnit.str())); + } + } + + // ToggleUnitRequirements: flip ignoring of unit/building build prereqs for the local player + // (science prereqs still apply). Mark the control bar dirty so build buttons re-evaluate. + if (m_toggleUnitRequirements) + { + Player *player = ThePlayerList ? ThePlayerList->getLocalPlayer() : nullptr; + if (player) + { + player->toggleIgnoreUnitPrereqs(); + if (TheControlBar) + TheControlBar->markUIDirty(); + } + } + + // ToggleInfiniteEnergy: flip infinite power for the local player. The setter refreshes + // power-dependent objects; mark the control bar dirty so the power UI updates. + if (m_toggleInfiniteEnergy) + { + Player *player = ThePlayerList ? ThePlayerList->getLocalPlayer() : nullptr; + if (player) + { + player->toggleInfinitePower(); + if (TheControlBar) + TheControlBar->markUIDirty(); + } + } + + // GrantAllUpgrades: complete every player-type upgrade for the local player. + if (m_grantAllUpgrades) + { + Player *player = ThePlayerList ? ThePlayerList->getLocalPlayer() : nullptr; + if (player && TheUpgradeCenter) + { + for (UpgradeTemplate *tmpl = TheUpgradeCenter->firstUpgradeTemplate(); + tmpl != nullptr; tmpl = tmpl->friend_getNext()) + { + if (tmpl->getUpgradeType() != UPGRADE_TYPE_PLAYER) + continue; + if (player->hasUpgradeComplete( tmpl )) + continue; + player->addUpgrade( tmpl, UPGRADE_STATUS_COMPLETE ); + } + } + } + + // PromoteUnit: change veterancy of the player's currently selected (trainable) units by + // m_promoteUnit levels (negative demotes), clamped to the valid range. + if (m_promoteUnit != 0 && TheInGameUI) + { + const DrawableList *selected = TheInGameUI->getAllSelectedLocalDrawables(); + if (selected) + { + for (DrawableList::const_iterator it = selected->begin(); it != selected->end(); ++it) + { + Drawable *draw = *it; + Object *obj = draw ? draw->getObject() : nullptr; + ExperienceTracker *exp = obj ? obj->getExperienceTracker() : nullptr; + if (!exp || !exp->isTrainable()) + continue; + + Int newLevel = (Int)exp->getVeterancyLevel() + m_promoteUnit; + if (newLevel < LEVEL_FIRST) + newLevel = LEVEL_FIRST; + else if (newLevel > LEVEL_LAST) + newLevel = LEVEL_LAST; + + exp->setVeterancyLevel( (VeterancyLevel)newLevel ); + } + } + } + + // GiveSalvage: change the crate-upgrade tier of the player's selected salvagers by + // m_giveSalvage steps (negative removes), clamped 0..2. Weapon tier only applies to + // weapon-salvagers, armor tier only to armor-salvagers; other units are unaffected. + if (m_giveSalvage != 0 && TheInGameUI) + { + const DrawableList *selected = TheInGameUI->getAllSelectedLocalDrawables(); + if (selected) + { + for (DrawableList::const_iterator it = selected->begin(); it != selected->end(); ++it) + { + Drawable *draw = *it; + Object *obj = draw ? draw->getObject() : nullptr; + if (!obj) + continue; + + Bool changed = FALSE; + + if (obj->isKindOf( KINDOF_WEAPON_SALVAGER )) + { + Int cur = obj->testWeaponSetFlag( WEAPONSET_CRATEUPGRADE_TWO ) ? 2 + : obj->testWeaponSetFlag( WEAPONSET_CRATEUPGRADE_ONE ) ? 1 : 0; + Int next = cur + m_giveSalvage; + next = next < 0 ? 0 : (next > 2 ? 2 : next); + if (next != cur) + { + setWeaponSalvageTier( obj, next ); + changed = TRUE; + } + } + + if (obj->isKindOf( KINDOF_ARMOR_SALVAGER )) + { + Int cur = obj->testArmorSetFlag( ARMORSET_CRATE_UPGRADE_TWO ) ? 2 + : obj->testArmorSetFlag( ARMORSET_CRATE_UPGRADE_ONE ) ? 1 : 0; + Int next = cur + m_giveSalvage; + next = next < 0 ? 0 : (next > 2 ? 2 : next); + if (next != cur) + { + setArmorSalvageTier( obj, next ); + changed = TRUE; + } + } + + // play the salvage crate pickup sound on the unit when its tier changed. + if (changed && TheAudio && TheAudio->getMiscAudio()) + { + AudioEventRTS sound = TheAudio->getMiscAudio()->m_crateSalvage; + sound.setObjectID( obj->getID() ); + TheAudio->addAudioEvent( &sound ); + } + } + } + } + + // Notify the player that the command ran. + if (TheInGameUI) + { + UnicodeString uName; + uName.translate( m_name ); + UnicodeString msg; + msg.format( L"Chat command executed: %s", uName.str() ); + TheInGameUI->message( msg ); + } +} + +//------------------------------------------------------------------------------------------------- +ChatCommandStore::~ChatCommandStore() +{ + clear(); +} + +//------------------------------------------------------------------------------------------------- +void ChatCommandStore::clear() +{ + for (std::vector::iterator it = m_commands.begin(); it != m_commands.end(); ++it) + delete *it; + m_commands.clear(); +} + +//------------------------------------------------------------------------------------------------- +void ChatCommandStore::reset() +{ + // Chat commands are static definitions loaded once from ChatCommands.ini; they must persist + // across game resets (like ThingTemplates), so do not clear them here. +} + +//------------------------------------------------------------------------------------------------- +const ChatCommand* ChatCommandStore::findChatCommand( const AsciiString& name ) const +{ + for (std::vector::const_iterator it = m_commands.begin(); it != m_commands.end(); ++it) + { + if ((*it)->getName().compareNoCase(name) == 0) + return *it; + } + return nullptr; +} + +//------------------------------------------------------------------------------------------------- +/*static*/ void ChatCommandStore::parseChatCommandDefinition( INI* ini ) +{ + // read the command name that follows the "ChatCommand" keyword + AsciiString name; + name.set( ini->getNextToken() ); + + if (!TheChatCommandStore) + return; + + // command names must be unique; a duplicate would shadow the earlier definition at dispatch. + if (TheChatCommandStore->findChatCommand( name ) != nullptr) + { + DEBUG_CRASH(("[LINE: %d - FILE: '%s'] Duplicate ChatCommand '%s'", ini->getLineNum(), ini->getFilename().str(), name.str())); + throw INI_INVALID_DATA; + } + + ChatCommand* command = new ChatCommand; + command->setName( name ); + + // consume the block to its "End" token; no attributes are defined yet + ini->initFromINI( command, command->getFieldParse() ); + + TheChatCommandStore->m_commands.push_back( command ); + + DEBUG_LOG((">>> ADDED CHAT COMMAND '%s'", command->getName().str())); +} + +//------------------------------------------------------------------------------------------------- +/*static*/ void INI::parseChatCommandDefinition( INI* ini ) +{ + ChatCommandStore::parseChatCommandDefinition( ini ); +} diff --git a/GeneralsMD/Code/GameEngine/Source/Common/GameEngine.cpp b/GeneralsMD/Code/GameEngine/Source/Common/GameEngine.cpp index fccf1aaccd1..1b96d2b4cb2 100644 --- a/GeneralsMD/Code/GameEngine/Source/Common/GameEngine.cpp +++ b/GeneralsMD/Code/GameEngine/Source/Common/GameEngine.cpp @@ -47,6 +47,7 @@ #include "Common/FileSystem.h" #include "Common/ArchiveFileSystem.h" #include "Common/LocalFileSystem.h" +#include "Common/ChatCommand.h" #include "Common/GlobalData.h" #include "Common/PerfTimer.h" #include "Common/RandomValue.h" @@ -562,6 +563,8 @@ void GameEngine::init() initSubsystem(TheArmorStore,"TheArmorStore", MSGNEW("GameEngineSubsystem") ArmorStore(), &xferCRC, nullptr, "Data\\INI\\Armor"); initSubsystem(TheBuildAssistant,"TheBuildAssistant", MSGNEW("GameEngineSubsystem") BuildAssistant, nullptr); initSubsystem(TheBuffTemplateStore, "TheBuffTemplateStore", MSGNEW("GameEngineSubsystem") BuffTemplateStore(), &xferCRC, NULL, "Data\\INI\\BuffTemplate", TRUE); + // ChatCommands.ini is optional and client-only; do not feed it into the CRC. + initSubsystem(TheChatCommandStore, "TheChatCommandStore", MSGNEW("GameEngineSubsystem") ChatCommandStore(), nullptr, NULL, "Data\\INI\\ChatCommands", TRUE); #ifdef DUMP_PERF_STATS/////////////////////////////////////////////////////////////////////////// GetPrecisionTimer(&endTime64);////////////////////////////////////////////////////////////////// diff --git a/GeneralsMD/Code/GameEngine/Source/Common/RTS/Energy.cpp b/GeneralsMD/Code/GameEngine/Source/Common/RTS/Energy.cpp index 702a300c884..605ad71ba3d 100644 --- a/GeneralsMD/Code/GameEngine/Source/Common/RTS/Energy.cpp +++ b/GeneralsMD/Code/GameEngine/Source/Common/RTS/Energy.cpp @@ -61,11 +61,18 @@ Energy::Energy() m_energyConsumption = 0; m_owner = nullptr; m_powerSabotagedTillFrame = 0; + m_infinitePower = FALSE; } //----------------------------------------------------------------------------- Int Energy::getProduction() const { + if( m_infinitePower ) + { + // always report at least enough production to cover consumption. + return m_energyProduction > m_energyConsumption ? m_energyProduction : m_energyConsumption; + } + if( TheGameLogic->getFrame() < m_powerSabotagedTillFrame ) { //Power sabotaged, therefore no power. @@ -79,6 +86,9 @@ Real Energy::getEnergySupplyRatio() const { DEBUG_ASSERTCRASH(m_energyProduction >= 0 && m_energyConsumption >= 0, ("neg Energy numbers")); + if( m_infinitePower ) + return 1.0f; + if( TheGameLogic->getFrame() < m_powerSabotagedTillFrame ) { //Power sabotaged, therefore no power, no ratio. @@ -94,6 +104,9 @@ Real Energy::getEnergySupplyRatio() const //------------------------------------------------------------------------------------------------- Bool Energy::hasSufficientPower(void) const { + if( m_infinitePower ) + return TRUE; + if( TheGameLogic->getFrame() < m_powerSabotagedTillFrame ) { //Power sabotaged, therefore no power. @@ -260,13 +273,14 @@ void Energy::crc( Xfer *xfer ) // ------------------------------------------------------------------------------------------------ /** Xfer method * Version Info: - * 1: Initial version */ + * 1: Initial version + * 4: infinite power cheat flag */ // ------------------------------------------------------------------------------------------------ void Energy::xfer( Xfer *xfer ) { // version - XferVersion currentVersion = 3; + XferVersion currentVersion = 4; XferVersion version = currentVersion; xfer->xferVersion( &version, currentVersion ); @@ -294,6 +308,12 @@ void Energy::xfer( Xfer *xfer ) xfer->xferUnsignedInt( &m_powerSabotagedTillFrame ); } + // infinite power cheat flag + if( version >= 4 ) + { + xfer->xferBool( &m_infinitePower ); + } + } // ------------------------------------------------------------------------------------------------ diff --git a/GeneralsMD/Code/GameEngine/Source/Common/RTS/Player.cpp b/GeneralsMD/Code/GameEngine/Source/Common/RTS/Player.cpp index c50c5557de2..1a6468153fc 100644 --- a/GeneralsMD/Code/GameEngine/Source/Common/RTS/Player.cpp +++ b/GeneralsMD/Code/GameEngine/Source/Common/RTS/Player.cpp @@ -417,6 +417,8 @@ void Player::init(const PlayerTemplate* pt) m_DEMO_instantBuild = FALSE; #endif + m_ignoreUnitPrereqs = FALSE; + if (pt) { m_side = pt->getSide(); @@ -3134,7 +3136,8 @@ Bool Player::canBuild(const ThingTemplate *tmplate) const for (Int i = 0; i < tmplate->getPrereqCount(); i++) { const ProductionPrerequisite *pre = tmplate->getNthPrereq(i); - if (pre->isSatisfied(this) == false ) + // when ignoring unit prereqs, only science prereqs are enforced. + if (pre->isSatisfied(this, ignoresUnitPrereqs()) == false ) prereqsOK = false; } @@ -3472,6 +3475,14 @@ void Player::onPowerBrownOutChange( Bool brownOut ) iterateObjects( doPowerDisable, &brownOut );// This function is so cool. } +//------------------------------------------------------------------------------------------------- +void Player::setInfinitePower( Bool enable ) +{ + m_energy.setInfinitePower( enable ); + // refresh power-dependent objects to match the new supply state. + onPowerBrownOutChange( !m_energy.hasSufficientPower() ); +} + diff --git a/GeneralsMD/Code/GameEngine/Source/Common/RTS/ProductionPrerequisite.cpp b/GeneralsMD/Code/GameEngine/Source/Common/RTS/ProductionPrerequisite.cpp index 6b062c55a52..dc49d00f657 100644 --- a/GeneralsMD/Code/GameEngine/Source/Common/RTS/ProductionPrerequisite.cpp +++ b/GeneralsMD/Code/GameEngine/Source/Common/RTS/ProductionPrerequisite.cpp @@ -147,7 +147,7 @@ const ThingTemplate *ProductionPrerequisite::getExistingBuildFacilityTemplate( c } //----------------------------------------------------------------------------- -Bool ProductionPrerequisite::isSatisfied(const Player *player) const +Bool ProductionPrerequisite::isSatisfied(const Player *player, Bool ignoreUnitPrereqs) const { Int i; @@ -161,6 +161,10 @@ Bool ProductionPrerequisite::isSatisfied(const Player *player) const return false; } + // unit/building prerequisites can be bypassed (science prereqs above are always enforced). + if (ignoreUnitPrereqs) + return true; + // the player must have at least one instance of each prereq unit. Int ownCount[MAX_PREREQ]; Int cnt = calcNumPrereqUnitsOwned(player, ownCount); diff --git a/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/InGameChat.cpp b/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/InGameChat.cpp index a13c2064324..1b3356d4274 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/InGameChat.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/InGameChat.cpp @@ -30,6 +30,7 @@ // INCLUDES /////////////////////////////////////////////////////////////////////////////////////// #include "PreRTS.h" // This must go first in EVERY cpp file in the GameEngine +#include "Common/ChatCommand.h" #include "Common/Player.h" #include "Common/PlayerList.h" #include "GameClient/DisconnectMenu.h" @@ -182,6 +183,18 @@ Bool handleInGameSlashCommands(UnicodeString uText) return TRUE; // was a slash command } + // User-defined chat commands from ChatCommands.ini. Singleplayer/skirmish only for now. + const Bool isSinglePlayer = (!TheGameInfo || !TheGameInfo->isMultiPlayer()); + if (isSinglePlayer && TheChatCommandStore) + { + const ChatCommand *command = TheChatCommandStore->findChatCommand(token); + if (command) + { + command->execute(); + return TRUE; // was a slash command + } + } + return FALSE; // not a slash command } From ebd737128f7283d49204f41f724753da1b0480ff Mon Sep 17 00:00:00 2001 From: pWn3d Date: Fri, 19 Jun 2026 18:12:31 +0200 Subject: [PATCH 3/3] renamed chat commands --- .../GameEngine/Include/Common/ChatCommand.h | 26 +++---- .../Code/GameEngine/Include/Common/Player.h | 7 ++ .../GameEngine/Source/Common/ChatCommand.cpp | 69 +++++++++++-------- .../GameEngine/Source/Common/RTS/Player.cpp | 10 ++- .../Source/Common/System/Upgrade.cpp | 9 ++- .../Source/Common/Thing/ThingTemplate.cpp | 5 ++ 6 files changed, 85 insertions(+), 41 deletions(-) diff --git a/GeneralsMD/Code/GameEngine/Include/Common/ChatCommand.h b/GeneralsMD/Code/GameEngine/Include/Common/ChatCommand.h index d2886f32348..ad07aa74342 100644 --- a/GeneralsMD/Code/GameEngine/Include/Common/ChatCommand.h +++ b/GeneralsMD/Code/GameEngine/Include/Common/ChatCommand.h @@ -48,30 +48,32 @@ class ChatCommand const FieldParse* getFieldParse() const { return s_fieldParseTable; } - Int getMoney() const { return m_money; } - UnsignedInt getRank() const { return m_rank; } + Int getAddMoney() const { return m_addMoney; } + UnsignedInt getAddRank() const { return m_addRank; } Bool getReadyTimers() const { return m_readyTimers; } - const AsciiString& getSpawnUnit() const { return m_spawnUnit; } - Bool getToggleUnitRequirements() const { return m_toggleUnitRequirements; } + const AsciiString& getSpawnObjectAtCursor() const { return m_spawnObjectAtCursor; } + Bool getTogglePrerequisites() const { return m_togglePrerequisites; } Bool getToggleInfiniteEnergy() const { return m_toggleInfiniteEnergy; } Bool getGrantAllUpgrades() const { return m_grantAllUpgrades; } - Int getPromoteUnit() const { return m_promoteUnit; } - Int getGiveSalvage() const { return m_giveSalvage; } + Int getAddVeterancyLevel() const { return m_addVeterancyLevel; } + Int getAddSalvageTier() const { return m_addSalvageTier; } + Real getProductionSpeedMultiplier() const { return m_productionSpeedMultiplier; } /** Run this command's effects. Inspects the parsed members and acts accordingly. */ void execute() const; private: AsciiString m_name; - Int m_money = 0; ///< "Money" attribute; signed amount, defaults to 0. - UnsignedInt m_rank = 0; ///< "Rank" attribute; ranks to grant, capped at the max rank. Defaults to 0. + Int m_addMoney = 0; ///< "AddMoney" attribute; signed amount, defaults to 0. + UnsignedInt m_addRank = 0; ///< "AddRank" attribute; ranks to grant, capped at the max rank. Defaults to 0. Bool m_readyTimers = FALSE; ///< "ReadyTimers" attribute; when TRUE, set all of the player's special power timers to ready. - AsciiString m_spawnUnit; ///< "SpawnUnit" attribute; ObjectTemplate name to spawn for the local player at the mouse cursor. - Bool m_toggleUnitRequirements = FALSE; ///< "ToggleUnitRequirements" attribute; when TRUE, toggles ignoring unit/building build prereqs (science still applies). + AsciiString m_spawnObjectAtCursor; ///< "SpawnObjectAtCursor" attribute; ObjectTemplate name to spawn for the local player at the mouse cursor. + Bool m_togglePrerequisites = FALSE; ///< "TogglePrerequisites" attribute; when TRUE, toggles ignoring unit/building build prereqs (science still applies). Bool m_toggleInfiniteEnergy = FALSE; ///< "ToggleInfiniteEnergy" attribute; when TRUE, toggles infinite power for the local player. Bool m_grantAllUpgrades = FALSE; ///< "GrantAllUpgrades" attribute; when TRUE, grants the local player all player-type upgrades. - Int m_promoteUnit = 0; ///< "PromoteUnit" attribute; promote selected units by this many veterancy levels (negative demotes), capped to the valid range. - Int m_giveSalvage = 0; ///< "GiveSalvage" attribute; change selected salvagers' crate-upgrade tier by this much (negative removes), capped 0..2. + Int m_addVeterancyLevel = 0; ///< "AddVeterancyLevel" attribute; promote selected units by this many veterancy levels (negative demotes), capped to the valid range. + Int m_addSalvageTier = 0; ///< "AddSalvageTier" attribute; change selected salvagers' crate-upgrade tier by this much (negative removes), capped 0..2. + Real m_productionSpeedMultiplier = 0.0f; ///< "ProductionSpeedMultiplier" attribute; build-speed multiplier for the local player (>1 builds faster). 0 means the field was absent (no change). static const FieldParse s_fieldParseTable[]; }; diff --git a/GeneralsMD/Code/GameEngine/Include/Common/Player.h b/GeneralsMD/Code/GameEngine/Include/Common/Player.h index c714c6c0748..2ffc06194a1 100644 --- a/GeneralsMD/Code/GameEngine/Include/Common/Player.h +++ b/GeneralsMD/Code/GameEngine/Include/Common/Player.h @@ -414,6 +414,10 @@ class Player : public Snapshot /// Returns production cost change based on typeof (Used for upgrades) Real getProductionTimeChangeBasedOnKindOf(KindOfMaskType kindOf) const; + /// Build-speed multiplier applied to units, upgrades and buildings. >1 builds faster. Set via the ProductionSpeedMultiplier chat command. + Real getProductionSpeedMultiplier() const { return m_productionSpeedMultiplier; } + void setProductionSpeedMultiplier( Real m ) { m_productionSpeedMultiplier = m; } + /** Return bonus or penalty for construction of this thing. */ @@ -862,6 +866,9 @@ class Player : public Snapshot // Production Time modifier (we can re-use the same types) mutable KindOfPercentProductionChangeList m_kindOfPercentProductionTimeChangeList; + // Global build-speed multiplier for this player (>1 = faster). Defaults to 1.0 (no change). + Real m_productionSpeedMultiplier; + typedef std::list SpecialPowerReadyTimerList; typedef SpecialPowerReadyTimerList::iterator SpecialPowerReadyTimerListIterator; diff --git a/GeneralsMD/Code/GameEngine/Source/Common/ChatCommand.cpp b/GeneralsMD/Code/GameEngine/Source/Common/ChatCommand.cpp index 9fcfc07f109..73d825dd9ad 100644 --- a/GeneralsMD/Code/GameEngine/Source/Common/ChatCommand.cpp +++ b/GeneralsMD/Code/GameEngine/Source/Common/ChatCommand.cpp @@ -54,15 +54,16 @@ ChatCommandStore* TheChatCommandStore = nullptr; const FieldParse ChatCommand::s_fieldParseTable[] = { - { "Money", INI::parseInt, nullptr, offsetof( ChatCommand, m_money ) }, - { "Rank", INI::parseUnsignedInt, nullptr, offsetof( ChatCommand, m_rank ) }, + { "AddMoney", INI::parseInt, nullptr, offsetof( ChatCommand, m_addMoney ) }, + { "AddRank", INI::parseUnsignedInt, nullptr, offsetof( ChatCommand, m_addRank ) }, { "ReadyTimers", INI::parseBool, nullptr, offsetof( ChatCommand, m_readyTimers ) }, - { "SpawnUnit", INI::parseAsciiString, nullptr, offsetof( ChatCommand, m_spawnUnit ) }, - { "ToggleUnitRequirements", INI::parseBool, nullptr, offsetof( ChatCommand, m_toggleUnitRequirements ) }, + { "SpawnObjectAtCursor", INI::parseAsciiString, nullptr, offsetof( ChatCommand, m_spawnObjectAtCursor ) }, + { "TogglePrerequisites", INI::parseBool, nullptr, offsetof( ChatCommand, m_togglePrerequisites ) }, { "ToggleInfiniteEnergy", INI::parseBool, nullptr, offsetof( ChatCommand, m_toggleInfiniteEnergy ) }, { "GrantAllUpgrades", INI::parseBool, nullptr, offsetof( ChatCommand, m_grantAllUpgrades ) }, - { "PromoteUnit", INI::parseInt, nullptr, offsetof( ChatCommand, m_promoteUnit ) }, - { "GiveSalvage", INI::parseInt, nullptr, offsetof( ChatCommand, m_giveSalvage ) }, + { "AddVeterancyLevel", INI::parseInt, nullptr, offsetof( ChatCommand, m_addVeterancyLevel ) }, + { "AddSalvageTier", INI::parseInt, nullptr, offsetof( ChatCommand, m_addSalvageTier ) }, + { "ProductionSpeedMultiplier", INI::parseReal, nullptr, offsetof( ChatCommand, m_productionSpeedMultiplier ) }, { NULL, NULL, 0, 0 } // keep this last }; @@ -103,17 +104,17 @@ void ChatCommand::execute() const { // Money: positive grants cash, negative removes it. The local player's funds can never // go below zero (withdraw caps at the current balance), so a large removal just empties it. - if (m_money != 0) + if (m_addMoney != 0) { Player *player = ThePlayerList ? ThePlayerList->getLocalPlayer() : nullptr; Money *money = player ? player->getMoney() : nullptr; if (money) { // Suppress the built-in deposit/withdraw sound; play the cash-hack sound instead (below). - if (m_money > 0) - money->deposit( (UnsignedInt)m_money, FALSE ); + if (m_addMoney > 0) + money->deposit( (UnsignedInt)m_addMoney, FALSE ); else - money->withdraw( (UnsignedInt)(-m_money), FALSE ); + money->withdraw( (UnsignedInt)(-m_addMoney), FALSE ); // Play the same sound the Cash Hack special power uses, for the local player. if (TheAudio && TheAudio->getMiscAudio()) @@ -127,11 +128,11 @@ void ChatCommand::execute() const // Rank: grant additional rank levels. setRankLevel() clamps to the maximum rank, // so over-granting just tops the player out. - if (m_rank > 0) + if (m_addRank > 0) { Player *player = ThePlayerList ? ThePlayerList->getLocalPlayer() : nullptr; if (player) - player->setRankLevel( player->getRankLevel() + (Int)m_rank ); + player->setRankLevel( player->getRankLevel() + (Int)m_addRank ); } // ReadyTimers: set every special power timer the player owns to ready (available now). @@ -175,11 +176,11 @@ void ChatCommand::execute() const } } - // SpawnUnit: create one instance of the named ObjectTemplate for the local player, + // SpawnObjectAtCursor: create one instance of the named ObjectTemplate for the local player, // placed on the terrain under the mouse cursor. - if (!m_spawnUnit.isEmpty()) + if (!m_spawnObjectAtCursor.isEmpty()) { - const ThingTemplate *tmpl = TheThingFactory ? TheThingFactory->findTemplate( m_spawnUnit ) : nullptr; + const ThingTemplate *tmpl = TheThingFactory ? TheThingFactory->findTemplate( m_spawnObjectAtCursor ) : nullptr; Player *player = ThePlayerList ? ThePlayerList->getLocalPlayer() : nullptr; Team *team = player ? player->getDefaultTeam() : nullptr; if (tmpl && team && TheMouse && TheTacticalView) @@ -197,13 +198,13 @@ void ChatCommand::execute() const } else if (!tmpl) { - DEBUG_LOG((">>> CHAT COMMAND SpawnUnit: ThingTemplate '%s' not found.", m_spawnUnit.str())); + DEBUG_LOG((">>> CHAT COMMAND SpawnObjectAtCursor: ThingTemplate '%s' not found.", m_spawnObjectAtCursor.str())); } } - // ToggleUnitRequirements: flip ignoring of unit/building build prereqs for the local player + // TogglePrerequisites: flip ignoring of unit/building build prereqs for the local player // (science prereqs still apply). Mark the control bar dirty so build buttons re-evaluate. - if (m_toggleUnitRequirements) + if (m_togglePrerequisites) { Player *player = ThePlayerList ? ThePlayerList->getLocalPlayer() : nullptr; if (player) @@ -245,9 +246,9 @@ void ChatCommand::execute() const } } - // PromoteUnit: change veterancy of the player's currently selected (trainable) units by - // m_promoteUnit levels (negative demotes), clamped to the valid range. - if (m_promoteUnit != 0 && TheInGameUI) + // AddVeterancyLevel: change veterancy of the player's currently selected (trainable) units by + // m_addVeterancyLevel levels (negative demotes), clamped to the valid range. + if (m_addVeterancyLevel != 0 && TheInGameUI) { const DrawableList *selected = TheInGameUI->getAllSelectedLocalDrawables(); if (selected) @@ -260,7 +261,7 @@ void ChatCommand::execute() const if (!exp || !exp->isTrainable()) continue; - Int newLevel = (Int)exp->getVeterancyLevel() + m_promoteUnit; + Int newLevel = (Int)exp->getVeterancyLevel() + m_addVeterancyLevel; if (newLevel < LEVEL_FIRST) newLevel = LEVEL_FIRST; else if (newLevel > LEVEL_LAST) @@ -271,10 +272,10 @@ void ChatCommand::execute() const } } - // GiveSalvage: change the crate-upgrade tier of the player's selected salvagers by - // m_giveSalvage steps (negative removes), clamped 0..2. Weapon tier only applies to + // AddSalvageTier: change the crate-upgrade tier of the player's selected salvagers by + // m_addSalvageTier steps (negative removes), clamped 0..2. Weapon tier only applies to // weapon-salvagers, armor tier only to armor-salvagers; other units are unaffected. - if (m_giveSalvage != 0 && TheInGameUI) + if (m_addSalvageTier != 0 && TheInGameUI) { const DrawableList *selected = TheInGameUI->getAllSelectedLocalDrawables(); if (selected) @@ -292,7 +293,7 @@ void ChatCommand::execute() const { Int cur = obj->testWeaponSetFlag( WEAPONSET_CRATEUPGRADE_TWO ) ? 2 : obj->testWeaponSetFlag( WEAPONSET_CRATEUPGRADE_ONE ) ? 1 : 0; - Int next = cur + m_giveSalvage; + Int next = cur + m_addSalvageTier; next = next < 0 ? 0 : (next > 2 ? 2 : next); if (next != cur) { @@ -305,7 +306,7 @@ void ChatCommand::execute() const { Int cur = obj->testArmorSetFlag( ARMORSET_CRATE_UPGRADE_TWO ) ? 2 : obj->testArmorSetFlag( ARMORSET_CRATE_UPGRADE_ONE ) ? 1 : 0; - Int next = cur + m_giveSalvage; + Int next = cur + m_addSalvageTier; next = next < 0 ? 0 : (next > 2 ? 2 : next); if (next != cur) { @@ -325,6 +326,20 @@ void ChatCommand::execute() const } } + // ProductionSpeedMultiplier: set the local player's global build-speed multiplier (>1 builds faster). + // A value of 0 means the attribute was not present in the INI, so leave the multiplier untouched. + // Mark the control bar dirty so any build-time UI re-evaluates. + if (m_productionSpeedMultiplier > 0.0f) + { + Player *player = ThePlayerList ? ThePlayerList->getLocalPlayer() : nullptr; + if (player) + { + player->setProductionSpeedMultiplier( m_productionSpeedMultiplier ); + if (TheControlBar) + TheControlBar->markUIDirty(); + } + } + // Notify the player that the command ran. if (TheInGameUI) { diff --git a/GeneralsMD/Code/GameEngine/Source/Common/RTS/Player.cpp b/GeneralsMD/Code/GameEngine/Source/Common/RTS/Player.cpp index 1a6468153fc..17e9e40ec58 100644 --- a/GeneralsMD/Code/GameEngine/Source/Common/RTS/Player.cpp +++ b/GeneralsMD/Code/GameEngine/Source/Common/RTS/Player.cpp @@ -505,6 +505,8 @@ void Player::init(const PlayerTemplate* pt) deleteInstance(tof); } + m_productionSpeedMultiplier = 1.0f; + getAcademyStats()->init( this ); //Always off at the beginning of a game! Only GameLogic::update has @@ -4358,7 +4360,7 @@ void Player::xfer( Xfer *xfer ) { // version - const XferVersion currentVersion = 8; + const XferVersion currentVersion = 9; XferVersion version = currentVersion; xfer->xferVersion( &version, currentVersion ); @@ -5023,6 +5025,12 @@ void Player::xfer( Xfer *xfer ) } //------------------------ + // production speed multiplier (build-time scale for units/upgrades/buildings) + if (version >= 9) + xfer->xferReal(&m_productionSpeedMultiplier); + else + m_productionSpeedMultiplier = 1.0f; + } // end xfer diff --git a/GeneralsMD/Code/GameEngine/Source/Common/System/Upgrade.cpp b/GeneralsMD/Code/GameEngine/Source/Common/System/Upgrade.cpp index c65466e7c95..e5733ad66db 100644 --- a/GeneralsMD/Code/GameEngine/Source/Common/System/Upgrade.cpp +++ b/GeneralsMD/Code/GameEngine/Source/Common/System/Upgrade.cpp @@ -163,7 +163,14 @@ Int UpgradeTemplate::calcTimeToBuild( Player *player ) const #endif ///@todo modify this by power state of player - return m_buildTime * LOGICFRAMES_PER_SECOND; + Int buildTime = m_buildTime * LOGICFRAMES_PER_SECOND; + + // global per-player build-speed multiplier (ProductionSpeedMultiplier chat command); >1 researches faster + Real speedMultiplier = player->getProductionSpeedMultiplier(); + if (speedMultiplier > 0.0f) + buildTime = REAL_TO_INT_CEIL( buildTime / speedMultiplier ); + + return buildTime; } diff --git a/GeneralsMD/Code/GameEngine/Source/Common/Thing/ThingTemplate.cpp b/GeneralsMD/Code/GameEngine/Source/Common/Thing/ThingTemplate.cpp index a148a6aa1d7..38b8660a316 100644 --- a/GeneralsMD/Code/GameEngine/Source/Common/Thing/ThingTemplate.cpp +++ b/GeneralsMD/Code/GameEngine/Source/Common/Thing/ThingTemplate.cpp @@ -1597,6 +1597,11 @@ Int ThingTemplate::calcTimeToBuild( const Player* player) const factionModifier *= player->getProductionTimeChangeBasedOnKindOf(m_kindof); buildTime *= factionModifier; + // global per-player build-speed multiplier (ProductionSpeedMultiplier chat command); >1 builds faster + Real speedMultiplier = player->getProductionSpeedMultiplier(); + if (speedMultiplier > 0.0f) + buildTime /= speedMultiplier; + #if defined(RTS_DEBUG) || defined(_ALLOW_DEBUG_CHEATS_IN_RELEASE) if( player->buildsInstantly() ) {