1
0
mirror of https://github.com/vcmi/vcmi.git synced 2025-11-25 22:42:04 +02:00

Merge branch 'develop' into feature/nullkiller2

This commit is contained in:
Mircea TheHonestCTO
2025-08-20 01:50:35 +02:00
17 changed files with 319 additions and 144 deletions

View File

@@ -272,11 +272,10 @@ jobs:
if: ${{ startsWith(matrix.platform, 'android') }} if: ${{ startsWith(matrix.platform, 'android') }}
run: | run: |
OUTPUT_DIRECTORY="${{ github.workspace }}/out/build/${{ matrix.preset }}/android-build/vcmi-app/build/outputs" OUTPUT_DIRECTORY="${{ github.workspace }}/out/build/${{ matrix.preset }}/android-build/vcmi-app/build/outputs"
mv "$OUTPUT_DIRECTORY/apk/release/vcmi-release.apk" "${{ github.workspace }}/$VCMI_PACKAGE_FILE_NAME.apk" mv "$OUTPUT_DIRECTORY/apk/release/vcmi-release.apk" "${{github.workspace}}/out/build/${{matrix.preset}}/${{ env.VCMI_PACKAGE_FILE_NAME }}.apk"
mv "$OUTPUT_DIRECTORY/bundle/release/vcmi-release.aab" "${{ github.workspace }}/$VCMI_PACKAGE_FILE_NAME.aab" mv "$OUTPUT_DIRECTORY/bundle/release/vcmi-release.aab" "${{github.workspace}}/out/build/${{matrix.preset}}/${{ env.VCMI_PACKAGE_FILE_NAME }}.aab"
- name: Upload Artifact - name: Upload Artifact
if: ${{ matrix.pack == 1 }}
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
with: with:
name: ${{ env.VCMI_PACKAGE_FILE_NAME }} - ${{ matrix.platform }} name: ${{ env.VCMI_PACKAGE_FILE_NAME }} - ${{ matrix.platform }}

View File

