diff --git a/.gitignore b/.gitignore index 53a7717d167c8c0aa8f4f81e3dde02c8e35478a1..1fd3410e3f8b45207c211b5ed889ec5aaa514f0d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ +assets/local-game.properties ## Java *.class diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index e782024182a3f59cee40d2e6efeab9da77724e2c..81bc0f86a5b44a41b45bcab0936b485147c53b92 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,5 +1,6 @@ stages: - test + - deploy # TODO?: could switch this to a matrix, but doing so is annoying. GitLab's CI is vastly inferior to GitHub's CI # in terms of readability and ease-of-writing (and GH Actions is already real bad on that front, soooo) @@ -9,11 +10,30 @@ test-ubuntu: stage: test script: - apt update - - apt install -y curl unzip zip sed - - curl -s "https://get.sdkman.io" | bash - - source "$HOME/.sdkman/bin/sdkman-init.sh" - - sdk install java + - apt install -y curl unzip zip sed openjdk-17-jdk # Note; if/when UI-based tests are added, this script must be modified to allow for virtual displays. # I have a script for that lying around, but won't be adding it for now because I just spent a solid 8 # hours fighting socket.io, and I'm not prepared for another 8 hours of fighting the CI - ./gradlew test + +deploy: + stage: deploy + environment: + name: production + url: 10.212.27.55 + script: + - apt update + - apt install -y openssh-client + # Required to get the SSH agent to run or add it to the environment or whatever. I don't know, I don't normally hotpatch SSH agents into CI scripts for fun + # My definition of good enough on the insanely rare system that doesn't have an SSH client installed is just to reboot to make the errors go away + - eval $(ssh-agent -s) + # Add the key + - ssh-add <(echo "$SSH_PRIVATE_KEY" | base64 -d) + # SSH to the server, restart the service + # Disabling StrictHostKeyChecking is required because the CI server doesn't have a cached entry. That would, in an interactive environment, add a prompt, but + # fails headless tests. Disabling it avoids that, and it doesn't really have any consequences because even if the server has been compromised, + # the host key wouldn't be possible to store trivially, and therefore fail + # It _can_ be stored in a CI variable, but why? It's easier not to + - ssh -o StrictHostKeyChecking=no ubuntu@10.212.27.55 "cd /app && git checkout production && git pull origin production && ./gradlew server:dist && sudo systemctl restart netrunner-server" + only: + - production diff --git a/android/build.gradle b/android/build.gradle index b7f7fb0eed40d221685fcac0db5a7dc7b7e90e22..c4c1483280418cfc615d6dfc4050947625594ff7 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -15,6 +15,7 @@ android { } packagingOptions { exclude 'META-INF/robovm/ios/robovm.xml' + exclude "META-INF/INDEX.LIST" } compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 @@ -22,7 +23,7 @@ android { } defaultConfig { applicationId "tdt4240.netrunner" - minSdkVersion 14 + minSdkVersion 23 targetSdkVersion 33 versionCode 1 versionName "1.0" diff --git a/android/src/tdt4240/netrunner/AndroidLauncher.kt b/android/src/tdt4240/netrunner/AndroidLauncher.kt index ba9b7f6d33ffa1e5c700c61a3f8dbb86cfe9c26b..37b8f753547c2cf6e367a8db1d5010732cc7cb12 100644 --- a/android/src/tdt4240/netrunner/AndroidLauncher.kt +++ b/android/src/tdt4240/netrunner/AndroidLauncher.kt @@ -11,4 +11,4 @@ class AndroidLauncher : AndroidApplication() { val config = AndroidApplicationConfiguration() initialize(Netrunner(), config) } -} \ No newline at end of file +} diff --git a/assets/bg_0.png b/assets/bg_0.png new file mode 100644 index 0000000000000000000000000000000000000000..03fdc7b8cc2ffe3827599a706efe636a465afc71 Binary files /dev/null and b/assets/bg_0.png differ diff --git a/assets/bg_1.png b/assets/bg_1.png new file mode 100644 index 0000000000000000000000000000000000000000..f1d215e2f682a8e44f4076c31752aaadf33ed972 Binary files /dev/null and b/assets/bg_1.png differ diff --git a/assets/bg_2.png b/assets/bg_2.png new file mode 100644 index 0000000000000000000000000000000000000000..2cfef0217bfd15943aa53298c9e8ae3607ddb345 Binary files /dev/null and b/assets/bg_2.png differ diff --git a/assets/char_blue_run.png b/assets/char_blue_run.png new file mode 100644 index 0000000000000000000000000000000000000000..c0ce025a148c8facacd2e2dbf06e34b96397c5ee Binary files /dev/null and b/assets/char_blue_run.png differ diff --git a/assets/char_blue_stand.png b/assets/char_blue_stand.png new file mode 100644 index 0000000000000000000000000000000000000000..1785fb299ba5b053aec095b3b1354a1289d632a1 Binary files /dev/null and b/assets/char_blue_stand.png differ diff --git a/assets/char_green_run.png b/assets/char_green_run.png new file mode 100644 index 0000000000000000000000000000000000000000..d65d36f56c8f8b8bdb9ecf429e82bbded53e9319 Binary files /dev/null and b/assets/char_green_run.png differ diff --git a/assets/char_green_stand.png b/assets/char_green_stand.png new file mode 100644 index 0000000000000000000000000000000000000000..6c5b55bdc82805f8c0b72065452802b3d730c55d Binary files /dev/null and b/assets/char_green_stand.png differ diff --git a/assets/char_red_run.png b/assets/char_red_run.png new file mode 100644 index 0000000000000000000000000000000000000000..f58e0f1bbf112b297a73a3f3a794a2a6977dcc36 Binary files /dev/null and b/assets/char_red_run.png differ diff --git a/assets/char_red_stand.png b/assets/char_red_stand.png new file mode 100644 index 0000000000000000000000000000000000000000..83013005d970e73e56483818db1d3a96ba28bb6a Binary files /dev/null and b/assets/char_red_stand.png differ diff --git a/assets/char_yellow_run.png b/assets/char_yellow_run.png new file mode 100644 index 0000000000000000000000000000000000000000..4530e0476c171dda9760db948009bd673ebd96c4 Binary files /dev/null and b/assets/char_yellow_run.png differ diff --git a/assets/char_yellow_stand.png b/assets/char_yellow_stand.png new file mode 100644 index 0000000000000000000000000000000000000000..ee5f72a44034be9f6ca7299576c276fa86a9d93e Binary files /dev/null and b/assets/char_yellow_stand.png differ diff --git a/assets/default.fnt b/assets/default.fnt new file mode 100644 index 0000000000000000000000000000000000000000..81d3449d9d19789dc0a8d596de8265751b1b0f7b --- /dev/null +++ b/assets/default.fnt @@ -0,0 +1,101 @@ +info face="Droid Sans" size=17 bold=0 italic=0 charset="" unicode=0 stretchH=100 smooth=1 aa=1 padding=0,0,0,0 spacing=1,1 +common lineHeight=20 base=18 scaleW=256 scaleH=128 pages=1 packed=0 +page id=0 file="default.png" +chars count=96 +char id=32 x=0 y=0 width=0 height=0 xoffset=0 yoffset=16 xadvance=4 page=0 chnl=0 +char id=124 x=0 y=0 width=6 height=20 xoffset=1 yoffset=3 xadvance=9 page=0 chnl=0 +char id=106 x=6 y=0 width=9 height=20 xoffset=-4 yoffset=3 xadvance=4 page=0 chnl=0 +char id=81 x=15 y=0 width=15 height=19 xoffset=-2 yoffset=3 xadvance=12 page=0 chnl=0 +char id=74 x=30 y=0 width=11 height=19 xoffset=-5 yoffset=3 xadvance=4 page=0 chnl=0 +char id=125 x=41 y=0 width=10 height=18 xoffset=-3 yoffset=3 xadvance=6 page=0 chnl=0 +char id=123 x=51 y=0 width=10 height=18 xoffset=-3 yoffset=3 xadvance=6 page=0 chnl=0 +char id=93 x=61 y=0 width=8 height=18 xoffset=-3 yoffset=3 xadvance=5 page=0 chnl=0 +char id=91 x=69 y=0 width=8 height=18 xoffset=-2 yoffset=3 xadvance=5 page=0 chnl=0 +char id=41 x=77 y=0 width=9 height=18 xoffset=-3 yoffset=3 xadvance=5 page=0 chnl=0 +char id=40 x=86 y=0 width=9 height=18 xoffset=-3 yoffset=3 xadvance=5 page=0 chnl=0 +char id=64 x=95 y=0 width=18 height=17 xoffset=-3 yoffset=3 xadvance=14 page=0 chnl=0 +char id=121 x=113 y=0 width=13 height=17 xoffset=-3 yoffset=6 xadvance=8 page=0 chnl=0 +char id=113 x=126 y=0 width=13 height=17 xoffset=-3 yoffset=6 xadvance=9 page=0 chnl=0 +char id=112 x=139 y=0 width=13 height=17 xoffset=-2 yoffset=6 xadvance=9 page=0 chnl=0 +char id=103 x=152 y=0 width=13 height=17 xoffset=-3 yoffset=6 xadvance=8 page=0 chnl=0 +char id=38 x=165 y=0 width=16 height=16 xoffset=-3 yoffset=3 xadvance=11 page=0 chnl=0 +char id=37 x=181 y=0 width=18 height=16 xoffset=-3 yoffset=3 xadvance=14 page=0 chnl=0 +char id=36 x=199 y=0 width=12 height=16 xoffset=-2 yoffset=3 xadvance=9 page=0 chnl=0 +char id=63 x=211 y=0 width=11 height=16 xoffset=-3 yoffset=3 xadvance=7 page=0 chnl=0 +char id=33 x=222 y=0 width=7 height=16 xoffset=-2 yoffset=3 xadvance=4 page=0 chnl=0 +char id=48 x=229 y=0 width=13 height=16 xoffset=-3 yoffset=3 xadvance=9 page=0 chnl=0 +char id=57 x=242 y=0 width=13 height=16 xoffset=-3 yoffset=3 xadvance=9 page=0 chnl=0 +char id=56 x=0 y=20 width=13 height=16 xoffset=-3 yoffset=3 xadvance=9 page=0 chnl=0 +char id=54 x=13 y=20 width=13 height=16 xoffset=-3 yoffset=3 xadvance=9 page=0 chnl=0 +char id=53 x=26 y=20 width=12 height=16 xoffset=-2 yoffset=3 xadvance=9 page=0 chnl=0 +char id=51 x=38 y=20 width=13 height=16 xoffset=-3 yoffset=3 xadvance=9 page=0 chnl=0 +char id=100 x=51 y=20 width=13 height=16 xoffset=-3 yoffset=3 xadvance=9 page=0 chnl=0 +char id=98 x=64 y=20 width=13 height=16 xoffset=-2 yoffset=3 xadvance=9 page=0 chnl=0 +char id=85 x=77 y=20 width=14 height=16 xoffset=-2 yoffset=3 xadvance=11 page=0 chnl=0 +char id=83 x=91 y=20 width=13 height=16 xoffset=-3 yoffset=3 xadvance=8 page=0 chnl=0 +char id=79 x=104 y=20 width=15 height=16 xoffset=-2 yoffset=3 xadvance=12 page=0 chnl=0 +char id=71 x=119 y=20 width=14 height=16 xoffset=-2 yoffset=3 xadvance=11 page=0 chnl=0 +char id=67 x=133 y=20 width=13 height=16 xoffset=-2 yoffset=3 xadvance=10 page=0 chnl=0 +char id=127 x=146 y=20 width=12 height=15 xoffset=-2 yoffset=3 xadvance=10 page=0 chnl=0 +char id=35 x=158 y=20 width=15 height=15 xoffset=-3 yoffset=3 xadvance=10 page=0 chnl=0 +char id=92 x=173 y=20 width=11 height=15 xoffset=-3 yoffset=3 xadvance=6 page=0 chnl=0 +char id=47 x=184 y=20 width=11 height=15 xoffset=-3 yoffset=3 xadvance=6 page=0 chnl=0 +char id=59 x=195 y=20 width=8 height=15 xoffset=-3 yoffset=6 xadvance=4 page=0 chnl=0 +char id=55 x=203 y=20 width=13 height=15 xoffset=-3 yoffset=3 xadvance=9 page=0 chnl=0 +char id=52 x=216 y=20 width=14 height=15 xoffset=-3 yoffset=3 xadvance=9 page=0 chnl=0 +char id=50 x=230 y=20 width=13 height=15 xoffset=-3 yoffset=3 xadvance=9 page=0 chnl=0 +char id=49 x=243 y=20 width=9 height=15 xoffset=-2 yoffset=3 xadvance=9 page=0 chnl=0 +char id=116 x=0 y=36 width=10 height=15 xoffset=-3 yoffset=4 xadvance=5 page=0 chnl=0 +char id=108 x=10 y=36 width=6 height=15 xoffset=-2 yoffset=3 xadvance=4 page=0 chnl=0 +char id=107 x=16 y=36 width=12 height=15 xoffset=-2 yoffset=3 xadvance=8 page=0 chnl=0 +char id=105 x=28 y=36 width=7 height=15 xoffset=-2 yoffset=3 xadvance=4 page=0 chnl=0 +char id=104 x=35 y=36 width=12 height=15 xoffset=-2 yoffset=3 xadvance=10 page=0 chnl=0 +char id=102 x=47 y=36 width=11 height=15 xoffset=-3 yoffset=3 xadvance=5 page=0 chnl=0 +char id=90 x=58 y=36 width=13 height=15 xoffset=-3 yoffset=3 xadvance=9 page=0 chnl=0 +char id=89 x=71 y=36 width=13 height=15 xoffset=-3 yoffset=3 xadvance=8 page=0 chnl=0 +char id=88 x=84 y=36 width=14 height=15 xoffset=-3 yoffset=3 xadvance=9 page=0 chnl=0 +char id=87 x=98 y=36 width=19 height=15 xoffset=-3 yoffset=3 xadvance=15 page=0 chnl=0 +char id=86 x=117 y=36 width=14 height=15 xoffset=-3 yoffset=3 xadvance=9 page=0 chnl=0 +char id=84 x=131 y=36 width=13 height=15 xoffset=-3 yoffset=3 xadvance=8 page=0 chnl=0 +char id=82 x=144 y=36 width=13 height=15 xoffset=-2 yoffset=3 xadvance=10 page=0 chnl=0 +char id=80 x=157 y=36 width=12 height=15 xoffset=-2 yoffset=3 xadvance=9 page=0 chnl=0 +char id=78 x=169 y=36 width=14 height=15 xoffset=-2 yoffset=3 xadvance=12 page=0 chnl=0 +char id=77 x=183 y=36 width=17 height=15 xoffset=-2 yoffset=3 xadvance=14 page=0 chnl=0 +char id=76 x=200 y=36 width=11 height=15 xoffset=-2 yoffset=3 xadvance=8 page=0 chnl=0 +char id=75 x=211 y=36 width=13 height=15 xoffset=-2 yoffset=3 xadvance=9 page=0 chnl=0 +char id=73 x=224 y=36 width=10 height=15 xoffset=-3 yoffset=3 xadvance=5 page=0 chnl=0 +char id=72 x=234 y=36 width=14 height=15 xoffset=-2 yoffset=3 xadvance=11 page=0 chnl=0 +char id=70 x=0 y=51 width=11 height=15 xoffset=-2 yoffset=3 xadvance=8 page=0 chnl=0 +char id=69 x=11 y=51 width=11 height=15 xoffset=-2 yoffset=3 xadvance=8 page=0 chnl=0 +char id=68 x=22 y=51 width=14 height=15 xoffset=-2 yoffset=3 xadvance=11 page=0 chnl=0 +char id=66 x=36 y=51 width=13 height=15 xoffset=-2 yoffset=3 xadvance=10 page=0 chnl=0 +char id=65 x=49 y=51 width=15 height=15 xoffset=-3 yoffset=3 xadvance=10 page=0 chnl=0 +char id=58 x=64 y=51 width=7 height=13 xoffset=-2 yoffset=6 xadvance=4 page=0 chnl=0 +char id=117 x=71 y=51 width=12 height=13 xoffset=-2 yoffset=6 xadvance=10 page=0 chnl=0 +char id=115 x=83 y=51 width=11 height=13 xoffset=-3 yoffset=6 xadvance=7 page=0 chnl=0 +char id=111 x=94 y=51 width=13 height=13 xoffset=-3 yoffset=6 xadvance=9 page=0 chnl=0 +char id=101 x=107 y=51 width=13 height=13 xoffset=-3 yoffset=6 xadvance=9 page=0 chnl=0 +char id=99 x=120 y=51 width=12 height=13 xoffset=-3 yoffset=6 xadvance=7 page=0 chnl=0 +char id=97 x=132 y=51 width=12 height=13 xoffset=-3 yoffset=6 xadvance=9 page=0 chnl=0 +char id=60 x=144 y=51 width=13 height=12 xoffset=-3 yoffset=5 xadvance=9 page=0 chnl=0 +char id=122 x=157 y=51 width=11 height=12 xoffset=-3 yoffset=6 xadvance=7 page=0 chnl=0 +char id=120 x=168 y=51 width=13 height=12 xoffset=-3 yoffset=6 xadvance=8 page=0 chnl=0 +char id=119 x=181 y=51 width=17 height=12 xoffset=-3 yoffset=6 xadvance=12 page=0 chnl=0 +char id=118 x=198 y=51 width=13 height=12 xoffset=-3 yoffset=6 xadvance=8 page=0 chnl=0 +char id=114 x=211 y=51 width=10 height=12 xoffset=-2 yoffset=6 xadvance=6 page=0 chnl=0 +char id=110 x=221 y=51 width=12 height=12 xoffset=-2 yoffset=6 xadvance=10 page=0 chnl=0 +char id=109 x=233 y=51 width=17 height=12 xoffset=-2 yoffset=6 xadvance=15 page=0 chnl=0 +char id=94 x=0 y=66 width=13 height=11 xoffset=-3 yoffset=3 xadvance=9 page=0 chnl=0 +char id=62 x=13 y=66 width=13 height=11 xoffset=-3 yoffset=5 xadvance=9 page=0 chnl=0 +char id=42 x=26 y=66 width=13 height=10 xoffset=-3 yoffset=3 xadvance=9 page=0 chnl=0 +char id=43 x=39 y=66 width=13 height=10 xoffset=-3 yoffset=6 xadvance=9 page=0 chnl=0 +char id=61 x=52 y=66 width=13 height=8 xoffset=-3 yoffset=7 xadvance=9 page=0 chnl=0 +char id=39 x=65 y=66 width=6 height=8 xoffset=-2 yoffset=3 xadvance=3 page=0 chnl=0 +char id=34 x=71 y=66 width=9 height=8 xoffset=-2 yoffset=3 xadvance=6 page=0 chnl=0 +char id=44 x=80 y=66 width=8 height=7 xoffset=-3 yoffset=14 xadvance=4 page=0 chnl=0 +char id=126 x=88 y=66 width=13 height=6 xoffset=-3 yoffset=8 xadvance=9 page=0 chnl=0 +char id=46 x=101 y=66 width=7 height=6 xoffset=-2 yoffset=13 xadvance=4 page=0 chnl=0 +char id=96 x=108 y=66 width=8 height=6 xoffset=0 yoffset=2 xadvance=9 page=0 chnl=0 +char id=45 x=116 y=66 width=9 height=5 xoffset=-3 yoffset=10 xadvance=5 page=0 chnl=0 +char id=95 x=125 y=66 width=13 height=4 xoffset=-4 yoffset=17 xadvance=6 page=0 chnl=0 +kernings count=-1 diff --git a/assets/default.png b/assets/default.png new file mode 100644 index 0000000000000000000000000000000000000000..ca368e077621ef4097d8adf4e004abc8c4ddb7b0 Binary files /dev/null and b/assets/default.png differ diff --git a/assets/powerup.png b/assets/powerup.png new file mode 100644 index 0000000000000000000000000000000000000000..208c1983e0a9ebbe4c4cfd1836d823f7ff0adee2 Binary files /dev/null and b/assets/powerup.png differ diff --git a/assets/tile-platform-left.png b/assets/tile-platform-left.png new file mode 100644 index 0000000000000000000000000000000000000000..9d7d5eb9099bb79c87bbef7a60443a97424e41c0 Binary files /dev/null and b/assets/tile-platform-left.png differ diff --git a/assets/tile-platform-mid.png b/assets/tile-platform-mid.png new file mode 100644 index 0000000000000000000000000000000000000000..88a3f39251c31be01307bac8a4ea53488af9df4a Binary files /dev/null and b/assets/tile-platform-mid.png differ diff --git a/assets/tile-platform-right.png b/assets/tile-platform-right.png new file mode 100644 index 0000000000000000000000000000000000000000..6fae9f1d5e8237237e658411259ce9a20a0a78b7 Binary files /dev/null and b/assets/tile-platform-right.png differ diff --git a/assets/tile-wall-bottom.png b/assets/tile-wall-bottom.png new file mode 100644 index 0000000000000000000000000000000000000000..52c113c2aeaa0ed8b2391fc79b74da636b0bcb9a Binary files /dev/null and b/assets/tile-wall-bottom.png differ diff --git a/assets/tile-wall-mid.png b/assets/tile-wall-mid.png new file mode 100644 index 0000000000000000000000000000000000000000..626f0d4ed55e0929b813d9a3bb6f920a81f833e4 Binary files /dev/null and b/assets/tile-wall-mid.png differ diff --git a/assets/tile-wall-top.png b/assets/tile-wall-top.png new file mode 100644 index 0000000000000000000000000000000000000000..1216f1b554ed8ff3ce2793b063a7394f672171dd Binary files /dev/null and b/assets/tile-wall-top.png differ diff --git a/assets/uiskin.atlas b/assets/uiskin.atlas new file mode 100644 index 0000000000000000000000000000000000000000..d1f3db64ac070df5a0e90884e22aecba63766600 --- /dev/null +++ b/assets/uiskin.atlas @@ -0,0 +1,200 @@ + +uiskin.png +size: 256,128 +format: RGBA8888 +filter: Linear,Linear +repeat: none +check-off + rotate: false + xy: 11, 5 + size: 14, 14 + orig: 14, 14 + offset: 0, 0 + index: -1 +textfield + rotate: false + xy: 11, 5 + size: 14, 14 + split: 3, 3, 3, 3 + orig: 14, 14 + offset: 0, 0 + index: -1 +check-on + rotate: false + xy: 125, 35 + size: 14, 14 + orig: 14, 14 + offset: 0, 0 + index: -1 +cursor + rotate: false + xy: 23, 1 + size: 3, 3 + split: 1, 1, 1, 1 + orig: 3, 3 + offset: 0, 0 + index: -1 +default + rotate: false + xy: 1, 50 + size: 254, 77 + orig: 254, 77 + offset: 0, 0 + index: -1 +default-pane + rotate: false + xy: 11, 1 + size: 5, 3 + split: 1, 1, 1, 1 + orig: 5, 3 + offset: 0, 0 + index: -1 +default-rect-pad + rotate: false + xy: 11, 1 + size: 5, 3 + split: 1, 1, 1, 1 + orig: 5, 3 + offset: 0, 0 + index: -1 +default-pane-noborder + rotate: false + xy: 170, 44 + size: 1, 1 + split: 0, 0, 0, 0 + orig: 1, 1 + offset: 0, 0 + index: -1 +default-rect + rotate: false + xy: 38, 25 + size: 3, 3 + split: 1, 1, 1, 1 + orig: 3, 3 + offset: 0, 0 + index: -1 +default-rect-down + rotate: false + xy: 170, 46 + size: 3, 3 + split: 1, 1, 1, 1 + orig: 3, 3 + offset: 0, 0 + index: -1 +default-round + rotate: false + xy: 112, 29 + size: 12, 20 + split: 5, 5, 5, 4 + pad: 4, 4, 1, 1 + orig: 12, 20 + offset: 0, 0 + index: -1 +default-round-down + rotate: false + xy: 99, 29 + size: 12, 20 + split: 5, 5, 5, 4 + pad: 4, 4, 1, 1 + orig: 12, 20 + offset: 0, 0 + index: -1 +default-round-large + rotate: false + xy: 57, 29 + size: 20, 20 + split: 5, 5, 5, 4 + orig: 20, 20 + offset: 0, 0 + index: -1 +default-scroll + rotate: false + xy: 78, 29 + size: 20, 20 + split: 2, 2, 2, 2 + orig: 20, 20 + offset: 0, 0 + index: -1 +default-select + rotate: false + xy: 29, 29 + size: 27, 20 + split: 4, 14, 4, 4 + orig: 27, 20 + offset: 0, 0 + index: -1 +default-select-selection + rotate: false + xy: 26, 16 + size: 3, 3 + split: 1, 1, 1, 1 + orig: 3, 3 + offset: 0, 0 + index: -1 +default-slider + rotate: false + xy: 29, 20 + size: 8, 8 + split: 2, 2, 2, 2 + orig: 8, 8 + offset: 0, 0 + index: -1 +default-slider-knob + rotate: false + xy: 1, 1 + size: 9, 18 + orig: 9, 18 + offset: 0, 0 + index: -1 +default-splitpane + rotate: false + xy: 17, 1 + size: 5, 3 + split: 0, 5, 0, 0 + orig: 5, 3 + offset: 0, 0 + index: -1 +default-splitpane-vertical + rotate: false + xy: 125, 29 + size: 3, 5 + split: 0, 0, 0, 5 + orig: 3, 5 + offset: 0, 0 + index: -1 +default-window + rotate: false + xy: 1, 20 + size: 27, 29 + split: 4, 3, 20, 3 + orig: 27, 29 + offset: 0, 0 + index: -1 +selection + rotate: false + xy: 174, 48 + size: 1, 1 + orig: 1, 1 + offset: 0, 0 + index: -1 +tree-minus + rotate: false + xy: 140, 35 + size: 14, 14 + orig: 14, 14 + offset: 0, 0 + index: -1 +tree-plus + rotate: false + xy: 155, 35 + size: 14, 14 + orig: 14, 14 + offset: 0, 0 + index: -1 +white + rotate: false + xy: 129, 31 + size: 3, 3 + orig: 3, 3 + offset: 0, 0 + index: -1 diff --git a/assets/uiskin.json b/assets/uiskin.json new file mode 100644 index 0000000000000000000000000000000000000000..2030cc8228f6bd2696bfbcacff472c40e67228b8 --- /dev/null +++ b/assets/uiskin.json @@ -0,0 +1,71 @@ +{ +BitmapFont: { default-font: { file: default.fnt } }, +Color: { + green: { a: 1, b: 0, g: 1, r: 0 }, + white: { a: 1, b: 1, g: 1, r: 1 }, + red: { a: 1, b: 0, g: 0, r: 1 }, + black: { a: 1, b: 0, g: 0, r: 0 }, + gray: { a: 1, b: 0.8, g: 0.8, r: 0.8 }, +}, +TintedDrawable: { + dialogDim: { name: white, color: { r: 0, g: 0, b: 0, a: 0.45 } }, +}, +ButtonStyle: { + default: { down: default-round-down, up: default-round, disabled: default-round }, + toggle: { parent: default, checked: default-round-down } +}, +TextButtonStyle: { + default: { parent: default, font: default-font, fontColor: white, disabledFontColor: gray }, + toggle: { parent: default, checked: default-round-down, downFontColor: red } +}, +ScrollPaneStyle: { + default: { vScroll: default-scroll, hScrollKnob: default-round-large, background: default-rect, hScroll: default-scroll, vScrollKnob: default-round-large } +}, +SelectBoxStyle: { + default: { + font: default-font, fontColor: white, background: default-select, + scrollStyle: default, + listStyle: { font: default-font, selection: default-select-selection } + } +}, +SplitPaneStyle: { + default-vertical: { handle: default-splitpane-vertical }, + default-horizontal: { handle: default-splitpane } +}, +WindowStyle: { + default: { titleFont: default-font, background: default-window, titleFontColor: white }, + dialog: { parent: default, stageBackground: dialogDim } +}, +ProgressBarStyle: { + default-horizontal: { background: default-slider, knob: default-slider-knob }, + default-vertical: { background: default-slider, knob: default-round-large } +}, +SliderStyle: { + default-horizontal: { parent: default-horizontal }, + default-vertical: { parent: default-vertical } +}, +LabelStyle: { + default: { font: default-font, fontColor: white } +}, +TextFieldStyle: { + default: { selection: selection, background: textfield, font: default-font, fontColor: white, cursor: cursor } +}, +CheckBoxStyle: { + default: { checkboxOn: check-on, checkboxOff: check-off, font: default-font, fontColor: white } +}, +ListStyle: { + default: { fontColorUnselected: white, selection: selection, fontColorSelected: white, font: default-font } +}, +TouchpadStyle: { + default: { background: default-pane, knob: default-round-large } +}, +TreeStyle: { + default: { minus: tree-minus, plus: tree-plus, selection: default-select-selection } +}, +TextTooltipStyle: { + default: { + label: { font: default-font, fontColor: white }, + background: default-pane, wrapWidth: 150 + } +}, +} diff --git a/assets/uiskin.png b/assets/uiskin.png new file mode 100644 index 0000000000000000000000000000000000000000..c1e5f1a210e11a24ba2065e93e665839852b6aed Binary files /dev/null and b/assets/uiskin.png differ diff --git a/build.gradle b/build.gradle index 1769bf4de5e65767aebbcf7e854cd17bea2bca56..bd44856f61447af7b289bbaf3d1226040cad3383 100644 --- a/build.gradle +++ b/build.gradle @@ -12,6 +12,8 @@ buildscript { classpath 'com.android.tools.build:gradle:7.3.1' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion" classpath "com.adarshr:gradle-test-logger-plugin:3.2.0" + + } } @@ -50,6 +52,11 @@ project(":desktop") { api "com.badlogicgames.gdx:gdx-backend-lwjgl3:$gdxVersion" api "com.badlogicgames.gdx:gdx-platform:$gdxVersion:natives-desktop" api "org.jetbrains.kotlin:kotlin-stdlib:$kotlinVersion" + + implementation "org.slf4j:slf4j-api:2.0.6" + implementation 'ch.qos.logback:logback-classic:1.4.5' + + } } @@ -67,6 +74,9 @@ project(":android") { natives "com.badlogicgames.gdx:gdx-platform:$gdxVersion:natives-x86" natives "com.badlogicgames.gdx:gdx-platform:$gdxVersion:natives-x86_64" api "org.jetbrains.kotlin:kotlin-stdlib:$kotlinVersion" + + implementation "org.slf4j:slf4j-api:2.0.6" + implementation 'org.slf4j:slf4j-android:1.7.30' } } @@ -81,6 +91,8 @@ project(":core") { implementation ('io.socket:socket.io-client:2.1.0') implementation 'com.google.code.gson:gson:2.10.1' + + implementation "org.slf4j:slf4j-api:2.0.6" } } @@ -131,5 +143,9 @@ project(":model") { dependencies { implementation 'com.google.code.gson:gson:2.10.1' + testImplementation 'org.jetbrains.kotlin:kotlin-test' + testImplementation 'org.junit.jupiter:junit-jupiter-api:5.9.2' + testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.9.2' + } } diff --git a/core/src/tdt4240/netrunner/Move.kt b/core/src/tdt4240/netrunner/Move.kt deleted file mode 100644 index 0e176b0cc740f1f0669469f5cc780ae33d917a88..0000000000000000000000000000000000000000 --- a/core/src/tdt4240/netrunner/Move.kt +++ /dev/null @@ -1,14 +0,0 @@ -package tdt4240.netrunner - -import com.badlogic.gdx.Gdx -import com.badlogic.gdx.Input -import com.badlogic.gdx.math.Vector2 - -class Move { - - private var width: Int = Gdx.graphics.width - public var position: Vector2 = Vector2(width.toFloat()/2, 0f) - - - -} \ No newline at end of file diff --git a/core/src/tdt4240/netrunner/Netrunner.kt b/core/src/tdt4240/netrunner/Netrunner.kt index cba53a02edf9caa14ef0daaad6071a459fa4ffe4..56cd9cc76d2da5b178d14af8bef17b1bc5808d81 100644 --- a/core/src/tdt4240/netrunner/Netrunner.kt +++ b/core/src/tdt4240/netrunner/Netrunner.kt @@ -1,9 +1,9 @@ package tdt4240.netrunner -import com.badlogic.gdx.ApplicationAdapter import com.badlogic.gdx.Gdx -import com.badlogic.gdx.Input import com.badlogic.gdx.InputMultiplexer +import com.badlogic.gdx.Game +import org.slf4j.LoggerFactory import com.badlogic.gdx.graphics.Texture import com.badlogic.gdx.graphics.g2d.BitmapFont import com.badlogic.gdx.graphics.g2d.SpriteBatch @@ -11,28 +11,59 @@ import com.badlogic.gdx.math.Vector2 import com.badlogic.gdx.scenes.scene2d.Stage import com.badlogic.gdx.scenes.scene2d.ui.Skin import com.badlogic.gdx.utils.ScreenUtils +import com.badlogic.gdx.graphics.glutils.ShapeRenderer +import com.badlogic.gdx.utils.viewport.ExtendViewport +import com.badlogic.gdx.utils.viewport.Viewport +import tdt4240.netrunner.game.Client +import tdt4240.netrunner.model.game.data.PlayerColor +import tdt4240.netrunner.view.LoadingScreen +import java.util.* -class Netrunner : ApplicationAdapter() { - - private lateinit var stage: Stage +class Netrunner : Game() { private lateinit var skin: Skin - private lateinit var font: BitmapFont private lateinit var batch: SpriteBatch private lateinit var img: Texture private var position: Vector2 = Vector2(300f, 0f) private var speed: Float = 10.0f private lateinit var buttonUp: SimpleButton private lateinit var buttonDown: SimpleButton - public var inputMultiplexer = InputMultiplexer() + private var inputMultiplexer = InputMultiplexer() + lateinit var shapeRenderer: ShapeRenderer + lateinit var uiViewport: Viewport + var characterTextures = mutableMapOf<PlayerColor, Texture>() override fun create() { - Gdx.input.inputProcessor = inputMultiplexer + val localProps = Gdx.files.internal("local-game.properties") + if (localProps.exists()) { + val p = Properties() + p.load(localProps.reader()) + for (property in p) { + System.setProperty(property.key as String, property.value as String) + } + logger.info("local-game.properties found and loaded") + } else { + logger.info("No local-game.properties found") + } + batch = SpriteBatch() + shapeRenderer = ShapeRenderer() + + skin = Skin(Gdx.files.internal("uiskin.json")) + + uiViewport = ExtendViewport(MIN_WIDTH, MIN_HEIGHT) + + setScreen(LoadingScreen(this)) + + for (color in PlayerColor.values()) { + characterTextures[color] = Texture(Gdx.files.internal(color.graphic)) + } + + Gdx.input.inputProcessor = inputMultiplexer img = Texture("whiteball.png") buttonUp = SimpleButton(inputMultiplexer, Texture("up.png"), 550f, 100f,60f,60f) - { moveUp() } + { moveUp() } buttonDown = SimpleButton(inputMultiplexer, Texture("down.png"), 550f, 20f,60f,60f) - { moveDown() } + { moveDown() } } override fun render() { @@ -46,25 +77,13 @@ class Netrunner : ApplicationAdapter() { } override fun dispose() { - batch.dispose() - img.dispose() - } + Client.sock.close() + batch.dispose() + skin.dispose() - fun move(dt: Float){ - if (position.y == 0f){ - position.y = 0f + img.height - } - if (position.y == Gdx.graphics.height.toFloat()){ - position.y = Gdx.graphics.height.toFloat() - img.height - } - else { - if (Gdx.input.isKeyPressed(Input.Keys.DPAD_DOWN)) { - position.y -= dt * speed - } - if (Gdx.input.isKeyPressed(Input.Keys.DPAD_UP)) { - position.y += dt * speed - } + for (tex in characterTextures) { + tex.value.dispose() } } fun moveUp() { @@ -74,5 +93,10 @@ class Netrunner : ApplicationAdapter() { position.y -= speed } + companion object { + const val MIN_WIDTH = 720f; + const val MIN_HEIGHT = MIN_WIDTH * (9f/16f) -} \ No newline at end of file + private val logger = LoggerFactory.getLogger(Netrunner::class.java) + } +} diff --git a/core/src/tdt4240/netrunner/game/Client.kt b/core/src/tdt4240/netrunner/game/Client.kt new file mode 100644 index 0000000000000000000000000000000000000000..f4c826ad85579179036ef4732763ed1b15b3959f --- /dev/null +++ b/core/src/tdt4240/netrunner/game/Client.kt @@ -0,0 +1,75 @@ +package tdt4240.netrunner.game + +import io.socket.client.IO +import io.socket.client.Socket +import io.socket.engineio.client.transports.WebSocket +import org.slf4j.LoggerFactory +import java.net.URI + +// This is some top sneaky fixes right here +// Both serverAddr and serverPort are sourced from properties. For now, server-addr is mandatory. +// These are fairly trivial to set from the command line: https://stackoverflow.com/a/5189966/6296561 +// Not sure how it's set from the IDE, but this is a solid TODO for later documentation. +// +// TODO: replace the default value for server-addr with the VM IP +object Client { + private val logger = LoggerFactory.getLogger(Client::class.java) + + // Note: the defaults are set to the "public" matchmaking server. + // "public" because it requires a VPN or being on campus to access, but in theory, if we had + // a public server, this would be it. + private val serverAddr: String = System.getProperty("netrunner.server-addr", "10.212.27.55") + private val serverPort: Int = System.getProperty("netrunner.server-port", "80").toInt() + // TODO: figure out the certificate situation. Insecure connections are awful for a large array of reasons. + // On the other hand, it might not be a priority. It adds complexity (not stonks), and security isn't + // one of our quality attributes. + private val tls: Boolean = false + + val sock: Socket = IO.socket(URI((if (!tls) "ws" else "wss") + "://" + serverAddr + ":" + serverPort), + IO.Options.builder() + .setTransports(arrayOf(WebSocket.NAME)) + .build()) + + /** + * Reflects the object's internal connection status. + */ + @Suppress("MemberVisibilityCanBePrivate") + var sockStatus = ConnInfo() + + init { + sock.on(Socket.EVENT_CONNECT_ERROR) { it -> + logger.error("Received error"); + if (it[0] is Exception) { + logger.error("Error is exception: {}", (it[0] as Exception).stackTraceToString()) + synchronized(this) { + sockStatus.connecting = false + sockStatus.except = it[0] as Exception + } + sock.io().reconnection(false) + + } + } + + sock.on("connect") { + logger.info("Connected successfully") + synchronized(this) { + sockStatus = ConnInfo() + } + } + sock.on(Socket.EVENT_DISCONNECT) { + logger.warn("Disconnect received.") + synchronized(this) { + sockStatus.disconnected = true + } + } + } + + fun connect() { + if (sock.connected() || sock.io().isReconnecting) return; + logger.info("Connecting...") + sockStatus = ConnInfo(connecting = true) + sock.connect() + sock.io().reconnection(true) + } + +} \ No newline at end of file diff --git a/core/src/tdt4240/netrunner/game/ConnInfo.kt b/core/src/tdt4240/netrunner/game/ConnInfo.kt new file mode 100644 index 0000000000000000000000000000000000000000..2c328c02367cdb99b0ae5fb43725e8002eaacd13 --- /dev/null +++ b/core/src/tdt4240/netrunner/game/ConnInfo.kt @@ -0,0 +1,21 @@ +package tdt4240.netrunner.game + +data class ConnInfo( + /** + * Control variable; reflects whether there's an ongoing connection attempt or not. + * This variable is true in the time between calling connect(), and the connection + * either failing or connecting. + * + * sock.io() has `isReconnecting`, but it's unclear whether that applies to all + * connections or just reconnections. + */ + var connecting: Boolean = false, + /** + * Control variable; reflects whether or not the current connection has DC'd. + * That may or may not be accompanied by an Exception, though generally will if + * the connection failed or abruptly ended. + */ + var disconnected: Boolean = false, + var except: Exception? = null, +) { +} \ No newline at end of file diff --git a/core/src/tdt4240/netrunner/game/GameController.kt b/core/src/tdt4240/netrunner/game/GameController.kt new file mode 100644 index 0000000000000000000000000000000000000000..bf4268547af20c85632476af79a796eb54697572 --- /dev/null +++ b/core/src/tdt4240/netrunner/game/GameController.kt @@ -0,0 +1,95 @@ +package tdt4240.netrunner.game + +import com.badlogic.gdx.Gdx +import io.socket.client.Ack +import org.slf4j.LoggerFactory +import tdt4240.netrunner.Netrunner +import tdt4240.netrunner.model.game.EcsEngine +import tdt4240.netrunner.model.game.data.PlayerColor +import tdt4240.netrunner.model.requests.JoinGameRequest +import tdt4240.netrunner.model.response.EntityData +import tdt4240.netrunner.model.response.StatusCode +import tdt4240.netrunner.model.response.StatusResponse +import tdt4240.netrunner.model.util.gsonImpl +import tdt4240.netrunner.view.controllers.GroundRenderer +import tdt4240.netrunner.view.controllers.PlayerRenderer + +/** + * Client game processor + * + * Note that the instance also acts as a sync lock. Anyone invoking any data in this class, + * except certain methods, are required to do so in a synchronized(gameController) block + */ +class GameController(val game: Netrunner) { + var gameFound = false + private set + var failed = false + private set + + + val engine = EcsEngine().apply { + addController(PlayerRenderer(this@GameController, this)) + addController(GroundRenderer(this@GameController, this)) + } + + fun joinGame(username: String, playerColor: PlayerColor) { + + // Has to be registered early, or the players event can be missed, which is + // a bit of a problem. + // Actually, that's an understatement + registerGameEvents() + Client.sock.emit("join-matchmaking", gsonImpl.toJson( + JoinGameRequest(username, playerColor) + ), Ack { + val res = gsonImpl.fromJson(it[0] as String, StatusResponse::class.java) + + synchronized(this) { + if (res.status != StatusCode.OK) { + // TODO: more fine-grained error handling + failed = true + logger.error("Failed to connect: {}", res.message) + deregisterGameEvents() + } else { + gameFound = true + logger.info("Successfully connected to server") + + } + } + }) + } + + fun render() { + + engine.tick(Gdx.graphics.deltaTime.toDouble()) + engine.render() + } + + private fun registerGameEvents() { + Client.sock.on("entities") { + val entityData = gsonImpl.fromJson(it[0] as String, EntityData::class.java) + synchronized(engine) { + engine.entities = entityData.entities.toMutableList() + } + } + + logger.debug("Game events registered") + } + + fun deregisterGameEvents() { + // Note: Any registered `sock.on()`s made in this class MUST be deregistered here. + Client.sock.off("players") + + logger.debug("Game events deregistered") + // Note: this is not an elegant solution. It would be better for the server to emit + // some type of "game started" event, but it is what it is. We're like two weeks behind at this + // point. It's fine + // besides, it doesn't error out, and unless someone is obnoxiously fast, they won't have time + // to rejoin a game between this being sent and leaving being processed. + Client.sock.emit("leave-matchmaking") + } + + companion object { + private val logger = LoggerFactory.getLogger(GameController::class.java) + } + +} \ No newline at end of file diff --git a/core/src/tdt4240/netrunner/view/GameLoadingScreen.kt b/core/src/tdt4240/netrunner/view/GameLoadingScreen.kt new file mode 100644 index 0000000000000000000000000000000000000000..9657000d39d06b6fe90a6988e1aae9c4bc1e3933 --- /dev/null +++ b/core/src/tdt4240/netrunner/view/GameLoadingScreen.kt @@ -0,0 +1,70 @@ +package tdt4240.netrunner.view + +import com.badlogic.gdx.Gdx +import com.badlogic.gdx.Screen +import com.badlogic.gdx.graphics.Color +import com.badlogic.gdx.graphics.GL20 +import com.badlogic.gdx.scenes.scene2d.Stage +import com.badlogic.gdx.scenes.scene2d.ui.Table +import tdt4240.netrunner.Netrunner +import tdt4240.netrunner.game.GameController +import tdt4240.netrunner.model.game.data.PlayerColor + +class GameLoadingScreen(val game: Netrunner, val username: String, val color: PlayerColor) : Screen { + private lateinit var stage: Stage + private lateinit var table: Table + private lateinit var gameController: GameController + + override fun show() { + stage = Stage(game.uiViewport) + table = Table() + + table.skin = game.skin + table.setFillParent(true) + + stage.addActor(table.apply { + row() + add("Searching for a lobby...", "default-font", Color.WHITE) + }) + + gameController = GameController(game) + gameController.joinGame(username, color) + } + + override fun render(delta: Float) { + Gdx.gl.glClearColor(0f, 0f, 0f, 1f) + Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT) + + if (gameController.failed) { + // TODO: figure out a warning system + game.screen = MenuScreen(game) + dispose() + return + } else if (gameController.gameFound) { + game.screen = GameScreen(game, gameController) + dispose() + return + } + + stage.act(delta) + stage.draw() + } + + override fun resize(width: Int, height: Int) { + stage.viewport.update(width, height, true) + } + + override fun pause() { + } + + override fun resume() { + } + + override fun hide() { + } + + override fun dispose() { + stage.dispose() + } + +} \ No newline at end of file diff --git a/core/src/tdt4240/netrunner/view/GameScreen.kt b/core/src/tdt4240/netrunner/view/GameScreen.kt new file mode 100644 index 0000000000000000000000000000000000000000..d6b4761efea75a19c6019df89ce9a86483e1c6bf --- /dev/null +++ b/core/src/tdt4240/netrunner/view/GameScreen.kt @@ -0,0 +1,73 @@ +package tdt4240.netrunner.view + +import com.badlogic.gdx.Gdx +import com.badlogic.gdx.ScreenAdapter +import com.badlogic.gdx.graphics.Color +import com.badlogic.gdx.graphics.GL20 +import com.badlogic.gdx.graphics.OrthographicCamera +import com.badlogic.gdx.scenes.scene2d.InputEvent +import com.badlogic.gdx.scenes.scene2d.Stage +import com.badlogic.gdx.scenes.scene2d.ui.Table +import com.badlogic.gdx.scenes.scene2d.ui.TextButton +import com.badlogic.gdx.scenes.scene2d.utils.ClickListener +import tdt4240.netrunner.Netrunner +import tdt4240.netrunner.game.GameController + +class GameScreen(private val game: Netrunner, private val controller: GameController) : ScreenAdapter() { + private val stage = Stage(game.uiViewport) + private var cam = OrthographicCamera(Netrunner.MIN_WIDTH, Netrunner.MIN_HEIGHT) + + init { + val buttonStyle = TextButton.TextButtonStyle() + buttonStyle.font = game.skin.getFont("default-font") + buttonStyle.downFontColor = Color.RED; + + val exitButton = TextButton("Exit", buttonStyle) //bruk evt. game.skin + //if we want an exact position + //exitButton.setPosition(1000f, 10f) + // stage.addActor(exitButton) + + exitButton.addListener(object : ClickListener() { + override fun clicked(event: InputEvent?, x: Float, y: Float) { + game.screen = MenuScreen(game) + dispose() + } + }) + + val table = Table() + table.setFillParent(true) + table.row() + table.add(exitButton).pad(50f).expand().top().right() + table.row() + stage.addActor(table) + } + + override fun show() { + Gdx.input.inputProcessor = stage + } + + override fun render(delta: Float) { + Gdx.gl.glClearColor(0f, 0f, 0f, 1f) + Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT) + + cam.update() + game.batch.projectionMatrix = cam.combined + // The game has to be rendered prior to the UI for display order reasons + controller.render() + + + stage.act(delta) + stage.draw() + } + + override fun resize(width: Int, height: Int) { + super.resize(width, height) + stage.viewport.update(width, height, true) + } + + override fun dispose() { + stage.dispose() + controller.deregisterGameEvents() + } + +} diff --git a/core/src/tdt4240/netrunner/view/LoadingScreen.kt b/core/src/tdt4240/netrunner/view/LoadingScreen.kt new file mode 100644 index 0000000000000000000000000000000000000000..3e197e32f1b8678b0041a0070fdb67184b139062 --- /dev/null +++ b/core/src/tdt4240/netrunner/view/LoadingScreen.kt @@ -0,0 +1,80 @@ +package tdt4240.netrunner.view + +import com.badlogic.gdx.Gdx +import com.badlogic.gdx.Screen +import com.badlogic.gdx.graphics.Color +import com.badlogic.gdx.graphics.GL20 +import com.badlogic.gdx.scenes.scene2d.Stage +import com.badlogic.gdx.scenes.scene2d.ui.Table +import tdt4240.netrunner.Netrunner +import tdt4240.netrunner.game.Client + +class LoadingScreen(val game: Netrunner) : Screen { + private lateinit var stage: Stage + private lateinit var table: Table + + private var waiting = true + + override fun show() { + stage = Stage(game.uiViewport) + table = Table() + table.skin = game.skin + table.setFillParent(true) + + stage.addActor(table.apply { + row() + add("Connecting to server...", "default-font", Color.WHITE) + + }) + + Client.connect() + Gdx.input.inputProcessor = stage + } + + override fun render(delta: Float) { + Gdx.gl.glClearColor(0f, 0f, 0f, 1f) + Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT) + + if (waiting) { + if (!Client.sockStatus.connecting) { + waiting = false + + if (Client.sock.connected()) { + // we've connected; proceed to the menu + game.screen = MenuScreen(game) + dispose() + } else if (Client.sockStatus.except != null) { + // Failed to connect; notify the user. + // The default assumption is heavy load and not a + // server error, because to an end user, it doesn't matter. + table.clear() + table.row() + table.add("Failed to connect! :(\nTry again later", + "default-font", + Color.WHITE) + } + } + } + + stage.act(delta) + stage.draw() + } + + override fun resize(width: Int, height: Int) { + stage.viewport.update(width, height, true) + } + + override fun pause() { + } + + override fun resume() { + } + + override fun hide() { + } + + override fun dispose() { + stage.dispose() + } + +} \ No newline at end of file diff --git a/core/src/tdt4240/netrunner/view/MenuScreen.kt b/core/src/tdt4240/netrunner/view/MenuScreen.kt new file mode 100644 index 0000000000000000000000000000000000000000..7e3ddc3fe5f8d576eae48ab242f8cd1c25458c1b --- /dev/null +++ b/core/src/tdt4240/netrunner/view/MenuScreen.kt @@ -0,0 +1,82 @@ +package tdt4240.netrunner.view + +import com.badlogic.gdx.Gdx +import com.badlogic.gdx.ScreenAdapter +import com.badlogic.gdx.graphics.Color +import com.badlogic.gdx.graphics.GL20 +import com.badlogic.gdx.scenes.scene2d.InputEvent +import com.badlogic.gdx.scenes.scene2d.Stage +import com.badlogic.gdx.scenes.scene2d.ui.* +import com.badlogic.gdx.scenes.scene2d.utils.ClickListener +import tdt4240.netrunner.Netrunner +import tdt4240.netrunner.model.game.data.PlayerColor + + +class MenuScreen(private val game: Netrunner) : ScreenAdapter() { + + private lateinit var stage: Stage + private lateinit var usernameInput: TextField + private lateinit var colorSelectBox: SelectBox<String> + + override fun show() { + stage = Stage(game.uiViewport) + + usernameInput = TextField("", game.skin) + val labelStyle = Label.LabelStyle(game.skin.getFont("default-font"), Color.WHITE) + + val label = Label("Username:", labelStyle) + val avatarLabel = Label("Choice of avatar:", labelStyle) + + colorSelectBox = SelectBox<String>(game.skin) + colorSelectBox.setItems(*PlayerColor.uiRepresentations) + + val playButton = TextButton("Play", game.skin) + + playButton.addListener(object : ClickListener() { + override fun clicked(event: InputEvent?, x: Float, y: Float) { + // TODO: validate username text and player color selection (at least if we add a "none" as a default opt) + game.screen = GameLoadingScreen( + game, + usernameInput.text, + PlayerColor.reverseString[colorSelectBox.selected]!! + ) + dispose() + } + }) + + val table = Table() + table.setFillParent(true) + table.add(label) + table.row() + table.add(usernameInput).size(500f, 100f) + table.row() + table.add(avatarLabel) + table.row() + table.add(colorSelectBox).size(400f, 100f) + table.row() + table.add(playButton).pad(20f).size(300f, 100f) + table.row() + //table.add(chosenColorLabel) + table.row() + stage.addActor(table) + + Gdx.input.inputProcessor = stage + } + + override fun render(delta: Float) { + + Gdx.gl.glClearColor(0.3f, 0f, 0.5f, 0.3f) + Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT) + + stage.act(delta) + stage.draw() + } + + override fun resize(width: Int, height: Int) { + stage.viewport.update(width, height, true) + } + + override fun dispose() { + stage.dispose() + } +} diff --git a/core/src/tdt4240/netrunner/view/controllers/GroundRenderer.kt b/core/src/tdt4240/netrunner/view/controllers/GroundRenderer.kt new file mode 100644 index 0000000000000000000000000000000000000000..06196862dafe87c3775bcb3d7f76c56217d155ce --- /dev/null +++ b/core/src/tdt4240/netrunner/view/controllers/GroundRenderer.kt @@ -0,0 +1,32 @@ +package tdt4240.netrunner.view.controllers + +import com.badlogic.gdx.graphics.glutils.ShapeRenderer +import tdt4240.netrunner.game.GameController +import tdt4240.netrunner.model.game.EcsController +import tdt4240.netrunner.model.game.EcsEngine +import tdt4240.netrunner.model.game.components.level.GroundComponent + +class GroundRenderer(val gController: GameController, engine: EcsEngine) : EcsController(engine) { + override fun render() { + gController.game.shapeRenderer.apply { + begin(ShapeRenderer.ShapeType.Filled) + val ground = super.ecs.getEntitiesByComponent(GroundComponent::class.java) + + for (block in ground) { + // TODO: explore local culling or possibly GL_CULL_FACE. + // The latter is significantly easier to implement, but does mean we have to process + // everything + + block.getComponent(GroundComponent::class.java)!!.let { + // This isn't great, but polygons only support line mode :/ + this.rect(it.startPos.x, 0f, + it.endPos.x, it.endPos.y) + + } + + } + + end() + } + } +} \ No newline at end of file diff --git a/core/src/tdt4240/netrunner/view/controllers/PlayerRenderer.kt b/core/src/tdt4240/netrunner/view/controllers/PlayerRenderer.kt new file mode 100644 index 0000000000000000000000000000000000000000..38415370903f2de30d36fe9641a7d98592beb3ca --- /dev/null +++ b/core/src/tdt4240/netrunner/view/controllers/PlayerRenderer.kt @@ -0,0 +1,36 @@ +package tdt4240.netrunner.view.controllers + +import tdt4240.netrunner.game.GameController +import tdt4240.netrunner.model.game.EcsController +import tdt4240.netrunner.model.game.EcsEngine +import tdt4240.netrunner.model.game.components.dynamic.PositionComponent +import tdt4240.netrunner.model.game.components.living.PlayerComponent +import tdt4240.netrunner.model.game.components.living.PlayerGraphicsComponent + +class PlayerRenderer(val gController: GameController, engine: EcsEngine) : EcsController(engine) { + override fun render() { + gController.game.batch.begin() + val players = super.ecs.getEntitiesByComponent(PlayerComponent::class.java) + for (player in players) { + player.getComponent(PositionComponent::class.java).also { pos -> + if (pos == null) { + throw RuntimeException("Misconfiguration") + } + player.getComponent(PlayerGraphicsComponent::class.java).also { graphics -> + val color = graphics!!.color + gController.game.apply { + batch.draw(characterTextures[color], pos.pos.x, pos.pos.y) + } + } + player.getComponent(PlayerComponent::class.java).also {playerComponent -> + // TODO: render username + // TODO: remove when there's code here. + // This exists to silence the compiler + @Suppress("UNUSED_EXPRESSION") + null + } + } + } + gController.game.batch.end() + } +} \ No newline at end of file diff --git a/desktop/src/tdt4240/netrunner/DesktopLauncher.kt b/desktop/src/tdt4240/netrunner/DesktopLauncher.kt index f9840b4e6fcb16e6097b417d14357453f74e2e2b..000ea5636a32d66ad4505605a1e1cc23b953a64b 100644 --- a/desktop/src/tdt4240/netrunner/DesktopLauncher.kt +++ b/desktop/src/tdt4240/netrunner/DesktopLauncher.kt @@ -14,4 +14,4 @@ object DesktopLauncher { config.setTitle("Netrunner") Lwjgl3Application(Netrunner(), config) } -} \ No newline at end of file +} diff --git a/docs/Building-and-running.md b/docs/Building-and-running.md new file mode 100644 index 0000000000000000000000000000000000000000..9dabbee4f43cb726e908bfa4361ad8c7992b8c5e --- /dev/null +++ b/docs/Building-and-running.md @@ -0,0 +1,44 @@ +# Building, running, and deploying. + +Building components can be done from the command line or your favorite IDE. This will not be demonstrated in detail, as this is fairly straight-forward for the components involved. + +## Setting up a debug environment + +**Note:** this must be re-done on each network, as your IP will not remain constant. Redoing it on known networks may also be necessary depending on network settings, but you'll notice that if the game fails to connect. + +For debug purposes, it's often more convenient to use a local server. Due to the IP issues this introduces, you're required to specify an IP to connect to. + +This is done by creating `assets/local-game.properties`. This file must contain: + +``` +netrunner.server-addr=xxx.xxx.xxx.xxx +netrunner.server-port=5000 +``` + +The port is 5000 by default in the debug build, so this generally doesn't need to be changed. To find the IP to use, this depends on which configuration you're running + + +### Desktop debugging + +For desktop debugging, the IP can simply be `127.0.0.1`. + +### Android debugging/remote server access + +To connect to the server from Android, you have to specify the local IP. How you go about this depends on your OS: + +* Linux: `ifconfig`, `ip a`, look for the correct network adapter, or `hostname -I`. Note that some commands may require additional packages. GUI options may exist for different distros, but linking all of them is an exercise in futility. That said, gnome-based DEs shows the IP in the network settings. +* Windows: `ipconfig`, or GUI options. See: https://www.digitalcitizen.life/find-ip-address-windows/ +* MacOS: `ifconfig`, or GUI options. See: https://www.wikihow.com/Find-Your-IP-Address-on-a-Mac + +## Deploying + +**Note:** prior to deployment, specifically of the desktop and Android apps, `assets/game-local.properties` MUST be removed. This may change in the future if we figure out how to do conditional asset inclusion in different builds. + +To deploy the project, the `:dist` target can be used. Example commands: + +* Desktop: `./gradlew desktop:dist` +* Server: `./gradlew server:dist` + +Android is an exception, and uses a different target: `./gradlew android:assembleRelease` + +See also [LibGDX' own documentation on deployment](https://libgdx.com/wiki/deployment/deploying-your-application) \ No newline at end of file diff --git a/etc/systemd/system/netrunner-server.service b/etc/systemd/system/netrunner-server.service new file mode 100644 index 0000000000000000000000000000000000000000..94d2317eab8192fcdeeff30ec89fa11bf2b04c39 --- /dev/null +++ b/etc/systemd/system/netrunner-server.service @@ -0,0 +1,16 @@ +[Unit] +Description=Netrunner server instance +Wants=network-online.target +After=network.target + +[Service] +Restart=on-failure +RestartSec=120s +Type=idle +WorkingDirectory=/app +# This is fragile, but it's fine. we're not going to iterate past 1.0 anyway. The simple fix is presumably script fuckery in a post-install script +# to move it to the expected location +ExecStart=java -Dnetrunner.server-port=80 -jar /app/server/build/libs/server-1.0.jar + +[Install] +WantedBy=multi-user.target diff --git a/model/src/main/kotlin/tdt4240/netrunner/model/game/EcsController.kt b/model/src/main/kotlin/tdt4240/netrunner/model/game/EcsController.kt new file mode 100644 index 0000000000000000000000000000000000000000..c90fab47208a82f1a33265894c37d7397e45b3fb --- /dev/null +++ b/model/src/main/kotlin/tdt4240/netrunner/model/game/EcsController.kt @@ -0,0 +1,16 @@ +package tdt4240.netrunner.model.game + +import tdt4240.netrunner.model.game.components.Component + +/** + * Controller for the ECS system, for a type of component. + * + * Implementations must override tick() or render() at least. + */ +open class EcsController (protected val ecs: EcsEngine) { + + open fun tick(delta: Double) {} + + + open fun render() {} +} \ No newline at end of file diff --git a/model/src/main/kotlin/tdt4240/netrunner/model/game/EcsEngine.kt b/model/src/main/kotlin/tdt4240/netrunner/model/game/EcsEngine.kt new file mode 100644 index 0000000000000000000000000000000000000000..126a12bca10fea077fadde59dc3bea22207572d3 --- /dev/null +++ b/model/src/main/kotlin/tdt4240/netrunner/model/game/EcsEngine.kt @@ -0,0 +1,38 @@ +package tdt4240.netrunner.model.game + +import tdt4240.netrunner.model.game.components.Component + +class EcsEngine { + // Ashley (LibGDX's own ECS library) also stores entities in a single list. + // This isn't fantastic, but with how limited java is (C++ templates would be far better for this), + // there aren't all that many great ways to store entities + var entities = mutableListOf<Entity>() + private val controllers = mutableListOf<EcsController>() + + fun addEntity(e: Entity) { + entities.add(e) + } + + fun tick(delta: Double) { + for (controller in controllers) { + controller.tick(delta) + } + } + + fun render() { + for (controller in controllers) { + controller.render() + } + } + + fun <T : Component> getEntitiesByComponent(clazz: Class<T>): List<Entity> { + return entities.filter { + it.getComponent(clazz) != null + } + } + + fun addController(controller: EcsController) { + controllers.add(controller) + } + +} \ No newline at end of file diff --git a/model/src/main/kotlin/tdt4240/netrunner/model/game/Entity.kt b/model/src/main/kotlin/tdt4240/netrunner/model/game/Entity.kt index 48cbdd81a1f217489e9429e8bcd98cdef2689bde..f2c0fa333137257239b4389563a299d3677113d5 100644 --- a/model/src/main/kotlin/tdt4240/netrunner/model/game/Entity.kt +++ b/model/src/main/kotlin/tdt4240/netrunner/model/game/Entity.kt @@ -2,11 +2,15 @@ package tdt4240.netrunner.model.game import tdt4240.netrunner.model.game.components.Component -class Entity(val components: MutableSet<Component>) { - inline fun <reified T> getComponent(): T? { - for (component in components) { - if (component is T) return component - } - return null +class Entity(initComponents: MutableSet<Component>) { + val components = initComponents.associateBy { + it::class.java.name } + + @Suppress("UNCHECKED_CAST") + fun <T : Component> getComponent(clazz: Class<T>): T? { + return components[clazz.name] as T? + } + + } \ No newline at end of file diff --git a/model/src/main/kotlin/tdt4240/netrunner/model/game/components/dynamic/GravityComponent.kt b/model/src/main/kotlin/tdt4240/netrunner/model/game/components/dynamic/GravityComponent.kt new file mode 100644 index 0000000000000000000000000000000000000000..20c834b1adf67b27f10de335e85baefc79921e91 --- /dev/null +++ b/model/src/main/kotlin/tdt4240/netrunner/model/game/components/dynamic/GravityComponent.kt @@ -0,0 +1,13 @@ +package tdt4240.netrunner.model.game.components.dynamic + +import tdt4240.netrunner.model.game.components.Component +import tdt4240.netrunner.model.util.Vec2f + + +class GravityComponent(val gravSpeed: Vec2f = Vec2f(0f, GRAVITY)) : Component { + override val componentName: String = this::class.java.name + + companion object { + const val GRAVITY = 9.81f + } +} \ No newline at end of file diff --git a/model/src/main/kotlin/tdt4240/netrunner/model/game/components/level/GroundComponent.kt b/model/src/main/kotlin/tdt4240/netrunner/model/game/components/level/GroundComponent.kt new file mode 100644 index 0000000000000000000000000000000000000000..6dc5d0088c66217fa0cdf03caf431d7f3a97275b --- /dev/null +++ b/model/src/main/kotlin/tdt4240/netrunner/model/game/components/level/GroundComponent.kt @@ -0,0 +1,40 @@ +package tdt4240.netrunner.model.game.components.level + +import tdt4240.netrunner.model.game.components.Component +import tdt4240.netrunner.model.util.Vec2f + +/** + * ECS component defining the ground. + * + * Note that {@param startPos} and {@param endPos} define the top-left and top-right corners + * respectively. The bottom left and right corners are implicitly at height 0, at the x coordinate + * defined by the matching position. + */ +data class GroundComponent(val startPos: Vec2f, val endPos: Vec2f) : Component { + init { + if (startPos > endPos) { + // Enforce order + throw IllegalArgumentException("start has to be before end") + } else if (startPos == endPos) { + // Enforce min size + throw IllegalArgumentException("Cannot have a zero-width ground component") + } else if (endPos.x - startPos.x > MAX_WIDTH) { + // Enforce max size + throw IllegalArgumentException("Length exceeds max component width") + } + } + // Deserialisation necessity + override val componentName: String = this::class.java.name + + fun yAt(x: Float) : Float { + val slope = endPos.y - startPos.y + val slopePerX = slope / (endPos.x - startPos.x) + + val relativeX = startPos.x - x + + return slopePerX * relativeX + } + companion object { + const val MAX_WIDTH = 100f + } +} \ No newline at end of file diff --git a/model/src/main/kotlin/tdt4240/netrunner/model/game/components/level/PlatformComponent.kt b/model/src/main/kotlin/tdt4240/netrunner/model/game/components/level/PlatformComponent.kt new file mode 100644 index 0000000000000000000000000000000000000000..8833dacdb6bc625f00da85df96290ad8993a95ef --- /dev/null +++ b/model/src/main/kotlin/tdt4240/netrunner/model/game/components/level/PlatformComponent.kt @@ -0,0 +1,7 @@ +package tdt4240.netrunner.model.game.components.level + +import tdt4240.netrunner.model.game.components.Component + +data class PlatformComponent(val width: Int) : Component { + override val componentName: String = this::class.java.name +} \ No newline at end of file diff --git a/model/src/main/kotlin/tdt4240/netrunner/model/game/components/living/PlayerGraphicsComponent.kt b/model/src/main/kotlin/tdt4240/netrunner/model/game/components/living/PlayerGraphicsComponent.kt index fea96586f791583442389f650f5f6ed445543f0e..febb8c65d99f6210e93c216245a00ebf287040ed 100644 --- a/model/src/main/kotlin/tdt4240/netrunner/model/game/components/living/PlayerGraphicsComponent.kt +++ b/model/src/main/kotlin/tdt4240/netrunner/model/game/components/living/PlayerGraphicsComponent.kt @@ -1,9 +1,9 @@ package tdt4240.netrunner.model.game.components.living import tdt4240.netrunner.model.game.components.Component -import tdt4240.netrunner.model.util.Vec3f +import tdt4240.netrunner.model.game.data.PlayerColor -class PlayerGraphicsComponent(var color: Vec3f) : Component { +class PlayerGraphicsComponent(var color: PlayerColor) : Component { override val componentName: String = this::class.java.name } \ No newline at end of file diff --git a/model/src/main/kotlin/tdt4240/netrunner/model/game/data/PlayerColor.kt b/model/src/main/kotlin/tdt4240/netrunner/model/game/data/PlayerColor.kt new file mode 100644 index 0000000000000000000000000000000000000000..e03ead08b5083e718ae86098397d3c97ccff4a50 --- /dev/null +++ b/model/src/main/kotlin/tdt4240/netrunner/model/game/data/PlayerColor.kt @@ -0,0 +1,14 @@ +package tdt4240.netrunner.model.game.data + +enum class PlayerColor(val graphic: String, val uiRepresentation: String) { + WHITE("whiteball.png", "White"); + + companion object { + val uiRepresentations by lazy { + values().map { it.uiRepresentation}.toTypedArray() + } + val reverseString by lazy { + values().associateBy { it.uiRepresentation } + } + } +} \ No newline at end of file diff --git a/model/src/main/kotlin/tdt4240/netrunner/model/game/factories/GroundFactory.kt b/model/src/main/kotlin/tdt4240/netrunner/model/game/factories/GroundFactory.kt new file mode 100644 index 0000000000000000000000000000000000000000..5a23e43f0ee273d45138c97298efc4094e3da406 --- /dev/null +++ b/model/src/main/kotlin/tdt4240/netrunner/model/game/factories/GroundFactory.kt @@ -0,0 +1,26 @@ +package tdt4240.netrunner.model.game.factories + +import tdt4240.netrunner.model.game.Entity +import tdt4240.netrunner.model.game.components.level.GroundComponent +import tdt4240.netrunner.model.util.Vec2f + +class GroundFactory { + private lateinit var startPos: Vec2f + private lateinit var endPos: Vec2f + + fun withStartPos(startPos: Vec2f) : GroundFactory { + this.startPos = startPos + return this + } + + fun withEndPos(endPos: Vec2f): GroundFactory { + this.endPos = endPos + return this + } + + fun build() : Entity { + return Entity(mutableSetOf( + GroundComponent(startPos, endPos) + )) + } +} \ No newline at end of file diff --git a/model/src/main/kotlin/tdt4240/netrunner/model/game/factories/PlatformFactory.kt b/model/src/main/kotlin/tdt4240/netrunner/model/game/factories/PlatformFactory.kt new file mode 100644 index 0000000000000000000000000000000000000000..a5c8dfaa76409a66e3ee0289c6e8412928581c0f --- /dev/null +++ b/model/src/main/kotlin/tdt4240/netrunner/model/game/factories/PlatformFactory.kt @@ -0,0 +1,17 @@ +package tdt4240.netrunner.model.game.factories + +import tdt4240.netrunner.model.game.Entity +import tdt4240.netrunner.model.game.components.dynamic.PositionComponent +import tdt4240.netrunner.model.game.components.level.PlatformComponent +import tdt4240.netrunner.model.util.Vec2f + +class PlatformFactory { + lateinit var left: Vec2f + var width: Int = 0 + + fun build() = Entity(mutableSetOf( + PositionComponent(left), + PlatformComponent(width) + )) + +} \ No newline at end of file diff --git a/model/src/main/kotlin/tdt4240/netrunner/model/game/factories/PlayerFactory.kt b/model/src/main/kotlin/tdt4240/netrunner/model/game/factories/PlayerFactory.kt index 478e25895f05f2f80326679b196494cbf34f274a..274c887862c0249131e79182110cf0ae8374cd01 100644 --- a/model/src/main/kotlin/tdt4240/netrunner/model/game/factories/PlayerFactory.kt +++ b/model/src/main/kotlin/tdt4240/netrunner/model/game/factories/PlayerFactory.kt @@ -1,20 +1,19 @@ package tdt4240.netrunner.model.game.factories import tdt4240.netrunner.model.game.Entity -import tdt4240.netrunner.model.game.components.Component import tdt4240.netrunner.model.game.components.dynamic.PositionComponent import tdt4240.netrunner.model.game.components.dynamic.VelocityComponent import tdt4240.netrunner.model.game.components.living.LivingComponent import tdt4240.netrunner.model.game.components.living.PlayerComponent import tdt4240.netrunner.model.game.components.living.PlayerGraphicsComponent +import tdt4240.netrunner.model.game.data.PlayerColor import tdt4240.netrunner.model.util.Vec2f -import tdt4240.netrunner.model.util.Vec3f class PlayerFactory { // Leveraging lateinit properties makes it easier to validate completion of mandatory properties private lateinit var username: String private lateinit var initPosition: Vec2f - private var color: Vec3f = Vec3f(1f, .5f, 1f) + private var color: PlayerColor = PlayerColor.WHITE fun withPosition(pos: Vec2f): PlayerFactory { initPosition = pos @@ -24,7 +23,7 @@ class PlayerFactory { this.username = username return this } - fun withColor(color: Vec3f): PlayerFactory { + fun withColor(color: PlayerColor): PlayerFactory { this.color = color return this } diff --git a/model/src/main/kotlin/tdt4240/netrunner/model/requests/JoinGameRequest.kt b/model/src/main/kotlin/tdt4240/netrunner/model/requests/JoinGameRequest.kt index de8e90986e36b920e60f18b351a5ab4c152e3208..ee948849a05b25760654e76f10d51c74d0b8860b 100644 --- a/model/src/main/kotlin/tdt4240/netrunner/model/requests/JoinGameRequest.kt +++ b/model/src/main/kotlin/tdt4240/netrunner/model/requests/JoinGameRequest.kt @@ -1,5 +1,5 @@ package tdt4240.netrunner.model.requests -import tdt4240.netrunner.model.util.Vec3f +import tdt4240.netrunner.model.game.data.PlayerColor -data class JoinGameRequest(val username: String, val color: Vec3f) +data class JoinGameRequest(val username: String, val color: PlayerColor) diff --git a/model/src/main/kotlin/tdt4240/netrunner/model/response/EntityData.kt b/model/src/main/kotlin/tdt4240/netrunner/model/response/EntityData.kt new file mode 100644 index 0000000000000000000000000000000000000000..626f4e8ef254b29a61b520d8c4dcd4eb9cde87e0 --- /dev/null +++ b/model/src/main/kotlin/tdt4240/netrunner/model/response/EntityData.kt @@ -0,0 +1,7 @@ +package tdt4240.netrunner.model.response + +import tdt4240.netrunner.model.game.Entity + +data class EntityData(var entities: List<Entity>) { + operator fun get(idx: Int) = entities[idx] +} \ No newline at end of file diff --git a/model/src/main/kotlin/tdt4240/netrunner/model/response/PlayerData.kt b/model/src/main/kotlin/tdt4240/netrunner/model/response/PlayerData.kt deleted file mode 100644 index 6ba49b97c1fd0a010ba26ae891cd166fb0ec42bd..0000000000000000000000000000000000000000 --- a/model/src/main/kotlin/tdt4240/netrunner/model/response/PlayerData.kt +++ /dev/null @@ -1,7 +0,0 @@ -package tdt4240.netrunner.model.response - -import tdt4240.netrunner.model.game.Entity - -data class PlayerData(var players: List<Entity>) { - operator fun get(idx: Int) = players[idx] -} \ No newline at end of file diff --git a/model/src/main/kotlin/tdt4240/netrunner/model/util/Vec2f.kt b/model/src/main/kotlin/tdt4240/netrunner/model/util/Vec2f.kt index 1eb685d0bc1458b21b558b7c6db0f303ff6bfa3f..908ffbfc1b20cb38a63b2f71e86636151a50d8d0 100644 --- a/model/src/main/kotlin/tdt4240/netrunner/model/util/Vec2f.kt +++ b/model/src/main/kotlin/tdt4240/netrunner/model/util/Vec2f.kt @@ -1,6 +1,40 @@ package tdt4240.netrunner.model.util +import kotlin.math.pow +import kotlin.math.sqrt + data class Vec2f(var x: Float, var y: Float) { constructor() : this(0f, 0f) {} - // TODO: operator funcs (+, -) + + operator fun compareTo(other: Vec2f): Int = when { + x > other.x -> 1 + x < other.x -> -1 + y > other.y -> 1 + y < other.y -> -1 + else -> 0 + } + + operator fun times(constant: Float) : Vec2f = Vec2f(x * constant, y * constant) + operator fun times(constant: Double) : Vec2f = Vec2f(x * constant.toFloat(), y * constant.toFloat()) + + operator fun minus(other: Vec2f): Vec2f + = Vec2f(x - other.x, y - other.y) + operator fun plus(other: Vec2f): Vec2f + = Vec2f(x + other.x, y + other.y) + + operator fun unaryMinus() = Vec2f(-x, -y) + operator fun unaryPlus() = Vec2f(x, y) + + operator fun plusAssign(other: Vec2f) { + x += other.x + y += other.y + } + + operator fun minusAssign(other: Vec2f) { + x -= other.x + y -= other.y + } + + fun len(origin: Vec2f = Vec2f(0f, 0f)) + = sqrt((x - origin.x).pow(2) + (y - origin.y).pow(2)) } \ No newline at end of file diff --git a/model/src/test/kotlin/tdt4240/netrunner/model/util/VecTests.kt b/model/src/test/kotlin/tdt4240/netrunner/model/util/VecTests.kt new file mode 100644 index 0000000000000000000000000000000000000000..27ed5d4aadfb8017ecd7dd516c116f8f47508a9d --- /dev/null +++ b/model/src/test/kotlin/tdt4240/netrunner/model/util/VecTests.kt @@ -0,0 +1,84 @@ +package tdt4240.netrunner.model.util + +import kotlin.test.* +import kotlin.math.* + +class VecTests { + @Test + fun testVec2RelativeComparisonOperators() { + val v1 = Vec2f(0f, 0f) + val v2 = Vec2f(1f, 0f) + val v3 = Vec2f(0f, 1f) + val v4 = Vec2f(-1f, -1f) + + assertTrue(v2 > v1) + assertTrue(v2 > v3) + assertTrue(v3 > v4) + assertTrue(v2 > v3) + + assertFalse(v1 > v2) + assertFalse(v3 > v2) + assertFalse(v4 > v3) + assertFalse(v3 > v2) + + assertTrue(v1 < v2) + assertTrue(v3 < v2) + assertTrue(v4 < v3) + assertTrue(v3 < v2) + + val v5 = Vec2f(0.5f, 0.5f) + assertTrue(v5 > v1) + assertTrue(v1 < v5) + assertFalse(v1 > v5) + assertFalse(v5 < v1) + + assertFalse(v1 < v1) + assertFalse(v1 > v1) + assertTrue(v1 >= v1) + assertTrue(v1 <= v1) + } + + @Test + fun testVec2PlusMinusAssign() { + val v1 = Vec2f(0f, 0f) + val v2 = Vec2f(4f, 1f) + val v3 = Vec2f(-1f, 3f) + assertEquals(v2, v1 + v2) + assertEquals(Vec2f(3f, 4f), v2 + v3) + assertEquals(v3, v3 + v1) + + assertEquals(v3, v3 - v1) + assertEquals(v2, v2 - v1) + assertEquals(-v2, v1 - v2) + + val v2Cache = v2.copy() + val v3Cache = v3.copy() + v2 += v3 + assertEquals(v3Cache, v3) + assertNotEquals(v2Cache, v2) + + assertEquals(Vec2f(3f, 4f), v2) + v2 -= v3 + assertEquals(v2Cache, v2) + assertEquals(v3Cache, v3) + + } + + @Test + fun testVecLength() { + val v1 = Vec2f(1f, 1f) + val v2 = Vec2f(2f, 2f) + + val v3 = Vec2f(4f, 1f) + + assertEquals(sqrt(2f), v1.len()) + assertEquals(2f * sqrt(2f), v2.len()) + assertEquals(sqrt(2f), v2.len(v1)) + assertEquals(sqrt(2f), v1.len(v2)) + + assertEquals(3f, v3.len(v1)) + assertEquals(3f, v1.len(v3)) + + + } +} \ No newline at end of file diff --git a/server-bootstrap.sh b/server-bootstrap.sh new file mode 100755 index 0000000000000000000000000000000000000000..38565368af5d3136645f96510c4fc6d92e4d4341 --- /dev/null +++ b/server-bootstrap.sh @@ -0,0 +1,10 @@ +#!/usr/bin/bash +if [ "$EUID" -ne 0 ]; then + echo "This script requires root access." + exit +fi + +apt update +apt install -y curl unzip zip openjdk-17-jdk-headless + +cp etc / -r diff --git a/server/src/main/java/tdt4240/netrunner/game/WorldGen.kt b/server/src/main/java/tdt4240/netrunner/game/WorldGen.kt new file mode 100644 index 0000000000000000000000000000000000000000..9e44e1db537dc3960a447ea67bb28322b5e61059 --- /dev/null +++ b/server/src/main/java/tdt4240/netrunner/game/WorldGen.kt @@ -0,0 +1,26 @@ +package tdt4240.netrunner.game + +import tdt4240.netrunner.model.game.Entity +import tdt4240.netrunner.model.game.components.level.GroundComponent +import tdt4240.netrunner.model.game.factories.GroundFactory +import tdt4240.netrunner.model.util.Vec2f +import tdt4240.netrunner.server.game.GameRoom +import kotlin.math.roundToInt + +class WorldGen(val room: GameRoom) { + fun genGround(length: Int) : List<Entity> { + val entities = mutableListOf<Entity>() + for (i in -3..(length / GroundComponent.MAX_WIDTH).roundToInt() + 2) { + entities.add(GroundFactory() + .withStartPos(Vec2f(i * GroundComponent.MAX_WIDTH, BASE_HEIGHT)) + .withEndPos(Vec2f((i + 1) * GroundComponent.MAX_WIDTH, BASE_HEIGHT)) + .build() + ) + } + return entities + } + + companion object { + const val BASE_HEIGHT = 100f + } +} \ No newline at end of file diff --git a/server/src/main/java/tdt4240/netrunner/game/controllers/MovementController.kt b/server/src/main/java/tdt4240/netrunner/game/controllers/MovementController.kt new file mode 100644 index 0000000000000000000000000000000000000000..a423935116dc8c83814380757beb98e89076e1df --- /dev/null +++ b/server/src/main/java/tdt4240/netrunner/game/controllers/MovementController.kt @@ -0,0 +1,40 @@ +package tdt4240.netrunner.game.controllers + +import tdt4240.netrunner.model.game.EcsController +import tdt4240.netrunner.model.game.EcsEngine +import tdt4240.netrunner.model.game.components.dynamic.GravityComponent +import tdt4240.netrunner.model.game.components.dynamic.PositionComponent +import tdt4240.netrunner.model.game.components.dynamic.VelocityComponent +import tdt4240.netrunner.model.game.components.level.GroundComponent + +class MovementController(ecs: EcsEngine) : EcsController(ecs) { + override fun tick(delta: Double) { + for (entity in super.ecs.entities) { + entity.getComponent(PositionComponent::class.java)?.let { pos -> + entity.getComponent(VelocityComponent::class.java)?.let { vel -> + val groundComponent = super.ecs.getEntitiesByComponent(GroundComponent::class.java) + .map { + it.getComponent(GroundComponent::class.java)!! + }.firstOrNull { component -> + pos.pos >= component.startPos && pos.pos < component.endPos + } + + + val groundY = groundComponent?.yAt(pos.pos.x) + if (groundY != null && pos.pos.y <= groundY) { + pos.pos.y = groundY + vel.velocity.y = 0f + } else { + entity.getComponent(GravityComponent::class.java)?.let { gravity -> + // Update gravity acceleration + vel.velocity.plusAssign(gravity.gravSpeed * delta) + } + } + + // Increment velocity + pos.pos.plusAssign(vel.velocity) + } + } + } + } +} \ No newline at end of file diff --git a/server/src/main/java/tdt4240/netrunner/server/ServerLauncher.kt b/server/src/main/java/tdt4240/netrunner/server/ServerLauncher.kt index 0d473d60a52bf05ca1f257bcc5c0e3226f7a962b..a41c988fddd240059475a15677951a4e2c891f13 100644 --- a/server/src/main/java/tdt4240/netrunner/server/ServerLauncher.kt +++ b/server/src/main/java/tdt4240/netrunner/server/ServerLauncher.kt @@ -13,10 +13,8 @@ import org.eclipse.jetty.server.ServerConnector import org.eclipse.jetty.server.handler.HandlerList import org.eclipse.jetty.servlet.ServletContextHandler import org.eclipse.jetty.servlet.ServletHolder -import org.eclipse.jetty.util.log.Log -import org.eclipse.jetty.util.log.Logger import org.eclipse.jetty.websocket.server.WebSocketUpgradeFilter -import tdt4240.netrunner.server.controllers.AuthController +import org.slf4j.LoggerFactory import tdt4240.netrunner.server.controllers.GameController import java.net.InetSocketAddress import javax.servlet.http.HttpServlet @@ -31,10 +29,12 @@ import javax.servlet.http.HttpServletResponse * @param port The port to listen to. Defaults to 5000, though this is primarily recommended for * production use. The port should be set to a different value for tests. */ -class ServerLauncher(val port: Int = 5000) { +class ServerLauncher(val port: Int = System.getProperty("netrunner.server-port", "5000").toInt()) { /** * The server itself. */ + // No. Just no. No one likes this, IntelliJ + @Suppress("MemberVisibilityCanBePrivate") val jettyServer = Server(InetSocketAddress("127.0.0.1", port)).apply { connectors = arrayOf( ServerConnector(this).also { @@ -44,9 +44,11 @@ class ServerLauncher(val port: Int = 5000) { ) } + @Suppress("MemberVisibilityCanBePrivate") val engineIO = EngineIoServer(EngineIoServerOptions.newFromDefault().apply { // Placeholder for potential later use }) + @Suppress("MemberVisibilityCanBePrivate") val socketIO = SocketIoServer(engineIO, SocketIoServerOptions.newFromDefault().apply { // Placeholder for potential later use }) @@ -55,11 +57,11 @@ class ServerLauncher(val port: Int = 5000) { * The controller for authentication-related processes */ val controllers = listOf( - AuthController(), GameController(socketIO), ) init { + logger.info("Port set to {}", port) // Server config {{{ // Registering socket.io is done via a servlet, which requires overriding a bunch of this stuff val servletHandler = ServletContextHandler(ServletContextHandler.SESSIONS) @@ -96,6 +98,7 @@ class ServerLauncher(val port: Int = 5000) { on("connection") { data -> // Attempt to parse the client val client = data.firstOrNull() as? SocketIoSocket + logger.info("Received connection from {} (ID: {})", "UNK:NO IP SUPPORT", client?.id ?: "null?") // The client should never be null in practice, but if it is, this ensures nothing is executed. // The socket.io-server-java docs are also awful when it comes to nullability here. @@ -116,6 +119,7 @@ class ServerLauncher(val port: Int = 5000) { } } + logger.info("Server is ready") } fun start() { @@ -142,6 +146,8 @@ class ServerLauncher(val port: Int = 5000) { internal fun getClientsInRoom(room: String) = socketIO.namespace("/").adapter.listClients(room) companion object { + private val logger = LoggerFactory.getLogger(ServerLauncher::class.java) + init { System.setProperty("org.eclipse.jetty.util.log.announce", "false") } diff --git a/server/src/main/java/tdt4240/netrunner/server/controllers/AuthController.kt b/server/src/main/java/tdt4240/netrunner/server/controllers/AuthController.kt deleted file mode 100644 index 8d8ee49e92fffd84658bea85c35852f65d20eabf..0000000000000000000000000000000000000000 --- a/server/src/main/java/tdt4240/netrunner/server/controllers/AuthController.kt +++ /dev/null @@ -1,35 +0,0 @@ -package tdt4240.netrunner.server.controllers - -import com.google.gson.Gson -import io.socket.socketio.server.SocketIoSocket -import io.socket.socketio.server.SocketIoSocket.ReceivedByLocalAcknowledgementCallback -import tdt4240.netrunner.model.requests.AuthRequest -import tdt4240.netrunner.model.response.StatusCode -import tdt4240.netrunner.model.response.StatusResponse -import tdt4240.netrunner.model.util.gsonImpl -import tdt4240.netrunner.server.util.SocketIOInputNormaliser -import java.util.logging.Logger - -/** - * Handles the backend's authentication processes, and interactions with the parts of the database - * concerned with authentication. - */ -class AuthController : ServerFeatureController() { - - override fun register(socket: SocketIoSocket) { - socket.on("login") { - - val (params, ack) = SocketIOInputNormaliser.normaliseSingleWithAck<AuthRequest>(it) ?: return@on - - // TODO: replace with real authentication. For obvious reasons, this is very insecure, and not exactly - // open for multiple users - if (params.username == "a" && params.password == "a") { - // TODO: session management goes here - ack.sendAcknowledgement(gsonImpl.toJson(StatusResponse(StatusCode.OK))) - } else { - ack.sendAcknowledgement(gsonImpl.toJson(StatusResponse(StatusCode.DATA_ERROR, "Invalid credentials"))) - } - } - } - -} diff --git a/server/src/main/java/tdt4240/netrunner/server/controllers/GameController.kt b/server/src/main/java/tdt4240/netrunner/server/controllers/GameController.kt index 20388cb2765c9b9401b9926ec086b860acd8d1d7..32bbaa6bc1cfc5c9e3a0965e8f680200dd52b86d 100644 --- a/server/src/main/java/tdt4240/netrunner/server/controllers/GameController.kt +++ b/server/src/main/java/tdt4240/netrunner/server/controllers/GameController.kt @@ -1,8 +1,8 @@ package tdt4240.netrunner.server.controllers -import com.google.gson.Gson import io.socket.socketio.server.SocketIoServer import io.socket.socketio.server.SocketIoSocket +import org.slf4j.LoggerFactory import tdt4240.netrunner.model.requests.JoinGameRequest import tdt4240.netrunner.model.response.StatusCode import tdt4240.netrunner.model.response.StatusResponse @@ -22,18 +22,23 @@ class GameController(val server: SocketIoServer) : ServerFeatureController() { /** * Used for matchmaking purposes: contains queued games. All games present in this map - * must also be present in @{link GameController#games} + * must also be present in {@link GameController#games} */ val playerGameMap = ConcurrentHashMap<String, String>() val queuedGames = ConcurrentLinkedDeque<GameRoom>() var running = true + // Absolute overkill, but might as well private val hasher = MessageDigest.getInstance("SHA3-256") override fun register(socket: SocketIoSocket) { socket.on("join-matchmaking") { val (joinReq, ack) = SocketIOInputNormaliser.normaliseSingleWithAck<JoinGameRequest>(it) ?: return@on + if (ack == null) { + // No ack, no game. It's the law + return@on + } if (joinGame(socket, joinReq)) { ack.sendAcknowledgement(gsonImpl.toJson(StatusResponse(StatusCode.OK))) } else { @@ -41,23 +46,27 @@ class GameController(val server: SocketIoServer) : ServerFeatureController() { } } socket.on("leave-matchmaking") { + logger.info("Player {} requested to leave", socket.id) val ack = SocketIOInputNormaliser.normaliseAck(it) - leaveGame(socket) // There's no real reason to not acknowledge a leave request as complete. At least // not for now. - ack.sendAcknowledgement(gsonImpl.toJson(StatusResponse(StatusCode.OK))) + ack?.sendAcknowledgement(gsonImpl.toJson(StatusResponse(StatusCode.OK))) } socket.on("jump") { val ack = SocketIOInputNormaliser.normaliseAck(it) val roomId = playerGameMap[socket.id] if (roomId == null) { - ack.sendAcknowledgement(gsonImpl.toJson(StatusResponse(StatusCode.FAIL, "That's a shady move :squint:"))) + // I'm increasingly growing uncertain that acks are the way to go. It's a bit of a callback hell + // and the risk it gets worse is _very_ real. + // But like, what's the alternative? `socket.emit("error", someObj)` isn't much better + // Might just have to be accepted for now. + ack?.sendAcknowledgement(gsonImpl.toJson(StatusResponse(StatusCode.FAIL, "That's a shady move :squint:"))) } else { val room = games[roomId] if (room == null) { - ack.sendAcknowledgement(gsonImpl.toJson(StatusResponse(StatusCode.FAIL, "You're not in a game"))) + ack?.sendAcknowledgement(gsonImpl.toJson(StatusResponse(StatusCode.FAIL, "You're not in a game"))) return@on } @@ -101,6 +110,7 @@ class GameController(val server: SocketIoServer) : ServerFeatureController() { game.emitPlayers() playerGameMap[socket.id] = game.socketRoomID + logger.info("Player {} (username: {}) joined room {}", socket.id, req.username, game.socketRoomID) } return res } @@ -121,10 +131,30 @@ class GameController(val server: SocketIoServer) : ServerFeatureController() { private fun generateGameID() : String { var id: String do { - id = hasher.digest( + val hash = hasher.digest( Random().nextLong().toString().encodeToByteArray() - ).toString() + ) + // The rest of the digest are bytes that need to be decoded into their hex form to + // look like a standard hash + val decoded: StringBuilder = StringBuilder(2 * hash.size) + for (i in hash.indices) { + val hex = Integer.toHexString(0xff and hash[i].toInt()) + if (hex.length == 1) { + decoded.append('0') + } + decoded.append(hex) + } + id = decoded.toString() } while (games.containsKey(id)) return id; } + + fun notifyStart(room: GameRoom) { + logger.info("Room ${room.socketRoomID} notified start") + queuedGames.remove(room) + } + + companion object { + private val logger = LoggerFactory.getLogger(GameController::class.java) + } } diff --git a/server/src/main/java/tdt4240/netrunner/server/game/GameRoom.kt b/server/src/main/java/tdt4240/netrunner/server/game/GameRoom.kt index a06a58d9b1656e4b92f2f59138b7666460ac32d8..a310366ef82d5184a593b0676708eeab99711d5e 100644 --- a/server/src/main/java/tdt4240/netrunner/server/game/GameRoom.kt +++ b/server/src/main/java/tdt4240/netrunner/server/game/GameRoom.kt @@ -1,26 +1,39 @@ package tdt4240.netrunner.server.game -import com.google.gson.Gson import io.socket.socketio.server.SocketIoServer -import io.socket.socketio.server.SocketIoSocket +import org.slf4j.LoggerFactory +import tdt4240.netrunner.game.WorldGen +import tdt4240.netrunner.game.controllers.MovementController +import tdt4240.netrunner.model.game.EcsEngine import tdt4240.netrunner.model.game.Entity import tdt4240.netrunner.model.game.factories.PlayerFactory import tdt4240.netrunner.model.requests.JoinGameRequest -import tdt4240.netrunner.model.response.PlayerData +import tdt4240.netrunner.model.response.EntityData import tdt4240.netrunner.model.util.Vec2f -import tdt4240.netrunner.model.util.Vec3f import tdt4240.netrunner.model.util.gsonImpl import tdt4240.netrunner.server.controllers.GameController import java.util.concurrent.ConcurrentHashMap class GameRoom(val socketRoomID: String, val server: SocketIoServer, val controller: GameController){ + private val logger = LoggerFactory.getLogger("room-$socketRoomID") + private val gen = WorldGen(this) + val createdAt = System.currentTimeMillis() val players = ConcurrentHashMap<String, Entity>() + val ecs = EcsEngine().also { + it.entities.addAll(gen.genGround(STANDARD_FIELD_LENGTH)) + } var started = false private set var done = false private set + init { + ecs.addController(MovementController(ecs)) + + logger.info("New room created. ID: ${socketRoomID}") + } + val thread = Thread(::tick).also { if (controller.running) { it.start() @@ -28,22 +41,32 @@ class GameRoom(val socketRoomID: String, val server: SocketIoServer, val control } private fun tick() { + var prevTime = System.currentTimeMillis() while (controller.running && !done) { if (!started) { if (System.currentTimeMillis() - createdAt >= MAX_WAIT_TIME * 1000 && players.size >= MIN_PLAYERS || players.size == MAX_PLAYERS) { + logger.info("Play threshold reached. Starting game") started = true + // TODO: timer // TODO: broadcast time remaining every second or so? // How many players have joined is automatically broadcast whenever someone joins or leaves, // so that doesn't need to be handled here. + prevTime = System.currentTimeMillis() + ecs.entities.addAll(players.values.toMutableList()) + + server.namespace("/").broadcast(socketRoomID, "game-started") + controller.notifyStart(this) + } else { Thread.sleep(1000) continue } } - // TODO: check if the game has actually started here. - // TODO: spawn a thread to invoke tick() - // TODO: if the game hasn't started, do a countdown wrt. MAX_WAIT_TIME + + val delta = (System.currentTimeMillis() - prevTime).toDouble() + + ecs.tick(delta) // Preliminary code: broadcasts the players to the server. Game ticks go above this. try { @@ -54,12 +77,31 @@ class GameRoom(val socketRoomID: String, val server: SocketIoServer, val control Thread.sleep((1000.0 / 60.0).toLong()) } - // TODO: emit leaderboard event and DC all clients + // TODO: emit leaderboard event, if we have the time to implement that FR + + server.namespace("/").adapter.listClients(socketRoomID) + .forEach { + it.leaveRoom(socketRoomID) + } } fun emitPlayers() { - synchronized(controller) { - server.namespace("/").broadcast(socketRoomID, "players", gsonImpl.toJson(PlayerData(players.values.toList()))) + for (i in 0..5) { + try { + // we should probably separate the world from the player entities, just for + // convenience (and performance, sending a few hundred entities over and over + // isn't great. It works fine, but it's a waste of data). + server.namespace("/") + .broadcast(socketRoomID, + "entities", + gsonImpl.toJson( + EntityData(if (started) ecs.entities else players.values.toList()) + ) + ) + return + } catch (e: ConcurrentModificationException) { + logger.error("Failed to broadcast: ConcurrentModificationException") + } } } @@ -76,7 +118,7 @@ class GameRoom(val socketRoomID: String, val server: SocketIoServer, val control players[socketID] = PlayerFactory() .withUsername(req.username) .withColor(req.color) - .withPosition(Vec2f(0f, 0f)) // TODO: generate position based on join index + .withPosition(Vec2f(40f * players.size, 0f)) // TODO: generate position based on join index .build() true } else { @@ -129,6 +171,14 @@ class GameRoom(val socketRoomID: String, val server: SocketIoServer, val control companion object { const val MAX_PLAYERS = 10 const val MIN_PLAYERS = 2 + + /** + * The max time (in seconds) to wait after starting to play, given the MIN_PLAYERS threshold + * has been reached + */ const val MAX_WAIT_TIME = 120 + + const val STANDARD_FIELD_LENGTH = 40_000 + } -} \ No newline at end of file +} diff --git a/server/src/main/java/tdt4240/netrunner/server/util/SocketIOInputNormaliser.kt b/server/src/main/java/tdt4240/netrunner/server/util/SocketIOInputNormaliser.kt index 2aa239b5b5145ffd6defc3b0df9bec2676fbf891..1e60fb2e8b0220f5e55cbe6442ea66b8e4d44904 100644 --- a/server/src/main/java/tdt4240/netrunner/server/util/SocketIOInputNormaliser.kt +++ b/server/src/main/java/tdt4240/netrunner/server/util/SocketIOInputNormaliser.kt @@ -12,8 +12,9 @@ object SocketIOInputNormaliser { */ fun normaliseAck( input: Array<out Any> - ): ReceivedByLocalAcknowledgementCallback { - return input[input.size - 1] as ReceivedByLocalAcknowledgementCallback + ): ReceivedByLocalAcknowledgementCallback? { + if (input.isEmpty()) return null + return input[input.size - 1] as? ReceivedByLocalAcknowledgementCallback } /** @@ -24,11 +25,16 @@ object SocketIOInputNormaliser { */ inline fun <reified T> normaliseSingleWithAck( input: Array<out Any> - ): Pair<T, ReceivedByLocalAcknowledgementCallback>? { - val ack = input[input.size - 1] as ReceivedByLocalAcknowledgementCallback + ): Pair<T, ReceivedByLocalAcknowledgementCallback?>? { + val ack = input[input.size - 1] as? ReceivedByLocalAcknowledgementCallback if (input.size == 1) { // I'm 99% sure the ack is always present, so this shouldn't cause problems. - ack.sendAcknowledgement(Gson().toJson(StatusResponse(StatusCode.INSUFFICIENT_ARGUMENTS, + // + // hi, past me. You were wrong, the ack is not always present, because reasons? I don't know, + // it's an awfully designed library. + // + // Pain + ack?.sendAcknowledgement(Gson().toJson(StatusResponse(StatusCode.INSUFFICIENT_ARGUMENTS, "Developer error: insufficient arguments passed to endpoint"))) return null } @@ -36,7 +42,7 @@ object SocketIOInputNormaliser { // TODO: try-catch with error handling val res = gsonImpl.fromJson(input[0] as String, T::class.java) if (res == null) { - ack.sendAcknowledgement(Gson().toJson(StatusResponse(StatusCode.DATA_ERROR, "Received null or incorrect data"))) + ack?.sendAcknowledgement(Gson().toJson(StatusResponse(StatusCode.DATA_ERROR, "Received null or incorrect data"))) return null } diff --git a/server/src/main/resources/jetty-logging.properties b/server/src/main/resources/jetty-logging.properties index d4fa607d644f8731be51f9e158b5b5e61558f39d..2b0dc6c300d496bb40b7a72a75f952acde88ec88 100644 --- a/server/src/main/resources/jetty-logging.properties +++ b/server/src/main/resources/jetty-logging.properties @@ -1,2 +1 @@ -org.eclipse.jetty.util.log.announce=false -org.eclipse.jetty.util.log.IGNORED=true \ No newline at end of file +org.eclipse.jetty.util.log.announce=false \ No newline at end of file diff --git a/server/src/main/resources/logback.xml b/server/src/main/resources/logback.xml index 291f879e7fca6b3333cde4ad0bc0722efe2b4290..1f5f90f224c203d95046531bb3b524d15cb83496 100644 --- a/server/src/main/resources/logback.xml +++ b/server/src/main/resources/logback.xml @@ -1,7 +1,20 @@ <configuration> + <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender"> + <encoder> + <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} -%kvp- %msg%n</pattern> + </encoder> + </appender> + + <root level="INFO"> + <appender-ref ref="STDOUT" /> + </root> <!-- Jetty spews out such an excessively large amount of logs that it makes debug effectively impossible. Even in tests, there's a very negative result. + + Optimally, Jetty would be set to warning or higher, but for reasons I'm not going to pretend + to understand, WARNING does nothing? --> - <logger name="org.eclipse.jetty" level="WARNING" /> + <logger name="org.eclipse.jetty" level="WARN" /> + <logger name="tdt4240.netrunner" level="DEBUG" /> </configuration> \ No newline at end of file diff --git a/server/src/test/kotlin/tdt4240/netrunner/server/AuthTests.kt b/server/src/test/kotlin/tdt4240/netrunner/server/AuthTests.kt deleted file mode 100644 index c94e0f5ba7fc6f1d16fd8356d89f9cf18982e523..0000000000000000000000000000000000000000 --- a/server/src/test/kotlin/tdt4240/netrunner/server/AuthTests.kt +++ /dev/null @@ -1,84 +0,0 @@ -package tdt4240.netrunner.server - -import com.google.gson.Gson -import io.socket.client.Ack -import io.socket.client.IO -import io.socket.client.Socket -import io.socket.engineio.client.transports.WebSocket -import tdt4240.netrunner.model.requests.AuthRequest -import tdt4240.netrunner.model.response.StatusCode -import tdt4240.netrunner.model.response.StatusResponse -import tdt4240.netrunner.server.controllers.GameController -import tdt4240.netrunner.server.util.AsyncAssert -import java.net.URI -import java.util.concurrent.atomic.AtomicInteger -import kotlin.test.* - -class AuthTests { - private val server = ServerLauncher(60009) - private lateinit var client: Socket - @BeforeTest - fun initServer() { - server.start() - // make sure the server doesn't spawn excessive threads. - server.getController<GameController>().apply { - running = false - } - - // Base socket setup - client = IO.socket(URI("ws://127.0.0.1:" + server.port), IO.Options.builder() - // Force websocket transport rather than polling - .setTransports(arrayOf(WebSocket.NAME)) - .build()) - - } - - /** - * This is the core test for ensuring that the client-server connection itself works. The check - * _could_ be elsewhere, but it's convenient not to. - */ - @Test - fun validateConnection() { - client.connect() - - AsyncAssert.waitFor(true, property = client::connected) - } - - @Test - fun validateLoginExists() { - val counter = AtomicInteger(0) - client.on("connect") { - // Note; due to how these libraries work, the data sent has to be serialized somehow sane, and - // in this case, that means as a JSON string. - client.emit("login", Gson().toJson(AuthRequest("a", "b")), Ack { args -> - // Deserialize the argument (JSON string) as a StatusResponse - val response = Gson().fromJson(args[0] as String, StatusResponse::class.java) - assertEquals(StatusCode.DATA_ERROR, response.status) - counter.incrementAndGet() - }) - client.emit("login", Gson().toJson(AuthRequest("a", "a")), Ack { args -> - // Deserialize the argument (JSON string) as a StatusResponse - val response = Gson().fromJson(args[0] as String, StatusResponse::class.java) - assertEquals(StatusCode.OK, response.status) - counter.incrementAndGet() - }) - } - client.connect() - for (i in 0..10) { - if (counter.get() == 2) { - // When both the endpoints have been hit, return from the function. - // This is necessary to avoid the fail - return - } - // Wait a semi-long amount of time to give everything time to catch up - Thread.sleep(500) - } - // If the code gets here, the counter hasn't been incremented (in time) to break out of the for loop. - fail("Didn't get both responses: received " + counter.get()) - } - - @AfterTest - fun stopServer() { - server.stop() - } -} diff --git a/server/src/test/kotlin/tdt4240/netrunner/server/controllers/GameControllerTests.kt b/server/src/test/kotlin/tdt4240/netrunner/server/controllers/GameControllerTests.kt index a5cb7817b8904a74d38fee79817178fe517ee2fa..ed8b7c2ad297ec69c46b10f4b721e41a17827b33 100644 --- a/server/src/test/kotlin/tdt4240/netrunner/server/controllers/GameControllerTests.kt +++ b/server/src/test/kotlin/tdt4240/netrunner/server/controllers/GameControllerTests.kt @@ -3,7 +3,7 @@ package tdt4240.netrunner.server.controllers import io.socket.client.Socket import org.junit.jupiter.api.TestInstance import tdt4240.netrunner.model.game.components.living.PlayerComponent -import tdt4240.netrunner.model.response.PlayerData +import tdt4240.netrunner.model.response.EntityData import tdt4240.netrunner.model.response.StatusCode import tdt4240.netrunner.model.util.gsonImpl import tdt4240.netrunner.server.ServerLauncher @@ -133,7 +133,7 @@ class GameControllerTests { addPlayer(sockets[sockets.size - 1], "PickleRick11", 1) - assertEquals(2, gameController.queuedGames.size) + assertEquals(1, gameController.queuedGames.size) assertEquals(11, gameController.playerGameMap.size) assertEquals(2, gameController.games.size) @@ -150,15 +150,15 @@ class GameControllerTests { private fun addPlayer(client: Socket, user: String, expectedPlayerCount: Int) { val control = AtomicBoolean(false) val receivedPlayers = AtomicBoolean(false) - client.once("players") { + client.once("entities") { entities -> // Not entirely clear on why an explicit cast is necessary. // It's clearly able to figure it out at compile time, just not in the IDE - val data = gsonImpl.fromJson(it[0] as String, PlayerData::class.java) as PlayerData - assertEquals(expectedPlayerCount, data.players.size) - assertNotNull(data[data.players.size - 1].getComponent<PlayerComponent>()) + val data = gsonImpl.fromJson(entities[0] as String, EntityData::class.java) as EntityData + assertEquals(expectedPlayerCount, data.entities.size) + assertNotNull(data[data.entities.size - 1].getComponent(PlayerComponent::class.java)) assertTrue( - data.players.any { - it.getComponent<PlayerComponent>()!!.username == user + data.entities.any { + it.getComponent(PlayerComponent::class.java)!!.username == user } ) receivedPlayers.set(true) diff --git a/server/src/test/kotlin/tdt4240/netrunner/server/util/SocketUtil.kt b/server/src/test/kotlin/tdt4240/netrunner/server/util/SocketUtil.kt index 5ad9d2b1233dde6c2eda06eefd6d9b1b42229aad..c86361bc0a3eadaa97c3d75f48f1f7d442f7761a 100644 --- a/server/src/test/kotlin/tdt4240/netrunner/server/util/SocketUtil.kt +++ b/server/src/test/kotlin/tdt4240/netrunner/server/util/SocketUtil.kt @@ -1,27 +1,26 @@ package tdt4240.netrunner.server.util -import com.google.gson.Gson import io.socket.client.Ack import io.socket.client.IO import io.socket.client.Socket import io.socket.engineio.client.transports.WebSocket +import tdt4240.netrunner.model.game.data.PlayerColor import tdt4240.netrunner.model.requests.JoinGameRequest import tdt4240.netrunner.model.response.StatusResponse -import tdt4240.netrunner.model.util.Vec3f import tdt4240.netrunner.model.util.gsonImpl import java.net.URI fun Socket.leaveMatch(also: ((res: StatusResponse) -> Unit)?) { emit("leave-matchmaking", Ack { - val res = Gson().fromJson(it[0] as String, StatusResponse::class.java) + val res = gsonImpl.fromJson(it[0] as String, StatusResponse::class.java) also?.invoke(res) }) } fun Socket.joinMatch(username: String, also: ((res: StatusResponse) -> Unit)?) { - emit("join-matchmaking", gsonImpl.toJson(JoinGameRequest(username, Vec3f(1.0f, 0.0f, 1.0f))), Ack { - val res = Gson().fromJson(it[0] as String, StatusResponse::class.java) + emit("join-matchmaking", gsonImpl.toJson(JoinGameRequest(username, PlayerColor.WHITE)), Ack { + val res = gsonImpl.fromJson(it[0] as String, StatusResponse::class.java) also?.invoke(res) })