Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions .github/workflows/godot-tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
name: Godot GSDK Tests

on:
pull_request:
paths:
- 'experimental/godot/**'
- '.github/workflows/godot-tests.yml'

permissions:
contents: read

jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Setup Godot
uses: chickensoft-games/setup-godot@v2
with:
version: 4.4.1
use-dotnet: false
include-templates: false

- name: Install GUT
working-directory: experimental/godot
run: |
GUT_VERSION="9.4.0"
curl -sL "https://github.com/bitwes/Gut/archive/refs/tags/v${GUT_VERSION}.tar.gz" -o gut.tar.gz
tar -xzf gut.tar.gz
cp -r "Gut-${GUT_VERSION}/addons/gut" addons/gut
rm -rf gut.tar.gz "Gut-${GUT_VERSION}"

- name: Import Godot project
working-directory: experimental/godot
run: godot --headless --import --quit || true

- name: Run tests
working-directory: experimental/godot
run: godot --headless -s addons/gut/gut_cmdln.gd -gdir=res://tests/ -ginclude_subdirs -gexit
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@

PlayFab Game Server SDK for C#, C++, and Java environments. The GSDK is used to integrate with PlayFab Multiplayer Servers and modify the game server lifecycle (check [here](https://docs.microsoft.com/en-us/gaming/playfab/features/multiplayer/servers/multiplayer-game-server-lifecycle) for more info).

## Experimental GSDKs

