diff --git a/config/extra-paths.json b/config/extra-paths.json index 5983b47..8522615 100755 --- a/config/extra-paths.json +++ b/config/extra-paths.json @@ -18,7 +18,11 @@ "src/res/KM_ResPalettes.pas", "src/res/KM_ResMapElements.pas", "src/res/KM_ResLocales.pas", - "src/render/KM_Render.pas" + "src/render/KM_Render.pas", + "src/KM_GameApp.pas", + "src/res/KM_Resource.pas", + "src/common/KM_Defaults.pas", + "src/res/KM_ResTypes.pas" ], "source_paths": [ "code/resource" diff --git a/port-source/kam_remake/src/KM_GameApp.pas b/port-source/kam_remake/src/KM_GameApp.pas new file mode 100644 index 0000000..3c68a4c --- /dev/null +++ b/port-source/kam_remake/src/KM_GameApp.pas @@ -0,0 +1,1450 @@ +unit KM_GameApp; +{$I KaM_Remake.inc} +interface +uses + {$IFDEF WDC} UITypes, {$ENDIF} + {$IFDEF FPC} Controls, {$ENDIF} + Classes, + Dialogs, SysUtils, + KM_CommonTypes, KM_Defaults, KM_RenderControl, KM_Video, + KM_Campaigns, KM_Game, KM_InterfaceDefaults, KM_InterfaceMainMenu, KM_InterfaceTypes, KM_Resource, + KM_Music, KM_Maps, KM_MapTypes, KM_CampaignClasses, KM_Networking, + KM_GameSettings, + KM_KeysSettings, + KM_ServerSettings, + KM_Render, + KM_GameTypes, KM_Points, KM_Console, + KM_WorkerThread; + +type + //Methods relevant to gameplay + TKMGameApp = class + private + fGlobalTickCount: Cardinal; + fIsExiting: Boolean; + fIsLoading: Boolean; + + fCampaigns: TKMCampaignsCollection; + fServerSettings: TKMServerSettings; + fNetworking: TKMNetworking; + + fMainMenuInterface: TKMMainMenuInterface; + + fChat: TKMChat; + + fOnStatusBarUpdate: TProc; + fOnGameSpeedChange: TSingleEvent; + fOnGameStart: TKMGameModeChangeEvent; + fOnGameEnd: TKMGameModeChangeEvent; + + fOnOptionsChange: TKMEvent; + + // Worker threads + fSaveWorkerThreadHolder: TKMWorkerThreadHolder; // Worker thread for normal saves and save at the end of PT + fBaseSaveWorkerThreadHolder: TKMWorkerThreadHolder; // Worker thread for base save only + fAutoSaveWorkerThreadHolder: TKMWorkerThreadHolder; // Worker thread for autosaves only + fSavePointWorkerThreadHolder: TKMWorkerThreadHolder; // Worker thread for savepoints only + + procedure CreateGame(aGameMode: TKMGameMode); + + procedure GameLoadingStep(const aText: UnicodeString); + procedure LoadGameAssets; + procedure LoadGameFromSave(const aFilePath: String; aGameMode: TKMGameMode; const aGIPPath: String = ''); + procedure LoadGameFromScript(const aMissionFullFilePath, aGameName: String; aFullCRC, aSimpleCRC: Cardinal; aCampaign: TKMCampaign; + aMap: Byte; aGameMode: TKMGameMode; aDesiredLoc: ShortInt; aDesiredColor: Cardinal; + aDifficulty: TKMMissionDifficulty = mdNone; aAIType: TKMAIType = aitNone); + procedure LoadGameSavePoint(aTick: Cardinal); + procedure LoadGameFromScratch(aSizeX, aSizeY: Integer; aGameMode: TKMGameMode); + function SaveName(const aName, aExt: UnicodeString; aIsMultiplayer: Boolean): UnicodeString; + + procedure GameStarted(aGameMode: TKMGameMode); + procedure GameEnded(aGameMode: TKMGameMode); + procedure GameDestroyed; + procedure GameFinished; + function GetGameSettings: TKMGameSettings; + procedure SetOnOptionsChange(const aEvent: TKMEvent); + + procedure InitMainMenu(aScreenX, aScreenY: Word); + function GetActiveInterface: TKMUserInterfaceCommon; + + procedure UpdatePerflog; + public + constructor Create(aRenderControl: TKMRenderControl; aScreenX, aScreenY: Word; aVSync: Boolean; aOnLoadingStep: TKMEvent; + aOnLoadingText: TUnicodeStringEvent; aOnStatusBarUpdate: TProc; NoMusic: Boolean = False); + destructor Destroy; override; + procedure AfterConstruction(aReturnToOptions: Boolean); reintroduce; + + procedure PrepageStopGame(aMsg: TKMGameResultMsg); + procedure StopGame(aMsg: TKMGameResultMsg; const aTextMsg: UnicodeString = ''); + procedure AnnounceReturnToLobby; + procedure PrepareReturnToLobby(aTimestamp: TDateTime); + procedure StopGameReturnToLobby; + function CanClose: Boolean; + procedure Resize(X,Y: Integer); + procedure ToggleLocale(const aLocale: AnsiString; aReturnToMenuPage: TKMMenuPageType); + procedure NetworkInit; + procedure SendMPGameInfo; + function CheckDATConsistency: Boolean; + + function RenderVersion: UnicodeString; + procedure PrintScreen(const aFilename: UnicodeString = ''; aImageType: TKMImageType = itJpeg); + procedure SaveGameWholeMapToImage(aImageType: TKMImageType; aMaxImageSize: Integer = 0); + + procedure PreloadGameResources; + + //These are all different game kinds we can start + procedure NewCampaignMap(aCampaignIdStr: UnicodeString; aMap: Word; aDifficulty: TKMMissionDifficulty = mdNone); + procedure NewSingleMap(const aMissionFullPath, aGameName: UnicodeString; aDesiredLoc: ShortInt = -1; + aDesiredColor: Cardinal = NO_OVERWRITE_COLOR; aDifficulty: TKMMissionDifficulty = mdNone; + aAIType: TKMAIType = aitNone); + procedure NewSingleSave(const aSaveName: UnicodeString); + procedure NewMultiplayerMap(const aFileName: UnicodeString; aMapKind: TKMMapKind; aCRC: Cardinal; aSpectating: Boolean; + aDifficulty: TKMMissionDifficulty); + procedure NewMultiplayerSave(const aSaveName: UnicodeString; Spectating: Boolean); + + procedure NewRestartLastSPGame; + procedure NewRestartLast(const aGameName, aMissionFileRel, aSave: UnicodeString; aGameMode: TKMGameMode; aCampName: TKMCampaignId; + aCampMap: Byte; aLocation: Byte; aColor: Cardinal; aDifficulty: TKMMissionDifficulty = mdNone; + aAIType: TKMAIType = aitNone); + + procedure NewEmptyMap(aSizeX, aSizeY: Integer); + procedure NewMapEditor(const aFullFilePath: UnicodeString; aMultiplayerLoadMode: Boolean); overload; + procedure NewMapEditor(const aFullFilePath: UnicodeString; aSizeX: Integer = 0; aSizeY: Integer = 0; + aMapFullCRC: Cardinal = 0; aMapSimpleCRC: Cardinal = 0; aMultiplayerLoadMode: Boolean = False); overload; + + procedure NewReplay(const aFilePath: UnicodeString); + procedure NewSaveAndReplay(const aSavPath, aRplPath: UnicodeString); + + function TryLoadSavePoint(aTick: Integer): Boolean; + procedure LoadPrevSavePoint; + + procedure SaveMapEditor(const aPathName: UnicodeString); + + property Campaigns: TKMCampaignsCollection read fCampaigns; + function Game: TKMGame; + property GameSettings: TKMGameSettings read GetGameSettings; + + property ActiveInterface: TKMUserInterfaceCommon read GetActiveInterface; + property MainMenuInterface: TKMMainMenuInterface read fMainMenuInterface; + + property Networking: TKMNetworking read fNetworking; + property GlobalTickCount: Cardinal read fGlobalTickCount; + property Chat: TKMChat read fChat; + + procedure KeyDown(Key: Word; Shift: TShiftState; aIsFirst: Boolean); + procedure KeyPress(Key: Char); + procedure KeyUp(Key: Word; Shift: TShiftState); + procedure MouseDown(Button: TMouseButton; Shift: TShiftState; X,Y: Integer); + procedure MouseMove(Shift: TShiftState; X,Y: Integer); + procedure MouseUp(Button: TMouseButton; Shift: TShiftState; X,Y: Integer); + procedure MouseWheel(Shift: TShiftState; WheelSteps: Integer; X,Y: Integer; var aHandled: Boolean); + procedure SendFPSMeasurement(aFPS: Integer); + + procedure UnlockAllCampaigns; + + procedure DebugControlsUpdated(Sender: TObject; aSenderTag: Integer); + + property OnGameSpeedActualChange: TSingleEvent read fOnGameSpeedChange write fOnGameSpeedChange; + property OnGameStart: TKMGameModeChangeEvent read fOnGameStart write fOnGameStart; + property OnGameEnd: TKMGameModeChangeEvent read fOnGameEnd write fOnGameEnd; + property OnOptionsChange: TKMEvent read fOnOptionsChange write SetOnOptionsChange; + + procedure Render(aForPrintScreen: Boolean = False); + procedure UpdateState; + procedure UpdateStateIdle(aFrameTime: Cardinal); + procedure DoGameTick; + end; + + +var + gGameApp: TKMGameApp; + + +implementation +uses + {$IFDEF MSWindows} Windows, {$ENDIF} + {$IFDEF Unix} LCLType, {$ENDIF} + DateUtils, Math, TypInfo, KromUtils, + {$IFDEF USE_MAD_EXCEPT} KM_Exceptions, {$ENDIF} + KM_System, + KM_FormLogistics, + KM_Main, KM_Controls, KM_Log, KM_Sound, KM_GameInputProcess, KM_GameInputProcess_Multi, + KM_HandsCollection, + KM_GameSavePoints, + KM_Cursor, KM_ResTexts, KM_ResKeys, KM_ResTypes, + KM_IoGraphicUtils, KM_Settings, + KM_Saves, KM_CommonUtils, KM_CommonShellUtils, + {$IFDEF DBG_RNG_SPY}KM_RandomChecks,{$ENDIF} + KM_DevPerfLog, KM_DevPerfLogTypes; + + +{ TKMGameApp } +// Creating everything needed for MainMenu, game stuff is created on StartGame +constructor TKMGameApp.Create(aRenderControl: TKMRenderControl; aScreenX, aScreenY: Word; aVSync: Boolean; aOnLoadingStep: TKMEvent; + aOnLoadingText: TUnicodeStringEvent; aOnStatusBarUpdate: TProc; NoMusic: Boolean = False); +begin + inherited Create; + + fCampaigns := TKMCampaignsCollection.Create; + + fOnStatusBarUpdate := aOnStatusBarUpdate; + + gResKeys := TKMResKeys.Create; + + gKeySettings.UpdateResKeys; + + // When creating local server from inside the game, + // it makes sense to store its settings along with the game's in the shared folder + fServerSettings := TKMServerSettings.Create(slShared); + + gRender := TKMRender.Create(aRenderControl, aScreenX, aScreenY, aVSync); + + fChat := TKMChat.Create; + + gCursor := TKMCursor.Create; + + {$IFDEF DBG_RNG_SPY} + if gGameSettings.DebugSaveRandomChecks and not DBG_DISALLOW_TO_SAVE_RANDOM_CHECKS then + gRandomCheckLogger := TKMRandomCheckLogger.Create; + {$ENDIF} + + gRes := TKMResource.Create(aOnLoadingStep, aOnLoadingText); + gRes.LoadMainResources(gGameSettings.Locale, gGameSettings.GFX.LoadFullFonts); + + {$IFDEF USE_MAD_EXCEPT}gExceptions.LoadTranslation;{$ENDIF} + + //Show the message if user has old OpenGL drivers (pre-1.4) + if gRender.IsOldGLVersion then + //MessageDlg works better than Application.MessageBox or others, it stays on top and + //pauses here until the user clicks ok. + MessageDlg(gResTexts[TX_GAME_ERROR_OLD_OPENGL] + EolW + EolW + gResTexts[TX_GAME_ERROR_OLD_OPENGL_2], mtWarning, [mbOk], 0); + + gSoundPlayer := TKMSoundPlayer.Create(gGameSettings.SFX.SoundFXVolume); + gMusic := TKMMusicLib.Create(gGameSettings.SFX.MusicVolume); + gSoundPlayer.OnRequestFade := gMusic.Fade; + gSoundPlayer.OnRequestUnfade := gMusic.Unfade; + + //If game was reinitialized from options menu then we should return there + InitMainMenu(aScreenX, aScreenY); + + // Start the Music playback as soon as loading is complete + if not NoMusic and gGameSettings.SFX.MusicEnabled then + gMusic.PlayMenuTrack; + + gMusic.ToggleShuffle(gGameSettings.SFX.ShuffleOn); //Determine track order + + fSaveWorkerThreadHolder := TKMWorkerThreadHolder.Create('SaveWorker'); + fAutoSaveWorkerThreadHolder := TKMWorkerThreadHolder.Create('AutoSaveWorker'); + fBaseSaveWorkerThreadHolder := TKMWorkerThreadHolder.Create('BaseSaveWorker'); + fSavePointWorkerThreadHolder := TKMWorkerThreadHolder.Create('SavePointsWorker'); + + fOnGameStart := GameStarted; + fOnGameEnd := GameEnded; +end; + + +procedure TKMGameApp.AfterConstruction(aReturnToOptions: Boolean); +begin + if aReturnToOptions then + fMainMenuInterface.PageChange(gpOptions) + else + fMainMenuInterface.PageChange(gpMainMenu); +end; + + +{ Destroy what was created } +destructor TKMGameApp.Destroy; +begin + //Freeing network sockets and OpenAL can result in events like Resize/Paint occuring (seen in crash reports) + //Set fIsExiting so we know to skip them + fIsExiting := True; + + //We might have crashed part way through .Create, so we can't assume ANYTHING exists here. + //Doing so causes a 2nd exception which overrides 1st. Hence check <> nil on everything except Free (TObject.Free does that already) + + // Stop music immediately, so it doesn't keep playing and jerk while things get destroyed + if gMusic <> nil then gMusic.Stop; + + StopGame(grSilent); + + //This will ensure all queued work is completed before destruction + FreeAndNil(fSaveWorkerThreadHolder); + FreeAndNil(fAutoSaveWorkerThreadHolder); + FreeAndNil(fBaseSaveWorkerThreadHolder); + FreeAndNil(fSavePointWorkerThreadHolder); + + FreeAndNil(fChat); + FreeThenNil(fCampaigns); + FreeThenNil(fServerSettings); + FreeThenNil(fMainMenuInterface); + FreeThenNil(gRes); + FreeThenNil(gSoundPlayer); + FreeThenNil(gMusic); + FreeAndNil(fNetworking); + {$IFDEF DBG_RNG_SPY} FreeAndNil(gRandomCheckLogger); {$ENDIF} + FreeAndNil(gCursor); + + FreeThenNil(gRender); + + FreeThenNil(gResKeys); + + inherited; +end; + + +procedure TKMGameApp.InitMainMenu(aScreenX, aScreenY: Word); +begin + fMainMenuInterface := TKMMainMenuInterface.Create(aScreenX, aScreenY, fCampaigns, + NewSingleMap, NewCampaignMap, NewMapEditor, NewReplay, NewSingleSave, + ToggleLocale, PreloadGameResources, NetworkInit); +end; + + +procedure TKMGameApp.DebugControlsUpdated(Sender: TObject; aSenderTag: Integer); +begin + if gGame = nil then + fMainMenuInterface.DebugControlsUpdated(aSenderTag) + else + gGame.DebugControlsUpdated(Sender, aSenderTag); +end; + + +//Determine if the game can be closed without loosing any important progress +function TKMGameApp.CanClose: Boolean; +begin + //There are no unsaved changes in MainMenu and in Replays + //In all other cases (maybe except gsOnHold?) there are potentially unsaved changes + Result := (gGame = nil) or gGame.Params.IsReplay; +end; + + +procedure TKMGameApp.ToggleLocale(const aLocale: AnsiString; aReturnToMenuPage: TKMMenuPageType); +begin + Assert(gGame = nil, 'We don''t want to recreate whole fGame for that. Let''s limit it only to MainMenu'); + + // Stop campaign scanning, since we will rescan them on Campaigns menu creation + fCampaigns.TerminateScan; + + gLog.AddTime('Toggle to locale ' + UnicodeString(aLocale)); + fMainMenuInterface.PageChange(gpLoading, gResTexts[TX_MENU_NEW_LOCALE]); + Render; //Force to repaint information screen + + fIsLoading := True; + gGameSettings.Locale := aLocale; //Wrong Locale will be ignored + + //Release resources that use Locale info + FreeAndNil(fNetworking); + FreeAndNil(fMainMenuInterface); + + //Recreate resources that use Locale info + gRes.LoadLocaleResources(gGameSettings.Locale); + //Fonts might need reloading too + gRes.LoadLocaleFonts(gGameSettings.Locale, gGameSettings.GFX.LoadFullFonts); + + {$IFDEF USE_MAD_EXCEPT}gExceptions.LoadTranslation;{$ENDIF} + + InitMainMenu(gRender.ScreenX, gRender.ScreenY); + fMainMenuInterface.PageChange(aReturnToMenuPage); + Resize(gRender.ScreenX, gRender.ScreenY); //Force the recreated main menu to resize to the user's screen + fIsLoading := False; +end; + + +//Preload game resources while in menu +procedure TKMGameApp.PreloadGameResources; +begin + gRes.PreloadGameResources(gGameSettings.GFX.AlphaShadows); +end; + + +procedure TKMGameApp.Resize(X,Y: Integer); +begin + if Self = nil then Exit; + if fIsExiting then Exit; + + gRender.Resize(X, Y); + gVideoPlayer.Resize(X, Y); + + //Main menu is invisible while in game, but it still exists and when we return to it + //it must be properly sized (player could resize the screen while playing) + fMainMenuInterface.Resize(X, Y); + + if gGame <> nil then + gGame.ActiveInterface.Resize(X, Y); +end; + + +// This event happens every ~33ms if the Key is Down and holded +procedure TKMGameApp.KeyDown(Key: Word; Shift: TShiftState; aIsFirst: Boolean); +var + keyHandled: Boolean; +begin + if gVideoPlayer.IsActive then + begin + gVideoPlayer.KeyDown(Key, Shift); + Exit; + end; + + if gGame <> nil then + gGame.ActiveInterface.KeyDown(Key, Shift, aIsFirst, keyHandled) + else + fMainMenuInterface.KeyDown(Key, Shift, aIsFirst, keyHandled); +end; + + +procedure TKMGameApp.KeyPress(Key: Char); +begin + if gGame <> nil then + gGame.ActiveInterface.KeyPress(Key) + else + fMainMenuInterface.KeyPress(Key); +end; + + +procedure TKMGameApp.KeyUp(Key: Word; Shift: TShiftState); +var + keyHandled: Boolean; +begin + //List of conflicting keys that we should try to avoid using in debug/game: + // F12 Pauses Execution and switches to debug + // F10 sets focus on MainMenu1 + // F9 is the default key in Fraps for video capture + // F4 and F9 are used in debug to control run-flow + // others.. unknown + + keyHandled := False; + + if gGame <> nil then + gGame.ActiveInterface.KeyUp(Key, Shift, keyHandled) + else + fMainMenuInterface.KeyUp(Key, Shift, keyHandled); +end; + + +procedure TKMGameApp.MouseDown(Button: TMouseButton; Shift: TShiftState; X, Y: Integer); +begin + if gVideoPlayer.IsActive then + begin + gVideoPlayer.MouseDown(Button, Shift, X, Y); + Exit; + end; + + if gGame <> nil then + gGame.ActiveInterface.MouseDown(Button,Shift,X,Y) + else + fMainMenuInterface.MouseDown(Button,Shift,X,Y); +end; + + +procedure TKMGameApp.MouseMove(Shift: TShiftState; X,Y: Integer); +var + ctrl: TKMControl; + ctrlID: Integer; +begin + if not InRange(X, 1, gRender.ScreenX - 1) + or not InRange(Y, 1, gRender.ScreenY - 1) then + Exit; // Exit if Cursor is outside of frame + + if gGame <> nil then + gGame.ActiveInterface.MouseMove(Shift,X,Y) + else + //fMainMenuInterface = nil while loading a new locale + if fMainMenuInterface <> nil then + fMainMenuInterface.MouseMove(Shift, X,Y); + + if Assigned(fOnStatusBarUpdate) then + begin + fOnStatusBarUpdate(spCursorCoord, Format('Cursor: %d:%d', [X, Y])); + fOnStatusBarUpdate(spTile, Format('Tile: %.1f:%.1f [%d:%d]', [gCursor.Float.X, gCursor.Float.Y, gCursor.Cell.X, gCursor.Cell.Y])); + if SHOW_CONTROLS_ID then + begin + if gGame <> nil then + ctrl := gGame.ActiveInterface.MyControls.HitControl(X,Y, True) + else + ctrl := fMainMenuInterface.MyControls.HitControl(X,Y, True); + ctrlID := -1; + if ctrl <> nil then + ctrlID := ctrl.ID; + fOnStatusBarUpdate(spControlId, Format('Control ID: %d', [ctrlID])); + end; + end; +end; + + +procedure TKMGameApp.MouseUp(Button: TMouseButton; Shift: TShiftState; X, Y: Integer); +begin + if gGame <> nil then + gGame.ActiveInterface.MouseUp(Button,Shift,X,Y) + else + fMainMenuInterface.MouseUp(Button, Shift, X,Y); +end; + + +procedure TKMGameApp.MouseWheel(Shift: TShiftState; WheelSteps: Integer; X, Y: Integer; var aHandled: Boolean); +begin + aHandled := False; + if Self = nil then Exit; + + if gGame <> nil then + gGame.ActiveInterface.MouseWheel(Shift, WheelSteps, X, Y, aHandled) + else + fMainMenuInterface.MouseWheel(Shift, WheelSteps, X, Y, aHandled); +end; + + +procedure TKMGameApp.SendFPSMeasurement(aFPS: Integer); +begin + fNetworking.SendFPSMeasurement(aFPS); +end; + + +function TKMGameApp.Game: TKMGame; +begin + if Self = nil then Exit(nil); + + Result := gGame; +end; + + +procedure TKMGameApp.GameLoadingStep(const aText: UnicodeString); +begin + fMainMenuInterface.AppendLoadingText(aText); + Render; +end; + + +procedure TKMGameApp.LoadGameAssets; +begin + //Load the resources if necessary + fMainMenuInterface.PageChange(gpLoading); + Render; + + GameLoadingStep(gResTexts[TX_MENU_LOADING_DEFINITIONS]); + gRes.OnLoadingText := GameLoadingStep; + gRes.LoadGameResources(gGameSettings.GFX.AlphaShadows); + + GameLoadingStep(gResTexts[TX_MENU_LOADING_INITIALIZING]); + + GameLoadingStep(gResTexts[TX_MENU_LOADING_SCRIPT]); +end; + + +procedure TKMGameApp.UnlockAllCampaigns; +begin + if fCampaigns <> nil then + begin + fCampaigns.UnlockAllCampaignsMissions; + fCampaigns.SaveProgress; + + fMainMenuInterface.RefreshCampaigns; + end; +end; + + +procedure TKMGameApp.CreateGame(aGameMode: TKMGameMode); +begin + //Reset controls if MainForm exists (KMR could be run without main form) + if gMain <> nil then + gMain.FormMain.ControlsReset; + + gGame := TKMGame.Create(aGameMode, gRender, GameDestroyed, + fSaveWorkerThreadHolder, + fBaseSaveWorkerThreadHolder, + fAutoSaveWorkerThreadHolder, + fSavePointWorkerThreadHolder); +end; + + +procedure TKMGameApp.PrepageStopGame(aMsg: TKMGameResultMsg); +var + lastSentCmdsTick: Integer; +begin + if (gGame = nil) or gGame.ReadyToStop then Exit; + + gSoundPlayer.AbortAllLongSounds; //SFX with a long duration should be stopped when quitting + gSoundPlayer.AbortAllScriptSounds; // Looped sounds should be stopped as well + + if aMsg in [grWin, grDefeat, grCancel, grSilent] then + begin + //If the game was a part of a campaign, select that campaign, + //so we know which menu to show next and unlock next map + fCampaigns.SetActive(fCampaigns.CampaignById(gGame.CampaignName)); + + if fCampaigns.ActiveCampaign <> nil then + begin + //Always save campaign data, even if the player lost (scripter can choose when to modify it) + fCampaigns.ActiveCampaign.SavedData.ScriptDataStream.Clear; + gGame.SaveCampaignScriptData(fCampaigns.ActiveCampaign.SavedData.ScriptDataStream); + + if aMsg = grWin then + fCampaigns.ActiveCampaign.UnlockNextMission(gGame.CampaignMap); //Unlock next map before save campaign progress + + //Always save Campaigns progress after mission + //We always save script data with SaveCampaignScriptData + //then campaign progress should be saved always as well on any outcome (win or lose) + //otherwise we will get inconsistency + fCampaigns.ActiveCampaign.SavedData.SaveProgress; + end; + end; + + if gGame.Params.IsMultiPlayerOrSpec then + begin + if fNetworking.Connected then + begin + if TKMGameInputProcess_Multi(gGame.GameInputProcess) <> nil then + lastSentCmdsTick := TKMGameInputProcess_Multi(gGame.GameInputProcess).LastSentCmdsTick + else + lastSentCmdsTick := LAST_SENT_COMMANDS_TICK_NONE; + fNetworking.AnnounceDisconnect(lastSentCmdsTick); + end; + fNetworking.Disconnect; + end; + + gGame.ReadyToStop := True; + + if (gGame.GamePlayInterface <> nil) and (gGame.GamePlayInterface.GuiGameSpectator <> nil) then + gGame.GamePlayInterface.GuiGameSpectator.CloseDropBox; + + if (gGame.GameResult in [grWin, grDefeat]) and not gGame.Params.IsReplay then + begin + GameFinished; + if gGameSettings.AutosaveAtGameEnd then + // We have to use local time for local save name (Now) and UTC time inside the save (UTCNow, it will be converted back to local on read) + gGame.Save(Format('%s %s #%d', [gGame.Params.Name, FormatDateTime('yyyy-mm-dd', Now), gGameSettings.DayGamesCount]), UTCNow); + end; + + if Assigned(fOnGameEnd) then + fOnGameEnd(gGame.Params.Mode); +end; + + +//Game needs to be stopped +//1. Disconnect from network +//2. Save games replay +//3. Fill in game results +//4. Fill in menu message if needed +//5. Free the game object +//6. Switch to MainMenu +procedure TKMGameApp.StopGame(aMsg: TKMGameResultMsg; const aTextMsg: UnicodeString = ''); +begin + if gGame = nil then Exit; + + PrepageStopGame(aMsg); + + case aMsg of + grWin, + grDefeat, + grCancel: case gGame.Params.Mode of + gmSingle: fMainMenuInterface.PageChange(gpSinglePlayer); + gmCampaign: if aTextMsg = '' then //Rely on text message (for campaign it should contain CampaignID) + fMainMenuInterface.PageChange(gpMainMenu) //Goto main menu in case we fail campaing mission + else + fMainMenuInterface.PageChange(gpCampaign, aTextMsg); //Goto Campaign menu in case we win campaing mission + gmMulti, + gmMultiSpectate: fMainMenuInterface.PageChange(gpMultiplayer); + end; + grReplayEnd: fMainMenuInterface.PageChange(gpReplays); + grError, + grDisconnect: begin + if gGame.Params.IsMultiPlayerOrSpec then + //After Error page User will go to the main menu, but Mutex will be still locked. + //We will need to unlock it on gGame destroy, so mark it with GameLockedMutex + gGame.LockedMutex := True; + fMainMenuInterface.PageChange(gpError, aTextMsg); + end; + grSilent: ;//Used when loading new savegame from gameplay UI + grMapEdEnd: fMainMenuInterface.PageChange(gpMapEditor); + end; + + FreeThenNil(gGame); + gLog.AddTime('Gameplay ended - ' + GetEnumName(TypeInfo(TKMGameResultMsg), Integer(aMsg)) + ' /' + aTextMsg); +end; + + +procedure TKMGameApp.AnnounceReturnToLobby; +begin + //When this GIC command is executed, it will run PrepareReturnToLobby + gGame.GameInputProcess.CmdGame(gicGameSaveReturnLobby, UTCNow); +end; + + +procedure TKMGameApp.PrepareReturnToLobby(aTimestamp: TDateTime); +begin + if gGame = nil then Exit; + gGame.Save(RETURN_TO_LOBBY_SAVE, aTimestamp); + gGame.IsPaused := True; //Freeze the game + fNetworking.AnnounceReadyToReturnToLobby; + //Now we wait for other clients to reach this step, then fNetworking triggers StopGameReturnToLobby +end; + + +procedure TKMGameApp.StopGameReturnToLobby; +begin + if gGame = nil then Exit; + + // Wait till 'paused' save will be made. We want to show it properly in the Lobby UI + gSystem.Cursor := kmcAnimatedDirSelector; + try + gGame.WaitForSaveToBeDone; + finally + gSystem.Cursor := kmcDefault; + end; + + if Assigned(fOnGameEnd) then + fOnGameEnd(gGame.Params.Mode); + + FreeThenNil(gGame); + + fNetworking.ReturnToLobby; //Clears gGame event pointers from Networking + fMainMenuInterface.ReturnToLobby(RETURN_TO_LOBBY_SAVE); //Assigns Lobby event pointers to Networking and selects map + if fNetworking.IsHost then + fNetworking.SendPlayerListAndRefreshPlayersSetup; //Call now that events are attached to lobby + + gLog.AddTime('Gameplay ended - Return to lobby'); +end; + + +procedure TKMGameApp.LoadGameFromSave(const aFilePath: String; aGameMode: TKMGameMode; const aGIPPath: String = ''); +const + SAVE_SUFFIX_DEBUG = '_dbg'; +var + loadError, filePath: String; +begin + //Save const aFilePath locally, since it could be destroyed as some Game Object instance in StopGame + //!!!!! DO NOT USE aMissionFile or aGameName further in this method + filePath := aFilePath; + //---------------------------------------------------------------------- + StopGame(grSilent); //Stop everything silently + LoadGameAssets; + + CreateGame(aGameMode); + try + gGame.LoadFromFile(filePath, aGIPPath); + except + on E: Exception do + begin + //Trap the exception and show it to the user in nicer form. + //Note: While debugging, Delphi will still stop execution for the exception, + //unless Tools > Debugger > Exception > "Stop on Delphi Exceptions" is unchecked. + //But to normal player the dialog won't show. + loadError := Format(gResTexts[TX_MENU_PARSE_ERROR], [filePath]) + '||' + E.ClassName + ': ' + E.Message; + // Log the error first, because we can crash while stopping the game + gLog.AddTime('Game creation Exception: ' + loadError + {$IFDEF WDC} + sLineBreak + E.StackTrace {$ENDIF} + ); + StopGame(grError, loadError); + Exit; + end; + end; + + gGame.AfterLoad; //Call after load separately, so errors in it could be sended in crashreport + + if SAVE_GAME_AFTER_LOAD then + gGame.Save(gGame.Params.Name + SAVE_SUFFIX_DEBUG); + + if Assigned(fOnStatusBarUpdate) then + fOnStatusBarUpdate(spMapSize, gGame.MapSizeInfo); +end; + + +//Do not use _const_ aMissionFile, aGameName: UnicodeString, as for some unknown reason sometimes aGameName is not accessed after StopGame(grSilent) (pointing to a wrong value) +procedure TKMGameApp.LoadGameFromScript(const aMissionFullFilePath, aGameName: String; aFullCRC, aSimpleCRC: Cardinal; aCampaign: TKMCampaign; + aMap: Byte; aGameMode: TKMGameMode; aDesiredLoc: ShortInt; aDesiredColor: Cardinal; + aDifficulty: TKMMissionDifficulty = mdNone; aAIType: TKMAIType = aitNone); +var + loadError, missionFullFilePath, gameName: String; +begin + //Save const parameters locally, since it could be destroyed as some Game Object instance in StopGame + //!!!!! DO NOT USE aMissionFile or aGameName further in this method + missionFullFilePath := aMissionFullFilePath; + gameName := aGameName; + //!!!!! ------------------------------------------------------------ + StopGame(grSilent); //Stop everything silently + LoadGameAssets; + + CreateGame(aGameMode); + try + gGame.Start(missionFullFilePath, gameName, aFullCRC, aSimpleCRC, aCampaign, aMap, aDesiredLoc, aDesiredColor, aDifficulty, aAIType); + except + on E : Exception do + begin + //Trap the exception and show it to the user in nicer form. + //Note: While debugging, Delphi will still stop execution for the exception, + //unless Tools > Debugger > Exception > "Stop on Delphi Exceptions" is unchecked. + //But to normal player the dialog won't show. + loadError := Format(gResTexts[TX_MENU_PARSE_ERROR], [missionFullFilePath]) + '||' + E.ClassName + ': ' + E.Message; + StopGame(grError, loadError); + gLog.AddTime('Game creation Exception: ' + loadError + {$IFDEF WDC} + sLineBreak + E.StackTrace {$ENDIF} + ); + Exit; + end; + end; + + //Clear chat for SP game, as it useddthere only for console commands + if gGame.Params.IsSingleplayerGame then + fChat.Clear; + + gGame.AfterStart; //Call after start separately, so errors in it could be sended in crashreport + + if Assigned(fOnStatusBarUpdate) then + fOnStatusBarUpdate(spMapSize, gGame.MapSizeInfo); +end; + + +procedure TKMGameApp.LoadGameSavePoint(aTick: Cardinal); +var + loadError: string; + savedReplays: TKMSavePointCollection; + gameMode: TKMGameMode; + saveFile: UnicodeString; + gameInputProcess: TKMGameInputProcess; +begin + if (gGame = nil) then Exit; + + // Get existing configuration + savedReplays := gGame.SavePoints; + gGame.SavePoints := nil; + gameMode := gGame.Params.Mode; + saveFile := gGame.SaveFile; + // Store GIP locally, to restore it later + // GIP is the same for every checkpoint, that is why its not stored in the saved replay checkpoint, so we can reuse it + gameInputProcess := gGame.GameInputProcess; + gGame.GameInputProcess := nil; + + StopGame(grSilent); //Stop everything silently + LoadGameAssets; + + CreateGame(gameMode); + try + // SavedReplays have been just created, and we will reassign them in the next line. + // Then Free the newly created save replays object first + gGame.SavePoints.Free; + gGame.SavePoints := savedReplays; + gGame.LoadSavePoint(aTick, saveFile); + gGame.LastReplayTickLocal := Max(gGame.LastReplayTickLocal, savedReplays.LastTick); + // Free GIP, which was created on game creation + gGame.GameInputProcess.Free; + // Restore GIP + gGame.GameInputProcess := gameInputProcess; + // Move GIP cursor to the actual position + gGame.GameInputProcess.MoveCursorTo(aTick); + except + on E: Exception do + begin + //Trap the exception and show it to the user in nicer form. + //Note: While debugging, Delphi will still stop execution for the exception, + //unless Tools > Debugger > Exception > "Stop on Delphi Exceptions" is unchecked. + //But to normal player the dialog won't show. + loadError := '||' + E.ClassName + ': ' + E.Message; + StopGame(grError, loadError); + gLog.AddTime('Game creation Exception: ' + loadError + {$IFDEF WDC} + sLineBreak + E.StackTrace {$ENDIF} + ); + Exit; + end; + end; + + gGame.AfterLoad; //Call after load separately, so errors in it could be sended in crashreport + + if Assigned(fOnStatusBarUpdate) then + fOnStatusBarUpdate(spMapSize, gGame.MapSizeInfo); +end; + + +procedure TKMGameApp.LoadGameFromScratch(aSizeX, aSizeY: Integer; aGameMode: TKMGameMode); +var + loadError: string; +begin + StopGame(grSilent); //Stop everything silently + LoadGameAssets; + + CreateGame(aGameMode); + gGame.SetSeed(4); //Every time the game will be the same as previous. Good for debug. + try + gGame.MapEdStartEmptyMap(aSizeX, aSizeY); + except + on E : Exception do + begin + //Trap the exception and show it to the user in nicer form. + //Note: While debugging, Delphi will still stop execution for the exception, + //unless Tools > Debugger > Exception > "Stop on Delphi Exceptions" is unchecked. + //But to normal player the dialog won't show. + loadError := Format(gResTexts[TX_MENU_PARSE_ERROR], ['-']) + '||' + E.ClassName + ': ' + E.Message; + StopGame(grError, loadError); + gLog.AddTime('Game creation Exception: ' + loadError + {$IFDEF WDC} + sLineBreak + E.StackTrace {$ENDIF} + ); + Exit; + end; + end; + + if Assigned(fOnStatusBarUpdate) then + fOnStatusBarUpdate(spMapSize, gGame.MapSizeInfo); +end; + + +procedure TKMGameApp.NewCampaignMap(aCampaignIdStr: UnicodeString; aMap: Word; aDifficulty: TKMMissionDifficulty = mdNone); +var + camp: TKMCampaign; +begin + camp := fCampaigns.CampaignByIdU(aCampaignIdStr); + LoadGameFromScript(camp.GetMissionFile(aMap), camp.GetMissionTitle(aMap), 0, 0, camp, aMap, gmCampaign, -1, NO_OVERWRITE_COLOR, aDifficulty); + + fCampaigns.SetActive(camp); + + if Assigned(fOnGameStart) and (gGame <> nil) then + fOnGameStart(gGame.Params.Mode); +end; + + +procedure TKMGameApp.NewSingleMap(const aMissionFullPath, aGameName: UnicodeString; aDesiredLoc: ShortInt = -1; + aDesiredColor: Cardinal = NO_OVERWRITE_COLOR; aDifficulty: TKMMissionDifficulty = mdNone; + aAIType: TKMAIType = aitNone); +begin + LoadGameFromScript(aMissionFullPath, aGameName, 0, 0, nil, 0, gmSingle, aDesiredLoc, aDesiredColor, aDifficulty, aAIType); + + if Assigned(fOnGameStart) and (gGame <> nil) then + fOnGameStart(gGame.Params.Mode); +end; + + +procedure TKMGameApp.NewSingleSave(const aSaveName: UnicodeString); +begin + //Convert SaveName to local FilePath + LoadGameFromSave(SaveName(aSaveName, EXT_SAVE_MAIN, False), gmSingle); + + if Assigned(fOnGameStart) and (gGame <> nil) then + fOnGameStart(gGame.Params.Mode); +end; + + +procedure TKMGameApp.NewMultiplayerMap(const aFileName: UnicodeString; aMapKind: TKMMapKind; aCRC: Cardinal; aSpectating: Boolean; + aDifficulty: TKMMissionDifficulty); +var + gameMode: TKMGameMode; +begin + if aSpectating then + gameMode := gmMultiSpectate + else + gameMode := gmMulti; + + LoadGameFromScript(TKMapsCollection.FullPath(aFileName, '.dat', aMapKind, aCRC), aFileName, + aCRC, 0, nil, 0, gameMode, 0, NO_OVERWRITE_COLOR, aDifficulty); + + //Starting the game might have failed (e.g. fatal script error) + if gGame <> nil then + begin + gGame.GamePlayInterface.GameStarted; + + if Assigned(fOnGameStart) and (gGame <> nil) then + fOnGameStart(gGame.Params.Mode); + end; +end; + + +procedure TKMGameApp.NewMultiplayerSave(const aSaveName: UnicodeString; Spectating: Boolean); +var + gameMode: TKMGameMode; +begin + if Spectating then + gameMode := gmMultiSpectate + else + gameMode := gmMulti; + //Convert SaveName to local FilePath + //aFileName is the same for all players, but Path to it is different + LoadGameFromSave(SaveName(aSaveName, EXT_SAVE_MAIN, True), gameMode); + + //Copy the chat and typed lobby message to the in-game chat + gGame.GamePlayInterface.GameStarted; + + if Assigned(fOnGameStart) and (gGame <> nil) then + fOnGameStart(gGame.Params.Mode); +end; + + +procedure TKMGameApp.NewRestartLastSPGame; +var + gameMode: TKMGameMode; + repeatGameName: UnicodeString; + repeatMissionFileRel: UnicodeString; + repeatSave: UnicodeString; + repeatCampName: TKMCampaignId; + repeatCampMap: Byte; + repeatLocation: Byte; + repeatColor: Cardinal; + repeatDifficulty: TKMMissionDifficulty; + repeatAIType: TKMAIType; +begin + if gGame = nil then Exit; + + if not gGame.Params.IsSingleplayerGame then Exit; + + //Remember which map we played so we could restart it + gameMode := gGame.Params.Mode; + repeatGameName := gGame.Params.Name; + repeatMissionFileRel := gGame.Params.MissionFileRel; + repeatSave := gGame.SaveFile; + repeatCampName := gGame.CampaignName; + repeatCampMap := gGame.CampaignMap; + repeatLocation := gGame.PlayerLoc; + repeatColor := gGame.PlayerColor; + repeatDifficulty := gGame.Params.MissionDifficulty; + repeatAIType := gGame.AIType; + + StopGame(grSilent); + + gGameApp.NewRestartLast(repeatGameName, repeatMissionFileRel, repeatSave, gameMode, repeatCampName, repeatCampMap, + repeatLocation, repeatColor, repeatDifficulty, repeatAIType); +end; + + +procedure TKMGameApp.NewRestartLast(const aGameName, aMissionFileRel, aSave: UnicodeString; aGameMode: TKMGameMode; + aCampName: TKMCampaignId; aCampMap: Byte; aLocation: Byte; aColor: Cardinal; + aDifficulty: TKMMissionDifficulty = mdNone; aAIType: TKMAIType = aitNone); +begin + if FileExists(ExeDir + aMissionFileRel) then + LoadGameFromScript(ExeDir + aMissionFileRel, aGameName, 0, 0, fCampaigns.CampaignById(aCampName), aCampMap, aGameMode, aLocation, aColor, aDifficulty, aAIType) + else + if FileExists(ChangeFileExt(ExeDir + aSave, EXT_SAVE_BASE_DOT)) then + LoadGameFromSave(ChangeFileExt(ExeDir + aSave, EXT_SAVE_BASE_DOT), aGameMode) + else + fMainMenuInterface.PageChange(gpError, 'Can not repeat last mission'); + + if Assigned(fOnGameStart) and (gGame <> nil) then + fOnGameStart(gGame.Params.Mode); + +end; + + +//Used by Runner util +procedure TKMGameApp.NewEmptyMap(aSizeX, aSizeY: Integer); +begin + LoadGameFromScratch(aSizeX, aSizeY, gmSingle); + + if Assigned(fOnGameStart) and (gGame <> nil) then + fOnGameStart(gGame.Params.Mode); +end; + + +procedure TKMGameApp.NewMapEditor(const aFullFilePath: UnicodeString; aMultiplayerLoadMode: Boolean); +begin + NewMapEditor(aFullFilePath, 0, 0, 0, 0, aMultiplayerLoadMode); +end; + + +procedure TKMGameApp.NewMapEditor(const aFullFilePath: UnicodeString; aSizeX: Integer = 0; aSizeY: Integer = 0; + aMapFullCRC: Cardinal = 0; aMapSimpleCRC: Cardinal = 0; aMultiplayerLoadMode: Boolean = False); +begin + if aFullFilePath <> '' then + LoadGameFromScript(aFullFilePath, TruncateExt(ExtractFileName(aFullFilePath)), aMapFullCRC, aMapSimpleCRC, nil, 0, gmMapEd, 0, NO_OVERWRITE_COLOR) + else + begin + aSizeX := EnsureRange(aSizeX, MIN_MAP_SIZE, MAX_MAP_SIZE); + aSizeY := EnsureRange(aSizeY, MIN_MAP_SIZE, MAX_MAP_SIZE); + LoadGameFromScratch(aSizeX, aSizeY, gmMapEd); + end; + + // gGame could be nil if we failed to load map + if gGame = nil then Exit; + + gGame.MapEditorInterface.SetLoadMode(aMultiplayerLoadMode); + + if Assigned(fOnGameStart) then + fOnGameStart(gGame.Params.Mode); +end; + + +procedure TKMGameApp.SaveMapEditor(const aPathName: UnicodeString); +begin + if aPathName <> '' then + gGame.SaveMapEditor(aPathName); +end; + + +procedure TKMGameApp.NewReplay(const aFilePath: UnicodeString); +begin + Assert(ExtractFileExt(aFilePath) = EXT_SAVE_BASE_DOT); + LoadGameFromSave(aFilePath, gmReplaySingle); //Will be changed to gmReplayMulti depending on save contents + + if Assigned(fOnGameStart) and (gGame <> nil) then + fOnGameStart(gGame.Params.Mode); +end; + + +procedure TKMGameApp.NewSaveAndReplay(const aSavPath, aRplPath: UnicodeString); +begin + LoadGameFromSave(aSavPath, gmReplaySingle, aRplPath); //Will be changed to gmReplayMulti depending on save contents + + if Assigned(fOnGameStart) and (gGame <> nil) then + fOnGameStart(gGame.Params.Mode); +end; + + +// Good for debug, when need to load savepoint from debugger before known crash during debugging tick +procedure TKMGameApp.LoadPrevSavePoint; +begin + if (gGame = nil) or (gGame.SavePoints = nil) then Exit; + + TryLoadSavePoint(gGame.SavePoints.LatestPointTickBefore(gGame.Params.Tick)); +end; + + +function TKMGameApp.TryLoadSavePoint(aTick: Integer): Boolean; +begin + Result := False; + + if (gGame = nil) or (gGame.SavePoints = nil) then Exit; + + if not gGame.SavePoints.Contains(aTick) then Exit; + + LoadGameSavePoint(aTick); + Result := True; + + if Assigned(fOnGameStart) and (gGame <> nil) then + fOnGameStart(gGame.Params.Mode); +end; + + +procedure TKMGameApp.GameStarted(aGameMode: TKMGameMode); +begin + Assert(gGame <> nil); + + if gMain <> nil then + begin + gMain.FormMain.SetExportGameStats(aGameMode in [gmMultiSpectate, gmReplaySingle, gmReplayMulti]); + gMain.FormMain.GameStarted(aGameMode); + gMain.FormMain.SetMySpecHandIndex(gMySpectator.HandID); + end; + + if Assigned(FormLogistics) then + FormLogistics.UpdateView(gGame.GetHandsCount, True); +end; + + +function TKMGameApp.GetActiveInterface: TKMUserInterfaceCommon; +begin + if gGame = nil then + Result := fMainMenuInterface + else + Result := gGame.ActiveInterface; +end; + + +function TKMGameApp.GetGameSettings: TKMGameSettings; +begin + if Self = nil then Exit(nil); + + Result := gGameSettings; +end; + + +procedure TKMGameApp.GameEnded(aGameMode: TKMGameMode); +begin + if gMain <> nil then + begin + gMain.FormMain.SetExportGameStats((aGameMode in [gmMultiSpectate, gmReplaySingle, gmReplayMulti]) + or (gGame.GameResult in [grWin, grDefeat])); + gMain.FormMain.GameEnded(aGameMode); + end; + + if Assigned(FormLogistics) then + FormLogistics.Clear; +end; + + +procedure TKMGameApp.GameDestroyed; +begin + if gMain <> nil then + gMain.FormMain.SetExportGameStats(False); +end; + + +//Happens when game was won or lost +procedure TKMGameApp.GameFinished; +begin + if CompareDate(gGameSettings.LastDayGamePlayed, Today) < 0 then + gGameSettings.DayGamesCount := 0; + + gGameSettings.LastDayGamePlayed := Today; + gGameSettings.DayGamesCount := gGameSettings.DayGamesCount + 1; +end; + + +function TKMGameApp.SaveName(const aName, aExt: UnicodeString; aIsMultiplayer: Boolean): UnicodeString; +begin + Result := TKMSavesCollection.FullPath(aName, aExt, aIsMultiplayer); +end; + + +procedure TKMGameApp.NetworkInit; +begin + if fNetworking = nil then + fNetworking := TKMNetworking.Create(fServerSettings.MasterServerAddress, + fServerSettings.AutoKickTimeout, + fServerSettings.PingInterval, + fServerSettings.MasterAnnounceInterval, + fServerSettings.ServerUDPScanPort, + fServerSettings.ServerPacketsAccumulatingDelay, + fServerSettings.ServerDynamicFOW, + fServerSettings.ServerMapsRosterEnabled, + fServerSettings.ServerMapsRosterStr, + KMRange(fServerSettings.ServerLimitPTFrom, fServerSettings.ServerLimitPTTo), + KMRange(fServerSettings.ServerLimitSpeedFrom, fServerSettings.ServerLimitSpeedTo), + KMRange(fServerSettings.ServerLimitSpeedAfterPTFrom, fServerSettings.ServerLimitSpeedAfterPTTo)); + + // Set event handlers anyway. Those could be reset by someone + fNetworking.OnMPGameInfoChanged := SendMPGameInfo; + fNetworking.OnStartMap := NewMultiplayerMap; + fNetworking.OnStartSave := NewMultiplayerSave; + fNetworking.OnAnnounceReturnToLobby := AnnounceReturnToLobby; + fNetworking.OnDoReturnToLobby := StopGameReturnToLobby; +end; + + +//Called by fNetworking to access MissionTime/GameName if they are valid +//fNetworking knows nothing about fGame +procedure TKMGameApp.SendMPGameInfo; +begin + if gGame <> nil then + fNetworking.AnnounceGameInfo(gGame.MissionTime, gGame.Params.Name) + else + fNetworking.AnnounceGameInfo(-1, ''); //fNetworking will fill the details from lobby +end; + + +procedure TKMGameApp.SetOnOptionsChange(const aEvent: TKMEvent); +begin + fOnOptionsChange := aEvent; + + fMainMenuInterface.SetOnOptionsChange(aEvent); +end; + + +procedure TKMGameApp.Render(aForPrintScreen: Boolean = False); +begin + if Self = nil then Exit; + if SKIP_RENDER then Exit; + if fIsExiting then Exit; + if gRender.Blind then Exit; + if fIsLoading then Exit; //Don't render while toggling locale + + gRender.BeginFrame; + + {$IFDEF DBG_PERFLOG} + gPerfLogs.StackGFX.FrameBegin; + gPerfLogs.SectionEnter(psFrameFullG); + + try + {$ENDIF} + if gVideoPlayer.IsActive then + gVideoPlayer.Paint + else + if gGame <> nil then + gGame.Render(gRender) + else + fMainMenuInterface.Paint; + + gRender.RenderBrightness(gGameSettings.GFX.Brightness); + {$IFDEF DBG_PERFLOG} + finally + gPerfLogs.SectionLeave(psFrameFullG); + gPerfLogs.StackGFX.FrameEnd; + end; + + if gMain <> nil then + gPerfLogs.Render(TOOLBAR_WIDTH + 10, gMain.FormMain.RenderArea.Width - 10, gMain.FormMain.RenderArea.Height - 10); + {$ENDIF} + + gRender.EndFrame; + gRender.Query.QueriesSwapBuffers; + + if not aForPrintScreen + and (gGame <> nil) + and Assigned(fOnStatusBarUpdate) then + fOnStatusBarUpdate(spObjectUID, 'Obj: ' + IntToStr(gCursor.ObjectUID)); +end; + + +function TKMGameApp.RenderVersion: UnicodeString; +begin + Result := 'OpenGL ' + gRender.RendererVersion; +end; + + +procedure TKMGameApp.PrintScreen(const aFilename: UnicodeString = ''; aImageType: TKMImageType = itJpeg); +var + strDate, fileName: string; + w, h: Integer; + pixelData: TKMCardinalArray; +begin + // Looks like we need two frames to flush the render-ahead queue? + // Otherwise game controls are rendered too, f.e. + Render(True); + Render(True); + if aFilename = '' then + begin + DateTimeToString(strDate, 'yyyy-mm-dd hh-nn-ss', Now); //2007-12-23 15-24-33 + fileName := ExeDir + 'screenshots\KaM Remake ' + strDate + '.jpeg'; + end + else + fileName := aFilename; + + gRender.ReadRenderedToScreenPixels(w, h, pixelData); + + SavePixelDataToFile(fileName, aImageType, w, h, pixelData); +end; + + +procedure TKMGameApp.SaveGameWholeMapToImage(aImageType: TKMImageType; aMaxImageSize: Integer = 0); +var + dateStr, fileName: string; + screenX, screenY, w, h: Integer; + zoom, mapSizeX, mapSizeY, mapSizeMax, saveSizeMax: Single; + pos: TKMPointF; + zoomBehaviour: TKMZoomBehaviour; + pixelData: TKMCardinalArray; +begin + if gGame = nil then Exit; + + SAVE_MAP_TO_FBO_RENDER := True; + try + // Save viewport params + screenX := gRender.ScreenX; + screenY := gRender.ScreenY; + zoom := gGame.ActiveInterface.Viewport.Zoom; + pos := gGame.ActiveInterface.Viewport.Position; + zoomBehaviour := gGameSettings.ZoomBehaviour; + + // calc resize dimensions + // We show MapX - 1 tiles + mapSizeX := gGame.MapSize.X - 1; + // We show MapY - 1 + top padding for elevated tiles on the map top rows + mapSizeY := gGame.MapSize.Y - 1 + gGame.ActiveInterface.Viewport.TopPad; + mapSizeMax := Max(mapSizeX, mapSizeY); + + saveSizeMax := Min(mapSizeMax * CELL_SIZE_PX, gRender.MaxFBOSize); + + if aMaxImageSize > 0 then + saveSizeMax := Min(saveSizeMax, aMaxImageSize); + + // zbLoose could lead to bad positioned map image + gGameSettings.ZoomBehaviour := zbRestricted; + Resize(Round(saveSizeMax * mapSizeX / mapSizeMax), Round(saveSizeMax * mapSizeY / mapSizeMax)); + + // Zoom out as max as possible + gGame.ActiveInterface.Viewport.Zoom := 0.01; + + // Render once is enough, since we render to an off-screen buffer (FBO) + Render(True); + + DateTimeToString(dateStr, 'yyyy-mm-dd hh-nn-ss', Now); //2007-12-23 15-24-33 + fileName := ExeDir + 'screenshots\' + gGame.Params.Name + ' ' + dateStr + IMAGE_TYPE_EXT[aImageType]; + + gRender.ReadRenderedToFBOPixels(w, h, pixelData); + + SavePixelDataToFile(fileName, aImageType, w, h, pixelData); + finally + SAVE_MAP_TO_FBO_RENDER := False; + end; + + // Restore viewport params + gGameSettings.ZoomBehaviour := zoomBehaviour; // before Resize + Resize(screenX, screenY); + gGame.ActiveInterface.Viewport.Zoom := zoom; + gGame.ActiveInterface.Viewport.Position := pos; +end; + + +function TKMGameApp.CheckDATConsistency: Boolean; +const + // That's the magic CRC of official .dat files and our .json specifications + // Please write down a reason for when you change it: + // - $28810991 - restored + DEFINES_CRC: Cardinal = $28810991; +begin + Result := ALLOW_MP_MODS or (gRes.GetDATCRC = DEFINES_CRC); +end; + + +procedure TKMGameApp.UpdatePerflog; +{$IFNDEF Unix} +{$IFDEF DBG_PERFLOG} +var + memUsed, stackUsed: NativeUInt; +{$ENDIF} +{$ENDIF} +begin + {$IFNDEF Unix} + {$IFDEF DBG_PERFLOG} + memUsed := GetMemUsed; + stackUsed := GetCommittedStackSize; + + gPerfLogs.SectionAddValue(psMemUsed, memUsed div (10*1024), fGlobalTickCount, IntToStr(memUsed div (1024*1024)) + 'Mb'); + gPerfLogs.SectionAddValue(psStackUsed, stackUsed div 128, fGlobalTickCount, IntToStr(stackUsed div 1024) + 'Kb'); + {$ENDIF} + {$ENDIF} +end; + + +procedure TKMGameApp.UpdateState; +begin + if fIsLoading then Exit; //Don't update while toggling locale + + //Some PCs seem to change 8087CW randomly between events like Timers and OnMouse*, + //so we need to set it right before we do game logic processing + Set8087CW($133F); + + Inc(fGlobalTickCount); + //Always update networking for auto reconnection and query timeouts + if fNetworking <> nil then + fNetworking.UpdateState(fGlobalTickCount); + + gVideoPlayer.UpdateState; + + if gGame <> nil then + begin + gGame.UpdateState(fGlobalTickCount); + if gGame.Params.IsMultiPlayerOrSpec and (fGlobalTickCount mod 100 = 0) then + SendMPGameInfo; //Send status to the server every 10 seconds + end + else + fMainMenuInterface.UpdateState(fGlobalTickCount); + + //Every 1000ms + if fGlobalTickCount mod 10 = 0 then + begin + // Music + if gGameSettings.SFX.MusicEnabled and gMusic.IsEnded then + gMusic.PlayNextTrack; //Feed new music track + + //StatusBar + if (gGame <> nil) and not (gGame.IsPaused and BLOCK_GAME_ON_PAUSE) and Assigned(fOnStatusBarUpdate) then + fOnStatusBarUpdate(spTime, 'Time: ' + TimeToString(gGame.MissionTime)); + end; + + UpdatePerflog; +end; + + +// This is our real-time "thread", use it wisely +procedure TKMGameApp.UpdateStateIdle(aFrameTime: Cardinal); +begin + if gGame <> nil then gGame.UpdateStateIdle(aFrameTime); + if gMusic <> nil then gMusic.UpdateStateIdle; + if gSoundPlayer <> nil then gSoundPlayer.UpdateStateIdle; + if fNetworking <> nil then fNetworking.UpdateStateIdle; + if gRes <> nil then gRes.UpdateStateIdle; +end; + + +procedure TKMGameApp.DoGameTick; +begin + if gGame <> nil then + gGame.UpdateGame; +end; + + +end. diff --git a/port-source/kam_remake/src/common/KM_Defaults.pas b/port-source/kam_remake/src/common/KM_Defaults.pas new file mode 100644 index 0000000..a7e5b7c --- /dev/null +++ b/port-source/kam_remake/src/common/KM_Defaults.pas @@ -0,0 +1,1127 @@ +unit KM_Defaults; +{$I KaM_Remake.inc} +interface + +// Global constants +const + MAX_MAP_SIZE = 256; + MIN_MAP_SIZE = 32; + CELL_SIZE_PX = 40; // Single cell size in pixels (width) + CELL_HEIGHT_DIV = 33.333; // Height divider, controlls terrains pseudo-3d look + TOOLBAR_WIDTH = 224; // Toolbar width in game + MAPED_TOOLBAR_WIDTH = 284; // Toolbar width in map editor + TERRAIN_PACE = 200; // Each tile gets updated once per ** ticks (100 by default), Warning, it affects field/tree growth rate + FOW_PACE = 10; // Each tile gets updated once per ** ticks (10 by default) + + MIN_FPS_CAP = 10; // Minimum FPS Cap - limit fps + DEF_FPS_CAP = 60; // Default FPS Cap + MAX_FPS_CAP = 1000; // Maximum FPS Cap (means no CAP at all) + FPS_INTERVAL = 1000; // Time in ms between FPS measurements, bigger value = more accurate result + MENU_DESIGN_X = 1024; // Thats the size menu was designed for. All elements are placed in this size + MENU_DESIGN_Y = 768; // Thats the size menu was designed for. All elements are placed in this size + RESOLUTION_WIDTH_MIN = 1024; // Lowest supported resolution X + RESOLUTION_HEIGHT_MIN = 720; // Lowest supported resolution Y + + // It would be nice to have the declaration as: + // GAME_REVISION_NUM = {$I KM_Revision.inc} + // This would also allow to disuse the initialization section down below + // However for some odd reason this kills Reys IDE .. + {$I KM_Revision.inc}; + {$I KM_NetProtocolRevision.inc}; + +const + // Anything that is a debug mechanism not intended for the Release whould be listed here, so that we know at a glance what kind of build we are having + GAME_VERSION_POSTFIX = '' + {$IFDEF DBG_SKIP_SECURE_AUTH} + ' [UNSECURE]' {$ENDIF} + {$IFDEF DEBUG} + ' [DEBUG]' {$ENDIF} + {$IFDEF WDC64} + ' [x64]' {$ENDIF} + {$IFDEF DBG_RNG_SPY} + ' [RNGSPY]' {$ENDIF} + {$IFDEF DBG_PERFLOG} + ' [PERFLOG]' {$ENDIF} + ; + + GAME_VERSION_PREFIX = ''; // Custom prefix for the test builds + GAME_VERSION_POSTFIX2 = ''; // Custom postfix for the test builds + +var + // Game revision is set in initialisation block + GAME_REVISION: AnsiString; // "r12345" Should be updated for every release (each time save format is changed) + GAME_VERSION: AnsiString; // "Prefix r12345 [ Postfix ] Postfix" Game version string displayed in menu corner + NET_PROTOCOL_REVISON: AnsiString; // "r12345" Clients of this version may connect to the dedicated server + +const + GAME_TITLE = 'Knights and Merchants Remake'; + SETTINGS_FILE = 'KaM Remake Settings.xml'; + SERVER_SETTINGS_FILE = 'KaM Remake Server Settings.ini'; + DEFAULT_LOCALE: AnsiString = 'eng'; + + MP_NICKNAME_LENGTH_MAX = 16; + + LOGS_DELETE_WHEN_OLDER_THAN = 14; // In days + + TEMPLATE_LIBX_FILE_TEXT = 'text.%s.libx'; + + DEFAULT_WATER_LIGHT_MULTIPLIER = 1.3; // Default multiplier for terrain water light + + // Max number of ticks, played on 1 game update. + // We must limit number of ticks per update to be able to leave update cycle fast (when turn off ultra fast speedup, f.e.) + // Also there is a technical limit, of how many ticks we can calculate per update + MAX_TICKS_PER_GAME_UPDATE = 100; + +{$IFDEF DEBUG} + DEBUG_CFG = True; //Debug preset for most usable debug options. ON for Debug build configuration +{$ELSE} + DEBUG_CFG = False; //Debug preset for most usable debug options. OFF for Release build configuration +{$ENDIF} + +var + // These should be True (we can occasionally turn them Off to speed up the debug) + // We keep them as `var` to keep compiler happy (otherwise it sees a lot of "unused var usage" around) + CALC_EXPECTED_TICK :Boolean = not DEBUG_CFG; //Do we calculate expected tick and try to be in-time (send as many tick as needed to get to expected tick) + DYNAMIC_TERRAIN :Boolean = True; //Update terrain each tick to grow things + CHEATS_SP_ENABLED :Boolean = True; //Enable cheats in game (add_resource, instant_win, etc) + FREE_POINTERS :Boolean = True; //If True, units/houses will be freed and removed from the list once they are no longer needed + CAP_MAX_FPS :Boolean = True; //Should limit rendering performance to avoid GPU overheating (disable to measure debug performance) + CRASH_ON_REPLAY :Boolean = True; //Crash as soon as replay consistency fails (random numbers mismatch) + BLOCK_DUPLICATE_APP :Boolean = not DEBUG_CFG; //Do not allow to run multiple games at once (to prevent MP cheating) + DO_NETWORK_AUTH :Boolean = not DEBUG_CFG; //Do network auth. If no its equivalent to use KM_NetAuthUnsecure + QUERY_ON_FORM_CLOSE :Boolean = not DEBUG_CFG; //Do we ask player about lost changes on game exit ? + RESET_DEBUG_CONTROLS :Boolean = not DEBUG_CFG; //Reset Debug controls (F11) on game start + SKIP_LOG_TEMP_COMMANDS:Boolean = True; + BLOCK_GAME_ON_PAUSE :Boolean = not DEBUG_CFG; // Should we block game input, viewport scrolling etc on game pause? + DELETE_OLD_LOGS :Boolean = not DEBUG_CFG; // Should we delete old logs? + + // Gameplay features (aka "feature flags") + // Implemented. Once the feature is implemented and adopted by the players the flag is safe to remove + FEAT_ALLOW_TO_DISMISS_UNITS :Boolean = True; // Show button to order citizens to go back to school + FEAT_SETTINGS_IN_MYDOC :Boolean = True; // Save settings in the "C:\Users\Username\My Documents\My Games\GAME_TITLE\" folder as many other games do + FEAT_UNIT_INTERACTION :Boolean = True; // Unit interaction + FEAT_AVOID_UNITS_IN_PATH :Boolean = True; // Add additional cost to tiles in A* if they are occupied by other units (IsUnit=1) + FEAT_CUSTOM_RANDOM :Boolean = True; // Use our custom random number generator instead of the "Random()" + FEAT_USE_WALKING_DISTANCE :Boolean = True; // Use the walking distance for deciding place to mine rather than direct distance + FEAT_ARCHER_RANDOM_TARGETS :Boolean = True; // Archers use random targets instead of closest + FEAT_HUNGARIAN_GROUP_ORDER :Boolean = True; // Use Hungarian algorithm to reorder warrior groups when walking + FEAT_AI_GENERATE_NAVMESH :Boolean = True; // Generate navmesh for AI to plan attacks/defenses + FEAT_AI_GENERATE_INFLUENCE :Boolean = True; // Generate influence maps for AI to plan attacks/defenses + FEAT_SAVE_COMPRESSION :Boolean = True; // Apply compression to savegame files, (disable to make them more readable) + FEAT_SFX_ADJUST_SAMPLE_RATE :Boolean = True; // Adjust SFX sample rate + + // Not fully implemented yet + FEAT_CCL_WALKCONNECT :Boolean = False; // Use CCL instead of FloodFill for walk-connect (CCL is generaly worse. It's a bit slower, counts 1 tile areas and needs more AreaIDs to work / makes sparsed IDs) + FEAT_DYNAMIC_FOG_OF_WAR :Boolean = False; // Whenever dynamic fog of war is enabled or not + CHECK_8087CW :Boolean = False; // Check that 8087CW (FPU flags) are set correctly each frame, in case some lib/API changed them + FEAT_SCROLL_ACCEL :Boolean = False; // Acceleration for viewport scrolling + PATHFINDER_TO_USE :Byte = 1; // Use TKMPathfindingAStarNew + FEAT_VIDEOS_UNDER_WINE :Boolean = False; // Enable videos under Wine + + //Cache / delivery / pathfinding + CACHE_PATHFINDING :Boolean = True; //Cache routes incase they are needed soon (Vortamic PF runs x4 faster even with lame approach) + CACHE_PATHFINDING_AVOID_LOCKED :Boolean = True; //Cache unsuccesfull tries to find route with avoid locked (as non walkable) + DELIVERY_BID_CALC_USE_PATHFINDING :Boolean = True; //Do we use simple distance on map or pathfinding for calc delivery bids cost? + {$IFDEF WDC} //Work only in Delphi + CACHE_DELIVERY_BIDS: Boolean = True; //Cache delivery bids cost. Must be turned ON if we want to use pathfinding for bid calc, huge impact on performance in that case + {$ENDIF} + UPDATE_DELIVERY_MODE_IMMEDIATELY: Boolean = DEBUG_CFG; // Do we update house delivery mode immediately ? + + WARFARE_ORDER_SEQUENTIAL :Boolean = True; //Pick weapon orders like KaM did + WARFARE_ORDER_PROPORTIONAL :Boolean = False; //New proportional way (looks like a bad idea) + + //These are debug things, should be False + {Runner} + {$IFDEF PARALLEL_RUNNER} + THREAD_NUMBER :Word = 1; // Thread number for parallel runner and saves + {$ENDIF} + DEFAULT_PEACE_TIME :Word = 0; //Default peacetime for SP games when SP_DEFAULT_ADVANCED_AI set to True + {AI} + SP_BOOST_AI_BUILD :Boolean = False; //Boost build algorithm of the new AI (performance impact) + SP_DEFAULT_ADVANCED_AI :Boolean = False; //Set advanced AI as default for SP games + {User interface options} + DEBUG_SPEEDUP_SPEED :Integer = 300; //Speed for speedup from debug menu + DEBUG_LOGS :Boolean = True; //Log debug info + DEBUG_SCRIPTING_EXEC :Boolean = False; //Use slow debug executor (about 3 times slower! never use on release version). Using it we can find exact position of execution time error (row/col/pos/module) + USE_KMR_DIR_FOR_SETTINGS:Boolean = DEBUG_CFG; // Do we use KMR local directory for settings? + SKIP_RNG_CHECKS_FOR_SOME_GIC: Boolean = True; //Skip rng checks for Autosave and few other commands to have same AI city with predefined seed + mapconfig + ALLOW_SELECT_ALLIES :Boolean = True; //Do we allow to select ally units or groups + ALLOW_SELECT_ALL :Boolean = DEBUG_CFG; //Do we allow to select all entities (allies and enemies) + DBG_SHOW_EXACT_WARE_COUNT :Boolean = DEBUG_CFG; // Do we show resource amount with K postfix (10123 -> 10k) + DBG_SKIP_SAVE_POINTS_TO_FILE :Boolean = False; // Skip save game savepoints (into .spt file) + DBG_GAME_SAVE_STRIP_FOR_CRC :Boolean = False; // Strip unsynced data from Game saves, to compare saves CRC + ALLOW_LOAD_UNSUP_VERSION_SAVE: + Boolean = DEBUG_CFG; //Allow to try load saves / replay with unsupported version + + DBG_LOG_SETTINGS_SAVE_CALLSTACK :Boolean = True; //todo: DEBUG. Remove after bugfix + DBG_DISALLOW_TO_SAVE_RANDOM_CHECKS :Boolean = False; // Forbid saving of random checks data in to separate file (used by Runner) + DBG_NET_RANDOM_CHECK_EVERY_TICK :Boolean = True; //todo: DEBUG. Remove in release, no need to check on every tick in release version + + DBG_KILL_GROUP_BUTTON :Boolean = DEBUG_CFG; // The button to kill group + DBG_KILL_UNIT_BUTTON :Boolean = DEBUG_CFG; // Show unit 'Kill' button + DBG_SAVE_GAME_AS_TEXT :Boolean = True; {Save game serialized} //todo: DEBUG. set to False before releases + + SHOW_ENEMIES_STATS :Boolean = False; //Do we allow to show enemies stats during the game + SHOW_CONTROLS_OVERLAY :Boolean = False; //Draw colored overlays ontop of controls! always Off here + SHOW_CONTROLS_ID :Boolean = False; //Draw controls ID + SHOW_FOCUSED_CONTROL :Boolean = False; //Outline focused control + SHOW_CONTROL_OVER :Boolean = False; //Outline control with mouse over + SHOW_TEXT_OUTLINES :Boolean = False; //Display text areas outlines + SKIP_RENDER_TEXT :Boolean = False; //Skip painting labels + ENABLE_DESIGN_CONTORLS :Boolean = False; //Enable special mode to allow to move/edit controls + MODE_DESIGN_CONTROLS :Boolean = False; //Special mode to move/edit controls activated by F7, it must block OnClick events! always Off here + DBG_UI_HINT_POS :Boolean = False; //Show coordinates near cursor, with axis + + OVERLAY_RESOLUTIONS :Boolean = False; //Render constraining frame + LOCAL_SERVER_LIST :Boolean = False; //Instead of loading server list from master server, add localhost:56789 (good for testing) + SHOW_LOG_IN_CHAT :Boolean = False; //Show log messages in MP game chat + LOG_GAME_TICK :Boolean = False; //Log game tick + {Gameplay display} + SHOW_UNITS_IN_HOUSE :Boolean = False; //Should we hide units when they are in house ? + IGNORE_MOUSE_SCROLLING :Boolean = False; //Ignore mouse scrolling + SKIP_RENDER :Boolean = False; //Skip all the rendering in favor of faster logic + DO_NOT_SKIP_LOAD_TILESET:Boolean = False; //Do not skip load tileset even if SKIP_RENDER is set + SKIP_SOUND :Boolean = False; //Skip all the sounds in favor of faster logic + SKIP_LOADING_CURSOR :Boolean = False; //Skip loading and setting cursor + SKIP_SETTINGS_SAVE :Boolean = False; //Skip save main/game settings into the ini file +// SKIP_POINTER_REF_CHECK :Boolean = False; //Skip Pointer reference check (gGame.AllowGetPointer) + AGGRESSIVE_REPLAYS :Boolean = True; //Write a command gicTempDoNothing every tick in order to find exactly when a replay mismatch occurs + SHOW_GAME_TICK :Boolean = DEBUG_CFG; //Show game tick next to game time + SHOW_FPS :Boolean = False; //Show FPS + SHOW_TERRAIN_IDS :Boolean = False; //Show number of every tile terrain on it (also show layers terrain ids) + SKIP_TER_RENDER_ANIMS :Boolean = False; //Skip render terrain animations + SKIP_TER_RENDER_LIGHT :Boolean = False; //Skip render terrain lighting + SKIP_TER_RENDER_SHADOW :Boolean = False; //Skip render terrain shadows + DO_DEBUG_TER_RENDER :Boolean = False; //Do we do debug terrain layers? If yes, then only chosen terrain layers will be rendered + DEBUG_TERRAIN_LAYERS :set of Byte = [0,1,2,3]; //Terrain layers to render while debugging with DO_DEBUG_LAYERS + SKIP_RENDER_TER_LAYERS :Boolean = False; //Do not render terrain layers + SHOW_TERRAIN_KINDS :Boolean = False; //Show terrain kind ids on every tile corner + SHOW_TERRAIN_OVERLAYS :Boolean = False; //Show terrain tile overlays + SHOW_TERRAIN_HEIGHT :Boolean = False; //Show terrain tile overlays + SHOW_TERRAIN_TILES_GRID :Boolean = False; //Show terrain tiles grid + SHOW_BRUSH_APPLY_AREA :Boolean = False; //Show brushes apply area + SHOW_TERRAIN_WIRES :Boolean = False; //Makes terrain height visible + SHOW_TERRAIN_PASS :Byte = 0; //Byte(TKMTerrainPassability) + SHOW_UNIT_ROUTES :Boolean = False; //Draw unit routes + SHOW_UNIT_ROUTES_STEPS :Boolean = False; //Draw unit routes step numbers + SHOW_SEL_BUFFER :Boolean = False; //Display selection buffer + SHOW_PROJECTILES :Boolean = False; //Shows projectiles trajectory + SHOW_POINTER_DOTS :Boolean = False; //Show pointer count as small dots below unit/houses + SHOW_GROUND_LINES :Boolean = False; //Show a line below all sprites to mark the ground height used in Z-Order + SHOW_UNIT_MOVEMENT :Boolean = False; //Draw unit movement overlay (occupied tile), Only if unit interaction enabled + SHOW_VIEWPORT_POS :Boolean = False; //Draw viewport position + SHOW_JAM_METER :Boolean = False; //Show jam meter value on terrain + SHOW_TILE_OBJECT_ID :Boolean = False; //Show tiles object ID + SHOW_TILES_OWNER :Boolean = False; //Show tiles owner + SHOW_TREE_AGE :Boolean = False; //Show tiles tree age + SHOW_FIELD_AGE :Boolean = False; //Show tiles field age + SHOW_TILE_LOCK :Boolean = False; //Show tiles lock + SHOW_TILE_UNIT :Boolean = False; //Show tiles units + SHOW_VERTEX_UNIT :Boolean = False; //Show vertex unit (if vertex is occupied) + SHOW_UIDs :Boolean = False; //Show units/groups/houses UIDs + SHOW_WALK_CONNECT :Boolean = False; //Show floodfill areas of interconnected areas + SHOW_GROUP_MEMBERS_POS :Boolean = False; //Show group members position + TEST_VIEW_CLIP_INSET :Boolean = False; //Renders smaller area to see if everything gets clipped well + OUTLINE_ALL_SPRITES :Boolean = False; //Render outline around every sprite + SHOW_ATTACK_RADIUS :Boolean = False; //Render towers/archers attack radius + DISPLAY_SOUNDS :Boolean = False; //Display sounds on map + RENDER_3D :Boolean = False; //Experimental 3D render + LINEAR_FILTER_SPRITES :Boolean = False; //To check pixel sampling alignment issues (bouncing) at 100% zoom + HOUSE_BUILDING_STEP :Single = 0; + WATER_LIGHT_MULTIPLIER :Single = DEFAULT_WATER_LIGHT_MULTIPLIER; //Terrain light multiplier + OVERLAY_NAVMESH :Boolean = False; //Show navmesh + OVERLAY_HIGHLIGHT_POLY :Integer = 0; //Highlight plygon in navmesh + OVERLAY_DEFENCES :Boolean = False; //Show AI defence perimeters + OVERLAY_DEFENCES_A :Boolean = False; //Show AI defence perimeters (Animation) + OVERLAY_INFLUENCE :Boolean = False; //Show influence map + OVERLAY_OWNERSHIP :Boolean = False; //Show ownership map + OVERLAY_AVOID :Boolean = False; //Show avoidance map + OVERLAY_AI_BUILD :Boolean = False; //Show build progress of new AI + OVERLAY_AI_COMBAT :Boolean = False; //Show combat marks of new AI + OVERLAY_AI_PATHFINDING :Boolean = False; //Show combat marks of new AI + OVERLAY_AI_EYE :Boolean = False; //Show Eye vision of new AI + OVERLAY_AI_SOIL :Boolean = False; //Show Soil vision of new AI + OVERLAY_AI_FLATAREA :Boolean = False; //Show FlatArea vision of new AI + OVERLAY_AI_ROUTES :Boolean = False; //Show Routes to resources vision of new AI + OVERLAY_AI_SUPERVISOR :Boolean = False; //Show Supervisor vision of new AI + OVERLAY_AI_VEC_FLD_ENEM :Boolean = False; //Show Vector field of enemy (combat AI) + OVERLAY_AI_VEC_FLD_ALLY :Boolean = False; //Show Vector field of ally (combat AI) + OVERLAY_AI_CLUSTERS :Boolean = False; //Show combat clusters (combat AI) + OVERLAY_AI_ALLIEDGROUPS :Boolean = False; //Show show allied groups (combat AI) + {Render} + SAVE_MAP_TO_FBO_RENDER :Boolean = False; //Do Render is performed into FBO off-screen buffer? + {Stats} + SHOW_SPRITE_COUNT :Boolean = False; //display rendered controls/sprites count + SHOW_POINTER_COUNT :Boolean = False; //Show debug total count of unit/house pointers being tracked + SHOW_CMDQUEUE_COUNT :Boolean = False; //Show how many commands were processed and stored by TGameInputProcess + SHOW_NETWORK_DELAY :Boolean = False; //Show the current delay in multiplayer game + SHOW_ARMYEVALS :Boolean = False; //Show result of enemy armies evaluation + SHOW_AI_WARE_BALANCE :Boolean = False; //Show wares balance (Produced - Consumed) + SHOW_NET_PACKETS_STATS :Boolean = False; //Show network packet statistics + SHOW_NET_PACKETS_LIMIT :Integer = 1; + SHOW_SELECTED_OBJ_INFO :Boolean = False; //Show selected object (Unit/Group + Unit/House) data (UID/order/action etc) + SHOW_HANDS_INFO :Boolean = False; //Show hands info + SHOW_VIEWPORT_INFO :Boolean = False; //Show viewport info + SHOW_GIP :Boolean = False; //Show GIP commands + SHOW_GIP_AS_BYTES :Boolean = False; //Show GIP commands as bytes (or as 'parsed type' if False) + INI_HITPOINT_RESTORE :Boolean = False; //Use the hitpoint restore rate from the INI file to compare with KaM + ALLOW_CAMP_SCRIPT_DATA_UNCOMPRESSED + :Boolean = False; //Allow to save script data uncompressed + SLOW_MAP_SCAN :Boolean = False; //Scan maps with a pause to emulate uncached file access + SLOW_CAMPAIGN_SCAN :Boolean = False; //Scan campaigns with a pause to emulate uncached file access + SLOW_CAMP_PROGRESS_SAVE_LOAD:Boolean = False; //Scan campaigns with a pause to emulate uncached file access + SLOW_SAVE_SCAN :Boolean = False; //Scan saves with a pause to emulate uncached file access + SLOW_GAME_SAVE_ASYNC :Boolean = False; //Emulate slow game save (in the async save thread) + SLOW_MAP_SAVE_LOAD :Boolean = False; //Load map or save to emulate slow network + DO_PERF_LOGGING :Boolean = False; //Write each ticks time to log (DEPRECATED PERF_LOGGER) + MP_RESULTS_IN_SP :Boolean = False; //Display each players stats in SP + SHOW_DEBUG_OVERLAY_BEVEL:Boolean = True; //Show debug text overlay Bevel (for better text readability) + SHOW_LOG_IN_GUI :Boolean = False; //Show log in GUI + UPDATE_LOG_FOR_GUI :Boolean = False; //Update log to be shown in GUI + DEBUG_TEXT_FONT_ID :Integer = 7; //Debug font ID (7 is fntMonospaced) + + {Gameplay} + LOBBY_SET_SPECS_DEFAULT :Boolean = DEBUG_CFG; //Set 'Allow spectators' flag in the lobby by default + LOBBY_HOST_AS_SPECTATOR :Boolean = DEBUG_CFG; //Host lobby as spectator by default + CUSTOM_SEED_VALUE :Integer = 0; //Custom seed value. Not applied if set to 0 + PAUSE_GAME_BEFORE_TICK :Integer = -1; //Pause after specified game tick + MAKE_SAVEPT_BEFORE_TICK :Integer = -1; //Make savepoint after a certain tick (for both game and replay) + ALLOW_SAVE_IN_REPLAY :Boolean = DEBUG_CFG; //Allow to save game from replay, good for debug + SAVE_NO_COMPRESSION :Boolean = False; // do not use compression for game save body + SAVE_GAME_AFTER_LOAD :Boolean = False; // Save game immediately after load, could be useful for debugging + + {Gameplay cheats} + UNLOCK_CAMPAIGN_MAPS :Boolean = False; //Unlock more maps for debug + REDUCE_SHOOTING_RANGE :Boolean = False; //Reduce shooting range for debug + MULTIPLAYER_CHEATS :Boolean = DEBUG_CFG; //Allow cheats and debug overlays (e.g. CanWalk) in Multiplayer + DEBUG_CHEATS :Boolean = DEBUG_CFG; //Cheats for debug (place scout and reveal map) which can be turned On from menu + MULTIPLAYER_SPEEDUP :Boolean = DEBUG_CFG; //Allow you to use F8 to speed up multiplayer for debugging (only effects local client) + SKIP_EXE_CRC :Boolean = False; //Don't check KaM_Remake.exe CRC before MP game (useful for testing with different versions) + ALLOW_MP_MODS :Boolean = DEBUG_CFG; //Don't let people enter MP mode if they are using mods (unit.dat, house.dat, etc.) + ALLOW_TAKE_AI_PLAYERS :Boolean = False; //Allow to load SP maps without Human player (usefull for AI testing) + {Data output} + BLOCK_SAVE :Boolean = False; //Block saving game (used in parallel Runner) + BLOCK_FILE_WRITE :Boolean = False; //Block to write into txt file (used in parallel Runner) + WRITE_DECODED_MISSION :Boolean = False; //Save decoded mission as txt file + WRITE_WALKTO_LOG :Boolean = False; //Write even more output into log + slows down game noticably + WriteResourceInfoToTXT :Boolean = False; //Whenever to write txt files with defines data properties on loading + LOG_FONTS_RAM_USAGE :Boolean = False; + + +const + MAX_WARES_IN_HOUSE = 5; //Maximum resource items allowed to be in house + MAX_WARES_OUT_WORKSHOP = 5; //Maximum sum resource items allowed to output in workshops. Value: 5 - 20; + MAX_WARES_ORDER = 999; //Number of max allowed items to be ordered in production houses (Weapon/Armor/etc) + + MAX_UNITS_AROUND_HOUSE = 50; + +const + MAX_WOODCUTTER_CUT_PNT_DISTANCE = 5; //Max distance for woodcutter new cutting point from his house + +const + MAX_HANDS = 27; // Maximum players (human or AI) per map + MAX_LOBBY_PLAYERS = 12; // Maximum number of players (not spectators) allowed in the lobby. Map can have additional AI locations up to MAX_HANDS (for co-op). + MAX_LOBBY_SPECTATORS = 2; // Slots available in lobby. Additional slots can be used by spectators + MAX_LOBBY_SLOTS = MAX_LOBBY_PLAYERS + MAX_LOBBY_SPECTATORS; + MAX_TEAMS = MAX_LOBBY_PLAYERS div 2; + + GAME_SPEED_NORMAL = 1; + GAME_SP_SPEED_MAX = 10; + GAME_MP_SPEED_MAX = 2.5; + + SPEED_PACE_DEFAULT = 100; // ms, frequency of game updates + + + AUTOSAVE_COUNT_DEF = 5; //How many autosaves to backup by default - this MUST be variable (Parallel Runner) + AUTOSAVE_COUNT_MIN = 1; + AUTOSAVE_COUNT_MAX = 50; // 99 max, since it should be 2-digit width to cope with our rename algo + AUTOSAVE_FREQUENCY_MIN = 300; // 30 sec + AUTOSAVE_FREQUENCY_MAX = 6000; // 10 minutes + AUTOSAVE_FREQUENCY_DEFAULT = 600; // 1 min, How often to do autosave, every N ticks + BASESAVE_NAME = 'basesave'; + AUTOSAVE_SAVE_NAME = 'autosave'; + AUTOSAVE_AFTER_PT_END_SAVE_NAME = 'autosave_after_pt_end'; + CRASHREPORT_SAVE_NAME = 'crashreport'; + + // Checkpoint, which are made in the memory while watching replay + REPLAY_SAVEPOINT_FREQUENCY_MIN = 30*10; //30 sec + REPLAY_SAVEPOINT_FREQUENCY_MAX = 60*60*10; // 1 hour + REPLAY_SAVEPOINT_FREQUENCY_DEF = 5*60*10; // 5 min + REPLAY_SAVEPOINT_CNT_MAX = 60; // Max number of replay autosaves + + // Checkpoints, which are made during the game and saved in the .spt file +{$IFDEF DEBUG} +var +{$ENDIF} + GAME_SAVE_CHECKPOINT_FREQ_MIN: Integer = 5*60*10; // 5 min + GAME_SAVE_CHECKPOINT_FREQ_MAX: Integer = 2*60*60*10; // 2 hours + GAME_SAVE_CHECKPOINT_FREQ_DEF: Integer = 15*60*10; // 15 minutes + GAME_SAVE_CHECKPOINT_CNT_LIMIT_MIN: Integer = 0; // Min limit for number of game checkpoints + GAME_SAVE_CHECKPOINT_CNT_LIMIT_MAX: Integer = 60; // Max limit for number of game checkpoints + GAME_SAVE_CHECKPOINT_CNT_LIMIT_DEF: Integer = 30; // Def limit for number of game checkpoints +{$IFDEF DEBUG} +const +{$ENDIF} + + BEACON_COOLDOWN = 400; //Minimum time in milliseconds between beacons + + DYNAMIC_HOTKEYS_NUM = 20; // Number of dynamic hotkeys + + GLOBAL_TICK_UPDATE_FREQ = 100; // In ms. + +var + HITPOINT_RESTORE_PACE: Word = 100; //1 hitpoint is restored to units every X ticks (using Humbelum's advice) + +const + TERRAIN_MAX_BLENDING_LEVEL = 15; + TERRAIN_DEF_BLENDING_LVL = (TERRAIN_MAX_BLENDING_LEVEL + 1) div 2; + MAPED_BRUSH_MAX_SIZE = 32; + + //Here we store options that are hidden somewhere in code + //Unit condition + UNIT_CONDITION_PACE = 10; // Check unit conditions only once per 10 ticks + UNIT_CONDITION_FULL_THRESHOLD = 0.9; // Unit condition level, until which we allow unit to eat foods + UNIT_MAX_CONDITION = 45*60; // Minutes of life. In KaM it's 45min + UNIT_MIN_CONDITION = 6*60; // If unit condition is less it will look for Inn. In KaM it's 6min + TROOPS_FEED_MAX = 0.55; // Maximum amount of condition a troop can have to order food (more than this means they won't order food) + UNIT_CONDITION_BASE = 0.6; // Base amount of health a unit starts with (measured in KaM) + UNIT_CONDITION_RANDOM = 0.1; // Random jitter of unit's starting health (KaM did not have this, all units started the same) + TROOPS_TRAINED_CONDITION = 0.6; // Condition troops start with when trained (measured from KaM) + + //Units are fed acording to this: (from knightsandmerchants.de tips and tricks) + //Bread = +40% + //Sausages = +60% + //Wine = +20% (We changed this to +30% for balance) + //Fish = +50% + BREAD_RESTORE = 0.4; + SAUSAGE_RESTORE = 0.6; + WINE_RESTORE = 0.3; + FISH_RESTORE = 0.5; + + + DEFAULT_HITPOINT_RESTORE = 100; //1 hitpoint is restored to units every X ticks (using Humbelum's advice) + TIME_BETWEEN_MESSAGES = 4*600; //Time between messages saying house is unoccupied or unit is hungry. In KaM it's 4 minutes + + WATCHTOWER_RANGE_MAX = 6.99; //Measured in KaM. Distance from the doorway of tower + WATCHTOWER_RANGE_MIN = 0; //In KaM towers have no minimum range, they will shoot any unit less than the range + + GROUP_LINK_RADIUS = 5; //Radius to search for groups to link to after being trained at the barracks (measured from KaM) + + FRIENDLY_FIRE = True; //Whenever archers could kill fellow men with their arrows + + NET_DROP_PLAYER_MIN_WAIT = 15; // in seconds. Host must wait at least this long before dropping disconnected players + ANNOUNCE_BUILD_MAP = 30*60*10; //30 minutes + ANNOUNCE_BATTLE_MAP = 2*60*10; //2 minutes + + RESULTS_UPDATE_RATE = 15; //Each 1.5 sec + CHARTS_SAMPLING_FOR_ECONOMY = 150; //Each 15sec + CHARTS_SAMPLING_FOR_TACTICS = 30; //Each 3sec, cos average game length is much shorter + + RETURN_TO_LOBBY_SAVE = 'paused'; + DOWNLOADED_LOBBY_SAVE = 'downloaded'; + + SERVER_DEFAULT_UDP_SCAN_PORT = 56788; + SERVER_DEFAULT_UDP_ANNOUNCE_PORT = 56789; + EMPTY_ROOM_DEFAULT_GAME_REVISION = 0; //Placeholder for game revision in room + + LOC_RANDOM = 0; + LOC_SPECTATE = -1; + LOC_ANY = -1000; + + MIN_PLAYER_COLOR_DIST = 0.15; // Minimun color distance between players + + EXT_SAVE_TXT_DOT = '.txt'; + EXT_SAVE_REPLAY = 'rpl'; + EXT_SAVE_MAIN = 'sav'; + EXT_SAVE_BASE = 'bas'; + EXT_SAVE_MP_LOCAL = 'sloc'; + EXT_FILE_SCRIPT = 'script'; + + // Well-known extensions + // Note that typically extension include a dot, so if would be desireable to operate with only dotted extensions (below) and disuse ones above + EXT_SAVE_REPLAY_DOT = '.' + EXT_SAVE_REPLAY; + EXT_SAVE_GAME_SAVEPTS_DOT = '.spt'; + EXT_SAVE_MAIN_DOT = '.' + EXT_SAVE_MAIN; + EXT_SAVE_MAIN_TXT_DOT = '.' + EXT_SAVE_MAIN + EXT_SAVE_TXT_DOT; + EXT_SAVE_BASE_DOT = '.' + EXT_SAVE_BASE; + EXT_SAVE_MP_LOCAL_DOT = '.' + EXT_SAVE_MP_LOCAL; + EXT_SAVE_RNG_LOG_DOT = '.rng'; + EXT_FILE_SCRIPT_DOT = '.' + EXT_FILE_SCRIPT; + +type + TKMHandID = {type} ShortInt; + TKMHandIDArray = array of TKMHandID; + TKMHandID2Array = array of TKMHandIDArray; + TKMHandEnabledArray = array [0..MAX_HANDS-1] of Boolean; + +const + // Used to reset on new game start + OWN_MARGIN_DEF :Byte = 190; + OWN_THRESHOLD_DEF:Byte = 126; + +var + //Values are empirical + //todo -cPractical: Can move this to AIInfluences as parameter + OWN_MARGIN :Byte = 190; + OWN_THRESHOLD:Byte = 126; + + +const + LAST_SENT_COMMANDS_TICK_NONE = 0; + +type + TKMCursorMode = ( + cmNone, + cmErase, //Remove player controlled assets (plans, houses) with a confirmation dialog + cmRoad, + cmField, + cmWine, + cmHouses, // Gameplay + + //Map Editor + cmElevate, //Height elevation + cmEqualize, //Height equalization + cmConstHeight, //Constant height brush + cmElevateAll, //Terrain kind elevation + cmBrush, //Terrain brush + cmTiles, // Individual tiles + cmObjects, //Terrain objects + cmObjectsBrush, //Objects brush + cmMagicWater, //Magic water + cmSelection, //Selection manipulations + cmUnits, //Units + cmMarkers, //CenterScreen, FOW, Defence, AIStart, Rally/Cutting Point markers + cmEyedropper, //Terrain eyedropper + cmPaintBucket, //PaintBucket - change color(team) for map objects + cmUniversalEraser, //Universal eraser for units/groups/houses/terrain objects/roads and fields (corn/wine) + cmRotateTile, //Rotate terrain tile + cmOverlays //Tile overlays + ); + + // How cursor field placing will act (depends on which tile LMB was pressed) + TKMCursorFieldMode = ( + cfmNone, // Disabled + cfmPlan, // Placing plans + cfmErase // Erasing plans + ); + + // Shape types for MapEditor + TKMMapEdShape = (hsCircle, hsSquare); + + +const + MARKER_REVEAL = 1; + MARKER_DEFENCE = 2; + MARKER_CENTERSCREEN = 3; + MARKER_AISTART = 4; + MARKER_RALLY_POINT = 5; + + DATE_TIME_ZERO: TDateTime = 0; // DateTime as 0, for simplicity. Some compilers (Delphi Berlin, f.e.) can't handle TDateTime(0) + +const + DIR_CURSOR_CIRCLE_RAD = 32; //Radius of the direction selector cursor restriction area + DIR_CURSOR_NA_RAD = 20; //Radius of centeral part that has no direction + + +type + // Game result + TKMGameResultMsg = ( + grWin, // Player has won the game + grDefeat, // Player was defeated + grCancel, // Game was cancelled (unfinished) + grError, // Some known error occured + grDisconnect, // Disconnected from multiplayer game + grSilent, // Used when loading savegame from running game (show no screens) + grReplayEnd, // Replay was cancelled - return to menu without screens + grMapEdEnd, // Map Editor was closed - return to menu without screens + grGameContinues // Game is not finished yet, it is continious + ); + +type + TKMAllianceType = (atEnemy, atAlly); + +const + FOG_OF_WAR_MIN = 80; //Minimum value for explored but FOW terrain, MIN/ACT determines FOW darkness + FOG_OF_WAR_ACT = 160; //Until this value FOW is not rendered at all + FOG_OF_WAR_MAX = 255; //This is max value that FOW can be, MAX-ACT determines how long until FOW appears + FOG_OF_WAR_INC = 128; //Increment for FOW + FOG_OF_WAR_DEC = 12; //Decrement for FOW + +const + MAPED_HISTORY_DEPTH_MIN = 20; + MAPED_HISTORY_DEPTH_MAX = 1000; + MAPED_HISTORY_DEPTH_DEF = 250; + + +const + MAPS_FOLDER_NAME = 'Maps'; + MAPS_MP_FOLDER_NAME = 'MapsMP'; + MAPS_DL_FOLDER_NAME = 'MapsDL'; + MAPS_RMG_NAME = 'Randomly generated map'; + TUTORIALS_FOLDER_NAME = 'Tutorials'; + CAMPAIGNS_FOLDER_NAME = 'Campaigns'; + SAVES_FOLDER_NAME = 'Saves'; + SAVES_MP_FOLDER_NAME = 'SavesMP'; + SAVES_CMP_FOLDER_NAME = 'SavesCMP'; + + +{ Terrain } +type + //* Terrain passability + TKMTerrainPassability = ( + tpNone, + tpWalk, // General passability of tile for any walking units + tpWalkRoad, // Type of passability for Serfs when transporting wares, only roads have it + tpBuildNoObj, // Can we build a house on this tile after removing an object on the tile or house near it? + tpBuild, // Can we build a house on this tile? + tpMakeRoads, // Thats less strict than house building, roads Can be placed almost everywhere where units Can walk, except e.g. bridges + tpCutTree, // Can tree be cut + tpFish, // Water tiles where fish Can move around + tpCrab, // Sand tiles where crabs Can move around + tpWolf, // Soil tiles where wolfs Can move around + tpElevate, // Nodes which are forbidden to be elevated by workers (house basements, water, etc..) + tpWorker, // Like CanWalk but allows walking on building sites + tpOwn, // For AI ownership + tpFactor // Allows vertex (top left) to be factored as a neighbour in flattening algorithm (it is to stop mines flattening strangely due to surrounding hills) + ); + TKMTerrainPassabilitySet = set of TKMTerrainPassability; + + TKMHeightPass = (hpWalking, hpBuilding, hpBuildingMines); + +const + PASSABILITY_GUI_TEXT: array [TKMTerrainPassability] of UnicodeString = ( + 'Unused', + 'Can walk', + 'Can walk road', + 'Can build without|object', + 'Can build', + 'Can make roads', + 'Can cut tree', + 'Can fish', + 'Can crab', + 'Can wolf', + 'Can elevate', + 'Can worker', + 'Can own', + 'Can factor' + ); + + +type + TKMWalkConnect = ( + wcWalk, + wcRoad, + wcFish, //Required for fisherman finding fish in a pond, NOT for fish movement (uses steering). Updated ONLY on load because water doesn't change. + wcWork //CanWorker areas + ); + + TKMWalkConnectSet = set of TKMWalkConnect; + + +const + UID_NONE: Integer = -1; // Would be better to have it 0. But now it's -1 for backwards compatibility + + +{Units} +type + //* Unit type + TKMUnitType = (utNone, utAny, + utSerf, utWoodcutter, utMiner, utAnimalBreeder, + utFarmer, utCarpenter, utBaker, utButcher, + utFisher, utBuilder, utStonemason, utSmith, + utMetallurgist, utRecruit, + + utMilitia, utAxeFighter, utSwordFighter, utBowman, + utCrossbowman, utLanceCarrier, utPikeman, utScout, + utKnight, utBarbarian, + + utRebel, utRogue, utWarrior, utVagabond, + //utCatapult, utBallista, + + utWolf, utFish, utWatersnake, utSeastar, + utCrab, utWaterflower, utWaterleaf, utDuck); + //* Unit type set + TKMUnitTypeSet = set of TKMUnitType; + +const + UNIT_MIN = utSerf; + UNIT_MAX = utDuck; + CITIZEN_MIN = utSerf; + CITIZEN_MAX = utRecruit; + WARRIOR_MIN = utMilitia; + WARRIOR_MAX = utVagabond; + WARRIOR_EQUIPABLE_BARRACKS_MIN = utMilitia; //Available from barracks + WARRIOR_EQUIPABLE_BARRACKS_MAX = utKnight; + WARRIOR_EQUIPABLE_TH_MIN = utBarbarian; //Available from Townhall + WARRIOR_EQUIPABLE_TH_MAX = utVagabond; + HUMANS_MIN = utSerf; + HUMANS_MAX = utVagabond; + ANIMAL_MIN = utWolf; + ANIMAL_MAX = utDuck; + + UNITS_VALID = [UNIT_MIN..UNIT_MAX]; + UNITS_ANIMALS = [ANIMAL_MIN..ANIMAL_MAX]; + UNITS_CITIZEN = [CITIZEN_MIN..CITIZEN_MAX]; + UNITS_WARRIORS = [WARRIOR_MIN..WARRIOR_MAX]; + UNITS_HUMAN = [HUMANS_MIN..HUMANS_MAX]; + + WARRIORS_IRON = [utSwordFighter, utCrossbowman, utPikeman, utKnight]; + + CITIZENS_CNT = Integer(CITIZEN_MAX) - Integer(CITIZEN_MIN) + 1; + WARRIORS_CNT = Integer(WARRIOR_MAX) - Integer(WARRIOR_MIN) + 1; + +type + TKMCheckAxis = (axX, axY); + +// Used for AI defence and linking troops +type + //* Group type + TKMGroupType = (gtNone, gtAny, gtMelee, gtAntiHorse, gtRanged, gtMounted); + //* Group type set + TKMGroupTypeSet = set of TKMGroupType; + +const + GROUP_TYPE_MIN = gtMelee; + GROUP_TYPE_MAX = gtMounted; + GROUP_TYPE_MIN_OFF = Ord(GROUP_TYPE_MIN); + GROUP_TYPES: array [GROUP_TYPE_MIN..GROUP_TYPE_MAX] of Byte = (0, 1, 2, 3); + GROUP_TYPES_VALID = [GROUP_TYPE_MIN..GROUP_TYPE_MAX]; + GROUP_TYPES_CNT = Integer(GROUP_TYPE_MAX) - Integer(GROUP_TYPE_MIN) + 1; + +type + TKMGroupTypeArray = array [GROUP_TYPE_MIN..GROUP_TYPE_MAX] of Word; + //* AI army type + TKMArmyType = (atIronThenLeather, atLeather, atIron, atIronAndLeather); + +const + UNIT_TO_GROUP_TYPE: array [WARRIOR_MIN..WARRIOR_MAX] of TKMGroupType = ( + gtMelee,gtMelee,gtMelee, //utMilitia, utAxeFighter, utSwordsman + gtRanged,gtRanged, //utBowman, utArbaletman + gtAntiHorse,gtAntiHorse, //utPikeman, utHallebardman, + gtMounted,gtMounted, //utHorseScout, utCavalry, + gtMelee, //utBarbarian + //TPR Army + gtAntiHorse, //utPeasant + gtRanged, //utSlingshot + gtMelee, //utMetalBarbarian + gtMounted //utHorseman + {gtRanged,gtRanged, //utCatapult, utBallista,} + ); + + //AI's prefences for training troops + AI_TROOP_TRAIN_ORDER: array [GROUP_TYPE_MIN..GROUP_TYPE_MAX, 1..3] of TKMUnitType = ( + (utSwordFighter, utAxeFighter, utMilitia), + (utPikeman, utLanceCarrier, utNone), + (utCrossbowman, utBowman, utNone), + (utKnight, utScout, utNone)); + +type + TKMGoInDirection = (gdGoOutside=-1, gdGoInside=1); //Switch to set if unit goes into house or out of it + + TKMUnitThought = (thNone, thEat, thHome, thBuild, thStone, thWood, thDeath, thQuest, thDismiss); + +const + //TileCursors + TC_OUTLINE = 0; + TC_BLOCK = 479; + TC_BLOCK_MINE = 480; + TC_ENTRANCE = 481; + TC_BLOCK_ENTRANCE = 482; + +type + TKMUnitTaskType = ( + uttUnknown, // Uninitialized task to detect bugs + uttSelfTrain, uttDeliver, uttBuildRoad, uttBuildWine, uttBuildField, + uttBuildHouseArea, uttBuildHouse, uttBuildHouseRepair, uttGoHome, uttDismiss, + uttGoEat, uttMining, uttDie, uttGoOutShowHungry, uttAttackHouse, + uttThrowRock + ); + + TKMUnitActionName = (uanStay, uanWalkTo, uanGoInOut, uanAbandonWalk, uanFight, uanStormAttack, uanSteer); + + TKMUnitActionType = ( + uaWalk = 120, uaWork, uaSpec, uaDie, uaWork1, + uaWork2, uaWorkEnd, uaEat, uaWalkArm, uaWalkTool, + uaWalkBooty, uaWalkTool2, uaWalkBooty2, uaUnknown); + TKMUnitActionTypeSet = set of TKMUnitActionType; + +const + UNIT_ACT_MIN = uaWalk; + UNIT_ACT_MAX = uaWalkBooty2; + UNIT_ACT_STR: array [TKMUnitActionType] of string = ( + 'uaWalk', 'uaWork', 'uaSpec', 'uaDie', 'uaWork1', + 'uaWork2', 'uaWorkEnd', 'uaEat', 'uaWalkArm', 'uaWalkTool', + 'uaWalkBooty', 'uaWalkTool2', 'uaWalkBooty2', 'uaUnknown'); + + +type + TKMGatheringScript = ( + gsNone, + gsWoodCutterCut, gsWoodCutterPlant, + gsFarmerSow, gsFarmerCorn, gsFarmerWine, + gsFisherCatch, + gsStoneCutter, + gsCoalMiner, gsGoldMiner, gsIronMiner, + gsHorseBreeder, gsSwineBreeder); + +{Houses in game} +type + //House has 3 basic states: no owner inside, owner inside, owner working inside + TKMHouseState = (hstEmpty, hstIdle, hstWork); + //These are house building states + //todo -cComplicated: Can add other possible build states like hbsDigged, hbsRoadConnected, hbsBuildStart + TKMHouseBuildState = (hbsNoGlyph, hbsWood, hbsStone, hbsDone); + + TKMHouseActionType = ( + haWork1, haWork2, haWork3, haWork4, haWork5, //Start, InProgress, .., .., Finish + haSmoke, haFlagpole, haIdle, + haFlag1, haFlag2, haFlag3, + haFire1, haFire2, haFire3, haFire4, haFire5, haFire6, haFire7, haFire8); + TKMHouseActionSet = set of TKMHouseActionType; + +const + HOUSE_ACTION_STR: array [TKMHouseActionType] of string = ( + 'ha_Work1', 'ha_Work2', 'ha_Work3', 'ha_Work4', 'ha_Work5', //Start, InProgress, .., .., Finish + 'ha_Smoke', 'ha_FlagShtok', 'ha_Idle', + 'ha_Flag1', 'ha_Flag2', 'ha_Flag3', + 'ha_Fire1', 'ha_Fire2', 'ha_Fire3', 'ha_Fire4', 'ha_Fire5', 'ha_Fire6', 'ha_Fire7', 'ha_Fire8' + ); + + +{Terrain} +type + //* Field type + TKMFieldType = ( + ftNone, + ftRoad, + ftCorn, + ftWine, + ftInitWine //Reset rotation and set grapes ground, but without Grapes yet + ); + + TKMHouseStage = ( + hsNone, //Nothing, clear area + hsFence, //Wooden fence, partially walkable as workers digg it up + hsBuilt //Done + ); + + //There are 4 steps in tile blocking scheme: + // 0. Tile is normally walkable + // 1. Set the tile as CantBuild + // 2. Sets the tile as CantWalk to anyone except workers who are performing + // the digging task, so they could escape the area. + // The Worker will push out any unit on his way. + // sidenote: CanElevate is per-vertex property, hence it's not identical to CanWorker + // 3. Set the tile as fully blocked + TKMTileLock = ( // CanBuild CanWalk CanWorker CanElevate House Digged Fenced + tlNone, // X X X X - - - + tlFenced, // - X X X X - X + tlDigged, // - - X X X X X + tlHouse, // - - - - X X - + //Used by workers making roads/fields to prevent you from building over them + tlFieldWork,// - X X X - X - + tlRoadWork // - X X X - X - + ); + + // Panel indexes on KM_FormMain.StatusBar + TKMStatusBarPanel = ( + spKMRVersion, + spMapSize, + spCursorCoord, + spTile, + spTime, + spFPS, + spObjectUID, + spControlId); + +type + TKMMapSize = (msNone, msXS, msS, msM, msL, msXL, msXXL); + +const + MAP_SIZE_ENUM_MIN = msXS; + MAP_SIZE_ENUM_MAX = msXXL; + +type + //Enum representing map visible layers + TKMGameVisibleLayer = ( + mlObjects, + mlHouses, + mlUnits, + mlOverlays, + mlMiningRadius, + mlTowersAttackRadius, + mlUnitsAttackRadius, + mlDefencesAll, + mlFlatTerrain + ); + + TKMMapVisibleLayerSet = set of TKMGameVisibleLayer; //Set of above enum + + //Enum representing mapEd visible layers + TKMMapEdVisibleLayers = ( + melDeposits, + melRevealFOW, + melCenterScreen, + melAIStart, + melSelection, + melWaterFlow, + melMapResize, + melDefences + ); + + TKMMapEdVisibleLayerSet = set of TKMMapEdVisibleLayers; //Set of above enum + + TKMDebugControls = (dcNone, dcFlatTerrain); + +const + DEV_SETTINGS_XML_FILENAME = 'kmr_dev.xml'; + + +const + //Colors available for selection in multiplayer + MP_COLOR_COUNT = 26; + MP_PLAYER_COLORS: array [1..MP_COLOR_COUNT] of Cardinal = ( + $FF0000EB, // 1 Red + $FF076CF8, // 2 Orange + $FF00B5FF, // 3 Gold + $FF07FFFF, // 4 Lauenburg yellow + $FF0EC5A2, // 5 Lime green + $FF88FF88, // 6 Light green + $FF07FF07, // 7 Neon green + $FF00A100, // 8 Bright green + $FF134B00, // 9 Dark green + $FF7A9E00, // 10 Teal + $FFFACE64, // 11 Sky blue + $FFFB886D, // 12 Light violet blue + $FFCC6600, // 13 Medium blue + $FF581F00, // 14 Dark blue + $FFCB3972, // 15 Violet (Amethyst) + $FF720468, // 16 Purple + $FFDE8FFB, // 17 Pink + $FFFF07FF, // 18 Magenta + $FF4A00A8, // 19 Dark pink + $FF00005E, // 20 Maroon + $FF666690, // 21 Light brown + $FF103C52, // 22 Brown + $FF519EC9, // 23 Tan + $FFFFFFFF, // 24 White + $FF838383, // 25 Grey + $FF1B1B1B // 26 Black + ); + + //Players colors, as they appear in KaM when the color is not specified in the script, copied from palette values. + //Using these as the defaults when loading the script means the colors will be the same as KaM when not defined. + //Default IDs from KaM: + {229, //Red + 36, //Cyan + 106, //Green + 20, //Magenta + 233, //Yellow + 213, //Grey + 3, //Black + 3, //Black + 255 //White} + DEFAULT_PLAYERS_COLORS: array [0..MAX_HANDS-1] of Cardinal = ( + $FF0000EB, // 1 Red + $FF076CF8, // 2 Orange + $FF00B5FF, // 3 Gold + $FF07FFFF, // 4 Lauenburg yellow + $FF0EC5A2, // 5 Lime green + $FF88FF88, // 6 Light green + $FF07FF07, // 7 Neon green + $FF00A100, // 8 Bright green + $FF134B00, // 9 Dark green + $FF7A9E00, // 10 Teal + $FFFACE64, // 11 Sky blue + $FFFB886D, // 12 Light violet blue + $FFCC6600, // 13 Medium blue + $FF581F00, // 14 Dark blue + $FFCB3972, // 15 Violet (Amethyst) + $FF720468, // 16 Purple + $FFDE8FFB, // 17 Pink + $FFFF07FF, // 18 Magenta + $FF9DB4FB, // 27 Light Light Red + $FF4A00A8, // 19 Dark pink + $FF00005E, // 20 Maroon + $FF666690, // 21 Light brown + $FF103C52, // 22 Brown + $FF519EC9, // 23 Tan + $FFFFFFFF, // 24 White + $FF838383, // 25 Grey + $FF1B1B1B // 26 Black + ); + + // Colors which are used for an MP teams + MP_TEAM_COLORS: array[0..MAX_TEAMS - 1] of Cardinal = ( + $FF0000EB, // 1 Red + $FF076CF8, // 2 Orange + $FF07FFFF, // 3 Lauenburg yellow + $FF07FF07, // 4 Neon green + $FFFACE64, // 5 Sky blue + $FFDE8FFB // 6 Pink + ); + + // DEBUG colors (transparent color - opacity will be added by debug tools) + tcBlack = $000000; + tcBlue = $FF0000; + tcCream = $F0FBFF; + tcCyan = $FFFF00; + tcDarkGrey = $808080; + tcFuchsia = $FF00FF; + tcGreen = $008000; + tcGrey = $808080; + tcLightGrey = $C0C0C0; + tcLimeGreen = $00FF00; + tcMaroon = $000080; + tcMediumGrey = $A4A0A0; + tcMintGreen = $C0DCC0; + tcNavyBlue = $800000; + tcOliveGreen = $008080; + tcPurple = $800080; + tcRed = $0000FF; + tcSilver = $C0C0C0; + tcSkyBlue = $F0CAA6; + tcTeal = $808000; + tcWhite = $FFFFFF; + tcYellow = $00FFFF; + + //Interface colors + icGreen = $FF00C000; + icYellow = $FF07FFFF; + icOrange = $FF0099FF; + icRed = $FF0707FF; + icBlue = $FFFF0707; + icCyan = $FFFFFF00; + + icTransparent = $00; + icDarkGray = $FF606060; + icDarkGrayTrans = $60606060; + icDarkestGrayTrans = $20303030; + icGray = $FF808080; + icGray2 = $FF888888; + icLightGray = $FFA0A0A0; + icLightGray2 = $FFD0D0D0; + icLightGrayTrans = $80A0A0A0; + icWhite = $FFFFFFFF; + icBlack = $FF000000; + icLightCyan = $FFFFFF80; + icLightOrange = $FF80CCFF; + icLightRed = $FF7070FF; + icLight2Red = $FFB0B0FF; + icLightLightRed = $FF9DB4FB; + icDarkOrange = $FF0060FF; + icDarkCyan = $FFB0B000; + icLightGreen = $FF00F000; + icDeepGreen = $FF008000; + icGreenYellow = $FF00FFBB; + icTeal = $FF808000; + + icPink = $FFFF00FF; + icDarkPink = $FFAA00AA; + + icSteelBlue = $FFA56D53; + icDarkBlue = $FF770707; + + icRoyalYellow = $FF4AC7FF; + icGoldenYellow = $FF00B0FF; + icAmberBrown = $FF006797; + icDarkGoldenRod = $FF0080B0; // brown shade color + + icBarColorGreen = $FF00AA26; + icBarColorBlue = $FFBBAA00; + + icBlackish = $FF333333; + + // Interface colors (by usage) + clWhiteText = icWhite; + clBlackText = icBlackish; + + clPingLow = icGreen; + clPingNormal = icYellow; + clPingHigh = icOrange; + clPingCritical = icRed; + + clFpsCritical = icRed; + clFpsLow = icOrange; + clFpsNormal = icYellow; + clFpsHigh = icGreen; + + clTextSelection = icSteelBlue; + + clMPSrvDetailsGameInfoFont = icLightGray; + clStatsUnitDefault = icWhite; + clStatsUnitMissingHL = $FF0080FF; + + clMessageUnitUnread = icGoldenYellow; + clMessageUnitUnreadHL = icRoyalYellow; + clMessageUnitRead = icDarkGoldenRod; + clMessageUnitReadHL = icAmberBrown; + + clLobbyOpponentAll = icRoyalYellow; + clLobbyOpponentAllHL = icAmberBrown; + + clListSelShape = $88888888; + clListSelOutline = icWhite; + clListSelShapeUnfocused = $66666666; + clListSelOutlineUnfocused = icLightGray; + clListSeparatorShape = icDarkGray; + + clMapEdBtnField = icYellow; + clMapEdBtnWine = icYellow; + + clChartHighlight = icPink; + clChartHighlight2 = icDarkPink; + clChartDashedHLn = icDarkGray; + clChartDashedVLn = icDarkGrayTrans; + clChartPeacetimeLn = icDarkGoldenRod; + clChartPeacetimeLbl = icGoldenYellow; + clChartSeparator = icTransparent; + + clChkboxOutline = icLightGrayTrans; + + clPlayerSelf = icRed; + clPlayerAlly = icYellow; + clPlayerEnemy = icCyan; + + clScriptCmdName = icYellow; + clScriptCmdParam = icLightGray; + + clSaveLoadOk = icWhite; + clSaveLoadTry = icLightLightRed; + clSaveLoadError = icLightRed; + +// clGameSelf = icRed; +// clGameAlly = icYellow; +// clGameEnemy = icCyan; + + GROUP_IMG: array [GROUP_TYPE_MIN..GROUP_TYPE_MAX] of Word = ( + 371, 374, + 376, 377); + + GROUP_TXT_COLOR: array [GROUP_TYPE_MIN..GROUP_TYPE_MAX] of Cardinal = ( + icWhite, + icGreen, + icPink, + icRed); + +var + ExeDir: UnicodeString; + +const + WARRIORS_POWER_RATES: array [WARRIOR_MIN..WARRIOR_MAX] of Single = ( + 1, 2.4, 5.2, // utMilitia, utAxeFighter, utSwordFighter + 2.4, 4, // utBowman, utCrossbowman + 2, 4, // utLanceCarrier, utPikeman + 3.3, 6.3, // utScout, utKnight + 5.3, 0.8, 1.2, // utBarbarian, utRebel, utRogue + 5.3, 1.9 // utWarrior, utVagabond + ); + +implementation +uses + SysUtils; + +initialization +begin + GAME_REVISION := AnsiString('r' + IntToStr(GAME_REVISION_NUM)); + GAME_VERSION := GAME_VERSION_PREFIX + GAME_REVISION + GAME_VERSION_POSTFIX + GAME_VERSION_POSTFIX2; + //Clients of this net protocol version may connect to the dedicated server + NET_PROTOCOL_REVISON := AnsiString('r' + IntToStr(NET_PROTOCOL_REVISION_NUM)); +end; + +end. diff --git a/port-source/kam_remake/src/res/KM_ResSprites.pas b/port-source/kam_remake/src/res/KM_ResSprites.pas index 28fcaed..de13ba9 100644 --- a/port-source/kam_remake/src/res/KM_ResSprites.pas +++ b/port-source/kam_remake/src/res/KM_ResSprites.pas @@ -2,7 +2,7 @@ unit KM_ResSprites; {$I KaM_Remake.inc} interface uses - Classes, Math, SysUtils, Generics.Collections, + Classes, Math, SysUtils, SyncObjs, Generics.Collections, KM_CommonTypes, KM_Defaults, KM_Pics, KM_IoPNG, KM_RenderTypes, KM_ResTexts, KM_ResTileset, KM_ResHouses, KM_ResTypes, KM_ResTilesetTypes, @@ -27,9 +27,9 @@ const RXX_HEADER: array [TKMRXXFormat] of AnsiString = ('', '', 'RXX1', 'RXX2'); type - {$IFDEF LOAD_GAME_RES_ASYNC} - TTGameResourceLoader = class; - {$ENDIF} + TKMGameResourceRequestState = (grsIdle, grsAcquire, grsCPURunning, + grsReadyForGPU, grsCancelling, grsReady, grsFailed); + TKMGameResourceWorkerOutcome = (grwoCompleted, grwoCancelled, grwoFailed); // Atlas data, needed for Texture Atlas Generation TKMSpriteAtlasData = record @@ -57,14 +57,17 @@ type fAtlases: TKMSpriteAtlases; procedure Allocate(aCount: Integer); virtual; //Allocate space for data that is being loaded + procedure AllocateRXData(aCount: Integer); procedure AllocateTemp(aCount: Integer); procedure ReadRXZHeader(aStream: TStream; out aFormat: TKMRXXFormat); procedure CollectSpriteFilesToOverloadInFolder(const aFolder: string; aFileList: TStringList); {$IFNDEF NO_OGL} procedure MakeGFX_BinPacking(aTexType: TKMTexFormat; aIDList: TList; var aBaseRAM, aColorRAM, aTexCount: Cardinal; - aFillGFXData: Boolean = True; aOnCheckTerminated: TBooleanFuncSimple = nil); overload; + aFillGFXData: Boolean = True; aOnCheckTerminated: TBooleanFuncSimple = nil; aMaxAtlasSize: Integer = -1); overload; procedure MakeGFX_BinPacking(aTexType: TKMTexFormat; aStartingIndex: Integer; var aBaseRAM, aColorRAM, aTexCount: Cardinal; aFillGFXData: Boolean = True; aOnCheckTerminated: TBooleanFuncSimple = nil); overload; + procedure MakeGFXInternal(aAlphaShadows: Boolean; aIDList: TList; aFillGFXData: Boolean; + aOnCheckTerminated: TBooleanFuncSimple; aMaxAtlasSize: Integer); {$ENDIF} public constructor Create(aRT: TRXType; aTemp: Boolean = False); @@ -84,11 +87,13 @@ type //Load from atlas format, no MakeGFX bin packing needed procedure LoadFromRXAFile(const aFileName: string); + procedure LoadFromRXAStream(aStream: TStream; aAllocateGFXData: Boolean); procedure LoadFromRXAAndGenTextures(const aFileName: string); procedure OverloadGeneratedFromFolder(aAlphaShadows: Boolean; const aFolder: string; aSoftenShadows: Boolean = True; aOnCheckTerminated: TBooleanFuncSimple = nil); procedure LoadFromRXXFile(const aFileName: string; aStartingIndex: Integer = 1); + procedure LoadFromRXXStream(aStream: TStream; aStartingIndex: Integer; aAllocateGFXData: Boolean); procedure OverloadRXDataFromFolder(const aFolder: string; aOnProgress: TProc; aSoftenShadows: Boolean = True); function GetSoftenShadowType(aID: Integer): TKMSpriteSoftening; @@ -114,9 +119,7 @@ type procedure ClearGameResGenTemp; - {$IFDEF LOAD_GAME_RES_ASYNC} - procedure GenerateTexturesFromLoadedRXZ(aIsRXA: Boolean); - {$ENDIF} + procedure GenerateTexturesFromLoadedRXZ; procedure ClearTemp; virtual;//Release non-required data end; @@ -134,11 +137,20 @@ type fGenTerrainToTerKindLegacy: array of TKMGenTerrainInfo; fGameRXTypes: TStringList; //list of TRXType for game resources + fGameRequestState: TKMGameResourceRequestState; + fGameRequestSerial: QWord; + fGameRXIndex: Integer; + fGameRequestFailureClass, fGameRequestFailureText: string; + fGameResWorker: TThread; + fWorkerCompletionCS: TCriticalSection; + fWorkerCompletionPending: Boolean; + fWorkerCompletionSerial: QWord; + fWorkerCompletionRXType: TRXType; + fWorkerCompletionOutcome: TKMGameResourceWorkerOutcome; + fWorkerCompletionErrorClass, fWorkerCompletionErrorText: string; + fGamePublishedThroughIndex: Integer; + fGameRecoveryPending: array[TRXType] of Boolean; fTemp: Boolean; - {$IFDEF LOAD_GAME_RES_ASYNC} - fGameResLoader: TTGameResourceLoader; // thread of game resource loader - fGameResLoadCompleted: Boolean; // Flag to show Game Resource Loading is completed - {$ENDIF} fGenTerrainTransitions: array[Succ(tkCustom)..High(TKMTerrainKind)] of array[Succ(mkNone)..High(TKMTileMaskKind)] @@ -161,17 +173,21 @@ type function GetSpritesRXAFilePath(aRT: TRXType): string; - {$IFDEF LOAD_GAME_RES_ASYNC} - procedure ManageAsyncResLoader(const aCallerName: String); - procedure StopAsyncResourceLoader; - function GetNextLoadRxTypeIndex(aRT: TRXType): Integer; - {$ENDIF} + function GetGameResourcesReady: Boolean; + procedure AcceptGameResourceRequest(aAlphaShadows, aForceReload: Boolean); + procedure FailGameResourceRequest(aRT: TRXType; const aClass, aMessage: string); + procedure AdvanceGameResourceRequest; + procedure StartGameResourceCPUStage(aRequestSerial: QWord; aRT: TRXType; aAlphaShadows: Boolean); + function TryFinalizeCurrentGameResourceGPU: Boolean; + procedure ClearGameResourceCPUPreparation(aRT: TRXType); public constructor Create(aStepProgress: TKMEvent = nil; aStepCaption: TUnicodeStringEvent = nil; aTemp: Boolean = False); destructor Destroy; override; procedure LoadMenuResources; procedure LoadGameResources(aAlphaShadows: Boolean; aForceReload: Boolean = False); + procedure PreloadGameResources(aAlphaShadows: Boolean); + procedure StartGameSpriteRecovery; procedure ClearTemp; class function AllTilesOnOneAtlas: Boolean; @@ -184,9 +200,7 @@ type property Sprites[aRT: TRXType]: TKMSpritePack read GetSprites; default; property GenTerrainTransitions[aTerKind: TKMTerrainKind; aMaskKind: TKMTileMaskKind; aMaskType: TKMTileMaskType; aMaskSubtype: TKMTileMaskSubType]: Word read GetGenTerrainTransitions; - {$IFDEF LOAD_GAME_RES_ASYNC} - property GameResLoadCompleted: Boolean read fGameResLoadCompleted; - {$ENDIF} + property GameResourcesReady: Boolean read GetGameResourcesReady; //Used externally to access raw RGBA data (e.g. by ExportAnim) function LoadSprites(aRT: TRXType; aAlphaShadows: Boolean): Boolean; @@ -200,27 +214,21 @@ type procedure UpdateStateIdle; end; - TKMAsyncLoadStage = (lsLoad, lsGenMain, lsOverload, lsGenOverload); - - {$IFDEF LOAD_GAME_RES_ASYNC} - // Game resource loader thread - TTGameResourceLoader = class(TThread) + TKMGameResourceWorker = class(TThread) private - fResSprites: TKMResSprites; + fOwner: TKMResSprites; + fSerial: QWord; + fRT: TRXType; fAlphaShadows: Boolean; + fInput: TStream; + fInputIsRXA: Boolean; function IsTerminated: Boolean; - procedure Log(const aString: string); + procedure Complete(aOutcome: TKMGameResourceWorkerOutcome; const aClass, aMessage: string); public - RXType: TRXType; - LoadStepDone: LongBool; // flag to show, when another RXX / RXA load is completed - LastLoadedRXA: LongBool; // flag to show, what type of RX we were loading: RXA or RXX - LoadStage: TKMAsyncLoadStage; - constructor Create(aResSprites: TKMResSprites; aAlphaShadows: Boolean; aRxType: TRXType); + constructor Create(aOwner: TKMResSprites; aSerial: QWord; aRT: TRXType; aAlphaShadows, aInputIsRXA: Boolean; aInput: TStream); destructor Destroy; override; - procedure Execute; override; end; - {$ENDIF} TKMTexCoords = record TexID: Cardinal; @@ -242,9 +250,6 @@ uses StrUtils, {$ENDIF} KromUtils, - {$IFDEF LOAD_GAME_RES_ASYNC} - - {$ENDIF} KM_SoftShadows, KM_Resource, KM_ResUnits, {$IFNDEF NO_OGL} KM_Render, @@ -257,7 +262,6 @@ uses const MAX_GAME_ATLAS_SIZE = 2048; //Max atlas size for KaM. No need for bigger atlases - SPRITE_TYPE_EXPORT_NAME: array [TKMSpriteAtlasType] of string = ('Base', 'Mask'); LOG_EXTRA_GFX: Boolean = False; OVERLOAD_SKIP_MASK = 'skip'; @@ -569,11 +573,16 @@ end; procedure TKMSpritePack.Allocate(aCount: Integer); begin - fRXData.Count := aCount; - - aCount := fRXData.Count + 1; if not fTemp then - SetLength(gGFXData[fRT], aCount); + SetLength(gGFXData[fRT], aCount + 1); + AllocateRXData(aCount); +end; + + +procedure TKMSpritePack.AllocateRXData(aCount: Integer); +begin + fRXData.Count := aCount; + aCount := aCount + 1; SetLength(fRXData.Flag, aCount); SetLength(fRXData.Size, aCount); SetLength(fRXData.Pivot, aCount); @@ -775,10 +784,23 @@ end; procedure TKMSpritePack.LoadFromRXXFile(const aFileName: string; aStartingIndex: Integer = 1); +var + inputStream: TStream; +begin + if not KaMResourceExists(aFileName) then Exit; + inputStream := KaMOpenResourceStream(aFileName); + try + LoadFromRXXStream(inputStream, aStartingIndex, True); + finally + inputStream.Free; + end; +end; + + +procedure TKMSpritePack.LoadFromRXXStream(aStream: TStream; aStartingIndex: Integer; aAllocateGFXData: Boolean); var I: Integer; rxxCount: Integer; - inputStream: TStream; decompressionStream: TDecompressionStream; rxxFormat: TKMRXXFormat; begin @@ -787,17 +809,13 @@ begin else if SKIP_RENDER then Exit; end; - if not KaMResourceExists(aFileName) then Exit; - - inputStream := KaMOpenResourceStream(aFileName); - try - ReadRXZHeader(inputStream, rxxFormat); + ReadRXZHeader(aStream, rxxFormat); // Not sure what to do yet, silently "fail" for now if rxxFormat = rxxUnknown then Exit; - decompressionStream := TDecompressionStream.Create(inputStream); + decompressionStream := TDecompressionStream.Create(aStream); try decompressionStream.Read(rxxCount, 4); @@ -807,7 +825,8 @@ begin if rxxCount = 0 then Exit; - Allocate(aStartingIndex + rxxCount - 1); + if aAllocateGFXData then Allocate(aStartingIndex + rxxCount - 1) + else AllocateRXData(aStartingIndex + rxxCount - 1); decompressionStream.Read(fRXData.Flag[aStartingIndex], rxxCount); @@ -830,40 +849,46 @@ begin finally decompressionStream.Free; end; +end; + + +procedure TKMSpritePack.LoadFromRXAFile(const aFileName: string); +var + inputStream: TFileStream; +begin + if not FileExists(aFileName) then Exit; + inputStream := TFileStream.Create(aFileName, fmOpenRead or fmShareDenyNone); + try + LoadFromRXAStream(inputStream, True); finally inputStream.Free; end; end; -procedure TKMSpritePack.LoadFromRXAFile(const aFileName: string); +procedure TKMSpritePack.LoadFromRXAStream(aStream: TStream; aAllocateGFXData: Boolean); {$IFNDEF NO_OGL} var I: Integer; SAT: TKMSpriteAtlasType; rxxCount, atlasCount, spriteCount, dataCount: Integer; - inputStream: TFileStream; decompressionStream: TDecompressionStream; rxxFormat: TKMRXXFormat; {$ENDIF} begin - if not FileExists(aFileName) then Exit; - {$IFNDEF NO_OGL} case fRT of rxTiles: if SKIP_RENDER and not DO_NOT_SKIP_LOAD_TILESET then Exit; else if SKIP_RENDER then Exit; end; - inputStream := TFileStream.Create(aFileName, fmOpenRead or fmShareDenyNone); - try - ReadRXZHeader(inputStream, rxxFormat); + ReadRXZHeader(aStream, rxxFormat); // Not sure what to do yet, silently "fail" for now if rxxFormat = rxxUnknown then Exit; - decompressionStream := TDecompressionStream.Create(inputStream); + decompressionStream := TDecompressionStream.Create(aStream); try decompressionStream.Read(rxxCount, 4); @@ -872,7 +897,7 @@ begin if rxxCount = 0 then Exit; - Allocate(rxxCount); + if aAllocateGFXData then Allocate(rxxCount) else AllocateRXData(rxxCount); decompressionStream.Read(fRXData.Flag[1], fRXData.Count); @@ -916,9 +941,6 @@ begin finally decompressionStream.Free; end; - finally - inputStream.Free; - end; {$ENDIF} end; @@ -926,9 +948,7 @@ end; procedure TKMSpritePack.LoadFromRXAAndGenTextures(const aFileName: string); begin LoadFromRXAFile(aFileName); - {$IFDEF LOAD_GAME_RES_ASYNC} - GenerateTexturesFromLoadedRXZ(True); - {$ENDIF} + GenerateTexturesFromLoadedRXZ; end; @@ -1279,7 +1299,7 @@ begin for I := aStartingIndex to fRXData.Count do idList.Add(I); - MakeGFX(aAlphaShadows, idList, aFillGFXData, aOnCheckTerminated); + MakeGFXInternal(aAlphaShadows, idList, aFillGFXData, aOnCheckTerminated, GetMaxAtlasSize); finally idList.Free; end; @@ -1287,6 +1307,13 @@ end; procedure TKMSpritePack.MakeGFX(aAlphaShadows: Boolean; aIDList: TList; aFillGFXData: Boolean = True; aOnCheckTerminated: TBooleanFuncSimple = nil); +begin + MakeGFXInternal(aAlphaShadows, aIDList, aFillGFXData, aOnCheckTerminated, GetMaxAtlasSize); +end; + + +procedure TKMSpritePack.MakeGFXInternal(aAlphaShadows: Boolean; aIDList: TList; aFillGFXData: Boolean; + aOnCheckTerminated: TBooleanFuncSimple; aMaxAtlasSize: Integer); var I, K: Integer; texType: TKMTexFormat; @@ -1300,7 +1327,7 @@ begin else texType := tfRGB5A1; - MakeGFX_BinPacking(texType, aIDList, baseRAM, colorRAM, texCount, aFillGFXData, aOnCheckTerminated); + MakeGFX_BinPacking(texType, aIDList, baseRAM, colorRAM, texCount, aFillGFXData, aOnCheckTerminated, aMaxAtlasSize); if LOG_EXTRA_GFX then begin @@ -1468,16 +1495,13 @@ begin Inc(aTexCount); - if aFillGFXData and EXPORT_SPRITE_ATLASES and (fRT in EXPORT_SPRITE_ATLASES_LIST) then - SaveToPng(aSpriteInfo[I].Width, aSpriteInfo[I].Height, atlasData, - ExeDir + 'Export\GenTextures\' + RX_INFO[fRT].FileName + '_' + SPRITE_TYPE_EXPORT_NAME[aMode] + IntToStr(I) + '.png'); end; end; // This algorithm is planned to take advantage of more efficient 2D bin packing procedure TKMSpritePack.MakeGFX_BinPacking(aTexType: TKMTexFormat; aIDList: TList; var aBaseRAM, aColorRAM, aTexCount: Cardinal; - aFillGFXData: Boolean = True; aOnCheckTerminated: TBooleanFuncSimple = nil); + aFillGFXData: Boolean = True; aOnCheckTerminated: TBooleanFuncSimple = nil; aMaxAtlasSize: Integer = -1); function CheckTerminated: Boolean; begin Result := Assigned(aOnCheckTerminated) and aOnCheckTerminated; @@ -1487,7 +1511,7 @@ var I, J, K: Integer; spriteSizes: TIndexSizeArray; spriteInfo: TBinArray; - atlasSize, allTilesAtlasSize: Integer; + atlasSize, allTilesAtlasSize, maxAtlasSize: Integer; begin aBaseRAM := 0; aColorRAM := 0; @@ -1507,17 +1531,22 @@ begin end; SetLength(spriteSizes, K); + if aMaxAtlasSize > 0 then + maxAtlasSize := aMaxAtlasSize + else + maxAtlasSize := GetMaxAtlasSize; + //For RX with only 1 texture we can set small size, as 512, it will be auto enlarged to POT(image size) if K = 1 then atlasSize := 512 else if fRT = rxTiles then begin allTilesAtlasSize := MakePOT(Ceil(sqrt(K))*(32+2*fPad)); //Tiles are 32x32 - atlasSize := Min(GetMaxAtlasSize, allTilesAtlasSize); //Use smallest possible atlas size for tiles (should be 1024, until many new tiles were added) + atlasSize := Min(maxAtlasSize, allTilesAtlasSize); //Use smallest possible atlas size for tiles (should be 1024, until many new tiles were added) if atlasSize = allTilesAtlasSize then AllTilesInOneTexture := True; end else - atlasSize := GetMaxAtlasSize; + atlasSize := maxAtlasSize; SetLength(spriteInfo, 0); BinPack(spriteSizes, atlasSize, fPad, spriteInfo); @@ -1564,12 +1593,11 @@ begin end; -{$IFDEF LOAD_GAME_RES_ASYNC} // Generate texture atlases from previosly prepared SpriteInfo data (loaded from RXX, Bin Packed, copied to atlas) // Preparation was done asynchroniously by TTGameResourceLoader thread // Texture generating task can be done only by main thread, as OpenGL does not work with multiple threads // Note: this could be from the loader thread by using `Synchronise` procedure -procedure TKMSpritePack.GenerateTexturesFromLoadedRXZ(aIsRXA: Boolean); +procedure TKMSpritePack.GenerateTexturesFromLoadedRXZ; {$IFNDEF NO_OGL} var I: Integer; @@ -1592,15 +1620,10 @@ begin //Now that we know texture IDs we can fill GFXData structure SetGFXData(texID, Container, SAT); - if ((not aIsRXA and EXPORT_SPRITE_ATLASES) or (aIsRXA and EXPORT_SPRITE_ATLASES_RXA)) - and (fRT in EXPORT_SPRITE_ATLASES_LIST) then - SaveToPng(Container.Width, Container.Height, Data, - ExeDir + 'Export\GenTextures\' + RX_INFO[fRT].FileName + IfThen(aIsRXA, '_rxa_', '_') + SPRITE_TYPE_EXPORT_NAME[SAT] + IntToStr(texID) + '.png'); end; end; {$ENDIF} end; -{$ENDIF} { TKMResSprites } @@ -1611,6 +1634,19 @@ begin inherited Create; fGameRXTypes := TStringList.Create; + fWorkerCompletionCS := TCriticalSection.Create; + fGameRequestState := grsIdle; + fGameRequestSerial := 0; + fGameRXIndex := -1; + fGameRequestFailureClass := ''; + fGameRequestFailureText := ''; + fGameResWorker := nil; + fWorkerCompletionPending := False; + fWorkerCompletionSerial := 0; + fWorkerCompletionErrorClass := ''; + fWorkerCompletionErrorText := ''; + fGamePublishedThroughIndex := -1; + FillChar(fGameRecoveryPending, SizeOf(fGameRecoveryPending), 0); fTemp := aTemp; for RT := Low(TRXType) to High(TRXType) do @@ -1628,19 +1664,30 @@ end; destructor TKMResSprites.Destroy; var RT: TRXType; + cleanupRT: TRXType; + hasCleanup: Boolean; begin + Inc(fGameRequestSerial); + hasCleanup := (fGameRXIndex >= 0) and (fGameRXIndex < fGameRXTypes.Count); + if hasCleanup then + cleanupRT := TRXType(StrToInt(fGameRXTypes[fGameRXIndex])); + if fGameResWorker <> nil then + begin + fGameResWorker.Terminate; + fGameResWorker.WaitFor; + FreeAndNil(fGameResWorker); + end; + fWorkerCompletionPending := False; + if hasCleanup then + ClearGameResourceCPUPreparation(cleanupRT); fGameRXTypes.Free; - {$IFDEF LOAD_GAME_RES_ASYNC} - // Stop resource loader before Freeing SpritePack, as loader use fRXData and could get an exception there on game exit - if fGameResLoader <> nil then - StopAsyncResourceLoader; - {$ENDIF} if not fTemp then KaMUnpublishGeneratedTerrainProvider(Self); for RT := Low(TRXType) to High(TRXType) do fSprites[RT].Free; + fWorkerCompletionCS.Free; inherited; end; @@ -1659,25 +1706,6 @@ begin end; -{$IFDEF LOAD_GAME_RES_ASYNC} -//Get next game resource RXType to load. Returns -1 if its the last one -function TKMResSprites.GetNextLoadRxTypeIndex(aRT: TRXType): Integer; -var - I: Integer; -begin - Result := -1; - for I := 0 to fGameRXTypes.Count - 1 do - if StrToInt(fGameRXTypes[I]) = Integer(aRT) then - begin - if I = fGameRXTypes.Count - 1 then - Exit(-1) - else - Exit(I + 1); - end; -end; -{$ENDIF} - - //aLegacyGeneration - support for maps with rev <= r10745, where transitions were written as generated terrain Id //instead of generation parameters (TekKind + FullMaskType) //Also there was no mkSoft mask and tkSnowOnTerrain terrain kind @@ -1951,85 +1979,277 @@ begin end; -{$IFDEF LOAD_GAME_RES_ASYNC} -procedure TKMResSprites.StopAsyncResourceLoader; -begin - fGameResLoader.Terminate; - fGameResLoader.WaitFor; - FreeThenNil(fGameResLoader); -end; -{$ENDIF} - - procedure TKMResSprites.LoadGameResources(aAlphaShadows: Boolean; aForceReload: Boolean = False); - - procedure LoadAllResources; - var - RT: TRXType; - begin - for RT := Low(TRXType) to High(TRXType) do - if RX_INFO[RT].Usage = ruGame then - begin - if Assigned(fStepCaption) then - fStepCaption(gResTexts[RX_INFO[RT].LoadingTextID]); - - if fAlphaShadows and FileExists(GetSpritesRXAFilePath(RT)) then - begin - gLog.AddTime('Reading ' + RX_INFO[RT].FileName + '.rxa'); - LoadRXASpritesAndGenTextures(RT); - end - else - begin - gLog.AddTime('Reading ' + RX_INFO[RT].FileName + '.rx'); - LoadSprites(RT, fAlphaShadows); - {$IFNDEF NO_OGL} - fSprites[RT].MakeGFX(fAlphaShadows); - {$ENDIF} - end; - // Clear temp data as fast as possible - fSprites[RT].ClearTemp; - fSprites[RT].ClearGameResGenTemp; - end; - - {$IFDEF LOAD_GAME_RES_ASYNC} - fGameResLoadCompleted := True; - {$ENDIF} - end; - begin - gLog.AddTime('TKMResSprites.LoadGameResources'); - //Remember which version we load, so if it changes inbetween games we reload it + AcceptGameResourceRequest(aAlphaShadows, aForceReload); + repeat + AdvanceGameResourceRequest; + if (fGameRequestState = grsCPURunning) and (fGameResWorker <> nil) then + fGameResWorker.WaitFor + else if (fGameRequestState = grsCancelling) and (fGameResWorker <> nil) then + fGameResWorker.WaitFor + else if fGameRequestState in [grsReady, grsFailed, grsReadyForGPU] then + Break; + until False; + if fGameRequestState = grsReadyForGPU then + raise Exception.Create('Game resource GPU finalization is currently unavailable'); + if fGameRequestState = grsFailed then + raise Exception.Create('Game resource load failed [' + fGameRequestFailureClass + ']: ' + fGameRequestFailureText); +end; + + +procedure TKMResSprites.PreloadGameResources(aAlphaShadows: Boolean); +begin + AcceptGameResourceRequest(aAlphaShadows, False); + if fGameRequestState <> grsFailed then AdvanceGameResourceRequest; +end; + + +procedure TKMResSprites.AcceptGameResourceRequest(aAlphaShadows, aForceReload: Boolean); +var + oldRT: TRXType; +begin + if (fGameRequestSerial <> 0) and not aForceReload and (fAlphaShadows = aAlphaShadows) then Exit; + + Inc(fGameRequestSerial); + if fGameRequestSerial = 0 then Inc(fGameRequestSerial); fAlphaShadows := aAlphaShadows; - {$IFDEF LOAD_GAME_RES_ASYNC} - fGameResLoadCompleted := False; - if gGameSettings.AsyncGameResLoader then + fGameRequestFailureClass := ''; + fGameRequestFailureText := ''; + fGamePublishedThroughIndex := -1; + FillChar(fGameRecoveryPending, SizeOf(fGameRecoveryPending), 0); + if fGameResWorker <> nil then begin - if fGameResLoader <> nil then + fGameResWorker.Terminate; + fGameRequestState := grsCancelling; + Exit; + end; + if fGameRequestState = grsReadyForGPU then + begin + oldRT := TRXType(StrToInt(fGameRXTypes[fGameRXIndex])); + ClearGameResourceCPUPreparation(oldRT); + end; + fGameRXIndex := 0; + fGameRequestState := grsAcquire; +end; + + +function TKMResSprites.GetGameResourcesReady: Boolean; +begin + Result := fGameRequestState = grsReady; +end; + + +procedure TKMResSprites.ClearGameResourceCPUPreparation(aRT: TRXType); +begin + fSprites[aRT].ClearTemp; + fSprites[aRT].ClearGameResGenTemp; +end; + + +procedure TKMResSprites.FailGameResourceRequest(aRT: TRXType; const aClass, aMessage: string); +begin + if (fGameRXIndex >= 0) and (fGameRXIndex < fGameRXTypes.Count) then + ClearGameResourceCPUPreparation(aRT); + fGameRequestFailureClass := aClass; + fGameRequestFailureText := aMessage; + fGameRXIndex := -1; + fGameRequestState := grsFailed; +end; + + +procedure TKMResSprites.StartGameResourceCPUStage(aRequestSerial: QWord; aRT: TRXType; aAlphaShadows: Boolean); +var + names: array[0..2] of string; + I: Integer; + reference: string; + input: TStream; + worker: TKMGameResourceWorker; +begin + names[0] := 'data/Sprites/' + RX_INFO[aRT].FileName + '.rxa'; + names[1] := 'data/Sprites/' + RX_INFO[aRT].FileName + '_a.rxx'; + names[2] := 'data/Sprites/' + RX_INFO[aRT].FileName + '.rxx'; + input := nil; + if not aAlphaShadows then I := 2 else I := 0; + while I <= 2 do + begin + reference := KaMResolvePackagedResource(names[I]); + if KaMResourceExists(reference) then begin - if aForceReload then - StopAsyncResourceLoader - else begin - while not fGameResLoadCompleted do - begin - ManageAsyncResLoader('LoadGameResources'); //check if we have some work to do in this thread - Sleep(5); // wait till load will be completed by fGameResLoader thread - end; - Exit; + input := KaMOpenResourceStream(reference); + Break; + end; + Inc(I); + end; + if input = nil then + raise Exception.Create('Game sprite resource not found: ' + names[2]); + try + worker := TKMGameResourceWorker.Create(Self, aRequestSerial, aRT, aAlphaShadows, I = 0, input); + input := nil; + fGameResWorker := worker; + fGameRequestState := grsCPURunning; + try + worker.Start; + except + fGameResWorker := nil; + worker.Free; + // Execute never ran, so there is no worker-owned preparation to clear. + fGameRXIndex := -1; + raise; + end; + finally + input.Free; + end; +end; + + +function TKMResSprites.TryFinalizeCurrentGameResourceGPU: Boolean; +var + RT: TRXType; +begin + Result := False; + if (fGameRequestSerial = 0) or (fGameRXIndex < 0) or (fGameRXIndex >= fGameRXTypes.Count) then + raise Exception.Create('Game resource GPU finalization has no current pack'); + RT := TRXType(StrToInt(fGameRXTypes[fGameRXIndex])); + if not ((fGameRXIndex = fGamePublishedThroughIndex + 1) + or ((fGameRXIndex <= fGamePublishedThroughIndex) and fGameRecoveryPending[RT])) then + begin + ClearGameResourceCPUPreparation(RT); + raise Exception.Create('Invalid game sprite publication state'); + end; + {$IFNDEF NO_OGL} + if not TKMRender.TextureBackendReady then Exit; + try + SetLength(gGFXData[RT], fSprites[RT].fRXData.Count + 1); + fSprites[RT].GenerateTexturesFromLoadedRXZ; + except + ClearGameResourceCPUPreparation(RT); + raise; + end; + ClearGameResourceCPUPreparation(RT); + {$ELSE} + ClearGameResourceCPUPreparation(RT); + {$ENDIF} + if fGameRXIndex = fGamePublishedThroughIndex + 1 then + fGamePublishedThroughIndex := fGameRXIndex; + fGameRecoveryPending[RT] := False; + Result := True; +end; + + +procedure TKMResSprites.AdvanceGameResourceRequest; +var + RT: TRXType; + serial: QWord; + outcome: TKMGameResourceWorkerOutcome; + errorClass, errorText: string; + I: Integer; +begin + if fGameRequestState = grsCancelling then + begin + if fGameResWorker <> nil then + begin + fWorkerCompletionCS.Acquire; + try + if not fWorkerCompletionPending then Exit; + finally fWorkerCompletionCS.Release end; + RT := TRXType(StrToInt(fGameRXTypes[fGameRXIndex])); + fGameResWorker.WaitFor; FreeAndNil(fGameResWorker); + fWorkerCompletionCS.Acquire; + try + fWorkerCompletionPending := False; + finally + fWorkerCompletionCS.Release; + end; + ClearGameResourceCPUPreparation(RT); + end; + fGameRXIndex := 0; fGameRequestState := grsAcquire; + end; + if fGameRequestState = grsAcquire then + begin + try + if (fGameRXIndex < 0) or (fGameRXIndex >= fGameRXTypes.Count) then + raise Exception.Create('Game resource scheduler index is invalid'); + RT := TRXType(StrToInt(fGameRXTypes[fGameRXIndex])); + if Assigned(fStepCaption) then fStepCaption(gResTexts[RX_INFO[RT].LoadingTextID]); + StartGameResourceCPUStage(fGameRequestSerial, RT, fAlphaShadows); + except + on E: Exception do + begin + FailGameResourceRequest(RT, E.ClassName, E.Message); + raise; end; end; - - if aForceReload then + Exit; + end; + if fGameRequestState = grsCPURunning then + begin + fWorkerCompletionCS.Acquire; + try + if not fWorkerCompletionPending then Exit; + serial := fWorkerCompletionSerial; RT := fWorkerCompletionRXType; + outcome := fWorkerCompletionOutcome; errorClass := fWorkerCompletionErrorClass; errorText := fWorkerCompletionErrorText; + fWorkerCompletionPending := False; + finally fWorkerCompletionCS.Release end; + fGameResWorker.WaitFor; FreeAndNil(fGameResWorker); + if serial <> fGameRequestSerial then begin - fGameResLoadCompleted := False; - fGameResLoader := TTGameResourceLoader.Create(Self, fAlphaShadows, TRXType(StrToInt(fGameRXTypes[0]))); + ClearGameResourceCPUPreparation(RT); + fGameRXIndex := 0; + fGameRequestState := grsAcquire; + Exit; end; - end - else - LoadAllResources; - - {$ELSE} - LoadAllResources; - {$ENDIF} + if (fGameRXIndex < 0) or (RT <> TRXType(StrToInt(fGameRXTypes[fGameRXIndex]))) then + begin + FailGameResourceRequest(RT, 'Exception', 'Game resource worker completion does not match current pack'); + raise Exception.Create('Game resource worker completion does not match current pack'); + end; + if outcome <> grwoCompleted then + begin + if outcome = grwoCancelled then + FailGameResourceRequest(RT, 'Cancelled', 'Game resource worker was cancelled') + else + FailGameResourceRequest(RT, errorClass, errorText); + raise Exception.Create('Game resource load failed [' + fGameRequestFailureClass + ']: ' + fGameRequestFailureText); + end; + fGameRequestState := grsReadyForGPU; + end; + if fGameRequestState = grsReadyForGPU then + begin + try + if not TryFinalizeCurrentGameResourceGPU then Exit; + except + on E: Exception do + begin + fGameRequestFailureClass := E.ClassName; + fGameRequestFailureText := E.Message; + fGameRXIndex := -1; + fGameRequestState := grsFailed; + raise; + end; + end; + if fGameRXIndex < fGameRXTypes.Count - 1 then begin Inc(fGameRXIndex); fGameRequestState := grsAcquire; Exit end; + for I := 0 to fGamePublishedThroughIndex do + if fGameRecoveryPending[TRXType(StrToInt(fGameRXTypes[I]))] then begin fGameRXIndex := I; fGameRequestState := grsAcquire; Exit end; + fGameRXIndex := -1; fGameRequestState := grsReady; + end; +end; + + +procedure TKMResSprites.StartGameSpriteRecovery; +var + I: Integer; +begin + FillChar(fGameRecoveryPending, SizeOf(fGameRecoveryPending), 0); + if (fGameRequestSerial = 0) or (fGameRequestState = grsFailed) then Exit; + for I := 0 to fGamePublishedThroughIndex do + fGameRecoveryPending[TRXType(StrToInt(fGameRXTypes[I]))] := True; + if (fGameRequestState = grsReady) and (fGamePublishedThroughIndex >= 0) then + begin + fGameRXIndex := 0; + fGameRequestState := grsAcquire; + AdvanceGameResourceRequest; + end; end; @@ -2098,10 +2318,8 @@ function TKMResSprites.LoadRXASpritesAndGenTextures(aRT: TRXType): Boolean; begin Result := LoadRXASprites(aRT); - {$IFDEF LOAD_GAME_RES_ASYNC} if Result then - fSprites[aRT].GenerateTexturesFromLoadedRXZ(True); - {$ENDIF} + fSprites[aRT].GenerateTexturesFromLoadedRXZ; end; @@ -2127,73 +2345,10 @@ begin end; -{$IFDEF LOAD_GAME_RES_ASYNC} -procedure TKMResSprites.ManageAsyncResLoader(const aCallerName: String); -var - nextRXTypeI: Integer; -begin - if gGameSettings.AsyncGameResLoader - and (fGameResLoader <> nil) - and fGameResLoader.LoadStepDone then - begin - gLog.AddTime('[AsyncGameResLoader MainTh] [%s] GenTextures RT = %s Stage = %s', - [aCallerName, - GetEnumName(TypeInfo(TRXType), Integer(fGameResLoader.RXType)), - GetEnumName(TypeInfo(TKMAsyncLoadStage), Integer(fGameResLoader.LoadStage))]); - - case fGameResLoader.LoadStage of - lsLoad: ; - lsGenMain: begin - if Assigned(fStepCaption) then - fStepCaption(gResTexts[RX_INFO[fGameResLoader.RXType].LoadingTextID]); - // Generate texture atlas from prepared data for game resources - // OpenGL work mainly with 1 thread only, so we have to call gl functions only from main thread - // That is why we need call this method from main thread only - - // Note: this could be from the loader thread by using `Synchronise` procedure - fSprites[fGameResLoader.RXType].GenerateTexturesFromLoadedRXZ(fGameResLoader.LastLoadedRXA); - - fSprites[fGameResLoader.RXType].ClearTemp; //Clear fRXData sprites temp data, which is not needed anymore - fSprites[fGameResLoader.RXType].ClearGameResGenTemp; //Clear all the temp data used for atlas texture generating - end; - lsOverload: ; - lsGenOverload: begin - if fGameResLoader.LastLoadedRXA then - begin - fSprites[fGameResLoader.RXType].GenerateTexturesFromLoadedRXZ(True); - end; - - nextRXTypeI := GetNextLoadRxTypeIndex(fGameResLoader.RXType); // get next RXType to load - if (nextRXTypeI = -1) then - begin - //Load is completed, we can stop loading thread - StopAsyncResourceLoader; - fGameResLoadCompleted := True; // mark loading game res as completed - end - else - fGameResLoader.RXType := TRXType(StrToInt(fGameRXTypes[nextRXTypeI])); - end; - end; - - if fGameResLoader <> nil then - begin - fGameResLoader.LoadStage := TKMAsyncLoadStage((Ord(fGameResLoader.LoadStage) + 1) mod 4); - - // Make this atomic, since LoadStepDone is accessed in different threads - AtomicExchange(Integer(fGameResLoader.LoadStepDone), Integer(False)); - end; - - gLog.AddTime('[AsyncGameResLoader MainTh] [' + aCallerName + '] DONE'); - end; -end; -{$ENDIF} - - procedure TKMResSprites.UpdateStateIdle; begin - {$IFDEF LOAD_GAME_RES_ASYNC} - ManageAsyncResLoader('UpdateStateIdle'); - {$ENDIF} + if (fGameRequestSerial <> 0) and not (fGameRequestState in [grsReady, grsFailed]) then + AdvanceGameResourceRequest; end; @@ -2207,105 +2362,80 @@ begin end; -{$IFDEF LOAD_GAME_RES_ASYNC} -{ TTGameResourceLoader } -constructor TTGameResourceLoader.Create(aResSprites: TKMResSprites; aAlphaShadows: Boolean; aRxType: TRXType); +constructor TKMGameResourceWorker.Create(aOwner: TKMResSprites; aSerial: QWord; aRT: TRXType; aAlphaShadows, aInputIsRXA: Boolean; aInput: TStream); begin - inherited Create(False); - - {$IFDEF DEBUG} - TThread.NameThreadForDebugging('GameResourceLoader', ThreadID); - {$ENDIF} - - fResSprites := aResSprites; - fAlphaShadows := aAlphaShadows; - RXType := aRxType; - FreeOnTerminate := False; //object can be automatically removed after its termination - LoadStage := lsLoad; - - Log('Started'); + inherited Create(True); + FreeOnTerminate := False; + fOwner := aOwner; fSerial := aSerial; fRT := aRT; fAlphaShadows := aAlphaShadows; + fInputIsRXA := aInputIsRXA; fInput := aInput; end; -destructor TTGameResourceLoader.Destroy; +destructor TKMGameResourceWorker.Destroy; begin - Log('Stopped'); - + fInput.Free; inherited; end; -procedure TTGameResourceLoader.Log(const aString: string); +procedure TKMGameResourceWorker.Complete(aOutcome: TKMGameResourceWorkerOutcome; const aClass, aMessage: string); begin - gLog.AddTime('[AsyncGameResLoader Thread] ' + aString); + fOwner.fWorkerCompletionCS.Acquire; + try + fOwner.fWorkerCompletionSerial := fSerial; + fOwner.fWorkerCompletionRXType := fRT; + fOwner.fWorkerCompletionOutcome := aOutcome; + fOwner.fWorkerCompletionErrorClass := aClass; + fOwner.fWorkerCompletionErrorText := aMessage; + fOwner.fWorkerCompletionPending := True; + finally fOwner.fWorkerCompletionCS.Release end; end; -procedure TTGameResourceLoader.Execute; +function TKMGameResourceWorker.IsTerminated: Boolean; begin - inherited; + Result := Terminated; +end; - while not Terminated do - begin - if not LoadStepDone then - begin - if SLOW_ASYNC_RES_LOADER then - Sleep(5000); - Log(Format('RT = %s Stage = %s', [GetEnumName(TypeInfo(TRXType), Integer(RXType)), - GetEnumName(TypeInfo(TKMAsyncLoadStage), Integer(LoadStage))])); - - case LoadStage of - lsLoad: begin - if fAlphaShadows and FileExists(fResSprites.GetSpritesRXAFilePath(RXType)) then - begin - Log('Start Load RXA ''' + RX_INFO[RXType].FileName + '.rxa'''); - fResSprites.LoadRXASprites(RXType); - if Terminated then Exit; - - AtomicExchange(Integer(LastLoadedRXA), Integer(True)); - Log('DONE Load RXA ''' + RX_INFO[RXType].FileName + '.rxa'''); - end - else - begin - Log('Start Load RXX ''' + RX_INFO[RXType].FileName + '.rxx'''); - fResSprites.LoadSprites(RXType, fAlphaShadows); - if Terminated then Exit; - - {$IFNDEF NO_OGL} - fResSprites.fSprites[RXType].MakeGFX(fAlphaShadows, 1, False, IsTerminated); - {$ENDIF} - - AtomicExchange(Integer(LastLoadedRXA), Integer(False)); - Log('DONE Load RXX ''' + RX_INFO[RXType].FileName + '.rxa'''); - end; - end; - lsGenMain: ; - lsOverload: begin - Log('OverloadFromFolder RT = ' + GetEnumName(TypeInfo(TRXType), Integer(RXType))); - fResSprites[RXType].OverloadGeneratedFromFolder(fAlphaShadows, ExeDir + 'Sprites' + PathDelim, True, IsTerminated); // Legacy support - // 'Sprites' folder name confused some of the players, cause there is already data/Sprites folder - fResSprites[RXType].OverloadGeneratedFromFolder(fAlphaShadows, ExeDir + 'Modding graphics' + PathDelim, True, IsTerminated); - Log('DONE OverloadFromFolder RT = ' + GetEnumName(TypeInfo(TRXType), Integer(RXType))); - end; - lsGenOverload: ; - end; - - LoadStage := TKMAsyncLoadStage((Ord(LoadStage) + 1) mod 4); - - // Make this atomic, since LoadStepDone is accessed in different threads - AtomicExchange(Integer(LoadStepDone), Integer(True)); +procedure TKMGameResourceWorker.Execute; +var + I: Integer; + idList: TList; +begin + try + try + if fInputIsRXA then + fOwner.fSprites[fRT].LoadFromRXAStream(fInput, False) + else + fOwner.fSprites[fRT].LoadFromRXXStream(fInput, 1, False); + finally + fInput.Free; + fInput := nil; end; - Sleep(1); // sleep for a bit + if Terminated then begin Complete(grwoCancelled, '', ''); Exit end; + if not fInputIsRXA then + begin + fOwner.fSprites[fRT].OverloadRXDataFromFolder(ExeDir + 'Sprites' + PathDelim, nil); + if Terminated then begin Complete(grwoCancelled, '', ''); Exit end; + fOwner.fSprites[fRT].OverloadRXDataFromFolder(ExeDir + 'Modding graphics' + PathDelim, nil); + if Terminated then begin Complete(grwoCancelled, '', ''); Exit end; + {$IFNDEF NO_OGL} + idList := TList.Create; + try + for I := 1 to fOwner.fSprites[fRT].fRXData.Count do + idList.Add(I); + fOwner.fSprites[fRT].MakeGFXInternal(fAlphaShadows, idList, False, IsTerminated, MAX_GAME_ATLAS_SIZE); + finally + idList.Free; + end; + {$ENDIF} + end; + if Terminated then Complete(grwoCancelled, '', '') else Complete(grwoCompleted, '', ''); + except + on E: Exception do Complete(grwoFailed, E.ClassName, E.Message); end; end; -function TTGameResourceLoader.IsTerminated: Boolean; -begin - Result := Terminated; -end; -{$ENDIF} - - end. diff --git a/port-source/kam_remake/src/res/KM_ResTypes.pas b/port-source/kam_remake/src/res/KM_ResTypes.pas new file mode 100644 index 0000000..340ddcd --- /dev/null +++ b/port-source/kam_remake/src/res/KM_ResTypes.pas @@ -0,0 +1,332 @@ +unit KM_ResTypes; +{$I KaM_Remake.inc} +interface +uses + KM_ResTilesetTypes; + +type + TKMChopableAge = (caAge1, caAge2, caAge3, caAgeFull, caAgeFall, caAgeStump); + TKMChopableAgeSet = set of TKMChopableAge; + + //* Ware type + TKMWareType = ( + wtNone, + wtTrunk, wtStone, wtTimber, wtIronOre, wtGoldOre, + wtCoal, wtIron, wtGold, wtWine, wtCorn, + wtBread, wtFlour, wtLeather, wtSausage, wtPig, + wtSkin, wtWoodenShield, wtIronShield, wtLeatherArmor, wtIronArmor, + wtAxe, wtSword, wtLance, wtPike, wtBow, + wtCrossbow, wtHorse, wtFish, + + // Special ware types + wtAll, wtWarfare, wtFood + ); + + //* Ware type set + TKMWareTypeSet = set of TKMWareType; + +const + WARE_MIN = wtTrunk; + WARE_MAX = wtFish; + WARE_MAX_ALL = wtFood; + WARFARE_MIN = wtWoodenShield; + WEAPON_MIN = wtWoodenShield; + WEAPON_MAX = wtCrossbow; + WARFARE_MAX = wtHorse; + + WARES_VALID = [WARE_MIN..WARE_MAX]; + WARES_WARFARE = [WARFARE_MIN..WARFARE_MAX]; + WARES_FOOD = [wtWine, wtBread, wtSausage, wtFish]; + + WARE_CNT = Integer(WARE_MAX) - Integer(WARE_MIN) + 1; + WARFARE_CNT = Integer(WARFARE_MAX) - Integer(WEAPON_MIN) + 1; + + WARFARE_IRON = [wtIronShield, wtIronArmor, wtSword, wtPike, wtCrossbow]; + + +type + //* House type + TKMHouseType = ( + htNone, htAny, + htArmorSmithy, htArmorWorkshop, htBakery, htBarracks, htButchers, + htCoalMine, htFarm, htFishermans, htGoldMine, htInn, + htIronMine, htIronSmithy, htMarket, htMetallurgists, htMill, + htQuarry, htSawmill, htSchool, htSiegeWorkshop, htStables, + htStore, htSwine, htTannery, htTownHall, htWatchTower, + htWeaponSmithy, htWeaponWorkshop, htVineyard, htWoodcutters + ); + + //* House type set + TKMHouseTypeSet = set of TKMHouseType; + TKMHouseTypeArray = array of TKMHouseType; + + + TKMKeyFunction = ( + kfNone, + + // faCommon + kfScrollLeft, // Scroll Left + kfScrollRight, // Scroll Right + kfScrollUp, // Scroll Up + kfScrollDown, // Scroll Down + + kfMapDragScroll, // Map drag scroll + + kfZoomIn, // Zoom In + kfZoomOut, // Zoom Out + kfZoomReset, // Reset Zoom + + kfCloseMenu, // Close opened menu + + kfMusicPrevTrack, // Music previous track + kfMusicNextTrack, // Music next track + kfMusicDisable, // Music disable + kfMusicShuffle, // Music shuffle + kfMusicVolumeUp, // Music volume up + kfMusicVolumeDown, // Music volume down + kfMusicMute, // Music mute + + kfSoundVolumeUp, // Sound volume up + kfSoundVolumeDown, // Sound volume down + kfSoundMute, // Sound mute + + kfMuteAll, // Mute music and sound + + kfDebugWindow, // Debug window + kfDebugRevealmap, // Debug Menu Reveal Map + kfDebugVictory, // Debug Menu Victory + kfDebugDefeat, // Debug Menu Defeat + kfDebugAddscout, // Debug Menu Add Scout + + // faGame + kfMenuBuild, // Build Menu + kfMenuRatio, // Ratio Menu + kfMenuStats, // Stats Menu + kfMenuMenu, // Main Menu + + kfSpeedup1, // Speed up 1 + kfSpeedup2, // Speed up 2 + kfSpeedup3, // Speed up 3 + kfSpeedup4, // Speed up 4 + + kfBeacon, // Beacon + kfPause, // Pause + kfShowTeams, // Show teams in MP + + kfCenterAlert, // Center to alert + kfDeleteMsg, // Delete message + kfChat, // Show the chat + + kfNextEntitySameType, // Select next building/unit with same type + kfPlayerColorMode, // Switch player color mode + + kfPlanRoad, // Plan road + kfPlanField, // Plan field + kfPlanWine, // Plan wine + kfErasePlan, // Erase plan + + kfSelect1, // Select 1 + kfSelect2, // Select 2 + kfSelect3, // Select 3 + kfSelect4, // Select 4 + kfSelect5, // Select 5 + kfSelect6, // Select 6 + kfSelect7, // Select 7 + kfSelect8, // Select 8 + kfSelect9, // Select 9 + kfSelect10, // Select 10 + kfSelect11, // Select 11 + kfSelect12, // Select 12 + kfSelect13, // Select 13 + kfSelect14, // Select 14 + kfSelect15, // Select 15 + kfSelect16, // Select 16 + kfSelect17, // Select 17 + kfSelect18, // Select 18 + kfSelect19, // Select 19 + kfSelect20, // Select 20 + + // faUnit + kfArmyHalt, // Halt Command + kfArmySplit, // Split up Command + kfArmyLink, // Linkup Command + kfArmyFood, // Food Command + kfArmyStorm, // Storm Command + kfArmyAddLine, // Formation Increase Line Command + kfArmyDelLine, // Formation Shrink Line Command + kfArmyRotateCw, // Turn Right Command + kfArmyRotateCcw, // Turn Left Command + + // faHouse + kfTrainGotoPrev, // Goto previuos unit + kfTrainEquipUnit, // Train or Equip unit + kfTrainGotoNext, // Goto next unit + + // faSpecReplay + kfSpecpanelSelectDropbox, // Select dropbox on spectator panel + kfReplayPlayNextTick, // Play next tick in replay + kfSpectatePlayer1, // Spectate player 1 + kfSpectatePlayer2, // Spectate player 2 + kfSpectatePlayer3, // Spectate player 3 + kfSpectatePlayer4, // Spectate player 4 + kfSpectatePlayer5, // Spectate player 5 + kfSpectatePlayer6, // Spectate player 6 + kfSpectatePlayer7, // Spectate player 7 + kfSpectatePlayer8, // Spectate player 8 + kfSpectatePlayer9, // Spectate player 9 + kfSpectatePlayer10, // Spectate player 10 + kfSpectatePlayer11, // Spectate player 11 + kfSpectatePlayer12, // Spectate player 12 + + // faMapEdit + kfMapedExtra, // Maped Extra's menu + kfMapedSaveMap, // Maped Save map + kfMapedTerrain, // Maped Terrain Editing + kfMapedVillage, // Maped Village Planning + kfMapedVisual, // Maped Visual Scripts + kfMapedGlobal, // Maped Global Scripting + kfMapedMainMenu, // Maped Main Menu + kfMapedSubMenu1, // Maped Sub-menu 1 + kfMapedSubMenu2, // Maped Sub-menu 2 + kfMapedSubMenu3, // Maped Sub-menu 3 + kfMapedSubMenu4, // Maped Sub-menu 4 + kfMapedSubMenu5, // Maped Sub-menu 5 + kfMapedSubMenu6, // Maped Sub-menu 6 + kfMapedSubMenuAction1, // Maped Sub-menu Action 1 + kfMapedSubMenuAction2, // Maped Sub-menu Action 2 + kfMapedSubMenuAction3, // Maped Sub-menu Action 3 + kfMapedSubMenuAction4, // Maped Sub-menu Action 4 + kfMapedSubMenuAction5, // Maped Sub-menu Action 5 + kfMapedSubMenuAction6, // Maped Sub-menu Action 6 + kfMapedSubMenuAction7, // Maped Sub-menu Action 6 + kfMapedObjPalette, // Maped Objects palette + kfMapedTilesPalette, // Maped Tiles palette + kfMapedUnivErasor, // Maped Universal erasor + kfMapedPaintBucket, // Maped Paint bucket + kfMapedHistory, // Maped History + kfMapedFlatTerrain, // Maped Flat terrain + kfMapedTilesGrid // Maped Tiles grid + ); + + TKMKeyFunctionSet = set of TKMKeyFunction; + + TKMKeyFuncArea = (faCommon, + faGame, + faUnit, + faHouse, + faSpecReplay, + faMapEdit); + + TKMKeyFuncAreaSet = set of TKMKeyFuncArea; + +const + KEY_FUNC_LOW = Succ(kfNone); // 1st key function + + KEY_FUNCS_ALL = [KEY_FUNC_LOW..High(TKMKeyFunction)]; + +type + // Cursors + TKMCursorImageType = ( + kmcDefault, kmcInfo, kmcAttack, kmcJoinYes, kmcJoinNo, kmcEdit, kmcDragUp, + kmcDir0, kmcDir1, kmcDir2, kmcDir3, kmcDir4, kmcDir5, kmcDir6, kmcDir7, kmcDirNA, + kmcScroll0, kmcScroll1, kmcScroll2, kmcScroll3, kmcScroll4, kmcScroll5, kmcScroll6, kmcScroll7, + kmcBeacon, kmcDrag, + kmcInvisible, // for some reason kmcInvisible should be at its current position in enum. Otherwise 1px dot will appear while TroopSelection is on + kmcPaintBucket, + kmcAnimatedDirSelector + ); + +const + // Indexes of cursor images in GUI.RX + CURSOR_SPRITE_INDEX: array [TKMCursorImageType] of Word = ( + 1, 452, 457, 460, 450, 453, 449, + 511, 512, 513, 514, 515, 516, 517, 518, 519, + 4, 7, 3, 9, 5, 8, 2, 6, + 456, 451, 999, 661, 0); + +type + TRXUsage = (ruMenu, ruGame, ruCustom); //Where sprites are used + + TKMSpriteAtlasType = (saBase, saMask); + + TRXInfo = record + FileName: string; //Used for logging and filenames + TeamColors: Boolean; //sprites should be generated with color masks + Usage: TRXUsage; //Menu and Game sprites are loaded separately + LoadingTextID: Word; + end; + + TKMGenTerrainInfo = record + TerKind: TKMTerrainKind; + Mask: TKMMaskFullType; + end; + +// ResSprites Types +type + TRXType = ( + rxTrees, + rxHouses, + rxUnits, + rxGui, + rxGuiMain, + rxCustom, //Used for loading stuff like campaign maps (there is no main RXX file) + rxTiles //Tiles + ); + //Colors to paint beneath player color areas (flags) + //The blacker/whiter - the more contrast player color will be + FLAG_COLOR_DARK = $FF101010; //Dark-grey (Black) + FLAG_COLOR_LITE = $FFFFFFFF; //White + +type + // Original KaM format for RX files + { + TRXData = record + Count: Integer; + Flag: array of Byte; // Sprite is valid + Size: array of record X,Y: Word; end; + Pivot: array of record X,Y: Integer; end; + Data: array of array of Byte; + end;} + + // RXX and RXX1 file format, with Pivot type changed and SizeNoShadow added + TRXData = record + Count: Integer; + Flag: array of Byte; //Sprite is valid + Size: array of record X,Y: Word; end; + Pivot: array of record X,Y: SmallInt; end; + SizeNoShadow: array of record Left, Top, Right, Bottom: SmallInt; end; //Image object (without shadow) rect in the image sizes + {unused in RXX} Data: array of array of Byte; //Used for RXX utils (Packer / Editor) + RGBA: array {Index} of array {YX} of Cardinal; //Expanded image + Mask: array of array of Byte; //Mask for team colors + HasMask: array of Boolean; //Flag if Mask for team colors is used + end; + PRXData = ^TRXData; + +const + {$I KM_TextIDs.inc} + + RX_INFO: array [TRXType] of TRXInfo = ( + (FileName: 'Trees'; TeamColors: False; Usage: ruGame; LoadingTextID: TX_MENU_LOADING_TREES;), + (FileName: 'Houses'; TeamColors: True; Usage: ruGame; LoadingTextID: TX_MENU_LOADING_HOUSES;), + (FileName: 'Units'; TeamColors: True; Usage: ruGame; LoadingTextID: TX_MENU_LOADING_UNITS;), + (FileName: 'GUI'; TeamColors: True; Usage: ruMenu; LoadingTextID: 0;), + (FileName: 'GUIMain'; TeamColors: False; Usage: ruMenu; LoadingTextID: 0;), + (FileName: 'Custom'; TeamColors: False; Usage: ruCustom; LoadingTextID: 0;), + (FileName: 'Tileset'; TeamColors: False; Usage: ruMenu; LoadingTextID: TX_MENU_LOADING_TILESET;)); + + function GetKeyFunctionStr(aKeyFun: TKMKeyFunction): string; + + +implementation +uses + TypInfo; + + +function GetKeyFunctionStr(aKeyFun: TKMKeyFunction): string; +begin +// Result := TRttiEnumerationType.GetName(aKeyFun); + Result := GetEnumName(TypeInfo(TKMKeyFunction), Integer(aKeyFun)); +end; + + +end. diff --git a/port-source/kam_remake/src/res/KM_Resource.pas b/port-source/kam_remake/src/res/KM_Resource.pas new file mode 100644 index 0000000..344f4fd --- /dev/null +++ b/port-source/kam_remake/src/res/KM_Resource.pas @@ -0,0 +1,335 @@ +unit KM_Resource; +{$I KaM_Remake.inc} +interface +uses + {$IFDEF Unix} LCLIntf, LCLType, {$ENDIF} + Classes, SysUtils, + KM_CommonTypes, KM_Defaults, + KM_ResTypes, + KM_ResCursors, + KM_ResFonts, + KM_ResHouses, + KM_ResLocales, + KM_ResMapElements, + KM_ResPalettes, + KM_ResSound, + KM_ResSprites, + KM_ResTileset, + KM_ResUnits, + KM_ResWares, + KM_ResInterpolation; + + +type + TResourceLoadState = (rlsNone, rlsMenu, rlsAll); //Resources are loaded in 2 steps, for menu and the rest + + TKMResource = class + private + fDataState: TResourceLoadState; + + fCursors: TKMResCursors; + fFonts: TKMResFonts; + fHouses: TKMResHouses; + fPalettes: TKMResPalettes; + fUnits: TKMResUnits; + fWares: TKMResWares; + fSounds: TKMResSounds; + fSprites: TKMResSprites; + fTileset: TKMResTileset; + fMapElements: TKMResMapElements; + fInterpolation: TKMResInterpolation; + + function GetGameResourcesReady: Boolean; + procedure EnsureGameResourceInterpolation; + procedure StepRefresh; + procedure StepCaption(const aCaption: UnicodeString); + public + OnLoadingStep: TKMEvent; + OnLoadingText: TUnicodeStringEvent; + + constructor Create(aOnLoadingStep: TKMEvent; aOnLoadingText: TUnicodeStringEvent); + destructor Destroy; override; + + function GetDATCRC: Cardinal; + + procedure LoadMainResources(const aLocale: AnsiString = ''; aLoadFullFonts: Boolean = True); + procedure LoadLocaleAndFonts(const aLocale: AnsiString = ''; aLoadFullFonts: Boolean = True); + procedure LoadLocaleResources(const aLocale: AnsiString = ''); + procedure PreloadGameResources(aAlphaShadows: Boolean); + procedure LoadGameResources(aAlphaShadows: Boolean; aForceReload: Boolean = False); + procedure StartGameSpriteRecovery; + procedure LoadLocaleFonts(const aLocale: AnsiString; aLoadFullFonts: Boolean); + + property DataState: TResourceLoadState read fDataState; + property GameResourcesReady: Boolean read GetGameResourcesReady; + property Palettes: TKMResPalettes read fPalettes; + property Cursors: TKMResCursors read fCursors; + property MapElements: TKMResMapElements read fMapElements; + property Fonts: TKMResFonts read fFonts; + property Sounds: TKMResSounds read fSounds; + property Sprites: TKMResSprites read fSprites; + property Tileset: TKMResTileset read fTileset; + property Houses: TKMResHouses read fHouses; + property Units: TKMResUnits read fUnits; + property Wares: TKMResWares read fWares; + property Interpolation: TKMResInterpolation read fInterpolation; + + procedure UpdateStateIdle; + + function IsMsgHouseUnnocupied(aMsgId: Word): Boolean; + end; + + +var + gRes: TKMResource; + + +implementation +uses + TypInfo, + {$IFNDEF NO_OGL} + KM_System, + {$ENDIF} + KromUtils, KM_Log, KM_Points, + KM_ResTexts, KM_ResKeyFuncs, KM_ResTilesetTypes; + + +{ TKMResource } +constructor TKMResource.Create(aOnLoadingStep: TKMEvent; aOnLoadingText: TUnicodeStringEvent); +begin + inherited Create; + + fDataState := rlsNone; + gLog.AddTime('Resource loading state - None'); + + OnLoadingStep := aOnLoadingStep; + OnLoadingText := aOnLoadingText; +end; + + +destructor TKMResource.Destroy; +begin + FreeAndNil(fCursors); + FreeAndNil(fHouses); + FreeAndNil(gResLocales); + FreeAndNil(fMapElements); + FreeAndNil(fPalettes); + FreeAndNil(fFonts); + FreeAndNil(fWares); + FreeAndNil(fSprites); + FreeAndNil(fSounds); + FreeAndNil(gResTexts); + FreeAndNil(fTileset); + FreeAndNil(fUnits); + FreeAndNil(gResKeyFuncs); + FreeAndNil(fInterpolation); + + inherited; +end; + + +procedure TKMResource.UpdateStateIdle; +begin + try + fSprites.UpdateStateIdle; + except + if fDataState = rlsAll then + fDataState := rlsMenu; + raise; + end; + + if (fDataState <> rlsNone) and (fInterpolation <> nil) and fSprites.GameResourcesReady then + fDataState := rlsAll + else if fDataState = rlsAll then + fDataState := rlsMenu; +end; + + +function TKMResource.GetGameResourcesReady: Boolean; +begin + Result := fDataState = rlsAll; +end; + + +procedure TKMResource.EnsureGameResourceInterpolation; +begin + if fInterpolation = nil then + begin + gLog.AddTime('LoadGameResources ... Interpolations from interp.dat'); + fInterpolation := TKMResInterpolation.Create; + fInterpolation.LoadFromFile(ExeDir + 'data' + PathDelim + 'defines' + PathDelim + 'interp.dat'); + end; +end; + + +procedure TKMResource.StepRefresh; +begin + if Assigned(OnLoadingStep) then OnLoadingStep; +end; + + +procedure TKMResource.StepCaption(const aCaption: UnicodeString); +begin + if Assigned(OnLoadingText) then OnLoadingText(aCaption); +end; + + +//CRC of data files that can cause inconsitencies +function TKMResource.GetDATCRC: Cardinal; +begin + Result := gRes.Houses.CRC xor + fUnits.CRC xor + fMapElements.CRC xor + fTileset.CRC; +end; + + +procedure TKMResource.LoadMainResources(const aLocale: AnsiString = ''; aLoadFullFonts: Boolean = True); +var + tileColors: TKMColor3bArray; +begin + StepCaption('Reading palettes ...'); + fPalettes := TKMResPalettes.Create; + //We are using only default palette in the game for now, so no need to load all palettes + fPalettes.LoadDefaultPalette(ExeDir + 'data' + PathDelim + 'gfx' + PathDelim); + gLog.AddTime('Reading palettes done'); + + fSprites := TKMResSprites.Create(StepRefresh, StepCaption); + + fCursors := TKMResCursors.Create; + + fUnits := TKMResUnits.Create; // Load units prior to Sprites, as we could use it on SoftenShadows override for png in Sprites folder + fSprites.LoadMenuResources; + + {$IFNDEF NO_OGL} + gSystem.MakeCursors(fSprites[rxGui]); + gSystem.Cursor := kmcDefault; + {$ENDIF} + fCursors.SetRXDataPointer(@fSprites[rxGui].RXData); + + gResKeyFuncs := TKMResKeyFuncs.Create; + + LoadLocaleAndFonts(aLocale, aLoadFullFonts); + + fTileset := TKMResTileset.Create; + if not SKIP_RENDER then + begin + if fSprites.Sprites[rxTiles].RXData.Count <> TILES_CNT then + gLog.AddTime('fSprites.Sprites[rxTiles].RXData.Count = ' + IntToStr(fSprites.Sprites[rxTiles].RXData.Count)); + + tileColors := fSprites.Sprites[rxTiles].GetAverageSpriteColors(TILES_CNT); + fTileset.SetTileColors(tileColors); + end; + + fMapElements := TKMResMapElements.Create; + fMapElements.LoadFromFile(ExeDir + 'data' + PathDelim + 'defines' + PathDelim + 'mapelem.dat'); + + fSprites.ClearTemp; + + fWares := TKMResWares.Create; + fHouses := TKMResHouses.Create; + + StepRefresh; + gLog.AddTime('ReadGFX is done'); + fDataState := rlsMenu; + gLog.AddTime('Resource loading state - Menu'); +end; + + +procedure TKMResource.LoadLocaleResources(const aLocale: AnsiString = ''); +begin + FreeAndNil(gResLocales); + FreeAndNil(gResTexts); + FreeAndNil(fSounds); + + gResLocales := TKMResLocales.Create(ExeDir + 'data' + PathDelim + 'locales.txt', aLocale); + + gResTexts := TKMTextLibraryMulti.Create; + gResTexts.LoadLocale(ExeDir + 'data' + PathDelim + 'text' + PathDelim + 'text.%s.libx', False); + + fSounds := TKMResSounds.Create(gResLocales.UserLocale, gResLocales.FallbackLocale, gResLocales.DefaultLocale); +end; + + +procedure TKMResource.LoadLocaleAndFonts(const aLocale: AnsiString = ''; aLoadFullFonts: Boolean = True); +begin + // Locale info is needed for DAT export and font loading + LoadLocaleResources(aLocale); + + StepCaption('Reading fonts ...'); + fFonts := TKMResFonts.Create; + if aLoadFullFonts or gResLocales.LocaleByCode(aLocale).NeedsFullFonts then + fFonts.LoadFonts(fllFull) + else + fFonts.LoadFonts(fllMinimal); + gLog.AddTime('Read fonts is done'); +end; + + +procedure TKMResource.LoadLocaleFonts(const aLocale: AnsiString; aLoadFullFonts: Boolean); +begin + if (Fonts.LoadLevel <> fllFull) + and (aLoadFullFonts or gResLocales.LocaleByCode(aLocale).NeedsFullFonts) then + Fonts.LoadFonts(fllFull); +end; + + +procedure TKMResource.PreloadGameResources(aAlphaShadows: Boolean); +begin + try + EnsureGameResourceInterpolation; + fSprites.PreloadGameResources(aAlphaShadows); + if fSprites.GameResourcesReady then + fDataState := rlsAll + else + fDataState := rlsMenu; + except + fDataState := rlsMenu; + raise; + end; +end; + + +procedure TKMResource.LoadGameResources(aAlphaShadows: Boolean; aForceReload: Boolean = False); +begin + try + EnsureGameResourceInterpolation; + + gLog.AddTime('LoadGameResources ... AlphaShadows: ' + BoolToStr(aAlphaShadows, True) + '. Forced: ' + BoolToStr(aForceReload, True)); + fSprites.LoadGameResources(aAlphaShadows, aForceReload); + if not fSprites.GameResourcesReady then + raise Exception.Create('Game resources failed to become ready'); + + fDataState := rlsAll; + except + fDataState := rlsMenu; + raise; + end; + + gLog.AddTime('Resource loading state - Game'); +end; + + +procedure TKMResource.StartGameSpriteRecovery; +begin + if fDataState <> rlsNone then + fDataState := rlsMenu; + try + fSprites.StartGameSpriteRecovery; + if fSprites.GameResourcesReady then + fDataState := rlsAll; + except + if fDataState <> rlsNone then + fDataState := rlsMenu; + raise; + end; +end; + + +function TKMResource.IsMsgHouseUnnocupied(aMsgId: Word): Boolean; +begin + Result := (aMsgId >= TX_MSG_HOUSE_UNOCCUPIED__22) and (aMsgId <= TX_MSG_HOUSE_UNOCCUPIED__22 + 22); +end; + + +end. diff --git a/port-source/kam_remake/src/settings/KM_GameSettings.pas b/port-source/kam_remake/src/settings/KM_GameSettings.pas index 44cb9a9..097a4c8 100644 --- a/port-source/kam_remake/src/settings/KM_GameSettings.pas +++ b/port-source/kam_remake/src/settings/KM_GameSettings.pas @@ -111,9 +111,6 @@ type fLastPassword: string; fFlashOnMessage: Boolean; - //Misc - fAsyncGameResLoader: Boolean; - //Menu fMenu_FavouriteMapsStr: UnicodeString; fMenu_MapSPType: Byte; @@ -161,7 +158,6 @@ type procedure SetSpeedPace(const aValue: Word); function GetFavouriteMaps: TKMMapsCRCList; - function GetAsyncGameResLoader: Boolean; public GFX: TKMSettingsGFX; SFX: TKMSettingsSFX; @@ -229,9 +225,6 @@ type property LastRoom: string read fLastRoom write fLastRoom; property LastPassword: string read fLastPassword write fLastPassword; - //Misc - property AsyncGameResLoader: Boolean read GetAsyncGameResLoader; - // Menu property MenuMapSPType: Byte read fMenu_MapSPType write fMenu_MapSPType; property MenuReplaysType: Byte read fMenu_ReplaysType write fMenu_ReplaysType; @@ -265,6 +258,7 @@ var implementation uses + DOM, SysUtils, INIfiles, Math, KM_TerrainTypes; @@ -323,14 +317,6 @@ begin end; -function TKMGameSettings.GetAsyncGameResLoader: Boolean; -begin - if Self = nil then Exit(False); - - Result := fAsyncGameResLoader; -end; - - function TKMGameSettings.GetFavouriteMaps: TKMMapsCRCList; begin if Self = nil then Exit(nil); @@ -391,7 +377,6 @@ var nMenuSP, nMenuReplay, nMenuMapEd, - nMisc, nDebug: TKMXmlNode; tempCard: Int64; begin @@ -517,10 +502,6 @@ begin fLastPassword := nMultiplayer.Attributes['LastPassword'].AsString(''); fFlashOnMessage := nMultiplayer.Attributes['FlashOnMessage'].AsBoolean(True); - // Misc - nMisc := nGameSettings.AddOrFindChild('Misc'); - fAsyncGameResLoader := nMisc.Attributes['AsyncGameResLoader'].AsBoolean(True); - // Menu nMenu := nGameSettings.AddOrFindChild('Menu'); fMenu_FavouriteMapsStr := nMenu.Attributes['FavouriteMaps'].AsString(''); @@ -590,8 +571,8 @@ var nMenuSP, nMenuReplay, nMenuMapEd, - nMisc, nDebug: TKMXmlNode; + nMisc: TDOMElement; begin if Self = nil then Exit; if BLOCK_FILE_WRITE then Exit; @@ -708,9 +689,9 @@ begin nMultiplayer.Attributes['LastPassword'] := fLastPassword; nMultiplayer.Attributes['FlashOnMessage'] := fFlashOnMessage; - // Misc - nMisc := nGameSettings.AddOrFindChild('Misc'); - nMisc.Attributes['AsyncGameResLoader'] := fAsyncGameResLoader; + nMisc := TDOMElement(TDOMNode(nGameSettings).FindNode('Misc')); + if nMisc <> nil then + nMisc.RemoveAttribute('AsyncGameResLoader'); // Menu nMenu := nGameSettings.AddOrFindChild('Menu');