scraps

Abandon all hope, ye who enter here.
git clone https://git.neptards.moe/neptards/scraps.git
Log | Files | Refs | Submodules | README | LICENSE

command_character.cpp (25672B)


      1 #include "scraps/game/action_eval/command.hpp" // IWYU pragma: associated
      2 
      3 #include "scraps/game/action_eval/action_helper.hpp"
      4 #include "scraps/game/action_eval/enter_leave.hpp"
      5 #include "scraps/game/action_eval/test_helper.hpp"
      6 #include "scraps/game/character_state.hpp"
      7 #include "scraps/game/file_state.hpp"
      8 #include "scraps/game/room_state.hpp"
      9 #include "scraps/game/text_replace.hpp"
     10 #include "scraps/string_utils.hpp"
     11 #include "scraps/uuid.hpp"
     12 
     13 #include <libshit/except.hpp>
     14 
     15 #include <boost/container/static_vector.hpp>
     16 
     17 #include <cstdint>
     18 #include <optional>
     19 #include <string>
     20 #include <utility>
     21 #include <variant>
     22 
     23 // IWYU pragma: no_include <type_traits>
     24 // IWYU pragma: no_forward_declare Scraps::Game::IdKey
     25 // IWYU pragma: no_forward_declare Scraps::Game::NameKey
     26 // IWYU pragma: no_forward_declare Scraps::Game::UuidKey
     27 
     28 namespace Scraps::Game::ActionEvalPrivate
     29 {
     30   TEST_SUITE_BEGIN("Scraps::Game::ActionEval");
     31 
     32   CommandRes CommandCharacterActionSet(CommandParam p)
     33   {
     34     auto c = p.gc->GetCharacterColl().Get<NameKey>(
     35       p.Replace(p.eval.tmp_str0, p.cmd.GetParam0()));
     36     return c ? CommandActionSetCommon(p, c->GetActions()) : CommandRes::OK;
     37   }
     38 
     39   TEST_CASE_FIXTURE(Fixture, "CommandCharacterActionSet")
     40   {
     41     dummy.game.GetCharacters()[1].GetActions()[0].SetActive(false);
     42 
     43     SetCmd("nosuch", "invalid", "", "");
     44     CHECK(CommandCharacterActionSet(c) == CommandRes::OK);
     45 
     46     auto act = st.GetCharacterColl().At(1).GetActions().GetColl().At(0);
     47     SetCmd("chara_1", "action_0-Active", "", "");
     48     CHECK(CommandCharacterActionSet(c) == CommandRes::OK);
     49     CHECK(act.GetActive() == true);
     50   }
     51 
     52   CommandRes CommandPlayerActionSet(CommandParam p)
     53   { return CommandActionSetCommon(p, p.gc->GetPlayer().GetActions()); }
     54 
     55   // ---------------------------------------------------------------------------
     56 
     57   static CommandRes CharaPropSet(CommandParam p, bool js)
     58   {
     59     auto parts = SplitMax<3>(p.Replace(p.eval.tmp_str0, p.cmd.GetParam0()), ':');
     60     if (parts.size() != 2) return CommandRes::OK;
     61     auto c = p.gc->GetCharacterColl().Get<NameKey>(parts[0]);
     62     if (!c) return CommandRes::OK;
     63 
     64     return CommandCustomPropertySetCommon(p, c->GetProperties(), parts[1], js);
     65   }
     66 
     67   CommandRes CommandCharacterCustomPropertySet(CommandParam p)
     68   { return CharaPropSet(p, false); }
     69   CommandRes CommandCharacterCustomPropertySetJs(CommandParam p)
     70   { return CharaPropSet(p, true); }
     71 
     72   TEST_CASE_FIXTURE(Fixture, "CommandCharacterCustomPropertySet")
     73   {
     74     auto cp = st.GetCharacterColl().At(0).GetProperties();
     75     SetCmd("invalid", "Equals", "foo", "");
     76     CHECK(CommandCharacterCustomPropertySet(c) == CommandRes::OK);
     77     SetCmd("chara_0:key_0:shit", "Equals", "foo", "");
     78     CHECK(CommandCharacterCustomPropertySet(c) == CommandRes::OK);
     79     CHECK(cp.Get("key_0")->second == "Value 0");
     80 
     81     SetCmd("chara_0:key_0", "Equals", "10/2", "");
     82     CHECK(CommandCharacterCustomPropertySet(c) == CommandRes::OK);
     83     CHECK(cp.Get("key_0")->second == "10/2");
     84     CHECK(CommandCharacterCustomPropertySetJs(c) == CommandRes::OK);
     85     CHECK(cp.Get("key_0")->second == "5");
     86   }
     87 
     88   static CommandRes PlayerPropSet(CommandParam p, bool js)
     89   {
     90     auto name = p.Replace(p.eval.tmp_str0, p.cmd.GetParam0());
     91     return CommandCustomPropertySetCommon(
     92       p, p.gc->GetPlayer().GetProperties(), name, js);
     93   }
     94 
     95   CommandRes CommandPlayerCustomPropertySet(CommandParam p)
     96   { return PlayerPropSet(p, false); }
     97   CommandRes CommandPlayerCustomPropertySetJs(CommandParam p)
     98   { return PlayerPropSet(p, true); }
     99 
    100   TEST_CASE_FIXTURE(Fixture, "CommandPlayerCustomPropertySet")
    101   {
    102     SetCmd("key_0", "Equals", "foo", "");
    103     CHECK(CommandPlayerCustomPropertySet(c) == CommandRes::OK);
    104     CHECK(st.GetPlayer().GetProperties().Get("key_0")->second == "foo");
    105 
    106     dummy.game.GetCharacters()[0].GetProperties()[0].SetName(
    107       dummy.GetString("foo:bar"));
    108     Reinit();
    109     SetCmd("foo:bar", "Equals", "2*3", "");
    110     CHECK(CommandPlayerCustomPropertySet(c) == CommandRes::OK);
    111     CHECK(st.GetPlayer().GetProperties().Get("foo:bar")->second == "2*3");
    112     CHECK(CommandPlayerCustomPropertySetJs(c) == CommandRes::OK);
    113     CHECK(st.GetPlayer().GetProperties().Get("foo:bar")->second == "6");
    114   }
    115 
    116   // ---------------------------------------------------------------------------
    117 
    118   static CommandRes DescriptionSetCommon(CommandParam p, CharacterProxy c)
    119   {
    120     p.Replace(p.eval.tmp_str0, p.cmd.GetParam3());
    121     c.SetDescription(p.eval.tmp_str0);
    122     return CommandRes::OK;
    123   }
    124 
    125   CommandRes CommandCharacterDescriptionSet(CommandParam p)
    126   {
    127     return DescriptionSetCommon(p, p.gc->GetCharacterColl().At<NameKey>(
    128       p.Replace(p.eval.tmp_str0, p.cmd.GetParam0())));
    129   }
    130 
    131   TEST_CASE_FIXTURE(Fixture, "CommandCharacterDescriptionSet")
    132   {
    133     SetCmd("nosuch", "", "", "foo");
    134     CHECK_THROWS(CommandCharacterDescriptionSet(c));
    135 
    136     SetCmd("chara_0", "", "", "new desc");
    137     CHECK(CommandCharacterDescriptionSet(c) == CommandRes::OK);
    138     CHECK(st.GetCharacterColl().At(0).GetDescription() == "new desc");
    139   }
    140 
    141   CommandRes CommandPlayerDescriptionSet(CommandParam p)
    142   { return DescriptionSetCommon(p, p.gc->GetPlayer()); }
    143 
    144   // ---------------------------------------------------------------------------
    145 
    146   CommandRes CommandCharacterDescriptionShow(CommandParam p)
    147   {
    148     auto c = p.gc->GetCharacterColl().At<NameKey>(
    149       p.Replace(p.eval.tmp_str0, p.cmd.GetParam0()));
    150     p.gc.Log(c.GetDescription());
    151     return CommandRes::OK;
    152   }
    153 
    154   TEST_CASE_FIXTURE(Fixture, "CommandCharacterDescriptionShow")
    155   {
    156     SetCmd("nosuch", "", "", "");
    157     CHECK_THROWS(CommandCharacterDescriptionShow(c));
    158     CHECK(log == "");
    159 
    160     SetCmd("chara_0", "", "", "");
    161     CHECK(CommandCharacterDescriptionShow(c) == CommandRes::OK);
    162     CHECK(log == "This is character #0\n"); log.clear();
    163   }
    164 
    165   CommandRes CommandPlayerDescriptionShow(CommandParam p)
    166   {
    167     p.eval.tmp_str0.assign(p.gc->GetPlayer().GetDescription());
    168     ReplaceText(p.eval.tmp_str0, *p.gc, 0);
    169     p.gc.Log(p.eval.tmp_str0);
    170     return CommandRes::OK;
    171   }
    172 
    173   // ---------------------------------------------------------------------------
    174 
    175   static CommandRes GenderSetCommon(
    176     CommandParam p, CharacterProxy c, Libshit::StringView gender)
    177   {
    178     auto g = RagsStr2Gender(p.Replace(p.eval.tmp_str0, gender));
    179     if (!g)
    180       LIBSHIT_THROW(ActionEvalError, "Invalid gender string",
    181                     "String", p.eval.tmp_str0);
    182 
    183     c.SetGender(*g);
    184     return CommandRes::OK;
    185   }
    186 
    187   CommandRes CommandCharacterGenderSet(CommandParam p)
    188   {
    189     auto c = p.gc->GetCharacterColl().Get<NameKey>(
    190       p.Replace(p.eval.tmp_str0, p.cmd.GetParam0()));
    191     return c ? GenderSetCommon(p, *c, p.cmd.GetParam1()) : CommandRes::OK;
    192   }
    193 
    194   TEST_CASE_FIXTURE(Fixture, "CommandCharacterGenderSet")
    195   {
    196     using G = Format::Proto::Gender;
    197     auto chara = st.GetCharacterColl().At(0);
    198     SetCmd("nosuch", "Female", "", "");
    199     CHECK(CommandCharacterGenderSet(c) == CommandRes::OK);
    200     CHECK(chara.GetGender() == G::MALE);
    201 
    202     auto check_ok = [&](const char* str, G gender)
    203     {
    204       CAPTURE(str); CAPTURE(gender);
    205       SetCmd("chara_0", str, "", "");
    206       CHECK(CommandCharacterGenderSet(c) == CommandRes::OK);
    207       CHECK(chara.GetGender() == gender);
    208     };
    209     check_ok("Female", G::FEMALE);
    210     check_ok("Male", G::MALE);
    211     check_ok("Other", G::OTHER);
    212     check_ok("  Male ", G::MALE);
    213 
    214     SetCmd("chara_0", "male", "", "");
    215     CHECK_THROWS(CommandCharacterGenderSet(c));
    216   }
    217 
    218   CommandRes CommandPlayerGenderSet(CommandParam p)
    219   { return GenderSetCommon(p, p.gc->GetPlayer(), p.cmd.GetParam0()); }
    220 
    221   // ---------------------------------------------------------------------------
    222 
    223   static CommandRes ImageSetCommon(
    224     CommandParam p, CharacterProxy c, Libshit::StringView fname)
    225   {
    226     auto f = p.gc->GetFileColl().StateGet<NameKey>(
    227       p.Replace(p.eval.tmp_str0, fname));
    228 
    229     c.SetImageId(f ? f->id : FileId{});
    230     return CommandRes::OK;
    231   }
    232 
    233   CommandRes CommandCharacterImageSet(CommandParam p)
    234   {
    235     auto c = p.gc->GetCharacterColl().Get<NameKey>(
    236       p.Replace(p.eval.tmp_str0, p.cmd.GetParam0()));
    237     return c ? ImageSetCommon(p, *c, p.cmd.GetParam1()) : CommandRes::OK;
    238   }
    239 
    240   TEST_CASE_FIXTURE(Fixture, "CommandCharacterImageSet")
    241   {
    242     SetCmd("nosuch", "nosuch", "", "");
    243     CHECK(CommandCharacterImageSet(c) == CommandRes::OK);
    244 
    245     auto chara = st.GetCharacterColl().At(1);
    246     CHECK(chara.GetImageId() != FileId{});
    247     SetCmd("chara_1", "nosuch", "", "");
    248     CHECK(CommandCharacterImageSet(c) == CommandRes::OK);
    249     CHECK(chara.GetImageId() == FileId{});
    250 
    251     SetCmd("chara_1", "file_2", "", "");
    252     CHECK(CommandCharacterImageSet(c) == CommandRes::OK);
    253     CHECK(chara.GetImageId() == st.GetFileColl().At(2).GetId());
    254   }
    255 
    256   CommandRes CommandPlayerImageSet(CommandParam p)
    257   { return ImageSetCommon(p, p.gc->GetPlayer(), p.cmd.GetParam0()); }
    258 
    259   // ---------------------------------------------------------------------------
    260 
    261   CommandRes CommandPlayerImageSetOverlay(CommandParam p)
    262   {
    263     auto f = p.gc->GetFileColl().StateGet<NameKey>(
    264       p.Replace(p.eval.tmp_str0, p.cmd.GetParam0()));
    265 
    266     p.gc->GetPlayer().SetOverlayImageId(f ? f->id : FileId{});
    267     return CommandRes::OK;
    268   }
    269 
    270   TEST_CASE_FIXTURE(Fixture, "CommandPlayerImageSetOverlay")
    271   {
    272     SetCmd("file_0", "", "", "");
    273     CHECK(CommandPlayerImageSetOverlay(c) == CommandRes::OK);
    274     CHECK(st.GetPlayer().GetOverlayImageId() == st.GetFileColl().At(0).GetId());
    275 
    276     SetCmd("nosuch", "", "", "");
    277     CHECK(CommandPlayerImageSetOverlay(c) == CommandRes::OK);
    278     CHECK(st.GetPlayer().GetOverlayImageId() == FileId{});
    279   }
    280 
    281   // ---------------------------------------------------------------------------
    282 
    283   CommandRes CommandCharacterImageShow(CommandParam p)
    284   {
    285     auto c = p.gc->GetCharacterColl().Get<NameKey>(
    286       p.Replace(p.eval.tmp_str0, p.cmd.GetParam0()));
    287     if (!c) return CommandRes::OK;
    288 
    289     if (p.gc->GetInlineImages())
    290     {
    291       auto f = p.gc->GetRawFileColl().StateAt<IdKey>(c->GetImageId());
    292       p.gc.ImageLog(f.id);
    293     }
    294     else if (p.gc->GetShowMainImage())
    295     {
    296       auto f = p.gc->GetFileColl().Get<IdKey>(c->GetImageId());
    297       if (f && f->IsValidImage())
    298       {
    299         p.gc->SetMainImageId(f->GetId());
    300         p.gc->SetMainOverlayImageId(f->GetId());
    301       }
    302     }
    303 
    304     return CommandRes::OK;
    305   }
    306 
    307   TEST_CASE_FIXTURE(Fixture, "CommandCharacterImageShow")
    308   {
    309     st.GetCharacterColl().At(1).SetImageId(FileId{1337});
    310     auto check = [&](const char* chara, FileId id)
    311     {
    312       st.SetMainImageId(FileId{1234});
    313       st.SetMainOverlayImageId(FileId{1234});
    314       SetCmd(chara, "", "", "");
    315       CHECK(CommandCharacterImageShow(c) == CommandRes::OK);
    316       CHECK(st.GetMainImageId() == id);
    317       CHECK(st.GetMainOverlayImageId() == id);
    318     };
    319     check("nosuch", FileId{1234});
    320     check("chara_0", st.GetFileColl().At(0).GetId());
    321     check("chara_1", FileId{1234});
    322 
    323     dummy.game.SetShowMainImage(false);
    324     check("chara_0", FileId{1234});
    325     check("chara_1", FileId{1234});
    326 
    327     dummy.game.SetInlineImages(true);
    328     FileId fid{445566};
    329     gc.SetImageLogFunction([&](FileId i) { fid = i; });
    330 
    331     check("nosuch", FileId{1234});
    332     CHECK(fid == FileId{445566});
    333 
    334     fid = {};
    335     check("chara_0", FileId{1234});
    336     CHECK(fid == st.GetFileColl().At(0).GetId());
    337 
    338     fid = FileId{665544};
    339     SetCmd("chara_1", "", "", "");
    340     CHECK_THROWS(CommandCharacterImageShow(c));
    341     CHECK(fid == FileId{665544});
    342   }
    343 
    344   // ---------------------------------------------------------------------------
    345 
    346   namespace
    347   {
    348     struct CommandCharacterMoveCoro final : EvalItem
    349     {
    350       CommandCharacterMoveCoro(
    351         CharacterProxy c, CommandCtx& ctx, Libshit::StringView param3)
    352         : c{c}, ctx{ctx}, param3{param3} {}
    353       ActionResult Call(ActionEvalState& eval, GameController& gc) override;
    354       CharacterProxy c;
    355       CommandCtx& ctx;
    356       Libshit::StringView param3;
    357       std::uint8_t state = 0;
    358     };
    359   }
    360 
    361   ActionResult CommandCharacterMoveCoro::Call(
    362     ActionEvalState& eval, GameController& gc)
    363   {
    364     SCRAPS_AE_SWITCH()
    365     {
    366       ctx.cmd_res = CommandRes::OK;
    367       if (auto p = gc->GetPlayer(); c.GetRoomId() == p.GetRoomId())
    368       {
    369         auto a = p.GetActions().GetColl().Get<NameKey>(
    370           "<<On Character Leave>>");
    371         if (a) SCRAPS_AE_CALL_DEFER(1, OuterAction, *a, ObjectId{});
    372       }
    373       SCRAPS_AE_RESUME(1);
    374 
    375       eval.tmp_str0.assign(param3);
    376       ReplaceText(eval.tmp_str0, *gc, ctx.loop_item);
    377 
    378       RoomId dest{};
    379       if (eval.tmp_str0 == Uuid::VOID_ROOM_STR);
    380       else if (eval.tmp_str0 == Uuid::PLAYER_ROOM_STR)
    381         dest = gc->GetPlayer().GetRoomId();
    382       else if (auto uuid = Uuid::TryParse(eval.tmp_str0))
    383       {
    384         if (auto r = gc->GetRoomColl().StateGet<UuidKey>(*uuid))
    385           dest = r->id;
    386       }
    387       else
    388       {
    389         if (auto r = gc->GetRoomColl().StateGet<NameKey>(eval.tmp_str0))
    390           dest = r->id;
    391         else
    392         {
    393           eval.query_title = "Error in command CT_MoveChar.  Could not locate "
    394             "a room called " + eval.tmp_str0;
    395           SCRAPS_AE_GENERIC_RETURN(ActionResult::MSGBOX);
    396         }
    397       }
    398 
    399       c.SetRoomId(dest);
    400 
    401       auto p = gc->GetPlayer();
    402       if (dest == p.GetRoomId())
    403       {
    404         auto a = p.GetActions().GetColl().Get<NameKey>(
    405           "<<On Character Enter>>");
    406         if (a) SCRAPS_AE_TAIL_CALL(OuterAction, *a, ObjectId{});
    407       }
    408 
    409       SCRAPS_AE_RETURN();
    410     }
    411   }
    412 
    413   CommandRes CommandCharacterMove(CommandParam p)
    414   {
    415     auto c = p.gc->GetCharacterColl().At<NameKey>(
    416       p.Replace(p.eval.tmp_str0, p.cmd.GetParam0()));
    417     p.eval.stack.Push<CommandCharacterMoveCoro>(c, p.ctx, p.cmd.GetParam1());
    418     return CommandRes::CALL;
    419   }
    420 
    421   TEST_CASE_FIXTURE(Fixture, "CommandCharacterMove")
    422   {
    423     SetCmd("nosuch", "nosuch", "", "");
    424     CHECK_THROWS(CommandCharacterMove(c));
    425 
    426     {
    427       auto ch = st.GetCharacterColl().At(1);
    428       // moving to a non-existing name room -> msgbox
    429       auto orig = ch.GetRoomId();
    430       SetCmd("chara_1", "nosuch", "", "");
    431       REQUIRE(CommandCharacterMove(c) == CommandRes::CALL);
    432       ctx.cmd_res = CommandRes::CALL;
    433       CHECK(Call(eval, gc) == ActionResult::MSGBOX);
    434       CHECK(eval.query_title == "Error in command CT_MoveChar.  Could not "
    435             "locate a room called nosuch");
    436       REQUIRE(eval.stack.Empty());
    437       CHECK(ch.GetRoomId() == orig);
    438       CHECK(ctx.cmd_res == CommandRes::OK);
    439 
    440       auto simple_move = [&](Libshit::NonowningString room, RoomId rid)
    441       {
    442         SetCmd("chara_1", room, "", "");
    443         REQUIRE(CommandCharacterMove(c) == CommandRes::CALL);
    444         ctx.cmd_res = CommandRes::CALL;
    445         CallCheckLast();
    446         CHECK(ch.GetRoomId() == rid);
    447         CHECK(ctx.cmd_res == CommandRes::OK);
    448       };
    449       // moving to a non-existing UUID room -> move to void
    450       simple_move("12345678-1234-5678-9abc-123456789012", {});
    451       // moving to an existing room
    452       simple_move("room_2", st.GetRoomColl().At(2).GetId());
    453       simple_move("00000000000040040000000000000001",
    454                   st.GetRoomColl().At(1).GetId());
    455 
    456       // special cases, no actions
    457       simple_move(Uuid::PLAYER_ROOM_STR, st.GetPlayerRoom().GetId());
    458       simple_move(Uuid::VOID_ROOM_STR, {});
    459     }
    460 
    461     // generate those actions
    462     auto acts = dummy.game.GetCharacters()[0].GetActions();
    463     acts[0].SetName(dummy.GetString("<<On Character Enter>>"));
    464     acts[1].SetName(dummy.GetString("<<On Character Leave>>"));
    465     Reinit();
    466 
    467     auto ch = st.GetCharacterColl().At(1);
    468     // move to player, with action
    469     SetCmd("chara_1", "room_0", "", "");
    470     REQUIRE(CommandCharacterMove(c) == CommandRes::CALL);
    471     ctx.cmd_res = CommandRes::CALL;
    472     CHECK(Call(eval, gc) == ActionResult::IDLE);
    473     CHECK(ch.GetRoomId() == st.GetRoomColl().At(0).GetId());
    474     CHECK(ctx.cmd_res == CommandRes::OK);
    475     REQUIRE(eval.stack.Size() == 1); // tail call
    476     CallCheckOa(st.GetPlayer().GetActions().GetColl().At(0).GetId(), {});
    477 
    478     // move from player, with action
    479     auto check = [&](const char* room)
    480     {
    481       SetCmd("chara_1", room, "", "");
    482       REQUIRE(CommandCharacterMove(c) == CommandRes::CALL);
    483       ctx.cmd_res = CommandRes::CALL;
    484       CHECK(Call(eval, gc) == ActionResult::IDLE);
    485       CHECK(ch.GetRoomId() == st.GetRoomColl().At(0).GetId()); // still in old room
    486       REQUIRE(eval.stack.Size() == 2); // normal call
    487       CallCheckOa(st.GetPlayer().GetActions().GetColl().At(1).GetId(), {});
    488     };
    489     check("room_1");
    490     CallCheckLast();
    491     CHECK(ch.GetRoomId() == st.GetRoomColl().At(1).GetId()); // new
    492     CHECK(ctx.cmd_res == CommandRes::OK);
    493 
    494     // in case of exception, chara not moved, but action still execd
    495     ch.SetRoomId(st.GetRoomColl().At(0).GetId());
    496     check("nosuch");
    497     CHECK(Call(eval, gc) == ActionResult::MSGBOX);
    498     REQUIRE(eval.stack.Empty());
    499     eval.stack.Clear();
    500     CHECK(ch.GetRoomId() == st.GetRoomColl().At(0).GetId());
    501     CHECK(ctx.cmd_res == CommandRes::OK);
    502   }
    503 
    504   CommandRes CommandPlayerMove(CommandParam p)
    505   {
    506     auto uuid = Uuid::TryParse(p.Replace(p.eval.tmp_str0, p.cmd.GetParam0()));
    507     auto rooms = p.gc->GetRoomColl();
    508     // this doesn't throw in rags, but instead moves player to the void room,
    509     // and hell breaks loose
    510     auto& r = uuid ? rooms.StateAt<UuidKey>(*uuid) :
    511       rooms.StateAt<NameKey>(p.eval.tmp_str0);
    512     p.gc->GetPlayer().SetRoomId(r.id);
    513 
    514     PushClearCtx(p);
    515     p.eval.stack.Push<RoomEntered>(false);
    516     return CommandRes::CALL;
    517   }
    518 
    519   TEST_CASE_FIXTURE(Fixture, "CommandPlayerMove")
    520   {
    521     SetCmd("nosuch", "", "", "");
    522     CHECK_THROWS(CommandPlayerMove(c));
    523     SetCmd("ffffffffffffffffffffffffffffffff", "", "", "");
    524     CHECK_THROWS(CommandPlayerMove(c));
    525 
    526     SetCmd("room_1", "", "", "");
    527     CHECK(CommandPlayerMove(c) == CommandRes::CALL);
    528     ctx.cmd_res = CommandRes::CALL;
    529     auto rid = st.GetRoomColl().At(1).GetId();
    530     CHECK(st.GetPlayer().GetRoomId() == rid);
    531 
    532     REQUIRE(eval.stack.Size() == 2);
    533     CHECK(dynamic_cast<RoomEntered*>(&eval.stack.Back()));
    534     eval.stack.Pop();
    535     CallCheckLast();
    536     CHECK(ctx.cmd_res == CommandRes::OK);
    537   }
    538 
    539   // ---------------------------------------------------------------------------
    540 
    541   CommandRes CommandCharacterMoveToObject(CommandParam p)
    542   {
    543     auto c = p.gc->GetCharacterColl().Get<NameKey>(
    544       p.Replace(p.eval.tmp_str0, p.cmd.GetParam0()));
    545     if (!c) return CommandRes::OK;
    546     auto o = GetObjectUuid(*p.gc, p.Replace(p.eval.tmp_str0, p.cmd.GetParam1()));
    547     if (!o) return CommandRes::OK;
    548 
    549     if (auto l = o->GetLocation(); auto rid = std::get_if<RoomId>(&l))
    550     {
    551       c->SetRoomId(*rid);
    552       if (*rid == p.gc->GetPlayer().GetRoomId())
    553         RoomEnteredSimple(p.eval, p.gc);
    554     }
    555     return CommandRes::OK;
    556   }
    557 
    558   TEST_CASE_FIXTURE(Fixture, "CommandCharacterMoveToObject")
    559   {
    560     SetCmd("nosuch", "nosuch", "", "");
    561     CHECK(CommandCharacterMoveToObject(c) == CommandRes::OK);
    562     SetCmd("nosuch", "00000000000b1ec10000000000000000", "", "");
    563     CHECK(CommandCharacterMoveToObject(c) == CommandRes::OK);
    564 
    565     auto chara = p.gc->GetCharacterColl().At(1);
    566     auto old = chara.GetRoomId();
    567     SetCmd("chara_1", "nosuch", "", "");
    568     CHECK(CommandCharacterMoveToObject(c) == CommandRes::OK);
    569     CHECK(chara.GetRoomId() == old);
    570 
    571     // object 0 is nowhere
    572     auto obj = p.gc->GetObjectColl().At(0);
    573     REQUIRE(std::holds_alternative<LocationNone>(obj.GetLocation()));
    574     SetCmd("chara_1", "00000000000b1ec10000000000000000", "", "");
    575     CHECK(CommandCharacterMoveToObject(c) == CommandRes::OK);
    576     CHECK(chara.GetRoomId() == old);
    577 
    578     // obj 0 is in a room
    579     obj.SetLocation(p.gc->GetRoomColl().At(2).GetId());
    580     SetCmd("chara_1", "00000000000b1ec10000000000000000", "", "");
    581     CHECK(CommandCharacterMoveToObject(c) == CommandRes::OK);
    582     CHECK(chara.GetRoomId() == p.gc->GetRoomColl().At(2).GetId());
    583     CHECK(log == "");
    584 
    585     // obj0 is next to the player
    586     obj.SetLocation(p.gc->GetPlayer().GetRoomId());
    587     CHECK(CommandCharacterMoveToObject(c) == CommandRes::OK);
    588     CHECK(chara.GetRoomId() == p.gc->GetPlayer().GetRoomId());
    589     CHECK(log == "\nThis is room #0\nCharacter #1 is here.\n");
    590     log.clear();
    591   }
    592 
    593   // ---------------------------------------------------------------------------
    594 
    595   CommandRes CommandPlayerMoveToCharacter(CommandParam p)
    596   {
    597     auto c = p.gc->GetCharacterColl().Get<NameKey>(
    598       p.Replace(p.eval.tmp_str0, p.cmd.GetParam0()));
    599     auto pl = p.gc->GetPlayer();
    600     if (!c || c->GetRoomId() == RoomId{} || c->GetRoomId() == pl.GetRoomId())
    601       return CommandRes::OK;
    602 
    603     pl.SetRoomId(c->GetRoomId());
    604     PushClearCtx(p);
    605     p.eval.stack.Push<RoomEntered>(false);
    606     return CommandRes::CALL;
    607   }
    608 
    609   TEST_CASE_FIXTURE(Fixture, "CommandPlayerMoveToCharacter")
    610   {
    611     auto rid = st.GetPlayer().GetRoomId();
    612     SetCmd("nosuch", "", "", "");
    613     CHECK(CommandPlayerMoveToCharacter(c) == CommandRes::OK);
    614     CHECK(st.GetPlayer().GetRoomId() == rid);
    615 
    616     // nowhere -> not moved
    617     st.GetCharacterColl().At(1).SetRoomId({});
    618     SetCmd("chara_1", "", "", "");
    619     CHECK(CommandPlayerMoveToCharacter(c) == CommandRes::OK);
    620     CHECK(st.GetPlayer().GetRoomId() == rid);
    621 
    622     // same room as player -> not moved
    623     st.GetCharacterColl().At(1).SetRoomId(rid);
    624     CHECK(CommandPlayerMoveToCharacter(c) == CommandRes::OK);
    625     CHECK(st.GetPlayer().GetRoomId() == rid);
    626 
    627     // different room -> moved
    628     st.GetCharacterColl().At(1).SetRoomId(st.GetRoomColl().At(1).GetId());
    629     CHECK(CommandPlayerMoveToCharacter(c) == CommandRes::CALL);
    630     ctx.cmd_res = CommandRes::CALL;
    631     REQUIRE(eval.stack.Size() == 2);
    632     CHECK(dynamic_cast<RoomEntered*>(&eval.stack.Back()));
    633     CHECK(st.GetPlayer().GetRoomId() == st.GetRoomColl().At(1).GetId());
    634     eval.stack.Pop();
    635     CallCheckLast();
    636     CHECK(ctx.cmd_res == CommandRes::OK);
    637   }
    638 
    639   // ---------------------------------------------------------------------------
    640 
    641   CommandRes CommandPlayerMoveToObject(CommandParam p)
    642   {
    643     auto o = GetObjectUuid(*p.gc, p.Replace(p.eval.tmp_str0, p.cmd.GetParam0()));
    644     if (!o || !std::holds_alternative<RoomId>(o->GetLocation()))
    645       return CommandRes::OK;
    646 
    647     auto pl = p.gc->GetPlayer();
    648     pl.SetRoomId(std::get<RoomId>(o->GetLocation()));
    649     PushClearCtx(p);
    650     p.eval.stack.Push<RoomEntered>(false);
    651     return CommandRes::CALL;
    652   }
    653 
    654   TEST_CASE_FIXTURE(Fixture, "CommandPlayerMoveToObject")
    655   {
    656     auto rid = st.GetPlayer().GetRoomId();
    657     SetCmd("invalid", "", "", "");
    658     CHECK(CommandPlayerMoveToObject(c) == CommandRes::OK);
    659     CHECK(st.GetPlayer().GetRoomId() == rid);
    660     SetCmd("ffffffffffffffffffffffffffffffff", "", "", "");
    661     CHECK(CommandPlayerMoveToObject(c) == CommandRes::OK);
    662     CHECK(st.GetPlayer().GetRoomId() == rid);
    663 
    664     // not in a room -> not moved
    665     st.GetObjectColl().At(0).SetLocation(st.GetObjectColl().At(0).GetId());
    666     SetCmd("00000000000b1ec10000000000000000", "", "", "");
    667     CHECK(CommandPlayerMoveToObject(c) == CommandRes::OK);
    668     CHECK(st.GetPlayer().GetRoomId() == rid);
    669 
    670     // in a room room -> "moved"
    671     st.GetObjectColl().At(0).SetLocation(rid);
    672     CHECK(CommandPlayerMoveToObject(c) == CommandRes::CALL);
    673     ctx.cmd_res = CommandRes::CALL;
    674     REQUIRE(eval.stack.Size() == 2);
    675     CHECK(dynamic_cast<RoomEntered*>(&eval.stack.Back()));
    676     CHECK(st.GetPlayer().GetRoomId() == rid);
    677     eval.stack.Pop();
    678     CallCheckLast();
    679     CHECK(ctx.cmd_res == CommandRes::OK);
    680 
    681     // in a different room
    682     st.GetObjectColl().At(0).SetLocation(st.GetRoomColl().At(1).GetId());
    683     CHECK(CommandPlayerMoveToObject(c) == CommandRes::CALL);
    684     ctx.cmd_res = CommandRes::CALL;
    685     REQUIRE(eval.stack.Size() == 2);
    686     CHECK(dynamic_cast<RoomEntered*>(&eval.stack.Back()));
    687     CHECK(st.GetPlayer().GetRoomId() == st.GetRoomColl().At(1).GetId());
    688     eval.stack.Pop();
    689     CallCheckLast();
    690     CHECK(ctx.cmd_res == CommandRes::OK);
    691   }
    692 
    693   // ---------------------------------------------------------------------------
    694 
    695   CommandRes CommandCharacterNameOverrideSet(CommandParam p)
    696   {
    697     if (auto c = p.gc->GetCharacterColl().Get<NameKey>(
    698           p.Replace(p.eval.tmp_str0, p.cmd.GetParam0())))
    699     {
    700       auto val = p.Replace(p.eval.tmp_str0, p.cmd.GetParam2());
    701       c->SetNameOverride(std::string{val});
    702     }
    703 
    704     return CommandRes::OK;
    705   }
    706 
    707   TEST_CASE_FIXTURE(Fixture, "CommandCharacterNameOverrideSet")
    708   {
    709     auto chara = st.GetCharacterColl().At(0);
    710     SetCmd("nosuch", "", "foo", "");
    711     CHECK(CommandCharacterNameOverrideSet(c) == CommandRes::OK);
    712     CHECK(chara.GetNameOverride() == "Character #0");
    713 
    714     SetCmd("chara_0", "", "bar", "");
    715     CHECK(CommandCharacterNameOverrideSet(c) == CommandRes::OK);
    716     CHECK(chara.GetNameOverride() == "bar");
    717 
    718     chara.SetNameOverride("<[playername]>");
    719     SetCmd("chara_0", "", "{[playername]}", "");
    720     CHECK(CommandCharacterNameOverrideSet(c) == CommandRes::OK);
    721     CHECK(chara.GetNameOverride() == "{<[playername]>}");
    722   }
    723 
    724   CommandRes CommandPlayerNameOverrideSet(CommandParam p)
    725   {
    726     auto val = p.ReplaceDouble(p.eval.tmp_str0, p.cmd.GetParam2());
    727     p.gc->GetPlayer().SetNameOverride(std::string{val});
    728 
    729     return CommandRes::OK;
    730   }
    731 
    732   TEST_CASE_FIXTURE(Fixture, "CommandPlayerNameOverrideSet")
    733   {
    734     auto pl = st.GetCharacterColl().At(0);
    735     SetCmd("", "", "bar", "");
    736     CHECK(CommandPlayerNameOverrideSet(c) == CommandRes::OK);
    737     CHECK(pl.GetNameOverride() == "bar");
    738 
    739     pl.SetNameOverride("<[playername]>");
    740     SetCmd("", "", "{[playername]}", "");
    741     CHECK(CommandPlayerNameOverrideSet(c) == CommandRes::OK);
    742     CHECK(pl.GetNameOverride() == "{<<[playername]>>}");
    743   }
    744 
    745   TEST_SUITE_END();
    746 }