The [`experimental/`](experimental/) directory contains community-driven GSDK implementations for additional platforms (e.g., Go, Godot). These are **not officially supported** by PlayFab. They are provided on a best-effort basis — for help, please use [GitHub Issues](https://github.com/PlayFab/gsdk/issues) or the [Microsoft Game Dev Discord](https://aka.ms/msftgamedevdiscord).

## Prerequisites

[getting started guide](https://docs.microsoft.com/en-us/gaming/playfab/features/multiplayer/servers/integrating-game-servers-with-gsdk)
Expand Down
8 changes: 8 additions & 0 deletions experimental/godot/.gutconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"dirs": ["res://tests/"],
"double_strategy": "partial",
"include_subdirs": true,
"log_level": 1,
"prefix": "test_",
"suffix": ".gd"
}
125 changes: 125 additions & 0 deletions experimental/godot/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
# Godot GSDK

This is an implementation of the PlayFab Game Server SDK (GSDK) for the [Godot Engine](https://godotengine.org/) (4.x) using GDScript. It's considered experimental and is not yet ready for production use or supported. Expect bugs and breaking changes :)

## Requirements

- Godot Engine 4.x

## Installation

1. Copy the `addons/playfab_gsdk/` directory into your Godot project's `addons/` folder.
2. In the Godot Editor, go to **Project > Project Settings > Plugins** and enable the **PlayFab GSDK** plugin. This will automatically register the `PlayFabGSDK` autoload singleton.

Alternatively, you can manually add the autoload:
1. Go to **Project > Project Settings > Globals > AutoLoad**.
2. Add `addons/playfab_gsdk/gsdk.gd` with the name `PlayFabGSDK`.

## Files

| File | Description |
|------|-------------|
| `gsdk.gd` | Public API — autoload singleton with methods like `start()`, `ready_for_players()`, `register_health_callback()`, etc. |
| `internal_gsdk.gd` | Internal implementation — handles heartbeat loop, state transitions, configuration management, and callbacks. |
| `types.gd` | Type definitions — game state and operation enums, configuration key constants, and serialization helpers. |
| `gsdk_logger.gd` | Logging — writes log messages to both the Godot console and a log file. |
| `playfab_gsdk_plugin.gd` | Editor plugin — registers the `PlayFabGSDK` autoload when the plugin is enabled. |
| `plugin.cfg` | Plugin descriptor for the Godot editor. |

## Things to remember

- `ready_for_players()` is async. It blocks using `await` until the game server transitions to Active. Use `await PlayFabGSDK.ready_for_players()`.

## Configuration

The GSDK reads its configuration from a JSON file whose path is specified by the `GSDK_CONFIG_FILE` environment variable. This file is automatically provided by the PlayFab VM agent when running on PlayFab Multiplayer Servers.

Additionally, the following environment variables are read:
- `PF_TITLE_ID` — PlayFab Title ID
- `PF_BUILD_ID` — PlayFab Build ID
- `PF_REGION` — Region where the build is deployed

## Usage

Here is a sample of calling the GSDK from your game server's main script:

```gdscript
extends Node

func _ready() -> void:
# Register callbacks
PlayFabGSDK.register_health_callback(_on_health_check)
PlayFabGSDK.register_shutdown_callback(_on_shutdown)
PlayFabGSDK.register_maintenance_callback(_on_maintenance)

# Start the GSDK and wait for allocation
_start_gsdk()

func _start_gsdk() -> void:
PlayFabGSDK.log_message("Before ReadyForPlayers")
var is_active := await PlayFabGSDK.ready_for_players()
if is_active:
PlayFabGSDK.log_message("Server is now active and ready for players!")
else:
PlayFabGSDK.log_message("Server failed to transition to active state")

func _on_health_check() -> bool:
# Return true if the server is healthy, false otherwise
return true

func _on_shutdown() -> void:
PlayFabGSDK.log_message("Server is shutting down")
get_tree().quit()

func _on_maintenance(maintenance_time: String) -> void:
PlayFabGSDK.log_message("Maintenance scheduled at: %s" % maintenance_time)
```

### Updating Connected Players

```gdscript
# Tell PlayFab about connected players
PlayFabGSDK.update_connected_players([
{"PlayerId": "player-1"},
{"PlayerId": "player-2"},
])
```

## Running Tests

Unit tests use the [GUT (Godot Unit Testing)](https://github.com/bitwes/Gut) framework.

### Setup

1. Install GUT in your Godot project. The easiest way is via the [Godot Asset Library](https://godotengine.org/asset-library/asset/1709):
- In the Godot Editor: **AssetLib > Search "GUT" > Download and Install**
- Or manually: clone [bitwes/Gut](https://github.com/bitwes/Gut) into `addons/gut/`
2. Enable the GUT plugin: **Project > Project Settings > Plugins > Gut > Enable**

### Running from the Editor

1. Open the GUT panel: **Project > Tools > GUT**
2. Set the test directory to `res://tests/`
3. Click **Run All**

### Running from the Command Line

```bash
godot --headless -s addons/gut/gut_cmdln.gd -gdir=res://tests/ -gexit
```

### Getting Configuration

```gdscript
# Get all configuration settings
var config := PlayFabGSDK.get_config_settings()
print("Server ID: ", config.get("serverId", ""))
print("Region: ", config.get("region", ""))

# Get specific directories
var logs_dir := PlayFabGSDK.get_logs_directory()
var shared_dir := PlayFabGSDK.get_shared_content_directory()

# Get connection info
var conn_info := PlayFabGSDK.get_game_server_connection_info()
```
108 changes: 108 additions & 0 deletions experimental/godot/addons/playfab_gsdk/gsdk.gd
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
extends Node
## PlayFab Game Server SDK (GSDK) for Godot Engine.
##
## Add this script as an Autoload singleton named "PlayFabGSDK" in your Godot project,
## either manually or by enabling the PlayFab GSDK plugin.
## This provides the public API for integrating your Godot game server with
## PlayFab Multiplayer Servers.

var _internal: Node = null


func _ready() -> void:
var InternalGsdk := preload("res://addons/playfab_gsdk/internal_gsdk.gd")
_internal = InternalGsdk.new()
_internal.name = "InternalGSDK"
add_child(_internal)


## Starts communication with the PlayFab Multiplayer Servers agent.
## Kicks off the heartbeat loop. This is called automatically by other methods
## if not called explicitly.
func start() -> void:
_internal.start_internal()


## Starts communication with debug logging enabled.
func start_with_debug_logs() -> void:
_internal.debug_logs = true
_internal.start_internal()


## Transitions the game server state to StandingBy, telling the PlayFab service
## that the game server is ready to accept players.[br]
## [b]This is an async method[/b] — use: [code]await PlayFabGSDK.ready_for_players()[/code][br]
## Returns [code]true[/code] if the game server successfully transitioned to Active state.
func ready_for_players() -> bool:
_internal.start_internal()
if _internal.current_game_state != PlayFabGsdkTypes.GameState.ACTIVE:
_internal.current_game_state = PlayFabGsdkTypes.GameState.STANDING_BY
await _internal.transitioned_to_active
return _internal.current_game_state == PlayFabGsdkTypes.GameState.ACTIVE


## Logs a message to the GSDK log output.
func log_message(message: String) -> void:
_internal.start_internal()
_internal._logger.log_info(message)


## Returns connection information (IP address and ports) for the game server.
func get_game_server_connection_info() -> Dictionary:
_internal.start_internal()
return _internal.configuration.get("gameServerConnectionInfo", {})


## Registers a health check callback. The callback should return [code]true[/code]
## for healthy, [code]false[/code] for unhealthy. It is called on each heartbeat.
func register_health_callback(callback: Callable) -> void:
_internal.start_internal()
_internal.health_callback = callback


## Registers a shutdown callback. Called when the server is being terminated
## by the PlayFab agent.
func register_shutdown_callback(callback: Callable) -> void:
_internal.start_internal()
_internal.shutdown_callback = callback


## Registers a maintenance callback. Called when a scheduled maintenance event
## is approaching. The callback receives the maintenance datetime as a [String]
## in ISO 8601 / RFC 3339 format.
func register_maintenance_callback(callback: Callable) -> void:
_internal.start_internal()
_internal.maintenance_callback = callback


## Returns the directory path for log files that will be uploaded to PlayFab.
func get_logs_directory() -> String:
_internal.start_internal()
return _internal.config_map.get(PlayFabGsdkTypes.LOG_FOLDER_KEY, "")


## Returns the shared content directory path shared among all game servers on the VM.
func get_shared_content_directory() -> String:
_internal.start_internal()
return _internal.config_map.get(PlayFabGsdkTypes.SHARED_CONTENT_FOLDER_KEY, "")


## Returns the list of initial players that have access to this game server.
## Only available after game server allocation.
func get_initial_players() -> PackedStringArray:
_internal.start_internal()
return _internal.initial_players


## Updates the list of connected players. Each element should be a [Dictionary]
## with a [code]"PlayerId"[/code] key.[br]
## Example: [code][{"PlayerId": "player1"}, {"PlayerId": "player2"}][/code]
func update_connected_players(players: Array) -> void:
_internal.start_internal()
_internal.connected_players = players


## Returns all configuration settings as a [Dictionary].
func get_config_settings() -> Dictionary:
_internal.start_internal()
return _internal.config_map
53 changes: 53 additions & 0 deletions experimental/godot/addons/playfab_gsdk/gsdk_logger.gd
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
## Logger for the PlayFab GSDK.
##
## Writes log messages to both the Godot console (stdout) and a log file
## in the configured log directory.

var _log_file: FileAccess = null


## Initializes the logger by opening a log file in the specified directory.
func initialize(directory: String) -> void:
if not DirAccess.dir_exists_absolute(directory):
DirAccess.make_dir_recursive_absolute(directory)

var timestamp := int(Time.get_unix_time_from_system())
var pid := OS.get_process_id()
var filename := "%s/GSDK_output_%d_%d.txt" % [directory, timestamp, pid]
_log_file = FileAccess.open(filename, FileAccess.WRITE)
if _log_file == null:
push_error("GSDK: Failed to open log file: %s" % filename)


## Logs an informational message.
func log_info(message: String) -> void:
var formatted := "[INFO] %s" % message
print(formatted)
_write_to_file(formatted)


## Logs a warning message.
func log_warn(message: String) -> void:
var formatted := "[WARN] %s" % message
push_warning(formatted)
_write_to_file(formatted)


## Logs an error message.
func log_error(message: String) -> void:
var formatted := "[ERROR] %s" % message
push_error(formatted)
_write_to_file(formatted)


## Logs a debug message.
func log_debug(message: String) -> void:
var formatted := "[DEBUG] %s" % message
print(formatted)
_write_to_file(formatted)


func _write_to_file(message: String) -> void:
if _log_file != null:
_log_file.store_line(message)
_log_file.flush()
Loading
Loading