509 lines
15 KiB
C++
509 lines
15 KiB
C++
#include <iostream>
|
|
#include "windows.h"
|
|
#include "InputEmulator.h"
|
|
#include "../ComponentsManager.h"
|
|
#include "../../Constants.h"
|
|
#include "../../framework.h"
|
|
#include "../../Input/Bindings/KeyboardBinding.h"
|
|
#include "../../Input/Bindings/XinputBinding.h"
|
|
#include "../../Input/Bindings/MouseBinding.h"
|
|
#include "../../Input/Bindings/Ds4Binding.h"
|
|
#include "../../Input/Bindings/DVLBinding.h"
|
|
#include "../../Input/KeyConfig/Config.h"
|
|
#include "../../Utilities/Operations.h"
|
|
#include "../../Utilities/EnumBitwiseOperations.h"
|
|
#include "../../FileSystem/ConfigFile.h"
|
|
#include "../GameState.h"
|
|
#include "../Pause.h"
|
|
#include "../../Utils.h"
|
|
|
|
const std::string KEY_CONFIG_FILE_NAME = "keyconfig.ini";
|
|
|
|
using namespace TLAC::Input;
|
|
using namespace TLAC::Input::KeyConfig;
|
|
using namespace TLAC::Utilities;
|
|
|
|
namespace TLAC::Components
|
|
{
|
|
|
|
InputEmulator::InputEmulator()
|
|
{
|
|
|
|
}
|
|
|
|
InputEmulator::~InputEmulator()
|
|
{
|
|
delete TestBinding;
|
|
delete ServiceBinding;
|
|
|
|
delete StartBinding;
|
|
delete SankakuBinding;
|
|
delete ShikakuBinding;
|
|
delete BatsuBinding;
|
|
delete MaruBinding;
|
|
|
|
delete LeftBinding;
|
|
delete RightBinding;
|
|
|
|
delete MenuLBinding;
|
|
delete MenuRBinding;
|
|
delete MenuCircleBinding;
|
|
}
|
|
|
|
const char* InputEmulator::GetDisplayName()
|
|
{
|
|
return "input_emulator";
|
|
}
|
|
|
|
void InputEmulator::Initialize(ComponentsManager* manager)
|
|
{
|
|
componentsManager = manager;
|
|
componentsManager->SetIsInputEmulatorUsed(true);
|
|
|
|
inputState = GetInputStatePtr((void*)INPUT_STATE_PTR_ADDRESS);
|
|
inputState->HideCursor();
|
|
|
|
TestBinding = new Binding();
|
|
ServiceBinding = new Binding();
|
|
Sw1Binding = new Binding();
|
|
Sw2Binding = new Binding();
|
|
StartBinding = new Binding();
|
|
|
|
SankakuBinding = new Binding();
|
|
ShikakuBinding = new Binding();
|
|
BatsuBinding = new Binding();
|
|
MaruBinding = new Binding();
|
|
|
|
LeftBinding = new Binding();
|
|
RightBinding = new Binding();
|
|
|
|
MenuLBinding = new Binding();
|
|
MenuRBinding = new Binding();
|
|
|
|
MenuCircleBinding = new Binding();
|
|
|
|
CoinBinding = new Binding();
|
|
ToonBinding = new Binding();
|
|
WireframeBinding = new Binding();
|
|
|
|
FileSystem::ConfigFile configFile(framework::GetModuleDirectory(), KEY_CONFIG_FILE_NAME);
|
|
configFile.OpenRead();
|
|
|
|
Config::BindConfigKeys(configFile.ConfigMap, "JVS_TEST", *TestBinding, { "F1" });
|
|
Config::BindConfigKeys(configFile.ConfigMap, "JVS_SERVICE", *ServiceBinding, { "F2" });
|
|
Config::BindConfigKeys(configFile.ConfigMap, "JVS_SW1", *Sw1Binding, { "F11" });
|
|
Config::BindConfigKeys(configFile.ConfigMap, "JVS_SW2", *Sw2Binding, { "F12" });
|
|
Config::BindConfigKeys(configFile.ConfigMap, "JVS_START", *StartBinding, { "Enter" });
|
|
Config::BindConfigKeys(configFile.ConfigMap, "JVS_TRIANGLE", *SankakuBinding, { "W", "I" });
|
|
Config::BindConfigKeys(configFile.ConfigMap, "JVS_SQUARE", *ShikakuBinding, { "A", "J" });
|
|
Config::BindConfigKeys(configFile.ConfigMap, "JVS_CROSS", *BatsuBinding, { "S", "K" });
|
|
Config::BindConfigKeys(configFile.ConfigMap, "JVS_CIRCLE", *MaruBinding, { "D", "L" });
|
|
Config::BindConfigKeys(configFile.ConfigMap, "JVS_LEFT", *LeftBinding, { "Q", "U" });
|
|
Config::BindConfigKeys(configFile.ConfigMap, "JVS_RIGHT", *RightBinding, { "E", "O" });
|
|
Config::BindConfigKeys(configFile.ConfigMap, "MENU_L", *MenuLBinding, { "Left", "Up" });
|
|
Config::BindConfigKeys(configFile.ConfigMap, "MENU_R", *MenuRBinding, { "Down", "Right" });
|
|
Config::BindConfigKeys(configFile.ConfigMap, "MENU_CIRCLE", *MenuCircleBinding, { "D", "L", "Spacebar" });
|
|
Config::BindConfigKeys(configFile.ConfigMap, "TOON", *ToonBinding, { "F9" });
|
|
Config::BindConfigKeys(configFile.ConfigMap, "COIN", *CoinBinding, { "F10" });
|
|
Config::BindConfigKeys(configFile.ConfigMap, "WIREFRAME", *WireframeBinding, { "Backspace" });
|
|
|
|
mouseScrollPvSelection = configFile.GetBooleanValue("mouse_scroll_pv_selection");
|
|
|
|
TestBinding->AddBinding(new DivallerBinding(DivallerButton::DVL_L3));
|
|
ServiceBinding->AddBinding(new DivallerBinding(DivallerButton::DVL_L2));
|
|
CoinBinding->AddBinding(new DivallerBinding(DivallerButton::DVL_L1));
|
|
StartBinding->AddBinding(new DivallerBinding(DivallerButton::DVL_FN));
|
|
SankakuBinding->AddBinding(new DivallerBinding(DivallerButton::DVL_TRIANGLE));
|
|
ShikakuBinding->AddBinding(new DivallerBinding(DivallerButton::DVL_SQUARE));
|
|
BatsuBinding->AddBinding(new DivallerBinding(DivallerButton::DVL_CROSS));
|
|
MaruBinding->AddBinding(new DivallerBinding(DivallerButton::DVL_CIRCLE));
|
|
}
|
|
|
|
void InputEmulator::Update()
|
|
{
|
|
return;
|
|
}
|
|
|
|
void InputEmulator::OnFocusLost()
|
|
{
|
|
// to prevent buttons from being "stuck"
|
|
inputState->ClearState();
|
|
inputState->HideCursor();
|
|
}
|
|
|
|
void InputEmulator::UpdateInput()
|
|
{
|
|
if (!componentsManager->GetUpdateGameInput())
|
|
return;
|
|
|
|
if (!componentsManager->IsDwGuiActive())
|
|
{
|
|
UpdateJvsInput();
|
|
|
|
if (mouseScrollPvSelection && !componentsManager->IsDwGuiHovered())
|
|
UpdateMousePvScroll();
|
|
}
|
|
|
|
UpdateDwGuiInput();
|
|
}
|
|
|
|
void InputEmulator::UpdateJvsInput()
|
|
{
|
|
auto tappedFunc = [](void* binding) { return ((Binding*)binding)->AnyTapped(); };
|
|
auto releasedFunc = [](void* binding) { return ((Binding*)binding)->AnyReleased(); };
|
|
auto downFunc = [](void* binding) { return ((Binding*)binding)->AnyDown(); };
|
|
|
|
lastDownState = inputState->Down.Buttons;
|
|
|
|
inputState->Tapped.Buttons = GetJvsButtonsState(tappedFunc);
|
|
inputState->Released.Buttons = GetJvsButtonsState(releasedFunc);
|
|
inputState->Down.Buttons = GetJvsButtonsState(downFunc);
|
|
inputState->DoubleTapped.Buttons = GetJvsButtonsState(tappedFunc);
|
|
inputState->IntervalTapped.Buttons = GetJvsButtonsState(tappedFunc);
|
|
|
|
// if not interacting with L and R, use values from the slider instead
|
|
if ( ( (inputState->Tapped.Buttons | inputState->Released.Buttons | inputState->Down.Buttons)
|
|
& (JVS_L | JVS_R) ) == 0)
|
|
{
|
|
UpdateSliderLR();
|
|
}
|
|
|
|
SetMetaButtons();
|
|
|
|
UpdateHoldState();
|
|
heldButtons = GetButtonFromHold();
|
|
|
|
if ((lastDownState &= inputState->Tapped.Buttons) != 0)
|
|
{
|
|
inputState->Down.Buttons ^= inputState->Tapped.Buttons;
|
|
if (IsHold() && !TargetInspector::IsAnyRepress())
|
|
inputState->Down.Buttons |= heldButtons;
|
|
}
|
|
|
|
// repress held down buttons to not block input
|
|
//inputState->Down.Buttons ^= inputState->Tapped.Buttons;
|
|
|
|
if (CoinBinding->AnyTapped())
|
|
addCoin();
|
|
|
|
if (ToonBinding->AnyTapped())
|
|
toggleNpr1();
|
|
|
|
if (WireframeBinding->AnyTapped())
|
|
toggleWireframe();
|
|
}
|
|
|
|
HoldState InputEmulator::GetHoldState()
|
|
{
|
|
return (HoldState) * ((int*)HOLD_STATE_ADDRESS);
|
|
}
|
|
|
|
int InputEmulator::GetMaxHoldState()
|
|
{
|
|
return *(int*)MAX_HOLD_STATE_ADDRESS;
|
|
}
|
|
|
|
bool InputEmulator::IsHold()
|
|
{
|
|
return ((holdState != HOLD_NONE) && (GetMaxHoldState() != 1));
|
|
}
|
|
|
|
void InputEmulator::UpdateHoldState()
|
|
{
|
|
holdState = GetHoldState();
|
|
|
|
for (int i = 0; i < 4; ++i)
|
|
{
|
|
int holdId = 1 << (i + 6);
|
|
|
|
if ((holdId & holdState) != 0)
|
|
holdTbl[i] = 1;
|
|
else
|
|
holdTbl[i] = 0;
|
|
}
|
|
}
|
|
|
|
void InputEmulator::UpdateDwGuiInput()
|
|
{
|
|
auto keyboard = Keyboard::GetInstance();
|
|
auto mouse = Mouse::GetInstance();
|
|
|
|
auto pos = mouse->GetRelativePosition();
|
|
inputState->MouseX = (int)pos.x;
|
|
inputState->MouseY = (int)pos.y;
|
|
|
|
auto deltaPos = mouse->GetDeltaPosition();
|
|
inputState->MouseDeltaX = (int)deltaPos.x;
|
|
inputState->MouseDeltaY = (int)deltaPos.y;
|
|
|
|
inputState->Key = GetKeyState();
|
|
|
|
for (int i = 0; i < sizeof(keyBits) / sizeof(KeyBit); i++)
|
|
UpdateInputBit(keyBits[i].Bit, keyBits[i].KeyCode);
|
|
|
|
for (int i = InputBufferType_Tapped; i < InputBufferType_Max; i++)
|
|
{
|
|
inputState->SetBit(scrollUpBit, mouse->GetIsScrolledUp(), (InputBufferType)i);
|
|
inputState->SetBit(scrollDownBit, mouse->GetIsScrolledDown(), (InputBufferType)i);
|
|
}
|
|
}
|
|
|
|
void InputEmulator::UpdateMousePvScroll()
|
|
{
|
|
// I originally wanted to use a MouseBinding set to JVS_LEFT / JVS_RIGHT
|
|
// but that ended up being too slow because a PV slot can only be scrolled to once the scroll animation has finished playing
|
|
int* slotsToScroll = (int*)PV_SEL_SLOTS_TO_SCROLL;
|
|
int* modulesToScroll = (int*)MODULE_SEL_SLOTS_TO_SCROLL;
|
|
|
|
auto mouse = Mouse::GetInstance();
|
|
if (mouse->GetIsScrolledUp())
|
|
{
|
|
if (*(int*)PV_SEL_SLOTS_CONST < 26) *slotsToScroll -= 1;
|
|
if (*(int*)MODULE_IS_RECOMMENDED == 0) *modulesToScroll -= 1;
|
|
}
|
|
if (mouse->GetIsScrolledDown())
|
|
{
|
|
if (*(int*)PV_SEL_SLOTS_CONST < 26) *slotsToScroll += 1;
|
|
if (*(int*)MODULE_IS_RECOMMENDED == 0) *modulesToScroll += 1;
|
|
}
|
|
}
|
|
|
|
InputState* InputEmulator::GetInputStatePtr(void* address)
|
|
{
|
|
return (InputState*)(*(uint64_t*)address);
|
|
}
|
|
|
|
JvsButtons InputEmulator::GetJvsButtonsState(bool(*buttonTestFunc)(void*))
|
|
{
|
|
JvsButtons buttons = JVS_NONE;
|
|
|
|
if ((Pause::pause || !(*(GameState*)CURRENT_GAME_STATE_ADDRESS == GS_GAME && *(SubGameState*)CURRENT_GAME_SUB_STATE_ADDRESS == SUB_GAME_MAIN)) &&
|
|
buttonTestFunc(MenuLBinding))
|
|
{
|
|
buttons |= JVS_L;
|
|
return buttons;
|
|
}
|
|
if ((Pause::pause || !(*(GameState*)CURRENT_GAME_STATE_ADDRESS == GS_GAME && *(SubGameState*)CURRENT_GAME_SUB_STATE_ADDRESS == SUB_GAME_MAIN)) &&
|
|
buttonTestFunc(MenuRBinding))
|
|
{
|
|
buttons |= JVS_R;
|
|
return buttons;
|
|
}
|
|
if ((Pause::pause || !(*(GameState*)CURRENT_GAME_STATE_ADDRESS == GS_GAME && *(SubGameState*)CURRENT_GAME_SUB_STATE_ADDRESS == SUB_GAME_MAIN)) &&
|
|
buttonTestFunc(MenuCircleBinding))
|
|
{
|
|
buttons |= JVS_CIRCLE;
|
|
return buttons;
|
|
}
|
|
|
|
if (buttonTestFunc(TestBinding))
|
|
buttons |= JVS_TEST;
|
|
if (buttonTestFunc(ServiceBinding))
|
|
buttons |= JVS_SERVICE;
|
|
if (buttonTestFunc(Sw1Binding))
|
|
buttons |= JVS_SW1;
|
|
if (buttonTestFunc(Sw2Binding))
|
|
buttons |= JVS_SW2;
|
|
|
|
if (buttonTestFunc(StartBinding))
|
|
buttons |= JVS_START;
|
|
|
|
if (buttonTestFunc(SankakuBinding))
|
|
buttons |= JVS_TRIANGLE;
|
|
if (buttonTestFunc(ShikakuBinding))
|
|
buttons |= JVS_SQUARE;
|
|
if (buttonTestFunc(BatsuBinding))
|
|
buttons |= JVS_CROSS;
|
|
if (buttonTestFunc(MaruBinding))
|
|
buttons |= JVS_CIRCLE;
|
|
|
|
if (buttonTestFunc(LeftBinding))
|
|
buttons |= JVS_L;
|
|
if (buttonTestFunc(RightBinding))
|
|
buttons |= JVS_R;
|
|
|
|
return buttons;
|
|
}
|
|
|
|
JvsButtons InputEmulator::GetButtonFromHold()
|
|
{
|
|
JvsButtons buttons = JVS_NONE;
|
|
|
|
if (holdTbl[0])
|
|
buttons |= JVS_TRIANGLE;
|
|
if (holdTbl[1])
|
|
buttons |= JVS_CIRCLE;
|
|
if (holdTbl[2])
|
|
buttons |= JVS_CROSS;
|
|
if (holdTbl[3])
|
|
buttons |= JVS_SQUARE;
|
|
|
|
return buttons;
|
|
}
|
|
|
|
char InputEmulator::GetKeyState()
|
|
{
|
|
auto keyboard = Keyboard::GetInstance();
|
|
|
|
bool upper = keyboard->IsDown(VK_SHIFT);
|
|
constexpr char caseDifference = 'A' - 'a';
|
|
|
|
char inputKey = 0x00;
|
|
|
|
for (char key = '0'; key < 'Z'; key++)
|
|
{
|
|
if (keyboard->IsIntervalTapped(key))
|
|
inputKey = (upper || key < 'A') ? key : (key - caseDifference);
|
|
}
|
|
|
|
if (keyboard->IsIntervalTapped(VK_BACK))
|
|
inputKey = 0x08;
|
|
|
|
if (keyboard->IsIntervalTapped(VK_TAB))
|
|
inputKey = 0x09;
|
|
|
|
if (keyboard->IsIntervalTapped(VK_SPACE))
|
|
inputKey = 0x20;
|
|
|
|
if (keyboard->IsDoubleTapped(VK_ESCAPE))
|
|
*(bool*)SHOULD_EXIT_BOOL_ADDRESS = true;
|
|
|
|
return inputKey;
|
|
}
|
|
|
|
void InputEmulator::UpdateInputBit(uint32_t bit, uint8_t keycode)
|
|
{
|
|
auto keyboard = Keyboard::GetInstance();
|
|
|
|
inputState->SetBit(bit, keyboard->IsTapped(keycode), InputBufferType_Tapped);
|
|
inputState->SetBit(bit, keyboard->IsReleased(keycode), InputBufferType_Released);
|
|
inputState->SetBit(bit, keyboard->IsDown(keycode), InputBufferType_Down);
|
|
inputState->SetBit(bit, keyboard->IsDoubleTapped(keycode), InputBufferType_DoubleTapped);
|
|
inputState->SetBit(bit, keyboard->IsIntervalTapped(keycode), InputBufferType_IntervalTapped);
|
|
|
|
}
|
|
|
|
typedef uint8_t getSliderSensorFunc(void*, int);
|
|
void InputEmulator::UpdateSliderLR()
|
|
{
|
|
getSliderSensorFunc* getSliderSensorTapped = (getSliderSensorFunc*)GET_SLIDER_TAPPED_ADDRESS;
|
|
getSliderSensorFunc* getSliderSensorReleased = (getSliderSensorFunc*)GET_SLIDER_RELEASED_ADDRESS;
|
|
getSliderSensorFunc* getSliderSensorDown = (getSliderSensorFunc*)GET_SLIDER_DOWN_ADDRESS;
|
|
|
|
if (getSliderSensorTapped((void*)SLIDER_CTRL_TASK_ADDRESS, 36))
|
|
inputState->Tapped.Buttons |= JVS_L;
|
|
else
|
|
inputState->Tapped.Buttons &= ~JVS_L;
|
|
|
|
if (getSliderSensorReleased((void*)SLIDER_CTRL_TASK_ADDRESS, 36))
|
|
inputState->Released.Buttons |= JVS_L;
|
|
else
|
|
inputState->Released.Buttons &= ~JVS_L;
|
|
|
|
if (getSliderSensorDown((void*)SLIDER_CTRL_TASK_ADDRESS, 36))
|
|
inputState->Down.Buttons |= JVS_L;
|
|
else
|
|
inputState->Down.Buttons &= ~JVS_L;
|
|
|
|
|
|
if (getSliderSensorTapped((void*)SLIDER_CTRL_TASK_ADDRESS, 37))
|
|
inputState->Tapped.Buttons |= JVS_R;
|
|
else
|
|
inputState->Tapped.Buttons &= ~JVS_R;
|
|
|
|
if (getSliderSensorReleased((void*)SLIDER_CTRL_TASK_ADDRESS, 37))
|
|
inputState->Released.Buttons |= JVS_R;
|
|
else
|
|
inputState->Released.Buttons &= ~JVS_R;
|
|
|
|
if (getSliderSensorDown((void*)SLIDER_CTRL_TASK_ADDRESS, 37))
|
|
inputState->Down.Buttons |= JVS_R;
|
|
else
|
|
inputState->Down.Buttons &= ~JVS_R;
|
|
}
|
|
|
|
void InputEmulator::SetMetaButtons()
|
|
{
|
|
// bit 0x6e is used to skip a bunch of screens
|
|
if ((inputState->Down.Buttons & (JVS_L | JVS_R)) == 0) // ask sega, okay? idk why this is needed
|
|
{
|
|
if ((inputState->Tapped.Buttons & (JVS_START | JVS_TRIANGLE | JVS_SQUARE | JVS_CROSS | JVS_CIRCLE)) != 0)
|
|
inputState->SetBit(0x6e, true, InputBufferType_Tapped);
|
|
else
|
|
inputState->SetBit(0x6e, false, InputBufferType_Tapped);
|
|
|
|
if ((inputState->Down.Buttons & (JVS_START | JVS_TRIANGLE | JVS_SQUARE | JVS_CROSS | JVS_CIRCLE)) != 0)
|
|
inputState->SetBit(0x6e, true, InputBufferType_Down);
|
|
else
|
|
inputState->SetBit(0x6e, false, InputBufferType_Down);
|
|
|
|
if ((inputState->Released.Buttons & (JVS_START | JVS_TRIANGLE | JVS_SQUARE | JVS_CROSS | JVS_CIRCLE)) != 0)
|
|
inputState->SetBit(0x6e, true, InputBufferType_Released);
|
|
else
|
|
inputState->SetBit(0x6e, false, InputBufferType_Released);
|
|
}
|
|
}
|
|
|
|
void InputEmulator::addCoin()
|
|
{
|
|
printf("[TLAC] Adding coin\n");
|
|
unsigned char* credits = (unsigned char*)0x14CD93788;
|
|
if (*credits < 9)
|
|
{
|
|
*(unsigned char*)0x14CD93A9C = 1;
|
|
(*credits)++;
|
|
(*(unsigned char*)0x14CD93A98)++;
|
|
if ((*(unsigned char*)0x14CD93A98) == 0) (*(unsigned char*)0x14CD93A98)++;
|
|
}
|
|
}
|
|
|
|
void InputEmulator::toggleNpr1()
|
|
{
|
|
switch(*(byte*)0x0000000140502FC6)
|
|
{
|
|
case 0xC3: // default -> force on
|
|
InjectCode((void*)0x0000000140502FC0, { 0xC7, 0x05, 0x6E });
|
|
InjectCode((void*)0x0000000140502FC6, { 0x01, 0x00, 0x00, 0x00, 0xC3 });
|
|
*(byte*)0x00000001411AD638 = 1;
|
|
|
|
printf("[TLAC] NPR1 forced\n");
|
|
break;
|
|
case 0x01: // default -> force off
|
|
InjectCode((void*)0x0000000140502FC0, { 0xC7, 0x05, 0x6E });
|
|
InjectCode((void*)0x0000000140502FC6, { 0x00, 0x00, 0x00, 0x00, 0xC3 });
|
|
*(byte*)0x00000001411AD638 = 0;
|
|
|
|
printf("[TLAC] NPR1 disabled\n");
|
|
break;
|
|
default: // force off -> default
|
|
InjectCode((void*)0x0000000140502FC0, { 0x89, 0x0D, 0x72 });
|
|
InjectCode((void*)0x0000000140502FC6, { 0xC3, 0xCC, 0xCC, 0xCC, 0xCC });
|
|
|
|
printf("[TLAC] NPR1 restored\n");
|
|
}
|
|
}
|
|
|
|
void InputEmulator::toggleWireframe()
|
|
{
|
|
static bool wireframeEnabled = false;
|
|
|
|
if (!wireframeEnabled)
|
|
{
|
|
// inject samyuu's wireframe code
|
|
InjectCode((void*)0x00000000140500BE6, { 0xBA, 0x01, 0x1B, 0x00, 0x00, // mov edx,00001B01
|
|
0xB9, 0x08, 0x04, 0x00, 0x00, // mov ecx,00000408
|
|
0xE8, 0xE1, 0x5A, 0x3B, 0x00, // call 1408B66D6
|
|
0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90 });
|
|
|
|
wireframeEnabled = true;
|
|
printf("[TLAC] Wireframe enabled\n");
|
|
}
|
|
else
|
|
{
|
|
InjectCode((void*)0x00000000140500BE6, { 0xB9, 0x1C, 0x00, 0x00, 0x00, 0xE8, 0x30, 0x7C, 0xF3, 0xFF, 0xB9, 0x1B, 0x00, 0x00, 0x00, 0x8B, 0xD8, 0xE8, 0x24, 0x7C, 0xF3, 0xFF, 0xB9, 0x1A, 0x00, 0x00, 0x00 });
|
|
|
|
wireframeEnabled = false;
|
|
printf("[TLAC] Wireframe disabled\n");
|
|
}
|
|
}
|
|
} |