sync: restore source from Google Drive
Sync changed files to Google Drive / sync (push) Successful in 1h45m13s

This commit is contained in:
admin_d committed 2026-09-12 16:50:30 +05:00
1 parent e2d8247e91
commit 7063d1b1c9
993 files changed
+949976

No files matched your search

+634
View File
@@ -0,0 +1,634 @@
unit KM_FogOfWar;
{$I KaM_Remake.inc}
interface
uses
Math, KM_CommonClasses, KM_CommonTypes, KM_Defaults, KM_Points;
// Fog of War state for each player
type
TKMFogOfWarCommon = class
public
function CheckTileInitialRevelation(const X,Y: Word): Boolean; virtual; abstract;
function CheckVerticeRevelation(const X,Y: Word): Byte; virtual; abstract;
function CheckTileRevelation(const X,Y: Word): Byte; virtual; abstract;
function CheckRevelation(const aPoint: TKMPointF): Byte; virtual; abstract;
function CheckVerticeRenderRev(const X,Y: Word): Byte; virtual; abstract;
function CheckTileRenderRev(const X,Y: Word): Byte; virtual; abstract;
function CheckRenderRev(const aPoint: TKMPointF): Byte; virtual; abstract;
end;
TKMFogOfWar = class(TKMFogOfWarCommon)
private
fAnimStep: Cardinal;
fMapX: Word;
fMapY: Word;
fDynamicFOW: Boolean; //Local copy for faster access, updated in UpdateState
//Initial revealers from static script
fInitialRevealAll: Boolean;
fInitialRevealers: TKMPointTagList;
// Used to optimize RevealCircle
// It doesn't work if a cover function is called
// No need to save/load it, it's just an optimisation
fRevealedRadius: array [0..MAX_MAP_SIZE-1, 0..MAX_MAP_SIZE-1] of Word;
fRenderRevRevealedRad: array [0..MAX_MAP_SIZE-1, 0..MAX_MAP_SIZE-1] of Word;
fCoverHasBeenCalled: Boolean;
fRevealedToMax: TBoolean2Array;
(*Revelation: array of array of packed record
//Lies within range 0, TERRAIN_FOG_OF_WAR_MIN..TERRAIN_FOG_OF_WAR_MAX.
Visibility: Byte;
{LastTerrain: Byte;
LastHeight: Byte;
LastObj: Byte;
LastHouse: TKMHouseType;}
end;*)
procedure SetMapSize(aMapX, aMapY: Word);
function CheckVerticeRev(aRevArray: PKMByte2Array; const X,Y: Word): Byte; inline;
function CheckTileRev(aRevArray: PKMByte2Array; const X,Y: Word): Byte; inline;
function CheckRev(aRevArray: PKMByte2Array; const aPoint: TKMPointF): Byte; inline;
public
Revelation: TKMByte2Array; //Public for faster access from Render
RenderRevelation: TKMByte2Array; //Revelation for render - we have to render sprites a bit around actual FOW revelation
constructor Create(aMapX, aMapY: Word; aDynamicFOW: Boolean);
destructor Destroy; override;
property InitialRevealAll: Boolean read fInitialRevealAll write fInitialRevealAll;
property InitialRevealers: TKMPointTagList read fInitialRevealers;
procedure RevealCircle(const aPos: TKMPoint; Radius,Amount: Word);
procedure CoverCircle(const aPos: TKMPoint; Radius: Word);
procedure RevealRect(const TL, BR: TKMPoint; Amount: Word);
procedure CoverRect(const TL, BR: TKMPoint);
procedure RevealEverything;
procedure CoverEverything;
function CheckTileInitialRevelation(const X,Y: Word): Boolean; override;
function CheckVerticeRevelation(const X,Y: Word): Byte; override;
function CheckTileRevelation(const X,Y: Word): Byte; override;
function CheckRevelation(const aPoint: TKMPointF): Byte; override;
function CheckVerticeRenderRev(const X,Y: Word): Byte; override;
function CheckTileRenderRev(const X,Y: Word): Byte; override;
function CheckRenderRev(const aPoint: TKMPointF): Byte; override;
procedure SyncFOW(aFOW: TKMFogOfWar);
procedure Save(SaveStream: TKMemoryStream);
procedure Load(LoadStream: TKMemoryStream);
procedure UpdateState(aDynamicFOW: Boolean);
end;
// FOW that is always revealed (used by MapEd, Replays)
TKMFogOfWarOpen = class(TKMFogOfWarCommon)
public
function CheckTileInitialRevelation(const X,Y: Word): Boolean; override;
function CheckVerticeRevelation(const X,Y: Word): Byte; override;
function CheckTileRevelation(const X,Y: Word): Byte; override;
function CheckRevelation(const aPoint: TKMPointF): Byte; override;
function CheckVerticeRenderRev(const X,Y: Word): Byte; override;
function CheckTileRenderRev(const X,Y: Word): Byte; override;
function CheckRenderRev(const aPoint: TKMPointF): Byte; override;
end;
implementation
uses
SysUtils, KM_DevPerfLog, KM_DevPerfLogTypes;
const
// Addition to Revelation radius for Render revelation
RENDER_RADIUS_ADD = 5; // 4 is not enough sometimes (Barracks?), 5 looks good
{ TKMFogOfWar }
// Init with Terrain size only once on creation as terrain size never change during the game
constructor TKMFogOfWar.Create(aMapX, aMapY: Word; aDynamicFOW: Boolean);
begin
inherited Create;
fInitialRevealAll := False;
fInitialRevealers := TKMPointTagList.Create;
SetMapSize(aMapX, aMapY);
fDynamicFOW := aDynamicFOW;
end;
destructor TKMFogOfWar.Destroy;
begin
FreeAndNil(fInitialRevealers);
inherited;
end;
procedure TKMFogOfWar.SetMapSize(aMapX, aMapY: Word);
begin
fMapX := aMapX;
fMapY := aMapY;
SetLength(fRevealedToMax, fMapY, fMapX);
SetLength(Revelation, fMapY, fMapX);
SetLength(RenderRevelation, fMapY, fMapX);
end;
// Reveal circle on map
// Amount controls how "strong" terrain is revealed, almost instantly or slowly frame-by-frame in multiple calls
procedure TKMFogOfWar.RevealCircle(const aPos: TKMPoint; Radius, Amount: Word);
procedure RevealFor(aForRevelation: Boolean; aRadius, aAmount: Word);
var
I, K: Word;
I1, I2, K1, K2: Word;
sqrRadius, sqrI: Integer;
revArray: PKMByte2Array;
begin
if aForRevelation then
revArray := @Revelation
else
revArray := @RenderRevelation;
//Avoid repeated computing (+2% performance)
I1 := Max(aPos.Y-aRadius, 0);
I2 := Min(aPos.Y+aRadius, fMapY-1);
K1 := Max(aPos.X-aRadius, 0);
K2 := Min(aPos.X+aRadius, fMapX-1);
sqrRadius := Sqr(aRadius);
//Inline maths here to gain performance
if aAmount >= FOG_OF_WAR_MAX then
begin
for I := I1 to I2 do
begin
sqrI := Sqr(aPos.Y - I);
for K := K1 to K2 do
if (Sqr(aPos.X - K) + sqrI) <= sqrRadius then
begin
revArray^[I, K] := FOG_OF_WAR_MAX;
if aForRevelation then
fRevealedToMax[I, K] := True;
end;
end;
end
else
begin
for I := I1 to I2 do
begin
sqrI := Sqr(aPos.Y - I);
for K := K1 to K2 do
if (Sqr(aPos.X - K) + sqrI) <= sqrRadius then
begin
revArray^[I, K] := Min(revArray^[I, K] + aAmount, FOG_OF_WAR_MAX);
if aForRevelation and (revArray^[I, K] = FOG_OF_WAR_MAX) then
fRevealedToMax[I, K] := True;
end;
end;
end;
end;
var
aroundRadius: Word;
begin
{$IFDEF DBG_PERFLOG}
gPerfLogs.SectionEnter(psGameFOW);
{$ENDIF}
try
aroundRadius := Radius + RENDER_RADIUS_ADD;
if not fCoverHasBeenCalled and not fDynamicFOW then
begin
if fRevealedRadius[aPos.Y, aPos.X] < Radius then
begin
fRevealedRadius[aPos.Y, aPos.X] := Radius;
RevealFor(True, Radius, Amount);
end;
if fRenderRevRevealedRad[aPos.Y, aPos.X] < aroundRadius then
begin
fRenderRevRevealedRad[aPos.Y, aPos.X] := aroundRadius;
RevealFor(False, aroundRadius, FOG_OF_WAR_MAX);
end;
end else begin
RevealFor(True, Radius, Amount);
RevealFor(False, aroundRadius, FOG_OF_WAR_MAX);
end;
finally
{$IFDEF DBG_PERFLOG}
gPerfLogs.SectionLeave(psGameFOW);
{$ENDIF}
end;
end;
procedure TKMFogOfWar.CoverCircle(const aPos: TKMPoint; Radius: Word);
procedure CoverFor(aForRevelation: Boolean; aRadius: Word);
var
I, K: Word;
I1, I2, K1, K2: Word;
sqrRadius: Integer;
revArray: PKMByte2Array;
begin
if aForRevelation then
revArray := @Revelation
else
revArray := @RenderRevelation;
// Avoid repeated computing (+2% performance)
I1 := Max(aPos.Y-aRadius, 0);
I2 := Min(aPos.Y+aRadius, fMapY-1);
K1 := Max(aPos.X-aRadius, 0);
K2 := Min(aPos.X+aRadius, fMapX-1);
sqrRadius := Sqr(aRadius);
//Inline maths here to gain performance
for I := I1 to I2 do
for K := K1 to K2 do
if (Sqr(aPos.X - K) + Sqr(aPos.Y - I)) <= sqrRadius then
revArray^[I,K] := 0;
end;
begin
{$IFDEF DBG_PERFLOG}
gPerfLogs.SectionEnter(psGameFOW);
{$ENDIF}
try
CoverFor(True, Radius);
CoverFor(False, Max(0, Integer(Radius) - RENDER_RADIUS_ADD));
fCoverHasBeenCalled := True;
finally
{$IFDEF DBG_PERFLOG}
gPerfLogs.SectionLeave(psGameFOW);
{$ENDIF}
end;
end;
procedure TKMFogOfWar.RevealRect(const TL, BR: TKMPoint; Amount: Word);
var
I, K: Word;
begin
{$IFDEF DBG_PERFLOG}
gPerfLogs.SectionEnter(psGameFOW);
{$ENDIF}
try
for I := TL.Y to BR.Y do
for K := TL.X to BR.X do
begin
Revelation[I,K] := Min(Revelation[I,K] + Amount, FOG_OF_WAR_MAX);
if Revelation[I,K] = FOG_OF_WAR_MAX then
fRevealedToMax[I,K] := True;
end;
// Reveal with bigger radius for AroundRevelation
for I := Max(0, TL.Y - RENDER_RADIUS_ADD) to Min(fMapY - 1, BR.Y + RENDER_RADIUS_ADD) do
for K := Max(0, TL.X - RENDER_RADIUS_ADD) to Min(fMapX - 1, BR.X + RENDER_RADIUS_ADD) do
RenderRevelation[I,K] := FOG_OF_WAR_MAX;
finally
{$IFDEF DBG_PERFLOG}
gPerfLogs.SectionLeave(psGameFOW);
{$ENDIF}
end;
end;
procedure TKMFogOfWar.CoverRect(const TL, BR: TKMPoint);
var
I, K: Word;
begin
{$IFDEF DBG_PERFLOG}
gPerfLogs.SectionEnter(psGameFOW);
{$ENDIF}
try
for I := TL.Y to BR.Y do
for K := TL.X to BR.X do
Revelation[I,K] := 0;
// Cover with smaller radius for AroundRevelation, as nearby could be reveled tiles
for I := Min(fMapY - 1, TL.Y + RENDER_RADIUS_ADD) to Max(0, BR.Y - RENDER_RADIUS_ADD) do
for K := Min(fMapX - 1, TL.X + RENDER_RADIUS_ADD) to Max(0, BR.X - RENDER_RADIUS_ADD) do
RenderRevelation[I,K] := 0;
fCoverHasBeenCalled := True;
finally
{$IFDEF DBG_PERFLOG}
gPerfLogs.SectionLeave(psGameFOW);
{$ENDIF}
end;
end;
// Reveal whole map to max value
procedure TKMFogOfWar.RevealEverything;
var
I, K: Word;
begin
{$IFDEF DBG_PERFLOG}
gPerfLogs.SectionEnter(psGameFOW);
{$ENDIF}
try
for I := 0 to fMapY - 1 do
for K := 0 to fMapX - 1 do
begin
Revelation[I, K] := FOG_OF_WAR_MAX;
RenderRevelation[I, K] := FOG_OF_WAR_MAX;
end;
finally
{$IFDEF DBG_PERFLOG}
gPerfLogs.SectionLeave(psGameFOW);
{$ENDIF}
end;
end;
procedure TKMFogOfWar.CoverEverything;
var
I, K: Word;
begin
{$IFDEF DBG_PERFLOG}
gPerfLogs.SectionEnter(psGameFOW);
{$ENDIF}
try
for I := 0 to fMapY - 1 do
for K := 0 to fMapX - 1 do
begin
Revelation[I, K] := 0;
RenderRevelation[I, K] := FOG_OF_WAR_MAX;
end;
fCoverHasBeenCalled := True;
finally
{$IFDEF DBG_PERFLOG}
gPerfLogs.SectionLeave(psGameFOW);
{$ENDIF}
end;
end;
// Check if requested vertice is revealed for given player
// Return value of revelation is 0..255
// 0 unrevealed, 255 revealed completely
// but False in cases where it will effect the gameplay (e.g. unit hit test)
function TKMFogOfWar.CheckVerticeRev(aRevArray: PKMByte2Array; const X,Y: Word): Byte;
var
F: Byte;
begin
// I like how "alive" the fog looks with some tweaks
// pulsating around units and slowly thickening when they leave :)
F := aRevArray^[Y,X];
if fDynamicFOW then
if (F >= FOG_OF_WAR_ACT) then
Result := 255
else
Result := (F shl 8) div FOG_OF_WAR_ACT
else
if (F >= FOG_OF_WAR_MIN) then
Result := 255
else
Result := 0;
end;
function TKMFogOfWar.CheckVerticeRevelation(const X, Y: Word): Byte;
begin
Result := CheckVerticeRev(@Revelation, X, Y);
end;
function TKMFogOfWar.CheckRevelation(const aPoint: TKMPointF): Byte;
begin
Result := CheckRev(@Revelation, aPoint);
end;
function TKMFogOfWar.CheckTileRevelation(const X, Y: Word): Byte;
begin
Result := CheckTileRev(@Revelation, X, Y);
end;
function TKMFogOfWar.CheckVerticeRenderRev(const X, Y: Word): Byte;
begin
Result := CheckVerticeRev(@RenderRevelation, X, Y);
end;
function TKMFogOfWar.CheckTileRenderRev(const X, Y: Word): Byte;
begin
Result := CheckTileRev(@RenderRevelation, X, Y);
end;
function TKMFogOfWar.CheckRenderRev(const aPoint: TKMPointF): Byte;
begin
Result := CheckRev(@RenderRevelation, aPoint);
end;
function TKMFogOfWar.CheckTileInitialRevelation(const X,Y: Word): Boolean;
var
I: Integer;
P: TKMPoint;
begin
if fInitialRevealAll then
Exit(True);
Result := False;
P := TKMPoint.New(X, Y);
for I := 0 to fInitialRevealers.Count - 1 do
Result := Result or (KMDistanceSqr(P, fInitialRevealers[I]) < Sqr(fInitialRevealers.Tag[I]));
end;
// Check if requested tile is revealed for given player
// Input values for tiles (X,Y) are in 1..N range
// Return value of revelation within 0..255 (0 unrevealed, 255 fully revealed)
// but False in cases where it will effect the gameplay (e.g. unit hit test)
function TKMFogOfWar.CheckTileRev(aRevArray: PKMByte2Array; const X,Y: Word): Byte;
begin
if (X <= 0) or (X >= fMapX)
or (Y <= 0) or (Y >= fMapY) then
Exit(0);
// Check all four corners and choose max
Result := CheckVerticeRev(aRevArray,X-1,Y-1);
if Result = 255 then Exit;
Result := Max(Result, CheckVerticeRev(aRevArray,X,Y-1));
if Result = 255 then Exit;
Result := Max(Result, CheckVerticeRev(aRevArray,X,Y));
if Result = 255 then Exit;
Result := Max(Result, CheckVerticeRev(aRevArray,X-1,Y));
end;
// Check exact revelation of the point (interpolate between vertices)
function TKMFogOfWar.CheckRev(aRevArray: PKMByte2Array; const aPoint: TKMPointF): Byte;
var
A, B, C, D, Y1, Y2: Byte;
begin
if (aPoint.X <= 0) or (aPoint.X >= fMapX - 1)
or (aPoint.Y <= 0) or (aPoint.Y >= fMapY - 1) then
Exit(0);
//Interpolate as follows:
//A-B
//C-D
A := CheckVerticeRev(aRevArray, Trunc(aPoint.X), Trunc(aPoint.Y) );
B := CheckVerticeRev(aRevArray, Trunc(aPoint.X)+1, Trunc(aPoint.Y) );
C := CheckVerticeRev(aRevArray, Trunc(aPoint.X), Trunc(aPoint.Y)+1 );
D := CheckVerticeRev(aRevArray, Trunc(aPoint.X)+1, Trunc(aPoint.Y)+1 );
Y1 := Round(A + (B - A) * Frac(aPoint.X));
Y2 := Round(C + (D - C) * Frac(aPoint.X));
Result := Round(Y1 + (Y2 - Y1) * Frac(aPoint.Y));
end;
// Synchronize FOW revelation between players
procedure TKMFogOfWar.SyncFOW(aFOW: TKMFogOfWar);
var
I, K: Word;
begin
for I := 0 to fMapY - 1 do
for K := 0 to fMapX - 1 do
begin
Revelation[I, K] := Max(Revelation[I, K], aFOW.Revelation[I, K]);
RenderRevelation[I, K] := Max(RenderRevelation[I, K], aFOW.RenderRevelation[I, K]);
end;
end;
procedure TKMFogOfWar.Save(SaveStream: TKMemoryStream);
var
I: Word;
begin
SaveStream.PlaceMarker('FOW');
SaveStream.Write(fAnimStep);
SaveStream.Write(fInitialRevealAll);
fInitialRevealers.SaveToStream(SaveStream);
//Because each player has FOW it can become a bottleneck (8.7ms per run) due to autosaving (e.g. on Paradise Island)
//so save it out 1 row at a time (due to 2D arrays not being continguous we can't save it all at once)
for I := 0 to fMapY - 1 do
begin
SaveStream.Write(fRevealedToMax[I, 0], fMapX * SizeOf(fRevealedToMax[I, 0]));
SaveStream.Write(Revelation[I, 0], fMapX * SizeOf(Revelation[I, 0]));
SaveStream.Write(RenderRevelation[I, 0], fMapX * SizeOf(RenderRevelation[I, 0]));
end;
end;
procedure TKMFogOfWar.Load(LoadStream: TKMemoryStream);
var
I: Word;
begin
LoadStream.CheckMarker('FOW');
LoadStream.Read(fAnimStep);
LoadStream.Read(fInitialRevealAll);
fInitialRevealers.LoadFromStream(LoadStream);
SetMapSize(fMapX, fMapY);
for I := 0 to fMapY - 1 do
begin
LoadStream.Read(fRevealedToMax[I, 0], fMapX * SizeOf(fRevealedToMax[I, 0]));
LoadStream.Read(Revelation[I, 0], fMapX * SizeOf(Revelation[I, 0]));
LoadStream.Read(RenderRevelation[I, 0], fMapX * SizeOf(RenderRevelation[I, 0]));
end;
end;
//Decrease FOW revelation as time goes
procedure TKMFogOfWar.UpdateState(aDynamicFOW: Boolean);
var
I, K: Word;
begin
fDynamicFOW := aDynamicFOW;
if not fDynamicFOW then Exit;
{$IFDEF DBG_PERFLOG}
gPerfLogs.SectionEnter(psGameFOW);
{$ENDIF}
try
Inc(fAnimStep);
for I := 0 to fMapY - 1 do
for K := 0 to fMapX - 1 do
if {(Revelation[I, K] > 0)//(Revelation[I, K] > FOG_OF_WAR_MIN)
and }((I * fMapX + K + fAnimStep) mod FOW_PACE = 0) then
begin
if (Revelation[I, K] > FOG_OF_WAR_MAX - FOG_OF_WAR_DEC) then
begin
if not fRevealedToMax[I, K] then
Revelation[I, K] := Max(0, Revelation[I, K] - FOG_OF_WAR_DEC)
else
fRevealedToMax[I, K] := False;
end else
Revelation[I, K] := Max(0, Revelation[I, K] - FOG_OF_WAR_DEC);
{//Remember what we have seen last
if Revelation[I, K].Visibility <= FOG_OF_WAR_MIN then
begin
Revelation[I, K].LastTerrain := gTerrain.Land^[I, K].BaseLayer.Terrain;
end;}
end;
finally
{$IFDEF DBG_PERFLOG}
gPerfLogs.SectionLeave(psGameFOW);
{$ENDIF}
end;
end;
{ TKMFogOfWarOpen }
function TKMFogOfWarOpen.CheckTileInitialRevelation(const X,Y: Word): Boolean;
begin
Result := True;
end;
function TKMFogOfWarOpen.CheckRenderRev(const aPoint: TKMPointF): Byte;
begin
Result := FOG_OF_WAR_MAX;
end;
function TKMFogOfWarOpen.CheckRevelation(const aPoint: TKMPointF): Byte;
begin
Result := FOG_OF_WAR_MAX;
end;
function TKMFogOfWarOpen.CheckTileRenderRev(const X, Y: Word): Byte;
begin
Result := FOG_OF_WAR_MAX;
end;
function TKMFogOfWarOpen.CheckTileRevelation(const X, Y: Word): Byte;
begin
Result := FOG_OF_WAR_MAX;
end;
function TKMFogOfWarOpen.CheckVerticeRenderRev(const X, Y: Word): Byte;
begin
Result := FOG_OF_WAR_MAX;
end;
function TKMFogOfWarOpen.CheckVerticeRevelation(const X, Y: Word): Byte;
begin
Result := FOG_OF_WAR_MAX;
end;
end.
File diff suppressed because it is too large. Load diff
+138
View File
@@ -0,0 +1,138 @@
unit KM_GameClasses;
{$I KaM_Remake.inc}
interface
uses
KM_Minimap,
KM_CommonClasses,
KM_WorkerThread;
type
//MP Game local data, which should not be transfered over net (for different reasons described below)
TKMGameMPLocalData = class
private
fLastReplayTick: Cardinal; //we can't put it into game, since this tick could be different for every player and we will get different save files CRC
fStartLoc: Integer; //Starting loc is used to check if we are allowed to load minimap. F.e. we do not need to load minimap if we changed loc to other after back to lobby
fMinimap: TKMMinimap; //Minimap could be unique for each player, then we can't save it to game it too
procedure LoadHeader(LoadStream: TKMemoryStream);
procedure LoadMinimap(LoadStream: TKMemoryStream; aMinimap: TKMMinimap);
public
constructor Create; overload;
constructor Create(aLastReplayTick: Cardinal; aStartLoc: Integer; aMinimap: TKMMinimap); overload;
property LastReplayTick: Cardinal read fLastReplayTick write fLastReplayTick;
procedure Save(SaveStream: TKMemoryStream);
procedure Load(LoadStream: TKMemoryStream; aMinimap: TKMMinimap = nil);
procedure SaveToFile(const aFilePath: String);
procedure SaveToFileAsync(const aFilePath: String; aWorkerThread: TKMWorkerThread);
function LoadFromFile(const aFilePath: String): Boolean;
end;
implementation
uses
SysUtils,
KM_Defaults,
KM_WorkerThreadUtils;
constructor TKMGameMPLocalData.Create;
begin
inherited;
Create(0, -1, nil);
end;
constructor TKMGameMPLocalData.Create(aLastReplayTick: Cardinal; aStartLoc: Integer; aMinimap: TKMMinimap);
begin
inherited Create;
fLastReplayTick := aLastReplayTick;
fStartLoc := aStartLoc;
fMinimap := aMinimap;
end;
procedure TKMGameMPLocalData.Save(SaveStream: TKMemoryStream);
begin
SaveStream.Write(fLastReplayTick);
SaveStream.Write(fStartLoc);
if fMinimap <> nil then
fMinimap.SaveToStream(SaveStream);
end;
procedure TKMGameMPLocalData.Load(LoadStream: TKMemoryStream; aMinimap: TKMMinimap = nil);
begin
LoadHeader(LoadStream);
LoadMinimap(LoadStream, aMinimap);
end;
procedure TKMGameMPLocalData.LoadHeader(LoadStream: TKMemoryStream);
begin
LoadStream.Read(fLastReplayTick);
LoadStream.Read(fStartLoc);
end;
procedure TKMGameMPLocalData.LoadMinimap(LoadStream: TKMemoryStream; aMinimap: TKMMinimap);
begin
if aMinimap <> nil then
aMinimap.LoadFromStream(LoadStream);
end;
procedure TKMGameMPLocalData.SaveToFile(const aFilePath: String);
var
saveStream: TKMemoryStream;
begin
saveStream := TKMemoryStreamBinary.Create;
try
Save(saveStream);
saveStream.SaveToFile(aFilePath);
finally
saveStream.Free;
end;
end;
procedure TKMGameMPLocalData.SaveToFileAsync(const aFilePath: String; aWorkerThread: TKMWorkerThread);
var
saveStream: TKMemoryStream;
begin
saveStream := TKMemoryStreamBinary.Create;
Save(saveStream);
AsyncSaveToFileAndFree(saveStream, aFilePath, aWorkerThread);
end;
function TKMGameMPLocalData.LoadFromFile(const aFilePath: String): Boolean;
var
loadStream: TKMemoryStream;
choosenStartLoc: Integer;
begin
Result := False;
if not FileExists(aFilePath) then Exit;
loadStream := TKMemoryStreamBinary.Create;
try
loadStream.LoadFromFile(aFilePath);
choosenStartLoc := fStartLoc;
LoadHeader(loadStream);
if (choosenStartLoc = LOC_ANY) // for not MP game, f.e.
or (choosenStartLoc = LOC_SPECTATE) // allow to see minimap for spectator loc
or (fStartLoc = choosenStartLoc) then // allow, if we was on the same loc
begin
LoadMinimap(loadStream, fMinimap);
Result := True;
end;
finally
loadStream.Free;
end;
end;
end.
+331
View File
@@ -0,0 +1,331 @@
unit KM_GameInfo;
{$I KaM_Remake.inc}
interface
uses
KM_CommonClasses, KM_Maps, KM_MapTypes, KM_Defaults,
KM_HandTypes, KM_CommonTypes;
type
TKMGameInfoParseErrorType = (gipetNone, gipetUnsupportedFormat, gipetUnsupportedVersion);
TKMGameInfoParseError = record
ErrorString: UnicodeString;
ErrorType: TKMGameInfoParseErrorType;
end;
//Info that is relevant to any game, be it Save or a Mission
TKMGameInfo = class
private
fMapTxtInfo: TKMMapTxtInfo;
fParseError: TKMGameInfoParseError;
procedure ResetParseError;
function GetVersionUnicode: UnicodeString;
function IsOnlyAILoc(aLoc: Integer): Boolean;
public
Title: UnicodeString; //Used for campaigns and to store in savegames
Version: AnsiString; //Savegame version, yet unused in maps, they always have actual version
MapFullCRC: Cardinal; //CRC of entire map, used for reporting which map was played to master server
MapSimpleCRC: Cardinal; //CRC of .dat + .map files
DATCRC: Cardinal; //CRC of defines .dat files (data\defines)
TickCount: Cardinal; //Current tick count of the game (unused for maps)
SaveTimestamp: TDateTime; //UTC time when the save was created (unused for maps)
MissionMode: TKMissionMode; //Fighting or Build-a-City map
MissionDifficulty: TKMMissionDifficulty;
MapSizeX, MapSizeY: Integer;
PlayerCount: Byte;
Enabled: array [0..MAX_HANDS-1] of Boolean;
CanBeHuman: array [0..MAX_HANDS-1] of Boolean;
CanBeClassicAI: array [0..MAX_HANDS-1] of Boolean;
CanBeAdvancedAI: array [0..MAX_HANDS-1] of Boolean;
OwnerNickname: array [0..MAX_HANDS-1] of AnsiString; //Nickname of the player who plays this location
HandTypes: array [0..MAX_HANDS-1] of TKMHandType;
Color: array [0..MAX_HANDS-1] of Cardinal;
Team: array [0..MAX_HANDS-1] of Integer;
//To be used in Savegames
constructor Create; overload;
constructor Create(aMapTxtInfo: TKMMapTxtInfo); overload;
destructor Destroy; override;
procedure Save(SaveStream: TKMemoryStream);
procedure Load(LoadStream: TKMemoryStream);
property TxtInfo: TKMMapTxtInfo read fMapTxtInfo write fMapTxtInfo;
property ParseError: TKMGameInfoParseError read fParseError;
function IsValid(aCheckDATCRC: Boolean): Boolean;
function AICount: Byte;
function HumanCount: Byte;
function HumanUsableLocs: TKMHandIDArray;
function FixedLocsColors: TKMCardinalArray;
function GetTimeText: UnicodeString;
function GetTitleWithTime: UnicodeString;
function GetSaveTimestamp: UnicodeString;
function ColorUsed(aColor: Cardinal): Boolean;
property VersionU: UnicodeString read GetVersionUnicode;
end;
implementation
uses
SysUtils,
KM_Log, KM_Resource, KM_ResTexts, KM_CommonUtils;
{ TKMGameInfo }
constructor TKMGameInfo.Create;
begin
Create(TKMMapTxtInfo.Create);
end;
constructor TKMGameInfo.Create(aMapTxtInfo: TKMMapTxtInfo);
begin
inherited Create;
fMapTxtInfo := aMapTxtInfo;
ResetParseError;
end;
destructor TKMGameInfo.Destroy;
begin
if fMapTxtInfo <> nil then
fMapTxtInfo.Free;
inherited;
end;
procedure TKMGameInfo.ResetParseError;
begin
fParseError.ErrorString := '';
fParseError.ErrorType := gipetNone;
end;
function TKMGameInfo.GetVersionUnicode: UnicodeString;
begin
Result := UnicodeString(Version);
end;
procedure TKMGameInfo.Load(LoadStream: TKMemoryStream);
procedure LoadGameInfoData;
var
I: Integer;
begin
LoadStream.Read(DATCRC); //Don't check it here (maps don't care), if required somebody else will check it
LoadStream.Read(MapFullCRC);
LoadStream.Read(MapSimpleCRC);
LoadStream.ReadW(Title); //GameName
LoadStream.Read(TickCount);
LoadStream.Read(SaveTimestamp);
LoadStream.Read(MissionMode, SizeOf(MissionMode));
LoadStream.Read(MissionDifficulty, SizeOf(MissionDifficulty));
LoadStream.Read(MapSizeX);
LoadStream.Read(MapSizeY);
LoadStream.Read(PlayerCount);
for I := 0 to PlayerCount - 1 do
begin
LoadStream.Read(CanBeHuman[I]);
LoadStream.Read(CanBeClassicAI[I]);
LoadStream.Read(CanBeAdvancedAI[I]);
LoadStream.Read(Enabled[I]);
LoadStream.ReadA(OwnerNickname[I]);
LoadStream.Read(HandTypes[I], SizeOf(HandTypes[I]));
LoadStream.Read(Color[I]);
LoadStream.Read(Team[I]);
end;
fMapTxtInfo.Load(LoadStream);
end;
var
ansiStr: AnsiString;
begin
ResetParseError;
LoadStream.ReadA(ansiStr);
if ansiStr <> 'KaM_GameInfo' then
begin
fParseError.ErrorString := Format(gResTexts[TX_SAVE_UNSUPPORTED_FORMAT], [Copy(ansiStr, 1, 8)]);
fParseError.ErrorType := gipetUnsupportedFormat;
Exit;
end;
LoadStream.ReadA(Version);
if Version <> GAME_REVISION then
begin
fParseError.ErrorString := Format(gResTexts[TX_SAVE_UNSUPPORTED_VERSION], [Version]);
fParseError.ErrorType := gipetUnsupportedVersion;
// Exit; //need to try load game data anyway, in case we will try to load unsupported version save
end;
if fParseError.ErrorType = gipetUnsupportedVersion then
begin
try
LoadGameInfoData;
except
on E: Exception do
gLog.AddTime('Error while loading game info from save of unsupported version "%s"', [Version]); //silently log error
end;
end
else
LoadGameInfoData; //Load without catching exception
end;
procedure TKMGameInfo.Save(SaveStream: TKMemoryStream);
var
I: Integer;
begin
SaveStream.WriteA('KaM_GameInfo');
SaveStream.WriteA(GAME_REVISION); //Save current revision
SaveStream.Write(gRes.GetDATCRC);
SaveStream.Write(MapFullCRC);
SaveStream.Write(MapSimpleCRC);
SaveStream.WriteW(Title); //GameName
SaveStream.Write(TickCount);
// Game times differ for game and replay
// Set default value there in that case
if DBG_GAME_SAVE_STRIP_FOR_CRC then
SaveStream.Write(DATE_TIME_ZERO)
else
SaveStream.Write(SaveTimestamp);
SaveStream.Write(MissionMode, SizeOf(MissionMode));
SaveStream.Write(MissionDifficulty, SizeOf(MissionDifficulty));
SaveStream.Write(MapSizeX);
SaveStream.Write(MapSizeY);
SaveStream.Write(PlayerCount);
for I := 0 to PlayerCount - 1 do
begin
SaveStream.Write(CanBeHuman[I]);
SaveStream.Write(CanBeClassicAI[I]);
SaveStream.Write(CanBeAdvancedAI[I]);
SaveStream.Write(Enabled[I]);
SaveStream.WriteA(OwnerNickname[I]);
SaveStream.Write(HandTypes[I], SizeOf(HandTypes[I]));
SaveStream.Write(Color[I]);
SaveStream.Write(Team[I]);
end;
fMapTxtInfo.Save(SaveStream);
end;
function TKMGameInfo.IsValid(aCheckDATCRC: Boolean): Boolean;
begin
Result := (PlayerCount > 0) and (not aCheckDATCRC or (DATCRC = gRes.GetDATCRC));
end;
//How many AI players are in this game,
//so that Lobby could automatically create this much AIs when the save is selected
function TKMGameInfo.AICount: Byte;
var
I: Integer;
begin
Result := 0;
for I := 0 to PlayerCount - 1 do
if HandTypes[I] = hndComputer then
Inc(Result);
end;
function TKMGameInfo.HumanCount: Byte;
var
I: Integer;
begin
Result := 0;
for I := 0 to PlayerCount - 1 do
if Enabled[I] and (HandTypes[I] = hndHuman) then
Inc(Result);
end;
function TKMGameInfo.HumanUsableLocs: TKMHandIDArray;
var
I: Integer;
begin
SetLength(Result, 0);
for I := 0 to MAX_HANDS - 1 do
if CanBeHuman[I] then
begin
SetLength(Result, Length(Result)+1);
Result[Length(Result)-1] := I;
end;
end;
function TKMGameInfo.IsOnlyAILoc(aLoc: Integer): Boolean;
begin
Assert(aLoc < MAX_HANDS);
Result := not CanBeHuman[aLoc] and (CanBeClassicAI[aLoc] or CanBeAdvancedAI[aLoc]);
end;
// Color is fixed for loc if map has BlockColorSelection attribute
// or if its only AI loc, no available for player
// *** We don't need to check if loc is only for AI for now, it works fine without it
function TKMGameInfo.FixedLocsColors: TKMCardinalArray;
var
I: Integer;
begin
SetLength(Result, 0);
if Self = nil then Exit;
SetLength(Result, PlayerCount);
for I := 0 to PlayerCount - 1 do
if TxtInfo.BlockColorSelection or IsOnlyAILoc(I) then
Result[I] := Color[I]
else
Result[I] := 0;
end;
function TKMGameInfo.GetTimeText: UnicodeString;
begin
Result := TickToTimeStr(TickCount);
end;
function TKMGameInfo.GetTitleWithTime: UnicodeString;
begin
if IsValid(True) then
Result := Title + ' ' + TickToTimeStr(TickCount)
else
Result := Title;
end;
function TKMGameInfo.GetSaveTimestamp: UnicodeString;
begin
Result := FormatDateTime('ddddd t', UTCToLocal(SaveTimestamp));
end;
function TKMGameInfo.ColorUsed(aColor: Cardinal): Boolean;
var
I: Integer;
begin
for I := 0 to MAX_HANDS - 1 do
if Enabled[I] and (Color[I] = aColor) then
begin
Result := True;
Exit;
end;
Result := False;
end;
end.
+80
View File
@@ -0,0 +1,80 @@
unit KM_GameOptions;
{$I KaM_Remake.inc}
interface
uses
KM_CommonClasses, KM_MapTypes;
type
//Game options set in MP lobby
//(maybe later we could use some of these for SP games too)
TKMGameOptions = class
public
Peacetime: Word; //Peacetime in minutes
SpeedPT: Single; //Game speed during peacetime
SpeedAfterPT: Single; //Game speed after peacetime (usually slower)
RandomSeed: Integer;
MissionDifficulty: TKMMissionDifficulty;
constructor Create;
procedure Reset;
procedure Save(SaveStream: TKMemoryStream);
procedure Load(LoadStream: TKMemoryStream);
function ToString: string; reintroduce;
end;
implementation
uses
SysUtils, TypInfo, KM_Defaults;
{ TKMGameOptions }
constructor TKMGameOptions.Create;
begin
inherited;
//Default values are not always 0
Reset;
end;
//Resets values to defaults
procedure TKMGameOptions.Reset;
begin
Peacetime := DEFAULT_PEACE_TIME;
SpeedPT := 1;
SpeedAfterPT := 1;
RandomSeed := 0; //Must be init later on. 0 is an erroneous value for KaMSeed
MissionDifficulty := mdNone;
end;
procedure TKMGameOptions.Load(LoadStream: TKMemoryStream);
begin
LoadStream.Read(Peacetime);
LoadStream.Read(SpeedPT);
LoadStream.Read(SpeedAfterPT);
LoadStream.Read(RandomSeed);
LoadStream.Read(MissionDifficulty, SizeOf(MissionDifficulty));
end;
procedure TKMGameOptions.Save(SaveStream: TKMemoryStream);
begin
SaveStream.Write(Peacetime);
SaveStream.Write(SpeedPT);
SaveStream.Write(SpeedAfterPT);
SaveStream.Write(RandomSeed);
SaveStream.Write(MissionDifficulty, SizeOf(MissionDifficulty));
end;
function TKMGameOptions.ToString: string;
begin
Result := Format('PT = %d; SpeedPT = %s; SpeedAfterPT = %s; Seed = %d; Difficulty = %s',
[Peacetime, FormatFloat('0.##', SpeedPT), FormatFloat('0.##', SpeedAfterPT),
RandomSeed, GetEnumName(TypeInfo(TKMMissionDifficulty), Integer(MissionDifficulty))]);
end;
end.
+364
View File
@@ -0,0 +1,364 @@
unit KM_GameParams;
{$I KaM_Remake.inc}
interface
uses
KM_Defaults, KM_CommonTypes, KM_GameTypes, KM_MapTypes;
type
TKMGameModeSetEvent = procedure (aGameMode: TKMGameMode) of object;
// This class represents some of the game parameters, kind of KM_Game light interface
// Usefull to split KM_Game and to reduce units dependencies
TKMGameParams = class
private
fMode: TKMGameMode;
fMissionMode: TKMissionMode;
fTick: Cardinal;
fTickFrac: Single;
fVisibleLayers: TKMMapVisibleLayerSet;
fName: UnicodeString;
fMapSimpleCRC: Cardinal; //CRC of map (based on Map and Dat) used in MapEd
fMapFullCRC: Cardinal; //CRC of map for reporting stats to master server. Also used in MapEd
fMissionFileRelSP: UnicodeString; //Relative pathname to mission we are playing, so it gets saved to crashreport. SP only, see GetMissionFile.
fMissionDifficulty: TKMMissionDifficulty;
fDynamicFOW: Boolean;
fBlockPointerOperations: Boolean;
fOnRecalcMapCRC: TKMEvent;
fOnSetVisibleLayers: TKMEvent;
// Do not saved fields
// fMissionFullFilePath is not saved, so its only available when player start game, f.e. in the MapEditor
// not available on game load, since we could not have the mission files at all, but only sve files
fMissionFullFilePath: string;
procedure SetTick(aGameTick: Cardinal);
procedure SetTickFrac(aGameTickFrac: Single);
procedure SetMode(aGameMode: TKMGameMode);
function GetMissionFileRel: UnicodeString;
procedure SetMissionFileSP(const aMissionFileRelSP: UnicodeString);
function GetDynamicFOW: Boolean;
procedure SetDynamicFOW(const aDynamicFOW: Boolean);
procedure SetBlockPointer(aBlockPointer: Boolean);
function GetMapFullCRC: Cardinal;
function GetMapSimpleCRC: Cardinal;
procedure SetVisibleLayers(const aValue: TKMMapVisibleLayerSet);
public
constructor Create(aGameMode: TKMGameMode; aOnRecalcMapCRC, aOnSetVisibleLayers: TKMEvent; out aSetGameTickEvent: TCardinalEvent;
out aSetGameTickFracEvent: TSingleEvent; out aSetGameModeEvent: TKMGameModeSetEvent;
out aSetMissionFileSP: TUnicodeStringEvent; out aSetBlockPointer: TBooleanEvent);
destructor Destroy; override;
property Mode: TKMGameMode read fMode;
property MissionMode: TKMissionMode read fMissionMode write fMissionMode;
property Tick: Cardinal read fTick;
property TickFrac: Single read fTickFrac;
property VisibleLayers: TKMMapVisibleLayerSet read fVisibleLayers write SetVisibleLayers;
property Name: UnicodeString read fName write fName;
property MapSimpleCRC: Cardinal read GetMapSimpleCRC write fMapSimpleCRC;
property MapFullCRC: Cardinal read GetMapFullCRC write fMapFullCRC;
property MissionFileRelSP: UnicodeString read fMissionFileRelSP;
property MissionFileRel: UnicodeString read GetMissionFileRel;
property MissionDifficulty: TKMMissionDifficulty read fMissionDifficulty write fMissionDifficulty;
property DynamicFOW: Boolean read GetDynamicFOW write SetDynamicFOW;
property BlockPointerOperations: Boolean read fBlockPointerOperations;
property MissionFullFilePath: string read fMissionFullFilePath write fMissionFullFilePath;
function GuessMissionFullFilePath: string;
function IsCRCCalculated: Boolean;
function IsMapEditor: Boolean;
function IsCampaign: Boolean;
function IsMultiPlayerOrSpec: Boolean;
function IsMultiplayerGame: Boolean;
function IsSpectatorGame: Boolean;
function IsMultiplayer: Boolean;
function IsReplay: Boolean;
function IsReplayOrSpectate: Boolean;
function IsSingle: Boolean;
function IsSingleplayerGame: Boolean;
function IsSingleplayer: Boolean;
function IsNormalGame: Boolean;
function IsGame: Boolean;
function IsTactic: Boolean;
function IsNormalMission: Boolean;
function HasMissionDifficulty: Boolean;
function AllowPointerOperations: Boolean;
{$IFDEF RUNNER}
procedure GetGameModeSetEvent(out aSetGameModeEvent: TKMGameModeSetEvent);
{$ENDIF}
end;
var
gGameParams: TKMGameParams;
implementation
uses
KM_MapUtils;
{ TKMGameParams }
constructor TKMGameParams.Create(aGameMode: TKMGameMode; aOnRecalcMapCRC, aOnSetVisibleLayers: TKMEvent;
out aSetGameTickEvent: TCardinalEvent; out aSetGameTickFracEvent: TSingleEvent;
out aSetGameModeEvent: TKMGameModeSetEvent; out aSetMissionFileSP: TUnicodeStringEvent;
out aSetBlockPointer: TBooleanEvent);
begin
inherited Create;
fOnRecalcMapCRC := aOnRecalcMapCRC;
fOnSetVisibleLayers := aOnSetVisibleLayers;
fVisibleLayers := [mlObjects, mlHouses, mlUnits, mlOverlays];
fMode := aGameMode;
fTick := 0;
fMissionDifficulty := mdNone;
DynamicFOW := False;
aSetGameTickEvent := SetTick;
aSetGameTickFracEvent := SetTickFrac;
aSetGameModeEvent := SetMode;
aSetMissionFileSP := SetMissionFileSP;
aSetBlockPointer := SetBlockPointer;
fBlockPointerOperations := False;
gGameParams := Self;
end;
destructor TKMGameParams.Destroy;
begin
gGameParams := nil; //should be destoryed by creator (KM_Game)
inherited;
end;
{$IFDEF RUNNER}
procedure TKMGameParams.GetGameModeSetEvent(out aSetGameModeEvent: TKMGameModeSetEvent);
begin
aSetGameModeEvent := SetMode;
end;
{$ENDIF}
function TKMGameParams.GetDynamicFOW: Boolean;
begin
if Self = nil then Exit(FEAT_DYNAMIC_FOG_OF_WAR);
Result := fDynamicFOW;
end;
function TKMGameParams.GetMapFullCRC: Cardinal;
begin
// Lazy load of MapCRC
if fMapFullCRC = 0 then
if Assigned(fOnRecalcMapCRC) then
fOnRecalcMapCRC;
Result := fMapFullCRC;
end;
function TKMGameParams.GetMapSimpleCRC: Cardinal;
begin
// Lazy load of MapCRC
if fMapSimpleCRC = 0 then
if Assigned(fOnRecalcMapCRC) then
fOnRecalcMapCRC;
Result := fMapSimpleCRC;
end;
function TKMGameParams.GetMissionFileRel: UnicodeString;
begin
Result := GuessMissionPathRel(fMissionFileRelSP, fName, fMapFullCRC, IsMultiplayer);
end;
function TKMGameParams.GuessMissionFullFilePath: string;
var
missionRel: string;
begin
if fMissionFullFilePath <> '' then Exit(fMissionFullFilePath);
Result := '';
missionRel := MissionFileRel;
if missionRel <> '' then
Result := ExeDir + missionRel;
end;
procedure TKMGameParams.SetBlockPointer(aBlockPointer: Boolean);
begin
fBlockPointerOperations := aBlockPointer;
end;
procedure TKMGameParams.SetDynamicFOW(const aDynamicFOW: Boolean);
begin
fDynamicFOW := aDynamicFOW;
end;
procedure TKMGameParams.SetMode(aGameMode: TKMGameMode);
begin
fMode := aGameMode;
end;
procedure TKMGameParams.SetTick(aGameTick: Cardinal);
begin
fTick := aGameTick;
end;
procedure TKMGameParams.SetTickFrac(aGameTickFrac: Single);
begin
fTickFrac := aGameTickFrac;
end;
procedure TKMGameParams.SetVisibleLayers(const aValue: TKMMapVisibleLayerSet);
begin
fVisibleLayers := aValue;
if Assigned(fOnSetVisibleLayers) then
fOnSetVisibleLayers;
end;
procedure TKMGameParams.SetMissionFileSP(const aMissionFileRelSP: UnicodeString);
begin
fMissionFileRelSP := aMissionFileRelSP;
end;
function TKMGameParams.IsMapEditor: Boolean;
begin
Result := fMode = gmMapEd;
end;
function TKMGameParams.IsCampaign: Boolean;
begin
Result := fMode = gmCampaign;
end;
function TKMGameParams.IsCRCCalculated: Boolean;
begin
Result := (fMapSimpleCRC <> 0) and (fMapFullCRC <> 0);
end;
function TKMGameParams.IsTactic: Boolean;
begin
Result := fMissionMode = mmFighting;
end;
function TKMGameParams.IsNormalMission: Boolean;
begin
Result := fMissionMode = mmBuilding;
end;
function TKMGameParams.IsMultiplayerGame: Boolean;
begin
Result := fMode = gmMulti;
end;
function TKMGameParams.IsSpectatorGame: Boolean;
begin
Result := fMode = gmMultiSpectate;
end;
// We often need to see if game is MP
function TKMGameParams.IsMultiPlayerOrSpec: Boolean;
begin
Result := fMode in [gmMulti, gmMultiSpectate];
end;
function TKMGameParams.IsMultiplayer: Boolean;
begin
Result := fMode in [gmMulti, gmMultiSpectate, gmReplayMulti];
end;
function TKMGameParams.IsSingle: Boolean;
begin
Result := fMode = gmSingle;
end;
function TKMGameParams.IsSingleplayerGame: Boolean;
begin
Result := fMode in [gmSingle, gmCampaign];
end;
function TKMGameParams.IsSingleplayer: Boolean;
begin
Result := fMode in [gmSingle, gmCampaign, gmReplaySingle];
end;
function TKMGameParams.IsNormalGame: Boolean;
begin
Result := fMode in [gmSingle, gmCampaign, gmMulti];
end;
function TKMGameParams.IsGame: Boolean;
begin
Result := fMode in [gmSingle, gmCampaign, gmMulti, gmMultiSpectate];
end;
function TKMGameParams.IsReplay: Boolean;
begin
Result := fMode in [gmReplaySingle, gmReplayMulti];
end;
function TKMGameParams.IsReplayOrSpectate: Boolean;
begin
Result := fMode in [gmMultiSpectate, gmReplaySingle, gmReplayMulti];
end;
function TKMGameParams.HasMissionDifficulty: Boolean;
begin
Result := fMissionDifficulty <> mdNone;
end;
function TKMGameParams.AllowPointerOperations: Boolean;
begin
Result := IsSingleplayerGame or IsMapEditor or not BlockPointerOperations {or SKIP_POINTER_REF_CHECK};
end;
end.
+490
View File
@@ -0,0 +1,490 @@
unit KM_GameSavePoints;
{$I KaM_Remake.inc}
interface
uses
SyncObjs, Generics.Collections,
KM_CommonClasses, KM_WorkerThread
{$IFDEF FPC}
, KM_Sort
{$ENDIF}
;
type
TKMSavePoint = class
private
fStreamCompressed: TKMemoryStream; // Compressed stream of a game save (save point)
fTick: Cardinal;
// Opened spectator menu, viewports position etc...
public
constructor Create(aStream: TKMemoryStream; aTick: Cardinal);
destructor Destroy; override;
property StreamCompressed: TKMemoryStream read fStreamCompressed;
property Tick: Cardinal read fTick;
end;
TKMSavePointCollection = class
private
fAsyncThreadsCnt: Byte; //Number of worker threads working atm. Used to make saves or create compressed savepoints
fWaitCS: TCriticalSection;
fSaveCS: TCriticalSection;
fSavePoints: TDictionary<Cardinal, TKMSavePoint>;
//Properties to restore after load saved replay
fLastTick: Cardinal;
function GetCount: Integer;
function GetSavePoint(aTick: Cardinal): TKMSavePoint;
function GetStream(aTick: Cardinal): TKMemoryStream;
function GetLastTick: Cardinal;
procedure SetLastTick(const aLastTick: Cardinal);
public
constructor Create;
destructor Destroy; override;
property LastTick: Cardinal read GetLastTick write SetLastTick;
procedure Clear;
procedure Lock;
procedure Unlock;
property Count: Integer read GetCount;
property SavePoint[aTick: Cardinal]: TKMSavePoint read GetSavePoint;
property Stream[aTick: Cardinal]: TKMemoryStream read GetStream; default;
function Contains(aTick: Cardinal): Boolean;
procedure FillTicks(aTicksList: TList<Cardinal>);
procedure NewSavePoint(aStream: TKMemoryStream; aTick: Cardinal);
procedure NewSavePointAsyncAndFree(var aStream: TKMemoryStream; aTick: Cardinal; aWorkerThread: TKMWorkerThread);
function LatestPointTickBefore(aTick: Cardinal): Cardinal;
procedure Save(aSaveStream: TKMemoryStream);
procedure Load(aLoadStream: TKMemoryStream);
procedure SaveToFileAsync(const aFileName: UnicodeString; aWorkerThread: TKMWorkerThread);
procedure LoadFromFile(const aFileName: UnicodeString);
end;
implementation
uses
SysUtils, Classes;
{ TKMSavePoint }
constructor TKMSavePoint.Create(aStream: TKMemoryStream; aTick: Cardinal);
begin
inherited Create;
fStreamCompressed := aStream;
fTick := aTick;
end;
destructor TKMSavePoint.Destroy;
begin
fStreamCompressed.Free;
inherited;
end;
{ TKMSavePointCollection }
constructor TKMSavePointCollection.Create;
begin
inherited;
fSaveCS := TCriticalSection.Create;
fWaitCS := TCriticalSection.Create;
fSavePoints := TDictionary<Cardinal, TKMSavePoint>.Create;
fLastTick := 0;
end;
destructor TKMSavePointCollection.Destroy;
begin
{$IFDEF WDC}
// Wait till all threads release waitLock
while True do
begin
fWaitCS.Enter;
try
if fAsyncThreadsCnt = 0 then
Break;
finally
fWaitCS.Leave;
end;
Sleep(100);
end;
{$ENDIF}
Lock; // Lock even in destructor
try
Clear;
fSavePoints.Free; // TKMList will free all objects of the list
finally
Unlock;
end;
fSaveCS.Free;
fWaitCS.Free;
inherited;
end;
function TKMSavePointCollection.GetCount: Integer;
begin
if Self = nil then Exit(0);
Lock;
try
Result := fSavePoints.Count;
finally
Unlock;
end;
end;
function TKMSavePointCollection.GetLastTick: Cardinal;
begin
if Self = nil then Exit(0);
Result := fLastTick;
end;
procedure TKMSavePointCollection.SetLastTick(const aLastTick: Cardinal);
begin
if Self = nil then Exit;
fLastTick := aLastTick;
end;
procedure TKMSavePointCollection.Clear;
var
savePoint: TKMSavePoint;
begin
if Self = nil then Exit;
Lock;
try
for savePoint in fSavePoints.Values do
savePoint.Free;
fSavePoints.Clear;
finally
Unlock;
end;
end;
function TKMSavePointCollection.Contains(aTick: Cardinal): Boolean;
begin
if Self = nil then Exit(False);
Lock;
try
Result := fSavePoints.ContainsKey(aTick);
finally
Unlock;
end;
end;
procedure TKMSavePointCollection.FillTicks(aTicksList: TList<Cardinal>);
var
Tick: Cardinal;
begin
if Self = nil then Exit;
Lock;
try
for Tick in fSavePoints.Keys do
aTicksList.Add(Tick);
finally
Unlock;
end;
end;
function TKMSavePointCollection.GetSavePoint(aTick: Cardinal): TKMSavePoint;
begin
Result := nil;
if Self = nil then Exit;
Lock;
try
if fSavePoints.ContainsKey(aTick) then
Result := fSavePoints[aTick];
finally
Unlock;
end;
end;
function TKMSavePointCollection.GetStream(aTick: Cardinal): TKMemoryStream;
var
savePoint: TKMSavePoint;
begin
Result := nil;
if Self = nil then Exit;
Lock;
try
if fSavePoints.TryGetValue(aTick, savePoint) then
Result := savePoint.StreamCompressed;
finally
Unlock;
end;
end;
procedure TKMSavePointCollection.NewSavePointAsyncAndFree(var aStream: TKMemoryStream; aTick: Cardinal; aWorkerThread: TKMWorkerThread);
{$IFDEF WDC}
var
localStream: TKMemoryStream;
{$ENDIF}
begin
{$IFDEF WDC}
// fSavePoints could be accessed by different threads
Lock;
try
// Check if we don't have same tick save here too, since we work in multithread environment
if fSavePoints.ContainsKey(aTick) then Exit;
finally
Unlock;
end;
localStream := aStream;
aStream := nil; //So caller doesn't use it by mistake
// Increase save threads counter in main thread
AtomicIncrement(fAsyncThreadsCnt);
aWorkerThread.QueueWork(
procedure
var
S: TKMemoryStream;
begin
S := TKMemoryStreamBinary.Create;
try
localStream.SaveToStreamCompressed(S);
finally
localStream.Free;
end;
// fSavePoints could be accessed by different threads
Lock;
try
fSavePoints.Add(aTick, TKMSavePoint.Create(S, aTick));
finally
Unlock;
end;
// Decrease thread counter
AtomicDecrement(fAsyncThreadsCnt);
end, 'NewSavePointAsyncAndFree');
{$ELSE}
NewSavePoint(aStream, aTick);
{$ENDIF}
end;
procedure TKMSavePointCollection.NewSavePoint(aStream: TKMemoryStream; aTick: Cardinal);
var
S: TKMemoryStream;
begin
if Self = nil then Exit;
Lock;
try
// Check if we don't have same tick save here too, since we work in multithread environment
if fSavePoints.ContainsKey(aTick) then Exit;
S := TKMemoryStreamBinary.Create;
aStream.SaveToStreamCompressed(S);
fSavePoints.Add(aTick, TKMSavePoint.Create(S, aTick));
finally
Unlock;
end;
end;
{$IFDEF FPC}
function CompareKeys(const aKey1, aKey2): Integer;
var
k1: Cardinal absolute aKey1;
k2: Cardinal absolute aKey2;
begin
if k1 < k2 then Result := -1
else if k1 > k2 then Result := +1
else Result := 0;
end;
{$ENDIF}
procedure TKMSavePointCollection.Save(aSaveStream: TKMemoryStream);
var
keyArray : TArray<Cardinal>;
key: Cardinal;
savePoint: TKMSavePoint;
begin
if Self = nil then Exit;
Lock;
try
aSaveStream.PlaceMarker('SavePoints');
aSaveStream.Write(fLastTick);
aSaveStream.Write(fSavePoints.Count);
keyArray := fSavePoints.Keys.ToArray;
{$IFNDEF FPC}
TArray.Sort<Cardinal>(keyArray);
{$ELSE}
SortCustom(keyArray, Low(keyArray), High(keyArray), SizeOf(keyArray[0]), CompareKeys);
{$ENDIF}
//todo -cComplicated: potential OutOfMemory error in this cycle?
for key in keyArray do
begin
aSaveStream.PlaceMarker('SavePoint');
aSaveStream.Write(key);
savePoint := fSavePoints.Items[key];
aSaveStream.Write(Cardinal(savePoint.fStreamCompressed.Size));
aSaveStream.CopyFrom(savePoint.fStreamCompressed, 0);
end;
finally
Unlock;
end;
end;
procedure TKMSavePointCollection.SaveToFileAsync(const aFileName: UnicodeString; aWorkerThread: TKMWorkerThread);
{$IFNDEF WDC}
var
localStream: TKMemoryStream;
{$ENDIF}
begin
if Self = nil then Exit;
{$IFDEF WDC}
// Increase save threads counter in main thread
AtomicIncrement(fAsyncThreadsCnt);
aWorkerThread.QueueWork(
procedure
var
localStream: TKMemoryStream;
begin
localStream := TKMemoryStreamBinary.Create;
try
Save(localStream); // Save has Lock / Unlock inside already
// Decrease thread counter since we saved all data into thread local stream
AtomicDecrement(fAsyncThreadsCnt);
localStream.SaveToFile(aFileName);
finally
localStream.Free;
end;
end, 'Save SavePoints');
{$ELSE}
localStream := TKMemoryStreamBinary.Create;
try
Save(localStream);
localStream.SaveToFile(aFileName);
finally
localStream.Free;
end;
{$ENDIF}
end;
procedure TKMSavePointCollection.Lock;
begin
if Self = nil then Exit;
fSaveCS.Enter;
end;
procedure TKMSavePointCollection.Unlock;
begin
if Self = nil then Exit;
fSaveCS.Leave;
end;
procedure TKMSavePointCollection.LoadFromFile(const aFileName: UnicodeString);
var
S: TKMemoryStream;
begin
if Self = nil then Exit;
if not FileExists(aFileName) then Exit;
S := TKMemoryStreamBinary.Create;
try
S.LoadFromFile(aFileName);
Load(S);
finally
S.Free;
end;
end;
// Get latest savepoint tick, before aTick
// 0 - if not found
function TKMSavePointCollection.LatestPointTickBefore(aTick: Cardinal): Cardinal;
var
key: Cardinal;
begin
Result := 0;
if Self = nil then Exit;
Lock;
try
for key in fSavePoints.Keys do
if (key <= aTick) and (key > Result) then
Result := key;
finally
Unlock;
end;
end;
procedure TKMSavePointCollection.Load(aLoadStream: TKMemoryStream);
var
I, cnt: Integer;
tick, size: Cardinal;
savePoint: TKMSavePoint;
stream: TKMemoryStream;
begin
if Self = nil then Exit;
Lock;
try
fSavePoints.Clear;
aLoadStream.CheckMarker('SavePoints');
aLoadStream.Read(fLastTick);
aLoadStream.Read(cnt);
for I := 0 to cnt - 1 do
begin
aLoadStream.CheckMarker('SavePoint');
aLoadStream.Read(tick);
aLoadStream.Read(size);
stream := TKMemoryStreamBinary.Create;
stream.CopyFrom(aLoadStream, size);
savePoint := TKMSavePoint.Create(stream, tick);
fSavePoints.Add(tick, savePoint);
end;
finally
Unlock;
end;
end;
end.
+57
View File
@@ -0,0 +1,57 @@
unit KM_GameTypes;
{$I KaM_Remake.inc}
interface
uses
KM_Defaults, KM_MapTypes, KM_CampaignTypes, KM_CampaignClasses, KM_Points;
const
SAVE_HEADER_MARKER = 'Header';
SAVE_BODY_MARKER = 'BodyCompressed';
type
TKMGameMode = (
gmSingle,
gmCampaign,
gmMulti, //Different GIP, networking,
gmMultiSpectate,
gmMapEd, //Army handling, lite updates,
gmReplaySingle, //No input, different results screen to gmReplayMulti
gmReplayMulti //No input, different results screen to gmReplaySingle
);
TKMAIType = (aitNone, aitClassic, aitAdvanced);
TKMAITypeSet = set of TKMAIType;
//Message kind determines icon and available actions for Message
TKMMessageKind = (
mkText, //Mission text message
mkHouse,
mkGroup,
mkQuill //Utility message (warnings in script loading)
);
TKMGameModeChangeEvent = procedure (aGameMode: TKMGameMode) of object;
TKMNewSingleMapEvent = procedure (const aMissionFile, aGameName: UnicodeString; aDesiredLoc: ShortInt = -1;
aDesiredColor: Cardinal = $00000000; aDifficulty: TKMMissionDifficulty = mdNone;
aAIType: TKMAIType = aitNone) of object;
TKMNewCampaignMapEvent = procedure (aCampaign: UnicodeString; aMap: Word; aDifficulty: TKMMissionDifficulty = mdNone) of object;
TKMNewMapEditorEvent = procedure (const aFileName: UnicodeString; aSizeX: Integer = 0; aSizeY: Integer = 0; aMapFullCRC: Cardinal = 0;
aMapSimpleCRC: Cardinal = 0; aMultiplayerLoadMode: Boolean = False) of object;
TKMGameShowMessageEvent = procedure (aKind: TKMMessageKind; aTextID: Integer; const aLoc: TKMPoint; aEntityUID: Integer; aHandIndex: TKMHandID) of object;
const
// Location color 'magic value', which means we should not overwrite location color which was set in the map editor
// Used on game start
NO_OVERWRITE_COLOR = $00000000; //todo -cPractical: refactor, get rid of 'magic color value'
// Value we use to set hand / player without team choosen
NO_TEAM = 0;
implementation
end.
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,517 @@
unit KM_GameInputProcess_Multi;
{$I KaM_Remake.inc}
interface
uses
KM_GameInputProcess,
KM_Hand,
KM_CommonClasses, KM_CommonTypes, KM_Defaults;
const
MAX_SCHEDULE = 100; //Size of ring buffers (10 sec) Make them large so overruns do not occur
DELAY_ADJUST = 40; //How often to adjust fDelay (every 4 seconds) This must be higher than MAX_DELAY
MIN_DELAY = 2; //A delay of 1 is not possible because that means the command shall be processed on the next tick after it was issued, but that could be 0.0001ms after the player clicks, meaning there is no way the command would have been sent. Therefore the delay must be 2 at minimum.
MAX_DELAY = 32; //Maximum number of ticks (3.2 sec) to plan ahead (highest value fDelay can take)
type
TKMDataType = (kdpCommands, kdpRandomCheck);
TKMCommandsPack = class
private
fCount: Word;
fItems: array of TKMGameInputCommand; //1..n
function GetItem(aIndex: Integer): TKMGameInputCommand;
public
property Count: Word read fCount;
procedure Clear;
procedure Add(aCommand: TKMGameInputCommand);
function CRC: Cardinal;
property Items[aIndex: Integer]: TKMGameInputCommand read GetItem;
procedure Save(SaveStream: TKMemoryStream);
procedure Load(LoadStream: TKMemoryStream);
end;
TKMRandomCheck = record
OurCheck: Cardinal;
PlayerCheck: array [1..MAX_LOBBY_SLOTS] of Cardinal;
PlayerCheckPending: array [1..MAX_LOBBY_SLOTS] of Boolean;
end;
TKMGameInputProcess_Multi = class(TKMGameInputProcess)
private
fDelay: Word; //How many ticks ahead the commands are scheduled
fLastSentCmdsTick: Cardinal; //Needed for resync (last tick, for which commands were sent)
fNumberConsecutiveWaits: Word; //Number of consecutive times we have been waiting for network
//Each player can have any number of commands scheduled for execution in one tick
fSchedule: array [0..MAX_SCHEDULE-1, 1..MAX_LOBBY_SLOTS] of TKMCommandsPack; //Ring buffer
//All players must send us data every tick
fRecievedData: array [0..MAX_SCHEDULE-1, 1..MAX_LOBBY_SLOTS] of Boolean; //Ring buffer
//Mark commands we've already sent to other players
fSent: array [0..MAX_SCHEDULE-1] of Boolean; //Ring buffer
//Did the player issue a command for this tick?
//If not it must be cleared from last time (we can't clear it earlier as it might be needed for resync)
fCommandIssued: array [0..MAX_SCHEDULE-1] of Boolean;
//Store random seeds at each tick then confirm with other players
fRandomCheck: array [0..MAX_SCHEDULE-1] of TKMRandomCheck; //Ring buffer
procedure SendCommands(aTick: Cardinal; aPlayerIndex: ShortInt = -1);
procedure SendRandomCheck(aTick: Cardinal);
procedure DoRandomCheck(aTick: Cardinal; aPlayerIndex: ShortInt);
procedure SetDelay(aNewDelay: Integer);
protected
procedure DoTakeCommand(const aCommand: TKMGameInputCommand); override;
public
constructor Create(aReplayState: TKMGIPReplayState);
destructor Destroy; override;
procedure WaitingForConfirmation(aTick: Cardinal);
procedure AdjustDelay(aGameSpeed: Single);
function GetNetworkDelay: Word;
property NumberConsecutiveWaits: Word read fNumberConsecutiveWaits;
property LastSentCmdsTick: Cardinal read fLastSentCmdsTick;
function GetWaitingPlayers(aTick: Cardinal): TKMByteArray;
procedure RecieveCommands(aStream: TKMemoryStream; aSenderIndex: ShortInt); //Called by TKMNetwork when it has data for us
procedure ResyncFromTick(aSender: ShortInt; aTick: Cardinal);
function CommandsConfirmed(aTick: Cardinal): Boolean;
procedure RunningTimer(aTick: Cardinal); override;
procedure UpdateState(aTick: Cardinal); override;
end;
implementation
uses
TypInfo,
SysUtils, Math, KromUtils,
KM_Game, KM_GameParams, KM_HandsCollection, KM_NetTypes,
KM_ResTexts, KM_ResSound, KM_Sound, KM_CommonUtils,
KM_GameTypes,
KM_Networking;
type
TKMRngCheckPlayerData = record
PlayerIndex: Integer;
HandID: TKMHandID;
Nickname: AnsiString;
Check: Cardinal;
function ToStr: string;
end;
{ TKMRngCheckPlayerData }
function TKMRngCheckPlayerData.ToStr: string;
begin
Result := Format('PlayerIndex = %d; Hand = %d; Nick = %s; Check = %d', [PlayerIndex, HandID, Nickname, Check]);
end;
{ TKMCommandsPack }
procedure TKMCommandsPack.Clear;
begin
fCount := 0;
end;
procedure TKMCommandsPack.Add(aCommand: TKMGameInputCommand);
begin
Inc(fCount);
if fCount >= Length(fItems) then
SetLength(fItems, fCount + 8);
fItems[fCount] := aCommand;
end;
function TKMCommandsPack.GetItem(aIndex:integer): TKMGameInputCommand;
begin
Result := fItems[aIndex];
end;
// Return CRC of the pack
function TKMCommandsPack.CRC: Cardinal;
var
I: Integer;
begin
Result := 0;
for I := 1 to fCount do
Result := Result xor Adler32CRC(@fItems[I], SizeOf(fItems[I]));
end;
procedure TKMCommandsPack.Save(SaveStream: TKMemoryStream);
var
I: Integer;
begin
SaveStream.Write(fCount);
for I := 1 to fCount do
begin
//gLog.AddTime('Saving "%s"', [GetEnumName(TypeInfo(TGameInputCommandType), Integer(fItems[I].CommandType))]);
SaveCommandToMemoryStream(fItems[I], SaveStream);
end;
end;
procedure TKMCommandsPack.Load(LoadStream: TKMemoryStream);
var
I: Integer;
begin
LoadStream.Read(fCount);
SetLength(fItems, fCount + 1);
for I := 1 to fCount do
LoadCommandFromMemoryStream(fItems[I], LoadStream);
end;
{ TKMGameInputProcess_Multi }
constructor TKMGameInputProcess_Multi.Create(aReplayState: TKMGIPReplayState);
var
I: Integer;
K: ShortInt;
begin
inherited Create(aReplayState);
gNetworking.OnCommands := RecieveCommands;
gNetworking.OnResyncFromTick := ResyncFromTick;
AdjustDelay(1); //Initialise the delay
//Allocate memory for all commands packs
for I := 0 to MAX_SCHEDULE - 1 do
for K := 1 to MAX_LOBBY_SLOTS do
begin
fSchedule[I, K] := TKMCommandsPack.Create;
fRandomCheck[I].PlayerCheckPending[K] := False; //We don't have anything to be checked yet
end;
end;
destructor TKMGameInputProcess_Multi.Destroy;
var
I: Integer;
K: ShortInt;
begin
for I := 0 to MAX_SCHEDULE - 1 do
for K := 1 to MAX_LOBBY_SLOTS do
fSchedule[I, K].Free;
inherited;
end;
// Stack the command into schedule
procedure TKMGameInputProcess_Multi.DoTakeCommand(const aCommand: TKMGameInputCommand);
var
I, tick: Cardinal;
begin
Assert(fDelay < MAX_SCHEDULE, 'Error, fDelay >= MAX_SCHEDULE');
if ((gGameParams.Mode = gmMultiSpectate) and not (aCommand.CommandType in ALLOWED_BY_SPECTATORS)) // Do not allow spectators to command smth
or (gGameParams.IsMultiplayerGame // in multiplayer game
and IsSelectedObjectCommand(aCommand.CommandType) // block only commands for selected object
and (gMySpectator.Selected <> nil) // if there is selected object
and not gMySpectator.IsSelectedMyObj) then // and we try to make command to ally's object
Exit;
if gGame.IsPeaceTime and (aCommand.CommandType in BLOCKED_BY_PEACETIME) then
begin
gNetworking.PostLocalMessage(gResTexts[TX_MP_BLOCKED_BY_PEACETIME], csNone);
gSoundPlayer.Play(sfxCantPlace);
Exit;
end;
if not gGameParams.IsSpectatorGame
and gMySpectator.Hand.AI.HasLost
and not (aCommand.CommandType in ALLOWED_AFTER_DEFEAT) then
begin
gSoundPlayer.Play(sfxCantPlace);
Exit;
end;
//Find first unsent pack
tick := MAX_SCHEDULE; //Out of range value
for I := gGameParams.Tick + fDelay to gGameParams.Tick + MAX_SCHEDULE - 1 do
if not fSent[I mod MAX_SCHEDULE] then
begin
tick := I mod MAX_SCHEDULE; //Place in a ring buffer
Break;
end;
Assert(tick < MAX_SCHEDULE, 'Could not find place for new commands');
if not fCommandIssued[tick] then
begin
// Clear old data (it was kept in case it was required for resync)
fSchedule[tick, gNetworking.MySlotIndex].Clear;
fCommandIssued[tick] := True;
end;
fSchedule[tick, gNetworking.MySlotIndex].Add(aCommand);
// gLog.AddTime('Scheduled cmd Tick: %d, CMD_TYPE = %s', [Tick, GetEnumName(TypeInfo(TKMGameInputCommandType), Integer(aCommand.CommandType))]);
end;
procedure TKMGameInputProcess_Multi.WaitingForConfirmation(aTick: Cardinal);
begin
//This is a notification that the game is waiting for a tick to be ready
if fNumberConsecutiveWaits < High(fNumberConsecutiveWaits) then
inc(fNumberConsecutiveWaits);
//Mostly unused at the moment, could be used later for e.g. better fDelay calculation.
end;
function TKMGameInputProcess_Multi.GetNetworkDelay: Word;
begin
Result := fDelay;
end;
procedure TKMGameInputProcess_Multi.SetDelay(aNewDelay: Integer);
begin
fDelay := EnsureRange(aNewDelay, MIN_DELAY, MAX_DELAY);
end;
procedure TKMGameInputProcess_Multi.AdjustDelay(aGameSpeed: Single);
begin
// Half of the maximum round trip is a good guess for delay. +1.2 is our safety net to account
// for processing the packet and random variations in ping. It's always better for commands to
// be slightly delayed than for the game to freeze/lag regularly.
if (gNetworking.Room.GetNotDroppedCount = 1) then
SetDelay(MIN_DELAY) //We can set the lowest delay if we are the only MP player
else
SetDelay(Ceil(aGameSpeed * (gNetworking.Room.GetMaxHighestRoundTripLatency / 200 + 1.2)));
end;
procedure TKMGameInputProcess_Multi.SendCommands(aTick: Cardinal; aPlayerIndex: ShortInt = -1);
var
sendStream: TKMemoryStream;
begin
sendStream := TKMemoryStreamBinary.Create;
try
sendStream.Write(Byte(kdpCommands));
// Target Tick in 1..n range
sendStream.Write(aTick);
// Write all commands to the stream
fSchedule[aTick mod MAX_SCHEDULE, gNetworking.MySlotIndex].Save(sendStream);
gNetworking.SendCommands(sendStream, aPlayerIndex); //Send to all players by default
finally
sendStream.Free;
end;
end;
procedure TKMGameInputProcess_Multi.SendRandomCheck(aTick: Cardinal);
var
sendStream: TKMemoryStream;
begin
sendStream := TKMemoryStreamBinary.Create;
try
sendStream.Write(Byte(kdpRandomCheck));
sendStream.Write(aTick); //Target Tick in 1..n range
sendStream.Write(fRandomCheck[aTick mod MAX_SCHEDULE].OurCheck); //Write our random check to the stream
gNetworking.SendCommands(sendStream); //Send to all opponents
finally
sendStream.Free;
end;
end;
procedure TKMGameInputProcess_Multi.DoRandomCheck(aTick: Cardinal; aPlayerIndex: ShortInt);
var
myData, otherData: TKMRngCheckPlayerData;
errorStr: string;
begin
with fRandomCheck[aTick mod MAX_SCHEDULE] do
begin
if OurCheck <> PlayerCheck[aPlayerIndex] then
begin
myData.PlayerIndex := gNetworking.MySlotIndex;
myData.HandID := gNetworking.MyRoomSlot.HandIndex;
myData.Nickname := gNetworking.MyRoomSlot.Nickname;
myData.Check := OurCheck;
otherData.PlayerIndex := aPlayerIndex;
otherData.HandID := gNetworking.Room[aPlayerIndex].HandIndex;
otherData.Nickname := gNetworking.Room[aPlayerIndex].Nickname;
otherData.Check := PlayerCheck[aPlayerIndex];
errorStr := Format(#13#10 + 'Random check mismatch for tick %d processed at tick %d:' + #13#10 +
'MyPlayer: [%s],' + #13#10 +
'OtherPlayer: [%s]',
[aTick, gGameParams.Tick, myData.ToStr, otherData.ToStr]);
gNetworking.AskToSendCrashreport(aPlayerIndex, errorStr);
raise Exception.Create(errorStr);
end;
PlayerCheckPending[aPlayerIndex] := False;
end;
end;
//Decode recieved messages (Commands from other players, Confirmations, Errors)
procedure TKMGameInputProcess_Multi.RecieveCommands(aStream: TKMemoryStream; aSenderIndex: ShortInt);
var
dataType: TKMDataType;
tick: Cardinal;
CRC: Cardinal;
begin
aStream.Read(dataType, 1); //Decode header
aStream.Read(tick); //Target tick
// gLog.AddTime('Received commands for Tick %d', [Tick]);
case dataType of
kdpCommands:
begin
//Recieving commands too late will happen during reconnections, so just ignore it
if (tick > gGameParams.Tick) then
// Do not check if player is dropped - we could receive scheduled commmands from already dropped player,
// that we should store/execute to be in sync with other players
{and not gNetworking.Room[aSenderIndex].Dropped}
begin
fSchedule[tick mod MAX_SCHEDULE, aSenderIndex].Load(aStream);
fRecievedData[tick mod MAX_SCHEDULE, aSenderIndex] := True;
end;
end;
kdpRandomCheck: //Other player is confirming that random seeds matched at a tick in the past
begin
aStream.Read(CRC); //Read the random check from the message
fRandomCheck[tick mod MAX_SCHEDULE].PlayerCheck[aSenderIndex] := CRC; //Store it for this player
fRandomCheck[tick mod MAX_SCHEDULE].PlayerCheckPending[aSenderIndex] := True;
//If we have processed this tick already, check now
if tick <= gGameParams.Tick then
DoRandomCheck(tick, aSenderIndex);
end;
end;
end;
//We must resend the commands from aTick to the last sent tick to the specified player
procedure TKMGameInputProcess_Multi.ResyncFromTick(aSender: ShortInt; aTick: Cardinal);
var
I: Cardinal;
begin
for I := aTick to fLastSentCmdsTick do
SendCommands(I, aSender);
end;
//Are all the commands are confirmed?
function TKMGameInputProcess_Multi.CommandsConfirmed(aTick: Cardinal): Boolean;
var
I: Integer;
begin
Result := True;
for I := 1 to gNetworking.Room.Count do
Result := Result and
(fRecievedData[aTick mod MAX_SCHEDULE, I] or gNetworking.Room[I].NoNeedToWait(aTick));
end;
//Indexes of players we are waiting for
function TKMGameInputProcess_Multi.GetWaitingPlayers(aTick: Cardinal): TKMByteArray;
var
I, K: Integer;
begin
SetLength(Result, MAX_LOBBY_SLOTS);
K := 0;
for I := 1 to gNetworking.Room.Count do
if not (fRecievedData[aTick mod MAX_SCHEDULE, I] or gNetworking.Room[I].NoNeedToWait(aTick)) then
begin
Result[K] := I;
Inc(K);
end;
SetLength(Result, K);
end;
//Timer is called after all commands from player are taken,
//upcoming commands will be stacked into next batch
procedure TKMGameInputProcess_Multi.RunningTimer(aTick: Cardinal);
var
I, K, tick: Cardinal;
begin
inherited;
fNumberConsecutiveWaits := 0; //We are not waiting if the game is running
tick := aTick mod MAX_SCHEDULE; //Place in a ring buffer
fRandomCheck[tick].OurCheck := Cardinal(KaMRandom(MaxInt{$IFDEF DBG_RNG_SPY}, 'TKMGameInputProcess_Multi.RunningTimer'{$ENDIF})); //thats our CRC (must go before commands for replay compatibility)
// Execute commands
for I := 1 to gNetworking.Room.Count do
for K := 1 to fSchedule[tick, I].Count do
begin
// We should store/execute last commands from dropped players too to be in sync with other players,
// that could receive mkDisconnect in other tick, then we do.
// --------------------------------------------------------------------------------
// But we should store/execute commands of dropped players only for a certain ticks
// because dropped players commands will not be cleared in schedule by this player,
// since we call schedule[].Clear only for MyNetPlayer
// So we have to skip those old commands
// (There was a bug, that gicGameSpeed / gicWareDistribution commands from dropped player
// were stored/executed after he left the game every MAX_SCHEDULE ticks,
// because they were stored in the ring buffer of fSchedule)
if (not gNetworking.Room[I].Dropped or gNetworking.Room[I].NeedWaitForLastCommands(aTick))
//Don't allow exploits like moving enemy soldiers (but maybe one day you can control disconnected allies?)
and ((gNetworking.Room[I].HandIndex = fSchedule[tick, I].Items[K].HandIndex)
or (fSchedule[tick, I].Items[K].CommandType in ALLOWED_BY_SPECTATORS)) then
begin
// Store the command first so if Exec fails we still have it in the replay
StoreCommand(fSchedule[tick, I].Items[K]);
ExecCommand(fSchedule[tick, I].Items[K]);
// Returning to the lobby ends the game
if gGame = nil then Exit;
end;
end;
// If we miss a few random checks during reconnections no one cares, inconsistencies will be detected as soon as it is over
// To reduce network load, send random checks once every 10 ticks
if gNetworking.Connected then
if DBG_NET_RANDOM_CHECK_EVERY_TICK or (aTick mod 10 = 1) then
SendRandomCheck(aTick);
// It is possible that we have already recieved other player's random checks, if so check them now
for I := 1 to gNetworking.Room.Count do
begin
if not gNetworking.Room[I].Dropped and fRandomCheck[tick].PlayerCheckPending[I] then
DoRandomCheck(aTick, I);
end;
FillChar(fRecievedData[tick], SizeOf(fRecievedData[tick]), #0); //Reset
fSent[tick] := False;
if aTick mod DELAY_ADJUST = 0 then
AdjustDelay(gGame.SpeedActual); //Adjust fDelay every X ticks
end;
procedure TKMGameInputProcess_Multi.UpdateState(aTick: Cardinal);
var
I: Integer;
begin
inherited;
for I := aTick + 1 to aTick + fDelay do
//If the network is not connected then we must send the commands later (fSent will remain False)
if (not fSent[I mod MAX_SCHEDULE]) and gNetworking.Connected
and (gNetworking.NetGameState = lgsGame) then //Don't send commands unless game is running normally
begin
if not fCommandIssued[I mod MAX_SCHEDULE] then
// No one has used it since last time through the ring buffer
fSchedule[I mod MAX_SCHEDULE, gNetworking.MySlotIndex].Clear;
fCommandIssued[I mod MAX_SCHEDULE] := False; //Make it as requiring clearing next time around
fLastSentCmdsTick := I;
SendCommands(I);
// gLog.AddTime('fDelay = %d; Send Commands for Tick = %d', [fDelay, I]);
fSent[I mod MAX_SCHEDULE] := True;
fRecievedData[I mod MAX_SCHEDULE, gNetworking.MySlotIndex] := True; //Recieved commands from self
end;
end;
end.
@@ -0,0 +1,108 @@
unit KM_GameInputProcess_Single;
{$I KaM_Remake.inc}
interface
uses
KM_CommonClasses, KM_GameInputProcess;
type
TKMGameInputProcess_Single = class(TKMGameInputProcess)
protected
procedure DoTakeCommand(const aCommand: TKMGameInputCommand); override;
procedure SaveExtra(SaveStream: TKMemoryStream); override;
procedure LoadExtra(LoadStream: TKMemoryStream); override;
public
procedure ReplayTimer(aTick: Cardinal); override;
procedure RunningTimer(aTick: Cardinal); override;
end;
implementation
uses
Math, KM_Game, KM_GameParams, KM_Defaults, KM_CommonUtils;
procedure TKMGameInputProcess_Single.DoTakeCommand(const aCommand: TKMGameInputCommand);
begin
if gGameParams.IsReplay then Exit;
StoreCommand(aCommand); //Store the command for the replay (store it first in case Exec crashes and we want to debug it)
ExecCommand(aCommand); //Execute the command now
end;
procedure TKMGameInputProcess_Single.ReplayTimer(aTick: Cardinal);
var
myRand: Cardinal;
gicCommand: TKMGameInputCommand;
begin
//This is to match up with multiplayer random check generation, so multiplayer replays can be replayed in singleplayer mode
KaMRandom(MaxInt{$IFDEF DBG_RNG_SPY}, 'TKMGameInputProcess_Single.ReplayTimer'{$ENDIF});
//There are still more commands left
if fCursor <= Count then
begin
while (aTick > fQueue[fCursor].Tick) and (fQueue[fCursor].Command.CmdType <> gicNone) and (fCursor < Count) do
Inc(fCursor);
while (fCursor <= Count) and (aTick = fQueue[fCursor].Tick) do //Could be several commands in one Tick
begin
//Call to KaMRandom, just like in StoreCommand
//We did not generate random checks for those commands
if SKIP_RNG_CHECKS_FOR_SOME_GIC and (fQueue[fCursor].Command.CmdType in SKIP_RANDOM_CHECKS_FOR) then
myRand := 0
else
myRand := Cardinal(KaMRandom(MaxInt{$IFDEF DBG_RNG_SPY}, 'TKMGameInputProcess_Single.ReplayTimer 2'{$ENDIF}));
while not fGic2StoredConverter.ParseNextStoredPackedCommand(fQueue[fCursor].Command, gicCommand) do
Inc(fCursor);
ExecCommand(gicCommand); // Should always be called to maintain randoms flow
// CRC check after the command
if (fQueue[fCursor].Rand <> myRand)
and not gGame.IgnoreConsistencyCheckErrors then
begin
if Assigned(fOnReplayDesync) then // Call before ReplayInconsistency, fOnReplayDesync could be free after it!
fOnReplayDesync(fCursor);
if CRASH_ON_REPLAY then
begin
Inc(fCursor); // Must be done before exiting in case user decides to continue the replay
gGame.ReplayInconsistency(fQueue[fCursor-1], myRand);
Exit; // ReplayInconsistency sometimes calls GIP.Free, so exit immediately
end;
Exit;
end;
Inc(fCursor);
end;
end;
end;
procedure TKMGameInputProcess_Single.RunningTimer(aTick: Cardinal);
begin
inherited;
// This is to match up with multiplayer CRC generation, so multiplayer replays can be replayed in singleplayer mode
KaMRandom(MaxInt{$IFDEF DBG_RNG_SPY}, 'TKMGameInputProcess_Single.RunningTimer'{$ENDIF});
end;
procedure TKMGameInputProcess_Single.SaveExtra(SaveStream: TKMemoryStream);
begin
SaveStream.Write(gGame.LastReplayTickLocal);
end;
procedure TKMGameInputProcess_Single.LoadExtra(LoadStream: TKMemoryStream);
var
lastReplayTick: Cardinal;
begin
LoadStream.Read(lastReplayTick);
gGame.LastReplayTickLocal := lastReplayTick;
end;
end.
+66
View File
@@ -0,0 +1,66 @@
unit KM_GameUIDTracker;
interface
uses
KM_CommonClasses;
type
// Units-Houses tracker, to issue unique IDs
TKMGameUIDTracker = class
private
fUIDTracker: Integer;
public
constructor Create;
procedure Load(LoadStream: TKMemoryStream);
procedure Save(SaveStream: TKMemoryStream);
function GetNewUID: Integer;
end;
var
gUIDTracker: TKMGameUIDTracker;
implementation
{ TKMGameUIDTracker }
constructor TKMGameUIDTracker.Create;
begin
inherited;
fUIDTracker := 0;
end;
procedure TKMGameUIDTracker.Load(LoadStream: TKMemoryStream);
begin
LoadStream.CheckMarker('UIDTracker');
LoadStream.Read(fUIDTracker);
end;
procedure TKMGameUIDTracker.Save(SaveStream: TKMemoryStream);
begin
SaveStream.PlaceMarker('UIDTracker');
SaveStream.Write(fUIDTracker);
end;
function TKMGameUIDTracker.GetNewUID: Integer;
const
//Prime numbers let us generate sequence of non-repeating values of max_value length
MAX_VALUE = 16777213;
STEP = 8765423;
begin
//UIDs have the following properties:
// - allow -1 to indicate no UID (const UID_NONE = -1)
// - fit within 24bit (we can use that much for RGB colorcoding in unit picking)
// - Start from 1, so that black colorcode can be detected in render and then re-mapped to -1
fUIDTracker := (fUIDTracker + STEP) mod MAX_VALUE + 1; //1..N range, 0 is nothing for colorpicker
Result := fUIDTracker;
end;
end.
+592
View File
@@ -0,0 +1,592 @@
unit KM_RandomChecks;
{$I KaM_Remake.inc}
interface
uses
{$IFDEF FPC}zstream, {$ENDIF}
{$IFDEF WDC}ZLib, {$ENDIF}
Generics.Collections,
KM_CommonClasses
{$IF DEFINED(WDC) OR (FPC_FULLVERSION >= 30200)}, KM_WorkerThread{$ENDIF};
type
TKMLogRngType = (lrtNone, lrtInt, lrtSingle, lrtExt);
TKMRngLogRecord = record
ValueType: TKMLogRngType;
Seed: Integer;
ValueI: Integer;
ValueS: Single;
ValueE: Extended;
CallerId: Byte;
end;
TKMRLRecordList = TList<TKMRngLogRecord>;
// Logger for random check calls
TKMRandomCheckLogger = class
private
fEnabled: Boolean;
fGameTick: Cardinal;
fSavedTicksCnt: Cardinal;
fRngChecksInTick: TKMRLRecordList;
// fSaveStream: TKMemoryStream;
// fRngLogStream: TKMemoryStream;
fCallers: TDictionary<Byte, AnsiString>;
fRngLog: TDictionary<Cardinal, TKMRLRecordList>;
fTickStreamQueue: TObjectQueue<TKMemoryStream>;
function IsEnabled: Boolean;
function GetCallerID(const aCaller: AnsiString; aValue: Extended; aValueType: TKMLogRngType): Byte;
procedure AddRecordToList(aTick: Cardinal; const aRec: TKMRngLogRecord);
procedure AddRecordToDict(aTick: Cardinal; const aRec: TKMRngLogRecord);
procedure LoadFromStreamAndParseToDict(aLoadStream: TKMemoryStream);
procedure SaveTickToStream(aStream: TKMemoryStream; aRngChecksInTick: TKMRLRecordList);
procedure LoadHeader(aLoadStream: TKMemoryStream);
// procedure ParseSaveStream;
procedure ParseStreamToDict(aLoadStream: TKMemoryStream);
function GetEnabled: Boolean;
public
constructor Create;
destructor Destroy; override;
procedure AddToLog(const aCaller: AnsiString; aValue: Integer; aSeed: Integer); overload;
procedure AddToLog(const aCaller: AnsiString; aValue: Single; aSeed: Integer); overload;
procedure AddToLog(const aCaller: AnsiString; aValue: Extended; aSeed: Integer); overload;
// property RngLogStream: TKMemoryStream read fRngLogStream;
property Enabled: Boolean read GetEnabled write fEnabled;
{$IF DEFINED(WDC) OR (FPC_FULLVERSION >= 30200)}
procedure SaveToPathAsync(const aPath: String; aWorkerThread: TKMWorkerThread);
{$ENDIF}
// procedure ParseSaveStreamAndSaveAsText(aPath: String);
procedure SaveAsText(const aPath: String);
procedure LoadFromPath(const aPath: String);
procedure LoadFromPathAndParseToDict(const aPath: String);
procedure Clear;
procedure UpdateState(aGameTick: Cardinal);
end;
var
gRandomCheckLogger: TKMRandomCheckLogger;
implementation
uses
Classes, SysUtils, Math,
KM_GameSettings,
KM_Log, KM_WorkerThreadUtils;
var
MAX_TICKS_CNT: Integer = 5*60*10; // 5 minutes
{ TKMRandomLogger }
constructor TKMRandomCheckLogger.Create;
begin
// fRngLogStream := TKMemoryStream.Create;
fCallers := TDictionary<Byte, AnsiString>.Create;
fRngLog := TDictionary<Cardinal, TKMRLRecordList>.Create;
fTickStreamQueue := TObjectQueue<TKMemoryStream>.Create;
fTickStreamQueue.OwnsObjects := True; // Set the OwnsObjects to True - the Queue will free them automatically
fRngChecksInTick := TKMRLRecordList.Create;
fSavedTicksCnt := 0;
fEnabled := True;
// fSaveStream := TKMemoryStreamBinary.Create;
end;
destructor TKMRandomCheckLogger.Destroy;
begin
Clear;
FreeAndNil(fRngLog);
FreeAndNil(fCallers);
FreeAndNil(fRngChecksInTick);
FreeAndNil(fTickStreamQueue);
// FreeAndNil(fRngLogStream);
// FreeAndNil(fSaveStream);
inherited;
end;
function TKMRandomCheckLogger.IsEnabled: Boolean;
begin
Result := (Self <> nil) and fEnabled and gGameSettings.DebugSaveRandomChecks;
end;
function TKMRandomCheckLogger.GetCallerID(const aCaller: AnsiString; aValue: Extended; aValueType: TKMLogRngType): Byte;
var
callerPair: TPair<Byte, AnsiString>;
begin
for callerPair in fCallers do
begin
if callerPair.Value = aCaller then
Exit(callerPair.Key);
end;
Result := fCallers.Count;
fCallers.Add(Result, aCaller);
end;
function TKMRandomCheckLogger.GetEnabled: Boolean;
begin
if Self = nil then Exit(False);
Result := fEnabled;
end;
procedure TKMRandomCheckLogger.AddToLog(const aCaller: AnsiString; aValue: Integer; aSeed: Integer);
var
rec: TKMRngLogRecord;
begin
if not IsEnabled then Exit;
rec.Seed := aSeed;
rec.ValueType := lrtInt;
rec.ValueI := aValue;
rec.CallerId := GetCallerID(aCaller, aValue, lrtInt);
AddRecordToList(fGameTick, rec);
end;
procedure TKMRandomCheckLogger.AddToLog(const aCaller: AnsiString; aValue: Single; aSeed: Integer);
var
rec: TKMRngLogRecord;
begin
if not IsEnabled then Exit;
rec.Seed := aSeed;
rec.ValueType := lrtSingle;
rec.ValueS := aValue;
rec.CallerId := GetCallerID(aCaller, aValue, lrtInt);
AddRecordToList(fGameTick, rec);
end;
procedure TKMRandomCheckLogger.AddToLog(const aCaller: AnsiString; aValue: Extended; aSeed: Integer);
var
rec: TKMRngLogRecord;
begin
if not IsEnabled then Exit;
rec.Seed := aSeed;
rec.ValueType := lrtExt;
rec.ValueE := aValue;
rec.CallerId := GetCallerID(aCaller, aValue, lrtInt);
AddRecordToList(fGameTick, rec);
end;
procedure TKMRandomCheckLogger.AddRecordToDict(aTick: Cardinal; const aRec: TKMRngLogRecord);
var
list: TList<TKMRngLogRecord>;
begin
if not IsEnabled then Exit;
if not fRngLog.TryGetValue(aTick, list) then
begin
list := TList<TKMRngLogRecord>.Create;
fRngLog.Add(aTick, list);
end;
list.Add(aRec);
end;
procedure TKMRandomCheckLogger.AddRecordToList(aTick: Cardinal; const aRec: TKMRngLogRecord);
begin
if not IsEnabled then Exit;
fRngChecksInTick.Add(aRec);
end;
procedure TKMRandomCheckLogger.SaveTickToStream(aStream: TKMemoryStream; aRngChecksInTick: TKMRLRecordList);
var
I: Integer;
rngValueType: TKMLogRngType;
begin
if not IsEnabled then Exit;
aStream.Write(fGameTick); //Tick
aStream.Write(Integer(aRngChecksInTick.Count)); //Number of log records in tick
// Inc(Cnt, fRngChecksInTick.Count);
for I := 0 to aRngChecksInTick.Count - 1 do
begin
aStream.Write(aRngChecksInTick[I].Seed);
rngValueType := aRngChecksInTick[I].ValueType;
aStream.Write(rngValueType, SizeOf(rngValueType));
aStream.Write(aRngChecksInTick[I].CallerId);
case rngValueType of
lrtInt: aStream.Write(aRngChecksInTick[I].ValueI);
lrtSingle: aStream.Write(aRngChecksInTick[I].ValueS);
lrtExt: aStream.Write(aRngChecksInTick[I].ValueE);
end;
end;
end;
procedure TKMRandomCheckLogger.UpdateState(aGameTick: Cardinal);
var
tickStream: TKMemoryStream;
begin
if not IsEnabled then Exit;
fGameTick := aGameTick;
// Delete oldest stream object from queue
if fTickStreamQueue.Count >= MAX_TICKS_CNT then
begin
fTickStreamQueue.Dequeue; // Will also automatically free an object, because of OwnObjects property
fTickStreamQueue.TrimExcess;
end;
tickStream := TKMemoryStreamBinary.Create;
SaveTickToStream(tickStream, fRngChecksInTick);
fTickStreamQueue.Enqueue(tickStream);
fRngChecksInTick.Clear;
fSavedTicksCnt := fTickStreamQueue.Count;
end;
procedure TKMRandomCheckLogger.LoadHeader(aLoadStream: TKMemoryStream);
var
I, Count: Integer;
callerId: Byte;
callerName: AnsiString;
begin
if (Self = nil) then Exit;
aLoadStream.CheckMarker('CallersTable');
aLoadStream.Read(Count);
for I := 0 to Count - 1 do
begin
aLoadStream.Read(callerId);
aLoadStream.ReadA(callerName);
fCallers.Add(callerId, callerName);
end;
aLoadStream.CheckMarker('KaMRandom_calls');
aLoadStream.Read(fSavedTicksCnt);
end;
procedure TKMRandomCheckLogger.LoadFromPath(const aPath: String);
var
I: Integer;
tickStreamSize: Cardinal;
loadStream, tickStream: TKMemoryStream;
begin
if Self = nil then Exit;
if not FileExists(aPath) then
begin
gLog.AddTime('RandomsChecks file ''' + aPath + ''' was not found. Skip load rng');
Exit;
end;
Clear;
loadStream := TKMemoryStreamBinary.Create;
try
loadStream.LoadFromFileCompressed(aPath, 'RNGCompressed');
LoadHeader(loadStream);
for I := 0 to fSavedTicksCnt - 1 do
begin
tickStream := TKMemoryStreamBinary.Create;
loadStream.Read(tickStreamSize);
tickStream.CopyFrom(loadStream, tickStreamSize);
fTickStreamQueue.Enqueue(tickStream);
end;
// fSaveStream.CopyFrom(LoadStream, LoadStream.Size - LoadStream.Position);
finally
loadStream.Free;
end;
end;
procedure TKMRandomCheckLogger.LoadFromStreamAndParseToDict(aLoadStream: TKMemoryStream);
begin
Clear;
LoadHeader(aLoadStream);
ParseStreamToDict(aLoadStream);
end;
procedure TKMRandomCheckLogger.LoadFromPathAndParseToDict(const aPath: String);
var
loadStream: TKMemoryStream;
begin
if Self = nil then Exit;
if not FileExists(aPath) then
begin
gLog.AddTime('RandomsChecks file ''' + aPath + ''' was not found. Skip load rng');
Exit;
end;
loadStream := TKMemoryStreamBinary.Create;
try
loadStream.LoadFromFileCompressed(aPath, 'RNGCompressed');
LoadFromStreamAndParseToDict(loadStream);
finally
loadStream.Free;
end;
end;
//procedure TKMRandomCheckLogger.ParseSaveStream;
//var
// ReadStream: TKMemoryStream;
//begin
// ReadStream := TKMemoryStreamBinary.Create;
// try
// ReadStream.CopyFrom(fSaveStream, 0);
// ReadStream.Position := 0;
// ParseStreamToDict(ReadStream);
// finally
// ReadStream.Free;
// end;
//end;
procedure TKMRandomCheckLogger.ParseStreamToDict(aLoadStream: TKMemoryStream);
var
logRec: TKMRngLogRecord;
procedure ClearLogRec;
begin
//@Rey: Consider using LogRec := default(TKMRngLogRecord);
logRec.ValueI := 0;
logRec.ValueS := 0;
logRec.ValueE := 0;
logRec.CallerId := 0;
logRec.ValueType := lrtNone;
end;
var
I, K, countInTick: Integer;
tick, tickStreamSize: Cardinal;
begin
for I := 0 to fSavedTicksCnt - 1 do
begin
aLoadStream.Read(tickStreamSize); // load tick stream size and omit it, we don't use it here
aLoadStream.Read(tick);
aLoadStream.Read(countInTick);
for K := 0 to countInTick - 1 do
begin
ClearLogRec;
aLoadStream.Read(logRec.Seed);
aLoadStream.Read(logRec.ValueType, SizeOf(logRec.ValueType));
aLoadStream.Read(logRec.CallerId);
case logRec.ValueType of
lrtInt: aLoadStream.Read(logRec.ValueI);
lrtSingle: aLoadStream.Read(logRec.ValueS);
lrtExt: aLoadStream.Read(logRec.ValueE);
end;
AddRecordToDict(tick, logRec);
end;
end;
end;
//procedure TKMRandomCheckLogger.ParseSaveStreamAndSaveAsText(aPath: String);
//begin
// ParseSaveStream;
// SaveAsText(aPath);
//end;
{$IF DEFINED(WDC) OR (FPC_FULLVERSION >= 30200)}
procedure TKMRandomCheckLogger.SaveToPathAsync(const aPath: String; aWorkerThread: TKMWorkerThread);
var
saveStream, tickStream: TKMemoryStream;
// CompressionStream: TCompressionStream;
callerPair: TPair<Byte, AnsiString>;
enumerator: TEnumerator<TKMemoryStream>;
begin
if (Self = nil) then Exit;
saveStream := TKMemoryStreamBinary.Create;
// Allocate memory for save stream, could save up to 25% of save time
saveStream.SetSize(fTickStreamQueue.Count * 2 * 1024); // on 1 hour game *1024 is usually enough
saveStream.PlaceMarker('CallersTable');
saveStream.Write(Integer(fCallers.Count));
for callerPair in fCallers do
begin
saveStream.Write(callerPair.Key);
saveStream.WriteA(callerPair.Value);
end;
saveStream.PlaceMarker('KaMRandom_calls');
saveStream.Write(Integer(fSavedTicksCnt));
enumerator := fTickStreamQueue.GetEnumerator;
while enumerator.MoveNext do
begin
tickStream := enumerator.Current;
saveStream.Write(Cardinal(tickStream.Size));
saveStream.CopyFrom(tickStream, 0);
end;
enumerator.Free;
saveStream.TrimToPosition;
// SaveStream.CopyFrom(fSaveStream, 0);
// for LogPair in fRngLog do
// begin
// SaveTickToStream(SaveStream, LogPair.Value);
// Inc(Cnt, LogPair.Value.Count);
// end;
// SaveStream.WriteA('Total COUNT = ');
// SaveStream.WriteA(AnsiString(IntToStr(Cnt)));
// CompressionStream := TCompressionStream.Create(clNone, SaveStream);
// CompressionStream.CopyFrom(fRngLogStream, 0);
//SaveStream now contains the compressed data from SourceStream
// CompressionStream.Free;
AsyncSaveToFileCompressedAndFree(saveStream, aPath, 'RNGCompressed', aWorkerThread);
end;
{$ENDIF}
procedure TKMRandomCheckLogger.SaveAsText(const aPath: String);
const
MAX_SL_COUNT = 1024*1024; // Avoid huge files and OOM errors
var
I, cnt, K: Integer;
keyTick: Cardinal;
SL: TStringList;
S, valS: String;
callersIdList: TList<Byte>;
callerId: Byte;
logTicksList: TList<Cardinal>;
logRecList: TList<TKMRngLogRecord>;
begin
if Self = nil then Exit;
cnt := 0;
K := 0;
SL := TStringList.Create;
try
callersIdList := TList<Byte>.Create(fCallers.Keys);
try
SL.Add('Callers: ' + IntToStr(callersIdList.Count));
callersIdList.Sort;
for callerId in callersIdList do
SL.Add(Format('%d - %s', [callerId, fCallers[callerId]]));
finally
callersIdList.Free;
end;
logTicksList := TList<Cardinal>.Create(fRngLog.Keys);
try
SL.Add('LogRngRecords: ' + IntToStr(logTicksList.Count) + ' ticks');
logTicksList.Sort;
for keyTick in logTicksList do
begin
logRecList := fRngLog[keyTick];
SL.Add(Format('Tick: %d, Tick Rng Count: %d', [keyTick, logRecList.Count]));
Inc(cnt, logRecList.Count);
for I := 0 to logRecList.Count - 1 do
begin
valS := 'NaN';
case logRecList[I].ValueType of
lrtInt: valS := 'I ' + IntToStr(logRecList[I].ValueI);
lrtSingle: valS := 'S ' + FormatFloat('0.##############################', logRecList[I].ValueS);
lrtExt: valS := 'E ' + FormatFloat('0.##############################', logRecList[I].ValueE);
end;
S := Format('%3d. %-50sSeed: %12d Val: %s', [I, fCallers[logRecList[I].CallerId], logRecList[I].Seed, valS]);
SL.Add(S);
end;
if SL.Count > MAX_SL_COUNT then
begin
SL.SaveToFile(aPath + IntToStr(K));
SL.Clear;
Inc(K);
end;
end;
finally
logTicksList.Free;
end;
SL.Add('Total randomchecks count = ' + IntToStr(cnt));
if K = 0 then
SL.SaveToFile(aPath)
else
SL.SaveToFile(aPath + IntToStr(K));
finally
SL.Free;
end;
end;
procedure TKMRandomCheckLogger.Clear;
var
list: TList<TKMRngLogRecord>;
// TickStream: TKMemoryStream;
// enumerator: TEnumerator<TKMemoryStreamBinary>;
begin
if Self = nil then Exit;
fCallers.Clear;
fCallers.TrimExcess;
// fSaveStream.Clear;
// enumerator := fTickStreamQueue.GetEnumerator;
//
// while enumerator.MoveNext do
// begin
// TickStream := enumerator.Current;
// TickStream.Free;
// end;
fTickStreamQueue.Clear;
fTickStreamQueue.TrimExcess;
for list in fRngLog.Values do
list.Free;
// fRngLog.Clear; // It seems no need to clear, FastMM says there is memory leak because of it
fRngLog.TrimExcess;
fEnabled := True;
end;
end.
+430
View File
@@ -0,0 +1,430 @@
unit KM_Alerts;
{$I KaM_Remake.inc}
interface
uses
Classes, Generics.Collections,
KM_Pics, KM_Viewport, KM_ResSound,
KM_Defaults, KM_Points;
type
TAlertType = (atBeacon, atFight);
const
ALERT_DURATION: array [TAlertType] of Byte = (80, 50); //Typical beacon duration after which it will be gone
type
TKMAlert = class
private
fAlertType: TAlertType;
fExpiration: Cardinal;
fLoc: TKMPointF;
fOwner: TKMHandID;
protected
function GetTexMinimap: TKMPic; virtual; abstract;
function GetTexTerrain: TKMPic; virtual; abstract;
function GetTeamColor: Cardinal; virtual; abstract;
function GetVisibleMinimap: Boolean; virtual; abstract;
function GetVisibleTerrain: Boolean; virtual; abstract;
public
constructor Create(aAlertType: TAlertType; aLoc: TKMPointF; aOwner: TKMHandID; aExpiry: Cardinal);
property AlertType: TAlertType read fAlertType;
property Loc: TKMPointF read fLoc;
property Owner: TKMHandID read fOwner;
property Expiration: Cardinal read fExpiration;
property TexMinimap: TKMPic read GetTexMinimap;
property TexTerrain: TKMPic read GetTexTerrain;
property TeamColor: Cardinal read GetTeamColor;
property VisibleMinimap: Boolean read GetVisibleMinimap;
property VisibleTerrain: Boolean read GetVisibleTerrain;
function IsExpired(aTick: Cardinal): Boolean;
procedure Update(const aView: TKMRect); virtual;
end;
//Alerts are signals in game that draw Players attention to something,
//Unlike simple messages that are fired-and-forget, alerts do have a lifespan
//and some interaction with e.g. Viewport
//Alerts are not saved between game sessions because by nature they are short
//lived and last only a few seconds
TKMAlerts = class
private
fViewport: TKMViewport;
fList: TList<TKMAlert>;
function GetAlert(aIndex: Integer): TKMAlert;
function GetCount: Integer;
public
constructor Create(aViewport: TKMViewport);
destructor Destroy; override;
procedure ClearBeaconsExcept(aOwner: TKMHandID);
procedure AddBeacon(const aLoc: TKMPointF; aOwner: TKMHandID; aColor: Cardinal; aShowUntil: Cardinal);
procedure AddFight(const aLoc: TKMPointF; aPlayer: TKMHandID; aAsset: TKMAttackNotification; aShowUntil: Cardinal);
function GetLatestAlert: TKMAlert;
property Count: Integer read GetCount;
property Items[aIndex: Integer]: TKMAlert read GetAlert; default;
procedure Paint(aPass: Byte);
procedure UpdateState(aTickCount: Cardinal);
end;
implementation
uses
KM_HandsCollection, KM_Hand,
KM_RenderPool,
KM_Sound,
KM_ResTypes;
type
//These classes are only accessed locally, hence they aren't interfaced
TKMAlertBeacon = class(TKMAlert)
private
fFlagColor: Cardinal; //Spectators don't have Hand so fOwner isn't enough
protected
function GetTexMinimap: TKMPic; override;
function GetTexTerrain: TKMPic; override;
function GetTeamColor: Cardinal; override;
function GetVisibleMinimap: Boolean; override;
function GetVisibleTerrain: Boolean; override;
public
constructor Create(const aLoc: TKMPointF; aOwner: TKMHandID; aColor: Cardinal; aExpiry: Cardinal);
end;
TKMAlertAttacked = class(TKMAlert)
private
fAsset: TKMAttackNotification; // What was attacked
fLastLookedAt: Byte;
protected
function GetTexMinimap: TKMPic; override;
function GetTexTerrain: TKMPic; override;
function GetTeamColor: Cardinal; override;
function GetVisibleMinimap: Boolean; override;
function GetVisibleTerrain: Boolean; override;
public
constructor Create(const aLoc: TKMPointF; aOwner: TKMHandID; aAsset: TKMAttackNotification; aExpiry: Cardinal);
property Asset: TKMAttackNotification read fAsset;
procedure Refresh(aExpiry: Cardinal);
procedure Update(const aView: TKMRect); override;
end;
const
FIGHT_DISTANCE = 12; //Fights this far apart are treated as separate
INTERVAL_ATTACKED_MSG = 20; //Time between audio messages saying you are being attacked
MAX_BEACONS = 5; //Maximum number of simultanious beacons per player
{ TKMAlert }
constructor TKMAlert.Create(aAlertType: TAlertType; aLoc: TKMPointF; aOwner: TKMHandID; aExpiry: Cardinal);
begin
inherited Create;
fAlertType := aAlertType;
fLoc := aLoc;
fOwner := aOwner;
fExpiration := aExpiry;
end;
function TKMAlert.IsExpired(aTick: Cardinal): Boolean;
begin
Result := aTick >= fExpiration;
end;
procedure TKMAlert.Update(const aView: TKMRect);
begin
//
end;
{ TKMAlertBeacon }
constructor TKMAlertBeacon.Create(const aLoc: TKMPointF; aOwner: TKMHandID; aColor: Cardinal; aExpiry: Cardinal);
begin
inherited Create(atBeacon, aLoc, aOwner, aExpiry);
// Owner and Color became separate because Spectators set beacons under HAND_NONE, but still in their color
fFlagColor := aColor;
end;
function TKMAlertBeacon.GetTexMinimap: TKMPic;
begin
Result.RX := rxGui;
Result.ID := 54;
end;
function TKMAlertBeacon.GetTexTerrain: TKMPic;
begin
Result.RX := rxGui;
Result.ID := 250; //Located near the house tablets so we can include it in soft shadows range
end;
function TKMAlertBeacon.GetTeamColor: Cardinal;
begin
Result := fFlagColor;
end;
function TKMAlertBeacon.GetVisibleMinimap: Boolean;
begin
//Beacons placed by player are always visible until expired
Result := True;
end;
function TKMAlertBeacon.GetVisibleTerrain: Boolean;
begin
Result := True;
end;
{ TKMAlertFight }
constructor TKMAlertAttacked.Create(const aLoc: TKMPointF; aOwner: TKMHandID; aAsset: TKMAttackNotification; aExpiry: Cardinal);
begin
inherited Create(atFight, aLoc, aOwner, aExpiry);
fAsset := aAsset;
fLastLookedAt := High(Byte) - 1;
end;
function TKMAlertAttacked.GetTexMinimap: TKMPic;
begin
Result.RX := rxGui;
Result.ID := 53;
end;
function TKMAlertAttacked.GetTexTerrain: TKMPic;
begin
//Has no sprite on terrain
Result.RX := rxTrees;
Result.ID := 0;
end;
function TKMAlertAttacked.GetTeamColor: Cardinal;
begin
Result := gHands[fOwner].FlagColor;
end;
function TKMAlertAttacked.GetVisibleMinimap: Boolean;
begin
//UnderAttack alerts are visible only when not looked at
//When looked at alert is muted for the next 20sec
Result := fLastLookedAt >= INTERVAL_ATTACKED_MSG;
end;
function TKMAlertAttacked.GetVisibleTerrain: Boolean;
begin
Result := False;
end;
procedure TKMAlertAttacked.Refresh(aExpiry: Cardinal);
begin
fExpiration := aExpiry;
end;
procedure TKMAlertAttacked.Update(const aView: TKMRect);
begin
inherited;
//If alerts gets into view - mute it.
//Alert will be unmuted when going out of view
if KMInRect(fLoc, aView) then
fLastLookedAt := 0
else
begin
Inc(fLastLookedAt);
//Alert is automatically repeated
if fLastLookedAt >= INTERVAL_ATTACKED_MSG * 2 then
fLastLookedAt := INTERVAL_ATTACKED_MSG;
//Make the sound
if (fOwner = gMySpectator.HandID)
and (fLastLookedAt = INTERVAL_ATTACKED_MSG) then
gSoundPlayer.PlayNotification(fAsset);
end;
end;
{ TKMAlerts }
constructor TKMAlerts.Create(aViewport: TKMViewport);
begin
inherited Create;
fViewport := aViewport;
fList := TList<TKMAlert>.Create;
end;
destructor TKMAlerts.Destroy;
var
I: Integer;
begin
for I := 0 to fList.Count - 1 do
fList[I].Free;
fList.Free;
inherited;
end;
function TKMAlerts.GetAlert(aIndex: Integer): TKMAlert;
begin
Result := fList[aIndex];
end;
function TKMAlerts.GetCount: Integer;
begin
Result := fList.Count;
end;
//Ally has placed a beacon for ue
procedure TKMAlerts.AddBeacon(const aLoc: TKMPointF; aOwner: TKMHandID; aColor: Cardinal; aShowUntil: Cardinal);
procedure RemoveExcessBeacons;
var
I, oldestID, qty: Integer;
oldestExpiry: Cardinal;
begin
qty := 0;
oldestID := -1;
oldestExpiry := 0;
for I := 0 to fList.Count - 1 do
if (fList[I].AlertType = atBeacon)
and (fList[I].Owner = aOwner) then
begin
Inc(qty);
if (oldestID = -1) or (fList[I].fExpiration < oldestExpiry) then
begin
oldestExpiry := fList[I].fExpiration;
oldestID := I;
end;
end;
if (qty > MAX_BEACONS) and (oldestID <> -1) then
begin
fList[oldestID].Free; // We probably should use TObjectList, tand skip manual list item freeing
fList.Delete(oldestID);
end;
end;
begin
//If this player has too many beacons remove his oldest one
RemoveExcessBeacons;
fList.Add(TKMAlertBeacon.Create(aLoc, aOwner, aColor, aShowUntil));
gSoundPlayer.Play(sfxnBeacon);
end;
//Clear all beacons except those, whose owner is aOwner
procedure TKMAlerts.ClearBeaconsExcept(aOwner: TKMHandID);
var
I: Integer;
begin
for I := fList.Count - 1 downto 0 do
if (fList[I].AlertType = atBeacon)
and (fList[I].Owner <> aOwner) then
begin
fList[I].Free; // We probably should use TObjectList, tand skip manual list item freeing
fList.Delete(I);
end;
end;
//Player belongings signal that they are under attack
procedure TKMAlerts.AddFight(const aLoc: TKMPointF; aPlayer: TKMHandID; aAsset: TKMAttackNotification; aShowUntil: Cardinal);
var
I: Integer;
begin
//Check previous alerts and see if there's one like that already
for I := 0 to fList.Count - 1 do
if fList[I] is TKMAlertAttacked then
with TKMAlertAttacked(fList[I]) do
if (Owner = aPlayer) and (Asset = aAsset)
and (KMLength(Loc, aLoc) < FIGHT_DISTANCE) then
begin
Refresh(aShowUntil);
Exit;
end;
//Otherwise create a new alert
fList.Add(TKMAlertAttacked.Create(aLoc, aPlayer, aAsset, aShowUntil));
end;
function TKMAlerts.GetLatestAlert: TKMAlert;
var
I: Integer;
best: Cardinal;
begin
Result := nil;
best := 0;
for I := 0 to fList.Count - 1 do
if fList[I].GetVisibleMinimap then
if (Result = nil) or (fList[I].Expiration >= best) then
begin
Result := fList[I];
best := fList[I].Expiration;
end;
end;
//We want beacons to be Z sorted on explored area, but always on top in FOW areas
//so that beacon is not occluded by houses in FOW (which we need to render to show
//their rooftops from under the FOW) to block seeing enemy town silhouettes
//Rendered in 2 passes
//1 - beacon amid other sprites
//2 - if in FOW render always
procedure TKMAlerts.Paint(aPass: Byte);
var
I: Integer;
R: TKMRect;
begin
R := KMRectGrowNoLimits(fViewport.GetMinimapClip, 4); //Beacons may stick up over a few tiles
for I := 0 to fList.Count - 1 do
if fList[I].VisibleTerrain
and KMInRect(fList[I].Loc, R) then
begin
case aPass of
0: if gMySpectator.FogOfWar.CheckRevelation(fList[I].Loc) > 0 then
gRenderPool.AddAlert(fList[I].Loc, fList[I].TexTerrain.ID, fList[I].TeamColor);
1: if gMySpectator.FogOfWar.CheckRevelation(fList[I].Loc) < FOG_OF_WAR_MAX then
gRenderPool.RenderSpriteOnTerrain(fList[I].Loc, fList[I].TexTerrain.ID, fList[I].TeamColor, True);
end;
end;
end;
procedure TKMAlerts.UpdateState(aTickCount: Cardinal);
var
I: Integer;
begin
//Update alerts visibility
if (aTickCount mod 10 = 0) then
for I := fList.Count - 1 downto 0 do
fList[I].Update(fViewport.GetMinimapClip);
//Remove expired alerts
for I := fList.Count - 1 downto 0 do
if fList[I].IsExpired(aTickCount) then
begin
fList[I].Free; // We probably should use TObjectList, tand skip manual list item freeing
fList.Delete(I);
end;
end;
end.
@@ -0,0 +1,188 @@
unit KM_MessageLog;
{$I KaM_Remake.inc}
interface
uses
KM_CommonClasses, KM_GameTypes, KM_Points;
type
//Individual message
TKMLogMessage = class
private
fKind: TKMMessageKind;
fEntityUID: Integer;
fLoc: TKMPoint;
fTextID: Integer;
fIsReadGIP: Boolean; //This is synced through GIP
fIsReadLocal: Boolean; //This is used locally so it responds instantly
public
constructor Create(aKind: TKMMessageKind; aTextID: Integer; const aLoc: TKMPoint; aEntityUID: Integer);
constructor Load(LoadStream: TKMemoryStream);
function IsGoto: Boolean;
function IsRead: Boolean;
function Text: UnicodeString;
property Loc: TKMPoint read fLoc;
property EntityUID: Integer read fEntityUID;
property Kind: TKMMessageKind read fKind;
property IsReadGIP: Boolean write fIsReadGIP;
property IsReadLocal: Boolean write fIsReadLocal;
procedure Save(SaveStream: TKMemoryStream);
end;
TKMMessageLog = class
private
fReadAtCountGIP: Integer; // Last time player opened log when log count was equal to
fReadAtCountLocal: Integer;
fCountLog: Integer;
fListLog: array of TKMLogMessage;
function GetMessageLog(aIndex: Integer): TKMLogMessage;
function GetReadAtCount: Integer;
public
destructor Destroy; override;
property CountLog: Integer read fCountLog;
property ReadAtCountGIP: Integer read fReadAtCountGIP write fReadAtCountGIP;
property ReadAtCountLocal: Integer read fReadAtCountLocal write fReadAtCountLocal;
property ReadAtCount: Integer read GetReadAtCount;
property MessagesLog[aIndex: Integer]: TKMLogMessage read GetMessageLog; default;
function HasNewMessages: Boolean;
procedure Add(aKind: TKMMessageKind; aTextID: Integer; const aLoc: TKMPoint; aEntityUID: Integer);
procedure Save(SaveStream: TKMemoryStream);
procedure Load(LoadStream: TKMemoryStream);
end;
implementation
uses
SysUtils, Math,
KM_ResTexts,
KM_HandEntity;
{ TKMLogMessage }
constructor TKMLogMessage.Create(aKind: TKMMessageKind; aTextID: Integer; const aLoc: TKMPoint; aEntityUID: Integer);
begin
inherited Create;
fKind := aKind;
fLoc := aLoc;
fEntityUID := aEntityUID;
fTextID := aTextID;
end;
constructor TKMLogMessage.Load(LoadStream: TKMemoryStream);
begin
inherited Create;
LoadStream.CheckMarker('LogMessage');
LoadStream.Read(fEntityUID);
LoadStream.Read(fLoc);
LoadStream.Read(fTextID);
LoadStream.Read(fKind, SizeOf(TKMMessageKind));
LoadStream.Read(fIsReadGIP);
end;
//Check wherever message has a GoTo option
function TKMLogMessage.IsGoto: Boolean;
begin
Result := fKind in [mkHouse, mkGroup];
end;
function TKMLogMessage.IsRead: Boolean;
begin
Result := fIsReadLocal or fIsReadGIP;
end;
function TKMLogMessage.Text: UnicodeString;
begin
Result := gResTexts[fTextID];
end;
procedure TKMLogMessage.Save(SaveStream: TKMemoryStream);
begin
SaveStream.PlaceMarker('LogMessage');
SaveStream.Write(fEntityUID);
SaveStream.Write(fLoc);
SaveStream.Write(fTextID);
SaveStream.Write(fKind, SizeOf(TKMMessageKind));
SaveStream.Write(fIsReadGIP);
end;
{ TKMMessageLog }
destructor TKMMessageLog.Destroy;
var
I: Integer;
begin
for I := 0 to fCountLog - 1 do
FreeAndNil(fListLog[I]);
inherited;
end;
function TKMMessageLog.GetMessageLog(aIndex: Integer): TKMLogMessage;
begin
Assert(InRange(aIndex, 0, fCountLog - 1));
Result := fListLog[aIndex];
end;
function TKMMessageLog.GetReadAtCount: Integer;
begin
Result := Max(fReadAtCountGIP, fReadAtCountLocal);
end;
function TKMMessageLog.HasNewMessages: Boolean;
begin
Result := ReadAtCount < fCountLog;
end;
procedure TKMMessageLog.Add(aKind: TKMMessageKind; aTextID: Integer; const aLoc: TKMPoint; aEntityUID: Integer);
begin
SetLength(fListLog, fCountLog + 1);
fListLog[fCountLog] := TKMLogMessage.Create(aKind, aTextID, aLoc, aEntityUID);
Inc(fCountLog);
end;
procedure TKMMessageLog.Save(SaveStream: TKMemoryStream);
var
I: Integer;
begin
SaveStream.PlaceMarker('MessageLog');
SaveStream.Write(fCountLog);
SaveStream.Write(fReadAtCountGIP);
for I := 0 to fCountLog - 1 do
MessagesLog[I].Save(SaveStream);
end;
procedure TKMMessageLog.Load(LoadStream: TKMemoryStream);
var
I: Integer;
begin
LoadStream.CheckMarker('MessageLog');
LoadStream.Read(fCountLog);
LoadStream.Read(fReadAtCountGIP);
SetLength(fListLog, fCountLog);
for I := 0 to fCountLog - 1 do
fListLog[I] := TKMLogMessage.Load(LoadStream);
end;
end.
@@ -0,0 +1,168 @@
unit KM_MessageStack;
{$I KaM_Remake.inc}
interface
uses
KM_CommonClasses, KM_GameTypes, KM_Points;
type
//Individual message
TKMStackMessage = class
fKind: TKMMessageKind;
fLoc: TKMPoint;
fText: UnicodeString;
public
IsRead: Boolean; //Does not gets saved, because it's UI thing
constructor Create(aKind: TKMMessageKind; const aText: UnicodeString; const aLoc: TKMPoint);
constructor Load(LoadStream: TKMemoryStream);
function Icon: Word;
function IsGoto: Boolean;
property Loc: TKMPoint read fLoc;
property Text: UnicodeString read fText;
property Kind: TKMMessageKind read fKind;
procedure Save(SaveStream: TKMemoryStream);
end;
TKMMessageStack = class
private
fCountStack: Integer;
fListStack: array of TKMStackMessage;
function GetMessageStack(aIndex: Integer): TKMStackMessage;
public
destructor Destroy; override;
property CountStack: Integer read fCountStack;
property MessagesStack[aIndex: Integer]: TKMStackMessage read GetMessageStack; default;
procedure Add(aKind: TKMMessageKind; const aText: UnicodeString; const aLoc: TKMPoint);
procedure RemoveStack(aIndex: Integer);
procedure Save(SaveStream: TKMemoryStream);
procedure Load(LoadStream: TKMemoryStream);
end;
implementation
uses
SysUtils, Math;
{ TKMStackMessage }
constructor TKMStackMessage.Create(aKind: TKMMessageKind; const aText: UnicodeString; const aLoc: TKMPoint);
begin
inherited Create;
fKind := aKind;
fLoc := aLoc;
fText := aText;
end;
constructor TKMStackMessage.Load(LoadStream: TKMemoryStream);
begin
inherited Create;
LoadStream.CheckMarker('StackMessage');
LoadStream.Read(fLoc);
LoadStream.ReadW(fText);
LoadStream.Read(fKind, SizeOf(TKMMessageKind));
LoadStream.Read(IsRead);
end;
//Check wherever message has a GoTo option
function TKMStackMessage.IsGoto: Boolean;
begin
Result := fKind in [mkHouse, mkGroup];
end;
//GUIMain icon index associated with that message kind
function TKMStackMessage.Icon: Word;
const
MSG_ICON: array [TKMMessageKind] of Word = (491, 492, 493, 495);
begin
Result := MSG_ICON[fKind];
end;
procedure TKMStackMessage.Save(SaveStream: TKMemoryStream);
begin
SaveStream.PlaceMarker('StackMessage');
SaveStream.Write(fLoc);
SaveStream.WriteW(fText);
SaveStream.Write(fKind, SizeOf(TKMMessageKind));
SaveStream.Write(IsRead);
end;
{ TKMMessageStack }
destructor TKMMessageStack.Destroy;
var
I: Integer;
begin
for I := 0 to fCountStack - 1 do
FreeAndNil(fListStack[I]);
inherited;
end;
function TKMMessageStack.GetMessageStack(aIndex: Integer): TKMStackMessage;
begin
Assert(InRange(aIndex, 0, fCountStack - 1));
Result := fListStack[aIndex];
end;
procedure TKMMessageStack.Add(aKind: TKMMessageKind; const aText: UnicodeString; const aLoc: TKMPoint);
begin
SetLength(fListStack, fCountStack + 1);
fListStack[fCountStack] := TKMStackMessage.Create(aKind, aText, aLoc);
Inc(fCountStack);
end;
procedure TKMMessageStack.RemoveStack(aIndex: Integer);
begin
if not InRange(aIndex, 0, fCountStack - 1) then Exit; // Somehow could happen (was reported), but hard to reproduce
FreeAndNil(fListStack[aIndex]); //Release the deleted message
//Move the messages to cover the gap
if aIndex <> fCountStack - 1 then
Move(fListStack[aIndex + 1], fListStack[aIndex], (fCountStack - 1 - aIndex) * SizeOf(TKMStackMessage));
//Keep it neat
Dec(fCountStack);
SetLength(fListStack, fCountStack);
end;
procedure TKMMessageStack.Save(SaveStream: TKMemoryStream);
var
I: Integer;
begin
SaveStream.PlaceMarker('MessageStack');
SaveStream.Write(fCountStack);
for I := 0 to fCountStack - 1 do
MessagesStack[I].Save(SaveStream);
end;
procedure TKMMessageStack.Load(LoadStream: TKMemoryStream);
var
I: Integer;
begin
LoadStream.CheckMarker('MessageStack');
LoadStream.Read(fCountStack);
SetLength(fListStack, fCountStack);
for I := 0 to fCountStack - 1 do
fListStack[I] := TKMStackMessage.Load(LoadStream);
end;
end.