Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions src/Basescape/BasescapeState.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ BasescapeState::BasescapeState(Base *base, Globe *globe) : _base(base), _globe(g
// while browsing, restored on exit in btnGeoscapeClick. PRD-J07: fenced in
// SHARED - every base in _bases is real and fully browsable by any player, so
// no entry filtering / old_bases juggling.)
if (_game->getCoopMod()->getCoopStatic() == true && !_game->getCoopMod()->isSharedCampaign() && _base->_coopBase == false && _game->getCoopMod()->getCoopCampaign() == true)
if (_game->getCoopMod()->getCoopStatic() == true && !_game->getCoopMod()->isSharedCampaign() && _base && _base->_coopBase == false && _game->getCoopMod()->getCoopCampaign() == true)
{

// coop
Expand Down Expand Up @@ -225,7 +225,11 @@ BasescapeState::BasescapeState(Base *base, Globe *globe) : _base(base), _globe(g


// COOP
if (_base->_coopBase == true)
// _base can be null here: vanilla allows BasescapeState(nullptr, ...) (e.g.
// GeoscapeState's no-base path, or a SHARED replica's ItemsArrivingState
// "Go to Base" whose _base was never resolved). init()->setBase() normalizes
// a null base to a real one, but this ctor block runs first, so guard it.
if (_base && _base->_coopBase == true)
{
_btnNewBase->setVisible(false);
_btnFacilities->setVisible(false);
Expand Down
22 changes: 20 additions & 2 deletions src/CoopMod/SharedEcon.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2278,7 +2278,23 @@ void alertApply(Game* game, Json::Value& payload, Base* base, int /*seat*/)
}
else if (cls == "ItemsArrivingState")
{
gs->popup(new ItemsArrivingState(gs));
const Json::Value& jr = payload["rows"];
if (jr.isArray() && !jr.empty())
{
std::vector<ArrivalRow> rows;
for (Json::ArrayIndex i = 0; i < jr.size(); ++i)
{
ArrivalRow row;
row.type = jr[i].get("type", 0).asInt();
row.name = jr[i].get("name", "").asString();
row.qty = jr[i].get("qty", 0).asInt();
row.base = jr[i].get("base", "").asString();
row.baseIdx = jr[i].get("baseIdx", -1).asInt();
row.ownerSeat = jr[i].get("ownerSeat", -1).asInt();
rows.push_back(row);
}
gs->popup(new ItemsArrivingState(gs, rows));
}
}
else if (cls == "ResearchRequiredState")
{
Expand Down Expand Up @@ -3337,7 +3353,7 @@ void hostAlienBaseFound(Game* game, AlienBase* alienBase)

void hostAlert(Game* game, const std::string& cls, const std::string& msg,
Base* base, int craftId, const std::vector<std::string>& names,
const std::vector<int>& ids, bool flag)
const std::vector<int>& ids, bool flag, const Json::Value& rows)
{
if (!sharedHost(game)) return;
Json::Value p;
Expand All @@ -3351,6 +3367,8 @@ void hostAlert(Game* game, const std::string& cls, const std::string& msg,
Json::Value ji(Json::arrayValue);
for (int i : ids) ji.append(i);
p["ids"] = ji;
if (rows.isArray() && !rows.empty())
p["rows"] = rows;
submitLocalCmd(game, "alert", base ? baseIndex(game, base) : 0, p);
}

Expand Down
3 changes: 2 additions & 1 deletion src/CoopMod/SharedEcon.h
Original file line number Diff line number Diff line change
Expand Up @@ -337,7 +337,8 @@ void hostAlienBaseFound(Game* game, AlienBase* alienBase);
void hostAlert(Game* game, const std::string& cls, const std::string& msg = "",
Base* base = nullptr, int craftId = -1,
const std::vector<std::string>& names = {},
const std::vector<int>& ids = {}, bool flag = false);
const std::vector<int>& ids = {}, bool flag = false,
const Json::Value& rows = Json::Value());
/// Playtest: host broadcasts that a craft's landing decision is resolved, so every
/// other seat's broker ConfirmLandingState closes itself.
void broadcastLandClose(Game* game, Craft* craft);
Expand Down
44 changes: 43 additions & 1 deletion src/CoopMod/TestServer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
#include "../Engine/Options.h"
#include "../Engine/State.h"
#include "../Geoscape/GeoscapeState.h"
#include "../Geoscape/ItemsArrivingState.h"
#include "../Geoscape/GeoscapeCraftState.h"
#include "../Geoscape/GeoscapeEventState.h"
#include "../Geoscape/MonthlyReportState.h"
Expand Down Expand Up @@ -2905,9 +2906,50 @@ bool TestServer::executeShared11(const std::string& cmd, const Json::Value& req,
SharedEcon::hostAlert(_game, req.get("cls", "").asString(),
req.get("msg", "").asString(), alertBase,
req.get("craft_id", -1).asInt(), alertNames, alertIds,
req.get("flag", false).asBool());
req.get("flag", false).asBool(), req.get("rows", Json::Value()));
resp["ok"] = true;
}
else if (cmd == "items_arriving_goto")
{
// Repro for the SHARED "ordered soldiers arrive" crash (host orders
// soldiers in a shared base -> the 2nd player crashes on arrival). On a
// replica the ItemsArrivingState is raised via SharedEcon::hostAlert and its
// _base is null (the replica's own transfers are frozen, so the ctor finds
// no hour-0 transfer). Fire the REAL "Go to Base" path; pre-fix this built
// BasescapeState(nullptr) and faulted in the ctor's coop block.
if (auto* ia = topState<ItemsArrivingState>(_game))
{
ia->harnessGotoBase();
resp["ok"] = true;
}
else
{
resp["error"] = "ItemsArrivingState not on top";
}
}
else if (cmd == "items_arriving_rows")
{
// Read the arrival popup's displayed row labels (owner-prefixed) plus this
// machine's seat and the shared player roster, for the arrival owner-label test.
if (auto* ia = topState<ItemsArrivingState>(_game))
{
Json::Value arr(Json::arrayValue);
for (const auto& s : ia->harnessRows())
arr.append(s);
resp["rows"] = arr;
resp["localSeat"] = connectionTCP::localSeat();
Json::Value players(Json::arrayValue);
if (_game->getSavedGame())
for (const auto& p : _game->getSavedGame()->getCoopPlayers())
players.append(p);
resp["coopPlayers"] = players;
resp["ok"] = true;
}
else
{
resp["error"] = "ItemsArrivingState not on top";
}
}
else if (cmd == "spawn_craft")
{
// Add a fully-armed craft (e.g. STR_INTERCEPTOR) to the first own base.
Expand Down
24 changes: 22 additions & 2 deletions src/Geoscape/GeoscapeState.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3893,8 +3893,28 @@ void GeoscapeState::time1Hour()
}
if (window)
{
popup(new ItemsArrivingState(this));
SharedEcon::hostAlert(_game, "ItemsArrivingState");
ItemsArrivingState* ia = new ItemsArrivingState(this);
if (ia->getRows().empty())
{
delete ia;
}
else
{
popup(ia);
Json::Value rowsJson(Json::arrayValue);
for (const auto& r : ia->getRows())
{
Json::Value j;
j["type"] = r.type;
j["name"] = r.name;
j["qty"] = r.qty;
j["base"] = r.base;
j["baseIdx"] = r.baseIdx;
j["ownerSeat"] = r.ownerSeat;
rowsJson.append(j);
}
SharedEcon::hostAlert(_game, "ItemsArrivingState", "", nullptr, -1, {}, {}, false, rowsJson);
}
}
// Handle Production
for (auto* xbase : *_game->getSavedGame()->getBases())
Expand Down
97 changes: 91 additions & 6 deletions src/Geoscape/ItemsArrivingState.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -29,20 +29,32 @@
#include "../Savegame/Base.h"
#include "../Savegame/Transfer.h"
#include "../Savegame/Craft.h"
#include "../Savegame/Soldier.h"
#include "../Mod/RuleItem.h"
#include "GeoscapeState.h"
#include "../Engine/Options.h"
#include "../Basescape/BasescapeState.h"
#include "../CoopMod/connectionTCP.h"

namespace OpenXcom
{

static std::string formatRow(const ArrivalRow& r)
{
if (r.type == TRANSFER_SOLDIER && r.ownerSeat >= 0
&& r.ownerSeat != connectionTCP::localSeat())
{
std::string owner = connectionTCP::seatName(r.ownerSeat);
if (!owner.empty())
return "[" + owner + "] " + r.name;
}
return r.name;
}

/**
* Initializes all the elements in the Items Arriving window.
* @param game Pointer to the core game.
* @param state Pointer to the Geoscape state.
* Builds the shared window widgets.
*/
ItemsArrivingState::ItemsArrivingState(GeoscapeState *state) : _state(state), _base(0)
void ItemsArrivingState::buildUI()
{
_screen = false;

Expand Down Expand Up @@ -95,7 +107,18 @@ ItemsArrivingState::ItemsArrivingState(GeoscapeState *state) : _state(state), _b
_lstTransfers->setSelectable(true);
_lstTransfers->setBackground(_window);
_lstTransfers->setMargin(2);
}

/**
* Initializes all the elements in the Items Arriving window.
* @param game Pointer to the core game.
* @param state Pointer to the Geoscape state.
*/
ItemsArrivingState::ItemsArrivingState(GeoscapeState *state) : _state(state), _base(0)
{
buildUI();

int baseIdx = 0;
for (auto* xbase : *_game->getSavedGame()->getBases())
{
for (auto transferIt = xbase->getTransfers()->begin(); transferIt != xbase->getTransfers()->end();)
Expand All @@ -121,7 +144,16 @@ ItemsArrivingState::ItemsArrivingState(GeoscapeState *state) : _state(state), _b
// Remove transfer
std::ostringstream ss;
ss << transfer->getQuantity();
_lstTransfers->addRow(3, transfer->getName(_game->getLanguage()).c_str(), ss.str().c_str(), xbase->getName().c_str());
ArrivalRow row;
row.type = transfer->getType();
row.name = transfer->getName(_game->getLanguage());
row.qty = transfer->getQuantity();
row.base = xbase->getName();
row.baseIdx = baseIdx;
row.ownerSeat = (transfer->getType() == TRANSFER_SOLDIER && transfer->getSoldier())
? transfer->getSoldier()->getOwnerPlayerId() : -1;
_rows.push_back(row);
_lstTransfers->addRow(3, formatRow(row).c_str(), ss.str().c_str(), xbase->getName().c_str());
delete transfer;
transferIt = xbase->getTransfers()->erase(transferIt);
}
Expand All @@ -130,6 +162,34 @@ ItemsArrivingState::ItemsArrivingState(GeoscapeState *state) : _state(state), _b
++transferIt;
}
}
++baseIdx;
}
}

