├── .gitignore ├── .gitmodules ├── Makefile ├── README.md ├── Screenshots ├── Screenshot_1.jpg ├── Screenshot_2.jpg └── Screenshot_3.jpg ├── config_templates └── theme.json ├── icon.jpg ├── icon_large.png ├── include ├── AmiigoElements.h ├── AmiigoSettings.h ├── AmiigoUI.h ├── NFCDumper.h ├── Networking.h ├── emuiibo.hpp └── utils.h ├── romfs ├── API.cache ├── FragmentDefault.glsl ├── TextFragment.glsl ├── TextVertex.glsl ├── VertexDefault.glsl ├── bgFragment.glsl └── cacert.pem └── source ├── AmiigoElements.cpp ├── AmiigoSettings.cpp ├── AmiigoUI.cpp ├── NFCDumper.cpp ├── Networking.cpp ├── Utils.cpp ├── emuiibo.cpp └── main.cpp /.gitignore: -------------------------------------------------------------------------------- 1 | build/* 2 | *.nro 3 | *.nacp 4 | *.elf 5 | *.nso -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "Arriba"] 2 | path = Arriba 3 | url = https://github.com/CompSciOrBust/Arriba 4 | [submodule "json"] 5 | path = json 6 | url = https://github.com/nlohmann/json 7 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | #--------------------------------------------------------------------------------- 2 | .SUFFIXES: 3 | #--------------------------------------------------------------------------------- 4 | 5 | ifeq ($(strip $(DEVKITPRO)),) 6 | $(error "Please set DEVKITPRO in your environment. export DEVKITPRO=/devkitpro") 7 | endif 8 | 9 | TOPDIR ?= $(CURDIR) 10 | include $(DEVKITPRO)/libnx/switch_rules 11 | 12 | #--------------------------------------------------------------------------------- 13 | # TARGET is the name of the output 14 | # BUILD is the directory where object files & intermediate files will be placed 15 | # SOURCES is a list of directories containing source code 16 | # DATA is a list of directories containing data files 17 | # INCLUDES is a list of directories containing header files 18 | # ROMFS is the directory containing data to be added to RomFS, relative to the Makefile (Optional) 19 | # 20 | # NO_ICON: if set to anything, do not use icon. 21 | # NO_NACP: if set to anything, no .nacp file is generated. 22 | # APP_TITLE is the name of the app stored in the .nacp file (Optional) 23 | # APP_AUTHOR is the author of the app stored in the .nacp file (Optional) 24 | # APP_VERSION is the version of the app stored in the .nacp file (Optional) 25 | # APP_TITLEID is the titleID of the app stored in the .nacp file (Optional) 26 | # ICON is the filename of the icon (.jpg), relative to the project folder. 27 | # If not set, it attempts to use one of the following (in this order): 28 | # - .jpg 29 | # - icon.jpg 30 | # - /default_icon.jpg 31 | # 32 | # CONFIG_JSON is the filename of the NPDM config file (.json), relative to the project folder. 33 | # If not set, it attempts to use one of the following (in this order): 34 | # - .json 35 | # - config.json 36 | # If a JSON file is provided or autodetected, an ExeFS PFS0 (.nsp) is built instead 37 | # of a homebrew executable (.nro). This is intended to be used for sysmodules. 38 | # NACP building is skipped as well. 39 | #--------------------------------------------------------------------------------- 40 | TARGET := $(notdir $(CURDIR)) 41 | BUILD := build 42 | SOURCES := source Arriba/source zipper/zipper 43 | DATA := data 44 | INCLUDES := include Arriba/include json/single_include zipper/zipper 45 | ROMFS := romfs 46 | APP_TITLE := Amiigo 47 | APP_AUTHOR := CompSciOrBust 48 | APP_VERSION := 2.4.0 49 | 50 | #--------------------------------------------------------------------------------- 51 | # options for code generation 52 | #--------------------------------------------------------------------------------- 53 | ARCH := -march=armv8-a+crc+crypto -mtune=cortex-a57 -mtp=soft -fPIE 54 | 55 | CFLAGS := -g -Wall -O3 -ffunction-sections \ 56 | $(ARCH) $(DEFINES) 57 | 58 | CFLAGS += $(INCLUDE) -D__SWITCH__ -DVERSION='"$(APP_VERSION)"' `freetype-config --libs` 59 | 60 | CXXFLAGS := $(CFLAGS) -fno-rtti -fno-exceptions `freetype-config --cflags` 61 | 62 | ASFLAGS := -g $(ARCH) 63 | LDFLAGS = -specs=$(DEVKITPRO)/libnx/switch.specs -g $(ARCH) -Wl,-Map,$(notdir $*.map) 64 | 65 | LIBS := `freetype-config --libs` `curl-config --libs` -lglfw3 -lEGL -lglapi -ldrm_nouveau -lnx -lm -lglad -lcurl -lz -lminizip 66 | 67 | #--------------------------------------------------------------------------------- 68 | # list of directories containing libraries, this must be the top level containing 69 | # include and lib 70 | #--------------------------------------------------------------------------------- 71 | LIBDIRS := $(PORTLIBS) $(LIBNX) 72 | 73 | 74 | #--------------------------------------------------------------------------------- 75 | # no real need to edit anything past this point unless you need to add additional 76 | # rules for different file extensions 77 | #--------------------------------------------------------------------------------- 78 | ifneq ($(BUILD),$(notdir $(CURDIR))) 79 | #--------------------------------------------------------------------------------- 80 | 81 | export OUTPUT := $(CURDIR)/$(TARGET) 82 | export TOPDIR := $(CURDIR) 83 | 84 | export VPATH := $(foreach dir,$(SOURCES),$(CURDIR)/$(dir)) \ 85 | $(foreach dir,$(DATA),$(CURDIR)/$(dir)) 86 | 87 | export DEPSDIR := $(CURDIR)/$(BUILD) 88 | 89 | CFILES := $(foreach dir,$(SOURCES),$(notdir $(wildcard $(dir)/*.c))) 90 | CPPFILES := $(foreach dir,$(SOURCES),$(notdir $(wildcard $(dir)/*.cpp))) 91 | SFILES := $(foreach dir,$(SOURCES),$(notdir $(wildcard $(dir)/*.s))) 92 | BINFILES := $(foreach dir,$(DATA),$(notdir $(wildcard $(dir)/*.*))) 93 | 94 | #--------------------------------------------------------------------------------- 95 | # use CXX for linking C++ projects, CC for standard C 96 | #--------------------------------------------------------------------------------- 97 | ifeq ($(strip $(CPPFILES)),) 98 | #--------------------------------------------------------------------------------- 99 | export LD := $(CC) 100 | #--------------------------------------------------------------------------------- 101 | else 102 | #--------------------------------------------------------------------------------- 103 | export LD := $(CXX) 104 | #--------------------------------------------------------------------------------- 105 | endif 106 | #--------------------------------------------------------------------------------- 107 | 108 | export OFILES_BIN := $(addsuffix .o,$(BINFILES)) 109 | export OFILES_SRC := $(CPPFILES:.cpp=.o) $(CFILES:.c=.o) $(SFILES:.s=.o) 110 | export OFILES := $(OFILES_BIN) $(OFILES_SRC) 111 | export HFILES_BIN := $(addsuffix .h,$(subst .,_,$(BINFILES))) 112 | 113 | export INCLUDE := $(foreach dir,$(INCLUDES),-I$(CURDIR)/$(dir)) \ 114 | $(foreach dir,$(LIBDIRS),-I$(dir)/include) \ 115 | -I$(CURDIR)/$(BUILD) 116 | 117 | export LIBPATHS := $(foreach dir,$(LIBDIRS),-L$(dir)/lib) 118 | 119 | ifeq ($(strip $(CONFIG_JSON)),) 120 | jsons := $(wildcard *.json) 121 | ifneq (,$(findstring $(TARGET).json,$(jsons))) 122 | export APP_JSON := $(TOPDIR)/$(TARGET).json 123 | else 124 | ifneq (,$(findstring config.json,$(jsons))) 125 | export APP_JSON := $(TOPDIR)/config.json 126 | endif 127 | endif 128 | else 129 | export APP_JSON := $(TOPDIR)/$(CONFIG_JSON) 130 | endif 131 | 132 | ifeq ($(strip $(ICON)),) 133 | icons := $(wildcard *.jpg) 134 | ifneq (,$(findstring $(TARGET).jpg,$(icons))) 135 | export APP_ICON := $(TOPDIR)/$(TARGET).jpg 136 | else 137 | ifneq (,$(findstring icon.jpg,$(icons))) 138 | export APP_ICON := $(TOPDIR)/icon.jpg 139 | endif 140 | endif 141 | else 142 | export APP_ICON := $(TOPDIR)/$(ICON) 143 | endif 144 | 145 | ifeq ($(strip $(NO_ICON)),) 146 | export NROFLAGS += --icon=$(APP_ICON) 147 | endif 148 | 149 | ifeq ($(strip $(NO_NACP)),) 150 | export NROFLAGS += --nacp=$(CURDIR)/$(TARGET).nacp 151 | endif 152 | 153 | ifneq ($(APP_TITLEID),) 154 | export NACPFLAGS += --titleid=$(APP_TITLEID) 155 | endif 156 | 157 | ifneq ($(ROMFS),) 158 | export NROFLAGS += --romfsdir=$(CURDIR)/$(ROMFS) 159 | endif 160 | 161 | .PHONY: $(BUILD) clean all 162 | 163 | #--------------------------------------------------------------------------------- 164 | all: $(BUILD) 165 | 166 | $(BUILD): 167 | @[ -d $@ ] || mkdir -p $@ 168 | @$(MAKE) --no-print-directory -C $(BUILD) -f $(CURDIR)/Makefile 169 | #--------------------------------------------------------------------------------- 170 | clean: 171 | @echo clean ... 172 | ifeq ($(strip $(APP_JSON)),) 173 | @rm -fr $(BUILD) $(TARGET).nro $(TARGET).nacp $(TARGET).elf 174 | else 175 | @rm -fr $(BUILD) $(TARGET).nsp $(TARGET).nso $(TARGET).npdm $(TARGET).elf 176 | endif 177 | 178 | 179 | #--------------------------------------------------------------------------------- 180 | else 181 | .PHONY: all 182 | 183 | DEPENDS := $(OFILES:.o=.d) 184 | 185 | #--------------------------------------------------------------------------------- 186 | # main targets 187 | #--------------------------------------------------------------------------------- 188 | ifeq ($(strip $(APP_JSON)),) 189 | 190 | all : $(OUTPUT).nro 191 | 192 | ifeq ($(strip $(NO_NACP)),) 193 | $(OUTPUT).nro : $(OUTPUT).elf $(OUTPUT).nacp 194 | else 195 | $(OUTPUT).nro : $(OUTPUT).elf 196 | endif 197 | 198 | else 199 | 200 | all : $(OUTPUT).nsp 201 | 202 | $(OUTPUT).nsp : $(OUTPUT).nso $(OUTPUT).npdm 203 | 204 | $(OUTPUT).nso : $(OUTPUT).elf 205 | 206 | endif 207 | 208 | $(OUTPUT).elf : $(OFILES) 209 | 210 | $(OFILES_SRC) : $(HFILES_BIN) 211 | 212 | #--------------------------------------------------------------------------------- 213 | # you need a rule like this for each extension you use as binary data 214 | #--------------------------------------------------------------------------------- 215 | %.bin.o %_bin.h : %.bin 216 | #--------------------------------------------------------------------------------- 217 | @echo $(notdir $<) 218 | @$(bin2o) 219 | 220 | -include $(DEPENDS) 221 | 222 | #--------------------------------------------------------------------------------------- 223 | endif 224 | #--------------------------------------------------------------------------------------- 225 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Logo](icon.jpg) 2 | 3 | # Amiigo 4 | 5 | Amiigo is a GUI for creating and switching virtual Amiibo for use with the [Emuiibo](https://github.com/XorTroll/emuiibo) system module. Amiigo runs entirely on the Nintendo Switch console and no PC is required for setup. A Nintendo Switch with custom firmware is required to use Amiigo. Simply run the NRO file and Amiigo will handle the rest. 6 | 7 | All Amiibo data is obtained from the [AmiiboAPI](https://www.amiiboapi.com/) and the [emutool](https://github.com/XorTroll/emuiibo/tree/master/emutool) source code was referenced extensively to understand how virtual Amiibo generation works. 8 | 9 | Amiigo is built with the [Arriba](https://github.com/CompSciOrBust/Arriba) UI framework, which was designed with Amiigo in mind. 10 | 11 | Discord Server 12 | 13 | * [Credits](#Credits) 14 | * [Usage](#Usage) 15 | * [Support](#Support) 16 | * [Building](#Building) 17 | * [Donating](#Donating) 18 | 19 | 20 | 21 | ## Credits 22 | 23 | [XorTroll](https://github.com/XorTroll/) for [Emuiibo](https://github.com/XorTroll/emuiibo) and general help. 24 | 25 | [N3evin](https://github.com/N3evin/) for [AmiiboAPI](https://github.com/N3evin/AmiiboAPI) which is used in Amiigo Store. 26 | 27 | nlohmann for his [JSON library](https://github.com/nlohmann/json). 28 | 29 | [Kronos2308](https://github.com/Kronos2308/) for maintaining Amiigo Mod. 30 | 31 | Victoria_Borodinova for the [sombrero](https://pixabay.com/illustrations/sombrero-hat-mexico-mexican-4280389/) used in the logo. 32 | 33 | Za for bringing pizzas. 34 | 35 | All of the beta testers in the [CompSciOrBust Discord server](https://discord.gg/ZhRn3nn). 36 | 37 | [Kim-Dewelski](https://github.com/Kim-Dewelski) for being a long time friend who has provided much useful programming advice. 38 | 39 | ## Usage 40 | 41 | Use the "Amiigo Store" to generate new virtual Amiibos. 42 | 43 | Use the "My Amiibo" list to emulate any of your existing virtual Amiibos. 44 | 45 | D-Pad / analog sticks move between on screen buttons. 46 | 47 | A selects an Amiibo from the list or click an on screen button. 48 | 49 | X toggles emulation state. 50 | 51 | B backs out of a category if one is selected. 52 | 53 | Y / Long touchscreen tap opens the context menu for supported items. 54 | 55 | Everything that can be done with physical buttons can also be done via touchscreen. 56 | 57 | ## Support 58 | 59 | For support please use [the offical GBATemp thread](https://gbatemp.net/threads/amiigo-emuiibo-gui.549964/) unless you are reporting a bug in which case open a issue here on GitHub. Alternatively join [my Discord server](https://discord.gg/ZhRn3nn). 60 | 61 | ## Screenshots 62 | 63 | 64 | 65 | ## Building 66 | 67 | Install [LibNX](https://switchbrew.org/wiki/Setting_up_Development_Environment). 68 | 69 | Recursively clone this repo. 70 | 71 | Run (dkp-)pacman -S switch-glfw 72 | 73 | Run (dkp-)pacman -S switch-glad 74 | 75 | Run (dkp-)pacman -S switch-curl 76 | 77 | Run Make 78 | 79 | Optionally run "nxlink -s Amiigo.nro" after opening NetLoader to get debugging info. 80 | 81 | ## Donating 82 | Before donating to someone who makes bad homebrew consider donating to a charity instead. 83 | |Method|Info|How it will be used| 84 | |--|--|--| 85 | |BTC|1GUYKgask9u81MspethuF826iT8VCSg6XP|This is the "buy me a beer" option. I'll just spend it on whatever I want.| 86 | |Deliveroo credit|Contact me on Twitter or Discord|I'll order pizza instead of cooking and use the time saved to write Homebrew.| 87 | -------------------------------------------------------------------------------- /Screenshots/Screenshot_1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CompSciOrBust/Amiigo/f0e55acd8a49f6f7d839795399dab15c2a3e3c64/Screenshots/Screenshot_1.jpg -------------------------------------------------------------------------------- /Screenshots/Screenshot_2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CompSciOrBust/Amiigo/f0e55acd8a49f6f7d839795399dab15c2a3e3c64/Screenshots/Screenshot_2.jpg -------------------------------------------------------------------------------- /Screenshots/Screenshot_3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CompSciOrBust/Amiigo/f0e55acd8a49f6f7d839795399dab15c2a3e3c64/Screenshots/Screenshot_3.jpg -------------------------------------------------------------------------------- /config_templates/theme.json: -------------------------------------------------------------------------------- 1 | { 2 | "StatusBar_Neutral_R": 0.5, 3 | "StatusBar_Neutral_G": 0.7, 4 | "StatusBar_Neutral_B": 0.7, 5 | "StatusBar_Neutral_A": 1.0, 6 | 7 | "SelectorList_Neutral_R": 0.22, 8 | "SelectorList_Neutral_G": 0.47, 9 | "SelectorList_Neutral_B": 0.97, 10 | "SelectorList_Neutral_A": 1.0, 11 | 12 | "SelectorList_HighlightA_R": 0.1, 13 | "SelectorList_HighlightA_G": 0.95, 14 | "SelectorList_HighlightA_B": 0.98, 15 | "SelectorList_HighlightA_A": 1.0, 16 | 17 | "SelectorList_HighlightB_R": 0.5, 18 | "SelectorList_HighlightB_G": 0.85, 19 | "SelectorList_HighlightB_B": 1.0, 20 | "SelectorList_HighlightB_A": 1.0, 21 | 22 | "AmiigoStore_Neutral_R": 0.2, 23 | "AmiigoStore_Neutral_G": 0.76, 24 | "AmiigoStore_Neutral_B": 0.45, 25 | "AmiigoStore_Neutral_A": 1.0, 26 | 27 | "AmiigoStore_HighlightA_R": 0.6, 28 | "AmiigoStore_HighlightA_G": 0.95, 29 | "AmiigoStore_HighlightA_B": 0.98, 30 | "AmiigoStore_HighlightA_A": 1.0, 31 | 32 | "AmiigoStore_HighlightB_R": 0.1, 33 | "AmiigoStore_HighlightB_G": 0.98, 34 | "AmiigoStore_HighlightB_B": 0.55, 35 | "AmiigoStore_HighlightB_A": 1.0, 36 | 37 | "Settings_Neutral_R": 0.57, 38 | "Settings_Neutral_G": 0.21, 39 | "Settings_Neutral_B": 0.93, 40 | "Settings_Neutral_A": 1.0, 41 | 42 | "Settings_HighlightA_R": 0.9, 43 | "Settings_HighlightA_G": 0.95, 44 | "Settings_HighlightA_B": 0.94, 45 | "Settings_HighlightA_A": 1.0, 46 | 47 | "Settings_HighlightB_R": 1.0, 48 | "Settings_HighlightB_G": 0.85, 49 | "Settings_HighlightB_B": 1.0, 50 | "Settings_HighlightB_A": 1.0 51 | } -------------------------------------------------------------------------------- /icon.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CompSciOrBust/Amiigo/f0e55acd8a49f6f7d839795399dab15c2a3e3c64/icon.jpg -------------------------------------------------------------------------------- /icon_large.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CompSciOrBust/Amiigo/f0e55acd8a49f6f7d839795399dab15c2a3e3c64/icon_large.png -------------------------------------------------------------------------------- /include/AmiigoElements.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #include 4 | #include 5 | 6 | namespace Amiigo::Elements { 7 | // Dirty hack to allow the context menu to delete things. How do we fix it properly? Don't ask me I'm offline in bulgaria 8 | inline AmiiboEntry amiiboEntryGlobal; 9 | class selectorContextMenu : public Arriba::Primitives::Quad { 10 | private: 11 | std::vector buttonVector; 12 | 13 | public: 14 | selectorContextMenu(int x, int y, AmiiboEntry entry); 15 | virtual void onFrame(); 16 | void closeMenu(); 17 | }; 18 | } // namespace Amiigo::Elements 19 | -------------------------------------------------------------------------------- /include/AmiigoSettings.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #include 4 | #include 5 | 6 | namespace Amiigo::Settings { 7 | // Vars 8 | inline bool saveAmiibosToCategory = false; 9 | inline bool useRandomisedUUID = false; 10 | inline std::string updateURL = ""; 11 | inline char amiigoPath[FS_MAX_PATH]; 12 | inline long unsigned int updateTime = 0; 13 | inline unsigned char categoryMode = 0; 14 | inline std::string emuiiboVersionText = ""; 15 | void loadSettings(); 16 | void saveSettings(); 17 | 18 | enum categoryModes { 19 | saveToRoot, 20 | saveByGameName, 21 | saveByAmiiboSeries, 22 | saveByCurrentFolder, 23 | categoryCount = saveByCurrentFolder+1 24 | }; 25 | } // namespace Amiigo::Settings 26 | 27 | typedef Arriba::Maths::vec4 colour; 28 | 29 | namespace Amiigo::Settings::Colour { 30 | // Header 31 | inline colour statusBar = {0.5, 0.7, 0.7, 0.9}; 32 | // Amiibo list 33 | inline colour listNeutral = {0.22, 0.47, 0.93, 0.97}; 34 | inline colour listHighlightA = {0.1, 0.95, 0.98, 0.97}; 35 | inline colour listHighlightB = {0.5, 0.85, 1, 0.97}; 36 | // Amiigo Maker 37 | inline colour makerNeutral = {0.20, 0.76, 0.45, 0.97}; 38 | inline colour makerHighlightA = {0.6, 0.95, 0.98, 0.97}; 39 | inline colour makerHighlightB = {0.1, 0.98, 0.55, 0.97}; 40 | // Settings 41 | inline colour settingsNeutral = {0.57, 0.21, 0.93, 0.97}; 42 | inline colour settingsHighlightA = {0.9, 0.95, 0.94, 0.97}; 43 | inline colour settingsHighlightB = {1, 0.85, 1, 0.97}; 44 | } // namespace Amiigo::Settings::Colour 45 | -------------------------------------------------------------------------------- /include/AmiigoUI.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | namespace Amiigo::UI { 10 | // Variables 11 | inline int isRunning = 1; 12 | inline int statusHeight = Arriba::Graphics::windowHeight * 0.1; 13 | inline int switcherWidth = Arriba::Graphics::windowWidth * 0.3; 14 | inline int switcherHeight = Arriba::Graphics::windowHeight - statusHeight; 15 | inline std::vector seriesList; 16 | inline std::vector creatorData; 17 | inline bool makerIsInCategory = false; 18 | inline std::string selectedSeries; 19 | inline std::vector selectorAmiibos; 20 | inline std::string selectorPath = "sdmc:/emuiibo/amiibo"; 21 | // UI Object pointers 22 | // Scene bases 23 | inline Arriba::Primitives::Quad* splashScene = nullptr; 24 | inline Arriba::Primitives::Quad* selectorScene = nullptr; 25 | inline Arriba::Primitives::Quad* sceneSwitcher = nullptr; 26 | inline Arriba::Primitives::Quad* makerSwitcher = nullptr; 27 | // Lists 28 | inline std::vector lists; 29 | inline Arriba::Elements::InertialList* selectorList = nullptr; 30 | inline Arriba::Elements::InertialList* makerList = nullptr; 31 | // Switcher buttons 32 | inline std::vector buttons; 33 | inline Arriba::Elements::Button* selectorButton = nullptr; 34 | inline Arriba::Elements::Button* makerButton = nullptr; 35 | inline Arriba::Elements::Button* settingsButton = nullptr; 36 | inline Arriba::Elements::Button* exitButton = nullptr; 37 | // Settings objects 38 | inline Arriba::Primitives::Quad* settingsScene = nullptr; 39 | 40 | 41 | void initUI(); 42 | void initSplash(); 43 | void initSceneSwitcher(); 44 | void initSelector(); 45 | void initMaker(); 46 | void initSettings(); 47 | void handleInput(); 48 | void switcherPressed(); 49 | void selectorInput(int index); 50 | void makerInput(int index); 51 | void updateSelectorStrings(); 52 | void selectorContextMenuSpawner(int index, Arriba::Maths::vec2 pos); 53 | } // namespace Amiigo::UI 54 | -------------------------------------------------------------------------------- /include/NFCDumper.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | 4 | namespace Amiigo::NFC::Dumper { 5 | // Variables 6 | static NfcDeviceHandle readerHandle; 7 | static int readerCount = 0; 8 | 9 | // Structs 10 | struct ModelFormat { 11 | unsigned short gameCharacterID; 12 | char characterVariant; 13 | char series; 14 | unsigned short modelNumber; 15 | char figureType; 16 | char unused; 17 | } NX_PACKED; 18 | static_assert(sizeof(ModelFormat) == 8); 19 | 20 | // Functions 21 | void init(); 22 | void exit(); 23 | bool dumpNFC(); 24 | } // namespace Amiigo::NFC::Dumper 25 | -------------------------------------------------------------------------------- /include/Networking.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #include 4 | #include 5 | 6 | bool retrieveToFile(std::string URL, std::string path); 7 | bool retrieveToString(std::string URL, std::string mimeType, std::string *out); 8 | bool hasNetworkConnection(); 9 | -------------------------------------------------------------------------------- /include/emuiibo.hpp: -------------------------------------------------------------------------------- 1 | 2 | #pragma once 3 | #include 4 | #include 5 | 6 | namespace emu { 7 | 8 | struct VirtualAmiiboUuidInfo { 9 | bool use_random_uuid; 10 | u8 uuid[10]; 11 | }; 12 | 13 | struct VirtualAmiiboDate { 14 | u16 year; 15 | u8 month; 16 | u8 day; 17 | }; 18 | 19 | struct VirtualAmiiboData { 20 | VirtualAmiiboUuidInfo uuid_info; 21 | char name[40 + 1]; 22 | VirtualAmiiboDate first_write_date; 23 | VirtualAmiiboDate last_write_date; 24 | MiiCharInfo mii_charinfo; 25 | 26 | inline bool IsValid() { 27 | return strlen(this->name) > 0; 28 | } 29 | }; 30 | 31 | struct VirtualAmiiboAreaEntry { 32 | u64 program_id; 33 | u32 access_id; 34 | }; 35 | 36 | enum class EmulationStatus : u32 { 37 | On, 38 | Off, 39 | }; 40 | 41 | enum class VirtualAmiiboStatus : u32 { 42 | Invalid, 43 | Connected, 44 | Disconnected 45 | }; 46 | 47 | struct Version { 48 | u8 major; 49 | u8 minor; 50 | u8 micro; 51 | bool dev_build; 52 | 53 | inline constexpr bool EqualsExceptBuild(const Version &other) { 54 | return (other.major == this->major) && (other.minor == this->minor) && (other.micro == this->micro); 55 | } 56 | }; 57 | 58 | bool IsAvailable(); 59 | 60 | Result Initialize(); 61 | void Exit(); 62 | 63 | Version GetVersion(); 64 | 65 | void GetVirtualAmiiboDirectory(char *out_path, const size_t out_path_size); 66 | 67 | EmulationStatus GetEmulationStatus(); 68 | void SetEmulationStatus(const EmulationStatus status); 69 | 70 | Result GetActiveVirtualAmiibo(VirtualAmiiboData *out_amiibo_data, char *out_path, size_t out_path_size); 71 | Result SetActiveVirtualAmiibo(const char *path, const size_t path_size); 72 | void ResetActiveVirtualAmiibo(); 73 | 74 | VirtualAmiiboStatus GetActiveVirtualAmiiboStatus(); 75 | void SetActiveVirtualAmiiboStatus(const VirtualAmiiboStatus status); 76 | 77 | bool IsApplicationIdIntercepted(const u64 app_id); 78 | 79 | inline bool IsCurrentApplicationIdIntercepted() { 80 | bool intercepted = false; 81 | u64 process_id = 0; 82 | if(R_SUCCEEDED(pmdmntGetApplicationProcessId(&process_id))) { 83 | u64 program_id = 0; 84 | if(R_SUCCEEDED(pmdmntGetProgramId(&program_id, process_id))) { 85 | intercepted = IsApplicationIdIntercepted(program_id); 86 | } 87 | } 88 | return intercepted; 89 | } 90 | 91 | Result TryParseVirtualAmiibo(const char *path, const size_t path_size, VirtualAmiiboData *out_amiibo_data); 92 | Result GetActiveVirtualAmiiboAreas(VirtualAmiiboAreaEntry *out_area_buf, const size_t out_area_size, u32 *out_area_count); 93 | Result GetActiveVirtualAmiiboCurrentArea(u32 *out_access_id); 94 | Result SetActiveVirtualAmiiboCurrentArea(const u32 access_id); 95 | Result SetActiveVirtualAmiiboUuidInfo(const VirtualAmiiboUuidInfo uuid_info); 96 | 97 | } -------------------------------------------------------------------------------- /include/utils.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #include 4 | 5 | struct AmiiboEntry { 6 | std::string name; 7 | bool isCategory; 8 | std::string path; 9 | }; 10 | 11 | struct AmiiboCreatorData { 12 | std::string name; 13 | std::string gameName; 14 | std::string amiiboSeries; 15 | unsigned short game_character_id; 16 | char character_variant; 17 | char figure_type; 18 | unsigned short model_number; 19 | char series; 20 | }; 21 | 22 | bool checkIfFileExists(const char* path); 23 | std::vector scanForAmiibo(const char* path); 24 | std::vector getListOfSeries(); 25 | std::vector getAmiibosFromSeries(std::string series); 26 | void createVirtualAmiibo(AmiiboCreatorData amiibo); 27 | void firstTimeSetup(); 28 | bool checkForUpdates(); 29 | -------------------------------------------------------------------------------- /romfs/API.cache: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CompSciOrBust/Amiigo/f0e55acd8a49f6f7d839795399dab15c2a3e3c64/romfs/API.cache -------------------------------------------------------------------------------- /romfs/FragmentDefault.glsl: -------------------------------------------------------------------------------- 1 | #version 330 core 2 | in vec2 texCoords; 3 | out vec4 FragColor; 4 | 5 | uniform sampler2D _texture; 6 | uniform vec4 colour; 7 | 8 | void main() 9 | { 10 | FragColor = colour * texture(_texture, texCoords); 11 | } -------------------------------------------------------------------------------- /romfs/TextFragment.glsl: -------------------------------------------------------------------------------- 1 | #version 330 core 2 | in vec2 texCoords; 3 | out vec4 color; 4 | 5 | uniform sampler2D text; 6 | uniform vec4 colour; 7 | 8 | void main() 9 | { 10 | color = colour * vec4(1.0, 1.0, 1.0, texture(text, texCoords).r); 11 | } -------------------------------------------------------------------------------- /romfs/TextVertex.glsl: -------------------------------------------------------------------------------- 1 | #version 330 core 2 | layout (location = 0) in vec3 vertex; 3 | layout (location = 1) in vec2 texureCordsIn; 4 | out vec2 texCoords; 5 | 6 | uniform mat4 transform; 7 | uniform mat4 projection; 8 | 9 | void main() 10 | { 11 | gl_Position = projection * transform * vec4(vertex.x, vertex.y, vertex.z, 1.0); 12 | texCoords = texureCordsIn; 13 | } -------------------------------------------------------------------------------- /romfs/VertexDefault.glsl: -------------------------------------------------------------------------------- 1 | #version 330 core 2 | layout (location = 0) in vec3 aPos; 3 | layout (location = 1) in vec2 texureCordsIn; 4 | out vec2 texCoords; 5 | 6 | uniform mat4 transform; 7 | uniform mat4 projection; 8 | 9 | void main() 10 | { 11 | gl_Position = projection * transform * vec4(aPos.x, aPos.y, aPos.z, 1.0); 12 | texCoords = texureCordsIn; 13 | } -------------------------------------------------------------------------------- /romfs/bgFragment.glsl: -------------------------------------------------------------------------------- 1 | #version 330 core 2 | in vec2 texCoords; 3 | out vec4 FragColor; 4 | 5 | uniform sampler2D _texture; 6 | uniform vec4 colour; 7 | uniform float iTime; 8 | 9 | void main() 10 | { 11 | vec4 col = vec4(0.0); 12 | vec2 uv = texCoords - 0.5; 13 | uv *= 200; 14 | col.a = 1.0; 15 | col.b = 1.0/distance(floor(uv.y / 4.0), 100.0 * sin(iTime * 0.2 + floor(uv.x))) * 4.0; 16 | col.b += 1.0/distance(floor(uv.x / 2.0), 100.0 * cos(iTime * 0.2 + floor(uv.y))) * 4.0; 17 | col.r = col.b; 18 | col.g = col.b; 19 | 20 | FragColor = col; 21 | } -------------------------------------------------------------------------------- /source/AmiigoElements.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | 7 | namespace Amiigo::Elements { 8 | selectorContextMenu::selectorContextMenu(int x, int y, AmiiboEntry entry) : Arriba::Primitives::Quad(x, y, 0, 0, Arriba::Graphics::Pivot::centre) { 9 | name = "ContextMenu"; 10 | Arriba::activeLayer++; 11 | setColour({0, 0, 0, 1}); 12 | amiiboEntryGlobal = entry; 13 | // Spawn favorite button 14 | if (entry.name != "¤ Favorites" && entry.name != "¤ Back") { 15 | Arriba::Elements::Button* favoriteButton = new Arriba::Elements::Button(); 16 | favoriteButton->setParent(this); 17 | // Check if the amiibo is currently favorited 18 | std::string tempLine; 19 | std::ifstream fileStream("sdmc:/emuiibo/overlay/favorites.txt"); 20 | bool isFavorited = false; 21 | while (getline(fileStream, tempLine)) { 22 | if (tempLine == entry.path) { 23 | isFavorited = true; 24 | break; 25 | } 26 | } 27 | if (!isFavorited) 28 | favoriteButton->setText("Favorite"); 29 | else 30 | favoriteButton->setText("Unfavorite"); 31 | favoriteButton->tag = "ContextMenuButton"; 32 | // Handle adding amiibo to favorites 33 | if (!isFavorited) { 34 | favoriteButton->registerCallback([](){ 35 | mkdir("sdmc:/emuiibo/overlay/", 0); 36 | std::ofstream favFile("sdmc:/emuiibo/overlay/favorites.txt", std::ofstream::app); 37 | favFile << Amiigo::Elements::amiiboEntryGlobal.path << "\n"; 38 | favFile.close(); 39 | // Close the menu 40 | Arriba::highlightedObject = nullptr; 41 | }); 42 | } else { 43 | // Handle removing amiibo from favorites 44 | favoriteButton->registerCallback([](){ 45 | // Backup all paths to a vector, except for the one we're unfavoriting 46 | std::string tempLine; 47 | std::ifstream favFile("sdmc:/emuiibo/overlay/favorites.txt"); 48 | std::vector amiiboPaths; 49 | while (getline(favFile, tempLine)) if (Amiigo::Elements::amiiboEntryGlobal.path != tempLine) amiiboPaths.push_back(tempLine); 50 | favFile.close(); 51 | // Write the vector back 52 | std::ofstream reFavFile("sdmc:/emuiibo/overlay/favorites.txt", std::ofstream::trunc); 53 | for (unsigned int i = 0; i < amiiboPaths.size(); i++) { 54 | reFavFile << amiiboPaths[i] << "\n"; 55 | } 56 | reFavFile.close(); 57 | // Close menu 58 | Amiigo::UI::updateSelectorStrings(); 59 | Arriba::highlightedObject = nullptr; 60 | }); 61 | } 62 | } 63 | // Spawn new folder button 64 | if (Amiigo::UI::selectorPath != "Favorites") { 65 | Arriba::Elements::Button* newFolderButton = new Arriba::Elements::Button(); 66 | newFolderButton->setParent(this); 67 | newFolderButton->setText("New folder"); 68 | newFolderButton->tag = "ContextMenuButton"; 69 | // If pressed create a new folder 70 | newFolderButton->registerCallback([](){ 71 | // Get folder name from on screen keyboard 72 | SwkbdConfig kbinput; 73 | swkbdCreate(&kbinput, 0); 74 | swkbdConfigMakePresetDefault(&kbinput); 75 | swkbdConfigSetGuideText(&kbinput, "Enter folder name"); 76 | swkbdConfigSetInitialText(&kbinput, "New folder"); 77 | char *kbout = reinterpret_cast(malloc(256)); 78 | swkbdShow(&kbinput, kbout, 255); 79 | swkbdClose(&kbinput); 80 | std::string newFolderPath = Amiigo::UI::selectorPath + "/" + kbout; 81 | printf("%s\n", newFolderPath.c_str()); 82 | mkdir(newFolderPath.c_str(), 0); 83 | free(kbout); 84 | // Refresh list and close menu 85 | Amiigo::UI::updateSelectorStrings(); 86 | Arriba::highlightedObject = nullptr; 87 | }); 88 | } 89 | // Spawn delete button 90 | if (entry.name != "¤ Favorites" && Amiigo::UI::selectorPath != "Favorites" && entry.name != "¤ Back") { 91 | Arriba::Elements::Button* deleteButton = new Arriba::Elements::Button(); 92 | deleteButton->setParent(this); 93 | deleteButton->setText("Delete"); 94 | deleteButton->tag = "ContextMenuButton"; 95 | // If pressed delete the current entry and reload the list 96 | deleteButton->registerCallback([](){ 97 | fsdevDeleteDirectoryRecursively(amiiboEntryGlobal.path.c_str()); 98 | Amiigo::UI::updateSelectorStrings(); 99 | // Results in program abort. Idk why. Set highlighted to nullptr as workaround to close menu. 100 | // static_cast(Arriba::findObjectByName("ContextMenu"))->destroy(); 101 | Arriba::highlightedObject = nullptr; 102 | }); 103 | } 104 | // If no buttons were spawned then destory self 105 | buttonVector = Arriba::findObjectsByTag("ContextMenuButton"); 106 | if (buttonVector.size() == 0) closeMenu(); 107 | // Resize menu 108 | int menuHeight = 103 * buttonVector.size() + 3; 109 | int menuWidth = 306; 110 | bool tooFarX = false; 111 | bool tooFarY = false; 112 | tooFarX = transform.position.x + menuWidth > Arriba::Graphics::windowWidth; 113 | tooFarY = transform.position.y + menuHeight > Arriba::Graphics::windowHeight; 114 | Arriba::Graphics::Pivot pivotMode = Arriba::Graphics::Pivot::centre; 115 | 116 | if (tooFarX && tooFarY) { 117 | pivotMode = Arriba::Graphics::Pivot::bottomRight; 118 | } else if (tooFarX && !tooFarY) { 119 | pivotMode = Arriba::Graphics::Pivot::topRight; 120 | } else if (!tooFarX && tooFarY) { 121 | pivotMode = Arriba::Graphics::Pivot::bottomLeft; 122 | } else { 123 | pivotMode = Arriba::Graphics::Pivot::topLeft; 124 | } 125 | 126 | setDimensions(menuWidth, menuHeight, pivotMode); 127 | int topYDelta = menuHeight - top; 128 | // Position buttons 129 | for (unsigned int i = 0; i < buttonVector.size(); i++) { 130 | static_cast(buttonVector[i])->setDimensions(menuWidth-6, 100, Arriba::Graphics::Pivot::topLeft); 131 | buttonVector[i]->transform.position.y = static_cast(i*100 + (i+1)*3) - topYDelta; 132 | buttonVector[i]->transform.position.x = 3; 133 | Arriba::highlightedObject = buttonVector[0]; 134 | } 135 | } 136 | 137 | void selectorContextMenu::onFrame() { 138 | // We have to use buttonUp to detect B being pressed otherwise handleInput from AmiigoUI.h wil force the selector to go up a directory 139 | if ((Arriba::Input::touch.end && Arriba::highlightedObject == nullptr) || Arriba::Input::buttonUp(Arriba::Input::BButtonSwitch) || Arriba::highlightedObject == nullptr) { 140 | closeMenu(); 141 | return; 142 | } 143 | // Check if up / down is pressed 144 | if (Arriba::Input::buttonDown(Arriba::Input::DPadDown) || Arriba::Input::buttonDown(Arriba::Input::DPadUp)) { 145 | for (unsigned int i = 0; i < buttonVector.size(); i++) { 146 | if (Arriba::highlightedObject == buttonVector[i]) { 147 | if (Arriba::Input::buttonDown(Arriba::Input::DPadDown) && i != buttonVector.size()-1) { 148 | Arriba::highlightedObject = buttonVector[i+1]; 149 | // We don't want to repeatedly do this for each item in a single frame 150 | break; 151 | } 152 | if (Arriba::Input::buttonDown(Arriba::Input::DPadUp) && i != 0) Arriba::highlightedObject = buttonVector[i-1]; 153 | } 154 | } 155 | } 156 | } 157 | 158 | void selectorContextMenu::closeMenu() { 159 | destroy(); 160 | Arriba::activeLayer--; 161 | Arriba::highlightedObject = Arriba::findObjectByName("SelectorList"); 162 | } 163 | } // namespace Amiigo::Elements 164 | -------------------------------------------------------------------------------- /source/AmiigoSettings.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | #include 8 | 9 | namespace Amiigo::Settings { 10 | void loadSettings() { 11 | // Parse main config 12 | if (checkIfFileExists("sdmc:/config/amiigo/settings.json")) { 13 | // Load the file from the sd 14 | std::ifstream fileStream("sdmc:/config/amiigo/settings.json"); 15 | std::string tempLine; 16 | std::string fileString; 17 | while (getline(fileStream, tempLine)) fileString += tempLine; 18 | 19 | // Parse the file 20 | nlohmann::json settingsJson = nlohmann::json::parse(fileString); 21 | fileStream.close(); 22 | if (settingsJson.contains("saveAmiibosToCategory")) saveAmiibosToCategory = settingsJson["saveAmiibosToCategory"].get(); 23 | if (settingsJson.contains("useRandomisedUUID")) useRandomisedUUID = settingsJson["useRandomisedUUID"].get(); 24 | if (settingsJson.contains("timeToCheckUpdate")) updateTime = settingsJson["timeToCheckUpdate"].get(); 25 | if (settingsJson.contains("categoryMode")) categoryMode = settingsJson["categoryMode"].get() % Amiigo::Settings::categoryModes::categoryCount; 26 | 27 | // Back compat with versions < 2.2.0 28 | if (!settingsJson.contains("categoryMode") && settingsJson.contains("saveAmiibosToCategory")) { 29 | if (saveAmiibosToCategory) 30 | Amiigo::Settings::categoryMode = Amiigo::Settings::saveByGameName; 31 | else 32 | Amiigo::Settings::categoryMode = Amiigo::Settings::saveToRoot; 33 | saveSettings(); 34 | } 35 | if (!settingsJson.contains("useRandomisedUUID")) saveSettings(); 36 | } 37 | 38 | // Parse theme config 39 | if (checkIfFileExists("sdmc:/config/amiigo/theme.json")) { 40 | // Load the theme file 41 | std::ifstream fileStream("sdmc:/config/amiigo/theme.json"); 42 | std::string tempLine; 43 | std::string fileString; 44 | while (getline(fileStream, tempLine)) fileString += tempLine; 45 | 46 | // Parse the file to load the colours 47 | nlohmann::json themeJson = nlohmann::json::parse(fileString); 48 | fileStream.close(); 49 | 50 | // Status bar 51 | if (themeJson.contains("StatusBar_Neutral_R")) Colour::statusBar.r = themeJson["StatusBar_Neutral_R"].get(); 52 | if (themeJson.contains("StatusBar_Neutral_G")) Colour::statusBar.g = themeJson["StatusBar_Neutral_G"].get(); 53 | if (themeJson.contains("StatusBar_Neutral_B")) Colour::statusBar.b = themeJson["StatusBar_Neutral_B"].get(); 54 | if (themeJson.contains("StatusBar_Neutral_A")) Colour::statusBar.a = themeJson["StatusBar_Neutral_A"].get(); 55 | 56 | // Amiibo list neutral 57 | if (themeJson.contains("SelectorList_Neutral_R")) Colour::listNeutral.r = themeJson["SelectorList_Neutral_R"].get(); 58 | if (themeJson.contains("SelectorList_Neutral_G")) Colour::listNeutral.g = themeJson["SelectorList_Neutral_G"].get(); 59 | if (themeJson.contains("SelectorList_Neutral_B")) Colour::listNeutral.b = themeJson["SelectorList_Neutral_B"].get(); 60 | if (themeJson.contains("SelectorList_Neutral_A")) Colour::listNeutral.a = themeJson["SelectorList_Neutral_A"].get(); 61 | // Amiibo list highlightA 62 | if (themeJson.contains("SelectorList_HighlightA_R")) Colour::listHighlightA.r = themeJson["SelectorList_HighlightA_R"].get(); 63 | if (themeJson.contains("SelectorList_HighlightA_G")) Colour::listHighlightA.g = themeJson["SelectorList_HighlightA_G"].get(); 64 | if (themeJson.contains("SelectorList_HighlightA_B")) Colour::listHighlightA.b = themeJson["SelectorList_HighlightA_B"].get(); 65 | if (themeJson.contains("SelectorList_HighlightA_A")) Colour::listHighlightA.a = themeJson["SelectorList_HighlightA_A"].get(); 66 | // Amiibo list highlightB 67 | if (themeJson.contains("SelectorList_HighlightB_R")) Colour::listHighlightB.r = themeJson["SelectorList_HighlightB_R"].get(); 68 | if (themeJson.contains("SelectorList_HighlightB_G")) Colour::listHighlightB.g = themeJson["SelectorList_HighlightB_G"].get(); 69 | if (themeJson.contains("SelectorList_HighlightB_B")) Colour::listHighlightB.b = themeJson["SelectorList_HighlightB_B"].get(); 70 | if (themeJson.contains("SelectorList_HighlightB_A")) Colour::listHighlightB.a = themeJson["SelectorList_HighlightB_A"].get(); 71 | 72 | // Amiigo store neutral 73 | if (themeJson.contains("AmiigoStore_Neutral_R")) Colour::makerNeutral.r = themeJson["AmiigoStore_Neutral_R"].get(); 74 | if (themeJson.contains("AmiigoStore_Neutral_G")) Colour::makerNeutral.g = themeJson["AmiigoStore_Neutral_G"].get(); 75 | if (themeJson.contains("AmiigoStore_Neutral_B")) Colour::makerNeutral.b = themeJson["AmiigoStore_Neutral_B"].get(); 76 | if (themeJson.contains("AmiigoStore_Neutral_A")) Colour::makerNeutral.a = themeJson["AmiigoStore_Neutral_A"].get(); 77 | // Amiigo store highlightA 78 | if (themeJson.contains("AmiigoStore_HighlightA_R")) Colour::makerHighlightA.r = themeJson["AmiigoStore_HighlightA_R"].get(); 79 | if (themeJson.contains("AmiigoStore_HighlightA_G")) Colour::makerHighlightA.g = themeJson["AmiigoStore_HighlightA_G"].get(); 80 | if (themeJson.contains("AmiigoStore_HighlightA_B")) Colour::makerHighlightA.b = themeJson["AmiigoStore_HighlightA_B"].get(); 81 | if (themeJson.contains("AmiigoStore_HighlightA_A")) Colour::makerHighlightA.a = themeJson["AmiigoStore_HighlightA_A"].get(); 82 | // Amiigo store highlightB 83 | if (themeJson.contains("AmiigoStore_HighlightB_R")) Colour::makerHighlightB.r = themeJson["AmiigoStore_HighlightB_R"].get(); 84 | if (themeJson.contains("AmiigoStore_HighlightB_G")) Colour::makerHighlightB.g = themeJson["AmiigoStore_HighlightB_G"].get(); 85 | if (themeJson.contains("AmiigoStore_HighlightB_B")) Colour::makerHighlightB.b = themeJson["AmiigoStore_HighlightB_B"].get(); 86 | if (themeJson.contains("AmiigoStore_HighlightB_A")) Colour::makerHighlightB.a = themeJson["AmiigoStore_HighlightB_A"].get(); 87 | 88 | // Amiigo settings neutral 89 | if (themeJson.contains("Settings_Neutral_R")) Colour::settingsNeutral.r = themeJson["Settings_Neutral_R"].get(); 90 | if (themeJson.contains("Settings_Neutral_G")) Colour::settingsNeutral.g = themeJson["Settings_Neutral_G"].get(); 91 | if (themeJson.contains("Settings_Neutral_B")) Colour::settingsNeutral.b = themeJson["Settings_Neutral_B"].get(); 92 | if (themeJson.contains("Settings_Neutral_A")) Colour::settingsNeutral.a = themeJson["Settings_Neutral_A"].get(); 93 | // Amiigo settings highlightA 94 | if (themeJson.contains("Settings_HighlightA_R")) Colour::settingsHighlightA.r = themeJson["Settings_HighlightA_R"].get(); 95 | if (themeJson.contains("Settings_HighlightA_G")) Colour::settingsHighlightA.g = themeJson["Settings_HighlightA_G"].get(); 96 | if (themeJson.contains("Settings_HighlightA_B")) Colour::settingsHighlightA.b = themeJson["Settings_HighlightA_B"].get(); 97 | if (themeJson.contains("Settings_HighlightA_A")) Colour::settingsHighlightA.a = themeJson["Settings_HighlightA_A"].get(); 98 | // Amiigo settings highlightB 99 | if (themeJson.contains("Settings_HighlightB_R")) Colour::settingsHighlightB.r = themeJson["Settings_HighlightB_R"].get(); 100 | if (themeJson.contains("Settings_HighlightB_G")) Colour::settingsHighlightB.g = themeJson["Settings_HighlightB_G"].get(); 101 | if (themeJson.contains("Settings_HighlightB_B")) Colour::settingsHighlightB.b = themeJson["Settings_HighlightB_B"].get(); 102 | if (themeJson.contains("Settings_HighlightB_A")) Colour::settingsHighlightB.a = themeJson["Settings_HighlightB_A"].get(); 103 | } 104 | } 105 | 106 | void saveSettings() { 107 | if (checkIfFileExists("sdmc:/config/amiigo/settings.json")) std::remove("sdmc:/config/amiigo/settings.json"); 108 | std::ofstream fileStream("sdmc:/config/amiigo/settings.json"); 109 | nlohmann::json settingsJson; 110 | settingsJson["timeToCheckUpdate"] = updateTime; 111 | settingsJson["categoryMode"] = categoryMode; 112 | settingsJson["useRandomisedUUID"] = useRandomisedUUID; 113 | fileStream << settingsJson << std::endl; 114 | fileStream.close(); 115 | } 116 | } // namespace Amiigo::Settings 117 | -------------------------------------------------------------------------------- /source/AmiigoUI.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | #include 6 | #include 7 | 8 | #include 9 | #include 10 | 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | 18 | namespace Amiigo::UI { 19 | void initUI() { 20 | Amiigo::Settings::loadSettings(); 21 | Arriba::Colour::neutral = Amiigo::Settings::Colour::listNeutral; 22 | Arriba::Colour::highlightA = Amiigo::Settings::Colour::listHighlightA; 23 | Arriba::Colour::highlightB = Amiigo::Settings::Colour::listHighlightB; 24 | if (!checkIfFileExists("sdmc:/config/amiigo/API.json") || !checkIfFileExists("sdmc:/atmosphere/contents/0100000000000352/exefs.nsp")) initSplash(); 25 | if (emu::IsAvailable()) emu::Initialize(); 26 | initSceneSwitcher(); 27 | initSelector(); 28 | initMaker(); 29 | initSettings(); 30 | Arriba::highlightedObject = selectorButton; 31 | // Cache these now for performance gains later 32 | lists = Arriba::findObjectsByTag("List"); 33 | buttons = Arriba::findObjectsByTag("SwitcherButton"); 34 | // Check if a new Amiigo version is available 35 | if (checkForUpdates()) { 36 | settingsButton->setText("Update"); 37 | Arriba::findObjectByName("UpdaterButton")->enabled = true; 38 | } 39 | } 40 | 41 | void initSplash() { 42 | Arriba::activeLayer++; 43 | splashScene = new Arriba::Primitives::Quad(0, 0, Arriba::Graphics::windowWidth, Arriba::Graphics::windowHeight, Arriba::Graphics::Pivot::topLeft); 44 | splashScene->setColour({0.25, 0.25, 0.25, 0.95}); 45 | // Title text 46 | Arriba::Primitives::Text* titleText = new Arriba::Primitives::Text("Amiigo", 128); 47 | titleText->transform.position = {Arriba::Graphics::windowWidth/2, Arriba::Graphics::windowHeight/2 - 140, 0}; 48 | titleText->setColour({1, 1, 1, 1}); 49 | titleText->setParent(splashScene); 50 | // By text 51 | Arriba::Primitives::Text* byText = new Arriba::Primitives::Text("by CompSciOrBust", 64); 52 | byText->transform.position = {Arriba::Graphics::windowWidth/2, Arriba::Graphics::windowHeight/2 - 15, 0}; 53 | byText->setColour({1, 1, 1, 1}); 54 | byText->setParent(splashScene); 55 | // Doing text 56 | Arriba::Primitives::Text* doingText = new Arriba::Primitives::Text("", 48); 57 | doingText->transform.position = {Arriba::Graphics::windowWidth/2, Arriba::Graphics::windowHeight/2 + 160, 0}; 58 | doingText->setColour({1, 1, 1, 1}); 59 | doingText->setParent(splashScene); 60 | std::thread initThread(firstTimeSetup); 61 | // Hide other elements 62 | if (settingsScene) settingsScene->enabled = false; 63 | if (sceneSwitcher) sceneSwitcher->enabled = false; 64 | while (!checkIfFileExists("sdmc:/config/amiigo/API.json") || !checkIfFileExists("sdmc:/atmosphere/contents/0100000000000352/exefs.nsp") || checkIfFileExists("sdmc:/config/amiigo/update.flag")) { 65 | splashScene->setColour({(sin(Arriba::time)+1)/4, (cos(Arriba::time)+1)/4, 0.5, 0.95}); 66 | Arriba::findObjectByName("AmiigoBG")->renderer->thisShader.setFloat1("iTime", Arriba::time); 67 | if (!hasNetworkConnection()) doingText->setText("Waiting for internet connection..."); 68 | else if (!checkIfFileExists("sdmc:/config/amiigo/API.json")) doingText->setText("Caching API data..."); 69 | else if (!checkIfFileExists("sdmc:/atmosphere/contents/0100000000000352/exefs.nsp")) doingText->setText("Installing Emuiibo..."); 70 | else if (checkIfFileExists("sdmc:/config/amiigo/update.flag")) doingText->setText("Updating Amiigo..."); 71 | Arriba::drawFrame(); 72 | } 73 | initThread.join(); 74 | splashScene->destroy(); 75 | if (settingsScene) settingsScene->enabled = true; 76 | if (sceneSwitcher) sceneSwitcher->enabled = true; 77 | Arriba::activeLayer--; 78 | } 79 | 80 | void initSceneSwitcher() { 81 | // Set up divider quads 82 | Arriba::Primitives::Quad* div1 = new Arriba::Primitives::Quad(Arriba::Graphics::windowWidth - switcherWidth - 1, statusHeight, 5, switcherHeight, Arriba::Graphics::Pivot::topLeft); 83 | div1->setColour({0, 0, 0, 1}); 84 | Arriba::Primitives::Quad* div2 = new Arriba::Primitives::Quad(0, statusHeight - 1, Arriba::Graphics::windowWidth, 5, Arriba::Graphics::Pivot::topLeft); 85 | div2->setColour({0, 0, 0, 1}); 86 | int buttomDivX = Arriba::Graphics::windowWidth - switcherWidth; 87 | Arriba::Primitives::Quad* div3 = new Arriba::Primitives::Quad(buttomDivX, statusHeight + (switcherHeight/4) - 1, switcherWidth, 5, Arriba::Graphics::Pivot::topLeft); 88 | div3->setColour({0, 0, 0, 1}); 89 | Arriba::Primitives::Quad* div4 = new Arriba::Primitives::Quad(buttomDivX, statusHeight + (switcherHeight/4)*2 - 1, switcherWidth, 5, Arriba::Graphics::Pivot::topLeft); 90 | div4->setColour({0, 0, 0, 1}); 91 | Arriba::Primitives::Quad* div5 = new Arriba::Primitives::Quad(buttomDivX, statusHeight + (switcherHeight/4)*3 - 1, switcherWidth, 5, Arriba::Graphics::Pivot::topLeft); 92 | div5->setColour({0, 0, 0, 1}); 93 | // Set up status bar 94 | Arriba::Primitives::Quad* statusBar = new Arriba::Primitives::Quad(0, 0, Arriba::Graphics::windowWidth, statusHeight - 1, Arriba::Graphics::Pivot::topLeft); 95 | statusBar->setColour(Amiigo::Settings::Colour::statusBar); 96 | Arriba::Primitives::Text* statusText = new Arriba::Primitives::Text("Amiigo + Arriba", 34); 97 | statusText->name = "StatusBarText"; 98 | statusText->setDimensions(statusText->width, statusText->height, Arriba::Graphics::centre); 99 | statusText->transform.position = {statusBar->width/2, statusBar->height/2, 0}; 100 | statusText->setParent(statusBar); 101 | // Set up switcher quad 102 | sceneSwitcher = new Arriba::Primitives::Quad(Arriba::Graphics::windowWidth, statusHeight, switcherWidth, switcherHeight, Arriba::Graphics::Pivot::topRight); 103 | sceneSwitcher->setColour({0, 0, 0, 0}); 104 | // Set up Amiibo list button 105 | selectorButton = new Arriba::Elements::Button(); 106 | selectorButton->setText("My Amiibo"); 107 | selectorButton->setDimensions(switcherWidth, switcherHeight/4 - 1, Arriba::Graphics::Pivot::topRight); 108 | selectorButton->setParent(sceneSwitcher); 109 | selectorButton->name = "SelectorButton"; 110 | selectorButton->tag = "SwitcherButton"; 111 | selectorButton->registerCallback(switcherPressed); 112 | // Set up Amiigo maker button 113 | makerButton = new Arriba::Elements::Button(); 114 | makerButton->setText("Amiigo Store"); 115 | makerButton->transform.position.y = selectorButton->height + 1; 116 | makerButton->setDimensions(switcherWidth, switcherHeight/4 - 1, Arriba::Graphics::Pivot::topRight); 117 | makerButton->setParent(sceneSwitcher); 118 | makerButton->name = "MakerButton"; 119 | makerButton->tag = "SwitcherButton"; 120 | makerButton->registerCallback(switcherPressed); 121 | // Set up settings button 122 | settingsButton = new Arriba::Elements::Button(); 123 | settingsButton->setText("Settings"); 124 | settingsButton->transform.position.y = makerButton->height + makerButton->transform.position.y + 1; 125 | settingsButton->setDimensions(switcherWidth, switcherHeight/4 - 1, Arriba::Graphics::Pivot::topRight); 126 | settingsButton->setParent(sceneSwitcher); 127 | settingsButton->name = "SettingsButton"; 128 | settingsButton->tag = "SwitcherButton"; 129 | settingsButton->registerCallback(switcherPressed); 130 | // Set up exit button 131 | exitButton = new Arriba::Elements::Button(); 132 | exitButton->setText("Exit"); 133 | exitButton->transform.position.y = settingsButton->height + settingsButton->transform.position.y + 1; 134 | exitButton->setDimensions(switcherWidth, switcherHeight/4, Arriba::Graphics::Pivot::topRight); 135 | exitButton->setParent(sceneSwitcher); 136 | exitButton->name = "ExitButton"; 137 | exitButton->tag = "SwitcherButton"; 138 | exitButton->registerCallback([](){isRunning = 0;}); 139 | } 140 | 141 | void initSelector() { 142 | // Set up the list 143 | selectorList = new Arriba::Elements::InertialList(0, statusHeight, Arriba::Graphics::windowWidth - switcherWidth - 1, Arriba::Graphics::windowHeight - statusHeight, {}); 144 | updateSelectorStrings(); 145 | selectorList->name = "SelectorList"; 146 | selectorList->tag = "List"; 147 | selectorList->registerCallback(selectorInput); 148 | selectorList->registerAltCallback(selectorContextMenuSpawner); 149 | } 150 | 151 | void initMaker() { 152 | seriesList = getListOfSeries(); 153 | makerList = new Arriba::Elements::InertialList(0, statusHeight, Arriba::Graphics::windowWidth - switcherWidth - 1, Arriba::Graphics::windowHeight - statusHeight, seriesList); 154 | makerList->registerCallback(makerInput); 155 | makerList->name = "MakerList"; 156 | makerList->tag = "List"; 157 | makerList->enabled = false; 158 | } 159 | 160 | void initSettings() { 161 | const int buttonHeight = 100; 162 | const int buttonCount = 4 + 1; 163 | settingsScene = new Arriba::Primitives::Quad(0, statusHeight, Arriba::Graphics::windowWidth - switcherWidth - 1, Arriba::Graphics::windowHeight - statusHeight, Arriba::Graphics::Pivot::topLeft); 164 | // Not a list but pretending makes scene switching easier 165 | settingsScene->name = "SettingsScene"; 166 | settingsScene->tag = "List"; 167 | settingsScene->enabled = false; 168 | settingsScene->setColour({0, 0, 0, 0}); 169 | // Toggle category save button 170 | Arriba::Elements::Button* categoryButton = new Arriba::Elements::Button(); 171 | categoryButton->setParent(settingsScene); 172 | categoryButton->setDimensions(550, buttonHeight, Arriba::Graphics::Pivot::centre); 173 | categoryButton->transform.position = {settingsScene->width / 2 + 165, settingsScene->height * 1/buttonCount, 0}; 174 | switch (Amiigo::Settings::categoryMode) { 175 | case Amiigo::Settings::categoryModes::saveToRoot: 176 | categoryButton->setText("Save to game name"); 177 | break; 178 | 179 | case Amiigo::Settings::categoryModes::saveByGameName: 180 | categoryButton->setText("Save to Amiibo series"); 181 | break; 182 | 183 | case Amiigo::Settings::categoryModes::saveByAmiiboSeries: 184 | categoryButton->setText("Save to current folder"); 185 | break; 186 | 187 | case Amiigo::Settings::categoryModes::saveByCurrentFolder: 188 | categoryButton->setText("Save to root"); 189 | break; 190 | 191 | default: 192 | categoryButton->setText("Error"); 193 | break; 194 | } 195 | categoryButton->name = "CategorySettingsButton"; 196 | // Callback to save setting 197 | categoryButton->registerCallback([](){ 198 | Amiigo::Settings::categoryMode = (Amiigo::Settings::categoryMode+1) % Amiigo::Settings::categoryModes::categoryCount; 199 | printf("%d", Amiigo::Settings::categoryMode); 200 | Amiigo::Settings::saveSettings(); 201 | switch (Amiigo::Settings::categoryMode) { 202 | case Amiigo::Settings::categoryModes::saveToRoot: 203 | static_cast(Arriba::findObjectByName("CategorySettingsButton"))->setText("Save to game name"); 204 | static_cast(Arriba::findObjectByName("StatusBarText"))->setText("Amiibos will save to sdmc:/emuiibo/amiibo"); 205 | break; 206 | 207 | case Amiigo::Settings::categoryModes::saveByGameName: 208 | static_cast(Arriba::findObjectByName("CategorySettingsButton"))->setText("Save to Amiibo series"); 209 | static_cast(Arriba::findObjectByName("StatusBarText"))->setText("Amiibos will save to sdmc:/emuiibo/game name"); 210 | break; 211 | 212 | case Amiigo::Settings::categoryModes::saveByAmiiboSeries: 213 | static_cast(Arriba::findObjectByName("CategorySettingsButton"))->setText("Save to current folder"); 214 | static_cast(Arriba::findObjectByName("StatusBarText"))->setText("Amiibos will save to sdmc:/emuiibo/amiibo series"); 215 | break; 216 | 217 | case Amiigo::Settings::categoryModes::saveByCurrentFolder: 218 | static_cast(Arriba::findObjectByName("CategorySettingsButton"))->setText("Save to root"); 219 | static_cast(Arriba::findObjectByName("StatusBarText"))->setText("Amiibos will save to the current location"); 220 | break; 221 | 222 | default: 223 | static_cast(Arriba::findObjectByName("CategorySettingsButton"))->setText("Error"); 224 | static_cast(Arriba::findObjectByName("StatusBarText"))->setText("Error, uknown category mode"); 225 | break; 226 | } 227 | }); 228 | // Update API cache button 229 | Arriba::Elements::Button* apiUpdateButton = new Arriba::Elements::Button(); 230 | apiUpdateButton->setParent(settingsScene); 231 | apiUpdateButton->setDimensions(550, buttonHeight, Arriba::Graphics::Pivot::centre); 232 | apiUpdateButton->transform.position = {settingsScene->width / 2 + 165, settingsScene->height * 2/buttonCount, 0}; 233 | apiUpdateButton->setText("Update API cache"); 234 | apiUpdateButton->name = "UpdateAPIButton"; 235 | // Callback for updating API cache 236 | apiUpdateButton->registerCallback([](){ 237 | if (!hasNetworkConnection()) { 238 | static_cast(Arriba::findObjectByName("StatusBarText"))->setText("No network connection!"); 239 | } else { 240 | if (checkIfFileExists("sdmc:/config/amiigo/API.json")) remove("sdmc:/config/amiigo/API.json"); 241 | // This is kinda jank but I'm lazy and this won't be used often 242 | initSplash(); 243 | seriesList = getListOfSeries(); 244 | } 245 | }); 246 | // Toggle random UUIDs button 247 | Arriba::Elements::Button* randomUUIDButton = new Arriba::Elements::Button(); 248 | randomUUIDButton->setParent(settingsScene); 249 | randomUUIDButton->setDimensions(550, buttonHeight, Arriba::Graphics::Pivot::centre); 250 | randomUUIDButton->transform.position = {settingsScene->width / 2 + 165, settingsScene->height * 3/buttonCount, 0}; 251 | switch (Amiigo::Settings::useRandomisedUUID) { 252 | case true: 253 | randomUUIDButton->setText("Disable random UUID"); 254 | break; 255 | 256 | case false: 257 | randomUUIDButton->setText("Enable random UUID"); 258 | break; 259 | 260 | default: 261 | randomUUIDButton->setText("UUID Status error"); 262 | break; 263 | } 264 | randomUUIDButton->name = "ToggleRandomUUIDButton"; 265 | randomUUIDButton->registerCallback([](){ 266 | Amiigo::Settings::useRandomisedUUID = !Amiigo::Settings::useRandomisedUUID; 267 | Amiigo::Settings::saveSettings(); 268 | switch (Amiigo::Settings::useRandomisedUUID) { 269 | case true: 270 | static_cast(Arriba::findObjectByName("ToggleRandomUUIDButton"))->setText("Disable random UUID"); 271 | static_cast(Arriba::findObjectByName("StatusBarText"))->setText("Amiibos will now generate with random UUIDs"); 272 | break; 273 | 274 | case false: 275 | static_cast(Arriba::findObjectByName("ToggleRandomUUIDButton"))->setText("Enable random UUID"); 276 | static_cast(Arriba::findObjectByName("StatusBarText"))->setText("Amiibos will now generate with static UUIDs"); 277 | break; 278 | 279 | default: 280 | static_cast(Arriba::findObjectByName("ToggleRandomUUIDButton"))->setText("UUID Status error"); 281 | break; 282 | } 283 | }); 284 | // Check for updates button 285 | Arriba::Elements::Button* updaterButton = new Arriba::Elements::Button(); 286 | updaterButton->setParent(settingsScene); 287 | updaterButton->setDimensions(550, buttonHeight, Arriba::Graphics::Pivot::centre); 288 | updaterButton->transform.position = {settingsScene->width / 2 + 165, settingsScene->height * 4/buttonCount, 0}; 289 | updaterButton->setText("Update Amiigo"); 290 | updaterButton->name = "UpdaterButton"; 291 | updaterButton->enabled = false; 292 | // Callback for updating Amiigo 293 | updaterButton->registerCallback([](){ 294 | if (!hasNetworkConnection()) { 295 | static_cast(Arriba::findObjectByName("StatusBarText"))->setText("No network connection!"); 296 | } else { 297 | std::ofstream fileStream("sdmc:/config/amiigo/update.flag"); 298 | fileStream.close(); 299 | initSplash(); 300 | } 301 | }); 302 | 303 | // Credits Quad 304 | Arriba::Primitives::Quad* creditsQuad = new Arriba::Primitives::Quad(0, 0, 330, Arriba::Graphics::windowHeight - statusHeight, Arriba::Graphics::Pivot::topLeft); 305 | creditsQuad->setParent(settingsScene); 306 | creditsQuad->setColour({0, 0, 0, 0.9}); 307 | int yOffset = 0; 308 | // et Emuiibo version 309 | char emuVer[12]; 310 | emu::Version emuiiboVersion = emu::GetVersion(); 311 | sprintf(emuVer, "%u.%u.%u", emuiiboVersion.major, emuiiboVersion.minor, emuiiboVersion.micro); 312 | Amiigo::Settings::emuiiboVersionText = emuVer; 313 | // Credits text 314 | Arriba::Primitives::Text* creditsTitleText = new Arriba::Primitives::Text("Credits", 64); 315 | creditsTitleText->setColour({0, 0.7, 1, 1}); 316 | creditsTitleText->setParent(creditsQuad); 317 | creditsTitleText->transform.position = {creditsQuad->width/2, yOffset += creditsTitleText->height + 30, 0}; 318 | for (int i = 0; i < 5; i++) { 319 | std::string titleText = "Place holder"; 320 | std::string nameText = "Place holder"; 321 | switch (i) { 322 | case 0: 323 | titleText = "Developer"; 324 | nameText = "CompSciOrBust"; 325 | break; 326 | case 1: 327 | titleText = "Emuiibo " + Amiigo::Settings::emuiiboVersionText; 328 | nameText = "XorTroll"; 329 | break; 330 | case 2: 331 | titleText = "Contribuyente"; 332 | nameText = "Kronos2308"; 333 | break; 334 | case 3: 335 | titleText = "The Pizza Guy"; 336 | nameText = "Za"; 337 | break; 338 | case 4: 339 | titleText = "Amiibo API"; 340 | nameText = "N3evin"; 341 | break; 342 | case 5: 343 | titleText = "Beta testers"; 344 | nameText = "Too many to name <3"; 345 | break; 346 | } 347 | Arriba::Primitives::Text* titleTextObject = new Arriba::Primitives::Text(titleText.c_str(), 38); 348 | titleTextObject->setParent(creditsQuad); 349 | titleTextObject->transform.position = {creditsQuad->width/2, yOffset += titleTextObject->height + 20, 0}; 350 | Arriba::Primitives::Text* nameTextObject = new Arriba::Primitives::Text(nameText.c_str(), 28); 351 | nameTextObject->setParent(creditsQuad); 352 | nameTextObject->transform.position = {creditsQuad->width/2, yOffset += nameTextObject->height + 10, 0}; 353 | titleTextObject->setColour({0, 0.7, 1, 1}); 354 | nameTextObject->setColour({0, 0.7, 1, 1}); 355 | } 356 | } 357 | 358 | void handleInput() { 359 | // We only want to handle input for the base layer of the UI 360 | if (Arriba::activeLayer != 0) return; 361 | // If no object is selected 362 | if (Arriba::highlightedObject == nullptr) { 363 | if (Arriba::Input::buttonDown(Arriba::Input::controllerButton(Arriba::Input::DPadRight | Arriba::Input::DPadLeft | Arriba::Input::DPadUp | Arriba::Input::DPadDown))) Arriba::highlightedObject = selectorButton; 364 | } 365 | // If one of the lists are selected 366 | for (size_t i = 0; i < lists.size(); i++) { 367 | // Right pressed 368 | if (Arriba::highlightedObject == lists[i] && Arriba::Input::buttonDown(Arriba::Input::DPadRight)) { 369 | Arriba::highlightedObject = selectorButton; 370 | return; 371 | } 372 | // Selector list is selected 373 | if (lists[i]->name == "SelectorList" && lists[i]->enabled) { 374 | if (Arriba::Input::buttonDown(Arriba::Input::BButtonSwitch) && selectorPath != "sdmc:/emuiibo/amiibo") { 375 | selectorPath = selectorPath.substr(0, selectorPath.find_last_of("/")); 376 | if (selectorPath.length() < sizeof("sdmc:/emuiibo/amiibo")) selectorPath = "sdmc:/emuiibo/amiibo"; 377 | updateSelectorStrings(); 378 | } 379 | if (Arriba::Input::buttonDown(Arriba::Input::XButtonSwitch)) { 380 | switch (emu::GetEmulationStatus()) { 381 | case emu::EmulationStatus::On: 382 | emu::ResetActiveVirtualAmiibo(); 383 | emu::SetEmulationStatus(emu::EmulationStatus::Off); 384 | static_cast(Arriba::findObjectByName("StatusBarText"))->setText("Emuiibo disabled"); 385 | break; 386 | case emu::EmulationStatus::Off: 387 | emu::SetEmulationStatus(emu::EmulationStatus::On); 388 | static_cast(Arriba::findObjectByName("StatusBarText"))->setText("Emuiibo enabled"); 389 | break; 390 | default: 391 | static_cast(Arriba::findObjectByName("StatusBarText"))->setText("Error: Unkown emulation status!"); 392 | break; 393 | } 394 | } 395 | // Maker list is selected 396 | } else if (lists[i]->name == "MakerList" && lists[i]->enabled) { 397 | if (Arriba::Input::buttonDown(Arriba::Input::BButtonSwitch) && makerIsInCategory) { 398 | static_cast(lists[i])->updateStrings(seriesList); 399 | makerIsInCategory = false; 400 | } 401 | // Settings is selected 402 | } else if (lists[i]->name == "SettingsScene" && lists[i]->enabled) { 403 | // Left / right pressed 404 | if (Arriba::Input::buttonDown(Arriba::Input::DPadRight) && Arriba::highlightedObject->tag != "SwitcherButton") Arriba::highlightedObject = selectorButton; 405 | if (Arriba::Input::buttonDown(Arriba::Input::DPadLeft) && Arriba::highlightedObject->tag == "SwitcherButton") Arriba::highlightedObject = Arriba::findObjectByName("CategorySettingsButton"); 406 | // Up pressed 407 | if (Arriba::Input::buttonDown(Arriba::Input::DPadUp)) { 408 | if (Arriba::highlightedObject == Arriba::findObjectByName("UpdateAPIButton")) Arriba::highlightedObject = Arriba::findObjectByName("CategorySettingsButton"); 409 | else if (Arriba::highlightedObject == Arriba::findObjectByName("ToggleRandomUUIDButton")) Arriba::highlightedObject = Arriba::findObjectByName("UpdateAPIButton"); 410 | else if (Arriba::highlightedObject == Arriba::findObjectByName("UpdaterButton")) Arriba::highlightedObject = Arriba::findObjectByName("ToggleRandomUUIDButton"); 411 | } 412 | // Down pressed 413 | if (Arriba::Input::buttonDown(Arriba::Input::DPadDown)) { 414 | if (Arriba::highlightedObject == Arriba::findObjectByName("CategorySettingsButton")) Arriba::highlightedObject = Arriba::findObjectByName("UpdateAPIButton"); 415 | else if (Arriba::highlightedObject == Arriba::findObjectByName("UpdateAPIButton")) Arriba::highlightedObject = Arriba::findObjectByName("ToggleRandomUUIDButton"); 416 | else if (Arriba::highlightedObject == Arriba::findObjectByName("ToggleRandomUUIDButton") && Arriba::findObjectByName("UpdaterButton")->enabled) Arriba::highlightedObject = Arriba::findObjectByName("UpdaterButton"); 417 | } 418 | } 419 | } 420 | // If one of the switcher buttons are selected 421 | for (size_t i = 0; i < buttons.size(); i++) { 422 | if (Arriba::highlightedObject == buttons[i]) { 423 | // If left is pressed switch to whichever list is enabled 424 | if (Arriba::Input::buttonDown(Arriba::Input::DPadLeft)) { 425 | for (size_t j = 0; j < lists.size(); j++) { 426 | if (lists[j]->enabled) Arriba::highlightedObject = lists[j]; 427 | } 428 | } else if (Arriba::Input::buttonDown(Arriba::Input::DPadUp)) { // If up is pressed go to whichever button is above in the switcher 429 | if (buttons[i]->name == "MakerButton") Arriba::highlightedObject = selectorButton; 430 | else if (buttons[i]->name == "SettingsButton") Arriba::highlightedObject = makerButton; 431 | else if (buttons[i]->name == "ExitButton") Arriba::highlightedObject = settingsButton; 432 | } else if (Arriba::Input::buttonDown(Arriba::Input::DPadDown)) { // If down button is pressed go to whichever button is below in the switcher 433 | if (buttons[i]->name == "SelectorButton") Arriba::highlightedObject = makerButton; 434 | else if (buttons[i]->name == "MakerButton") Arriba::highlightedObject = settingsButton; 435 | else if (buttons[i]->name == "SettingsButton") Arriba::highlightedObject = exitButton; 436 | } 437 | break; 438 | } 439 | } 440 | } 441 | 442 | void switcherPressed() { 443 | // Disable all the lists 444 | for (size_t i = 0; i < lists.size(); i++) lists[i]->enabled = false; 445 | // Enable a list depending on which button was pressed 446 | if (Arriba::highlightedObject == selectorButton) { 447 | selectorList->enabled = true; 448 | selectorPath = "sdmc:/emuiibo/amiibo"; 449 | updateSelectorStrings(); 450 | Arriba::Colour::neutral = Amiigo::Settings::Colour::listNeutral; 451 | Arriba::Colour::highlightA = Amiigo::Settings::Colour::listHighlightA; 452 | Arriba::Colour::highlightB = Amiigo::Settings::Colour::listHighlightB; 453 | static_cast(Arriba::findObjectByName("StatusBarText"))->setText("Amiigo + Arriba"); 454 | } else if (Arriba::highlightedObject == makerButton) { 455 | makerList->enabled = true; 456 | if (makerIsInCategory) { 457 | makerIsInCategory = false; 458 | } 459 | makerList->updateStrings(seriesList); 460 | Arriba::Colour::neutral = Amiigo::Settings::Colour::makerNeutral; 461 | Arriba::Colour::highlightA = Amiigo::Settings::Colour::makerHighlightA; 462 | Arriba::Colour::highlightB = Amiigo::Settings::Colour::makerHighlightB; 463 | static_cast(Arriba::findObjectByName("StatusBarText"))->setText("Amiigo Store"); 464 | } else if (Arriba::highlightedObject == settingsButton) { 465 | settingsScene->enabled = true; 466 | Arriba::Colour::neutral = Amiigo::Settings::Colour::settingsNeutral; 467 | Arriba::Colour::highlightA = Amiigo::Settings::Colour::settingsHighlightA; 468 | Arriba::Colour::highlightB = Amiigo::Settings::Colour::settingsHighlightB; 469 | static_cast(Arriba::findObjectByName("StatusBarText"))->setText("Settings"); 470 | } 471 | } 472 | 473 | void selectorInput(int index) { 474 | if (selectorAmiibos[index].isCategory) { 475 | if (checkIfFileExists(selectorAmiibos[index].path.c_str()) || selectorAmiibos[index].path == "Favorites") { 476 | selectorPath = selectorAmiibos[index].path; 477 | updateSelectorStrings(); 478 | } else { 479 | static_cast(Arriba::findObjectByName("StatusBarText"))->setText("Folder does not exist"); 480 | } 481 | } else { 482 | char path[FS_MAX_PATH]; 483 | strcpy(path, selectorAmiibos[index].path.c_str()); 484 | emu::SetEmulationStatus(emu::EmulationStatus::On); 485 | emu::SetActiveVirtualAmiibo(path, FS_MAX_PATH); 486 | static_cast(Arriba::findObjectByName("StatusBarText"))->setText(path); 487 | } 488 | } 489 | 490 | void makerInput(int index) { 491 | if (makerIsInCategory) { 492 | if (index == 0) { 493 | makerList->updateStrings(seriesList); 494 | makerIsInCategory = false; 495 | } else { 496 | createVirtualAmiibo(creatorData[index-1]); 497 | } 498 | } else { 499 | creatorData = getAmiibosFromSeries(seriesList[index]); 500 | std::vector amiiboNames = {"¤ Back"}; 501 | for (size_t i = 0; i < creatorData.size(); i++) { 502 | amiiboNames.push_back(creatorData[i].name); 503 | } 504 | makerList->updateStrings(amiiboNames); 505 | selectedSeries = seriesList[index]; 506 | makerIsInCategory = true; 507 | } 508 | } 509 | 510 | void updateSelectorStrings() { 511 | selectorAmiibos = scanForAmiibo(selectorPath.c_str()); 512 | std::vector amiiboNames; 513 | for (size_t i = 0; i < selectorAmiibos.size(); i++) { 514 | amiiboNames.push_back(selectorAmiibos[i].name); 515 | } 516 | selectorList->updateStrings(amiiboNames); 517 | } 518 | 519 | void selectorContextMenuSpawner(int index, Arriba::Maths::vec2 pos) { 520 | if (index != -1) Arriba::Primitives::Quad* contextMenu = new Amiigo::Elements::selectorContextMenu(static_cast(pos.x), static_cast(pos.y), selectorAmiibos[index]); 521 | } 522 | } // namespace Amiigo::UI 523 | -------------------------------------------------------------------------------- /source/NFCDumper.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | 6 | // Goldleaf code used as implementation reference 7 | // https://github.com/XorTroll/Goldleaf/blob/master/Goldleaf/source/nfp/nfp_Amiibo.cpp 8 | 9 | namespace Amiigo::NFC::Dumper { 10 | void init() { 11 | nfpInitialize(NfpServiceType_Debug); 12 | nfpListDevices(&readerCount, &readerHandle, 1); 13 | nfpStartDetection(&readerHandle); 14 | } 15 | 16 | void exit() { 17 | nfpExit(); 18 | nfpStopDetection(&readerHandle); 19 | } 20 | 21 | bool dumpNFC() { 22 | AmiiboCreatorData amiiboInfo; 23 | amiiboInfo.name = "Default"; 24 | amiiboInfo.gameName = "Dump"; 25 | amiiboInfo.amiiboSeries = "Dump"; 26 | 27 | // Mount the tag 28 | Result res = nfpMount(&readerHandle, NfpDeviceType_Amiibo, NfpMountTarget_All); 29 | // If device failed to mount 30 | if (!R_SUCCEEDED(res)) { 31 | nfpUnmount(&readerHandle); 32 | nfpStartDetection(&readerHandle); 33 | return false; 34 | } 35 | 36 | // Get the tag info 37 | NfpTagInfo amiiboTagInfo; 38 | nfpGetTagInfo(&readerHandle, &amiiboTagInfo); 39 | // TODO: Dump UUID 40 | 41 | // Get register info 42 | NfpRegisterInfo amiiboRegInfo; 43 | nfpGetRegisterInfo(&readerHandle, &amiiboRegInfo); 44 | // TODO: Dump mii data and get first write 45 | 46 | // Get common area 47 | NfpCommonInfo amiiboCommonArea; 48 | // TODO: Get last write / version 49 | 50 | // Get model info 51 | NfpModelInfo _amiiboModelInfo = {}; 52 | nfpGetModelInfo(&readerHandle, &_amiiboModelInfo); 53 | // Convert from offcial NFP format to Emuiibo format 54 | _amiiboModelInfo.amiibo_id[5] = _amiiboModelInfo.amiibo_id[4]; 55 | _amiiboModelInfo.amiibo_id[4] = 0; 56 | _amiiboModelInfo.amiibo_id[7] = 2; 57 | // Store model info in creator struct 58 | ModelFormat* amiiboModelInfo = reinterpret_cast(_amiiboModelInfo.amiibo_id); 59 | amiiboInfo.game_character_id = amiiboModelInfo->gameCharacterID; 60 | amiiboInfo.character_variant = amiiboModelInfo->characterVariant; 61 | amiiboInfo.figure_type = amiiboModelInfo->figureType; 62 | amiiboInfo.model_number = __builtin_bswap16(amiiboModelInfo->modelNumber); 63 | amiiboInfo.series = amiiboModelInfo->series; 64 | 65 | // Get Amiibo name from user 66 | SwkbdConfig kbinput; 67 | swkbdCreate(&kbinput, 0); 68 | swkbdConfigMakePresetDefault(&kbinput); 69 | swkbdConfigSetGuideText(&kbinput, "Enter Amiibo name"); 70 | swkbdConfigSetInitialText(&kbinput, amiiboRegInfo.amiibo_name); 71 | char *amiiboName = reinterpret_cast(malloc(256)); 72 | swkbdShow(&kbinput, amiiboName, 255); 73 | swkbdClose(&kbinput); 74 | amiiboInfo.name = amiiboName; 75 | free(amiiboName); 76 | 77 | if (amiiboInfo.name == "") { 78 | nfpUnmount(&readerHandle); 79 | nfpStartDetection(&readerHandle); 80 | static_cast(Arriba::findObjectByName("StatusBarText"))->setText("Dump failed (No name provided)"); 81 | return false; 82 | } 83 | 84 | // Create the virtual amiibo 85 | createVirtualAmiibo(amiiboInfo); 86 | static_cast(Arriba::findObjectByName("StatusBarText"))->setText("Amiibo dumped to SD"); 87 | nfpUnmount(&readerHandle); 88 | nfpStartDetection(&readerHandle); 89 | return true; 90 | } 91 | } // namespace Amiigo::NFC::Dumper 92 | -------------------------------------------------------------------------------- /source/Networking.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | 7 | bool retrieveToFile(std::string URL, std::string path) { 8 | if (!hasNetworkConnection()) return false; 9 | std::ofstream file(path.c_str(), std::ofstream::trunc | std::ofstream::binary); 10 | if (file) { 11 | CURL *curl = curl_easy_init(); 12 | curl_easy_setopt(curl, CURLOPT_URL, URL.c_str()); 13 | curl_easy_setopt(curl, CURLOPT_USERAGENT, "Amiigo"); 14 | curl_easy_setopt(curl, CURLOPT_CAINFO, "romfs:/certificate.pem"); 15 | curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0L); 16 | curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 0L); 17 | curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L); 18 | curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, +[](const char* in, std::size_t size, std::size_t num, std::ofstream* out){ 19 | out->write(in, size * num); 20 | return (size * num); 21 | }); 22 | curl_easy_setopt(curl, CURLOPT_WRITEDATA, &file); 23 | curl_easy_setopt(curl, CURLOPT_NOPROGRESS, 1L); 24 | CURLcode errorCode = curl_easy_perform(curl); 25 | curl_easy_cleanup(curl); 26 | if (errorCode != CURLE_OK) { 27 | printf("ERROR: failed to download %s to %s\nCurl error code:%d\n", URL.c_str(), path.c_str(), errorCode); 28 | file.close(); 29 | return false; 30 | } 31 | } 32 | file.close(); 33 | return true; 34 | } 35 | 36 | // https://github.com/XorTroll/Goldleaf/blob/e1f5f9f9c797911e1902df37df2f0cdcc8940868/Goldleaf/source/net/net_Network.cpp#L48 37 | bool retrieveToString(std::string URL, std::string mimeType, std::string *out) { 38 | CURL *curl = curl_easy_init(); 39 | if (!mimeType.empty()) { 40 | curl_slist *header_data = curl_slist_append(header_data, ("Content-Type: " + mimeType).c_str()); 41 | header_data = curl_slist_append(header_data, ("Accept: " + mimeType).c_str()); 42 | curl_easy_setopt(curl, CURLOPT_HTTPHEADER, header_data); 43 | } 44 | std::string content; 45 | curl_easy_setopt(curl, CURLOPT_URL, URL.c_str()); 46 | curl_easy_setopt(curl, CURLOPT_USERAGENT, "Amiigo"); 47 | curl_easy_setopt(curl, CURLOPT_CAINFO, "romfs:/certificate.pem"); 48 | curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0L); 49 | curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 0L); 50 | curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L); 51 | // curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, StringWriteImpl); 52 | curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, +[](const char* in, std::size_t size, std::size_t count, std::string *out){ 53 | const auto total_size = size * count; 54 | out->append(in, total_size); 55 | return total_size; 56 | }); 57 | curl_easy_setopt(curl, CURLOPT_WRITEDATA, out); 58 | CURLcode errorCode = curl_easy_perform(curl); 59 | curl_easy_cleanup(curl); 60 | if (errorCode != CURLE_OK) { 61 | printf("ERROR: failed to download %s\nCurl error code:%d\n", URL.c_str(), errorCode); 62 | return false; 63 | } 64 | return true; 65 | } 66 | 67 | bool hasNetworkConnection() { 68 | NifmInternetConnectionStatus status; 69 | nifmGetInternetConnectionStatus(nullptr, nullptr, &status); 70 | return status == NifmInternetConnectionStatus_Connected; 71 | } 72 | -------------------------------------------------------------------------------- /source/Utils.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | #include 8 | #include 9 | #include 10 | 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | 17 | bool checkIfFileExists(const char* path) { 18 | return !access(path, F_OK); 19 | } 20 | 21 | std::vector scanForAmiibo(const char* path) { 22 | // Open path 23 | DIR* folder = opendir(path); 24 | std::vector amiibos; 25 | if (folder) { 26 | // List folder entries 27 | dirent* entry; 28 | while (entry = readdir(folder)) { 29 | AmiiboEntry amiibo; 30 | amiibo.name = entry->d_name; 31 | char flagPath[512] = ""; 32 | strcat(flagPath, path); 33 | strcat(flagPath, "/"); 34 | strcat(flagPath, entry->d_name); 35 | amiibo.path = flagPath; 36 | strcat(flagPath, "/amiibo.flag"); 37 | amiibo.isCategory = !checkIfFileExists(flagPath); 38 | amiibos.push_back(amiibo); 39 | } 40 | closedir(folder); 41 | // Sort alphabetically 42 | std::sort(amiibos.begin(), amiibos.end(), [](AmiiboEntry amiiboA, AmiiboEntry amiiboB)->bool{ 43 | int maxLength = (amiiboA.name.length() < amiiboB.name.length()) ? amiiboA.name.length() : amiiboB.name.length(); 44 | int iterations = 0; 45 | while (iterations < maxLength) { 46 | if (std::tolower(amiiboA.name[iterations]) != std::tolower(amiiboB.name[iterations])) 47 | return std::tolower(amiiboA.name[iterations]) < std::tolower(amiiboB.name[iterations]); 48 | else 49 | iterations++; 50 | } 51 | return false; 52 | }); 53 | // Prepend favorites if path is sdmc:/emuiibo/amiibo 54 | if (!strcmp(path, "sdmc:/emuiibo/amiibo")) { 55 | amiibos.insert(amiibos.begin(), {"¤ Favorites", true, "Favorites"}); 56 | } else { 57 | // If not in "sdmc:/emuiibo/amiibo" then add back entry 58 | std::string upDir = path; 59 | upDir = upDir.substr(0, upDir.find_last_of("/")); 60 | amiibos.insert(amiibos.begin(), {"¤ Back", true, upDir}); 61 | } 62 | return amiibos; 63 | } 64 | // Check if path is supposed to be favorites 65 | if (!strcmp(path, "Favorites")) { 66 | // Add in the back button 67 | amiibos.insert(amiibos.begin(), {"¤ Back", true, "sdmc:/emuiibo/amiibo"}); 68 | // Read each line from the favorites file 69 | std::string tempLine; 70 | std::ifstream fileStream("sdmc:/emuiibo/overlay/favorites.txt"); 71 | while (getline(fileStream, tempLine)) { 72 | // Check if Amiibo or dir 73 | char flagPath[512] = ""; 74 | strcat(flagPath, tempLine.c_str()); 75 | strcat(flagPath, "/amiibo.flag"); 76 | // Add to amiibo list 77 | amiibos.push_back({tempLine.substr(tempLine.find_last_of('/')+1, tempLine.length()+1 - tempLine.find_last_of('/')), !checkIfFileExists(flagPath), tempLine}); 78 | } 79 | fileStream.close(); 80 | return amiibos; 81 | } 82 | } 83 | 84 | std::vector getListOfSeries() { 85 | std::vector series; 86 | if (checkIfFileExists("sdmc:/config/amiigo/API.json")) { 87 | std::string APIData; 88 | std::string tempLine; 89 | std::ifstream fileStream("sdmc:/config/amiigo/API.json"); 90 | // Read the file from disk 91 | while (getline(fileStream, tempLine)) { 92 | APIData += tempLine; 93 | } 94 | fileStream.close(); 95 | nlohmann::json APIJson = nlohmann::json::parse(APIData, nullptr, false); 96 | // Check API cache is valid 97 | if (APIJson.is_discarded()) { 98 | printf("API cache is corrupt\n"); 99 | remove("sdmc:/config/amiigo/API.json"); 100 | return {"Error, API cache corrupt!", "Try updating cache in settings!"}; 101 | } 102 | // Loop over every entry under the Amiibo object 103 | for (int i = 0; i < APIJson["amiibo"].size(); i++) { 104 | bool isKnown = false; 105 | std::string seriesName = APIJson["amiibo"][i]["amiiboSeries"].get(); 106 | // Check if series is in list 107 | for (size_t i = 0; i < series.size(); i++) { 108 | if (series[i] == seriesName) isKnown = true; 109 | } 110 | // If not add it to the list 111 | if (!isKnown) series.push_back(seriesName); 112 | } 113 | } else { 114 | return {"Error, no API cache!"}; 115 | } 116 | 117 | // Sort alphabetically 118 | std::sort(series.begin(), series.end(), [](std::string seriesA, std::string seriesB)->bool{ 119 | int maxLength = (seriesA.length() < seriesB.length()) ? seriesA.length() : seriesB.length(); 120 | int iterations = 0; 121 | while (iterations < maxLength) { 122 | if (std::tolower(seriesA[iterations]) != std::tolower(seriesB[iterations])) 123 | return std::tolower(seriesA[iterations]) < std::tolower(seriesB[iterations]); 124 | else 125 | iterations++; 126 | } 127 | return false; 128 | }); 129 | return series; 130 | } 131 | 132 | // Written on 27/01/2021 for Kronos, can't remember how it works but does magic bit shifting 133 | unsigned short shiftAndDec(std::string input) { 134 | unsigned short value = std::stoi(input, nullptr, 16); 135 | unsigned short a = value & 0xFF00; 136 | a = 0x00FF & (a >> 8); 137 | value = value << 8; 138 | value = 0xffff & value; 139 | value = value | a; 140 | return value; 141 | } 142 | 143 | std::vector getAmiibosFromSeries(std::string series) { 144 | std::vector amiibos; 145 | if (checkIfFileExists("sdmc:/config/amiigo/API.json")) { 146 | std::string APIData; 147 | std::string tempLine; 148 | std::ifstream fileStream("sdmc:/config/amiigo/API.json"); 149 | // Read the file from disk 150 | while (getline(fileStream, tempLine)) { 151 | APIData += tempLine; 152 | } 153 | nlohmann::json APIJson = nlohmann::json::parse(APIData); 154 | fileStream.close(); 155 | // Loop over every entry under the AMiibo object 156 | for (int i = 0; i < APIJson["amiibo"].size(); i++) { 157 | // If series matches add it to the list 158 | if (APIJson["amiibo"][i]["amiiboSeries"].get() == series) { 159 | // Process the API data the same way Emutool does 160 | // https://github.com/XorTroll/emuiibo/blob/90cbc54a95c0aa4a9ceb6dd55b633de206763094/emutool/emutool/AmiiboUtils.cs#L144 161 | AmiiboCreatorData newAmiibo; 162 | newAmiibo.name = APIJson["amiibo"][i]["name"].get(); 163 | std::string fullID = APIJson["amiibo"][i]["head"].get() + APIJson["amiibo"][i]["tail"].get(); 164 | // Get strings needed to derive IDs 165 | // Var names taken from emutool 166 | std::string character_game_id_str = fullID.substr(0, 4); 167 | std::string character_variant_str = fullID.substr(4, 2); 168 | std::string figure_type_str = fullID.substr(6, 2); 169 | std::string model_no_str = fullID.substr(8, 4); 170 | std::string series_str = fullID.substr(12, 2); 171 | // Swap endianess for game ID 172 | newAmiibo.game_character_id = shiftAndDec(character_game_id_str); 173 | // Get character variant 174 | newAmiibo.character_variant = static_cast(stoi(character_variant_str, nullptr, 16)); 175 | // Get figure type 176 | newAmiibo.figure_type = static_cast(stoi(figure_type_str, nullptr, 16)); 177 | // Get model number 178 | newAmiibo.model_number = (unsigned short)stoi(model_no_str, nullptr, 16); 179 | // Get series ID 180 | newAmiibo.series = static_cast(stoi(series_str, nullptr, 16)); 181 | // Get the Game series name (only used for categorization) 182 | newAmiibo.gameName = APIJson["amiibo"][i]["gameSeries"].get(); 183 | // Get the Amiibo series name (only used for categorization) 184 | newAmiibo.amiiboSeries = APIJson["amiibo"][i]["amiiboSeries"].get(); 185 | // Add new amiibo to list 186 | amiibos.push_back(newAmiibo); 187 | } 188 | } 189 | } else { 190 | return {{"Error, API cache vanished?", 0, 0, 0, 0, 0}}; 191 | } 192 | 193 | // Sort alphabetically 194 | std::sort(amiibos.begin(), amiibos.end(), [](AmiiboCreatorData amiiboA, AmiiboCreatorData amiiboB)->bool{ 195 | int maxLength = (amiiboA.name.length() < amiiboB.name.length()) ? amiiboA.name.length() : amiiboB.name.length(); 196 | int iterations = 0; 197 | while (iterations < maxLength) { 198 | if (std::tolower(amiiboA.name[iterations]) != std::tolower(amiiboB.name[iterations])) 199 | return std::tolower(amiiboA.name[iterations]) < std::tolower(amiiboB.name[iterations]); 200 | else 201 | iterations++; 202 | } 203 | return false; 204 | }); 205 | return amiibos; 206 | } 207 | 208 | void createVirtualAmiibo(AmiiboCreatorData amiibo) { 209 | std::string pathBase = "sdmc:/emuiibo/amiibo/"; 210 | switch (Amiigo::Settings::categoryMode) { 211 | case Amiigo::Settings::saveByGameName: 212 | pathBase += amiibo.gameName + "/"; 213 | mkdir(pathBase.c_str(), 0); 214 | break; 215 | 216 | case Amiigo::Settings::saveByAmiiboSeries: 217 | pathBase += amiibo.amiiboSeries + "/"; 218 | mkdir(pathBase.c_str(), 0); 219 | break; 220 | 221 | case Amiigo::Settings::saveByCurrentFolder: 222 | if (Amiigo::UI::selectorPath != "Favorites") pathBase = Amiigo::UI::selectorPath + "/"; 223 | break;; 224 | } 225 | pathBase += amiibo.name; 226 | mkdir(pathBase.c_str(), 0); 227 | std::ofstream fileStream(pathBase + "/amiibo.flag"); 228 | fileStream.close(); 229 | fileStream.open(pathBase + "/amiibo.json"); 230 | nlohmann::json amiiboJson; 231 | amiiboJson["name"] = amiibo.name; 232 | amiiboJson["write_counter"] = 0; 233 | amiiboJson["version"] = 0; 234 | amiiboJson["mii_charinfo_file"] = "mii-charinfo.bin"; 235 | amiiboJson["first_write_date"]["y"] = 2019; 236 | amiiboJson["first_write_date"]["m"] = 1; 237 | amiiboJson["first_write_date"]["d"] = 1; 238 | amiiboJson["last_write_date"]["y"] = 2019; 239 | amiiboJson["last_write_date"]["m"] = 1; 240 | amiiboJson["last_write_date"]["d"] = 1; 241 | amiiboJson["id"]["game_character_id"] = amiibo.game_character_id; 242 | amiiboJson["id"]["character_variant"] = amiibo.character_variant; 243 | amiiboJson["id"]["figure_type"] = amiibo.figure_type; 244 | amiiboJson["id"]["series"] = amiibo.series; 245 | amiiboJson["id"]["model_number"] = amiibo.model_number; 246 | if (!Amiigo::Settings::useRandomisedUUID) 247 | amiiboJson["uuid"] = {rand() % 256, rand() % 256, rand() % 256, rand() % 256, rand() % 256, rand() % 256, rand() % 256, 0, 0, 0}; 248 | else 249 | amiiboJson["use_random_uuid"] = true; 250 | fileStream << std::setw(4) << amiiboJson << std::endl; 251 | fileStream.close(); 252 | } 253 | 254 | void firstTimeSetup() { 255 | // Cache API data 256 | while (!checkIfFileExists("sdmc:/config/amiigo/API.json")) { 257 | if (checkIfFileExists("sdmc:/config/amiigo/API.tmp")) remove("sdmc:/config/amiigo/API.tmp"); 258 | 259 | // After 5 seconds of no connection unpack stored cache 260 | if (Arriba::time > 5 && !hasNetworkConnection()) { 261 | unzFile zipFile = unzOpen("romfs:/API.cache"); 262 | unz_file_info fileInfo; 263 | unzOpenCurrentFile(zipFile); 264 | unzGetCurrentFileInfo(zipFile, &fileInfo, nullptr, 0, nullptr, 0, nullptr, 0); 265 | void* buffer = malloc(398163); 266 | FILE* outFile = fopen("sdmc:/config/amiigo/API.tmp", "wb"); 267 | for (int i = unzReadCurrentFile(zipFile, buffer, 398163); i > 0; i = unzReadCurrentFile(zipFile, buffer, 398163)) fwrite(buffer, 1, i, outFile); 268 | fclose(outFile); 269 | free(buffer); 270 | } else if (!retrieveToFile("https://www.amiiboapi.com/api/amiibo", "sdmc:/config/amiigo/API.tmp")) {continue;} 271 | 272 | rename("sdmc:/config/amiigo/API.tmp", "sdmc:/config/amiigo/API.json"); 273 | 274 | // Check file is valid JSON 275 | std::string apiLine = ""; 276 | std::string tempLine = ""; 277 | std::ifstream apiStream("sdmc:/config/amiigo/API.json"); 278 | while (getline(apiStream, tempLine)) apiLine += tempLine; 279 | apiStream.close(); 280 | if (!nlohmann::json::accept(apiLine)) remove("sdmc:/config/amiigo/API.json"); 281 | } 282 | // Install emuiibo 283 | if (!checkIfFileExists("sdmc:/atmosphere/contents/0100000000000352/exefs.nsp")) { 284 | bool hasValidEmuiiboJSON = false; 285 | nlohmann::json emuiiboInfo; 286 | while (!hasValidEmuiiboJSON) { 287 | printf("Downloading Emuiibo zip\n"); 288 | while (!hasNetworkConnection()) continue; 289 | if (checkIfFileExists("sdmc:/config/amiigo/emuiibo.tmp")) remove("sdmc:/config/amiigo/emuiibo.tmp"); 290 | // We should probably do this in a more robust way 291 | std::string emuiiboReleaseInfo; 292 | while (!retrieveToString("https://api.github.com/repos/XorTroll/Emuiibo/releases", "application/json", &emuiiboReleaseInfo)) continue; 293 | emuiiboInfo = nlohmann::json::parse(emuiiboReleaseInfo, nullptr, false); 294 | hasValidEmuiiboJSON = !emuiiboInfo.is_discarded(); 295 | } 296 | printf("hasValidEmuiiboJSON: %d\n", hasValidEmuiiboJSON); 297 | while (!retrieveToFile(emuiiboInfo[0]["assets"][0]["browser_download_url"].get().c_str(), "sdmc:/config/amiigo/emuiibo.tmp")) continue; 298 | printf("Unzipping\n"); 299 | // Extract the files from the emuiibo zip 300 | mkdir("sdmc:/atmosphere/contents/", 0); 301 | mkdir("sdmc:/atmosphere/contents/0100000000000352/", 0); 302 | mkdir("sdmc:/atmosphere/contents/0100000000000352/flags/", 0); 303 | std::ofstream fileStream("sdmc:/atmosphere/contents/0100000000000352/flags/boot2.flag"); 304 | fileStream.close(); 305 | unzFile zipFile = unzOpen("sdmc:/config/amiigo/emuiibo.tmp"); 306 | unz_global_info zipInfo; 307 | unzGetGlobalInfo(zipFile, &zipInfo); 308 | for (int i = 0; i < zipInfo.number_entry; i++) { 309 | char fileName[256]; 310 | unz_file_info fileInfo; 311 | unzOpenCurrentFile(zipFile); 312 | unzGetCurrentFileInfo(zipFile, &fileInfo, fileName, sizeof(fileName), nullptr, 0, nullptr, 0); 313 | printf("Zip index:%d is %s\n", i, fileName); 314 | if (strcmp("SdOut/atmosphere/contents/0100000000000352/exefs.nsp", fileName) == 0) { 315 | void* buffer = malloc(500000); 316 | FILE* outfile = fopen("sdmc:/atmosphere/contents/0100000000000352/exefs.nsp", "wb"); 317 | for (int j = unzReadCurrentFile(zipFile, buffer, 500000); j > 0; j = unzReadCurrentFile(zipFile, buffer, 500000)) { 318 | fwrite(buffer, 1, j, outfile); 319 | } 320 | fclose(outfile); 321 | free(buffer); 322 | break; 323 | } 324 | unzCloseCurrentFile(zipFile); 325 | unzGoToNextFile(zipFile); 326 | } 327 | printf("Unzip done\n"); 328 | unzClose(zipFile); 329 | remove("sdmc:/config/amiigo/emuiibo.tmp"); 330 | // Launch the sysmodule 331 | pmshellInitialize(); 332 | NcmProgramLocation emuiiboLoc = {0x0100000000000352, NcmStorageId_None}; 333 | pmshellLaunchProgram(0, &emuiiboLoc, nullptr); 334 | pmshellExit(); 335 | } 336 | // If flag exists download update 337 | if (checkIfFileExists("sdmc:/config/amiigo/update.flag")) { 338 | while (!hasNetworkConnection()) continue; 339 | bool DLSuccess = retrieveToFile(Amiigo::Settings::updateURL, "sdmc:/switch/Failed_Amiigo_Update.nro"); 340 | if (checkIfFileExists("sdmc:/switch/Failed_Amiigo_Update.nro") && DLSuccess) { 341 | romfsExit(); 342 | if (checkIfFileExists(Amiigo::Settings::amiigoPath)) remove(Amiigo::Settings::amiigoPath); 343 | rename("sdmc:/switch/Failed_Amiigo_Update.nro", Amiigo::Settings::amiigoPath); 344 | } 345 | if (Amiigo::Settings::amiigoPath) envSetNextLoad(Amiigo::Settings::amiigoPath, Amiigo::Settings::amiigoPath); 346 | Amiigo::UI::isRunning = 0; 347 | remove("sdmc:/config/amiigo/update.flag"); 348 | } 349 | } 350 | 351 | bool checkForUpdates() { 352 | // Return false if no internet 353 | printf("Checking for updates\n"); 354 | if (!hasNetworkConnection()) { 355 | printf("No connection\b"); 356 | return false; 357 | } 358 | 359 | printf("Getting network time\n"); 360 | timeInitialize(); 361 | long unsigned int time = Amiigo::Settings::updateTime; 362 | timeGetCurrentTime(TimeType_NetworkSystemClock, &time); 363 | timeExit(); 364 | 365 | // Only check for updates once every 24 hours, unless an update is found. 366 | if (Amiigo::Settings::updateTime > time) { 367 | printf("Last check less than 24 hours ago\n"); 368 | return false; 369 | } 370 | printf("Getting API data\n"); 371 | std::string amiigoReleaseInfo; 372 | bool releaseInfoSuccess = retrieveToString("https://api.github.com/repos/CompSciOrBust/Amiigo/releases", "application/json", &amiigoReleaseInfo); 373 | // User is probably being rate limited 374 | if (amiigoReleaseInfo.size() < 300 || !releaseInfoSuccess) { 375 | printf("%s\n", amiigoReleaseInfo.c_str()); 376 | printf("Error, getting Amiigo update info failed\n"); 377 | return false; 378 | } 379 | nlohmann::json amiigoInfoParsed = nlohmann::json::parse(amiigoReleaseInfo, nullptr, false); 380 | 381 | // If data is corrupt do nothing 382 | if (amiigoInfoParsed.is_discarded()) { 383 | printf("%s\n", amiigoReleaseInfo.c_str()); 384 | printf("Error, Amiigo update info corrupt\n"); 385 | return false; 386 | } 387 | 388 | // If on the latest update wait another 24 hours before checking again 389 | if (amiigoInfoParsed[0]["tag_name"].get() == VERSION) { 390 | Amiigo::Settings::updateTime = time + 86400; 391 | Amiigo::Settings::saveSettings(); 392 | return false; 393 | } else {Amiigo::Settings::updateURL = amiigoInfoParsed[0]["assets"][0]["browser_download_url"].get().c_str();} 394 | return true; 395 | } 396 | -------------------------------------------------------------------------------- /source/emuiibo.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | namespace emu { 4 | 5 | namespace { 6 | 7 | #define EMU_EMUIIBO_SERVICE_NAME "emuiibo" 8 | constexpr auto EmuiiboServiceName = smEncodeName(EMU_EMUIIBO_SERVICE_NAME); 9 | 10 | Service g_EmuiiboService; 11 | 12 | inline bool smAtmosphereHasService(const SmServiceName name) { 13 | auto has = false; 14 | tipcDispatchInOut(smGetServiceSessionTipc(), 65100, name, has); 15 | return has; 16 | } 17 | 18 | } 19 | 20 | bool IsAvailable() { 21 | return smAtmosphereHasService(EmuiiboServiceName); 22 | } 23 | 24 | Result Initialize() { 25 | if(serviceIsActive(&g_EmuiiboService)) { 26 | return 0; 27 | } 28 | return smGetService(&g_EmuiiboService, EMU_EMUIIBO_SERVICE_NAME); 29 | } 30 | 31 | void Exit() { 32 | serviceClose(&g_EmuiiboService); 33 | } 34 | 35 | Version GetVersion() { 36 | Version ver = {}; 37 | serviceDispatchOut(&g_EmuiiboService, 0, ver); 38 | return ver; 39 | } 40 | 41 | void GetVirtualAmiiboDirectory(char *out_path, const size_t out_path_size) { 42 | serviceDispatch(&g_EmuiiboService, 1, 43 | .buffer_attrs = { SfBufferAttr_HipcMapAlias | SfBufferAttr_Out }, 44 | .buffers = { { out_path, out_path_size } }, 45 | ); 46 | } 47 | 48 | EmulationStatus GetEmulationStatus() { 49 | EmulationStatus status = EmulationStatus::Off; 50 | serviceDispatchOut(&g_EmuiiboService, 2, status); 51 | return status; 52 | } 53 | 54 | void SetEmulationStatus(const EmulationStatus status) { 55 | serviceDispatchIn(&g_EmuiiboService, 3, status); 56 | } 57 | 58 | Result GetActiveVirtualAmiibo(VirtualAmiiboData *out_amiibo_data, char *out_path, const size_t out_path_size) { 59 | return serviceDispatchOut(&g_EmuiiboService, 4, *out_amiibo_data, 60 | .buffer_attrs = { 61 | SfBufferAttr_HipcMapAlias | SfBufferAttr_Out 62 | }, 63 | .buffers = { 64 | { out_path, out_path_size } 65 | }, 66 | ); 67 | } 68 | 69 | Result SetActiveVirtualAmiibo(const char *path, const size_t path_size) { 70 | return serviceDispatch(&g_EmuiiboService, 5, 71 | .buffer_attrs = { SfBufferAttr_HipcMapAlias | SfBufferAttr_In }, 72 | .buffers = { { path, path_size } }, 73 | ); 74 | } 75 | 76 | void ResetActiveVirtualAmiibo() { 77 | serviceDispatch(&g_EmuiiboService, 6); 78 | } 79 | 80 | VirtualAmiiboStatus GetActiveVirtualAmiiboStatus() { 81 | VirtualAmiiboStatus status = VirtualAmiiboStatus::Invalid; 82 | serviceDispatchOut(&g_EmuiiboService, 7, status); 83 | return status; 84 | } 85 | 86 | void SetActiveVirtualAmiiboStatus(const VirtualAmiiboStatus status) { 87 | serviceDispatchIn(&g_EmuiiboService, 8, status); 88 | } 89 | 90 | bool IsApplicationIdIntercepted(const u64 app_id) { 91 | bool intercepted; 92 | serviceDispatchInOut(&g_EmuiiboService, 9, app_id, intercepted); 93 | return intercepted; 94 | } 95 | 96 | Result TryParseVirtualAmiibo(const char *path, const size_t path_size, VirtualAmiiboData *out_amiibo_data) { 97 | return serviceDispatchOut(&g_EmuiiboService, 10, *out_amiibo_data, 98 | .buffer_attrs = { 99 | SfBufferAttr_HipcMapAlias | SfBufferAttr_In 100 | }, 101 | .buffers = { 102 | { path, path_size } 103 | }, 104 | ); 105 | } 106 | 107 | Result GetActiveVirtualAmiiboAreas(VirtualAmiiboAreaEntry *out_area_buf, const size_t out_area_size, u32 *out_area_count) { 108 | return serviceDispatchOut(&g_EmuiiboService, 11, *out_area_count, 109 | .buffer_attrs = { 110 | SfBufferAttr_HipcMapAlias | SfBufferAttr_Out 111 | }, 112 | .buffers = { 113 | { out_area_buf, out_area_size } 114 | }, 115 | ); 116 | } 117 | 118 | Result GetActiveVirtualAmiiboCurrentArea(u32 *out_access_id) { 119 | return serviceDispatchOut(&g_EmuiiboService, 12, *out_access_id); 120 | } 121 | 122 | Result SetActiveVirtualAmiiboCurrentArea(const u32 access_id) { 123 | return serviceDispatchIn(&g_EmuiiboService, 13, access_id); 124 | } 125 | 126 | Result SetActiveVirtualAmiiboUuidInfo(const VirtualAmiiboUuidInfo uuid_info) { 127 | return serviceDispatchIn(&g_EmuiiboService, 14, uuid_info); 128 | } 129 | 130 | } -------------------------------------------------------------------------------- /source/main.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | int main(int argc, char *argv[]) { 11 | // Init 12 | socketInitializeDefault(); 13 | nxlinkStdio(); 14 | romfsInit(); 15 | nifmInitialize(NifmServiceType_User); 16 | // Create folder for caching API data 17 | mkdir("sdmc:/config/amiigo", 0); 18 | // Create folder for Amiibos 19 | mkdir("sdmc:/emuiibo/amiibo", 0); 20 | Arriba::init(); 21 | // Save current path 22 | strcpy(Amiigo::Settings::amiigoPath, argv[0]); 23 | printf("%s\n", Amiigo::Settings::amiigoPath); 24 | // Load bg 25 | Arriba::Primitives::Quad* bg = new Arriba::Primitives::Quad(0, 0, Arriba::Graphics::windowWidth, Arriba::Graphics::windowHeight, Arriba::Graphics::Pivot::topLeft); 26 | // If a custom background shader exists use it, otherwise load the default from romfs 27 | if(checkIfFileExists("sdmc:/config/amiigo/bgFragment.glsl")) 28 | bg->renderer->thisShader.updateFragments("romfs:/VertexDefault.glsl", "sdmc:/config/amiigo/bgFragment.glsl"); 29 | else 30 | bg->renderer->thisShader.updateFragments("romfs:/VertexDefault.glsl", "romfs:/bgFragment.glsl"); 31 | bg->name = "AmiigoBG"; 32 | // Init NFC dumper 33 | Amiigo::NFC::Dumper::init(); 34 | // Init UI 35 | Amiigo::UI::initUI(); 36 | 37 | // Main loop 38 | while (appletMainLoop()) { 39 | // Handle UI input and rendering 40 | Amiigo::UI::handleInput(); 41 | if(Arriba::Input::buttonUp(Arriba::Input::PlusButtonSwitch) || !Amiigo::UI::isRunning) break; 42 | Arriba::drawFrame(); 43 | bg->renderer->thisShader.setFloat1("iTime", Arriba::time); 44 | 45 | // Scan for Physical amiibos to dump 46 | NfpDeviceState nfpState; 47 | bool hasDumped = false; 48 | nfpGetDeviceState(&Amiigo::NFC::Dumper::readerHandle, &nfpState); 49 | if (nfpState == NfpDeviceState_TagFound) hasDumped = Amiigo::NFC::Dumper::dumpNFC(); 50 | if (hasDumped) Amiigo::UI::updateSelectorStrings(); 51 | } 52 | // Deinit 53 | socketExit(); 54 | romfsExit(); 55 | nifmExit(); 56 | Amiigo::NFC::Dumper::exit(); 57 | Arriba::exit(); 58 | if(emu::IsAvailable()) emu::Exit(); 59 | return 0; 60 | } 61 | --------------------------------------------------------------------------------