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..ad07aa74342
--- /dev/null
+++ b/GeneralsMD/Code/GameEngine/Include/Common/ChatCommand.h
@@ -0,0 +1,112 @@
+/*
+** 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 getAddMoney() const { return m_addMoney; }
+ UnsignedInt getAddRank() const { return m_addRank; }
+ Bool getReadyTimers() const { return m_readyTimers; }
+ 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 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_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_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_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[];
+};
+
+//-------------------------------------------------------------------------------------------------
+/** 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/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/Include/Common/Player.h b/GeneralsMD/Code/GameEngine/Include/Common/Player.h
index ef47af80734..2ffc06194a1 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 );
@@ -405,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.
*/
@@ -842,6 +855,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
@@ -851,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/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..73d825dd9ad
--- /dev/null
+++ b/GeneralsMD/Code/GameEngine/Source/Common/ChatCommand.cpp
@@ -0,0 +1,418 @@
+/*
+** 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[] =
+{
+ { "AddMoney", INI::parseInt, nullptr, offsetof( ChatCommand, m_addMoney ) },
+ { "AddRank", INI::parseUnsignedInt, nullptr, offsetof( ChatCommand, m_addRank ) },
+ { "ReadyTimers", INI::parseBool, nullptr, offsetof( ChatCommand, m_readyTimers ) },
+ { "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 ) },
+ { "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
+};
+
+//-------------------------------------------------------------------------------------------------
+// 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_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_addMoney > 0)
+ money->deposit( (UnsignedInt)m_addMoney, FALSE );
+ else
+ money->withdraw( (UnsignedInt)(-m_addMoney), 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_addRank > 0)
+ {
+ Player *player = ThePlayerList ? ThePlayerList->getLocalPlayer() : nullptr;
+ if (player)
+ player->setRankLevel( player->getRankLevel() + (Int)m_addRank );
+ }
+
+ // 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