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 34: Line 34:
#include <Windows.h> //VirtualProtect, SetConsoleCtrlHandler
#include <Windows.h> //VirtualProtect, SetConsoleCtrlHandler


#include <cstdio> //std::puts
#include <iostream>
#include <cstdlib> //__argc, __argv
#include <cstdlib> //__argc, __argv
#include <cctype> //std::toupper
#include <cctype> //std::toupper
Line 137: Line 137:
}
}


std::puts(Temp);
std::cout << Temp << '\n';


FileLog->Serialize(V, Event);
FileLog->Serialize(V, Event);
}
/*
* Allows user input in the console when running a server
* This function runs in a separate thread, not sure if this is a good solution though...
*/
DWORD WINAPI UpdateServerConsoleInput(PVOID){
std::string cmd;
while(GIsRunning){
std::getline(std::cin,cmd);
GEngine->Exec(cmd.c_str(), *GWarn);
if(ToUpper(cmd) == "EXIT" || ToUpper(cmd) == "QUIT")
return 0;
}
return 0;
}
}


Line 146: Line 165:
* and thus prevents the log file from being empty
* and thus prevents the log file from being empty
*/
*/
BOOL CALLBACK HandlerRoutine(DWORD dwCtrlType){
BOOL WINAPI HandlerRoutine(DWORD dwCtrlType){
if(dwCtrlType == CTRL_CLOSE_EVENT){
if(dwCtrlType == CTRL_CLOSE_EVENT){
FileLog->Flush();
FileLog->Flush();
Line 169: Line 188:
GIsRunning = 1;
GIsRunning = 1;
DWORD ThreadID;
CreateThread(NULL, 0, UpdateServerConsoleInput, NULL, 0, &ThreadID);


double OldTime = appSeconds();
double OldTime = appSeconds();

Revision as of 22:22, 27 November 2017

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 using the UT99 headers the call to appInit always fails due to the parameters'
*	types being substantially different in RC (especially FConfigCacheIni). By replacing the appInit call in
*	SWRepublicCommando.exe it is possible to use the parameters that are originally passed and because they're
*	just pointers it's no problem to use them to call appInit from Core.dll.
*	Also it's subsystem should be changed from window to console
*	Everything compiles fine with Visual Studio .NET 2003 which is being used to achieve maximum compatibility
*	since it was also used to compile Republic Commando
*	The following settings are required in order to compile everything without errors:
*		- Character Set = Not Set
*		- Struct Member Alignment = 4 Bytes
*		- Calling Convention = __fastcall
*/

#pragma comment(lib, "Core.lib")
#pragma comment(lib, "Engine.lib")

#include <Windows.h>	//VirtualProtect, SetConsoleCtrlHandler

#include <iostream>
#include <cstdlib>	//__argc, __argv
#include <cctype>	//std::toupper
#include <string>

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

namespace{
	//Global variables

	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...

	//All commandlets that shipped with RC, used for autocompleting user input
	const char* DefaultCommandlets[40][2] = {
		{"Editor", "AnalyzeContent"},
		{"Editor", "BatchExport"},
		{"Editor", "ChecksumPackage"},
		{"Editor", "CheckUnicode"},
		{"Editor", "CompareInt"},
		{"IpDrv", "Compress"},
		{"Editor", "CompressToDXT"},
		{"Editor", "Conform"},
		{"Editor", "ConvertMaterial"},
		{"Editor", "CutdownContent"},
		{"Editor", "DataRip"},
		{"IpDrv", "Decompress"},
		{"XGame", "DumpGameList"},
		{"Editor", "DumpInt"},
		{"XGame", "DumpMapList"},
		{"XGame", "DumpMutatorList"},
		{"Editor", "DumpSoundParams"},
		{"Editor", "DumpSoundPropLog"},
		{"XGame", "DumpWeaponList"},
		{"Editor", "DXTConvert"},
		{"Editor", "Exec"},
		{"Editor", "FontUpdate"},
		{"Core", "HelloWorld"},
		{"Editor", "ImportAse"},
		{"Editor", "ImportTexture"},
		{"Editor", "ListPackageContents"},
		{"Editor", "Make"},
		{"Editor", "MapConvert"},
		{"Editor", "Master"},
		{"Editor", "MergeInt"},
		{"Editor", "ModifyPackageFlags"},
		{"Editor", "PackageFlag"},
		{"Editor", "Pkg"},
		{"Editor", "RearrangeInt"},
		{"Editor", "ResavePackage"},
		{"Engine", "Server"},
		{"Editor", "StripSource"},
		{"Editor", "TestMath"},
		{"Editor", "UpdateUMod"},
		{"Editor", "XACTExport"}
	};

	/*
	*	Helper function used for case-insensitive string comparisons
	*/
	std::string ToUpper(const std::string& s){
		std::string result;

		result.reserve(s.size());

		for(std::size_t i = 0; i < s.size(); i++)
			result += std::toupper(s[i]);

		return result;
	}

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

		if(Event==NAME_Title){
			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::cout << Temp << '\n';

		FileLog->Serialize(V, Event);
	}

	/*
	*	Allows user input in the console when running a server
	*	This function runs in a separate thread, not sure if this is a good solution though...
	*/
	DWORD WINAPI UpdateServerConsoleInput(PVOID){
		std::string cmd;

		while(GIsRunning){
			std::getline(std::cin,cmd);

			GEngine->Exec(cmd.c_str(), *GWarn);

			if(ToUpper(cmd) == "EXIT" || ToUpper(cmd) == "QUIT")
				return 0;
		}

		return 0;
	}

	/*
	*	Console handler that flushes the log file in case console is closed while running a server
	*	and thus prevents the log file from being empty
	*/
	BOOL WINAPI HandlerRoutine(DWORD dwCtrlType){
		if(dwCtrlType == CTRL_CLOSE_EVENT){
			FileLog->Flush();

			return TRUE;
		}

		return FALSE;
	}

	/*
	*	Replacement for UServerCommandlet::Main since the one from Engine.dll crashes because it doesn't assign a value to GEngine
	*/
	void UServerCommandletMain(){
		SetConsoleCtrlHandler(HandlerRoutine, TRUE);	//Setting handler routine that prevents the log from being empty

		UClass* EngineClass = UObject::StaticLoadClass(UEngine::StaticClass(), NULL, "Engine.GameEngine", NULL, LOAD_NoFail, NULL);

		GEngine = ConstructObject<UEngine>(EngineClass);

		GEngine->Init();
		
		GIsRunning = 1;

		DWORD ThreadID;
		CreateThread(NULL, 0, UpdateServerConsoleInput, NULL, 0, &ThreadID);

		double OldTime = appSeconds();

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

			//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;
	}

	/*
	*	Returns a specific property of a commandlet, valid properties are: IsClient, IsEditor, IsServer, LazyLoad, ShowErrorCount
	*	This function launches a Commandlet itself that gets the property in UnrealScript and returns it. This is necessary because
	*	the UT99 headers are (obviously) not fully compatible with RC and thus getting default proerties via UClass::GetDefaultObject
	*	doesn't work. This however, works fine. The only downside is that there needs to exist a package called UCC.u that contains this custom commandlet...
	*/
	int GetCommandletProperty(std::string Cmdlet, std::string Property){
		//Static because there's no reason to reload the class and create the object each time this function is called
		static UClass* Class = UObject::StaticLoadClass(UCommandlet::StaticClass(), NULL, "UCC.GetCommandletPropertiesCommandlet", NULL, LOAD_NoFail, NULL);
		static UCommandlet* Commandlet = ConstructObject<UCommandlet>(Class);

		Commandlet->InitExecution();
		Commandlet->ParseParms(("CommandletClass=" + Cmdlet).c_str());

		return Commandlet->Main(FString(Property.c_str()));
	}
}

/*
*	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*/){
	FOutputDeviceWindowsError Error;	//Error handling using message boxes for a better overview
	std::string CommandletCmdLine;	//Contains only the command-line options for the commandlet that is being executed to avoid problems with some commandlets

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

	FileLog = InLog;

	//vtable hack to have InWarn call a different Serialize function that prints to the console
	//Have to do this because creating new FFeedbackContext and passing it to appInit results in a crash
	//probaby due to different vtable layouts which so far has not been possible to fix.
	//But since this works fine, there really is no reason to investigate the issue any further...
	{//===============================================================
		PVOID* vtable = *reinterpret_cast<PVOID**>(InWarn);
		DWORD dwNull;

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

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

	InWarn->Log("=======================================");
	InWarn->Log("ucc.exe for Star Wars Republic Commando");
	InWarn->Log("made by Leon0628");
	InWarn->Log("=======================================\n");

	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;

			std::string Token = ToUpper(__argv[1]);
			std::string ClassName = Token;
			DWORD LoadFlags = LOAD_NoWarn | LOAD_Quiet;

			//Looking it up in list of default commandlets and if found constructing proper class name
			for(int i = 0; i < ARRAY_COUNT(DefaultCommandlets); i++){
				if(Token == ToUpper(DefaultCommandlets[i][1]) ||	//Name
				   Token == ToUpper(DefaultCommandlets[i][1]) + "COMMANDLET"){	//Name + "Commandlet"
					
					ClassName = std::string(DefaultCommandlets[i][0]) + "." + DefaultCommandlets[i][1] + "Commandlet";

					break;
				}
			}

			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...

			UClass* Class = UObject::StaticLoadClass(UCommandlet::StaticClass(), NULL, ClassName.c_str(), NULL, LoadFlags, NULL);

			if(!Class)	//If class failed to load, appending "Commandlet" and trying again
				Class = UObject::StaticLoadClass(UCommandlet::StaticClass(), NULL, (ClassName + "Commandlet").c_str(), NULL, LoadFlags, NULL);

			if(Class){
				UCommandlet* Commandlet = ConstructObject<UCommandlet>(Class);

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

				GIsClient = GetCommandletProperty(ClassName, "IsClient");
				GIsEditor = GetCommandletProperty(ClassName, "IsEditor");
				GIsServer = GetCommandletProperty(ClassName, "IsServer");
				GLazyLoad = GetCommandletProperty(ClassName, "LazyLoad");

				Commandlet->InitExecution();
				Commandlet->ParseParms(CommandletCmdLine.c_str());

				if(GetCommandletProperty(ClassName, "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))	//If no UnrealScript Main function is found, the commandlet is written in native code
					Commandlet->Main(CommandletCmdLine.c_str());
				else
					Commandlet->Main(FString(CommandletCmdLine.c_str()));	//For non-native commandlets this overload has to be used

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

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

		appPreExit();	//For some reason this crashes (sometimes?) when there are compiler errors with ucc make

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

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