sync: restore source from Google Drive
Sync changed files to Google Drive / sync (push) Successful in 1h45m13s
Sync changed files to Google Drive / sync (push) Successful in 1h45m13s
This commit is contained in:
1 parent
e2d8247e91
commit
7063d1b1c9
993 files changed
+949976
No files matched your search
+634
@@ -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.
|
||||
+3406
File diff suppressed because it is too large.
Load diff
+138
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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.
|
||||
+2086
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.
|
||||
|
||||
@@ -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
@@ -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.
|
||||
@@ -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.
|
||||
Reference in new issue
Block a user