Epic Online Services (EOS) offer different storage options: Title Storage and Player Data Storage. If backend calls are properly authorized (e.g. using an AWS API Gateway), EOS storage can easily be managed by a backend service.

Scope of this post

This article presents an example program which can be used to access EOS Title Storage from a Go backend service.

Backend Authorization

Usually, all storage-related EOS calls require login. However, running an “accountportal” login on a backend server does not work, because it would simply try to start a web browser on the server. There are other options available to use EOS from a backend service:

  • Manually perform a web-based login (by http redirection) and use the token for web-based games
  • Use login-tokens sent by clients (be careful about the security implications)
  • Perform EOS calls without login by using the EOS Client Policy Type “trusted server” (make sure backend calls are restricted or authorized in some way)

For this example, we are choosing the third option.

Preparation: EOS Client Policy

In order to run the example program, we need to create a client policy with type “trusted server” for the product on the Epic Games Developer Portal. Make sure that the option “user required” is unchecked.

Screenshot Client Policy Type Trusted Server

Preparation: cgo

EOS does not offer a native Go API. We are using the C EOS SDK with cgo. cgo may require some setup, depending on your OS. The main requirement is that a gcc executable needs to be located in the system PATH. The easiest way to set this up is to use a Linux based Go development environment with gcc installed, either as native environment or in a development docker image.

Product Setup

As usual, when accessing EOS we need product/sandbox/deployment ids and client credentials. Assuming that the configuration is static for the service, we are using C constants to represent these values. Feel free to replace the encryption key with 64 hex digits of your choice.

Note that this code looks like a comment section, but when using cgo, all C code is within a Go comment section directly above the import “C” statement. In order to avoid duplicate symbols when linking, use different .go files for each of the following code sections.

package main

/*
const char *PRODUCT_NAME = "your product name";
const char *PRODUCT_VERSION = "1.0";
const char *PRODUCT_ID = "your product id";
const char *SANDBOX_ID = "your sandbox id";
const char *DEPLOYMENT_ID = "your deployment id";
const char *CLIENT_CREDENTIALS_ID = "your client credentials id";
const char *CLIENT_CREDENTIALS_SECRET = "your client credentials secret";
const char *ENCRYPTION_KEY = "0000000000000000000000000000000000000000000000000000000000000000";
*/
import "C"

Implementing Callbacks

The main problem when using EOS from a Go service is that it is not possible to pass a Go function as callback for a C function call. See for example this blog post explaining the issue.

We are solving this by providing C wrapper functions as callbacks for EOS, and calling the Go functions from these C wrapper functions.

package main

/*
#include <eos_logging.h>
#include <eos_titlestorage_types.h>

extern void eosLoggingCallback(const EOS_LogMessage *InMsg);
void EOS_CALL EOSSDKLoggingCallback(const EOS_LogMessage *InMsg) {
	eosLoggingCallback(InMsg);
}

extern EOS_TitleStorage_EReadResult eosReadFileDataCallback(const EOS_TitleStorage_ReadFileDataCallbackInfo *Data);
EOS_TitleStorage_EReadResult EOS_CALL EOSSDKReadFileDataCallback(const EOS_TitleStorage_ReadFileDataCallbackInfo *Data) {
	return eosReadFileDataCallback(Data);
}

extern void eosReadFileCompleteCallback(const EOS_TitleStorage_ReadFileCallbackInfo *Info);
void EOS_CALL EOSSDKReadFileCompleteCallback(const EOS_TitleStorage_ReadFileCallbackInfo *Info) {
	eosReadFileCompleteCallback(Info);
}
*/
import "C"

Using Title Storage

All file uploads to EOS Title Storage can only be done using the Epic Games Developer Portal. Before accessing the storage from a backend service, we need to manually upload files which are required by the backend. This can be done in the section “Game Services” - “Title Storage”. We need to make sure to use the same encryption key when uploading as the one configured in the setup section above.

Screenshot Title Storage Upload

Additional notes on the implementation:

  • The example code downloads a file named title_storage_test.txt. Feel free to change this filename accordingly.
  • The downloaded data can be accessed in the eosReadFileDataCallback function.
  • When calling EOS API and using EOS data types, we need to include EOS header files. In order to do this, we are adding #cgo flags, assuming that the SDK is located in the folder ”../EOS-SDK” and we are targeting Linux.
  • The Go callbacks are exposed to C by using the magic //export <function name> comment

Other than that, accessing title storage is pretty straightforward.

package main

/*
package main

/*
#cgo CFLAGS: -I../EOS-SDK/SDK/Include
#cgo LDFLAGS: -L../EOS-SDK/SDK/Bin -lEOSSDK-Linux-Shipping

#include <stdlib.h>
#include <eos_init.h>
#include <eos_sdk.h>
#include <eos_logging.h>
#include <eos_titlestorage.h>

extern const char *PRODUCT_NAME;
extern const char *PRODUCT_VERSION;
extern const char *PRODUCT_ID;
extern const char *SANDBOX_ID;
extern const char *DEPLOYMENT_ID;
extern const char *CLIENT_CREDENTIALS_ID;
extern const char *CLIENT_CREDENTIALS_SECRET;
extern const char *ENCRYPTION_KEY;

extern void EOS_CALL EOSSDKLoggingCallback(const EOS_LogMessage *InMsg);
extern EOS_TitleStorage_EReadResult EOS_CALL EOSSDKReadFileDataCallback(const EOS_TitleStorage_ReadFileDataCallbackInfo *Data);
extern void EOS_CALL EOSSDKReadFileCompleteCallback(const EOS_TitleStorage_ReadFileCallbackInfo *Info);
*/
import "C"
import (
	"log"
	"os"
	"unsafe"
)

