-
Notifications
You must be signed in to change notification settings - Fork 6
feat(gameye): configurable environments, DI config, multi-port, and region routing #24
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 1 commit
4a35325
4a05258
8704568
2bcd3c1
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -18,28 +18,72 @@ namespace GameyeAllocatorModule; | |
|
|
||
| /// <summary> | ||
| /// Module configuration for dependency injection. | ||
| /// Registers IGameApiClient as a singleton for accessing Unity services like Secret Manager. | ||
| /// Edit the <see cref="GameyeAllocatorConfig"/> instance below to configure the allocator | ||
| /// for your project — image name, environment, region, ports, and version. | ||
| /// </summary> | ||
| public class ModuleConfig : ICloudCodeSetup | ||
| { | ||
| public void Setup(ICloudCodeConfig config) | ||
| { | ||
| config.Dependencies.AddSingleton(GameApiClient.Create()); | ||
| config.Dependencies.AddScoped<IGameyeHttpClientFactory, GameyeHttpClientFactory>(); | ||
|
|
||
| // ────────────────────────────────────────────────────────────── | ||
| // Gameye allocator configuration — edit the values below. | ||
| // ────────────────────────────────────────────────────────────── | ||
| config.Dependencies.AddSingleton(new GameyeAllocatorConfig | ||
| { | ||
| // Required — the application image name registered in the Gameye Admin Panel. | ||
| ImageName = "test_nginx", | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Does this name map to onboarding steps, in other allocators we use "your-image-name" to help make it clear users need to change this.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good point, |
||
|
|
||
| // The API environment. Use Sandbox for development, Production for live. | ||
| Environment = GameyeEnvironment.Sandbox, | ||
|
|
||
| // Default deployment region — used when no pool-to-location mapping matches. | ||
| DefaultLocation = "eu-west", | ||
|
|
||
| // Option A — Unity QoS automatic region selection (recommended). | ||
| // Maps the value Unity puts in MatchProperties["Region"] to a Gameye location. | ||
| // LocationByRegion = new Dictionary<string, string> | ||
| // { | ||
| // { "eu-west", "eu-west" }, | ||
| // { "us-central", "us-central" }, | ||
| // { "asia-northeast", "asia-northeast" }, | ||
| // }, | ||
|
|
||
| // Option B — pool-name mapping (use when Unity QoS is not configured). | ||
| // LocationByPool = new Dictionary<string, string> | ||
| // { | ||
| // { "eu-west-pool", "eu-west" }, | ||
| // { "us-central-pool", "us-central" }, | ||
| // { "ap-ne-pool", "asia-northeast" }, | ||
| // }, | ||
|
|
||
| // Primary game server port (must match your Dockerfile EXPOSE / Admin Panel config). | ||
| GamePort = 80, | ||
|
|
||
| // Optional — pin a specific Docker image tag / version. | ||
| // When null, Gameye uses the highest-priority tag configured in the Admin Panel. | ||
| // Version = "v1.2.3", | ||
|
|
||
| // Optional — additional ports to expose to game clients (e.g. voice, query, RCON). | ||
| // These are returned in AllocationData as "port_{name}" alongside the primary port. | ||
| // AdditionalPorts = new Dictionary<string, int> | ||
| // { | ||
| // { "query", 27015 }, | ||
| // { "rcon", 27020 }, | ||
| // }, | ||
| }); | ||
| } | ||
| } | ||
|
|
||
| public class GameyeAllocator(IGameApiClient gameApiClient, IGameyeHttpClientFactory httpClientFactory, ILogger<GameyeAllocator> logger) : IMatchmakerAllocator | ||
| public class GameyeAllocator( | ||
| IGameApiClient gameApiClient, | ||
| IGameyeHttpClientFactory httpClientFactory, | ||
| GameyeAllocatorConfig allocatorConfig, | ||
| ILogger<GameyeAllocator> logger) : IMatchmakerAllocator | ||
| { | ||
| // Configuration - users should modify these constants for their setup | ||
| private const string ImageName = "MyGame"; // TODO: Replace with your Gameye application image name | ||
| private const string DefaultLocation = "europe"; // TODO: Replace with your preferred region | ||
| private const int GamePort = 7777; // TODO: Replace with your game server port | ||
|
|
||
| // Gameye Constants | ||
| private const string GameyeApiUrl = "https://api.gameye.io"; | ||
|
|
||
| // Secret names - these must match the secrets stored in Unity Dashboard | ||
| // Secret names — these must match the secrets stored in Unity Dashboard | ||
| private const string GameyeApiTokenSecretName = "GAMEYE_API_TOKEN"; | ||
|
|
||
| [CloudCodeFunction("Matchmaker_AllocateServer")] | ||
|
|
@@ -50,11 +94,14 @@ public async Task<AllocateResponse> Allocate(IExecutionContext context, Allocate | |
| Secret gameyeApiToken = await gameApiClient.SecretManager.GetSecret(context, GameyeApiTokenSecretName); | ||
| using HttpClient client = httpClientFactory.Create(gameyeApiToken.Value); | ||
|
|
||
| var resolvedLocation = ResolveLocation(request.MatchmakingResults); | ||
|
|
||
| var sessionRequest = new SessionRequest | ||
| { | ||
| Id = request.MatchId, | ||
| Location = DefaultLocation, | ||
| Image = ImageName, | ||
| Location = resolvedLocation, | ||
| Image = allocatorConfig.ImageName, | ||
| Version = allocatorConfig.Version, | ||
| Env = new Dictionary<string, string> | ||
| { | ||
| { "MATCH_ID", request.MatchId }, | ||
|
|
@@ -67,7 +114,7 @@ public async Task<AllocateResponse> Allocate(IExecutionContext context, Allocate | |
| }; | ||
|
|
||
| var content = new StringContent(JsonConvert.SerializeObject(sessionRequest), Encoding.UTF8, "application/json"); | ||
| HttpResponseMessage response = await client.PostAsync($"{GameyeApiUrl}/session", content); | ||
| HttpResponseMessage response = await client.PostAsync($"{allocatorConfig.ApiBaseUrl}/session", content); | ||
|
|
||
| string responseContent = await response.Content.ReadAsStringAsync(); | ||
| if (!response.IsSuccessStatusCode) | ||
|
|
@@ -81,14 +128,27 @@ public async Task<AllocateResponse> Allocate(IExecutionContext context, Allocate | |
|
|
||
| var sessionResponse = JsonConvert.DeserializeObject<SessionResponse>(responseContent); | ||
|
|
||
| return new AllocateResponse(AllocateStatus.Created) | ||
| var allocationData = new Dictionary<string, object> | ||
| { | ||
| AllocationData = new Dictionary<string, object> | ||
| { "sessionId", sessionResponse?.Id ?? request.MatchId }, | ||
| { "host", sessionResponse?.Host ?? string.Empty }, | ||
| { "port", FindPort(sessionResponse?.Ports, allocatorConfig.GamePort) }, | ||
| { "location", resolvedLocation }, | ||
| }; | ||
|
|
||
| // Include additional named ports so game clients can access them. | ||
| foreach (var (name, containerPort) in allocatorConfig.AdditionalPorts) | ||
| { | ||
| int hostPort = FindPort(sessionResponse?.Ports, containerPort); | ||
| if (hostPort > 0) | ||
| { | ||
| { "sessionId", sessionResponse?.Id ?? request.MatchId }, | ||
| { "host", sessionResponse?.Host ?? string.Empty }, | ||
| { "port", FindGamePort(sessionResponse?.Ports) }, | ||
| }, | ||
| allocationData[$"port_{name}"] = hostPort; | ||
| } | ||
| } | ||
|
|
||
| return new AllocateResponse(AllocateStatus.Created) | ||
| { | ||
| AllocationData = allocationData, | ||
| }; | ||
| } | ||
| catch (Exception e) | ||
|
|
@@ -128,7 +188,7 @@ public async Task<PollResponse> Poll(IExecutionContext context, PollRequest requ | |
| { | ||
| Secret gameyeApiToken = await gameApiClient.SecretManager.GetSecret(context, GameyeApiTokenSecretName); | ||
| using HttpClient client = httpClientFactory.Create(gameyeApiToken.Value); | ||
| HttpResponseMessage response = await client.GetAsync($"{GameyeApiUrl}/session/{sessionId}"); | ||
| HttpResponseMessage response = await client.GetAsync($"{allocatorConfig.ApiBaseUrl}/session/{sessionId}"); | ||
| string responseContent = await response.Content.ReadAsStringAsync(); | ||
|
|
||
| if (!response.IsSuccessStatusCode) | ||
|
|
@@ -155,7 +215,7 @@ public async Task<PollResponse> Poll(IExecutionContext context, PollRequest requ | |
| { | ||
| AssignmentData = AssignmentData.IpPort( | ||
| sessionResponse.Host ?? string.Empty, | ||
| FindGamePort(sessionResponse.Ports) | ||
| FindPort(sessionResponse.Ports, allocatorConfig.GamePort) | ||
| ), | ||
| }, | ||
| "created" or "restarting" => new PollResponse(PollStatus.Pending), | ||
|
|
@@ -177,15 +237,55 @@ public async Task<PollResponse> Poll(IExecutionContext context, PollRequest requ | |
| } | ||
|
|
||
| /// <summary> | ||
| /// Finds the host port mapped to the configured game port from the session's port mappings. | ||
| /// Falls back to the first available port if the configured port is not found. | ||
| /// Resolves the Gameye location for this match using a three-tier priority: | ||
| /// 1. <c>MatchProperties["Region"]</c> → <see cref="GameyeAllocatorConfig.LocationByRegion"/> | ||
| /// (Unity QoS has already picked the best region — use it directly) | ||
| /// 2. <c>PoolName</c> → <see cref="GameyeAllocatorConfig.LocationByPool"/> | ||
| /// (fallback for studios using per-region pools without QoS) | ||
| /// 3. <see cref="GameyeAllocatorConfig.DefaultLocation"/> | ||
| /// </summary> | ||
| private string ResolveLocation(MatchmakingResults results) | ||
| { | ||
| // Priority 1 — Unity QoS resolved region | ||
| if (results.MatchProperties.TryGetValue("Region", out var regionObj)) | ||
| { | ||
| var region = regionObj?.ToString(); | ||
| if (!string.IsNullOrEmpty(region) && | ||
| allocatorConfig.LocationByRegion.TryGetValue(region, out var regionLocation)) | ||
| { | ||
| logger.LogInformation("Region resolved via QoS: MatchProperties[Region]={Region} → {Location}", region, regionLocation); | ||
| return regionLocation; | ||
| } | ||
|
|
||
| if (!string.IsNullOrEmpty(region)) | ||
| { | ||
| logger.LogWarning("MatchProperties[Region]={Region} has no entry in LocationByRegion — falling through", region); | ||
| } | ||
| } | ||
|
|
||
| // Priority 2 — pool name mapping | ||
| var pool = results.PoolName; | ||
| if (!string.IsNullOrEmpty(pool) && | ||
| allocatorConfig.LocationByPool.TryGetValue(pool, out var poolLocation)) | ||
| { | ||
| logger.LogInformation("Region resolved via pool: PoolName={Pool} → {Location}", pool, poolLocation); | ||
| return poolLocation; | ||
| } | ||
|
|
||
| // Priority 3 — static default | ||
| return allocatorConfig.DefaultLocation; | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Finds the host port mapped to the given container port from the session's port mappings. | ||
| /// Falls back to the first available port if the target port is not found. | ||
| /// </summary> | ||
| private static int FindGamePort(List<PortMapping>? ports) | ||
| private static int FindPort(List<PortMapping>? ports, int containerPort) | ||
| { | ||
| if (ports == null || ports.Count == 0) | ||
| return 0; | ||
|
|
||
| var match = ports.FirstOrDefault(p => p.Container == GamePort); | ||
| var match = ports.FirstOrDefault(p => p.Container == containerPort); | ||
| return match?.Host ?? ports[0].Host; | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Using this pool-to-region mapping is a decision made by the game dev to not use QoS. The original statement can be understood as something Unity Matchmaker strictly does, which is not true.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good catch — you're right that pool-based routing is a developer decision (an alternative to Unity QoS), not something Matchmaker strictly does. Reworded to "can be configured to use pools for region routing" in 4a05258. Thanks!