If you want to help us maintaining this wiki, check out our discord server: https://discord.gg/3u69jMa 

Difference between revisions of "Republic Commando UCC"

From SWRC Wiki
Jump to navigation Jump to search
Line 15: Line 15:
/*
/*
* This code compiles to a dll. In order to use it, SWRepublicCommando.exe needs to be modified so
* This code compiles to a dll. In order to use it, SWRepublicCommando.exe needs to be modified so
* that it calls this dll's 'uccMain' instead of 'appInit' from Core.dll
* that it calls this dll's 'uccMain' instead of 'appInit' from Core.dll.
* This is necessary because using the UT99 headers the call to appInit always fails due to the parameters'
* This is necessary because the call to appInit always fails due to problems with FConfigCacheIni
* types being substantially different in RC (especially FConfigCacheIni). By replacing the appInit call in
* that have not yet been possible to solve completely. By intercepting the call to appInit in the original
* SWRepublicCommando.exe it is possible to use the parameters that are originally passed by the game and since
* game executable it is possible to simply use the parameters that are passed by the game.
* they're just pointers it's no problem to use them to call appInit from Core.dll.
* Also the exe's subsystem has to be changed from window to console for obvious reasons...
* Everything compiles fine with Visual Studio .NET 2003 which is being used to achieve maximum compatibility
* Everything compiles fine with Visual Studio .NET 2003 which is being used to achieve maximum compatibility
* since it was also used to compile RC
* since it was also used to compile RC
* The following settings are required in order to compile everything without errors:
* The following settings are required in order to compile everything without errors:
* - Character Set = Not Set
* - Character Set = Not Set //Important as RC does not use unicode
* - Struct Member Alignment = 4 Bytes
* - Struct Member Alignment = 4 Bytes //Probably not necessary, but just in case...
* - Calling Convention = __fastcall
* - Calling Convention = __fastcall //RC uses __fastcall as default calling convention
*/
*/


#include <Windows.h>
#include <Windows.h>


#include <cstdlib> //__argc, __argv
#include <cstdlib> //__argc, __argv
#include <cctype> //std::toupper
#include <cstdio> //puts, fgets
#include <cstdio> //puts, fgets


#include "Engine/Inc/Engine.h" //Core and Engine
#include "../../Engine/Inc/Engine.h" //Core and Engine
#include "Core/Inc/FOutputDeviceWindowsError.h" //For modal error messages that are not displayed in the console
#include "../../Core/Inc/FOutputDeviceWindowsError.h" //For modal error messages that are not displayed in the console


