mirror of
https://github.com/TheOnlyZac/sly1.git
synced 2024-11-26 23:20:43 +00:00
Cleanup and update documentation
This commit is contained in:
parent
5b6ead71b4
commit
773afd902c
@ -75,7 +75,7 @@ cd scripts
|
||||
|
||||
**Prerequisites**: `git`, `make`, `7zip`
|
||||
|
||||
*Note: Building on Windows is temporarily not working. You can still build on Windows using WSL.*
|
||||
*Note: Building Windows is untested with the new build system. If you encounter issues, you can still build on Windows using WSL.*
|
||||
|
||||
```powershell
|
||||
cd scripts
|
||||
@ -142,7 +142,6 @@ The project is divided into the following directories:
|
||||
* `config` - Config files for Splat (binary splitting tool).
|
||||
* `scripts` - Utility scripts for setting up the build environment.
|
||||
* `docs` - Documentation and instructions for contributing.
|
||||
* `test` - Handwritten unit tests for the decomp code.
|
||||
* `tools` - Utilities for function matching.
|
||||
* `reference` - Reference files for functions and data structures.
|
||||
|
||||
|
1
config/readme.txt
Normal file
1
config/readme.txt
Normal file
@ -0,0 +1 @@
|
||||
This directory contains config files for Splat, which is used for Binary splitting.
|
1
disc/readme.txt
Normal file
1
disc/readme.txt
Normal file
@ -0,0 +1 @@
|
||||
To build the project, you need to extract the executable file SCUS_971.98 from your own copy of the game and put it in this directory.
|
@ -1,4 +1,4 @@
|
||||
This directory contains reference files you can use to help with decompiling game functions. They are all source code files we wrote by hand before we used Splat to decompile and reassemble the game to a byte-matching executable. Because of this, none of the functions in this directory is matching, but we tried to make them as close as possible to the original game functions.
|
||||
This directory contains reference files you can use to help with decompiling game functions. They are all source code files we wrote by hand before we used Splat to decompile and reassemble the game to a byte-matching executable. Because of this, none of the functions in this directory match, but we tried to make them as close as possible to the original game functions.
|
||||
|
||||
The include directory contains header files that are used by the source files in the src dir. Many of the short data structures/enums are correct and can be copied directly into the actual decomp. The longer ones are probably not correct (missing fields, wrong field types, etc), but they can be used as a reference.
|
||||
|
||||
|
@ -1,24 +0,0 @@
|
||||
macro(add_unit_test)
|
||||
set(options PARALLEL)
|
||||
set(oneValueArgs NAME)
|
||||
set(multiValueArgs SOURCES LIBS)
|
||||
cmake_parse_arguments(TEST "${options}" "${oneValueArgs}"
|
||||
"${multiValueArgs}" ${ARGN})
|
||||
message(STATUS "Generating Test ${TEST_NAME}... (${TEST_SOURCES})")
|
||||
add_executable(${TEST_NAME} EXCLUDE_FROM_ALL ${TEST_SOURCES})
|
||||
target_link_libraries(${TEST_NAME} ${TEST_LIBS})
|
||||
if(TEST_PARALLEL AND HAVE_MPI)
|
||||
set(TESTCOMMAND ${MPIEXEC})
|
||||
set(TESTARGS ${MPIEXEC_NUMPROC_FLAG} 3 ${MPIEXEC_PREFLAGS}
|
||||
"./${TEST_NAME}" ${MPIEXEC_POSTFLAGS})
|
||||
set(TESTCOMMAND ${TESTCOMMAND} ${TESTARGS})
|
||||
else()
|
||||
set(TESTCOMMAND ${TEST_NAME})
|
||||
endif()
|
||||
add_test(NAME ${TEST_NAME}
|
||||
WORKING_DIRECTORY ${CMAKE_BINARY_DIR}/bin/tests
|
||||
COMMAND ${TESTCOMMAND})
|
||||
|
||||
get_property(TESTNAMES GLOBAL PROPERTY TESTNAMES)
|
||||
set_property(GLOBAL PROPERTY TESTNAMES ${TESTNAMES} ${TEST_NAME})
|
||||
endmacro(add_unit_test)
|
@ -1,2 +0,0 @@
|
||||
|
||||
add_unit_test(PARALLEL TRUE NAME clock.set_clock_rate SOURCES set_clock_rate.cpp LIBS ${P2_LIB_TARGET})
|
@ -1,24 +0,0 @@
|
||||
#include <clock.h>
|
||||
|
||||
#include <test/test.h>
|
||||
|
||||
int main()
|
||||
{
|
||||
SetClockRate(1.0);
|
||||
JtAssert(g_rtClock == 1.0f);
|
||||
JtAssert(g_clock.fEnabled);
|
||||
|
||||
SetClockRate(0.5);
|
||||
JtAssert(g_rtClock == 0.5f);
|
||||
JtAssert(g_clock.fEnabled);
|
||||
|
||||
SetClockRate(0);
|
||||
JtAssert(g_rtClock == 0.f);
|
||||
JtAssert(!g_clock.fEnabled);
|
||||
|
||||
SetClockRate(-1);
|
||||
JtAssert(g_rtClock == -1.f);
|
||||
JtAssert(!g_clock.fEnabled);
|
||||
|
||||
return 0;
|
||||
}
|
@ -1,2 +0,0 @@
|
||||
|
||||
add_unit_test(PARALLEL TRUE NAME coin.collect_coins SOURCES collect_coins.cpp LIBS ${P2_LIB_TARGET})
|
@ -1,49 +0,0 @@
|
||||
#include <coin.h>
|
||||
#include <gs.h>
|
||||
|
||||
#include <test/test.h>
|
||||
|
||||
int main()
|
||||
{
|
||||
COIN coin;
|
||||
|
||||
// Test collecting a coin normally
|
||||
g_pgsCur->ccoin = 0;
|
||||
g_pgsCur->ccharm = 0;
|
||||
OnCoinSmack(&coin);
|
||||
|
||||
JtAssert(g_pgsCur->ccoin == 1);
|
||||
JtAssert(g_pgsCur->ccharm == 0);
|
||||
|
||||
g_pgsCur->ccoin = 98;
|
||||
OnCoinSmack(&coin);
|
||||
|
||||
JtAssert(g_pgsCur->ccoin == 99);
|
||||
JtAssert(g_pgsCur->ccharm == 0);
|
||||
|
||||
// Test collecting a 100 coins to get a charm
|
||||
OnCoinSmack(&coin);
|
||||
|
||||
JtAssert(g_pgsCur->ccoin == 0);
|
||||
JtAssert(g_pgsCur->ccharm == 1);
|
||||
|
||||
// Test collecting 100 coins to get an extra life
|
||||
g_pgsCur->ccoin = 99;
|
||||
g_pgsCur->ccharm = 2;
|
||||
g_pgsCur->clife = 5;
|
||||
OnCoinSmack(&coin);
|
||||
|
||||
JtAssert(g_pgsCur->ccoin == 0);
|
||||
JtAssert(g_pgsCur->ccharm == 2);
|
||||
JtAssert(g_pgsCur->clife == 6);
|
||||
|
||||
// Test collecting a coin when coins, lives, and charms are all at max
|
||||
g_pgsCur->ccoin = 99;
|
||||
g_pgsCur->ccharm = 2;
|
||||
g_pgsCur->clife = 99;
|
||||
OnCoinSmack(&coin);
|
||||
|
||||
JtAssert(g_pgsCur->ccoin == 99);
|
||||
JtAssert(g_pgsCur->ccharm == 2);
|
||||
JtAssert(g_pgsCur->clife == 99);
|
||||
}
|
@ -1,3 +0,0 @@
|
||||
|
||||
add_unit_test(PARALLEL TRUE NAME difficulty.change_suck SOURCES change_suck.cpp LIBS ${P2_LIB_TARGET})
|
||||
add_unit_test(PARALLEL TRUE NAME difficulty.world_preload SOURCES world_preload.cpp LIBS ${P2_LIB_TARGET})
|
@ -1,29 +0,0 @@
|
||||
#include <difficulty.h>
|
||||
#include <gs.h>
|
||||
|
||||
#include <test/test.h>
|
||||
|
||||
int main()
|
||||
{
|
||||
// Initialize difficulty
|
||||
OnDifficultyGameLoad(&g_difficulty);
|
||||
OnDifficultyWorldPreLoad(&g_difficulty);
|
||||
OnDifficultyWorldPostLoad(&g_difficulty);
|
||||
|
||||
// Test changing level suck
|
||||
g_plsCur->uSuck = 0.0f;
|
||||
JtAssert(g_plsCur->uSuck == 0.0f);
|
||||
|
||||
ChangeSuck(0.1, &g_difficulty);
|
||||
JtAssert(g_plsCur->uSuck == 0.1f);
|
||||
|
||||
ChangeSuck(1.0, &g_difficulty);
|
||||
JtAssert(g_plsCur->uSuck == 1.0f);
|
||||
|
||||
ChangeSuck(-1.0, &g_difficulty);
|
||||
JtAssert(g_plsCur->uSuck == -1.0f);
|
||||
|
||||
// Test collect key scenario
|
||||
OnDifficultyCollectKey(&g_difficulty);
|
||||
JtAssert(g_plsCur->uSuck == 0.0f);
|
||||
}
|
@ -1,48 +0,0 @@
|
||||
#include <difficulty.h>
|
||||
#include <gs.h>
|
||||
|
||||
#include <test/test.h>
|
||||
|
||||
void SetGameLevel(GAMEWORLD gameworld, WORLDLEVEL worldlevel);
|
||||
|
||||
int main()
|
||||
{
|
||||
// Initialize difficulty
|
||||
OnDifficultyGameLoad(&g_difficulty);
|
||||
|
||||
|
||||
// jb_intro = easy
|
||||
SetGameLevel(GAMEWORLD_Intro, WORLDLEVEL_Approach);
|
||||
OnDifficultyWorldPreLoad(&g_difficulty);
|
||||
JtAssert(g_difficulty.pdifficultyLevel == &g_difficultyEasy);
|
||||
|
||||
|
||||
// cw_security = easy
|
||||
SetGameLevel(GAMEWORLD_Clockwerk, WORLDLEVEL_Level2);
|
||||
OnDifficultyWorldPreLoad(&g_difficulty);
|
||||
JtAssert(g_difficulty.pdifficultyLevel == &g_difficultyEasy);
|
||||
|
||||
|
||||
// uw_bonus_security
|
||||
SetGameLevel(GAMEWORLD_Underwater, WORLDLEVEL_Level3);
|
||||
|
||||
// no key = medium
|
||||
g_plsCur->fls = static_cast<FLS>((int)g_plsCur->fls & ~(int)FLS_KeyCollected); // unset KeyCollected flag
|
||||
OnDifficultyWorldPreLoad(&g_difficulty);
|
||||
JtAssert(g_difficulty.pdifficultyLevel == &g_difficultyMedium);
|
||||
|
||||
// with key = hard
|
||||
g_plsCur->fls = static_cast<FLS>((int)g_plsCur->fls | (int)FLS_KeyCollected); // setKeyCollected flag
|
||||
OnDifficultyWorldPreLoad(&g_difficulty);
|
||||
JtAssert(g_difficulty.pdifficultyLevel == &g_difficultyHard);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
void SetGameLevel(GAMEWORLD gameworld, WORLDLEVEL worldlevel)
|
||||
{
|
||||
g_pwsCur = &g_pgsCur->aws[(int)gameworld];
|
||||
g_plsCur = &g_pwsCur->als[(int)worldlevel];
|
||||
g_pgsCur->gameworldCur = gameworld;
|
||||
g_pgsCur->worldlevelCur = worldlevel;
|
||||
}
|
@ -1,4 +0,0 @@
|
||||
|
||||
add_unit_test(PARALLEL TRUE NAME game.set_coin_count SOURCES set_coin_count.cpp LIBS ${P2_LIB_TARGET})
|
||||
add_unit_test(PARALLEL TRUE NAME game.set_charm_count SOURCES set_charm_count.cpp LIBS ${P2_LIB_TARGET})
|
||||
add_unit_test(PARALLEL TRUE NAME game.charm_available SOURCES charm_available.cpp LIBS ${P2_LIB_TARGET})
|
@ -1,43 +0,0 @@
|
||||
#include <game.h>
|
||||
#include <joy.h>
|
||||
#include <gs.h>
|
||||
|
||||
#include <test/test.h>
|
||||
|
||||
int main()
|
||||
{
|
||||
g_pgsCur->ccoin = 1; // set coins to 1
|
||||
g_grfcht &= ~((int)FCHT_InfiniteCharms); // disable infinite charms
|
||||
|
||||
// Confirm max charm count is 2
|
||||
JtAssert(CcharmMost() == 2);
|
||||
|
||||
// Test checking if a charm is available
|
||||
g_pgsCur->ccharm = 0;
|
||||
JtAssert(FCharmAvailable() == false);
|
||||
|
||||
g_pgsCur->ccharm = 1;
|
||||
JtAssert(FCharmAvailable() == true);
|
||||
|
||||
g_pgsCur->ccharm = -1;
|
||||
JtAssert(FCharmAvailable() == false);
|
||||
|
||||
g_pgsCur->ccharm = 0;
|
||||
g_grfcht |= (int)FCHT_InfiniteCharms; // enable infinite charms cheat
|
||||
JtAssert(FCharmAvailable() == true);
|
||||
|
||||
// Test setting charm count
|
||||
SetCcharm(0);
|
||||
JtAssert(g_pgsCur->ccharm == 0);
|
||||
|
||||
SetCcharm(1);
|
||||
JtAssert(g_pgsCur->ccharm == 1);
|
||||
|
||||
SetCcharm(3);
|
||||
JtAssert(g_pgsCur->ccharm == 3);
|
||||
|
||||
SetCcharm(-1);
|
||||
JtAssert(g_pgsCur->ccharm == -1);
|
||||
|
||||
return 0;
|
||||
}
|
@ -1,26 +0,0 @@
|
||||
#include <game.h>
|
||||
#include <joy.h>
|
||||
#include <gs.h>
|
||||
|
||||
#include <test/test.h>
|
||||
|
||||
int main()
|
||||
{
|
||||
// Confirm max charm count is 2
|
||||
JtAssert(CcharmMost() == 2);
|
||||
|
||||
// Test setting charm count
|
||||
SetCcharm(0);
|
||||
JtAssert(g_pgsCur->ccharm == 0);
|
||||
|
||||
SetCcharm(1);
|
||||
JtAssert(g_pgsCur->ccharm == 1);
|
||||
|
||||
SetCcharm(3);
|
||||
JtAssert(g_pgsCur->ccharm == 3);
|
||||
|
||||
SetCcharm(-1);
|
||||
JtAssert(g_pgsCur->ccharm == -1);
|
||||
|
||||
return 0;
|
||||
}
|
@ -1,29 +0,0 @@
|
||||
#include <game.h>
|
||||
#include <joy.h>
|
||||
#include <gs.h>
|
||||
|
||||
#include <test/test.h>
|
||||
|
||||
int main()
|
||||
{
|
||||
g_pgsCur->ccoin = 1; // set coins to 1
|
||||
g_pgsCur->ccharm = 0; // set charms to 0
|
||||
|
||||
// Test setting coin count
|
||||
SetCcoin(0);
|
||||
JtAssert(g_pgsCur->ccoin == 0);
|
||||
|
||||
SetCcoin(14);
|
||||
JtAssert(g_pgsCur->ccoin == 14);
|
||||
|
||||
SetCcoin(99);
|
||||
JtAssert(g_pgsCur->ccoin == 99);
|
||||
|
||||
SetCcoin(-1);
|
||||
JtAssert(g_pgsCur->ccoin == -1);
|
||||
|
||||
SetCcoin(101);
|
||||
JtAssert(g_pgsCur->ccoin == 101);
|
||||
|
||||
return 0;
|
||||
}
|
@ -1,2 +0,0 @@
|
||||
|
||||
add_unit_test(PARALLEL TRUE NAME gs.calculate_gs_percent SOURCES calculate_gs_percent.cpp LIBS ${P2_LIB_TARGET})
|
@ -1,14 +0,0 @@
|
||||
#include <gs.h>
|
||||
|
||||
#include <test/test.h>
|
||||
|
||||
int main()
|
||||
{
|
||||
PopulatePchzLevelTable();
|
||||
|
||||
// Test save file percent calculation
|
||||
int percent = CalculatePercentCompletion(g_pgsCur);
|
||||
JtAssert(percent == 0);
|
||||
|
||||
return 0;
|
||||
}
|
@ -1,3 +0,0 @@
|
||||
|
||||
add_unit_test(PARALLEL TRUE NAME joy.chetkido SOURCES chetkido.cpp LIBS ${P2_LIB_TARGET})
|
||||
add_unit_test(PARALLEL TRUE NAME joy.add_fcht SOURCES add_fcht.cpp LIBS ${P2_LIB_TARGET})
|
@ -1,23 +0,0 @@
|
||||
#include <joy.h>
|
||||
|
||||
#include <test/test.h>
|
||||
|
||||
int main()
|
||||
{
|
||||
// Test setting setting cheat flag
|
||||
g_grfcht = 0x0;
|
||||
|
||||
AddFcht((int)FCHT_InfiniteCharms);
|
||||
JtAssert(g_grfcht == 0x2);
|
||||
|
||||
AddFcht((int)FCHT_LowGravity);
|
||||
JtAssert(g_grfcht == 0x6);
|
||||
|
||||
AddFcht((int)FCHT_Invulnerability);
|
||||
JtAssert(g_grfcht == 0x7);
|
||||
|
||||
AddFcht((int)FCHT_LowFriction);
|
||||
JtAssert(g_grfcht == 0xF);
|
||||
|
||||
return 0;
|
||||
}
|
@ -1,25 +0,0 @@
|
||||
#include <joy.h>
|
||||
#include <gs.h>
|
||||
|
||||
#include <test/test.h>
|
||||
|
||||
#include <cstring>
|
||||
|
||||
int main()
|
||||
{
|
||||
// Test chetkido string output
|
||||
g_pgsCur->gameworldCur = GAMEWORLD_Snow;
|
||||
g_pgsCur->worldlevelCur = WORLDLEVEL_Approach;
|
||||
g_pgsCur->ccoin = 99;
|
||||
g_pgsCur->clife = 0;
|
||||
|
||||
CheatActivateChetkido();
|
||||
JtAssert(strstr(chetkido_buffer, "The password is: chetkido") != NULL);
|
||||
|
||||
g_pgsCur->ccoin = 98;
|
||||
|
||||
CheatActivateChetkido();
|
||||
JtAssert(strstr(chetkido_buffer, "The password is: chetkido") == NULL);
|
||||
|
||||
return 0;
|
||||
}
|
30
test/test.h
30
test/test.h
@ -1,30 +0,0 @@
|
||||
/**
|
||||
* @file test.h
|
||||
*
|
||||
* @brief Contains macros for testing and debugging.
|
||||
*/
|
||||
#ifndef TEST_H
|
||||
#define TEST_H
|
||||
|
||||
#include <stdexcept>
|
||||
#include <exception>
|
||||
|
||||
#define __FILENAME__ (strrchr(__FILE__, '/') ? strrchr(__FILE__, '/') + 1 : __FILE__)
|
||||
|
||||
// Custom assert macro that doesn't call abort
|
||||
#define JtAssert(assertion) \
|
||||
{ \
|
||||
if (!(assertion)) \
|
||||
{ \
|
||||
printf( \
|
||||
"\nAn assertion failure has occurred\n" \
|
||||
"========================================\n" \
|
||||
"Assertion: %s\n" \
|
||||
"File: %s\nLine %d, in %s\n" \
|
||||
"========================================\n\n", \
|
||||
#assertion, __FILENAME__, __LINE__, __FUNCTION__); \
|
||||
exit(1); \
|
||||
} \
|
||||
}
|
||||
|
||||
#endif // TEST_H
|
@ -1,3 +0,0 @@
|
||||
|
||||
add_unit_test(PARALLEL TRUE NAME util.limit_lm SOURCES limit_lm.cpp LIBS ${P2_LIB_TARGET})
|
||||
add_unit_test(PARALLEL TRUE NAME util.limit_abs SOURCES limit_abs.cpp LIBS ${P2_LIB_TARGET})
|
@ -1,36 +0,0 @@
|
||||
#include <util.h>
|
||||
|
||||
#include <test/test.h>
|
||||
|
||||
// disable warning for truncating double to float
|
||||
#pragma warning(disable: 4305)
|
||||
|
||||
int main()
|
||||
{
|
||||
// Clamp value between -1 and 1
|
||||
JtAssert(GLimitAbs(-5, 1) == -1.f);
|
||||
JtAssert(GLimitAbs(-1, 1) == -1.f);
|
||||
JtAssert(GLimitAbs(-0.4, 1) == -0.4f);
|
||||
JtAssert(GLimitAbs(0.76, 1) == 0.76f);
|
||||
JtAssert(GLimitAbs(1, 1) == 1.f);
|
||||
JtAssert(GLimitAbs(17, 1) == 1.f);
|
||||
|
||||
// Clamp value between -3.14 and 3.14
|
||||
JtAssert(GLimitAbs(-5, 3.14) == -3.14f);
|
||||
JtAssert(GLimitAbs(-1, 3.14) == -1.f);
|
||||
JtAssert(GLimitAbs(-0.4, 3.14) == -0.4f);
|
||||
JtAssert(GLimitAbs(0.76, 3.14) == 0.76f);
|
||||
JtAssert(GLimitAbs(3.14, 3.14) == 3.14f);
|
||||
JtAssert(GLimitAbs(17, 3.14) == 3.14f);
|
||||
|
||||
// Clamp value between -100 and 100
|
||||
JtAssert(GLimitAbs(-1000, 100) == -100.f);
|
||||
JtAssert(GLimitAbs(-100, 100) == -100.f);
|
||||
JtAssert(GLimitAbs(-17.3, 100) == -17.3f);
|
||||
JtAssert(GLimitAbs(0, 100) == 0.f);
|
||||
JtAssert(GLimitAbs(91, 100) == 91.f);
|
||||
JtAssert(GLimitAbs(100.2, 100) == 100.f);
|
||||
JtAssert(GLimitAbs(420.69, 100) == 100.f);
|
||||
|
||||
return 0;
|
||||
}
|
@ -1,27 +0,0 @@
|
||||
#include <util.h>
|
||||
|
||||
#include <test/test.h>
|
||||
|
||||
// disable warning for truncating double to float
|
||||
#pragma warning(disable: 4305)
|
||||
|
||||
int main()
|
||||
{
|
||||
// Clamp value using global 0-1 limit
|
||||
JtAssert(GLimitLm(&g_lmZeroOne, -2.3) == 0.f);
|
||||
JtAssert(GLimitLm(&g_lmZeroOne, 0) == 0.f);
|
||||
JtAssert(GLimitLm(&g_lmZeroOne, 0.234) == 0.234f);
|
||||
JtAssert(GLimitLm(&g_lmZeroOne, 0.7) == 0.7f);
|
||||
JtAssert(GLimitLm(&g_lmZeroOne, 1) == 1.f);
|
||||
JtAssert(GLimitLm(&g_lmZeroOne, 4) == 1.f);
|
||||
|
||||
// Clamp value using local limit
|
||||
LM lmFiveTen(5, 10);
|
||||
JtAssert(GLimitLm(&lmFiveTen, 1) == 5.f);
|
||||
JtAssert(GLimitLm(&lmFiveTen, 5) == 5.f);
|
||||
JtAssert(GLimitLm(&lmFiveTen, 7) == 7.f);
|
||||
JtAssert(GLimitLm(&lmFiveTen, 10) == 10.f);
|
||||
JtAssert(GLimitLm(&lmFiveTen, 99) == 10.f);
|
||||
|
||||
return 0;
|
||||
}
|
@ -1,2 +0,0 @@
|
||||
|
||||
add_unit_test(PARALLEL TRUE NAME xform.set_exits SOURCES set_exits.cpp LIBS ${P2_LIB_TARGET})
|
@ -1,25 +0,0 @@
|
||||
#include <xform.h>
|
||||
#include <cassert>
|
||||
|
||||
#include <test/test.h>
|
||||
|
||||
int main()
|
||||
{
|
||||
EXIT exit;
|
||||
|
||||
// todo: why does the custom assert macro cause errors here
|
||||
|
||||
SetExitExits(&exit, EXITS_Blocked);
|
||||
assert(exit.fKeyed == EXITS_Blocked);
|
||||
|
||||
SetExitExits(&exit, EXITS_Disabled);
|
||||
assert(exit.fKeyed == EXITS_Disabled);
|
||||
|
||||
SetExitExits(&exit, EXITS_Enabled);
|
||||
assert(exit.fKeyed == EXITS_Enabled);
|
||||
|
||||
SetExitExits(&exit, EXITS_Exiting);
|
||||
assert(exit.fKeyed == EXITS_Exiting);
|
||||
|
||||
return 0;
|
||||
}
|
Loading…
Reference in New Issue
Block a user