render.cpp 51 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561
  1. /*
  2. * render.cpp, part of VCMI engine
  3. *
  4. * Authors: listed in file AUTHORS in main folder
  5. *
  6. * License: GNU General Public License v2.0 or later
  7. * Full text of license available in license.txt file, in main folder
  8. *
  9. */
  10. #include "StdInc.h"
  11. #include "battle/AccessibilityInfo.h"
  12. #include "battle/BattleAttackInfo.h"
  13. #include "battle/CObstacleInstance.h"
  14. #include "battle/IBattleInfoCallback.h"
  15. #include "constants/EntityIdentifiers.h"
  16. #include "mapObjects/CGTownInstance.h"
  17. #include "vcmi/spells/Caster.h"
  18. #include "BAI/v13/hex.h"
  19. #include "BAI/v13/hexactmask.h"
  20. #include "BAI/v13/render.h"
  21. #include "common.h"
  22. #include <algorithm>
  23. #include "schema/v13/constants.h"
  24. #include "schema/v13/types.h"
  25. namespace MMAI::BAI::V13
  26. {
  27. namespace S13 = Schema::V13;
  28. using IHex = Schema::V13::IHex;
  29. using IStack = Schema::V13::IStack;
  30. using ISupplementaryData = Schema::V13::ISupplementaryData;
  31. using GA = Schema::V13::GlobalAttribute;
  32. using HA = Schema::V13::HexAttribute;
  33. using PA = Schema::V13::PlayerAttribute;
  34. using SA = StackAttribute;
  35. using SF1 = StackFlag1;
  36. using SF2 = StackFlag2;
  37. namespace
  38. {
  39. std::string PadLeft(const std::string & input, size_t desiredLength, char paddingChar)
  40. {
  41. std::ostringstream ss;
  42. ss << std::right << std::setfill(paddingChar) << std::setw(desiredLength) << input;
  43. return ss.str();
  44. }
  45. // Plain message overload: expect(cond, "expectation failed");
  46. inline void expect(bool exp, std::string_view message)
  47. {
  48. if(exp)
  49. return;
  50. throw std::runtime_error(std::string(message));
  51. }
  52. template<typename... Args>
  53. inline void expect(bool exp, std::string_view format, const Args &... args)
  54. requires(sizeof...(Args) > 0)
  55. {
  56. if(exp)
  57. return;
  58. boost::format f{std::string(format)};
  59. // Fold expression: expands to (f % arg1, f % arg2, ...)
  60. ((f % args), ...);
  61. throw std::runtime_error(f.str());
  62. }
  63. }
  64. namespace
  65. {
  66. struct Context
  67. {
  68. const CPlayerBattleCallback * battle{};
  69. std::vector<const CStack *> allstacks;
  70. std::array<const CStack *, 7> l_CStacks{};
  71. std::array<const CStack *, 7> r_CStacks{};
  72. std::vector<const CStack *> l_CStacksAll;
  73. std::vector<const CStack *> r_CStacksAll;
  74. std::vector<const CStack *> l_CStacksExtra;
  75. std::vector<const CStack *> r_CStacksExtra;
  76. std::vector<const CStack *> l_CStacksSummons;
  77. std::vector<const CStack *> r_CStacksSummons;
  78. std::vector<const CStack *> l_CStacksMachines;
  79. std::vector<const CStack *> r_CStacksMachines;
  80. std::array<const CStack *, 165> hexstacks{};
  81. std::map<const CStack *, ReachabilityInfo> rinfos;
  82. };
  83. std::array<const CStack *, 7> getAllStacksForSide(const Context & ctx, bool side)
  84. {
  85. return side ? ctx.r_CStacks : ctx.l_CStacks;
  86. }
  87. // Return (attr == N/A), but after performing some checks
  88. bool isNA(int v, const CStack * stack, const std::string_view attrname)
  89. {
  90. if(v == S13::NULL_VALUE_UNENCODED)
  91. {
  92. expect(!stack, "%s: N/A but stack != nullptr", attrname);
  93. return true;
  94. }
  95. expect(stack, "%s: != N/A but stack = nullptr", attrname);
  96. return false;
  97. };
  98. bool checkReachable(const Context & ctx, BattleHex bh, bool v, const CStack * stack)
  99. {
  100. auto distance = ctx.rinfos.at(stack).distances.at(bh.toInt());
  101. auto canreach = (stack->getMovementRange() >= distance);
  102. // XXX: if v=false, returns true when UNreachable
  103. // if v=true returns true when reachable
  104. return v ? canreach : !canreach;
  105. };
  106. void ensureReachability(const Context & ctx, BattleHex bh, bool v, const CStack * stack, const char * attrname)
  107. {
  108. expect(checkReachable(ctx, bh, v, stack), "%s: (bhex=%d) reachability expected: %d", attrname, bh.toInt(), v);
  109. };
  110. void ensureValueMatch(int have, int want, const std::string_view attrname, const std::string & desc = "")
  111. {
  112. desc.empty() ? expect(have == want, "%s: have: %d, want: %d", attrname, have, want)
  113. : expect(have == want, "%s: have: %d, want: %d (%s)", attrname, have, want, desc.c_str());
  114. };
  115. void ensureStackNullOrMatch(HexAttribute a, const CStack * cstack, int have, auto wantfunc, const std::string_view attrname)
  116. {
  117. auto vmax = std::get<3>(S13::HEX_ENCODING.at(EI(a)));
  118. if(isNA(have, cstack, attrname))
  119. return;
  120. int want = wantfunc();
  121. want = std::min(want, vmax);
  122. have = std::min(have, vmax); // this is usually done by the encoder
  123. ensureValueMatch(have, want, attrname);
  124. };
  125. void ensureMeleeability(const Context & ctx, BattleHex bh, HexActMask mask, HexAction ha, const CStack * cstack, const char * attrname)
  126. {
  127. auto mv = mask.test(EI(ha));
  128. // if AMOVE is allowed, we must be able to reach hex
  129. // (no else -- we may still be able to reach it)
  130. if(mv == 1)
  131. ensureReachability(ctx, bh, true, cstack, attrname);
  132. auto r_nbh = bh.cloneInDirection(BattleHex::EDir::RIGHT, false);
  133. auto l_nbh = bh.cloneInDirection(BattleHex::EDir::LEFT, false);
  134. auto nbh = BattleHex{};
  135. switch(ha)
  136. {
  137. case HexAction::AMOVE_TR:
  138. nbh = bh.cloneInDirection(BattleHex::EDir::TOP_RIGHT, false);
  139. break;
  140. case HexAction::AMOVE_R:
  141. nbh = bh.cloneInDirection(BattleHex::EDir::RIGHT, false);
  142. break;
  143. case HexAction::AMOVE_BR:
  144. nbh = bh.cloneInDirection(BattleHex::EDir::BOTTOM_RIGHT, false);
  145. break;
  146. case HexAction::AMOVE_BL:
  147. nbh = bh.cloneInDirection(BattleHex::EDir::BOTTOM_LEFT, false);
  148. break;
  149. case HexAction::AMOVE_L:
  150. nbh = bh.cloneInDirection(BattleHex::EDir::LEFT, false);
  151. break;
  152. case HexAction::AMOVE_TL:
  153. nbh = bh.cloneInDirection(BattleHex::EDir::TOP_LEFT, false);
  154. break;
  155. case HexAction::AMOVE_2TR:
  156. nbh = r_nbh.cloneInDirection(BattleHex::EDir::TOP_RIGHT, false);
  157. break;
  158. case HexAction::AMOVE_2R:
  159. nbh = r_nbh.cloneInDirection(BattleHex::EDir::RIGHT, false);
  160. break;
  161. case HexAction::AMOVE_2BR:
  162. nbh = r_nbh.cloneInDirection(BattleHex::EDir::BOTTOM_RIGHT, false);
  163. break;
  164. case HexAction::AMOVE_2BL:
  165. nbh = l_nbh.cloneInDirection(BattleHex::EDir::BOTTOM_LEFT, false);
  166. break;
  167. case HexAction::AMOVE_2L:
  168. nbh = l_nbh.cloneInDirection(BattleHex::EDir::LEFT, false);
  169. break;
  170. case HexAction::AMOVE_2TL:
  171. nbh = l_nbh.cloneInDirection(BattleHex::EDir::TOP_LEFT, false);
  172. break;
  173. default:
  174. THROW_FORMAT("Unexpected HexAction: %d", EI(ha));
  175. break;
  176. }
  177. auto estacks = getAllStacksForSide(ctx, !EI(cstack->unitSide()));
  178. const auto it = std::ranges::find_if( // NOLINT(readability-qualified-auto)
  179. estacks,
  180. [&nbh](const auto & stack)
  181. {
  182. return stack && stack->coversPos(nbh);
  183. }
  184. );
  185. const auto * estack = it == estacks.end() ? nullptr : *it;
  186. if(mv)
  187. {
  188. expect(estack, "%s: =1 (bhex %d, nbhex %d), but estack is nullptr", attrname, bh.toInt(), nbh.toInt());
  189. // must not pass "nbh" for defender position, as it could be its rear hex
  190. expect(
  191. cstack->isMeleeAttackPossible(cstack, estack, bh),
  192. "%s: =1 (bhex %d, nbhex %d), but VCMI says isMeleeAttackPossible=0",
  193. attrname,
  194. bh.toInt(),
  195. nbh.toInt()
  196. );
  197. }
  198. };
  199. // as opposed to ensureHexShootableOrNA, this hexattr works with a mask
  200. // values are 0 or 1 and this check requires a valid target
  201. void ensureShootability(const Context & ctx, BattleHex bh, int v, const CStack * cstack, const char * attrname)
  202. {
  203. auto canshoot = ctx.battle->battleCanShoot(cstack);
  204. auto estacks = getAllStacksForSide(ctx, !EI(cstack->unitSide()));
  205. const auto it = std::ranges::find_if( // NOLINT(readability-qualified-auto)
  206. estacks,
  207. [&bh](auto estack)
  208. {
  209. return estack && estack->coversPos(bh);
  210. }
  211. );
  212. const auto * estack = it == estacks.end() ? nullptr : *it;
  213. // XXX: the estack on `bh` might be "hidden" from the state
  214. // in which case the mask for shooting will be 0 although
  215. // there IS a stack to shoot on this hex
  216. if(v)
  217. {
  218. expect(estack, "%s: =%d, but estack is nullptr", attrname, bh.toInt());
  219. expect(canshoot, "%s: =%d but canshoot=%d", attrname, v, canshoot);
  220. }
  221. else
  222. {
  223. // stack must be unable to shoot
  224. // OR there must be no target at hex
  225. expect(!canshoot || !estack, "%s: =%d but canshoot=%d and estack is not null", attrname, v, canshoot);
  226. }
  227. };
  228. void ensureCorrectMaskOrNA(const Context & ctx, BattleHex bh, int v, const CStack * cstack, const std::string_view attrname)
  229. {
  230. if(isNA(v, cstack, attrname))
  231. return;
  232. auto basename = std::string(attrname);
  233. auto mask = HexActMask(v);
  234. ensureReachability(ctx, bh, mask.test(EI(HexAction::MOVE)), cstack, (basename + "{MOVE}").c_str());
  235. ensureShootability(ctx, bh, mask.test(EI(HexAction::SHOOT)), cstack, (basename + "{SHOOT}").c_str());
  236. ensureMeleeability(ctx, bh, mask, HexAction::AMOVE_TR, cstack, (basename + "{AMOVE_TR}").c_str());
  237. ensureMeleeability(ctx, bh, mask, HexAction::AMOVE_R, cstack, (basename + "{AMOVE_R}").c_str());
  238. ensureMeleeability(ctx, bh, mask, HexAction::AMOVE_BR, cstack, (basename + "{AMOVE_BR}").c_str());
  239. ensureMeleeability(ctx, bh, mask, HexAction::AMOVE_BL, cstack, (basename + "{AMOVE_BL}").c_str());
  240. ensureMeleeability(ctx, bh, mask, HexAction::AMOVE_L, cstack, (basename + "{AMOVE_L}").c_str());
  241. ensureMeleeability(ctx, bh, mask, HexAction::AMOVE_TL, cstack, (basename + "{AMOVE_TL}").c_str());
  242. ensureMeleeability(ctx, bh, mask, HexAction::AMOVE_2TR, cstack, (basename + "{AMOVE_2TR}").c_str());
  243. ensureMeleeability(ctx, bh, mask, HexAction::AMOVE_2R, cstack, (basename + "{AMOVE_2R}").c_str());
  244. ensureMeleeability(ctx, bh, mask, HexAction::AMOVE_2BR, cstack, (basename + "{AMOVE_2BR}").c_str());
  245. ensureMeleeability(ctx, bh, mask, HexAction::AMOVE_2BL, cstack, (basename + "{AMOVE_2BL}").c_str());
  246. ensureMeleeability(ctx, bh, mask, HexAction::AMOVE_2L, cstack, (basename + "{AMOVE_2L}").c_str());
  247. ensureMeleeability(ctx, bh, mask, HexAction::AMOVE_2TL, cstack, (basename + "{AMOVE_2TL}").c_str());
  248. };
  249. }
  250. // This function used during model development and is never called otherwise
  251. void Verify(const State * state) // NOSONAR - function used for debugging only
  252. {
  253. const auto * battle = state->battle;
  254. auto hexes = Hexes();
  255. const CStack * astack = nullptr;
  256. expect(battle, "no battle to verify");
  257. Context ctx;
  258. ctx.battle = battle;
  259. ctx.allstacks = battle->battleGetStacks();
  260. std::ranges::sort(
  261. ctx.allstacks,
  262. [](const CStack * a, const CStack * b)
  263. {
  264. return a->unitId() < b->unitId();
  265. }
  266. );
  267. for(auto & cstack : ctx.allstacks)
  268. {
  269. if(cstack->unitId() == battle->battleActiveUnit()->unitId())
  270. astack = cstack;
  271. if(cstack->unitSlot() < 0)
  272. {
  273. if(cstack->unitSlot() == SlotID::SUMMONED_SLOT_PLACEHOLDER)
  274. cstack->unitSide() == BattleSide::DEFENDER ? ctx.r_CStacksSummons.push_back(cstack) : ctx.l_CStacksSummons.push_back(cstack);
  275. else if(cstack->unitSlot() == SlotID::WAR_MACHINES_SLOT)
  276. cstack->unitSide() == BattleSide::DEFENDER ? ctx.r_CStacksMachines.push_back(cstack) : ctx.l_CStacksMachines.push_back(cstack);
  277. }
  278. else
  279. {
  280. cstack->unitSide() == BattleSide::DEFENDER ? ctx.r_CStacks.at(cstack->unitSlot()) = cstack : ctx.l_CStacks.at(cstack->unitSlot()) = cstack;
  281. }
  282. ctx.rinfos.try_emplace(cstack, battle->getReachability(cstack));
  283. for(const auto & bh : cstack->getHexes())
  284. {
  285. if(!bh.isAvailable())
  286. continue; // war machines rear hex, arrow towers
  287. expect(!ctx.hexstacks.at(Hex::CalcId(bh)), "hex occupied by multiple stacks?");
  288. ctx.hexstacks.at(Hex::CalcId(bh)) = cstack;
  289. }
  290. }
  291. ctx.l_CStacksAll.insert(ctx.l_CStacksAll.end(), ctx.l_CStacks.begin(), ctx.l_CStacks.end());
  292. ctx.l_CStacksAll.insert(ctx.l_CStacksAll.end(), ctx.l_CStacksSummons.begin(), ctx.l_CStacksSummons.end());
  293. ctx.l_CStacksAll.insert(ctx.l_CStacksAll.end(), ctx.l_CStacksMachines.begin(), ctx.l_CStacksMachines.end());
  294. ctx.r_CStacksAll.insert(ctx.r_CStacksAll.end(), ctx.r_CStacks.begin(), ctx.r_CStacks.end());
  295. ctx.r_CStacksAll.insert(ctx.r_CStacksAll.end(), ctx.r_CStacksSummons.begin(), ctx.r_CStacksSummons.end());
  296. ctx.r_CStacksAll.insert(ctx.r_CStacksAll.end(), ctx.r_CStacksMachines.begin(), ctx.r_CStacksMachines.end());
  297. ctx.l_CStacksExtra.insert(ctx.l_CStacksExtra.end(), ctx.l_CStacksSummons.begin(), ctx.l_CStacksSummons.end());
  298. ctx.l_CStacksExtra.insert(ctx.l_CStacksExtra.end(), ctx.l_CStacksMachines.begin(), ctx.l_CStacksMachines.end());
  299. ctx.r_CStacksExtra.insert(ctx.r_CStacksExtra.end(), ctx.r_CStacksSummons.begin(), ctx.r_CStacksSummons.end());
  300. ctx.r_CStacksExtra.insert(ctx.r_CStacksExtra.end(), ctx.r_CStacksMachines.begin(), ctx.r_CStacksMachines.end());
  301. auto SideStacks = std::map<bool, std::vector<const CStack *> *>{
  302. {false, &ctx.l_CStacksAll},
  303. {true, &ctx.r_CStacksAll}
  304. };
  305. auto ended = state->supdata->ended;
  306. if(!astack)
  307. expect(ended, "astack is NULL, but ended is not true");
  308. else if(ended)
  309. {
  310. // at battle-end, activeStack is usually the ENEMY stack
  311. // XXX: this expect will incorrectly throw if we retreated as a regular action
  312. // (in which case our stack will be active, but we would have lost the battle)
  313. // expect(state->supdata->victory == (astack->getOwner() == battle->battleGetMySide()), "state->supdata->victory is %d, but astack->side=%d and myside=%d", state->supdata->victory, astack->getOwner(), battle->battleGetMySide());
  314. // at battle-end, even regardless of the actual active stack,
  315. // battlefield->astack must be nullptr
  316. expect(!state->battlefield->astack, "ended, but battlefield->astack is not NULL");
  317. expect(state->supdata->getIsBattleEnded(), "ended, but state->supdata->getIsBattleEnded() is false");
  318. }
  319. // XXX: good morale is NOT handled here for simplicity
  320. // See comments in Battlefield::GetQueue how to handle it.
  321. auto tmp = std::vector<battle::Units>{};
  322. battle->battleGetTurnOrder(tmp, S13::STACK_QUEUE_SIZE, 0);
  323. auto queue = std::vector<const battle::Unit *>{};
  324. for(auto & units : tmp)
  325. {
  326. for(auto & unit : units)
  327. {
  328. if(queue.size() < S13::STACK_QUEUE_SIZE)
  329. queue.push_back(unit);
  330. else
  331. break;
  332. }
  333. }
  334. const auto * gstats = state->supdata->getGlobalStats();
  335. auto gmask = GlobalActionMask(gstats->getAttr(GA::ACTION_MASK));
  336. ensureValueMatch(gmask.test(EI(GlobalAction::RETREAT)), false, "GA.ACTION_MASK[RETREAT]");
  337. if(ended)
  338. {
  339. static_assert(EI(Side::LEFT) == EI(BattleSide::ATTACKER));
  340. static_assert(EI(Side::RIGHT) == EI(BattleSide::DEFENDER));
  341. auto fin = battle->battleIsFinished();
  342. // XXX: The logic in battleIsFinished is flawed and returns no value
  343. // (i.e. "not finished") if both sides have units, which can
  344. // happen if the WE some has retreated as a regular action (not via reset).
  345. // ASSERT(fin.has_value(), "ended, but battleIsFinished returns no value?");
  346. if(fin.has_value())
  347. {
  348. // NONE means draw (no units on battlefield) -- our value will be null in this case
  349. (fin == BattleSide::NONE) ? ensureValueMatch(gstats->getAttr(GA::BATTLE_WINNER), S13::NULL_VALUE_UNENCODED, "GA.BATTLE_WINNER (draw)")
  350. : ensureValueMatch(gstats->getAttr(GA::BATTLE_WINNER), EI(fin.value()), "GA.BATTLE_WINNER");
  351. }
  352. else
  353. {
  354. // we have retreated *as an action*
  355. // There seems to be no way to ask vcmi "which side retreated"
  356. }
  357. ensureValueMatch(gstats->getAttr(GA::BATTLE_SIDE_ACTIVE_PLAYER), S13::NULL_VALUE_UNENCODED, "GA.BATTLE_SIDE_ACTIVE_PLAYER");
  358. ensureValueMatch(gmask.test(EI(GlobalAction::WAIT)), false, "GA.ACTION_MASK[WAIT]");
  359. }
  360. else
  361. {
  362. static_assert(EI(Side::LEFT) == EI(BattleSide::LEFT_SIDE));
  363. static_assert(EI(Side::RIGHT) == EI(BattleSide::RIGHT_SIDE));
  364. ASSERT(astack != nullptr, "not ended, but no astack either");
  365. ensureValueMatch(gstats->getAttr(GA::BATTLE_WINNER), S13::NULL_VALUE_UNENCODED, "GA.BATTLE_WINNER (battle ongoing)");
  366. ensureValueMatch(gstats->getAttr(GA::BATTLE_SIDE_ACTIVE_PLAYER), EI(astack->unitSide()), "GA.BATTLE_SIDE_ACTIVE_PLAYER");
  367. ensureValueMatch(gmask.test(EI(GlobalAction::WAIT)), !astack->waitedThisTurn, "GA.ACTION_MASK[WAIT]");
  368. }
  369. auto alogs = state->supdata->getAttackLogs();
  370. for(int ihex = 0; ihex < 165; ihex++)
  371. {
  372. int x = ihex % 15;
  373. int y = ihex / 15;
  374. auto & hex = state->battlefield->hexes->at(y).at(x);
  375. auto bh = hex->bhex;
  376. expect(bh == BattleHex(x + 1, y), "hex->bhex mismatch");
  377. auto ainfo = battle->getAccessibility();
  378. auto aa = ainfo.at(bh.toInt());
  379. for(int i = 0; i < EI(HexAttribute::_count); i++)
  380. {
  381. auto attr = static_cast<HexAttribute>(i);
  382. auto v = hex->attrs.at(i);
  383. const auto * cstack = ctx.hexstacks.at(ihex);
  384. if(cstack)
  385. {
  386. expect(!!hex->stack, "cstack is present, but hex->stack is nullptr");
  387. expect(hex->stack->cstack == cstack, "hex->cstack != cstack");
  388. }
  389. else
  390. {
  391. expect(!hex->stack, "cstack is nullptr, but hex->stack is present");
  392. }
  393. switch(attr)
  394. {
  395. case HA::Y_COORD:
  396. expect(v == y, "HEX.Y_COORD: %d != %d", v, y);
  397. break;
  398. case HA::X_COORD:
  399. expect(v == x, "HEX.X_COORD: %d != %d", v, x);
  400. break;
  401. case HA::STATE_MASK:
  402. {
  403. auto obstacles = battle->battleGetAllObstaclesOnPos(bh, false);
  404. auto anyobstacle = [&obstacles](auto fn)
  405. {
  406. return std::any_of(
  407. obstacles.begin(),
  408. obstacles.end(),
  409. [&fn](const std::shared_ptr<const CObstacleInstance> & obstacle)
  410. {
  411. return fn(obstacle.get());
  412. }
  413. );
  414. };
  415. auto mask = HexStateMask(v);
  416. BattleSide side = astack ? astack->unitSide() : BattleSide::ATTACKER; // XXX: Hex defaults to 0 if there is no astack
  417. if(mask.test(EI(HexState::PASSABLE)))
  418. {
  419. expect(
  420. aa == EAccessibility::ACCESSIBLE || (EI(side) && aa == EAccessibility::GATE),
  421. "HEX.STATE_MASK: PASSABLE bit is set, but accessibility is %d (side: %d)",
  422. EI(aa),
  423. EI(side)
  424. );
  425. }
  426. else
  427. {
  428. if(aa == EAccessibility::OBSTACLE || aa == EAccessibility::ALIVE_STACK)
  429. break;
  430. switch(aa)
  431. {
  432. case EAccessibility::ACCESSIBLE:
  433. throw std::runtime_error("HEX.STATE_MASK: PASSABLE bit not set, but accessibility is ACCESSIBLE");
  434. break;
  435. case EAccessibility::ALIVE_STACK:
  436. case EAccessibility::OBSTACLE:
  437. case EAccessibility::DESTRUCTIBLE_WALL:
  438. case EAccessibility::GATE:
  439. break;
  440. case EAccessibility::UNAVAILABLE:
  441. // only Fort and Boat battles can have unavailable hexes
  442. expect(
  443. battle->battleGetFortifications().wallsHealth > 0 || battle->battleTerrainType() == TerrainId::WATER,
  444. "Found UNAVAILABLE accessibility on non-fort, non-boat battlefield: tertype=%d",
  445. EI(battle->battleTerrainType())
  446. );
  447. break;
  448. case EAccessibility::SIDE_COLUMN:
  449. // side hexes should are not included in the observation
  450. throw std::runtime_error("HEX.STATE_MASK: SIDE_COLUMN accessibility found");
  451. break;
  452. default:
  453. throw std::runtime_error("Unexpected accessibility: " + std::to_string(EI(aa)));
  454. }
  455. }
  456. if(mask.test(EI(HexState::STOPPING)))
  457. {
  458. auto stopping = anyobstacle(std::mem_fn(&CObstacleInstance::stopsMovement));
  459. expect(stopping, "HEX.STATE_MASK: STOPPING bit is set, but no obstacle stops movement");
  460. }
  461. if(mask.test(EI(HexState::DAMAGING_L)))
  462. {
  463. auto damaging = anyobstacle(
  464. [side](const CObstacleInstance * o)
  465. {
  466. if(o->obstacleType == CObstacleInstance::MOAT)
  467. return true;
  468. if(!o->triggersEffects())
  469. return false;
  470. auto s = SpellID(o->ID);
  471. if(s == SpellID::FIRE_WALL)
  472. return true;
  473. if(s != SpellID::LAND_MINE)
  474. return false;
  475. const auto * so = dynamic_cast<const SpellCreatedObstacle *>(o);
  476. auto bside = static_cast<bool>(side);
  477. return (side == so->casterSide) ? bside : !bside;
  478. }
  479. );
  480. expect(damaging, "HEX.STATE_MASK: DAMAGING bit is set, but no obstacle triggers a damaging effect");
  481. }
  482. if(mask.test(EI(HexState::DAMAGING_R)))
  483. {
  484. auto damaging = anyobstacle(
  485. [side](const CObstacleInstance * o)
  486. {
  487. if(o->obstacleType == CObstacleInstance::MOAT)
  488. return true;
  489. if(!o->triggersEffects())
  490. return false;
  491. auto s = SpellID(o->ID);
  492. if(s == SpellID::FIRE_WALL)
  493. return true;
  494. if(s != SpellID::LAND_MINE)
  495. return false;
  496. const auto * so = dynamic_cast<const SpellCreatedObstacle *>(o);
  497. auto bside = static_cast<bool>(side);
  498. return (side == so->casterSide) ? !bside : bside;
  499. }
  500. );
  501. expect(damaging, "HEX.STATE_MASK: DAMAGING bit is set, but no obstacle triggers a damaging effect");
  502. }
  503. }
  504. break;
  505. case HA::ACTION_MASK:
  506. {
  507. if(ended)
  508. {
  509. expect(v == 0, "HEX.ACTION_MASK: battle ended, but action mask is %d", v);
  510. }
  511. else
  512. {
  513. ensureCorrectMaskOrNA(ctx, bh, v, astack, "HEX.ACTION_MASK");
  514. }
  515. }
  516. break;
  517. case HA::IS_REAR:
  518. {
  519. ensureValueMatch(v, cstack ? cstack->occupiedHex() == hex->bhex : 0, "HEX.IS_REAR");
  520. }
  521. break;
  522. case HA::STACK_SIDE:
  523. ensureStackNullOrMatch(
  524. attr,
  525. cstack,
  526. v,
  527. [&cstack]
  528. {
  529. return EI(cstack->unitSide());
  530. },
  531. "HA.STACK_SIDE"
  532. );
  533. break;
  534. case HA::STACK_SLOT:
  535. {
  536. if(!cstack)
  537. break;
  538. auto want = static_cast<int>(cstack->unitSlot());
  539. if(want == SlotID::WAR_MACHINES_SLOT)
  540. want = S13::STACK_SLOT_WARMACHINES;
  541. else if(want < 0 || want > 7)
  542. want = S13::STACK_SLOT_SPECIAL;
  543. ensureValueMatch(v, want, "HA.STACK_SLOT");
  544. }
  545. break;
  546. case HA::STACK_QUANTITY:
  547. ensureStackNullOrMatch(
  548. attr,
  549. cstack,
  550. v,
  551. [&cstack]
  552. {
  553. return std::round(S13::STACK_QTY_MAX * static_cast<float>(cstack->getCount()) / S13::STACK_QTY_MAX);
  554. },
  555. "HEX.STACK_QUANTITY"
  556. );
  557. break;
  558. case HA::STACK_ATTACK:
  559. ensureStackNullOrMatch(
  560. attr,
  561. cstack,
  562. v,
  563. [&cstack]
  564. {
  565. return cstack->getAttack(false);
  566. },
  567. "HEX.STACK_ATTACK"
  568. );
  569. break;
  570. case HA::STACK_DEFENSE:
  571. ensureStackNullOrMatch(
  572. attr,
  573. cstack,
  574. v,
  575. [&cstack]
  576. {
  577. return cstack->getDefense(false);
  578. },
  579. "HEX.STACK_DEFENSE"
  580. );
  581. break;
  582. case HA::STACK_SHOTS:
  583. ensureStackNullOrMatch(
  584. attr,
  585. cstack,
  586. v,
  587. [&cstack]
  588. {
  589. return cstack->shots.available();
  590. },
  591. "HEX.STACK_SHOTS"
  592. );
  593. break;
  594. case HA::STACK_DMG_MIN:
  595. ensureStackNullOrMatch(
  596. attr,
  597. cstack,
  598. v,
  599. [&cstack]
  600. {
  601. return cstack->getMinDamage(false);
  602. },
  603. "HEX.STACK_DMG_MIN"
  604. );
  605. break;
  606. case HA::STACK_DMG_MAX:
  607. ensureStackNullOrMatch(
  608. attr,
  609. cstack,
  610. v,
  611. [&cstack]
  612. {
  613. return cstack->getMaxDamage(false);
  614. },
  615. "HEX.STACK_DMG_MAX"
  616. );
  617. break;
  618. case HA::STACK_HP:
  619. ensureStackNullOrMatch(
  620. attr,
  621. cstack,
  622. v,
  623. [&cstack]
  624. {
  625. return cstack->getMaxHealth();
  626. },
  627. "HEX.STACK_HP"
  628. );
  629. break;
  630. case HA::STACK_HP_LEFT:
  631. ensureStackNullOrMatch(
  632. attr,
  633. cstack,
  634. v,
  635. [&cstack]
  636. {
  637. return cstack->getFirstHPleft();
  638. },
  639. "HEX.STACK_VALUE_REL"
  640. );
  641. break;
  642. case HA::STACK_SPEED:
  643. ensureStackNullOrMatch(
  644. attr,
  645. cstack,
  646. v,
  647. [&cstack]
  648. {
  649. return cstack->getMovementRange();
  650. },
  651. "HEX.STACK_SPEED"
  652. );
  653. break;
  654. case HA::STACK_QUEUE:
  655. {
  656. // at battle end, queue is messed up
  657. // (the stack that dealt the killing blow is still "active", but not on 0 pos)
  658. if(ended || !cstack)
  659. break;
  660. auto qbits = std::bitset<S13::STACK_QUEUE_SIZE>(v);
  661. for(int n = 0; n < S13::STACK_QUEUE_SIZE; ++n)
  662. {
  663. int have = qbits.test(n);
  664. int want = (queue.at(n) == cstack);
  665. ensureValueMatch(have, want, ("HEX.STACK_QUEUE[" + std::to_string(n) + "]"));
  666. }
  667. }
  668. break;
  669. case HA::STACK_VALUE_ONE:
  670. {
  671. ensureStackNullOrMatch(
  672. attr,
  673. cstack,
  674. v,
  675. [&cstack]
  676. {
  677. return Stack::GetValue(cstack->unitType());
  678. },
  679. "HEX.STACK_VALUE_ONE"
  680. );
  681. }
  682. break;
  683. case HA::STACK_VALUE_REL:
  684. {
  685. int tot = 0;
  686. for(auto & s : ctx.allstacks)
  687. tot += s->getCount() * Stack::GetValue(s->unitType());
  688. ensureStackNullOrMatch(
  689. attr,
  690. cstack,
  691. v,
  692. [&cstack, &tot]
  693. {
  694. return 1000LL * cstack->getCount() * Stack::GetValue(cstack->unitType()) / tot;
  695. },
  696. "HEX.STACK_VALUE_REL"
  697. );
  698. }
  699. break;
  700. // These require historical information
  701. // (CPlayerCallback does not provide such)
  702. case HA::STACK_VALUE_REL0:
  703. case HA::STACK_VALUE_KILLED_REL:
  704. case HA::STACK_VALUE_KILLED_ACC_REL0:
  705. case HA::STACK_VALUE_LOST_REL:
  706. case HA::STACK_VALUE_LOST_ACC_REL0:
  707. case HA::STACK_DMG_DEALT_REL:
  708. case HA::STACK_DMG_DEALT_ACC_REL0:
  709. case HA::STACK_DMG_RECEIVED_REL:
  710. case HA::STACK_DMG_RECEIVED_ACC_REL0:
  711. break;
  712. case HA::STACK_FLAGS1:
  713. {
  714. if(isNA(v, cstack, "HEX.STACK_FLAGS"))
  715. break;
  716. for(int j = 0; j < EI(StackFlag1::_count); j++)
  717. {
  718. auto f = static_cast<StackFlag1>(j);
  719. auto vf = hex->stack->flag(f);
  720. switch(f)
  721. {
  722. case SF1::IS_ACTIVE:
  723. // at battle end, queue is messed up
  724. // (the stack that dealt the killing blow is still "active", but not on 0 pos)
  725. if(ended)
  726. break;
  727. if(vf == 0)
  728. expect(cstack != astack, "HEX.STACK_FLAGS1.IS_ACTIVE: =0 but cstack == astack");
  729. else
  730. expect(cstack == astack, "HEX.STACK_FLAGS1.IS_ACTIVE: =%d but cstack != astack", vf);
  731. break;
  732. case SF1::WILL_ACT:
  733. ensureValueMatch(vf, cstack->willMove(), "HEX.STACK_FLAGS1.WILL_ACT");
  734. break;
  735. case SF1::CAN_WAIT:
  736. ensureValueMatch(vf, cstack->willMove() && !cstack->waitedThisTurn, "HEX.STACK_FLAGS1.CAN_WAIT");
  737. break;
  738. case SF1::CAN_RETALIATE:
  739. // XXX: ableToRetaliate() calls CAmmo's (i.e. CRetaliations's) canUse() method
  740. // which takes into account relevant bonuses (e.g. NO_RETALIATION from expert Blind)
  741. // It does *NOT* take into account the attacker's BLOCKS_RETALIATION bonus
  742. // (if it did, what would be the correct CAN_RETALIATE value for friendly units?)
  743. ensureValueMatch(vf, cstack->ableToRetaliate(), "HEX.STACK_FLAGS1.CAN_RETALIATE");
  744. break;
  745. case SF1::SLEEPING:
  746. cstack->unitType()->getId() == CreatureID::AMMO_CART
  747. ? ensureValueMatch(vf, false, "HEX.STACK_FLAGS1.SLEEPING")
  748. : ensureValueMatch(vf, cstack->hasBonusOfType(BonusType::NOT_ACTIVE), "HEX.STACK_FLAGS1.SLEEPING");
  749. break;
  750. case SF1::BLOCKED:
  751. ensureValueMatch(vf, cstack->canShoot() && battle->battleIsUnitBlocked(cstack), "HEX.STACK_FLAGS1.TWO_HEX_ATTACK_BREATH");
  752. break;
  753. case SF1::BLOCKING:
  754. {
  755. auto adjUnits = battle->battleAdjacentUnits(cstack);
  756. bool want = std::ranges::any_of(
  757. adjUnits,
  758. [&battle, &cstack](const auto & adjstack)
  759. {
  760. return adjstack->unitSide() != cstack->unitSide() && adjstack->canShoot() && battle->battleIsUnitBlocked(adjstack)
  761. && !adjstack->hasBonusOfType(BonusType::FREE_SHOOTING) && !adjstack->hasBonusOfType(BonusType::SIEGE_WEAPON);
  762. }
  763. );
  764. ensureValueMatch(vf, want, "HEX.STACK_FLAGS1.BLOCKING");
  765. }
  766. break;
  767. case SF1::IS_WIDE:
  768. ensureValueMatch(vf, cstack->occupiedHex().isAvailable(), "HEX.STACK_FLAGS1.IS_WIDE");
  769. break;
  770. case SF1::FLYING:
  771. ensureValueMatch(vf, cstack->hasBonusOfType(BonusType::FLYING), "HEX.STACK_FLAGS1.FLYING");
  772. break;
  773. case SF1::ADDITIONAL_ATTACK:
  774. ensureValueMatch(vf, cstack->hasBonusOfType(BonusType::ADDITIONAL_ATTACK), "HEX.STACK_FLAGS1.ADDITIONAL_ATTACK");
  775. break;
  776. case SF1::NO_MELEE_PENALTY:
  777. ensureValueMatch(vf, cstack->hasBonusOfType(BonusType::NO_MELEE_PENALTY), "HEX.STACK_FLAGS1.NO_MELEE_PENALTY");
  778. break;
  779. case SF1::TWO_HEX_ATTACK_BREATH:
  780. ensureValueMatch(vf, cstack->hasBonusOfType(BonusType::TWO_HEX_ATTACK_BREATH), "HEX.STACK_FLAGS1.TWO_HEX_ATTACK_BREATH");
  781. break;
  782. case SF1::BLOCKS_RETALIATION:
  783. ensureValueMatch(vf, cstack->hasBonusOfType(BonusType::BLOCKS_RETALIATION), "HEX.STACK_FLAGS1.BLOCKS_RETALIATION");
  784. break;
  785. case SF1::SHOOTER:
  786. ensureValueMatch(vf, cstack->hasBonusOfType(BonusType::SHOOTER), "HEX.STACK_FLAGS1.SHOOTER");
  787. // canShoot returns false if ammo = 0
  788. // ensureValueMatch(vf, cstack->canShoot(), "HEX.STACK_FLAGS1.SHOOTER (canShoot)");
  789. break;
  790. case SF1::NON_LIVING:
  791. {
  792. auto undead = cstack->hasBonusOfType(BonusType::UNDEAD);
  793. auto nonliving = cstack->hasBonusOfType(BonusType::NON_LIVING);
  794. ensureValueMatch(vf, undead || nonliving, "HEX.STACK_FLAGS1.NON_LIVING", cstack->getDescription());
  795. }
  796. break;
  797. case SF1::WAR_MACHINE:
  798. ensureValueMatch(vf, cstack->hasBonusOfType(BonusType::SIEGE_WEAPON), "HEX.STACK_FLAGS1.WAR_MACHINE");
  799. break;
  800. case SF1::FIREBALL:
  801. ensureValueMatch(
  802. vf, cstack->hasBonusOfType(BonusType::SPELL_LIKE_ATTACK, SpellID(SpellID::FIREBALL)), "HEX.STACK_FLAGS1.FIREBALL"
  803. );
  804. break;
  805. case SF1::DEATH_CLOUD:
  806. ensureValueMatch(
  807. vf, cstack->hasBonusOfType(BonusType::SPELL_LIKE_ATTACK, SpellID(SpellID::DEATH_CLOUD)), "HEX.STACK_FLAGS1.DEATH_CLOUD"
  808. );
  809. break;
  810. case SF1::THREE_HEADED_ATTACK:
  811. ensureValueMatch(vf, cstack->hasBonusOfType(BonusType::THREE_HEADED_ATTACK), "HEX.STACK_FLAGS1.THREE_HEADED_ATTACK");
  812. break;
  813. case SF1::ALL_AROUND_ATTACK:
  814. ensureValueMatch(vf, cstack->hasBonusOfType(BonusType::ATTACKS_ALL_ADJACENT), "HEX.STACK_FLAGS1.ALL_AROUND_ATTACK");
  815. break;
  816. case SF1::RETURN_AFTER_STRIKE:
  817. ensureValueMatch(vf, cstack->hasBonusOfType(BonusType::RETURN_AFTER_STRIKE), "HEX.STACK_FLAGS1.RETURN_AFTER_STRIKE");
  818. break;
  819. case SF1::ENEMY_DEFENCE_REDUCTION:
  820. ensureValueMatch(vf, cstack->hasBonusOfType(BonusType::ENEMY_DEFENCE_REDUCTION), "HEX.STACK_FLAGS1.ENEMY_DEFENCE_REDUCTION");
  821. break;
  822. case SF1::LIFE_DRAIN:
  823. ensureValueMatch(vf, cstack->hasBonusOfType(BonusType::LIFE_DRAIN), "HEX.STACK_FLAGS1.LIFE_DRAIN");
  824. break;
  825. case SF1::DOUBLE_DAMAGE_CHANCE:
  826. ensureValueMatch(vf, cstack->hasBonusOfType(BonusType::DOUBLE_DAMAGE_CHANCE), "HEX.STACK_FLAGS1.DOUBLE_DAMAGE_CHANCE");
  827. break;
  828. case SF1::DEATH_STARE:
  829. ensureValueMatch(vf, cstack->hasBonusOfType(BonusType::DEATH_STARE), "HEX.STACK_FLAGS1.DEATH_STARE");
  830. break;
  831. default:
  832. THROW_FORMAT("Unexpected StackFlag: %d", EI(f));
  833. }
  834. }
  835. }
  836. break;
  837. case HA::STACK_FLAGS2:
  838. {
  839. if(!isNA(v, cstack, "HEX.STACK_FLAGS"))
  840. {
  841. for(int j = 0; j < EI(StackFlag2::_count); j++)
  842. {
  843. auto f = static_cast<StackFlag2>(j);
  844. auto vf = hex->stack->flag(f);
  845. switch(f)
  846. {
  847. case SF2::AGE:
  848. ensureValueMatch(vf, cstack->hasBonusFrom(BonusSource::SPELL_EFFECT, SpellID(SpellID::AGE)), "HEX.STACK_FLAGS2.AGE");
  849. break;
  850. case SF2::AGE_ATTACK:
  851. ensureValueMatch(
  852. vf, cstack->hasBonusOfType(BonusType::SPELL_AFTER_ATTACK, SpellID(SpellID::AGE)), "HEX.STACK_FLAGS2.AGE_ATTACK"
  853. );
  854. break;
  855. case SF2::BIND:
  856. ensureValueMatch(vf, cstack->hasBonusFrom(BonusSource::SPELL_EFFECT, SpellID(SpellID::BIND)), "HEX.STACK_FLAGS2.BIND");
  857. break;
  858. case SF2::BIND_ATTACK:
  859. ensureValueMatch(
  860. vf, cstack->hasBonusOfType(BonusType::SPELL_AFTER_ATTACK, SpellID(SpellID::BIND)), "HEX.STACK_FLAGS2.BIND_ATTACK"
  861. );
  862. break;
  863. case SF2::BLIND:
  864. {
  865. auto blind = cstack->hasBonusFrom(BonusSource::SPELL_EFFECT, SpellID(SpellID::BLIND));
  866. auto paralyzed = cstack->hasBonusFrom(BonusSource::SPELL_EFFECT, SpellID(SpellID::PARALYZE));
  867. ensureValueMatch(vf, blind || paralyzed, "HEX.STACK_FLAGS2.BLIND");
  868. }
  869. break;
  870. case SF2::BLIND_ATTACK:
  871. {
  872. auto blind = cstack->hasBonusOfType(BonusType::SPELL_AFTER_ATTACK, SpellID(SpellID::BLIND));
  873. auto paralyze = cstack->hasBonusOfType(BonusType::SPELL_AFTER_ATTACK, SpellID(SpellID::PARALYZE));
  874. ensureValueMatch(vf, blind || paralyze, "HEX.STACK_FLAGS2.BLIND_ATTACK");
  875. }
  876. break;
  877. case SF2::CURSE:
  878. ensureValueMatch(vf, cstack->hasBonusFrom(BonusSource::SPELL_EFFECT, SpellID(SpellID::CURSE)), "HEX.STACK_FLAGS2.CURSE");
  879. break;
  880. case SF2::CURSE_ATTACK:
  881. ensureValueMatch(
  882. vf, cstack->hasBonusOfType(BonusType::SPELL_AFTER_ATTACK, SpellID(SpellID::CURSE)), "HEX.STACK_FLAGS2.CURSE_ATTACK"
  883. );
  884. break;
  885. case SF2::DISPEL_ATTACK:
  886. ensureValueMatch(
  887. vf,
  888. cstack->hasBonusOfType(BonusType::SPELL_AFTER_ATTACK, SpellID(SpellID::DISPEL_HELPFUL_SPELLS)),
  889. "HEX.STACK_FLAGS2.DISPEL_ATTACK"
  890. );
  891. break;
  892. case SF2::PETRIFY:
  893. ensureValueMatch(
  894. vf, cstack->hasBonusFrom(BonusSource::SPELL_EFFECT, SpellID(SpellID::STONE_GAZE)), "HEX.STACK_FLAGS2.PETRIFY"
  895. );
  896. break;
  897. case SF2::PETRIFY_ATTACK:
  898. ensureValueMatch(
  899. vf,
  900. cstack->hasBonusOfType(BonusType::SPELL_AFTER_ATTACK, SpellID(SpellID::STONE_GAZE)),
  901. "HEX.STACK_FLAGS2.PETRIFY_ATTACK"
  902. );
  903. break;
  904. case SF2::POISON:
  905. ensureValueMatch(vf, cstack->hasBonusFrom(BonusSource::SPELL_EFFECT, SpellID(SpellID::POISON)), "HEX.STACK_FLAGS2.POISON");
  906. break;
  907. case SF2::POISON_ATTACK:
  908. ensureValueMatch(
  909. vf, cstack->hasBonusOfType(BonusType::SPELL_AFTER_ATTACK, SpellID(SpellID::POISON)), "HEX.STACK_FLAGS2.POISON_ATTACK"
  910. );
  911. break;
  912. case SF2::WEAKNESS:
  913. ensureValueMatch(
  914. vf, cstack->hasBonusFrom(BonusSource::SPELL_EFFECT, SpellID(SpellID::WEAKNESS)), "HEX.STACK_FLAGS2.WEAKNESS"
  915. );
  916. break;
  917. case SF2::WEAKNESS_ATTACK:
  918. ensureValueMatch(
  919. vf,
  920. cstack->hasBonusOfType(BonusType::SPELL_AFTER_ATTACK, SpellID(SpellID::WEAKNESS)),
  921. "HEX.STACK_FLAGS2.WEAKNESS_ATTACK"
  922. );
  923. break;
  924. default:
  925. THROW_FORMAT("Unexpected StackFlag2: %d", EI(f));
  926. }
  927. }
  928. }
  929. }
  930. break;
  931. default:
  932. THROW_FORMAT("Unexpected HexAttribute: %d", EI(attr));
  933. }
  934. }
  935. }
  936. // Mask is undefined at battle end
  937. if(ended)
  938. return;
  939. }
  940. // This intentionally uses the IState interface to ensure that
  941. // the schema is properly exposing all needed informaton
  942. std::string Render(const Schema::IState * istate, const Action * action) // NOSONAR - function used for debugging only
  943. {
  944. auto supdata_ = istate->getSupplementaryData();
  945. expect(supdata_.has_value(), "supdata_ holds no value");
  946. expect(supdata_.type() == typeid(const ISupplementaryData *), "supdata_ of unexpected type");
  947. const auto * sup = std::any_cast<const ISupplementaryData *>(supdata_);
  948. expect(sup, "sup holds a nullptr");
  949. const auto * gstats = sup->getGlobalStats();
  950. const auto * lpstats = sup->getLeftPlayerStats();
  951. const auto * rpstats = sup->getRightPlayerStats();
  952. const auto * mystats = gstats->getAttr(GA::BATTLE_SIDE) ? rpstats : lpstats;
  953. auto hexes = sup->getHexes();
  954. auto alogs = sup->getAttackLogs();
  955. const IStack * astack = nullptr;
  956. // find an active hex (i.e. with active stack on it)
  957. for(auto & row : hexes)
  958. {
  959. for(auto & hex : row)
  960. {
  961. const auto * const stack = hex->getStack();
  962. if(stack && stack->getFlag(SF1::IS_ACTIVE))
  963. {
  964. expect(!astack || astack == stack, "two active stacks found");
  965. astack = stack;
  966. }
  967. }
  968. }
  969. auto ended = gstats->getAttr(GA::BATTLE_WINNER) != S13::NULL_VALUE_UNENCODED;
  970. if(!astack && !ended)
  971. logAi->error("could not find an active stack (battle has not ended).");
  972. std::string nocol = "\033[0m";
  973. std::string redcol = "\033[31m"; // red
  974. std::string bluecol = "\033[34m"; // blue
  975. std::string darkcol = "\033[90m";
  976. std::string activemod = "\033[107m\033[7m"; // bold+reversed
  977. std::string ukncol = "\033[7m"; // white
  978. std::vector<std::stringstream> lines;
  979. //
  980. // 1. Add logs table:
  981. //
  982. // #1 attacks #5 for 16 dmg (1 killed)
  983. // #5 attacks #1 for 4 dmg (0 killed)
  984. // ...
  985. //
  986. for(auto & alog : alogs)
  987. {
  988. auto row = std::stringstream();
  989. auto attcol = ukncol;
  990. auto attalias = '?';
  991. auto defcol = ukncol;
  992. auto defalias = '?';
  993. if(alog->getAttacker())
  994. {
  995. attcol = (alog->getAttacker()->getAttr(SA::SIDE) == 0) ? redcol : bluecol;
  996. attalias = alog->getAttacker()->getAlias();
  997. }
  998. if(alog->getDefender())
  999. {
  1000. defcol = (alog->getDefender()->getAttr(SA::SIDE) == 0) ? redcol : bluecol;
  1001. defalias = alog->getDefender()->getAlias();
  1002. }
  1003. row << attcol << "#" << attalias << nocol;
  1004. row << " attacks ";
  1005. row << defcol << "#" << defalias << nocol;
  1006. row << " for " << alog->getDamageDealt() << " dmg";
  1007. row << " (kills: " << alog->getUnitsKilled() << ", value: " << alog->getValueKilled() << " / " << alog->getValueKilledPermille() << "‰)";
  1008. lines.push_back(std::move(row));
  1009. }
  1010. //
  1011. // 2. Build ASCII table
  1012. // (+populate aliveStacks var)
  1013. // NOTE: the contents below look mis-aligned in some editors.
  1014. // In (my) terminal, it all looks correct though.
  1015. //
  1016. // ▕₁▕₂▕₃▕₄▕₅▕₆▕₇▕₈▕₉▕₀▕₁▕₂▕₃▕₄▕₅▕
  1017. // ┃▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔┃
  1018. // ¹┨ 1 ◌ ○ ○ ○ ○ ◌ ◌ ◌ ◌ ◌ ◌ ◌ ◌ 1 ┠¹
  1019. // ²┨ ◌ ○ ○ ○ ○ ○ ○ ◌ ◌ ◌ ◌ ◌ ◌ ◌ ◌ ┠²
  1020. // ³┨ ◌ ○ ○ ○ ○ ○ ○ ◌ ▦ ▦ ◌ ◌ ◌ ◌ ◌ ┠³
  1021. // ⁴┨ ◌ ○ ○ ○ ○ ○ ○ ○ ▦ ▦ ▦ ◌ ◌ ◌ ◌ ┠⁴
  1022. // ⁵┨ 2 ◌ ○ ○ ▦ ▦ ◌ ○ ◌ ◌ ◌ ◌ ◌ ◌ 2 ┠⁵
  1023. // ⁶┨ ◌ ○ ○ ○ ▦ ▦ ◌ ○ ○ ◌ ◌ ◌ ◌ ◌ ◌ ┠⁶
  1024. // ⁷┨ 3 3 ○ ○ ○ ▦ ◌ ○ ○ ◌ ◌ ▦ ◌ ◌ 3 ┠⁷
  1025. // ⁸┨ ◌ ○ ○ ○ ○ ○ ○ ○ ○ ◌ ◌ ▦ ▦ ◌ ◌ ┠⁸
  1026. // ⁹┨ ◌ ○ ○ ○ ○ ○ ○ ○ ◌ ◌ ◌ ◌ ◌ ◌ ◌ ┠⁹
  1027. // ⁰┨ ◌ ○ ○ ○ ○ ○ ○ ○ ◌ ◌ ◌ ◌ ◌ ◌ ◌ ┠⁰
  1028. // ¹┨ 4 ◌ ○ ○ ○ ○ ○ ◌ ◌ ◌ ◌ ◌ ◌ ◌ 4 ┠¹
  1029. // ┃▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁┃
  1030. // ▕¹▕²▕³▕⁴▕⁵▕⁶▕⁷▕⁸▕⁹▕⁰▕¹▕²▕³▕⁴▕⁵▕
  1031. //
  1032. // s=special; can be any number, slot is always 7 (SPECIAL), visualized A,B,C...
  1033. auto tablestartrow = lines.size();
  1034. lines.emplace_back() << " ₀▏₁▏₂▏₃▏₄▏₅▏₆▏₇▏₈▏₉▏₀▏₁▏₂▏₃▏₄";
  1035. lines.emplace_back() << " ┃▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔┃ ";
  1036. static const std::array<std::string, 10> nummap{"₀", "₁", "₂", "₃", "₄", "₅", "₆", "₇", "₈", "₉"};
  1037. bool addspace = true;
  1038. auto seenstacks = std::map<const IStack *, IHex *>{};
  1039. bool divlines = true;
  1040. // y even "▏"
  1041. // y odd "▕"
  1042. for(int y = 0; y < 11; y++)
  1043. {
  1044. for(int x = 0; x < 15; x++)
  1045. {
  1046. auto sym = std::string("?");
  1047. auto & hex = hexes.at(y).at(x);
  1048. const auto * stack = hex->getStack();
  1049. const char * spacer = (y % 2 == 0) ? " " : "";
  1050. auto & row = (x == 0) ? (lines.emplace_back() << nummap.at(y % 10) << "┨" << spacer) : lines.back();
  1051. if(addspace)
  1052. {
  1053. if(divlines && (x != 0))
  1054. {
  1055. row << darkcol << (y % 2 == 0 ? "▏" : "▕") << nocol;
  1056. }
  1057. else
  1058. {
  1059. row << " ";
  1060. }
  1061. }
  1062. addspace = true;
  1063. auto smask = HexStateMask(hex->getAttr(HA::STATE_MASK));
  1064. auto col = nocol;
  1065. // First put symbols based on hex state.
  1066. // If there's a stack on this hex, symbol will be overriden.
  1067. HexStateMask mpass = 1 << EI(HexState::PASSABLE);
  1068. HexStateMask mstop = 1 << EI(HexState::STOPPING);
  1069. HexStateMask mdmgl = 1 << EI(HexState::DAMAGING_L);
  1070. HexStateMask mdmgr = 1 << EI(HexState::DAMAGING_R);
  1071. HexStateMask mdefault = 0; // or mother :)
  1072. std::vector<std::tuple<std::string, std::string, HexStateMask>> symbols{
  1073. {"⨻", bluecol, mpass | mstop | mdmgl},
  1074. {"⨻", redcol, mpass | mstop | mdmgr},
  1075. {"✶", bluecol, mpass | mdmgl },
  1076. {"✶", redcol, mpass | mdmgr },
  1077. {"△", nocol, mpass | mstop },
  1078. {"○", nocol, mpass }, // changed to "◌" if unreachable
  1079. {"◼", nocol, mdefault }
  1080. };
  1081. for(auto & tuple : symbols)
  1082. {
  1083. const auto & [s, c, m] = tuple;
  1084. if((smask & m) == m)
  1085. {
  1086. sym = s;
  1087. col = c;
  1088. break;
  1089. }
  1090. }
  1091. auto amask = HexActMask(hex->getAttr(HA::ACTION_MASK));
  1092. if(col == nocol && !amask.test(EI(HexAction::MOVE)))
  1093. { // || supdata->getIsBattleEnded()
  1094. col = darkcol;
  1095. sym = sym == "○" ? "◌" : sym;
  1096. }
  1097. if(stack)
  1098. {
  1099. auto seen = seenstacks.find(stack) != seenstacks.end();
  1100. // MSVC mandates constexpr `n` here
  1101. constexpr int n = std::get<2>(S13::HEX_ENCODING[EI(HA::STACK_FLAGS1)]);
  1102. auto flags = std::bitset<n>(stack->getAttr(SA::FLAGS1));
  1103. sym = std::string(1, stack->getAlias());
  1104. col = stack->getAttr(SA::SIDE) ? bluecol : redcol;
  1105. if(stack->getAttr(SA::QUEUE) & 1)
  1106. col += activemod;
  1107. if(flags.test(EI(SF1::IS_WIDE)) && !seen)
  1108. {
  1109. if(stack->getAttr(SA::SIDE) == 0)
  1110. {
  1111. sym += "↠";
  1112. addspace = false;
  1113. }
  1114. else if(stack->getAttr(SA::SIDE) == 1 && hex->getAttr(HA::X_COORD) < 14)
  1115. {
  1116. sym += "↞";
  1117. addspace = false;
  1118. }
  1119. }
  1120. if(!seen)
  1121. seenstacks.try_emplace(stack, hex);
  1122. }
  1123. row << col << sym << nocol;
  1124. if(x == 15 - 1)
  1125. {
  1126. row << (y % 2 == 0 ? " " : " ") << "┠" << nummap.at(y % 10);
  1127. }
  1128. }
  1129. }
  1130. lines.emplace_back() << " ┃▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁┃";
  1131. lines.emplace_back() << " ⁰▕¹▕²▕³▕⁴▕⁵▕⁶▕⁷▕⁸▕⁹▕⁰▕¹▕²▕³▕⁴";
  1132. //
  1133. // 3. Add side table stuff
  1134. //
  1135. // ▕₁▕₂▕₃▕₄▕₅▕₆▕₇▕₈▕₉▕₀▕₁▕₂▕₃▕₄▕₅▕
  1136. // ┃▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔┃ Player: RED
  1137. // ₁┨ ○ ○ ○ ○ ○ ◌ ◌ ◌ ◌ ◌ ◌ ◌ ◌ ◌ ◌ ┠₁ Last action:
  1138. // ₂┨ ○ ○ ○ ○ ○ ○ ◌ ◌ ◌ ◌ ◌ ◌ ◌ ◌ ◌ ┠₂ DMG dealt: 0
  1139. // ₃┨ 1 ○ ○ ○ ○ ○ ◌ ◌ ▦ ▦ ◌ ◌ ◌ ◌ 1 ┠₃ Units killed: 0
  1140. // ...
  1141. for(int i = 0; i <= lines.size(); i++)
  1142. {
  1143. std::string name;
  1144. std::string value;
  1145. auto side = gstats->getAttr(GA::BATTLE_SIDE);
  1146. switch(i)
  1147. {
  1148. case 1:
  1149. name = "Player";
  1150. if(ended)
  1151. value = "";
  1152. else
  1153. value = side ? bluecol + "BLUE" + nocol : redcol + "RED" + nocol;
  1154. break;
  1155. case 2:
  1156. name = "Last action";
  1157. value = action ? action->name() + " [" + std::to_string(action->action) + "]" : "";
  1158. break;
  1159. case 3:
  1160. name = "DMG dealt";
  1161. value = boost::str(boost::format("%d (%d since start)") % mystats->getAttr(PA::DMG_DEALT_NOW_ABS) % mystats->getAttr(PA::DMG_DEALT_ACC_ABS));
  1162. break;
  1163. case 4:
  1164. name = "DMG received";
  1165. value =
  1166. boost::str(boost::format("%d (%d since start)") % mystats->getAttr(PA::DMG_RECEIVED_NOW_ABS) % mystats->getAttr(PA::DMG_RECEIVED_ACC_ABS));
  1167. break;
  1168. case 5:
  1169. name = "Value killed";
  1170. value =
  1171. boost::str(boost::format("%d (%d since start)") % mystats->getAttr(PA::VALUE_KILLED_NOW_ABS) % mystats->getAttr(PA::VALUE_KILLED_ACC_ABS));
  1172. break;
  1173. case 6:
  1174. name = "Value lost";
  1175. value = boost::str(boost::format("%d (%d since start)") % mystats->getAttr(PA::VALUE_LOST_NOW_ABS) % mystats->getAttr(PA::VALUE_LOST_ACC_ABS));
  1176. break;
  1177. case 7:
  1178. {
  1179. // XXX: if there's a draw, this text will be incorrect
  1180. auto restext = gstats->getAttr(GA::BATTLE_WINNER) ? (bluecol + "BLUE WINS") : (redcol + "RED WINS");
  1181. name = "Battle result";
  1182. value = ended ? (restext + nocol) : "";
  1183. }
  1184. break;
  1185. case 8:
  1186. name = "Army value (L)";
  1187. value = boost::str(
  1188. boost::format("%d (%.0f‰ of current BF value)") % lpstats->getAttr(PA::ARMY_VALUE_NOW_ABS) % lpstats->getAttr(PA::ARMY_VALUE_NOW_REL)
  1189. );
  1190. break;
  1191. case 9:
  1192. name = "Army value (R)";
  1193. value = boost::str(
  1194. boost::format("%d (%.0f‰ of current BF value)") % rpstats->getAttr(PA::ARMY_VALUE_NOW_ABS) % rpstats->getAttr(PA::ARMY_VALUE_NOW_REL)
  1195. );
  1196. break;
  1197. case 10:
  1198. name = "Current BF value";
  1199. value = boost::str(
  1200. boost::format("%d (%.0f‰ of starting BF value)") % gstats->getAttr(GA::BFIELD_VALUE_NOW_ABS) % gstats->getAttr(GA::BFIELD_VALUE_NOW_REL0)
  1201. );
  1202. break;
  1203. default:
  1204. continue;
  1205. }
  1206. lines.at(tablestartrow + i) << PadLeft(name, 17, ' ') << ": " << value;
  1207. }
  1208. lines.emplace_back() << "";
  1209. //
  1210. // 5. Add stacks table:
  1211. //
  1212. // Stack # | 0 1 2 3 4 5 6 A B C 0 1 2 3 4 5 6 A B C
  1213. // -----------------+--------------------------------------------------------------------------------
  1214. // Qty | 0 34 0 0 0 0 0 0 0 0 0 17 0 0 0 0 0 0 0 0
  1215. // Attack | 0 8 0 0 0 0 0 0 0 0 0 6 0 0 0 0 0 0 0 0
  1216. // ...10 more... | ...
  1217. // -----------------+--------------------------------------------------------------------------------
  1218. //
  1219. // table with 24 columns (1 header, 3 dividers, 10 stacks per side)
  1220. // Each row represents a separate attribute
  1221. using RowDef = std::tuple<StackAttribute, std::string>;
  1222. // max to show
  1223. constexpr int max_stacks_per_side = 10;
  1224. // All cell text is aligned right
  1225. auto colwidths = std::array<int, 4 + (2 * max_stacks_per_side)>{};
  1226. colwidths.fill(5); // default col width
  1227. colwidths.at(0) = 16; // header col
  1228. // Divider column indexes
  1229. auto divcolids = {1, max_stacks_per_side + 2, (2 * max_stacks_per_side) + 3};
  1230. for(int i : divcolids)
  1231. colwidths.at(i) = 2; // divider col
  1232. // {Attribute, name, colwidth}
  1233. const auto rowdefs = std::vector<RowDef>{
  1234. RowDef{SA::FLAGS1, "Stack #" }, // stack alias (1..7, S or M)
  1235. RowDef{SA::SIDE, "" }, // divider row
  1236. RowDef{SA::QUANTITY, "Qty" },
  1237. RowDef{SA::ATTACK, "Attack" },
  1238. RowDef{SA::DEFENSE, "Defense" },
  1239. RowDef{SA::SHOTS, "Shots" },
  1240. RowDef{SA::DMG_MIN, "Dmg (min)" },
  1241. RowDef{SA::DMG_MAX, "Dmg (max)" },
  1242. RowDef{SA::HP, "HP" },
  1243. RowDef{SA::HP_LEFT, "HP left" },
  1244. RowDef{SA::SPEED, "Speed" },
  1245. RowDef{SA::QUEUE, "Queue" },
  1246. RowDef{SA::VALUE_ONE, "Value (one)" },
  1247. RowDef{SA::VALUE_REL, " Value (‰)"}, // manually pad to 16 (unicode length issue)
  1248. // RowDef{SA::ESTIMATED_DMG, "Est. DMG%"},
  1249. RowDef{SA::FLAGS1, "State" }, // "WAR" = CAN_WAIT, WILL_ACT, CAN_RETAL
  1250. RowDef{SA::FLAGS1, "Attack mods" }, // "DB" = Double, Blinding
  1251. // 2 values per column to avoid too long table
  1252. RowDef{SA::FLAGS1, "Blocked/ing" },
  1253. RowDef{SA::FLAGS1, "Fly/Sleep" },
  1254. RowDef{SA::FLAGS1, "NoRetal/NoMelee" },
  1255. RowDef{SA::FLAGS1, "Wide/Breath" },
  1256. RowDef{SA::SIDE, "" }, // divider row
  1257. };
  1258. // Table with nrows and ncells, each cell a 3-element tuple
  1259. // cell: color, width, txt
  1260. using TableCell = std::tuple<std::string, int, std::string>;
  1261. using TableRow = std::array<TableCell, colwidths.size()>;
  1262. auto table = std::vector<TableRow>{};
  1263. auto divrow = TableRow{};
  1264. for(int i = 0; i < colwidths.size(); i++)
  1265. divrow[i] = {nocol, colwidths.at(i), std::string(colwidths.at(i), '-')};
  1266. for(int i : divcolids)
  1267. divrow.at(i) = {nocol, colwidths.at(i), std::string(colwidths.at(i) - 1, '-') + "+"};
  1268. int specialcounter = 0;
  1269. // Attribute rows
  1270. for(const auto & [a, aname] : rowdefs)
  1271. {
  1272. if(a == SA::SIDE)
  1273. { // divider row
  1274. table.push_back(divrow);
  1275. continue;
  1276. }
  1277. auto row = TableRow{};
  1278. // Header col
  1279. row.at(0) = {nocol, colwidths.at(0), aname};
  1280. // Div cols
  1281. for(int i : {1, 2 + max_stacks_per_side, static_cast<int>(colwidths.size() - 1)})
  1282. row.at(i) = {nocol, colwidths.at(i), "|"};
  1283. // Stack cols
  1284. for(auto side : {0, 1})
  1285. {
  1286. auto sidestacks = std::array<std::pair<const IStack *, IHex *>, max_stacks_per_side>{};
  1287. auto extracounter = 0;
  1288. for(const auto & [stack, hex] : seenstacks)
  1289. {
  1290. if(stack->getAttr(SA::SIDE) == side)
  1291. {
  1292. int slot = stack->getAlias() >= '0' && stack->getAlias() <= '6' ? stack->getAlias() - '0' : 7 + extracounter;
  1293. if(slot < max_stacks_per_side)
  1294. sidestacks.at(slot) = {stack, hex};
  1295. if(slot >= 7)
  1296. extracounter += 1;
  1297. }
  1298. }
  1299. for(int i = 0; i < sidestacks.size(); ++i)
  1300. {
  1301. auto & [stack, hex] = sidestacks.at(i);
  1302. auto colid = 2 + i + side + (max_stacks_per_side * side);
  1303. if(!stack)
  1304. {
  1305. row.at(colid) = {nocol, colwidths.at(colid), ""};
  1306. continue;
  1307. }
  1308. std::string value;
  1309. // MSVC mandates constexpr `n` here
  1310. constexpr int n1 = std::get<2>(S13::HEX_ENCODING[EI(HA::STACK_FLAGS1)]);
  1311. constexpr int n2 = std::get<2>(S13::HEX_ENCODING[EI(HA::STACK_FLAGS2)]);
  1312. auto flags1 = std::bitset<n1>(stack->getAttr(SA::FLAGS1));
  1313. auto flags2 = std::bitset<n2>(stack->getAttr(SA::FLAGS2));
  1314. auto color = stack->getAttr(SA::SIDE) ? bluecol : redcol;
  1315. if(a == SA::QUEUE)
  1316. {
  1317. auto qbits = std::bitset<S13::STACK_QUEUE_SIZE>(stack->getAttr(SA::QUEUE));
  1318. for(int n = 0; n < S13::STACK_QUEUE_SIZE; ++n)
  1319. {
  1320. if(qbits.test(n))
  1321. {
  1322. value = std::to_string(n);
  1323. break;
  1324. }
  1325. }
  1326. }
  1327. else if(a == SA::VALUE_ONE && stack->getAttr(a) >= 1000)
  1328. {
  1329. std::ostringstream oss;
  1330. oss << std::fixed << std::setprecision(1) << (stack->getAttr(a) / 1000.0);
  1331. value = oss.str();
  1332. value[value.size() - 2] = 'k';
  1333. if(value.rfind("K0") == (value.size() - 2))
  1334. value.resize(value.size() - 1);
  1335. }
  1336. else if(a == SA::FLAGS1)
  1337. {
  1338. auto fmt = boost::format("%d/%d");
  1339. switch(specialcounter)
  1340. {
  1341. case 0:
  1342. value = std::string(1, stack->getAlias());
  1343. break;
  1344. case 1:
  1345. {
  1346. value = std::string("");
  1347. value += flags1.test(EI(SF1::CAN_WAIT)) ? "W" : " ";
  1348. value += flags1.test(EI(SF1::WILL_ACT)) ? "A" : " ";
  1349. value += flags1.test(EI(SF1::CAN_RETALIATE)) ? "R" : " ";
  1350. }
  1351. break;
  1352. case 2:
  1353. {
  1354. value = std::string("");
  1355. value += flags1.test(EI(SF1::ADDITIONAL_ATTACK)) ? "D" : " ";
  1356. value += flags2.test(EI(SF2::BLIND_ATTACK)) ? "B" : " ";
  1357. }
  1358. break;
  1359. case 3:
  1360. value = boost::str(fmt % flags1.test(EI(SF1::BLOCKED)) % flags1.test(EI(SF1::BLOCKING)));
  1361. break;
  1362. case 4:
  1363. value = boost::str(fmt % flags1.test(EI(SF1::FLYING)) % flags1.test(EI(SF1::SLEEPING)));
  1364. break;
  1365. case 5:
  1366. value = boost::str(fmt % flags1.test(EI(SF1::BLOCKS_RETALIATION)) % flags1.test(EI(SF1::NO_MELEE_PENALTY)));
  1367. break;
  1368. case 6:
  1369. value = boost::str(fmt % flags1.test(EI(SF1::IS_WIDE)) % flags1.test(EI(SF1::TWO_HEX_ATTACK_BREATH)));
  1370. break;
  1371. default:
  1372. THROW_FORMAT("Unexpected specialcounter: %d", specialcounter);
  1373. }
  1374. }
  1375. else
  1376. {
  1377. value = std::to_string(stack->getAttr(a));
  1378. }
  1379. if((stack->getAttr(SA::QUEUE) & 1) && !ended)
  1380. color += activemod;
  1381. row.at(colid) = {color, colwidths.at(colid), value};
  1382. }
  1383. }
  1384. if(a == SA::FLAGS1)
  1385. ++specialcounter;
  1386. table.push_back(row);
  1387. }
  1388. for(auto & r : table)
  1389. {
  1390. auto line = std::stringstream();
  1391. for(auto & [color, width, txt] : r)
  1392. line << color << PadLeft(txt, width, ' ') << nocol;
  1393. lines.push_back(std::move(line));
  1394. }
  1395. //
  1396. // 7. Join rows into a single string
  1397. //
  1398. std::string res = lines[0].str();
  1399. for(int i = 1; i < lines.size(); i++)
  1400. res += "\n" + lines[i].str();
  1401. return res;
  1402. }
  1403. }