@@ -0,0 +1,42 @@
// SPDX-License-Identifier: GPL-2.0-or-later

package org.dolphinemu.dolphinemu.features.skylanders.ui;

public class SkylanderSlot
{
private String mLabel;
private final int mSlotNum;
private int mPortalSlot;

public SkylanderSlot(String label, int slot)
{
mLabel = label;
mSlotNum = slot;
mPortalSlot = -1;
}

public String getLabel()
{
return mLabel;
}

public void setLabel(String label)
{
mLabel = label;
}

public int getSlotNum()
{
return mSlotNum;
}

public int getPortalSlot()
{
return mPortalSlot;
}

public void setPortalSlot(int mPortalSlot)
{
this.mPortalSlot = mPortalSlot;
}
}
@@ -0,0 +1,160 @@
// SPDX-License-Identifier: GPL-2.0-or-later

package org.dolphinemu.dolphinemu.features.skylanders.ui;

import android.content.Intent;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.ArrayAdapter;
import android.widget.Toast;

import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog;
import androidx.recyclerview.widget.RecyclerView;

import com.google.android.material.dialog.MaterialAlertDialogBuilder;

import org.dolphinemu.dolphinemu.R;
import org.dolphinemu.dolphinemu.activities.EmulationActivity;
import org.dolphinemu.dolphinemu.databinding.DialogCreateSkylanderBinding;
import org.dolphinemu.dolphinemu.databinding.ListItemSkylanderSlotBinding;
import org.dolphinemu.dolphinemu.features.skylanders.SkylanderConfig;
import org.dolphinemu.dolphinemu.features.skylanders.model.SkylanderPair;

import java.util.ArrayList;
import java.util.List;

public class SkylanderSlotAdapter extends RecyclerView.Adapter<SkylanderSlotAdapter.ViewHolder>
implements AdapterView.OnItemClickListener
{
public static class ViewHolder extends RecyclerView.ViewHolder
{
public ListItemSkylanderSlotBinding binding;

public ViewHolder(@NonNull ListItemSkylanderSlotBinding binding)
{
super(binding.getRoot());
this.binding = binding;
}
}

private final List<SkylanderSlot> mSlots;
private final EmulationActivity mActivity;

private DialogCreateSkylanderBinding mBinding;

public SkylanderSlotAdapter(List<SkylanderSlot> slots, EmulationActivity context)
{
mSlots = slots;
mActivity = context;
}

@NonNull
@Override
public SkylanderSlotAdapter.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType)
{
LayoutInflater inflater = LayoutInflater.from(parent.getContext());
ListItemSkylanderSlotBinding binding =
ListItemSkylanderSlotBinding.inflate(inflater, parent, false);
return new ViewHolder(binding);
}

