Lesson 35 - Get Compute Auth Token Working
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
VC_redist.x64.exe /q
|
||||
Engine\Extras\Redist\en-us\UEPrereqSetup_x64.exe /q
|
||||
@@ -0,0 +1,527 @@
|
||||
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
AWSTemplateFormatVersion: "2010-09-09"
|
||||
|
||||
Description: >
|
||||
This CloudFormation template sets up a game backend service with a single Amazon GameLift fleet. After player
|
||||
authenticates and start a game via POST /start_game, a lambda handler searches for an existing viable game session
|
||||
with open player slot on the fleet, and if not found, creates a new game session. The game client is then expected
|
||||
to poll POST /get_game_connection to receive a viable game session.
|
||||
|
||||
Parameters:
|
||||
ApiGatewayStageNameParameter:
|
||||
Type: String
|
||||
Default: v1
|
||||
Description: Name of the Api Gateway stage
|
||||
|
||||
BuildNameParameter:
|
||||
Type: String
|
||||
Default: Sample GameLift Build
|
||||
Description: Name of the build
|
||||
|
||||
BuildOperatingSystemParameter:
|
||||
Type: String
|
||||
Default: WINDOWS_2016
|
||||
Description: Operating system of the build
|
||||
|
||||
BuildServerSdkVersionParameter:
|
||||
Type: String
|
||||
Description: GameLift Server SDK version used in the server build
|
||||
|
||||
BuildS3BucketParameter:
|
||||
Type: String
|
||||
Description: Bucket that stores the server build
|
||||
|
||||
BuildS3KeyParameter:
|
||||
Type: String
|
||||
Description: Key of the server build in the S3 bucket
|
||||
|
||||
BuildVersionParameter:
|
||||
Type: String
|
||||
Description: Version number of the build
|
||||
|
||||
FleetDescriptionParameter:
|
||||
Type: String
|
||||
Default: Deployed by the Amazon GameLift Plug-in for Unreal.
|
||||
Description: Description of the fleet
|
||||
|
||||
FleetNameParameter:
|
||||
Type: String
|
||||
Default: Sample GameLift Fleet
|
||||
Description: Name of the fleet
|
||||
|
||||
FleetTcpFromPortParameter:
|
||||
Type: Number
|
||||
Default: 33430
|
||||
Description: Starting port number for TCP ports to be opened
|
||||
|
||||
FleetTcpToPortParameter:
|
||||
Type: Number
|
||||
Default: 33440
|
||||
Description: Ending port number for TCP ports to be opened
|
||||
|
||||
FleetUdpFromPortParameter:
|
||||
Type: Number
|
||||
Default: 33430
|
||||
Description: Starting port number for UDP ports to be opened
|
||||
|
||||
FleetUdpToPortParameter:
|
||||
Type: Number
|
||||
Default: 33440
|
||||
Description: Ending port number for UDP ports to be opened
|
||||
|
||||
GameNameParameter:
|
||||
Type: String
|
||||
Default: MyGame
|
||||
Description: Game name to prepend before resource names
|
||||
MaxLength: 30
|
||||
|
||||
LambdaZipS3BucketParameter:
|
||||
Type: String
|
||||
Description: S3 bucket that stores the lambda function zip
|
||||
|
||||
LambdaZipS3KeyParameter:
|
||||
Type: String
|
||||
Description: S3 key that stores the lambda function zip
|
||||
|
||||
LaunchParametersParameter:
|
||||
Type: String
|
||||
Description: Parameters used to launch the game server process
|
||||
|
||||
LaunchPathParameter:
|
||||
Type: String
|
||||
Description: Location of the game server executable in the build
|
||||
|
||||
MaxPlayersPerGameParameter:
|
||||
Type: Number
|
||||
Default: 10
|
||||
Description: Maximum number of players per game session
|
||||
|
||||
MaxTransactionsPerFiveMinutesPerIpParameter:
|
||||
Type: Number
|
||||
Default: 100
|
||||
MaxValue: 20000000
|
||||
MinValue: 100
|
||||
|
||||
UnrealEngineVersionParameter:
|
||||
Type: String
|
||||
Description: "Unreal Engine Version being used by the plugin"
|
||||
|
||||
EnableMetricsParameter:
|
||||
Type: String
|
||||
Default: "false"
|
||||
AllowedValues: ["true", "false"]
|
||||
Description: "Enable telemetry metrics collection using OTEL collector"
|
||||
|
||||
Conditions:
|
||||
ShouldCreateMetricsResources: !Equals [!Ref EnableMetricsParameter, "true"]
|
||||
|
||||
Resources:
|
||||
ApiGatewayCloudWatchRole:
|
||||
Type: "AWS::IAM::Role"
|
||||
Properties:
|
||||
AssumeRolePolicyDocument:
|
||||
Version: "2012-10-17"
|
||||
Statement:
|
||||
- Effect: Allow
|
||||
Principal:
|
||||
Service:
|
||||
- apigateway.amazonaws.com
|
||||
Action: "sts:AssumeRole"
|
||||
ManagedPolicyArns:
|
||||
- "arn:aws:iam::aws:policy/service-role/AmazonAPIGatewayPushToCloudWatchLogs"
|
||||
|
||||
Account:
|
||||
Type: "AWS::ApiGateway::Account"
|
||||
Properties:
|
||||
CloudWatchRoleArn: !GetAtt ApiGatewayCloudWatchRole.Arn
|
||||
|
||||
GameRequestLambdaFunctionExecutionRole:
|
||||
Type: "AWS::IAM::Role"
|
||||
Properties:
|
||||
AssumeRolePolicyDocument:
|
||||
Version: "2012-10-17"
|
||||
Statement:
|
||||
- Effect: Allow
|
||||
Principal:
|
||||
Service:
|
||||
- lambda.amazonaws.com
|
||||
Action:
|
||||
- "sts:AssumeRole"
|
||||
ManagedPolicyArns:
|
||||
- "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
|
||||
Policies:
|
||||
- PolicyName: !Sub ${GameNameParameter}GameRequestLambdaFunctionGameLiftPolicies
|
||||
PolicyDocument:
|
||||
Version: "2012-10-17"
|
||||
Statement:
|
||||
- Effect: Allow
|
||||
Action:
|
||||
- "gamelift:CreateGameSession"
|
||||
- "gamelift:CreatePlayerSession"
|
||||
- "gamelift:SearchGameSessions"
|
||||
Resource: "*"
|
||||
|
||||
RestApi:
|
||||
Type: "AWS::ApiGateway::RestApi"
|
||||
Properties:
|
||||
Name: !Sub ${GameNameParameter}RestApi
|
||||
|
||||
ResultsRequestLambdaFunctionExecutionRole:
|
||||
Type: "AWS::IAM::Role"
|
||||
Properties:
|
||||
AssumeRolePolicyDocument:
|
||||
Version: "2012-10-17"
|
||||
Statement:
|
||||
- Effect: Allow
|
||||
Principal:
|
||||
Service:
|
||||
- lambda.amazonaws.com
|
||||
Action:
|
||||
- "sts:AssumeRole"
|
||||
ManagedPolicyArns:
|
||||
- "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
|
||||
Policies:
|
||||
- PolicyName: !Sub ${GameNameParameter}ResultsRequestLambdaFunctionGameLiftPolicies
|
||||
PolicyDocument:
|
||||
Version: "2012-10-17"
|
||||
Statement:
|
||||
- Effect: Allow
|
||||
Action:
|
||||
- "gamelift:CreateGameSession"
|
||||
- "gamelift:CreatePlayerSession"
|
||||
- "gamelift:SearchGameSessions"
|
||||
Resource: "*"
|
||||
|
||||
MetricsInstanceRole:
|
||||
Type: "AWS::IAM::Role"
|
||||
Condition: ShouldCreateMetricsResources
|
||||
Properties:
|
||||
AssumeRolePolicyDocument:
|
||||
Version: '2012-10-17'
|
||||
Statement:
|
||||
- Effect: Allow
|
||||
Principal:
|
||||
Service: gamelift.amazonaws.com
|
||||
Action: sts:AssumeRole
|
||||
Policies:
|
||||
- PolicyName: AMPRemoteWriteAccess
|
||||
PolicyDocument:
|
||||
Version: '2012-10-17'
|
||||
Statement:
|
||||
- Effect: Allow
|
||||
Action:
|
||||
- aps:RemoteWrite
|
||||
Resource: !Sub 'arn:aws:aps:*:${AWS::AccountId}:workspace/*'
|
||||
- PolicyName: OTelCollectorEMFExporter
|
||||
PolicyDocument:
|
||||
Version: '2012-10-17'
|
||||
Statement:
|
||||
- Effect: Allow
|
||||
Action:
|
||||
- logs:PutLogEvents
|
||||
- logs:CreateLogStream
|
||||
- logs:CreateLogGroup
|
||||
- logs:PutRetentionPolicy
|
||||
Resource: !Sub 'arn:aws:logs:*:${AWS::AccountId}:log-group:*:log-stream:*'
|
||||
|
||||
UserPool:
|
||||
Type: "AWS::Cognito::UserPool"
|
||||
Properties:
|
||||
AdminCreateUserConfig:
|
||||
AllowAdminCreateUserOnly: false
|
||||
AutoVerifiedAttributes:
|
||||
- email
|
||||
EmailConfiguration:
|
||||
EmailSendingAccount: COGNITO_DEFAULT
|
||||
EmailVerificationMessage: "Please verify your email to complete account registration for the GameLift Plugin Single-fleet deployment scenario. Confirmation Code {####}."
|
||||
EmailVerificationSubject: GameLift Plugin - Deployment Scenario Account Verification
|
||||
Policies:
|
||||
PasswordPolicy:
|
||||
MinimumLength: 8
|
||||
RequireLowercase: true
|
||||
RequireNumbers: true
|
||||
RequireSymbols: true
|
||||
RequireUppercase: true
|
||||
Schema:
|
||||
- Name: email
|
||||
AttributeDataType: String
|
||||
Mutable: false
|
||||
Required: true
|
||||
UserPoolName: !Sub ${GameNameParameter}UserPool
|
||||
UsernameAttributes:
|
||||
- email
|
||||
|
||||
BuildAccessRole:
|
||||
Type: "AWS::IAM::Role"
|
||||
Properties:
|
||||
AssumeRolePolicyDocument:
|
||||
Version: "2012-10-17"
|
||||
Statement:
|
||||
- Effect: Allow
|
||||
Principal:
|
||||
Service:
|
||||
- cloudformation.amazonaws.com
|
||||
- gamelift.amazonaws.com
|
||||
Action: "sts:AssumeRole"
|
||||
Policies:
|
||||
- PolicyName: !Sub ${GameNameParameter}BuildS3AccessPolicy
|
||||
PolicyDocument:
|
||||
Version: "2012-10-17"
|
||||
Statement:
|
||||
- Effect: Allow
|
||||
Action:
|
||||
- "s3:GetObject"
|
||||
- "s3:GetObjectVersion"
|
||||
Resource:
|
||||
- "Fn::Sub": "arn:aws:s3:::${BuildS3BucketParameter}/${BuildS3KeyParameter}"
|
||||
RoleName: !Sub ${GameNameParameter}BuildIAMRole
|
||||
|
||||
GameRequestApiResource:
|
||||
Type: "AWS::ApiGateway::Resource"
|
||||
Properties:
|
||||
ParentId: !GetAtt RestApi.RootResourceId
|
||||
PathPart: start_game
|
||||
RestApiId: !Ref RestApi
|
||||
|
||||
ResultsRequestApiResource:
|
||||
Type: "AWS::ApiGateway::Resource"
|
||||
Properties:
|
||||
ParentId: !GetAtt RestApi.RootResourceId
|
||||
PathPart: get_game_connection
|
||||
RestApiId: !Ref RestApi
|
||||
|
||||
UserPoolClient:
|
||||
Type: "AWS::Cognito::UserPoolClient"
|
||||
Properties:
|
||||
AccessTokenValidity: 1
|
||||
ClientName: !Sub ${GameNameParameter}UserPoolClient
|
||||
ExplicitAuthFlows:
|
||||
- ALLOW_USER_PASSWORD_AUTH
|
||||
- ALLOW_REFRESH_TOKEN_AUTH
|
||||
GenerateSecret: false
|
||||
IdTokenValidity: 1
|
||||
PreventUserExistenceErrors: ENABLED
|
||||
ReadAttributes:
|
||||
- email
|
||||
- preferred_username
|
||||
RefreshTokenValidity: 30
|
||||
SupportedIdentityProviders:
|
||||
- COGNITO
|
||||
UserPoolId: !Ref UserPool
|
||||
|
||||
WebACL:
|
||||
Type: "AWS::WAFv2::WebACL"
|
||||
DependsOn:
|
||||
- ApiDeployment
|
||||
Properties:
|
||||
DefaultAction:
|
||||
Allow:
|
||||
{}
|
||||
Description: !Sub "WebACL for game: ${GameNameParameter}"
|
||||
Name: !Sub ${GameNameParameter}WebACL
|
||||
Rules:
|
||||
- Name: !Sub ${GameNameParameter}WebACLPerIpThrottleRule
|
||||
Action:
|
||||
Block:
|
||||
{}
|
||||
Priority: 0
|
||||
Statement:
|
||||
RateBasedStatement:
|
||||
AggregateKeyType: IP
|
||||
Limit: !Ref MaxTransactionsPerFiveMinutesPerIpParameter
|
||||
VisibilityConfig:
|
||||
CloudWatchMetricsEnabled: true
|
||||
MetricName: !Sub ${GameNameParameter}WebACLPerIpThrottleRuleMetrics
|
||||
SampledRequestsEnabled: true
|
||||
Scope: REGIONAL
|
||||
VisibilityConfig:
|
||||
CloudWatchMetricsEnabled: true
|
||||
MetricName: !Sub ${GameNameParameter}WebACLMetrics
|
||||
SampledRequestsEnabled: true
|
||||
|
||||
ApiDeployment:
|
||||
Type: "AWS::ApiGateway::Deployment"
|
||||
DependsOn:
|
||||
- GameRequestApiMethod
|
||||
- ResultsRequestApiMethod
|
||||
Properties:
|
||||
RestApiId: !Ref RestApi
|
||||
StageDescription:
|
||||
DataTraceEnabled: true
|
||||
LoggingLevel: INFO
|
||||
MetricsEnabled: true
|
||||
StageName: !Ref ApiGatewayStageNameParameter
|
||||
|
||||
Authorizer:
|
||||
Type: "AWS::ApiGateway::Authorizer"
|
||||
Properties:
|
||||
IdentitySource: method.request.header.Auth
|
||||
Name: CognitoAuthorizer
|
||||
ProviderARNs:
|
||||
- "Fn::GetAtt":
|
||||
- UserPool
|
||||
- Arn
|
||||
RestApiId: !Ref RestApi
|
||||
Type: COGNITO_USER_POOLS
|
||||
|
||||
GameRequestApiMethod:
|
||||
Type: "AWS::ApiGateway::Method"
|
||||
Properties:
|
||||
AuthorizationType: COGNITO_USER_POOLS
|
||||
AuthorizerId: !Ref Authorizer
|
||||
HttpMethod: POST
|
||||
Integration:
|
||||
Type: AWS_PROXY
|
||||
IntegrationHttpMethod: POST
|
||||
Uri: !Sub "arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${GameRequestLambdaFunction.Arn}/invocations"
|
||||
OperationName: GameRequest
|
||||
ResourceId: !Ref GameRequestApiResource
|
||||
RestApiId: !Ref RestApi
|
||||
|
||||
ResultsRequestApiMethod:
|
||||
Type: "AWS::ApiGateway::Method"
|
||||
Properties:
|
||||
AuthorizationType: COGNITO_USER_POOLS
|
||||
AuthorizerId: !Ref Authorizer
|
||||
HttpMethod: POST
|
||||
Integration:
|
||||
Type: AWS_PROXY
|
||||
IntegrationHttpMethod: POST
|
||||
Uri: !Sub "arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${ResultsRequestLambdaFunction.Arn}/invocations"
|
||||
OperationName: ResultsRequest
|
||||
ResourceId: !Ref ResultsRequestApiResource
|
||||
RestApiId: !Ref RestApi
|
||||
|
||||
WebACLAssociation:
|
||||
Type: "AWS::WAFv2::WebACLAssociation"
|
||||
DependsOn:
|
||||
- ApiDeployment
|
||||
- WebACL
|
||||
Properties:
|
||||
ResourceArn: !Sub
|
||||
- "arn:aws:apigateway:${REGION}::/restapis/${REST_API_ID}/stages/${STAGE_NAME}"
|
||||
- REGION: !Ref "AWS::Region"
|
||||
REST_API_ID: !Ref RestApi
|
||||
STAGE_NAME: !Ref ApiGatewayStageNameParameter
|
||||
WebACLArn: !GetAtt WebACL.Arn
|
||||
|
||||
ServerBuild:
|
||||
Type: "AWS::GameLift::Build"
|
||||
Properties:
|
||||
Name: !Ref BuildNameParameter
|
||||
OperatingSystem: !Ref BuildOperatingSystemParameter
|
||||
ServerSdkVersion: !Ref BuildServerSdkVersionParameter
|
||||
StorageLocation:
|
||||
Bucket: !Ref BuildS3BucketParameter
|
||||
Key: !Ref BuildS3KeyParameter
|
||||
RoleArn: !GetAtt BuildAccessRole.Arn
|
||||
Version: !Ref BuildVersionParameter
|
||||
|
||||
FleetResource:
|
||||
Type: "AWS::GameLift::Fleet"
|
||||
Properties:
|
||||
BuildId: !Ref ServerBuild
|
||||
CertificateConfiguration:
|
||||
CertificateType: GENERATED
|
||||
Description: !Sub
|
||||
- "${FleetDescriptionParameter} Using Unreal Engine Version ${UnrealEngineVersionParameter}${MetricsText}"
|
||||
- MetricsText: !If [ShouldCreateMetricsResources, " with telemetry metrics enabled", ""]
|
||||
DesiredEC2Instances: 1
|
||||
EC2InboundPermissions:
|
||||
- FromPort: !Ref FleetTcpFromPortParameter
|
||||
IpRange: "0.0.0.0/0"
|
||||
Protocol: TCP
|
||||
ToPort: !Ref FleetTcpToPortParameter
|
||||
- FromPort: !Ref FleetUdpFromPortParameter
|
||||
IpRange: "0.0.0.0/0"
|
||||
Protocol: UDP
|
||||
ToPort: !Ref FleetUdpToPortParameter
|
||||
EC2InstanceType: c5.large
|
||||
FleetType: ON_DEMAND
|
||||
InstanceRoleARN: !If [ShouldCreateMetricsResources, !GetAtt MetricsInstanceRole.Arn, !Ref "AWS::NoValue"]
|
||||
InstanceRoleCredentialsProvider: !If [ShouldCreateMetricsResources, "SHARED_CREDENTIAL_FILE", !Ref "AWS::NoValue"]
|
||||
Name: !Ref FleetNameParameter
|
||||
NewGameSessionProtectionPolicy: FullProtection
|
||||
ResourceCreationLimitPolicy:
|
||||
NewGameSessionsPerCreator: 5
|
||||
PolicyPeriodInMinutes: 2
|
||||
RuntimeConfiguration:
|
||||
GameSessionActivationTimeoutSeconds: 300
|
||||
MaxConcurrentGameSessionActivations: 1
|
||||
ServerProcesses:
|
||||
- ConcurrentExecutions: 1
|
||||
LaunchPath: !Ref LaunchPathParameter
|
||||
Parameters: !Ref LaunchParametersParameter
|
||||
|
||||
AliasResource:
|
||||
Type: "AWS::GameLift::Alias"
|
||||
Properties:
|
||||
Description: !Sub Alias to access ${GameNameParameter} fleet
|
||||
Name: !Sub ${GameNameParameter}FleetAlias
|
||||
RoutingStrategy:
|
||||
Type: SIMPLE
|
||||
FleetId: !Ref FleetResource
|
||||
|
||||
ResultsRequestLambdaFunction:
|
||||
Type: "AWS::Lambda::Function"
|
||||
Properties:
|
||||
Code:
|
||||
S3Bucket: !Ref LambdaZipS3BucketParameter
|
||||
S3Key: !Ref LambdaZipS3KeyParameter
|
||||
Description: Lambda function to handle game requests
|
||||
Environment:
|
||||
Variables:
|
||||
FleetAlias: !Ref AliasResource
|
||||
FunctionName: !Sub ${GameNameParameter}ResultsRequestLambda
|
||||
Handler: results_request.handler
|
||||
MemorySize: 128
|
||||
Role: !GetAtt ResultsRequestLambdaFunctionExecutionRole.Arn
|
||||
Runtime: python3.14
|
||||
|
||||
GameRequestLambdaFunction:
|
||||
Type: "AWS::Lambda::Function"
|
||||
Properties:
|
||||
Code:
|
||||
S3Bucket: !Ref LambdaZipS3BucketParameter
|
||||
S3Key: !Ref LambdaZipS3KeyParameter
|
||||
Description: Lambda function to handle game requests
|
||||
Environment:
|
||||
Variables:
|
||||
FleetAlias: !Ref AliasResource
|
||||
MaxPlayersPerGame: !Ref MaxPlayersPerGameParameter
|
||||
FunctionName: !Sub ${GameNameParameter}GameRequestLambda
|
||||
Handler: game_request.handler
|
||||
MemorySize: 128
|
||||
Role: !GetAtt GameRequestLambdaFunctionExecutionRole.Arn
|
||||
Runtime: python3.14
|
||||
|
||||
ResultsRequestLambdaFunctionApiGatewayPermission:
|
||||
Type: "AWS::Lambda::Permission"
|
||||
Properties:
|
||||
Action: "lambda:InvokeFunction"
|
||||
FunctionName: !GetAtt ResultsRequestLambdaFunction.Arn
|
||||
Principal: apigateway.amazonaws.com
|
||||
SourceArn: !Sub "arn:aws:execute-api:${AWS::Region}:${AWS::AccountId}:${RestApi}/*/*/*"
|
||||
|
||||
GameRequestLambdaFunctionApiGatewayPermission:
|
||||
Type: "AWS::Lambda::Permission"
|
||||
Properties:
|
||||
Action: "lambda:InvokeFunction"
|
||||
FunctionName: !GetAtt GameRequestLambdaFunction.Arn
|
||||
Principal: apigateway.amazonaws.com
|
||||
SourceArn: !Sub "arn:aws:execute-api:${AWS::Region}:${AWS::AccountId}:${RestApi}/*/*/*"
|
||||
|
||||
Outputs:
|
||||
ApiGatewayEndpoint:
|
||||
Description: Url of ApiGateway Endpoint
|
||||
Value: !Sub "https://${RestApi}.execute-api.${AWS::Region}.amazonaws.com/${ApiGatewayStageNameParameter}/"
|
||||
|
||||
UserPoolClientId:
|
||||
Description: Id of UserPoolClient
|
||||
Value: !Ref UserPoolClient
|
||||
|
||||
IdentityRegion:
|
||||
Description: Region name
|
||||
Value: !Ref "AWS::Region"
|
||||
@@ -0,0 +1,44 @@
|
||||
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
---
|
||||
# THIS IS A SAMPLE CLOUDFORMATION PARAMETERS FILE
|
||||
GameNameParameter:
|
||||
value: "{{AWSGAMELIFT::SYS::GAMENAME}}"
|
||||
LambdaZipS3BucketParameter:
|
||||
value: "{{AWSGAMELIFT::VARS::LambdaZipS3BucketParameter}}"
|
||||
LambdaZipS3KeyParameter:
|
||||
value: "{{AWSGAMELIFT::VARS::LambdaZipS3KeyParameter}}"
|
||||
ApiGatewayStageNameParameter:
|
||||
value: "{{AWSGAMELIFT::VARS::ApiGatewayStageNameParameter}}"
|
||||
BuildS3BucketParameter:
|
||||
value: "{{AWSGAMELIFT::VARS::BuildS3BucketParameter}}"
|
||||
BuildS3KeyParameter:
|
||||
value: "{{AWSGAMELIFT::VARS::BuildS3KeyParameter}}"
|
||||
LaunchPathParameter:
|
||||
value: "{{AWSGAMELIFT::VARS::LaunchPathParameter}}"
|
||||
LaunchParametersParameter:
|
||||
value: "-port=7777 -UNATTENDED LOG=server.log"
|
||||
BuildNameParameter:
|
||||
value: "Sample GameLift Build for {{AWSGAMELIFT::SYS::GAMENAME}}"
|
||||
BuildVersionParameter:
|
||||
value: "1"
|
||||
BuildOperatingSystemParameter:
|
||||
value: "{{AWSGAMELIFT::VARS::BuildOperatingSystemParameter}}"
|
||||
BuildServerSdkVersionParameter:
|
||||
value: "5.4.0"
|
||||
FleetTcpFromPortParameter:
|
||||
value: "7770"
|
||||
FleetTcpToPortParameter:
|
||||
value: "7780"
|
||||
FleetUdpFromPortParameter:
|
||||
value: "7770"
|
||||
FleetUdpToPortParameter:
|
||||
value: "7780"
|
||||
FleetNameParameter:
|
||||
value: "Single-Region Fleet"
|
||||
UnrealEngineVersionParameter:
|
||||
value: "{{AWSGAMELIFT::VARS::UnrealEngineVersionParameter}}"
|
||||
EnableMetricsParameter:
|
||||
value: "{{AWSGAMELIFT::VARS::EnableMetricsParameter}}"
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
---
|
||||
# Key/value pairs to be added to the game's awsGameLiftClientConfig.yml file
|
||||
# These values will be replaced at the end of create/update of
|
||||
# the feature's CloudFormation stack.
|
||||
|
||||
user_pool_client_id: "{{AWSGAMELIFT::CFNOUTPUT::UserPoolClientId}}"
|
||||
identity_api_gateway_base_url: "{{AWSGAMELIFT::CFNOUTPUT::ApiGatewayEndpoint}}"
|
||||
identity_region: "{{AWSGAMELIFT::CFNOUTPUT::IdentityRegion}}"
|
||||
@@ -0,0 +1,79 @@
|
||||
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import boto3
|
||||
import os
|
||||
import json
|
||||
|
||||
|
||||
def handler(event, context):
|
||||
"""
|
||||
Handles requests to start games from the game client.
|
||||
This function first looks for any game session
|
||||
:param event: lambda event, contains the region to player latency mapping in `regionToLatencyMapping` key, as well
|
||||
as the player information from the Cognito id tokens.
|
||||
:param context: lambda context, not used by this function
|
||||
:return:
|
||||
- 202 (Accepted) if the matchmaking request is accepted and is now being processed
|
||||
- 409 (Conflict) if the another matchmaking request is in progress
|
||||
- 500 (Internal Error) if error occurred when processing the matchmaking request
|
||||
"""
|
||||
|
||||
gamelift = boto3.client('gamelift')
|
||||
fleet_alias = os.environ['FleetAlias']
|
||||
max_players_per_game = int(os.environ['MaxPlayersPerGame'])
|
||||
|
||||
player_id = event["requestContext"]["authorizer"]["claims"]["sub"]
|
||||
print(f'Handling start game request. PlayerId: {player_id}')
|
||||
|
||||
# NOTE: latency mapping is not used in this deployment scenario
|
||||
region_to_latency_mapping = get_region_to_latency_mapping(event)
|
||||
if region_to_latency_mapping:
|
||||
print(f"Region to latency mapping: {region_to_latency_mapping}")
|
||||
else:
|
||||
print("No regionToLatencyMapping mapping provided")
|
||||
|
||||
if not has_viable_game_sessions(gamelift, fleet_alias):
|
||||
create_game_session(gamelift, fleet_alias, max_players_per_game)
|
||||
|
||||
return {
|
||||
'headers': {
|
||||
'Content-Type': 'text/plain'
|
||||
},
|
||||
'statusCode': 202
|
||||
}
|
||||
|
||||
|
||||
def has_viable_game_sessions(gamelift, fleet_alias):
|
||||
print(f"Checking for viable game sessions: {fleet_alias}")
|
||||
|
||||
# NOTE: SortExpression="creationTimeMillis ASC" is not needed because we are looking for any viable game sessions,
|
||||
# hence the order does not matter.
|
||||
search_game_sessions_response = gamelift.search_game_sessions(
|
||||
AliasId=fleet_alias,
|
||||
FilterExpression="hasAvailablePlayerSessions=true",
|
||||
)
|
||||
return len(search_game_sessions_response['GameSessions']) != 0
|
||||
|
||||
|
||||
def create_game_session(gamelift, fleet_alias, max_players_per_game):
|
||||
print(f"Creating game session: {fleet_alias}")
|
||||
gamelift.create_game_session(
|
||||
AliasId=fleet_alias,
|
||||
MaximumPlayerSessionCount=max_players_per_game,
|
||||
)
|
||||
|
||||
|
||||
def get_region_to_latency_mapping(event):
|
||||
request_body = event.get("body")
|
||||
if not request_body:
|
||||
return None
|
||||
|
||||
try:
|
||||
request_body_json = json.loads(request_body)
|
||||
except ValueError:
|
||||
print(f"Error parsing request body: {request_body}")
|
||||
return None
|
||||
|
||||
if request_body_json and request_body_json.get('regionToLatencyMapping'):
|
||||
return request_body_json.get('regionToLatencyMapping')
|
||||
@@ -0,0 +1,73 @@
|
||||
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import boto3
|
||||
import os
|
||||
import json
|
||||
|
||||
|
||||
def handler(event, context):
|
||||
"""
|
||||
Handles requests to describe the game session connection information after a StartGame request.
|
||||
This function will look up the MatchmakingRequest table to find a pending matchmaking request by
|
||||
the player, and if it is QUEUED, look up the GameSessionPlacement table to find the game's
|
||||
connection information.
|
||||
:param event: lambda event, contains the region to player latency mapping in `regionToLatencyMapping` key, as well
|
||||
as the player information from the Cognito id tokens. Cognito provides a `sub` user attribute that we use as a
|
||||
Player ID. Unlike `username` value `sub` is UUID for a user which is never reassigned to another user.
|
||||
:param context: lambda context, not used by this function
|
||||
:return:
|
||||
- 200 (OK) if the game connection is ready, along with server info: "IpAddress", "Port", "DnsName", "PlayerSessionId", "PlayerId"
|
||||
- 204 (No Content) if the requested game is still in progress of matchmaking
|
||||
- 404 (Not Found) if no game has been started by the player, or if all started game were expired
|
||||
- 500 (Internal Error) if errors occurred during matchmaking or placement
|
||||
"""
|
||||
|
||||
gamelift = boto3.client('gamelift')
|
||||
fleet_alias = os.environ['FleetAlias']
|
||||
|
||||
player_id = event["requestContext"]["authorizer"]["claims"]["sub"]
|
||||
print(f'Handling request result request. PlayerId: {player_id}')
|
||||
|
||||
oldest_viable_game_session = get_oldest_viable_game_session(gamelift, fleet_alias)
|
||||
if oldest_viable_game_session:
|
||||
player_session = create_player_session(gamelift, oldest_viable_game_session['GameSessionId'], player_id)
|
||||
|
||||
game_session_connection_info = dict((k, player_session[k]) for k in ('IpAddress', 'Port', 'DnsName', 'PlayerSessionId', 'PlayerId'))
|
||||
game_session_connection_info['GameSessionArn'] = player_session['GameSessionId']
|
||||
print(f"Connection info: {game_session_connection_info}")
|
||||
|
||||
return {
|
||||
'body': json.dumps(game_session_connection_info),
|
||||
'headers': {
|
||||
'Content-Type': 'text/plain'
|
||||
},
|
||||
'statusCode': 200
|
||||
}
|
||||
else:
|
||||
return {
|
||||
'headers': {
|
||||
'Content-Type': 'text/plain'
|
||||
},
|
||||
'statusCode': 404
|
||||
}
|
||||
|
||||
|
||||
def get_oldest_viable_game_session(gamelift, fleet_alias):
|
||||
print("Checking for viable game sessions:", fleet_alias)
|
||||
search_game_sessions_response = gamelift.search_game_sessions(
|
||||
AliasId=fleet_alias,
|
||||
FilterExpression="hasAvailablePlayerSessions=true",
|
||||
SortExpression="creationTimeMillis ASC",
|
||||
)
|
||||
print(f"Received search game session response: {search_game_sessions_response}")
|
||||
return next(iter(search_game_sessions_response['GameSessions']), None)
|
||||
|
||||
def create_player_session(gamelift, game_session_id, player_id):
|
||||
print(f"Creating PlayerSession session on GameSession: {game_session_id}, PlayerId: {player_id}")
|
||||
create_player_session_response = gamelift.create_player_session(
|
||||
GameSessionId=game_session_id,
|
||||
PlayerId=player_id
|
||||
)
|
||||
print(f"Received create player session response: {create_player_session_response}")
|
||||
return create_player_session_response['PlayerSession']
|
||||
@@ -0,0 +1,43 @@
|
||||
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
#
|
||||
# GameLift Region Mappings
|
||||
# Keep synchronized with: https://docs.aws.amazon.com/general/latest/gr/gamelift.html
|
||||
#
|
||||
# ----------------------------------------------------------------------------------------
|
||||
# Region Name Region Endpoint Protocol
|
||||
# ----------------------------------------------------------------------------------------
|
||||
# US East (Ohio) us-east-2 gamelift.us-east-2.amazonaws.com HTTPS
|
||||
# US East (N. Virginia) us-east-1 gamelift.us-east-1.amazonaws.com HTTPS
|
||||
# US West (N. California) us-west-1 gamelift.us-west-1.amazonaws.com HTTPS
|
||||
# US West (Oregon) us-west-2 gamelift.us-west-2.amazonaws.com HTTPS
|
||||
# Asia Pacific (Mumbai) ap-south-1 gamelift.ap-south-1.amazonaws.com HTTPS
|
||||
# Asia Pacific (Seoul) ap-northeast-2 gamelift.ap-northeast-2.amazonaws.com HTTPS
|
||||
# Asia Pacific (Singapore) ap-southeast-1 gamelift.ap-southeast-1.amazonaws.com HTTPS
|
||||
# Asia Pacific (Sydney) ap-southeast-2 gamelift.ap-southeast-2.amazonaws.com HTTPS
|
||||
# Asia Pacific (Tokyo) ap-northeast-1 gamelift.ap-northeast-1.amazonaws.com HTTPS
|
||||
# Canada (Central) ca-central-1 gamelift.ca-central-1.amazonaws.com HTTPS
|
||||
# Europe (Frankfurt) eu-central-1 gamelift.eu-central-1.amazonaws.com HTTPS
|
||||
# Europe (Ireland) eu-west-1 gamelift.eu-west-1.amazonaws.com HTTPS
|
||||
# Europe (London) eu-west-2 gamelift.eu-west-2.amazonaws.com HTTPS
|
||||
# South America (São Paulo) sa-east-1 gamelift.sa-east-1.amazonaws.com HTTPS
|
||||
# ----------------------------------------------------------------------------------------
|
||||
|
||||
{
|
||||
five_letter_region_codes: {
|
||||
us-east-2 : usea2,
|
||||
us-east-1 : usea1,
|
||||
us-west-1 : uswe1,
|
||||
us-west-2 : uswe2,
|
||||
ap-south-1 : apso1,
|
||||
ap-northeast-2 : apne2,
|
||||
ap-southeast-1 : apse1,
|
||||
ap-southeast-2 : apse2,
|
||||
ap-northeast-1 : apne1,
|
||||
ca-central-1 : cace1,
|
||||
eu-central-1 : euce1,
|
||||
eu-west-1 : euwe1,
|
||||
eu-west-2 : euwe2,
|
||||
sa-east-1 : saea1
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import boto3
|
||||
import requests
|
||||
import json
|
||||
import time
|
||||
import argparse
|
||||
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("-g", "--game", help="game name", type=str, required=True)
|
||||
parser.add_argument("-r", "--region", help="region name, e.g. eu-west-1", type=str, required=True)
|
||||
parser.add_argument("-p", "--profile", help="profile name in the AWS shared credentials file ~/.aws/credentials", type=str, required=True)
|
||||
args = parser.parse_args()
|
||||
|
||||
GAME_NAME = args.game.lower() # e.g. 'GameLiftSampleGame2ue4'
|
||||
REGION = args.region # e.g. 'eu-west-1'
|
||||
PROFILE_NAME = args.profile
|
||||
|
||||
USER_POOL_NAME = GAME_NAME + 'UserPool'
|
||||
USER_POOL_CLIENT_NAME = GAME_NAME + 'UserPoolClient'
|
||||
USERNAME = 'testuser@example.com'
|
||||
PASSWORD = 'TestPassw0rd.'
|
||||
REST_API_NAME = GAME_NAME + 'RestApi'
|
||||
REST_API_STAGE = 'dev'
|
||||
GAME_REQUEST_PATH = 'start_game'
|
||||
RESULTS_REQUEST_PATH = 'get_game_connection'
|
||||
session = boto3.Session(profile_name=PROFILE_NAME)
|
||||
cognito_idp = session.client('cognito-idp', region_name=REGION)
|
||||
apig = session.client('apigateway', region_name=REGION)
|
||||
REGION_TO_LATENCY_MAPPING = {
|
||||
"regionToLatencyMapping": {
|
||||
"us-west-2": 50,
|
||||
"us-east-1": 100,
|
||||
"eu-west-1": 150,
|
||||
"ap-northeast-1": 300
|
||||
}
|
||||
}
|
||||
GAME_REQUEST_PAYLOAD = json.dumps(REGION_TO_LATENCY_MAPPING)
|
||||
|
||||
|
||||
def main():
|
||||
user_pool = find_user_pool(USER_POOL_NAME)
|
||||
user_pool_id = user_pool['Id']
|
||||
print("User Pool Id:", user_pool_id)
|
||||
|
||||
user_pool_client = find_user_pool_client(user_pool_id, USER_POOL_CLIENT_NAME)
|
||||
user_pool_client_id = user_pool_client['ClientId']
|
||||
print("User Pool Client Id:", user_pool_client_id)
|
||||
|
||||
try:
|
||||
cognito_idp.sign_up(
|
||||
ClientId=user_pool_client_id,
|
||||
Username=USERNAME,
|
||||
Password=PASSWORD,
|
||||
)
|
||||
|
||||
print("Created user:", USERNAME)
|
||||
|
||||
cognito_idp.admin_confirm_sign_up(
|
||||
UserPoolId=user_pool_id,
|
||||
Username=USERNAME,
|
||||
)
|
||||
|
||||
init_auth_result = cognito_idp.initiate_auth(
|
||||
AuthFlow='USER_PASSWORD_AUTH',
|
||||
AuthParameters={
|
||||
'USERNAME': USERNAME,
|
||||
'PASSWORD': PASSWORD,
|
||||
},
|
||||
ClientId=user_pool_client_id
|
||||
)
|
||||
|
||||
assert init_auth_result['ResponseMetadata']['HTTPStatusCode'] == 200, "Unsuccessful init_auth"
|
||||
print("Authenticated via username and password")
|
||||
|
||||
id_token = init_auth_result['AuthenticationResult']['IdToken']
|
||||
headers = {
|
||||
'Auth': id_token
|
||||
}
|
||||
results_request_url = get_rest_api_endpoint(REST_API_NAME, REGION, REST_API_STAGE, RESULTS_REQUEST_PATH)
|
||||
game_request_url = get_rest_api_endpoint(REST_API_NAME, REGION, REST_API_STAGE, GAME_REQUEST_PATH)
|
||||
|
||||
print ("results_request_url: " + results_request_url)
|
||||
print ("game_request_url: " + game_request_url)
|
||||
|
||||
results_request_response = requests.post(url=results_request_url, headers=headers)
|
||||
assert results_request_response.status_code == 204 or results_request_response.status_code == 200, \
|
||||
"Expect 'POST /get_game_info' status code to be 200 (Success) or 204 (No Content). Actual: " \
|
||||
f"{str(results_request_response.status_code)}"
|
||||
print("Verified mock ResultsRequest response", results_request_response)
|
||||
|
||||
game_request_response = requests.post(url=game_request_url, headers=headers, data=GAME_REQUEST_PAYLOAD)
|
||||
assert game_request_response.status_code == 202, "Expect 'POST /start_game' status code to be 202 (Accepted)/ Actual: " \
|
||||
f"{str(results_request_response.status_code)}"
|
||||
print("Verified lambda GameRequest response", game_request_response)
|
||||
|
||||
#game_request_info = json.loads(game_request_response.content)
|
||||
print(f"Received start game info: {game_request_response}")
|
||||
|
||||
print("Waiting for game session to be created...")
|
||||
time.sleep(10) # 10 seconds
|
||||
|
||||
results_request_response = requests.post(url=results_request_url, headers=headers)
|
||||
assert results_request_response.status_code == 200, "Expect 'POST /get_game_info' status code to be 200 (Success). Actual: " \
|
||||
f"{str(results_request_response.status_code)}"
|
||||
print("Verified lambda ResultsRequest response", results_request_response.content)
|
||||
|
||||
game_connection_info = json.loads(results_request_response.content)
|
||||
print(f"Received game connection info: {game_connection_info}")
|
||||
assert game_connection_info['IpAddress'] != ''
|
||||
assert game_connection_info['Port'] > 0
|
||||
assert REGION in game_connection_info['DnsName'], \
|
||||
f"Expect {game_connection_info['DnsName']} to contain '{REGION}'"
|
||||
assert "psess-" in game_connection_info['PlayerSessionId'], \
|
||||
f"Expect {game_connection_info['PlayerSessionId']} to contain 'psess-'"
|
||||
assert REGION in game_connection_info['GameSessionArn'], \
|
||||
f"Expect {game_connection_info['GameSessionArn']} to contain '{REGION}'"
|
||||
print("Verified game connection info:", game_connection_info)
|
||||
|
||||
finally:
|
||||
cognito_idp.admin_delete_user(
|
||||
UserPoolId=user_pool_id,
|
||||
Username=USERNAME,
|
||||
)
|
||||
|
||||
print("Deleted user:", USERNAME)
|
||||
|
||||
print("Test Succeeded!")
|
||||
|
||||
|
||||
def find_user_pool(user_pool_name):
|
||||
print("Finding user pool:", user_pool_name)
|
||||
result = cognito_idp.list_user_pools(MaxResults=50)
|
||||
pools = result['UserPools']
|
||||
return next(x for x in pools if x['Name'] == user_pool_name)
|
||||
|
||||
|
||||
def find_user_pool_client(user_pool_id, user_pool_client_name):
|
||||
print("Finding user pool client:", user_pool_client_name)
|
||||
results = cognito_idp.list_user_pool_clients(UserPoolId=user_pool_id)
|
||||
clients = results['UserPoolClients']
|
||||
return next(x for x in clients if x['ClientName'] == user_pool_client_name)
|
||||
|
||||
|
||||
def find_rest_api(rest_api_name):
|
||||
print("Finding rest api:", rest_api_name)
|
||||
results = apig.get_rest_apis()
|
||||
rest_apis = results['items']
|
||||
return next(x for x in rest_apis if x['name'] == rest_api_name)
|
||||
|
||||
|
||||
def get_rest_api_endpoint(rest_api_name, region, stage, path):
|
||||
print("Getting rest api endpoint", rest_api_name)
|
||||
rest_api = find_rest_api(rest_api_name)
|
||||
rest_api_id = rest_api['id']
|
||||
return f'https://{rest_api_id}.execute-api.{region}.amazonaws.com/{stage}/{path}'
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,481 @@
|
||||
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
AWSTemplateFormatVersion: "2010-09-09"
|
||||
|
||||
Description: >
|
||||
This CloudFormation template sets up a game backend service with a single Amazon GameLift fleet. After player
|
||||
authenticates and start a game via POST /start_game, a lambda handler searches for an existing viable game session
|
||||
with open player slot on the fleet, and if not found, creates a new game session. The game client is then expected
|
||||
to poll POST /get_game_connection to receive a viable game session.
|
||||
|
||||
Parameters:
|
||||
ApiGatewayStageNameParameter:
|
||||
Type: String
|
||||
Default: v1
|
||||
Description: Name of the Api Gateway stage
|
||||
|
||||
ContainerGroupDefinitionNameParameter:
|
||||
Type: String
|
||||
Default: SampleCGDName
|
||||
Description: Name of the Api Gateway stage
|
||||
|
||||
ContainerImageNameParameter:
|
||||
Type: String
|
||||
Default: SampleContainerImageName
|
||||
Description: Name for the Container Image
|
||||
|
||||
ContainerImageUriParameter:
|
||||
Type: String
|
||||
Description: URI pointing to a Container Image in ECR
|
||||
|
||||
FleetDescriptionParameter:
|
||||
Type: String
|
||||
Default: Deployed by the Amazon GameLift Plug-in for Unreal.
|
||||
Description: Description of the fleet
|
||||
|
||||
TotalMemoryLimitParameter:
|
||||
Type: Number
|
||||
Default: 4000
|
||||
Description: The maximum amount of memory (in MiB) to allocate to the container group
|
||||
|
||||
TotalVcpuLimitParameter:
|
||||
Type: Number
|
||||
Default: 2
|
||||
Description: The maximum amount of CPU units to allocate to the container group
|
||||
|
||||
FleetUdpFromPortParameter:
|
||||
Type: Number
|
||||
Default: 33430
|
||||
Description: Starting port number for UDP ports to be opened
|
||||
|
||||
FleetUdpToPortParameter:
|
||||
Type: Number
|
||||
Default: 33440
|
||||
Description: Ending port number for UDP ports to be opened
|
||||
|
||||
GameNameParameter:
|
||||
Type: String
|
||||
Default: MyGame
|
||||
Description: Game name to prepend before resource names
|
||||
MaxLength: 30
|
||||
|
||||
LambdaZipS3BucketParameter:
|
||||
Type: String
|
||||
Description: S3 bucket that stores the lambda function zip
|
||||
|
||||
LambdaZipS3KeyParameter:
|
||||
Type: String
|
||||
Description: S3 key that stores the lambda function zip
|
||||
|
||||
LaunchPathParameter:
|
||||
Type: String
|
||||
Description: Location of the game server executable in the build
|
||||
|
||||
MaxPlayersPerGameParameter:
|
||||
Type: Number
|
||||
Default: 10
|
||||
Description: Maximum number of players per game session
|
||||
|
||||
MaxTransactionsPerFiveMinutesPerIpParameter:
|
||||
Type: Number
|
||||
Default: 100
|
||||
MaxValue: 20000000
|
||||
MinValue: 100
|
||||
|
||||
UnrealEngineVersionParameter:
|
||||
Type: String
|
||||
Description: "Unreal Engine Version being used by the plugin"
|
||||
|
||||
EnableMetricsParameter:
|
||||
Type: String
|
||||
Default: "false"
|
||||
AllowedValues: ["true", "false"]
|
||||
Description: "Enable telemetry metrics collection using OTEL collector"
|
||||
|
||||
Conditions:
|
||||
ShouldCreateMetricsResources: !Equals [!Ref EnableMetricsParameter, "true"]
|
||||
|
||||
Resources:
|
||||
ApiGatewayCloudWatchRole:
|
||||
Type: "AWS::IAM::Role"
|
||||
Properties:
|
||||
AssumeRolePolicyDocument:
|
||||
Version: "2012-10-17"
|
||||
Statement:
|
||||
- Effect: Allow
|
||||
Principal:
|
||||
Service:
|
||||
- apigateway.amazonaws.com
|
||||
Action: "sts:AssumeRole"
|
||||
ManagedPolicyArns:
|
||||
- "arn:aws:iam::aws:policy/service-role/AmazonAPIGatewayPushToCloudWatchLogs"
|
||||
|
||||
Account:
|
||||
Type: "AWS::ApiGateway::Account"
|
||||
Properties:
|
||||
CloudWatchRoleArn: !GetAtt ApiGatewayCloudWatchRole.Arn
|
||||
|
||||
GameRequestLambdaFunctionExecutionRole:
|
||||
Type: "AWS::IAM::Role"
|
||||
Properties:
|
||||
AssumeRolePolicyDocument:
|
||||
Version: "2012-10-17"
|
||||
Statement:
|
||||
- Effect: Allow
|
||||
Principal:
|
||||
Service:
|
||||
- lambda.amazonaws.com
|
||||
Action:
|
||||
- "sts:AssumeRole"
|
||||
ManagedPolicyArns:
|
||||
- "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
|
||||
Policies:
|
||||
- PolicyName: !Sub ${GameNameParameter}GameRequestLambdaFunctionGameLiftPolicies
|
||||
PolicyDocument:
|
||||
Version: "2012-10-17"
|
||||
Statement:
|
||||
- Effect: Allow
|
||||
Action:
|
||||
- "gamelift:CreateGameSession"
|
||||
- "gamelift:CreatePlayerSession"
|
||||
- "gamelift:SearchGameSessions"
|
||||
Resource: "*"
|
||||
|
||||
RestApi:
|
||||
Type: "AWS::ApiGateway::RestApi"
|
||||
Properties:
|
||||
Name: !Sub ${GameNameParameter}RestApi
|
||||
|
||||
ResultsRequestLambdaFunctionExecutionRole:
|
||||
Type: "AWS::IAM::Role"
|
||||
Properties:
|
||||
AssumeRolePolicyDocument:
|
||||
Version: "2012-10-17"
|
||||
Statement:
|
||||
- Effect: Allow
|
||||
Principal:
|
||||
Service:
|
||||
- lambda.amazonaws.com
|
||||
Action:
|
||||
- "sts:AssumeRole"
|
||||
ManagedPolicyArns:
|
||||
- "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
|
||||
Policies:
|
||||
- PolicyName: !Sub ${GameNameParameter}ResultsRequestLambdaFunctionGameLiftPolicies
|
||||
PolicyDocument:
|
||||
Version: "2012-10-17"
|
||||
Statement:
|
||||
- Effect: Allow
|
||||
Action:
|
||||
- "gamelift:CreateGameSession"
|
||||
- "gamelift:CreatePlayerSession"
|
||||
- "gamelift:SearchGameSessions"
|
||||
Resource: "*"
|
||||
|
||||
UserPool:
|
||||
Type: "AWS::Cognito::UserPool"
|
||||
Properties:
|
||||
AdminCreateUserConfig:
|
||||
AllowAdminCreateUserOnly: false
|
||||
AutoVerifiedAttributes:
|
||||
- email
|
||||
EmailConfiguration:
|
||||
EmailSendingAccount: COGNITO_DEFAULT
|
||||
EmailVerificationMessage: "Please verify your email to complete account registration for the GameLift Plugin Single-fleet deployment scenario. Confirmation Code {####}."
|
||||
EmailVerificationSubject: GameLift Plugin - Deployment Scenario Account Verification
|
||||
Policies:
|
||||
PasswordPolicy:
|
||||
MinimumLength: 8
|
||||
RequireLowercase: true
|
||||
RequireNumbers: true
|
||||
RequireSymbols: true
|
||||
RequireUppercase: true
|
||||
Schema:
|
||||
- Name: email
|
||||
AttributeDataType: String
|
||||
Mutable: false
|
||||
Required: true
|
||||
UserPoolName: !Sub ${GameNameParameter}UserPool
|
||||
UsernameAttributes:
|
||||
- email
|
||||
|
||||
GameRequestApiResource:
|
||||
Type: "AWS::ApiGateway::Resource"
|
||||
Properties:
|
||||
ParentId: !GetAtt RestApi.RootResourceId
|
||||
PathPart: start_game
|
||||
RestApiId: !Ref RestApi
|
||||
|
||||
ResultsRequestApiResource:
|
||||
Type: "AWS::ApiGateway::Resource"
|
||||
Properties:
|
||||
ParentId: !GetAtt RestApi.RootResourceId
|
||||
PathPart: get_game_connection
|
||||
RestApiId: !Ref RestApi
|
||||
|
||||
UserPoolClient:
|
||||
Type: "AWS::Cognito::UserPoolClient"
|
||||
Properties:
|
||||
AccessTokenValidity: 1
|
||||
ClientName: !Sub ${GameNameParameter}UserPoolClient
|
||||
ExplicitAuthFlows:
|
||||
- ALLOW_USER_PASSWORD_AUTH
|
||||
- ALLOW_REFRESH_TOKEN_AUTH
|
||||
GenerateSecret: false
|
||||
IdTokenValidity: 1
|
||||
PreventUserExistenceErrors: ENABLED
|
||||
ReadAttributes:
|
||||
- email
|
||||
- preferred_username
|
||||
RefreshTokenValidity: 30
|
||||
SupportedIdentityProviders:
|
||||
- COGNITO
|
||||
UserPoolId: !Ref UserPool
|
||||
|
||||
WebACL:
|
||||
Type: "AWS::WAFv2::WebACL"
|
||||
DependsOn:
|
||||
- ApiDeployment
|
||||
Properties:
|
||||
DefaultAction:
|
||||
Allow:
|
||||
{}
|
||||
Description: !Sub "WebACL for game: ${GameNameParameter}"
|
||||
Name: !Sub ${GameNameParameter}WebACL
|
||||
Rules:
|
||||
- Name: !Sub ${GameNameParameter}WebACLPerIpThrottleRule
|
||||
Action:
|
||||
Block:
|
||||
{}
|
||||
Priority: 0
|
||||
Statement:
|
||||
RateBasedStatement:
|
||||
AggregateKeyType: IP
|
||||
Limit: !Ref MaxTransactionsPerFiveMinutesPerIpParameter
|
||||
VisibilityConfig:
|
||||
CloudWatchMetricsEnabled: true
|
||||
MetricName: !Sub ${GameNameParameter}WebACLPerIpThrottleRuleMetrics
|
||||
SampledRequestsEnabled: true
|
||||
Scope: REGIONAL
|
||||
VisibilityConfig:
|
||||
CloudWatchMetricsEnabled: true
|
||||
MetricName: !Sub ${GameNameParameter}WebACLMetrics
|
||||
SampledRequestsEnabled: true
|
||||
|
||||
ApiDeployment:
|
||||
Type: "AWS::ApiGateway::Deployment"
|
||||
DependsOn:
|
||||
- GameRequestApiMethod
|
||||
- ResultsRequestApiMethod
|
||||
Properties:
|
||||
RestApiId: !Ref RestApi
|
||||
StageDescription:
|
||||
DataTraceEnabled: true
|
||||
LoggingLevel: INFO
|
||||
MetricsEnabled: true
|
||||
StageName: !Ref ApiGatewayStageNameParameter
|
||||
|
||||
Authorizer:
|
||||
Type: "AWS::ApiGateway::Authorizer"
|
||||
Properties:
|
||||
IdentitySource: method.request.header.Auth
|
||||
Name: CognitoAuthorizer
|
||||
ProviderARNs:
|
||||
- "Fn::GetAtt":
|
||||
- UserPool
|
||||
- Arn
|
||||
RestApiId: !Ref RestApi
|
||||
Type: COGNITO_USER_POOLS
|
||||
|
||||
GameRequestApiMethod:
|
||||
Type: "AWS::ApiGateway::Method"
|
||||
Properties:
|
||||
AuthorizationType: COGNITO_USER_POOLS
|
||||
AuthorizerId: !Ref Authorizer
|
||||
HttpMethod: POST
|
||||
Integration:
|
||||
Type: AWS_PROXY
|
||||
IntegrationHttpMethod: POST
|
||||
Uri: !Sub "arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${GameRequestLambdaFunction.Arn}/invocations"
|
||||
OperationName: GameRequest
|
||||
ResourceId: !Ref GameRequestApiResource
|
||||
RestApiId: !Ref RestApi
|
||||
|
||||
ResultsRequestApiMethod:
|
||||
Type: "AWS::ApiGateway::Method"
|
||||
Properties:
|
||||
AuthorizationType: COGNITO_USER_POOLS
|
||||
AuthorizerId: !Ref Authorizer
|
||||
HttpMethod: POST
|
||||
Integration:
|
||||
Type: AWS_PROXY
|
||||
IntegrationHttpMethod: POST
|
||||
Uri: !Sub "arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${ResultsRequestLambdaFunction.Arn}/invocations"
|
||||
OperationName: ResultsRequest
|
||||
ResourceId: !Ref ResultsRequestApiResource
|
||||
RestApiId: !Ref RestApi
|
||||
|
||||
WebACLAssociation:
|
||||
Type: "AWS::WAFv2::WebACLAssociation"
|
||||
DependsOn:
|
||||
- ApiDeployment
|
||||
- WebACL
|
||||
Properties:
|
||||
ResourceArn: !Sub
|
||||
- "arn:aws:apigateway:${REGION}::/restapis/${REST_API_ID}/stages/${STAGE_NAME}"
|
||||
- REGION: !Ref "AWS::Region"
|
||||
REST_API_ID: !Ref RestApi
|
||||
STAGE_NAME: !Ref ApiGatewayStageNameParameter
|
||||
WebACLArn: !GetAtt WebACL.Arn
|
||||
|
||||
ContainerFleetRole:
|
||||
Type: "AWS::IAM::Role"
|
||||
Properties:
|
||||
AssumeRolePolicyDocument:
|
||||
Version: "2012-10-17"
|
||||
Statement:
|
||||
- Effect: Allow
|
||||
Principal:
|
||||
Service:
|
||||
- cloudformation.amazonaws.com
|
||||
- gamelift.amazonaws.com
|
||||
Action:
|
||||
- "sts:AssumeRole"
|
||||
ManagedPolicyArns:
|
||||
- "arn:aws:iam::aws:policy/GameLiftContainerFleetPolicy"
|
||||
Policies: !If
|
||||
- ShouldCreateMetricsResources
|
||||
- - PolicyName: AMPRemoteWriteAccess
|
||||
PolicyDocument:
|
||||
Version: '2012-10-17'
|
||||
Statement:
|
||||
- Effect: Allow
|
||||
Action:
|
||||
- aps:RemoteWrite
|
||||
Resource: !Sub 'arn:aws:aps:*:${AWS::AccountId}:workspace/*'
|
||||
- PolicyName: OTelCollectorEMFExporter
|
||||
PolicyDocument:
|
||||
Version: '2012-10-17'
|
||||
Statement:
|
||||
- Effect: Allow
|
||||
Action:
|
||||
- logs:PutLogEvents
|
||||
- logs:CreateLogStream
|
||||
- logs:CreateLogGroup
|
||||
- logs:PutRetentionPolicy
|
||||
Resource: !Sub 'arn:aws:logs:*:${AWS::AccountId}:log-group:*:log-stream:*'
|
||||
- []
|
||||
|
||||
ContainerGroupResource:
|
||||
Type: "AWS::GameLift::ContainerGroupDefinition"
|
||||
Properties:
|
||||
GameServerContainerDefinition:
|
||||
ContainerName: !Ref ContainerImageNameParameter
|
||||
ImageUri: !Ref ContainerImageUriParameter
|
||||
ServerSdkVersion: "5.4.0"
|
||||
PortConfiguration:
|
||||
ContainerPortRanges:
|
||||
- FromPort: !Ref FleetUdpFromPortParameter
|
||||
Protocol: "UDP"
|
||||
ToPort: !Ref FleetUdpToPortParameter
|
||||
Name: !Ref ContainerGroupDefinitionNameParameter
|
||||
OperatingSystem: "AMAZON_LINUX_2023"
|
||||
TotalVcpuLimit: !Ref TotalVcpuLimitParameter
|
||||
TotalMemoryLimitMebibytes: !Ref TotalMemoryLimitParameter
|
||||
|
||||
ContainerFleetResource:
|
||||
DependsOn:
|
||||
- ContainerGroupResource
|
||||
Type: "AWS::GameLift::ContainerFleet"
|
||||
Properties:
|
||||
GameServerContainerGroupDefinitionName: !Ref ContainerGroupDefinitionNameParameter
|
||||
InstanceConnectionPortRange:
|
||||
FromPort: !Ref FleetUdpFromPortParameter
|
||||
ToPort: !Ref FleetUdpToPortParameter
|
||||
Description: !Sub
|
||||
- "${FleetDescriptionParameter} Using Unreal Engine Version ${UnrealEngineVersionParameter}${MetricsText}"
|
||||
- MetricsText: !If [ShouldCreateMetricsResources, " with telemetry metrics enabled", ""]
|
||||
InstanceInboundPermissions:
|
||||
- FromPort: !Ref FleetUdpFromPortParameter
|
||||
IpRange: "0.0.0.0/0"
|
||||
Protocol: UDP
|
||||
ToPort: !Ref FleetUdpToPortParameter
|
||||
InstanceType: c4.xlarge
|
||||
BillingType: ON_DEMAND
|
||||
FleetRoleArn: !GetAtt ContainerFleetRole.Arn
|
||||
NewGameSessionProtectionPolicy: FullProtection
|
||||
GameSessionCreationLimitPolicy:
|
||||
NewGameSessionsPerCreator: 5
|
||||
PolicyPeriodInMinutes: 2
|
||||
|
||||
AliasResource:
|
||||
Type: "AWS::GameLift::Alias"
|
||||
Properties:
|
||||
Description: !Sub Alias to access ${GameNameParameter} fleet
|
||||
Name: !Sub ${GameNameParameter}FleetAlias
|
||||
RoutingStrategy:
|
||||
Type: SIMPLE
|
||||
FleetId: !Ref ContainerFleetResource
|
||||
|
||||
ResultsRequestLambdaFunction:
|
||||
Type: "AWS::Lambda::Function"
|
||||
Properties:
|
||||
Code:
|
||||
S3Bucket: !Ref LambdaZipS3BucketParameter
|
||||
S3Key: !Ref LambdaZipS3KeyParameter
|
||||
Description: Lambda function to handle game requests
|
||||
Environment:
|
||||
Variables:
|
||||
FleetAlias: !Ref AliasResource
|
||||
FunctionName: !Sub ${GameNameParameter}ResultsRequestLambda
|
||||
Handler: results_request.handler
|
||||
MemorySize: 128
|
||||
Role: !GetAtt ResultsRequestLambdaFunctionExecutionRole.Arn
|
||||
Runtime: python3.14
|
||||
|
||||
GameRequestLambdaFunction:
|
||||
Type: "AWS::Lambda::Function"
|
||||
Properties:
|
||||
Code:
|
||||
S3Bucket: !Ref LambdaZipS3BucketParameter
|
||||
S3Key: !Ref LambdaZipS3KeyParameter
|
||||
Description: Lambda function to handle game requests
|
||||
Environment:
|
||||
Variables:
|
||||
FleetAlias: !Ref AliasResource
|
||||
MaxPlayersPerGame: !Ref MaxPlayersPerGameParameter
|
||||
FunctionName: !Sub ${GameNameParameter}GameRequestLambda
|
||||
Handler: game_request.handler
|
||||
MemorySize: 128
|
||||
Role: !GetAtt GameRequestLambdaFunctionExecutionRole.Arn
|
||||
Runtime: python3.14
|
||||
|
||||
ResultsRequestLambdaFunctionApiGatewayPermission:
|
||||
Type: "AWS::Lambda::Permission"
|
||||
Properties:
|
||||
Action: "lambda:InvokeFunction"
|
||||
FunctionName: !GetAtt ResultsRequestLambdaFunction.Arn
|
||||
Principal: apigateway.amazonaws.com
|
||||
SourceArn: !Sub "arn:aws:execute-api:${AWS::Region}:${AWS::AccountId}:${RestApi}/*/*/*"
|
||||
|
||||
GameRequestLambdaFunctionApiGatewayPermission:
|
||||
Type: "AWS::Lambda::Permission"
|
||||
Properties:
|
||||
Action: "lambda:InvokeFunction"
|
||||
FunctionName: !GetAtt GameRequestLambdaFunction.Arn
|
||||
Principal: apigateway.amazonaws.com
|
||||
SourceArn: !Sub "arn:aws:execute-api:${AWS::Region}:${AWS::AccountId}:${RestApi}/*/*/*"
|
||||
|
||||
Outputs:
|
||||
ApiGatewayEndpoint:
|
||||
Description: Url of ApiGateway Endpoint
|
||||
Value: !Sub "https://${RestApi}.execute-api.${AWS::Region}.amazonaws.com/${ApiGatewayStageNameParameter}/"
|
||||
|
||||
UserPoolClientId:
|
||||
Description: Id of UserPoolClient
|
||||
Value: !Ref UserPoolClient
|
||||
|
||||
IdentityRegion:
|
||||
Description: Region name
|
||||
Value: !Ref "AWS::Region"
|
||||
@@ -0,0 +1,33 @@
|
||||
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
---
|
||||
# THIS IS A SAMPLE CLOUDFORMATION PARAMETERS FILE
|
||||
GameNameParameter:
|
||||
value: "{{AWSGAMELIFT::SYS::GAMENAME}}"
|
||||
LambdaZipS3BucketParameter:
|
||||
value: "{{AWSGAMELIFT::VARS::LambdaZipS3BucketParameter}}"
|
||||
LambdaZipS3KeyParameter:
|
||||
value: "{{AWSGAMELIFT::VARS::LambdaZipS3KeyParameter}}"
|
||||
ApiGatewayStageNameParameter:
|
||||
value: "{{AWSGAMELIFT::VARS::ApiGatewayStageNameParameter}}"
|
||||
ContainerGroupDefinitionNameParameter:
|
||||
value: "{{AWSGAMELIFT::VARS::ContainerGroupDefinitionNameParameter}}"
|
||||
ContainerImageNameParameter:
|
||||
value: "{{AWSGAMELIFT::VARS::ContainerImageNameParameter}}"
|
||||
ContainerImageUriParameter:
|
||||
value: "{{AWSGAMELIFT::VARS::ContainerImageUriParameter}}"
|
||||
LaunchPathParameter:
|
||||
value: "{{AWSGAMELIFT::VARS::LaunchPathParameter}}"
|
||||
TotalVcpuLimitParameter:
|
||||
value: "{{AWSGAMELIFT::VARS::TotalVcpuLimitParameter}}"
|
||||
TotalMemoryLimitParameter:
|
||||
value: "{{AWSGAMELIFT::VARS::TotalMemoryLimitParameter}}"
|
||||
FleetUdpFromPortParameter:
|
||||
value: "{{AWSGAMELIFT::VARS::FleetUdpFromPortParameter}}"
|
||||
FleetUdpToPortParameter:
|
||||
value: "{{AWSGAMELIFT::VARS::FleetUdpToPortParameter}}"
|
||||
UnrealEngineVersionParameter:
|
||||
value: "{{AWSGAMELIFT::VARS::UnrealEngineVersionParameter}}"
|
||||
EnableMetricsParameter:
|
||||
value: "{{AWSGAMELIFT::VARS::EnableMetricsParameter}}"
|
||||
@@ -0,0 +1,11 @@
|
||||
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
---
|
||||
# Key/value pairs to be added to the game's awsGameLiftClientConfig.yml file
|
||||
# These values will be replaced at the end of create/update of
|
||||
# the feature's CloudFormation stack.
|
||||
|
||||
user_pool_client_id: "{{AWSGAMELIFT::CFNOUTPUT::UserPoolClientId}}"
|
||||
identity_api_gateway_base_url: "{{AWSGAMELIFT::CFNOUTPUT::ApiGatewayEndpoint}}"
|
||||
identity_region: "{{AWSGAMELIFT::CFNOUTPUT::IdentityRegion}}"
|
||||
@@ -0,0 +1,79 @@
|
||||
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import boto3
|
||||
import os
|
||||
import json
|
||||
|
||||
|
||||
def handler(event, context):
|
||||
"""
|
||||
Handles requests to start games from the game client.
|
||||
This function first looks for any game session
|
||||
:param event: lambda event, contains the region to player latency mapping in `regionToLatencyMapping` key, as well
|
||||
as the player information from the Cognito id tokens.
|
||||
:param context: lambda context, not used by this function
|
||||
:return:
|
||||
- 202 (Accepted) if the matchmaking request is accepted and is now being processed
|
||||
- 409 (Conflict) if the another matchmaking request is in progress
|
||||
- 500 (Internal Error) if error occurred when processing the matchmaking request
|
||||
"""
|
||||
|
||||
gamelift = boto3.client('gamelift')
|
||||
fleet_alias = os.environ['FleetAlias']
|
||||
max_players_per_game = int(os.environ['MaxPlayersPerGame'])
|
||||
|
||||
player_id = event["requestContext"]["authorizer"]["claims"]["sub"]
|
||||
print(f'Handling start game request. PlayerId: {player_id}')
|
||||
|
||||
# NOTE: latency mapping is not used in this deployment scenario
|
||||
region_to_latency_mapping = get_region_to_latency_mapping(event)
|
||||
if region_to_latency_mapping:
|
||||
print(f"Region to latency mapping: {region_to_latency_mapping}")
|
||||
else:
|
||||
print("No regionToLatencyMapping mapping provided")
|
||||
|
||||
if not has_viable_game_sessions(gamelift, fleet_alias):
|
||||
create_game_session(gamelift, fleet_alias, max_players_per_game)
|
||||
|
||||
return {
|
||||
'headers': {
|
||||
'Content-Type': 'text/plain'
|
||||
},
|
||||
'statusCode': 202
|
||||
}
|
||||
|
||||
|
||||
def has_viable_game_sessions(gamelift, fleet_alias):
|
||||
print(f"Checking for viable game sessions: {fleet_alias}")
|
||||
|
||||
# NOTE: SortExpression="creationTimeMillis ASC" is not needed because we are looking for any viable game sessions,
|
||||
# hence the order does not matter.
|
||||
search_game_sessions_response = gamelift.search_game_sessions(
|
||||
AliasId=fleet_alias,
|
||||
FilterExpression="hasAvailablePlayerSessions=true",
|
||||
)
|
||||
return len(search_game_sessions_response['GameSessions']) != 0
|
||||
|
||||
|
||||
def create_game_session(gamelift, fleet_alias, max_players_per_game):
|
||||
print(f"Creating game session: {fleet_alias}")
|
||||
gamelift.create_game_session(
|
||||
AliasId=fleet_alias,
|
||||
MaximumPlayerSessionCount=max_players_per_game,
|
||||
)
|
||||
|
||||
|
||||
def get_region_to_latency_mapping(event):
|
||||
request_body = event.get("body")
|
||||
if not request_body:
|
||||
return None
|
||||
|
||||
try:
|
||||
request_body_json = json.loads(request_body)
|
||||
except ValueError:
|
||||
print(f"Error parsing request body: {request_body}")
|
||||
return None
|
||||
|
||||
if request_body_json and request_body_json.get('regionToLatencyMapping'):
|
||||
return request_body_json.get('regionToLatencyMapping')
|
||||
@@ -0,0 +1,73 @@
|
||||
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import boto3
|
||||
import os
|
||||
import json
|
||||
|
||||
|
||||
def handler(event, context):
|
||||
"""
|
||||
Handles requests to describe the game session connection information after a StartGame request.
|
||||
This function will look up the MatchmakingRequest table to find a pending matchmaking request by
|
||||
the player, and if it is QUEUED, look up the GameSessionPlacement table to find the game's
|
||||
connection information.
|
||||
:param event: lambda event, contains the region to player latency mapping in `regionToLatencyMapping` key, as well
|
||||
as the player information from the Cognito id tokens. Cognito provides a `sub` user attribute that we use as a
|
||||
Player ID. Unlike `username` value `sub` is UUID for a user which is never reassigned to another user.
|
||||
:param context: lambda context, not used by this function
|
||||
:return:
|
||||
- 200 (OK) if the game connection is ready, along with server info: "IpAddress", "Port", "DnsName", "PlayerSessionId", "PlayerId"
|
||||
- 204 (No Content) if the requested game is still in progress of matchmaking
|
||||
- 404 (Not Found) if no game has been started by the player, or if all started game were expired
|
||||
- 500 (Internal Error) if errors occurred during matchmaking or placement
|
||||
"""
|
||||
|
||||
gamelift = boto3.client('gamelift')
|
||||
fleet_alias = os.environ['FleetAlias']
|
||||
|
||||
player_id = event["requestContext"]["authorizer"]["claims"]["sub"]
|
||||
print(f'Handling request result request. PlayerId: {player_id}')
|
||||
|
||||
oldest_viable_game_session = get_oldest_viable_game_session(gamelift, fleet_alias)
|
||||
if oldest_viable_game_session:
|
||||
player_session = create_player_session(gamelift, oldest_viable_game_session['GameSessionId'], player_id)
|
||||
|
||||
game_session_connection_info = dict((k, player_session[k]) for k in ('IpAddress', 'Port', 'DnsName', 'PlayerSessionId', 'PlayerId'))
|
||||
game_session_connection_info['GameSessionArn'] = player_session['GameSessionId']
|
||||
print(f"Connection info: {game_session_connection_info}")
|
||||
|
||||
return {
|
||||
'body': json.dumps(game_session_connection_info),
|
||||
'headers': {
|
||||
'Content-Type': 'text/plain'
|
||||
},
|
||||
'statusCode': 200
|
||||
}
|
||||
else:
|
||||
return {
|
||||
'headers': {
|
||||
'Content-Type': 'text/plain'
|
||||
},
|
||||
'statusCode': 404
|
||||
}
|
||||
|
||||
|
||||
def get_oldest_viable_game_session(gamelift, fleet_alias):
|
||||
print("Checking for viable game sessions:", fleet_alias)
|
||||
search_game_sessions_response = gamelift.search_game_sessions(
|
||||
AliasId=fleet_alias,
|
||||
FilterExpression="hasAvailablePlayerSessions=true",
|
||||
SortExpression="creationTimeMillis ASC",
|
||||
)
|
||||
print(f"Received search game session response: {search_game_sessions_response}")
|
||||
return next(iter(search_game_sessions_response['GameSessions']), None)
|
||||
|
||||
def create_player_session(gamelift, game_session_id, player_id):
|
||||
print(f"Creating PlayerSession session on GameSession: {game_session_id}, PlayerId: {player_id}")
|
||||
create_player_session_response = gamelift.create_player_session(
|
||||
GameSessionId=game_session_id,
|
||||
PlayerId=player_id
|
||||
)
|
||||
print(f"Received create player session response: {create_player_session_response}")
|
||||
return create_player_session_response['PlayerSession']
|
||||
@@ -0,0 +1,43 @@
|
||||
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
#
|
||||
# GameLift Region Mappings
|
||||
# Keep synchronized with: https://docs.aws.amazon.com/general/latest/gr/gamelift.html
|
||||
#
|
||||
# ----------------------------------------------------------------------------------------
|
||||
# Region Name Region Endpoint Protocol
|
||||
# ----------------------------------------------------------------------------------------
|
||||
# US East (Ohio) us-east-2 gamelift.us-east-2.amazonaws.com HTTPS
|
||||
# US East (N. Virginia) us-east-1 gamelift.us-east-1.amazonaws.com HTTPS
|
||||
# US West (N. California) us-west-1 gamelift.us-west-1.amazonaws.com HTTPS
|
||||
# US West (Oregon) us-west-2 gamelift.us-west-2.amazonaws.com HTTPS
|
||||
# Asia Pacific (Mumbai) ap-south-1 gamelift.ap-south-1.amazonaws.com HTTPS
|
||||
# Asia Pacific (Seoul) ap-northeast-2 gamelift.ap-northeast-2.amazonaws.com HTTPS
|
||||
# Asia Pacific (Singapore) ap-southeast-1 gamelift.ap-southeast-1.amazonaws.com HTTPS
|
||||
# Asia Pacific (Sydney) ap-southeast-2 gamelift.ap-southeast-2.amazonaws.com HTTPS
|
||||
# Asia Pacific (Tokyo) ap-northeast-1 gamelift.ap-northeast-1.amazonaws.com HTTPS
|
||||
# Canada (Central) ca-central-1 gamelift.ca-central-1.amazonaws.com HTTPS
|
||||
# Europe (Frankfurt) eu-central-1 gamelift.eu-central-1.amazonaws.com HTTPS
|
||||
# Europe (Ireland) eu-west-1 gamelift.eu-west-1.amazonaws.com HTTPS
|
||||
# Europe (London) eu-west-2 gamelift.eu-west-2.amazonaws.com HTTPS
|
||||
# South America (São Paulo) sa-east-1 gamelift.sa-east-1.amazonaws.com HTTPS
|
||||
# ----------------------------------------------------------------------------------------
|
||||
|
||||
{
|
||||
five_letter_region_codes: {
|
||||
us-east-2 : usea2,
|
||||
us-east-1 : usea1,
|
||||
us-west-1 : uswe1,
|
||||
us-west-2 : uswe2,
|
||||
ap-south-1 : apso1,
|
||||
ap-northeast-2 : apne2,
|
||||
ap-southeast-1 : apse1,
|
||||
ap-southeast-2 : apse2,
|
||||
ap-northeast-1 : apne1,
|
||||
ca-central-1 : cace1,
|
||||
eu-central-1 : euce1,
|
||||
eu-west-1 : euwe1,
|
||||
eu-west-2 : euwe2,
|
||||
sa-east-1 : saea1
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import boto3
|
||||
import requests
|
||||
import json
|
||||
import time
|
||||
import argparse
|
||||
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("-g", "--game", help="game name", type=str, required=True)
|
||||
parser.add_argument("-r", "--region", help="region name, e.g. eu-west-1", type=str, required=True)
|
||||
parser.add_argument("-p", "--profile", help="profile name in the AWS shared credentials file ~/.aws/credentials", type=str, required=True)
|
||||
args = parser.parse_args()
|
||||
|
||||
GAME_NAME = args.game.lower() # e.g. 'GameLiftSampleGame2ue4'
|
||||
REGION = args.region # e.g. 'eu-west-1'
|
||||
PROFILE_NAME = args.profile
|
||||
|
||||
USER_POOL_NAME = GAME_NAME + 'UserPool'
|
||||
USER_POOL_CLIENT_NAME = GAME_NAME + 'UserPoolClient'
|
||||
USERNAME = 'testuser@example.com'
|
||||
PASSWORD = 'TestPassw0rd.'
|
||||
REST_API_NAME = GAME_NAME + 'RestApi'
|
||||
REST_API_STAGE = 'dev'
|
||||
GAME_REQUEST_PATH = 'start_game'
|
||||
RESULTS_REQUEST_PATH = 'get_game_connection'
|
||||
session = boto3.Session(profile_name=PROFILE_NAME)
|
||||
cognito_idp = session.client('cognito-idp', region_name=REGION)
|
||||
apig = session.client('apigateway', region_name=REGION)
|
||||
REGION_TO_LATENCY_MAPPING = {
|
||||
"regionToLatencyMapping": {
|
||||
"us-west-2": 50,
|
||||
"us-east-1": 100,
|
||||
"eu-west-1": 150,
|
||||
"ap-northeast-1": 300
|
||||
}
|
||||
}
|
||||
GAME_REQUEST_PAYLOAD = json.dumps(REGION_TO_LATENCY_MAPPING)
|
||||
|
||||
|
||||
def main():
|
||||
user_pool = find_user_pool(USER_POOL_NAME)
|
||||
user_pool_id = user_pool['Id']
|
||||
print("User Pool Id:", user_pool_id)
|
||||
|
||||
user_pool_client = find_user_pool_client(user_pool_id, USER_POOL_CLIENT_NAME)
|
||||
user_pool_client_id = user_pool_client['ClientId']
|
||||
print("User Pool Client Id:", user_pool_client_id)
|
||||
|
||||
try:
|
||||
cognito_idp.sign_up(
|
||||
ClientId=user_pool_client_id,
|
||||
Username=USERNAME,
|
||||
Password=PASSWORD,
|
||||
)
|
||||
|
||||
print("Created user:", USERNAME)
|
||||
|
||||
cognito_idp.admin_confirm_sign_up(
|
||||
UserPoolId=user_pool_id,
|
||||
Username=USERNAME,
|
||||
)
|
||||
|
||||
init_auth_result = cognito_idp.initiate_auth(
|
||||
AuthFlow='USER_PASSWORD_AUTH',
|
||||
AuthParameters={
|
||||
'USERNAME': USERNAME,
|
||||
'PASSWORD': PASSWORD,
|
||||
},
|
||||
ClientId=user_pool_client_id
|
||||
)
|
||||
|
||||
assert init_auth_result['ResponseMetadata']['HTTPStatusCode'] == 200, "Unsuccessful init_auth"
|
||||
print("Authenticated via username and password")
|
||||
|
||||
id_token = init_auth_result['AuthenticationResult']['IdToken']
|
||||
headers = {
|
||||
'Auth': id_token
|
||||
}
|
||||
results_request_url = get_rest_api_endpoint(REST_API_NAME, REGION, REST_API_STAGE, RESULTS_REQUEST_PATH)
|
||||
game_request_url = get_rest_api_endpoint(REST_API_NAME, REGION, REST_API_STAGE, GAME_REQUEST_PATH)
|
||||
|
||||
print ("results_request_url: " + results_request_url)
|
||||
print ("game_request_url: " + game_request_url)
|
||||
|
||||
results_request_response = requests.post(url=results_request_url, headers=headers)
|
||||
assert results_request_response.status_code == 204 or results_request_response.status_code == 200, \
|
||||
"Expect 'POST /get_game_info' status code to be 200 (Success) or 204 (No Content). Actual: " \
|
||||
f"{str(results_request_response.status_code)}"
|
||||
print("Verified mock ResultsRequest response", results_request_response)
|
||||
|
||||
game_request_response = requests.post(url=game_request_url, headers=headers, data=GAME_REQUEST_PAYLOAD)
|
||||
assert game_request_response.status_code == 202, "Expect 'POST /start_game' status code to be 202 (Accepted)/ Actual: " \
|
||||
f"{str(results_request_response.status_code)}"
|
||||
print("Verified lambda GameRequest response", game_request_response)
|
||||
|
||||
#game_request_info = json.loads(game_request_response.content)
|
||||
print(f"Received start game info: {game_request_response}")
|
||||
|
||||
print("Waiting for game session to be created...")
|
||||
time.sleep(10) # 10 seconds
|
||||
|
||||
results_request_response = requests.post(url=results_request_url, headers=headers)
|
||||
assert results_request_response.status_code == 200, "Expect 'POST /get_game_info' status code to be 200 (Success). Actual: " \
|
||||
f"{str(results_request_response.status_code)}"
|
||||
print("Verified lambda ResultsRequest response", results_request_response.content)
|
||||
|
||||
game_connection_info = json.loads(results_request_response.content)
|
||||
print(f"Received game connection info: {game_connection_info}")
|
||||
assert game_connection_info['IpAddress'] != ''
|
||||
assert game_connection_info['Port'] > 0
|
||||
assert REGION in game_connection_info['DnsName'], \
|
||||
f"Expect {game_connection_info['DnsName']} to contain '{REGION}'"
|
||||
assert "psess-" in game_connection_info['PlayerSessionId'], \
|
||||
f"Expect {game_connection_info['PlayerSessionId']} to contain 'psess-'"
|
||||
assert REGION in game_connection_info['GameSessionArn'], \
|
||||
f"Expect {game_connection_info['GameSessionArn']} to contain '{REGION}'"
|
||||
print("Verified game connection info:", game_connection_info)
|
||||
|
||||
finally:
|
||||
cognito_idp.admin_delete_user(
|
||||
UserPoolId=user_pool_id,
|
||||
Username=USERNAME,
|
||||
)
|
||||
|
||||
print("Deleted user:", USERNAME)
|
||||
|
||||
print("Test Succeeded!")
|
||||
|
||||
|
||||
def find_user_pool(user_pool_name):
|
||||
print("Finding user pool:", user_pool_name)
|
||||
result = cognito_idp.list_user_pools(MaxResults=50)
|
||||
pools = result['UserPools']
|
||||
return next(x for x in pools if x['Name'] == user_pool_name)
|
||||
|
||||
|
||||
def find_user_pool_client(user_pool_id, user_pool_client_name):
|
||||
print("Finding user pool client:", user_pool_client_name)
|
||||
results = cognito_idp.list_user_pool_clients(UserPoolId=user_pool_id)
|
||||
clients = results['UserPoolClients']
|
||||
return next(x for x in clients if x['ClientName'] == user_pool_client_name)
|
||||
|
||||
|
||||
def find_rest_api(rest_api_name):
|
||||
print("Finding rest api:", rest_api_name)
|
||||
results = apig.get_rest_apis()
|
||||
rest_apis = results['items']
|
||||
return next(x for x in rest_apis if x['name'] == rest_api_name)
|
||||
|
||||
|
||||
def get_rest_api_endpoint(rest_api_name, region, stage, path):
|
||||
print("Getting rest api endpoint", rest_api_name)
|
||||
rest_api = find_rest_api(rest_api_name)
|
||||
rest_api_id = rest_api['id']
|
||||
return f'https://{rest_api_id}.execute-api.{region}.amazonaws.com/{stage}/{path}'
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,864 @@
|
||||
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
||||
# SPDX-License-Identifier: MIT-0
|
||||
|
||||
AWSTemplateFormatVersion: "2010-09-09"
|
||||
|
||||
Description: >
|
||||
This CloudFormation template sets up a scenario to use FlexMatch -- a managed matchmaking service provided by
|
||||
GameLift. The template demonstrates best practices in acquiring the matchmaking ticket status, by listening to
|
||||
FlexMatch events in conjunction with a low frequency poller to ensure incomplete tickets are periodically pinged
|
||||
and therefore are not discarded by GameLift.
|
||||
|
||||
Parameters:
|
||||
ApiGatewayStageNameParameter:
|
||||
Type: String
|
||||
Default: v1
|
||||
Description: Name of the Api Gateway stage
|
||||
|
||||
BuildNameParameter:
|
||||
Type: String
|
||||
Default: Sample GameLift Build
|
||||
Description: Name of the build
|
||||
|
||||
BuildOperatingSystemParameter:
|
||||
Type: String
|
||||
Default: "WINDOWS_2016"
|
||||
Description: Operating system of the build
|
||||
|
||||
BuildServerSdkVersionParameter:
|
||||
Type: String
|
||||
Description: GameLift Server SDK version used in the server build
|
||||
|
||||
BuildS3BucketParameter:
|
||||
Type: String
|
||||
Description: Bucket that stores the server build
|
||||
|
||||
BuildS3KeyParameter:
|
||||
Type: String
|
||||
Description: Key of the server build in the S3 bucket
|
||||
|
||||
BuildVersionParameter:
|
||||
Type: String
|
||||
Description: Version number of the build
|
||||
|
||||
FleetDescriptionParameter:
|
||||
Type: String
|
||||
Default: Deployed by the Amazon GameLift Plug-in for Unreal.
|
||||
Description: Description of the fleet
|
||||
|
||||
FleetNameParameter:
|
||||
Type: String
|
||||
Default: Sample GameLift Fleet
|
||||
Description: Name of the fleet
|
||||
|
||||
FleetTcpFromPortParameter:
|
||||
Type: Number
|
||||
Default: 33430
|
||||
Description: Starting port number for TCP ports to be opened
|
||||
|
||||
FleetTcpToPortParameter:
|
||||
Type: Number
|
||||
Default: 33440
|
||||
Description: Ending port number for TCP ports to be opened
|
||||
|
||||
FleetUdpFromPortParameter:
|
||||
Type: Number
|
||||
Default: 33430
|
||||
Description: Starting port number for UDP ports to be opened
|
||||
|
||||
FleetUdpToPortParameter:
|
||||
Type: Number
|
||||
Default: 33440
|
||||
Description: Ending port number for UDP ports to be opened
|
||||
|
||||
GameNameParameter:
|
||||
Type: String
|
||||
Default: MyGame
|
||||
Description: Game name to prepend before resource names
|
||||
MaxLength: 30
|
||||
|
||||
LambdaZipS3BucketParameter:
|
||||
Type: String
|
||||
Description: S3 bucket that stores the lambda function zip
|
||||
|
||||
LambdaZipS3KeyParameter:
|
||||
Type: String
|
||||
Description: S3 key that stores the lambda function zip
|
||||
|
||||
LaunchParametersParameter:
|
||||
Type: String
|
||||
Description: Parameters used to launch the game server process
|
||||
|
||||
LaunchPathParameter:
|
||||
Type: String
|
||||
Description: Location of the game server executable in the build
|
||||
|
||||
MatchmakerTimeoutInSecondsParameter:
|
||||
Type: Number
|
||||
Default: 60
|
||||
Description: Time in seconds before matchmaker times out to place players on a server
|
||||
|
||||
MatchmakingTimeoutInSecondsParameter:
|
||||
Type: Number
|
||||
Default: 60
|
||||
Description: Time in seconds before matchmaker times out to wait for enough players to create game session placement
|
||||
|
||||
MaxTransactionsPerFiveMinutesPerIpParameter:
|
||||
Type: Number
|
||||
Default: 100
|
||||
MaxValue: 20000000
|
||||
MinValue: 100
|
||||
|
||||
NumPlayersPerGameParameter:
|
||||
Type: Number
|
||||
Default: 2
|
||||
Description: Number of players per game session
|
||||
|
||||
QueueTimeoutInSecondsParameter:
|
||||
Type: Number
|
||||
Default: 60
|
||||
Description: Time in seconds before game session placement times out to place players on a server
|
||||
|
||||
TeamNameParameter:
|
||||
Type: String
|
||||
Default: MySampleTeam
|
||||
Description: Team name used in matchmaking ruleset and StartMatchmaking API requests
|
||||
|
||||
TicketIdIndexNameParameter:
|
||||
Type: String
|
||||
Default: ticket-id-index
|
||||
Description: Name of the global secondary index on MatchmakingRequest table with partition key TicketId
|
||||
|
||||
UnrealEngineVersionParameter:
|
||||
Type: String
|
||||
Description: "Unreal Engine Version being used by the plugin"
|
||||
|
||||
EnableMetricsParameter:
|
||||
Type: String
|
||||
Default: "false"
|
||||
AllowedValues: ["true", "false"]
|
||||
Description: "Enable telemetry metrics collection using OTEL collector"
|
||||
|
||||
Conditions:
|
||||
ShouldCreateMetricsResources: !Equals [!Ref EnableMetricsParameter, "true"]
|
||||
|
||||
Resources:
|
||||
ApiGatewayCloudWatchRole:
|
||||
Type: "AWS::IAM::Role"
|
||||
Properties:
|
||||
AssumeRolePolicyDocument:
|
||||
Version: "2012-10-17"
|
||||
Statement:
|
||||
- Effect: Allow
|
||||
Principal:
|
||||
Service:
|
||||
- apigateway.amazonaws.com
|
||||
Action: "sts:AssumeRole"
|
||||
ManagedPolicyArns:
|
||||
- "arn:aws:iam::aws:policy/service-role/AmazonAPIGatewayPushToCloudWatchLogs"
|
||||
|
||||
Account:
|
||||
Type: "AWS::ApiGateway::Account"
|
||||
Properties:
|
||||
CloudWatchRoleArn: !GetAtt ApiGatewayCloudWatchRole.Arn
|
||||
|
||||
FlexMatchStatusPollerLambdaFunctionExecutionRole:
|
||||
Type: "AWS::IAM::Role"
|
||||
Properties:
|
||||
AssumeRolePolicyDocument:
|
||||
Version: "2012-10-17"
|
||||
Statement:
|
||||
- Effect: Allow
|
||||
Principal:
|
||||
Service:
|
||||
- lambda.amazonaws.com
|
||||
Action:
|
||||
- "sts:AssumeRole"
|
||||
ManagedPolicyArns:
|
||||
- "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
|
||||
Policies:
|
||||
- PolicyName: !Sub ${GameNameParameter}FlexMatchStatusPollerLambdaFunctionPolicies
|
||||
PolicyDocument:
|
||||
Version: "2012-10-17"
|
||||
Statement:
|
||||
- Effect: Allow
|
||||
Action:
|
||||
- "dynamodb:Scan"
|
||||
- "dynamodb:UpdateItem"
|
||||
- "gamelift:DescribeMatchmaking"
|
||||
Resource: "*"
|
||||
|
||||
GameRequestLambdaFunctionExecutionRole:
|
||||
Type: "AWS::IAM::Role"
|
||||
Properties:
|
||||
AssumeRolePolicyDocument:
|
||||
Version: "2012-10-17"
|
||||
Statement:
|
||||
- Effect: Allow
|
||||
Principal:
|
||||
Service:
|
||||
- lambda.amazonaws.com
|
||||
Action:
|
||||
- "sts:AssumeRole"
|
||||
ManagedPolicyArns:
|
||||
- "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
|
||||
Policies:
|
||||
- PolicyName: !Sub ${GameNameParameter}GameRequestLambdaFunctionPolicies
|
||||
PolicyDocument:
|
||||
Version: "2012-10-17"
|
||||
Statement:
|
||||
- Effect: Allow
|
||||
Action:
|
||||
- "dynamodb:PutItem"
|
||||
- "dynamodb:UpdateItem"
|
||||
- "dynamodb:GetItem"
|
||||
- "dynamodb:Query"
|
||||
- "gamelift:StartMatchmaking"
|
||||
Resource: "*"
|
||||
|
||||
MatchmakerEventHandlerLambdaFunctionExecutionRole:
|
||||
Type: "AWS::IAM::Role"
|
||||
Properties:
|
||||
AssumeRolePolicyDocument:
|
||||
Version: "2012-10-17"
|
||||
Statement:
|
||||
- Effect: Allow
|
||||
Principal:
|
||||
Service:
|
||||
- lambda.amazonaws.com
|
||||
Action:
|
||||
- "sts:AssumeRole"
|
||||
ManagedPolicyArns:
|
||||
- "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
|
||||
Policies:
|
||||
- PolicyName: !Sub ${GameNameParameter}MatchmakerEventHandlerLambdaFunctionPolicies
|
||||
PolicyDocument:
|
||||
Version: "2012-10-17"
|
||||
Statement:
|
||||
- Effect: Allow
|
||||
Action:
|
||||
- "dynamodb:Query"
|
||||
- "dynamodb:UpdateItem"
|
||||
Resource: "*"
|
||||
|
||||
RestApi:
|
||||
Type: "AWS::ApiGateway::RestApi"
|
||||
Properties:
|
||||
Name: !Sub ${GameNameParameter}RestApi
|
||||
|
||||
ResultsRequestLambdaFunctionExecutionRole:
|
||||
Type: "AWS::IAM::Role"
|
||||
Properties:
|
||||
AssumeRolePolicyDocument:
|
||||
Version: "2012-10-17"
|
||||
Statement:
|
||||
- Effect: Allow
|
||||
Principal:
|
||||
Service:
|
||||
- lambda.amazonaws.com
|
||||
Action:
|
||||
- "sts:AssumeRole"
|
||||
ManagedPolicyArns:
|
||||
- "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
|
||||
Policies:
|
||||
- PolicyName: !Sub ${GameNameParameter}ResultsRequestLambdaFunctionPolicies
|
||||
PolicyDocument:
|
||||
Version: "2012-10-17"
|
||||
Statement:
|
||||
- Effect: Allow
|
||||
Action:
|
||||
- "dynamodb:Query"
|
||||
Resource: "*"
|
||||
|
||||
MetricsInstanceRole:
|
||||
Type: "AWS::IAM::Role"
|
||||
Condition: ShouldCreateMetricsResources
|
||||
Properties:
|
||||
AssumeRolePolicyDocument:
|
||||
Version: '2012-10-17'
|
||||
Statement:
|
||||
- Effect: Allow
|
||||
Principal:
|
||||
Service: gamelift.amazonaws.com
|
||||
Action: sts:AssumeRole
|
||||
Policies:
|
||||
- PolicyName: AMPRemoteWriteAccess
|
||||
PolicyDocument:
|
||||
Version: '2012-10-17'
|
||||
Statement:
|
||||
- Effect: Allow
|
||||
Action:
|
||||
- aps:RemoteWrite
|
||||
Resource: !Sub 'arn:aws:aps:*:${AWS::AccountId}:workspace/*'
|
||||
- PolicyName: OTelCollectorEMFExporter
|
||||
PolicyDocument:
|
||||
Version: '2012-10-17'
|
||||
Statement:
|
||||
- Effect: Allow
|
||||
Action:
|
||||
- logs:PutLogEvents
|
||||
- logs:CreateLogStream
|
||||
- logs:CreateLogGroup
|
||||
- logs:PutRetentionPolicy
|
||||
Resource: !Sub 'arn:aws:logs:*:${AWS::AccountId}:log-group:*:log-stream:*'
|
||||
|
||||
UserPool:
|
||||
Type: "AWS::Cognito::UserPool"
|
||||
Properties:
|
||||
AdminCreateUserConfig:
|
||||
AllowAdminCreateUserOnly: false
|
||||
AutoVerifiedAttributes:
|
||||
- email
|
||||
EmailConfiguration:
|
||||
EmailSendingAccount: COGNITO_DEFAULT
|
||||
EmailVerificationMessage: "Please verify your email to complete account registration for the GameLift Plugin FlexMatch fleet deployment scenario. Confirmation Code {####}."
|
||||
EmailVerificationSubject: GameLift Plugin - Deployment Scenario Account Verification
|
||||
Policies:
|
||||
PasswordPolicy:
|
||||
MinimumLength: 8
|
||||
RequireLowercase: true
|
||||
RequireNumbers: true
|
||||
RequireSymbols: true
|
||||
RequireUppercase: true
|
||||
Schema:
|
||||
- Name: email
|
||||
AttributeDataType: String
|
||||
Mutable: false
|
||||
Required: true
|
||||
UserPoolName: !Sub ${GameNameParameter}UserPool
|
||||
UsernameAttributes:
|
||||
- email
|
||||
|
||||
BuildAccessRole:
|
||||
Type: "AWS::IAM::Role"
|
||||
Properties:
|
||||
AssumeRolePolicyDocument:
|
||||
Version: "2012-10-17"
|
||||
Statement:
|
||||
- Effect: Allow
|
||||
Principal:
|
||||
Service:
|
||||
- cloudformation.amazonaws.com
|
||||
- gamelift.amazonaws.com
|
||||
Action: "sts:AssumeRole"
|
||||
Policies:
|
||||
- PolicyName: !Sub ${GameNameParameter}BuildS3AccessPolicy
|
||||
PolicyDocument:
|
||||
Version: "2012-10-17"
|
||||
Statement:
|
||||
- Effect: Allow
|
||||
Action:
|
||||
- "s3:GetObject"
|
||||
- "s3:GetObjectVersion"
|
||||
Resource:
|
||||
- "Fn::Sub": "arn:aws:s3:::${BuildS3BucketParameter}/${BuildS3KeyParameter}"
|
||||
RoleName: !Sub ${GameNameParameter}BuildIAMRole
|
||||
|
||||
GameRequestApiResource:
|
||||
Type: "AWS::ApiGateway::Resource"
|
||||
Properties:
|
||||
ParentId: !GetAtt RestApi.RootResourceId
|
||||
PathPart: start_game
|
||||
RestApiId: !Ref RestApi
|
||||
|
||||
MatchmakingRequestTable:
|
||||
Type: "AWS::DynamoDB::Table"
|
||||
Properties:
|
||||
AttributeDefinitions:
|
||||
- AttributeName: PlayerId
|
||||
AttributeType: S
|
||||
- AttributeName: TicketId
|
||||
AttributeType: S
|
||||
- AttributeName: StartTime
|
||||
AttributeType: "N"
|
||||
GlobalSecondaryIndexes:
|
||||
- IndexName: !Ref TicketIdIndexNameParameter
|
||||
KeySchema:
|
||||
- AttributeName: TicketId
|
||||
KeyType: HASH
|
||||
Projection:
|
||||
ProjectionType: ALL
|
||||
ProvisionedThroughput:
|
||||
ReadCapacityUnits: 5
|
||||
WriteCapacityUnits: 5
|
||||
KeySchema:
|
||||
- AttributeName: PlayerId
|
||||
KeyType: HASH
|
||||
- AttributeName: StartTime
|
||||
KeyType: RANGE
|
||||
ProvisionedThroughput:
|
||||
ReadCapacityUnits: 5
|
||||
WriteCapacityUnits: 5
|
||||
TableName: !Sub ${GameNameParameter}MatchmakingRequestTable
|
||||
TimeToLiveSpecification:
|
||||
AttributeName: ExpirationTime
|
||||
Enabled: true
|
||||
|
||||
ResultsRequestApiResource:
|
||||
Type: "AWS::ApiGateway::Resource"
|
||||
Properties:
|
||||
ParentId: !GetAtt RestApi.RootResourceId
|
||||
PathPart: get_game_connection
|
||||
RestApiId: !Ref RestApi
|
||||
|
||||
UserPoolClient:
|
||||
Type: "AWS::Cognito::UserPoolClient"
|
||||
Properties:
|
||||
AccessTokenValidity: 1
|
||||
ClientName: !Sub ${GameNameParameter}UserPoolClient
|
||||
ExplicitAuthFlows:
|
||||
- ALLOW_USER_PASSWORD_AUTH
|
||||
- ALLOW_REFRESH_TOKEN_AUTH
|
||||
GenerateSecret: false
|
||||
IdTokenValidity: 1
|
||||
PreventUserExistenceErrors: ENABLED
|
||||
ReadAttributes:
|
||||
- email
|
||||
- preferred_username
|
||||
RefreshTokenValidity: 30
|
||||
SupportedIdentityProviders:
|
||||
- COGNITO
|
||||
UserPoolId: !Ref UserPool
|
||||
|
||||
WebACL:
|
||||
Type: "AWS::WAFv2::WebACL"
|
||||
DependsOn:
|
||||
- ApiDeployment
|
||||
Properties:
|
||||
DefaultAction:
|
||||
Allow:
|
||||
{}
|
||||
Description: !Sub "WebACL for game: ${GameNameParameter}"
|
||||
Name: !Sub ${GameNameParameter}WebACL
|
||||
Rules:
|
||||
- Name: !Sub ${GameNameParameter}WebACLPerIpThrottleRule
|
||||
Action:
|
||||
Block:
|
||||
{}
|
||||
Priority: 0
|
||||
Statement:
|
||||
RateBasedStatement:
|
||||
AggregateKeyType: IP
|
||||
Limit: !Ref MaxTransactionsPerFiveMinutesPerIpParameter
|
||||
VisibilityConfig:
|
||||
CloudWatchMetricsEnabled: true
|
||||
MetricName: !Sub ${GameNameParameter}WebACLPerIpThrottleRuleMetrics
|
||||
SampledRequestsEnabled: true
|
||||
Scope: REGIONAL
|
||||
VisibilityConfig:
|
||||
CloudWatchMetricsEnabled: true
|
||||
MetricName: !Sub ${GameNameParameter}WebACLMetrics
|
||||
SampledRequestsEnabled: true
|
||||
|
||||
ApiDeployment:
|
||||
Type: "AWS::ApiGateway::Deployment"
|
||||
DependsOn:
|
||||
- GameRequestApiMethod
|
||||
- ResultsRequestApiMethod
|
||||
Properties:
|
||||
RestApiId: !Ref RestApi
|
||||
StageDescription:
|
||||
DataTraceEnabled: true
|
||||
LoggingLevel: INFO
|
||||
MetricsEnabled: true
|
||||
StageName: !Ref ApiGatewayStageNameParameter
|
||||
|
||||
Authorizer:
|
||||
Type: "AWS::ApiGateway::Authorizer"
|
||||
Properties:
|
||||
IdentitySource: method.request.header.Auth
|
||||
Name: CognitoAuthorizer
|
||||
ProviderARNs:
|
||||
- "Fn::GetAtt":
|
||||
- UserPool
|
||||
- Arn
|
||||
RestApiId: !Ref RestApi
|
||||
Type: COGNITO_USER_POOLS
|
||||
|
||||
GameSessionQueue:
|
||||
Type: "AWS::GameLift::GameSessionQueue"
|
||||
Properties:
|
||||
Destinations:
|
||||
- DestinationArn: !Sub "arn:aws:gamelift:${AWS::Region}:${AWS::AccountId}:fleet/${OnDemandFleetResource}"
|
||||
- DestinationArn: !Sub "arn:aws:gamelift:${AWS::Region}:${AWS::AccountId}:fleet/${C5LargeSpotFleetResource}"
|
||||
- DestinationArn: !Sub "arn:aws:gamelift:${AWS::Region}:${AWS::AccountId}:fleet/${C4LargeSpotFleetResource}"
|
||||
Name: !Sub ${GameNameParameter}GameSessionQueue
|
||||
TimeoutInSeconds: !Ref QueueTimeoutInSecondsParameter
|
||||
|
||||
MatchmakingRuleSet:
|
||||
Type: "AWS::GameLift::MatchmakingRuleSet"
|
||||
Properties:
|
||||
Name: !Sub ${GameNameParameter}MatchmakingRuleSet
|
||||
RuleSetBody: !Sub
|
||||
- |-
|
||||
{
|
||||
"name": "MyMatchmakingRuleSet",
|
||||
"ruleLanguageVersion": "1.0",
|
||||
"teams": [{
|
||||
"name": "${teamName}",
|
||||
"minPlayers": ${minPlayers},
|
||||
"maxPlayers": ${maxPlayers}
|
||||
}]
|
||||
}
|
||||
- maxPlayers: !Ref NumPlayersPerGameParameter
|
||||
minPlayers: !Ref NumPlayersPerGameParameter
|
||||
teamName: !Ref TeamNameParameter
|
||||
|
||||
FlexMatchStatusPollerLambdaFunction:
|
||||
Type: "AWS::Lambda::Function"
|
||||
Properties:
|
||||
Code:
|
||||
S3Bucket: !Ref LambdaZipS3BucketParameter
|
||||
S3Key: !Ref LambdaZipS3KeyParameter
|
||||
Description: Lambda function to handle game requests
|
||||
Environment:
|
||||
Variables:
|
||||
MatchmakingRequestTableName: !Ref MatchmakingRequestTable
|
||||
FunctionName: !Sub ${GameNameParameter}FlexMatchStatusPollerLambda
|
||||
Handler: flexmatch_status_poller.handler
|
||||
MemorySize: 128
|
||||
Role: !GetAtt FlexMatchStatusPollerLambdaFunctionExecutionRole.Arn
|
||||
Runtime: python3.14
|
||||
|
||||
GameRequestApiMethod:
|
||||
Type: "AWS::ApiGateway::Method"
|
||||
Properties:
|
||||
AuthorizationType: COGNITO_USER_POOLS
|
||||
AuthorizerId: !Ref Authorizer
|
||||
HttpMethod: POST
|
||||
Integration:
|
||||
Type: AWS_PROXY
|
||||
IntegrationHttpMethod: POST
|
||||
Uri: !Sub "arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${GameRequestLambdaFunction.Arn}/invocations"
|
||||
OperationName: GameRequest
|
||||
ResourceId: !Ref GameRequestApiResource
|
||||
RestApiId: !Ref RestApi
|
||||
|
||||
MatchmakerEventHandlerLambdaFunction:
|
||||
Type: "AWS::Lambda::Function"
|
||||
Properties:
|
||||
Code:
|
||||
S3Bucket: !Ref LambdaZipS3BucketParameter
|
||||
S3Key: !Ref LambdaZipS3KeyParameter
|
||||
Description: Lambda function to handle game requests
|
||||
Environment:
|
||||
Variables:
|
||||
MatchmakingRequestTableName: !Ref MatchmakingRequestTable
|
||||
TicketIdIndexName: !Ref TicketIdIndexNameParameter
|
||||
FunctionName: !Sub ${GameNameParameter}MatchmakerEventHandlerLambda
|
||||
Handler: matchmaker_event_handler.handler
|
||||
MemorySize: 128
|
||||
Role: !GetAtt MatchmakerEventHandlerLambdaFunctionExecutionRole.Arn
|
||||
Runtime: python3.14
|
||||
|
||||
ResultsRequestApiMethod:
|
||||
Type: "AWS::ApiGateway::Method"
|
||||
Properties:
|
||||
AuthorizationType: COGNITO_USER_POOLS
|
||||
AuthorizerId: !Ref Authorizer
|
||||
HttpMethod: POST
|
||||
Integration:
|
||||
Type: AWS_PROXY
|
||||
IntegrationHttpMethod: POST
|
||||
Uri: !Sub "arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${ResultsRequestLambdaFunction.Arn}/invocations"
|
||||
OperationName: ResultsRequest
|
||||
ResourceId: !Ref ResultsRequestApiResource
|
||||
RestApiId: !Ref RestApi
|
||||
|
||||
ResultsRequestLambdaFunction:
|
||||
Type: "AWS::Lambda::Function"
|
||||
Properties:
|
||||
Code:
|
||||
S3Bucket: !Ref LambdaZipS3BucketParameter
|
||||
S3Key: !Ref LambdaZipS3KeyParameter
|
||||
Description: Lambda function to handle game requests
|
||||
Environment:
|
||||
Variables:
|
||||
MatchmakingRequestTableName: !Ref MatchmakingRequestTable
|
||||
FunctionName: !Sub ${GameNameParameter}ResultsRequestLambda
|
||||
Handler: results_request.handler
|
||||
MemorySize: 128
|
||||
Role: !GetAtt ResultsRequestLambdaFunctionExecutionRole.Arn
|
||||
Runtime: python3.14
|
||||
|
||||
WebACLAssociation:
|
||||
Type: "AWS::WAFv2::WebACLAssociation"
|
||||
DependsOn:
|
||||
- ApiDeployment
|
||||
- WebACL
|
||||
Properties:
|
||||
ResourceArn: !Sub
|
||||
- "arn:aws:apigateway:${REGION}::/restapis/${REST_API_ID}/stages/${STAGE_NAME}"
|
||||
- REGION: !Ref "AWS::Region"
|
||||
REST_API_ID: !Ref RestApi
|
||||
STAGE_NAME: !Ref ApiGatewayStageNameParameter
|
||||
WebACLArn: !GetAtt WebACL.Arn
|
||||
|
||||
FlexMatchStatusPollerScheduledRule:
|
||||
Type: "AWS::Events::Rule"
|
||||
Properties:
|
||||
Description: !Sub ${GameNameParameter}FlexMatchStatusPollerScheduledRule
|
||||
ScheduleExpression: rate(1 minute)
|
||||
State: ENABLED
|
||||
Targets:
|
||||
- Arn: !GetAtt FlexMatchStatusPollerLambdaFunction.Arn
|
||||
Id: !Sub ${GameNameParameter}FlexMatchStatusPollerScheduledRule
|
||||
|
||||
MatchmakerEventTopicKey:
|
||||
Type: "AWS::KMS::Key"
|
||||
Properties:
|
||||
Description: KMS key for FlexMatch SNS notifications
|
||||
KeyPolicy:
|
||||
Version: "2012-10-17"
|
||||
Statement:
|
||||
- Effect: Allow
|
||||
Principal:
|
||||
AWS: !Sub "arn:aws:iam::${AWS::AccountId}:root"
|
||||
Action: "kms:*"
|
||||
Resource: "*"
|
||||
- Effect: Allow
|
||||
Principal:
|
||||
Service: gamelift.amazonaws.com
|
||||
Action:
|
||||
- "kms:Decrypt"
|
||||
- "kms:GenerateDataKey"
|
||||
Resource: "*"
|
||||
|
||||
MatchmakerEventTopic:
|
||||
Type: "AWS::SNS::Topic"
|
||||
Properties:
|
||||
KmsMasterKeyId: !Ref MatchmakerEventTopicKey
|
||||
Subscription:
|
||||
- Endpoint: !GetAtt MatchmakerEventHandlerLambdaFunction.Arn
|
||||
Protocol: lambda
|
||||
TopicName: !Sub ${GameNameParameter}MatchmakerEventTopic
|
||||
|
||||
ServerBuild:
|
||||
Type: "AWS::GameLift::Build"
|
||||
Properties:
|
||||
Name: !Ref BuildNameParameter
|
||||
OperatingSystem: !Ref BuildOperatingSystemParameter
|
||||
ServerSdkVersion: !Ref BuildServerSdkVersionParameter
|
||||
StorageLocation:
|
||||
Bucket: !Ref BuildS3BucketParameter
|
||||
Key: !Ref BuildS3KeyParameter
|
||||
RoleArn: !GetAtt BuildAccessRole.Arn
|
||||
Version: !Ref BuildVersionParameter
|
||||
|
||||
FlexMatchStatusPollerLambdaPermission:
|
||||
Type: "AWS::Lambda::Permission"
|
||||
Properties:
|
||||
Action: "lambda:InvokeFunction"
|
||||
FunctionName: !Ref FlexMatchStatusPollerLambdaFunction
|
||||
Principal: events.amazonaws.com
|
||||
SourceArn: !GetAtt FlexMatchStatusPollerScheduledRule.Arn
|
||||
|
||||
MatchmakerEventHandlerLambdaPermission:
|
||||
Type: "AWS::Lambda::Permission"
|
||||
Properties:
|
||||
Action: "lambda:InvokeFunction"
|
||||
FunctionName: !Ref MatchmakerEventHandlerLambdaFunction
|
||||
Principal: sns.amazonaws.com
|
||||
SourceArn: !Ref MatchmakerEventTopic
|
||||
|
||||
MatchmakerEventTopicPolicy:
|
||||
Type: "AWS::SNS::TopicPolicy"
|
||||
DependsOn: MatchmakerEventTopic
|
||||
Properties:
|
||||
PolicyDocument:
|
||||
Version: "2012-10-17"
|
||||
Statement:
|
||||
- Effect: Allow
|
||||
Principal:
|
||||
Service: gamelift.amazonaws.com
|
||||
Action:
|
||||
- "sns:Publish"
|
||||
Resource: !Ref MatchmakerEventTopic
|
||||
Topics:
|
||||
- Ref: MatchmakerEventTopic
|
||||
|
||||
ResultsRequestLambdaFunctionApiGatewayPermission:
|
||||
Type: "AWS::Lambda::Permission"
|
||||
Properties:
|
||||
Action: "lambda:InvokeFunction"
|
||||
FunctionName: !GetAtt ResultsRequestLambdaFunction.Arn
|
||||
Principal: apigateway.amazonaws.com
|
||||
SourceArn: !Sub "arn:aws:execute-api:${AWS::Region}:${AWS::AccountId}:${RestApi}/*/*/*"
|
||||
|
||||
MatchmakingConfiguration:
|
||||
Type: "AWS::GameLift::MatchmakingConfiguration"
|
||||
DependsOn:
|
||||
- GameSessionQueue
|
||||
- MatchmakingRuleSet
|
||||
Properties:
|
||||
AcceptanceRequired: false
|
||||
BackfillMode: MANUAL
|
||||
Description: Matchmaking configuration for sample GameLift game
|
||||
FlexMatchMode: WITH_QUEUE
|
||||
GameSessionQueueArns:
|
||||
- "Fn::GetAtt":
|
||||
- GameSessionQueue
|
||||
- Arn
|
||||
Name: !Sub ${GameNameParameter}MatchmakingConfiguration
|
||||
NotificationTarget: !Ref MatchmakerEventTopic
|
||||
RequestTimeoutSeconds: !Ref MatchmakerTimeoutInSecondsParameter
|
||||
RuleSetName: !Ref MatchmakingRuleSet
|
||||
|
||||
C4LargeSpotFleetResource:
|
||||
Type: "AWS::GameLift::Fleet"
|
||||
Properties:
|
||||
BuildId: !Ref ServerBuild
|
||||
CertificateConfiguration:
|
||||
CertificateType: GENERATED
|
||||
Description: !Sub
|
||||
- "${FleetDescriptionParameter} Using Unreal Engine Version ${UnrealEngineVersionParameter}${MetricsText}"
|
||||
- MetricsText: !If [ShouldCreateMetricsResources, " with telemetry metrics enabled", ""]
|
||||
DesiredEC2Instances: 1
|
||||
EC2InboundPermissions:
|
||||
- FromPort: !Ref FleetTcpFromPortParameter
|
||||
IpRange: "0.0.0.0/0"
|
||||
Protocol: TCP
|
||||
ToPort: !Ref FleetTcpToPortParameter
|
||||
- FromPort: !Ref FleetUdpFromPortParameter
|
||||
IpRange: "0.0.0.0/0"
|
||||
Protocol: UDP
|
||||
ToPort: !Ref FleetUdpToPortParameter
|
||||
EC2InstanceType: c4.large
|
||||
FleetType: SPOT
|
||||
InstanceRoleARN: !If [ShouldCreateMetricsResources, !GetAtt MetricsInstanceRole.Arn, !Ref "AWS::NoValue"]
|
||||
InstanceRoleCredentialsProvider: !If [ShouldCreateMetricsResources, "SHARED_CREDENTIAL_FILE", !Ref "AWS::NoValue"]
|
||||
Locations:
|
||||
- Location: us-west-2
|
||||
- Location: us-east-1
|
||||
- Location: eu-west-1
|
||||
Name: !Ref FleetNameParameter
|
||||
NewGameSessionProtectionPolicy: FullProtection
|
||||
ResourceCreationLimitPolicy:
|
||||
NewGameSessionsPerCreator: 5
|
||||
PolicyPeriodInMinutes: 2
|
||||
RuntimeConfiguration:
|
||||
GameSessionActivationTimeoutSeconds: 300
|
||||
MaxConcurrentGameSessionActivations: 1
|
||||
ServerProcesses:
|
||||
- ConcurrentExecutions: 1
|
||||
LaunchPath: !Ref LaunchPathParameter
|
||||
Parameters: !Ref LaunchParametersParameter
|
||||
|
||||
C5LargeSpotFleetResource:
|
||||
Type: "AWS::GameLift::Fleet"
|
||||
Properties:
|
||||
BuildId: !Ref ServerBuild
|
||||
CertificateConfiguration:
|
||||
CertificateType: GENERATED
|
||||
Description: !Sub
|
||||
- "${FleetDescriptionParameter} Using Unreal Engine Version ${UnrealEngineVersionParameter}${MetricsText}"
|
||||
- MetricsText: !If [ShouldCreateMetricsResources, " with telemetry metrics enabled", ""]
|
||||
DesiredEC2Instances: 1
|
||||
EC2InboundPermissions:
|
||||
- FromPort: !Ref FleetTcpFromPortParameter
|
||||
IpRange: "0.0.0.0/0"
|
||||
Protocol: TCP
|
||||
ToPort: !Ref FleetTcpToPortParameter
|
||||
- FromPort: !Ref FleetUdpFromPortParameter
|
||||
IpRange: "0.0.0.0/0"
|
||||
Protocol: UDP
|
||||
ToPort: !Ref FleetUdpToPortParameter
|
||||
EC2InstanceType: c5.large
|
||||
FleetType: SPOT
|
||||
InstanceRoleARN: !If [ShouldCreateMetricsResources, !GetAtt MetricsInstanceRole.Arn, !Ref "AWS::NoValue"]
|
||||
InstanceRoleCredentialsProvider: !If [ShouldCreateMetricsResources, "SHARED_CREDENTIAL_FILE", !Ref "AWS::NoValue"]
|
||||
Locations:
|
||||
- Location: us-west-2
|
||||
- Location: us-east-1
|
||||
- Location: eu-west-1
|
||||
Name: !Ref FleetNameParameter
|
||||
NewGameSessionProtectionPolicy: FullProtection
|
||||
ResourceCreationLimitPolicy:
|
||||
NewGameSessionsPerCreator: 5
|
||||
PolicyPeriodInMinutes: 2
|
||||
RuntimeConfiguration:
|
||||
GameSessionActivationTimeoutSeconds: 300
|
||||
MaxConcurrentGameSessionActivations: 1
|
||||
ServerProcesses:
|
||||
- ConcurrentExecutions: 1
|
||||
LaunchPath: !Ref LaunchPathParameter
|
||||
Parameters: !Ref LaunchParametersParameter
|
||||
|
||||
OnDemandFleetResource:
|
||||
Type: "AWS::GameLift::Fleet"
|
||||
Properties:
|
||||
BuildId: !Ref ServerBuild
|
||||
CertificateConfiguration:
|
||||
CertificateType: GENERATED
|
||||
Description: !Sub
|
||||
- "${FleetDescriptionParameter} Using Unreal Engine Version ${UnrealEngineVersionParameter}${MetricsText}"
|
||||
- MetricsText: !If [ShouldCreateMetricsResources, " with telemetry metrics enabled", ""]
|
||||
DesiredEC2Instances: 1
|
||||
EC2InboundPermissions:
|
||||
- FromPort: !Ref FleetTcpFromPortParameter
|
||||
IpRange: "0.0.0.0/0"
|
||||
Protocol: TCP
|
||||
ToPort: !Ref FleetTcpToPortParameter
|
||||
- FromPort: !Ref FleetUdpFromPortParameter
|
||||
IpRange: "0.0.0.0/0"
|
||||
Protocol: UDP
|
||||
ToPort: !Ref FleetUdpToPortParameter
|
||||
EC2InstanceType: c5.large
|
||||
FleetType: ON_DEMAND
|
||||
InstanceRoleARN: !If [ShouldCreateMetricsResources, !GetAtt MetricsInstanceRole.Arn, !Ref "AWS::NoValue"]
|
||||
InstanceRoleCredentialsProvider: !If [ShouldCreateMetricsResources, "SHARED_CREDENTIAL_FILE", !Ref "AWS::NoValue"]
|
||||
Locations:
|
||||
- Location: us-west-2
|
||||
- Location: us-east-1
|
||||
- Location: eu-west-1
|
||||
Name: !Ref FleetNameParameter
|
||||
NewGameSessionProtectionPolicy: FullProtection
|
||||
ResourceCreationLimitPolicy:
|
||||
NewGameSessionsPerCreator: 5
|
||||
PolicyPeriodInMinutes: 2
|
||||
RuntimeConfiguration:
|
||||
GameSessionActivationTimeoutSeconds: 300
|
||||
MaxConcurrentGameSessionActivations: 1
|
||||
ServerProcesses:
|
||||
- ConcurrentExecutions: 1
|
||||
LaunchPath: !Ref LaunchPathParameter
|
||||
Parameters: !Ref LaunchParametersParameter
|
||||
|
||||
GameRequestLambdaFunction:
|
||||
Type: "AWS::Lambda::Function"
|
||||
Properties:
|
||||
Code:
|
||||
S3Bucket: !Ref LambdaZipS3BucketParameter
|
||||
S3Key: !Ref LambdaZipS3KeyParameter
|
||||
Description: Lambda function to handle game requests
|
||||
Environment:
|
||||
Variables:
|
||||
MatchmakingConfigurationName: !GetAtt MatchmakingConfiguration.Name
|
||||
MatchmakingRequestTableName: !Ref MatchmakingRequestTable
|
||||
TeamName: !Ref TeamNameParameter
|
||||
FunctionName: !Sub ${GameNameParameter}GameRequestLambda
|
||||
Handler: game_request.handler
|
||||
MemorySize: 128
|
||||
Role: !GetAtt GameRequestLambdaFunctionExecutionRole.Arn
|
||||
Runtime: python3.14
|
||||
|
||||
GameRequestLambdaFunctionApiGatewayPermission:
|
||||
Type: "AWS::Lambda::Permission"
|
||||
Properties:
|
||||
Action: "lambda:InvokeFunction"
|
||||
FunctionName: !GetAtt GameRequestLambdaFunction.Arn
|
||||
Principal: apigateway.amazonaws.com
|
||||
SourceArn: !Sub "arn:aws:execute-api:${AWS::Region}:${AWS::AccountId}:${RestApi}/*/*/*"
|
||||
|
||||
Outputs:
|
||||
ApiGatewayEndpoint:
|
||||
Description: Url of ApiGateway Endpoint
|
||||
Value: !Sub "https://${RestApi}.execute-api.${AWS::Region}.amazonaws.com/${ApiGatewayStageNameParameter}/"
|
||||
|
||||
UserPoolClientId:
|
||||
Description: Id of UserPoolClient
|
||||
Value: !Ref UserPoolClient
|
||||
|
||||
IdentityRegion:
|
||||
Description: Region name
|
||||
Value: !Ref "AWS::Region"
|
||||
@@ -0,0 +1,44 @@
|
||||
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
---
|
||||
# THIS IS A SAMPLE CLOUDFORMATION PARAMETERS FILE
|
||||
GameNameParameter:
|
||||
value: "{{AWSGAMELIFT::SYS::GAMENAME}}"
|
||||
LambdaZipS3BucketParameter:
|
||||
value: "{{AWSGAMELIFT::VARS::LambdaZipS3BucketParameter}}"
|
||||
LambdaZipS3KeyParameter:
|
||||
value: "{{AWSGAMELIFT::VARS::LambdaZipS3KeyParameter}}"
|
||||
ApiGatewayStageNameParameter:
|
||||
value: "{{AWSGAMELIFT::VARS::ApiGatewayStageNameParameter}}"
|
||||
BuildS3BucketParameter:
|
||||
value: "{{AWSGAMELIFT::VARS::BuildS3BucketParameter}}"
|
||||
BuildS3KeyParameter:
|
||||
value: "{{AWSGAMELIFT::VARS::BuildS3KeyParameter}}"
|
||||
LaunchPathParameter:
|
||||
value: "{{AWSGAMELIFT::VARS::LaunchPathParameter}}"
|
||||
LaunchParametersParameter:
|
||||
value: "-port=7777 -UNATTENDED LOG=server.log"
|
||||
BuildNameParameter:
|
||||
value: "Sample GameLift Build for {{AWSGAMELIFT::SYS::GAMENAME}}"
|
||||
BuildVersionParameter:
|
||||
value: "1"
|
||||
BuildOperatingSystemParameter:
|
||||
value: "{{AWSGAMELIFT::VARS::BuildOperatingSystemParameter}}"
|
||||
BuildServerSdkVersionParameter:
|
||||
value: "5.4.0"
|
||||
FleetTcpFromPortParameter:
|
||||
value: "7770"
|
||||
FleetTcpToPortParameter:
|
||||
value: "7780"
|
||||
FleetUdpFromPortParameter:
|
||||
value: "7770"
|
||||
FleetUdpToPortParameter:
|
||||
value: "7780"
|
||||
FleetNameParameter:
|
||||
value: "FlexMatch Fleet"
|
||||
UnrealEngineVersionParameter:
|
||||
value: "{{AWSGAMELIFT::VARS::UnrealEngineVersionParameter}}"
|
||||
EnableMetricsParameter:
|
||||
value: "{{AWSGAMELIFT::VARS::EnableMetricsParameter}}"
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
---
|
||||
# Key/value pairs to be added to the game's awsGameLiftClientConfig.yml file
|
||||
# These values will be replaced at the end of create/update of
|
||||
# the feature's CloudFormation stack.
|
||||
|
||||
user_pool_client_id: "{{AWSGAMELIFT::CFNOUTPUT::UserPoolClientId}}"
|
||||
identity_api_gateway_base_url: "{{AWSGAMELIFT::CFNOUTPUT::ApiGatewayEndpoint}}"
|
||||
identity_region: "{{AWSGAMELIFT::CFNOUTPUT::IdentityRegion}}"
|
||||
@@ -0,0 +1,145 @@
|
||||
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import boto3
|
||||
from boto3.dynamodb.conditions import Attr, And
|
||||
from botocore.exceptions import ClientError
|
||||
import os
|
||||
import time
|
||||
|
||||
NON_TERMINAL_REQUEST_QUERY_LIMIT = 50
|
||||
MATCHMAKING_STARTED_STATUS = 'MatchmakingStarted'
|
||||
MATCHMAKING_SUCCEEDED_STATUS = 'MatchmakingSucceeded'
|
||||
MATCHMAKING_TIMED_OUT_STATUS = 'MatchmakingTimedOut'
|
||||
MATCHMAKING_CANCELLED_STATUS = 'MatchmakingCancelled'
|
||||
MATCHMAKING_FAILED_STATUS = 'MatchmakingFailed'
|
||||
MAX_PARTITION_SIZE = 10
|
||||
MIN_TIME_ELAPSED_BEFORE_UPDATE_IN_SECONDS = 30
|
||||
|
||||
|
||||
def handler(event, context):
|
||||
"""
|
||||
Finds all non-terminal matchmaking requests and poll for their status. It is recommended by GameLift
|
||||
to regularly poll for ticket status at a low rate.
|
||||
See: https://docs.aws.amazon.com/gamelift/latest/flexmatchguide/match-client.html#match-client-track
|
||||
|
||||
:param event: lambda event, not used by this function
|
||||
:param context: lambda context, not used by this function
|
||||
:return: None
|
||||
"""
|
||||
lambda_start_time = round(time.time())
|
||||
|
||||
print(f"Polling non-terminal matchmaking tickets. Lambda start time: {lambda_start_time}")
|
||||
|
||||
matchmaking_request_table_name = os.environ['MatchmakingRequestTableName']
|
||||
|
||||
dynamodb = boto3.resource('dynamodb')
|
||||
matchmaking_request_table = dynamodb.Table(matchmaking_request_table_name)
|
||||
|
||||
gamelift = boto3.client('gamelift')
|
||||
|
||||
matchmaking_requests = matchmaking_request_table.scan(
|
||||
Limit=NON_TERMINAL_REQUEST_QUERY_LIMIT,
|
||||
FilterExpression=And(Attr('TicketStatus').eq(MATCHMAKING_STARTED_STATUS),
|
||||
Attr('LastUpdatedTime').lt(lambda_start_time - MIN_TIME_ELAPSED_BEFORE_UPDATE_IN_SECONDS))
|
||||
)
|
||||
|
||||
if matchmaking_requests['Count'] <= 0:
|
||||
print("No non-terminal matchmaking requests found")
|
||||
|
||||
for matchmaking_requests in partition(matchmaking_requests['Items'], MAX_PARTITION_SIZE):
|
||||
ticket_id_to_request_mapping = {request['TicketId']: request for request in matchmaking_requests}
|
||||
describe_matchmaking_result = gamelift.describe_matchmaking(
|
||||
TicketIds=list(ticket_id_to_request_mapping.keys())
|
||||
)
|
||||
ticket_list = describe_matchmaking_result['TicketList']
|
||||
if len(ticket_list) != len(matchmaking_requests):
|
||||
print(f"Resulting TicketList length: {len(ticket_list)} from DescribeMatchmaking "
|
||||
f"does not match the request size: {len(matchmaking_requests)}")
|
||||
for ticket in ticket_list:
|
||||
ticket_id = ticket['TicketId']
|
||||
ticket_status = ticket['Status']
|
||||
matchmaking_request_status = to_matchmaking_request_status(ticket_status)
|
||||
matchmaking_request = ticket_id_to_request_mapping[ticket_id]
|
||||
player_id = matchmaking_request['PlayerId']
|
||||
start_time = matchmaking_request['StartTime']
|
||||
last_updated_time = matchmaking_request['LastUpdatedTime']
|
||||
try:
|
||||
attribute_updates = {
|
||||
'LastUpdatedTime': {
|
||||
'Value': lambda_start_time
|
||||
}
|
||||
}
|
||||
if ticket_status in ['COMPLETED', 'FAILED', 'TIMED_OUT', 'CANCELLED']:
|
||||
print(f'Ticket: {ticket_id} status was updated to {ticket_status}')
|
||||
attribute_updates.update({
|
||||
'TicketStatus': {
|
||||
'Value': matchmaking_request_status
|
||||
}
|
||||
})
|
||||
if ticket_status == 'COMPLETED':
|
||||
# parse the playerSessionId
|
||||
matched_player_sessions = ticket.get('GameSessionConnectionInfo', {}).get('MatchedPlayerSessions')
|
||||
player_session_id = None
|
||||
if matched_player_sessions is not None and len(matched_player_sessions) == 1:
|
||||
player_session_id = matched_player_sessions[0].get('PlayerSessionId')
|
||||
|
||||
attribute_updates.update({
|
||||
'IpAddress': {
|
||||
'Value': ticket.get('GameSessionConnectionInfo', {}).get('IpAddress')
|
||||
},
|
||||
'DnsName': {
|
||||
'Value': ticket.get('GameSessionConnectionInfo', {}).get('DnsName')
|
||||
},
|
||||
'Port': {
|
||||
'Value': str(ticket.get('GameSessionConnectionInfo', {}).get('Port'))
|
||||
},
|
||||
'GameSessionArn': {
|
||||
'Value': str(ticket.get('GameSessionConnectionInfo', {}).get('GameSessionArn'))
|
||||
},
|
||||
'PlayerSessionId': {
|
||||
'Value' : str(player_session_id)
|
||||
}
|
||||
})
|
||||
else:
|
||||
print(f'No updates to ticket: {ticket_id} compared to '
|
||||
f'{lambda_start_time - last_updated_time} seconds ago')
|
||||
|
||||
matchmaking_request_table.update_item(
|
||||
Key={
|
||||
'PlayerId': player_id,
|
||||
'StartTime': start_time
|
||||
},
|
||||
AttributeUpdates=attribute_updates,
|
||||
Expected={
|
||||
'TicketStatus': {
|
||||
'Value': MATCHMAKING_STARTED_STATUS,
|
||||
'ComparisonOperator': 'EQ'
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
except ClientError as e:
|
||||
error_code = e.response['Error']['Code']
|
||||
if error_code == 'ConditionCheckFailedException':
|
||||
print(f"Ticket: {ticket_id} status has been updated (likely by MatchMakerEventHandler). "
|
||||
f"No change is made")
|
||||
continue
|
||||
raise e
|
||||
|
||||
|
||||
def partition(collection, n):
|
||||
"""Yield successive n-sized partitions from collection."""
|
||||
for i in range(0, len(collection), n):
|
||||
yield collection[i:i + n]
|
||||
|
||||
|
||||
def to_matchmaking_request_status(ticket_status):
|
||||
if ticket_status == 'COMPLETED':
|
||||
return MATCHMAKING_SUCCEEDED_STATUS
|
||||
if ticket_status == 'FAILED':
|
||||
return MATCHMAKING_FAILED_STATUS
|
||||
if ticket_status == 'TIMED_OUT':
|
||||
return MATCHMAKING_TIMED_OUT_STATUS
|
||||
if ticket_status == 'CANCELLED':
|
||||
return MATCHMAKING_CANCELLED_STATUS
|
||||
@@ -0,0 +1,133 @@
|
||||
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import boto3
|
||||
from boto3.dynamodb.conditions import Key
|
||||
import time
|
||||
import os
|
||||
import json
|
||||
|
||||
DEFAULT_TTL_IN_SECONDS = 10 * 60 # 10 minutes
|
||||
MATCHMAKING_STARTED_STATUS = 'MatchmakingStarted'
|
||||
MATCHMAKING_SUCCEEDED_STATUS = 'MatchmakingSucceeded'
|
||||
MATCHMAKING_TIMED_OUT_STATUS = 'MatchmakingTimedOut'
|
||||
MATCHMAKING_CANCELLED_STATUS = 'MatchmakingCancelled'
|
||||
MATCHMAKING_FAILED_STATUS = 'MatchmakingFailed'
|
||||
|
||||
|
||||
def handler(event, context):
|
||||
"""
|
||||
Handles requests to start games from the game client.
|
||||
This function records the game request from the client in the MatchmakingRequest table and calls
|
||||
GameLift to start matchmaking.
|
||||
:param event: lambda event, contains the region to player latency mapping in `regionToLatencyMapping` key, as well
|
||||
as the player information from the Cognito id tokens.
|
||||
:param context: lambda context, not used by this function
|
||||
:return:
|
||||
- 202 (Accepted) if the matchmaking request is accepted and is now being processed
|
||||
- 409 (Conflict) if the another matchmaking request is in progress
|
||||
- 500 (Internal Error) if error occurred when calling GameLift to start matchmaking
|
||||
"""
|
||||
player_id = event["requestContext"]["authorizer"]["claims"]["sub"]
|
||||
start_time = round(time.time())
|
||||
print(f'Handling start game request. PlayerId: {player_id}, StartTime: {start_time}')
|
||||
|
||||
region_to_latency_mapping = get_region_to_latency_mapping(event)
|
||||
if region_to_latency_mapping:
|
||||
print(f"Region to latency mapping: {region_to_latency_mapping}")
|
||||
else:
|
||||
print("No regionToLatencyMapping mapping provided")
|
||||
|
||||
matchmaking_request_table_name = os.environ['MatchmakingRequestTableName']
|
||||
team_name = os.environ['TeamName']
|
||||
matchmaking_configuration_name = os.environ['MatchmakingConfigurationName']
|
||||
|
||||
dynamodb = boto3.resource('dynamodb')
|
||||
matchmaking_request_table = dynamodb.Table(matchmaking_request_table_name)
|
||||
|
||||
gamelift = boto3.client('gamelift')
|
||||
|
||||
matchmaking_requests = matchmaking_request_table.query(
|
||||
KeyConditionExpression=Key('PlayerId').eq(player_id),
|
||||
ScanIndexForward=False
|
||||
)
|
||||
|
||||
if matchmaking_requests['Count'] > 0 \
|
||||
and not is_matchmaking_request_terminal(matchmaking_requests['Items'][0]):
|
||||
# A existing matchmaking request in progress
|
||||
return {
|
||||
'headers': {
|
||||
'Content-Type': 'text/plain'
|
||||
},
|
||||
'statusCode': 409 # Conflict
|
||||
}
|
||||
|
||||
try:
|
||||
player = {
|
||||
'PlayerId': player_id,
|
||||
'Team': team_name
|
||||
}
|
||||
if region_to_latency_mapping:
|
||||
player['LatencyInMs'] = region_to_latency_mapping
|
||||
|
||||
start_matchmaking_request = {
|
||||
"ConfigurationName": matchmaking_configuration_name,
|
||||
"Players": [player]
|
||||
}
|
||||
print(f"Starting matchmaking in GameLift. Request: {start_matchmaking_request}")
|
||||
start_matchmaking_result = gamelift.start_matchmaking(**start_matchmaking_request)
|
||||
|
||||
ticket_id = start_matchmaking_result['MatchmakingTicket']['TicketId']
|
||||
ticket_status = MATCHMAKING_STARTED_STATUS
|
||||
|
||||
matchmaking_request_table.put_item(
|
||||
Item={
|
||||
'PlayerId': player_id,
|
||||
'StartTime': start_time,
|
||||
'LastUpdatedTime': start_time,
|
||||
'ExpirationTime': start_time + DEFAULT_TTL_IN_SECONDS,
|
||||
'TicketStatus': ticket_status,
|
||||
'TicketId': ticket_id
|
||||
}
|
||||
)
|
||||
|
||||
return {
|
||||
# Matchmaking request enqueued
|
||||
'headers': {
|
||||
'Content-Type': 'text/plain'
|
||||
},
|
||||
'statusCode': 202
|
||||
}
|
||||
except Exception as ex:
|
||||
print(f'Error occurred when calling GameLift to start matchmaking. Exception: {ex}')
|
||||
return {
|
||||
# Error occurred when enqueuing matchmaking request
|
||||
'headers': {
|
||||
'Content-Type': 'text/plain'
|
||||
},
|
||||
'statusCode': 500
|
||||
}
|
||||
|
||||
|
||||
def is_matchmaking_request_terminal(matchmaking_request):
|
||||
return matchmaking_request['TicketStatus'] in [
|
||||
MATCHMAKING_SUCCEEDED_STATUS,
|
||||
MATCHMAKING_TIMED_OUT_STATUS,
|
||||
MATCHMAKING_CANCELLED_STATUS,
|
||||
MATCHMAKING_FAILED_STATUS
|
||||
]
|
||||
|
||||
|
||||
def get_region_to_latency_mapping(event):
|
||||
request_body = event.get("body")
|
||||
if not request_body:
|
||||
return None
|
||||
|
||||
try:
|
||||
request_body_json = json.loads(request_body)
|
||||
except ValueError:
|
||||
print(f"Error parsing request body: {request_body}")
|
||||
return None
|
||||
|
||||
if request_body_json and request_body_json.get('regionToLatencyMapping'):
|
||||
return request_body_json.get('regionToLatencyMapping')
|
||||
@@ -0,0 +1,107 @@
|
||||
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import boto3
|
||||
from boto3.dynamodb.conditions import Key
|
||||
import os
|
||||
import json
|
||||
import time
|
||||
|
||||
MATCHMAKING_STARTED_STATUS = 'MatchmakingStarted'
|
||||
MATCHMAKING_SUCCEEDED_STATUS = 'MatchmakingSucceeded'
|
||||
MATCHMAKING_TIMED_OUT_STATUS = 'MatchmakingTimedOut'
|
||||
MATCHMAKING_CANCELLED_STATUS = 'MatchmakingCancelled'
|
||||
MATCHMAKING_FAILED_STATUS = 'MatchmakingFailed'
|
||||
|
||||
|
||||
def handler(event, context):
|
||||
"""
|
||||
Handles game session event from GameLift FlexMatch. This function parses the event messages and updates all
|
||||
related requests in the the MatchmakingPlacement DynamoDB table,
|
||||
which will be looked up to fulfill game client's Results Requests.
|
||||
:param event: lambda event containing game session event from GameLift queue
|
||||
:param context: lambda context, not used by this function
|
||||
:return: None
|
||||
"""
|
||||
lambda_start_time = round(time.time())
|
||||
message = json.loads(event['Records'][0]['Sns']['Message'])
|
||||
print(f'Handling FlexMatch event. StartTime: {lambda_start_time}. Message: {message}')
|
||||
|
||||
status_type = message['detail']['type']
|
||||
|
||||
if status_type not in [MATCHMAKING_SUCCEEDED_STATUS, MATCHMAKING_TIMED_OUT_STATUS, MATCHMAKING_CANCELLED_STATUS,
|
||||
MATCHMAKING_FAILED_STATUS]:
|
||||
print(f'Received non-terminal status type: {status_type}. Skip processing.')
|
||||
return
|
||||
|
||||
tickets = message['detail']['tickets']
|
||||
ip_address = message['detail']['gameSessionInfo'].get('ipAddress')
|
||||
dns_name = message['detail']['gameSessionInfo'].get('dnsName')
|
||||
port = str(message['detail']['gameSessionInfo'].get('port'))
|
||||
game_session_arn = str(message['detail']['gameSessionInfo'].get('gameSessionArn'))
|
||||
players = message['detail']['gameSessionInfo']['players']
|
||||
players_map = {player.get('playerId'):player.get('playerSessionId') for player in players}
|
||||
|
||||
ticket_id_index_name = os.environ['TicketIdIndexName']
|
||||
|
||||
matchmaking_request_table_name = os.environ['MatchmakingRequestTableName']
|
||||
dynamodb = boto3.resource('dynamodb')
|
||||
matchmaking_request_table = dynamodb.Table(matchmaking_request_table_name)
|
||||
|
||||
for ticket in tickets:
|
||||
ticket_id = ticket['ticketId']
|
||||
matchmaking_requests = matchmaking_request_table.query(
|
||||
IndexName=ticket_id_index_name,
|
||||
KeyConditionExpression=Key('TicketId').eq(ticket_id)
|
||||
)
|
||||
|
||||
if matchmaking_requests['Count'] <= 0:
|
||||
print(f"Cannot find matchmaking request with ticket id: {ticket_id}")
|
||||
continue
|
||||
|
||||
matchmaking_request_status = matchmaking_requests['Items'][0]['TicketStatus']
|
||||
player_id = matchmaking_requests['Items'][0]['PlayerId']
|
||||
player_session_id = players_map.get(player_id)
|
||||
print(f'Processing Ticket: {ticket_id}, PlayerId: {player_id}, PlayerSessionId: {player_session_id}')
|
||||
matchmaking_request_start_time = matchmaking_requests['Items'][0]['StartTime']
|
||||
|
||||
if matchmaking_request_status != MATCHMAKING_STARTED_STATUS:
|
||||
print(f"Unexpected TicketStatus on matchmaking request. Expected: 'MatchmakingStarted'. "
|
||||
f"Found: {matchmaking_request_status}")
|
||||
continue
|
||||
|
||||
attribute_updates = {
|
||||
'TicketStatus': {
|
||||
'Value': status_type
|
||||
},
|
||||
'LastUpdatedTime': {
|
||||
'Value': lambda_start_time
|
||||
}
|
||||
}
|
||||
|
||||
if status_type == MATCHMAKING_SUCCEEDED_STATUS:
|
||||
attribute_updates.update({
|
||||
'IpAddress': {
|
||||
'Value': ip_address
|
||||
},
|
||||
'DnsName': {
|
||||
'Value': dns_name
|
||||
},
|
||||
'Port': {
|
||||
'Value': port
|
||||
},
|
||||
'GameSessionArn': {
|
||||
'Value': game_session_arn
|
||||
},
|
||||
'PlayerSessionId': {
|
||||
'Value': player_session_id
|
||||
}
|
||||
})
|
||||
|
||||
matchmaking_request_table.update_item(
|
||||
Key={
|
||||
'PlayerId': player_id,
|
||||
'StartTime': matchmaking_request_start_time
|
||||
},
|
||||
AttributeUpdates=attribute_updates
|
||||
)
|
||||
@@ -0,0 +1,84 @@
|
||||
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import boto3
|
||||
import os
|
||||
import json
|
||||
from boto3.dynamodb.conditions import Key
|
||||
|
||||
MATCHMAKING_STARTED_STATUS = 'MatchmakingStarted'
|
||||
MATCHMAKING_SUCCEEDED_STATUS = 'MatchmakingSucceeded'
|
||||
|
||||
|
||||
def handler(event, context):
|
||||
"""
|
||||
Handles requests to describe the game session connection information after a StartGame request.
|
||||
This function will look up the MatchmakingRequest table to find the latest matchmaking request
|
||||
by the player, and return its game connection information if any.
|
||||
:param event: lambda event, contains the region to player latency mapping in `regionToLatencyMapping` key, as well
|
||||
as the player information from the Cognito id tokens. Cognito provides a `sub` user attribute that we use as a
|
||||
Player ID. Unlike `username` value `sub` is UUID for a user which is never reassigned to another user.
|
||||
:param context: lambda context, not used by this function
|
||||
:return:
|
||||
- 200 (OK) if the game connection is ready, along with server info: "IpAddress", "Port", "DnsName", "PlayerSessionId", "PlayerId", "PlayerSessionId", "PlayerId"
|
||||
- 204 (No Content) if the requested game is still in progress of matchmaking
|
||||
- 404 (Not Found) if no game has been started by the player, or if all started game were expired
|
||||
- 500 (Internal Error) if errors occurred during matchmaking or placement
|
||||
"""
|
||||
player_id = event["requestContext"]["authorizer"]["claims"]["sub"]
|
||||
print(f'Handling request result request. PlayerId: {player_id}')
|
||||
|
||||
matchmaking_request_table_name = os.environ['MatchmakingRequestTableName']
|
||||
|
||||
dynamodb = boto3.resource('dynamodb')
|
||||
matchmaking_request_table = dynamodb.Table(matchmaking_request_table_name)
|
||||
|
||||
matchmaking_requests = matchmaking_request_table.query(
|
||||
KeyConditionExpression=Key('PlayerId').eq(player_id),
|
||||
ScanIndexForward=False
|
||||
)
|
||||
|
||||
if matchmaking_requests['Count'] <= 0:
|
||||
return {
|
||||
'headers': {
|
||||
'Content-Type': 'text/plain'
|
||||
},
|
||||
'statusCode': 404
|
||||
}
|
||||
|
||||
latest_matchmaking_request = matchmaking_requests['Items'][0]
|
||||
|
||||
print(f'Current Matchmaking Request: {latest_matchmaking_request}')
|
||||
|
||||
matchmaking_request_status = latest_matchmaking_request['TicketStatus']
|
||||
|
||||
if matchmaking_request_status == MATCHMAKING_STARTED_STATUS:
|
||||
# still waiting for ticket to be processed
|
||||
return {
|
||||
'headers': {
|
||||
'Content-Type': 'text/plain'
|
||||
},
|
||||
'statusCode': 204
|
||||
}
|
||||
elif matchmaking_request_status == MATCHMAKING_SUCCEEDED_STATUS:
|
||||
game_session_connection_info = \
|
||||
dict((k, latest_matchmaking_request[k]) for k in ('IpAddress', 'Port', 'DnsName', 'PlayerSessionId', 'PlayerId', 'GameSessionArn'))
|
||||
print(f"Connection info: {game_session_connection_info}")
|
||||
|
||||
return {
|
||||
'body': json.dumps(game_session_connection_info),
|
||||
'headers': {
|
||||
'Content-Type': 'text/plain'
|
||||
},
|
||||
'statusCode': 200
|
||||
}
|
||||
else:
|
||||
# We count MatchmakingCancelled as internal error also because cancelling placement requests is not
|
||||
# in the current implementation, so it should never happen.
|
||||
print(f'Received non-successful terminal status {matchmaking_request_status}, responding with 500 error.')
|
||||
return {
|
||||
'headers': {
|
||||
'Content-Type': 'text/plain'
|
||||
},
|
||||
'statusCode': 500
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
#
|
||||
# GameLift Region Mappings
|
||||
# Keep synchronized with: https://docs.aws.amazon.com/general/latest/gr/gamelift.html
|
||||
#
|
||||
# ----------------------------------------------------------------------------------------
|
||||
# Region Name Region Endpoint Protocol
|
||||
# ----------------------------------------------------------------------------------------
|
||||
# US East (Ohio) us-east-2 gamelift.us-east-2.amazonaws.com HTTPS
|
||||
# US East (N. Virginia) us-east-1 gamelift.us-east-1.amazonaws.com HTTPS
|
||||
# US West (N. California) us-west-1 gamelift.us-west-1.amazonaws.com HTTPS
|
||||
# US West (Oregon) us-west-2 gamelift.us-west-2.amazonaws.com HTTPS
|
||||
# Asia Pacific (Mumbai) ap-south-1 gamelift.ap-south-1.amazonaws.com HTTPS
|
||||
# Asia Pacific (Seoul) ap-northeast-2 gamelift.ap-northeast-2.amazonaws.com HTTPS
|
||||
# Asia Pacific (Singapore) ap-southeast-1 gamelift.ap-southeast-1.amazonaws.com HTTPS
|
||||
# Asia Pacific (Sydney) ap-southeast-2 gamelift.ap-southeast-2.amazonaws.com HTTPS
|
||||
# Asia Pacific (Tokyo) ap-northeast-1 gamelift.ap-northeast-1.amazonaws.com HTTPS
|
||||
# Canada (Central) ca-central-1 gamelift.ca-central-1.amazonaws.com HTTPS
|
||||
# Europe (Frankfurt) eu-central-1 gamelift.eu-central-1.amazonaws.com HTTPS
|
||||
# Europe (Ireland) eu-west-1 gamelift.eu-west-1.amazonaws.com HTTPS
|
||||
# Europe (London) eu-west-2 gamelift.eu-west-2.amazonaws.com HTTPS
|
||||
# South America (São Paulo) sa-east-1 gamelift.sa-east-1.amazonaws.com HTTPS
|
||||
# ----------------------------------------------------------------------------------------
|
||||
|
||||
{
|
||||
five_letter_region_codes: {
|
||||
us-east-2 : usea2,
|
||||
us-east-1 : usea1,
|
||||
us-west-1 : uswe1,
|
||||
us-west-2 : uswe2,
|
||||
ap-south-1 : apso1,
|
||||
ap-northeast-2 : apne2,
|
||||
ap-southeast-1 : apse1,
|
||||
ap-southeast-2 : apse2,
|
||||
ap-northeast-1 : apne1,
|
||||
ca-central-1 : cace1,
|
||||
eu-central-1 : euce1,
|
||||
eu-west-1 : euwe1,
|
||||
eu-west-2 : euwe2,
|
||||
sa-east-1 : saea1
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,217 @@
|
||||
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import string
|
||||
import boto3
|
||||
import requests
|
||||
import json
|
||||
import time
|
||||
import argparse
|
||||
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("-g", "--game", help="game name", type=str, required=True)
|
||||
parser.add_argument("-r", "--region", help="region name, e.g. eu-west-1", type=str, required=True)
|
||||
parser.add_argument("-p", "--profile", help="profile name in the AWS shared credentials file ~/.aws/credentials", type=str, required=True)
|
||||
args = parser.parse_args()
|
||||
|
||||
GAME_NAME = args.game.lower() # e.g. 'GameLiftSampleGame5ue4'
|
||||
REGION = args.region # e.g. 'eu-west-1'
|
||||
PROFILE_NAME = args.profile
|
||||
|
||||
USER_POOL_NAME = GAME_NAME + 'UserPool'
|
||||
USER_POOL_CLIENT_NAME = GAME_NAME + 'UserPoolClient'
|
||||
USERNAME1 = 'testuser1@example.com'
|
||||
USERNAME2 = 'testuser2@example.com'
|
||||
USERNAMES = [USERNAME1, USERNAME2]
|
||||
PASSWORD = 'TestPassw0rd.'
|
||||
REST_API_NAME = GAME_NAME + 'RestApi'
|
||||
REST_API_STAGE = 'dev'
|
||||
GAME_REQUEST_PATH = 'start_game'
|
||||
RESULTS_REQUEST_PATH = 'get_game_connection'
|
||||
session = boto3.Session(profile_name=PROFILE_NAME)
|
||||
cognito_idp = session.client('cognito-idp', region_name=REGION)
|
||||
apig = session.client('apigateway', region_name=REGION)
|
||||
REGION_US_WEST_2 = 'us-west-2'
|
||||
REGION_EU_WEST_1 = 'eu-west-1'
|
||||
REGION_US_EAST_1 = 'us-east-1'
|
||||
NO_LATENCY = 'no-latency'
|
||||
REGIONS_TO_TEST = [REGION_US_WEST_2, REGION_EU_WEST_1, NO_LATENCY]
|
||||
US_WEST_2_FIRST_REGION_TO_LATENCY_MAPPING = {
|
||||
"regionToLatencyMapping": {
|
||||
"us-west-2": 50,
|
||||
"us-east-1": 100,
|
||||
"eu-west-1": 150,
|
||||
"ap-northeast-1": 300
|
||||
}
|
||||
}
|
||||
EU_WEST_1_FIRST_REGION_TO_LATENCY_MAPPING = {
|
||||
"regionToLatencyMapping": {
|
||||
"us-west-2": 50,
|
||||
"us-east-1": 100,
|
||||
"eu-west-1": 10,
|
||||
"ap-northeast-1": 300
|
||||
}
|
||||
}
|
||||
REGION_TO_GAME_REQUEST_PAYLOAD_MAPPING = {
|
||||
REGION_US_WEST_2: json.dumps(US_WEST_2_FIRST_REGION_TO_LATENCY_MAPPING),
|
||||
REGION_EU_WEST_1: json.dumps(EU_WEST_1_FIRST_REGION_TO_LATENCY_MAPPING),
|
||||
NO_LATENCY: None
|
||||
}
|
||||
REGION_TO_GAME_SESSION_ARN_EXPECTED_LOCATION = {
|
||||
REGION_US_WEST_2: REGION_US_WEST_2,
|
||||
REGION_EU_WEST_1: REGION_EU_WEST_1,
|
||||
NO_LATENCY: REGION_US_EAST_1
|
||||
}
|
||||
|
||||
|
||||
def main():
|
||||
|
||||
user_pool = find_user_pool(USER_POOL_NAME)
|
||||
user_pool_id = user_pool['Id']
|
||||
print("User Pool Id:", user_pool_id)
|
||||
|
||||
user_pool_client = find_user_pool_client(user_pool_id, USER_POOL_CLIENT_NAME)
|
||||
user_pool_client_id = user_pool_client['ClientId']
|
||||
print("User Pool Client Id:", user_pool_client_id)
|
||||
|
||||
try:
|
||||
for region in REGIONS_TO_TEST:
|
||||
game_request_payload = REGION_TO_GAME_REQUEST_PAYLOAD_MAPPING.get(region)
|
||||
expected_game_session_region = REGION_TO_GAME_SESSION_ARN_EXPECTED_LOCATION.get(region)
|
||||
|
||||
headers_list = []
|
||||
for username in USERNAMES:
|
||||
regional_username = get_regional_user_name(username, region)
|
||||
|
||||
cognito_idp.sign_up(
|
||||
ClientId=user_pool_client_id,
|
||||
Username=regional_username,
|
||||
Password=PASSWORD,
|
||||
)
|
||||
|
||||
print(f"Created user: {regional_username}")
|
||||
|
||||
cognito_idp.admin_confirm_sign_up(
|
||||
UserPoolId=user_pool_id,
|
||||
Username=regional_username,
|
||||
)
|
||||
|
||||
init_auth_result = cognito_idp.initiate_auth(
|
||||
AuthFlow='USER_PASSWORD_AUTH',
|
||||
AuthParameters={
|
||||
'USERNAME': regional_username,
|
||||
'PASSWORD': PASSWORD,
|
||||
},
|
||||
ClientId=user_pool_client_id
|
||||
)
|
||||
|
||||
assert init_auth_result['ResponseMetadata']['HTTPStatusCode'] == 200, "Unsuccessful init_auth"
|
||||
print(f"Authenticated via username and password for {regional_username}")
|
||||
|
||||
id_token = init_auth_result['AuthenticationResult']['IdToken']
|
||||
headers = {
|
||||
'Auth': id_token
|
||||
}
|
||||
headers_list.append(headers)
|
||||
|
||||
results_request_url = get_rest_api_endpoint(REST_API_NAME, REGION, REST_API_STAGE, RESULTS_REQUEST_PATH)
|
||||
game_request_url = get_rest_api_endpoint(REST_API_NAME, REGION, REST_API_STAGE, GAME_REQUEST_PATH)
|
||||
|
||||
print(f"results_request_url: {results_request_url}")
|
||||
print(f"game_request_url: {game_request_url}")
|
||||
|
||||
for headers in headers_list:
|
||||
results_request_response = requests.post(url=results_request_url, headers=headers)
|
||||
assert results_request_response.status_code == 404, \
|
||||
f"Expect 'POST /get_game_connection' status code to be 404 (Not Found). Actual: " \
|
||||
f"{str(results_request_response.status_code)}"
|
||||
print("Verified lambda ResultsRequest response", results_request_response)
|
||||
|
||||
game_request_response = requests.post(url=game_request_url, headers=headers, data=game_request_payload)
|
||||
|
||||
print(f"Game request response '{game_request_response}'")
|
||||
|
||||
assert game_request_response.status_code == 202, \
|
||||
f"Expect 'POST /start_game' status code to be 202 (Accepted), actual: " \
|
||||
f"{str(game_request_response.status_code)}"
|
||||
print("Verified lambda GameRequest response", game_request_response)
|
||||
|
||||
print("Waiting for matchmaking request to be processed...")
|
||||
|
||||
verified_players = 0
|
||||
while verified_players != len(headers_list):
|
||||
verified_players = 0
|
||||
time.sleep(10) # 10 seconds
|
||||
for headers in headers_list:
|
||||
results_request_response = requests.post(url=results_request_url, headers=headers)
|
||||
if results_request_response.status_code == 204:
|
||||
print("Match is not ready yet")
|
||||
continue
|
||||
assert results_request_response.status_code == 200, \
|
||||
f"Expect 'POST /get_game_connection' status code to be 200 (Success), actual: " \
|
||||
f"{str(results_request_response.status_code)}"
|
||||
print("Verified lambda ResultsRequest response", results_request_response)
|
||||
|
||||
game_connection_info = json.loads(results_request_response.content)
|
||||
|
||||
print(f"Game connection info '{game_connection_info}'")
|
||||
|
||||
assert game_connection_info['IpAddress'] != ''
|
||||
assert int(game_connection_info['Port']) > 0
|
||||
assert REGION in game_connection_info['DnsName'], \
|
||||
f"Expect {game_connection_info['DnsName']} to contain '{REGION}'"
|
||||
assert expected_game_session_region in game_connection_info['GameSessionArn'], \
|
||||
f"Expect {game_connection_info['GameSessionArn']} to contain '{expected_game_session_region}'"
|
||||
assert "psess-" in game_connection_info['PlayerSessionId'], \
|
||||
f"Expect {game_connection_info['PlayerSessionId']} to contain 'psess-'"
|
||||
print("Verified game connection info:", game_connection_info)
|
||||
verified_players += 1
|
||||
print(f"{verified_players} players' game sessions verified")
|
||||
|
||||
finally:
|
||||
for region in REGIONS_TO_TEST:
|
||||
for username in USERNAMES:
|
||||
regional_username = get_regional_user_name(username, region)
|
||||
cognito_idp.admin_delete_user(
|
||||
UserPoolId=user_pool_id,
|
||||
Username=regional_username,
|
||||
)
|
||||
print("Deleted user:", regional_username)
|
||||
|
||||
print("Test Succeeded!")
|
||||
|
||||
|
||||
def get_regional_user_name(username, region):
|
||||
return f"{region}_{username}"
|
||||
|
||||
|
||||
def find_user_pool(user_pool_name):
|
||||
print("Finding user pool:", user_pool_name)
|
||||
result = cognito_idp.list_user_pools(MaxResults=50)
|
||||
pools = result['UserPools']
|
||||
return next(x for x in pools if x['Name'] == user_pool_name)
|
||||
|
||||
|
||||
def find_user_pool_client(user_pool_id, user_pool_client_name):
|
||||
print("Finding user pool client:", user_pool_client_name)
|
||||
results = cognito_idp.list_user_pool_clients(UserPoolId=user_pool_id)
|
||||
clients = results['UserPoolClients']
|
||||
return next(x for x in clients if x['ClientName'] == user_pool_client_name)
|
||||
|
||||
|
||||
def find_rest_api(rest_api_name):
|
||||
print("Finding rest api:", rest_api_name)
|
||||
results = apig.get_rest_apis()
|
||||
rest_apis = results['items']
|
||||
return next(x for x in rest_apis if x['name'] == rest_api_name)
|
||||
|
||||
|
||||
def get_rest_api_endpoint(rest_api_name, region, stage, path):
|
||||
print("Getting rest api endpoint", rest_api_name)
|
||||
rest_api = find_rest_api(rest_api_name)
|
||||
rest_api_id = rest_api['id']
|
||||
return f'https://{rest_api_id}.execute-api.{region}.amazonaws.com/{stage}/{path}'
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,736 @@
|
||||
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
||||
# SPDX-License-Identifier: MIT-0
|
||||
|
||||
AWSTemplateFormatVersion: "2010-09-09"
|
||||
|
||||
Description: >
|
||||
This CloudFormation template sets up a scenario to use FlexMatch -- a managed matchmaking service provided by
|
||||
GameLift. The template demonstrates best practices in acquiring the matchmaking ticket status, by listening to
|
||||
FlexMatch events in conjunction with a low frequency poller to ensure incomplete tickets are periodically pinged
|
||||
and therefore are not discarded by GameLift.
|
||||
|
||||
Parameters:
|
||||
ApiGatewayStageNameParameter:
|
||||
Type: String
|
||||
Default: v1
|
||||
Description: Name of the Api Gateway stage
|
||||
|
||||
ContainerGroupDefinitionNameParameter:
|
||||
Type: String
|
||||
Default: SampleCGDName
|
||||
Description: Name of the Api Gateway stage
|
||||
|
||||
ContainerImageNameParameter:
|
||||
Type: String
|
||||
Default: SampleContainerImageName
|
||||
Description: Name for the Container Image
|
||||
|
||||
ContainerImageUriParameter:
|
||||
Type: String
|
||||
Description: URI pointing to a Container Image in ECR
|
||||
|
||||
FleetDescriptionParameter:
|
||||
Type: String
|
||||
Default: Deployed by the Amazon GameLift Plug-in for Unreal.
|
||||
Description: Description of the fleet
|
||||
|
||||
TotalMemoryLimitParameter:
|
||||
Type: Number
|
||||
Default: 4
|
||||
Description: The maximum amount of memory (in MiB) to allocate to the container group
|
||||
|
||||
TotalVcpuLimitParameter:
|
||||
Type: Number
|
||||
Default: 2
|
||||
Description: The maximum amount of CPU units to allocate to the container group
|
||||
|
||||
FleetUdpFromPortParameter:
|
||||
Type: Number
|
||||
Default: 33430
|
||||
Description: Starting port number for UDP ports to be opened
|
||||
|
||||
FleetUdpToPortParameter:
|
||||
Type: Number
|
||||
Default: 33440
|
||||
Description: Ending port number for UDP ports to be opened
|
||||
|
||||
GameNameParameter:
|
||||
Type: String
|
||||
Default: MyGame
|
||||
Description: Game name to prepend before resource names
|
||||
MaxLength: 30
|
||||
|
||||
LambdaZipS3BucketParameter:
|
||||
Type: String
|
||||
Description: S3 bucket that stores the lambda function zip
|
||||
|
||||
LambdaZipS3KeyParameter:
|
||||
Type: String
|
||||
Description: S3 key that stores the lambda function zip
|
||||
|
||||
LaunchPathParameter:
|
||||
Type: String
|
||||
Description: Location of the game server executable in the build
|
||||
|
||||
MatchmakerTimeoutInSecondsParameter:
|
||||
Type: Number
|
||||
Default: 60
|
||||
Description: Time in seconds before matchmaker times out to place players on a server
|
||||
|
||||
MatchmakingTimeoutInSecondsParameter:
|
||||
Type: Number
|
||||
Default: 60
|
||||
Description: Time in seconds before matchmaker times out to wait for enough players to create game session placement
|
||||
|
||||
MaxTransactionsPerFiveMinutesPerIpParameter:
|
||||
Type: Number
|
||||
Default: 100
|
||||
MaxValue: 20000000
|
||||
MinValue: 100
|
||||
|
||||
NumPlayersPerGameParameter:
|
||||
Type: Number
|
||||
Default: 2
|
||||
Description: Number of players per game session
|
||||
|
||||
QueueTimeoutInSecondsParameter:
|
||||
Type: Number
|
||||
Default: 60
|
||||
Description: Time in seconds before game session placement times out to place players on a server
|
||||
|
||||
TeamNameParameter:
|
||||
Type: String
|
||||
Default: MySampleTeam
|
||||
Description: Team name used in matchmaking ruleset and StartMatchmaking API requests
|
||||
|
||||
TicketIdIndexNameParameter:
|
||||
Type: String
|
||||
Default: ticket-id-index
|
||||
Description: Name of the global secondary index on MatchmakingRequest table with partition key TicketId
|
||||
|
||||
UnrealEngineVersionParameter:
|
||||
Type: String
|
||||
Description: "Unreal Engine Version being used by the plugin"
|
||||
|
||||
EnableMetricsParameter:
|
||||
Type: String
|
||||
Default: "false"
|
||||
AllowedValues: ["true", "false"]
|
||||
Description: "Enable telemetry metrics collection using OTEL collector"
|
||||
|
||||
Conditions:
|
||||
ShouldCreateMetricsResources: !Equals [!Ref EnableMetricsParameter, "true"]
|
||||
|
||||
Resources:
|
||||
ApiGatewayCloudWatchRole:
|
||||
Type: "AWS::IAM::Role"
|
||||
Properties:
|
||||
AssumeRolePolicyDocument:
|
||||
Version: "2012-10-17"
|
||||
Statement:
|
||||
- Effect: Allow
|
||||
Principal:
|
||||
Service:
|
||||
- apigateway.amazonaws.com
|
||||
Action: "sts:AssumeRole"
|
||||
ManagedPolicyArns:
|
||||
- "arn:aws:iam::aws:policy/service-role/AmazonAPIGatewayPushToCloudWatchLogs"
|
||||
|
||||
Account:
|
||||
Type: "AWS::ApiGateway::Account"
|
||||
Properties:
|
||||
CloudWatchRoleArn: !GetAtt ApiGatewayCloudWatchRole.Arn
|
||||
|
||||
FlexMatchStatusPollerLambdaFunctionExecutionRole:
|
||||
Type: "AWS::IAM::Role"
|
||||
Properties:
|
||||
AssumeRolePolicyDocument:
|
||||
Version: "2012-10-17"
|
||||
Statement:
|
||||
- Effect: Allow
|
||||
Principal:
|
||||
Service:
|
||||
- lambda.amazonaws.com
|
||||
Action:
|
||||
- "sts:AssumeRole"
|
||||
ManagedPolicyArns:
|
||||
- "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
|
||||
Policies:
|
||||
- PolicyName: !Sub ${GameNameParameter}FlexMatchStatusPollerLambdaFunctionPolicies
|
||||
PolicyDocument:
|
||||
Version: "2012-10-17"
|
||||
Statement:
|
||||
- Effect: Allow
|
||||
Action:
|
||||
- "dynamodb:Scan"
|
||||
- "dynamodb:UpdateItem"
|
||||
- "gamelift:DescribeMatchmaking"
|
||||
Resource: "*"
|
||||
|
||||
GameRequestLambdaFunctionExecutionRole:
|
||||
Type: "AWS::IAM::Role"
|
||||
Properties:
|
||||
AssumeRolePolicyDocument:
|
||||
Version: "2012-10-17"
|
||||
Statement:
|
||||
- Effect: Allow
|
||||
Principal:
|
||||
Service:
|
||||
- lambda.amazonaws.com
|
||||
Action:
|
||||
- "sts:AssumeRole"
|
||||
ManagedPolicyArns:
|
||||
- "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
|
||||
Policies:
|
||||
- PolicyName: !Sub ${GameNameParameter}GameRequestLambdaFunctionPolicies
|
||||
PolicyDocument:
|
||||
Version: "2012-10-17"
|
||||
Statement:
|
||||
- Effect: Allow
|
||||
Action:
|
||||
- "dynamodb:PutItem"
|
||||
- "dynamodb:UpdateItem"
|
||||
- "dynamodb:GetItem"
|
||||
- "dynamodb:Query"
|
||||
- "gamelift:StartMatchmaking"
|
||||
Resource: "*"
|
||||
|
||||
MatchmakerEventHandlerLambdaFunctionExecutionRole:
|
||||
Type: "AWS::IAM::Role"
|
||||
Properties:
|
||||
AssumeRolePolicyDocument:
|
||||
Version: "2012-10-17"
|
||||
Statement:
|
||||
- Effect: Allow
|
||||
Principal:
|
||||
Service:
|
||||
- lambda.amazonaws.com
|
||||
Action:
|
||||
- "sts:AssumeRole"
|
||||
ManagedPolicyArns:
|
||||
- "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
|
||||
Policies:
|
||||
- PolicyName: !Sub ${GameNameParameter}MatchmakerEventHandlerLambdaFunctionPolicies
|
||||
PolicyDocument:
|
||||
Version: "2012-10-17"
|
||||
Statement:
|
||||
- Effect: Allow
|
||||
Action:
|
||||
- "dynamodb:Query"
|
||||
- "dynamodb:UpdateItem"
|
||||
Resource: "*"
|
||||
|
||||
RestApi:
|
||||
Type: "AWS::ApiGateway::RestApi"
|
||||
Properties:
|
||||
Name: !Sub ${GameNameParameter}RestApi
|
||||
|
||||
ResultsRequestLambdaFunctionExecutionRole:
|
||||
Type: "AWS::IAM::Role"
|
||||
Properties:
|
||||
AssumeRolePolicyDocument:
|
||||
Version: "2012-10-17"
|
||||
Statement:
|
||||
- Effect: Allow
|
||||
Principal:
|
||||
Service:
|
||||
- lambda.amazonaws.com
|
||||
Action:
|
||||
- "sts:AssumeRole"
|
||||
ManagedPolicyArns:
|
||||
- "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
|
||||
Policies:
|
||||
- PolicyName: !Sub ${GameNameParameter}ResultsRequestLambdaFunctionPolicies
|
||||
PolicyDocument:
|
||||
Version: "2012-10-17"
|
||||
Statement:
|
||||
- Effect: Allow
|
||||
Action:
|
||||
- "dynamodb:Query"
|
||||
Resource: "*"
|
||||
|
||||
UserPool:
|
||||
Type: "AWS::Cognito::UserPool"
|
||||
Properties:
|
||||
AdminCreateUserConfig:
|
||||
AllowAdminCreateUserOnly: false
|
||||
AutoVerifiedAttributes:
|
||||
- email
|
||||
EmailConfiguration:
|
||||
EmailSendingAccount: COGNITO_DEFAULT
|
||||
EmailVerificationMessage: "Please verify your email to complete account registration for the GameLift Plugin FlexMatch fleet deployment scenario. Confirmation Code {####}."
|
||||
EmailVerificationSubject: GameLift Plugin - Deployment Scenario Account Verification
|
||||
Policies:
|
||||
PasswordPolicy:
|
||||
MinimumLength: 8
|
||||
RequireLowercase: true
|
||||
RequireNumbers: true
|
||||
RequireSymbols: true
|
||||
RequireUppercase: true
|
||||
Schema:
|
||||
- Name: email
|
||||
AttributeDataType: String
|
||||
Mutable: false
|
||||
Required: true
|
||||
UserPoolName: !Sub ${GameNameParameter}UserPool
|
||||
UsernameAttributes:
|
||||
- email
|
||||
|
||||
GameRequestApiResource:
|
||||
Type: "AWS::ApiGateway::Resource"
|
||||
Properties:
|
||||
ParentId: !GetAtt RestApi.RootResourceId
|
||||
PathPart: start_game
|
||||
RestApiId: !Ref RestApi
|
||||
|
||||
MatchmakingRequestTable:
|
||||
Type: "AWS::DynamoDB::Table"
|
||||
Properties:
|
||||
AttributeDefinitions:
|
||||
- AttributeName: PlayerId
|
||||
AttributeType: S
|
||||
- AttributeName: TicketId
|
||||
AttributeType: S
|
||||
- AttributeName: StartTime
|
||||
AttributeType: "N"
|
||||
GlobalSecondaryIndexes:
|
||||
- IndexName: !Ref TicketIdIndexNameParameter
|
||||
KeySchema:
|
||||
- AttributeName: TicketId
|
||||
KeyType: HASH
|
||||
Projection:
|
||||
ProjectionType: ALL
|
||||
ProvisionedThroughput:
|
||||
ReadCapacityUnits: 5
|
||||
WriteCapacityUnits: 5
|
||||
KeySchema:
|
||||
- AttributeName: PlayerId
|
||||
KeyType: HASH
|
||||
- AttributeName: StartTime
|
||||
KeyType: RANGE
|
||||
ProvisionedThroughput:
|
||||
ReadCapacityUnits: 5
|
||||
WriteCapacityUnits: 5
|
||||
TableName: !Sub ${GameNameParameter}MatchmakingRequestTable
|
||||
TimeToLiveSpecification:
|
||||
AttributeName: ExpirationTime
|
||||
Enabled: true
|
||||
|
||||
ResultsRequestApiResource:
|
||||
Type: "AWS::ApiGateway::Resource"
|
||||
Properties:
|
||||
ParentId: !GetAtt RestApi.RootResourceId
|
||||
PathPart: get_game_connection
|
||||
RestApiId: !Ref RestApi
|
||||
|
||||
UserPoolClient:
|
||||
Type: "AWS::Cognito::UserPoolClient"
|
||||
Properties:
|
||||
AccessTokenValidity: 1
|
||||
ClientName: !Sub ${GameNameParameter}UserPoolClient
|
||||
ExplicitAuthFlows:
|
||||
- ALLOW_USER_PASSWORD_AUTH
|
||||
- ALLOW_REFRESH_TOKEN_AUTH
|
||||
GenerateSecret: false
|
||||
IdTokenValidity: 1
|
||||
PreventUserExistenceErrors: ENABLED
|
||||
ReadAttributes:
|
||||
- email
|
||||
- preferred_username
|
||||
RefreshTokenValidity: 30
|
||||
SupportedIdentityProviders:
|
||||
- COGNITO
|
||||
UserPoolId: !Ref UserPool
|
||||
|
||||
WebACL:
|
||||
Type: "AWS::WAFv2::WebACL"
|
||||
DependsOn:
|
||||
- ApiDeployment
|
||||
Properties:
|
||||
DefaultAction:
|
||||
Allow:
|
||||
{}
|
||||
Description: !Sub "WebACL for game: ${GameNameParameter}"
|
||||
Name: !Sub ${GameNameParameter}WebACL
|
||||
Rules:
|
||||
- Name: !Sub ${GameNameParameter}WebACLPerIpThrottleRule
|
||||
Action:
|
||||
Block:
|
||||
{}
|
||||
Priority: 0
|
||||
Statement:
|
||||
RateBasedStatement:
|
||||
AggregateKeyType: IP
|
||||
Limit: !Ref MaxTransactionsPerFiveMinutesPerIpParameter
|
||||
VisibilityConfig:
|
||||
CloudWatchMetricsEnabled: true
|
||||
MetricName: !Sub ${GameNameParameter}WebACLPerIpThrottleRuleMetrics
|
||||
SampledRequestsEnabled: true
|
||||
Scope: REGIONAL
|
||||
VisibilityConfig:
|
||||
CloudWatchMetricsEnabled: true
|
||||
MetricName: !Sub ${GameNameParameter}WebACLMetrics
|
||||
SampledRequestsEnabled: true
|
||||
|
||||
ApiDeployment:
|
||||
Type: "AWS::ApiGateway::Deployment"
|
||||
DependsOn:
|
||||
- GameRequestApiMethod
|
||||
- ResultsRequestApiMethod
|
||||
Properties:
|
||||
RestApiId: !Ref RestApi
|
||||
StageDescription:
|
||||
DataTraceEnabled: true
|
||||
LoggingLevel: INFO
|
||||
MetricsEnabled: true
|
||||
StageName: !Ref ApiGatewayStageNameParameter
|
||||
|
||||
Authorizer:
|
||||
Type: "AWS::ApiGateway::Authorizer"
|
||||
Properties:
|
||||
IdentitySource: method.request.header.Auth
|
||||
Name: CognitoAuthorizer
|
||||
ProviderARNs:
|
||||
- "Fn::GetAtt":
|
||||
- UserPool
|
||||
- Arn
|
||||
RestApiId: !Ref RestApi
|
||||
Type: COGNITO_USER_POOLS
|
||||
|
||||
GameSessionQueue:
|
||||
Type: "AWS::GameLift::GameSessionQueue"
|
||||
Properties:
|
||||
Destinations:
|
||||
- DestinationArn: !Sub "arn:aws:gamelift:${AWS::Region}:${AWS::AccountId}:containerfleet/${ContainerFleetResource}"
|
||||
Name: !Sub ${GameNameParameter}GameSessionQueue
|
||||
TimeoutInSeconds: !Ref QueueTimeoutInSecondsParameter
|
||||
|
||||
MatchmakingRuleSet:
|
||||
Type: "AWS::GameLift::MatchmakingRuleSet"
|
||||
Properties:
|
||||
Name: !Sub ${GameNameParameter}MatchmakingRuleSet
|
||||
RuleSetBody: !Sub
|
||||
- |-
|
||||
{
|
||||
"name": "MyMatchmakingRuleSet",
|
||||
"ruleLanguageVersion": "1.0",
|
||||
"teams": [{
|
||||
"name": "${teamName}",
|
||||
"minPlayers": ${minPlayers},
|
||||
"maxPlayers": ${maxPlayers}
|
||||
}]
|
||||
}
|
||||
- maxPlayers: !Ref NumPlayersPerGameParameter
|
||||
minPlayers: !Ref NumPlayersPerGameParameter
|
||||
teamName: !Ref TeamNameParameter
|
||||
|
||||
FlexMatchStatusPollerLambdaFunction:
|
||||
Type: "AWS::Lambda::Function"
|
||||
Properties:
|
||||
Code:
|
||||
S3Bucket: !Ref LambdaZipS3BucketParameter
|
||||
S3Key: !Ref LambdaZipS3KeyParameter
|
||||
Description: Lambda function to handle game requests
|
||||
Environment:
|
||||
Variables:
|
||||
MatchmakingRequestTableName: !Ref MatchmakingRequestTable
|
||||
FunctionName: !Sub ${GameNameParameter}FlexMatchStatusPollerLambda
|
||||
Handler: flexmatch_status_poller.handler
|
||||
MemorySize: 128
|
||||
Role: !GetAtt FlexMatchStatusPollerLambdaFunctionExecutionRole.Arn
|
||||
Runtime: python3.14
|
||||
|
||||
GameRequestApiMethod:
|
||||
Type: "AWS::ApiGateway::Method"
|
||||
Properties:
|
||||
AuthorizationType: COGNITO_USER_POOLS
|
||||
AuthorizerId: !Ref Authorizer
|
||||
HttpMethod: POST
|
||||
Integration:
|
||||
Type: AWS_PROXY
|
||||
IntegrationHttpMethod: POST
|
||||
Uri: !Sub "arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${GameRequestLambdaFunction.Arn}/invocations"
|
||||
OperationName: GameRequest
|
||||
ResourceId: !Ref GameRequestApiResource
|
||||
RestApiId: !Ref RestApi
|
||||
|
||||
MatchmakerEventHandlerLambdaFunction:
|
||||
Type: "AWS::Lambda::Function"
|
||||
Properties:
|
||||
Code:
|
||||
S3Bucket: !Ref LambdaZipS3BucketParameter
|
||||
S3Key: !Ref LambdaZipS3KeyParameter
|
||||
Description: Lambda function to handle game requests
|
||||
Environment:
|
||||
Variables:
|
||||
MatchmakingRequestTableName: !Ref MatchmakingRequestTable
|
||||
TicketIdIndexName: !Ref TicketIdIndexNameParameter
|
||||
FunctionName: !Sub ${GameNameParameter}MatchmakerEventHandlerLambda
|
||||
Handler: matchmaker_event_handler.handler
|
||||
MemorySize: 128
|
||||
Role: !GetAtt MatchmakerEventHandlerLambdaFunctionExecutionRole.Arn
|
||||
Runtime: python3.14
|
||||
|
||||
ResultsRequestApiMethod:
|
||||
Type: "AWS::ApiGateway::Method"
|
||||
Properties:
|
||||
AuthorizationType: COGNITO_USER_POOLS
|
||||
AuthorizerId: !Ref Authorizer
|
||||
HttpMethod: POST
|
||||
Integration:
|
||||
Type: AWS_PROXY
|
||||
IntegrationHttpMethod: POST
|
||||
Uri: !Sub "arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${ResultsRequestLambdaFunction.Arn}/invocations"
|
||||
OperationName: ResultsRequest
|
||||
ResourceId: !Ref ResultsRequestApiResource
|
||||
RestApiId: !Ref RestApi
|
||||
|
||||
ResultsRequestLambdaFunction:
|
||||
Type: "AWS::Lambda::Function"
|
||||
Properties:
|
||||
Code:
|
||||
S3Bucket: !Ref LambdaZipS3BucketParameter
|
||||
S3Key: !Ref LambdaZipS3KeyParameter
|
||||
Description: Lambda function to handle game requests
|
||||
Environment:
|
||||
Variables:
|
||||
MatchmakingRequestTableName: !Ref MatchmakingRequestTable
|
||||
FunctionName: !Sub ${GameNameParameter}ResultsRequestLambda
|
||||
Handler: results_request.handler
|
||||
MemorySize: 128
|
||||
Role: !GetAtt ResultsRequestLambdaFunctionExecutionRole.Arn
|
||||
Runtime: python3.14
|
||||
|
||||
WebACLAssociation:
|
||||
Type: "AWS::WAFv2::WebACLAssociation"
|
||||
DependsOn:
|
||||
- ApiDeployment
|
||||
- WebACL
|
||||
Properties:
|
||||
ResourceArn: !Sub
|
||||
- "arn:aws:apigateway:${REGION}::/restapis/${REST_API_ID}/stages/${STAGE_NAME}"
|
||||
- REGION: !Ref "AWS::Region"
|
||||
REST_API_ID: !Ref RestApi
|
||||
STAGE_NAME: !Ref ApiGatewayStageNameParameter
|
||||
WebACLArn: !GetAtt WebACL.Arn
|
||||
|
||||
FlexMatchStatusPollerScheduledRule:
|
||||
Type: "AWS::Events::Rule"
|
||||
Properties:
|
||||
Description: !Sub ${GameNameParameter}FlexMatchStatusPollerScheduledRule
|
||||
ScheduleExpression: rate(1 minute)
|
||||
State: ENABLED
|
||||
Targets:
|
||||
- Arn: !GetAtt FlexMatchStatusPollerLambdaFunction.Arn
|
||||
Id: !Sub ${GameNameParameter}FlexMatchStatusPollerScheduledRule
|
||||
|
||||
MatchmakerEventTopicKey:
|
||||
Type: "AWS::KMS::Key"
|
||||
Properties:
|
||||
Description: KMS key for FlexMatch SNS notifications
|
||||
KeyPolicy:
|
||||
Version: "2012-10-17"
|
||||
Statement:
|
||||
- Effect: Allow
|
||||
Principal:
|
||||
AWS: !Sub "arn:aws:iam::${AWS::AccountId}:root"
|
||||
Action: "kms:*"
|
||||
Resource: "*"
|
||||
- Effect: Allow
|
||||
Principal:
|
||||
Service: gamelift.amazonaws.com
|
||||
Action:
|
||||
- "kms:Decrypt"
|
||||
- "kms:GenerateDataKey"
|
||||
Resource: "*"
|
||||
|
||||
MatchmakerEventTopic:
|
||||
Type: "AWS::SNS::Topic"
|
||||
Properties:
|
||||
KmsMasterKeyId: !Ref MatchmakerEventTopicKey
|
||||
Subscription:
|
||||
- Endpoint: !GetAtt MatchmakerEventHandlerLambdaFunction.Arn
|
||||
Protocol: lambda
|
||||
TopicName: !Sub ${GameNameParameter}MatchmakerEventTopic
|
||||
|
||||
ContainerFleetRole:
|
||||
Type: "AWS::IAM::Role"
|
||||
Properties:
|
||||
AssumeRolePolicyDocument:
|
||||
Version: "2012-10-17"
|
||||
Statement:
|
||||
- Effect: Allow
|
||||
Principal:
|
||||
Service:
|
||||
- cloudformation.amazonaws.com
|
||||
- gamelift.amazonaws.com
|
||||
Action:
|
||||
- "sts:AssumeRole"
|
||||
ManagedPolicyArns:
|
||||
- "arn:aws:iam::aws:policy/GameLiftContainerFleetPolicy"
|
||||
Policies: !If
|
||||
- ShouldCreateMetricsResources
|
||||
- - PolicyName: AMPRemoteWriteAccess
|
||||
PolicyDocument:
|
||||
Version: '2012-10-17'
|
||||
Statement:
|
||||
- Effect: Allow
|
||||
Action:
|
||||
- aps:RemoteWrite
|
||||
Resource: !Sub 'arn:aws:aps:*:${AWS::AccountId}:workspace/*'
|
||||
- PolicyName: OTelCollectorEMFExporter
|
||||
PolicyDocument:
|
||||
Version: '2012-10-17'
|
||||
Statement:
|
||||
- Effect: Allow
|
||||
Action:
|
||||
- logs:PutLogEvents
|
||||
- logs:CreateLogStream
|
||||
- logs:CreateLogGroup
|
||||
- logs:PutRetentionPolicy
|
||||
Resource: !Sub 'arn:aws:logs:*:${AWS::AccountId}:log-group:*:log-stream:*'
|
||||
- []
|
||||
|
||||
ContainerGroupResource:
|
||||
Type: "AWS::GameLift::ContainerGroupDefinition"
|
||||
Properties:
|
||||
GameServerContainerDefinition:
|
||||
ContainerName: !Ref ContainerImageNameParameter
|
||||
ImageUri: !Ref ContainerImageUriParameter
|
||||
ServerSdkVersion: "5.4.0"
|
||||
PortConfiguration:
|
||||
ContainerPortRanges:
|
||||
- FromPort: !Ref FleetUdpFromPortParameter
|
||||
Protocol: "UDP"
|
||||
ToPort: !Ref FleetUdpToPortParameter
|
||||
Name: !Ref ContainerGroupDefinitionNameParameter
|
||||
OperatingSystem: "AMAZON_LINUX_2023"
|
||||
TotalVcpuLimit: !Ref TotalVcpuLimitParameter
|
||||
TotalMemoryLimitMebibytes: !Ref TotalMemoryLimitParameter
|
||||
|
||||
ContainerFleetResource:
|
||||
DependsOn:
|
||||
- ContainerGroupResource
|
||||
Type: "AWS::GameLift::ContainerFleet"
|
||||
Properties:
|
||||
GameServerContainerGroupDefinitionName: !Ref ContainerGroupDefinitionNameParameter
|
||||
InstanceConnectionPortRange:
|
||||
FromPort: !Ref FleetUdpFromPortParameter
|
||||
ToPort: !Ref FleetUdpToPortParameter
|
||||
Description: !Sub
|
||||
- "${FleetDescriptionParameter} Using Unreal Engine Version ${UnrealEngineVersionParameter}${MetricsText}"
|
||||
- MetricsText: !If [ShouldCreateMetricsResources, " with telemetry metrics enabled", ""]
|
||||
InstanceInboundPermissions:
|
||||
- FromPort: !Ref FleetUdpFromPortParameter
|
||||
IpRange: "0.0.0.0/0"
|
||||
Protocol: UDP
|
||||
ToPort: !Ref FleetUdpToPortParameter
|
||||
Locations:
|
||||
- Location: us-west-2
|
||||
- Location: us-east-1
|
||||
- Location: eu-west-1
|
||||
InstanceType: c4.xlarge
|
||||
BillingType: ON_DEMAND
|
||||
FleetRoleArn: !GetAtt ContainerFleetRole.Arn
|
||||
NewGameSessionProtectionPolicy: FullProtection
|
||||
GameSessionCreationLimitPolicy:
|
||||
NewGameSessionsPerCreator: 5
|
||||
PolicyPeriodInMinutes: 2
|
||||
|
||||
FlexMatchStatusPollerLambdaPermission:
|
||||
Type: "AWS::Lambda::Permission"
|
||||
Properties:
|
||||
Action: "lambda:InvokeFunction"
|
||||
FunctionName: !Ref FlexMatchStatusPollerLambdaFunction
|
||||
Principal: events.amazonaws.com
|
||||
SourceArn: !GetAtt FlexMatchStatusPollerScheduledRule.Arn
|
||||
|
||||
MatchmakerEventHandlerLambdaPermission:
|
||||
Type: "AWS::Lambda::Permission"
|
||||
Properties:
|
||||
Action: "lambda:InvokeFunction"
|
||||
FunctionName: !Ref MatchmakerEventHandlerLambdaFunction
|
||||
Principal: sns.amazonaws.com
|
||||
SourceArn: !Ref MatchmakerEventTopic
|
||||
|
||||
MatchmakerEventTopicPolicy:
|
||||
Type: "AWS::SNS::TopicPolicy"
|
||||
DependsOn: MatchmakerEventTopic
|
||||
Properties:
|
||||
PolicyDocument:
|
||||
Version: "2012-10-17"
|
||||
Statement:
|
||||
- Effect: Allow
|
||||
Principal:
|
||||
Service: gamelift.amazonaws.com
|
||||
Action:
|
||||
- "sns:Publish"
|
||||
Resource: !Ref MatchmakerEventTopic
|
||||
Topics:
|
||||
- Ref: MatchmakerEventTopic
|
||||
|
||||
ResultsRequestLambdaFunctionApiGatewayPermission:
|
||||
Type: "AWS::Lambda::Permission"
|
||||
Properties:
|
||||
Action: "lambda:InvokeFunction"
|
||||
FunctionName: !GetAtt ResultsRequestLambdaFunction.Arn
|
||||
Principal: apigateway.amazonaws.com
|
||||
SourceArn: !Sub "arn:aws:execute-api:${AWS::Region}:${AWS::AccountId}:${RestApi}/*/*/*"
|
||||
|
||||
MatchmakingConfiguration:
|
||||
Type: "AWS::GameLift::MatchmakingConfiguration"
|
||||
DependsOn:
|
||||
- GameSessionQueue
|
||||
- MatchmakingRuleSet
|
||||
Properties:
|
||||
AcceptanceRequired: false
|
||||
BackfillMode: MANUAL
|
||||
Description: Matchmaking configuration for sample GameLift game
|
||||
FlexMatchMode: WITH_QUEUE
|
||||
GameSessionQueueArns:
|
||||
- "Fn::GetAtt":
|
||||
- GameSessionQueue
|
||||
- Arn
|
||||
Name: !Sub ${GameNameParameter}MatchmakingConfiguration
|
||||
NotificationTarget: !Ref MatchmakerEventTopic
|
||||
RequestTimeoutSeconds: !Ref MatchmakerTimeoutInSecondsParameter
|
||||
RuleSetName: !Ref MatchmakingRuleSet
|
||||
|
||||
GameRequestLambdaFunction:
|
||||
Type: "AWS::Lambda::Function"
|
||||
Properties:
|
||||
Code:
|
||||
S3Bucket: !Ref LambdaZipS3BucketParameter
|
||||
S3Key: !Ref LambdaZipS3KeyParameter
|
||||
Description: Lambda function to handle game requests
|
||||
Environment:
|
||||
Variables:
|
||||
MatchmakingConfigurationName: !GetAtt MatchmakingConfiguration.Name
|
||||
MatchmakingRequestTableName: !Ref MatchmakingRequestTable
|
||||
TeamName: !Ref TeamNameParameter
|
||||
FunctionName: !Sub ${GameNameParameter}GameRequestLambda
|
||||
Handler: game_request.handler
|
||||
MemorySize: 128
|
||||
Role: !GetAtt GameRequestLambdaFunctionExecutionRole.Arn
|
||||
Runtime: python3.14
|
||||
|
||||
GameRequestLambdaFunctionApiGatewayPermission:
|
||||
Type: "AWS::Lambda::Permission"
|
||||
Properties:
|
||||
Action: "lambda:InvokeFunction"
|
||||
FunctionName: !GetAtt GameRequestLambdaFunction.Arn
|
||||
Principal: apigateway.amazonaws.com
|
||||
SourceArn: !Sub "arn:aws:execute-api:${AWS::Region}:${AWS::AccountId}:${RestApi}/*/*/*"
|
||||
|
||||
Outputs:
|
||||
ApiGatewayEndpoint:
|
||||
Description: Url of ApiGateway Endpoint
|
||||
Value: !Sub "https://${RestApi}.execute-api.${AWS::Region}.amazonaws.com/${ApiGatewayStageNameParameter}/"
|
||||
|
||||
UserPoolClientId:
|
||||
Description: Id of UserPoolClient
|
||||
Value: !Ref UserPoolClient
|
||||
|
||||
IdentityRegion:
|
||||
Description: Region name
|
||||
Value: !Ref "AWS::Region"
|
||||
@@ -0,0 +1,33 @@
|
||||
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
---
|
||||
# THIS IS A SAMPLE CLOUDFORMATION PARAMETERS FILE
|
||||
GameNameParameter:
|
||||
value: "{{AWSGAMELIFT::SYS::GAMENAME}}"
|
||||
LambdaZipS3BucketParameter:
|
||||
value: "{{AWSGAMELIFT::VARS::LambdaZipS3BucketParameter}}"
|
||||
LambdaZipS3KeyParameter:
|
||||
value: "{{AWSGAMELIFT::VARS::LambdaZipS3KeyParameter}}"
|
||||
ApiGatewayStageNameParameter:
|
||||
value: "{{AWSGAMELIFT::VARS::ApiGatewayStageNameParameter}}"
|
||||
ContainerGroupDefinitionNameParameter:
|
||||
value: "{{AWSGAMELIFT::VARS::ContainerGroupDefinitionNameParameter}}"
|
||||
ContainerImageNameParameter:
|
||||
value: "{{AWSGAMELIFT::VARS::ContainerImageNameParameter}}"
|
||||
ContainerImageUriParameter:
|
||||
value: "{{AWSGAMELIFT::VARS::ContainerImageUriParameter}}"
|
||||
LaunchPathParameter:
|
||||
value: "{{AWSGAMELIFT::VARS::LaunchPathParameter}}"
|
||||
TotalVcpuLimitParameter:
|
||||
value: "{{AWSGAMELIFT::VARS::TotalVcpuLimitParameter}}"
|
||||
TotalMemoryLimitParameter:
|
||||
value: "{{AWSGAMELIFT::VARS::TotalMemoryLimitParameter}}"
|
||||
FleetUdpFromPortParameter:
|
||||
value: "{{AWSGAMELIFT::VARS::FleetUdpFromPortParameter}}"
|
||||
FleetUdpToPortParameter:
|
||||
value: "{{AWSGAMELIFT::VARS::FleetUdpToPortParameter}}"
|
||||
UnrealEngineVersionParameter:
|
||||
value: "{{AWSGAMELIFT::VARS::UnrealEngineVersionParameter}}"
|
||||
EnableMetricsParameter:
|
||||
value: "{{AWSGAMELIFT::VARS::EnableMetricsParameter}}"
|
||||
@@ -0,0 +1,11 @@
|
||||
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
---
|
||||
# Key/value pairs to be added to the game's awsGameLiftClientConfig.yml file
|
||||
# These values will be replaced at the end of create/update of
|
||||
# the feature's CloudFormation stack.
|
||||
|
||||
user_pool_client_id: "{{AWSGAMELIFT::CFNOUTPUT::UserPoolClientId}}"
|
||||
identity_api_gateway_base_url: "{{AWSGAMELIFT::CFNOUTPUT::ApiGatewayEndpoint}}"
|
||||
identity_region: "{{AWSGAMELIFT::CFNOUTPUT::IdentityRegion}}"
|
||||
@@ -0,0 +1,145 @@
|
||||
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import boto3
|
||||
from boto3.dynamodb.conditions import Attr, And
|
||||
from botocore.exceptions import ClientError
|
||||
import os
|
||||
import time
|
||||
|
||||
NON_TERMINAL_REQUEST_QUERY_LIMIT = 50
|
||||
MATCHMAKING_STARTED_STATUS = 'MatchmakingStarted'
|
||||
MATCHMAKING_SUCCEEDED_STATUS = 'MatchmakingSucceeded'
|
||||
MATCHMAKING_TIMED_OUT_STATUS = 'MatchmakingTimedOut'
|
||||
MATCHMAKING_CANCELLED_STATUS = 'MatchmakingCancelled'
|
||||
MATCHMAKING_FAILED_STATUS = 'MatchmakingFailed'
|
||||
MAX_PARTITION_SIZE = 10
|
||||
MIN_TIME_ELAPSED_BEFORE_UPDATE_IN_SECONDS = 30
|
||||
|
||||
|
||||
def handler(event, context):
|
||||
"""
|
||||
Finds all non-terminal matchmaking requests and poll for their status. It is recommended by GameLift
|
||||
to regularly poll for ticket status at a low rate.
|
||||
See: https://docs.aws.amazon.com/gamelift/latest/flexmatchguide/match-client.html#match-client-track
|
||||
|
||||
:param event: lambda event, not used by this function
|
||||
:param context: lambda context, not used by this function
|
||||
:return: None
|
||||
"""
|
||||
lambda_start_time = round(time.time())
|
||||
|
||||
print(f"Polling non-terminal matchmaking tickets. Lambda start time: {lambda_start_time}")
|
||||
|
||||
matchmaking_request_table_name = os.environ['MatchmakingRequestTableName']
|
||||
|
||||
dynamodb = boto3.resource('dynamodb')
|
||||
matchmaking_request_table = dynamodb.Table(matchmaking_request_table_name)
|
||||
|
||||
gamelift = boto3.client('gamelift')
|
||||
|
||||
matchmaking_requests = matchmaking_request_table.scan(
|
||||
Limit=NON_TERMINAL_REQUEST_QUERY_LIMIT,
|
||||
FilterExpression=And(Attr('TicketStatus').eq(MATCHMAKING_STARTED_STATUS),
|
||||
Attr('LastUpdatedTime').lt(lambda_start_time - MIN_TIME_ELAPSED_BEFORE_UPDATE_IN_SECONDS))
|
||||
)
|
||||
|
||||
if matchmaking_requests['Count'] <= 0:
|
||||
print("No non-terminal matchmaking requests found")
|
||||
|
||||
for matchmaking_requests in partition(matchmaking_requests['Items'], MAX_PARTITION_SIZE):
|
||||
ticket_id_to_request_mapping = {request['TicketId']: request for request in matchmaking_requests}
|
||||
describe_matchmaking_result = gamelift.describe_matchmaking(
|
||||
TicketIds=list(ticket_id_to_request_mapping.keys())
|
||||
)
|
||||
ticket_list = describe_matchmaking_result['TicketList']
|
||||
if len(ticket_list) != len(matchmaking_requests):
|
||||
print(f"Resulting TicketList length: {len(ticket_list)} from DescribeMatchmaking "
|
||||
f"does not match the request size: {len(matchmaking_requests)}")
|
||||
for ticket in ticket_list:
|
||||
ticket_id = ticket['TicketId']
|
||||
ticket_status = ticket['Status']
|
||||
matchmaking_request_status = to_matchmaking_request_status(ticket_status)
|
||||
matchmaking_request = ticket_id_to_request_mapping[ticket_id]
|
||||
player_id = matchmaking_request['PlayerId']
|
||||
start_time = matchmaking_request['StartTime']
|
||||
last_updated_time = matchmaking_request['LastUpdatedTime']
|
||||
try:
|
||||
attribute_updates = {
|
||||
'LastUpdatedTime': {
|
||||
'Value': lambda_start_time
|
||||
}
|
||||
}
|
||||
if ticket_status in ['COMPLETED', 'FAILED', 'TIMED_OUT', 'CANCELLED']:
|
||||
print(f'Ticket: {ticket_id} status was updated to {ticket_status}')
|
||||
attribute_updates.update({
|
||||
'TicketStatus': {
|
||||
'Value': matchmaking_request_status
|
||||
}
|
||||
})
|
||||
if ticket_status == 'COMPLETED':
|
||||
# parse the playerSessionId
|
||||
matched_player_sessions = ticket.get('GameSessionConnectionInfo', {}).get('MatchedPlayerSessions')
|
||||
player_session_id = None
|
||||
if matched_player_sessions is not None and len(matched_player_sessions) == 1:
|
||||
player_session_id = matched_player_sessions[0].get('PlayerSessionId')
|
||||
|
||||
attribute_updates.update({
|
||||
'IpAddress': {
|
||||
'Value': ticket.get('GameSessionConnectionInfo', {}).get('IpAddress')
|
||||
},
|
||||
'DnsName': {
|
||||
'Value': ticket.get('GameSessionConnectionInfo', {}).get('DnsName')
|
||||
},
|
||||
'Port': {
|
||||
'Value': str(ticket.get('GameSessionConnectionInfo', {}).get('Port'))
|
||||
},
|
||||
'GameSessionArn': {
|
||||
'Value': str(ticket.get('GameSessionConnectionInfo', {}).get('GameSessionArn'))
|
||||
},
|
||||
'PlayerSessionId': {
|
||||
'Value' : str(player_session_id)
|
||||
}
|
||||
})
|
||||
else:
|
||||
print(f'No updates to ticket: {ticket_id} compared to '
|
||||
f'{lambda_start_time - last_updated_time} seconds ago')
|
||||
|
||||
matchmaking_request_table.update_item(
|
||||
Key={
|
||||
'PlayerId': player_id,
|
||||
'StartTime': start_time
|
||||
},
|
||||
AttributeUpdates=attribute_updates,
|
||||
Expected={
|
||||
'TicketStatus': {
|
||||
'Value': MATCHMAKING_STARTED_STATUS,
|
||||
'ComparisonOperator': 'EQ'
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
except ClientError as e:
|
||||
error_code = e.response['Error']['Code']
|
||||
if error_code == 'ConditionCheckFailedException':
|
||||
print(f"Ticket: {ticket_id} status has been updated (likely by MatchMakerEventHandler). "
|
||||
f"No change is made")
|
||||
continue
|
||||
raise e
|
||||
|
||||
|
||||
def partition(collection, n):
|
||||
"""Yield successive n-sized partitions from collection."""
|
||||
for i in range(0, len(collection), n):
|
||||
yield collection[i:i + n]
|
||||
|
||||
|
||||
def to_matchmaking_request_status(ticket_status):
|
||||
if ticket_status == 'COMPLETED':
|
||||
return MATCHMAKING_SUCCEEDED_STATUS
|
||||
if ticket_status == 'FAILED':
|
||||
return MATCHMAKING_FAILED_STATUS
|
||||
if ticket_status == 'TIMED_OUT':
|
||||
return MATCHMAKING_TIMED_OUT_STATUS
|
||||
if ticket_status == 'CANCELLED':
|
||||
return MATCHMAKING_CANCELLED_STATUS
|
||||
@@ -0,0 +1,133 @@
|
||||
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import boto3
|
||||
from boto3.dynamodb.conditions import Key
|
||||
import time
|
||||
import os
|
||||
import json
|
||||
|
||||
DEFAULT_TTL_IN_SECONDS = 10 * 60 # 10 minutes
|
||||
MATCHMAKING_STARTED_STATUS = 'MatchmakingStarted'
|
||||
MATCHMAKING_SUCCEEDED_STATUS = 'MatchmakingSucceeded'
|
||||
MATCHMAKING_TIMED_OUT_STATUS = 'MatchmakingTimedOut'
|
||||
MATCHMAKING_CANCELLED_STATUS = 'MatchmakingCancelled'
|
||||
MATCHMAKING_FAILED_STATUS = 'MatchmakingFailed'
|
||||
|
||||
|
||||
def handler(event, context):
|
||||
"""
|
||||
Handles requests to start games from the game client.
|
||||
This function records the game request from the client in the MatchmakingRequest table and calls
|
||||
GameLift to start matchmaking.
|
||||
:param event: lambda event, contains the region to player latency mapping in `regionToLatencyMapping` key, as well
|
||||
as the player information from the Cognito id tokens.
|
||||
:param context: lambda context, not used by this function
|
||||
:return:
|
||||
- 202 (Accepted) if the matchmaking request is accepted and is now being processed
|
||||
- 409 (Conflict) if the another matchmaking request is in progress
|
||||
- 500 (Internal Error) if error occurred when calling GameLift to start matchmaking
|
||||
"""
|
||||
player_id = event["requestContext"]["authorizer"]["claims"]["sub"]
|
||||
start_time = round(time.time())
|
||||
print(f'Handling start game request. PlayerId: {player_id}, StartTime: {start_time}')
|
||||
|
||||
region_to_latency_mapping = get_region_to_latency_mapping(event)
|
||||
if region_to_latency_mapping:
|
||||
print(f"Region to latency mapping: {region_to_latency_mapping}")
|
||||
else:
|
||||
print("No regionToLatencyMapping mapping provided")
|
||||
|
||||
matchmaking_request_table_name = os.environ['MatchmakingRequestTableName']
|
||||
team_name = os.environ['TeamName']
|
||||
matchmaking_configuration_name = os.environ['MatchmakingConfigurationName']
|
||||
|
||||
dynamodb = boto3.resource('dynamodb')
|
||||
matchmaking_request_table = dynamodb.Table(matchmaking_request_table_name)
|
||||
|
||||
gamelift = boto3.client('gamelift')
|
||||
|
||||
matchmaking_requests = matchmaking_request_table.query(
|
||||
KeyConditionExpression=Key('PlayerId').eq(player_id),
|
||||
ScanIndexForward=False
|
||||
)
|
||||
|
||||
if matchmaking_requests['Count'] > 0 \
|
||||
and not is_matchmaking_request_terminal(matchmaking_requests['Items'][0]):
|
||||
# A existing matchmaking request in progress
|
||||
return {
|
||||
'headers': {
|
||||
'Content-Type': 'text/plain'
|
||||
},
|
||||
'statusCode': 409 # Conflict
|
||||
}
|
||||
|
||||
try:
|
||||
player = {
|
||||
'PlayerId': player_id,
|
||||
'Team': team_name
|
||||
}
|
||||
if region_to_latency_mapping:
|
||||
player['LatencyInMs'] = region_to_latency_mapping
|
||||
|
||||
start_matchmaking_request = {
|
||||
"ConfigurationName": matchmaking_configuration_name,
|
||||
"Players": [player]
|
||||
}
|
||||
print(f"Starting matchmaking in GameLift. Request: {start_matchmaking_request}")
|
||||
start_matchmaking_result = gamelift.start_matchmaking(**start_matchmaking_request)
|
||||
|
||||
ticket_id = start_matchmaking_result['MatchmakingTicket']['TicketId']
|
||||
ticket_status = MATCHMAKING_STARTED_STATUS
|
||||
|
||||
matchmaking_request_table.put_item(
|
||||
Item={
|
||||
'PlayerId': player_id,
|
||||
'StartTime': start_time,
|
||||
'LastUpdatedTime': start_time,
|
||||
'ExpirationTime': start_time + DEFAULT_TTL_IN_SECONDS,
|
||||
'TicketStatus': ticket_status,
|
||||
'TicketId': ticket_id
|
||||
}
|
||||
)
|
||||
|
||||
return {
|
||||
# Matchmaking request enqueued
|
||||
'headers': {
|
||||
'Content-Type': 'text/plain'
|
||||
},
|
||||
'statusCode': 202
|
||||
}
|
||||
except Exception as ex:
|
||||
print(f'Error occurred when calling GameLift to start matchmaking. Exception: {ex}')
|
||||
return {
|
||||
# Error occurred when enqueuing matchmaking request
|
||||
'headers': {
|
||||
'Content-Type': 'text/plain'
|
||||
},
|
||||
'statusCode': 500
|
||||
}
|
||||
|
||||
|
||||
def is_matchmaking_request_terminal(matchmaking_request):
|
||||
return matchmaking_request['TicketStatus'] in [
|
||||
MATCHMAKING_SUCCEEDED_STATUS,
|
||||
MATCHMAKING_TIMED_OUT_STATUS,
|
||||
MATCHMAKING_CANCELLED_STATUS,
|
||||
MATCHMAKING_FAILED_STATUS
|
||||
]
|
||||
|
||||
|
||||
def get_region_to_latency_mapping(event):
|
||||
request_body = event.get("body")
|
||||
if not request_body:
|
||||
return None
|
||||
|
||||
try:
|
||||
request_body_json = json.loads(request_body)
|
||||
except ValueError:
|
||||
print(f"Error parsing request body: {request_body}")
|
||||
return None
|
||||
|
||||
if request_body_json and request_body_json.get('regionToLatencyMapping'):
|
||||
return request_body_json.get('regionToLatencyMapping')
|
||||
@@ -0,0 +1,107 @@
|
||||
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import boto3
|
||||
from boto3.dynamodb.conditions import Key
|
||||
import os
|
||||
import json
|
||||
import time
|
||||
|
||||
MATCHMAKING_STARTED_STATUS = 'MatchmakingStarted'
|
||||
MATCHMAKING_SUCCEEDED_STATUS = 'MatchmakingSucceeded'
|
||||
MATCHMAKING_TIMED_OUT_STATUS = 'MatchmakingTimedOut'
|
||||
MATCHMAKING_CANCELLED_STATUS = 'MatchmakingCancelled'
|
||||
MATCHMAKING_FAILED_STATUS = 'MatchmakingFailed'
|
||||
|
||||
|
||||
def handler(event, context):
|
||||
"""
|
||||
Handles game session event from GameLift FlexMatch. This function parses the event messages and updates all
|
||||
related requests in the the MatchmakingPlacement DynamoDB table,
|
||||
which will be looked up to fulfill game client's Results Requests.
|
||||
:param event: lambda event containing game session event from GameLift queue
|
||||
:param context: lambda context, not used by this function
|
||||
:return: None
|
||||
"""
|
||||
lambda_start_time = round(time.time())
|
||||
message = json.loads(event['Records'][0]['Sns']['Message'])
|
||||
print(f'Handling FlexMatch event. StartTime: {lambda_start_time}. Message: {message}')
|
||||
|
||||
status_type = message['detail']['type']
|
||||
|
||||
if status_type not in [MATCHMAKING_SUCCEEDED_STATUS, MATCHMAKING_TIMED_OUT_STATUS, MATCHMAKING_CANCELLED_STATUS,
|
||||
MATCHMAKING_FAILED_STATUS]:
|
||||
print(f'Received non-terminal status type: {status_type}. Skip processing.')
|
||||
return
|
||||
|
||||
tickets = message['detail']['tickets']
|
||||
ip_address = message['detail']['gameSessionInfo'].get('ipAddress')
|
||||
dns_name = message['detail']['gameSessionInfo'].get('dnsName')
|
||||
port = str(message['detail']['gameSessionInfo'].get('port'))
|
||||
game_session_arn = str(message['detail']['gameSessionInfo'].get('gameSessionArn'))
|
||||
players = message['detail']['gameSessionInfo']['players']
|
||||
players_map = {player.get('playerId'):player.get('playerSessionId') for player in players}
|
||||
|
||||
ticket_id_index_name = os.environ['TicketIdIndexName']
|
||||
|
||||
matchmaking_request_table_name = os.environ['MatchmakingRequestTableName']
|
||||
dynamodb = boto3.resource('dynamodb')
|
||||
matchmaking_request_table = dynamodb.Table(matchmaking_request_table_name)
|
||||
|
||||
for ticket in tickets:
|
||||
ticket_id = ticket['ticketId']
|
||||
matchmaking_requests = matchmaking_request_table.query(
|
||||
IndexName=ticket_id_index_name,
|
||||
KeyConditionExpression=Key('TicketId').eq(ticket_id)
|
||||
)
|
||||
|
||||
if matchmaking_requests['Count'] <= 0:
|
||||
print(f"Cannot find matchmaking request with ticket id: {ticket_id}")
|
||||
continue
|
||||
|
||||
matchmaking_request_status = matchmaking_requests['Items'][0]['TicketStatus']
|
||||
player_id = matchmaking_requests['Items'][0]['PlayerId']
|
||||
player_session_id = players_map.get(player_id)
|
||||
print(f'Processing Ticket: {ticket_id}, PlayerId: {player_id}, PlayerSessionId: {player_session_id}')
|
||||
matchmaking_request_start_time = matchmaking_requests['Items'][0]['StartTime']
|
||||
|
||||
if matchmaking_request_status != MATCHMAKING_STARTED_STATUS:
|
||||
print(f"Unexpected TicketStatus on matchmaking request. Expected: 'MatchmakingStarted'. "
|
||||
f"Found: {matchmaking_request_status}")
|
||||
continue
|
||||
|
||||
attribute_updates = {
|
||||
'TicketStatus': {
|
||||
'Value': status_type
|
||||
},
|
||||
'LastUpdatedTime': {
|
||||
'Value': lambda_start_time
|
||||
}
|
||||
}
|
||||
|
||||
if status_type == MATCHMAKING_SUCCEEDED_STATUS:
|
||||
attribute_updates.update({
|
||||
'IpAddress': {
|
||||
'Value': ip_address
|
||||
},
|
||||
'DnsName': {
|
||||
'Value': dns_name
|
||||
},
|
||||
'Port': {
|
||||
'Value': port
|
||||
},
|
||||
'GameSessionArn': {
|
||||
'Value': game_session_arn
|
||||
},
|
||||
'PlayerSessionId': {
|
||||
'Value': player_session_id
|
||||
}
|
||||
})
|
||||
|
||||
matchmaking_request_table.update_item(
|
||||
Key={
|
||||
'PlayerId': player_id,
|
||||
'StartTime': matchmaking_request_start_time
|
||||
},
|
||||
AttributeUpdates=attribute_updates
|
||||
)
|
||||
@@ -0,0 +1,84 @@
|
||||
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import boto3
|
||||
import os
|
||||
import json
|
||||
from boto3.dynamodb.conditions import Key
|
||||
|
||||
MATCHMAKING_STARTED_STATUS = 'MatchmakingStarted'
|
||||
MATCHMAKING_SUCCEEDED_STATUS = 'MatchmakingSucceeded'
|
||||
|
||||
|
||||
def handler(event, context):
|
||||
"""
|
||||
Handles requests to describe the game session connection information after a StartGame request.
|
||||
This function will look up the MatchmakingRequest table to find the latest matchmaking request
|
||||
by the player, and return its game connection information if any.
|
||||
:param event: lambda event, contains the region to player latency mapping in `regionToLatencyMapping` key, as well
|
||||
as the player information from the Cognito id tokens. Cognito provides a `sub` user attribute that we use as a
|
||||
Player ID. Unlike `username` value `sub` is UUID for a user which is never reassigned to another user.
|
||||
:param context: lambda context, not used by this function
|
||||
:return:
|
||||
- 200 (OK) if the game connection is ready, along with server info: "IpAddress", "Port", "DnsName", "PlayerSessionId", "PlayerId", "PlayerSessionId", "PlayerId"
|
||||
- 204 (No Content) if the requested game is still in progress of matchmaking
|
||||
- 404 (Not Found) if no game has been started by the player, or if all started game were expired
|
||||
- 500 (Internal Error) if errors occurred during matchmaking or placement
|
||||
"""
|
||||
player_id = event["requestContext"]["authorizer"]["claims"]["sub"]
|
||||
print(f'Handling request result request. PlayerId: {player_id}')
|
||||
|
||||
matchmaking_request_table_name = os.environ['MatchmakingRequestTableName']
|
||||
|
||||
dynamodb = boto3.resource('dynamodb')
|
||||
matchmaking_request_table = dynamodb.Table(matchmaking_request_table_name)
|
||||
|
||||
matchmaking_requests = matchmaking_request_table.query(
|
||||
KeyConditionExpression=Key('PlayerId').eq(player_id),
|
||||
ScanIndexForward=False
|
||||
)
|
||||
|
||||
if matchmaking_requests['Count'] <= 0:
|
||||
return {
|
||||
'headers': {
|
||||
'Content-Type': 'text/plain'
|
||||
},
|
||||
'statusCode': 404
|
||||
}
|
||||
|
||||
latest_matchmaking_request = matchmaking_requests['Items'][0]
|
||||
|
||||
print(f'Current Matchmaking Request: {latest_matchmaking_request}')
|
||||
|
||||
matchmaking_request_status = latest_matchmaking_request['TicketStatus']
|
||||
|
||||
if matchmaking_request_status == MATCHMAKING_STARTED_STATUS:
|
||||
# still waiting for ticket to be processed
|
||||
return {
|
||||
'headers': {
|
||||
'Content-Type': 'text/plain'
|
||||
},
|
||||
'statusCode': 204
|
||||
}
|
||||
elif matchmaking_request_status == MATCHMAKING_SUCCEEDED_STATUS:
|
||||
game_session_connection_info = \
|
||||
dict((k, latest_matchmaking_request[k]) for k in ('IpAddress', 'Port', 'DnsName', 'PlayerSessionId', 'PlayerId', 'GameSessionArn'))
|
||||
print(f"Connection info: {game_session_connection_info}")
|
||||
|
||||
return {
|
||||
'body': json.dumps(game_session_connection_info),
|
||||
'headers': {
|
||||
'Content-Type': 'text/plain'
|
||||
},
|
||||
'statusCode': 200
|
||||
}
|
||||
else:
|
||||
# We count MatchmakingCancelled as internal error also because cancelling placement requests is not
|
||||
# in the current implementation, so it should never happen.
|
||||
print(f'Received non-successful terminal status {matchmaking_request_status}, responding with 500 error.')
|
||||
return {
|
||||
'headers': {
|
||||
'Content-Type': 'text/plain'
|
||||
},
|
||||
'statusCode': 500
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
#
|
||||
# GameLift Region Mappings
|
||||
# Keep synchronized with: https://docs.aws.amazon.com/general/latest/gr/gamelift.html
|
||||
#
|
||||
# ----------------------------------------------------------------------------------------
|
||||
# Region Name Region Endpoint Protocol
|
||||
# ----------------------------------------------------------------------------------------
|
||||
# US East (Ohio) us-east-2 gamelift.us-east-2.amazonaws.com HTTPS
|
||||
# US East (N. Virginia) us-east-1 gamelift.us-east-1.amazonaws.com HTTPS
|
||||
# US West (N. California) us-west-1 gamelift.us-west-1.amazonaws.com HTTPS
|
||||
# US West (Oregon) us-west-2 gamelift.us-west-2.amazonaws.com HTTPS
|
||||
# Asia Pacific (Mumbai) ap-south-1 gamelift.ap-south-1.amazonaws.com HTTPS
|
||||
# Asia Pacific (Seoul) ap-northeast-2 gamelift.ap-northeast-2.amazonaws.com HTTPS
|
||||
# Asia Pacific (Singapore) ap-southeast-1 gamelift.ap-southeast-1.amazonaws.com HTTPS
|
||||
# Asia Pacific (Sydney) ap-southeast-2 gamelift.ap-southeast-2.amazonaws.com HTTPS
|
||||
# Asia Pacific (Tokyo) ap-northeast-1 gamelift.ap-northeast-1.amazonaws.com HTTPS
|
||||
# Canada (Central) ca-central-1 gamelift.ca-central-1.amazonaws.com HTTPS
|
||||
# Europe (Frankfurt) eu-central-1 gamelift.eu-central-1.amazonaws.com HTTPS
|
||||
# Europe (Ireland) eu-west-1 gamelift.eu-west-1.amazonaws.com HTTPS
|
||||
# Europe (London) eu-west-2 gamelift.eu-west-2.amazonaws.com HTTPS
|
||||
# South America (São Paulo) sa-east-1 gamelift.sa-east-1.amazonaws.com HTTPS
|
||||
# ----------------------------------------------------------------------------------------
|
||||
|
||||
{
|
||||
five_letter_region_codes: {
|
||||
us-east-2 : usea2,
|
||||
us-east-1 : usea1,
|
||||
us-west-1 : uswe1,
|
||||
us-west-2 : uswe2,
|
||||
ap-south-1 : apso1,
|
||||
ap-northeast-2 : apne2,
|
||||
ap-southeast-1 : apse1,
|
||||
ap-southeast-2 : apse2,
|
||||
ap-northeast-1 : apne1,
|
||||
ca-central-1 : cace1,
|
||||
eu-central-1 : euce1,
|
||||
eu-west-1 : euwe1,
|
||||
eu-west-2 : euwe2,
|
||||
sa-east-1 : saea1
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,217 @@
|
||||
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import string
|
||||
import boto3
|
||||
import requests
|
||||
import json
|
||||
import time
|
||||
import argparse
|
||||
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("-g", "--game", help="game name", type=str, required=True)
|
||||
parser.add_argument("-r", "--region", help="region name, e.g. eu-west-1", type=str, required=True)
|
||||
parser.add_argument("-p", "--profile", help="profile name in the AWS shared credentials file ~/.aws/credentials", type=str, required=True)
|
||||
args = parser.parse_args()
|
||||
|
||||
GAME_NAME = args.game.lower() # e.g. 'GameLiftSampleGame5ue4'
|
||||
REGION = args.region # e.g. 'eu-west-1'
|
||||
PROFILE_NAME = args.profile
|
||||
|
||||
USER_POOL_NAME = GAME_NAME + 'UserPool'
|
||||
USER_POOL_CLIENT_NAME = GAME_NAME + 'UserPoolClient'
|
||||
USERNAME1 = 'testuser1@example.com'
|
||||
USERNAME2 = 'testuser2@example.com'
|
||||
USERNAMES = [USERNAME1, USERNAME2]
|
||||
PASSWORD = 'TestPassw0rd.'
|
||||
REST_API_NAME = GAME_NAME + 'RestApi'
|
||||
REST_API_STAGE = 'dev'
|
||||
GAME_REQUEST_PATH = 'start_game'
|
||||
RESULTS_REQUEST_PATH = 'get_game_connection'
|
||||
session = boto3.Session(profile_name=PROFILE_NAME)
|
||||
cognito_idp = session.client('cognito-idp', region_name=REGION)
|
||||
apig = session.client('apigateway', region_name=REGION)
|
||||
REGION_US_WEST_2 = 'us-west-2'
|
||||
REGION_EU_WEST_1 = 'eu-west-1'
|
||||
REGION_US_EAST_1 = 'us-east-1'
|
||||
NO_LATENCY = 'no-latency'
|
||||
REGIONS_TO_TEST = [REGION_US_WEST_2, REGION_EU_WEST_1, NO_LATENCY]
|
||||
US_WEST_2_FIRST_REGION_TO_LATENCY_MAPPING = {
|
||||
"regionToLatencyMapping": {
|
||||
"us-west-2": 50,
|
||||
"us-east-1": 100,
|
||||
"eu-west-1": 150,
|
||||
"ap-northeast-1": 300
|
||||
}
|
||||
}
|
||||
EU_WEST_1_FIRST_REGION_TO_LATENCY_MAPPING = {
|
||||
"regionToLatencyMapping": {
|
||||
"us-west-2": 50,
|
||||
"us-east-1": 100,
|
||||
"eu-west-1": 10,
|
||||
"ap-northeast-1": 300
|
||||
}
|
||||
}
|
||||
REGION_TO_GAME_REQUEST_PAYLOAD_MAPPING = {
|
||||
REGION_US_WEST_2: json.dumps(US_WEST_2_FIRST_REGION_TO_LATENCY_MAPPING),
|
||||
REGION_EU_WEST_1: json.dumps(EU_WEST_1_FIRST_REGION_TO_LATENCY_MAPPING),
|
||||
NO_LATENCY: None
|
||||
}
|
||||
REGION_TO_GAME_SESSION_ARN_EXPECTED_LOCATION = {
|
||||
REGION_US_WEST_2: REGION_US_WEST_2,
|
||||
REGION_EU_WEST_1: REGION_EU_WEST_1,
|
||||
NO_LATENCY: REGION_US_EAST_1
|
||||
}
|
||||
|
||||
|
||||
def main():
|
||||
|
||||
user_pool = find_user_pool(USER_POOL_NAME)
|
||||
user_pool_id = user_pool['Id']
|
||||
print("User Pool Id:", user_pool_id)
|
||||
|
||||
user_pool_client = find_user_pool_client(user_pool_id, USER_POOL_CLIENT_NAME)
|
||||
user_pool_client_id = user_pool_client['ClientId']
|
||||
print("User Pool Client Id:", user_pool_client_id)
|
||||
|
||||
try:
|
||||
for region in REGIONS_TO_TEST:
|
||||
game_request_payload = REGION_TO_GAME_REQUEST_PAYLOAD_MAPPING.get(region)
|
||||
expected_game_session_region = REGION_TO_GAME_SESSION_ARN_EXPECTED_LOCATION.get(region)
|
||||
|
||||
headers_list = []
|
||||
for username in USERNAMES:
|
||||
regional_username = get_regional_user_name(username, region)
|
||||
|
||||
cognito_idp.sign_up(
|
||||
ClientId=user_pool_client_id,
|
||||
Username=regional_username,
|
||||
Password=PASSWORD,
|
||||
)
|
||||
|
||||
print(f"Created user: {regional_username}")
|
||||
|
||||
cognito_idp.admin_confirm_sign_up(
|
||||
UserPoolId=user_pool_id,
|
||||
Username=regional_username,
|
||||
)
|
||||
|
||||
init_auth_result = cognito_idp.initiate_auth(
|
||||
AuthFlow='USER_PASSWORD_AUTH',
|
||||
AuthParameters={
|
||||
'USERNAME': regional_username,
|
||||
'PASSWORD': PASSWORD,
|
||||
},
|
||||
ClientId=user_pool_client_id
|
||||
)
|
||||
|
||||
assert init_auth_result['ResponseMetadata']['HTTPStatusCode'] == 200, "Unsuccessful init_auth"
|
||||
print(f"Authenticated via username and password for {regional_username}")
|
||||
|
||||
id_token = init_auth_result['AuthenticationResult']['IdToken']
|
||||
headers = {
|
||||
'Auth': id_token
|
||||
}
|
||||
headers_list.append(headers)
|
||||
|
||||
results_request_url = get_rest_api_endpoint(REST_API_NAME, REGION, REST_API_STAGE, RESULTS_REQUEST_PATH)
|
||||
game_request_url = get_rest_api_endpoint(REST_API_NAME, REGION, REST_API_STAGE, GAME_REQUEST_PATH)
|
||||
|
||||
print(f"results_request_url: {results_request_url}")
|
||||
print(f"game_request_url: {game_request_url}")
|
||||
|
||||
for headers in headers_list:
|
||||
results_request_response = requests.post(url=results_request_url, headers=headers)
|
||||
assert results_request_response.status_code == 404, \
|
||||
f"Expect 'POST /get_game_connection' status code to be 404 (Not Found). Actual: " \
|
||||
f"{str(results_request_response.status_code)}"
|
||||
print("Verified lambda ResultsRequest response", results_request_response)
|
||||
|
||||
game_request_response = requests.post(url=game_request_url, headers=headers, data=game_request_payload)
|
||||
|
||||
print(f"Game request response '{game_request_response}'")
|
||||
|
||||
assert game_request_response.status_code == 202, \
|
||||
f"Expect 'POST /start_game' status code to be 202 (Accepted), actual: " \
|
||||
f"{str(game_request_response.status_code)}"
|
||||
print("Verified lambda GameRequest response", game_request_response)
|
||||
|
||||
print("Waiting for matchmaking request to be processed...")
|
||||
|
||||
verified_players = 0
|
||||
while verified_players != len(headers_list):
|
||||
verified_players = 0
|
||||
time.sleep(10) # 10 seconds
|
||||
for headers in headers_list:
|
||||
results_request_response = requests.post(url=results_request_url, headers=headers)
|
||||
if results_request_response.status_code == 204:
|
||||
print("Match is not ready yet")
|
||||
continue
|
||||
assert results_request_response.status_code == 200, \
|
||||
f"Expect 'POST /get_game_connection' status code to be 200 (Success), actual: " \
|
||||
f"{str(results_request_response.status_code)}"
|
||||
print("Verified lambda ResultsRequest response", results_request_response)
|
||||
|
||||
game_connection_info = json.loads(results_request_response.content)
|
||||
|
||||
print(f"Game connection info '{game_connection_info}'")
|
||||
|
||||
assert game_connection_info['IpAddress'] != ''
|
||||
assert int(game_connection_info['Port']) > 0
|
||||
assert REGION in game_connection_info['DnsName'], \
|
||||
f"Expect {game_connection_info['DnsName']} to contain '{REGION}'"
|
||||
assert expected_game_session_region in game_connection_info['GameSessionArn'], \
|
||||
f"Expect {game_connection_info['GameSessionArn']} to contain '{expected_game_session_region}'"
|
||||
assert "psess-" in game_connection_info['PlayerSessionId'], \
|
||||
f"Expect {game_connection_info['PlayerSessionId']} to contain 'psess-'"
|
||||
print("Verified game connection info:", game_connection_info)
|
||||
verified_players += 1
|
||||
print(f"{verified_players} players' game sessions verified")
|
||||
|
||||
finally:
|
||||
for region in REGIONS_TO_TEST:
|
||||
for username in USERNAMES:
|
||||
regional_username = get_regional_user_name(username, region)
|
||||
cognito_idp.admin_delete_user(
|
||||
UserPoolId=user_pool_id,
|
||||
Username=regional_username,
|
||||
)
|
||||
print("Deleted user:", regional_username)
|
||||
|
||||
print("Test Succeeded!")
|
||||
|
||||
|
||||
def get_regional_user_name(username, region):
|
||||
return f"{region}_{username}"
|
||||
|
||||
|
||||
def find_user_pool(user_pool_name):
|
||||
print("Finding user pool:", user_pool_name)
|
||||
result = cognito_idp.list_user_pools(MaxResults=50)
|
||||
pools = result['UserPools']
|
||||
return next(x for x in pools if x['Name'] == user_pool_name)
|
||||
|
||||
|
||||
def find_user_pool_client(user_pool_id, user_pool_client_name):
|
||||
print("Finding user pool client:", user_pool_client_name)
|
||||
results = cognito_idp.list_user_pool_clients(UserPoolId=user_pool_id)
|
||||
clients = results['UserPoolClients']
|
||||
return next(x for x in clients if x['ClientName'] == user_pool_client_name)
|
||||
|
||||
|
||||
def find_rest_api(rest_api_name):
|
||||
print("Finding rest api:", rest_api_name)
|
||||
results = apig.get_rest_apis()
|
||||
rest_apis = results['items']
|
||||
return next(x for x in rest_apis if x['name'] == rest_api_name)
|
||||
|
||||
|
||||
def get_rest_api_endpoint(rest_api_name, region, stage, path):
|
||||
print("Getting rest api endpoint", rest_api_name)
|
||||
rest_api = find_rest_api(rest_api_name)
|
||||
rest_api_id = rest_api['id']
|
||||
return f'https://{rest_api_id}.execute-api.{region}.amazonaws.com/{stage}/{path}'
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,43 @@
|
||||
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
#
|
||||
# GameLift Region Mappings
|
||||
# Keep synchronized with: https://docs.aws.amazon.com/general/latest/gr/gamelift.html
|
||||
#
|
||||
# ----------------------------------------------------------------------------------------
|
||||
# Region Name Region Endpoint Protocol
|
||||
# ----------------------------------------------------------------------------------------
|
||||
# US East (Ohio) us-east-2 gamelift.us-east-2.amazonaws.com HTTPS
|
||||
# US East (N. Virginia) us-east-1 gamelift.us-east-1.amazonaws.com HTTPS
|
||||
# US West (N. California) us-west-1 gamelift.us-west-1.amazonaws.com HTTPS
|
||||
# US West (Oregon) us-west-2 gamelift.us-west-2.amazonaws.com HTTPS
|
||||
# Asia Pacific (Mumbai) ap-south-1 gamelift.ap-south-1.amazonaws.com HTTPS
|
||||
# Asia Pacific (Seoul) ap-northeast-2 gamelift.ap-northeast-2.amazonaws.com HTTPS
|
||||
# Asia Pacific (Singapore) ap-southeast-1 gamelift.ap-southeast-1.amazonaws.com HTTPS
|
||||
# Asia Pacific (Sydney) ap-southeast-2 gamelift.ap-southeast-2.amazonaws.com HTTPS
|
||||
# Asia Pacific (Tokyo) ap-northeast-1 gamelift.ap-northeast-1.amazonaws.com HTTPS
|
||||
# Canada (Central) ca-central-1 gamelift.ca-central-1.amazonaws.com HTTPS
|
||||
# Europe (Frankfurt) eu-central-1 gamelift.eu-central-1.amazonaws.com HTTPS
|
||||
# Europe (Ireland) eu-west-1 gamelift.eu-west-1.amazonaws.com HTTPS
|
||||
# Europe (London) eu-west-2 gamelift.eu-west-2.amazonaws.com HTTPS
|
||||
# South America (São Paulo) sa-east-1 gamelift.sa-east-1.amazonaws.com HTTPS
|
||||
# ----------------------------------------------------------------------------------------
|
||||
|
||||
{
|
||||
five_letter_region_codes: {
|
||||
us-east-2 : usea2,
|
||||
us-east-1 : usea1,
|
||||
us-west-1 : uswe1,
|
||||
us-west-2 : uswe2,
|
||||
ap-south-1 : apso1,
|
||||
ap-northeast-2 : apne2,
|
||||
ap-southeast-1 : apse1,
|
||||
ap-southeast-2 : apse2,
|
||||
ap-northeast-1 : apne1,
|
||||
ca-central-1 : cace1,
|
||||
eu-central-1 : euce1,
|
||||
eu-west-1 : euwe1,
|
||||
eu-west-2 : euwe2,
|
||||
sa-east-1 : saea1
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
$ErrorActionPreference="Stop"
|
||||
echo "Running aws ecr get-login-password --region {{REGION}} --profile {{PROFILE_NAME}} | docker login --username AWS --password-stdin {{ECR_REGISTRY_URL}}"
|
||||
aws ecr get-login-password --region {{REGION}} --profile {{PROFILE_NAME}} | docker login --username AWS --password-stdin {{ECR_REGISTRY_URL}}
|
||||
if (!$LastExitCode -eq 0) {
|
||||
echo "Docker login has failed."
|
||||
exit $LastExitCode
|
||||
}
|
||||
echo "Running docker tag {{IMAGE_ID}} {{ECR_REPO_URI}}:{{IMAGE_TAG}}"
|
||||
docker tag {{IMAGE_ID}} {{ECR_REPO_URI}}:{{IMAGE_TAG}}
|
||||
if (!$LastExitCode -eq 0) {
|
||||
echo "Docker tag has failed."
|
||||
exit $LastExitCode
|
||||
}
|
||||
echo "Running docker push {{ECR_REPO_URI}}:{{IMAGE_TAG}}"
|
||||
docker push {{ECR_REPO_URI}}:{{IMAGE_TAG}}
|
||||
if ($LastExitCode -eq 0)
|
||||
{
|
||||
echo "ECR Container image setup succeeded."
|
||||
}
|
||||
else
|
||||
{
|
||||
echo "ECR Container image setup has failed."
|
||||
}
|
||||
|
||||
61
Plugins/GameLiftPlugin/Resources/Containers/SampleDockerfile
Normal file
61
Plugins/GameLiftPlugin/Resources/Containers/SampleDockerfile
Normal file
@@ -0,0 +1,61 @@
|
||||
# Base image
|
||||
# ----------
|
||||
# Add the base image that you want to use over here,
|
||||
# Make sure to use an image with the same architecture as the
|
||||
# Instance type you are planning to use on your fleets.
|
||||
FROM public.ecr.aws/amazonlinux/amazonlinux
|
||||
|
||||
# Game build directory
|
||||
# --------------------
|
||||
# Add your game build directory in the 'GAME_BUILD_DIRECTORY' env variable below.
|
||||
#
|
||||
# Game executable
|
||||
# ---------------
|
||||
# Add the relative path to your executable in the 'GAME_EXECUTABLE' env variable below.
|
||||
# The game build provided over here needs to be integrated with gamelift server sdk.
|
||||
# This template assumes that the executable path is relative to the game build directory.
|
||||
#
|
||||
# Launch parameters
|
||||
# -----------------
|
||||
# Add any launch parameters to pass into your executable in the 'LAUNCH_PARAMS' env variable below.
|
||||
#
|
||||
# Default directory
|
||||
# -----------------
|
||||
# The value provided in 'HOME_DIR' below will be where the game executable and logs exist.
|
||||
#
|
||||
ARG GAME_BUILD_DIRECTORY
|
||||
ARG GAME_EXECUTABLE
|
||||
ARG LAUNCH_PARAMS
|
||||
|
||||
ENV GAME_BUILD_DIRECTORY=$GAME_BUILD_DIRECTORY \
|
||||
GAME_EXECUTABLE=$GAME_EXECUTABLE \
|
||||
LAUNCH_PARAMS=$LAUNCH_PARAMS \
|
||||
HOME_DIR="/local/game"
|
||||
|
||||
|
||||
# install dependencies as necessary
|
||||
RUN yum install -y shadow-utils
|
||||
|
||||
RUN mkdir -p $HOME_DIR
|
||||
COPY ./gamebuild/$GAME_BUILD_DIRECTORY/ $HOME_DIR
|
||||
|
||||
# Change directory to home
|
||||
WORKDIR $HOME_DIR
|
||||
|
||||
RUN useradd -m gamescale && \
|
||||
echo "gamescale ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers && \
|
||||
chown -R gamescale:gamescale $HOME_DIR
|
||||
|
||||
# Add permissions to game build
|
||||
RUN chmod +x ./$GAME_EXECUTABLE
|
||||
|
||||
USER gamescale
|
||||
|
||||
# Check directory before starting the container
|
||||
RUN ls -lhrt .
|
||||
|
||||
# Check path before starting the container
|
||||
RUN echo $PATH
|
||||
|
||||
# Start the game build
|
||||
ENTRYPOINT ["/bin/sh", "-c", "./$GAME_EXECUTABLE", "$LAUNCH_PARAMS"]
|
||||
@@ -0,0 +1,39 @@
|
||||
$ErrorActionPreference="Stop"
|
||||
echo "Running aws ecr get-login-password --region {{REGION}} --profile {{PROFILE_NAME}} | docker login --username AWS --password-stdin {{ECR_REGISTRY_URL}}"
|
||||
aws ecr get-login-password --region {{REGION}} --profile {{PROFILE_NAME}} | docker login --username AWS --password-stdin {{ECR_REGISTRY_URL}}
|
||||
if (!$LastExitCode -eq 0) {
|
||||
echo "Docker login has failed."
|
||||
exit $LastExitCode
|
||||
}
|
||||
|
||||
echo "Running docker ps"
|
||||
docker ps
|
||||
if (!$LastExitCode -eq 0) {
|
||||
echo "Docker ps has failed. Please start Docker Desktop and try again."
|
||||
exit $LastExitCode
|
||||
}
|
||||
|
||||
$docker_build_cmd = 'docker build -t {{REPO_NAME}}:{{IMAGE_TAG}} "{{IMAGE_PATH}}" --progress=plain --build-arg GAME_BUILD_DIRECTORY="{{GAME_BUILD_DIRECTORY}}" --build-arg GAME_EXECUTABLE="{{GAME_EXECUTABLE}}"'
|
||||
echo "Running: $docker_build_cmd"
|
||||
Invoke-Expression $docker_build_cmd
|
||||
if (!$LastExitCode -eq 0) {
|
||||
echo "Docker build has failed."
|
||||
exit $LastExitCode
|
||||
}
|
||||
echo "Running docker tag {{REPO_NAME}}:{{IMAGE_TAG}} {{ECR_REPO_URI}}:{{IMAGE_TAG}}"
|
||||
docker tag {{REPO_NAME}}:{{IMAGE_TAG}} {{ECR_REPO_URI}}:{{IMAGE_TAG}}
|
||||
if (!$LastExitCode -eq 0) {
|
||||
echo "Docker tag has failed."
|
||||
exit $LastExitCode
|
||||
}
|
||||
echo "Running docker push {{ECR_REPO_URI}}:{{IMAGE_TAG}}"
|
||||
docker push {{ECR_REPO_URI}}:{{IMAGE_TAG}}
|
||||
if ($LastExitCode -eq 0)
|
||||
{
|
||||
echo "ECR Container image setup succeeded."
|
||||
}
|
||||
else
|
||||
{
|
||||
echo "ECR Container image setup has failed."
|
||||
}
|
||||
|
||||
BIN
Plugins/GameLiftPlugin/Resources/Icon128.png
Normal file
BIN
Plugins/GameLiftPlugin/Resources/Icon128.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.6 KiB |
Reference in New Issue
Block a user