rest of the fucking owl

This commit is contained in:
R2DLiu 2020-07-02 23:32:57 -04:00
commit 746ab9586b
32 changed files with 5684 additions and 16 deletions

View file

@ -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;

View file

@ -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

View file

@ -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:

File diff suppressed because it is too large Load diff

View 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;
};
}

View file

@ -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()
{

View file

@ -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,
};

View file

@ -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()

View file

@ -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())

View 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();
}

View 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;
};

View 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;
}

View 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();
};

View 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;
}

View 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;
};

View 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?
}

View 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];
};

View 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() {}

View 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;
};

View 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;
}
}

View 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;
};

View 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);
}
}

View 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);
};

View 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);
}
}*/

View 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
*/

View 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);
}

View 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;
};

View file

@ -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);

View file

@ -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;

View file

@ -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;

View file

@ -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),

View file

@ -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;
}