/**
* Initializes the window from a network-supplied row list (SHARED replica),
* without scanning or deleting any transfers.
* @param state Pointer to the Geoscape state.
* @param rows Arrival rows to display.
*/
ItemsArrivingState::ItemsArrivingState(GeoscapeState *state, const std::vector<ArrivalRow>& rows) : _state(state), _base(0)
{
buildUI();

for (const ArrivalRow& r : rows)
{
_rows.push_back(r);
std::ostringstream ss;
ss << r.qty;
_lstTransfers->addRow(3, formatRow(r).c_str(), ss.str().c_str(), r.base.c_str());
}

if (!rows.empty())
{
auto* bases = _game->getSavedGame()->getBases();
int idx = rows.front().baseIdx;
if (idx >= 0 && idx < (int)bases->size())
_base = bases->at(idx);
}
}

Expand Down Expand Up @@ -158,7 +218,32 @@ void ItemsArrivingState::btnGotoBaseClick(Action *)
{
_state->timerReset();
_game->popState();
_game->pushState(new BasescapeState(_base, _state->getGlobe()));
// A SHARED replica's ItemsArrivingState is raised via SharedEcon::hostAlert
// with no hour-0 transfer of its own, so _base is null here; fall back to a
// real base (the BasescapeState ctor's coop block dereferences it).
// getSelectedBase() never returns null when any base exists.
Base *target = _base;
if (!target && !_game->getSavedGame()->getBases()->empty())
target = _game->getSavedGame()->getSelectedBase();
_game->pushState(new BasescapeState(target, _state->getGlobe()));
}