@Override
public void onBindViewHolder(@NonNull ViewHolder holder, int position)
{
SkylanderSlot slot = mSlots.get(position);
holder.binding.textSkylanderName.setText(slot.getLabel());

holder.binding.buttonClearSkylander.setOnClickListener(v ->
{
SkylanderConfig.removeSkylander(slot.getPortalSlot());
mActivity.clearSkylander(slot.getSlotNum());
});

holder.binding.buttonLoadSkylander.setOnClickListener(v ->
{
Intent loadSkylander = new Intent(Intent.ACTION_OPEN_DOCUMENT);
loadSkylander.addCategory(Intent.CATEGORY_OPENABLE);
loadSkylander.setType("*/*");
mActivity.setSkylanderData(0, 0, "", position);
mActivity.startActivityForResult(loadSkylander, EmulationActivity.REQUEST_SKYLANDER_FILE);
});

LayoutInflater inflater = LayoutInflater.from(mActivity);
mBinding = DialogCreateSkylanderBinding.inflate(inflater);

List<String> skylanderNames = new ArrayList<>(SkylanderConfig.REVERSE_LIST_SKYLANDERS.keySet());
skylanderNames.sort(String::compareToIgnoreCase);

mBinding.skylanderDropdown.setAdapter(
new ArrayAdapter<>(mActivity, R.layout.support_simple_spinner_dropdown_item,
skylanderNames));
mBinding.skylanderDropdown.setOnItemClickListener(this);

holder.binding.buttonCreateSkylander.setOnClickListener(v ->
{
if (mBinding.getRoot().getParent() != null)
{
((ViewGroup) mBinding.getRoot().getParent()).removeAllViews();
}
AlertDialog createDialog = new MaterialAlertDialogBuilder(mActivity)
.setTitle(R.string.create_skylander_title)
.setView(mBinding.getRoot())
.setPositiveButton(R.string.create_skylander, null)
.setNegativeButton(R.string.cancel, null)
.show();
createDialog.getButton(android.app.AlertDialog.BUTTON_POSITIVE).setOnClickListener(
v1 ->
{
if (!mBinding.skylanderId.getText().toString().isBlank() &&
!mBinding.skylanderVar.getText().toString().isBlank())
{
Intent createSkylander = new Intent(Intent.ACTION_CREATE_DOCUMENT);
createSkylander.addCategory(Intent.CATEGORY_OPENABLE);
createSkylander.setType("*/*");
int id = Integer.parseInt(mBinding.skylanderId.getText().toString());
int var = Integer.parseInt(mBinding.skylanderVar.getText().toString());
String name = SkylanderConfig.LIST_SKYLANDERS.get(new SkylanderPair(id, var));
if (name != null)
{
createSkylander.putExtra(Intent.EXTRA_TITLE,
name + ".sky");
mActivity.setSkylanderData(id, var, name, position);
}
else
{
createSkylander.putExtra(Intent.EXTRA_TITLE,
"Unknown(ID" + id + "Var" + var + ").sky");
mActivity.setSkylanderData(id, var, "Unknown", position);
}
mActivity.startActivityForResult(createSkylander,
EmulationActivity.REQUEST_CREATE_SKYLANDER);
createDialog.dismiss();
}
else
{
Toast.makeText(mActivity, R.string.invalid_skylander,
Toast.LENGTH_SHORT).show();
}
});
});

}

@Override
public int getItemCount()
{
return mSlots.size();
}

@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id)
{
SkylanderPair skylanderIdVar =
SkylanderConfig.REVERSE_LIST_SKYLANDERS.get(parent.getItemAtPosition(position));
mBinding.skylanderId.setText(String.valueOf(skylanderIdVar.getId()));
mBinding.skylanderVar.setText(String.valueOf(skylanderIdVar.getVar()));
}
}
@@ -60,6 +60,7 @@ public final class MenuFragment extends Fragment implements View.OnClickListener
buttonsActionsMap.append(R.id.menu_change_disc, EmulationActivity.MENU_ACTION_CHANGE_DISC);
buttonsActionsMap.append(R.id.menu_exit, EmulationActivity.MENU_ACTION_EXIT);
buttonsActionsMap.append(R.id.menu_settings, EmulationActivity.MENU_ACTION_SETTINGS);
buttonsActionsMap.append(R.id.menu_skylanders, EmulationActivity.MENU_ACTION_SKYLANDERS);
}

private FragmentIngameMenuBinding mBinding;
@@ -110,6 +111,12 @@ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceStat
if (!getArguments().getBoolean(KEY_WII, true))
{
mBinding.menuRefreshWiimotes.setVisibility(View.GONE);
mBinding.menuSkylanders.setVisibility(View.GONE);
}

if (!BooleanSetting.MAIN_EMULATE_SKYLANDER_PORTAL.getBooleanGlobal())
{
mBinding.menuSkylanders.setVisibility(View.GONE);
}

LinearLayout options = mBinding.layoutOptions;
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="?attr/colorControlNormal"
android:pathData="M19,6.41L17.59,5 12,10.59 6.41,5 5,6.41 10.59,12 5,17.59 6.41,19 12,13.41 17.59,19 19,17.59 13.41,12 19,6.41z"/>
</vector>
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="?attr/colorControlNormal"
android:pathData="M6,20Q5.175,20 4.588,19.413Q4,18.825 4,18V15H6V18Q6,18 6,18Q6,18 6,18H18Q18,18 18,18Q18,18 18,18V15H20V18Q20,18.825 19.413,19.413Q18.825,20 18,20ZM11,16V7.85L8.4,10.45L7,9L12,4L17,9L15.6,10.45L13,7.85V16Z"/>
</vector>
@@ -0,0 +1,72 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout android:id="@+id/root"
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="@dimen/spacing_medlarge">

