og-edopro/gframe/single_mode.cpp
Edoardo Lolletti f07528beca Restucture way duel object is handled
Implement a stateful Duel object wrapping the udnerlying core api automatically managing its lifetime and simplifying calling the various OCG_ functions
2026-08-12 23:49:32 +02:00

559 lines
18 KiB
C++

#include "single_mode.h"
#include "game_config.h"
#include "duelclient.h"
#include "game.h"
#include "core_utils.h"
#include "sound_manager.h"
#include "fmt.h"
#include "localtime.h"
#include "CGUIFileSelectListBox/CGUIFileSelectListBox.h"
#include <IrrlichtDevice.h>
#include <IGUIWindow.h>
#include <IGUIStaticText.h>
#include <IGUIEditBox.h>
#include <IGUIButton.h>
#include <IGUIContextMenu.h>
#include <IGUITabControl.h>
namespace ygo {
DuelPtr SingleMode::pduel = nullptr;
bool SingleMode::is_closing = false;
bool SingleMode::is_continuing = false;
bool SingleMode::is_restarting = false;
Replay SingleMode::last_replay;
Replay SingleMode::new_replay;
ReplayStream SingleMode::replay_stream;
Signal SingleMode::singleSignal;
epro::thread SingleMode::single_mode_thread;
bool SingleMode::StartPlay(DuelOptions&& duelOptions) {
if(mainGame->dInfo.isSingleMode)
return false;
if(single_mode_thread.joinable())
single_mode_thread.join();
single_mode_thread = epro::thread(SinglePlayThread, std::move(duelOptions));
return true;
}
void SingleMode::StopPlay(bool is_exiting) {
is_closing = is_exiting;
is_continuing = false;
is_restarting = false;
mainGame->actionSignal.Set();
if(is_closing) {
singleSignal.SetNoWait(true);
if(single_mode_thread.joinable())
single_mode_thread.join();
} else
singleSignal.Set();
}
void SingleMode::Restart() {
StopPlay();
is_restarting = true;
}
void SingleMode::SetResponse(void* resp, size_t len) {
if(!pduel)
return;
last_replay.Write<uint8_t>(static_cast<uint8_t>(len), false);
last_replay.WriteData(resp, len);
pduel->DuelSetResponse(resp, static_cast<uint32_t>(len));
}
int SingleMode::SinglePlayThread(DuelOptions&& duelOptions) {
Utils::SetThreadName("SinglePlay");
uint64_t opt = duelOptions.duelFlags;
std::string script_name = "";
auto InitReplay = [&]() {
uint16_t buffer[20];
BufferIO::EncodeUTF16(mainGame->dInfo.selfnames[0].data(), buffer, 20);
last_replay.WriteData(buffer, 40, false);
new_replay.WriteData(buffer, 40, false);
BufferIO::EncodeUTF16(mainGame->dInfo.opponames[0].data(), buffer, 20);
last_replay.WriteData(buffer, 40, false);
new_replay.WriteData(buffer, 40, false);
last_replay.Write<uint32_t>(duelOptions.startingLP, false);
last_replay.Write<uint32_t>(duelOptions.startingDrawCount, false);
last_replay.Write<uint32_t>(duelOptions.drawCountPerTurn, false);
last_replay.Write<uint64_t>(opt, false);
last_replay.Write<uint16_t>((uint16_t)script_name.size(), false);
last_replay.WriteData(script_name.data(), script_name.size(), false);
last_replay.Flush();
new_replay.Write<uint64_t>(opt);
};
mainGame->btnLeaveGame->setRelativePosition(mainGame->Resize(205, 5, 295, 45));
is_continuing = false;
is_restarting = false;
auto rnd = Utils::GetRandomNumberGenerator();
restart:
mainGame->dInfo.isSingleMode = true;
OCG_Player team = { duelOptions.startingLP, duelOptions.startingDrawCount, duelOptions.drawCountPerTurn };
bool hand_test = mainGame->dInfo.isHandTest = (duelOptions.scriptName == "hand-test-mode");
if(hand_test)
opt |= DUEL_ATTACK_FIRST_TURN;
const auto seed = Utils::GetRandomNumberGeneratorSeed();
pduel = mainGame->SetupDuel({ { seed[0], seed[1], seed[2], seed[3] }, opt, team, team });
mainGame->dInfo.duel_params = opt;
mainGame->dInfo.duel_field = mainGame->GetMasterRule(mainGame->dInfo.duel_params);
matManager.SetActiveVertices(mainGame->dInfo.HasFieldFlag(DUEL_3_COLUMNS_FIELD),
!mainGame->dInfo.HasFieldFlag(DUEL_SEPARATE_PZONE));
mainGame->dInfo.compat_mode = false;
mainGame->dInfo.legacy_race_size = false;
mainGame->dInfo.startlp = mainGame->dInfo.lp[0] = mainGame->dInfo.lp[1] = duelOptions.startingLP;
mainGame->dInfo.strLP[0] = mainGame->dInfo.strLP[1] = epro::to_wstring(mainGame->dInfo.lp[0]);
mainGame->dInfo.selfnames = { mainGame->ebNickName->getText() };
mainGame->dInfo.opponames = { L"" };
mainGame->dInfo.player_type = 0;
mainGame->dInfo.turn = 0;
bool loaded = true;
bool saveReplay = !hand_test || gGameConfig->saveHandTest;
if(saveReplay) {
auto replay_header = ExtendedReplayHeader::CreateDefaultHeader(REPLAY_YRP1, static_cast<uint32_t>(time(nullptr)));
replay_header.SetSeed(seed);
replay_header.base.flag |= REPLAY_SINGLE_MODE;
if(hand_test)
replay_header.base.flag |= REPLAY_HAND_TEST;
last_replay.BeginRecord(true, Replay::GetReplayFilePath(EPRO_TEXT("_LastReplay.yrp")));
last_replay.WriteHeader(replay_header);
//records the replay with the new system
new_replay.BeginRecord();
replay_header.base.id = REPLAY_YRPX;
new_replay.WriteHeader(replay_header);
replay_stream.clear();
}
if(hand_test) {
script_name = "hand-test-mode";
InitReplay();
Deck playerdeck(mainGame->deckBuilder.GetCurrentDeck());
if ((duelOptions.duelFlags & DUEL_PSEUDO_SHUFFLE) == 0)
std::shuffle(playerdeck.main.begin(), playerdeck.main.end(), rnd);
auto LoadDeck = [&](uint8_t team) {
OCG_NewCardInfo card_info = { team, 0, 0, team, 0, 0, POS_FACEDOWN_DEFENSE };
card_info.loc = LOCATION_DECK;
last_replay.Write<uint32_t>(static_cast<uint32_t>(playerdeck.main.size()), false);
for (int32_t i = (int32_t)playerdeck.main.size() - 1; i >= 0; --i) {
card_info.code = playerdeck.main[i]->code;
pduel->DuelNewCard(&card_info);
last_replay.Write<uint32_t>(playerdeck.main[i]->code, false);
}
card_info.loc = LOCATION_EXTRA;
last_replay.Write<uint32_t>(static_cast<uint32_t>(playerdeck.extra.size()), false);
for (int32_t i = (int32_t)playerdeck.extra.size() - 1; i >= 0; --i) {
card_info.code = playerdeck.extra[i]->code;
pduel->DuelNewCard(&card_info);
last_replay.Write<uint32_t>(playerdeck.extra[i]->code, false);
}
};
LoadDeck(0);
if (duelOptions.handTestNoOpponent) {
last_replay.Write<uint32_t>(0, false);
last_replay.Write<uint32_t>(0, false);
} else {
LoadDeck(1);
}
last_replay.Flush();
const char cmd[] = "Debug.ReloadFieldEnd()";
loaded = pduel->LoadScript(cmd, sizeof(cmd) - 1, " ");
} else {
if(open_file) {
script_name = Utils::ToUTF8IfNeeded(open_file_name);
if(!mainGame->LoadScript(pduel.get(), script_name)) {
script_name = epro::format("./puzzles/{}", script_name);
loaded = mainGame->LoadScript(pduel.get(), script_name);
}
} else {
script_name = duelOptions.scriptName;
loaded = mainGame->LoadScript(pduel.get(), script_name);
}
InitReplay();
}
if(!loaded) {
pduel = nullptr;
mainGame->dInfo.isSingleMode = false;
mainGame->dInfo.isHandTest = false;
open_file = false;
last_replay.EndRecord();
new_replay.EndRecord();
std::unique_lock<epro::mutex> lock(mainGame->gMutex);
if(is_restarting) {
mainGame->dInfo.isInDuel = false;
mainGame->dInfo.isStarted = false;
mainGame->dInfo.isSingleMode = false;
mainGame->dInfo.isHandTest = false;
if(!hand_test) {
mainGame->closeDuelWindow = true;
mainGame->closeDoneSignal.Wait(lock);
}
mainGame->btnLeaveGame->setRelativePosition(mainGame->Resize(205, 5, 295, 80));
if(!hand_test) {
mainGame->ShowElement(mainGame->wSinglePlay);
mainGame->stTip->setVisible(false);
}
mainGame->SetMessageWindow();
mainGame->device->setEventReceiver(&mainGame->menuHandler);
if(hand_test) {
mainGame->btnChainIgnore->setVisible(false);
mainGame->btnChainAlways->setVisible(false);
mainGame->btnChainWhenAvail->setVisible(false);
mainGame->btnCancelOrFinish->setVisible(false);
mainGame->btnShuffle->setVisible(false);
mainGame->wChat->setVisible(false);
mainGame->btnRestartSingle->setVisible(false);
mainGame->wPhase->setVisible(false);
mainGame->deckBuilder.Initialize(false);
}
} else
mainGame->btnLeaveGame->setRelativePosition(mainGame->Resize(205, 5, 295, 80));
is_restarting = false;
return 0;
}
mainGame->gMutex.lock();
if(!hand_test && !is_restarting) {
mainGame->HideElement(mainGame->wSinglePlay);
mainGame->ClearCardInfo();
}
is_restarting = false;
mainGame->mTopMenu->setVisible(false);
mainGame->wCardImg->setVisible(true);
mainGame->wInfos->setVisible(true);
mainGame->btnLeaveGame->setVisible(true);
mainGame->btnLeaveGame->setText(gDataManager->GetSysString(1210).data());
mainGame->btnRestartSingle->setVisible(true);
mainGame->wPhase->setVisible(true);
mainGame->dField.Clear();
mainGame->dInfo.isFirst = true;
mainGame->dInfo.isTeam1 = true;
mainGame->dInfo.isInDuel = true;
mainGame->dInfo.isStarted = true;
mainGame->dInfo.isCatchingUp = false;
mainGame->dInfo.checkRematch = false;
mainGame->SetMessageWindow();
mainGame->device->setEventReceiver(&mainGame->dField);
mainGame->gMutex.unlock();
is_closing = false;
is_continuing = true;
int engFlag = 0;
for(auto& message : CoreUtils::ParseMessages(pduel.get()))
is_continuing = SinglePlayAnalyze(message) && is_continuing;
if(is_continuing) {
pduel->StartDuel();
do {
engFlag = pduel->DuelProcess();
for(auto& message : CoreUtils::ParseMessages(pduel.get())) {
if(message.message == MSG_WIN && hand_test)
continue;
is_continuing = SinglePlayAnalyze(message) && is_continuing;
}
} while(is_continuing && engFlag && mainGame->dInfo.curMsg != MSG_WIN);
}
pduel = nullptr;
if(saveReplay && !is_restarting) {
last_replay.EndRecord(0x1000);
auto oldbuffer = last_replay.GetSerializedBuffer();
CoreUtils::Packet tmp{};
tmp.message = OLD_REPLAY_MODE;
tmp.buffer.swap(oldbuffer);
new_replay.WritePacket(tmp);
new_replay.EndRecord();
}
if(is_closing) {
open_file = false;
is_restarting = false;
mainGame->gMutex.lock();
mainGame->btnLeaveGame->setRelativePosition(mainGame->Resize(205, 5, 295, 80));
mainGame->gMutex.unlock();
return 0;
}
gSoundManager->StopSounds();
bool was_restarting = is_restarting;
if(saveReplay && !was_restarting) {
auto now = std::time(nullptr);
std::unique_lock<epro::mutex> lock(mainGame->gMutex);
mainGame->PopupSaveWindow(gDataManager->GetSysString(1340), epro::format(L"{:%Y-%m-%d %H-%M-%S}", epro::localtime(now)), gDataManager->GetSysString(1342));
mainGame->replaySignal.Wait(lock);
if(mainGame->saveReplay)
new_replay.SaveReplay(Utils::ToPathString(mainGame->ebFileSaveName->getText()));
}
new_replay.Reset();
last_replay.Reset();
mainGame->gMutex.lock();
mainGame->dField.Clear();
mainGame->gMutex.unlock();
if(!is_closing) {
if(was_restarting || hand_test) {
std::lock_guard<epro::mutex> lock(mainGame->gMutex);
for(auto wit = mainGame->fadingList.begin(); wit != mainGame->fadingList.end(); ++wit) {
if(wit->isFadein)
wit->autoFadeoutFrame = 1;
}
mainGame->wACMessage->setVisible(false);
mainGame->wANAttribute->setVisible(false);
mainGame->wANCard->setVisible(false);
mainGame->wANNumber->setVisible(false);
mainGame->wANRace->setVisible(false);
mainGame->wCardSelect->setVisible(false);
mainGame->wCardDisplay->setVisible(false);
mainGame->wCmdMenu->setVisible(false);
mainGame->wMessage->setVisible(false);
mainGame->wOptions->setVisible(false);
mainGame->wPosSelect->setVisible(false);
mainGame->wQuery->setVisible(false);
mainGame->stHintMsg->setVisible(false);
if(was_restarting)
goto restart;
}
std::unique_lock<epro::mutex> lock(mainGame->gMutex);
mainGame->dInfo.isInDuel = false;
mainGame->dInfo.isStarted = false;
mainGame->dInfo.isSingleMode = false;
mainGame->dInfo.isHandTest = false;
if(!hand_test) {
mainGame->closeDuelWindow = true;
mainGame->closeDoneSignal.Wait(lock);
}
mainGame->btnLeaveGame->setRelativePosition(mainGame->Resize(205, 5, 295, 80));
if(!hand_test) {
mainGame->ShowElement(mainGame->wSinglePlay);
mainGame->stTip->setVisible(false);
}
mainGame->SetMessageWindow();
mainGame->device->setEventReceiver(&mainGame->menuHandler);
if(hand_test) {
mainGame->btnChainIgnore->setVisible(false);
mainGame->btnChainAlways->setVisible(false);
mainGame->btnChainWhenAvail->setVisible(false);
mainGame->btnCancelOrFinish->setVisible(false);
mainGame->btnShuffle->setVisible(false);
mainGame->wChat->setVisible(false);
mainGame->btnRestartSingle->setVisible(false);
mainGame->wPhase->setVisible(false);
mainGame->deckBuilder.Initialize(false);
}
}
open_file = false;
return 0;
}
bool SingleMode::SinglePlayAnalyze(CoreUtils::Packet& packet) {
auto Analyze = [&packet]()->bool {
DuelClient::answered = false;
return DuelClient::ClientAnalyze(packet);
};
replay_stream.clear();
if(is_closing || !is_continuing)
return false;
mainGame->dInfo.curMsg = packet.message;
bool record = true;
bool record_last = false;
switch(mainGame->dInfo.curMsg) {
case MSG_RETRY: {
std::unique_lock<epro::mutex> lock(mainGame->gMutex);
mainGame->stMessage->setText(gDataManager->GetSysString(1434).data());
mainGame->PopupElement(mainGame->wMessage);
mainGame->actionSignal.Wait(lock);
return false;
}
case MSG_HINT: {
const auto* pbuf = packet.data();
int type = BufferIO::Read<uint8_t>(pbuf);
auto player = BufferIO::Read<uint8_t>(pbuf);
/*uint64_t data = BufferIO::Read<uint64_t>(pbuf);*/
bool analyze = false;
switch (type) {
case 1:
case 2:
case 3:
case 5: {
analyze = player == 0;
break;
}
case 4:
case 6:
case 7:
case 8:
case 9:
case 11: {
analyze = player != 0;
break;
}
case 10:
case 200:
case 201:
case 202:
case 203: {
analyze = true;
break;
}
}
if(analyze)
Analyze();
if(type > 0 && type < 6 && type != 4)
record = false;
break;
}
case MSG_AI_NAME:
case MSG_SHOW_HINT: {
auto* pbuf = packet.data();
auto len = BufferIO::Read<uint16_t>(pbuf);
if((len + 1u) != packet.buff_size() - (sizeof(uint16_t)))
break;
pbuf[len] = 0;
if(packet.message == MSG_AI_NAME) {
mainGame->dInfo.opponames[0] = BufferIO::DecodeUTF8({ reinterpret_cast<char*>(pbuf), len });
} else {
std::unique_lock<epro::mutex> lock(mainGame->gMutex);
mainGame->stMessage->setText(BufferIO::DecodeUTF8({ reinterpret_cast<char*>(pbuf), len }).data());
mainGame->PopupElement(mainGame->wMessage);
mainGame->actionSignal.Wait(lock);
}
break;
}
case MSG_SELECT_BATTLECMD:
case MSG_SELECT_IDLECMD: {
record = false;
SinglePlayRefresh();
if(!Analyze())
singleSignal.Wait();
break;
}
case MSG_SELECT_EFFECTYN:
case MSG_SELECT_YESNO:
case MSG_SELECT_OPTION:
case MSG_SELECT_CARD:
case MSG_SELECT_TRIBUTE:
case MSG_SELECT_UNSELECT_CARD:
case MSG_SELECT_CHAIN:
case MSG_SELECT_PLACE:
case MSG_SELECT_DISFIELD:
case MSG_SELECT_POSITION:
case MSG_SELECT_COUNTER:
case MSG_SELECT_SUM:
case MSG_SORT_CARD:
case MSG_SORT_CHAIN:
case MSG_ROCK_PAPER_SCISSORS:
case MSG_ANNOUNCE_RACE:
case MSG_ANNOUNCE_ATTRIB:
case MSG_ANNOUNCE_CARD:
case MSG_ANNOUNCE_NUMBER: {
record = false;
if(mainGame->dInfo.curMsg == MSG_SELECT_CHAIN || mainGame->dInfo.curMsg == MSG_NEW_TURN) {
SinglePlayRefresh(0, LOCATION_MZONE);
SinglePlayRefresh(1, LOCATION_MZONE);
SinglePlayRefresh(0, LOCATION_SZONE);
SinglePlayRefresh(1, LOCATION_SZONE);
record_last = true;
}
if(!Analyze())
singleSignal.Wait();
break;
}
default: {
Analyze();
break;
}
}
auto* pbuf = packet.data();
switch(mainGame->dInfo.curMsg) {
case MSG_SHUFFLE_DECK: {
auto player = BufferIO::Read<uint8_t>(pbuf);
SinglePlayRefresh(player, LOCATION_DECK, 0x2181fff);
break;
}
case MSG_SWAP_GRAVE_DECK: {
auto player = BufferIO::Read<uint8_t>(pbuf);
SinglePlayRefresh(player, LOCATION_GRAVE, 0x2181fff);
break;
}
case MSG_REVERSE_DECK: {
SinglePlayRefresh(0, LOCATION_DECK, 0x2181fff);
SinglePlayRefresh(1, LOCATION_DECK, 0x2181fff);
break;
}
case MSG_MOVE: {
pbuf += 4;
auto previous = CoreUtils::ReadLocInfo(pbuf, false);
auto current = CoreUtils::ReadLocInfo(pbuf,false);
if(previous.location && !(current.location & 0x80) && (previous.location != current.location || previous.controler != current.controler))
SinglePlayRefreshSingle(current.controler, current.location, current.sequence);
break;
}
case MSG_TAG_SWAP: {
auto player = BufferIO::Read<uint8_t>(pbuf);
SinglePlayRefresh(player, LOCATION_DECK, 0x181fff);
SinglePlayRefresh(player, LOCATION_EXTRA, 0x181fff);
break;
}
case MSG_NEW_PHASE:
case MSG_SUMMONED:
case MSG_SPSUMMONED:
case MSG_FLIPSUMMONED:
case MSG_CHAINED:
case MSG_CHAIN_SOLVED:
case MSG_DAMAGE_STEP_START:
case MSG_DAMAGE_STEP_END: {
SinglePlayRefresh();
break;
}
case MSG_CHAIN_END: {
SinglePlayRefresh();
SinglePlayRefresh(0, LOCATION_DECK);
SinglePlayRefresh(1, LOCATION_DECK);
break;
}
case MSG_RELOAD_FIELD: {
SinglePlayReload();
std::lock_guard<epro::mutex> lock(mainGame->gMutex);
mainGame->dField.RefreshAllCards();
break;
}
}
if(record)
replay_stream.insert(record_last ? replay_stream.end() : replay_stream.begin(), std::move(packet));
new_replay.WriteStream(replay_stream);
new_replay.Flush();
return is_continuing;
}
void SingleMode::SinglePlayRefresh(uint8_t player, uint8_t location, uint32_t flag) {
std::vector<uint8_t> buffer;
uint32_t len = 0;
OCG_QueryInfo info{ flag, player, location };
auto buff = pduel->DuelQueryLocation(&len, &info);
if(len == 0)
return;
buffer.resize(buffer.size() + len);
memcpy(buffer.data(), buff, len);
mainGame->gMutex.lock();
mainGame->dField.UpdateFieldCard(mainGame->LocalPlayer(player), location, buffer.data());
mainGame->gMutex.unlock();
buffer.insert(buffer.begin(), location);
buffer.insert(buffer.begin(), player);
replay_stream.emplace_back(MSG_UPDATE_DATA, buffer.data(), buffer.size());
}
void SingleMode::SinglePlayRefreshSingle(uint8_t player, uint8_t location, uint8_t sequence, uint32_t flag) {
std::vector<uint8_t> buffer;
uint32_t len = 0;
OCG_QueryInfo info{ flag, player, location, sequence };
auto buff = pduel->DuelQuery(&len, &info);
if(buff == nullptr)
return;
buffer.resize(buffer.size() + len);
memcpy(buffer.data(), buff, len);
mainGame->gMutex.lock();
mainGame->dField.UpdateCard(mainGame->LocalPlayer(player), location, sequence, buffer.data());
mainGame->gMutex.unlock();
buffer.insert(buffer.begin(), sequence);
buffer.insert(buffer.begin(), location);
buffer.insert(buffer.begin(), player);
replay_stream.emplace_back(MSG_UPDATE_CARD, buffer.data(), buffer.size());
}
void SingleMode::SinglePlayRefresh(uint32_t flag) {
for(int p = 0; p < 2; p++)
for(int loc = LOCATION_HAND; loc != LOCATION_GRAVE; loc *= 2)
SinglePlayRefresh(p, loc, flag);
}
void SingleMode::SinglePlayReload() {
for(int p = 0; p < 2; p++)
for(int loc = LOCATION_DECK; loc != LOCATION_OVERLAY; loc *= 2)
SinglePlayRefresh(p, loc, 0x2ffdfff);
}
}