unit KM_ResSprites; {$I KaM_Remake.inc} interface uses 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, KM_BinPacking {$IFDEF FPC}, zstream {$ENDIF} {$IFDEF WDC}, ZLib {$ENDIF}; type // How do we want to soften the sprite TKMSpriteSoftening = (ssNone, ssOnlyShadow, ssWhole); TKMRXXFormat = ( rxxUnknown, // Unknown header, probably not an RXX file at all rxxZero, // Legacy KMR format rxxOne, // Same as previous, but with an explicit header and SizeNoShadow rxxTwo // Same as previous, but with dynamic length text in header ); const RXX_HEADER_LENGTH = 4; RXX_HEADER: array [TKMRXXFormat] of AnsiString = ('', '', 'RXX1', 'RXX2'); type TKMGameResourceRequestState = (grsIdle, grsAcquire, grsCPURunning, grsReadyForGPU, grsCancelling, grsReady, grsFailed); TKMGameResourceWorkerOutcome = (grwoCompleted, grwoCancelled, grwoFailed); // Atlas data, needed for Texture Atlas Generation TKMSpriteAtlasData = record Container: TKMBinItem; TexType: TKMTexFormat; Data: TKMCardinalArray; end; TKMSpriteAtlases = array [TKMSpriteAtlasType] of array {atlas number} of TKMSpriteAtlasData; // Base class for Sprite loading TKMSpritePack = class private fTemp: Boolean; fPad: Byte; // Padding between sprites to avoid neighbour edge visibility procedure SetGFXData(aTexID: Cardinal; aSpriteInfo: TKMBinItem; aAtlasType: TKMSpriteAtlasType); procedure PrepareAtlases(aSpriteInfo: TBinArray; aMode: TKMSpriteAtlasType; aTexType: TKMTexFormat; aFillGFXData: Boolean = True; aOnCheckTerminated: TBooleanFuncSimple = nil); protected fRT: TRXType; fRXData: TRXData; 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; aFillGFXData: Boolean = True; aOnCheckTerminated: TBooleanFuncSimple = nil; aMaxAtlasSize: Integer = -1); overload; procedure MakeGFX_BinPacking(aTexType: TKMTexFormat; aStartingIndex: Integer; 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); procedure AddImage(const aFolder, aFilename: string; aIndex: Integer; aEnlargeOnly: Boolean = False); property RT: TRXType read fRT; property RXData: TRXData read fRXData; property Padding: Byte read fPad write fPad; property Atlases: TKMSpriteAtlases read fAtlases; {$IFNDEF NO_OGL} procedure MakeGFX(aAlphaShadows: Boolean; aStartingIndex: Integer = 1; aFillGFXData: Boolean = True; aOnCheckTerminated: TBooleanFuncSimple = nil); overload; procedure MakeGFX(aAlphaShadows: Boolean; aIDList: TList; aFillGFXData: Boolean = True; aOnCheckTerminated: TBooleanFuncSimple = nil); overload; {$ENDIF} procedure DeleteSpriteTexture(aIndex: Integer); //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; procedure SoftenShadowsList(aIdList: TList); procedure SoftenShadowsRange(aFrom, aTo: Integer; aOnlyShadows: Boolean = True); procedure DetermineImagesObjectSizeAll; procedure DetermineImagesObjectSizeList(aIdList: TList); procedure RemoveMarketWaresShadows(aResHouses: TKMResHouses); procedure RemoveSnowHouseShadows(aResHouses: TKMResHouses); procedure RemoveHouseWorkBackgrounds(aResHouses: TKMResHouses); function GetAverageSpriteColors(aCount: Integer): TKMColor3bArray; function IsEmpty: Boolean; procedure ClearGameResGenTemp; procedure GenerateTexturesFromLoadedRXZ; procedure ClearTemp; virtual;//Release non-required data end; TKMResSprites = class private fAlphaShadows: Boolean; //Remember which state we loaded fSprites: array [TRXType] of TKMSpritePack; fStepProgress: TKMEvent; fStepCaption: TUnicodeStringEvent; fGenTexIdStartI: Integer; fGenTexIdStartILegacy: Integer; fGenTerrainToTerKind: array of TKMGenTerrainInfo; 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; fGenTerrainTransitions: array[Succ(tkCustom)..High(TKMTerrainKind)] of array[Succ(mkNone)..High(TKMTileMaskKind)] of array[Succ(tmtNone)..High(TKMTileMaskType)] of array[TKMTileMaskSubType] //mask components (subtypes) // of array[0..3] //Terrain Rotation of Word; fGenTerrainTransitionsLegacy: array[Succ(tkCustom)..High(TKMTerrainKind)] of array[Succ(mkNone)..High(TKMTileMaskKind)] of array[Succ(tmtNone)..High(TKMTileMaskType)] of array[TKMTileMaskSubType] //mask components (subtypes) // of array[0..3] //Terrain Rotation of Word; function GetGenTerrainTransitions(aTerKind: TKMTerrainKind; aMaskKind: TKMTileMaskKind; aMaskType: TKMTileMaskType; aMaskSubtype: TKMTileMaskSubType): Word; function GetRXFileName(aRX: TRXType): string; function GetSprites(aRT: TRXType): TKMSpritePack; function GetSpritesRXAFilePath(aRT: TRXType): string; 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; procedure GenerateTerrainTransitions(aSprites: TKMSpritePack; aLegacyGeneration: Boolean = False); function GetGenTerrainInfo(aTerrain: Integer): TKMGenTerrainInfo; function GetGenTerrainInfoLegacy(aTerrain: Integer): TKMGenTerrainInfo; property Sprites[aRT: TRXType]: TKMSpritePack read GetSprites; default; property GenTerrainTransitions[aTerKind: TKMTerrainKind; aMaskKind: TKMTileMaskKind; aMaskType: TKMTileMaskType; aMaskSubtype: TKMTileMaskSubType]: Word read GetGenTerrainTransitions; property GameResourcesReady: Boolean read GetGameResourcesReady; //Used externally to access raw RGBA data (e.g. by ExportAnim) function LoadSprites(aRT: TRXType; aAlphaShadows: Boolean): Boolean; function LoadRXASprites(aRT: TRXType): Boolean; function LoadRXASpritesAndGenTextures(aRT: TRXType): Boolean; property AlphaShadows: Boolean read fAlphaShadows; property FileName[aRX: TRXType]: string read GetRXFileName; procedure UpdateStateIdle; end; TKMGameResourceWorker = class(TThread) private fOwner: TKMResSprites; fSerial: QWord; fRT: TRXType; fAlphaShadows: Boolean; fInput: TStream; fInputIsRXA: Boolean; function IsTerminated: Boolean; procedure Complete(aOutcome: TKMGameResourceWorkerOutcome; const aClass, aMessage: string); public constructor Create(aOwner: TKMResSprites; aSerial: QWord; aRT: TRXType; aAlphaShadows, aInputIsRXA: Boolean; aInput: TStream); destructor Destroy; override; procedure Execute; override; end; TKMTexCoords = record TexID: Cardinal; u1,v1,u2,v2: Single; //Top-Left, Bottom-Right uv coords end; var gGFXData: array [TRXType] of array of record Tex, Alt: TKMTexCoords; //AltID used for team colors and house building steps PxWidth, PxHeight: Word; end; implementation uses Types, {$IFDEF WDC} IOUtils, StrUtils, {$ENDIF} KromUtils, KM_SoftShadows, KM_Resource, KM_ResUnits, KM_Render, TypInfo, KM_Log, KM_CommonClasses, KaMResourceInput, KaMGeneratedTerrainBridge, KM_CommonUtils, KM_Utils, KM_Points, KM_GameSettings; const MAX_GAME_ATLAS_SIZE = 2048; //Max atlas size for KaM. No need for bigger atlases OVERLOAD_SKIP_MASK = 'skip'; var AllTilesInOneTexture: Boolean = False; function GetMaxAtlasSize: Integer; begin Result := MAX_GAME_ATLAS_SIZE; Result := Min(Result, TKMRender.MaxTextureSize); end; { TKMSpritePack } constructor TKMSpritePack.Create(aRT: TRXType; aTemp: Boolean = False); begin inherited Create; fRT := aRT; fTemp := aTemp; //Terrain tiles need padding to avoid edge bleeding if fRT = rxTiles then fPad := 1; end; //This is a crude solution to allow Campaigns to delete sprites they add procedure TKMSpritePack.DeleteSpriteTexture(aIndex: Integer); begin if gGFXData[fRT, aIndex].Tex.TexID <> 0 then TKMRender.DeleteTexture(gGFXData[fRT, aIndex].Tex.TexID); if gGFXData[fRT, aIndex].Alt.TexID <> 0 then TKMRender.DeleteTexture(gGFXData[fRT, aIndex].Alt.TexID); gGFXData[fRT, aIndex].Tex.TexID := 0; gGFXData[fRT, aIndex].Alt.TexID := 0; end; function TKMSpritePack.GetSoftenShadowType(aID: Integer): TKMSpriteSoftening; var step, spriteID: Integer; UT: TKMUnitType; dir: TKMDirection; begin Result := ssNone; case fRT of rxHouses: // Smooth smoke and flame if InRange(aID, 889, 892) or InRange(aID, 1615, 1638) then Result := ssWhole else Result := ssOnlyShadow; rxUnits: begin // Smooth thought bubbles if InRange(aID, 6251, 6322) then Exit(ssWhole); // Smooth all death animations for all units for UT := HUMANS_MIN to HUMANS_MAX do for dir := dirN to dirNW do for step := 1 to 30 do begin spriteID := gRes.Units[UT].UnitAnim[uaDie, dir].Step[step] + 1; // Sprites in units.dat are 0 indexed if (aID = spriteID) and (spriteID > 0) then Exit(ssWhole); end; if Result = ssNone then Result := ssOnlyShadow; end; rxTrees: Result := ssOnlyShadow; rxGui: if InRange(aID, 105, 128) // Field plans or InRange(aID, 249, 281) // House tablets only (shadow softening messes up other rxGui sprites) or InRange(aID, 461, 468) // Field fences or InRange(aID, 660, 660) then // Woodcutter cutting point sign Result := ssOnlyShadow; end; end; procedure TKMSpritePack.SoftenShadowsList(aIdList: TList); var I, id: Integer; shadowConverter: TKMSoftShadowConverter; spriteSoftening: TKMSpriteSoftening; begin if aIdList.Count = 0 then Exit; shadowConverter := TKMSoftShadowConverter.Create(@fRXData); try for I := 0 to aIdList.Count - 1 do begin id := aIdList[I]; if fRXData.Flag[id] <> 0 then begin spriteSoftening := GetSoftenShadowType(id); case spriteSoftening of ssNone: ; ssOnlyShadow: shadowConverter.ConvertShadows(id, True); ssWhole: begin shadowConverter.ConvertShadows(id, False); shadowConverter.ConvertShadows(id, True); end; end; end; end; finally shadowConverter.Free; end; end; // Make old style KaM checkerboard shadows smooth and transparent procedure TKMSpritePack.SoftenShadowsRange(aFrom, aTo: Integer; aOnlyShadows: Boolean = True); var I: Integer; shadowConverter: TKMSoftShadowConverter; begin shadowConverter := TKMSoftShadowConverter.Create(@fRXData); try for I := aFrom to aTo do if fRXData.Flag[I] <> 0 then shadowConverter.ConvertShadows(I, aOnlyShadows); finally shadowConverter.Free; end; end; procedure TKMSpritePack.DetermineImagesObjectSizeAll; var I: Integer; shadowConverter: TKMSoftShadowConverter; begin shadowConverter := TKMSoftShadowConverter.Create(@fRXData); try for I := 1 to fRXData.Count do if fRXData.Flag[I] <> 0 then shadowConverter.DetermineImageObjectSize(I); finally shadowConverter.Free; end; end; procedure TKMSpritePack.DetermineImagesObjectSizeList(aIdList: TList); var I, id: Integer; shadowConverter: TKMSoftShadowConverter; begin if aIdList.Count = 0 then Exit; shadowConverter := TKMSoftShadowConverter.Create(@fRXData); try for I := 0 to aIdList.Count - 1 do begin id := aIdList[I]; if fRXData.Flag[id] <> 0 then shadowConverter.DetermineImageObjectSize(id); end; finally shadowConverter.Free; end; end; procedure TKMSpritePack.RemoveSnowHouseShadows(aResHouses: TKMResHouses); var snowID: Integer; shadowConverter: TKMSoftShadowConverter; HT: TKMHouseType; begin Assert(fRT = rxHouses); shadowConverter := TKMSoftShadowConverter.Create(@fRXData); try for HT := HOUSE_MIN to HOUSE_MAX do begin snowID := aResHouses[HT].SnowPic + 1; if fRXData.Flag[snowID] <> 0 then shadowConverter.RemoveShadow(snowID, True); end; finally shadowConverter.Free; end; end; procedure TKMSpritePack.RemoveHouseWorkBackgrounds(aResHouses: TKMResHouses); procedure RemoveBackground(SpriteF, SpriteB, OffsetX, OffsetY: Integer); function IsSame(XF, YF, XB, YB: Integer; TreatEdgeAsSame: Boolean): Boolean; var Foreground, Background: Cardinal; begin if (XF >= 0) and (YF >= 0) and (XB >= 0) and (YB >= 0) and (XB < fRXData.Size[SpriteB].X) and (YB < fRXData.Size[SpriteB].Y) and (XF < fRXData.Size[SpriteF].X) and (YF < fRXData.Size[SpriteF].Y) then begin Foreground := fRXData.RGBA[SpriteF, YF*fRXData.Size[SpriteF].X + XF]; Background := fRXData.RGBA[SpriteB, YB*fRXData.Size[SpriteB].X + XB]; Result := (Foreground shr 24 = 0) or (Foreground = Background); end else Result := TreatEdgeAsSame; end; var X, Y, I, K, XOff, YOff, MatchingNeighbours: Integer; begin for X := 0 to fRXData.Size[SpriteF].X - 1 do for Y := 0 to fRXData.Size[SpriteF].Y - 1 do begin XOff := X + OffsetX + fRXData.Pivot[SpriteF].X - fRXData.Pivot[SpriteB].X; YOff := Y + OffsetY + fRXData.Pivot[SpriteF].Y - fRXData.Pivot[SpriteB].Y; if IsSame(X, Y, XOff, YOff, False) then begin //To avoid making incorrect 1px holes, at least two direct neighbour must match too MatchingNeighbours := 0; for I := -1 to 1 do for K := -1 to 1 do if (K=0) xor (I=0) then if IsSame(X+I, Y+K, XOff+I, YOff+K, True) then Inc(MatchingNeighbours); if MatchingNeighbours >= 2 then fRXData.RGBA[SpriteF, Y*fRXData.Size[SpriteF].X + X] := $0; end; end; end; var HT: TKMHouseType; HA: TKMHouseActionType; A: TKMAnimLoop; Step, beastHouse, beast, beastAge: Integer; const HOUSE_BEAST_LOOKUP: array[1..3] of TKMHouseType = (htSwine, htStables, htMarket); begin //Actions for HT := HOUSE_MIN to HOUSE_MAX do begin for HA := Low(TKMHouseActionType) to High(TKMHouseActionType) do begin if HA in [haWork1..haWork5, haIdle] then begin A := aResHouses[HT].Anim[HA]; for Step := 1 to A.Count do begin if A.Step[Step] > -1 then begin RemoveBackground(A.Step[Step]+1, aResHouses[HT].StonePic+1, A.MoveX, A.MoveY); end; end; end; end; end; //Beasts for beastHouse := 1 to 3 do begin for beast := 1 to 5 do begin for beastAge := 1 to 3 do begin //Market only has 2 beasts and no age if (beastHouse = 3) and ((beast > 3) or (beastAge <> 1)) then Continue; A := aResHouses.BeastAnim[HOUSE_BEAST_LOOKUP[beastHouse], beast, beastAge]; for Step := 1 to A.Count do begin if A.Step[Step] > -1 then begin RemoveBackground(A.Step[Step]+1, aResHouses[HOUSE_BEAST_LOOKUP[beastHouse]].StonePic+1, A.MoveX, A.MoveY); end; end; end; end; end; end; procedure TKMSpritePack.RemoveMarketWaresShadows(aResHouses: TKMResHouses); var I: Integer; shadowConverter: TKMSoftShadowConverter; begin Assert(fRT = rxHouses); shadowConverter := TKMSoftShadowConverter.Create(@fRXData); try for I := MARKET_WARES_TEX_START + 1 to MARKET_WARES_TEX_START + MARKET_WARES_TEX_CNT - 1 do if fRXData.Flag[I] <> 0 then shadowConverter.RemoveShadow(I, False); finally shadowConverter.Free; end; end; procedure TKMSpritePack.Allocate(aCount: Integer); begin if not fTemp then 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); //SizeNoShadow is used only for Units if fRT = rxUnits then SetLength(fRXData.SizeNoShadow, aCount); // Next could be cleared SetLength(fRXData.RGBA, aCount); SetLength(fRXData.Mask, aCount); SetLength(fRXData.HasMask, aCount); end; procedure TKMSpritePack.AllocateTemp(aCount: Integer); var cnt: Integer; begin cnt := aCount + 1; // Next array are cleared after loading, thus we have to allocate memory for them again if Length(fRXData.RGBA) < cnt then SetLength(fRXData.RGBA, cnt); if Length(fRXData.Mask) < cnt then SetLength(fRXData.Mask, cnt); if Length(fRXData.HasMask) < cnt then SetLength(fRXData.HasMask, cnt); end; //Release RAM that is no longer needed procedure TKMSpritePack.ClearTemp; begin // Clear only what we will not use for Render SetLength(fRXData.RGBA, 0); SetLength(fRXData.Mask, 0); SetLength(fRXData.HasMask, 0); end; //Add PNG images to spritepack if user has any addons in Sprites folder procedure TKMSpritePack.AddImage(const aFolder, aFilename: string; aIndex: Integer; aEnlargeOnly: Boolean = False); type TKMSpriteMaskType = (smtNone, smtPlain, smtSmart); var I,K: Integer; Tr, Tg, Tb, T: Byte; Thue, Tsat, Tbri: Single; ft: TextFile; maskFile: array [TKMSpriteMaskType] of string; maskTyp: TKMSpriteMaskType; pngWidth, pngHeight: Word; pngData: TKMCardinalArray; txtFileName: string; begin Assert(SameText(ExtractFileExt(aFilename), '.png')); if aIndex > fRXData.Count then Allocate(aIndex) else // RGBA / Mask / HasMask arrays are cleared after 1st usage, so we should reallocate memory for them if aEnlargeOnly then AllocateTemp(aIndex); LoadFromPng(aFolder + aFilename, pngWidth, pngHeight, pngData); Assert((pngWidth <= MAX_GAME_ATLAS_SIZE) and (pngHeight <= MAX_GAME_ATLAS_SIZE), Format('Image size should be less than %dx%d pixels', [MAX_GAME_ATLAS_SIZE, MAX_GAME_ATLAS_SIZE])); fRXData.Flag[aIndex] := Byte(pngWidth * pngHeight <> 0); //Mark as used (required for saving RXX) fRXData.Size[aIndex].X := pngWidth; fRXData.Size[aIndex].Y := pngHeight; SetLength(fRXData.RGBA[aIndex], pngWidth * pngHeight); SetLength(fRXData.Mask[aIndex], pngWidth * pngHeight); //Should allocate space for it's always comes along for K := 0 to pngHeight - 1 do for I := 0 to pngWidth - 1 do fRXData.RGBA[aIndex, K * pngWidth + I] := pngData[K * pngWidth + I]; maskFile[smtPlain] := aFolder + StringReplace(aFilename, '.png', 'm.png', [rfReplaceAll, rfIgnoreCase]); maskFile[smtSmart] := aFolder + StringReplace(aFilename, '.png', 'a.png', [rfReplaceAll, rfIgnoreCase]); //Determine mask processing mode if FileExists(maskFile[smtPlain]) then maskTyp := smtPlain else if FileExists(maskFile[smtSmart]) then maskTyp := smtSmart else maskTyp := smtNone; fRXData.HasMask[aIndex] := maskTyp in [smtPlain, smtSmart]; //Load and process the mask if it exists if fRXData.HasMask[aIndex] then begin //Plain masks are used 'as is' //Smart masks are designed for the artist, they convert color brightness into a mask LoadFromPng(maskFile[maskTyp], pngWidth, pngHeight, pngData); if (fRXData.Size[aIndex].X = pngWidth) and (fRXData.Size[aIndex].Y = pngHeight) then begin //We don't handle transparency in Masks for K := 0 to pngHeight - 1 do for I := 0 to pngWidth - 1 do case maskTyp of smtPlain: begin //For now process just red (assume pic is greyscale) fRXData.Mask[aIndex, K*pngWidth+I] := pngData[K*pngWidth+I] and $FF; end; smtSmart: begin if Cardinal(pngData[K*pngWidth+I] and $FFFFFF) <> 0 then begin Tr := fRXData.RGBA[aIndex, K*pngWidth+I] and $FF; Tg := fRXData.RGBA[aIndex, K*pngWidth+I] shr 8 and $FF; Tb := fRXData.RGBA[aIndex, K*pngWidth+I] shr 16 and $FF; //Determine color brightness ConvertRGB2HSB(Tr, Tg, Tb, Thue, Tsat, Tbri); //Make background RGBA black or white for more saturated colors if Tbri < 0.5 then fRXData.RGBA[aIndex, K*pngWidth+I] := FLAG_COLOR_DARK else fRXData.RGBA[aIndex, K*pngWidth+I] := FLAG_COLOR_LITE; //Map brightness from 0..1 to 0..255..0 T := Trunc((0.5 - Abs(Tbri - 0.5)) * 510); fRXData.Mask[aIndex, K*pngWidth+I] := T; end else fRXData.Mask[aIndex, K*pngWidth+I] := 0; end; end; end; end; //Read pivot info txtFileName := aFolder + StringReplace(aFilename, '.png', '.txt', [rfReplaceAll, rfIgnoreCase]); if FileExists(txtFileName) then begin AssignFile(ft, txtFileName); Reset(ft); ReadLn(ft, fRXData.Pivot[aIndex].X); ReadLn(ft, fRXData.Pivot[aIndex].Y); //SizeNoShadow is used only for Units if fRT = rxUnits then begin ReadLn(ft, fRXData.SizeNoShadow[aIndex].Left); ReadLn(ft, fRXData.SizeNoShadow[aIndex].Top); ReadLn(ft, fRXData.SizeNoShadow[aIndex].Right); ReadLn(ft, fRXData.SizeNoShadow[aIndex].Bottom); end; CloseFile(ft); end; end; // Read header of the RXX / RXA files procedure TKMSpritePack.ReadRXZHeader(aStream: TStream; out aFormat: TKMRXXFormat); const ZLIB_HEADER: AnsiString = AnsiChar($78) + AnsiChar($DA); // Header of ZLIB archiver. We used for RXX before introducing RXX1 format version var strFormat: AnsiString; metadataLen: Word; begin SetLength(strFormat, RXX_HEADER_LENGTH); aStream.Read(strFormat[1], RXX_HEADER_LENGTH); if strFormat = RXX_HEADER[rxxOne] then begin aFormat := rxxOne; // Skip 32 bytes of metadata aStream.Seek(32, soFromCurrent); end else if strFormat = RXX_HEADER[rxxTwo] then begin aFormat := rxxTwo; // For now we just skip metadata (but in the future we could show it in e.g. RXXEditor) aStream.Read(metadataLen, SizeOf(metadataLen)); aStream.Seek(metadataLen, soFromCurrent); end else if strFormat[1] + strFormat[2] = ZLIB_HEADER then begin aFormat := rxxZero; // Reset position aStream.Position := 0; end else aFormat := rxxUnknown; 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; decompressionStream: TDecompressionStream; rxxFormat: TKMRXXFormat; begin ReadRXZHeader(aStream, rxxFormat); // Not sure what to do yet, silently "fail" for now if rxxFormat = rxxUnknown then Exit; decompressionStream := TDecompressionStream.Create(aStream); try decompressionStream.Read(rxxCount, 4); if gLog <> nil then gLog.AddTime(RX_INFO[fRT].FileName + ' - %d', [rxxCount]); if rxxCount = 0 then Exit; if aAllocateGFXData then Allocate(aStartingIndex + rxxCount - 1) else AllocateRXData(aStartingIndex + rxxCount - 1); decompressionStream.Read(fRXData.Flag[aStartingIndex], rxxCount); for I := aStartingIndex to aStartingIndex + rxxCount - 1 do if fRXData.Flag[I] = 1 then begin decompressionStream.Read(fRXData.Size[I].X, SizeOf(fRXData.Size[I])); decompressionStream.Read(fRXData.Pivot[I].X, SizeOf(fRXData.Pivot[I])); //SizeNoShadow is used only for Units if fRT = rxUnits then decompressionStream.Read(fRXData.SizeNoShadow[I].Left, SizeOf(fRXData.SizeNoShadow[I])); //Data part of each sprite is 32BPP RGBA in Remake RXX files SetLength(fRXData.RGBA[I], fRXData.Size[I].X * fRXData.Size[I].Y); SetLength(fRXData.Mask[I], fRXData.Size[I].X * fRXData.Size[I].Y); decompressionStream.Read(fRXData.RGBA[I, 0], 4 * fRXData.Size[I].X * fRXData.Size[I].Y); decompressionStream.Read(fRXData.HasMask[I], 1); if fRXData.HasMask[I] then decompressionStream.Read(fRXData.Mask[I, 0], fRXData.Size[I].X * fRXData.Size[I].Y); end; 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.LoadFromRXAStream(aStream: TStream; aAllocateGFXData: Boolean); {$IFNDEF NO_OGL} var I: Integer; SAT: TKMSpriteAtlasType; rxxCount, atlasCount, spriteCount, dataCount: Integer; decompressionStream: TDecompressionStream; rxxFormat: TKMRXXFormat; {$ENDIF} begin {$IFNDEF NO_OGL} ReadRXZHeader(aStream, rxxFormat); // Not sure what to do yet, silently "fail" for now if rxxFormat = rxxUnknown then Exit; decompressionStream := TDecompressionStream.Create(aStream); try decompressionStream.Read(rxxCount, 4); gLog.AddTime(RX_INFO[fRT].FileName + ' - %d', [rxxCount]); if rxxCount = 0 then Exit; if aAllocateGFXData then Allocate(rxxCount) else AllocateRXData(rxxCount); decompressionStream.Read(fRXData.Flag[1], fRXData.Count); //Sprite info for I := 1 to fRXData.Count do if fRXData.Flag[I] = 1 then begin decompressionStream.Read(fRXData.Size[I].X, SizeOf(fRXData.Size[I])); decompressionStream.Read(fRXData.Pivot[I].X, SizeOf(fRXData.Pivot[I])); //SizeNoShadow is used only for Units if fRT = rxUnits then decompressionStream.Read(fRXData.SizeNoShadow[I].Left, SizeOf(fRXData.SizeNoShadow[I])); decompressionStream.Read(fRXData.HasMask[I], 1); // Check if our load resource thread was terminated if TThread.CheckTerminated then Exit; end; //Atlases for SAT := Low(fAtlases) to High(fAtlases) do begin decompressionStream.Read(atlasCount, 4); SetLength(fAtlases[SAT], atlasCount); for I := Low(fAtlases[SAT]) to High(fAtlases[SAT]) do with fAtlases[SAT, I] do begin decompressionStream.Read(Container.Width, 2); decompressionStream.Read(Container.Height, 2); decompressionStream.Read(spriteCount, 4); SetLength(Container.Sprites, spriteCount); decompressionStream.Read(Container.Sprites[0], spriteCount * SizeOf(Container.Sprites[0])); decompressionStream.Read(TexType, SizeOf(TKMTexFormat)); decompressionStream.Read(dataCount, 4); SetLength(Data, dataCount); decompressionStream.Read(Data[0], dataCount*SizeOf(Data[0])); // Check if our load resource thread was terminated if TThread.CheckTerminated then Exit; end; end; finally decompressionStream.Free; end; {$ENDIF} end; procedure TKMSpritePack.LoadFromRXAAndGenTextures(const aFileName: string); begin LoadFromRXAFile(aFileName); GenerateTexturesFromLoadedRXZ; end; procedure TKMSpritePack.CollectSpriteFilesToOverloadInFolder(const aFolder: string; aFileList: TStringList); {$IFDEF WDC} var filePath: string; filterPredicate: TDirectory.TFilterPredicate; {$ENDIF} begin {$IFDEF WDC} filterPredicate := function(const aPath: string; const aSearchRec: TSearchRec): Boolean var tmp: Integer; begin if ExtractRelativePath(aFolder, aPath).Contains(OVERLOAD_SKIP_MASK) then Exit(False); // Search filter we are using makes sure we get only X_*****.png filenames // Hence we need to check only for the ***** being digits Result := TryStrToInt(Copy(aSearchRec.Name, 3, Length(aSearchRec.Name)-6), tmp); end; for filePath in TDirectory.GetFiles(aFolder, IntToStr(Ord(fRT) + 1) + '_*.png', TSearchOption.soAllDirectories, filterPredicate) do aFileList.Add(ExtractRelativePath(aFolder, filePath)); {$ENDIF} end; procedure TKMSpritePack.OverloadGeneratedFromFolder(aAlphaShadows: Boolean; const aFolder: string; aSoftenShadows: Boolean = True; aOnCheckTerminated: TBooleanFuncSimple = nil); // Append all PNGs including the subfolders // Pattern is X_nnnn.png, where nnnn is dynamic (1..n chars) for modders convenience procedure AppendFolder(idList: TList); var I, id: Integer; fileList: TStringList; s: string; begin fileList := TStringList.Create; CollectSpriteFilesToOverloadInFolder(aFolder, fileList); try // Going in reverse allows us to allocate max required sprites early on (since filesnames usually come sorted by name) for I := fileList.Count - 1 downto 0 do begin s := ExtractFileName(fileList.Strings[I]); if TryStrToInt(Copy(s, 3, Length(s)-6), id) then begin AddImage(aFolder, fileList.Strings[I], id, True); idList.Add(id); end; end; // Soften shadows for overloaded sprites if aSoftenShadows then SoftenShadowsList(idList); // Determine objects size only for units (used for hitbox) //todo -cComplicated: do we need it for houses too ? if fRT = rxUnits then DetermineImagesObjectSizeList(idList); finally fileList.Free; end; end; var idList: TList; begin if not DirectoryExists(aFolder) then Exit; {$IFDEF WDC} idList := TList.Create; try AppendFolder(idList); {$IFNDEF NO_OGL} MakeGFX(aAlphaShadows, idList, False, aOnCheckTerminated); {$ENDIF} finally idList.Free; end; {$ENDIF} end; // Parse all valid files in Sprites folder: // - append or replace original sprites with new ones // - exclude original sprites if necessary as well procedure TKMSpritePack.OverloadRXDataFromFolder(const aFolder: string; aOnProgress: TProc; aSoftenShadows: Boolean = True); {$IFDEF WDC} // Append all PNGs including the subfolders // Pattern is X_nnnn.png, where nnnn is dynamic (1..n chars) for modders convenience procedure AppendFolder; var I, id: Integer; fileList: TStringList; idList: TList; s: string; begin idList := TList.Create; fileList := TStringList.Create; CollectSpriteFilesToOverloadInFolder(aFolder, fileList); try // Going in reverse allows us to allocate max required sprites early on (since filesnames usually come sorted by name) for I := fileList.Count - 1 downto 0 do begin s := ExtractFileName(fileList.Strings[I]); if TryStrToInt(Copy(s, 3, Length(s)-6), id) then begin AddImage(aFolder, fileList.Strings[I], id); idList.Add(id); end; // When packing RXX with HD anims the sheer count is so large we need a progress updates if (fileList.Count - I) mod 2500 = 0 then if Assigned(aOnProgress) then aOnProgress(Format('Appending from folder %d/%d', [fileList.Count - I, fileList.Count])); end; // Soften shadows for overloaded sprites if aSoftenShadows then SoftenShadowsList(idList); // Determine objects size only for units (used for hitbox) //todo -cComplicated: do we need it for houses too ? if fRT = rxUnits then DetermineImagesObjectSizeList(idList); finally idList.Free; fileList.Free; end; end; // Delete the extra sprites denoted by the X_**** filenames (without extensions) procedure DeleteExtra; var id: Integer; filePath, s: string; filterPredicate: TDirectory.TFilterPredicate; begin filterPredicate := function(const aPath: string; const aSearchRec: TSearchRec): Boolean var tmp: Integer; begin if ExtractRelativePath(aFolder, aPath).Contains(OVERLOAD_SKIP_MASK) then Exit(False); // Search filter we are using makes sure we get only X_***** filenames // Hence we need to check only for the ***** being digits Result := TryStrToInt(Copy(aSearchRec.Name, 3, Length(aSearchRec.Name)-2), tmp); end; for filePath in TDirectory.GetFiles(aFolder, IntToStr(Ord(fRT) + 1) + '_*', TSearchOption.soAllDirectories, filterPredicate) do begin //todo -cPractical: Would be much simpler if we had our own folder parser .. // so we could do our thing right in the callback, without need to iterate over the temp string array s := ExtractFileName(filePath); if TryStrToInt(Copy(s, 3, Length(s)-2), id) then fRXData.Flag[id] := 0; end; end; {$ENDIF} begin if not DirectoryExists(aFolder) then Exit; {$IFDEF WDC} AppendFolder; DeleteExtra; {$ENDIF} end; function TKMSpritePack.GetAverageSpriteColors(aCount: Integer): TKMColor3bArray; var I, L, M: Integer; pixelCount: Cardinal; accR, accG, accB: Cardinal; begin SetLength(Result, Min(fRXData.Count, aCount)); for I := 1 to Min(fRXData.Count, aCount) do begin accR := 0; accG := 0; accB := 0; for L := 0 to fRXData.Size[I].Y - 1 do for M := 0 to fRXData.Size[I].X - 1 do begin Inc(accR, fRXData.RGBA[I, L * fRXData.Size[I].X + M] and $FF); Inc(accG, fRXData.RGBA[I, L * fRXData.Size[I].X + M] shr 8 and $FF); Inc(accB, fRXData.RGBA[I, L * fRXData.Size[I].X + M] shr 16 and $FF); end; pixelCount := Max(1, fRXData.Size[I].X * fRXData.Size[I].Y); Result[I-1].R := Round(accR / pixelCount); Result[I-1].G := Round(accG / pixelCount); Result[I-1].B := Round(accB / pixelCount); end; end; function TKMSpritePack.IsEmpty: Boolean; begin Result := fRXData.Count = 0; end; {$IFNDEF NO_OGL} // Take RX data and make nice atlas texture out of it // Atlases should be POT to improve performance and avoid driver bugs // In result we have GFXData structure filled procedure TKMSpritePack.MakeGFX(aAlphaShadows: Boolean; aStartingIndex: Integer = 1; aFillGFXData: Boolean = True; aOnCheckTerminated: TBooleanFuncSimple = nil); var I: Integer; idList: TList; begin idList := TList.Create; try for I := aStartingIndex to fRXData.Count do idList.Add(I); MakeGFXInternal(aAlphaShadows, idList, aFillGFXData, aOnCheckTerminated, GetMaxAtlasSize); finally idList.Free; end; 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 texType: TKMTexFormat; begin if fRXData.Count = 0 then Exit; if aAlphaShadows and (fRT in [rxTrees,rxHouses,rxUnits,rxGui,rxTiles]) or not aAlphaShadows and (fRT = rxGuiMain) then texType := tfRGBA8 else texType := tfRGB5A1; MakeGFX_BinPacking(texType, aIDList, aFillGFXData, aOnCheckTerminated, aMaxAtlasSize); end; {$ENDIF} //Set GFXData from SpriteInfo procedure TKMSpritePack.SetGFXData(aTexID: Cardinal; aSpriteInfo: TKMBinItem; aAtlasType: TKMSpriteAtlasType); var K: Integer; spriteID: Integer; txCoords: TKMTexCoords; begin for K := 0 to High(aSpriteInfo.Sprites) do begin spriteID := aSpriteInfo.Sprites[K].SpriteID; txCoords.TexID := aTexID; txCoords.u1 := aSpriteInfo.Sprites[K].OriginX / aSpriteInfo.Width; txCoords.v1 := aSpriteInfo.Sprites[K].OriginY / aSpriteInfo.Height; txCoords.u2 := (aSpriteInfo.Sprites[K].OriginX + fRXData.Size[spriteID].X) / aSpriteInfo.Width; txCoords.v2 := (aSpriteInfo.Sprites[K].OriginY + fRXData.Size[spriteID].Y) / aSpriteInfo.Height; if aAtlasType = saBase then begin gGFXData[fRT, spriteID].Tex := txCoords; gGFXData[fRT, spriteID].PxWidth := fRXData.Size[spriteID].X; gGFXData[fRT, spriteID].PxHeight := fRXData.Size[spriteID].Y; end else gGFXData[fRT, spriteID].Alt := txCoords; end; end; {$IFNDEF NO_OGL} procedure TKMSpritePack.MakeGFX_BinPacking(aTexType: TKMTexFormat; aStartingIndex: Integer; aFillGFXData: Boolean = True; aOnCheckTerminated: TBooleanFuncSimple = nil); var I: Integer; idList: TList; begin idList := TList.Create; try for I := aStartingIndex to fRXData.Count do idList.Add(I); MakeGFX_BinPacking(aTexType, idList, aFillGFXData, aOnCheckTerminated); finally idList.Free; end; end; {$ENDIF} procedure TKMSpritePack.PrepareAtlases(aSpriteInfo: TBinArray; aMode: TKMSpriteAtlasType; aTexType: TKMTexFormat; aFillGFXData: Boolean = True; aOnCheckTerminated: TBooleanFuncSimple = nil); var I, K, L, M: Integer; CT, CL, Pixel: Cardinal; texID: Cardinal; ID: Integer; atlasData: TKMCardinalArray; begin // gLog.AddTime('Length(aSpriteInfo) = ' + IntToStr(Length(aSpriteInfo))); //Prepare atlases for I := 0 to Length(aSpriteInfo) - 1 do begin // if I * 2 + 1 >= I - 1 then // gLog.AddTime('I = ' + IntToStr(I)); Assert(MakePOT(aSpriteInfo[I].Width) = aSpriteInfo[I].Width); Assert(MakePOT(aSpriteInfo[I].Height) = aSpriteInfo[I].Height); SetLength(atlasData, 0); SetLength(atlasData, aSpriteInfo[I].Width * aSpriteInfo[I].Height); //Copy sprite to Atlas for K := 0 to High(aSpriteInfo[I].Sprites) do begin ID := aSpriteInfo[I].Sprites[K].SpriteID; for L := 0 to fRXData.Size[ID].Y - 1 do for M := 0 to fRXData.Size[ID].X - 1 do begin CT := aSpriteInfo[I].Sprites[K].OriginY; CL := aSpriteInfo[I].Sprites[K].OriginX; Pixel := (CT + L) * aSpriteInfo[I].Width + CL + M; if aMode = saBase then atlasData[Pixel] := fRXData.RGBA[ID, L * fRXData.Size[ID].X + M] else atlasData[Pixel] := $FFFFFF or (fRXData.Mask[ID, L * fRXData.Size[ID].X + M] shl 24); //Fill padding with edge pixels if fPad > 0 then begin if (M = 0) then begin atlasData[Pixel - 1] := atlasData[Pixel]; if (L = 0) then atlasData[Pixel - aSpriteInfo[I].Width - 1] := atlasData[Pixel] else if (L = fRXData.Size[ID].Y - 1) then atlasData[Pixel + aSpriteInfo[I].Width - 1] := atlasData[Pixel]; end; if (M = fRXData.Size[ID].X - 1) then begin atlasData[Pixel + 1] := atlasData[Pixel]; if (L = 0) then atlasData[Pixel - aSpriteInfo[I].Width + 1] := atlasData[Pixel] else if (L = fRXData.Size[ID].Y - 1) then atlasData[Pixel + aSpriteInfo[I].Width + 1] := atlasData[Pixel]; end; if (L = 0) then atlasData[Pixel - aSpriteInfo[I].Width] := atlasData[Pixel]; if (L = fRXData.Size[ID].Y - 1) then atlasData[Pixel + aSpriteInfo[I].Width] := atlasData[Pixel]; end; end; end; if aFillGFXData then begin texID := TKMRender.GenTexture(aSpriteInfo[I].Width, aSpriteInfo[I].Height, @atlasData[0], aTexType, ftNearest, ftNearest); //Now that we know texture IDs we can fill GFXData structure SetGFXData(texID, aSpriteInfo[I], aMode); end else begin Assert(InRange(I, Low(fAtlases[aMode]), High(fAtlases[aMode])), Format('Preloading sprite index out of range: %d, range [%d;%d]', [I, Low(fAtlases[aMode]), High(fAtlases[aMode])])); // Save prepared data for generating later (in main thread) fAtlases[aMode, I].Container := aSpriteInfo[I]; fAtlases[aMode, I].TexType := aTexType; fAtlases[aMode, I].Data := atlasData; end; end; end; // This algorithm is planned to take advantage of more efficient 2D bin packing {$IFNDEF NO_OGL} procedure TKMSpritePack.MakeGFX_BinPacking(aTexType: TKMTexFormat; aIDList: TList; aFillGFXData: Boolean = True; aOnCheckTerminated: TBooleanFuncSimple = nil; aMaxAtlasSize: Integer = -1); function CheckTerminated: Boolean; begin Result := Assigned(aOnCheckTerminated) and aOnCheckTerminated; end; var I, J, K: Integer; spriteSizes: TIndexSizeArray; spriteInfo: TBinArray; atlasSize, allTilesAtlasSize, maxAtlasSize: Integer; begin //Prepare base atlases SetLength(spriteSizes, aIDList.Count);// fRXData.Count - aStartingIndex + 1); K := 0; for J := 0 to aIDList.Count - 1 do begin I := aIDList[J]; if (fRXData.Size[I].X * fRXData.Size[I].Y <> 0) and (Length(fRXData.RGBA[I]) > 0) then begin spriteSizes[K].ID := I; spriteSizes[K].X := fRXData.Size[I].X; spriteSizes[K].Y := fRXData.Size[I].Y; Inc(K); end; 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(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 := maxAtlasSize; SetLength(spriteInfo, 0); BinPack(spriteSizes, atlasSize, fPad, spriteInfo); if CheckTerminated then Exit; //Our thread could be terminated and asked to stop. Exit immediately then SetLength(fAtlases[saBase], Length(spriteInfo)); PrepareAtlases(spriteInfo, saBase, aTexType, aFillGFXData, aOnCheckTerminated); if CheckTerminated then Exit; //Prepare masking atlases SetLength(spriteSizes, aIDList.Count); K := 0; for J := 0 to aIDList.Count - 1 do begin I := aIDList[J]; if (fRXData.Size[I].X * fRXData.Size[I].Y <> 0) and fRXData.HasMask[I] and (Length(fRXData.Mask[I]) > 0) then begin spriteSizes[K].ID := I; spriteSizes[K].X := fRXData.Size[I].X; spriteSizes[K].Y := fRXData.Size[I].Y; Inc(K); end; end; SetLength(spriteSizes, K); SetLength(spriteInfo, 0); BinPack(spriteSizes, atlasSize, fPad, spriteInfo); if CheckTerminated then Exit; SetLength(fAtlases[saMask], Length(spriteInfo)); PrepareAtlases(spriteInfo, saMask, tfAlpha8, aFillGFXData, aOnCheckTerminated); end; {$ENDIF} procedure TKMSpritePack.ClearGameResGenTemp; var SAT: TKMSpriteAtlasType; begin for SAT := Low(fAtlases) to High(fAtlases) do SetLength(fAtlases[SAT], 0); end; // 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; var I: Integer; SAT: TKMSpriteAtlasType; texID: Cardinal; begin for SAT := Low(fAtlases) to High(fAtlases) do for I := Low(fAtlases[SAT]) to High(fAtlases[SAT]) do begin with fAtlases[SAT,I] do begin texID := TKMRender.GenTexture(Container.Width, Container.Height, @Data[0], TexType, ftNearest, ftNearest); //Now that we know texture IDs we can fill GFXData structure SetGFXData(texID, Container, SAT); end; end; end; { TKMResSprites } constructor TKMResSprites.Create(aStepProgress: TKMEvent = nil; aStepCaption: TUnicodeStringEvent = nil; aTemp: Boolean = False); var RT: TRXType; 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 begin fSprites[RT] := TKMSpritePack.Create(RT, fTemp); if RX_INFO[RT].Usage = ruGame then fGameRXTypes.Add(IntToStr(Integer(RT))); end; fStepProgress := aStepProgress; fStepCaption := aStepCaption; 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; if not fTemp then KaMUnpublishGeneratedTerrainProvider(Self); for RT := Low(TRXType) to High(TRXType) do fSprites[RT].Free; fWorkerCompletionCS.Free; inherited; end; //Clear unused RAM procedure TKMResSprites.ClearTemp; var RT: TRXType; begin for RT := Low(TRXType) to High(TRXType) do begin fSprites[RT].ClearTemp; fSprites[RT].ClearGameResGenTemp; //Its probably empty for a menu resources, but anyway end; end; //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 procedure TKMResSprites.GenerateTerrainTransitions(aSprites: TKMSpritePack; aLegacyGeneration: Boolean = False); // procedure Rotate(aAngle: Byte; aXFrom, aYFrom: Integer; var aXTo, aYTo: Integer; aBase: Integer); // begin // case aAngle of // 0: begin // aXTo := aXFrom; // aYTo := aYFrom; // end; // 1: begin // aYTo := aXFrom; // aXTo := aBase - aYFrom; // end; // 2: begin // aXTo := aBase - aXFrom; // aYTo := aBase - aYFrom; // end; // 3: begin // aXTo := aYFrom; // aYTo := aBase - aXFrom; // end // else raise Exception.Create('Wrong Rotate angle'); // end; // // end; var TK: TKMTerrainKind; MK: TKMTileMaskKind; MT: TKMTileMaskType; MST: TKMTileMaskSubType; terrainId, maskId: Word; maskFullType: TKMMaskFullType; genTerrainInfo: TKMGenTerrainInfo; texId,{ K,} L, M, tmp, totalTex, uniqueTex: Integer; genTilesCnt, genTilesCntTemp: Integer; straightPx{, RotatePixel}, maskCol: Cardinal; generatedMasks: TDictionary; begin Assert(not aLegacyGeneration or (aSprites = nil)); if gLog <> nil then gLog.AddTime('GenerateTerrainTransitions started. Legacy = ' + BoolToStr(aLegacyGeneration, True)); if aLegacyGeneration then begin //static arrays could be reset via its variable FillChar(fGenTerrainTransitionsLegacy, SizeOf(fGenTerrainTransitionsLegacy), #0); //Init array, it could be init on previous tileset load texId := 4999; fGenTexIdStartILegacy := texId; genTilesCntTemp := (Integer(High(TKMTerrainKind)) - 1)*Integer(High(TKMTileMaskKind)) *Integer(High(TKMTileMaskType))*(Integer(High(TKMTileMaskSubType)) + 1); SetLength(fGenTerrainToTerKindLegacy, genTilesCntTemp); FillChar(fGenTerrainToTerKindLegacy[0], SizeOf(fGenTerrainToTerKindLegacy[0])*GenTilesCntTemp, #0); if gLog <> nil then gLog.AddTime('Legacy: TexId = %d GenTilesCntTemp = %d', [texId, genTilesCntTemp]); end else begin //static arrays could be reset via its variable FillChar(fGenTerrainTransitions, SizeOf(fGenTerrainTransitions), #0); //Init array, it could be init on previous tileset load texId := Length(aSprites.fRXData.RGBA) + 1; fGenTexIdStartI := texId; genTilesCnt := Integer(High(TKMTerrainKind))*Integer(High(TKMTileMaskKind)) *Integer(High(TKMTileMaskType))*(Integer(High(TKMTileMaskSubType)) + 1); SetLength(fGenTerrainToTerKind, genTilesCnt); aSprites.Allocate(texId + genTilesCnt); FillChar(fGenTerrainToTerKind[0], SizeOf(fGenTerrainToTerKind[0])*GenTilesCnt, #0); if gLog <> nil then gLog.AddTime('TexId = %d GenTilesCnt = %d', [texId, genTilesCnt]); end; generatedMasks := TDictionary.Create; totalTex := 0; uniqueTex := 0; // K := 0; try //for all Terrain Kinds for TK := Succ(tkCustom) to High(TKMTerrainKind) do begin if aLegacyGeneration and (TK = tkSnowOnGrass) then Continue; terrainId := BASE_TERRAIN[TK] + 1; // in fRXData Tiles are 1-based //for all Mask Kinds for MK := Succ(mkNone) to High(TKMTileMaskKind) do begin if aLegacyGeneration and (MK = mkSoft2) then Continue; //for all Mask Types for MT := Succ(tmtNone) to High(TKMTileMaskType) do begin // for all Mask subtypes for MST := Low(TKMTileMaskSubType) to High(TKMTileMaskSubType) do //Mask subtypes (actual masks for layers) begin maskId := TILE_MASKS_FOR_LAYERS[MK, MT, MST] + 1; if maskId = 0 then Continue; //Ignore non existent masks Inc(totalTex); // We could use same mask image for several masktypes/subtypes if generatedMasks.TryGetValue(maskId, maskFullType) then begin if aLegacyGeneration then tmp := fGenTerrainTransitionsLegacy[TK, maskFullType.Kind, maskFullType.MType, maskFullType.SubType] else tmp := fGenTerrainTransitions[TK, maskFullType.Kind, maskFullType.MType, maskFullType.SubType]; if tmp <> 0 then begin if aLegacyGeneration then fGenTerrainTransitionsLegacy[TK, MK, MT, MST] := tmp else fGenTerrainTransitions[TK, MK, MT, MST] := tmp; Continue; end; end else begin maskFullType.Kind := MK; maskFullType.MType := MT; maskFullType.SubType := MST; generatedMasks.Add(maskId, maskFullType); end; // for K := 0 to 3 do //Rotation // begin genTerrainInfo.TerKind := TK; genTerrainInfo.Mask.Kind := MK; genTerrainInfo.Mask.MType := MT; genTerrainInfo.Mask.SubType := MST; if aLegacyGeneration then begin fGenTerrainTransitionsLegacy[TK, MK, MT, MST] := texId - 1; fGenTerrainToTerKindLegacy[texId - fGenTexIdStartILegacy] := genTerrainInfo; end else begin SetLength(aSprites.fRXData.RGBA[texId], aSprites.fRXData.Size[terrainId].X*aSprites.fRXData.Size[terrainId].Y); //32*32 actually... SetLength(aSprites.fRXData.Mask[texId], aSprites.fRXData.Size[terrainId].X*aSprites.fRXData.Size[terrainId].Y); aSprites.fRXData.Flag[texId] := aSprites.fRXData.Flag[terrainId]; aSprites.fRXData.Size[texId].X := aSprites.fRXData.Size[terrainId].X; aSprites.fRXData.Size[texId].Y := aSprites.fRXData.Size[terrainId].Y; aSprites.fRXData.Pivot[texId] := aSprites.fRXData.Pivot[terrainId]; aSprites.fRXData.HasMask[texId] := False; fGenTerrainTransitions[TK, MK, MT, MST] := texId - 1; //TexId is 1-based, but textures we use - 0 based fGenTerrainToTerKind[texId - fGenTexIdStartI] := genTerrainInfo; // gLog.AddTime('TerKind: %10s Mask: %10s TexId: %d ', [ // GetEnumName(TypeInfo(TKMTerrainKind), Integer(I)), // GetEnumName(TypeInfo(TKMTileMaskType), Integer(J)), TexId]); // fGenTerrainToTerKind.Add(IntToStr(TexId) + '=' + IntToStr(Integer(I))); for L := 0 to aSprites.fRXData.Size[terrainId].Y - 1 do for M := 0 to aSprites.fRXData.Size[terrainId].X - 1 do begin // Rotate(K, L, M, P, Q, aSprites.fRXData.Size[TerrainId].X - 1); straightPx := L * aSprites.fRXData.Size[terrainId].X + M; // RotatePixel := StraightPixel; //P * aSprites.fRXData.Size[TerrainId].X + Q; case TILE_MASK_KIND_USAGE[MK] of mkuPixel: maskCol := ($FFFFFF or (aSprites.fRXData.RGBA[maskId, straightPx] shl 24)); mkuAlpha: maskCol := aSprites.fRXData.RGBA[maskId, straightPx]; else raise Exception.Create('Unexpected type'); end; aSprites.fRXData.RGBA[texId, straightPx] := maskCol and aSprites.fRXData.RGBA[terrainId, straightPx{RotatePixel}]; end; end; Inc(uniqueTex); Inc(texId); // end; end; end; end; end; if aLegacyGeneration then begin SetLength(fGenTerrainToTerKindLegacy, texId - fGenTexIdStartILegacy); if gLog <> nil then gLog.AddTime('TexId = %d; TexId - fGenTexIdStartILegacy = %d; uniqueTex = %d; totalTex = %d', [texId, texId - fGenTexIdStartILegacy, uniqueTex, totalTex]); end else begin // There could be usused place in arrays, as we could use same mask image for different purposes aSprites.Allocate(texId); SetLength(fGenTerrainToTerKind, texId - fGenTexIdStartI); if gLog <> nil then gLog.AddTime('TexId = %d; TexId - fGenTexIdStartI = %d; uniqueTex = %d; totalTex = %d', [texId, texId - fGenTexIdStartI, uniqueTex, totalTex]); end; finally if gLog <> nil then gLog.AddTime('GeneratedMasks cnt = ' + IntToStr(generatedMasks.Count)); generatedMasks.Free; end; if gLog <> nil then gLog.AddTime('GenerateTerrainTransitions Done'); end; function TKMResSprites.GetGenTerrainInfo(aTerrain: Integer): TKMGenTerrainInfo; begin Assert(aTerrain + 1 - fGenTexIdStartI < Length(fGenTerrainToTerKind), Format('Trying to get terrain info out of range: TileID = %d, GenStart = %d, GenTerrain arr length = %d', [aTerrain, fGenTexIdStartI, Length(fGenTerrainToTerKind)])); Result := fGenTerrainToTerKind[aTerrain + 1 - fGenTexIdStartI]; //TexId is 1-based, but textures we use - 0 based end; function TKMResSprites.GetGenTerrainInfoLegacy(aTerrain: Integer): TKMGenTerrainInfo; begin Assert(aTerrain + 1 - fGenTexIdStartILegacy < Length(fGenTerrainToTerKindLegacy), Format('Trying to get terrain info LEGACY out of range: TileID = %d, GenStart = %d, GenTerrain arr length = %d', [aTerrain, fGenTexIdStartILegacy, Length(fGenTerrainToTerKindLegacy)])); Result := fGenTerrainToTerKindLegacy[aTerrain + 1 - fGenTexIdStartILegacy]; //TexId is 1-based, but textures we use - 0 based end; function TKMResSprites.GetRXFileName(aRX: TRXType): string; begin Result := RX_INFO[aRX].FileName; end; function TKMResSprites.GetSprites(aRT: TRXType): TKMSpritePack; begin Result := fSprites[aRT]; end; function TKMResSprites.GetSpritesRXAFilePath(aRT: TRXType): string; begin Result := ExeDir + 'data' + PathDelim + 'Sprites' + PathDelim + RX_INFO[aRT].FileName + '.rxa'; end; function TKMResSprites.GetGenTerrainTransitions(aTerKind: TKMTerrainKind; aMaskKind: TKMTileMaskKind; aMaskType: TKMTileMaskType; aMaskSubtype: TKMTileMaskSubType): Word; begin Result := fGenTerrainTransitions[aTerKind, aMaskKind, aMaskType, aMaskSubtype]; end; procedure TKMResSprites.LoadMenuResources; var RT: TRXType; begin for RT := Low(TRXType) to High(TRXType) do if RX_INFO[RT].Usage = ruMenu then begin if Assigned(fStepCaption) then fStepCaption('Reading ' + RX_INFO[RT].FileName + ' ...'); gLog.AddTime('Reading ' + RX_INFO[RT].FileName + '.rx'); // Only GUI needs alpha channel for shadows LoadSprites(RT, RT = rxGUI); // We also use alpha channel in the generated tiles {$IFNDEF NO_OGL} fSprites[RT].MakeGFX(RT in [rxGUI, rxTiles]); {$ENDIF} if Assigned(fStepProgress) then fStepProgress; end; end; procedure TKMResSprites.LoadGameResources(aAlphaShadows: Boolean; aForceReload: Boolean = False); begin 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; fGameRequestFailureClass := ''; fGameRequestFailureText := ''; fGamePublishedThroughIndex := -1; FillChar(fGameRecoveryPending, SizeOf(fGameRecoveryPending), 0); if fGameResWorker <> nil then begin 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 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; 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 ClearGameResourceCPUPreparation(RT); fGameRXIndex := 0; fGameRequestState := grsAcquire; Exit; end; 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; //Try to load RXX first, then RX, then use Folder function TKMResSprites.LoadSprites(aRT: TRXType; aAlphaShadows: Boolean): Boolean; var alphaReference, normalReference: string; begin if gLog <> nil then gLog.AddTime('Load Sprites started'); Result := False; if aRT = rxTiles then begin alphaReference := KaMResolvePackagedResource('data/Sprites/' + RX_INFO[aRT].FileName + '_a.rxx'); normalReference := KaMResolvePackagedResource('data/Sprites/' + RX_INFO[aRT].FileName + '.rxx'); if aAlphaShadows and KaMResourceExists(alphaReference) then begin fSprites[aRT].LoadFromRXXFile(alphaReference); Result := True; end else if KaMResourceExists(normalReference) then begin fSprites[aRT].LoadFromRXXFile(normalReference); Result := True; end else Exit; end else begin if aAlphaShadows and FileExists(ExeDir + 'data' + PathDelim + 'Sprites' + PathDelim + RX_INFO[aRT].FileName + '_a.rxx') then begin fSprites[aRT].LoadFromRXXFile(ExeDir + 'data' + PathDelim + 'Sprites' + PathDelim + RX_INFO[aRT].FileName + '_a.rxx'); Result := True; end else if FileExists(ExeDir + 'data' + PathDelim + 'Sprites' + PathDelim + RX_INFO[aRT].FileName + '.rxx') then begin fSprites[aRT].LoadFromRXXFile(ExeDir + 'data' + PathDelim + 'Sprites' + PathDelim + RX_INFO[aRT].FileName + '.rxx'); Result := True; end else Exit; end; fSprites[aRT].OverloadRXDataFromFolder(ExeDir + 'Sprites' + PathDelim, nil); // Legacy support // 'Sprites' folder name confused some of the players, cause there is already data/Sprites folder fSprites[aRT].OverloadRXDataFromFolder(ExeDir + 'Modding graphics' + PathDelim, nil); if gLog <> nil then gLog.AddTime('Load Sprites Done'); // Generate terrain transitions if aRT = rxTiles then begin GenerateTerrainTransitions(fSprites[aRT]); GenerateTerrainTransitions(nil, True); //To get support for maps rev <= 10745 if not fTemp then KaMPublishGeneratedTerrainProvider(Self, GetGenTerrainInfo, GetGenTerrainInfoLegacy, GetGenTerrainTransitions); end; end; function TKMResSprites.LoadRXASpritesAndGenTextures(aRT: TRXType): Boolean; begin Result := LoadRXASprites(aRT); if Result then fSprites[aRT].GenerateTexturesFromLoadedRXZ; end; function TKMResSprites.LoadRXASprites(aRT: TRXType): Boolean; var rxaFile: string; begin Result := False; rxaFile := GetSpritesRXAFilePath(aRT); if not FileExists(rxaFile) then Exit; fSprites[aRT].LoadFromRXAFile(rxaFile); Result := True; end; class function TKMResSprites.AllTilesOnOneAtlas: Boolean; begin Result := AllTilesInOneTexture; end; procedure TKMResSprites.UpdateStateIdle; begin if (fGameRequestSerial <> 0) and not (fGameRequestState in [grsReady, grsFailed]) then AdvanceGameResourceRequest; end; constructor TKMGameResourceWorker.Create(aOwner: TKMResSprites; aSerial: QWord; aRT: TRXType; aAlphaShadows, aInputIsRXA: Boolean; aInput: TStream); begin inherited Create(True); FreeOnTerminate := False; fOwner := aOwner; fSerial := aSerial; fRT := aRT; fAlphaShadows := aAlphaShadows; fInputIsRXA := aInputIsRXA; fInput := aInput; end; destructor TKMGameResourceWorker.Destroy; begin fInput.Free; inherited; end; procedure TKMGameResourceWorker.Complete(aOutcome: TKMGameResourceWorkerOutcome; const aClass, aMessage: string); begin 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; function TKMGameResourceWorker.IsTerminated: Boolean; begin Result := Terminated; end; 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; 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; end.