From 891fb62a77b9a86f2bc71cc02a82089412982b2f Mon Sep 17 00:00:00 2001
From: notoraptor <stevenbocco@gmail.com>
Date: Thu, 1 Aug 2019 15:53:23 -0400
Subject: Refactored SVG map into a React component

- Create link to diplomacy map folder into web/src/diplomacy/maps
- Remove old web/src/gui/map folder.
- [web] Handle click only on current map.
- [web/game] Remove useless `wait` state.
- Remove unused nodejs modules.
- [web] Use queue to handle game notifications in sequential order.
- Make all calls to setState() asynchronous in Page and ContentGame components.
- Make sure notifications are handled in the order in which they come.
---
 diplomacy/web/src/gui/pages/content_game.jsx | 249 ++++++++++++++++-----------
 1 file changed, 144 insertions(+), 105 deletions(-)

(limited to 'diplomacy/web/src/gui/pages/content_game.jsx')

diff --git a/diplomacy/web/src/gui/pages/content_game.jsx b/diplomacy/web/src/gui/pages/content_game.jsx
index a98c739..bb7d41e 100644
--- a/diplomacy/web/src/gui/pages/content_game.jsx
+++ b/diplomacy/web/src/gui/pages/content_game.jsx
@@ -21,7 +21,6 @@ import {SelectViaForm} from "../forms/select_via_form";
 import {Order} from "../utils/order";
 import {Row} from "../components/layouts";
 import {Tabs} from "../components/tabs";
-import {Map} from "../map/map";
 import {extendOrderBuilding, ORDER_BUILDER, POSSIBLE_ORDERS} from "../utils/order_building";
 import {PowerOrderCreationForm} from "../forms/power_order_creation_form";
 import {MessageForm} from "../forms/message_form";
@@ -44,6 +43,9 @@ import {Button} from "../components/button";
 import {saveGameToDisk} from "../utils/saveGameToDisk";
 import {Game} from '../../diplomacy/engine/game';
 import {PowerOrdersActionBar} from "../components/power_orders_actions_bar";
+import {SvgStandard} from "../maps/standard/SvgStandard";
+import {MapData} from "../utils/map_data";
+import {Queue} from "../../diplomacy/utils/queue";
 
 const HotKey = require('react-shortcut');
 
@@ -75,6 +77,10 @@ const PRETTY_ROLES = {
     [STRINGS.OBSERVER_TYPE]: 'Observer'
 };
 
