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

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

No files matched your search

+85
View File
@@ -0,0 +1,85 @@
unit KM_CampaignClasses;
{$I KaM_Remake.inc}
interface
uses
KM_CommonClasses;
type
//Unique campaign identification, stored as 3 ANSI letters (TSK, TPR, etc)
//3 bytes are used to avoid string types issues
TKMCampaignId = class
private
fID: AnsiString;
function GetID: AnsiString;
procedure SetID(aID: AnsiString);
public
constructor Create(aID: AnsiString);
constructor Load(aLoadStream: TKMemoryStream);
procedure Save(aSaveStream: TKMemoryStream);
property ID: AnsiString read GetID write SetID;
class function isIdValid(aID: AnsiString): Boolean;
end;
const
CAMPAIGN_ID_LENGTH = 3;
implementation
uses
SysUtils;
{ TKMCampaignId }
constructor TKMCampaignId.Create(aID: AnsiString);
begin
inherited Create;
if not isIdValid(aID) then
raise Exception.Create('Wrong campaign ID: ' + string(aID));
fID := aID;
end;
constructor TKMCampaignId.Load(aLoadStream: TKMemoryStream);
begin
inherited Create;
aLoadStream.ReadA(fID);
end;
function TKMCampaignId.GetID: AnsiString;
begin
if Self = nil then Exit('');
Result := fID;
end;
procedure TKMCampaignId.Save(aSaveStream: TKMemoryStream);
begin
if Self = nil then Exit;
aSaveStream.WriteA(fID);
end;
procedure TKMCampaignId.SetID(aID: AnsiString);
begin
if not isIdValid(aID) then
raise Exception.Create('Wrong campaign ID: ' + string(aID));
fID := aID;
end;
class function TKMCampaignId.isIdValid(aID: AnsiString): Boolean;
begin
Result := length(aID) = CAMPAIGN_ID_LENGTH;
end;
end.
+14
View File
@@ -0,0 +1,14 @@
unit KM_CampaignTypes;
{$I KaM_Remake.inc}
interface
const
CAMPAIGN_DATA_SCRIPT_FILENAME = 'campaigndata';
CAMPAIGN_SCRIPTS_FOLDER_NAME = 'Scripts';
CAMPAIGN_SOUNDS_FOLDER_NAME = 'Sounds';
implementation
end.
+31
View File
@@ -0,0 +1,31 @@
unit KM_CampaignUtils;
{$I KaM_Remake.inc}
interface
type
TKMCampaignUtils = class
public
class function GetMissionFile(const aPath, aShortName: UnicodeString; aIndex: Byte; const aExt: UnicodeString = '.dat'): String;
class function GetMissionName(const aShortName: UnicodeString; aIndex: Byte): String;
end;
implementation
uses
SysUtils;
class function TKMCampaignUtils.GetMissionFile(const aPath, aShortName: UnicodeString; aIndex: Byte; const aExt: UnicodeString = '.dat'): String;
begin
var missionName := GetMissionName(aShortName, aIndex);
Result := aPath + missionName + PathDelim + missionName + aExt;
end;
class function TKMCampaignUtils.GetMissionName(const aShortName: UnicodeString; aIndex: Byte): String;
begin
Result := aShortName + Format('%.2d', [aIndex + 1]);
end;
end.
File diff suppressed because it is too large. Load diff
+105
View File
@@ -0,0 +1,105 @@
unit KM_MapTypes;
{$I KaM_Remake.inc}
interface
uses
KM_Defaults, KM_CommonTypes, KM_ResTypes;
type
TKMMapKind = (mkUnknown, mkSP, mkMP, mkDL);
TKMMapKindSet = set of TKMMapKind;
// Sketch of the goal and message displaying system used in KaM (from scripting point of view anyway)
// This is very similar to that used in KaM and is quite flexable/expandable.
// (we can add more parameters/conditions as well as existing KaM ones, possibly using a new script command)
// Some things are probably named unclearly, please give me suggestions or change them. Goals are the one part
// of scripting that seems to confuse everyone at first, mainly because of the TGoalStatus. In 99% of cases gsTrue and gtDefeat
// go together, because the if the defeat conditions is NOT True you lose, not the other way around. I guess it should be called a
// "survival" conditions rather than defeat.
// I put some examples below to give you an idea of how it works. Remember this is basically a copy of the goal scripting system in KaM,
// not something I designed. It can change, this is just easiest to implement from script compatability point of view.
TKMGoalType = (
gltNone = 0, // Means: It is not required for victory or defeat (e.g. simply display a message)
gltVictory, // Means: "The following condition must be True for you to win"
gltSurvive // Means: "The following condition must be True or else you lose"
);
// Conditions are the same numbers as in KaM script
TKMGoalCondition = (
gcUnknown0, // Not used/unknown
gcBuildTutorial, // Must build a tannery (and other buildings from tutorial?) for it to be True. In KaM tutorial messages will be dispalyed if this is a goal
gcTime, // A certain time must pass
gcBuildings, // Storehouse, school, barracks, TownHall
gcTroops, // All troops
gcUnknown5, // Not used/unknown
gcMilitaryAssets, // All Troops, Coal mine, Weapons Workshop, Tannery, Armory workshop, Stables, Iron mine, Iron smithy, Weapons smithy, Armory smithy, Barracks, Town hall and Vehicles Workshop
gcSerfsAndSchools, // Serfs (possibly all citizens?) and schoolhouses
gcEconomyBuildings // School, Inn and Storehouse
//We can come up with our own
);
TKMGoalStatus = (gsTrue = 0, gsFalse = 1); // Weird that it's inverted, but KaM uses it that way
TKMissionMode = (mmBuilding, mmFighting);
//* Mission difficulty
TKMMissionDifficulty = (mdNone, mdEasy3, mdEasy2, mdEasy1, mdNormal, mdHard1, mdHard2, mdHard3);
//* Set of mission difficulties
TKMMissionDifficultySet = set of TKMMissionDifficulty;
const
{$I KM_TextIDs.inc}
GOAL_BUILDINGS_HOUSES: array[0..3] of TKMHouseType = (htStore, htSchool, htBarracks, htTownHall);
//We discontinue support of other goals in favor of PascalScript scripts
GOALS_SUPPORTED: set of TKMGoalCondition =
[gcBuildings, gcTroops, gcMilitaryAssets, gcSerfsAndSchools, gcEconomyBuildings];
// Used for error's logging
GOAL_CONDITION_STR: array [TKMGoalCondition] of string = (
'Unknown 0',
'Build Tannery',
'Time',
'Store School Barracks Townhall',
'Troops',
'Unknown 5',
'Military assets',
'Serfs&Schools',
'School Inn Store'
);
MISSION_DIFFICULTY_MIN = mdEasy3;
MISSION_DIFFICULTY_MAX = mdHard3;
DIFFICULTY_LEVELS_TX: array[MISSION_DIFFICULTY_MIN..MISSION_DIFFICULTY_MAX] of Integer = (
TX_MISSION_DIFFICULTY_EASY3,
TX_MISSION_DIFFICULTY_EASY2,
TX_MISSION_DIFFICULTY_EASY1,
TX_MISSION_DIFFICULTY_NORMAL,
TX_MISSION_DIFFICULTY_HARD1,
TX_MISSION_DIFFICULTY_HARD2,
TX_MISSION_DIFFICULTY_HARD3
);
DIFFICULTY_LEVELS_COLOR: array[TKMMissionDifficulty] of Cardinal = (
icLightGray2,
icLightGreen,
icGreen,
icGreenYellow,
icYellow,
icOrange,
icDarkOrange,
icRed
);
//Map folder name by folder type. Containing single maps, for SP/MP/DL mode
MAP_FOLDER_NAME: array [TKMMapKind] of string = ('', MAPS_FOLDER_NAME, MAPS_MP_FOLDER_NAME, MAPS_DL_FOLDER_NAME);
CUSTOM_MAP_PARAM_DESCR_TX: array[TKMCustomScriptParam] of Integer = (TX_MAP_CUSTOM_PARAM_TH_TROOP_COST, TX_MAP_CUSTOM_PARAM_MARKET_PRICE);
implementation
end.
+66
View File
@@ -0,0 +1,66 @@
unit KM_MapUtils;
{$I KaM_Remake.inc}
interface
uses
KM_Defaults, KM_MapTypes;
function GuessMPPathRel(const aName, aExt: string; aCRC: Cardinal): string;
function GuessMissionPathRel(const aMissionFileRelSP, aMissionName: string; aMapFullCRC: Cardinal; aIsMultiplayer: Boolean): string;
function DetermineMapKind(const aFolderName: UnicodeString; out aMapKind: TKMMapKind): Boolean;
function GetMapKind(aIsMultiplayer: Boolean): TKMMapKind;
implementation
uses
SysUtils;
function GuessMPPathRel(const aName, aExt: string; aCRC: Cardinal): string;
var
S: UnicodeString;
begin
S := aName + '_' + IntToHex(aCRC, 8);
Result := MAP_FOLDER_NAME[mkDL] + PathDelim + S + PathDelim + S + aExt;
if not FileExists(ExeDir + Result) then
Result := MAP_FOLDER_NAME[mkMP] + PathDelim + aName + PathDelim + aName + aExt;
end;
function GuessMissionPathRel(const aMissionFileRelSP, aMissionName: string; aMapFullCRC: Cardinal; aIsMultiplayer: Boolean): string;
begin
if aIsMultiplayer then
//In MP we can't store it since it will be MapsMP or MapsDL on different clients
Result := GuessMPPathRel(aMissionName, '.dat', aMapFullCRC)
else
Result := aMissionFileRelSP; //In SP we store it
end;
//Try to determine TMapFolder for specified aFolderName
//Returns True when succeeded
function DetermineMapKind(const aFolderName: UnicodeString; out aMapKind: TKMMapKind): Boolean;
var
MK: TKMMapKind;
begin
Result := False;
// no need to test mkUnknown
for MK := Succ(Low(TKMMapKind)) to High(TKMMapKind) do
if aFolderName = MAP_FOLDER_NAME[MK] then
begin
aMapKind := MK;
Exit(True);
end;
end;
function GetMapKind(aIsMultiplayer: Boolean): TKMMapKind;
begin
if aIsMultiplayer then
Result := mkMP
else
Result := mkSP;
end;
end.
+88
View File
@@ -0,0 +1,88 @@
unit KM_MapUtilsExt;
{$I KaM_Remake.inc}
interface
uses
KM_Defaults, KM_Maps, KM_MapTypes;
function TryOpenMapPDF(aMap: TKMMapInfo): Boolean;
function GetGoalDescription(aPlayer1, aPlayer2: TKMHandID; aGoalType: TKMGoalType; aGoalCondition: TKMGoalCondition;
aColPlayer1, aColPlayer2, aColTxt, aColBld: Cardinal): string;
implementation
uses
SysUtils,
KM_Resource, KM_ResTexts, KM_ResHouses, KM_ResTypes,
KM_CommonShellUtils, KM_CommonUtils;
function TryOpenMapPDF(aMap: TKMMapInfo): Boolean;
var
pdfPath: string;
begin
Result := False;
if aMap = nil then Exit;
pdfPath := aMap.DetermineReadmeFilePath;
if pdfPath <> '' then
Result := ShellOpenFile(pdfPath);
end;
// Format mission goal description
function GetGoalDescription(aPlayer1, aPlayer2: TKMHandID; aGoalType: TKMGoalType; aGoalCondition: TKMGoalCondition;
aColPlayer1, aColPlayer2, aColTxt, aColBld: Cardinal): string;
type
TKMGoalTypeDescWordKind = (dwkFirst, dwkSecond);
const
GOAL_TYPE_DESC_TX: array[TKMGoalType, TKMGoalTypeDescWordKind] of Integer = (
(TX_MAPED_GOALS_TYPE_NONE, TX_MAPED_GOALS_TYPE_NONE),
(TX_GOAL_CONDITION_DESC_WIN_WORD, TX_GOAL_CONDITION_DESC_DESTROYED_WORD),
(TX_GOAL_CONDITION_DESC_SURVIVE_WORD, TX_GOAL_CONDITION_DESC_SAVED_WORD)
);
GOAL_COND_DESC_WORD_TX: array[TKMGoalCondition] of Integer = (
TX_MAPED_GOALS_CONDITION_NONE,
TX_MAPED_GOALS_CONDITION_TUTORIAL,
TX_MAPED_GOALS_CONDITION_TIME,
TX_MAPED_GOALS_CONDITION_BUILDS,
TX_MAPED_GOALS_CONDITION_TROOPS,
TX_MAPED_GOALS_CONDITION_UNKNOWN,
TX_MAPED_GOALS_CONDITION_ASSETS,
TX_MAPED_GOALS_CONDITION_SERFS,
TX_MAPED_GOALS_CONDITION_ECONOMY
);
var
I: Integer;
goalCondStr, housesStr: string;
begin
case aGoalCondition of
gcBuildings:
begin
housesStr := '';
for I := Low(GOAL_BUILDINGS_HOUSES) to High(GOAL_BUILDINGS_HOUSES) do
begin
if housesStr <> '' then
housesStr := housesStr + ', ';
housesStr := housesStr + gRes.Houses[GOAL_BUILDINGS_HOUSES[I]].HouseName;
end;
goalCondStr := Format(gResTexts[TX_GOAL_CONDITION_COND_DESC_BUILDINGS], [WrapWrappedColor(housesStr, aColBld)]);
end;
else
goalCondStr := gResTexts[GOAL_COND_DESC_WORD_TX[aGoalCondition]];
end;
// Format text out of string like:
// 'For player %0:s to %1:s player's %2:s %3:s must be %4:s'
// We want to get smth like (depends of the locale, ofc)
// 'For player 1 to win player's 2 troops must be destroyed'
Result := Format(gResTexts[TX_GOAL_CONDITION_DESC_PATTERN], [WrapColor(aPlayer1 + 1, aColPlayer1),
WrapColor(gResTexts[GOAL_TYPE_DESC_TX[aGoalType, dwkFirst]], aColTxt),
WrapColor(aPlayer2 + 1, aColPlayer2),
WrapColor(goalCondStr, aColTxt),
WrapColor(gResTexts[GOAL_TYPE_DESC_TX[aGoalType, dwkSecond]], aColTxt)]);
end;
end.
File diff suppressed because it is too large. Load diff
+330
View File
@@ -0,0 +1,330 @@
unit KM_MissionScript;
{$I KaM_Remake.inc}
interface
uses
KM_Defaults;
type
TKMMissionParsingMode = (
mpmSingle,
mpmMulti, //Skip players
mpmEditor //Ignore errors, load armies differently
);
TKMCommandType = (
ctUnknown = 0,
ctSetMap,
ctSetMaxPlayer,
ctSetCurrPlayer,
ctHumanPlayer,
ctUserPlayer,
ctSetHouse,
ctSetTactic,
ctAIPlayer,
ctAdvancedAIPlayer,
ctEnablePlayer,
ctSetNewRemap,
ctSetMapColor,
ctSetRGBColor,
ctCenterScreen,
ctChooseLoc,
ctChooseLocAddWare,
ctChooseLocAddUnit,
ctClearUp,
ctBlockTrade,
ctBlockUnit,
ctBlockHouse,
ctReleaseHouse,
ctReleaseAllHouses,
ctAddGoal,
ctAddLostGoal,
ctSetUnit,
ctSetRoad,
ctSetField,
ctSetWinefield,
ctSetFieldStaged,
ctSetWinefieldStaged,
ctSetStock,
ctAddWare,
ctSetAlliance,
ctSetHouseDamage,
ctSetHouseDeliveryMode,
ctSetHouseRepairMode,
ctSetHouseClosedForWorker,
ctSetUnitByStock,
ctUnitAddToLast,
ctSetUnitFood,
ctSetGroup,
ctSetGroupFood,
ctSendGroup,
ctAttackPosition,
ctAddWareToSecond,
ctAddWareTo,
ctAddWareToLast,
ctAddWareToAll,
ctAddWeapon,
ctAICharacter,
ctAINoBuild,
ctAIAutoRepair,
ctAIAutoAttack,
ctAIAutoDefend,
ctAIDefendAllies,
ctAIUnlimitedEquip,
ctAIArmyType,
ctAIStartPosition,
ctAIDefence,
ctAIAttack,
ctCopyAIAttack,
ctClearAIAttack,
ctSetRallyPoint);
const
COMMANDVALUES: array [TKMCommandType] of AnsiString = (
'',
'SET_MAP',
'SET_MAX_PLAYER',
'SET_CURR_PLAYER',
'SET_HUMAN_PLAYER', //Default human player (name left for compatibility with KaM)
'SET_USER_PLAYER', //Player can be human
'SET_HOUSE',
'SET_TACTIC',
'SET_AI_PLAYER', //Player can be AI
'SET_ADVANCED_AI_PLAYER', //Player can be Advanced AI
'ENABLE_PLAYER',
'SET_NEW_REMAP',
'SET_MAP_COLOR',
'SET_RGB_COLOR',
'CENTER_SCREEN',
'CHOOSE_LOC',
'CHOOSE_LOC_ADD_WARE',
'CHOOSE_LOC_ADD_UNIT',
'CLEAR_UP',
'BLOCK_TRADE',
'BLOCK_UNIT',
'BLOCK_HOUSE',
'RELEASE_HOUSE',
'RELEASE_ALL_HOUSES',
'ADD_GOAL',
'ADD_LOST_GOAL',
'SET_UNIT',
'SET_STREET',
'SET_FIELD',
'SET_WINEFIELD',
'SET_FIELD_STAGED',
'SET_WINEFIELD_STAGED',
'SET_STOCK','ADD_WARE',
'SET_ALLIANCE',
'SET_HOUSE_DAMAGE',
'SET_HOUSE_DELIVERY_MODE',
'SET_HOUSE_REPAIR_MODE',
'SET_HOUSE_CLOSED_FOR_WORKER',
'SET_UNIT_BY_STOCK',
'ADD_UNIT_TO_LAST',
'SET_UNIT_FOOD',
'SET_GROUP',
'SET_GROUP_FOOD',
'SEND_GROUP',
'ATTACK_POSITION',
'ADD_WARE_TO_SECOND',
'ADD_WARE_TO',
'ADD_WARE_TO_LAST',
'ADD_WARE_TO_ALL',
'ADD_WEAPON',
'SET_AI_CHARACTER',
'SET_AI_NO_BUILD',
'SET_AI_AUTO_REPAIR',
'SET_AI_AUTO_ATTACK',
'SET_AI_AUTO_DEFEND',
'SET_AI_DEFEND_ALLIES',
'SET_AI_UNLIMITED_EQUIP',
'SET_AI_ARMY_TYPE',
'SET_AI_START_POSITION',
'SET_AI_DEFENSE',
'SET_AI_ATTACK',
'COPY_AI_ATTACK',
'CLEAR_AI_ATTACK',
'SET_RALLY_POINT');
type
TKMMissionParserCommon = class
protected
fMissionFileName: string;
fLastHand: TKMHandID; //Current Player
// Fatal errors generate exceptions
// Minor error descriptions accumulate here
fMinorErrors: string;
function TextToCommandType(const aCommandText: AnsiString): TKMCommandType;
function ReadMissionFile(const aFileName: string): AnsiString;
procedure TokenizeScript(const aText: AnsiString; aMaxCmd: Byte; aCommands: array of AnsiString);
procedure ProcessCommand(CommandType: TKMCommandType; P: array of Integer; const TextParam: AnsiString = ''); virtual; abstract;
procedure AddError(const aErrorMsg: string);
public
property MinorErrors: string read fMinorErrors;
procedure LoadMission(const aFileName: string); virtual;
end;
implementation
uses
Classes, SysUtils;
{ TKMMissionParserCommon }
procedure TKMMissionParserCommon.LoadMission(const aFileName: string);
begin
fMissionFileName := aFileName;
fLastHand := -1;
end;
function TKMMissionParserCommon.TextToCommandType(const aCommandText: AnsiString): TKMCommandType;
var
ct: TKMCommandType;
begin
Result := ctUnknown;
for ct := Low(TKMCommandType) to High(TKMCommandType) do
if aCommandText = '!' + COMMANDVALUES[ct] then
Exit(ct);
end;
// Read mission file to a string and if necessary - decode it
function TKMMissionParserCommon.ReadMissionFile(const aFileName: string): AnsiString;
var
I, num: Cardinal;
F: TMemoryStream;
begin
if not FileExists(aFileName) then
raise Exception.Create(Format('Mission file %s could not be found', [aFileName]));
// Load and decode .DAT file into FileText
F := TMemoryStream.Create;
try
F.LoadFromFile(aFileName);
if F.Size = 0 then
raise Exception.Create(Format('Mission file %s is empty', [aFileName]));
// Detect whether mission is encoded so we can support decoded/encoded .DAT files
// We can't test 1st char, it can be any. Instead see how often common chracters meet
num := 0;
for I := 0 to F.Size - 1 do //tab, eol, 0..9, space, !
if PByte(NativeUInt(F.Memory)+I)^ in [9,10,13,ord('0')..ord('9'),$20,$21] then
inc(num);
//Usually 30-50% is numerals/spaces, tested on typical KaM maps, take half of that as margin
if (num/F.Size < 0.20) then
for I := 0 to F.Size - 1 do
PByte(NativeUInt(F.Memory)+I)^ := PByte(NativeUInt(F.Memory)+I)^ xor 239;
//Save text after decoding but before cleaning
if WRITE_DECODED_MISSION then
F.SaveToFile(aFileName + '.txt');
for I := 0 to F.Size - 1 do
if PByte(NativeUInt(F.Memory)+I)^ in [9, 10, 13] then //tab, eol
PByte(NativeUInt(F.Memory)+I)^ := $20; //Space
num := 0;
for I := 0 to F.Size - 1 do
begin
PByte(NativeUInt(F.Memory)+num)^ := PByte(NativeUInt(F.Memory)+I)^;
if (num <= 0)
or (
(PWord(NativeUInt(F.Memory) + num-1)^ <> $2020) //Skip double spaces and !!
and (PWord(NativeUInt(F.Memory) + num-1)^ <> $2121)) then
Inc(num);
end;
SetLength(Result, num); //Because some extra characters were removed
F.Position := 0;
F.ReadBuffer(Result[1], num);
finally
F.Free;
end;
//FileText should now be formatted nicely with 1 space between each parameter/command
end;
procedure TKMMissionParserCommon.TokenizeScript(const aText: AnsiString; aMaxCmd: Byte; aCommands: array of AnsiString);
var
I, J, K, intParam: Integer;
commandText, strParam, textParam: AnsiString;
paramList: array of Integer;
commandType: TKMCommandType;
doProcess: Boolean;
begin
SetLength(paramList, aMaxCmd);
I := 1;
repeat
if aText[I] = '!' then
begin
// Extract command until a space
commandText := '';
repeat
commandText := commandText + aText[I];
Inc(I);
until((aText[I] = #32) or (I >= Length(aText)));
// We can skip certain commands to speed up the scan
// for implementations that need only Preview/Info
doProcess := Length(aCommands) = 0;
for J := Low(aCommands) to High(aCommands) do
if (commandText = aCommands[J]) then
doProcess := True;
if doProcess then
begin
// Default uninitialized values
textParam := '';
for K := 0 to aMaxCmd - 1 do
paramList[K] := -1;
// Now convert command into type
commandType := TextToCommandType(commandText);
Inc(I);
// Extract parameters
for K := 0 to aMaxCmd - 1 do
if (I < Length(aText)) and (aText[I] <> '!') then
begin
strParam := '';
repeat
strParam := strParam + aText[I];
Inc(I);
until((I >= Length(aText)) or (aText[I] = '!') or (aText[I] = #32)); // Until we find another ! OR we run out of data
// Convert to an integer, if possible
if TryStrToInt(string(strParam), intParam) then
paramList[K] := intParam
else
if K = 0 then
textParam := strParam; // Accept text for first parameter
if (I <= Length(aText)) and (aText[I] = #32) then
Inc(I);
end;
// We now have command text and parameters, so process them
ProcessCommand(commandType, paramList, textParam);
end;
end
else
Inc(I);
until (I >= Length(aText));
end;
// A nice way of debugging script errors.
// Shows the error to the user so they know exactly what they did wrong.
procedure TKMMissionParserCommon.AddError(const aErrorMsg: string);
begin
fMinorErrors := fMinorErrors + aErrorMsg + '|';
end;
end.
+149
View File
@@ -0,0 +1,149 @@
unit KM_MissionScript_Info;
{$I KaM_Remake.inc}
interface
uses
KM_MissionScript, KM_Maps;
type
TKMMissionParsing = (
pmBase, // Load base map info for SP maplist (player count, tactic, description)
pmExtra, // Load extra map info to be displayed when map is selected (goals, alliances, etc)
pmPlayers // Load information only about players (count, allowed types)
);
TKMMissionParserInfo = class(TKMMissionParserCommon)
private
fMapInfo: TKMMapInfo; // We are given this structure and asked to fill it
procedure LoadMapInfo(const aFileName: string);
protected
procedure ProcessCommand(CommandType: TKMCommandType; P: array of Integer; const TextParam: AnsiString = ''); override;
public
procedure LoadMission(const aFileName: string; aMapInfo: TKMMapInfo; aParsing: TKMMissionParsing); reintroduce;
end;
implementation
uses
SysUtils, Math,
KM_Resource,
KM_CommonClasses, KM_Defaults, KM_Utils, KM_MapTypes;
{ TKMMissionParserInfo }
procedure TKMMissionParserInfo.LoadMission(const aFileName: string; aMapInfo: TKMMapInfo; aParsing: TKMMissionParsing);
const
COMMANDS_BASE: array [0..3] of AnsiString = (
'!SET_MAX_PLAYER', '!SET_TACTIC', '!SET_CURR_PLAYER', '!SET_USER_PLAYER');
COMMANDS_EXTRA: array [0..11] of AnsiString = (
'!SET_MAX_PLAYER', '!SET_TACTIC',
'!SET_CURR_PLAYER', '!SET_HUMAN_PLAYER', '!SET_USER_PLAYER',
'!SET_AI_PLAYER', '!SET_ADVANCED_AI_PLAYER', '!ADD_GOAL', '!ADD_LOST_GOAL', '!SET_ALLIANCE', '!SET_MAP_COLOR', '!SET_RGB_COLOR');
COMMANDS_PLAYERS: array [0..5] of AnsiString = (
'!SET_MAX_PLAYER', '!SET_CURR_PLAYER', '!SET_USER_PLAYER', '!SET_HUMAN_PLAYER', '!SET_AI_PLAYER', '!SET_ADVANCED_AI_PLAYER');
var
fileText: AnsiString;
begin
fMapInfo := aMapInfo;
inherited LoadMission(aFileName);
fileText := ReadMissionFile(aFileName);
if fileText = '' then
raise Exception.Create('Script is empty');
// For info we need only few commands, it makes sense to skip the rest
case aParsing of
pmBase: TokenizeScript(fileText, 4, COMMANDS_BASE);
pmExtra: TokenizeScript(fileText, 4, COMMANDS_EXTRA);
pmPlayers: TokenizeScript(fileText, 4, COMMANDS_PLAYERS);
end;
LoadMapInfo(ChangeFileExt(fMissionFileName, '.map'));
end;
procedure TKMMissionParserInfo.ProcessCommand(CommandType: TKMCommandType; P: array of Integer; const TextParam: AnsiString = '');
begin
case CommandType of
ctSetMaxPlayer: fMapInfo.LocCount := P[0];
ctSetTactic: fMapInfo.MissionMode := mmFighting;
ctSetCurrPlayer: fLastHand := P[0];
ctHumanPlayer: begin
//Default human player can be human, obviously
fMapInfo.DefaultHuman := P[0];
fMapInfo.CanBeHuman[P[0]] := True;
end;
ctUserPlayer: if P[0] = -1 then
fMapInfo.CanBeHuman[fLastHand] := True
else
fMapInfo.CanBeHuman[P[0]] := True;
ctAIPlayer: if P[0] = -1 then
fMapInfo.CanBeClassicAI[fLastHand] := True
else
fMapInfo.CanBeClassicAI[P[0]] := True;
ctAdvancedAIPlayer:if P[0] = -1 then
fMapInfo.CanBeAdvancedAI[fLastHand] := True
else
fMapInfo.CanBeAdvancedAI[P[0]] := True;
ctAddGoal: if fLastHand >= 0 then
//If the condition is time then P[3] is the time, else it is player ID
if TKMGoalCondition(P[0]) = gcTime then
fMapInfo.AddGoal(gltVictory, fLastHand, TKMGoalCondition(P[0]), TKMGoalStatus(P[1]), -1)
else
fMapInfo.AddGoal(gltVictory, fLastHand, TKMGoalCondition(P[0]), TKMGoalStatus(P[1]), P[3]);
ctAddLostGoal: if fLastHand >= 0 then
//If the condition is time then P[3] is the time, else it is player ID
if TKMGoalCondition(P[0]) = gcTime then
fMapInfo.AddGoal(gltSurvive, fLastHand, TKMGoalCondition(P[0]), TKMGoalStatus(P[1]), -1)
else
fMapInfo.AddGoal(gltSurvive, fLastHand, TKMGoalCondition(P[0]), TKMGoalStatus(P[1]), P[3]);
ctSetAlliance: if (fLastHand >= 0) and (P[0] <> fLastHand) then //Can't be enemies with yourself
if P[1] = 1 then
fMapInfo.Alliances[fLastHand, P[0]] := atAlly
else
fMapInfo.Alliances[fLastHand, P[0]] := atEnemy;
ctSetMapColor: if fLastHand >= 0 then
//For now simply use the minimap color for all color, it is too hard to load all 8 shades from ctSetNewRemap
fMapInfo.FlagColors[fLastHand] := gRes.Palettes.DefaultPalette.Color32(P[0]);
ctSetRGBColor: if fLastHand >= 0 then
fMapInfo.FlagColors[fLastHand] := P[0] or $FF000000;
end;
end;
// Acquire essential terrain details
procedure TKMMissionParserInfo.LoadMapInfo(const aFileName: string);
var
S: TKMemoryStream;
newX, newY: Integer;
begin
if not FileExists(aFileName) then
raise Exception.Create('Map file couldn''t be found');
S := TKMemoryStreamBinary.Create;
try
S.LoadFromFile(aFileName);
LoadMapHeader(S, newX, newY);
finally
S.Free;
end;
fMapInfo.MapSizeX := newX;
fMapInfo.MapSizeY := newY;
end;
end.
@@ -0,0 +1,266 @@
unit KM_MissionScript_Preview;
{$I KaM_Remake.inc}
interface
uses
KM_MissionScript,
KM_Defaults, KM_Points;
type
TKMTilePreview = record
TileID: Word;
TileHeight: Byte; //Used for calculating light
TileOwner: TKMHandID;
Revealed: Boolean;
end;
TKMHandPreview = record
Color: Cardinal;
StartingLoc: TKMPoint;
CanHuman, CanAI: Boolean;
end;
//Specially optimized mission parser for map previews
TKMMissionParserPreview = class(TKMMissionParserCommon)
private
fMapX: Integer;
fMapY: Integer;
fHandPreview: array [0 .. MAX_HANDS-1] of TKMHandPreview;
fMapPreview: array of TKMTilePreview;
fRevealFor: array of TKMHandID;
function GetTileInfo(X, Y: Integer): TKMTilePreview;
function GetPlayerInfo(aIndex: Byte): TKMHandPreview;
procedure LoadMapData(const aFileName: string);
protected
procedure ProcessCommand(CommandType: TKMCommandType; P: array of Integer; const TextParam: AnsiString = ''); override;
public
property MapPreview[X, Y: Integer]: TKMTilePreview read GetTileInfo;
property PlayerPreview[aIndex: Byte]: TKMHandPreview read GetPlayerInfo;
property MapX: Integer read fMapX;
property MapY: Integer read fMapY;
procedure LoadMission(const aFileName: string; const aRevealFor: array of TKMHandID); reintroduce;
end;
implementation
uses
Classes, SysUtils, Math,
KM_TerrainUtils,
KM_HandTypes,
KM_Resource, KM_ResHouses, KM_ResUnits,
KM_CommonClasses, KM_CommonUtils, KM_Utils,
KM_ResTypes, KM_TerrainTypes;
{ TKMMissionParserPreview }
function TKMMissionParserPreview.GetTileInfo(X,Y: Integer): TKMTilePreview;
begin
Result := fMapPreview[(Y-1)*fMapX + X-1];
end;
function TKMMissionParserPreview.GetPlayerInfo(aIndex: Byte): TKMHandPreview;
begin
Result := fHandPreview[aIndex];
end;
// Load terrain data into liteweight structure, take only what we need for preview
procedure TKMMissionParserPreview.LoadMapData(const aFileName: string);
var
I: Integer;
S: TKMemoryStream;
tileBasic: TKMTerrainTileBasic;
gameRev: Integer;
begin
if not FileExists(aFileName) then
raise Exception.Create('Map file couldn''t be found');
S := TKMemoryStreamBinary.Create;
try
gameRev := 0;
S.LoadFromFile(aFileName);
LoadMapHeader(S, fMapX, fMapY, gameRev);
SetLength(fMapPreview, fMapX * fMapY);
for I := 0 to fMapX * fMapY - 1 do
begin
ReadTileFromStream(S, tileBasic, gameRev);
fMapPreview[I].TileID := tileBasic.BaseLayer.Terrain;
fMapPreview[I].TileHeight := tileBasic.Height;
// Fill in blanks
fMapPreview[I].TileOwner := HAND_NONE;
fMapPreview[I].Revealed := False;
end;
finally
S.Free;
end;
end;
procedure TKMMissionParserPreview.ProcessCommand(CommandType: TKMCommandType; P: array of Integer; const TextParam: AnsiString = '');
function PointInMap(X, Y: Integer): Boolean;
begin
Result := InRange(X, 1, fMapX)
and InRange(Y, 1, fMapY);
end;
procedure SetOwner(X,Y: Word);
begin
fMapPreview[X-1 + (Y-1)*fMapX].TileOwner := fLastHand;
end;
function RevealForPlayer(aPlayerIndex: TKMHandID): Boolean;
var
I: Integer;
begin
Result := False;
for I := 0 to Length(fRevealFor)-1 do
if (fRevealFor[I] = aPlayerIndex) then
Exit(True);
end;
procedure RevealCircle(X,Y,Radius: Word);
var
I, K: Word;
begin
if not RevealForPlayer(fLastHand) then
Exit;
// Unknown which overload to take
for I := Max(Integer(Y-Radius),1) to Min(Integer(Y+Radius),Integer(fMapY)) do
for K := Max(Integer(X-Radius),1) to Min(Integer(X+Radius),Integer(fMapX)) do
if (Sqr(X-K) + Sqr(Y-I)) <= Sqr(Radius) then
fMapPreview[(I-1)*fMapX + K-1].Revealed := True;
end;
var
I, K: Integer;
HA: TKMHouseArea;
valid: Boolean;
loc: TKMPoint;
begin
case CommandType of
ctSetCurrPlayer: fLastHand := P[0];
ctSetHouse: if InRange(P[0], Low(HOUSE_ID_TO_TYPE), High(HOUSE_ID_TO_TYPE))
and PointInMap(P[1]+1, P[2]+1) then
begin
RevealCircle(P[1]+1, P[2]+1, gRes.Houses[HOUSE_ID_TO_TYPE[P[0]]].Sight);
HA := gRes.Houses[HOUSE_ID_TO_TYPE[P[0]]].BuildArea;
for i:=1 to 4 do for k:=1 to 4 do
if HA[i,k] <> 0 then
if InRange(P[1]+1+k-3, 1, fMapX) and InRange(P[2]+1+i-4, 1, fMapY) then
SetOwner(P[1]+1+k-3, P[2]+1+i-4);
end;
ctSetMapColor: if InRange(fLastHand, 0, MAX_HANDS-1) then
fHandPreview[fLastHand].Color := gRes.Palettes.DefaultPalette.Color32(P[0]);
ctSetRGBColor: if InRange(fLastHand, 0, MAX_HANDS-1) then
fHandPreview[fLastHand].Color := P[0] or $FF000000;
ctCenterScreen: if PointInMap(P[0]+1, P[1]+1) then
fHandPreview[fLastHand].StartingLoc := TKMPoint.New(P[0]+1,P[1]+1);
ctHumanPlayer: //Default human player can be human, obviously
fHandPreview[P[0]].CanHuman := True;
ctUserPlayer: if P[0] = -1 then
fHandPreview[fLastHand].CanHuman := True
else
fHandPreview[P[0]].CanHuman := True;
ctAIPlayer,
ctAdvancedAIPlayer:if P[0] = -1 then
fHandPreview[fLastHand].CanAI := True
else
fHandPreview[P[0]].CanAI := True;
ctSetRoad,
ctSetField,
ctSetWinefield,
ctSetFieldStaged,
ctSetWinefieldStaged:
if PointInMap(P[0]+1, P[1]+1) then
SetOwner(P[0]+1, P[1]+1);
ctSetUnit: if PointInMap(P[1]+1, P[2]+1) and
not (UNIT_OLD_ID_TO_TYPE[P[0]] in [ANIMAL_MIN..ANIMAL_MAX]) then //Skip animals
begin
SetOwner(P[1]+1, P[2]+1);
RevealCircle(P[1]+1, P[2]+1, gRes.Units[UNIT_OLD_ID_TO_TYPE[P[0]]].Sight);
end;
ctSetStock: if PointInMap(P[1]+1, P[2]+1) then
begin
//Set Store and roads below
ProcessCommand(ctSetHouse,[11,P[0]+1,P[1]+1]);
ProcessCommand(ctSetRoad, [ P[0]-2,P[1]+1]);
ProcessCommand(ctSetRoad, [ P[0]-1,P[1]+1]);
ProcessCommand(ctSetRoad, [ P[0] ,P[1]+1]);
end;
ctSetGroup: if InRange(P[0], Low(UNIT_ID_TO_TYPE), High(UNIT_ID_TO_TYPE)) and (UNIT_ID_TO_TYPE[P[0]] <> utNone)
and PointInMap(P[1]+1, P[2]+1) then
for I := 0 to P[5] - 1 do
begin
loc := GetPositionInGroup2(P[1]+1,P[2]+1,TKMDirection(P[3]+1), I, P[4],fMapX,fMapY,valid);
if valid then
begin
SetOwner(loc.X,loc.Y);
RevealCircle(P[1]+1, P[2]+1, gRes.Units[UNIT_OLD_ID_TO_TYPE[P[0]]].Sight);
end;
end;
ctClearUp: begin
if (P[0] = 255) then
begin
if RevealForPlayer(fLastHand) then
for I := 0 to fMapX * fMapY - 1 do
fMapPreview[I].Revealed := True;
end
else if PointInMap(P[0]+1, P[1]+1) then
RevealCircle(P[0]+1, P[1]+1, P[2]);
end;
end;
end;
// We use custom mission loader for speed (compare only used commands)
procedure TKMMissionParserPreview.LoadMission(const aFileName: string; const aRevealFor: array of TKMHandID);
const
ALLOWED_COMMANDS: array [0..16] of AnsiString = (
'!SET_MAP', '!SET_MAP_COLOR', '!SET_RGB_COLOR', '!SET_AI_PLAYER', '!SET_ADVANCED_AI_PLAYER', '!CENTER_SCREEN',
'!SET_CURR_PLAYER', '!SET_HUMAN_PLAYER', '!SET_USER_PLAYER',
'!SET_STREET', '!SET_FIELD', '!SET_WINEFIELD', '!SET_STOCK',
'!SET_HOUSE', '!CLEAR_UP', '!SET_UNIT', '!SET_GROUP');
var
I: Integer;
fileText: AnsiString;
begin
inherited LoadMission(aFileName);
SetLength(fRevealFor, Length(aRevealFor));
for I := Low(aRevealFor) to High(aRevealFor) do
fRevealFor[I] := aRevealFor[I];
FillChar(fHandPreview, SizeOf(fHandPreview), #0);
for I := 0 to MAX_HANDS-1 do
fHandPreview[I].Color := DEFAULT_PLAYERS_COLORS[I];
fileText := ReadMissionFile(aFileName);
if fileText = '' then
raise Exception.Create('Script is empty');
// We need to load map dimensions first, so that SetGroup could access map bounds
LoadMapData(ChangeFileExt(fMissionFileName, '.map'));
TokenizeScript(fileText, 6, ALLOWED_COMMANDS);
end;
end.
File diff suppressed because it is too large. Load diff