@@ -425,7 +425,7 @@ void ObjectClusterizer::clusterizeObject(
for(auto & path : pathCache) for(auto & path : pathCache)
{ {
#if NKAI_TRACE_LEVEL >= 2 #if NKAI_TRACE_LEVEL >= 2
logAi->trace("Checking path %s", path.toString()); logAi->trace("ObjectClusterizer Checking path %s", path.toString());
#endif #endif
if(ai->heroManager->getHeroRole(path.targetHero) == HeroRole::SCOUT) if(ai->heroManager->getHeroRole(path.targetHero) == HeroRole::SCOUT)

View File

@@ -62,7 +62,7 @@ Goals::TGoalVec ClusterBehavior::decomposeCluster(const Nullkiller * ai, std::sh
for(auto path = paths.begin(); path != paths.end();) for(auto path = paths.begin(); path != paths.end();)
{ {
#if NKAI_TRACE_LEVEL >= 2 #if NKAI_TRACE_LEVEL >= 2
logAi->trace("Checking path %s", path->toString()); logAi->trace("ClusterBehavior Checking path %s", path->toString());
#endif #endif
auto blocker = ai->objectClusterizer->getBlocker(*path); auto blocker = ai->objectClusterizer->getBlocker(*path);

View File

@@ -72,6 +72,8 @@ void DeepDecomposer::decompose(TGoalVec & result, TSubgoal behavior, int depthLi
// 0 - goals directly from behavior // 0 - goals directly from behavior
Goals::TSubgoal task = depth >= 1 ? aggregateGoals(0, subgoal) : subgoal; Goals::TSubgoal task = depth >= 1 ? aggregateGoals(0, subgoal) : subgoal;
// Issue with CGameHandler::spawnWanderingMonsters, see getFreeTiles(tiles, true);
// danger not linked GraphPaths::addChainInfo, so spawning only with nearby unblocked
#if NKAI_TRACE_LEVEL >= 1 #if NKAI_TRACE_LEVEL >= 1
logAi->trace("Found task %s", task->toString()); logAi->trace("Found task %s", task->toString());
#endif #endif

View File

@@ -1,19 +1,71 @@
; VCMI Installer Script Instructions ; ============================================================================
; VCMI Installer – Adding a New Translation
; Steps to Add a New Translation to the Installer: ; ============================================================================
;
; 1. Download the ISL file for your language from the Inno Setup repository: ; 1. Download the base ISL file for your language:
; https://github.com/jrsoftware/issrc/tree/main/Files/Languages ; - Get the appropriate .isl file from the official Inno Setup repository:
; ; https://github.com/jrsoftware/issrc/tree/main/Files/Languages
; 2. Add the required VCMI custom messages to the downloaded ISL file. ;
; - Refer to the English.isl file for examples of the necessary custom messages. ; 2. Add VCMI custom messages:
; - Ensure all custom messages, including WindowsVersionNotSupported, are properly translated and aligned with the English version. ; - Open the downloaded .isl file and insert all VCMI-specific messages.
; 3. Update the ConfirmUninstall message: ; - Use English.isl (VCMI version) as a reference.
; - Custom Uninstall Wizard is used, ensure the ConfirmUninstall message is consistent with the English version and accurately reflects the intended functionality. ; - Ensure translations keep placeholders (%1, %2, etc.) intact and match
; 4. Update the WindowsVersionNotSupported message: ; the format of the English version exactly.
; - Ensure the WindowsVersionNotSupported message is consistent with the English version and accurately reflects the intended functionality. ;
; 5. Add the new language entry to the [Languages] section of the script: ; 3. Update/translate the following modified messages:
; - Use the correct syntax to include the language and its corresponding ISL file in the installer configuration. ; These differ from the default Inno Setup language files and are required
; for VCMI's custom installer functionality.
;
; ------------------------------------------------------------------------
; WindowsVersionNotSupported
; ------------------------------------------------------------------------
; Why: VCMI adds a more descriptive message for unsupported Windows versions.
; Original:
; WindowsVersionNotSupported=This program does not support the version of Windows your computer is running.
; VCMI version:
; WindowsVersionNotSupported=This program cannot run on your version of Windows. Please ensure you are using the correct Windows version.
;
; ------------------------------------------------------------------------
; PrivilegesRequiredOverride* messages
; ------------------------------------------------------------------------
; Why: VCMI customizes privilege escalation messages to clarify installation
; for all users vs. current user and highlight administrative rights.
; Messages to add/update:
; PrivilegesRequiredOverrideTitle
; PrivilegesRequiredOverrideInstruction
; PrivilegesRequiredOverrideText1
; PrivilegesRequiredOverrideText2
; PrivilegesRequiredOverrideAllUsers
; PrivilegesRequiredOverrideAllUsersRecommended
; PrivilegesRequiredOverrideCurrentUser
; PrivilegesRequiredOverrideCurrentUserRecommended
;
; Example (VCMI English):
; PrivilegesRequiredOverrideTitle=Administrator Privileges Required
; PrivilegesRequiredOverrideInstruction=Choose how to run the installer
; PrivilegesRequiredOverrideText1=%1 requires administrative rights to install for all users. You can also install just for your account (no administrative rights required) or for all users (administrator rights required).
; PrivilegesRequiredOverrideText2=%1 can be installed only for your account (no administrative rights required) or for all users (administrator rights required).
; PrivilegesRequiredOverrideAllUsers=Run as &Administrator (install for all users)
; PrivilegesRequiredOverrideAllUsersRecommended=Run as &Administrator (recommended)
; PrivilegesRequiredOverrideCurrentUser=Run as &Standard User (install for me only)
; PrivilegesRequiredOverrideCurrentUserRecommended=Run as &Standard User (recommended)
;
; ------------------------------------------------------------------------
; ConfirmUninstall
; ------------------------------------------------------------------------
; Why: VCMI uses a custom uninstall wizard. The message must reflect this.
; Original:
; ConfirmUninstall=Are you sure you want to completely remove %1 and all of its components?
; VCMI version:
; ConfirmUninstall=Are you sure you want to run the %1 uninstall wizard?
;
; 4. Add the new language to the installer:
; - In the [Languages] section of the script, register your translation:
; Name: "YourLanguage"; MessagesFile: "{#LangPath}\YourLanguage.isl"
;
; 5. Verify consistency:
; - Check all custom messages against the English VCMI ISL file.
; - Test the installer to confirm all messages appear correctly.
; Manual preprocessor definitions are provided using ISCC.exe parameters. ; Manual preprocessor definitions are provided using ISCC.exe parameters.
@@ -123,14 +175,13 @@ Name: "{code:GetUserDesktopFolder}\{cm:ShortcutLauncher}"; Filename: "{app}\VCMI
[Tasks] [Tasks]
Name: "desktop"; Description: "{cm:CreateDesktopShortcuts}"; GroupDescription: "{cm:SystemIntegration}" Name: "desktop"; Description: "{cm:CreateDesktopShortcuts}"; GroupDescription: "{cm:SystemIntegration}"; Check: not IsPRInstaller
Name: "startmenu"; Description: "{cm:CreateStartMenuShortcuts}"; GroupDescription: "{cm:SystemIntegration}" Name: "startmenu"; Description: "{cm:CreateStartMenuShortcuts}"; GroupDescription: "{cm:SystemIntegration}"; Check: not IsPRInstaller
Name: "fileassociation_h3m"; Description: "{cm:AssociateH3MFiles}"; GroupDescription: "{cm:SystemIntegration}"; Flags: unchecked Name: "fileassociation_h3m"; Description: "{cm:AssociateH3MFiles}"; GroupDescription: "{cm:SystemIntegration}"; Flags: unchecked; Check: not IsPRInstaller
Name: "fileassociation_vcmimap"; Description: "{cm:AssociateVCMIMapFiles}"; GroupDescription: "{cm:SystemIntegration}" Name: "fileassociation_vcmimap"; Description: "{cm:AssociateVCMIMapFiles}"; GroupDescription: "{cm:SystemIntegration}"; Check: not IsPRInstaller
Name: "firewallrules"; Description: "{cm:AddFirewallRules}"; GroupDescription: "{cm:VCMISettings}"; Check: IsAdminInstallMode
Name: "h3copyfiles"; Description: "{cm:CopyH3Files}"; GroupDescription: "{cm:VCMISettings}"; Check: IsHeroes3Installed and IsCopyFilesNeeded
Name: "firewallrules"; Description: "{cm:AddFirewallRules}"; GroupDescription: "{cm:VCMISettings}"; Check: not IsPRInstaller and IsAdminInstallMode
Name: "h3copyfiles"; Description: "{cm:CopyH3Files}"; GroupDescription: "{cm:VCMISettings}"; Check: not IsPRInstaller and IsHeroes3Installed and IsCopyFilesNeeded
[Registry] [Registry]
Root: HKCU; Subkey: "Software\{#VCMIFolder}"; ValueType: string; ValueName: "InstallPath"; ValueData: "{app}"; Flags: uninsdeletekey Root: HKCU; Subkey: "Software\{#VCMIFolder}"; ValueType: string; ValueName: "InstallPath"; ValueData: "{app}"; Flags: uninsdeletekey
@@ -328,7 +379,7 @@ begin
// For 32-bit installer on ARM64, return the 32-bit Program Files directory // For 32-bit installer on ARM64, return the 32-bit Program Files directory
Result := ExpandConstant('{commonpf32}') Result := ExpandConstant('{commonpf32}')
else else
// For AMR64 installer, return the Program Files directory // For AMR64 installer, return the Program Files directory
Result := ExpandConstant('{commonpf}') Result := ExpandConstant('{commonpf}')
end end
else if IsWin64 then else if IsWin64 then
@@ -382,21 +433,18 @@ end;
procedure OnTaskCheck(Sender: TObject); procedure OnTaskCheck(Sender: TObject);
var var
i: Integer; idx: Integer;
begin begin
// Loop through all tasks in the tasks list // Get the index of the currently clicked task
for i := 0 to WizardForm.TasksList.Items.Count - 1 do idx := WizardForm.TasksList.ItemIndex;
// Check if the clicked task is the "AddFirewallRules" one
if WizardForm.TasksList.Items[idx] = ExpandConstant('{cm:AddFirewallRules}') then
begin begin
// Check if the current task is "firewallrules" // If it was just unchecked, show the warning
if WizardForm.TasksList.Items[i] = ExpandConstant('{cm:AddFirewallRules}') then if not WizardForm.TasksList.Checked[idx] then
begin begin
// Check if the "firewallrules" task is unchecked MsgBox(ExpandConstant('{cm:Warning}') + '!' + #13#10 + #13#10 + ExpandConstant('{cm:InstallForMeOnly1}') + #13#10 + ExpandConstant('{cm:InstallForMeOnly2}'), mbError, MB_OK);
if not WizardForm.TasksList.Checked[i] then
begin
// Show a custom warning message box
MsgBox(ExpandConstant('{cm:Warning}') + '!' + #13#10 + #13#10 + ExpandConstant('{cm:InstallForMeOnly1}') + #13#10 + ExpandConstant('{cm:InstallForMeOnly2}'), mbError, MB_OK);
end;
Exit; // Task found, exit the loop
end; end;
end; end;
end; end;
@@ -425,24 +473,27 @@ function IsUCRTNeeded: Boolean;
var var
FileName: String; FileName: String;
begin begin
Result := False; // Default to not copying files Result := True; // Default to copy the file
// Normalize and extract the file name from CurrentFileName
FileName := ExtractFileName(ExpandConstant(CurrentFileName)); FileName := ExtractFileName(ExpandConstant(CurrentFileName));
// Check for file existence based on architecture // Only check system if the file name contains "api"
if IsWin64 then if Pos('API', UpperCase(FileName)) = 1 then
begin begin
if ExpandConstant('{#InstallerArch}') = 'x64' then // Check existence based on architecture
// For 64-bit installer on 64-bit OS, check System32 if IsWin64 then
Result := not FileExists(ExpandConstant('{win}\System32\' + FileName)) begin
if ExpandConstant('{#InstallerArch}') = 'x64' then
// For 64-bit installer on 64-bit OS, check System32
Result := not FileExists(ExpandConstant('{win}\System32\' + FileName))
else
// For 32-bit installer on 64-bit OS, check SysWOW64
Result := not FileExists(ExpandConstant('{win}\SysWOW64\' + FileName));
end
else else
// For 32-bit installer on 64-bit OS, check SysWOW64 // For 32-bit OS, always check System32
Result := not FileExists(ExpandConstant('{win}\SysWOW64\' + FileName)); Result := not FileExists(ExpandConstant('{win}\System32\' + FileName));
end end;
else
// For 32-bit OS, always check System32
Result := not FileExists(ExpandConstant('{win}\System32\' + FileName));
end; end;
@@ -464,6 +515,14 @@ begin
end; end;
function IsPRInstaller(): Boolean;
begin
// Skip Tasks page if this is a PR build
Result := Pos('-PR-', ExpandConstant('{#InstallerName}')) > 0;
end;
function InitializeSetup(): Boolean; function InitializeSetup(): Boolean;
var var
InstallPath: String; InstallPath: String;
@@ -578,7 +637,21 @@ end;
function ShouldSkipPage(PageID: Integer): Boolean; function ShouldSkipPage(PageID: Integer): Boolean;
begin begin
Result := False; // Default is not to skip the page Result := False; // Default is not to skip the page
// Don't skip Target page if this is a PR build and upgrade
if IsPRInstaller and IsUpgrade and (PageID = wpSelectDir) then
begin
Result := False;
Exit;
end;
// Skip Tasks page if this is a PR build
if IsPRInstaller and (PageID = wpSelectTasks) then
begin
Result := True;
Exit;
end;
if IsUpgrade then if IsUpgrade then
begin begin
if (PageID = wpLicense) or (PageID = wpSelectTasks) or (PageID = wpReady) then if (PageID = wpLicense) or (PageID = wpSelectTasks) or (PageID = wpReady) then
@@ -594,7 +667,6 @@ procedure CurPageChanged(CurPageID: Integer);
begin begin
// Ensure the footer message is visible on every page // Ensure the footer message is visible on every page
FooterLabel.Visible := True; FooterLabel.Visible := True;
end; end;
@@ -678,12 +750,42 @@ begin
end; end;
procedure CreateDefaultSettingsFile();
var
ConfigDir, SettingsFile, Language, JSONContent: String;
begin
ConfigDir := GlobalUserDocsFolder + '\' + '{#VCMIFilesFolder}' + '\config';
SettingsFile := ConfigDir + '\settings.json';
if not FileExists(SettingsFile) then
begin
Language := ActiveLanguage;
if Language = '' then
Language := 'english';
JSONContent :=
'{' + #13#10 +
Chr(9) + '"general" : {' + #13#10 +
Chr(9) + Chr(9) + '"language" : "' + Language + '"' + #13#10 +
Chr(9) + '}' + #13#10 +
'}';
if not DirExists(ConfigDir) then
ForceDirectories(ConfigDir);
SaveStringToFile(SettingsFile, JSONContent, False);
end;
end;
procedure RunPreInstallTasks(); procedure RunPreInstallTasks();
begin begin
// Remove Legacy installer when needed // Remove Legacy installer when needed
RemoveLegacyInstaller(); RemoveLegacyInstaller();
// Copy H3 files when needed // Copy H3 files when needed
PerformHeroes3FileCopy(); PerformHeroes3FileCopy();
// Create default language JSON - for future use
// CreateDefaultSettingsFile();
end; end;
@@ -863,3 +965,4 @@ begin
Abort; Abort;
end; end;
end; end;

View File

@@ -468,11 +468,15 @@ endif()
# Finding packages # # Finding packages #
############################################ ############################################
set(BOOST_COMPONENTS date_time filesystem locale program_options system) set(BOOST_COMPONENTS date_time filesystem locale program_options)
if(ENABLE_INNOEXTRACT) if(ENABLE_INNOEXTRACT)
list(APPEND BOOST_COMPONENTS iostreams) list(APPEND BOOST_COMPONENTS iostreams)
endif() endif()
find_package(Boost 1.48.0 REQUIRED COMPONENTS ${BOOST_COMPONENTS}) find_package(Boost 1.48.0 REQUIRED COMPONENTS ${BOOST_COMPONENTS})
if(Boost_MAJOR_VERSION EQUAL 1 AND Boost_MINOR_VERSION LESS 69)
list(APPEND BOOST_COMPONENTS system)
find_package(Boost 1.48.0 REQUIRED COMPONENTS ${BOOST_COMPONENTS})
endif()
find_package(ZLIB REQUIRED) find_package(ZLIB REQUIRED)
# Conan compatibility # Conan compatibility

View File

@@ -8,84 +8,87 @@ To use cheat code, press `Tab` key or click/tap on status bar to open game chat
### Spells ### Spells
`nwcthereisnospoon`, `nwcmidichlorians`, `nwctim`, `vcmiistari` or `vcmispells` - give a spell book, all spells and 999 mana to currently selected hero. Also allows casting spell up to 100 times per combat round - `nwcthereisnospoon`, `nwcmidichlorians`, `nwctim`, `vcmiistari` or `vcmispells` - give a spell book, all spells and 999 mana to currently selected hero. Also allows casting spell up to 100 times per combat round
### Secondary Skills ### Secondary Skills
`vcmiskill <skillID> <mastery>` - give a secondary skill to currently selected hero - `vcmiskill <skillID> <mastery>` - give a secondary skill to currently selected hero
Examples: Examples:
`vcmiskill learning` - give expert level learning skill
`vcmiskill leadership 2` - give advanced level leadership skill - `vcmiskill learning` - give expert level learning skill
`vcmiskill wisdom 0` - remove wisdom skill - `vcmiskill leadership 2` - give advanced level leadership skill
`vcmiskill every` - give all skills on expert level - `vcmiskill wisdom 0` - remove wisdom skill
`vcmiskill every 0` - remove all skills - `vcmiskill every` - give all skills on expert level
- `vcmiskill every 0` - remove all skills
### Army ### Army
`nwctrinity`, `nwcpadme`, `nwcavertingoureyes`, `vcmiainur` or `vcmiarchangel` - give 5 Archangels in every empty slot (to currently selected hero) - `nwctrinity`, `nwcpadme`, `nwcavertingoureyes`, `vcmiainur` or `vcmiarchangel` - give 5 Archangels in every empty slot (to currently selected hero)
`nwcagents`, `nwcdarthmaul`, `nwcfleshwound` or `vcmiangband` or `vcmiblackknight` - give 10 black knight in every empty slot - `nwcagents`, `nwcdarthmaul`, `nwcfleshwound` or `vcmiangband` or `vcmiblackknight` - give 10 black knight in every empty slot
`vcmiglaurung` or `vcmicrystal` - give 5000 crystal dragons in every empty slot - `vcmiglaurung` or `vcmicrystal` - give 5000 crystal dragons in every empty slot
`vcmiazure` - give 5000 azure dragons in every empty slot - `vcmiazure` - give 5000 azure dragons in every empty slot
`vcmifaerie` - give 5000 faerie dragons in every empty slot - `vcmifaerie` - give 5000 faerie dragons in every empty slot
- Alternative usage: `vcmiarmy <creatureID> <amount>`
Alternative usage: `vcmiarmy <creatureID> <amount>`
Gives specific creature in every slot, with optional amount. Examples: Gives specific creature in every slot, with optional amount. Examples:
`vcmiarmy imp` - give 5, 50, 500... 500k imps in every free slot
`vcmiarmy grandElf 100` - gives 100 grand elves in every free slot - `vcmiarmy imp` - give 5, 50, 500... 500k imps in every free slot
- `vcmiarmy grandElf 100` - gives 100 grand elves in every free slot
### Town buildings ### Town buildings
`nwczion`, `nwccoruscant`, `nwconlyamodel`, `vcmiarmenelos` or `vcmibuild` - build all buildings in currently selected town - `nwczion`, `nwccoruscant`, `nwconlyamodel`, `vcmiarmenelos` or `vcmibuild` - build all buildings in currently selected town
### Artifacts ### Artifacts
`nwclotsofguns`, `nwcr2d2`, `nwcantioch`, `vcminoldor` or `vcmimachines` - give ballista, ammo cart and first aid tent - `nwclotsofguns`, `nwcr2d2`, `nwcantioch`, `vcminoldor` or `vcmimachines` - give ballista, ammo cart and first aid tent
`vcmiforgeofnoldorking` or `vcmiartifacts` - give all artifacts, except spell book, spell scrolls and war machines. Artifacts added via mods included - `vcmiforgeofnoldorking` or `vcmiartifacts` - give all artifacts, except spell book, spell scrolls and war machines. Artifacts added via mods included
`vcmiscrolls` - give spell scrolls for every possible spells - `vcmiscrolls` - give spell scrolls for every possible spells
### Movement points ### Movement points
`nwcnebuchadnezzar`, `nwcpodracer`, `nwccoconuts`, `vcminahar` or `vcmimove` - give unlimited (or specified amount of) movement points and free ship boarding - `nwcnebuchadnezzar`, `nwcpodracer`, `nwccoconuts`, `vcminahar` or `vcmimove` - give unlimited (or specified amount of) movement points and free ship boarding
Alternative usage: `vcmimove <amount>` - gives specified amount of movement points - Alternative usage: `vcmimove <amount>` - gives specified amount of movement points
### Resources ### Resources
`nwctheconstruct`, `nwcwatto`, `nwcshrubbery`, `vcmiformenos` or `vcmiresources` - give resources (100000 gold, 100 of wood, ore and rare resources) - `nwctheconstruct`, `nwcwatto`, `nwcshrubbery`, `vcmiformenos` or `vcmiresources` - give resources (100000 gold, 100 of wood, ore and rare resources)
Alternative usage: `vcmiresources <amount>` - gives specified amount of all resources and x1000 of gold - Alternative usage: `vcmiresources <amount>` - gives specified amount of all resources and x1000 of gold
### Fog of War ### Fog of War
`nwcwhatisthematrix`, `nwcrevealourselves`, `nwcgeneraldirection`, `vcmieagles` or `vcmimap` - reveal Fog of War - `nwcwhatisthematrix`, `nwcrevealourselves`, `nwcgeneraldirection`, `vcmieagles` or `vcmimap` - reveal Fog of War
`nwcignoranceisbliss`, `vcmiungoliant` or `vcmihidemap` - conceal Fog of War - `nwcignoranceisbliss`, `vcmiungoliant` or `vcmihidemap` - conceal Fog of War
### Experience ### Experience
`nwcneo`, `nwcquigon`, `nwcigotbetter`, `vcmiglorfindel` or `vcmilevel` - advances currently selected hero to the next level - `nwcneo`, `nwcquigon`, `nwcigotbetter`, `vcmiglorfindel` or `vcmilevel` - advances currently selected hero to the next level
Alternative usage: `vcmilevel <amount>` - advances hero by specified number of levels - Alternative usage: `vcmilevel <amount>` - advances hero by specified number of levels
- `vcmiolorin` or `vcmiexp` - gives selected hero 10000 experience - `vcmiolorin` or `vcmiexp` - gives selected hero 10000 experience
Alternative usage: `vcmiexp <amount>` - gives selected hero specified amount of experience - Alternative usage: `vcmiexp <amount>` - gives selected hero specified amount of experience
### Luck and morale ### Luck and morale
`nwcfollowthewhiterabbit`, `nwccastleanthrax` or `vcmiluck` - the currently selected hero permanently gains maximum luck - `nwcfollowthewhiterabbit`, `nwccastleanthrax` or `vcmiluck` - the currently selected hero permanently gains maximum luck
`nwcmorpheus`, `nwcmuchrejoicing` or `vcmimorale` - the currently selected hero permanently gains maximum morale - `nwcmorpheus`, `nwcmuchrejoicing` or `vcmimorale` - the currently selected hero permanently gains maximum morale
### Puzzle map ### Puzzle map
`nwcoracle`, `nwcprophecy`, `nwcalreadygotone` or `vcmiobelisk` - reveals the puzzle map - `nwcoracle`, `nwcprophecy`, `nwcalreadygotone` or `vcmiobelisk` - reveals the puzzle map
### Finishing the game ### Finishing the game
`nwcredpill`, `nwctrojanrabbit`, `vcmisilmaril` or `vcmiwin` - player wins - `nwcredpill`, `nwctrojanrabbit`, `vcmisilmaril` or `vcmiwin` - player wins
`nwcbluepill`, `nwcsirrobin`, `vcmimelkor` or `vcmilose` - player loses - `nwcbluepill`, `nwcsirrobin`, `vcmimelkor` or `vcmilose` - player loses
### Misc ### Misc
`nwctheone` or `vcmigod` - reveals the whole map, gives 5 archangels in each empty slot, unlimited movement points and permanent flight - `nwctheone` or `vcmigod` - reveals the whole map, gives 5 archangels in each empty slot, unlimited movement points and permanent flight
`nwcphisherprice` or `vcmicolor` - change game color palette to Heroes II like until game restart - `nwcphisherprice` or `vcmicolor` - change game color palette to Heroes II like until game restart
`vcmigray` - change game color palette to grayscale until game restart - `vcmigray` - change game color palette to grayscale until game restart
## Using cheat codes on other players ## Using cheat codes on other players
@@ -97,10 +100,10 @@ By default, all cheat codes apply to current player. Alternatively, it is possib
### Examples ### Examples
`vcmieagles blue` - reveal FoW only for blue player - `vcmieagles blue` - reveal FoW only for blue player
`vcmieagles ai` - reveal FoW only for AI players - `vcmieagles ai` - reveal FoW only for AI players
`vcmieagles all` - reveal FoW for all players on map - `vcmieagles all` - reveal FoW for all players on map
`vcminahar ai` - give 1000000 movement points to each hero of every AI player - `vcminahar ai` - give 1000000 movement points to each hero of every AI player
## Multiplayer chat commands ## Multiplayer chat commands
@@ -116,10 +119,10 @@ Following commands can be used by any player in multiplayer:
- `!help` - displays in-game list of available commands - `!help` - displays in-game list of available commands
- `!cheaters` - lists players that have entered cheat at any point of the game - `!cheaters` - lists players that have entered cheat at any point of the game
- `!vote` - initiates voting to change one of the possible options: - `!vote` - initiates voting to change one of the possible options:
- - `!vote simturns allow X` - allow simultaneous turns for specified number of days, or until contact - `!vote simturns allow X` - allow simultaneous turns for specified number of days, or until contact
- - `!vote simturns force X` - force simultaneous turns for specified number of days, blocking player contacts - `!vote simturns force X` - force simultaneous turns for specified number of days, blocking player contacts
- - `!vote simturns abort` - abort simultaneous turns once this turn ends - `!vote simturns abort` - abort simultaneous turns once this turn ends
- - `!vote timer prolong X` - prolong base timer for all players by specified number of seconds - `!vote timer prolong X` - prolong base timer for all players by specified number of seconds
## Client Commands ## Client Commands

View File

@@ -6,12 +6,16 @@
- manually: <https://github.com/vcmi/vcmi/releases/latest> - manually: <https://github.com/vcmi/vcmi/releases/latest>
- via Homebrew: `brew install --cask --no-quarantine vcmi/vcmi/vcmi` - via Homebrew: `brew install --cask --no-quarantine vcmi/vcmi/vcmi`
- Daily builds (might be unstable) - Daily builds (might be unstable)
- Intel (x86_64) builds: <https://builds.vcmi.download/branch/develop/macOS/intel> - [Intel (x86_64) builds](https://builds.vcmi.download/branch/develop/macos-arm/)
- Apple Silicon (arm64) builds: <https://builds.vcmi.download/branch/develop/macOS/arm> - [Apple Silicon (arm64) builds](https://builds.vcmi.download/branch/develop/macos-intel/)
If the app doesn't open, right-click the app bundle - select *Open* menu item - press *Open* button. You may also need to allow running it in System Settings - Privacy & Security. If the app doesn't open, right-click the app bundle - select *Open* menu item - press *Open* button. On macOS 15 Sequoia and later there will be no *Open* button, instead you'll see the following dialog:
Please report about gameplay problem on forums: [Help & Bugs](https://forum.vcmi.eu/c/international-board/help-bugs) Make sure to specify what hardware and macOS version you use. ![open error on macOS 15](images/macos15-open.png)
To fix it, go to System Settings - Privacy & Security tab - scroll down and press the *Open Anyway* button (marked with an arrow on the image below). After that confirm your action and enter your administrator password.
![macOS 15 - Privacy & Security](images/macos15-privacy.png)
## Step 2: Installing Heroes III data files ## Step 2: Installing Heroes III data files

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 204 KiB

View File

@@ -791,7 +791,7 @@ bool CGameInfoCallback::isTeleportEntrancePassable(const CGTeleport * obj, Playe
return obj && obj->isEntrance() && !isTeleportChannelImpassable(obj->channel, player); return obj && obj->isEntrance() && !isTeleportChannelImpassable(obj->channel, player);
} }
void CGameInfoCallback::getFreeTiles(std::vector<int3> & tiles) const void CGameInfoCallback::getFreeTiles(std::vector<int3> & tiles, bool skipIfNearbyGuarded) const
{ {
std::vector<int> floors; std::vector<int> floors;
floors.reserve(gameState().getMap().levels()); floors.reserve(gameState().getMap().levels());
@@ -808,7 +808,21 @@ void CGameInfoCallback::getFreeTiles(std::vector<int3> & tiles) const
{ {
tinfo = getTile(int3 (xd,yd,zd)); tinfo = getTile(int3 (xd,yd,zd));
if (tinfo->isLand() && tinfo->getTerrain()->isPassable() && !tinfo->blocked()) //land and free if (tinfo->isLand() && tinfo->getTerrain()->isPassable() && !tinfo->blocked()) //land and free
{
// Ensure that CGameHandler::spawnWanderingMonsters won't set a random monster next to another monster
// because Nullkiller AI is not able to go to one monster without falling into the attack range of the nearby one
// See GraphPaths::addChainInfo if(node.linkDanger > 0) (no link between random monsters and map monsters)
// Ivan: When new monster spawns, AI should receive AIGateway::newObject call for each object visible to AI. Probably you need to invalidate
// that data for AI & force recalculation on next turn. New queries to gamestate, like getGuardingCreaturePosition that are done either
// from AIGateway::newObject method or at any point later should correctly include newly spawned monsters
// TODO: Ensure this linking issue is properly fixed, not just with the workaround below
if (skipIfNearbyGuarded && guardingCreaturePosition(int3 (xd,yd,zd)).isValid())
{
continue;
}
tiles.emplace_back(xd, yd, zd); tiles.emplace_back(xd, yd, zd);
}
} }
} }
} }

View File

@@ -102,7 +102,7 @@ public:
bool isTeleportEntrancePassable(const CGTeleport * obj, PlayerColor player) const override; bool isTeleportEntrancePassable(const CGTeleport * obj, PlayerColor player) const override;
//used for random spawns //used for random spawns
void getFreeTiles(std::vector<int3> &tiles) const; void getFreeTiles(std::vector<int3> &tiles, bool skipIfNearbyGuarded) const;
void getTilesInRange(FowTilesType & tiles, const int3 & pos, int radius, ETileVisibility mode, std::optional<PlayerColor> player = std::optional<PlayerColor>(), int3::EDistanceFormula formula = int3::DIST_2D) const override; void getTilesInRange(FowTilesType & tiles, const int3 & pos, int radius, ETileVisibility mode, std::optional<PlayerColor> player = std::optional<PlayerColor>(), int3::EDistanceFormula formula = int3::DIST_2D) const override;
void getAllTiles(FowTilesType &tiles, std::optional<PlayerColor> player, int level, const std::function<bool(const TerrainTile *)> & filter) const override; void getAllTiles(FowTilesType &tiles, std::optional<PlayerColor> player, int level, const std::function<bool(const TerrainTile *)> & filter) const override;

View File

@@ -976,8 +976,9 @@ CStackBasicDescriptor CGHeroInstance::calculateNecromancy (const BattleResult &b
if (improvedNecromancy->empty()) if (improvedNecromancy->empty())
return CStackBasicDescriptor(); return CStackBasicDescriptor();
bool hasRaisedUnitsBonus = hasBonusOfType(BonusType::UNDEAD_RAISE_PERCENTAGE);
int raisedUnitsPercentage = std::clamp(valOfBonuses(BonusType::UNDEAD_RAISE_PERCENTAGE), 0, 100); int raisedUnitsPercentage = std::clamp(valOfBonuses(BonusType::UNDEAD_RAISE_PERCENTAGE), 0, 100);
if (raisedUnitsPercentage == 0) if(raisedUnitsPercentage == 0 && !hasRaisedUnitsBonus)
return CStackBasicDescriptor(); return CStackBasicDescriptor();
const std::map<CreatureID,si32> &casualties = battleResult.casualties[CBattleInfoEssentials::otherSide(battleResult.winner)]; const std::map<CreatureID,si32> &casualties = battleResult.casualties[CBattleInfoEssentials::otherSide(battleResult.winner)];

View File

@@ -418,7 +418,8 @@ std::vector<CIdentifierStorage::ObjectData> CIdentifierStorage::getPossibleIdent
} }
std::string fullID = request.type + '.' + request.name; std::string fullID = request.type + '.' + request.name;
std::string fullIDCaseCorrected = request.caseSensitive ? fullID : registeredObjectsCaseLookup.at(boost::algorithm::to_lower_copy(fullID)); std::string fullLowerID = boost::algorithm::to_lower_copy(fullID);
std::string fullIDCaseCorrected = (request.caseSensitive || !registeredObjectsCaseLookup.count(fullLowerID)) ? fullID : registeredObjectsCaseLookup.at(fullLowerID);
auto entries = registeredObjects.equal_range(fullIDCaseCorrected); auto entries = registeredObjects.equal_range(fullIDCaseCorrected);

View File

@@ -3995,7 +3995,7 @@ void CGameHandler::spawnWanderingMonsters(CreatureID creatureID)
{ {
std::vector<int3>::iterator tile; std::vector<int3>::iterator tile;
std::vector<int3> tiles; std::vector<int3> tiles;
gameState().getFreeTiles(tiles); gameState().getFreeTiles(tiles, true);
ui32 amount = tiles.size() / 200; //Chance is 0.5% for each tile ui32 amount = tiles.size() / 200; //Chance is 0.5% for each tile
RandomGeneratorUtil::randomShuffle(tiles, getRandomGenerator()); RandomGeneratorUtil::randomShuffle(tiles, getRandomGenerator());

View File

@@ -447,52 +447,96 @@ bool BattleFlowProcessor::tryMakeAutomaticActionOfBallistaOrTowers(const CBattle
if ((stackCreatureId == CreatureID::ARROW_TOWERS || stackCreatureId == CreatureID::BALLISTA) if ((stackCreatureId == CreatureID::ARROW_TOWERS || stackCreatureId == CreatureID::BALLISTA)
&& (!curOwner || !gameHandler->randomizer->rollCombatAbility(curOwner->id, curOwner->valOfBonuses(BonusType::MANUAL_CONTROL, BonusSubtypeID(stackCreatureId))))) && (!curOwner || !gameHandler->randomizer->rollCombatAbility(curOwner->id, curOwner->valOfBonuses(BonusType::MANUAL_CONTROL, BonusSubtypeID(stackCreatureId)))))
{ {
BattleAction attack; BattleAction attack;
attack.actionType = EActionType::SHOOT; attack.actionType = EActionType::SHOOT;
attack.side = next->unitSide(); attack.side = next->unitSide();
attack.stackNumber = next->unitId(); attack.stackNumber = next->unitId();
// TODO: unify logic with AI? const TStacks possibleTargets = battle.battleGetStacksIf([&next, &battle](const CStack * s)
// Find best target using logic similar to H3 AI
const auto & isBetterTarget = [&battle](const battle::Unit * candidate, const battle::Unit * current)
{ {
bool candidateInsideWalls = battle.battleIsInsideWalls(candidate->getPosition()); return s->unitOwner() != next->unitOwner() && s->isValidTarget() && battle.battleCanShoot(next, s->getPosition());
bool currentInsideWalls = battle.battleIsInsideWalls(current->getPosition()); });
if (candidateInsideWalls != currentInsideWalls) struct TargetInfo
return candidateInsideWalls > currentInsideWalls; {
bool insideTheWalls;
// also check for war machines - shooters are more dangerous than war machines, ballista or catapult bool canAttackNextTurn;
bool candidateCanShoot = candidate->canShoot() && candidate->unitType()->warMachine == ArtifactID::NONE; bool isParalyzed;
bool currentCanShoot = current->canShoot() && current->unitType()->warMachine == ArtifactID::NONE; bool isMachine;
float towerAttackValue;
if (candidateCanShoot != currentCanShoot) const CStack * stack;
return candidateCanShoot > currentCanShoot;
int64_t candidateTargetValue = static_cast<int64_t>(candidate->unitType()->getAIValue() * candidate->getCount());
int64_t currentTargetValue = static_cast<int64_t>(current->unitType()->getAIValue() * current->getCount());
return candidateTargetValue > currentTargetValue;
}; };
const battle::Unit * target = nullptr; const auto & getCanAttackNextTurn = [&battle] (const battle::Unit * unit)
for(auto & elem : battle.battleGetAllStacks(true))
{ {
if (elem->unitOwner() == next->unitOwner()) if (battle.battleCanShoot(unit))
continue; return true;
if (!elem->isValidTarget()) BattleHexArray attackableHexes;
continue; battle.battleGetAvailableHexes(unit, true, false, &attackableHexes);
return !attackableHexes.empty();
};
if (!battle.battleCanShoot(next, elem->getPosition())) const auto & getTowerAttackValue = [&battle, &next] (const battle::Unit * unit)
continue; {
float unitValue = static_cast<float>(unit->unitType()->getAIValue());
float singleHpValue = unitValue / static_cast<float>(unit->getMaxHealth());
float fullHp = static_cast<float>(unit->getTotalHealth());
if (target && !isBetterTarget(elem, target)) int distance = BattleHex::getDistance(next->getPosition(), unit->getPosition());
continue; BattleAttackInfo attackInfo(next, unit, distance, true);
DamageEstimation estimation = battle.calculateDmgRange(attackInfo);
float avgDmg = (static_cast<float>(estimation.damage.max) + static_cast<float>(estimation.damage.min)) / 2;
float realAvgDmg = avgDmg > fullHp ? fullHp : avgDmg;
float avgUnitKilled = (static_cast<float>(estimation.kills.max) + static_cast<float>(estimation.kills.min)) / 2;
float dmgValue = realAvgDmg * singleHpValue;
float killValue = avgUnitKilled * unitValue;
target = elem; return dmgValue + killValue;
};
std::vector<TargetInfo>targetsInfo;
for (const CStack * possibleTarget : possibleTargets)
{
bool isMachine = possibleTarget->unitType()->warMachine != ArtifactID::NONE;
bool isParalyzed = possibleTarget->hasBonusOfType(BonusType::NOT_ACTIVE) && !isMachine;
const TargetInfo targetInfo =
{
battle.battleIsInsideWalls(possibleTarget->getPosition()),
getCanAttackNextTurn(possibleTarget),
isParalyzed,
isMachine,
getTowerAttackValue(possibleTarget),
possibleTarget
};
targetsInfo.push_back(targetInfo);
}
const auto & isBetterTarget = [](const TargetInfo & candidate, const TargetInfo & current)
{
if (candidate.isParalyzed != current.isParalyzed)
return candidate.isParalyzed < current.isParalyzed;
if (candidate.isMachine != current.isMachine)
return candidate.isMachine < current.isMachine;
if (candidate.canAttackNextTurn != current.canAttackNextTurn)
return candidate.canAttackNextTurn > current.canAttackNextTurn;
if (candidate.insideTheWalls != current.insideTheWalls)
return candidate.insideTheWalls > current.insideTheWalls;
return candidate.towerAttackValue > current.towerAttackValue;
};
const TargetInfo * target = nullptr;
for(const auto & elem : targetsInfo)
{
if (target == nullptr || isBetterTarget(elem, *target))
target = &elem;
} }
if(target == nullptr) if(target == nullptr)
@@ -501,7 +545,7 @@ bool BattleFlowProcessor::tryMakeAutomaticActionOfBallistaOrTowers(const CBattle
} }
else else
{ {
attack.aimToUnit(target); attack.aimToUnit(target->stack);
makeAutomaticAction(battle, next, attack); makeAutomaticAction(battle, next, attack);
} }
return true; return true;