mirror of
				https://github.com/hedge-dev/UnleashedRecomp.git
				synced 2025-10-30 07:11:05 +00:00 
			
		
		
		
	Merge branch 'hedge-dev:main' into custom_aspect_ratios
This commit is contained in:
		
						commit
						ceecdf8748
					
				
					 18 changed files with 156 additions and 31 deletions
				
			
		
							
								
								
									
										4
									
								
								.github/workflows/validate.yml
									
										
									
									
										vendored
									
									
								
							
							
						
						
									
										4
									
								
								.github/workflows/validate.yml
									
										
									
									
										vendored
									
									
								
							|  | @ -145,13 +145,11 @@ jobs: | |||
|           Move-Item -Path ".\out\build\${{ env.CMAKE_PRESET }}\UnleashedRecomp\dxil.dll" -Destination ".\release\dxil.dll" | ||||
|           Move-Item -Path ".\out\build\${{ env.CMAKE_PRESET }}\UnleashedRecomp\UnleashedRecomp.exe" -Destination ".\release\UnleashedRecomp.exe" | ||||
| 
 | ||||
|           Compress-Archive -Path .\release\* -DestinationPath .\UnleashedRecomp-Windows.zip | ||||
| 
 | ||||
|       - name: Upload Artifact | ||||
|         uses: actions/upload-artifact@v4 | ||||
|         with: | ||||
|           name: UnleashedRecomp-Windows-${{ env.CMAKE_PRESET }} | ||||
|           path: .\UnleashedRecomp-Windows.zip | ||||
|           path: .\release | ||||
| 
 | ||||
|       - name: Upload PDB | ||||
|         uses: actions/upload-artifact@v4 | ||||
|  |  | |||
							
								
								
									
										11
									
								
								README.md
									
										
									
									
									
								
							
							
						
						
									
										11
									
								
								README.md
									
										
									
									
									
								
							|  | @ -278,9 +278,16 @@ Simply booting at least once in Desktop Mode will enable the Deck to use the fil | |||
| 
 | ||||
| ## FAQ | ||||
| 
 | ||||
| ### Do you have a Discord server? | ||||
| ### Do you have a website or Discord server? | ||||
| 
 | ||||
