diff --git a/.gitignore b/.gitignore index 6fe7df2d..867d5fe7 100644 --- a/.gitignore +++ b/.gitignore @@ -330,3 +330,51 @@ ASALocalRun/ # MFractors (Xamarin productivity tool) working folder .mfractor/ + +# LaTeX +*.acn +*.acr +*.alg +*.aux +*.bak +*.bbl +*.bcf +*.blg +*.brf +*.bst +*.dvi +*.fdb_latexmk +*.fls +*.glg +*.glo +*.gls +*.idx +*.ilg +*.ind +*.ist +*.lof +*.log +*.lol +*.lot +*.maf +*.mtc +*.mtc1 +*.nav +*.nlo +*.nls +*.out +*.pdf +*.pyg +*.run.xml +*.snm +*.synctex.gz +*.tex.backup +*.tex~ +*.thm +*.toc +*.vrb +*.xdy +*.xml +*blx.bib +.bak +.mtc diff --git a/ApiDocs/en/apiReference.tex b/ApiDocs/en/apiReference.tex new file mode 100644 index 00000000..38147006 --- /dev/null +++ b/ApiDocs/en/apiReference.tex @@ -0,0 +1,7 @@ +\section{API reference} + +\input{standaloneFuncs.tex} +\newpage +\input{enumerations.tex} +\newpage +\input{types.tex} diff --git a/ApiDocs/en/attack.tex b/ApiDocs/en/attack.tex new file mode 100644 index 00000000..75571b11 --- /dev/null +++ b/ApiDocs/en/attack.tex @@ -0,0 +1,49 @@ +\subsection{Attack} +\label{Attack} +Represents attack of \hyperref[UnitImpl]{unit implementation}. +\subsubsection{Properties} +\begin{center} +\begin{tabularx}{\linewidth}{| l | X |} +\hline +\textbf{Name} & \textbf{Description} \\ +\hline +id & Returns attack \hyperref[Id]{id}. This is different for every \hyperref[DynamicUpgrade]{dynamic upgrade} unit gets\\ +\hline +type & Returns attack \hyperref[AttackCategory]{type}\\ +\hline +source & Returns attack \hyperref[SourceCategory]{source}\\ +\hline +reach & Returns attack \hyperref[ReachCategory]{reach}\\ +\hline +initiative & Returns attack initiative value\\ +\hline +power & Returns attack power (accuracy)\\ +\hline +damage & Returns damage the attack can inflict. Damage depends on attack type\\ +\hline +heal & Returns healing the attack can apply. Healing depends on attack type\\ +\hline +infinite & Returns true if attack has long effect duration. Effect depends on attack type\\ +\hline +crit & Returns true if attack can inflict critical damage\\ +\hline +level & Returns level for boost damage, lower damage and lower initiative attacks\\ +\hline +melee & Returns true if attack is melee (\texttt{L\_ADJACENT} or custom reach marked as \texttt{MELEE} in \texttt{LAttR.dbf})\\ +\hline +maxTargets & Returns maximum number of targets (1, 6 or \texttt{MAX\_TARGTS} value for custom reach in \texttt{LAttR.dbf})\\ +\hline +critDamage & Returns critical damage percent \texttt{[0 : 255]}\\ +\hline +critPower & Returns critical damage chance \texttt{[0 : 100]}\\ +\hline +damageRatio & Returns damage ratio \texttt{[0 : 255]} for additional targets\\ +\hline +damageRatioPerTarget & Returns true if damage ratio reapplied for each consecutive target\\ +\hline +damageSplit & Returns true if damage is split among targets\\ +\hline +wards & Returns array of \hyperref[Modifier]{modifiers} applied by bestow wards attack\\ +\hline +\end{tabularx} +\end{center} \ No newline at end of file diff --git a/ApiDocs/en/battle.tex b/ApiDocs/en/battle.tex new file mode 100644 index 00000000..ec3c3051 --- /dev/null +++ b/ApiDocs/en/battle.tex @@ -0,0 +1,87 @@ +\subsection{Battle} +\label{Battle} +Represents battle state. +\subsubsection{Properties} +\begin{center} +\begin{tabularx}{\linewidth}{| l | X |} +\hline +currentRound & Returns current round in battle. Round counting starts from 1, but there is a special round 0 when units with `Doppelganger' \hyperref[Attack]{attacks} present\\ +\hline +autoBattle & Returns \texttt{true} if autobattle mode is turned on\\ +\hline +fastBattle & Returns \texttt{true} if fast battle is turned on. When fast battle is active, auto battle is active too\\ +\hline +attackerPlayer & Returns \hyperref[Player]{player} that started battle or \texttt{nil} if not found\\ +\hline +defenderPlayer & Returns \hyperref[Player]{player} that was attacked or \texttt{nil} if not found\\ +\hline +attacker & Returns \hyperref[Stack]{stack} that started battle. Only stacks can initiate battles\\ +\hline +defender & Returns \hyperref[Group]{group} that was attacked. Defender group can represent units of a \hyperref[Stack]{stack}, \hyperref[Fort]{fort} or \hyperref[Ruin]{ruin}. Use \texttt{group.id.type} to get an actual type of a group\\ +\hline +decidedToRetreat & Returns \texttt{true} if decision about groups retreat was made and should not be reconsidered\\ +\hline +afterBattle & Returns \texttt{true} if battle is over but healers can make one more turn to heal allies\\ +\hline +duel & Returns \texttt{true} if battle is a duel between thief and a stack leader. All units except leaders are marked with \texttt{Hidden} \hyperref[BattleStatus]{battle status}\\ +\hline +turnsOrder & Returns list of \hyperref[BattleTurn]{battle turns} remaining in the current round of battle. Position of elements in the list corresponds to order of remaining turns in current round. In other words, at the start of a round, battle turns in the list are sorted according to units initiative, including an additional random initiative\\ +\hline +\end{tabularx} +\end{center} + +\subsubsection{Methods} +\begin{center} +\begin{tabularx}{\linewidth}{| l | X |} +\hline +getUnitStatus(id, BattleStatus.Defend) & Returns \texttt{true} if a unit with a specified \hyperref[Id]{id} has a specified \hyperref[BattleStatus]{battle status}\\ +\hline +isUnitAttacker(unit) & Returns \texttt{true} if \hyperref[Unit]{unit} belongs to attacker group.\\ +isUnitAttacker(id) & Method also accepts unit \hyperref[Id]{ids}\\ +\hline +getUnitActions(unit) & Returns possible actions and attack options for specified \hyperref[Unit]{unit}.\\ +getUnitActions(id) & Method also accepts unit \hyperref[Id]{ids}\\ +\hline +getRetreatStatus(true) & Returns retreat status of attacker (\texttt{true}) or defender (\texttt{false}) group\\ +getRetreatStatus(false) &\\ +\hline +isUnitRevived(unit) & Returns \texttt{true} if specified \hyperref[Unit]{unit} was revived during battle.\\ +isUnitRevived(id) & Method also accepts unit \hyperref[Id]{ids}\\ +\hline +isUnitWaiting(unit) & Returns \texttt{true} if specified \hyperref[Unit]{unit} skipped its turn and waiting.\\ +isUnitWaiting(id) & Method also accepts unit \hyperref[Id]{ids}\\ +\hline +getUnitShatteredArmor(unit) & Returns amount of \hyperref[Unit]{unit} armor shattered in battle so far.\\ +getUnitShatteredArmor(id) & Method also accepts unit \hyperref[Id]{ids}\\ +\hline +getUnitFortificationArmor(unit) & Returns armor that is granted to \hyperref[Unit]{unit} by fortification, if any.\\ +getUnitFortificationArmor(id) & Method also accepts unit \hyperref[Id]{ids}\\ +\hline +isUnitResistantToClass(unit, attackClass) & Returns true if specified \hyperref[Unit]{unit} is resistant to \hyperref[AttackCategory]{attack class}.\\ +isUnitResistantToClass(id, attackClass) & Method also accepts unit \hyperref[Id]{ids}\\ +\hline +isUnitResistantToSource(unit, attackSource) & Returns true if specified \hyperref[Unit]{unit} is resistant to \hyperref[SourceCategory]{attack source}.\\ +isUnitResistantToSource(id, attackSource) & Method also accepts unit \hyperref[Id]{ids}\\ +\hline +getUnitDisableRound(unit) & Returns round when paralyze, petrify or fear was applied to \hyperref[Unit]{unit}. Returns 0 if unit is not disabled.\\ +getUnitDisableRound(id) & Method also accepts unit \hyperref[Id]{ids}\\ +\hline +getUnitPoisonRound(unit) & Returns round when long poison was applied to \hyperref[Unit]{unit}. Returns 0 if unit is not poisoned.\\ +getUnitPoisonRound(id) & Method also accepts unit \hyperref[Id]{ids}\\ +\hline +getUnitFrostbiteRound(unit) & Returns round when long frostbite was applied to \hyperref[Unit]{unit}. Returns 0 if unit is not frozen.\\ +getUnitFrostbiteRound(id) & Method also accepts unit \hyperref[Id]{ids}\\ +\hline +getUnitBlisterRound(unit) & Returns round when long blister was applied to \hyperref[Unit]{unit}. Returns 0 if unit is not burning.\\ +getUnitBlisterRound(id) & Method also accepts unit \hyperref[Id]{ids}\\ +\hline +getUnitTransformRound(unit) & Returns round when long transform was applied to \hyperref[Unit]{unit}. Returns 0 if unit is not transformed.\\ +getUnitTransformRound(id) & Method also accepts unit \hyperref[Id]{ids}\\ +\hline +setRetreatStatus(true, Retreat.FullRetreat) & Sets \hyperref[Retreat]{retreat status} of attacker (\texttt{true}) or defender (\texttt{false}) group. This method can be only used in AI battle action script\\ +setRetreatStatus(false, Retreat.NoRetreat) &\\ +\hline +setDecidedToRetreat() & Notifies battle state that the decision about groups retreat was made and it is final. This method can be only used in AI battle action script\\ +\hline +\end{tabularx} +\end{center} \ No newline at end of file diff --git a/ApiDocs/en/battleturn.tex b/ApiDocs/en/battleturn.tex new file mode 100644 index 00000000..5b1d891b --- /dev/null +++ b/ApiDocs/en/battleturn.tex @@ -0,0 +1,15 @@ +\subsection{Battle turn} +\label{BattleTurn} +Represents \hyperref[Unit]{unit} action inside battle round\\ +\subsubsection{Properties} +\begin{center} +\begin{tabularx}{\linewidth}{| l | X |} +\hline +\textbf{Name} & \textbf{Description} \\ +\hline +unit & Returns \hyperref[Unit]{unit} that performs the turn\\ +\hline +attackCount & Returns number of attacks unit can perform in its turn. Units with double attack (\texttt{turn.unit.impl.attacksTwice}) will have 2 in attackCount\\ +\hline +\end{tabularx} +\end{center} \ No newline at end of file diff --git a/ApiDocs/en/building.tex b/ApiDocs/en/building.tex new file mode 100644 index 00000000..4a523b6d --- /dev/null +++ b/ApiDocs/en/building.tex @@ -0,0 +1,18 @@ +\subsection{Building} +\label{Building} +Represents building in \hyperref[Player]{player's} capital. +\subsubsection{Properties} +\begin{center} +\begin{tabularx}{\linewidth}{| l | X |} +\hline +\textbf{Name} & \textbf{Description} \\ +\hline +id & Returns building \hyperref[Id]{id}. \texttt{BUILD\_ID} value from \texttt{GBuild.dbf}\\ +cost & Returns building \hyperref[Currency]{cost}. \texttt{COST} value from \texttt{GBuild.dbf}\\ +type & Returns building \hyperref[BuildingCategory]{type}. \texttt{CATEGORY} value from \texttt{GBuild.dbf}\\ +requiredBuilding & Returns \hyperref[Building]{building} that is required to build this one or \texttt{nil} if building has no requirements. \texttt{REQUIRED} value from \texttt{GBuild.dbf}\\ +branch & Returns unit \hyperref[UnitBranch]{branch} or -1 if building type is not \texttt{Unit}\\ +level & Returns building level or 0 if building type is not \texttt{Unit}\\ +\hline +\end{tabularx} +\end{center} diff --git a/ApiDocs/en/crystal.tex b/ApiDocs/en/crystal.tex new file mode 100644 index 00000000..5fbe248f --- /dev/null +++ b/ApiDocs/en/crystal.tex @@ -0,0 +1,17 @@ +\subsection{Crystal} +\label{Crystal} +Represents gold mine or mana source on a map. +\subsubsection{Properties} +\begin{center} +\begin{tabularx}{\linewidth}{| l | X |} +\hline +\textbf{Name} & \textbf{Description} \\ +\hline +id & Returns crystal \hyperref[Id]{id}. The value is unique for every crystal on scenario map\\ +\hline +position & Returns crystal position as a \hyperref[Point]{point}\\ +\hline +resource & Returns crystal \hyperref[Resource]{resource type}\\ +\hline +\end{tabularx} +\end{center} \ No newline at end of file diff --git a/ApiDocs/en/currency.tex b/ApiDocs/en/currency.tex new file mode 100644 index 00000000..bb11da09 --- /dev/null +++ b/ApiDocs/en/currency.tex @@ -0,0 +1,34 @@ +\subsection{Currency} +\label{Currency} +Represents game currency, mana and gold united. +\subsubsection{Properties} +\begin{center} +\begin{tabularx}{\linewidth}{| l | X |} +\hline +\textbf{Name} & \textbf{Description} \\ +\hline +gold & Returns or sets amount of gold\\ +\hline +infernalMana & Returns or sets amount of infernal mana\\ +\hline +lifeMana & Returns or sets amount of life mana\\ +\hline +deathMana & Returns or sets amount of death mana\\ +\hline +runicMana & Returns or sets amount of runic mana\\ +\hline +groveMana & Returns or sets amount of grove mana\\ +\hline +\end{tabularx} +\end{center} + +\subsubsection{Methods} +\begin{center} +\begin{tabularx}{\linewidth}{| l | X |} +\hline +\textbf{Name} & \textbf{Description} \\ +\hline +Currency.new(existing) & Creates new currency from existing object\\ +\hline +\end{tabularx} +\end{center} \ No newline at end of file diff --git a/ApiDocs/en/customModifiersExample.tex b/ApiDocs/en/customModifiersExample.tex new file mode 100644 index 00000000..24b3ec7d --- /dev/null +++ b/ApiDocs/en/customModifiersExample.tex @@ -0,0 +1,88 @@ +\subsection{Custom modifiers} +Modifiers in the game stack on top of each other, making ordered modifiers chain. +First modifier takes base values from the unit it modifies, next - values modified by the first modifier and so on. +Custom modifiers allow you to modify every stat of unit and its attacks in a single script file with a number of uniform functions.\\ +Most of the custom modifier functions have the following form: +\begin{center} +\begin{lstlisting}[language=Lua] +function getSomething(unit, prev) + if someCondition then + return modifiedValue + end + + return prev +end +\end{lstlisting} +\end{center} +\begin{itemize} +\item \texttt{unit} has type \hyperref[Unit]{Unit}. The unit is presented in a state before the current modifier is applied. +\item \texttt{prev} is a previous value of the stat. It is either a base value or a value modified by the previous modifier. +\end{itemize} +Check \href{https://github.com/VladimirMakeev/D2ModdingToolset/tree/master/Scripts/Modifiers}{Scripts/Modifiers} for script examples.\\ +\href{https://github.com/VladimirMakeev/D2ModdingToolset/blob/master/Scripts/Modifiers/template.lua}{template.lua} contains a complete list of available functions.\\ + +\textbf{Due to how modifiers chain work, you have no direct access to final unit stats}\\ +For example: +\begin{itemize} +\item Lets say we have a unit with base of 50 initiative; +\item Then we give it a potion that increases damage by 50\% \textbf{if unit initiative} \texttt{> 60}; +\item Then we give it another potion that increases initiative to 70. +\end{itemize} +The damage bonus \textbf{will not work} in this case, even though a final unit initiative is 70 that is \texttt{> 60}. This is because \textbf{damage modifier applied earlier than initiative modifier}. +If we get \texttt{unit.impl.attack1.initiative} from the damage modifier script it will be 50, because \textbf{initiative modifier is not applied yet}.\\ + +\textbf{Dangerous way around to get final unit stats}\\ +The limitation described above \textbf{can be avoided} by getting unit instance via \texttt{getScenario:getUnit(unit.id)}.\\ +But you have to be \textbf{very careful} using this approach, as you can easily fall into a deadloop.\\ + +Lets see a \textbf{bad example} where we created a modifier that grants bonus armor and regen depending on each other: +\begin{center} +\begin{lstlisting}[language=Lua] +function getArmor(unit, prev) + local finalUnit = getScenario():getUnit(unit.id) + return prev + finalUnit.impl.regen / 5 +end + +function getRegen(unit, prev) + local finalUnit = getScenario():getUnit(unit.id) + return prev + finalUnit.impl.armor / 10 +end +\end{lstlisting} +\end{center} +Or it can be two different modifiers, does not matter: +\begin{center} +\begin{lstlisting}[language=Lua] +-- MyBonusArmorMod.lua +function getArmor(unit, prev) + local finalUnit = getScenario():getUnit(unit.id) + return prev + finalUnit.impl.regen / 5 +end +\end{lstlisting} +\end{center} +When this modifier(s) applied to a unit, we are getting circular dependence here: \textbf{final armor depends on final regen while final regen depends on final armor}.\\ +Imagine what happens when the game tries to get unit armor:\\ +It calls \texttt{getArmor} that calls \texttt{getRegen} that calls \texttt{getArmor} that calls \texttt{getRegen} that calls \texttt{getArmor} that calls \texttt{getRegen} that calls \texttt{getArmor}... and so on until your \textbf{game hang or crash to desktop}.\\ + +As a \textbf{good example}, you could refer to a third stat, thus avoiding deadloop condition: +\begin{center} +\begin{lstlisting}[language=Lua] +-- MyBonusArmorMod.lua +function getArmor(unit, prev) + local finalUnit = getScenario():getUnit(unit.id) + return prev + finalUnit.impl.regen / 5 +end +\end{lstlisting} +\end{center} +and +\begin{center} +\begin{lstlisting}[language=Lua] +-- MyBonusRegenMod.lua +function getRegen(unit, prev) + local finalUnit = getScenario():getUnit(unit.id) + return prev + finalUnit.impl.level / 10 +end +\end{lstlisting} +\end{center} +This way, regen depends on level, and armor depends on regen and there is no circular dependence in this case.\\ + +\textbf{Remember that this is subject for all modifiers that can potentially happen to be applied to the same unit}. \ No newline at end of file diff --git a/ApiDocs/en/customReachExample.tex b/ApiDocs/en/customReachExample.tex new file mode 100644 index 00000000..58f75e74 --- /dev/null +++ b/ApiDocs/en/customReachExample.tex @@ -0,0 +1,35 @@ +\subsection{Targeting scripts (custom attack reaches, specified via LAttR.dbf)} +Targeting scripts are used to specify either selection or attack targets of custom attack reach: +\begin{itemize} +\item \textbf{Selection} targets are targets that can be \textbf{selected (clicked)} (specified as \texttt{SEL\_SCRIPT} in \texttt{LAttR.dbf}); +\item \textbf{Attack} targets are targets that will be \textbf{affected by attack} (specified as \texttt{ATT\_SCRIPT} in \texttt{LAttR.dbf}). For instance, in case of 'pierce` attack, you can only click adjacent targets, but the attack will not only affect the selected target but also the one behind it (if any). Thus the 'pierce` attack uses \texttt{getAdjacentTargets.lua} \textbf{as selection} script and \texttt{getSelectedTargetAndOneBehindIt.lua} as attack script. +\end{itemize} +Targeting scripts use uniform \texttt{getTargets} function for both selection and attack scripts with the following arguments: +\begin{itemize} +\item \texttt{attacker} is the \hyperref[UnitSlot]{unit slot} of the attacker unit; +\item \texttt{selected} is the \hyperref[UnitSlot]{unit slot} of the unit that was selected (clicked).\\ +\texttt{selected.position == -1} and \texttt{selected.unit == nil} if this is a selection script (no target is clicked yet); +\item \texttt{allies} are \hyperref[UnitSlot]{unit slots} of all the allies on the battlefield (excluding the attacker); +\item \texttt{targets} are \hyperref[UnitSlot]{unit slots} of all the targets on the battlefield on which the attack can be performed. For instance, if targets are allies and the attack is Revive, then it will only include dead allies that can be revived; +\item \texttt{targetsAreAllies} specified whether targets are allies; +\item \texttt{item} specifies an \hyperref[Item]{item} (orb or talisman) used to perform the attack, or \texttt{nil} if no item is used; +\item \texttt{battle} specifies an information about current \hyperref[Battle]{battle}; +\item \texttt{isMarking} specified whether the script is being called to mark targets visually on the battlefield. Can be used to provide consistent visual representation for randomized scripts, as soft alternative to \texttt{MRK\_TARGTS} flag in \texttt{LAttR.dbf}. Always \texttt{false} if this is a selection script. +\end{itemize} +Example of attack script of pierce attack (\texttt{getSelectedTargetAndOneBehindIt.lua}): +\begin{center} +\begin{lstlisting}[language=Lua] +function getTargets(attacker, selected, allies, targets, targetsAreAllies, item, battle, isMarking) + -- Get the selected target and the one behind it (pierce attack) + local result = {selected} + for i = 1, #targets do + local target = targets[i] + if target.backline and target.position == selected.position + 1 then + table.insert(result, target) + break + end + end + return result +end +\end{lstlisting} +\end{center} \ No newline at end of file diff --git a/ApiDocs/en/diplomacy.tex b/ApiDocs/en/diplomacy.tex new file mode 100644 index 00000000..b78bfceb --- /dev/null +++ b/ApiDocs/en/diplomacy.tex @@ -0,0 +1,31 @@ +\subsection{Diplomacy} +\label{Diplomacy} +Represents diplomacy relations between \hyperref[RaceCategory]{races} in \hyperref[Scenario]{scenario}. +\subsubsection{Methods} +\begin{center} +\begin{tabularx}{\linewidth}{| l | X |} +\hline +\textbf{Name} & \textbf{Description} \\ +\hline +getCurrentRelation(race1, race2) & Returns current diplomacy relations value between two \hyperref[RaceCategory]{races} in range \texttt{[0 : 100]}\\ +\hline +getPreviousRelation(race1, race2) & Returns previous diplomacy relations value between two \hyperref[RaceCategory]{races} in range \texttt{[0 : 100]}\\ +\hline +getAlliance(race1, race2) & Returns \texttt{true} if two \hyperref[RaceCategory]{races} are in alliance\\ +\hline +getAllianceTurn(race1, race2) & Returns turn number when two \hyperref[RaceCategory]{races} made an alliance. Returns zero if races are not in alliance\\ +\hline +getAlwaysAtWar(race1, race2) & Returns \texttt{true} if two \hyperref[RaceCategory]{races} are always at war\\ +\hline +getAiCouldNotBreakAlliance(race1, race2) & Returns \texttt{true} if diplomacy relations prohibit AI-controlled \hyperref[RaceCategory]{races} from breaking alliance\\ +\hline +getRelationType(value) & Returns \hyperref[Relation]{relation type} according to diplomacy relations value\\ +\hline +\end{tabularx} +\end{center} +\paragraph{Relations value to type mapping} +\begin{itemize} +\item \texttt{[0 : D\_WAR]} - \texttt{Relation.War} +\item \texttt{[D\_WAR : D\_NEUTRAL]} - \texttt{Relation.Neutral} +\item \texttt{[D\_NEUTRAL : 100]} - \texttt{Relation.Peace} +\end{itemize} diff --git a/ApiDocs/en/docs.tex b/ApiDocs/en/docs.tex new file mode 100644 index 00000000..616bf217 --- /dev/null +++ b/ApiDocs/en/docs.tex @@ -0,0 +1,102 @@ +\documentclass[10pt]{article} +\usepackage[T1]{fontenc} +\usepackage[utf8]{inputenc} +\usepackage[english]{babel} +%\usepackage{xcolor} +\usepackage{listings} +\usepackage{hyperref} +\usepackage{float} +\usepackage{fancyhdr} +\usepackage{graphicx} +\usepackage{textcmds} +\usepackage[table]{xcolor} +\usepackage{tabularx} + +\definecolor{dkgreen}{rgb}{0,0.6,0} +\definecolor{lghtgray}{rgb}{0.97,0.97,0.97} +\definecolor{lghtgray2}{rgb}{0.94,0.94,0.94} +\definecolor{gray}{rgb}{0.5,0.5,0.5} +\definecolor{kwblue}{rgb}{0.2,0.6,1.0} + +\lstdefinestyle{lua}{ + defaultdialect=[5.0]Lua, + deletekeywords={type}, + frame=single, + language=Lua, + aboveskip=3mm, + belowskip=3mm, + showstringspaces=false, + columns=flexible, + backgroundcolor=\color{lghtgray}, + basicstyle={\small\ttfamily}, + numbers=left, + numberstyle=\footnotesize\color{black}, + keywordstyle=\color{kwblue}\bfseries, + commentstyle=\color{dkgreen}, + stringstyle=\color{brown}, + breaklines=true, + breakatwhitespace=true, + tabsize=4, + extendedchars=false, + inputencoding=utf8 +} + +\lstset{style=lua} + +\usepackage{geometry} + \geometry{ + a4paper, + total={170mm,257mm}, + left=20mm, + top=20mm, + } + +\usepackage{hyperref} +\hypersetup{ + colorlinks=true, + linkcolor=teal, + urlcolor=blue, + } + + +% In case you want watermark on each page +%\usepackage{draftwatermark} +%\SetWatermarkText{Draft} +%\SetWatermarkScale{2} +%\SetWatermarkColor{red} + + +\author{mak} +\title{Lua API for D2 modding toolset\\Documentation for version 0.15} + +\begin{document} +\pagestyle{fancy} + % clear all header fields +\fancyhead{} + % clear all footer fields +\fancyfoot{} +% page number in the center for even and odd pages +\fancyfoot[CE,CO]{\thepage} +\fancyhead[RO,RE]{\textcolor{gray}{D2 modding toolset 0.15}} + +\maketitle +\newpage +\tableofcontents +\newpage +\listoffigures + +% Overview +\include{overview.tex} +% API reference +\include{apiReference.tex} +% Examples +\include{examples.tex} +% Targeting scripts +%\include{targetingScripts.tex} +% Event condition examples +%\include{eventConditions.tex} +% Custom modifiers +%\include{customModifiers.tex} + + +\end{document} \ No newline at end of file diff --git a/ApiDocs/en/doppelgangerExample.tex b/ApiDocs/en/doppelgangerExample.tex new file mode 100644 index 00000000..36365912 --- /dev/null +++ b/ApiDocs/en/doppelgangerExample.tex @@ -0,0 +1,23 @@ +\subsection{doppelganger.lua} +\texttt{doppelganger} and \texttt{target} have type \hyperref[Unit]{Unit}. \texttt{item} is \hyperref[Item]{Item} used to perform the attack. \texttt{battle} specifies information about \hyperref[Battle]{battle} state. +\begin{center} +\begin{lstlisting}[language=Lua] +function getLevel(doppelganger, target, item, battle) + -- Get current doppelganger implementation + local impl = doppelganger.impl + -- Get target unit implementation + local targImpl = target.impl + + -- Get least level value from both + local level = math.min(impl.level, targImpl.level) + + -- Make sure doppelganger transform level is not lesser than target's base + local baseImpl = target.baseImpl + if level < baseImpl.level then + level = baseImpl.level + end + + return level +end +\end{lstlisting} +\end{center} \ No newline at end of file diff --git a/ApiDocs/en/drainLevelExample.tex b/ApiDocs/en/drainLevelExample.tex new file mode 100644 index 00000000..f06ddb71 --- /dev/null +++ b/ApiDocs/en/drainLevelExample.tex @@ -0,0 +1,10 @@ +\subsection{drainLevel.lua} +\texttt{attacker} and \texttt{target} have type \hyperref[Unit]{Unit}. \texttt{item} is \hyperref[Item]{Item} used to perform the attack. \texttt{battle} specifies information about \hyperref[Battle]{battle} state. +\begin{center} +\begin{lstlisting}[language=Lua] +function getLevel(attacker, target, item, battle) + -- transform into unit with its level minus 1 and minus attacker over-level + return math.max(1, target.impl.level - 1 - attacker.impl.level + attacker.baseImpl.level); +end +\end{lstlisting} +\end{center} \ No newline at end of file diff --git a/ApiDocs/en/dynamicupgrade.tex b/ApiDocs/en/dynamicupgrade.tex new file mode 100644 index 00000000..3c934a2c --- /dev/null +++ b/ApiDocs/en/dynamicupgrade.tex @@ -0,0 +1,13 @@ +\subsection{Dynamic upgrade} +\label{DynamicUpgrade} +Represents rules that applied when unit makes its progress gaining levels. Records in \texttt{GDynUpgr.dbf} are dynamic upgrades. +\subsubsection{Properties} +\begin{center} +\begin{tabularx}{\linewidth}{| l | X |} +\hline +\textbf{Name} & \textbf{Description} \\ +\hline +xpNext & Returns number of experience points added with each dynamic upgrade. \texttt{XP\_NEXT} value from \texttt{GDynUpgr.dbf}\\ +\hline +\end{tabularx} +\end{center} \ No newline at end of file diff --git a/ApiDocs/en/enumerations.tex b/ApiDocs/en/enumerations.tex new file mode 100644 index 00000000..ce58663a --- /dev/null +++ b/ApiDocs/en/enumerations.tex @@ -0,0 +1,773 @@ +\subsection{Enumerations} +\subsubsection{Race} +\label{RaceCategory} +Race categories correspond to the contents of \texttt{LRace.dbf} and can be accessed using table \texttt{Race}. +By default, table contains following races:\\ +\begin{tabularx}{\linewidth}{| l | X |} +\hline +\textbf{Name} & \textbf{Description} \\ +\hline +Race.Human & Empire\\ +\hline +Race.Undead & Undead Hordes\\ +\hline +Race.Heretic & Legions of the Damned\\ +\hline +Race.Dwarf & Mountain Clans\\ +\hline +Race.Neutral & Neutrals\\ +\hline +Race.Elf & Elven Alliance\\ +\hline +\end{tabularx} + +\subsubsection{Subrace} +\label{SubraceCategory} +Subrace categories correspond to the contents of \texttt{LSubRace.dbf} and can be accessed using table \texttt{Subrace}. +By default, table contains following subraces:\\ +\begin{tabularx}{\linewidth}{| l | X |} +\hline +\textbf{Name} & \textbf{Description} \\ +\hline +Subrace.Custom &\\ +\hline +Subrace.Human &\\ +\hline +Subrace.Undead &\\ +\hline +Subrace.Heretic &\\ +\hline +Subrace.Dwarf &\\ +\hline +Subrace.Neutral &\\ +\hline +Subrace.NeutralHuman &\\ +\hline +Subrace.NeutralElf &\\ +\hline +Subrace.NeutralGreenSkin &\\ +\hline +Subrace.NeutralDragon &\\ +\hline +Subrace.NeutralMarsh &\\ +\hline +Subrace.NeutralWater &\\ +\hline +Subrace.NeutralBarbarian &\\ +\hline +Subrace.NeutralWolf &\\ +\hline +Subrace.Elf &\\ +\hline +\end{tabularx} + +\subsubsection{Lord} +\label{LordCategory} +Lord categories correspond to the contents of \texttt{LLord.dbf} and can be accessed using table \texttt{Lord}. +By default, table contains following lord types:\\ +\begin{tabularx}{\linewidth}{| l | X |} +\hline +\textbf{Name} & \textbf{Description} \\ +\hline +Lord.Mage &\\ +\hline +Lord.Warrior &\\ +\hline +Lord.Diplomat &\\ +\hline +\end{tabularx} + +\subsubsection{Terrain} +\label{TerrainCategory} +Terrain types correspond to the contents of \texttt{LTerrain.dbf} and can be accessed using table \texttt{Terrain}. +By default, table contains following terrain types:\\ +\begin{tabularx}{\linewidth}{| l | X |} +\hline +\textbf{Name} & \textbf{Description} \\ +\hline +Terrain.Human & Empire native terrain\\ +\hline +Terrain.Undead & Undead Hordes native terrain\\ +\hline +Terrain.Heretic & Legions of the Damned native terrain\\ +\hline +Terrain.Dwarf & Mountain Clans native terrain\\ +\hline +Terrain.Neutral & Neutrals native terrain\\ +\hline +Terrain.Elf & Elven Alliance native terrain\\ +\hline +\end{tabularx} + +\subsubsection{Ground} +\label{GroundCategory} +Ground types correspond to the contents of \texttt{LGround.dbf} and can be accessed using table \texttt{Ground}. +By default, table contains following ground types:\\ +\begin{tabularx}{\linewidth}{| l | X |} +\hline +\textbf{Name} & \textbf{Description} \\ +\hline +Ground.Plain &\\ +\hline +Ground.Forest &\\ +\hline +Ground.Water &\\ +\hline +Ground.Mountain & Special type, indicates there is a mountain present on the tile.\\ +\hline +\end{tabularx} + +\subsubsection{Unit} +\label{UnitCategory} +Unit types correspond to the contents of \texttt{LUnitC.dbf} and can be accessed using table \texttt{Unit}. +By default, table contains following unit types:\\ +\begin{tabularx}{\linewidth}{| l | X |} +\hline +\textbf{Name} & \textbf{Description} \\ +\hline +Unit.Soldier & Ordinary units that can be hired in cities or mercenary camps\\ +\hline +Unit.Noble & Nobles (thieves) units\\ +\hline +Unit.Leader & Stack leader units\\ +\hline +Unit.Summon & Units summoned on the strategic map. Living Armor, for example\\ +\hline +Unit.Illusion & Illusions summoned on the strategic map\\ +\hline +Unit.Guardian & Race capital guardian units\\ +\hline +\end{tabularx} + +\subsubsection{Leader} +\label{LeaderCategory} +Leader types correspond to the contents of \texttt{LLeadC.dbf} and can be accessed using table \texttt{Leader}. +By default, table contains following leader types:\\ +\begin{tabularx}{\linewidth}{| l | X |} +\hline +\textbf{Name} & \textbf{Description} \\ +\hline +Leader.Fighter & Leaders with melee (\texttt{L\_ADJACENT}) attacks. Default starting leader for \texttt{Warrior} \hyperref[LordCategory]{lord type}\\ +\hline +Leader.Explorer & Leaders with ranged (\texttt{L\_ANY}) attacks. Default starting leader for \texttt{Diplomat} \hyperref[LordCategory]{lord type}\\ +\hline +Leader.Mage & Leaders with ranged (\texttt{L\_ALL}) attacks. Default starting leader for \texttt{Mage} \hyperref[LordCategory]{lord type}\\ +\hline +Leader.Rod & Leaders with rod placement ability\\ +\hline +Leader.Noble & Nobles (thieves)\\ +\hline +\end{tabularx} + +\subsubsection{Ability} +\label{AbilityCategory} +Leader ability types correspond to the contents of \texttt{LLeadA.dbf} and can be accessed using table \texttt{Ability}. +By default, table contains following leader abilities:\\ +\begin{tabularx}{\linewidth}{| l | X |} +\hline +\textbf{Name} & \textbf{Description} \\ +\hline +Ability.Incorruptible &\\ +\hline +Ability.WeaponMaster &\\ +\hline +Ability.WandScrollUse &\\ +\hline +Ability.WeaponArmorUse &\\ +\hline +Ability.BannerUse &\\ +\hline +Ability.JewelryUse &\\ +\hline +Ability.Rod &\\ +\hline +Ability.OrbUse &\\ +\hline +Ability.TalismanUse &\\ +\hline +Ability.TravelItemUse &\\ +\hline +Ability.CriticalHit &\\ +\hline +\end{tabularx} + +\subsubsection{Attack} +\label{AttackCategory} +Attack types correspond to the contents of \texttt{LAttC.dbf} and can be accessed using table \texttt{Attack}. +By default, table contains following attack types:\\ +\begin{tabularx}{\linewidth}{| l | X |} +\hline +\textbf{Name} & \textbf{Description} \\ +\hline +Attack.Damage &\\ +\hline +Attack.Drain &\\ +\hline +Attack.Paralyze &\\ +\hline +Attack.Heal &\\ +\hline +Attack.Fear &\\ +\hline +Attack.BoostDamage &\\ +\hline +Attack.Petrify &\\ +\hline +Attack.LowerDamage &\\ +\hline +Attack.LowerInitiative &\\ +\hline +Attack.Poison &\\ +\hline +Attack.Frostbite &\\ +\hline +Attack.Revive &\\ +\hline +Attack.DrainOverflow &\\ +\hline +Attack.Cure &\\ +\hline +Attack.Summon &\\ +\hline +Attack.DrainLevel &\\ +\hline +Attack.GiveAttack &\\ +\hline +Attack.Doppelganger &\\ +\hline +Attack.TransformSelf &\\ +\hline +Attack.TransformOther &\\ +\hline +Attack.Blister &\\ +\hline +Attack.BestowWards &\\ +\hline +Attack.Shatter &\\ +\hline +\end{tabularx} + +\subsubsection{Source} +\label{SourceCategory} +Attack sources correspond to the contents of \texttt{LAttS.dbf} and can be accessed using table \texttt{Source}. +By default, table contains following attack sources:\\ +\begin{tabularx}{\linewidth}{| l | X |} +\hline +\textbf{Name} & \textbf{Description} \\ +\hline +Source.Weapon &\\ +\hline +Source.Mind &\\ +\hline +Source.Life &\\ +\hline +Source.Death &\\ +\hline +Source.Fire &\\ +\hline +Source.Water &\\ +\hline +Source.Earth &\\ +\hline +Source.Air &\\ +\hline +\end{tabularx} + +\subsubsection{Reach} +\label{ReachCategory} +Attack reach types correspond to the contents of \texttt{LAttR.dbf} and can be accessed using table \texttt{Reach}. +By default, table contains following attack reach types:\\ +\begin{tabularx}{\linewidth}{| l | X |} +\hline +\textbf{Name} & \textbf{Description} \\ +\hline +Reach.All &\\ +\hline +Reach.Any &\\ +\hline +Reach.Adjacent &\\ +\hline +\end{tabularx} + +\subsubsection{Immune} +\label{ImmuneCategory} +Immune categories correspond to the contents of \texttt{LImmune.dbf} and can be accessed using table \texttt{Immune}. +By default, table contains following immune categories:\\ +\begin{tabularx}{\linewidth}{| l | X |} +\hline +\textbf{Name} & \textbf{Description} \\ +\hline +Immune.NotImmune & Target (unit or stack) is not immune\\ +\hline +Immune.Once & Target has one-time immunity (resistant)\\ +\hline +Immune.Always & Target is always immune\\ +\hline +\end{tabularx} + +\subsubsection{Item} +\label{ItemCategory} +Item types correspond to the contents of \texttt{LMagItm.dbf} and can be accessed using table \texttt{Item}. +By default, table contains following item types:\\ +\begin{tabularx}{\linewidth}{| l | X |} +\hline +\textbf{Name} & \textbf{Description} \\ +\hline +Item.Armor &\\ +\hline +Item.Jewel &\\ +\hline +Item.Weapon &\\ +\hline +Item.Banner &\\ +\hline +Item.PotionBoost &\\ +\hline +Item.PotionHeal &\\ +\hline +Item.PotionRevive &\\ +\hline +Item.PotionPermanent &\\ +\hline +Item.Scroll &\\ +\hline +Item.Wand &\\ +\hline +Item.Valuable &\\ +\hline +Item.Orb &\\ +\hline +Item.Talisman &\\ +\hline +Item.TravelItem &\\ +\hline +Item.Special &\\ +\hline +\end{tabularx} + +\subsubsection{Equipment} +\label{EquipmentCategory} +Leader equipment types can be accessed using table \texttt{Equipment}. +By default, table contains following equipment types:\\ +\begin{tabularx}{\linewidth}{| l | X |} +\hline +\textbf{Name} & \textbf{Description} \\ +\hline +Equipment.Banner &\\ +\hline +Equipment.Tome &\\ +\hline +Equipment.Battle1 &\\ +\hline +Equipment.Battle2 &\\ +\hline +Equipment.Artifact1 &\\ +\hline +Equipment.Artifact2 &\\ +\hline +Equipment.Boots &\\ +\hline +\end{tabularx} + +\subsubsection{DeathAnimation} +\label{DeathAnimationCategory} +Death animation types correspond to the contents of \texttt{LDthAnim.dbf} and can be accessed using table \texttt{DeathAnimation}. +By default, table contains following animations types:\\ +\begin{tabularx}{\linewidth}{| l | X |} +\hline +\textbf{Name} & \textbf{Description} \\ +\hline +DeathAnimation.Human &\\ +\hline +DeathAnimation.Heretic &\\ +\hline +DeathAnimation.Dwarf &\\ +\hline +DeathAnimation.Undead &\\ +\hline +DeathAnimation.Neutral &\\ +\hline +DeathAnimation.Dragon &\\ +\hline +DeathAnimation.Ghost &\\ +\hline +DeathAnimation.Elf &\\ +\hline +\end{tabularx} + +\subsubsection{BattleStatus} +\label{BattleStatus} +Battle statuses can be accessed using table \texttt{BattleStatus}. +By default, table contains following statuses:\\ +\begin{tabularx}{\linewidth}{| l | X |} +\hline +\textbf{Name} & \textbf{Description} \\ +\hline +BattleStatus.XpCounted & Unit was killed and its experience points were counted\\ +\hline +BattleStatus.Dead & Unit dead\\ +\hline +BattleStatus.Paralyze & Unit paralyzed\\ +\hline +BattleStatus.Petrify & Unit petrified\\ +\hline +BattleStatus.DisableLong & Long disable applied (paralyze, petrify or fear)\\ +\hline +BattleStatus.BoostDamageLvl1 & 1st level damage boost applied (25\% by default)\\ +\hline +BattleStatus.BoostDamageLvl2 & 2nd level damage boost applied (50\% by default)\\ +\hline +BattleStatus.BoostDamageLvl3 & 3rd level damage boost applied (75\% by default)\\ +\hline +BattleStatus.BoostDamageLvl4 & 4th level damage boost applied (100\% by default)\\ +\hline +BattleStatus.BoostDamageLong & Long damage boost (until battle is over or lower damage applied)\\ +\hline +BattleStatus.LowerDamageLvl1 & 1st level lower damage applied (50\% by default)\\ +\hline +BattleStatus.LowerDamageLvl2 & 2nd level lower damage applied (33\% by default)\\ +\hline +BattleStatus.LowerDamageLong & Long lower damage (until battle is over or removed)\\ +\hline +BattleStatus.LowerInitiative & Lower initiative applied (50\% by default)\\ +\hline +BattleStatus.LowerInitiativeLong & Long lower initiative\\ +\hline +BattleStatus.Poison & Poison DoT attack applied\\ +\hline +BattleStatus.PoisonLong & Long poison applied\\ +\hline +BattleStatus.Frostbite & Frostbite DoT attack applied\\ +\hline +BattleStatus.FrostbiteLong & Long frostbite applied\\ +\hline +BattleStatus.Blister & Blister DoT attack applied\\ +\hline +BattleStatus.BlisterLong & Long blister applied\\ +\hline +BattleStatus.Cured & Cure applied\\ +\hline +BattleStatus.Transform & Unit transformed by another unit\\ +\hline +BattleStatus.TransformLong & Long transformation applied by another unit\\ +\hline +BattleStatus.TransformSelf & Unit transfomed itself\\ +\hline +BattleStatus.TransformDoppelganger & Doppelganger transformation\\ +\hline +BattleStatus.TransformDrainLevel & Drain level applied\\ +\hline +BattleStatus.Summon & Unit was summoned during battle\\ +\hline +BattleStatus.Retreated & Unit retreated from battle\\ +\hline +BattleStatus.Retreat & Unit is retreating\\ +\hline +BattleStatus.Hidden & Unit is hidden. For example, while leader dueling a thief\\ +\hline +BattleStatus.Defend & Defend was used in this round\\ +\hline +BattleStatus.Unsummoned & Unsummon effect applied\\ +\hline +\end{tabularx} + +\subsubsection{BattleAction} +\label{BattleAction} +Battle action types can be accessed using table \texttt{BattleAction}. +By default, table contains following battle actions:\\ +\begin{tabularx}{\linewidth}{| l | X |} +\hline +\textbf{Name} & \textbf{Description} \\ +\hline +BattleAction.Attack &\\ +\hline +BattleAction.Skip &\\ +\hline +BattleAction.Retreat &\\ +\hline +BattleAction.Wait &\\ +\hline +BattleAction.Defend &\\ +\hline +BattleAction.Auto &\\ +\hline +BattleAction.UseItem &\\ +\hline +\end{tabularx} + +\subsubsection{Retreat} +\label{Retreat} +Retreat types can be accessed using table \texttt{Retreat}. +By default, table contains following retreat decisions:\\ +\begin{tabularx}{\linewidth}{| l | X |} +\hline +\textbf{Name} & \textbf{Description} \\ +\hline +Retreat.NoRetreat & Group does not retreat from battle\\ +\hline +Retreat.CoverAndRetreat & Frontline units defend and cover retreating leader and backline units\\ +\hline +Retreat.FullRetreat & Entire group retreats\\ +\hline +\end{tabularx} + +\subsubsection{Relation} +\label{Relation} +Diplomacy relation types can be accessed using table \texttt{Relation}. +By default, table contains following types:\\ +\begin{tabularx}{\linewidth}{| l | X |} +\hline +\textbf{Name} & \textbf{Description} \\ +\hline +Relation.War &\\ +\hline +Relation.Neutral &\\ +\hline +Relation.Peace &\\ +\hline +\end{tabularx} + +\subsubsection{Order} +\label{Order} +Stack orders correspond to the contents of \texttt{LOrder.dbf} and can be accessed using table \texttt{Order}. +By default, table contains following order types:\\ +\begin{tabularx}{\linewidth}{| l | X |} +\hline +\textbf{Name} & \textbf{Description} \\ +\hline +Order.Normal &\\ +\hline +Order.Stand &\\ +\hline +Order.Guard &\\ +\hline +Order.AttackStack &\\ +\hline +Order.DefendStack &\\ +\hline +Order.SecureCity &\\ +\hline +Order.Roam &\\ +\hline +Order.MoveToLocation &\\ +\hline +Order.DefendLocation &\\ +\hline +Order.Bezerk &\\ +\hline +Order.Assist &\\ +\hline +Order.Steal &\\ +\hline +Order.DefendCity &\\ +\hline +\end{tabularx} + +\subsubsection{Resource} +\label{Resource} +Resource types correspond to the contents of \texttt{LRes.dbf} and can be accessed using table \texttt{Resource}. +By default, table contains following resource types:\\ +\begin{tabularx}{\linewidth}{| l | X |} +\hline +\textbf{Name} & \textbf{Description} \\ +\hline +Resource.Gold &\\ +\hline +Resource.InfernalMana & \texttt{L\_RED}\\ +\hline +Resource.LifeMana & \texttt{L\_YELLOW}\\ +\hline +Resource.DeathMana & \texttt{L\_ORANGE}\\ +\hline +Resource.RunicMana & \texttt{L\_WHITE}\\ +\hline +Resource.GroveMana & \texttt{L\_BLUE}\\ +\hline +\end{tabularx} + +\subsubsection{IdType} +\label{IdType} +Identifier types can be accessed using table \texttt{IdType}. +By default, table contains following types:\\ +\begin{tabularx}{\linewidth}{| l | X |} +\hline +\textbf{Name} & \textbf{Description} \\ +\hline +IdType.Empty & Empty id\\ +\hline +IdType.ApplicationText & Entries of \texttt{TApp.dbf} and \texttt{TAppEdit.dbf}\\ +\hline +IdType.Building & Entries of \texttt{GBuild.dbf}\\ +\hline +IdType.Race & Entries of \texttt{GRace.dbf}\\ +\hline +IdType.Lord & Entries of \texttt{GLord.dbf}\\ +\hline +IdType.Spell & Entries of \texttt{GSpells.dbf}\\ +\hline +IdType.UnitGlobal & Unit implementations, entries of \texttt{GUnits.dbf}\\ +\hline +IdType.UnitGenerated & Runtime-generated unit implementations\\ +\hline +IdType.UnitModifier & Unit modifiers, entries of \texttt{GModif.dbf}\\ +\hline +IdType.Attack & Attacks, entries of \texttt{GAttacks.dbf}\\ +\hline +IdType.TextGlobal & Entries of \texttt{TGlobal.dbf}\\ +\hline +IdType.LandmarkGlobal & Entries of \texttt{GLmark.dbf}\\ +\hline +IdType.ItemGlobal & Base items, entries of \texttt{GItem.dbf}\\ +\hline +IdType.NobleAction & Noble (thief) actions, entries of \texttt{GAction.dbf}\\ +\hline +IdType.DynamicUpgrade & Dynamic upgrade rules, entries of \texttt{GDynUpgr.dbf}\\ +\hline +IdType.DynamicAttack & Runtime-generated unit primary attacks\\ +\hline +IdType.DynamicAltAttack & Runtime-generated unit primary alternative attacks\\ +\hline +IdType.DynamicAttack2 & Runtime-generated unit secondary attacks\\ +\hline +IdType.DynamicAltAttack2 & Runtime-generated unit secondary alternative attacks\\ +\hline +IdType.CampaignFile & Campaign files\\ +\hline +IdType.Plan & Utility for fast object lookup by map coordinates\\ +\hline +IdType.ObjectCount & Number of objects in scenario file\\ +\hline +IdType.ScenarioFile & Scenario files\\ +\hline +IdType.Map & Scenario map\\ +\hline +IdType.MapBlock & Blocks of scenario map\\ +\hline +IdType.ScenarioInfo & Scenario information\\ +\hline +IdType.SpellEffects &\\ +\hline +IdType.Fortification & Capitals and villages\\ +\hline +IdType.Player & Players in scenario\\ +\hline +IdType.PlayerKnownSpells & Spells known by player in scenario\\ +\hline +IdType.Fog & Fog of war for player in scenario\\ +\hline +IdType.PlayerBuildings & Capital buildings\\ +\hline +IdType.Road & Roads on scenario map\\ +\hline +IdType.Stack & Stacks in scenario\\ +\hline +IdType.Unit & Units in scenario\\ +\hline +IdType.Landmark & Landmarks in scenario\\ +\hline +IdType.Item & Items in scenario\\ +\hline +IdType.Bag & Bags in scenario\\ +\hline +IdType.Site & Sites in scenario\\ +\hline +IdType.Ruin & Ruins in scenario\\ +\hline +IdType.Tomb & Grave markers in scenario\\ +\hline +IdType.Rod & Rods in scenario\\ +\hline +IdType.Crystal & Gold mines and mana sources in scenario\\ +\hline +IdType.Diplomacy & Diplomacy rules in scenario\\ +\hline +IdType.SpellCast &\\ +\hline +\end{tabularx} +\begin{tabularx}{\linewidth}{| l | X |} +\hline +\textbf{Name} & \textbf{Description} \\ +\hline +IdType.Location & Location on scenario map\\ +\hline +IdType.StackTemplate & Stack templates in scenario\\ +\hline +IdType.Event & Events in scenario\\ +\hline +IdType.StackDestroyed & Information about stacks defeated in scenario\\ +\hline +IdType.TalismanCharges & Talisman charges counter in scenario\\ +\hline +IdType.Mountains & Mountains in scenario\\ +\hline +IdType.SubRace & Subraces in scenario\\ +\hline +IdType.SubRaceType & Entries of \texttt{GSubRace.dbf}\\ +\hline +IdType.QuestLog & Scenario quest log\\ +\hline +IdType.TurnSummary & Brief information about last turn in scenario\\ +\hline +IdType.ScenarioVariable & Scenario variables\\ +\hline +\end{tabularx} + +\subsubsection{Difficulty} +\label{Difficulty} +Difficulty types correspond to the contents of \texttt{Ldiff.dbf} and can be accessed using table \texttt{Difficulty}. +By default, table contains following difficulty types:\\ +\begin{tabularx}{\linewidth}{| l | X |} +\hline +\textbf{Name} & \textbf{Description} \\ +\hline +Easy &\\ +\hline +Average &\\ +\hline +Hard &\\ +\hline +VeryHard &\\ +\hline +\end{tabularx} + +\subsubsection{Building} +\label{BuildingCategory} +Building types correspond to the contents of \texttt{Lbuild.dbf} and can be accessed using table \texttt{Building}. +By default, table contains following building types:\\ +\begin{tabularx}{\linewidth}{| l | X |} +\hline +\textbf{Name} & \textbf{Description} \\ +\hline +Guild & Allows to hire thieves\\ +\hline +Heal & Temple, necessary for healing and resurrection in towns\\ +\hline +Magic & Mage tower, necessary for spells research\\ +\hline +Unit & Building that is needed to upgrade units\\ +\hline +\end{tabularx} + +\subsubsection{UnitBranch} +\label{UnitBranch} +\begin{tabularx}{\linewidth}{| l | X |} +\hline +\textbf{Name} & \textbf{Description} \\ +\hline +Fighter &\\ +\hline +Archer &\\ +\hline +Mage &\\ +\hline +Special &\\ +\hline +Sideshow &\\ +\hline +Hero &\\ +\hline +Noble &\\ +\hline +Summon &\\ +\hline +\end{tabularx} \ No newline at end of file diff --git a/ApiDocs/en/eventConditionsExample.tex b/ApiDocs/en/eventConditionsExample.tex new file mode 100644 index 00000000..e0e5afb8 --- /dev/null +++ b/ApiDocs/en/eventConditionsExample.tex @@ -0,0 +1,43 @@ +\subsection{Event conditions} +\subsubsection{Check if all tiles in location have the same terrain (Human)} +\begin{center} +\begin{lstlisting}[language=Lua] +-- You can use lambda functions freely +local forEachTile = function (location, f) + local pos = location.position + -- Use integers for tile coordinates + local halfR = math.floor(location.radius / 2) + local startX = pos.x - halfR + local startY = pos.y - halfR + local endX = pos.x + halfR + local endY = pos.y + halfR + + for x = startX,endX,1 do + for y = startY,endY,1 do + f(x, y) + end + end +end + +local location = scenario:getLocation('S143LO0000') +if not location then + return false +end + +local tilesTotal = location.radius * location.radius +local count = 0 + +forEachTile(location, function (x, y) + local tile = scenario:getTile(x, y) + if not tile then + return false + end + + if tile.terrain == Terrain.Human then + count = count + 1 + end +end) + +return tilesTotal == count +\end{lstlisting} +\end{center} \ No newline at end of file diff --git a/ApiDocs/en/examples.tex b/ApiDocs/en/examples.tex new file mode 100644 index 00000000..7b46e4d8 --- /dev/null +++ b/ApiDocs/en/examples.tex @@ -0,0 +1,18 @@ +\section{Examples} +See \href{https://github.com/VladimirMakeev/D2ModdingToolset/tree/master/Scripts}{Scripts} directory for additional examples. +\input{doppelgangerExample.tex} +\newpage +\input{transformSelfExample.tex} +\newpage +\input{transformOtherExample.tex} +\newpage +\input{drainLevelExample.tex} +\newpage +\input{summonExample.tex} +\newpage +\input{customReachExample.tex} +\newpage +\input{eventConditionsExample.tex} +\newpage +\input{customModifiersExample.tex} +\newpage \ No newline at end of file diff --git a/ApiDocs/en/fog.tex b/ApiDocs/en/fog.tex new file mode 100644 index 00000000..1e44eeaf --- /dev/null +++ b/ApiDocs/en/fog.tex @@ -0,0 +1,14 @@ +\subsection{Fog} +\label{Fog} +Represents \hyperref[Player]{player's} fog of war. +\subsubsection{Methods} +\begin{center} +\begin{tabularx}{\linewidth}{| l | X |} +\hline +\textbf{Name} & \textbf{Description} \\ +\hline +getFog(x, y) & Returns \texttt{true} if specified map position is covered by fog of war. Map position can be specified by pair of coordinates or a \hyperref[Point]{point}\\ +getFog(Point.new(3, 7)) &\\ +\hline +\end{tabularx} +\end{center} \ No newline at end of file diff --git a/ApiDocs/en/fort.tex b/ApiDocs/en/fort.tex new file mode 100644 index 00000000..cb86e914 --- /dev/null +++ b/ApiDocs/en/fort.tex @@ -0,0 +1,31 @@ +\subsection{Fort} +\label{Fort} +Represents Capital or City on a map. Fort contains a garrison \hyperref[Group]{group} of 6 unit \hyperref[UnitSlot]{slots}. Note that the garrison group is different to a group of visiting \hyperref[Stack]{stack}. +\subsubsection{Properties} +\begin{center} +\begin{tabularx}{\linewidth}{| l | X |} +\hline +\textbf{Name} & \textbf{Description} \\ +\hline +id & Returns fort \hyperref[Id]{id}. The value is unique for every fort on scenario map\\ +\hline +position & Returns fort position as a \hyperref[Point]{point}\\ +\hline +entrance & Return fort entrance coordinates as a \hyperref[Point]{point}\\ +\hline +owner & Returns \hyperref[Player]{player} that owns the fort. Neutral forts are owned by neutral player\\ +\hline +group & Returns fort units as a \hyperref[Group]{group}\\ +\hline +visitor & Returns visitor \hyperref[Stack]{stack}, or \texttt{nil} if none\\ +\hline +subrace & Returns fort \hyperref[SubraceCategory]{subrace}\\ +\hline +inventory & Returns array of inventory \hyperref[Item]{items}\\ +\hline +capital & Returns \texttt{true} if fort is a capital city\\ +\hline +tier & Returns fort tier (level). Tiers are in range \texttt{[1 : 6]}. Tier 6 corresponds to the capital city\\ +\hline +\end{tabularx} +\end{center} \ No newline at end of file diff --git a/ApiDocs/en/game.tex b/ApiDocs/en/game.tex new file mode 100644 index 00000000..08028fe2 --- /dev/null +++ b/ApiDocs/en/game.tex @@ -0,0 +1,21 @@ +\subsection{Game} +\label{Game} +Represents game restrictions and constants. Allows to access \texttt{settings.lua}. +\subsubsection{Properties} +\begin{center} +\begin{tabularx}{\linewidth}{| l | X |} +\hline +\textbf{Name} & \textbf{Description} \\ +\hline +unitMaxDamage & Maximum damage unit attack can inflict in battle. \texttt{unitMaxDamage} from \texttt{settings.lua}\\ +\hline +unitMinDamage & Minimum damage unit attack can inflict in battle. Currently 1\\ +\hline +unitMaxArmor & Maximum armor unit can have. \texttt{unitMaxArmor} from \texttt{settings.lua}\\ +\hline +leaderAdditionalDamage & Additional damage granted by leader ability 'Heavy strike'. Currently 100\\ +\hline +editor & Returns \texttt{true} if Scenario Editor (\texttt{ScenEdit.exe}) is running, \texttt{false} if game (\texttt{Discipl2.exe}) \\ +\hline +\end{tabularx} +\end{center} diff --git a/ApiDocs/en/global.tex b/ApiDocs/en/global.tex new file mode 100644 index 00000000..7c17d5da --- /dev/null +++ b/ApiDocs/en/global.tex @@ -0,0 +1,13 @@ +\subsection{Global} +\label{Global} +Represents global data storage used by game. Allows to access contents of dbf files in \texttt{Globals} folder of the game. +\subsubsection{Properties} +\begin{center} +\begin{tabularx}{\linewidth}{| l | X |} +\hline +\textbf{Name} & \textbf{Description} \\ +\hline +variables & Returns \hyperref[GlobalVariables]{global variables}\\ +\hline +\end{tabularx} +\end{center} \ No newline at end of file diff --git a/ApiDocs/en/globalvariables.tex b/ApiDocs/en/globalvariables.tex new file mode 100644 index 00000000..7814b1f3 --- /dev/null +++ b/ApiDocs/en/globalvariables.tex @@ -0,0 +1,148 @@ +\subsection{GlobalVariables} +\label{GlobalVariables} +Allows to access contents of \texttt{GVars.dbf}. +\subsubsection{Properties} +\begin{center} +\begin{tabularx}{\linewidth}{| l | X |} +\hline +\textbf{Name} & \textbf{Description}\\ +\hline +weapnMstr & Instructor skill bonus experience, \texttt{WEAPN\_MSTR}\\ +\hline +batInit & Max additional initiative points that randomly added to unit in battle. \texttt{BAT\_INIT}\\ +\hline +batDamage & Max additional damage points that randomly added to unit damage in battle. \texttt{BAT\_DAMAGE}\\ +\hline +batRound & \texttt{BAT\_ROUND}\\ +\hline +batBreak & \texttt{BAT\_BREAK}\\ +\hline +batBModif & \texttt{BAT\_BMODIF}\\ +\hline +batLoweri & Initiative debuff. \texttt{BATLOWERI}\\ +\hline +ldrMaxAbil & Maximum number of abilities leader can learn. \texttt{LDRMAXABIL}\\ +\hline +spyDiscov & Spy discovery chance per turn. \texttt{SPY\_DISCOV}\\ +\hline +poisonC & Damage from thief action 'poison city'. \texttt{POISON\_C}\\ +\hline +poisonS & Damage from thief action 'poison stack'. \texttt{POISON\_S}\\ +\hline +bribe & Bribe multiplier. \texttt{BRIBE}\\ +\hline +stealRace & \texttt{STEAL\_RACE}\\ +\hline +stealNeut & \texttt{STEAL\_NEUT}\\ +\hline +riotMin & Minimal riot duration in days. \texttt{RIOT\_MIN}\\ +\hline +riotMax & Maximal riot duration in days. \texttt{RIOT\_MAX}\\ +\hline +riotDmg & Percentage of riot damage. \texttt{RIOT\_DMG}\\ +\hline +sellRatio & Percentage of the original price of the items at sale. \texttt{SELL\_RATIO}\\ +\hline +tCapture & Land transformation after city capture. \texttt{T\_CAPTURE}\\ +\hline +tCapital & Land transformation per turn by capital. \texttt{T\_CAPITAL}\\ +\hline +rodRange & Range of land transformation by rod per turn. \texttt{ROD\_RANGE}\\ +\hline +crystalP & Profit per mana crystal or gold mine per turn. \texttt{CRYSTAL\_P}\\ +\hline +constUrg & \texttt{CONST\_URG}\\ +\hline +regenLwar & Bonus per day regeneration for fighter leader. \texttt{REGEN\_LWAR}\\ +\hline +regenRuin & Bonus per day regeneration for units in ruins. \texttt{REGEN\_RUIN}\\ +\hline +dPeace & Diplomacy level representing peace. \texttt{D\_PEACE}\\ +\hline +dWar & Diplomacy level representing war. \texttt{D\_WAR}\\ +\hline +dNeutral & Diplomacy level representing neutrality. \texttt{D\_NEUTRAL}\\ +\hline +dGold & \texttt{D\_GOLD}\\ +\hline +dMkAlly & \texttt{D\_MK\_ALLY}\\ +\hline +dAttakSc & \texttt{D\_ATTACK\_SC}\\ +\hline +dAttakFo & \texttt{D\_ATTACK\_FO}\\ +\hline +dAttakPc & \texttt{D\_ATTACK\_PC}\\ +\hline +dRod & \texttt{D\_ROD}\\ +\hline +dRefAlly & \texttt{D\_REF\_ALLY}\\ +\hline +dBkAlly & \texttt{D\_BK\_ALLY}\\ +\hline +dNoble & \texttt{D\_NOBLE}\\ +\hline +dBkaChnc & \texttt{D\_BKA\_CHANCE}\\ +\hline +dBkaTurn & \texttt{D\_BKA\_TURN}\\ +\hline +\end{tabularx} +\end{center} +\begin{center} +\begin{tabularx}{\linewidth}{| l | X |} +\hline +\textbf{Name} & \textbf{Description}\\ +\hline +protCap & Capital protection. \texttt{PROT\_CAP}\\ +\hline +bonusE & Additional gold on easy difficulty. \texttt{BONUS\_E}\\ +\hline +bonusA & Additional gold on average difficulty. \texttt{BONUS\_A}\\ +\hline +bonusH & Additional gold on hard difficulty. \texttt{BONUS\_H}\\ +\hline +bonusV & Additional gold on very hard difficulty. \texttt{BONUS\_V}\\ +\hline +incomeE & Income increase on easy difficulty. \texttt{INCOME\_E}\\ +\hline +incomeA & Income increase on average difficulty. \texttt{INCOME\_A}\\ +\hline +incomeH & Income increase on hard difficulty. \texttt{INCOME\_H}\\ +\hline +incomeV & Income increase on very hard difficulty. \texttt{INCOME\_V}\\ +\hline +guRange & \texttt{GU\_RANGE}\\ +\hline +paRange & \texttt{PA\_RANGE}\\ +\hline +loRange & \texttt{LO\_RANGE}\\ +\hline +defendBonus & Armor bonus when unit uses defend in battle. \texttt{DFENDBONUS}\\ +\hline +talisChrg & \texttt{TALIS\_CHRG}\\ +\hline +gainSpell & Chance to get spells with capture of a capital. \texttt{GAIN\_SPELL}\\ +\hline +rodCost & Rod placement cost. \texttt{ROD\_COST}\\ +\hline +\end{tabularx} +\end{center} +\subsubsection{Methods} +\begin{center} +\begin{tabularx}{\linewidth}{| l | X |} +\hline +\textbf{Name} & \textbf{Description}\\ +\hline +morale(n) & Input tier values must be in range \texttt{[1 : 6]}. \texttt{MORALE\_n}\\ +\hline +batBoostd(n) & Damage boost values for various levels. Levels must be in range \texttt{[1 : 4]}. \texttt{BATBOOSTDn}\\ +\hline +batLowerd(n) & Damage debuff values for various levels. Levels must be in range \texttt{[1 : 2]}. \texttt{BATLOWERDn}\\ +\hline +tCity(n) & Land transformation per turn by cities of different tiers. Tier must be in range \texttt{[1 : 5]}. \texttt{T\_CITYn}\\ +\hline +prot(n) & City protection values for various tier levels. Tier must be in range \texttt{[1 : 6]}. In case of tier 6, returns \texttt{protCap}. \texttt{PROT\_n}\\ +\hline +splPwr(n) & Input tier values must be in range \texttt{[1 : 5]}. \texttt{SPLPWR\_n}\\ +\hline +\end{tabularx} +\end{center} \ No newline at end of file diff --git a/ApiDocs/en/group.tex b/ApiDocs/en/group.tex new file mode 100644 index 00000000..0fe361a0 --- /dev/null +++ b/ApiDocs/en/group.tex @@ -0,0 +1,35 @@ +\subsection{Group} +\label{Group} +Represents 6 unit \hyperref[UnitSlot]{slots}. +\subsubsection{Properties} +\begin{center} +\begin{tabularx}{\linewidth}{| l | X |} +\hline +\textbf{Name} & \textbf{Description} \\ +\hline +id & Returns group \hyperref[Id]{id}\\ +\hline +slots & Returns group as an array of 6 \hyperref[UnitSlot]{slots}\\ +\hline +units & Returns group as an array of \hyperref[Unit]{units}\\ +\hline +subrace & Returns group \hyperref[SubraceCategory]{subrace}. In case of group inside \hyperref[Ruin]{ruin}, returns -1 since ruins do not belong to subraces\\ +\hline +\end{tabularx} +\end{center} + +\subsubsection{Methods} +\begin{center} +\begin{tabularx}{\linewidth}{| l | X |} +\hline +\textbf{Name} & \textbf{Description} \\ +\hline +hasUnit(unit) & Returns true if group has specified \hyperref[Unit]{unit}\\ +\hline +hasUnit(Id.new('S143UN0001')) & Returns true if group has specified unit \hyperref[Id]{id}\\ +\hline +getUnitPosition(unit) & Returns \hyperref[Unit]{unit} position in group, or -1 if unit not found\\ +getUnitPosition(unit.id) &\\ +\hline +\end{tabularx} +\end{center} \ No newline at end of file diff --git a/ApiDocs/en/id.tex b/ApiDocs/en/id.tex new file mode 100644 index 00000000..362d5b9c --- /dev/null +++ b/ApiDocs/en/id.tex @@ -0,0 +1,38 @@ +\subsection{Id} +\label{Id} +Represents object identifier. Identifiers used to search scenario objects. +\subsubsection{Properties} +\begin{center} +\begin{tabularx}{\linewidth}{| l | X |} +\hline +\textbf{Name} & \textbf{Description} \\ +\hline +value & Returns integer representation of id. Can be used as Lua table key for best performance.\\ +\hline +typeIndex & Returns identified object index among the same type of scenario objects (units, stacks, items, etc.). Can be used as Lua table key for best performance.\\ +\hline +type & Returns \hyperref[IdType]{type} of identifier. Identifier type can help to distinguish one object from another\\ +\hline +\end{tabularx} +\end{center} +\subsubsection{Methods} +\begin{center} +\begin{tabularx}{\linewidth}{| l | X |} +\hline +\textbf{Name} & \textbf{Description} \\ +\hline +Id.new('S143KC0001') & Creates id from string\\ +\hline +Id.emptyId() & Returns empty identifier\\ +\hline +tostring(id) & Converts id to string\\ +\hline +Id.summonId(position) & Creates special id for summoning units in battle using specified position in group. Position in group should be in \texttt{[0 : 5]} range.\\ +\hline +\end{tabularx} +\end{center} +%\paragraph{Examples:} +%\begin{center} +%\begin{lstlisting}[language=Lua] +%\end{lstlisting} +%\end{center} \ No newline at end of file diff --git a/ApiDocs/en/item.tex b/ApiDocs/en/item.tex new file mode 100644 index 00000000..d3e95df8 --- /dev/null +++ b/ApiDocs/en/item.tex @@ -0,0 +1,17 @@ +\subsection{Item} +\label{Item} +Represents item object in the current scenario. +\subsubsection{Properties} +\begin{center} +\begin{tabularx}{\linewidth}{| l | X |} +\hline +\textbf{Name} & \textbf{Description} \\ +\hline +id & Returns item \hyperref[Id]{id}. This is different to id of \hyperref[ItemBase]{item base}. The value is unique for every item on scenario map\\ +\hline +base & Returns \hyperref[ItemBase]{item base}\\ +\hline +sellValue & Returns item \hyperref[Currency]{sell value}, it accounts global sell ratio and used talisman charges (if applicable)\\ +\hline +\end{tabularx} +\end{center} \ No newline at end of file diff --git a/ApiDocs/en/itembase.tex b/ApiDocs/en/itembase.tex new file mode 100644 index 00000000..9bde4ef7 --- /dev/null +++ b/ApiDocs/en/itembase.tex @@ -0,0 +1,21 @@ +\subsection{Item base} +\label{ItemBase} +Represents base item of any type (described in \texttt{GItem.dbf}) +\subsubsection{Properties} +\begin{center} +\begin{tabularx}{\linewidth}{| l | X |} +\hline +\textbf{Name} & \textbf{Description} \\ +\hline +id & Returns item \hyperref[Id]{id}. \texttt{ITEM\_ID} value from \texttt{GItem.dbf}\\ +\hline +type & Returns item \hyperref[ItemCategory]{type}\\ +\hline +value & Returns item \hyperref[Currency]{value}\\ +\hline +unitImpl & Returns related \hyperref[UnitImpl]{unit implementation}. For instance: in case of `Angel Orb', Angel unit implementation is returned\\ +\hline +attack & Returns \hyperref[Attack]{attack} that this item performs (in case of orb or talisman), or \texttt{nil} if no attack is associated with the item. For instance: in case of `Orb of Fire', corresponding attack from \texttt{Gattacks.dbf} is returned\\ +\hline +\end{tabularx} +\end{center} \ No newline at end of file diff --git a/ApiDocs/en/location.tex b/ApiDocs/en/location.tex new file mode 100644 index 00000000..a670eb22 --- /dev/null +++ b/ApiDocs/en/location.tex @@ -0,0 +1,19 @@ +\subsection{Location} +\label{Location} +Represents location object in scenario. +\subsubsection{Properties} +\begin{center} +\begin{tabularx}{\linewidth}{| l | X |} +\hline +\textbf{Name} & \textbf{Description} \\ +\hline +id & Returns location \hyperref[Id]{id}. The value is unique for every location on scenario map\\ +\hline +position & Returns location position as a \hyperref[Point]{point}\\ +\hline +radius & Returns radius of location\\ +\hline +name & Returns location name\\ +\hline +\end{tabularx} +\end{center} \ No newline at end of file diff --git a/ApiDocs/en/mercenary.tex b/ApiDocs/en/mercenary.tex new file mode 100644 index 00000000..1a602d37 --- /dev/null +++ b/ApiDocs/en/mercenary.tex @@ -0,0 +1,19 @@ +\subsection{Mercenary camp} +\label{Mercenary} +Represents Mercenary camp on a map. +\subsubsection{Properties} +\begin{center} +\begin{tabularx}{\linewidth}{| l | X |} +\hline +\textbf{Name} & \textbf{Description} \\ +\hline +id & Returns mercenary camp \hyperref[Id]{id}. The value is unique for every mercenary camp on scenario map\\ +\hline +position & Returns mercenary camp position as a \hyperref[Point]{point}\\ +\hline +visitors & Returns list of \hyperref[Player]{players} that have visited the mercenary camp\\ +\hline +units & Returns list of \hyperref[MercenaryUnit]{mercenary units}\\ +\hline +\end{tabularx} +\end{center} \ No newline at end of file diff --git a/ApiDocs/en/mercenaryunit.tex b/ApiDocs/en/mercenaryunit.tex new file mode 100644 index 00000000..55348f2b --- /dev/null +++ b/ApiDocs/en/mercenaryunit.tex @@ -0,0 +1,15 @@ +\subsection{Mercenary unit} +\label{MercenaryUnit} +Represents unit for hire in \hyperref[Mercenary]{mercenary camp}. +\subsubsection{Properties} +\begin{center} +\begin{tabularx}{\linewidth}{| l | X |} +\hline +\textbf{Name} & \textbf{Description} \\ +\hline +impl & Returns \hyperref[UnitImpl]{unit implementation}\\ +\hline +unique & Returns \texttt{true} is unit can be hired only once\\ +\hline +\end{tabularx} +\end{center} \ No newline at end of file diff --git a/ApiDocs/en/merchant.tex b/ApiDocs/en/merchant.tex new file mode 100644 index 00000000..bc0eb074 --- /dev/null +++ b/ApiDocs/en/merchant.tex @@ -0,0 +1,21 @@ +\subsection{Merchant} +\label{Merchant} +Represents Merchant on a map. +\subsubsection{Properties} +\begin{center} +\begin{tabularx}{\linewidth}{| l | X |} +\hline +\textbf{Name} & \textbf{Description} \\ +\hline +id & Returns merchant \hyperref[Id]{id}. The value is unique for every merchant on scenario map\\ +\hline +position & Returns merchant position as a \hyperref[Point]{point}\\ +\hline +visitors & Returns list of \hyperref[Player]{players} that have visited the merchant\\ +\hline +items & Returns list of \hyperref[MerchantItem]{merchant items}\\ +\hline +temple & Returns \texttt{true} if merchant can be used as a temple for AI to heal\\ +\hline +\end{tabularx} +\end{center} \ No newline at end of file diff --git a/ApiDocs/en/merchantitem.tex b/ApiDocs/en/merchantitem.tex new file mode 100644 index 00000000..285b15a0 --- /dev/null +++ b/ApiDocs/en/merchantitem.tex @@ -0,0 +1,15 @@ +\subsection{Merchant item} +\label{MerchantItem} +Represents item sold by \hyperref[Merchant]{merchant}. +\subsubsection{Properties} +\begin{center} +\begin{tabularx}{\linewidth}{| l | X |} +\hline +\textbf{Name} & \textbf{Description} \\ +\hline +base & Returns \hyperref[ItemBase]{base item}\\ +\hline +amount & Returns amount of items in merchant stock\\ +\hline +\end{tabularx} +\end{center} \ No newline at end of file diff --git a/ApiDocs/en/modifier.tex b/ApiDocs/en/modifier.tex new file mode 100644 index 00000000..fd531a1b --- /dev/null +++ b/ApiDocs/en/modifier.tex @@ -0,0 +1,13 @@ +\subsection{Modifier} +\label{Modifier} +Represents unit modifier. Modifiers wrap \hyperref[UnitImpl]{unit implementation}. +\subsubsection{Properties} +\begin{center} +\begin{tabularx}{\linewidth}{| l | X |} +\hline +\textbf{Name} & \textbf{Description} \\ +\hline +id & Returns modifier \hyperref[Id]{id}. \texttt{MODIF\_ID} value from \texttt{Gmodif.dbf}\\ +\hline +\end{tabularx} +\end{center} \ No newline at end of file diff --git a/ApiDocs/en/overview.tex b/ApiDocs/en/overview.tex new file mode 100644 index 00000000..6001377c --- /dev/null +++ b/ApiDocs/en/overview.tex @@ -0,0 +1,25 @@ +\section{Overview} +All scripts are expected to be written using \href{https://www.lua.org/}{Lua} language \href{https://www.lua.org/ftp/lua-5.4.1.tar.gz}{version 5.4.1} and should be placed in Scripts folder. Scripts folder itself should be placed in the game folder. + + +Currently used scripts and their meanings: +\begin{itemize} +\item settings.lua - mss32 proxy dll settings that changes game rules +\item doppelganger.lua - logic that computes level of doppelganger transform (category \texttt{L\_DOPPELGANGER}) +\item transformSelf.lua - computes unit level and determines free attacks to give for transform-self attacks (category \texttt{L\_TRANSFORM\_SELF}) +\item transformOther.lua - computes unit level for transform-other attacks (category \texttt{L\_TRANSFORM\_OTHER}) +\item summon.lua - computes summoned unit level for summon attacks (category \texttt{L\_SUMMON}) +\item textids.lua - contains interface text mapping for custom functionality +\item getAllTargets.lua - contains selection/attack targeting logic for \texttt{L\_ANY}/\texttt{L\_ALL} attack reaches +\item getAdjacentTargets.lua - contains selection/attack targeting logic for adjacent/all-adjacent attack reach +\item getSelectedTargetAndAllAdjacentToIt.lua - contains attack targeting logic for selective-cleave attack reach +\item getSelectedTargetAndOneAdjacentToIt.lua - contains attack targeting logic for single selective-cleave attack reach +\item getSelectedTargetAndOneBehindIt.lua - contains attack targeting logic for pierce attack reach +\item getSelectedLineTargets.lua - contains attack targeting logic for wide-cleave attack reach +\item getSelectedColumnTargets.lua - contains attack targeting logic for column attack reach +\item getSelectedArea2x2Targets.lua - contains attack targeting logic for 2x2 area splash attack reach +\item getSelectedTargetAndTwoChainedRandom.lua - contains attack targeting logic for random chain attack reach +\item getSelectedTargetAndOneRandom.lua - contains attack targeting logic for additional random target +\item getWoundedFemaleGreenskinTargets.lua - contains targeting logic that only allows to reach wounded female greenskins +\item \href{https://github.com/VladimirMakeev/D2ModdingToolset/tree/master/Scripts/Modifiers}{Scripts/Modifiers} - contain custom modifier script examples +\end{itemize} diff --git a/ApiDocs/en/player.tex b/ApiDocs/en/player.tex new file mode 100644 index 00000000..bf69b5b8 --- /dev/null +++ b/ApiDocs/en/player.tex @@ -0,0 +1,39 @@ +\subsection{Player} +\label{Player} +Represents game player including AI and neutrals. +\subsubsection{Properties} +\begin{center} +\begin{tabularx}{\linewidth}{| l | X |} +\hline +\textbf{Name} & \textbf{Description} \\ +\hline +id & Returns player \hyperref[Id]{id}. The value is unique for every player on scenario map\\ +\hline +race & Returns player \hyperref[RaceCategory]{race}\\ +\hline +lord & Returns player \hyperref[LordCategory]{lord type}\\ +\hline +bank & Returns player \hyperref[Currency]{bank}\\ +\hline +human & Returns \texttt{true} if player is human (not AI)\\ +\hline +alwaysAi & Returns \texttt{true} if player is always AI\\ +\hline +fog & Returns player's \hyperref[Fog]{fog of war}. In fully loaded scenario, player objects always have fog of war. During scenario loading this property can return \texttt{nil}\\ +\hline +buildings & Returns list of \hyperref[Building]{buildings} that are already built\\ +\hline +\end{tabularx} +\end{center} + +\subsubsection{Methods} +\begin{center} +\begin{tabularx}{\linewidth}{| l | X |} +\hline +\textbf{Name} & \textbf{Description} \\ +\hline +hasBuilding('g000bb0001') & Returns \texttt{true} if player has built building with specified id\\ +hasBuilding(Id.new('g000bb0001')) & Method also accepts \hyperref[Id]{ids}\\ +\hline +\end{tabularx} +\end{center} \ No newline at end of file diff --git a/ApiDocs/en/point.tex b/ApiDocs/en/point.tex new file mode 100644 index 00000000..fd3ac95e --- /dev/null +++ b/ApiDocs/en/point.tex @@ -0,0 +1,40 @@ +\subsection{Point} +\label{Point} +Represents point in 2D space. +\subsubsection{Properties} +\begin{center} +\begin{tabularx}{\linewidth}{| l | X |} +\hline +\textbf{Name} & \textbf{Description} \\ +\hline +x & Access x coordinate for reading and writing\\ +\hline +y & Access y coordinate for reading and writing\\ +\hline +\end{tabularx} +\end{center} +\subsubsection{Methods} +\begin{center} +\begin{tabularx}{\linewidth}{| l | X |} +\hline +\textbf{Name} & \textbf{Description} \\ +\hline +Point.new() & Creates point with both coordinates set to 0\\ +\hline +Point.new(x, y) & Creates point with specified coordinates\\ +\hline +tostring(p) & Converts point to string \texttt{'(x, y)'}\\ +\hline +\end{tabularx} +\end{center} +\subsubsection{Examples} +\begin{center} +\begin{lstlisting}[language=Lua] +-- x = 0, y = 0 +local p = Point.new() +-- x = 1, y = 3 +local p2 = Point.new(1, 3) +-- s == '(1, 3)' +local s = tostring(p2) +\end{lstlisting} +\end{center} \ No newline at end of file diff --git a/ApiDocs/en/resourcemarket.tex b/ApiDocs/en/resourcemarket.tex new file mode 100644 index 00000000..198619ef --- /dev/null +++ b/ApiDocs/en/resourcemarket.tex @@ -0,0 +1,180 @@ +\subsection{Resource market} +\label{ResourceMarket} +Represents resource market on a map. +\subsubsection{Properties} +\begin{center} +\begin{tabularx}{\linewidth}{| l | X |} +\hline +\textbf{Name} & \textbf{Description} \\ +\hline +id & Returns market \hyperref[Id]{id}. The value is unique for every resource market on scenario map\\ +\hline +position & Returns market position as a \hyperref[Point]{point}\\ +\hline +visitors & Returns list of \hyperref[Player]{players} that have visited the market\\ +\hline +stock & Returns market \hyperref[Currency]{stock}\\ +\hline +customRates & Returns \texttt{true} if resource market uses custom exchange rates\\ +\hline +\end{tabularx} +\end{center} + +\subsubsection{Methods} +\begin{center} +\begin{tabularx}{\linewidth}{| l | X |} +\hline +\textbf{Name} & \textbf{Description} \\ +\hline +isInfinite(Resource.Gold) & Returns \texttt{true} if market has infinite amount of specified \hyperref[Resource]{resource}\\ +\hline +\end{tabularx} +\end{center} + +\subsubsection{Custom exchange rates} +Custom exchange rates are defined by Lua script which must declare function with a predefined name: +\texttt{getExchangeRates}. +Function must return table with exchanges that are possible. Empty table means that market could not exchange resources at all.\\ +Function signature: +\begin{center} +\begin{lstlisting}[language=Lua] +function getExchangeRates(visitorStack, market, serverSide) + return { } +end +\end{lstlisting} +\end{center} +Arguments are: +\begin{itemize} +\item \texttt{visitorStack} - \hyperref[Stack]{stack} that is currently visiting the market +\item \texttt{market} - \hyperref[ResourceMarket]{resource market} itself +\item \texttt{serverSide} - \texttt{true} if exchange rates script is executed by the server +\end{itemize} +Possible exchange is represented by a table that specifies resource that player can sell and a list of resources it can get, with rates.\\ +Format is shown by the following listing: +\begin{center} +\begin{lstlisting}[language=Lua] +{ + player_resource, + { market_resource, player_resource_amount, market_resource_amount } +} +\end{lstlisting} +\end{center} +Exchange that allows player to sell 2 gold for 1 life mana: +\begin{center} +\begin{lstlisting}[language=Lua] +{ + Resource.Gold, + { Resource.LifeMana, 2, 1 } +} +\end{lstlisting} +\end{center} +Exchange that allows player to sell gold: 2 gold for 1 life mana or 7 gold for 4 death mana: +\begin{center} +\begin{lstlisting}[language=Lua] +{ + Resource.Gold, + { Resource.LifeMana, 2, 1 }, + { Resource.DeathMana, 7, 4 } +} +\end{lstlisting} +\end{center} +Exchange that allows player to sell runic mana: 1 runic mana for 10 gold or 4 runic mana for 2 grove mana: +\begin{center} +\begin{lstlisting}[language=Lua] +{ + Resource.RunicMana, + { Resource.Gold, 1, 10 }, + { Resource.GroveMana, 4, 2 } +} +\end{lstlisting} +\end{center} +Market exchange rates table must contain list of possible exchanges. Here is the example of exchange rates when player is allowed to trade gold for life mana and life mana for other mana types: +\begin{center} +\begin{lstlisting}[language=Lua] +{ + { + Resource.Gold, + { Resource.LifeMana, 2, 1 } + }, + { + Resource.LifeMana, + { Resource.DeathMana, 4, 1 }, + { Resource.RunicMana, 4, 1 }, + { Resource.InfernalMana, 4, 1 }, + { Resource.GroveMana, 4, 1 } + } +} +\end{lstlisting} +\end{center} +\texttt{getExchangeRates} function must return such table.\\ +\subsubsection{Examples} +Custom exchange rates where market is only allowed to exchange 7 gold for 1 life mana: +\begin{center} +\begin{lstlisting}[language=Lua] +function getExchangeRates(visitorStack, market, serverSide) + return { + { + Resource.Gold, + { Resource.LifeMana, 7, 1 } + } + } +end +\end{lstlisting} +\end{center} +\newpage +All functions and objects of Lua API can be used in custom exchange rates scripts. This allows mod and map makers to check for lord types, specific leaders or stack groups, units and more.\\ +Example of custom exchange rates where exchange rates depend on number of previously visited resource markets: +\begin{center} +\begin{lstlisting}[language=Lua] +-- Returns true if market was visited by specified race +local function isMarketVisited(market, race) + local visitors = market.visitors + for i = 1, #visitors do + local visitor = visitors[i] + if visitor.race == race then + return true + end + end + + return false +end + +function getExchangeRates(visitor, currentMarket, serverSide) + local visitorRace = visitor.owner.race + local marketsVisited = 0 + + local countVisitedMarkets = function (market) + if isMarketVisited(market, visitorRace) then + marketsVisited = marketsVisited + 1 + end + end + + -- Count how many markets we have visited so far + getScenario():forEachMarket(countVisitedMarkets) + + -- Check if current resource market is already visited. + -- If not, it means we entered it for the first time + -- and game have not yet marked it as visited. + if not isMarketVisited(currentMarket, visitorRace) then + marketsVisited = marketsVisited + 1 + end + + local exchangeRate = { 100, 50, 25, 12 } + -- Make sure index stays within array bounds + marketsVisited = math.min(marketsVisited, #exchangeRate) + + local rate = exchangeRate[marketsVisited] + + return { + { + Resource.Gold, + { + -- Exchange (100 / 50 / 25 / 12) gold for 50 life mana + -- Rate depends on how many resource markets player have visited so far + { Resource.LifeMana, rate, 50 } + } + } + } +end +\end{lstlisting} +\end{center} \ No newline at end of file diff --git a/ApiDocs/en/rod.tex b/ApiDocs/en/rod.tex new file mode 100644 index 00000000..a878a98f --- /dev/null +++ b/ApiDocs/en/rod.tex @@ -0,0 +1,17 @@ +\subsection{Rod} +\label{Rod} +Represents rod object in scenario. Rods are planted to transform terrain and capture resources. +\subsubsection{Properties} +\begin{center} +\begin{tabularx}{\linewidth}{| l | X |} +\hline +\textbf{Name} & \textbf{Description} \\ +\hline +id & Returns rod \hyperref[Id]{id}. The value is unique for every rod on scenario map\\ +\hline +position & Returns rod position as a \hyperref[Point]{point}\\ +\hline +owner & Returns \hyperref[Player]{player} that planted the rod\\ +\hline +\end{tabularx} +\end{center} \ No newline at end of file diff --git a/ApiDocs/en/ruin.tex b/ApiDocs/en/ruin.tex new file mode 100644 index 00000000..7b7d4cf9 --- /dev/null +++ b/ApiDocs/en/ruin.tex @@ -0,0 +1,23 @@ +\subsection{Ruin} +\label{Ruin} +Represents Ruin on a map. Ruin contains a garrison \hyperref[Group]{group} of 6 unit \hyperref[UnitSlot]{slots}. +\subsubsection{Properties} +\begin{center} +\begin{tabularx}{\linewidth}{| l | X |} +\hline +\textbf{Name} & \textbf{Description} \\ +\hline +id & Returns ruin \hyperref[Id]{id}. The value is unique for every ruin on scenario map\\ +\hline +position & Returns ruin position as a \hyperref[Point]{point}\\ +\hline +looter & Returns \hyperref[Player]{player} that have looted the ruin or \texttt{nil} if none\\ +\hline +group & Returns ruin units as a \hyperref[Group]{group}\\ +\hline +item & Returns \hyperref[Item]{item} reward for looting the ruin or \texttt{nil} if none\\ +\hline +cash & Returns \hyperref[Currency]{cash} reward for looting the ruin\\ +\hline +\end{tabularx} +\end{center} \ No newline at end of file diff --git a/ApiDocs/en/scenario.tex b/ApiDocs/en/scenario.tex new file mode 100644 index 00000000..6df7192c --- /dev/null +++ b/ApiDocs/en/scenario.tex @@ -0,0 +1,189 @@ +\subsection{Scenario} +\label{Scenario} +Represents scenario map with all its objects and state +\subsubsection{Properties} +\begin{center} +\begin{tabularx}{\linewidth}{| l | X |} +\hline +\textbf{Name} & \textbf{Description} \\ +\hline +name & Returns scenario name or empty string if scenario is unnamed\\ +\hline +description & Returns scenario description or empty string if scenario has no description\\ +\hline +author & Returns scenario author or empty string if no author specified\\ +\hline +seed & Returns scenario initial seed used by random generator\\ +\hline +day & Returns number of current day in game\\ +\hline +size & Returns scenario map size\\ +\hline +difficulty & Returns scenario map \hyperref[Difficulty]{difficulty} selected by player\\ +\hline +variables & Returns \hyperref[ScenarioVariables]{scenario variables}. If scenario has no variables defined, returns \texttt{nil}\\ +\hline +diplomacy & Returns object that holds \hyperref[Diplomacy]{diplomacy} relations between races. Fully loaded scenario always have diplomacy relations. During scenario loading this property can return \texttt{nil}\\ +\hline +\end{tabularx} +\end{center} + +\subsubsection{Methods} +\begin{center} +\begin{tabularx}{\linewidth}{| l | X |} +\hline +\textbf{Name} & \textbf{Description} \\ +\hline +getLocation('S143LO0001') & Searches for \hyperref[Location]{location} by id string or \hyperref[Id]{id}, returns \texttt{nil} if not found\\ +getLocation(Id.new('S143LO0001')) & \\ +\hline +getPlayer('S143PL0001') & Searches for \hyperref[Player]{player} by id string or \hyperref[Id]{id}, returns \texttt{nil} if not found\\ +getPlayer(Id.new('S143PL0001')) & \\ +\hline +getUnit('S143UN0007') & Searches for \hyperref[Unit]{unit} by id string or \hyperref[Id]{id}, returns \texttt{nil} if not found\\ +getUnit(Id.new('S143UN0007')) & \\ +\hline +getItem('S143IM0001') & Searches for \hyperref[Item]{item} by id string or \hyperref[Id]{id}, returns \texttt{nil} if not found\\ +getItem(Id.new('S143IM0001')) & \\ +\hline +getTile(3, 5) & Searches for \hyperref[Tile]{tile} by pair of coordinates or \hyperref[Point]{point}, returns \texttt{nil} if not found\\ +getTile(Point.new(3, 5)) &\\ +\hline +getStack(10, 15) & Searches for \hyperref[Stack]{stack} by pair of coordinates, \hyperref[Point]{point}, id string or \hyperref[Id]{id}, returns \texttt{nil} if not found\\ +getStack(Point.new(10, 15)) &\\ +getStack('S143KC0005') &\\ +getStack(Id.new('S143KC0005')) &\\ +\hline +getFort(10, 15) & Searches for \hyperref[Fort]{fort} by pair of coordinates, \hyperref[Point]{point}, id string or \hyperref[Id]{id}, returns \texttt{nil} if not found\\ +getFort(Point.new(10, 15)) &\\ +getFort('S143FT0005') &\\ +getFort(Id.new('S143FT0005')) &\\ +\hline +getRuin(10, 15) & Searches for \hyperref[Ruin]{ruin} by pair of coordinates, \hyperref[Point]{point}, id string or \hyperref[Id]{id}, returns \texttt{nil} if not found\\ +getRuin(Point.new(10, 15)) &\\ +getRuin('S143RU0000') &\\ +getRuin(Id.new('S143RU0000')) &\\ +\hline +getRod(10, 15) & Searches for \hyperref[Rod]{rod} by pair of coordinates, \hyperref[Point]{point}, id string or \hyperref[Id]{id}, returns \texttt{nil} if not found\\ +getRod(Point.new(10, 15)) &\\ +getRod('S143RD0003') &\\ +getRod(Id.new('S143RD0003')) &\\ +\hline +getCrystal(10, 15) & Searches for \hyperref[Crystal]{crystal} by pair of coordinates, \hyperref[Point]{point}, id string or \hyperref[Id]{id}, returns \texttt{nil} if not found\\ +getCrystal(Point.new(10, 15)) &\\ +getCrystal('S143CR0004') &\\ +getCrystal(Id.new('S143CR0004')) &\\ +\hline +getMerchant(10, 15) & Searches for \hyperref[Merchant]{merchant} by pair of coordinates, \hyperref[Point]{point}, id string or \hyperref[Id]{id}, returns \texttt{nil} if not found\\ +getMerchant(Point.new(10, 15)) &\\ +getMerchant('S143SI0002') &\\ +getMerchant(Id.new('S143SI0002')) &\\ +\hline +getMercenary(10, 15) & Searches for \hyperref[Mercenary]{mercenary camp} by pair of coordinates, \hyperref[Point]{point}, id string or \hyperref[Id]{id}, returns \texttt{nil} if not found\\ +getMercenary(Point.new(10, 15)) &\\ +getMercenary('S143SI0002') &\\ +getMercenary(Id.new('S143SI0002')) &\\ +\hline +getTrainer(10, 15) & Searches for \hyperref[Trainer]{trainer} by pair of coordinates, \hyperref[Point]{point}, id string or \hyperref[Id]{id}, returns \texttt{nil} if not found\\ +getTrainer(Point.new(10, 15)) &\\ +getTrainer('S143SI0002') &\\ +getTrainer(Id.new('S143SI0002')) &\\ +\hline +getMarket(10, 15) & Searches for \hyperref[ResourceMarket]{market} by pair of coordinates, \hyperref[Point]{point}, id string or \hyperref[Id]{id}, returns \texttt{nil} if not found\\ +getMarket(Point.new(10, 15)) &\\ +getMarket('S143SI0002') &\\ +getMarket(Id.new('S143SI0002')) &\\ +\hline +\end{tabularx} +\end{center} +\begin{center} +\begin{tabularx}{\linewidth}{| l | X |} +\hline +\textbf{Name} & \textbf{Description} \\ +\hline +findStackByUnit(unit) & Searches for \hyperref[Stack]{stack} that has specified \hyperref[Unit]{unit} among all the stacks in the whole scenario.\\ +findStackByUnit('S143UN0007') & You can also use unit id string or \hyperref[Id]{id}. Returns \texttt{nil} if not found.\\ +findStackByUnit(Id.new('S143UN0007')) & Note that \textbf{this search is heavy in terms of performance}, so you probably want to minimize excessive calls and use variables to store its results.\\ +\hline +findFortByUnit(unit) & Searches for \hyperref[Fort]{fort} that has specified \hyperref[Unit]{unit} in its garrison among all the forts in the whole scenario. Only garrison units are counted, visiting stack is ignored.\\ +findFortByUnit('S143UN0007') & You can also use unit id string or \hyperref[Id]{id}. Returns \texttt{nil} if not found.\\ +findFortByUnit(Id.new('S143UN0007')) & Note that \textbf{this search is heavy in terms of performance}, so you probably want to minimize excessive calls and use variables to store its results.\\ +\hline +findRuinByUnit(unit) & Searches for \hyperref[Ruin]{ruin} that has specified \hyperref[Unit]{unit} among all the forts in the whole scenario.\\ +findRuinByUnit('S143UN0007') & You can also use unit id string or \hyperref[Id]{id}. Returns \texttt{nil} if not found.\\ +findRuinByUnit(Id.new('S143UN0007')) & Note that \textbf{this search is heavy in terms of performance}, so you probably want to minimize excessive calls and use variables to store its results.\\ +\hline +forEachStack(f) & Searches for every \hyperref[Stack]{stack} on a map and calls specified function on it\\ +\hline +forEachLocation(f) & Searches for every \hyperref[Location]{location} on a map and calls specified function on it\\ +\hline +forEachFort(f) & Searches for every \hyperref[Fort]{fort} on a map and calls specified function on it\\ +\hline +forEachRuin(f) & Searches for every \hyperref[Ruin]{ruin} on a map and calls specified function on it\\ +\hline +forEachRod(f) & Searches for every \hyperref[Rod]{rod} on a map and calls specified function on it\\ +\hline +forEachPlayer(f) & Searches for every \hyperref[Player]{player} on a map and calls specified function on it\\ +\hline +forEachUnit(f) & Searches for every \hyperref[Unit]{unit} on a map and calls specified function on it\\ +\hline +forEachCrystal(f) & Searches for every \hyperref[Crystal]{crystal} on a map and calls specified function on it\\ +\hline +forEachMerchant(f) & Searches for every \hyperref[Merchant]{merchant} on a map and calls specified function on it\\ +\hline +forEachMercenary(f) & Searches for every \hyperref[Mercenary]{mercenary camp} on a map and calls specified function on it\\ +\hline +forEachTrainer(f) & Searches for every \hyperref[Trainer]{trainer} on a map and calls specified function on it\\ +\hline +forEachMarket(f) & Searches for every \hyperref[ResourceMarket]{market} on a map and calls specified function on it\\ +\hline +\end{tabularx} +\end{center} +\newpage +\subsubsection{Examples} +Search for \hyperref[Location]{location} by \hyperref[Id]{id}, check if found: +\begin{center} +\begin{lstlisting}[language=Lua] +local location = scenario:getLocation('S143LO0001') +if not location then + return +end +\end{lstlisting} +\end{center} +Access \hyperref[ScenarioVariables]{scenario variables}. Check for \texttt{nil} in case when no variables defined: +\begin{center} +\begin{lstlisting}[language=Lua] +local variables = scenario.variables +if not variables then + return +end + +local v = variables:getVariable('VAR1') +\end{lstlisting} +\end{center} +Count neutral \hyperref[Stack]{stacks} on map. \texttt{forEachStack} visits each stack on a map and calls provided function which takes visited \hyperref[Stack]{stack} as its argument: +\begin{center} +\begin{lstlisting}[language=Lua] +local count = 0 + +scenario:forEachStack(function (stack) + if stack.owner.race == Race.Neutral then + count = count + 1 + end +end) +\end{lstlisting} +\end{center} +The same logic can be implemented by storing function that count stacks in a variable: +\begin{center} +\begin{lstlisting}[language=Lua] +local count = 0 + +local countNeutralStacks = function (stack) + if stack.owner.race == Race.Neutral then + count = count + 1 + end +end + +scenario:forEachStack(countNeutralStacks) +\end{lstlisting} +\end{center} \ No newline at end of file diff --git a/ApiDocs/en/scenariovariable.tex b/ApiDocs/en/scenariovariable.tex new file mode 100644 index 00000000..6cd45879 --- /dev/null +++ b/ApiDocs/en/scenariovariable.tex @@ -0,0 +1,15 @@ +\subsection{Scenario variable} +\label{ScenarioVariable} +Represents scenario variable used by events. +\subsubsection{Properties} +\begin{center} +\begin{tabularx}{\linewidth}{| l | X |} +\hline +\textbf{Name} & \textbf{Description} \\ +\hline +name & Returns variable name\\ +\hline +value & Returns variable value\\ +\hline +\end{tabularx} +\end{center} \ No newline at end of file diff --git a/ApiDocs/en/scenariovariables.tex b/ApiDocs/en/scenariovariables.tex new file mode 100644 index 00000000..a2c06e15 --- /dev/null +++ b/ApiDocs/en/scenariovariables.tex @@ -0,0 +1,25 @@ +\subsection{Scenario variables} +\label{ScenarioVariables} +Stores \hyperref[ScenarioVariable]{scenario variables}, allows searching them by name. +\subsubsection{Methods} +\begin{center} +\begin{tabularx}{\linewidth}{| l | X |} +\hline +\textbf{Name} & \textbf{Description} \\ +\hline +getVariable('name') & Searches for \hyperref[ScenarioVariable]{scenario variable} by its name, returns \texttt{nil} if not found\\ +\hline +\end{tabularx} +\end{center} +\subsubsection{Examples} +\begin{center} +\begin{lstlisting}[language=Lua] +local variable = variables:getVariable('VAR1') +if variable == nil then + return +end + +-- s == 'VAR1' +local s = variable.name +\end{lstlisting} +\end{center} \ No newline at end of file diff --git a/ApiDocs/en/stack.tex b/ApiDocs/en/stack.tex new file mode 100644 index 00000000..b4b56813 --- /dev/null +++ b/ApiDocs/en/stack.tex @@ -0,0 +1,49 @@ +\subsection{Stack} +\label{Stack} +Represents \hyperref[Group]{group} of 6 unit \hyperref[UnitSlot]{slots} on a map. One of the units is a leader. +\subsubsection{Properties} +\begin{center} +\begin{tabularx}{\linewidth}{| l | X |} +\hline +\textbf{Name} & \textbf{Description} \\ +\hline +id & Returns stack \hyperref[Id]{id}. The value is unique for every stack on scenario map\\ +\hline +position & Returns stack position as a \hyperref[Point]{point}\\ +\hline +owner & Returns \hyperref[Player]{player} that owns the stack. Neutral stacks are owned by neutral player\\ +\hline +inside & Returns \hyperref[Fort]{fort} that this stack is visiting or \texttt{nil} if none\\ +\hline +group & Returns stack units as a \hyperref[Group]{group}\\ +\hline +leader & Returns stack leader \hyperref[Unit]{unit}\\ +\hline +inventory & Returns array of inventory \hyperref[Item]{items}. This includes equipped items\\ +\hline +subrace & Returns stack \hyperref[SubraceCategory]{subrace}\\ +\hline +order & Returns stack \hyperref[OrderCategory]{order}\\ +\hline +orderTargetId & Returns stack's order target \hyperref[Id]{id}\\ +\hline +aiOrder & Returns stack AI \hyperref[OrderCategory]{order}\\ +\hline +movement & Returns stack current movement points\\ +\hline +invisible & Returns \texttt{true} if stack is invisible\\ +\hline +battlesWon & Returns number of battles won by the stack\\ +\hline +\end{tabularx} +\end{center} +\subsubsection{Methods} +\begin{center} +\begin{tabularx}{\linewidth}{| l | X |} +\hline +\textbf{Name} & \textbf{Description} \\ +\hline +getEquippedItem(Equipment.Boots) & Returns equipped \hyperref[Item]{item} by \hyperref[EquipmentCategory]{equipment} value\\ +\hline +\end{tabularx} +\end{center} \ No newline at end of file diff --git a/ApiDocs/en/standaloneFuncs.tex b/ApiDocs/en/standaloneFuncs.tex new file mode 100644 index 00000000..67e4cabb --- /dev/null +++ b/ApiDocs/en/standaloneFuncs.tex @@ -0,0 +1,45 @@ +\subsection{Standalone functions} +\subsubsection{Functions} +\begin{center} +\begin{tabularx}{\linewidth}{| l | X |} +\hline +\textbf{Name} & \textbf{Description} \\ +\hline +log('') & Writes message to \texttt{luaDebug.log} file when \texttt{debugHooks} is set to \texttt{true} in \texttt{settings.lua}.\\ +\hline +getScenario() & Returns current scenario. The function only accessible to scripts where scenario access is appropriate: +\begin{itemize} +\item summon.lua +\item doppelganger.lua +\item transformSelf.lua +\item transformOther.lua +\item drainLevel.lua +\item custom attack reach scripts +\item custom unit modifier script +\end{itemize} +\\ +\hline +randomNumber(maxValue) & Generates random number in range \texttt{[0 : maxValue)} using ingame generator.\\ +\hline +getGlobal() & Returns \hyperref[Global]{global data storage} used by game\\ +\hline +getGame() & Returns \hyperref[Game]{game} restrictions and constants\\ +\hline +\end{tabularx} +\end{center} +\subsubsection{Examples} +\begin{center} +\begin{lstlisting}[language=Lua] +local unit = getScenario():getUnit(unitId) +log('Unit current level:' .. unit.impl.level) + +local n = randomNumber(100) + +local data = getGlobal() +-- Access contents of GVars.dbf +local variables = data.variables + +-- Access unit maximum damage, currently set in game settings +getGame().unitMaxDamage +\end{lstlisting} +\end{center} \ No newline at end of file diff --git a/ApiDocs/en/summonExample.tex b/ApiDocs/en/summonExample.tex new file mode 100644 index 00000000..29d22257 --- /dev/null +++ b/ApiDocs/en/summonExample.tex @@ -0,0 +1,21 @@ +\subsection{summon.lua} +\texttt{summoner} has type \hyperref[Unit]{Unit}. \texttt{summonImpl} is \hyperref[UnitImpl]{Unit implementation}. \texttt{item} is \hyperref[Item]{Item} used to perform the attack. \texttt{battle} specifies an information about current \hyperref[Battle]{battle}. +\begin{center} +\begin{lstlisting}[language=Lua] +function getLevel(summoner, summonImpl, item, battle) + -- Use base level of summon if cheap item is used to summon it + if item and item.base.value.gold < 500 then + return summonImpl.level + end + + -- Summon unit with level twice as big as summoner level + -- or with level of summon implementation, whichever is bigger. + local impl = summoner.impl + local summonerLevel = impl.level + + local summonLevel = summonImpl.level + + return math.max(summonerLevel * 2, summonLevel) +end +\end{lstlisting} +\end{center} \ No newline at end of file diff --git a/ApiDocs/en/tile.tex b/ApiDocs/en/tile.tex new file mode 100644 index 00000000..92c5a986 --- /dev/null +++ b/ApiDocs/en/tile.tex @@ -0,0 +1,15 @@ +\subsection{Tile} +\label{Tile} +Represents map tile. +\subsubsection{Properties} +\begin{center} +\begin{tabularx}{\linewidth}{| l | X |} +\hline +\textbf{Name} & \textbf{Description} \\ +\hline +terrain & Returns tile \hyperref[TerrainCategory]{terrain type}\\ +\hline +ground & Returns tile \hyperref[GroundCategory]{ground type}\\ +\hline +\end{tabularx} +\end{center} \ No newline at end of file diff --git a/ApiDocs/en/trainer.tex b/ApiDocs/en/trainer.tex new file mode 100644 index 00000000..50c32e92 --- /dev/null +++ b/ApiDocs/en/trainer.tex @@ -0,0 +1,17 @@ +\subsection{Trainer} +\label{Trainer} +Represents Trainer on a map. +\subsubsection{Properties} +\begin{center} +\begin{tabularx}{\linewidth}{| l | X |} +\hline +\textbf{Name} & \textbf{Description} \\ +\hline +id & Returns trainer \hyperref[Id]{id}. The value is unique for every trainer on scenario map\\ +\hline +position & Returns trainer position as a \hyperref[Point]{point}\\ +\hline +visitors & Returns list of \hyperref[Player]{players} that have visited the trainer\\ +\hline +\end{tabularx} +\end{center} \ No newline at end of file diff --git a/ApiDocs/en/transformOtherExample.tex b/ApiDocs/en/transformOtherExample.tex new file mode 100644 index 00000000..348dc8b3 --- /dev/null +++ b/ApiDocs/en/transformOtherExample.tex @@ -0,0 +1,10 @@ +\subsection{transformOther.lua} +\texttt{attacker} and \texttt{target} have type \hyperref[Unit]{Unit}. \texttt{transformImpl} is \hyperref[UnitImpl]{Unit implementation}. \texttt{item} is \hyperref[Item]{Item} used to perform the attack. \texttt{battle} specifies information about \hyperref[Battle]{battle} state. +\begin{center} +\begin{lstlisting}[language=Lua] +function getLevel(attacker, target, transformImpl, item, battle) + -- transform using target level with a minimum of transform impl level + return math.max(target.impl.level, transformImpl.level); +end +\end{lstlisting} +\end{center} \ No newline at end of file diff --git a/ApiDocs/en/transformSelfExample.tex b/ApiDocs/en/transformSelfExample.tex new file mode 100644 index 00000000..2f31a18e --- /dev/null +++ b/ApiDocs/en/transformSelfExample.tex @@ -0,0 +1,10 @@ +\subsection{transformSelf.lua} +\texttt{unit} has type \hyperref[Unit]{Unit}. \texttt{transformImpl} is \hyperref[UnitImpl]{Unit implementation}. \texttt{item} is \hyperref[Item]{Item} used to perform the attack. \texttt{battle} specifies an information about current \hyperref[Battle]{battle}. +\begin{center} +\begin{lstlisting}[language=Lua] +function getLevel(unit, transformImpl, item, battle) + -- Transform into current level or level of resulting unit's template, whichever is bigger. + return math.max(unit.impl.level, transformImpl.level) +end +\end{lstlisting} +\end{center} \ No newline at end of file diff --git a/ApiDocs/en/types.tex b/ApiDocs/en/types.tex new file mode 100644 index 00000000..fd14561e --- /dev/null +++ b/ApiDocs/en/types.tex @@ -0,0 +1,78 @@ +\section{Data types} + +\input{point.tex} +\newpage +\input{id.tex} +\newpage +\input{currency.tex} +\newpage +\input{global.tex} +\newpage +\input{globalvariables.tex} +\newpage +\input{game.tex} +\newpage +\input{modifier.tex} +\newpage +\input{dynamicupgrade.tex} +\newpage +\input{itembase.tex} +\newpage +\input{item.tex} +\newpage +\input{building.tex} +\newpage +\input{attack.tex} +\newpage +\input{unit.tex} +\newpage +\input{unitdummy.tex} +\newpage +\input{unitimpl.tex} +\newpage +\input{unitslot.tex} +\newpage +\input{group.tex} +\newpage +\input{fog.tex} +\newpage +\input{player.tex} +\newpage +\input{stack.tex} +\newpage +\input{fort.tex} +\newpage +\input{merchantitem.tex} +\newpage +\input{merchant.tex} +\newpage +\input{mercenaryunit.tex} +\newpage +\input{mercenary.tex} +\newpage +\input{trainer.tex} +\newpage +\input{resourcemarket.tex} +\newpage +\input{ruin.tex} +\newpage +\input{rod.tex} +\newpage +\input{crystal.tex} +\newpage +\input{location.tex} +\newpage +\input{scenariovariable.tex} +\newpage +\input{scenariovariables.tex} +\newpage +\input{tile.tex} +\newpage +\input{diplomacy.tex} +\newpage +\input{scenario.tex} +\newpage +\input{battleturn.tex} +\newpage +\input{battle.tex} +\newpage \ No newline at end of file diff --git a/ApiDocs/en/unit.tex b/ApiDocs/en/unit.tex new file mode 100644 index 00000000..b332ddcc --- /dev/null +++ b/ApiDocs/en/unit.tex @@ -0,0 +1,29 @@ +\subsection{Unit} +\label{Unit} +Represents game unit that participates in a battle, takes damage and performs attacks. Unit can also be a leader. Leaders are main units in stacks. +\subsubsection{Properties} +\begin{center} +\begin{tabularx}{\linewidth}{| l | X |} +\hline +\textbf{Name} & \textbf{Description} \\ +\hline +id & Returns unit \hyperref[Id]{id}. This is different to id of \hyperref[UnitImpl]{unit implementation}. The value is unique for every unit on \hyperref[Scenario]{scenario map}\\ +\hline +hp & Returns unit's current hit points\\ +\hline +hpMax & Returns unit's maximum hit points\\ +\hline +xp & Returns unit's current experience points\\ +\hline +impl & Returns unit's current \hyperref[UnitImpl]{implementation}. Current implementation describes unit stats according to its levels and possible transformations applied during battle\\ +\hline +baseImpl & Returns unit's base \hyperref[UnitImpl]{implementation}. Base implementation is a record in \texttt{GUnits.dbf} that describes unit basic stats\\ +\hline +leveledImpl & Returns unit's leveled (generated) \hyperref[UnitImpl]{implementation}. Leveled implementation is unit's current implementation without modifiers, or base implementation plus upgrades from \texttt{GDynUpgr.dbf} according to unit's level. This does not include leader upgrades from \texttt{GleaUpg.dbf}, because the upgrades are modifiers\\ +\hline +original & Returns original unit \hyperref[UnitDummy]{dummy} that represents unit state before transformation, or \texttt{nil} if unit is not transformed. The state does not include any unit modifiers thus contains only leveled implementation. Unit can be transformed by transform-self, transform-other, drain-level or doppelganger attack\\ +\hline +originalModifiers & Returns array of original \hyperref[Modifier]{modifiers} that were applied to unit before transformation, or empty array if unit is not transformed. Usually, modifiers are reapplied after transformation, but there are cases where some modifiers are incompatible with a new form, thus not getting applied to it\\ +\hline +\end{tabularx} +\end{center} \ No newline at end of file diff --git a/ApiDocs/en/unitdummy.tex b/ApiDocs/en/unitdummy.tex new file mode 100644 index 00000000..3594912b --- /dev/null +++ b/ApiDocs/en/unitdummy.tex @@ -0,0 +1,25 @@ +\subsection{Unit dummy} +\label{UnitDummy} +Represents preserved state of game \hyperref[Unit]{unit}. Used, for instance, to preserve unit state before transformation, so it can be restored later. Properties are identical to unit, except that there is no \texttt{original} and \texttt{originalModifiers}. +\subsubsection{Properties} +\begin{center} +\begin{tabularx}{\linewidth}{| l | X |} +\hline +\textbf{Name} & \textbf{Description} \\ +\hline +id & Returns unit \hyperref[Id]{id}. This is different to id of \hyperref[UnitImpl]{unit implementation}. The value is unique for every unit on \hyperref[Scenario]{scenario map}\\ +\hline +hp & Returns unit's current hit points\\ +\hline +hpMax & Returns unit's maximum hit points\\ +\hline +xp & Returns unit's current experience points\\ +\hline +impl & Returns unit's current \hyperref[UnitImpl]{implementation}. Current implementation describes unit stats according to its levels and possible transformations applied during battle\\ +\hline +baseImpl & Returns unit's base \hyperref[UnitImpl]{implementation}. Base implementation is a record in \texttt{GUnits.dbf} that describes unit basic stats\\ +\hline +leveledImpl & Returns unit's leveled (generated) \hyperref[UnitImpl]{implementation}. Leveled implementation is unit's current implementation without modifiers, or base implementation plus upgrades from \texttt{GDynUpgr.dbf} according to unit's level. This does not include leader upgrades from \texttt{GleaUpg.dbf}, because the upgrades are modifiers\\ +\hline +\end{tabularx} +\end{center} \ No newline at end of file diff --git a/ApiDocs/en/unitimpl.tex b/ApiDocs/en/unitimpl.tex new file mode 100644 index 00000000..58390621 --- /dev/null +++ b/ApiDocs/en/unitimpl.tex @@ -0,0 +1,115 @@ +\subsection{Unit implementation} +\label{UnitImpl} +Represents \hyperref[Unit]{unit} template. Records in \texttt{GUnits.dbf} are unit implementations. +\subsubsection{How unit implementation works and why it is different to unit} +Unit implementation is a unit template that can be used by different individual units on scenario map. It is different to unit, because different unit instances can have the same implementation. For example, you can have 3 Squires of level 1 in your party, each having the same implementation.\\ +There are 3 different stages of unit implementation that build on top of each other: +\begin{itemize} +\item \texttt{Global}, corresponds to a record from \texttt{GUnits.dbf}; +\begin{itemize} +\item Returned by \hyperref[Unit]{unit}.\texttt{baseImpl} or \hyperref[UnitImpl]{impl}.\texttt{global}; +\item Its \hyperref[Id]{id} corresponds to \texttt{UNIT\_ID} from \texttt{GUnits.dbf}. +\end{itemize} +\item \texttt{Generated}, equals \texttt{Global} plus level upgrades from \texttt{GDynUpgr.dbf} (if any); +\begin{itemize} +\item Returned by \hyperref[Unit]{unit}.\texttt{leveledImpl} or \hyperref[UnitImpl]{impl}.\texttt{generated}; +\item Its \hyperref[Id]{id} is different to id of inherited \texttt{Global} implementation; +\item If unit has no level upgrades, \hyperref[Unit]{unit}.\texttt{leveledImpl}/\hyperref[UnitImpl]{impl}.\texttt{generated} equals \hyperref[Unit]{unit}.\texttt{baseImpl}/\hyperref[UnitImpl]{impl}.\texttt{global}. +\end{itemize} +\item \texttt{Modified}, equals \texttt{Generated} plus applied modifiers from \texttt{Gmodif.dbf} (if any). +\begin{itemize} +\item Returned by \hyperref[Unit]{unit}.\texttt{impl}; +\item Its \hyperref[Id]{id} equals to id of inherited \texttt{Generated} implementation; +\item If unit has no modifiers, \hyperref[Unit]{unit}.\texttt{impl} equals \hyperref[Unit]{unit}.\texttt{leveledImpl}/\hyperref[UnitImpl]{impl}.\texttt{generated}. +\end{itemize} +\end{itemize} +\subsubsection{Unit implementation changes} +Unit implementation changes when \hyperref[Unit]{unit}: +\begin{itemize} +\item Gets an upgrade, does not matter if it transforms to higher tier unit or simply gets over-level; +\item Gets transformed: by Transform-Self, Transform-Other, Drain-Level, or Doppelganger attack; +\item Gets \hyperref[Modifier]{modified}: when consuming a potion, affected by a spell, equipping an item, getting a leader upgrade, etc.; +\end{itemize} +\newpage +\subsubsection{Properties} +\begin{center} +\begin{tabularx}{\linewidth}{| l | X |} +\hline +\textbf{Name} & \textbf{Description} \\ +\hline +id & Returns unit implementation \hyperref[Id]{id}. \texttt{UNIT\_ID} value from \texttt{GUnits.dbf}\\ +\hline +base & Returns base unit implementation. \texttt{BASE\_UNIT} value from \texttt{GUnits.dbf}\\ +\hline +type & Returns unit \hyperref[UnitCategory]{type}\\ +\hline +leaderType & Returns \hyperref[LeaderCategory]{leader type} (or -1 if unit is not a leader)\\ +\hline +global & Returns global unit implementation - a record from \texttt{GUnits.dbf}. Same as \hyperref[Unit]{unit}.\texttt{baseImpl}\\ +\hline +generated & Returns generated unit implementation. Equals global plus upgrades from \texttt{GDynUpgr.dbf} according to unit's level. Same as \hyperref[Unit]{unit}.\texttt{leveledImpl}\\ +\hline +modifiers & Returns array of applied \hyperref[Modifier]{modifiers}\\ +\hline +level & Returns unit's implementation level. \texttt{LEVEL} value from \texttt{GUnits.dbf}\\ +\hline +xpNext & Returns experience points needed for next level. \texttt{XP\_NEXT} value from \texttt{GUnits.dbf}\\ +\hline +xpKilled & Returns experience points granted for killing the unit. \texttt{XP\_KILLED} value from \texttt{GUnits.dbf}\\ +\hline +hp & Returns unit's hit points. \texttt{HIT\_POINT} value from \texttt{GUnits.dbf}\\ +\hline +armor & Returns unit's armor. \texttt{ARMOR} value from \texttt{GUnits.dbf}\\ +\hline +regen & Returns unit's regen. \texttt{REGEN} value from \texttt{GUnits.dbf}\\ +\hline +race & Returns unit's race. \texttt{ID} value from \texttt{LRace.dbf}. See \hyperref[RaceCategory]{Race enumeration} for all possible values\\ +\hline +subrace & Returns unit's subrace. \texttt{ID} value from \texttt{LSubRace.dbf}. See \hyperref[SubraceCategory]{Subrace enumeration} for all possible values\\ +\hline +small & Indicates if the unit is small (occupies single slot). \texttt{SIZE\_SMALL} value from \texttt{GUnits.dbf}\\ +\hline +male & Indicates if the unit is male. \texttt{SEX\_M} value from \texttt{GUnits.dbf}\\ +\hline +waterOnly & Indicates if the unit is water only. \texttt{WATER\_ONLY} value from \texttt{GUnits.dbf}\\ +\hline +attacksTwice & Indicates if the unit attacks twice. \texttt{ATCK\_TWICE} value from \texttt{GUnits.dbf}\\ +\hline +dynUpgLvl & Returns level after which \texttt{dynUpg2} rules are applied. \texttt{DYN\_UPG\_LV} from \texttt{GUnits.dbf}\\ +\hline +dynUpg1 & Returns dynamic upgrade 1\\ +\hline +dynUpg2 & Returns dynamic upgrade 2\\ +\hline +attack1 & Returns primary \hyperref[Attack]{attack} or \texttt{nil} if no primary attack used\\ +\hline +attack2 & Returns secondary \hyperref[Attack]{attack} or \texttt{nil} if no secondary attack used\\ +\hline +altAttack & Returns alternative \hyperref[Attack]{attack} or \texttt{nil} if no alternative attack used\\ +\hline +movement & Returns leader maximum movement points (or 0 if unit is not a leader)\\ +\hline +scout & Returns leader scouting range (or 0 if unit is not a leader)\\ +\hline +leadership & Returns current leadership value (or 0 if unit is not a leader)\\ +\hline +\end{tabularx} +\end{center} +\subsubsection{Methods} +\begin{center} +\begin{tabularx}{\linewidth}{| l | X |} +\hline +\textbf{Name} & \textbf{Description} \\ +\hline +hasAbility(Ability.OrbUse) & Returns \texttt{true} if leader has specified \hyperref[AbilityCategory]{ability} (or false if unit is not a leader)\\ +\hline +hasMoveBonus(Ground.Water) & Returns \texttt{true} if leader has movement bonus on specified \hyperref[GroundCategory]{ground} (or false if unit is not a leader)\\ +\hline +hasModifier('G000UM5021') & Returns \texttt{true} if the implementation has modifier specified by id or id string\\ +\hline +getImmuneToAttackClass(Attack.Paralyze) & Returns \hyperref[ImmuneCategory]{immune type} for specified \hyperref[AttackCategory]{attack type}\\ +\hline +getImmuneToAttackSource(Source.Water) & Returns \hyperref[ImmuneCategory]{immune type} for specified \hyperref[SourceCategory]{attack source type}\\ +\hline +\end{tabularx} +\end{center} \ No newline at end of file diff --git a/ApiDocs/en/unitslot.tex b/ApiDocs/en/unitslot.tex new file mode 100644 index 00000000..500ec379 --- /dev/null +++ b/ApiDocs/en/unitslot.tex @@ -0,0 +1,62 @@ +\subsection{Unit slot} +\label{UnitSlot} +Represents one of the twelve unit slots on battlefield. Unit positions on a battlefield are mirrored. Frontline positions are even, backline - odd. +\begin{center} +\begin{tabular}{c c c} +Attacker & & Defender\\ +% Attacker group represented as a table +\begin{tabular}{| c | c |} +\hline +1 & 0\\ +\hline +3 & 2\\ +\hline +5 & 4\\ +\hline +\end{tabular} & +VS & +% Defender group +\begin{tabular}{| c | c |} +\hline +0 & 1\\ +\hline +2 & 3\\ +\hline +4 & 5\\ +\hline +\end{tabular} \\ + +\end{tabular} +\end{center} + +\subsubsection{Properties} +\begin{center} +\begin{tabularx}{\linewidth}{| l | X |} +\hline +\textbf{Name} & \textbf{Description} \\ +\hline +unit & Returns unit that occupies the slot or \texttt{nil} if slot is empty\\ +\hline +position & Returns position of the slot (0-5)\\ +\hline +line & Returns line index of the slot: 0 - frontline, 1 - backline\\ +\hline +column & Returns column index of the slot: 0 - top, 1 - middle, 2 - bottom\\ +\hline +frontline & Returns \texttt{true} if slot is on the frontline\\ +\hline +backline & Returns \texttt{true} if slot is on the backline\\ +\hline +\end{tabularx} +\end{center} + +\subsubsection{Methods} +\begin{center} +\begin{tabularx}{\linewidth}{| l | X |} +\hline +\textbf{Name} & \textbf{Description} \\ +\hline +distance(otherSlot) & Returns distance between two slots (used for adjacent slot calculations)\\ +\hline +\end{tabularx} +\end{center} \ No newline at end of file diff --git a/D2RSG b/D2RSG index 01446029..63970135 160000 --- a/D2RSG +++ b/D2RSG @@ -1 +1 @@ -Subproject commit 01446029a7da5f0fafd4ee8507ced79e333cb0c4 +Subproject commit 63970135152b54a94a8e115135b737e0d26bf44f diff --git a/Scripts/battleAi.lua b/Scripts/battleAi.lua new file mode 100644 index 00000000..dbfccd75 --- /dev/null +++ b/Scripts/battleAi.lua @@ -0,0 +1,3092 @@ +-- Returns true if table 't' contains specified value +function hasValue(t, value) + for index, v in ipairs(t) do + if v == value then + return true + end + end + + return false +end + +-- Returns true if unit is alive +function isUnitAlive(unit) + return unit.hp > 0 +end + +-- Returns true if unit primary attack inflicts damage +function isUnitAttackInflictsDamage(unit) + if not isUnitAlive(unit) then + return false + end + + local impl = unit.impl + local attack = impl.attack1 + local attackClass = attack.type + + return attackClass == Attack.Damage + or attackClass == Attack.Drain + or attackClass == Attack.DrainOverflow + or attackClass == Attack.Poison + or attackClass == Attack.Blister + or attackClass == Attack.Frostbite +end + +-- Returns true if at least one unit from 'other group' is not always immune to attacks of units in 'group' +function groupCanInflictDamageToOther(group, otherGroup) + local otherGroupUnits = otherGroup.units + if #otherGroupUnits <= 0 then + return true + end + + local groupUnits = group.units + if #groupUnits <= 0 then + return false + end + + for i = 1, #groupUnits do + local unit = groupUnits[i] + if isUnitAttackInflictsDamage(unit) then + local impl = unit.impl + local attack = impl.attack1 + local attackClass = attack.type + local attackSource = attack.source + + for j = 1, #otherGroupUnits do + local otherUnit = otherGroupUnits[j] + local otherUnitImpl = otherUnit.impl + + if otherUnitImpl:getImmuneToAttackClass(attackClass) ~= Immune.Always + and otherUnitImpl:getImmuneToAttackSource(attackSource) ~= Immune.Always + then + return true + end + end + end + end + + return false +end + +function getCityProtection(group, ignoreCityProtection) + local groupId = group.id + local groupType = group.id.type + local fort = nil + + if groupType == IdType.Stack then + fort = getScenario():getStack(groupId).inside + elseif groupType == IdType.Fortification then + fort = getScenario():getFort(groupId) + end + + if fort == nil or ignoreCityProtection then + return 0 + end + + if fort.capital then + return getGlobal().variables.protCap + end + + return getGlobal().variables:prot(fort.tier) +end + +function computeImmuneCoefficient(impl) + local coeff = 0.0 + + if impl:getImmuneToAttackSource(Source.Weapon) ~= Immune.NotImmune then + coeff = coeff + 57.0 + end + + if impl:getImmuneToAttackSource(Source.Mind) ~= Immune.NotImmune then + coeff = coeff + 5.0 + end + + if impl:getImmuneToAttackSource(Source.Life) ~= Immune.NotImmune then + coeff = coeff + 6.0 + end + + if impl:getImmuneToAttackSource(Source.Death) ~= Immune.NotImmune then + coeff = coeff + 10.0 + end + + if impl:getImmuneToAttackSource(Source.Fire) ~= Immune.NotImmune then + coeff = coeff + 9.0 + end + + if impl:getImmuneToAttackSource(Source.Water) ~= Immune.NotImmune then + coeff = coeff + 2.0 + end + + if impl:getImmuneToAttackSource(Source.Air) ~= Immune.NotImmune then + coeff = coeff + 5.0 + end + + if impl:getImmuneToAttackSource(Source.Earth) ~= Immune.NotImmune then + coeff = coeff + 3.0 + end + + if impl:getImmuneToAttackClass(Attack.Poison) ~= Immune.NotImmune then + coeff = coeff + 5.0 + end + + return coeff +end + +function computePowerCoefficient(impl) + local attack = impl.attack1 + local attackClass = attack.type + + if attackClass == Attack.Heal + or attackClass == Attack.BoostDamage + or attackClass == Attack.Cure + or attackClass == Attack.Summon + or attackClass == Attack.GiveAttack + or attackClass == Attack.Doppelganger + then + return 100.0 + end + + return attack.power +end + +function computeAttackClassCoefficient(impl, ignoreCityProtection) + local attack = impl.attack1 + local attackClass = attack.type + + if attackClass == Attack.Paralyze or attackClass == Attack.Petrify then + return 30.0 + end + + if attackClass == Attack.Damage then + return 1.0 + end + + if attackClass == Attack.Drain then + return 1.5 + end + + if attackClass == Attack.Heal then + return 1.0 + end + + if attackClass == Attack.Fear then + return 30.0 + end + + if attackClass == Attack.BoostDamage or attackClass == Attack.BestowWards then + return 40.0 + end + + if attackClass == Attack.Shatter then + return 30.0 + end + + if attackClass == Attack.LowerDamage or attackClass == Attack.LowerInitiative then + return 40.0 + end + + if attackClass == Attack.DrainOverflow then + return 2.0 + end + + if attackClass == Attack.Summon then + return 200.0 + end + + if attackClass == Attack.DrainLevel then + return 100.0 + end + + if attackClass == Attack.GiveAttack then + return 50.0 + end + + if attackClass == Attack.Doppelganger then + if ignoreCityProtection == false then + return 200.0 + end + + return 100.0 + end + + if attackClass == Attack.TransformSelf then + return 100.0 + end + + if attackClass ~= Attack.TransformOther then + return 1.0 + end + + if attack.reach == Reach.All then + return 100.0 + end + + return 60.0 +end + +function computeAttackReachCoefficient(impl, enemiesCount) + local attack = impl.attack1 + local reach = attack.reach + + if reach == Reach.Adjacent then + return 1.0 + end + + if reach == Reach.Any then + return 1.5 + end + + if reach ~= Reach.All then + return 1.0 + end + + return (enemiesCount - 1) * 0.4 + 1.0 +end + +function getMaxDamage(unitOrItemId, impl) + local t = unitOrItemId.type + + if t == IdType.UnitGlobal or t == IdType.UnitGenerated then + local id + if t == IdType.UnitGenerated then + id = impl.global.id + else + id = impl.id + end + + local idx = id.typeIndex + + -- Check IDs of leaders who can have 'Heavy Strike' ability + if idx == 0x19 + or idx == 0x20 + or idx == 0x44 + or idx == 0x45 + or idx == 0x47 + or idx == 0x70 + or idx == 0x71 + or idx == 0x96 + or idx == 0x8009 + or idx == 0x8011 + then + return getGame().unitMaxDamage + getGame().leaderAdditionalDamage + end + end + + return getGame().unitMaxDamage +end + +function getUnitOrItemMaxDamage(unitOrItemId) + if unitOrItemId.type ~= IdType.Unit then + return getMaxDamage(unitOrItemId, nil) + end + + local unit = getScenario():getUnit(unitOrItemId) + local impl = unit.impl + return getMaxDamage(impl.id, impl) +end + +function computeDamageDrainHealCoefficient(unit, impl) + local attack = impl.attack1 + local attackClass = attack.type + + if attackClass == Attack.Damage + or attackClass == Attack.Drain + or attackClass == Attack.DrainOverflow + then + local damage = attack.damage + local maxDamage = getMaxDamage(unit.impl.id, unit.impl) + + return math.min(damage, maxDamage) + elseif attackClass == Attack.Heal then + return attack.heal + else + return 1.0 + end +end + +function computeSecondAttackCoefficient(impl) + local attack = impl.attack2 + if attack == nil then + return 1.0 + end + + local attackClass = attack.type + + if attackClass == Attack.Cure then + return 1.0 + end + + if attackClass == Attack.Poison + or attackClass == Attack.Frostbite + or attackClass == Attack.Blister + then + return 1.15 + end + + if attackClass == Attack.Revive then + return 3.0 + end + + return 1.0 +end + +function computeUnitCoefficient(unit, ignoreCityProtection, battle, cityArmor, enemiesCount, a6) + if isUnitAlive(unit) == false and a6 == false then + return 0.0 + end + + local impl + if battle ~= nil and battle:getUnitStatus(unit.id, BattleStatus.Transform) then + impl = unit.original.impl + else + impl = unit.impl + end + + local attackInfinite = impl.attack1.infinite + local hasSecondAttack = impl.attack2 ~= nil + + local unitHp + if a6 then + unitHp = unit.hpMax + else + unitHp = unit.hp + end + + local armor = impl.armor + local unitArmor = armor + cityArmor + + local immuneCoeff = computeImmuneCoefficient(impl) + local powerCoeff = computePowerCoefficient(impl) + local attackClassCoeff = computeAttackClassCoefficient(impl, ignoreCityProtection) + local attackReachCoeff = computeAttackReachCoefficient(impl, enemiesCount) + local dmgDrainHealCoeff = computeDamageDrainHealCoefficient(unit, impl) + local unitMaxArmor = getGame().unitMaxArmor + + if unitArmor > unitMaxArmor then + unitArmor = unitMaxArmor + end + + local v17 = unitHp / (60.0 - 60.0 * unitArmor * 0.01) + + local coeff = v17 / (unitHp / 60.0) + * (powerCoeff + * 0.01 + * dmgDrainHealCoeff + * attackReachCoeff + * attackClassCoeff + * ((immuneCoeff * 0.01 + 1.0) * unitHp)) + * 0.01; + + if attackInfinite then + coeff = coeff * 1.25 + elseif hasSecondAttack then + coeff = computeSecondAttackCoefficient(impl) * coeff + end + + if impl.attacksTwice then + coeff = coeff * 2.0 + end + + return coeff +end + +function computeGroupCoefficient(group, ignoreCityProtection, enemiesCount, battle, ignoreCityProtection2, a8) + local cityArmor = 0 + if ignoreCityProtection2 == false then + cityArmor = getCityProtection(group, ignoreCityProtection) + end + + local coeff = 0.0 + local units = group.units + for i = 1, #units do + local unit = units[i] + if isUnitAlive(unit) then + coeff = coeff + computeUnitCoefficient(unit, ignoreCityProtection, battle, cityArmor, enemiesCount, a8) + end + end + + return coeff +end + +function sub_5DEE03(groupSubrace, otherGroup, ignoreCityProtection, position, enemiesCount, ignoreCityProtection2) + local totalCoeff = 0.0 + local biggestStackCoeff = 0.0 + local biggestStackCoeffId = nil + + -- Check stacks on tiles in -10 .. +10 square around position + for x = position.x - 10, position.x + 10, 1 do + for y = position.y - 10, position.y + 10, 1 do + local stack = getScenario():getStack(x, y) + if stack ~= nil and groupSubrace ~= stack.subrace and stack.order ~= Order.Stand then + local stackCoeff = computeGroupCoefficient(stack.group, ignoreCityProtection, enemiesCount, nil, ignoreCityProtection2, false) + totalCoeff = totalCoeff + stackCoeff + + if biggestStackCoeff < stackCoeff then + biggestStackCoeff = stackCoeff + biggestStackCoeffId = stack.id + end + end + end + end + + if biggestStackCoeffId ~= nil and biggestStackCoeffId == otherGroup.id then + return 0.0 + end + + return totalCoeff +end + +function sub_5DECF0(group, groupCoeff, otherGroup, otherGroupCoeff, nearbyStacksCoeff) + if not groupCanInflictDamageToOther(otherGroup, group) then + return 0.0 + end + + local v8 = 0.0 + if groupCoeff >= otherGroupCoeff * 0.5 then + v8 = groupCoeff + nearbyStacksCoeff + else + if groupCoeff >= nearbyStacksCoeff then + v8 = groupCoeff + else + v8 = nearbyStacksCoeff + end + end + + local coeff = otherGroupCoeff / v8 * 0.5 + + if coeff >= 1.0 then + return 1.0 + end + + return coeff +end + +function sub_5DED63(group, otherGroup, battle, position, countNearbyStacks, ignoreCityProtection) + local unitsCount = #group.units + local otherGroupUnitsCount = #otherGroup.units + + local groupCoeff = computeGroupCoefficient(group, true, otherGroupUnitsCount, battle, true, false) + local otherGroupCoeff = computeGroupCoefficient(otherGroup, false, unitsCount, battle, ignoreCityProtection, false) + + local nearbyStacksCoeff = 0.0 + if countNearbyStacks == true then + nearbyStacksCoeff = sub_5DEE03(group.subrace, otherGroup, false, position, unitsCount, ignoreCityProtection) + end + + return sub_5DECF0(otherGroup, otherGroupCoeff, group, groupCoeff, nearbyStacksCoeff) +end + +-- Returns distance between two points, length of a line segment connecting two points. +function distance(a, b) + local x = b.x - a.x + local y = b.y - a.y + return math.sqrt(x * x + y * y) +end + +function computeStackAndFortRelativeCoefficient(stack, enemyFort, battle, ignoreCityProtection) + if groupCanInflictDamageToOther(stack.group, enemyFort.group) == false then + return 0.0 + end + + local enemyCoeff = 0.0 + local stackUnitsCount = #stack.group.units + local enemyStackUnitsCount = 0 + + local enemyFortVisitor = enemyFort.visitor + if enemyFortVisitor ~= nil then + local enemyStackGroup = enemyFortVisitor.group + enemyCoeff = computeGroupCoefficient(enemyStackGroup, false, stackUnitsCount, battle, ignoreCityProtection, false) + enemyStackUnitsCount = #enemyStackGroup.units + end + + enemyCoeff = enemyCoeff + computeGroupCoefficient(enemyFort.group, false, stackUnitsCount, battle, ignoreCityProtection, false) + + if enemyStackUnitsCount == 0 then + enemyStackUnitsCount = #enemyFort.group.units + end + + local stackCoeff = computeGroupCoefficient(stack.group, true, enemyStackUnitsCount, battle, true, false) + local stackPosition = stack.position + + local enemyFortEntrance = enemyFort.entrance + -- How is this possible, if stack is attacking the fort it means it stands next to its entrance + -- This is original game logic + if distance(stackPosition, enemyFortEntrance) > 10.0 then + -- Accumulate enemy coefficient from nearby stacks + -- that are not inside cities and with non-Stand orders + getScenario():forEachStack(function (currStack) + if currStack.subrace ~= enemyFort.subrace then + return + end + + if currStack.order == Order.Stand then + return + end + + if currStack.inside then + return + end + + if distance(currStack.position, enemyFortEntrance) >= 8.0 then + return + end + + local c = computeGroupCoefficient(currStack.group, false, stackUnitsCount, battle, false, false) + enemyCoeff = enemyCoeff + c + end) + end + + local relativeCoeff = stackCoeff / enemyCoeff * 0.5 + if relativeCoeff >= 1.0 then + return 1.0 + end + + return relativeCoeff +end + +function computeGroupsRelativeCoefficient(battle, activeUnitGroup, enemyGroup) + -- Check if activeUnitGroup is not stack or enemyGroup is not fort + if activeUnitGroup.id.type ~= IdType.Stack or enemyGroup.id.type ~= IdType.Fortification then + local pos = Point.new(-1, -1) + return sub_5DED63(activeUnitGroup, enemyGroup, battle, pos, false, true) + else + -- Enemy group is a fort + local enemyFort = getScenario():getFort(enemyGroup.id) + -- Active unit group is a stack + local activeStack = getScenario():getStack(activeUnitGroup.id) + return computeStackAndFortRelativeCoefficient(activeStack, enemyFort, battle, true) + end +end + +function isStackHaveMovement(group) + if group.id.type == IdType.Stack then + return getScenario():getStack(group.id).movement > 0 + end + + return false +end + +-- Returns true if specified race is unplayable (can't be controlled by a human player) +function isRaceUnplayable(race) + return race ~= Race.Human + and race ~= Race.Heretic + and race ~= Race.Undead + and race ~= Race.Dwarf + and race ~= Race.Elf +end + +function checkGroupCanRetreat(possibleActions, activeUnit, activeUnitGroup, battle, ignoreUnplayableRaces, ignoreHumanPlayers) + if not hasValue(possibleActions, BattleAction.Retreat) then + -- Retreat is not a possible action. Group can't retreat + return false + end + + -- Don't retreat if: + -- battle is over and healers have their last turn + -- battle has not started yet + if battle.afterBattle or battle.currentRound < 1 then + return false + end + + local activePlayer = nil + if battle:isUnitAttacker(activeUnit) then + activePlayer = battle.attackerPlayer + else + activePlayer = battle.defenderPlayer + end + + if not ignoreHumanPlayers then + if activePlayer.isHuman then + return false + end + end + + if not ignoreUnplayableRaces then + if isRaceUnplayable(activePlayer.race) then + return false + end + end + + assert(activeUnitGroup.id.type == IdType.Stack) + + local stack = nil + if activeUnitGroup.id.type == IdType.Stack then + stack = getScenario():getStack(activeUnitGroup.id) + + local leader = stack.leader + local unitType = leader.impl.type + if unitType == Unit.Summon or unitType == Unit.Noble then + -- Summons and thieves (nobles) never retreat from battle + return false + end + end + + local order = stack.order + + local enemyGroup = nil + if not battle:isUnitAttacker(activeUnit) then + enemyGroup = battle.attacker.group + else + enemyGroup = battle.defender + end + + -- Allow retreat if: + -- group order is Normal or Roam + -- group order is Attack and we are not fighting with its target group + -- group defending and it's AI order is not Assist + if order == Order.Normal + or order == Order.Roam + or order == Order.Attack + and stack.orderTargetId ~= enemyGroup.id + then + if not battle:isUnitAttacker(activeUnit) + or stack.aiOrder ~= Order.Assist + then + return true + end + end + + return false +end + +function isAttackClassCanInflictDamage(attackClass) + return attackClass == Attack.Damage + or attackClass == Attack.Drain + or attackClass == Attack.DrainOverflow + or attackClass == Attack.Summon + or attackClass == Attack.Poison + or attackClass == Attack.Blister + or attackClass == Attack.Frostbite +end + +function getTargetGroupByAttackClass(attackClass, battle, allyGroup) + -- Check if attack class targets enemy group or allies + if attackClass == Attack.Paralyze + or attackClass == Attack.Petrify + or attackClass == Attack.Damage + or attackClass == Attack.Drain + or attackClass == Attack.Fear + or attackClass == Attack.LowerDamage + or attackClass == Attack.LowerInitiative + or attackClass == Attack.Poison + or attackClass == Attack.Frostbite + or attackClass == Attack.Blister + or attackClass == Attack.Shatter + or attackClass == Attack.DrainOverflow + or attackClass == Attack.DrainLevel + or attackClass == Attack.TransformOther + then + -- Attack targets enemy group + local attacker = battle.attacker.group + if attacker.id == allyGroup.id then + -- Attacker is an ally, so enemy is defender + return battle.defender + end + + -- Enemy is attacker + return attacker + end + + -- Attack targets allied group + return allyGroup +end + +-- Returns true if units in target group are always immune to specified attack class and damage source +function isTargetGroupAlwaysImmuneToAttackClassAndSource(attackClass, attackSource, allyGroup, attackTargetGroup, attackTargets) + if allyGroup.id == attackTargetGroup.id then + -- Game expects we can always apply attacks on units from allied group + return false + end + + local slots = attackTargetGroup.slots + for i = 1, #attackTargets do + local target = attackTargets[i] + if target < 0 then + goto continue + end + + -- +1 because of lua 1-based indexing + local slot = slots[target + 1] + local unit = slot.unit + if not unit then + goto continue + end + + local impl = unit.impl + + local immuneToSource = impl:getImmuneToAttackSource(attackSource) == Immune.Always + local immuneToClass = impl:getImmuneToAttackClass(attackClass) == Immune.Always + if not immuneToSource and not immuneToClass then + return false + end + + ::continue:: + end + + return true +end + +-- Returns +-- true/false indicating whether unit can attack or not +-- possible attack targets for specified unit in battle (if unit can attack) +function getUnitBattleAttackTargets(battle, unit) + if battle:getUnitStatus(unit.id, BattleStatus.Retreated) + or battle:getUnitStatus(unit.id, BattleStatus.Retreat) + then + return false, nil + end + + local possibleActions + , attackTargetGroup + , attackPossibleTargets + , item1TargetGroup + , item1PossibleTargets + , item2TargetGroup + , item2PossibleTargets = battle:getUnitActions(unit) + + if hasValue(possibleActions, BattleAction.UseItem) then + -- Unit can use items, thats considered enough + return true, attackPossibleTargets + end + + if not hasValue(possibleActions, BattleAction.Attack) then + -- Unit can not attack + return false, nil + end + + local impl = unit.impl + local attack = impl.attack1 + local attackClass = attack.type + local attackSource = attack.source + + local allyGroup = nil + if battle:isUnitAttacker(unit) then + allyGroup = battle.attacker.group + else + allyGroup = battle.defender + end + + local targetGroup = getTargetGroupByAttackClass(attackClass, battle, allyGroup) + + if not isTargetGroupAlwaysImmuneToAttackClassAndSource(attackClass, attackSource, allyGroup, targetGroup, attackPossibleTargets) then + return true, attackPossibleTargets + end + + return false, nil +end + +function groupCanPerformOffenceBattleActions(battle, group, enemyGroup) + local units = group.units + local unitCount = #units + if unitCount <= 0 then + return false + end + + for i = 1, #units do + local unit = units[i] + if isUnitAlive(unit) then + local impl = unit.impl + local attack = impl.attack1 + local attackClass = attack.type + + if isAttackClassCanInflictDamage(attackClass) then + local canAttack, attackTargets = getUnitBattleAttackTargets(battle, unit) + if canAttack then + if attackClass ~= Attack.Summon then + local attackSource = attack.source + local enemySlots = enemyGroup.slots + local enemyIsNotImmune = false + + for j = 1, #attackTargets do + local targetPosition = attackTargets[j] + -- +1 because of lua 1-based indexing + local slot = enemySlots[targetPosition + 1] + local enemyUnit = slot.unit + local enemyUnitImpl = enemyUnit.impl + + if enemyUnitImpl:getImmuneToAttackClass(attackClass) ~= Immune.Always + and enemyUnitImpl:getImmuneToAttackSource(attackSource) ~= Immune.Always + then + enemyIsNotImmune = true + break + end + end + + if enemyIsNotImmune then + return true + end + end + end + end + end + end + + return false +end + +-- Returns unit impl with respect to transform +function getOriginalUnitImpl(unit) + local original = unit.original + if original ~= nil then + return original.impl + end + + return unit.impl +end + +-- Returns true if unit is a leader +function isLeader(unit) + return getOriginalUnitImpl(unit).leaderType ~= -1 +end + +function isGroupHasLessThanTwoUnitsAlive(battle, group, checkNonBattlingUnits) + local units = group.units + if #units <= 0 then + return true + end + + local aliveCount = 0 + + for i = 1, #units do + local unit = units[i] + if checkNonBattlingUnits == false + and (battle:getUnitStatus(unit.id, BattleStatus.Retreated) + or battle:getUnitStatus(unit.id, BattleStatus.Hidden)) + then + -- Skip unit + else + if isUnitAlive(unit) then + aliveCount = aliveCount + 1 + end + end + + if aliveCount >= 2 then + return false + end + end + + return true +end + +-- Returns BattleAction +function retreatGroupUnitsFromBattle(unit, group, battle) + -- If unit at back lane or it's a leader, retreat + if (group:getUnitPosition(unit) % 2 == 1) or isLeader(unit) then + return BattleAction.Retreat + end + + -- Check alive defenders at front lane positions + local slots = group.slots + local defenders = false + for i = 1, #slots do + local slot = slots[i] + if slot.frontline then + local groupUnit = slot.unit + if groupUnit ~= nil then + if battle:getUnitStatus(groupUnit.id, BattleStatus.Defend) + and battle:getUnitStatus(groupUnit.id, BattleStatus.Dead) == false + then + defenders = true + break + end + end + end + end + + if defenders == false then + -- No alive and defending units found in the front lane. + -- Check if group have less than 2 alive units + if isGroupHasLessThanTwoUnitsAlive(battle, group, false) then + return BattleAction.Auto + else + return BattleAction.Defend + end + end + + return BattleAction.Retreat +end + +-- Must return result (true or false), battleAction, target unit id, attacker unit id +function checkGroupShouldRetreat(battle, possibleActions, activeUnit, activeUnitGroup, enemyGroup, activeUnitIsAttacker, relativeCoeff) + if battle.currentRound < 1 then + return false, BattleAction.Retreat, Id.emptyId(), Id.emptyId() + end + + if battle:getRetreatStatus(activeUnitIsAttacker) == Retreat.NoRetreat then + if battle.decidedToRetreat == true and activeUnitIsAttacker == false then + if relativeCoeff < 0.3 + and isStackHaveMovement(enemyGroup) == false + and checkGroupCanRetreat(possibleActions, activeUnit, activeUnitGroup, battle, false, false) == true + then + battle:setRetreatStatus(false, Retreat.CoverAndRetreat) + end + + battle:setDecidedToRetreat() + end + + if battle:getRetreatStatus(activeUnitIsAttacker) == Retreat.NoRetreat + and checkGroupCanRetreat(possibleActions, activeUnit, activeUnitGroup, battle, true, true) == true + and groupCanPerformOffenceBattleActions(battle, activeUnitGroup, enemyGroup) == false + then + battle:setRetreatStatus(activeUnitIsAttacker, Retreat.FullRetreat) + end + end + + if battle:getRetreatStatus(activeUnitIsAttacker) == Retreat.FullRetreat then + return true, BattleAction.Retreat, activeUnit.id, activeUnit.id + end + + if battle:getRetreatStatus(activeUnitIsAttacker) == Retreat.CoverAndRetreat then + local action = retreatGroupUnitsFromBattle(activeUnit, activeUnitGroup, battle) + + return true, action, activeUnit.id, activeUnit.id + end + + return false, BattleAction.Retreat, Id.emptyId(), Id.emptyId() +end + +function computePercentage(percentage, value) + return percentage * value / 100 +end + +function computeDamageWithBuffs(attack, maxDamage, battle, unit, addRandomDamage, easyDifficulty) + local attackDamage = attack.damage + if attackDamage > maxDamage then + attackDamage = maxDamage + end + + local variables = getGlobal().variables + + local randomAdditionalDamage = 0 + if addRandomDamage then + local batDamage = variables.batDamage + + if easyDifficulty then + batDamage = batDamage * 2 + end + + randomAdditionalDamage = randomNumber(batDamage) + end + + local damage = attackDamage + randomAdditionalDamage + + if battle ~= nil then + if battle:getUnitStatus(unit.id, BattleStatus.BoostDamageLvl1) then + damage = damage + computePercentage(variables:batBoostd(1), attackDamage) + end + + if battle:getUnitStatus(unit.id, BattleStatus.BoostDamageLvl2) then + damage = damage + computePercentage(variables:batBoostd(2), attackDamage) + end + + if battle:getUnitStatus(unit.id, BattleStatus.BoostDamageLvl3) then + damage = damage + computePercentage(variables:batBoostd(3), attackDamage) + end + + if battle:getUnitStatus(unit.id, BattleStatus.BoostDamageLvl4) then + damage = damage + computePercentage(variables:batBoostd(4), attackDamage) + end + + if battle:getUnitStatus(unit.id, BattleStatus.LowerDamageLvl1) then + damage = damage - computePercentage(variables:batLowerd(1), attackDamage) + end + + if battle:getUnitStatus(unit.id, BattleStatus.LowerDamageLvl2) then + damage = damage - computePercentage(variables:batLowerd(2), attackDamage) + end + end + + if damage < getGame().unitMinDamage then + return getGame().unitMinDamage + end + + if damage > maxDamage then + damage = maxDamage + end + + return damage +end + +function attackGetDamageWithBuffs(attack, maxDamage, battle, unit) + if not attack then + return 0 + end + + local attackClass = attack.type + + if attackClass == Attack.Damage + or attackClass == Attack.Drain + or attackClass == Attack.DrainOverflow + or attackClass == Attack.Poison + or attackClass == Attack.Blister + or attackClass == Attack.Frostbite + then + -- Attack inflicts damage, compute damage with buffs + return computeDamageWithBuffs(attack, maxDamage, battle, unit, false, false) + end + + -- Attack does not inflict damage + return 0 +end + +function attackGetDamageWithBuffsCheckTransform(unit, battle) + local impl = unit.impl + local unitMaxDamage = getMaxDamage(impl.id, impl) + + local attack = impl.attack1 + if attack.type == Attack.TransformSelf then + return attackGetDamageWithBuffs(impl.altAttack, unitMaxDamage, battle, unit) + end + + local attackDamage = attackGetDamageWithBuffs(attack, unitMaxDamage, battle, unit) + return attackGetDamageWithBuffs(impl.attack2, unitMaxDamage, battle, unit) + attackDamage +end + +function isDamagingAttack(attack) + if not attack then + return false + end + + local attackClass = attack.type + return attackClass == Attack.Damage + or attackClass == Attack.Drain + or attackClass == Attack.DrainOverflow + or attackClass == Attack.Poison + or attackClass == Attack.Blister + or attackClass == Attack.Frostbite +end + +function getSoldierAttackSource(impl) + local attack = impl.attack1 + if isDamagingAttack(attack) then + return attack.source + end + + local attack2 = impl.attack2 + if isDamagingAttack(attack2) then + return attack2.source + end + + local altAttack = impl.altAttack + if altAttack ~= nil and isDamagingAttack(altAttack) then + return altAttack.source + end + + -- Unknown soldier attack source + return -1 +end + +function computeArmor(unit, battle) + local impl = unit.impl + local armor = impl.armor + local shattered = battle:getUnitShatteredArmor(unit) + local fortArmor = battle:getUnitFortificationArmor(unit) + + armor = armor - shattered + if armor < fortArmor then + armor = fortArmor + end + + if battle:getUnitStatus(unit.id, BattleStatus.Defend) then + local v1 = 100 - armor + + local v2 = getGlobal().variables.defendBonus * 0.01 * v1 + + local v3 = armor + v2 + + if v3 > getGame().unitMaxArmor then + v3 = getGame().unitMaxArmor + end + + armor = v3 + end + + return armor +end + +function computeEffectiveHp(unit, battle) + if not isUnitAlive(unit) then + return false + end + + local armor = computeArmor(unit, battle) + local hp = unit.hp + + -- Vanilla formula for effective hp + return hp * armor / 100 + hp +end + +function computeTargetUnitAiPriority(unit, battle, damageWithBuffs) + local impl = unit.impl + local bigUnit = impl.small == false + local attack = impl.attack1 + local attackClass = attack.type + + local attack2 = impl.attack2 + + if bigUnit or attack.reach == Reach.Adjacent or attackClass == Attack.BoostDamage then + local effectiveHp = computeEffectiveHp(unit, battle) + if effectiveHp > damageWithBuffs then + return 10000 - effectiveHp + else + return 10000 + effectiveHp + end + end + + local unitValue = impl.xpKilled + if attackClass == Attack.Heal or (attack2 ~= nil and attack2.type == Attack.Heal) then + return 10000 + unitValue * 2 + end + + if attackClass == Attack.Paralyze or (attack2 ~= nil and attack2.type == Attack.Paralyze) + or attackClass == Attack.Petrify or (attack2 ~= nil and attack2.type == Attack.Petrify) + then + return 10000 + unitValue * 8 + end + + if attackClass == Attack.Summon or (attack2 ~= nil and attack2.type == Attack.Summon) then + return 10000 + unitValue * 10 + end + + if attackClass == Attack.TransformOther or (attack2 ~= nil and attack2.type == Attack.TransformOther) then + return 10000 + unitValue * 9 + end + + if attackClass == Attack.GiveAttack or (attack2 ~= nil and attack2.type == Attack.GiveAttack) then + return 10000 + unitValue * 3 + end + + return 10000 + unitValue +end + +-- Returns unit that is selected as an attack target among possible targets or nil +function selectAttackTarget(battle, damageWithBuffs, attackTargetGroup, attackPossibleTargets, attackSource) + local slots = attackTargetGroup.slots + local maxTargetValue = 0 + local selectedUnit = nil + + for i = 1, #attackPossibleTargets do + local attackTargetPosition = attackPossibleTargets[i] + + if attackTargetPosition < 0 then + goto continue + end + + -- +1 because of lua 1-based indexing + local slot = slots[attackTargetPosition + 1] + local unit = slot.unit + if not unit then + goto continue + end + + local currentHp = unit.hp + if not battle:getUnitStatus(unit.id, BattleStatus.Summon) and currentHp > 0 then + if currentHp <= damageWithBuffs and computeEffectiveHp(unit, battle) <= damageWithBuffs then + local impl = unit.impl + local immune = impl:getImmuneToAttackSource(attackSource) + local targetValue = computeTargetUnitAiPriority(unit, battle, damageWithBuffs) + + if immune == Immune.Once + and battle:isUnitResistantToSource(unit, attackSource) then + -- Target unit has resistance to attack source, reduce its value + targetValue = targetValue * 0.69999999 + end + + if targetValue > maxTargetValue and immune ~= Immune.Always then + maxTargetValue = targetValue + selectedUnit = unit + end + end + end + + ::continue:: + end + + return selectedUnit +end + +-- Leader can use items only if it has no other target, 'item use' action is permitted and its items can target someone +function canLeaderUseItemInBattle(selectedAttackTarget, possibleActions, item1PossibleTargets, item2PossibleTargets) + if selectedAttackTarget ~= nil then + return false + end + + return hasValue(possibleActions, BattleAction.UseItem) and (#item1PossibleTargets or #item2PossibleTargets) +end + +function sub_5D10F9(item) + if not item then + return 0 + end + + local base = item.base + local itemType = base.type + + if itemType == Item.Talisman then + return 4 + end + + if itemType == Item.Orb then + return 3 + end + + if itemType == Item.PotionRevive then + return 2 + end + + if itemType == Item.PotionHeal then + return 1 + end + + return 0 +end + +function getLeaderArtifactAttack(unit, artifactIndex) + local stack = getScenario():findStackByUnit(unit) + if not stack then + return nil + end + + if stack.leader.id ~= unit.id then + return nil + end + + local artifact1 = stack:getEquippedItem(Equipment.Artifact1) + local art1Attack = nil + if artifact1 then + art1Attack = artifact1.base.attack + end + + local artifact2 = stack:getEquippedItem(Equipment.Artifact2) + local art2Attack = nil + if artifact2 then + art2Attack = artifact2.base.attack + end + + if art1Attack == nil then + art1Attack = art2Attack + art2Attack = nil + end + + if artifactIndex == 1 then + if art1Attack then + return art1Attack + end + + return art2Attack + end + + if artifactIndex == 2 then + return art2Attack + end + + -- Wrong index + return nil +end + +-- Checks if attack is transform or doppelganger and returns their alternative attack +-- Otherwise returns attack itself +function getAttackWrToTransformDoppel(impl, first) + local attack = nil + if first then + attack = impl.attack1 + else + attack = impl.attack2 + end + + local attackClass = attack.type + + if attackClass == Attack.TransformSelf + or attackClass == Attack.Doppelganger + then + if first then + return impl.altAttack + else + return impl.altAttack2 + end + end + + return attack +end + +-- Returns unit attacks and their numbers +function getUnitAttacks(unitId, checkTransformedAttack) + local attacks = {} + + local unit = getScenario():getUnit(unitId) + local impl = unit.impl + + local attack = nil + if checkTransformedAttack then + attack = getAttackWrToTransformDoppel(impl, true) + else + attack = impl.attack1 + end + + table.insert(attacks, { attack, 0 }) + + if impl.attack2 then + local attack2 = nil + + if checkTransformedAttack then + attack2 = getAttackWrToTransformDoppel(impl, false) + else + attack2 = impl.attack2 + end + + table.insert(attacks, { attack2, 1 }) + end + + local art1Attack = getLeaderArtifactAttack(unit, 1) + if art1Attack ~= nil then + table.insert(attacks, { art1Attack, 2 }) + end + + local art2Attack = getLeaderArtifactAttack(unit, 2) + if art2Attack ~= nil then + table.insert(attacks, { art2Attack, 2 }) + end + + return attacks +end + +function getAttackById(id, attackNumber, checkTransformedAttack) + if id.type == IdType.Unit then + local attacks = getUnitAttacks(id, checkTransformedAttack) + if attackNumber > #attacks then + return nil + end + + local pair = attacks[attackNumber] + return pair[0] + elseif id.type == IdType.Item then + local item = getScenario():getItem(id) + local itemBase = item.base + local itemType = itemBase.type + + if itemType == Item.PotionBoost + or itemType == Item.PotionHeal + or itemType == Item.PotionRevive + or itemType == Item.PotionPermanent + then + return nil + end + + return itemBase.attack + end + + return nil +end + +function getAttackByIdAndCheckTransformed(id, attackNumber) + return getAttackById(id, attackNumber, true) +end + +function isUnitHasLessThanHalfHitPoints(unit) + return unit.hp / unit.hpMax <= 0.5 +end + +function getAttackingItemTargets(attack, itemTargetGroup, itemPossibleTargets) + local attackClass = attack.type + + if attackClass == Attack.Summon then + -- Choose frontlane slots as summon item targets + local targets = {} + + for i = 1, #itemPossibleTargets do + if itemPossibleTargets[i] % 2 == 0 then + table.insert(targets, itemPossibleTargets[i]) + end + end + + return targets + end + + if attackClass == Attack.Heal then + -- Choose units that have less than 50% of hit points left + local targets = {} + + local slots = itemTargetGroup.slots + for i = 1, #itemPossibleTargets do + local possibleTarget = itemPossibleTargets[i] + -- +1 because of lua 1-based indexing + local unit = slots[possibleTarget + 1] + if isUnitHasLessThanHalfHitPoints(unit) then + table.insert(targets, possibleTarget) + end + end + + return targets + end + + return itemPossibleTargets +end + +function pickRandomIfEqual(v1, v2) + if v1 == v2 then + return randomNumber(2) == 1 + end + + return false +end + +-- Returns true/false, targetId +function findHealAttackTarget(battle, targetGroup, possibleTargets) + local primaryHealRatio = 1.0 + local primaryTarget = nil + + local secondaryHealRatio = 1.0 + local secondaryTarget = nil + + local slots = targetGroup.slots + for i=1, #possibleTargets do + local possibleTarget = possibleTargets[i] + -- +1 because of lua 1-based indexing + local slot = slots[possibleTarget + 1] + local unit = slot.unit + + if not unit then goto continue end + + local ratio = unit.hp / unit.hpMax + + if not (ratio < primaryHealRatio or pickRandomIfEqual(ratio, primaryHealRatio)) then + goto continue + end + + if battle:getUnitStatus(unit.id, BattleStatus.Retreat) then + if secondaryTarget and battle:getUnitStatus(secondaryTarget.id, BattleStatus.Summon) then + -- Prefer healing summoned units over retreating allies + goto continue + end + elseif battle:getUnitStatus(unit.id, BattleStatus.Summon) then + primaryTarget = unit + primaryHealRatio = ratio + goto continue + end + + if ratio < secondaryHealRatio or pickRandomIfEqual(ratio, secondaryHealRatio) then + secondaryTarget = unit + secondaryHealRatio = ratio + end + + ::continue:: + end + + local target = primaryTarget + if not target then + target = secondaryTarget + end + + if not target then + return false, nil + end + + return true, target.id +end + +-- Returns true/false, targetId +function findReviveAttackTarget(battle, targetGroup, possibleTargets) + local primaryTarget = nil + local secondaryTarget = nil + local maxRatio = 1.0 + local maxXpKilled = 0 + + local slots = targetGroup.slots + for i=1, #possibleTargets do + local possibleTarget = possibleTargets[i] + -- +1 because of lua 1-based indexing + local slot = slots[possibleTarget + 1] + local unit = slot.unit + + if not unit then goto continue end + + if isUnitAlive(unit) then + if primaryTarget ~= nil then + goto continue + end + + local ratio = unit.hp / unit.hpMax + if ratio >= maxRatio then + if not pickRandomIfEqual(ratio, maxRatio) then + goto continue + end + + maxRatio = ratio + secondaryTarget = unit + end + else + local impl = unit.impl + local xpKilled = impl.xpKilled + + if xpKilled >= maxXpKilled or pickRandomIfEqual(xpKilled, maxXpKilled) then + primaryTarget = unit + maxXpKilled = xpKilled + end + end + + ::continue:: + end + + local target = primaryTarget + if not target then + target = secondaryTarget + end + + if not target then + return false, nil + end + + return true, target.id +end + +-- Returns true/false, targetId +function findDamageAttackTargetsForAllReach(targetGroup, possibleTargets) + local possibleTarget = -1 + + for i=1, #possibleTargets do + possibleTarget = possibleTargets[i] + if possibleTarget >= 0 then + break + end + end + + if possibleTarget < 0 then + return false, nil + end + + local slots = targetGroup.slots + -- +1 because of lua 1-based indexing + local slot = slots[possibleTarget + 1] + local unit = slot.unit + if unit then + return true, unit.id + end + + return true, Id.summonId(possibleTarget) +end + +-- Returns true/false, targetId +function findDamageAttackTargetWithAnyReach(targetGroup, possibleTargets, damage, battle, attackClass, attackSource, unitStatus) + local maxPriority = 0 + local slots = targetGroup.slots + local targetUnitId = Id.emptyId() + + for i=1, #possibleTargets do + local possibleTarget = possibleTargets[i] + + if possibleTarget < 0 then + goto continue + end + + -- +1 because of lua 1-based indexing + local slot = slots[possibleTarget + 1] + local unit = slot.unit + if not unit then + goto continue + end + + local canTargetUnit = false + if not unitStatus then + canTargetUnit = true + else + canTargetUnit = not battle:getUnitStatus(unit.id, unitStatus) + end + + if canTargetUnit then + local impl = unit.impl + + local sourceImmune = impl:getImmuneToAttackSource(attackSource) + local classImmune = impl:getImmuneToAttackClass(attackClass) + + if sourceImmune ~= Immune.Always and classImmune ~= Immune.Always then + local priority = computeTargetUnitAiPriority(unit, battle, damage) + + if (sourceImmune == Immune.Once and battle:isUnitResistantToSource(unit, attackSource)) + or (classImmune == Immune.Once and battle:isUnitResistantToClass(unit, attackClass)) then + priority = priority * 0.69999999 + end + + if priority > maxPriority then + maxPriority = priority + targetUnitId = unit.id + end + end + end + + ::continue:: + end + + return targetUnitId ~= Id.emptyId(), targetUnitId +end + +-- Returns targetId +function findDamageAttackTargetWithAdjacentReach(targetGroup, possibleTargets, battle, attackSource, attackClass) + local primaryTarget = Id.emptyId() + local nonSummonTarget = Id.emptyId() + local secondaryTarget = Id.emptyId() + -- These constants are taken directly from game + local minEffHp = 999999 + local summonMinEffHp = 999999 + + local slots = targetGroup.slots + + for i=1, #possibleTargets do + local possibleTarget = possibleTargets[i] + -- +1 because of lua 1-based indexing + local slot = slots[possibleTarget + 1] + local unit = slot.unit + + if not unit then + goto continue + end + + local isSummon = battle:getUnitStatus(unit.id, BattleStatus.Summon) + if isSummon and primaryTarget ~= Id.emptyId() then + -- Don't update primaryTarget + goto continue + end + + local effectiveHp = computeEffectiveHp(unit, battle) + if effectiveHp > 0 and effectiveHp < minEffHp then + local impl = unit.impl + + if impl:getImmuneToAttackSource(attackSource) ~= Immune.Always + and impl:getImmuneToAttackClass(attackClass) ~= Immune.Always then + if isSummon then + if effectiveHp < summonMinEffHp or pickRandomIfEqual(effectiveHp, summonMinEffHp) then + summonMinEffHp = effectiveHp + secondaryTarget = unit.id + end + else + minEffHp = effectiveHp + nonSummonTarget = unit.id + end + end + end + + primaryTarget = nonSummonTarget + ::continue:: + end + + if primaryTarget == Id.emptyId() then + return secondaryTarget + end + + return primaryTarget +end + +-- Returns true/false, targetId +function findDamageAttackTargetsForNonAllReach(attack, damage, targetGroup, possibleTargets, battle) + local reach = attack.reach + local attackSource = attack.source + local attackClass = attack.type + + if reach ~= Reach.Adjacent then + return findDamageAttackTargetWithAnyReach(targetGroup, possibleTargets, damage, battle, attackClass, attackSource, nil) + end + + local targetId = findDamageAttackTargetWithAdjacentReach(targetGroup, possibleTargets, battle, attackSource, attackClass) + return targetId ~= Id.emptyId(), targetId +end + +-- Assumption: returns true if unit with Paralyze or Petrify attack should use Block action +-- because there are no allied units that can pertorm offence battle actions +function sub_5D02D5(attackClass, battle, enemyGroup) + if attackClass ~= Attack.Paralyze and attackClass ~= Attack.Petrify then + return false + end + + local attacker = battle.attacker + local group = attacker.group + if group.id == enemyGroup.id then + group = battle.defender + end + + if groupCanPerformOffenceBattleActions(battle, group, enemyGroup) then + return false + end + + return true +end + +function findParalyzeOrPetrifyAttackTarget(battle, targetGroup, possibleTargets, attackClass) + if sub_5D02D5(attackClass, battle, targetGroup) then + -- Do not search for target + return false, nil + end + + local attackingUnit = nil + local nonAtackingUnit = nil + local retreatingUnit = nil + local attackingUnitMaxXpKilled = 0 + local nonAttackingUnitMaxXpKilled = 0 + + local slots = targetGroup.slots + for i=1, #possibleTargets do + local possibleTarget = possibleTargets[i] + -- +1 because of lua 1-based indexing + local slot = slots[possibleTarget + 1] + local unit = slot.unit + + if not unit then + goto continue + end + + if not battle:getUnitStatus(unit.id, BattleStatus.Paralyze) + and not battle:getUnitStatus(unit.id, BattleStatus.Petrify) then + if battle:getUnitStatus(unit.id, BattleStatus.Retreat) then + retreatingUnit = unit + else + local impl = unit.impl + local xpKilled = impl.xpKilled + if xpKilled > attackingUnitMaxXpKilled then + if getUnitBattleAttackTargets(battle, unit, nil) then + attackingUnit = unit + attackingUnitMaxXpKilled = xpKilled + elseif xpKilled > nonAttackingUnitMaxXpKilled + or pickRandomIfEqual(xpKilled, nonAttackingUnitMaxXpKilled) then + nonAtackingUnit = unit + nonAttackingUnitMaxXpKilled = xpKilled + end + end + end + end + + ::continue:: + end + + local targetUnit = attackingUnit + if not attackingUnit then + targetUnit = nonAtackingUnit + + if not nonAtackingUnit then + targetUnit = retreatingUnit + + if retreatingUnit ~= nil then + local attacker = battle.attacker + local group = attacker.group + if group.id == targetGroup.id then + group = battle.defender + end + + if groupCanPerformOffenceBattleActions(battle, group, targetGroup) then + targetUnit = retreatingUnit + end + end + end + end + + if not targetUnit then + return false, nil + end + + return targetUnit.id ~= Id.emptyId(), targetUnit.id +end + +-- Returns true/false, targetId +function findDotAttackTarget(unitStatus, battle, unitOrItemId, targetGroup, possibleTargets) + local attack = nil + local damage = 0 + + if unitOrItemId.type == IdType.Unit then + local unit = getScenario():getUnit(unitOrItemId) + if not unit then + -- This should never happen + return false, nil + end + + local impl = unit.impl + attack = impl.attack1 + damage = attackGetDamageWithBuffsCheckTransform(unit, battle) + else + local item = getScenario():getItem(unitOrItemId) + if not item then + -- This should never happen + return false, nil + end + + local itemBase = item.base + attack = itemBase.attack + damage = attack.damage + end + + local attackSource = attack.source + local attackClass = attack.type + + local ok, targetId = findDamageAttackTargetWithAnyReach(targetGroup, possibleTargets, damage, battle, attackClass, attackSource, unitStatus) + if ok then + return true, targetId + end + + return findDamageAttackTargetWithAnyReach(targetGroup, possibleTargets, damage, battle, attackClass, attackSource, nil) +end + +function checkImplHasDamageOrDrainOrDrainOverflowAttack( impl ) + local attack = impl.attack1 + local attackClass = attack.type + + return attackClass == Attack.Damage + or attackClass == Attack.Drain + or attackClass == Attack.DrainOverflow +end + +-- Returns true/false, targetId +function findBoostDamageAttackTarget(battle, targetGroup, possibleTargets) + local targetUnit = nil + local damagedTargetUnit = nil + local maxXpKilled = 0 + local damagedUnitMaxXpKilled = 0 + + local slots = targetGroup.slots + for i=1, #possibleTargets do + local possibleTarget = possibleTargets[i] + -- +1 because of lua 1-based indexing + local slot = slots[possibleTarget + 1] + local unit = slot.unit + + if not unit then + goto continue + end + + local impl = unit.impl + if not battle:getUnitStatus(unit.id, BattleStatus.Retreat) + and not battle:getUnitStatus(unit.id, BattleStatus.Paralyze) + and not battle:getUnitStatus(unit.id, BattleStatus.Petrify) + and checkImplHasDamageOrDrainOrDrainOverflowAttack(impl) then + local xpKilled = impl.xpKilled + if xpKilled > maxXpKilled or pickRandomIfEqual(xpKilled, maxXpKilled) then + local hp = unit.hp + local hpMax = unit.hpMax + + if hp / hpMax < 0.25 then + if xpKilled > damagedUnitMaxXpKilled + or pickRandomIfEqual(xpKilled, damagedUnitMaxXpKilled) then + damagedTargetUnit = unit + damagedUnitMaxXpKilled = xpKilled + end + else + targetUnit = unit + maxXpKilled = xpKilled + end + end + end + + ::continue:: + end + + local target = targetUnit + if not target then + target = damagedTargetUnit + end + + if not target then + return false, nil + end + + return true, target.id +end + +-- Priority of 1 considered top, 999 - lowest priority +function getUnitCurePriority(unit, battle) + local id = unit.id + + if battle:getUnitStatus(id, BattleStatus.Transform) then + return 1 + end + + if battle:getUnitStatus(id, BattleStatus.Paralyze) + or battle:getUnitStatus(id, BattleStatus.Petrify) then + return 2 + end + + if battle:getUnitStatus(id, BattleStatus.Poison) then + return 3 + end + + if battle:getUnitStatus(id, BattleStatus.Frostbite) then + return 4 + end + + if battle:getUnitStatus(id, BattleStatus.Blister) then + return 5 + end + + return 999 +end + +-- Returns true/false, targetId +function findCureAttackTarget(battle, targetGroup, possibleTargets) + local primaryTarget = nil + local primaryTargetMinValue = 99 + local primaryTargetMaxXpKilled = 0 + + local secondaryTarget = nil + local secondaryTargetMinValue = 99 + local secondaryTargetMaxXpKilled = 0 + + local slots = targetGroup.slots + + for i=1, #possibleTargets do + local possibleTarget = possibleTargets[i] + -- +1 because of lua 1-based indexing + local slot = slots[possibleTarget + 1] + local unit = slot.unit + + if not unit then + goto continue + end + + local value = getUnitCurePriority(unit, battle) + local retreating = battle:getUnitStatus(unit.id, BattleStatus.Retreat) + + if retreating or battle:getUnitStatus(unit.id, BattleStatus.Summon) then + if not primaryTarget + and (not retreating + or not secondaryTarget + or not battle:getUnitStatus(secondaryTarget.id, BattleStatus.Summon)) + and value <= secondaryTargetMinValue then + if value < secondaryTargetMinValue then + secondaryTargetMaxXpKilled = 0 + end + + local impl = unit.impl + local xpKilled = impl.xpKilled + if xpKilled > secondaryTargetMaxXpKilled + or pickRandomIfEqual(xpKilled, secondaryTargetMaxXpKilled) then + secondaryTarget = unit + secondaryTargetMinValue = value + secondaryTargetMaxXpKilled = xpKilled + end + end + elseif value <= primaryTargetMinValue then + if value < primaryTargetMinValue then + primaryTargetMaxXpKilled = 0 + end + + local impl = unit.impl + local xpKilled = impl.xpKilled + if xpKilled > primaryTargetMaxXpKilled + or pickRandomIfEqual(xpKilled, primaryTargetMaxXpKilled) then + primaryTarget = unit + primaryTargetMinValue = value + primaryTargetMaxXpKilled = xpKilled + end + end + + ::continue:: + end + + local targetUnit = primaryTarget + if not targetUnit then + targetUnit = secondaryTarget + end + + if not targetUnit then + return false, nil + end + + return true, targetUnit.id +end + +-- Returns true/false, targetId +function findSummonAttackTarget(targetGroup, possibleTargets) + local slots = targetGroup.slots + + -- Pick starting index in [0 : #possibleTargets) range + -- +1 to convert index to [1 : #possibleTargets] range because of lua 1-based indexing + local index = randomNumber(#possibleTargets) + 1 + local initialIndex = index + + -- Search starting from random index, prefer frontline targets, if possible. + -- If end of possibleTargets list reached, continue from beginning until starting index. + repeat + local target = possibleTargets[index] + if slots[target + 1].frontline then + break + end + + index = index + 1 + if index > #possibleTargets then + index = 1 + end + until (index == initialIndex) + + local possibleTarget = possibleTargets[index] + + -- +1 because of lua 1-based indexing + local slot = slots[possibleTarget + 1] + local unit = slot.unit + + local targetId + if unit then + targetId = unit.id + else + targetId = Id.summonId(possibleTarget) + end + + return true, targetId +end + +-- Returns true/false, targetId +function findTransformOtherAttackTarget(battle, targetGroup, possibleTargets) + local primaryTarget = nil + local secondaryTarget = nil + local primaryTargetMaxXpKilled = 0 + local secondaryTargetMaxXpKilled = 0 + + local slots = targetGroup.slots + + for i=1, #possibleTargets do + local possibleTarget = possibleTargets[i] + -- +1 because of lua 1-based indexing + local slot = slots[possibleTarget + 1] + local unit = slot.unit + + if not unit then + goto continue + end + + if battle:getUnitStatus(unit.id, BattleStatus.Transform) then + -- Unit already transformed + goto continue + end + + local impl = unit.impl + local xpKilled = impl.xpKilled + + if xpKilled > primaryTargetMaxXpKilled + or pickRandomIfEqual(xpKilled, primaryTargetMaxXpKilled) then + local hp = unit.hp + local hpMax = unit.hpMax + + if hp / hpMax <= 0.2 + or battle:getUnitStatus(unit.id, BattleStatus.Retreat) then + if xpKilled > secondaryTargetMaxXpKilled + or pickRandomIfEqual(xpKilled, secondaryTargetMaxXpKilled) then + secondaryTarget = unit + secondaryTargetMaxXpKilled = xpKilled + end + else + primaryTarget = unit + primaryTargetMaxXpKilled = xpKilled + end + end + + ::continue:: + end + + local target = primaryTarget + if not target then + target = secondaryTarget + end + + if not target then + return false, nil + end + + return true, target.id +end + +function getTargetUnit(targetPosition, targetGroup, enemyGroup) + if targetPosition >= 0 then + -- +1 because of lua 1-based indexing + local slot = targetGroup.slots[targetPosition + 1] + return slot.unit + end + + -- +1 because of lua 1-based indexing + local slot = enemyGroup.slots[-(targetPosition + 1) + 1] + return slot.unit +end + +function isAttackEffectiveAgainstGroup(attack, group) + local attackClass = attack.type + if not isAttackClassCanInflictDamage(attackClass) then + return true + end + + local units = group.units + if #units <= 0 then + -- Empty group ?! + return false + end + + local attackSource = attack.source + for i=1, #units do + local unit = units[i] + local impl = unit.impl + if impl:getImmuneToAttackSource(attackSource) ~= Immune.Always + and impl:getImmuneToAttackClass(attackClass) ~= Immune.Always then + return true + end + end + + -- All units are always immune to attack + return false +end + +-- Returns true/false, targetId +function findDoppelgangerAttackTarget(unitOrItemId, battle, targetGroup, possibleTargets) + local unit = getScenario():getUnit(unitOrItemId) + if not unit then + -- This should never happen + return false, nil + end + + local enemyGroup = battle.attacker.group + if targetGroup.id == enemyGroup.id then + enemyGroup = battle.defender + end + + local unitPosition = targetGroup:getUnitPosition(unit) + local primaryTargetXpKilled = 0 + local secondaryTargetXpKilled = 0 + local primaryTarget = nil + local secondaryTarget = nil + + for i=1, #possibleTargets do + local possibleTarget = possibleTargets[i] + local targetUnit = getTargetUnit(possibleTarget, targetGroup, enemyGroup) + if not targetUnit then + goto continue + end + + local impl = targetUnit.impl + local attack = impl.attack1 + if not isAttackEffectiveAgainstGroup(attack, enemyGroup) then + goto continue + end + + local xpKilled = impl.xpKilled + if attack.melee == (unitPosition % 2 == 0) then + if xpKilled > primaryTargetXpKilled or pickRandomIfEqual(xpKilled, primaryTargetXpKilled) then + primaryTargetXpKilled = xpKilled + primaryTarget = targetUnit + end + elseif primaryTarget == nil then + if xpKilled > secondaryTargetXpKilled or pickRandomIfEqual(xpKilled, secondaryTargetXpKilled) then + secondaryTargetXpKilled = xpKilled + secondaryTarget = targetUnit + end + end + + ::continue:: + end + + if primaryTarget then + return true, primaryTarget.id + end + + if secondaryTarget then + return true, secondaryTarget.id + end + + return false, nil +end + +function countAliveUnitsInGroup(group) + local units = group.units + local aliveUnits = 0 + + for i=1, #units do + local unit = units[i] + if isUnitAlive(unit) then + aliveUnits = aliveUnits + 1 + end + end + + return aliveUnits +end + +-- Returns true if specified unit can (and should) perform TransformSelf attack +function isTransformSelfPossible(unit, battle) + if battle:getUnitStatus(unit.id, BattleStatus.TransformSelf) then + return false + end + + local allyGroup = nil + local enemyGroup = nil + if battle:isUnitAttacker(unit) then + allyGroup = battle.attacker.group + enemyGroup = battle.defender + else + allyGroup = battle.defender + enemyGroup = battle.attacker.group + end + + local slots = allyGroup.slots + + -- +1 because of lua 1-based indexing + if allyGroup:getUnitPosition(unit) % 2 == 1 + and (slots[0 + 1].unit or slots[2 + 1].unit or slots[4 + 1].unit) then + -- Do not transform self if we stand at the frontlane with allied units + return false + end + + if unit.hp / unit.hpMax <= 0.25 then + -- Don not transform self if we have less than 25% of health + return false + end + + -- Transform self only if there are less than 3 alive enemies + return countAliveUnitsInGroup(enemyGroup) < 3 +end + +-- Returns true/false, targetId +function findTransformSelfAttackTarget(unitOrItemId, battle, targetGroup, possibleTargets) + if unitOrItemId.type == IdType.Unit then + local unit = getScenario():getUnit(unitOrItemId) + if not unit then + -- This should never happen + return false, nil + end + + if isTransformSelfPossible(unit, battle) then + return true, unit.id + end + + return false, nil + end + + -- unitOrItemId belongs to item + local minXpKilled = 99999 + local targetUnit = nil + + local slots = targetGroup.slots + for i=1, #possibleTargets do + local possibleTarget = possibleTargets[i] + -- +1 because of lua 1-based indexing + local slot = slots[possibleTarget + 1] + local unit = slot.unit + if not unit then + goto continue + end + + if isTransformSelfPossible(unit, battle) then + local impl = unit.impl + local xpKilled = impl.xpKilled + + if xpKilled < minXpKilled then + minXpKilled = xpKilled + targetUnit = unit + end + end + + ::continue:: + end + + if targetUnit then + return true, targetUnit.id + end + + return false, nil +end + +-- Returns true/false, targetId +function findDrainLevelAttackTarget(battle, targetGroup, possibleTargets) + local slots = targetGroup.slots + + local primaryTarget = nil + local secondaryTarget = nil + local primaryTargetMaxXpKilled = 0 + local secondaryTargetMaxXpKilled = 0 + local maxLevel = 1 + + for i=1, #possibleTargets do + local possibleTarget = possibleTargets[i] + -- +1 because of lua 1-based indexing + local slot = slots[possibleTarget + 1] + local unit = slot.unit + if not unit then + goto continue + end + + local impl = unit.impl + local level = impl.level + if level ~= 1 and level >= maxLevel then + if level > maxLevel then + primaryTargetMaxXpKilled = 0 + end + + maxLevel = level + local xpKilled = impl.xpKilled + if battle:getUnitStatus(unit.id, BattleStatus.Retreat) then + if xpKilled > secondaryTargetMaxXpKilled + or pickRandomIfEqual(xpKilled, secondaryTargetMaxXpKilled) then + secondaryTarget = unit + secondaryTargetMaxXpKilled = xpKilled + end + elseif xpKilled > primaryTargetMaxXpKilled + or pickRandomIfEqual(xpKilled, primaryTargetMaxXpKilled) then + primaryTarget = unit + primaryTargetMaxXpKilled = xpKilled + end + end + + ::continue:: + end + + if primaryTarget then + return true, primaryTarget.id + end + + if secondaryTarget then + return true, secondaryTarget.id + end + + return false, nil +end + +-- Returns true/false, targetId +function findGiveAttackTarget(battle, targetGroup, possibleTargets) + local slots = targetGroup.slots + + local primaryTarget = nil + local secondaryTarget = nil + local primaryTargetMaxXpKilled = 0 + local secondaryTargetMaxXpKilled = 0 + local secondaryTargetIsBooster = true + + for i=1, #possibleTargets do + local possibleTarget = possibleTargets[i] + -- +1 because of lua 1-based indexing + local slot = slots[possibleTarget + 1] + local unit = slot.unit + if not unit then + goto continue + end + + if not battle:getUnitStatus(unit.id, BattleStatus.Retreat) + and not battle:getUnitStatus(unit.id, BattleStatus.Paralyze) + and not battle:getUnitStatus(unit.id, BattleStatus.Petrify) + then + local impl = unit.impl + local xpKilled = impl.xpKilled + local attack = impl.attack1 + local attackClass = attack.type + + if attackClass == Attack.Damage + or attackClass == Attack.Drain + or attackClass == Attack.DrainOverflow + then + if xpKilled > primaryTargetMaxXpKilled + or pickRandomIfEqual(xpKilled, primaryTargetMaxXpKilled) + then + local canAttack, _ = getUnitBattleAttackTargets(battle, unit) + if canAttack then + primaryTarget = unit + primaryTargetMaxXpKilled = xpKilled + end + end + elseif not primaryTarget + and (xpKilled > primaryTargetMaxXpKilled + or pickRandomIfEqual(xpKilled, primaryTargetMaxXpKilled)) + then + local boost = attackClass == Attack.BoostDamage + if not boost or secondaryTargetIsBooster then + local canAttack, _ = getUnitBattleAttackTargets(battle, unit) + if canAttack then + secondaryTarget = unit + secondaryTargetMaxXpKilled = xpKilled + secondaryTargetIsBooster = boost + end + end + end + end + + ::continue:: + end + + if primaryTarget then + return true, primaryTarget.id + end + + if secondaryTarget then + return true, secondaryTarget.id + end + + return false, nil +end + +function checkBestowWardsAttackCanBePerformed(battle, unitOrItemId) + local group = nil + + -- Strange: we accessing enemy group here ? + -- This is what original game logic does + if battle:isUnitAttacker(unitOrItemId) then + group = battle.defender + else + group = battle.attacker.group + end + + local units = group.units + for i=1, #units do + local unit = units[i] + + if not battle:getUnitStatus(unit.id, BattleStatus.Dead) + and not battle:getUnitStatus(unit.id, BattleStatus.XpCounted) + and not battle:getUnitStatus(unit.id, BattleStatus.Retreated) + and not battle:getUnitStatus(unit.id, BattleStatus.Retreat) + and not battle:getUnitStatus(unit.id, BattleStatus.Hidden) + and not battle:getUnitStatus(unit.id, BattleStatus.Unsummoned) + then + -- At least one unit can be buffed with 'Bestow wards' + return true + end + end + + -- No units can be buffed + return false +end + +-- Returns true/false, targetId +function findBestowWardsAttackTarget(battle, targetGroup, possibleTargets) + local primaryTarget = nil + local secondaryTarget = nil + local primaryTargetMaxXpKilled = 0 + local secondaryTargetMaxXpKilled = 0 + + local slots = targetGroup.slots + for i=1, #possibleTargets do + local possibleTarget = possibleTargets[i] + -- +1 because of lua 1-based indexing + local slot = slots[possibleTarget + 1] + local unit = slot.unit + if not unit then + goto continue + end + + if battle:getUnitStatus(unit.id, BattleStatus.Retreat) + or battle:getUnitStatus(unit.id, BattleStatus.Paralyze) + or battle:getUnitStatus(unit.id, BattleStatus.Petrify) + then + goto continue + end + + local impl = unit.impl + local xpKilled = impl.xpKilled + if xpKilled > primaryTargetMaxXpKilled + or pickRandomIfEqual(xpKilled, primaryTargetMaxXpKilled) + then + if unit.hp / unit.hpMax < 0.25 then + if xpKilled > secondaryTargetMaxXpKilled + or pickRandomIfEqual(xpKilled, secondaryTargetMaxXpKilled) + then + secondaryTarget = unit + secondaryTargetMaxXpKilled = xpKilled + end + else + primaryTarget = unit + primaryTargetMaxXpKilled = xpKilled + end + end + + ::continue:: + end + + if primaryTarget then + return true, primaryTarget.id + end + + if secondaryTarget then + return true, secondaryTarget.id + end + + return false, nil +end + +-- Returns true/false, targetId +function findShatterAttackTarget(battle, targetGroup, possibleTargets) + local primaryTarget = nil + local secondaryTarget = nil + local primaryTargetMaxArmor = 0 + local secondaryTargetMaxArmor = 0 + + local slots = targetGroup.slots + for i=1, #possibleTargets do + local possibleTarget = possibleTargets[i] + -- +1 because of lua 1-based indexing + local slot = slots[possibleTarget + 1] + local unit = slot.unit + if not unit then + goto continue + end + + if battle:getUnitStatus(unit.id, BattleStatus.Retreat) + or battle:getUnitStatus(unit.id, BattleStatus.Paralyze) + or battle:getUnitStatus(unit.id, BattleStatus.Petrify) + then + goto continue + end + + local impl = unit.impl + local armor = impl.armor + if armor > primaryTargetMaxArmor + or pickRandomIfEqual(armor, primaryTargetMaxArmor) + then + if unit.hp / unit.hpMax < 0.25 then + if armor > secondaryTargetMaxArmor + or pickRandomIfEqual(armor, secondaryTargetMaxArmor) + then + secondaryTarget = unit + secondaryTargetMaxArmor = armor + end + else + primaryTarget = unit + primaryTargetMaxArmor = armor + end + end + + ::continue:: + end + + if primaryTarget then + return true, primaryTarget.id + end + + if secondaryTarget then + return true, secondaryTarget.id + end + + return false, nil +end + +-- Returns true/false, targetId +function findFearAttackTarget(battle, targetGroup, possibleTargets) + local primaryTarget = nil + local secondaryTarget = nil + local primaryTargetMaxXpKilled = 0 + local secondaryTargetMaxXpKilled = 0 + + local slots = targetGroup.slots + for i=1, #possibleTargets do + local possibleTarget = possibleTargets[i] + -- +1 because of lua 1-based indexing + local slot = slots[possibleTarget + 1] + local unit = slot.unit + if not unit then + goto continue + end + + if battle:getUnitStatus(unit.id, BattleStatus.Retreat) then + goto continue + end + + local impl = unit.impl + local xpKilled = impl.xpKilled + if xpKilled > primaryTargetMaxXpKilled + or pickRandomIfEqual(xpKilled, primaryTargetMaxXpKilled) + then + local canAttack, _ = getUnitBattleAttackTargets(battle, unit) + if canAttack then + primaryTarget = unit + primaryTargetMaxXpKilled = xpKilled + elseif xpKilled > secondaryTargetMaxXpKilled + or pickRandomIfEqual(xpKilled, secondaryTargetMaxXpKilled) + then + secondaryTarget = unit + secondaryTargetMaxXpKilled = xpKilled + end + end + + ::continue:: + end + + if primaryTarget then + return true, primaryTarget.id + end + + if secondaryTarget then + return true, secondaryTarget.id + end + + return false, nil +end + +-- Returns true/false, targetId +function findAttackTarget(unitOrItemId, attack, targetGroup, possibleTargets, battle) + local attackClass = attack.type + + if attackClass == Attack.Heal then + return findHealAttackTarget(battle, targetGroup, possibleTargets) + end + + if attackClass == Attack.Damage + or attackClass == Attack.Drain + or attackClass == Attack.DrainOverflow + then + if attack.reach == Reach.All then + return findDamageAttackTargetsForAllReach(targetGroup, possibleTargets) + end + + local maxDamage = getUnitOrItemMaxDamage(unitOrItemId) + local damage = math.min(maxDamage, attack.damage) + return findDamageAttackTargetsForNonAllReach(attack, damage, targetGroup, possibleTargets, battle) + end + + if attackClass == Attack.Paralyze then + return findParalyzeOrPetrifyAttackTarget(battle, targetGroup, possibleTargets, attackClass) + end + + if attackClass == Attack.Poison then + return findDotAttackTarget(BattleStatus.Poison, battle, unitOrItemId, targetGroup, possibleTargets) + end + + if attackClass == Attack.BoostDamage then + return findBoostDamageAttackTarget(battle, targetGroup, possibleTargets) + end + + if attackClass == Attack.Cure then + return findCureAttackTarget(battle, targetGroup, possibleTargets) + end + + if attackClass == Attack.Summon then + return findSummonAttackTarget(targetGroup, possibleTargets) + end + + if attackClass == Attack.TransformOther then + return findTransformOtherAttackTarget(battle, targetGroup, possibleTargets) + end + + if attackClass == Attack.Revive then + return findReviveAttackTarget(targetGroup, possibleTargets) + end + + if attackClass == Attack.Doppelganger then + return findDoppelgangerAttackTarget(unitOrItemId, battle, targetGroup, possibleTargets) + end + + if attackClass == Attack.TransformSelf then + return findTransformSelfAttackTarget(unitOrItemId, battle, targetGroup, possibleTargets) + end + + if attackClass == Attack.Petrify then + return findParalyzeOrPetrifyAttackTarget(battle, targetGroup, possibleTargets, attackClass) + end + + if attackClass == Attack.DrainLevel then + return findDrainLevelAttackTarget(battle, targetGroup, possibleTargets) + end + + if attackClass == Attack.GiveAttack then + return findGiveAttackTarget(battle, targetGroup, possibleTargets) + end + + if attackClass == Attack.Frostbite then + return findDotAttackTarget(BattleStatus.Frostbite, battle, unitOrItemId, targetGroup, possibleTargets) + end + + if attackClass == Attack.Blister then + return findDotAttackTarget(BattleStatus.Blister, battle, unitOrItemId, targetGroup, possibleTargets) + end + + if attackClass == Attack.BestowWards then + local canAttack = checkBestowWardsAttackCanBePerformed(battle, unitOrItemId) + if not canAttack then + return findHealAttackTarget(battle, targetGroup, possibleTargets) + end + + return findBestowWardsAttackTarget(battle, targetGroup, possibleTargets) + end + + if attackClass == Attack.Shatter then + return findShatterAttackTarget(battle, targetGroup, possibleTargets) + end + + if attackClass == Attack.Fear then + return findFearAttackTarget(battle, targetGroup, possibleTargets) + end + + return false, nil +end + +function getNonAttackingItemTargets(item, targetGroup, itemPossibleTargets) + local base = item.base + local itemType = base.type + + if itemType == Item.PotionRevive then + local targets = {} + for i=1, #itemPossibleTargets do + if itemPossibleTargets[i] % 2 == 1 then + -- Prefer reviving frontlane units + table.insert(targets, itemPossibleTargets[i]) + end + end + + return targets + end + + if itemType == Item.PotionHeal then + local slots = targetGroup.slots + + local targets = {} + for i=1, #itemPossibleTargets do + local possibleTarget = itemPossibleTargets[i] + -- +1 because of lua 1-based indexing + local slot = slots[possibleTarget + 1] + + local unit = slot.unit + if unit ~= nil and isUnitHasLessThanHalfHitPoints(unit) then + -- Prefer healing units that have 50% of hit points or less + table.insert(targets, possibleTarget) + end + end + + return targets + end + + return itemPossibleTargets +end + +-- Returns true/false, targetId +function findHealOrRevivePotionAttackTarget(item, battle, itemTargetGroup, itemPossibleTargets) + local base = item.base + local itemType = base.type + + if itemType == Item.PotionHeal then + return findHealAttackTarget(battle, itemTargetGroup, itemPossibleTargets) + end + + if itemType == Item.PotionRevive then + return findReviveAttackTarget(battle, itemTargetGroup, itemPossibleTargets) + end + + return false, nil +end + +-- Returns true/false, targetId, attackerId +function findItemAttackTarget(activeUnit, item, itemTargetGroup, itemPossibleTargets, battle) + if #itemPossibleTargets <= 0 then + return false, nil, nil + end + + local attackerId = item.id + local attack = getAttackByIdAndCheckTransformed(item.id, 1) + if attack then + local targets = getAttackingItemTargets(attack, itemTargetGroup, itemPossibleTargets) + if #targets then + local found, targetId = findAttackTarget(item.id, attack, itemTargetGroup, targets, battle) + return found, targetId, attackerId + end + else + local targets = getNonAttackingItemTargets(item, itemTargetGroup, itemPossibleTargets) + if #targets then + local found, targetId = findHealOrRevivePotionAttackTarget(item, battle, itemTargetGroup, targets) + return found, targetId, attackerId + end + end + + return false, nil, nil +end + +-- Returns true/false, targetId, attackerId +function findEquipmentAttackTarget(activeUnit, activeUnitGroup, item1TargetGroup, item1PossibleTargets, item2TargetGroup, item2PossibleTargets, battle) + if activeUnitGroup.id.type ~= IdType.Stack then + return false, nil, nil + end + + local stack = getScenario():getStack(activeUnitGroup.id) + if not stack then + -- This should never happen + return false, nil, nil + end + + local item1 = stack:getEquippedItem(Equipment.Battle1) + local item1Value = sub_5D10F9(item1) + + local item2 = stack:getEquippedItem(Equipment.Battle2) + local item2Value = sub_5D10F9(item2) + + if item1Value < item2Value then + if item2Value > 0 then + local ok, targetId, attackerId = findItemAttackTarget(activeUnit, item2, item2TargetGroup, item2PossibleTargets, battle) + if ok then + return ok, targetId, attackerId + end + end + + if item1Value <= 0 then + return false, nil, nil + end + + return findItemAttackTarget(activeUnit, item1, item1TargetGroup, item1PossibleTargets, battle) + end + + if item1Value > 0 then + local ok, targetId, attackerId = findItemAttackTarget(activeUnit, item1, item1TargetGroup, item1PossibleTargets, battle) + if ok then + return ok, targetId, attackerId + end + end + + if item2Value > 0 then + local ok, targetId, attackerId = findItemAttackTarget(activeUnit, item2, item2TargetGroup, item2PossibleTargets, battle) + if ok then + return ok, targetId, attackerId + end + end + + return false, nil, nil +end + +function sub_5CFA6C(impl) + local attack1 = impl.attack1 + local attack1Class = attack1.type + + if attack1Class == Attack.Damage + or attack1Class == Attack.Drain + or attack1Class == Attack.DrainOverflow + then + local attack2 = impl.attack2 + if not attack2 then + return true + end + + local attack2Class = attack2.type + + if attack2Class == Attack.LowerDamage + or attack2Class == Attack.LowerInitiative + or attack2Class == Attack.Damage + or attack2Class == Attack.Drain + or attack2Class == Attack.DrainOverflow + then + return true + end + end + + return false +end + +-- Returns true/false +function sub_5CFB6C(impl, allyGroupId, targetGroupId) + local attack1 = impl.attack1 + local attack1Class = attack1.type + + if attack1Class ~= Attack.Doppelganger or allyGroupId == targetGroupId then + -- Attack is not Doppelganger or groups are the same + return false + end + + return true +end + +-- Returns secondary attack in case when primary attack has Damage, Drain or DrainOverflow attack class +function tryGetDamageOrDrainSecondAttack(impl) + local attack = impl.attack1 + local attackClass = attack.type + + if attackClass == Attack.Damage + or attackClass == Attack.Drain + or attackClass == Attack.DrainOverflow + then + return impl.attack2 + end + + return attack +end + +function getTransformSelfAltAttackOrSecondAttack(impl, attack) + local attack1 = impl.attack1 + + if attack1.type == Attack.TransformSelf then + return impl.altAttack + end + + if attack and attack1.id == attack.id then + return impl.attack2 + end + + return attack1 +end + +-- Returns true/false, target id +function sub_5D36BF(activeUnit, damage, attack2, altAttack, enemyGroup, possibleTargets, battle) + local ok, targetId = findAttackTarget(activeUnit.id, attack2, enemyGroup, possibleTargets, battle) + if ok then + return true, targetId + end + + if not altAttack then + return false, nil + end + + local attackClass = altAttack.type + + if attackClass ~= Attack.Damage + and attackClass ~= Attack.Drain + and attackClass ~= Attack.DrainOverflow + then + return findAttackTarget(activeUnit.id, altAttack, enemyGroup, possibleTargets, battle) + end + + if altAttack.reach ~= Reach.All then + return findDamageAttackTargetsForNonAllReach(altAttack, damage, enemyGroup, possibleTargets, battle) + end + + local allyGroup = nil + if battle:isUnitAttacker(activeUnit) then + allyGroup = battle.attacker.group + else + allyGroup = battle.defender + end + + if isTargetGroupAlwaysImmuneToAttackClassAndSource(attackClass, altAttack.source, allyGroup, enemyGroup, possibleTargets) then + return false, nil + end + + return findDamageAttackTargetsForAllReach(enemyGroup, possibleTargets) +end + +-- Returns BattleAction, target unit id, attacker unit id +function chooseAction( + -- Battle state + battle, + -- Unit for which to choose action + activeUnit, + -- List of unit possible battle actions + possibleActions, + -- Group that is a target for a unit attack + attackTargetGroup, + -- List of unit attack targets, indices of attack target group slots. + attackPossibleTargets, + -- Group that is a target for a `Battle1` equipped item in case of leader unit or nil if unit is not leader or don't wear item. + item1TargetGroup, + -- List of `Battle1` equipped item targets, indices of group slots. + item1PossibleTargets, + -- Group that is a target for a `Battle2` equipped item in case of leader unit or nil if unit is not leader or don't wear item. + item2TargetGroup, + -- List of `Battle2` equipped item targets, indices of group slots. + item2PossibleTargets) + + local activeUnitIsAttacker = battle:isUnitAttacker(activeUnit) + + local activeUnitGroup = nil + local enemyGroup = nil + + if activeUnitIsAttacker then + activeUnitGroup = battle.attacker.group + enemyGroup = battle.defender + else + enemyGroup = battle.attacker.group + activeUnitGroup = battle.defender + end + + local unknownGroup = nil + if attackTargetGroup.id == enemyGroup.id then + unknownGroup = enemyGroup + else + unknownGroup = activeUnitGroup + end + + local relativeCoeff = computeGroupsRelativeCoefficient(battle, activeUnitGroup, enemyGroup) + + local ok, + action, + selectedTargetId, + selectedAttackerId = checkGroupShouldRetreat(battle, possibleActions, activeUnit, activeUnitGroup, enemyGroup, activeUnitIsAttacker, relativeCoeff) + + if ok then + -- Decision to retreat was made + return action, selectedTargetId, selectedAttackerId + end + + local activeUnitIsLeader = isLeader(activeUnit) + if activeUnitIsLeader then + if relativeCoeff <= 0.3 + and checkGroupCanRetreat(possibleActions, activeUnit, activeUnitGroup, battle, false, false) + and not isGroupHasLessThanTwoUnitsAlive(battle, activeUnitGroup, true) + then + local shouldRetreat = true + if not activeUnitIsAttacker and isStackHaveMovement(enemyGroup) then + shouldRetreat = false + end + + if shouldRetreat then + -- Leader should retreat + return BattleAction.Retreat, activeUnit.id, activeUnit.id + end + end + end + + -- Check if we can select attack target + local selectedAttackTarget = nil + local damage = 0 + + if #attackPossibleTargets ~= 0 then + damage = attackGetDamageWithBuffsCheckTransform(activeUnit, battle) + if damage > 0 then + local attackSource = getSoldierAttackSource(activeUnit.impl) + selectedAttackTarget = selectAttackTarget(battle, damage, unknownGroup, attackPossibleTargets, attackSource) + end + end + + -- Check leader and its items + if activeUnitIsLeader + and relativeCoeff <= 0.7 + and canLeaderUseItemInBattle(selectedAttackTarget, possibleActions, item1PossibleTargets, item2PossibleTargets) + then + -- Leader can use item in battle + local ok, + selectedTargetId, + selectedAttackerId = findEquipmentAttackTarget(activeUnit, activeUnitGroup, item1TargetGroup, item1PossibleTargets, item2TargetGroup, item2PossibleTargets, battle) + if ok then + -- Use item + return BattleAction.UseItem, selectedTargetId, selectedAttackerId + end + end + + -- Check if we should defend + if #attackPossibleTargets == 0 + or not hasValue(possibleActions, BattleAction.Attack) then + -- Decision to defend was made + return BattleAction.Defend, activeUnit.id, activeUnit.id + end + + -- Check if we could perform attack action + local v43 = true + local activeUnitImpl = activeUnit.impl + local attack = activeUnitImpl.attack1 + if attack.reach == Reach.All then + local v1 = isTargetGroupAlwaysImmuneToAttackClassAndSource(attack.type, attack.source, activeUnitGroup, unknownGroup, attackPossibleTargets) + local v2 = sub_5D02D5(attack.type, battle, unknownGroup) + if not v1 + and not v2 + then + local ok, targetId = findDamageAttackTargetsForAllReach(unknownGroup, attackPossibleTargets) + if ok then + -- Attack with reach 'All' + return BattleAction.Attack, targetId, activeUnit.id + end + end + + v43 = false + end + + if v43 then + if selectedAttackTarget then + -- Attack selected target + return BattleAction.Attack, selectedAttackTarget.id, activeUnit.id + end + + if sub_5CFA6C(activeUnitImpl) + or sub_5CFB6C(activeUnitImpl, activeUnitGroup.id, attackTargetGroup.id) then + local found, targetId = findDamageAttackTargetsForNonAllReach(attack, damage, unknownGroup, attackPossibleTargets, battle) + if found then + -- Attack with reaches other than 'All' + return BattleAction.Attack, targetId, activeUnit.id + end + else + local attack2 = tryGetDamageOrDrainSecondAttack(activeUnitImpl) + local altAttack = getTransformSelfAltAttackOrSecondAttack(activeUnitImpl, attack2) + + local ok, targetId = sub_5D36BF(activeUnit, damage, attack2, altAttack, unknownGroup, attackPossibleTargets, battle) + if ok then + -- Attack considering secondary or alt. attacks + return BattleAction.Attack, targetId, activeUnit.id + end + end + end + + -- Nothing to attack, defend + return BattleAction.Defend, activeUnit.id, activeUnit.id +end diff --git a/Scripts/settings.lua b/Scripts/settings.lua index 7f81c395..c593154d 100644 --- a/Scripts/settings.lua +++ b/Scripts/settings.lua @@ -127,38 +127,38 @@ settings = { outlineColor = { red = 0, green = 0, blue = 0 }, - - -- Movement cost on water tiles - water = { - -- Default movement cost - default = 6, - -- Movement cost for non water-only stacks with dead leader - withDeadLeader = 12, - -- Movement cost for stacks with water movement bonus - withBonus = 2, - -- Movement cost for water-only stacks - waterOnly = 2, - }, - - -- Movement cost on forest tiles - forest = { - -- Default movement cost - default = 4, - -- Movement cost for stacks with dead leader - withDeadLeader = 8, - -- Movement cost for stacks with forest movement bonus - withBonus = 2, - }, - - -- Movement cost on plain tiles - plain = { - -- Default movement cost - default = 2, - -- Movement cost for stacks with dead leader - withDeadLeader = 4, - -- Movement cost for stacks without plain movement bonus on road tiles - onRoad = 1, - }, + + -- Movement cost on water tiles + water = { + -- Default movement cost + default = 6, + -- Movement cost for non water-only stacks with dead leader + withDeadLeader = 12, + -- Movement cost for stacks with water movement bonus + withBonus = 2, + -- Movement cost for water-only stacks + waterOnly = 2, + }, + + -- Movement cost on forest tiles + forest = { + -- Default movement cost + default = 4, + -- Movement cost for stacks with dead leader + withDeadLeader = 8, + -- Movement cost for stacks with forest movement bonus + withBonus = 2, + }, + + -- Movement cost on plain tiles + plain = { + -- Default movement cost + default = 2, + -- Movement cost for stacks with dead leader + withDeadLeader = 4, + -- Movement cost for stacks without plain movement bonus on road tiles + onRoad = 1, + }, }, lobby = { @@ -243,6 +243,11 @@ settings = { carryXpOverUpgrade = false, -- Allows units to receive multiple upgrades per single battle allowMultiUpgrade = false, + -- Shows message box when error occurs in AI battle action script. + -- When disabled, error messages are silently written to error log + debugAi = false, + -- Fallback action for AI controlled units in case of script errors + fallbackAction = BattleAction.Defend, }, -- Create mss32 proxy dll log files with debug info diff --git a/Scripts/textids.lua b/Scripts/textids.lua index a16be5a0..0406ccb9 100644 --- a/Scripts/textids.lua +++ b/Scripts/textids.lua @@ -253,20 +253,36 @@ textids = { -- Fallback text "Wrong room password" wrongRoomPassword = "", }, - - generator = { - -- Description text for randomly generated scenarios - -- Fallback text "Random scenario based on template '%TMPL%'. Seed: %SEED%. Starting gold: %GOLD%. Roads: %ROADS%%. Forest: %FOREST%%." - description = "", - -- Generator could not process game data from dbf tables or .ff files - -- Error details are logged in mssProxyError.log - -- Fallback text "Could not read game data needed for scenario generator.\nSee mssProxyError.log for details" - wrongGameData = "", - -- Error occured during scenario generation - -- Fallback text "Error during random scenario map generation.\nSee mssProxyError.log for details". - generationError = "", - -- Generator failed to create scenario after specified number of attempts - -- Fallback text "Could not generate scenario map after %NUM% attempts.\nPlease, adjust template contents or settings" - limitExceeded = "", - }, + + generator = { + -- Description text for randomly generated scenarios + -- Fallback text "Random scenario based on template '%TMPL%'. Seed: %SEED%. Starting gold: %GOLD%. Roads: %ROADS%%. Forest: %FOREST%%." + description = "", + -- Generator could not process game data from dbf tables or .ff files + -- Error details are logged in mssProxyError.log + -- Fallback text "Could not read game data needed for scenario generator.\nSee mssProxyError.log for details" + wrongGameData = "", + -- Error occured during scenario generation + -- Fallback text "Error during random scenario map generation.\nSee mssProxyError.log for details". + generationError = "", + -- Generator failed to create scenario after specified number of attempts + -- Fallback text "Could not generate scenario map after %NUM% attempts.\nPlease, adjust template contents or settings" + limitExceeded = "", + }, + + resourceMarket = { + -- Resource market site description for encyclopedia + -- Fallback text is "(Resource market)" + encyDesc = "", + -- Infinite amount of resources string. + -- Fallback text is "Inf." + infiniteAmount = "", + -- Exchange description for market window in game. + -- The text must contain keywords "%RES1%" and "%RES2%". + -- Fallback text is "You offer %RES1% to get %RES2% in return." + exchangeDesc = "", + -- Exchange is not available hint for market window in game. + -- Fallback text is "N/A" + exchangeNotAvailable = "", + } } diff --git a/luaApi.md b/luaApi.md index c0a4fac1..0190e151 100644 --- a/luaApi.md +++ b/luaApi.md @@ -42,15 +42,33 @@ The function only accessible to scripts where scenario access is appropriate: - `drainLevel.lua` - custom attack reach scripts - custom unit modifier script +- AI battle actions script `checkEventCondition` has `scenario` as its argument so `getScenario` is not bound to it. ```lua getScenario():getUnit(unitId) ``` +##### randomNumber +Generates random number in range \[0 : maxValue) using ingame generator +```lua +local n = randomNumber(100) +``` +##### getGlobal +Returns [global data storage](luaApi.md#global) used by game. +```lua +local data = getGlobal() +local variables = data.variables +``` +##### getGame +Returns [game](luaApi.md#game) restrictions and constants. +```lua +getGame().unitMaxDamage +``` --- #### Enumerations + ##### Race ```lua Race = { Human, Undead, Heretic, Dwarf, Neutral, Elf } @@ -167,11 +185,94 @@ BattleStatus = { } ``` +##### BattleAction +``` +BattleAction = { Attack, Skip, Retreat, Wait, Defend, Auto, UseItem } +``` + +##### Retreat +``` +Retreat = { NoRetreat, CoverAndRetreat, FullRetreat } +``` + ##### Relation ``` Relation = { War, Neutral, Peace } ``` +##### Order +``` +Order = { Normal, Stand, Guard, AttackStack, DefendStack, SecureCity, + Roam, MoveToLocation, DefendLocation, Bezerk, Assist, Steal, DefendCity } +``` + +##### IdType +``` +IdType = { + Empty, -- Empty id + ApplicationText, -- Entries of TApp.dbf and TAppEdit.dbf + Building, -- Entries of GBuild.dbf + Race, -- Entries of GRace.dbf + Lord, -- Entries of GLord.dbf + Spell, -- Entries of GSpells.dbf + UnitGlobal, -- Unit implementations, entries of GUnits.dbf + UnitGenerated, -- Runtime-generated unit implementations + UnitModifier, -- Unit modifiers, entries of GModif.dbf + Attack, -- Attacks, entries of GAttacks.dbf + TextGlobal, -- Entries of TGlobal.dbf + LandmarkGlobal, -- Entries of GLmark.dbf + ItemGlobal, -- Base items, entries of GItem.dbf + NobleAction, -- Noble (thief) actions, entries of GAction.dbf + DynamicUpgrade, -- Dynamic upgrade rules, entries of GDynUpgr.dbf + DynamicAttack, -- Runtime-generated unit primary attacks + DynamicAltAttack, -- Runtime-generated unit primary alternative attacks + DynamicAttack2, -- Runtime-generated unit secondary attacks + DynamicAltAttack2, -- Runtime-generated unit secondary alternative attacks + CampaignFile, -- Campaign files + Plan, -- Utility for fast object lookup by map coordinates + ObjectCount, -- Number of objects in scenario file + ScenarioFile, -- Scenario files + Map, -- Scenario map + MapBlock, -- Blocks of scenario map + ScenarioInfo, -- Scenario information + SpellEffects, + Fortification, -- Capitals and villages + Player, -- Players in scenario + PlayerKnownSpells, -- Spells known by player in scenario + Fog, -- Fog of war for player in scenario + PlayerBuildings, -- Capital buildings + Road, -- Roads on scenario map + Stack, -- Stacks in scenario + Unit, -- Units in scenario + Landmark, -- Landmarks in scenario + Item, -- Items in scenario + Bag, -- Bags in scenario + Site, -- Sites in scenario + Ruin, -- Ruins in scenario + Tomb, -- Grave markers in scenario + Rod, -- Rods in scenario + Crystal, -- Gold mines and mana sources in scenario + Diplomacy, -- Diplomacy rules in scenario + SpellCast, + Location, -- Location on scenario map + StackTemplate, -- Stack templates in scenario + Event, -- Events in scenario + StackDestroyed, -- Information about stacks defeated in scenario + TalismanCharges, -- Talisman charges counter in scenario + Mountains, -- Mountains in scenario + SubRace, -- Subraces in scenario + SubRaceType, -- Entries of GSubRace.dbf + QuestLog, -- Scenario quest log + TurnSummary, -- Brief information about last turn in scenario + ScenarioVariable -- Scenario variables +} +``` + +##### Resource +``` +Resource = { Gold, InfernalMana, LifeMana, DeathMana, RunicMana, GroveMana } +``` + --- #### Point @@ -214,6 +315,213 @@ id.value -- Can be used as Lua table key for best performance. id.typeIndex ``` +##### type +Returns [type](luaApi.md#idtype) of identifier. +Identifier type can help to distinguish one object from another. +```lua +id.type +``` +##### summonId +Creates special id for summoning units in battle using specified position in group. +Position in group should be in \[0 : 5\] range. +```lua +Id.summonId(possibleTarget) +``` + +--- + +#### Game +Represents game restrictions and constants. +Allows to access `settings.lua`. + +Methods: +##### unitMaxDamage +Maximum damage unit attack can inflict in battle. `unitMaxDamage` from `settings.lua`. +```lua +game.unitMaxDamage +``` +##### unitMinDamage +Minimum damage unit attack can inflict in battle. Currently 1. +```lua +game.unitMinDamage +``` +##### unitMaxArmor +Maximum armor unit can have. `unitMaxArmor` from `settings.lua`. +```lua +game.unitMaxArmor +``` +##### leaderAdditionalDamage +Additional damage granted by leader ability `Heavy strike`. Currently 100. +```lua +game.leaderAdditionalDamage +``` + +--- + +#### Global +Represents global data storage used by game. +Allows to access contents of dbf files in 'Globals' folder of the game. + +Methods: +##### variables +Returns [global variables](luaApi.md#global-variables). +```lua +local v = getGlobal().variables +``` + +--- + +#### Global Variables +Allows to access contents of `GVars.dbf`. + +Methods: +```lua +-- Instructor skill bonus experience, 'WEAPN_MSTR' +variables.weapnMstr +-- Max additional initiative points that randomly added to unit in battle. 'BAT_INIT' +variables.batInit +-- Max additional damage points that randomly added to unit damage in battle. 'BAT_DAMAGE' +variables.batDamage +-- 'BAT_ROUND' +variables.batRound +-- 'BAT_BREAK' +variables.batBreak +-- 'BAT_BMODIF' +variables.batBModif +-- Initiative debuff. 'BATLOWERI' +variables.batLoweri +-- Maximum number of abilities leader can learn. 'LDRMAXABIL' +variables.ldrMaxAbil +-- Spy discovery chance per turn. 'SPY_DISCOV' +variables.spyDiscov +-- Damage from thief action 'poison city'. 'POISON_C' +variables.poisonC +-- Damage from thief action 'poison stack'. 'POISON_S' +variables.poisonS +-- Bribe multiplier. 'BRIBE' +variables.bribe +-- 'STEAL_RACE' +variables.stealRace +-- 'STEAL_NEUT' +variables.stealNeut +-- Minimal riot duration in days. 'RIOT_MIN' +variables.riotMin +-- Maximal riot duration in days. 'RIOT_MAX' +variables.riotMax +-- Percentage of riot damage. 'RIOT_DMG' +variables.riotDmg +-- Percentage of the original price of the items at sale. 'SELL_RATIO' +variables.sellRatio +-- Land transformation after city capture. 'T_CAPTURE' +variables.tCapture +-- Land transformation per turn by capital. 'T_CAPITAL' +variables.tCapital +-- Range of land transformation by rod per turn. 'ROD_RANGE' +variables.rodRange +-- Profit per mana crystal or gold mine per turn. 'CRYSTAL_P' +variables.crystalP +-- 'CONST_URG' +variables.constUrg +-- Bonus per day regeneration for fighter leader. 'REGEN_LWAR' +variables.regenLwar +-- Bonus per day regeneration for units in ruins. 'REGEN_RUIN' +variables.regenRuin +-- Diplomacy level representing peace. 'D_PEACE' +variables.dPeace +-- Diplomacy level representing war. 'D_WAR' +variables.dWar +-- Diplomacy level representing neutrality. 'D_NEUTRAL' +variables.dNeutral +-- 'D_GOLD' +variables.dGold +-- 'D_MK_ALLY' +variables.dMkAlly +-- 'D_ATTACK_SC' +variables.dAttakSc +-- 'D_ATTACK_FO' +variables.dAttakFo +-- 'D_ATTACK_PC' +variables.dAttakPc +-- 'D_ROD' +variables.dRod +-- 'D_REF_ALLY' +variables.dRefAlly +-- 'D_BK_ALLY' +variables.dBkAlly +-- 'D_NOBLE' +variables.dNoble +-- 'D_BKA_CHANCE' +variables.dBkaChnc +-- 'D_BKA_TURN' +variables.dBkaTurn +-- Capital protection. 'PROT_CAP' +variables.protCap +-- Additional gold on easy difficulty. 'BONUS_E' +variables.bonusE +-- Additional gold on average difficulty. 'BONUS_A' +variables.bonusA +-- Additional gold on hard difficulty. 'BONUS_H' +variables.bonusH +-- Additional gold on very hard difficulty. 'BONUS_V' +variables.bonusV +-- Income increase on easy difficulty. 'INCOME_E' +variables.incomeE +-- Income increase on average difficulty. 'INCOME_A' +variables.incomeA +-- Income increase on hard difficulty. 'INCOME_H' +variables.incomeH +-- Income increase on very hard difficulty. 'INCOME_V' +variables.incomeV +-- 'GU_RANGE' +variables.guRange +-- 'PA_RANGE' +variables.paRange +-- 'LO_RANGE' +variables.loRange +-- Armor bonus when unit uses defend in battle. 'DFENDBONUS' +variables.defendBonus +-- 'TALIS_CHRG' +variables.talisChrg +-- Chance to get spells with capture of a capital. 'GAIN_SPELL' +variables.gainSpell +``` +##### rodCost +Rod placement [cost](luaApi.md#currency). `ROD_COST` +```lua +variables.rodCost +``` +##### morale +Input tier values must be in range \[1 : 6\]. `MORALE_n` +```lua +variables:morale(1) +``` +##### batBoostd +Damage boost values for various levels. Levels must be in range \[1 : 4\]. `BATBOOSTDn` +```lua +variables:batBoostd(4) +``` +##### batLowerd +Damage debuff values for various levels. Levels must be in range \[1 : 2\]. `BATLOWERDn` +```lua +variables:batLowerd(1) +``` +##### tCity +Land transformation per turn by cities of different tiers. Tier must be in range \[1 : 5\]. `T_CITYn` +```lua +variables:tCity(1) +``` +##### prot +City protection values for various tier levels. Tier must be in range \[1 : 6\]. In case of tier 6, returns `protCap`. `PROT_n` +```lua +variables:prot(3) +``` +##### splPwr +Input tier values must be in range \[1 : 5\]. `SPLPWR_n` +```lua +variables:splPwr(2) +``` + +--- #### Modifier Represents unit modifier. Modifiers wrap [unit implementation](luaApi.md#unit-implementation). @@ -483,6 +791,18 @@ Returns true if group has specified [unit](luaApi.md#unit-1) or [unit id](luaApi group:hasUnit(unit) group:hasUnit(Id.new('S143UN0001')) ``` +##### getUnitPosition +Returns unit position in group, or -1 if unit not found. +```lua +group:getUnitPosition(unit) +group:getUnitPosition(unit.id) +``` +##### subrace +Returns group [subrace](luaApi.md#subrace). +In case of group inside [ruin](luaApi.md#ruin), returns -1 since ruins do not belong to subraces. +```lua +group.subrace +``` --- @@ -594,6 +914,21 @@ Returns equipped [item](luaApi.md#item-2) by [equipment](luaApi.md#equipment) va ```lua stack:getEquippedItem(Equipment.Boots) ``` +##### order +Returns stack [order](luaApi.md#order). +```lua +stack.order +``` +##### orderTargetId +Returns stack's order target [id](luaApi.md#id). +```lua +stack.orderTargetId +``` +##### aiOrder +Returns stack [AI order](luaApi.md#order). +```lua +stack.aiOrder +``` ```lua --- Returns stack current movement points. stack.movement @@ -620,6 +955,11 @@ Returns fort position as a [point](luaApi.md#point). ```lua fort.position ``` +##### entrance +Return fort entrance coordinates as a [point](luaApi.md#point). +```lua +fort.entrance +``` ##### owner Returns [player](luaApi.md#player) that owns the fort. Neutral forts are owned by neutral player. ```lua @@ -658,6 +998,121 @@ fort.tier --- +#### Merchant item +Represents item sold by [merchant](luaApi.md#merchant). + +Methods: +##### base +Returns [base item](luaApi.md#item-base). +```lua +merchantItem.base +``` +##### amount +Returns amount of items in merchant stock. +```lua +merchantItem.amount +``` + +--- + +#### Merchant +Represents Merchant on a map. + +Methods: +##### id +Returns merchant [id](luaApi.md#id). The value is unique for every merchant on scenario map. +```lua +merchant.id +``` +##### position +Returns merchant position as a [point](luaApi.md#point). +```lua +merchant.position +``` +##### visitors +Returns list of [players](luaApi.md#player) that have visited the merchant. +```lua +merchant.visitors +``` +##### items +Returns list of [merchant items](luaApi.md#merchant-item). +```lua +merchant.items +``` +##### temple +Returns true if merchant can be used as a temple for AI to heal. +```lua +merchant.temple +``` + +--- + +#### Mercenary unit +Represents unit for hire in mercenary camp. + +Methods: +##### impl +Returns [unit implementation](luaApi.md#unit-implementation). +```lua +mercenary.impl +``` +##### unique +Returns true is unit can be hired only once. +```lua +mercenaryUnit.unique +``` + +--- + +#### Mercenary +Represents Mercenary camp on a map. + +Methods: +##### id +Returns mercenary camp [id](luaApi.md#id). The value is unique for every mercenary camp on scenario map. +```lua +mercenary.id +``` +##### position +Returns mercenary camp position as a [point](luaApi.md#point). +```lua +mercenary.position +``` +##### visitors +Returns list of [players](luaApi.md#player) that have visited the mercenary camp. +```lua +mercenary.visitors +``` +##### units +Returns list of [mercenary units](luaApi.md#mercenary-unit). +```lua +mercenary.units +``` + +--- + +#### Trainer +Represents Trainer on a map. + +Methods: +##### id +Returns trainer [id](luaApi.md#id). The value is unique for every trainer on scenario map. +```lua +trainer.id +``` +##### position +Returns trainer position as a [point](luaApi.md#point). +```lua +trainer.position +``` +##### visitors +Returns list of [players](luaApi.md#player) that have visited the trainer. +```lua +trainer.visitors +``` + +--- + #### Ruin Represents Ruin on a map. Ruin contains a garrison [group](luaApi.md#group) of 6 [unit slots](luaApi.md#unit-slot). @@ -717,6 +1172,28 @@ rod.owner --- +#### Crystal +Represents gold mine or mana source on a map. + +Methods: +##### id +Returns crystal [id](luaApi.md#id). +```lua +crystal.id +``` +##### position +Returns crystal position as a [point](luaApi.md#point). +```lua +crystal.position +``` +##### resource +Returns crystal [resource](luaApi.md#resource) type. +```lua +crystal.resource +``` + +--- + #### Dynamic upgrade Represents rules that applied when unit makes its progress gaining levels. Records in GDynUpgr.dbf are dynamic upgrades. @@ -942,6 +1419,70 @@ if (unit == nil) then return end ``` +##### getItem +Searches for [item](luaApi.md#item-2) by id string or item [id](luaApi.md#id), returns `nil` if not found. +```lua +local item = scenario:getItem('S143IM0001') +if not item then + return +end +``` +##### getCrystal +Searches for [crystal](luaApi.md#crystal) by: +- id string +- [id](luaApi.md#id) +- pair of coordinates +- [point](luaApi.md#point) + +Returns `nil` if not found. +```lua +local crystal = scenario:getCrystal('S143CR0003') +if not crystal then + return +end +``` +##### getMerchant +Searches for [merchant](luaApi.md#merchant) by: +- id string +- [id](luaApi.md#id) +- pair of coordinates +- [point](luaApi.md#point) + +Returns `nil` if not found. +```lua +local merchant = scenario:getMerchant('S143SI0001') +if not merchant then + return +end +``` +##### getMercenary +Searches for [mercenary camp](luaApi.md#mercenary) by: +- id string +- [id](luaApi.md#id) +- pair of coordinates +- [point](luaApi.md#point) + +Returns `nil` if not found. +```lua +local mercenary = scenario:getMercenary('S143SI0002') +if not mercenary then + return +end +``` +##### getTrainer +Searches for [trainer](luaApi.md#trainer) by: +- id string +- [id](luaApi.md#id) +- pair of coordinates +- [point](luaApi.md#point) + +Returns `nil` if not found. +```lua +local trainer = scenario:getTrainer('S143SI0005') +if not trainer then + return +end +``` ##### findStackByUnit Searches for [stack](luaApi.md#stack) that has specified [unit](luaApi.md#unit-1) among all the stacks in the whole [scenario](luaApi.md#scenario). You can also use unit id string or [id](luaApi.md#id). @@ -976,6 +1517,26 @@ if ruin == nil then end ``` **Note** that this search is heavy in terms of performance, so you probably want to minimize excessive calls and use variables to store its results. +##### name +Returns scenario name or empty string if scenario is unnamed. +```lua +scenario.name +``` +##### description +Returns scenario description or empty string if scenario has no description. +```lua +scenario.description +``` +##### author +Returns scenario author or empty string if no author specified. +```lua +scenario.author +``` +##### seed +Returns scenario initial seed used by random generator. +```lua +scenario.seed +``` ##### day Returns number of current day in game. ```lua @@ -995,6 +1556,83 @@ if diplomacy == nil then return end ``` +##### forEachStack +Searches for every [stack](luaApi.md#stack) on a map and calls specified function on it. +```lua +scenario:forEachStack(function (stack) + log('Visit stack ' .. tostring(stack.id)) +end) +``` +##### forEachLocation +Searches for every [location](luaApi.md#location) on a map and calls specified function on it. +```lua +scenario:forEachLocation(function (location) + log('Visit location ' .. tostring(location.id)) +end) +``` +##### forEachFort +Searches for every [fort](luaApi.md#fort) on a map and calls specified function on it. +```lua +scenario:forEachFort(function (city) + log('Visit city ' .. tostring(city.id)) +end) +``` +##### forEachRuin +Searches for every [ruin](luaApi.md#ruin) on a map and calls specified function on it. +```lua +scenario:forEachRuin(function (ruin) + log('Visit ruin ' .. tostring(ruin.id)) +end) +``` +##### forEachRod +Searches for every [rod](luaApi.md#rod) on a map and calls specified function on it. +```lua +scenario:forEachRod(function (rod) + log('Visit rod ' .. tostring(rod.id)) +end) +``` +##### forEachPlayer +Searches for every [player](luaApi.md#player) on a map and calls specified function on it. +```lua +scenario:forEachPlayer(function (player) + log('Visit player ' .. tostring(player.id)) +end) +``` +##### forEachUnit +Searches for every [unit](luaApi.md#unit-1) on a map and calls specified function on it. +```lua +scenario:forEachUnit(function (unit) + log('Visit unit ' .. tostring(unit.id)) +end) +``` +##### forEachCrystal +Searches for every [crystal](luaApi.md#crystal) on a map and calls specified function on it. +```lua +scenario:forEachCrystal(function (crystal) + log('Visit crystal ' .. tostring(crystal.id)) +end) +``` +##### forEachMerchant +Searches for every [merchant](luaApi.md#merchant) on a map and calls specified function on it. +```lua +scenario:forEachMerchant(function (merchant) + log('Visit merchant ' .. tostring(merchant.id)) +end) +``` +##### forEachMercenary +Searches for every [mercenary camp](luaApi.md#mercenary) on a map and calls specified function on it. +```lua +scenario:forEachMercenary(function (mercenary) + log('Visit mercenary ' .. tostring(mercenary.id)) +end) +``` +##### forEachTrainer +Searches for every [trainer](luaApi.md#trainer) on a map and calls specified function on it. +```lua +scenario:forEachTrainer(function (trainer) + log('Visit trainer ' .. tostring(trainer.id)) +end) +``` --- @@ -1159,6 +1797,24 @@ item.sellValue --- +#### Battle Turn +Represents unit action inside battle round. + +Methods: +##### unit +Returns [unit](luaApi.md#unit-1) that performs the turn. +```lua +turn.unit +``` +##### attackCount +Returns number of attacks unit can perform in its turn. +Units with double attack (`turn.unit.impl.attacksTwice`) will have 2 in `attackCount`. +```lua +turn.attackCount +``` + +--- + #### Battle Represents battle information. @@ -1180,13 +1836,19 @@ Returns true if autobattle mode is turned on. ```lua battle.autoBattle ``` +##### fastBattle +Returns true if fast battle is turned on. +When fast battle is active, [auto battle](luaApi.md#autobattle) is active too. +```lua +battle.fastBattle +``` ##### attackerPlayer -Returns [player](luaApi.md#player) that started battle. +Returns [player](luaApi.md#player) that started battle or `nil` if not found. ```lua battle.attackerPlayer ``` ##### defenderPlayer -Returns [player](luaApi.md#player) that was attacked. +Returns [player](luaApi.md#player) that was attacked or `nil` if not found. ```lua battle.defenderPlayer ``` @@ -1199,10 +1861,152 @@ battle.attacker ##### defender Returns [group](luaApi.md#group) that was attacked. Defender group can represent units of a [stack](luaApi.md#stack), [fort](luaApi.md#fort) or [ruin](luaApi.md#ruin). -Use `group.id` to get actual type of a group. +Use `group.id.type` to get actual type of a group. ```lua battle.defender ``` +##### isUnitAttacker +Returns true if [unit](luaApi.md#unit-1) belongs to attacker group. +Method also accepts unit [ids](luaApi.md#id). +```lua +battle:isUnitAttacker(unit) +-- Same check with unit id +battle:isUnitAttacker(unit.id) +``` +##### getUnitActions +Returns possible actions and attack options for specified [unit](luaApi.md#unit-1). +Method also accepts unit [ids](luaApi.md#id). +Returns: +- list of [battle actions](luaApi.md#BattleAction) that unit can perform. +- [group](luaApi.md#group) that is a target for a unit attack. +- list of unit attack targets, indices of attack target group slots. +- [group](luaApi.md#group) that is a target for a `Battle1` equipped item in case of leader unit. +- list of `Battle1` equipped item targets, indices of group slots. +- [group](luaApi.md#group) that is a target for a `Battle2` equipped item in case of leader unit. +- list of `Battle2` equipped item targets, indices of group slots. +```lua +local actions, + attackTargetGroup, + attackTargets, + item1TargetGroup, + item1Targets, + item2TargetGroup, + item2Targets = battle:getUnitActions(unit) +``` +##### getRetreatStatus +Returns [retreat status](luaApi.md#Retreat) of attacker or defender group. +```lua +local attackerStatus = battle:getRetreatStatus(true) +local defenderStatus = battle:getRetreatStatus(false) +``` +##### decidedToRetreat +Returns true if decision about groups retreat was made and should not be reconsidered. +```lua +battle.decidedToRetreat +``` +##### afterBattle +Returns true if battle is over but healers can make one more turn to heal allies. +```lua +battle.afterBattle +``` +##### duel +Returns true if battle is a duel between thief and a stack leader. +All units except leaders are marked with `Hidden` [battle status](luaApi.md#battlestatus). +```lua +battle.duel +``` +##### turnsOrder +Returns list of [battle turns](luaApi.md#battle-turn) remaining in the current round of battle. +Position of elements in the list corresponds to order of remaining turns in current round. +In other words, at the start of a round, battle turns in the list are sorted according to units initiative, including an additional random initiative. +```lua +battle.turnsOrder +``` +##### isUnitRevived +Returns true if specified [unit](luaApi.md#unit-1) was revived during battle. +Method also accepts unit [ids](luaApi.md#id). +```lua +battle:isUnitRevived(unit) +``` +##### isUnitWaiting +Returns true if specified [unit](luaApi.md#unit-1) skipped its turn and waiting. +Method also accepts unit [ids](luaApi.md#id). +```lua +battle:isUnitWaiting(battle.attacker.leader) +``` +##### getUnitShatteredArmor +Returns amount of unit armor shattered in battle so far. +Method also accepts unit [ids](luaApi.md#id). +```lua +battle:getUnitShatteredArmor(unit) +-- Same but using id +battle:getUnitShatteredArmor(unit.id) +``` +##### getUnitFortificationArmor +Returns armor that is granted to unit by fortification, if any. +Method also accepts unit [ids](luaApi.md#id). +```lua +battle:getUnitFortificationArmor(unit) +-- Same but using id +battle:getUnitFortificationArmor(unit.id) +``` +##### isUnitResistantToSource +Returns true if specified unit is resistant to [attack source](luaApi.md#source). +Method also accepts unit [ids](luaApi.md#id). +```lua +battle:isUnitResistantToSource(unit, attackSource) +``` +##### isUnitResistantToClass +Returns true if specified unit is resistant to [attack class](luaApi.md#attack). +Method also accepts unit [ids](luaApi.md#id). +```lua +battle:isUnitResistantToClass(unit, attackSource) +``` +##### getUnitDisableRound +Returns round when paralyze, petrify or fear was applied to [unit](luaApi.md#unit-1). Returns 0 if unit is not disabled. +Method also accepts unit [ids](luaApi.md#id). +```lua +battle:getUnitDisableRound(unit.id) +``` +##### getUnitPoisonRound +Returns round when long poison was applied to [unit](luaApi.md#unit-1). Returns 0 if unit is not poisoned. +Method also accepts unit [ids](luaApi.md#id). +```lua +battle:getUnitPoisonRound(unit) +``` +##### getUnitFrostbiteRound +Returns round when long frostbite was applied to [unit](luaApi.md#unit-1). Returns 0 if unit is not frozen. +Method also accepts unit [ids](luaApi.md#id). +```lua +battle:getUnitFrostbiteRound(unit) +``` +##### getUnitBlisterRound +Returns round when long blister was applied to [unit](luaApi.md#unit-1). Returns 0 if unit is not burning. +Method also accepts unit [ids](luaApi.md#id). +```lua +battle:getUnitBlisterRound(unit) +``` +##### getUnitTransformRound +Returns round when long transform was applied to [unit](luaApi.md#unit-1). Returns 0 if unit is not transformed. +Method also accepts unit [ids](luaApi.md#id). +```lua +battle:getUnitTransformRound(unit) +``` +##### setRetreatStatus +Sets [retreat status](luaApi.md#Retreat) of attacker or defender group. +This method can be only used in AI battle action script. +```lua +-- Attacker group is going to fully retreat +battle:setRetreatStatus(true, Retreat.FullRetreat) +-- Do not retreat defender group +battle:setRetreatStatus(false, Retreat.NoRetreat) +``` +##### setDecidedToRetreat +Notifies battle state that the decision about groups retreat was made and it is final. +This method can be only used in AI battle action script. +```lua +battle:setDecidedToRetreat() +``` --- diff --git a/mss32/include/aiattitudes.h b/mss32/include/aiattitudes.h index 85ce08ea..64557836 100644 --- a/mss32/include/aiattitudes.h +++ b/mss32/include/aiattitudes.h @@ -20,7 +20,7 @@ #ifndef AIATTITUDES_H #define AIATTITUDES_H -#include "categories.h" +#include "aiattitudescat.h" #include "d2assert.h" #include "textandid.h" diff --git a/mss32/include/aiattitudescat.h b/mss32/include/aiattitudescat.h new file mode 100644 index 00000000..ab81f25c --- /dev/null +++ b/mss32/include/aiattitudescat.h @@ -0,0 +1,57 @@ +/* + * This file is part of the modding toolset for Disciples 2. + * (https://github.com/VladimirMakeev/D2ModdingToolset) + * Copyright (C) 2024 Vladimir Makeev. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef AIATTITUDESCAT_H +#define AIATTITUDESCAT_H + +#include "categories.h" + +namespace game { + +struct LAttitudesCategoryTable : CEnumConstantTable +{ }; + +struct LAttitudesCategory : public Category +{ }; + +namespace AttitudeCategories { + +struct Categories +{ + LAttitudesCategory* small; + LAttitudesCategory* medium; + LAttitudesCategory* large; + LAttitudesCategory* humongous; +}; + +Categories& get(); + +} // namespace AttitudeCategories + +namespace LAttitudesCategoryTableApi { + +using Api = CategoryTableApi::Api; + +Api& get(); + +} // namespace LAttitudesCategoryTableApi + +} // namespace game + +#endif // AIATTITUDESCAT_H diff --git a/mss32/include/aiattitudestable.h b/mss32/include/aiattitudestable.h new file mode 100644 index 00000000..bc97ae70 --- /dev/null +++ b/mss32/include/aiattitudestable.h @@ -0,0 +1,70 @@ +/* + * This file is part of the modding toolset for Disciples 2. + * (https://github.com/VladimirMakeev/D2ModdingToolset) + * Copyright (C) 2024 Vladimir Makeev. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef AIATTITUDESTABLE_H +#define AIATTITUDESTABLE_H + +#include "aiattitudescat.h" +#include "d2map.h" +#include "smartptr.h" + +namespace game { + +struct CAiAttitudesTableVftable; +struct CAiAttitudes; + +struct CAiAttitudesTableData +{ + Map> attitudes; +}; + +assert_size(CAiAttitudesTableData, 28); + +struct CAiAttitudesTable +{ + CAiAttitudesTableVftable* vftable; + CAiAttitudesTableData* data; +}; + +assert_size(CAiAttitudesTable, 8); + +struct CAiAttitudesTableVftable +{ + using Destructor = CAiAttitudesTable*(__thiscall*)(CAiAttitudesTable* thisptr, char flags); + Destructor destructor; +}; + +assert_vftable_size(CAiAttitudesTableVftable, 1); + +namespace CAiAttitudesTableApi { + +struct Api +{ + using Find = CAiAttitudes*(__thiscall*)(const CAiAttitudesTable* table, + const LAttitudesCategory* category); + Find find; +}; + +Api& get(); + +} // namespace CAiAttitudesTableApi + +} // namespace game + +#endif // AIATTITUDESTABLE_H diff --git a/mss32/include/aipriority.h b/mss32/include/aipriority.h index 6b135bd9..ef13c4cc 100644 --- a/mss32/include/aipriority.h +++ b/mss32/include/aipriority.h @@ -20,14 +20,28 @@ #ifndef AIPRIORITY_H #define AIPRIORITY_H +#include "d2assert.h" + namespace game { +struct IAiPriorityVftable; + struct IAiPriority { - const void* vftable; + const IAiPriorityVftable* vftable; int priority; }; +assert_size(IAiPriority, 8); + +struct IAiPriorityVftable +{ + using Destructor = void(__thiscall*)(IAiPriority* thisptr, char flags); + Destructor destructor; +}; + +assert_vftable_size(IAiPriorityVftable, 1); + } // namespace game #endif // AIPRIORITY_H diff --git a/mss32/include/attackutils.h b/mss32/include/attackutils.h index bc83add1..1e3bd1ec 100644 --- a/mss32/include/attackutils.h +++ b/mss32/include/attackutils.h @@ -24,9 +24,12 @@ namespace game { struct CMidgardID; struct IAttack; struct CAttackImpl; +struct LAttackSource; +struct LAttackClass; enum class AttackClassId : int; enum class AttackReachId : int; +enum class AttackSourceId : int; } // namespace game namespace hooks { @@ -61,6 +64,9 @@ bool isNormalDamageAttack(game::AttackClassId id); /** Attack uses modifiable value of IAttack::getQtyDamage. */ bool isModifiableDamageAttack(game::AttackClassId id); +const game::LAttackSource* getAttackSourceById(game::AttackSourceId id); +const game::LAttackClass* getAttackClassById(game::AttackClassId id); + } // namespace hooks #endif // ATTACKUTILS_H diff --git a/mss32/include/battlemsgdata.h b/mss32/include/battlemsgdata.h index 40cd2102..90b1eb9f 100644 --- a/mss32/include/battlemsgdata.h +++ b/mss32/include/battlemsgdata.h @@ -143,9 +143,9 @@ union UnitFlags { std::uint8_t indexInGroup : 3; bool attacker : 1; - std::uint8_t waited : 1; + bool waited : 1; /** Performed first attack while attacking twice. */ - std::uint8_t attackedOnceOfTwice : 1; + bool attackedOnceOfTwice : 1; bool revived : 1; /** * Indicates that unit waited and then started retreating. @@ -297,7 +297,7 @@ struct BattleMsgData * Original positions are saved and restored when duel ends. * Non-leader units in both groups are marked with BattleStatus::Hidden. */ - bool duel; + std::uint8_t duel; char padding2; char unknown11[4]; }; @@ -307,6 +307,20 @@ assert_offset(BattleMsgData, turnsOrder, 3696); assert_offset(BattleMsgData, attackerStackUnitIds, 3816); assert_offset(BattleMsgData, battleStateFlags2, 3913); +struct PossibleTargets +{ + const CMidgardID* attackTargetGroupId; + const TargetSet* attackTargets; + + const CMidgardID* item1TargetGroupId; + const TargetSet* item1Targets; + + const CMidgardID* item2TargetGroupId; + const TargetSet* item2Targets; +}; + +assert_size(PossibleTargets, 24); + namespace BattleMsgDataApi { struct Api @@ -402,7 +416,7 @@ struct Api const CMidgardID* unitId); CanPerformAttackOnUnitWithStatusCheck canPerformAttackOnUnitWithStatusCheck; - using IsUnitAttackSourceWardRemoved = bool(__thiscall*)(BattleMsgData* thisptr, + using IsUnitAttackSourceWardRemoved = bool(__thiscall*)(const BattleMsgData* thisptr, const CMidgardID* unitId, const LAttackSource* attackSource); IsUnitAttackSourceWardRemoved isUnitAttackSourceWardRemoved; @@ -412,7 +426,7 @@ struct Api const LAttackSource* attackSource); RemoveUnitAttackSourceWard removeUnitAttackSourceWard; - using IsUnitAttackClassWardRemoved = bool(__thiscall*)(BattleMsgData* thisptr, + using IsUnitAttackClassWardRemoved = bool(__thiscall*)(const BattleMsgData* thisptr, const CMidgardID* unitId, const LAttackClass* attackClass); IsUnitAttackClassWardRemoved isUnitAttackClassWardRemoved; @@ -489,6 +503,8 @@ struct Api FindSpecificAttackTarget findBoostAttackTarget; /** Used by AI to determine fear attack target. */ FindSpecificAttackTarget findFearAttackTarget; + /** Used by AI to determine heal attack target. */ + FindSpecificAttackTarget findHealAttackTarget; using FindDoppelgangerAttackTarget = bool(__stdcall*)(const IMidgardObjectMap* objectMap, const CMidgardID* unitId, @@ -652,6 +668,9 @@ struct Api using IsAutoBattle = bool(__thiscall*)(const BattleMsgData* thisptr); IsAutoBattle isAutoBattle; + using IsFastBattle = bool(__thiscall*)(const BattleMsgData* thisptr); + IsFastBattle isFastBattle; + using AlliesNotPreventingAdjacentAttack = bool(__stdcall*)(const BattleMsgData* battleMsgData, const CMidUnitGroup* unitGroup, int unitPosition, @@ -693,6 +712,39 @@ struct Api using BeforeBattleRound = void(__thiscall*)(BattleMsgData* thisptr); BeforeBattleRound beforeBattleRound; + + using AiChooseBattleAction = void(__stdcall*)(const IMidgardObjectMap* objectMap, + BattleMsgData* battleMsgData, + const CMidgardID* unitId, + const Set* possibleActions, + const PossibleTargets* possibleTargets, + BattleAction* battleAction, + CMidgardID* targetUnitId, + CMidgardID* attackerUnitId); + AiChooseBattleAction aiChooseBattleAction; + + /** Returns retreat status for attacker or defender group. */ + using GetRetreatStatus = RetreatStatus(__thiscall*)(const BattleMsgData* thisptr, + bool attacker); + GetRetreatStatus getRetreatStatus; + + /** Sets retreat status for attacker or defender group. */ + using SetRetreatStatus = void(__thiscall*)(BattleMsgData* thisptr, + bool attacker, + RetreatStatus status); + SetRetreatStatus setRetreatStatus; + + /** Returns true if decision about group retreat was made and should not be reconsidered. */ + using IsRetreatDecisionWasMade = bool(__thiscall*)(const BattleMsgData* thisptr); + IsRetreatDecisionWasMade isRetreatDecisionWasMade; + + /** Sets flag to hint that retreat decision was made and should not be reconsidered. */ + using SetRetreatDecisionWasMade = void(__thiscall*)(BattleMsgData* thisptr); + SetRetreatDecisionWasMade setRetreatDecisionWasMade; + + /** Returns true if battle is over but healers can make one more turn. */ + using IsAfterBattle = bool(__thiscall*)(const BattleMsgData* thisptr); + IsAfterBattle isAfterBattle; }; Api& get(); diff --git a/mss32/include/battlemsgdatahooks.h b/mss32/include/battlemsgdatahooks.h index 8067aa7c..77421bf7 100644 --- a/mss32/include/battlemsgdatahooks.h +++ b/mss32/include/battlemsgdatahooks.h @@ -27,6 +27,7 @@ namespace game { struct CMidgardID; struct BattleMsgData; struct IMidgardObjectMap; +struct PossibleTargets; using TargetSet = Set; using GroupIdTargetsPair = Pair; @@ -66,6 +67,15 @@ void __stdcall updateBattleActionsHooked(const game::IMidgardObjectMap* objectMa void __fastcall beforeBattleRoundHooked(game::BattleMsgData* thisptr, int /*%edx*/); +void __stdcall aiChooseBattleActionHooked(const game::IMidgardObjectMap* objectMap, + game::BattleMsgData* battleMsgData, + const game::CMidgardID* unitId, + const game::Set* possibleActions, + const game::PossibleTargets* possibleTargets, + game::BattleAction* battleAction, + game::CMidgardID* targetUnitId, + game::CMidgardID* attackerUnitId); + } // namespace hooks #endif // BATTLEMSGDATAHOOKS_H diff --git a/mss32/include/bindings/battlemsgdataview.h b/mss32/include/bindings/battlemsgdataview.h index 845b762a..cf508883 100644 --- a/mss32/include/bindings/battlemsgdataview.h +++ b/mss32/include/bindings/battlemsgdataview.h @@ -20,7 +20,10 @@ #ifndef BATTLEMSGDATAVIEW_H #define BATTLEMSGDATAVIEW_H +#include "unitview.h" #include +#include +#include namespace sol { class state; @@ -30,6 +33,8 @@ namespace game { struct BattleMsgData; struct IMidgardObjectMap; struct CMidgardID; +enum class BattleAction : int; +enum class RetreatStatus : std::uint8_t; } // namespace game namespace bindings { @@ -39,6 +44,24 @@ class PlayerView; class StackView; class GroupView; +class BattleTurnView +{ +public: + BattleTurnView(const game::CMidgardID& unitId, + char attackCount, + const game::IMidgardObjectMap* objectMap); + + static void bind(sol::state& lua); + + UnitView getUnit() const; + int getAttackCount() const; + +private: + game::CMidgardID unitId; + const game::IMidgardObjectMap* objectMap; + char attackCount; +}; + class BattleMsgDataView { public: @@ -51,6 +74,7 @@ class BattleMsgDataView int getCurrentRound() const; bool getAutoBattle() const; + bool getFastBattle() const; std::optional getAttackerPlayer() const; std::optional getDefenderPlayer() const; @@ -58,6 +82,120 @@ class BattleMsgDataView std::optional getAttacker() const; std::optional getDefender() const; + game::RetreatStatus getRetreatStatus(bool attacker) const; + bool isRetreatDecisionWasMade() const; + + bool isUnitAttacker(const UnitView& unit) const; + bool isUnitAttackerId(const IdView& unitId) const; + bool isAfterBattle() const; + bool isDuel() const; + + using UnitActions = std::tuple, // Possible unit actions + std::optional, // Attack target group + std::vector, // Attack targets + std::optional, // Item 1 target group + std::vector, // Item 1 targets + std::optional, // Item 2 target group + std::vector>; // Item 2 targets + + UnitActions getUnitActions(const UnitView& unit) const; + UnitActions getUnitActionsById(const IdView& unitId) const; + + int getUnitShatteredArmor(const UnitView& unit) const; + int getUnitShatteredArmorById(const IdView& unitId) const; + + int getUnitFortificationArmor(const UnitView& unit) const; + int getUnitFortificationArmorById(const IdView& unitId) const; + + bool isUnitResistantToSource(const UnitView& unit, int sourceId) const; + bool isUnitResistantToSourceById(const IdView& unitId, int sourceId) const; + + bool isUnitResistantToClass(const UnitView& unit, int classId) const; + bool isUnitResistantToClassById(const IdView& unitId, int classId) const; + + std::vector getTurnsOrder() const; + + bool isUnitRevived(const UnitView& unit) const; + bool isUnitRevivedById(const IdView& unitId) const; + + bool isUnitWaiting(const UnitView& unit) const; + bool isUnitWaitingById(const IdView& unitId) const; + + int getUnitDisableRound(const UnitView& unit) const; + int getUnitDisableRoundById(const IdView& unitId) const; + + int getUnitPoisonRound(const UnitView& unit) const; + int getUnitPoisonRoundById(const IdView& unitId) const; + + int getUnitFrostbiteRound(const UnitView& unit) const; + int getUnitFrostbiteRoundById(const IdView& unitId) const; + + int getUnitBlisterRound(const UnitView& unit) const; + int getUnitBlisterRoundById(const IdView& unitId) const; + + int getUnitTransformRound(const UnitView& unit) const; + int getUnitTransformRoundById(const IdView& unitId) const; + +protected: + template + static void bindAccessMethods(T& view) + { + view["getUnitStatus"] = &BattleMsgDataView::getUnitStatus; + view["currentRound"] = sol::property(&BattleMsgDataView::getCurrentRound); + view["autoBattle"] = sol::property(&BattleMsgDataView::getAutoBattle); + view["fastBattle"] = sol::property(&BattleMsgDataView::getFastBattle); + view["attackerPlayer"] = sol::property(&BattleMsgDataView::getAttackerPlayer); + view["defenderPlayer"] = sol::property(&BattleMsgDataView::getDefenderPlayer); + view["attacker"] = sol::property(&BattleMsgDataView::getAttacker); + view["defender"] = sol::property(&BattleMsgDataView::getDefender); + view["isUnitAttacker"] = sol::overload<>(&BattleMsgDataView::isUnitAttacker, + &BattleMsgDataView::isUnitAttackerId); + view["getUnitActions"] = sol::overload<>(&BattleMsgDataView::getUnitActions, + &BattleMsgDataView::getUnitActionsById); + view["getRetreatStatus"] = &BattleMsgDataView::getRetreatStatus; + view["decidedToRetreat"] = sol::property(&BattleMsgDataView::isRetreatDecisionWasMade); + view["afterBattle"] = sol::property(&BattleMsgDataView::isAfterBattle); + view["duel"] = sol::property(&BattleMsgDataView::isDuel); + view["turnsOrder"] = sol::property(&BattleMsgDataView::getTurnsOrder); + view["isUnitRevived"] = sol::overload<>(&BattleMsgDataView::isUnitRevived, + &BattleMsgDataView::isUnitRevivedById); + view["isUnitWaiting"] = sol::overload<>(&BattleMsgDataView::isUnitWaiting, + &BattleMsgDataView::isUnitWaitingById); + + view["getUnitShatteredArmor"] = sol::overload<>( + &BattleMsgDataView::getUnitShatteredArmor, + &BattleMsgDataView::getUnitShatteredArmorById); + + view["getUnitFortificationArmor"] = sol::overload<>( + &BattleMsgDataView::getUnitFortificationArmor, + &BattleMsgDataView::getUnitFortificationArmorById); + + view["isUnitResistantToSource"] = sol::overload<>( + &BattleMsgDataView::isUnitResistantToSource, + &BattleMsgDataView::isUnitResistantToSourceById); + + view["isUnitResistantToClass"] = sol::overload<>( + &BattleMsgDataView::isUnitResistantToClass, + &BattleMsgDataView::isUnitResistantToClassById); + + view["getUnitDisableRound"] = sol::overload<>(&BattleMsgDataView::getUnitDisableRound, + &BattleMsgDataView::getUnitDisableRoundById); + + view["getUnitPoisonRound"] = sol::overload<>(&BattleMsgDataView::getUnitPoisonRound, + &BattleMsgDataView::getUnitPoisonRoundById); + + view["getUnitFrostbiteRound"] = sol::overload<>( + &BattleMsgDataView::getUnitFrostbiteRound, + &BattleMsgDataView::getUnitFrostbiteRoundById); + + view["getUnitBlisterRound"] = sol::overload<>(&BattleMsgDataView::getUnitBlisterRound, + &BattleMsgDataView::getUnitBlisterRoundById); + + view["getUnitTransformRound"] = sol::overload<>( + &BattleMsgDataView::getUnitTransformRound, + &BattleMsgDataView::getUnitTransformRoundById); + } + private: std::optional getPlayer(const game::CMidgardID& playerId) const; diff --git a/mss32/include/bindings/battlemsgdataviewmutable.h b/mss32/include/bindings/battlemsgdataviewmutable.h new file mode 100644 index 00000000..906c4ae4 --- /dev/null +++ b/mss32/include/bindings/battlemsgdataviewmutable.h @@ -0,0 +1,45 @@ +/* + * This file is part of the modding toolset for Disciples 2. + * (https://github.com/VladimirMakeev/D2ModdingToolset) + * Copyright (C) 2023 Vladimir Makeev. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef BATTLEMSGDATAVIEWMUTABLE_H +#define BATTLEMSGDATAVIEWMUTABLE_H + +#include "battlemsgdataview.h" + +namespace bindings { + +class BattleMsgDataViewMutable : public BattleMsgDataView +{ +public: + BattleMsgDataViewMutable(game::BattleMsgData* battleMsgData, + const game::IMidgardObjectMap* objectMap); + + static void bind(sol::state& lua); + + void setRetreatStatus(bool attacker, std::uint8_t value); + void setDecidedToRetreat(); + +private: + game::BattleMsgData* battleMsgData; + const game::IMidgardObjectMap* objectMap; +}; + +} // namespace bindings + +#endif // BATTLEMSGDATAVIEWMUTABLE_H diff --git a/mss32/include/bindings/buildingview.h b/mss32/include/bindings/buildingview.h new file mode 100644 index 00000000..ebe5301f --- /dev/null +++ b/mss32/include/bindings/buildingview.h @@ -0,0 +1,57 @@ +/* + * This file is part of the modding toolset for Disciples 2. + * (https://github.com/VladimirMakeev/D2ModdingToolset) + * Copyright (C) 2024 Vladimir Makeev. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef BUILDINGVIEW_H +#define BUILDINGVIEW_H + +#include "currencyview.h" +#include "idview.h" +#include + +namespace sol { +class state; +} + +namespace game { +struct TBuildingType; +} + +namespace bindings { + +class BuildingView +{ +public: + BuildingView(const game::TBuildingType* building); + + static void bind(sol::state& lua); + + IdView getId() const; + CurrencyView getCost() const; + int getCategory() const; + std::optional getRequiredBuilding() const; + int getUnitBranch() const; + int getLevel() const; + +private: + const game::TBuildingType* building; +}; + +} // namespace bindings + +#endif // BUILDINGVIEW_H diff --git a/mss32/include/bindings/crystalview.h b/mss32/include/bindings/crystalview.h new file mode 100644 index 00000000..93b60d1d --- /dev/null +++ b/mss32/include/bindings/crystalview.h @@ -0,0 +1,53 @@ +/* + * This file is part of the modding toolset for Disciples 2. + * (https://github.com/VladimirMakeev/D2ModdingToolset) + * Copyright (C) 2024 Vladimir Makeev. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef CRYSTALVIEW_H +#define CRYSTALVIEW_H + +#include "idview.h" +#include "point.h" + +namespace sol { +class state; +} + +namespace game { +struct CMidCrystal; +} + +namespace bindings { + +class CrystalView +{ +public: + CrystalView(const game::CMidCrystal* crystal); + + static void bind(sol::state& lua); + + IdView getId() const; + int getResourceType() const; + Point getPosition() const; + +private: + const game::CMidCrystal* crystal; +}; + +} // namespace bindings + +#endif // CRYSTALVIEW_H diff --git a/mss32/include/bindings/fortview.h b/mss32/include/bindings/fortview.h index 0f39e947..67a34ecc 100644 --- a/mss32/include/bindings/fortview.h +++ b/mss32/include/bindings/fortview.h @@ -50,6 +50,7 @@ class FortView IdView getId() const; Point getPosition() const; + Point getEntrance() const; std::optional getOwner() const; GroupView getGroup() const; std::optional getVisitor() const; diff --git a/mss32/include/bindings/gameview.h b/mss32/include/bindings/gameview.h new file mode 100644 index 00000000..653e0eb7 --- /dev/null +++ b/mss32/include/bindings/gameview.h @@ -0,0 +1,43 @@ +/* + * This file is part of the modding toolset for Disciples 2. + * (https://github.com/VladimirMakeev/D2ModdingToolset) + * Copyright (C) 2023 Vladimir Makeev. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef GAMEVIEW_H +#define GAMEVIEW_H + +namespace sol { +class state; +} + +namespace bindings { + +class GameView +{ +public: + static void bind(sol::state& lua); + + int getUnitMaxDamage() const; + int getUnitMinDamage() const; + int getLeaderAdditionalDamage() const; + int getUnitMaxArmor() const; + bool isEditor() const; +}; + +} // namespace bindings + +#endif // GAMEVIEW_H diff --git a/mss32/include/bindings/globalvariablesview.h b/mss32/include/bindings/globalvariablesview.h new file mode 100644 index 00000000..d331dd90 --- /dev/null +++ b/mss32/include/bindings/globalvariablesview.h @@ -0,0 +1,133 @@ +/* + * This file is part of the modding toolset for Disciples 2. + * (https://github.com/VladimirMakeev/D2ModdingToolset) + * Copyright (C) 2024 Vladimir Makeev. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef GLOBALVARIABLESVIEW_H +#define GLOBALVARIABLESVIEW_H + +#include "currencyview.h" + +namespace sol { +class state; +} + +namespace game { +struct GlobalVariables; +} + +namespace bindings { + +class GlobalVariablesView +{ +public: + GlobalVariablesView(const game::GlobalVariables* variables); + + static void bind(sol::state& lua); + + int getMorale(int tier) const; + int getWeapnMstr() const; + + int getBatInit() const; + int getBatDamage() const; + int getBatRound() const; + int getBatBreak() const; + int getBatBModif() const; + + int getBatBoostd(int level) const; + int getBatLowerd(int level) const; + int getBatLoweri() const; + + int getLdrMaxAbil() const; + int getSpyDiscov() const; + + int getPoisonS() const; + int getPoisonC() const; + + int getBribe() const; + + int getStealRace() const; + int getStealNeut() const; + + int getRiotMin() const; + int getRiotMax() const; + int getRiotDmg() const; + + int getSellRatio() const; + + int getTCapture() const; + int getTCapital() const; + int getTCity(int tier) const; + + CurrencyView getRodCost() const; + int getRodRange() const; + + int getCrystalP() const; + + int getConstUrg() const; + + int getRegenLWar() const; + int getRegenRuin() const; + + int getDPeace() const; + int getDWar() const; + int getDNeutral() const; + int getDGold() const; + int getDMkAlly() const; + int getDAttackSc() const; + int getDAttackFo() const; + int getDAttackPc() const; + int getDRod() const; + int getDRefAlly() const; + int getDBkAlly() const; + int getDNoble() const; + int getDBkaChnc() const; + int getDBkaTurn() const; + + int getProt(int tier) const; + int getProtCap() const; + + int getBonusE() const; + int getBonusA() const; + int getBonusH() const; + int getBonusV() const; + + int getIncomeE() const; + int getIncomeA() const; + int getIncomeH() const; + int getIncomeV() const; + + int getGuRange() const; + int getPaRange() const; + int getLoRange() const; + + int getDfendBonus() const; + int getTalisChrg() const; + + int getSplpwr(int level) const; + + int getGainSpell() const; + + int getDefendBonus() const; + +private: + const game::GlobalVariables* variables; +}; + +} // namespace bindings + +#endif // GLOBALVARIABLESVIEW_H diff --git a/mss32/include/bindings/globalview.h b/mss32/include/bindings/globalview.h new file mode 100644 index 00000000..ead91a1f --- /dev/null +++ b/mss32/include/bindings/globalview.h @@ -0,0 +1,41 @@ +/* + * This file is part of the modding toolset for Disciples 2. + * (https://github.com/VladimirMakeev/D2ModdingToolset) + * Copyright (C) 2024 Vladimir Makeev. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef GLOBALVIEW_H +#define GLOBALVIEW_H + +#include "globalvariablesview.h" + +namespace sol { +class state; +} + +namespace bindings { + +class GlobalView +{ +public: + static void bind(sol::state& lua); + + GlobalVariablesView getGlobalVariables() const; +}; + +} // namespace bindings + +#endif // GLOBALVIEW_H diff --git a/mss32/include/bindings/groupview.h b/mss32/include/bindings/groupview.h index e8d25f2f..47635643 100644 --- a/mss32/include/bindings/groupview.h +++ b/mss32/include/bindings/groupview.h @@ -58,6 +58,11 @@ class GroupView bool hasUnit(const bindings::UnitView& unit) const; bool hasUnitById(const bindings::IdView& unitId) const; + int getUnitPosition(const bindings::UnitView& unit) const; + int getUnitPositionById(const bindings::IdView& unitId) const; + + int getSubrace() const; + protected: const game::CMidUnitGroup* group; const game::IMidgardObjectMap* objectMap; diff --git a/mss32/include/bindings/idview.h b/mss32/include/bindings/idview.h index 61b3c2fc..9414a9fe 100644 --- a/mss32/include/bindings/idview.h +++ b/mss32/include/bindings/idview.h @@ -56,9 +56,11 @@ struct IdView // For Lua hash table int getValue() const; + int getType() const; int getTypeIndex() const; static IdView getEmptyId(); + static IdView getSummonId(int position); game::CMidgardID id; }; diff --git a/mss32/include/bindings/locationview.h b/mss32/include/bindings/locationview.h index e2c68654..b18de57f 100644 --- a/mss32/include/bindings/locationview.h +++ b/mss32/include/bindings/locationview.h @@ -22,6 +22,7 @@ #include "idview.h" #include "point.h" +#include namespace sol { class state; @@ -43,6 +44,7 @@ class LocationView IdView getId() const; Point getPosition() const; int getRadius() const; + std::string getName() const; private: const game::CMidLocation* location; diff --git a/mss32/include/bindings/merchantview.h b/mss32/include/bindings/merchantview.h new file mode 100644 index 00000000..ac31cddf --- /dev/null +++ b/mss32/include/bindings/merchantview.h @@ -0,0 +1,60 @@ +/* + * This file is part of the modding toolset for Disciples 2. + * (https://github.com/VladimirMakeev/D2ModdingToolset) + * Copyright (C) 2024 Vladimir Makeev. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef MERCHANTVIEW_H +#define MERCHANTVIEW_H + +#include "itembaseview.h" +#include "siteview.h" + +namespace game { +struct CMidSiteMerchant; +} + +namespace bindings { + +class MerchantItemView +{ +public: + MerchantItemView(const game::CMidgardID& globalItemId, int amount); + + static void bind(sol::state& lua); + + ItemBaseView getItemBase() const; + int getAmount(); + +private: + game::CMidgardID globalItemId; + int amount; +}; + +class MerchantView : public SiteView +{ +public: + MerchantView(const game::CMidSiteMerchant* merchant, const game::IMidgardObjectMap* objectMap); + + static void bind(sol::state& lua); + + std::vector getItems() const; + bool isMission() const; +}; + +} // namespace bindings + +#endif // MERCHANTVIEW_H diff --git a/mss32/include/bindings/mercsview.h b/mss32/include/bindings/mercsview.h new file mode 100644 index 00000000..510d0eca --- /dev/null +++ b/mss32/include/bindings/mercsview.h @@ -0,0 +1,59 @@ +/* + * This file is part of the modding toolset for Disciples 2. + * (https://github.com/VladimirMakeev/D2ModdingToolset) + * Copyright (C) 2024 Vladimir Makeev. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef MERCSVIEW_H +#define MERCSVIEW_H + +#include "siteview.h" +#include "unitimplview.h" + +namespace game { +struct CMidSiteMercs; +} + +namespace bindings { + +class MercenaryUnitView +{ +public: + MercenaryUnitView(const game::CMidgardID& unitImplId, bool unique); + + static void bind(sol::state& lua); + + UnitImplView getImpl() const; + bool isUnique() const; + +private: + game::CMidgardID unitImplId; + bool unique; +}; + +class MercsView : public SiteView +{ +public: + MercsView(const game::CMidSiteMercs* mercs, const game::IMidgardObjectMap* objectMap); + + static void bind(sol::state& lua); + + std::vector getUnits() const; +}; + +} // namespace bindings + +#endif // MERCSVIEW_H diff --git a/mss32/include/bindings/playerview.h b/mss32/include/bindings/playerview.h index d83e07e7..a8083498 100644 --- a/mss32/include/bindings/playerview.h +++ b/mss32/include/bindings/playerview.h @@ -20,6 +20,7 @@ #ifndef PLAYERVIEW_H #define PLAYERVIEW_H +#include "buildingview.h" #include "currencyview.h" #include "idview.h" #include @@ -53,6 +54,10 @@ class PlayerView std::optional getFog() const; + std::vector getBuildings() const; + bool hasBuilding(const std::string& id) const; + bool hasBuildingById(const IdView& id) const; + private: const game::CMidPlayer* player; const game::IMidgardObjectMap* objectMap; diff --git a/mss32/include/bindings/resourcemarketview.h b/mss32/include/bindings/resourcemarketview.h new file mode 100644 index 00000000..6e280601 --- /dev/null +++ b/mss32/include/bindings/resourcemarketview.h @@ -0,0 +1,49 @@ +/* + * This file is part of the modding toolset for Disciples 2. + * (https://github.com/VladimirMakeev/D2ModdingToolset) + * Copyright (C) 2024 Vladimir Makeev. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef RESOURCEMARKETVIEW_H +#define RESOURCEMARKETVIEW_H + +#include "currencyview.h" +#include "siteview.h" + +namespace hooks { +struct CMidSiteResourceMarket; +} + +namespace bindings { + +class ResourceMarketView : public SiteView +{ +public: + ResourceMarketView(const hooks::CMidSiteResourceMarket* market, + const game::IMidgardObjectMap* objectMap); + + static void bind(sol::state& lua); + + bool isResourceInfinite(game::CurrencyType type) const; + CurrencyView getStock() const; + bool hasCustomExchangeRates() const; + +private: + const hooks::CMidSiteResourceMarket* getMarket() const; +}; +} // namespace bindings + +#endif // RESOURCEMARKETVIEW_H diff --git a/mss32/include/bindings/scenarioview.h b/mss32/include/bindings/scenarioview.h index 419dda66..ba73d326 100644 --- a/mss32/include/bindings/scenarioview.h +++ b/mss32/include/bindings/scenarioview.h @@ -20,6 +20,7 @@ #ifndef SCENARIOVIEW_H #define SCENARIOVIEW_H +#include #include #include @@ -48,6 +49,12 @@ class UnitView; class PlayerView; class RodView; class DiplomacyView; +class ItemView; +class CrystalView; +class MerchantView; +class MercsView; +class TrainerView; +class ResourceMarketView; /** * Returns stub values if objectMap is null. @@ -119,6 +126,56 @@ class ScenarioView /** Searches for unit by id. */ std::optional getUnitById(const IdView& id) const; + /** Searches for item by id string. */ + std::optional getItem(const std::string& id) const; + /** Searches for item by id. */ + std::optional getItemById(const IdView& id) const; + + /** Searches for crystal by id string. */ + std::optional getCrystal(const std::string& id) const; + /** Searches for crystal by id. */ + std::optional getCrystalById(const IdView& id) const; + /** Searches for crystal by coordinate pair. */ + std::optional getCrystalByCoordinates(int x, int y) const; + /** Searches for crystal at specified point. */ + std::optional getCrystalByPoint(const Point& p) const; + + /** Searches for merchant by id string. */ + std::optional getMerchant(const std::string& id) const; + /** Searches for merchant by id. */ + std::optional getMerchantById(const IdView& id) const; + /** Searches for merchant by coordinate pair. */ + std::optional getMerchantByCoordinates(int x, int y) const; + /** Searches for merchant at specified point. */ + std::optional getMerchantByPoint(const Point& p) const; + + /** Searches for mercenaries by id string. */ + std::optional getMercs(const std::string& id) const; + /** Searches for mercenaries by id. */ + std::optional getMercsById(const IdView& id) const; + /** Searches for mercenaries by coordinate pair. */ + std::optional getMercsByCoordinates(int x, int y) const; + /** Searches for mercenaries at specified point. */ + std::optional getMercsByPoint(const Point& p) const; + + /** Searches for trainer by id string. */ + std::optional getTrainer(const std::string& id) const; + /** Searches for trainer by id. */ + std::optional getTrainerById(const IdView& id) const; + /** Searches for trainer by coordinate pair. */ + std::optional getTrainerByCoordinates(int x, int y) const; + /** Searches for trainer at specified point. */ + std::optional getTrainerByPoint(const Point& p) const; + + /** Searches for market by id string. */ + std::optional getMarket(const std::string& id) const; + /** Searches for market by id. */ + std::optional getMarketById(const IdView& id) const; + /** Searches for market by coordinate pair. */ + std::optional getMarketByCoordinates(int x, int y) const; + /** Searches for market at specified point. */ + std::optional getMarketByPoint(const Point& p) const; + /** Searches for stack that has specified unit among all the stacks in the whole scenario. */ std::optional findStackByUnit(const UnitView& unit) const; std::optional findStackByUnitId(const IdView& unitId) const; @@ -137,11 +194,29 @@ class ScenarioView std::optional findRuinByUnitId(const IdView& unitId) const; std::optional findRuinByUnitIdString(const std::string& unitId) const; + std::string getName() const; + std::string getDescription() const; + std::string getAuthor() const; + std::uint32_t getSeed() const; int getCurrentDay() const; int getSize() const; + int getDifficulty() const; std::optional getDiplomacy() const; + void forEachStack(const std::function& callback) const; + void forEachLocation(const std::function& callback) const; + void forEachFort(const std::function& callback) const; + void forEachRuin(const std::function& callback) const; + void forEachRod(const std::function& callback) const; + void forEachPlayer(const std::function& callback) const; + void forEachUnit(const std::function& callback) const; + void forEachCrystal(const std::function& callback) const; + void forEachMerchant(const std::function& callback) const; + void forEachMercenary(const std::function& callback) const; + void forEachTrainer(const std::function& callback) const; + void forEachMarket(const std::function& callback) const; + private: const game::CMidgardID* getObjectId(int x, int y, game::IdType type) const; diff --git a/mss32/include/bindings/siteview.h b/mss32/include/bindings/siteview.h new file mode 100644 index 00000000..e7f92939 --- /dev/null +++ b/mss32/include/bindings/siteview.h @@ -0,0 +1,65 @@ +/* + * This file is part of the modding toolset for Disciples 2. + * (https://github.com/VladimirMakeev/D2ModdingToolset) + * Copyright (C) 2024 Vladimir Makeev. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef SITEVIEW_H +#define SITEVIEW_H + +#include "idview.h" +#include "playerview.h" +#include "point.h" +#include + +namespace sol { +class state; +} + +namespace game { +struct CMidSite; +struct IMidgardObjectMap; +} // namespace game + +namespace bindings { + +class SiteView +{ +public: + SiteView(const game::CMidSite* site, const game::IMidgardObjectMap* objectMap); + + static void bind(sol::state& lua); + + IdView getId() const; + Point getPosition() const; + std::vector getVisitors() const; + +protected: + template + static void bindAccessMethods(T& view) + { + view["id"] = sol::property(&SiteView::getId); + view["position"] = sol::property(&SiteView::getPosition); + view["visitors"] = sol::property(&SiteView::getVisitors); + } + + const game::CMidSite* site; + const game::IMidgardObjectMap* objectMap; +}; + +} // namespace bindings + +#endif // SITEVIEW_H diff --git a/mss32/include/bindings/stackview.h b/mss32/include/bindings/stackview.h index 4f208add..597447d4 100644 --- a/mss32/include/bindings/stackview.h +++ b/mss32/include/bindings/stackview.h @@ -69,6 +69,10 @@ class StackView std::vector getInventoryItems() const; std::optional getLeaderEquippedItem(const game::EquippedItemIdx& idx) const; + int getOrder() const; + IdView getOrderTargetId() const; + int getAiOrder() const; + private: const game::CMidStack* stack; const game::IMidgardObjectMap* objectMap; diff --git a/mss32/include/bindings/trainerview.h b/mss32/include/bindings/trainerview.h new file mode 100644 index 00000000..bd942bab --- /dev/null +++ b/mss32/include/bindings/trainerview.h @@ -0,0 +1,41 @@ +/* + * This file is part of the modding toolset for Disciples 2. + * (https://github.com/VladimirMakeev/D2ModdingToolset) + * Copyright (C) 2024 Vladimir Makeev. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef TRAINERVIEW_H +#define TRAINERVIEW_H + +#include "siteview.h" + +namespace game { +struct CMidSiteTrainer; +} + +namespace bindings { + +class TrainerView : public SiteView +{ +public: + TrainerView(const game::CMidSiteTrainer* trainer, const game::IMidgardObjectMap* objectMap); + + static void bind(sol::state& lua); +}; + +} // namespace bindings + +#endif // TRAINERVIEW_H diff --git a/mss32/include/bindings/unitimplview.h b/mss32/include/bindings/unitimplview.h index 55773e8c..1b019e99 100644 --- a/mss32/include/bindings/unitimplview.h +++ b/mss32/include/bindings/unitimplview.h @@ -99,8 +99,10 @@ class UnitImplView std::optional getAttack() const; /** Returns secondary attack. */ std::optional getAttack2() const; - /** Returns alternative attack. */ + /** Returns alternative attack for primary attack. */ std::optional getAltAttack() const; + /** Returns alternative attack for secondary attack. */ + std::optional getAltAttack2() const; /** Returns immune category id for specified attack class id. */ int getImmuneToAttackClass(int attackClassId) const; diff --git a/mss32/include/categories.h b/mss32/include/categories.h index b6c6f3de..85ba7236 100644 --- a/mss32/include/categories.h +++ b/mss32/include/categories.h @@ -52,12 +52,6 @@ struct Category T id; }; -struct LAttitudesCategoryTable : CEnumConstantTable -{ }; - -struct LAttitudesCategory : public Category -{ }; - static const int emptyCategoryId = -1; namespace CategoryTableApi { @@ -99,7 +93,7 @@ struct Api /** Looks for category with the specified id, otherwise initializes the value with null table * and id = -1. */ - using FindCategoryById = Category*(__thiscall*)(Table* thisptr, + using FindCategoryById = Category*(__thiscall*)(const Table* thisptr, Category* value, const int* categoryId); FindCategoryById findCategoryById; diff --git a/mss32/include/categoryids.h b/mss32/include/categoryids.h index 5e419937..ab466746 100644 --- a/mss32/include/categoryids.h +++ b/mss32/include/categoryids.h @@ -404,6 +404,36 @@ enum class AiSpellId : int Dwarf, }; +/** Resource ids from Lres.dbf. */ +enum class ResourceId : int +{ + Gold, + InfernalMana, /**< L_RED */ + LifeMana, /**< L_YELLOW */ + DeathMana, /**< L_ORANGE */ + RunicMana, /**< L_WHITE */ + GroveMana, /**< L_BLUE */ +}; + +/** Noble action ids from LAction.dbf. */ +enum class ActionId : int +{ + PoisonStack, + Spy, + StealItem, + Assassinate, + Misfit, + Duel, + PoisonCity, + StealSpell, + Bribe, + StealGold, + StealMerchant, + StealMage, + SpyRuin, + RiotCity, +}; + } // namespace game #endif // CATEGORYIDS_H diff --git a/mss32/include/cmdstackvisitmsg.h b/mss32/include/cmdstackvisitmsg.h new file mode 100644 index 00000000..a83fc4c2 --- /dev/null +++ b/mss32/include/cmdstackvisitmsg.h @@ -0,0 +1,37 @@ +/* + * This file is part of the modding toolset for Disciples 2. + * (https://github.com/VladimirMakeev/D2ModdingToolset) + * Copyright (C) 2024 Vladimir Makeev. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef CMDSTACKVISITMSG_H +#define CMDSTACKVISITMSG_H + +#include "commandmsg.h" + +namespace game { + +struct CCmdStackVisitMsg : public CCommandMsg +{ + CMidgardID visitorStackId; + CMidgardID siteId; +}; + +assert_size(CCmdStackVisitMsg, 24); + +} + +#endif // CMDSTACKVISITMSG_H diff --git a/mss32/include/currency.h b/mss32/include/currency.h index 18e718d3..ad4fe119 100644 --- a/mss32/include/currency.h +++ b/mss32/include/currency.h @@ -165,6 +165,10 @@ struct Api std::int16_t value); SetCurrency set; + /** Returns specified bank currency. */ + using GetCurrency = std::int16_t(__thiscall*)(const Bank* bank, CurrencyType currencyType); + GetCurrency get; + /** Returns true if all currencies in bank are zero. */ using IsZero = bool(__thiscall*)(const Bank* bank); IsZero isZero; diff --git a/mss32/include/customaibattle.h b/mss32/include/customaibattle.h new file mode 100644 index 00000000..10170f95 --- /dev/null +++ b/mss32/include/customaibattle.h @@ -0,0 +1,44 @@ +/* + * This file is part of the modding toolset for Disciples 2. + * (https://github.com/VladimirMakeev/D2ModdingToolset) + * Copyright (C) 2024 Vladimir Makeev. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef CUSTOMAIBATTLE_H +#define CUSTOMAIBATTLE_H + +#include "categoryids.h" +#include +#include + +namespace hooks { + +using AttitudeBattleLogicMap = std::unordered_map; + +struct CustomAiBattleLogic +{ + AttitudeBattleLogicMap attitudeBattleLogic; + bool customBattleLogicEnabled; +}; + +const CustomAiBattleLogic& getCustomAiBattleLogic(); + +void initializeCustomAiBattleLogic(); + +} // namespace hooks + +#endif // CUSTOMAIBATTLE_H diff --git a/mss32/include/customnobleactioncategories.h b/mss32/include/customnobleactioncategories.h new file mode 100644 index 00000000..f4872d12 --- /dev/null +++ b/mss32/include/customnobleactioncategories.h @@ -0,0 +1,45 @@ +/* + * This file is part of the modding toolset for Disciples 2. + * (https://github.com/VladimirMakeev/D2ModdingToolset) + * Copyright (C) 2024 Vladimir Makeev. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef CUSTOMNOBLEACTIONCATEGORIES_H +#define CUSTOMNOBLEACTIONCATEGORIES_H + +#include "nobleactioncat.h" +#include + +namespace hooks { + +using CustomNobleActionCat = std::pair; + +struct CustomNobleActionCategories +{ + CustomNobleActionCat stealMarket; +}; + +CustomNobleActionCategories& getCustomNobleActionCategories(); + +game::LNobleActionCatTable* __fastcall nobleActionCatTableCtorHooked( + game::LNobleActionCatTable* thisptr, + int /*%edx*/, + const char* globalsFolderPath, + void* codeBaseEnvProxy); + +} // namespace hooks + +#endif // CUSTOMNOBLEACTIONCATEGORIES_H diff --git a/mss32/include/customnobleactionhooks.h b/mss32/include/customnobleactionhooks.h new file mode 100644 index 00000000..7737deec --- /dev/null +++ b/mss32/include/customnobleactionhooks.h @@ -0,0 +1,63 @@ +/* + * This file is part of the modding toolset for Disciples 2. + * (https://github.com/VladimirMakeev/D2ModdingToolset) + * Copyright (C) 2024 Vladimir Makeev. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef CUSTOMNOBLEACTIONHOOKS_H +#define CUSTOMNOBLEACTIONHOOKS_H + +#include "d2set.h" +#include "nobleactioncat.h" + +namespace game { +struct INobleActionResult; +struct IMidgardObjectMap; +struct CMidgardID; +struct String; +struct CCmdNobleResultMsg; +struct CPhaseGame; +struct CMidPlayer; +} // namespace game + +namespace hooks { + +game::INobleActionResult* __stdcall createNobleActionResultHooked( + game::IMidgardObjectMap* objectMap, + const game::LNobleActionCat* actionCategory, + const game::CMidgardID* targetObjectId, + const game::CMidgardID* id); + +bool __stdcall getSiteNobleActionsHooked(const game::IMidgardObjectMap* objectMap, + const game::CMidgardID* playerId, + const game::CMidgardID* objectId, + game::Set* nobleActions); + +bool __stdcall getPossibleNobleActionsHooked(const game::IMidgardObjectMap* objectMap, + const game::CMidgardID* playerId, + const game::CMidgardID* objectId, + game::Set* nobleActions); + +game::String* __stdcall getNobleActionResultDescriptionHooked( + game::String* description, + const game::LNobleActionCat nobleActionCat, + const game::CCmdNobleResultMsg* nobleResultMsg, + const game::CPhaseGame* phaseGame, + const game::CMidPlayer* player); + +} // namespace hooks + +#endif // CUSTOMNOBLEACTIONHOOKS_H diff --git a/mss32/include/ddfoldedinv.h b/mss32/include/ddfoldedinv.h new file mode 100644 index 00000000..eaf36869 --- /dev/null +++ b/mss32/include/ddfoldedinv.h @@ -0,0 +1,65 @@ +/* + * This file is part of the modding toolset for Disciples 2. + * (https://github.com/VladimirMakeev/D2ModdingToolset) + * Copyright (C) 2024 Vladimir Makeev. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef DDFOLDEDINV_H +#define DDFOLDEDINV_H + +#include "middropsource.h" +#include "middroptarget.h" +#include "midgardobjectmap.h" + +namespace game { + +struct CListBoxInterf; +struct IMidDropManager; +struct CInventoryCounter; +struct CursorHandle; + +struct CDDFoldedInvData +{ + CListBoxInterf* listBox; + IMidDropManager* midDropManager; + CMidgardID unknownId; + CInventoryCounter* inventoryCounter; + CMidgardID objectId; + char unknown4[4]; + IMidgardObjectMap* objectMap; + char unknown5[28]; + IMqImage2* dragDropValidImage; + IMqImage2* dragDropInvalidImage; + SmartPtr defaultCursor; + SmartPtr noDragDropCursor; + char unknown6[4]; +}; + +assert_size(CDDFoldedInvData, 84); + +// TODO: has bigger vftable than IMidDropSource: 27 methods instead of 10 +struct CDDFoldedInv + : public IMidDropSource + , public IMidDropTarget +{ + CDDFoldedInvData* foldedInvData; +}; + +assert_size(CDDFoldedInv, 12); + +} // namespace game + +#endif // DDFOLDEDINV_H diff --git a/mss32/include/ddfoldedinvdisplay.h b/mss32/include/ddfoldedinvdisplay.h new file mode 100644 index 00000000..934e4d30 --- /dev/null +++ b/mss32/include/ddfoldedinvdisplay.h @@ -0,0 +1,56 @@ +/* + * This file is part of the modding toolset for Disciples 2. + * (https://github.com/VladimirMakeev/D2ModdingToolset) + * Copyright (C) 2024 Vladimir Makeev. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef DDFOLDEDINVDISPLAY_H +#define DDFOLDEDINVDISPLAY_H + +#include "ddfoldedinv.h" +#include "miditemext.h" + +namespace game { + +struct CMidDragDropInterf; + +struct CDDFoldedInvDisplayData +{ + IMidDropManager* dropManager; + bool unknown; + char padding[3]; + int unknown2; + int unknown3; + CMidDragDropInterf* dragDropInterface; + int elementCount; + int unknown6; + int unknown7; +}; + +assert_size(CDDFoldedInvDisplayData, 32); + +struct CDDFoldedInvDisplay + : public CDDFoldedInv + , public IMidItemExt +{ + CDDFoldedInvDisplayData* foldedInvDisplayData; +}; + +assert_size(CDDFoldedInvDisplay, 20); + +} // namespace game + +#endif // DDFOLDEDINVDISPLAY_H diff --git a/mss32/include/ddstackgroup.h b/mss32/include/ddstackgroup.h index 96d589dd..3f1d6d15 100644 --- a/mss32/include/ddstackgroup.h +++ b/mss32/include/ddstackgroup.h @@ -53,6 +53,28 @@ struct CDDStackGroup : public CDDUnitGroup assert_size(CDDStackGroup, 20); +namespace CDDStackGroupApi { + +struct Api +{ + using Constructor = CDDStackGroup*(__thiscall*)(CDDStackGroup* thisptr, + CMidUnitGroupAdapter* unitGroupAdapter, + const CMidgardID* groupId, + IMidgardObjectMap* objectMap, + IMidDropManager* dropManager, + int leftSide, + const CMqRect* rSlotArea, + ITask* taskOpenInterf, + CPhaseGame* phaseGame, + CMidDragDropInterf* dragDropInterf, + const char* addUnitDialogName); + Constructor constructor; +}; + +Api& get(); + +} // namespace CDDStackGroupApi + } // namespace game #endif // DDSTACKGROUP_H diff --git a/mss32/include/ddstackinventorydisplay.h b/mss32/include/ddstackinventorydisplay.h new file mode 100644 index 00000000..19348f6e --- /dev/null +++ b/mss32/include/ddstackinventorydisplay.h @@ -0,0 +1,53 @@ +/* + * This file is part of the modding toolset for Disciples 2. + * (https://github.com/VladimirMakeev/D2ModdingToolset) + * Copyright (C) 2024 Vladimir Makeev. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef DDSTACKINVENTORYDISPLAY_H +#define DDSTACKINVENTORYDISPLAY_H + +#include "ddfoldedinvdisplay.h" + +namespace game { + +struct CDDStackInventoryDisplay : public CDDFoldedInvDisplay +{ }; + +assert_size(CDDStackInventoryDisplay, 20); + +namespace CDDStackInventoryDisplayApi { + +struct Api +{ + using Constructor = CDDStackInventoryDisplay*(__thiscall*)(CDDStackInventoryDisplay* thisptr, + CMidDragDropInterf* dragDrop, + CListBoxInterf* listBox, + const CMidgardID* objectId, + IMidgardObjectMap* objectMap); + Constructor constructor; + + using SetElementCount = void(__thiscall*)(CDDStackInventoryDisplay* thisptr, int elementCount); + SetElementCount setElementCount; +}; + +Api& get(); + +} // namespace CDDStackInventoryDisplayApi + +} // namespace game + +#endif // DDSTACKINVENTORYDISPLAY_H diff --git a/mss32/include/displayhandlers.h b/mss32/include/displayhandlers.h index 1ec1a99e..34958d62 100644 --- a/mss32/include/displayhandlers.h +++ b/mss32/include/displayhandlers.h @@ -20,14 +20,19 @@ #ifndef DISPLAYHANDLERS_H #define DISPLAYHANDLERS_H +#include "d2map.h" #include "idset.h" #include "imagelayerlist.h" #include "midgardid.h" +#include "smartptr.h" namespace game { struct IMidgardObjectMap; struct CMidVillage; +struct IMapElement; +struct CMidSite; +struct alignas(8) TypeDescriptor; namespace DisplayHandlersApi { @@ -43,11 +48,70 @@ struct Api bool animatedIso); DisplayHandler villageHandler; + + DisplayHandler siteHandler; }; Api& get(); } // namespace DisplayHandlersApi + +struct ImageDisplayHandlerVftable; + +struct ImageDisplayHandler +{ + ImageDisplayHandlerVftable* vftable; + DisplayHandlersApi::Api::DisplayHandler handler; +}; + +assert_size(ImageDisplayHandler, 8); + +struct ImageDisplayHandlerVftable +{ + using Destructor = void(__thiscall*)(ImageDisplayHandler* thisptr, char flags); + Destructor destructor; + + using RunHandler = void(__thiscall*)(ImageDisplayHandler* thisptr, + ImageLayerList* list, + const IMapElement* mapElement, + const IMidgardObjectMap* objectMap, + const CMidgardID* playerId, + const IdSet* objectives, + int a6, + bool animatedIso); + RunHandler runHandler; +}; + +assert_vftable_size(ImageDisplayHandlerVftable, 2); + +using ImageDisplayHandlerPtr = SmartPtr; +using ImageDisplayHandlerMap = Map; +using ImageDisplayHandlerMapIt = MapIterator; +using ImageDisplayHandlerMapInsertIt = Pair; + +using ImageDisplayHandlerMapFind = + ImageDisplayHandlerMapIt*(__thiscall*)(ImageDisplayHandlerMap* thisptr, + ImageDisplayHandlerMapIt* iterator, + const TypeDescriptor** descriptor); + +namespace ImageDisplayHandlerApi { + +struct Api +{ + using AddHandler = + ImageDisplayHandlerMapInsertIt*(__thiscall*)(ImageDisplayHandlerMap* thisptr, + ImageDisplayHandlerMapInsertIt* iterator, + const TypeDescriptor** descriptor, + const ImageDisplayHandlerPtr* handler); + AddHandler addHandler; +}; + +Api& get(); + +ImageDisplayHandlerMap* instance(); + +} // namespace ImageDisplayHandlerApi + } // namespace game #endif // DISPLAYHANDLERS_H diff --git a/mss32/include/displayhandlershooks.h b/mss32/include/displayhandlershooks.h index 2d533c7f..c900ef2e 100644 --- a/mss32/include/displayhandlershooks.h +++ b/mss32/include/displayhandlershooks.h @@ -32,6 +32,14 @@ void __stdcall displayHandlerVillageHooked(game::ImageLayerList* list, int a6, bool animatedIso); +void __stdcall getMapElementIsoLayerImagesHooked(game::ImageLayerList* list, + const game::IMapElement* mapElement, + const game::IMidgardObjectMap* objectMap, + const game::CMidgardID* playerId, + const game::IdSet* objectives, + int unknown, + bool animatedIso); + } #endif // DISPLAYHANDLERSHOOKS_H diff --git a/mss32/include/draganddropinterf.h b/mss32/include/draganddropinterf.h index e0b32318..87584bfa 100644 --- a/mss32/include/draganddropinterf.h +++ b/mss32/include/draganddropinterf.h @@ -63,6 +63,12 @@ struct Api { using GetDialog = CDialogInterf*(__thiscall*)(CDragAndDropInterf* thisptr); GetDialog getDialog; + + using Constructor = CDragAndDropInterf*(__thiscall*)(CDragAndDropInterf* thisptr, + const char* dialogName, + ITask* task, + bool a8); + Constructor constructor; }; Api& get(); diff --git a/mss32/include/dynamiccast.h b/mss32/include/dynamiccast.h index ad3cba66..542aa833 100644 --- a/mss32/include/dynamiccast.h +++ b/mss32/include/dynamiccast.h @@ -55,7 +55,7 @@ struct BaseClassDescriptor std::uint32_t attributes; /**< Flags, usually 0. */ }; -struct BaseClassArray +struct alignas(16) BaseClassArray { // Nonstandard extension: zero-sized array in struct/union #pragma warning(suppress : 4200) @@ -63,7 +63,7 @@ struct BaseClassArray }; /** Describes inheritance hierarchy of a class. */ -struct ClassHierarchyDescriptor +struct alignas(16) ClassHierarchyDescriptor { std::uint32_t signature; std::uint32_t attributes; @@ -77,7 +77,7 @@ struct ClassHierarchyDescriptor * Pointer to this structure can be found in memory just before class vftable. * @see http://www.openrce.org/articles/full_view/23 for additional info. */ -struct CompleteObjectLocator +struct alignas(16) CompleteObjectLocator { std::uint32_t signature; std::uint32_t offset; /**< Offset of this vftable in complete class. */ @@ -175,10 +175,24 @@ struct Rtti TypeDescriptor* IUsNobleType; TypeDescriptor* IUsSummonType; TypeDescriptor* IItemExPotionBoostType; + TypeDescriptor* IMapElementType; + TypeDescriptor* CMidRoadType; + TypeDescriptor* CCmdStackVisitMsgType; + TypeDescriptor* CMidSiteType; + + BaseClassDescriptor* IMidObjectDescriptor; + BaseClassDescriptor* IMidScenarioObjectDescriptor; + BaseClassDescriptor* IMapElementDescriptor; + BaseClassDescriptor* IAiPriorityDescriptor; + BaseClassDescriptor* CMidSiteDescriptor; + BaseClassDescriptor* CNetMsgDescriptor; + BaseClassDescriptor* CNetMsgMapEntryDescriptor; }; const Rtti& rtti(); +const void* typeInfoVftable(); + } // namespace RttiApi } // namespace game diff --git a/mss32/include/editor.h b/mss32/include/editor.h index 9a478210..49022af8 100644 --- a/mss32/include/editor.h +++ b/mss32/include/editor.h @@ -20,6 +20,8 @@ #ifndef EDITOR_H #define EDITOR_H +#include "intvector.h" + namespace game { struct CMidgardID; @@ -31,6 +33,12 @@ struct TRaceType; struct CCapital; struct CVisitorAddPlayer; struct String; +struct CMqPoint; +struct CMidgardMap; +struct LSiteCategory; +struct IMqImage2; +struct CMidSite; +struct CTextBoxInterf; /** * Returns player id depending on RAD_CASTER radio button selection in DLG_EFFECT_CASTMAP dialog. @@ -44,7 +52,9 @@ using FindPlayerByRaceCategory = CMidPlayer*(__stdcall*)(const LRaceCategory* ra IMidgardObjectMap* objectMap); /** Returns true if tiles are suitable for site or ruin. */ -using CanPlace = bool(__stdcall*)(int, int, int); +using CanPlace = bool(__stdcall*)(const CMqPoint* position, + const CMidgardMap* map, + const IMidgardObjectMap* objectMap); /** Returns number of CMidStack objects on map. */ using CountStacksOnMap = int(__stdcall*)(IMidgardObjectMap* objectMap); @@ -68,6 +78,26 @@ using GetObjectNamePos = String*(__stdcall*)(String* description, const IMidgardObjectMap* objectMap, const CMidgardID* objectId); +/** + * Returns indices (G000SI0000) of site images in .ff files + * for specified site category. + */ +using GetSiteImageIndices = void(__stdcall*)(IntVector* indices, + const LSiteCategory* site, + int animatedIso); + +using GetSiteImage = IMqImage2*(__stdcall*)(const LSiteCategory* site, + int imageIndex, + bool animatedIso); + +using GetSiteAtPosition = const CMidSite*(__stdcall*)(const CMqPoint* mapPosition, + const IMidgardObjectMap* objectMap); + +using ShowOrHideSiteOnStrategicMap = void(__stdcall*)(const CMidSite* site, + const IMidgardObjectMap* objectMap, + const CMidgardID* playerId, + int a4); + /** Scenario Editor functions that can be hooked. */ struct EditorFunctions { @@ -80,6 +110,10 @@ struct EditorFunctions IsRaceCategoryPlayable isRaceCategoryPlayable; ChangeCapitalTerrain changeCapitalTerrain; GetObjectNamePos getObjectNamePos; + GetSiteImageIndices getSiteImageIndices; + GetSiteImage getSiteImage; + GetSiteAtPosition getSiteAtPosition; + ShowOrHideSiteOnStrategicMap showOrHideSiteOnStrategicMap; }; extern EditorFunctions editorFunctions; diff --git a/mss32/include/effectgiveresources.h b/mss32/include/effectgiveresources.h new file mode 100644 index 00000000..1d1d1457 --- /dev/null +++ b/mss32/include/effectgiveresources.h @@ -0,0 +1,37 @@ +/* + * This file is part of the modding toolset for Disciples 2. + * (https://github.com/VladimirMakeev/D2ModdingToolset) + * Copyright (C) 2024 Vladimir Makeev. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef EFFECTGIVERESOURCES_H +#define EFFECTGIVERESOURCES_H + +namespace game { +struct IEventEffect; +struct CMidgardID; +struct Bank; +} // namespace game + +namespace hooks { + +game::IEventEffect* createEffectGiveResources(const game::CMidgardID& playerId, + const game::Bank& resources, + bool add); + +} + +#endif // EFFECTGIVERESOURCES_H diff --git a/mss32/include/effectstealmarket.h b/mss32/include/effectstealmarket.h new file mode 100644 index 00000000..1630f7e7 --- /dev/null +++ b/mss32/include/effectstealmarket.h @@ -0,0 +1,39 @@ +/* + * This file is part of the modding toolset for Disciples 2. + * (https://github.com/VladimirMakeev/D2ModdingToolset) + * Copyright (C) 2024 Vladimir Makeev. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef EFFECTSTEALMARKET_H +#define EFFECTSTEALMARKET_H + +#include + +namespace game { +struct IEventEffect; +struct CMidgardID; +enum class CurrencyType : int; +} // namespace game + +namespace hooks { + +game::IEventEffect* createEffectStealMarket(const game::CMidgardID& marketId, + game::CurrencyType resource, + std::int16_t amount); + +} // namespace hooks + +#endif // EFFECTSTEALMARKET_H diff --git a/mss32/include/encparamidplayer.h b/mss32/include/encparamidplayer.h index afabdcf4..75993cb7 100644 --- a/mss32/include/encparamidplayer.h +++ b/mss32/include/encparamidplayer.h @@ -33,6 +33,24 @@ struct CEncParamIDPlayer : CEncParamBase assert_size(CEncParamIDPlayer, 16); +namespace CEncParamIDPlayerApi { + +struct Api +{ + using Constructor = CEncParamIDPlayer*(__thiscall*)(CEncParamIDPlayer* thisptr, + const CMidgardID* objectId, + const CMidgardID* playerId, + SmartPointer* functor); + Constructor constructor; + + using Destructor = void(__thiscall*)(CEncParamIDPlayer* thisptr); + Destructor destructor; +}; + +Api& get(); + +} // namespace CEncParamIDPlayerApi + } // namespace game #endif // ENCPARAMPLAYERID_H diff --git a/mss32/include/eventeffectbase.h b/mss32/include/eventeffectbase.h new file mode 100644 index 00000000..c3562e7b --- /dev/null +++ b/mss32/include/eventeffectbase.h @@ -0,0 +1,32 @@ +/* + * This file is part of the modding toolset for Disciples 2. + * (https://github.com/VladimirMakeev/D2ModdingToolset) + * Copyright (C) 2024 Vladimir Makeev. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef EVENTEFFECTBASE_H +#define EVENTEFFECTBASE_H + +#include "eventeffect.h" + +namespace game { + +struct CEventEffectBase : public IEventEffect +{ }; + +} // namespace game + +#endif // EVENTEFFECTBASE_H diff --git a/mss32/include/exchangeresourcesmsg.h b/mss32/include/exchangeresourcesmsg.h new file mode 100644 index 00000000..a06efd61 --- /dev/null +++ b/mss32/include/exchangeresourcesmsg.h @@ -0,0 +1,64 @@ +/* + * This file is part of the modding toolset for Disciples 2. + * (https://github.com/VladimirMakeev/D2ModdingToolset) + * Copyright (C) 2024 Vladimir Makeev. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef EXCHANGERESOURCESMSG_H +#define EXCHANGERESOURCESMSG_H + +#include "currency.h" +#include "midgardid.h" +#include "netmsg.h" +#include + +namespace game { +struct TypeDescriptor; +struct CPhaseGame; +} // namespace game + +namespace hooks { + +/** Describes exchange between player and a resource market site. */ +struct CExchangeResourcesMsg : public game::CNetMsg +{ + CExchangeResourcesMsg(); + + CExchangeResourcesMsg(const game::CMidgardID& siteId, + const game::CMidgardID& visitorStackId, + game::CurrencyType playerCurrency, + game::CurrencyType siteCurrency, + std::uint16_t amount); + + game::CMidgardID siteId; + game::CMidgardID visitorStackId; + game::CurrencyType playerCurrency; + game::CurrencyType siteCurrency; + std::uint16_t amount; +}; + +game::TypeDescriptor* getExchangeResourcesMsgTypeDescriptor(); + +void sendExchangeResourcesMsg(game::CPhaseGame* phaseGame, + const game::CMidgardID& siteId, + const game::CMidgardID& visitorStackId, + game::CurrencyType playerCurrency, + game::CurrencyType marketCurrency, + std::uint16_t amount); + +} // namespace hooks + +#endif // EXCHANGERESOURCESMSG_H diff --git a/mss32/include/game.h b/mss32/include/game.h index 1c3b9af6..ceada754 100644 --- a/mss32/include/game.h +++ b/mss32/include/game.h @@ -21,10 +21,12 @@ #define GAME_H #include "attacktypepairvector.h" +#include "d2set.h" #include "faceimg.h" #include "globaldata.h" #include "idlist.h" #include "mqpoint.h" +#include "nobleactioncat.h" #include "smartptr.h" namespace game { @@ -79,6 +81,11 @@ struct LDeathAnimCategory; struct CMidStreamEnvFile; struct CMidgardScenarioMap; struct TBuildingType; +struct CScenarioVisitor; +struct LSiteCategory; +struct CMidSite; +struct CTextBoxInterf; +struct CCmdNobleResultMsg; enum class ModifierElementTypeFlag : int; @@ -731,6 +738,32 @@ using GetBuildingStatus = BuildingStatus(__stdcall*)(const IMidgardObjectMap* ob const CMidgardID* buildingId, bool ignoreBuildTurnAndCost); +/** Removes stack from plan, fort and object map. */ +using RemoveStack = bool(__stdcall*)(const CMidgardID* stackId, + CMidgardPlan* plan, + IMidgardObjectMap* objectMap, + CScenarioVisitor* visitor); + +using GetSiteNameSuffix = const char*(__stdcall*)(const LSiteCategory* siteCategory); + +using UpdateEncLayoutSite = void(__stdcall*)(const CMidSite* site, CTextBoxInterf* textBox); + +using GetSiteSound = String*(__stdcall*)(String* soundName, const CMidSite* site); + +using SiteHasSound = bool(__stdcall*)(const CMidSite* site); + +using GetNobleActions = bool(__stdcall*)(const IMidgardObjectMap* objectMap, + const CMidgardID* playerId, + const CMidgardID* objectId, + Set* nobleActions); + +using GetNobleActionResultDescription = + String*(__stdcall*)(String* description, + const LNobleActionCat nobleActionCat, + const CCmdNobleResultMsg* nobleResultMsg, + const CPhaseGame* phaseGame, + const CMidPlayer* player); + /** Game and editor functions that can be hooked. */ struct Functions { @@ -857,6 +890,16 @@ struct Functions GetUnitRequiredBuildings getUnitRequiredBuildings; ComputeMovementCost computeMovementCost; GetBuildingStatus getBuildingStatus; + RemoveStack removeStack; + GetSiteNameSuffix getSiteNameSuffix; + UpdateEncLayoutSite updateEncLayoutSite; + GetSiteSound getSiteSound; + SiteHasSound siteHasSound; + /** Returns actions that noble can do with the site specified by id. */ + GetNobleActions getSiteNobleActions; + /** Returns all actions that noble can possibly perform on object with specified id. */ + GetNobleActions getPossibleNobleActions; + GetNobleActionResultDescription getNobleActionResultDescription; }; /** Global variables used in game. */ diff --git a/mss32/include/gameutils.h b/mss32/include/gameutils.h index 64fd5baa..2438413f 100644 --- a/mss32/include/gameutils.h +++ b/mss32/include/gameutils.h @@ -20,6 +20,8 @@ #ifndef GAMEUTILS_H #define GAMEUTILS_H +#include "mqpoint.h" + namespace game { struct CMidgardID; struct CMidUnitGroup; @@ -42,6 +44,8 @@ struct CMidDiplomacy; struct CMidgardMapFog; struct TBuildingType; struct CPlayerBuildings; +struct CMidLocation; +struct CMidStackDestroyed; } // namespace game namespace hooks { @@ -76,6 +80,9 @@ const game::CScenarioInfo* getScenarioInfo(const game::IMidgardObjectMap* object const game::CMidPlayer* getPlayer(const game::IMidgardObjectMap* objectMap, const game::CMidgardID* playerId); +game::CMidPlayer* getPlayerToChange(game::IMidgardObjectMap* objectMap, + const game::CMidgardID* playerId); + const game::CMidPlayer* getPlayer(const game::IMidgardObjectMap* objectMap, const game::BattleMsgData* battleMsgData, const game::CMidgardID* unitId); @@ -86,9 +93,16 @@ const game::CMidPlayer* getPlayerByUnitId(const game::IMidgardObjectMap* objectM const game::CMidgardID getPlayerIdByUnitId(const game::IMidgardObjectMap* objectMap, const game::CMidgardID* unitId); +const game::CMidPlayer* getNeutralPlayer(const game::IMidgardObjectMap* objectMap); + +/** Returns player that controls specified unit group. In case of ruins, returns Neutrals player. */ +const game::CMidPlayer* getGroupOwner(const game::IMidgardObjectMap* objectMap, + const game::CMidgardID* groupId); + const game::CMidScenVariables* getScenarioVariables(const game::IMidgardObjectMap* objectMap); const game::CMidgardPlan* getMidgardPlan(const game::IMidgardObjectMap* objectMap); +game::CMidgardPlan* getMidgardPlanToChange(game::IMidgardObjectMap* objectMap); const game::CMidgardMap* getMidgardMap(const game::IMidgardObjectMap* objectMap); @@ -174,6 +188,17 @@ const game::CMidDiplomacy* getDiplomacy(const game::IMidgardObjectMap* objectMap const game::CMidgardMapFog* getFog(const game::IMidgardObjectMap* objectMap, const game::CMidPlayer* player); +const game::CMidLocation* getLocation(const game::IMidgardObjectMap* objectMap, + const game::CMidgardID* locationId); + +const game::CMidStackDestroyed* getStackDestroyed(const game::IMidgardObjectMap* objectMap); + +bool isInventoryContainsItem(const game::IMidgardObjectMap* objectMap, + const game::CMidInventory& inventory, + const game::CMidgardID& globalItemId); + +const game::CMqPoint getObjectEntrance(const game::CMqPoint& position, int sizeX, int sizeY); + } // namespace hooks #endif // GAMEUTILS_H diff --git a/mss32/include/globaldata.h b/mss32/include/globaldata.h index 31ac4253..dfdcbd6a 100644 --- a/mss32/include/globaldata.h +++ b/mss32/include/globaldata.h @@ -77,6 +77,7 @@ struct CUnitGenerator; struct CItemBase; struct CDynUpgrade; struct CTileVariation; +struct CAiAttitudesTable; using RacesMap = mq_c_s>; using DynUpgradeList = List>; @@ -144,9 +145,9 @@ struct GlobalData int* transf; DynUpgradeList* dynUpgrade; CTileVariation* tileVariation; - int* aiAttitudes; + CAiAttitudesTable* aiAttitudes; int* aiMessages; - GlobalVariables** globalVariables; + GlobalVariables* globalVariables; CUnitGenerator* unitGenerator; int initialized; }; diff --git a/mss32/include/globalvariables.h b/mss32/include/globalvariables.h index 26d9858c..d1344d74 100644 --- a/mss32/include/globalvariables.h +++ b/mss32/include/globalvariables.h @@ -26,8 +26,10 @@ namespace game { +struct CProxyCodeBaseEnv; + /** Holds global game settings read from GVars.dbf. */ -struct GlobalVariables +struct GlobalVariablesData { /** 'MORALE_n' */ int morale[6]; @@ -51,7 +53,7 @@ struct GlobalVariables int batBmodif; /** Max abilities leader can learn. 'LDRMAXABIL' */ int maxLeaderAbilities; - /** Spy discovery change per turn. 'SPY_DISCOV' */ + /** Spy discovery chance per turn. 'SPY_DISCOV' */ int spyDiscoveryChance; /** 'POISON_S' */ int poisonStackDamage; @@ -83,9 +85,9 @@ struct GlobalVariables int rodRange; /** Profit per mana crystal or gold mine per turn. 'CRYSTAL_P' */ int crystalProfit; - /** 'CONST_UPG' */ - int constUpg; - /** Change to get spells with capture of a capital. 'GAIN_SPELL' */ + /** 'CONST_URG' */ + int constUrg; + /** Chance to get spells with capture of a capital. 'GAIN_SPELL' */ int gainSpellChance; /** Bonus per day regeneration for units in ruins. 'REGEN_RUIN' */ int regenRuin; @@ -155,9 +157,40 @@ struct GlobalVariables String tutorialName; }; -assert_size(GlobalVariables, 352); -assert_offset(GlobalVariables, rodPlacementCost, 148); -assert_offset(GlobalVariables, talismanCharges, 296); +assert_size(GlobalVariablesData, 352); +assert_offset(GlobalVariablesData, rodPlacementCost, 148); +assert_offset(GlobalVariablesData, talismanCharges, 296); + +struct GlobalVariablesDataHooked : public GlobalVariablesData +{ + /** Maximum amount of resource the noble can steal from resource market. 'STEAL_RMKT' */ + int stealRmkt; + /** Minimal resource market riot duration in days. 'RMKT_RIOT_MIN' */ + int rmktRiotMin; + /** Maximal resource market riot duration in days. 'RMKT_RIOT_MAX' */ + int rmktRiotMax; +}; + +struct GlobalVariables +{ + GlobalVariablesDataHooked* data; +}; + +assert_size(GlobalVariables, 4); + +namespace GlobalVariablesApi { + +struct Api +{ + using Constructor = GlobalVariables*(__thiscall*)(GlobalVariables* thisptr, + const char* directory, + CProxyCodeBaseEnv* proxy); + Constructor constructor; +}; + +Api& get(); + +} // namespace GlobalVariablesApi } // namespace game diff --git a/mss32/include/globalvariableshooks.h b/mss32/include/globalvariableshooks.h new file mode 100644 index 00000000..a750a6fc --- /dev/null +++ b/mss32/include/globalvariableshooks.h @@ -0,0 +1,37 @@ +/* + * This file is part of the modding toolset for Disciples 2. + * (https://github.com/VladimirMakeev/D2ModdingToolset) + * Copyright (C) 2024 Vladimir Makeev. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef GLOBALVARIABLESHOOKS_H +#define GLOBALVARIABLESHOOKS_H + +namespace game { +struct GlobalVariables; +struct CProxyCodeBaseEnv; +} // namespace game + +namespace hooks { + +game::GlobalVariables* __fastcall globalVariablesCtorHooked(game::GlobalVariables* thisptr, + int /*%edx*/, + const char* directory, + game::CProxyCodeBaseEnv* proxy); + +} + +#endif // GLOBALVARIABLESHOOKS_H diff --git a/mss32/include/hooks.h b/mss32/include/hooks.h index b7d70255..7588e0c8 100644 --- a/mss32/include/hooks.h +++ b/mss32/include/hooks.h @@ -66,6 +66,11 @@ struct IUsUnit; struct TBuildingType; struct TUsUnitImpl; struct EditBoxData; +struct CMidgardPlan; +struct CScenarioVisitor; +struct LSiteCategory; +struct CMidSite; +struct CTextBoxInterf; enum class BuildingBranchNumber : int; enum class CanApplyPotionResult : int; @@ -295,6 +300,23 @@ game::BuildingStatus __stdcall getBuildingStatusHooked(const game::IMidgardObjec const game::CMidgardID* buildingId, bool ignoreBuildTurnAndCost); +bool __stdcall removeStackHooked(const game::CMidgardID* stackId, + game::CMidgardPlan* plan, + game::IMidgardObjectMap* objectMap, + game::CScenarioVisitor* visitor); + +bool __stdcall setStackSrcTemplateHooked(const game::CMidgardID* stackId, + const game::CMidgardID* stackTemplateId, + game::IMidgardObjectMap* objectMap, + int apply); +const char* __stdcall getSiteNameSuffixHooked(const game::LSiteCategory* siteCategory); + +void __stdcall updateEncLayoutSiteHooked(const game::CMidSite* site, game::CTextBoxInterf* textBox); + +game::String* __stdcall getSiteSoundHooked(game::String* soundName, const game::CMidSite* site); + +bool __stdcall siteHasSoundHooked(const game::CMidSite* site); + } // namespace hooks #endif // HOOKS_H diff --git a/mss32/include/imagelayerlist.h b/mss32/include/imagelayerlist.h index a3a41965..e1545459 100644 --- a/mss32/include/imagelayerlist.h +++ b/mss32/include/imagelayerlist.h @@ -22,6 +22,7 @@ #include "d2list.h" #include "d2pair.h" +#include "idset.h" namespace game { @@ -30,6 +31,7 @@ struct CIsoLayer; struct CFortification; struct IMidgardObjectMap; struct CMidgardID; +struct IMapElement; using ImageLayerPair = Pair; using ImageLayerList = List; @@ -56,6 +58,26 @@ struct Api const IMidgardObjectMap* objectMap, const CMidgardID* playerId); AddShieldImageLayer addShieldImageLayer; + + /** + * Adds images and iso layers to the list depending on specified map element. + * Images describe how map element should look on each layer on a strategic map. + * @param list - list of images and corresponding iso layers. + * @param mapElement - map element to show on a strategic map. + * @param objectMap - objects storage. + * @param playerId - id of current player. + * @param objectives - list of scenario objective ids. + * @param unknown + * @param animatedIso - whether strategic map should show animated graphics. + */ + using GetMapElementIsoLayerImages = void(__stdcall*)(ImageLayerList* list, + const IMapElement* mapElement, + const IMidgardObjectMap* objectMap, + const CMidgardID* playerId, + const IdSet* objectives, + int unknown, + bool animatedIso); + GetMapElementIsoLayerImages getMapElementIsoLayerImages; }; Api& get(); diff --git a/mss32/include/interface.h b/mss32/include/interface.h index 6b3661f5..e89b4372 100644 --- a/mss32/include/interface.h +++ b/mss32/include/interface.h @@ -68,10 +68,8 @@ struct CInterfaceVftable using Destructor = void(__thiscall*)(CInterface* thisptr, char flags); Destructor destructor; - /** Draws childs of specified root element. */ - using Draw = void(__thiscall*)(CInterface* thisptr, - CInterface* rootElement, - IMqRenderer2* renderer); + /** Draws interface element and its children. */ + using Draw = void(__stdcall*)(CInterface* thisptr, IMqRenderer2* renderer); Draw draw; /** Handles mouse position changes. */ diff --git a/mss32/include/isoview.h b/mss32/include/isoview.h index 234559c3..8b773feb 100644 --- a/mss32/include/isoview.h +++ b/mss32/include/isoview.h @@ -64,6 +64,19 @@ struct CIsoView : public CFullScreenInterf CIsoViewData* isoViewData; }; +namespace CIsoViewApi { + +struct Api +{ + using UpdateSelectedTileInfo = void(__thiscall*)(CIsoView* thisptr, + const CMqPoint* mapPosition); + UpdateSelectedTileInfo updateSelectedTileInfo; +}; + +Api& get(); + +} // namespace CIsoViewApi + } // namespace editor } // namespace game diff --git a/mss32/include/isoviewhooks.h b/mss32/include/isoviewhooks.h new file mode 100644 index 00000000..712c9083 --- /dev/null +++ b/mss32/include/isoviewhooks.h @@ -0,0 +1,40 @@ +/* + * This file is part of the modding toolset for Disciples 2. + * (https://github.com/VladimirMakeev/D2ModdingToolset) + * Copyright (C) 2024 Vladimir Makeev. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef ISOVIEWHOOKS_H +#define ISOVIEWHOOKS_H + +namespace game { + +namespace editor { +struct CIsoView; +} + +struct CMqPoint; +} // namespace game + +namespace hooks { + +void __fastcall updateSelectedTileInfoHooked(game::editor::CIsoView* thisptr, + int /* %edx */, + const game::CMqPoint* mapPosition); + +} + +#endif // ISOVIEWHOOKS_H diff --git a/mss32/include/log.h b/mss32/include/log.h index 3059902d..83a6a661 100644 --- a/mss32/include/log.h +++ b/mss32/include/log.h @@ -20,15 +20,15 @@ #ifndef LOG_H #define LOG_H -#include +#include namespace hooks { /** Prints message to the file only if debug mode setting is enabled. */ -void logDebug(const std::string& logFile, const std::string& message); +void logDebug(std::string_view logFile, std::string_view message); /** Prints message to the file. */ -void logError(const std::string& logFile, const std::string& message); +void logError(std::string_view logFile, std::string_view message); } // namespace hooks diff --git a/mss32/include/lordtype.h b/mss32/include/lordtype.h index 04461fa6..1067154b 100644 --- a/mss32/include/lordtype.h +++ b/mss32/include/lordtype.h @@ -81,6 +81,7 @@ assert_size(TLordTypeData, 56); assert_offset(TLordTypeData, name, 4); assert_offset(TLordTypeData, description, 12); assert_offset(TLordTypeData, buildList, 32); +assert_offset(TLordTypeData, lordCategory, 44); /** Holds information read from GLord.dbf. */ struct TLordType : public IMidObject diff --git a/mss32/include/mainview2.h b/mss32/include/mainview2.h index 6544d300..26a81b71 100644 --- a/mss32/include/mainview2.h +++ b/mss32/include/mainview2.h @@ -32,6 +32,7 @@ struct CZoomInterface; struct CDialogInterf; struct IMqImage2; struct CToggleButton; +struct CCommandMsg; struct CMainView2 : public CFullScreenInterf @@ -46,7 +47,7 @@ struct CMainView2 CDialogInterf* resourcePopup; char unknown14; char padding[3]; - int unknown15; + CInterface* emptyInterface; CDialogInterf* dialogInterf; CMqRect imgResourceArea; CMqPoint bgndFillSize; @@ -58,7 +59,7 @@ struct CMainView2 IMqImage2* isoPalFill; CMqPoint imgPaletteTopLeftXBottomRightY; IMqImage2* isoBlackbar; - int unknown34; + int isoAreaWidthBlackbarAdjusted; int unknown35; bool unknown36; bool unknown37; @@ -97,6 +98,10 @@ struct Api CMainView2* mainView, ToggleButtonCallback* callback); CreateToggleButtonFunctor createToggleButtonFunctor; + + using HandleCmdStackVisitMsg = void(__thiscall*)(CMainView2* thisptr, + const CCommandMsg* stackVisitMsg); + HandleCmdStackVisitMsg handleCmdStackVisitMsg; }; Api& get(); diff --git a/mss32/include/mainview2hooks.h b/mss32/include/mainview2hooks.h index 5d3b6620..dcc5228c 100644 --- a/mss32/include/mainview2hooks.h +++ b/mss32/include/mainview2hooks.h @@ -22,12 +22,17 @@ namespace game { struct CMainView2; -} +struct CCommandMsg; +} // namespace game namespace hooks { void __fastcall mainView2ShowIsoDialogHooked(game::CMainView2* thisptr, int /*%edx*/); -} +void __fastcall mainView2HandleCmdStackVisitMsgHooked(game::CMainView2* thisptr, + int /*%edx*/, + const game::CCommandMsg* stackVisitMsg); + +} // namespace hooks #endif // MAINVIEW2HOOKS_H diff --git a/mss32/include/mapelement.h b/mss32/include/mapelement.h index 69baa03f..2d36bdaa 100644 --- a/mss32/include/mapelement.h +++ b/mss32/include/mapelement.h @@ -43,19 +43,9 @@ struct IMapElementVftable { using Destructor = void(__thiscall*)(IMapElement* thisptr, char flags); Destructor destructor; - - using Initialize = bool(__thiscall*)(IMapElement* thisptr, - const IMidgardObjectMap* objectMap, - const CMidgardID* leaderId, - const CMidgardID* ownerId, - const CMidgardID* subraceId, - const CMqPoint* position); - Initialize initialize; - - void* method2; }; -assert_vftable_size(IMapElementVftable, 3); +assert_vftable_size(IMapElementVftable, 1); } // namespace game diff --git a/mss32/include/mapinterf.h b/mss32/include/mapinterf.h index eda0e5ff..2eed223d 100644 --- a/mss32/include/mapinterf.h +++ b/mss32/include/mapinterf.h @@ -22,7 +22,10 @@ #include "isoview.h" -namespace game::editor { +namespace game { +struct CToggleButton; + +namespace editor { struct CTaskMapChange; @@ -76,12 +79,27 @@ struct Api CreateTask createAddMountainsTask; CreateTask createAddTreesTask; CreateTask createChangeHeightTask; + + struct ToggleButtonCallback + { + using Callback = void(__thiscall*)(CMapInterf* thisptr, bool, CToggleButton*); + + Callback callback; + int unknown; + }; + + using CreateToggleButtonFunctor = SmartPointer*(__stdcall*)(SmartPointer* functor, + int a2, + CMapInterf* mapInterf, + ToggleButtonCallback* callback); + CreateToggleButtonFunctor createToggleButtonFunctor; }; Api& get(); } // namespace CMapInterfApi -} // namespace game::editor +} // namespace editor +} // namespace game #endif // MAPINTERF_H diff --git a/mss32/include/menubase.h b/mss32/include/menubase.h index 50052610..a086243f 100644 --- a/mss32/include/menubase.h +++ b/mss32/include/menubase.h @@ -181,6 +181,18 @@ struct Api CMenuBase* menu, const PictureCallback* callback); CreatePictureFunctor createPictureFunctor; + + using RadioButtonCallback = void(__thiscall*)(CMenuBase* thisptr, int index); + + /** + * Creates functor for radio button that will handle button press events. + * Reused from CMenuLord. + */ + using CreateRadioButtonFunctor = SmartPointer(__stdcall*)(SmartPointer* functor, + int, + CMenuBase* menu, + const RadioButtonCallback* callback); + CreateRadioButtonFunctor createRadioButtonFunctor; }; Api& get(); diff --git a/mss32/include/midclient.h b/mss32/include/midclient.h index fba61671..1fffc607 100644 --- a/mss32/include/midclient.h +++ b/mss32/include/midclient.h @@ -25,25 +25,26 @@ #include "midclientcore.h" #include "midcommandqueue2.h" #include "midgardid.h" +#include "playerlistentry.h" #include "textmessage.h" #include "uievent.h" namespace game { struct CPhase; +struct ILoveChat; +struct INotifyPlayerList; struct CMidClientData { CPhase* phase; - int unknown2; - Vector messages; - List list; - List list2; + bool scenarioStarted; + char padding[3]; + Vector textMessages; /**< Chat, player join and disconnect messages. */ + List chatList; /**< Chat messages subscribers. */ + List notifyPlayerList; List list3; - int unknown7; - int unknown8; - int unknown9; - int unknown10; + Vector playerListEntries; UiEvent notificationFadeEvent; /**< Restores window notify state. */ /** Flashes game window each second to notify player, for example when battle starts. */ UiEvent notificationShowEvent; diff --git a/mss32/include/midclientcore.h b/mss32/include/midclientcore.h index 12b976af..a9295946 100644 --- a/mss32/include/midclientcore.h +++ b/mss32/include/midclientcore.h @@ -25,17 +25,18 @@ namespace game { struct CMidgard; -struct IMidgardObjectMap; +struct CMidDataCache2; struct CMidCommandQueue2; struct CoreCommandUpdate; struct CCommandCanIgnore; struct CMidHotseatManager; +struct CNetMsg; struct CMidClientCoreData { CMidgard* midgard; int unknown; - IMidgardObjectMap* objectMap; + CMidDataCache2* dataCache; int unknown3; CMidCommandQueue2* commandQueue; CoreCommandUpdate* coreCommandUpdate; @@ -54,7 +55,7 @@ namespace CMidClientCoreApi { struct Api { - using GetObjectMap = IMidgardObjectMap*(__thiscall*)(CMidClientCore* thisptr); + using GetObjectMap = CMidDataCache2*(__thiscall*)(CMidClientCore* thisptr); GetObjectMap getObjectMap; }; diff --git a/mss32/include/midcommandqueue2.h b/mss32/include/midcommandqueue2.h index 3e890331..d3973b6e 100644 --- a/mss32/include/midcommandqueue2.h +++ b/mss32/include/midcommandqueue2.h @@ -82,6 +82,18 @@ struct CMidCommandQueue2::INotifyCQVftable assert_vftable_size(CMidCommandQueue2::INotifyCQVftable, 2); +namespace CMidCommandQueue2Api { + +struct Api +{ + using ProcessCommands = void(__thiscall*)(CMidCommandQueue2* thisptr); + ProcessCommands processCommands; +}; + +Api& get(); + +} // namespace CMidCommandQueue2Api + } // namespace game #endif // MIDCOMMANDQUEUE2_H diff --git a/mss32/include/midcrystal.h b/mss32/include/midcrystal.h new file mode 100644 index 00000000..fbc4c193 --- /dev/null +++ b/mss32/include/midcrystal.h @@ -0,0 +1,42 @@ +/* + * This file is part of the modding toolset for Disciples 2. + * (https://github.com/VladimirMakeev/D2ModdingToolset) + * Copyright (C) 2024 Vladimir Makeev. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef MIDCRYSTAL_H +#define MIDCRYSTAL_H + +#include "aipriority.h" +#include "mapelement.h" +#include "midscenarioobject.h" +#include "resourcetype.h" + +namespace game { + +/** Represents gold mine or mana crystal in scenario file and game. */ +struct CMidCrystal : public IMidScenarioObject +{ + IMapElement mapElement; + IAiPriority aiPriority; + LResourceType resourceType; +}; + +assert_size(CMidCrystal, 48); + +} // namespace game + +#endif // MIDCRYSTAL_H diff --git a/mss32/include/middatacache.h b/mss32/include/middatacache.h index bf9d4478..87b1bbd4 100644 --- a/mss32/include/middatacache.h +++ b/mss32/include/middatacache.h @@ -55,6 +55,24 @@ struct CMidDataCache2 : public IMidgardObjectMap assert_size(CMidDataCache2, 36); +namespace CMidDataCache2Api { + +struct Api +{ + using ChangeNotify = void(__thiscall*)(CMidDataCache2* thisptr, + CMidDataCache2::INotify* notify); + + /** Adds subscriber that will receive scenario object chages notifications. */ + ChangeNotify addNotify; + + /** Removes scenario object chages notifications subscriber. */ + ChangeNotify removeNotify; +}; + +Api& get(); + +} // namespace CMidDataCache2Api + } // namespace game #endif // MIDDATACACHE_H diff --git a/mss32/include/middragdropinterf.h b/mss32/include/middragdropinterf.h index 9b5f92f3..5d92bdae 100644 --- a/mss32/include/middragdropinterf.h +++ b/mss32/include/middragdropinterf.h @@ -35,6 +35,36 @@ struct CMidDragDropInterf : public CDragAndDropInterf assert_offset(CMidDragDropInterf, phaseGame, 24); +namespace CMidDragDropInterfApi { + +struct Api +{ + using Constructor = CMidDragDropInterf*(__thiscall*)(CMidDragDropInterf* thisptr, + const char* dialogName, + ITask* task, + CPhaseGame* phaseGame); + Constructor constructor; + + /** + * This is not a scalar deleting destructor, its a common one. + * It does not free memory allocated for an object + */ + using Destructor = void(__thiscall*)(CMidDragDropInterf* thisptr); + Destructor destructor; + + using RemoveDropTarget = void(__thiscall*)(CMidDragDropInterf* thisptr, + IMidDropTarget* dropTarget); + RemoveDropTarget removeDropTarget; + + using RemoveDropSource = void(__thiscall*)(CMidDragDropInterf* thisptr, + IMidDropSource* dropSource); + RemoveDropSource removeDropSource; +}; + +Api& get(); + +} // namespace CMidDragDropInterfApi + } // namespace game #endif // MIDDRAGDROPINTERF_H diff --git a/mss32/include/middropmanager.h b/mss32/include/middropmanager.h index 5387e92c..c04470f7 100644 --- a/mss32/include/middropmanager.h +++ b/mss32/include/middropmanager.h @@ -26,6 +26,7 @@ namespace game { struct IMidDropManagerVftable; struct IMidDropSource; +struct IMidDropTarget; struct IMidDropManager { @@ -40,6 +41,7 @@ struct IMidDropManagerVftable using GetDropSource = IMidDropSource*(__thiscall*)(IMidDropManager* thisptr); GetDropSource getDropSource; + /** Actual 'drop' action logic. */ using SetDropSource = void(__thiscall*)(IMidDropManager* thisptr, IMidDropSource* value, bool a3); @@ -50,7 +52,23 @@ struct IMidDropManagerVftable using ResetDropSource = void(__thiscall*)(IMidDropManager* thisptr); ResetDropSource resetDropSource; - void* methods[9]; + void* method6; + void* method7; + void* method8; + + void* method9; + + using AddDropSource = void(__thiscall*)(IMidDropManager* thisptr, IMidDropSource* dropSource); + AddDropSource addDropSource; + + using AddDropTarget = void(__thiscall*)(IMidDropManager* thisptr, IMidDropTarget* dropTarget); + AddDropTarget addDropTarget; + + void* method12; + void* method13; + + using IsShiftPressed = bool (*)(); + IsShiftPressed isShiftPressed; }; assert_vftable_size(IMidDropManagerVftable, 14); diff --git a/mss32/include/middropsource.h b/mss32/include/middropsource.h index 19e60f81..f5cebfcb 100644 --- a/mss32/include/middropsource.h +++ b/mss32/include/middropsource.h @@ -25,6 +25,8 @@ namespace game { struct IMidDropSourceVftable; +struct TypeDescriptor; +struct IMidDropManager; struct IMidDropSource { @@ -43,7 +45,11 @@ struct IMidDropSourceVftable GetCursorHandle getCursorHandle; void* method2; - void* method3; + + using HandleMouse = void(__thiscall*)(IMidDropSource* thisptr, + int mouseKey, + const CMqPoint* mousePosition); + HandleMouse handleMouse; using IsPointOverButton = bool(__thiscall*)(IMidDropSource* thisptr, const CMqPoint* point); IsPointOverButton isPointOverButton; @@ -56,10 +62,12 @@ struct IMidDropSourceVftable using Method7 = int(__thiscall*)(IMidDropSource* thisptr); Method7 method7; - void* method8; + using CastTo = void*(__thiscall*)(IMidDropSource* thisptr, const TypeDescriptor* type); + CastTo castTo; - using Method9 = int(__thiscall*)(IMidDropSource* thisptr, bool buttonPressed); - Method9 method9; + using GetDropManager = IMidDropManager*(__thiscall*)(IMidDropSource* thisptr, + bool buttonPressed); + GetDropManager getDropManager; }; assert_vftable_size(IMidDropSourceVftable, 10); diff --git a/mss32/include/midfreetask.h b/mss32/include/midfreetask.h index dd7f51cd..afe4cb86 100644 --- a/mss32/include/midfreetask.h +++ b/mss32/include/midfreetask.h @@ -35,6 +35,19 @@ struct CMidFreeTask : public IMidTask assert_size(CMidFreeTask, 16); +namespace CMidFreeTaskApi { + +struct Api +{ + using Constructor = CMidFreeTask*(__thiscall*)(CMidFreeTask* thisptr, + CTaskManager* taskManager); + Constructor constructor; +}; + +Api& get(); + +} // namespace CMidFreeTaskApi + } // namespace game #endif // MIDFREETASK_H diff --git a/mss32/include/midgardid.h b/mss32/include/midgardid.h index 2c5ea77d..76f1fdf2 100644 --- a/mss32/include/midgardid.h +++ b/mss32/include/midgardid.h @@ -141,6 +141,16 @@ static constexpr bool operator<(const CMidgardID& first, const CMidgardID& secon extern const CMidgardID invalidId; extern const CMidgardID emptyId; +struct CMidgardIDHash +{ + std::size_t operator()(const game::CMidgardID& id) const + { + // All identifiers and their 32-bit value representaions must be unique. + // Use raw value as a hash. + return static_cast(id.value); + } +}; + namespace CMidgardIDApi { struct Api diff --git a/mss32/include/midgardmap.h b/mss32/include/midgardmap.h index decf5cfd..2a7175ac 100644 --- a/mss32/include/midgardmap.h +++ b/mss32/include/midgardmap.h @@ -50,6 +50,12 @@ struct Api IMidgardObjectMap* objectMap); ChangeTerrain changeTerrain; + using GetTerrain = bool(__thiscall*)(const CMidgardMap* thisptr, + LTerrainCategory* terrain, + const CMqPoint* position, + const IMidgardObjectMap* objectMap); + GetTerrain getTerrain; + /** Returns ground type of map tile at specified position. */ using GetGround = bool(__thiscall*)(const CMidgardMap* thisptr, LGroundCategory* ground, diff --git a/mss32/include/midgardobjectmap.h b/mss32/include/midgardobjectmap.h index c5f894dc..e4adb6f3 100644 --- a/mss32/include/midgardobjectmap.h +++ b/mss32/include/midgardobjectmap.h @@ -75,7 +75,7 @@ struct IMidgardObjectMapVftable using GetObjectsTotal = int(__thiscall*)(const IMidgardObjectMap* thisptr); GetObjectsTotal getObjectsTotal; - using GetIterator = IteratorPtr*(__thiscall*)(IMidgardObjectMap* thisptr, + using GetIterator = IteratorPtr*(__thiscall*)(const IMidgardObjectMap* thisptr, IteratorPtr* iterator); /** Returns an iterator to the first record. */ diff --git a/mss32/include/midgardplan.h b/mss32/include/midgardplan.h index 2a83b0d5..3403fad8 100644 --- a/mss32/include/midgardplan.h +++ b/mss32/include/midgardplan.h @@ -21,6 +21,7 @@ #define MIDGARDPLAN_H #include "d2vector.h" +#include "idlist.h" #include "midgardid.h" #include "midscenarioobject.h" #include "mqpoint.h" @@ -28,6 +29,8 @@ namespace game { +struct IMapElement; + struct MidgardPlanElement { std::uint16_t y; @@ -39,13 +42,35 @@ assert_size(MidgardPlanElement, 8); using PlanElements = Vector; +/** + * Element flags packing: + * + * +--+--+--+--+--+--+--+--+ + * |S3|D3|S2|D2|S1|D1|S0|D0| + * +--+--+--+--+--+--+--+--+ + * 7 6 5 4 3 2 1 0 + * + * DN - bit indicating presence of a dynamic element on a tile with relative X position of N. + * SN - bit indicating presence of a static element on a tile with relative X position of N. + * + * Relative X position: (X & 3) + */ + +/** Utility object used for mapping IMapElements coordinates to scenario object ids. */ struct CMidgardPlan : public IMidScenarioObject { CMidgardID unknownId; int mapSize; - char data[5184]; /**< 144 * 36. Accessed as: 36 * posY + (posX >> 2) */ - PlanElements elements; - PlanElements elements2; + /** + * 36 x 144 array of bit flags indicating presence of a static or dynamic elements on a tile. + * Each array element stores flags of 4 adjacent tiles along X axis. + * Accessed as: 36 * Y + X / 4. + */ + std::uint8_t elementFlags[5184]; + /** Static map objects such as: Fort, Road, Landmark, Site, Ruin, Crystal, Location. */ + PlanElements staticElements; + /** Dynamic objects: Stack, Bag, Tomb, Rod. */ + PlanElements dynamicElements; }; assert_size(CMidgardPlan, 5232); @@ -69,6 +94,23 @@ struct Api const IdType* objectTypes, std::uint32_t typesTotal); IsPositionContainsObjects isPositionContainsObjects; + + /** Returns true if CMidSite object can be placed at specified position. */ + using CanPlaceSite = bool(__stdcall*)(const CMqPoint* mapPosition, + const CMidgardPlan* plan, + const CMidgardID* siteId); + CanPlaceSite canPlaceSite; + + /** Adds map element to plan. */ + using AddMapElement = bool(__thiscall*)(CMidgardPlan* thisptr, + const IMapElement* mapElement, + bool unknown); + AddMapElement addMapElement; + + using GetObjectsAtPoint = bool(__thiscall*)(const CMidgardPlan* thisptr, + IdList* objectIds, + const CMqPoint* mapPosition); + GetObjectsAtPoint getObjectsAtPoint; }; Api& get(); diff --git a/mss32/include/miditemext.h b/mss32/include/miditemext.h new file mode 100644 index 00000000..99d928a4 --- /dev/null +++ b/mss32/include/miditemext.h @@ -0,0 +1,32 @@ +/* + * This file is part of the modding toolset for Disciples 2. + * (https://github.com/VladimirMakeev/D2ModdingToolset) + * Copyright (C) 2024 Vladimir Makeev. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef MIDITEMEXT_H +#define MIDITEMEXT_H + +namespace game { + +struct IMidItemExt +{ + void* vftable; +}; + +} // namespace game + +#endif // MIDITEMEXT_H diff --git a/mss32/include/midplayer.h b/mss32/include/midplayer.h index 5502256a..b3367d9f 100644 --- a/mss32/include/midplayer.h +++ b/mss32/include/midplayer.h @@ -20,7 +20,7 @@ #ifndef MIDPLAYER_H #define MIDPLAYER_H -#include "categories.h" +#include "aiattitudescat.h" #include "currency.h" #include "d2set.h" #include "midgardid.h" diff --git a/mss32/include/midserverlogic.h b/mss32/include/midserverlogic.h index 0eeff320..c8df0e52 100644 --- a/mss32/include/midserverlogic.h +++ b/mss32/include/midserverlogic.h @@ -21,6 +21,7 @@ #define MIDSERVERLOGIC_H #include "d2set.h" +#include "idlist.h" #include "idset.h" #include "midmsgsender.h" #include "midserverlogiccore.h" @@ -32,6 +33,8 @@ struct CMidServer; struct CStreamBits; struct AiLogic; struct CMidgardScenarioMap; +struct IEventEffect; +struct CMqPoint; /* * All the fields initially point to the same parent logic. Is this some kind of enumerable @@ -85,10 +88,11 @@ struct CMidServerLogic CStreamBits* streamBits; int unknown7; CMidServerLogicData2 data2; - int unknown8; + int currentPlayerIndex; int unknown9; bool turnNumberIsZero; char padding[3]; + // Assumption: List> messagesAndStreams; List list; int unknown11; int unknown12; @@ -110,7 +114,7 @@ assert_offset(CMidServerLogic, data, 24); assert_offset(CMidServerLogic, aiLogic, 36); assert_offset(CMidServerLogic, playersIdList, 56); assert_offset(CMidServerLogic, data2, 80); -assert_offset(CMidServerLogic, unknown8, 244); +assert_offset(CMidServerLogic, currentPlayerIndex, 244); assert_offset(CMidServerLogic, unknown11, 272); assert_offset(CMidServerLogic, list2, 300); assert_offset(CMidServerLogic, unknown17, 316); @@ -139,6 +143,84 @@ struct Api const CMidgardID* toStackId, const IdSet* itemIds); StackExchangeItem stackExchangeItem; + + using ApplyEventEffectsAndCheckMidEventTriggerers = + bool(__thiscall*)(CMidServerLogic** thisptr, + List* effectsList, + const CMidgardID* triggererId, + const CMidgardID* playingStackId); + ApplyEventEffectsAndCheckMidEventTriggerers applyEventEffectsAndCheckMidEventTriggerers; + + using StackMove = bool(__thiscall*)(CMidServerLogic** thisptr, + const CMidgardID* playerId, + List>* movementPath, + const CMidgardID* stackId, + const CMqPoint* startingPoint, + const CMqPoint* endPoint); + StackMove stackMove; + + using FilterAndProcessEventsNoPlayer = bool(__stdcall*)(IMidgardObjectMap* objectMap, + List* eventObjectList, + List* effectsList, + bool* stopProcessing, + IdList* executedEvents, + const CMidgardID* triggererStackId, + const CMidgardID* playingStackId); + FilterAndProcessEventsNoPlayer filterAndProcessEventsNoPlayer; + + using CheckAndExecuteEvent = bool(__stdcall*)(IMidgardObjectMap* objectMap, + List* effectsList, + bool* stopProcessing, + const CMidgardID* eventId, + const CMidgardID* playerId, + const CMidgardID* stackTriggererId, + const CMidgardID* playingStackId, + int samePlayer); + CheckAndExecuteEvent checkAndExecuteEvent; + + using ExecuteEventEffects = void(__stdcall*)(IMidgardObjectMap* objectMap, + List* effectsList, + bool* stopProcessing, + const CMidgardID* eventId, + const CMidgardID* playerId, + const CMidgardID* stackTriggererId, + const CMidgardID* playingStackId); + ExecuteEventEffects executeEventEffects; + + using FilterAndProcessEvents = bool(__stdcall*)(IMidgardObjectMap* objectMap, + List* eventObjectList, + List* effectsList, + bool* stopProcessing, + IdList* executedEvents, + const CMidgardID* playerId, + const CMidgardID* triggererStackId, + const CMidgardID* playingStackId); + FilterAndProcessEvents filterAndProcessEvents; + + using CheckEventConditions = bool(__stdcall*)(const IMidgardObjectMap* objectMap, + List* effectsList, + const CMidgardID* playerId, + const CMidgardID* stackTriggererId, + int samePlayer, + const CMidgardID* eventId); + CheckEventConditions checkEventConditions; + + using Constructor = CMidServerLogic*(__thiscall*)(CMidServerLogic* thisptr, + CMidServer* server, + bool multiplayerGame, + bool hotseatGame, + int a5, + int gameVersion); + Constructor constructor; + + using GetPlayerInfo = NetPlayerInfo*(__thiscall*)(CMidServerLogic* thisptr, + std::uint32_t playerNetId); + GetPlayerInfo getPlayerInfo; + + /** Returns true if player with specified id is current: it actively plays its turn. */ + using IsCurrentPlayer = bool(__stdcall*)(CMidServerLogic* serverLogic, + const CMidgardID* playerId); + IsCurrentPlayer isCurrentPlayer; }; Api& get(); diff --git a/mss32/include/midserverlogiccore.h b/mss32/include/midserverlogiccore.h index 9c823ca9..86f6948a 100644 --- a/mss32/include/midserverlogiccore.h +++ b/mss32/include/midserverlogiccore.h @@ -35,6 +35,7 @@ struct CMidServer; struct IMidgardObjectMap; struct NetMsgEntryData; struct NetPlayerInfo; +struct CMidEvent; struct CMidServerLogicCoreData { @@ -47,7 +48,7 @@ struct CMidServerLogicCoreData int gameVersion; NetMsgEntryData** netMsgEntryData; IMidgardObjectMap* objectMap; - List list; + List eventObjectsList; int unknown4; std::uint32_t playerNetId; Vector* players; @@ -70,7 +71,7 @@ struct CMidServerLogicCoreData }; assert_size(CMidServerLogicCoreData, 72); -assert_offset(CMidServerLogicCoreData, list, 24); +assert_offset(CMidServerLogicCoreData, eventObjectsList, 24); assert_offset(CMidServerLogicCoreData, players, 48); struct CMidServerLogicCore : public IMqNetTraffic diff --git a/mss32/include/midserverlogichooks.h b/mss32/include/midserverlogichooks.h index d58ed619..248ddff7 100644 --- a/mss32/include/midserverlogichooks.h +++ b/mss32/include/midserverlogichooks.h @@ -20,12 +20,19 @@ #ifndef MIDSERVERLOGICHOOKS_H #define MIDSERVERLOGICHOOKS_H +#include "d2pair.h" #include "d2set.h" +#include "idlist.h" +#include "mqpoint.h" namespace game { struct IMidMsgSender; struct CMidServerLogic; -struct CMidgardID; +struct IMidgardObjectMap; +struct CMidEvent; +struct IEventEffect; +struct ITestCondition; +struct CMidServer; } // namespace game namespace hooks { @@ -37,6 +44,133 @@ bool __fastcall midServerLogicSendRefreshInfoHooked(const game::CMidServerLogic* const game::Set* objectsList, std::uint32_t playerNetId); +bool __fastcall applyEventEffectsAndCheckMidEventTriggerersHooked( + game::CMidServerLogic** thisptr, + int /*%edx*/, + game::List* effectsList, + const game::CMidgardID* triggererId, + const game::CMidgardID* playingStackId); + +bool __fastcall stackMoveHooked(game::CMidServerLogic** thisptr, + int /*%edx*/, + const game::CMidgardID* playerId, + game::List>* movementPath, + const game::CMidgardID* stackId, + const game::CMqPoint* startingPoint, + const game::CMqPoint* endPoint); + +bool __stdcall filterAndProcessEventsNoPlayerHooked(game::IMidgardObjectMap* objectMap, + game::List* eventObjectList, + game::List* effectsList, + bool* stopProcessing, + game::IdList* executedEvents, + const game::CMidgardID* triggererStackId, + const game::CMidgardID* playingStackId); + +bool __stdcall filterAndProcessEventsHooked(game::IMidgardObjectMap* objectMap, + game::List* eventObjectList, + game::List* effectsList, + bool* stopProcessing, + game::IdList* executedEvents, + const game::CMidgardID* playerId, + const game::CMidgardID* triggererStackId, + const game::CMidgardID* playingStackId); + +bool __stdcall checkEventConditionsHooked(const game::IMidgardObjectMap* objectMap, + game::List* effectsList, + const game::CMidgardID* playerId, + const game::CMidgardID* stackTriggererId, + int samePlayer, + const game::CMidgardID* eventId); + +void __stdcall executeEventEffectsHooked(game::IMidgardObjectMap* objectMap, + game::List* effectsList, + bool* stopProcessing, + const game::CMidgardID* eventId, + const game::CMidgardID* playerId, + const game::CMidgardID* stackTriggererId, + const game::CMidgardID* playingStackId); + +bool __fastcall testFreqHooked(const game::ITestCondition* thisptr, + int /*%edx*/, + const game::IMidgardObjectMap* objectMap, + const game::CMidgardID* playerId, + const game::CMidgardID* eventId); + +bool __fastcall testLocationHooked(const game::ITestCondition* thisptr, + int /*%edx*/, + const game::IMidgardObjectMap* objectMap, + const game::CMidgardID* playerId, + const game::CMidgardID* eventId); + +bool __fastcall testEnterCityHooked(const game::ITestCondition* thisptr, + int /*%edx*/, + const game::IMidgardObjectMap* objectMap, + const game::CMidgardID* playerId, + const game::CMidgardID* eventId); + +bool __fastcall testLeaderToCityHooked(const game::ITestCondition* thisptr, + int /*%edx*/, + const game::IMidgardObjectMap* objectMap, + const game::CMidgardID* playerId, + const game::CMidgardID* eventId); + +bool __fastcall testOwnCityHooked(const game::ITestCondition* thisptr, + int /*%edx*/, + const game::IMidgardObjectMap* objectMap, + const game::CMidgardID* playerId, + const game::CMidgardID* eventId); + +bool __fastcall testDiplomacyHooked(const game::ITestCondition* thisptr, + int /*%edx*/, + const game::IMidgardObjectMap* objectMap, + const game::CMidgardID* playerId, + const game::CMidgardID* eventId); + +bool __fastcall testAllianceHooked(const game::ITestCondition* thisptr, + int /*%edx*/, + const game::IMidgardObjectMap* objectMap, + const game::CMidgardID* playerId, + const game::CMidgardID* eventId); + +bool __fastcall testLootRuinHooked(const game::ITestCondition* thisptr, + int /*%edx*/, + const game::IMidgardObjectMap* objectMap, + const game::CMidgardID* playerId, + const game::CMidgardID* eventId); + +bool __fastcall testTransformLandHooked(const game::ITestCondition* thisptr, + int /*%edx*/, + const game::IMidgardObjectMap* objectMap, + const game::CMidgardID* playerId, + const game::CMidgardID* eventId); + +bool __fastcall testVisitSiteHooked(const game::ITestCondition* thisptr, + int /*%edx*/, + const game::IMidgardObjectMap* objectMap, + const game::CMidgardID* playerId, + const game::CMidgardID* eventId); + +bool __fastcall testItemToLocationHooked(const game::ITestCondition* thisptr, + int /*%edx*/, + const game::IMidgardObjectMap* objectMap, + const game::CMidgardID* playerId, + const game::CMidgardID* eventId); + +bool __fastcall testVarInRangeHooked(const game::ITestCondition* thisptr, + int /*%edx*/, + const game::IMidgardObjectMap* objectMap, + const game::CMidgardID* playerId, + const game::CMidgardID* eventId); + +game::CMidServerLogic* __fastcall midServerLogicCtorHooked(game::CMidServerLogic* thisptr, + int /*%edx*/, + game::CMidServer* server, + bool multiplayerGame, + bool hotseatGame, + int a5, + int gameVersion); + } // namespace hooks #endif // MIDSERVERLOGICHOOKS_H diff --git a/mss32/include/midsite.h b/mss32/include/midsite.h index c372b736..e0ae76fd 100644 --- a/mss32/include/midsite.h +++ b/mss32/include/midsite.h @@ -31,6 +31,9 @@ namespace game { +struct IMidgardObjectMap; +struct CampaignStream; + /** Base class for site objects. */ struct CMidSite : public IMidScenarioObject { @@ -50,6 +53,42 @@ assert_size(CMidSite, 120); assert_offset(CMidSite, mapElement, 8); assert_offset(CMidSite, title, 52); +struct CMidSiteVftable : public IMidScenarioObjectVftable +{ + using GetEntrancePoint = CMqPoint*(__thiscall*)(const CMidSite* thisptr, CMqPoint* entrance); + GetEntrancePoint getEntrancePoint; + + using StreamSiteData = void(__thiscall*)(CMidSite* thisptr, + CampaignStream* stream, + const CMidgardID* siteId); + StreamSiteData streamSiteData; +}; + +assert_vftable_size(CMidSiteVftable, 6); + +namespace CMidSiteApi { + +struct Api +{ + using Constructor = CMidSite*(__thiscall*)(CMidSite* thisptr, + const CMidgardID* siteId, + const LSiteCategory* siteCategory); + Constructor constructor; + + using SetData = bool(__thiscall*)(CMidSite* thisptr, + IMidgardObjectMap* objectMap, + int imgIso, + const char* imgIntf, + const CMqPoint* position, + const char* title, + const char* description); + SetData setData; +}; + +Api& get(); + +} // namespace CMidSiteApi + } // namespace game #endif // MIDSITE_H diff --git a/mss32/include/midsitemage.h b/mss32/include/midsitemage.h new file mode 100644 index 00000000..12925502 --- /dev/null +++ b/mss32/include/midsitemage.h @@ -0,0 +1,50 @@ +/* + * This file is part of the modding toolset for Disciples 2. + * (https://github.com/VladimirMakeev/D2ModdingToolset) + * Copyright (C) 2024 Vladimir Makeev. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef MIDSITEMAGE_H +#define MIDSITEMAGE_H + +#include "idlist.h" +#include "midsite.h" + +namespace game { + +/** Holds mage tower related data in scenario file and game. */ +struct CMidSiteMage : public CMidSite +{ + IdList spells; +}; + +assert_size(CMidSiteMage, 136); + +namespace CMidSiteMageApi { + +struct Api +{ + using Constructor = CMidSiteMage*(__thiscall*)(CMidSiteMage* thisptr, const CMidgardID* siteId); + Constructor constructor; +}; + +Api& get(); + +} // namespace CMidSiteMageApi + +} // namespace game + +#endif // MIDSITEMAGE_H diff --git a/mss32/include/midsitemerchant.h b/mss32/include/midsitemerchant.h new file mode 100644 index 00000000..c6c1705d --- /dev/null +++ b/mss32/include/midsitemerchant.h @@ -0,0 +1,58 @@ +/* + * This file is part of the modding toolset for Disciples 2. + * (https://github.com/VladimirMakeev/D2ModdingToolset) + * Copyright (C) 2024 Vladimir Makeev. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef MIDSITEMERCHANT_H +#define MIDSITEMERCHANT_H + +#include "d2list.h" +#include "d2pair.h" +#include "itemcategory.h" +#include "midsite.h" + +namespace game { + +using ItemList = List>; + +/** Holds merchant related data in scenario file and game. */ +struct CMidSiteMerchant : public CMidSite +{ + ItemList items; + bool mission; + char padding[3]; + Set canBuyItemCategories; +}; + +assert_size(CMidSiteMerchant, 168); + +namespace CMidSiteMerchantApi { + +struct Api +{ + using Constructor = CMidSiteMerchant*(__thiscall*)(CMidSiteMerchant* thisptr, + const CMidgardID* siteId); + Constructor constructor; +}; + +Api& get(); + +} // namespace CMidSiteMerchantApi + +} // namespace game + +#endif // MIDSITEMERCHANT_H diff --git a/mss32/include/midsitemercs.h b/mss32/include/midsitemercs.h new file mode 100644 index 00000000..ac54125e --- /dev/null +++ b/mss32/include/midsitemercs.h @@ -0,0 +1,61 @@ +/* + * This file is part of the modding toolset for Disciples 2. + * (https://github.com/VladimirMakeev/D2ModdingToolset) + * Copyright (C) 2024 Vladimir Makeev. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef MIDSITEMERCS_H +#define MIDSITEMERCS_H + +#include "d2list.h" +#include "midsite.h" + +namespace game { + +struct MercenaryUnit +{ + CMidgardID unitId; + bool unique; + char padding[3]; +}; + +assert_size(MercenaryUnit, 8); + +/** Holds mercenary camp related data in scenario file and game. */ +struct CMidSiteMercs : public CMidSite +{ + List units; + int unknown; +}; + +assert_size(CMidSiteMercs, 140); + +namespace CMidSiteMercsApi { + +struct Api +{ + using Constructor = CMidSiteMercs*(__thiscall*)(CMidSiteMercs* thisptr, + const CMidgardID* siteId); + Constructor constructor; +}; + +Api& get(); + +} // namespace CMidSiteMercsApi + +} // namespace game + +#endif // MIDSITEMERCS_H diff --git a/mss32/include/midsiteresourcemarket.h b/mss32/include/midsiteresourcemarket.h new file mode 100644 index 00000000..25846730 --- /dev/null +++ b/mss32/include/midsiteresourcemarket.h @@ -0,0 +1,106 @@ +/* + * This file is part of the modding toolset for Disciples 2. + * (https://github.com/VladimirMakeev/D2ModdingToolset) + * Copyright (C) 2024 Vladimir Makeev. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef MIDSITERESOURCEMARKET_H +#define MIDSITERESOURCEMARKET_H + +#include "currency.h" +#include "midsite.h" +#include +#include + +namespace game { +struct alignas(8) TypeDescriptor; +struct IMidgardObjectMap; +} // namespace game + +namespace hooks { + +struct ExchangeRates +{ + game::CurrencyType resource2; /**< Resource to buy. */ + int amount1; /**< Amount of resource1 needed to get amount2 of resource2. */ + int amount2; /**< Amount of resource2 needed to get amount1 of resource1. */ +}; + +struct ResourceExchange +{ + game::CurrencyType resource1; /**< Resource to sell. */ + std::vector rates; /**< Resources to buy and exchange rates. */ +}; + +using MarketExchangeRates = std::vector; + +union InfiniteStock +{ + struct + { + bool lifeMana : 1; + bool infernalMana : 1; + bool runicMana : 1; + bool deathMana : 1; + bool groveMana : 1; + bool gold : 1; + } parts; + + std::uint8_t value; +}; + +assert_size(InfiniteStock, 1); + +struct CMidSiteResourceMarket : public game::CMidSite +{ + /** User defined exchange rates script, if set. */ + std::string exchangeRatesScript; + /** Market resources, if finite. */ + game::Bank stock; + /** Specifies whether certain resource types are infinite or not. */ + InfiniteStock infiniteStock; + /** If true, resource market uses custom exchange rates script. */ + bool customExchangeRates; +}; + +game::TypeDescriptor* getResourceMarketTypeDescriptor(); + +CMidSiteResourceMarket* createResourceMarket(const game::CMidgardID* siteId); + +void addResourceMarketStreamRegister(); + +bool isMarketStockInfinite(const InfiniteStock& stock, game::CurrencyType currency); + +bool getExchangeRates(const game::IMidgardObjectMap* objectMap, + const game::CMidgardID& marketId, + const game::CMidgardID& visitorStackId, + MarketExchangeRates& exchangeRates, + bool serverSide = false); + +const ExchangeRates* findExchangeRates(const MarketExchangeRates& marketRates, + game::CurrencyType playerCurrency, + game::CurrencyType marketCurrency); + +bool exchangeResources(game::IMidgardObjectMap* objectMap, + const game::CMidgardID& marketId, + const game::CMidgardID& visitorStackId, + game::CurrencyType playerCurrency, + game::CurrencyType marketCurrency, + std::uint16_t amount); + +} // namespace hooks + +#endif // MIDSITERESOURCEMARKET_H diff --git a/mss32/include/midsitetrainer.h b/mss32/include/midsitetrainer.h new file mode 100644 index 00000000..2a227639 --- /dev/null +++ b/mss32/include/midsitetrainer.h @@ -0,0 +1,48 @@ +/* + * This file is part of the modding toolset for Disciples 2. + * (https://github.com/VladimirMakeev/D2ModdingToolset) + * Copyright (C) 2024 Vladimir Makeev. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef MIDSITETRAINER_H +#define MIDSITETRAINER_H + +#include "midsite.h" + +namespace game { + +/** Represents training camp in scenario file and game. */ +struct CMidSiteTrainer : public CMidSite +{ }; + +assert_size(CMidSiteTrainer, 120); + +namespace CMidSiteTrainerApi { + +struct Api +{ + using Constructor = CMidSiteTrainer*(__thiscall*)(CMidSiteTrainer* thisptr, + const CMidgardID* siteId); + Constructor constructor; +}; + +Api& get(); + +} // namespace CMidSiteTrainerApi + +} // namespace game + +#endif // MIDSITETRAINER_H diff --git a/mss32/include/midstack.h b/mss32/include/midstack.h index 8daa4478..cb0b5bb0 100644 --- a/mss32/include/midstack.h +++ b/mss32/include/midstack.h @@ -94,6 +94,26 @@ assert_offset(CMidStack, inventory, 100); assert_offset(CMidStack, leaderEquippedItems, 124); assert_offset(CMidStack, orderTargetId, 172); +struct CMidStackIMapElementVftable : public IMapElementVftable +{ + using Initialize = bool(__thiscall*)(IMapElement* thisptr, + const IMidgardObjectMap* objectMap, + const CMidgardID* leaderId, + const CMidgardID* ownerId, + const CMidgardID* subraceId, + const CMqPoint* position); + Initialize initialize; + + /** + * Removes all units from stack, destroys all its items and sets leader, + * owner and subrace ids as empty. + */ + using Cleanup = bool(__thiscall*)(IMapElement* thisptr, const IMidgardObjectMap* objectMap); + Cleanup cleanup; +}; + +assert_vftable_size(CMidStackIMapElementVftable, 3); + namespace CMidStackApi { struct Api @@ -113,7 +133,7 @@ struct Api Api& get(); -const IMapElementVftable* vftable(); +const CMidStackIMapElementVftable* vftable(); } // namespace CMidStackApi diff --git a/mss32/include/midstackdestroyed.h b/mss32/include/midstackdestroyed.h new file mode 100644 index 00000000..e836d937 --- /dev/null +++ b/mss32/include/midstackdestroyed.h @@ -0,0 +1,46 @@ +/* + * This file is part of the modding toolset for Disciples 2. + * (https://github.com/VladimirMakeev/D2ModdingToolset) + * Copyright (C) 2024 Vladimir Makeev. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef MIDSTACKDESTROYED_H +#define MIDSTACKDESTROYED_H + +#include "d2list.h" +#include "midscenarioobject.h" + +namespace game { + +struct MidStackDestroyedEntry +{ + CMidgardID stackId; + CMidgardID killerId; + CMidgardID stackSrcTemplateId; +}; + +assert_size(MidStackDestroyedEntry, 12); + +struct CMidStackDestroyed : public IMidScenarioObject +{ + List destroyedStacks; +}; + +assert_size(CMidStackDestroyed, 24); + +} // namespace game + +#endif // MIDSTACKDESTROYED_H diff --git a/mss32/include/midtaskopeninterfparam.h b/mss32/include/midtaskopeninterfparam.h new file mode 100644 index 00000000..42f8466c --- /dev/null +++ b/mss32/include/midtaskopeninterfparam.h @@ -0,0 +1,37 @@ +/* + * This file is part of the modding toolset for Disciples 2. + * (https://github.com/VladimirMakeev/D2ModdingToolset) + * Copyright (C) 2024 Vladimir Makeev. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef MIDTASKOPENINTERFPARAM_H +#define MIDTASKOPENINTERFPARAM_H + +#include "midfreetask.h" + +namespace game { + +template +struct CMidTaskOpenInterfParam : public CMidFreeTask +{ + T* interf; +}; + +assert_size(CMidTaskOpenInterfParam, 20); + +} // namespace game + +#endif // MIDTASKOPENINTERFPARAM_H diff --git a/mss32/include/midtaskopeninterfparamresmarket.h b/mss32/include/midtaskopeninterfparamresmarket.h new file mode 100644 index 00000000..8a60dcd1 --- /dev/null +++ b/mss32/include/midtaskopeninterfparamresmarket.h @@ -0,0 +1,39 @@ +/* + * This file is part of the modding toolset for Disciples 2. + * (https://github.com/VladimirMakeev/D2ModdingToolset) + * Copyright (C) 2024 Vladimir Makeev. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef MIDTASKOPENINTERFPARAMRESMARKET_H +#define MIDTASKOPENINTERFPARAMRESMARKET_H + +namespace game { +struct ITask; +struct CTaskManager; +struct CPhaseGame; +struct CMidgardID; +} // namespace game + +namespace hooks { + +/** Creates task that will show resource market interface when visited in game. */ +game::ITask* createMidTaskOpenInterfParamResMarket(game::CTaskManager* taskManager, + game::CPhaseGame* phaseGame, + const game::CMidgardID& visitorStackId, + const game::CMidgardID& siteId); +} // namespace hooks + +#endif // MIDTASKOPENINTERFPARAMRESMARKET_H diff --git a/mss32/include/midunitgroupadapter.h b/mss32/include/midunitgroupadapter.h index 61fc39ca..d6352bb8 100644 --- a/mss32/include/midunitgroupadapter.h +++ b/mss32/include/midunitgroupadapter.h @@ -65,6 +65,22 @@ struct CMidUnitGroupAdapter : public IUnitGroup assert_size(CMidUnitGroupAdapter, 8); +namespace CMidUnitGroupAdapterApi { + +struct Api +{ + using Constructor = CMidUnitGroupAdapter*(__thiscall*)(CMidUnitGroupAdapter* thisptr, + IMidgardObjectMap* objectMap, + const CMidgardID* groupId, + const CMidgardID* playerId, + int leftSide); + Constructor constructor; +}; + +Api& get(); + +} + } // namespace game #endif // MIDUNITGROUPADAPTER_H diff --git a/mss32/include/mqstream.h b/mss32/include/mqstream.h index 0a46b9b4..26eb8d65 100644 --- a/mss32/include/mqstream.h +++ b/mss32/include/mqstream.h @@ -21,6 +21,7 @@ #define MQSTREAM_H #include "d2assert.h" +#include namespace game { @@ -43,7 +44,14 @@ struct CMqStreamVftable using Serialize = void(__thiscall*)(CMqStream* thisptr, const void* data, int count); Serialize serialize; - void* methods[3]; + using Method2 = int(__thiscall*)(CMqStream* thisptr); + Method2 method2; + + using GetNumBytes = std::uint32_t(__thiscall*)(CMqStream* thisptr); + GetNumBytes getNumBytes; + + using GetBuffer = void*(__thiscall*)(CMqStream* thisptr); + GetBuffer getBuffer; }; assert_vftable_size(CMqStreamVftable, 5); diff --git a/mss32/include/mquikernel.h b/mss32/include/mquikernel.h index 98e5fe07..d47bcd36 100644 --- a/mss32/include/mquikernel.h +++ b/mss32/include/mquikernel.h @@ -21,7 +21,6 @@ #define MQUIKERNEL_H #include "d2assert.h" -#define WIN32_LEAN_AND_MEAN #include #include diff --git a/mss32/include/nativegameinfo.h b/mss32/include/nativegameinfo.h index 0d32ac65..eba6ca65 100644 --- a/mss32/include/nativegameinfo.h +++ b/mss32/include/nativegameinfo.h @@ -66,6 +66,7 @@ class NativeGameInfo final : public rsg::GameInfo const rsg::SiteTexts& getMerchantTexts() const override; const rsg::SiteTexts& getRuinTexts() const override; const rsg::SiteTexts& getTrainerTexts() const override; + const rsg::SiteTexts& getMarketTexts() const override; private: bool readGameInfo(const std::filesystem::path& gameFolderPath); @@ -115,6 +116,7 @@ class NativeGameInfo final : public rsg::GameInfo rsg::SiteTexts merchantTexts; rsg::SiteTexts ruinTexts; rsg::SiteTexts trainerTexts; + rsg::SiteTexts marketTexts; }; } // namespace hooks diff --git a/mss32/include/netmsg.h b/mss32/include/netmsg.h index c06abf76..6203190a 100644 --- a/mss32/include/netmsg.h +++ b/mss32/include/netmsg.h @@ -88,6 +88,9 @@ struct Api { using Destructor = void(__thiscall*)(CNetMsg* thisptr); Destructor destructor; + + using Serialize = void(__thiscall*)(CNetMsg* thisptr, CMqStream* stream); + Serialize serialize; }; Api& get(); diff --git a/mss32/include/netmsgmapentry.h b/mss32/include/netmsgmapentry.h index f9f8b760..78ea41b4 100644 --- a/mss32/include/netmsgmapentry.h +++ b/mss32/include/netmsgmapentry.h @@ -68,7 +68,9 @@ struct CNetMsgMapEntry_memberVftable; struct CNetMsgMapEntry_member : public CNetMsgMapEntryT { void* data; - bool(__thiscall* callback)(void* thisptr, CNetMsg* netMessage, std::uint32_t idFrom); + + using Callback = bool(__thiscall*)(void* thisptr, CNetMsg* netMessage, std::uint32_t idFrom); + Callback callback; }; assert_size(CNetMsgMapEntry_member, 12); diff --git a/mss32/include/netmsgmapentryexchangeresourcesmsg.h b/mss32/include/netmsgmapentryexchangeresourcesmsg.h new file mode 100644 index 00000000..f75e8744 --- /dev/null +++ b/mss32/include/netmsgmapentryexchangeresourcesmsg.h @@ -0,0 +1,37 @@ +/* + * This file is part of the modding toolset for Disciples 2. + * (https://github.com/VladimirMakeev/D2ModdingToolset) + * Copyright (C) 2024 Vladimir Makeev. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef NETMSGMAPENTRYEXCHANGERESOURCESMSG_H +#define NETMSGMAPENTRYEXCHANGERESOURCESMSG_H + +#include "netmsgmapentry.h" + +namespace game { +struct CMidServerLogic; +} + +namespace hooks { + +game::CNetMsgMapEntry_member* createNetMsgMapEntryExchangeResourcesMsg( + game::CMidServerLogic* serverLogic, + game::CNetMsgMapEntry_member::Callback callback); + +} + +#endif // NETMSGMAPENTRYEXCHANGERESOURCESMSG_H diff --git a/mss32/include/nobleactioncat.h b/mss32/include/nobleactioncat.h new file mode 100644 index 00000000..8e2a97dd --- /dev/null +++ b/mss32/include/nobleactioncat.h @@ -0,0 +1,70 @@ +/* + * This file is part of the modding toolset for Disciples 2. + * (https://github.com/VladimirMakeev/D2ModdingToolset) + * Copyright (C) 2024 Vladimir Makeev. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef NOBLEACTIONCAT_H +#define NOBLEACTIONCAT_H + +#include "categories.h" + +namespace game { + +struct LNobleActionCatTable : public CEnumConstantTable +{ }; + +struct LNobleActionCat : public Category +{ }; + +namespace NobleActionCategories { + +struct Categories +{ + LNobleActionCat* poisonStack; + LNobleActionCat* spy; + LNobleActionCat* stealItem; + LNobleActionCat* assassinate; + LNobleActionCat* misfit; + LNobleActionCat* duel; + LNobleActionCat* poisonCity; + LNobleActionCat* stealSpell; + LNobleActionCat* bribe; + LNobleActionCat* stealGold; + LNobleActionCat* riotCity; + LNobleActionCat* stealMerchant; + LNobleActionCat* stealMage; + LNobleActionCat* spyRuin; +}; + +Categories& get(); + +} // namespace NobleActionCategories + +namespace LNobleActionCatTableApi { + +using Api = CategoryTableApi::Api; + +Api& get(); + +/** Returns address of LNobleActionCatTable::vftable used in game. */ +const void* vftable(); + +} // namespace LNobleActionCatTableApi + +} // namespace game + +#endif // NOBLEACTIONCAT_H diff --git a/mss32/include/nobleactioncategoryset.h b/mss32/include/nobleactioncategoryset.h new file mode 100644 index 00000000..fe770e79 --- /dev/null +++ b/mss32/include/nobleactioncategoryset.h @@ -0,0 +1,50 @@ +/* + * This file is part of the modding toolset for Disciples 2. + * (https://github.com/VladimirMakeev/D2ModdingToolset) + * Copyright (C) 2024 Vladimir Makeev. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef NOBLEACTIONCATEGORYSET_H +#define NOBLEACTIONCATEGORYSET_H + +#include "d2pair.h" +#include "d2set.h" +#include "nobleactioncat.h" + +namespace game { + +using NobleActionCatSet = Set; +using NobleActionCatSetIterator = SetIterator; +using NobleActionCatSetInsertIterator = Pair; + +namespace NobleActionCatSetApi { + +struct Api +{ + using Insert = + NobleActionCatSetInsertIterator*(__thiscall*)(NobleActionCatSet* thisptr, + NobleActionCatSetInsertIterator* result, + const LNobleActionCat* category); + Insert insert; +}; + +Api& get(); + +} // namespace NobleActionCatSetApi + +} // namespace game + +#endif // NOBLEACTIONCATEGORYSET_H diff --git a/mss32/include/nobleactionresult.h b/mss32/include/nobleactionresult.h new file mode 100644 index 00000000..97e9cb58 --- /dev/null +++ b/mss32/include/nobleactionresult.h @@ -0,0 +1,82 @@ +/* + * This file is part of the modding toolset for Disciples 2. + * (https://github.com/VladimirMakeev/D2ModdingToolset) + * Copyright (C) 2024 Vladimir Makeev. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef NOBLEACTIONRESULT_H +#define NOBLEACTIONRESULT_H + +#include "d2list.h" + +namespace game { + +struct INobleActionResultVftable; +struct CMidgardID; +struct IMidgardObjectMap; +struct IEventEffect; +struct LNobleActionCat; + +struct INobleActionResult +{ + INobleActionResultVftable* vftable; +}; + +assert_size(INobleActionResult, 4); + +struct INobleActionResultVftable +{ + using Destructor = void(__thiscall*)(INobleActionResult* thisptr, char flags); + Destructor destructor; + + /** Returns integer value that is specific to action result. */ + using GetValue = int(__thiscall*)(const INobleActionResult* thisptr); + GetValue getValue; + + /** Returns id value that is specific to action result. */ + using GetId = const CMidgardID*(__thiscall*)(const INobleActionResult* thisptr); + GetId getId; + + /** Applies noble action result and returns true on success. */ + using Apply = bool(__thiscall*)(INobleActionResult* thisptr, + IMidgardObjectMap* objectMap, + List* effects, + const CMidgardID* stackId, + const CMidgardID* targetObjectId); + Apply apply; +}; + +assert_vftable_size(INobleActionResultVftable, 4); + +namespace NobleActionsApi { + +struct Api +{ + /** Creates noble action result according to category specified. */ + using Create = INobleActionResult*(__stdcall*)(IMidgardObjectMap* objectMap, + const LNobleActionCat* actionCategory, + const CMidgardID* targetObjectId, + const CMidgardID* id); + Create create; +}; + +Api& get(); + +} // namespace NobleActionsApi + +} // namespace game + +#endif // NOBLEACTIONRESULT_H diff --git a/mss32/include/nobleactionresultstealmarket.h b/mss32/include/nobleactionresultstealmarket.h new file mode 100644 index 00000000..2990518a --- /dev/null +++ b/mss32/include/nobleactionresultstealmarket.h @@ -0,0 +1,36 @@ +/* + * This file is part of the modding toolset for Disciples 2. + * (https://github.com/VladimirMakeev/D2ModdingToolset) + * Copyright (C) 2024 Vladimir Makeev. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef NOBLEACTIONRESULTSTEALMARKET_H +#define NOBLEACTIONRESULTSTEALMARKET_H + +namespace game { +struct INobleActionResult; +struct IMidgardObjectMap; +struct CMidgardID; +} // namespace game + +namespace hooks { + +game::INobleActionResult* createStealMarketActionResult(const game::IMidgardObjectMap* objectMap, + const game::CMidgardID& marketId); + +} // namespace hooks + +#endif // NOBLEACTIONRESULTSTEALMARKET_H diff --git a/mss32/include/notifyplayerlist.h b/mss32/include/notifyplayerlist.h index 18b40709..5e88875d 100644 --- a/mss32/include/notifyplayerlist.h +++ b/mss32/include/notifyplayerlist.h @@ -36,7 +36,8 @@ struct INotifyPlayerListVftable using Destructor = void(__thiscall*)(INotifyPlayerList* thisptr, char flags); Destructor destructor; - void* method1; + using Method1 = void(__thiscall*)(INotifyPlayerList* thisptr, int a2); + Method1 method1; }; assert_vftable_size(INotifyPlayerListVftable, 2); diff --git a/mss32/include/objectinterf.h b/mss32/include/objectinterf.h index 9ecaa71c..7bce2e56 100644 --- a/mss32/include/objectinterf.h +++ b/mss32/include/objectinterf.h @@ -26,6 +26,8 @@ namespace game { namespace editor { +struct CTaskObj; + struct CObjectInterfData { int selectedMode; @@ -41,6 +43,19 @@ struct CObjectInterf : public CIsoView assert_size(CObjectInterf, 24); +namespace CObjectInterfApi { + +struct Api +{ + // thisptr points to ITaskManagerHolder inside CObjectInterf + using CreateTaskObj = CTaskObj*(__thiscall*)(ITaskManagerHolder* thisptr); + CreateTaskObj createTaskObj; +}; + +Api& get(); + +} // namespace CObjectInterfApi + } // namespace editor } // namespace game diff --git a/mss32/include/objectinterfhooks.h b/mss32/include/objectinterfhooks.h new file mode 100644 index 00000000..62ed995c --- /dev/null +++ b/mss32/include/objectinterfhooks.h @@ -0,0 +1,39 @@ +/* + * This file is part of the modding toolset for Disciples 2. + * (https://github.com/VladimirMakeev/D2ModdingToolset) + * Copyright (C) 2024 Vladimir Makeev. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef OBJECTINTERFHOOKS_H +#define OBJECTINTERFHOOKS_H + +namespace game { +struct ITaskManagerHolder; + +namespace editor { +struct CTaskObj; +} + +} // namespace game + +namespace hooks { + +game::editor::CTaskObj* __fastcall createTaskObjHooked(game::ITaskManagerHolder* thisptr, + int /* %edx */); + +} + +#endif // OBJECTINTERFHOOKS_H diff --git a/mss32/include/originalfunctions.h b/mss32/include/originalfunctions.h index a71082ad..3a3ca89c 100644 --- a/mss32/include/originalfunctions.h +++ b/mss32/include/originalfunctions.h @@ -37,6 +37,9 @@ #include "exchangeinterf.h" #include "game.h" #include "gameimages.h" +#include "globalvariables.h" +#include "imagelayerlist.h" +#include "mainview2.h" #include "menubase.h" #include "menuload.h" #include "menunewskirmishhotseat.h" @@ -49,12 +52,20 @@ #include "midevent.h" #include "midgardscenariomap.h" #include "midmsgsender.h" +#include "midserverlogic.h" #include "midunit.h" #include "mqnetplayer.h" #include "netmsg.h" +#include "nobleactionresult.h" +#include "objectinterf.h" #include "pickupdropinterf.h" +#include "scenedit.h" +#include "scenpropinterf.h" #include "sitemerchantinterf.h" +#include "taskobjaddsite.h" +#include "taskobjprop.h" #include "testcondition.h" +#include "visitors.h" namespace hooks { @@ -98,6 +109,7 @@ struct OriginalFunctions game::ITestConditionApi::Api::Create createTestCondition; game::CMidEventApi::Api::CheckValid checkEventValid; game::BattleMsgDataApi::Api::BeforeBattleRound beforeBattleRound; + game::BattleMsgDataApi::Api::AiChooseBattleAction aiChooseBattleAction; game::CMidUnitVftable::InitWithSoldierImpl initWithSoldierImpl; game::CMidEvEffectApi::Api::CreateFromCategory createEventEffectFromCategory; @@ -146,6 +158,51 @@ struct OriginalFunctions game::CMidDataCache2::INotifyVftable::OnObjectChanged cityStackInterfOnObjectChanged; game::CMidDataCache2::INotifyVftable::OnObjectChanged siteMerchantInterfOnObjectChanged; + + game::editor::CScenPropInterfApi::Api::Constructor scenPropInterfCtor; + + game::CMidServerLogicApi::Api::ApplyEventEffectsAndCheckMidEventTriggerers + applyEventEffectsAndCheckMidEventTriggerers; + game::CMidServerLogicApi::Api::StackMove stackMove; + game::CMidServerLogicApi::Api::FilterAndProcessEventsNoPlayer filterAndProcessEventsNoPlayer; + game::CMidServerLogicApi::Api::CheckAndExecuteEvent checkAndExecuteEvent; + game::CMidServerLogicApi::Api::FilterAndProcessEvents filterAndProcessEvents; + game::CMidServerLogicApi::Api::CheckEventConditions checkEventConditions; + game::CMidServerLogicApi::Api::ExecuteEventEffects executeEventEffects; + + game::ITestConditionVftable::Test testFrequency; + game::ITestConditionVftable::Test testLocation; + game::ITestConditionVftable::Test testEnterCity; + game::ITestConditionVftable::Test testLeaderToCity; + game::ITestConditionVftable::Test testOwnCity; + game::ITestConditionVftable::Test testDiplomacy; + game::ITestConditionVftable::Test testAlliance; + game::ITestConditionVftable::Test testLootRuin; + game::ITestConditionVftable::Test testTransformLand; + game::ITestConditionVftable::Test testVisitSite; + game::ITestConditionVftable::Test testItemToLocation; + game::ITestConditionVftable::Test testVarInRange; + + game::RemoveStack removeStack; + game::VisitorApi::Api::SetStackSrcTemplate setStackSrcTemplate; + + game::editor::CObjectInterfApi::Api::CreateTaskObj createTaskObj; + + game::ImageLayerListApi::Api::GetMapElementIsoLayerImages getMapElementIsoLayerImages; + game::editor::CTaskObjVftable::DoAction taskObjPropDoAction; + game::editor::CTaskObjVftable::DoAction taskObjAddSiteDoAction; + game::CScenEditApi::Api::ReadScenData readScenData; + + game::CMainView2Api::Api::HandleCmdStackVisitMsg handleCmdStackVisitMsg; + + game::CMidServerLogicApi::Api::Constructor midServerLogicCtor; + + game::NobleActionsApi::Api::Create createNobleActionResult; + game::GetNobleActions getSiteNobleActions; + game::GetNobleActions getPossibleNobleActions; + game::GetNobleActionResultDescription getNobleActionResultDescription; + + game::GlobalVariablesApi::Api::Constructor globalVariablesCtor; }; OriginalFunctions& getOriginalFunctions(); diff --git a/mss32/include/paperdollchildinterf.h b/mss32/include/paperdollchildinterf.h new file mode 100644 index 00000000..dd6ddd00 --- /dev/null +++ b/mss32/include/paperdollchildinterf.h @@ -0,0 +1,64 @@ +/* + * This file is part of the modding toolset for Disciples 2. + * (https://github.com/VladimirMakeev/D2ModdingToolset) + * Copyright (C) 2024 Vladimir Makeev. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef PAPERDOLLCHILDINTERF_H +#define PAPERDOLLCHILDINTERF_H + +#include "interface.h" +#include "midgardid.h" + +namespace game { + +struct CDialogInterf; +struct CMidDragDropInterf; +struct CDDEquipmentGroup; +struct CPhaseGame; + +struct CPaperdollChildInterf : public CInterface +{ + CDialogInterf* paperdollDialog; + CMidDragDropInterf* dragDropInterface; + Vector leaderItemAreas; + CDDEquipmentGroup* equipmentGroup; + CMidgardID stackId; + CPhaseGame* phaseGame; +}; + +assert_size(CPaperdollChildInterf, 44); + +namespace CPaperdollChildInterfApi { + +struct Api +{ + using Constructor = CPaperdollChildInterf*(__thiscall*)(CPaperdollChildInterf* thisptr, + CMidDragDropInterf* dragDrop, + CPhaseGame* phaseGame, + const CMidgardID* stackId, + CInterface* parentInterface, + const CMqRect* paperdollArea); + Constructor constructor; +}; + +Api& get(); + +} // namespace CPaperdollChildInterfApi + +} // namespace game + +#endif // PAPERDOLLCHILDINTERF_H diff --git a/mss32/include/phase.h b/mss32/include/phase.h index bbb29ded..ae81f80c 100644 --- a/mss32/include/phase.h +++ b/mss32/include/phase.h @@ -25,9 +25,11 @@ namespace game { struct CMidClient; -struct IMidgardObjectMap; +struct CMidDataCache2; struct CMidgardID; struct CInterface; +struct CMidCommandQueue2; +struct CEncParamBase; struct CPhaseData { @@ -49,11 +51,17 @@ namespace CPhaseApi { struct Api { - using GetObjectMap = IMidgardObjectMap*(__thiscall*)(CPhase* thisptr); - GetObjectMap getObjectMap; + using GetObjectMap = CMidDataCache2*(__thiscall*)(CPhase* thisptr); + GetObjectMap getDataCache; using GetCurrentPlayerId = const CMidgardID*(__thiscall*)(CPhase* thisptr); GetCurrentPlayerId getCurrentPlayerId; + + using GetCommandQueue = CMidCommandQueue2*(__thiscall*)(CPhase* thisptr); + GetCommandQueue getCommandQueue; + + using ShowEncyclopediaPopup = void(__thiscall*)(CPhase* thisptr, const CEncParamBase* encParam); + ShowEncyclopediaPopup showEncyclopediaPopup; }; Api& get(); diff --git a/mss32/include/phasegame.h b/mss32/include/phasegame.h index 016ced26..af8b5cae 100644 --- a/mss32/include/phasegame.h +++ b/mss32/include/phasegame.h @@ -45,7 +45,8 @@ struct CPhaseGameData SmartPointer palMapIsoScroller; IIsoCBScroll* audioRegionCtrl; CMidClient* midClient; - int unknown8; + bool clientTakesTurn; + char padding[3]; CMidObjectNotify* midObjectNotify; CMidAnim2System* animSystem; void* listPtr; diff --git a/mss32/include/pictureinterf.h b/mss32/include/pictureinterf.h index a86c8736..c3cd0ebf 100644 --- a/mss32/include/pictureinterf.h +++ b/mss32/include/pictureinterf.h @@ -63,6 +63,11 @@ struct Api const CMqPoint* offset); SetImage setImage; + using SetImageWithAnchor = void(__thiscall*)(CPictureInterf* thisptr, + IMqImage2* image, + char anchor); + SetImageWithAnchor setImageWithAnchor; + /** Assigns mouse button press functor. */ using AssignFunctor = void(__thiscall*)(CPictureInterf* thisptr, SmartPointer* functor); AssignFunctor assignFunctor; diff --git a/mss32/include/raceset.h b/mss32/include/raceset.h index 6bc40bb8..7b4fcf60 100644 --- a/mss32/include/raceset.h +++ b/mss32/include/raceset.h @@ -43,6 +43,9 @@ struct Api RaceSetIterator* iterator, LRaceCategory* raceCategory); Add add; + + using Find = SetIterator* (__thiscall*)(const RaceSet* thisptr, SetIterator* iterator, const LRaceCategory* raceCategory); + Find find; }; Api& get(); diff --git a/mss32/include/resetstackext.h b/mss32/include/resetstackext.h index 3bef0f7d..3c0e6c25 100644 --- a/mss32/include/resetstackext.h +++ b/mss32/include/resetstackext.h @@ -20,6 +20,8 @@ #ifndef RESETSTACKEXT_H #define RESETSTACKEXT_H +#include "idvector.h" + namespace game { struct IResetStackExtVftable; @@ -34,7 +36,23 @@ struct IResetStackExtVftable using Destructor = void(__thiscall*)(IResetStackExt* thisptr, bool freeMemory); Destructor destructor; - void* methods[4]; + using HireLeader = bool( + __thiscall*)(IResetStackExt* thisptr, const CMidgardID* unitImplId, int a3, int a4, int a5); + HireLeader hireLeader; + + void* methods[2]; + + /** + * Returns ids of units or leaders that can be hired. + * @param[in] thisptr object pointer. + * @param[inout] unitImplIds vector where ids will be stored. + * @param leaders - if set to 1, leader ids will be returned. + * Any other value results in return of unit ids. Game logic uses 2 for units here. + */ + using GetUnitIdsForHire = void(__thiscall*)(IResetStackExt* thisptr, + IdVector* unitImplIds, + int leaders); + GetUnitIdsForHire getUnitIdsForHire; using GetStackId = CMidgardID*(__thiscall*)(IResetStackExt* thisptr, CMidgardID* value); GetStackId getStackId; diff --git a/mss32/include/resourcemarketinterface.h b/mss32/include/resourcemarketinterface.h new file mode 100644 index 00000000..bd3bab59 --- /dev/null +++ b/mss32/include/resourcemarketinterface.h @@ -0,0 +1,40 @@ +/* + * This file is part of the modding toolset for Disciples 2. + * (https://github.com/VladimirMakeev/D2ModdingToolset) + * Copyright (C) 2024 Vladimir Makeev. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef RESOURCEMARKETINTERFACE_H +#define RESOURCEMARKETINTERFACE_H + +namespace game { +struct CInterface; + +namespace editor { +struct CTaskObjProp; +} +} // namespace game + +namespace hooks { +struct CMidSiteResourceMarket; + +/** Create resource market interface to show in Scenario Editor. */ +game::CInterface* createResourceMarketInterface(game::editor::CTaskObjProp* task, + CMidSiteResourceMarket* market); + +} // namespace hooks + +#endif // RESOURCEMARKETINTERFACE_H diff --git a/mss32/include/resourcetype.h b/mss32/include/resourcetype.h new file mode 100644 index 00000000..a1479a51 --- /dev/null +++ b/mss32/include/resourcetype.h @@ -0,0 +1,51 @@ +/* + * This file is part of the modding toolset for Disciples 2. + * (https://github.com/VladimirMakeev/D2ModdingToolset) + * Copyright (C) 2024 Vladimir Makeev. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef RESOURCETYPE_H +#define RESOURCETYPE_H + +#include "categories.h" + +namespace game { + +struct LResourceTypeTable : public CEnumConstantTable +{ }; + +struct LResourceType : public Category +{ }; + +namespace ResourceTypes { + +struct Categories +{ + LResourceType* gold; + LResourceType* infernalMana; + LResourceType* lifeMana; + LResourceType* deathMana; + LResourceType* runicMana; + LResourceType* groveMana; +}; + +Categories& get(); + +} // namespace ResourceTypes + +} // namespace game + +#endif // RESOURCETYPE_H diff --git a/mss32/include/scenedit.h b/mss32/include/scenedit.h index 11d2de98..0e1fa8ed 100644 --- a/mss32/include/scenedit.h +++ b/mss32/include/scenedit.h @@ -94,6 +94,10 @@ struct Api { using Instance = CScenEdit*(__cdecl*)(); Instance instance; + + /** Reads contents of dbf files from ScenData folder. */ + using ReadScenData = bool(__thiscall*)(CScenEdit* thisptr); + ReadScenData readScenData; }; Api& get(); diff --git a/mss32/include/scenedithooks.h b/mss32/include/scenedithooks.h new file mode 100644 index 00000000..d11325f2 --- /dev/null +++ b/mss32/include/scenedithooks.h @@ -0,0 +1,42 @@ +/* + * This file is part of the modding toolset for Disciples 2. + * (https://github.com/VladimirMakeev/D2ModdingToolset) + * Copyright (C) 2024 Vladimir Makeev. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef SCENEDITHOOKS_H +#define SCENEDITHOOKS_H + +#include +#include +#include + +namespace game { +struct CScenEdit; +} + +namespace hooks { + +using NameDescPair = std::pair; +using MarketNames = std::vector; + +bool __fastcall readScenDataHooked(game::CScenEdit* thisptr, int /*%edx*/); + +const MarketNames& getMarketNames(); + +} + +#endif // SCENEDITHOOKS_H diff --git a/mss32/include/scenpropinterf.h b/mss32/include/scenpropinterf.h new file mode 100644 index 00000000..7dc2ea10 --- /dev/null +++ b/mss32/include/scenpropinterf.h @@ -0,0 +1,76 @@ +/* + * This file is part of the modding toolset for Disciples 2. + * (https://github.com/VladimirMakeev/D2ModdingToolset) + * Copyright (C) 2024 Vladimir Makeev. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef SCENPROPINTERF_H +#define SCENPROPINTERF_H + +#include "popupinterf.h" + +namespace game { +struct CSpinButtonInterf; + +namespace editor { + +struct CScenPropInterfData +{ + char unknown[156]; +}; + +/** Represents DLG_PROP from ScenEdit.dlg. */ +struct CScenPropInterf : public CPopupInterf +{ + CScenPropInterfData* data; +}; + +assert_size(CScenPropInterf, 28); + +namespace CScenPropInterfApi { + +struct Api +{ + using Constructor = CScenPropInterf*(__thiscall*)(CScenPropInterf* thisptr, + ITask* task, + char* a3); + Constructor constructor; + + struct SpinButtonCallback + { + using Callback = void(__thiscall*)(void* thisptr, CSpinButtonInterf* spinButton); + + Callback callback; + int unknown; + }; + + /** Reuse function from CCapitalInterf. */ + using CreateSpinButtonFunctor = SmartPointer*(__stdcall*)(SmartPointer* functor, + int dummy, + void* interf, + SpinButtonCallback* callback); + CreateSpinButtonFunctor createSpinButtonFunctor; +}; + +Api& get(); + +} // namespace CScenPropInterfApi + +} // namespace editor + +} // namespace game + +#endif // SCENPROPINTERF_H diff --git a/mss32/include/scenpropinterfhooks.h b/mss32/include/scenpropinterfhooks.h new file mode 100644 index 00000000..4261b27f --- /dev/null +++ b/mss32/include/scenpropinterfhooks.h @@ -0,0 +1,41 @@ +/* + * This file is part of the modding toolset for Disciples 2. + * (https://github.com/VladimirMakeev/D2ModdingToolset) + * Copyright (C) 2024 Vladimir Makeev. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef SCENPROPINTERFHOOKS_H +#define SCENPROPINTERFHOOKS_H + +namespace game { +struct ITask; + +namespace editor { +struct CScenPropInterf; +} + +} // namespace game + +namespace hooks { + +game::editor::CScenPropInterf* __fastcall scenPropInterfCtorHooked( + game::editor::CScenPropInterf* thisptr, + int /*%edx*/, + game::ITask* task, + char* a3); +} + +#endif // SCENPROPINTERFHOOKS_H diff --git a/mss32/include/settings.h b/mss32/include/settings.h index a3e0e881..7a0e00c0 100644 --- a/mss32/include/settings.h +++ b/mss32/include/settings.h @@ -20,10 +20,14 @@ #ifndef SETTINGS_H #define SETTINGS_H +#include #include #include #include -#include + +namespace game { +enum class BattleAction : int; +} namespace hooks { @@ -78,7 +82,7 @@ struct Settings bool freeTransformSelfAttack; bool freeTransformSelfAttackInfinite; bool fixEffectiveHpFormula; - + struct AdditionalLordIncome { int warrior = 0; @@ -172,6 +176,8 @@ struct Settings struct Battle { + game::BattleAction fallbackAction; + bool debugAi{false}; bool allowRetreatedUnitsToUpgrade{false}; bool carryXpOverUpgrade{false}; bool allowMultiUpgrade{false}; diff --git a/mss32/include/sitecategoryhooks.h b/mss32/include/sitecategoryhooks.h new file mode 100644 index 00000000..f166a9a2 --- /dev/null +++ b/mss32/include/sitecategoryhooks.h @@ -0,0 +1,44 @@ +/* + * This file is part of the modding toolset for Disciples 2. + * (https://github.com/VladimirMakeev/D2ModdingToolset) + * Copyright (C) 2024 Vladimir Makeev. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef SITECATEGORYHOOKS_H +#define SITECATEGORYHOOKS_H + +#include "sitecategories.h" +#include + +namespace hooks { + +struct CustomSiteCategories +{ + std::filesystem::path exchangeRatesScript; + game::LSiteCategory resourceMarket; + bool exists{}; +}; + +CustomSiteCategories& customSiteCategories(); + +game::LSiteCategoryTable* __fastcall siteCategoryTableCtorHooked(game::LSiteCategoryTable* thisptr, + int /*%edx*/, + const char* globalsFolderPath, + void* codeBaseEnvProxy); + +} // namespace hooks + +#endif // SITECATEGORYHOOKS_H diff --git a/mss32/include/sitemerchantinterf.h b/mss32/include/sitemerchantinterf.h index 72c3a710..18832267 100644 --- a/mss32/include/sitemerchantinterf.h +++ b/mss32/include/sitemerchantinterf.h @@ -50,6 +50,7 @@ struct CSiteMerchantInterf : public CMidDataCache2::INotify }; assert_size(CSiteMerchantInterf, 40); +assert_offset(CSiteMerchantInterf, data, 36); namespace CSiteMerchantInterfApi { diff --git a/mss32/include/siteresourcemarketinterf.h b/mss32/include/siteresourcemarketinterf.h new file mode 100644 index 00000000..e9b6b335 --- /dev/null +++ b/mss32/include/siteresourcemarketinterf.h @@ -0,0 +1,40 @@ +/* + * This file is part of the modding toolset for Disciples 2. + * (https://github.com/VladimirMakeev/D2ModdingToolset) + * Copyright (C) 2024 Vladimir Makeev. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef SITERESOURCEMARKETINTERF_H +#define SITERESOURCEMARKETINTERF_H + +namespace game { +struct CInterface; +struct ITask; +struct CPhaseGame; +struct CMidgardID; +} // namespace game + +namespace hooks { + +/** Create resource market interface to show in game. */ +game::CInterface* createSiteResourceMarketInterf(game::ITask* task, + game::CPhaseGame* phaseGame, + const game::CMidgardID& visitorStackId, + const game::CMidgardID& marketId); + +} // namespace hooks + +#endif // SITERESOURCEMARKETINTERF_H diff --git a/mss32/include/sounds.h b/mss32/include/sounds.h index 34ec36b4..fc9f47c8 100644 --- a/mss32/include/sounds.h +++ b/mss32/include/sounds.h @@ -29,6 +29,56 @@ struct CWavStore; struct Wdb; struct CLogFile; +enum class SoundEffect : int +{ + Appear, + Boatsnd, + Entrsite, + Entrruin, + Entrcity, + Exitcity, + Occupy, + Spelldis, + Lootruin, + Enroll, + Occupy2, + Beep, + Botreprt, + Seebat, + Sbattle, + Snoble, + Stolen, + Building, + Openbook, + Closbook, + Bkpopup, + Openintr, + Closintr, + Pboost, + Pheal, + Previve, + Useitem, + Buyitem, + Citygrow, + Takebag, + Spinrock, + Chngface, + Soundfx, + Givegold, + Tradspel, + Reftrspe, + Traditem, + Reftritm, + Alliance, + Refallia, + Brkallia, + Aichat, + AUNN7778, + AUNN7788, + Endriot, + Creatstk, +}; + struct SoundsData { String string; @@ -48,6 +98,29 @@ struct Sounds assert_size(Sounds, 4); +using SoundsPtr = SmartPtr; + +namespace SoundsApi { + +struct Api +{ + using Instance = SoundsPtr*(__stdcall*)(SoundsPtr* sounds); + Instance instance; + + using SoundsPtrSetData = void(__thiscall*)(SoundsPtr* thisptr, Sounds* data); + SoundsPtrSetData soundsPtrSetData; + + using PlaySound = int(__thiscall*)(Sounds* thisptr, + SoundEffect effect, + int a2, + SmartPointer* functor); + PlaySound playSound; +}; + +Api& get(); + +} // namespace SoundsApi + } // namespace game #endif // SOUNDS_H diff --git a/mss32/include/stacktemplatecache.h b/mss32/include/stacktemplatecache.h new file mode 100644 index 00000000..646e8e60 --- /dev/null +++ b/mss32/include/stacktemplatecache.h @@ -0,0 +1,49 @@ +/* + * This file is part of the modding toolset for Disciples 2. + * (https://github.com/VladimirMakeev/D2ModdingToolset) + * Copyright (C) 2024 Vladimir Makeev. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef STACKTEMPLATECACHE_H +#define STACKTEMPLATECACHE_H + +#include "midgardid.h" +#include + +namespace hooks { + +using StacksSet = std::unordered_set; + +/** Adds mapping of stack template and a stack created from it. */ +void stackTemplateCacheAdd(const game::CMidgardID& stackTemplateId, + const game::CMidgardID& stackId); + +/** Removes existing stack template and stack mapping. */ +void stackTemplateCacheRemove(const game::CMidgardID& stackTemplateId, + const game::CMidgardID& stackId); + +/** Returns stacks created from template or nullptr if no stacks were created. */ +const StacksSet* stackTemplateCacheFind(const game::CMidgardID& stackTemplateId); + +/** Returns true if at least 1 stack was created from specified template. */ +bool stackTemplateCacheCheck(const game::CMidgardID& stackTemplateId); + +/** Clears entire cache. */ +void stackTemplateCacheClear(); + +} // namespace hooks + +#endif // STACKTEMPLATECACHE_H diff --git a/mss32/include/streambits.h b/mss32/include/streambits.h new file mode 100644 index 00000000..cbcfe5a8 --- /dev/null +++ b/mss32/include/streambits.h @@ -0,0 +1,66 @@ +/* + * This file is part of the modding toolset for Disciples 2. + * (https://github.com/VladimirMakeev/D2ModdingToolset) + * Copyright (C) 2024 Vladimir Makeev. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef STREAMBITS_H +#define STREAMBITS_H + +#include "mqstream.h" + +namespace game { + +struct CMidgardID; + +struct CStreamBits : public CMqStream +{ + void* buffer; + int bufferSize; + int unknown4; + char unknown5; + char unknown6; + char padding[2]; +}; + +assert_size(CStreamBits, 24); + +namespace CStreamBitsApi { + +struct Api +{ + using SerializeId = bool(__stdcall*)(CStreamBits* stream, CMidgardID* id); + SerializeId serializeId; + + /** Create CStreamBits object in a read mode. */ + using ReadConstructor = CStreamBits*(__thiscall*)(CStreamBits* thisptr, + int dummy, + void* buffer, + std::uint32_t bufferSize, + bool a5); + ReadConstructor readConstructor; + + using Destructor = void(__thiscall*)(CStreamBits* thisptr); + Destructor destructor; +}; + +Api& get(); + +} // namespace CStreamBitsApi + +} // namespace game + +#endif // STREAMBITS_H diff --git a/mss32/include/taskmanager.h b/mss32/include/taskmanager.h index 750ff141..f304d35a 100644 --- a/mss32/include/taskmanager.h +++ b/mss32/include/taskmanager.h @@ -25,6 +25,7 @@ namespace game { struct ITaskManagerHolder; +struct ITask; struct CTaskManagerData { @@ -42,6 +43,18 @@ struct CTaskManager assert_size(CTaskManager, 8); +namespace CTaskManagerApi { + +struct Api +{ + using SetCurrentTask = void(__thiscall*)(CTaskManager* taskManager, ITask* task); + SetCurrentTask setCurrentTask; +}; + +Api& get(); + +} // namespace CTaskManagerApi + } // namespace game #endif // TASKMANAGER_H diff --git a/mss32/include/taskobj.h b/mss32/include/taskobj.h new file mode 100644 index 00000000..4a701d48 --- /dev/null +++ b/mss32/include/taskobj.h @@ -0,0 +1,81 @@ +/* + * This file is part of the modding toolset for Disciples 2. + * (https://github.com/VladimirMakeev/D2ModdingToolset) + * Copyright (C) 2024 Vladimir Makeev. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef TASKOBJ_H +#define TASKOBJ_H + +#include "canselect.h" +#include "taskbase.h" + +namespace game { + +struct CursorHandle; + +namespace editor { + +struct CObjectInterf; + +struct CTaskObjData +{ + CObjectInterf* objectInterf; + int unknown2; + int unknown3; + int unknown4; +}; + +assert_size(CTaskObjData, 16); + +/** Base class for tasks related to scenario objects. */ +struct CTaskObj : public CTaskBase +{ + ICanSelect canSelect; + CTaskObjData* taskObjData; +}; + +assert_size(CTaskObj, 28); + +struct CTaskObjVftable : public ITaskVftable +{ + /** Returns brush size when the task is active. */ + using GetBrushSize = CMqPoint*(__thiscall*)(const CTaskObj* thisptr, CMqPoint* size); + GetBrushSize getBrushSize; + + using UnknownMethod = int(__thiscall*)(CTaskObj* thisptr, int a2); + UnknownMethod unknown; + + using DoAction = bool(__thiscall*)(CTaskObj* thisptr, const CMqPoint* mapPosition); + /** Logic depends on child class. Creates scenario objects or deletes them, moves etc. */ + DoAction doAction; + + /** Returns true if action can be performed. */ + DoAction checkActionPossible; + + using GetCursor = SmartPtr*(__thiscall*)(const CTaskObj* thisptr, + SmartPtr* cursor, + bool selectionAllowed); + GetCursor getCursor; +}; + +assert_vftable_size(CTaskObjVftable, 11); + +} // namespace editor + +} // namespace game + +#endif // TASKOBJ_H diff --git a/mss32/include/taskobjaddsite.h b/mss32/include/taskobjaddsite.h new file mode 100644 index 00000000..0c131e59 --- /dev/null +++ b/mss32/include/taskobjaddsite.h @@ -0,0 +1,69 @@ +/* + * This file is part of the modding toolset for Disciples 2. + * (https://github.com/VladimirMakeev/D2ModdingToolset) + * Copyright (C) 2024 Vladimir Makeev. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef TASKOBJADDSITE_H +#define TASKOBJADDSITE_H + +#include "sitecategories.h" +#include "taskobj.h" + +namespace game { + +namespace editor { + +struct CObjectInterf; + +struct CTaskObjAddSiteData +{ + LSiteCategory siteCategory; + SmartPtr selectUnitCursor; + SmartPtr noDragDropCursor; +}; + +assert_size(CTaskObjAddSiteData, 28); + +/** Creates site objects on scenario map. */ +struct CTaskObjAddSite : public CTaskObj +{ + CTaskObjAddSiteData* siteData; +}; + +assert_size(CTaskObjAddSite, 32); + +namespace CTaskObjAddSiteApi { + +struct Api +{ + using Constructor = CTaskObjAddSite*(__thiscall*)(CTaskObjAddSite* thisptr, + CObjectInterf* objInterf, + LSiteCategory category); + Constructor constructor; + + CTaskObjVftable::DoAction doAction; +}; + +Api& get(); + +} // namespace CTaskObjAddSiteApi + +} // namespace editor + +} // namespace game + +#endif // TASKOBJADDSITE_H diff --git a/mss32/include/taskobjaddsitehooks.h b/mss32/include/taskobjaddsitehooks.h new file mode 100644 index 00000000..02ab57ed --- /dev/null +++ b/mss32/include/taskobjaddsitehooks.h @@ -0,0 +1,39 @@ +/* + * This file is part of the modding toolset for Disciples 2. + * (https://github.com/VladimirMakeev/D2ModdingToolset) + * Copyright (C) 2024 Vladimir Makeev. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef TASKOBJADDSITEHOOKS_H +#define TASKOBJADDSITEHOOKS_H + +namespace game { +struct CMqPoint; + +namespace editor { +struct CTaskObjAddSite; +} +} // namespace game + +namespace hooks { + +bool __fastcall taskObjAddSiteDoActionHooked(game::editor::CTaskObjAddSite* thisptr, + int /*%edx*/, + const game::CMqPoint* mapPosition); + +} + +#endif // TASKOBJADDSITEHOOKS_H diff --git a/mss32/include/taskobjerase.h b/mss32/include/taskobjerase.h new file mode 100644 index 00000000..6067b453 --- /dev/null +++ b/mss32/include/taskobjerase.h @@ -0,0 +1,68 @@ +/* + * This file is part of the modding toolset for Disciples 2. + * (https://github.com/VladimirMakeev/D2ModdingToolset) + * Copyright (C) 2024 Vladimir Makeev. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef TASKOBJERASE_H +#define TASKOBJERASE_H + +#include "taskobj.h" + +namespace game { + +namespace editor { + +struct CTaskObjEraseData +{ + char unknown[12]; + SmartPointer ptr; + SmartPointer ptr2; + int unknown2; + int unknown3; + int unknown4; + int unknown5; + int unknown6; + int unknown7; +}; + +assert_size(CTaskObjEraseData, 52); + +struct CTaskObjErase : public CTaskObj +{ + CTaskObjEraseData* taskEraseData; +}; + +assert_size(CTaskObjErase, 32); + +namespace CTaskObjEraseApi { + +struct Api +{ + using Constructor = CTaskObjErase*(__thiscall*)(CTaskObjErase* thisptr, + CObjectInterf* objInterf); + Constructor constructor; +}; + +Api& get(); + +} // namespace CTaskObjEraseApi + +} // namespace editor + +} // namespace game + +#endif // TASKOBJERASE_H diff --git a/mss32/include/taskobjmove.h b/mss32/include/taskobjmove.h new file mode 100644 index 00000000..7ba87b27 --- /dev/null +++ b/mss32/include/taskobjmove.h @@ -0,0 +1,65 @@ +/* + * This file is part of the modding toolset for Disciples 2. + * (https://github.com/VladimirMakeev/D2ModdingToolset) + * Copyright (C) 2024 Vladimir Makeev. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef TASKOBJMOVE_H +#define TASKOBJMOVE_H + +#include "taskobj.h" + +namespace game { + +namespace editor { + +struct CObjMoveSelect; + +struct CTaskObjMoveData +{ + CObjMoveSelect* objMoveSelect; + int unknown; + int unknown2; + char unknown3[32]; +}; + +assert_size(CTaskObjMoveData, 44); + +/** Moves existing objects on scenario map. */ +struct CTaskObjMove : public CTaskObj +{ + CTaskObjMoveData* moveData; +}; + +assert_size(CTaskObjMove, 32); + +namespace CTaskObjMoveApi { + +struct Api +{ + using Constructor = CTaskObjMove*(__thiscall*)(CTaskObjMove* thisptr, CObjectInterf* objInterf); + Constructor constructor; +}; + +Api& get(); + +} // namespace CTaskObjMoveApi + +} // namespace editor + +} // namespace game + +#endif // TASKOBJMOVE_H diff --git a/mss32/include/taskobjprop.h b/mss32/include/taskobjprop.h new file mode 100644 index 00000000..cb8ed156 --- /dev/null +++ b/mss32/include/taskobjprop.h @@ -0,0 +1,65 @@ +/* + * This file is part of the modding toolset for Disciples 2. + * (https://github.com/VladimirMakeev/D2ModdingToolset) + * Copyright (C) 2024 Vladimir Makeev. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef TASKOBJPROP_H +#define TASKOBJPROP_H + +#include "taskobj.h" + +namespace game { + +struct CInterface; + +namespace editor { + +/** Shows scenario map objects properties. */ +struct CTaskObjProp : public CTaskObj +{ + CInterface* propertiesInterface; +}; + +assert_size(CTaskObjProp, 32); + +struct CTaskObjPropVftable : public CTaskObjVftable +{ + using ClosePropertiesInterface = void(__thiscall*)(CTaskObjProp* thisptr); + ClosePropertiesInterface closePropertiesInterface; +}; + +assert_vftable_size(CTaskObjPropVftable, 12); + +namespace CTaskObjPropApi { + +struct Api +{ + using Constructor = CTaskObjProp*(__thiscall*)(CTaskObjProp* thisptr, CObjectInterf* objInterf); + Constructor constructor; + + CTaskObjVftable::DoAction doAction; +}; + +Api& get(); + +} // namespace CTaskObjPropApi + +} // namespace editor + +} // namespace game + +#endif // TASKOBJPROP_H diff --git a/mss32/include/taskobjprophooks.h b/mss32/include/taskobjprophooks.h new file mode 100644 index 00000000..a954a0cd --- /dev/null +++ b/mss32/include/taskobjprophooks.h @@ -0,0 +1,39 @@ +/* + * This file is part of the modding toolset for Disciples 2. + * (https://github.com/VladimirMakeev/D2ModdingToolset) + * Copyright (C) 2024 Vladimir Makeev. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef TASKOBJPROPHOOKS_H +#define TASKOBJPROPHOOKS_H + +namespace game { +struct CMqPoint; + +namespace editor { +struct CTaskObjProp; +} +} // namespace game + +namespace hooks { + +bool __fastcall taskObjPropDoActionHooked(game::editor::CTaskObjProp* thisptr, + int /*%edx*/, + const game::CMqPoint* mapPosition); + +} + +#endif // TASKOBJPROPHOOKS_H diff --git a/mss32/include/testcondition.h b/mss32/include/testcondition.h index d452b11a..23274f40 100644 --- a/mss32/include/testcondition.h +++ b/mss32/include/testcondition.h @@ -59,6 +59,24 @@ struct Api bool samePlayer, const CMidgardID* triggererStackId); Create create; + + ITestConditionVftable::Test testFrequency; + ITestConditionVftable::Test testLocation; + ITestConditionVftable::Test testEnterCity; + ITestConditionVftable::Test testOwnCity; + ITestConditionVftable::Test testKillStack; + ITestConditionVftable::Test testOwnItem; + ITestConditionVftable::Test testLeaderOwnItem; + ITestConditionVftable::Test testDiplomacy; + ITestConditionVftable::Test testAlliance; + ITestConditionVftable::Test testLootRuin; + ITestConditionVftable::Test testTransformLand; + ITestConditionVftable::Test testVisitSite; + ITestConditionVftable::Test testLeaderToZone; + ITestConditionVftable::Test testLeaderToCity; + ITestConditionVftable::Test testItemToLocation; + ITestConditionVftable::Test testStackExists; + ITestConditionVftable::Test testVarInRange; }; Api& get(); diff --git a/mss32/include/testkillstack.h b/mss32/include/testkillstack.h new file mode 100644 index 00000000..4b0a11e2 --- /dev/null +++ b/mss32/include/testkillstack.h @@ -0,0 +1,38 @@ +/* + * This file is part of the modding toolset for Disciples 2. + * (https://github.com/VladimirMakeev/D2ModdingToolset) + * Copyright (C) 2024 Vladimir Makeev. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef TESTKILLSTACK_H +#define TESTKILLSTACK_H + +#include "testcondition.h" + +namespace game { + +struct CMidCondKillStack; + +struct CTestKillStack : ITestCondition +{ + CMidCondKillStack* condKillStack; +}; + +assert_size(CTestKillStack, 8); + +} // namespace game + +#endif // TESTKILLSTACK_H diff --git a/mss32/include/testkillstackhooks.h b/mss32/include/testkillstackhooks.h new file mode 100644 index 00000000..5b3ae6f1 --- /dev/null +++ b/mss32/include/testkillstackhooks.h @@ -0,0 +1,39 @@ +/* + * This file is part of the modding toolset for Disciples 2. + * (https://github.com/VladimirMakeev/D2ModdingToolset) + * Copyright (C) 2024 Vladimir Makeev. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef TESTKILLSTACKHOOKS_H +#define TESTKILLSTACKHOOKS_H + +namespace game { +struct CTestKillStack; +struct IMidgardObjectMap; +struct CMidgardID; +} // namespace game + +namespace hooks { + +bool __fastcall testKillStackHooked(const game::CTestKillStack* thisptr, + int /*%edx*/, + const game::IMidgardObjectMap* objectMap, + const game::CMidgardID* playerId, + const game::CMidgardID* eventId); + +} + +#endif // TESTKILLSTACKHOOKS_H diff --git a/mss32/include/testleaderownitem.h b/mss32/include/testleaderownitem.h new file mode 100644 index 00000000..274bfb7a --- /dev/null +++ b/mss32/include/testleaderownitem.h @@ -0,0 +1,38 @@ +/* + * This file is part of the modding toolset for Disciples 2. + * (https://github.com/VladimirMakeev/D2ModdingToolset) + * Copyright (C) 2024 Vladimir Makeev. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef TESTLEADEROWNITEM_H +#define TESTLEADEROWNITEM_H + +#include "testcondition.h" + +namespace game { + +struct CMidCondLeaderOwnItem; + +struct CTestLeaderOwnItem : public ITestCondition +{ + CMidCondLeaderOwnItem* condLeaderOwnItem; +}; + +assert_size(CTestLeaderOwnItem, 8); + +} // namespace game + +#endif // TESTLEADEROWNITEM_H diff --git a/mss32/include/testleaderownitemhooks.h b/mss32/include/testleaderownitemhooks.h new file mode 100644 index 00000000..38ecc9c7 --- /dev/null +++ b/mss32/include/testleaderownitemhooks.h @@ -0,0 +1,39 @@ +/* + * This file is part of the modding toolset for Disciples 2. + * (https://github.com/VladimirMakeev/D2ModdingToolset) + * Copyright (C) 2024 Vladimir Makeev. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef TESTLEADEROWNITEMHOOKS_H +#define TESTLEADEROWNITEMHOOKS_H + +namespace game { +struct CTestLeaderOwnItem; +struct IMidgardObjectMap; +struct CMidgardID; +} // namespace game + +namespace hooks { + +bool __fastcall testLeaderOwnItemHooked(const game::CTestLeaderOwnItem* thisptr, + int /*%edx*/, + const game::IMidgardObjectMap* objectMap, + const game::CMidgardID* playerId, + const game::CMidgardID* eventId); + +} + +#endif // TESTLEADEROWNITEMHOOKS_H diff --git a/mss32/include/testleadertozone.h b/mss32/include/testleadertozone.h new file mode 100644 index 00000000..483734ba --- /dev/null +++ b/mss32/include/testleadertozone.h @@ -0,0 +1,40 @@ +/* + * This file is part of the modding toolset for Disciples 2. + * (https://github.com/VladimirMakeev/D2ModdingToolset) + * Copyright (C) 2024 Vladimir Makeev. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef TESTLEADERTOZONE_H +#define TESTLEADERTOZONE_H + +#include "midgardid.h" +#include "testcondition.h" + +namespace game { + +struct CMidCondLeaderToZone; + +struct CTestLeaderToZone : public ITestCondition +{ + CMidCondLeaderToZone* condLeaderToZone; + CMidgardID stackId; +}; + +assert_size(CTestLeaderToZone, 12); + +} // namespace game + +#endif // TESTLEADERTOZONE_H diff --git a/mss32/include/testleadertozonehooks.h b/mss32/include/testleadertozonehooks.h new file mode 100644 index 00000000..465c480a --- /dev/null +++ b/mss32/include/testleadertozonehooks.h @@ -0,0 +1,39 @@ +/* + * This file is part of the modding toolset for Disciples 2. + * (https://github.com/VladimirMakeev/D2ModdingToolset) + * Copyright (C) 2024 Vladimir Makeev. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef TESTLEADERTOZONEHOOKS_H +#define TESTLEADERTOZONEHOOKS_H + +namespace game { +struct CTestLeaderToZone; +struct IMidgardObjectMap; +struct CMidgardID; +} // namespace game + +namespace hooks { + +bool __fastcall testLeaderToZoneHooked(const game::CTestLeaderToZone* thisptr, + int /*%edx*/, + const game::IMidgardObjectMap* objectMap, + const game::CMidgardID* playerId, + const game::CMidgardID* eventId); + +} + +#endif // TESTLEADERTOZONEHOOKS_H diff --git a/mss32/include/testownitem.h b/mss32/include/testownitem.h new file mode 100644 index 00000000..c547ebc1 --- /dev/null +++ b/mss32/include/testownitem.h @@ -0,0 +1,38 @@ +/* + * This file is part of the modding toolset for Disciples 2. + * (https://github.com/VladimirMakeev/D2ModdingToolset) + * Copyright (C) 2024 Vladimir Makeev. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef TESTOWNITEM_H +#define TESTOWNITEM_H + +#include "testcondition.h" + +namespace game { + +struct CMidCondOwnItem; + +struct CTestOwnItem : public ITestCondition +{ + CMidCondOwnItem* condOwnItem; +}; + +assert_size(CTestOwnItem, 8); + +} // namespace game + +#endif // TESTOWNITEM_H diff --git a/mss32/include/testownitemhooks.h b/mss32/include/testownitemhooks.h new file mode 100644 index 00000000..72c4a868 --- /dev/null +++ b/mss32/include/testownitemhooks.h @@ -0,0 +1,38 @@ +/* + * This file is part of the modding toolset for Disciples 2. + * (https://github.com/VladimirMakeev/D2ModdingToolset) + * Copyright (C) 2024 Vladimir Makeev. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef TESTOWNITEMHOOKS_H +#define TESTOWNITEMHOOKS_H + +namespace game { +struct CTestOwnItem; +struct IMidgardObjectMap; +struct CMidgardID; +} // namespace game + +namespace hooks { + +bool __fastcall testOwnItemHooked(const game::CTestOwnItem* thisptr, + int /*%edx*/, + const game::IMidgardObjectMap* objectMap, + const game::CMidgardID* playerId, + const game::CMidgardID* eventId); + +} +#endif // TESTOWNITEMHOOKS_H diff --git a/mss32/include/teststackexists.h b/mss32/include/teststackexists.h new file mode 100644 index 00000000..3458a71c --- /dev/null +++ b/mss32/include/teststackexists.h @@ -0,0 +1,38 @@ +/* + * This file is part of the modding toolset for Disciples 2. + * (https://github.com/VladimirMakeev/D2ModdingToolset) + * Copyright (C) 2024 Vladimir Makeev. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef TESTSTACKEXISTS_H +#define TESTSTACKEXISTS_H + +#include "testcondition.h" + +namespace game { + +struct CMidCondStackExists; + +struct CTestStackExists : public ITestCondition +{ + CMidCondStackExists* condStackExists; +}; + +assert_size(CTestStackExists, 8); + +} // namespace game + +#endif // TESTSTACKEXISTS_H diff --git a/mss32/include/teststackexistshooks.h b/mss32/include/teststackexistshooks.h new file mode 100644 index 00000000..7acabb59 --- /dev/null +++ b/mss32/include/teststackexistshooks.h @@ -0,0 +1,39 @@ +/* + * This file is part of the modding toolset for Disciples 2. + * (https://github.com/VladimirMakeev/D2ModdingToolset) + * Copyright (C) 2024 Vladimir Makeev. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef TESTSTACKEXISTSHOOKS_H +#define TESTSTACKEXISTSHOOKS_H + +namespace game { +struct CTestStackExists; +struct IMidgardObjectMap; +struct CMidgardID; +} // namespace game + +namespace hooks { + +bool __fastcall testStackExistsHooked(const game::CTestStackExists* thisptr, + int /*%edx*/, + const game::IMidgardObjectMap* objectMap, + const game::CMidgardID* playerId, + const game::CMidgardID* eventId); + +} + +#endif // TESTSTACKEXISTSHOOKS_H diff --git a/mss32/include/textids.h b/mss32/include/textids.h index d2b8a0bc..bf447d2c 100644 --- a/mss32/include/textids.h +++ b/mss32/include/textids.h @@ -118,6 +118,19 @@ struct TextIds std::string generationError; std::string limitExceeded; } rsg; + + struct ResourceMarket + { + std::string encyDesc; + std::string infiniteAmount; + std::string exchangeDesc; + std::string exchangeNotAvailable; + } resourceMarket; + + struct NobleActions + { + std::string stealMarketSuccess; + } nobleActions; }; const TextIds& textIds(); diff --git a/mss32/include/timer.h b/mss32/include/timer.h new file mode 100644 index 00000000..83af59b7 --- /dev/null +++ b/mss32/include/timer.h @@ -0,0 +1,84 @@ +/* + * This file is part of the modding toolset for Disciples 2. + * (https://github.com/VladimirMakeev/D2ModdingToolset) + * Copyright (C) 2024 Vladimir Makeev. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef TIMER_H +#define TIMER_H + +#include "log.h" +#include +#include +#include + +namespace hooks { + +// #define D2_MEASURE_EVENTS_TIME + +/** Measures scope execution time and writes it to the log file. */ +class ScopedTimer +{ + using Clock = std::chrono::high_resolution_clock; + +public: + ScopedTimer(std::string_view description, std::string_view log) + : description{description} + , log{log} + , start{Clock::now()} + { } + + ~ScopedTimer() + { + const auto elapsed{Clock::now() - start}; + const auto us{std::chrono::duration_cast(elapsed).count()}; + logDebug(log, fmt::format("{:s} time {:d} us", description, us)); + } + +private: + const std::string_view description; + const std::string_view log; + const Clock::time_point start; +}; + +/** Measures scope execution time and accumulates it using provided reference. */ +class ScopedValueTimer +{ + using Clock = std::chrono::high_resolution_clock; + using Duration = std::chrono::microseconds; + using Type = Duration::rep; + +public: + ScopedValueTimer(Type& value) + : value{value} + , start{Clock::now()} + { } + + ~ScopedValueTimer() + { + const auto elapsed{Clock::now() - start}; + const auto us{std::chrono::duration_cast(elapsed).count()}; + value += us; + } + +private: + Type& value; + const Clock::time_point start; +}; + +} // namespace hooks + +#endif // TIMER_H diff --git a/mss32/include/togglebutton.h b/mss32/include/togglebutton.h index 23f066c4..c074cbc2 100644 --- a/mss32/include/togglebutton.h +++ b/mss32/include/togglebutton.h @@ -20,6 +20,7 @@ #ifndef TOGGLEBUTTON_H #define TOGGLEBUTTON_H +#include "functordispatch2.h" #include "interface.h" #include "smartptr.h" @@ -27,13 +28,26 @@ namespace game { struct CToggleButtonVftable; struct CDialogInterf; +struct CToggleButton; +struct IMqImage2; + +enum class ToggleButtonState : int +{ + Normal, + Hovered, + Clicked, + NormalChecked, + HoveredChecked, + ClickedChecked, + Disabled, +}; struct CToggleButtonData { int buttonChildIndex; bool checked; char padding[3]; - SmartPointer ptr; + SmartPtr> onClickedFunctor; SmartPointer ptrArray[7]; }; @@ -52,13 +66,23 @@ assert_size(CToggleButton, 12); struct CToggleButtonVftable : public CInterfaceVftable { - void* method34; + /** Sets image for specified toggle button state. */ + using SetImage = void(__thiscall*)(CToggleButton* thisptr, + IMqImage2* image, + ToggleButtonState state); + SetImage setImage; + /** Enables or disables toggle button. */ using SetEnabled = void(__thiscall*)(CToggleButton* thisptr, bool value); SetEnabled setEnabled; - void* method36; - void* method37; + /** Returns true if toggle button is enabled. */ + using IsEnabled = bool(__thiscall*)(const CToggleButton* thisptr); + IsEnabled isEnabled; + + /** Calls onClickedFunctor callback if previously set. */ + using CallOnClicked = void(__thiscall*)(CToggleButton* thisptr); + CallOnClicked callOnClicked; }; assert_vftable_size(CToggleButtonVftable, 38); diff --git a/mss32/include/trainingcampinterf.h b/mss32/include/trainingcampinterf.h new file mode 100644 index 00000000..70da12fe --- /dev/null +++ b/mss32/include/trainingcampinterf.h @@ -0,0 +1,80 @@ +/* + * This file is part of the modding toolset for Disciples 2. + * (https://github.com/VladimirMakeev/D2ModdingToolset) + * Copyright (C) 2024 Vladimir Makeev. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef TRAININGCAMPINTERF_H +#define TRAININGCAMPINTERF_H + +#include "draganddropinterf.h" +#include "intvector.h" + +namespace game { + +struct CMidSiteTrainer; + +namespace editor { + +struct CTaskObjProp; + +struct CTrainingCampInterfData +{ + CTaskObjProp* taskObjProp; + CMidSiteTrainer* trainingCamp; + char unknown2[20]; + IntVector siteIndices; + int selectedImgIsoIndex; +}; + +assert_size(CTrainingCampInterfData, 48); + +/** Represents DLG_TRAINING_CAMP from ScenEdit.dlg */ +struct CTrainingCampInterf : public CDragAndDropInterf +{ + CTrainingCampInterfData* campData; +}; + +assert_size(CTrainingCampInterf, 28); + +namespace CTrainingCampInterfApi { + +struct Api +{ + struct ButtonCallback + { + using Callback = void(__thiscall*)(CTrainingCampInterf* thisptr); + + Callback callback; + int unknown; + }; + + using CreateButtonFunctor = SmartPointer*(__stdcall*)(SmartPointer* functor, + int dummy, + CTrainingCampInterf* interf, + ButtonCallback* callback); + CreateButtonFunctor createButtonFunctor; +}; + +Api& get(); + +} // namespace CTrainingCampInterfApi + +} + +} + +#endif // TRAININGCAMPINTERF_H diff --git a/mss32/include/utils.h b/mss32/include/utils.h index 92cb6c2e..bf97fc92 100644 --- a/mss32/include/utils.h +++ b/mss32/include/utils.h @@ -32,6 +32,7 @@ struct CMidMsgBoxButtonHandler; struct IMidgardObjectMap; struct UiEvent; struct CInterface; +enum class SoundEffect : int; } // namespace game namespace hooks { @@ -56,6 +57,9 @@ const std::filesystem::path& templatesFolder(); /** Returns full path to the exports folder. */ const std::filesystem::path& exportsFolder(); +/** Returns full path to the ScenData folder. */ +const std::filesystem::path& scenDataFolder(); + /** Returns full path to the executable that is currently running. */ const std::filesystem::path& exePath(); @@ -117,10 +121,12 @@ std::uint32_t createMessageEvent(game::UiEvent* messageEvent, bool computeHash(const std::filesystem::path& folder, std::string& hash); /** Executes function for each scenario object with specified id type. */ -void forEachScenarioObject(game::IMidgardObjectMap* objectMap, +void forEachScenarioObject(const game::IMidgardObjectMap* objectMap, game::IdType idType, const std::function& func); +void playSoundEffect(game::SoundEffect effect); + template static inline void replaceRttiInfo(game::RttiInfo& dst, const T* src, bool copyVftable = true) { diff --git a/mss32/include/visitorcreatesite.h b/mss32/include/visitorcreatesite.h new file mode 100644 index 00000000..27ee118c --- /dev/null +++ b/mss32/include/visitorcreatesite.h @@ -0,0 +1,63 @@ +/* + * This file is part of the modding toolset for Disciples 2. + * (https://github.com/VladimirMakeev/D2ModdingToolset) + * Copyright (C) 2024 Vladimir Makeev. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef VISITORCREATESITE_H +#define VISITORCREATESITE_H + +#include "d2string.h" +#include "mqpoint.h" +#include "sitecategories.h" +#include "visitors.h" + +namespace game { + +namespace editor { + +struct CVisitorCreateSite : public CScenarioVisitor +{ + LSiteCategory category; + CMqPoint position; + int imgIso; + String interfaceImage; + String name; + String description; +}; + +assert_size(CVisitorCreateSite, 80); + +namespace CVisitorCreateSiteApi { + +struct Api +{ + using CanApply = bool(__thiscall*)(const CVisitorCreateSite* thisptr); + CanApply canApply; + + using Apply = bool(__thiscall*)(const CVisitorCreateSite* thisptr); + Apply apply; +}; + +Api& get(); + +} // namespace CVisitorCreateSiteApi + +} // namespace editor + +} // namespace game + +#endif // VISITORCREATESITE_H diff --git a/mss32/include/visitorcreatesitehooks.h b/mss32/include/visitorcreatesitehooks.h new file mode 100644 index 00000000..e1465316 --- /dev/null +++ b/mss32/include/visitorcreatesitehooks.h @@ -0,0 +1,38 @@ +/* + * This file is part of the modding toolset for Disciples 2. + * (https://github.com/VladimirMakeev/D2ModdingToolset) + * Copyright (C) 2024 Vladimir Makeev. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef VISITORCREATESITEHOOKS_H +#define VISITORCREATESITEHOOKS_H + +namespace game::editor { +struct CVisitorCreateSite; + +} + +namespace hooks { + +bool __fastcall visitorCreateSiteCanApplyHooked(const game::editor::CVisitorCreateSite* thisptr, + int /* %edx */); + +bool __fastcall visitorCreateSiteApplyHooked(const game::editor::CVisitorCreateSite* thisptr, + int /* %edx */); + +} // namespace hooks + +#endif // VISITORCREATESITEHOOKS_H diff --git a/mss32/include/visitors.h b/mss32/include/visitors.h index 3c389632..d01b9173 100644 --- a/mss32/include/visitors.h +++ b/mss32/include/visitors.h @@ -25,6 +25,10 @@ namespace game { struct CMidgardID; struct IMidgardObjectMap; struct CScenarioVisitorVftable; +struct LAttitudesCategory; +struct LItemCategory; +struct LSiteCategory; +struct CMqPoint; /** * Base for all visitor classes. @@ -188,6 +192,123 @@ struct Api IMidgardObjectMap* objectMap, int apply); ExtractUnitFromGroup extractUnitFromGroup; + + /** + * Changes player attitude. + * Uses CVisitorPlayerSetAttitude. + * Can be used only in Scenario Editor. + * @param[in] playerId id of player to change. + * @param[in] attitude new attitude category to set. + * @param[in] objectMap interface used for objects search. + * @param apply specifies whether attitude should be changed. + * @returns true if player attitude was changed when apply set to 1. If apply set to 0, returns + * whether visitor can be applied. + */ + using PlayerSetAttitude = bool(__stdcall*)(const CMidgardID* playerId, + const LAttitudesCategory* attitude, + IMidgardObjectMap* objectMap, + int apply); + PlayerSetAttitude playerSetAttitude; + + /** + * Sets source template id for a stack. + * Uses CVisitorSetStackSrcTemplate. + * @param[in] stackId id of stack to change. + * @param[in] stackTemplateId source template id to set. + * @param[in] objectMap interface used for objects search. + * @param apply specifies whether stack source template should be changed. + * @returns true if source template was set when apply set to 1. If apply set to 0, returns + * whether visitor can be applied. + */ + using SetStackSrcTemplate = bool(__stdcall*)(const CMidgardID* stackId, + const CMidgardID* stackTemplateId, + IMidgardObjectMap* objectMap, + int apply); + SetStackSrcTemplate setStackSrcTemplate; + + /** + * Allows merchant to buy items of specified category. + * Can be called only in Scenario Editor. + * Uses CVisitorMerchantAddBuyCategory. + * @param[in] siteId id of merchant to change. + * @param[in] itemCategory category of items merchant is allowed to buy. + * @param[in] objectMap interface used for objects search. + * @param apply specifies whether merchant object should be changed. + * @returns true if merchant was changed successfully when apply set to 1. If ally set to 0, + * returns whether visitor can be applied. + */ + using MerchantAddBuyCategory = bool(__stdcall*)(const CMidgardID* siteId, + const LItemCategory* itemCategory, + IMidgardObjectMap* objectMap, + int apply); + MerchantAddBuyCategory merchantAddBuyCategory; + + /** + * Creates site of specified category. + * Can be called only in Scenario Editor. + * Uses CVisitorCreateSite. + * @param[in] site site category to create. + * @param[in] mapPosition position on a map where to create a new site. + * @param imgIso site scenario map image index. + * @param[in] imgIntf site interface image. + * @param[in] name site name. + * @param[in] description site description. + * @param[in] objectMap interface used for objects search. + * @param apply specifies whether site object should be created. + * @returns true if site was creates successfully when apply set to 1. If apply set to 0, + * returns wheter visitor can be applied. + */ + using CreateSite = bool(__stdcall*)(const LSiteCategory* site, + const CMqPoint* mapPosition, + int imgIso, + const char* imgIntf, + const char* name, + const char* description, + IMidgardObjectMap* objectMap, + int apply); + CreateSite createSite; + + /** + * Changes site object information. + * Can be called only in Scenario Editor. + * Uses CVisitorChangeSiteInfo. + * @param[in] siteId id of existing site object to change. + * @param[in] name new name for a site. + * @param[in] description new description for a site. + * @param[in] objectMap interface used for objects search. + * @param apply specifies whether site object info should be changed. + * @returns true if site info was changed successfully when apply set to 1. + * If apply set to 0, returns wheter visitor can be applied. + */ + using ChangeSiteInfo = bool(__stdcall*)(const CMidgardID* siteId, + const char* name, + const char* description, + IMidgardObjectMap* objectMap, + int apply); + ChangeSiteInfo changeSiteInfo; + + /** + * Changes site object image on a strategic map. + * Can be called only in Scenario Editor. + * Uses CVisitorChangeSiteImage. + * @param[in] siteId id of existing site object to change. + * @param imgIso strategic map image index. + * @param[in] objectMap interface used for objects search. + * @param apply specifies whether site object image should be changed. + * @returns true if site image was changed successfully when apply set to 1. + * If apply set to 0, returns whether visitor can be applied. + */ + using ChangeSiteImage = bool(__stdcall*)(const CMidgardID* siteId, + int imgIso, + IMidgardObjectMap* objectMap, + int apply); + ChangeSiteImage changeSiteImage; + + using ChangeSiteAiPriority = bool(__stdcall*)(const CMidgardID* siteId, + int aiPriority, + IMidgardObjectMap* objectMap, + int apply); + ChangeSiteAiPriority changeSiteAiPriority; }; Api& get(); diff --git a/mss32/include/widgetinterf.h b/mss32/include/widgetinterf.h index 47c68f73..0c7e3671 100644 --- a/mss32/include/widgetinterf.h +++ b/mss32/include/widgetinterf.h @@ -26,6 +26,7 @@ #include "functordispatch3.h" #include "functordispatch4.h" #include "interface.h" +#include "mqpoint.h" #include "uievent.h" namespace game { diff --git a/mss32/midgardid.natvis b/mss32/midgardid.natvis new file mode 100644 index 00000000..3cf799f6 --- /dev/null +++ b/mss32/midgardid.natvis @@ -0,0 +1,76 @@ + + + + + {{ value={(unsigned int)value,X} }} + + "Global (G)" + "Campaign (C)" + "Scenario (S)" + "External (X)" + ((unsigned int)value >> 22) & 0xff + "Empty (00)" + "App text (TA)" + "Building (BB)" + "Race (RR)" + "Lord (LR)" + "Spell (SS)" + "Unit global (UU)" + "Unit generated (UG)" + "Unit modifier (UM)" + "Attack (AA)" + "Text global (TG)" + "Landmark global (MG)" + "Item global (IG)" + "Noble action (NA)" + "Dynamic upgrade (DU)" + "Dynamic attack (DA)" + "Dynamic alt. attack (AL)" + "Dynamic attack 2 (DC)" + "Dynamic alt. attack 2 (AC)" + "Campaign file (CC)" + "CW" + "CO" + "Plan (PN)" + "Object count (OB)" + "Scenario file (SC)" + "Map (MP)" + "Map block (MB)" + "Scenario info (IF)" + "Spell effects (ET)" + "Fortification (FT)" + "Player (PL)" + "Player known spells (KS)" + "Fog (FG)" + "Player buildings (PB)" + "Road (RA)" + "Stack (KC)" + "Unit (UU)" + "Landmark (MM)" + "Item (IM)" + "Bag (BG)" + "Site (SI)" + "Ruin (RU)" + "Tomb (TB)" + "Rod (RD)" + "Crystal (CR)" + "Diplomacy (DP)" + "Spell cast (ST)" + "Location (LO)" + "Stack template (TM)" + "Event (EV)" + "Stack destroyed (SD)" + "Talisman charges (TC)" + "MT" + "Mountains (ML)" + "Subrace (SR)" + "Subrace type (BR)" + "Quest log (QL)" + "Turn summary (TS)" + "Scenario variables (SV)" + "Invalid" + (unsigned int)value & 0xffff + + + + \ No newline at end of file diff --git a/mss32/mss32.vcxproj b/mss32/mss32.vcxproj index 92f429ca..4eb1aef0 100644 --- a/mss32/mss32.vcxproj +++ b/mss32/mss32.vcxproj @@ -155,6 +155,8 @@ call ..\buildDetours.bat ..\Detours + + @@ -195,9 +197,20 @@ call ..\buildDetours.bat ..\Detours + + + + + + + + + + + @@ -227,10 +240,17 @@ call ..\buildDetours.bat ..\Detours + + + + + + + @@ -238,12 +258,16 @@ call ..\buildDetours.bat ..\Detours + + + + @@ -291,10 +315,15 @@ call ..\buildDetours.bat ..\Detours + + + + + @@ -303,15 +332,34 @@ call ..\buildDetours.bat ..\Detours + + + + + + + + + + + + + + + + + + + @@ -454,19 +502,27 @@ call ..\buildDetours.bat ..\Detours + + + + + + + + @@ -478,17 +534,30 @@ call ..\buildDetours.bat ..\Detours + + + + + + + + + + + + + @@ -516,6 +585,8 @@ call ..\buildDetours.bat ..\Detours + + @@ -568,6 +639,8 @@ call ..\buildDetours.bat ..\Detours + + @@ -637,9 +710,20 @@ call ..\buildDetours.bat ..\Detours + + + + + + + + + + + @@ -670,13 +754,25 @@ call ..\buildDetours.bat ..\Detours + + + + + + + + + + + + @@ -689,6 +785,7 @@ call ..\buildDetours.bat ..\Detours + @@ -752,6 +849,7 @@ call ..\buildDetours.bat ..\Detours + @@ -773,18 +871,34 @@ call ..\buildDetours.bat ..\Detours + + + + + + + + + + + + + + + + @@ -794,6 +908,8 @@ call ..\buildDetours.bat ..\Detours + + @@ -1093,12 +1209,17 @@ call ..\buildDetours.bat ..\Detours + + + + + @@ -1109,9 +1230,32 @@ call ..\buildDetours.bat ..\Detours + + + + + + + + + + + + + + + + + + + + + + + @@ -1206,6 +1350,11 @@ call ..\buildDetours.bat ..\Detours + + + false + + diff --git a/mss32/mss32.vcxproj.filters b/mss32/mss32.vcxproj.filters index fa589d57..46d3fff2 100644 --- a/mss32/mss32.vcxproj.filters +++ b/mss32/mss32.vcxproj.filters @@ -50,6 +50,12 @@ {54ad4f5d-bf7a-487f-9a3c-94547ca33f44} + + {4414afc9-2087-4905-bf4d-220042197dd5} + + + {6454c2cc-1767-4c10-9a0a-dee340ea6ca0} + @@ -1265,12 +1271,225 @@ bindings + + bindings + + + bindings + + + bindings + game game + + bindings + + + game + + + bindings + + + bindings + + + bindings + + + bindings + + + bindings + + + game + + + hooks + + + game + + + game + + + features + + + hooks + + + hooks + + + hooks + + + features + + + hooks + + + hooks + + + game + + + game + + + game + + + game + + + game + + + game + + + game + + + game + + + game + + + game + + + game + + + game + + + hooks + + + hooks + + + hooks + + + hooks + + + hooks + + + game + + + game + + + game + + + hooks + + + game + + + game + + + game + + + game + + + game + + + game + + + game + + + game + + + hooks + + + game + + + game + + + hooks + + + hooks + + + features\custom scenario objects + + + features\custom scenario objects + + + features\custom scenario objects + + + features\custom scenario objects + + + bindings + + + bindings + + + game + + + game + + + features\custom noble actions + + + features\custom noble actions + + + game + + + game + + + hooks + + + features\custom noble actions + + + features\custom noble actions + + + features\custom noble actions + @@ -3295,6 +3514,15 @@ bindings + + bindings + + + bindings + + + bindings + game @@ -3304,6 +3532,216 @@ game + + bindings + + + game + + + game + + + bindings + + + game + + + game + + + game + + + game + + + bindings + + + bindings + + + bindings + + + bindings + + + game + + + hooks + + + game + + + game + + + features + + + game + + + hooks + + + utils + + + game + + + game + + + hooks + + + game + + + hooks + + + features + + + game + + + hooks + + + game + + + hooks + + + game + + + hooks + + + hooks + + + game + + + game + + + game + + + game + + + hooks + + + game + + + game + + + game + + + game + + + game + + + hooks + + + hooks + + + game + + + game + + + hooks + + + game + + + hooks + + + game + + + hooks + + + game + + + hooks + + + features\custom scenario objects + + + features\custom scenario objects + + + features\custom scenario objects + + + features\custom scenario objects + + + bindings + + + bindings + + + game + + + game + + + features\custom noble actions + + + features\custom noble actions + + + game + + + game + + + hooks + + + features\custom noble actions + + + features\custom noble actions + + + features\custom noble actions + @@ -3313,4 +3751,7 @@ + + + \ No newline at end of file diff --git a/mss32/src/aiattitudescat.cpp b/mss32/src/aiattitudescat.cpp new file mode 100644 index 00000000..52663a9a --- /dev/null +++ b/mss32/src/aiattitudescat.cpp @@ -0,0 +1,113 @@ +/* + * This file is part of the modding toolset for Disciples 2. + * (https://github.com/VladimirMakeev/D2ModdingToolset) + * Copyright (C) 2024 Vladimir Makeev. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "aiattitudescat.h" +#include "version.h" +#include + +namespace game { +namespace AttitudeCategories { + +// clang-format off +static std::array categories = {{ + // Akella + Categories{ + (LAttitudesCategory*)0x83a0f0, + (LAttitudesCategory*)0x83a0c8, + (LAttitudesCategory*)0x83a0b8, + (LAttitudesCategory*)0x83a0d8, + }, + // Russobit + Categories{ + (LAttitudesCategory*)0x83a0f0, + (LAttitudesCategory*)0x83a0c8, + (LAttitudesCategory*)0x83a0b8, + (LAttitudesCategory*)0x83a0d8, + }, + // Gog + Categories{ + (LAttitudesCategory*)0x8380a0, + (LAttitudesCategory*)0x838078, + (LAttitudesCategory*)0x838068, + (LAttitudesCategory*)0x838088, + }, + // Scenario Editor + Categories{ + (LAttitudesCategory*)0x665c28, + (LAttitudesCategory*)0x665c00, + (LAttitudesCategory*)0x665bf0, + (LAttitudesCategory*)0x665c10, + } +}}; +// clang-format on + +Categories& get() +{ + return categories[static_cast(hooks::gameVersion())]; +} + +} // namespace AttitudeCategories + +namespace LAttitudesCategoryTableApi { + +// clang-format off +static std::array functions = {{ + // Akella + Api{ + (Api::Constructor)0x591c13, + (Api::Init)0x591d7a, + (Api::ReadCategory)0x591df2, + (Api::InitDone)0x591d35, + (Api::FindCategoryById)0x590da2, + }, + // Russobit + Api{ + (Api::Constructor)0x591c13, + (Api::Init)0x591d7a, + (Api::ReadCategory)0x591df2, + (Api::InitDone)0x591d35, + (Api::FindCategoryById)0x590da2, + }, + // Gog + Api{ + (Api::Constructor)0x590d2b, + (Api::Init)0x590e92, + (Api::ReadCategory)0x590f0a, + (Api::InitDone)0x590e4d, + (Api::FindCategoryById)0x58feba, + }, + // Scenario Editor + Api{ + (Api::Constructor)0x53d86e, + (Api::Init)0x53d9d5, + (Api::ReadCategory)0x53da4d, + (Api::InitDone)0x53d990, + (Api::FindCategoryById)0x4440e5, + } +}}; +// clang-format on + +Api& get() +{ + return functions[static_cast(hooks::gameVersion())]; +} + +} // namespace LAttitudesCategoryTableApi + +} // namespace game diff --git a/mss32/src/aiattitudestable.cpp b/mss32/src/aiattitudestable.cpp new file mode 100644 index 00000000..57d8b5fb --- /dev/null +++ b/mss32/src/aiattitudestable.cpp @@ -0,0 +1,52 @@ +/* + * This file is part of the modding toolset for Disciples 2. + * (https://github.com/VladimirMakeev/D2ModdingToolset) + * Copyright (C) 2024 Vladimir Makeev. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "aiattitudestable.h" +#include "version.h" +#include + +namespace game::CAiAttitudesTableApi { + +// clang-format off +static std::array functions = {{ + // Akella + Api{ + (Api::Find)0 + }, + // Russobit + Api{ + (Api::Find)0 + }, + // Gog + Api{ + (Api::Find)0 + }, + // Scenario Editor + Api{ + (Api::Find)0x534880, + } +}}; +// clang-format on + +Api& get() +{ + return functions[static_cast(hooks::gameVersion())]; +} + +} // namespace game::CAiAttitudesTableApi diff --git a/mss32/src/attackutils.cpp b/mss32/src/attackutils.cpp index 97eb7552..926aaafb 100644 --- a/mss32/src/attackutils.cpp +++ b/mss32/src/attackutils.cpp @@ -76,7 +76,7 @@ int getBoostDamage(int level) using namespace game; const auto& global = GlobalDataApi::get(); - const auto vars = *(*global.getGlobalData())->globalVariables; + const auto vars = (*global.getGlobalData())->globalVariables->data; int count = std::size(vars->battleBoostDamage); return (0 < level && level <= count) ? vars->battleBoostDamage[level - 1] : 0; @@ -87,7 +87,7 @@ int getLowerDamage(int level) using namespace game; const auto& global = GlobalDataApi::get(); - const auto vars = *(*global.getGlobalData())->globalVariables; + const auto vars = (*global.getGlobalData())->globalVariables->data; int count = std::size(vars->battleLowerDamage); return (0 < level && level <= count) ? vars->battleLowerDamage[level - 1] : 0; @@ -101,7 +101,7 @@ int getLowerInitiative(int level) return 0; const auto& global = GlobalDataApi::get(); - const auto vars = *(*global.getGlobalData())->globalVariables; + const auto vars = (*global.getGlobalData())->globalVariables->data; return vars->battleLowerIni; } @@ -231,4 +231,128 @@ int getAttackMaxTargets(const game::AttackReachId id) return 0; } +const game::LAttackSource* getAttackSourceById(game::AttackSourceId id) +{ + using namespace game; + + const auto& attackSources{AttackSourceCategories::get()}; + + switch (id) { + default: { + const auto& customSources = hooks::getCustomAttacks().sources; + for (const auto& customSource : customSources) { + if (customSource.source.id == id) { + return &customSource.source; + } + } + + // Could not find source id even in custom sources + return nullptr; + } + case AttackSourceId::Weapon: + return attackSources.weapon; + + case AttackSourceId::Mind: + return attackSources.mind; + + case AttackSourceId::Life: + return attackSources.life; + + case AttackSourceId::Death: + return attackSources.death; + + case AttackSourceId::Fire: + return attackSources.fire; + + case AttackSourceId::Water: + return attackSources.water; + + case AttackSourceId::Earth: + return attackSources.earth; + + case AttackSourceId::Air: + return attackSources.air; + } +} + +const game::LAttackClass* getAttackClassById(game::AttackClassId id) +{ + using namespace game; + + const auto& attackClasses{AttackClassCategories::get()}; + + switch (id) { + case AttackClassId::Damage: + return attackClasses.damage; + + case AttackClassId::Drain: + return attackClasses.drain; + + case AttackClassId::Paralyze: + return attackClasses.paralyze; + + case AttackClassId::Heal: + return attackClasses.heal; + + case AttackClassId::Fear: + return attackClasses.fear; + + case AttackClassId::BoostDamage: + return attackClasses.boostDamage; + + case AttackClassId::Petrify: + return attackClasses.petrify; + + case AttackClassId::LowerDamage: + return attackClasses.lowerDamage; + + case AttackClassId::LowerInitiative: + return attackClasses.lowerInitiative; + + case AttackClassId::Poison: + return attackClasses.poison; + + case AttackClassId::Frostbite: + return attackClasses.frostbite; + + case AttackClassId::Revive: + return attackClasses.revive; + + case AttackClassId::DrainOverflow: + return attackClasses.drainOverflow; + + case AttackClassId::Cure: + return attackClasses.cure; + + case AttackClassId::Summon: + return attackClasses.summon; + + case AttackClassId::DrainLevel: + return attackClasses.drainLevel; + + case AttackClassId::GiveAttack: + return attackClasses.giveAttack; + + case AttackClassId::Doppelganger: + return attackClasses.doppelganger; + + case AttackClassId::TransformSelf: + return attackClasses.transformSelf; + + case AttackClassId::TransformOther: + return attackClasses.transformOther; + + case AttackClassId::Blister: + return attackClasses.blister; + + case AttackClassId::BestowWards: + return attackClasses.bestowWards; + + case AttackClassId::Shatter: + return attackClasses.shatter; + } + + return nullptr; +} + } // namespace hooks diff --git a/mss32/src/battlemsgdata.cpp b/mss32/src/battlemsgdata.cpp index a74e0018..a534b19d 100644 --- a/mss32/src/battlemsgdata.cpp +++ b/mss32/src/battlemsgdata.cpp @@ -61,6 +61,7 @@ static std::array functions = {{ (Api::FindAttackTargetWithAllReach)0x5d0ee1, (Api::FindSpecificAttackTarget)0x5d19e9, (Api::FindSpecificAttackTarget)0x5d3079, + (Api::FindSpecificAttackTarget)0x5d130b, (Api::FindDoppelgangerAttackTarget)0x5d2409, (Api::FindDamageAttackTargetWithNonAllReach)0x5d0a1b, (Api::FindDamageAttackTargetWithAnyReach)0x5d0cf1, @@ -87,6 +88,7 @@ static std::array functions = {{ (Api::FillEmptyTargetsListForAllAnyAttackReach)0x66429c, (Api::FillTargetsListForAdjacentAttackReach)0x664312, (Api::IsAutoBattle)0x6243c8, + (Api::IsFastBattle)0x6243e1, (Api::AlliesNotPreventingAdjacentAttack)0x65c19c, (Api::GiveAttack)0x623ade, (Api::RemoveFiniteBoostLowerDamage)0x627b07, @@ -94,6 +96,12 @@ static std::array functions = {{ (Api::UpdateBattleActions)0x625253, (Api::GetItemAttackTargets)0x62571b, (Api::BeforeBattleRound)0x623440, + (Api::AiChooseBattleAction)0x5CF388, + (Api::GetRetreatStatus)0x624f37, + (Api::SetRetreatStatus)0x624f77, + (Api::IsRetreatDecisionWasMade)0x624f01, + (Api::SetRetreatDecisionWasMade)0x624f17, + (Api::IsAfterBattle)0x622b4b, }, // Russobit Api{ @@ -131,6 +139,7 @@ static std::array functions = {{ (Api::FindAttackTargetWithAllReach)0x5d0ee1, (Api::FindSpecificAttackTarget)0x5d19e9, (Api::FindSpecificAttackTarget)0x5d3079, + (Api::FindSpecificAttackTarget)0x5d130b, (Api::FindDoppelgangerAttackTarget)0x5d2409, (Api::FindDamageAttackTargetWithNonAllReach)0x5d0a1b, (Api::FindDamageAttackTargetWithAnyReach)0x5d0cf1, @@ -157,6 +166,7 @@ static std::array functions = {{ (Api::FillEmptyTargetsListForAllAnyAttackReach)0x66429c, (Api::FillTargetsListForAdjacentAttackReach)0x664312, (Api::IsAutoBattle)0x6243c8, + (Api::IsFastBattle)0x6243e1, (Api::AlliesNotPreventingAdjacentAttack)0x65c19c, (Api::GiveAttack)0x623ade, (Api::RemoveFiniteBoostLowerDamage)0x627b07, @@ -164,6 +174,12 @@ static std::array functions = {{ (Api::UpdateBattleActions)0x625253, (Api::GetItemAttackTargets)0x62571b, (Api::BeforeBattleRound)0x623440, + (Api::AiChooseBattleAction)0x5CF388, + (Api::GetRetreatStatus)0x624f37, + (Api::SetRetreatStatus)0x624f77, + (Api::IsRetreatDecisionWasMade)0x624f01, + (Api::SetRetreatDecisionWasMade)0x624f17, + (Api::IsAfterBattle)0x622b4b, }, // Gog Api{ @@ -201,6 +217,7 @@ static std::array functions = {{ (Api::FindAttackTargetWithAllReach)0x5cfe12, (Api::FindSpecificAttackTarget)0x5d091a, (Api::FindSpecificAttackTarget)0x5d1faa, + (Api::FindSpecificAttackTarget)0x5d023c, (Api::FindDoppelgangerAttackTarget)0x5d133a, (Api::FindDamageAttackTargetWithNonAllReach)0x5cf94c, (Api::FindDamageAttackTargetWithAnyReach)0x5cfc22, @@ -227,6 +244,7 @@ static std::array functions = {{ (Api::FillEmptyTargetsListForAllAnyAttackReach)0x662d1c, (Api::FillTargetsListForAdjacentAttackReach)0x662d92, (Api::IsAutoBattle)0x622f58, + (Api::IsFastBattle)0x622f71, (Api::AlliesNotPreventingAdjacentAttack)0x65ac1c, (Api::GiveAttack)0x62266e, (Api::RemoveFiniteBoostLowerDamage)0x626647, @@ -234,6 +252,12 @@ static std::array functions = {{ (Api::UpdateBattleActions)0x623d93, (Api::GetItemAttackTargets)0x62425b, (Api::BeforeBattleRound)0x621fd0, + (Api::AiChooseBattleAction)0x5ce2b9, + (Api::GetRetreatStatus)0x623ac7, + (Api::SetRetreatStatus)0x623b07, + (Api::IsRetreatDecisionWasMade)0x623a91, + (Api::SetRetreatDecisionWasMade)0x623aa7, + (Api::IsAfterBattle)0x6216db, }, }}; // clang-format on diff --git a/mss32/src/battlemsgdatahooks.cpp b/mss32/src/battlemsgdatahooks.cpp index ff651b16..003c9a1f 100644 --- a/mss32/src/battlemsgdatahooks.cpp +++ b/mss32/src/battlemsgdatahooks.cpp @@ -18,15 +18,30 @@ */ #include "battlemsgdatahooks.h" +#include "attackview.h" #include "batattack.h" +#include "bindings/battlemsgdataviewmutable.h" +#include "bindings/groupview.h" +#include "bindings/idview.h" +#include "bindings/unitview.h" +#include "customaibattle.h" #include "customattacks.h" +#include "fortification.h" #include "gameutils.h" +#include "groupview.h" #include "intset.h" #include "log.h" +#include "midplayer.h" #include "midstack.h" #include "modifierutils.h" #include "originalfunctions.h" +#include "racetype.h" +#include "scripts.h" +#include "settings.h" +#include "unitimplview.h" #include "unitutils.h" +#include "usunitimpl.h" +#include "utils.h" #include #include @@ -392,4 +407,158 @@ void __fastcall beforeBattleRoundHooked(game::BattleMsgData* thisptr, int /*%edx freeTransformSelf.used = false; } +void __stdcall aiChooseBattleActionHooked(const game::IMidgardObjectMap* objectMap, + game::BattleMsgData* battleMsgData, + const game::CMidgardID* unitId, + const game::Set* possibleActions, + const game::PossibleTargets* possibleTargets, + game::BattleAction* battleAction, + game::CMidgardID* targetUnitId, + game::CMidgardID* attackerUnitId) +{ + using namespace game; + + const auto& chooseBattleAction{getOriginalFunctions().aiChooseBattleAction}; + + const auto& customBattleLogic{getCustomAiBattleLogic()}; + if (!customBattleLogic.customBattleLogicEnabled) { + // Custom battle action logic disabled, use original + chooseBattleAction(objectMap, battleMsgData, unitId, possibleActions, possibleTargets, + battleAction, targetUnitId, attackerUnitId); + return; + } + + CMidgardID allyGroupId{}; + gameFunctions().getAllyOrEnemyGroupId(&allyGroupId, battleMsgData, unitId, true); + + const CMidPlayer* allyPlayer{getGroupOwner(objectMap, &allyGroupId)}; + if (!allyPlayer) { + const auto message{fmt::format("Could not find player that owns unit {:s} in group {:s}", + idToString(unitId), idToString(&allyGroupId))}; + + if (userSettings().battle.debugAi) { + showErrorMessageBox(message); + } else { + logError("mssProxyError.log", message); + } + + chooseBattleAction(objectMap, battleMsgData, unitId, possibleActions, possibleTargets, + battleAction, targetUnitId, attackerUnitId); + return; + } + + const auto& battleLogicMap{customBattleLogic.attitudeBattleLogic}; + const auto it{battleLogicMap.find(allyPlayer->attitude.id)}; + if (it == battleLogicMap.cend()) { + const auto message{fmt::format("Could not find battle action script by attitude {:d}", + static_cast(allyPlayer->attitude.id))}; + + if (userSettings().battle.debugAi) { + showErrorMessageBox(message); + } else { + logError("mssProxyError.log", message); + } + + chooseBattleAction(objectMap, battleMsgData, unitId, possibleActions, possibleTargets, + battleAction, targetUnitId, attackerUnitId); + return; + } + + const auto& actionScript{it->second}; + + std::optional env; + const auto path{scriptsFolder() / actionScript}; + auto chooseAction = getScriptFunction(path, "chooseAction", env, true, true); + if (!chooseAction) { + const auto message{ + fmt::format("Could not find 'chooseAction' function. Script '{:s}'", path.string())}; + + if (userSettings().battle.debugAi) { + showErrorMessageBox(message); + } else { + logError("mssProxyError.log", message); + } + + chooseBattleAction(objectMap, battleMsgData, unitId, possibleActions, possibleTargets, + battleAction, targetUnitId, attackerUnitId); + return; + } + + try { + const bindings::BattleMsgDataViewMutable battleView{battleMsgData, objectMap}; + + const CMidUnit* unit = static_cast( + objectMap->vftable->findScenarioObjectById(objectMap, unitId)); + const bindings::UnitView activeUnitView{unit}; + + std::vector actions; + actions.reserve(possibleActions->length); + for (auto& v : *possibleActions) { + actions.push_back(v); + } + + const CMidgardID* attTargGroupId{possibleTargets->attackTargetGroupId}; + std::optional attackTargetGroupView; + if (const auto* group = getGroup(objectMap, attTargGroupId)) { + attackTargetGroupView = bindings::GroupView{group, objectMap, attTargGroupId}; + } + + std::vector attackTargets; + attackTargets.reserve(possibleTargets->attackTargets->length); + for (auto& v : *possibleTargets->attackTargets) { + attackTargets.push_back(v); + } + + const CMidgardID* item1TargGroupId{possibleTargets->item1TargetGroupId}; + std::optional item1TargetGroupView; + if (const auto* group = getGroup(objectMap, item1TargGroupId)) { + item1TargetGroupView = bindings::GroupView{group, objectMap, item1TargGroupId}; + } + + std::vector item1Targets; + item1Targets.reserve(possibleTargets->item1Targets->length); + for (auto& v : *possibleTargets->item1Targets) { + item1Targets.push_back(v); + } + + const CMidgardID* item2TargGroupId{possibleTargets->item2TargetGroupId}; + std::optional item2TargetGroupView; + if (const auto* group = getGroup(objectMap, item2TargGroupId)) { + item2TargetGroupView = bindings::GroupView{group, objectMap, item2TargGroupId}; + } + + std::vector item2Targets; + item2Targets.reserve(possibleTargets->item2Targets->length); + for (auto& v : *possibleTargets->item2Targets) { + item2Targets.push_back(v); + } + + int chosenAction = 0; + bindings::IdView chosenTargetId{(CMidgardID*)nullptr}; + bindings::IdView chosenAttackerId{(CMidgardID*)nullptr}; + sol::tie(chosenAction, chosenTargetId, + chosenAttackerId) = (*chooseAction)(battleView, activeUnitView, actions, + attackTargetGroupView, attackTargets, + item1TargetGroupView, item1Targets, + item2TargetGroupView, item2Targets); + + *targetUnitId = chosenTargetId.id; + *attackerUnitId = chosenAttackerId.id; + *battleAction = static_cast(chosenAction); + } catch (const std::exception& e) { + const auto message{ + fmt::format("Failed to run '{:s}' script. Reason: {:s}", path.string(), e.what())}; + + if (userSettings().battle.debugAi) { + showErrorMessageBox(message); + } else { + logError("mssProxyError.log", message); + } + + *targetUnitId = *unitId; + *attackerUnitId = *unitId; + *battleAction = userSettings().battle.fallbackAction; + } +} + } // namespace hooks diff --git a/mss32/src/bindings/battlemsgdataview.cpp b/mss32/src/bindings/battlemsgdataview.cpp index 99b72797..2c5bd55e 100644 --- a/mss32/src/bindings/battlemsgdataview.cpp +++ b/mss32/src/bindings/battlemsgdataview.cpp @@ -18,7 +18,11 @@ */ #include "battlemsgdataview.h" +#include "attackclasscat.h" +#include "attackutils.h" #include "battlemsgdata.h" +#include "customattacks.h" +#include "game.h" #include "gameutils.h" #include "idview.h" #include "playerview.h" @@ -27,6 +31,31 @@ namespace bindings { +BattleTurnView::BattleTurnView(const game::CMidgardID& unitId, + char attackCount, + const game::IMidgardObjectMap* objectMap) + : unitId{unitId} + , objectMap{objectMap} + , attackCount{attackCount} +{ } + +void BattleTurnView::bind(sol::state& lua) +{ + auto view = lua.new_usertype("BattleTurnView"); + view["unit"] = sol::property(&BattleTurnView::getUnit); + view["attackCount"] = sol::property(&BattleTurnView::getAttackCount); +} + +UnitView BattleTurnView::getUnit() const +{ + return UnitView{game::gameFunctions().findUnitById(objectMap, &unitId)}; +} + +int BattleTurnView::getAttackCount() const +{ + return attackCount; +} + BattleMsgDataView::BattleMsgDataView(const game::BattleMsgData* battleMsgData, const game::IMidgardObjectMap* objectMap) : battleMsgData{battleMsgData} @@ -36,13 +65,7 @@ BattleMsgDataView::BattleMsgDataView(const game::BattleMsgData* battleMsgData, void BattleMsgDataView::bind(sol::state& lua) { auto view = lua.new_usertype("BattleView"); - view["getUnitStatus"] = &BattleMsgDataView::getUnitStatus; - view["currentRound"] = sol::property(&BattleMsgDataView::getCurrentRound); - view["autoBattle"] = sol::property(&BattleMsgDataView::getAutoBattle); - view["attackerPlayer"] = sol::property(&BattleMsgDataView::getAttackerPlayer); - view["defenderPlayer"] = sol::property(&BattleMsgDataView::getDefenderPlayer); - view["attacker"] = sol::property(&BattleMsgDataView::getAttacker); - view["defender"] = sol::property(&BattleMsgDataView::getDefender); + bindAccessMethods(view); } bool BattleMsgDataView::getUnitStatus(const IdView& unitId, int status) const @@ -62,6 +85,11 @@ bool BattleMsgDataView::getAutoBattle() const return game::BattleMsgDataApi::get().isAutoBattle(battleMsgData); } +bool BattleMsgDataView::getFastBattle() const +{ + return game::BattleMsgDataApi::get().isFastBattle(battleMsgData); +} + std::optional BattleMsgDataView::getAttackerPlayer() const { return getPlayer(battleMsgData->attackerPlayerId); @@ -92,6 +120,293 @@ std::optional BattleMsgDataView::getDefender() const return GroupView{group, objectMap, &battleMsgData->defenderGroupId}; } +game::RetreatStatus BattleMsgDataView::getRetreatStatus(bool attacker) const +{ + return game::BattleMsgDataApi::get().getRetreatStatus(battleMsgData, attacker); +} + +bool BattleMsgDataView::isRetreatDecisionWasMade() const +{ + return game::BattleMsgDataApi::get().isRetreatDecisionWasMade(battleMsgData); +} + +bool BattleMsgDataView::isUnitAttacker(const UnitView& unit) const +{ + return isUnitAttackerId(unit.getId()); +} + +bool BattleMsgDataView::isUnitAttackerId(const IdView& unitId) const +{ + return game::BattleMsgDataApi::get().isUnitAttacker(battleMsgData, &unitId.id); +} + +bool BattleMsgDataView::isAfterBattle() const +{ + return game::BattleMsgDataApi::get().isAfterBattle(battleMsgData); +} + +bool BattleMsgDataView::isDuel() const +{ + return battleMsgData->duel & 1u; +} + +BattleMsgDataView::UnitActions BattleMsgDataView::getUnitActions(const UnitView& unit) const +{ + return getUnitActionsById(unit.getId()); +} + +BattleMsgDataView::UnitActions BattleMsgDataView::getUnitActionsById(const IdView& unitId) const +{ + using namespace game; + + std::vector actions; + std::optional attackTargetGroup; + std::vector attackTargets; + std::optional item1TargetGroup; + std::vector item1Targets; + std::optional item2TargetGroup; + std::vector item2Targets; + + { + const auto& setApi = IntSetApi::get(); + + Set battleActions; + setApi.constructor((IntSet*)&battleActions); + + GroupIdTargetsPair attackTargetsPair{}; + setApi.constructor(&attackTargetsPair.second); + + GroupIdTargetsPair item1TargetsPair{}; + setApi.constructor(&item1TargetsPair.second); + + GroupIdTargetsPair item2TargetsPair{}; + setApi.constructor(&item2TargetsPair.second); + + game::BattleMsgDataApi::get().updateBattleActions(objectMap, battleMsgData, &unitId.id, + &battleActions, &attackTargetsPair, + &item1TargetsPair, &item2TargetsPair); + + // Copy data into std containers for sol2 + { + actions.reserve(battleActions.length); + for (auto& v : battleActions) { + actions.push_back(v); + } + + if (const auto* group = hooks::getGroup(objectMap, &attackTargetsPair.first)) { + attackTargetGroup = bindings::GroupView{group, objectMap, &attackTargetsPair.first}; + } + + attackTargets.reserve(attackTargetsPair.second.length); + for (auto& v : attackTargetsPair.second) { + attackTargets.push_back(v); + } + + if (const auto* group = hooks::getGroup(objectMap, &item1TargetsPair.first)) { + item1TargetGroup = bindings::GroupView{group, objectMap, &item1TargetsPair.first}; + } + + item1Targets.reserve(item1TargetsPair.second.length); + for (auto& v : item1TargetsPair.second) { + item1Targets.push_back(v); + } + + if (const auto* group = hooks::getGroup(objectMap, &item2TargetsPair.first)) { + item2TargetGroup = bindings::GroupView{group, objectMap, &item2TargetsPair.first}; + } + + item2Targets.reserve(item2TargetsPair.second.length); + for (auto& v : item2TargetsPair.second) { + item2Targets.push_back(v); + } + } + + setApi.destructor(&item2TargetsPair.second); + setApi.destructor(&item1TargetsPair.second); + setApi.destructor(&attackTargetsPair.second); + setApi.destructor((IntSet*)&battleActions); + } + + return {actions, attackTargetGroup, attackTargets, item1TargetGroup, + item1Targets, item2TargetGroup, item2Targets}; +} + +int BattleMsgDataView::getUnitShatteredArmor(const UnitView& unit) const +{ + return getUnitShatteredArmorById(unit.getId()); +} + +int BattleMsgDataView::getUnitShatteredArmorById(const IdView& unitId) const +{ + return game::BattleMsgDataApi::get().getUnitShatteredArmor(battleMsgData, &unitId.id); +} + +int BattleMsgDataView::getUnitFortificationArmor(const UnitView& unit) const +{ + return getUnitFortificationArmorById(unit.getId()); +} + +int BattleMsgDataView::getUnitFortificationArmorById(const IdView& unitId) const +{ + return game::BattleMsgDataApi::get().getUnitFortificationArmor(battleMsgData, &unitId.id); +} + +bool BattleMsgDataView::isUnitResistantToSource(const UnitView& unit, int sourceId) const +{ + return isUnitResistantToSourceById(unit.getId(), sourceId); +} + +bool BattleMsgDataView::isUnitResistantToSourceById(const IdView& unitId, int sourceId) const +{ + using namespace game; + + auto attackSource{hooks::getAttackSourceById(static_cast(sourceId))}; + if (!attackSource) { + return false; + } + + return !BattleMsgDataApi::get().isUnitAttackSourceWardRemoved(battleMsgData, &unitId.id, + attackSource); +} + +bool BattleMsgDataView::isUnitResistantToClass(const UnitView& unit, int classId) const +{ + return isUnitResistantToClassById(unit.getId(), classId); +} + +bool BattleMsgDataView::isUnitResistantToClassById(const IdView& unitId, int classId) const +{ + using namespace game; + + auto attackClass{hooks::getAttackClassById(static_cast(classId))}; + if (!attackClass) { + return false; + } + + return BattleMsgDataApi::get().isUnitAttackClassWardRemoved(battleMsgData, &unitId.id, + attackClass); +} + +std::vector BattleMsgDataView::getTurnsOrder() const +{ + std::vector turns; + + for (const auto& battleTurn : battleMsgData->turnsOrder) { + if (battleTurn.unitId == game::invalidId) { + break; + } + + turns.emplace_back(battleTurn.unitId, battleTurn.attackCount, objectMap); + } + + return turns; +} + +bool BattleMsgDataView::isUnitRevived(const UnitView& unit) const +{ + return isUnitRevivedById(unit.getId()); +} + +bool BattleMsgDataView::isUnitRevivedById(const IdView& unitId) const +{ + const auto* info{game::BattleMsgDataApi::get().getUnitInfoById(battleMsgData, &unitId.id)}; + if (!info) { + return false; + } + + return info->unitFlags.parts.revived; +} + +bool BattleMsgDataView::isUnitWaiting(const UnitView& unit) const +{ + return isUnitWaitingById(unit.getId()); +} + +bool BattleMsgDataView::isUnitWaitingById(const IdView& unitId) const +{ + const auto* info{game::BattleMsgDataApi::get().getUnitInfoById(battleMsgData, &unitId.id)}; + if (!info) { + return false; + } + + return info->unitFlags.parts.waited; +} + +int BattleMsgDataView::getUnitDisableRound(const UnitView& unit) const +{ + return getUnitDisableRoundById(unit.getId()); +} + +int BattleMsgDataView::getUnitDisableRoundById(const IdView& unitId) const +{ + const auto* info{game::BattleMsgDataApi::get().getUnitInfoById(battleMsgData, &unitId.id)}; + if (!info) { + return 0; + } + + return info->disableAppliedRound; +} + +int BattleMsgDataView::getUnitPoisonRound(const UnitView& unit) const +{ + return getUnitPoisonRoundById(unit.getId()); +} + +int BattleMsgDataView::getUnitPoisonRoundById(const IdView& unitId) const +{ + const auto* info{game::BattleMsgDataApi::get().getUnitInfoById(battleMsgData, &unitId.id)}; + if (!info) { + return 0; + } + + return info->poisonAppliedRound; +} + +int BattleMsgDataView::getUnitFrostbiteRound(const UnitView& unit) const +{ + return getUnitFrostbiteRoundById(unit.getId()); +} + +int BattleMsgDataView::getUnitFrostbiteRoundById(const IdView& unitId) const +{ + const auto* info{game::BattleMsgDataApi::get().getUnitInfoById(battleMsgData, &unitId.id)}; + if (!info) { + return 0; + } + + return info->frostbiteAppliedRound; +} + +int BattleMsgDataView::getUnitBlisterRound(const UnitView& unit) const +{ + return getUnitBlisterRoundById(unit.getId()); +} + +int BattleMsgDataView::getUnitBlisterRoundById(const IdView& unitId) const +{ + const auto* info{game::BattleMsgDataApi::get().getUnitInfoById(battleMsgData, &unitId.id)}; + if (!info) { + return 0; + } + + return info->blisterAppliedRound; +} + +int BattleMsgDataView::getUnitTransformRound(const UnitView& unit) const +{ + return getUnitTransformRoundById(unit.getId()); +} + +int BattleMsgDataView::getUnitTransformRoundById(const IdView& unitId) const +{ + const auto* info{game::BattleMsgDataApi::get().getUnitInfoById(battleMsgData, &unitId.id)}; + if (!info) { + return 0; + } + + return info->transformAppliedRound; +} + std::optional BattleMsgDataView::getPlayer(const game::CMidgardID& playerId) const { auto player{hooks::getPlayer(objectMap, &playerId)}; diff --git a/mss32/src/bindings/battlemsgdataviewmutable.cpp b/mss32/src/bindings/battlemsgdataviewmutable.cpp new file mode 100644 index 00000000..a338a248 --- /dev/null +++ b/mss32/src/bindings/battlemsgdataviewmutable.cpp @@ -0,0 +1,59 @@ +/* + * This file is part of the modding toolset for Disciples 2. + * (https://github.com/VladimirMakeev/D2ModdingToolset) + * Copyright (C) 2023 Vladimir Makeev. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "battlemsgdataviewmutable.h" +#include "battlemsgdata.h" +#include "gameutils.h" +#include "idview.h" +#include "playerview.h" +#include "stackview.h" +#include "unitview.h" +#include + +namespace bindings { + +BattleMsgDataViewMutable::BattleMsgDataViewMutable(game::BattleMsgData* battleMsgData, + const game::IMidgardObjectMap* objectMap) + : BattleMsgDataView(battleMsgData, objectMap) + , battleMsgData{battleMsgData} + , objectMap{objectMap} +{ } + +void BattleMsgDataViewMutable::bind(sol::state& lua) +{ + auto view = lua.new_usertype("BattleViewMutable", sol::base_classes, + sol::bases()); + bindAccessMethods(view); + + view["setRetreatStatus"] = &BattleMsgDataViewMutable::setRetreatStatus; + view["setDecidedToRetreat"] = &BattleMsgDataViewMutable::setDecidedToRetreat; +} + +void BattleMsgDataViewMutable::setRetreatStatus(bool attacker, std::uint8_t value) +{ + game::BattleMsgDataApi::get().setRetreatStatus(battleMsgData, attacker, + static_cast(value & 3u)); +} + +void BattleMsgDataViewMutable::setDecidedToRetreat() +{ + game::BattleMsgDataApi::get().setRetreatDecisionWasMade(battleMsgData); +} + +} // namespace bindings diff --git a/mss32/src/bindings/buildingview.cpp b/mss32/src/bindings/buildingview.cpp new file mode 100644 index 00000000..18b15008 --- /dev/null +++ b/mss32/src/bindings/buildingview.cpp @@ -0,0 +1,101 @@ +/* + * This file is part of the modding toolset for Disciples 2. + * (https://github.com/VladimirMakeev/D2ModdingToolset) + * Copyright (C) 2024 Vladimir Makeev. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "buildingview.h" +#include "buildingtype.h" +#include "globaldata.h" +#include + +namespace bindings { + +BuildingView::BuildingView(const game::TBuildingType* building) + : building{building} +{ } + +void BuildingView::bind(sol::state& lua) +{ + auto view = lua.new_usertype("BuildingView"); + view["id"] = sol::property(&BuildingView::getId); + view["cost"] = sol::property(&BuildingView::getCost); + view["type"] = sol::property(&BuildingView::getCategory); + view["requiredBuilding"] = sol::property(&BuildingView::getRequiredBuilding); + view["branch"] = sol::property(&BuildingView::getUnitBranch); + view["level"] = sol::property(&BuildingView::getLevel); +} + +IdView BuildingView::getId() const +{ + return IdView{building->id}; +} + +CurrencyView BuildingView::getCost() const +{ + return CurrencyView{building->data->cost}; +} + +int BuildingView::getCategory() const +{ + return static_cast(building->data->category.id); +} + +std::optional BuildingView::getRequiredBuilding() const +{ + using namespace game; + + const CMidgardID& requiredId{building->data->requiredId}; + if (requiredId == emptyId || requiredId == invalidId) { + return std::nullopt; + } + + const auto& globalApi{GlobalDataApi::get()}; + const GlobalData* global{*globalApi.getGlobalData()}; + + auto requiredBuilding{(const TBuildingType*)globalApi.findById(global->buildings, &requiredId)}; + if (!requiredBuilding) { + return std::nullopt; + } + + return BuildingView{requiredBuilding}; +} + +int BuildingView::getUnitBranch() const +{ + using namespace game; + + if (building->data->category.id != BuildingCategories::get().unit->id) { + return emptyCategoryId; + } + + auto unitBuilding{reinterpret_cast(building)}; + return static_cast(unitBuilding->branch.id); +} + +int BuildingView::getLevel() const +{ + using namespace game; + + if (building->data->category.id != BuildingCategories::get().unit->id) { + return 0; + } + + auto unitBuilding{reinterpret_cast(building)}; + return unitBuilding->level; +} + +} // namespace bindings diff --git a/mss32/src/bindings/crystalview.cpp b/mss32/src/bindings/crystalview.cpp new file mode 100644 index 00000000..6a2beeac --- /dev/null +++ b/mss32/src/bindings/crystalview.cpp @@ -0,0 +1,54 @@ +/* + * This file is part of the modding toolset for Disciples 2. + * (https://github.com/VladimirMakeev/D2ModdingToolset) + * Copyright (C) 2024 Vladimir Makeev. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "crystalview.h" +#include "midcrystal.h" +#include + +namespace bindings { + +CrystalView::CrystalView(const game::CMidCrystal* crystal) + : crystal{crystal} +{ } + +void CrystalView::bind(sol::state& lua) +{ + auto view = lua.new_usertype("CrystalView"); + + view["id"] = sol::property(&CrystalView::getId); + view["resource"] = sol::property(&CrystalView::getResourceType); + view["position"] = sol::property(&CrystalView::getPosition); +} + +IdView CrystalView::getId() const +{ + return IdView{crystal->id}; +} + +int CrystalView::getResourceType() const +{ + return static_cast(crystal->resourceType.id); +} + +Point CrystalView::getPosition() const +{ + return Point{crystal->mapElement.position}; +} + +} // namespace bindings diff --git a/mss32/src/bindings/fortview.cpp b/mss32/src/bindings/fortview.cpp index 373d4a77..17a33dad 100644 --- a/mss32/src/bindings/fortview.cpp +++ b/mss32/src/bindings/fortview.cpp @@ -38,6 +38,7 @@ void FortView::bind(sol::state& lua) auto fortView = lua.new_usertype("FortView"); fortView["id"] = sol::property(&FortView::getId); fortView["position"] = sol::property(&FortView::getPosition); + fortView["entrance"] = sol::property(&FortView::getEntrance); fortView["owner"] = sol::property(&FortView::getOwner); fortView["group"] = sol::property(&FortView::getGroup); fortView["visitor"] = sol::property(&FortView::getVisitor); @@ -57,6 +58,14 @@ Point FortView::getPosition() const return Point{fort->mapElement.position}; } +Point FortView::getEntrance() const +{ + const auto& mapElement{fort->mapElement}; + const auto& position{mapElement.position}; + + return Point{hooks::getObjectEntrance(position, mapElement.sizeX, mapElement.sizeY)}; +} + std::optional FortView::getOwner() const { auto player = hooks::getPlayer(objectMap, &fort->ownerId); diff --git a/mss32/src/bindings/gameview.cpp b/mss32/src/bindings/gameview.cpp new file mode 100644 index 00000000..807966c5 --- /dev/null +++ b/mss32/src/bindings/gameview.cpp @@ -0,0 +1,63 @@ +/* + * This file is part of the modding toolset for Disciples 2. + * (https://github.com/VladimirMakeev/D2ModdingToolset) + * Copyright (C) 2023 Vladimir Makeev. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "gameview.h" +#include "restrictions.h" +#include "settings.h" +#include "version.h" +#include + +namespace bindings { + +void GameView::bind(sol::state& lua) +{ + auto view = lua.new_usertype("GameView"); + view["unitMaxDamage"] = sol::property(&GameView::getUnitMaxDamage); + view["unitMinDamage"] = sol::property(&GameView::getUnitMinDamage); + view["unitMaxArmor"] = sol::property(&GameView::getUnitMaxArmor); + view["leaderAdditionalDamage"] = sol::property(&GameView::getLeaderAdditionalDamage); + view["editor"] = sol::property(&GameView::isEditor); +} + +int GameView::getUnitMaxDamage() const +{ + return hooks::userSettings().unitMaxDamage; +} + +int GameView::getUnitMinDamage() const +{ + return game::gameRestrictions().attackDamage->min; +} + +int GameView::getLeaderAdditionalDamage() const +{ + return static_cast(*game::gameRestrictions().fighterExplorerLeaderBonusMaxDamage); +} + +int GameView::getUnitMaxArmor() const +{ + return hooks::userSettings().unitMaxArmor; +} + +bool GameView::isEditor() const +{ + return !hooks::executableIsGame(); +} + +} // namespace bindings diff --git a/mss32/src/bindings/globalvariablesview.cpp b/mss32/src/bindings/globalvariablesview.cpp new file mode 100644 index 00000000..496c0620 --- /dev/null +++ b/mss32/src/bindings/globalvariablesview.cpp @@ -0,0 +1,451 @@ +/* + * This file is part of the modding toolset for Disciples 2. + * (https://github.com/VladimirMakeev/D2ModdingToolset) + * Copyright (C) 2024 Vladimir Makeev. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "globalvariablesview.h" +#include "globalvariables.h" +#include + +namespace bindings { + +GlobalVariablesView::GlobalVariablesView(const game::GlobalVariables* variables) + : variables{variables} +{ } + +void GlobalVariablesView::bind(sol::state& lua) +{ + auto view = lua.new_usertype("GlobalVariablesView"); + + view["morale"] = &GlobalVariablesView::getMorale; + view["weapnMstr"] = sol::property(&GlobalVariablesView::getWeapnMstr); + + view["batInit"] = sol::property(&GlobalVariablesView::getBatInit); + view["batDamage"] = sol::property(&GlobalVariablesView::getBatDamage); + view["batRound"] = sol::property(&GlobalVariablesView::getBatRound); + view["batBreak"] = sol::property(&GlobalVariablesView::getBatBreak); + view["batBModif"] = sol::property(&GlobalVariablesView::getBatBModif); + view["batBoostd"] = &GlobalVariablesView::getBatBoostd; + view["batLowerd"] = &GlobalVariablesView::getBatLowerd; + view["batLoweri"] = sol::property(&GlobalVariablesView::getBatLoweri); + + view["ldrMaxAbil"] = sol::property(&GlobalVariablesView::getLdrMaxAbil); + view["spyDiscov"] = sol::property(&GlobalVariablesView::getSpyDiscov); + view["poisonC"] = sol::property(&GlobalVariablesView::getPoisonC); + view["poisonS"] = sol::property(&GlobalVariablesView::getPoisonS); + view["bribe"] = sol::property(&GlobalVariablesView::getBribe); + + view["stealRace"] = sol::property(&GlobalVariablesView::getStealRace); + view["stealNeut"] = sol::property(&GlobalVariablesView::getStealNeut); + + view["riotMin"] = sol::property(&GlobalVariablesView::getRiotMin); + view["riotMax"] = sol::property(&GlobalVariablesView::getRiotMax); + view["riotDmg"] = sol::property(&GlobalVariablesView::getRiotDmg); + + view["sellRatio"] = sol::property(&GlobalVariablesView::getSellRatio); + + view["tCapture"] = sol::property(&GlobalVariablesView::getTCapture); + view["tCapital"] = sol::property(&GlobalVariablesView::getTCapital); + view["tCity"] = &GlobalVariablesView::getTCity; + + view["rodCost"] = sol::property(&GlobalVariablesView::getRodCost); + view["rodRange"] = sol::property(&GlobalVariablesView::getRodRange); + + view["crystalP"] = sol::property(&GlobalVariablesView::getCrystalP); + view["constUrg"] = sol::property(&GlobalVariablesView::getConstUrg); + + view["regenLwar"] = sol::property(&GlobalVariablesView::getRegenLWar); + view["regenRuin"] = sol::property(&GlobalVariablesView::getRegenRuin); + + view["dPeace"] = sol::property(&GlobalVariablesView::getDPeace); + view["dWar"] = sol::property(&GlobalVariablesView::getDWar); + view["dNeutral"] = sol::property(&GlobalVariablesView::getDNeutral); + view["dGold"] = sol::property(&GlobalVariablesView::getDGold); + view["dMkAlly"] = sol::property(&GlobalVariablesView::getDMkAlly); + view["dAttakSc"] = sol::property(&GlobalVariablesView::getDAttackSc); + view["dAttakFo"] = sol::property(&GlobalVariablesView::getDAttackFo); + view["dAttakPc"] = sol::property(&GlobalVariablesView::getDAttackPc); + view["dRod"] = sol::property(&GlobalVariablesView::getDRod); + view["dRefAlly"] = sol::property(&GlobalVariablesView::getDRefAlly); + view["dBkAlly"] = sol::property(&GlobalVariablesView::getDBkAlly); + view["dNoble"] = sol::property(&GlobalVariablesView::getDNoble); + view["dBkaChnc"] = sol::property(&GlobalVariablesView::getDBkaChnc); + view["dBkaTurn"] = sol::property(&GlobalVariablesView::getDBkaTurn); + + view["prot"] = &GlobalVariablesView::getProt; + view["protCap"] = sol::property(&GlobalVariablesView::getProtCap); + + view["bonusE"] = sol::property(&GlobalVariablesView::getBonusE); + view["bonusA"] = sol::property(&GlobalVariablesView::getBonusA); + view["bonusH"] = sol::property(&GlobalVariablesView::getBonusH); + view["bonusV"] = sol::property(&GlobalVariablesView::getBonusV); + + view["incomeE"] = sol::property(&GlobalVariablesView::getIncomeE); + view["incomeA"] = sol::property(&GlobalVariablesView::getIncomeA); + view["incomeH"] = sol::property(&GlobalVariablesView::getIncomeH); + view["incomeV"] = sol::property(&GlobalVariablesView::getIncomeV); + + view["guRange"] = sol::property(&GlobalVariablesView::getGuRange); + view["paRange"] = sol::property(&GlobalVariablesView::getPaRange); + view["loRange"] = sol::property(&GlobalVariablesView::getLoRange); + + view["defendBonus"] = sol::property(&GlobalVariablesView::getDefendBonus); + view["talisChrg"] = sol::property(&GlobalVariablesView::getTalisChrg); + + view["splPwr"] = &GlobalVariablesView::getSplpwr; + view["gainSpell"] = sol::property(&GlobalVariablesView::getGainSpell); +} + +int GlobalVariablesView::getMorale(int tier) const +{ + if (tier < 1 || tier > 6) { + return 0; + } + + return variables->data->morale[tier - 1]; +} + +int GlobalVariablesView::getWeapnMstr() const +{ + return variables->data->weaponMaster; +} + +int GlobalVariablesView::getBatInit() const +{ + return variables->data->additionalBattleIni; +} + +int GlobalVariablesView::getBatDamage() const +{ + return variables->data->additionalBattleDmg; +} + +int GlobalVariablesView::getBatRound() const +{ + return variables->data->batRound; +} + +int GlobalVariablesView::getBatBreak() const +{ + return variables->data->batBreak; +} + +int GlobalVariablesView::getBatBModif() const +{ + return variables->data->batBmodif; +} + +int GlobalVariablesView::getBatBoostd(int level) const +{ + if (level < 1 || level > 4) { + return 0; + } + + return variables->data->battleBoostDamage[level - 1]; +} + +int GlobalVariablesView::getBatLowerd(int level) const +{ + if (level < 1 || level > 2) { + return 0; + } + + return variables->data->battleLowerDamage[level - 1]; +} + +int GlobalVariablesView::getBatLoweri() const +{ + return variables->data->battleLowerIni; +} + +int GlobalVariablesView::getLdrMaxAbil() const +{ + return variables->data->maxLeaderAbilities; +} + +int GlobalVariablesView::getSpyDiscov() const +{ + return variables->data->spyDiscoveryChance; +} + +int GlobalVariablesView::getPoisonS() const +{ + return variables->data->poisonStackDamage; +} + +int GlobalVariablesView::getPoisonC() const +{ + return variables->data->poisonCityDamage; +} + +int GlobalVariablesView::getBribe() const +{ + return variables->data->bribeMultiplier; +} + +int GlobalVariablesView::getStealRace() const +{ + return variables->data->stealRace; +} + +int GlobalVariablesView::getStealNeut() const +{ + return variables->data->stealNeut; +} + +int GlobalVariablesView::getRiotMin() const +{ + return variables->data->riotDaysMin; +} + +int GlobalVariablesView::getRiotMax() const +{ + return variables->data->riotDaysMax; +} + +int GlobalVariablesView::getRiotDmg() const +{ + return variables->data->riotDamage; +} + +int GlobalVariablesView::getSellRatio() const +{ + return variables->data->sellRatio; +} + +int GlobalVariablesView::getTCapture() const +{ + return variables->data->landTransformCapture; +} + +int GlobalVariablesView::getTCapital() const +{ + return variables->data->landTransformCapital; +} + +int GlobalVariablesView::getTCity(int tier) const +{ + if (tier < 1 || tier > 5) { + return 0; + } + + return variables->data->landTransformCity[tier - 1]; +} + +CurrencyView GlobalVariablesView::getRodCost() const +{ + return CurrencyView{variables->data->rodPlacementCost}; +} + +int GlobalVariablesView::getRodRange() const +{ + return variables->data->rodRange; +} + +int GlobalVariablesView::getCrystalP() const +{ + return variables->data->crystalProfit; +} + +int GlobalVariablesView::getConstUrg() const +{ + return variables->data->constUrg; +} + +int GlobalVariablesView::getRegenLWar() const +{ + return variables->data->fighterLeaderRegen; +} + +int GlobalVariablesView::getRegenRuin() const +{ + return variables->data->regenRuin; +} + +int GlobalVariablesView::getDPeace() const +{ + return variables->data->diplomacyPeace; +} + +int GlobalVariablesView::getDWar() const +{ + return variables->data->diplomacyWar; +} + +int GlobalVariablesView::getDNeutral() const +{ + return variables->data->diplomacyNeutral; +} + +int GlobalVariablesView::getDGold() const +{ + return variables->data->dGold; +} + +int GlobalVariablesView::getDMkAlly() const +{ + return variables->data->dMkAlly; +} + +int GlobalVariablesView::getDAttackSc() const +{ + return variables->data->dAttackSc; +} + +int GlobalVariablesView::getDAttackFo() const +{ + return variables->data->dAttackFo; +} + +int GlobalVariablesView::getDAttackPc() const +{ + return variables->data->dAttackPc; +} + +int GlobalVariablesView::getDRod() const +{ + return variables->data->dRod; +} + +int GlobalVariablesView::getDRefAlly() const +{ + return variables->data->dRefAlly; +} + +int GlobalVariablesView::getDBkAlly() const +{ + return variables->data->dBkAlly; +} + +int GlobalVariablesView::getDNoble() const +{ + return variables->data->dNoble; +} + +int GlobalVariablesView::getDBkaChnc() const +{ + return variables->data->dBkaChance; +} + +int GlobalVariablesView::getDBkaTurn() const +{ + return variables->data->dBkaTurn; +} + +int GlobalVariablesView::getProt(int tier) const +{ + if (tier < 1 || tier > 6) { + return 0; + } + + if (tier == 6) { + return getProtCap(); + } + + return variables->data->cityProtection[tier - 1]; +} + +int GlobalVariablesView::getProtCap() const +{ + return variables->data->capitalProtection; +} + +int GlobalVariablesView::getBonusE() const +{ + return variables->data->bonusGoldEasy; +} + +int GlobalVariablesView::getBonusA() const +{ + return variables->data->bonusGoldAverage; +} + +int GlobalVariablesView::getBonusH() const +{ + return variables->data->bonusGoldHard; +} + +int GlobalVariablesView::getBonusV() const +{ + return variables->data->bonusGoldVeryHard; +} + +int GlobalVariablesView::getIncomeE() const +{ + return variables->data->incomeEasy; +} + +int GlobalVariablesView::getIncomeA() const +{ + return variables->data->incomeAverage; +} + +int GlobalVariablesView::getIncomeH() const +{ + return variables->data->incomeHard; +} + +int GlobalVariablesView::getIncomeV() const +{ + return variables->data->incomeVeryHard; +} + +int GlobalVariablesView::getGuRange() const +{ + return variables->data->guRange; +} + +int GlobalVariablesView::getPaRange() const +{ + return variables->data->paRange; +} + +int GlobalVariablesView::getLoRange() const +{ + return variables->data->loRange; +} + +int GlobalVariablesView::getDfendBonus() const +{ + return variables->data->defendBonus; +} + +int GlobalVariablesView::getTalisChrg() const +{ + return variables->data->talismanCharges; +} + +int GlobalVariablesView::getSplpwr(int level) const +{ + if (level < 1 || level > 5) { + return 0; + } + + return variables->data->spellPower[level - 1]; +} + +int GlobalVariablesView::getGainSpell() const +{ + return variables->data->gainSpellChance; +} + +int GlobalVariablesView::getDefendBonus() const +{ + return variables->data->defendBonus; +} + +} // namespace bindings diff --git a/mss32/src/bindings/globalview.cpp b/mss32/src/bindings/globalview.cpp new file mode 100644 index 00000000..38b9e101 --- /dev/null +++ b/mss32/src/bindings/globalview.cpp @@ -0,0 +1,42 @@ +/* + * This file is part of the modding toolset for Disciples 2. + * (https://github.com/VladimirMakeev/D2ModdingToolset) + * Copyright (C) 2024 Vladimir Makeev. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "globalview.h" +#include "globaldata.h" +#include + +namespace bindings { + +void GlobalView::bind(sol::state& lua) +{ + auto view = lua.new_usertype("GlobalView"); + + view["variables"] = sol::property(&GlobalView::getGlobalVariables); +} + +GlobalVariablesView GlobalView::getGlobalVariables() const +{ + using namespace game; + + const GlobalData* global = *GlobalDataApi::get().getGlobalData(); + + return GlobalVariablesView{global->globalVariables}; +} + +} // namespace bindings diff --git a/mss32/src/bindings/groupview.cpp b/mss32/src/bindings/groupview.cpp index bf409d26..c9b8da97 100644 --- a/mss32/src/bindings/groupview.cpp +++ b/mss32/src/bindings/groupview.cpp @@ -18,8 +18,14 @@ */ #include "groupview.h" +#include "fortification.h" #include "game.h" +#include "gameutils.h" #include "idview.h" +#include "midgardobjectmap.h" +#include "midruin.h" +#include "midstack.h" +#include "midsubrace.h" #include "midunitgroup.h" #include "unitslotview.h" #include "unitview.h" @@ -42,6 +48,9 @@ void GroupView::bind(sol::state& lua) group["slots"] = sol::property(&GroupView::getSlots); group["units"] = sol::property(&GroupView::getUnits); group["hasUnit"] = sol::overload<>(&GroupView::hasUnit, &GroupView::hasUnitById); + group["getUnitPosition"] = sol::overload<>(&GroupView::getUnitPosition, + &GroupView::getUnitPositionById); + group["subrace"] = sol::property(&GroupView::getSubrace); } IdView GroupView::getId() const @@ -105,4 +114,40 @@ bool GroupView::hasUnitById(const bindings::IdView& unitId) const return false; } +int GroupView::getUnitPosition(const bindings::UnitView& unit) const +{ + return getUnitPositionById(unit.getId()); +} + +int GroupView::getUnitPositionById(const bindings::IdView& unitId) const +{ + return game::CMidUnitGroupApi::get().getUnitPosition(group, &unitId.id); +} + +int GroupView::getSubrace() const +{ + using namespace game; + + const auto type = CMidgardIDApi::get().getType(&groupId); + switch (type) { + case IdType::Stack: { + const CMidStack* stack{hooks::getStack(objectMap, &groupId)}; + + auto obj{objectMap->vftable->findScenarioObjectById(objectMap, &stack->subraceId)}; + const CMidSubRace* subrace = static_cast(obj); + return static_cast(subrace->subraceCategory.id); + } + + case IdType::Fortification: { + const CFortification* fort{hooks::getFort(objectMap, &groupId)}; + + auto obj{objectMap->vftable->findScenarioObjectById(objectMap, &fort->subraceId)}; + const CMidSubRace* subrace = static_cast(obj); + return static_cast(subrace->subraceCategory.id); + } + } + + return game::emptyCategoryId; +} + } // namespace bindings diff --git a/mss32/src/bindings/idview.cpp b/mss32/src/bindings/idview.cpp index 0ae7344a..87c85682 100644 --- a/mss32/src/bindings/idview.cpp +++ b/mss32/src/bindings/idview.cpp @@ -55,8 +55,10 @@ void IdView::bind(sol::state& lua) sol::constructors()); id["value"] = sol::property(&IdView::getValue); + id["type"] = sol::property(&IdView::getType); id["typeIndex"] = sol::property(&IdView::getTypeIndex); id["emptyId"] = IdView::getEmptyId; + id["summonId"] = IdView::getSummonId; } int IdView::getValue() const @@ -64,6 +66,11 @@ int IdView::getValue() const return id.value; } +int IdView::getType() const +{ + return static_cast(game::CMidgardIDApi::get().getType(&id)); +} + int IdView::getTypeIndex() const { // Duplicates impl of CMidgardIDApi::GetTypeIndex for better performance @@ -75,4 +82,14 @@ IdView IdView::getEmptyId() return IdView{game::emptyId}; } +IdView IdView::getSummonId(int position) +{ + using namespace game; + + CMidgardID summonId = emptyId; + CMidgardIDApi::get().summonUnitIdFromPosition(&summonId, position); + + return IdView{summonId}; +} + } // namespace bindings diff --git a/mss32/src/bindings/locationview.cpp b/mss32/src/bindings/locationview.cpp index 3d29efe1..1854612e 100644 --- a/mss32/src/bindings/locationview.cpp +++ b/mss32/src/bindings/locationview.cpp @@ -33,6 +33,7 @@ void LocationView::bind(sol::state& lua) location["id"] = sol::property(&LocationView::getId); location["position"] = sol::property(&LocationView::getPosition); location["radius"] = sol::property(&LocationView::getRadius); + location["name"] = sol::property(&LocationView::getName); } IdView LocationView::getId() const @@ -51,4 +52,10 @@ int LocationView::getRadius() const return location->radius * 2 + 1; } +std::string LocationView::getName() const +{ + const auto& name{location->name}; + return name.string ? name.string : ""; +} + } // namespace bindings diff --git a/mss32/src/bindings/merchantview.cpp b/mss32/src/bindings/merchantview.cpp new file mode 100644 index 00000000..08560b38 --- /dev/null +++ b/mss32/src/bindings/merchantview.cpp @@ -0,0 +1,92 @@ +/* + * This file is part of the modding toolset for Disciples 2. + * (https://github.com/VladimirMakeev/D2ModdingToolset) + * Copyright (C) 2024 Vladimir Makeev. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "merchantview.h" +#include "globaldata.h" +#include "itemutils.h" +#include "midsitemerchant.h" +#include + +namespace bindings { + +MerchantItemView::MerchantItemView(const game::CMidgardID& globalItemId, int amount) + : globalItemId{globalItemId} + , amount{amount} +{ } + +void MerchantItemView::bind(sol::state& lua) +{ + auto view = lua.new_usertype("MerchantItemView"); + + view["base"] = sol::property(&MerchantItemView::getItemBase); + view["amount"] = sol::property(&MerchantItemView::getAmount); +} + +ItemBaseView MerchantItemView::getItemBase() const +{ + using namespace game; + + const auto& global = GlobalDataApi::get(); + auto* globalData = *global.getGlobalData(); + + const auto* globalItem = global.findItemById(globalData->itemTypes, &globalItemId); + return ItemBaseView{globalItem}; +} + +int MerchantItemView::getAmount() +{ + return amount; +} + +MerchantView::MerchantView(const game::CMidSiteMerchant* merchant, + const game::IMidgardObjectMap* objectMap) + : SiteView(merchant, objectMap) +{ } + +void MerchantView::bind(sol::state& lua) +{ + auto view = lua.new_usertype("MerchantView", sol::base_classes, + sol::bases()); + bindAccessMethods(view); + + view["items"] = sol::property(&MerchantView::getItems); + view["temple"] = sol::property(&MerchantView::isMission); +} + +std::vector MerchantView::getItems() const +{ + const auto* merchant = static_cast(site); + + std::vector items; + items.reserve(merchant->items.length); + + for (const auto& [itemId, amount] : merchant->items) { + items.emplace_back(itemId, amount); + } + + return items; +} + +bool MerchantView::isMission() const +{ + const auto* merchant = static_cast(site); + return merchant->mission; +} + +} // namespace bindings diff --git a/mss32/src/bindings/mercsview.cpp b/mss32/src/bindings/mercsview.cpp new file mode 100644 index 00000000..632a3748 --- /dev/null +++ b/mss32/src/bindings/mercsview.cpp @@ -0,0 +1,78 @@ +/* + * This file is part of the modding toolset for Disciples 2. + * (https://github.com/VladimirMakeev/D2ModdingToolset) + * Copyright (C) 2024 Vladimir Makeev. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "mercsview.h" +#include "midsitemercs.h" +#include "unitutils.h" +#include "usunitimpl.h" +#include + +namespace bindings { + +MercenaryUnitView::MercenaryUnitView(const game::CMidgardID& unitImplId, bool unique) + : unitImplId{unitImplId} + , unique{unique} +{ } + +void MercenaryUnitView::bind(sol::state& lua) +{ + auto view = lua.new_usertype("MercenaryUnitView"); + + view["impl"] = sol::property(&MercenaryUnitView::getImpl); + view["unique"] = sol::property(&MercenaryUnitView::isUnique); +} + +UnitImplView MercenaryUnitView::getImpl() const +{ + return UnitImplView{hooks::getGlobalUnitImpl(&unitImplId)}; +} + +bool MercenaryUnitView::isUnique() const +{ + return unique; +} + +MercsView::MercsView(const game::CMidSiteMercs* mercs, const game::IMidgardObjectMap* objectMap) + : SiteView(mercs, objectMap) +{ } + +void MercsView::bind(sol::state& lua) +{ + auto view = lua.new_usertype("MercsView", sol::base_classes, sol::bases()); + + bindAccessMethods(view); + + view["units"] = sol::property(&MercsView::getUnits); +} + +std::vector MercsView::getUnits() const +{ + const auto* mercs = static_cast(site); + + std::vector units; + units.reserve(mercs->units.length); + + for (const auto& mercUnit : mercs->units) { + units.emplace_back(mercUnit.unitId, mercUnit.unique); + } + + return units; +} + +} // namespace bindings diff --git a/mss32/src/bindings/playerview.cpp b/mss32/src/bindings/playerview.cpp index f56177bf..6af14f2b 100644 --- a/mss32/src/bindings/playerview.cpp +++ b/mss32/src/bindings/playerview.cpp @@ -23,6 +23,7 @@ #include "globaldata.h" #include "lordtype.h" #include "midplayer.h" +#include "playerbuildings.h" #include "racetype.h" #include @@ -43,6 +44,8 @@ void PlayerView::bind(sol::state& lua) view["human"] = sol::property(&PlayerView::isHuman); view["alwaysAi"] = sol::property(&PlayerView::isAlwaysAi); view["fog"] = sol::property(&PlayerView::getFog); + view["buildings"] = sol::property(&PlayerView::getBuildings); + view["hasBuilding"] = sol::overload<>(&PlayerView::hasBuilding, &PlayerView::hasBuildingById); } IdView PlayerView::getId() const @@ -98,4 +101,56 @@ std::optional PlayerView::getFog() const return FogView{fog}; } +std::vector PlayerView::getBuildings() const +{ + using namespace game; + + std::vector buildings; + + auto playerBuildings{hooks::getPlayerBuildings(objectMap, player)}; + if (!playerBuildings) { + return buildings; + } + + const auto& buildingsList{playerBuildings->buildings}; + + const auto& globalApi{GlobalDataApi::get()}; + const GlobalData* global{*globalApi.getGlobalData()}; + + buildings.reserve(buildingsList.length); + for (auto node = buildingsList.head->next; node != buildingsList.head; node = node->next) { + auto buildingType{(const TBuildingType*)globalApi.findById(global->buildings, &node->data)}; + if (!buildingType) { + continue; + } + + buildings.push_back(BuildingView{buildingType}); + } + + return buildings; +} + +bool PlayerView::hasBuilding(const std::string& id) const +{ + return hasBuildingById(IdView{id}); +} + +bool PlayerView::hasBuildingById(const IdView& id) const +{ + auto playerBuildings{hooks::getPlayerBuildings(objectMap, player)}; + if (!playerBuildings) { + return false; + } + + const auto& buildings{playerBuildings->buildings}; + + for (auto node = buildings.head->next; node != buildings.head; node = node->next) { + if (node->data == id.id) { + return true; + } + } + + return false; +} + } // namespace bindings diff --git a/mss32/src/bindings/resourcemarketview.cpp b/mss32/src/bindings/resourcemarketview.cpp new file mode 100644 index 00000000..d004a4ee --- /dev/null +++ b/mss32/src/bindings/resourcemarketview.cpp @@ -0,0 +1,62 @@ +/* + * This file is part of the modding toolset for Disciples 2. + * (https://github.com/VladimirMakeev/D2ModdingToolset) + * Copyright (C) 2024 Vladimir Makeev. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "resourcemarketview.h" +#include "midsiteresourcemarket.h" +#include + +namespace bindings { + +ResourceMarketView::ResourceMarketView(const hooks::CMidSiteResourceMarket* market, + const game::IMidgardObjectMap* objectMap) + : SiteView(market, objectMap) +{ } + +void ResourceMarketView::bind(sol::state& lua) +{ + auto view = lua.new_usertype("ResourceMarketView", sol::base_classes, + sol::bases()); + bindAccessMethods(view); + + view["isInfinite"] = &ResourceMarketView::isResourceInfinite; + view["stock"] = sol::property(&ResourceMarketView::getStock); + view["customRates"] = sol::property(&ResourceMarketView::hasCustomExchangeRates); +} + +bool ResourceMarketView::isResourceInfinite(game::CurrencyType type) const +{ + return hooks::isMarketStockInfinite(getMarket()->infiniteStock, type); +} + +CurrencyView ResourceMarketView::getStock() const +{ + return CurrencyView{getMarket()->stock}; +} + +bool ResourceMarketView::hasCustomExchangeRates() const +{ + return getMarket()->customExchangeRates; +} + +const hooks::CMidSiteResourceMarket* ResourceMarketView::getMarket() const +{ + return static_cast(site); +} + +} // namespace bindings diff --git a/mss32/src/bindings/scenarioview.cpp b/mss32/src/bindings/scenarioview.cpp index 70179f39..a7b3c20b 100644 --- a/mss32/src/bindings/scenarioview.cpp +++ b/mss32/src/bindings/scenarioview.cpp @@ -18,24 +18,41 @@ */ #include "scenarioview.h" +#include "crystalview.h" #include "diplomacyview.h" #include "dynamiccast.h" +#include "fortification.h" #include "fortview.h" #include "gameutils.h" #include "idview.h" +#include "itemview.h" #include "locationview.h" +#include "merchantview.h" +#include "mercsview.h" +#include "midcrystal.h" #include "midgardmapblock.h" #include "midgardobjectmap.h" #include "midgardplan.h" +#include "midplayer.h" +#include "midrod.h" +#include "midruin.h" #include "midscenvariables.h" +#include "midsitemerchant.h" +#include "midsitemercs.h" +#include "midsiteresourcemarket.h" +#include "midsitetrainer.h" +#include "midunit.h" #include "playerview.h" #include "point.h" +#include "resourcemarketview.h" #include "rodview.h" #include "ruinview.h" #include "scenarioinfo.h" #include "scenvariablesview.h" +#include "sitecategoryhooks.h" #include "stackview.h" #include "tileview.h" +#include "trainerview.h" #include "unitview.h" #include @@ -66,6 +83,26 @@ void ScenarioView::bind(sol::state& lua) &ScenarioView::getRodByPoint); scenario["getPlayer"] = sol::overload<>(&ScenarioView::getPlayer, &ScenarioView::getPlayerById); scenario["getUnit"] = sol::overload<>(&ScenarioView::getUnit, &ScenarioView::getUnitById); + scenario["getItem"] = sol::overload<>(&ScenarioView::getItem, &ScenarioView::getItemById); + scenario["getCrystal"] = sol::overload<>(&ScenarioView::getCrystal, + &ScenarioView::getCrystalById, + &ScenarioView::getCrystalByCoordinates, + &ScenarioView::getCrystalByPoint); + scenario["getMerchant"] = sol::overload<>(&ScenarioView::getMerchant, + &ScenarioView::getMerchantById, + &ScenarioView::getMerchantByCoordinates, + &ScenarioView::getMerchantByPoint); + scenario["getMercenary"] = sol::overload<>(&ScenarioView::getMercs, &ScenarioView::getMercsById, + &ScenarioView::getMercsByCoordinates, + &ScenarioView::getMercsByPoint); + scenario["getTrainer"] = sol::overload<>(&ScenarioView::getTrainer, + &ScenarioView::getTrainerById, + &ScenarioView::getTrainerByCoordinates, + &ScenarioView::getTrainerByPoint); + scenario["getMarket"] = sol::overload<>(&ScenarioView::getMarket, &ScenarioView::getMarketById, + &ScenarioView::getMarketByCoordinates, + &ScenarioView::getMarketByPoint); + scenario["findStackByUnit"] = sol::overload<>(&ScenarioView::findStackByUnit, &ScenarioView::findStackByUnitId, &ScenarioView::findStackByUnitIdString); @@ -75,9 +112,26 @@ void ScenarioView::bind(sol::state& lua) scenario["findRuinByUnit"] = sol::overload<>(&ScenarioView::findRuinByUnit, &ScenarioView::findRuinByUnitId, &ScenarioView::findRuinByUnitIdString); + scenario["name"] = sol::property(&ScenarioView::getName); + scenario["description"] = sol::property(&ScenarioView::getDescription); + scenario["author"] = sol::property(&ScenarioView::getAuthor); + scenario["seed"] = sol::property(&ScenarioView::getSeed); scenario["day"] = sol::property(&ScenarioView::getCurrentDay); scenario["size"] = sol::property(&ScenarioView::getSize); + scenario["difficulty"] = sol::property(&ScenarioView::getDifficulty); scenario["diplomacy"] = sol::property(&ScenarioView::getDiplomacy); + scenario["forEachStack"] = &ScenarioView::forEachStack; + scenario["forEachLocation"] = &ScenarioView::forEachLocation; + scenario["forEachFort"] = &ScenarioView::forEachFort; + scenario["forEachRuin"] = &ScenarioView::forEachRuin; + scenario["forEachRod"] = &ScenarioView::forEachRod; + scenario["forEachPlayer"] = &ScenarioView::forEachPlayer; + scenario["forEachUnit"] = &ScenarioView::forEachUnit; + scenario["forEachCrystal"] = &ScenarioView::forEachCrystal; + scenario["forEachMerchant"] = &ScenarioView::forEachMerchant; + scenario["forEachMercenary"] = &ScenarioView::forEachMercenary; + scenario["forEachTrainer"] = &ScenarioView::forEachTrainer; + scenario["forEachMarket"] = &ScenarioView::forEachMarket; } std::optional ScenarioView::getLocation(const std::string& id) const @@ -440,6 +494,291 @@ std::optional ScenarioView::getUnitById(const IdView& id) const return {UnitView{(const CMidUnit*)obj}}; } +std::optional ScenarioView::getItem(const std::string& id) const +{ + return getItemById(IdView{id}); +} + +std::optional ScenarioView::getItemById(const IdView& id) const +{ + using namespace game; + + if (!objectMap) { + return std::nullopt; + } + + if (CMidgardIDApi::get().getType(&id.id) != IdType::Item) { + return std::nullopt; + } + + auto obj = objectMap->vftable->findScenarioObjectById(objectMap, &id.id); + if (!obj) { + return std::nullopt; + } + + return {ItemView{&id.id, objectMap}}; +} + +std::optional ScenarioView::getCrystal(const std::string& id) const +{ + return getCrystalById(IdView{id}); +} + +std::optional ScenarioView::getCrystalById(const IdView& id) const +{ + using namespace game; + + if (!objectMap) { + return std::nullopt; + } + + if (CMidgardIDApi::get().getType(&id.id) != IdType::Crystal) { + return std::nullopt; + } + + auto obj = objectMap->vftable->findScenarioObjectById(objectMap, &id.id); + if (!obj) { + return std::nullopt; + } + + return {CrystalView{static_cast(obj)}}; +} + +std::optional ScenarioView::getCrystalByCoordinates(int x, int y) const +{ + auto crystalId = getObjectId(x, y, game::IdType::Crystal); + if (!crystalId) { + return std::nullopt; + } + + return getCrystalById(IdView{crystalId}); +} + +std::optional ScenarioView::getCrystalByPoint(const Point& p) const +{ + return getCrystalByCoordinates(p.x, p.y); +} + +std::optional ScenarioView::getMerchant(const std::string& id) const +{ + return getMerchantById(IdView{id}); +} + +std::optional ScenarioView::getMerchantById(const IdView& id) const +{ + using namespace game; + + if (!objectMap) { + return std::nullopt; + } + + if (CMidgardIDApi::get().getType(&id.id) != IdType::Site) { + return std::nullopt; + } + + auto obj = objectMap->vftable->findScenarioObjectById(objectMap, &id.id); + if (!obj) { + return std::nullopt; + } + + auto site = static_cast(obj); + if (site->siteCategory.id != SiteCategories::get().merchant->id) { + return std::nullopt; + } + + return MerchantView{static_cast(site), objectMap}; +} + +std::optional ScenarioView::getMerchantByCoordinates(int x, int y) const +{ + auto merchantId = getObjectId(x, y, game::IdType::Site); + if (!merchantId) { + return std::nullopt; + } + + return getMerchantById(IdView{merchantId}); +} + +std::optional ScenarioView::getMerchantByPoint(const Point& p) const +{ + return getMerchantByCoordinates(p.x, p.y); +} + +std::optional ScenarioView::getMercs(const std::string& id) const +{ + return getMercsById(IdView{id}); +} + +std::optional ScenarioView::getMercsById(const IdView& id) const +{ + using namespace game; + + if (!objectMap) { + return std::nullopt; + } + + if (CMidgardIDApi::get().getType(&id.id) != IdType::Site) { + return std::nullopt; + } + + auto obj = objectMap->vftable->findScenarioObjectById(objectMap, &id.id); + if (!obj) { + return std::nullopt; + } + + auto site = static_cast(obj); + if (site->siteCategory.id != SiteCategories::get().mercenaries->id) { + return std::nullopt; + } + + return MercsView{static_cast(site), objectMap}; +} + +std::optional ScenarioView::getMercsByCoordinates(int x, int y) const +{ + auto mercenariesId = getObjectId(x, y, game::IdType::Site); + if (!mercenariesId) { + return std::nullopt; + } + + return getMercsById(IdView{mercenariesId}); +} + +std::optional ScenarioView::getMercsByPoint(const Point& p) const +{ + return getMercsByCoordinates(p.x, p.y); +} + +std::optional ScenarioView::getTrainer(const std::string& id) const +{ + return getTrainerById(IdView{id}); +} + +std::optional ScenarioView::getTrainerById(const IdView& id) const +{ + using namespace game; + + if (!objectMap) { + return std::nullopt; + } + + if (CMidgardIDApi::get().getType(&id.id) != IdType::Site) { + return std::nullopt; + } + + auto obj = objectMap->vftable->findScenarioObjectById(objectMap, &id.id); + if (!obj) { + return std::nullopt; + } + + auto site = static_cast(obj); + if (site->siteCategory.id != SiteCategories::get().trainer->id) { + return std::nullopt; + } + + return TrainerView{static_cast(site), objectMap}; +} + +std::optional ScenarioView::getTrainerByCoordinates(int x, int y) const +{ + auto trainerId = getObjectId(x, y, game::IdType::Site); + if (!trainerId) { + return std::nullopt; + } + + return getTrainerById(IdView{trainerId}); +} + +std::optional ScenarioView::getTrainerByPoint(const Point& p) const +{ + return getTrainerByCoordinates(p.x, p.y); +} + +std::optional ScenarioView::getMarket(const std::string& id) const +{ + return getMarketById(IdView{id}); +} + +std::optional ScenarioView::getMarketById(const IdView& id) const +{ + using namespace game; + + if (!objectMap) { + return std::nullopt; + } + + if (CMidgardIDApi::get().getType(&id.id) != IdType::Site) { + return std::nullopt; + } + + auto obj = objectMap->vftable->findScenarioObjectById(objectMap, &id.id); + if (!obj) { + return std::nullopt; + } + + auto site = static_cast(obj); + if (site->siteCategory.id != hooks::customSiteCategories().resourceMarket.id) { + return std::nullopt; + } + + return ResourceMarketView{static_cast(site), objectMap}; +} + +std::optional ScenarioView::getMarketByCoordinates(int x, int y) const +{ + auto marketId = getObjectId(x, y, game::IdType::Site); + if (!marketId) { + return std::nullopt; + } + + return getMarketById(IdView{marketId}); +} + +std::optional ScenarioView::getMarketByPoint(const Point& p) const +{ + return getMarketByCoordinates(p.x, p.y); +} + +std::string ScenarioView::getName() const +{ + if (!objectMap) { + return ""; + } + + auto info = hooks::getScenarioInfo(objectMap); + return info->name ? info->name : ""; +} + +std::string ScenarioView::getDescription() const +{ + if (!objectMap) { + return ""; + } + + auto info = hooks::getScenarioInfo(objectMap); + return info->description ? info->description : ""; +} + +std::string ScenarioView::getAuthor() const +{ + if (!objectMap) { + return ""; + } + + auto info = hooks::getScenarioInfo(objectMap); + return info->creator ? info->creator : ""; +} + +std::uint32_t ScenarioView::getSeed() const +{ + if (!objectMap) { + return 0u; + } + + auto info = hooks::getScenarioInfo(objectMap); + return static_cast(info->mapSeed); +} + int ScenarioView::getCurrentDay() const { if (!objectMap) { @@ -460,6 +799,16 @@ int ScenarioView::getSize() const return info ? info->mapSize : 0; } +int ScenarioView::getDifficulty() const +{ + if (!objectMap) { + return game::emptyCategoryId; + } + + auto info = hooks::getScenarioInfo(objectMap); + return info ? static_cast(info->gameDifficulty.id) : game::emptyCategoryId; +} + std::optional ScenarioView::getDiplomacy() const { if (!objectMap) { @@ -469,6 +818,256 @@ std::optional ScenarioView::getDiplomacy() const return DiplomacyView{hooks::getDiplomacy(objectMap)}; } +void ScenarioView::forEachStack(const std::function& callback) const +{ + if (!objectMap) { + return; + } + + using namespace game; + + const auto dynamicCast = RttiApi::get().dynamicCast; + const auto& rtti = RttiApi::rtti(); + + auto runCallback = [this, &callback, &dynamicCast, &rtti](const IMidScenarioObject* obj) { + auto* stack = (const CMidStack*)dynamicCast(obj, 0, rtti.IMidScenarioObjectType, + rtti.CMidStackType, 0); + + const StackView stackView{stack, objectMap}; + callback(stackView); + }; + + hooks::forEachScenarioObject(objectMap, IdType::Stack, runCallback); +} + +void ScenarioView::forEachLocation(const std::function& callback) const +{ + if (!objectMap) { + return; + } + + using namespace game; + + const auto dynamicCast = RttiApi::get().dynamicCast; + const auto& rtti = RttiApi::rtti(); + + auto runCallback = [&callback, &dynamicCast, &rtti](const IMidScenarioObject* obj) { + auto* location = (const CMidLocation*)dynamicCast(obj, 0, rtti.IMidScenarioObjectType, + rtti.CMidLocationType, 0); + + const LocationView locationView{location}; + callback(locationView); + }; + + hooks::forEachScenarioObject(objectMap, IdType::Location, runCallback); +} + +void ScenarioView::forEachFort(const std::function& callback) const +{ + if (!objectMap) { + return; + } + + using namespace game; + + auto runCallback = [this, &callback](const IMidScenarioObject* obj) { + auto* fort{static_cast(obj)}; + + const FortView fortView{fort, objectMap}; + callback(fortView); + }; + + hooks::forEachScenarioObject(objectMap, IdType::Fortification, runCallback); +} + +void ScenarioView::forEachRuin(const std::function& callback) const +{ + if (!objectMap) { + return; + } + + using namespace game; + + auto runCallback = [this, &callback](const IMidScenarioObject* obj) { + auto* ruin{static_cast(obj)}; + + const RuinView ruinView{ruin, objectMap}; + callback(ruinView); + }; + + hooks::forEachScenarioObject(objectMap, IdType::Ruin, runCallback); +} + +void ScenarioView::forEachRod(const std::function& callback) const +{ + if (!objectMap) { + return; + } + + using namespace game; + + auto runCallback = [this, &callback](const IMidScenarioObject* obj) { + auto* rod{static_cast(obj)}; + + const RodView rodView{rod, objectMap}; + callback(rodView); + }; + + hooks::forEachScenarioObject(objectMap, IdType::Rod, runCallback); +} + +void ScenarioView::forEachPlayer(const std::function& callback) const +{ + if (!objectMap) { + return; + } + + using namespace game; + + auto runCallback = [this, &callback](const IMidScenarioObject* obj) { + auto* player{static_cast(obj)}; + + const PlayerView playerView{player, objectMap}; + callback(playerView); + }; + + hooks::forEachScenarioObject(objectMap, IdType::Player, runCallback); +} + +void ScenarioView::forEachUnit(const std::function& callback) const +{ + if (!objectMap) { + return; + } + + using namespace game; + + auto runCallback = [&callback](const IMidScenarioObject* obj) { + auto* unit{static_cast(obj)}; + + const UnitView unitView{unit}; + callback(unitView); + }; + + hooks::forEachScenarioObject(objectMap, IdType::Unit, runCallback); +} + +void ScenarioView::forEachCrystal(const std::function& callback) const +{ + if (!objectMap) { + return; + } + + using namespace game; + + auto runCallback = [&callback](const IMidScenarioObject* obj) { + auto* crystal{static_cast(obj)}; + + const CrystalView crystalView{crystal}; + callback(crystalView); + }; + + hooks::forEachScenarioObject(objectMap, IdType::Crystal, runCallback); +} + +void ScenarioView::forEachMerchant(const std::function& callback) const +{ + if (!objectMap) { + return; + } + + using namespace game; + + const auto merchantId{SiteCategories::get().merchant->id}; + + auto runCallback = [this, &callback, &merchantId](const IMidScenarioObject* obj) { + const auto* site{static_cast(obj)}; + if (site->siteCategory.id != merchantId) { + return; + } + + const MerchantView view{static_cast(site), objectMap}; + callback(view); + }; + + hooks::forEachScenarioObject(objectMap, IdType::Site, runCallback); +} + +void ScenarioView::forEachMercenary(const std::function& callback) const +{ + if (!objectMap) { + return; + } + + using namespace game; + + const auto mercsId{SiteCategories::get().mercenaries->id}; + + auto runCallback = [this, &callback, &mercsId](const IMidScenarioObject* obj) { + const auto* site{static_cast(obj)}; + if (site->siteCategory.id != mercsId) { + return; + } + + const MercsView view{static_cast(site), objectMap}; + callback(view); + }; + + hooks::forEachScenarioObject(objectMap, IdType::Site, runCallback); +} + +void ScenarioView::forEachTrainer(const std::function& callback) const +{ + if (!objectMap) { + return; + } + + using namespace game; + + const auto trainerId{SiteCategories::get().trainer->id}; + + auto runCallback = [this, &callback, &trainerId](const IMidScenarioObject* obj) { + const auto* site{static_cast(obj)}; + if (site->siteCategory.id != trainerId) { + return; + } + + const TrainerView view{static_cast(site), objectMap}; + callback(view); + }; + + hooks::forEachScenarioObject(objectMap, IdType::Site, runCallback); +} + +void ScenarioView::forEachMarket( + const std::function& callback) const +{ + if (!objectMap) { + return; + } + + if (!hooks::customSiteCategories().exists) { + return; + } + + using namespace game; + + const auto marketId{hooks::customSiteCategories().resourceMarket.id}; + + auto runCallback = [this, &callback, &marketId](const IMidScenarioObject* obj) { + const auto* site{static_cast(obj)}; + if (site->siteCategory.id != marketId) { + return; + } + + const ResourceMarketView view{static_cast(site), + objectMap}; + callback(view); + }; + + hooks::forEachScenarioObject(objectMap, IdType::Site, runCallback); +} + const game::CMidgardID* ScenarioView::getObjectId(int x, int y, game::IdType type) const { using namespace game; diff --git a/mss32/src/bindings/siteview.cpp b/mss32/src/bindings/siteview.cpp new file mode 100644 index 00000000..141f9477 --- /dev/null +++ b/mss32/src/bindings/siteview.cpp @@ -0,0 +1,61 @@ +/* + * This file is part of the modding toolset for Disciples 2. + * (https://github.com/VladimirMakeev/D2ModdingToolset) + * Copyright (C) 2024 Vladimir Makeev. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "siteview.h" +#include "gameutils.h" +#include "midsite.h" +#include + +namespace bindings { + +SiteView::SiteView(const game::CMidSite* site, const game::IMidgardObjectMap* objectMap) + : site{site} + , objectMap{objectMap} +{ } + +void SiteView::bind(sol::state& lua) +{ + auto view = lua.new_usertype("SiteView"); + + bindAccessMethods(view); +} + +IdView SiteView::getId() const +{ + return IdView{site->id}; +} + +Point SiteView::getPosition() const +{ + return Point{site->mapElement.position}; +} + +std::vector SiteView::getVisitors() const +{ + std::vector visitors; + visitors.reserve(site->visitors.length); + + for (const auto& playerId : site->visitors) { + visitors.emplace_back(hooks::getPlayer(objectMap, &playerId), objectMap); + } + + return visitors; +} + +} // namespace bindings diff --git a/mss32/src/bindings/stackview.cpp b/mss32/src/bindings/stackview.cpp index 65d3b3f1..5e760a0f 100644 --- a/mss32/src/bindings/stackview.cpp +++ b/mss32/src/bindings/stackview.cpp @@ -51,6 +51,9 @@ void StackView::bind(sol::state& lua) stackView["inventory"] = sol::property(&StackView::getInventoryItems); stackView["getEquippedItem"] = &StackView::getLeaderEquippedItem; + stackView["order"] = sol::property(&StackView::getOrder); + stackView["orderTargetId"] = sol::property(&StackView::getOrderTargetId); + stackView["aiOrder"] = sol::property(&StackView::getAiOrder); } IdView StackView::getId() const @@ -150,4 +153,19 @@ std::optional StackView::getLeaderEquippedItem(const game::EquippedIte return {ItemView{&itemId, objectMap}}; } +int StackView::getOrder() const +{ + return static_cast(stack->order.id); +} + +IdView StackView::getOrderTargetId() const +{ + return IdView{stack->orderTargetId}; +} + +int StackView::getAiOrder() const +{ + return static_cast(stack->aiOrder.id); +} + } // namespace bindings diff --git a/mss32/src/bindings/trainerview.cpp b/mss32/src/bindings/trainerview.cpp new file mode 100644 index 00000000..6c1c2634 --- /dev/null +++ b/mss32/src/bindings/trainerview.cpp @@ -0,0 +1,39 @@ +/* + * This file is part of the modding toolset for Disciples 2. + * (https://github.com/VladimirMakeev/D2ModdingToolset) + * Copyright (C) 2024 Vladimir Makeev. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "trainerview.h" +#include "midsitetrainer.h" +#include + +namespace bindings { + +TrainerView::TrainerView(const game::CMidSiteTrainer* trainer, + const game::IMidgardObjectMap* objectMap) + : SiteView(trainer, objectMap) +{ } + +void TrainerView::bind(sol::state& lua) +{ + auto view = lua.new_usertype("TrainerView", sol::base_classes, + sol::bases()); + + bindAccessMethods(view); +} + +} // namespace bindings diff --git a/mss32/src/bindings/unitimplview.cpp b/mss32/src/bindings/unitimplview.cpp index 3be4b3f4..19303293 100644 --- a/mss32/src/bindings/unitimplview.cpp +++ b/mss32/src/bindings/unitimplview.cpp @@ -72,6 +72,7 @@ void UnitImplView::bind(sol::state& lua) impl["attack1"] = sol::property(&UnitImplView::getAttack); impl["attack2"] = sol::property(&UnitImplView::getAttack2); impl["altAttack"] = sol::property(&UnitImplView::getAltAttack); + impl["altAttack2"] = sol::property(&UnitImplView::getAltAttack2); impl["base"] = sol::property(&UnitImplView::getBaseUnit); impl["global"] = sol::property(&UnitImplView::getGlobal); @@ -414,6 +415,16 @@ std::optional UnitImplView::getAltAttack() const return AttackView{altAttack}; } +std::optional UnitImplView::getAltAttack2() const +{ + auto altAttack = hooks::getAltAttack(impl, false); + if (!altAttack) { + return std::nullopt; + } + + return AttackView{altAttack}; +} + int UnitImplView::getImmuneToAttackClass(int attackClassId) const { using namespace game; diff --git a/mss32/src/currency.cpp b/mss32/src/currency.cpp index cd1141b0..93c28b9f 100644 --- a/mss32/src/currency.cpp +++ b/mss32/src/currency.cpp @@ -36,6 +36,7 @@ static std::array functions = {{ (Api::SetInvalid)0x5870ac, (Api::SetZero)0x5879a7, (Api::SetCurrency)0x5878bc, + (Api::GetCurrency)0x58787b, (Api::IsZero)0x58797e, (Api::IsValid)0x58790f, (Api::ToString)0x5872d9, @@ -52,6 +53,7 @@ static std::array functions = {{ (Api::SetInvalid)0x5870ac, (Api::SetZero)0x5879a7, (Api::SetCurrency)0x5878bc, + (Api::GetCurrency)0x58787b, (Api::IsZero)0x58797e, (Api::IsValid)0x58790f, (Api::ToString)0x5872d9, @@ -68,6 +70,7 @@ static std::array functions = {{ (Api::SetInvalid)0x58625f, (Api::SetZero)0x586b5a, (Api::SetCurrency)0x586a6f, + (Api::GetCurrency)0x586a2e, (Api::IsZero)0x586b31, (Api::IsValid)0x586ac2, (Api::ToString)0x58648c, @@ -84,6 +87,7 @@ static std::array functions = {{ (Api::SetInvalid)0x52dae8, (Api::SetZero)0x52e233, (Api::SetCurrency)0x52e148, + (Api::GetCurrency)0x52e107, (Api::IsZero)0x52e20a, (Api::IsValid)0x52e19b, (Api::ToString)0x52dd15, diff --git a/mss32/src/customaibattle.cpp b/mss32/src/customaibattle.cpp new file mode 100644 index 00000000..698433da --- /dev/null +++ b/mss32/src/customaibattle.cpp @@ -0,0 +1,80 @@ +/* + * This file is part of the modding toolset for Disciples 2. + * (https://github.com/VladimirMakeev/D2ModdingToolset) + * Copyright (C) 2024 Vladimir Makeev. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "customaibattle.h" +#include "dbffile.h" +#include "log.h" +#include "utils.h" +#include + +namespace hooks { + +static CustomAiBattleLogic customAiBattleLogic; + +const CustomAiBattleLogic& getCustomAiBattleLogic() +{ + return customAiBattleLogic; +} + +void initializeCustomAiBattleLogic() +{ + const std::filesystem::path dbfFilePath{globalsFolder() / "GAI.dbf"}; + + utils::DbfFile dbf; + if (!dbf.open(dbfFilePath)) { + logError("mssProxyError.log", + fmt::format("Could not open {:s}", dbfFilePath.filename().string())); + return; + } + + static const char actionScriptColumnName[]{"ACTION_S"}; + + customAiBattleLogic.customBattleLogicEnabled = dbf.column(actionScriptColumnName) != nullptr; + if (!customAiBattleLogic.customBattleLogicEnabled) { + return; + } + + const auto recordsTotal{dbf.recordsTotal()}; + customAiBattleLogic.attitudeBattleLogic.reserve(recordsTotal); + + for (std::uint32_t i = 0u; i < recordsTotal; ++i) { + utils::DbfRecord record; + if (!dbf.record(record, i)) { + logError("mssProxyError.log", fmt::format("Could not read record {:d} from {:s}", i, + dbfFilePath.filename().string())); + return; + } + + if (record.isDeleted()) { + continue; + } + + int id{-1}; + record.value(id, "CATEGORY"); + const auto attitude{static_cast(id)}; + + std::string scriptPath; + record.value(scriptPath, actionScriptColumnName); + scriptPath = trimSpaces(scriptPath); + + customAiBattleLogic.attitudeBattleLogic[attitude] = std::move(scriptPath); + } +} + +} // namespace hooks diff --git a/mss32/src/customnobleactioncategories.cpp b/mss32/src/customnobleactioncategories.cpp new file mode 100644 index 00000000..964dbe37 --- /dev/null +++ b/mss32/src/customnobleactioncategories.cpp @@ -0,0 +1,117 @@ +/* + * This file is part of the modding toolset for Disciples 2. + * (https://github.com/VladimirMakeev/D2ModdingToolset) + * Copyright (C) 2024 Vladimir Makeev. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "customnobleactioncategories.h" +#include "dbffile.h" +#include "log.h" +#include "utils.h" +#include +#include +#include + +namespace hooks { + +static const char stealMarketActionCategoryName[]{"L_STEAL_MARKET"}; + +static CustomNobleActionCategories customCategories; + +static void checkCustomActionCategories(const std::filesystem::path& dbfFilePath) +{ + utils::DbfFile dbf; + if (!dbf.open(dbfFilePath)) { + logError("mssProxyError.log", + fmt::format("Could not open {:s}", dbfFilePath.filename().string())); + return; + } + + const std::uint32_t recordsTotal{dbf.recordsTotal()}; + for (std::uint32_t i = 0u; i < recordsTotal; ++i) { + utils::DbfRecord record; + if (!dbf.record(record, i)) { + continue; + } + + if (record.isDeleted()) { + continue; + } + + std::string text; + record.value(text, "TEXT"); + text = trimSpaces(text); + + if (text == stealMarketActionCategoryName) { + auto& customCategories{getCustomNobleActionCategories()}; + customCategories.stealMarket.second = true; + break; + } + } +} + +CustomNobleActionCategories& getCustomNobleActionCategories() +{ + return customCategories; +} + +game::LNobleActionCatTable* __fastcall nobleActionCatTableCtorHooked( + game::LNobleActionCatTable* thisptr, + int /*%edx*/, + const char* globalsFolderPath, + void* codeBaseEnvProxy) +{ + using namespace game; + + static const char dbfFileName[] = "LAction.dbf"; + checkCustomActionCategories(std::filesystem::path(globalsFolderPath) / dbfFileName); + + thisptr->bgn = nullptr; + thisptr->end = nullptr; + thisptr->allocatedMemEnd = nullptr; + thisptr->allocator = nullptr; + thisptr->vftable = LNobleActionCatTableApi::vftable(); + + const auto& table = LNobleActionCatTableApi::get(); + const auto& actions = NobleActionCategories::get(); + + table.init(thisptr, codeBaseEnvProxy, globalsFolderPath, dbfFileName); + table.readCategory(actions.poisonStack, thisptr, "L_POISON_STACK", dbfFileName); + table.readCategory(actions.spy, thisptr, "L_SPY", dbfFileName); + table.readCategory(actions.stealItem, thisptr, "L_STEAL_ITEM", dbfFileName); + table.readCategory(actions.assassinate, thisptr, "L_ASSASSINATE", dbfFileName); + table.readCategory(actions.misfit, thisptr, "L_MISFIT", dbfFileName); + table.readCategory(actions.duel, thisptr, "L_DUEL", dbfFileName); + table.readCategory(actions.poisonCity, thisptr, "L_POISON_CITY", dbfFileName); + table.readCategory(actions.stealSpell, thisptr, "L_STEAL_SPELL", dbfFileName); + table.readCategory(actions.bribe, thisptr, "L_BRIBE", dbfFileName); + table.readCategory(actions.stealGold, thisptr, "L_STEAL_GOLD", dbfFileName); + table.readCategory(actions.stealMerchant, thisptr, "L_STEAL_MERCHANT", dbfFileName); + table.readCategory(actions.stealMage, thisptr, "L_STEAL_MAGE", dbfFileName); + table.readCategory(actions.spyRuin, thisptr, "L_SPY_RUIN", dbfFileName); + table.readCategory(actions.riotCity, thisptr, "L_RIOT_CITY", dbfFileName); + + auto& customCategories{getCustomNobleActionCategories()}; + if (customCategories.stealMarket.second) { + table.readCategory(&customCategories.stealMarket.first, thisptr, + stealMarketActionCategoryName, dbfFileName); + } + + table.initDone(thisptr); + return thisptr; +} + +} // namespace hooks diff --git a/mss32/src/customnobleactionhooks.cpp b/mss32/src/customnobleactionhooks.cpp new file mode 100644 index 00000000..fa90ded4 --- /dev/null +++ b/mss32/src/customnobleactionhooks.cpp @@ -0,0 +1,145 @@ +/* + * This file is part of the modding toolset for Disciples 2. + * (https://github.com/VladimirMakeev/D2ModdingToolset) + * Copyright (C) 2024 Vladimir Makeev. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "customnobleactionhooks.h" +#include "customnobleactioncategories.h" +#include "midsite.h" +#include "midsiteresourcemarket.h" +#include "nobleactioncategoryset.h" +#include "nobleactionresultstealmarket.h" +#include "originalfunctions.h" +#include "sitecategories.h" +#include "sitecategoryhooks.h" +#include "textids.h" +#include "utils.h" + +namespace hooks { + +static bool canStealFromResourceMarket(const CMidSiteResourceMarket* market) +{ + using namespace game; + + // If any of market resources are infinite, noble can always steal them + if (isMarketStockInfinite(market->infiniteStock, CurrencyType::Gold) + || isMarketStockInfinite(market->infiniteStock, CurrencyType::InfernalMana) + || isMarketStockInfinite(market->infiniteStock, CurrencyType::LifeMana) + || isMarketStockInfinite(market->infiniteStock, CurrencyType::DeathMana) + || isMarketStockInfinite(market->infiniteStock, CurrencyType::RunicMana) + || isMarketStockInfinite(market->infiniteStock, CurrencyType::GroveMana)) { + return true; + } + + // Otherwise check if stock is not empty + return !BankApi::get().isZero(&market->stock); +} + +game::INobleActionResult* __stdcall createNobleActionResultHooked( + game::IMidgardObjectMap* objectMap, + const game::LNobleActionCat* actionCategory, + const game::CMidgardID* targetObjectId, + const game::CMidgardID* id) +{ + using namespace game; + + const auto& customActions{getCustomNobleActionCategories()}; + + if (customActions.stealMarket.second + && customActions.stealMarket.first.id == actionCategory->id) { + return createStealMarketActionResult(objectMap, *targetObjectId); + } + + return getOriginalFunctions().createNobleActionResult(objectMap, actionCategory, targetObjectId, + id); +} + +bool __stdcall getSiteNobleActionsHooked(const game::IMidgardObjectMap* objectMap, + const game::CMidgardID* playerId, + const game::CMidgardID* objectId, + game::Set* nobleActions) +{ + using namespace game; + + const auto& customSites{customSiteCategories()}; + const auto& customActions{getCustomNobleActionCategories()}; + + if (customSites.exists && customActions.stealMarket.second) { + auto site{(const CMidSite*)objectMap->vftable->findScenarioObjectById(objectMap, objectId)}; + + if (site->siteCategory.id == customSites.resourceMarket.id + && canStealFromResourceMarket((const CMidSiteResourceMarket*)site)) { + NobleActionCatSetInsertIterator it{}; + NobleActionCatSetApi::get().insert(nobleActions, &it, &customActions.stealMarket.first); + return true; + } + } + + return getOriginalFunctions().getSiteNobleActions(objectMap, playerId, objectId, nobleActions); +} + +bool __stdcall getPossibleNobleActionsHooked(const game::IMidgardObjectMap* objectMap, + const game::CMidgardID* playerId, + const game::CMidgardID* objectId, + game::Set* nobleActions) +{ + using namespace game; + + const auto& customSites{customSiteCategories()}; + const auto& customActions{getCustomNobleActionCategories()}; + + if (customSites.exists && customActions.stealMarket.second) { + auto site{(const CMidSite*)objectMap->vftable->findScenarioObjectById(objectMap, objectId)}; + + if (site->siteCategory.id == customSites.resourceMarket.id) { + NobleActionCatSetInsertIterator it{}; + NobleActionCatSetApi::get().insert(nobleActions, &it, &customActions.stealMarket.first); + return true; + } + } + + return getOriginalFunctions().getSiteNobleActions(objectMap, playerId, objectId, nobleActions); +} + +game::String* __stdcall getNobleActionResultDescriptionHooked( + game::String* description, + const game::LNobleActionCat nobleActionCat, + const game::CCmdNobleResultMsg* nobleResultMsg, + const game::CPhaseGame* phaseGame, + const game::CMidPlayer* player) +{ + using namespace game; + + const auto& customActions{getCustomNobleActionCategories()}; + + if (customActions.stealMarket.second + && nobleActionCat.id == customActions.stealMarket.first.id) { + static const char fallback[]{ + "\\c000;000;000;\\hC;\\vC;\\fNormal;Resources have been stolen from the market!"}; + const auto text{ + getInterfaceText(textIds().nobleActions.stealMarketSuccess.c_str(), fallback)}; + + StringApi::get().initFromString(description, text.c_str()); + return description; + } + + return getOriginalFunctions().getNobleActionResultDescription(description, nobleActionCat, + nobleResultMsg, phaseGame, + player); +} + +} // namespace hooks diff --git a/mss32/src/ddstackgroup.cpp b/mss32/src/ddstackgroup.cpp new file mode 100644 index 00000000..bb4264e8 --- /dev/null +++ b/mss32/src/ddstackgroup.cpp @@ -0,0 +1,52 @@ +/* + * This file is part of the modding toolset for Disciples 2. + * (https://github.com/VladimirMakeev/D2ModdingToolset) + * Copyright (C) 2024 Vladimir Makeev. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "ddstackgroup.h" +#include "version.h" +#include + +namespace game::CDDStackGroupApi { + +// clang-format off +static std::array functions = {{ + // Akella + Api{ + (Api::Constructor)0x4ffe64, + }, + // Russobit + Api{ + (Api::Constructor)0x4ffe64, + }, + // Gog + Api{ + (Api::Constructor)0x4ff166, + }, + // Scenario Editor + Api{ + (Api::Constructor)0x476f31, + } +}}; +// clang-format on + +Api& get() +{ + return functions[static_cast(hooks::gameVersion())]; +} + +} diff --git a/mss32/src/ddstackinventorydisplay.cpp b/mss32/src/ddstackinventorydisplay.cpp new file mode 100644 index 00000000..1b476191 --- /dev/null +++ b/mss32/src/ddstackinventorydisplay.cpp @@ -0,0 +1,51 @@ +/* + * This file is part of the modding toolset for Disciples 2. + * (https://github.com/VladimirMakeev/D2ModdingToolset) + * Copyright (C) 2024 Vladimir Makeev. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "ddstackinventorydisplay.h" +#include "version.h" +#include + +namespace game::CDDStackInventoryDisplayApi { + +// clang-format off +static std::array functions = {{ + // Akella + Api{ + (Api::Constructor)0x502839, + (Api::SetElementCount)0x5062e2, + }, + // Russobit + Api{ + (Api::Constructor)0x502839, + (Api::SetElementCount)0x5062e2, + }, + // Gog + Api{ + (Api::Constructor)0x501b1e, + (Api::SetElementCount)0x50556f, + }, +}}; +// clang-format on + +Api& get() +{ + return functions[static_cast(hooks::gameVersion())]; +} + +} // namespace game::CDDStackInventoryDisplayApi diff --git a/mss32/src/diplomacyhooks.cpp b/mss32/src/diplomacyhooks.cpp index 2f49a0e1..09301b56 100644 --- a/mss32/src/diplomacyhooks.cpp +++ b/mss32/src/diplomacyhooks.cpp @@ -144,9 +144,9 @@ bool __stdcall areRelationsAtWarHooked(const std::uint32_t* relation) using namespace game; const GlobalData* data = *GlobalDataApi::get().getGlobalData(); - const GlobalVariables* variables = *data->globalVariables; + const GlobalVariables* variables = data->globalVariables; - return *relation <= variables->diplomacyWar; + return *relation <= variables->data->diplomacyWar; } bool __stdcall areRelationsNeutralHooked(const std::uint32_t* relation) @@ -154,9 +154,10 @@ bool __stdcall areRelationsNeutralHooked(const std::uint32_t* relation) using namespace game; const GlobalData* data = *GlobalDataApi::get().getGlobalData(); - const GlobalVariables* variables = *data->globalVariables; + const GlobalVariables* variables = data->globalVariables; - return *relation > variables->diplomacyWar && *relation <= variables->diplomacyNeutral; + return *relation > variables->data->diplomacyWar + && *relation <= variables->data->diplomacyNeutral; } bool __stdcall areRelationsPeacefulHooked(const std::uint32_t* relation) @@ -164,9 +165,9 @@ bool __stdcall areRelationsPeacefulHooked(const std::uint32_t* relation) using namespace game; const GlobalData* data = *GlobalDataApi::get().getGlobalData(); - const GlobalVariables* variables = *data->globalVariables; + const GlobalVariables* variables = data->globalVariables; - return *relation > variables->diplomacyNeutral; + return *relation > variables->data->diplomacyNeutral; } -} \ No newline at end of file +} // namespace hooks diff --git a/mss32/src/displayhandlerhooks.cpp b/mss32/src/displayhandlerhooks.cpp index dc1c8fba..ccb273bf 100644 --- a/mss32/src/displayhandlerhooks.cpp +++ b/mss32/src/displayhandlerhooks.cpp @@ -18,19 +18,61 @@ */ #include "displayhandlershooks.h" +#include "dynamiccast.h" #include "game.h" #include "gameimages.h" #include "gameutils.h" #include "isolayers.h" +#include "mempool.h" #include "midgardobjectmap.h" #include "midplayer.h" +#include "midsiteresourcemarket.h" #include "midvillage.h" +#include "originalfunctions.h" #include "racetype.h" #include "stringintlist.h" #include namespace hooks { +struct ResourceMarketDisplayHandler : public game::ImageDisplayHandler +{ }; + +static void __fastcall resMarketDisplayHandlerDtor(ResourceMarketDisplayHandler* thisptr, + int /*%edx*/, + char flags) +{ + if (flags & 1) { + game::Memory::get().freeNonZero(thisptr); + } +} + +static void __fastcall resMarketDisplayHandlerRunHandler(ResourceMarketDisplayHandler* thisptr, + int /*%edx*/, + game::ImageLayerList* list, + const game::IMapElement* mapElement, + const game::IMidgardObjectMap* objectMap, + const game::CMidgardID* playerId, + const game::IdSet* objectives, + int a6, + bool animatedIso) +{ + using namespace game; + + auto* site = (CMidSiteResourceMarket*)RttiApi::get() + .dynamicCast(mapElement, 0, RttiApi::rtti().IMapElementType, + getResourceMarketTypeDescriptor(), 0); + + thisptr->handler(list, site, objectMap, playerId, objectives, a6, animatedIso); +} + +// clang-format off +static game::ImageDisplayHandlerVftable resMarketDisplayHandlerVftable{ + (game::ImageDisplayHandlerVftable::Destructor)resMarketDisplayHandlerDtor, + (game::ImageDisplayHandlerVftable::RunHandler)resMarketDisplayHandlerRunHandler, +}; +// clang-format on + static std::string getVillageImageName(game::RaceId raceId, int villageTier, bool shadow) { using game::RaceId; @@ -195,4 +237,75 @@ void __stdcall displayHandlerVillageHooked(game::ImageLayerList* list, } } +static void registerResourceMarketDisplayHandler(game::ImageDisplayHandlerMap* handlerMap) +{ + using namespace game; + + auto handler = (ResourceMarketDisplayHandler*)Memory::get().allocate( + sizeof(ResourceMarketDisplayHandler)); + handler->vftable = &resMarketDisplayHandlerVftable; + handler->handler = (DisplayHandlersApi::Api::DisplayHandler)DisplayHandlersApi::get() + .siteHandler; + + SmartPointer ptr{}; + SmartPointerApi::get().createOrFree(&ptr, handler); + + const auto* marketTypeDescriptor{getResourceMarketTypeDescriptor()}; + + ImageDisplayHandlerMapInsertIt iterator{}; + ImageDisplayHandlerApi::get().addHandler(handlerMap, &iterator, &marketTypeDescriptor, + (ImageDisplayHandlerPtr*)&ptr); + + SmartPointerApi::get().createOrFree(&ptr, nullptr); +} + +void __stdcall getMapElementIsoLayerImagesHooked(game::ImageLayerList* list, + const game::IMapElement* mapElement, + const game::IMidgardObjectMap* objectMap, + const game::CMidgardID* playerId, + const game::IdSet* objectives, + int unknown, + bool animatedIso) +{ + using namespace game; + + getOriginalFunctions().getMapElementIsoLayerImages(list, mapElement, objectMap, playerId, + objectives, unknown, animatedIso); + + auto handlerMap{ImageDisplayHandlerApi::instance()}; + + bool firstTime{true}; + if (firstTime) { + firstTime = false; + + // Register display handler for custom site + registerResourceMarketDisplayHandler(handlerMap); + } + + const auto typeId{*RttiApi::get().typeIdOperator}; + const auto typeIdsNotEqual{*RttiApi::get().typeInfoInequalityOperator}; + + const auto* mapElementDescriptor{typeId(mapElement)}; + const auto* resourceMarketDescriptor{getResourceMarketTypeDescriptor()}; + + if (typeIdsNotEqual(mapElementDescriptor, resourceMarketDescriptor)) { + // Filter out map elements that were handled by original function + return; + } + + // Reuse IdSet::find since game itself uses it + auto find = (ImageDisplayHandlerMapFind)IdSetApi::get().find; + + ImageDisplayHandlerMapIt iterator; + find(handlerMap, &iterator, &mapElementDescriptor); + if (iterator != handlerMap->end()) { + auto& pair = *iterator; + + ImageDisplayHandlerPtr& ptr = pair.second; + ImageDisplayHandler* handler = ptr.data; + handler->vftable->runHandler(handler, list, mapElement, objectMap, playerId, objectives, + unknown, animatedIso); + } +} + } // namespace hooks diff --git a/mss32/src/displayhandlers.cpp b/mss32/src/displayhandlers.cpp index aeb131a8..52dba12e 100644 --- a/mss32/src/displayhandlers.cpp +++ b/mss32/src/displayhandlers.cpp @@ -21,25 +21,30 @@ #include "version.h" #include -namespace game::DisplayHandlersApi { +namespace game { +namespace DisplayHandlersApi { // clang-format off static std::array functions = {{ // Akella Api{ (Api::DisplayHandler)0x5bcb73, + (Api::DisplayHandler)0x5bd31a, }, // Russobit Api{ (Api::DisplayHandler)0x5bcb73, + (Api::DisplayHandler)0x5bd31a, }, // Gog Api{ (Api::DisplayHandler)0x5bbc37, + (Api::DisplayHandler)0x5bc3de, }, // Scenario Editor Api{ (Api::DisplayHandler)0x55d9eb, + (Api::DisplayHandler)0x55e192, }, }}; // clang-format on @@ -49,4 +54,54 @@ Api& get() return functions[static_cast(hooks::gameVersion())]; } -} // namespace game::DisplayHandlersApi +} // namespace DisplayHandlersApi + +namespace ImageDisplayHandlerApi { + +// clang-format off +static std::array functions = {{ + // Akella + Api{ + (Api::AddHandler)0x5be078, + }, + // Russobit + Api{ + (Api::AddHandler)0x5be078, + }, + // Gog + Api{ + (Api::AddHandler)0x5bd13c, + }, + // Scenario Editor + Api{ + (Api::AddHandler)0x55ef15, + }, +}}; +// clang-format on + +Api& get() +{ + return functions[static_cast(hooks::gameVersion())]; +} + +// clang-format off +static std::array instances = {{ + // Akella + (ImageDisplayHandlerMap*)0x83a8d0, + // Russobit + (ImageDisplayHandlerMap*)0x83a8d0, + // Gog + (ImageDisplayHandlerMap*)0x838878, + // Scenario Editor + (ImageDisplayHandlerMap*)0x666440, +}}; +// clang-format on + +ImageDisplayHandlerMap* instance() +{ + return instances[static_cast(hooks::gameVersion())]; +} + +} // namespace ImageDisplayHandlerApi + +} // namespace game diff --git a/mss32/src/draganddropinterf.cpp b/mss32/src/draganddropinterf.cpp index 3e164672..e2fa1033 100644 --- a/mss32/src/draganddropinterf.cpp +++ b/mss32/src/draganddropinterf.cpp @@ -24,19 +24,27 @@ namespace game::CDragAndDropInterfApi { // clang-format off -static std::array functions = {{ +static std::array functions = {{ // Akella Api{ - (Api::GetDialog)0x56cea4 + (Api::GetDialog)0x56cea4, + (Api::Constructor)0x56c881, }, // Russobit Api{ - (Api::GetDialog)0x56cea4 + (Api::GetDialog)0x56cea4, + (Api::Constructor)0x56c881, }, // Gog Api{ - (Api::GetDialog)0x56c54e - } + (Api::GetDialog)0x56c54e, + (Api::Constructor)0x56bf2b, + }, + // Scenario Editor + Api{ + (Api::GetDialog)0x567488, + (Api::Constructor)0x566e6c, + }, }}; // clang-format on diff --git a/mss32/src/dynamiccast.cpp b/mss32/src/dynamiccast.cpp index f8fce20b..8003f1c1 100644 --- a/mss32/src/dynamiccast.cpp +++ b/mss32/src/dynamiccast.cpp @@ -105,6 +105,17 @@ static const std::array types = {{ (TypeDescriptor*)0x793d90, // IUsNobleType (TypeDescriptor*)0x7974b8, // IUsSummonType (TypeDescriptor*)0x7afcf8, // IItemExPotionBoostType + (TypeDescriptor*)0x796880, // IMapElementType + (TypeDescriptor*)0x793bc8, // CMidRoadType + (TypeDescriptor*)0x793040, // CCmdStackVisitMsgType + (TypeDescriptor*)0x793b90, // CMidSiteType + (BaseClassDescriptor*)0x719cf0, // IMidObjectDescriptor + (BaseClassDescriptor*)0x71f190, // IMidScenarioObjectDescriptor + (BaseClassDescriptor*)0x71f328, // IMapElementDescriptor + (BaseClassDescriptor*)0x71f310, // IAiPriorityDescriptor + (BaseClassDescriptor*)0x71ffb0, // CMidSiteDescriptor + (BaseClassDescriptor*)0x6f6dd0, // CNetMsgDescriptor + (BaseClassDescriptor*)0x6f7338, // CNetMsgMapEntryDescriptor }, // Russobit Rtti{ @@ -155,6 +166,17 @@ static const std::array types = {{ (TypeDescriptor*)0x793d90, // IUsNobleType (TypeDescriptor*)0x7974b8, // IUsSummonType (TypeDescriptor*)0x7afcf8, // IItemExPotionBoostType + (TypeDescriptor*)0x796880, // IMapElementType + (TypeDescriptor*)0x793bc8, // CMidRoadType + (TypeDescriptor*)0x793040, // CCmdStackVisitMsgType + (TypeDescriptor*)0x793b90, // CMidSiteType + (BaseClassDescriptor*)0x719cf0, // IMidObjectDescriptor + (BaseClassDescriptor*)0x71f190, // IMidScenarioObjectDescriptor + (BaseClassDescriptor*)0x71f328, // IMapElementDescriptor + (BaseClassDescriptor*)0x71f310, // IAiPriorityDescriptor + (BaseClassDescriptor*)0x71ffb0, // CMidSiteDescriptor + (BaseClassDescriptor*)0x6f6dd0, // CNetMsgDescriptor + (BaseClassDescriptor*)0x6f7338, // CNetMsgMapEntryDescriptor }, // Gog Rtti{ @@ -205,6 +227,17 @@ static const std::array types = {{ (TypeDescriptor*)0x791d38, // IUsNobleType (TypeDescriptor*)0x795460, // IUsSummonType (TypeDescriptor*)0x7adcb0, // IItemExPotionBoostType + (TypeDescriptor*)0x794828, // IMapElementType + (TypeDescriptor*)0x791b70, // CMidRoadType + (TypeDescriptor*)0x790fe8, // CCmdStackVisitMsgType + (TypeDescriptor*)0x791b38, // CMidSiteType + (BaseClassDescriptor*)0x717c58, // IMidObjectDescriptor + (BaseClassDescriptor*)0x71d0f8, // IMidScenarioObjectDescriptor + (BaseClassDescriptor*)0x71d290, // IMapElementDescriptor + (BaseClassDescriptor*)0x71d278, // IAiPriorityDescriptor + (BaseClassDescriptor*)0x71df18, // CMidSiteDescriptor + (BaseClassDescriptor*)0x6f4d38, // CNetMsgDescriptor + (BaseClassDescriptor*)0x6f52a0, // CNetMsgMapEntryDescriptor }, // Scenario Editor Rtti{ @@ -255,8 +288,30 @@ static const std::array types = {{ (TypeDescriptor*)0x64c3d0, // IUsNobleType (TypeDescriptor*)0x64f068, // IUsSummonType (TypeDescriptor*)0x656e28, // IItemExPotionBoostType + (TypeDescriptor*)0x648b70, // IMapElementType + (TypeDescriptor*)0x649488, // CMidRoadType + (TypeDescriptor*)nullptr, // CCmdStackVisitMsgType + (TypeDescriptor*)0x649470, // CMidSiteType + (BaseClassDescriptor*)0x5f5f90, // IMidObjectDescriptor + (BaseClassDescriptor*)0x5f5fa8, // IMidScenarioObjectDescriptor + (BaseClassDescriptor*)0x5f6748, // IMapElementDescriptor + (BaseClassDescriptor*)0x5f6730, // IAiPriorityDescriptor + (BaseClassDescriptor*)0x5f6cc8, // CMidSiteDescriptor + (BaseClassDescriptor*)nullptr, // CNetMsgDescriptor + (BaseClassDescriptor*)nullptr, // CNetMsgMapEntryDescriptor }, }}; + +static const std::array vftables = { + // Akella + (const void*)0x6f5ae4, + // Russobit + (const void*)0x6f5ae4, + // Gog + (const void*)0x6f3a94, + // Scenario Editor + (const void*)0x5e3cb4, +}; // clang-format on Api& get() @@ -269,4 +324,9 @@ const Rtti& rtti() return types[static_cast(hooks::gameVersion())]; } +const void* typeInfoVftable() +{ + return vftables[static_cast(hooks::gameVersion())]; +} + } // namespace game::RttiApi diff --git a/mss32/src/editor.cpp b/mss32/src/editor.cpp index c901e394..3e2023fb 100644 --- a/mss32/src/editor.cpp +++ b/mss32/src/editor.cpp @@ -32,6 +32,10 @@ EditorFunctions editorFunctions{ (IsRaceCategoryPlayable)0x419193, (ChangeCapitalTerrain)0x50afb4, (GetObjectNamePos)0x45797c, + (GetSiteImageIndices)0x556835, + (GetSiteImage)0x5569f4, + (GetSiteAtPosition)0x410cf7, + (ShowOrHideSiteOnStrategicMap)0x553907, }; // clang-format on diff --git a/mss32/src/effectgiveresources.cpp b/mss32/src/effectgiveresources.cpp new file mode 100644 index 00000000..6352b5de --- /dev/null +++ b/mss32/src/effectgiveresources.cpp @@ -0,0 +1,119 @@ +/* + * This file is part of the modding toolset for Disciples 2. + * (https://github.com/VladimirMakeev/D2ModdingToolset) + * Copyright (C) 2024 Vladimir Makeev. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "effectgiveresources.h" +#include "eventeffectbase.h" +#include "gameutils.h" +#include "mempool.h" +#include "midgardobjectmap.h" +#include "midmsgsender.h" +#include "midplayer.h" + +namespace hooks { + +struct CEffectGiveResources : public game::CEventEffectBase +{ + game::Bank resources; + game::CMidgardID playerId; + bool add; +}; + +static void __fastcall effectGiveDtor(CEffectGiveResources* thisptr, int /*%edx*/, char flags) +{ + if (flags & 1) { + game::Memory::get().freeNonZero(thisptr); + } +} + +static bool __fastcall effectGiveApply(CEffectGiveResources* thisptr, + int /*%edx*/, + game::IMidgardObjectMap* objectMap, + game::IMidMsgSender* msgSender, + game::IdVector* triggerers) +{ + using namespace game; + + const auto& bankApi{BankApi::get()}; + if (!bankApi.isValid(&thisptr->resources)) { + return false; + } + + CMidPlayer* player{getPlayerToChange(objectMap, &thisptr->playerId)}; + + Bank bank; + bankApi.setZero(&bank); + + if (thisptr->add) { + bankApi.add(&bank, &player->bank); + bankApi.add(&bank, &thisptr->resources); + } else { + bankApi.copy(&bank, &player->bank); + bankApi.subtract(&bank, &thisptr->resources); + } + + if (bankApi.isValid(&bank)) { + bankApi.copy(&player->bank, &bank); + } + + msgSender->vftable->sendObjectsChanges(msgSender); + return true; +} + +static bool __fastcall effectGiveMethod2(CEffectGiveResources* thisptr, int /*%edx*/, int) +{ + return true; +} + +static bool __fastcall effectGiveMethod3(CEffectGiveResources* thisptr, int /*%edx*/) +{ + return true; +} + +static bool __fastcall effectGiveStopProcessing(const CEffectGiveResources* thisptr, int /*%edx*/) +{ + return false; +} + +// clang-format off +static game::IEventEffectVftable effectGiveVftable{ + (game::IEventEffectVftable::Destructor)effectGiveDtor, + (game::IEventEffectVftable::Apply)effectGiveApply, + (game::IEventEffectVftable::Method2)effectGiveMethod2, + (game::IEventEffectVftable::Method3)effectGiveMethod3, + (game::IEventEffectVftable::StopProcessing)effectGiveStopProcessing, +}; +// clang-format on + +game::IEventEffect* createEffectGiveResources(const game::CMidgardID& playerId, + const game::Bank& resources, + bool add) +{ + using namespace game; + + auto effect{(CEffectGiveResources*)Memory::get().allocate(sizeof(CEffectGiveResources))}; + effect->vftable = &effectGiveVftable; + BankApi::get().setZero(&effect->resources); + BankApi::get().copy(&effect->resources, &resources); + effect->playerId = playerId; + effect->add = add; + + return effect; +} + +} // namespace hooks diff --git a/mss32/src/effectstealmarket.cpp b/mss32/src/effectstealmarket.cpp new file mode 100644 index 00000000..b515ac3e --- /dev/null +++ b/mss32/src/effectstealmarket.cpp @@ -0,0 +1,106 @@ +/* + * This file is part of the modding toolset for Disciples 2. + * (https://github.com/VladimirMakeev/D2ModdingToolset) + * Copyright (C) 2024 Vladimir Makeev. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "effectstealmarket.h" +#include "eventeffectbase.h" +#include "mempool.h" +#include "midgardobjectmap.h" +#include "midmsgsender.h" +#include "midsiteresourcemarket.h" + +namespace hooks { + +struct CEffectStealMarket : public game::CEventEffectBase +{ + game::CMidgardID marketId; + game::CurrencyType resource; + std::int16_t amount; +}; + +static void __fastcall effectStealDtor(CEffectStealMarket* thisptr, int /*%edx*/, char flags) +{ + if (flags & 1) { + game::Memory::get().freeNonZero(thisptr); + } +} + +static bool __fastcall effectStealApply(CEffectStealMarket* thisptr, + int /*%edx*/, + game::IMidgardObjectMap* objectMap, + game::IMidMsgSender* msgSender, + game::IdVector* triggerers) +{ + using namespace game; + + auto constMarket{(const CMidSiteResourceMarket*) + objectMap->vftable->findScenarioObjectById(objectMap, &thisptr->marketId)}; + + if (isMarketStockInfinite(constMarket->infiniteStock, thisptr->resource)) { + return true; + } + + auto market{(CMidSiteResourceMarket*)objectMap->vftable + ->findScenarioObjectByIdForChange(objectMap, &thisptr->marketId)}; + + const std::int16_t initialAmount{BankApi::get().get(&market->stock, thisptr->resource)}; + BankApi::get().set(&market->stock, thisptr->resource, initialAmount - thisptr->amount); + + msgSender->vftable->sendObjectsChanges(msgSender); + return true; +} + +static bool __fastcall effectStealMethod2(CEffectStealMarket* thisptr, int /*%edx*/, int) +{ + return true; +} + +static bool __fastcall effectStealMethod3(CEffectStealMarket* thisptr, int /*%edx*/) +{ + return true; +} + +static bool __fastcall effectStealStopProcessing(const CEffectStealMarket* thisptr, int /*%edx*/) +{ + return false; +} + +// clang-format off +static game::IEventEffectVftable effectStealVftable{ + (game::IEventEffectVftable::Destructor)effectStealDtor, + (game::IEventEffectVftable::Apply)effectStealApply, + (game::IEventEffectVftable::Method2)effectStealMethod2, + (game::IEventEffectVftable::Method3)effectStealMethod3, + (game::IEventEffectVftable::StopProcessing)effectStealStopProcessing, +}; +// clang-format on + +game::IEventEffect* createEffectStealMarket(const game::CMidgardID& marketId, + game::CurrencyType resource, + std::int16_t amount) +{ + auto effect = (CEffectStealMarket*)game::Memory::get().allocate(sizeof(CEffectStealMarket)); + effect->vftable = &effectStealVftable; + effect->marketId = marketId; + effect->resource = resource; + effect->amount = amount; + + return effect; +} + +} // namespace hooks diff --git a/mss32/src/encparamidplayer.cpp b/mss32/src/encparamidplayer.cpp new file mode 100644 index 00000000..843c08a5 --- /dev/null +++ b/mss32/src/encparamidplayer.cpp @@ -0,0 +1,51 @@ +/* + * This file is part of the modding toolset for Disciples 2. + * (https://github.com/VladimirMakeev/D2ModdingToolset) + * Copyright (C) 2024 Vladimir Makeev. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "encparamidplayer.h" +#include "version.h" +#include + +namespace game::CEncParamIDPlayerApi { + +// clang-format off +static std::array functions = {{ + // Akella + Api{ + (Api::Constructor)0x5744df, + (Api::Destructor)0x57456b, + }, + // Russobit + Api{ + (Api::Constructor)0x5744df, + (Api::Destructor)0x57456b, + }, + // Gog + Api{ + (Api::Constructor)0x573b38, + (Api::Destructor)0x573bc4, + } +}}; +// clang-format on + +Api& get() +{ + return functions[static_cast(hooks::gameVersion())]; +} + +} // namespace game::CEncParamIDPlayerApi diff --git a/mss32/src/exchangeresourcesmsg.cpp b/mss32/src/exchangeresourcesmsg.cpp new file mode 100644 index 00000000..c790eb89 --- /dev/null +++ b/mss32/src/exchangeresourcesmsg.cpp @@ -0,0 +1,196 @@ +/* + * This file is part of the modding toolset for Disciples 2. + * (https://github.com/VladimirMakeev/D2ModdingToolset) + * Copyright (C) 2024 Vladimir Makeev. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "exchangeresourcesmsg.h" +#include "dynamiccast.h" +#include "mempool.h" +#include "midclient.h" +#include "midclientcore.h" +#include "midgard.h" +#include "midobjectlock.h" +#include "phasegame.h" +#include "streambits.h" + +namespace hooks { + +static void __fastcall exchangeResourcesMsgDestructor(CExchangeResourcesMsg* thisptr, + int /*%edx*/, + char flags); + +static void __fastcall exchangeResourcesMsgSerialize(CExchangeResourcesMsg* thisptr, + int /*%edx*/, + game::CStreamBits* stream); + +static game::ClassHierarchyDescriptor* getMsgHierarchyDescriptor() +{ + using namespace game; + + // clang-format off + static game::BaseClassDescriptor baseClassDescriptor{ + getExchangeResourcesMsgTypeDescriptor(), + 1u, // base class array has 1 more element after this descriptor + game::PMD{ 0, -1, 0 }, + 0u + }; + + static game::BaseClassArray baseClassArray{ + &baseClassDescriptor, + RttiApi::rtti().CNetMsgDescriptor + }; + + static game::ClassHierarchyDescriptor hierarchyDescriptor{ + 0, + 0, + 2u, + &baseClassArray + }; + // clang-format on + + return &hierarchyDescriptor; +} + +using MsgRttiInfo = game::RttiInfo; + +static void setupRttiInfo(MsgRttiInfo& rttiInfo) +{ + using namespace game; + + // Use our own vftable + rttiInfo.vftable.destructor = (CNetMsgVftable::Destructor)exchangeResourcesMsgDestructor; + rttiInfo.vftable.serialize = (CNetMsgVftable::Serialize)exchangeResourcesMsgSerialize; +} + +static MsgRttiInfo& getMsgRttiInfo() +{ + using namespace game; + + // clang-format off + static const game::CompleteObjectLocator objectLocator{ + 0u, + offsetof(CExchangeResourcesMsg, vftable), + 0u, + getExchangeResourcesMsgTypeDescriptor(), + getMsgHierarchyDescriptor() + }; + // clang-format on + + static MsgRttiInfo rttiInfo{&objectLocator}; + + static bool firstTime{true}; + if (firstTime) { + firstTime = false; + setupRttiInfo(rttiInfo); + } + + return rttiInfo; +} + +CExchangeResourcesMsg::CExchangeResourcesMsg() + : siteId{game::invalidId} + , visitorStackId{game::invalidId} + , playerCurrency{game::CurrencyType::Gold} + , siteCurrency{game::CurrencyType::Gold} + , amount{0u} +{ + vftable = &getMsgRttiInfo().vftable; +} + +CExchangeResourcesMsg::CExchangeResourcesMsg(const game::CMidgardID& siteId, + const game::CMidgardID& visitorStackId, + game::CurrencyType playerCurrency, + game::CurrencyType siteCurrency, + std::uint16_t amount) + : siteId{siteId} + , visitorStackId{visitorStackId} + , playerCurrency{playerCurrency} + , siteCurrency{siteCurrency} + , amount{amount} +{ + vftable = &getMsgRttiInfo().vftable; +} + +static void __fastcall exchangeResourcesMsgDestructor(CExchangeResourcesMsg* thisptr, + int /*%edx*/, + char flags) +{ + using namespace game; + + CNetMsgApi::get().destructor(thisptr); + if (flags & 1) { + Memory::get().freeNonZero(thisptr); + } +} + +static void __fastcall exchangeResourcesMsgSerialize(CExchangeResourcesMsg* thisptr, + int /*%edx*/, + game::CStreamBits* stream) +{ + using namespace game; + + CNetMsgApi::get().serialize(thisptr, stream); + + const auto& serializeId{CStreamBitsApi::get().serializeId}; + + serializeId(stream, &thisptr->siteId); + serializeId(stream, &thisptr->visitorStackId); + + stream->vftable->serialize(stream, &thisptr->playerCurrency, sizeof(thisptr->playerCurrency)); + stream->vftable->serialize(stream, &thisptr->siteCurrency, sizeof(thisptr->siteCurrency)); + stream->vftable->serialize(stream, &thisptr->amount, sizeof(thisptr->amount)); +} + +game::TypeDescriptor* getExchangeResourcesMsgTypeDescriptor() +{ + using namespace game; + + // clang-format off + static game::TypeDescriptor descriptor{ + game::RttiApi::typeInfoVftable(), + nullptr, + ".?AVCExchangeResourcesMsg@@", + }; + // clang-format on + + return &descriptor; +} + +void sendExchangeResourcesMsg(game::CPhaseGame* phaseGame, + const game::CMidgardID& siteId, + const game::CMidgardID& visitorStackId, + game::CurrencyType playerCurrency, + game::CurrencyType marketCurrency, + std::uint16_t amount) +{ + using namespace game; + + if (!phaseGame->data->clientTakesTurn) { + return; + } + + ++phaseGame->data->midObjectLock->pendingNetworkUpdates; + + CExchangeResourcesMsg message{siteId, visitorStackId, playerCurrency, marketCurrency, amount}; + + CMidClient* client{phaseGame->data->midClient}; + CMidgard* midgard{client->core.data->midgard}; + + CMidgardApi::get().sendNetMsgToServer(midgard, &message); +} + +} // namespace hooks diff --git a/mss32/src/game.cpp b/mss32/src/game.cpp index 9e00d13b..76e6f8be 100644 --- a/mss32/src/game.cpp +++ b/mss32/src/game.cpp @@ -150,6 +150,14 @@ static std::array functions = {{ (GetUnitRequiredBuildings)0x61f7fe, (ComputeMovementCost)0x603bc6, (GetBuildingStatus)0x5ddcb3, + (RemoveStack)0x60f5f8, + (GetSiteNameSuffix)0x5c5c3f, + (UpdateEncLayoutSite)0x579470, + (GetSiteSound)0x5093fb, + (SiteHasSound)0x50948c, + (GetNobleActions)0x5d44c5, + (GetNobleActions)0x5d4b28, + (GetNobleActionResultDescription)0x49cf6f, }, // Russobit Functions{ @@ -276,6 +284,14 @@ static std::array functions = {{ (GetUnitRequiredBuildings)0x61f7fe, (ComputeMovementCost)0x603bc6, (GetBuildingStatus)0x5ddcb3, + (RemoveStack)0x60f5f8, + (GetSiteNameSuffix)0x5c5c3f, + (UpdateEncLayoutSite)0x579470, + (GetSiteSound)0x5093fb, + (SiteHasSound)0x50948c, + (GetNobleActions)0x5d44c5, + (GetNobleActions)0x5d4b28, + (GetNobleActionResultDescription)0x49cf6f, }, // Gog Functions{ @@ -402,6 +418,14 @@ static std::array functions = {{ (GetUnitRequiredBuildings)0x61e33a, (ComputeMovementCost)0x6027f3, (GetBuildingStatus)0x5dc9e8, + (RemoveStack)0x60e12c, + (GetSiteNameSuffix)0x5c4c28, + (UpdateEncLayoutSite)0x578b2b, + (GetSiteSound)0x5086eb, + (SiteHasSound)0x50877c, + (GetNobleActions)0x5d33ee, + (GetNobleActions)0x5d3a51, + (GetNobleActionResultDescription)0x49c8dc, }, // Scenario Editor Functions{ @@ -445,7 +469,7 @@ static std::array functions = {{ (IsGroupOwnerPlayerHuman)nullptr, (AttackShouldMiss)nullptr, (GenerateRandomNumber)nullptr, - (GenerateRandomNumberStd)nullptr, + (GenerateRandomNumberStd)0x48341a, (GetUnitPositionInGroup)nullptr, (GetSummonUnitImplIdByAttack)nullptr, (GetSummonUnitImplId)nullptr, @@ -528,6 +552,14 @@ static std::array functions = {{ (GetUnitRequiredBuildings)0x52314a, (ComputeMovementCost)nullptr, (GetBuildingStatus)0x4d8a11, + (RemoveStack)nullptr, + (GetSiteNameSuffix)0x565306, + (UpdateEncLayoutSite)0x4ca874, + (GetSiteSound)nullptr, + (SiteHasSound)nullptr, + (GetNobleActions)nullptr, + (GetNobleActions)nullptr, + (GetNobleActionResultDescription)nullptr, }, }}; diff --git a/mss32/src/gameutils.cpp b/mss32/src/gameutils.cpp index 618d0c14..a260f9c7 100644 --- a/mss32/src/gameutils.cpp +++ b/mss32/src/gameutils.cpp @@ -29,6 +29,7 @@ #include "lordtype.h" #include "midclient.h" #include "midclientcore.h" +#include "middatacache.h" #include "middiplomacy.h" #include "midgard.h" #include "midgardmap.h" @@ -37,6 +38,8 @@ #include "midgardobjectmap.h" #include "midgardplan.h" #include "midgardscenariomap.h" +#include "miditem.h" +#include "midlocation.h" #include "midplayer.h" #include "midrod.h" #include "midruin.h" @@ -44,8 +47,10 @@ #include "midserver.h" #include "midserverlogic.h" #include "midstack.h" +#include "midstackdestroyed.h" #include "midunit.h" #include "playerbuildings.h" +#include "racetype.h" #include "scenarioinfo.h" #include "scenedit.h" #include "unitutils.h" @@ -191,6 +196,18 @@ const game::CMidPlayer* getPlayer(const game::IMidgardObjectMap* objectMap, rtti.CMidPlayerType, 0); } +game::CMidPlayer* getPlayerToChange(game::IMidgardObjectMap* objectMap, + const game::CMidgardID* playerId) +{ + using namespace game; + + auto playerObj = objectMap->vftable->findScenarioObjectByIdForChange(objectMap, playerId); + const auto& rtti = RttiApi::rtti(); + const auto dynamicCast = RttiApi::get().dynamicCast; + return (CMidPlayer*)dynamicCast(playerObj, 0, rtti.IMidScenarioObjectType, rtti.CMidPlayerType, + 0); +} + const game::CMidPlayer* getPlayer(const game::IMidgardObjectMap* objectMap, const game::BattleMsgData* battleMsgData, const game::CMidgardID* unitId) @@ -232,6 +249,46 @@ const game::CMidgardID getPlayerIdByUnitId(const game::IMidgardObjectMap* object return emptyId; } +const game::CMidPlayer* getNeutralPlayer(const game::IMidgardObjectMap* objectMap) +{ + using namespace game; + + const auto neutralRaceId{RaceCategories::get().neutral->id}; + const CMidPlayer* neutrals{}; + + auto getNeutrals = [neutralRaceId, &neutrals](const IMidScenarioObject* obj) { + auto player{static_cast(obj)}; + + if (neutralRaceId == player->raceType->data->raceType.id) { + neutrals = player; + } + }; + + forEachScenarioObject(objectMap, IdType::Player, getNeutrals); + return neutrals; +} + +const game::CMidPlayer* getGroupOwner(const game::IMidgardObjectMap* objectMap, + const game::CMidgardID* groupId) +{ + using namespace game; + + switch (CMidgardIDApi::get().getType(groupId)) { + case IdType::Stack: { + const CMidStack* allyStack{getStack(objectMap, groupId)}; + return getPlayer(objectMap, &allyStack->ownerId); + } + case IdType::Fortification: { + const CFortification* allyFort{getFort(objectMap, groupId)}; + return getPlayer(objectMap, &allyFort->ownerId); + } + case IdType::Ruin: + return getNeutralPlayer(objectMap); + } + + return nullptr; +} + const game::CMidScenVariables* getScenarioVariables(const game::IMidgardObjectMap* objectMap) { const auto id{createIdWithType(objectMap, game::IdType::ScenarioVariable)}; @@ -256,6 +313,17 @@ const game::CMidgardPlan* getMidgardPlan(const game::IMidgardObjectMap* objectMa return static_cast(obj); } +game::CMidgardPlan* getMidgardPlanToChange(game::IMidgardObjectMap* objectMap) +{ + const auto id{createIdWithType(objectMap, game::IdType::Plan)}; + auto obj{objectMap->vftable->findScenarioObjectByIdForChange(objectMap, &id)}; + if (!obj) { + return nullptr; + } + + return static_cast(obj); +} + const game::CMidgardMap* getMidgardMap(const game::IMidgardObjectMap* objectMap) { const auto id{createIdWithType(objectMap, game::IdType::Map)}; @@ -499,8 +567,8 @@ int getWeaponMasterBonusXpPercent(const game::IMidgardObjectMap* objectMap, auto stack = getStack(objectMap, groupId); auto leader = fn.findUnitById(objectMap, &stack->leaderId); if (hasLeaderAbility(leader->unitImpl, LeaderAbilityCategories::get().weaponMaster)) { - const auto vars = *(*GlobalDataApi::get().getGlobalData())->globalVariables; - return vars->weaponMaster; + const auto vars = (*GlobalDataApi::get().getGlobalData())->globalVariables; + return vars->data->weaponMaster; } } @@ -733,4 +801,64 @@ const game::CMidgardMapFog* getFog(const game::IMidgardObjectMap* objectMap, return static_cast(obj); } +const game::CMidLocation* getLocation(const game::IMidgardObjectMap* objectMap, + const game::CMidgardID* locationId) +{ + using namespace game; + + auto obj{objectMap->vftable->findScenarioObjectById(objectMap, locationId)}; + if (!obj) { + return nullptr; + } + + const auto dynamicCast = RttiApi::get().dynamicCast; + const auto& rtti = RttiApi::rtti(); + + return (const CMidLocation*)dynamicCast(obj, 0, rtti.IMidScenarioObjectType, + rtti.CMidLocationType, 0); +} + +const game::CMidStackDestroyed* getStackDestroyed(const game::IMidgardObjectMap* objectMap) +{ + using namespace game; + + const auto id{createIdWithType(objectMap, game::IdType::StackDestroyed)}; + + auto obj{objectMap->vftable->findScenarioObjectById(objectMap, &id)}; + if (!obj) { + return nullptr; + } + + return static_cast(obj); +} + +bool isInventoryContainsItem(const game::IMidgardObjectMap* objectMap, + const game::CMidInventory& inventory, + const game::CMidgardID& globalItemId) +{ + using namespace game; + + const int itemsTotal{inventory.vftable->getItemsCount(&inventory)}; + for (int i = 0; i < itemsTotal; ++i) { + const CMidgardID* id{inventory.vftable->getItem(&inventory, i)}; + + auto obj{objectMap->vftable->findScenarioObjectById(objectMap, id)}; + auto item{static_cast(obj)}; + if (item->globalItemId == globalItemId) { + return true; + } + } + + return false; +} + +const game::CMqPoint getObjectEntrance(const game::CMqPoint& position, int sizeX, int sizeY) +{ + game::CMqPoint entrance; + entrance.x = position.x + sizeX - 1; + entrance.y = position.y + sizeY - 1; + + return entrance; +} + } // namespace hooks diff --git a/mss32/src/globalvariables.cpp b/mss32/src/globalvariables.cpp new file mode 100644 index 00000000..f3f12cf1 --- /dev/null +++ b/mss32/src/globalvariables.cpp @@ -0,0 +1,52 @@ +/* + * This file is part of the modding toolset for Disciples 2. + * (https://github.com/VladimirMakeev/D2ModdingToolset) + * Copyright (C) 2024 Vladimir Makeev. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "globalvariables.h" +#include "version.h" +#include + +namespace game::GlobalVariablesApi { + +// clang-format off +static std::array functions = {{ + // Akella + Api{ + (Api::Constructor)0x587a19, + }, + // Russobit + Api{ + (Api::Constructor)0x587a19, + }, + // Gog + Api{ + (Api::Constructor)0x586bce, + }, + // Scenario Editor + Api{ + (Api::Constructor)0x530a98, + }, +}}; +// clang-format on + +Api& get() +{ + return functions[static_cast(hooks::gameVersion())]; +} + +} \ No newline at end of file diff --git a/mss32/src/globalvariableshooks.cpp b/mss32/src/globalvariableshooks.cpp new file mode 100644 index 00000000..66a0cf7f --- /dev/null +++ b/mss32/src/globalvariableshooks.cpp @@ -0,0 +1,101 @@ +/* + * This file is part of the modding toolset for Disciples 2. + * (https://github.com/VladimirMakeev/D2ModdingToolset) + * Copyright (C) 2024 Vladimir Makeev. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "globalvariableshooks.h" +#include "dbffile.h" +#include "globalvariables.h" +#include "log.h" +#include "mempool.h" +#include "originalfunctions.h" +#include +#include +#include + +namespace hooks { + +static const char stealRmktColumn[]{"STEAL_RMKT"}; +static const char rmktRiotMinColumn[]{"RMKT_R_MIN"}; +static const char rmktRiotMaxColumn[]{"RMKT_R_MAX"}; + +static bool hasCustomVariables(const utils::DbfFile& dbfFile) +{ + return dbfFile.column(stealRmktColumn) || dbfFile.column(rmktRiotMinColumn) + || dbfFile.column(rmktRiotMaxColumn); +} + +game::GlobalVariables* __fastcall globalVariablesCtorHooked(game::GlobalVariables* thisptr, + int /*%edx*/, + const char* directory, + game::CProxyCodeBaseEnv* proxy) +{ + using namespace game; + + static const char dbfFileName[]{"GVars.dbf"}; + + const auto originalCtor{getOriginalFunctions().globalVariablesCtor}; + + // We can not open database right after original c-tor, file is still being locked + utils::DbfFile dbfFile; + if (!dbfFile.open(std::filesystem::path{directory} / dbfFileName)) { + logError("mssProxyError.log", fmt::format("Could not open {:s}", dbfFileName)); + return originalCtor(thisptr, directory, proxy); + } + + if (!hasCustomVariables(dbfFile)) { + return originalCtor(thisptr, directory, proxy); + } + + utils::DbfRecord record; + if (!dbfFile.record(record, 0u) || record.isDeleted()) { + return originalCtor(thisptr, directory, proxy); + } + + const auto extendedDataSize{sizeof(GlobalVariablesDataHooked)}; + + auto extendedData = (GlobalVariablesDataHooked*)Memory::get().allocate(extendedDataSize); + std::memset(extendedData, 0, extendedDataSize); + + int stealAmount{}; + if (record.value(stealAmount, stealRmktColumn)) { + extendedData->stealRmkt = std::clamp(stealAmount, 0, INT_MAX); + } + + int riotMin{}; + if (record.value(riotMin, rmktRiotMinColumn)) { + extendedData->rmktRiotMin = std::clamp(riotMin, 0, INT_MAX); + } + + int riotMax{}; + if (record.value(riotMax, rmktRiotMaxColumn)) { + extendedData->rmktRiotMax = std::clamp(riotMax, 0, INT_MAX); + } + + originalCtor(thisptr, directory, proxy); + + // Copy original variables into extended data structure and assign it instead of original. + // We do not free memory of GlobalVariablesData::tutorialName here, original d-tor will do + std::memcpy(extendedData, thisptr->data, sizeof(GlobalVariablesData)); + Memory::get().freeNonZero(thisptr->data); + + thisptr->data = extendedData; + + return thisptr; +} + +} // namespace hooks diff --git a/mss32/src/hooks.cpp b/mss32/src/hooks.cpp index 2193d159..e3ab0ff5 100644 --- a/mss32/src/hooks.cpp +++ b/mss32/src/hooks.cpp @@ -58,6 +58,8 @@ #include "customattacks.h" #include "customattackutils.h" #include "custombuildingcategories.h" +#include "customnobleactioncategories.h" +#include "customnobleactionhooks.h" #include "d2string.h" #include "dbfaccess.h" #include "dbtable.h" @@ -90,12 +92,15 @@ #include "fortification.h" #include "gameutils.h" #include "globaldata.h" +#include "globalvariableshooks.h" #include "groupupgradehooks.h" #include "idlist.h" #include "interfmanager.h" #include "interftexthooks.h" #include "intvector.h" #include "isoenginegroundhooks.h" +#include "isoview.h" +#include "isoviewhooks.h" #include "itembase.h" #include "itemcategory.h" #include "itemexpotionboost.h" @@ -132,6 +137,7 @@ #include "midscenvariables.h" #include "midserverlogic.h" #include "midserverlogichooks.h" +#include "midsite.h" #include "midstack.h" #include "midunitdescriptor.h" #include "midunitdescriptorhooks.h" @@ -146,6 +152,8 @@ #include "netplayerinfo.h" #include "netsingleplayer.h" #include "netsingleplayerhooks.h" +#include "objectinterf.h" +#include "objectinterfhooks.h" #include "originalfunctions.h" #include "phasegame.h" #include "playerbuildings.h" @@ -156,13 +164,30 @@ #include "scenariodata.h" #include "scenariodataarray.h" #include "scenarioinfo.h" +#include "scenedit.h" +#include "scenedithooks.h" +#include "scenpropinterfhooks.h" #include "settings.h" +#include "sitecategoryhooks.h" #include "sitemerchantinterf.h" #include "sitemerchantinterfhooks.h" #include "smartptr.h" #include "stackbattleactionmsg.h" +#include "stacktemplatecache.h" #include "summonhooks.h" +#include "taskobjaddsite.h" +#include "taskobjaddsitehooks.h" +#include "taskobjprop.h" +#include "taskobjprophooks.h" #include "testconditionhooks.h" +#include "testkillstackhooks.h" +#include "testleaderownitemhooks.h" +#include "testleadertozone.h" +#include "testleadertozonehooks.h" +#include "testownitemhooks.h" +#include "teststackexistshooks.h" +#include "textboxinterf.h" +#include "textids.h" #include "transformotherhooks.h" #include "transformselfhooks.h" #include "umattack.h" @@ -183,6 +208,8 @@ #include "usunitimpl.h" #include "utils.h" #include "version.h" +#include "visitorcreatesite.h" +#include "visitorcreatesitehooks.h" #include "visitors.h" #include #include @@ -201,6 +228,8 @@ static Hooks getGameHooks() auto& fn = gameFunctions(); auto& battle = BattleMsgDataApi::get(); auto& orig = getOriginalFunctions(); + auto& serverLogic{CMidServerLogicApi::get()}; + auto& eventConditions{ITestConditionApi::get()}; // clang-format off Hooks hooks{ @@ -329,7 +358,7 @@ static Hooks getGameHooks() // Fix missing modifiers of alternative attacks {fn.getUnitAttacks, getUnitAttacksHooked}, // Support custom event conditions - {ITestConditionApi::get().create, createTestConditionHooked, (void**)&orig.createTestCondition}, + {eventConditions.create, createTestConditionHooked, (void**)&orig.createTestCondition}, // Support custom event effects //{IEffectResultApi::get().create, createEffectResultHooked, (void**)&orig.createEffectResult}, // Support additional as well as high tier units in hire list @@ -380,6 +409,44 @@ static Hooks getGameHooks() {fn.changeUnitXpCheckUpgrade, changeUnitXpCheckUpgradeHooked}, // Allow player to customize movement cost {fn.computeMovementCost, computeMovementCostHooked}, + // Support custom scripts for AI battle actions + {battle.aiChooseBattleAction, aiChooseBattleActionHooked, (void**)&orig.aiChooseBattleAction}, + // Profile and speed up events system + {serverLogic.applyEventEffectsAndCheckMidEventTriggerers, applyEventEffectsAndCheckMidEventTriggerersHooked, (void**)&orig.applyEventEffectsAndCheckMidEventTriggerers}, + {serverLogic.stackMove, stackMoveHooked, (void**)&orig.stackMove}, + {serverLogic.filterAndProcessEventsNoPlayer, filterAndProcessEventsNoPlayerHooked, (void**)&orig.filterAndProcessEventsNoPlayer}, + {serverLogic.filterAndProcessEvents, filterAndProcessEventsHooked, (void**)&orig.filterAndProcessEvents}, + {serverLogic.checkEventConditions, checkEventConditionsHooked, (void**)&orig.checkEventConditions}, + {serverLogic.executeEventEffects, executeEventEffectsHooked, (void**)&orig.executeEventEffects}, + {eventConditions.testFrequency, testFreqHooked, (void**)&orig.testFrequency}, + {eventConditions.testLocation, testLocationHooked, (void**)&orig.testLocation}, + {eventConditions.testLeaderToZone, testLeaderToZoneHooked}, + {eventConditions.testEnterCity, testEnterCityHooked, (void**)&orig.testEnterCity}, + {eventConditions.testLeaderToCity, testLeaderToCityHooked, (void**)&orig.testLeaderToCity}, + {eventConditions.testOwnCity, testOwnCityHooked, (void**)&orig.testOwnCity}, + {eventConditions.testKillStack, testKillStackHooked}, + {eventConditions.testOwnItem, testOwnItemHooked}, + {eventConditions.testLeaderOwnItem, testLeaderOwnItemHooked}, + {eventConditions.testDiplomacy, testDiplomacyHooked, (void**)&orig.testDiplomacy}, + {eventConditions.testAlliance, testAllianceHooked, (void**)&orig.testAlliance}, + {eventConditions.testLootRuin, testLootRuinHooked, (void**)&orig.testLootRuin}, + {eventConditions.testTransformLand, testTransformLandHooked, (void**)&orig.testTransformLand}, + {eventConditions.testVisitSite, testVisitSiteHooked, (void**)&orig.testVisitSite}, + {eventConditions.testItemToLocation, testItemToLocationHooked, (void**)&orig.testItemToLocation}, + {eventConditions.testStackExists, testStackExistsHooked}, + {eventConditions.testVarInRange, testVarInRangeHooked, (void**)&orig.testVarInRange}, + {fn.removeStack, removeStackHooked, (void**)&orig.removeStack}, + {VisitorApi::get().setStackSrcTemplate, setStackSrcTemplateHooked, (void**)&orig.setStackSrcTemplate}, + // Support resource market site + {CMainView2Api::get().handleCmdStackVisitMsg, mainView2HandleCmdStackVisitMsgHooked, (void**)&orig.handleCmdStackVisitMsg}, + {CMidServerLogicApi::get().constructor, midServerLogicCtorHooked, (void**)&orig.midServerLogicCtor}, + {fn.getSiteSound, getSiteSoundHooked}, + {fn.siteHasSound, siteHasSoundHooked}, + // Support custom noble actions + {NobleActionsApi::get().create, createNobleActionResultHooked, (void**)&orig.createNobleActionResult}, + {fn.getSiteNobleActions, getSiteNobleActionsHooked, (void**)&orig.getSiteNobleActions}, + {fn.getPossibleNobleActions, getPossibleNobleActionsHooked, (void**)&orig.getPossibleNobleActions}, + {fn.getNobleActionResultDescription, getNobleActionResultDescriptionHooked, (void**)&orig.getNobleActionResultDescription}, }; // clang-format on @@ -547,6 +614,15 @@ static Hooks getScenarioEditorHooks() {CMidgardScenarioMapApi::get().checkObjects, checkMapObjectsHooked}, // Support custom modifiers {CMidgardScenarioMapApi::get().stream, scenarioMapStreamHooked, (void**)&orig.scenarioMapStream}, + // Allow changing Netrals attitude from scenario properties menu + {editor::CScenPropInterfApi::get().constructor, scenPropInterfCtorHooked, (void**)&orig.scenPropInterfCtor}, + // Support new sites + {editor::CVisitorCreateSiteApi::get().canApply, visitorCreateSiteCanApplyHooked}, + {editor::CVisitorCreateSiteApi::get().apply, visitorCreateSiteApplyHooked}, + {editor::CObjectInterfApi::get().createTaskObj, createTaskObjHooked, (void**)&orig.createTaskObj}, + {editor::CTaskObjPropApi::get().doAction, taskObjPropDoActionHooked, (void**)&orig.taskObjPropDoAction}, + {editor::CTaskObjAddSiteApi::get().doAction, taskObjAddSiteDoActionHooked, (void**)&orig.taskObjAddSiteDoAction}, + {CScenEditApi::get().readScenData, readScenDataHooked, (void**)&orig.readScenData}, }; // clang-format on @@ -738,6 +814,23 @@ Hooks getHooks() // Fix input of 'io' (U+0451) and 'IO' (U+0401) hooks.emplace_back(HookInfo{CEditBoxInterfApi::get().isCharValid, editBoxIsCharValidHooked}); + // Support new sites + hooks.emplace_back( + HookInfo{LSiteCategoryTableApi::get().constructor, siteCategoryTableCtorHooked}); + hooks.emplace_back(HookInfo{fn.getSiteNameSuffix, getSiteNameSuffixHooked}); + // Support new site on a strategic map + hooks.emplace_back(HookInfo{ImageLayerListApi::get().getMapElementIsoLayerImages, + getMapElementIsoLayerImagesHooked, + (void**)&orig.getMapElementIsoLayerImages}); + // Support encyclopedia info for a new sites + hooks.emplace_back(HookInfo{fn.updateEncLayoutSite, updateEncLayoutSiteHooked}); + // Support custom noble action categories + hooks.emplace_back( + HookInfo{LNobleActionCatTableApi::get().constructor, nobleActionCatTableCtorHooked}); + // Support new global variables + hooks.emplace_back(HookInfo{GlobalVariablesApi::get().constructor, globalVariablesCtorHooked, + (void**)&orig.globalVariablesCtor}); + return hooks; } @@ -1097,7 +1190,7 @@ game::CBuildingBranch* __fastcall buildingBranchCtorHooked(game::CBuildingBranch const auto& phase = CPhaseApi::get(); auto playerId = phase.getCurrentPlayerId(&phaseGame->phase); - auto objectMap = phase.getObjectMap(&phaseGame->phase); + auto objectMap = phase.getDataCache(&phaseGame->phase); auto findScenarioObjectById = objectMap->vftable->findScenarioObjectById; auto playerObject = findScenarioObjectById(objectMap, playerId); @@ -1630,13 +1723,18 @@ void __stdcall beforeBattleTurnHooked(game::BattleMsgData* battleMsgData, // Fix free transform-self to disable Wait/Defend/Retreat auto unitInfo = battle.getUnitInfoById(battleMsgData, unitId); if (unitInfo) - unitInfo->unitFlags.parts.attackedOnceOfTwice = 1; + unitInfo->unitFlags.parts.attackedOnceOfTwice = true; } freeTransformSelf.turnCount++; } void __stdcall throwExceptionHooked(const game::os_exception* thisptr, const void* throwInfo) { + // TODO: this is wrong, os_exception is a base class and it does not store message as a char* + // Instead, there are plenty of child classes that store messages in their own way. + // There should be proper way to get message, perhaps using vftable. + // This particular example leads to a crash when there are problems with .dlg files and + // CAutoDialogException is thrown if (thisptr && thisptr->message) { showErrorMessageBox(fmt::format("Caught exception '{:s}'.\n" "The {:s} will probably crash now.", @@ -1777,7 +1875,7 @@ bool __stdcall enableUnitInHireListUiHooked(const game::CMidPlayer* player, { using namespace game; - auto objectMap = CPhaseApi::get().getObjectMap(&phaseGame->phase); + auto objectMap = CPhaseApi::get().getDataCache(&phaseGame->phase); auto playerBuildings = getPlayerBuildings(objectMap, player); if (!playerBuildings) { @@ -1994,11 +2092,26 @@ int __stdcall loadScenarioMapHooked(int a1, game::CMidStreamEnvFile* streamEnv, game::CMidgardScenarioMap* scenarioMap) { - int result = getOriginalFunctions().loadScenarioMap(a1, streamEnv, scenarioMap); + stackTemplateCacheClear(); + const int result = getOriginalFunctions().loadScenarioMap(a1, streamEnv, scenarioMap); // Write-mode validation is done in midUnitStreamHooked validateUnits(scenarioMap); + using namespace game; + + const auto& dynamicCast{RttiApi::get().dynamicCast}; + const auto& rtti{RttiApi::rtti()}; + + auto addStackFromTemplateToCache = [&dynamicCast, &rtti](const IMidScenarioObject* obj) { + auto stack{(const CMidStack*)dynamicCast(obj, 0, rtti.IMidScenarioObjectType, + rtti.CMidStackType, 0)}; + if (stack->sourceTemplateId != emptyId) { + stackTemplateCacheAdd(stack->sourceTemplateId, stack->id); + } + }; + + forEachScenarioObject(scenarioMap, IdType::Stack, addStackFromTemplateToCache); return result; } @@ -2244,20 +2357,10 @@ bool __stdcall isUnitUpgradePendingHooked(const game::CMidgardID* unitId, return false; } -bool __fastcall editBoxIsCharValidHooked(const game::EditBoxData* thisptr, - int /*%edx*/, - char character) +bool __fastcall editBoxIsCharValidHooked(const game::EditBoxData* thisptr, int /*%edx*/, char ch) { using namespace game; - // Cast to int using unsigned char - // so extended symbols (>= 127) are handled correctly by isalpha() - const int ch = static_cast(character); - - if (!(ch && ch != VK_ESCAPE && (thisptr->allowEnter || ch != '\n' && ch != '\r'))) { - return false; - } - // clang-format off // These characters are language specific and depend on actual code page being used. // For example, in cp-1251 they will represent Russian alphabet. @@ -2277,77 +2380,75 @@ bool __fastcall editBoxIsCharValidHooked(const game::EditBoxData* thisptr, }; // clang-format on - if (thisptr->filter == EditFilter::TextOnly) { - if (isalpha(ch) || isspace(ch)) { - return true; - } - - // Check if ch is language specific character - if (strchr(reinterpret_cast(languageSpecific), ch)) { - return true; - } + // Cast to int using unsigned char + // so extended symbols (>= 127) are handled correctly by isalpha() + const int a2 = static_cast(ch); + if (!a2 || a2 == VK_ESCAPE || !thisptr->allowEnter && (a2 == '\n' || a2 == '\r')) { return false; } - if (thisptr->filter >= EditFilter::AlphaNum) { - if (thisptr->filter == EditFilter::DigitsOnly) { - return isdigit(ch) != 0; - } + const int filter = (int)thisptr->filter; - if (thisptr->filter >= EditFilter::AlphaNumNoSlash) { - if (thisptr->filter >= EditFilter::NamesDot) { - if (thisptr->filter == EditFilter::NamesDot) { - return true; - } - - if ((ch < 'a' || ch > 'z') && (ch < 'A' || ch > 'Z') && (ch < '0' || ch > '9') - && ch != ' ' && ch != '\\' && ch != '_' && ch != '-') { - return false; - } + const int v2 = filter - 1; + if (!v2) { + if (isalpha(a2) || isspace(a2)) { + return true; + } - return true; - } + const char* v10 = strchr((const char*)languageSpecific, a2); + return v10 != nullptr; + } - if (iscntrl(ch)) { - return false; - } + const int v3 = v2 - 1; + if (!v3) { + // Remove two symbols from forbidden list. + // This allows players to enter 'io' (U+0451) and 'IO' (U+0401) in cp-1251 + static const unsigned char forbiddenSymbols[] = {'`', '^', '~', /* 0xA8, 0xB8,*/ 0}; - if (!strchr("/?*\"<>|+':\\", ch)) { + if (!strchr((const char*)forbiddenSymbols, a2)) { + if (isalpha(a2) || isdigit(a2) || isspace(a2) || ispunct(a2)) { return true; } - return false; - } - - if (iscntrl(ch)) { - return false; - } - - if (!strchr("/?*\"<>|+'", ch)) { - return true; + const char* v10 = strchr((const char*)languageSpecific, a2); + return v10 != nullptr; } return false; } - // Remove two symbols from forbidden list. - // This allows players to enter 'io' (U+0451) and 'IO' (U+0401) in cp-1251 - static const char forbiddenSymbols[] = {'`', '^', '~', /* 0xA8, 0xB8, */ 0}; + const int v4 = v3 - 1; + if (!v4) { + return isdigit(a2) != 0; + } - if (!strchr(forbiddenSymbols, ch)) { - if (isalpha(ch) || isdigit(ch) || isspace(ch) || ispunct(ch)) { - return true; + const int v5 = v4 - 1; + if (!v5) { + if (iscntrl(a2)) { + return false; } - if (strchr(reinterpret_cast(languageSpecific), ch)) { - return true; + const char* v8 = strchr("/?*\"<>|+'", a2); + return !v8; + } + + const int v6 = v5 - 1; + if (!v6) { + if (iscntrl(a2)) { + return false; } - return false; + const char* v8 = strchr("/?*\"<>|+':\\", a2); + return !v8; } - return false; + if (v6 == 1) { + return isalnum(a2) || strchr((const char*)languageSpecific, a2) != nullptr || a2 == ' ' + || a2 == '\'' || a2 == '_' || a2 == '-'; + } else { + return true; + } } // Conditions should be checked from most critical to less critical to provide correct flow of unit @@ -2396,4 +2497,157 @@ game::BuildingStatus __stdcall getBuildingStatusHooked(const game::IMidgardObjec return BuildingStatus::CanBeBuilt; } +bool __stdcall removeStackHooked(const game::CMidgardID* stackId, + game::CMidgardPlan* plan, + game::IMidgardObjectMap* objectMap, + game::CScenarioVisitor* visitor) +{ + using namespace game; + + const CMidStack* stack{getStack(objectMap, stackId)}; + if (stack && stack->sourceTemplateId != emptyId) { + stackTemplateCacheRemove(stack->sourceTemplateId, stack->id); + } + + return getOriginalFunctions().removeStack(stackId, plan, objectMap, visitor); +} + +bool __stdcall setStackSrcTemplateHooked(const game::CMidgardID* stackId, + const game::CMidgardID* stackTemplateId, + game::IMidgardObjectMap* objectMap, + int apply) +{ + const bool result{ + getOriginalFunctions().setStackSrcTemplate(stackId, stackTemplateId, objectMap, apply)}; + + if (apply == 1 && result) { + stackTemplateCacheAdd(*stackTemplateId, *stackId); + } + + return result; +} + +const char* __stdcall getSiteNameSuffixHooked(const game::LSiteCategory* siteCategory) +{ + using namespace game; + + const auto& categories = SiteCategories::get(); + + const auto id = siteCategory->id; + + if (id == categories.merchant->id) { + return "MERH"; + } + + if (id == categories.mageTower->id) { + return "MAGE"; + } + + if (id == categories.mercenaries->id) { + return "MERC"; + } + + if (customSiteCategories().exists && id == customSiteCategories().resourceMarket.id) { + return "RMKT"; + } + + return "TRAI"; +} + +void __stdcall updateEncLayoutSiteHooked(const game::CMidSite* site, game::CTextBoxInterf* textBox) +{ + if (!site) { + return; + } + + using namespace game; + + // \hC;\vC;\fLarge;%NAME%\fNormal;\n%TYPE%\n\n%DESC% + std::string str{getInterfaceText("X005TA0868")}; + + const auto& categories{SiteCategories::get()}; + const auto id{site->siteCategory.id}; + + const char* textId{}; + if (id == categories.merchant->id) { + // (Merchant) + textId = "X005TA0873"; + } else if (id == categories.mageTower->id) { + // (Magic shop) + textId = "X005TA0874"; + } else if (id == categories.mercenaries->id) { + // (Mercenary) + textId = "X005TA0875"; + } else if (customSiteCategories().exists && id == customSiteCategories().resourceMarket.id) { + textId = textIds().resourceMarket.encyDesc.c_str(); + } else { + // (Trainer) + textId = "X005TA0876"; + } + + const std::string type{getInterfaceText(textId)}; + + const char* name{site->title.string ? site->title.string : ""}; + const char* description{site->description.string ? site->description.string : ""}; + + replace(str, "%NAME%", name); + replace(str, "%TYPE%", type); + replace(str, "%DESC%", description); + + CTextBoxInterfApi::get().setString(textBox, str.c_str() ? str.c_str() : ""); +} + +game::String* __stdcall getSiteSoundHooked(game::String* soundName, const game::CMidSite* site) +{ + using namespace game; + + const auto& init{StringApi::get().initFromString}; + + const SiteId id{site->siteCategory.id}; + + if (customSiteCategories().exists && id == customSiteCategories().resourceMarket.id) { + init(soundName, "RESMARKT"); + return soundName; + } + + const auto& sites{SiteCategories::get()}; + + if (id == sites.mageTower->id) { + init(soundName, "MAGICTW"); + return soundName; + } + + if (id == sites.trainer->id) { + init(soundName, "TRAINCMP"); + return soundName; + } + + if (id != sites.merchant->id || site->imgIso) { + init(soundName, ""); + return soundName; + } + + init(soundName, "WINDMILL"); + return soundName; +} + +bool __stdcall siteHasSoundHooked(const game::CMidSite* site) +{ + using namespace game; + + const auto& sites{SiteCategories::get()}; + const SiteId id{site->siteCategory.id}; + + if (id == sites.mageTower->id || id == sites.trainer->id + || id == sites.merchant->id && !site->imgIso) { + return true; + } + + if (customSiteCategories().exists && id == customSiteCategories().resourceMarket.id) { + return true; + } + + return false; +} + } // namespace hooks diff --git a/mss32/src/imagelayerlist.cpp b/mss32/src/imagelayerlist.cpp index 80895258..5fa9fe3b 100644 --- a/mss32/src/imagelayerlist.cpp +++ b/mss32/src/imagelayerlist.cpp @@ -30,24 +30,28 @@ static std::array functions = {{ (Api::PushBack)0x522151, (Api::Clear)0x522289, (Api::AddShieldImageLayer)0x5bca73, + (Api::GetMapElementIsoLayerImages)0x5bc668, }, // Russobit Api{ (Api::PushBack)0x522151, (Api::Clear)0x522289, (Api::AddShieldImageLayer)0x5bca73, + (Api::GetMapElementIsoLayerImages)0x5bc668, }, // Gog Api{ (Api::PushBack)0x524c10, (Api::Clear)0x407a04, (Api::AddShieldImageLayer)0x5bbb37, + (Api::GetMapElementIsoLayerImages)0x5bb72c, }, // Scenario Editor Api{ (Api::PushBack)0x55e5b7, (Api::Clear)0x5541ca, (Api::AddShieldImageLayer)0x55d8eb, + (Api::GetMapElementIsoLayerImages)0x55d4e0, }, }}; // clang-format on diff --git a/mss32/src/isoview.cpp b/mss32/src/isoview.cpp new file mode 100644 index 00000000..3d165a42 --- /dev/null +++ b/mss32/src/isoview.cpp @@ -0,0 +1,36 @@ +/* + * This file is part of the modding toolset for Disciples 2. + * (https://github.com/VladimirMakeev/D2ModdingToolset) + * Copyright (C) 2024 Vladimir Makeev. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "isoview.h" + +namespace game::editor::CIsoViewApi { + +Api& get() +{ + // clang-format off + static Api api + { + (Api::UpdateSelectedTileInfo)0x45fa7c, + }; + // clang-format on + + return api; +} + +} // namespace game::editor::CIsoViewApi diff --git a/mss32/src/isoviewhooks.cpp b/mss32/src/isoviewhooks.cpp new file mode 100644 index 00000000..96f860d1 --- /dev/null +++ b/mss32/src/isoviewhooks.cpp @@ -0,0 +1,123 @@ +/* + * This file is part of the modding toolset for Disciples 2. + * (https://github.com/VladimirMakeev/D2ModdingToolset) + * Copyright (C) 2021 Vladimir Makeev. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "isoviewhooks.h" +#include "gameutils.h" +#include "groundcat.h" +#include "image2text.h" +#include "isoview.h" +#include "midgardmap.h" +#include "midgardscenariomap.h" +#include "scenedit.h" +#include "terraincat.h" +#include "utils.h" +#include + +namespace hooks { + +static const char* groundToString(const game::LGroundCategory& ground) +{ + using namespace game; + + const auto& categories = GroundCategories::get(); + if (ground.id == categories.plain->id) { + return "Plains"; + } + + if (ground.id == categories.water->id) { + return "Water"; + } + + if (ground.id == categories.forest->id) { + return "Forest"; + } + + if (ground.id == categories.mountain->id) { + return "Mountain"; + } + + return ""; +} + +static const char* terrainToString(const game::LTerrainCategory& terrain) +{ + using namespace game; + + const auto& categories = TerrainCategories::get(); + if (terrain.id == categories.neutral->id) { + return "Neutral"; + } + + if (terrain.id == categories.human->id) { + return "Empire"; + } + + if (terrain.id == categories.dwarf->id) { + return "Mountain Clans"; + } + + if (terrain.id == categories.heretic->id) { + return "Legions of the Damned"; + } + + if (terrain.id == categories.undead->id) { + return "Undead Hordes"; + } + + if (terrain.id == categories.elf->id) { + return "Elven Alliance"; + } + + return ""; +} + +void __fastcall updateSelectedTileInfoHooked(game::editor::CIsoView* thisptr, + int /* %edx */, + const game::CMqPoint* mapPosition) +{ + using namespace game; + + const CScenEdit* scenEdit = CScenEditApi::get().instance(); + const IMidgardObjectMap* objectMap = scenEdit->data->unknown2->data->scenarioMap; + + const CMidgardMap* map = getMidgardMap(objectMap); + const auto& mapApi = CMidgardMapApi::get(); + + LTerrainCategory terrain{}; + if (!mapApi.getTerrain(map, &terrain, mapPosition, objectMap)) { + return; + } + + LGroundCategory ground{}; + if (!mapApi.getGround(map, &ground, mapPosition, objectMap)) { + return; + } + + // \c000;255;000;%X%:%Y%\n%GROUND%\n%TERRAIN% + std::string info = getInterfaceText("X100TA0774"); + + replace(info, "%X%", std::to_string(mapPosition->x)); + replace(info, "%Y%", std::to_string(mapPosition->y)); + replace(info, "%GROUND%", groundToString(ground)); + replace(info, "%TERRAIN%", terrainToString(terrain)); + + CImage2TextApi::get().setText(thisptr->isoViewData->selectedTileInfo, info.c_str()); +} + +} // namespace hooks diff --git a/mss32/src/itemtransferhooks.cpp b/mss32/src/itemtransferhooks.cpp index 68767b16..c3267c31 100644 --- a/mss32/src/itemtransferhooks.cpp +++ b/mss32/src/itemtransferhooks.cpp @@ -116,7 +116,7 @@ static void transferItems(const std::vector& items, { using namespace game; - auto objectMap = CPhaseApi::get().getObjectMap(&phaseGame->phase); + auto objectMap = CPhaseApi::get().getDataCache(&phaseGame->phase); const auto& exchangeItem = VisitorApi::get().exchangeItem; const auto& sendExchangeItemMsg = NetMessagesApi::get().sendStackExchangeItemMsg; @@ -139,7 +139,7 @@ static void transferCityToStack(game::CPhaseGame* phaseGame, { using namespace game; - auto objectMap = CPhaseApi::get().getObjectMap(&phaseGame->phase); + auto objectMap = CPhaseApi::get().getDataCache(&phaseGame->phase); auto obj = objectMap->vftable->findScenarioObjectById(objectMap, cityId); if (!obj) { logError("mssProxyError.log", fmt::format("Could not find city {:s}", idToString(cityId))); @@ -205,7 +205,7 @@ static void transferStackToCity(game::CPhaseGame* phaseGame, { using namespace game; - auto objectMap = CPhaseApi::get().getObjectMap(&phaseGame->phase); + auto objectMap = CPhaseApi::get().getDataCache(&phaseGame->phase); auto obj = objectMap->vftable->findScenarioObjectById(objectMap, cityId); if (!obj) { logError("mssProxyError.log", fmt::format("Could not find city {:s}", idToString(cityId))); @@ -364,7 +364,7 @@ static void transferStackToStack(game::CPhaseGame* phaseGame, { using namespace game; - auto objectMap = CPhaseApi::get().getObjectMap(&phaseGame->phase); + auto objectMap = CPhaseApi::get().getDataCache(&phaseGame->phase); auto srcObj = objectMap->vftable->findScenarioObjectById(objectMap, srcStackId); if (!srcObj) { logError("mssProxyError.log", @@ -539,7 +539,7 @@ static void transferBagToStack(game::CPhaseGame* phaseGame, { using namespace game; - auto objectMap = CPhaseApi::get().getObjectMap(&phaseGame->phase); + auto objectMap = CPhaseApi::get().getDataCache(&phaseGame->phase); auto bagObj = objectMap->vftable->findScenarioObjectById(objectMap, bagId); if (!bagObj) { logError("mssProxyError.log", fmt::format("Could not find bag {:s}", idToString(bagId))); @@ -592,7 +592,7 @@ static void transferStackToBag(game::CPhaseGame* phaseGame, { using namespace game; - auto objectMap = CPhaseApi::get().getObjectMap(&phaseGame->phase); + auto objectMap = CPhaseApi::get().getDataCache(&phaseGame->phase); auto stackObj = objectMap->vftable->findScenarioObjectById(objectMap, stackId); if (!stackObj) { logError("mssProxyError.log", @@ -758,7 +758,7 @@ static void sellItemsToMerchant(game::CPhaseGame* phaseGame, { using namespace game; - auto objectMap = CPhaseApi::get().getObjectMap(&phaseGame->phase); + auto objectMap = CPhaseApi::get().getDataCache(&phaseGame->phase); auto stackObj = objectMap->vftable->findScenarioObjectById(objectMap, stackId); if (!stackObj) { logError("mssProxyError.log", @@ -941,7 +941,7 @@ void __fastcall merchantSellValuables(game::CSiteMerchantInterf* thisptr, int /* using namespace game; auto phaseGame = thisptr->dragDropInterf.phaseGame; - auto objectMap = CPhaseApi::get().getObjectMap(&phaseGame->phase); + auto objectMap = CPhaseApi::get().getDataCache(&phaseGame->phase); auto stackId = &thisptr->data->stackId; const auto sellPrice = computeItemsSellPrice(objectMap, stackId, isValuable); @@ -972,7 +972,7 @@ void __fastcall merchantSellAll(game::CSiteMerchantInterf* thisptr, int /*%edx*/ using namespace game; auto phaseGame = thisptr->dragDropInterf.phaseGame; - auto objectMap = CPhaseApi::get().getObjectMap(&phaseGame->phase); + auto objectMap = CPhaseApi::get().getDataCache(&phaseGame->phase); auto stackId = &thisptr->data->stackId; const auto sellPrice = computeItemsSellPrice(objectMap, stackId); diff --git a/mss32/src/log.cpp b/mss32/src/log.cpp index b4883d09..1dba50f3 100644 --- a/mss32/src/log.cpp +++ b/mss32/src/log.cpp @@ -28,7 +28,7 @@ namespace hooks { -static void logAction(const std::string& logFile, const std::string& message) +static void logAction(std::string_view logFile, std::string_view message) { using namespace std::chrono; @@ -42,14 +42,14 @@ static void logAction(const std::string& logFile, const std::string& message) file << "[" << std::put_time(&tm, "%c") << "]\t" << tid << "\t" << message << "\n"; } -void logDebug(const std::string& logFile, const std::string& message) +void logDebug(std::string_view logFile, std::string_view message) { if (userSettings().debugMode) { logAction(logFile, message); } } -void logError(const std::string& logFile, const std::string& message) +void logError(std::string_view logFile, std::string_view message) { logAction(logFile, message); } diff --git a/mss32/src/main.cpp b/mss32/src/main.cpp index 68c7858d..0e680b79 100644 --- a/mss32/src/main.cpp +++ b/mss32/src/main.cpp @@ -19,6 +19,7 @@ #pragma comment(lib, "detours.lib") +#include "customaibattle.h" #include "customattacks.h" #include "custommodifiers.h" #include "hooks.h" @@ -248,5 +249,6 @@ BOOL APIENTRY DllMain(HMODULE hDll, DWORD reason, LPVOID reserved) // Thread sync is excessive because the data is read-only or thread-exclusive once initialized. hooks::initializeCustomAttacks(); hooks::initializeCustomModifiers(); + hooks::initializeCustomAiBattleLogic(); return TRUE; } diff --git a/mss32/src/mainview2.cpp b/mss32/src/mainview2.cpp index ea34c015..8b367bfc 100644 --- a/mss32/src/mainview2.cpp +++ b/mss32/src/mainview2.cpp @@ -30,18 +30,21 @@ static std::array functions = {{ (Api::ShowIsoDialog)0x4893a5, (Api::ShowDialog)0x4889d8, (Api::CreateToggleButtonFunctor)0x48c5d7, + (Api::HandleCmdStackVisitMsg)0x48b62f, }, // Russobit Api{ (Api::ShowIsoDialog)0x4893a5, (Api::ShowDialog)0x4889d8, (Api::CreateToggleButtonFunctor)0x48c5d7, + (Api::HandleCmdStackVisitMsg)0x48b62f, }, // Gog Api{ (Api::ShowIsoDialog)0x488f8e, (Api::ShowDialog)0x4885c1, (Api::CreateToggleButtonFunctor)0x48c19d, + (Api::HandleCmdStackVisitMsg)0x48b218, }, }}; // clang-format on diff --git a/mss32/src/mainview2hooks.cpp b/mss32/src/mainview2hooks.cpp index acc752d5..4d636719 100644 --- a/mss32/src/mainview2hooks.cpp +++ b/mss32/src/mainview2hooks.cpp @@ -18,15 +18,23 @@ */ #include "mainview2hooks.h" +#include "cmdstackvisitmsg.h" #include "dialoginterf.h" +#include "dynamiccast.h" #include "gameimages.h" #include "gameutils.h" #include "isolayers.h" #include "log.h" #include "mainview2.h" #include "mapgraphics.h" +#include "midcommandqueue2.h" +#include "midsite.h" +#include "midtaskopeninterfparamresmarket.h" +#include "originalfunctions.h" #include "phasegame.h" #include "scenarioinfo.h" +#include "sitecategoryhooks.h" +#include "taskmanager.h" #include "togglebutton.h" #include @@ -76,7 +84,7 @@ static void __fastcall mainView2OnToggleGrid(game::CMainView2* thisptr, gridVisible = toggleOn; if (gridVisible) { - auto objectMap{game::CPhaseApi::get().getObjectMap(&thisptr->phaseGame->phase)}; + auto objectMap{game::CPhaseApi::get().getDataCache(&thisptr->phaseGame->phase)}; auto scenarioInfo{getScenarioInfo(objectMap)}; showGrid(scenarioInfo->mapSize); @@ -127,4 +135,45 @@ void __fastcall mainView2ShowIsoDialogHooked(game::CMainView2* thisptr, int /*%e buttonApi.setChecked(toggleButton, gridVisible); } +void __fastcall mainView2HandleCmdStackVisitMsgHooked(game::CMainView2* thisptr, + int /*%edx*/, + const game::CCommandMsg* stackVisitMsg) +{ + using namespace game; + + const auto& dynamicCast{RttiApi::get().dynamicCast}; + const auto& rtti{RttiApi::rtti()}; + + auto msg{(const CCmdStackVisitMsg*)dynamicCast(stackVisitMsg, 0, rtti.CCommandMsgType, + rtti.CCmdStackVisitMsgType, 0)}; + + const auto& phaseApi{CPhaseApi::get()}; + CPhase* phase{&thisptr->phaseGame->phase}; + + IMidgardObjectMap* objectMap{phaseApi.getDataCache(phase)}; + + auto obj{objectMap->vftable->findScenarioObjectById(objectMap, &msg->siteId)}; + auto site{ + (const CMidSite*)dynamicCast(obj, 0, rtti.IMidScenarioObjectType, rtti.CMidSiteType, 0)}; + if (!customSiteCategories().exists + || customSiteCategories().resourceMarket.id != site->siteCategory.id) { + return getOriginalFunctions().handleCmdStackVisitMsg(thisptr, stackVisitMsg); + } + + // Handle stack visiting resource market + ITaskManagerHolder* holder{&thisptr->taskManagerHolder}; + CTaskManager* taskManager{holder->vftable->getTaskManager(holder)}; + + const CMidgardID& siteId{msg->siteId}; + const CMidgardID& visitorStackId{msg->visitorStackId}; + + ITask* task{createMidTaskOpenInterfParamResMarket(taskManager, thisptr->phaseGame, + visitorStackId, siteId)}; + + auto commandQueue{phaseApi.getCommandQueue(phase)}; + CMidCommandQueue2Api::get().processCommands(commandQueue); + + CTaskManagerApi::get().setCurrentTask(taskManager, task); +} + } // namespace hooks diff --git a/mss32/src/mapinterf.cpp b/mss32/src/mapinterf.cpp index 23478340..832c8c46 100644 --- a/mss32/src/mapinterf.cpp +++ b/mss32/src/mapinterf.cpp @@ -30,6 +30,7 @@ Api& get() (Api::CreateTask)0x462918, (Api::CreateTask)0x462b42, (Api::CreateTask)0x462997, + (Api::CreateToggleButtonFunctor)0x462f74, }; // clang-format on diff --git a/mss32/src/menubase.cpp b/mss32/src/menubase.cpp index ac9cda63..6e291b34 100644 --- a/mss32/src/menubase.cpp +++ b/mss32/src/menubase.cpp @@ -39,6 +39,7 @@ static std::array functions = {{ (Api::CreateListBoxFunctor)0x4ea8da, (Api::CreateSpinButtonFunctor)0x4e5867, (Api::CreatePictureFunctor)0x492cb0, + (Api::CreateRadioButtonFunctor)0x4e833a, }, // Russobit Api{ @@ -54,6 +55,7 @@ static std::array functions = {{ (Api::CreateListBoxFunctor)0x4ea8da, (Api::CreateSpinButtonFunctor)0x4e5867, (Api::CreatePictureFunctor)0x492cb0, + (Api::CreateRadioButtonFunctor)0x4e833a, }, // Gog Api{ @@ -69,6 +71,7 @@ static std::array functions = {{ (Api::CreateListBoxFunctor)0x4e9d75, (Api::CreateSpinButtonFunctor)0x4e4f74, (Api::CreatePictureFunctor)0x492766, + (Api::CreateRadioButtonFunctor)0x4e7839, }, }}; diff --git a/mss32/src/midcommandqueue2.cpp b/mss32/src/midcommandqueue2.cpp new file mode 100644 index 00000000..b32919b3 --- /dev/null +++ b/mss32/src/midcommandqueue2.cpp @@ -0,0 +1,48 @@ +/* + * This file is part of the modding toolset for Disciples 2. + * (https://github.com/VladimirMakeev/D2ModdingToolset) + * Copyright (C) 2024 Vladimir Makeev. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "midcommandqueue2.h" +#include "version.h" +#include + +namespace game::CMidCommandQueue2Api { + +// clang-format off +static std::array functions = {{ + // Akella + Api{ + (Api::ProcessCommands)0x410678, + }, + // Russobit + Api{ + (Api::ProcessCommands)0x410678, + }, + // Gog + Api{ + (Api::ProcessCommands)0x410236, + } +}}; +// clang-format on + +Api& get() +{ + return functions[static_cast(hooks::gameVersion())]; +} + +} // namespace game::CMidCommandQueue2Api diff --git a/mss32/src/midcondscript.cpp b/mss32/src/midcondscript.cpp index b1159ca8..5d38606c 100644 --- a/mss32/src/midcondscript.cpp +++ b/mss32/src/midcondscript.cpp @@ -41,6 +41,7 @@ #include "scripts.h" #include "testcondition.h" #include "textboxinterf.h" +#include "timer.h" #include "utils.h" #include #include @@ -718,6 +719,14 @@ bool __fastcall testScriptDoTest(const CTestScript* thisptr, const game::CMidgardID* playerId, const game::CMidgardID* eventId) { +#ifdef D2_MEASURE_EVENTS_TIME + extern const std::string_view eventsPerformanceLog; + ScopedTimer timer{" Test condition 'script'", eventsPerformanceLog}; + + extern long long conditionsTotalTime; + ScopedValueTimer valueTimer{conditionsTotalTime}; +#endif + const auto& body = thisptr->condition->code; if (body.empty()) { return false; diff --git a/mss32/src/midcondvarcmp.cpp b/mss32/src/midcondvarcmp.cpp index 5cfe6b87..8802c722 100644 --- a/mss32/src/midcondvarcmp.cpp +++ b/mss32/src/midcondvarcmp.cpp @@ -38,6 +38,7 @@ #include "radiobuttoninterf.h" #include "testcondition.h" #include "textids.h" +#include "timer.h" #include "utils.h" #include #include @@ -520,6 +521,14 @@ bool __fastcall testVarCmpDoTest(const CTestVarCmp* thisptr, const game::CMidgardID* playerId, const game::CMidgardID* eventId) { +#ifdef D2_MEASURE_EVENTS_TIME + extern const std::string_view eventsPerformanceLog; + ScopedTimer timer{" Test contidion 'var cmp'", eventsPerformanceLog}; + + extern long long conditionsTotalTime; + ScopedValueTimer valueTimer{conditionsTotalTime}; +#endif + auto variables = getScenarioVariables(objectMap); if (!variables) { // Sanity check, this should never happen diff --git a/mss32/src/middatacache.cpp b/mss32/src/middatacache.cpp new file mode 100644 index 00000000..bc11b306 --- /dev/null +++ b/mss32/src/middatacache.cpp @@ -0,0 +1,51 @@ +/* + * This file is part of the modding toolset for Disciples 2. + * (https://github.com/VladimirMakeev/D2ModdingToolset) + * Copyright (C) 2024 Vladimir Makeev. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "middatacache.h" +#include "version.h" +#include + +namespace game::CMidDataCache2Api { + +// clang-format off +static std::array functions = {{ + // Akella + Api{ + (Api::ChangeNotify)0x41781d, + (Api::ChangeNotify)0x41782d, + }, + // Russobit + Api{ + (Api::ChangeNotify)0x41781d, + (Api::ChangeNotify)0x41782d, + }, + // Gog + Api{ + (Api::ChangeNotify)0x417411, + (Api::ChangeNotify)0x417421, + }, +}}; +// clang-format on + +Api& get() +{ + return functions[static_cast(hooks::gameVersion())]; +} + +} // namespace game::CMidDataCache2Api diff --git a/mss32/src/middragdropinterf.cpp b/mss32/src/middragdropinterf.cpp new file mode 100644 index 00000000..8a6847b8 --- /dev/null +++ b/mss32/src/middragdropinterf.cpp @@ -0,0 +1,57 @@ +/* + * This file is part of the modding toolset for Disciples 2. + * (https://github.com/VladimirMakeev/D2ModdingToolset) + * Copyright (C) 2024 Vladimir Makeev. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "middragdropinterf.h" +#include "version.h" +#include + +namespace game::CMidDragDropInterfApi { + +// clang-format off +static std::array functions = {{ + // Akella + Api{ + (Api::Constructor)0x4cc8e2, + (Api::Destructor)0x4cc971, + (Api::RemoveDropTarget)0x56d09b, + (Api::RemoveDropSource)0x56d010, + }, + // Russobit + Api{ + (Api::Constructor)0x4cc8e2, + (Api::Destructor)0x4cc971, + (Api::RemoveDropTarget)0x56d09b, + (Api::RemoveDropSource)0x56d010, + }, + // Gog + Api{ + (Api::Constructor)0x4cbfd9, + (Api::Destructor)0x4cc068, + (Api::RemoveDropTarget)0x56c745, + (Api::RemoveDropSource)0x56c6ba, + }, +}}; +// clang-format on + +Api& get() +{ + return functions[static_cast(hooks::gameVersion())]; +} + +} // namespace game::CMidDragDropInterfApi diff --git a/mss32/src/midfreetask.cpp b/mss32/src/midfreetask.cpp new file mode 100644 index 00000000..4964b913 --- /dev/null +++ b/mss32/src/midfreetask.cpp @@ -0,0 +1,52 @@ +/* + * This file is part of the modding toolset for Disciples 2. + * (https://github.com/VladimirMakeev/D2ModdingToolset) + * Copyright (C) 2024 Vladimir Makeev. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "midfreetask.h" +#include "version.h" +#include + +namespace game::CMidFreeTaskApi { + +// clang-format off +std::array functions = {{ + // Akella + Api{ + (Api::Constructor)0x4d77d5, + }, + // Russobit + Api{ + (Api::Constructor)0x4d77d5, + }, + // Gog + Api{ + (Api::Constructor)0x4d6e0a, + }, + // Scenario Editor + Api{ + (Api::Constructor)0, + }, +}}; +// clang-format on + +Api& get() +{ + return functions[static_cast(hooks::gameVersion())]; +} + +} // namespace game::CMidFreeTaskApi diff --git a/mss32/src/midgardmap.cpp b/mss32/src/midgardmap.cpp index 5599b6b3..9b59e905 100644 --- a/mss32/src/midgardmap.cpp +++ b/mss32/src/midgardmap.cpp @@ -28,21 +28,25 @@ static std::array functions = {{ // Akella Api{ (Api::ChangeTerrain)nullptr, + (Api::GetTerrain)0, (Api::GetGround)0x5e6f63, }, // Russobit Api{ (Api::ChangeTerrain)nullptr, + (Api::GetTerrain)0, (Api::GetGround)0x5e6f63, }, // Gog Api{ (Api::ChangeTerrain)nullptr, + (Api::GetTerrain)0, (Api::GetGround)0x5e5c78, }, // Scenario Editor Api{ (Api::ChangeTerrain)0x4e3d8d, + (Api::GetTerrain)0x4e393e, (Api::GetGround)0x4e39f4, }, }}; diff --git a/mss32/src/midgardplan.cpp b/mss32/src/midgardplan.cpp index 18471931..a555d347 100644 --- a/mss32/src/midgardplan.cpp +++ b/mss32/src/midgardplan.cpp @@ -29,21 +29,33 @@ static std::array functions = {{ Api{ (Api::GetObjectId)0x5f685f, (Api::IsPositionContainsObjects)0x5f69ae, + (Api::CanPlaceSite)nullptr, + (Api::AddMapElement)nullptr, + (Api::GetObjectsAtPoint)nullptr, }, // Russobit Api{ (Api::GetObjectId)0x5f685f, (Api::IsPositionContainsObjects)0x5f69ae, + (Api::CanPlaceSite)nullptr, + (Api::AddMapElement)nullptr, + (Api::GetObjectsAtPoint)nullptr, }, // Gog Api{ (Api::GetObjectId)0x5f54e2, (Api::IsPositionContainsObjects)0x5f5631, + (Api::CanPlaceSite)nullptr, + (Api::AddMapElement)nullptr, + (Api::GetObjectsAtPoint)nullptr, }, // Scenario Editor Api{ (Api::GetObjectId)0x4e4a42, (Api::IsPositionContainsObjects)0x4e4b91, + (Api::CanPlaceSite)0x511248, + (Api::AddMapElement)0x4e4e15, + (Api::GetObjectsAtPoint)0x4e4cf7, }, }}; // clang-format on diff --git a/mss32/src/midserverlogic.cpp b/mss32/src/midserverlogic.cpp index 6515aa3d..60951b0f 100644 --- a/mss32/src/midserverlogic.cpp +++ b/mss32/src/midserverlogic.cpp @@ -30,18 +30,48 @@ static std::array functions = {{ (Api::GetObjectMap)0x4298aa, (Api::SendRefreshInfo)0x42972f, (Api::StackExchangeItem)0x41f5dc, + (Api::ApplyEventEffectsAndCheckMidEventTriggerers)0x41f986, + (Api::StackMove)0x41e983, + (Api::FilterAndProcessEventsNoPlayer)0x442d07, + (Api::CheckAndExecuteEvent)0x4420f8, + (Api::ExecuteEventEffects)0x44267c, + (Api::FilterAndProcessEvents)0x441d45, + (Api::CheckEventConditions)0x44213a, + (Api::Constructor)0x41fea6, + (Api::GetPlayerInfo)0x429866, + (Api::IsCurrentPlayer)0x41e77f, }, // Russobit Api{ (Api::GetObjectMap)0x4298aa, (Api::SendRefreshInfo)0x42972f, (Api::StackExchangeItem)0x41f5dc, + (Api::ApplyEventEffectsAndCheckMidEventTriggerers)0x41f986, + (Api::StackMove)0x41e983, + (Api::FilterAndProcessEventsNoPlayer)0x442d07, + (Api::CheckAndExecuteEvent)0x4420f8, + (Api::ExecuteEventEffects)0x44267c, + (Api::FilterAndProcessEvents)0x441d45, + (Api::CheckEventConditions)0x44213a, + (Api::Constructor)0x41fea6, + (Api::GetPlayerInfo)0x429866, + (Api::IsCurrentPlayer)0x41e77f, }, // Gog Api{ (Api::GetObjectMap)0x5a77e8, (Api::SendRefreshInfo)0x42915a, (Api::StackExchangeItem)0x41f0c4, + (Api::ApplyEventEffectsAndCheckMidEventTriggerers)0x41f46e, + (Api::StackMove)0x41e46b, + (Api::FilterAndProcessEventsNoPlayer)0x44296c, + (Api::CheckAndExecuteEvent)0x441d5d, + (Api::ExecuteEventEffects)0x4422e1, + (Api::FilterAndProcessEvents)0x4419aa, + (Api::CheckEventConditions)0x441d9f, + (Api::Constructor)0x41f98e, + (Api::GetPlayerInfo)0x429291, + (Api::IsCurrentPlayer)0x41e267, }, }}; diff --git a/mss32/src/midserverlogichooks.cpp b/mss32/src/midserverlogichooks.cpp index 0a5463c9..511aa64f 100644 --- a/mss32/src/midserverlogichooks.cpp +++ b/mss32/src/midserverlogichooks.cpp @@ -18,23 +18,71 @@ */ #include "midserverlogichooks.h" +#include "dynamiccast.h" +#include "exchangeresourcesmsg.h" +#include "gameutils.h" #include "idset.h" #include "log.h" #include "logutils.h" #include "midgardscenariomap.h" +#include "midplayer.h" #include "midserver.h" #include "midserverlogic.h" +#include "midsiteresourcemarket.h" +#include "midstack.h" +#include "netmsgcallbacks.h" +#include "netmsgmapentryexchangeresourcesmsg.h" +#include "netplayerinfo.h" #include "originalfunctions.h" +#include "racetype.h" #include "refreshinfo.h" #include "settings.h" +#include "timer.h" #include "unitstovalidate.h" #include "unitutils.h" #include "utils.h" +#include #include #include +#include +#include +#include namespace hooks { +extern const std::string_view eventsPerformanceLog{"eventsPerformance.log"}; + +long long conditionsTotalTime = 0; +long long conditionsSystemTime = 0; +long long effectsTime = 0; + +static bool __fastcall exchangeResourcesMsgHandler(game::CMidServerLogic* thisptr, + int /*%edx*/, + const CExchangeResourcesMsg* netMessage, + std::uint32_t idFrom) +{ + using namespace game; + + const NetPlayerInfo* playerInfo{CMidServerLogicApi::get().getPlayerInfo(thisptr, idFrom)}; + if (!playerInfo) { + return false; + } + + if (!CMidServerLogicApi::get().isCurrentPlayer(thisptr, &playerInfo->playerId)) { + return true; + } + + auto objectMap{thisptr->coreData->objectMap}; + if (!exchangeResources(objectMap, netMessage->siteId, netMessage->visitorStackId, + netMessage->playerCurrency, netMessage->siteCurrency, + netMessage->amount)) { + return false; + } + + thisptr->IMidMsgSender::vftable->sendObjectsChanges(thisptr); + return true; +} + void addValidatedUnitsToChangedObjects(game::CMidgardScenarioMap* scenarioMap) { using namespace game; @@ -146,4 +194,391 @@ bool __fastcall midServerLogicSendRefreshInfoHooked(const game::CMidServerLogic* return true; } +bool __fastcall applyEventEffectsAndCheckMidEventTriggerersHooked( + game::CMidServerLogic** thisptr, + int /*%edx*/, + game::List* effectsList, + const game::CMidgardID* triggererId, + const game::CMidgardID* playingStackId) +{ +#if 0 + const ScopedTimer timer{"Apply event effects and check triggerers", eventsPerformanceLog}; +#endif + + return getOriginalFunctions().applyEventEffectsAndCheckMidEventTriggerers(thisptr, effectsList, + triggererId, + playingStackId); +} + +bool __fastcall stackMoveHooked(game::CMidServerLogic** thisptr, + int /*%edx*/, + const game::CMidgardID* playerId, + game::List>* movementPath, + const game::CMidgardID* stackId, + const game::CMqPoint* startingPoint, + const game::CMqPoint* endPoint) +{ + using namespace game; + +#if 0 + char message[256]; + const auto result{fmt::format_to_n(message, sizeof(message) - 1u, + "Stack move from ({:d}, {:d}) to ({:d}, {:d})", + startingPoint->x, startingPoint->y, endPoint->x, + endPoint->y)}; + message[result.size] = 0; + + const ScopedTimer timer{std::string_view{message, result.size}, eventsPerformanceLog}; +#endif + + return getOriginalFunctions().stackMove(thisptr, playerId, movementPath, stackId, startingPoint, + endPoint); +} + +bool __stdcall filterAndProcessEventsNoPlayerHooked(game::IMidgardObjectMap* objectMap, + game::List* eventObjectList, + game::List* effectsList, + bool* stopProcessing, + game::IdList* executedEvents, + const game::CMidgardID* triggererStackId, + const game::CMidgardID* playingStackId) +{ + using namespace game; + +#if 0 + const ScopedTimer timer{"Filter and process events (no player)", eventsPerformanceLog}; +#endif + + return CMidServerLogicApi::get().filterAndProcessEvents(objectMap, eventObjectList, effectsList, + stopProcessing, executedEvents, + &emptyId, triggererStackId, + playingStackId); +} + +bool __stdcall filterAndProcessEventsHooked(game::IMidgardObjectMap* objectMap, + game::List* eventObjectList, + game::List* effectsList, + bool* stopProcessing, + game::IdList* executedEvents, + const game::CMidgardID* playerId, + const game::CMidgardID* triggererStackId, + const game::CMidgardID* playingStackId) +{ + using namespace game; + +#if 0 + const ScopedTimer timer{"Filter and process events", eventsPerformanceLog}; +#endif + + if (*playerId != emptyId && gameFunctions().ignorePlayerEvents(playerId, objectMap)) { + return false; + } + + CMidgardID triggererId{*triggererStackId}; + const CMidStack* triggererStack{}; + + if (triggererId != emptyId) { + triggererStack = getStack(objectMap, &triggererId); + if (!triggererStack && triggererId != emptyId) { + triggererId = emptyId; + } + } + + // Use unordered set for fast lookup of executed events + std::unordered_set executedEventsSet(executedEvents->length); + for (const CMidgardID& id : *executedEvents) { + executedEventsSet.insert(id); + } + + const auto& rtti = RttiApi::rtti(); + const auto dynamicCast = RttiApi::get().dynamicCast; + + // Cache players so we won't waste time searching them every time + std::unordered_map racePlayerMap; + + auto cachePlayer = [triggererStack, &racePlayerMap, &rtti, &dynamicCast](const auto* obj) { + auto* player = (const CMidPlayer*)dynamicCast(obj, 0, rtti.IMidScenarioObjectType, + rtti.CMidPlayerType, 0); + + if (!triggererStack || player->id == triggererStack->ownerId) { + const RaceId raceId{player->raceType->data->raceType.id}; + racePlayerMap[raceId] = player->id; + } + }; + + forEachScenarioObject(objectMap, IdType::Player, cachePlayer); + + const auto& raceSetFind{RaceSetApi::get().find}; + const auto& checkAndExecuteEvent{CMidServerLogicApi::get().checkAndExecuteEvent}; + + for (const CMidEvent* evt : *eventObjectList) { + if (!evt->enabled) { + // Event disabled, don't waste time checking anything + continue; + } + + if (executedEventsSet.find(evt->id) != executedEventsSet.end()) { + // Event already executed, skip + continue; + } + + const auto end{evt->racesCanTrigger.end()}; + + bool eventExecuted = false; + LRaceCategory raceCategory{}; + SetIterator it{}; + for (const auto& [raceId, playerCanTriggerId] : racePlayerMap) { + // Only category id is checked during search. Don't bother setting other fields + raceCategory.id = raceId; + raceSetFind(&evt->racesCanTrigger, &it, &raceCategory); + + if (it == end) { + // Race can't trigger + continue; + } + +#ifdef D2_MEASURE_EVENTS_TIME + conditionsTotalTime = 0; + conditionsSystemTime = 0; + effectsTime = 0; + + char message[256]; + const auto result{fmt::format_to_n(message, sizeof(message) - 1u, " Event '{:s}'", + evt->name.string ? evt->name.string : "?")}; + message[result.size] = 0; + + { + const ScopedTimer eventTimer{std::string_view{message, result.size}, + eventsPerformanceLog}; +#endif + + const bool samePlayer = *playerId == playerCanTriggerId; + // Player can trigger, check event conditions and execute effects + if (checkAndExecuteEvent(objectMap, effectsList, stopProcessing, &evt->id, + &playerCanTriggerId, &triggererId, playingStackId, + samePlayer)) { + eventExecuted = true; + } + +#ifdef D2_MEASURE_EVENTS_TIME + } + + const auto overhead{conditionsSystemTime - conditionsTotalTime}; + logDebug("eventsPerformance.log", + fmt::format("{:s} conditions time {:d} us, " + "conditions system time {:d} us (overhead {:d} us), " + "effects time {:d} us", + message, conditionsTotalTime, conditionsSystemTime, overhead, + effectsTime)); +#endif + } + + if (eventExecuted) { + IdListApi::get().pushBack(executedEvents, &evt->id); + return true; + } + } + + return false; +} + +bool __stdcall checkEventConditionsHooked(const game::IMidgardObjectMap* objectMap, + game::List* effectsList, + const game::CMidgardID* playerId, + const game::CMidgardID* stackTriggererId, + int samePlayer, + const game::CMidgardID* eventId) +{ + using namespace game; + +#ifdef D2_MEASURE_EVENTS_TIME + const ScopedValueTimer timer{conditionsSystemTime}; +#endif + + return getOriginalFunctions().checkEventConditions(objectMap, effectsList, playerId, + stackTriggererId, samePlayer, eventId); +} + +void __stdcall executeEventEffectsHooked(game::IMidgardObjectMap* objectMap, + game::List* effectsList, + bool* stopProcessing, + const game::CMidgardID* eventId, + const game::CMidgardID* playerId, + const game::CMidgardID* stackTriggererId, + const game::CMidgardID* playingStackId) +{ + using namespace game; + +#ifdef D2_MEASURE_EVENTS_TIME + const ScopedValueTimer timer{effectsTime}; +#endif + + getOriginalFunctions().executeEventEffects(objectMap, effectsList, stopProcessing, eventId, + playerId, stackTriggererId, playingStackId); +} + +static bool doTestHooked(game::ITestConditionVftable::Test testFunc, + const char* name, + const game::ITestCondition* thisptr, + const game::IMidgardObjectMap* objectMap, + const game::CMidgardID* playerId, + const game::CMidgardID* eventId) +{ + using namespace game; + +#ifdef D2_MEASURE_EVENTS_TIME + char message[256]; + const auto result{ + fmt::format_to_n(message, sizeof(message) - 1u, " Test condition '{:s}'", name)}; + message[result.size] = 0; + + const ScopedTimer conditionTimer{std::string_view{message, result.size}, eventsPerformanceLog}; + const ScopedValueTimer timer{conditionsTotalTime}; +#endif + + return testFunc(thisptr, objectMap, playerId, eventId); +} + +bool __fastcall testFreqHooked(const game::ITestCondition* thisptr, + int /*%edx*/, + const game::IMidgardObjectMap* objectMap, + const game::CMidgardID* playerId, + const game::CMidgardID* eventId) +{ + return doTestHooked(getOriginalFunctions().testFrequency, "frequency", thisptr, objectMap, + playerId, eventId); +} + +bool __fastcall testLocationHooked(const game::ITestCondition* thisptr, + int /*%edx*/, + const game::IMidgardObjectMap* objectMap, + const game::CMidgardID* playerId, + const game::CMidgardID* eventId) +{ + return doTestHooked(getOriginalFunctions().testLocation, "location", thisptr, objectMap, + playerId, eventId); +} + +bool __fastcall testEnterCityHooked(const game::ITestCondition* thisptr, + int /*%edx*/, + const game::IMidgardObjectMap* objectMap, + const game::CMidgardID* playerId, + const game::CMidgardID* eventId) +{ + return doTestHooked(getOriginalFunctions().testEnterCity, "enter city", thisptr, objectMap, + playerId, eventId); +} + +bool __fastcall testLeaderToCityHooked(const game::ITestCondition* thisptr, + int /*%edx*/, + const game::IMidgardObjectMap* objectMap, + const game::CMidgardID* playerId, + const game::CMidgardID* eventId) +{ + return doTestHooked(getOriginalFunctions().testLeaderToCity, "leader to city", thisptr, + objectMap, playerId, eventId); +} + +bool __fastcall testOwnCityHooked(const game::ITestCondition* thisptr, + int /*%edx*/, + const game::IMidgardObjectMap* objectMap, + const game::CMidgardID* playerId, + const game::CMidgardID* eventId) +{ + return doTestHooked(getOriginalFunctions().testOwnCity, "own city", thisptr, objectMap, + playerId, eventId); +} + +bool __fastcall testDiplomacyHooked(const game::ITestCondition* thisptr, + int /*%edx*/, + const game::IMidgardObjectMap* objectMap, + const game::CMidgardID* playerId, + const game::CMidgardID* eventId) +{ + return doTestHooked(getOriginalFunctions().testDiplomacy, "diplomacy", thisptr, objectMap, + playerId, eventId); +} + +bool __fastcall testAllianceHooked(const game::ITestCondition* thisptr, + int /*%edx*/, + const game::IMidgardObjectMap* objectMap, + const game::CMidgardID* playerId, + const game::CMidgardID* eventId) +{ + return doTestHooked(getOriginalFunctions().testAlliance, "alliance", thisptr, objectMap, + playerId, eventId); +} + +bool __fastcall testLootRuinHooked(const game::ITestCondition* thisptr, + int /*%edx*/, + const game::IMidgardObjectMap* objectMap, + const game::CMidgardID* playerId, + const game::CMidgardID* eventId) +{ + return doTestHooked(getOriginalFunctions().testLootRuin, "loot ruin", thisptr, objectMap, + playerId, eventId); +} + +bool __fastcall testTransformLandHooked(const game::ITestCondition* thisptr, + int /*%edx*/, + const game::IMidgardObjectMap* objectMap, + const game::CMidgardID* playerId, + const game::CMidgardID* eventId) +{ + return doTestHooked(getOriginalFunctions().testTransformLand, "transform land", thisptr, + objectMap, playerId, eventId); +} + +bool __fastcall testVisitSiteHooked(const game::ITestCondition* thisptr, + int /*%edx*/, + const game::IMidgardObjectMap* objectMap, + const game::CMidgardID* playerId, + const game::CMidgardID* eventId) +{ + return doTestHooked(getOriginalFunctions().testVisitSite, "visit site", thisptr, objectMap, + playerId, eventId); +} + +bool __fastcall testItemToLocationHooked(const game::ITestCondition* thisptr, + int /*%edx*/, + const game::IMidgardObjectMap* objectMap, + const game::CMidgardID* playerId, + const game::CMidgardID* eventId) +{ + return doTestHooked(getOriginalFunctions().testItemToLocation, "item to location", thisptr, + objectMap, playerId, eventId); +} + +bool __fastcall testVarInRangeHooked(const game::ITestCondition* thisptr, + int /*%edx*/, + const game::IMidgardObjectMap* objectMap, + const game::CMidgardID* playerId, + const game::CMidgardID* eventId) +{ + return doTestHooked(getOriginalFunctions().testVarInRange, "var in range", thisptr, objectMap, + playerId, eventId); +} + +game::CMidServerLogic* __fastcall midServerLogicCtorHooked(game::CMidServerLogic* thisptr, + int /*%edx*/, + game::CMidServer* server, + bool multiplayerGame, + bool hotseatGame, + int a5, + int gameVersion) +{ + using namespace game; + + getOriginalFunctions().midServerLogicCtor(thisptr, server, multiplayerGame, hotseatGame, a5, + gameVersion); + + auto netMsgEntryData{thisptr->coreData->netMsgEntryData}; + + auto callback = (CNetMsgMapEntry_member::Callback)exchangeResourcesMsgHandler; + auto entry{createNetMsgMapEntryExchangeResourcesMsg(thisptr, callback)}; + + NetMsgApi::get().addEntry(netMsgEntryData, (CNetMsgMapEntry*)entry); + return thisptr; +} + } // namespace hooks diff --git a/mss32/src/midsite.cpp b/mss32/src/midsite.cpp new file mode 100644 index 00000000..efa4404a --- /dev/null +++ b/mss32/src/midsite.cpp @@ -0,0 +1,56 @@ +/* + * This file is part of the modding toolset for Disciples 2. + * (https://github.com/VladimirMakeev/D2ModdingToolset) + * Copyright (C) 2024 Vladimir Makeev. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "midsite.h" +#include "version.h" +#include + +namespace game::CMidSiteApi { + +// clang-format off +static std::array functions = {{ + // Akella + Api{ + (Api::Constructor)0x601f13, + (Api::SetData)nullptr, + }, + // Russobit + Api{ + (Api::Constructor)0x601f13, + (Api::SetData)nullptr, + }, + // Gog + Api{ + (Api::Constructor)0x600bc6, + (Api::SetData)nullptr, + }, + // Scenario Editor + Api{ + (Api::Constructor)0x4f3203, + (Api::SetData)0x4f33a0, + } +}}; +// clang-format on + +Api& get() +{ + return functions[static_cast(hooks::gameVersion())]; +} + +} // namespace game::CMidSiteApi diff --git a/mss32/src/midsitemage.cpp b/mss32/src/midsitemage.cpp new file mode 100644 index 00000000..688e7b71 --- /dev/null +++ b/mss32/src/midsitemage.cpp @@ -0,0 +1,52 @@ +/* + * This file is part of the modding toolset for Disciples 2. + * (https://github.com/VladimirMakeev/D2ModdingToolset) + * Copyright (C) 2024 Vladimir Makeev. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "midsitemage.h" +#include "version.h" +#include + +namespace game::CMidSiteMageApi { + +// clang-format off +static std::array functions = {{ + // Akella + Api{ + (Api::Constructor)0x6046a1, + }, + // Russobit + Api{ + (Api::Constructor)0x6046a1, + }, + // Gog + Api{ + (Api::Constructor)0x6031d1, + }, + // Scenario Editor + Api{ + (Api::Constructor)0x4fcd1c, + } +}}; +// clang-format on + +Api& get() +{ + return functions[static_cast(hooks::gameVersion())]; +} + +} // namespace game::CMidSiteMageApi diff --git a/mss32/src/midsitemerchant.cpp b/mss32/src/midsitemerchant.cpp new file mode 100644 index 00000000..75fee46a --- /dev/null +++ b/mss32/src/midsitemerchant.cpp @@ -0,0 +1,52 @@ +/* + * This file is part of the modding toolset for Disciples 2. + * (https://github.com/VladimirMakeev/D2ModdingToolset) + * Copyright (C) 2024 Vladimir Makeev. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "midsitemerchant.h" +#include "version.h" +#include + +namespace game::CMidSiteMerchantApi { + +// clang-format off +static std::array functions = {{ + // Akella + Api{ + (Api::Constructor)0x602538, + }, + // Russobit + Api{ + (Api::Constructor)0x602538, + }, + // Gog + Api{ + (Api::Constructor)0x6011eb, + }, + // Scenario Editor + Api{ + (Api::Constructor)0x4fa303, + } +}}; +// clang-format on + +Api& get() +{ + return functions[static_cast(hooks::gameVersion())]; +} + +} // namespace game::CMidSiteMerchantApi diff --git a/mss32/src/midsitemercs.cpp b/mss32/src/midsitemercs.cpp new file mode 100644 index 00000000..b56974be --- /dev/null +++ b/mss32/src/midsitemercs.cpp @@ -0,0 +1,52 @@ +/* + * This file is part of the modding toolset for Disciples 2. + * (https://github.com/VladimirMakeev/D2ModdingToolset) + * Copyright (C) 2024 Vladimir Makeev. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "midsitemercs.h" +#include "version.h" +#include + +namespace game::CMidSiteMercsApi { + +// clang-format off +static std::array functions = {{ + // Akella + Api{ + (Api::Constructor)0x603e9d, + }, + // Russobit + Api{ + (Api::Constructor)0x603e9d, + }, + // Gog + Api{ + (Api::Constructor)0x602aca, + }, + // Scenario Editor + Api{ + (Api::Constructor)0x4fd598, + } +}}; +// clang-format on + +Api& get() +{ + return functions[static_cast(hooks::gameVersion())]; +} + +} // namespace game::CMidSiteMercsApi diff --git a/mss32/src/midsiteresourcemarket.cpp b/mss32/src/midsiteresourcemarket.cpp new file mode 100644 index 00000000..0612d06b --- /dev/null +++ b/mss32/src/midsiteresourcemarket.cpp @@ -0,0 +1,584 @@ +/* + * This file is part of the modding toolset for Disciples 2. + * (https://github.com/VladimirMakeev/D2ModdingToolset) + * Copyright (C) 2024 Vladimir Makeev. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "midsiteresourcemarket.h" +#include "bindings/resourcemarketview.h" +#include "bindings/stackview.h" +#include "campaignstream.h" +#include "dynamiccast.h" +#include "gameutils.h" +#include "log.h" +#include "mempool.h" +#include "midgardobjectmap.h" +#include "midgardstream.h" +#include "midplayer.h" +#include "midstack.h" +#include "scenarioobjectstreams.h" +#include "scripts.h" +#include "sitecategoryhooks.h" +#include "streamregister.h" +#include "utils.h" +#include +#include +#include + +namespace hooks { + +// Rtti info below is put into functions so it will be initialized +// the first time CMidSiteResourceMarket object is created. +// Delayed initialization of rtti structures is needed +// because of typeInfoVftable() implementation which expects gameVersion() to work correctly. +// gameVersion() can be called only after determineGameVersion() is called from DllMain. + +static game::ClassHierarchyDescriptor* getResourceMarketHierarchyDescriptor() +{ + // clang-format off + static game::BaseClassDescriptor baseClassDescriptor{ + getResourceMarketTypeDescriptor(), + 5u, // base class array has 5 more elements after this descriptor + game::PMD{ 0, -1, 0 }, + 0u + }; + + static game::BaseClassArray baseClassArray{ + &baseClassDescriptor, + game::RttiApi::rtti().CMidSiteDescriptor, + game::RttiApi::rtti().IMidScenarioObjectDescriptor, + game::RttiApi::rtti().IMidObjectDescriptor, + game::RttiApi::rtti().IMapElementDescriptor, + game::RttiApi::rtti().IAiPriorityDescriptor, + }; + + static game::ClassHierarchyDescriptor hierarchyDescriptor{ + 0u, + 1u, + 6u, // base class array has 6 elements + &baseClassArray + }; + // clang-format on + + return &hierarchyDescriptor; +} + +static game::RttiInfo& getMarketSiteRttiInfo() +{ + // clang-format off + // CompleteObjectLocator for IMidScenarioObject base class + static const game::CompleteObjectLocator objectLocator{ + 0u, + offsetof(CMidSiteResourceMarket, vftable), + 0u, + getResourceMarketTypeDescriptor(), + getResourceMarketHierarchyDescriptor() + }; + + // CompleteObjectLocator and vftable for IMidScenarioObject base class + static game::RttiInfo rttiInfo{ + &objectLocator + }; + // clang-format on + + return rttiInfo; +} + +static game::RttiInfo& getMarketMapElementRttiInfo() +{ + // clang-format off + // CompleteObjectLocator for IMapElement base class + static const game::CompleteObjectLocator objectLocator{ + 0u, + offsetof(CMidSiteResourceMarket, CMidSiteResourceMarket::mapElement), + 0u, + getResourceMarketTypeDescriptor(), + getResourceMarketHierarchyDescriptor() + }; + + // CompleteObjectLocator and vftable for IMapElement base class + static game::RttiInfo rttiInfo{ + &objectLocator + }; + // clang-format on + + return rttiInfo; +} + +static game::RttiInfo& getMarketAiPriorityRttiInfo() +{ + // clang-format off + // CompleteObjectLocator for IAiPriority base class + static const game::CompleteObjectLocator objectLocator{ + 0u, + offsetof(CMidSiteResourceMarket, CMidSiteResourceMarket::aiPriority), + 0u, + getResourceMarketTypeDescriptor(), + getResourceMarketHierarchyDescriptor() + }; + + // CompleteObjectLocator and vftable for IAiPriority base class + static game::RttiInfo rttiInfo{ + &objectLocator + }; + // clang-format on + + return rttiInfo; +} + +static game::IMidScenarioObjectVftable::Destructor midSiteDtor = nullptr; + +static void __fastcall marketSiteDtor(CMidSiteResourceMarket* thisptr, int /*%edx*/, char flags) +{ + // Destroy market specific data + thisptr->exchangeRatesScript.~basic_string(); + + if (midSiteDtor) { + // Allow CMidSite d-tor clear itself and free our memory + midSiteDtor((game::IMidObject*)thisptr, flags); + } +} + +static void __fastcall marketStream(CMidSiteResourceMarket* thisptr, + int /* %edx */, + game::CampaignStream* campaignStream, + const game::CMidgardID* siteId) +{ + using namespace game; + + IMidgardStream* stream{campaignStream->stream}; + IMidgardStreamVftable* vftable{stream->vftable}; + + vftable->streamBool(stream, "CUSTOM", &thisptr->customExchangeRates); + if (thisptr->customExchangeRates) { + int length{static_cast(thisptr->exchangeRatesScript.length())}; + vftable->streamInt(stream, "CODE_LEN", &length); + + if (vftable->readMode(stream)) { + thisptr->exchangeRatesScript.resize(length); + } + + vftable->streamString(stream, "CODE", thisptr->exchangeRatesScript.data()); + } + + vftable->streamCurrency(stream, "BANK", &thisptr->stock); + vftable->streamByte(stream, "INF", &thisptr->infiniteStock.value); +} + +static void __fastcall marketMapElementDtor(game::IMapElement* thisptr, int /*%edx*/, char flags) +{ + constexpr auto offset = offsetof(CMidSiteResourceMarket, CMidSiteResourceMarket::mapElement); + + const auto ptr = reinterpret_cast(thisptr) - offset; + CMidSiteResourceMarket* site = reinterpret_cast(ptr); + marketSiteDtor(site, 0, flags); +} + +static void __fastcall marketAiPriorityDtor(game::IAiPriority* thisptr, int /*%edx*/, char flags) +{ + constexpr auto offset = offsetof(CMidSiteResourceMarket, CMidSiteResourceMarket::aiPriority); + + const auto ptr = reinterpret_cast(thisptr) - offset; + CMidSiteResourceMarket* site = reinterpret_cast(ptr); + marketSiteDtor(site, 0, flags); +} + +/** Get vftables from CMidSite and update methods specific to resource market */ +static void setupResourceMarketRttiInfo(const CMidSiteResourceMarket* market) +{ + using namespace game; + + { + // For IMidScenarioObject + auto& rttiInfo = getMarketSiteRttiInfo(); + + std::memcpy(&rttiInfo.vftable, market->vftable, sizeof(CMidSiteVftable)); + // Remember base class destructor to destroy CMidSite data properly + midSiteDtor = market->vftable->destructor; + // Use our own d-tor + rttiInfo.vftable.destructor = (IMidScenarioObjectVftable::Destructor)marketSiteDtor; + // Make sure we use market-specific site data stream method + rttiInfo.vftable.streamSiteData = (CMidSiteVftable::StreamSiteData)marketStream; + } + + { + // For IMapElement + auto& rttiInfo = getMarketMapElementRttiInfo(); + + std::memcpy(&rttiInfo.vftable, market->mapElement.vftable, sizeof(IMapElementVftable)); + rttiInfo.vftable.destructor = (IMapElementVftable::Destructor)marketMapElementDtor; + } + + { + // For IAiPriority + auto& rttiInfo = getMarketAiPriorityRttiInfo(); + + std::memcpy(&rttiInfo.vftable, market->aiPriority.vftable, sizeof(IAiPriorityVftable)); + rttiInfo.vftable.destructor = (IAiPriorityVftable::Destructor)marketAiPriorityDtor; + } +} + +static void readExchangeRates(const sol::table& table, MarketExchangeRates& exchangeRates) +{ + exchangeRates.clear(); + // if table size is > 6 we 100% have duplicates + exchangeRates.reserve(table.size()); + + bool resourceExchangeExists[6]{}; + + for (std::size_t i = 0u; i < table.size(); ++i) { + const sol::table& exchangeTable = table[i + 1]; + + ResourceExchange exchange; + exchange.resource1 = exchangeTable[1]; + + if (resourceExchangeExists[(int)exchange.resource1]) { + throw std::runtime_error("Duplicate resource exchange found!"); + } + + resourceExchangeExists[(int)exchange.resource1] = true; + + const sol::table& ratesArray = exchangeTable[2]; + // if rates array size is > 6 we 100% have duplicates + exchange.rates.reserve(ratesArray.size()); + + bool exchangeRateExists[6]{}; + + for (std::size_t j = 0u; j < ratesArray.size(); ++j) { + const sol::table& ratesTable = ratesArray[j + 1]; + + ExchangeRates rates; + rates.resource2 = ratesTable[1]; + + if (exchangeRateExists[(int)rates.resource2]) { + throw std::runtime_error("Duplicate exchange rate found!"); + } + + exchangeRateExists[(int)rates.resource2] = true; + + rates.amount1 = ratesTable[2]; + rates.amount2 = ratesTable[3]; + + exchange.rates.push_back(rates); + } + + exchangeRates.push_back(exchange); + } +} + +game::TypeDescriptor* getResourceMarketTypeDescriptor() +{ + // clang-format off + static game::TypeDescriptor descriptor{ + game::RttiApi::typeInfoVftable(), + nullptr, + ".?AVCMidSiteResourceMarket@@" + }; + // clang-format on + + return &descriptor; +} + +CMidSiteResourceMarket* createResourceMarket(const game::CMidgardID* siteId) +{ + if (!customSiteCategories().exists) { + return nullptr; + } + + using namespace game; + + auto market = (CMidSiteResourceMarket*)Memory::get().allocate(sizeof(CMidSiteResourceMarket)); + CMidSiteApi::get().constructor(market, siteId, &customSiteCategories().resourceMarket); + + static bool firstTime = true; + if (firstTime) { + firstTime = false; + setupResourceMarketRttiInfo(market); + } + + new (&market->exchangeRatesScript) std::string(); + market->customExchangeRates = false; + + BankApi::get().setZero(&market->stock); + market->infiniteStock.value = 0u; + + // Use our own rtti info and vftables + market->vftable = &getMarketSiteRttiInfo().vftable; + market->mapElement.vftable = &getMarketMapElementRttiInfo().vftable; + market->aiPriority.vftable = &getMarketAiPriorityRttiInfo().vftable; + return market; +} + +struct CResourceMarketStreamRegister : public game::CStreamRegisterBase +{ }; + +void __fastcall resMarketStreamRegisterDtor(CResourceMarketStreamRegister* thisptr, + int /*%edx*/, + char flags) +{ + if (flags & 1) { + game::Memory::get().freeNonZero(thisptr); + } +} + +static game::IMidScenarioObject* __fastcall resMargetStreamRegisterStream( + CResourceMarketStreamRegister* thisptr, + int /*%edx*/, + const game::CMidgardID* objectId) +{ + return createResourceMarket(objectId); +} + +// clang-format off +static game::CStreamRegisterBaseVftable resMarketStreamRegisterVftable{ + (game::CStreamRegisterBaseVftable::Destructor)resMarketStreamRegisterDtor, + (game::CStreamRegisterBaseVftable::Stream)resMargetStreamRegisterStream + +}; +// clang-format on + +void addResourceMarketStreamRegister() +{ + using namespace game; + + static bool firstTime = true; + if (!firstTime) { + return; + } + + firstTime = false; + + static CResourceMarketStreamRegister streamRegister; + // Make sure we use our own vftable + streamRegister.vftable = &resMarketStreamRegisterVftable; + + const auto& typeInfoRawName = *RttiApi::get().typeInfoRawName; + const char* rawName = typeInfoRawName(getResourceMarketTypeDescriptor()); + + // Associate stream register with a new site + ScenarioObjectStreamsApi::get().addStreamRegister(&streamRegister, rawName); +} + +bool isMarketStockInfinite(const InfiniteStock& stock, game::CurrencyType currency) +{ + using namespace game; + + switch (currency) { + case CurrencyType::Gold: + return stock.parts.gold; + case CurrencyType::InfernalMana: + return stock.parts.infernalMana; + case CurrencyType::LifeMana: + return stock.parts.lifeMana; + case CurrencyType::DeathMana: + return stock.parts.deathMana; + case CurrencyType::RunicMana: + return stock.parts.runicMana; + case CurrencyType::GroveMana: + return stock.parts.groveMana; + } + + return false; +} + +bool getExchangeRates(const game::IMidgardObjectMap* objectMap, + const game::CMidgardID& marketId, + const game::CMidgardID& visitorStackId, + MarketExchangeRates& exchangeRates, + bool serverSide) +{ + using namespace game; + + auto obj{objectMap->vftable->findScenarioObjectById(objectMap, &marketId)}; + auto market{(const CMidSiteResourceMarket*)obj}; + if (!market) { + return false; + } + + static const char functionName[]{"getExchangeRates"}; + const bool bindScenario{true}; + const bool alwaysExist{true}; + + std::optional env; + std::optional getExchangeRates; + if (market->customExchangeRates) { + sol::protected_function_result result; + env = executeScript(market->exchangeRatesScript, result, bindScenario); + if (!result.valid()) { + const CMqPoint& pos{market->mapElement.position}; + const sol::error err = result; + logError("mssProxyError.log", fmt::format("Failed to load custom exchange rates" + " for resource market {:s} at ({:d}, {:d}).\n" + "Script:\n'{:s}'\n" + "Reason: {:s}", + idToString(&market->id), pos.x, pos.y, + market->exchangeRatesScript, err.what())); + return false; + } + + getExchangeRates = getProtectedScriptFunction(env.value(), functionName, alwaysExist); + } else { + // Resource market uses default exchange rates + const auto& path{scriptsFolder() / customSiteCategories().exchangeRatesScript}; + getExchangeRates = getScriptFunction(path, functionName, env, alwaysExist, bindScenario); + } + + try { + const CMidStack* visitorStack{getStack(objectMap, &visitorStackId)}; + if (!visitorStack) { + return false; + } + + const bindings::StackView visitor{visitorStack, objectMap}; + const bindings::ResourceMarketView marketView{market, objectMap}; + const sol::table table = (*getExchangeRates)(visitor, marketView, serverSide); + + readExchangeRates(table, exchangeRates); + } catch (const std::exception& e) { + const CMqPoint& pos{market->mapElement.position}; + logError("mssProxyError.log", fmt::format("Failed to get exchange rates" + " for resource market {:s} at ({:d}, {:d}).\n" + "Reason: '{:s}'", + idToString(&market->id), pos.x, pos.y, e.what())); + return false; + } + + return true; +} + +const ExchangeRates* findExchangeRates(const MarketExchangeRates& marketRates, + game::CurrencyType playerCurrency, + game::CurrencyType marketCurrency) +{ + auto playerResourceMatch = [playerCurrency](const ResourceExchange& exchange) { + return exchange.resource1 == playerCurrency; + }; + + const auto ratesIt{std::find_if(marketRates.cbegin(), marketRates.cend(), playerResourceMatch)}; + if (ratesIt == marketRates.cend()) { + return nullptr; + } + + auto marketResourceMatch = [marketCurrency](const ExchangeRates& exchRates) { + return exchRates.resource2 == marketCurrency; + }; + + const ResourceExchange& exchange{*ratesIt}; + const auto exchangeIt{ + std::find_if(exchange.rates.cbegin(), exchange.rates.cend(), marketResourceMatch)}; + if (exchangeIt == exchange.rates.cend()) { + // Could not exchange, bug ? + return nullptr; + } + + return &(*exchangeIt); +} + +bool exchangeResources(game::IMidgardObjectMap* objectMap, + const game::CMidgardID& marketId, + const game::CMidgardID& visitorStackId, + game::CurrencyType playerCurrency, + game::CurrencyType marketCurrency, + std::uint16_t amount) +{ + using namespace game; + + MarketExchangeRates rates; + if (!getExchangeRates(objectMap, marketId, visitorStackId, rates, true)) { + // Failed to get exchange rates from script. Bug. + return false; + } + + auto foundRate{findExchangeRates(rates, playerCurrency, marketCurrency)}; + if (!foundRate) { + return false; + } + + const ExchangeRates& rate{*foundRate}; + + const std::uint16_t playerExchangeAmount = amount * rate.amount1; + + const auto* stack{getStack(objectMap, &visitorStackId)}; + const auto* player{getPlayer(objectMap, &stack->ownerId)}; + + const auto& bankApi{BankApi::get()}; + const auto playerResource{bankApi.get(&player->bank, playerCurrency)}; + const bool playerCanExchange{playerResource >= playerExchangeAmount}; + + if (!playerCanExchange) { + // Not enough resources. Why we are here? + // Was it a net message from cheater who tries to exchange? + return false; + } + + // Check if market has enough resources, or they are infinite + + auto site{objectMap->vftable->findScenarioObjectById(objectMap, &marketId)}; + auto market{static_cast(site)}; + + const std::uint16_t marketExchangeAmount = amount * rate.amount2; + + const bool infiniteResource{isMarketStockInfinite(market->infiniteStock, marketCurrency)}; + + bool marketCanExchange{}; + if (infiniteResource) { + marketCanExchange = true; + } else { + // Check market stock amount + const auto marketResource{bankApi.get(&market->stock, marketCurrency)}; + + marketCanExchange = marketResource >= marketExchangeAmount; + } + + if (!marketCanExchange) { + // Same question as above: why we are here if market can't exchange resources? + // Why net message was sent at all? + return false; + } + + auto playerToChange{getPlayerToChange(objectMap, &stack->ownerId)}; + // Player sells its resource + bankApi.set(&playerToChange->bank, playerCurrency, playerResource - playerExchangeAmount); + // And gets resource from market + const auto current{bankApi.get(&playerToChange->bank, marketCurrency)}; + bankApi.set(&playerToChange->bank, marketCurrency, current + marketExchangeAmount); + + const bool infiniteResource2{isMarketStockInfinite(market->infiniteStock, playerCurrency)}; + + // Change resource market data only when necessary + if (!infiniteResource || !infiniteResource2) { + auto obj{objectMap->vftable->findScenarioObjectByIdForChange(objectMap, &marketId)}; + auto marketToChange{static_cast(obj)}; + + if (!infiniteResource) { + // Market sells its resource + const auto marketResource{bankApi.get(&market->stock, marketCurrency)}; + bankApi.set(&marketToChange->stock, marketCurrency, + marketResource - marketExchangeAmount); + } + + if (!infiniteResource2) { + // And gets resource from player + const auto marketResource{bankApi.get(&market->stock, playerCurrency)}; + bankApi.set(&marketToChange->stock, playerCurrency, + marketResource + playerExchangeAmount); + } + } + + return true; +} + +} // namespace hooks diff --git a/mss32/src/midsitetrainer.cpp b/mss32/src/midsitetrainer.cpp new file mode 100644 index 00000000..b7f4059e --- /dev/null +++ b/mss32/src/midsitetrainer.cpp @@ -0,0 +1,52 @@ +/* + * This file is part of the modding toolset for Disciples 2. + * (https://github.com/VladimirMakeev/D2ModdingToolset) + * Copyright (C) 2024 Vladimir Makeev. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "midsitetrainer.h" +#include "version.h" +#include + +namespace game::CMidSiteTrainerApi { + +// clang-format off +static std::array functions = {{ + // Akella + Api{ + (Api::Constructor)0x619a18, + }, + // Russobit + Api{ + (Api::Constructor)0x619a18, + }, + // Gog + Api{ + (Api::Constructor)0x618536, + }, + // Scenario Editor + Api{ + (Api::Constructor)0x503565, + } +}}; +// clang-format on + +Api& get() +{ + return functions[static_cast(hooks::gameVersion())]; +} + +} // namespace game::CMidSiteTrainerApi diff --git a/mss32/src/midstack.cpp b/mss32/src/midstack.cpp index 2b2bd92b..8ac5be3d 100644 --- a/mss32/src/midstack.cpp +++ b/mss32/src/midstack.cpp @@ -47,15 +47,15 @@ static std::array functions = {{ }, }}; -static std::array vftables = {{ +static std::array vftables = {{ // Akella - (const IMapElementVftable*)0x6f0a94, + (const CMidStackIMapElementVftable*)0x6f0a94, // Russobit - (const IMapElementVftable*)0x6f0a94, + (const CMidStackIMapElementVftable*)0x6f0a94, // Gog - (const IMapElementVftable*)0x6eea34, + (const CMidStackIMapElementVftable*)0x6eea34, // Scenario Editor - (const IMapElementVftable*)0x5da744, + (const CMidStackIMapElementVftable*)0x5da744, }}; // clang-format on @@ -64,7 +64,7 @@ Api& get() return functions[static_cast(hooks::gameVersion())]; } -const IMapElementVftable* vftable() +const CMidStackIMapElementVftable* vftable() { return vftables[static_cast(hooks::gameVersion())]; } diff --git a/mss32/src/midtaskopeninterfparamresmarket.cpp b/mss32/src/midtaskopeninterfparamresmarket.cpp new file mode 100644 index 00000000..14b3f40d --- /dev/null +++ b/mss32/src/midtaskopeninterfparamresmarket.cpp @@ -0,0 +1,88 @@ +/* + * This file is part of the modding toolset for Disciples 2. + * (https://github.com/VladimirMakeev/D2ModdingToolset) + * Copyright (C) 2024 Vladimir Makeev. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "midtaskopeninterfparamresmarket.h" +#include "interface.h" +#include "interfmanager.h" +#include "mempool.h" +#include "midtaskopeninterfparam.h" +#include "siteresourcemarketinterf.h" +#include + +namespace hooks { + +/** Task that opens resource market interface. */ +using CMidTaskOpenInterfResMarket = game::CMidTaskOpenInterfParam; + +static game::ITaskVftable taskResMarketVftable; +static game::ITaskVftable::Destructor midFreeTaskDtor = nullptr; + +static void __fastcall taskResMarketDtor(CMidTaskOpenInterfResMarket* thisptr, + int /*%edx*/, + char flags) +{ + using namespace game; + + if (thisptr->interf) { + // Hide and delete interface, as other site related tasks do + auto interfManager{thisptr->interfManager.data}; + interfManager->CInterfManager::vftable->hideInterface(interfManager, thisptr->interf); + + thisptr->interf->vftable->destructor(thisptr->interf, 1); + thisptr->interf = nullptr; + } + + // Call base class d-tor + if (midFreeTaskDtor) { + midFreeTaskDtor(thisptr, flags); + } +} + +game::ITask* createMidTaskOpenInterfParamResMarket(game::CTaskManager* taskManager, + game::CPhaseGame* phaseGame, + const game::CMidgardID& visitorStackId, + const game::CMidgardID& siteId) +{ + using namespace game; + + const auto& allocateMemory{Memory::get().allocate}; + + auto task{(CMidTaskOpenInterfResMarket*)allocateMemory(sizeof(CMidTaskOpenInterfResMarket))}; + CMidFreeTaskApi::get().constructor(task, taskManager); + + static bool firstTime{true}; + if (firstTime) { + firstTime = false; + + midFreeTaskDtor = task->vftable->destructor; + std::memcpy(&taskResMarketVftable, task->vftable, sizeof(ITaskVftable)); + + taskResMarketVftable.destructor = (ITaskVftable::Destructor)taskResMarketDtor; + } + + // Use our own vftable + task->vftable = &taskResMarketVftable; + task->interf = createSiteResourceMarketInterf(task, phaseGame, visitorStackId, siteId); + + auto interfManager{task->interfManager.data}; + interfManager->CInterfManager::vftable->showInterface(interfManager, task->interf); + return task; +} + +} // namespace hooks diff --git a/mss32/src/midunitgroupadapter.cpp b/mss32/src/midunitgroupadapter.cpp new file mode 100644 index 00000000..7964a3d7 --- /dev/null +++ b/mss32/src/midunitgroupadapter.cpp @@ -0,0 +1,52 @@ +/* + * This file is part of the modding toolset for Disciples 2. + * (https://github.com/VladimirMakeev/D2ModdingToolset) + * Copyright (C) 2024 Vladimir Makeev. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "midunitgroupadapter.h" +#include "version.h" +#include + +namespace game::CMidUnitGroupAdapterApi { + +// clang-format off +static std::array functions = {{ + // Akella + Api{ + (Api::Constructor)0x56ffd5, + }, + // Russobit + Api{ + (Api::Constructor)0x56ffd5, + }, + // Gog + Api{ + (Api::Constructor)0x56f577, + }, + // Scenario Editor + Api{ + (Api::Constructor)0, + } +}}; +// clang-format on + +Api& get() +{ + return functions[static_cast(hooks::gameVersion())]; +} + +} // namespace game::CMidUnitGroupAdapterApi diff --git a/mss32/src/nativegameinfo.cpp b/mss32/src/nativegameinfo.cpp index 8fd6788e..84d6060e 100644 --- a/mss32/src/nativegameinfo.cpp +++ b/mss32/src/nativegameinfo.cpp @@ -36,6 +36,7 @@ #include "nativespellinfo.h" #include "nativeunitinfo.h" #include "racetype.h" +#include "sitecategoryhooks.h" #include "strategicspell.h" #include "ussoldierimpl.h" #include "usstackleader.h" @@ -330,6 +331,11 @@ const rsg::SiteTexts& NativeGameInfo::getTrainerTexts() const return trainerTexts; } +const rsg::SiteTexts& NativeGameInfo::getMarketTexts() const +{ + return marketTexts; +} + bool NativeGameInfo::readGameInfo(const std::filesystem::path& gameFolderPath) { // Some parts of the data nedded by scenario generator is not loaded by game @@ -740,11 +746,17 @@ bool NativeGameInfo::readCityNames(const std::filesystem::path& scenDataFolderPa bool NativeGameInfo::readSiteTexts(const std::filesystem::path& scenDataFolderPath) { + const bool resourceMarketExists{customSiteCategories().exists}; + return readSiteText(mercenaryTexts, scenDataFolderPath / "Campname.dbf") && readSiteText(mageTexts, scenDataFolderPath / "Magename.dbf") && readSiteText(merchantTexts, scenDataFolderPath / "Mercname.dbf") && readSiteText(ruinTexts, scenDataFolderPath / "Ruinname.dbf", false) - && readSiteText(trainerTexts, scenDataFolderPath / "Trainame.dbf"); + && readSiteText(trainerTexts, scenDataFolderPath / "Trainame.dbf") + // If resource market feature exists, 'Marketname.dbf' must be valid + && (!resourceMarketExists + || (resourceMarketExists + && readSiteText(marketTexts, scenDataFolderPath / "Marketname.dbf"))); } } // namespace hooks diff --git a/mss32/src/netmsg.cpp b/mss32/src/netmsg.cpp index e706a090..373b9af9 100644 --- a/mss32/src/netmsg.cpp +++ b/mss32/src/netmsg.cpp @@ -28,18 +28,22 @@ static std::array functions = {{ // Akella Api{ (Api::Destructor)0x55cc83, + (Api::Serialize)0x55cc9f, }, // Russobit Api{ (Api::Destructor)0x55cc83, + (Api::Serialize)0x55cc9f, }, // Gog Api{ (Api::Destructor)0x55c470, + (Api::Serialize)0x55c48c, }, // Scenario Editor Api{ (Api::Destructor)nullptr, + (Api::Serialize)nullptr, }, }}; // clang-format on diff --git a/mss32/src/netmsgmapentryexchangeresourcesmsg.cpp b/mss32/src/netmsgmapentryexchangeresourcesmsg.cpp new file mode 100644 index 00000000..7659b8c7 --- /dev/null +++ b/mss32/src/netmsgmapentryexchangeresourcesmsg.cpp @@ -0,0 +1,170 @@ +/* + * This file is part of the modding toolset for Disciples 2. + * (https://github.com/VladimirMakeev/D2ModdingToolset) + * Copyright (C) 2024 Vladimir Makeev. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "netmsgmapentryexchangeresourcesmsg.h" +#include "dynamiccast.h" +#include "exchangeresourcesmsg.h" +#include "mempool.h" +#include "netmsg.h" +#include "streambits.h" + +namespace hooks { + +struct CNetMsgMapEntryExchangeResources : public game::CNetMsgMapEntry_member +{ }; + +static void __fastcall destructor(CNetMsgMapEntryExchangeResources* thisptr, + int /*%edx*/, + char flags) +{ + if (flags & 1) { + game::Memory::get().freeNonZero(thisptr); + } +} + +static const char* __fastcall getRawName(CNetMsgMapEntryExchangeResources* thisptr, int /*%edx*/) +{ + const auto& rawName{*game::RttiApi::get().typeInfoRawName}; + + return rawName(getExchangeResourcesMsgTypeDescriptor()); +} + +static bool __fastcall process(CNetMsgMapEntryExchangeResources* thisptr, + int /*%edx*/, + game::NetMessageHeader* header, + std::uint32_t idFrom, + std::uint32_t playerNetId) +{ + using namespace game; + + const auto& streamApi{CStreamBitsApi::get()}; + + CStreamBits stream; + streamApi.readConstructor(&stream, 0, header, header->length, false); + + CExchangeResourcesMsg message; + message.vftable->serialize(&message, &stream); + + const bool result{thisptr->vftable->runCallback(thisptr, &message, idFrom, playerNetId)}; + + streamApi.destructor(&stream); + return result; +} + +static bool __fastcall runCallback(CNetMsgMapEntryExchangeResources* thisptr, + int /*%edx*/, + game::CNetMsg* netMessage, + std::uint32_t idFrom, + std::uint32_t) +{ + return thisptr->callback(thisptr->data, netMessage, idFrom); +} + +static game::TypeDescriptor* getNetMsgMapEntryExchangeResourcesTypeDescriptor() +{ + using namespace game; + + // clang-format off + static game::TypeDescriptor descriptor{ + game::RttiApi::typeInfoVftable(), + nullptr, + ".?AVCNetMsgMapEntryExchangeResources@@", + }; + // clang-format on + + return &descriptor; +} + +static game::ClassHierarchyDescriptor* getHierarchyDescriptor() +{ + using namespace game; + + // clang-format off + static game::BaseClassDescriptor baseClassDescriptor{ + getNetMsgMapEntryExchangeResourcesTypeDescriptor(), + 1u, // base class array has 1 more element after this descriptor + game::PMD{ 0, -1, 0 }, + 0u + }; + + static game::BaseClassArray baseClassArray{ + &baseClassDescriptor, + RttiApi::rtti().CNetMsgMapEntryDescriptor + }; + + static game::ClassHierarchyDescriptor hierarchyDescriptor{ + 0, + 0, + 2u, + &baseClassArray + }; + // clang-format on + + return &hierarchyDescriptor; +} + +using MapEntryRttiInfo = game::RttiInfo; + +static MapEntryRttiInfo& getMsgRttiInfo() +{ + using namespace game; + + // clang-format off + static const game::CompleteObjectLocator objectLocator{ + 0u, + offsetof(CNetMsgMapEntryExchangeResources, vftable), + 0u, + getNetMsgMapEntryExchangeResourcesTypeDescriptor(), + getHierarchyDescriptor() + }; + // clang-format on + + static MapEntryRttiInfo rttiInfo{&objectLocator}; + return rttiInfo; +} + +game::CNetMsgMapEntry_member* createNetMsgMapEntryExchangeResourcesMsg( + game::CMidServerLogic* serverLogic, + game::CNetMsgMapEntry_member::Callback callback) +{ + using namespace game; + + auto entry{(CNetMsgMapEntryExchangeResources*)Memory::get().allocate( + sizeof(CNetMsgMapEntryExchangeResources))}; + + static bool firstTime{true}; + if (firstTime) { + firstTime = false; + + // Use our own vftable + auto& vftable{getMsgRttiInfo().vftable}; + vftable.destructor = (CNetMsgMapEntry_memberVftable::Destructor)destructor; + vftable.getName = (CNetMsgMapEntry_memberVftable::GetName)getRawName; + vftable.process = (CNetMsgMapEntry_memberVftable::Process)process; + vftable.runCallback = (CNetMsgMapEntry_memberVftable::RunCallback)runCallback; + } + + entry->vftable = &getMsgRttiInfo().vftable; + entry->data = serverLogic; + entry->callback = callback; + + return entry; +} + +} // namespace hooks diff --git a/mss32/src/nobleactioncat.cpp b/mss32/src/nobleactioncat.cpp new file mode 100644 index 00000000..ba3970c2 --- /dev/null +++ b/mss32/src/nobleactioncat.cpp @@ -0,0 +1,170 @@ +/* + * This file is part of the modding toolset for Disciples 2. + * (https://github.com/VladimirMakeev/D2ModdingToolset) + * Copyright (C) 2024 Vladimir Makeev. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "nobleactioncat.h" +#include "version.h" +#include + +namespace game { + +namespace NobleActionCategories { + +// clang-format off +static std::array categories = {{ + // Akella + Categories{ + (LNobleActionCat*)0x839ea0, + (LNobleActionCat*)0x839e90, + (LNobleActionCat*)0x839eb0, + (LNobleActionCat*)0x839f10, + (LNobleActionCat*)0x839f30, + (LNobleActionCat*)0x839f48, + (LNobleActionCat*)0x839ee0, + (LNobleActionCat*)0x839ec0, + (LNobleActionCat*)0x839ed0, + (LNobleActionCat*)0x839f58, + (LNobleActionCat*)0x839f20, + (LNobleActionCat*)0x839f00, + (LNobleActionCat*)0x839ef0, + (LNobleActionCat*)0x839e80, + }, + // Russobit + Categories{ + (LNobleActionCat*)0x839ea0, + (LNobleActionCat*)0x839e90, + (LNobleActionCat*)0x839eb0, + (LNobleActionCat*)0x839f10, + (LNobleActionCat*)0x839f30, + (LNobleActionCat*)0x839f48, + (LNobleActionCat*)0x839ee0, + (LNobleActionCat*)0x839ec0, + (LNobleActionCat*)0x839ed0, + (LNobleActionCat*)0x839f58, + (LNobleActionCat*)0x839f20, + (LNobleActionCat*)0x839f00, + (LNobleActionCat*)0x839ef0, + (LNobleActionCat*)0x839e80, + }, + // Gog + Categories{ + (LNobleActionCat*)0x837e50, + (LNobleActionCat*)0x837e40, + (LNobleActionCat*)0x837e60, + (LNobleActionCat*)0x837ec0, + (LNobleActionCat*)0x837ee0, + (LNobleActionCat*)0x837ef8, + (LNobleActionCat*)0x837e90, + (LNobleActionCat*)0x837e70, + (LNobleActionCat*)0x837e80, + (LNobleActionCat*)0x837f08, + (LNobleActionCat*)0x837ed0, + (LNobleActionCat*)0x837eb0, + (LNobleActionCat*)0x837ea0, + (LNobleActionCat*)0x837e30, + }, + // Scenario Editor + Categories{ + (LNobleActionCat*)0x665ce0, + (LNobleActionCat*)0x665cd0, + (LNobleActionCat*)0x665cf0, + (LNobleActionCat*)0x665d50, + (LNobleActionCat*)0x665d70, + (LNobleActionCat*)0x665d88, + (LNobleActionCat*)0x665d20, + (LNobleActionCat*)0x665d00, + (LNobleActionCat*)0x665d10, + (LNobleActionCat*)0x665d98, + (LNobleActionCat*)0x665d60, + (LNobleActionCat*)0x665d40, + (LNobleActionCat*)0x665d30, + (LNobleActionCat*)0x665cc0, + } +}}; +// clang-format on + +Categories& get() +{ + return categories[static_cast(hooks::gameVersion())]; +} + +} // namespace NobleActionCategories + +namespace LNobleActionCatTableApi { + +// clang-format off +static std::array functions = {{ + // Akella + Api{ + (Api::Constructor)0x58f64f, + (Api::Init)0x58f89c, + (Api::ReadCategory)0x58f914, + (Api::InitDone)0x58f857, + (Api::FindCategoryById)0, + }, + // Russobit + Api{ + (Api::Constructor)0x58f64f, + (Api::Init)0x58f89c, + (Api::ReadCategory)0x58f914, + (Api::InitDone)0x58f857, + (Api::FindCategoryById)0, + }, + // Gog + Api{ + (Api::Constructor)0x58e764, + (Api::Init)0x58e9b1, + (Api::ReadCategory)0x58ea29, + (Api::InitDone)0x58e96c, + (Api::FindCategoryById)0, + }, + // Scenario Editor + Api{ + (Api::Constructor)0x54047a, + (Api::Init)0x5406c7, + (Api::ReadCategory)0x54073f, + (Api::InitDone)0x540682, + (Api::FindCategoryById)0, + } +}}; + +static std::array vftables = {{ + // Akella + (const void*)0x6eae34, + // Russobit + (const void*)0x6eae34, + // Gog + (const void*)0x6e8dd4, + // Scenario Editor + (const void*)0x5dfd4c, +}}; +// clang-format on + +Api& get() +{ + return functions[static_cast(hooks::gameVersion())]; +} + +const void* vftable() +{ + return vftables[static_cast(hooks::gameVersion())]; +} + +} // namespace LNobleActionCatTableApi + +} // namespace game diff --git a/mss32/src/nobleactioncategoryset.cpp b/mss32/src/nobleactioncategoryset.cpp new file mode 100644 index 00000000..dfe33123 --- /dev/null +++ b/mss32/src/nobleactioncategoryset.cpp @@ -0,0 +1,48 @@ +/* + * This file is part of the modding toolset for Disciples 2. + * (https://github.com/VladimirMakeev/D2ModdingToolset) + * Copyright (C) 2024 Vladimir Makeev. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "nobleactioncategoryset.h" +#include "version.h" +#include + +namespace game::NobleActionCatSetApi { + +// clang-format off +static std::array functions = {{ + // Akella + Api{ + (Api::Insert)0x47a0de, + }, + // Russobit + Api{ + (Api::Insert)0x47a0de, + }, + // Gog + Api{ + (Api::Insert)0x479c92, + }, +}}; +// clang-format on + +Api& get() +{ + return functions[static_cast(hooks::gameVersion())]; +} + +} // namespace game::NobleActionCatSetApi diff --git a/mss32/src/nobleactionresult.cpp b/mss32/src/nobleactionresult.cpp new file mode 100644 index 00000000..986599e3 --- /dev/null +++ b/mss32/src/nobleactionresult.cpp @@ -0,0 +1,48 @@ +/* + * This file is part of the modding toolset for Disciples 2. + * (https://github.com/VladimirMakeev/D2ModdingToolset) + * Copyright (C) 2024 Vladimir Makeev. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "nobleactionresult.h" +#include "version.h" +#include + +namespace game::NobleActionsApi { + +// clang-format off +static std::array functions = {{ + // Akella + Api{ + (Api::Create)0x44359e, + }, + // Russobit + Api{ + (Api::Create)0x44359e, + }, + // Gog + Api{ + (Api::Create)0x4431e8, + }, +}}; +// clang-format on + +Api& get() +{ + return functions[static_cast(hooks::gameVersion())]; +} + +} // namespace game::NobleActionsApi diff --git a/mss32/src/nobleactionresultstealmarket.cpp b/mss32/src/nobleactionresultstealmarket.cpp new file mode 100644 index 00000000..f9fedcfa --- /dev/null +++ b/mss32/src/nobleactionresultstealmarket.cpp @@ -0,0 +1,156 @@ +/* + * This file is part of the modding toolset for Disciples 2. + * (https://github.com/VladimirMakeev/D2ModdingToolset) + * Copyright (C) 2024 Vladimir Makeev. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "nobleactionresultstealmarket.h" +#include "effectgiveresources.h" +#include "effectstealmarket.h" +#include "game.h" +#include "gameutils.h" +#include "globalvariables.h" +#include "idlist.h" +#include "mempool.h" +#include "midgardobjectmap.h" +#include "midsiteresourcemarket.h" +#include "midstack.h" +#include "nobleactionresult.h" +#include + +namespace hooks { + +struct CNobleActionResultStealMarket : public game::INobleActionResult +{ + game::CMidgardID marketId; + game::CurrencyType resource; + std::int16_t amount; +}; + +static void __fastcall stealMarketDtor(CNobleActionResultStealMarket* thisptr, + int /*%edx*/, + char flags) +{ + if (flags & 1) { + game::Memory::get().freeNonZero(thisptr); + } +} + +static int __fastcall stealMarketGetValue(const CNobleActionResultStealMarket*, int /*%edx*/) +{ + return 0; +} + +static const game::CMidgardID* __fastcall stealMarketGetId( + const CNobleActionResultStealMarket* thisptr, + int /*%edx*/) +{ + return &thisptr->marketId; +} + +static bool __fastcall stealMarketApply(CNobleActionResultStealMarket* thisptr, + int /*%edx*/, + game::IMidgardObjectMap* objectMap, + game::List* effects, + const game::CMidgardID* stackId, + const game::CMidgardID* targetObjectId) +{ + using namespace game; + + auto steal{createEffectStealMarket(thisptr->marketId, thisptr->resource, thisptr->amount)}; + + // Reuse IdList::pushBack as the game does in other INobleActionResult::Apply methods + IdListApi::get().pushBack((IdList*)effects, (const CMidgardID*)&steal); + + const CMidStack* noble{getStack(objectMap, stackId)}; + + Bank resources; + BankApi::get().setZero(&resources); + BankApi::get().set(&resources, thisptr->resource, thisptr->amount); + + auto giveResources{createEffectGiveResources(noble->ownerId, resources, true)}; + IdListApi::get().pushBack((IdList*)effects, (const CMidgardID*)&giveResources); + + return true; +} + +// clang-format off +static game::INobleActionResultVftable stealMarketVftable{ + (game::INobleActionResultVftable::Destructor)stealMarketDtor, + (game::INobleActionResultVftable::GetValue)stealMarketGetValue, + (game::INobleActionResultVftable::GetId)stealMarketGetId, + (game::INobleActionResultVftable::Apply)stealMarketApply, +}; +// clang-format on + +game::INobleActionResult* createStealMarketActionResult(const game::IMidgardObjectMap* objectMap, + const game::CMidgardID& marketId) +{ + using namespace game; + + auto stealMarket = (CNobleActionResultStealMarket*)Memory::get().allocate( + sizeof(CNobleActionResultStealMarket)); + stealMarket->vftable = &stealMarketVftable; + stealMarket->marketId = marketId; + + auto market{ + (const CMidSiteResourceMarket*)objectMap->vftable->findScenarioObjectById(objectMap, + &marketId)}; + + // clang-format off + const std::array resources{ + CurrencyType::Gold, CurrencyType::InfernalMana, + CurrencyType::LifeMana, CurrencyType::DeathMana, + CurrencyType::RunicMana, CurrencyType::GroveMana + }; + // clang-format on + + std::vector availableResources; + availableResources.reserve(resources.size()); + + for (const auto resource : resources) { + if (isMarketStockInfinite(market->infiniteStock, resource) + || BankApi::get().get(&market->stock, resource) > 0) { + availableResources.push_back(resource); + } + } + + if (availableResources.empty()) { + // This should never happen + stealMarket->resource = CurrencyType::Gold; + stealMarket->amount = 0; + } else { + // Pick random resource from those available and compute amount to steal + const int resourceIndex{gameFunctions().generateRandomNumber(availableResources.size())}; + const CurrencyType randomResource{availableResources[resourceIndex]}; + + const GlobalData* global{*GlobalDataApi::get().getGlobalData()}; + const GlobalVariables* variables{global->globalVariables}; + + int maxStealAmount{variables->data->stealRmkt}; + if (!isMarketStockInfinite(market->infiniteStock, randomResource)) { + maxStealAmount = std::min(maxStealAmount, + (int)BankApi::get().get(&market->stock, randomResource)); + } + + stealMarket->resource = randomResource; + stealMarket->amount = gameFunctions().generateRandomNumber(maxStealAmount) + 1; + } + + return stealMarket; +} + +} // namespace hooks diff --git a/mss32/src/objectinterf.cpp b/mss32/src/objectinterf.cpp new file mode 100644 index 00000000..5d718258 --- /dev/null +++ b/mss32/src/objectinterf.cpp @@ -0,0 +1,31 @@ +/* + * This file is part of the modding toolset for Disciples 2. + * (https://github.com/VladimirMakeev/D2ModdingToolset) + * Copyright (C) 2024 Vladimir Makeev. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "objectinterf.h" + +namespace game::editor::CObjectInterfApi { + +Api& get() +{ + static Api api{(Api::CreateTaskObj)0x449d5f}; + + return api; +} + +} // namespace game::editor::CObjectInterfApi diff --git a/mss32/src/objectinterfhooks.cpp b/mss32/src/objectinterfhooks.cpp new file mode 100644 index 00000000..07f5420c --- /dev/null +++ b/mss32/src/objectinterfhooks.cpp @@ -0,0 +1,65 @@ +/* + * This file is part of the modding toolset for Disciples 2. + * (https://github.com/VladimirMakeev/D2ModdingToolset) + * Copyright (C) 2024 Vladimir Makeev. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "objectinterfhooks.h" +#include "log.h" +#include "mempool.h" +#include "objectinterf.h" +#include "originalfunctions.h" +#include "sitecategoryhooks.h" +#include "taskobjaddsite.h" +#include + +namespace hooks { + +game::editor::CTaskObj* __fastcall createTaskObjHooked(game::ITaskManagerHolder* thisptr, + int /* %edx */) +{ + using namespace game; + using namespace editor; + + CTaskObj* task = getOriginalFunctions().createTaskObj(thisptr); + if (task) { + return task; + } + + const auto ptr = reinterpret_cast(thisptr) - 8u; + CObjectInterf* objInterf = reinterpret_cast(ptr); + + // TOG_RES_MARKET radio button in DLG_OBJECTS, ScenEdit.dlg + constexpr int togResMarket = 13; + if (objInterf->objInterfData->selectedMode != togResMarket) { + logError("mssProxyError.log", fmt::format("Unknown selection mode {:d}", + objInterf->objInterfData->selectedMode)); + return nullptr; + } + + if (!customSiteCategories().exists) { + logError("mssProxyError.log", fmt::format("Toggle button was added to the UI" + " but new site category does not exist.")); + return nullptr; + } + + auto addSite = (CTaskObjAddSite*)Memory::get().allocate(sizeof(CTaskObjAddSite)); + CTaskObjAddSiteApi::get().constructor(addSite, objInterf, + customSiteCategories().resourceMarket); + return addSite; +} + +} // namespace hooks diff --git a/mss32/src/paperdollchildinterf.cpp b/mss32/src/paperdollchildinterf.cpp new file mode 100644 index 00000000..95f6a5ff --- /dev/null +++ b/mss32/src/paperdollchildinterf.cpp @@ -0,0 +1,52 @@ +/* + * This file is part of the modding toolset for Disciples 2. + * (https://github.com/VladimirMakeev/D2ModdingToolset) + * Copyright (C) 2024 Vladimir Makeev. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "paperdollchildinterf.h" +#include "version.h" +#include + +namespace game::CPaperdollChildInterfApi { + +// clang-format off +static std::array functions = {{ + // Akella + Api{ + (Api::Constructor)0x4cc5d0, + }, + // Russobit + Api{ + (Api::Constructor)0x4cc5d0, + }, + // Gog + Api{ + (Api::Constructor)0x4cbcea, + }, + // Scenario Editor + Api{ + (Api::Constructor)0, + } +}}; +// clang-format on + +Api& get() +{ + return functions[static_cast(hooks::gameVersion())]; +} + +} // namespace game::CPaperdollChildInterfApi diff --git a/mss32/src/phase.cpp b/mss32/src/phase.cpp index bcef07af..9451b97a 100644 --- a/mss32/src/phase.cpp +++ b/mss32/src/phase.cpp @@ -29,16 +29,22 @@ static std::array functions = {{ Api{ (Api::GetObjectMap)0x404f06, (Api::GetCurrentPlayerId)0x404e71, + (Api::GetCommandQueue)0x404f13, + (Api::ShowEncyclopediaPopup)0x404f57, }, // Russobit Api{ (Api::GetObjectMap)0x404f06, (Api::GetCurrentPlayerId)0x404e71, + (Api::GetCommandQueue)0x404f13, + (Api::ShowEncyclopediaPopup)0x404f57, }, // Gog Api{ (Api::GetObjectMap)0x404b8e, (Api::GetCurrentPlayerId)0x404af9, + (Api::GetCommandQueue)0x404b9b, + (Api::ShowEncyclopediaPopup)0x404bdf, } }}; // clang-format on diff --git a/mss32/src/pictureinterf.cpp b/mss32/src/pictureinterf.cpp index b28178b5..0c7098df 100644 --- a/mss32/src/pictureinterf.cpp +++ b/mss32/src/pictureinterf.cpp @@ -28,21 +28,25 @@ static std::array functions = {{ // Akella Api{ (Api::SetImage)0x5318a0, + (Api::SetImageWithAnchor)0x5318ea, (Api::AssignFunctor)0x531843, }, // Russobit Api{ (Api::SetImage)0x5318a0, + (Api::SetImageWithAnchor)0x5318ea, (Api::AssignFunctor)0x531843, }, // Gog Api{ (Api::SetImage)0x530db8, + (Api::SetImageWithAnchor)0x530e29, (Api::AssignFunctor)0x530d5b, }, // Scenario Editor Api{ (Api::SetImage)0x494988, + (Api::SetImageWithAnchor)0x4949d2, (Api::AssignFunctor)0, }, }}; diff --git a/mss32/src/raceset.cpp b/mss32/src/raceset.cpp index 397587f5..17528feb 100644 --- a/mss32/src/raceset.cpp +++ b/mss32/src/raceset.cpp @@ -29,16 +29,19 @@ static std::array functions = {{ Api{ (Api::Clear)0x424879, (Api::Add)0x4246d4, + (Api::Find)0x442d2d, }, // Russobit Api{ (Api::Clear)0x424879, (Api::Add)0x4246d4, + (Api::Find)0x442d2d, }, // Gog Api{ (Api::Clear)0x42434a, (Api::Add)0x4241e7, + (Api::Find)0x442992, } }}; // clang-format on diff --git a/mss32/src/radiobuttoninterf.cpp b/mss32/src/radiobuttoninterf.cpp index c9ee8371..9623c5ed 100644 --- a/mss32/src/radiobuttoninterf.cpp +++ b/mss32/src/radiobuttoninterf.cpp @@ -30,21 +30,21 @@ static std::array functions = {{ (Api::SetEnabled)0, (Api::SetButtonEnabled)0, (Api::SetCheckedButton)0, - (Api::SetOnButtonPressed)0, + (Api::SetOnButtonPressed)0x5c951c, }, // Russobit Api{ (Api::SetEnabled)0, (Api::SetButtonEnabled)0, (Api::SetCheckedButton)0, - (Api::SetOnButtonPressed)0, + (Api::SetOnButtonPressed)0x5c951c, }, // Gog Api{ (Api::SetEnabled)0, (Api::SetButtonEnabled)0, (Api::SetCheckedButton)0, - (Api::SetOnButtonPressed)0, + (Api::SetOnButtonPressed)0x5c84ea, }, // Scenario Editor Api{ diff --git a/mss32/src/resourcemarketinterface.cpp b/mss32/src/resourcemarketinterface.cpp new file mode 100644 index 00000000..761400f2 --- /dev/null +++ b/mss32/src/resourcemarketinterface.cpp @@ -0,0 +1,488 @@ +/* + * This file is part of the modding toolset for Disciples 2. + * (https://github.com/VladimirMakeev/D2ModdingToolset) + * Copyright (C) 2024 Vladimir Makeev. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "resourcemarketinterface.h" +#include "button.h" +#include "dialoginterf.h" +#include "draganddropinterf.h" +#include "editboxinterf.h" +#include "editor.h" +#include "intvector.h" +#include "mapinterf.h" +#include "mempool.h" +#include "midgardscenariomap.h" +#include "midsiteresourcemarket.h" +#include "mqpresentationmanager.h" +#include "pictureinterf.h" +#include "rendererimpl.h" +#include "scenedit.h" +#include "sitecategoryhooks.h" +#include "spinbuttoninterf.h" +#include "stringarray.h" +#include "taskobjprop.h" +#include "togglebutton.h" +#include "trainingcampinterf.h" +#include "utils.h" +#include "visitors.h" +#include +#include +#include + +namespace hooks { + +static const char dialogName[] = "DLG_RESOURCE_MARKET"; + +struct CResourceMarketInterf : public game::CDragAndDropInterf +{ + std::string customRates; + game::IntVector imageIndices; + game::editor::CTaskObjProp* taskObjProp; + CMidSiteResourceMarket* market; + int selectedIndex; +}; + +static game::CInterfaceVftable resMarketInterfVftable{}; + +static game::CInterfaceVftable::Destructor dragDropDtor = nullptr; + +static void __fastcall resMarketInterfDtor(CResourceMarketInterf* thisptr, int /*%edx*/, char flags) +{ + using namespace game; + + thisptr->customRates.~basic_string(); + IntVectorApi::get().destructor(&thisptr->imageIndices); + + if (dragDropDtor) { + dragDropDtor(thisptr, flags); + } +} + +static void setEditBoxManaValue(game::CDialogInterf* dialog, + const char* editBoxName, + std::int16_t mana) +{ + using namespace game; + + char buffer[5] = {}; + fmt::format_to_n(buffer, sizeof(buffer) - 1u, "{:d}", mana); + + CEditBoxInterf* editBox{CDialogInterfApi::get().findEditBox(dialog, editBoxName)}; + CEditBoxInterfApi::get().setString(editBox, buffer); +} + +static std::int16_t getEditBoxManaValue(game::CDialogInterf* dialog, const char* editBoxName) +{ + using namespace game; + + CEditBoxInterf* editBox{CDialogInterfApi::get().findEditBox(dialog, editBoxName)}; + const String& string{editBox->data->editBoxData.inputString}; + + std::int16_t value{}; + auto [ptr, error] = std::from_chars(string.string, string.string + string.length, value); + if (error != std::errc()) { + return 0; + } + + return value; +} + +static void setInfiniteManaToggle(game::CDialogInterf* dialog, + const char* buttonName, + bool infinite) +{ + using namespace game; + + CToggleButton* button{CDialogInterfApi::get().findToggleButton(dialog, buttonName)}; + CToggleButtonApi::get().setChecked(button, infinite); +} + +static bool getInfiniteManaToggle(game::CDialogInterf* dialog, const char* buttonName) +{ + using namespace game; + + const CToggleButton* button{CDialogInterfApi::get().findToggleButton(dialog, buttonName)}; + return button->data->checked; +} + +static void __fastcall resMarketInterfOkButtonHandler(CResourceMarketInterf* thisptr, int /*%edx*/) +{ + using namespace game; + using namespace editor; + + const auto& dialogApi{CDialogInterfApi::get()}; + CDialogInterf* dialog{thisptr->dragAndDropInterfData->dialogInterf}; + + CEditBoxInterf* editName{dialogApi.findEditBox(dialog, "EDIT_NAME")}; + CEditBoxInterf* editDesc{dialogApi.findEditBox(dialog, "EDIT_DESCRIPTION")}; + + const auto& getCString{StringApi::get().cStr}; + + const char* name{getCString(&editName->data->editBoxData.inputString)}; + const char* description{getCString(&editDesc->data->editBoxData.inputString)}; + + CScenEdit* scenEdit{CScenEditApi::get().instance()}; + IMidgardObjectMap* objectMap{scenEdit->data->unknown2->data->scenarioMap}; + + VisitorApi::get().changeSiteInfo(&thisptr->market->id, name, description, objectMap, 1); + + // Returned object is the same as thisptr->market, the call is needed to mark it for change + objectMap->vftable->findScenarioObjectByIdForChange(objectMap, &thisptr->market->id); + + Bank* stock{&thisptr->market->stock}; + const auto& setCurrency{BankApi::get().set}; + setCurrency(stock, CurrencyType::LifeMana, getEditBoxManaValue(dialog, "EDIT_LIFE")); + setCurrency(stock, CurrencyType::InfernalMana, getEditBoxManaValue(dialog, "EDIT_INFERNAL")); + setCurrency(stock, CurrencyType::RunicMana, getEditBoxManaValue(dialog, "EDIT_RUNIC")); + setCurrency(stock, CurrencyType::DeathMana, getEditBoxManaValue(dialog, "EDIT_DEATH")); + setCurrency(stock, CurrencyType::GroveMana, getEditBoxManaValue(dialog, "EDIT_GROVE")); + setCurrency(stock, CurrencyType::Gold, getEditBoxManaValue(dialog, "EDIT_GOLD")); + + InfiniteStock& infiniteStock{thisptr->market->infiniteStock}; + infiniteStock.parts.lifeMana = getInfiniteManaToggle(dialog, "TOG_INF_LIFE"); + infiniteStock.parts.infernalMana = getInfiniteManaToggle(dialog, "TOG_INF_INFERNAL"); + infiniteStock.parts.runicMana = getInfiniteManaToggle(dialog, "TOG_INF_RUNIC"); + infiniteStock.parts.deathMana = getInfiniteManaToggle(dialog, "TOG_INF_DEATH"); + infiniteStock.parts.groveMana = getInfiniteManaToggle(dialog, "TOG_INF_GROVE"); + infiniteStock.parts.gold = getInfiniteManaToggle(dialog, "TOG_INF_GOLD"); + + const CToggleButton* customRates{dialogApi.findToggleButton(dialog, "TOG_CUSTOM_RATES")}; + thisptr->market->customExchangeRates = customRates->data->checked; + + if (customRates->data->checked) { + thisptr->market->exchangeRatesScript = thisptr->customRates; + } + + const CSpinButtonInterf* spin{dialogApi.findSpinButton(dialog, "SPIN_AI_PRIORITY")}; + const int aiPriority{spin->data->selectedOption}; + + VisitorApi::get().changeSiteAiPriority(&thisptr->market->id, aiPriority, objectMap, 1); + + CTaskObjPropVftable* vftable{(CTaskObjPropVftable*)thisptr->taskObjProp->vftable}; + vftable->closePropertiesInterface(thisptr->taskObjProp); +} + +static void updateMarketImage(CResourceMarketInterf* thisptr, int imageIndex) +{ + using namespace game; + + CDialogInterf* dialog{thisptr->dragAndDropInterfData->dialogInterf}; + CPictureInterf* picture{CDialogInterfApi::get().findPicture(dialog, "IMG_SITE")}; + + const LSiteCategory* category{&customSiteCategories().resourceMarket}; + IMqImage2* image{editorFunctions.getSiteImage(category, imageIndex, false)}; + + CPictureInterfApi::get().setImageWithAnchor(picture, image, 9); + + CScenEdit* scenEdit{CScenEditApi::get().instance()}; + IMidgardObjectMap* objectMap{scenEdit->data->unknown2->data->scenarioMap}; + + if (!VisitorApi::get().changeSiteImage(&thisptr->market->id, imageIndex, objectMap, 1)) { + return; + } + + editorFunctions.showOrHideSiteOnStrategicMap(thisptr->market, objectMap, nullptr, 1); +} + +static void __fastcall resMarketInterfImgUpButtonHandler(CResourceMarketInterf* thisptr, + int /*%edx*/) +{ + using namespace game; + + const auto& indices{thisptr->imageIndices}; + + if (thisptr->selectedIndex == 0) { + thisptr->selectedIndex = indices.end - indices.bgn; + } + + --thisptr->selectedIndex; + const int imageIndex{indices.bgn[thisptr->selectedIndex]}; + + updateMarketImage(thisptr, imageIndex); +} + +static void __fastcall resMarketInterfImgDownButtonHandler(CResourceMarketInterf* thisptr, + int /*%edx*/) +{ + using namespace game; + + ++thisptr->selectedIndex; + + const auto& indices{thisptr->imageIndices}; + if (thisptr->selectedIndex == indices.end - indices.bgn) { + thisptr->selectedIndex = 0; + } + + const int imageIndex{indices.bgn[thisptr->selectedIndex]}; + updateMarketImage(thisptr, imageIndex); +} + +static void __fastcall resMarketInterfLoadButtonHandler(CResourceMarketInterf* thisptr, + int /*%edx*/) +{ + using namespace game; + + const auto& managerApi{CMqPresentationManagerApi::get()}; + + PresentationMgrPtr manager; + managerApi.getPresentationManager(&manager); + + auto* renderer{manager.data->data->renderer}; + IMqDisplay2* display = (IMqDisplay2*)renderer; + + display->vftable->flipToGDISurface(display); + + managerApi.presentationMgrPtrSetData(&manager, nullptr); + + std::string script; + if (readUserSelectedFile(script, "Lua scripts (*.lua)\0*.lua\0\0", + scriptsFolder().string().c_str())) { + CDialogInterf* dialog{thisptr->dragAndDropInterfData->dialogInterf}; + CEditBoxInterf* editRates{CDialogInterfApi::get().findEditBox(dialog, "EDIT_EXC_RATES")}; + + CEditBoxInterfApi::get().setString(editRates, script.c_str()); + std::swap(thisptr->customRates, script); + } +} + +static void setupAiPriority(game::CSpinButtonInterf* spin, int priority) +{ + using namespace game; + + const auto& stringArrayApi{StringArrayApi::get()}; + const auto& stringApi{StringApi::get()}; + const auto& spinApi{CSpinButtonInterfApi::get()}; + + StringArray options{}; + stringArrayApi.reserve(&options, 7); + + char buffer[2]; + buffer[1] = 0; + + for (int i = 0; i < 7; ++i) { + buffer[0] = static_cast(i + '0'); + + String option{}; + stringApi.initFromString(&option, buffer); + + stringArrayApi.pushBack(&options, &option); + stringApi.free(&option); + } + + spinApi.setOptions(spin, &options); + spinApi.setSelectedOption(spin, priority); + + stringArrayApi.destructor(&options); +} + +static void __fastcall resMarketInterfOnCustomRatesToggleHandler(CResourceMarketInterf* thisptr, + int /*%edx*/, + bool pressed, + game::CToggleButton* button) +{ + using namespace game; + + const auto& dialogApi{CDialogInterfApi::get()}; + CDialogInterf* dialog{thisptr->dragAndDropInterfData->dialogInterf}; + + CButtonInterf* loadButton{dialogApi.findButton(dialog, "BTN_LOAD")}; + loadButton->vftable->setEnabled(loadButton, pressed); +} + +static void setupUi(const CResourceMarketInterf* thisptr) +{ + using namespace game; + using namespace editor; + + const auto& dialogApi{CDialogInterfApi::get()}; + CDialogInterf* dialog{thisptr->dragAndDropInterfData->dialogInterf}; + + const auto& editBox{CEditBoxInterfApi::get()}; + + editBox.setFilterAndLength(dialog, "EDIT_NAME", dialogName, EditFilter::NoFilter, 40); + editBox.setFilterAndLength(dialog, "EDIT_DESCRIPTION", dialogName, EditFilter::NoFilter, 100); + + CEditBoxInterf* nameEdit{dialogApi.findEditBox(dialog, "EDIT_NAME")}; + CEditBoxInterf* descriptionEdit{dialogApi.findEditBox(dialog, "EDIT_DESCRIPTION")}; + + const CMidSiteResourceMarket* market{thisptr->market}; + + const char* title{market->title.string ? market->title.string : ""}; + editBox.setString(nameEdit, title); + + const char* description{market->description.string ? market->description.string : ""}; + editBox.setString(descriptionEdit, description); + + // Make sure resource edit boxes allow entering only valid amounts + editBox.setFilterAndLength(dialog, "EDIT_LIFE", dialogName, EditFilter::DigitsOnly, 4); + editBox.setFilterAndLength(dialog, "EDIT_INFERNAL", dialogName, EditFilter::DigitsOnly, 4); + editBox.setFilterAndLength(dialog, "EDIT_RUNIC", dialogName, EditFilter::DigitsOnly, 4); + editBox.setFilterAndLength(dialog, "EDIT_DEATH", dialogName, EditFilter::DigitsOnly, 4); + editBox.setFilterAndLength(dialog, "EDIT_GROVE", dialogName, EditFilter::DigitsOnly, 4); + editBox.setFilterAndLength(dialog, "EDIT_GOLD", dialogName, EditFilter::DigitsOnly, 4); + + // Resource amount settings + const Bank& stock{market->stock}; + setEditBoxManaValue(dialog, "EDIT_LIFE", stock.lifeMana); + setEditBoxManaValue(dialog, "EDIT_INFERNAL", stock.infernalMana); + setEditBoxManaValue(dialog, "EDIT_RUNIC", stock.runicMana); + setEditBoxManaValue(dialog, "EDIT_DEATH", stock.deathMana); + setEditBoxManaValue(dialog, "EDIT_GROVE", stock.groveMana); + setEditBoxManaValue(dialog, "EDIT_GOLD", stock.gold); + + // Infinite resource amounts + const InfiniteStock& infiniteStock{market->infiniteStock}; + setInfiniteManaToggle(dialog, "TOG_INF_LIFE", infiniteStock.parts.lifeMana); + setInfiniteManaToggle(dialog, "TOG_INF_INFERNAL", infiniteStock.parts.infernalMana); + setInfiniteManaToggle(dialog, "TOG_INF_RUNIC", infiniteStock.parts.runicMana); + setInfiniteManaToggle(dialog, "TOG_INF_DEATH", infiniteStock.parts.deathMana); + setInfiniteManaToggle(dialog, "TOG_INF_GROVE", infiniteStock.parts.groveMana); + setInfiniteManaToggle(dialog, "TOG_INF_GOLD", infiniteStock.parts.gold); + + CToggleButton* customRates{dialogApi.findToggleButton(dialog, "TOG_CUSTOM_RATES")}; + CToggleButtonApi::get().setChecked(customRates, market->customExchangeRates); + + const auto freeFunctor{SmartPointerApi::get().createOrFreeNoDtor}; + SmartPointer functor; + + { + using ButtonCallback = editor::CMapInterfApi::Api::ToggleButtonCallback; + ButtonCallback callback{}; + callback.callback = (ButtonCallback::Callback)resMarketInterfOnCustomRatesToggleHandler; + + editor::CMapInterfApi::get().createToggleButtonFunctor(&functor, 0, + (editor::CMapInterf*)thisptr, + &callback); + CToggleButtonApi::get().assignFunctor(dialog, "TOG_CUSTOM_RATES", dialogName, &functor, 0); + freeFunctor(&functor, nullptr); + } + + CEditBoxInterf* editRates{dialogApi.findEditBox(dialog, "EDIT_EXC_RATES")}; + CEditBoxInterfApi::get().setFilterAndLength(dialog, "EDIT_EXC_RATES", dialogName, + EditFilter::NoFilter, 2048); + + if (market->customExchangeRates) { + CEditBoxInterfApi::get().setString(editRates, market->exchangeRatesScript.c_str()); + } + + CSpinButtonInterf* spin{dialogApi.findSpinButton(dialog, "SPIN_AI_PRIORITY")}; + setupAiPriority(spin, market->aiPriority.priority); + + const auto& createButtonFunctor{CTrainingCampInterfApi::get().createButtonFunctor}; + const auto assignFunctor{CButtonInterfApi::get().assignFunctor}; + + using ButtonCallback = CTrainingCampInterfApi::Api::ButtonCallback; + ButtonCallback callback{}; + + { + callback.callback = (ButtonCallback::Callback)resMarketInterfOkButtonHandler; + createButtonFunctor(&functor, 0, (CTrainingCampInterf*)thisptr, &callback); + assignFunctor(dialog, "BTN_OK", dialogName, &functor, 0); + freeFunctor(&functor, nullptr); + } + + { + callback.callback = (ButtonCallback::Callback)resMarketInterfImgUpButtonHandler; + createButtonFunctor(&functor, 0, (CTrainingCampInterf*)thisptr, &callback); + assignFunctor(dialog, "BTN_UP", dialogName, &functor, 0); + freeFunctor(&functor, nullptr); + } + + { + callback.callback = (ButtonCallback::Callback)resMarketInterfImgDownButtonHandler; + createButtonFunctor(&functor, 0, (CTrainingCampInterf*)thisptr, &callback); + assignFunctor(dialog, "BTN_DOWN", dialogName, &functor, 0); + freeFunctor(&functor, nullptr); + } + + { + callback.callback = (ButtonCallback::Callback)resMarketInterfLoadButtonHandler; + createButtonFunctor(&functor, 0, (CTrainingCampInterf*)thisptr, &callback); + assignFunctor(dialog, "BTN_LOAD", dialogName, &functor, 0); + freeFunctor(&functor, nullptr); + } + + CButtonInterf* loadButton{dialogApi.findButton(dialog, "BTN_LOAD")}; + loadButton->vftable->setEnabled(loadButton, market->customExchangeRates); +} + +game::CInterface* createResourceMarketInterface(game::editor::CTaskObjProp* task, + CMidSiteResourceMarket* market) +{ + using namespace game; + using namespace editor; + + auto* interf = (CResourceMarketInterf*)Memory::get().allocate(sizeof(CResourceMarketInterf)); + CDragAndDropInterfApi::get().constructor(interf, dialogName, task, 0); + + static bool firstTime{true}; + if (firstTime) { + firstTime = false; + // Remember base class d-tor for proper cleanup + dragDropDtor = interf->vftable->destructor; + + std::memcpy(&resMarketInterfVftable, interf->vftable, sizeof(CInterfaceVftable)); + } + + // Use our own destructor and vftable + resMarketInterfVftable.destructor = (CInterfaceVftable::Destructor)resMarketInterfDtor; + interf->vftable = &resMarketInterfVftable; + + new (&interf->customRates) std::string(); + + if (market->customExchangeRates) { + interf->customRates = market->exchangeRatesScript; + } + + auto& indices = interf->imageIndices; + + indices.bgn = nullptr; + indices.end = nullptr; + indices.allocatedMemEnd = nullptr; + indices.allocator = nullptr; + IntVectorApi::get().reserve(&indices, 1u); + + interf->taskObjProp = task; + interf->market = market; + interf->selectedIndex = 0; + + editorFunctions.getSiteImageIndices(&indices, &customSiteCategories().resourceMarket, 0); + + const int imgIso = market->imgIso; + int index = 0; + for (int* i = indices.bgn; i != indices.end; ++i, ++index) { + if (imgIso == *i) { + interf->selectedIndex = index; + break; + } + } + + const int imageIndex{indices.bgn[interf->selectedIndex]}; + IMqImage2* siteImage{ + editorFunctions.getSiteImage(&customSiteCategories().resourceMarket, imageIndex, false)}; + + CDialogInterf* dialog{interf->dragAndDropInterfData->dialogInterf}; + CPictureInterf* picture{CDialogInterfApi::get().findPicture(dialog, "IMG_SITE")}; + CPictureInterfApi::get().setImageWithAnchor(picture, siteImage, 9); + + setupUi(interf); + + return interf; +} + +} // namespace hooks diff --git a/mss32/src/resourcetype.cpp b/mss32/src/resourcetype.cpp new file mode 100644 index 00000000..3b4276c8 --- /dev/null +++ b/mss32/src/resourcetype.cpp @@ -0,0 +1,72 @@ +/* + * This file is part of the modding toolset for Disciples 2. + * (https://github.com/VladimirMakeev/D2ModdingToolset) + * Copyright (C) 2024 Vladimir Makeev. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "resourcetype.h" +#include "version.h" +#include + +namespace game::ResourceTypes { + +// clang-format off +static std::array categories = {{ + // Akella + Categories{ + (LResourceType*)0x83a208, + (LResourceType*)0x83a248, + (LResourceType*)0x83a228, + (LResourceType*)0x83a218, + (LResourceType*)0x83a258, + (LResourceType*)0x83a238, + }, + // Russobit + Categories{ + (LResourceType*)0x83a208, + (LResourceType*)0x83a248, + (LResourceType*)0x83a228, + (LResourceType*)0x83a218, + (LResourceType*)0x83a258, + (LResourceType*)0x83a238, + }, + // Gog + Categories{ + (LResourceType*)0x8381b8, + (LResourceType*)0x8381f8, + (LResourceType*)0x8381d8, + (LResourceType*)0x8381c8, + (LResourceType*)0x838208, + (LResourceType*)0x8381e8, + }, + // Scenario Editor + Categories{ + (LResourceType*)0x664f40, + (LResourceType*)0x664f80, + (LResourceType*)0x664f60, + (LResourceType*)0x664f50, + (LResourceType*)0x664f90, + (LResourceType*)0x664f70, + } +}}; +// clang-format on + +Categories& get() +{ + return categories[static_cast(hooks::gameVersion())]; +} + +} // namespace game::ResourceTypes diff --git a/mss32/src/scenedit.cpp b/mss32/src/scenedit.cpp index 3c8ef0e8..ef774a68 100644 --- a/mss32/src/scenedit.cpp +++ b/mss32/src/scenedit.cpp @@ -24,6 +24,7 @@ namespace game::CScenEditApi { // clang-format off Api functions{ (Api::Instance)0x4013af, + (Api::ReadScenData)0x402b0b, }; // clang-format on diff --git a/mss32/src/scenedithooks.cpp b/mss32/src/scenedithooks.cpp new file mode 100644 index 00000000..c23915d5 --- /dev/null +++ b/mss32/src/scenedithooks.cpp @@ -0,0 +1,93 @@ +/* + * This file is part of the modding toolset for Disciples 2. + * (https://github.com/VladimirMakeev/D2ModdingToolset) + * Copyright (C) 2024 Vladimir Makeev. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "scenedithooks.h" +#include "originalfunctions.h" +#include "dbf/dbffile.h" +#include "utils.h" +#include "game.h" +#include + +namespace hooks { + +static MarketNames marketNames; + +static bool readMarketNames(const std::filesystem::path& dbPath) +{ + utils::DbfFile db; + if (!db.open(dbPath)) { + return false; + } + + const std::uint32_t total{db.recordsTotal()}; + marketNames.reserve(total); + + const auto& oemToChar = *game::gameFunctions().oemToCharA; + + for (std::uint32_t i = 0u; i < total; ++i) { + utils::DbfRecord record; + if (!db.record(record, i)) { + return false; + } + + if (record.isDeleted()) { + continue; + } + + std::string name; + if (!record.value(name, "NAME")) { + return false; + } + + oemToChar(name.c_str(), name.data()); + + std::string description; + if (!record.value(description, "DESC")) { + return false; + } + + oemToChar(description.c_str(), description.data()); + + marketNames.emplace_back(NameDescPair{trimSpaces(name), trimSpaces(description)}); + } + + return true; +} + +bool __fastcall readScenDataHooked(game::CScenEdit* thisptr, int /*%edx*/) +{ + using namespace game; + + const auto dbPath{scenDataFolder() / "Marketname.dbf"}; + if (std::filesystem::exists(dbPath)) { + if (!readMarketNames(dbPath)) { + showErrorMessageBox( + fmt::format("Failed to read resource market names from '{:s}'", dbPath.string())); + } + } + + return getOriginalFunctions().readScenData(thisptr); +} + +const MarketNames& getMarketNames() +{ + return marketNames; +} + +} \ No newline at end of file diff --git a/mss32/src/scenpropinterf.cpp b/mss32/src/scenpropinterf.cpp new file mode 100644 index 00000000..9e1cb2d4 --- /dev/null +++ b/mss32/src/scenpropinterf.cpp @@ -0,0 +1,34 @@ +/* + * This file is part of the modding toolset for Disciples 2. + * (https://github.com/VladimirMakeev/D2ModdingToolset) + * Copyright (C) 2024 Vladimir Makeev. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "scenpropinterf.h" + +namespace game::editor::CScenPropInterfApi { + +Api& get() +{ + static Api api{ + (Api::Constructor)0x47396A, + (Api::CreateSpinButtonFunctor)0x4441b1, + }; + + return api; +} + +} // namespace game::editor::CScenPropInterfApi diff --git a/mss32/src/scenpropinterfhooks.cpp b/mss32/src/scenpropinterfhooks.cpp new file mode 100644 index 00000000..415a6dc6 --- /dev/null +++ b/mss32/src/scenpropinterfhooks.cpp @@ -0,0 +1,147 @@ +/* + * This file is part of the modding toolset for Disciples 2. + * (https://github.com/VladimirMakeev/D2ModdingToolset) + * Copyright (C) 2024 Vladimir Makeev. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "scenpropinterfhooks.h" +#include "aiattitudes.h" +#include "aiattitudestable.h" +#include "dialoginterf.h" +#include "gameutils.h" +#include "globaldata.h" +#include "midgardscenariomap.h" +#include "midplayer.h" +#include "originalfunctions.h" +#include "racetype.h" +#include "scenedit.h" +#include "scenpropinterf.h" +#include "smartptr.h" +#include "spinbuttoninterf.h" +#include "stringarray.h" +#include "utils.h" +#include "visitors.h" + +namespace hooks { + +static void __fastcall onSpinButtonSelectionChanged(game::editor::CScenPropInterf* thisptr, + int /*%edx*/, + game::CSpinButtonInterf* spinButton) +{ + using namespace game; + + const CScenEdit* scenEdit{CScenEditApi::get().instance()}; + auto* objectMap{scenEdit->data->unknown2->data->scenarioMap}; + + const auto* neutralPlayer{getNeutralPlayer(objectMap)}; + + const GlobalData* global{*GlobalDataApi::get().getGlobalData()}; + const auto* table{global->attitudesCategories}; + + const auto* tableRecord{table->bgn + spinButton->data->selectedOption}; + int id{static_cast(tableRecord->id)}; + + LAttitudesCategory attitude{}; + LAttitudesCategoryTableApi::get().findCategoryById(table, &attitude, &id); + + VisitorApi::get().playerSetAttitude(&neutralPlayer->id, &attitude, objectMap, 1); +} + +game::editor::CScenPropInterf* __fastcall scenPropInterfCtorHooked( + game::editor::CScenPropInterf* thisptr, + int /*%edx*/, + game::ITask* task, + char* a3) +{ + using namespace game; + + getOriginalFunctions().scenPropInterfCtor(thisptr, task, a3); + + static const char spinButtonName[]{"SPIN_ATTITUDE"}; + + const auto& dialogApi{CDialogInterfApi::get()}; + CDialogInterf* dialog{*thisptr->dialog}; + + if (!dialogApi.findControl(dialog, spinButtonName)) { + return thisptr; + } + + // Spinbox related to Neutrals attitude found, setup its options + CSpinButtonInterf* spinButton{dialogApi.findSpinButton(dialog, spinButtonName)}; + + const auto& globalApi{GlobalDataApi::get()}; + GlobalData* global{*globalApi.getGlobalData()}; + const auto* table{global->attitudesCategories}; + + const CScenEdit* scenEdit{CScenEditApi::get().instance()}; + const auto* objectMap{scenEdit->data->unknown2->data->scenarioMap}; + + const auto* neutralPlayer{getNeutralPlayer(objectMap)}; + const auto neutralsAttitude{neutralPlayer->attitude.id}; + + const auto& findCategory{LAttitudesCategoryTableApi::get().findCategoryById}; + LAttitudesCategory category{}; + + const auto& stringArrayApi{StringArrayApi::get()}; + StringArray attitudeNames{}; + stringArrayApi.reserve(&attitudeNames, 1u); + + std::size_t currentAttitudeIndex = 0u; + std::size_t index = 0u; + for (auto* record = table->bgn; record != table->end; ++record, ++index) { + int id{static_cast(record->id)}; + findCategory(table, &category, &id); + + const CAiAttitudes* attitudes{ + CAiAttitudesTableApi::get().find(global->aiAttitudes, &category)}; + + const auto& nameText{attitudes->data->name}; + const char* name{globalApi.findTextById(nameText.texts, &nameText.id)}; + + String attitudeName{}; + StringApi::get().initFromString(&attitudeName, name); + + stringArrayApi.pushBack(&attitudeNames, &attitudeName); + StringApi::get().free(&attitudeName); + + if (category.id == neutralsAttitude) { + currentAttitudeIndex = index; + } + } + + const auto& spinApi{CSpinButtonInterfApi::get()}; + spinApi.setOptions(spinButton, &attitudeNames); + spinApi.setSelectedOption(spinButton, currentAttitudeIndex); + + stringArrayApi.destructor(&attitudeNames); + + using SpinCallback = editor::CScenPropInterfApi::Api::SpinButtonCallback; + + SpinCallback callback{}; + callback.callback = (SpinCallback::Callback)onSpinButtonSelectionChanged; + + SmartPointer functor{}; + editor::CScenPropInterfApi::get().createSpinButtonFunctor(&functor, 0, thisptr, &callback); + + static const char dialogName[]{"DLG_PROP"}; + spinApi.assignFunctor(dialog, spinButtonName, dialogName, &functor); + + SmartPointerApi::get().createOrFreeNoDtor(&functor, nullptr); + + return thisptr; +} + +} // namespace hooks diff --git a/mss32/src/scripts.cpp b/mss32/src/scripts.cpp index f95f5db1..a006d07f 100644 --- a/mss32/src/scripts.cpp +++ b/mss32/src/scripts.cpp @@ -20,30 +20,42 @@ #include "scripts.h" #include "attackview.h" #include "battlemsgdata.h" -#include "battlemsgdataview.h" +#include "battlemsgdataviewmutable.h" +#include "buildingview.h" #include "categoryids.h" +#include "crystalview.h" #include "currencyview.h" #include "diplomacyview.h" #include "dynupgradeview.h" #include "fogview.h" #include "fortview.h" +#include "game.h" #include "gameutils.h" +#include "gameview.h" +#include "globalvariablesview.h" +#include "globalview.h" #include "groupview.h" #include "idview.h" #include "itembaseview.h" #include "itemview.h" #include "locationview.h" #include "log.h" +#include "merchantview.h" +#include "mercsview.h" #include "midstack.h" #include "modifierview.h" #include "playerview.h" #include "point.h" +#include "resourcemarketview.h" +#include "rodview.h" #include "ruinview.h" #include "scenariovariableview.h" #include "scenarioview.h" #include "scenvariablesview.h" +#include "siteview.h" #include "stackview.h" #include "tileview.h" +#include "trainerview.h" #include "unitimplview.h" #include "unitslotview.h" #include "unitview.h" @@ -273,6 +285,136 @@ static void bindApi(sol::state& lua) "Defend", BattleStatus::Defend, "Unsummoned", BattleStatus::Unsummoned ); + + lua.new_enum("BattleAction", + "Attack", BattleAction::Attack, + "Skip", BattleAction::Skip, + "Retreat", BattleAction::Retreat, + "Wait", BattleAction::Wait, + "Defend", BattleAction::Defend, + "Auto", BattleAction::Auto, + "UseItem", BattleAction::UseItem + // Restrict access to Resolve action from scripts + ); + + lua.new_enum("Retreat", + "NoRetreat", RetreatStatus::NoRetreat, + "CoverAndRetreat", RetreatStatus::CoverAndRetreat, + "FullRetreat", RetreatStatus::FullRetreat + ); + + lua.new_enum("IdType", + "Empty", IdType::Empty, + "ApplicationText", IdType::ApplicationText, + "Building", IdType::Building, + "Race", IdType::Race, + "Lord", IdType::Lord, + "Spell", IdType::Spell, + "UnitGlobal", IdType::UnitGlobal, + "UnitGenerated", IdType::UnitGenerated, + "UnitModifier", IdType::UnitModifier, + "Attack", IdType::Attack, + "TextGlobal", IdType::TextGlobal, + "LandmarkGlobal", IdType::LandmarkGlobal, + "ItemGlobal", IdType::ItemGlobal, + "NobleAction", IdType::NobleAction, + "DynamicUpgrade", IdType::DynamicUpgrade, + "DynamicAttack", IdType::DynamicAttack, + "DynamicAltAttack", IdType::DynamicAltAttack, + "DynamicAttack2", IdType::DynamicAttack2, + "DynamicAltAttack2", IdType::DynamicAltAttack2, + "CampaignFile", IdType::CampaignFile, + // Do not list id types that are not understood yet + //"CW", IdType::CW, + //"CO", IdType::CO, + "Plan", IdType::Plan, + "ObjectCount", IdType::ObjectCount, + "ScenarioFile", IdType::ScenarioFile, + "Map", IdType::Map, + "MapBlock", IdType::MapBlock, + "ScenarioInfo", IdType::ScenarioInfo, + "SpellEffects", IdType::SpellEffects, + "Fortification", IdType::Fortification, + "Player", IdType::Player, + "PlayerKnownSpells", IdType::PlayerKnownSpells, + "Fog", IdType::Fog, + "PlayerBuildings", IdType::PlayerBuildings, + "Road", IdType::Road, + "Stack", IdType::Stack, + "Unit", IdType::Unit, + "Landmark", IdType::Landmark, + "Item", IdType::Item, + "Bag", IdType::Bag, + "Site", IdType::Site, + "Ruin", IdType::Ruin, + "Tomb", IdType::Tomb, + "Rod", IdType::Rod, + "Crystal", IdType::Crystal, + "Diplomacy", IdType::Diplomacy, + "SpellCast", IdType::SpellCast, + "Location", IdType::Location, + "StackTemplate", IdType::StackTemplate, + "Event", IdType::Event, + "StackDestroyed", IdType::StackDestroyed, + "TalismanCharges", IdType::TalismanCharges, + //"MT", IdType::MT, + "Mountains", IdType::Mountains, + "SubRace", IdType::SubRace, + "SubRaceType", IdType::SubRaceType, + "QuestLog", IdType::QuestLog, + "TurnSummary", IdType::TurnSummary, + "ScenarioVariable", IdType::ScenarioVariable + ); + + lua.new_enum("Order", + "Normal", OrderId::Normal, + "Stand", OrderId::Stand, + "Guard", OrderId::Guard, + "AttackStack", OrderId::AttackStack, + "DefendStack", OrderId::DefendStack, + "SecureCity", OrderId::SecureCity, + "Roam", OrderId::Roam, + "MoveToLocation", OrderId::MoveToLocation, + "DefendLocation", OrderId::DefendLocation, + "Bezerk", OrderId::Bezerk, + "Assist", OrderId::Assist, + "Steal", OrderId::Steal, + "DefendCity", OrderId::DefendCity + ); + + lua.new_enum("Resource", + "Gold", ResourceId::Gold, + "InfernalMana", ResourceId::InfernalMana, + "LifeMana", ResourceId::LifeMana, + "DeathMana", ResourceId::DeathMana, + "RunicMana", ResourceId::RunicMana, + "GroveMana", ResourceId::GroveMana + ); + + lua.new_enum("Difficulty", + "Easy", DifficultyLevelId::Easy, + "Average", DifficultyLevelId::Average, + "Hard", DifficultyLevelId::Hard, + "VeryHard", DifficultyLevelId::VeryHard + ); + + lua.new_enum("Building", + "Guild", BuildingId::Guild, + "Heal", BuildingId::Heal, + "Magic", BuildingId::Magic, + "Unit", BuildingId::Unit + ); + + lua.new_enum("UnitBranch", + "Fighter", UnitBranchId::Fighter, + "Archer", UnitBranchId::Archer, + "Mage", UnitBranchId::Mage, + "Special", UnitBranchId::Special, + "Sideshow", UnitBranchId::Sideshow, + "Hero", UnitBranchId::Hero, + "Noble", UnitBranchId::Noble, + "Summon", UnitBranchId::Summon + ); // clang-format on bindings::UnitView::bind(lua); @@ -297,11 +439,29 @@ static void bindApi(sol::state& lua) bindings::ItemView::bind(lua); bindings::PlayerView::bind(lua); bindings::ModifierView::bind(lua); + bindings::BattleTurnView::bind(lua); bindings::BattleMsgDataView::bind(lua); + bindings::BattleMsgDataViewMutable::bind(lua); bindings::DiplomacyView::bind(lua); bindings::FogView::bind(lua); + bindings::RodView::bind(lua); + bindings::CrystalView::bind(lua); + bindings::SiteView::bind(lua); + bindings::MerchantItemView::bind(lua); + bindings::MerchantView::bind(lua); + bindings::MercenaryUnitView::bind(lua); + bindings::MercsView::bind(lua); + bindings::TrainerView::bind(lua); + bindings::ResourceMarketView::bind(lua); + bindings::GlobalVariablesView::bind(lua); + bindings::GlobalView::bind(lua); + bindings::GameView::bind(lua); + bindings::BuildingView::bind(lua); lua.set_function("log", [](const std::string& message) { logDebug("luaDebug.log", message); }); + lua.set_function("randomNumber", [](std::uint32_t maxValue) { + return game::gameFunctions().generateRandomNumber(maxValue); + }); } // https://sol2.readthedocs.io/en/latest/threading.html @@ -316,7 +476,7 @@ sol::state& getLua() if (lua == nullptr) { lua = std::make_unique(); lua->open_libraries(sol::lib::base, sol::lib::package, sol::lib::math, sol::lib::table, - sol::lib::os, sol::lib::string); + sol::lib::os, sol::lib::string, sol::lib::debug); bindApi(*lua); } @@ -344,6 +504,16 @@ bindings::ScenarioView getScenario() return {getObjectMap()}; } +bindings::GlobalView getGlobal() +{ + return bindings::GlobalView(); +} + +bindings::GameView getGame() +{ + return bindings::GameView(); +} + sol::environment executeScript(const std::string& source, sol::protected_function_result& result, bool bindScenario) @@ -356,6 +526,9 @@ sol::environment executeScript(const std::string& source, result = lua.safe_script(source, env, [](lua_State*, sol::protected_function_result pfr) { return pfr; }); + env["getGlobal"] = &getGlobal; + env["getGame"] = &getGame; + if (bindScenario) { env["getScenario"] = &getScenario; } diff --git a/mss32/src/settings.cpp b/mss32/src/settings.cpp index 421fb06b..2951c4c6 100644 --- a/mss32/src/settings.cpp +++ b/mss32/src/settings.cpp @@ -18,6 +18,7 @@ */ #include "settings.h" +#include "battlemsgdata.h" #include "log.h" #include "scripts.h" #include "utils.h" @@ -279,9 +280,13 @@ static void readBattleSettings(const sol::table& table, Settings::Battle& value) def.carryXpOverUpgrade); value.allowMultiUpgrade = readSetting(category.value(), "allowMultiUpgrade", def.allowMultiUpgrade); + value.debugAi = readSetting(category.value(), "debugAi", def.debugAi); + value.fallbackAction = readSetting(category.value(), "fallbackAction", def.fallbackAction, + game::BattleAction::Attack, game::BattleAction::UseItem); } -static void readAdditionalLordIncomeSettings(const sol::table& table, Settings::AdditionalLordIncome& value) +static void readAdditionalLordIncomeSettings(const sol::table& table, + Settings::AdditionalLordIncome& value) { const auto& def = defaultSettings().additionalLordIncome; @@ -414,6 +419,7 @@ const Settings& baseSettings() settings.movementCost.plain.onRoad = 1; settings.movementCost.textColor = Color{200, 200, 200}; settings.movementCost.show = false; + settings.battle.fallbackAction = game::BattleAction::Defend; settings.debugMode = false; initialized = true; diff --git a/mss32/src/sitecategoryhooks.cpp b/mss32/src/sitecategoryhooks.cpp new file mode 100644 index 00000000..e29ef08b --- /dev/null +++ b/mss32/src/sitecategoryhooks.cpp @@ -0,0 +1,114 @@ +/* + * This file is part of the modding toolset for Disciples 2. + * (https://github.com/VladimirMakeev/D2ModdingToolset) + * Copyright (C) 2024 Vladimir Makeev. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "sitecategoryhooks.h" +#include "dbf/dbffile.h" +#include "log.h" +#include "midsiteresourcemarket.h" +#include "utils.h" +#include + +namespace hooks { + +static const char* resourceMarketSiteCategoryName{"L_RESOURCE_MARKET"}; + +CustomSiteCategories& customSiteCategories() +{ + static CustomSiteCategories customSites{}; + + return customSites; +} + +static bool readCustomSites(const std::filesystem::path& dbfFilePath) +{ + utils::DbfFile dbf; + if (!dbf.open(dbfFilePath)) { + logError("mssProxyError.log", + fmt::format("Could not open {:s}", dbfFilePath.filename().string())); + return false; + } + + const std::uint32_t recordsTotal{dbf.recordsTotal()}; + for (std::uint32_t i = 0u; i < recordsTotal; ++i) { + utils::DbfRecord record; + if (!dbf.record(record, i)) { + logError("mssProxyError.log", fmt::format("Could not read record {:d} from {:s}", i, + dbfFilePath.filename().string())); + return false; + } + + if (record.isDeleted()) { + continue; + } + + std::string siteName; + record.value(siteName, "TEXT"); + siteName = trimSpaces(siteName); + + if (siteName == resourceMarketSiteCategoryName) { + std::string scriptPath; + record.value(scriptPath, "SCRIPT"); + customSiteCategories().exchangeRatesScript = trimSpaces(scriptPath); + return true; + } + } + + return false; +} + +game::LSiteCategoryTable* __fastcall siteCategoryTableCtorHooked(game::LSiteCategoryTable* thisptr, + int /*%edx*/, + const char* globalsFolderPath, + void* codeBaseEnvProxy) +{ + using namespace game; + + static const char dbfFileName[] = "LSite.dbf"; + const auto dbfFilePath{std::filesystem::path(globalsFolderPath) / dbfFileName}; + + const bool customSitesExist{readCustomSites(dbfFilePath)}; + customSiteCategories().exists = customSitesExist; + + thisptr->bgn = nullptr; + thisptr->end = nullptr; + thisptr->allocatedMemEnd = nullptr; + thisptr->allocator = nullptr; + thisptr->vftable = LSiteCategoryTableApi::vftable(); + + const auto& table = LSiteCategoryTableApi::get(); + const auto& sites = SiteCategories::get(); + + table.init(thisptr, codeBaseEnvProxy, globalsFolderPath, dbfFileName); + table.readCategory(sites.merchant, thisptr, "L_MERCHANT", dbfFileName); + table.readCategory(sites.mageTower, thisptr, "L_MAGE_TOWER", dbfFileName); + table.readCategory(sites.mercenaries, thisptr, "L_MERCENARIES", dbfFileName); + table.readCategory(sites.trainer, thisptr, "L_TRAINER", dbfFileName); + + if (customSitesExist) { + table.readCategory(&customSiteCategories().resourceMarket, thisptr, + resourceMarketSiteCategoryName, dbfFileName); + // Register CStreamRegister for our new site + addResourceMarketStreamRegister(); + } + + table.initDone(thisptr); + return thisptr; +} + +} // namespace hooks diff --git a/mss32/src/siteresourcemarketinterf.cpp b/mss32/src/siteresourcemarketinterf.cpp new file mode 100644 index 00000000..62625aee --- /dev/null +++ b/mss32/src/siteresourcemarketinterf.cpp @@ -0,0 +1,1224 @@ +/* + * This file is part of the modding toolset for Disciples 2. + * (https://github.com/VladimirMakeev/D2ModdingToolset) + * Copyright (C) 2024 Vladimir Makeev. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "siteresourcemarketinterf.h" +#include "autodialog.h" +#include "button.h" +#include "ddstackgroup.h" +#include "ddstackinventorydisplay.h" +#include "dialoginterf.h" +#include "dynamiccast.h" +#include "encparamidplayer.h" +#include "exchangeresourcesmsg.h" +#include "game.h" +#include "gameutils.h" +#include "image2outline.h" +#include "image2text.h" +#include "listbox.h" +#include "log.h" +#include "mainview2.h" +#include "mempool.h" +#include "menubase.h" +#include "middatacache.h" +#include "middragdropinterf.h" +#include "midgard.h" +#include "midplayer.h" +#include "midsiteresourcemarket.h" +#include "midstack.h" +#include "midunit.h" +#include "midunitgroupadapter.h" +#include "multilayerimg.h" +#include "paperdollchildinterf.h" +#include "phasegame.h" +#include "pictureinterf.h" +#include "radiobuttoninterf.h" +#include "resetstackext.h" +#include "scripts.h" +#include "sitecategoryhooks.h" +#include "sounds.h" +#include "stackview.h" +#include "task.h" +#include "taskmanager.h" +#include "textboxinterf.h" +#include "textids.h" +#include "togglebutton.h" +#include "unitutils.h" +#include "usunitimpl.h" +#include "utils.h" +#include +#include +#include +#define WIN32_LEAN_AND_MEAN +#include + +namespace hooks { + +static const char dialogName[]{"DLG_RESOURCE_MARKET"}; + +struct CSiteResourceMarketInterf + : public game::CMidDataCache2::INotify + , public game::IResetStackExt + , public game::CMidDragDropInterf +{ + MarketExchangeRates exchangeRates; + game::CMqRect paperdollArea; + game::CMqRect groupArea; + game::CMidgardID visitorStackId; + game::CMidgardID marketId; + int exchangeStep; + game::CInterface* paperdoll; + game::CDDStackGroup* stackGroup; + game::IMqImage2* groupBackground; + game::CDDStackInventoryDisplay* inventory; +}; + +static CSiteResourceMarketInterf* resetStackExtToSiteInterf(game::IResetStackExt* resetStack) +{ + using namespace game; + + constexpr auto offset{offsetof(CSiteResourceMarketInterf, IResetStackExt::vftable)}; + + auto ptr{reinterpret_cast(resetStack) - offset}; + return reinterpret_cast(ptr); +} + +static CSiteResourceMarketInterf* dragDropToSiteInterf(game::CMidDragDropInterf* dragDrop) +{ + using namespace game; + + constexpr auto offset{offsetof(CSiteResourceMarketInterf, CMidDragDropInterf::vftable)}; + + auto ptr{reinterpret_cast(dragDrop) - offset}; + return reinterpret_cast(ptr); +} + +// Do the same way as the game: d-tor is implemented in IResetStackExt vftable +// CMidDragDropInterf d-tor adjusts this pointer and calls it +static void __fastcall siteResMarketInterfResetStackExtDtor(game::IResetStackExt* thisptr, + int /*%edx*/, + char flags) +{ + using namespace game; + + CSiteResourceMarketInterf* interf{resetStackExtToSiteInterf(thisptr)}; + + if (interf->paperdoll) { + CInterface* marketInterface = interf; + int index = -1; + marketInterface->vftable->getChildIndex(marketInterface, &index, interf->paperdoll); + marketInterface->vftable->deleteChildAt(marketInterface, &index); + } + + CMidDataCache2* dataCache{CPhaseApi::get().getDataCache(&interf->phaseGame->phase)}; + CMidDataCache2Api::get().removeNotify(dataCache, interf); + + if (interf->inventory) { + interf->inventory->IMidDropSource::vftable->destructor(interf->inventory, 1); + } + + if (interf->groupBackground) { + interf->groupBackground->vftable->destructor(interf->groupBackground, 1); + } + + if (interf->stackGroup) { + interf->stackGroup->IMidDropSource::vftable->destructor(interf->stackGroup, 1); + } + + interf->exchangeRates.~vector(); + + CMidDragDropInterfApi::get().destructor(interf); + + if (flags & 1) { + Memory::get().freeNonZero(interf); + } +} + +static bool __fastcall siteResMarketResetStackExtHireLeader(game::IResetStackExt*, + int /*%edx*/, + const game::CMidgardID*, + int, + int, + int) +{ + return true; +} + +static bool __fastcall siteResMarketResetStackExtMethod2(game::IResetStackExt*, int /*%edx*/) +{ + return true; +} + +static bool __fastcall siteResMarketResetStackExtMethod3(game::IResetStackExt*, int /*%edx*/) +{ + return false; +} + +static void __fastcall siteResMarketResetStackExtGetUnitIdsForHire(game::IResetStackExt*, + int /*%edx*/, + game::IdVector*, + int) +{ + return; +} + +static game::CMidgardID* __fastcall siteResMarketResetStackExtGetStackId( + game::IResetStackExt* thisptr, + int /*%edx*/, + game::CMidgardID* value) +{ + auto interf{resetStackExtToSiteInterf(thisptr)}; + + *value = interf->visitorStackId; + return value; +} + +static game::IResetStackExtVftable siteResMarketStackExtVftable{ + (game::IResetStackExtVftable::Destructor)siteResMarketInterfResetStackExtDtor, + (game::IResetStackExtVftable::HireLeader)siteResMarketResetStackExtHireLeader, + (void*)siteResMarketResetStackExtMethod2, + (void*)siteResMarketResetStackExtMethod3, + (game::IResetStackExtVftable::GetUnitIdsForHire)siteResMarketResetStackExtGetUnitIdsForHire, + (game::IResetStackExtVftable::GetStackId)siteResMarketResetStackExtGetStackId}; + +static void __fastcall siteResMarketInterfDragDropDtor(game::CMidDragDropInterf* thisptr, + int /*%edx*/, + char flags) +{ + using namespace game; + + constexpr auto dragDropOffset{offsetof(CSiteResourceMarketInterf, CMidDragDropInterf::vftable)}; + + auto ptr{reinterpret_cast(thisptr) - dragDropOffset}; + auto interf{reinterpret_cast(ptr)}; + + siteResMarketInterfResetStackExtDtor(interf, 0, flags); +} + +static void __fastcall siteResMarketInterfBtnCloseHandler(CSiteResourceMarketInterf* thisptr, + int /*%edx*/) +{ + using namespace game; + + auto currentTask{thisptr->dragAndDropInterfData->task}; + auto taskManager{currentTask->vftable->getTaskManager(currentTask)}; + + CTaskManagerApi::get().setCurrentTask(taskManager, nullptr); +} + +static const char* getCurrencyImage(game::CurrencyType currency) +{ + using namespace game; + + switch (currency) { + default: + case CurrencyType::Gold: + return "DLG_BEGIN_TURN_GOLD"; + case CurrencyType::InfernalMana: + return "DLG_BEGIN_TURN_REDM_P"; + case CurrencyType::LifeMana: + return "DLG_BEGIN_TURN_BLUEM_P"; + case CurrencyType::DeathMana: + return "DLG_BEGIN_TURN_BLACKM_P"; + case CurrencyType::RunicMana: + return "DLG_BEGIN_TURN_WHITEM_P"; + case CurrencyType::GroveMana: + return "_GREEN_MANA__GREEN_MANA"; + } +} + +static game::CMultiLayerImg* createResourceToggleImage(const game::CMqPoint& size, + game::CurrencyType currency, + const char* amount, + bool selected = false) +{ + using namespace game; + + const auto& allocateMem{Memory::get().allocate}; + + auto text{(CImage2Text*)allocateMem(sizeof(CImage2Text))}; + CImage2TextApi::get().constructor(text, size.x, size.y); + CImage2TextApi::get().setText(text, amount); + + const auto& multilayerApi{CMultiLayerImgApi::get()}; + + auto multilayerImage{(CMultiLayerImg*)allocateMem(sizeof(CMultiLayerImg))}; + multilayerApi.constructor(multilayerImage); + + const Color color{0x00b2d9f5u}; + auto outline{(CImage2Outline*)allocateMem(sizeof(CImage2Outline))}; + // We always use outline to get consistent images whether button is selected or not. + // When button is not selected, outline is completely transparent + CImage2OutlineApi::get().constructor(outline, &size, &color, selected ? 0xff : 0); + + constexpr int offset{10}; + + multilayerApi.addImage(multilayerImage, outline, -999, -999); + + IMqImage2* currencyImage{AutoDialogApi::get().loadImage(getCurrencyImage(currency))}; + multilayerApi.addImage(multilayerImage, currencyImage, offset, -999); + + multilayerApi.addImage(multilayerImage, text, -10, -999); + + return multilayerImage; +} + +static std::string getResourceAmountString(std::uint16_t amount, bool infinite) +{ + if (infinite) { + return std::string{ + "\\hR;\\vC;" + + getInterfaceText(textIds().resourceMarket.infiniteAmount.c_str(), "Inf.")}; + } + + return fmt::format("\\hR;\\vC;{:d}", amount); +} + +static void updateExchangeRatesUi(CSiteResourceMarketInterf* interf) +{ + using namespace game; + + const auto& dialogApi{CDialogInterfApi::get()}; + CDialogInterf* dialog{interf->dragAndDropInterfData->dialogInterf}; + + const auto& radio{CRadioButtonInterfApi::get()}; + + CRadioButtonInterf* radioP{dialogApi.findRadioButton(dialog, "RAD_RESOURCE_P")}; + if (radioP->data->selectedButton < 0) { + return; + } + + const CurrencyType selectedResource{(CurrencyType)radioP->data->selectedButton}; + + const auto& exchangeRates{interf->exchangeRates}; + + const auto it{std::find_if(exchangeRates.cbegin(), exchangeRates.cend(), + [selectedResource](const ResourceExchange& exchange) { + return (int)exchange.resource1 == (int)selectedResource; + })}; + + // clang-format off + const std::array rates{ + dialogApi.findTextBox(dialog, "TXT_RATE_GOLD"), + dialogApi.findTextBox(dialog, "TXT_RATE_INFERNAL"), + dialogApi.findTextBox(dialog, "TXT_RATE_LIFE"), + dialogApi.findTextBox(dialog, "TXT_RATE_DEATH"), + dialogApi.findTextBox(dialog, "TXT_RATE_RUNIC"), + dialogApi.findTextBox(dialog, "TXT_RATE_GROVE") + }; + // clang-format on + + bool exchangePossible[6]{}; + + if (it != exchangeRates.cend()) { + // Player can exchange selected resource to something from the market + char text[32]; + for (const auto& rate : it->rates) { + const int index{(int)rate.resource2}; + exchangePossible[index] = true; + + const auto result{ + fmt::format_to_n(text, sizeof(text) - 1u, "{:d}/{:d}", rate.amount1, rate.amount2)}; + text[result.size] = 0; + + CTextBoxInterf* textBox{rates[index]}; + CTextBoxInterfApi::get().setString(textBox, text); + } + } + + const auto notAvailableText{ + getInterfaceText(textIds().resourceMarket.exchangeNotAvailable.c_str(), "N/A")}; + // Mark unavailable exchanges + for (std::size_t i = 0u; i < std::size(exchangePossible); ++i) { + if (exchangePossible[i]) { + continue; + } + + CTextBoxInterf* textBox{rates[i]}; + CTextBoxInterfApi::get().setString(textBox, notAvailableText.c_str()); + } +} + +static game::CurrencyType getPlayerCurrencyType(CSiteResourceMarketInterf* thisptr) +{ + using namespace game; + + const auto& dialogApi{CDialogInterfApi::get()}; + CDialogInterf* dialog{thisptr->dragAndDropInterfData->dialogInterf}; + + const auto* button{dialogApi.findRadioButton(dialog, "RAD_RESOURCE_P")}; + return static_cast(button->data->selectedButton); +} + +static game::CurrencyType getMarketCurrencyType(CSiteResourceMarketInterf* thisptr) +{ + using namespace game; + + const auto& dialogApi{CDialogInterfApi::get()}; + CDialogInterf* dialog{thisptr->dragAndDropInterfData->dialogInterf}; + + const auto* button{dialogApi.findRadioButton(dialog, "RAD_RESOURCE_M")}; + return static_cast(button->data->selectedButton); +} + +static bool isExchangePossible(CSiteResourceMarketInterf* thisptr, int exchangeStep) +{ + using namespace game; + + const auto playerCurrency{getPlayerCurrencyType(thisptr)}; + const auto marketCurrency{getMarketCurrencyType(thisptr)}; + + auto rates{findExchangeRates(thisptr->exchangeRates, playerCurrency, marketCurrency)}; + if (!rates) { + return false; + } + + // Check if player can exchange + const std::uint16_t playerExchangeAmount = exchangeStep * rates->amount1; + + const CMidDataCache2* dataCache{CPhaseApi::get().getDataCache(&thisptr->phaseGame->phase)}; + const auto* stack{getStack(dataCache, &thisptr->visitorStackId)}; + const auto* player{getPlayer(dataCache, &stack->ownerId)}; + + const auto& bankApi{BankApi::get()}; + const auto playerResource{bankApi.get(&player->bank, playerCurrency)}; + if (playerResource < playerExchangeAmount) { + return false; + } + + auto site{dataCache->vftable->findScenarioObjectById(dataCache, &thisptr->marketId)}; + auto market{static_cast(site)}; + + if (isMarketStockInfinite(market->infiniteStock, marketCurrency)) { + return true; + } + + // Check if market can exchange + const std::uint16_t marketExchangeAmount = exchangeStep * rates->amount2; + + const auto marketResource{bankApi.get(&market->stock, marketCurrency)}; + return marketResource >= marketExchangeAmount; +} + +static void updateMarketExchangeAmount(CSiteResourceMarketInterf* interf, + game::CurrencyType currency) +{ + using namespace game; + + const CMidDataCache2* dataCache{CPhaseApi::get().getDataCache(&interf->phaseGame->phase)}; + auto obj{dataCache->vftable->findScenarioObjectById(dataCache, &interf->marketId)}; + auto market{static_cast(obj)}; + + const CurrencyType playerCurrency{getPlayerCurrencyType(interf)}; + auto rates{findExchangeRates(interf->exchangeRates, playerCurrency, currency)}; + + std::uint16_t amount = 0u; + if (rates) { + amount = interf->exchangeStep * rates->amount2; + } + + const auto string{getResourceAmountString(amount, false)}; + + const auto& dialogApi{CDialogInterfApi::get()}; + CDialogInterf* dialog{interf->dragAndDropInterfData->dialogInterf}; + + CRadioButtonInterf* radio{dialogApi.findRadioButton(dialog, "RAD_RESOURCE_M")}; + const auto& buttons{radio->data->buttons}; + + const CToggleButton* button{buttons.bgn[0].first}; + const CMqRect* area{button->vftable->getArea((CInterface*)button)}; + const CMqPoint size{area->right - area->left, area->bottom - area->top}; + + auto image{createResourceToggleImage(size, currency, string.c_str())}; + + CPictureInterf* picture{dialogApi.findPicture(dialog, "IMG_RESOURCE_M")}; + CPictureInterfApi::get().setImageWithAnchor(picture, image, 9); +} + +static void updatePlayerExchangeAmount(CSiteResourceMarketInterf* interf, + game::CurrencyType currency) +{ + using namespace game; + + const CMidDataCache2* dataCache{CPhaseApi::get().getDataCache(&interf->phaseGame->phase)}; + + const CMidStack* visitorStack{getStack(dataCache, &interf->visitorStackId)}; + const CMidPlayer* visitorPlayer{getPlayer(dataCache, &visitorStack->ownerId)}; + + std::uint16_t amount = 0u; + + const CurrencyType marketCurrency{getMarketCurrencyType(interf)}; + auto rates{findExchangeRates(interf->exchangeRates, currency, marketCurrency)}; + if (rates) { + amount = interf->exchangeStep * rates->amount1; + } + + const auto string{getResourceAmountString(amount, false)}; + + const auto& dialogApi{CDialogInterfApi::get()}; + CDialogInterf* dialog{interf->dragAndDropInterfData->dialogInterf}; + + CRadioButtonInterf* radio{dialogApi.findRadioButton(dialog, "RAD_RESOURCE_P")}; + const auto& buttons{radio->data->buttons}; + + const CToggleButton* button{buttons.bgn[0].first}; + const CMqRect* area{button->vftable->getArea((CInterface*)button)}; + const CMqPoint size{area->right - area->left, area->bottom - area->top}; + + auto image{createResourceToggleImage(size, currency, string.c_str())}; + + CPictureInterf* picture{dialogApi.findPicture(dialog, "IMG_RESOURCE_P")}; + CPictureInterfApi::get().setImageWithAnchor(picture, image, 9); +} + +static void updateExchangeButtons(CSiteResourceMarketInterf* thisptr) +{ + using namespace game; + + updateMarketExchangeAmount(thisptr, getMarketCurrencyType(thisptr)); + updatePlayerExchangeAmount(thisptr, getPlayerCurrencyType(thisptr)); + + const auto& dialogApi{CDialogInterfApi::get()}; + CDialogInterf* dialog{thisptr->dragAndDropInterfData->dialogInterf}; + + auto* stepDownButton{dialogApi.findButton(dialog, "BTN_EXCHANGE_DOWN")}; + // Allow to decrease exchange step until 1. + stepDownButton->vftable->setEnabled(stepDownButton, thisptr->exchangeStep > 1); + + const bool currentPossible{isExchangePossible(thisptr, thisptr->exchangeStep)}; + + auto* exchangeButton{dialogApi.findButton(dialog, "BTN_EXCHANGE")}; + // Update exchange button depending on exchange possibility at current step + exchangeButton->vftable->setEnabled(exchangeButton, currentPossible); + + if (!currentPossible) { + // Disable up button if exchange at current step is not possible + auto* stepUpButton{dialogApi.findButton(dialog, "BTN_EXCHANGE_UP")}; + stepUpButton->vftable->setEnabled(stepUpButton, false); + return; + } + + // Disable up button if it is not possible to perform exchange at the next exchange step + const bool nextPossible{isExchangePossible(thisptr, thisptr->exchangeStep + 1)}; + + auto* stepUpButton{dialogApi.findButton(dialog, "BTN_EXCHANGE_UP")}; + stepUpButton->vftable->setEnabled(stepUpButton, nextPossible); +} + +static std::string getResourceName(game::CurrencyType resource) +{ + using namespace game; + + const char* textId{}; + switch (resource) { + case CurrencyType::Gold: + textId = "X100TA0082"; + break; + case CurrencyType::LifeMana: + textId = "X100TA0098"; + break; + case CurrencyType::DeathMana: + textId = "X100TA0096"; + break; + case CurrencyType::RunicMana: + textId = "X100TA0097"; + break; + case CurrencyType::InfernalMana: + textId = "X100TA0099"; + break; + case CurrencyType::GroveMana: + textId = "X160TA0038"; + break; + } + + return getInterfaceText(textId); +} + +static void updateExchangeDescription(CSiteResourceMarketInterf* interf) +{ + using namespace game; + + std::string desc{getInterfaceText(textIds().resourceMarket.exchangeDesc.c_str(), + "You offer %RES1% to get %RES2% in return.")}; + + const auto playerCurrency{getPlayerCurrencyType(interf)}; + const auto marketCurrency{getMarketCurrencyType(interf)}; + + replace(desc, "%RES1%", getResourceName(playerCurrency)); + replace(desc, "%RES2%", getResourceName(marketCurrency)); + + CDialogInterf* dialog{interf->dragAndDropInterfData->dialogInterf}; + CTextBoxInterf* exchangeDesc{CDialogInterfApi::get().findTextBox(dialog, "TXT_EXCHANGE_DESC")}; + + CTextBoxInterfApi::get().setString(exchangeDesc, desc.c_str()); +} + +static void __fastcall onMarketResourceTogglePressed(CSiteResourceMarketInterf* interf, + int /*%edx*/, + int index) +{ + using namespace game; + + // Resource selection changed, reset exchange step + interf->exchangeStep = 1; + + updateExchangeRatesUi(interf); + updateExchangeButtons(interf); + updateExchangeDescription(interf); +} + +static void __fastcall onPlayerResourceTogglePressed(CSiteResourceMarketInterf* interf, + int /*%edx*/, + int index) +{ + using namespace game; + + // Resource selection changed, reset exchange step + interf->exchangeStep = 1; + + updateExchangeRatesUi(interf); + updateExchangeButtons(interf); + updateExchangeDescription(interf); +} + +static void __fastcall exchangeButtonHandler(CSiteResourceMarketInterf* thisptr, int /*%edx*/) +{ + using namespace game; + + const auto playerCurrency{getPlayerCurrencyType(thisptr)}; + const auto marketCurrency{getMarketCurrencyType(thisptr)}; + + sendExchangeResourcesMsg(thisptr->phaseGame, thisptr->marketId, thisptr->visitorStackId, + playerCurrency, marketCurrency, thisptr->exchangeStep); + + // Clear exchange step + thisptr->exchangeStep = 1; + updateExchangeButtons(thisptr); + + playSoundEffect(SoundEffect::Buyitem); +} + +static void __fastcall exchangeStepDownButtonHandler(CSiteResourceMarketInterf* thisptr, + int /*%edx*/) +{ + if (thisptr->exchangeStep == 1) { + return; + } + + --thisptr->exchangeStep; + updateExchangeButtons(thisptr); +} + +static void __fastcall exchangeStepUpButtonHandler(CSiteResourceMarketInterf* thisptr, int /*%edx*/) +{ + ++thisptr->exchangeStep; + updateExchangeButtons(thisptr); +} + +static void setupResourceToggleButton(game::CDialogInterf* dialog, + const char* buttonName, + game::CurrencyType currency, + std::uint16_t amount, + bool infinite = false) +{ + using namespace game; + + const auto string{getResourceAmountString(amount, infinite)}; + const char* ptr{string.c_str()}; + + CToggleButton* button{CDialogInterfApi::get().findToggleButton(dialog, buttonName)}; + const CMqRect* area{button->vftable->getArea((CInterface*)button)}; + const CMqPoint size{area->right - area->left, area->bottom - area->top}; + + auto normal{createResourceToggleImage(size, currency, ptr)}; + button->vftable->setImage(button, normal, ToggleButtonState::Normal); + + auto normalChecked{createResourceToggleImage(size, currency, ptr, true)}; + button->vftable->setImage(button, normalChecked, ToggleButtonState::NormalChecked); + + auto hovered{createResourceToggleImage(size, currency, ptr, true)}; + button->vftable->setImage(button, hovered, ToggleButtonState::Hovered); + + auto hoveredChecked{createResourceToggleImage(size, currency, ptr, true)}; + button->vftable->setImage(button, hoveredChecked, ToggleButtonState::HoveredChecked); + + auto clicked{createResourceToggleImage(size, currency, ptr, true)}; + button->vftable->setImage(button, clicked, ToggleButtonState::Clicked); + + auto clickedChecked{createResourceToggleImage(size, currency, ptr, true)}; + button->vftable->setImage(button, clickedChecked, ToggleButtonState::ClickedChecked); +} + +static void updatePlayerResources(CSiteResourceMarketInterf* interf) +{ + using namespace game; + + CMidDataCache2* dataCache{CPhaseApi::get().getDataCache(&interf->phaseGame->phase)}; + + const CMidStack* visitorStack{getStack(dataCache, &interf->visitorStackId)}; + const CMidPlayer* visitorPlayer{getPlayer(dataCache, &visitorStack->ownerId)}; + const Bank& playerBank{visitorPlayer->bank}; + + CDialogInterf* dialog{interf->dragAndDropInterfData->dialogInterf}; + setupResourceToggleButton(dialog, "TOG_GOLD_P", CurrencyType::Gold, playerBank.gold); + setupResourceToggleButton(dialog, "TOG_LIFE_P", CurrencyType::LifeMana, playerBank.lifeMana); + setupResourceToggleButton(dialog, "TOG_DEATH_P", CurrencyType::DeathMana, playerBank.deathMana); + setupResourceToggleButton(dialog, "TOG_RUNIC_P", CurrencyType::RunicMana, playerBank.runicMana); + setupResourceToggleButton(dialog, "TOG_INFERNAL_P", CurrencyType::InfernalMana, + playerBank.infernalMana); + setupResourceToggleButton(dialog, "TOG_GROVE_P", CurrencyType::GroveMana, playerBank.groveMana); +} + +static void updateMarketResources(CSiteResourceMarketInterf* interf) +{ + using namespace game; + + CMidDataCache2* dataCache{CPhaseApi::get().getDataCache(&interf->phaseGame->phase)}; + + auto obj{dataCache->vftable->findScenarioObjectById(dataCache, &interf->marketId)}; + auto market{(const CMidSiteResourceMarket*)obj}; + + const Bank& stock{market->stock}; + const auto& inf{market->infiniteStock.parts}; + + CDialogInterf* dialog{interf->dragAndDropInterfData->dialogInterf}; + setupResourceToggleButton(dialog, "TOG_GOLD_M", CurrencyType::Gold, stock.gold, inf.gold); + setupResourceToggleButton(dialog, "TOG_LIFE_M", CurrencyType::LifeMana, stock.lifeMana, + inf.lifeMana); + setupResourceToggleButton(dialog, "TOG_DEATH_M", CurrencyType::DeathMana, stock.deathMana, + inf.deathMana); + setupResourceToggleButton(dialog, "TOG_RUNIC_M", CurrencyType::RunicMana, stock.runicMana, + inf.runicMana); + setupResourceToggleButton(dialog, "TOG_INFERNAL_M", CurrencyType::InfernalMana, + stock.infernalMana, inf.infernalMana); + setupResourceToggleButton(dialog, "TOG_GROVE_M", CurrencyType::GroveMana, stock.groveMana, + inf.groveMana); +} + +static void setupUi(CSiteResourceMarketInterf* interf) +{ + using namespace game; + + // Reuse button functors from CMenuBase + const auto& menuBase{CMenuBaseApi::get()}; + const auto& button{CButtonInterfApi::get()}; + const auto& freeFunctor{SmartPointerApi::get().createOrFreeNoDtor}; + + const auto& dialogApi{CDialogInterfApi::get()}; + CDialogInterf* dialog{interf->dragAndDropInterfData->dialogInterf}; + SmartPointer functor; + + using ButtonCallback = CMenuBaseApi::Api::ButtonCallback; + auto buttonCallback = (ButtonCallback)siteResMarketInterfBtnCloseHandler; + + menuBase.createButtonFunctor(&functor, 0, (CMenuBase*)interf, &buttonCallback); + button.assignFunctor(dialog, "BTN_CLOSE", dialogName, &functor, 0); + freeFunctor(&functor, nullptr); + + buttonCallback = (ButtonCallback)exchangeButtonHandler; + menuBase.createButtonFunctor(&functor, 0, (CMenuBase*)interf, &buttonCallback); + button.assignFunctor(dialog, "BTN_EXCHANGE", dialogName, &functor, 0); + freeFunctor(&functor, nullptr); + + buttonCallback = (ButtonCallback)exchangeStepDownButtonHandler; + menuBase.createButtonFunctor(&functor, 0, (CMenuBase*)interf, &buttonCallback); + button.assignFunctor(dialog, "BTN_EXCHANGE_DOWN", dialogName, &functor, 0); + freeFunctor(&functor, nullptr); + + buttonCallback = (ButtonCallback)exchangeStepUpButtonHandler; + menuBase.createButtonFunctor(&functor, 0, (CMenuBase*)interf, &buttonCallback); + button.assignFunctor(dialog, "BTN_EXCHANGE_UP", dialogName, &functor, 0); + freeFunctor(&functor, nullptr); + + updateMarketResources(interf); + updatePlayerResources(interf); + + updateExchangeButtons(interf); + updateExchangeRatesUi(interf); + + const auto& radio{CRadioButtonInterfApi::get()}; + + using Callback = CMenuBaseApi::Api::RadioButtonCallback; + + Callback callback = (Callback)onMarketResourceTogglePressed; + menuBase.createRadioButtonFunctor(&functor, 0, (CMenuBase*)interf, &callback); + + radio.setOnButtonPressed(dialog, "RAD_RESOURCE_M", dialogName, &functor); + freeFunctor(&functor, nullptr); + + callback = (Callback)onPlayerResourceTogglePressed; + menuBase.createRadioButtonFunctor(&functor, 0, (CMenuBase*)interf, &callback); + + radio.setOnButtonPressed(dialog, "RAD_RESOURCE_P", dialogName, &functor); + freeFunctor(&functor, nullptr); + + onMarketResourceTogglePressed(interf, 0, static_cast(CurrencyType::Gold)); + onPlayerResourceTogglePressed(interf, 0, static_cast(CurrencyType::Gold)); +} + +static void removeStackGroupDropSourceTarget(CSiteResourceMarketInterf* thisptr) +{ + if (!thisptr->stackGroup) { + return; + } + + using namespace game; + IMidDropManager* dropManager = &thisptr->dropManager; + if (dropManager->vftable->getDropSource(dropManager) == thisptr->stackGroup) { + dropManager->vftable->resetDropSource(dropManager); + } + + CMidDragDropInterfApi::get().removeDropTarget(thisptr, thisptr->stackGroup); + CMidDragDropInterfApi::get().removeDropSource(thisptr, thisptr->stackGroup); + + thisptr->stackGroup->IMidDropSource::vftable->destructor(thisptr->stackGroup, 1); + thisptr->stackGroup = nullptr; +} + +static void createPaperdollInterface(CSiteResourceMarketInterf* thisptr) +{ + using namespace game; + + auto paperdoll = (CPaperdollChildInterf*)Memory::get().allocate(sizeof(CPaperdollChildInterf)); + + CMidDragDropInterf* dragDrop = thisptr; + CPaperdollChildInterfApi::get().constructor(paperdoll, dragDrop, thisptr->phaseGame, + &thisptr->visitorStackId, dragDrop, + &thisptr->paperdollArea); + + if (thisptr->paperdoll) { + CInterface* interf = thisptr; + int index = -1; + interf->vftable->getChildIndex(interf, &index, thisptr->paperdoll); + interf->vftable->deleteChildAt(interf, &index); + } + + thisptr->paperdoll = paperdoll; + + if (thisptr->groupBackground) { + thisptr->groupBackground->vftable->destructor(thisptr->groupBackground, 1); + thisptr->groupBackground = nullptr; + } + + removeStackGroupDropSourceTarget(thisptr); +} + +static void hidePaperdoll(CSiteResourceMarketInterf* thisptr) +{ + using namespace game; + + if (thisptr->paperdoll) { + CInterface* interf = thisptr; + int index = -1; + interf->vftable->getChildIndex(interf, &index, thisptr->paperdoll); + interf->vftable->deleteChildAt(interf, &index); + thisptr->paperdoll = nullptr; + } + + IMqImage2* backgroundImage{AutoDialogApi::get().loadImage("DLG_MERCHANT_GROUP_BG")}; + if (thisptr->groupBackground) { + thisptr->groupBackground->vftable->destructor(thisptr->groupBackground, 1); + } + + thisptr->groupBackground = backgroundImage; +} + +static void createStackGroupInterface(CSiteResourceMarketInterf* thisptr) +{ + using namespace game; + + CMidDataCache2* dataCache{CPhaseApi::get().getDataCache(&thisptr->phaseGame->phase)}; + + const auto* stack{getStack(dataCache, &thisptr->visitorStackId)}; + auto leader{ + (const CMidUnit*)dataCache->vftable->findScenarioObjectById(dataCache, &stack->leaderId)}; + + auto leaderName{getUnitName(leader)}; + + const auto& dialogApi{CDialogInterfApi::get()}; + CDialogInterf* dialog{thisptr->dragAndDropInterfData->dialogInterf}; + + auto nameTextBox{dialogApi.findTextBox(dialog, "TXT_LEADER_NAME")}; + CTextBoxInterfApi::get().setString(nameTextBox, leaderName); + + // 'Items of party %NAME%' + auto description{getInterfaceText("X005TA0106")}; + replace(description, "%NAME%", leaderName); + + auto itemsTextBox{dialogApi.findTextBox(dialog, "TXT_LEADER")}; + CTextBoxInterfApi::get().setString(itemsTextBox, description.c_str()); + + auto faceImage{gameFunctions().createUnitFaceImage(&leader->unitImpl->id, true)}; + if (faceImage) { + faceImage->vftable->setUnknown68(faceImage, 1); + faceImage->vftable->setLeftSide(faceImage, 1); + } + + auto leaderImage{dialogApi.findPicture(dialog, "IMG_LEADER")}; + CPictureInterfApi::get().setImageWithAnchor(leaderImage, (IMqImage2*)faceImage, 18); + + const auto& dragDrop{CMidDragDropInterfApi::get()}; + dragDrop.removeDropSource(thisptr, thisptr->stackGroup); + if (thisptr->stackGroup) { + dragDrop.removeDropTarget(thisptr, thisptr->stackGroup); + } + + const CMidgard* midgard{CMidgardApi::get().instance()}; + const CMidgardID* currentPlayerId{&midgard->data->netPlayerClientPtr->second}; + + auto adapter{(CMidUnitGroupAdapter*)Memory::get().allocate(sizeof(CMidUnitGroupAdapter))}; + CMidUnitGroupAdapterApi::get().constructor(adapter, dataCache, &thisptr->visitorStackId, + currentPlayerId, 1); + + IMidDropManager* dropManager{&thisptr->dropManager}; + + auto stackGroup{(CDDStackGroup*)Memory::get().allocate(sizeof(CDDStackGroup))}; + CDDStackGroupApi::get().constructor(stackGroup, adapter, &thisptr->visitorStackId, dataCache, + dropManager, 1, &thisptr->groupArea, + thisptr->dragAndDropInterfData->task, thisptr->phaseGame, + thisptr, nullptr); + + auto currentSource{dropManager->vftable->getDropSource(dropManager)}; + if (currentSource == thisptr->stackGroup && currentSource != nullptr) { + dropManager->vftable->setDropSource(dropManager, stackGroup, true); + } + + if (thisptr->stackGroup) { + thisptr->stackGroup->IMidDropSource::vftable->destructor(thisptr->stackGroup, 1); + } + + thisptr->stackGroup = stackGroup; + dropManager->vftable->addDropSource(dropManager, thisptr->stackGroup); + dropManager->vftable->addDropTarget(dropManager, thisptr->stackGroup); + hidePaperdoll(thisptr); +} + +static void createPaperdollOrStackGroup(CSiteResourceMarketInterf* thisptr, bool toggleOn) +{ + if (toggleOn) { + createPaperdollInterface(thisptr); + } else { + createStackGroupInterface(thisptr); + } +} + +static void createStackInventory(CSiteResourceMarketInterf* thisptr) +{ + using namespace game; + + CMidDataCache2* dataCache{CPhaseApi::get().getDataCache(&thisptr->phaseGame->phase)}; + + const auto& dialogApi{CDialogInterfApi::get()}; + CDialogInterf* dialog{thisptr->dragAndDropInterfData->dialogInterf}; + + auto listBox{dialogApi.findListBox(dialog, "MLBOX_RINVENTORY")}; + + auto inventory{ + (CDDStackInventoryDisplay*)Memory::get().allocate(sizeof(CDDStackInventoryDisplay))}; + CDDStackInventoryDisplayApi::get().constructor(inventory, thisptr, listBox, + &thisptr->visitorStackId, dataCache); + + auto currentInv{thisptr->inventory}; + + IMidDropManager* dropManager{&thisptr->dropManager}; + auto current{dropManager->vftable->getDropSource(dropManager)}; + + if (current == currentInv && current != nullptr) { + dropManager->vftable->setDropSource(dropManager, inventory, 1); + } + + CMidDragDropInterfApi::get().removeDropSource(thisptr, thisptr->inventory); + + int elementCount = 0; + if (thisptr->inventory) { + CMidDragDropInterfApi::get().removeDropTarget(thisptr, thisptr->inventory); + + elementCount = thisptr->inventory->foldedInvDisplayData->elementCount; + + thisptr->inventory->IMidDropSource::vftable->destructor(thisptr->inventory, 1); + } + + thisptr->inventory = inventory; + CDDStackInventoryDisplayApi::get().setElementCount(inventory, elementCount); + dropManager->vftable->addDropSource(dropManager, inventory); + dropManager->vftable->addDropTarget(dropManager, inventory); +} + +static void onVisitorStackChanged(CSiteResourceMarketInterf* thisptr) +{ + using namespace game; + + const auto& dialogApi{CDialogInterfApi::get()}; + CDialogInterf* dialog{thisptr->dragAndDropInterfData->dialogInterf}; + + const auto* toggle{dialogApi.findToggleButton(dialog, "TOG_PAPERDOLL")}; + createPaperdollOrStackGroup(thisptr, toggle->data->checked); +} + +static void __fastcall onPaperdollTogglePressed(CSiteResourceMarketInterf* thisptr, + int /*%edx*/, + bool toggleOn, + game::CToggleButton*) +{ + using namespace game; + + IMidDropManager* dropManager = &thisptr->dropManager; + if (dropManager->vftable->getDropSource(dropManager)) { + dropManager->vftable->resetDropSource(dropManager); + } + + createPaperdollOrStackGroup(thisptr, toggleOn); +} + +static void __fastcall siteResMarketInterfOnObjectChanged(game::CMidDataCache2::INotify* thisptr, + int /*%edx*/, + game::IMidScenarioObject* scenarioObject) +{ + if (!scenarioObject) { + return; + } + + using namespace game; + + const CMidgardID* objectId = &scenarioObject->id; + const auto type = CMidgardIDApi::get().getType(objectId); + + auto interf = (CSiteResourceMarketInterf*)thisptr; + + switch (type) { + case IdType::Stack: { + if (*objectId == interf->visitorStackId) { + onVisitorStackChanged(interf); + + if (interf->inventory) { + createStackInventory(interf); + } + } + + return; + } + case IdType::Item: { + if (!interf->inventory) { + return; + } + + const CDDFoldedInvData* invData{interf->inventory->foldedInvData}; + const CMidStack* stack{getStack(invData->objectMap, &invData->objectId)}; + const CMidInventory* inventory{&stack->inventory}; + + if (inventory->vftable->getItemIndex(inventory, objectId) > -1) { + createStackInventory(interf); + } + + return; + } + case IdType::Player: { + CPhaseGame* phaseGame{interf->phaseGame}; + const CMidDataCache2* dataCache{CPhaseApi::get().getDataCache(&phaseGame->phase)}; + const CMidStack* visitorStack{getStack(dataCache, &interf->visitorStackId)}; + + if (visitorStack->ownerId == *objectId) { + updatePlayerResources(interf); + updateExchangeButtons(interf); + } + return; + } + case IdType::Site: { + if (interf->marketId == *objectId) { + updateMarketResources(interf); + updateExchangeButtons(interf); + } + return; + } + } +} + +static game::CInterfaceVftable::Draw drawInterface = nullptr; + +static void __stdcall siteResMarketDraw(game::CMidDragDropInterf* thisptr, + game::IMqRenderer2* renderer) +{ + using namespace game; + + CSiteResourceMarketInterf* interf{dragDropToSiteInterf(thisptr)}; + + IMqImage2* background{interf->groupBackground}; + if (background) { + const CMqPoint start{interf->paperdollArea.left, interf->paperdollArea.top}; + background->vftable->render(background, renderer, &start, nullptr, nullptr, nullptr); + } + + if (drawInterface) { + drawInterface(thisptr, renderer); + } +} + +static game::CInterfaceVftable::HandleMouse handleMouse = nullptr; + +static int __fastcall siteResMarketHandleMouse(game::CMidDragDropInterf* thisptr, + int /*%edx*/, + std::uint32_t mouseButton, + const game::CMqPoint* mousePosition) +{ + using namespace game; + + const CSiteResourceMarketInterf* interf{dragDropToSiteInterf(thisptr)}; + + if (mouseButton != WM_RBUTTONDOWN) { + return handleMouse(thisptr, mouseButton, mousePosition); + } + + const auto& dialogApi{CDialogInterfApi::get()}; + CDialogInterf* dialog{interf->dragAndDropInterfData->dialogInterf}; + + const CPictureInterf* picture{dialogApi.findPicture(dialog, "IMG_LEADER")}; + const CMqRect* area{picture->vftable->getArea(picture)}; + + if (!MqRectApi::get().ptInRect(area, mousePosition)) { + return handleMouse(thisptr, mouseButton, mousePosition); + } + + const auto* paperdoll{dialogApi.findToggleButton(dialog, "TOG_PAPERDOLL")}; + if (paperdoll->data->checked) { + return handleMouse(thisptr, mouseButton, mousePosition); + } + + if (interf->visitorStackId == emptyId) { + return handleMouse(thisptr, mouseButton, mousePosition); + } + + const auto& phaseApi{CPhaseApi::get()}; + CPhase* phase{&interf->phaseGame->phase}; + + CMidDataCache2* dataCache{phaseApi.getDataCache(phase)}; + const CMidStack* stack{getStack(dataCache, &interf->visitorStackId)}; + if (!stack) { + return handleMouse(thisptr, mouseButton, mousePosition); + } + + CEncParamIDPlayer encParam; + CEncParamIDPlayerApi::get().constructor(&encParam, &stack->leaderId, + phaseApi.getCurrentPlayerId(phase), nullptr); + + phaseApi.showEncyclopediaPopup(phase, &encParam); + CEncParamIDPlayerApi::get().destructor(&encParam); + + return 1; +} + +static game::CMidDataCache2::INotifyVftable siteResMarketNotifyVftable{ + (game::CMidDataCache2::INotifyVftable::OnObjectChanged)siteResMarketInterfOnObjectChanged}; + +static game::CInterfaceVftable::Destructor dragDropInterfDtor = nullptr; + +static game::RttiInfo siteResMarketRttiInfo; + +game::CInterface* createSiteResourceMarketInterf(game::ITask* task, + game::CPhaseGame* phaseGame, + const game::CMidgardID& visitorStackId, + const game::CMidgardID& marketId) +{ + using namespace game; + + auto interf = (CSiteResourceMarketInterf*)Memory::get().allocate( + sizeof(CSiteResourceMarketInterf)); + + CMidDragDropInterfApi::get().constructor(interf, dialogName, task, phaseGame); + static bool firstTime{true}; + if (firstTime) { + firstTime = false; + + const auto* vftable{interf->CMidDragDropInterf::vftable}; + // Remember base class destructor for proper cleanup + dragDropInterfDtor = vftable->destructor; + drawInterface = vftable->draw; + handleMouse = vftable->handleMouse; + + replaceRttiInfo(siteResMarketRttiInfo, vftable, true); + + siteResMarketRttiInfo.vftable.destructor = (CInterfaceVftable::Destructor) + siteResMarketInterfDragDropDtor; + siteResMarketRttiInfo.vftable.draw = (CInterfaceVftable::Draw)siteResMarketDraw; + siteResMarketRttiInfo.vftable.handleMouse = (CInterfaceVftable::HandleMouse) + siteResMarketHandleMouse; + } + + // Use our own vftables + interf->CInterface::vftable = &siteResMarketRttiInfo.vftable; + interf->CMidDataCache2::INotify::vftable = &siteResMarketNotifyVftable; + interf->IResetStackExt::vftable = &siteResMarketStackExtVftable; + + new (&interf->exchangeRates) MarketExchangeRates(); + interf->visitorStackId = visitorStackId; + interf->marketId = marketId; + interf->exchangeStep = 1; + interf->paperdoll = nullptr; + interf->stackGroup = nullptr; + interf->groupBackground = nullptr; + interf->inventory = nullptr; + + CMidDataCache2* dataCache{CPhaseApi::get().getDataCache(&phaseGame->phase)}; + CMidDataCache2Api::get().addNotify(dataCache, interf); + + const auto& dialogApi{CDialogInterfApi::get()}; + CDialogInterf* dialog{interf->dragAndDropInterfData->dialogInterf}; + + if (getExchangeRates(dataCache, marketId, visitorStackId, interf->exchangeRates)) { + setupUi(interf); + } else { + // Exchange rates script contain errors, disable exchange button + CButtonInterf* button{dialogApi.findButton(dialog, "BTN_EXCHANGE")}; + button->vftable->setEnabled(button, false); + } + + // Setup leader image, etc + const CPictureInterf* paperdollBg{dialogApi.findPicture(dialog, "IMG_PAPERDOLL_BG")}; + interf->paperdollArea = *paperdollBg->vftable->getArea(paperdollBg); + + const CPictureInterf* slotImage{dialogApi.findPicture(dialog, "IMG_SLOT1")}; + interf->groupArea = *slotImage->vftable->getArea(slotImage); + + dialogApi.hideControl(dialog, "IMG_PAPERDOLL_BG"); + dialogApi.hideControl(dialog, "IMG_SLOT1"); + + auto obj{dataCache->vftable->findScenarioObjectById(dataCache, &marketId)}; + auto market{static_cast(obj)}; + const char* title{market->title.string ? market->title.string : ""}; + + CTextBoxInterf* marketText{dialogApi.findTextBox(dialog, "TXT_LABEL1")}; + CTextBoxInterfApi::get().setString(marketText, title); + + createStackGroupInterface(interf); + createStackInventory(interf); + + using ButtonCallback = CMainView2Api::Api::ToggleButtonCallback; + ButtonCallback callback{}; + callback.callback = (ButtonCallback::Callback)&onPaperdollTogglePressed; + + SmartPointer functor{}; + CMainView2Api::get().createToggleButtonFunctor(&functor, 0, (CMainView2*)interf, &callback); + + CToggleButtonApi::get().assignFunctor(dialog, "TOG_PAPERDOLL", dialogName, &functor, 0); + SmartPointerApi::get().createOrFreeNoDtor(&functor, nullptr); + + playSoundEffect(SoundEffect::Entrsite); + + return interf; +} + +} // namespace hooks diff --git a/mss32/src/sounds.cpp b/mss32/src/sounds.cpp new file mode 100644 index 00000000..42d2b798 --- /dev/null +++ b/mss32/src/sounds.cpp @@ -0,0 +1,60 @@ +/* + * This file is part of the modding toolset for Disciples 2. + * (https://github.com/VladimirMakeev/D2ModdingToolset) + * Copyright (C) 2024 Vladimir Makeev. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "sounds.h" +#include "version.h" +#include + +namespace game::SoundsApi { + +// clang-format off +static std::array functions = {{ + // Akella + Api{ + (Api::Instance)0x5b0e27, + (Api::SoundsPtrSetData)0x482062, + (Api::PlaySound)0x5b10da, + }, + // Russobit + Api{ + (Api::Instance)0x5b0e27, + (Api::SoundsPtrSetData)0x482062, + (Api::PlaySound)0x5b10da, + }, + // Gog + Api{ + (Api::Instance)0x5b011b, + (Api::SoundsPtrSetData)0x481bea, + (Api::PlaySound)0x5b03ce, + }, + // Scenario Editor + Api{ + (Api::Instance)0, + (Api::SoundsPtrSetData)0, + (Api::PlaySound)0, + }, +}}; +// clang-format on + +Api& get() +{ + return functions[static_cast(hooks::gameVersion())]; +} + +} // namespace game::SoundsApi diff --git a/mss32/src/spinbuttoninterf.cpp b/mss32/src/spinbuttoninterf.cpp index 7355ce29..7003f000 100644 --- a/mss32/src/spinbuttoninterf.cpp +++ b/mss32/src/spinbuttoninterf.cpp @@ -49,9 +49,9 @@ static std::array functions = {{ // Scenario Editor Api{ (Api::SetValues)0, - (Api::SetOptions)0, - (Api::SetSelectedOption)0, - (Api::AssignFunctor)0, + (Api::SetOptions)0x490a65, + (Api::SetSelectedOption)0x490a40, + (Api::AssignFunctor)0x4d1798, }, }}; // clang-format on diff --git a/mss32/src/stacktemplatecache.cpp b/mss32/src/stacktemplatecache.cpp new file mode 100644 index 00000000..33924d96 --- /dev/null +++ b/mss32/src/stacktemplatecache.cpp @@ -0,0 +1,72 @@ +/* + * This file is part of the modding toolset for Disciples 2. + * (https://github.com/VladimirMakeev/D2ModdingToolset) + * Copyright (C) 2024 Vladimir Makeev. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "stacktemplatecache.h" +#include + +namespace hooks { + +using StackTemplateCache = std::unordered_map; + +static StackTemplateCache cache; + +void stackTemplateCacheAdd(const game::CMidgardID& stackTemplateId, const game::CMidgardID& stackId) +{ + StacksSet& stacksCreatedFromTemplate{cache[stackTemplateId]}; + + stacksCreatedFromTemplate.insert(stackId); +} + +void stackTemplateCacheRemove(const game::CMidgardID& stackTemplateId, + const game::CMidgardID& stackId) +{ + const auto it{cache.find(stackTemplateId)}; + if (it == cache.cend()) { + // No mapping found + return; + } + + StacksSet& stacksCreatedFromTemplate{it->second}; + stacksCreatedFromTemplate.erase(stackId); +} + +const StacksSet* stackTemplateCacheFind(const game::CMidgardID& stackTemplateId) +{ + const auto it{cache.find(stackTemplateId)}; + if (it == cache.cend()) { + return nullptr; + } + + return &it->second; +} + +bool stackTemplateCacheCheck(const game::CMidgardID& stackTemplateId) +{ + const StacksSet* stacks{stackTemplateCacheFind(stackTemplateId)}; + return stacks && stacks->size(); +} + +void stackTemplateCacheClear() +{ + cache.clear(); +} + +} // namespace hooks diff --git a/mss32/src/streambits.cpp b/mss32/src/streambits.cpp new file mode 100644 index 00000000..70c05c7e --- /dev/null +++ b/mss32/src/streambits.cpp @@ -0,0 +1,54 @@ +/* + * This file is part of the modding toolset for Disciples 2. + * (https://github.com/VladimirMakeev/D2ModdingToolset) + * Copyright (C) 2024 Vladimir Makeev. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "streambits.h" +#include "version.h" +#include + +namespace game::CStreamBitsApi { + +// clang-format off +static std::array functions = {{ + // Akella + Api{ + (Api::SerializeId)0x47e666, + (Api::ReadConstructor)0x5613ef, + (Api::Destructor)0x561422, + }, + // Russobit + Api{ + (Api::SerializeId)0x47e666, + (Api::ReadConstructor)0x5613ef, + (Api::Destructor)0x561422, + }, + // Gog + Api{ + (Api::SerializeId)0x47e230, + (Api::ReadConstructor)0x560b8c, + (Api::Destructor)0x560bbf, + }, +}}; +// clang-format on + +Api& get() +{ + return functions[static_cast(hooks::gameVersion())]; +} + +} // namespace game::CStreamBitsApi diff --git a/mss32/src/taskmanager.cpp b/mss32/src/taskmanager.cpp new file mode 100644 index 00000000..4f6f7ee4 --- /dev/null +++ b/mss32/src/taskmanager.cpp @@ -0,0 +1,48 @@ +/* + * This file is part of the modding toolset for Disciples 2. + * (https://github.com/VladimirMakeev/D2ModdingToolset) + * Copyright (C) 2024 Vladimir Makeev. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "taskmanager.h" +#include "version.h" +#include + +namespace game::CTaskManagerApi { + +// clang-format off +static std::array functions = {{ + // Akella + Api{ + (Api::SetCurrentTask)0x5c9fc8, + }, + // Russobit + Api{ + (Api::SetCurrentTask)0x5c9fc8, + }, + // Gog + Api{ + (Api::SetCurrentTask)0x5c8f71, + } +}}; +// clang-format on + +Api& get() +{ + return functions[static_cast(hooks::gameVersion())]; +} + +} // namespace game::CTaskManagerApi diff --git a/mss32/src/taskobjaddsite.cpp b/mss32/src/taskobjaddsite.cpp new file mode 100644 index 00000000..953541e1 --- /dev/null +++ b/mss32/src/taskobjaddsite.cpp @@ -0,0 +1,31 @@ +/* + * This file is part of the modding toolset for Disciples 2. + * (https://github.com/VladimirMakeev/D2ModdingToolset) + * Copyright (C) 2024 Vladimir Makeev. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "taskobjaddsite.h" + +namespace game::editor::CTaskObjAddSiteApi { + +Api& get() +{ + static Api api{(Api::Constructor)0x41061d, (CTaskObjVftable::DoAction)0x41082d}; + + return api; +} + +} // namespace game::editor::CTaskObjAddSiteApi diff --git a/mss32/src/taskobjaddsitehooks.cpp b/mss32/src/taskobjaddsitehooks.cpp new file mode 100644 index 00000000..1e21b41b --- /dev/null +++ b/mss32/src/taskobjaddsitehooks.cpp @@ -0,0 +1,87 @@ +/* + * This file is part of the modding toolset for Disciples 2. + * (https://github.com/VladimirMakeev/D2ModdingToolset) + * Copyright (C) 2024 Vladimir Makeev. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "taskobjaddsitehooks.h" +#include "taskobjaddsite.h" +#include "sitecategoryhooks.h" +#include "originalfunctions.h" +#include "visitors.h" +#include "scenedit.h" +#include "utils.h" +#include "editor.h" +#include "midsite.h" +#include "scenedithooks.h" +#include "game.h" +#include + +namespace hooks { + +bool __fastcall taskObjAddSiteDoActionHooked(game::editor::CTaskObjAddSite* thisptr, + int /*%edx*/, + const game::CMqPoint* mapPosition) +{ + using namespace game; + using namespace editor; + + const auto& marketCategory{customSiteCategories().resourceMarket}; + if (!customSiteCategories().exists || thisptr->siteData->siteCategory.id != marketCategory.id) { + return getOriginalFunctions().taskObjAddSiteDoAction(thisptr, mapPosition); + } + + // Add resource market site + CScenEdit* scenEdit{CScenEditApi::get().instance()}; + IMidgardObjectMap* objectMap{scenEdit->data->unknown2->data->scenarioMap}; + + const std::string name{getInterfaceText("X002TA0043")}; + const std::string description{getInterfaceText("X002TA0044")}; + + if (!VisitorApi::get().createSite(&marketCategory, mapPosition, 0, "", name.c_str(), + description.c_str(), objectMap, 1)) { + showErrorMessageBox("Could not create resource market object"); + return false; + } + + const CMidSite* site{editorFunctions.getSiteAtPosition(mapPosition, objectMap)}; + if (!site) { + showErrorMessageBox( + fmt::format("Could not find resource market after creation at ({:d}, {:d})", + mapPosition->x, mapPosition->y)); + return true; + } + + if (site->siteCategory.id != marketCategory.id) { + showErrorMessageBox(fmt::format("Wrong resource market site category {:d}, expected {:d}", + (int)site->siteCategory.id, (int)marketCategory.id)); + return true; + } + + const auto& names{getMarketNames()}; + const int index{gameFunctions().generateRandomNumberStd(names.size())}; + + const auto& siteName{names[index].first}; + const auto& siteDescription{names[index].second}; + + VisitorApi::get().changeSiteInfo(&site->id, siteName.c_str(), siteDescription.c_str(), + objectMap, 1); + + editorFunctions.showOrHideSiteOnStrategicMap(site, objectMap, nullptr, 1); + return false; +} + +} diff --git a/mss32/src/taskobjerase.cpp b/mss32/src/taskobjerase.cpp new file mode 100644 index 00000000..2d255eb0 --- /dev/null +++ b/mss32/src/taskobjerase.cpp @@ -0,0 +1,31 @@ +/* + * This file is part of the modding toolset for Disciples 2. + * (https://github.com/VladimirMakeev/D2ModdingToolset) + * Copyright (C) 2024 Vladimir Makeev. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "taskobjerase.h" + +namespace game::editor::CTaskObjEraseApi { + +Api& get() +{ + static Api api{(Api::Constructor)0x40b8aa}; + + return api; +} + +} // namespace game::editor::CTaskObjEraseApi diff --git a/mss32/src/taskobjmove.cpp b/mss32/src/taskobjmove.cpp new file mode 100644 index 00000000..478f284a --- /dev/null +++ b/mss32/src/taskobjmove.cpp @@ -0,0 +1,31 @@ +/* + * This file is part of the modding toolset for Disciples 2. + * (https://github.com/VladimirMakeev/D2ModdingToolset) + * Copyright (C) 2024 Vladimir Makeev. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "taskobjmove.h" + +namespace game::editor::CTaskObjMoveApi { + +Api& get() +{ + static Api api{(Api::Constructor)0x40e285}; + + return api; +} + +} // namespace game::editor::CTaskObjMoveApi diff --git a/mss32/src/taskobjprop.cpp b/mss32/src/taskobjprop.cpp new file mode 100644 index 00000000..07f40d05 --- /dev/null +++ b/mss32/src/taskobjprop.cpp @@ -0,0 +1,34 @@ +/* + * This file is part of the modding toolset for Disciples 2. + * (https://github.com/VladimirMakeev/D2ModdingToolset) + * Copyright (C) 2024 Vladimir Makeev. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "taskobjprop.h" + +namespace game::editor::CTaskObjPropApi { + +Api& get() +{ + static Api api{ + (Api::Constructor)0x40f6c8, + (CTaskObjVftable::DoAction)0x40F78E, + }; + + return api; +} + +} // namespace game::editor::CTaskObjPropApi diff --git a/mss32/src/taskobjprophooks.cpp b/mss32/src/taskobjprophooks.cpp new file mode 100644 index 00000000..0ac8cc74 --- /dev/null +++ b/mss32/src/taskobjprophooks.cpp @@ -0,0 +1,103 @@ +/* + * This file is part of the modding toolset for Disciples 2. + * (https://github.com/VladimirMakeev/D2ModdingToolset) + * Copyright (C) 2024 Vladimir Makeev. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "taskobjprophooks.h" +#include "taskobjprop.h" +#include "midgardplan.h" +#include "gameutils.h" +#include "scenedit.h" +#include "midgardscenariomap.h" +#include "dynamiccast.h" +#include "mapelement.h" +#include "midsiteresourcemarket.h" +#include "interfmanager.h" +#include "originalfunctions.h" +#include "resourcemarketinterface.h" + +namespace hooks { + +static game::IMapElement* getMapElementAtPosition(const game::CMqPoint* mapPosition) +{ + using namespace game; + using namespace editor; + + const CScenEdit* scenEdit{CScenEditApi::get().instance()}; + const IMidgardObjectMap* objectMap{scenEdit->data->unknown2->data->scenarioMap}; + + const auto& dynamicCast{RttiApi::get().dynamicCast}; + const auto* mapElementType{RttiApi::rtti().IMapElementType}; + + const CMidgardPlan* plan{getMidgardPlan(objectMap)}; + + const auto& list{IdListApi::get()}; + IdList objectIds; + list.constructor(&objectIds); + + IMapElement* mapElement{}; + if (CMidgardPlanApi::get().getObjectsAtPoint(plan, &objectIds, mapPosition)) { + const auto* objectType{RttiApi::rtti().IMidScenarioObjectType}; + + for (const auto& objectId : objectIds) { + auto* object{objectMap->vftable->findScenarioObjectById(objectMap, &objectId)}; + + mapElement = (IMapElement*)dynamicCast(object, 0, objectType, mapElementType, 0); + + if (!dynamicCast(mapElement, 0, mapElementType, RttiApi::rtti().CMidRoadType, 0)) { + break; + } + } + } + + list.destructor(&objectIds); + return mapElement; +} + +bool __fastcall taskObjPropDoActionHooked(game::editor::CTaskObjProp* thisptr, + int /*%edx*/, + const game::CMqPoint* mapPosition) +{ + using namespace game; + using namespace editor; + + const auto& dynamicCast{RttiApi::get().dynamicCast}; + const auto* mapElementType{RttiApi::rtti().IMapElementType}; + + IMapElement* mapElement{getMapElementAtPosition(mapPosition)}; + if (!mapElement) { + return false; + } + + auto* market{(CMidSiteResourceMarket*)dynamicCast(mapElement, 0, mapElementType, + getResourceMarketTypeDescriptor(), 0)}; + if (!market) { + // Map element is not resource market, let the game handle it + return getOriginalFunctions().taskObjPropDoAction(thisptr, mapPosition); + } + + CInterface* marketProps = createResourceMarketInterface(thisptr, market); + + thisptr->propertiesInterface = marketProps; + CInterfManagerImpl* interfManager{thisptr->interfManager.data}; + interfManager->CInterfManagerImpl::CInterfManager::vftable->showInterface(interfManager, + marketProps); + + return true; +} + +} \ No newline at end of file diff --git a/mss32/src/testcondition.cpp b/mss32/src/testcondition.cpp index 92dc34e6..02386919 100644 --- a/mss32/src/testcondition.cpp +++ b/mss32/src/testcondition.cpp @@ -28,14 +28,65 @@ static std::array functions = {{ // Akella Api{ (Api::Create)0x4422b7, + (ITestConditionVftable::Test)0x444d8f, // frequency + (ITestConditionVftable::Test)0x444e22, // location + (ITestConditionVftable::Test)0x44521f, // enter city + (ITestConditionVftable::Test)0x4453d1, // own city + (ITestConditionVftable::Test)0x4454cd, // kill stack + (ITestConditionVftable::Test)0x44564a, // own item + (ITestConditionVftable::Test)0x445755, // leader own item + (ITestConditionVftable::Test)0x4458fa, // diplomacy + (ITestConditionVftable::Test)0x445a77, // alliance + (ITestConditionVftable::Test)0x445b0a, // loot ruin + (ITestConditionVftable::Test)0x445bbf, // transform land + (ITestConditionVftable::Test)0x445d74, // visit site + (ITestConditionVftable::Test)0x445044, // leader to zone + (ITestConditionVftable::Test)0x4452d5, // leader to city + (ITestConditionVftable::Test)0x445e9b, // item to location + (ITestConditionVftable::Test)0x44609a, // stack exists + (ITestConditionVftable::Test)0x446239, // var in range }, // Russobit Api{ (Api::Create)0x4422b7, + (ITestConditionVftable::Test)0x444d8f, // frequency + (ITestConditionVftable::Test)0x444e22, // location + (ITestConditionVftable::Test)0x44521f, // enter city + (ITestConditionVftable::Test)0x4453d1, // own city + (ITestConditionVftable::Test)0x4454cd, // kill stack + (ITestConditionVftable::Test)0x44564a, // own item + (ITestConditionVftable::Test)0x445755, // leader own item + (ITestConditionVftable::Test)0x4458fa, // diplomacy + (ITestConditionVftable::Test)0x445a77, // alliance + (ITestConditionVftable::Test)0x445b0a, // loot ruin + (ITestConditionVftable::Test)0x445bbf, // transform land + (ITestConditionVftable::Test)0x445d74, // visit site + (ITestConditionVftable::Test)0x445044, // leader to zone + (ITestConditionVftable::Test)0x4452d5, // leader to city + (ITestConditionVftable::Test)0x445e9b, // item to location + (ITestConditionVftable::Test)0x44609a, // stack exists + (ITestConditionVftable::Test)0x446239, // var in range }, // Gog Api{ (Api::Create)0x441f1c, + (ITestConditionVftable::Test)0x444993, // frequency + (ITestConditionVftable::Test)0x6fd6b8, // location + (ITestConditionVftable::Test)0x444e23, // enter city + (ITestConditionVftable::Test)0x444fd5, // own city + (ITestConditionVftable::Test)0x4450d1, // kill stack + (ITestConditionVftable::Test)0x44524e, // own item + (ITestConditionVftable::Test)0x445359, // leader own item + (ITestConditionVftable::Test)0x4454fe, // diplomacy + (ITestConditionVftable::Test)0x44567b, // alliance + (ITestConditionVftable::Test)0x44570e, // loot ruin + (ITestConditionVftable::Test)0x4457c3, // transform land + (ITestConditionVftable::Test)0x445978, // visit site + (ITestConditionVftable::Test)0x444c48, // leader to zone + (ITestConditionVftable::Test)0x444ed9, // leader to city + (ITestConditionVftable::Test)0x445a9f, // item to location + (ITestConditionVftable::Test)0x445c9e, // stack exists + (ITestConditionVftable::Test)0x445e3d, // var in range }, }}; // clang-format on diff --git a/mss32/src/testkillstackhooks.cpp b/mss32/src/testkillstackhooks.cpp new file mode 100644 index 00000000..68f2ddbb --- /dev/null +++ b/mss32/src/testkillstackhooks.cpp @@ -0,0 +1,74 @@ +/* + * This file is part of the modding toolset for Disciples 2. + * (https://github.com/VladimirMakeev/D2ModdingToolset) + * Copyright (C) 2024 Vladimir Makeev. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "testkillstackhooks.h" +#include "eventconditions.h" +#include "gameutils.h" +#include "midevent.h" +#include "midplayer.h" +#include "midstackdestroyed.h" +#include "testkillstack.h" +#include "timer.h" + +namespace hooks { + +bool __fastcall testKillStackHooked(const game::CTestKillStack* thisptr, + int /*%edx*/, + const game::IMidgardObjectMap* objectMap, + const game::CMidgardID* playerId, + const game::CMidgardID* eventId) +{ + using namespace game; + +#ifdef D2_MEASURE_EVENTS_TIME + extern const std::string_view eventsPerformanceLog; + ScopedTimer timer{" Test condition 'kill stack'", eventsPerformanceLog}; + + extern long long conditionsTotalTime; + ScopedValueTimer valueTimer{conditionsTotalTime}; +#endif + + const CMidStackDestroyed* stackDestroyed{getStackDestroyed(objectMap)}; + const CMidgardID& killStackId{thisptr->condKillStack->stackId}; + const CMidPlayer* neutrals{}; + + const auto& affectsPlayer{CMidEventApi::get().affectsPlayer}; + + for (const auto& entry : stackDestroyed->destroyedStacks) { + if (entry.stackId == killStackId || entry.stackSrcTemplateId == killStackId) { + CMidgardID killerId{entry.killerId}; + if (killerId == emptyId) { + if (!neutrals) { + // Search for neutrals only when necessary + neutrals = getNeutralPlayer(objectMap); + } + + killerId = neutrals->id; + } + + if (affectsPlayer(objectMap, &killerId, eventId)) { + return true; + } + } + } + + return false; +} + +} // namespace hooks diff --git a/mss32/src/testleaderownitemhooks.cpp b/mss32/src/testleaderownitemhooks.cpp new file mode 100644 index 00000000..0e48758d --- /dev/null +++ b/mss32/src/testleaderownitemhooks.cpp @@ -0,0 +1,75 @@ +/* + * This file is part of the modding toolset for Disciples 2. + * (https://github.com/VladimirMakeev/D2ModdingToolset) + * Copyright (C) 2024 Vladimir Makeev. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "testleaderownitemhooks.h" +#include "eventconditions.h" +#include "gameutils.h" +#include "midstack.h" +#include "stacktemplatecache.h" +#include "testleaderownitem.h" +#include "timer.h" + +namespace hooks { + +bool __fastcall testLeaderOwnItemHooked(const game::CTestLeaderOwnItem* thisptr, + int /*%edx*/, + const game::IMidgardObjectMap* objectMap, + const game::CMidgardID* playerId, + const game::CMidgardID* eventId) +{ + using namespace game; + +#ifdef D2_MEASURE_EVENTS_TIME + extern const std::string_view eventsPerformanceLog; + ScopedTimer timer{" Test condition 'leader own item'", eventsPerformanceLog}; + + extern long long conditionsTotalTime; + ScopedValueTimer valueTimer{conditionsTotalTime}; +#endif + + const auto* condition{thisptr->condLeaderOwnItem}; + const CMidgardID* stackId{&condition->stackId}; + + if (CMidgardIDApi::get().getType(stackId) == IdType::Stack) { + const CMidStack* stack{getStack(objectMap, stackId)}; + + return stack && isInventoryContainsItem(objectMap, stack->inventory, condition->itemId); + } + + const StacksSet* stacksFromTemplate{stackTemplateCacheFind(*stackId)}; + if (!stacksFromTemplate) { + // No stacks were created from template, condition can't be met + return false; + } + + for (const auto& id : *stacksFromTemplate) { + const CMidStack* stack{getStack(objectMap, &id)}; + if (!stack) { + continue; + } + + if (isInventoryContainsItem(objectMap, stack->inventory, condition->itemId)) { + return true; + } + } + + return false; +} + +} // namespace hooks diff --git a/mss32/src/testleadertozonehooks.cpp b/mss32/src/testleadertozonehooks.cpp new file mode 100644 index 00000000..50a1772e --- /dev/null +++ b/mss32/src/testleadertozonehooks.cpp @@ -0,0 +1,127 @@ +/* + * This file is part of the modding toolset for Disciples 2. + * (https://github.com/VladimirMakeev/D2ModdingToolset) + * Copyright (C) 2024 Vladimir Makeev. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "testleadertozonehooks.h" +#include "dynamiccast.h" +#include "eventconditions.h" +#include "gameutils.h" +#include "midgardobjectmap.h" +#include "midgardplan.h" +#include "midlocation.h" +#include "midstack.h" +#include "mqrect.h" +#include "testleadertozone.h" +#include "timer.h" + +namespace hooks { + +static bool isStackInsideLocation(const game::IMidgardObjectMap* objectMap, + const game::CMidgardID* stackId, + const game::CMidgardID* locationId) +{ + using namespace game; + + const CMidStack* stack{getStack(objectMap, stackId)}; + if (!stack || stack->insideId != emptyId) { + return false; + } + + const CMidLocation* location{getLocation(objectMap, locationId)}; + const CMqPoint& pos{location->position}; + const int radius{location->radius}; + + const int startX{pos.x - radius}; + const int startY{pos.y - radius}; + const int endX{pos.x + radius + 1}; + const int endY{pos.y + radius + 1}; + const CMqRect area{startX, startY, endX, endY}; + + return MqRectApi::get().ptInRect(&area, &stack->position); +} + +bool __fastcall testLeaderToZoneHooked(const game::CTestLeaderToZone* thisptr, + int /*%edx*/, + const game::IMidgardObjectMap* objectMap, + const game::CMidgardID* playerId, + const game::CMidgardID* eventId) +{ + using namespace game; + +#ifdef D2_MEASURE_EVENTS_TIME + extern const std::string_view eventsPerformanceLog; + ScopedTimer timer{" Test condition 'leader to zone'", eventsPerformanceLog}; + + extern long long conditionsTotalTime; + ScopedValueTimer valueTimer{conditionsTotalTime}; +#endif + + if (thisptr->stackId == emptyId) { + return false; + } + + const CMidgardID& stackId{thisptr->condLeaderToZone->stackId}; + const CMidgardID& locationId{thisptr->condLeaderToZone->locationId}; + + if (CMidgardIDApi::get().getType(&stackId) == IdType::Stack) { + const CMidStack* stack{getStack(objectMap, &thisptr->stackId)}; + return stack && isStackInsideLocation(objectMap, &stackId, &locationId); + } + + // Instead of checking every stack we check location tiles. + // Condition is met if there is a tile with a stack owned by specified player + // and it was created from a template. + // This approach reduces number of checks from O(N) to a maximum 49 in case of 7x7 location. + + const auto& getObjectId{CMidgardPlanApi::get().getObjectId}; + const CMidgardPlan* plan{getMidgardPlan(objectMap)}; + + const CMidLocation* location{getLocation(objectMap, &locationId)}; + const CMqPoint& pos{location->position}; + const int diameter{location->radius * 2 + 1}; + const int radius{diameter / 2}; + + const int startX{pos.x - radius}; + const int startY{pos.y - radius}; + const int endX{pos.x + radius + 1}; + const int endY{pos.y + radius + 1}; + + const IdType type{IdType::Stack}; + for (int y = startY; y < endY; ++y) { + for (int x = startX; x < endX; ++x) { + const CMqPoint position{x, y}; + const CMidgardID* objectId{getObjectId(plan, &position, &type)}; + if (!objectId) { + continue; + } + + const CMidStack* stack{getStack(objectMap, objectId)}; + if (!stack) { + continue; + } + + if (stack->ownerId == *playerId && stack->sourceTemplateId == stackId) { + return true; + } + } + } + + return false; +} + +} // namespace hooks diff --git a/mss32/src/testownitemhooks.cpp b/mss32/src/testownitemhooks.cpp new file mode 100644 index 00000000..cfcf0130 --- /dev/null +++ b/mss32/src/testownitemhooks.cpp @@ -0,0 +1,107 @@ +/* + * This file is part of the modding toolset for Disciples 2. + * (https://github.com/VladimirMakeev/D2ModdingToolset) + * Copyright (C) 2024 Vladimir Makeev. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "testownitemhooks.h" +#include "eventconditions.h" +#include "fortification.h" +#include "gameutils.h" +#include "midevent.h" +#include "midgardobjectmap.h" +#include "miditem.h" +#include "midstack.h" +#include "testownitem.h" +#include "timer.h" +#include "utils.h" +#include + +namespace hooks { + +bool __fastcall testOwnItemHooked(const game::CTestOwnItem* thisptr, + int /*%edx*/, + const game::IMidgardObjectMap* objectMap, + const game::CMidgardID* playerId, + const game::CMidgardID* eventId) +{ + using namespace game; + +#ifdef D2_MEASURE_EVENTS_TIME + extern const std::string_view eventsPerformanceLog; + ScopedTimer timer{" Test condition 'own item'", eventsPerformanceLog}; + + extern long long conditionsTotalTime; + ScopedValueTimer valueTimer{conditionsTotalTime}; +#endif + + // Get all players to which event can be applied, by their races + std::unordered_set affectedPlayers; + auto getAffectedPlayer = [objectMap, eventId, + &affectedPlayers](const IMidScenarioObject* player) { + if (CMidEventApi::get().affectsPlayer(objectMap, &player->id, eventId)) { + affectedPlayers.insert(player->id); + } + }; + + forEachScenarioObject(objectMap, IdType::Player, getAffectedPlayer); + + const CMidgardID* itemId{&thisptr->condOwnItem->itemId}; + + IteratorPtr current{}; + objectMap->vftable->begin(objectMap, ¤t); + + IteratorPtr end{}; + objectMap->vftable->end(objectMap, &end); + + const auto& getType{CMidgardIDApi::get().getType}; + const auto& free{SmartPointerApi::get().createOrFree}; + + while (!current.data->vftable->end(current.data, end.data)) { + const CMidgardID* id{current.data->vftable->getObjectId(current.data)}; + const IdType idType{getType(id)}; + + if (idType == IdType::Stack) { + const CMidStack* stack{getStack(objectMap, id)}; + + if (affectedPlayers.find(stack->ownerId) != affectedPlayers.cend()) { + if (isInventoryContainsItem(objectMap, stack->inventory, *itemId)) { + free((SmartPointer*)¤t, nullptr); + free((SmartPointer*)&end, nullptr); + return true; + } + } + } else if (idType == IdType::Fortification) { + const CFortification* fort{getFort(objectMap, id)}; + + if (affectedPlayers.find(fort->ownerId) != affectedPlayers.cend()) { + if (isInventoryContainsItem(objectMap, fort->inventory, *itemId)) { + free((SmartPointer*)¤t, nullptr); + free((SmartPointer*)&end, nullptr); + return true; + } + } + } + + current.data->vftable->advance(current.data); + } + + free((SmartPointer*)¤t, nullptr); + free((SmartPointer*)&end, nullptr); + return false; +} + +} // namespace hooks diff --git a/mss32/src/teststackexistshooks.cpp b/mss32/src/teststackexistshooks.cpp new file mode 100644 index 00000000..b99673d2 --- /dev/null +++ b/mss32/src/teststackexistshooks.cpp @@ -0,0 +1,74 @@ +/* + * This file is part of the modding toolset for Disciples 2. + * (https://github.com/VladimirMakeev/D2ModdingToolset) + * Copyright (C) 2024 Vladimir Makeev. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "teststackexistshooks.h" +#include "eventconditions.h" +#include "gameutils.h" +#include "stacktemplatecache.h" +#include "teststackexists.h" +#include "timer.h" + +namespace hooks { + +bool __fastcall testStackExistsHooked(const game::CTestStackExists* thisptr, + int /*%edx*/, + const game::IMidgardObjectMap* objectMap, + const game::CMidgardID* playerId, + const game::CMidgardID* eventId) +{ + using namespace game; + +#ifdef D2_MEASURE_EVENTS_TIME + extern const std::string_view eventsPerformanceLog; + ScopedTimer timer{" Test condition 'stack exists'", eventsPerformanceLog}; + + extern long long conditionsTotalTime; + ScopedValueTimer valueTimer{conditionsTotalTime}; +#endif + + const CMidgardID* stackId{&thisptr->condStackExists->stackId}; + + if (CMidgardIDApi::get().getType(stackId) == IdType::Stack) { + const CMidStack* stack{getStack(objectMap, stackId)}; + + if (thisptr->condStackExists->existanceStatus) { + return stack == nullptr; + } + + return stack != nullptr; + } + + // Stack id belongs to stack template id + const bool stackCreatedFromTemplate{stackTemplateCacheCheck(*stackId)}; + if (stackCreatedFromTemplate) { + if (thisptr->condStackExists->existanceStatus) { + return false; + } + + return true; + } + + if (!thisptr->condStackExists->existanceStatus) { + return false; + } + + return true; +} + +} // namespace hooks diff --git a/mss32/src/textids.cpp b/mss32/src/textids.cpp index 58099f70..8ec6bcd9 100644 --- a/mss32/src/textids.cpp +++ b/mss32/src/textids.cpp @@ -124,6 +124,31 @@ void readGeneratorTextIds(const sol::table& table, TextIds::ScenarioGenerator& v value.limitExceeded = rsg.get_or("limitExceeded", std::string()); } +void readResourceMarketTextIds(const sol::table& table, TextIds::ResourceMarket& value) +{ + auto marketTable = table.get>("resourceMarket"); + if (!marketTable.has_value()) { + return; + } + + auto& market = marketTable.value(); + value.encyDesc = market.get_or("encyDesc", std::string()); + value.infiniteAmount = market.get_or("infiniteAmount", std::string()); + value.exchangeDesc = market.get_or("exchangeDesc", std::string()); + value.exchangeNotAvailable = market.get_or("exchangeNotAvailable", std::string()); +} + +void readNobleActionsTextIds(const sol::table& table, TextIds::NobleActions& value) +{ + auto actionsTable = table.get>("nobleActions"); + if (!actionsTable.has_value()) { + return; + } + + auto& actions = actionsTable.value(); + value.stealMarketSuccess = actions.get_or("stealMarketSuccess", std::string()); +} + void readInterfTextIds(const sol::table& table, TextIds::Interf& value) { auto interf = table.get>("interf"); @@ -178,6 +203,8 @@ void initialize(TextIds& value) readEventsTextIds(table, value.events); readLobbyTextIds(table, value.lobby); readGeneratorTextIds(table, value.rsg); + readResourceMarketTextIds(table, value.resourceMarket); + readNobleActionsTextIds(table, value.nobleActions); } catch (const std::exception& e) { showErrorMessageBox(fmt::format("Failed to read script '{:s}'.\n" "Reason: '{:s}'", diff --git a/mss32/src/togglebutton.cpp b/mss32/src/togglebutton.cpp index 010aeb53..0a9ab28d 100644 --- a/mss32/src/togglebutton.cpp +++ b/mss32/src/togglebutton.cpp @@ -43,7 +43,7 @@ static std::array functions = {{ // Scenario Editor Api{ (Api::SetChecked)0x4945d1, - (Api::AssignFunctor)nullptr, + (Api::AssignFunctor)0x4d0f8e, }, }}; // clang-format on diff --git a/mss32/src/trainingcampinterf.cpp b/mss32/src/trainingcampinterf.cpp new file mode 100644 index 00000000..cbe00d89 --- /dev/null +++ b/mss32/src/trainingcampinterf.cpp @@ -0,0 +1,31 @@ +/* + * This file is part of the modding toolset for Disciples 2. + * (https://github.com/VladimirMakeev/D2ModdingToolset) + * Copyright (C) 2024 Vladimir Makeev. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "trainingcampinterf.h" + +namespace game::editor::CTrainingCampInterfApi { + +Api& get() +{ + static Api api{(Api::CreateButtonFunctor)0x448baa}; + + return api; +} + +} // namespace game::editor::CTrainingCampInterfApi diff --git a/mss32/src/uimanager.cpp b/mss32/src/uimanager.cpp index d73c1244..23a22025 100644 --- a/mss32/src/uimanager.cpp +++ b/mss32/src/uimanager.cpp @@ -24,7 +24,7 @@ namespace game::CUIManagerApi { // clang-format off -static std::array functions = {{ +static std::array functions = {{ // Akella Api{ (Api::Get)0x561afc, @@ -76,6 +76,23 @@ static std::array functions = {{ (Api::GetMousePosition)0x5613ec, (Api::RegisterMessage)0x5613b9, }, + // Scenario Editor + Api{ + (Api::Get)0x486dd7, + (Api::CreateTimerEventFunctor)0, + (Api::CreateTimerEvent)0, + (Api::CreateUpdateEventFunctor)0, + (Api::CreateUiEvent)0, + (Api::CreateUiEvent)0, + (Api::CreateUiEvent)0, + (Api::CreateUiEvent)0, + (Api::CreateUiEvent)0, + (Api::CreateUiEvent)0, + (Api::CreateMessageEventFunctor)0, + (Api::CreateMessageEvent)0, + (Api::GetMousePosition)0, + (Api::RegisterMessage)0, + }, }}; // clang-format on diff --git a/mss32/src/unitutils.cpp b/mss32/src/unitutils.cpp index ff8c9f3b..dbe11a79 100644 --- a/mss32/src/unitutils.cpp +++ b/mss32/src/unitutils.cpp @@ -242,6 +242,10 @@ game::IAttack* getAttack(const game::IUsUnit* unit, bool primary, bool checkAltA game::IAttack* getAltAttack(const game::IUsUnit* unit, bool primary) { auto attack = getAttack(unit, primary, false); + if (!attack) { + return nullptr; + } + auto altAttack = getGlobalAttack(attack->vftable->getAltAttackId(attack)); if (!altAttack) { return nullptr; @@ -360,24 +364,23 @@ static int getLordRegenBonus(const game::CMidPlayer* player) { using namespace game; - const auto& fn = gameFunctions(); - const auto& globalApi = GlobalDataApi::get(); - - const auto globalData = *globalApi.getGlobalData(); - const auto vars = *globalData->globalVariables; - if (!player || player->capturedById != emptyId) { return 0; } - if (fn.isRaceCategoryUnplayable(&player->raceType->data->raceType)) { + if (gameFunctions().isRaceCategoryUnplayable(&player->raceType->data->raceType)) { return 0; } + const auto& globalApi = GlobalDataApi::get(); + + const auto globalData = *globalApi.getGlobalData(); + const auto vars = globalData->globalVariables; + const auto lords = globalData->lords; const auto lordType = (const TLordType*)globalApi.findById(lords, &player->lordId); if (lordType->data->lordCategory.id == LordCategories::get().warrior->id) { - return vars->fighterLeaderRegen; + return vars->data->fighterLeaderRegen; } return 0; @@ -434,7 +437,7 @@ int getUnitRegen(const game::IMidgardObjectMap* objectMap, const game::CMidgardI const auto& globalApi = GlobalDataApi::get(); const auto globalData = *globalApi.getGlobalData(); - const auto vars = *globalData->globalVariables; + const auto vars = globalData->globalVariables; const CMidPlayer* player = nullptr; const CFortification* fort = nullptr; @@ -464,7 +467,7 @@ int getUnitRegen(const game::IMidgardObjectMap* objectMap, const game::CMidgardI result = getFortRegen(result, objectMap, fort); } else if (ruin) { // Units in ruins have fixed regen value, no other factors apply - result = vars->regenRuin; + result = vars->data->regenRuin; } else { // Terrain bonus apply only outside result += getTerrainRegenBonus(objectMap, player, stack); diff --git a/mss32/src/utils.cpp b/mss32/src/utils.cpp index 80dc58ec..ff6af68e 100644 --- a/mss32/src/utils.cpp +++ b/mss32/src/utils.cpp @@ -27,6 +27,8 @@ #include "midmsgboxbuttonhandlerstd.h" #include "midscenvariables.h" #include "smartptr.h" +#include "mquikernelsimple.h" +#include "sounds.h" #include "uimanager.h" #include #include @@ -89,6 +91,12 @@ const std::filesystem::path& exportsFolder() return folder; } +const std::filesystem::path& scenDataFolder() +{ + static const std::filesystem::path folder{gameFolder() / "ScenData"}; + return folder; +} + const std::filesystem::path& exePath() { static std::filesystem::path exe{}; @@ -216,7 +224,26 @@ bool readUserSelectedFile(std::string& contents, const char* filter, const char* ofn.lpstrFileTitle = NULL; ofn.Flags = OFN_PATHMUSTEXIST | OFN_FILEMUSTEXIST; - ofn.hwndOwner = FindWindowEx(nullptr, nullptr, "MQ_UIManager", nullptr); + // Handle DisciplesGL wrapper window presence. + // Wrapper creates its own window for the fullscreen mode + // and it should be used as owner for open dialog box to be displayed correctly. + static const char disciplesGlClassName[] = "7903f211-51ca-4a51-9ec5-e1301db2d24d"; + HWND disciplesGlWindow = FindWindowEx(nullptr, nullptr, disciplesGlClassName, nullptr); + if (disciplesGlWindow) { + ofn.hwndOwner = disciplesGlWindow; + } else { + // Use original game window handle if there is no wrapper + // or wrapper works in a windowed mode. + using namespace game; + + UIManagerPtr manager; + CUIManagerApi::get().get(&manager); + const auto* kernel = manager.data->data->uiKernel; + + ofn.hwndOwner = kernel->vftable->getWindowHandle(kernel); + + SmartPointerApi::get().createOrFree((game::SmartPointer*)&manager, nullptr); + } if (!GetOpenFileName(&ofn)) { return false; @@ -407,7 +434,7 @@ bool computeHash(const std::filesystem::path& folder, std::string& hash) return true; } -void forEachScenarioObject(game::IMidgardObjectMap* objectMap, +void forEachScenarioObject(const game::IMidgardObjectMap* objectMap, game::IdType idType, const std::function& func) { @@ -437,4 +464,20 @@ void forEachScenarioObject(game::IMidgardObjectMap* objectMap, free((SmartPointer*)&end, nullptr); } +void playSoundEffect(game::SoundEffect effect) +{ + using namespace game; + + const auto& api{SoundsApi::get()}; + + SmartPtr sounds{}; + api.instance(&sounds); + + SmartPointer functor{}; + api.playSound(sounds.data, effect, -1, &functor); + + SmartPointerApi::get().createOrFreeNoDtor(&functor, nullptr); + api.soundsPtrSetData(&sounds, nullptr); +} + } // namespace hooks diff --git a/mss32/src/visitorcreatesite.cpp b/mss32/src/visitorcreatesite.cpp new file mode 100644 index 00000000..27277928 --- /dev/null +++ b/mss32/src/visitorcreatesite.cpp @@ -0,0 +1,36 @@ +/* + * This file is part of the modding toolset for Disciples 2. + * (https://github.com/VladimirMakeev/D2ModdingToolset) + * Copyright (C) 2024 Vladimir Makeev. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "visitorcreatesite.h" + +namespace game::editor::CVisitorCreateSiteApi { + +Api& get() +{ + // clang-format off + static Api api{ + (Api::CanApply)0x5110b4, + (Api::Apply)0x51155e, + }; + // clang-format on + + return api; +} + +} // namespace game::editor::CVisitorCreateSiteApi diff --git a/mss32/src/visitorcreatesitehooks.cpp b/mss32/src/visitorcreatesitehooks.cpp new file mode 100644 index 00000000..e75d5405 --- /dev/null +++ b/mss32/src/visitorcreatesitehooks.cpp @@ -0,0 +1,143 @@ +/* + * This file is part of the modding toolset for Disciples 2. + * (https://github.com/VladimirMakeev/D2ModdingToolset) + * Copyright (C) 2024 Vladimir Makeev. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "visitorcreatesitehooks.h" +#include "editor.h" +#include "gameutils.h" +#include "itemcategory.h" +#include "mempool.h" +#include "midgardobjectmap.h" +#include "midgardplan.h" +#include "midsitemage.h" +#include "midsitemerchant.h" +#include "midsitemercs.h" +#include "midsiteresourcemarket.h" +#include "midsitetrainer.h" +#include "sitecategoryhooks.h" +#include "visitorcreatesite.h" + +namespace hooks { + +bool __fastcall visitorCreateSiteCanApplyHooked(const game::editor::CVisitorCreateSite* thisptr, + int /* %edx */) +{ + using namespace game; + + const CMidgardMap* map = getMidgardMap(thisptr->objectMap); + if (!editorFunctions.canPlaceSite(&thisptr->position, map, thisptr->objectMap)) { + return false; + } + + const CMidgardPlan* plan = getMidgardPlan(thisptr->objectMap); + if (!CMidgardPlanApi::get().canPlaceSite(&thisptr->position, plan, nullptr)) { + return false; + } + + const auto& categories = SiteCategories::get(); + const auto id = thisptr->category.id; + if (id == categories.merchant->id || id == categories.mageTower->id + || id == categories.mercenaries->id || id == categories.trainer->id) { + return true; + } + + if (customSiteCategories().exists) { + return id == customSiteCategories().resourceMarket.id; + } + + return false; +} + +bool __fastcall visitorCreateSiteApplyHooked(const game::editor::CVisitorCreateSite* thisptr, + int /* %edx */) +{ + using namespace game; + + IMidgardObjectMap* objectMap = thisptr->objectMap; + + CMidgardPlan* plan = getMidgardPlanToChange(objectMap); + CMidgardID siteId; + objectMap->vftable->createScenarioIdIncFreeIndex(objectMap, &siteId, IdType::Site); + + const auto& memory = Memory::get(); + const auto& categories = SiteCategories::get(); + const auto id = thisptr->category.id; + + CMidSite* site = nullptr; + if (id == categories.merchant->id) { + auto merchant = (CMidSiteMerchant*)memory.allocate(sizeof(CMidSiteMerchant)); + + site = CMidSiteMerchantApi::get().constructor(merchant, &siteId); + } else if (id == categories.mageTower->id) { + auto mage = (CMidSiteMage*)memory.allocate(sizeof(CMidSiteMage)); + + site = CMidSiteMageApi::get().constructor(mage, &siteId); + } else if (id == categories.mercenaries->id) { + auto mercs = (CMidSiteMercs*)memory.allocate(sizeof(CMidSiteMercs)); + + site = CMidSiteMercsApi::get().constructor(mercs, &siteId); + } else if (id == categories.trainer->id) { + auto trainer = (CMidSiteTrainer*)memory.allocate(sizeof(CMidSiteTrainer)); + + site = CMidSiteTrainerApi::get().constructor(trainer, &siteId); + } else if (customSiteCategories().exists && id == customSiteCategories().resourceMarket.id) { + site = createResourceMarket(&siteId); + } + + if (!site) { + return false; + } + + if (!objectMap->vftable->insertObject(objectMap, site)) { + return false; + } + + const char* imgIntf = thisptr->interfaceImage.string ? thisptr->interfaceImage.string : ""; + const char* title = thisptr->name.string ? thisptr->name.string : ""; + const char* description = thisptr->description.string ? thisptr->description.string : ""; + + if (!CMidSiteApi::get().setData(site, objectMap, thisptr->imgIso, imgIntf, &thisptr->position, + title, description)) { + return false; + } + + if (!CMidgardPlanApi::get().addMapElement(plan, &site->mapElement, false)) { + return false; + } + + if (id == categories.merchant->id) { + const auto& addBuyCategory = VisitorApi::get().merchantAddBuyCategory; + const auto& items = ItemCategories::get(); + + addBuyCategory(&siteId, items.armor, objectMap, 1); + addBuyCategory(&siteId, items.jewel, objectMap, 1); + addBuyCategory(&siteId, items.weapon, objectMap, 1); + addBuyCategory(&siteId, items.banner, objectMap, 1); + addBuyCategory(&siteId, items.potionBoost, objectMap, 1); + addBuyCategory(&siteId, items.potionHeal, objectMap, 1); + addBuyCategory(&siteId, items.potionRevive, objectMap, 1); + addBuyCategory(&siteId, items.potionPermanent, objectMap, 1); + addBuyCategory(&siteId, items.scroll, objectMap, 1); + addBuyCategory(&siteId, items.wand, objectMap, 1); + addBuyCategory(&siteId, items.valuable, objectMap, 1); + } + + return true; +} + +} // namespace hooks diff --git a/mss32/src/visitors.cpp b/mss32/src/visitors.cpp index f8017515..0354b233 100644 --- a/mss32/src/visitors.cpp +++ b/mss32/src/visitors.cpp @@ -24,7 +24,7 @@ namespace game::VisitorApi { // clang-format off -std::array functions = {{ +std::array functions = {{ // Akella Api{ (Api::ChangeUnitHp)0x5e88f4, @@ -36,6 +36,13 @@ std::array functions = {{ (Api::TransformUnit)0x5e968e, (Api::UndoTransformUnit)0x5e96df, (Api::ExtractUnitFromGroup)0x5e8d72, + (Api::PlayerSetAttitude)nullptr, + (Api::SetStackSrcTemplate)0x5e9ef8, + (Api::MerchantAddBuyCategory)nullptr, + (Api::CreateSite)nullptr, + (Api::ChangeSiteInfo)nullptr, + (Api::ChangeSiteImage)nullptr, + (Api::ChangeSiteAiPriority)nullptr, }, // Russobit Api{ @@ -48,6 +55,13 @@ std::array functions = {{ (Api::TransformUnit)0x5e968e, (Api::UndoTransformUnit)0x5e96df, (Api::ExtractUnitFromGroup)0x5e8d72, + (Api::PlayerSetAttitude)nullptr, + (Api::SetStackSrcTemplate)0x5e9ef8, + (Api::MerchantAddBuyCategory)nullptr, + (Api::CreateSite)nullptr, + (Api::ChangeSiteInfo)nullptr, + (Api::ChangeSiteImage)nullptr, + (Api::ChangeSiteAiPriority)nullptr, }, // Gog Api{ @@ -60,7 +74,33 @@ std::array functions = {{ (Api::TransformUnit)0x5e838d, (Api::UndoTransformUnit)0x5e83de, (Api::ExtractUnitFromGroup)0x5e7a71, - } + (Api::PlayerSetAttitude)nullptr, + (Api::SetStackSrcTemplate)0x5e8bf7, + (Api::MerchantAddBuyCategory)nullptr, + (Api::CreateSite)nullptr, + (Api::ChangeSiteInfo)nullptr, + (Api::ChangeSiteImage)nullptr, + (Api::ChangeSiteAiPriority)nullptr, + }, + // Scenario Editor + Api{ + (Api::ChangeUnitHp)0, + (Api::ChangeUnitXp)0, + (Api::UpgradeUnit)0, + (Api::ForceUnitMax)0, + (Api::AddUnitToGroup)0, + (Api::ExchangeItem)0, + (Api::TransformUnit)0, + (Api::UndoTransformUnit)0, + (Api::ExtractUnitFromGroup)0, + (Api::PlayerSetAttitude)0x4e9baa, + (Api::SetStackSrcTemplate)0, + (Api::MerchantAddBuyCategory)0x4eb0df, + (Api::CreateSite)0x4eac6f, + (Api::ChangeSiteInfo)0x4eadb9, + (Api::ChangeSiteImage)0x4eae6a, + (Api::ChangeSiteAiPriority)0x4eaeb8, + }, }}; // clang-format on