namespace{
namespace{
//Global variables
//Variables used by FFeedbackContextUCCSerialize


FOutputDevice* FileLog; //Used by FFeedbackContextAnsiSerialize to write to the log file because GLog might be reassigned
FOutputDevice* FileLog; //Used by FFeedbackContextAnsiSerialize to write to the log file because GLog might be reassigned
int WarningCount = 0; //Incremented whenever a warning occurs in FFeedbackContextAnsiSerialize
int WarningCount = 0; //Incremented whenever a warning occurs in FFeedbackContextAnsiSerialize
int ErrorCount = 0; //Same as WarningCount just with errors...
int ErrorCount = 0; //Same as WarningCount just with errors...
 
//Variables for ServerCommandlet
 
FString CurrentCmd; //Contains the next command that is to be passed to UEngine::Exec gathered by the input thread
//the command is then executed by the main thread to avoid issues


/*
/*
* Replacement for Serialize function of 'InWarn', passed to appInit which is an FFeedbackContextWindows
* Replacement for Serialize function of 'InWarn', passed to appInit which is an FFeedbackContextWindows
*/
*/
void __stdcall FFeedbackContextAnsiSerialize(const TCHAR* V, EName Event){
void __stdcall FFeedbackContextUCCSerialize(const TCHAR* V, EName Event){
TCHAR Buffer[1024]= "";
TCHAR Buffer[1024]= "";
const TCHAR* Temp = V;
const TCHAR* Temp = V;
Line 55: Line 57:
SetConsoleTitle(V);
SetConsoleTitle(V);


return; //Prevents the server from spamming the player count to the log
return; //Prevents the server from spamming the player count to the log
}else if(Event == NAME_Heading){
}else if(Event == NAME_Heading){
appSprintf(Buffer, "\n--------------------%s--------------------", V);
appSprintf(Buffer, "\n--------------------%s--------------------", V);


Temp = Buffer;
Temp = Buffer;
V = Buffer; //So that the log file also contains the formatted string
V = Buffer; //So that the log file also contains the formatted string
}else if(Event == NAME_Warning || Event == NAME_ExecWarning || Event == NAME_ScriptWarning){
}else if(Event == NAME_Warning || Event == NAME_ExecWarning || Event == NAME_ScriptWarning){
appSprintf(Buffer, "%s: %s", *FName(Event), V);
appSprintf(Buffer, "%s: %s", *FName(Event), V);
Line 76: Line 78:


std::puts(Temp);
std::puts(Temp);
FileLog->Serialize(V, Event);
FileLog->Serialize(V, Event);
}
}
Line 82: Line 83:
/*
/*
* Allows user input in the console while running a server
* Allows user input in the console while running a server
* This function runs in a separate thread, not sure if that is a good solution though...
* This function runs in a separate thread in order to not having
* to pause the main loop while waiting for input
*/
*/
DWORD WINAPI UpdateServerConsoleInput(PVOID){
DWORD WINAPI UpdateServerConsoleInput(PVOID){
Line 88: Line 90:


while(GIsRunning && !GIsRequestingExit){
while(GIsRunning && !GIsRequestingExit){
if(std::fgets(Cmd, sizeof(Cmd), stdin))
if(std::fgets(Cmd, sizeof(Cmd), stdin)){
GEngine->Exec(Cmd, *GWarn);
Cmd[appStrlen(Cmd) - 1] = '\0'; //Removing newline added by fgets
CurrentCmd = Cmd; //Updating CurrentCmd so that it can be executed by the main thread
  //Nothing has been done in terms of thread safety as so far there haven't been any issues...
 
//Returning in case user requested exit to not get to fgets again
if(CurrentCmd.Caps() == "EXIT" || CurrentCmd.Caps() == "QUIT")
return 0;
}
}
}


Line 107: Line 116:
GEngine->Init();
GEngine->Init();


//Creating input thread
CreateThread(NULL, 0, UpdateServerConsoleInput, NULL, 0, NULL);
CreateThread(NULL, 0, UpdateServerConsoleInput, NULL, 0, NULL);


Line 116: Line 126:
while(GIsRunning && !GIsRequestingExit){
while(GIsRunning && !GIsRequestingExit){
double NewTime = appSeconds();
double NewTime = appSeconds();
//Executing console commands that are gathered by UpdateServerConsoleInput in a different thread
if(CurrentCmd.Num() > 0){
GEngine->Exec(*CurrentCmd, *GWarn);
CurrentCmd.Empty();
}


//Update the world
//Update the world
Line 147: Line 163:
__declspec(dllexport) void uccMain(const TCHAR* InPackage, const TCHAR* InCmdLine, FOutputDevice* InLog, FOutputDeviceError* /*InError*/,
__declspec(dllexport) void uccMain(const TCHAR* InPackage, const TCHAR* InCmdLine, FOutputDevice* InLog, FOutputDeviceError* /*InError*/,
  FFeedbackContext* InWarn, FConfigCache*(*ConfigFactory)(), UBOOL /*RequireConfig*/){
  FFeedbackContext* InWarn, FConfigCache*(*ConfigFactory)(), UBOOL /*RequireConfig*/){
FOutputDeviceWindowsError Error; //Error handling using message boxes which is just nicer
//Error handling using message boxes which is just nicer than having everything in the console
FOutputDeviceWindowsError Error;


FileLog = InLog;
FileLog = InLog;
Line 155: Line 172:
* This is necessary because creating a new FFeedbackContext causes problems with 'BeginSlowTask' which
* This is necessary because creating a new FFeedbackContext causes problems with 'BeginSlowTask' which
* leads to a crash in appLoadPackageFile for whatever reason...
* leads to a crash in appLoadPackageFile for whatever reason...
* A nice side-effect though, is that the features of FFeedbackContextWindows, like message boxes remain with the
* A nice side-effect though, is that the features of FFeedbackContextWindows, like message boxes, remain with the
* only change being the log output to the console
* only change being the log output to the console
*/
*/
{//===============================================================
{//===============================================================
PVOID* vtable = *reinterpret_cast<PVOID**>(InWarn);
void** vtable = *reinterpret_cast<void***>(InWarn);
DWORD dwNull;
DWORD dwNull;


VirtualProtect(&vtable[0], 4, PAGE_EXECUTE_READWRITE, &dwNull);
VirtualProtect(&vtable[0], 4, PAGE_READWRITE, &dwNull);


vtable[0] = FFeedbackContextAnsiSerialize;
vtable[0] = FFeedbackContextUCCSerialize;
}//===============================================================
}//===============================================================


Line 182: Line 199:


if(Token == "MAKE" || Token == "MAKECOMMANDLET" || Token == "EDITOR.MAKE" || Token == "EDITOR.MAKECOMMANDLET")
if(Token == "MAKE" || Token == "MAKECOMMANDLET" || Token == "EDITOR.MAKE" || Token == "EDITOR.MAKECOMMANDLET")
LoadFlags |= LOAD_DisallowFiles; //Not sure what this does but the original ucc has it as well...
LoadFlags |= LOAD_DisallowFiles; //Not entirely sure what this does but the original ucc has it as well...


UClass* Class = LoadClass<UCommandlet>(NULL, *Token, NULL, LoadFlags, NULL);
UClass* Class = LoadClass<UCommandlet>(NULL, *Token, NULL, LoadFlags, NULL);


if(!Class) //If class failed to load, appending "Commandlet" and trying again
if(!Class) //If class failed to load, appending "Commandlet" and trying again
Class = LoadClass<UCommandlet>(NULL, *(Token + "Commandlet"), NULL, LoadFlags, NULL);
Class = LoadClass<UCommandlet>(NULL, *(Token + "Commandlet"), NULL, LoadFlags, NULL);


if(!Class){
if(!Class){
TArray<FRegistryObjectInfo> List; //Loading list of commandlets declared in int files
TArray<FRegistryObjectInfo> List; //Loading list of commandlets declared in .int files
UObject::GetRegistryObjects(List, UClass::StaticClass(), UCommandlet::StaticClass(), 0);
UObject::GetRegistryObjects(List, UClass::StaticClass(), UCommandlet::StaticClass(), 0);


for(int i = 0; i < List.Num(); i++){ //Looking Token up in list and autocompleting class name if found
for(int i = 0; i < List.Num(); i++){ //Looking Token up in list and autocompleting class name if found
FString Str = List(i).Object.Caps();
FString Str = List[i].Object.Caps();


while(Str.InStr(".") >= 0)
while(Str.InStr(".") >= 0)
Line 200: Line 217:


if(Token == Str || Token + "COMMANDLET" == Str){
if(Token == Str || Token + "COMMANDLET" == Str){
Class = LoadClass<UCommandlet>(NULL, *List(i).Object, NULL, LoadFlags, NULL);
Class = LoadClass<UCommandlet>(NULL, *List[i].Object, NULL, LoadFlags, NULL);


break;
break;
Line 221: Line 238:
GLazyLoad = Default->LazyLoad;
GLazyLoad = Default->LazyLoad;


FString CommandletCmdLine; //Contains only the command-line options that are passed to the commandlet to avoid problems with some commandlets
//Contains only the command-line options that are passed to the commandlet to avoid problems with some commandlets
FString CommandletCmdLine;


for(int i = 2; i < __argc; i++)
for(int i = 2; i < __argc; i++)
Line 230: Line 248:


if(Default->LogToStdout)
if(Default->LogToStdout)
GLog = InWarn; //Redirecting commandlet output to console
GLog = InWarn; //Redirecting commandlet output to console
if(Token == "SERVER" || Token == "SERVERCOMMANDLET" || Token == "ENGINE.SERVER" || Token == "ENGINE.SERVERCOMMANDLET")
if(Token == "SERVER" || Token == "SERVERCOMMANDLET" || Token == "ENGINE.SERVER" || Token == "ENGINE.SERVERCOMMANDLET")
UServerCommandletMain(); //The ServerCommandlet has a special Main function
UServerCommandletMain(); //The ServerCommandlet has a special Main function
else if(Commandlet->FindFunction(NAME_Main))
else if(Commandlet->FindFunction(NAME_Main))
Commandlet->Main(CommandletCmdLine.GetIntSizeString()); //If UnrealScript main function exists this overload needs to be used
Commandlet->Main(GetIntSizeString(CommandletCmdLine)); //If UnrealScript main function is found this overload has to be used
else
else
Commandlet->Main(*CommandletCmdLine);
Commandlet->Main(*CommandletCmdLine);
Line 263: Line 281:


appExit();
appExit();
std::exit(EXIT_SUCCESS); //Needed in order to prevent this function from returning to SWRepublicCommando.exe
std::exit(EXIT_SUCCESS); //Needed in order to prevent this function from returning to SWRepublicCommando.exe
}
}
</source>
</source>

Revision as of 16:04, 28 April 2018

Author: Leon

Used Tools: MS Visual Studio 2003

Description: A self written UCC for Republic Commando. Used for executing Unreal Commandlets.

Github: here

Latest Build: here


UCC.cpp

/*
*	This code compiles to a dll. In order to use it, SWRepublicCommando.exe needs to be modified so
*	that it calls this dll's 'uccMain' instead of 'appInit' from Core.dll.
*	This is necessary because the call to appInit always fails due to problems with	FConfigCacheIni
*	that have not yet been possible to solve completely. By intercepting the call to appInit in the original
*	game executable it is possible to simply use the parameters that are passed by the game.
*	Everything compiles fine with Visual Studio .NET 2003 which is being used to achieve maximum compatibility
*	since it was also used to compile RC
*	The following settings are required in order to compile everything without errors:
*		- Character Set = Not Set	//Important as RC does not use unicode
*		- Struct Member Alignment = 4 Bytes	//Probably not necessary, but just in case...
*		- Calling Convention = __fastcall	//RC uses __fastcall as default calling convention
*/

#include <Windows.h>

#include <cstdlib> //__argc, __argv
#include <cstdio> //puts, fgets

#include "../../Engine/Inc/Engine.h" //Core and Engine
#include "../../Core/Inc/FOutputDeviceWindowsError.h" //For modal error messages that are not displayed in the console

namespace{
	//Variables used by FFeedbackContextUCCSerialize

	FOutputDevice* FileLog; //Used by FFeedbackContextAnsiSerialize to write to the log file because GLog might be reassigned
	int WarningCount = 0; //Incremented whenever a warning occurs in FFeedbackContextAnsiSerialize
	int ErrorCount = 0; //Same as WarningCount just with errors...

	//Variables for ServerCommandlet

	FString CurrentCmd; //Contains the next command that is to be passed to UEngine::Exec gathered by the input thread
						//the command is then executed by the main thread to avoid issues

	/*
	*	Replacement for Serialize function of 'InWarn', passed to appInit which is an FFeedbackContextWindows
	*/
	void __stdcall FFeedbackContextUCCSerialize(const TCHAR* V, EName Event){
		TCHAR Buffer[1024]= "";
		const TCHAR* Temp = V;

		if(Event == NAME_Title){
			SetConsoleTitle(V);

			return; //Prevents the server from spamming the player count to the log
		}else if(Event == NAME_Heading){
			appSprintf(Buffer, "\n--------------------%s--------------------", V);

			Temp = Buffer;
			V = Buffer; //So that the log file also contains the formatted string
		}else if(Event == NAME_Warning || Event == NAME_ExecWarning || Event == NAME_ScriptWarning){
			appSprintf(Buffer, "%s: %s", *FName(Event), V);

			WarningCount++;

			Temp = Buffer;
		}else if(Event == NAME_Error || Event == NAME_Critical){
			appSprintf(Buffer, "%s: %s", *FName(Event), V);

			ErrorCount++;

			Temp = Buffer;
		}

		std::puts(Temp);
		FileLog->Serialize(V, Event);
	}

	/*
	*	Allows user input in the console while running a server
	*	This function runs in a separate thread in order to not having
	*	to pause the main loop while waiting for input
	*/
	DWORD WINAPI UpdateServerConsoleInput(PVOID){
		char Cmd[1024];

		while(GIsRunning && !GIsRequestingExit){
			if(std::fgets(Cmd, sizeof(Cmd), stdin)){
				Cmd[appStrlen(Cmd) - 1] = '\0'; //Removing newline added by fgets
				CurrentCmd = Cmd; //Updating CurrentCmd so that it can be executed by the main thread
								  //Nothing has been done in terms of thread safety as so far there haven't been any issues...

				//Returning in case user requested exit to not get to fgets again
				if(CurrentCmd.Caps() == "EXIT" || CurrentCmd.Caps() == "QUIT")
					return 0;
			}
		}

		return 0;
	}

	/*
	*	Replacement for UServerCommandlet::Main since the one from Engine.dll crashes because it doesn't assign a value to GEngine
	*/
	void UServerCommandletMain(){
		UClass* EngineClass = LoadClass<UEngine>(NULL, "Engine.GameEngine", NULL, LOAD_NoFail, NULL);

		//Literally the only reason for this function to be rewritten
		//The original one doesn't assign a value to GEngine which leads to a gpf...
		GEngine = ConstructObject<UEngine>(EngineClass);

		GEngine->Init();

		//Creating input thread
		CreateThread(NULL, 0, UpdateServerConsoleInput, NULL, 0, NULL);

		double OldTime = appSeconds();

		GIsRunning = 1;

		//Main loop
		while(GIsRunning && !GIsRequestingExit){
			double NewTime = appSeconds();

			//Executing console commands that are gathered by UpdateServerConsoleInput in a different thread
			if(CurrentCmd.Num() > 0){
				GEngine->Exec(*CurrentCmd, *GWarn);
				CurrentCmd.Empty();
			}

			//Update the world
			GEngine->Tick(NewTime - OldTime);
			OldTime = NewTime;

			//Enforce optional maximum tick rate
			float MaxTickRate = GEngine->GetMaxTickRate();

			if(MaxTickRate > 0.0f){
				float Delta = (1.0f / MaxTickRate) - (appSeconds() - OldTime);

				appSleep(Delta > 0.0f ? Delta : 0.0f);
			}
		}

		GIsRunning = 0;
	}

	void ShowBanner(){
		GWarn->Log("=======================================");
		GWarn->Log("ucc.exe for Star Wars Republic Commando");
		GWarn->Log("made by Leon0628");
		GWarn->Log("=======================================\n");
	}
}

/*
*	Entry point, called by modified SWRepublicCommando.exe
*/
__declspec(dllexport) void uccMain(const TCHAR* InPackage, const TCHAR* InCmdLine, FOutputDevice* InLog, FOutputDeviceError* /*InError*/,
								   FFeedbackContext* InWarn, FConfigCache*(*ConfigFactory)(), UBOOL /*RequireConfig*/){
	//Error handling using message boxes which is just nicer than having everything in the console
	FOutputDeviceWindowsError Error;

	FileLog = InLog;

	/*
	*	Replacing Serialize function in vtable of InWarn with custom one that prints to the console
	*	This is necessary because creating a new FFeedbackContext causes problems with 'BeginSlowTask' which
	*	leads to a crash in appLoadPackageFile for whatever reason...
	*	A nice side-effect though, is that the features of FFeedbackContextWindows, like message boxes, remain with the
	*	only change being the log output to the console
	*/
	{//===============================================================
		void** vtable = *reinterpret_cast<void***>(InWarn);
		DWORD dwNull;

		VirtualProtect(&vtable[0], 4, PAGE_READWRITE, &dwNull);

		vtable[0] = FFeedbackContextUCCSerialize;
	}//===============================================================

	try{
		GIsStarted = 1;
		GIsGuarded = 1;

		appInit(InPackage, InCmdLine, InLog, &Error, InWarn, ConfigFactory, 1);
		UObject::SetLanguage("int");

		if(__argc > 1){
			//Initializing global state
			GIsUCC = GIsClient = GIsServer = GIsEditor = GIsScriptable = GLazyLoad = 1;

			FString Token = FString(__argv[1]).Caps();
			DWORD LoadFlags = LOAD_NoWarn | LOAD_Quiet;

			if(Token == "MAKE" || Token == "MAKECOMMANDLET" || Token == "EDITOR.MAKE" || Token == "EDITOR.MAKECOMMANDLET")
				LoadFlags |= LOAD_DisallowFiles; //Not entirely sure what this does but the original ucc has it as well...

			UClass* Class = LoadClass<UCommandlet>(NULL, *Token, NULL, LoadFlags, NULL);

			if(!Class) //If class failed to load, appending "Commandlet" and trying again
				Class = LoadClass<UCommandlet>(NULL, *(Token + "Commandlet"), NULL, LoadFlags, NULL);

			if(!Class){
				TArray<FRegistryObjectInfo> List; //Loading list of commandlets declared in .int files
				UObject::GetRegistryObjects(List, UClass::StaticClass(), UCommandlet::StaticClass(), 0);

				for(int i = 0; i < List.Num(); i++){ //Looking Token up in list and autocompleting class name if found
					FString Str = List[i].Object.Caps();

					while(Str.InStr(".") >= 0)
						Str = Str.Mid(Str.InStr(".") + 1);

					if(Token == Str || Token + "COMMANDLET" == Str){
						Class = LoadClass<UCommandlet>(NULL, *List[i].Object, NULL, LoadFlags, NULL);

						break;
					}
				}
			}

			if(Class){
				UCommandlet* Commandlet = ConstructObject<UCommandlet>(Class);
				UCommandlet* Default = static_cast<UCommandlet*>(Class->GetDefaultObject());

				if(Default->ShowBanner)
					ShowBanner();

				InWarn->Logf("Executing %s\n", Class->GetFullName());

				GIsClient = Default->IsClient;
				GIsEditor = Default->IsEditor;
				GIsServer = Default->IsServer;
				GLazyLoad = Default->LazyLoad;

				//Contains only the command-line options that are passed to the commandlet to avoid problems with some commandlets
				FString CommandletCmdLine;

				for(int i = 2; i < __argc; i++)
					CommandletCmdLine += FString(__argv[i]) + " ";

				Commandlet->InitExecution();
				Commandlet->ParseParms(*CommandletCmdLine);

				if(Default->LogToStdout)
					GLog = InWarn; //Redirecting commandlet output to console
				
				if(Token == "SERVER" || Token == "SERVERCOMMANDLET" || Token == "ENGINE.SERVER" || Token == "ENGINE.SERVERCOMMANDLET")
					UServerCommandletMain(); //The ServerCommandlet has a special Main function
				else if(Commandlet->FindFunction(NAME_Main))
					Commandlet->Main(GetIntSizeString(CommandletCmdLine)); //If UnrealScript main function is found this overload has to be used
				else
					Commandlet->Main(*CommandletCmdLine);

				if(Default->ShowErrorCount)
					InWarn->Logf("\n%s - %i error(s), %i warning(s)", ErrorCount == 0 ? "Success" : "Failure", ErrorCount, WarningCount);

				GLog = InLog;
			}else{
				ShowBanner();
				InWarn->Logf("Commandlet %s not found", __argv[1]);
			}
		}else{
			ShowBanner();
			InWarn->Log("Usage:");
			InWarn->Log("    ucc CommandletName <parameters>");
		}

		appPreExit();

		GIsGuarded = 0;
	}catch(...){
		GLog = InLog;
		GIsGuarded = 0;
		Error.HandleError();
	}

	appExit();
	std::exit(EXIT_SUCCESS); //Needed in order to prevent this function from returning to SWRepublicCommando.exe
}