<com.google.android.material.textfield.TextInputLayout
android:id="@+id/layout_skylander_dropdown"
style="@style/Widget.Material3.TextInputLayout.OutlinedBox.ExposedDropdownMenu"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingBottom="@dimen/spacing_medlarge"
android:paddingHorizontal="@dimen/spacing_medlarge"
android:hint="@string/skylander_label"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">

<com.google.android.material.textfield.MaterialAutoCompleteTextView
android:id="@+id/skylander_dropdown"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:spinnerMode="dialog"
android:imeOptions="actionDone" />

</com.google.android.material.textfield.TextInputLayout>

<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
app:layout_constraintTop_toBottomOf="@+id/layout_skylander_dropdown"
android:gravity="center_vertical"
android:baselineAligned="false">

<com.google.android.material.textfield.TextInputLayout
android:id="@+id/layout_skylander_id"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:hint="@string/skylander_id"
android:paddingHorizontal="@dimen/spacing_medlarge">

<com.google.android.material.textfield.TextInputEditText
android:id="@+id/skylander_id"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="number" />

</com.google.android.material.textfield.TextInputLayout>

<com.google.android.material.textfield.TextInputLayout
android:id="@+id/layout_skylander_var"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:hint="@string/skylander_variant"
android:paddingHorizontal="@dimen/spacing_medlarge">

<com.google.android.material.textfield.TextInputEditText
android:id="@+id/skylander_var"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="number" />

</com.google.android.material.textfield.TextInputLayout>

</LinearLayout>

</androidx.constraintlayout.widget.ConstraintLayout>
@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.appcompat.widget.LinearLayoutCompat xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content">

<androidx.recyclerview.widget.RecyclerView
android:id="@+id/skylanders_manager"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:scrollbars="vertical"
android:fadeScrollbars="false"/>
</androidx.appcompat.widget.LinearLayoutCompat>
@@ -101,6 +101,11 @@
android:text="@string/emulation_change_disc"
style="@style/InGameMenuOption" />

<Button
android:id="@+id/menu_skylanders"
android:text="@string/emulate_skylander_portal"
style="@style/InGameMenuOption" />

</LinearLayout>

</ScrollView>
@@ -0,0 +1,61 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout android:id="@+id/root"
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="@dimen/spacing_medlarge">

<Button
android:id="@+id/button_create_skylander"
style="?attr/materialIconButtonFilledTonalStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:layout_marginEnd="@dimen/spacing_small"
android:layout_toStartOf="@id/button_load_skylander"
android:contentDescription="@string/create_skylander"
android:tooltipText="@string/create_skylander"
app:icon="@drawable/ic_add" />

<Button
android:id="@+id/button_load_skylander"
style="?attr/materialIconButtonFilledTonalStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:layout_marginEnd="@dimen/spacing_small"
android:layout_toStartOf="@+id/button_clear_skylander"
android:contentDescription="@string/load_skylander"
android:tooltipText="@string/load_skylander"
app:icon="@drawable/ic_load" />

<Button
android:id="@+id/button_clear_skylander"
style="?attr/materialIconButtonFilledTonalStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_centerVertical="true"
android:layout_marginEnd="@dimen/spacing_large"
android:contentDescription="@string/remove_skylander"
android:tooltipText="@string/remove_skylander"
app:icon="@drawable/ic_clear" />

<com.google.android.material.textview.MaterialTextView
android:id="@+id/text_skylander_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:layout_centerVertical="true"
android:layout_marginBottom="@dimen/spacing_medlarge"
android:layout_marginEnd="@dimen/spacing_large"
android:layout_marginStart="@dimen/spacing_large"
android:layout_toStartOf="@+id/button_create_skylander"
android:textAlignment="viewStart"
android:textColor="?attr/colorOnSurface"
android:textSize="16sp"
tools:text="Wii Remote with Motion Plus Pointing" />

