Monopoly — Portage moderne 0.1.0
Portage moderne C/C++23 du Monopoly de 1999
Loading...
Searching...
No Matches
Engine.cpp
Go to the documentation of this file.
2#include "Engine.hpp"
3#include "AudioRuntime.hpp"
4#include "Presentation.hpp"
8#include "Messaging.hpp"
9#include "RulesEngine.hpp"
11#include "FontRuntime.hpp"
12#include "EuropeanDeed.hpp"
13#include "UDSoundRuntime.hpp"
14#include "UDPennyVoice.hpp"
15#include "UISound.hpp"
16#include "AIUtility.hpp"
17#include "GPUFrame.hpp"
18#include "LegacyAssets.hpp"
19#include "ModernGltfMesh.hpp"
23#include "ModernEnvironment.hpp"
25#include "ModernIBarSkin.hpp"
26#include "ModernMenuSkin.hpp"
29#include "IBarLayout.hpp"
30#include "MoneyFormat.hpp"
32#include "Timers.hpp"
33#include "UIMessages.hpp"
35#include "Display.hpp"
36#include "BoardRules.hpp"
40#include "RuleBuildings.hpp"
41#include "RuntimeState.hpp"
42#include "SequencePlayback.hpp"
43#include "World3DGPUScene.hpp"
46#include "OpeningMovies.hpp"
48#include "TextureCatalog.hpp"
49#include "PieceMovePlayback.hpp"
50#include "PieceRuntime.hpp"
51#include "PieceJailPlayback.hpp"
52#include "PieceIdlePlayback.hpp"
53#include "PieceIdleDisplay.hpp"
56#include "AuctionPlayback.hpp"
66#include "StatsPlayback.hpp"
69#include "ChatFluffPlayback.hpp"
70#include "StatsBankPlayback.hpp"
78#include "StatsDeedPlayback.hpp"
90#include "TradeNamePlayback.hpp"
95#include "DiceDisplay.hpp"
96#include "IBar.hpp"
99#include "ChatTextPlayback.hpp"
101#include "StatsTextPlayback.hpp"
102#include "PlayerSelection.hpp"
104#include "MousePointer.hpp"
106#include "RuleCards.hpp"
109#include "LocalPlayers.hpp"
110#include "UserInterface.hpp"
111#include "TimeStep.hpp"
112
113#include <SDL3/SDL.h>
114#include <SDL3/SDL_gpu.h>
115
116#include <iostream>
117#include <algorithm>
118#include <cstddef>
119#include <cstdint>
120#include <cstdlib>
121#include <filesystem>
122#include <fstream>
123#include <chrono>
124#include <cstdlib>
125#include <limits>
126#include <memory>
127#include <optional>
128#include <string>
129#include <string_view>
130#include <tuple>
131#include <unordered_set>
132#include <vector>
133
135{
136 void handleAudioPlaybackFailure(std::string_view context,
137 const std::string& error) noexcept;
138
139 namespace
140 {
141 SDL_GPUDevice* gpuDevice = nullptr;
142 SDL_Window* gameWindow = nullptr;
143 std::unique_ptr<SequencePlayback> playback;
149 std::unique_ptr<audio::Runtime> audioRuntime;
150 std::unique_ptr<voicechat::AudioRuntime> voiceChatAudioRuntime;
153 std::string lastVoiceNetworkError;
154 std::unique_ptr<fonts::Runtime> fontRuntime;
155 bool fontDisabled{};
156 using EuropeanDeedSelection = std::tuple<data::LanguageId, int, int, int>;
157 std::optional<EuropeanDeedSelection> europeanDeedSelection;
159 std::vector<sequence::SequenceNodeId> activeSequenceSounds;
160 bool audioDisabled{};
161 std::unordered_set<std::string> reportedAudioFailures;
162 std::array<
163 std::shared_ptr<const data::MeshRenderData>,
165 std::array<bool, data::ModernTokenCount> modernTokenAttempted{};
167 struct StartupCPUProfile
168 {
169 using Clock = std::chrono::steady_clock;
171 Clock::time_point epoch{};
172 void begin()
173 {
174 const auto* value = std::getenv("MONOPOLY_PROFILE_STARTUP");
175 *this = {};
176 enabled = value && std::string_view(value) == "1";
177 if (enabled) epoch = Clock::now();
178 }
179 [[nodiscard]] Clock::time_point start() const
180 { return enabled ? Clock::now() : Clock::time_point{}; }
181 void report(std::string_view phase, Clock::time_point started, std::size_t objects = 0) const
182 {
183 if (!enabled) return;
184 const auto ended = Clock::now();
185 std::cerr << "Startup CPU profile: phase=" << phase
186 << " elapsed_ms=" << std::chrono::duration<double,std::milli>(ended-started).count()
187 << " since_session_ms=" << std::chrono::duration<double,std::milli>(ended-epoch).count()
188 << " objects=" << objects
189 << " scope=CPU elapsed only; no added GPU fence; not completed GPU time\n";
190 }
192 std::array<std::shared_ptr<const data::MeshRenderData>, data::ModernSceneKindCount> modernSceneMeshes{};
193 std::array<bool, data::ModernSceneKindCount> modernSceneAttempted{};
194 std::unique_ptr<data::ModernTokenVariantCache> modernTokenVariants;
195 std::unique_ptr<ModernEnvironment> modernEnvironment;
196 std::unordered_set<std::string> reportedModernAssetFailures;
198 std::optional<std::uint8_t> activePieceMoveToken;
199 std::optional<World3DRenderer> worldRenderer;
200 std::unique_ptr<World2DRenderer> overlayRenderer;
201 std::optional<data::DataId> activeBoardSequence;
202 std::optional<World3DCamera> activeWorldCamera;
203 pieces::PieceMovePlayback pieceMovePlayback;
204 pieces::PieceJailPlayback pieceJailPlayback;
205 pieces::PieceIdlePlayback pieceIdlePlayback;
206 pieces::PieceIdleDisplay pieceIdleDisplay;
207 pieces::PieceBuildingDisplay pieceBuildingDisplay;
208 pieces::PieceShadowDisplay pieceShadowDisplay;
209 auctionui::Playback auctionPlayback;
210 auctionui::TextPlayback auctionTextPlayback;
211 auctionui::PennyBagsPlayback auctionPennyBagsPlayback;
212 tradeui::BackdropPlayback tradeBackdropPlayback;
213 tradeui::TokenPlayback tradeTokenPlayback;
214 tradeui::ActionButtonPlayback tradeActionButtonPlayback;
215 optionsui::FilePlayback optionsFilePlayback;
216 optionsui::SavePlayback optionsSavePlayback;
217 optionsui::CustomBoardPlayback optionsCustomBoardPlayback;
218 ibar::EscapeConfirmationPlayback escapeConfirmationPlayback;
219 optionsui::NavigationPlayback optionsNavigationPlayback;
220 statsui::Playback statsPlayback;
221 chat::RecipientPlayback chatRecipientPlayback;
222 chat::OptionPlayback chatOptionPlayback;
223 chat::FluffPlayback chatFluffPlayback;
224 statsui::BankPlayback statsBankPlayback;
225 statsui::CalculatorPlayback statsCalculatorPlayback;
226 statsui::CalculatorDeedPickerPlayback statsCalculatorDeedPickerPlayback;
227 statsui::PlayerPlayback statsPlayerPlayback;
228 statsui::PlayerCashPlayback statsPlayerCashPlayback;
229 statsui::PlayerAuxPlayback statsPlayerAuxPlayback;
230 statsui::FutureImmunityPlayback statsFutureImmunityPlayback;
231 statsui::FutureImmunityTextPlayback statsFutureImmunityTextPlayback;
232 statsui::DeedPlayback statsDeedPlayback;
233 statsui::DeedFloaterPlayback statsDeedFloaterPlayback;
234 statsui::DeedFloaterTextPlayback statsDeedFloaterTextPlayback;
235 statsui::DeedBarPlayback statsDeedBarPlayback;
236 statsui::DeedValueTextPlayback statsDeedValueTextPlayback;
237 optionsui::OptionPlayback optionsOptionPlayback;
238 optionsui::TogglePlayback optionsTogglePlayback;
239 optionsui::HelpPlayback optionsHelpPlayback;
240 tradeui::PropertyPlayback tradePropertyPlayback;
241 tradeui::OfferIconPlayback tradeOfferIconPlayback;
242 tradeui::CashTextPlayback tradeCashTextPlayback;
243 tradeui::NamePlayback tradeNamePlayback;
244 tradeui::PartnerSelectionPlayback tradePartnerSelectionPlayback;
245 tradeui::PanelTextPlayback tradePanelTextPlayback;
246 tradeui::CashDialogPlayback tradeCashDialogPlayback;
247 tradeui::ContractDialogPlayback tradeContractDialogPlayback;
248 tradeui::ContractTextPlayback tradeContractTextPlayback;
249 boarddisplay::BoardBackdropPlayback boardBackdropPlayback;
250 boarddisplay::OwnershipHighlightPlayback ownershipHighlightPlayback;
251 boarddisplay::BoardLightingController boardLightingController;
252 pieces::TokenPoseTracker lightingTokenPoseTracker;
253 std::uint64_t lastBoardLightingTick{};
254 dice::Playback dicePlayback;
255 dice::TwoDPlayback dice2DPlayback;
256 ibar::BackdropPlayback iBarBackdropPlayback;
257 ibar::ScoreTextPlayback iBarScoreTextPlayback;
258 ibar::RuntimeTextPlayback iBarRuntimeTextPlayback;
259 chat::TextPlayback chatTextPlayback;
260 optionsui::VisualPlayback optionsVisualPlayback;
261 statsui::TextPlayback statsTextPlayback;
262 playerselection::PlayerSelectionPlayback playerSelectionPlayback;
263 mouse::Playback mousePointerPlayback;
264 mouse::NativeCursor nativeMouseCursor;
266 std::optional<statsui::AccountRuntime> statsAccountRuntime;
267 bool diceQueueLockHeld{};
268 std::optional<pieces::PieceIdleTransitionPlan> pendingPieceIdleTransition;
271 std::optional<pieces::PieceMoveSpecialRequest> pendingPieceMoveSpecial;
274
275 struct IBarBSSMAvailability
276 {
281 };
282
283 [[nodiscard]] IBarBSSMAvailability iBarBSSMAvailability(
284 const rules::GameState& state,
285 rules::PlayerNumber player)
286 {
287 IBarBSSMAvailability result{};
288 if (player >= state.numberOfPlayers || player >= rules::MaxPlayers)
289 return result;
290
291 for (std::uint8_t squareNo = 0; squareNo < rules::SquareCount; ++squareNo)
292 {
294 if (bit == 0) continue;
295
297 state, player, squareNo, true).error == 0)
298 result.buildProperties |= bit;
300 state, player, squareNo, false).error == 0)
301 result.sellProperties |= bit;
302
303 const auto& square = state.squares[squareNo];
304 if (square.owner != player) continue;
305 const auto squareType =
308
309 if (!square.mortgaged)
310 {
311 bool groupHasBuildings = false;
312 for (std::uint8_t testNo = 0; testNo < rules::SquareCount; ++testNo)
313 {
314 const auto testType =
315 static_cast<rules::board::SquareType>(testNo);
317 state.squares[testNo].owner == player &&
318 state.squares[testNo].houses != 0)
319 {
320 groupHasBuildings = true;
321 break;
322 }
323 }
325 result.mortgageProperties |= bit;
326 }
327 else
328 {
329 const std::int64_t mortgage = definition.mortgageCost;
330 const std::int64_t fees = mortgage +
331 (mortgage * state.options.interestRate + 50) / 100;
332 if (state.players[player].cash >= fees)
333 result.unmortgageProperties |= bit;
334 }
335 }
336 return result;
337 }
338
339 [[nodiscard]] sequence::Matrix3D boardStartupScale() noexcept
340 {
341 // UDBoard.cpp:998-1002 builds mxScale with SetScale(0.10f) and
342 // immediately applies it through LE_SEQNCR_MoveTheWorks.
343 auto scale = sequence::identity3D();
344 scale.values[0] = 0.10F;
345 scale.values[5] = 0.10F;
346 scale.values[10] = 0.10F;
347 return scale;
348 }
349
350 [[nodiscard]] std::expected<void, std::string> syncBoardPlayback(
351 SequencePlayback& session, const display::State& state)
352 {
353 const bool shouldRun = state.board3DOn;
354 if (!shouldRun)
355 {
356 if (!activeBoardSequence) return {};
357 const auto stopped = session.stop(*activeBoardSequence,
359 if (!stopped) return stopped;
360 activeBoardSequence.reset();
361 return {};
362 }
363
364 // UDBoard.cpp selects medium detail; stock Europe always uses the
365 // classic mesh. Its board/language/currency arrive through textures.
366 const auto resources = session.resources();
367 if (!resources)
368 return std::unexpected("board playback requires a resource snapshot");
369 const auto mesh = state.city == -1
371 : (resources->context().board == data::BoardEdition::Usa
374 // Refresh BEFORE comparing DataId: e.g. USA city 1 -> 2 shares the
375 // city mesh but must publish new immutable CPU/GPU texture assets.
376 const auto textures = session.configureBoardTextures(
378 state.customBoardPath);
379 if (!textures) return textures;
380 const auto desired = data::boardMeshDataId(mesh);
381 if (activeBoardSequence == desired) return {};
383 {
384 const auto stopped = session.stop(*activeBoardSequence,
386 if (!stopped) return stopped;
387 }
388 const auto started = session.startMoved(desired,
390 if (!started) return started;
392 return {};
393 }
394
395 [[nodiscard]] std::expected<void, std::string> syncPieceIdlePlayback(
396 SequencePlayback& session)
397 {
400 pendingPieceIdleTransition = std::move(*plan);
401
403 !pieceMovePlayback.active() && !pieceJailPlayback.active())
404 {
406 auto plan = std::move(*pendingPieceIdleTransition);
408 const auto begun = pieceIdlePlayback.begin(std::move(plan));
409 if (!begun)
410 {
413 return std::unexpected(begun.error());
414 }
415 }
416
417 if (!pieceIdlePlayback.active()) return {};
418 const auto step = pieceIdlePlayback.tick(session);
419 if (!step)
420 {
424 return std::unexpected(step.error());
425 }
426 if (step->completed)
427 {
430 }
431 return {};
432 }
433 [[nodiscard]] std::expected<void, std::string> syncPersistentPieceIdles(
434 SequencePlayback& session, bool boardVisible)
435 {
436 pieces::PieceIdleDisplayContext context{};
437 context.boardVisible = boardVisible;
438 context.animationsEnabled = display::stateReadOnly().optionTokenAnimationsOn;
439
441 if (pieceMovePlayback.active() &&
442 state.currentPlayer < state.numberOfPlayers)
443 context.movingPlayer = state.currentPlayer;
444 context.paddywagonPlayer = pieceJailPlayback.playerInPaddywagon();
445 context.idleMovingOut = pieceIdlePlayback.movingOutPlayer();
446 context.idleMovingIn = pieceIdlePlayback.movingInPlayer();
447
448 const auto synced = pieceIdleDisplay.sync(
450 if (!synced) return std::unexpected(synced.error());
451 return {};
452 }
453
454 [[nodiscard]] std::expected<void, std::string> syncPieceShadows(
455 SequencePlayback& session, const rules::GameState& ruleState,
456 const display::State& displayState, bool boardVisible)
457 {
458 pieces::PieceShadowDisplayContext context{};
459 context.boardVisible = boardVisible;
460 context.lightingOn = displayState.optionLightingOn;
461 context.game3DOn = displayState.game3DOn;
462 context.paddywagonPlayer = pieceJailPlayback.playerInPaddywagon();
463 context.goingToJailStatus = pieceJailPlayback.state();
464 context.currentUIPlayer = ui::localplayers::currentUIPlayer();
465
466 for (rules::PlayerNumber player = 0;
467 player < rules::MaxPlayers; ++player)
468 {
469 const auto idle = pieceIdleDisplay.shownSequence(player);
470 if (idle != data::EmptyDataId)
471 context.runtimeSources.idleSequences[player] = idle;
472 }
473
474 const auto moving = pieceMovePlayback.currentSequence();
475 if (moving != data::EmptyDataId)
476 {
477 context.runtimeSources.movingSequence = moving;
478 context.currentPlayerTokenSequenceActive = true;
479 }
480
481 context.runtimeSources.playerMovingOut = pieceIdlePlayback.movingOutPlayer();
482 const auto movingOut = pieceIdlePlayback.movingOutSequence();
483 if (movingOut != data::EmptyDataId)
484 context.runtimeSources.playerMovingOutSequence = movingOut;
485
486 context.runtimeSources.playerMovingIn = pieceIdlePlayback.movingInPlayer();
487 const auto movingIn = pieceIdlePlayback.movingInSequence();
488 if (movingIn != data::EmptyDataId)
489 context.runtimeSources.playerMovingInSequence = movingIn;
490
491 const auto jailToken = pieceJailPlayback.activeTokenSequence();
493 context.runtimeSources.jailTokenAnimation = jailToken;
494
495 const auto synced = pieceShadowDisplay.sync(
496 ruleState, context, session);
497 if (!synced) return std::unexpected(synced.error());
498 return {};
499 }
500
501 [[nodiscard]] std::expected<void, std::string> syncBoardLighting(
502 SequencePlayback& session, const rules::GameState& ruleState,
503 const display::State& displayState)
504 {
505 const auto tick = displayState.boardTick;
506 const auto elapsed = tick >= lastBoardLightingTick
507 ? tick - lastBoardLightingTick
508 : tick;
510 const auto player = ruleState.currentPlayer;
511 const bool hasCurrentPlayer=player < ruleState.numberOfPlayers && player < rules::MaxPlayers;
512 if (elapsed == 0)
513 {
514 if (worldRenderer)
515 {
516 auto current=boardLightingController.current();
517 if(!hasCurrentPlayer) current.spotlight.enabled=false;
518 worldRenderer->setLighting(current);
519 }
520 return {};
521 }
522
523 boarddisplay::BoardLightingInputs inputs{};
524 inputs.game3DOn = displayState.game3DOn;
525 inputs.board3DOn = displayState.board3DOn && hasCurrentPlayer;
526 inputs.lightingOn = displayState.optionLightingOn;
527 inputs.tokenAnimationActive = pieceMovePlayback.active();
528 inputs.tick = tick;
529 inputs.numberOfTicks = elapsed;
530 inputs.lastBoardActivityTick = displayState.lastBoardActivityTick;
531
533 {
534 const auto& source = ruleState.players[player];
535 if (source.colour >= rules::MaxPlayerColours)
536 return std::unexpected(
537 "board spotlight player colour is outside legacy 0..5 range");
538 inputs.playerColour = source.colour;
539
540 auto pose = pieces::tokenOrientation(source.currentSquare);
541 if (!pose)
542 return std::unexpected(
543 "board spotlight current player square is outside legacy range");
544
545 if (pieceMovePlayback.active())
546 {
547 pieces::TokenRuntimeSources sources{};
548 const auto idle = pieceIdleDisplay.shownSequence(player);
549 if (idle != data::EmptyDataId)
550 sources.idleSequences[player] = idle;
551 const auto moving = pieceMovePlayback.currentSequence();
552 if (moving != data::EmptyDataId)
553 {
554 sources.movingSequence = moving;
557 session.runtime());
558 }
559 }
560 inputs.tokenPosition = {pose->x, pose->y, pose->z};
561 }
562 // Saved-game restoration can briefly expose NobodyPlayer and zero
563 // players while the real board is already visible. Keep global
564 // lighting alive and defer only the player-dependent spotlight.
565
566 const auto lighting = boardLightingController.tick(inputs);
567 if (!lighting) return std::unexpected(lighting.error());
568 if (worldRenderer) worldRenderer->setLighting(*lighting);
569 return {};
570 }
571
572 [[nodiscard]] std::expected<void, std::string> syncDicePlayback(
573 SequencePlayback& session, std::uint64_t tick,
574 bool boardVisible, bool iBarVisible)
575 {
576 if (!dicePlayback.active())
577 {
578 if (auto request = userinterface::takePendingDiceRoll())
579 {
580 diceQueueLockHeld = true;
581 const auto begun = dicePlayback.begin(*request);
582 if (!begun)
583 {
585 diceQueueLockHeld = false;
586 return std::unexpected(begun.error());
587 }
588 }
589 }
590
591 const auto step = dicePlayback.tick(tick, boardVisible, iBarVisible,
593 if (!step)
594 {
596 diceQueueLockHeld = false;
597 dicePlayback.reset();
599 return std::unexpected(step.error());
600 }
601 if (step->cameraTakeover)
602 {
603 std::optional<std::uint8_t> randomFourteen;
604 if (display::stateReadOnly().board3DOn)
605 randomFourteen = static_cast<std::uint8_t>(std::rand() % 14);
607 }
608 if (step->announceRoll)
609 {
611 const auto total = static_cast<std::uint8_t>(
612 projected.dice[0] + projected.dice[1]);
614 total, display::stateReadOnly().optionTokenAnimationsOn))
616 reaction->policy, reaction->watchAfterStart);
617 }
618 if (step->cameraRelease)
620 if (step->queueRelease)
621 {
622 if (!step->cameraRelease &&
626 {
628 diceQueueLockHeld = false;
629 }
630 }
631 return {};
632 }
633 [[nodiscard]] std::expected<void, std::string> syncPieceBuildings(
634 SequencePlayback& session, bool boardVisible)
635 {
636 const auto synced = pieceBuildingDisplay.sync(
638 if (!synced) return std::unexpected(synced.error());
639 return {};
640 }
642 {
644 {
647 }
649 {
652 }
653 }
654
655 [[nodiscard]] std::expected<void, std::string> playPieceTokenVoice(
656 std::uint8_t token, udsound::TokenVoiceLine line,
657 udsound::TokenVoiceClipPolicy policy, bool watchAfterStart)
658 {
659 auto* output = audioPlayback();
660 if (output == nullptr) return {};
661 auto played = monopolySoundRuntime.tokenVoice(*output,
662 display::stateReadOnly().optionTokenVoicesOn, token, line, policy,
663 static_cast<std::uint32_t>(std::rand()));
664 if (!played)
665 {
666 handleAudioPlaybackFailure("Token voice disabled audio", played.error());
667 return {};
668 }
669 if (watchAfterStart && played->started)
670 monopolySoundRuntime.watchTokenVoice(*output, token);
671 auto watched = monopolySoundRuntime.syncTokenVoices(*output);
672 if (!watched)
673 {
674 handleAudioPlaybackFailure("Token voice watch disabled audio", watched.error());
675 return {};
676 }
678 return {};
679 }
680
681 [[nodiscard]] std::expected<void, std::string> playPennybagsComment(
683 udsound::TokenVoiceClipPolicy policy, bool watchAfterStart)
684 {
685 auto* output = audioPlayback();
686 if (output == nullptr) return {};
687 auto played = monopolySoundRuntime.pennybagsVoice(*output,
688 display::stateReadOnly().optionHostCommentsOn, line, policy,
689 static_cast<std::uint32_t>(std::rand()));
690 if (!played)
691 {
692 handleAudioPlaybackFailure("Pennybags voice disabled audio", played.error());
693 return {};
694 }
695 if (watchAfterStart && played->started)
696 monopolySoundRuntime.watchPennybags(*output);
697 auto watched = monopolySoundRuntime.syncTokenVoices(*output);
698 if (!watched)
699 {
700 handleAudioPlaybackFailure("Pennybags voice watch disabled audio", watched.error());
701 return {};
702 }
704 return {};
705 }
706
707 [[nodiscard]] std::expected<void, std::string> playPennybagsSpecific(
709 bool watchAfterStart)
710 {
711 auto* output = audioPlayback();
712 if (output == nullptr) return {};
713 auto played = monopolySoundRuntime.pennybagsSpecific(*output,
714 display::stateReadOnly().optionHostCommentsOn, wave, policy);
715 if (!played)
716 {
717 handleAudioPlaybackFailure("Pennybags specific disabled audio", played.error());
718 return {};
719 }
720 if (watchAfterStart && played->started)
721 monopolySoundRuntime.watchPennybags(*output);
722 auto watched = monopolySoundRuntime.syncTokenVoices(*output);
723 if (!watched)
724 {
725 handleAudioPlaybackFailure("Pennybags specific watch disabled audio", watched.error());
726 return {};
727 }
729 return {};
730 }
731
732 [[nodiscard]] std::expected<void, std::string> syncPieceMovePlayback(
733 SequencePlayback& session, bool boardVisible, std::uint64_t tick)
734 {
736 {
738 {
739 if (special->special == pieces::PieceMoveSpecial::GoToJail)
740 pendingPieceMoveSpecial = std::move(*special);
741 // LeaveJail only clears the historical GoingToJailStatus;
742 // its projection has already moved from square 40 to 10.
743 }
744 }
745
747 !pieceMovePlayback.active())
748 {
751 const auto randomBit = pendingPieceMoveSpecial->before == 30 ?
752 static_cast<std::uint8_t>(std::rand() & 1) :
753 static_cast<std::uint8_t>(0);
755 tick, true, randomBit);
756 if (!begun)
757 {
762 return std::unexpected(begun.error());
763 }
765 }
766
767 if (pieceJailPlayback.active())
768 {
769 const auto step = pieceJailPlayback.tick(
771 if (!step)
772 {
777 return std::unexpected(step.error());
778 }
779 if (step->camera)
780 display::state().desiredBoardCamera = *step->camera;
781 if (step->playSiren && display::stateReadOnly().optionTokenVoicesOn)
782 {
783 if (auto* output = audioPlayback())
784 {
785 const auto siren = monopolySoundRuntime.siren(
786 *output, static_cast<std::uint8_t>(std::rand() % udsound::SirenCount));
787 if (!siren)
788 {
789 handleAudioPlaybackFailure("Paddywagon siren disabled audio", siren.error());
790 return {};
791 }
792 }
793 }
794 if (step->completed)
795 {
799 }
800 return {};
801 }
803 {
805 {
806 activePieceMoveSpecial = plan->special;
807 activePieceMoveToken = plan->token;
812 {
813 bool localHuman = false;
815 for (rules::PlayerNumber player = 0;
816 player < projected.numberOfPlayers && player < rules::MaxPlayers; ++player)
817 if (projected.players[player].token == plan->token)
818 {
819 localHuman = ui::localplayers::slotIsLocalHumanPlayer(player);
820 break;
821 }
823 if (const auto host = penny::offBoardPennybagsReaction(victory, localHuman))
824 {
825 const auto voice = playPennybagsComment(
826 host->voice, host->policy, host->watchAfterStart);
827 if (!voice) return voice;
828 }
829 if (!victory && !localHuman)
830 {
831 const auto line = (std::rand() & 1) == 0 ?
833 const auto voice = playPieceTokenVoice(plan->token, line,
835 if (!voice) return voice;
836 }
837 }
838 const auto begun = pieceMovePlayback.begin(std::move(*plan));
839 if (!begun)
840 {
844 activePieceMoveToken.reset();
845 return std::unexpected(begun.error());
846 }
847 }
848 }
849
850 if (!pieceMovePlayback.active()) return {};
851
852 const auto step = pieceMovePlayback.tick(boardVisible, session);
853 if (!step)
854 {
857 return std::unexpected(step.error());
858 }
859
860 if (step->camera)
861 display::state().desiredBoardCamera = *step->camera;
862
863 if (step->passedGo)
864 {
865 const auto voice = playPennybagsComment(
868 if (!voice) return voice;
869 }
870
871 if (step->looped &&
873 {
875 {
879 }
880 // UDPieces repeats WonGame at each lap; only the queue release is one-shot.
882 {
883 const auto voice = playPieceTokenVoice(*activePieceMoveToken,
886 if (!voice) return voice;
887 }
888 }
889
890 if (step->completed)
891 {
893 {
895 for (rules::PlayerNumber player = 0;
896 player < projected.numberOfPlayers && player < rules::MaxPlayers; ++player)
897 {
898 if (projected.players[player].token != *activePieceMoveToken)
899 continue;
900 const auto square = projected.players[player].currentSquare;
901 penny::LandingEconomics economics{};
903 if (square == 4)
904 {
905 economics.totalWorth = ai::totalWorth(projected, player);
906 economics.valid = true;
907 }
908 else if (square < 40 && projected.squares[square].owner != rules::NobodyPlayer &&
909 projected.squares[square].owner != player && !cardMove)
910 {
911 economics.totalWorth = ai::totalWorth(projected, player);
913 static_cast<rules::board::SquareType>(square),
915 economics.valid = true;
916 }
917 if (auto* output = audioPlayback())
918 {
920 output->boardEdition(), output->language(),
921 display::stateReadOnly().city, square);
922 if (announcement)
923 {
926 if (!spoken) return spoken;
927 }
928 }
929 const auto reactionRandom = static_cast<std::uint32_t>(std::rand() % 100);
931 projected, player, square, reactionRandom);
933 {
934 const auto voice = playPennybagsComment(pennybagsReaction->voice,
935 pennybagsReaction->policy, pennybagsReaction->watchAfterStart);
936 if (!voice) return voice;
937 }
938 const auto reaction = penny::landedOnSquareReaction(projected, player, square,
940 if (reaction)
941 {
942 const auto voice = playPieceTokenVoice(*activePieceMoveToken, reaction->line,
943 reaction->policy, reaction->watchAfterStart);
944 if (!voice) return voice;
945 }
947 break;
948 }
949 }
954 activePieceMoveToken.reset();
955 }
956 return {};
957 }
958
960 const sequence::SequenceEvent& event,
961 std::int32_t topLevelClock) noexcept
962 {
963 if (const auto message = sequence::lifecycleMessage(event, topLevelClock))
964 (void)uimsg::send(*message);
965 }
966
968 SequencePlayback& session, std::int32_t topLevelClock) noexcept
969 {
970 for (const auto& outcome : session.commands().outcomes())
971 for (const auto& event : outcome.events)
973 for (const auto& event : session.commands().cycleEvents())
975 }
976 }
977
978 namespace
979 {
980 std::expected<void, std::string> syncEuropeanDeeds(SequencePlayback& session,
981 int city, int currency, int housesPerHotel)
982 {
983 const auto resources = session.resources();
984 if (!resources || resources->context().board == data::BoardEdition::Usa)
985 return {};
986 const auto language = resources->context().language;
987 const EuropeanDeedSelection selection{language, city, currency, housesPerHotel};
988 if (europeanDeedSelection == selection) return {};
989 auto* fonts = fontPlayback();
990 if (!fonts) return std::unexpected("Europe deeds require the retail font runtime");
991 data::BitmapRuntimeCache templates;
992 const deeds::TemplateResolver resolve = [&](data::DataId id)
993 -> std::expected<std::shared_ptr<const data::BitmapRuntimeAsset>, std::string>
994 {
995 const auto metadata = resources->data().metadata(id);
996 if (!metadata) return std::unexpected(metadata.error().detail);
997 const auto bytes = resources->data().load(id);
998 if (!bytes) return std::unexpected(bytes.error().detail);
999 const auto bitmap = templates.resolve(id, metadata->type, *bytes);
1000 if (!bitmap) return std::unexpected(bitmap.error().detail);
1001 return *bitmap;
1002 };
1003 std::array<data::DataId, 56> next{};
1004 const auto rollback = [&]()
1005 {
1006 for (const auto id : next)
1007 if (id != data::EmptyDataId) (void)session.runtimeBitmaps().remove(id);
1008 };
1009 // Userifce startup/loadgame: publish all fronts then all backs in
1010 // logical board order. Never expose a partly regenerated catalog.
1011 for (int square = 0; square < 40; ++square)
1012 {
1013 const auto property = ibar::layout::propertyIndex(square);
1014 if (property < 0) continue;
1015 for (int side = 0; side < 2; ++side)
1016 {
1017 const auto image = deeds::render({square, static_cast<int>(language),
1018 city, currency, side == 0, housesPerHotel}, *fonts, resolve);
1019 if (!image) { rollback(); return std::unexpected(image.error()); }
1020 const auto id = session.runtimeBitmaps().create(image->width, image->height, false);
1021 if (!id) { rollback(); return std::unexpected(id.error()); }
1022 next[static_cast<std::size_t>(property + side * 28)] = *id;
1023 const auto updated = session.runtimeBitmaps().update(*id, *image);
1024 if (!updated) { rollback(); return std::unexpected(updated.error()); }
1025 }
1026 }
1027 session.setEuropeanDeeds(next);
1028 europeanDeedSelection = selection;
1029 return {};
1030 }
1031 }
1032
1033 namespace
1034 {
1035 [[nodiscard]] std::optional<std::shared_ptr<const data::MeshRenderData>>
1036 resolveModernScene(data::DataId id, std::optional<data::DataId> root,
1037 std::uint16_t priority)
1038 {
1039 const auto resources = startup::resources();
1040 if (!resources) return {};
1041 const auto& state = display::stateReadOnly();
1042 const data::ModernSceneContext context{resources->context().board,
1043 state.city, resources->context().language, state.system,
1044 state.customBoardPath};
1048 {
1050 modernSceneOptions, context)) continue;
1051 const auto options = data::modernSceneLoadOptions(kind);
1052 if (!options) return {};
1053 const auto index = static_cast<std::size_t>(kind);
1055 return modernSceneMeshes[index]
1056 ? std::optional{modernSceneMeshes[index]} : std::nullopt;
1058 const char* base = SDL_GetBasePath();
1059 if (!base || !*base) return {};
1061 const auto path = std::filesystem::path(base) / "assets/modern" /
1062 std::filesystem::path(definition.relativeGlbPath);
1063 std::error_code filesystemError;
1064 if (!std::filesystem::is_regular_file(path, filesystemError)) return {};
1065 auto loaded = data::loadModernGltfMesh(path, *options);
1066 if (!loaded)
1067 {
1068 std::cerr << "Modern scene rejected: " << path.string() << ": "
1069 << loaded.error().detail << " - using retail HMD fallback.\n";
1070 return {};
1071 }
1073 {
1074 std::cerr << "Modern scene rejected: " << path.string()
1075 << ": geometry does not match its calibrated contract - using retail HMD fallback.\n";
1076 return {};
1077 }
1078 modernSceneMeshes[index] = std::move(*loaded);
1079 std::cerr << "Modern scene asset: " << path.string() << '\n';
1080 return std::optional{modernSceneMeshes[index]};
1081 }
1082 return {};
1083 }
1084
1087 {
1088 return [](
1090 std::optional<data::DataId> rootSequenceDataId,
1091 std::uint16_t priority)
1092 -> std::expected<
1093 std::optional<std::shared_ptr<const data::MeshRenderData>>,
1094 data::MeshRuntimeError>
1095 {
1096 if (auto scene = resolveModernScene(id, rootSequenceDataId, priority))
1097 return scene;
1098 if (data::qualifiedModernTokenVariantSequence(id, rootSequenceDataId, priority))
1099 {
1100 const char* base = SDL_GetBasePath();
1101 if (!base || !*base) return std::optional<std::shared_ptr<const data::MeshRenderData>>{};
1103 modernTokenVariants = std::make_unique<data::ModernTokenVariantCache>(
1104 std::filesystem::path(base) / "assets/modern");
1105 const bool previouslyAttempted = modernTokenVariants->attempted(*rootSequenceDataId);
1106 if (auto variant = modernTokenVariants->resolve(id, rootSequenceDataId, priority))
1107 {
1109 std::cerr << "Modern token sequence pack: root "
1110 << *rootSequenceDataId << " - complete modern states loaded.\n";
1111 return std::optional{std::move(variant)};
1112 }
1113 if (!previouslyAttempted && modernTokenVariants->loadError(*rootSequenceDataId))
1114 std::cerr << "Modern token sequence pack rejected: "
1115 << modernTokenVariants->loadError(*rootSequenceDataId)->detail
1116 << " - using complete retail sequence.\n";
1117 return std::optional<std::shared_ptr<const data::MeshRenderData>>{};
1118 }
1119 const auto* definition =
1121 if (!data::qualifiedModernTokenSequence(id, rootSequenceDataId, priority))
1122 return std::optional<
1123 std::shared_ptr<const data::MeshRenderData>>{};
1124
1125 const auto token =
1126 static_cast<std::size_t>(definition->token);
1128 {
1130 return std::optional{
1132 return std::optional<
1133 std::shared_ptr<const data::MeshRenderData>>{};
1134 }
1135
1137 const char* basePath = SDL_GetBasePath();
1138 if (!basePath || *basePath == '\0')
1139 return std::optional<
1140 std::shared_ptr<const data::MeshRenderData>>{};
1141
1142 const auto path =
1143 std::filesystem::path(basePath) /
1144 std::filesystem::path(definition->relativeGlbPath);
1145 std::error_code filesystemError;
1146 if (!std::filesystem::is_regular_file(
1148 return std::optional<
1149 std::shared_ptr<const data::MeshRenderData>>{};
1150
1151 data::ModernGltfLoadOptions loadOptions;
1152 loadOptions.unitsPerMeter = definition->unitsPerMeter;
1153 loadOptions.yawDegrees = definition->yawDegrees;
1154 loadOptions.localOffset = definition->localOffset;
1155 loadOptions.groundToZero = true;
1157 path,
1158 loadOptions);
1159 if (!loaded)
1160 {
1161 std::string diagnostic =
1162 "Modern token " +
1163 std::string(definition->slug) +
1164 " rejected: " +
1165 loaded.error().detail;
1166 if (reportedModernAssetFailures.emplace(
1167 diagnostic).second)
1168 std::cerr << diagnostic
1169 << " - using retail HMD fallback.\n";
1170 return std::optional<
1171 std::shared_ptr<const data::MeshRenderData>>{};
1172 }
1173
1174 modernTokenMeshes[token] = std::move(*loaded);
1175 std::cerr << "Modern token asset: "
1176 << definition->slug << " <- "
1177 << path.string() << '\n';
1178 return std::optional{
1180 };
1181 }
1182
1183
1185 {
1186 startupCPUProfile = {};
1187 modernTokenMeshes = {};
1189 modernSceneMeshes = {};
1191 modernTokenVariants.reset();
1192 modernEnvironment.reset();
1194 }
1195 }
1196
1197
1199 {
1200 modernSceneOptions = options;
1201 }
1202
1204 {
1205 const auto resources = startup::resources();
1206 const auto& view = display::stateReadOnly();
1207 return modernSceneOptions.proceduralBoard && resources &&
1208 resources->context().board == data::BoardEdition::Usa &&
1209 resources->context().language == data::LanguageId::EnglishUs &&
1210 view.city == 0 && view.system == 13 && view.customBoardPath.empty();
1211 }
1212
1214 {
1215 if (!gpuDevice) return nullptr;
1216 if (!playback)
1217 if (auto resources = startup::resources())
1218 {
1219 startupCPUProfile.begin();
1220 const auto warmStarted = startupCPUProfile.start();
1222 // Warm the existing idle-safe modern geometry. Qualified
1223 // rigid movement reuses it with complete CNK transforms;
1224 // unqualified sequences retain their historical HMD frames.
1225 for (const auto& definition :
1227 {
1228 const auto probe =
1230 definition.token);
1231 if (definition.staticIdleReplacement &&
1232 probe != data::EmptyDataId)
1234 probe,
1236 definition.token),
1238 }
1239
1240 startupCPUProfile.report("idle_token_warm", warmStarted,
1241 std::count_if(modernTokenMeshes.begin(), modernTokenMeshes.end(), [](const auto& mesh){ return bool(mesh); }));
1242 const auto sessionStarted = startupCPUProfile.start();
1243 playback = std::make_unique<SequencePlayback>(
1244 std::move(resources),
1245 std::move(modernResolver));
1246 startupCPUProfile.report("session_construct", sessionStarted);
1247 if (modernSceneOptions.proceduralBoard)
1248 {
1249 const auto rasterText = [](std::string_view text, int size, bool bold = true, bool italic = false)
1250 -> std::expected<data::LegacyBitmapRGBA8, std::string>
1251 {
1252 auto* font=fontPlayback();
1253 if(!font || !font->ready()) return std::unexpected("HUD font unavailable");
1254 auto rendered=font->renderPresentation(text,0xFFFFFF,size,bold ? 700 : 400,italic);
1255 if(!rendered) return std::unexpected(rendered.error().detail);
1256 return std::move(*rendered);
1257 };
1258 auto menuSkin = std::make_shared<menu::ModernMenuSkin>(
1259 startup::resources()->context().board, startup::resources()->context().language,
1260 [rasterText](std::string_view text) { return rasterText(text, 54); });
1261 menuSkin->configureTradeCashPresentation([]
1262 {
1263 const auto snapshot = startup::resources();
1264 const auto& state = display::stateReadOnly();
1265 return snapshot && snapshot->context().board == data::BoardEdition::Usa &&
1266 snapshot->context().language == data::LanguageId::EnglishUs &&
1267 state.city == 0 && state.system == 13 && state.customBoardPath.empty();
1268 });
1269 // Optional static preview captured from the qualified GPU scene.
1270 // A missing or malformed preview keeps the branded menu fallback.
1271 std::ifstream preview(std::filesystem::path(SDL_GetBasePath()) /
1272 "assets/modern/presentation/menu_city.png", std::ios::binary | std::ios::ate);
1273 if (preview && preview.tellg() > 0 && preview.tellg() <= 8 * 1024 * 1024)
1274 {
1275 std::vector<std::byte> bytes(static_cast<std::size_t>(preview.tellg()));
1276 preview.seekg(0);
1277 if (preview.read(reinterpret_cast<char*>(bytes.data()),
1278 static_cast<std::streamsize>(bytes.size())))
1280 {8U * 1024U * 1024U, 4096, 64U * 1024U * 1024U}); decoded)
1281 {
1282 auto image = std::make_shared<data::LegacyBitmapRGBA8>();
1283 image->width = (*decoded)->width;
1284 image->height = (*decoded)->height;
1285 image->pixels = (*decoded)->rgba;
1286 menuSkin->configureBackground(std::move(image));
1287 }
1288 }
1289 auto tokenPreviews = std::make_shared<menu::ModernTokenPreview>(
1290 std::filesystem::path(SDL_GetBasePath()) / "assets/modern/presentation/tokens");
1291 menuSkin->configureTokenImages([tokenPreviews](std::uint8_t token, std::uint8_t frame)
1292 { return tokenPreviews->image(token, frame); });
1293 tradePartnerSelectionPlayback.configureTokenImages([tokenPreviews](std::uint8_t token)
1294 { return tokenPreviews->image(token,255); });
1295 playback->world2D().configureModernMenuSkin(std::move(menuSkin));
1296 auto skin = std::make_shared<ibar::ModernIBarSkin>(
1297 startup::resources()->context().language,
1298 [rasterText](std::string_view text) { return rasterText(text, 12); });
1299 skin->configureTokenImages([tokenPreviews](std::uint8_t token)
1300 { return tokenPreviews->image(token,255); });
1301 skin->configureActionText([rasterText](std::string_view text) { return rasterText(text, 36); });
1302 skin->configureLayoutProvider([] { return ibar::stateReadOnly().actionButtonLayout; });
1303 skin->configurePresentationContext([]
1304 {
1305 const auto snapshot = startup::resources();
1306 const auto& state = display::stateReadOnly();
1307 return snapshot && snapshot->context().board == data::BoardEdition::Usa &&
1308 snapshot->context().language == data::LanguageId::EnglishUs &&
1309 state.city == 0 && state.system == 13 && state.customBoardPath.empty();
1310 });
1311 skin->configureDeedPlacementProvider([](data::DataId root, std::uint16_t priority,
1312 const sequence::Matrix2D& raster) -> std::optional<std::array<float, 4>>
1313 {
1314 const auto& state = display::stateReadOnly();
1315 if ((state.current2DView != display::Screen2D::Trade &&
1316 state.current2DView != display::Screen2D::Portfolio) || priority != 1002 ||
1317 !root || root != iBarBackdropPlayback.buyAuctionPopupDeed()) return {};
1318 // Present the pending purchase in the Trade deed panel or Portfolio board viewport.
1319 // Preserve the retail popup node, lifetime, input routing and clock.
1320 const auto& m = raster.values;
1321 if (m[0] != 1 || m[1] != 0 || m[2] != 0 || m[3] != 0 ||
1322 m[4] != 1 || m[5] != 0 || m[8] != 1) return {};
1323 const float panelLeft = state.current2DView == display::Screen2D::Trade ? 600.0F : 100.0F;
1324 return std::array<float, 4>{panelLeft+1.37665F-m[6], -m[7],
1325 panelLeft+198.62335F-m[6], 225.0F-m[7]};
1326 });
1327 skin->configureIdleCardPresentation([](data::DataId root, std::uint16_t priority,
1329 -> std::optional<ibar::ModernIBarSkin::IdleCardPresentation>
1330 {
1331 const auto snapshot = startup::resources();
1332 const auto& state = display::stateReadOnly();
1333 if (!snapshot || snapshot->context().board != data::BoardEdition::Usa ||
1334 snapshot->context().language != data::LanguageId::EnglishUs ||
1335 state.city != 0 || state.system != 13 || !state.customBoardPath.empty() ||
1336 state.current2DView != state.desired2DView ||
1337 (state.current2DView != display::Screen2D::Trade &&
1338 state.current2DView != display::Screen2D::Portfolio) || priority != 1005) return {};
1339 const auto& m = sequenceToWorld.values;
1340 if (m[0] != 1 || m[1] != 0 || m[2] != 0 || m[3] != 0 ||
1341 m[4] != 1 || m[5] != 0 || m[8] != 1) return {};
1342 // Keep pending cards clear of the Trade chooser and Portfolio columns.
1343 // St. Charles has an authored 400x239 image; other idle cards are 400x240.
1344 const float halfHeight = root == data::packDataId(
1345 data::LegacyGroupId::LanguageGraphics, 0x29) ? 59.75F : 60.0F;
1346 bool occluded = iBarBackdropPlayback.propertyHoverDeed() != data::EmptyDataId;
1347 if (state.current2DView == display::Screen2D::Trade)
1348 {
1349 occluded = occluded || tradePropertyPlayback.hoverDeedOccupiesRightPanel() ||
1350 iBarBackdropPlayback.buyAuctionPopupDeed() != data::EmptyDataId;
1351 }
1352 else
1353 {
1355 occluded = occluded || (calculator.visible && calculator.hoveredFunction.has_value()) ||
1356 (calculator.picker == statsui::CalculatorPicker::Deed && calculator.hoveredDeed.has_value()) ||
1358 // The Portfolio purchase deed is upper-left; normal
1359 // Stats grid/floater deeds are below this card rectangle.
1360 }
1362 {600.0F-m[6], 112.5F-halfHeight-m[7],
1363 800.0F-m[6], 112.5F+halfHeight-m[7]}, occluded};
1364 });
1365 skin->configurePropertyDescriptors(
1366 [](unsigned propertyIndex) -> std::optional<ibar::ModernIBarSkin::PropertyDescriptor>
1367 {
1368 const auto snapshot = startup::resources();
1369 if (!snapshot || snapshot->context().board != data::BoardEdition::Usa ||
1370 !snapshot->language() || !snapshot->language()->catalog) return {};
1371 const auto& state = display::stateReadOnly();
1372 if (state.city != 0 || state.system != 13 || !state.customBoardPath.empty()) return {};
1373 for (int square = 0; square < 40; ++square)
1374 {
1375 if (ibar::layout::propertyIndex(square) != static_cast<int>(propertyIndex)) continue;
1376 const auto name16 = statsui::statsPropertyName(*snapshot->language()->catalog,
1377 data::BoardEdition::Usa, 0, square);
1378 if (!name16) return {};
1379 const auto name = fonts::transcodeUtf8(std::u16string_view(*name16));
1380 const auto& definition = rules::board::originalDefinition(
1381 static_cast<rules::board::SquareType>(square));
1382 const auto price = money::format(definition.purchaseCost, 13, true,
1384 if (!name || !price) return {};
1386 static_cast<std::uint8_t>(definition.group), *price};
1387 }
1388 return {};
1389 }, [rasterText](std::string_view text) { return rasterText(text, 18); });
1390 const auto snapshot = startup::resources();
1391 if (snapshot && snapshot->context().board == data::BoardEdition::Usa &&
1392 snapshot->context().language == data::LanguageId::EnglishUs &&
1393 snapshot->language() && snapshot->language()->catalog)
1394 {
1395 std::map<data::DataId, ibar::ModernIBarSkin::DeedDescriptor> descriptors;
1396 for (int square = 0; square < 40; ++square)
1397 {
1398 const int index = ibar::layout::propertyIndex(square);
1399 if (index < 0) continue;
1400 const auto name16 = statsui::statsPropertyName(*snapshot->language()->catalog,
1401 data::BoardEdition::Usa, 0, square);
1402 if (!name16) continue;
1403 const auto name = fonts::transcodeUtf8(std::u16string_view(*name16));
1404 if (!name) continue;
1405 for (const bool front : {true, false})
1406 {
1407 const auto plan = deeds::plan({square, 1, 0, 13, front, 5, true});
1408 if (!plan || plan->text.empty()) continue;
1410 for (const auto& fill : plan->fills)
1411 descriptor.fills.push_back({fill.x, fill.y, fill.width, fill.height, fill.color});
1412 for (const auto& region : plan->text)
1413 descriptor.text.push_back({region.text, region.y, region.height,
1414 region.justification, region.verticalLeeway, region.fontSize,
1415 region.color, region.bold, region.italic, region.verticalCenter});
1416 descriptor.text.front().text = *name;
1417 const auto deedTag = static_cast<data::DataTag>((front ? 0x0CD0 : 0x0B53) + index);
1418 if (front)
1419 {
1420 switch (deedTag)
1421 {
1422 case 0x0CD2: case 0x0CDA: case 0x0CE1: case 0x0CE9:
1424 case 0x0CD7:
1426 case 0x0CE4:
1428 default: break;
1429 }
1430 }
1432 deedTag), std::move(descriptor));
1433 }
1434 }
1435 std::map<data::DataId, ibar::ModernIBarSkin::DrawCardDescriptor> drawCards;
1436 // Only the qualified idle face owners: original deck/flip/out
1437 // animation clocks and transitional artwork remain authored.
1438#include "ModernDrawCardText.inc"
1439 for (unsigned index = 0; index < ModernUsaDrawCardBodies.size(); ++index)
1440 {
1441 const bool community = index >= 16;
1442 const auto tag = static_cast<data::DataTag>((community ? 0x0059 : 0x0028) + index % 16);
1444 ibar::ModernIBarSkin::DrawCardDescriptor{community ? "Community Chest" : "Chance",
1445 // Production idle St. Charles face (root50029, leaf50983) is400x239; all other31 are400x240.
1446 std::string(ModernUsaDrawCardBodies[index]), 400, index == 1 ? 239U : 240U});
1447 }
1448 const auto cardRaster = [rasterText](std::string_view text, int size, bool bold, bool italic)
1449 -> std::expected<data::LegacyBitmapRGBA8, std::string>
1450 {
1451 const auto& state = display::stateReadOnly();
1452 if (state.city != 0 || state.system != 13 || !state.customBoardPath.empty())
1453 return std::unexpected("USA card presentation context changed");
1454 return rasterText(text, size, bold, italic);
1455 };
1456 skin->configureDrawCardDescriptors(std::move(drawCards), cardRaster);
1457 skin->configureDeedDescriptors(std::move(descriptors),
1458 [rasterText](std::string_view text, int size, bool bold, bool italic)
1459 -> std::expected<data::LegacyBitmapRGBA8, std::string>
1460 {
1461 const auto& state = display::stateReadOnly();
1462 if (state.city != 0 || state.system != 13 || !state.customBoardPath.empty())
1463 return std::unexpected("USA deed presentation context changed");
1464 return rasterText(text, size, bold, italic);
1465 });
1466 }
1467 playback->world2D().configureModernIBarSkin(std::move(skin));
1468 }
1469 europeanDeedSelection.reset();
1470 }
1471 return playback.get();
1472 }
1473
1474
1475 namespace
1476 {
1478 {
1479 europeanDeedSelection.reset();
1480 pieceMovePlayback = {};
1481 pieceJailPlayback = {};
1482 pieceIdlePlayback = {};
1483 pieceIdleDisplay.reset();
1484 pieceBuildingDisplay.reset();
1485 pieceShadowDisplay.reset();
1486 auctionPlayback.reset();
1487 auctionTextPlayback.reset();
1489 tradeBackdropPlayback.reset();
1490 tradeTokenPlayback.reset();
1492 optionsFilePlayback.reset();
1493 optionsSavePlayback.reset();
1497 statsPlayback.reset();
1498 chatRecipientPlayback.reset();
1499 chatOptionPlayback.reset();
1500 chatFluffPlayback.reset();
1501 statsBankPlayback.reset();
1504 statsPlayerPlayback.reset();
1506 statsPlayerAuxPlayback.reset();
1509 statsDeedPlayback.reset();
1512 statsDeedBarPlayback.reset();
1514 optionsOptionPlayback.reset();
1515 optionsTogglePlayback.reset();
1516 optionsHelpPlayback.reset();
1517 tradePropertyPlayback.reset();
1518 tradeOfferIconPlayback.reset();
1519 tradeCashTextPlayback.reset();
1520 tradeNamePlayback.reset();
1522 tradePanelTextPlayback.reset();
1526 boardBackdropPlayback.reset();
1531 if (diceQueueLockHeld) userinterface::unlockGameQueue();
1532 diceQueueLockHeld = false;
1533 dicePlayback.reset();
1534 dice2DPlayback.reset();
1535 iBarBackdropPlayback.reset();
1536 iBarScoreTextPlayback.reset();
1538 chatTextPlayback.reset();
1539 optionsVisualPlayback.reset();
1540 statsTextPlayback.reset();
1542 mousePointerPlayback.reset();
1543 nativeMouseCursor.reset();
1545 display::cancelDiceCameraOverride();
1547 pieceIdleQueueLockHeld = false;
1548 activePieceMoveSpecial = pieces::PieceMoveSpecial::None;
1550 pieceMoveQueueLockHeld = false;
1552 activePieceMoveToken.reset();
1553 if (tokenVoiceQueueLockHeld) userinterface::unlockGameQueue();
1555 activeSequenceSounds.clear();
1556 activeBoardSequence.reset();
1557 activeWorldCamera.reset();
1558 }
1559
1561 {
1562 // No old sequence callback or command may cross the new-game
1563 // boundary. Resources and render devices remain owned by Engine.
1564 if (playback)
1565 {
1566 openingMovies.reset(*playback);
1569 (void)playback->stopAll();
1570 }
1573 sequenceVideoRuntime.reset();
1574 playback.reset();
1576 // The renderer survives the menu transition; the old scene does not.
1577 if (worldRenderer)
1578 if (auto* cache = worldRenderer->meshCache()) cache->clear();
1579 if (audioRuntime) audioRuntime->stopAll();
1581 stopVoiceChat();
1582 if (voiceChatAudioRuntime) voiceChatAudioRuntime->closeAllReceivers();
1584
1585 std::vector<uimsg::Message> retained;
1586 uimsg::Message queued;
1587 while (uimsg::receive(queued))
1588 {
1589 switch (queued.type)
1590 {
1591 case uimsg::Type::SequenceStarted:
1592 case uimsg::Type::SequenceReachedEnd:
1593 case uimsg::Type::SequenceDeleted:
1594 case uimsg::Type::VideoJump:
1595 case uimsg::Type::ButtonPressed:
1596 case uimsg::Type::ButtonReleased:
1597 case uimsg::Type::ButtonRollEnter:
1598 case uimsg::Type::ButtonRollExit:
1599 case uimsg::Type::ButtonTrack:
1600 case uimsg::Type::ButtonOnOff:
1601 break;
1602 default: retained.push_back(std::move(queued)); break;
1603 }
1604 }
1605 for (const auto& message : retained) (void)uimsg::send(message);
1606 userinterface::resetTimeStep();
1607 userinterface::resetRuleProjection();
1608 runtime::state().gameInProgress = false;
1609 runtime::state().gamePaused = false;
1610 display::setTokenAnimationStackActive(false);
1611 display::state().mouseLeftPressed = false;
1612 display::state().mouseRightPressed = false;
1613 display::state().manualMouseCamLock = false;
1614 display::state().manualCameraRequested = false;
1615 display::state().bssmCameraState = 0;
1616 display::state().demoModeDesired = false;
1617 display::state().currentBoardCamera.reset();
1618 display::state().worldCamera = display::initialBoardCamera();
1619 if (!playerselection::initialize() || !rules::initialize()) return false;
1620 display::setBackdrop(display::Screen2D::PlayerSelect);
1621 playerselection::switchPhase(display::PlayerSetupPhase::LocalOrNetwork);
1622 mouse::setEnabled(true);
1623 display::showAll2();
1624 return true;
1625 }
1626
1628 {
1629 if (const auto error = openingMovies.takeError(); !error.empty())
1630 std::cerr << "Opening movies: " << error << '\n';
1631 if (openingMovies.active())
1632 {
1633 display::setBackdrop(display::Screen2D::Black);
1634 mouse::setEnabled(false);
1635 display::showAll2();
1636 }
1637 else if (openingMovies.takePlayerSelectionRequest())
1638 {
1639 display::setBackdrop(display::Screen2D::PlayerSelect);
1640 playerselection::switchPhase(display::PlayerSetupPhase::HiScore);
1641 mouse::setEnabled(true);
1642 display::showAll2();
1643 }
1644 }
1645 }
1646
1648 {
1649 if (auto* session = sequencePlayback())
1650 {
1651 openingMovies.begin(timers::tickCount(), startedByLobby,
1652 display::stateReadOnly().board3DOn, *session);
1654 }
1655 else
1656 {
1657 std::cerr << "Opening movies: no resource playback owner\n";
1658 display::setBackdrop(display::Screen2D::PlayerSelect);
1659 playerselection::switchPhase(display::PlayerSetupPhase::HiScore);
1660 mouse::setEnabled(true);
1661 display::showAll2();
1662 }
1663 }
1664
1666 {
1667 if (!openingMovies.active() || message.type == uimsg::Type::Quit) return false;
1668 if (message.type == uimsg::Type::KeyboardPressed ||
1669 message.type == uimsg::Type::MouseLeftDown ||
1670 message.type == uimsg::Type::MouseMiddleDown ||
1671 message.type == uimsg::Type::MouseRightDown)
1672 {
1673 if (playback) openingMovies.skip(*playback);
1675 }
1676 return true;
1677 }
1678
1680 {
1681 if (audioDisabled)
1682 return nullptr;
1683 if (!audioRuntime)
1684 if (auto resources = startup::resources())
1685 audioRuntime = std::make_unique<audio::Runtime>(std::move(resources));
1686 return audioRuntime.get();
1687 }
1688
1689 namespace
1690 {
1692 const voicechat::packet::Event& event, std::uint32_t sourceId)
1693 {
1694 auto* output = audioPlayback();
1695 if (output == nullptr)
1696 return false;
1699 std::make_unique<voicechat::AudioRuntime>(*output);
1700 return voiceChatAudioRuntime->handleEvent(event, sourceId);
1701 }
1702 }
1703
1705 {
1706 voiceChatStartPending = false;
1707 if (!messaging::networkMode())
1708 {
1709 stopVoiceChat();
1710 return false;
1711 }
1712
1713 const auto& voiceOptions =
1714 userinterface::ruleStateReadOnly().options.voiceChat;
1715 const auto settings =
1716 voicechat::legacy::captureSettings(voiceOptions);
1717 if (!settings)
1718 return false;
1719
1720 auto* output = audioPlayback();
1721 if (output == nullptr)
1722 return false;
1724 voiceChatAudioRuntime = std::make_unique<voicechat::AudioRuntime>(*output);
1725 if (voiceChatAudioRuntime->captureActive())
1726 return true;
1727
1728 // A resync can fill the last MESS slot with STOP. Retry CHAT once the
1729 // queue drains instead of leaving a healthy microphone permanently off.
1730 if (messaging::queuedActionCount() >= messaging::MessageQueueCapacity)
1731 {
1732 voiceChatStartPending = true;
1733 return false;
1734 }
1735
1736 const auto started = voiceChatAudioRuntime->startCapture(*settings);
1737 if (!started)
1738 {
1739 std::cerr << "Voice chat capture unavailable: "
1740 << started.error() << '\n';
1741 return false;
1742 }
1743 return true;
1744 }
1745
1747 {
1748 voiceChatStartPending = false;
1750 voiceChatAudioRuntime->stopCapture();
1751 }
1752
1754 {
1755 if (fontDisabled) return nullptr;
1756 if (!fontRuntime)
1757 {
1758 auto candidate = std::make_unique<fonts::Runtime>();
1759 std::vector<std::filesystem::path> roots;
1760 if (const char* base = SDL_GetBasePath(); base && *base)
1761 roots.emplace_back(base);
1762#ifdef _WIN32
1763 if (const char* windows = std::getenv("WINDIR"); windows && *windows)
1764 roots.emplace_back(std::filesystem::path(windows) / "Fonts");
1765#endif
1766 const auto arial = fonts::resolveRetailArial(roots);
1767 if (!arial)
1768 {
1769 std::cerr << "Font runtime disabled: " << arial.error().detail << '\n';
1770 fontDisabled = true;
1771 return nullptr;
1772 }
1773 if (const auto selected = candidate->setFont(*arial, "Arial"); !selected)
1774 {
1775 std::cerr << "Font runtime disabled: " << selected.error().detail << '\n';
1776 fontDisabled = true;
1777 return nullptr;
1778 }
1779 // DISPLAY_initialize() retail establishes Arial at 12 px,
1780 // weight 700 and non-italic before every subsystem derives its
1781 // saved font slots. Keep that shared baseline exact.
1782 if (const auto sized = candidate->setSize(12); !sized)
1783 {
1784 std::cerr << "Font runtime disabled: " << sized.error().detail << '\n';
1785 fontDisabled = true;
1786 return nullptr;
1787 }
1788 candidate->setWeight(700);
1789 candidate->setItalic(false);
1790 candidate->setUnderline(false);
1791 candidate->setStrikeOut(false);
1792 if (const auto saved = candidate->saveSettings(0); !saved)
1793 {
1794 std::cerr << "Font runtime disabled: " << saved.error().detail << '\n';
1795 fontDisabled = true;
1796 return nullptr;
1797 }
1798
1799 // UDChat derives slots 8/9 from the display font by changing the
1800 // size to 7. UDTrade uses slot 7 at size 8, weight 700.
1801 if (const auto sized = candidate->setSize(7); !sized)
1802 {
1803 std::cerr << "Font runtime disabled: " << sized.error().detail << '\n';
1804 fontDisabled = true;
1805 return nullptr;
1806 }
1807 if (const auto saved = candidate->saveSettings(8); !saved)
1808 {
1809 std::cerr << "Font runtime disabled: " << saved.error().detail << '\n';
1810 fontDisabled = true;
1811 return nullptr;
1812 }
1813 if (const auto saved = candidate->saveSettings(9); !saved)
1814 {
1815 std::cerr << "Font runtime disabled: " << saved.error().detail << '\n';
1816 fontDisabled = true;
1817 return nullptr;
1818 }
1819 if (const auto restored = candidate->restoreSettings(0); !restored)
1820 {
1821 std::cerr << "Font runtime disabled: " << restored.error().detail << '\n';
1822 fontDisabled = true;
1823 return nullptr;
1824 }
1825 if (const auto sized = candidate->setSize(8); !sized)
1826 {
1827 std::cerr << "Font runtime disabled: " << sized.error().detail << '\n';
1828 fontDisabled = true;
1829 return nullptr;
1830 }
1831 candidate->setWeight(700);
1832 if (const auto saved = candidate->saveSettings(7); !saved)
1833 {
1834 std::cerr << "Font runtime disabled: " << saved.error().detail << '\n';
1835 fontDisabled = true;
1836 return nullptr;
1837 }
1838 if (const auto restored = candidate->restoreSettings(0); !restored)
1839 {
1840 std::cerr << "Font runtime disabled: " << restored.error().detail << '\n';
1841 fontDisabled = true;
1842 return nullptr;
1843 }
1844 fontRuntime = std::move(candidate);
1845 }
1846 return fontRuntime.get();
1847 }
1848
1853
1855 std::string_view context,
1856 const std::string& error) noexcept
1857 {
1858 std::string diagnostic(context);
1859 diagnostic += ": ";
1860 diagnostic += error;
1861 if (reportedAudioFailures.emplace(diagnostic).second)
1862 std::cerr << diagnostic << '\r\n';
1863
1864 // A bad/missing asset must not silence every other sound. Only errors
1865 // that indicate loss of the SDL audio backend quarantine playback for
1866 // the remainder of this session.
1867 const std::string_view detail(error);
1868 const bool backendFailure =
1869 detail.starts_with("SDL_InitSubSystem(audio):") ||
1870 detail.starts_with("SDL_OpenAudioDeviceStream:") ||
1871 detail.starts_with("SDL_SetAudio") ||
1872 detail.starts_with("SDL_ResumeAudioStreamDevice") ||
1873 detail.starts_with("SDL_PauseAudioStreamDevice") ||
1874 detail.starts_with("SDL_PutAudioStreamData") ||
1875 detail.starts_with("SDL_FlushAudioStream") ||
1876 detail.starts_with("audio seek");
1877
1878 if (!backendFailure)
1879 return;
1880
1882 if (audioRuntime) audioRuntime->stopAll();
1883 activeSequenceSounds.clear();
1884 if (tokenVoiceQueueLockHeld) userinterface::unlockGameQueue();
1886 audioDisabled = true;
1887 }
1888
1890 {
1891 if (auto* output = audioPlayback())
1892 {
1893 const auto result = monopolySoundRuntime.warning(*output);
1894 if (!result) handleAudioPlaybackFailure("Warning sound disabled audio", result.error());
1895 }
1896 }
1897
1899 {
1900 if (auto* output = audioPlayback())
1901 {
1902 const auto result = monopolySoundRuntime.click(*output);
1903 if (!result) handleAudioPlaybackFailure("Click sound disabled audio", result.error());
1904 }
1905 }
1906
1908 {
1909 if (auto* output = audioPlayback())
1910 {
1911 const auto result = monopolySoundRuntime.build(*output);
1912 if (!result) handleAudioPlaybackFailure("Build sound disabled audio", result.error());
1913 }
1914 }
1915
1917 {
1918 if (auto* output = audioPlayback())
1919 {
1920 const auto result = monopolySoundRuntime.unbuild(*output);
1921 if (!result) handleAudioPlaybackFailure("Unbuild sound disabled audio", result.error());
1922 }
1923 }
1924
1926 {
1927 if (auto* output = audioPlayback())
1928 {
1929 const auto result = monopolySoundRuntime.saveFailure(*output);
1930 if (!result) handleAudioPlaybackFailure("Save failure sound disabled audio", result.error());
1931 }
1932 }
1933
1935 udsound::TokenVoiceClipPolicy policy, bool watchAfterStart) noexcept
1936 {
1937 (void)playPieceTokenVoice(token, line, policy, watchAfterStart);
1938 }
1939
1941 udsound::TokenVoiceClipPolicy policy, bool watchAfterStart) noexcept
1942 {
1943 (void)playPennybagsComment(line, policy, watchAfterStart);
1944 }
1945
1947 {
1948 return monopolySoundRuntime.postLockVoiceSlotEmpty();
1949 }
1950
1952 {
1953 const auto resources = startup::resources();
1954 return !resources || resources->context().board == data::BoardEdition::Usa;
1955 }
1956
1958 {
1959 auto* output = audioPlayback();
1960 if (output == nullptr) return;
1961 if (output->boardEdition() == data::BoardEdition::Europe)
1962 {
1963 // !USA_VERSION calls WAV_pb235 specifically: alternate 2 of
1964 // PB_JailPayMoneyOrTryForDoubles (WAV_pb233..WAV_pb235).
1965 const auto pb235 = udsound::pennybagsVoiceAt(output->boardEdition(),
1966 udsound::PennybagsVoice::JailPayMoneyOrTryForDoubles, 2);
1967 if (pb235)
1969 udsound::TokenVoiceClipPolicy::WaitForAnyOldSoundThenPlay, false);
1970 }
1971 else
1972 (void)playPennybagsComment(udsound::PennybagsVoice::JailPayMoneyOrTryForDoubles,
1973 udsound::TokenVoiceClipPolicy::WaitForAnyOldSoundThenPlay, false);
1974 }
1975
1976 std::expected<void, std::string> syncSequenceAudio(SequencePlayback& session)
1977 {
1978 auto* output = audioPlayback();
1979 if (output == nullptr)
1980 {
1981 for (const auto& instance : session.runtime().soundInstances())
1982 (void)session.runtime().requestSoundFailure(instance.node);
1983 return {};
1984 }
1985
1986 const auto instances = session.runtime().soundInstances();
1987 std::vector<sequence::SequenceNodeId> next;
1988 next.reserve(instances.size());
1989 for (const auto& instance : instances)
1990 {
1991 const audio::PlaybackKey key{audio::PlaybackDomain::Sequence, instance.node};
1992 const bool known = std::find(activeSequenceSounds.begin(),
1994 std::uint8_t effectiveVolume = instance.volume;
1995 std::int32_t pan = instance.dimensionality == 0 ?
1996 static_cast<std::int32_t>(instance.panning) : 0;
1997 if (const auto center = session.soundScreenCenterX2D(instance.node);
1998 instance.dimensionality == 2 && center)
1999 {
2000 const auto spatial = audio::legacy2DSoundMix(
2001 instance.volume, *center);
2002 effectiveVolume = spatial.volume;
2003 pan = spatial.panning;
2004 }
2005 // SEQCMD_SetVolume/SetPan write the sound buffer directly, even
2006 // for 2D sounds. Only a later positional update replaces them.
2007 if (instance.volumeOverride) effectiveVolume = *instance.volumeOverride;
2008 if (instance.panningOverride) pan = *instance.panningOverride;
2009 const float gain =
2010 static_cast<float>(effectiveVolume) / 100.0F;
2011 if (!known)
2012 {
2013 std::expected<void, std::string> started =
2014 instance.contentsDataId != data::EmptyDataId
2015 ? output->play(
2016 key,
2017 instance.contentsDataId,
2018 gain,
2019 instance.endingAction == 3,
2020 instance.pitch,
2021 pan)
2022 : output->playFile(
2023 key,
2024 instance.fileName,
2025 gain,
2026 instance.endingAction == 3,
2027 instance.pitch,
2028 pan);
2029 if (!started)
2030 return std::unexpected(started.error());
2031 }
2032 else
2033 {
2034 output->setGain(key, gain);
2035 output->setPitch(key, instance.pitch);
2036 output->setPanning(key, pan);
2037 output->setLooping(key, instance.endingAction == 3);
2038 }
2039 const auto synchronized = output->synchronizeSequence(key,
2040 instance.clock, false, instance.seekGeneration);
2041 // Retail SEQCMD_Pause gates the sequence clock, while the sound
2042 // hardware continues playing (L_Rend0D::FeedAudioStream).
2043 if (!synchronized) return synchronized;
2044 if (const auto position = output->positionTicks(key))
2045 {
2046 const auto supplied = session.runtime().requestSoundClock(instance.node, *position);
2047 if (!supplied) return std::unexpected(std::string("sequence sound clock failed"));
2048 }
2049 else
2050 (void)session.runtime().requestSoundFailure(instance.node);
2051 next.push_back(instance.node);
2052 }
2053
2054 for (const auto node : activeSequenceSounds)
2055 {
2056 if (std::find(next.begin(), next.end(), node) == next.end())
2057 output->stop({audio::PlaybackDomain::Sequence, node});
2058 }
2059 activeSequenceSounds = std::move(next);
2060 output->update();
2061 return {};
2062 }
2063
2064 std::expected<void, std::string> syncMonopolyAudio(
2065 const rules::GameState& ruleState,
2067 {
2068 auto* output = audioPlayback();
2069 if (output == nullptr) return {};
2070 const auto count = std::min<rules::PlayerNumber>(
2071 ruleState.numberOfPlayers, rules::MaxPlayers);
2072 for (rules::PlayerNumber player = 0; player < count; ++player)
2073 {
2074 const auto cash = monopolySoundRuntime.syncCash(
2075 *output, iBarBackdropPlayback.scoreTextState(player).lastCashChange);
2076 if (!cash) return cash;
2077 }
2078 const auto creditsMusic =
2079 displayState.desired2DView == display::Screen2D::Options &&
2080 userinterface::optionsStateReadOnly().currentScreen ==
2081 optionsui::Screen::Credits;
2082 const auto music = monopolySoundRuntime.syncMusic(
2083 *output, runtime::state().gameInProgress,
2084 displayState.optionMusicOn, displayState.optionMusicTuneIndex,
2085 creditsMusic);
2086 if (!music) return music;
2087 const auto watched = monopolySoundRuntime.syncTokenVoices(*output);
2088 if (!watched) return std::unexpected(watched.error());
2090 return {};
2091 }
2092
2095
2096 bool initialize(
2098 presentation::PresentMode presentMode)
2099 {
2100 if (window == nullptr)
2101 {
2102 return false;
2103 }
2104
2106 auto* preferencePath = SDL_GetPrefPath("Giscolab", "Monopoly");
2107 if (!preferencePath) return false;
2108 const auto historyPath = std::filesystem::path(preferencePath) / "acchist.txt";
2109 const auto playerHistoryPath = std::filesystem::path(preferencePath) / "Monopoly.ini";
2111 if (const auto configured = playerselection::configureHistory(playerHistoryPath); !configured)
2112 {
2113 std::cerr << "Player history: " << configured.error() << '\n';
2114 gameWindow = nullptr;
2115 return false;
2116 }
2117 statsAccountRuntime.emplace();
2118 if (const auto opened = statsAccountRuntime->open(historyPath); !opened)
2119 {
2120 std::cerr << "Stats account history: " << opened.error() << '\n';
2121 statsAccountRuntime.reset();
2122 gameWindow = nullptr;
2123 return false;
2124 }
2125 rules::cards::setBankPayoutObserver([](rules::cards::BankPayout payout)
2126 {
2127 if (auto* accounts = statsAccounts())
2128 {
2129 if (payout == rules::cards::BankPayout::Dividend50) accounts->recordDividend();
2130 else accounts->recordBankError();
2131 }
2132 });
2133
2134 if (!uimsg::initialize())
2135 {
2136 gameWindow = nullptr;
2137 return false;
2138 }
2139
2140 if (!timers::initialize())
2141 {
2142 uimsg::shutdown();
2143 gameWindow = nullptr;
2144 return false;
2145 }
2146
2152 true,
2153 nullptr
2154 );
2155
2156 if (gpuDevice == nullptr)
2157 {
2158 std::cerr
2159 << "SDL_CreateGPUDevice failed: "
2160 << SDL_GetError()
2161 << '\r\n';
2162
2163 timers::shutdown();
2164 uimsg::shutdown();
2165 gameWindow = nullptr;
2166 return false;
2167 }
2168
2170 {
2171 std::cerr
2172 << "SDL_ClaimWindowForGPUDevice failed: "
2173 << SDL_GetError()
2174 << '\r\n';
2175
2177 gpuDevice = nullptr;
2178
2179 timers::shutdown();
2180 uimsg::shutdown();
2181 gameWindow = nullptr;
2182 return false;
2183 }
2184
2185 const auto configuredPresentation =
2186 presentation::configureSwapchain(
2187 gpuDevice,
2188 gameWindow,
2189 presentMode);
2191 {
2192 std::cerr << "GPU presentation setup failed: "
2193 << configuredPresentation.error() << '\r\n';
2196 gpuDevice = nullptr;
2197 timers::shutdown();
2198 uimsg::shutdown();
2199 gameWindow = nullptr;
2200 return false;
2201 }
2203 presentation::toSDLPresentMode(presentMode))
2204 {
2205 std::cerr
2206 << "Requested GPU present mode is unsupported; "
2207 << "falling back to VSync.\r\n";
2208 }
2209
2210 // display.cpp original charge le fond 3D pendant
2211 // DISPLAY_initialize().
2212 //
2213 // Ce bitmap n'est pas indispensable au démarrage :
2214 // en cas d'absence on conserve simplement un fond noir.
2215 if (!legacyassets::initialize(gpuDevice))
2216 {
2217 std::cerr
2218 << "Legacy 3D background unavailable: "
2219 << SDL_GetError()
2220 << '\r\n';
2221 }
2222
2223 voicechat::setReceiveEventSink(&receiveVoiceChatAudioEvent);
2224 return true;
2225 }
2226
2228 {
2229 // Le vieux timer Windows tournait indépendamment à 60 Hz.
2230 // Notre implémentation moderne rattrape ici les ticks écoulés.
2231 timers::pump();
2232
2233 messaging::pumpNetwork();
2234 if (messaging::consumeHostDisconnected())
2235 {
2236 // Source MESS_StopAllNetworking: return to a fresh local game,
2237 // never promote a disconnected client into the old game's bank.
2238 if (!returnToLocalPlayerSelection()) return false;
2239 }
2240 if (auto help = optionsui::pollFullHelp(); !help)
2241 std::cerr << "Full help: " << help.error() << '\n';
2242 // MESS owns connectivity; closing the final connection tears down both
2243 // capture and playback, including sources that never sent STOP.
2244 if (!messaging::networkMode() && (voiceChatNetworkActive ||
2245 (voiceChatAudioRuntime && voiceChatAudioRuntime->captureActive())))
2246 {
2247 stopVoiceChat();
2249 voiceChatAudioRuntime->closeAllReceivers();
2250 voicechat::resetSession();
2251 }
2252 voiceChatNetworkActive = messaging::networkMode();
2254 voiceChatStartPending = false;
2255 else if (voiceChatStartPending &&
2256 messaging::queuedActionCount() < messaging::MessageQueueCapacity)
2258 const std::string networkError(messaging::networkError());
2259 if (!networkError.empty() && networkError != lastVoiceNetworkError)
2260 std::cerr << "Voice network: " << networkError << '\n';
2261 lastVoiceNetworkError = networkError;
2262
2263 if (voiceChatAudioRuntime && voiceChatAudioRuntime->captureActive())
2264 {
2265 const auto pumped =
2266 voiceChatAudioRuntime->pumpCapture(timers::tickCount());
2267 if (!pumped)
2268 {
2269 std::cerr << "Voice chat capture stopped: "
2270 << pumped.error() << '\n';
2271 voiceChatAudioRuntime->stopCapture();
2272 }
2273 }
2274
2275 // Ensuite viendront les équivalents de :
2276 // LI_SEQNCR_TimerTick()
2277 // LI_ANIM3D_TickScene()
2278
2279 // La presentation SDL_GPU etait auparavant definie mais jamais
2280 // appelee. Un cycle moteur correspond maintenant a une soumission
2281 // de frame, comme le cycle d'affichage ArtLib original.
2282 std::optional<std::pair<World3DRect, World3DCamera>> retailWorldView;
2283 bool modernBoardPublished = false;
2284 auto* session = sequencePlayback();
2285 if (session)
2286 {
2287 // Starts may resolve/decode meshes before update(), so include
2288 // the complete sync/publish CPU interval on the first board frame.
2289 const auto publishStarted = !startupCPUProfile.published ? startupCPUProfile.start() : StartupCPUProfile::Clock::time_point{};
2290 const auto tick = timers::tickCount();
2291 if (tick > static_cast<std::uint64_t>(std::numeric_limits<std::int32_t>::max()))
2292 return SDL_SetError("Sequence parent clock exceeds signed runtime range");
2293 openingMovies.update(tick, sequenceVideoRuntime.states(), *session);
2296 {
2297 const auto cleaned = sequenceVideoRuntime.reset(*session);
2299 }
2301 {
2302 // Video surfaces enter the same command FIFO as all other UI
2303 // surfaces, before the single update/presentation below.
2304 const auto videoSync = sequenceVideoRuntime.sync(*session);
2305 if (!videoSync)
2306 {
2307 if (!openingMovies.active())
2308 return SDL_SetError("Sequence video runtime: %s", videoSync.error().c_str());
2309 openingMovies.decoderFailed(videoSync.error(), *session);
2310 const auto cleaned = sequenceVideoRuntime.reset(*session);
2312 if (!cleaned)
2313 std::cerr << "Opening video cleanup queued for retry: "
2314 << cleaned.error() << '\n';
2316 }
2317 else for (const auto& notice : *videoSync)
2318 {
2319 uimsg::Message message{};
2320 message.type = uimsg::Type::VideoJump;
2321 message.numberA = notice.event.decisionFrame;
2322 message.numberB = notice.event.jumpToFrame;
2323 message.numberC = notice.event.alternativeTaken ? 1 : 0;
2324 (void)uimsg::send(message);
2325 }
2326 }
2327 std::optional<openingmovies::MovieRect> openingMovieRect;
2328 const auto movieResources = session->resources();
2329 const auto& movieDisplay = display::stateReadOnly();
2330 const bool modernOpeningContext = modernSceneOptions.proceduralBoard && movieResources &&
2331 movieResources->context().board == data::BoardEdition::Usa &&
2332 movieResources->context().language == data::LanguageId::EnglishUs &&
2333 movieDisplay.city == 0 && movieDisplay.system == 13 && movieDisplay.customBoardPath.empty();
2334 if (modernOpeningContext && openingMovies.phase() == openingmovies::Phase::Movies)
2335 for (const auto& movie : sequenceVideoRuntime.states())
2336 if (movie.node == openingMovies.movieNode() && movie.initialized && movie.surface &&
2337 movie.boundingBox && movie.options.drawSolid && movie.options.alphaLevel == 255 &&
2338 movie.worldTransform.values == sequence::identity2D().values)
2339 {
2340 const auto& box = *movie.boundingBox;
2341 if (box.left >= 3 && box.top >= 3 && box.right <= 797 && box.bottom <= 597 &&
2342 box.left < box.right && box.top < box.bottom)
2343 openingMovieRect = openingmovies::MovieRect{box.left,box.top,box.right,box.bottom};
2344 break;
2345 }
2347 [](std::string_view text)->std::expected<data::LegacyBitmapRGBA8,std::string>
2348 {
2349 auto* font = fontPlayback();
2350 if (!font || !font->ready()) return std::unexpected("opening skip hint font unavailable");
2351 auto image = font->renderPresentation(text,0xD3EBF5,18,400,false);
2352 if (!image) return std::unexpected(image.error().detail);
2353 return std::move(*image);
2354 }, *session);
2356 std::cerr << "Opening movie decoration unavailable: " << movieDecoration.error() << '\n';
2358 const auto& displayState = display::stateReadOnly();
2359 const bool boardVisible =
2360 display::isBoardVisible(displayState.desired2DView);
2361 const bool iBarVisible =
2362 display::isIBarVisible(displayState.desired2DView);
2363 const auto& ruleState = userinterface::ruleStateReadOnly();
2364 // Source generates at game start/load, after player configuration.
2365 // Only game screens consume deeds; setup and movies need no templates.
2366 if (boardVisible || displayState.desired2DView == display::Screen2D::Auction)
2367 {
2368 const auto deedsSync = syncEuropeanDeeds(*session, displayState.city,
2369 displayState.system, ruleState.options.housesPerHotel);
2370 if (!deedsSync)
2371 return SDL_SetError("Europe deed generation: %s", deedsSync.error().c_str());
2372 }
2374 playerselection::renderStateReadOnly(), fontPlayback(), *session);
2376 return SDL_SetError("Player selection playback: %s", playerSelectionSync.error().c_str());
2377 if (const auto started = playerSelectionPlayback.takeStartedPhase())
2378 playerselection::visualPhaseStarted(*started);
2379 playerselection::setPlaybackState(playerSelectionPlayback.interactable(),
2380 playerSelectionPlayback.ruleHits(), playerSelectionPlayback.restoreRect(),
2381 playerSelectionPlayback.shortRect());
2382 const auto auctionSync = auctionPlayback.sync(
2383 userinterface::auctionStateReadOnly(), ruleState,
2384 displayState.desired2DView, displayState.city, *session);
2385 if (!auctionSync)
2386 return SDL_SetError("Auction playback: %s", auctionSync.error().c_str());
2387 const auto auctionTextSync = auctionTextPlayback.sync(
2388 userinterface::auctionStateReadOnly(), ruleState,
2389 displayState.desired2DView, displayState.system,
2390 fontPlayback(), *session);
2391 if (!auctionTextSync)
2392 return SDL_SetError("Auction text playback: %s",
2393 auctionTextSync.error().c_str());
2394 const auto pennyBagsSync = auctionPennyBagsPlayback.sync(
2395 userinterface::auctionState(), ruleState,
2396 displayState.desired2DView, *session,
2397 [](std::uint32_t playerMask, std::int64_t serial)
2398 -> std::expected<void, std::string>
2399 {
2400 return userinterface::sendReadyResponses(
2401 playerMask, serial);
2402 });
2403 if (!pennyBagsSync)
2404 return SDL_SetError("Auction Pennybags playback: %s",
2405 pennyBagsSync.error().c_str());
2406 if (pennyBagsSync->sound)
2407 {
2408 const auto& sound = *pennyBagsSync->sound;
2409 std::expected<void, std::string> played{};
2410 if (sound.specificOffset)
2411 {
2412 auto* output = audioPlayback();
2413 if (output != nullptr)
2414 {
2415 const auto* choice = udsound::pennybagsChoice(
2416 output->boardEdition(), sound.voice);
2417 if (choice == nullptr || *sound.specificOffset >= choice->alternateCount)
2418 return SDL_SetError("Auction Pennybags specific offset is invalid");
2419 const auto wave = data::packDataId(data::LegacyGroupId::LanguageDialog,
2420 static_cast<data::DataTag>(choice->firstTag + *sound.specificOffset));
2422 udsound::TokenVoiceClipPolicy::ClipOldSoundIfPlaying, false);
2423 }
2424 }
2425 else
2426 {
2427 played = playPennybagsComment(sound.voice,
2428 udsound::TokenVoiceClipPolicy::ClipOldSoundIfPlaying, false);
2429 }
2430 if (!played) return SDL_SetError("Auction Pennybags audio: %s", played.error().c_str());
2431 }
2432 if (pennyBagsSync->requestedBackdrop)
2433 display::setBackdrop(*pennyBagsSync->requestedBackdrop);
2434 const auto tradeBackdropSync = tradeBackdropPlayback.sync(
2435 userinterface::tradeStateReadOnly(), ruleState,
2436 displayState.desired2DView, *session);
2437 if (!tradeBackdropSync)
2438 return SDL_SetError("Trade backdrop playback: %s",
2439 tradeBackdropSync.error().c_str());
2440 const auto tradeTokenSync = tradeTokenPlayback.sync(
2441 userinterface::tradeStateReadOnly(), ruleState,
2442 displayState.desired2DView, *session);
2443 if (!tradeTokenSync)
2444 return SDL_SetError("Trade token playback: %s",
2445 tradeTokenSync.error().c_str());
2447 userinterface::tradeStateReadOnly(),
2448 displayState.desired2DView, *session);
2450 return SDL_SetError("Trade action-button playback: %s",
2451 tradeActionButtonSync.error().c_str());
2452 const auto optionsFileSync = optionsFilePlayback.sync(
2453 userinterface::optionsStateReadOnly(),
2454 displayState.desired2DView, *session);
2455 if (!optionsFileSync)
2456 return SDL_SetError("Options File-screen playback: %s",
2457 optionsFileSync.error().c_str());
2458 const auto optionsSaveSync = optionsSavePlayback.sync(
2459 userinterface::optionsSaveStateReadOnly(),
2460 displayState.desired2DView, fontPlayback(), *session);
2461 if (!optionsSaveSync)
2462 return SDL_SetError("Options Load/Save playback: %s",
2463 optionsSaveSync.error().c_str());
2465 userinterface::optionsCustomBoardState(),
2466 displayState.desired2DView, fontPlayback(), *session);
2468 return SDL_SetError("Options custom-board playback: %s",
2469 optionsCustomBoardSync.error().c_str());
2470 const auto escapeSync = escapeConfirmationPlayback.sync(
2471 ibar::stateReadOnly(), isUsaBoardEdition(), *session);
2472 if (!escapeSync)
2473 return SDL_SetError("Escape confirmation playback: %s",
2474 escapeSync.error().c_str());
2475 const auto chatTextSync = chatTextPlayback.sync(
2476 chat::stateReadOnly(), ruleState, userinterface::chatSenderPlayer(),
2477 fontPlayback(), *session);
2478 if (!chatTextSync)
2479 return SDL_SetError("Chat text playback: %s", chatTextSync.error().c_str());
2480 const auto chatRecipientSync = chatRecipientPlayback.sync(
2481 chat::stateReadOnly(), ruleState, *session);
2482 if (!chatRecipientSync)
2483 return SDL_SetError("UDChat recipient playback: %s",
2484 chatRecipientSync.error().c_str());
2485 const auto chatOptionSync = chatOptionPlayback.sync(
2486 chat::stateReadOnly(), *session);
2487 if (!chatOptionSync)
2488 return SDL_SetError("UDChat option playback: %s",
2489 chatOptionSync.error().c_str());
2490 const auto chatFluffSync = chatFluffPlayback.sync(
2491 chat::stateReadOnly(), *session, true);
2492 if (!chatFluffSync)
2493 return SDL_SetError("UDChat fluff playback: %s",
2494 chatFluffSync.error().c_str());
2495 const auto statsSync = statsPlayback.sync(
2496 userinterface::statsStateReadOnly(),
2497 displayState.desired2DView, *session);
2498 if (!statsSync)
2499 return SDL_SetError("UDStats playback: %s",
2500 statsSync.error().c_str());
2502 displayState.desired2DView,
2503 userinterface::statsCalculatorStateReadOnly(),
2504 ruleState, *session);
2506 return SDL_SetError("UDStats calculator playback: %s",
2507 statsCalculatorSync.error().c_str());
2508 const auto statsCalculatorPickerSync =
2510 userinterface::statsCalculatorStateReadOnly(),
2511 displayState.city, displayState.desired2DView, *session);
2513 return SDL_SetError("UDStats calculator deed picker: %s",
2514 statsCalculatorPickerSync.error().c_str());
2516 const auto optionsVisualSync = optionsVisualPlayback.sync(
2517 userinterface::optionsState(), displayState.desired2DView,
2519 if (!optionsVisualSync)
2520 return SDL_SetError("Options visual playback: %s", optionsVisualSync.error().c_str());
2522 userinterface::optionsStateReadOnly(),
2523 displayState.desired2DView, *session);
2525 return SDL_SetError("Options navigation playback: %s",
2526 optionsNavigationSync.error().c_str());
2527 const auto optionsOptionSync = optionsOptionPlayback.sync(
2528 userinterface::optionsStateReadOnly(),
2529 displayState.desired2DView, *session);
2530 if (!optionsOptionSync)
2531 return SDL_SetError("Options Option-screen playback: %s",
2532 optionsOptionSync.error().c_str());
2533 const auto optionsToggleSync = optionsTogglePlayback.sync(
2534 userinterface::optionsStateReadOnly(),
2535 displayState.desired2DView, *session);
2536 if (!optionsToggleSync)
2537 return SDL_SetError("Options toggle playback: %s",
2538 optionsToggleSync.error().c_str());
2539 const auto optionsHelpSync = optionsHelpPlayback.sync(
2540 userinterface::optionsStateReadOnly(),
2541 displayState.desired2DView, *session);
2542 if (!optionsHelpSync)
2543 return SDL_SetError("Options Help-screen playback: %s",
2544 optionsHelpSync.error().c_str());
2545 const auto tradePropertySync = tradePropertyPlayback.sync(
2546 userinterface::tradeState(), ruleState,
2547 displayState.desired2DView, SDL_GetTicks(), *session,
2548 mouse::stateReadOnly(), tick, displayState.city);
2549 if (!tradePropertySync)
2550 return SDL_SetError("Trade deed playback: %s",
2551 tradePropertySync.error().c_str());
2552 const auto tradeIconSync = tradeOfferIconPlayback.sync(
2553 userinterface::tradeStateReadOnly(),
2554 displayState.desired2DView, *session);
2555 if (!tradeIconSync)
2556 return SDL_SetError("Trade offer-icon playback: %s",
2557 tradeIconSync.error().c_str());
2558 const auto tradeCashTextSync = tradeCashTextPlayback.sync(
2559 userinterface::tradeStateReadOnly(), ruleState,
2560 displayState.desired2DView, displayState.system,
2562 if (!tradeCashTextSync)
2563 return SDL_SetError("Trade cash-text playback: %s",
2564 tradeCashTextSync.error().c_str());
2566 userinterface::tradeStateReadOnly(), ruleState, displayState.desired2DView,
2568 if (!tradePartnerSync)
2569 return SDL_SetError("Trade partner selector: %s", tradePartnerSync.error().c_str());
2570 const auto tradeNameSync = tradeNamePlayback.sync(
2571 userinterface::tradeStateReadOnly(), ruleState,
2572 displayState.desired2DView, fontPlayback(), *session, modernTradeText);
2573 if (!tradeNameSync)
2574 return SDL_SetError("Trade name playback: %s",
2575 tradeNameSync.error().c_str());
2577 userinterface::tradeStateReadOnly(),
2578 displayState.desired2DView, fontPlayback(), *session, modernTradeText);
2579 if (!tradePanelTextSync)
2580 return SDL_SetError("Trade panel-text playback: %s",
2581 tradePanelTextSync.error().c_str());
2583 userinterface::tradeState(),
2584 displayState.desired2DView, tick, *session);
2586 return SDL_SetError("Trade cash-dialog playback: %s",
2587 tradeCashDialogSync.error().c_str());
2589 userinterface::tradeStateReadOnly(), ruleState,
2590 displayState.desired2DView, displayState.city,
2591 fontPlayback(), *session);
2593 return SDL_SetError("Trade contract-text playback: %s",
2594 tradeContractTextSync.error().c_str());
2596 userinterface::tradeStateReadOnly(),
2597 displayState.desired2DView, *session);
2598 if (!tradeContractSync)
2599 return SDL_SetError("Trade contract-dialog playback: %s",
2600 tradeContractSync.error().c_str());
2601 auto& dicePrompt = userinterface::dicePromptState();
2602 dicePrompt.show();
2603 const auto& iBarRules = userinterface::iBarRuleStateReadOnly();
2605 iBarRules.player < ruleState.numberOfPlayers &&
2606 iBarRules.player < rules::MaxPlayers
2607 ? iBarRules.player
2608 : ruleState.currentPlayer;
2609 const auto effectiveRuleMode = ibar::resolveRuleMode(
2612 ibar::resolveRulePlayer(projectedIBarPlayer);
2613 const bool activePlayerCanTrade =
2614 iBarActivePlayer < ruleState.numberOfPlayers &&
2615 iBarActivePlayer < rules::MaxPlayers &&
2616 ruleState.players[iBarActivePlayer].currentSquare < 41;
2617 const bool tradeEligible = activePlayerCanTrade &&
2618 ui::localplayers::tradeSourcePlayer(
2619 ruleState, iBarActivePlayer) != rules::MaxPlayers;
2620 const auto bssmAvailability =
2625 statsPlayerInputs.iBarPlayerLocalHuman =
2626 iBarActivePlayer < rules::MaxPlayers &&
2627 ui::localplayers::slotIsLocalHumanPlayer(iBarActivePlayer);
2628 statsPlayerInputs.buildProperties = bssmAvailability.buildProperties;
2629 statsPlayerInputs.sellProperties = bssmAvailability.sellProperties;
2630 statsPlayerInputs.mortgageProperties = bssmAvailability.mortgageProperties;
2631 statsui::setPropertyActionContext(
2632 userinterface::statsState(), effectiveRuleMode, iBarActivePlayer,
2633 statsPlayerInputs.iBarPlayerLocalHuman,
2634 bssmAvailability.buildProperties, bssmAvailability.sellProperties,
2635 bssmAvailability.mortgageProperties, modernTradeText);
2636 statsPlayerAuxPlayback.prepareLayout(userinterface::statsState(), *session);
2637 const auto statsPlayerSync = statsPlayerPlayback.sync(
2638 userinterface::statsStateReadOnly(), ruleState, statsPlayerInputs,
2639 displayState.desired2DView, *session);
2640 if (!statsPlayerSync)
2641 return SDL_SetError("UDStats Player playback: %s",
2642 statsPlayerSync.error().c_str());
2644 userinterface::statsStateReadOnly(), ruleState, statsPlayerInputs,
2645 displayState.desired2DView, *session);
2647 return SDL_SetError("UDStats Player cash playback: %s",
2648 statsPlayerCashSync.error().c_str());
2650 userinterface::statsStateReadOnly(), ruleState, statsPlayerInputs,
2651 displayState.desired2DView, *session);
2652 if (!statsPlayerAuxSync)
2653 return SDL_SetError("UDStats Player aux playback: %s",
2654 statsPlayerAuxSync.error().c_str());
2655 statsui::setFutureImmunityIcons(
2656 userinterface::statsFutureImmunityState(),
2657 statsPlayerAuxPlayback.iconHits());
2659 userinterface::statsFutureImmunityStateReadOnly(),
2660 displayState.desired2DView, *session);
2662 return SDL_SetError("UDStats Future/Immunity playback: %s",
2663 statsFutureImmunitySync.error().c_str());
2664 const auto statsFutureImmunityTextSync =
2666 userinterface::statsFutureImmunityStateReadOnly(), ruleState,
2667 displayState.desired2DView, displayState.city,
2668 fontPlayback(), *session);
2670 return SDL_SetError("UDStats Future/Immunity text playback: %s",
2671 statsFutureImmunityTextSync.error().c_str());
2672 const auto statsDeedSync = statsDeedPlayback.sync(
2673 userinterface::statsStateReadOnly(), ruleState, statsPlayerInputs,
2674 displayState.desired2DView, *session);
2675 if (!statsDeedSync)
2676 return SDL_SetError("UDStats Deed playback: %s",
2677 statsDeedSync.error().c_str());
2678 const bool calculatorDeedPopupVisible =
2679 userinterface::statsCalculatorStateReadOnly().picker == statsui::CalculatorPicker::Deed;
2681 userinterface::statsStateReadOnly(), ruleState, statsPlayerInputs,
2682 displayState.city, displayState.desired2DView, *session,
2685 return SDL_SetError("UDStats Deed floater playback: %s",
2686 statsDeedFloaterSync.error().c_str());
2688 userinterface::statsStateReadOnly(), ruleState, statsPlayerInputs,
2689 displayState.system, displayState.desired2DView,
2692 return SDL_SetError("UDStats Deed floater text playback: %s",
2693 statsDeedFloaterTextSync.error().c_str());
2694 const auto statsDeedBarSync = statsDeedBarPlayback.sync(
2695 userinterface::statsStateReadOnly(), ruleState, statsPlayerInputs,
2696 displayState.desired2DView, *session);
2697 if (!statsDeedBarSync)
2698 return SDL_SetError("UDStats Deed owner-bar playback: %s",
2699 statsDeedBarSync.error().c_str());
2701 userinterface::statsStateReadOnly(), ruleState, statsPlayerInputs,
2702 displayState.system, displayState.desired2DView,
2705 return SDL_SetError("UDStats Deed value-text playback: %s",
2706 statsDeedValueTextSync.error().c_str());
2708 {
2709 const auto statsResources = session->resources();
2710 const auto statsTextSync = statsTextPlayback.sync(
2711 userinterface::statsStateReadOnly(), ruleState, statsPlayerInputs,
2712 userinterface::statsCalculatorStateReadOnly(),
2713 userinterface::statsFutureImmunityStateReadOnly(), statsAccountRuntime->state(),
2714 displayState.city, displayState.system, displayState.desired2DView,
2715 fontPlayback(), *session, modernSceneOptions.proceduralBoard && statsResources &&
2716 statsResources->context().board == data::BoardEdition::Usa &&
2717 statsResources->context().language == data::LanguageId::EnglishUs &&
2718 displayState.city == 0 && displayState.system == 13 &&
2719 displayState.customBoardPath.empty());
2720 if (!statsTextSync)
2721 return SDL_SetError("Stats text playback: %s", statsTextSync.error().c_str());
2722 const auto& stats = userinterface::statsStateReadOnly();
2723 if (displayState.desired2DView == display::Screen2D::Portfolio &&
2724 stats.screen == statsui::Screen::Bank && stats.activeSort == 3)
2725 statsAccountRuntime->setScrollLimit(statsTextPlayback.historyScrollLimit());
2726 }
2727 const auto statsBankSync = statsBankPlayback.sync(
2728 userinterface::statsStateReadOnly(), ruleState,
2729 displayState.desired2DView, *session,
2730 statsAccountRuntime ? &statsAccountRuntime->state() : nullptr);
2731 if (!statsBankSync)
2732 return SDL_SetError("UDStats Bank playback: %s", statsBankSync.error().c_str());
2733 const auto& iBarState = ibar::stateReadOnly();
2734 const auto selectedDeed = iBarState.selectedDeed;
2735 const auto selectedBit = selectedDeed
2736 ? ibar::layout::propertyBit(*selectedDeed)
2737 : 0u;
2738
2740 iBarInputs.desired2DView = displayState.desired2DView;
2741 iBarInputs.ruleMode = effectiveRuleMode;
2742 iBarInputs.rulePlayer = projectedIBarPlayer;
2743 iBarInputs.trackRules = !ibar::stateReadOnly().localRuleModeActive;
2744 iBarInputs.gameInProgress = runtime::state().gameInProgress;
2745 iBarInputs.tradeEligible = tradeEligible;
2746 iBarInputs.rollDiceDesired = dicePrompt.currentStartTurn;
2747 iBarInputs.raiseCashCanBankrupt = iBarRules.raiseCashCanBankrupt;
2748 const bool deedActive = effectiveRuleMode == ibar::RuleMode::DeedActive;
2749 iBarInputs.canBuild = deedActive
2750 ? (bssmAvailability.buildProperties & selectedBit) != 0
2751 : bssmAvailability.buildProperties != 0;
2752 iBarInputs.canSell = deedActive
2753 ? (bssmAvailability.sellProperties & selectedBit) != 0
2754 : bssmAvailability.sellProperties != 0;
2755 iBarInputs.canMortgage = deedActive
2756 ? (bssmAvailability.mortgageProperties & selectedBit) != 0
2757 : bssmAvailability.mortgageProperties != 0;
2758 iBarInputs.canUnmortgage = deedActive
2759 ? (bssmAvailability.unmortgageProperties & selectedBit) != 0
2760 : bssmAvailability.unmortgageProperties != 0;
2761 iBarInputs.aiButtonRemoteState =
2762 effectiveRuleMode == ibar::RuleMode::OtherPlayerRemote
2763 ? false
2764 : iBarActivePlayer != rules::BankPlayer &&
2765 !ui::localplayers::slotIsLocalHumanPlayer(iBarActivePlayer);
2766 iBarInputs.bankHovered =
2767 iBarState.playerCurrentMouseOver ==
2768 static_cast<int>(rules::BankPlayer);
2769 iBarInputs.pressedButtonIndex =
2770 iBarState.pendingPressedButton;
2771 iBarInputs.desiredCardIndex =
2772 iBarState.desiredCardIndex;
2773 iBarInputs.desiredBuyAuctionSquare =
2774 iBarState.desiredBuyAuctionSquare;
2775 iBarInputs.desiredBoardCamera = displayState.desiredBoardCamera;
2776
2778 scoreInputs.gameInProgress = runtime::state().gameInProgress;
2779 scoreInputs.hoveredPlayer = iBarState.playerCurrentMouseOver;
2780 scoreInputs.tick = static_cast<std::uint32_t>(tick);
2781 for (std::size_t player = 0; player < iBarState.players.size(); ++player)
2782 scoreInputs.visiblePlayers[player] = iBarState.players[player].visible;
2783 const auto scorePlan = ibar::planScoreStrip(ruleState, scoreInputs);
2784 if (!scorePlan)
2785 return SDL_SetError("IBar score strip plan: %s", scorePlan.error().c_str());
2786 iBarInputs.scoreStrip = *scorePlan;
2787
2790 (displayState.desired2DView == display::Screen2D::Main ||
2791 displayState.desired2DView == display::Screen2D::Trade);
2794 titleInputs.projectedMode = iBarRules.mode;
2795 titleInputs.buildProperties = bssmAvailability.buildProperties;
2796 titleInputs.sellProperties = bssmAvailability.sellProperties;
2797 titleInputs.mortgageProperties = bssmAvailability.mortgageProperties;
2798 titleInputs.unmortgageProperties = bssmAvailability.unmortgageProperties;
2799 titleInputs.freeUnmortgageProperties = iBarRules.freeUnmortgageSet;
2800 titleInputs.placeBuildingProperties = iBarRules.placeBuildingSet;
2801 titleInputs.selectedDeed = selectedDeed;
2802 iBarInputs.propertyTitles = ibar::planPropertyTitles(ruleState, titleInputs);
2803 ibar::setPropertyHitState(iBarInputs.propertyTitles.visibleProperties);
2804 iBarInputs.propertyCurrentMouseOver =
2805 iBarState.propertyCurrentMouseOver;
2806 iBarInputs.tick = tick;
2807
2808 const auto actionHitState = ibar::ruleActionHitState(
2810 ibar::setRuleActionHitState(
2811 actionHitState.layout,
2812 actionHitState.activeSlots,
2813 iBarInputs.ruleMode,
2815 iBarInputs.aiButtonRemoteState);
2816 const auto backdropSync = iBarBackdropPlayback.sync(
2817 ruleState, iBarVisible, iBarActivePlayer, *session,
2818 iBarInputs);
2819 if (!backdropSync)
2820 return SDL_SetError("IBar backdrop playback: %s",
2821 backdropSync.error().c_str());
2822 std::array<ibar::ScoreTextState, rules::MaxPlayers> scoreTextStates{};
2823 for (rules::PlayerNumber player = 0; player < rules::MaxPlayers; ++player)
2824 scoreTextStates[player] = iBarBackdropPlayback.scoreTextState(player);
2825 const auto resources = startup::resources();
2826 const auto scoreTextSync = iBarScoreTextPlayback.sync(
2828 resources ? resources->context().board : data::BoardEdition::Usa,
2830 modernSceneOptions.proceduralBoard && resources &&
2831 resources->context().board == data::BoardEdition::Usa &&
2832 resources->context().language == data::LanguageId::EnglishUs &&
2833 displayState.city == 0 && displayState.system == 13 &&
2834 displayState.customBoardPath.empty());
2835 if (!scoreTextSync)
2836 return SDL_SetError("IBar score text playback: %s",
2837 scoreTextSync.error().c_str());
2838 const auto runtimeTextSync = iBarRuntimeTextPlayback.sync(
2839 ruleState, iBarState, iBarRules, iBarVisible,
2840 displayState.desired2DView == display::Screen2D::Main ||
2841 displayState.desired2DView == display::Screen2D::Trade,
2842 iBarActivePlayer, tick, displayState.system,
2843 resources ? resources->context().board : data::BoardEdition::Usa,
2844 fontPlayback(), *session, modernSceneOptions.proceduralBoard && resources &&
2845 resources->context().board == data::BoardEdition::Usa &&
2846 resources->context().language == data::LanguageId::EnglishUs &&
2847 displayState.city == 0 && displayState.system == 13 &&
2848 displayState.customBoardPath.empty());
2849 if (!runtimeTextSync)
2850 return SDL_SetError("IBar runtime text playback: %s",
2851 runtimeTextSync.error().c_str());
2852 if (const auto cardToRead = iBarBackdropPlayback.takeCardReadRequest())
2853 {
2854 if (auto* output = audioPlayback())
2855 {
2856 const auto wave = penny::cardReadWave(
2857 output->boardEdition(), output->language(),
2858 displayState.city, displayState.system,
2859 *cardToRead);
2860 if (wave)
2861 {
2862 const auto spoken = playPennybagsSpecific(*wave,
2863 udsound::TokenVoiceClipPolicy::WaitForAnyOldSoundThenPlay,
2864 false);
2865 if (!spoken)
2866 return SDL_SetError("Card voice playback: %s",
2867 spoken.error().c_str());
2868 }
2869 }
2870 }
2871 if (!audioDisabled)
2872 {
2873 const auto soundSync = syncMonopolyAudio(ruleState, displayState);
2874 if (!soundSync)
2875 handleAudioPlaybackFailure("Monopoly audio playback disabled", soundSync.error());
2876 }
2877 if (const auto consumed = iBarBackdropPlayback.consumedPressedButton())
2878 ibar::clearPendingPressedButton(*consumed);
2879 const auto dice2DSync = dice2DPlayback.sync(ruleState.dice,
2880 dicePrompt.currentStartTurn, iBarVisible, dicePrompt.diceRollNotification, *session);
2881 if (!dice2DSync)
2882 return SDL_SetError("Dice 2D playback: %s", dice2DSync.error().c_str());
2883 const auto diceSync = syncDicePlayback(*session, tick,
2884 boardVisible, iBarVisible);
2885 if (!diceSync)
2886 return SDL_SetError("Dice playback: %s",
2887 diceSync.error().c_str());
2889 boardVisible, tick);
2890 if (!pieceSync)
2891 return SDL_SetError("Piece move playback: %s",
2892 pieceSync.error().c_str());
2893 // UDBoard.cpp uses TokenAnimStackIndex only to choose floating-camera amplitude.
2894 display::setTokenAnimationStackActive(pieceMovePlayback.active());
2895 const auto idleSync = syncPieceIdlePlayback(*session);
2896 if (!idleSync)
2897 return SDL_SetError("Piece idle playback: %s",
2898 idleSync.error().c_str());
2899 const auto persistentIdleSync =
2900 syncPersistentPieceIdles(*session, boardVisible);
2901 if (!persistentIdleSync)
2902 return SDL_SetError("Persistent piece idle: %s",
2903 persistentIdleSync.error().c_str());
2904 const auto shadowSync = syncPieceShadows(
2905 *session, ruleState, displayState, boardVisible);
2906 if (!shadowSync)
2907 return SDL_SetError("Piece shadow display: %s",
2908 shadowSync.error().c_str());
2909 const auto buildingSync = syncPieceBuildings(*session, boardVisible);
2910 if (!buildingSync)
2911 return SDL_SetError("Piece building display: %s",
2912 buildingSync.error().c_str());
2914 displayState.desired2DView, displayState.game3DOn, displayState.city,
2915 displayState.desiredBoardCamera, static_cast<std::uint32_t>(tick),
2916 displayState.customBoardPath};
2917 const auto boardBackdropSync = boardBackdropPlayback.sync(
2919 if (!boardBackdropSync)
2920 return SDL_SetError("Board backdrop playback: %s",
2921 boardBackdropSync.error().c_str());
2923 if (!boardSync)
2924 return SDL_SetError("Board sequence playback: %s",
2925 boardSync.error().c_str());
2926 const auto ownershipPlan = boarddisplay::planOwnershipHighlights(
2927 ruleState, displayState.desired2DView, displayState.board3DOn,
2928 displayState.desiredBoardCamera);
2929 if (!ownershipPlan)
2930 return SDL_SetError("Board ownership plan: %s",
2931 ownershipPlan.error().c_str());
2932 const auto ownershipSync = ownershipHighlightPlayback.sync(
2934 if (!ownershipSync)
2935 return SDL_SetError("Board ownership playback: %s",
2936 ownershipSync.error().c_str());
2937
2938 // L_Mouse/L_Seqncr used a top-level mouse grouping sequence that
2939 // followed the logical 800x600 pointer and temporarily yielded to
2940 // the native I-beam over the UDChat edit field.
2941 const auto pointerSync = mousePointerPlayback.sync(
2942 mouse::stateReadOnly(), *session);
2943 if (!pointerSync)
2944 {
2946 std::cerr << "Mouse pointer DAT playback unavailable: "
2947 << pointerSync.error() << '\n';
2949 }
2950 else
2951 {
2953 }
2954 const bool standardPointerPresentation = modernSceneOptions.proceduralBoard && resources &&
2955 resources->context().board == data::BoardEdition::Usa &&
2956 resources->context().language == data::LanguageId::EnglishUs &&
2957 displayState.city == 0 && displayState.system == 13 &&
2958 displayState.customBoardPath.empty();
2959 if (overlayRenderer)
2960 overlayRenderer->configureStandardPointerPresentation(standardPointerPresentation);
2961 const auto nativeKind = (openingMovies.active() ||
2963 ? mouse::NativeCursorKind::Hidden
2964 : mouse::nativeCursorKind(
2965 mouse::stateReadOnly(), !standardPointerPresentation && mousePointerPlayback.visible());
2966 if (!nativeMouseCursor.sync(nativeKind))
2967 return SDL_SetError("Mouse cursor synchronization failed: %s",
2968 SDL_GetError());
2969
2970 const auto desiredWorldViewport =
2971 display::worldViewport(displayState.viewportInUse);
2975 const auto* queuedViewport = session->commands().viewportState(
2976 static_cast<std::uint8_t>(RenderSlot::World3D));
2978 {
2979 const auto viewportQueued =
2980 session->setViewport3D(desiredWorldViewport);
2981 if (!viewportQueued)
2982 return SDL_SetError("World3D viewport command: %s",
2983 viewportQueued.error().c_str());
2984 }
2985
2986 const auto updateStarted = !startupCPUProfile.published ? startupCPUProfile.start() : StartupCPUProfile::Clock::time_point{};
2987 const auto updated = session->update(static_cast<std::int32_t>(tick));
2988 const auto sequenceUpdateEnded = !startupCPUProfile.published ? startupCPUProfile.start() : StartupCPUProfile::Clock::time_point{};
2989 if (!updated) return SDL_SetError("Sequence playback: %s", updated.error().c_str());
2990 std::vector<sequence::SequenceMeshRenderItem> decorations;
2991 std::vector<std::shared_ptr<const data::MeshRuntimeAsset>> staticSceneAssets;
2992 std::optional<data::MeshBounds> modernBoardBounds;
2993 std::optional<float> modernBoardGroundY;
2994 std::vector<data::MeshBounds> modernBuildingBounds;
2995 if (modernSceneOptions.parisBoard || modernSceneOptions.usaBoard || modernSceneOptions.proceduralBoard)
2996 {
2997 const auto boardInstances = session->runtime().meshInstances();
2998 for (const auto node : session->world().order())
2999 {
3000 const auto* board = session->world().find(node);
3001 if (!board || board->contentsDataId != data::modernSceneDefinition(
3002 data::ModernSceneKind::ParisBoard).legacyMeshId ||
3003 !board->asset ||
3004 board->asset->origin != data::MeshAssetOrigin::ModernGltf) continue;
3005 const bool boardRootPriority = std::any_of(boardInstances.begin(), boardInstances.end(),
3006 [&](const auto& instance)
3007 {
3008 return instance.node == node &&
3009 instance.rootSequencePriority == display::Board3DPriority;
3010 });
3011 if (!boardRootPriority) continue;
3012 const auto geometry = board->renderData ? board->renderData : board->asset->renderData;
3013 if (!geometry) continue;
3014 modernBoardGroundY = board->worldTransform.values[13];
3015 modernBoardBounds = presentationWorldBounds(geometry->bounds, board->worldTransform);
3016 if (modernSceneOptions.proceduralBoard) staticSceneAssets.push_back(board->asset);
3017 if (!modernSceneOptions.environment && !modernSceneOptions.usaBoard &&
3018 !modernSceneOptions.proceduralBoard) break;
3019 if (!modernEnvironment)
3020 {
3021 if (modernSceneOptions.proceduralEnvironment)
3022 SDL_SetWindowTitle(gameWindow, "Monopoly Modern - Loading game...");
3023 modernEnvironment = std::make_unique<ModernEnvironment>(
3024 std::filesystem::path(SDL_GetBasePath()) / "assets/modern", true);
3025 }
3026 const auto environmentStarted = !startupCPUProfile.published ? startupCPUProfile.start() : StartupCPUProfile::Clock::time_point{};
3027 decorations = modernEnvironment->items(board->worldTransform,
3028 static_cast<std::uint32_t>(tick), true,
3029 modernSceneOptions.environment && !modernSceneOptions.usaBoard,
3030 modernSceneOptions.proceduralEnvironment);
3031 if (!startupCPUProfile.published) startupCPUProfile.report("environment_items", environmentStarted, decorations.size());
3032 if(modernSceneOptions.proceduralEnvironment)
3033 for(const auto& decoration:decorations)
3034 {
3035 if(decoration.node == (ModernEnvironmentNodeBase|4U) ||
3036 decoration.node == (ModernEnvironmentNodeBase|5U)) continue;
3037 if (!decoration.asset) continue;
3038 const auto decorationGeometry = decoration.renderData ? decoration.renderData : decoration.asset->renderData;
3039 if (!decorationGeometry) continue;
3040 const auto bounds=presentationWorldBounds(decorationGeometry->bounds, decoration.worldTransform);
3041 const auto cityNode = decoration.node;
3042 if (cityNode == (ModernEnvironmentNodeBase | 2U) ||
3045 modernBuildingBounds.push_back(bounds);
3046 for(std::size_t axis=0;axis<3;++axis)
3047 {
3048 modernBoardBounds->minimum[axis]=std::min(modernBoardBounds->minimum[axis],bounds.minimum[axis]);
3049 modernBoardBounds->maximum[axis]=std::max(modernBoardBounds->maximum[axis],bounds.maximum[axis]);
3050 }
3051 }
3052 break;
3053 }
3054 }
3055 if (!staticSceneAssets.empty())
3056 for (const auto& decoration : decorations)
3057 if (decoration.asset && std::none_of(staticSceneAssets.begin(), staticSceneAssets.end(),
3058 [&](const auto& asset) { return asset == decoration.asset; }))
3059 staticSceneAssets.push_back(decoration.asset);
3060 const auto nativeStarted = !startupCPUProfile.published ? startupCPUProfile.start() : StartupCPUProfile::Clock::time_point{};
3061 const auto nativeSync = session->setNativeSceneItems(std::move(decorations));
3062 if (!nativeSync) return SDL_SetError("Native scene: %s", nativeSync.error().c_str());
3063 if (modernBoardBounds && !startupCPUProfile.published)
3064 {
3065 // Update ends before environment decode; retain separate CPU
3066 // interval by reporting its saved end rather than including it.
3067 if (startupCPUProfile.enabled)
3068 std::cerr << "Startup CPU profile: phase=sequence_update elapsed_ms="
3069 << std::chrono::duration<double,std::milli>(sequenceUpdateEnded-updateStarted).count()
3070 << " scope=CPU elapsed only; no added GPU fence\n";
3071 startupCPUProfile.report("native_scene_publish", nativeStarted, session->world().size());
3072 startupCPUProfile.report("modern_assets_publish_total", publishStarted, session->world().size());
3073 startupCPUProfile.published = true;
3074 }
3075 // Hidden 3D views skip rendering but must still retire stopped meshes.
3076 if (worldRenderer)
3077 if (auto* cache = worldRenderer->meshCache())
3078 {
3079 const bool validStaticContext = modernSceneOptions.proceduralBoard &&
3080 displayState.city == 0 && displayState.system == 13 && displayState.customBoardPath.empty();
3081 if (!staticSceneAssets.empty())
3082 {
3083 const auto retainedCount = staticSceneAssets.size();
3084 const bool retained = cache->retainStaticAssets(std::move(staticSceneAssets));
3085 static std::size_t reportedRetainedCount{};
3087 {
3088 std::cerr << "Modern static GPU scene: retained=" << retainedCount << '\n';
3090 }
3091 }
3092 else if (!validStaticContext) (void)cache->retainStaticAssets({});
3094 }
3095 publishSequenceLifecycleEvents(*session, static_cast<std::int32_t>(tick));
3096
3097 {
3098 const auto audioSync = syncSequenceAudio(*session);
3099 if (!audioSync)
3100 {
3101 handleAudioPlaybackFailure("Sequence audio playback disabled", audioSync.error());
3102 }
3103 }
3105 if (const auto* command = session->commands().viewportState(
3106 static_cast<std::uint8_t>(RenderSlot::World3D)))
3107 viewport = {command->viewport.left, command->viewport.top,
3108 command->viewport.right, command->viewport.bottom};
3109 if (viewport.empty()) session->world().clearView();
3110 else
3111 {
3112 World3DCamera camera = displayState.worldCamera;
3113 if (const auto* command = session->commands().cameraState(
3114 static_cast<std::uint8_t>(RenderSlot::World3D)))
3115 {
3116 if (!activeWorldCamera) activeWorldCamera = camera;
3117 if (const auto resolved = resolveWorld3DCamera(
3118 *command, session->runtime()))
3120 camera = *activeWorldCamera;
3121 }
3122 else
3123 {
3124 retailWorldView = std::pair{viewport, camera};
3125 if (modernBoardBounds && boardcamera::isPresentationSquareDefault(displayState))
3126 {
3127 int pixelWidth=0,pixelHeight=0;
3128 if(SDL_GetWindowSizeInPixels(gameWindow,&pixelWidth,&pixelHeight))
3129 camera = fitModernTopDownBoard(*modernBoardBounds, camera,
3130 static_cast<float>(viewport.right-viewport.left)/(viewport.bottom-viewport.top),
3131 modernBoardControlReservation(viewport,pixelWidth,pixelHeight,desiredWorldViewport.bottom));
3132 }
3133 else if (modernBoardBounds && boardcamera::isPresentationDefault(displayState))
3135 static_cast<float>(viewport.right-viewport.left)/(viewport.bottom-viewport.top),
3136 modernSceneOptions.proceduralEnvironment ? 42.0F : 48.0F,
3137 modernSceneOptions.proceduralEnvironment ? 28.0F : 8.0F, 0.23F);
3138 activeWorldCamera = camera;
3139 }
3140 const bool avoidModernBuildings = modernSceneOptions.proceduralEnvironment && resources &&
3141 resources->context().board == data::BoardEdition::Usa &&
3142 resources->context().language == data::LanguageId::EnglishUs &&
3143 displayState.city == 0 && displayState.system == 13 && displayState.customBoardPath.empty() &&
3144 displayState.desired2DView == display::Screen2D::Main &&
3145 displayState.viewportInUse == display::Viewport3D::Main && modernBoardBounds && modernBoardGroundY;
3146 // Render-only travel correction; labelled CNK, dice, BSSM,
3147 // floating and locked manual orbit views retain their composition.
3148 // A manually requested preset may interpolate after its transient
3149 // request flag clears; it is not guaranteed to remain excluded.
3150 if(avoidModernBuildings && displayState.bssmCameraState==0 && !displayState.diceCameraControlActive &&
3151 !displayState.floatingCameraActive && !displayState.manualCameraRequested &&
3152 !displayState.manualMouseCamLock && !session->commands().cameraState(
3153 static_cast<std::uint8_t>(RenderSlot::World3D)))
3157 std::max(camera.nearPlane * 2, 1.0F), std::max(camera.nearPlane * 8, 1.0F));
3158 const auto configured = session->world().configureView(viewport, camera);
3159 if (!configured) return SDL_SetError("Invalid DISPLAY World3D camera/viewport");
3160 if (!worldRenderer)
3161 {
3162 const auto shaderPath = std::filesystem::path(SDL_GetBasePath()) / "shaders";
3163 const auto rendererStarted = startupCPUProfile.start();
3164 auto loaded = World3DRenderer::load(gpuDevice, shaderPath,
3166 std::filesystem::path(SDL_GetBasePath()) /
3167 "assets/modern/lighting/studio_environment.mstudio");
3168 if (!loaded) return SDL_SetError("World3D pipeline: %s", loaded.error().detail.c_str());
3169 worldRenderer = std::move(*loaded);
3170 if (!startupCPUProfile.rendererReported) startupCPUProfile.report("renderer_load_shaders_studio", rendererStarted);
3171 startupCPUProfile.rendererReported = true;
3172 }
3173 worldRenderer->setBilinearFiltering(displayState.optionFilteringOn);
3174 }
3175 const auto lightingSync = syncBoardLighting(
3176 *session, ruleState, displayState);
3177 if (!lightingSync)
3178 return SDL_SetError("Board lighting: %s",
3179 lightingSync.error().c_str());
3180 if (worldRenderer)
3181 {
3182 // Derived from the actual published board, not the requested option.
3184 worldRenderer->setModernPresentation(modernBoardBounds.has_value());
3185 worldRenderer->setPresentationShadows(modernBoardBounds.has_value());
3186 worldRenderer->setPresentationAntialiasing(modernBoardBounds.has_value());
3188 }
3189 }
3190 if (session && session->world2D().size() && !overlayRenderer)
3191 {
3192 const auto shaderPath = std::filesystem::path(SDL_GetBasePath()) / "shaders";
3193 auto loaded = World2DRenderer::load(gpuDevice, shaderPath,
3195 if (!loaded) return SDL_SetError("World2D pipeline: %s", loaded.error().c_str());
3196 overlayRenderer = std::move(*loaded);
3197 // This lazy creation follows the native cursor update above. Apply
3198 // the same guard now so even its first frame omits the retail cursor.
3199 const auto pointerResources = startup::resources();
3200 const auto& pointerDisplayState = display::stateReadOnly();
3201 overlayRenderer->configureStandardPointerPresentation(
3202 modernSceneOptions.proceduralBoard && pointerResources &&
3203 pointerResources->context().board == data::BoardEdition::Usa &&
3204 pointerResources->context().language == data::LanguageId::EnglishUs &&
3205 pointerDisplayState.city == 0 && pointerDisplayState.system == 13 &&
3206 pointerDisplayState.customBoardPath.empty());
3207 }
3208 if (session && worldRenderer)
3209 if (auto* cache = worldRenderer->meshCache())
3210 {
3211 const bool profileUpload = startupCPUProfile.published && !startupCPUProfile.uploaded;
3212 const auto uploadStarted = profileUpload ? startupCPUProfile.start() : StartupCPUProfile::Clock::time_point{};
3213 bool modernBoardRejected{};
3214 const auto prepared = session->prepareModernMeshes(*cache,
3215 [&modernBoardRejected](const data::MeshRenderData* failed)
3216 {
3217 const auto paris = static_cast<std::size_t>(data::ModernSceneKind::ParisBoard);
3218 const auto usa = static_cast<std::size_t>(data::ModernSceneKind::UsaBoard);
3219 const auto procedural = static_cast<std::size_t>(data::ModernSceneKind::ProceduralUsaBoard);
3220 if (modernSceneMeshes[paris].get() == failed || modernSceneMeshes[usa].get() == failed ||
3222 modernBoardRejected = true;
3224 if (modernEnvironment) (void)modernEnvironment->rejectGeometry(failed);
3225 });
3226 if (!prepared)
3227 return SDL_SetError("Modern asset fallback: %s", prepared.error().c_str());
3228 if (profileUpload)
3229 {
3230 startupCPUProfile.report("prepare_modern_meshes_cpu", uploadStarted, session->world().size());
3231 startupCPUProfile.uploaded = true;
3232 }
3234 {
3235 modernBoardPublished = false;
3236 (void)cache->retainStaticAssets({});
3237 worldRenderer->setModernPresentation(false);
3238 worldRenderer->setPresentationShadows(false);
3239 worldRenderer->setPresentationAntialiasing(false);
3240 worldRenderer->setLighting(boardLightingController.current());
3241 if (retailWorldView)
3242 {
3244 const auto restored = session->world().configureView(retailWorldView->first, retailWorldView->second);
3245 if (!restored) return SDL_SetError("Retail fallback camera restoration failed");
3246 }
3247 const auto removed = session->setNativeSceneItems({});
3248 if (!removed)
3249 return SDL_SetError("Native scene fallback: %s", removed.error().c_str());
3250 }
3251 }
3252 const auto& presentationState = display::stateReadOnly();
3253 const bool modernMenuBackdrop = modernSceneOptions.proceduralBoard && !openingMovies.active() &&
3254 presentationState.city == 0 && presentationState.system == 13 &&
3255 presentationState.customBoardPath.empty() &&
3256 (presentationState.current2DView == display::Screen2D::Options ||
3257 presentationState.current2DView == display::Screen2D::PlayerSelect ||
3258 presentationState.current2DView == display::Screen2D::PlayerSelectRules ||
3259 presentationState.current2DView == display::Screen2D::Auction ||
3260 presentationState.current2DView == display::Screen2D::Trade);
3261 const auto presentationResources = session ? session->resources() : nullptr;
3262 const bool modernBoardBackdrop = modernBoardPublished && modernSceneOptions.proceduralBoard &&
3263 presentationResources && presentationResources->context().board == data::BoardEdition::Usa &&
3264 presentationResources->context().language == data::LanguageId::EnglishUs &&
3265 !openingMovies.active() && !openingMoviePresentation.active() &&
3266 presentationState.city == 0 && presentationState.system == 13 &&
3267 presentationState.customBoardPath.empty() &&
3268 (presentationState.current2DView == display::Screen2D::Portfolio ||
3269 presentationState.current2DView == display::Screen2D::Main);
3270 const SDL_FColor backdrop = modernBoardBackdrop ? SDL_FColor{22.0F/255, 60.0F/255, 61.0F/255, 1} :
3272 SDL_FColor{13.0F/255, 35.0F/255, 38.0F/255, 1} : SDL_FColor{0, 0, 0, 1};
3273 const bool profilePresent = startupCPUProfile.published && !startupCPUProfile.presented;
3274 const auto presentStarted = profilePresent ? startupCPUProfile.start() : StartupCPUProfile::Clock::time_point{};
3275 const auto presented=gpuframe::present(gpuDevice, gameWindow,
3277 session ? &session->world() : nullptr,
3278 overlayRenderer.get(), session ? &session->world2D() : nullptr, backdrop);
3280 {
3281 startupCPUProfile.report("first_modern_present_cpu", presentStarted, session ? session->world().size() : 0);
3282 startupCPUProfile.presented = true;
3283 }
3284 static bool presentationReported=false;
3285 if(presented && worldRenderer && worldRenderer->presentationShadowsActive() && !presentationReported)
3286 {
3287 std::cerr << "Modern presentation GPU: shadows=enabled, MSAA="
3288 << worldRenderer->presentationSampleCount() << '\n';
3290 }
3291 return presented;
3292 }
3293
3294 void shutdown()
3295 {
3296 optionsui::cancelFullHelp();
3297 if (playback)
3298 {
3299 openingMovies.reset(*playback);
3302 (void)playback->stopAll();
3303 }
3305 rules::cards::setBankPayoutObserver(nullptr);
3306 statsAccountRuntime.reset();
3308 voicechat::setReceiveEventSink(nullptr);
3310 voiceChatAudioRuntime->closeAllReceivers();
3311 voiceChatAudioRuntime.reset();
3312 voiceChatNetworkActive = false;
3313 voiceChatStartPending = false;
3314 lastVoiceNetworkError.clear();
3316 audioRuntime.reset();
3317 audioDisabled = false;
3318 reportedAudioFailures.clear();
3319 fontRuntime.reset();
3320 fontDisabled = false;
3321 sequenceVideoRuntime.reset();
3322 playback.reset();
3324 activeBoardSequence.reset();
3325 activeWorldCamera.reset();
3326 overlayRenderer.reset();
3327 worldRenderer.reset(); // GPU objects must be released before the device.
3328 legacyassets::shutdown();
3329
3330 if (gpuDevice != nullptr)
3331 {
3332 if (gameWindow != nullptr)
3333 {
3335 gpuDevice,
3337 );
3338 }
3339
3341 }
3342
3343 gpuDevice = nullptr;
3344 gameWindow = nullptr;
3345
3346 timers::shutdown();
3347 uimsg::shutdown();
3348 }
3349}
3350
3351
3352
3353
3354
std::size_t count
decision::EconomicActionPlan plan
std::vector< std::uint8_t > data
const char * name
std::size_t root
std::string_view text
LegacyBitmapMetadata metadata
LegacyBitmapRGBA8 image
std::vector< std::byte > bytes
fonts::Runtime & font
bool rendererReported
Definition Engine.cpp:170
ibar::layout::PropertyMask mortgageProperties
Definition Engine.cpp:279
bool published
Definition Engine.cpp:170
bool uploaded
Definition Engine.cpp:170
ibar::layout::PropertyMask unmortgageProperties
Definition Engine.cpp:280
bool enabled
Definition Engine.cpp:170
Clock::time_point epoch
Definition Engine.cpp:171
ibar::layout::PropertyMask sellProperties
Definition Engine.cpp:278
ibar::layout::PropertyMask buildProperties
Definition Engine.cpp:277
bool presented
Definition Engine.cpp:170
data::DataId id
std::uint16_t priority
fonts::Settings saved
std::shared_ptr< const data::HmdTextureImage > source
std::uint64_t key
unsigned state
unsigned index
int token
std::array< data::DataTag, 3 > roots
Kind kind
std::size_t frame
std::string path
std::string error
std::deque< Bytes > output
std::uint16_t value
Definition Timers.cpp:16
data::DataId idle
std::expected< void, std::string > start(data::DataId id, std::uint16_t priority=0, std::uint8_t labelOverride=0)
std::int64_t rentIfSteppedOn(const rules::GameState &state, SquareType square, rules::board::PropertySet propertiesOwned) noexcept
std::int64_t totalWorth(const rules::GameState &state, rules::PlayerNumber player) noexcept
rules::board::PropertySet propertiesOwnedByPlayer(const rules::GameState &state, rules::PlayerNumber player) noexcept
Definition AIUtility.cpp:49
bool qualifiedModernSceneGeometry(ModernSceneKind kind, const MeshRenderData &mesh) noexcept
std::uint32_t DataId
Definition DataBanks.hpp:10
constexpr DataId boardMeshDataId(BoardMeshKind mesh) noexcept
constexpr DataId EmptyDataId
Definition DataBanks.hpp:13
const ModernTokenDefinition * modernTokenForLegacyMesh(DataId id) noexcept
std::uint16_t DataTag
Definition DataBanks.hpp:11
std::function< std::expected< std::optional< std::shared_ptr< const MeshRenderData > >, MeshRuntimeError >(DataId, std::optional< DataId > rootSequenceDataId, std::uint16_t priority)> ModernMeshResolver
std::expected< std::shared_ptr< const MeshRenderData >, MeshRuntimeError > loadModernGltfMesh(const std::filesystem::path &path, ModernGltfLoadOptions options)
bool qualifiedModernSceneSequence(ModernSceneKind kind, DataId meshId, std::optional< DataId > rootSequenceDataId, std::uint16_t priority, const ModernSceneOptions &options, const ModernSceneContext &context) noexcept
constexpr BoardMeshKind usaBoardMeshForCity(int city) noexcept
std::optional< ModernGltfLoadOptions > modernSceneLoadOptions(ModernSceneKind kind) noexcept
std::expected< std::shared_ptr< const ModernTextureImage >, MeshRuntimeError > decodeModernImage(std::span< const std::byte > bytes, ModernImageEncoding encoding, ModernImageDecodeLimits limits)
const std::array< ModernTokenDefinition, ModernTokenCount > & modernTokenDefinitions() noexcept
bool qualifiedModernTokenSequence(DataId meshId, std::optional< DataId > rootSequenceDataId, std::uint16_t priority) noexcept
DataId representativeLegacyMesh(std::uint8_t token) noexcept
bool qualifiedModernTokenVariantSequence(DataId meshId, std::optional< DataId > rootSequenceDataId, std::uint16_t priority) noexcept
DataId idleSequenceDataId(std::uint8_t token) noexcept
constexpr std::size_t ModernTokenCount
constexpr DataId packDataId(std::uint16_t group, DataTag tag) noexcept
Definition DataBanks.hpp:39
constexpr std::size_t ModernSceneKindCount
const ModernSceneDefinition & modernSceneDefinition(ModernSceneKind kind) noexcept
std::expected< data::LegacyBitmapRGBA8, std::string > render(const Request &request, fonts::Runtime &fonts, const TemplateResolver &resolveTemplate)
std::expected< Plan, std::string > plan(const Request &request)
std::function< std::expected< std::shared_ptr< const data::BitmapRuntimeAsset >, std::string >(data::DataId)> TemplateResolver
State & state()
Definition Display.cpp:699
void releaseDiceCameraOverride()
Definition Display.cpp:392
constexpr std::uint16_t Board3DPriority
Definition Display.hpp:39
void endDiceCameraOverrideEarly()
Definition Display.cpp:398
void beginDiceCameraOverride(std::optional< std::uint8_t > randomFourteen)
Definition Display.cpp:381
const State & stateReadOnly()
Definition Display.cpp:704
void cancelDiceCameraOverride()
Definition Display.cpp:405
bool consumeOpeningMovieInput(const uimsg::Message &message)
Definition Engine.cpp:1665
std::expected< void, std::string > syncSequenceAudio(SequencePlayback &session)
Definition Engine.cpp:1976
void playTokenVoice(std::uint8_t token, udsound::TokenVoiceLine line, udsound::TokenVoiceClipPolicy policy, bool watchAfterStart) noexcept
Definition Engine.cpp:1934
void configureModernScene(data::ModernSceneOptions options) noexcept
Definition Engine.cpp:1198
void handleAudioPlaybackFailure(std::string_view context, const std::string &error) noexcept
Definition Engine.cpp:1854
audio::Runtime * audioPlayback()
Definition Engine.cpp:1679
void playClickSound() noexcept
Definition Engine.cpp:1898
data::MeshBounds presentationWorldBounds(const data::MeshBounds &local, const sequence::Matrix3D &world)
World3DLighting modernBoardPresentationLighting()
bool runCyclicFunctions()
Definition Engine.cpp:2227
bool modernTradePresentationEnabled() noexcept
Definition Engine.cpp:1203
void stopVoiceChat() noexcept
Definition Engine.cpp:1746
World3DCamera fitModernTopDownBoard(const data::MeshBounds &bounds, const World3DCamera &original, float aspect, float bottomReservedFraction=0) noexcept
void playWarningSound() noexcept
Definition Engine.cpp:1889
World3DCamera modernBoardPresentationCamera(const data::MeshBounds &bounds, float aspect, float elevationDegrees=48.0F, float yawDegrees=8.0F, float bottomReservedFraction=0.0F)
void playJailChoiceHostComment() noexcept
Definition Engine.cpp:1957
statsui::AccountRuntime * statsAccounts() noexcept
Definition Engine.cpp:2093
fonts::Runtime * fontPlayback()
Definition Engine.cpp:1753
void startOpeningMovies(bool startedByLobby)
Definition Engine.cpp:1647
std::optional< World3DCamera > resolveWorld3DCamera(const sequence::SetCameraCommand &command, const sequence::SequenceRuntime &runtime) noexcept
std::expected< void, std::string > syncMonopolyAudio(const rules::GameState &ruleState, const display::State &displayState)
Definition Engine.cpp:2064
World3DCamera avoidModernPresentationBuildings(const World3DCamera &original, std::span< const data::MeshBounds > buildings, float groundY, float clearance, float influenceRadius) noexcept
bool isUsaBoardEdition() noexcept
Definition Engine.cpp:1951
udsound::Runtime * monopolySoundPlayback()
Definition Engine.cpp:1849
constexpr sequence::SequenceNodeId ModernEnvironmentNodeBase
bool startVoiceChat() noexcept
Definition Engine.cpp:1704
float modernBoardControlReservation(const World3DRect &viewport, int pixelWidth, int pixelHeight, int controlsTop) noexcept
void playBuildSound() noexcept
Definition Engine.cpp:1907
void pruneWorld3DGPUScene(const SequenceWorld3DSlot &slot, MeshGPUCache &cache)
void playPennybagsVoice(udsound::PennybagsVoice line, udsound::TokenVoiceClipPolicy policy, bool watchAfterStart) noexcept
Definition Engine.cpp:1940
World3DCamera preserveModernTravelFraming(const World3DCamera &original, float groundY) noexcept
void playSaveFailureSound() noexcept
Definition Engine.cpp:1925
SequencePlayback * sequencePlayback()
Definition Engine.cpp:1213
void playUnbuildSound() noexcept
Definition Engine.cpp:1916
bool spokenPostLockSlotEmpty() noexcept
Definition Engine.cpp:1946
std::expected< std::string, Error > transcodeUtf8(std::u16string_view text)
int propertyIndex(int square) noexcept
std::uint32_t PropertyMask
PropertyMask propertyBit(int square) noexcept
const State & stateReadOnly()
Definition IBar.cpp:1291
std::expected< std::string, std::string > format(std::int64_t defaultValue, int monetarySystem, bool printSymbol, data::BoardEdition edition)
std::optional< PennybagsReaction > diceRollPennybagsReaction(std::uint8_t total, bool tokenAnimationsOn) noexcept
std::optional< PennybagsReaction > landedOnSquarePennybagsReaction(const rules::GameState &state, rules::PlayerNumber player, std::uint8_t square, std::uint32_t random100) noexcept
std::optional< data::DataId > squareAnnouncementWave(data::BoardEdition edition, data::LanguageId language, int city, std::uint8_t square) noexcept
std::optional< TokenReaction > landedOnSquareReaction(const rules::GameState &state, rules::PlayerNumber player, std::uint8_t square, std::uint32_t random100, LandingEconomics economics, bool justReadCard) noexcept
std::optional< PennybagsReaction > offBoardPennybagsReaction(bool victory, bool localHuman) noexcept
std::optional< TokenPose > tokenOrientation(std::uint8_t boardSquare) noexcept
constexpr std::uint16_t TokenPriority
const SquareDefinition & definition(SquareType square)
const SquareDefinition & originalDefinition(SquareType square)
PlacementCheck testBuildingPlacement(const GameState &state, PlayerNumber purchaser, std::uint8_t squareNo, bool adding)
const PendingPhase & current(const GameState &state)
void send(const GameState &state, const actions::Message &message, std::int64_t error)
constexpr std::size_t MaxPlayerColours
Definition RuleTypes.hpp:33
std::uint8_t PlayerNumber
Definition RuleTypes.hpp:12
constexpr std::size_t SquareCount
Definition RuleTypes.hpp:24
constexpr PlayerNumber MaxPlayers
Definition RuleTypes.hpp:14
constexpr PlayerNumber NobodyPlayer
Definition RuleTypes.hpp:16
Matrix3D identity3D() noexcept
std::optional< uimsg::Message > lifecycleMessage(const SequenceEvent &event, std::int32_t topLevelClock)
std::shared_ptr< const data::ResourceSnapshot > resources() noexcept
std::expected< std::u16string, std::string > statsPropertyName(const data::LanguageCatalog &catalog, data::BoardEdition edition, int city, int square)
constexpr std::uint8_t SirenCount
bool slotIsLocalHumanPlayer(PlayerNumber slot)
bool send(const Message &message)
const rules::GameState & ruleStateReadOnly()
std::optional< pieces::PieceMoveSpecialRequest > takePendingPieceMoveSpecial()
std::optional< pieces::PieceMovePlan > takePendingPieceMovePlan()
std::optional< pieces::PieceIdleTransitionPlan > takePendingPieceIdleTransitionPlan()
const pieces::PieceIdleState & pieceIdleStateReadOnly()
const statsui::FutureImmunityState & statsFutureImmunityStateReadOnly() noexcept
std::optional< dice::RollRequest > takePendingDiceRoll()
rules::GameState & ruleState()
const statsui::CalculatorUIState & statsCalculatorStateReadOnly() noexcept
pieces::BoardCameraView desiredBoardCamera
Definition Display.hpp:98
layout::ActionButtonLayout actionButtonLayout
Definition IBar.hpp:43
std::array< std::uint8_t, 2 > dice
std::array< PlayerState, MaxPlayers > players