| Unleashed Recompiled is not associated with any Discord servers. Use the [Issues](https://github.com/hedge-dev/UnleashedRecomp/issues) page if you need support. | ||||
| Unleashed Recompiled does not have an official website, nor is it affiliated with any Discord servers. | ||||
| 
 | ||||
| **Please link here when directing anyone to the project.** | ||||
| 
 | ||||
| > [!CAUTION] | ||||
| > Do not download builds of Unleashed Recompiled from anywhere but our [Releases](https://github.com/hedge-dev/UnleashedRecomp/releases/latest) page. | ||||
| > | ||||
| > **We will never distribute builds on other websites, via Discord servers or via third-party update tools.** | ||||
| 
 | ||||
| ### Why does the installer say my files are invalid? | ||||
| 
 | ||||
|  |  | |||
|  | @ -352,7 +352,7 @@ if (WIN32) | |||
|         Synchronization | ||||
|         winmm | ||||
|     ) | ||||
| endif() | ||||
| endif() | ||||
| 
 | ||||
| target_link_libraries(UnleashedRecomp PRIVATE | ||||
|     fmt::fmt | ||||
|  |  | |||
|  | @ -8,6 +8,7 @@ public: | |||
|     static inline bool s_isInit; | ||||
|     static inline bool s_isMissingDLC; | ||||
|     static inline bool s_isLoading; | ||||
|     static inline bool s_isSaving; | ||||
|     static inline bool s_isWerehog; | ||||
|     static inline bool s_isSaveDataCorrupt; | ||||
| 
 | ||||
|  |  | |||
|  | @ -38,6 +38,11 @@ static void CreateAudioDevice() | |||
| 
 | ||||
| void XAudioInitializeSystem() | ||||
| { | ||||
| #ifdef _WIN32 | ||||
|     // Force wasapi on Windows.
 | ||||
|     SDL_setenv("SDL_AUDIODRIVER", "wasapi", true); | ||||
| #endif | ||||
| 
 | ||||
|     SDL_SetHint(SDL_HINT_AUDIO_CATEGORY, "playback"); | ||||
|     SDL_SetHint(SDL_HINT_AUDIO_DEVICE_APP_NAME, "Unleashed Recompiled"); | ||||
| 
 | ||||
|  |  | |||
|  | @ -692,6 +692,17 @@ std::unordered_map<std::string_view, std::unordered_map<ELanguage, std::string>> | |||
|             { ELanguage::Italian,  "Impossibile creare un backend D3D12 (Windows) o Vulkan.\n\nAssicurati che:\n\n- Il tuo sistema soddisfi i requisiti minimi.\n- I driver della scheda grafica siano aggiornati.\n- Il tuo sistema operativo sia aggiornato." } | ||||
|         } | ||||
|     }, | ||||
|     { | ||||
|         "System_Win32_MissingDLLs", | ||||
|         { | ||||
|             { ELanguage::English,  "The module \"%s\" could not be found.\n\nPlease make sure that:\n\n- You extracted this copy of Unleashed Recompiled fully and not just the *.exe file.\n- You are not running Unleashed Recompiled from a *.zip file." }, | ||||
|             { ELanguage::Japanese, "モジュール\"%s\"が見つかりませんでした\n\n次の点を確認してください:\n\n※Unleashed Recompiledの*.exeファイルだけを抽出していなく、 コピーを完全に抽出してること\n※Unleashed Recompiledを*.zipファイルから実行していないこと" }, | ||||
|             { ELanguage::German,   "Das Modul \"%s\" konnte nicht gefunden werden.\n\nBitte stelle sicher, dass:\n\n- Diese Kopie von Unleashed Recompiled vollständig entpackt wurde und nicht nur die *.exe-Datei.\n- Unleashed Recompiled nicht direkt aus einer *.zip-Datei ausgeführt wird." }, | ||||
|             { ELanguage::French,   "Le module \"%s\" n'a pas pu être trouvé.\n\nVeuillez vous assurer que :\n\n- Vous avez extrait Unleashed Recompiled dans son entièreté et pas seulement le fichier *.exe.\n- Vous n'exécutez pas Unleashed Recompiled à partir d'un fichier *.zip." }, | ||||
|             { ELanguage::Spanish,  "No se pudo encontrar el módulo \"%s\".\n\nAsegúrese de que:\n\n- Ha extraido esta copia de Unleashed Recompiled por completo y no solo el archivo *.exe.\n- No está ejecutando Unleashed Recompiled desde un archivo *.zip." }, | ||||
|             { ELanguage::Italian,  "Impossibile trovare il modulo \"%s\".\n\nAssicurati che:\n\n- Hai estratto questa copia di Unleashed Recompiled correttamente e non solo il file *.exe.\n- Non stai eseguendo Unleashed Recompiled da un file *.zip." } | ||||
|         } | ||||
|     }, | ||||
|     { | ||||
|         "Common_On", | ||||
|         { | ||||
|  |  | |||
|  | @ -1,4 +1,5 @@ | |||
| #include <stdafx.h> | ||||
| #include <cpuid.h> | ||||
| #include <cpu/guest_thread.h> | ||||
| #include <gpu/video.h> | ||||
| #include <kernel/function.h> | ||||
|  | @ -27,6 +28,15 @@ | |||
| #include <timeapi.h> | ||||
| #endif | ||||
| 
 | ||||
| #if defined(_WIN32) && defined(UNLEASHED_RECOMP_D3D12) | ||||
| static std::array<std::string_view, 3> g_D3D12RequiredModules = | ||||
| { | ||||
|     "D3D12/D3D12Core.dll", | ||||
|     "dxcompiler.dll", | ||||
|     "dxil.dll" | ||||
| }; | ||||
| #endif | ||||
| 
 | ||||
| const size_t XMAIOBegin = 0x7FEA0000; | ||||
| const size_t XMAIOEnd = XMAIOBegin + 0x0000FFFF; | ||||
| 
 | ||||
|  | @ -147,6 +157,29 @@ uint32_t LdrLoadModule(const std::filesystem::path &path) | |||
|     return entry; | ||||
| } | ||||
| 
 | ||||
| __attribute__((constructor(101), target("no-avx,no-avx2"), noinline)) | ||||
| void init() | ||||
| { | ||||
| #ifdef __x86_64__ | ||||
|     uint32_t eax, ebx, ecx, edx; | ||||
| 
 | ||||
|     // Execute CPUID for processor info and feature bits.
 | ||||
|     __get_cpuid(1, &eax, &ebx, &ecx, &edx); | ||||
| 
 | ||||
|     // Check for AVX support.
 | ||||
|     if ((ecx & (1 << 28)) == 0) | ||||
|     { | ||||
|         printf("[*] CPU does not support the AVX instruction set.\n"); | ||||
| 
 | ||||
| #ifdef _WIN32 | ||||
|         MessageBoxA(nullptr, "Your CPU does not meet the minimum system requirements.", "Unleashed Recompiled", MB_ICONERROR); | ||||
| #endif | ||||
| 
 | ||||
|         std::_Exit(1); | ||||
|     } | ||||
| #endif | ||||
| } | ||||
| 
 | ||||
| int main(int argc, char *argv[]) | ||||
| { | ||||
| #ifdef _WIN32 | ||||
|  | @ -156,7 +189,7 @@ int main(int argc, char *argv[]) | |||
|     os::process::CheckConsole(); | ||||
| 
 | ||||
|     if (!os::registry::Init()) | ||||
|         LOGN_WARNING("OS doesn't support registry"); | ||||
|         LOGN_WARNING("OS does not support registry."); | ||||
| 
 | ||||
|     os::logger::Init(); | ||||
| 
 | ||||
|  | @ -180,6 +213,19 @@ int main(int argc, char *argv[]) | |||
| 
 | ||||
|     Config::Load(); | ||||
| 
 | ||||
| #if defined(_WIN32) && defined(UNLEASHED_RECOMP_D3D12) | ||||
|     for (auto& dll : g_D3D12RequiredModules) | ||||
|     { | ||||
|         if (!std::filesystem::exists(g_executableRoot / dll)) | ||||
|         { | ||||
|             char text[512]; | ||||
|             snprintf(text, sizeof(text), Localise("System_Win32_MissingDLLs").c_str(), dll.data()); | ||||
|             SDL_ShowSimpleMessageBox(SDL_MESSAGEBOX_ERROR, GameWindow::GetTitle(), text, GameWindow::s_pWindow); | ||||
|             std::_Exit(1); | ||||
|         } | ||||
|     } | ||||
| #endif | ||||
| 
 | ||||
|     // Check the time since the last time an update was checked. Store the new time if the difference is more than six hours.
 | ||||
|     constexpr double TimeBetweenUpdateChecksInSeconds = 6 * 60 * 60; | ||||
|     time_t timeNow = std::time(nullptr); | ||||
|  |  | |||
|  | @ -212,6 +212,9 @@ g_sdlEventListenerForInputPatches; | |||
| 
 | ||||
| static bool IsDPadThreshold(const SWA::SPadState* pPadState) | ||||
| { | ||||
|     if (Config::DisableDPadMovement) | ||||
|         return false; | ||||
| 
 | ||||
|     return pPadState->IsDown(SWA::eKeyState_DpadUp)   || | ||||
|            pPadState->IsDown(SWA::eKeyState_DpadDown) || | ||||
|            pPadState->IsDown(SWA::eKeyState_DpadLeft) || | ||||
|  | @ -237,6 +240,9 @@ static bool IsCursorThreshold(double deadzone = 0, bool isBelowThreshold = false | |||
| 
 | ||||
| static void SetDPadAnalogDirectionX(PPCRegister& pPadState, PPCRegister& x, bool invert, float max = 1.0f) | ||||
| { | ||||
|     if (Config::DisableDPadMovement) | ||||
|         return; | ||||
| 
 | ||||
|     auto pGuestPadState = (SWA::SPadState*)g_memory.Translate(pPadState.u32); | ||||
| 
 | ||||
|     if (pGuestPadState->IsDown(SWA::eKeyState_DpadLeft)) | ||||
|  | @ -248,6 +254,9 @@ static void SetDPadAnalogDirectionX(PPCRegister& pPadState, PPCRegister& x, bool | |||
| 
 | ||||
| static void SetDPadAnalogDirectionY(PPCRegister& pPadState, PPCRegister& y, bool invert, float max = 1.0f) | ||||
| { | ||||
|     if (Config::DisableDPadMovement) | ||||
|         return; | ||||
| 
 | ||||
|     auto pGuestPadState = (SWA::SPadState*)g_memory.Translate(pPadState.u32); | ||||
| 
 | ||||
|     if (pGuestPadState->IsDown(SWA::eKeyState_DpadUp)) | ||||
|  | @ -283,6 +292,9 @@ void PostureDPadSupportYMidAsmHook(PPCRegister& pPadState, PPCRegister& y) | |||
| 
 | ||||
| void PostureSpaceHurrierDPadSupportXMidAsmHook(PPCRegister& pPadState, PPCVRegister& vector) | ||||
| { | ||||
|     if (Config::DisableDPadMovement) | ||||
|         return; | ||||
|      | ||||
|     auto pGuestPadState = (SWA::SPadState*)g_memory.Translate(pPadState.u32); | ||||
| 
 | ||||
|     if (pGuestPadState->IsDown(SWA::eKeyState_DpadLeft)) | ||||
|  | @ -294,6 +306,9 @@ void PostureSpaceHurrierDPadSupportXMidAsmHook(PPCRegister& pPadState, PPCVRegis | |||
| 
 | ||||
| void PostureSpaceHurrierDPadSupportYMidAsmHook(PPCRegister& pPadState, PPCVRegister& vector) | ||||
| { | ||||
|     if (Config::DisableDPadMovement) | ||||
|         return; | ||||
| 
 | ||||
|     auto pGuestPadState = (SWA::SPadState*)g_memory.Translate(pPadState.u32); | ||||
| 
 | ||||
|     if (pGuestPadState->IsDown(SWA::eKeyState_DpadUp)) | ||||
|  | @ -303,6 +318,18 @@ void PostureSpaceHurrierDPadSupportYMidAsmHook(PPCRegister& pPadState, PPCVRegis | |||
|         vector.f32[3] = -1.0f; | ||||
| } | ||||
| 
 | ||||
| void SetXButtonHomingMidAsmHook(PPCRegister& r1) | ||||
| { | ||||
|     auto pXButtonHoming = (bool*)(g_memory.base + r1.u32 + 0x63); | ||||
| 
 | ||||
|     *pXButtonHoming = !Config::HomingAttackOnJump; | ||||
| } | ||||
| 
 | ||||
| bool IsHomingAttackOnJump() | ||||
| { | ||||
|     return Config::HomingAttackOnJump; | ||||
| } | ||||
| 
 | ||||
| // ------------- WORLD MAP ------------- //
 | ||||
| 
 | ||||
| bool WorldMapDeadzoneMidAsmHook(PPCRegister& pPadState) | ||||
|  | @ -403,17 +430,20 @@ PPC_FUNC(sub_8256C938) | |||
|         pWorldMapCursor->m_LeftStickVertical = rPadState.LeftStickVertical; | ||||
|         pWorldMapCursor->m_LeftStickHorizontal = rPadState.LeftStickHorizontal; | ||||
| 
 | ||||
|         if (rPadState.IsDown(SWA::eKeyState_DpadUp)) | ||||
|             pWorldMapCursor->m_LeftStickVertical = 1.0f; | ||||
| 
 | ||||
|         if (rPadState.IsDown(SWA::eKeyState_DpadDown)) | ||||
|             pWorldMapCursor->m_LeftStickVertical = -1.0f; | ||||
| 
 | ||||
|         if (rPadState.IsDown(SWA::eKeyState_DpadLeft)) | ||||
|             pWorldMapCursor->m_LeftStickHorizontal = -1.0f; | ||||
| 
 | ||||
|         if (rPadState.IsDown(SWA::eKeyState_DpadRight)) | ||||
|             pWorldMapCursor->m_LeftStickHorizontal = 1.0f; | ||||
|         if (!Config::DisableDPadMovement) | ||||
|         { | ||||
|             if (rPadState.IsDown(SWA::eKeyState_DpadUp)) | ||||
|                 pWorldMapCursor->m_LeftStickVertical = 1.0f; | ||||
|      | ||||
|             if (rPadState.IsDown(SWA::eKeyState_DpadDown)) | ||||
|                 pWorldMapCursor->m_LeftStickVertical = -1.0f; | ||||
|      | ||||
|             if (rPadState.IsDown(SWA::eKeyState_DpadLeft)) | ||||
|                 pWorldMapCursor->m_LeftStickHorizontal = -1.0f; | ||||
|      | ||||
|             if (rPadState.IsDown(SWA::eKeyState_DpadRight)) | ||||
|                 pWorldMapCursor->m_LeftStickHorizontal = 1.0f; | ||||
|         } | ||||
| 
 | ||||
|         if (sqrtl((pWorldMapCursor->m_LeftStickHorizontal * pWorldMapCursor->m_LeftStickHorizontal) + | ||||
|             (pWorldMapCursor->m_LeftStickVertical * pWorldMapCursor->m_LeftStickVertical)) > WORLD_MAP_ROTATE_DEADZONE) | ||||
|  |  | |||
|  | @ -146,3 +146,13 @@ PPC_FUNC(sub_824C1E60) | |||
| 
 | ||||
|     __imp__sub_824C1E60(ctx, base); | ||||
| } | ||||
| 
 | ||||
| // Remove boost filter
 | ||||
| void DisableBoostFilterMidAsmHook(PPCRegister& r11) | ||||
| { | ||||
|     if (Config::DisableBoostFilter) | ||||
|     { | ||||
|         if (r11.u32 == 1) | ||||
|             r11.u32 = 0; | ||||
|     } | ||||
| } | ||||
|  |  | |||
|  | @ -95,13 +95,6 @@ void PostUnleashMidAsmHook(PPCRegister& r30) | |||
|     g_isUnleashCancelled = false; | ||||
| } | ||||
| 
 | ||||
| void SetXButtonHomingMidAsmHook(PPCRegister& r1) | ||||
| { | ||||
|     auto pXButtonHoming = (bool*)(g_memory.base + r1.u32 + 0x63); | ||||
|      | ||||
|     *pXButtonHoming = !Config::HomingAttackOnJump; | ||||
| } | ||||
| 
 | ||||
| // SWA::Player::CEvilSonicContext
 | ||||
| PPC_FUNC_IMPL(__imp__sub_823B49D8); | ||||
| PPC_FUNC(sub_823B49D8) | ||||
|  |  | |||
|  | @ -97,6 +97,8 @@ PPC_FUNC(sub_824E5170) | |||
| 
 | ||||
|     __imp__sub_824E5170(ctx, base); | ||||
| 
 | ||||
|     App::s_isSaving = pSaveIcon->m_IsVisible; | ||||
| 
 | ||||
|     if (pSaveIcon->m_IsVisible) | ||||
|     { | ||||
|         App::s_isSaveDataCorrupt = false; | ||||
|  |  | |||
|  | @ -1,4 +1,4 @@ | |||
| VERSION_MILESTONE="" | ||||
| VERSION_MAJOR=1 | ||||
| VERSION_MINOR=0 | ||||
| VERSION_REVISION=1 | ||||
| VERSION_REVISION=2 | ||||
|  |  | |||
|  | @ -34,8 +34,14 @@ int Window_OnSDLEvent(void*, SDL_Event* event) | |||
|     switch (event->type) | ||||
|     { | ||||
|         case SDL_QUIT: | ||||
|         { | ||||
|             if (App::s_isSaving) | ||||
|                 break; | ||||
| 
 | ||||
|             App::Exit(); | ||||
| 
 | ||||
|             break; | ||||
|         } | ||||
| 
 | ||||
|         case SDL_KEYDOWN: | ||||
|         { | ||||
|  |  | |||
|  | @ -96,7 +96,7 @@ enum class ETripleBuffering : uint32_t | |||
| }; | ||||
| 
 | ||||
| static constexpr int32_t FPS_MIN = 15; | ||||
| static constexpr int32_t FPS_MAX = 240; | ||||
| static constexpr int32_t FPS_MAX = 241; | ||||
| 
 | ||||
| enum class EAntiAliasing : uint32_t | ||||
| { | ||||
|  |  | |||
|  | @ -78,8 +78,11 @@ CONFIG_DEFINE_ENUM_LOCALISED("Video", EUIAlignmentMode, UIAlignmentMode, EUIAlig | |||
| 
 | ||||
| CONFIG_DEFINE_HIDDEN("Codes", bool, AllowCancellingUnleash, false); | ||||
| CONFIG_DEFINE_HIDDEN("Codes", bool, DisableAutoSaveWarning, false); | ||||
| CONFIG_DEFINE_HIDDEN("Codes", bool, DisableBoostFilter, false); | ||||
| CONFIG_DEFINE_HIDDEN("Codes", bool, DisableDLCIcon, false); | ||||
| CONFIG_DEFINE_HIDDEN("Codes", bool, DisableDPadMovement, false); | ||||
| CONFIG_DEFINE_HIDDEN("Codes", bool, DisableDWMRoundedCorners, false); | ||||
| CONFIG_DEFINE_HIDDEN("Codes", bool, DisableLowResolutionFontOnCustomUI, false); | ||||
| CONFIG_DEFINE_HIDDEN("Codes", bool, EnableEventCollisionDebugView, false); | ||||
| CONFIG_DEFINE_HIDDEN("Codes", bool, EnableGIMipLevelDebugView, false); | ||||
| CONFIG_DEFINE_HIDDEN("Codes", bool, EnableObjectCollisionDebugView, false); | ||||
|  | @ -92,6 +95,5 @@ CONFIG_DEFINE_HIDDEN("Codes", bool, SaveScoreAtCheckpoints, false); | |||
| CONFIG_DEFINE_HIDDEN("Codes", bool, SkipIntroLogos, false); | ||||
| CONFIG_DEFINE_HIDDEN("Codes", bool, UseArrowsForTimeOfDayTransition, false); | ||||
| CONFIG_DEFINE_HIDDEN("Codes", bool, UseOfficialTitleOnTitleBar, false); | ||||
| CONFIG_DEFINE_HIDDEN("Codes", bool, DisableLowResolutionFontOnCustomUI, false); | ||||
| 
 | ||||
| CONFIG_DEFINE("Update", time_t, LastChecked, 0); | ||||
|  |  | |||
|  | @ -8,6 +8,8 @@ | |||
| #define GAME_INSTALL_DIRECTORY "." | ||||
| #endif | ||||
| 
 | ||||
| extern std::filesystem::path g_executableRoot; | ||||
| 
 | ||||
| inline std::filesystem::path GetGamePath() | ||||
| { | ||||
|     return GAME_INSTALL_DIRECTORY; | ||||
|  |  | |||
|  | @ -181,8 +181,8 @@ jump_address_on_true = 0x829E40A0 | |||
| # Disable Chip hints for shoe upgrades | ||||
| [[midasm_hook]] | ||||
| name = "DisableHintsMidAsmHook" | ||||
| address = 0x82691CB0 | ||||
| jump_address_on_true = 0x82691E24 | ||||
| address = 0x82691DD0 | ||||
| jump_address_on_true = 0x82691DD4 | ||||
| 
 | ||||
| # Disable navigation volumes | ||||
| [[midasm_hook]] | ||||
|  | @ -197,11 +197,18 @@ address = 0x823A4FF0 | |||
| registers = ["r4", "r5"] | ||||
| return_on_false = true | ||||
| 
 | ||||
| # Set default value for XButtonHoming. | ||||
| [[midasm_hook]] | ||||
| name = "SetXButtonHomingMidAsmHook" | ||||
| address = 0x8237AC90 | ||||
| registers = ["r1"] | ||||
| 
 | ||||
| # Disable XML reading for XButtonHoming. | ||||
| [[midasm_hook]] | ||||
| name = "IsHomingAttackOnJump" | ||||
| address = 0x8237ACE4 | ||||
| jump_address_on_true = 0x8237ACE8 | ||||
| 
 | ||||
| # Down force HFR fix | ||||
| [[midasm_hook]] | ||||
| name = "DownForceDeltaTimeFixMidAsmHook" | ||||
|  | @ -1093,3 +1100,8 @@ registers = ["r31", "r29", "r28"] | |||
| name = "ObjGrindDashPanelAllocMidAsmHook" | ||||
| address = 0x82614948 | ||||
| registers = ["r3"] | ||||
| 
 | ||||
| [[midasm_hook]] | ||||
| name = "DisableBoostFilterMidAsmHook" | ||||
| address = 0x82B48C9C | ||||
| registers = ["r11"] | ||||
|  |  | |||
|  | @ -1 +1 @@ | |||
| Subproject commit cd6fcb33bdcaff37c8c9d2083c7951e1d73ae9da | ||||
| Subproject commit 7b8e37aa3758c3ce2361433965cb94f2a0505eb2 | ||||
		Loading…
	
	Add table
		
		Reference in a new issue
	
	 JaceCear
						JaceCear