</RelativeLayout>
@@ -909,4 +909,18 @@ It can efficiently compress both junk data and encrypted Wii data.
<string name="about_support"><a href="https://forums.dolphin-emu.org/">Support</a></string>
<string name="about_copyright_warning">\u00A9 2003–2015+ Dolphin Team. \u201cGameCube\u201d and \u201cWii\u201d are trademarks of Nintendo. Dolphin is not affiliated with Nintendo in any way.</string>

<!-- Emulated USB Devices -->
<string name="emulated_usb_devices">Emulated USB Devices</string>
<string name="emulate_skylander_portal">Skylanders Portal</string>
<string name="skylanders_manager">Skylanders Manager</string>
<string name="load_skylander">Load</string>
<string name="remove_skylander">Remove</string>
<string name="create_skylander">Create</string>
<string name="create_skylander_title">Create Skylander</string>
<string name="skylander_label">Skylander</string>
<string name="skylander_slot">Slot %1$d</string>
<string name="skylander_id">ID</string>
<string name="skylander_variant">Variant</string>
<string name="invalid_skylander">Invalid Skylander Selection</string>

</resources>
@@ -14,6 +14,7 @@ add_library(main SHARED
IniFile.cpp
MainAndroid.cpp
RiivolutionPatches.cpp
SkylanderConfig.cpp
WiiUtils.cpp
)

@@ -0,0 +1,168 @@
// Copyright 2023 Dolphin Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later

#include <jni.h>

#include <array>

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

