mirror of
https://github.com/dolphin-emu/dolphin.git
synced 2025-10-05 07:39:01 +00:00
rest of the fucking owl
This commit is contained in:
parent
79cf986d36
commit
746ab9586b
32 changed files with 5684 additions and 16 deletions
|
@ -20,18 +20,8 @@ namespace Common
|
|||
|
||||
#define SLIPPI_REV_STR "2.1.1"
|
||||
|
||||
const std::string scm_rev_str = "Faster Melee - Slippi (" SLIPPI_REV_STR ")";
|
||||
const std::string scm_slippi_semver_str = SLIPPI_REV_STR;
|
||||
|
||||
#if !SCM_IS_MASTER
|
||||
"[" SCM_BRANCH_STR "] "
|
||||
#endif
|
||||
|
||||
#ifdef __INTEL_COMPILER
|
||||
BUILD_TYPE_STR SCM_DESC_STR "-ICC";
|
||||
#else
|
||||
BUILD_TYPE_STR SCM_DESC_STR;
|
||||
#endif
|
||||
const std::string scm_rev_str = "Faster Melee - Slippi (" SLIPPI_REV_STR ")" BUILD_TYPE_STR;
|
||||
|
||||
const std::string scm_rev_git_str = SCM_REV_STR;
|
||||
const std::string scm_desc_str = SCM_DESC_STR;
|
||||
|
|
|
@ -463,6 +463,24 @@ add_library(core
|
|||
PowerPC/Interpreter/Interpreter_Paired.cpp
|
||||
PowerPC/Interpreter/Interpreter_SystemRegisters.cpp
|
||||
PowerPC/Interpreter/Interpreter_Tables.cpp
|
||||
Slippi/SlippiGameFileLoader.cpp
|
||||
Slippi/SlippiGameFileLoader.h
|
||||
Slippi/SlippiMatchmaking.cpp
|
||||
Slippi/SlippiMatchmaking.h
|
||||
Slippi/SlippiNetplay.cpp
|
||||
Slippi/SlippiNetplay.h
|
||||
Slippi/SlippiPad.cpp
|
||||
Slippi/SlippiPad.h
|
||||
Slippi/SlippiPlayback.cpp
|
||||
Slippi/SlippiPlayback.h
|
||||
Slippi/SlippiReplayComm.cpp
|
||||
Slippi/SlippiReplayComm.h
|
||||
Slippi/SlippiSavestate.cpp
|
||||
Slippi/SlippiSavestate.h
|
||||
Slippi/SlippiTimer.cpp
|
||||
Slippi/SlippiTimer.h
|
||||
Slippi/SlippiUser.cpp
|
||||
Slippi/SlippiUser.h
|
||||
)
|
||||
|
||||
if(_M_X86)
|
||||
|
@ -542,6 +560,10 @@ elseif(_M_ARM_64)
|
|||
)
|
||||
endif()
|
||||
|
||||
target_include_directories(core PUBLIC
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/../Core
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/../Common
|
||||
)
|
||||
target_link_libraries(core
|
||||
PUBLIC
|
||||
audiocommon
|
||||
|
|
|
@ -15,6 +15,7 @@
|
|||
#include "Core/HW/EXI/EXI_DeviceIPL.h"
|
||||
#include "Core/HW/EXI/EXI_DeviceMemoryCard.h"
|
||||
#include "Core/HW/EXI/EXI_DeviceMic.h"
|
||||
#include "Core/HW/EXI/EXI_DeviceSlippi.h"
|
||||
#include "Core/HW/Memmap.h"
|
||||
|
||||
namespace ExpansionInterface
|
||||
|
@ -148,6 +149,10 @@ std::unique_ptr<IEXIDevice> EXIDevice_Create(const TEXIDevices device_type, cons
|
|||
result = std::make_unique<CEXIAgp>(channel_num);
|
||||
break;
|
||||
|
||||
case EXIDEVICE_SLIPPI:
|
||||
result = std::make_unique<CEXISlippi>(channel_num);
|
||||
break;
|
||||
|
||||
case EXIDEVICE_AM_BASEBOARD:
|
||||
case EXIDEVICE_NONE:
|
||||
default:
|
||||
|
|
2223
Source/Core/Core/HW/EXI/EXI_DeviceSlippi.cpp
Normal file
2223
Source/Core/Core/HW/EXI/EXI_DeviceSlippi.cpp
Normal file
File diff suppressed because it is too large
Load diff
224
Source/Core/Core/HW/EXI/EXI_DeviceSlippi.h
Normal file
224
Source/Core/Core/HW/EXI/EXI_DeviceSlippi.h
Normal file
|
@ -0,0 +1,224 @@
|
|||
// Copyright 2017 Dolphin Emulator Project
|
||||
// Licensed under GPLv2+
|
||||
// Refer to the license.txt file included.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <SlippiGame.h>
|
||||
|
||||
#include "Common/CommonTypes.h"
|
||||
#include "Common/FileUtil.h"
|
||||
#include "EXI_Device.h"
|
||||
#include "Core/Slippi/SlippiGameFileLoader.h"
|
||||
#include "Core/Slippi/SlippiMatchmaking.h"
|
||||
#include "Core/Slippi/SlippiNetplay.h"
|
||||
#include "Core/Slippi/SlippiReplayComm.h"
|
||||
#include "Core/Slippi/SlippiSavestate.h"
|
||||
#include "Core/Slippi/SlippiUser.h"
|
||||
|
||||
#define ROLLBACK_MAX_FRAMES 7
|
||||
#define MAX_NAME_LENGTH 15
|
||||
#define CONNECT_CODE_LENGTH 8
|
||||
|
||||
namespace ExpansionInterface
|
||||
{
|
||||
// Emulated Slippi device used to receive and respond to in-game messages
|
||||
class CEXISlippi : public IEXIDevice
|
||||
{
|
||||
public:
|
||||
CEXISlippi();
|
||||
virtual ~CEXISlippi();
|
||||
|
||||
void DMAWrite(u32 _uAddr, u32 _uSize) override;
|
||||
void DMARead(u32 addr, u32 size) override;
|
||||
|
||||
bool IsPresent() const override;
|
||||
|
||||
private:
|
||||
enum
|
||||
{
|
||||
CMD_UNKNOWN = 0x0,
|
||||
|
||||
// Recording
|
||||
CMD_RECEIVE_COMMANDS = 0x35,
|
||||
CMD_RECEIVE_GAME_INFO = 0x36,
|
||||
CMD_RECEIVE_POST_FRAME_UPDATE = 0x38,
|
||||
CMD_RECEIVE_GAME_END = 0x39,
|
||||
|
||||
// Playback
|
||||
CMD_PREPARE_REPLAY = 0x75,
|
||||
CMD_READ_FRAME = 0x76,
|
||||
CMD_GET_LOCATION = 0x77,
|
||||
CMD_IS_FILE_READY = 0x88,
|
||||
CMD_IS_STOCK_STEAL = 0x89,
|
||||
CMD_GET_GECKO_CODES = 0x8A,
|
||||
|
||||
// Online
|
||||
CMD_ONLINE_INPUTS = 0xB0,
|
||||
CMD_CAPTURE_SAVESTATE = 0xB1,
|
||||
CMD_LOAD_SAVESTATE = 0xB2,
|
||||
CMD_GET_MATCH_STATE = 0xB3,
|
||||
CMD_FIND_OPPONENT = 0xB4,
|
||||
CMD_SET_MATCH_SELECTIONS = 0xB5,
|
||||
CMD_OPEN_LOGIN = 0xB6,
|
||||
CMD_LOGOUT = 0xB7,
|
||||
CMD_UPDATE = 0xB8,
|
||||
CMD_GET_ONLINE_STATUS = 0xB9,
|
||||
CMD_CLEANUP_CONNECTION = 0xBA,
|
||||
|
||||
// Misc
|
||||
CMD_LOG_MESSAGE = 0xD0,
|
||||
CMD_FILE_LENGTH = 0xD1,
|
||||
CMD_FILE_LOAD = 0xD2,
|
||||
};
|
||||
|
||||
enum
|
||||
{
|
||||
FRAME_RESP_WAIT = 0,
|
||||
FRAME_RESP_CONTINUE = 1,
|
||||
FRAME_RESP_TERMINATE = 2,
|
||||
FRAME_RESP_FASTFORWARD = 3,
|
||||
};
|
||||
|
||||
std::unordered_map<u8, u32> payloadSizes = {
|
||||
// The actual size of this command will be sent in one byte
|
||||
// after the command is received. The other receive command IDs
|
||||
// and sizes will be received immediately following
|
||||
{CMD_RECEIVE_COMMANDS, 1},
|
||||
|
||||
// The following are all commands used to play back a replay and
|
||||
// have fixed sizes
|
||||
{CMD_PREPARE_REPLAY, 0},
|
||||
{CMD_READ_FRAME, 4},
|
||||
{CMD_IS_STOCK_STEAL, 5},
|
||||
{CMD_GET_LOCATION, 6},
|
||||
{CMD_IS_FILE_READY, 0},
|
||||
{CMD_GET_GECKO_CODES, 0},
|
||||
|
||||
// The following are used for Slippi online and also have fixed sizes
|
||||
{CMD_ONLINE_INPUTS, 17},
|
||||
{CMD_CAPTURE_SAVESTATE, 32},
|
||||
{CMD_LOAD_SAVESTATE, 32},
|
||||
{CMD_GET_MATCH_STATE, 0},
|
||||
{CMD_FIND_OPPONENT, 19},
|
||||
{CMD_SET_MATCH_SELECTIONS, 6},
|
||||
{CMD_OPEN_LOGIN, 0},
|
||||
{CMD_LOGOUT, 0},
|
||||
{CMD_UPDATE, 0},
|
||||
{CMD_GET_ONLINE_STATUS, 0},
|
||||
{CMD_CLEANUP_CONNECTION, 0},
|
||||
|
||||
// Misc
|
||||
{CMD_LOG_MESSAGE, 0xFFFF}, // Variable size... will only work if by itself
|
||||
{CMD_FILE_LENGTH, 0x40},
|
||||
{CMD_FILE_LOAD, 0x40},
|
||||
};
|
||||
|
||||
struct WriteMessage
|
||||
{
|
||||
std::vector<u8> data;
|
||||
std::string operation;
|
||||
};
|
||||
|
||||
// .slp File creation stuff
|
||||
u32 writtenByteCount = 0;
|
||||
|
||||
// vars for metadata generation
|
||||
time_t gameStartTime;
|
||||
s32 lastFrame;
|
||||
std::unordered_map<u8, std::unordered_map<u8, u32>> characterUsage;
|
||||
|
||||
void updateMetadataFields(u8* payload, u32 length);
|
||||
void configureCommands(u8* payload, u8 length);
|
||||
void writeToFileAsync(u8* payload, u32 length, std::string fileOption);
|
||||
void writeToFile(std::unique_ptr<WriteMessage> msg);
|
||||
std::vector<u8> generateMetadata();
|
||||
void createNewFile();
|
||||
void closeFile();
|
||||
std::string generateFileName();
|
||||
bool checkFrameFullyFetched(s32 frameIndex);
|
||||
bool shouldFFWFrame(s32 frameIndex);
|
||||
|
||||
// std::ofstream log;
|
||||
|
||||
File::IOFile m_file;
|
||||
std::vector<u8> m_payload;
|
||||
|
||||
// online play stuff
|
||||
u16 getRandomStage();
|
||||
void handleOnlineInputs(u8* payload);
|
||||
void prepareOpponentInputs(u8* payload);
|
||||
void handleSendInputs(u8* payload);
|
||||
void handleCaptureSavestate(u8* payload);
|
||||
void handleLoadSavestate(u8* payload);
|
||||
void startFindMatch(u8* payload);
|
||||
void prepareOnlineMatchState();
|
||||
void setMatchSelections(u8* payload);
|
||||
bool shouldSkipOnlineFrame(s32 frame);
|
||||
void handleLogInRequest();
|
||||
void handleLogOutRequest();
|
||||
void handleUpdateAppRequest();
|
||||
void prepareOnlineStatus();
|
||||
void handleConnectionCleanup();
|
||||
|
||||
// replay playback stuff
|
||||
void prepareGameInfo(u8* payload);
|
||||
void prepareGeckoList();
|
||||
void prepareCharacterFrameData(Slippi::FrameData* frame, u8 port, u8 isFollower);
|
||||
void prepareFrameData(u8* payload);
|
||||
void prepareIsStockSteal(u8* payload);
|
||||
void prepareIsFileReady();
|
||||
|
||||
// misc stuff
|
||||
void logMessageFromGame(u8* payload);
|
||||
void prepareFileLength(u8* payload);
|
||||
void prepareFileLoad(u8* payload);
|
||||
|
||||
void FileWriteThread(void);
|
||||
|
||||
Common::FifoQueue<std::unique_ptr<WriteMessage>, false> fileWriteQueue;
|
||||
bool writeThreadRunning = false;
|
||||
std::thread m_fileWriteThread;
|
||||
|
||||
std::unordered_map<u8, std::string> getNetplayNames();
|
||||
|
||||
std::vector<u8> playbackSavestatePayload;
|
||||
std::vector<u8> geckoList;
|
||||
|
||||
u32 stallFrameCount = 0;
|
||||
bool isConnectionStalled = false;
|
||||
|
||||
bool isSoftFFW = false;
|
||||
bool isHardFFW = false;
|
||||
int32_t lastFFWFrame = INT_MIN;
|
||||
std::vector<u8> m_read_queue;
|
||||
std::unique_ptr<Slippi::SlippiGame> m_current_game = nullptr;
|
||||
SlippiMatchmaking::MatchSearchSettings lastSearch;
|
||||
|
||||
u16* lastSelectedStage = nullptr;
|
||||
|
||||
u32 frameSeqIdx = 0;
|
||||
|
||||
bool isEnetInitialized = false;
|
||||
|
||||
std::default_random_engine generator;
|
||||
|
||||
// Frame skipping variables
|
||||
int framesToSkip = 0;
|
||||
bool isCurrentlySkipping = false;
|
||||
|
||||
protected:
|
||||
void TransferByte(u8& byte) override;
|
||||
|
||||
private:
|
||||
SlippiPlayerSelections localSelections;
|
||||
|
||||
std::unique_ptr<SlippiUser> user;
|
||||
std::unique_ptr<SlippiGameFileLoader> gameFileLoader;
|
||||
std::unique_ptr<SlippiNetplayClient> slippi_netplay;
|
||||
std::unique_ptr<SlippiMatchmaking> matchmaking;
|
||||
|
||||
std::map<s32, std::unique_ptr<SlippiSavestate>> activeSavestates;
|
||||
std::deque<std::unique_ptr<SlippiSavestate>> availableSavestates;
|
||||
};
|
||||
}
|
|
@ -24,7 +24,7 @@
|
|||
#include "InputCommon/GCPadStatus.h"
|
||||
|
||||
// clang-format off
|
||||
constexpr std::array<const char*, 138> s_hotkey_labels{{
|
||||
constexpr std::array<const char*, 142> s_hotkey_labels{{
|
||||
_trans("Open"),
|
||||
_trans("Change Disc"),
|
||||
_trans("Eject Disc"),
|
||||
|
@ -191,7 +191,14 @@ constexpr std::array<const char*, 138> s_hotkey_labels{{
|
|||
_trans("Undo Save State"),
|
||||
_trans("Save State"),
|
||||
_trans("Load State"),
|
||||
|
||||
// Slippi Playback
|
||||
_trans("Jump backwards in Slippi replay"),
|
||||
_trans("Pause/unpause Slippi replay"),
|
||||
_trans("Advance one frame in Slippi replay"),
|
||||
_trans("Jump forwards in Slippi replay"),
|
||||
}};
|
||||
|
||||
// clang-format on
|
||||
static_assert(NUM_HOTKEYS == s_hotkey_labels.size(), "Wrong count of hotkey_labels");
|
||||
|
||||
|
@ -347,7 +354,8 @@ constexpr std::array<HotkeyGroupInfo, NUM_HOTKEY_GROUPS> s_groups_info = {
|
|||
{_trans("Save State"), HK_SAVE_STATE_SLOT_1, HK_SAVE_STATE_SLOT_SELECTED},
|
||||
{_trans("Select State"), HK_SELECT_STATE_SLOT_1, HK_SELECT_STATE_SLOT_10},
|
||||
{_trans("Load Last State"), HK_LOAD_LAST_STATE_1, HK_LOAD_LAST_STATE_10},
|
||||
{_trans("Other State Hotkeys"), HK_SAVE_FIRST_STATE, HK_LOAD_STATE_FILE}}};
|
||||
{_trans("Other State Hotkeys"), HK_SAVE_FIRST_STATE, HK_LOAD_STATE_FILE},
|
||||
{_trans("Slippi playback controls"), HK_JUMP_BACK, HK_JUMP_FORWARD} } };
|
||||
|
||||
HotkeyManager::HotkeyManager()
|
||||
{
|
||||
|
|
|
@ -177,6 +177,12 @@ enum Hotkey
|
|||
HK_SAVE_STATE_FILE,
|
||||
HK_LOAD_STATE_FILE,
|
||||
|
||||
// Slippi Playback
|
||||
HK_JUMP_BACK,
|
||||
HK_TOGGLE_PLAY_PAUSE,
|
||||
HK_NEXT_FRAME,
|
||||
HK_JUMP_FORWARD,
|
||||
|
||||
NUM_HOTKEYS,
|
||||
};
|
||||
|
||||
|
@ -205,6 +211,7 @@ enum HotkeyGroup : int
|
|||
HKGP_SELECT_STATE,
|
||||
HKGP_LOAD_LAST_STATE,
|
||||
HKGP_STATE_MISC,
|
||||
HKGP_SLIPPI_PLAYBACK,
|
||||
|
||||
NUM_HOTKEY_GROUPS,
|
||||
};
|
||||
|
|
|
@ -348,9 +348,18 @@ void Interpreter::unknown_instruction(UGeckoInstruction inst)
|
|||
NOTICE_LOG(POWERPC, "r%d: 0x%08x r%d: 0x%08x r%d:0x%08x r%d: 0x%08x", i, rGPR[i], i + 1,
|
||||
rGPR[i + 1], i + 2, rGPR[i + 2], i + 3, rGPR[i + 3]);
|
||||
}
|
||||
ASSERT_MSG(POWERPC, 0,
|
||||
"\nIntCPU: Unknown instruction %08x at PC = %08x last_PC = %08x LR = %08x\n",
|
||||
inst.hex, PC, last_pc, LR);
|
||||
|
||||
std::string msg;
|
||||
|
||||
msg.append(StringFromFormat("\nIntCPU: Unknown instruction %08x at PC = %08x last_PC = %08x LR = %08x\n\n", inst.hex, PC, last_pc, LR));
|
||||
|
||||
std::vector<Dolphin_Debugger::CallstackEntry> callstack;
|
||||
Dolphin_Debugger::GetCallstack(callstack);
|
||||
|
||||
for (auto &it : callstack)
|
||||
msg.append(it.Name);
|
||||
|
||||
ASSERT_MSG(POWERPC, 0, msg.c_str());
|
||||
}
|
||||
|
||||
void Interpreter::ClearCache()
|
||||
|
|
|
@ -7,10 +7,12 @@
|
|||
#include <cstddef>
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
|
||||
#include "Common/BitUtils.h"
|
||||
#include "Common/CommonTypes.h"
|
||||
|
||||
#include "Core/Slippi/SlippiSavestate.h"
|
||||
#include "Core/ConfigManager.h"
|
||||
#include "Core/HW/CPU.h"
|
||||
#include "Core/HW/GPFifo.h"
|
||||
|
@ -18,6 +20,7 @@
|
|||
#include "Core/HW/Memmap.h"
|
||||
#include "Core/PowerPC/JitInterface.h"
|
||||
#include "Core/PowerPC/PowerPC.h"
|
||||
#include "Core/Debugger/Debugger_SymbolMap.h"
|
||||
|
||||
#include "VideoCommon/VideoBackendBase.h"
|
||||
|
||||
|
@ -427,6 +430,269 @@ u32 HostRead_Instruction(const u32 address)
|
|||
return inst.hex;
|
||||
}
|
||||
|
||||
// Taken from Ishii. SLIPPITODO: ask jas
|
||||
//static void Memcheck(u32 address, u32 var, bool write, size_t size)
|
||||
//{
|
||||
//*********************************************************************
|
||||
//* How to test memory sections
|
||||
//*********************************************************************
|
||||
// 1. Uncomment once of the memory analysis blocks below
|
||||
// 2. Start the application (release version is fine)
|
||||
// 3. At a bp somewhere before where you want to start looking for mem access
|
||||
// 4. Once hit, add a MBP in code section (something that will never get hit)
|
||||
// and turn off JIT Core
|
||||
// 5. Start the emulation again and memory accesses should get logged
|
||||
// 6. Make sure you have logging enabled for MI memmap
|
||||
|
||||
//*********************************************************************
|
||||
//* Looking for heap writes?
|
||||
//*********************************************************************
|
||||
// if (!write || size != 4)
|
||||
//{
|
||||
// return;
|
||||
// }
|
||||
|
||||
// static u32 heapStart = 0x80bd5c40;
|
||||
// static u32 heapEnd = 0x811AD5A0;
|
||||
|
||||
// static std::unordered_map<u32, bool> visited;
|
||||
|
||||
// // If we are writting to somewhere in heap, return
|
||||
// if (address >= heapStart && address < heapEnd)
|
||||
// return;
|
||||
|
||||
// // If we are not writting a pointer the somewhere in heap, return
|
||||
// if (var < heapStart || var >= heapEnd)
|
||||
// return;
|
||||
|
||||
// if (visited.count(address))
|
||||
// return;
|
||||
|
||||
// visited[address] = true;
|
||||
// ERROR_LOG(SLIPPI_ONLINE, "%x (%s) %x -> %x", PC, PowerPC::debug_interface.GetDescription(PC).c_str(), address,
|
||||
// var);
|
||||
|
||||
//*********************************************************************
|
||||
//* Looking for camera player position memory
|
||||
//*********************************************************************
|
||||
// static std::unordered_map<u32, bool> visited = {};
|
||||
// static std::unordered_map<std::string, bool> whitelist = {
|
||||
// {"PlayerThink_CameraBehavior", true}, // Per-Player update camera position function
|
||||
// {"CameraFunctionBlrl", true}, // Update camera position
|
||||
//};
|
||||
|
||||
// static std::vector<SlippiSavestate::PreserveBlock> soundStuff = {
|
||||
//
|
||||
//};
|
||||
|
||||
// auto sceneController = ReadFromHardware<FLAG_READ, u32>(0x80479d30);
|
||||
// if ((sceneController & 0xFF0000FF) != 0x08000002)
|
||||
//{
|
||||
// return;
|
||||
//}
|
||||
|
||||
// auto isLoading = ReadFromHardware<FLAG_READ, u32>(0x80479d64);
|
||||
// if (isLoading)
|
||||
//{
|
||||
// return;
|
||||
//}
|
||||
|
||||
// if (!write)
|
||||
//{
|
||||
// return;
|
||||
//}
|
||||
|
||||
// if (address >= 0x804dec00 && address < 0x804eec00)
|
||||
//{
|
||||
// return;
|
||||
//}
|
||||
|
||||
// if (visited.count(address))
|
||||
//{
|
||||
// return;
|
||||
//}
|
||||
|
||||
// visited[address] = true;
|
||||
|
||||
// for (auto it = soundStuff.begin(); it != soundStuff.end(); ++it)
|
||||
//{
|
||||
// if (address >= it->address && address < it->address + it->length)
|
||||
// {
|
||||
// return;
|
||||
// }
|
||||
//}
|
||||
|
||||
// if ((address & 0xFF000000) == 0xcc000000)
|
||||
//{
|
||||
// return;
|
||||
//}
|
||||
|
||||
// std::vector<Dolphin_Debugger::CallstackEntry> callstack;
|
||||
// Dolphin_Debugger::GetCallstack(callstack);
|
||||
|
||||
// bool isFound = false;
|
||||
// for (auto it = callstack.begin(); it != callstack.end(); ++it)
|
||||
//{
|
||||
// std::string func = PowerPC::debug_interface.GetDescription(it->vAddress).c_str();
|
||||
// if (whitelist.count(func))
|
||||
// {
|
||||
// isFound = true;
|
||||
// break;
|
||||
// }
|
||||
//}
|
||||
|
||||
// if (!isFound)
|
||||
//{
|
||||
// return;
|
||||
//}
|
||||
|
||||
// NOTICE_LOG(MEMMAP, "(%s) %x (%s) | %x (%x) <-> %x", write ? "Write" : "Read", PC,
|
||||
// PowerPC::debug_interface.GetDescription(PC).c_str(), var, size, address);
|
||||
|
||||
//*********************************************************************
|
||||
//* Looking for sound memory
|
||||
//*********************************************************************
|
||||
// static std::unordered_map<u32, bool> visited = {};
|
||||
// static std::unordered_map<std::string, bool> whitelist = {
|
||||
// {"__AIDHandler", true}, // lol
|
||||
// {"__AXOutAiCallback", true}, // lol
|
||||
// {"__AXOutNewFrame", true}, // lol
|
||||
// {"__AXSyncPBs", true}, // lol
|
||||
// {"SFX_PlaySFX", true}, // lol
|
||||
// {"__DSPHandler", true},
|
||||
//};
|
||||
|
||||
// static std::vector<SlippiSavestate::PreserveBlock> soundStuff = {
|
||||
// {0x804031A0, 0x24}, // [804031A0 - 804031C4)
|
||||
// {0x80407FB4, 0x34C}, // [80407FB4 - 80408300)
|
||||
// {0x80433C64, 0x1EE80}, // [80433C64 - 80452AE4)
|
||||
// {0x804A8D78, 0x17A68}, // [804A8D78 - 804C07E0)
|
||||
// {0x804C28E0, 0x399C}, // [804C28E0 - 804C627C)
|
||||
// {0x804D7474, 0x8}, // [804D7474 - 804D747C)
|
||||
// {0x804D74F0, 0x50}, // [804D74F0 - 804D7540)
|
||||
// {0x804D7548, 0x4}, // [804D7548 - 804D754C)
|
||||
// {0x804D7558, 0x24}, // [804D7558 - 804D757C)
|
||||
// {0x804D7580, 0xC}, // [804D7580 - 804D758C)
|
||||
// {0x804D759C, 0x4}, // [804D759C - 804D75A0)
|
||||
// {0x804D7720, 0x4}, // [804D7720 - 804D7724)
|
||||
// {0x804D7744, 0x4}, // [804D7744 - 804D7748)
|
||||
// {0x804D774C, 0x8}, // [804D774C - 804D7754)
|
||||
// {0x804D7758, 0x8}, // [804D7758 - 804D7760)
|
||||
// {0x804D7788, 0x10}, // [804D7788 - 804D7798)
|
||||
// {0x804D77C8, 0x4}, // [804D77C8 - 804D77CC)
|
||||
// {0x804D77D0, 0x4}, // [804D77D0 - 804D77D4)
|
||||
// {0x804D77E0, 0x4}, // [804D77E0 - 804D77E4)
|
||||
// {0x804DE358, 0x80}, // [804DE358 - 804DE3D8)
|
||||
// {0x804DE800, 0x70}, // [804DE800 - 804DE870)
|
||||
//};
|
||||
|
||||
// auto sceneController = ReadFromHardware<FLAG_READ, u32>(0x80479d30);
|
||||
// if ((sceneController & 0xFF0000FF) != 0x08000002)
|
||||
//{
|
||||
// return;
|
||||
//}
|
||||
|
||||
// auto isLoading = ReadFromHardware<FLAG_READ, u32>(0x80479d64);
|
||||
// if (isLoading)
|
||||
//{
|
||||
// return;
|
||||
//}
|
||||
|
||||
// if (address >= 0x804dec00)
|
||||
//{
|
||||
// return;
|
||||
//}
|
||||
////if (!write)
|
||||
////{
|
||||
//// return;
|
||||
////}
|
||||
|
||||
// if (visited.count(address))
|
||||
//{
|
||||
// return;
|
||||
//}
|
||||
|
||||
// visited[address] = true;
|
||||
|
||||
// for (auto it = soundStuff.begin(); it != soundStuff.end(); ++it)
|
||||
//{
|
||||
// if (address >= it->address && address < it->address + it->length)
|
||||
// {
|
||||
// return;
|
||||
// }
|
||||
//}
|
||||
|
||||
// if ((address & 0xFF000000) == 0xcc000000)
|
||||
//{
|
||||
// return;
|
||||
//}
|
||||
|
||||
// std::vector<Dolphin_Debugger::CallstackEntry> callstack;
|
||||
// Dolphin_Debugger::GetCallstack(callstack);
|
||||
|
||||
// bool isFound = false;
|
||||
// for (auto it = callstack.begin(); it != callstack.end(); ++it)
|
||||
//{
|
||||
// std::string func = PowerPC::debug_interface.GetDescription(it->vAddress).c_str();
|
||||
// if (whitelist.count(func))
|
||||
// {
|
||||
// isFound = true;
|
||||
// break;
|
||||
// }
|
||||
//}
|
||||
|
||||
// if (!isFound)
|
||||
//{
|
||||
// return;
|
||||
//}
|
||||
|
||||
// NOTICE_LOG(MEMMAP, "(%s) %x (%s) | %x (%x) <-> %x", write ? "Write" : "Read", PC,
|
||||
// PowerPC::debug_interface.GetDescription(PC).c_str(), var, size, address);
|
||||
|
||||
//*********************************************************************
|
||||
//* Detect writes in unknown memory region
|
||||
//*********************************************************************
|
||||
//static std::unordered_map<u32, bool> visited = {};
|
||||
|
||||
//auto sceneController = ReadFromHardware<FLAG_READ, u32>(0x80479d30);
|
||||
//if ((sceneController & 0xFF0000FF) != 0x08000002)
|
||||
//{
|
||||
// return;
|
||||
//}
|
||||
|
||||
//auto isLoading = ReadFromHardware<FLAG_READ, u32>(0x80479d64);
|
||||
//if (isLoading)
|
||||
//{
|
||||
// return;
|
||||
//}
|
||||
|
||||
//if (!write)
|
||||
//{
|
||||
// return;
|
||||
//}
|
||||
|
||||
//// [804fec00 - 80BD5C40)
|
||||
//if (address < 0x8071b000 || address >= 0x80bb0000)
|
||||
//{
|
||||
// return;
|
||||
//}
|
||||
|
||||
//if (visited.count(address))
|
||||
//{
|
||||
// return;
|
||||
//}
|
||||
|
||||
//visited[address] = true;
|
||||
|
||||
//if ((address & 0xFF000000) == 0xcc000000)
|
||||
//{
|
||||
// return;
|
||||
//}
|
||||
|
||||
//NOTICE_LOG(MEMMAP, "(%s) %x (%s) | %x (%x) <-> %x", write ? "Write" : "Read", PC,
|
||||
// PowerPC::debug_interface.GetDescription(PC).c_str(), var, size, address);
|
||||
//}
|
||||
|
||||
static void Memcheck(u32 address, u32 var, bool write, size_t size)
|
||||
{
|
||||
if (PowerPC::memchecks.HasAny())
|
||||
|
|
82
Source/Core/Core/Slippi/SlippiGameFileLoader.cpp
Normal file
82
Source/Core/Core/Slippi/SlippiGameFileLoader.cpp
Normal file
|
@ -0,0 +1,82 @@
|
|||
#include "SlippiGameFileLoader.h"
|
||||
|
||||
#include "Common/Logging/Log.h"
|
||||
#include "Common/FileUtil.h"
|
||||
#include "Core/Boot/Boot.h"
|
||||
#include "Core/Core.h"
|
||||
#include "Core/ConfigManager.h"
|
||||
#include "Core/Common/FileUtil.h"
|
||||
|
||||
std::string getFilePath(std::string fileName)
|
||||
{
|
||||
std::string dirPath = File::GetSysDirectory();
|
||||
std::string filePath = dirPath + "GameFiles/GALE01/" + fileName; // TODO: Handle other games?
|
||||
|
||||
if (File::Exists(filePath))
|
||||
{
|
||||
return filePath;
|
||||
}
|
||||
|
||||
filePath = filePath + ".diff";
|
||||
if (File::Exists(filePath))
|
||||
{
|
||||
return filePath;
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
// SLIPPITODO: Revisit. Modified this function a bit, unsure of functionality
|
||||
void ReadFileToBuffer(std::string& fileName, std::vector<u8>& buf)
|
||||
{
|
||||
// Clear anything that was in the buffer
|
||||
buf.clear();
|
||||
|
||||
// Don't do anything if a game is not running
|
||||
if (Core::GetState() != Core::State::Running)
|
||||
return;
|
||||
|
||||
File::IOFile file(fileName, "rb");
|
||||
auto fileSize = file.GetSize();
|
||||
buf.resize(fileSize);
|
||||
size_t bytes_read;
|
||||
file.ReadArray<u8>(vector->data(), std::min<u64>(file.GetSize(), vector->size()), &bytes_read);
|
||||
}
|
||||
|
||||
u32 SlippiGameFileLoader::LoadFile(std::string fileName, std::string& data)
|
||||
{
|
||||
if (fileCache.count(fileName))
|
||||
{
|
||||
data = fileCache[fileName];
|
||||
return (u32)data.size();
|
||||
}
|
||||
|
||||
INFO_LOG(SLIPPI, "Loading file: %s", fileName.c_str());
|
||||
|
||||
std::string gameFilePath = getFilePath(fileName);
|
||||
if (gameFilePath.empty())
|
||||
{
|
||||
fileCache[fileName] = "";
|
||||
data = "";
|
||||
return 0;
|
||||
}
|
||||
|
||||
std::string fileContents;
|
||||
File::ReadFileToString(gameFilePath, fileContents);
|
||||
|
||||
if (gameFilePath.substr(gameFilePath.length() - 5) == ".diff")
|
||||
{
|
||||
// If the file was a diff file, load the main file from ISO and apply patch
|
||||
std::vector<u8> buf;
|
||||
INFO_LOG(SLIPPI, "Will process diff");
|
||||
ReadFileToBuffer(fileName, buf);
|
||||
std::string diffContents = fileContents;
|
||||
|
||||
decoder.Decode((char*)buf.data(), buf.size(), diffContents, &fileContents);
|
||||
}
|
||||
|
||||
fileCache[fileName] = fileContents;
|
||||
data = fileCache[fileName];
|
||||
INFO_LOG(SLIPPI, "File size: %d", (u32)data.size());
|
||||
return (u32)data.size();
|
||||
}
|
17
Source/Core/Core/Slippi/SlippiGameFileLoader.h
Normal file
17
Source/Core/Core/Slippi/SlippiGameFileLoader.h
Normal file
|
@ -0,0 +1,17 @@
|
|||
#pragma once
|
||||
|
||||
#include "Common/CommonTypes.h"
|
||||
#include <open-vcdiff/src/google/vcdecoder.h>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
class SlippiGameFileLoader
|
||||
{
|
||||
public:
|
||||
u32 LoadFile(std::string fileName, std::string& contents);
|
||||
|
||||
protected:
|
||||
std::unordered_map<std::string, std::string> fileCache;
|
||||
open_vcdiff::VCDiffDecoder decoder;
|
||||
};
|
429
Source/Core/Core/Slippi/SlippiMatchmaking.cpp
Normal file
429
Source/Core/Core/Slippi/SlippiMatchmaking.cpp
Normal file
|
@ -0,0 +1,429 @@
|
|||
#include "SlippiMatchmaking.h"
|
||||
#include "Common/Common.h"
|
||||
#include "Common/ENetUtil.h"
|
||||
#include "Common/Logging/Log.h"
|
||||
#include "Common/StringUtil.h"
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
class MmMessageType
|
||||
{
|
||||
public:
|
||||
static std::string CREATE_TICKET;
|
||||
static std::string CREATE_TICKET_RESP;
|
||||
static std::string GET_TICKET_RESP;
|
||||
};
|
||||
|
||||
std::string MmMessageType::CREATE_TICKET = "create-ticket";
|
||||
std::string MmMessageType::CREATE_TICKET_RESP = "create-ticket-resp";
|
||||
std::string MmMessageType::GET_TICKET_RESP = "get-ticket-resp";
|
||||
|
||||
SlippiMatchmaking::SlippiMatchmaking(SlippiUser* user)
|
||||
{
|
||||
m_user = user;
|
||||
m_state = ProcessState::IDLE;
|
||||
m_errorMsg = "";
|
||||
|
||||
m_client = nullptr;
|
||||
m_server = nullptr;
|
||||
|
||||
MM_HOST = scm_slippi_semver_str.find("dev") == std::string::npos ? MM_HOST_PROD : MM_HOST_DEV;
|
||||
|
||||
generator = std::default_random_engine(Common::Timer::GetTimeMs());
|
||||
}
|
||||
|
||||
SlippiMatchmaking::~SlippiMatchmaking()
|
||||
{
|
||||
m_state = ProcessState::ERROR_ENCOUNTERED;
|
||||
m_errorMsg = "Matchmaking shut down";
|
||||
|
||||
if (m_matchmakeThread.joinable())
|
||||
m_matchmakeThread.join();
|
||||
|
||||
terminateMmConnection();
|
||||
}
|
||||
|
||||
void SlippiMatchmaking::FindMatch(MatchSearchSettings settings)
|
||||
{
|
||||
isMmConnected = false;
|
||||
|
||||
ERROR_LOG(SLIPPI_ONLINE, "[Matchmaking] Starting matchmaking...");
|
||||
|
||||
m_searchSettings = settings;
|
||||
|
||||
m_errorMsg = "";
|
||||
m_state = ProcessState::INITIALIZING;
|
||||
m_matchmakeThread = std::thread(&SlippiMatchmaking::MatchmakeThread, this);
|
||||
}
|
||||
|
||||
SlippiMatchmaking::ProcessState SlippiMatchmaking::GetMatchmakeState()
|
||||
{
|
||||
return m_state;
|
||||
}
|
||||
|
||||
std::string SlippiMatchmaking::GetErrorMessage()
|
||||
{
|
||||
return m_errorMsg;
|
||||
}
|
||||
|
||||
bool SlippiMatchmaking::IsSearching()
|
||||
{
|
||||
return searchingStates.count(m_state) != 0;
|
||||
}
|
||||
|
||||
std::unique_ptr<SlippiNetplayClient> SlippiMatchmaking::GetNetplayClient()
|
||||
{
|
||||
return std::move(m_netplayClient);
|
||||
}
|
||||
|
||||
void SlippiMatchmaking::sendMessage(json msg)
|
||||
{
|
||||
enet_uint32 flags = ENET_PACKET_FLAG_RELIABLE;
|
||||
u8 channelId = 0;
|
||||
|
||||
std::string msgContents = msg.dump();
|
||||
|
||||
ENetPacket* epac = enet_packet_create(msgContents.c_str(), msgContents.length(), flags);
|
||||
enet_peer_send(m_server, channelId, epac);
|
||||
}
|
||||
|
||||
int SlippiMatchmaking::receiveMessage(json& msg, int timeoutMs)
|
||||
{
|
||||
int hostServiceTimeoutMs = 250;
|
||||
|
||||
// Make sure loop runs at least once
|
||||
if (timeoutMs < hostServiceTimeoutMs)
|
||||
timeoutMs = hostServiceTimeoutMs;
|
||||
|
||||
// This is not a perfect way to timeout but hopefully it's close enough?
|
||||
int maxAttempts = timeoutMs / hostServiceTimeoutMs;
|
||||
|
||||
for (int i = 0; i < maxAttempts; i++)
|
||||
{
|
||||
ENetEvent netEvent;
|
||||
int net = enet_host_service(m_client, &netEvent, hostServiceTimeoutMs);
|
||||
if (net <= 0)
|
||||
continue;
|
||||
|
||||
switch (netEvent.type)
|
||||
{
|
||||
case ENET_EVENT_TYPE_RECEIVE:
|
||||
{
|
||||
std::vector<u8> buf;
|
||||
buf.insert(buf.end(), netEvent.packet->data, netEvent.packet->data + netEvent.packet->dataLength);
|
||||
|
||||
std::string str(buf.begin(), buf.end());
|
||||
INFO_LOG(SLIPPI_ONLINE, "[Matchmaking] Received: %s", str.c_str());
|
||||
msg = json::parse(str);
|
||||
|
||||
enet_packet_destroy(netEvent.packet);
|
||||
return 0;
|
||||
}
|
||||
case ENET_EVENT_TYPE_DISCONNECT:
|
||||
// Return -2 code to indicate we have lost connection to the server
|
||||
return -2;
|
||||
}
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
void SlippiMatchmaking::MatchmakeThread()
|
||||
{
|
||||
while (IsSearching())
|
||||
{
|
||||
switch (m_state)
|
||||
{
|
||||
case ProcessState::INITIALIZING:
|
||||
startMatchmaking();
|
||||
break;
|
||||
case ProcessState::MATCHMAKING:
|
||||
handleMatchmaking();
|
||||
break;
|
||||
case ProcessState::OPPONENT_CONNECTING:
|
||||
handleConnecting();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Clean up ENET connections
|
||||
terminateMmConnection();
|
||||
}
|
||||
|
||||
void SlippiMatchmaking::disconnectFromServer()
|
||||
{
|
||||
isMmConnected = false;
|
||||
|
||||
if (m_server)
|
||||
enet_peer_disconnect(m_server, 0);
|
||||
else
|
||||
return;
|
||||
|
||||
ENetEvent netEvent;
|
||||
while (enet_host_service(m_client, &netEvent, 3000) > 0)
|
||||
{
|
||||
switch (netEvent.type)
|
||||
{
|
||||
case ENET_EVENT_TYPE_RECEIVE:
|
||||
enet_packet_destroy(netEvent.packet);
|
||||
break;
|
||||
case ENET_EVENT_TYPE_DISCONNECT:
|
||||
m_server = nullptr;
|
||||
return;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// didn't disconnect gracefully force disconnect
|
||||
enet_peer_reset(m_server);
|
||||
m_server = nullptr;
|
||||
}
|
||||
|
||||
void SlippiMatchmaking::terminateMmConnection()
|
||||
{
|
||||
// Disconnect from server
|
||||
disconnectFromServer();
|
||||
|
||||
// Destroy client
|
||||
if (m_client)
|
||||
{
|
||||
enet_host_destroy(m_client);
|
||||
m_client = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
void SlippiMatchmaking::startMatchmaking()
|
||||
{
|
||||
// I don't understand why I have to do this... if I don't do this, rand always returns the
|
||||
// same value
|
||||
m_client = nullptr;
|
||||
|
||||
int retryCount = 0;
|
||||
while (m_client == nullptr && retryCount < 15)
|
||||
{
|
||||
m_hostPort = 49000 + (generator() % 2000);
|
||||
ERROR_LOG(SLIPPI_ONLINE, "[Matchmaking] Port to use: %d...", m_hostPort);
|
||||
|
||||
// We are explicitly setting the client address because we are trying to utilize our connection
|
||||
// to the matchmaking service in order to hole punch. This port will end up being the port
|
||||
// we listen on when we start our server
|
||||
ENetAddress clientAddr;
|
||||
clientAddr.host = ENET_HOST_ANY;
|
||||
clientAddr.port = m_hostPort;
|
||||
|
||||
m_client = enet_host_create(&clientAddr, 1, 3, 0, 0);
|
||||
retryCount++;
|
||||
}
|
||||
|
||||
if (m_client == nullptr)
|
||||
{
|
||||
// Failed to create client
|
||||
m_state = ProcessState::ERROR_ENCOUNTERED;
|
||||
m_errorMsg = "Failed to create mm client";
|
||||
ERROR_LOG(SLIPPI_ONLINE, "[Matchmaking] Failed to create client...");
|
||||
return;
|
||||
}
|
||||
|
||||
ENetAddress addr;
|
||||
enet_address_set_host(&addr, MM_HOST.c_str());
|
||||
addr.port = MM_PORT;
|
||||
|
||||
m_server = enet_host_connect(m_client, &addr, 3, 0);
|
||||
|
||||
if (m_server == nullptr)
|
||||
{
|
||||
// Failed to connect to server
|
||||
m_state = ProcessState::ERROR_ENCOUNTERED;
|
||||
m_errorMsg = "Failed to start connection to mm server";
|
||||
ERROR_LOG(SLIPPI_ONLINE, "[Matchmaking] Failed to start connection to mm server...");
|
||||
return;
|
||||
}
|
||||
|
||||
// Before we can request a ticket, we must wait for connection to be successful
|
||||
int connectAttemptCount = 0;
|
||||
while (!isMmConnected)
|
||||
{
|
||||
ENetEvent netEvent;
|
||||
int net = enet_host_service(m_client, &netEvent, 500);
|
||||
if (net <= 0 || netEvent.type != ENET_EVENT_TYPE_CONNECT)
|
||||
{
|
||||
// Not yet connected, will retry
|
||||
connectAttemptCount++;
|
||||
if (connectAttemptCount >= 20)
|
||||
{
|
||||
ERROR_LOG(SLIPPI_ONLINE, "[Matchmaking] Failed to connect to mm server...");
|
||||
m_state = ProcessState::ERROR_ENCOUNTERED;
|
||||
m_errorMsg = "Failed to connect to mm server";
|
||||
return;
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
m_client->intercept = ENetUtil::InterceptCallback;
|
||||
isMmConnected = true;
|
||||
ERROR_LOG(SLIPPI_ONLINE, "[Matchmaking] Connected to mm server...");
|
||||
}
|
||||
|
||||
ERROR_LOG(SLIPPI_ONLINE, "[Matchmaking] Trying to find match...");
|
||||
|
||||
if (!m_user->IsLoggedIn())
|
||||
{
|
||||
ERROR_LOG(SLIPPI_ONLINE, "[Matchmaking] Must be logged in to queue");
|
||||
m_state = ProcessState::ERROR_ENCOUNTERED;
|
||||
m_errorMsg = "Must be logged in to queue. Go back to menu";
|
||||
return;
|
||||
}
|
||||
|
||||
auto userInfo = m_user->GetUserInfo();
|
||||
|
||||
std::vector<u8> connectCodeBuf;
|
||||
connectCodeBuf.insert(connectCodeBuf.end(), m_searchSettings.connectCode.begin(),
|
||||
m_searchSettings.connectCode.end());
|
||||
|
||||
// Send message to server to create ticket
|
||||
json request;
|
||||
request["type"] = MmMessageType::CREATE_TICKET;
|
||||
request["user"] = { {"uid", userInfo.uid}, {"playKey", userInfo.playKey} };
|
||||
request["search"] = { {"mode", m_searchSettings.mode}, {"connectCode", connectCodeBuf} };
|
||||
request["appVersion"] = scm_slippi_semver_str;
|
||||
sendMessage(request);
|
||||
|
||||
// Get response from server
|
||||
json response;
|
||||
int rcvRes = receiveMessage(response, 5000);
|
||||
if (rcvRes != 0)
|
||||
{
|
||||
ERROR_LOG(SLIPPI_ONLINE, "[Matchmaking] Did not receive response from server for create ticket");
|
||||
m_state = ProcessState::ERROR_ENCOUNTERED;
|
||||
m_errorMsg = "Failed to join mm queue";
|
||||
return;
|
||||
}
|
||||
|
||||
std::string respType = response["type"];
|
||||
if (respType != MmMessageType::CREATE_TICKET_RESP)
|
||||
{
|
||||
ERROR_LOG(SLIPPI_ONLINE, "[Matchmaking] Received incorrect response for create ticket");
|
||||
ERROR_LOG(SLIPPI_ONLINE, "%s", response.dump().c_str());
|
||||
m_state = ProcessState::ERROR_ENCOUNTERED;
|
||||
m_errorMsg = "Invalid response when joining mm queue";
|
||||
return;
|
||||
}
|
||||
|
||||
std::string err = response.value("error", "");
|
||||
if (err.length() > 0)
|
||||
{
|
||||
ERROR_LOG(SLIPPI_ONLINE, "[Matchmaking] Received error from server for create ticket");
|
||||
m_state = ProcessState::ERROR_ENCOUNTERED;
|
||||
m_errorMsg = err;
|
||||
return;
|
||||
}
|
||||
|
||||
m_state = ProcessState::MATCHMAKING;
|
||||
ERROR_LOG(SLIPPI_ONLINE, "[Matchmaking] Request ticket success");
|
||||
}
|
||||
|
||||
void SlippiMatchmaking::handleMatchmaking()
|
||||
{
|
||||
// Deal with class shut down
|
||||
if (m_state != ProcessState::MATCHMAKING)
|
||||
return;
|
||||
|
||||
// Get response from server
|
||||
json getResp;
|
||||
int rcvRes = receiveMessage(getResp, 2000);
|
||||
if (rcvRes == -1)
|
||||
{
|
||||
INFO_LOG(SLIPPI_ONLINE, "[Matchmaking] Have not yet received assignment");
|
||||
return;
|
||||
}
|
||||
else if (rcvRes != 0)
|
||||
{
|
||||
// Right now the only other code is -2 meaning the server died probably?
|
||||
ERROR_LOG(SLIPPI_ONLINE, "[Matchmaking] Lost connection to the mm server");
|
||||
m_state = ProcessState::ERROR_ENCOUNTERED;
|
||||
m_errorMsg = "Lost connection to the mm server";
|
||||
return;
|
||||
}
|
||||
|
||||
std::string respType = getResp["type"];
|
||||
if (respType != MmMessageType::GET_TICKET_RESP)
|
||||
{
|
||||
ERROR_LOG(SLIPPI_ONLINE, "[Matchmaking] Received incorrect response for get ticket");
|
||||
m_state = ProcessState::ERROR_ENCOUNTERED;
|
||||
m_errorMsg = "Invalid response when getting mm status";
|
||||
return;
|
||||
}
|
||||
|
||||
std::string err = getResp.value("error", "");
|
||||
std::string latestVersion = getResp.value("latestVersion", "");
|
||||
if (err.length() > 0)
|
||||
{
|
||||
if (latestVersion != "")
|
||||
{
|
||||
// Update file to get new version number when the mm server tells us our version is outdated
|
||||
m_user->UpdateFile();
|
||||
m_user->AttemptLogin();
|
||||
m_user->OverwriteLatestVersion(latestVersion); // Force latest version for people whose file updates dont work
|
||||
}
|
||||
|
||||
ERROR_LOG(SLIPPI_ONLINE, "[Matchmaking] Received error from server for get ticket");
|
||||
m_state = ProcessState::ERROR_ENCOUNTERED;
|
||||
m_errorMsg = err;
|
||||
return;
|
||||
}
|
||||
|
||||
m_isSwapAttempt = false;
|
||||
m_netplayClient = nullptr;
|
||||
m_oppIp = getResp.value("oppAddress", "");
|
||||
m_isHost = getResp.value("isHost", false);
|
||||
|
||||
// Disconnect and destroy enet client to mm server
|
||||
terminateMmConnection();
|
||||
|
||||
m_state = ProcessState::OPPONENT_CONNECTING;
|
||||
ERROR_LOG(SLIPPI_ONLINE, "[Matchmaking] Opponent found. isDecider: %s", m_isHost ? "true" : "false");
|
||||
}
|
||||
|
||||
void SlippiMatchmaking::handleConnecting()
|
||||
{
|
||||
std::vector<std::string> ipParts;
|
||||
SplitString(m_oppIp, ':', ipParts);
|
||||
|
||||
// Is host is now used to specify who the decider is
|
||||
auto client = std::make_unique<SlippiNetplayClient>(ipParts[0], std::stoi(ipParts[1]), m_hostPort, m_isHost);
|
||||
|
||||
while (!m_netplayClient)
|
||||
{
|
||||
auto status = client->GetSlippiConnectStatus();
|
||||
if (status == SlippiNetplayClient::SlippiConnectStatus::NET_CONNECT_STATUS_INITIATED)
|
||||
{
|
||||
INFO_LOG(SLIPPI_ONLINE, "[Matchmaking] Connection not yet successful");
|
||||
Common::SleepCurrentThread(500);
|
||||
|
||||
// Deal with class shut down
|
||||
if (m_state != ProcessState::OPPONENT_CONNECTING)
|
||||
return;
|
||||
|
||||
continue;
|
||||
}
|
||||
else if (status != SlippiNetplayClient::SlippiConnectStatus::NET_CONNECT_STATUS_CONNECTED)
|
||||
{
|
||||
ERROR_LOG(SLIPPI_ONLINE, "[Matchmaking] Connection attempt failed, looking for someone else.");
|
||||
|
||||
// Return to the start to get a new ticket to find someone else we can hopefully connect with
|
||||
m_netplayClient = nullptr;
|
||||
m_state = ProcessState::INITIALIZING;
|
||||
return;
|
||||
}
|
||||
|
||||
ERROR_LOG(SLIPPI_ONLINE, "[Matchmaking] Connection success!");
|
||||
|
||||
// Successful connection
|
||||
m_netplayClient = std::move(client);
|
||||
}
|
||||
|
||||
// Connection success, our work is done
|
||||
m_state = ProcessState::CONNECTION_SUCCESS;
|
||||
}
|
98
Source/Core/Core/Slippi/SlippiMatchmaking.h
Normal file
98
Source/Core/Core/Slippi/SlippiMatchmaking.h
Normal file
|
@ -0,0 +1,98 @@
|
|||
#pragma once
|
||||
|
||||
#include "Common/CommonTypes.h"
|
||||
#include "Common/Thread.h"
|
||||
#include "Core/Slippi/SlippiNetplay.h"
|
||||
#include "Core/Slippi/SlippiUser.h"
|
||||
|
||||
#include <enet/enet.h>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
#include <json.hpp>
|
||||
using json = nlohmann::json;
|
||||
|
||||
class SlippiMatchmaking
|
||||
{
|
||||
public:
|
||||
SlippiMatchmaking(SlippiUser* user);
|
||||
~SlippiMatchmaking();
|
||||
|
||||
enum OnlinePlayMode
|
||||
{
|
||||
RANKED = 0,
|
||||
UNRANKED = 1,
|
||||
DIRECT = 2,
|
||||
};
|
||||
|
||||
enum ProcessState
|
||||
{
|
||||
IDLE,
|
||||
INITIALIZING,
|
||||
MATCHMAKING,
|
||||
OPPONENT_CONNECTING,
|
||||
CONNECTION_SUCCESS,
|
||||
ERROR_ENCOUNTERED,
|
||||
};
|
||||
|
||||
struct MatchSearchSettings
|
||||
{
|
||||
OnlinePlayMode mode = OnlinePlayMode::RANKED;
|
||||
std::string connectCode = "";
|
||||
};
|
||||
|
||||
void FindMatch(MatchSearchSettings settings);
|
||||
void MatchmakeThread();
|
||||
ProcessState GetMatchmakeState();
|
||||
bool IsSearching();
|
||||
std::unique_ptr<SlippiNetplayClient> GetNetplayClient();
|
||||
std::string GetErrorMessage();
|
||||
|
||||
protected:
|
||||
const std::string MM_HOST_DEV = "35.197.121.196"; // Dev host
|
||||
const std::string MM_HOST_PROD = "35.247.98.48"; // Production host
|
||||
const u16 MM_PORT = 43113;
|
||||
|
||||
std::string MM_HOST = "";
|
||||
|
||||
ENetHost* m_client;
|
||||
ENetPeer* m_server;
|
||||
|
||||
std::default_random_engine generator;
|
||||
|
||||
bool isMmConnected = false;
|
||||
|
||||
std::thread m_matchmakeThread;
|
||||
|
||||
MatchSearchSettings m_searchSettings;
|
||||
|
||||
ProcessState m_state;
|
||||
std::string m_errorMsg = "";
|
||||
|
||||
SlippiUser* m_user;
|
||||
|
||||
int m_isSwapAttempt = false;
|
||||
|
||||
int m_hostPort;
|
||||
std::string m_oppIp;
|
||||
bool m_isHost;
|
||||
|
||||
std::unique_ptr<SlippiNetplayClient> m_netplayClient;
|
||||
|
||||
const std::unordered_map<ProcessState, bool> searchingStates = {
|
||||
{ProcessState::INITIALIZING, true},
|
||||
{ProcessState::MATCHMAKING, true},
|
||||
{ProcessState::OPPONENT_CONNECTING, true},
|
||||
};
|
||||
|
||||
void disconnectFromServer();
|
||||
void terminateMmConnection();
|
||||
void sendMessage(json msg);
|
||||
int receiveMessage(json& msg, int maxAttempts);
|
||||
|
||||
void sendHolePunchMsg(std::string remoteIp, u16 remotePort, u16 localPort);
|
||||
|
||||
void startMatchmaking();
|
||||
void handleMatchmaking();
|
||||
void handleConnecting();
|
||||
};
|
701
Source/Core/Core/Slippi/SlippiNetplay.cpp
Normal file
701
Source/Core/Core/Slippi/SlippiNetplay.cpp
Normal file
|
@ -0,0 +1,701 @@
|
|||
// Copyright 2010 Dolphin Emulator Project
|
||||
// Licensed under GPLv2+
|
||||
// Refer to the license.txt file included.
|
||||
|
||||
#include "Core/Slippi/SlippiNetplay.h"
|
||||
#include "Common/Common.h"
|
||||
#include "Common/CommonPaths.h"
|
||||
#include "Common/CommonTypes.h"
|
||||
#include "Common/ENetUtil.h"
|
||||
#include "Common/MD5.h"
|
||||
#include "Common/MsgHandler.h"
|
||||
#include "Common/Timer.h"
|
||||
#include "Core/ConfigManager.h"
|
||||
#include "Core/Core.h"
|
||||
#include "Core/HW/EXI_DeviceIPL.h"
|
||||
#include "Core/HW/SI.h"
|
||||
#include "Core/HW/SI_DeviceGCController.h"
|
||||
#include "Core/HW/Sram.h"
|
||||
#include "Core/HW/WiimoteEmu/WiimoteEmu.h"
|
||||
#include "Core/HW/WiimoteReal/WiimoteReal.h"
|
||||
#include "Core/IPC_HLE/WII_IPC_HLE_Device_usb_bt_emu.h"
|
||||
#include "Core/Movie.h"
|
||||
#include "InputCommon/GCAdapter.h"
|
||||
#include "VideoCommon/OnScreenDisplay.h"
|
||||
#include "VideoCommon/VideoConfig.h"
|
||||
#include <SlippiGame.h>
|
||||
#include <algorithm>
|
||||
#include <fstream>
|
||||
#include <mbedtls/md5.h>
|
||||
#include <memory>
|
||||
#include <thread>
|
||||
|
||||
static std::mutex pad_mutex;
|
||||
static std::mutex ack_mutex;
|
||||
|
||||
// called from ---GUI--- thread
|
||||
SlippiNetplayClient::~SlippiNetplayClient()
|
||||
{
|
||||
m_do_loop.Clear();
|
||||
if (m_thread.joinable())
|
||||
m_thread.join();
|
||||
|
||||
if (m_server)
|
||||
{
|
||||
Disconnect();
|
||||
}
|
||||
|
||||
if (g_MainNetHost.get() == m_client)
|
||||
{
|
||||
g_MainNetHost.release();
|
||||
}
|
||||
if (m_client)
|
||||
{
|
||||
enet_host_destroy(m_client);
|
||||
m_client = nullptr;
|
||||
}
|
||||
|
||||
WARN_LOG(SLIPPI_ONLINE, "Netplay client cleanup complete");
|
||||
}
|
||||
|
||||
// called from ---SLIPPI EXI--- thread
|
||||
SlippiNetplayClient::SlippiNetplayClient(const std::string& address, const u16 remotePort, const u16 localPort,
|
||||
bool isDecider)
|
||||
#ifdef _WIN32
|
||||
: m_qos_handle(nullptr)
|
||||
, m_qos_flow_id(0)
|
||||
#endif
|
||||
{
|
||||
WARN_LOG(SLIPPI_ONLINE, "Initializing Slippi Netplay for port: %d, with host: %s", localPort,
|
||||
isDecider ? "true" : "false");
|
||||
|
||||
this->isDecider = isDecider;
|
||||
|
||||
// Local address
|
||||
ENetAddress* localAddr = nullptr;
|
||||
ENetAddress localAddrDef;
|
||||
|
||||
// It is important to be able to set the local port to listen on even in a client connection because
|
||||
// not doing so will break hole punching, the host is expecting traffic to come from a specific ip/port
|
||||
// and if the port does not match what it is expecting, it will not get through the NAT on some routers
|
||||
if (localPort > 0)
|
||||
{
|
||||
INFO_LOG(SLIPPI_ONLINE, "Setting up local address");
|
||||
|
||||
localAddrDef.host = ENET_HOST_ANY;
|
||||
localAddrDef.port = localPort;
|
||||
|
||||
localAddr = &localAddrDef;
|
||||
}
|
||||
|
||||
// TODO: Figure out how to use a local port when not hosting without accepting incoming connections
|
||||
m_client = enet_host_create(localAddr, 2, 3, 0, 0);
|
||||
|
||||
if (m_client == nullptr)
|
||||
{
|
||||
PanicAlertT("Couldn't Create Client");
|
||||
}
|
||||
|
||||
ENetAddress addr;
|
||||
enet_address_set_host(&addr, address.c_str());
|
||||
addr.port = remotePort;
|
||||
|
||||
m_server = enet_host_connect(m_client, &addr, 3, 0);
|
||||
|
||||
if (m_server == nullptr)
|
||||
{
|
||||
PanicAlertT("Couldn't create peer.");
|
||||
}
|
||||
|
||||
slippiConnectStatus = SlippiConnectStatus::NET_CONNECT_STATUS_INITIATED;
|
||||
|
||||
m_thread = std::thread(&SlippiNetplayClient::ThreadFunc, this);
|
||||
}
|
||||
|
||||
// Make a dummy client
|
||||
SlippiNetplayClient::SlippiNetplayClient(bool isDecider)
|
||||
{
|
||||
this->isDecider = isDecider;
|
||||
slippiConnectStatus = SlippiConnectStatus::NET_CONNECT_STATUS_FAILED;
|
||||
}
|
||||
|
||||
// called from ---NETPLAY--- thread
|
||||
unsigned int SlippiNetplayClient::OnData(sf::Packet& packet)
|
||||
{
|
||||
MessageId mid;
|
||||
packet >> mid;
|
||||
|
||||
switch (mid)
|
||||
{
|
||||
case NP_MSG_SLIPPI_PAD:
|
||||
{
|
||||
int32_t frame;
|
||||
packet >> frame;
|
||||
|
||||
// Pad received, try to guess what our local time was when the frame was sent by our opponent
|
||||
// before we initialized
|
||||
// We can compare this to when we sent a pad for last frame to figure out how far/behind we
|
||||
// are with respect to the opponent
|
||||
|
||||
u64 curTime = Common::Timer::GetTimeUs();
|
||||
|
||||
auto timing = lastFrameTiming;
|
||||
if (!hasGameStarted)
|
||||
{
|
||||
// Handle case where opponent starts sending inputs before our game has reached frame 1. This will
|
||||
// continuously say frame 0 is now to prevent opp from getting too far ahead
|
||||
timing.frame = 0;
|
||||
timing.timeUs = curTime;
|
||||
}
|
||||
|
||||
s64 opponentSendTimeUs = curTime - (pingUs / 2);
|
||||
s64 frameDiffOffsetUs = 16683 * (timing.frame - frame);
|
||||
s64 timeOffsetUs = opponentSendTimeUs - timing.timeUs + frameDiffOffsetUs;
|
||||
|
||||
INFO_LOG(SLIPPI_ONLINE, "[Offset] Opp Frame: %d, My Frame: %d. Time offset: %lld", frame, timing.frame,
|
||||
timeOffsetUs);
|
||||
|
||||
// Add this offset to circular buffer for use later
|
||||
if (frameOffsetData.buf.size() < SLIPPI_ONLINE_LOCKSTEP_INTERVAL)
|
||||
frameOffsetData.buf.push_back((s32)timeOffsetUs);
|
||||
else
|
||||
frameOffsetData.buf[frameOffsetData.idx] = (s32)timeOffsetUs;
|
||||
|
||||
frameOffsetData.idx = (frameOffsetData.idx + 1) % SLIPPI_ONLINE_LOCKSTEP_INTERVAL;
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> lk(pad_mutex); // TODO: Is this the correct lock?
|
||||
|
||||
auto packetData = (u8*)packet.getData();
|
||||
|
||||
INFO_LOG(SLIPPI_ONLINE, "Receiving a packet of inputs [%d]...", frame);
|
||||
|
||||
int32_t headFrame = remotePadQueue.empty() ? 0 : remotePadQueue.front()->frame;
|
||||
int inputsToCopy = frame - headFrame;
|
||||
for (int i = inputsToCopy - 1; i >= 0; i--)
|
||||
{
|
||||
auto pad = std::make_unique<SlippiPad>(frame - i, &packetData[5 + i * SLIPPI_PAD_DATA_SIZE]);
|
||||
|
||||
INFO_LOG(SLIPPI_ONLINE, "Rcv [%d] -> %02X %02X %02X %02X %02X %02X %02X %02X", pad->frame,
|
||||
pad->padBuf[0], pad->padBuf[1], pad->padBuf[2], pad->padBuf[3], pad->padBuf[4], pad->padBuf[5],
|
||||
pad->padBuf[6], pad->padBuf[7]);
|
||||
|
||||
remotePadQueue.push_front(std::move(pad));
|
||||
}
|
||||
}
|
||||
|
||||
// Send Ack
|
||||
sf::Packet spac;
|
||||
spac << (MessageId)NP_MSG_SLIPPI_PAD_ACK;
|
||||
spac << frame;
|
||||
Send(spac);
|
||||
}
|
||||
break;
|
||||
|
||||
case NP_MSG_SLIPPI_PAD_ACK:
|
||||
{
|
||||
std::lock_guard<std::mutex> lk(ack_mutex); // Trying to fix rare crash on ackTimers.count
|
||||
|
||||
// Store last frame acked
|
||||
int32_t frame;
|
||||
packet >> frame;
|
||||
|
||||
lastFrameAcked = frame > lastFrameAcked ? frame : lastFrameAcked;
|
||||
|
||||
// Remove old timings
|
||||
while (!ackTimers.Empty() && ackTimers.Front().frame < frame)
|
||||
{
|
||||
ackTimers.Pop();
|
||||
}
|
||||
|
||||
// Don't get a ping if we do not have the right ack frame
|
||||
if (ackTimers.Empty() || ackTimers.Front().frame != frame)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
auto sendTime = ackTimers.Front().timeUs;
|
||||
ackTimers.Pop();
|
||||
|
||||
pingUs = Common::Timer::GetTimeUs() - sendTime;
|
||||
if (g_ActiveConfig.bShowNetPlayPing && frame % SLIPPI_PING_DISPLAY_INTERVAL == 0)
|
||||
{
|
||||
OSD::AddTypedMessage(OSD::MessageType::NetPlayPing, StringFromFormat("Ping: %u", pingUs / 1000),
|
||||
OSD::Duration::NORMAL, OSD::Color::CYAN);
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case NP_MSG_SLIPPI_MATCH_SELECTIONS:
|
||||
{
|
||||
auto s = readSelectionsFromPacket(packet);
|
||||
ERROR_LOG(SLIPPI_ONLINE, "[Received Selections] Char: 0x%X, Color: 0x%X", s->characterId, s->characterId);
|
||||
matchInfo.remotePlayerSelections.Merge(*s);
|
||||
|
||||
// Set player name is not empty
|
||||
if (!matchInfo.remotePlayerSelections.playerName.empty())
|
||||
{
|
||||
oppName = matchInfo.remotePlayerSelections.playerName;
|
||||
}
|
||||
|
||||
// This might be a good place to reset some logic? Game can't start until we receive this msg
|
||||
// so this should ensure that everything is initialized before the game starts
|
||||
// TODO: This could cause issues in the case of a desync? If this is ever received mid-game, bad things
|
||||
// TODO: will happen. Consider improving this
|
||||
hasGameStarted = false;
|
||||
}
|
||||
break;
|
||||
|
||||
case NP_MSG_SLIPPI_CONN_SELECTED:
|
||||
{
|
||||
// Currently this is unused but the intent is to support two-way simultaneous connection attempts
|
||||
isConnectionSelected = true;
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
PanicAlertT("Unknown message received with id : %d", mid);
|
||||
break;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
void SlippiNetplayClient::writeToPacket(sf::Packet& packet, SlippiPlayerSelections& s)
|
||||
{
|
||||
packet << static_cast<MessageId>(NP_MSG_SLIPPI_MATCH_SELECTIONS);
|
||||
packet << s.characterId << s.characterColor << s.isCharacterSelected;
|
||||
packet << s.stageId << s.isStageSelected;
|
||||
packet << s.rngOffset;
|
||||
packet << s.playerName;
|
||||
packet << s.connectCode;
|
||||
}
|
||||
|
||||
std::unique_ptr<SlippiPlayerSelections> SlippiNetplayClient::readSelectionsFromPacket(sf::Packet& packet)
|
||||
{
|
||||
auto s = std::make_unique<SlippiPlayerSelections>();
|
||||
|
||||
packet >> s->characterId;
|
||||
packet >> s->characterColor;
|
||||
packet >> s->isCharacterSelected;
|
||||
|
||||
packet >> s->stageId;
|
||||
packet >> s->isStageSelected;
|
||||
|
||||
packet >> s->rngOffset;
|
||||
packet >> s->playerName;
|
||||
packet >> s->connectCode;
|
||||
|
||||
return std::move(s);
|
||||
}
|
||||
|
||||
void SlippiNetplayClient::Send(sf::Packet& packet)
|
||||
{
|
||||
enet_uint32 flags = ENET_PACKET_FLAG_RELIABLE;
|
||||
u8 channelId = 0;
|
||||
|
||||
MessageId mid = ((u8*)packet.getData())[0];
|
||||
if (mid == NP_MSG_SLIPPI_PAD || mid == NP_MSG_SLIPPI_PAD_ACK)
|
||||
{
|
||||
// Slippi communications do not need reliable connection and do not need to
|
||||
// be received in order. Channel is changed so that other reliable communications
|
||||
// do not block anything. This may not be necessary if order is not maintained?
|
||||
flags = ENET_PACKET_FLAG_UNSEQUENCED;
|
||||
channelId = 1;
|
||||
}
|
||||
|
||||
ENetPacket* epac = enet_packet_create(packet.getData(), packet.getDataSize(), flags);
|
||||
enet_peer_send(m_server, channelId, epac);
|
||||
}
|
||||
|
||||
void SlippiNetplayClient::Disconnect()
|
||||
{
|
||||
ENetEvent netEvent;
|
||||
slippiConnectStatus = SlippiConnectStatus::NET_CONNECT_STATUS_DISCONNECTED;
|
||||
if (m_server)
|
||||
enet_peer_disconnect(m_server, 0);
|
||||
else
|
||||
return;
|
||||
|
||||
while (enet_host_service(m_client, &netEvent, 3000) > 0)
|
||||
{
|
||||
switch (netEvent.type)
|
||||
{
|
||||
case ENET_EVENT_TYPE_RECEIVE:
|
||||
enet_packet_destroy(netEvent.packet);
|
||||
break;
|
||||
case ENET_EVENT_TYPE_DISCONNECT:
|
||||
m_server = nullptr;
|
||||
return;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
// didn't disconnect gracefully force disconnect
|
||||
enet_peer_reset(m_server);
|
||||
m_server = nullptr;
|
||||
}
|
||||
|
||||
void SlippiNetplayClient::SendAsync(std::unique_ptr<sf::Packet> packet)
|
||||
{
|
||||
{
|
||||
std::lock_guard<std::recursive_mutex> lkq(m_crit.async_queue_write);
|
||||
m_async_queue.Push(std::move(packet));
|
||||
}
|
||||
ENetUtil::WakeupThread(m_client);
|
||||
}
|
||||
|
||||
// called from ---NETPLAY--- thread
|
||||
void SlippiNetplayClient::ThreadFunc()
|
||||
{
|
||||
// Let client die 1 second before host such that after a swap, the client won't be connected to
|
||||
int attemptCountLimit = 16;
|
||||
|
||||
int attemptCount = 0;
|
||||
while (slippiConnectStatus == SlippiConnectStatus::NET_CONNECT_STATUS_INITIATED)
|
||||
{
|
||||
// This will confirm that connection went through successfully
|
||||
ENetEvent netEvent;
|
||||
int net = enet_host_service(m_client, &netEvent, 500);
|
||||
if (net > 0 && netEvent.type == ENET_EVENT_TYPE_CONNECT)
|
||||
{
|
||||
// TODO: Confirm gecko codes match?
|
||||
if (netEvent.peer)
|
||||
{
|
||||
WARN_LOG(SLIPPI_ONLINE, "[Netplay] Overwritting server");
|
||||
m_server = netEvent.peer;
|
||||
}
|
||||
|
||||
m_client->intercept = ENetUtil::InterceptCallback;
|
||||
slippiConnectStatus = SlippiConnectStatus::NET_CONNECT_STATUS_CONNECTED;
|
||||
INFO_LOG(SLIPPI_ONLINE, "Slippi online connection successful!");
|
||||
break;
|
||||
}
|
||||
|
||||
WARN_LOG(SLIPPI_ONLINE, "[Netplay] Not yet connected. Res: %d, Type: %d", net, netEvent.type);
|
||||
|
||||
// Time out after enough time has passed
|
||||
attemptCount++;
|
||||
if (attemptCount >= attemptCountLimit || !m_do_loop.IsSet())
|
||||
{
|
||||
slippiConnectStatus = SlippiConnectStatus::NET_CONNECT_STATUS_FAILED;
|
||||
INFO_LOG(SLIPPI_ONLINE, "Slippi online connection failed");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
bool qos_success = false;
|
||||
#ifdef _WIN32
|
||||
QOS_VERSION ver = { 1, 0 };
|
||||
|
||||
if (SConfig::GetInstance().bQoSEnabled && QOSCreateHandle(&ver, &m_qos_handle))
|
||||
{
|
||||
// from win32.c
|
||||
struct sockaddr_in sin = { 0 };
|
||||
|
||||
sin.sin_family = AF_INET;
|
||||
sin.sin_port = ENET_HOST_TO_NET_16(m_server->host->address.port);
|
||||
sin.sin_addr.s_addr = m_server->host->address.host;
|
||||
|
||||
if (QOSAddSocketToFlow(m_qos_handle, m_server->host->socket, reinterpret_cast<PSOCKADDR>(&sin),
|
||||
// this is 0x38
|
||||
QOSTrafficTypeControl, QOS_NON_ADAPTIVE_FLOW, &m_qos_flow_id))
|
||||
{
|
||||
DWORD dscp = 0x2e;
|
||||
|
||||
// this will fail if we're not admin
|
||||
// sets DSCP to the same as linux (0x2e)
|
||||
QOSSetFlow(m_qos_handle, m_qos_flow_id, QOSSetOutgoingDSCPValue, sizeof(DWORD), &dscp, 0, nullptr);
|
||||
|
||||
qos_success = true;
|
||||
}
|
||||
}
|
||||
#else
|
||||
if (SConfig::GetInstance().bQoSEnabled)
|
||||
{
|
||||
#ifdef __linux__
|
||||
// highest priority
|
||||
int priority = 7;
|
||||
setsockopt(m_server->host->socket, SOL_SOCKET, SO_PRIORITY, &priority, sizeof(priority));
|
||||
#endif
|
||||
|
||||
// https://www.tucny.com/Home/dscp-tos
|
||||
// ef is better than cs7
|
||||
int tos_val = 0xb8;
|
||||
qos_success = setsockopt(m_server->host->socket, IPPROTO_IP, IP_TOS, &tos_val, sizeof(tos_val)) == 0;
|
||||
}
|
||||
#endif
|
||||
|
||||
while (m_do_loop.IsSet())
|
||||
{
|
||||
ENetEvent netEvent;
|
||||
int net;
|
||||
net = enet_host_service(m_client, &netEvent, 250);
|
||||
while (!m_async_queue.Empty())
|
||||
{
|
||||
Send(*(m_async_queue.Front().get()));
|
||||
m_async_queue.Pop();
|
||||
}
|
||||
if (net > 0)
|
||||
{
|
||||
sf::Packet rpac;
|
||||
switch (netEvent.type)
|
||||
{
|
||||
case ENET_EVENT_TYPE_RECEIVE:
|
||||
rpac.append(netEvent.packet->data, netEvent.packet->dataLength);
|
||||
OnData(rpac);
|
||||
|
||||
enet_packet_destroy(netEvent.packet);
|
||||
break;
|
||||
case ENET_EVENT_TYPE_DISCONNECT:
|
||||
ERROR_LOG(SLIPPI_ONLINE, "[Netplay] Disconnected Event detected: %s",
|
||||
netEvent.peer == m_server ? "same client" : "diff client");
|
||||
|
||||
// If the disconnect event doesn't come from the client we are actually listening to,
|
||||
// it can be safely ignored
|
||||
if (netEvent.peer == m_server)
|
||||
{
|
||||
m_do_loop.Clear(); // Stop the loop, will trigger a disconnect
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef _WIN32
|
||||
if (m_qos_handle != 0)
|
||||
{
|
||||
if (m_qos_flow_id != 0)
|
||||
QOSRemoveSocketFromFlow(m_qos_handle, m_server->host->socket, m_qos_flow_id, 0);
|
||||
QOSCloseHandle(m_qos_handle);
|
||||
}
|
||||
#endif
|
||||
|
||||
Disconnect();
|
||||
return;
|
||||
}
|
||||
|
||||
bool SlippiNetplayClient::IsDecider()
|
||||
{
|
||||
return isDecider;
|
||||
}
|
||||
|
||||
bool SlippiNetplayClient::IsConnectionSelected()
|
||||
{
|
||||
return isConnectionSelected;
|
||||
}
|
||||
|
||||
SlippiNetplayClient::SlippiConnectStatus SlippiNetplayClient::GetSlippiConnectStatus()
|
||||
{
|
||||
return slippiConnectStatus;
|
||||
}
|
||||
|
||||
void SlippiNetplayClient::StartSlippiGame()
|
||||
{
|
||||
// Reset variables to start a new game
|
||||
lastFrameAcked = 0;
|
||||
|
||||
FrameTiming timing;
|
||||
timing.frame = 0;
|
||||
timing.timeUs = Common::Timer::GetTimeUs();
|
||||
lastFrameTiming = timing;
|
||||
hasGameStarted = false;
|
||||
|
||||
localPadQueue.clear();
|
||||
|
||||
remotePadQueue.clear();
|
||||
for (s32 i = 1; i <= 2; i++)
|
||||
{
|
||||
std::unique_ptr<SlippiPad> pad = std::make_unique<SlippiPad>(i);
|
||||
remotePadQueue.push_front(std::move(pad));
|
||||
}
|
||||
|
||||
// Reset match info for next game
|
||||
matchInfo.Reset();
|
||||
|
||||
// Reset ack timers
|
||||
ackTimers.Clear();
|
||||
}
|
||||
|
||||
void SlippiNetplayClient::SendConnectionSelected()
|
||||
{
|
||||
isConnectionSelected = true;
|
||||
|
||||
auto spac = std::make_unique<sf::Packet>();
|
||||
*spac << static_cast<MessageId>(NP_MSG_SLIPPI_CONN_SELECTED);
|
||||
SendAsync(std::move(spac));
|
||||
}
|
||||
|
||||
void SlippiNetplayClient::SendSlippiPad(std::unique_ptr<SlippiPad> pad)
|
||||
{
|
||||
auto status = slippiConnectStatus;
|
||||
bool connectionFailed = status == SlippiNetplayClient::SlippiConnectStatus::NET_CONNECT_STATUS_FAILED;
|
||||
bool connectionDisconnected = status == SlippiNetplayClient::SlippiConnectStatus::NET_CONNECT_STATUS_DISCONNECTED;
|
||||
if (connectionFailed || connectionDisconnected)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// if (pad && isDecider)
|
||||
//{
|
||||
// ERROR_LOG(SLIPPI_ONLINE, "[%d] %X %X %X %X %X %X %X %X", pad->frame, pad->padBuf[0], pad->padBuf[1],
|
||||
// pad->padBuf[2], pad->padBuf[3], pad->padBuf[4], pad->padBuf[5], pad->padBuf[6], pad->padBuf[7]);
|
||||
//}
|
||||
|
||||
if (pad)
|
||||
{
|
||||
// Add latest local pad report to queue
|
||||
localPadQueue.push_front(std::move(pad));
|
||||
}
|
||||
|
||||
// Remove pad reports that have been received and acked
|
||||
while (!localPadQueue.empty() && localPadQueue.back()->frame < lastFrameAcked)
|
||||
{
|
||||
localPadQueue.pop_back();
|
||||
}
|
||||
|
||||
if (localPadQueue.empty())
|
||||
{
|
||||
// If pad queue is empty now, there's no reason to send anything
|
||||
return;
|
||||
}
|
||||
|
||||
auto frame = localPadQueue.front()->frame;
|
||||
|
||||
auto spac = std::make_unique<sf::Packet>();
|
||||
*spac << static_cast<MessageId>(NP_MSG_SLIPPI_PAD);
|
||||
*spac << frame;
|
||||
|
||||
INFO_LOG(SLIPPI_ONLINE, "Sending a packet of inputs [%d]...", frame);
|
||||
for (auto it = localPadQueue.begin(); it != localPadQueue.end(); ++it)
|
||||
{
|
||||
INFO_LOG(SLIPPI_ONLINE, "Send [%d] -> %02X %02X %02X %02X %02X %02X %02X %02X", (*it)->frame, (*it)->padBuf[0],
|
||||
(*it)->padBuf[1], (*it)->padBuf[2], (*it)->padBuf[3], (*it)->padBuf[4], (*it)->padBuf[5],
|
||||
(*it)->padBuf[6], (*it)->padBuf[7]);
|
||||
spac->append((*it)->padBuf, SLIPPI_PAD_DATA_SIZE); // only transfer 8 bytes per pad
|
||||
}
|
||||
|
||||
SendAsync(std::move(spac));
|
||||
|
||||
u64 time = Common::Timer::GetTimeUs();
|
||||
|
||||
hasGameStarted = true;
|
||||
|
||||
FrameTiming timing;
|
||||
timing.frame = frame;
|
||||
timing.timeUs = time;
|
||||
lastFrameTiming = timing;
|
||||
|
||||
// Add send time to ack timers
|
||||
FrameTiming sendTime;
|
||||
sendTime.frame = frame;
|
||||
sendTime.timeUs = time;
|
||||
ackTimers.Push(sendTime);
|
||||
}
|
||||
|
||||
void SlippiNetplayClient::SetMatchSelections(SlippiPlayerSelections& s)
|
||||
{
|
||||
matchInfo.localPlayerSelections.Merge(s);
|
||||
|
||||
// Send packet containing selections
|
||||
auto spac = std::make_unique<sf::Packet>();
|
||||
writeToPacket(*spac, matchInfo.localPlayerSelections);
|
||||
SendAsync(std::move(spac));
|
||||
}
|
||||
|
||||
std::unique_ptr<SlippiRemotePadOutput> SlippiNetplayClient::GetSlippiRemotePad(int32_t curFrame)
|
||||
{
|
||||
std::lock_guard<std::mutex> lk(pad_mutex); // TODO: Is this the correct lock?
|
||||
|
||||
std::unique_ptr<SlippiRemotePadOutput> padOutput = std::make_unique<SlippiRemotePadOutput>();
|
||||
|
||||
if (remotePadQueue.empty())
|
||||
{
|
||||
auto emptyPad = std::make_unique<SlippiPad>(0);
|
||||
|
||||
padOutput->latestFrame = emptyPad->frame;
|
||||
|
||||
auto emptyIt = std::begin(emptyPad->padBuf);
|
||||
padOutput->data.insert(padOutput->data.end(), emptyIt, emptyIt + SLIPPI_PAD_FULL_SIZE);
|
||||
|
||||
return std::move(padOutput);
|
||||
}
|
||||
|
||||
padOutput->latestFrame = remotePadQueue.front()->frame;
|
||||
|
||||
// Copy the entire remaining remote buffer
|
||||
for (auto it = remotePadQueue.begin(); it != remotePadQueue.end(); ++it)
|
||||
{
|
||||
auto padIt = std::begin((*it)->padBuf);
|
||||
padOutput->data.insert(padOutput->data.end(), padIt, padIt + SLIPPI_PAD_FULL_SIZE);
|
||||
}
|
||||
|
||||
// Remove pad reports that should no longer be needed
|
||||
while (remotePadQueue.size() > 1 && remotePadQueue.back()->frame < curFrame)
|
||||
{
|
||||
remotePadQueue.pop_back();
|
||||
}
|
||||
|
||||
return std::move(padOutput);
|
||||
}
|
||||
|
||||
SlippiMatchInfo* SlippiNetplayClient::GetMatchInfo()
|
||||
{
|
||||
return &matchInfo;
|
||||
}
|
||||
|
||||
u64 SlippiNetplayClient::GetSlippiPing()
|
||||
{
|
||||
return pingUs;
|
||||
}
|
||||
|
||||
std::string SlippiNetplayClient::GetOpponentName()
|
||||
{
|
||||
return oppName;
|
||||
}
|
||||
|
||||
int32_t SlippiNetplayClient::GetSlippiLatestRemoteFrame()
|
||||
{
|
||||
std::lock_guard<std::mutex> lk(pad_mutex); // TODO: Is this the correct lock?
|
||||
|
||||
if (remotePadQueue.empty())
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
return remotePadQueue.front()->frame;
|
||||
}
|
||||
|
||||
s32 SlippiNetplayClient::CalcTimeOffsetUs()
|
||||
{
|
||||
if (frameOffsetData.buf.empty())
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
std::vector<s32> buf;
|
||||
std::copy(frameOffsetData.buf.begin(), frameOffsetData.buf.end(), std::back_inserter(buf));
|
||||
|
||||
// TODO: Does this work?
|
||||
std::sort(buf.begin(), buf.end());
|
||||
|
||||
int bufSize = (int)buf.size();
|
||||
int offset = (int)((1.0f / 3.0f) * bufSize);
|
||||
int end = bufSize - offset;
|
||||
|
||||
int sum = 0;
|
||||
for (int i = offset; i < end; i++)
|
||||
{
|
||||
sum += buf[i];
|
||||
}
|
||||
|
||||
int count = end - offset;
|
||||
if (count <= 0)
|
||||
{
|
||||
return 0; // What do I return here?
|
||||
}
|
||||
|
||||
return sum / count;
|
||||
}
|
203
Source/Core/Core/Slippi/SlippiNetplay.h
Normal file
203
Source/Core/Core/Slippi/SlippiNetplay.h
Normal file
|
@ -0,0 +1,203 @@
|
|||
// Copyright 2010 Dolphin Emulator Project
|
||||
// Licensed under GPLv2+
|
||||
// Refer to the license.txt file included.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "Common/CommonTypes.h"
|
||||
#include "Common/Event.h"
|
||||
#include "Common/FifoQueue.h"
|
||||
#include "Common/Timer.h"
|
||||
#include "Common/TraversalClient.h"
|
||||
#include "Core/NetPlayProto.h"
|
||||
#include "Core/Slippi/SlippiPad.h"
|
||||
#include "InputCommon/GCPadStatus.h"
|
||||
#include <SFML/Network/Packet.hpp>
|
||||
#include <array>
|
||||
#include <deque>
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
|
||||
#ifdef _WIN32
|
||||
#include <Qos2.h>
|
||||
#endif
|
||||
|
||||
#define SLIPPI_ONLINE_LOCKSTEP_INTERVAL 30 // Number of frames to wait before attempting to time-sync
|
||||
#define SLIPPI_PING_DISPLAY_INTERVAL 60
|
||||
|
||||
struct SlippiRemotePadOutput
|
||||
{
|
||||
int32_t latestFrame;
|
||||
std::vector<u8> data;
|
||||
};
|
||||
|
||||
class SlippiPlayerSelections
|
||||
{
|
||||
public:
|
||||
u8 characterId = 0;
|
||||
u8 characterColor = 0;
|
||||
bool isCharacterSelected = false;
|
||||
|
||||
u16 stageId = 0;
|
||||
bool isStageSelected = false;
|
||||
|
||||
u32 rngOffset = 0;
|
||||
|
||||
std::string playerName = "";
|
||||
std::string connectCode = "";
|
||||
|
||||
void Merge(SlippiPlayerSelections& s)
|
||||
{
|
||||
this->rngOffset = s.rngOffset;
|
||||
this->playerName = s.playerName;
|
||||
this->connectCode = s.connectCode;
|
||||
|
||||
if (s.isStageSelected)
|
||||
{
|
||||
this->stageId = s.stageId;
|
||||
this->isStageSelected = true;
|
||||
}
|
||||
|
||||
if (s.isCharacterSelected)
|
||||
{
|
||||
this->characterId = s.characterId;
|
||||
this->characterColor = s.characterColor;
|
||||
this->isCharacterSelected = true;
|
||||
}
|
||||
}
|
||||
|
||||
void Reset()
|
||||
{
|
||||
characterId = 0;
|
||||
characterColor = 0;
|
||||
isCharacterSelected = false;
|
||||
|
||||
stageId = 0;
|
||||
isStageSelected = false;
|
||||
|
||||
rngOffset = 0;
|
||||
playerName.clear();
|
||||
}
|
||||
};
|
||||
|
||||
class SlippiMatchInfo
|
||||
{
|
||||
public:
|
||||
SlippiPlayerSelections localPlayerSelections;
|
||||
SlippiPlayerSelections remotePlayerSelections;
|
||||
|
||||
void Reset()
|
||||
{
|
||||
localPlayerSelections.Reset();
|
||||
remotePlayerSelections.Reset();
|
||||
}
|
||||
};
|
||||
|
||||
class SlippiNetplayClient
|
||||
{
|
||||
public:
|
||||
void ThreadFunc();
|
||||
void SendAsync(std::unique_ptr<sf::Packet> packet);
|
||||
|
||||
SlippiNetplayClient(bool isDecider); // Make a dummy client
|
||||
SlippiNetplayClient(const std::string& address, const u16 remotePort, const u16 localPort, bool isDecider);
|
||||
~SlippiNetplayClient();
|
||||
|
||||
// Slippi Online
|
||||
enum class SlippiConnectStatus
|
||||
{
|
||||
NET_CONNECT_STATUS_UNSET,
|
||||
NET_CONNECT_STATUS_INITIATED,
|
||||
NET_CONNECT_STATUS_CONNECTED,
|
||||
NET_CONNECT_STATUS_FAILED,
|
||||
NET_CONNECT_STATUS_DISCONNECTED,
|
||||
};
|
||||
|
||||
bool IsDecider();
|
||||
bool IsConnectionSelected();
|
||||
SlippiConnectStatus GetSlippiConnectStatus();
|
||||
void StartSlippiGame();
|
||||
void SendConnectionSelected();
|
||||
void SendSlippiPad(std::unique_ptr<SlippiPad> pad);
|
||||
void SetMatchSelections(SlippiPlayerSelections& s);
|
||||
std::unique_ptr<SlippiRemotePadOutput> GetSlippiRemotePad(int32_t curFrame);
|
||||
SlippiMatchInfo* GetMatchInfo();
|
||||
u64 GetSlippiPing();
|
||||
std::string GetOpponentName();
|
||||
int32_t GetSlippiLatestRemoteFrame();
|
||||
s32 CalcTimeOffsetUs();
|
||||
|
||||
protected:
|
||||
struct
|
||||
{
|
||||
std::recursive_mutex game;
|
||||
// lock order
|
||||
std::recursive_mutex players;
|
||||
std::recursive_mutex async_queue_write;
|
||||
} m_crit;
|
||||
|
||||
Common::FifoQueue<std::unique_ptr<sf::Packet>, false> m_async_queue;
|
||||
|
||||
std::string oppName = "";
|
||||
|
||||
ENetHost* m_client = nullptr;
|
||||
ENetPeer* m_server = nullptr;
|
||||
std::thread m_thread;
|
||||
|
||||
std::string m_selected_game;
|
||||
Common::Flag m_is_running{ false };
|
||||
Common::Flag m_do_loop{ true };
|
||||
|
||||
unsigned int m_minimum_buffer_size = 6;
|
||||
|
||||
u32 m_current_game = 0;
|
||||
|
||||
// Slippi Stuff
|
||||
struct FrameTiming
|
||||
{
|
||||
int32_t frame;
|
||||
u64 timeUs;
|
||||
};
|
||||
|
||||
struct
|
||||
{
|
||||
// TODO: Should the buffer size be dynamic based on time sync interval or not?
|
||||
int idx;
|
||||
std::vector<s32> buf;
|
||||
} frameOffsetData;
|
||||
|
||||
bool isConnectionSelected = false;
|
||||
bool isDecider = false;
|
||||
int32_t lastFrameAcked;
|
||||
bool hasGameStarted = false;
|
||||
FrameTiming lastFrameTiming;
|
||||
u64 pingUs;
|
||||
std::deque<std::unique_ptr<SlippiPad>> localPadQueue; // most recent inputs at start of deque
|
||||
std::deque<std::unique_ptr<SlippiPad>> remotePadQueue; // most recent inputs at start of deque
|
||||
Common::FifoQueue<FrameTiming, false> ackTimers;
|
||||
SlippiConnectStatus slippiConnectStatus = SlippiConnectStatus::NET_CONNECT_STATUS_UNSET;
|
||||
SlippiMatchInfo matchInfo;
|
||||
|
||||
bool m_is_recording = false;
|
||||
|
||||
void writeToPacket(sf::Packet& packet, SlippiPlayerSelections& s);
|
||||
std::unique_ptr<SlippiPlayerSelections> readSelectionsFromPacket(sf::Packet& packet);
|
||||
|
||||
private:
|
||||
unsigned int OnData(sf::Packet& packet);
|
||||
void Send(sf::Packet& packet);
|
||||
void Disconnect();
|
||||
|
||||
bool m_is_connected = false;
|
||||
|
||||
#ifdef _WIN32
|
||||
HANDLE m_qos_handle;
|
||||
QOS_FLOWID m_qos_flow_id;
|
||||
#endif
|
||||
|
||||
u32 m_timebase_frame = 0;
|
||||
};
|
21
Source/Core/Core/Slippi/SlippiPad.cpp
Normal file
21
Source/Core/Core/Slippi/SlippiPad.cpp
Normal file
|
@ -0,0 +1,21 @@
|
|||
#include "SlippiPad.h"
|
||||
|
||||
// TODO: Confirm the default and padding values are right
|
||||
static u8 emptyPad[SLIPPI_PAD_FULL_SIZE] = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 };
|
||||
|
||||
SlippiPad::SlippiPad(int32_t frame)
|
||||
{
|
||||
this->frame = frame;
|
||||
memcpy(this->padBuf, emptyPad, SLIPPI_PAD_FULL_SIZE);
|
||||
}
|
||||
|
||||
SlippiPad::SlippiPad(int32_t frame, u8* padBuf) : SlippiPad(frame)
|
||||
{
|
||||
// Overwrite the data portion of the pad
|
||||
memcpy(this->padBuf, padBuf, SLIPPI_PAD_DATA_SIZE);
|
||||
}
|
||||
|
||||
SlippiPad::~SlippiPad()
|
||||
{
|
||||
// Do nothing?
|
||||
}
|
18
Source/Core/Core/Slippi/SlippiPad.h
Normal file
18
Source/Core/Core/Slippi/SlippiPad.h
Normal file
|
@ -0,0 +1,18 @@
|
|||
#pragma once
|
||||
|
||||
#include "Common/CommonTypes.h"
|
||||
|
||||
#define SLIPPI_PAD_FULL_SIZE 0xC
|
||||
#define SLIPPI_PAD_DATA_SIZE 0x8
|
||||
|
||||
class SlippiPad
|
||||
{
|
||||
public:
|
||||
SlippiPad(int32_t frame);
|
||||
SlippiPad(int32_t frame, u8* padBuf);
|
||||
~SlippiPad();
|
||||
|
||||
int32_t frame;
|
||||
u8 padBuf[SLIPPI_PAD_FULL_SIZE];
|
||||
};
|
||||
|
285
Source/Core/Core/Slippi/SlippiPlayback.cpp
Normal file
285
Source/Core/Core/Slippi/SlippiPlayback.cpp
Normal file
|
@ -0,0 +1,285 @@
|
|||
#include <memory>
|
||||
#include <mutex>
|
||||
|
||||
#ifdef _WIN32
|
||||
#include <share.h>
|
||||
#endif
|
||||
|
||||
#include "Common/Logging/Log.h"
|
||||
#include "Core/Core.h"
|
||||
#include "Core/HW/EXI_DeviceSlippi.h"
|
||||
#include "Core/NetPlayClient.h"
|
||||
#include "Core/State.h"
|
||||
#include "SlippiPlayback.h"
|
||||
|
||||
#define FRAME_INTERVAL 900
|
||||
#define SLEEP_TIME_MS 8
|
||||
|
||||
std::unique_ptr<SlippiPlaybackStatus> g_playbackStatus;
|
||||
extern std::unique_ptr<SlippiReplayComm> g_replayComm;
|
||||
|
||||
static std::mutex mtx;
|
||||
static std::mutex seekMtx;
|
||||
static std::mutex diffMtx;
|
||||
static std::unique_lock<std::mutex> processingLock(diffMtx);
|
||||
static std::condition_variable condVar;
|
||||
static std::condition_variable cv_waitingForTargetFrame;
|
||||
static std::condition_variable cv_processingDiff;
|
||||
static std::atomic<int> numDiffsProcessing(0);
|
||||
|
||||
s32 emod(s32 a, s32 b)
|
||||
{
|
||||
assert(b != 0);
|
||||
int r = a % b;
|
||||
return r >= 0 ? r : r + std::abs(b);
|
||||
}
|
||||
|
||||
std::string processDiff(std::vector<u8> iState, std::vector<u8> cState)
|
||||
{
|
||||
INFO_LOG(SLIPPI, "Processing diff");
|
||||
numDiffsProcessing += 1;
|
||||
cv_processingDiff.notify_one();
|
||||
std::string diff = std::string();
|
||||
open_vcdiff::VCDiffEncoder encoder((char*)iState.data(), iState.size());
|
||||
encoder.Encode((char*)cState.data(), cState.size(), &diff);
|
||||
|
||||
INFO_LOG(SLIPPI, "done processing");
|
||||
numDiffsProcessing -= 1;
|
||||
cv_processingDiff.notify_one();
|
||||
return diff;
|
||||
}
|
||||
|
||||
SlippiPlaybackStatus::SlippiPlaybackStatus()
|
||||
{
|
||||
shouldJumpBack = false;
|
||||
shouldJumpForward = false;
|
||||
inSlippiPlayback = false;
|
||||
shouldRunThreads = false;
|
||||
isHardFFW = false;
|
||||
isSoftFFW = false;
|
||||
lastFFWFrame = INT_MIN;
|
||||
currentPlaybackFrame = INT_MIN;
|
||||
targetFrameNum = INT_MAX;
|
||||
latestFrame = Slippi::GAME_FIRST_FRAME;
|
||||
}
|
||||
|
||||
void SlippiPlaybackStatus::startThreads()
|
||||
{
|
||||
shouldRunThreads = true;
|
||||
m_savestateThread = std::thread(&SlippiPlaybackStatus::SavestateThread, this);
|
||||
m_seekThread = std::thread(&SlippiPlaybackStatus::SeekThread, this);
|
||||
}
|
||||
|
||||
void SlippiPlaybackStatus::prepareSlippiPlayback(s32& frameIndex)
|
||||
{
|
||||
// block if there's too many diffs being processed
|
||||
while (shouldRunThreads && numDiffsProcessing > 3)
|
||||
{
|
||||
INFO_LOG(SLIPPI, "Processing too many diffs, blocking main process");
|
||||
cv_processingDiff.wait(processingLock);
|
||||
}
|
||||
|
||||
// Unblock thread to save a state every interval
|
||||
if (shouldRunThreads && ((currentPlaybackFrame + 122) % FRAME_INTERVAL == 0))
|
||||
condVar.notify_one();
|
||||
|
||||
// TODO: figure out why sometimes playback frame increments past targetFrameNum
|
||||
if (inSlippiPlayback && frameIndex >= targetFrameNum)
|
||||
{
|
||||
if (targetFrameNum < currentPlaybackFrame)
|
||||
{
|
||||
// Since playback logic only goes up in currentPlaybackFrame now due to handling rollback
|
||||
// playback, we need to rewind the currentPlaybackFrame here instead such that the playback
|
||||
// cursor will show up in the correct place
|
||||
currentPlaybackFrame = targetFrameNum;
|
||||
}
|
||||
|
||||
if (currentPlaybackFrame > targetFrameNum)
|
||||
{
|
||||
INFO_LOG(SLIPPI, "Reached frame %d. Target was %d. Unblocking", currentPlaybackFrame,
|
||||
targetFrameNum);
|
||||
}
|
||||
cv_waitingForTargetFrame.notify_one();
|
||||
}
|
||||
}
|
||||
|
||||
void SlippiPlaybackStatus::resetPlayback()
|
||||
{
|
||||
if (shouldRunThreads)
|
||||
{
|
||||
shouldRunThreads = false;
|
||||
|
||||
if (m_savestateThread.joinable())
|
||||
m_savestateThread.detach();
|
||||
|
||||
if (m_seekThread.joinable())
|
||||
m_seekThread.detach();
|
||||
|
||||
condVar.notify_one(); // Will allow thread to kill itself
|
||||
futureDiffs.clear();
|
||||
futureDiffs.rehash(0);
|
||||
}
|
||||
|
||||
shouldJumpBack = false;
|
||||
shouldJumpForward = false;
|
||||
isHardFFW = false;
|
||||
isSoftFFW = false;
|
||||
targetFrameNum = INT_MAX;
|
||||
inSlippiPlayback = false;
|
||||
}
|
||||
|
||||
void SlippiPlaybackStatus::processInitialState(std::vector<u8>& iState)
|
||||
{
|
||||
INFO_LOG(SLIPPI, "saving iState");
|
||||
State::SaveToBuffer(iState);
|
||||
SConfig::GetInstance().bHideCursor = false;
|
||||
};
|
||||
|
||||
void SlippiPlaybackStatus::SavestateThread()
|
||||
{
|
||||
Common::SetCurrentThreadName("Savestate thread");
|
||||
std::unique_lock<std::mutex> intervalLock(mtx);
|
||||
|
||||
INFO_LOG(SLIPPI, "Entering savestate thread");
|
||||
|
||||
while (shouldRunThreads)
|
||||
{
|
||||
// Wait to hit one of the intervals
|
||||
// Possible while rewinding that we hit this wait again.
|
||||
while (shouldRunThreads && (currentPlaybackFrame - Slippi::PLAYBACK_FIRST_SAVE) % FRAME_INTERVAL != 0)
|
||||
condVar.wait(intervalLock);
|
||||
|
||||
if (!shouldRunThreads)
|
||||
break;
|
||||
|
||||
s32 fixedFrameNumber = currentPlaybackFrame;
|
||||
if (fixedFrameNumber == INT_MAX)
|
||||
continue;
|
||||
|
||||
bool isStartFrame = fixedFrameNumber == Slippi::PLAYBACK_FIRST_SAVE;
|
||||
bool hasStateBeenProcessed = futureDiffs.count(fixedFrameNumber) > 0;
|
||||
|
||||
if (!inSlippiPlayback && isStartFrame)
|
||||
{
|
||||
processInitialState(iState);
|
||||
inSlippiPlayback = true;
|
||||
}
|
||||
else if (!hasStateBeenProcessed && !isStartFrame)
|
||||
{
|
||||
INFO_LOG(SLIPPI, "saving diff at frame: %d", fixedFrameNumber);
|
||||
State::SaveToBuffer(cState);
|
||||
|
||||
futureDiffs[fixedFrameNumber] = std::async(processDiff, iState, cState);
|
||||
}
|
||||
Common::SleepCurrentThread(SLEEP_TIME_MS);
|
||||
}
|
||||
|
||||
INFO_LOG(SLIPPI, "Exiting savestate thread");
|
||||
}
|
||||
|
||||
void SlippiPlaybackStatus::SeekThread()
|
||||
{
|
||||
Common::SetCurrentThreadName("Seek thread");
|
||||
std::unique_lock<std::mutex> seekLock(seekMtx);
|
||||
|
||||
INFO_LOG(SLIPPI, "Entering seek thread");
|
||||
|
||||
while (shouldRunThreads)
|
||||
{
|
||||
bool shouldSeek = inSlippiPlayback && (shouldJumpBack || shouldJumpForward || targetFrameNum != INT_MAX);
|
||||
|
||||
if (shouldSeek)
|
||||
{
|
||||
auto replayCommSettings = g_replayComm->getSettings();
|
||||
if (replayCommSettings.mode == "queue")
|
||||
clearWatchSettingsStartEnd();
|
||||
|
||||
bool paused = (Core::GetState() == Core::CORE_PAUSE);
|
||||
Core::SetState(Core::CORE_PAUSE);
|
||||
|
||||
u32 jumpInterval = 300; // 5 seconds;
|
||||
|
||||
if (shouldJumpForward)
|
||||
targetFrameNum = currentPlaybackFrame + jumpInterval;
|
||||
|
||||
if (shouldJumpBack)
|
||||
targetFrameNum = currentPlaybackFrame - jumpInterval;
|
||||
|
||||
// Handle edgecases for trying to seek before start or past end of game
|
||||
if (targetFrameNum < Slippi::PLAYBACK_FIRST_SAVE)
|
||||
targetFrameNum = Slippi::PLAYBACK_FIRST_SAVE;
|
||||
|
||||
if (targetFrameNum > latestFrame)
|
||||
{
|
||||
targetFrameNum = latestFrame;
|
||||
}
|
||||
|
||||
s32 closestStateFrame = targetFrameNum - emod(targetFrameNum - Slippi::PLAYBACK_FIRST_SAVE, FRAME_INTERVAL);
|
||||
|
||||
bool isLoadingStateOptimal =
|
||||
targetFrameNum < currentPlaybackFrame || closestStateFrame > currentPlaybackFrame;
|
||||
|
||||
if (isLoadingStateOptimal)
|
||||
{
|
||||
if (closestStateFrame <= Slippi::PLAYBACK_FIRST_SAVE)
|
||||
{
|
||||
State::LoadFromBuffer(iState);
|
||||
}
|
||||
else
|
||||
{
|
||||
// If this diff has been processed, load it
|
||||
if (futureDiffs.count(closestStateFrame) > 0)
|
||||
{
|
||||
std::string stateString;
|
||||
decoder.Decode((char*)iState.data(), iState.size(), futureDiffs[closestStateFrame].get(),
|
||||
&stateString);
|
||||
std::vector<u8> stateToLoad(stateString.begin(), stateString.end());
|
||||
State::LoadFromBuffer(stateToLoad);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Fastforward until we get to the frame we want
|
||||
if (targetFrameNum != closestStateFrame && targetFrameNum != latestFrame)
|
||||
{
|
||||
isHardFFW = true;
|
||||
SConfig::GetInstance().m_OCEnable = true;
|
||||
SConfig::GetInstance().m_OCFactor = 4.0f;
|
||||
|
||||
Core::SetState(Core::State::Running);
|
||||
cv_waitingForTargetFrame.wait(seekLock);
|
||||
Core::SetState(Core::State::Paused);
|
||||
|
||||
SConfig::GetInstance().m_OCFactor = 1.0f;
|
||||
SConfig::GetInstance().m_OCEnable = false;
|
||||
isHardFFW = false;
|
||||
}
|
||||
|
||||
if (!paused)
|
||||
Core::SetState(Core::State::Running);
|
||||
|
||||
shouldJumpBack = false;
|
||||
shouldJumpForward = false;
|
||||
targetFrameNum = INT_MAX;
|
||||
}
|
||||
|
||||
Common::SleepCurrentThread(SLEEP_TIME_MS);
|
||||
}
|
||||
|
||||
INFO_LOG(SLIPPI, "Exit seek thread");
|
||||
}
|
||||
|
||||
void SlippiPlaybackStatus::clearWatchSettingsStartEnd()
|
||||
{
|
||||
int startFrame = g_replayComm->current.startFrame;
|
||||
int endFrame = g_replayComm->current.endFrame;
|
||||
if (startFrame != Slippi::GAME_FIRST_FRAME || endFrame != INT_MAX)
|
||||
{
|
||||
if (g_playbackStatus->targetFrameNum < startFrame)
|
||||
g_replayComm->current.startFrame = g_playbackStatus->targetFrameNum;
|
||||
if (g_playbackStatus->targetFrameNum > endFrame)
|
||||
g_replayComm->current.endFrame = INT_MAX;
|
||||
}
|
||||
}
|
||||
|
||||
SlippiPlaybackStatus::~SlippiPlaybackStatus() {}
|
50
Source/Core/Core/Slippi/SlippiPlayback.h
Normal file
50
Source/Core/Core/Slippi/SlippiPlayback.h
Normal file
|
@ -0,0 +1,50 @@
|
|||
#pragma once
|
||||
|
||||
#include <climits>
|
||||
#include <future>
|
||||
#include <open-vcdiff/src/google/vcdecoder.h>
|
||||
#include <open-vcdiff/src/google/vcencoder.h>
|
||||
#include <SlippiLib/SlippiGame.h>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
#include "../../Common/CommonTypes.h"
|
||||
|
||||
class SlippiPlaybackStatus
|
||||
{
|
||||
public:
|
||||
SlippiPlaybackStatus();
|
||||
virtual ~SlippiPlaybackStatus();
|
||||
|
||||
bool shouldJumpBack = false;
|
||||
bool shouldJumpForward = false;
|
||||
bool inSlippiPlayback = false;
|
||||
volatile bool shouldRunThreads = false;
|
||||
bool isHardFFW = false;
|
||||
bool isSoftFFW = false;
|
||||
s32 lastFFWFrame = INT_MIN;
|
||||
s32 currentPlaybackFrame = INT_MIN;
|
||||
s32 targetFrameNum = INT_MAX;
|
||||
s32 latestFrame = Slippi::GAME_FIRST_FRAME;
|
||||
|
||||
std::thread m_savestateThread;
|
||||
std::thread m_seekThread;
|
||||
|
||||
void startThreads(void);
|
||||
void resetPlayback(void);
|
||||
void prepareSlippiPlayback(s32& frameIndex);
|
||||
|
||||
private:
|
||||
void SavestateThread(void);
|
||||
void SeekThread(void);
|
||||
void processInitialState(std::vector<u8>& iState);
|
||||
void clearWatchSettingsStartEnd();
|
||||
|
||||
std::unordered_map<int32_t, std::shared_future<std::string>>
|
||||
futureDiffs; // State diffs keyed by frameIndex, processed async
|
||||
std::vector<u8> iState; // The initial state
|
||||
std::vector<u8> cState; // The current (latest) state
|
||||
|
||||
open_vcdiff::VCDiffDecoder decoder;
|
||||
open_vcdiff::VCDiffEncoder* encoder = NULL;
|
||||
};
|
221
Source/Core/Core/Slippi/SlippiReplayComm.cpp
Normal file
221
Source/Core/Core/Slippi/SlippiReplayComm.cpp
Normal file
|
@ -0,0 +1,221 @@
|
|||
#include <cctype>
|
||||
#include <memory>
|
||||
#include "SlippiReplayComm.h"
|
||||
#include "Common/CommonPaths.h"
|
||||
#include "Common/FileUtil.h"
|
||||
#include "Common/Logging/LogManager.h"
|
||||
#include "Core/ConfigManager.h"
|
||||
|
||||
std::unique_ptr<SlippiReplayComm> g_replayComm;
|
||||
|
||||
// https://stackoverflow.com/questions/216823/whats-the-best-way-to-trim-stdstring
|
||||
// trim from start (in place)
|
||||
static inline void ltrim(std::string& s)
|
||||
{
|
||||
s.erase(s.begin(), std::find_if(s.begin(), s.end(), [](int ch) { return !std::isspace(ch); }));
|
||||
}
|
||||
|
||||
// trim from end (in place)
|
||||
static inline void rtrim(std::string& s)
|
||||
{
|
||||
s.erase(std::find_if(s.rbegin(), s.rend(), [](int ch) { return !std::isspace(ch); }).base(), s.end());
|
||||
}
|
||||
|
||||
// trim from both ends (in place)
|
||||
static inline void trim(std::string& s)
|
||||
{
|
||||
ltrim(s);
|
||||
rtrim(s);
|
||||
}
|
||||
|
||||
SlippiReplayComm::SlippiReplayComm()
|
||||
{
|
||||
INFO_LOG(EXPANSIONINTERFACE, "SlippiReplayComm: Using playback config path: %s",
|
||||
SConfig::GetInstance().m_strSlippiInput.c_str());
|
||||
configFilePath = SConfig::GetInstance().m_strSlippiInput.c_str();
|
||||
}
|
||||
|
||||
SlippiReplayComm::~SlippiReplayComm() {}
|
||||
|
||||
SlippiReplayComm::CommSettings SlippiReplayComm::getSettings()
|
||||
{
|
||||
return commFileSettings;
|
||||
}
|
||||
|
||||
std::string SlippiReplayComm::getReplayPath()
|
||||
{
|
||||
std::string replayFilePath = commFileSettings.replayPath;
|
||||
if (commFileSettings.mode == "queue")
|
||||
{
|
||||
// If we are in queue mode, let's grab the replay from the queue instead
|
||||
replayFilePath = commFileSettings.queue.empty() ? "" : commFileSettings.queue.front().path;
|
||||
}
|
||||
|
||||
return replayFilePath;
|
||||
}
|
||||
|
||||
bool SlippiReplayComm::isNewReplay()
|
||||
{
|
||||
loadFile();
|
||||
std::string replayFilePath = getReplayPath();
|
||||
|
||||
bool hasPathChanged = replayFilePath != previousReplayLoaded;
|
||||
bool isReplay = !!replayFilePath.length();
|
||||
|
||||
// The previous check is mostly good enough but it does not
|
||||
// work if someone tries to load the same replay twice in a row
|
||||
// the commandId was added to deal with this
|
||||
bool hasCommandChanged = commFileSettings.commandId != previousCommandId;
|
||||
|
||||
// This checks if the queue index has changed, this is to fix the
|
||||
// issue where the same replay showing up twice in a row in a
|
||||
// queue would never cause this function to return true
|
||||
bool hasQueueIdxChanged = false;
|
||||
if (commFileSettings.mode == "queue" && !commFileSettings.queue.empty())
|
||||
{
|
||||
hasQueueIdxChanged = commFileSettings.queue.front().index != previousIndex;
|
||||
}
|
||||
|
||||
bool isNewReplay = hasPathChanged || hasCommandChanged || hasQueueIdxChanged;
|
||||
|
||||
return isReplay && isNewReplay;
|
||||
}
|
||||
|
||||
void SlippiReplayComm::nextReplay()
|
||||
{
|
||||
if (commFileSettings.queue.empty())
|
||||
return;
|
||||
|
||||
// Increment queue position
|
||||
commFileSettings.queue.pop();
|
||||
}
|
||||
|
||||
std::unique_ptr<Slippi::SlippiGame> SlippiReplayComm::loadGame()
|
||||
{
|
||||
auto replayFilePath = getReplayPath();
|
||||
INFO_LOG(EXPANSIONINTERFACE, "Attempting to load replay file %s", replayFilePath.c_str());
|
||||
auto result = Slippi::SlippiGame::FromFile(replayFilePath);
|
||||
if (result)
|
||||
{
|
||||
// If we successfully loaded a SlippiGame, indicate as such so
|
||||
// that this game won't be considered new anymore. If the replay
|
||||
// file did not exist yet, result will be falsy, which will keep
|
||||
// the replay considered new so that the file will attempt to be
|
||||
// loaded again
|
||||
previousReplayLoaded = replayFilePath;
|
||||
previousCommandId = commFileSettings.commandId;
|
||||
if (commFileSettings.mode == "queue" && !commFileSettings.queue.empty())
|
||||
{
|
||||
previousIndex = commFileSettings.queue.front().index;
|
||||
}
|
||||
|
||||
WatchSettings ws;
|
||||
ws.path = replayFilePath;
|
||||
ws.startFrame = commFileSettings.startFrame;
|
||||
ws.endFrame = commFileSettings.endFrame;
|
||||
if (commFileSettings.mode == "queue")
|
||||
{
|
||||
ws = commFileSettings.queue.front();
|
||||
}
|
||||
|
||||
if (commFileSettings.outputOverlayFiles)
|
||||
{
|
||||
std::string dirpath = File::GetExeDirectory();
|
||||
File::WriteStringToFile(ws.gameStation, dirpath + DIR_SEP + "Slippi/out-station.txt");
|
||||
File::WriteStringToFile(ws.gameStartAt, dirpath + DIR_SEP + "Slippi/out-time.txt");
|
||||
}
|
||||
|
||||
current = ws;
|
||||
}
|
||||
|
||||
return std::move(result);
|
||||
}
|
||||
|
||||
void SlippiReplayComm::loadFile()
|
||||
{
|
||||
// TODO: Consider even only checking file mod time every 250 ms or something? Not sure
|
||||
// TODO: what the perf impact is atm
|
||||
|
||||
u64 modTime = File::GetFileModTime(configFilePath);
|
||||
if (modTime != 0 && modTime == configLastLoadModTime)
|
||||
{
|
||||
// TODO: Maybe be smarter than just using mod time? Look for other things that would
|
||||
// TODO: indicate that file has changed and needs to be reloaded?
|
||||
return;
|
||||
}
|
||||
|
||||
WARN_LOG(EXPANSIONINTERFACE, "File change detected in comm file: %s", configFilePath.c_str());
|
||||
configLastLoadModTime = modTime;
|
||||
|
||||
// TODO: Maybe load file in a more intelligent way to save
|
||||
// TODO: file operations
|
||||
std::string commFileContents;
|
||||
File::ReadFileToString(configFilePath, commFileContents);
|
||||
|
||||
auto res = json::parse(commFileContents, nullptr, false);
|
||||
if (res.is_discarded() || !res.is_object())
|
||||
{
|
||||
// Happens if there is a parse error, I think?
|
||||
commFileSettings.mode = "normal";
|
||||
commFileSettings.replayPath = "";
|
||||
commFileSettings.startFrame = Slippi::GAME_FIRST_FRAME;
|
||||
commFileSettings.endFrame = INT_MAX;
|
||||
commFileSettings.commandId = "";
|
||||
commFileSettings.outputOverlayFiles = false;
|
||||
commFileSettings.isRealTimeMode = false;
|
||||
commFileSettings.rollbackDisplayMethod = "off";
|
||||
|
||||
if (res.is_string())
|
||||
{
|
||||
// If we have a string, let's use that as the replayPath
|
||||
// This is really only here because when developing it might be easier
|
||||
// to just throw in a string instead of an object
|
||||
|
||||
commFileSettings.replayPath = res.get<std::string>();
|
||||
}
|
||||
else
|
||||
{
|
||||
WARN_LOG(EXPANSIONINTERFACE, "Comm file load error detected. Check file format");
|
||||
|
||||
// Reset in the case of read error. this fixes a race condition where file mod time changes but
|
||||
// the file is not readable yet?
|
||||
configLastLoadModTime = 0;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: Support file with only path string
|
||||
commFileSettings.mode = res.value("mode", "normal");
|
||||
commFileSettings.replayPath = res.value("replay", "");
|
||||
commFileSettings.startFrame = res.value("startFrame", Slippi::GAME_FIRST_FRAME);
|
||||
commFileSettings.endFrame = res.value("endFrame", INT_MAX);
|
||||
commFileSettings.commandId = res.value("commandId", "");
|
||||
commFileSettings.outputOverlayFiles = res.value("outputOverlayFiles", false);
|
||||
commFileSettings.isRealTimeMode = res.value("isRealTimeMode", false);
|
||||
commFileSettings.rollbackDisplayMethod = res.value("rollbackDisplayMethod", "off");
|
||||
|
||||
if (isFirstLoad)
|
||||
{
|
||||
auto queue = res["queue"];
|
||||
if (queue.is_array())
|
||||
{
|
||||
int index = 0;
|
||||
for (json::iterator it = queue.begin(); it != queue.end(); ++it)
|
||||
{
|
||||
json el = *it;
|
||||
WatchSettings w = {};
|
||||
w.path = el.value("path", "");
|
||||
w.startFrame = el.value("startFrame", Slippi::GAME_FIRST_FRAME);
|
||||
w.endFrame = el.value("endFrame", INT_MAX);
|
||||
w.gameStartAt = el.value("gameStartAt", "");
|
||||
w.gameStation = el.value("gameStation", "");
|
||||
w.index = index++;
|
||||
|
||||
commFileSettings.queue.push(w);
|
||||
};
|
||||
}
|
||||
|
||||
isFirstLoad = false;
|
||||
}
|
||||
}
|
65
Source/Core/Core/Slippi/SlippiReplayComm.h
Normal file
65
Source/Core/Core/Slippi/SlippiReplayComm.h
Normal file
|
@ -0,0 +1,65 @@
|
|||
#pragma once
|
||||
|
||||
#include <SlippiGame.h>
|
||||
#include <queue>
|
||||
#include <string>
|
||||
|
||||
#include <json.hpp>
|
||||
using json = nlohmann::json;
|
||||
|
||||
class SlippiReplayComm
|
||||
{
|
||||
public:
|
||||
typedef struct WatchSettings
|
||||
{
|
||||
std::string path;
|
||||
int startFrame = Slippi::GAME_FIRST_FRAME;
|
||||
int endFrame = INT_MAX;
|
||||
std::string gameStartAt = "";
|
||||
std::string gameStation = "";
|
||||
int index = 0;
|
||||
} WatchSettings;
|
||||
|
||||
// Loaded file contents
|
||||
typedef struct CommSettings
|
||||
{
|
||||
std::string mode;
|
||||
std::string replayPath;
|
||||
int startFrame = Slippi::GAME_FIRST_FRAME;
|
||||
int endFrame = INT_MAX;
|
||||
bool outputOverlayFiles;
|
||||
bool isRealTimeMode;
|
||||
std::string rollbackDisplayMethod; // off, normal, visible
|
||||
std::string commandId;
|
||||
std::queue<WatchSettings> queue;
|
||||
} CommSettings;
|
||||
|
||||
SlippiReplayComm();
|
||||
~SlippiReplayComm();
|
||||
|
||||
WatchSettings current;
|
||||
|
||||
CommSettings getSettings();
|
||||
void nextReplay();
|
||||
bool isNewReplay();
|
||||
std::unique_ptr<Slippi::SlippiGame> loadGame();
|
||||
|
||||
private:
|
||||
void loadFile();
|
||||
std::string getReplayPath();
|
||||
|
||||
std::string configFilePath;
|
||||
json fileData;
|
||||
std::string previousReplayLoaded;
|
||||
std::string previousCommandId;
|
||||
int previousIndex;
|
||||
|
||||
u64 configLastLoadModTime;
|
||||
|
||||
// Queue stuff
|
||||
bool isFirstLoad = true;
|
||||
bool provideNew = false;
|
||||
int queuePos = 0;
|
||||
|
||||
CommSettings commFileSettings;
|
||||
};
|
251
Source/Core/Core/Slippi/SlippiSavestate.cpp
Normal file
251
Source/Core/Core/Slippi/SlippiSavestate.cpp
Normal file
|
@ -0,0 +1,251 @@
|
|||
#include "SlippiSavestate.h"
|
||||
#include "Common/CommonFuncs.h"
|
||||
#include "Common/MemoryUtil.h"
|
||||
#include "Core/HW/AudioInterface.h"
|
||||
#include "Core/HW/DSP.h"
|
||||
#include "Core/HW/DVDInterface.h"
|
||||
#include "Core/HW/EXI.h"
|
||||
#include "Core/HW/GPFifo.h"
|
||||
#include "Core/HW/HW.h"
|
||||
#include "Core/HW/Memmap.h"
|
||||
#include "Core/HW/ProcessorInterface.h"
|
||||
#include "Core/HW/SI.h"
|
||||
#include "Core/HW/VideoInterface.h"
|
||||
#include <vector>
|
||||
|
||||
SlippiSavestate::SlippiSavestate()
|
||||
{
|
||||
initBackupLocs();
|
||||
|
||||
for (auto it = backupLocs.begin(); it != backupLocs.end(); ++it)
|
||||
{
|
||||
auto size = it->endAddress - it->startAddress;
|
||||
it->data = static_cast<u8*>(Common::AllocateAlignedMemory(size, 64));
|
||||
}
|
||||
|
||||
// u8 *ptr = nullptr;
|
||||
// PointerWrap p(&ptr, PointerWrap::MODE_MEASURE);
|
||||
|
||||
// getDolphinState(p);
|
||||
// const size_t buffer_size = reinterpret_cast<size_t>(ptr);
|
||||
// dolphinSsBackup.resize(buffer_size);
|
||||
}
|
||||
|
||||
SlippiSavestate::~SlippiSavestate()
|
||||
{
|
||||
for (auto it = backupLocs.begin(); it != backupLocs.end(); ++it)
|
||||
{
|
||||
Common::FreeAlignedMemory(it->data);
|
||||
}
|
||||
}
|
||||
|
||||
bool cmpFn(SlippiSavestate::PreserveBlock pb1, SlippiSavestate::PreserveBlock pb2)
|
||||
{
|
||||
return pb1.address < pb2.address;
|
||||
}
|
||||
|
||||
void SlippiSavestate::initBackupLocs()
|
||||
{
|
||||
static std::vector<ssBackupLoc> fullBackupRegions = {
|
||||
{0x80005520, 0x80005940, nullptr}, // Data Sections 0 and 1
|
||||
{0x803b7240, 0x804DEC00, nullptr}, // Data Sections 2-7 and in between sections including BSS
|
||||
|
||||
// Full Unknown Region: [804fec00 - 80BD5C40)
|
||||
// https://docs.google.com/spreadsheets/d/16ccNK_qGrtPfx4U25w7OWIDMZ-NxN1WNBmyQhaDxnEg/edit?usp=sharing
|
||||
{0x8065c000, 0x8071b000, nullptr}, // Unknown Region Pt1
|
||||
{0x80bb0000, 0x811AD5A0, nullptr}, // Unknown Region Pt2, Heap [80bd5c40 - 811AD5A0)
|
||||
};
|
||||
|
||||
static std::vector<PreserveBlock> excludeSections = {
|
||||
// Sound stuff
|
||||
{0x804031A0, 0x24}, // [804031A0 - 804031C4)
|
||||
{0x80407FB4, 0x34C}, // [80407FB4 - 80408300)
|
||||
{0x80433C64, 0x1EE80}, // [80433C64 - 80452AE4)
|
||||
{0x804A8D78, 0x17A68}, // [804A8D78 - 804C07E0)
|
||||
{0x804C28E0, 0x399C}, // [804C28E0 - 804C627C)
|
||||
{0x804D7474, 0x8}, // [804D7474 - 804D747C)
|
||||
{0x804D74F0, 0x50}, // [804D74F0 - 804D7540)
|
||||
{0x804D7548, 0x4}, // [804D7548 - 804D754C)
|
||||
{0x804D7558, 0x24}, // [804D7558 - 804D757C)
|
||||
{0x804D7580, 0xC}, // [804D7580 - 804D758C)
|
||||
{0x804D759C, 0x4}, // [804D759C - 804D75A0)
|
||||
{0x804D7720, 0x4}, // [804D7720 - 804D7724)
|
||||
{0x804D7744, 0x4}, // [804D7744 - 804D7748)
|
||||
{0x804D774C, 0x8}, // [804D774C - 804D7754)
|
||||
{0x804D7758, 0x8}, // [804D7758 - 804D7760)
|
||||
{0x804D7788, 0x10}, // [804D7788 - 804D7798)
|
||||
{0x804D77C8, 0x4}, // [804D77C8 - 804D77CC)
|
||||
{0x804D77D0, 0x4}, // [804D77D0 - 804D77D4)
|
||||
{0x804D77E0, 0x4}, // [804D77E0 - 804D77E4)
|
||||
{0x804DE358, 0x80}, // [804DE358 - 804DE3D8)
|
||||
{0x804DE800, 0x70}, // [804DE800 - 804DE870)
|
||||
|
||||
// The following need to be added to the ranges proper
|
||||
{0x804d6030, 0x4}, // ???
|
||||
{0x804d603c, 0x4}, // ???
|
||||
{0x804d7218, 0x4}, // ???
|
||||
{0x804d7228, 0x8}, // ???
|
||||
{0x804d7740, 0x4}, // ???
|
||||
{0x804d7754, 0x4}, // ???
|
||||
{0x804d77bc, 0x4}, // ???
|
||||
{0x804de7f0, 0x10}, // ???
|
||||
|
||||
// Camera Blocks, Temporarily added here
|
||||
//{0x80452c7c, 0x2B0}, // Cam Block 1, including gaps
|
||||
//{0x806e516c, 0xA8}, // Cam Block 2, including gaps
|
||||
};
|
||||
|
||||
static std::vector<ssBackupLoc> processedLocs = {};
|
||||
|
||||
// If the processed locations are already computed, just copy them directly
|
||||
if (processedLocs.size())
|
||||
{
|
||||
backupLocs.insert(backupLocs.end(), processedLocs.begin(), processedLocs.end());
|
||||
return;
|
||||
}
|
||||
|
||||
// Sort exclude sections
|
||||
std::sort(excludeSections.begin(), excludeSections.end(), cmpFn);
|
||||
|
||||
// Initialize backupLocs to full regions
|
||||
backupLocs.insert(backupLocs.end(), fullBackupRegions.begin(), fullBackupRegions.end());
|
||||
|
||||
// Remove exclude sections from backupLocs
|
||||
int idx = 0;
|
||||
for (auto it = excludeSections.begin(); it != excludeSections.end(); ++it)
|
||||
{
|
||||
PreserveBlock ipb = *it;
|
||||
|
||||
while (ipb.length > 0)
|
||||
{
|
||||
// Move up the backupLocs index until we reach a section relevant to us
|
||||
while (idx < backupLocs.size() && ipb.address >= backupLocs[idx].endAddress)
|
||||
{
|
||||
idx += 1;
|
||||
}
|
||||
|
||||
// Once idx is beyond backup locs, we are already not backup up this exclusion section
|
||||
if (idx >= backupLocs.size())
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
// Handle case where our exclusion starts before the actual backup section
|
||||
if (ipb.address < backupLocs[idx].startAddress)
|
||||
{
|
||||
int newSize = (s32)ipb.length - ((s32)backupLocs[idx].startAddress - (s32)ipb.address);
|
||||
|
||||
ipb.length = newSize > 0 ? newSize : 0;
|
||||
ipb.address = backupLocs[idx].startAddress;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Determine new size (how much we removed from backup)
|
||||
int newSize = (s32)ipb.length - ((s32)backupLocs[idx].endAddress - (s32)ipb.address);
|
||||
|
||||
// Add split section after exclusion
|
||||
if (backupLocs[idx].endAddress > ipb.address + ipb.length)
|
||||
{
|
||||
ssBackupLoc newLoc = { ipb.address + ipb.length, backupLocs[idx].endAddress, nullptr };
|
||||
backupLocs.insert(backupLocs.begin() + idx + 1, newLoc);
|
||||
}
|
||||
|
||||
// Modify section to end at the exclusion start
|
||||
backupLocs[idx].endAddress = ipb.address;
|
||||
if (backupLocs[idx].endAddress <= backupLocs[idx].startAddress)
|
||||
{
|
||||
backupLocs.erase(backupLocs.begin() + idx);
|
||||
}
|
||||
|
||||
// Set new size to see if there's still more to process
|
||||
newSize = newSize > 0 ? newSize : 0;
|
||||
ipb.address = ipb.address + (ipb.length - newSize);
|
||||
ipb.length = (u32)newSize;
|
||||
}
|
||||
}
|
||||
|
||||
processedLocs.clear();
|
||||
processedLocs.insert(processedLocs.end(), backupLocs.begin(), backupLocs.end());
|
||||
}
|
||||
|
||||
void SlippiSavestate::getDolphinState(PointerWrap& p)
|
||||
{
|
||||
// p.DoArray(Memory::m_pRAM, Memory::RAM_SIZE);
|
||||
// p.DoMarker("Memory");
|
||||
// VideoInterface::DoState(p);
|
||||
// p.DoMarker("VideoInterface");
|
||||
// SerialInterface::DoState(p);
|
||||
// p.DoMarker("SerialInterface");
|
||||
// ProcessorInterface::DoState(p);
|
||||
// p.DoMarker("ProcessorInterface");
|
||||
// DSP::DoState(p);
|
||||
// p.DoMarker("DSP");
|
||||
// DVDInterface::DoState(p);
|
||||
// p.DoMarker("DVDInterface");
|
||||
// GPFifo::DoState(p);
|
||||
// p.DoMarker("GPFifo");
|
||||
ExpansionInterface::DoState(p);
|
||||
p.DoMarker("ExpansionInterface");
|
||||
// AudioInterface::DoState(p);
|
||||
// p.DoMarker("AudioInterface");
|
||||
}
|
||||
|
||||
void SlippiSavestate::Capture()
|
||||
{
|
||||
// First copy memory
|
||||
for (auto it = backupLocs.begin(); it != backupLocs.end(); ++it)
|
||||
{
|
||||
auto size = it->endAddress - it->startAddress;
|
||||
Memory::CopyFromEmu(it->data, it->startAddress, size);
|
||||
}
|
||||
|
||||
//// Second copy dolphin states
|
||||
// u8 *ptr = &dolphinSsBackup[0];
|
||||
// PointerWrap p(&ptr, PointerWrap::MODE_WRITE);
|
||||
// getDolphinState(p);
|
||||
}
|
||||
|
||||
void SlippiSavestate::Load(std::vector<PreserveBlock> blocks)
|
||||
{
|
||||
// static std::vector<PreserveBlock> interruptStuff = {
|
||||
// {0x804BF9D2, 4},
|
||||
// {0x804C3DE4, 20},
|
||||
// {0x804C4560, 44},
|
||||
// {0x804D7760, 36},
|
||||
//};
|
||||
|
||||
// for (auto it = interruptStuff.begin(); it != interruptStuff.end(); ++it)
|
||||
// {
|
||||
// blocks.push_back(*it);
|
||||
// }
|
||||
|
||||
// Back up
|
||||
for (auto it = blocks.begin(); it != blocks.end(); ++it)
|
||||
{
|
||||
if (!preservationMap.count(*it))
|
||||
{
|
||||
// TODO: Clear preservation map when game ends
|
||||
preservationMap[*it] = std::vector<u8>(it->length);
|
||||
}
|
||||
|
||||
Memory::CopyFromEmu(&preservationMap[*it][0], it->address, it->length);
|
||||
}
|
||||
|
||||
// Restore memory blocks
|
||||
for (auto it = backupLocs.begin(); it != backupLocs.end(); ++it)
|
||||
{
|
||||
auto size = it->endAddress - it->startAddress;
|
||||
Memory::CopyToEmu(it->startAddress, it->data, size);
|
||||
}
|
||||
|
||||
//// Restore audio
|
||||
// u8 *ptr = &dolphinSsBackup[0];
|
||||
// PointerWrap p(&ptr, PointerWrap::MODE_READ);
|
||||
// getDolphinState(p);
|
||||
|
||||
// Restore
|
||||
for (auto it = blocks.begin(); it != blocks.end(); ++it)
|
||||
{
|
||||
Memory::CopyToEmu(it->address, &preservationMap[*it][0], it->length);
|
||||
}
|
||||
}
|
58
Source/Core/Core/Slippi/SlippiSavestate.h
Normal file
58
Source/Core/Core/Slippi/SlippiSavestate.h
Normal file
|
@ -0,0 +1,58 @@
|
|||
#pragma once
|
||||
|
||||
#include "Common/ChunkFile.h"
|
||||
#include "Common/CommonTypes.h"
|
||||
#include <unordered_map>
|
||||
|
||||
class PointerWrap;
|
||||
|
||||
class SlippiSavestate
|
||||
{
|
||||
public:
|
||||
struct PreserveBlock
|
||||
{
|
||||
u32 address;
|
||||
u32 length;
|
||||
|
||||
bool operator==(const PreserveBlock& p) const { return address == p.address && length == p.length; }
|
||||
};
|
||||
|
||||
SlippiSavestate();
|
||||
~SlippiSavestate();
|
||||
|
||||
void Capture();
|
||||
void Load(std::vector<PreserveBlock> blocks);
|
||||
|
||||
private:
|
||||
typedef struct
|
||||
{
|
||||
u32 startAddress;
|
||||
u32 endAddress;
|
||||
u8* data;
|
||||
} ssBackupLoc;
|
||||
|
||||
// These are the game locations to back up and restore
|
||||
std::vector<ssBackupLoc> backupLocs = {};
|
||||
|
||||
void initBackupLocs();
|
||||
|
||||
typedef struct
|
||||
{
|
||||
u32 address;
|
||||
u32 value;
|
||||
} ssBackupStaticToHeapPtr;
|
||||
|
||||
struct preserve_hash_fn
|
||||
{
|
||||
std::size_t operator()(const PreserveBlock& node) const
|
||||
{
|
||||
return node.address ^ node.length; // TODO: This is probably a bad hash
|
||||
}
|
||||
};
|
||||
|
||||
std::unordered_map<PreserveBlock, std::vector<u8>, preserve_hash_fn> preservationMap;
|
||||
|
||||
std::vector<u8> dolphinSsBackup;
|
||||
|
||||
void getDolphinState(PointerWrap& p);
|
||||
};
|
46
Source/Core/Core/Slippi/SlippiTimer.cpp
Normal file
46
Source/Core/Core/Slippi/SlippiTimer.cpp
Normal file
|
@ -0,0 +1,46 @@
|
|||
// SLIPPITODO: refactor with qt
|
||||
|
||||
/*#include "SlippiTimer.h"
|
||||
#include "DolphinWX/Frame.h"
|
||||
#include "SlippiPlayback.h"
|
||||
|
||||
extern std::unique_ptr<SlippiPlaybackStatus> g_playbackStatus;
|
||||
|
||||
void SlippiTimer::Notify()
|
||||
{
|
||||
if (!m_slider || !m_text)
|
||||
{
|
||||
// If there is no slider, do nothing
|
||||
return;
|
||||
}
|
||||
|
||||
int totalSeconds = (int)((g_playbackStatus->latestFrame - Slippi::GAME_FIRST_FRAME) / 60);
|
||||
int totalMinutes = (int)(totalSeconds / 60);
|
||||
int totalRemainder = (int)(totalSeconds % 60);
|
||||
|
||||
int currSeconds = int((g_playbackStatus->currentPlaybackFrame - Slippi::GAME_FIRST_FRAME) / 60);
|
||||
int currMinutes = (int)(currSeconds / 60);
|
||||
int currRemainder = (int)(currSeconds % 60);
|
||||
// Position string (i.e. MM:SS)
|
||||
char endTime[6];
|
||||
sprintf(endTime, "%02d:%02d", totalMinutes, totalRemainder);
|
||||
char currTime[6];
|
||||
sprintf(currTime, "%02d:%02d", currMinutes, currRemainder);
|
||||
|
||||
std::string time = std::string(currTime) + " / " + std::string(endTime);
|
||||
|
||||
// Setup the slider and gauge min/max values
|
||||
int minValue = m_slider->GetMin();
|
||||
int maxValue = m_slider->GetMax();
|
||||
if (maxValue != (int)g_playbackStatus->latestFrame || minValue != Slippi::PLAYBACK_FIRST_SAVE)
|
||||
{
|
||||
m_slider->SetRange(Slippi::PLAYBACK_FIRST_SAVE, (int)(g_playbackStatus->latestFrame));
|
||||
}
|
||||
|
||||
// Only update values while not actively seeking
|
||||
if (g_playbackStatus->targetFrameNum == INT_MAX && m_slider->isDraggingSlider == false)
|
||||
{
|
||||
m_text->SetLabel(_(time));
|
||||
m_slider->SetValue(g_playbackStatus->currentPlaybackFrame);
|
||||
}
|
||||
}*/
|
30
Source/Core/Core/Slippi/SlippiTimer.h
Normal file
30
Source/Core/Core/Slippi/SlippiTimer.h
Normal file
|
@ -0,0 +1,30 @@
|
|||
// SLIPPITODO: refactor with qt
|
||||
|
||||
/*#ifndef SLIPPI_TIMER_HEADER
|
||||
#define SLIPPI_TIMER_HEADER
|
||||
|
||||
#include <wx/timer.h>
|
||||
#include <wx/stattext.h>
|
||||
#include <Core/../DolphinWX/PlaybackSlider.h>
|
||||
|
||||
class CFrame;
|
||||
|
||||
class SlippiTimer : public wxTimer
|
||||
{
|
||||
public:
|
||||
SlippiTimer(CFrame* mainFrame, PlaybackSlider* slider, wxStaticText* text) {
|
||||
m_frame = mainFrame;
|
||||
m_slider = slider;
|
||||
m_text = text;
|
||||
}
|
||||
|
||||
// Called each time the timer's timeout expires
|
||||
void Notify() wxOVERRIDE;
|
||||
|
||||
CFrame* m_frame;
|
||||
PlaybackSlider* m_slider;
|
||||
wxStaticText* m_text;
|
||||
};
|
||||
|
||||
#endif
|
||||
*/
|
277
Source/Core/Core/Slippi/SlippiUser.cpp
Normal file
277
Source/Core/Core/Slippi/SlippiUser.cpp
Normal file
|
@ -0,0 +1,277 @@
|
|||
#include "SlippiUser.h"
|
||||
|
||||
#ifdef _WIN32
|
||||
#include "AtlBase.h"
|
||||
#include "AtlConv.h"
|
||||
#endif
|
||||
|
||||
#include "Common/CommonPaths.h"
|
||||
#include "Common/FileUtil.h"
|
||||
#include "Common/Logging/Log.h"
|
||||
#include "Common/MsgHandler.h"
|
||||
#include "Common/StringUtil.h"
|
||||
#include "Common/Thread.h"
|
||||
|
||||
#include "Core/ConfigManager.h"
|
||||
|
||||
#include <codecvt>
|
||||
#include <locale>
|
||||
|
||||
#include <json.hpp>
|
||||
using json = nlohmann::json;
|
||||
|
||||
#ifdef _WIN32
|
||||
#define MAX_SYSTEM_PROGRAM (4096)
|
||||
static void system_hidden(const char* cmd)
|
||||
{
|
||||
PROCESS_INFORMATION p_info;
|
||||
STARTUPINFO s_info;
|
||||
|
||||
memset(&s_info, 0, sizeof(s_info));
|
||||
memset(&p_info, 0, sizeof(p_info));
|
||||
s_info.cb = sizeof(s_info);
|
||||
|
||||
wchar_t utf16cmd[MAX_SYSTEM_PROGRAM] = { 0 };
|
||||
MultiByteToWideChar(CP_UTF8, 0, cmd, -1, utf16cmd, MAX_SYSTEM_PROGRAM);
|
||||
if (CreateProcessW(NULL, utf16cmd, NULL, NULL, 0, CREATE_NO_WINDOW, NULL, NULL, &s_info, &p_info))
|
||||
{
|
||||
DWORD ExitCode;
|
||||
WaitForSingleObject(p_info.hProcess, INFINITE);
|
||||
GetExitCodeProcess(p_info.hProcess, &ExitCode);
|
||||
CloseHandle(p_info.hProcess);
|
||||
CloseHandle(p_info.hThread);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
static void RunSystemCommand(const std::string& command)
|
||||
{
|
||||
#ifdef _WIN32
|
||||
_wsystem(UTF8ToUTF16(command).c_str());
|
||||
#else
|
||||
system(command.c_str());
|
||||
#endif
|
||||
}
|
||||
|
||||
SlippiUser::~SlippiUser()
|
||||
{
|
||||
// Wait for thread to terminate
|
||||
runThread = false;
|
||||
if (fileListenThread.joinable())
|
||||
fileListenThread.join();
|
||||
}
|
||||
|
||||
bool SlippiUser::AttemptLogin()
|
||||
{
|
||||
std::string userFilePath = getUserFilePath();
|
||||
|
||||
INFO_LOG(SLIPPI_ONLINE, "Looking for file at: %s", userFilePath.c_str());
|
||||
|
||||
{
|
||||
std::string userFilePathTxt =
|
||||
userFilePath + ".txt"; // Put the filename here in its own scope because we don't really need it elsewhere
|
||||
// If both files exist we just log they exist and take no further action
|
||||
if (File::Exists(userFilePathTxt) && File::Exists(userFilePath))
|
||||
{
|
||||
INFO_LOG(SLIPPI_ONLINE,
|
||||
"Found both .json.txt and .json file for user data. Using .json and ignoring the .json.txt");
|
||||
}
|
||||
// If only the .txt file exists copy the contents to a json file and delete the text file
|
||||
else if (File::Exists(userFilePathTxt))
|
||||
{
|
||||
// Attempt to copy the txt file to the json file path. If it fails log a warning
|
||||
if (!File::Copy(userFilePathTxt, userFilePath))
|
||||
{
|
||||
WARN_LOG(SLIPPI_ONLINE, "Could not copy file %s to %s", userFilePathTxt.c_str(), userFilePath.c_str());
|
||||
}
|
||||
// Attempt to delete the txt file. If it fails log an info because this isn't as critical
|
||||
if (!File::Delete(userFilePathTxt))
|
||||
{
|
||||
INFO_LOG(SLIPPI_ONLINE, "Failed to delete %s", userFilePathTxt.c_str());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Get user file
|
||||
std::string userFileContents;
|
||||
File::ReadFileToString(userFilePath, userFileContents);
|
||||
|
||||
userInfo = parseFile(userFileContents);
|
||||
|
||||
isLoggedIn = !userInfo.uid.empty();
|
||||
if (isLoggedIn)
|
||||
{
|
||||
WARN_LOG(SLIPPI_ONLINE, "Found user %s (%s)", userInfo.displayName.c_str(), userInfo.uid.c_str());
|
||||
}
|
||||
|
||||
return isLoggedIn;
|
||||
}
|
||||
|
||||
void SlippiUser::OpenLogInPage()
|
||||
{
|
||||
#ifdef _WIN32
|
||||
std::string folderSep = "%5C";
|
||||
#else
|
||||
std::string folderSep = "%2F";
|
||||
#endif
|
||||
|
||||
std::string url = "https://slippi.gg/online/enable";
|
||||
std::string path = getUserFilePath();
|
||||
path = ReplaceAll(path, "\\", folderSep);
|
||||
path = ReplaceAll(path, "/", folderSep);
|
||||
std::string fullUrl = url + "?path=" + path;
|
||||
|
||||
#ifdef _WIN32
|
||||
std::string command = "explorer \"" + fullUrl + "\"";
|
||||
#elif defined(__APPLE__)
|
||||
std::string command = "open \"" + fullUrl + "\"";
|
||||
#else
|
||||
std::string command = "xdg-open \"" + fullUrl + "\""; // Linux
|
||||
#endif
|
||||
|
||||
RunSystemCommand(command);
|
||||
}
|
||||
|
||||
void SlippiUser::UpdateFile()
|
||||
{
|
||||
#ifdef _WIN32
|
||||
std::string path = File::GetExeDirectory() + "/dolphin-slippi-tools.exe";
|
||||
std::string command = path + " user-update";
|
||||
system_hidden(command.c_str());
|
||||
#elif defined(__APPLE__)
|
||||
#else
|
||||
std::string path = "dolphin-slippi-tools";
|
||||
std::string command = path + " user-update";
|
||||
system(command.c_str());
|
||||
#endif
|
||||
}
|
||||
|
||||
void SlippiUser::UpdateApp()
|
||||
{
|
||||
#ifdef _WIN32
|
||||
auto isoPath = SConfig::GetInstance().m_strFilename;
|
||||
|
||||
std::string path = File::GetExeDirectory() + "/dolphin-slippi-tools.exe";
|
||||
std::string echoMsg = "echo Starting update process. If nothing happen after a few "
|
||||
"minutes, you may need to update manually from https://slippi.gg/netplay ...";
|
||||
std::string command = "start /b cmd /c " + echoMsg + " && \"" + path + "\" app-update -launch -iso \"" + isoPath + "\"";
|
||||
WARN_LOG(SLIPPI, "Executing app update command: %s", command);
|
||||
RunSystemCommand(command);
|
||||
#elif defined(__APPLE__)
|
||||
#else
|
||||
const char* appimage_path = getenv("APPIMAGE");
|
||||
if (!appimage_path)
|
||||
{
|
||||
CriticalAlertT("Automatic updates are not available for non-AppImage Linux builds.");
|
||||
return;
|
||||
}
|
||||
std::string path(appimage_path);
|
||||
std::string command = "appimageupdatetool " + path;
|
||||
WARN_LOG(SLIPPI, "Executing app update command: %s", command.c_str());
|
||||
RunSystemCommand(command);
|
||||
#endif
|
||||
}
|
||||
|
||||
void SlippiUser::ListenForLogIn()
|
||||
{
|
||||
if (runThread)
|
||||
return;
|
||||
|
||||
if (fileListenThread.joinable())
|
||||
fileListenThread.join();
|
||||
|
||||
runThread = true;
|
||||
fileListenThread = std::thread(&SlippiUser::FileListenThread, this);
|
||||
}
|
||||
|
||||
void SlippiUser::LogOut()
|
||||
{
|
||||
runThread = false;
|
||||
deleteFile();
|
||||
|
||||
UserInfo emptyUser;
|
||||
isLoggedIn = false;
|
||||
userInfo = emptyUser;
|
||||
}
|
||||
|
||||
void SlippiUser::OverwriteLatestVersion(std::string version)
|
||||
{
|
||||
userInfo.latestVersion = version;
|
||||
}
|
||||
|
||||
SlippiUser::UserInfo SlippiUser::GetUserInfo()
|
||||
{
|
||||
return userInfo;
|
||||
}
|
||||
|
||||
bool SlippiUser::IsLoggedIn()
|
||||
{
|
||||
return isLoggedIn;
|
||||
}
|
||||
|
||||
void SlippiUser::FileListenThread()
|
||||
{
|
||||
while (runThread)
|
||||
{
|
||||
if (AttemptLogin())
|
||||
{
|
||||
runThread = false;
|
||||
break;
|
||||
}
|
||||
|
||||
Common::SleepCurrentThread(500);
|
||||
}
|
||||
}
|
||||
|
||||
// On Linux platforms, the user.json file lives in the Sys/ directory in
|
||||
// order to deal with the fact that we want the configuration for AppImage
|
||||
// builds to be mutable.
|
||||
std::string SlippiUser::getUserFilePath()
|
||||
{
|
||||
#if defined(__APPLE__)
|
||||
std::string dirPath = File::GetBundleDirectory() + "/Contents/Resources";
|
||||
#elif defined(_WIN32)
|
||||
std::string dirPath = File::GetExeDirectory();
|
||||
#else
|
||||
std::string dirPath = File::GetSysDirectory();
|
||||
#endif
|
||||
std::string userFilePath = dirPath + DIR_SEP + "user.json";
|
||||
return userFilePath;
|
||||
}
|
||||
|
||||
inline std::string readString(json obj, std::string key)
|
||||
{
|
||||
auto item = obj.find(key);
|
||||
if (item == obj.end() || item.value().is_null())
|
||||
{
|
||||
return "";
|
||||
}
|
||||
|
||||
return obj[key];
|
||||
}
|
||||
|
||||
SlippiUser::UserInfo SlippiUser::parseFile(std::string fileContents)
|
||||
{
|
||||
UserInfo info;
|
||||
info.fileContents = fileContents;
|
||||
|
||||
auto res = json::parse(fileContents, nullptr, false);
|
||||
if (res.is_discarded() || !res.is_object())
|
||||
{
|
||||
return info;
|
||||
}
|
||||
|
||||
info.uid = readString(res, "uid");
|
||||
info.displayName = readString(res, "displayName");
|
||||
info.playKey = readString(res, "playKey");
|
||||
info.connectCode = readString(res, "connectCode");
|
||||
info.latestVersion = readString(res, "latestVersion");
|
||||
|
||||
return info;
|
||||
}
|
||||
|
||||
void SlippiUser::deleteFile()
|
||||
{
|
||||
std::string userFilePath = getUserFilePath();
|
||||
File::Delete(userFilePath);
|
||||
}
|
44
Source/Core/Core/Slippi/SlippiUser.h
Normal file
44
Source/Core/Core/Slippi/SlippiUser.h
Normal file
|
@ -0,0 +1,44 @@
|
|||
#pragma once
|
||||
|
||||
#include "Common/CommonTypes.h"
|
||||
#include <atomic>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
|
||||
class SlippiUser
|
||||
{
|
||||
public:
|
||||
struct UserInfo
|
||||
{
|
||||
std::string uid = "";
|
||||
std::string playKey = "";
|
||||
std::string displayName = "";
|
||||
std::string connectCode = "";
|
||||
std::string latestVersion = "";
|
||||
std::string fileContents = "";
|
||||
};
|
||||
|
||||
~SlippiUser();
|
||||
|
||||
bool AttemptLogin();
|
||||
void OpenLogInPage();
|
||||
void UpdateFile();
|
||||
void UpdateApp();
|
||||
void ListenForLogIn();
|
||||
void LogOut();
|
||||
void OverwriteLatestVersion(std::string version);
|
||||
UserInfo GetUserInfo();
|
||||
bool IsLoggedIn();
|
||||
void FileListenThread();
|
||||
|
||||
protected:
|
||||
std::string getUserFilePath();
|
||||
UserInfo parseFile(std::string fileContents);
|
||||
void deleteFile();
|
||||
|
||||
UserInfo userInfo;
|
||||
bool isLoggedIn = false;
|
||||
|
||||
std::thread fileListenThread;
|
||||
std::atomic<bool> runThread;
|
||||
};
|
|
@ -139,6 +139,7 @@ int main(int argc, char* argv[])
|
|||
UICommon::Init();
|
||||
Resources::Init();
|
||||
Settings::Instance().SetBatchModeEnabled(options.is_set("batch"));
|
||||
Settings::Instance().SetSlippiInputFile(static_cast<const char*>(options.get("slippi_input")));
|
||||
|
||||
// Hook up alerts from core
|
||||
Common::RegisterMsgAlertHandler(QtMsgAlertHandler);
|
||||
|
|
|
@ -564,6 +564,15 @@ void Settings::SetBatchModeEnabled(bool batch)
|
|||
m_batch = batch;
|
||||
}
|
||||
|
||||
std::string Settings::GetSlippiInputFile() const
|
||||
{
|
||||
return SConfig::GetInstance().m_strSlippiInput;
|
||||
}
|
||||
void Settings::SetSlippiInputFile(std::string path)
|
||||
{
|
||||
SConfig::GetInstance().m_strSlippiInput = path;
|
||||
}
|
||||
|
||||
bool Settings::IsSDCardInserted() const
|
||||
{
|
||||
return SConfig::GetInstance().m_WiiSDCard;
|
||||
|
|
|
@ -86,6 +86,8 @@ public:
|
|||
bool IsBatchModeEnabled() const;
|
||||
void SetBatchModeEnabled(bool batch);
|
||||
|
||||
std::string GetSlippiInputFile() const;
|
||||
void SetSlippiInputFile(std::string path);
|
||||
bool IsSDCardInserted() const;
|
||||
void SetSDCardInserted(bool inserted);
|
||||
bool IsUSBKeyboardConnected() const;
|
||||
|
|
|
@ -89,6 +89,7 @@ void GameCubePane::CreateWidgets()
|
|||
{std::make_pair(tr("<Nothing>"), ExpansionInterface::EXIDEVICE_NONE),
|
||||
std::make_pair(tr("Dummy"), ExpansionInterface::EXIDEVICE_DUMMY),
|
||||
std::make_pair(tr("Memory Card"), ExpansionInterface::EXIDEVICE_MEMORYCARD),
|
||||
std::make_pair(tr("Slippi"), ExpansionInterface::EXIDEVICE_SLIPPI),
|
||||
std::make_pair(tr("GCI Folder"), ExpansionInterface::EXIDEVICE_MEMORYCARDFOLDER),
|
||||
std::make_pair(tr("USB Gecko"), ExpansionInterface::EXIDEVICE_GECKO),
|
||||
std::make_pair(tr("Advance Game Port"), ExpansionInterface::EXIDEVICE_AGP),
|
||||
|
|
|
@ -118,6 +118,11 @@ std::unique_ptr<optparse::OptionParser> CreateParser(ParserOptions options)
|
|||
parser->add_option("-a", "--audio_emulation")
|
||||
.choices({"HLE", "LLE"})
|
||||
.help("Choose audio emulation from [%choices]");
|
||||
parser->add_option("-i", "--slippi_input")
|
||||
.action("store")
|
||||
.metavar("<file>")
|
||||
.type("string")
|
||||
.help("Path to Slippi replay config file (default: Slippi/playback.txt)");
|
||||
|
||||
return parser;
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue