841 changes: 841 additions & 0 deletions Source/Core/Core/IOS/USB/Emulated/Skylander.cpp

Large diffs are not rendered by default.

116 changes: 116 additions & 0 deletions Source/Core/Core/IOS/USB/Emulated/Skylander.h
@@ -0,0 +1,116 @@
// Copyright 2022 Dolphin Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later

#pragma once

#include <map>
#include <mutex>
#include <queue>
#include <vector>

#include "Common/CommonTypes.h"
#include "Common/IOFile.h"
#include "Core/IOS/USB/Common.h"

// The maximum possible characters the portal can handle.
// The status array is 32 bits and every character takes 2 bits.
// 32/2 = 16
constexpr u8 MAX_SKYLANDERS = 16;

namespace IOS::HLE::USB
{
class SkylanderUSB final : public Device
{
public:
SkylanderUSB(Kernel& ios, const std::string& device_name);
~SkylanderUSB();
DeviceDescriptor GetDeviceDescriptor() const override;
std::vector<ConfigDescriptor> GetConfigurations() const override;
std::vector<InterfaceDescriptor> GetInterfaces(u8 config) const override;
std::vector<EndpointDescriptor> GetEndpoints(u8 config, u8 interface, u8 alt) const override;
bool Attach() override;
bool AttachAndChangeInterface(u8 interface) override;
int CancelTransfer(u8 endpoint) override;
int ChangeInterface(u8 interface) override;
int GetNumberOfAltSettings(u8 interface) override;
int SetAltSetting(u8 alt_setting) override;
int SubmitTransfer(std::unique_ptr<CtrlMessage> message) override;
int SubmitTransfer(std::unique_ptr<BulkMessage> message) override;
int SubmitTransfer(std::unique_ptr<IntrMessage> message) override;
int SubmitTransfer(std::unique_ptr<IsoMessage> message) override;
void ScheduleTransfer(std::unique_ptr<TransferCommand> command, const std::array<u8, 64>& data,
s32 expected_count, u64 expected_time_us);

private:
Kernel& m_ios;
u16 m_vid = 0;
u16 m_pid = 0;
u8 m_active_interface = 0;
bool m_device_attached = false;
DeviceDescriptor m_device_descriptor;
std::vector<ConfigDescriptor> m_config_descriptor;
std::vector<InterfaceDescriptor> m_interface_descriptor;
std::vector<EndpointDescriptor> m_endpoint_descriptor;
std::queue<std::array<u8, 64>> m_queries;
};

struct Skylander final
{
File::IOFile sky_file;
u8 status = 0;
std::queue<u8> queued_status;
std::array<u8, 0x40 * 0x10> data{};
u32 last_id = 0;
void Save();

enum : u8
{
REMOVED = 0,
READY = 1,
REMOVING = 2,
ADDED = 3
};
};

struct SkylanderLEDColor final
{
u8 red = 0;
u8 green = 0;
u8 blue = 0;
};

class SkylanderPortal final
{
public:
void Activate();
void Deactivate();
bool IsActivated();
void UpdateStatus();
void SetLEDs(u8 side, u8 r, u8 g, u8 b);

std::array<u8, 64> GetStatus();
void QueryBlock(u8 sky_num, u8 block, u8* reply_buf);
void WriteBlock(u8 sky_num, u8 block, const u8* to_write_buf, u8* reply_buf);

bool CreateSkylander(const std::string& file_path, u16 sky_id, u16 sky_var);
bool RemoveSkylander(u8 sky_num);
u8 LoadSkylander(u8* buf, File::IOFile in_file);

protected:
std::mutex sky_mutex;

bool m_activated = true;
bool m_status_updated = false;
u8 m_interrupt_counter = 0;
SkylanderLEDColor m_color_right = {};
SkylanderLEDColor m_color_left = {};
SkylanderLEDColor m_color_trap = {};

std::array<Skylander, MAX_SKYLANDERS> skylanders;

private:
static bool IsSkylanderNumberValid(u8 sky_num);
static bool IsBlockNumberValid(u8 block);
};

} // namespace IOS::HLE::USB
87 changes: 60 additions & 27 deletions Source/Core/Core/IOS/USB/Host.cpp
Expand Up @@ -22,7 +22,10 @@
#include "Core/Config/MainSettings.h"
#include "Core/Core.h"
#include "Core/IOS/USB/Common.h"
#include "Core/IOS/USB/Emulated/Skylander.h"
#include "Core/IOS/USB/LibusbDevice.h"
#include "Core/NetPlayProto.h"
#include "Core/System.h"

namespace IOS::HLE
{
Expand All @@ -34,7 +37,7 @@ USBHost::~USBHost() = default;

std::optional<IPCReply> USBHost::Open(const OpenRequest& request)
{
if (!m_has_initialised && !Core::WantsDeterminism())
if (!m_has_initialised)
{
GetScanThread().Start();
// Force a device scan to complete, because some games (including Your Shape) only care
Expand Down Expand Up @@ -96,12 +99,15 @@ bool USBHost::ShouldAddDevice(const USB::Device& device) const
return true;
}

// This is called from the scan thread. Returns false if we failed to update the device list.
bool USBHost::UpdateDevices(const bool always_add_hooks)
void USBHost::Update()
{
if (Core::WantsDeterminism())
return true;
UpdateDevices();
}

// This is called from the scan thread. Returns false if we failed to update the device list.
bool USBHost::UpdateDevices(const bool always_add_hooks)
{
DeviceChangeHooks hooks;
std::set<u64> plugged_devices;
// If we failed to get a new, up-to-date list of devices, we cannot detect device removals.
Expand All @@ -115,31 +121,35 @@ bool USBHost::UpdateDevices(const bool always_add_hooks)
bool USBHost::AddNewDevices(std::set<u64>& new_devices, DeviceChangeHooks& hooks,
const bool always_add_hooks)
{
AddEmulatedDevices(new_devices, hooks, always_add_hooks);
#ifdef __LIBUSB__
auto whitelist = Config::GetUSBDeviceWhitelist();
if (whitelist.empty())
return true;

if (m_context.IsValid())
if (!Core::WantsDeterminism())
{
const int ret = m_context.GetDeviceList([&](libusb_device* device) {
libusb_device_descriptor descriptor;
libusb_get_device_descriptor(device, &descriptor);
if (whitelist.count({descriptor.idVendor, descriptor.idProduct}) == 0)
return true;
auto whitelist = Config::GetUSBDeviceWhitelist();
if (whitelist.empty())
return true;

auto usb_device = std::make_unique<USB::LibusbDevice>(m_ios, device, descriptor);
if (!ShouldAddDevice(*usb_device))
if (m_context.IsValid())
{
const int ret = m_context.GetDeviceList([&](libusb_device* device) {
libusb_device_descriptor descriptor;
libusb_get_device_descriptor(device, &descriptor);
if (whitelist.count({descriptor.idVendor, descriptor.idProduct}) == 0)
return true;

auto usb_device = std::make_unique<USB::LibusbDevice>(m_ios, device, descriptor);
if (!ShouldAddDevice(*usb_device))
return true;

const u64 id = usb_device->GetId();
new_devices.insert(id);
if (AddDevice(std::move(usb_device)) || always_add_hooks)
hooks.emplace(GetDeviceById(id), ChangeEvent::Inserted);
return true;

const u64 id = usb_device->GetId();
new_devices.insert(id);
if (AddDevice(std::move(usb_device)) || always_add_hooks)
hooks.emplace(GetDeviceById(id), ChangeEvent::Inserted);
return true;
});
if (ret != LIBUSB_SUCCESS)
WARN_LOG_FMT(IOS_USB, "GetDeviceList failed: {}", LibusbUtils::ErrorWrap(ret));
});
if (ret != LIBUSB_SUCCESS)
WARN_LOG_FMT(IOS_USB, "GetDeviceList failed: {}", LibusbUtils::ErrorWrap(ret));
}
}
#endif
return true;
Expand Down Expand Up @@ -175,21 +185,44 @@ void USBHost::DispatchHooks(const DeviceChangeHooks& hooks)
OnDeviceChangeEnd();
}

void USBHost::AddEmulatedDevices(std::set<u64>& new_devices, DeviceChangeHooks& hooks,
bool always_add_hooks)
{
if (Config::Get(Config::MAIN_EMULATE_SKYLANDER_PORTAL) && !NetPlay::IsNetPlayRunning())
{
auto skylanderportal = std::make_unique<USB::SkylanderUSB>(m_ios, "Skylander Portal");
if (ShouldAddDevice(*skylanderportal))
{
const u64 skyid = skylanderportal->GetId();
new_devices.insert(skyid);
if (AddDevice(std::move(skylanderportal)) || always_add_hooks)
{
hooks.emplace(GetDeviceById(skyid), ChangeEvent::Inserted);
}
}
}
}

USBHost::ScanThread::~ScanThread()
{
Stop();
}

void USBHost::ScanThread::WaitForFirstScan()
{
m_first_scan_complete_event.Wait();
if (m_thread_running.IsSet())
{
m_first_scan_complete_event.Wait();
}
}

void USBHost::ScanThread::Start()
{
if (Core::WantsDeterminism())
{
m_host->UpdateDevices();
return;

}
if (m_thread_running.TestAndSet())
{
m_thread = std::thread([this] {
Expand Down
3 changes: 3 additions & 0 deletions Source/Core/Core/IOS/USB/Host.h
Expand Up @@ -76,10 +76,13 @@ class USBHost : public Device

private:
bool AddDevice(std::unique_ptr<USB::Device> device);
void Update() override;
bool UpdateDevices(bool always_add_hooks = false);
bool AddNewDevices(std::set<u64>& new_devices, DeviceChangeHooks& hooks, bool always_add_hooks);
void DetectRemovedDevices(const std::set<u64>& plugged_devices, DeviceChangeHooks& hooks);
void DispatchHooks(const DeviceChangeHooks& hooks);
void AddEmulatedDevices(std::set<u64>& new_devices, DeviceChangeHooks& hooks,
bool always_add_hooks);

bool m_has_initialised = false;
LibusbUtils::Context m_context;
Expand Down
7 changes: 7 additions & 0 deletions Source/Core/Core/System.cpp
Expand Up @@ -20,6 +20,7 @@
#include "Core/HW/SI/SI.h"
#include "Core/HW/Sram.h"
#include "Core/HW/VideoInterface.h"
#include "IOS/USB/Emulated/Skylander.h"
#include "VideoCommon/CommandProcessor.h"
#include "VideoCommon/Fifo.h"
#include "VideoCommon/GeometryShaderManager.h"
Expand Down Expand Up @@ -47,6 +48,7 @@ struct System::Impl
Fifo::FifoManager m_fifo;
GeometryShaderManager m_geometry_shader_manager;
GPFifo::GPFifoManager m_gp_fifo;
IOS::HLE::USB::SkylanderPortal m_skylander_portal;
Memory::MemoryManager m_memory;
MemoryInterface::MemoryInterfaceState m_memory_interface_state;
PixelEngine::PixelEngineManager m_pixel_engine;
Expand Down Expand Up @@ -151,6 +153,11 @@ GPFifo::GPFifoManager& System::GetGPFifo() const
return m_impl->m_gp_fifo;
}

IOS::HLE::USB::SkylanderPortal& System::GetSkylanderPortal() const
{
return m_impl->m_skylander_portal;
}

Memory::MemoryManager& System::GetMemory() const
{
return m_impl->m_memory;
Expand Down
5 changes: 5 additions & 0 deletions Source/Core/Core/System.h
Expand Up @@ -47,6 +47,10 @@ namespace GPFifo
{
class GPFifoManager;
}
namespace IOS::HLE::USB
{
class SkylanderPortal;
};
namespace Memory
{
class MemoryManager;
Expand Down Expand Up @@ -116,6 +120,7 @@ class System
Fifo::FifoManager& GetFifo() const;
GeometryShaderManager& GetGeometryShaderManager() const;
GPFifo::GPFifoManager& GetGPFifo() const;
IOS::HLE::USB::SkylanderPortal& GetSkylanderPortal() const;
Memory::MemoryManager& GetMemory() const;
MemoryInterface::MemoryInterfaceState& GetMemoryInterfaceState() const;
PixelEngine::PixelEngineManager& GetPixelEngine() const;
Expand Down
2 changes: 2 additions & 0 deletions Source/Core/DolphinLib.props
Expand Up @@ -374,6 +374,7 @@
<ClInclude Include="Core\IOS\USB\Bluetooth\WiimoteDevice.h" />
<ClInclude Include="Core\IOS\USB\Bluetooth\WiimoteHIDAttr.h" />
<ClInclude Include="Core\IOS\USB\Common.h" />
<ClInclude Include="Core\IOS\USB\Emulated\Skylander.h" />
<ClInclude Include="Core\IOS\USB\Host.h" />
<ClInclude Include="Core\IOS\USB\LibusbDevice.h" />
<ClInclude Include="Core\IOS\USB\OH0\OH0.h" />
Expand Down Expand Up @@ -987,6 +988,7 @@
<ClCompile Include="Core\IOS\USB\Bluetooth\WiimoteDevice.cpp" />
<ClCompile Include="Core\IOS\USB\Bluetooth\WiimoteHIDAttr.cpp" />
<ClCompile Include="Core\IOS\USB\Common.cpp" />
<ClCompile Include="Core\IOS\USB\Emulated\Skylander.cpp" />
<ClCompile Include="Core\IOS\USB\Host.cpp" />
<ClCompile Include="Core\IOS\USB\LibusbDevice.cpp" />
<ClCompile Include="Core\IOS\USB\OH0\OH0.cpp" />
Expand Down
2 changes: 2 additions & 0 deletions Source/Core/DolphinQt/CMakeLists.txt
Expand Up @@ -325,6 +325,8 @@ add_executable(dolphin-emu
Settings/USBDeviceAddToWhitelistDialog.h
Settings/WiiPane.cpp
Settings/WiiPane.h
SkylanderPortal/SkylanderPortalWindow.cpp
SkylanderPortal/SkylanderPortalWindow.h
TAS/GCTASInputWindow.cpp
TAS/GCTASInputWindow.h
TAS/GBATASInputWindow.cpp
Expand Down
2 changes: 2 additions & 0 deletions Source/Core/DolphinQt/DolphinQt.vcxproj
Expand Up @@ -200,6 +200,7 @@
<ClCompile Include="Settings\PathPane.cpp" />
<ClCompile Include="Settings\USBDeviceAddToWhitelistDialog.cpp" />
<ClCompile Include="Settings\WiiPane.cpp" />
<ClCompile Include="SkylanderPortal\SkylanderPortalWindow.cpp" />
<ClCompile Include="TAS\GCTASInputWindow.cpp" />
<ClCompile Include="TAS\GBATASInputWindow.cpp" />
<ClCompile Include="TAS\IRWidget.cpp" />
Expand Down Expand Up @@ -379,6 +380,7 @@
<QtMoc Include="Settings\PathPane.h" />
<QtMoc Include="Settings\USBDeviceAddToWhitelistDialog.h" />
<QtMoc Include="Settings\WiiPane.h" />
<QtMoc Include="SkylanderPortal\SkylanderPortalWindow.h" />
<QtMoc Include="TAS\GCTASInputWindow.h" />
<QtMoc Include="TAS\GBATASInputWindow.h" />
<QtMoc Include="TAS\IRWidget.h" />
Expand Down
14 changes: 14 additions & 0 deletions Source/Core/DolphinQt/MainWindow.cpp
Expand Up @@ -111,6 +111,7 @@
#include "DolphinQt/RiivolutionBootWidget.h"
#include "DolphinQt/SearchBar.h"
#include "DolphinQt/Settings.h"
#include "DolphinQt/SkylanderPortal/SkylanderPortalWindow.h"
#include "DolphinQt/TAS/GBATASInputWindow.h"
#include "DolphinQt/TAS/GCTASInputWindow.h"
#include "DolphinQt/TAS/WiiTASInputWindow.h"
Expand Down Expand Up @@ -520,6 +521,7 @@ void MainWindow::ConnectMenuBar()
connect(m_menu_bar, &MenuBar::StartNetPlay, this, &MainWindow::ShowNetPlaySetupDialog);
connect(m_menu_bar, &MenuBar::BrowseNetPlay, this, &MainWindow::ShowNetPlayBrowser);
connect(m_menu_bar, &MenuBar::ShowFIFOPlayer, this, &MainWindow::ShowFIFOPlayer);
connect(m_menu_bar, &MenuBar::ShowSkylanderPortal, this, &MainWindow::ShowSkylanderPortal);
connect(m_menu_bar, &MenuBar::ConnectWiiRemote, this, &MainWindow::OnConnectWiiRemote);

// Movie
Expand Down Expand Up @@ -1301,6 +1303,18 @@ void MainWindow::ShowFIFOPlayer()
m_fifo_window->activateWindow();
}

void MainWindow::ShowSkylanderPortal()
{
if (!m_skylander_window)
{
m_skylander_window = new SkylanderPortalWindow;
}

m_skylander_window->show();
m_skylander_window->raise();
m_skylander_window->activateWindow();
}

void MainWindow::StateLoad()
{
QString path =
Expand Down
3 changes: 3 additions & 0 deletions Source/Core/DolphinQt/MainWindow.h
Expand Up @@ -43,6 +43,7 @@ class RegisterWidget;
class RenderWidget;
class SearchBar;
class SettingsWindow;
class SkylanderPortalWindow;
class ThreadWidget;
class ToolBar;
class WatchWidget;
Expand Down Expand Up @@ -159,6 +160,7 @@ class MainWindow final : public QMainWindow
void ShowNetPlaySetupDialog();
void ShowNetPlayBrowser();
void ShowFIFOPlayer();
void ShowSkylanderPortal();
void ShowMemcardManager();
void ShowResourcePackManager();
void ShowCheatsManager();
Expand Down Expand Up @@ -222,6 +224,7 @@ class MainWindow final : public QMainWindow
SettingsWindow* m_settings_window = nullptr;
GraphicsWindow* m_graphics_window = nullptr;
FIFOPlayerWindow* m_fifo_window = nullptr;
SkylanderPortalWindow* m_skylander_window = nullptr;
MappingWindow* m_hotkey_window = nullptr;
FreeLookWindow* m_freelook_window = nullptr;

Expand Down
2 changes: 2 additions & 0 deletions Source/Core/DolphinQt/MenuBar.cpp
Expand Up @@ -221,6 +221,8 @@ void MenuBar::AddToolsMenu()

tools_menu->addAction(tr("FIFO Player"), this, &MenuBar::ShowFIFOPlayer);

tools_menu->addAction(tr("&Skylanders Portal"), this, &MenuBar::ShowSkylanderPortal);

tools_menu->addSeparator();

tools_menu->addAction(tr("Start &NetPlay..."), this, &MenuBar::StartNetPlay);
Expand Down
1 change: 1 addition & 0 deletions Source/Core/DolphinQt/MenuBar.h
Expand Up @@ -89,6 +89,7 @@ class MenuBar final : public QMenuBar
void ShowAboutDialog();
void ShowCheatsManager();
void ShowResourcePackManager();
void ShowSkylanderPortal();
void ConnectWiiRemote(int id);

// Options
Expand Down
874 changes: 874 additions & 0 deletions Source/Core/DolphinQt/SkylanderPortal/SkylanderPortalWindow.cpp

Large diffs are not rendered by default.

64 changes: 64 additions & 0 deletions Source/Core/DolphinQt/SkylanderPortal/SkylanderPortalWindow.h
@@ -0,0 +1,64 @@
// Copyright 2022 Dolphin Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later

#pragma once

#include <array>
#include <optional>

#include <QDialog>
#include <QString>
#include <QWidget>

#include "Core/Core.h"
#include "Core/IOS/USB/Emulated/Skylander.h"

class QCheckBox;
class QGroupBox;
class QLineEdit;

struct Skylander
{
u8 portal_slot;
u16 sky_id;
u16 sky_var;
};

class SkylanderPortalWindow : public QWidget
{
Q_OBJECT
public:
explicit SkylanderPortalWindow(QWidget* parent = nullptr);
~SkylanderPortalWindow() override;

protected:
std::array<QLineEdit*, MAX_SKYLANDERS> m_edit_skylanders;
std::array<std::optional<Skylander>, MAX_SKYLANDERS> m_sky_slots;

private:
void CreateMainWindow();
void OnEmulationStateChanged(Core::State state);
void CreateSkylander(u8 slot);
void ClearSkylander(u8 slot);
void EmulatePortal(bool emulate);
void LoadSkylander(u8 slot);
void LoadSkylanderPath(u8 slot, const QString& path);
void UpdateEdits();
void closeEvent(QCloseEvent* bar) override;
bool eventFilter(QObject* object, QEvent* event) final override;

QCheckBox* m_checkbox;
QGroupBox* m_group_skylanders;
};

class CreateSkylanderDialog : public QDialog
{
Q_OBJECT

public:
explicit CreateSkylanderDialog(QWidget* parent);
QString GetFilePath() const;

protected:
QString m_file_path;
};