/**
* Test automation: fire the real "Go to Base" path (drives btnGotoBaseClick).
* Used by the SHARED "ordered soldiers arrive" crash repro, where a replica's
* hostAlert-raised popup carries a null _base.
*/
void ItemsArrivingState::harnessGotoBase()
{
btnGotoBaseClick(nullptr);
}

std::vector<std::string> ItemsArrivingState::harnessRows() const
{
std::vector<std::string> out;
for (const auto& r : _rows)
out.push_back(formatRow(r));
return out;
}

}
24 changes: 24 additions & 0 deletions src/Geoscape/ItemsArrivingState.h
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@
* along with OpenXcom. If not, see <http://www.gnu.org/licenses/>.
*/
#include "../Engine/State.h"
#include <vector>
#include <string>

namespace OpenXcom
{
Expand All @@ -29,6 +31,16 @@ class TextList;
class GeoscapeState;
class Base;

struct ArrivalRow
{
int type;
std::string name;
int qty;
std::string base;
int baseIdx;
int ownerSeat;
};

/**
* Items Arriving window that displays all
* the items that have arrived at bases.
Expand All @@ -42,15 +54,27 @@ class ItemsArrivingState : public State
Window *_window;
Text *_txtTitle, *_txtItem, *_txtQuantity, *_txtDestination;
TextList *_lstTransfers;
std::vector<ArrivalRow> _rows;
/// Builds the shared window widgets.
void buildUI();
public:
/// Creates the ItemsArriving state.
ItemsArrivingState(GeoscapeState *state);
/// Creates the ItemsArriving state from a network-supplied row list.
ItemsArrivingState(GeoscapeState *state, const std::vector<ArrivalRow>& rows);
/// Cleans up the ItemsArriving state.
~ItemsArrivingState();
/// Handler for clicking the OK button.
void btnOkClick(Action *action);
/// Handler for clicking the Go To Base button.
void btnGotoBaseClick(Action *action);
/// Test automation: fire the real "Go to Base" path (issue repro: a replica's
/// hostAlert-raised popup has a null _base).
void harnessGotoBase();
/// Gets the arrival rows backing this popup.
const std::vector<ArrivalRow>& getRows() const { return _rows; }
/// Test automation: the formatted first-column labels.
std::vector<std::string> harnessRows() const;
};

}
7 changes: 6 additions & 1 deletion tools/coop_test/test_shared_alerts.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,12 @@ def main():

_check(host, client, "LowFuelState", {"craft_id": craft_id}, "low fuel")

_check(host, client, "ItemsArrivingState", {}, "items arriving")
# ItemsArrivingState now requires a rows payload (an empty arrival popup no
# longer pops); pass one synthetic arrival so the client rebuilds + pops it.
_check(host, client, "ItemsArrivingState",
{"rows": [{"type": 0, "name": "STR_TEST_ARRIVAL", "qty": 1,
"base": "HostBase", "baseIdx": 0, "ownerSeat": -1}]},
"items arriving")

# New-possibility family (research/manufacture/purchase/craft/facility).
_check(host, client, "NewPossibleResearchState",
Expand Down
Loading
Loading