//export eosLoggingCallback
func eosLoggingCallback(msg *C.EOS_LogMessage) {
	var logLevel string
	switch msg.Level {
	case C.EOS_LOG_Fatal:
		logLevel = "Fatal"
	case C.EOS_LOG_Error:
		logLevel = "Error"
	case C.EOS_LOG_Warning:
		logLevel = "Warning"
	default:
		logLevel = "Message"
	}
	log.Printf("%s: %s - %s", logLevel, C.GoString(msg.Category), C.GoString(msg.Message))
}

//export eosReadFileDataCallback
func eosReadFileDataCallback(data *C.EOS_TitleStorage_ReadFileDataCallbackInfo) C.EOS_TitleStorage_EReadResult {
	log.Printf("Data")
	return C.EOS_TS_RR_ContinueReading
}

//export eosReadFileCompleteCallback
func eosReadFileCompleteCallback(info *C.EOS_TitleStorage_ReadFileCallbackInfo) {
	log.Printf("Info")
}

func main() {
	sdkOptions := C.EOS_InitializeOptions{
		ApiVersion:     C.EOS_INITIALIZE_API_LATEST,
		ProductName:    C.PRODUCT_NAME,
		ProductVersion: C.PRODUCT_VERSION,
	}
	initResult := C.EOS_Initialize(&sdkOptions)
	if initResult != C.EOS_Success {
		log.Fatalf("EOS_Initialize error %d", initResult)
	}
	defer C.EOS_Shutdown()
	C.EOS_Logging_SetLogLevel(C.EOS_LC_ALL_CATEGORIES, C.EOS_LOG_VeryVerbose)
	logCallbackResult := C.EOS_Logging_SetCallback((C.EOS_LogMessageFunc)(unsafe.Pointer(C.EOSSDKLoggingCallback)))
	if logCallbackResult != C.EOS_Success {
		log.Fatalf("EOS_Logging_SetCallback error %d", logCallbackResult)
	}
	clientCredentials := C.EOS_Platform_ClientCredentials{
		ClientId:     C.CLIENT_CREDENTIALS_ID,
		ClientSecret: C.CLIENT_CREDENTIALS_SECRET,
	}
	osTempDir := C.CString(os.TempDir())
	defer C.free(unsafe.Pointer(osTempDir))

	platformOptions := C.EOS_Platform_Options{
		ApiVersion:        C.EOS_PLATFORM_OPTIONS_API_LATEST,
		bIsServer:         1,
		ProductId:         C.PRODUCT_ID,
		SandboxId:         C.SANDBOX_ID,
		DeploymentId:      C.DEPLOYMENT_ID,
		ClientCredentials: clientCredentials,
		EncryptionKey:     C.ENCRYPTION_KEY,
		CacheDirectory:    osTempDir,
	}
	platformHandle := C.EOS_Platform_Create(&platformOptions)
	if platformHandle == nil {
		log.Fatalf("EOS_Platform_Create failed")
	}
	defer C.EOS_Platform_Release(platformHandle)
	C.EOS_Platform_Tick(platformHandle)

	titleStorageHandle := C.EOS_Platform_GetTitleStorageInterface(platformHandle)
	if titleStorageHandle == nil {
		log.Fatalf("EOS_Platform_GetTitleStorageInterface failed")
	}

	fileName := C.CString("title_storage_test.txt")
	defer C.free(unsafe.Pointer(fileName))
	readFileOptions := C.EOS_TitleStorage_ReadFileOptions{
		ApiVersion:           C.EOS_TITLESTORAGE_READFILEOPTIONS_API_LATEST,
		Filename:             fileName,
		ReadChunkLengthBytes: 8192,
		ReadFileDataCallback: ((C.EOS_TitleStorage_OnReadFileDataCallback)(unsafe.Pointer((C.EOSSDKReadFileDataCallback)))),
	}
	readFileHandle := C.EOS_TitleStorage_ReadFile(titleStorageHandle, &readFileOptions, unsafe.Pointer(&clientData),
		(C.EOS_TitleStorage_OnReadFileCompleteCallback)(unsafe.Pointer(C.EOSSDKReadFileCompleteCallback)))
	if readFileHandle == nil {
		log.Fatalf("Error invalid handle")
	}

	readFileResult := C.EOS_TitleStorageFileTransferRequest_GetFileRequestState(readFileHandle)
	for readFileResult == C.EOS_RequestInProgress {
		C.EOS_Platform_Tick(platformHandle)
		readFileResult = C.EOS_TitleStorageFileTransferRequest_GetFileRequestState(readFileHandle)
	}
	C.EOS_Platform_Tick(platformHandle)
}

Epic and Epic Games are trademarks or registered trademarks of Epic Games, Inc. in the United States of America and elsewhere.