extern "C" {

JNIEXPORT jobject JNICALL
Java_org_dolphinemu_dolphinemu_features_skylanders_SkylanderConfig_getSkylanderMap(JNIEnv* env,
jclass clazz)
{
jclass hash_map_class = env->FindClass("java/util/HashMap");
jmethodID hash_map_init = env->GetMethodID(hash_map_class, "<init>", "(I)V");
jobject hash_map_obj = env->NewObject(hash_map_class, hash_map_init,
static_cast<u16>(IOS::HLE::USB::list_skylanders.size()));
jmethodID hash_map_put = env->GetMethodID(
hash_map_class, "put", "(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;");

jclass skylander_class =
env->FindClass("org/dolphinemu/dolphinemu/features/skylanders/model/SkylanderPair");

jmethodID skylander_init = env->GetMethodID(skylander_class, "<init>", "(II)V");

for (const auto& it : IOS::HLE::USB::list_skylanders)
{
const std::string& name = it.second;
jobject skylander_obj =
env->NewObject(skylander_class, skylander_init, it.first.first, it.first.second);
env->CallObjectMethod(hash_map_obj, hash_map_put, skylander_obj, ToJString(env, name));
env->DeleteLocalRef(skylander_obj);
}

return hash_map_obj;
}

JNIEXPORT jobject JNICALL
Java_org_dolphinemu_dolphinemu_features_skylanders_SkylanderConfig_getInverseSkylanderMap(
JNIEnv* env, jclass clazz)
{
jclass hash_map_class = env->FindClass("java/util/HashMap");
jmethodID hash_map_init = env->GetMethodID(hash_map_class, "<init>", "(I)V");
jobject hash_map_obj = env->NewObject(hash_map_class, hash_map_init,
static_cast<u16>(IOS::HLE::USB::list_skylanders.size()));
jmethodID hash_map_put = env->GetMethodID(
hash_map_class, "put", "(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;");

jclass skylander_class =
env->FindClass("org/dolphinemu/dolphinemu/features/skylanders/model/SkylanderPair");

jmethodID skylander_init = env->GetMethodID(skylander_class, "<init>", "(II)V");

for (const auto& it : IOS::HLE::USB::list_skylanders)
{
const std::string& name = it.second;
jobject skylander_obj =
env->NewObject(skylander_class, skylander_init, it.first.first, it.first.second);
env->CallObjectMethod(hash_map_obj, hash_map_put, ToJString(env, name), skylander_obj);
env->DeleteLocalRef(skylander_obj);
}

return hash_map_obj;
}

JNIEXPORT jboolean JNICALL
Java_org_dolphinemu_dolphinemu_features_skylanders_SkylanderConfig_removeSkylander(JNIEnv* env,
jclass clazz,
jint slot)
{
auto& system = Core::System::GetInstance();
return static_cast<jboolean>(system.GetSkylanderPortal().RemoveSkylander(slot));
}

JNIEXPORT jobject JNICALL
Java_org_dolphinemu_dolphinemu_features_skylanders_SkylanderConfig_loadSkylander(JNIEnv* env,
jclass clazz,
jint slot,
jstring file_name)
{
File::IOFile sky_file(GetJString(env, file_name), "r+b");
if (!sky_file)
{
return nullptr;
}
std::array<u8, 0x40 * 0x10> file_data;
if (!sky_file.ReadBytes(file_data.data(), file_data.size()))
{
return nullptr;
}

jclass pair_class = env->FindClass("android/util/Pair");
jmethodID pair_init =
env->GetMethodID(pair_class, "<init>", "(Ljava/lang/Object;Ljava/lang/Object;)V");
jclass integer_class = env->FindClass("java/lang/Integer");
jmethodID int_init = env->GetMethodID(integer_class, "<init>", "(I)V");

auto& system = Core::System::GetInstance();
system.GetSkylanderPortal().RemoveSkylander(slot);
std::string name = "Unknown";

std::pair<u16, u16> id_var = system.GetSkylanderPortal().CalculateIDs(file_data);

const auto it = IOS::HLE::USB::list_skylanders.find(std::make_pair(id_var.first, id_var.second));

if (it != IOS::HLE::USB::list_skylanders.end())
{
name = it->second;
}

return env->NewObject(pair_class, pair_init,
env->NewObject(integer_class, int_init,
system.GetSkylanderPortal().LoadSkylander(
file_data.data(), std::move(sky_file))),
ToJString(env, name));
}

JNIEXPORT jobject JNICALL
Java_org_dolphinemu_dolphinemu_features_skylanders_SkylanderConfig_createSkylander(
JNIEnv* env, jclass clazz, jint id, jint var, jstring fileName, jint slot)
{
u16 sky_id = static_cast<u16>(id);
u16 sky_var = static_cast<u16>(var);

std::string file_name = GetJString(env, fileName);

auto& system = Core::System::GetInstance();
system.GetSkylanderPortal().CreateSkylander(file_name, sky_id, sky_var);
system.GetSkylanderPortal().RemoveSkylander(slot);

jclass pair_class = env->FindClass("android/util/Pair");
jmethodID pair_init =
env->GetMethodID(pair_class, "<init>", "(Ljava/lang/Object;Ljava/lang/Object;)V");

jclass integer_class = env->FindClass("java/lang/Integer");
jmethodID integer_init = env->GetMethodID(integer_class, "<init>", "(I)V");

File::IOFile sky_file(file_name, "r+b");
if (!sky_file)
{
return nullptr;
}
std::array<u8, 0x40 * 0x10> file_data;
if (!sky_file.ReadBytes(file_data.data(), file_data.size()))
{
return nullptr;
}

std::string name = "Unknown";

const auto it = IOS::HLE::USB::list_skylanders.find(std::make_pair(sky_id, sky_var));

if (it != IOS::HLE::USB::list_skylanders.end())
{
name = it->second;
}

return env->NewObject(pair_class, pair_init,
env->NewObject(integer_class, integer_init,
system.GetSkylanderPortal().LoadSkylander(
file_data.data(), std::move(sky_file))),
ToJString(env, name));
}
}

Large diffs are not rendered by default.

@@ -19,6 +19,7 @@ constexpr u8 MAX_SKYLANDERS = 16;

namespace IOS::HLE::USB
{
extern const std::map<const std::pair<const u16, const u16>, const char*> list_skylanders;
class SkylanderUSB final : public Device
{
public:
@@ -95,6 +96,7 @@ class SkylanderPortal final
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);
std::pair<u16, u16> CalculateIDs(const std::array<u8, 0x40 * 0x10>& file_data);

protected:
std::mutex sky_mutex;

Large diffs are not rendered by default.