diff --git a/BeaconClass.h b/BeaconClass.h index d70c173a..d450a387 100644 --- a/BeaconClass.h +++ b/BeaconClass.h @@ -1,18 +1,46 @@ +#pragma once + #include #include class BeaconClass { public: + enum class Flag : byte + { + // Set by BeaconManagerClass::PlaceBeacon once the beacon has been given a house, which + // it is for any house index below 8. VisibleToPlayer tests it first. + Assigned = 0x1, + + // Set by BeaconManagerClass::SelectBeacon on the beacon the local player clicked to type + // a message into, and cleared again when it is deselected. It is the beacon that + // DeleteBeacon and EditBeaconMessage resolve a -1 house / -1 slot to. + Selected = 0x2, + }; + BeaconClass() JMP_THIS(0x430210) void Draw(Surface* pSurface, RectangleStruct bounds) JMP_THIS(0x430250) void SetCoordAndHouse(CoordStruct coord, int houseId) JMP_THIS(0x430590) - // TODO bitfield functions void SetText(const wchar_t* pText) JMP_THIS(0x430620) void DrawRadar(Surface* pSurface, RectangleStruct bounds, bool toClear = false) JMP_THIS(0x430650) bool VisibleToPlayer() const JMP_THIS(0x4308B0) + bool HasFlag(Flag flag) const + { + return (this->Bitfield & static_cast(flag)) != 0; + } + + bool IsAssigned() const + { + return this->HasFlag(Flag::Assigned); + } + + bool IsSelected() const + { + return this->HasFlag(Flag::Selected); + } + CoordStruct Coord; byte Bitfield; byte gapD[1]; diff --git a/BeaconManagerClass.h b/BeaconManagerClass.h index 48986f60..73a80afe 100644 --- a/BeaconManagerClass.h +++ b/BeaconManagerClass.h @@ -1,3 +1,5 @@ +#pragma once + #include #include #include @@ -22,7 +24,7 @@ class __declspec(align(4)) BeaconManagerClass void DeleteBeacon(int houseId, int houseBeaconId) JMP_THIS(0x4311C0) void DeleteAllBeacons(int houseId) JMP_THIS(0x431410) bool SelectBeacon(int X, int Y, int Z) JMP_THIS(0x430F70) - void EditBeaconMessage(wchar_t* message, int houseId, int houseBeaconId, bool unknownBool) JMP_THIS(0x431450) + char EditBeaconMessage(const wchar_t* message, int houseId, int houseBeaconId, bool shouldBroadcast) JMP_THIS(0x431450) // TODO rest of the functions diff --git a/CommBufferClass.h b/CommBufferClass.h new file mode 100644 index 00000000..2c33b15e --- /dev/null +++ b/CommBufferClass.h @@ -0,0 +1,120 @@ +#pragma once + +#include + +struct CommHeaderType; + +// Flag bits shared by both queue entry types. Westwood declared these as +// single-bit bitfields; the game reads and writes them as a plain int. +enum CommQueueFlags +{ + COMMQUEUE_IS_ACTIVE = 0x1, // this entry holds a packet and is ready to be processed + COMMQUEUE_IS_READ = 0x2, // send queue: ACK received. receive queue: caller has read it + COMMQUEUE_IS_ACK = 0x4, // an ACK has been sent for this packet +}; + +// One outgoing queue entry. +struct SendQueueType +{ + int Flags; + int FirstTime; // time this packet was first sent + int LastTime; // time this packet was last sent + int SendCount; // number of times this packet has been sent + int BufLen; // size of the packet stored in this entry + CommHeaderType* Buffer; + int ExtraLen; // size of the extra data (an IPXAddressClass, for global conns) + void* ExtraBuffer; + short Port; // destination port this entry was queued for +}; +static_assert(sizeof(SendQueueType) == 0x24); + +// One incoming queue entry. Unlike RA's, YR's tracks the time the packet was +// received so IPXGlobalConnClass::Strip_Packets can age entries out. +struct ReceiveQueueType +{ + int Flags; + int Time; + int BufLen; + void* Buffer; + int ExtraLen; + void* ExtraBuffer; +}; +static_assert(sizeof(ReceiveQueueType) == 0x18); + +// Stores the packets sent and received by a single ConnectionClass. Entries +// are addressed through an index array rather than moved, so unqueueing an +// entry does not disturb the others. +class NOVTABLE CommBufferClass +{ +public: + virtual ~CommBufferClass() RX; + + // Clears both queues. Init_Send_Queue clears only the send queue, which + // is what a reconnect needs. + void Init() + { JMP_THIS(0x48B2A0); } + void Init_Send_Queue() + { JMP_THIS(0x48B390); } + + // Send queue. Queue_Send returns zero when the queue is full. + int Queue_Send(void* buf, int buflen, void* extrabuf, int extralen, short port) + { JMP_THIS(0x48B410); } + int UnQueue_Send(void* buf, int* buflen, int index, void* extrabuf, int* extralen, short port) + { JMP_THIS(0x48B570); } + SendQueueType* Get_Send(int index) + { JMP_THIS(0x48B720); } + int Num_Send() const + { return this->SendCount; } + int Max_Send() const + { return this->MaxSend; } + + // Receive queue. + int Queue_Receive(void* buf, int buflen, void* extrabuf, int extralen) + { JMP_THIS(0x48B750); } + int UnQueue_Receive(void* buf, int* buflen, int index, void* extrabuf, int* extralen) + { JMP_THIS(0x48B890); } + ReceiveQueueType* Get_Receive(int index) + { JMP_THIS(0x48B9E0); } + int Num_Receive() const + { return this->ReceiveCount; } + int Max_Receive() const + { return this->MaxReceive; } + + // Response time tracking. The caller feeds in a delay whenever it detects + // an outgoing message has been ACK'd; the class keeps a running mean. + void Add_Delay(unsigned int delay) + { JMP_THIS(0x48BA10); } + unsigned int Avg_Response_Time() + { JMP_THIS(0x48BA80); } + unsigned int Max_Response_Time() + { JMP_THIS(0x48BA90); } + + // Properties +public: + int MaxSend; + int MaxReceive; + int MaxPacketSize; + int MaxExtraSize; + + unsigned int DelaySum; + unsigned int NumDelay; + unsigned int MeanDelay; + unsigned int MaxDelay; + + SendQueueType* SendQueue; + int SendCount; // number of entries currently queued + unsigned int SendTotal; // total ever added, used as the outgoing packet ID + int* SendIndex; + + ReceiveQueueType* ReceiveQueue; + int ReceiveCount; + unsigned int ReceiveTotal; + int* ReceiveIndex; + + int DebugOffset; + int DebugSize; + char** DebugNames; + int DebugNameStart; + int DebugNameCount; +}; +static_assert(sizeof(CommBufferClass) == 0x58); diff --git a/ConnectionClass.h b/ConnectionClass.h new file mode 100644 index 00000000..b4ca0e61 --- /dev/null +++ b/ConnectionClass.h @@ -0,0 +1,113 @@ +#pragma once + +#include +#include + +// A single "connection" with another system. This is a pure virtual base +// class; a derived class supplies Init (protocol-specific setup) and Send +// (the actual hardware-dependent transmit). +// +// Every packet the application sends is prefixed with a CommHeaderType. The +// header carries a magic number (unique per product, so foreign traffic can +// be rejected), a code saying whether this is data or an ACK, and a packet ID +// used to detect resends and to hand packets to the application in order. +// +// Service() drives the ACK/retry logic and must be polled as often as +// possible. Because a derived class can override Service_Send_Queue and +// Service_Receive_Queue, a protocol that already guarantees delivery can skip +// ACKing entirely. + +// Values for CommHeaderType::Code. +enum class ConnectionEnum : unsigned char +{ + PACKET_DATA_ACK = 0, // a data packet requiring an ACK + PACKET_DATA_NOACK = 1, // a data packet not requiring an ACK + PACKET_ACK = 2, // an ACK for a packet + PACKET_COUNT = 3, // for computational purposes +}; + +// The header prefixed to every packet the connection sends. YR widened RA's +// 7-byte header to 14 bytes; `ForwardTo` holds the packet router forwarding +// mask and is only filled in for internet games routed via the packet router. +#pragma pack(push, 1) +struct CommHeaderType +{ + short MagicNumber; + ConnectionEnum Code; + char ForwardTo; + int PacketID; + int field_8; + short field_C; +}; +#pragma pack(pop) +static_assert(sizeof(CommHeaderType) == 14); + +class NOVTABLE ConnectionClass +{ +public: + virtual ~ConnectionClass() RX; + + virtual void Init() + { JMP_THIS(0x48BF10); } + + // Queues a packet for sending. `forwardto` is only written into the + // header when the game is an internet game running via the packet router. + virtual int Send_Packet(void* buf, int buflen, int ack_req, char forwardto) + { JMP_THIS(0x48BF40); } + + // Tells the connection a packet has arrived; the connection manager calls + // this after parsing an incoming datagram. + virtual int Receive_Packet(CommHeaderType* buf, int buflen) + { JMP_THIS(0x48C040); } + + // Pulls the next application packet out of the receive queue, stripping + // the CommHeaderType. Returns zero when nothing is ready. + virtual int Get_Packet(void* buf, int* buflen) + { JMP_THIS(0x48C320); } + + // The main polling routine. Should be called as often as possible. + virtual int Service() + { JMP_THIS(0x48C3B0); } + + // Returns the current time in 60ths of a second, which is the unit the + // retry logic works in. + static unsigned int Time() + { JMP_STD(0x48C600); } + +protected: + // Drops send queue entries that have been ACK'd. The base implementation + // is a stub returning zero; IPXGlobalConnClass overrides it. + virtual int Purge_Send_Queue() + { JMP_THIS(0x48C590); } + + virtual int Service_Send_Queue() + { JMP_THIS(0x48C3E0); } + virtual int Service_Receive_Queue() + { JMP_THIS(0x48C5A0); } + + // Performs the hardware-dependent send. Pure virtual, and protected + // because only the ACK/retry logic calls it, never the application. + virtual int Send(CommHeaderType* buf, int buflen, void* extrabuf, int extralen, bool isGlobalConn, short port) = 0; + + // Properties +public: + CommBufferClass* Queue; + int NumResends; + int NumLost; + int PercentLost; + int MissedOverall; + int MissedMagic; + unsigned int MaxPacketLen; // includes the CommHeaderType + CommHeaderType* PacketBuf; + unsigned short MagicNum; + unsigned int RetryDelta; // delay before a packet is re-sent + unsigned int MaxRetries; + unsigned int Timeout; + int NumRecNoAck; + int NumRecAck; + int NumSendNoAck; + int NumSendAck; + int LastSeqID; // ID of the last consecutively-received packet + int LastReadID; // ID of the last PACKET_DATA_ACK packet read +}; +static_assert(sizeof(ConnectionClass) == 0x4C); diff --git a/IPX.h b/IPX.h index 0fca4eb5..7d8c3f4c 100644 --- a/IPX.h +++ b/IPX.h @@ -1,7 +1,28 @@ #pragma once +// The four-byte network number and six-byte node (MAC) address that together +// identify a machine on an IPX network. Westwood declared these as raw array +// typedefs; they are wrapped in structs here so they can be used as members +// without decaying to pointers. +struct NetNumType +{ + unsigned char Value[4]; +}; +static_assert(sizeof(NetNumType) == 4); + +struct NetNodeType +{ + unsigned char Value[6]; +}; +static_assert(sizeof(NetNodeType) == 6); + class IPXAddressClass { +public: unsigned char NetworkNumber[4]; unsigned char NodeAddress[6]; + // YR carries UDP/IP endpoints in this struct as well as real IPX + // addresses, so the trailing two bytes are only meaningful in IP mode. + unsigned char field_A[2]; }; +static_assert(sizeof(IPXAddressClass) == 12); diff --git a/IPXConnClass.h b/IPXConnClass.h new file mode 100644 index 00000000..4a48745c --- /dev/null +++ b/IPXConnClass.h @@ -0,0 +1,51 @@ +#pragma once + +#include +#include +#include + +class NOVTABLE IPXConnClass : public ConnectionClass +{ +public: + enum IPXConnTag + { + CONN_NAME_MAX = 40 + }; + + DEFINE_REFERENCE(unsigned short, Socket, 0xAA0568) + DEFINE_REFERENCE(int, Configured, 0xAA05A4) + DEFINE_REFERENCE(int, SocketOpen, 0xAA05A8) + DEFINE_REFERENCE(int, Listening, 0xAA05AC) + + virtual void Init() override + { JMP_THIS(0x53F4E0); } + + static int __fastcall Open_Socket(unsigned short socket) + { JMP_THIS(0x53F5F0); } + static void __fastcall Close_Socket(unsigned short socket) + { JMP_THIS(0x53F630); } + + static int Start_Listening() + { JMP_STD(0x53F540); } + static int Stop_Listening() + { JMP_STD(0x53F5B0); } + + static int __fastcall Broadcast(void* buf, int buflen) + { JMP_THIS(0x53F830); } + +protected: + virtual int Send(CommHeaderType* buf, int buflen, void* extrabuf, int extralen, bool isGlobalConn, short port) override + { JMP_THIS(0x53F5D0); } + + virtual int Send_To_Address(CommHeaderType* buf, int buflen, IPXAddressClass* address, NetNodeType* nodeOverride, bool isGlobalConn, short port) + { JMP_THIS(0x53F650); } + +public: + IPXAddressClass Address; + NetNodeType ImmediateAddress; + + int Immed_Set; + int ID; + wchar_t Name[CONN_NAME_MAX]; +}; +static_assert(sizeof(IPXConnClass) == 0xB8); diff --git a/IPXGlobalConnClass.h b/IPXGlobalConnClass.h new file mode 100644 index 00000000..8a56581d --- /dev/null +++ b/IPXGlobalConnClass.h @@ -0,0 +1,72 @@ +#pragma once + +#include +#include + +class NOVTABLE IPXGlobalConnClass : public IPXConnClass +{ +public: + enum GlobalConnectionEnum + { + GLOBAL_MAGICNUM = 0x1235, + COMMAND_AND_CONQUER4 = 0xaa04, /// YR + COMMAND_AND_CONQUER3 = 0xaa03, /// RA2 + COMMAND_AND_CONQUER2 = 0xaa02, /// TS + COMMAND_AND_CONQUER1 = 0xaa01, + COMMAND_AND_CONQUER0 = 0xaa00 + }; + + virtual int Send_Packet(void* buf, int buflen, int ack_req, char forwardto) override + { JMP_THIS(0x540610); } + virtual int Receive_Packet(CommHeaderType* buf, int buflen) override + { JMP_THIS(0x540630); } + virtual int Get_Packet(void* buf, int* buflen) override + { JMP_THIS(0x540650); } + + virtual int Send_Packet(void* buf, int buflen, IPXAddressClass* address, int ack_req, short port, int packet_id) + { JMP_THIS(0x53FBD0); } + + virtual int Receive_Packet(void* buf, int buflen, IPXAddressClass* address, short port) + { JMP_THIS(0x53FCB0); } + + virtual int Get_Packet(void* buf, int* buflen, IPXAddressClass* address, unsigned short* product_id) + { JMP_THIS(0x53FF10); } + + virtual int Peek_Packet(int index, void* buf, int* buflen, IPXAddressClass* address, unsigned short* product_id, int* packet_id) + { JMP_THIS(0x53FFA0); } + + virtual int Mark_Packet_Read(int index) + { JMP_THIS(0x540030); } + + virtual int Purge_Send_Queue_To(IPXAddressClass* address, short port) + { JMP_THIS(0x540340); } + + virtual void Strip_Packets(unsigned int age) + { JMP_THIS(0x540110); } + + void Set_Bridge(NetNumType* bridge) + { JMP_THIS(0x5402B0); } + +protected: + virtual int Purge_Send_Queue() override + { JMP_THIS(0x5402D0); } + virtual int Service_Receive_Queue() override + { JMP_THIS(0x5400D0); } + virtual int Send(CommHeaderType* buf, int buflen, void* extrabuf, int extralen, bool isGlobalConn, short port) override + { JMP_THIS(0x540050); } + virtual int Send_To_Address(CommHeaderType* buf, int buflen, IPXAddressClass* address, NetNodeType* nodeOverride, bool isGlobalConn, short port) override + { JMP_THIS(0x5403F0); } + +public: + unsigned short ProductID; + + NetNumType BridgeNet; + NetNodeType BridgeNode; + int IsBridge; + bool field_C8; + IPXAddressClass* LastAddress; + int* LastPacketID; + int LastCount; + int LastRXIndex; +}; +static_assert(sizeof(IPXGlobalConnClass) == 0xDC); diff --git a/TacticalClass.h b/TacticalClass.h index c445b6aa..0a3d3b61 100644 --- a/TacticalClass.h +++ b/TacticalClass.h @@ -96,6 +96,10 @@ class NOVTABLE TacticalClass : public AbstractClass void FocusOn(CoordStruct* pDest, int Velocity) JMP_THIS(0x6D2420); + // Handles per-frame tactical view housekeeping and commits the desired viewport. + void AI() + JMP_THIS(0x6D2540); + // called when area needs to be marked for redrawing due to external factors // - alpha lights, terrain changes like cliff destruction, etc @@ -111,6 +115,9 @@ class NOVTABLE TacticalClass : public AbstractClass void AddSelectable(TechnoClass* pTechno, int x, int y) JMP_THIS(0x6D9EF0); + void RecalculateViewport() + JMP_THIS(0x6D8B30); + static void StartDrawActionLineTimer() JMP_STD(0x70D150); diff --git a/TheirSync.h b/TheirSync.h new file mode 100644 index 00000000..341005c1 --- /dev/null +++ b/TheirSync.h @@ -0,0 +1,16 @@ +#pragma once + +#include + +struct TheirSync +{ + DEFINE_ARRAY_REFERENCE(TheirSync, [7], Array, 0xAFA358) + + int Frame; + int CommandsSent; + int CommandsReceived; + int ResponseTime; + int RouterResponseTime; + int LastHeardTime; +}; +static_assert(sizeof(TheirSync) == 0x18); diff --git a/Timer.h b/Timer.h index f7cea73e..bbd61f0e 100644 --- a/Timer.h +++ b/Timer.h @@ -1,4 +1,8 @@ #pragma once + +#include +#include + template concept TimerType = std::convertible_to && requires (T t) { @@ -19,6 +23,15 @@ struct SystemTimer operator long() const { return SystemTimer::GetTime(); } }; +// timeGetTime() straight, where SystemTimer shifts it down by four - so this one counts in whole +// milliseconds and SystemTimer in sixteenths of one. The engine builds NetFrameTimer on it. +struct MSTimer +{ + static DWORD GetTime() JMP_STD(0x5D5890); + long operator()()const { return MSTimer::GetTime(); } + operator long() const { return MSTimer::GetTime(); } +}; + template struct TimerStruct { @@ -116,9 +129,23 @@ struct TimerStruct // Timer that counts down from specified value towards zero, counted in frames. using CDTimerClass = TimerStruct; using SysTimerClass = TimerStruct; +using MSTimerClass = TimerStruct; + +namespace GameTimers +{ + // The two timers a frame is waited out on in Main_Loop and Sync_Delay. Which one is used + // depends on the session: FrameTimer for a local game, NetFrameTimer for a networked one. + DEFINE_REFERENCE(SysTimerClass, FrameTimer, 0x887348u) + DEFINE_REFERENCE(MSTimerClass, NetFrameTimer, 0x887328u) + + // A static local of Queue_AI_Multiplayer, started from the frame the game began on. While it + // still has time left the per-frame sync CRCs are gathered but not compared. + DEFINE_REFERENCE(CDTimerClass, QueueAIMultiplayerSkipCRC, 0xAFA450u) +} static_assert(offsetof(CDTimerClass, TimeLeft) == 0x8); static_assert(sizeof(SysTimerClass) == 0xC); +static_assert(sizeof(MSTimerClass) == 0xC); // Timer that counts down towards zero at specified rate, counted in frames. class RateTimer : public CDTimerClass diff --git a/VoxClass.h b/VoxClass.h index a7697960..eac160c1 100644 --- a/VoxClass.h +++ b/VoxClass.h @@ -17,6 +17,21 @@ class VoxClass DEFINE_REFERENCE(int, EVAIndex, 0xB1D4C8u) + // A taunt command packs a country in the high nibble and the taunt in the low one: + // (country << 4) | taunt. PlayTaunt silently does nothing for anything outside these ranges. + static constexpr int TauntsPerCountry = 8; // taunt indices 1 - 8; 0 is not a taunt + static constexpr int TauntCountryCount = 10; // countries 0 - 9, in Rules' [Countries] order + + static constexpr bool IsValidTauntCommand(int command) + { + const int taunt = command & 0xF; + const int country = (command >> 4) & 0xF; + + return command == ((country << 4) | taunt) + && taunt >= 1 && taunt <= TauntsPerCountry + && country < TauntCountryCount; + } + static VoxClass* Find(const char* pName) { for(int i = 0; i < Array.Count; ++i) { @@ -50,6 +65,11 @@ class VoxClass static void __fastcall SilenceIndex(int index) { JMP_STD(0x752A40); } + // Plays one of the taunt streams, keyed by a packed country/taunt command - see + // IsValidTauntCommand. Returns 0 when there is nothing to play. + static int __fastcall PlayTaunt(int command) + { JMP_STD(0x752B70); } + static const char* GetName(int index) { JMP_STD(0x753330); } diff --git a/YRpp.props b/YRpp.props index 37e1286b..80bbcff3 100644 --- a/YRpp.props +++ b/YRpp.props @@ -163,6 +163,8 @@ + + @@ -214,6 +216,8 @@ + + @@ -308,6 +312,7 @@ +