From e276749c790d35c8b7a79a3509f35d7cc7b0efe7 Mon Sep 17 00:00:00 2001
From: VuceticBranislav <24853106+VuceticBranislav@users.noreply.github.com>
Date: Sat, 3 Feb 2024 12:43:47 +0100
Subject: [PATCH] feat(Era): Update Era source code to 3.9.10. Commit: 3c7f910
Signed-off-by: VuceticBranislav <24853106+VuceticBranislav@users.noreply.github.com>
---
Era/Heroes.pas | 20 +++++++++++++-
Era/Tweaks.pas | 75 ++++++++++++++++++++++++++++++++++++++++++++------
README.md | 2 +-
3 files changed, 86 insertions(+), 11 deletions(-)
diff --git a/Era/Heroes.pas b/Era/Heroes.pas
index ca2c461..7ea58a0 100644
--- a/Era/Heroes.pas
+++ b/Era/Heroes.pas
@@ -1057,6 +1057,14 @@ TTextTable = class
IsTwoLevelMap: boolean;
end; // .record TGameManager
+ PBattleStack = ^TBattleStack;
+ TBattleStack = packed record
+ Unk1: array [0..$34 - 1] of byte;
+ MonType: integer; // +0x34
+ Pos: integer; // +0x38
+ Unk2: array [$3C..$548 - 1] of byte;
+ end; // .record TBattleStack
+
PPCombatManager = ^PCombatManager;
PCombatManager = ^TCombatManager;
TCombatManager = packed record
@@ -1099,7 +1107,9 @@ TTextTable = class
// _Dlg_* dlg; // + 0x132FC
// _byte_ field_13300[3564];
- end; // .TCombatManager
+
+ function GetActiveStack: PBattleStack;
+ end; // .record TCombatManager
PPAdvManager = ^PAdvManager;
PAdvManager = ^TAdvManager;
@@ -1363,6 +1373,14 @@ procedure TNetData.Send (aDestPlayerId: integer);
PatchApi.Call(FASTCALL_, Ptr($5549E0), [@Self, aDestPlayerId, 0, 1]);
end;
+function TCombatManager.GetActiveStack: PBattleStack;
+Type
+ TGetActiveStackMethod = function (CombatMgr: PCombatManager): PBattleStack; cdecl;
+
+begin
+ result := TGetActiveStackMethod(Ptr($75AF06))(@Self);
+end;
+
procedure SendNetData (DestPlayerId, MsgId: integer; {n} Data: pointer; DataSize: integer);
var
NetDataBuf: UtilsB2.TArrayOfByte;
diff --git a/Era/Tweaks.pas b/Era/Tweaks.pas
index 13306c4..0926f10 100644
--- a/Era/Tweaks.pas
+++ b/Era/Tweaks.pas
@@ -96,6 +96,7 @@ function RandomRangeWithFreeParam (MinValue, MaxValue, FreeParam: integer): inte
fRangeMax: integer;
fCombatActionId: integer;
fFreeParam: integer;
+ fAttemptParam: integer;
end;
TBattleDeterministicRng = class (FastRand.TRng)
@@ -106,7 +107,7 @@ TBattleDeterministicRng = class (FastRand.TRng)
fCombatActionIdPtr: pinteger;
fFreeParamPtr: pinteger;
- procedure UpdateState (RangeMin, RangeMax: integer);
+ procedure UpdateState (RangeMin, RangeMax: integer; AttemptIndex: integer = 1);
public
constructor Create (CombatIdPtr, CombatRoundPtr, CombatActionIdPtr, FreeParamPtr: pinteger);
@@ -167,7 +168,7 @@ constructor TBattleDeterministicRng.Create (CombatIdPtr, CombatRoundPtr, CombatA
Self.fFreeParamPtr := FreeParamPtr;
end;
-procedure TBattleDeterministicRng.UpdateState (RangeMin, RangeMax: integer);
+procedure TBattleDeterministicRng.UpdateState (RangeMin, RangeMax: integer; AttemptIndex: integer = 1);
begin
Self.fState.fCombatRound := Crypto.Tm32Encode(Self.fCombatRoundPtr^);
Self.fState.fRangeMin := RangeMin;
@@ -175,6 +176,7 @@ procedure TBattleDeterministicRng.UpdateState (RangeMin, RangeMax: integer);
Self.fState.fRangeMax := RangeMax;
Self.fState.fCombatActionId := Crypto.Tm32Encode(Self.fCombatActionIdPtr^ + 1147022261);
Self.fState.fFreeParam := Crypto.Tm32Encode(Self.fFreeParamPtr^ + 641013956);
+ Self.fState.fAttemptParam := 1709573561 + AttemptIndex * 39437491;
end;
procedure TBattleDeterministicRng.Seed (NewSeed: integer);
@@ -189,6 +191,15 @@ function TBattleDeterministicRng.Random: integer;
end;
function TBattleDeterministicRng.RandomRange (MinValue, MaxValue: integer): integer;
+const
+ MAX_UNBIAS_ATTEMPTS = 100;
+
+var
+ RangeLen: cardinal;
+ BiasedRangeLen: cardinal;
+ MaxUnbiasedValue: cardinal;
+ i: integer;
+
begin
if MinValue >= MaxValue then begin
result := MinValue;
@@ -199,7 +210,23 @@ function TBattleDeterministicRng.RandomRange (MinValue, MaxValue: integer): inte
result := Crypto.FastHash(@Self.fState, sizeof(Self.fState));
if (MinValue > Low(integer)) or (MaxValue < High(integer)) then begin
- result := MinValue + integer(cardinal(result) mod cardinal(MaxValue - MinValue + 1));
+ i := 2;
+ RangeLen := cardinal(MaxValue - MinValue) + 1;
+ BiasedRangeLen := High(cardinal) mod RangeLen + 1;
+
+ if BiasedRangeLen = RangeLen then begin
+ BiasedRangeLen := 0;
+ end;
+
+ MaxUnbiasedValue := High(cardinal) - BiasedRangeLen;
+
+ while (cardinal(result) > MaxUnbiasedValue) and (i <= MAX_UNBIAS_ATTEMPTS) do begin
+ Inc(Self.fState.fAttemptParam, 39437491);
+ result := Crypto.FastHash(@Self.fState, sizeof(Self.fState));
+ Inc(i);
+ end;
+
+ result := MinValue + integer(cardinal(result) mod RangeLen);
end;
end;
@@ -860,6 +887,8 @@ function Hook_WoGBeforeBattleAction (Context: Core.PHookContext): longbool; stdc
BattleMgr: Heroes.PCombatManager;
begin
+ Inc(CombatActionId);
+
BattleMgr := Heroes.CombatManagerPtr^;
CombatOrigStackActionInfo.Action := BattleMgr.Action;
@@ -870,6 +899,33 @@ function Hook_WoGBeforeBattleAction (Context: Core.PHookContext): longbool; stdc
result := Core.EXEC_DEF_CODE;
end;
+function Hook_WoGBeforeBattleAction_HandleEnchantress (Context: Core.PHookContext): longbool; stdcall;
+const
+ LOCAL_ACTING_MON_TYPE = -$2C;
+
+var
+ BattleMgr: Heroes.PCombatManager;
+
+begin
+ BattleMgr := Heroes.CombatManagerPtr^;
+ Erm.v[997] := CombatRound;
+ Erm.FireErmEvent(TRIGGER_BG0);
+
+ // Monster type could be changed by script, use the one from combat manager
+ pinteger(Context.EBP + LOCAL_ACTING_MON_TYPE)^ := BattleMgr.GetActiveStack().MonType;
+
+ result := Core.EXEC_DEF_CODE;
+end;
+
+function Hook_WoGCallAfterBattleAction (Context: Core.PHookContext): longbool; stdcall;
+begin
+ Erm.v[997] := CombatRound;
+ Erm.FireErmEvent(TRIGGER_BG1);
+
+ Context.RetAddr := Ptr($75D317);
+ result := not Core.EXEC_DEF_CODE;
+end;
+
function Hook_SendBattleAction_CopyActionParams (Context: Core.PHookContext): longbool; stdcall;
begin
Context.EAX := CombatOrigStackActionInfo.ActionParam2;
@@ -935,11 +991,6 @@ function Hook_OnCombatRound_End (Context: Core.PHookContext): longbool; stdcall;
var
RngId: integer = 0; // Holds random generation attempt ID, auto resets at each reseeding. Used for debugging purposes
-procedure OnBeforeBattleAction (Event: GameExt.PEvent); stdcall;
-begin
- Inc(CombatActionId);
-end;
-
procedure Hook_SRand (OrigFunc: pointer; Seed: integer); stdcall;
var
CallerAddr: pointer;
@@ -2139,6 +2190,13 @@ procedure OnAfterWoG (Event: GameExt.PEvent); stdcall;
ApiJack.HookCode(Ptr($75C69E), @Hook_WoGBeforeBattleAction);
ApiJack.HookCode(Ptr($47883B), @Hook_SendBattleAction_CopyActionParams);
+ (* Trigger OnBeforeBattleAction before Enchantress, Hell Steed and creature experience mass spell processing *)
+ ApiJack.HookCode(Ptr($75C96C), @Hook_WoGBeforeBattleAction_HandleEnchantress);
+ Core.p.WriteDataPatch(Ptr($75CB26), [myAStr('9090909090909090909090')]);
+
+ (* Use CombatRound in OnAfterBattleAction trigger for v997 *)
+ ApiJack.HookCode(Ptr($75D306), @Hook_WoGCallAfterBattleAction);
+
(* Send and receive unique identifier for each battle to use in deterministic PRNG in multiplayer *)
ApiJack.HookCode(Ptr($763796), @Hook_ZvsAdd2Send);
ApiJack.HookCode(Ptr($763BA4), @Hook_ZvsGet4Receive);
@@ -2202,7 +2260,6 @@ procedure OnAfterVfsInit (Event: GameExt.PEvent); stdcall;
EventMan.GetInstance.On('OnAfterVfsInit', OnAfterVfsInit);
EventMan.GetInstance.On('OnAfterWoG', OnAfterWoG);
EventMan.GetInstance.On('OnBattleReplay', OnBattleReplay);
- EventMan.GetInstance.On('OnBeforeBattleAction', OnBeforeBattleAction);
EventMan.GetInstance.On('OnBeforeBattleReplay', OnBeforeBattleReplay);
EventMan.GetInstance.On('OnBeforeBattleUniversal', OnBeforeBattleUniversal);
EventMan.GetInstance.On('OnGenerateDebugInfo', OnGenerateDebugInfo);
diff --git a/README.md b/README.md
index 7d4ac26..375a391 100644
--- a/README.md
+++ b/README.md
@@ -28,7 +28,7 @@
Contains ERA and VFS projects as well as B2 library.
Original commits containing code are from ethernidee repositories:
- ERA: 9ddfbbf
+ ERA: 3c7f910
VFS: 5e4a7f0
B2: 001251e