Commit ffa50a2b authored by ervelae's avatar ervelae
Browse files

Initial commit

Skrt
parent fde95ed5
contracts/
.classpath
.project
.history/
.idea
.jqwik-database
.lib/
.worksheet
.settings/
# Without .classpath Eclipse creates its own project structure before reading Maven configuration including bin/
bin/
*.iml
*.ipr
*.iws
*.log
project/boot/
project/plugins/project/
project/project/
project/*-shim.sbt
project/target/
target/
openjfx/
dependency-reduced-pom.xml
.vscode/
image: maven:latest
stages:
- build
- test
build:
script:
- mvn compile
test:
script:
- mvn test
\ No newline at end of file
This diff is collapsed.
# Assignment
This README contains only general instructions on how to run the game. The development setup, assignment, program structure and hints for completing the assignment can be found in [doc/index.md](doc/index.md) (in Finnish).
# Project description
A game where a bunch of gorillas try to kill each other by throwing bananas on time-limited turns. Last survivor is the winner.
Requires Java 11 or later. Compatible with
Eclipse, IntelliJ IDEA and VS Code with Java Extension Pack. Minor issues with Netbeans.
## Installation
Maven:
```bash
$ git clone https://gitlab.utu.fi/tech/education/distributed-systems/distributed-gorilla
$ cd distributed-gorilla
$ mvn compile package exec:java
```
SBT:
```bash
$ git clone https://gitlab.utu.fi/tech/education/distributed-systems/distributed-gorilla
$ cd distributed-gorilla
$ sbt compile run
```
## JavaFX bugs
JavaFX has serious memory leaks that might lead to a crash in just seconds.
Use the following JVM parameter to switch to software rendering pipeline that
does not have the leaks
```
-Dprism.order=sw
```
E.g.
```bash
$ java -Dprism.order=sw -jar target/distributed-gorilla-1.0.jar
```
The game will allocate significant amounts of memory. Use the following switch
to restrict the heap size to avoid wasting RAM:
```
-Xmx2000m
```
References:
* https://bugs.openjdk.java.net/browse/JDK-8092801
* https://bugs.openjdk.java.net/browse/JDK-8088396
* https://bugs.openjdk.java.net/browse/JDK-8161997
* https://bugs.openjdk.java.net/browse/JDK-8156051
* https://bugs.openjdk.java.net/browse/JDK-8161914
* https://bugs.openjdk.java.net/browse/JDK-8188094
* https://stackoverflow.com/a/41398214
## Further instructions
* Java platform: https://gitlab.utu.fi/soft/ftdev/wikis/tutorials/jvm-platform
* Maven: https://gitlab.utu.fi/soft/ftdev/wikis/tutorials/maven-misc
* SBT: https://gitlab.utu.fi/soft/ftdev/wikis/tutorials/sbt-misc
* OOMkit: https://gitlab.utu.fi/tech/education/oom/oomkit
Course related
* https://gitlab.utu.fi/tech/education/distributed-systems/distributed-chat
* https://gitlab.utu.fi/tech/education/distributed-systems/distributed-crypto
* https://gitlab.utu.fi/tech/education/distributed-systems/distributed-classloader
## Screenshots
![Game](web/screenshot1.png)
# Tehtävä 2
Tehtävän 2 vastaus tänne
# Tehtävä 3
Tehtävän 3 vastaus tänne
\ No newline at end of file
// Project template
// Supported operating systems: Windows, Mac, Linux
// Supported JDKs: 8, 10+
// Project name
name := "distributed-gorilla"
// organization name
organization := "fi.utu.tech"
version := "1.0"
// project description
description := "Gorillasota 2029"
// main class
Compile/mainClass := Some("fi.utu.tech.distributed.gorilla.Main")
// force the java version by typing it here (remove the comment)
val force_javaVersion = None // Some(13)
// force the javafx version by typing it here (remove the comment)
val force_javaFxVersion = None // Some(13)
val useJavaFX = true
val useScalaOrScalaFX = true
// END_OF_SIMPLE_CONFIGURATION
// you can copy the rest for each new project
// --- --- ---
def fail(msg: String) = {
println("Error :-/")
println
println(msg)
System.exit(1)
null
}
val detectedJDK = System.getProperty("java.version").replace("-ea","").split('.').dropWhile(_.toInt<8).head.toInt
val javaVersionNum = force_javaVersion.getOrElse(detectedJDK)
val javaVersionString = javaVersionNum match {
case 7 => "1.7"
case 8 => "1.8"
case x if x > 8 => x.toString
}
val lts = 11
val dev = 13
val supported = javaVersionNum match {
case x if x < 8 => fail("Your Java installation is obsolete. Please upgrade to Java " + lts + "LTS")
case 9 => fail("Your Java installation is unsupported and has known issues. Please upgrade to Java " + lts + "LTS")
case x if x < lts => println("Consider upgrading to Java " + lts + " LTS"); true
case x if x > lts && x < dev => println("Consider upgrading to Java " + dev); true
case x if x > dev => println("Unsupported early access version. Consider switching back to Java " + dev); true
case _ => true
}
javacOptions ++= Seq("-source", javaVersionString, "-target", javaVersionString, "-encoding", "utf8", "-Xlint:unchecked", "-Xlint:deprecation")
javacOptions in doc := Seq("-source", javaVersionString)
enablePlugins(JShellPlugin)
compileOrder := CompileOrder.JavaThenScala
// Enables publishing to maven repo
publishMavenStyle := true
// Do not append Scala versions to the generated artifacts
crossPaths := false
// This forbids including Scala related libraries into the dependency
autoScalaLibrary := false
assemblyMergeStrategy in assembly := {
case PathList("META-INF", xs @ _*) => MergeStrategy.discard
case _ => MergeStrategy.first
}
// contains libraries provided by utu/ft dep
resolvers += "ftdev" at "https://ftdev.utu.fi/maven2"
fork in Global := true
val javaVersion = taskKey[Unit]("Prints the Java version.")
javaVersion := { println("SBT uses Java SDK located at "+System.getProperty("java.home")) }
publishTo := Some(Resolver.file("file", new File("/tmp/repository")))
val oomkit = "fi.utu.tech" % "oomkit" % "1.18"
val crypto = "fi.utu.tech" % "distributed-classloader" % "1.0"
val classloader = "fi.utu.tech" % "distributed-crypto" % "1.0"
libraryDependencies ++= Seq(oomkit, crypto, classloader)
////
//// JQWIK / JUNIT configuration
////
resolvers in ThisBuild += Resolver.jcenterRepo
val junit_version = "5.5.2"
// library dependencies. (orginization name) % (project name) % (version)
libraryDependencies ++= Seq(
"net.aichler" % "jupiter-interface" % JupiterKeys.jupiterVersion.value % Test,
"org.junit.platform" % "junit-platform-commons" % ("1"+junit_version.tail) % Test,
"org.junit.platform" % "junit-platform-runner" % ("1"+junit_version.tail) % Test,
"org.junit.jupiter" % "junit-jupiter-engine" % junit_version % Test,
"org.junit.jupiter" % "junit-jupiter-api" % junit_version % Test,
"org.junit.jupiter" % "junit-jupiter-migrationsupport" % junit_version % Test,
"org.junit.jupiter" % "junit-jupiter-params" % junit_version % Test,
"net.jqwik" % "jqwik" % "1.2.0" % Test,
"org.scalatest" %% "scalatest" % "3.0.8" % Test,
)
testOptions += Tests.Argument(TestFrameworks.JUnit, "-q", "-c")
////
//// JAVAFX configuration
////
val javafx_versions = if (!useJavaFX) (0,"-","-") else (force_javaFxVersion getOrElse javaVersionNum) match {
case 7 => (7, "7", "8.0.181-R13")
case 8 => (8, "8", "8.0.181-R13")
case 10 => (11, "11.0.2", "11-R16")
case x if x>10 => (13, "13", "12.0.2-R18")
case _ => fail("Unsupported Java version for JavaFX")
}
// JAVA_HOME location
val javaHomeDir = {
val path = try {
if (scala.sys.env("JAVA_HOME").trim.isEmpty) throw new Exception("Empty JAVA_HOME") else scala.sys.env("JAVA_HOME")
} catch {
case _: Throwable => System.getProperty("java.home") // not set -> ask from current JVM
}
val f = file(path)
if (!f.exists()) fail("The environment variable JAVA_HOME points to a non-existent directory!\nSolution: Edit your system settings (Windows control panel / *nix .bashrc) and fix the JAVA_HOME location.")
f
}
val osName: SettingKey[String] = SettingKey[String]("osName")
osName := (System.getProperty("os.name") match {
case n if n.startsWith("Linux") => "linux"
case n if n.startsWith("Mac") => "mac"
case n if n.startsWith("Windows") => "win"
case _ => throw new Exception("Unknown platform!")
})
def legacyJavaFX() = {
val searchDirs = Seq(
"/jre/lib/jfxrt.jar", // OpenJDK 7
"/jre/lib/ext/jfxrt.jar", // OpenJDK 8
"/lib/ext/jfxrt.jar" // Windows & Oracle Java 8
)
if (detectedJDK > 8) fail(s"Trying to use legacy non-modular JavaFX with a modern JDK [$detectedJDK].\nSolution: Check the line 'val force_javaFxVersion =' in build.sbt.")
val javaFxJAR = searchDirs.map{ searchDir => file(javaHomeDir + searchDir) }.find{ _.exists() }
javaFxJAR.getOrElse {
fail(s"Java FX runtime not installed in [${javaHomeDir.toString}]!\nSolution: Install JavaFX or consider upgrading your JDK so that JavaFX can be installed automatically.")
}
}
val jfx_sdk_version = javafx_versions._2
val jfx_scalafx_version = javafx_versions._3
val javaFxPath = Def.taskKey[File]("OpenJFX fetcher")
javaFxPath := {
val javaFxHome =
try {
val envHome = file(scala.sys.env("JAVAFX_HOME"))
if (envHome.toString.trim.isEmpty) throw new Exception("Empty JAVAFX_HOME")
println("Using OpenJFX from " + envHome)
envHome
}
catch { case _: Throwable =>
println("Using local OpenJFX")
baseDirectory.value / "openjfx"
}
if (!javaFxHome.exists()) java.nio.file.Files.createDirectory(javaFxHome.toPath)
val jfx_os = osName.value match {
case "linux" => "linux"
case "mac" => "osx"
case "win" => "windows"
}
val sdkURL = "http://download2.gluonhq.com/openjfx/" + jfx_sdk_version + "/openjfx-" + jfx_sdk_version + "_" + jfx_os + "-x64_bin-sdk.zip"
try {
val testDir = javaFxHome / "all.ok"
if (!testDir.exists()) {
println("Fetching OpenJFX from "+sdkURL+"..")
IO.unzipURL(new URL(sdkURL), javaFxHome)
java.nio.file.Files.createDirectory(testDir.toPath)
println("Fetching OpenJFX done.")
} else {
println("Using OpenJFX from "+javaFxHome)
}
javaFxHome
}
catch {
case t: Throwable => fail("Could not load OpenJFX! Reason:" + t.getMessage)
}
}
val jfxModules = Seq("base","controls","fxml","graphics","media","swing","web")
if (!useJavaFX) Seq() else javafx_versions._1 match {
case 7 =>
// TODO libraryDependencies
Seq(unmanagedJars in Compile += Attributed.blank(legacyJavaFX()))
case 8 =>
(if (useScalaOrScalaFX) Seq(libraryDependencies += "org.scalafx" %% "scalafx" % jfx_scalafx_version) else Seq()) ++
Seq(unmanagedJars in Compile += Attributed.blank(legacyJavaFX()))
case _ =>
Seq(
javaOptions in run ++= Seq(
"--module-path", (javaFxPath.value / ("javafx-sdk-" + jfx_sdk_version) / "lib").toString,
"--add-modules=" + jfxModules.map("javafx."+_).mkString(","))
) ++
(if (useScalaOrScalaFX) Seq(libraryDependencies += "org.scalafx" % "scalafx_2.13" % jfx_scalafx_version) else Seq()) ++
jfxModules.map(module => libraryDependencies += "org.openjfx" % ("javafx-"+module) % jfx_sdk_version classifier osName.value)
}
# Ohjelmarungon tuonti Eclipseen
Kloonaa ja tuo projekti Eclipseen käyttämällä Eclipsen Smart Import -työkalua. Ohjattu toiminto löytyy valikosta File > Import > Git > Projects from Git (Smart Import). Toiminto kloonaa projektin koneellesi sekä tuo projektin Eclipseen Maven-konfiguraation mukaisesti. Jos ajat ohjelman Eclipsessä käyttäen "Java Application" -kohtaa, valitse Main-metodiksi `Main - fi.utu.tech.distributed.gorilla`.
# Pelin toiminta
Pelin säännöt ovat yksinkertaiset: kentällä on n gorillaa, jotka heittelevät banaaneja tietyssä kulmassa tietyllä nopeudella toisiaan päin. Mikäli banaani osuu, gorilla kuolee. Viimeinen eloonjäänyt gorilla on voittaja. Peli on jaettu aikarajoitettuihin kierroksiin, jonka aikana kaikki pelaajat päättävät siirtonsa. Kierroksen lopuksi (aika päättynyt tai kaikki gorillat ovat tehneet siirtonsa) gorillat heittävät banaaninsa yhtäaikaisesti. Kierroksen aikana pelissä on aina tietty sää (esimerkiksi tuuli), joka vaikuttaa heittoon.
Käynnistyessään peli toistaa lyhyen Intron ja siirtyy tämän jälkeen valikkotilaan, ellei käyttäjä tätä ennen pakota tilanmuutosta käyttöliittymän painikkeilla.
## Kontrollit
Peliä ohjataan graafisen käyttöliittymän painikkeilla, näppäimistöllä sekä tekstipohjaisilla komennoilla.
### Päävalikon kontrollit
- Nuolinäppäimet + Enter
Huomioi, että päävalikon kontrollit **eivät toimi**, mikäli tekstinsyöttökenttä on valittuna! Napsauta tulostekenttää, jotta fokus siirtyy pois syöttökentästä.
### Alaosan painikkeet
- Intro: Käynnistä pelin intro uudelleen
- Menu: Siirry suoraan päävalikkon
- Game: Siirry pelitilaan esimerkiksi päävalikosta tai introsta
- <<, =, >>: Pysäytä kamera tai ohjaa sitä vasemmalle tai oikealle
- 0: Kohdista kamera omaan pelaajaan
- Restart: Käynnistä koko peli uudelleen - kova restart, sillä koko logiikka alustetaan
- Exit: Poistu pelistä
### Komennot
Komennot syötetään oikean alakulman tekstikenttään.
- q, quit, exit: Poistu ohjelmasta
- s, chat, say *viesti*: Puhu chattiin
- a, k, angle, kulma: Aseta heittokulma asteina
- v, n, velocity, nopeus: Aseta heittonopeus ja heitä banaani
# Tehtävänanto
Tässä harjoitustyössä on kolme päätehtävää, joista ensimmäinen on ohjelmointiharjoitus ja kaksi viimeistä selvitystehtäviä. Kahteen viimeiseen vastaukset kirjoitetaan [SELVITYS.md-tidostoon](../SELVITYS.md) pääkansiossa. Selvitystehtävät kannattaa tehdä ohjelmointitehtävän jälkeen.
Tehtävät:
1. Toteuta moninpeli (ml. chat) annettuun Gorillapeliin käyttämällä Mesh-tyylistä[^1] verkotusta applikaatiotasolla
2. Mesh-toteutus on mahdollista toteuttaa nopeasti käyttäen Javan ObjectStreameja, jolloin verkon yli välittyy yksittäisiä serialisoituja olioita. Tämä on kuitenkin abstraktion luoma "illuusio", sillä TCP-socketit kuljettavat matalammalla tasolla pelkkää datavirtaa, eivätkä siis pysty erottamaan missä kohtaa serialisoitu olio päättyy ja mistä seuraava alkaa. Oletetaan olevan yksittäisiä resursseja, kuten serialisoituja olioita, jotka halutaan kuljettaa verkon yli, ilman olemassa olevaa ObjectStreamin kaltaista abstraktiota. Miten viestitte/merkkaatte vastaanottajalle yksittäisten resurssien rajat? (Vinkki: Voitte selvittää, miten osa olemassa olevista protokollista ratkaisee tämän ongelman.)
3. Kerro lyhyesti muutama esimerkkiskenaario, mitä tietoturvaan liittyviä ongelmia Mesh-viestien välitys epäluotettujen solmujen kautta (eli siis toisten pelaajien, mahdollisesti toisen ohjelmabinäärin kautta) tuottaa ja miten ongelmia olisi mahdollista ratkaista.[^2] (Vinkki: digital signature, public key cryptography, Diffie–Hellman key exchange). Vastauksessa ei tarvitse keskittyä salausalgoritmien sisäiseen toimintaan/matemaattiseen teoriaan.
## Ensimmäisen tehtävän vaiheet ja haasteet
Ensimmäinen tehtävä on tehtävistä aikaavievin ja täten pilkottu kolmeen vaiheeseen helpottamaan työssä etenemistä. Näitä vaiheita ei tarvitse palauttaa erikseen tai muutoin eritellä. Riittää, että lopullinen ohjelma palautetaan. Jossakin vaiheessa saattaa olla tarpeen myös muokata edellisessä vaiheessa kirjoitettua koodia, mutta tässä esitetty järjestys auttaa kuitenkin suunnittelemaan oman työn aikataulutusta.
### Mesh-verkkokerroksen rakentaminen (35%)
Mesh-kerroksen tulisi olla irrallaan pelitoiminnalisuudesta (ts. sen luokkien ei tulisi riippua Gorillaluokista). Sen tehtäväksi siis jää pelkästään datan lähettäminen, välittäminen ja vastaanottaminen (ilman duplikaatteja) muilta solmuilta ("nodeilta"). Myöhemmissä vaiheessa moninpeli toteutetaan tämän Mesh-kerroksen päälle. Haasteena tässä vaiheessa on opetella luomaan ja käyttämään säikeitä sockettien ja Input- ja OutputStreamien kanssa, sekä harjoittaa säikeiden välistä synkronointia ja säieturvallisten rakenteiden käyttöä. Tämän lisäksi on päätettävä, missä muodossa Mesh-viestit välitetään verkon yli (mitä otsakedataa viesteihin liitetään) ja miten ne serialisoidaan.
Tarkemmin Mesh-verkosta, vaatimuksista sekä ehdotetusta toteutuksesta on kerrottu [omalla sivullansa](mesh.md).
### Chatin toimintaansaatto käyttäen Mesh-toteutusta (15 %)
Käytetään edellisen vaiheen Mesh-toteutusta pelin Chat-viestien välittämiseen. Haasteena on tutustua pelin ohjelmarunkoon riittävästi, jotta Mesh-palvelin saadaan käynnistettyä sekä yhdistettyä olemassa olevaan Mesh-verkkoon ja `ChatMessage`-oliot lähetettyä käyttöliittymää käyttäen. Myös vastaanotetut Chat-viestit tulee saada tulostettua vastaanottajan päässä. Riittää, että viestit saadaan lähetettyä kaikille (yksityisviestitukea ei tarvitse implementoida).
Ohjelman kulkua, sekä tarkemmat vaatimukset on selvitetty [omalla sivullansa](program-flow-chat.md).
### Joulu
Joululoman alkaessa olisi hyvä, että vähintään Mesh-toteutus olisi jotakuinkin toiminnassa. Chat-toiminnallisuuden lisääminen ei tähän päälle ole aikataulullisesti iso tehtävä.
### Pelin toimintaansaatto (50 %)
Pelin loppuosan toimintaansaatto on suurin yksittäinen osakokonaisuus. Tässä laajennetaan verkkotoiminnallisuus koko peliin käyttäen hyväksi edellisssä Chatin implementoinnissa opittuja tekniikoita. Haasteena on osata välittää dataa käyttöliittymäsäikeelle säieturvallisesti ilman, että käyttöliittymäsäie jumiutuu tai tietorakenteet rikkoontuvat. On myös tärkeää selvittää, mitkä oliot tulee siirtää verkon ylitse, ja mitkä puolestaan pystytään johtamaan pienemmästä määrästä dataa.
Tarkempi ohjeistus [omalla sivullansa](mesh-to-game.md).
Huomatkaa vielä kerran, että tämä tehtävä on melko laaja ja sisältää paljon eri osia. Tämän vuoksi on ensiarvoisen tärkeää aloittaa tehtävän teko **nyt** ja kysyä tarvittaessa neuvoa tai varmistusta omalle ratkaisulle harjoitustyöpajoissa!
[^1]: Verkko, johon uusi jäsen voi liittyä kenen tahansa jäsenen kautta. Tästä tarkemmin Mesh-verkon toteutuksesta puhuttaessa.
[^2]: Mikäli kiinnostut enemmän, ks. [distributed-crypto](https://gitlab.utu.fi/tech/education/distributed-systems/distributed-crypto)
*Tämän pelin kehityksen aikana ei vahingoitettu oikeita gorilloja. Muutama koodiapina on tosin saattanut menettää yöuniaan.*
# Moninpelin toteutus Mesh-verkon päälle
## Vaatimukset
- Toisessa osakokonaisuudessa implementoidun Chatin tulostus tulee siirtää JavaFX-säikeeseen
- Meshin työskentelijäsäikiden tulee kommunikoida säieturvallisen rakenteen läpi JavaFX-säikeelle (Vinkki: threadrunner-tehtävän 4. selvitystehtävä...)
- JavaFX-säiettä ei saa blokata tiedon vastaanottamisen aikana. Lähettämisen saa tehdä JavaFX-säikeestä
- Mikäli jokin joukko pelaajista lähtee pois pelistä, tapausta ei tarvitse käsitellä. Pelissä on toki "luovutussiirto", mutta tätä ei tarvitse implementoida.
- Moninpeli tulee pystyä pelaamaan läpi (banaaneja pitää pystyä heittämään)
- Jokaisen pelaajan gorilla tulee olla samassa sijainnissa ja saman pelaajan kontrolloima kaikkien pelaajien näkymässä
- Tietoa, joka voidaan johtaa siemenluvusta, ei tule välittää verkon ylitse
## Luokat
Luokkarakennetta avattiin jo aiemmin [chatin integrointitehtävän](program-flow-chat.md) yhteydessä. Tässä selostuksessa keskityttiin enemmän käyttöliittymälogiikkaan (`GorillaLogic`-luokka) kuin pelilogiikkaan, johon loput luokat liittyvät enemmissä määrin. Luokkien metodit on dokumentoitu Javadoc-tyylisesti, mutta tässä vielä lyhyet kuvaukset luokkien käyttötarkoituksista:
### GorillaLogic
Sisältää käyttöliittymälogiikan: Eli mitä tapahtuu kun tietty komento annetaan, milloin suoritetaan peliä ja milloin valikkoa, mitä tapahtuu kun aloitetaan moninpeli jne. Kutsuu tick-metodissa myös pelitilan (`GameState`) `tick()`-metodia. Luokka jäsennelty siten, että perittäessä oleellisten funktioiden uudelleenmäärittely on helppoa: Komennot kutsuvat `handleKomennonNimi()` -tyylisiä metodeita, jotka on mahdollista perivässä luokassa yliajaa.
### GameState
Sisältää pelin tilan ja metodit, joilla tilaa muutetaan. `GorillaLogic`-luokan kutsuma `tick()`-metodi päivittää pelin sisäistä ajan etenemistä. Pelin alkutila määritetään `GameConfiguration`-oliolla ja sen pohjalta luoduin pelaajaobjektein. Luokkaan on lisätty toinen, käyttämätön konstruktori, josta saattaa olla apua moninpelin toteutuksessa.
### GameConfiguration
Sisältää tarvittavat tiedot identtisen pelitilan luontiin. Identtisellä pelikonfiguraatiolla tulisi saada tuotettua identtinen pelitila, joka myös etenee deterministisesti. [^1] Luokkaan on lisätty toinen, käyttämätön konstruktori, josta saattaa olla apua moninpelin toteutuksessa.
### GameWorld
Sisältää pelitilan luoman pelimaailman (rakennusten, gorillojen sijainnit yms.)
### Player
Pelaajan tiedot, kuten nimen ja tämän siirrot sisältävä luokka. Pelaajaoliot voidaan luoda `GameConfiguration`-luokan nimilistan pohjalta.
[^1]: GameConfiguration-oliossa on mukana siemenluku, joka annetaan satunnaislukugeneraattorille => satunnaislukugeneraattori generoi identtisiä lukuja kaikissa solmuissa, generaattorin luomaa dataa käytetään pelimaailman tilan satunnaisoperaatiohin => sama, deterministinen pelimaailma kaikilla solmuilla
## Vaiheet:
Loppupelin toimintaansaatto on suurin implementoinnin osakokonaisuuksista, joten siihen kannattaa varata eniten aikaa. Tämän kokoisessa tehtävässä on hyvä jakaa tehtävä vielä pienempiin vaiheisiin. Mikäli alkuunpääsy tuntuu hankalalta, alla lista, jonka mukaan etenemistä kannattaa suunnitella:
- Suositellaan perityn `GorillaMultiplayerLogic` -luokan toteuttamista, mikäli 2. kohdassa ei vielä luotu
- Uuden tiedon saaminen Mesh-verkosta: Miten välittää Mesh-verkosta tietoa JavaFX-säikeeseen turvallisesti?
- Myös: Uuden tiedon lukeminen JavaFX-säikeessä. Missä kohtaa uudet viestit voisi tarkistaa?
- Siirretään Chatin tulostus Javafx-säikeeseen
- Mitä tapahtuu, kun valitaan "Palvelinyhteys" valikosta?
- Kuka päättää GameConfigurationin sisällön?
- Miten ne, jotka eivät päätä GameConfigurationin sisältöä, liittyvät peliin?
- Miten nämä asiat viestitään verkon yli; Miten neuvottelu tapahtuu?
- Identtisen pelitilan luonti
- Miten pelaajien sijainnit yhdistetään solmuihin?
- Miten siirrot siirretään verkon ylitse ja miten ne kohdistetaan oikeaan pelaajaan?
Näiden osaongelmien ratkaisemisen jälkeen peli tulisi olla minimivaatimuksiltaan hyväksytty
\ No newline at end of file
# Mesh-verkko
Mesh-verkon määritelmä tämän työn kontekstissa annetaan tässä dokumentissa. Älä käytä Googlea tai muuta hakukonetta määritelmän tarkentamiseen, sillä tässä esitelty malli on yksinkertaistettu versio verrattuna hakukoneitse löytyviin esimerkkeihin. Mikäli kaipaat tarkennusta johonkin kohtaan, lähetä viestiä kurssiassistenteille ([jastpa@utu.fi](mailto:jastpa@utu.fi) tai [jaanle@utu.fi](mailto:jaanle@utu.fi)) tai kysy harjoitustyöpajoissa.
Mesh-toteutuksen luonnissa ei tulisi käyttää yhtäkään Gorillapelin luokkaa, vaan tarkoitus on toteuttaa geneerinen verkkokerros, joka pystyy välittämään dataa siihen liittyneiden solmujen kesken. Gorillapeli muokataan myöhemmissä vaiheissa käyttämään Mesh-toteutusta. Periaatteessa kehityksen ajaksi Main-luokan `main()`-metodista voi kommentoida graafisen käyttöliittymän käynnistymisen pois päältä ja testata Mesh-toteutusta ilman Gorillapelin käynnistystä. Itse mesh-toteutuksen luokat suositellaan toteuttamaan omassa pakkauksessaan, esimerkiksi fi.utu.tech.distributed.mesh.
## Toimintakuvaus
Mesh-verkko koostuu TCP-sokketteja kommunikaatiossa hyödyntävistä *solmuista* (ts. pelin kontekstissa solmut ovat eri koneilla käynnissä olevia gorillapelejä). Yksittäinen solmu sisältää niin asiakas- kuin palvelinkomponentitkin, joka mahdollistaa solmun niin yhdistävän toiseen solmuun kuin myös kuuntelevan toisten solmujen yhteydenottopyyntöjäkin. Jokainen solmu ottaa yhteyden vain yhteen aiemmin käynnistyneeseen solmuun, mutta yksittäiseen solmuun voivat useat solmut yhdistää. Tämä tekee verkon rakenteesta "puumaisen" (ks. kuva).
```mermaid
graph BT
A(Solmu 1)
B("Solmu 2") --> A
C("Solmu 3") --> A
D("Solmu 4") --> C
E("Solmu 5") --> D
F("Solmu 6") --> D
G("Solmu 8") --> B
H("Solmu 9") --> C
```
*Graafi eräästä Mesh-verkosta. Nuolet esittävät yhdistämissuuntaa, mutta kommunikaatio on kaksisuuntaista*
Jotta solmu voi ottaa yhteyden muihin mesh-verkon solmuihin, täytyy se yhdistää verkkoon jonkun verkossa olevan solmun kautta. IP-osoite tällaiseen solmuun annetaan käyttäjän toimesta, eli muiden mekanismia solmujen etsimiseen ei tarvitse toteuttaa. Tämän lisäksi solmulle annetaan parametrina portti, johon serverikomponentti asetetaan kuuntelemaan toisten solmujen yhdistämispyyntöjä.
Kun solmut on saatu yhdistettyä TCP-soketeilla toisiinsa, pitäisi viestejä pystyä lähettämään mille tahansa solmulle, joka on verkkoon yhdistänyt. Viestit voi yksinkertaisimmillaan välittää tulvimistekniikalla [(flooding)](https://en.wikipedia.org/wiki/Flooding_(computer_networking)), jolloin siis jokainen solmu vastaanottaessaan viestin, edelleenlähettää sen kaikille naapureillensa (ts. kaikille solmuille, jotka ovat tähän yhdistäneet sekä solmuun, johon tämä itse on yhdistänyt). Tällöin on tärkeää kehittää jokin mekanismi, jolla hillitään kontrolloimatonta viestitulvaa. Eräs tapa on pitää kirjaa, mitkä viestit on vastaanotettu aiemmin, jolloin viestien turha uudelleenlähetys toistamiseen voidaan estää. Tulvimistekniikalla läettäessä voidaan myös yksityisviestit toteuttaa viestiin liitettävän lähetystunnisteen avulla.
## Vaatimukset
- Toteutetaan TCP-socketeilla
- Kaikkien solmujen tulee pystyä vastaanottamaan viestit kaikilta solmuilta
- Yhden solmun tulee tukea useaa tähän yhdistävää solmua säieturvallisesti
- Viesti tulee pystyä kohdistamaan tietylle solmulle
- Solmujen tulee suodattaa duplikaattiviestit sekä heille kuulumattomat viestit vastaanottaessa, eikä paljastaa niitä Mesh-verkkoa käyttävälle ohjelmalle
- Liitosten ei tarvitse olla redudantteja: Tilanteessa, jossa vertainen häviää verkosta, sallitaan, että kaikki tämän vertaisen kautta yhteydessä olevat irtoavat myös Mesh-verkosta
- Mesh-kerroksen tulee luovuttaa vastaanotettu validi, solmulle kuuluva tieto, Mesh-verkkoa käyttävälle ohjelmalle (ts. pelille) säieturvallisesti (Tämä selkenee paremmin 3. aliosiossa)
## Vihjeitä työn implementointiin
### Vinkki 1
Mesh-verkkototeutukseen riittää todennäköisesti 3 luokkaa: Yhteyspyyntöjä odottava ja julkisen APIn tarjoava palvelinluokka/pääluokka, vertaiskommunikaatiosta huolehtiva luokka (voi olla sisäluokka, "inner class") sekä Mesh-viestiluokka, jossa lähetettävä tieto kuljetetaan verkon ylise: sisältää viestin otsaketiedot, sekä itse hyötykuorman.
### Vinkki 2
Vertaisten ei tarvitse olla erityisen älykkäitä viestien edelleenvälityksessä. Toisin sanoen, vastaanotetun viestin riittää lähettää eteenpäin kaikille naapureille, mikäli se on vastaanotettu ensimmäistä kertaa. On vastaanottajan vastuulla hylätä viesti, joka on jo nähty tai muuten kelvoton.
### Vinkki 3
Mesh-verkkototeutuksen pääluokka voisi näyttää seuraavalle:
```java
public class Mesh {
/**
* Luo Mesh-palvelininstanssi
* @param port Portti, jossa uusien vertaisten liittymispyyntöjä kuunnellaan
*/
public Mesh(int port);
/**
* Käynnistä uusien vertaisten kuuntelusäie
*/
public void run();
/**
* Lähetä hyötykuorma kaikille vastaanottajille
* @param o Lähetettävä hyötykuorma
*/
public void broadcast(Serializable o);
/**
* Lähetä hyötykuorma valitulle vertaiselle
* @param o Lähetettävä hyötykuorma
* @param recipient Vastaanottavan vertaisen tunnus
*/
public void send(Serializable o, long recipient);
/**
* Sulje mesh-palvelin ja kaikki sen yhteydet
*/
public void close();
/**
* Lisää token, eli "viestitunniste"
* Käytännössä merkkaa viestin tällä tunnisteella luetuksi
* Määreenä private, koska tätä käyttävä luokka on sisäluokka (inner class)
* Jos et käytä sisäluokkaa, pitää olla public
* @param token Viestitunniste
*/
private void addToken(long token);
/**
* Tarkista, onko viestitunniste jo olemassa
* Määreenä private, koska tätä käyttävä luokka on sisäluokka (inner class)
* Jos et käytä sisäluokkaa, pitää olla public
* @param token Viestitunniste
*/
private boolean tokenExists(long token);
/**
* Yhdistä tämä vertainen olemassaolevaan Mesh-verkkoon
* @param addr Solmun ip-osoite, johon yhdistetään
* @param port Portti, jota vastapuolinen solmu kuuntelee
*/
public void connect(InetAddress addr, int port);
}
```
### Vinkki 4
Mikäli alkuun pääseminen tuntuu haastavalta, aloita kopioimalla vinkin 3 luokkarakenne `Mesh.java` -tiedostoon ja toteuta luokka siihen asti, että pysyt `run()`-metodissa kuuntelemaan uusia yhteyspyyntöjä.
Kuuntelijasäikeen lisäksi tarvitset luokan, jonka oliot kommunikoivat yhdistäneen asiakkaan kanssa. Ensialkuun riittää, että luot esimerkiksi `Handler`-nimisen luokan, jonka toteutat siihen asti, että se pystyy lukemaan socketeista olioita (kuten String) ja tulostamaan ne näytölle. Samaiseen luokkaan voit luoda myös send(String str) -metodin, joka puolestaan lähettäisi olioita socketin kautta.
Vastaavat rakenteet löytyvät edelleen [distributed-chat -keskustelusovellusta](https://gitlab.utu.fi/tech/education/distributed-systems/distributed-chat) sekä [example-sockets-sivuilta](https://gitlab.utu.fi/tech/education/distributed-systems/example-sockets), josta voit ottaa mallia.
### Vinkki 5
Voit yhdistää omaan koneeseesi ottamalla yhteyttä ns. loopback-osoitteeseen 127.0.0.1, "localhost"