+function noPromise() {
+    return new Promise(resolve => resolve());
+}
+
 export class ContentGame extends React.Component {
 
     constructor(props) {
@@ -111,7 +117,6 @@ export class ContentGame extends React.Component {
             historyShowOrders: true,
             historyCurrentLoc: null,
             historyCurrentOrders: null,
-            wait: null, // {power name => bool}
             orders: orders, // {power name => {loc => {local: bool, order: str}}}
             power: null,
             orderBuildingType: null,
@@ -156,6 +161,7 @@ export class ContentGame extends React.Component {
         this.setSelectedVia = this.setSelectedVia.bind(this);
         this.setWaitFlag = this.setWaitFlag.bind(this);
         this.vote = this.vote.bind(this);
+        this.updateDeadlineTimer = this.updateDeadlineTimer.bind(this);
     }
 
     static prettyRole(role) {
@@ -194,12 +200,24 @@ export class ContentGame extends React.Component {
         };
     }
 
+    setState(state) {
+        return new Promise(resolve => super.setState(state, resolve));
+    }
+
+    forceUpdate() {
+        return new Promise(resolve => super.forceUpdate(resolve));
+    }
+
+    /**
+     * Return current page object displaying this content.
+     * @returns {Page}
+     */
     getPage() {
         return this.context;
     }
 
     clearOrderBuildingPath() {
-        this.setState({
+        return this.setState({
             orderBuildingPath: []
         });
     }
@@ -275,7 +293,8 @@ export class ContentGame extends React.Component {
             engine.deadline_timer = 0;
             this.clearScheduleTimeout();
         }
-        this.getPage().load(`game: ${engine.game_id}`, <ContentGame data={engine}/>);
+        if (this.networkGameIsDisplayed(engine.client))
+            this.forceUpdate();
     }
 
     reloadDeadlineTimer(networkGame) {
@@ -287,13 +306,12 @@ export class ContentGame extends React.Component {
                 const server_remaining = server_end - server_current;
                 this.props.data.deadline_timer = server_remaining * schedule.time_unit;
                 if (!this.schedule_timeout_id)
-                    this.schedule_timeout_id = setInterval(() => this.updateDeadlineTimer(), schedule.time_unit * 1000);
+                    this.schedule_timeout_id = setInterval(this.updateDeadlineTimer, schedule.time_unit * 1000);
             })
             .catch(() => {
                 if (this.props.data.hasOwnProperty('deadline_timer'))
                     delete this.props.data.deadline_timer;
                 this.clearScheduleTimeout();
-                // this.getPage().error(`Error while updating deadline timer: ${error.toString()}`);
             });
     }
 
@@ -311,13 +329,10 @@ export class ContentGame extends React.Component {
     notifiedNetworkGame(networkGame, notification) {
         if (this.networkGameIsDisplayed(networkGame)) {
             const msg = `Game (${networkGame.local.game_id}) received notification ${notification.name}.`;
-            this.getPage().load(
-                `game: ${networkGame.local.game_id}`,
-                <ContentGame data={networkGame.local}/>,
-                {info: msg}
-            );
             this.reloadDeadlineTimer(networkGame);
+            return this.forceUpdate().then(() => this.getPage().info(msg));
         }
+        return noPromise();
     }
 
     notifiedPowersControllers(networkGame, notification) {
@@ -326,51 +341,46 @@ export class ContentGame extends React.Component {
             || !networkGame.channel.game_id_to_instances[networkGame.local.game_id].has(networkGame.local.role)
         )) {
             // This power game is now invalid.
-            this.getPage().disconnectGame(networkGame.local.game_id)
+            return this.getPage().disconnectGame(networkGame.local.game_id)
                 .then(() => {
                     if (this.networkGameIsDisplayed(networkGame)) {
-                        const page = this.getPage();
-                        page.loadGames(
+                        return this.getPage().loadGames(
                             {error: `${networkGame.local.game_id}/${networkGame.local.role} was kicked. Deadline over?`});
                     }
                 });
         } else {
-            this.notifiedNetworkGame(networkGame, notification);
+            return this.notifiedNetworkGame(networkGame, notification);
         }
     }
 
     notifiedGamePhaseUpdated(networkGame, notification) {
-        networkGame.getAllPossibleOrders()
+        return networkGame.getAllPossibleOrders()
             .then(allPossibleOrders => {
                 networkGame.local.setPossibleOrders(allPossibleOrders);
                 if (this.networkGameIsDisplayed(networkGame)) {
-                    this.getPage().load(
-                        `game: ${networkGame.local.game_id}`,
-                        <ContentGame data={networkGame.local}/>,
-                        {info: `Game update (${notification.name}) to ${networkGame.local.phase}.`}
-                    );
                     this.__store_orders(null);
-                    this.setState({orders: null, wait: null, messageHighlights: {}});
                     this.reloadDeadlineTimer(networkGame);
+                    return this.setState({orders: null, messageHighlights: {}})
+                        .then(() => this.getPage().info(
+                            `Game update (${notification.name}) to ${networkGame.local.phase}.`));
                 }
             })
             .catch(error => this.getPage().error('Error when updating possible orders: ' + error.toString()));
     }
 
     notifiedLocalStateChange(networkGame, notification) {
-        networkGame.getAllPossibleOrders()
+        return networkGame.getAllPossibleOrders()
             .then(allPossibleOrders => {
                 networkGame.local.setPossibleOrders(allPossibleOrders);
                 if (this.networkGameIsDisplayed(networkGame)) {
-                    this.getPage().load(
-                        `game: ${networkGame.local.game_id}`,
-                        <ContentGame data={networkGame.local}/>,
-                        {info: `Possible orders re-loaded.`}
-                    );
+                    this.reloadDeadlineTimer(networkGame);
+                    let result = null;
                     if (notification.power_name) {
-                        this.reloadPowerServerOrders(notification.power_name);
+                        result = this.reloadPowerServerOrders(notification.power_name);
+                    } else {
+                        result = this.forceUpdate();
                     }
-                    this.reloadDeadlineTimer(networkGame);
+                    return result.then(() => this.getPage().info(`Possible orders re-loaded.`));
                 }
             })
             .catch(error => this.getPage().error('Error when updating possible orders: ' + error.toString()));
@@ -385,48 +395,79 @@ export class ContentGame extends React.Component {
             messageHighlights[protagonist] = 1;
         else
             ++messageHighlights[protagonist];
-        this.setState({messageHighlights: messageHighlights});
-        this.notifiedNetworkGame(networkGame, notification);
+        return this.setState({messageHighlights: messageHighlights})
+            .then(() => this.notifiedNetworkGame(networkGame, notification));
     }
 
     bindCallbacks(networkGame) {
+        const collector = (game, notification) => {
+            game.queue.append(notification);
+        };
+        const consumer = (notification) => {
+            switch (notification.name) {
+                case 'powers_controllers':
+                    return this.notifiedPowersControllers(networkGame, notification);
+                case 'game_message_received':
+                    return this.notifiedNewGameMessage(networkGame, notification);
+                case 'game_processed':
+                case 'game_phase_update':
+                    return this.notifiedGamePhaseUpdated(networkGame, notification);
+                case 'cleared_centers':
+                case 'cleared_orders':
+                case 'cleared_units':
+                case 'power_orders_update':
+                case 'power_orders_flag':
+                    return this.notifiedLocalStateChange(networkGame, notification);
+                case 'game_status_update':
+                case 'omniscient_updated':
+                case 'power_vote_updated':
+                case 'power_wait_flag':
+                case 'vote_count_updated':
+                case 'vote_updated':
+                    return this.notifiedNetworkGame(networkGame, notification);
+                default:
+                    throw new Error(`Unhandled notification: ${notification.name}`);
+            }
+        };
         if (!networkGame.callbacksBound) {
-            networkGame.addOnClearedCenters(this.notifiedLocalStateChange);
-            networkGame.addOnClearedOrders(this.notifiedLocalStateChange);
-            networkGame.addOnClearedUnits(this.notifiedLocalStateChange);
-            networkGame.addOnPowersControllers(this.notifiedPowersControllers);
-            networkGame.addOnGameMessageReceived(this.notifiedNewGameMessage);
-            networkGame.addOnGameProcessed(this.notifiedGamePhaseUpdated);
-            networkGame.addOnGamePhaseUpdate(this.notifiedGamePhaseUpdated);
-            networkGame.addOnGameStatusUpdate(this.notifiedNetworkGame);
-            networkGame.addOnOmniscientUpdated(this.notifiedNetworkGame);
-            networkGame.addOnPowerOrdersUpdate(this.notifiedLocalStateChange);
-            networkGame.addOnPowerOrdersFlag(this.notifiedLocalStateChange);
-            networkGame.addOnPowerVoteUpdated(this.notifiedNetworkGame);
-            networkGame.addOnPowerWaitFlag(this.notifiedNetworkGame);
-            networkGame.addOnVoteCountUpdated(this.notifiedNetworkGame);
-            networkGame.addOnVoteUpdated(this.notifiedNetworkGame);
+            networkGame.queue = new Queue();
+            networkGame.addOnClearedCenters(collector);
+            networkGame.addOnClearedOrders(collector);
+            networkGame.addOnClearedUnits(collector);
+            networkGame.addOnPowerOrdersUpdate(collector);
+            networkGame.addOnPowerOrdersFlag(collector);
+            networkGame.addOnPowersControllers(collector);
+            networkGame.addOnGameMessageReceived(collector);
+            networkGame.addOnGameProcessed(collector);
+            networkGame.addOnGamePhaseUpdate(collector);
+            networkGame.addOnGameStatusUpdate(collector);
+            networkGame.addOnOmniscientUpdated(collector);
+            networkGame.addOnPowerVoteUpdated(collector);
+            networkGame.addOnPowerWaitFlag(collector);
+            networkGame.addOnVoteCountUpdated(collector);
+            networkGame.addOnVoteUpdated(collector);
             networkGame.callbacksBound = true;
             networkGame.local.markAllMessagesRead();
+            networkGame.queue.consumeAsync(consumer);
         }
     }
 
     // ]
 
     onChangeCurrentPower(event) {
-        this.setState({power: event.target.value, tabPastMessages: null, tabCurrentMessages: null});
+        return this.setState({power: event.target.value, tabPastMessages: null, tabCurrentMessages: null});
     }
 
     onChangeMainTab(tab) {
-        this.setState({tabMain: tab});
+        return this.setState({tabMain: tab});
     }
 
     onChangeTabCurrentMessages(tab) {
-        this.setState({tabCurrentMessages: tab});
+        return this.setState({tabCurrentMessages: tab});
     }
 
     onChangeTabPastMessages(tab) {
-        this.setState({tabPastMessages: tab});
+        return this.setState({tabPastMessages: tab});
     }
 
     sendMessage(networkGame, recipient, body) {
@@ -468,10 +509,6 @@ export class ContentGame extends React.Component {
         return this.state.power || (controllablePowers.length && controllablePowers[0]);
     }
 
-    __get_wait(engine) {
-        return this.state.wait ? this.state.wait : ContentGame.getServerWaitFlags(engine);
-    }
-
     // [ Methods involved in orders management.
 
     /**
@@ -535,12 +572,11 @@ export class ContentGame extends React.Component {
         const engine = this.props.data;
         const allOrders = this.__get_orders(engine);
         if (!allOrders.hasOwnProperty(powerName)) {
-            this.getPage().error(`Unknown power ${powerName}.`);
-            return;
+            return this.getPage().error(`Unknown power ${powerName}.`);
         }
         allOrders[powerName] = serverOrders[powerName];
         this.__store_orders(allOrders);
-        this.setState({orders: allOrders});
+        return this.setState({orders: allOrders});
     }
 
     /**
@@ -598,7 +634,7 @@ export class ContentGame extends React.Component {
         const orders = this.__get_orders(this.props.data);
         orders[powerName] = {};
         this.__store_orders(orders);
-        this.setState({orders: orders});
+        return this.setState({orders: orders});
     }
 
     /**
@@ -667,8 +703,8 @@ export class ContentGame extends React.Component {
 
     onOrderBuilding(powerName, path) {
         const pathToSave = path.slice(1);
-        this.getPage().success(`Building order ${pathToSave.join(' ')} ...`);
-        this.setState({orderBuildingPath: pathToSave});
+        return this.setState({orderBuildingPath: pathToSave})
+            .then(() => this.getPage().success(`Building order ${pathToSave.join(' ')} ...`));
     }
 
     onOrderBuilt(powerName, orderString) {
@@ -676,16 +712,14 @@ export class ContentGame extends React.Component {
         state.orderBuildingPath = [];
         if (!orderString) {
             Diplog.warn('No order built.');
-            this.setState(state);
-            return;
+            return this.setState(state);
         }
         const engine = this.props.data;
         const localOrder = new Order(orderString, true);
         const allOrders = this.__get_orders(engine);
         if (!allOrders.hasOwnProperty(powerName)) {
             Diplog.warn(`Unknown power ${powerName}.`);
-            this.setState(state);
-            return;
+            return this.setState(state);
         }
 
         if (!allOrders[powerName])
@@ -694,11 +728,11 @@ export class ContentGame extends React.Component {
         state.orders = allOrders;
         this.getPage().success(`Built order: ${orderString}`);
         this.__store_orders(allOrders);
-        this.setState(state);
+        return this.setState(state);
     }
 
     onChangeOrderType(form) {
-        this.setState({
+        return this.setState({
             orderBuildingType: form.order_type,
             orderBuildingPath: [],
         });
@@ -727,7 +761,9 @@ export class ContentGame extends React.Component {
         if (!currentPowerName)
             throw new Error(`Internal error: unable to detect current selected power name.`);
         networkGame.setWait(waitFlag, {power_name: currentPowerName})
-            .then(() => this.getPage().success(`Wait flag set to ${waitFlag} for ${currentPowerName}`))
+            .then(() => {
+                this.forceUpdate(() => this.getPage().success(`Wait flag set to ${waitFlag} for ${currentPowerName}`));
+            })
             .catch(error => {
                 Diplog.error(error.stack);
                 this.getPage().error(`Error while setting wait flag for ${currentPowerName}: ${error.toString()}`);
@@ -735,7 +771,7 @@ export class ContentGame extends React.Component {
     }
 
     __change_past_phase(newPhaseIndex) {
-        this.setState({
+        return this.setState({
             historyPhaseIndex: newPhaseIndex,
             historyCurrentLoc: null,
             historyCurrentOrders: null
@@ -780,7 +816,7 @@ export class ContentGame extends React.Component {
     }
 
     onChangeShowPastOrders(event) {
-        this.setState({historyShowOrders: event.target.checked});
+        return this.setState({historyShowOrders: event.target.checked});
     }
 
     onClickMessage(message) {
@@ -799,7 +835,7 @@ export class ContentGame extends React.Component {
     }
 
     displayLocationOrders(loc, orders) {
-        this.setState({
+        return this.setState({
             historyCurrentLoc: loc || null,
             historyCurrentOrders: orders && orders.length ? orders : null
         });
@@ -810,7 +846,7 @@ export class ContentGame extends React.Component {
     renderOrders(engine, currentPowerName) {
         const serverOrders = this.props.data.getServerOrders();
         const orders = this.__get_orders(engine);
-        const wait = this.__get_wait(engine);
+        const wait = ContentGame.getServerWaitFlags(engine);
 
         const render = [];
         render.push(<PowerOrders key={currentPowerName} name={currentPowerName} wait={wait[currentPowerName]}
@@ -908,27 +944,29 @@ export class ContentGame extends React.Component {
     }
 
     renderMapForResults(gameEngine, showOrders) {
-        return <Map key={'past-map'}
-                    id={'past-map'}
-                    game={gameEngine}
-                    mapInfo={this.getMapInfo(gameEngine.map_name)}
-                    onError={this.getPage().error}
-                    onHover={showOrders ? this.displayLocationOrders : null}
-                    showOrders={Boolean(showOrders)}
-                    orders={(gameEngine.order_history.contains(gameEngine.phase) && gameEngine.order_history.get(gameEngine.phase)) || null}
-        />;
+        return (
+            <div id="past-map" key="past-map">
+                <SvgStandard game={gameEngine}
+                             mapData={new MapData(this.getMapInfo(gameEngine.map_name), gameEngine)}
+                             onError={this.getPage().error}
+                             orders={(showOrders && gameEngine.order_history.contains(gameEngine.phase) && gameEngine.order_history.get(gameEngine.phase)) || null}
+                             onHover={showOrders ? this.displayLocationOrders : null}
+                             onSelectVia={this.onSelectVia}/>
+            </div>
+        );
     }
 
     renderMapForMessages(gameEngine, showOrders) {
-        return <Map key={'messages-map'}
-                    id={'messages-map'}
-                    game={gameEngine}
-                    mapInfo={this.getMapInfo(gameEngine.map_name)}
-                    onError={this.getPage().error}
-                    onHover={showOrders ? this.displayLocationOrders : null}
-                    showOrders={Boolean(showOrders)}
-                    orders={(gameEngine.order_history.contains(gameEngine.phase) && gameEngine.order_history.get(gameEngine.phase)) || null}
-        />;
+        return (
+            <div id="messages-map" key="messages-map">
+                <SvgStandard game={gameEngine}
+                             mapData={new MapData(this.getMapInfo(gameEngine.map_name), gameEngine)}
+                             onError={this.getPage().error}
+                             orders={(showOrders && gameEngine.order_history.contains(gameEngine.phase) && gameEngine.order_history.get(gameEngine.phase)) || null}
+                             onHover={showOrders ? this.displayLocationOrders : null}
+                             onSelectVia={this.onSelectVia}/>
+            </div>
+        );
     }
 
     renderMapForCurrent(gameEngine, powerName, orderType, orderPath) {
@@ -941,18 +979,19 @@ export class ContentGame extends React.Component {
                     orders[entry[0]].push(orderObject.order);
             }
         }
-        return <Map key={'current-map'}
-                    id={'current-map'}
-                    game={gameEngine}
-                    mapInfo={this.getMapInfo(gameEngine.map_name)}
-                    onError={this.getPage().error}
-                    orderBuilding={ContentGame.getOrderBuilding(powerName, orderType, orderPath)}
-                    onOrderBuilding={this.onOrderBuilding}
-                    onOrderBuilt={this.onOrderBuilt}
-                    showOrders={true}
-                    orders={orders}
-                    onSelectLocation={this.onSelectLocation}
-                    onSelectVia={this.onSelectVia}/>;
+        return (
+            <div id="current-map" key="current-map">
+                <SvgStandard game={gameEngine}
+                             mapData={new MapData(this.getMapInfo(gameEngine.map_name), gameEngine)}
+                             onError={this.getPage().error}
+                             orderBuilding={ContentGame.getOrderBuilding(powerName, orderType, orderPath)}
+                             onOrderBuilding={this.onOrderBuilding}
+                             onOrderBuilt={this.onOrderBuilt}
+                             orders={orders}
+                             onSelectLocation={this.onSelectLocation}
+                             onSelectVia={this.onSelectVia}/>
+            </div>
+        );
     }
 
     __get_engine_to_display(initialEngine) {
@@ -1265,10 +1304,10 @@ export class ContentGame extends React.Component {
                             navigation={navigation}/>
                 <Tabs menu={tabNames} titles={tabTitles} onChange={this.onChangeMainTab} active={mainTab}>
                     {/* Tab Phase history. */}
-                    {(hasTabPhaseHistory && this.renderTabResults(mainTab === 'phase_history', engine)) || ''}
-                    {this.renderTabMessages(mainTab === 'messages', engine, currentPowerName)}
+                    {(hasTabPhaseHistory && mainTab === 'phase_history' && this.renderTabResults(mainTab === 'phase_history', engine)) || ''}
+                    {mainTab === 'messages' && this.renderTabMessages(mainTab === 'messages', engine, currentPowerName)}
                     {/* Tab Current phase. */}
-                    {(hasTabCurrentPhase && this.renderTabCurrentPhase(
+                    {(mainTab === 'current_phase' && hasTabCurrentPhase && this.renderTabCurrentPhase(
                         mainTab === 'current_phase',
                         engine,
                         currentPowerName,
-- 
cgit v1.2.3

