Monopoly — Portage moderne 0.1.0
Portage moderne C/C++23 du Monopoly de 1999
Loading...
Searching...
No Matches
Application.cpp
Go to the documentation of this file.
1#include "Application.hpp"
3#include "Engine.hpp"
4#include "Display.hpp"
6#include "Game.hpp"
7#include "LogicalViewport.hpp"
8#include "Messaging.hpp"
9#include "MousePointer.hpp"
10#include "Presentation.hpp"
12#include "ChatRuntime.hpp"
14#include "StartupResources.hpp"
15#include "UIMessages.hpp"
16#include "UserInterface.hpp"
17
18#include <SDL3/SDL.h>
19
20#include <cmath>
21#include <charconv>
22#include <filesystem>
23#include <iostream>
24#include <optional>
25#include <string>
26#include <string_view>
27#include <vector>
28
29namespace
30{
31 struct MouseLogicalMapping
32 {
34 double scale{};
35 };
36
37
38 [[nodiscard]] bool contains(
40 double x,
41 double y) noexcept
42 {
43 return x >= rect.x && y >= rect.y &&
44 x < rect.x + rect.width &&
45 y < rect.y + rect.height;
46 }
47
48
49 [[nodiscard]] std::optional<MouseLogicalMapping>
52 float x,
53 float y)
54 {
55 int width = 0;
56 int height = 0;
58 return std::nullopt;
59
60 const auto windowX = static_cast<double>(x);
61 const auto windowY = static_cast<double>(y);
63
66 {
68 const auto viewport = monopoly::display::worldViewport(displayState.viewportInUse);
70 const bool tradeModal = displayState.desired2DView == monopoly::display::Screen2D::Trade &&
71 (trade.playerSelectVisible || trade.cashDialogVisible || trade.contractDialogVisible);
72 if (const auto overlay = monopoly::logicalviewport::windowToUIOverlay(
73 uiTransform, windowX, windowY,
74 {static_cast<double>(viewport.left), static_cast<double>(viewport.top),
75 static_cast<double>(viewport.right - viewport.left),
76 static_cast<double>(viewport.bottom - viewport.top)}, tradeModal))
77 return MouseLogicalMapping{*overlay, uiTransform.scale};
78 if (tradeModal) return std::nullopt;
79 const auto worldTransform =
82 worldTransform,
83 {static_cast<double>(viewport.left),
84 static_cast<double>(viewport.top),
85 static_cast<double>(viewport.right - viewport.left),
86 static_cast<double>(viewport.bottom - viewport.top)});
87
88 if (contains(pixels, windowX, windowY))
89 if (const auto point =
91 worldTransform, windowX, windowY))
92 return MouseLogicalMapping{*point, worldTransform.scale};
93 }
94
95 const auto uiTransform =
97 if (const auto point = monopoly::logicalviewport::windowToLogical(
98 uiTransform, windowX, windowY))
99 return MouseLogicalMapping{*point, uiTransform.scale};
100
101 return std::nullopt;
102 }
103
104
106 {
107 const char* basePath = SDL_GetBasePath();
108 if (basePath == nullptr || *basePath == '\0')
109 {
110 std::cerr << "SDL_GetBasePath failed for AI profiles.\n";
111 return false;
112 }
113
114 const auto directory = std::filesystem::path(basePath) /
115 "assets" / "ai";
117 directory);
118 if (!loaded)
119 {
120 std::cerr << "AI profile initialization failed: "
121 << loaded.error().detail << '\n';
122 return false;
123 }
124 return true;
125 }
126
127
128 void sendMouseMessage(
131 float x,
132 float y,
133 float deltaX = 0.0F,
134 float deltaY = 0.0F)
135 {
136 const auto point = mouseToLogical(window, x, y);
137
140 else if (type == monopoly::uimsg::Type::MouseLeftUp)
142
143 if (!point.has_value())
144 {
145 monopoly::mouse::updatePosition(-1, -1, false);
146 // Un mouvement dans une bande noire doit retirer les hovers.
147 // Les clics hors de la surface historique sont simplement ignores.
149 {
151 { type, -1, -1 }
152 );
153 }
154
155 return;
156 }
157
158 std::array<std::int32_t, 2> worldPoint{
159 static_cast<std::int32_t>(point->point.x),
160 static_cast<std::int32_t>(point->point.y)};
162 if (session)
164 const_cast<monopoly::engine::SequencePlayback*>(session)->world2D().screenToWorld(),
165 worldPoint[0], worldPoint[1]);
168
169 std::int64_t logicalDeltaX = 0;
170 std::int64_t logicalDeltaY = 0;
171 if (point->scale > 0.0)
172 {
173 logicalDeltaX = static_cast<std::int64_t>(
174 std::lround(static_cast<double>(deltaX) / point->scale));
175 logicalDeltaY = static_cast<std::int64_t>(
176 std::lround(static_cast<double>(deltaY) / point->scale));
177 }
178 if (session)
179 {
180 // L_Mouse computes deltas after inverse projection, including a
181 // camera change since the previous mouse event.
182 logicalDeltaX = static_cast<std::int64_t>(worldPoint[0]) - previousPosition.x;
183 logicalDeltaY = static_cast<std::int64_t>(worldPoint[1]) - previousPosition.y;
184 }
185 const auto modifier = (SDL_GetModState() & SDL_KMOD_CTRL) != 0
188 {
189 type,
190 worldPoint[0],
191 worldPoint[1],
195 }
196 );
197 }
198}
199
200namespace monopoly
201{
202 int Application::run(int argc, char** argv)
203 {
204 int result = 0;
205
206 std::vector<std::string_view> arguments;
207 for (int index = 1; index < argc; ++index) arguments.emplace_back(argv[index]);
208 const auto presentationOptions =
211 {
212 std::cerr << presentationOptions.error() << '\n';
213 return 1;
214 }
215
217 if (!modernOptions)
218 {
219 std::cerr << modernOptions.error() << '\n';
220 return 1;
221 }
224 modernOptions->remaining);
225 if (!resourceOptions)
226 {
227 std::cerr << resourceOptions.error() << '\n';
228 return 1;
229 }
231 if (!contextSelected)
232 {
233 std::cerr << contextSelected.error() << '\n';
234 return 1;
235 }
236 const auto& networkArguments = resourceOptions->remaining;
237
238 bool networkRequested = false;
239 bool networkHost = false;
240 bool gameplayRequested = false;
241 std::string networkAddress;
242 std::uint16_t networkPort = 0;
243 if (!networkArguments.empty())
244 {
245 const auto mode = networkArguments[0];
246 const auto usage = []
247 {
248 std::cerr << "Usage: MonopolyModern "
249 "[--windowed | --fullscreen | --exclusive-fullscreen] "
250 "[--resolution WIDTHxHEIGHT] "
251 "[--present-mode vsync|mailbox|immediate] "
252 "[--modern-board=retail|paris|usa|procedural] [--modern-buildings=retail|house] "
253 "[--modern-environment=retail|paris|procedural] "
254 "[--edition=usa|europe] [--language=en-us|en-uk|fr] "
255 "[--data-root <absolute folder>] "
256 "[--data-overrides <absolute manifest.tsv>] "
257 "[--check-resources] [--voice-host IPv4:port | "
258 "--voice-connect IPv4:port | --network-host IPv4:port | "
259 "--network-connect IPv4:port]\n"
260 "Network menu without arguments hosts on 0.0.0.0:28799.\n";
261 };
262 if (networkArguments.size() != 2 || (mode != "--voice-host" && mode != "--voice-connect" &&
263 mode != "--network-host" && mode != "--network-connect"))
264 {
265 usage();
266 return 1;
267 }
268 const auto endpoint = networkArguments[1];
269 const auto colon = endpoint.rfind(':');
270 if (colon == std::string_view::npos || colon == 0 || colon + 1 == endpoint.size())
271 {
272 usage();
273 return 1;
274 }
275 unsigned port = 0;
276 const auto parsed = std::from_chars(endpoint.data() + colon + 1,
277 endpoint.data() + endpoint.size(), port);
278 if (parsed.ec != std::errc{} || parsed.ptr != endpoint.data() + endpoint.size() ||
279 port == 0 || port > 65535)
280 {
281 usage();
282 return 1;
283 }
284 networkRequested = true;
285 gameplayRequested = mode == "--network-host" || mode == "--network-connect";
286 networkHost = mode == "--voice-host" || mode == "--network-host";
287 networkAddress = endpoint.substr(0, colon);
288 networkPort = static_cast<std::uint16_t>(port);
289 }
290
291 if (resourceOptions->dataRoot)
292 {
293 const auto selected = startup::selectResourceRoot(*resourceOptions->dataRoot);
294 if (!selected)
295 {
296 std::cerr << "Invalid --data-root: " << selected.error() << '\n';
297 return 1;
298 }
299 }
300 if (resourceOptions->dataOverrides)
301 {
302 const auto selected = startup::selectDataOverrideManifest(
303 *resourceOptions->dataOverrides);
304 if (!selected)
305 {
306 std::cerr << "Invalid --data-overrides: "
307 << selected.error() << '\n';
308 return 1;
309 }
310 }
311 if (resourceOptions->checkOnly)
312 {
313 const auto checked = startup::prepareResources(false);
314 SDL_Quit();
316 }
317
319 {
320 std::cerr << "SDL_Init failed: " << SDL_GetError() << '\n';
321 return 1;
322 }
323
324 // Audio is initialized before opening movies. A missing device is not
325 // fatal to gameplay; AudioRuntime can retry later if the device changes.
327 std::cerr << "SDL audio unavailable at startup: "
328 << SDL_GetError() << '\n';
329
330 const auto setup = startup::prepareResources(true);
332 {
333 SDL_Quit();
334 return setup == startup::ResourceSetupResult::Cancelled ? 0 : 1;
335 }
336
337 const auto [initialWidth, initialHeight] =
340 "Monopoly Modern",
344 );
345
346 if (window == nullptr)
347 {
348 std::cerr << "SDL_CreateWindow failed: " << SDL_GetError() << '\n';
349 SDL_Quit();
350 return 1;
351 }
352
353 const auto configuredWindow =
355 if (!configuredWindow)
356 {
357 std::cerr << "Window presentation setup failed: "
358 << configuredWindow.error() << '\n';
360 SDL_Quit();
361 return 1;
362 }
363
364 std::cout << "Presentation: "
366 << ", requested "
368 << '\n';
369
371
373 {
376 SDL_Quit();
377 return 1;
378 }
379
381
383 {
387 SDL_Quit();
388 return 1;
389 }
390
391 if (game::startup())
392 {
393 bool finished = false;
394
395 // A native Modern session replaces the old DirectPlay selector.
396 // Command-line endpoints configure joining; the menu defaults to hosting.
398 address = networkRequested ? networkAddress : std::string("0.0.0.0"),
399 port = networkRequested ? networkPort : std::uint16_t{28799}]()
400 {
403 if (!transport)
404 {
405 std::cerr << "Game network startup failed: " << transport.error() << '\n';
406 return false;
407 }
408 return messaging::startNetwork(std::move(*transport));
409 });
411 {
416 if (!transport)
417 {
418 std::cerr << "Voice network startup failed: " << transport.error() << '\n';
419 finished = true;
420 result = 1;
421 }
422 else if (!messaging::startNetwork(std::move(*transport)))
423 {
424 std::cerr << "Voice network owner could not be installed.\n";
425 finished = true;
426 result = 1;
427 }
428 }
429
430 // Voice host/connect is not a lobby launch. Only a real lobby
431 // owner may request the historical opening-movie bypass.
433
434 while (!finished)
435 {
436 SDL_Event event{};
437
438 while (SDL_PollEvent(&event))
439 {
440 if (event.type == SDL_EVENT_QUIT)
441 {
443 }
444 else if (
445 event.type == SDL_EVENT_MOUSE_BUTTON_DOWN &&
446 event.button.button == SDL_BUTTON_LEFT)
447 {
449 window,
451 event.button.x,
452 event.button.y
453 );
454 }
455 else if (
456 event.type == SDL_EVENT_MOUSE_BUTTON_UP &&
457 event.button.button == SDL_BUTTON_LEFT)
458 {
460 window,
462 event.button.x,
463 event.button.y
464 );
465 }
466 else if (
467 event.type == SDL_EVENT_MOUSE_BUTTON_DOWN &&
468 event.button.button == SDL_BUTTON_MIDDLE)
469 {
471 window,
473 event.button.x,
474 event.button.y
475 );
476 }
477 else if (
478 event.type == SDL_EVENT_MOUSE_BUTTON_UP &&
479 event.button.button == SDL_BUTTON_MIDDLE)
480 {
482 window,
484 event.button.x,
485 event.button.y
486 );
487 }
488 else if (
489 event.type == SDL_EVENT_MOUSE_BUTTON_DOWN &&
490 event.button.button == SDL_BUTTON_RIGHT)
491 {
493 window,
495 event.button.x,
496 event.button.y
497 );
498 }
499 else if (
500 event.type == SDL_EVENT_MOUSE_BUTTON_UP &&
501 event.button.button == SDL_BUTTON_RIGHT)
502 {
504 window,
506 event.button.x,
507 event.button.y
508 );
509 }
510 else if (event.type == SDL_EVENT_MOUSE_MOTION)
511 {
513 window,
515 event.motion.x,
516 event.motion.y,
517 event.motion.xrel,
518 event.motion.yrel
519 );
520 }
521 else if (event.type == SDL_EVENT_KEY_DOWN)
522 {
523 if (event.key.scancode == SDL_SCANCODE_F11 &&
524 !event.key.repeat)
525 {
526 const auto toggled =
529 if (!toggled)
530 std::cerr << "Fullscreen toggle failed: "
531 << toggled.error() << '\n';
532 }
533 else if (event.key.scancode != SDL_SCANCODE_ESCAPE || !event.key.repeat)
534 {
535 // A held Escape must not activate a newly opened menu after skipping a movie.
537 {
539 static_cast<std::int64_t>(
540 event.key.scancode
541 )
542 }
543 );
544 }
545 }
546 else if (event.type == SDL_EVENT_KEY_UP)
547 {
548 if (event.key.scancode != SDL_SCANCODE_F11)
550 {
552 static_cast<std::int64_t>(
553 event.key.scancode
554 )
555 }
556 );
557 }
558 else if (event.type == SDL_EVENT_TEXT_INPUT)
559 {
561
564
565 if (event.text.text != nullptr)
566 {
567 textMessage.text =
568 event.text.text;
569 }
570
572 }
573 }
574
575 if (!finished)
576 {
577 if (game::updateCycle())
578 {
579 // UDChat switches from the ArtLib TAB_pointer to the
580 // native I-beam only after its input state is updated.
583 {
584 std::cerr
585 << "GPU frame submission failed: "
586 << SDL_GetError()
587 << '\n';
588
589 finished = true;
590 result = 1;
591 }
592 else
593 {
594 frameTelemetry.framePresented();
595 }
596 }
597 else
598 {
599 finished = true;
600 }
601 }
602 }
603
604 // STOP is queued before the transport is destroyed. The final pump
605 // is best-effort; peers also clean up on EOF/heartbeat timeout.
609 }
610 else
611 {
612 std::cerr << "Game startup failed.\n";
613 result = 1;
614 }
615
617
620 SDL_Quit();
621
622 return result;
623 }
624}
625
626
std::uint32_t height
std::uint32_t width
unsigned index
std::filesystem::path directory
Rect rect
std::atomic< bool > finished
int run(int argc, char **argv)
static std::array< std::int32_t, 2 > transformPoint(const sequence::Matrix2D &matrix, std::int32_t x, std::int32_t y) noexcept
std::expected< void, profile::Error > initializeMessageIngressProfiles(const std::filesystem::path &directory)
const State & stateReadOnly() noexcept
std::expected< ParsedModernSceneOptions, std::string > parseModernSceneArguments(std::span< const std::string_view > arguments)
engine::World3DRect worldViewport(Viewport3D viewport) noexcept
Definition Display.hpp:76
constexpr bool isBoardVisible(Screen2D screen) noexcept
Definition Display.hpp:41
const State & stateReadOnly()
Definition Display.cpp:704
void configureModernScene(data::ModernSceneOptions options) noexcept
Definition Engine.cpp:1198
bool runCyclicFunctions()
Definition Engine.cpp:2227
void stopVoiceChat() noexcept
Definition Engine.cpp:1746
bool initialize(SDL_Window *window, presentation::PresentMode presentMode)
Definition Engine.cpp:2096
void startOpeningMovies(bool startedByLobby)
Definition Engine.cpp:1647
SequencePlayback * sequencePlayback()
Definition Engine.cpp:1213
void shutdown()
Definition Game.cpp:120
bool updateCycle()
Definition Game.cpp:78
bool startup()
Definition Game.cpp:27
std::optional< LogicalPoint > windowToUIOverlay(const Transform &uiTransform, double windowX, double windowY) noexcept
Transform makeTransform(int targetWidth, int targetHeight) noexcept
PixelRect logicalToPixelRect(const Transform &transform, const LogicalRect &logicalRect) noexcept
std::optional< LogicalPoint > windowToLogical(const Transform &transform, double windowX, double windowY) noexcept
Transform makeWorld3DTransform(int targetWidth, int targetHeight) noexcept
std::expected< std::unique_ptr< Transport >, std::string > openTcpTransport(bool host, std::string_view address, std::uint16_t port, TcpSessionMode mode)
bool startNetwork(std::unique_ptr< Transport > transport)
void setNetworkStarter(std::function< bool()> starter)
void setLeftDown(bool down) noexcept
void updatePosition(int x, int y, bool inside) noexcept
const State & stateReadOnly() noexcept
void updateChatCursor(const chat::State &chat) noexcept
std::expected< Options, std::string > parseArguments(std::span< const std::string_view > arguments)
std::expected< void, std::string > configureWindow(SDL_Window *window, const Options &options)
std::string_view presentModeName(PresentMode mode) noexcept
std::string_view windowModeName(WindowMode mode) noexcept
std::pair< int, int > initialWindowSize(const Options &options) noexcept
SDL_WindowFlags windowFlags(const Options &options) noexcept
std::expected< void, std::string > toggleBorderlessFullscreen(SDL_Window *window, const Options &options)
std::expected< void, std::string > selectResourceContext(data::ResourceContext context)
ResourceSetupResult prepareResources(bool interactive)
std::expected< ResourceArguments, std::string > parseResourceArguments(std::span< const std::string_view > arguments)
std::expected< void, std::string > selectDataOverrideManifest(std::string_view utf8Path)
std::expected< void, std::string > selectResourceRoot(std::string_view utf8Root)
bool send(const Message &message)
constexpr std::int64_t MouseModifierControl
const tradeui::State & tradeStateReadOnly() noexcept