Compare commits
210 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0d8f1ac4f7 | |||
| 54fd43142b | |||
| 5bd88445d2 | |||
| e6c9c12e3b | |||
| be124f4a05 | |||
| 85d604b978 | |||
| 47544c4f1b | |||
| 46b1388cad | |||
| 68827e177e | |||
| 0a5eb5d11c | |||
| 8fae4789ad | |||
| 8dd99726ce | |||
| 1223e1a4a9 | |||
| 9d9b6ae2c7 | |||
| a7cdf995c4 | |||
| 82582dae83 | |||
| 7c5a4675dd | |||
| 2979c91ee0 | |||
| 8263a1355a | |||
| 118869f461 | |||
| f7f570638b | |||
| 80c7e1c25d | |||
| 135e6a5327 | |||
| 4e853cf35f | |||
| f6c889c250 | |||
| 8f7c92684b | |||
| a5f2ac635d | |||
| 7ca0bcc3e0 | |||
| aa508dfd6c | |||
| 9c3f62db24 | |||
| 99a7154701 | |||
| 2d2bdddfa8 | |||
| 0bfcbe95b0 | |||
| 94a2e1f977 | |||
| 1bfbb5c75d | |||
| 61f0a23bfa | |||
| 08dc94b4b0 | |||
| ca11c135f1 | |||
| 60f59e03d4 | |||
| 1e794337b5 | |||
| 3509a967a6 | |||
| 4c0e6aeb36 | |||
| 3e138ab84e | |||
| 45fd696fec | |||
| b898357d76 | |||
| 830c04a706 | |||
| d242aeef7c | |||
| 71d6ac9ecc | |||
| 8415e3f155 | |||
| a15a0a0044 | |||
| e3e1da0082 | |||
| d1bf8e4c9a | |||
| f3c34dfa7f | |||
| 2af3fe7c35 | |||
| d01bbdec7b | |||
| 58c473fc9e | |||
| c411e9c560 | |||
| 1c5c4c8a34 | |||
| 9376b1d406 | |||
| cb98c70562 | |||
| d8bfc1c8e9 | |||
| 14c18df0eb | |||
| 208a284204 | |||
| ef2a00268f | |||
| d403908341 | |||
| fa54fd75c1 | |||
| 50d75e2f03 | |||
| 3dbd3a6c15 | |||
| 1aafc0bc4b | |||
| 2f7040485a | |||
| 736cf230fb | |||
| 3ce0a44a18 | |||
| 59494ea2d4 | |||
| 34c89b671e | |||
| bb0f805b4e | |||
| 573c5fd36d | |||
| 09ebacc1e4 | |||
| 7be2229280 | |||
| 3a8dc98e91 | |||
| b731f48ba7 | |||
| 9a88cfc945 | |||
| e22446bffc | |||
| 59a4648ad6 | |||
| ec4d8bfb07 | |||
| 1fe6d9ce5d | |||
| 8f5d90c538 | |||
| 8610f77386 | |||
| c38f177a16 | |||
| 815caff372 | |||
| c441318df9 | |||
| 7d2486d3be | |||
| 18d8b68fc8 | |||
| 4ecbe21602 | |||
| e718083010 | |||
| b810b2187b | |||
| 5b6b58f1cc | |||
| 6e35c599da | |||
| 66f840b269 | |||
| 39b6c5e13e | |||
| 000a5c0d9a | |||
| 0157102ae5 | |||
| a7bfb59e2f | |||
| 37faa39f10 | |||
| 147f84665e | |||
| 8aa71a4430 | |||
| cfeea5222d | |||
| fb02faffc9 | |||
| 815b60232c | |||
| 28f8d17237 | |||
| d311553de7 | |||
| e797470ca3 | |||
| 18f17564d5 | |||
| 57010d2872 | |||
| 004b2e2313 | |||
| 9b40396eed | |||
| 4bcb9a9874 | |||
| 9c3114b5f3 | |||
| 1cefe9c9d9 | |||
| 0e30e08247 | |||
| 38133e6761 | |||
| 4231ca3e97 | |||
| 75567c59b2 | |||
| a39775bd04 | |||
| 49019075e4 | |||
| 97a860df9e | |||
| 0b51f4f45a | |||
| 711de2e6aa | |||
| 74566f99cf | |||
| c301039bfc | |||
| e1029932d8 | |||
| db7892d9c4 | |||
| 7b32fe1147 | |||
| 54588276d1 | |||
| fc8a92789a | |||
| 7fcb47ed6f | |||
| d4bf69dc59 | |||
| b073760eef | |||
| 48d77b6cbe | |||
| 59765e33bb | |||
| fa95ce1418 | |||
| d2407d61f4 | |||
| d414c27ff2 | |||
| f4a32b1649 | |||
| 5407e245f7 | |||
| 02dc75c331 | |||
| 4989e26ac6 | |||
| 89d4cad336 | |||
| c0b6a5424a | |||
| f9b32bc979 | |||
| 25d145cffd | |||
| d47084d6f5 | |||
| c87e6f1463 | |||
| 23faf0f371 | |||
| 268cf283bd | |||
| 5ff4dcec53 | |||
| dac05de331 | |||
| b1ab8f3982 | |||
| 82dd5a9b8a | |||
| 18331a7cc3 | |||
| a2256b2c1e | |||
| 85c872df23 | |||
| 2b012aa256 | |||
| 4879b1b464 | |||
| a5204eaf29 | |||
| 14384dea5b | |||
| e5e93c1a65 | |||
| f1b6243d6d | |||
| 7daae485da | |||
| 3c9f19c5da | |||
| a7e43619f8 | |||
| 34cb17d0e4 | |||
| a6afbeeae1 | |||
| a5272b99ab | |||
| fb6f14371f | |||
| 32e4b04c87 | |||
| 22990fa35e | |||
| ac9e7eecff | |||
| c1a4b436fe | |||
| 0a57ac1a1e | |||
| a663286c0a | |||
| 3de3a2f113 | |||
| bce3326a0b | |||
| 2183241e08 | |||
| f6eb93ad16 | |||
| 827ba59c7b | |||
| b9878a9c33 | |||
| d0ea56daa4 | |||
| 4549fb4471 | |||
| 86303f8b84 | |||
| 2dd3ea2562 | |||
| 676e24e78d | |||
| d346ec4f97 | |||
| e85ae3f59f | |||
| c4942aa714 | |||
| dc784050bf | |||
| 76c182313e | |||
| 8579f15868 | |||
| bf6873cfd7 | |||
| 5f9a28e96c | |||
| 64742d6aad | |||
| 8e52ed21a5 | |||
| e7b95098b2 | |||
| d060697aa9 | |||
| ea8ac3831f | |||
| b868f3289b | |||
| 9c291e4e3b | |||
| 91a513e214 | |||
| 66925bc665 | |||
| ff38c9d6ef | |||
| e6b5cac53c |
@@ -104,5 +104,21 @@ $RECYCLE.BIN/
|
|||||||
## Jenv
|
## Jenv
|
||||||
.java-version
|
.java-version
|
||||||
|
|
||||||
## VSCode
|
## diagrams.net / draw.io
|
||||||
.vscode
|
# Draw.io temporary files
|
||||||
|
*.drawio.bkp
|
||||||
|
*.drawio.tmp
|
||||||
|
|
||||||
|
# Auto-save files
|
||||||
|
*.drawio.autosave
|
||||||
|
|
||||||
|
# Exported images (optional – nur wenn du Exporte nicht versionieren willst)
|
||||||
|
*.drawio.png
|
||||||
|
*.drawio.svg
|
||||||
|
*.drawio.pdf
|
||||||
|
|
||||||
|
## IntelliJ
|
||||||
|
.idea
|
||||||
|
|
||||||
|
## bin
|
||||||
|
bin/
|
||||||
@@ -0,0 +1,110 @@
|
|||||||
|
stages:
|
||||||
|
- lint
|
||||||
|
- report
|
||||||
|
- build
|
||||||
|
- test
|
||||||
|
|
||||||
|
# Reusable definitions
|
||||||
|
.gradle-cache: &gradle-cache
|
||||||
|
cache:
|
||||||
|
key: '$CI_PROJECT_ID-gradle'
|
||||||
|
paths:
|
||||||
|
- .gradle/
|
||||||
|
- ~/.gradle/caches/
|
||||||
|
|
||||||
|
.on-commit: &on-commit
|
||||||
|
rules:
|
||||||
|
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
|
||||||
|
when: never
|
||||||
|
- if: '$CI_COMMIT_BRANCH'
|
||||||
|
allow_failure: true
|
||||||
|
|
||||||
|
.on-mr: &on-mr
|
||||||
|
rules:
|
||||||
|
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
|
||||||
|
allow_failure: true # deactivate after all issues (shown by checkstyle) have been resolved
|
||||||
|
|
||||||
|
# Jobs
|
||||||
|
checkstyle:
|
||||||
|
<<: [*gradle-cache, *on-commit]
|
||||||
|
stage: lint
|
||||||
|
image: gradle:9.3.1-jdk25
|
||||||
|
script:
|
||||||
|
- gradle checkstyleMain checkstyleTest
|
||||||
|
allow_failure: true
|
||||||
|
artifacts:
|
||||||
|
when: always
|
||||||
|
paths:
|
||||||
|
- build/reports/checkstyle/main.html
|
||||||
|
- build/reports/checkstyle/main.xml
|
||||||
|
- build/reports/checkstyle/test.html
|
||||||
|
- build/reports/checkstyle/test.xml
|
||||||
|
expose_as: 'Checkstyle Report'
|
||||||
|
expire_in: 1 week
|
||||||
|
|
||||||
|
checkstyle-mr:
|
||||||
|
<<: [*gradle-cache, *on-mr]
|
||||||
|
stage: lint
|
||||||
|
image: gradle:9.3.1-jdk25
|
||||||
|
script:
|
||||||
|
- gradle checkstyleMain checkstyleTest
|
||||||
|
allow_failure: true
|
||||||
|
artifacts:
|
||||||
|
when: always
|
||||||
|
paths:
|
||||||
|
- build/reports/checkstyle/main.html
|
||||||
|
- build/reports/checkstyle/main.xml
|
||||||
|
- build/reports/checkstyle/test.html
|
||||||
|
- build/reports/checkstyle/test.xml
|
||||||
|
expose_as: 'Checkstyle Report'
|
||||||
|
expire_in: 1 week
|
||||||
|
|
||||||
|
checkstyle-report:
|
||||||
|
stage: report
|
||||||
|
image: python:3.14
|
||||||
|
needs:
|
||||||
|
- job: checkstyle-mr
|
||||||
|
artifacts: true
|
||||||
|
optional: true
|
||||||
|
rules:
|
||||||
|
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
|
||||||
|
script:
|
||||||
|
- python3 scripts/convert-checkstyle-gl-report.py
|
||||||
|
artifacts:
|
||||||
|
when: always
|
||||||
|
reports:
|
||||||
|
codequality: gl-code-quality-report.json
|
||||||
|
expire_in: 1 week
|
||||||
|
|
||||||
|
compile-check:
|
||||||
|
<<: *gradle-cache
|
||||||
|
stage: build
|
||||||
|
image: gradle:9.3.1-jdk25
|
||||||
|
script:
|
||||||
|
- gradle compileTestJava
|
||||||
|
needs: []
|
||||||
|
rules:
|
||||||
|
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
|
||||||
|
allow_failure: false
|
||||||
|
- if: '$CI_COMMIT_BRANCH'
|
||||||
|
allow_failure: false
|
||||||
|
|
||||||
|
test:
|
||||||
|
<<: *gradle-cache
|
||||||
|
stage: test
|
||||||
|
image: gradle:9.3.1-jdk25
|
||||||
|
script:
|
||||||
|
- gradle test
|
||||||
|
artifacts:
|
||||||
|
when: always
|
||||||
|
reports:
|
||||||
|
junit: build/test-results/test/*.xml
|
||||||
|
paths:
|
||||||
|
- build/reports/tests/test/
|
||||||
|
expose_as: 'Test Report'
|
||||||
|
expire_in: 1 week
|
||||||
|
rules:
|
||||||
|
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
|
||||||
|
allow_failure: false
|
||||||
|
- if: '$CI_COMMIT_BRANCH'
|
||||||
|
allow_failure: true
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"recommendations": [
|
||||||
|
"vscjava.vscode-java-pack",
|
||||||
|
"mhutchie.git-graph",
|
||||||
|
"gitlab.gitlab-workflow",
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"java.configuration.updateBuildConfiguration": "automatic"
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"version": "2.0.0",
|
||||||
|
"tasks": [
|
||||||
|
{
|
||||||
|
"label": "Export drawio to SVG",
|
||||||
|
"type": "shell",
|
||||||
|
"command": "./scripts/export-drawio.sh",
|
||||||
|
"problemMatcher": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Export puml to SVG",
|
||||||
|
"type": "shell",
|
||||||
|
"command": "./scripts/export-plantuml.sh",
|
||||||
|
"problemMatcher": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -35,6 +35,8 @@ This protocol handles game state synchronization, player actions, lobby and glob
|
|||||||
│ └── test/ # Unit tests
|
│ └── test/ # Unit tests
|
||||||
├── documents/ # Project resources and documentation
|
├── documents/ # Project resources and documentation
|
||||||
│ ├── images/ # Images for README and documentation
|
│ ├── images/ # Images for README and documentation
|
||||||
|
│ ├── diary # Diary files for each organizational meetup
|
||||||
|
│ ├── blog # Blog files covering project-related topics
|
||||||
│ ├── docs # Documentation
|
│ ├── docs # Documentation
|
||||||
│ ├── milestones/ # Milestone deliverables (6 milestones)
|
│ ├── milestones/ # Milestone deliverables (6 milestones)
|
||||||
│ └── (Other resources) # Additional project materials
|
│ └── (Other resources) # Additional project materials
|
||||||
|
|||||||
|
After Width: | Height: | Size: 4.8 KiB |
|
After Width: | Height: | Size: 3.0 KiB |
|
After Width: | Height: | Size: 194 KiB |
@@ -2,6 +2,8 @@ plugins {
|
|||||||
id 'application'
|
id 'application'
|
||||||
id 'java'
|
id 'java'
|
||||||
id 'org.openjfx.javafxplugin' version '0.1.0'
|
id 'org.openjfx.javafxplugin' version '0.1.0'
|
||||||
|
id 'checkstyle'
|
||||||
|
id 'com.diffplug.spotless' version '7.0.2'
|
||||||
}
|
}
|
||||||
|
|
||||||
group = 'ch.unibas.dmi.dbis'
|
group = 'ch.unibas.dmi.dbis'
|
||||||
@@ -23,18 +25,44 @@ repositories {
|
|||||||
|
|
||||||
javafx {
|
javafx {
|
||||||
version = "25.0.2"
|
version = "25.0.2"
|
||||||
modules = ['javafx.controls', 'javafx.fxml', 'javafx.base']
|
modules = ['javafx.controls', 'javafx.fxml', 'javafx.base', 'javafx.graphics', 'javafx.web']
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
|
// Source: https://mvnrepository.com/artifact/org.apache.logging.log4j
|
||||||
|
implementation("org.apache.logging.log4j:log4j-api:2.25.3")
|
||||||
|
runtimeOnly("org.apache.logging.log4j:log4j-core:2.25.3")
|
||||||
|
|
||||||
|
// Source: https://mvnrepository.com/artifact/org.fusesource.jansi/jansi
|
||||||
|
runtimeOnly("org.fusesource.jansi:jansi:2.4.2")
|
||||||
|
|
||||||
testImplementation("org.junit.jupiter:junit-jupiter:5.10.0")
|
testImplementation("org.junit.jupiter:junit-jupiter:5.10.0")
|
||||||
testRuntimeOnly("org.junit.platform:junit-platform-launcher:1.10.0")
|
testRuntimeOnly("org.junit.platform:junit-platform-launcher:1.10.0")
|
||||||
|
implementation 'org.json:json:20240303'
|
||||||
}
|
}
|
||||||
|
|
||||||
test {
|
test {
|
||||||
useJUnitPlatform()
|
useJUnitPlatform()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
checkstyle {
|
||||||
|
toolVersion = '10.21.0'
|
||||||
|
configFile = file('config/checkstyle/checkstyle.xml')
|
||||||
|
configProperties = [
|
||||||
|
'suppressionFile': file('config/checkstyle/suppressions.xml').absolutePath
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
spotless {
|
||||||
|
java {
|
||||||
|
googleJavaFormat('1.25.2').aosp()
|
||||||
|
importOrder()
|
||||||
|
removeUnusedImports()
|
||||||
|
trimTrailingWhitespace()
|
||||||
|
endWithNewline()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
tasks.named('jar', Jar) {
|
tasks.named('jar', Jar) {
|
||||||
manifest {
|
manifest {
|
||||||
attributes('Main-Class': application.mainClass.get())
|
attributes('Main-Class': application.mainClass.get())
|
||||||
@@ -42,6 +70,22 @@ tasks.named('jar', Jar) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Dieses JVM-Argument erlaubt JavaFX und anderen Libraries den Zugriff auf native Methoden.
|
||||||
|
// Ohne diese Einstellung erscheinen Warnungen und zukünftige Java-Versionen könnten den Zugriff blockieren.
|
||||||
|
// Siehe: https://openjdk.org/jeps/472
|
||||||
|
tasks.withType(JavaExec) {
|
||||||
|
jvmArgs += '--enable-native-access=ALL-UNNAMED'
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
tasks.register('cruntest', JavaExec) {
|
||||||
|
classpath = sourceSets.main.runtimeClasspath
|
||||||
|
mainClass = 'ch.unibas.dmi.dbis.cs108.casono.Main'
|
||||||
|
jvmArgs '--enable-native-access=ALL-UNNAMED'
|
||||||
|
args 'client', '0.0.0.0:1234'
|
||||||
|
}
|
||||||
|
|
||||||
tasks.register('fatJar', Jar) {
|
tasks.register('fatJar', Jar) {
|
||||||
group = 'build'
|
group = 'build'
|
||||||
description = 'Assembles a runnable fat JAR including runtime dependencies.'
|
description = 'Assembles a runnable fat JAR including runtime dependencies.'
|
||||||
|
|||||||
@@ -0,0 +1,174 @@
|
|||||||
|
<?xml version="1.0"?>
|
||||||
|
<!DOCTYPE module PUBLIC
|
||||||
|
"-//Checkstyle//DTD Checkstyle Configuration 1.3//EN"
|
||||||
|
"https://checkstyle.org/dtds/configuration_1_3.dtd">
|
||||||
|
|
||||||
|
<module name="Checker">
|
||||||
|
<!-- Fail build if checkstyle finds violations -->
|
||||||
|
<!-- https://checkstyle.sourceforge.io/config.html#Properties_1 -->
|
||||||
|
<property name="severity" value="error"/>
|
||||||
|
|
||||||
|
<!-- Only check java files -->
|
||||||
|
<!-- https://checkstyle.sourceforge.io/config.html#Properties_1 -->
|
||||||
|
<property name="fileExtensions" value="java"/>
|
||||||
|
|
||||||
|
<module name="SuppressionFilter">
|
||||||
|
<property name="file" value="${suppressionFile}"/>
|
||||||
|
</module>
|
||||||
|
|
||||||
|
<!-- Only allow spaces -->
|
||||||
|
<!-- https://checkstyle.sourceforge.io/checks/whitespace/filetabcharacter.html -->
|
||||||
|
<module name="FileTabCharacter"/>
|
||||||
|
|
||||||
|
<!-- Maximale Zeilenlänge -->
|
||||||
|
<!-- https://checkstyle.sourceforge.io/checks/sizes/linelength.html -->
|
||||||
|
<module name="LineLength">
|
||||||
|
<property name="max" value="100"/>
|
||||||
|
</module>
|
||||||
|
|
||||||
|
<module name="TreeWalker">
|
||||||
|
<!-- Allows for emptylines BETWEEN methods and classes but DISALLOWS newlines after an opening bracket -->
|
||||||
|
<!-- https://checkstyle.sourceforge.io/checks/whitespace/emptylineseparator.html -->
|
||||||
|
<module name="EmptyLineSeparator">
|
||||||
|
<property name="allowNoEmptyLineBetweenFields" value="true"/>
|
||||||
|
<property name="tokens" value="
|
||||||
|
METHOD_DEF,
|
||||||
|
CTOR_DEF,
|
||||||
|
STATIC_INIT,
|
||||||
|
INSTANCE_INIT,
|
||||||
|
CLASS_DEF,
|
||||||
|
INTERFACE_DEF,
|
||||||
|
ENUM_DEF
|
||||||
|
"/>
|
||||||
|
</module>
|
||||||
|
|
||||||
|
<!-- Enforce indentation of four whitespaces -->
|
||||||
|
<!-- https://checkstyle.sourceforge.io/checks/misc/indentation.html -->
|
||||||
|
<module name="Indentation">
|
||||||
|
<property name="basicOffset" value="4"/>
|
||||||
|
<property name="caseIndent" value="4"/>
|
||||||
|
<property name="throwsIndent" value="4"/>
|
||||||
|
<property name="arrayInitIndent" value="4"/>
|
||||||
|
<property name="lineWrappingIndentation" value="8"/>
|
||||||
|
</module>
|
||||||
|
|
||||||
|
<!-- No wildcard imports (import x.*) -->
|
||||||
|
<!-- https://checkstyle.sourceforge.io/checks/imports/avoidstarimport.html -->
|
||||||
|
<module name="AvoidStarImport"/>
|
||||||
|
|
||||||
|
<!-- No unused imports -->
|
||||||
|
<!-- https://checkstyle.sourceforge.io/checks/imports/unusedimports.html -->
|
||||||
|
<module name="UnusedImports"/>
|
||||||
|
|
||||||
|
<!-- No unordered / ungrouped imports -->
|
||||||
|
<!-- https://checkstyle.sourceforge.io/checks/imports/importorder.html -->
|
||||||
|
<module name="ImportOrder"/>
|
||||||
|
|
||||||
|
<!-- Classes, enums, records, ... as PascalCase -->
|
||||||
|
<!-- https://checkstyle.sourceforge.io/checks/naming/typename.html -->
|
||||||
|
<module name="TypeName"/>
|
||||||
|
|
||||||
|
<!-- Method names as camelCase -->
|
||||||
|
<!-- https://checkstyle.sourceforge.io/checks/naming/methodname.html -->
|
||||||
|
<module name="MethodName"/>
|
||||||
|
|
||||||
|
<!-- Parameter names as camelCase -->
|
||||||
|
<!-- https://checkstyle.sourceforge.io/checks/naming/parametername.html -->
|
||||||
|
<module name="ParameterName"/>
|
||||||
|
|
||||||
|
<!-- Lokal variables as camelCase -->
|
||||||
|
<!-- https://checkstyle.sourceforge.io/checks/naming/localvariablename.html -->
|
||||||
|
<module name="LocalVariableName"/>
|
||||||
|
|
||||||
|
<!-- Constants as UPPER_SNAKE_CASE -->
|
||||||
|
<!-- https://checkstyle.sourceforge.io/checks/naming/constantname.html -->
|
||||||
|
<module name="ConstantName"/>
|
||||||
|
|
||||||
|
<!-- Field names as camelCase -->
|
||||||
|
<!-- https://checkstyle.sourceforge.io/checks/naming/membername.html -->
|
||||||
|
<module name="MemberName">
|
||||||
|
<property name="format" value="^[a-z][a-zA-Z0-9]*$"/>
|
||||||
|
</module>
|
||||||
|
|
||||||
|
<!-- Restrict length of method name -->
|
||||||
|
<!-- https://checkstyle.sourceforge.io/checks/sizes/methodlength.html -->
|
||||||
|
<module name="MethodLength">
|
||||||
|
<property name="max" value="60"/>
|
||||||
|
<property name="countEmpty" value="false"/>
|
||||||
|
</module>
|
||||||
|
|
||||||
|
<!-- Restrict number of parameters -->
|
||||||
|
<!-- https://checkstyle.sourceforge.io/checks/sizes/parameternumber.html -->
|
||||||
|
<module name="ParameterNumber">
|
||||||
|
<property name="max" value="5"/>
|
||||||
|
</module>
|
||||||
|
|
||||||
|
<!-- Modifier-Reihenfolge: public static final ... -->
|
||||||
|
<!-- https://checkstyle.sourceforge.io/checks/modifier/modifierorder.html -->
|
||||||
|
<module name="ModifierOrder"/>
|
||||||
|
|
||||||
|
<!-- Require braces arround code block (no single instruction after if/else/while/...)-->
|
||||||
|
<!-- https://checkstyle.sourceforge.io/checks/blocks/needbraces.html -->
|
||||||
|
<module name="NeedBraces"/>
|
||||||
|
|
||||||
|
<!-- Require the left curly braces at the same line -->
|
||||||
|
<!-- https://checkstyle.sourceforge.io/checks/blocks/leftcurly.html -->
|
||||||
|
<module name="LeftCurly"/>
|
||||||
|
|
||||||
|
<!-- Require right curly braces at the same line as following instructions -->
|
||||||
|
<!-- https://checkstyle.sourceforge.io/checks/blocks/rightcurly.html -->
|
||||||
|
<module name="RightCurly"/>
|
||||||
|
|
||||||
|
<!-- Disallow empty code blocks -->
|
||||||
|
<!-- https://checkstyle.sourceforge.io/checks/blocks/emptyblock.html -->
|
||||||
|
<module name="EmptyBlock">
|
||||||
|
<property name="option" value="text"/>
|
||||||
|
</module>
|
||||||
|
|
||||||
|
<!-- Disallow empty code blocks after catch -->
|
||||||
|
<!-- https://checkstyle.sourceforge.io/checks/blocks/emptycatchblock.html -->
|
||||||
|
<module name="EmptyCatchBlock">
|
||||||
|
<property name="exceptionVariableName" value="expected|ignored"/>
|
||||||
|
</module>
|
||||||
|
|
||||||
|
<!-- Require string literals to be compared with .equals and not == -->
|
||||||
|
<!-- https://checkstyle.sourceforge.io/checks/coding/stringliteralequality.html -->
|
||||||
|
<module name="StringLiteralEquality"/>
|
||||||
|
|
||||||
|
<!-- Disallow standalone numbers in code -->
|
||||||
|
<!-- https://checkstyle.sourceforge.io/checks/coding/magicnumber.html -->
|
||||||
|
<module name="MagicNumber">
|
||||||
|
<property name="ignoreNumbers" value="-1, 0, 1, 2"/>
|
||||||
|
<property name="ignoreAnnotation" value="true"/>
|
||||||
|
</module>
|
||||||
|
|
||||||
|
<!-- Disallow nested code blocks dangling in code -->
|
||||||
|
<!-- https://checkstyle.sourceforge.io/checks/blocks/avoidnestedblocks.html -->
|
||||||
|
<module name="AvoidNestedBlocks"/>
|
||||||
|
|
||||||
|
<!-- Disallow System.out.println -->
|
||||||
|
<!-- https://checkstyle.sourceforge.io/checks/regexp/regexp.html -->
|
||||||
|
<module name="Regexp">
|
||||||
|
<property name="id" value="SystemOutErr"/>
|
||||||
|
<property name="format" value="System\.(out|err)\.print"/>
|
||||||
|
<property name="illegalPattern" value="true"/>
|
||||||
|
<property name="message" value="No `System.out.print(err)` allowed — use a logger."/>
|
||||||
|
</module>
|
||||||
|
|
||||||
|
<!-- Disallow whitespace before semicolon or bracket -->
|
||||||
|
<!-- https://checkstyle.sourceforge.io/checks/whitespace/nowhitespacebefore.html -->
|
||||||
|
<module name="NoWhitespaceBefore"/>
|
||||||
|
|
||||||
|
<!-- Disallow whitespace after code -->
|
||||||
|
<!-- https://checkstyle.sourceforge.io/checks/whitespace/whitespaceafter.html -->
|
||||||
|
<module name="WhitespaceAfter"/>
|
||||||
|
|
||||||
|
<!-- No trailing whitespace at line ending -->
|
||||||
|
<!-- https://checkstyle.sourceforge.io/checks/regexp/regexp.html -->
|
||||||
|
<module name="Regexp">
|
||||||
|
<property name="format" value=" +$"/>
|
||||||
|
<property name="illegalPattern" value="true"/>
|
||||||
|
<property name="message" value="No trailing whitespace."/>
|
||||||
|
</module>
|
||||||
|
</module>
|
||||||
|
</module>
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
<?xml version="1.0"?>
|
||||||
|
<!DOCTYPE suppressions PUBLIC
|
||||||
|
"-//Checkstyle//DTD SuppressionFilter Configuration 1.2//EN"
|
||||||
|
"https://checkstyle.org/dtds/suppressions_1_2.dtd">
|
||||||
|
|
||||||
|
<suppressions>
|
||||||
|
<!-- Allow literal, for example assertEquals(42, life.answer()) -->
|
||||||
|
<suppress checks="MagicNumber" files=".*Test\.java"/>
|
||||||
|
|
||||||
|
<!-- Allow longer method names as the are usually descriptive of the test -->
|
||||||
|
<suppress checks="MethodLength" files=".*Test\.java"/>
|
||||||
|
|
||||||
|
<!-- Allow methods with more parameters -->
|
||||||
|
<suppress checks="ParameterNumber" files=".*Test\.java"/>
|
||||||
|
|
||||||
|
<!-- Allow for System.out.println in tests -->
|
||||||
|
<suppress id="SystemOutErr" files=".*Test\.java"/>
|
||||||
|
|
||||||
|
<!-- Allow wildcard imports (import x.*) -->
|
||||||
|
<suppress checks="AvoidStarImport" files=".*Test\.java"/>
|
||||||
|
|
||||||
|
<!-- Allow for compacter line seperators -->
|
||||||
|
<suppress checks="EmptyLineSeparator" files=".*Test\.java"/>
|
||||||
|
</suppressions>
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
# Blog
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
This folder contains a collection of blog posts documenting the development process of our application. These blogs serve two key purposes:
|
||||||
|
|
||||||
|
1. **For External Audiences**
|
||||||
|
|
||||||
|
The blogs provide insights into the internal program flow and the interaction between components without requiring readers to dive into the source code. They offer a mixed-level overview of how different parts of the system work together.
|
||||||
|
|
||||||
|
2. **For Our Team**
|
||||||
|
|
||||||
|
The blogs serve as a record of our decision-making process. By documenting what decisions were made and why, we can track the evolution of our design choices and understand the reasoning behind them. This is invaluable for maintaining consistency across the team.
|
||||||
|
|
||||||
|
But keep in mind that these blogs **do not replace** the detailed source code documentation.
|
||||||
|
|
||||||
|
## Format
|
||||||
|
All blog posts follow the naming convention: `<YYYY-MM-DD>_<Title>.md`
|
||||||
|
|
||||||
|
## Blog Posts
|
||||||
|
|
||||||
|
### General
|
||||||
|
*(No posts yet)*
|
||||||
|
|
||||||
|
### Client
|
||||||
|
*(No posts yet)*
|
||||||
|
|
||||||
|
### Server
|
||||||
|
*(No posts yet)*
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
# MS2 Meeting
|
||||||
|
|
||||||
|
> 14.03.2025 (14:00)
|
||||||
|
|
||||||
|
In einem Meeting haben wir die Struktur zwischen **LobbyScreen**, **GameScreen**
|
||||||
|
und der **Chatbox** besprochen. Dabei ging es vor allem darum, wie der Chat in beide
|
||||||
|
UIs eingebettet wird und wie die Verbindung zwischen den Komponenten umgesetzt wird.
|
||||||
|
|
||||||
|
Außerdem wurde festgelegt, dass der **Endscreen** erst nach MS3 umgesetzt wird.
|
||||||
|
Zunächst werden die Texturen fertiggestellt und im GameScreen integriert.
|
||||||
|
Falls sich die Struktur (z. B. FXML oder CSS) noch ändert, kann der Endscreen
|
||||||
|
später aus der bestehenden Struktur abgeleitet werden, ohne Anpassungen doppelt
|
||||||
|
durchführen zu müssen.
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
# Server-Side Networking — How It All Fits Together
|
||||||
|
|
||||||
|
This doc is meant to give a solid mixed-level understanding of how our server-side networking works. No deep dives — just a clear picture of what's happening under the hood when a client connects and sends a command.
|
||||||
|
|
||||||
|
> **Note:** This is v1. Annotations and Reflection-based dispatching are on the roadmap but not covered here yet.
|
||||||
|
|
||||||
|
> **Disclamer:** This document has been written by Claude. I modified certain parts and verified its contents for correctness.
|
||||||
|
|
||||||
|
## Our Protocol at a Glance
|
||||||
|
|
||||||
|
Our protocol is inspired by **POP3** - a classic, text-based protocol that communicates over a raw TCP connection. The idea is simple: the client sends a command as a plain-text string, and the server responds with either a success or an error.
|
||||||
|
|
||||||
|
A typical exchange looks something like this:
|
||||||
|
|
||||||
|
```
|
||||||
|
Client → GET_DELTA SINCE=42
|
||||||
|
Server → +OK
|
||||||
|
PLAYER_FOLDED playerId=3
|
||||||
|
POT 240
|
||||||
|
NEXT_TURN playerId=1
|
||||||
|
.
|
||||||
|
```
|
||||||
|
|
||||||
|
Responses start with `+OK` on success or `-ERR` when something goes wrong. Commands are short, uppercase strings - sometimes followed by arguments.
|
||||||
|
|
||||||
|
We don't use HTTP, there's no JSON body, no headers. Just a raw socket, a text stream, and a clearly defined set of commands.
|
||||||
|
|
||||||
|
## Core Components & Their Roles
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
Here's a quick rundown of the main building blocks:
|
||||||
|
|
||||||
|
### `NetworkManager`
|
||||||
|
This is the entry point. It binds to a specific port and listens for incoming TCP connections.
|
||||||
|
Once a client connects, it creates a `Session` adds it to the `SessionManager` and goes back to waiting. Its not the responsibility of the `NetworkManager` to recieve and send data from and to each connected client.
|
||||||
|
|
||||||
|
### `Session`
|
||||||
|
Every client that connects has its own `Session` instance running on its own thread. This component owns the lifecycle of that connection: it reads incoming data, passes it along for processing, and writes responses back.
|
||||||
|
|
||||||
|
### `SessionId`
|
||||||
|
The `SessionId` is used to uniquely identify each session. Essentially its a wrapper arround the UUID type.
|
||||||
|
|
||||||
|
### `SessionManager`
|
||||||
|
The `SessionManager` stores all currently active sessions. It allows for the retrieval of a specific session by its id.
|
||||||
|
|
||||||
|
### `ProtocolParser`
|
||||||
|
Raw text (encapsulated in a `RawRequest`) coming off the socket isn't immediately useful — the `ProtocolParser` turns it with the help of the `Tokenizer` into a `PrimitiveRequest`.
|
||||||
|
|
||||||
|
### `RawRequest`
|
||||||
|
The `RawRequest` object is created after a message has been recieved.
|
||||||
|
It holds both a reference to the `Session` that recieved the message and the message itself.
|
||||||
|
|
||||||
|
### `PrimitiveRequest`
|
||||||
|
The `PrimitiveRequest` object is created by the `ProtocolParser` after the content of the `RawRequest` has been tokenized by the `Tokenizer`.
|
||||||
|
The command is stored in its own attribute and the arguments are accessible as a dictionary.
|
||||||
|
|
||||||
|
### `Tokenizer`
|
||||||
|
The job of the `tokenizer` is it, to take the raw string and turn it into tokens.
|
||||||
|
For example, the command `CHAT_LOBBY GAME=12 MESSAGE='All-in?'` will be decoded as `"CHAT_LOBBY", "GAME", "=", "12", "MESSAGE", "=", "All-in?"`.
|
||||||
|
It is unaware about the meaning.
|
||||||
|
|
||||||
|
### `CommandParser` (per command)
|
||||||
|
Takes in the `PrimitiveRequest` and checks if all required fields are provided with the correct value.
|
||||||
|
Creates a command specific `Request` object containing command arguments in a structured manner.
|
||||||
|
|
||||||
|
### `Request` (per command)
|
||||||
|
Has fields common along each implementation such as a reference to the `Session` that recieved the request.
|
||||||
|
Furthermore each implementation has fields unique to each command. For example, the `ChatLobbyRequest` has fields for the game and the message.
|
||||||
|
|
||||||
|
### `CommandRouter`
|
||||||
|
By identifying the type of class of the `Request`, the `CommandRouter` routes the request to the matching `CommandHandler`.
|
||||||
|
|
||||||
|
### `CommandHandler` (per command)
|
||||||
|
Each supported command has its own handler — a small, focused class that contains the logic for that specific command. They have access to the domain, containing inner parts of the game itself.
|
||||||
|
|
||||||
|
### `Response` (per response)
|
||||||
|
The interface has two sub-interfaces for either failed `ErrorResponse` or successfull `SuccessResponse` execution of commands.
|
||||||
|
|
||||||
|
Each step has exactly one responsibility. Increasing the ability to test and extend different components later on.
|
||||||
|
|
||||||
|
## Concurrency - Handling Multiple Clients
|
||||||
|
|
||||||
|
Every incoming connection spawns a new **Thread**. This means multiple clients can be served simultaneously without blocking each other.
|
||||||
|
|
||||||
|
As each `Session` runs entirely on its own thread, there's no shared mutable state between sessions, with exception of the `SessionManager` and other key components explained later.
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
@startuml
|
||||||
|
skinparam classAttributeIconSize 0
|
||||||
|
skinparam packageStyle rectangle
|
||||||
|
skinparam linetype ortho
|
||||||
|
skinparam backgroundColor transparent
|
||||||
|
|
||||||
|
' Network Layer
|
||||||
|
class NetworkManager {
|
||||||
|
+ start(port: int)
|
||||||
|
}
|
||||||
|
|
||||||
|
class Session {
|
||||||
|
- id: SessionId
|
||||||
|
+ Session(id: SessionId)
|
||||||
|
+ getId(): SessionId
|
||||||
|
+ run()
|
||||||
|
+ send(response: String)
|
||||||
|
}
|
||||||
|
|
||||||
|
class SessionId {
|
||||||
|
- value: UUID
|
||||||
|
+ SessionId()
|
||||||
|
+ SessionId(value: UUID)
|
||||||
|
}
|
||||||
|
|
||||||
|
class SessionManager {
|
||||||
|
- sessions: Map<UUID, Session>
|
||||||
|
+ add(session: Session)
|
||||||
|
+ getById(id: UUID): Session
|
||||||
|
+ remove(id: UUID)
|
||||||
|
}
|
||||||
|
|
||||||
|
NetworkManager --> Session : creates
|
||||||
|
NetworkManager --> SessionManager : adds session to
|
||||||
|
Session --> SessionId : identified by
|
||||||
|
Session --> RawRequest : creates
|
||||||
|
Session ..> Response : sends
|
||||||
|
SessionManager o-- Session : manages
|
||||||
|
|
||||||
|
' Protocol Layer
|
||||||
|
class RawRequest {
|
||||||
|
- session: Session
|
||||||
|
- rawMessage: String
|
||||||
|
}
|
||||||
|
|
||||||
|
class Tokenizer {
|
||||||
|
+ tokenize(input: String): List<String>
|
||||||
|
}
|
||||||
|
|
||||||
|
class ProtocolParser {
|
||||||
|
+ parse(raw: RawRequest): PrimitiveRequest
|
||||||
|
}
|
||||||
|
|
||||||
|
class PrimitiveRequest {
|
||||||
|
- command: String
|
||||||
|
- arguments: Map<String, String>
|
||||||
|
}
|
||||||
|
|
||||||
|
RawRequest --> ProtocolParser : passed to
|
||||||
|
ProtocolParser --> Tokenizer : uses
|
||||||
|
ProtocolParser --> PrimitiveRequest : produces
|
||||||
|
|
||||||
|
' Command Layer
|
||||||
|
interface CommandParser <<per command>> {
|
||||||
|
+ parse(primitive: PrimitiveRequest): Request
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Request <<per command>> {
|
||||||
|
- session: Session
|
||||||
|
}
|
||||||
|
|
||||||
|
class CommandRouter {
|
||||||
|
+ route(request: Request)
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CommandHandler <<per command>> {
|
||||||
|
+ handle(request: Request)
|
||||||
|
}
|
||||||
|
|
||||||
|
PrimitiveRequest --> CommandParser : passed to
|
||||||
|
CommandParser --> Request : produces
|
||||||
|
CommandRouter --> CommandHandler : dispatches to
|
||||||
|
CommandRouter ..> Request : receives
|
||||||
|
|
||||||
|
' Response Layer
|
||||||
|
interface Response {
|
||||||
|
+ encode(): String
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SuccessResponse
|
||||||
|
|
||||||
|
interface ErrorResponse
|
||||||
|
|
||||||
|
Response <|-- SuccessResponse
|
||||||
|
Response <|-- ErrorResponse
|
||||||
|
@enduml
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
# Skript to convert findings of checkstyle into code quality report for gitlab
|
||||||
|
import xml.etree.ElementTree as ET, json, hashlib
|
||||||
|
|
||||||
|
findings = []
|
||||||
|
for source in ["main", "test"]:
|
||||||
|
try:
|
||||||
|
tree = ET.parse(f"build/reports/checkstyle/{source}.xml")
|
||||||
|
for file in tree.findall("file"):
|
||||||
|
path = file.get("name")
|
||||||
|
for error in file.findall("error"):
|
||||||
|
line = int(error.get("line", 1))
|
||||||
|
msg = error.get("message", "")
|
||||||
|
fingerprint = hashlib.md5(f"{path}{line}{msg}".encode()).hexdigest()
|
||||||
|
findings.append({
|
||||||
|
"type": "issue",
|
||||||
|
"check_name": error.get("source", "checkstyle"),
|
||||||
|
"description": msg,
|
||||||
|
"severity": "minor",
|
||||||
|
"fingerprint": fingerprint,
|
||||||
|
"location": {
|
||||||
|
"path": "src/" + path.split("/src/")[1] if "/src/" in path else path,
|
||||||
|
"lines": {"begin": line}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
except FileNotFoundError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
with open("gl-code-quality-report.json", "w") as f:
|
||||||
|
json.dump(findings, f)
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Check for draw.io installation
|
||||||
|
DRAWIO_PATH=""
|
||||||
|
check_drawio_installed() {
|
||||||
|
if command -v drawio &>/dev/null; then
|
||||||
|
DRAWIO_PATH=$(command -v drawio)
|
||||||
|
return 0 # drawio via PATH
|
||||||
|
elif [[ -x "/Applications/draw.io.app/Contents/MacOS/draw.io" ]]; then
|
||||||
|
DRAWIO_PATH="/Applications/draw.io.app/Contents/MacOS/draw.io"
|
||||||
|
return 0 # For macOS check at default path
|
||||||
|
else
|
||||||
|
return 1 # not found
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
if ! check_drawio_installed; then
|
||||||
|
echo "Error: 'drawio' was not found. Make sure, that the draw.io cli is installed."
|
||||||
|
echo "https://www.diagrams.net/"
|
||||||
|
exit 1
|
||||||
|
else
|
||||||
|
echo "Draw.io installation found at: $DRAWIO_PATH"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Compare source (draw.io document) and exported file svg on mtime
|
||||||
|
needs_export() {
|
||||||
|
local drawio_file="$1"
|
||||||
|
local svg_file="$2"
|
||||||
|
|
||||||
|
# If no file found, export
|
||||||
|
if [[ ! -f "$svg_file" ]]; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$drawio_file" -nt "$svg_file" ]]; then
|
||||||
|
return 0
|
||||||
|
else
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# If (re-)export is necessary export with drawio cli
|
||||||
|
export_file() {
|
||||||
|
local drawio_file="$1"
|
||||||
|
local svg_file="${drawio_file%.drawio}.svg"
|
||||||
|
|
||||||
|
if needs_export "$drawio_file" "$svg_file"; then
|
||||||
|
echo "↻ Exporting: $drawio_file"
|
||||||
|
echo "$svg_file"
|
||||||
|
echo "$drawio_file"
|
||||||
|
|
||||||
|
"$DRAWIO_PATH" --export --format svg --transparent --output "$svg_file" "$drawio_file"
|
||||||
|
else
|
||||||
|
echo "✓ Up-to-date: $drawio_file"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Iterate over all .drawio files
|
||||||
|
main() {
|
||||||
|
# Source - https://stackoverflow.com/a/9612232
|
||||||
|
# Posted by Kevin, modified by community. See post 'Timeline' for change history
|
||||||
|
# Retrieved 2026-03-09, License - CC BY-SA 4.0
|
||||||
|
find . -name '*.drawio' -print0 |
|
||||||
|
while IFS= read -r -d '' line; do
|
||||||
|
export_file "$line"
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "✓ Exported all files"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Start script at entry point
|
||||||
|
main
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Check for plantuml installation
|
||||||
|
PLANTUML_PATH=""
|
||||||
|
check_plantuml_installed() {
|
||||||
|
if command -v plantuml &>/dev/null; then
|
||||||
|
PLANTUML_PATH=$(command -v plantuml)
|
||||||
|
return 0 # plantuml via PATH
|
||||||
|
else
|
||||||
|
return 1 # not found
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
if ! check_plantuml_installed; then
|
||||||
|
echo "Error: 'plantuml' was not found. Make sure, that plantuml is installed."
|
||||||
|
echo "https://plantuml.com/starting"
|
||||||
|
exit 1
|
||||||
|
else
|
||||||
|
echo "PlantUML installation found at: $PLANTUML_PATH"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Compare source (.puml document) and exported file svg on mtime
|
||||||
|
needs_export() {
|
||||||
|
local puml_file="$1"
|
||||||
|
local svg_file="$2"
|
||||||
|
|
||||||
|
# If no file found, export
|
||||||
|
if [[ ! -f "$svg_file" ]]; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$puml_file" -nt "$svg_file" ]]; then
|
||||||
|
return 0
|
||||||
|
else
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# If (re-)export is necessary export with plantuml cli
|
||||||
|
export_file() {
|
||||||
|
local puml_file="$1"
|
||||||
|
local svg_file="${puml_file%.puml}.svg"
|
||||||
|
|
||||||
|
if needs_export "$puml_file" "$svg_file"; then
|
||||||
|
echo "↻ Exporting: $puml_file"
|
||||||
|
"$PLANTUML_PATH" -tsvg "$puml_file"
|
||||||
|
else
|
||||||
|
echo "✓ Up-to-date: $puml_file"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Iterate over all .puml files
|
||||||
|
main() {
|
||||||
|
# Source - https://stackoverflow.com/a/9612232
|
||||||
|
# Posted by Kevin, modified by community. See post 'Timeline' for change history
|
||||||
|
# Retrieved 2026-03-09, License - CC BY-SA 4.0
|
||||||
|
find . -name '*.puml' -print0 |
|
||||||
|
while IFS= read -r -d '' line; do
|
||||||
|
export_file "$line"
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "✓ Exported all files"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Start script at entry point
|
||||||
|
main
|
||||||
@@ -2,9 +2,16 @@ package ch.unibas.dmi.dbis.cs108.casono;
|
|||||||
|
|
||||||
import ch.unibas.dmi.dbis.cs108.casono.client.ClientApp;
|
import ch.unibas.dmi.dbis.cs108.casono.client.ClientApp;
|
||||||
import ch.unibas.dmi.dbis.cs108.casono.server.ServerApp;
|
import ch.unibas.dmi.dbis.cs108.casono.server.ServerApp;
|
||||||
|
import org.apache.logging.log4j.LogManager;
|
||||||
|
import org.apache.logging.log4j.Logger;
|
||||||
|
|
||||||
|
/** Main entry point for Casono application. Handles client and server startup. */
|
||||||
public final class Main {
|
public final class Main {
|
||||||
|
/**
|
||||||
|
* Main entry point for Casono.
|
||||||
|
*
|
||||||
|
* @param args Command line arguments
|
||||||
|
*/
|
||||||
public static void main(String[] args) {
|
public static void main(String[] args) {
|
||||||
if (!isValid(args)) {
|
if (!isValid(args)) {
|
||||||
printUsage();
|
printUsage();
|
||||||
@@ -33,10 +40,12 @@ public final class Main {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static void printUsage() {
|
private static void printUsage() {
|
||||||
System.err.println("""
|
Logger logger = LogManager.getLogger(Main.class);
|
||||||
Usage:
|
logger.fatal(
|
||||||
java -jar xyz.jar server <listenPort>
|
"""
|
||||||
java -jar xyz.jar client <serverIp>:<serverPort>
|
Usage:
|
||||||
""");
|
java -jar xyz.jar server <listenPort>
|
||||||
|
java -jar xyz.jar client <serverIp>:<serverPort>
|
||||||
|
""");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,32 @@
|
|||||||
package ch.unibas.dmi.dbis.cs108.casono.client;
|
package ch.unibas.dmi.dbis.cs108.casono.client;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Entry point for the Casono client application. Handles client startup and connection parameters.
|
||||||
|
*/
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.client.ui.Launcher;
|
||||||
|
import org.apache.logging.log4j.LogManager;
|
||||||
|
import org.apache.logging.log4j.Logger;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Entry point for the Casono client application. Handles client startup and connection parameters.
|
||||||
|
*
|
||||||
|
* <p>Standardkonstruktor für die Anwendung.
|
||||||
|
*/
|
||||||
public class ClientApp {
|
public class ClientApp {
|
||||||
|
|
||||||
|
private static final Logger LOGGER = LogManager.getLogger(ClientApp.class);
|
||||||
|
|
||||||
|
/** Standardkonstruktor. */
|
||||||
|
public ClientApp() {
|
||||||
|
// Standardkonstruktor
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Starts the client application with the given address.
|
||||||
|
*
|
||||||
|
* @param arg Address in the format "ip:port".
|
||||||
|
* @throws IllegalArgumentException if the address format is invalid.
|
||||||
|
*/
|
||||||
public static void start(String arg) {
|
public static void start(String arg) {
|
||||||
String[] parts = arg.split(":", 2);
|
String[] parts = arg.split(":", 2);
|
||||||
if (parts.length != 2) {
|
if (parts.length != 2) {
|
||||||
@@ -9,6 +35,7 @@ public class ClientApp {
|
|||||||
String host = parts[0];
|
String host = parts[0];
|
||||||
int port = Integer.parseInt(parts[1]);
|
int port = Integer.parseInt(parts[1]);
|
||||||
|
|
||||||
System.out.println("You've selected the client. It will connect port " + port + " at host " + host);
|
LOGGER.info("You've selected the client. It will connect port {} at host {}", port, host);
|
||||||
|
Launcher.main(new String[] {});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
package ch.unibas.dmi.dbis.cs108.casono.client.ui;
|
||||||
|
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.client.ui.lobbyui.Casinomainui;
|
||||||
|
import javafx.application.Application;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Launcher for the Casono main UI.
|
||||||
|
*
|
||||||
|
* <p>Standardkonstruktor für die Anwendung.
|
||||||
|
*/
|
||||||
|
public class Launcher {
|
||||||
|
|
||||||
|
/** Standardkonstruktor. */
|
||||||
|
public Launcher() {
|
||||||
|
// Standardkonstruktor
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Main entry point for launching the UI.
|
||||||
|
*
|
||||||
|
* @param args Command line arguments
|
||||||
|
*/
|
||||||
|
public static void main(String[] args) {
|
||||||
|
Application.launch(Casinomainui.class, args);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
package ch.unibas.dmi.dbis.cs108.casono.client.ui.chatui;
|
||||||
|
|
||||||
|
import java.time.LocalTime;
|
||||||
|
import java.time.format.DateTimeFormatter;
|
||||||
|
import javafx.fxml.FXML;
|
||||||
|
import javafx.scene.control.Button;
|
||||||
|
import javafx.scene.control.Label;
|
||||||
|
import javafx.scene.control.ScrollPane;
|
||||||
|
import javafx.scene.control.TextField;
|
||||||
|
import javafx.scene.layout.VBox;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Controller-Klasse für das Chat-System innerhalb der Spieloberfläche.
|
||||||
|
*
|
||||||
|
* <p>Verwaltet das Anzeigen von Chatnachrichten, das Eingabefeld für eigene Nachrichten sowie den
|
||||||
|
* Senden-Button. Unterstützt drei Arten von Nachrichten: - Player-to-Player (Privat) - Lobby-Chat
|
||||||
|
* (Raum) - Globaler Chat (Serverweit)
|
||||||
|
*
|
||||||
|
* <p>Nachrichten werden in einem {@link VBox}-Container als {@link Label} angezeigt. Eigene
|
||||||
|
* Nachrichten werden über {@link #onSendToNetwork(String)} an das Netzwerkprotokoll weitergeleitet,
|
||||||
|
* während eingehende Nachrichten über {@link #receiveMessage(String, String)} verarbeitet und
|
||||||
|
* angezeigt werden.
|
||||||
|
*
|
||||||
|
* <p>Hinweis: Einige TODOs stehen in der zugehörigen FXML-Datei
|
||||||
|
*/
|
||||||
|
public class ChatController {
|
||||||
|
|
||||||
|
@FXML private VBox chatVBox;
|
||||||
|
|
||||||
|
@FXML private TextField inputField;
|
||||||
|
|
||||||
|
@FXML private Button sendButton;
|
||||||
|
|
||||||
|
@FXML private ScrollPane chatScrollPane;
|
||||||
|
|
||||||
|
private static final int CHAT_PADDING = 20;
|
||||||
|
|
||||||
|
/** Initialisiert den ChatController nach dem Laden der FXML. */
|
||||||
|
public void initialize() {
|
||||||
|
inputField.setOnAction(event -> sendMessage());
|
||||||
|
chatScrollPane.vvalueProperty().bind(chatVBox.heightProperty());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Diese Methode wird vom Senden-Button oder Enter ausgelöst. Sie gibt die eigene Nachricht an
|
||||||
|
* das Netzwerkprotokoll weiter.
|
||||||
|
*/
|
||||||
|
@FXML
|
||||||
|
private void sendMessage() {
|
||||||
|
String message = inputField.getText().trim();
|
||||||
|
if (!message.isEmpty()) {
|
||||||
|
inputField.clear();
|
||||||
|
|
||||||
|
// Hier wird die eigene Nachricht ans Netzwerkprotokoll übergeben
|
||||||
|
onSendToNetwork(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Diese Funktion muss vom Netzwerkprotokoll aufgerufen werden, wenn eine neue Nachricht von
|
||||||
|
* einem anderen Spieler kommt.
|
||||||
|
*
|
||||||
|
* @param player Name des Spielers
|
||||||
|
* @param message Nachricht des Spielers
|
||||||
|
*/
|
||||||
|
public void receiveMessage(String player, String message) {
|
||||||
|
String time = LocalTime.now().format(DateTimeFormatter.ofPattern("HH:mm"));
|
||||||
|
Label label = new Label("[" + time + "] " + player + ": " + message);
|
||||||
|
label.getStyleClass().add("info-text");
|
||||||
|
label.setWrapText(true); // Zeilenumbruch aktivieren
|
||||||
|
label.maxWidthProperty().bind(chatVBox.widthProperty().subtract(CHAT_PADDING));
|
||||||
|
chatVBox.getChildren().add(label);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Schnittstelle zum Netzwerkprotokoll. Diese Funktion wird automatisch aufgerufen, wenn der
|
||||||
|
* Benutzer eine eigene Nachricht sendet.
|
||||||
|
*
|
||||||
|
* @param message Nachricht, die der Benutzer abgeschickt hat
|
||||||
|
*/
|
||||||
|
public void onSendToNetwork(String message) {
|
||||||
|
// TODO: Netzwerkcode einfügen
|
||||||
|
receiveMessage("Du", message);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
package ch.unibas.dmi.dbis.cs108.casono.client.ui.gameui;
|
||||||
|
|
||||||
|
import javafx.fxml.FXML;
|
||||||
|
import javafx.scene.control.Label;
|
||||||
|
import javafx.scene.layout.VBox;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Controller für die Casino-Spielfläche.
|
||||||
|
*
|
||||||
|
* <p>Verantwortlich für: - die Darstellung des Pokertisches und der Spieleroberfläche, - die
|
||||||
|
* Verarbeitung von Benutzereingaben, - die Schnittstelle zur GameEngine und zum Netzwerkprotokoll.
|
||||||
|
*
|
||||||
|
* <p>Hinweise: - Die Methode `onTableClick()` dient aktuell nur als Test-Logik. Sie ist ggf. nicht
|
||||||
|
* mehr funktionsfähig und wird zukünftig durch die finale Spielinteraktion ersetzt.
|
||||||
|
*/
|
||||||
|
public class CasinoGameController {
|
||||||
|
|
||||||
|
@FXML private Label welcomeText;
|
||||||
|
@FXML private VBox casinoTable;
|
||||||
|
|
||||||
|
// TODO: Test-Logik: wird durch echte Spielinteraktionen ersetzt,
|
||||||
|
// sobald die GameEngine fertig ist
|
||||||
|
@FXML
|
||||||
|
public void onTableClick() {
|
||||||
|
welcomeText.setText("Einsatz akzeptiert!");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
package ch.unibas.dmi.dbis.cs108.casono.client.ui.gameui;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import javafx.application.Application;
|
||||||
|
import javafx.fxml.FXMLLoader;
|
||||||
|
import javafx.scene.Scene;
|
||||||
|
import javafx.stage.Stage;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hauptklasse für das Casino-Spiel-UI.
|
||||||
|
*
|
||||||
|
* <p>Startet die JavaFX-Anwendung, lädt die grafische Oberfläche aus der FXML-Datei und
|
||||||
|
* initialisiert die Haupt-Stage für das Spiel.
|
||||||
|
*
|
||||||
|
* <p>Aufgaben: - Lädt die FXML-Oberfläche "/ui-structure/Casinogameui.fxml". - Lädt das
|
||||||
|
* Anwendungs-Icon aus "/images/logoinverted.png". - Startet die Anwendung im Vollbildmodus.
|
||||||
|
*/
|
||||||
|
public class CasinoGameUI extends Application {
|
||||||
|
|
||||||
|
private static final int DEFAULT_WIDTH = 1200;
|
||||||
|
private static final int DEFAULT_HEIGHT = 800;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Startet die Haupt-Stage der Anwendung.
|
||||||
|
*
|
||||||
|
* @param stage Die vom System bereitgestellte Haupt-Stage.
|
||||||
|
* @throws IOException Wenn die FXML-Datei oder Ressourcen nicht geladen werden können.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void start(Stage stage) throws IOException {
|
||||||
|
FXMLLoader fxmlLoader =
|
||||||
|
new FXMLLoader(CasinoGameUI.class.getResource("/ui-structure/casinogameui.fxml"));
|
||||||
|
Scene scene = new Scene(fxmlLoader.load(), DEFAULT_WIDTH, DEFAULT_HEIGHT);
|
||||||
|
stage.setTitle("Casono (GAME)");
|
||||||
|
|
||||||
|
String iconPath = getClass().getResource("/images/logoinverted.png").toExternalForm();
|
||||||
|
stage.getIcons().add(new javafx.scene.image.Image(iconPath));
|
||||||
|
stage.setScene(scene);
|
||||||
|
stage.setFullScreen(true);
|
||||||
|
stage.show();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Startpunkt der Anwendung.
|
||||||
|
*
|
||||||
|
* @param args Befehlszeilenargumente.
|
||||||
|
*/
|
||||||
|
public static void main(String[] args) {
|
||||||
|
launch();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,542 @@
|
|||||||
|
package ch.unibas.dmi.dbis.cs108.casono.client.ui.gameui.gameuicomponents;
|
||||||
|
|
||||||
|
import java.net.CookieHandler;
|
||||||
|
import java.net.CookieManager;
|
||||||
|
import java.net.CookiePolicy;
|
||||||
|
import java.net.URI;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.Set;
|
||||||
|
import javafx.application.Platform;
|
||||||
|
import javafx.geometry.Insets;
|
||||||
|
import javafx.geometry.Pos;
|
||||||
|
import javafx.scene.Scene;
|
||||||
|
import javafx.scene.control.Alert;
|
||||||
|
import javafx.scene.control.Button;
|
||||||
|
import javafx.scene.control.ButtonType;
|
||||||
|
import javafx.scene.control.Label;
|
||||||
|
import javafx.scene.control.TextField;
|
||||||
|
import javafx.scene.image.Image;
|
||||||
|
import javafx.scene.image.ImageView;
|
||||||
|
import javafx.scene.input.KeyCode;
|
||||||
|
import javafx.scene.layout.HBox;
|
||||||
|
import javafx.scene.layout.Priority;
|
||||||
|
import javafx.scene.layout.StackPane;
|
||||||
|
import javafx.scene.layout.VBox;
|
||||||
|
import javafx.scene.shape.Rectangle;
|
||||||
|
import javafx.scene.web.WebEngine;
|
||||||
|
import javafx.scene.web.WebView;
|
||||||
|
import javafx.stage.Stage;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Experimenteller integrierter Browser für Casono.
|
||||||
|
*
|
||||||
|
* <p>Diese Klasse implementiert einen einfachen eingebetteten Webbrowser auf Basis von {@link
|
||||||
|
* javafx.scene.web.WebView}. Der Browser dient primär als Hilfswerkzeug innerhalb des Spiels, um
|
||||||
|
* externe Inhalte wie Webseiten oder Videos anzuzeigen.
|
||||||
|
*
|
||||||
|
* <p>Status Der Browser befindet sich derzeit in einer experimentellen Phase. Einige
|
||||||
|
* Sicherheitsmechanismen basieren auf experimentellen KI-gestützten Empfehlungen und können sich in
|
||||||
|
* zukünftigen Versionen noch ändern.
|
||||||
|
*
|
||||||
|
* <p>Zweck Der Browser wird aktuell experimentell genutzt, um: Pokerregeln direkt im Spiel zu
|
||||||
|
* erklären Hilfeseiten oder Dokumentationen anzuzeigen Videos (z.B. Tutorials oder Erklärungen)
|
||||||
|
* über Plattformen wie YouTube abzuspielen
|
||||||
|
*
|
||||||
|
* <p>Sicherheitsmechanismen Da externe Webseiten geladen werden können, wurden einige grundlegende
|
||||||
|
* Schutzmaßnahmen integriert: - HTTPS-Zwang für Webseiten - Whitelist für bekannte Domains -
|
||||||
|
* Warnung bei unbekannten Webseiten - JavaScript standardmäßig deaktiviert (man kann es jedoch für
|
||||||
|
* Google etc. einschalten) - Popup-Blocker - Automatische Cookie-Löschung beim Schließen
|
||||||
|
*/
|
||||||
|
public class CasinoBrowserController {
|
||||||
|
|
||||||
|
private static final Set<String> TRUSTED_DOMAINS = new HashSet<>();
|
||||||
|
|
||||||
|
private static final CookieManager COOKIE_MANAGER =
|
||||||
|
new CookieManager(null, CookiePolicy.ACCEPT_ORIGINAL_SERVER);
|
||||||
|
|
||||||
|
private static final org.apache.logging.log4j.Logger LOGGER =
|
||||||
|
org.apache.logging.log4j.LogManager.getLogger(CasinoBrowserController.class);
|
||||||
|
|
||||||
|
private static final int LOGO_HEIGHT = 40;
|
||||||
|
private static final int CORNER_RADIUS = 40;
|
||||||
|
private static final int HBOX_SPACIN = 10;
|
||||||
|
private static final int VBOX_SPACING = 10;
|
||||||
|
private static final int PADDING_SIZE = 10;
|
||||||
|
private static final int WINDOW_WIDTH = 1200;
|
||||||
|
private static final int WINDOW_HEIGHT = 800;
|
||||||
|
private static final int ALERT_WIDTH = 600;
|
||||||
|
private static final int ALERT_HEIGHT = 300;
|
||||||
|
private static final String LOGO_PATH = "/images/logoinverted.png";
|
||||||
|
private static final String LOGO_PATH_MAIN = "/images/logo.png";
|
||||||
|
|
||||||
|
static {
|
||||||
|
CookieHandler.setDefault(COOKIE_MANAGER);
|
||||||
|
|
||||||
|
TRUSTED_DOMAINS.add("wikipedia.org");
|
||||||
|
TRUSTED_DOMAINS.add("github.com");
|
||||||
|
TRUSTED_DOMAINS.add("oracle.com");
|
||||||
|
TRUSTED_DOMAINS.add("stackoverflow.com");
|
||||||
|
TRUSTED_DOMAINS.add("docs.oracle.com");
|
||||||
|
TRUSTED_DOMAINS.add("developer.mozilla.org");
|
||||||
|
TRUSTED_DOMAINS.add("maven.apache.org");
|
||||||
|
TRUSTED_DOMAINS.add("gradle.org");
|
||||||
|
TRUSTED_DOMAINS.add("spring.io");
|
||||||
|
TRUSTED_DOMAINS.add("jetbrains.com");
|
||||||
|
TRUSTED_DOMAINS.add("google.com");
|
||||||
|
TRUSTED_DOMAINS.add("bing.com");
|
||||||
|
TRUSTED_DOMAINS.add("duckduckgo.com");
|
||||||
|
TRUSTED_DOMAINS.add("metager.de");
|
||||||
|
TRUSTED_DOMAINS.add("unibas.ch");
|
||||||
|
TRUSTED_DOMAINS.add("mojeek.com");
|
||||||
|
TRUSTED_DOMAINS.add("searx.be ");
|
||||||
|
TRUSTED_DOMAINS.add("startpage.com");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean javascriptEnabled = false;
|
||||||
|
|
||||||
|
/** Löscht alle gespeicherten Cookies der aktuellen Browser-Sitzung. */
|
||||||
|
private static void clearCookies() {
|
||||||
|
try {
|
||||||
|
COOKIE_MANAGER.getCookieStore().removeAll();
|
||||||
|
} catch (Exception ignored) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Öffnet ein neues Browserfenster und lädt eine angegebene Webseite.
|
||||||
|
*
|
||||||
|
* <p>Falls die Webseite nicht zur Liste vertrauenswürdiger Domains gehört, wird der Benutzer
|
||||||
|
* gefragt, ob die Seite dennoch geladen werden soll.
|
||||||
|
*
|
||||||
|
* @param url die Startadresse der Webseite, die geladen werden soll
|
||||||
|
*/
|
||||||
|
public static void open(String url) {
|
||||||
|
Platform.runLater(
|
||||||
|
() -> {
|
||||||
|
Stage stage = new Stage();
|
||||||
|
|
||||||
|
WebView webView = new WebView();
|
||||||
|
WebEngine engine = webView.getEngine();
|
||||||
|
|
||||||
|
engine.setJavaScriptEnabled(false);
|
||||||
|
|
||||||
|
configurePopupBlocker(engine);
|
||||||
|
|
||||||
|
webView.getStyleClass().add("web-view");
|
||||||
|
|
||||||
|
StackPane webContainer = createWebContainer(webView);
|
||||||
|
|
||||||
|
Rectangle clip = new Rectangle();
|
||||||
|
clip.setArcWidth(CORNER_RADIUS);
|
||||||
|
clip.setArcHeight(CORNER_RADIUS);
|
||||||
|
webView.setClip(clip);
|
||||||
|
|
||||||
|
webView.widthProperty()
|
||||||
|
.addListener((o, a, b) -> clip.setWidth(b.doubleValue()));
|
||||||
|
webView.heightProperty()
|
||||||
|
.addListener((o, a, b) -> clip.setHeight(b.doubleValue()));
|
||||||
|
|
||||||
|
ImageView browserLogo = loadLogos(stage);
|
||||||
|
|
||||||
|
TextField urlField = createUrlField(url);
|
||||||
|
Label securityLabel = createSecurityLabel();
|
||||||
|
|
||||||
|
Button jsToggle = createJsToggle(engine);
|
||||||
|
Button backBtn = createBackButton(engine);
|
||||||
|
Button fwdBtn = createForwardButton(engine);
|
||||||
|
Button reloadBtn = createReloadButton(engine);
|
||||||
|
Button closeBtn = createCloseButton(stage, webView);
|
||||||
|
configureUrlEvents(engine, urlField, securityLabel);
|
||||||
|
|
||||||
|
HBox taskbar =
|
||||||
|
new HBox(
|
||||||
|
HBOX_SPACIN,
|
||||||
|
browserLogo,
|
||||||
|
backBtn,
|
||||||
|
fwdBtn,
|
||||||
|
reloadBtn,
|
||||||
|
urlField,
|
||||||
|
jsToggle,
|
||||||
|
securityLabel,
|
||||||
|
closeBtn);
|
||||||
|
|
||||||
|
taskbar.getStyleClass().add("taskbar-browser");
|
||||||
|
taskbar.setAlignment(Pos.CENTER_LEFT);
|
||||||
|
|
||||||
|
Scene scene = createScene(taskbar, webContainer);
|
||||||
|
|
||||||
|
configureKeyEvents(scene, engine);
|
||||||
|
|
||||||
|
stage.setScene(scene);
|
||||||
|
stage.setTitle("Casono Browser");
|
||||||
|
loadUrlSafely(engine, url, securityLabel);
|
||||||
|
stage.show();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* WebView Container
|
||||||
|
*
|
||||||
|
* @param webView WebView Inhalt
|
||||||
|
* @return StackPane Container
|
||||||
|
*/
|
||||||
|
public static StackPane createWebContainer(WebView webView) {
|
||||||
|
StackPane webContainer = new StackPane(webView);
|
||||||
|
webContainer.getStyleClass().add("browser-web-view");
|
||||||
|
VBox.setVgrow(webContainer, Priority.ALWAYS);
|
||||||
|
|
||||||
|
return webContainer;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Popup Blocker
|
||||||
|
*
|
||||||
|
* @param engine WebEngine nutzen
|
||||||
|
*/
|
||||||
|
public static void configurePopupBlocker(WebEngine engine) {
|
||||||
|
engine.setCreatePopupHandler(
|
||||||
|
config -> {
|
||||||
|
Alert alert = new Alert(Alert.AlertType.WARNING);
|
||||||
|
alert.setTitle("Popup blockiert");
|
||||||
|
alert.setHeaderText(null);
|
||||||
|
alert.setContentText("Popup wurde aus Sicherheitsgründen blockiert.");
|
||||||
|
|
||||||
|
try {
|
||||||
|
var stream = CasinoBrowserController.class.getResourceAsStream(LOGO_PATH);
|
||||||
|
Image logo = new Image(stream);
|
||||||
|
|
||||||
|
// Variable 'streamM' abgekürzt, um das 100-Zeichen-Limit (LineLength)
|
||||||
|
// einzuhalten
|
||||||
|
var streamM =
|
||||||
|
CasinoBrowserController.class.getResourceAsStream(LOGO_PATH_MAIN);
|
||||||
|
Image logomain = new Image(streamM);
|
||||||
|
|
||||||
|
if (logo != null) {
|
||||||
|
Stage alertStage = (Stage) alert.getDialogPane().getScene().getWindow();
|
||||||
|
if (alertStage != null) {
|
||||||
|
alertStage.getIcons().add(logo);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (logomain != null) {
|
||||||
|
ImageView logoView = new ImageView(logomain);
|
||||||
|
logoView.setFitHeight(LOGO_HEIGHT);
|
||||||
|
logoView.setPreserveRatio(true);
|
||||||
|
alert.setGraphic(logoView);
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
LOGGER.error("Logo konnte nicht geladen werden");
|
||||||
|
}
|
||||||
|
|
||||||
|
alert.showAndWait();
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Logos laden
|
||||||
|
*
|
||||||
|
* @param stage Fenster Stage
|
||||||
|
* @return ImageView Logo
|
||||||
|
*/
|
||||||
|
private static ImageView loadLogos(Stage stage) {
|
||||||
|
ImageView browserLogo = new ImageView();
|
||||||
|
|
||||||
|
try {
|
||||||
|
var stream = CasinoBrowserController.class.getResourceAsStream(LOGO_PATH);
|
||||||
|
Image logo = new Image(stream);
|
||||||
|
|
||||||
|
// Variable 'streamM' abgekürzt, um das 100-Zeichen-Limit (LineLength)
|
||||||
|
// einzuhalten
|
||||||
|
var streamM = CasinoBrowserController.class.getResourceAsStream(LOGO_PATH_MAIN);
|
||||||
|
Image logomain = new Image(streamM);
|
||||||
|
|
||||||
|
if (logo != null) {
|
||||||
|
stage.getIcons().add(logo);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (logomain != null) {
|
||||||
|
browserLogo.setImage(logomain);
|
||||||
|
browserLogo.setFitHeight(LOGO_HEIGHT);
|
||||||
|
browserLogo.setPreserveRatio(true);
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
LOGGER.error("Logo konnte nicht geladen werden");
|
||||||
|
}
|
||||||
|
|
||||||
|
return browserLogo;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* URL Feld
|
||||||
|
*
|
||||||
|
* @param url Start URL
|
||||||
|
* @return TextField Eingabe
|
||||||
|
*/
|
||||||
|
public static TextField createUrlField(String url) {
|
||||||
|
TextField urlField = new TextField(url);
|
||||||
|
urlField.getStyleClass().add("gray-input-field");
|
||||||
|
HBox.setHgrow(urlField, Priority.ALWAYS);
|
||||||
|
|
||||||
|
return urlField;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sicherheits Label
|
||||||
|
*
|
||||||
|
* @return Label Anzeige
|
||||||
|
*/
|
||||||
|
public static Label createSecurityLabel() {
|
||||||
|
Label securityLabel = new Label("SICHER");
|
||||||
|
securityLabel.getStyleClass().add("security-label");
|
||||||
|
|
||||||
|
return securityLabel;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JS Toggle
|
||||||
|
*
|
||||||
|
* @param engine WebEngine nutzen
|
||||||
|
* @return Button Umschalten
|
||||||
|
*/
|
||||||
|
public static Button createJsToggle(WebEngine engine) {
|
||||||
|
Button jsToggle = new Button("JS EINSCHALTEN");
|
||||||
|
jsToggle.getStyleClass().add("red-button");
|
||||||
|
|
||||||
|
jsToggle.setOnAction(
|
||||||
|
e -> {
|
||||||
|
javascriptEnabled = !javascriptEnabled;
|
||||||
|
engine.setJavaScriptEnabled(javascriptEnabled);
|
||||||
|
|
||||||
|
if (javascriptEnabled) {
|
||||||
|
jsToggle.setText("JS AUSSCHALTEN");
|
||||||
|
jsToggle.getStyleClass().removeAll("red-button");
|
||||||
|
jsToggle.getStyleClass().add("yellow-button");
|
||||||
|
|
||||||
|
} else {
|
||||||
|
jsToggle.setText("JS EINSCHALTEN");
|
||||||
|
jsToggle.getStyleClass().removeAll("yellow-button");
|
||||||
|
jsToggle.getStyleClass().add("red-button");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return jsToggle;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Zurück Button
|
||||||
|
*
|
||||||
|
* @param engine WebEngine nutzen
|
||||||
|
* @return Button Zurück
|
||||||
|
*/
|
||||||
|
public static Button createBackButton(WebEngine engine) {
|
||||||
|
Button backBtn = new Button("<");
|
||||||
|
backBtn.getStyleClass().add("gray-button");
|
||||||
|
backBtn.setOnAction(
|
||||||
|
e -> {
|
||||||
|
if (engine.getHistory().getCurrentIndex() > 0) {
|
||||||
|
engine.getHistory().go(-1);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return backBtn;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vorwärts Button
|
||||||
|
*
|
||||||
|
* @param engine WebEngine nutzen
|
||||||
|
* @return Button Vorwärts
|
||||||
|
*/
|
||||||
|
public static Button createForwardButton(WebEngine engine) {
|
||||||
|
Button fwdBtn = new Button(">");
|
||||||
|
fwdBtn.getStyleClass().add("gray-button");
|
||||||
|
fwdBtn.setOnAction(
|
||||||
|
e -> {
|
||||||
|
if (engine.getHistory().getCurrentIndex()
|
||||||
|
< engine.getHistory().getEntries().size() - 1) {
|
||||||
|
engine.getHistory().go(1);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return fwdBtn;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reload Button
|
||||||
|
*
|
||||||
|
* @param engine WebEngine nutzen
|
||||||
|
* @return Button Reload
|
||||||
|
*/
|
||||||
|
public static Button createReloadButton(WebEngine engine) {
|
||||||
|
Button reloadBtn = new Button("⟳");
|
||||||
|
reloadBtn.getStyleClass().add("gray-button");
|
||||||
|
reloadBtn.setOnAction(e -> engine.reload());
|
||||||
|
return reloadBtn;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Close Button
|
||||||
|
*
|
||||||
|
* @param stage Fenster Stage
|
||||||
|
* @param webView WebView Inhalt
|
||||||
|
* @return Button Schließen
|
||||||
|
*/
|
||||||
|
public static Button createCloseButton(Stage stage, WebView webView) {
|
||||||
|
Button closeBtn = new Button("X");
|
||||||
|
closeBtn.getStyleClass().add("red-button");
|
||||||
|
closeBtn.setOnAction(
|
||||||
|
e -> {
|
||||||
|
webView.getEngine().load("about:blank");
|
||||||
|
clearCookies();
|
||||||
|
stage.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
return closeBtn;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* URL Events
|
||||||
|
*
|
||||||
|
* @param engine WebEngine nutzen
|
||||||
|
* @param urlField URL Textfeld
|
||||||
|
* @param securityLabel Sicherheits Label
|
||||||
|
*/
|
||||||
|
public static void configureUrlEvents(
|
||||||
|
WebEngine engine, TextField urlField, Label securityLabel) {
|
||||||
|
urlField.setOnAction(e -> loadUrlSafely(engine, urlField.getText(), securityLabel));
|
||||||
|
engine.locationProperty().addListener((obs, o, n) -> urlField.setText(n));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scene erstellen
|
||||||
|
*
|
||||||
|
* @param taskbar Taskbar HBox
|
||||||
|
* @param webContainer Web Container
|
||||||
|
* @return Scene Fenster
|
||||||
|
*/
|
||||||
|
public static Scene createScene(HBox taskbar, StackPane webContainer) {
|
||||||
|
VBox root = new VBox(VBOX_SPACING, taskbar, webContainer);
|
||||||
|
root.getStyleClass().add("browser-root");
|
||||||
|
root.setPadding(new Insets(PADDING_SIZE));
|
||||||
|
|
||||||
|
Scene scene = new Scene(root, WINDOW_WIDTH, WINDOW_HEIGHT);
|
||||||
|
|
||||||
|
var css = CasinoBrowserController.class.getResource("/ui-structure/casinogameui.css");
|
||||||
|
|
||||||
|
if (css != null) {
|
||||||
|
scene.getStylesheets().add(css.toExternalForm());
|
||||||
|
}
|
||||||
|
|
||||||
|
return scene;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Key Events
|
||||||
|
*
|
||||||
|
* @param scene Scene Fenster
|
||||||
|
* @param engine WebEngine nutzen
|
||||||
|
*/
|
||||||
|
public static void configureKeyEvents(Scene scene, WebEngine engine) {
|
||||||
|
scene.setOnKeyPressed(
|
||||||
|
event -> {
|
||||||
|
if (event.getCode() == KeyCode.F5) {
|
||||||
|
engine.reload();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lädt eine URL in den Browser, nachdem grundlegende Sicherheitsprüfungen durchgeführt wurden.
|
||||||
|
*
|
||||||
|
* <p>Vor dem Laden einer Seite werden folgende Prüfungen durchgeführt: - Überprüfung des
|
||||||
|
* Protokolls (nur HTTPS erlaubt) - Überprüfung der Domain gegen eine Whitelist - Schutz vor
|
||||||
|
* Domain-Spoofing (Domain-Vortäuschung)
|
||||||
|
*
|
||||||
|
* <p>Falls eine Domain nicht als vertrauenswürdig eingestuft wird, muss der Benutzer
|
||||||
|
* bestätigen, dass die Seite dennoch geöffnet werden darf.
|
||||||
|
*
|
||||||
|
* @param engine der WebEngine-Renderer des Browsers
|
||||||
|
* @param url die zu ladende Webadresse
|
||||||
|
* @param securityLabel Label zur Anzeige des aktuellen Sicherheitsstatus
|
||||||
|
*/
|
||||||
|
private static void loadUrlSafely(WebEngine engine, String url, Label securityLabel) {
|
||||||
|
try {
|
||||||
|
if (!url.startsWith("http")) {
|
||||||
|
url = "https://" + url;
|
||||||
|
}
|
||||||
|
|
||||||
|
URI uri = new URI(url);
|
||||||
|
|
||||||
|
// HTTPS Pflicht
|
||||||
|
if (!"https".equalsIgnoreCase(uri.getScheme())) {
|
||||||
|
securityLabel.setText("BLOCKIERT");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
String host = uri.getHost();
|
||||||
|
if (host == null) {
|
||||||
|
securityLabel.setText("ERROR");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sicherer Domain Check
|
||||||
|
boolean trusted =
|
||||||
|
TRUSTED_DOMAINS.stream()
|
||||||
|
.anyMatch(domain -> host.equals(domain) || host.endsWith("." + domain));
|
||||||
|
|
||||||
|
if (!trusted) {
|
||||||
|
if (!showUnknownWebsiteAlert(host)) {
|
||||||
|
securityLabel.setText("BLOCKIERT");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
securityLabel.setText("UNBEKANNT");
|
||||||
|
} else {
|
||||||
|
securityLabel.setText("SICHER");
|
||||||
|
}
|
||||||
|
engine.load(uri.toString());
|
||||||
|
} catch (Exception e) {
|
||||||
|
securityLabel.setText("ERROR");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Zeigt Warnung an.
|
||||||
|
*
|
||||||
|
* @param host Website Host
|
||||||
|
* @return OK gedrückt
|
||||||
|
*/
|
||||||
|
private static boolean showUnknownWebsiteAlert(String host) {
|
||||||
|
Alert alert = new Alert(Alert.AlertType.CONFIRMATION);
|
||||||
|
|
||||||
|
alert.setTitle("Unbekannte Website");
|
||||||
|
alert.setHeaderText("Diese Website ist nicht bekannt");
|
||||||
|
alert.setContentText(
|
||||||
|
host
|
||||||
|
+ "\n\nDiese Seite ist nicht vom "
|
||||||
|
+ "Casono Browser verifiziert.\nMöchten Sie sie trotzdem öffnen?");
|
||||||
|
|
||||||
|
var stream = CasinoBrowserController.class.getResourceAsStream(LOGO_PATH);
|
||||||
|
Image logo = new Image(stream);
|
||||||
|
if (logo != null) {
|
||||||
|
Stage stage = (Stage) alert.getDialogPane().getScene().getWindow();
|
||||||
|
if (stage != null) {
|
||||||
|
stage.getIcons().add(logo);
|
||||||
|
}
|
||||||
|
ImageView logomain = new ImageView(logo);
|
||||||
|
logomain.setFitHeight(LOGO_HEIGHT);
|
||||||
|
logomain.setPreserveRatio(true);
|
||||||
|
alert.setGraphic(logomain);
|
||||||
|
}
|
||||||
|
|
||||||
|
alert.getDialogPane().setPrefSize(ALERT_WIDTH, ALERT_HEIGHT);
|
||||||
|
Optional<ButtonType> result = alert.showAndWait();
|
||||||
|
|
||||||
|
if (result.isEmpty() || result.get() != ButtonType.OK) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,138 @@
|
|||||||
|
package ch.unibas.dmi.dbis.cs108.casono.client.ui.gameui.gameuicomponents;
|
||||||
|
|
||||||
|
import javafx.application.Platform;
|
||||||
|
import javafx.fxml.FXML;
|
||||||
|
import javafx.scene.control.TextField;
|
||||||
|
import javafx.scene.input.KeyCode;
|
||||||
|
import javafx.scene.input.KeyEvent;
|
||||||
|
import javafx.scene.input.MouseEvent;
|
||||||
|
import javafx.scene.layout.HBox;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Controller für die interaktive Taskleiste innerhalb der Poker-UI.
|
||||||
|
*
|
||||||
|
* <p>Verantwortlich für: - Drag-and-Drop-Verschieben der Taskleiste, - Eingabe und Verwaltung von
|
||||||
|
* Spieleinsätzen, - Steuerung allgemeiner Menüfunktionen wie Exit.
|
||||||
|
*/
|
||||||
|
public class TaskbarController {
|
||||||
|
|
||||||
|
private static final org.apache.logging.log4j.Logger LOGGER =
|
||||||
|
org.apache.logging.log4j.LogManager.getLogger(CasinoBrowserController.class);
|
||||||
|
|
||||||
|
@FXML private HBox taskbar;
|
||||||
|
@FXML private TextField taskbarInput;
|
||||||
|
|
||||||
|
private double xOffset = 0;
|
||||||
|
private double yOffset = 0;
|
||||||
|
private static final double TASKBAR_SCALE = 0.9;
|
||||||
|
private static final int MIN_CREDITS = 5;
|
||||||
|
private static final int MAX_CREDITS = 100000;
|
||||||
|
private static final int CREDIT_STEP = 5;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wird aufgerufen, wenn die Taskleiste mit der Maus gedrückt wird. Speichert die relative
|
||||||
|
* Position, um später korrekt zu verschieben.
|
||||||
|
*
|
||||||
|
* @param event Das Mausereignis
|
||||||
|
*/
|
||||||
|
@FXML
|
||||||
|
private void onTaskbarPressed(MouseEvent event) {
|
||||||
|
xOffset = event.getSceneX() - taskbar.getLayoutX();
|
||||||
|
yOffset = event.getSceneY() - taskbar.getLayoutY();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wird aufgerufen, während die Taskleiste mit der Maus gezogen wird. Aktualisiert die Position
|
||||||
|
* und skaliert die Taskleiste leicht zur visuellen Rückmeldung.
|
||||||
|
*
|
||||||
|
* <p>TODO: Es muss noch gefixt werden, dass die Taskleiste nicht aus dem Fenster verschwinden
|
||||||
|
* kann.
|
||||||
|
*
|
||||||
|
* @param event Das Mausereignis
|
||||||
|
*/
|
||||||
|
@FXML
|
||||||
|
private void onTaskbarDragged(MouseEvent event) {
|
||||||
|
taskbar.setLayoutX(event.getSceneX() - xOffset);
|
||||||
|
taskbar.setLayoutY(event.getSceneY() - yOffset);
|
||||||
|
|
||||||
|
taskbar.setScaleX(TASKBAR_SCALE);
|
||||||
|
taskbar.setScaleY(TASKBAR_SCALE);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wird aufgerufen, wenn die Maus über der Taskleiste losgelassen wird. Setzt die Skalierung der
|
||||||
|
* Taskleiste wieder auf Normalgröße.
|
||||||
|
*
|
||||||
|
* @param event Das Mausereignis
|
||||||
|
*/
|
||||||
|
@FXML
|
||||||
|
private void onTaskbarReleased(MouseEvent event) {
|
||||||
|
taskbar.setScaleX(1.0);
|
||||||
|
taskbar.setScaleY(1.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wird aufgerufen, wenn im Textfeld die Enter-Taste gedrückt wird.
|
||||||
|
*
|
||||||
|
* @param event Das Tastaturereignis
|
||||||
|
*/
|
||||||
|
@FXML
|
||||||
|
private void onInputSubmitted(KeyEvent event) {
|
||||||
|
if (event.getCode() == KeyCode.ENTER) {
|
||||||
|
processBet();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wird aufgerufen, wenn der Submit-Button in der Taskleiste gedrückt wird. Löst die
|
||||||
|
* Verarbeitung des Einsatzes aus.
|
||||||
|
*/
|
||||||
|
@FXML
|
||||||
|
private void onInputSubmittedAction() {
|
||||||
|
processBet();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wird aufgerufen, wenn der Exit-Button in der Taskleiste gedrückt wird.
|
||||||
|
*
|
||||||
|
* <p>TODO: Logik implementieren, um zur Lobby zurückzukehren, ohne die gesamte Anwendung zu
|
||||||
|
* schließen (kein System.exit/Platform.exit).
|
||||||
|
*/
|
||||||
|
@FXML
|
||||||
|
private void onExitButtonClick() {
|
||||||
|
Platform.exit();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verarbeitet den im Textfeld eingegebenen Einsatz. Es werden ausschließlich ganzzahlige Werte
|
||||||
|
* im Bereich von 5 bis 100.000 Credits akzeptiert, die einem Vielfachen von 5 entsprechen
|
||||||
|
* (5er-Schritte). Der Einsatz wird aktuell nur auf der Konsole ausgegeben.
|
||||||
|
*/
|
||||||
|
private void processBet() {
|
||||||
|
String input = taskbarInput.getText();
|
||||||
|
try {
|
||||||
|
int credits = Integer.parseInt(input.trim());
|
||||||
|
|
||||||
|
if (credits >= MIN_CREDITS && credits <= MAX_CREDITS && credits % CREDIT_STEP == 0) {
|
||||||
|
// TODO: Credits müssen an die GameEngine gesendet werden
|
||||||
|
LOGGER.info("Einsatz gesetzt: {} Casono Credits", credits);
|
||||||
|
taskbarInput.clear();
|
||||||
|
} else {
|
||||||
|
LOGGER.info("Fehler: Nur 5er-Schritte (5, 10, ... 100.000) erlaubt!");
|
||||||
|
}
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
LOGGER.info("Fehler: Bitte nur eine Zahl eingeben!");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Öffnet den integrierten Casono Webbrowser.
|
||||||
|
*
|
||||||
|
* <p>TODO: Ersetze die Start-URL durch die offizielle Projekt-Website (z.B. Tipps & Tricks
|
||||||
|
* Seite), sobald die Inhalte für Strategien und Support bereitstehen.
|
||||||
|
*/
|
||||||
|
@FXML
|
||||||
|
private void onBrowserButtonClick() {
|
||||||
|
CasinoBrowserController.open("wikipedia.org");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
package ch.unibas.dmi.dbis.cs108.casono.client.ui.lobbyui;
|
||||||
|
|
||||||
|
/** Main UI application for Casono. Loads the main FXML layout and sets up the stage. */
|
||||||
|
import java.io.IOException;
|
||||||
|
import javafx.application.Application;
|
||||||
|
import javafx.fxml.FXMLLoader;
|
||||||
|
import javafx.scene.Scene;
|
||||||
|
import javafx.stage.Stage;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JavaFX Application class for the Casono main UI.
|
||||||
|
*
|
||||||
|
* <p>Standardkonstruktor für die Anwendung.
|
||||||
|
*/
|
||||||
|
public class Casinomainui extends Application {
|
||||||
|
|
||||||
|
/** Standardkonstruktor. */
|
||||||
|
public Casinomainui() {
|
||||||
|
// Standardkonstruktor
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final int SCENE_WIDTH = 1200;
|
||||||
|
private static final int SCENE_HEIGHT = 800;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
/**
|
||||||
|
* Starts the JavaFX application and loads the main UI.
|
||||||
|
*
|
||||||
|
* @param stage The primary stage for this application.
|
||||||
|
* @throws IOException If loading the FXML fails.
|
||||||
|
*/
|
||||||
|
public void start(Stage stage) throws IOException {
|
||||||
|
FXMLLoader fxmlLoader =
|
||||||
|
new FXMLLoader(getClass().getResource("/ui-structure/Casinomainui.fxml"));
|
||||||
|
Scene scene = new Scene(fxmlLoader.load(), SCENE_WIDTH, SCENE_HEIGHT);
|
||||||
|
stage.setTitle("Casono");
|
||||||
|
javafx.scene.image.Image icon =
|
||||||
|
new javafx.scene.image.Image(
|
||||||
|
getClass().getResource("/images/logoinverted.png").toExternalForm());
|
||||||
|
stage.getIcons().add(icon);
|
||||||
|
stage.setScene(scene);
|
||||||
|
stage.setFullScreen(true);
|
||||||
|
stage.show();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Main entry point for launching the application.
|
||||||
|
*
|
||||||
|
* @param args Command line arguments
|
||||||
|
*/
|
||||||
|
public static void main(String[] args) {
|
||||||
|
launch();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
package ch.unibas.dmi.dbis.cs108.casono.client.ui.lobbyui;
|
||||||
|
|
||||||
|
import javafx.application.Platform;
|
||||||
|
import javafx.fxml.FXML;
|
||||||
|
import javafx.scene.control.Button;
|
||||||
|
import javafx.scene.control.Label;
|
||||||
|
import javafx.scene.image.Image;
|
||||||
|
import javafx.scene.image.ImageView;
|
||||||
|
import javafx.scene.layout.AnchorPane;
|
||||||
|
import javafx.scene.layout.VBox;
|
||||||
|
import javafx.scene.shape.Rectangle;
|
||||||
|
import org.apache.logging.log4j.LogManager;
|
||||||
|
import org.apache.logging.log4j.Logger;
|
||||||
|
|
||||||
|
/** Controller for the Casono main UI lobby. Handles UI initialization and user actions. */
|
||||||
|
public class CasinomainuiController {
|
||||||
|
private static final Logger LOGGER = LogManager.getLogger(CasinomainuiController.class);
|
||||||
|
|
||||||
|
@FXML private AnchorPane rootPane;
|
||||||
|
@FXML private Label titleLabel;
|
||||||
|
@FXML private Label subtitleLabel;
|
||||||
|
@FXML private ImageView logoView;
|
||||||
|
@FXML private Rectangle greenBox;
|
||||||
|
@FXML private Button exitbutton;
|
||||||
|
@FXML private VBox casinoTable;
|
||||||
|
|
||||||
|
private LobbyButtonTranslationManager translationManager;
|
||||||
|
private LobbyButtonGridManager gridManager;
|
||||||
|
private int nextButtonId = 1;
|
||||||
|
|
||||||
|
public CasinomainuiController() {
|
||||||
|
// Default constructor
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Initializes the UI components and sets default values. */
|
||||||
|
@FXML
|
||||||
|
public void initialize() {
|
||||||
|
titleLabel.setText("Casono");
|
||||||
|
subtitleLabel.setText("Texas Hold'em Poker");
|
||||||
|
logoView.setImage(new Image(getClass().getResource("/images/logo.png").toExternalForm()));
|
||||||
|
|
||||||
|
translationManager = new LobbyButtonTranslationManager();
|
||||||
|
gridManager =
|
||||||
|
new LobbyButtonGridManager(new javafx.scene.layout.GridPane(), translationManager);
|
||||||
|
casinoTable.getChildren().clear();
|
||||||
|
casinoTable.getChildren().add(gridManager.getGridPane());
|
||||||
|
gridManager.renderLobbyButtons();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Handles the exit button action to close the application. */
|
||||||
|
@FXML
|
||||||
|
public void handleexitbutton() {
|
||||||
|
Platform.exit();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Handles creation of a new lobby button. */
|
||||||
|
@FXML
|
||||||
|
public void handleCreateLobbyButton() {
|
||||||
|
if (translationManager.isFull()) {
|
||||||
|
LOGGER.warn("Grid voll! Keine weiteren Lobbys moeglich.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
int buttonId = nextButtonId++;
|
||||||
|
int lobbyId = gridManager.createLobby();
|
||||||
|
try {
|
||||||
|
translationManager.addLobbyButton(buttonId, lobbyId);
|
||||||
|
LOGGER.info("ButtonID: {}, LobbyID: {}", buttonId, lobbyId);
|
||||||
|
gridManager.renderLobbyButtons();
|
||||||
|
} catch (Exception e) {
|
||||||
|
LOGGER.error("Fehler beim Hinzufügen: {}", e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,114 @@
|
|||||||
|
package ch.unibas.dmi.dbis.cs108.casono.client.ui.lobbyui;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Manages the grid for lobby buttons and rendering. Uses LobbyButtonTranslationManager for mapping
|
||||||
|
* ButtonID to LobbyID.
|
||||||
|
*/
|
||||||
|
import java.util.Map;
|
||||||
|
import javafx.scene.control.Button;
|
||||||
|
import javafx.scene.image.Image;
|
||||||
|
import javafx.scene.image.ImageView;
|
||||||
|
import javafx.scene.layout.GridPane;
|
||||||
|
import org.apache.logging.log4j.LogManager;
|
||||||
|
import org.apache.logging.log4j.Logger;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Manages the grid for lobby buttons and rendering. Uses LobbyButtonTranslationManager for mapping
|
||||||
|
* ButtonID to LobbyID.
|
||||||
|
*/
|
||||||
|
public class LobbyButtonGridManager {
|
||||||
|
private static final Logger LOGGER = LogManager.getLogger(LobbyButtonGridManager.class);
|
||||||
|
|
||||||
|
/** GridPane for the button grid. */
|
||||||
|
private final GridPane gridPane;
|
||||||
|
|
||||||
|
/** Manager for mapping ButtonID to LobbyID. */
|
||||||
|
private final LobbyButtonTranslationManager translationManager;
|
||||||
|
|
||||||
|
/** Number of rows in the grid. */
|
||||||
|
private static final int ROWS = 2;
|
||||||
|
|
||||||
|
/** Number of columns in the grid. */
|
||||||
|
private static final int COLS = 4;
|
||||||
|
|
||||||
|
/** Path to the button image. */
|
||||||
|
private static final String BUTTON_IMAGE_PATH = "/images/logo.png";
|
||||||
|
|
||||||
|
/** Max random lobby id. */
|
||||||
|
private static final int MAX_RANDOM_LOBBY_ID = 10000;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor for the GridManager.
|
||||||
|
*
|
||||||
|
* @param gridPane the GridPane for rendering
|
||||||
|
* @param translationManager the manager for mapping ButtonID to LobbyID
|
||||||
|
*/
|
||||||
|
public LobbyButtonGridManager(
|
||||||
|
GridPane gridPane, LobbyButtonTranslationManager translationManager) {
|
||||||
|
this.gridPane = gridPane;
|
||||||
|
this.translationManager = translationManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders all lobby buttons in the grid. Creates a button for each mapping with image and event
|
||||||
|
* handler.
|
||||||
|
*/
|
||||||
|
public void renderLobbyButtons() {
|
||||||
|
gridPane.getChildren().clear();
|
||||||
|
int index = 0;
|
||||||
|
Map<Integer, Integer> mapping = translationManager.getButtonIdToLobbyId();
|
||||||
|
if (mapping.isEmpty()) {
|
||||||
|
// No buttons to render
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (Map.Entry<Integer, Integer> entry : mapping.entrySet()) {
|
||||||
|
int buttonId = entry.getKey();
|
||||||
|
Button btn = new Button();
|
||||||
|
btn.setId("lobbyBtn-" + buttonId);
|
||||||
|
btn.setGraphic(
|
||||||
|
new ImageView(new Image(getClass().getResourceAsStream(BUTTON_IMAGE_PATH))));
|
||||||
|
btn.setOnAction(
|
||||||
|
e -> {
|
||||||
|
Integer lobbyId = translationManager.getLobbyIdForButton(buttonId);
|
||||||
|
if (lobbyId != null) {
|
||||||
|
joinLobby(lobbyId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
int row = index / COLS;
|
||||||
|
int col = index % COLS;
|
||||||
|
gridPane.add(btn, col, row);
|
||||||
|
index++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Placeholder for lobby creation logic. Returns a generated lobbyId.
|
||||||
|
*
|
||||||
|
* @return The generated lobbyId
|
||||||
|
*/
|
||||||
|
public int createLobby() {
|
||||||
|
// TODO: Replace with actual lobby creation logic
|
||||||
|
int lobbyId = (int) (Math.random() * MAX_RANDOM_LOBBY_ID + 1);
|
||||||
|
LOGGER.info("Lobby created: {}", lobbyId);
|
||||||
|
return lobbyId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Placeholder for joining a lobby.
|
||||||
|
*
|
||||||
|
* @param lobbyId The lobbyId to join
|
||||||
|
*/
|
||||||
|
public void joinLobby(int lobbyId) {
|
||||||
|
// TODO: Replace with actual join logic
|
||||||
|
LOGGER.info("Joining lobby: {}", lobbyId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Getter for the GridPane.
|
||||||
|
*
|
||||||
|
* @return The GridPane for the button grid
|
||||||
|
*/
|
||||||
|
public javafx.scene.layout.GridPane getGridPane() {
|
||||||
|
return gridPane;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
package ch.unibas.dmi.dbis.cs108.casono.client.ui.lobbyui;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verwaltet das Mapping zwischen Button-IDs und Lobby-IDs rein im Speicher. Keine Dateioperationen,
|
||||||
|
* nur Laufzeitdatenstruktur.
|
||||||
|
*/
|
||||||
|
public class LobbyButtonTranslationManager {
|
||||||
|
/** Maximale Anzahl an Buttons/Lobbys */
|
||||||
|
private static final int MAX_BUTTONS = 8;
|
||||||
|
|
||||||
|
/** Zuordnung ButtonID → LobbyID */
|
||||||
|
private final Map<Integer, Integer> buttonIdToLobbyId = new HashMap<>();
|
||||||
|
|
||||||
|
/** Konstruktor: initialisiert die Zuordnung leer. */
|
||||||
|
public LobbyButtonTranslationManager() {
|
||||||
|
// Zuordnung bleibt leer beim Start
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prüft, ob das Grid voll ist (MAX_BUTTONS erreicht).
|
||||||
|
*
|
||||||
|
* @return true, wenn Grid voll; sonst false
|
||||||
|
*/
|
||||||
|
public boolean isFull() {
|
||||||
|
return buttonIdToLobbyId.size() >= MAX_BUTTONS;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fügt eine Zuordnung ButtonID → LobbyID hinzu.
|
||||||
|
*
|
||||||
|
* @param buttonId Die ID des Buttons
|
||||||
|
* @param lobbyId Die ID der Lobby
|
||||||
|
* @throws Exception wenn das Grid voll ist
|
||||||
|
*/
|
||||||
|
public void addLobbyButton(int buttonId, int lobbyId) throws Exception {
|
||||||
|
if (isFull()) {
|
||||||
|
throw new Exception("Grid is full!");
|
||||||
|
}
|
||||||
|
buttonIdToLobbyId.put(buttonId, lobbyId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Entfernt eine Zuordnung für die gegebene ButtonID.
|
||||||
|
*
|
||||||
|
* @param buttonId Die ID des zu entfernenden Buttons
|
||||||
|
*/
|
||||||
|
public void removeLobbyButton(int buttonId) {
|
||||||
|
buttonIdToLobbyId.remove(buttonId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gibt die LobbyID für eine gegebene ButtonID zurück.
|
||||||
|
*
|
||||||
|
* @param buttonId Die ButtonID
|
||||||
|
* @return Die zugehoerige LobbyID oder null, falls nicht vorhanden
|
||||||
|
*/
|
||||||
|
public Integer getLobbyIdForButton(int buttonId) {
|
||||||
|
return buttonIdToLobbyId.get(buttonId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gibt die gesamte Zuordnung ButtonID → LobbyID zurück.
|
||||||
|
*
|
||||||
|
* @return Map aller Zuordnungen
|
||||||
|
*/
|
||||||
|
public Map<Integer, Integer> getButtonIdToLobbyId() {
|
||||||
|
return buttonIdToLobbyId;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,8 +1,47 @@
|
|||||||
package ch.unibas.dmi.dbis.cs108.casono.server;
|
package ch.unibas.dmi.dbis.cs108.casono.server;
|
||||||
|
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.domain.user.UserCleanupJob;
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.domain.user.UserRegistry;
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.network.NetworkManager;
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.network.events.DisconnectEvent;
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.network.events.EventBus;
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.network.sessions.SessionManager;
|
||||||
|
import java.time.Duration;
|
||||||
|
import java.util.concurrent.Executors;
|
||||||
|
import java.util.concurrent.ScheduledExecutorService;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import org.apache.logging.log4j.LogManager;
|
||||||
|
import org.apache.logging.log4j.Logger;
|
||||||
|
|
||||||
|
/** Application class for starting the server. */
|
||||||
public class ServerApp {
|
public class ServerApp {
|
||||||
|
public static final int USER_CLEANUP_JOB_DELAY = 0;
|
||||||
|
public static final int USER_CLEANUP_JOB_PERIOD = 10;
|
||||||
|
public static final int USER_CLEANUP_JOB_RECONNECT_THRESHOLD = 10;
|
||||||
|
|
||||||
public static void start(String arg) {
|
public static void start(String arg) {
|
||||||
int port = Integer.parseInt(arg);
|
int port = Integer.parseInt(arg);
|
||||||
System.out.println("You've selected the server. It will accept connections at port " + port);
|
|
||||||
|
Logger logger = LogManager.getLogger(ServerApp.class);
|
||||||
|
logger.info("Starting server at port {}", port);
|
||||||
|
|
||||||
|
EventBus eventBus = new EventBus();
|
||||||
|
SessionManager sessionManager = new SessionManager();
|
||||||
|
eventBus.subscribe(
|
||||||
|
DisconnectEvent.class, event -> sessionManager.removeSession(event.sessionId()));
|
||||||
|
NetworkManager networkManager = new NetworkManager(port, sessionManager, eventBus);
|
||||||
|
|
||||||
|
UserRegistry userRegistry = new UserRegistry();
|
||||||
|
eventBus.subscribe(
|
||||||
|
DisconnectEvent.class, event -> userRegistry.onDisconnect(event.sessionId()));
|
||||||
|
ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
|
||||||
|
scheduler.scheduleAtFixedRate(
|
||||||
|
new UserCleanupJob(
|
||||||
|
userRegistry, Duration.ofSeconds(USER_CLEANUP_JOB_RECONNECT_THRESHOLD)),
|
||||||
|
USER_CLEANUP_JOB_DELAY,
|
||||||
|
USER_CLEANUP_JOB_PERIOD,
|
||||||
|
TimeUnit.SECONDS);
|
||||||
|
|
||||||
|
networkManager.start();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
package ch.unibas.dmi.dbis.cs108.casono.server.domain.message;
|
||||||
|
|
||||||
|
/** Represents a message sent by someone */
|
||||||
|
public record Message(String sender, String content) {}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
package ch.unibas.dmi.dbis.cs108.casono.server.domain.message;
|
||||||
|
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.domain.user.UserRegistry;
|
||||||
|
|
||||||
|
public class MessageManager {
|
||||||
|
private final UserRegistry userRegistry;
|
||||||
|
|
||||||
|
public MessageManager(UserRegistry userRegistry) {
|
||||||
|
this.userRegistry = userRegistry;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void broadcast(Message message) {
|
||||||
|
userRegistry.getAllUsers().forEach(user -> user.enqueueMessage(message));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
package ch.unibas.dmi.dbis.cs108.casono.server.domain.user;
|
||||||
|
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.domain.message.Message;
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.network.sessions.SessionId;
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.ArrayDeque;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.Queue;
|
||||||
|
import java.util.concurrent.ConcurrentLinkedQueue;
|
||||||
|
|
||||||
|
/** Represents an authenticated user on the server. */
|
||||||
|
public class User {
|
||||||
|
private final UserId id;
|
||||||
|
private final String name;
|
||||||
|
private SessionId sessionId;
|
||||||
|
private Instant disconnectedAt;
|
||||||
|
private final Queue<Message> messages;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new User with the given ID, name and session.
|
||||||
|
*
|
||||||
|
* @param id the unique identifier for this user
|
||||||
|
* @param name the display name of this user
|
||||||
|
* @param sessionId the session currently associated with this user
|
||||||
|
*/
|
||||||
|
public User(UserId id, String name, SessionId sessionId) {
|
||||||
|
this.id = id;
|
||||||
|
this.name = name;
|
||||||
|
this.sessionId = sessionId;
|
||||||
|
this.disconnectedAt = null;
|
||||||
|
this.messages = new ConcurrentLinkedQueue<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the ID of this user.
|
||||||
|
*
|
||||||
|
* @return the user ID
|
||||||
|
*/
|
||||||
|
public UserId getId() {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the display name of this user.
|
||||||
|
*
|
||||||
|
* @return the user name
|
||||||
|
*/
|
||||||
|
public String getName() {
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the session currently associated with this user, if any.
|
||||||
|
*
|
||||||
|
* @return an Optional containing the session ID, or empty if disconnected
|
||||||
|
*/
|
||||||
|
public Optional<SessionId> getSessionId() {
|
||||||
|
return Optional.ofNullable(sessionId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the time at which this user disconnected, if applicable.
|
||||||
|
*
|
||||||
|
* @return an Optional containing the disconnect timestamp, or empty if connected
|
||||||
|
*/
|
||||||
|
public Optional<Instant> getDisconnectedAt() {
|
||||||
|
return Optional.ofNullable(disconnectedAt);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Associates this user with a new session, clearing the disconnect timestamp.
|
||||||
|
*
|
||||||
|
* @param sessionId the new session ID
|
||||||
|
*/
|
||||||
|
public void reassignSession(SessionId sessionId) {
|
||||||
|
this.sessionId = sessionId;
|
||||||
|
this.disconnectedAt = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Marks this user as disconnected by clearing the session and recording the timestamp. */
|
||||||
|
public void markDisconnected() {
|
||||||
|
this.sessionId = null;
|
||||||
|
this.disconnectedAt = Instant.now();
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized void enqueueMessage(Message message) {
|
||||||
|
messages.add(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized List<Message> dequeueAllMessages(Message message) {
|
||||||
|
List<Message> allMessages = new ArrayDeque<>(messages).stream().toList();
|
||||||
|
messages.clear();
|
||||||
|
return allMessages;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
package ch.unibas.dmi.dbis.cs108.casono.server.domain.user;
|
||||||
|
|
||||||
|
import java.time.Duration;
|
||||||
|
import java.time.Instant;
|
||||||
|
import org.apache.logging.log4j.LogManager;
|
||||||
|
import org.apache.logging.log4j.Logger;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Periodically periodicly run job to remove disconnected users who have exceeded the reconnect
|
||||||
|
* threshold.
|
||||||
|
*/
|
||||||
|
public class UserCleanupJob implements Runnable {
|
||||||
|
private final Logger logger;
|
||||||
|
private final UserRegistry registry;
|
||||||
|
private final Duration reconnectThreshold;
|
||||||
|
|
||||||
|
public UserCleanupJob(UserRegistry registry, Duration reconnectThreshold) {
|
||||||
|
this.logger = LogManager.getLogger(UserCleanupJob.class);
|
||||||
|
this.registry = registry;
|
||||||
|
this.reconnectThreshold = reconnectThreshold;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
logger.debug("Job started.");
|
||||||
|
Instant threshold = Instant.now().minus(reconnectThreshold);
|
||||||
|
|
||||||
|
for (User user : registry.getAllUsers()) {
|
||||||
|
if (user.getDisconnectedAt().isEmpty()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
Instant disconnectedAt = user.getDisconnectedAt().get();
|
||||||
|
if (disconnectedAt.isBefore(threshold)) {
|
||||||
|
if (registry.removeIfStillDisconnected(user.getId())) {
|
||||||
|
logger.info(
|
||||||
|
"Removed expired user {} ({})", user.getName(), user.getId().value());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.debug("Job finished.");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
package ch.unibas.dmi.dbis.cs108.casono.server.domain.user;
|
||||||
|
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.network.sessions.SessionId;
|
||||||
|
|
||||||
|
/** Creates new users, resolving name conflicts automatically. */
|
||||||
|
public class UserFactory {
|
||||||
|
private final UserRegistry registry;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new UserFactory backed by the given registry.
|
||||||
|
*
|
||||||
|
* @param registry the registry to register new users to
|
||||||
|
*/
|
||||||
|
public UserFactory(UserRegistry registry) {
|
||||||
|
this.registry = registry;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates and registers a new user with the given name and session. If the name is already
|
||||||
|
* taken, a numeric suffix is appended and incremented until a free name is found (e.g.
|
||||||
|
* Lars_001, Lars_002, ...).
|
||||||
|
*
|
||||||
|
* @param desiredName the preferred display name
|
||||||
|
* @param sessionId the session to associate with the new user
|
||||||
|
* @return the newly created and registered user
|
||||||
|
*/
|
||||||
|
public User create(String desiredName, SessionId sessionId) {
|
||||||
|
var result = registry.registerIfAvailable(desiredName, sessionId);
|
||||||
|
if (result.isPresent()) {
|
||||||
|
return result.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
int suffix = 1;
|
||||||
|
while (true) {
|
||||||
|
String candidate = desiredName + "_" + String.format("%03d", suffix);
|
||||||
|
result = registry.registerIfAvailable(candidate, sessionId);
|
||||||
|
if (result.isPresent()) {
|
||||||
|
return result.get();
|
||||||
|
}
|
||||||
|
suffix++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
package ch.unibas.dmi.dbis.cs108.casono.server.domain.user;
|
||||||
|
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
/** Represents a unique identifier for a user. */
|
||||||
|
public class UserId {
|
||||||
|
private final UUID value;
|
||||||
|
|
||||||
|
/** Creates a new UserId with a randomly generated UUID. */
|
||||||
|
public UserId() {
|
||||||
|
this.value = UUID.randomUUID();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new UserId with the specified UUID.
|
||||||
|
*
|
||||||
|
* @param value the UUID to use for this UserId
|
||||||
|
*/
|
||||||
|
public UserId(UUID value) {
|
||||||
|
this.value = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the UUID value of this UserId.
|
||||||
|
*
|
||||||
|
* @return the UUID value
|
||||||
|
*/
|
||||||
|
public UUID value() {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,107 @@
|
|||||||
|
package ch.unibas.dmi.dbis.cs108.casono.server.domain.user;
|
||||||
|
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.network.sessions.SessionId;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
|
||||||
|
/** Manages all active users on the server. */
|
||||||
|
public class UserRegistry {
|
||||||
|
private final ConcurrentHashMap<UserId, User> byId = new ConcurrentHashMap<>();
|
||||||
|
private final ConcurrentHashMap<String, User> byName = new ConcurrentHashMap<>();
|
||||||
|
private final ConcurrentHashMap<SessionId, User> bySessionId = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attempts to register a user under the given name atomically. Returns the registered user, or
|
||||||
|
* empty if the name is already taken.
|
||||||
|
*
|
||||||
|
* @param name the desired display name
|
||||||
|
* @param sessionId the session to associate with the new user
|
||||||
|
* @return an Optional containing the new user, or empty if the name was taken
|
||||||
|
*/
|
||||||
|
public synchronized Optional<User> registerIfAvailable(String name, SessionId sessionId) {
|
||||||
|
if (byName.containsKey(name)) {
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
User user = new User(new UserId(), name, sessionId);
|
||||||
|
byId.put(user.getId(), user);
|
||||||
|
byName.put(user.getName(), user);
|
||||||
|
bySessionId.put(sessionId, user);
|
||||||
|
return Optional.of(user);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes the user with the given ID, but only if they are still disconnected. This prevents
|
||||||
|
* removing a user who has reconnected between the cleanup job's check and its removal call.
|
||||||
|
*
|
||||||
|
* @param userId the ID of the user to remove
|
||||||
|
*/
|
||||||
|
public synchronized boolean removeIfStillDisconnected(UserId userId) {
|
||||||
|
User user = byId.get(userId);
|
||||||
|
if (user == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (user.getSessionId().isPresent()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
byId.remove(user.getId());
|
||||||
|
byName.remove(user.getName());
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Marks the user associated with the given session as disconnected, clearing the session
|
||||||
|
* association and recording the disconnect timestamp.
|
||||||
|
*
|
||||||
|
* @param sessionId the session ID of the disconnected client
|
||||||
|
*/
|
||||||
|
// TODO: Add to EventRegistry with DisconnectEvent
|
||||||
|
public synchronized void onDisconnect(SessionId sessionId) {
|
||||||
|
User user = bySessionId.remove(sessionId);
|
||||||
|
if (user == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
user.markDisconnected();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reassociates a user with a new session, effectively restoring them after a reconnect.
|
||||||
|
*
|
||||||
|
* @param userId the ID of the user to reconnect
|
||||||
|
* @param sessionId the new session ID
|
||||||
|
* @return an Optional containing the user, or empty if the user was not found
|
||||||
|
*/
|
||||||
|
public synchronized Optional<User> reassignSession(UserId userId, SessionId sessionId) {
|
||||||
|
User user = byId.get(userId);
|
||||||
|
if (user == null) {
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
user.reassignSession(sessionId);
|
||||||
|
bySessionId.put(sessionId, user);
|
||||||
|
return Optional.of(user);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Looks up a user by their session ID.
|
||||||
|
*
|
||||||
|
* @param sessionId the session ID to look up
|
||||||
|
* @return an Optional containing the user, or empty if no user is associated with this session
|
||||||
|
*/
|
||||||
|
public Optional<User> findBySessionId(SessionId sessionId) {
|
||||||
|
return Optional.ofNullable(bySessionId.get(sessionId));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns all currently registered users.
|
||||||
|
*
|
||||||
|
* @return a collection of all users
|
||||||
|
*/
|
||||||
|
public Collection<User> getAllUsers() {
|
||||||
|
return byId.values();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
package ch.unibas.dmi.dbis.cs108.casono.server.network;
|
||||||
|
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.network.events.DisconnectEvent;
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.network.events.EventBus;
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.network.sessions.Session;
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.network.sessions.SessionManager;
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.network.transport.TcpTransport;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.net.ServerSocket;
|
||||||
|
import java.net.Socket;
|
||||||
|
import org.apache.logging.log4j.LogManager;
|
||||||
|
import org.apache.logging.log4j.Logger;
|
||||||
|
|
||||||
|
/** Creates and manages the server socket. Accepts new incoming connections and creates sessions. */
|
||||||
|
public class NetworkManager implements Runnable {
|
||||||
|
private Integer port;
|
||||||
|
private Logger logger;
|
||||||
|
private Thread thread;
|
||||||
|
private Boolean running;
|
||||||
|
private SessionManager sessionManager;
|
||||||
|
private EventBus eventBus;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new NetworkManager with the given port, session manager, and event bus.
|
||||||
|
*
|
||||||
|
* @param port the port to listen on
|
||||||
|
* @param sessionManager the session manager to use
|
||||||
|
* @param eventBus the event bus for events
|
||||||
|
*/
|
||||||
|
public NetworkManager(Integer port, SessionManager sessionManager, EventBus eventBus) {
|
||||||
|
this.port = port;
|
||||||
|
this.logger = LogManager.getLogger(NetworkManager.class);
|
||||||
|
this.thread = new Thread(this, "networkManager");
|
||||||
|
this.running = true;
|
||||||
|
this.sessionManager = sessionManager;
|
||||||
|
this.eventBus = eventBus;
|
||||||
|
this.eventBus.subscribe(DisconnectEvent.class, event -> clientDisconnected(event));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Starts the internal thread to accept new connections. */
|
||||||
|
public void start() {
|
||||||
|
logger.debug("Starting at port {}", port);
|
||||||
|
thread.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles client disconnection events.
|
||||||
|
*
|
||||||
|
* @param event the disconnect event
|
||||||
|
*/
|
||||||
|
public void clientDisconnected(DisconnectEvent event) {
|
||||||
|
logger.info("Session {} disconnected", event.sessionId().value());
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Runs the network manager loop, accepting connections. */
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
try (ServerSocket serverSocket = new ServerSocket(port)) {
|
||||||
|
while (running) {
|
||||||
|
Socket clientSocket = serverSocket.accept();
|
||||||
|
|
||||||
|
logger.debug("Accepted connection from {}", clientSocket.getRemoteSocketAddress());
|
||||||
|
|
||||||
|
Session session = new Session(new TcpTransport(clientSocket), eventBus);
|
||||||
|
sessionManager.addSession(session);
|
||||||
|
session.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (IOException e) {
|
||||||
|
logger.fatal(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
package ch.unibas.dmi.dbis.cs108.casono.server.network.events;
|
||||||
|
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.network.sessions.SessionId;
|
||||||
|
|
||||||
|
/** Represents a disconnect event for a session. */
|
||||||
|
public record DisconnectEvent(SessionId sessionId) implements Event {}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
package ch.unibas.dmi.dbis.cs108.casono.server.network.events;
|
||||||
|
|
||||||
|
/** Marker interface for events in the event bus system. */
|
||||||
|
interface Event {}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
package ch.unibas.dmi.dbis.cs108.casono.server.network.events;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
import java.util.concurrent.CopyOnWriteArrayList;
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
|
/** A simple event bus for publishing and subscribing to events. */
|
||||||
|
public class EventBus {
|
||||||
|
private final Map<Class<?>, List<Consumer<Object>>> handlers = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Subscribes a handler to a specific event type.
|
||||||
|
*
|
||||||
|
* @param eventType the class of the event to subscribe to
|
||||||
|
* @param handler the consumer to handle the event
|
||||||
|
*/
|
||||||
|
@SuppressWarnings(
|
||||||
|
"unchecked") // This cast is safe, because handlers only get passed the type they
|
||||||
|
// subscribed to
|
||||||
|
public <T extends Event> void subscribe(Class<T> eventType, Consumer<T> handler) {
|
||||||
|
handlers.computeIfAbsent(eventType, k -> new CopyOnWriteArrayList<>())
|
||||||
|
.add((Consumer<Object>) (Consumer<?>) handler);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Publishes an event to all subscribed handlers.
|
||||||
|
*
|
||||||
|
* @param event the event to publish
|
||||||
|
*/
|
||||||
|
public <T extends Event> void publish(T event) {
|
||||||
|
Objects.requireNonNull(event, "event must not be null");
|
||||||
|
List<Consumer<Object>> subscribers = handlers.get(event.getClass());
|
||||||
|
if (subscribers != null) {
|
||||||
|
subscribers.forEach(h -> h.accept(event));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
package ch.unibas.dmi.dbis.cs108.casono.server.network.parser;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parser to convert the PrimitiveRequest to a Request and performing checks for required fields and
|
||||||
|
* data types
|
||||||
|
*/
|
||||||
|
public interface CommandParser {
|
||||||
|
/**
|
||||||
|
* Parses the provided PrimitiveRequest into a command-specific request
|
||||||
|
*
|
||||||
|
* @param primitiveRequest
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
Request parse(PrimitiveRequest primitiveRequest);
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
package ch.unibas.dmi.dbis.cs108.casono.server.network.parser;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/** Dispatcher all CommandParser are registered in */
|
||||||
|
public class CommandParserDispatcher {
|
||||||
|
private final Map<String, CommandParser> parsers = new HashMap<>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register a new CommandParser
|
||||||
|
*
|
||||||
|
* @param command
|
||||||
|
* @param parser the parser class
|
||||||
|
*/
|
||||||
|
public void register(String command, CommandParser parser) {
|
||||||
|
parsers.put(command, parser);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses the PrimitiveRequest into a Request using the appropriate CommandParser
|
||||||
|
*
|
||||||
|
* @param primitiveRequest the PrimitiveRequest to parse
|
||||||
|
* @return the parsed Request object
|
||||||
|
*/
|
||||||
|
public Request parse(PrimitiveRequest primitiveRequest) {
|
||||||
|
String command = primitiveRequest.command();
|
||||||
|
CommandParser parser = parsers.get(command);
|
||||||
|
|
||||||
|
if (parser == null) {
|
||||||
|
throw new UnknownCommandException(command);
|
||||||
|
}
|
||||||
|
|
||||||
|
return parser.parse(primitiveRequest);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
package ch.unibas.dmi.dbis.cs108.casono.server.network.parser;
|
||||||
|
|
||||||
|
/** Used in the PrimitiveRequest class to store the key of a parameter with its respective value */
|
||||||
|
public record Parameter(String key, String value) {}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
package ch.unibas.dmi.dbis.cs108.casono.server.network.parser;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/** Created by the ProtocolParser to allow easy access to the request contents */
|
||||||
|
public record PrimitiveRequest(int requestId, String command, List<Parameter> parameters) {}
|
||||||
@@ -0,0 +1,110 @@
|
|||||||
|
package ch.unibas.dmi.dbis.cs108.casono.server.network.parser;
|
||||||
|
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.network.transport.RawPacket;
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.tokenizer.RawToken;
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.tokenizer.Token;
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.tokenizer.TokenClassifier;
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.tokenizer.TokenType;
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.tokenizer.Tokenizer;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Iterator;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.NoSuchElementException;
|
||||||
|
|
||||||
|
/** ProtocolParser, used to parse RawPacket into a PrimitiveRequest */
|
||||||
|
public class ProtocolParser {
|
||||||
|
/**
|
||||||
|
* Parses the payload of the provided RawPacket
|
||||||
|
*
|
||||||
|
* @param packet the RawPacket containing the recieved data
|
||||||
|
* @return created PrimitiveRequest
|
||||||
|
*/
|
||||||
|
public static PrimitiveRequest parse(RawPacket packet) {
|
||||||
|
List<RawToken> rawTokens = Tokenizer.tokenize(packet.payload());
|
||||||
|
List<Token> tokens = TokenClassifier.classify(rawTokens);
|
||||||
|
|
||||||
|
Iterator<Token> iterator = tokens.iterator();
|
||||||
|
String command = readCommand(iterator);
|
||||||
|
List<Parameter> parameters = readParameters(iterator);
|
||||||
|
|
||||||
|
return new PrimitiveRequest(packet.requestId(), command, parameters);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reads the first token, which is ensured to be the command of the request
|
||||||
|
*
|
||||||
|
* @param iterator
|
||||||
|
* @return read command
|
||||||
|
*/
|
||||||
|
private static String readCommand(Iterator<Token> iterator) {
|
||||||
|
Token token = iterator.next();
|
||||||
|
if (token.type() != TokenType.COMMAND) {
|
||||||
|
throw new ProtocolParserException("Expected COMMAND got " + token.type());
|
||||||
|
}
|
||||||
|
|
||||||
|
return token.value();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Iterates over the remaining tokens, calling helperfunctions to validate token type
|
||||||
|
*
|
||||||
|
* @param iterator
|
||||||
|
* @return list containing all parsed parameters
|
||||||
|
*/
|
||||||
|
private static List<Parameter> readParameters(Iterator<Token> iterator) {
|
||||||
|
List<Parameter> parameters = new ArrayList<>();
|
||||||
|
|
||||||
|
try {
|
||||||
|
while (iterator.hasNext()) {
|
||||||
|
Token nextToken = iterator.next();
|
||||||
|
|
||||||
|
if (nextToken.type() == TokenType.EOF) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
String key = readKey(nextToken);
|
||||||
|
readSeperator(iterator.next());
|
||||||
|
String value = readValue(iterator.next());
|
||||||
|
|
||||||
|
parameters.add(new Parameter(key, value));
|
||||||
|
}
|
||||||
|
} catch (NoSuchElementException e) {
|
||||||
|
throw new ProtocolParserException("Ran out of tokens while reading parameter");
|
||||||
|
}
|
||||||
|
|
||||||
|
return parameters;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param token the current token
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
private static String readKey(Token token) {
|
||||||
|
if (token.type() != TokenType.KEY) {
|
||||||
|
throw new ProtocolParserException("Expected KEY got " + token.type());
|
||||||
|
}
|
||||||
|
|
||||||
|
return token.value();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param token the current token
|
||||||
|
*/
|
||||||
|
private static void readSeperator(Token token) {
|
||||||
|
if (token.type() != TokenType.SEPARATOR) {
|
||||||
|
throw new ProtocolParserException("Expected SEPARATOR got " + token.type());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param token the current token
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
private static String readValue(Token token) {
|
||||||
|
if (token.type() != TokenType.VALUE) {
|
||||||
|
throw new ProtocolParserException("Expected VALUE got " + token.type());
|
||||||
|
}
|
||||||
|
|
||||||
|
return token.value();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package ch.unibas.dmi.dbis.cs108.casono.server.network.parser;
|
||||||
|
|
||||||
|
public class ProtocolParserException extends RuntimeException {
|
||||||
|
public ProtocolParserException(String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
package ch.unibas.dmi.dbis.cs108.casono.server.network.parser;
|
||||||
|
|
||||||
|
/** Request, produced by the CommandParser */
|
||||||
|
public interface Request {}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
package ch.unibas.dmi.dbis.cs108.casono.server.network.parser;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exception thrown when the CommandParserDispatcher has no registered handler for the provided
|
||||||
|
* request
|
||||||
|
*/
|
||||||
|
public class UnknownCommandException extends RuntimeException {
|
||||||
|
public UnknownCommandException(String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
package ch.unibas.dmi.dbis.cs108.casono.server.network.sessions;
|
||||||
|
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.network.events.DisconnectEvent;
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.network.events.EventBus;
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.network.parser.PrimitiveRequest;
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.network.parser.ProtocolParser;
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.network.transport.RawPacket;
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.network.transport.TransportLayer;
|
||||||
|
import java.io.EOFException;
|
||||||
|
import java.io.IOException;
|
||||||
|
import org.apache.logging.log4j.LogManager;
|
||||||
|
import org.apache.logging.log4j.Logger;
|
||||||
|
|
||||||
|
/** Represents a client session in the network server. */
|
||||||
|
public class Session implements Runnable {
|
||||||
|
private SessionId id;
|
||||||
|
private Thread thread;
|
||||||
|
private TransportLayer transport;
|
||||||
|
private Logger logger;
|
||||||
|
private Boolean running;
|
||||||
|
private EventBus eventBus;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new Session with the given transport and event bus.
|
||||||
|
*
|
||||||
|
* @param transport the transport layer for communication
|
||||||
|
* @param eventBus the event bus for publishing events
|
||||||
|
* @throws IOException if an I/O error occurs during initialization
|
||||||
|
*/
|
||||||
|
public Session(TransportLayer transport, EventBus eventBus) throws IOException {
|
||||||
|
this.id = new SessionId();
|
||||||
|
this.thread = new Thread(this, "session-" + this.id.value());
|
||||||
|
this.transport = transport;
|
||||||
|
this.running = true;
|
||||||
|
this.eventBus = eventBus;
|
||||||
|
|
||||||
|
this.logger = LogManager.getLogger(Session.class.toString() + id.value());
|
||||||
|
this.logger.info("Created new session");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the ID of this session.
|
||||||
|
*
|
||||||
|
* @return the session ID
|
||||||
|
*/
|
||||||
|
public SessionId getId() {
|
||||||
|
return this.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Starts the session thread. */
|
||||||
|
public void start() {
|
||||||
|
thread.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Closes the session and its transport.
|
||||||
|
*
|
||||||
|
* @throws IOException if an I/O error occurs
|
||||||
|
*/
|
||||||
|
public void close() throws IOException {
|
||||||
|
transport.close();
|
||||||
|
this.running = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Runs the session loop, reading from the transport. */
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
while (running) {
|
||||||
|
try {
|
||||||
|
RawPacket rawPacket = transport.read();
|
||||||
|
logger.debug("Recieved: {}", rawPacket);
|
||||||
|
|
||||||
|
PrimitiveRequest primitiveRequest = ProtocolParser.parse(rawPacket);
|
||||||
|
logger.debug("Parsed request to {}", primitiveRequest);
|
||||||
|
} catch (EOFException e) {
|
||||||
|
logger.info("Client disconnected");
|
||||||
|
eventBus.publish(new DisconnectEvent(id));
|
||||||
|
break;
|
||||||
|
} catch (IOException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
package ch.unibas.dmi.dbis.cs108.casono.server.network.sessions;
|
||||||
|
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
/** Represents a unique identifier for a session. */
|
||||||
|
public class SessionId {
|
||||||
|
private final UUID value;
|
||||||
|
|
||||||
|
/** Creates a new SessionId with a randomly generated UUID. */
|
||||||
|
public SessionId() {
|
||||||
|
this.value = UUID.randomUUID();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new SessionId with the specified UUID.
|
||||||
|
*
|
||||||
|
* @param UUID to use for this SessionId
|
||||||
|
*/
|
||||||
|
public SessionId(UUID value) {
|
||||||
|
this.value = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the UUID value of this SessionId.
|
||||||
|
*
|
||||||
|
* @return the UUID value
|
||||||
|
*/
|
||||||
|
public UUID value() {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
package ch.unibas.dmi.dbis.cs108.casono.server.network.sessions;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
|
||||||
|
/** Manages active sessions in the server. */
|
||||||
|
public class SessionManager {
|
||||||
|
private Map<SessionId, Session> sessions;
|
||||||
|
|
||||||
|
/** Constructs a new SessionManager. */
|
||||||
|
public SessionManager() {
|
||||||
|
this.sessions = new ConcurrentHashMap<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a session to the manager.
|
||||||
|
*
|
||||||
|
* @param session the session to add
|
||||||
|
*/
|
||||||
|
public void addSession(Session session) {
|
||||||
|
sessions.put(session.getId(), session);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes a session by its ID.
|
||||||
|
*
|
||||||
|
* @param id the ID of the session to remove
|
||||||
|
* @return the removed session, or null if not found
|
||||||
|
*/
|
||||||
|
public Session removeSession(SessionId id) {
|
||||||
|
return sessions.remove(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes the specified session.
|
||||||
|
*
|
||||||
|
* @param session the session to remove
|
||||||
|
* @return the removed session, or null if not found
|
||||||
|
*/
|
||||||
|
public Session removeSession(Session session) {
|
||||||
|
return sessions.remove(session.getId());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves a session by its ID.
|
||||||
|
*
|
||||||
|
* @param id the ID of the session to retrieve
|
||||||
|
* @return the session with the specified ID, or null if not found
|
||||||
|
*/
|
||||||
|
public Session getSessionById(SessionId id) {
|
||||||
|
return sessions.get(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package ch.unibas.dmi.dbis.cs108.casono.server.network.transport;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Transport object, created by the transport layer to store raw data about the request
|
||||||
|
* prior to any processing
|
||||||
|
*/
|
||||||
|
public record RawPacket(int requestId, String payload) {}
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
package ch.unibas.dmi.dbis.cs108.casono.server.network.transport;
|
||||||
|
|
||||||
|
import java.io.DataInputStream;
|
||||||
|
import java.io.DataOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.net.Socket;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
|
||||||
|
/** Implements TCP-based transport layer for network communication. */
|
||||||
|
public class TcpTransport implements TransportLayer {
|
||||||
|
private Socket socket;
|
||||||
|
private DataInputStream in;
|
||||||
|
private DataOutputStream out;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new TcpTransport with the given socket.
|
||||||
|
*
|
||||||
|
* @param socket the socket to use for communication
|
||||||
|
* @throws IOException if an I/O error occurs
|
||||||
|
*/
|
||||||
|
public TcpTransport(Socket socket) throws IOException {
|
||||||
|
this.socket = socket;
|
||||||
|
this.in = new DataInputStream(socket.getInputStream());
|
||||||
|
this.out = new DataOutputStream(socket.getOutputStream());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reads a string from the socket.
|
||||||
|
*
|
||||||
|
* @return the read RawPacket
|
||||||
|
* @throws IOException if an I/O error occurs
|
||||||
|
*/
|
||||||
|
public RawPacket read() throws IOException {
|
||||||
|
int length = in.readInt();
|
||||||
|
int requestId = in.readInt();
|
||||||
|
byte[] rawPayload = new byte[length];
|
||||||
|
in.readFully(rawPayload);
|
||||||
|
|
||||||
|
String payload = new String(rawPayload, StandardCharsets.UTF_8);
|
||||||
|
return new RawPacket(requestId, payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Writes a string to the socket.
|
||||||
|
*
|
||||||
|
* @param data the RawPacket's data to write
|
||||||
|
* @throws IOException if an I/O error occurs
|
||||||
|
*/
|
||||||
|
public void write(RawPacket data) throws IOException {
|
||||||
|
int requestId = data.requestId();
|
||||||
|
byte[] rawPayload = data.payload().getBytes(StandardCharsets.UTF_8);
|
||||||
|
|
||||||
|
out.writeInt(requestId);
|
||||||
|
out.writeInt(rawPayload.length);
|
||||||
|
out.write(rawPayload);
|
||||||
|
out.flush();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Closes the socket.
|
||||||
|
*
|
||||||
|
* @throws IOException if an I/O error occurs
|
||||||
|
*/
|
||||||
|
public void close() throws IOException {
|
||||||
|
socket.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
package ch.unibas.dmi.dbis.cs108.casono.server.network.transport;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
/** Interface for transport layer implementations. */
|
||||||
|
public interface TransportLayer {
|
||||||
|
/**
|
||||||
|
* Reads data from the transport layer.
|
||||||
|
*
|
||||||
|
* @return the read data as a RawPacket
|
||||||
|
* @throws IOException if an I/O error occurs
|
||||||
|
*/
|
||||||
|
RawPacket read() throws IOException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Writes data to the transport layer.
|
||||||
|
*
|
||||||
|
* @param data the data to write
|
||||||
|
* @throws IOException if an I/O error occurs
|
||||||
|
*/
|
||||||
|
void write(RawPacket data) throws IOException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Closes the transport layer.
|
||||||
|
*
|
||||||
|
* @throws IOException if an I/O error occurs
|
||||||
|
*/
|
||||||
|
void close() throws IOException;
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
package ch.unibas.dmi.dbis.cs108.casono.server.tokenizer;
|
||||||
|
|
||||||
|
/** Represents a raw (unclassified) token in the tokenizer. */
|
||||||
|
public record RawToken(RawTokenType type, String value, int line, int column) {}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package ch.unibas.dmi.dbis.cs108.casono.server.tokenizer;
|
||||||
|
|
||||||
|
public enum RawTokenType {
|
||||||
|
WORD,
|
||||||
|
STRING,
|
||||||
|
SEPARATOR,
|
||||||
|
NEWLINE,
|
||||||
|
EOF
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
package ch.unibas.dmi.dbis.cs108.casono.server.tokenizer;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/** Internal state for the tokenizer. */
|
||||||
|
class State {
|
||||||
|
final String input;
|
||||||
|
int pos;
|
||||||
|
int line;
|
||||||
|
int column;
|
||||||
|
final List<RawToken> tokens;
|
||||||
|
|
||||||
|
State(String input) {
|
||||||
|
this.input = input;
|
||||||
|
this.pos = 0;
|
||||||
|
this.line = 1;
|
||||||
|
this.column = 1;
|
||||||
|
this.tokens = new ArrayList<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
char current() {
|
||||||
|
return input.charAt(pos);
|
||||||
|
}
|
||||||
|
|
||||||
|
char peek() {
|
||||||
|
if (pos + 1 >= input.length()) {
|
||||||
|
return '\0';
|
||||||
|
}
|
||||||
|
return input.charAt(pos + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
void advance() {
|
||||||
|
pos++;
|
||||||
|
column++;
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean isEof() {
|
||||||
|
return pos >= input.length();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
package ch.unibas.dmi.dbis.cs108.casono.server.tokenizer;
|
||||||
|
|
||||||
|
/** Represents a token in the tokenizer. */
|
||||||
|
public record Token(TokenType type, String value, int line, int column) {}
|
||||||
@@ -0,0 +1,103 @@
|
|||||||
|
package ch.unibas.dmi.dbis.cs108.casono.server.tokenizer;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class TokenClassifier {
|
||||||
|
public static List<Token> classify(List<RawToken> rawTokens) {
|
||||||
|
validateFirstToken(rawTokens);
|
||||||
|
|
||||||
|
List<Token> tokens = new ArrayList<>();
|
||||||
|
|
||||||
|
for (int i = 0; i < rawTokens.size(); i++) {
|
||||||
|
RawToken raw = rawTokens.get(i);
|
||||||
|
|
||||||
|
switch (raw.type()) {
|
||||||
|
case SEPARATOR -> {
|
||||||
|
validateSeparator(rawTokens, i);
|
||||||
|
tokens.add(
|
||||||
|
new Token(TokenType.SEPARATOR, raw.value(), raw.line(), raw.column()));
|
||||||
|
}
|
||||||
|
case WORD -> {
|
||||||
|
TokenType type = resolveWordType(rawTokens, i);
|
||||||
|
tokens.add(new Token(type, raw.value(), raw.line(), raw.column()));
|
||||||
|
}
|
||||||
|
case STRING -> {
|
||||||
|
validateString(rawTokens, i);
|
||||||
|
tokens.add(new Token(TokenType.VALUE, raw.value(), raw.line(), raw.column()));
|
||||||
|
}
|
||||||
|
|
||||||
|
case NEWLINE -> {
|
||||||
|
throw new TokenizerException(
|
||||||
|
"Unexpected newline in request", raw.line(), raw.column());
|
||||||
|
}
|
||||||
|
|
||||||
|
case EOF -> {
|
||||||
|
tokens.add(new Token(TokenType.EOF, raw.value(), raw.line(), raw.column()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return tokens;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void validateFirstToken(List<RawToken> rawTokens) {
|
||||||
|
if (rawTokens.isEmpty() || rawTokens.get(0).type() != RawTokenType.WORD) {
|
||||||
|
throw new TokenizerException("Expected COMMAND as first token", 1, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void validateSeparator(List<RawToken> rawTokens, int index) {
|
||||||
|
boolean missingKey = index == 0 || rawTokens.get(index - 1).type() != RawTokenType.WORD;
|
||||||
|
|
||||||
|
boolean missingValue =
|
||||||
|
index + 1 >= rawTokens.size()
|
||||||
|
|| (rawTokens.get(index + 1).type() != RawTokenType.WORD
|
||||||
|
&& rawTokens.get(index + 1).type() != RawTokenType.STRING);
|
||||||
|
|
||||||
|
boolean nextWordIsKey =
|
||||||
|
!missingValue
|
||||||
|
&& rawTokens.get(index + 1).type() == RawTokenType.WORD
|
||||||
|
&& index + 2 < rawTokens.size()
|
||||||
|
&& rawTokens.get(index + 2).type() == RawTokenType.SEPARATOR;
|
||||||
|
|
||||||
|
RawToken separator = rawTokens.get(index);
|
||||||
|
|
||||||
|
if (missingKey) {
|
||||||
|
throw new TokenizerException(
|
||||||
|
"Expected KEY before '='", separator.line(), separator.column());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (missingValue || nextWordIsKey) {
|
||||||
|
throw new TokenizerException(
|
||||||
|
"Expected VALUE after '='", separator.line(), separator.column());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void validateString(List<RawToken> rawTokens, int index) {
|
||||||
|
boolean afterSeparator =
|
||||||
|
index > 0 && rawTokens.get(index - 1).type() == RawTokenType.SEPARATOR;
|
||||||
|
|
||||||
|
if (!afterSeparator) {
|
||||||
|
RawToken token = rawTokens.get(index);
|
||||||
|
throw new TokenizerException("Unexpected string literal", token.line(), token.column());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static TokenType resolveWordType(List<RawToken> rawTokens, int index) {
|
||||||
|
boolean isFirst = index == 0;
|
||||||
|
boolean afterNewline = index > 0 && rawTokens.get(index - 1).type() == RawTokenType.NEWLINE;
|
||||||
|
boolean afterSeparator =
|
||||||
|
index > 0 && rawTokens.get(index - 1).type() == RawTokenType.SEPARATOR;
|
||||||
|
|
||||||
|
if (isFirst || afterNewline) {
|
||||||
|
return TokenType.COMMAND;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (afterSeparator) {
|
||||||
|
return TokenType.VALUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
return TokenType.KEY;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
package ch.unibas.dmi.dbis.cs108.casono.server.tokenizer;
|
||||||
|
|
||||||
|
/** Enumeration of token types used in the tokenizer. */
|
||||||
|
public enum TokenType {
|
||||||
|
COMMAND,
|
||||||
|
KEY,
|
||||||
|
VALUE,
|
||||||
|
SEPARATOR,
|
||||||
|
EOF
|
||||||
|
}
|
||||||
@@ -0,0 +1,139 @@
|
|||||||
|
package ch.unibas.dmi.dbis.cs108.casono.server.tokenizer;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A static utility class for tokenizing input strings into a sequence of tokens.
|
||||||
|
*
|
||||||
|
* <p>Strings can be enclosed in single quotes and may contain escaped single quotes using a
|
||||||
|
* backslash.
|
||||||
|
*/
|
||||||
|
public class Tokenizer {
|
||||||
|
/**
|
||||||
|
* Tokenizes the given input string into a list of tokens.
|
||||||
|
*
|
||||||
|
* @param input the string to tokenize
|
||||||
|
* @return a list of tokens representing the input
|
||||||
|
* @throws TokenizerException if the input contains unexpected characters, unterminated strings,
|
||||||
|
* or other syntax errors
|
||||||
|
*/
|
||||||
|
public static List<RawToken> tokenize(String input) {
|
||||||
|
State state = new State(input);
|
||||||
|
|
||||||
|
while (!state.isEof()) {
|
||||||
|
char c = state.current();
|
||||||
|
|
||||||
|
if (c == ' ' || c == '\t') {
|
||||||
|
state.advance();
|
||||||
|
|
||||||
|
} else if (c == '\n') {
|
||||||
|
readNewline(state);
|
||||||
|
|
||||||
|
} else if (c == '=') {
|
||||||
|
readSeparator(state);
|
||||||
|
|
||||||
|
} else if (c == '\'') {
|
||||||
|
readString(state);
|
||||||
|
|
||||||
|
} else if (Character.isLetterOrDigit(c) || c == '_') {
|
||||||
|
readWord(state);
|
||||||
|
|
||||||
|
} else {
|
||||||
|
throw new TokenizerException(
|
||||||
|
"Unexpected character '" + c + "'", state.line, state.column);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
state.tokens.add(new RawToken(RawTokenType.EOF, null, state.line, state.column));
|
||||||
|
return state.tokens;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reads a newline character and creates a NEWLINE token.
|
||||||
|
*
|
||||||
|
* <p>Updates the line counter and resets the column to 1.
|
||||||
|
*
|
||||||
|
* @param state the current parsing state
|
||||||
|
*/
|
||||||
|
private static void readNewline(State state) {
|
||||||
|
state.tokens.add(new RawToken(RawTokenType.NEWLINE, "", state.line, state.column));
|
||||||
|
state.advance();
|
||||||
|
state.line++;
|
||||||
|
state.column = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reads a separator character ('=') and creates a SEPARATOR token.
|
||||||
|
*
|
||||||
|
* @param state the current parsing state
|
||||||
|
*/
|
||||||
|
private static void readSeparator(State state) {
|
||||||
|
state.tokens.add(new RawToken(RawTokenType.SEPARATOR, "=", state.line, state.column));
|
||||||
|
state.advance();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reads a word (sequence of alphanumeric characters and underscores) and creates an appropriate
|
||||||
|
* token (COMMAND, KEY, or VALUE) based on the parsing context.
|
||||||
|
*
|
||||||
|
* <p>The token type is determined by the {@link #resolveWordType(State)} method.
|
||||||
|
*
|
||||||
|
* @param state the current parsing state
|
||||||
|
*/
|
||||||
|
private static void readWord(State state) {
|
||||||
|
int startColumn = state.column;
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
|
||||||
|
while (!state.isEof()
|
||||||
|
&& (Character.isLetterOrDigit(state.current()) || state.current() == '_')) {
|
||||||
|
sb.append(state.current());
|
||||||
|
state.advance();
|
||||||
|
}
|
||||||
|
|
||||||
|
state.tokens.add(new RawToken(RawTokenType.WORD, sb.toString(), state.line, startColumn));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reads a string literal enclosed in single quotes and creates a VALUE token.
|
||||||
|
*
|
||||||
|
* <p>Supports escaped single quotes using backslash notation (\'). Newlines within strings are
|
||||||
|
* properly tracked for line counting.
|
||||||
|
*
|
||||||
|
* @param state the current parsing state
|
||||||
|
* @throws TokenizerException if the string literal is not terminated before end of input
|
||||||
|
*/
|
||||||
|
private static void readString(State state) {
|
||||||
|
int startColumn = state.column;
|
||||||
|
state.advance();
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
if (state.isEof()) {
|
||||||
|
throw new TokenizerException(
|
||||||
|
"Unterminated string literal", state.line, startColumn);
|
||||||
|
}
|
||||||
|
|
||||||
|
char c = state.current();
|
||||||
|
|
||||||
|
if (c == '\\' && state.peek() == '\'') {
|
||||||
|
sb.append('\'');
|
||||||
|
state.advance();
|
||||||
|
state.advance();
|
||||||
|
|
||||||
|
} else if (c == '\'') {
|
||||||
|
state.advance();
|
||||||
|
break;
|
||||||
|
|
||||||
|
} else {
|
||||||
|
if (c == '\n') {
|
||||||
|
state.line++;
|
||||||
|
state.column = 1;
|
||||||
|
}
|
||||||
|
sb.append(c);
|
||||||
|
state.advance();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
state.tokens.add(new RawToken(RawTokenType.STRING, sb.toString(), state.line, startColumn));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
package ch.unibas.dmi.dbis.cs108.casono.server.tokenizer;
|
||||||
|
|
||||||
|
/** Exception thrown during tokenization. */
|
||||||
|
public class TokenizerException extends RuntimeException {
|
||||||
|
private final int line;
|
||||||
|
private final int column;
|
||||||
|
|
||||||
|
public TokenizerException(String message, int line, int column) {
|
||||||
|
super(message);
|
||||||
|
this.line = line;
|
||||||
|
this.column = column;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getLine() {
|
||||||
|
return line;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getColumn() {
|
||||||
|
return column;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
package ch.unibas.dmi.dbis.cs108.example;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A simple HelloWorld class.
|
|
||||||
*/
|
|
||||||
public class HelloWorld {
|
|
||||||
|
|
||||||
public static void main(String[] args) {
|
|
||||||
System.out.println("Hello World");
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
package ch.unibas.dmi.dbis.cs108.example.gui.javafx;
|
|
||||||
|
|
||||||
import javafx.application.Application;
|
|
||||||
import javafx.scene.Scene;
|
|
||||||
import javafx.scene.control.Label;
|
|
||||||
import javafx.scene.layout.StackPane;
|
|
||||||
import javafx.stage.Stage;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This is an example JavaFX-Application.
|
|
||||||
*/
|
|
||||||
public class GUI extends Application {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Launching this method will not work on some platforms.
|
|
||||||
* What you should do is to create a separate main class and launch the GUI class from there as is done in {@link Main}
|
|
||||||
*/
|
|
||||||
public static void main(String[] args) {
|
|
||||||
launch(args);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void start(Stage stage) {
|
|
||||||
String javaVersion = System.getProperty("java.version");
|
|
||||||
String javafxVersion = System.getProperty("javafx.version");
|
|
||||||
Label l = new Label("Hello, JavaFX " + javafxVersion + ", running on Java " + javaVersion + ".");
|
|
||||||
Scene scene = new Scene(new StackPane(l), 640, 480);
|
|
||||||
stage.setScene(scene);
|
|
||||||
stage.show();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
package ch.unibas.dmi.dbis.cs108.example.gui.javafx;
|
|
||||||
|
|
||||||
import javafx.application.Application;
|
|
||||||
|
|
||||||
public class Main {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This is simply a wrapper to launch the {@link GUI} class.
|
|
||||||
* The reason this class exists is documented in {@link GUI#main(String[])}
|
|
||||||
*/
|
|
||||||
public static void main(String[] args) {
|
|
||||||
Application.launch(GUI.class, args);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
After Width: | Height: | Size: 1.3 MiB |
|
After Width: | Height: | Size: 4.8 KiB |
|
After Width: | Height: | Size: 3.0 KiB |
|
After Width: | Height: | Size: 194 KiB |
@@ -0,0 +1,25 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<Configuration status="warn">
|
||||||
|
|
||||||
|
<Appenders>
|
||||||
|
<Console name="Console" target="SYSTEM_OUT">
|
||||||
|
<PatternLayout disableAnsi="false" pattern=
|
||||||
|
"%style{%d{HH:mm:ss.SSS}}{bright,black} %highlight{%-8level}{
|
||||||
|
FATAL=bright red,
|
||||||
|
ERROR=red,
|
||||||
|
WARN=yellow,
|
||||||
|
INFO=blue,
|
||||||
|
DEBUG=cyan,
|
||||||
|
TRACE=white
|
||||||
|
} %style{%-20logger{20}}{magenta} %msg%n"
|
||||||
|
/>
|
||||||
|
</Console>
|
||||||
|
</Appenders>
|
||||||
|
|
||||||
|
<Loggers>
|
||||||
|
<Root level="debug">
|
||||||
|
<AppenderRef ref="Console"/>
|
||||||
|
</Root>
|
||||||
|
</Loggers>
|
||||||
|
|
||||||
|
</Configuration>
|
||||||
@@ -0,0 +1,135 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
|
||||||
|
<?import javafx.scene.control.*?>
|
||||||
|
<?import javafx.scene.layout.*?>
|
||||||
|
<?import javafx.geometry.Insets?>
|
||||||
|
<?import javafx.scene.image.*?>
|
||||||
|
|
||||||
|
<!-- Hauptcontainer: Verknüpft die UI mit dem CasinoGameController und lädt casinogameui.css -->
|
||||||
|
<AnchorPane xmlns="http://javafx.com/javafx/21"
|
||||||
|
xmlns:fx="http://javafx.com/fxml/1"
|
||||||
|
fx:controller="ch.unibas.dmi.dbis.cs108.casono.client.ui.gameui.CasinoGameController"
|
||||||
|
styleClass="background"
|
||||||
|
stylesheets="@casinogameui.css">
|
||||||
|
|
||||||
|
<GridPane prefWidth="1200" prefHeight="800" AnchorPane.topAnchor="0" AnchorPane.bottomAnchor="0" AnchorPane.leftAnchor="0" AnchorPane.rightAnchor="0">
|
||||||
|
<columnConstraints>
|
||||||
|
<!-- Tisch-Box: 70% -->
|
||||||
|
<ColumnConstraints percentWidth="70.0" hgrow="ALWAYS" />
|
||||||
|
<!-- Leere-Box: 5% -->
|
||||||
|
<ColumnConstraints percentWidth="5.0" hgrow="ALWAYS" />
|
||||||
|
<!-- Chat-Box: 20% -->
|
||||||
|
<ColumnConstraints percentWidth="25.0" hgrow="ALWAYS" />
|
||||||
|
</columnConstraints>
|
||||||
|
|
||||||
|
<!-- Sorgt dafür, dass diese Zeile den gesamten verfügbaren vertikalen Platz nutzt -->
|
||||||
|
<rowConstraints>
|
||||||
|
<RowConstraints vgrow="ALWAYS" />
|
||||||
|
</rowConstraints>
|
||||||
|
|
||||||
|
<children>
|
||||||
|
<!-- Tisch-Box in Spalte 0 -->
|
||||||
|
<StackPane GridPane.columnIndex="0">
|
||||||
|
<!-- Innenabstand: Hält den Inhalt 10 Pixel von oben/unten und 20 Pixel von den Seiten fern -->
|
||||||
|
<padding>
|
||||||
|
<Insets top="20" right="10" bottom="120" left="20"/>
|
||||||
|
</padding>
|
||||||
|
|
||||||
|
<!-- Zentraler Casinotisch: Zentriert den Inhalt und nutzt durch die maximale Größe (1.7976931348623157E308) das gesamte Fenster -->
|
||||||
|
<VBox fx:id="casinoTable"
|
||||||
|
alignment="CENTER"
|
||||||
|
styleClass="casino-table"
|
||||||
|
spacing="20"
|
||||||
|
onMouseClicked="#onTableClick"
|
||||||
|
maxWidth="Infinity"
|
||||||
|
maxHeight="Infinity">
|
||||||
|
|
||||||
|
<!-- TODO: Platzhalter für die Poker-Spielfläche: Hier werden später dynamisch Karten, Chips und Einsätze der Gegner angezeigt -->
|
||||||
|
<HBox alignment="CENTER" spacing="15">
|
||||||
|
<VBox styleClass="dealer-box" prefWidth="80" prefHeight="120" />
|
||||||
|
<VBox styleClass="dealer-box" prefWidth="80" prefHeight="120" />
|
||||||
|
<VBox styleClass="dealer-box" prefWidth="80" prefHeight="120" />
|
||||||
|
</HBox>
|
||||||
|
|
||||||
|
<VBox alignment="CENTER" spacing="10">
|
||||||
|
<!-- Label mit Logo -->
|
||||||
|
<Label text="CAS0NO" styleClass="table-title">
|
||||||
|
<graphic>
|
||||||
|
<ImageView fitHeight="30.0" preserveRatio="true">
|
||||||
|
<image>
|
||||||
|
<Image url="@/images/logo.png" />
|
||||||
|
</image>
|
||||||
|
</ImageView>
|
||||||
|
</graphic>
|
||||||
|
</Label>
|
||||||
|
|
||||||
|
<!-- TODO: Platzhalter: Wird ersetzt, sobald die GameEngine fertig ist und echte Einsätze verarbeiten kann -->
|
||||||
|
<Label fx:id="welcomeText" text="Setzen Sie Ihren Einsatz" styleClass="table-title" />
|
||||||
|
</VBox>
|
||||||
|
</VBox>
|
||||||
|
</StackPane>
|
||||||
|
|
||||||
|
<!-- Leere-Box in Spalte 1: Schafft Platz zwischen dem Tisch-Box und der Chat-Box -->
|
||||||
|
<VBox GridPane.columnIndex="1"
|
||||||
|
alignment="CENTER"
|
||||||
|
minWidth="100"
|
||||||
|
minHeight="100">
|
||||||
|
<!-- Bleibt leer -->
|
||||||
|
</VBox>
|
||||||
|
|
||||||
|
<!-- TODO: Platzhalter: Chat-Box in Spalte 2: -->
|
||||||
|
<fx:include source="components/chatbox.fxml" GridPane.columnIndex="2"/>
|
||||||
|
|
||||||
|
</children>
|
||||||
|
</GridPane>
|
||||||
|
|
||||||
|
<!-- verschiebbare Taskbar -->
|
||||||
|
<AnchorPane>
|
||||||
|
<fx:include fx:id="taskbarInclude" source="gameuicomponents/taskbar.fxml"/>
|
||||||
|
</AnchorPane>
|
||||||
|
|
||||||
|
<!-- Gegner-Status 1: Eines von drei Panels, das Icon, Name und Kontostand des Mitspielers anzeigt -->
|
||||||
|
<GridPane AnchorPane.leftAnchor="0.0"
|
||||||
|
AnchorPane.rightAnchor="0.0"
|
||||||
|
AnchorPane.topAnchor="50">
|
||||||
|
|
||||||
|
<columnConstraints>
|
||||||
|
<ColumnConstraints percentWidth="5"/>
|
||||||
|
<ColumnConstraints percentWidth="16"/>
|
||||||
|
</columnConstraints>
|
||||||
|
|
||||||
|
<children>
|
||||||
|
<fx:include source="gameuicomponents/playerstatus.fxml" GridPane.columnIndex="1"/>
|
||||||
|
</children>
|
||||||
|
</GridPane>
|
||||||
|
|
||||||
|
<!-- Gegner-Status 2: Eines von drei Panels, das Icon, Name und Kontostand des Mitspielers anzeigt -->
|
||||||
|
<GridPane AnchorPane.leftAnchor="0.0"
|
||||||
|
AnchorPane.rightAnchor="0.0"
|
||||||
|
AnchorPane.topAnchor="20">
|
||||||
|
|
||||||
|
<columnConstraints>
|
||||||
|
<ColumnConstraints percentWidth="30"/>
|
||||||
|
<ColumnConstraints percentWidth="16"/>
|
||||||
|
</columnConstraints>
|
||||||
|
|
||||||
|
<children>
|
||||||
|
<fx:include source="gameuicomponents/playerstatus.fxml" GridPane.columnIndex="1"/>
|
||||||
|
</children>
|
||||||
|
</GridPane>
|
||||||
|
|
||||||
|
<!-- Gegner-Status 3: Eines von drei Panels, das Icon, Name und Kontostand des Mitspielers anzeigt -->
|
||||||
|
<GridPane AnchorPane.leftAnchor="0.0"
|
||||||
|
AnchorPane.rightAnchor="0.0"
|
||||||
|
AnchorPane.topAnchor="50">
|
||||||
|
|
||||||
|
<columnConstraints>
|
||||||
|
<ColumnConstraints percentWidth="55"/>
|
||||||
|
<ColumnConstraints percentWidth="16"/>
|
||||||
|
</columnConstraints>
|
||||||
|
|
||||||
|
<children>
|
||||||
|
<fx:include source="gameuicomponents/playerstatus.fxml" GridPane.columnIndex="1"/>
|
||||||
|
</children>
|
||||||
|
</GridPane>
|
||||||
|
</AnchorPane>
|
||||||
@@ -0,0 +1,160 @@
|
|||||||
|
/* Pixel-Art Font (WICHTIG: Nutze eine Monospace-Schrift für Pixel-Look) */
|
||||||
|
.root {
|
||||||
|
-fx-background-color: #000000;
|
||||||
|
-fx-font-family: "Monospaced", "Courier New";
|
||||||
|
}
|
||||||
|
|
||||||
|
.anchor-pane-bg {
|
||||||
|
-fx-background-color: #ffffff;
|
||||||
|
/* Optional: Bild entfernen oder als Overlay nutzen */
|
||||||
|
-fx-background-image: url("/images/background.png");
|
||||||
|
-fx-background-size: cover;
|
||||||
|
-fx-background-position: center center;
|
||||||
|
-fx-background-repeat: no-repeat;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* DIE SCHWEBENDE INFO-BOX (PIXEL-STYLE) */
|
||||||
|
.floating-chat-box {
|
||||||
|
-fx-background-color: #0d9e3b;
|
||||||
|
/* Pixel-Ränder: Keine Rundung (0px) oder nur sehr kleine Stufen */
|
||||||
|
-fx-background-radius: 58;
|
||||||
|
-fx-border-radius: 50;
|
||||||
|
-fx-border-color: #5c3d10 #3d260a #3d260a #5c3d10;
|
||||||
|
-fx-border-width: 12;
|
||||||
|
-fx-padding: 20;
|
||||||
|
/* Harter Schatten statt weicher Glow */
|
||||||
|
-fx-effect: dropshadow(one-pass-box, rgba(0,0,0,1), 0, 0, 15, 15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-header {
|
||||||
|
-fx-text-fill: #ffffff;
|
||||||
|
-fx-font-size: 22px;
|
||||||
|
-fx-font-weight: bold;
|
||||||
|
-fx-padding: 0 0 10 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-text {
|
||||||
|
-fx-text-fill: #ffffff;
|
||||||
|
-fx-font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* DER OVALE PIXEL-TISCH */
|
||||||
|
.casino-table {
|
||||||
|
-fx-background-color: #0d9e3b; /* Feste Farbe statt Gradient für Pixel-Look */
|
||||||
|
/* Oval durch festen Radius - Pixel-Art-Ovale sind stufig */
|
||||||
|
-fx-background-radius: 2000;
|
||||||
|
-fx-border-radius: 2000;
|
||||||
|
|
||||||
|
/* Dicker, eckiger Holzrand */
|
||||||
|
-fx-border-color: #5c3d10 #3d260a #3d260a #5c3d10;
|
||||||
|
-fx-border-width: 12;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.table-title {
|
||||||
|
-fx-text-fill: #ffffff;
|
||||||
|
-fx-font-size: 28px;
|
||||||
|
-fx-font-weight: bold;
|
||||||
|
-fx-effect: dropshadow(one-pass-box, #000, 0, 0, 3, 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/* Hauptcontainer der Statusbox */
|
||||||
|
.player-status-pane {
|
||||||
|
-fx-background-color: rgb(129, 13, 158);
|
||||||
|
-fx-background-radius: 15;
|
||||||
|
-fx-border-radius: 15;
|
||||||
|
|
||||||
|
/* -fx-border-color: #5c3d10 #3d260a #3d260a #5c3d10;
|
||||||
|
-fx-border-width: 2;
|
||||||
|
-fx-effect: dropshadow(gaussian, rgba(0, 0, 0, 0.5), 10, 0, 0, 4);*/
|
||||||
|
}
|
||||||
|
|
||||||
|
.player-status-pane:hover {
|
||||||
|
-fx-translate-y: -3;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Die zwei inneren Boxen */
|
||||||
|
.status-inner-box-top {
|
||||||
|
-fx-background-color: rgb(13, 93, 158);
|
||||||
|
-fx-background-radius: 19;
|
||||||
|
-fx-border-radius: 15;
|
||||||
|
-fx-padding: 5 10;
|
||||||
|
-fx-border-color: #5c3d10 #3d260a #3d260a #5c3d10;
|
||||||
|
-fx-border-width: 3;
|
||||||
|
-fx-effect: dropshadow(one-pass-box, rgba(0, 0, 0, 0.8), 0, 0, 3, 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-inner-box-midle {
|
||||||
|
-fx-background-color: rgba(13, 158, 59, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-inner-box-bottom {
|
||||||
|
-fx-background-color: rgb(158, 13, 102);
|
||||||
|
-fx-background-radius: 19;
|
||||||
|
-fx-border-radius: 15;
|
||||||
|
-fx-padding: 5 10;
|
||||||
|
-fx-border-color: #5c3d10 #3d260a #3d260a #5c3d10;
|
||||||
|
-fx-border-width: 3;
|
||||||
|
-fx-effect: dropshadow(one-pass-box, rgba(0, 0, 0, 0.8), 0, 0, 3, 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-title {
|
||||||
|
/* Titel-Text anpassen */
|
||||||
|
-fx-text-fill: #ffffff;
|
||||||
|
-fx-font-size: 32px;
|
||||||
|
-fx-font-weight: bold;
|
||||||
|
-fx-effect: dropshadow(one-pass-box, #000, 0, 0, 3, 3);
|
||||||
|
-fx-padding: 10 0 0 0; /* Abstand nach oben */
|
||||||
|
}
|
||||||
|
|
||||||
|
.green-box {
|
||||||
|
/* Grüne Box anpassen */
|
||||||
|
-fx-fill: #4CAF50;
|
||||||
|
-fx-stroke: #FFFFFF;
|
||||||
|
-fx-stroke-width: 3;
|
||||||
|
-fx-effect: dropshadow(one-pass-box, #000, 0, 0, 10, 10);
|
||||||
|
-fx-padding: 0 0 40 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* EXIT BUTTON */
|
||||||
|
.button-exit {
|
||||||
|
-fx-background-radius: 12;
|
||||||
|
-fx-border-radius: 12;
|
||||||
|
-fx-font-family: "Courier New";
|
||||||
|
-fx-font-weight: bold;
|
||||||
|
-fx-border-width: 2;
|
||||||
|
-fx-padding: 6 15;
|
||||||
|
-fx-cursor: hand;
|
||||||
|
-fx-background-color: #333333;
|
||||||
|
-fx-border-color: #666666;
|
||||||
|
-fx-text-fill: #CCCCCC;
|
||||||
|
}
|
||||||
|
.button-exit:hover {
|
||||||
|
-fx-translate-y: -3;
|
||||||
|
-fx-effect: dropshadow(one-pass-box, rgba(0, 0, 0, 0.8), 0, 0, 3, 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* LOBBY ERSTELLEN BUTTON */
|
||||||
|
.button-create-lobby {
|
||||||
|
-fx-background-radius: 12;
|
||||||
|
-fx-border-radius: 12;
|
||||||
|
-fx-font-family: "Courier New";
|
||||||
|
-fx-font-weight: bold;
|
||||||
|
-fx-border-width: 2;
|
||||||
|
-fx-padding: 6 15;
|
||||||
|
-fx-cursor: hand;
|
||||||
|
-fx-background-color: #1a7f2e;
|
||||||
|
-fx-border-color: #2ecc71;
|
||||||
|
-fx-text-fill: #ffffff;
|
||||||
|
}
|
||||||
|
.button-create-lobby:hover {
|
||||||
|
-fx-translate-y: -3;
|
||||||
|
-fx-effect: dropshadow(one-pass-box, rgba(0, 0, 0, 0.8), 0, 0, 3, 3);
|
||||||
|
-fx-background-color: #27ae60;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,125 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<?import javafx.scene.image.ImageView?>
|
||||||
|
<?import javafx.scene.control.*?>
|
||||||
|
<?import javafx.scene.layout.*?>
|
||||||
|
<?import javafx.geometry.Insets?>
|
||||||
|
|
||||||
|
<AnchorPane xmlns="http://javafx.com"
|
||||||
|
xmlns:fx="http://javafx.com"
|
||||||
|
fx:controller="ch.unibas.dmi.dbis.cs108.casono.client.ui.lobbyui.CasinomainuiController"
|
||||||
|
styleClass="anchor-pane-bg"
|
||||||
|
stylesheets="@Casinomainui.css">
|
||||||
|
|
||||||
|
<GridPane prefWidth="1200" prefHeight="800" AnchorPane.topAnchor="0" AnchorPane.bottomAnchor="0" AnchorPane.leftAnchor="0" AnchorPane.rightAnchor="0">
|
||||||
|
<columnConstraints>
|
||||||
|
<!-- Tisch bekommt 75% vom Fenster -->
|
||||||
|
<ColumnConstraints percentWidth="70.0" hgrow="ALWAYS" />
|
||||||
|
<!-- Info-Box bekommt 25% vom Fenster inkl. 5 prozent buffer -->
|
||||||
|
<ColumnConstraints percentWidth="5.0" hgrow="ALWAYS" />
|
||||||
|
<ColumnConstraints percentWidth="25.0" hgrow="ALWAYS" />
|
||||||
|
</columnConstraints>
|
||||||
|
|
||||||
|
<rowConstraints>
|
||||||
|
<RowConstraints vgrow="ALWAYS" />
|
||||||
|
</rowConstraints>
|
||||||
|
|
||||||
|
<children>
|
||||||
|
<!-- LINKER BEREICH (Tisch) -->
|
||||||
|
<GridPane GridPane.columnIndex="0" style="-fx-background-color: transparent;" alignment="CENTER">
|
||||||
|
<rowConstraints>
|
||||||
|
<RowConstraints percentHeight="25.0" vgrow="ALWAYS" />
|
||||||
|
<RowConstraints percentHeight="75.0" vgrow="ALWAYS" />
|
||||||
|
</rowConstraints>
|
||||||
|
|
||||||
|
<columnConstraints>
|
||||||
|
<ColumnConstraints hgrow="ALWAYS" />
|
||||||
|
</columnConstraints>
|
||||||
|
|
||||||
|
<children>
|
||||||
|
<HBox alignment="CENTER" spacing="20"
|
||||||
|
maxWidth="Infinity"
|
||||||
|
GridPane.rowIndex="0"
|
||||||
|
GridPane.columnIndex="0"
|
||||||
|
GridPane.halignment="CENTER">
|
||||||
|
<ImageView fx:id="logoView"
|
||||||
|
fitHeight="96"
|
||||||
|
fitWidth="96"
|
||||||
|
preserveRatio="true" />
|
||||||
|
<VBox alignment="CENTER" spacing="5">
|
||||||
|
<Label text="placeholder text wird in controller ueberschriebeen"
|
||||||
|
styleClass="table-title"
|
||||||
|
fx:id="titleLabel"
|
||||||
|
alignment="CENTER" />
|
||||||
|
<Label text="placeholder text wird in controller ueberschriebeen"
|
||||||
|
styleClass="table-title"
|
||||||
|
fx:id="subtitleLabel"
|
||||||
|
alignment="CENTER" />
|
||||||
|
</VBox>
|
||||||
|
</HBox>
|
||||||
|
<Button text="EXIT"
|
||||||
|
styleClass="button-exit"
|
||||||
|
fx:id="exitbutton"
|
||||||
|
onAction="#handleexitbutton"
|
||||||
|
GridPane.rowIndex="0"
|
||||||
|
GridPane.columnIndex="0"
|
||||||
|
GridPane.halignment="LEFT"
|
||||||
|
GridPane.valignment="TOP">
|
||||||
|
<GridPane.margin>
|
||||||
|
<Insets top="20" left="20" />
|
||||||
|
</GridPane.margin>
|
||||||
|
</Button>
|
||||||
|
<Button text="Lobby erstellen"
|
||||||
|
styleClass="button-create-lobby"
|
||||||
|
fx:id="createLobbyButton"
|
||||||
|
onAction="#handleCreateLobbyButton"
|
||||||
|
GridPane.rowIndex="0"
|
||||||
|
GridPane.columnIndex="0"
|
||||||
|
GridPane.halignment="LEFT"
|
||||||
|
GridPane.valignment="TOP">
|
||||||
|
<GridPane.margin>
|
||||||
|
<Insets top="60" left="20" />
|
||||||
|
</GridPane.margin>
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<VBox fx:id="casinoTable"
|
||||||
|
alignment="CENTER"
|
||||||
|
styleClass="casino-table"
|
||||||
|
spacing="20"
|
||||||
|
maxWidth="Infinity"
|
||||||
|
maxHeight="Infinity"
|
||||||
|
GridPane.rowIndex="1"
|
||||||
|
GridPane.columnIndex="0"
|
||||||
|
GridPane.halignment="CENTER">
|
||||||
|
<GridPane.margin>
|
||||||
|
<Insets left="20" bottom="20" />
|
||||||
|
</GridPane.margin>
|
||||||
|
</VBox>
|
||||||
|
</children>
|
||||||
|
</GridPane>
|
||||||
|
|
||||||
|
|
||||||
|
<VBox GridPane.columnIndex="1"
|
||||||
|
alignment="CENTER"
|
||||||
|
minWidth="100"
|
||||||
|
minHeight="100">
|
||||||
|
<!-- Bleibt leer -->
|
||||||
|
</VBox>
|
||||||
|
|
||||||
|
<!-- RECHTER BEREICH (Info-Box) -->
|
||||||
|
<VBox GridPane.columnIndex="2" alignment="CENTER">
|
||||||
|
<padding>
|
||||||
|
<Insets top="30" right="30" bottom="30" left="10"/>
|
||||||
|
</padding>
|
||||||
|
<VBox VBox.vgrow="ALWAYS" styleClass="floating-chat-box" spacing="10" maxWidth="Infinity">
|
||||||
|
<Label text="== INFO ==" styleClass="chat-header" alignment="CENTER" maxWidth="Infinity"/>
|
||||||
|
<Region minHeight="4" style="-fx-background-color: #ffffff;" />
|
||||||
|
<VBox spacing="15" VBox.vgrow="ALWAYS" styleClass="info-content">
|
||||||
|
<Label text="> CREDITS: 1000" styleClass="info-text"/>
|
||||||
|
<Label text="> JACKPOT: 9999" styleClass="info-text"/>
|
||||||
|
<Label text="> SYSTEM: OK" styleClass="info-text"/>
|
||||||
|
</VBox>
|
||||||
|
</VBox>
|
||||||
|
</VBox>
|
||||||
|
</children>
|
||||||
|
</GridPane>
|
||||||
|
</AnchorPane>
|
||||||
@@ -0,0 +1,251 @@
|
|||||||
|
.root {
|
||||||
|
-fx-background-color: #000000;
|
||||||
|
-fx-font-family: "Monospaced", "Courier New"; /* Schriftart */
|
||||||
|
}
|
||||||
|
|
||||||
|
.background {
|
||||||
|
-fx-background-image: url("/images/background.png");
|
||||||
|
-fx-background-size: cover; /* Bild füllt das ganze Fenster aus */
|
||||||
|
/* Zentriert das Bild und verhindert, dass es sich kachelartig wiederholt */
|
||||||
|
-fx-background-position: center center;
|
||||||
|
-fx-background-repeat: no-repeat;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-text {
|
||||||
|
-fx-text-fill: #ffffff;
|
||||||
|
-fx-font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.casino-table {
|
||||||
|
-fx-background-color: #0d9e3b;
|
||||||
|
-fx-background-radius: 210;
|
||||||
|
-fx-border-radius: 200;
|
||||||
|
-fx-border-color: #5c3d10 #3d260a #3d260a #5c3d10;
|
||||||
|
-fx-border-width: 12;
|
||||||
|
/*-fx-min-height: 60vh;*/
|
||||||
|
/*-fx-max-height: 60vh;*/
|
||||||
|
/*-fx-min-width: 90vh;*/
|
||||||
|
/*-fx-max-width: 90vh;*/
|
||||||
|
-fx-effect: dropshadow(one-pass-box, rgba(0,0,0,0.8), 0, 0, 15, 15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dealer-box {
|
||||||
|
-fx-background-color: #ffffff;
|
||||||
|
-fx-border-color: #000000;
|
||||||
|
-fx-border-width: 3;
|
||||||
|
-fx-background-radius: 0;
|
||||||
|
-fx-border-radius: 0;
|
||||||
|
-fx-min-width: 70;
|
||||||
|
-fx-min-height: 100;
|
||||||
|
-fx-effect: dropshadow(one-pass-box, rgba(0,0,0,0.8), 0, 0, 4, 4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-title {
|
||||||
|
-fx-text-fill: #ffffff;
|
||||||
|
-fx-font-size: 28px;
|
||||||
|
-fx-font-weight: bold;
|
||||||
|
-fx-effect: dropshadow(one-pass-box, #000000, 0, 0, 3, 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.taskbar {
|
||||||
|
-fx-background-color: rgba(13, 158, 59);
|
||||||
|
-fx-background-radius: 23;
|
||||||
|
-fx-border-radius: 20;
|
||||||
|
-fx-border-color: #5c3d10 #3d260a #3d260a #5c3d10;
|
||||||
|
-fx-border-width: 3;
|
||||||
|
-fx-padding: 10 25;
|
||||||
|
-fx-cursor: move;
|
||||||
|
-fx-effect: dropshadow(one-pass-box, rgba(0,0,0,0.8), 0, 0, 5, 5);
|
||||||
|
transition: all 0.2s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.taskbar:hover {
|
||||||
|
-fx-border-color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.black-input-field {
|
||||||
|
-fx-background-color: #1a1a1a;
|
||||||
|
-fx-text-fill: #ffffff;
|
||||||
|
-fx-font-family: "Courier New";
|
||||||
|
-fx-font-weight: bold;
|
||||||
|
-fx-background-radius: 8;
|
||||||
|
-fx-border-radius: 8;
|
||||||
|
-fx-border-color: #444444;
|
||||||
|
-fx-border-width: 2;
|
||||||
|
-fx-padding: 5 12;
|
||||||
|
}
|
||||||
|
|
||||||
|
.black-input-field:hover {
|
||||||
|
-fx-translate-y: -3;
|
||||||
|
-fx-effect: dropshadow(one-pass-box, rgba(0, 0, 0, 0.8), 0, 0, 3, 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.black-input-field:focused {
|
||||||
|
-fx-border-color: #ffffff;
|
||||||
|
-fx-background-color: #000000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gray-input-field {
|
||||||
|
-fx-background-color: #333333;
|
||||||
|
-fx-text-fill: #ffffff;
|
||||||
|
-fx-font-family: "Courier New";
|
||||||
|
-fx-font-weight: bold;
|
||||||
|
-fx-background-radius: 8;
|
||||||
|
-fx-border-radius: 8;
|
||||||
|
-fx-border-color: #666666;
|
||||||
|
-fx-color-color: #cccccc;
|
||||||
|
-fx-border-width: 2;
|
||||||
|
-fx-padding: 5 12;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gray-input-field:hover {
|
||||||
|
-fx-translate-y: -3;
|
||||||
|
-fx-effect: dropshadow(one-pass-box, rgba(0, 0, 0, 0.8), 0, 0, 3, 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.gray-input-field:focused {
|
||||||
|
-fx-border-color: #ffffff;
|
||||||
|
-fx-background-color: #333333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.yellow-button {
|
||||||
|
-fx-background-color: #b8860b;
|
||||||
|
-fx-border-color: #ffd700;
|
||||||
|
-fx-text-fill: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.red-button {
|
||||||
|
-fx-background-color: #8b0000;
|
||||||
|
-fx-border-color: #ff4444;
|
||||||
|
-fx-text-fill: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gray-button {
|
||||||
|
-fx-background-color: #333333;
|
||||||
|
-fx-border-color: #666666;
|
||||||
|
-fx-text-fill: #cccccc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gray-button, .yellow-button, .red-button {
|
||||||
|
-fx-background-radius: 12;
|
||||||
|
-fx-border-radius: 12;
|
||||||
|
-fx-font-family: "Courier New";
|
||||||
|
-fx-font-weight: bold;
|
||||||
|
-fx-border-width: 2;
|
||||||
|
-fx-padding: 6 15;
|
||||||
|
-fx-cursor: hand;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gray-button:hover, .yellow-button:hover, .red-button:hover {
|
||||||
|
-fx-translate-y: -3;
|
||||||
|
-fx-effect: dropshadow(one-pass-box, rgba(0, 0, 0, 0.8), 0, 0, 3, 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.player-status-pane {
|
||||||
|
-fx-background-color: rgba(13, 158, 59, 0);
|
||||||
|
-fx-background-radius: 15;
|
||||||
|
-fx-border-radius: 15;
|
||||||
|
/* -fx-border-color: #5c3d10 #3d260a #3d260a #5c3d10;
|
||||||
|
-fx-border-width: 2;
|
||||||
|
-fx-effect: dropshadow(gaussian, rgba(0, 0, 0, 0.5), 10, 0, 0, 4);*/
|
||||||
|
}
|
||||||
|
|
||||||
|
.player-status-pane:hover {
|
||||||
|
-fx-translate-y: -3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-inner-box-top {
|
||||||
|
-fx-background-color: rgba(13, 158, 59);
|
||||||
|
-fx-background-radius: 19;
|
||||||
|
-fx-border-radius: 15;
|
||||||
|
-fx-padding: 5 10;
|
||||||
|
-fx-border-color: #5c3d10 #3d260a #3d260a #5c3d10;
|
||||||
|
-fx-border-width: 3;
|
||||||
|
-fx-effect: dropshadow(one-pass-box, rgba(0, 0, 0, 0.8), 0, 0, 3, 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-inner-box-midle {
|
||||||
|
-fx-background-color: rgba(13, 158, 59, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-inner-box-bottom {
|
||||||
|
-fx-background-color: rgba(13, 158, 59);
|
||||||
|
-fx-background-radius: 19;
|
||||||
|
-fx-border-radius: 15;
|
||||||
|
-fx-padding: 5 10;
|
||||||
|
-fx-border-color: #5c3d10 #3d260a #3d260a #5c3d10;
|
||||||
|
-fx-border-width: 3;
|
||||||
|
-fx-effect: dropshadow(one-pass-box, rgba(0, 0, 0, 0.8), 0, 0, 3, 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-label-small {
|
||||||
|
-fx-text-fill: #ffffff;
|
||||||
|
-fx-font-family: "Monospaced"; /* Schriftart */
|
||||||
|
-fx-font-size: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-value-text {
|
||||||
|
-fx-text-fill: #ffffff;
|
||||||
|
-fx-font-family: "Monospaced"; /* Schriftart */
|
||||||
|
-fx-font-size: 15px;
|
||||||
|
-fx-font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-value-money {
|
||||||
|
-fx-text-fill: #ffffff;
|
||||||
|
-fx-font-family: "Monospaced"; /* Schriftart */
|
||||||
|
-fx-font-size: 15px;
|
||||||
|
-fx-font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-circle {
|
||||||
|
-fx-background-color: #000000;
|
||||||
|
-fx-background-radius: 50;
|
||||||
|
-fx-border-color: #ffffff;
|
||||||
|
-fx-border-width: 3;
|
||||||
|
-fx-border-radius: 50;
|
||||||
|
}
|
||||||
|
|
||||||
|
.browser-root {
|
||||||
|
-fx-background-image: url("/images/background.png");
|
||||||
|
-fx-background-size: cover;
|
||||||
|
-fx-background-position: center;
|
||||||
|
-fx-background-color: #0b2d15;
|
||||||
|
}
|
||||||
|
|
||||||
|
.browser-web-view {
|
||||||
|
-fx-background-color: rgb(13, 158, 59);
|
||||||
|
-fx-background-radius: 23;
|
||||||
|
-fx-border-radius: 20;
|
||||||
|
-fx-border-color: #5c3d10 #3d260a #3d260a #5c3d10;
|
||||||
|
-fx-border-width: 3;
|
||||||
|
-fx-padding: 0;
|
||||||
|
-fx-cursor: default;
|
||||||
|
-fx-effect: dropshadow(one-pass-box, rgba(0,0,0,0.8), 0, 0, 5, 5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.web-view {
|
||||||
|
-fx-background-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.taskbar-browser {
|
||||||
|
-fx-background-color: rgb(13, 158, 59);
|
||||||
|
-fx-background-radius: 23;
|
||||||
|
-fx-border-radius: 20;
|
||||||
|
-fx-border-color: #5c3d10 #3d260a #3d260a #5c3d10;
|
||||||
|
-fx-border-width: 3;
|
||||||
|
-fx-padding: 10 25;
|
||||||
|
-fx-cursor: default;
|
||||||
|
-fx-effect: dropshadow(one-pass-box, rgba(0,0,0,0.8), 0, 0, 5, 5);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*.taskbar-browser:hover {*/
|
||||||
|
/* -fx-border-color: #ffffff;*/
|
||||||
|
/*}*/
|
||||||
|
|
||||||
|
.security-label {
|
||||||
|
-fx-text-fill: #000000;
|
||||||
|
-fx-font-weight: bold;
|
||||||
|
-fx-font-size: 16px;
|
||||||
|
}
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
|
||||||
|
<?import javafx.scene.control.*?>
|
||||||
|
<?import javafx.scene.layout.*?>
|
||||||
|
<?import javafx.geometry.Insets?>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
Dieses Fenster wird später mit den Serveranfragen verknüpft,
|
||||||
|
um Nachrichten von Mitspielern und Systemmeldungen in Echtzeit anzuzeigen.
|
||||||
|
|
||||||
|
Der Chat unterscheidet intern zwischen:
|
||||||
|
- Player-to-Player (Privater 2-Personen-Chat)
|
||||||
|
- Lobby-Chat (Aktueller Raum)
|
||||||
|
- Globaler Chat (Gesamter Server)
|
||||||
|
|
||||||
|
Features, die später implementiert werden sollen:
|
||||||
|
- Schicke Chat-Bubbles mit Namen und Uhrzeit.
|
||||||
|
-->
|
||||||
|
|
||||||
|
<VBox xmlns="http://javafx.com/javafx/21"
|
||||||
|
xmlns:fx="http://javafx.com/fxml/1"
|
||||||
|
fx:controller="ch.unibas.dmi.dbis.cs108.casono.client.ui.chatui.ChatController"
|
||||||
|
alignment="CENTER"
|
||||||
|
stylesheets="@chatui.css">
|
||||||
|
|
||||||
|
<!-- Innenabstand: Schafft oben, rechts und unten 30 Pixel Platz, links nur 10 Pixel (asymmetrisch) -->
|
||||||
|
<padding>
|
||||||
|
<Insets top="30" right="30" bottom="30" left="10"/>
|
||||||
|
</padding>
|
||||||
|
|
||||||
|
<children>
|
||||||
|
<VBox VBox.vgrow="ALWAYS"
|
||||||
|
styleClass="chat-box"
|
||||||
|
spacing="10"
|
||||||
|
maxWidth="Infinity">
|
||||||
|
|
||||||
|
<children>
|
||||||
|
<Label text="== CHAT ==" styleClass="chat-header" alignment="CENTER" maxWidth="Infinity"/>
|
||||||
|
<Region minHeight="4" styleClass="chat-separator"/>
|
||||||
|
|
||||||
|
<!-- Chatnachrichten werden hier hinzugefügt -->
|
||||||
|
<ScrollPane fx:id="chatScrollPane" fitToWidth="true" vbarPolicy="AS_NEEDED" hbarPolicy="NEVER" VBox.vgrow="ALWAYS" styleClass="chat-scroll-pane">
|
||||||
|
<content>
|
||||||
|
<VBox fx:id="chatVBox" spacing="10" styleClass="chat-VBox"/>
|
||||||
|
</content>
|
||||||
|
</ScrollPane>
|
||||||
|
|
||||||
|
<!-- Eingabebereich -->
|
||||||
|
<HBox spacing="10">
|
||||||
|
<!-- TODO: Größe des TextFields dynamisch anpassen -->
|
||||||
|
<TextField fx:id="inputField" HBox.hgrow="ALWAYS" promptText="Nachricht eingeben..." styleClass="gray-input-field" onAction="#sendMessage"/>
|
||||||
|
<Button fx:id="sendButton" text="SENDEN" onAction="#sendMessage" styleClass="yellow-button"/>
|
||||||
|
</HBox>
|
||||||
|
</children>
|
||||||
|
</VBox>
|
||||||
|
</children>
|
||||||
|
</VBox>
|
||||||
@@ -0,0 +1,155 @@
|
|||||||
|
.chat-box {
|
||||||
|
-fx-background-color: #0d9e3b;
|
||||||
|
-fx-background-radius: 58;
|
||||||
|
-fx-border-radius: 50;
|
||||||
|
-fx-border-color: #5c3d10 #3d260a #3d260a #5c3d10;
|
||||||
|
-fx-border-width: 12;
|
||||||
|
-fx-padding: 20;
|
||||||
|
-fx-effect: dropshadow(one-pass-box, rgba(0,0,0,1), 0, 0, 15, 15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-header {
|
||||||
|
-fx-text-fill: #ffffff;
|
||||||
|
-fx-font-size: 22px;
|
||||||
|
-fx-font-weight: bold;
|
||||||
|
-fx-padding: 0 0 10 0;
|
||||||
|
-fx-effect: dropshadow(one-pass-box, #000000, 0, 0, 3, 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-separator {
|
||||||
|
-fx-background-color: #ffffff;
|
||||||
|
-fx-min-height: 4px;
|
||||||
|
-fx-effect: dropshadow(one-pass-box, #000000, 0, 0, 3, 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.black-input-field {
|
||||||
|
-fx-background-color: #1a1a1a;
|
||||||
|
-fx-text-fill: #ffffff;
|
||||||
|
-fx-font-family: "Courier New";
|
||||||
|
-fx-font-weight: bold;
|
||||||
|
-fx-background-radius: 8;
|
||||||
|
-fx-border-radius: 8;
|
||||||
|
-fx-border-color: #444444;
|
||||||
|
-fx-border-width: 2;
|
||||||
|
-fx-padding: 5 12;
|
||||||
|
}
|
||||||
|
|
||||||
|
.black-input-field:hover {
|
||||||
|
-fx-translate-y: -3;
|
||||||
|
-fx-effect: dropshadow(one-pass-box, rgba(0, 0, 0, 0.8), 0, 0, 3, 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.black-input-field:focused {
|
||||||
|
-fx-border-color: #ffffff;
|
||||||
|
-fx-background-color: #000000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gray-input-field {
|
||||||
|
-fx-background-color: #333333;
|
||||||
|
-fx-text-fill: #ffffff;
|
||||||
|
-fx-font-family: "Courier New";
|
||||||
|
-fx-font-weight: bold;
|
||||||
|
-fx-background-radius: 8;
|
||||||
|
-fx-border-radius: 8;
|
||||||
|
-fx-border-color: #666666;
|
||||||
|
-fx-color-color: #cccccc;
|
||||||
|
-fx-border-width: 2;
|
||||||
|
-fx-padding: 5 12;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gray-input-field:hover {
|
||||||
|
-fx-translate-y: -3;
|
||||||
|
-fx-effect: dropshadow(one-pass-box, rgba(0, 0, 0, 0.8), 0, 0, 3, 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.gray-input-field:focused {
|
||||||
|
-fx-border-color: #ffffff;
|
||||||
|
-fx-background-color: #333333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.yellow-button {
|
||||||
|
-fx-background-color: #b8860b;
|
||||||
|
-fx-border-color: #ffd700;
|
||||||
|
-fx-text-fill: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.red-button {
|
||||||
|
-fx-background-color: #8b0000;
|
||||||
|
-fx-border-color: #ff4444;
|
||||||
|
-fx-text-fill: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gray-button {
|
||||||
|
-fx-background-color: #333333;
|
||||||
|
-fx-border-color: #666666;
|
||||||
|
-fx-text-fill: #cccccc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gray-button, .yellow-button, .red-button {
|
||||||
|
-fx-background-radius: 12;
|
||||||
|
-fx-border-radius: 12;
|
||||||
|
-fx-font-family: "Courier New";
|
||||||
|
-fx-font-weight: bold;
|
||||||
|
-fx-border-width: 2;
|
||||||
|
-fx-padding: 6 15;
|
||||||
|
-fx-cursor: hand;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gray-button:hover, .yellow-button:hover, .red-button:hover {
|
||||||
|
-fx-translate-y: -3;
|
||||||
|
-fx-effect: dropshadow(one-pass-box, rgba(0, 0, 0, 0.8), 0, 0, 3, 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-scroll-pane {
|
||||||
|
-fx-background-color: transparent;
|
||||||
|
-fx-background: transparent;
|
||||||
|
-fx-border-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-VBox {
|
||||||
|
-fx-background-color: transparent;
|
||||||
|
-fx-background: transparent;
|
||||||
|
}
|
||||||
|
.scroll-pane {
|
||||||
|
-fx-background-color: transparent;
|
||||||
|
-fx-border-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scroll-pane .scroll-bar:vertical {
|
||||||
|
-fx-background-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scroll-pane .scroll-bar .track {
|
||||||
|
-fx-background-color: #5c3d10;
|
||||||
|
-fx-background-insets: 0;
|
||||||
|
-fx-background-radius: 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scroll-pane .scroll-bar .thumb {
|
||||||
|
-fx-background-color: #3d260a;
|
||||||
|
-fx-background-insets: 0;
|
||||||
|
-fx-background-radius: 5;
|
||||||
|
-fx-pref-width: 6;
|
||||||
|
-fx-pref-height: 6;
|
||||||
|
-fx-padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scroll-pane .scroll-bar:horizontal {
|
||||||
|
-fx-background-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scroll-pane .scroll-bar:horizontal .track {
|
||||||
|
-fx-background-color: #5c3d10;
|
||||||
|
-fx-background-radius: 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scroll-pane .scroll-bar:horizontal .thumb {
|
||||||
|
-fx-background-color: #3d260a;
|
||||||
|
-fx-background-radius: 5;
|
||||||
|
-fx-pref-height: 6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scroll-pane .scroll-bar:horizontal .thumb:hover {
|
||||||
|
-fx-pref-height: 10;
|
||||||
|
}
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
|
||||||
|
<?import javafx.scene.control.*?>
|
||||||
|
<?import javafx.scene.layout.*?>
|
||||||
|
<?import javafx.geometry.Insets?>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
TODO: Haupt-platzhalter für spieler-status & eingabe.
|
||||||
|
Dieses Layout dient aktuell der visuellen Struktur. Sobald die GameEngine und
|
||||||
|
Serveranfragen (API/Requests) durchgeführt werden können, werden Name, Kontostand
|
||||||
|
und Status-Anzeigen dynamisch mit Echtzeit-Daten vom Server befüllt.
|
||||||
|
-->
|
||||||
|
|
||||||
|
<VBox fx:id="playerStatusBox"
|
||||||
|
xmlns="http://javafx.com/javafx/21"
|
||||||
|
xmlns:fx="http://javafx.com/fxml/1"
|
||||||
|
styleClass="player-status-pane"
|
||||||
|
prefWidth="200"
|
||||||
|
prefHeight="100"
|
||||||
|
spacing="0">
|
||||||
|
|
||||||
|
<children>
|
||||||
|
<!-- Bereich für den Spielernamen -->
|
||||||
|
<HBox styleClass="status-inner-box-top"
|
||||||
|
alignment="CENTER_LEFT"
|
||||||
|
maxWidth="Infinity"
|
||||||
|
prefHeight="60">
|
||||||
|
|
||||||
|
<padding>
|
||||||
|
<Insets left="15"/>
|
||||||
|
</padding>
|
||||||
|
|
||||||
|
<Label text="NAME: " styleClass="status-label-small"/>
|
||||||
|
<!-- TODO: fx:id="playerName": Wird später durch den User-Namen aus der Server-Abfrage ersetzt -->
|
||||||
|
<Label fx:id="playerName" text="SPIELER" styleClass="status-value-text"/>
|
||||||
|
</HBox>
|
||||||
|
|
||||||
|
<!-- Schaft platz zwischen den Boxen -->
|
||||||
|
<HBox styleClass="status-inner-box-midle"
|
||||||
|
alignment="CENTER_LEFT"
|
||||||
|
maxWidth="Infinity"
|
||||||
|
prefHeight="10">
|
||||||
|
|
||||||
|
<padding>
|
||||||
|
<Insets left="15"/>
|
||||||
|
</padding>
|
||||||
|
</HBox>
|
||||||
|
|
||||||
|
<!-- Anzeige des Kapitals/Geldstands -->
|
||||||
|
<HBox styleClass="status-inner-box-bottom"
|
||||||
|
alignment="CENTER_LEFT"
|
||||||
|
maxWidth="Infinity"
|
||||||
|
prefHeight="30">
|
||||||
|
|
||||||
|
<padding>
|
||||||
|
<Insets left="15"/>
|
||||||
|
</padding>
|
||||||
|
|
||||||
|
<Label text="GELD: " styleClass="status-label-small"/>
|
||||||
|
<Label fx:id="playerMoney" text="0 $" styleClass="status-value-money"/>
|
||||||
|
</HBox>
|
||||||
|
|
||||||
|
<!-- TODO: User-Logo oder Icon: Platzhalter für das Profilbild oder das individuelle Logo des Spielers -->
|
||||||
|
<Pane prefHeight="0" maxWidth="Infinity">
|
||||||
|
<Region styleClass="status-circle"
|
||||||
|
layoutX="-10"
|
||||||
|
layoutY="-105"
|
||||||
|
prefWidth="30"
|
||||||
|
prefHeight="30"/>
|
||||||
|
</Pane>
|
||||||
|
</children>
|
||||||
|
</VBox>
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
|
||||||
|
<?import javafx.scene.control.*?>
|
||||||
|
<?import javafx.scene.layout.*?>
|
||||||
|
<?import javafx.geometry.Insets?>
|
||||||
|
|
||||||
|
<!-- verschiebbare Taskbar -->
|
||||||
|
<HBox xmlns="http://javafx.com/javafx/21"
|
||||||
|
xmlns:fx="http://javafx.com/fxml/1"
|
||||||
|
fx:controller="ch.unibas.dmi.dbis.cs108.casono.client.ui.gameui.gameuicomponents.TaskbarController"
|
||||||
|
fx:id="taskbar"
|
||||||
|
styleClass="taskbar"
|
||||||
|
spacing="15"
|
||||||
|
alignment="CENTER"
|
||||||
|
onMousePressed="#onTaskbarPressed"
|
||||||
|
onMouseDragged="#onTaskbarDragged"
|
||||||
|
onMouseReleased="#onTaskbarReleased"
|
||||||
|
layoutX="50.0" layoutY="710.0">
|
||||||
|
|
||||||
|
<!-- Platzhalter-Buttons -->
|
||||||
|
<Button text="EINSTELLUNGEN" styleClass="gray-button" />
|
||||||
|
<Button text="INFO" styleClass="gray-button" />
|
||||||
|
<!-- Buten für den Casono Browser -->
|
||||||
|
<Button text="HILFE" onAction="#onBrowserButtonClick" styleClass="gray-button" />
|
||||||
|
|
||||||
|
<!-- Vertikale-Trennlinie: Erzeugt eine optische Abgrenzung zwischen zwei Butten-Bereichen -->
|
||||||
|
<Separator orientation="VERTICAL" prefHeight="20" opacity="0.3" />
|
||||||
|
|
||||||
|
<!-- Eingabefeld: Reagiert auf die Enter-Taste, um den Einsatz zu bestätigen -->
|
||||||
|
<TextField fx:id="taskbarInput"
|
||||||
|
promptText="Betrag setzen..."
|
||||||
|
onKeyPressed="#onInputSubmitted"
|
||||||
|
styleClass="gray-input-field"
|
||||||
|
prefWidth="150" />
|
||||||
|
|
||||||
|
<!-- Bestätigungs-Button: Alternative zum Enter-Key, um den Einsatz abzusenden -->
|
||||||
|
<Button text="SETZTEN"
|
||||||
|
onAction="#onInputSubmittedAction"
|
||||||
|
styleClass="yellow-button"/>
|
||||||
|
|
||||||
|
<!-- Vertikale-Trennlinie: Erzeugt eine optische Abgrenzung zwischen zwei Butten-Bereichen -->
|
||||||
|
<Separator orientation="VERTICAL" prefHeight="20" opacity="0.3" />
|
||||||
|
|
||||||
|
<!-- Menü-Button: Bricht das aktuelle Spiel ab und kehrt zum Hauptmenü zurück -->
|
||||||
|
<Button text="VERLASSEN"
|
||||||
|
onAction="#onExitButtonClick"
|
||||||
|
styleClass="red-button"/>
|
||||||
|
|
||||||
|
<!-- Innenabstand: Hält den Inhalt 10 Pixel von oben/unten und 20 Pixel von den Seiten fern -->
|
||||||
|
<padding>
|
||||||
|
<Insets top="10" right="20" bottom="10" left="20"/>
|
||||||
|
</padding>
|
||||||
|
</HBox>
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"1": 1240,
|
||||||
|
"2": 3640,
|
||||||
|
"3": 7589,
|
||||||
|
"4": 3982,
|
||||||
|
"5": 5655,
|
||||||
|
"6": 6418,
|
||||||
|
"7": 8586,
|
||||||
|
"8": 7378
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
|
||||||
|
package ch.unibas.dmi.dbis.cs108.casono.client.ui.lobbyui;
|
||||||
|
|
||||||
|
import javafx.scene.layout.GridPane;
|
||||||
|
import org.junit.jupiter.api.*;
|
||||||
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
|
||||||
|
class LobbyButtonGridManagerTest {
|
||||||
|
LobbyButtonGridManager gridManager;
|
||||||
|
LobbyButtonTranslationManager translationManager;
|
||||||
|
GridPane gridPane;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setUp() {
|
||||||
|
gridPane = new GridPane();
|
||||||
|
translationManager = new LobbyButtonTranslationManager();
|
||||||
|
translationManager.getButtonIdToLobbyId().clear();
|
||||||
|
gridManager = new LobbyButtonGridManager(gridPane, translationManager);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testCreateLobbyReturnsId() {
|
||||||
|
int lobbyId = gridManager.createLobby();
|
||||||
|
assertTrue(lobbyId > 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testJoinLobbyPlaceholder() {
|
||||||
|
// check if exception is thrown
|
||||||
|
assertDoesNotThrow(() -> gridManager.joinLobby(123));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
|
||||||
|
package ch.unibas.dmi.dbis.cs108.casono.client.ui.lobbyui;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
import org.junit.jupiter.api.*;
|
||||||
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
|
||||||
|
class LobbyButtonTranslationManagerTest {
|
||||||
|
LobbyButtonTranslationManager manager;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setUp() {
|
||||||
|
manager = new LobbyButtonTranslationManager();
|
||||||
|
manager.getButtonIdToLobbyId().clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testAddLobbyButton() throws Exception {
|
||||||
|
manager.addLobbyButton(1, 100);
|
||||||
|
assertEquals(100, manager.getLobbyIdForButton(1));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testRemoveLobbyButton() throws Exception {
|
||||||
|
manager.addLobbyButton(2, 200);
|
||||||
|
manager.removeLobbyButton(2);
|
||||||
|
assertNull(manager.getLobbyIdForButton(2));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testIsFull() throws Exception {
|
||||||
|
for (int i = 1; i <= 8; i++) {
|
||||||
|
manager.addLobbyButton(i, 100 + i);
|
||||||
|
}
|
||||||
|
assertTrue(manager.isFull());
|
||||||
|
Exception ex = assertThrows(Exception.class, () -> manager.addLobbyButton(9, 109));
|
||||||
|
assertEquals("Grid is full!", ex.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testGetButtonIdToLobbyId() throws Exception {
|
||||||
|
manager.addLobbyButton(4, 400);
|
||||||
|
Map<Integer, Integer> map = manager.getButtonIdToLobbyId();
|
||||||
|
assertTrue(map.containsKey(4));
|
||||||
|
assertEquals(400, map.get(4));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
package ch.unibas.dmi.dbis.cs108.casono.server.tokenizer;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
|
||||||
|
class StateTest {
|
||||||
|
@Test
|
||||||
|
void testConstructor() {
|
||||||
|
String input = "hello";
|
||||||
|
State state = new State(input);
|
||||||
|
assertEquals(input, state.input);
|
||||||
|
assertEquals(0, state.pos);
|
||||||
|
assertEquals(1, state.line);
|
||||||
|
assertEquals(1, state.column);
|
||||||
|
assertTrue(state.tokens.isEmpty());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testCurrent() {
|
||||||
|
String input = "abc";
|
||||||
|
State state = new State(input);
|
||||||
|
assertEquals('a', state.current());
|
||||||
|
state.advance();
|
||||||
|
assertEquals('b', state.current());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testPeek() {
|
||||||
|
String input = "abc";
|
||||||
|
State state = new State(input);
|
||||||
|
assertEquals('b', state.peek());
|
||||||
|
state.advance();
|
||||||
|
assertEquals('c', state.peek());
|
||||||
|
state.advance();
|
||||||
|
assertEquals('\0', state.peek()); // End of input
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testAdvance() {
|
||||||
|
String input = "abc";
|
||||||
|
State state = new State(input);
|
||||||
|
assertEquals(0, state.pos);
|
||||||
|
assertEquals(1, state.column);
|
||||||
|
state.advance();
|
||||||
|
assertEquals(1, state.pos);
|
||||||
|
assertEquals(2, state.column);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testIsEof() {
|
||||||
|
String input = "a";
|
||||||
|
State state = new State(input);
|
||||||
|
assertFalse(state.isEof());
|
||||||
|
state.advance();
|
||||||
|
assertTrue(state.isEof());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
package ch.unibas.dmi.dbis.cs108.casono.server.tokenizer;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
|
||||||
|
class TokenizerExceptionTest {
|
||||||
|
@Test
|
||||||
|
void testConstructorAndGetters() {
|
||||||
|
String message = "Test error";
|
||||||
|
int line = 5;
|
||||||
|
int column = 10;
|
||||||
|
TokenizerException e = new TokenizerException(message, line, column);
|
||||||
|
assertEquals(message, e.getMessage());
|
||||||
|
assertEquals(line, e.getLine());
|
||||||
|
assertEquals(column, e.getColumn());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,67 +0,0 @@
|
|||||||
package ch.unibas.dmi.dbis.cs108.example;
|
|
||||||
|
|
||||||
import org.junit.jupiter.api.AfterEach;
|
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
|
||||||
import org.junit.jupiter.api.Test;
|
|
||||||
|
|
||||||
import java.io.ByteArrayOutputStream;
|
|
||||||
import java.io.PrintStream;
|
|
||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* An example test class.
|
|
||||||
* Checks the output of the {@link HelloWorld} class and makes sure it contains "Hello World"
|
|
||||||
*/
|
|
||||||
public class HelloWorldTest {
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Streams to store system.out and system.err content
|
|
||||||
*/
|
|
||||||
private ByteArrayOutputStream outStream = new ByteArrayOutputStream();
|
|
||||||
private ByteArrayOutputStream errStream = new ByteArrayOutputStream();
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Here we store the previous pointers to system.out / system.err
|
|
||||||
*/
|
|
||||||
private PrintStream outBackup;
|
|
||||||
private PrintStream errBackup;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This method is executed before each test.
|
|
||||||
* It redirects System.out and System.err to our variables {@link #outStream} and {@link #errStream}.
|
|
||||||
* This allows us to test their content later.
|
|
||||||
*/
|
|
||||||
@BeforeEach
|
|
||||||
public void redirectStdOutStdErr() {
|
|
||||||
outBackup = System.out;
|
|
||||||
errBackup = System.err;
|
|
||||||
System.setOut(new PrintStream(outStream));
|
|
||||||
System.setErr(new PrintStream(errStream));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This method is run after each test.
|
|
||||||
* It redirects System.out / System.err back to the normal streams.
|
|
||||||
*/
|
|
||||||
@AfterEach
|
|
||||||
public void reestablishStdOutStdErr() {
|
|
||||||
System.setOut(outBackup);
|
|
||||||
System.setErr(errBackup);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This is a normal JUnit-Test. It executes the HelloWorld-Method and verifies that it actually wrote "Hello World" to stdout
|
|
||||||
*/
|
|
||||||
@Test
|
|
||||||
public void testMain() {
|
|
||||||
HelloWorld.main(new String[0]);
|
|
||||||
String toTest = outStream.toString();
|
|
||||||
toTest = removeNewline(toTest);
|
|
||||||
assertTrue(toTest.contains("Hello World"));
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String removeNewline(String str) {
|
|
||||||
return str.replace("\n", "").replace("\r", "");
|
|
||||||
}
|
|
||||||
}
|
|
||||||