Lesson 35 - Get Compute Auth Token Working
78
Plugins/GameLiftPlugin/.gitignore
vendored
Normal file
@@ -0,0 +1,78 @@
|
||||
# Visual Studio user specific files
|
||||
.vs/
|
||||
*.vcxproj.filters
|
||||
|
||||
# Compiled Object files
|
||||
*.slo
|
||||
*.lo
|
||||
*.o
|
||||
*.obj
|
||||
|
||||
# Precompiled Headers
|
||||
*.gch
|
||||
*.pch
|
||||
|
||||
# Compiled Dynamic libraries
|
||||
#*.so
|
||||
#*.dylib
|
||||
#*.dll
|
||||
|
||||
# Fortran module files
|
||||
*.mod
|
||||
|
||||
# Compiled Static libraries
|
||||
*.lai
|
||||
*.la
|
||||
*.a
|
||||
#*.lib
|
||||
|
||||
# Executables
|
||||
*.exe
|
||||
*.out
|
||||
*.app
|
||||
*.ipa
|
||||
|
||||
# Backups
|
||||
*.bak
|
||||
|
||||
# These project files can be generated by the engine
|
||||
*.xcodeproj
|
||||
*.xcworkspace
|
||||
*.sln
|
||||
*.suo
|
||||
*.opensdf
|
||||
*.sdf
|
||||
*.VC.db
|
||||
*.VC.opendb
|
||||
|
||||
# Precompiled Assets
|
||||
SourceArt/**/*.png
|
||||
SourceArt/**/*.tga
|
||||
|
||||
# Binary Files
|
||||
Binaries/*
|
||||
Plugins/*/Binaries/*
|
||||
|
||||
# Builds
|
||||
Build/*
|
||||
|
||||
# Whitelist PakBlacklist-<BuildConfiguration>.txt files
|
||||
!Build/*/
|
||||
Build/*/**
|
||||
!Build/*/PakBlacklist*.txt
|
||||
|
||||
# Don't ignore icon files in Build
|
||||
!Build/**/*.ico
|
||||
|
||||
# Built data for maps
|
||||
*_BuiltData.uasset
|
||||
|
||||
# Configuration files generated by the Editor
|
||||
Saved/*
|
||||
|
||||
# Compiled source files for the engine to use
|
||||
Intermediate/*
|
||||
Plugins/*/Intermediate/*
|
||||
|
||||
# Cache files for the editor to use
|
||||
DerivedDataCache/*
|
||||
@@ -0,0 +1,58 @@
|
||||
; Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
||||
; SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
[CommonSettings]
|
||||
SourcePath=Plugins/GameLiftPlugin/Content/Localization/GameLiftPlugin
|
||||
DestinationPath=Plugins/GameLiftPlugin/Content/Localization/GameLiftPlugin
|
||||
ManifestName=GameLiftPlugin.manifest
|
||||
ArchiveName=GameLiftPlugin.archive
|
||||
PortableObjectName=GameLiftPlugin.po
|
||||
ResourceName=GameLiftPlugin.locres
|
||||
NativeCulture=en
|
||||
CulturesToGenerate=en
|
||||
CulturesToGenerate=fr
|
||||
CulturesToGenerate=de
|
||||
CulturesToGenerate=es
|
||||
CulturesToGenerate=es-419
|
||||
CulturesToGenerate=it
|
||||
CulturesToGenerate=ja
|
||||
CulturesToGenerate=ko
|
||||
CulturesToGenerate=ru
|
||||
CulturesToGenerate=zh-Hans
|
||||
CulturesToGenerate=ar
|
||||
CulturesToGenerate=pl
|
||||
CulturesToGenerate=pt-BR
|
||||
|
||||
;Gather text from source code
|
||||
[GatherTextStep0]
|
||||
CommandletClass=GatherTextFromSource
|
||||
SearchDirectoryPaths=Plugins/GameLiftPlugin/Source/
|
||||
FileNameFilters=*.cpp
|
||||
FileNameFilters=*.h
|
||||
FileNameFilters=*.c
|
||||
FileNameFilters=*.inl
|
||||
FileNameFilters=*.mm
|
||||
FileNameFilters=*.ini
|
||||
ShouldGatherFromEditorOnlyData=false
|
||||
|
||||
;Write Manifest
|
||||
[GatherTextStep1]
|
||||
CommandletClass=GenerateGatherManifest
|
||||
|
||||
;Write Archives
|
||||
[GatherTextStep2]
|
||||
CommandletClass=GenerateGatherArchive
|
||||
|
||||
;Import localized PO files
|
||||
[GatherTextStep3]
|
||||
CommandletClass=InternationalizationExport
|
||||
bImportLoc=true
|
||||
|
||||
;Write Localized Text Resource
|
||||
[GatherTextStep4]
|
||||
CommandletClass=GenerateTextLocalizationResource
|
||||
|
||||
;Export PO files
|
||||
[GatherTextStep5]
|
||||
CommandletClass=InternationalizationExport
|
||||
bExportLoc=true
|
||||
BIN
Plugins/GameLiftPlugin/Content/Editor/Icons/Account.png
Normal file
|
After Width: | Height: | Size: 6.2 KiB |
BIN
Plugins/GameLiftPlugin/Content/Editor/Icons/AccountMissing.png
Normal file
|
After Width: | Height: | Size: 7.1 KiB |
BIN
Plugins/GameLiftPlugin/Content/Editor/Icons/CloseX.png
Normal file
|
After Width: | Height: | Size: 338 B |
3
Plugins/GameLiftPlugin/Content/Editor/Icons/CloseX.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M10.773 2.2877L9.71231 1.22704L6 4.93935L2.28769 1.22704L1.22703 2.2877L4.93934 6.00001L1.22703 9.71232L2.28769 10.773L6 7.06067L9.71231 10.773L10.773 9.71232L7.06066 6.00001L10.773 2.2877Z" fill="#D5DBDB"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 360 B |
BIN
Plugins/GameLiftPlugin/Content/Editor/Icons/CopyPaste.png
Normal file
|
After Width: | Height: | Size: 221 B |
@@ -0,0 +1,3 @@
|
||||
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M13 11H11V13C11 13.5523 10.5523 14 10 14H1C0.447715 14 0 13.5523 0 13V4C0 3.44772 0.447715 3 1 3H3V1C3 0.447715 3.44772 0 4 0H13C13.5523 0 14 0.447715 14 1V10C14 10.5523 13.5523 11 13 11ZM5 2H12V9H11V4C11 3.44772 10.5523 3 10 3H5V2ZM2 12V5H9V12H2Z" fill="#C5C5C5"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 418 B |
BIN
Plugins/GameLiftPlugin/Content/Editor/Icons/Divider.png
Normal file
|
After Width: | Height: | Size: 138 B |
3
Plugins/GameLiftPlugin/Content/Editor/Icons/Divider.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="690" height="1" viewBox="0 0 690 1" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="690" height="1" fill="#C7C7C7" fill-opacity="0.2"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 168 B |
BIN
Plugins/GameLiftPlugin/Content/Editor/Icons/Documentation.png
Normal file
|
After Width: | Height: | Size: 2.2 KiB |
|
After Width: | Height: | Size: 14 KiB |
BIN
Plugins/GameLiftPlugin/Content/Editor/Icons/ExternalLink.png
Normal file
|
After Width: | Height: | Size: 205 B |
@@ -0,0 +1,5 @@
|
||||
<svg width="10" height="10" viewBox="0 0 10 10" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M0.625 9.375V0.625H4.125V1.5H1.5V8.5H8.5V5.875H9.375V9.375H0.625Z" fill="white"/>
|
||||
<path d="M9.375 4.5625V0.625H5.4375V1.5H8.5V4.5625H9.375Z" fill="white"/>
|
||||
<path d="M4.42288 4.95603L8.75391 0.625L9.37262 1.24372L5.0416 5.57475L4.42288 4.95603Z" fill="white"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 372 B |
BIN
Plugins/GameLiftPlugin/Content/Editor/Icons/FolderOpen.png
Normal file
|
After Width: | Height: | Size: 168 B |
@@ -0,0 +1,5 @@
|
||||
<svg width="11" height="11" viewBox="0 0 11 11" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect y="2.5" width="11" height="6" rx="1" fill="white"/>
|
||||
<rect y="2.5" width="6" height="3.42857" fill="white"/>
|
||||
<path d="M0 1.5V2H5.5L4.07323 0.910464C3.72458 0.644227 3.29807 0.5 2.8594 0.5H1C0.447715 0.5 0 0.947715 0 1.5Z" fill="white"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 345 B |
BIN
Plugins/GameLiftPlugin/Content/Editor/Icons/GameLiftAnywhere.png
Normal file
|
After Width: | Height: | Size: 1.8 KiB |
|
After Width: | Height: | Size: 1.2 KiB |
BIN
Plugins/GameLiftPlugin/Content/Editor/Icons/GameLiftLogo.png
Normal file
|
After Width: | Height: | Size: 4.5 KiB |
|
After Width: | Height: | Size: 1.8 KiB |
BIN
Plugins/GameLiftPlugin/Content/Editor/Icons/GameLiftTab.png
Normal file
|
After Width: | Height: | Size: 1.9 KiB |
BIN
Plugins/GameLiftPlugin/Content/Editor/Icons/GameLiftToolbar.png
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
BIN
Plugins/GameLiftPlugin/Content/Editor/Icons/Guidance.png
Normal file
|
After Width: | Height: | Size: 2.7 KiB |
4
Plugins/GameLiftPlugin/Content/Editor/Icons/Guidance.svg
Normal file
|
After Width: | Height: | Size: 12 KiB |
15
Plugins/GameLiftPlugin/Content/Editor/Icons/Icon.svg
Normal file
@@ -0,0 +1,15 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<svg viewBox="44.092 53.337 124.822 124.869" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M565 1245 c-43 -18 -76 -41 -116 -84 -37 -38 -68 -61 -89 -66 -46 -10 -116 -81 -129 -131 -10 -37 -15 -42 -46 -47 -56 -9 -120 -60 -149 -119 -67 -135 2 -297 142 -334 50 -12 62 -11 62 10 0 7 -22 18 -48 25 -50 12 -106 59 -132 111 -18 35 -17 129 3 166 23 45 80 91 129 105 24 6 48 13 54 14 6 2 14 22 18 45 9 54 56 103 116 121 25 7 49 19 54 27 25 42 84 94 132 118 157 77 356 -8 411 -175 l21 -64 52 -14 c28 -7 56 -11 61 -7 19 12 7 24 -38 37 -42 12 -47 18 -58 55 -55 183 -267 281 -450 207z" style="fill: rgb(255, 255, 255);" transform="matrix(0.1, 0, 0, -0.1, 43.074795, 180.319077)"/>
|
||||
<path d="M505 946 c-68 -22 -138 -62 -184 -105 -36 -35 -41 -44 -31 -56 11 -13 19 -9 60 29 156 146 379 152 548 16 65 -52 82 -60 82 -36 0 23 -88 94 -159 128 -59 29 -77 32 -171 35 -67 2 -119 -2 -145 -11z" style="fill: rgb(255, 255, 255);" transform="matrix(0.1, 0, 0, -0.1, 43.074795, 180.319077)"/>
|
||||
<path d="M1134 936 c-8 -22 3 -33 18 -18 9 9 9 15 0 24 -9 9 -13 7 -18 -6z" style="fill: rgb(255, 255, 255);" transform="matrix(0.1, 0, 0, -0.1, 43.074795, 180.319077)"/>
|
||||
<path d="M1176 891 c-4 -5 5 -31 19 -57 44 -83 31 -189 -32 -264 -29 -35 -114 -80 -151 -80 -17 0 -23 -5 -20 -17 3 -14 11 -17 38 -15 134 14 242 156 227 299 -8 77 -62 166 -81 134z" style="fill: rgb(255, 255, 255);" transform="matrix(0.1, 0, 0, -0.1, 43.074795, 180.319077)"/>
|
||||
<path d="M545 806 c-27 -7 -66 -21 -85 -30 -44 -23 -120 -84 -120 -97 0 -23 30 -18 61 12 19 17 59 43 89 58 47 22 68 26 145 26 79 0 97 -4 142 -28 29 -15 68 -41 88 -58 22 -20 41 -29 52 -25 14 6 12 11 -13 37 -45 47 -114 87 -179 104 -69 18 -111 18 -180 1z" style="fill: rgb(255, 255, 255);" transform="matrix(0.1, 0, 0, -0.1, 43.074795, 180.319077)"/>
|
||||
<path d="M567 679 c-53 -13 -107 -42 -141 -78 -26 -27 -27 -31 -12 -37 11 -4 27 2 44 19 51 47 92 62 172 62 79 0 129 -18 178 -65 22 -21 52 -21 52 0 0 19 -95 79 -151 94 -61 18 -85 18 -142 5z" style="fill: rgb(255, 255, 255);" transform="matrix(0.1, 0, 0, -0.1, 43.074795, 180.319077)"/>
|
||||
<path d="M620 535 l0 -65 -30 0 c-17 0 -47 7 -67 15 -110 46 -228 -18 -294 -159 -57 -122 -39 -244 41 -288 51 -27 90 -22 166 18 l67 35 136 -3 c132 -3 137 -4 180 -33 36 -24 54 -29 101 -30 49 0 63 4 85 25 85 78 70 246 -32 360 -62 69 -111 93 -177 87 -28 -2 -65 -10 -82 -17 -51 -22 -64 -10 -64 60 0 47 -3 60 -15 60 -12 0 -15 -14 -15 -65z m-119 -81 c17 -8 61 -18 99 -21 55 -5 82 -1 139 17 68 23 71 23 115 6 144 -55 228 -298 129 -375 -38 -30 -84 -26 -160 14 -66 35 -67 35 -194 35 -126 0 -128 0 -193 -35 -86 -46 -131 -47 -172 -4 -27 28 -29 36 -29 102 1 83 28 146 91 211 61 63 118 80 175 50z" style="fill: rgb(255, 255, 255);" transform="matrix(0.1, 0, 0, -0.1, 43.074795, 180.319077)"/>
|
||||
<path d="M810 375 c-17 -21 -2 -50 26 -50 14 0 19 7 19 30 0 34 -24 45 -45 20z" style="fill: rgb(255, 255, 255);" transform="matrix(0.1, 0, 0, -0.1, 43.074795, 180.319077)"/>
|
||||
<path d="M420 325 c0 -34 -1 -35 -41 -35 -33 0 -40 -3 -37 -17 2 -13 14 -19 41 -21 35 -3 37 -5 37 -38 0 -24 5 -34 15 -34 10 0 15 10 15 34 0 33 2 35 37 38 27 2 39 8 41 21 3 14 -4 17 -37 17 -40 0 -41 1 -41 35 0 24 -5 35 -15 35 -10 0 -15 -11 -15 -35z" style="fill: rgb(255, 255, 255);" transform="matrix(0.1, 0, 0, -0.1, 43.074795, 180.319077)"/>
|
||||
<path d="M714 296 c-10 -26 4 -48 28 -44 17 2 23 10 23 28 0 18 -6 26 -23 28 -13 2 -25 -3 -28 -12z" style="fill: rgb(255, 255, 255);" transform="matrix(0.1, 0, 0, -0.1, 43.074795, 180.319077)"/>
|
||||
<path d="M884 286 c-10 -26 4 -48 28 -44 17 2 23 10 23 28 0 18 -6 26 -23 28 -13 2 -25 -3 -28 -12z" style="fill: rgb(255, 255, 255);" transform="matrix(0.1, 0, 0, -0.1, 43.074795, 180.319077)"/>
|
||||
<path d="M794 206 c-10 -26 4 -48 28 -44 17 2 23 10 23 28 0 18 -6 26 -23 28 -13 2 -25 -3 -28 -12z" style="fill: rgb(255, 255, 255);" transform="matrix(0.1, 0, 0, -0.1, 43.074795, 180.319077)"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.8 KiB |
BIN
Plugins/GameLiftPlugin/Content/Editor/Icons/InProgressIcon.png
Normal file
|
After Width: | Height: | Size: 622 B |
@@ -0,0 +1,3 @@
|
||||
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M16 9C16 12.866 12.866 16 9 16C5.13401 16 2 12.866 2 9C2 5.13401 5.13401 2 9 2C12.866 2 16 5.13401 16 9ZM18 9C18 13.9706 13.9706 18 9 18C4.02944 18 0 13.9706 0 9C0 4.02944 4.02944 0 9 0C13.9706 0 18 4.02944 18 9ZM9 12C10.6569 12 12 10.6569 12 9C12 7.34315 10.6569 6 9 6C7.34315 6 6 7.34315 6 9C6 10.6569 7.34315 12 9 12Z" fill="#4C7EFF"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 491 B |
|
After Width: | Height: | Size: 141 B |
@@ -0,0 +1,3 @@
|
||||
<svg width="2" height="154" viewBox="0 0 2 154" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="2" height="154" fill="#4C7EFF"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 149 B |
|
After Width: | Height: | Size: 488 B |
@@ -0,0 +1,10 @@
|
||||
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_633_33456)">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M9 18C13.9706 18 18 13.9706 18 9C18 4.02944 13.9706 0 9 0C4.02944 0 0 4.02944 0 9C0 13.9706 4.02944 18 9 18ZM4.5 8.73537L6.02459 7.17601L8.18068 9.38128L11.9754 5.5L13.5 7.05936L8.18068 12.5L4.5 8.73537Z" fill="#4C7EFF"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_633_33456">
|
||||
<rect width="18" height="18" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 520 B |
|
After Width: | Height: | Size: 451 B |
@@ -0,0 +1,10 @@
|
||||
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_15082_4594)">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M9 18C13.9706 18 18 13.9706 18 9C18 4.02944 13.9706 0 9 0C4.02944 0 0 4.02944 0 9C0 13.9706 4.02944 18 9 18ZM9 7.40071L11.4007 5L13 6.59929L10.5993 9L13 11.4007L11.4007 13L9 10.5993L6.59929 13L5 11.4007L7.40071 9L5 6.59929L6.59929 5L9 7.40071Z" fill="#D13212"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_15082_4594">
|
||||
<rect width="18" height="18" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 562 B |
|
After Width: | Height: | Size: 143 B |
@@ -0,0 +1,3 @@
|
||||
<svg width="2" height="146" viewBox="0 0 2 146" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="2" height="146" fill="#AAB7B8"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 149 B |
|
After Width: | Height: | Size: 580 B |
@@ -0,0 +1,3 @@
|
||||
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect x="1" y="1" width="16" height="16" rx="8" stroke="#AAB7B8" stroke-width="2"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 187 B |
|
After Width: | Height: | Size: 434 B |
@@ -0,0 +1,10 @@
|
||||
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_15082_10615)">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M9 18C13.9706 18 18 13.9706 18 9C18 4.02944 13.9706 0 9 0C4.02944 0 0 4.02944 0 9C0 13.9706 4.02944 18 9 18ZM7.87988 10.25V4.25H10.1299V10.25H7.87988ZM10.1311 11.5H7.8811V13.75H10.1311V11.5Z" fill="#FFAA22"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_15082_10615">
|
||||
<rect width="18" height="18" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 511 B |
BIN
Plugins/GameLiftPlugin/Content/Editor/Icons/Reset.png
Normal file
|
After Width: | Height: | Size: 222 B |
3
Plugins/GameLiftPlugin/Content/Editor/Icons/Reset.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M9.00012 6C9.00012 4.34315 7.65698 3 6.00012 3L6.00012 2C8.20926 2 10.0001 3.79086 10.0001 6C10.0001 8.20914 8.20926 10 6.00012 10C3.79098 10 2.00012 8.20914 2.00014 6.00423C1.99025 4.83484 2.499 3.7432 3.35452 3L2.00012 3L2.00012 2L5.00012 2L5.00012 5L4.00012 5L4.00012 3.76395C3.36816 4.31827 2.99277 5.12926 3.00012 6C3.00012 7.65685 4.34327 9 6.00012 9C7.65698 9 9.00012 7.65685 9.00012 6Z" fill="white"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 562 B |
BIN
Plugins/GameLiftPlugin/Content/Editor/Icons/Start.png
Normal file
|
After Width: | Height: | Size: 178 B |
3
Plugins/GameLiftPlugin/Content/Editor/Icons/Start.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="7" height="8" viewBox="0 0 7 8" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M1.55582 0.340146C0.891358 -0.104126 0 0.372141 0 1.17145L0 6.82952C0 7.62792 0.889555 8.10437 1.55415 7.66194L5.79453 4.83905C6.38828 4.44378 6.38915 3.57179 5.7962 3.17533L1.55582 0.340146Z" fill="white"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 356 B |
BIN
Plugins/GameLiftPlugin/Content/Editor/Icons/StatusError.png
Normal file
|
After Width: | Height: | Size: 376 B |
@@ -0,0 +1,3 @@
|
||||
<svg width="10" height="10" viewBox="0 0 10 10" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M0 5C0 7.76142 2.23858 10 5 10C7.76142 10 10 7.76142 10 5C10 2.23858 7.76142 0 5 0C2.23858 0 0 2.23858 0 5ZM8.75 5C8.75 7.07107 7.07107 8.75 5 8.75C2.92893 8.75 1.25 7.07107 1.25 5C1.25 2.92893 2.92893 1.25 5 1.25C7.07107 1.25 8.75 2.92893 8.75 5ZM4.11612 5L2.78931 6.32681L3.67319 7.21069L5 5.88388L6.32681 7.21069L7.21069 6.32681L5.88388 5L7.21069 3.67319L6.32681 2.78931L5 4.11612L3.67319 2.78931L2.78931 3.67319L4.11612 5Z" fill="#D63F38"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 597 B |
BIN
Plugins/GameLiftPlugin/Content/Editor/Icons/StatusHelp.png
Normal file
|
After Width: | Height: | Size: 611 B |
@@ -0,0 +1,5 @@
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M0 10C0 15.5228 4.47715 20 10 20C15.5228 20 20 15.5228 20 10C20 4.47715 15.5228 0 10 0C4.47715 0 0 4.47715 0 10ZM17.5 10C17.5 14.1421 14.1421 17.5 10 17.5C5.85786 17.5 2.5 14.1421 2.5 10C2.5 5.85786 5.85786 2.5 10 2.5C14.1421 2.5 17.5 5.85786 17.5 10Z" fill="#E5E5E5"/>
|
||||
<path d="M8.125 12.5H10.625V15H8.125V12.5Z" fill="#E5E5E5"/>
|
||||
<path d="M10.625 7.5H6.875V5H13.125V11.25H11.875H8.125V8.75H10.625V7.5Z" fill="#E5E5E5"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 573 B |
BIN
Plugins/GameLiftPlugin/Content/Editor/Icons/StatusInactive.png
Normal file
|
After Width: | Height: | Size: 262 B |
@@ -0,0 +1,3 @@
|
||||
<svg width="9" height="9" viewBox="0 0 9 9" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M4 8.5C1.79086 8.5 0 6.70914 0 4.5C0 2.29086 1.79086 0.5 4 0.5C6.20914 0.5 8 2.29086 8 4.5C8 6.70914 6.20914 8.5 4 8.5ZM4 7.5C5.65685 7.5 7 6.15685 7 4.5C7 2.84315 5.65685 1.5 4 1.5C2.34315 1.5 1 2.84315 1 4.5C1 6.15685 2.34315 7.5 4 7.5ZM2 4H6V5H2V4Z" fill="white"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 416 B |
BIN
Plugins/GameLiftPlugin/Content/Editor/Icons/StatusInfo.png
Normal file
|
After Width: | Height: | Size: 633 B |
@@ -0,0 +1,3 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M0 12C0 18.6274 5.37258 24 12 24C18.6274 24 24 18.6274 24 12C24 5.37258 18.6274 0 12 0C5.37258 0 0 5.37258 0 12ZM21 12C21 16.9706 16.9706 21 12 21C7.02944 21 3 16.9706 3 12C3 7.02944 7.02944 3 12 3C16.9706 3 21 7.02944 21 12ZM10.5 13.5V15H9V18H10.5H13.5H15V15H13.5V10.5H12H10.5H9V13.5H10.5ZM13.5 9V6H10.5V9H13.5Z" fill="#0073BB"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 483 B |
BIN
Plugins/GameLiftPlugin/Content/Editor/Icons/StatusSuccess.png
Normal file
|
After Width: | Height: | Size: 370 B |
@@ -0,0 +1,3 @@
|
||||
<svg width="10" height="10" viewBox="0 0 10 10" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M5 10C2.23858 10 0 7.76142 0 5C0 2.23858 2.23858 0 5 0C7.76142 0 10 2.23858 10 5C10 7.76142 7.76142 10 5 10ZM5 8.75C7.07107 8.75 8.75 7.07107 8.75 5C8.75 2.92893 7.07107 1.25 5 1.25C2.92893 1.25 1.25 2.92893 1.25 5C1.25 7.07107 2.92893 8.75 5 8.75ZM2.24112 5L3.125 4.11612L4.375 5.36612L6.575 3.16612L7.45888 4.05L4.375 7.13388L2.24112 5Z" fill="#37A600"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 509 B |
BIN
Plugins/GameLiftPlugin/Content/Editor/Icons/StatusWarning.png
Normal file
|
After Width: | Height: | Size: 408 B |
@@ -0,0 +1,3 @@
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M19.8696 18.191L11.1196 0.690983C10.659 -0.230328 9.34423 -0.230328 8.88358 0.690983L0.133579 18.191C-0.281984 19.0221 0.322385 20 1.25161 20H18.7516C19.6808 20 20.2852 19.0221 19.8696 18.191ZM3.27416 17.5L10.0016 4.04508L16.7291 17.5H3.27416ZM8.75161 13.75H11.2516V16.25H8.75161V13.75ZM8.75161 12.5V7.5H11.2516V12.5H8.75161Z" fill="#FFB701"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 496 B |
BIN
Plugins/GameLiftPlugin/Content/Editor/Icons/Tooltip.png
Normal file
|
After Width: | Height: | Size: 532 B |
3
Plugins/GameLiftPlugin/Content/Editor/Icons/Tooltip.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M3.2916 1.94658C4.09329 1.41091 5.03582 1.125 6 1.125C7.29293 1.125 8.5329 1.63861 9.44714 2.55285C10.3614 3.46709 10.875 4.70707 10.875 6C10.875 6.96418 10.5891 7.90671 10.0534 8.7084C9.51774 9.51009 8.75637 10.1349 7.86558 10.5039C6.97479 10.8729 5.99459 10.9694 5.04894 10.7813C4.10328 10.5932 3.23464 10.1289 2.55286 9.44714C1.87108 8.76536 1.40678 7.89672 1.21868 6.95106C1.03057 6.00541 1.12711 5.02521 1.49609 4.13442C1.86507 3.24363 2.48991 2.48226 3.2916 1.94658ZM6 0C4.81331 0 3.65328 0.351894 2.66658 1.01118C1.67989 1.67047 0.910851 2.60754 0.456725 3.7039C0.00259973 4.80026 -0.11622 6.00665 0.115291 7.17054C0.346802 8.33443 0.918247 9.40352 1.75736 10.2426C2.59648 11.0818 3.66557 11.6532 4.82946 11.8847C5.99335 12.1162 7.19975 11.9974 8.2961 11.5433C9.39246 11.0891 10.3295 10.3201 10.9888 9.33342C11.6481 8.34672 12 7.18669 12 6C12 4.4087 11.3679 2.88258 10.2426 1.75736C9.11742 0.632141 7.5913 0 6 0ZM3.73813 4.48648L5.23951 4.51003C5.23951 4.10085 5.57957 3.83713 5.99459 3.83713C6.39924 3.83713 6.6672 4.13997 6.6672 4.48648C6.6672 4.86246 6.53296 4.99191 5.99459 5.29894C5.39397 5.64146 5.13942 6.04075 5.15414 6.77373V6.94489H6.56033V6.74335C6.56033 6.39628 6.75287 6.28922 7.18692 6.04784C7.25495 6.01001 7.32892 5.96888 7.40902 5.923C8.04388 5.56056 8.24826 5.01241 8.24826 4.37993C8.24826 3.35395 7.30645 2.4877 5.9945 2.4877C4.61245 2.4877 3.75008 3.3673 3.73813 4.48648ZM5.92507 9.04305C6.48242 9.04292 6.80567 8.75617 6.80567 8.2648C6.80567 7.76934 6.48306 7.48416 5.92467 7.48416C5.36946 7.48416 5.04047 7.76934 5.04047 8.2648C5.04047 8.75697 5.3693 9.04292 5.92507 9.04305Z" fill="white"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.7 KiB |
BIN
Plugins/GameLiftPlugin/Content/Editor/Icons/Whitepaper.png
Normal file
|
After Width: | Height: | Size: 2.2 KiB |
|
After Width: | Height: | Size: 11 KiB |
@@ -0,0 +1,29 @@
|
||||
%COPYRIGHT_LINE%
|
||||
|
||||
%PCH_INCLUDE_DIRECTIVE%
|
||||
%MY_HEADER_INCLUDE_DIRECTIVE%
|
||||
%ADDITIONAL_INCLUDE_DIRECTIVES%
|
||||
|
||||
// Sets default values
|
||||
%PREFIXED_CLASS_NAME%::%PREFIXED_CLASS_NAME%()
|
||||
{
|
||||
// Set this actor to call Tick() every frame. You can turn this off to improve performance if you don't need it.
|
||||
PrimaryActorTick.bCanEverTick = true;
|
||||
|
||||
}
|
||||
|
||||
// Called when the game starts or when spawned
|
||||
void %PREFIXED_CLASS_NAME%::BeginPlay()
|
||||
{
|
||||
Super::BeginPlay();
|
||||
%CURSORFOCUSLOCATION%
|
||||
}
|
||||
|
||||
// Called every frame
|
||||
void %PREFIXED_CLASS_NAME%::Tick(float DeltaTime)
|
||||
{
|
||||
Super::Tick(DeltaTime);
|
||||
|
||||
}
|
||||
|
||||
%ADDITIONAL_MEMBER_DEFINITIONS%
|
||||
@@ -0,0 +1,28 @@
|
||||
%COPYRIGHT_LINE%
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
%BASE_CLASS_INCLUDE_DIRECTIVE%
|
||||
#include "%UNPREFIXED_CLASS_NAME%.generated.h"
|
||||
|
||||
UCLASS(%UCLASS_SPECIFIER_LIST%)
|
||||
class %CLASS_MODULE_API_MACRO%%PREFIXED_CLASS_NAME% : public %PREFIXED_BASE_CLASS_NAME%
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
// Sets default values for this actor's properties
|
||||
%PREFIXED_CLASS_NAME%();
|
||||
|
||||
protected:
|
||||
// Called when the game starts or when spawned
|
||||
virtual void BeginPlay() override;
|
||||
|
||||
public:
|
||||
// Called every frame
|
||||
virtual void Tick(float DeltaTime) override;
|
||||
|
||||
%CLASS_FUNCTION_DECLARATIONS%
|
||||
%CLASS_PROPERTIES%
|
||||
};
|
||||
BIN
Plugins/GameLiftPlugin/Content/Maps/StartupMap.umap
Normal file
BIN
Plugins/GameLiftPlugin/Content/Maps/TestAnywhereFleetMap.umap
Normal file
BIN
Plugins/GameLiftPlugin/Content/Maps/TestCloudDeploymentMap.umap
Normal file
BIN
Plugins/GameLiftPlugin/Content/UI/WBP_StartupMenuWidget.uasset
Normal file
82
Plugins/GameLiftPlugin/GameLiftPlugin.uplugin
Normal file
@@ -0,0 +1,82 @@
|
||||
{
|
||||
"FileVersion": 3,
|
||||
"Version": 6,
|
||||
"VersionName": "3.1.1",
|
||||
"FriendlyName": "Amazon GameLift Plugin",
|
||||
"Description": "Integrate your session-based multiplayer game with Amazon GameLift and create a dedicated cloud hosting solution for your game servers.",
|
||||
"Category": "Networking",
|
||||
"CreatedBy": "Amazon, Inc.",
|
||||
"CreatedByURL": "",
|
||||
"DocsURL": "https://docs.aws.amazon.com/gamelift/latest/developerguide/unreal-plugin.html",
|
||||
"SupportURL": "",
|
||||
"EnabledByDefault": true,
|
||||
"Plugins": [
|
||||
{
|
||||
"Name": "WebBrowserWidget",
|
||||
"Enabled": true
|
||||
}
|
||||
],
|
||||
"Modules": [
|
||||
{
|
||||
"Name": "GameLiftClientSDK",
|
||||
"Type": "Runtime",
|
||||
"LoadingPhase": "Default",
|
||||
"PlatformAllowList": [
|
||||
"Win64"
|
||||
],
|
||||
"TargetAllowList": [
|
||||
"Editor",
|
||||
"Client"
|
||||
]
|
||||
},
|
||||
{
|
||||
"Name": "GameLiftCore",
|
||||
"Type": "Editor",
|
||||
"LoadingPhase": "Default",
|
||||
"PlatformAllowList": [
|
||||
"Win64"
|
||||
],
|
||||
"TargetAllowList": [
|
||||
"Editor"
|
||||
]
|
||||
},
|
||||
{
|
||||
"Name": "GameLiftPlugin",
|
||||
"Type": "Editor",
|
||||
"LoadingPhase": "Default",
|
||||
"PlatformAllowList": [
|
||||
"Win64"
|
||||
],
|
||||
"TargetAllowList": [
|
||||
"Editor"
|
||||
]
|
||||
},
|
||||
{
|
||||
"Name": "GameLiftServerSDK",
|
||||
"Type": "Runtime",
|
||||
"LoadingPhase": "PreDefault",
|
||||
"TargetAllowList": [
|
||||
"Server"
|
||||
]
|
||||
},
|
||||
{
|
||||
"Name": "GameLiftMetrics",
|
||||
"Type": "Runtime",
|
||||
"LoadingPhase": "PreDefault",
|
||||
"TargetAllowList": [
|
||||
"Server"
|
||||
]
|
||||
}
|
||||
],
|
||||
"LocalizationTargets": [
|
||||
{
|
||||
"Name": "GameLiftPlugin",
|
||||
"LoadingPolicy": "Always"
|
||||
}
|
||||
],
|
||||
"MarketplaceURL": "",
|
||||
"CanContainContent": true,
|
||||
"IsBetaVersion": false,
|
||||
"IsExperimentalVersion": false,
|
||||
"Installed": false
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||