First commit
This commit is contained in:
+63
@@ -0,0 +1,63 @@
|
|||||||
|
# Gradle
|
||||||
|
.gradle/
|
||||||
|
build/
|
||||||
|
!gradle/wrapper/gradle-wrapper.jar
|
||||||
|
!**/src/main/**/build/
|
||||||
|
!**/src/test/**/build/
|
||||||
|
.kotlin/
|
||||||
|
|
||||||
|
# Server testing directory
|
||||||
|
run/
|
||||||
|
|
||||||
|
# IntelliJ IDEA
|
||||||
|
.idea/
|
||||||
|
*.iws
|
||||||
|
*.iml
|
||||||
|
*.ipr
|
||||||
|
out/
|
||||||
|
!**/src/main/**/out/
|
||||||
|
!**/src/test/**/out/
|
||||||
|
|
||||||
|
# Eclipse
|
||||||
|
.apt_generated
|
||||||
|
.classpath
|
||||||
|
.factorypath
|
||||||
|
.project
|
||||||
|
.settings
|
||||||
|
.springBeans
|
||||||
|
.sts4-cache
|
||||||
|
bin/
|
||||||
|
!**/src/main/**/bin/
|
||||||
|
!**/src/test/**/bin/
|
||||||
|
|
||||||
|
# NetBeans
|
||||||
|
/nbproject/private/
|
||||||
|
/nbbuild/
|
||||||
|
/dist/
|
||||||
|
/nbdist/
|
||||||
|
/.nb-gradle/
|
||||||
|
|
||||||
|
# VS Code
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/java-formatter.xml
|
||||||
|
!.vscode/settings.json
|
||||||
|
|
||||||
|
# Mac OS
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
|
# Windows
|
||||||
|
Thumbs.db
|
||||||
|
desktop.ini
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
*.log
|
||||||
|
|
||||||
|
# Temporary files
|
||||||
|
*.tmp
|
||||||
|
*.bak
|
||||||
|
*.swp
|
||||||
|
*~
|
||||||
|
|
||||||
|
# Template specific
|
||||||
|
libs/HytaleServer.jar
|
||||||
|
buildSrc/
|
||||||
@@ -0,0 +1,155 @@
|
|||||||
|
# core-minigames
|
||||||
|
|
||||||
|
A Hytale-native minigame extension for server-side minigame definitions, runtime state, and trigger-volume logic.
|
||||||
|
|
||||||
|
The mod mirrors Hytale's own asset and trigger systems instead of maintaining a separate minigame-only registry layer. Minigames are `MinigameDefinition` assets, trigger logic is registered as real `TriggerEffect` and `TriggerCondition` codec entries, and runtime events are dispatched on Hytale's event bus.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- Asset-driven minigame definitions in `Server/Minigames/`
|
||||||
|
- Hytale Asset Editor support through `MinigameDefinition.CODEC`
|
||||||
|
- Hytale trigger-volume integration through native `TriggerEffect` and `TriggerCondition` types
|
||||||
|
- Asset-backed `MinigameId` picker fields in the trigger-volume editor
|
||||||
|
- Runtime lifecycle commands for creating, enabling, starting, stopping, and resetting minigames
|
||||||
|
- Multi-map runtime support using Hytale trigger-volume tags
|
||||||
|
- Player, team, score, lives, counter, status, and phase runtime state
|
||||||
|
- Hytale event-bus dispatch through `MinigameEvent`
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
### Build
|
||||||
|
|
||||||
|
```bash
|
||||||
|
gradlew.bat shadowJar
|
||||||
|
```
|
||||||
|
|
||||||
|
### Run with the local Hytale server
|
||||||
|
|
||||||
|
```bash
|
||||||
|
gradlew.bat runServer
|
||||||
|
```
|
||||||
|
|
||||||
|
### Create a minigame definition
|
||||||
|
Manually by adding a file under `src/main/resources/Server/Minigames/`:
|
||||||
|
or via the asset editor.
|
||||||
|
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"Id": "My_Minigame",
|
||||||
|
"Name": "My Minigame",
|
||||||
|
"Description": "A short description shown in menus.",
|
||||||
|
"Enabled": true,
|
||||||
|
"MinPlayers": 2,
|
||||||
|
"MaxPlayers": 8,
|
||||||
|
"TeamMode": "SOLO",
|
||||||
|
"WinCondition": "HIGHEST_SCORE",
|
||||||
|
"RoundLengthSeconds": 180,
|
||||||
|
"CountdownSeconds": 10
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Or create one from the server:
|
||||||
|
|
||||||
|
```text
|
||||||
|
/minigame create My_Minigame "My Minigame"
|
||||||
|
/minigame enable My_Minigame
|
||||||
|
/minigame start My_Minigame
|
||||||
|
```
|
||||||
|
TODO make this into its own UI in game.
|
||||||
|
|
||||||
|
|
||||||
|
## Trigger Volume Logic
|
||||||
|
|
||||||
|
Use normal Hytale trigger volumes for the area shape and interaction point, then attach the minigame trigger conditions and effects.
|
||||||
|
|
||||||
|
For multi-map minigames, tag volumes with `MinigameId=<id>`, `MapId=<map>`, optional `VariantId=<variant>`, and `MapRole=MainArena` on the primary arena volume. Team spawn volumes use `SpawnRole=TeamSpawn` and `TeamId=<team>`. `MapSelectionMode` controls whether a queued match uses votes or random selection.
|
||||||
|
|
||||||
|
Available condition types:
|
||||||
|
|
||||||
|
- `MinigamePhase`
|
||||||
|
- `PlayerInMinigame`
|
||||||
|
- `PlayerStatus`
|
||||||
|
- `PlayerScore`
|
||||||
|
- `TeamScore`
|
||||||
|
- `Counter`
|
||||||
|
- `CurrentRound`
|
||||||
|
- `PlayerLives`
|
||||||
|
- `TeamCondition`
|
||||||
|
- `TimerElapsed`
|
||||||
|
- `ObjectiveStatus`
|
||||||
|
- `WinConditionMet`
|
||||||
|
|
||||||
|
Available effect types:
|
||||||
|
|
||||||
|
- `StartMinigame`
|
||||||
|
- `JoinMinigameQueue`
|
||||||
|
- `LeaveMinigameQueue`
|
||||||
|
- `VoteForMap`
|
||||||
|
- `StartQueuedMinigame`
|
||||||
|
- `SpawnItem`
|
||||||
|
- `AwardKillScore`
|
||||||
|
- `EndMinigame`
|
||||||
|
- `ResetMinigame`
|
||||||
|
- `SetMinigamePhase`
|
||||||
|
- `ResetScores`
|
||||||
|
- `ModifyPlayerScore`
|
||||||
|
- `ModifyTeamScore`
|
||||||
|
- `ModifyCounter`
|
||||||
|
- `ModifyPlayerLives`
|
||||||
|
- `SetPlayerStatus`
|
||||||
|
- `SetRound`
|
||||||
|
- `StartTimer`
|
||||||
|
- `StopTimer`
|
||||||
|
- `SetSpawnPoint`
|
||||||
|
- `RespawnPlayer`
|
||||||
|
- `SendMinigameMessage`
|
||||||
|
- `AssignTeam`
|
||||||
|
- `SaveInventory`
|
||||||
|
- `RestoreInventory`
|
||||||
|
- `ClearInventory`
|
||||||
|
- `SetObjectiveStatus`
|
||||||
|
- `ModifyObjectiveProgress`
|
||||||
|
|
||||||
|
Fields named `MinigameId` use an editor dropdown backed by registered `MinigameDefinition` assets.
|
||||||
|
|
||||||
|
## Project Structure
|
||||||
|
|
||||||
|
```text
|
||||||
|
core-minigames/
|
||||||
|
|-- src/main/java/net/kewwbec/minigames/
|
||||||
|
| |-- MinigameCorePlugin.java
|
||||||
|
| |-- command/
|
||||||
|
| |-- event/
|
||||||
|
| |-- model/
|
||||||
|
| |-- runtime/
|
||||||
|
| |-- service/
|
||||||
|
| `-- volume/
|
||||||
|
| |-- condition/
|
||||||
|
| `-- effect/
|
||||||
|
|-- src/main/resources/Server/
|
||||||
|
| |-- Languages/en-US/
|
||||||
|
| `-- Minigames/
|
||||||
|
|-- docs/
|
||||||
|
`-- wiki/
|
||||||
|
```
|
||||||
|
|
||||||
|
## Documentation
|
||||||
|
|
||||||
|
| Doc | Description |
|
||||||
|
|-----|-------------|
|
||||||
|
| [Architecture](docs/architecture.md) | How the Hytale-native pieces fit together |
|
||||||
|
| [Minigame Definition](docs/minigame-definition.md) | `MinigameDefinition` JSON fields |
|
||||||
|
| [Events, Conditions & Effects](docs/events-conditions-effects.md) | Current trigger condition/effect reference |
|
||||||
|
| [Volumes](docs/volumes.md) | How to use normal Hytale trigger volumes for minigames |
|
||||||
|
| [Trigger Volume Tags](docs/tags.md) | Complete tag reference for maps, spawns, and signals |
|
||||||
|
| [Commands](docs/commands.md) | `/minigame` command reference |
|
||||||
|
| [Examples](docs/examples.md) | Small setup patterns |
|
||||||
|
|
||||||
|
## Build Commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
gradlew.bat compileJava
|
||||||
|
gradlew.bat processResources
|
||||||
|
gradlew.bat shadowJar
|
||||||
|
```
|
||||||
+206
@@ -0,0 +1,206 @@
|
|||||||
|
plugins {
|
||||||
|
id 'java'
|
||||||
|
id 'org.jetbrains.gradle.plugin.idea-ext' version '1.3'
|
||||||
|
id 'com.gradleup.shadow' version '9.3.1'
|
||||||
|
}
|
||||||
|
|
||||||
|
import org.gradle.internal.os.OperatingSystem
|
||||||
|
|
||||||
|
ext {
|
||||||
|
if (project.hasProperty('hytale_home')) {
|
||||||
|
hytaleHome = project.findProperty('hytale_home')
|
||||||
|
} else if (System.getenv('HYTALE_HOME')) {
|
||||||
|
hytaleHome = System.getenv('HYTALE_HOME')
|
||||||
|
} else {
|
||||||
|
def os = OperatingSystem.current()
|
||||||
|
if (os.isWindows()) {
|
||||||
|
hytaleHome = "${System.getProperty("user.home")}/AppData/Roaming/Hytale"
|
||||||
|
}
|
||||||
|
else if (os.isMacOsX()) {
|
||||||
|
hytaleHome = "${System.getProperty("user.home")}/Library/Application Support/Hytale"
|
||||||
|
}
|
||||||
|
else if (os.isLinux()) {
|
||||||
|
hytaleHome = "${System.getProperty("user.home")}/.var/app/com.hypixel.HytaleLauncher/data/Hytale"
|
||||||
|
if (!file(hytaleHome).exists()) {
|
||||||
|
hytaleHome = "${System.getProperty("user.home")}/.local/share/Hytale"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def hytaleHomePath = ext.has('hytaleHome') ? hytaleHome : null
|
||||||
|
def hasHytaleHome = (hytaleHomePath != null) && file(hytaleHomePath).exists()
|
||||||
|
if (!hasHytaleHome) {
|
||||||
|
logger.lifecycle("Hytale install not detected; run configs that launch the server will be skipped. Set -Phytale_home=/path/to/Hytale to enable them.")
|
||||||
|
}
|
||||||
|
|
||||||
|
java {
|
||||||
|
toolchain.languageVersion = JavaLanguageVersion.of(java_version)
|
||||||
|
withSourcesJar()
|
||||||
|
withJavadocJar()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Quiet warnings about missing Javadocs.
|
||||||
|
javadoc {
|
||||||
|
options.addStringOption('Xdoclint:-missing', '-quiet')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Adds the Hytale server as a build dependency, allowing you to reference and
|
||||||
|
// compile against their code without bundling it. When a local install is
|
||||||
|
// present, we still use its jar for launching the server in IDE run configs.
|
||||||
|
dependencies {
|
||||||
|
compileOnly("com.hypixel.hytale:Server:$hytale_build")
|
||||||
|
testCompileOnly("com.hypixel.hytale:Server:$hytale_build")
|
||||||
|
testRuntimeOnly("com.hypixel.hytale:Server:$hytale_build")
|
||||||
|
testImplementation("org.junit.jupiter:junit-jupiter:5.11.4")
|
||||||
|
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
|
||||||
|
if (hasHytaleHome) {
|
||||||
|
runtimeOnly(files("$hytaleHome/install/$patchline/package/game/latest/Server/HytaleServer.jar"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Your dependencies here
|
||||||
|
}
|
||||||
|
|
||||||
|
test {
|
||||||
|
useJUnitPlatform()
|
||||||
|
}
|
||||||
|
|
||||||
|
repositories {
|
||||||
|
mavenCentral()
|
||||||
|
maven {
|
||||||
|
name = "hytale-release"
|
||||||
|
url = uri("https://maven.hytale.com/release")
|
||||||
|
}
|
||||||
|
maven {
|
||||||
|
name = "hytale-pre-release"
|
||||||
|
url = uri("https://maven.hytale.com/pre-release")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Updates the manifest.json file with the latest properties defined in the
|
||||||
|
// build.properties file. Currently we update the version and if packs are
|
||||||
|
// included with the plugin.
|
||||||
|
tasks.register('updatePluginManifest') {
|
||||||
|
def manifestFile = file('src/main/resources/manifest.json')
|
||||||
|
doLast {
|
||||||
|
if (!manifestFile.exists()) {
|
||||||
|
throw new GradleException("Could not find manifest.json at ${manifestFile.path}!")
|
||||||
|
}
|
||||||
|
def manifestJson = new groovy.json.JsonSlurper().parseText(manifestFile.text)
|
||||||
|
manifestJson.Version = version
|
||||||
|
manifestJson.IncludesAssetPack = includes_pack.toBoolean()
|
||||||
|
manifestFile.text = groovy.json.JsonOutput.prettyPrint(groovy.json.JsonOutput.toJson(manifestJson))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Makes sure the plugin manifest is up to date.
|
||||||
|
tasks.named('processResources') {
|
||||||
|
dependsOn 'updatePluginManifest'
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.named('shadowJar') {
|
||||||
|
archiveClassifier.set('')
|
||||||
|
mergeServiceFiles()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure the shaded jar is produced during a normal build.
|
||||||
|
tasks.named('build') {
|
||||||
|
dependsOn 'shadowJar'
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasHytaleHome) {
|
||||||
|
// Create the working directory to run the server if it does not already exist.
|
||||||
|
def serverRunDir = file("$projectDir/run")
|
||||||
|
if (!serverRunDir.exists()) {
|
||||||
|
serverRunDir.mkdirs()
|
||||||
|
}
|
||||||
|
|
||||||
|
def createServerRunArguments = { String srcDir ->
|
||||||
|
def programParameters = '--allow-op --disable-sentry --assets="' + "${hytaleHome}/install/$patchline/package/game/latest/Assets.zip" + '"'
|
||||||
|
def modPaths = []
|
||||||
|
if (includes_pack.toBoolean()) {
|
||||||
|
modPaths << srcDir
|
||||||
|
}
|
||||||
|
if (load_user_mods.toBoolean()) {
|
||||||
|
modPaths << "${hytaleHome}/UserData/Mods"
|
||||||
|
}
|
||||||
|
if (!modPaths.isEmpty()) {
|
||||||
|
programParameters += ' --mods="' + modPaths.join(',') + '"'
|
||||||
|
}
|
||||||
|
return programParameters
|
||||||
|
}
|
||||||
|
|
||||||
|
def serverJar = file("$hytaleHome/install/$patchline/package/game/latest/Server/HytaleServer.jar")
|
||||||
|
def assetsZip = file("$hytaleHome/install/$patchline/package/game/latest/Assets.zip")
|
||||||
|
def shadowJarTask = tasks.named('shadowJar')
|
||||||
|
|
||||||
|
tasks.register('runServerJar', JavaExec) {
|
||||||
|
dependsOn shadowJarTask
|
||||||
|
mainClass = 'com.hypixel.hytale.Main'
|
||||||
|
classpath = files(serverJar)
|
||||||
|
workingDir = serverRunDir.absolutePath
|
||||||
|
doFirst {
|
||||||
|
def modPaths = [shadowJarTask.get().archiveFile.get().asFile.parentFile.absolutePath]
|
||||||
|
if (load_user_mods.toBoolean()) {
|
||||||
|
modPaths << "${hytaleHome}/UserData/Mods"
|
||||||
|
}
|
||||||
|
def args = [
|
||||||
|
'--allow-op',
|
||||||
|
'--disable-sentry',
|
||||||
|
"--assets=${assetsZip}",
|
||||||
|
"--mod=${modPaths.join(',')}"
|
||||||
|
]
|
||||||
|
logger.lifecycle("Running server with command: java -cp ${serverJar} ${mainClass.get()} ${args.join(' ')}")
|
||||||
|
setArgs(args)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.register('runServer') {
|
||||||
|
|
||||||
|
dependsOn 'runServerJar'
|
||||||
|
}
|
||||||
|
|
||||||
|
// Creates a run configuration in IDEA that will run the Hytale server with
|
||||||
|
// your plugin and the default assets.
|
||||||
|
idea.project.settings.runConfigurations {
|
||||||
|
'HytaleServer'(org.jetbrains.gradle.ext.Application) {
|
||||||
|
mainClass = 'com.hypixel.hytale.Main'
|
||||||
|
moduleName = project.idea.module.name + '.main'
|
||||||
|
programParameters = createServerRunArguments(sourceSets.main.java.srcDirs.first().parentFile.absolutePath)
|
||||||
|
workingDirectory = serverRunDir.absolutePath
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Creates a launch.json file for VSCode with the same configuration
|
||||||
|
tasks.register('generateVSCodeLaunch') {
|
||||||
|
def vscodeDir = file("$projectDir/.vscode")
|
||||||
|
def launchFile = file("$vscodeDir/launch.json")
|
||||||
|
doLast {
|
||||||
|
if (!vscodeDir.exists()) {
|
||||||
|
vscodeDir.mkdirs()
|
||||||
|
}
|
||||||
|
def programParams = createServerRunArguments("\${workspaceFolder}")
|
||||||
|
def launchConfig = [
|
||||||
|
version: "0.2.0",
|
||||||
|
configurations: [
|
||||||
|
[
|
||||||
|
type: "java",
|
||||||
|
name: "HytaleServer",
|
||||||
|
request: "launch",
|
||||||
|
mainClass: "com.hypixel.hytale.Main",
|
||||||
|
args: programParams,
|
||||||
|
cwd: "\${workspaceFolder}/run"
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
launchFile.text = groovy.json.JsonOutput.prettyPrint(groovy.json.JsonOutput.toJson(launchConfig))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
tasks.register('generateVSCodeLaunch') {
|
||||||
|
doLast {
|
||||||
|
logger.lifecycle("Skipped VSCode launch configuration because hytale_home is not set or the install path is missing.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,139 @@
|
|||||||
|
# Architecture
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
core-minigames extends Hytale's existing asset, trigger-volume, and event systems. It does not run a separate minigame logic registry beside Hytale's trigger-volume registry.
|
||||||
|
|
||||||
|
```text
|
||||||
|
Server/Minigames/*.json
|
||||||
|
|
|
||||||
|
v
|
||||||
|
MinigameDefinition asset store
|
||||||
|
|
|
||||||
|
v
|
||||||
|
MinigameRuntimeService
|
||||||
|
|
|
||||||
|
+--> Hytale event bus: MinigameEvent
|
||||||
|
|
|
||||||
|
+--> TriggerEffect.CODEC entries
|
||||||
|
|
|
||||||
|
`--> TriggerCondition.CODEC entries
|
||||||
|
```
|
||||||
|
|
||||||
|
## Asset Store
|
||||||
|
|
||||||
|
`MinigameDefinition` is registered as a Hytale asset store in `MinigameCorePlugin.setup()`.
|
||||||
|
|
||||||
|
Important details:
|
||||||
|
|
||||||
|
- Path: `Server/Minigames/`
|
||||||
|
- Codec: `MinigameDefinition.CODEC`
|
||||||
|
- Key function: `MinigameDefinition::getId`
|
||||||
|
- Editor dropdown source: `MinigameDefinition`
|
||||||
|
|
||||||
|
The trigger-volume editor can use this asset source for fields named `MinigameId`, so minigame trigger effects and conditions can pick from loaded minigame assets instead of free-typing IDs.
|
||||||
|
|
||||||
|
## Runtime
|
||||||
|
|
||||||
|
`DefaultMinigameRuntimeService` owns active `MinigameRuntime` instances keyed by runtime ID. A single minigame ID can have multiple active runtimes when multiple discovered maps or variants are started.
|
||||||
|
|
||||||
|
Runtime state includes:
|
||||||
|
|
||||||
|
- Current `MinigamePhase`
|
||||||
|
- Player state, score, lives, team, tags, and status
|
||||||
|
- Team membership and team score
|
||||||
|
- Objective state
|
||||||
|
- Counter and variable state
|
||||||
|
- Spectator and eliminated-player sets
|
||||||
|
- Runtime ID, map ID, and variant ID
|
||||||
|
|
||||||
|
`DefaultMinigameService` handles definition-level actions and delegates live actions to the runtime service.
|
||||||
|
|
||||||
|
## Events
|
||||||
|
|
||||||
|
`MinigameEvent` implements Hytale's `IEvent<String>`.
|
||||||
|
|
||||||
|
Runtime events dispatch through:
|
||||||
|
|
||||||
|
```java
|
||||||
|
HytaleServer.get()
|
||||||
|
.getEventBus()
|
||||||
|
.dispatchFor(MinigameEvent.class, event.minigameId())
|
||||||
|
.dispatch(event);
|
||||||
|
```
|
||||||
|
|
||||||
|
This keeps minigame events available to Hytale's event system instead of routing them through a custom `EventRegistry`.
|
||||||
|
|
||||||
|
## Trigger Conditions
|
||||||
|
|
||||||
|
Minigame trigger conditions extend Hytale's `TriggerCondition` and are registered directly into `TriggerCondition.CODEC`.
|
||||||
|
|
||||||
|
Current condition type IDs:
|
||||||
|
|
||||||
|
- `MinigamePhase`
|
||||||
|
- `PlayerInMinigame`
|
||||||
|
- `PlayerStatus`
|
||||||
|
- `PlayerScore`
|
||||||
|
- `TeamScore`
|
||||||
|
- `Counter`
|
||||||
|
- `CurrentRound`
|
||||||
|
- `PlayerLives`
|
||||||
|
- `TeamCondition`
|
||||||
|
- `TimerElapsed`
|
||||||
|
- `ObjectiveStatus`
|
||||||
|
- `WinConditionMet`
|
||||||
|
|
||||||
|
## Trigger Effects
|
||||||
|
|
||||||
|
Minigame trigger effects extend Hytale's `TriggerEffect` and are registered directly into `TriggerEffect.CODEC`.
|
||||||
|
|
||||||
|
Current effect type IDs:
|
||||||
|
|
||||||
|
- `StartMinigame`
|
||||||
|
- `EndMinigame`
|
||||||
|
- `ResetMinigame`
|
||||||
|
- `SetMinigamePhase`
|
||||||
|
- `ResetScores`
|
||||||
|
- `ModifyPlayerScore`
|
||||||
|
- `ModifyTeamScore`
|
||||||
|
- `ModifyCounter`
|
||||||
|
- `ModifyPlayerLives`
|
||||||
|
- `SetPlayerStatus`
|
||||||
|
- `JoinMinigameQueue`
|
||||||
|
- `LeaveMinigameQueue`
|
||||||
|
- `VoteForMap`
|
||||||
|
- `StartQueuedMinigame`
|
||||||
|
- `SpawnItem`
|
||||||
|
- `SetRound`
|
||||||
|
- `StartTimer`
|
||||||
|
- `StopTimer`
|
||||||
|
- `SetSpawnPoint`
|
||||||
|
- `RespawnPlayer`
|
||||||
|
- `SendMinigameMessage`
|
||||||
|
- `AssignTeam`
|
||||||
|
- `SaveInventory`
|
||||||
|
- `RestoreInventory`
|
||||||
|
- `ClearInventory`
|
||||||
|
- `SetObjectiveStatus`
|
||||||
|
- `ModifyObjectiveProgress`
|
||||||
|
- `AwardKillScore`
|
||||||
|
|
||||||
|
## Volumes
|
||||||
|
|
||||||
|
There are no custom minigame volume classes after the refactor. Hytale trigger volumes provide the region shape, enter/exit behavior, tags, and trigger effect assets.
|
||||||
|
|
||||||
|
To define a minigame area, create a normal Hytale trigger volume and tag it with `MinigameId=<id>`, `MapId=<map>`, optional `VariantId=<variant>`, and optionally `MapRole=MainArena`. Then attach minigame trigger conditions and effects to that trigger volume or to reusable Hytale trigger effect assets.
|
||||||
|
|
||||||
|
## Removed Layer
|
||||||
|
|
||||||
|
The following old architecture pieces were removed because they duplicated Hytale systems:
|
||||||
|
|
||||||
|
- `BuiltinRegistries`
|
||||||
|
- `ConditionRegistry`
|
||||||
|
- `EventRegistry`
|
||||||
|
- `MinigameRegistries`
|
||||||
|
- `TemplateRegistry`
|
||||||
|
- `VolumeRegistry`
|
||||||
|
- `VolumeTypeDefinition`
|
||||||
|
|
||||||
|
Future minigame features should keep following the same pattern: use Hytale asset stores, Hytale trigger codec registration, and Hytale event dispatch unless there is a concrete engine gap that requires a local adapter.
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
# Commands Reference
|
||||||
|
|
||||||
|
All commands are subcommands of `/minigame`.
|
||||||
|
|
||||||
|
## Definition Management
|
||||||
|
|
||||||
|
### `/minigame list`
|
||||||
|
|
||||||
|
Lists registered minigames and basic runtime state.
|
||||||
|
|
||||||
|
### `/minigame create <id> <name>`
|
||||||
|
|
||||||
|
Creates a new minigame definition with default settings.
|
||||||
|
|
||||||
|
```text
|
||||||
|
/minigame create Beach_Race "Beach Race"
|
||||||
|
```
|
||||||
|
|
||||||
|
Definitions created by command start disabled. Enable them when the asset is configured.
|
||||||
|
|
||||||
|
### `/minigame info <id>`
|
||||||
|
|
||||||
|
Shows the loaded definition and runtime status for a minigame.
|
||||||
|
|
||||||
|
### `/minigame edit <id> <field> <value>`
|
||||||
|
|
||||||
|
Edits a single definition field and saves the definition.
|
||||||
|
|
||||||
|
```text
|
||||||
|
/minigame edit Beach_Race MaxPlayers 16
|
||||||
|
/minigame edit Beach_Race RoundLengthSeconds 600
|
||||||
|
```
|
||||||
|
|
||||||
|
### `/minigame enable <id>`
|
||||||
|
|
||||||
|
Sets `Enabled = true`.
|
||||||
|
|
||||||
|
### `/minigame disable <id>`
|
||||||
|
|
||||||
|
Sets `Enabled = false`. This prevents new starts but does not necessarily stop an already running runtime.
|
||||||
|
|
||||||
|
### `/minigame validate <id>`
|
||||||
|
|
||||||
|
Runs definition validation. Current validation focuses on definition fields. The old custom-volume requirement for `MinigameAreaVolume` no longer applies.
|
||||||
|
|
||||||
|
### `/minigame delete <id>`
|
||||||
|
|
||||||
|
Deletes the minigame definition.
|
||||||
|
|
||||||
|
### `/minigame export <id>`
|
||||||
|
|
||||||
|
Exports the current definition JSON.
|
||||||
|
|
||||||
|
### `/minigame import <file>`
|
||||||
|
|
||||||
|
Imports a definition from a JSON file path relative to the data directory.
|
||||||
|
|
||||||
|
## Runtime Management
|
||||||
|
|
||||||
|
### `/minigame start <id>`
|
||||||
|
|
||||||
|
Starts a queued runtime for the given minigame. The selected map comes from `MapSelectionMode`: highest vote for `VOTE`, random discovered map for `RANDOM`.
|
||||||
|
|
||||||
|
### `/minigame stop <id>`
|
||||||
|
|
||||||
|
Stops all active runtimes for the minigame ID. An optional greedy `reason` argument can override the default admin-stop reason.
|
||||||
|
|
||||||
|
### `/minigame reset <id>`
|
||||||
|
|
||||||
|
Resets all active runtimes for the minigame ID.
|
||||||
|
|
||||||
|
## Map Commands
|
||||||
|
|
||||||
|
### `/minigame maps <id>`
|
||||||
|
|
||||||
|
Lists maps discovered from trigger volumes in the sender's current world. Run this command as a player in the world containing the tagged volumes.
|
||||||
|
|
||||||
|
### `/minigame vote <id> <mapId>`
|
||||||
|
|
||||||
|
Casts or updates the sender's vote in the minigame waiting session.
|
||||||
|
|
||||||
|
## Player Commands
|
||||||
|
|
||||||
|
### `/minigame join <id> [player]`
|
||||||
|
|
||||||
|
Adds the command sender, or the optional player target, to the minigame waiting queue.
|
||||||
|
|
||||||
|
### `/minigame leave [player]`
|
||||||
|
|
||||||
|
Currently registered but not implemented. Use the `LeaveMinigameQueue` trigger effect for in-world queue leaving until the command is wired.
|
||||||
|
|
||||||
|
### `/minigame spectate <id>`
|
||||||
|
|
||||||
|
Currently registered but not implemented. The command validates that the minigame exists and has a running runtime, then returns a not-implemented message.
|
||||||
|
|
||||||
|
## Volume Commands
|
||||||
|
|
||||||
|
The old custom minigame volume system was removed. Use Hytale's trigger-volume editor for real volume placement and attach the minigame trigger effects and conditions there.
|
||||||
|
|
||||||
|
`/minigame volume create <type>` currently remains as a command stub for future Hytale trigger-volume integration. It should not be used as the primary way to define arenas.
|
||||||
@@ -0,0 +1,138 @@
|
|||||||
|
# Events, Conditions & Effects
|
||||||
|
|
||||||
|
Minigame logic is implemented as Hytale-native trigger conditions and trigger effects. These are registered into `TriggerCondition.CODEC` and `TriggerEffect.CODEC`, so they appear beside Hytale's built-in trigger-volume logic.
|
||||||
|
|
||||||
|
## Events
|
||||||
|
|
||||||
|
`MinigameEvent` implements `IEvent<String>` and is dispatched through Hytale's event bus, keyed by minigame ID.
|
||||||
|
|
||||||
|
Current runtime events emitted by the service layer:
|
||||||
|
|
||||||
|
| Event ID | Fired When |
|
||||||
|
|----------|------------|
|
||||||
|
| `on_game_start` | A minigame runtime starts. |
|
||||||
|
| `on_game_end` | A minigame runtime ends. |
|
||||||
|
| `on_arena_reset` | A minigame runtime is reset. |
|
||||||
|
| `on_phase_change` | `SetMinigamePhase` changes the runtime phase. |
|
||||||
|
| `on_round_start` | `SetRound` starts a round, or the round timer advances to the next round. |
|
||||||
|
| `on_round_end` | The internal round timer expires. |
|
||||||
|
| `on_game_rounds_complete` | The final configured round ends. |
|
||||||
|
| `on_timer_expired` | A named signal timer expires. The internal `$round` timer is consumed by runtime round progression and is not re-dispatched. |
|
||||||
|
| `on_player_eliminated` | `SetPlayerStatus` sets `ELIMINATED`, or `ModifyPlayerLives` reduces lives to zero. |
|
||||||
|
| `on_objective_complete` | An objective status becomes `COMPLETE` or objective progress reaches completion. |
|
||||||
|
| `on_kill_score` | The kill scoring system awards points for a configured kill. |
|
||||||
|
|
||||||
|
The event payload is a string-object map. Runtime events include `runtime_id`, `minigame_id`, `map_id`, and `variant_id`. End events also include `reason`.
|
||||||
|
|
||||||
|
## Conditions
|
||||||
|
|
||||||
|
All minigame conditions share a `MinigameId` field. In the editor this is registered as an asset picker backed by `MinigameDefinition` assets.
|
||||||
|
|
||||||
|
| Type ID | Fields | Description |
|
||||||
|
|---------|--------|-------------|
|
||||||
|
| `MinigamePhase` | `MinigameId`, `Phase` | Passes when the runtime phase matches. |
|
||||||
|
| `PlayerInMinigame` | `MinigameId`, optional `PlayerId` | Passes when the player is in the runtime. If `PlayerId` is blank, the trigger context player is used. |
|
||||||
|
| `PlayerStatus` | `MinigameId`, optional `PlayerId`, `Status` | Passes when the player's minigame status matches. |
|
||||||
|
| `PlayerScore` | `MinigameId`, optional `PlayerId`, `Compare`, `Value` | Compares a player's score. |
|
||||||
|
| `TeamScore` | `MinigameId`, `TeamId`, `Compare`, `Value` | Compares a team's score. |
|
||||||
|
| `Counter` | `MinigameId`, `CounterId`, `Compare`, `Value` | Compares a named runtime counter. |
|
||||||
|
| `CurrentRound` | `MinigameId`, `Compare`, `Round` | Compares the current runtime round number. |
|
||||||
|
| `PlayerLives` | `MinigameId`, optional `PlayerId`, `Compare`, `Value` | Compares a player's remaining lives. |
|
||||||
|
| `TeamCondition` | `MinigameId`, optional `PlayerId`, `TeamId` | Passes when the player is assigned to the expected team. |
|
||||||
|
| `TimerElapsed` | `MinigameId`, `TimerName` | Passes when a named runtime timer exists and has elapsed. |
|
||||||
|
| `ObjectiveStatus` | `MinigameId`, `ObjectiveId`, `Status` | Passes when an objective has the expected status. |
|
||||||
|
| `WinConditionMet` | `MinigameId`, optional `TargetScore` | Evaluates the runtime definition's win condition for `LAST_ALIVE`, `FIRST_TO_SCORE`, and `OBJECTIVE_COMPLETE`. |
|
||||||
|
|
||||||
|
### Compare Values
|
||||||
|
|
||||||
|
`PlayerScore`, `TeamScore`, `Counter`, `CurrentRound`, and `PlayerLives` use the `NumberCompare` enum:
|
||||||
|
|
||||||
|
| Value | Meaning |
|
||||||
|
|-------|---------|
|
||||||
|
| `EQUAL` | Actual value equals `Value`. |
|
||||||
|
| `NOT_EQUAL` | Actual value does not equal `Value`. |
|
||||||
|
| `GREATER_THAN` | Actual value is greater than `Value`. |
|
||||||
|
| `GREATER_THAN_OR_EQUAL` | Actual value is greater than or equal to `Value`. |
|
||||||
|
| `LESS_THAN` | Actual value is less than `Value`. |
|
||||||
|
| `LESS_THAN_OR_EQUAL` | Actual value is less than or equal to `Value`. |
|
||||||
|
|
||||||
|
## Effects
|
||||||
|
|
||||||
|
All minigame effects that operate on a runtime include a `MinigameId` field. In the editor this is registered as an asset picker backed by `MinigameDefinition` assets.
|
||||||
|
|
||||||
|
| Type ID | Fields | Description |
|
||||||
|
|---------|--------|-------------|
|
||||||
|
| `StartMinigame` | `MinigameId` | Starts a minigame runtime using the triggering volume's map tags when present. |
|
||||||
|
| `JoinMinigameQueue` | `MinigameId` | Adds the triggering player to the minigame waiting session. |
|
||||||
|
| `LeaveMinigameQueue` | `MinigameId` | Removes the triggering player from the minigame waiting session and clears their vote. |
|
||||||
|
| `VoteForMap` | `MinigameId`, optional `MapId` | Votes for a map in the waiting session. If `MapId` is blank, the triggering volume's `MapId` tag is used. |
|
||||||
|
| `StartQueuedMinigame` | `MinigameId` | Starts a queued match using `MapSelectionMode` and discovered map tags. |
|
||||||
|
| `SpawnItem` | optional `MinigameId`, `ItemId`, optional `Amount`, `RespawnDelaySeconds`, `SpawnKey`, `OffsetX`, `OffsetY`, `OffsetZ` | Spawns an item entity on the ground at the trigger volume position. When attached to a `TICK` volume, it respawns after the previous item is picked up or despawns and the delay has elapsed. |
|
||||||
|
| `AwardKillScore` | optional `MinigameId`, `TargetIds`, `Points`, `AwardTarget` | Configures kill scoring for deaths inside this volume. `TargetIds` accepts `Player`, NPC role ids such as `fox`, or `*`. `AwardTarget` is `PLAYER`, `TEAM`, or `BOTH`. |
|
||||||
|
| `EndMinigame` | `MinigameId`, optional `Reason` | Ends the selected minigame runtime. |
|
||||||
|
| `ResetMinigame` | `MinigameId` | Resets the selected minigame runtime. |
|
||||||
|
| `SetMinigamePhase` | `MinigameId`, `Phase` | Sets the runtime phase directly. |
|
||||||
|
| `ResetScores` | `MinigameId` | Resets all player and team scores for the runtime. |
|
||||||
|
| `ModifyPlayerScore` | `MinigameId`, optional `PlayerId`, `Operation`, `Amount` | Sets, adds, or subtracts a player's score. |
|
||||||
|
| `ModifyTeamScore` | `MinigameId`, `TeamId`, `Operation`, `Amount` | Sets, adds, or subtracts a team's score. |
|
||||||
|
| `ModifyCounter` | `MinigameId`, `CounterId`, `Operation`, `Amount` | Sets, adds, or subtracts a named runtime counter. |
|
||||||
|
| `ModifyPlayerLives` | `MinigameId`, optional `PlayerId`, `Operation`, `Amount` | Sets, adds, or subtracts a player's remaining lives. |
|
||||||
|
| `SetPlayerStatus` | `MinigameId`, optional `PlayerId`, `Status` | Sets a player's minigame status. |
|
||||||
|
| `SetRound` | `MinigameId`, `Operation`, `Amount` | Sets, adds, or subtracts the runtime round number and dispatches `on_round_start`. |
|
||||||
|
| `StartTimer` | `MinigameId`, `TimerName`, `DurationSeconds` | Starts a named runtime timer and signal timer. |
|
||||||
|
| `StopTimer` | `MinigameId`, `TimerName` | Stops a named runtime timer and cancels the signal timer. |
|
||||||
|
| `SetSpawnPoint` | `MinigameId`, optional `PlayerId`, `DestinationId` | Stores a player's respawn destination/checkpoint. |
|
||||||
|
| `RespawnPlayer` | `MinigameId`, optional `PlayerId`, optional `DestinationId` | Respawns a player using explicit destination, checkpoint, then team spawn fallback. |
|
||||||
|
| `SendMinigameMessage` | `MinigameId`, `MessageKey`, `Target` | Sends a translated message to `PLAYER`, `TEAM`, or `ALL`. |
|
||||||
|
| `AssignTeam` | `MinigameId`, optional `PlayerId`, `TeamId` | Assigns a player to a team in the runtime. |
|
||||||
|
| `SaveInventory` | `MinigameId`, optional `PlayerId` | Saves the player's current inventory into runtime state. |
|
||||||
|
| `RestoreInventory` | `MinigameId`, optional `PlayerId` | Restores the player's saved runtime inventory. |
|
||||||
|
| `ClearInventory` | `MinigameId`, optional `PlayerId` | Clears the player's inventory. |
|
||||||
|
| `SetObjectiveStatus` | `MinigameId`, `ObjectiveId`, `Status` | Creates or updates an objective status and dispatches `on_objective_complete` when complete. |
|
||||||
|
| `ModifyObjectiveProgress` | `MinigameId`, `ObjectiveId`, `Operation`, `Amount` | Sets, adds, or subtracts objective progress and completes an active objective when progress reaches the required threshold. |
|
||||||
|
|
||||||
|
### Operation Values
|
||||||
|
|
||||||
|
Score, counter, and lives mutation effects use:
|
||||||
|
|
||||||
|
| Value | Meaning |
|
||||||
|
|-------|---------|
|
||||||
|
| `SET` | Replace the current value with `Amount`. |
|
||||||
|
| `ADD` | Add `Amount` to the current value. |
|
||||||
|
| `SUBTRACT` | Subtract `Amount` from the current value. |
|
||||||
|
|
||||||
|
## Player Resolution
|
||||||
|
|
||||||
|
Several conditions and effects allow `PlayerId` to be omitted. When it is blank, the implementation attempts to resolve the triggering player from Hytale's trigger context. This is the intended setup for normal enter/interact trigger volumes.
|
||||||
|
|
||||||
|
Use an explicit `PlayerId` only when the trigger is not naturally tied to the player who caused it.
|
||||||
|
|
||||||
|
## Respawn Resolution
|
||||||
|
|
||||||
|
`RespawnPlayer` resolves where to send the player in this order:
|
||||||
|
|
||||||
|
1. The effect's explicit `DestinationId`.
|
||||||
|
2. The player's stored checkpoint from `SetSpawnPoint`.
|
||||||
|
3. A random team spawn volume matching the player's `TeamId`.
|
||||||
|
|
||||||
|
Team spawn volumes are normal Hytale trigger volumes tagged with `MinigameId`, `MapId`, optional `VariantId`, `SpawnRole=TeamSpawn`, and `TeamId=<team id>`.
|
||||||
|
|
||||||
|
## Item Spawners
|
||||||
|
|
||||||
|
`SpawnItem` creates a normal Hytale item drop, not a direct inventory grant. Use it for health packs, ammo, weapons, armor, and other pickups placed in the arena.
|
||||||
|
|
||||||
|
For automatic respawns, attach it to a trigger volume that fires on `TICK`. The effect keeps one active item per volume plus `SpawnKey`; once that item reference is invalid, it waits `RespawnDelaySeconds` before spawning the next copy. If one volume hosts several item spawners, give each effect a different `SpawnKey`.
|
||||||
|
|
||||||
|
## Kill Scoring
|
||||||
|
|
||||||
|
`AwardKillScore` is configured on a normal trigger volume, but scoring is applied by the minigame death system rather than by volume enter/exit/tick. The volume must cover the area where deaths should count and should use the normal `MinigameId`, `MapId`, and optional `VariantId` tags for runtime routing.
|
||||||
|
|
||||||
|
Use `TargetIds = ["Player"]` for player kills, NPC role ids such as `fox` for specific NPC kills, or `["*"]` for any supported target. Friendly kills use `FriendlyFireKillScoreMode` on the `MinigameDefinition`.
|
||||||
|
|
||||||
|
## Runtime Resolution
|
||||||
|
|
||||||
|
Effects and conditions resolve a runtime in this order:
|
||||||
|
|
||||||
|
1. The triggering player's active runtime.
|
||||||
|
2. A runtime matching the triggering volume's `MinigameId`, `MapId`, and `VariantId` tags.
|
||||||
|
3. The configured `MinigameId` as a fallback.
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
# Examples
|
||||||
|
|
||||||
|
These examples describe the current Hytale-native setup: minigame definitions are assets, and world interaction is done with normal Hytale trigger volumes using the minigame trigger condition/effect types.
|
||||||
|
|
||||||
|
## Simple Start Pad
|
||||||
|
|
||||||
|
Definition file: `Server/Minigames/Quick_Brawl.json`
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"Id": "Quick_Brawl",
|
||||||
|
"Name": "Quick Brawl",
|
||||||
|
"Description": "A short solo score test.",
|
||||||
|
"Enabled": true,
|
||||||
|
"MinPlayers": 1,
|
||||||
|
"MaxPlayers": 8,
|
||||||
|
"TeamMode": "SOLO",
|
||||||
|
"WinCondition": "HIGHEST_SCORE",
|
||||||
|
"RoundLengthSeconds": 120,
|
||||||
|
"CountdownSeconds": 5,
|
||||||
|
"AllowPvp": true
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Editor setup:
|
||||||
|
|
||||||
|
1. Create a normal Hytale trigger volume at the start pad.
|
||||||
|
2. Add the `StartMinigame` effect.
|
||||||
|
3. Set `MinigameId` to `Quick_Brawl` using the asset picker.
|
||||||
|
|
||||||
|
## Score Zone
|
||||||
|
|
||||||
|
Definition file: `Server/Minigames/Hill_Points.json`
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"Id": "Hill_Points",
|
||||||
|
"Name": "Hill Points",
|
||||||
|
"Enabled": true,
|
||||||
|
"MinPlayers": 1,
|
||||||
|
"MaxPlayers": 12,
|
||||||
|
"TeamMode": "SOLO",
|
||||||
|
"WinCondition": "FIRST_TO_SCORE",
|
||||||
|
"RoundLengthSeconds": 300
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Editor setup:
|
||||||
|
|
||||||
|
1. Create a normal Hytale trigger volume around the scoring area.
|
||||||
|
2. Add `PlayerInMinigame` as a condition.
|
||||||
|
3. Add `ModifyPlayerScore` as an effect.
|
||||||
|
4. Set `MinigameId = Hill_Points`, `Operation = ADD`, and `Amount = 1`.
|
||||||
|
|
||||||
|
If the trigger context already contains the player who entered the volume, leave `PlayerId` blank.
|
||||||
|
|
||||||
|
## End When Score Reaches a Target
|
||||||
|
|
||||||
|
Condition:
|
||||||
|
|
||||||
|
- Type: `PlayerScore`
|
||||||
|
- `MinigameId = Hill_Points`
|
||||||
|
- `Compare = GREATER_THAN_OR_EQUAL`
|
||||||
|
- `Value = 100`
|
||||||
|
|
||||||
|
Effect:
|
||||||
|
|
||||||
|
- Type: `EndMinigame`
|
||||||
|
- `MinigameId = Hill_Points`
|
||||||
|
- `Reason = score_reached`
|
||||||
|
|
||||||
|
## Team Score Zone
|
||||||
|
|
||||||
|
Use `ModifyTeamScore` when the score belongs to a team instead of a player.
|
||||||
|
|
||||||
|
Required fields:
|
||||||
|
|
||||||
|
- `MinigameId`
|
||||||
|
- `TeamId`
|
||||||
|
- `Operation`
|
||||||
|
- `Amount`
|
||||||
|
|
||||||
|
The current implementation expects an explicit `TeamId`.
|
||||||
|
|
||||||
|
## Counter Gate
|
||||||
|
|
||||||
|
Increment:
|
||||||
|
|
||||||
|
- Effect: `ModifyCounter`
|
||||||
|
- `CounterId = captured_points`
|
||||||
|
- `Operation = ADD`
|
||||||
|
- `Amount = 1`
|
||||||
|
|
||||||
|
Gate:
|
||||||
|
|
||||||
|
- Condition: `Counter`
|
||||||
|
- `CounterId = captured_points`
|
||||||
|
- `Compare = GREATER_THAN_OR_EQUAL`
|
||||||
|
- `Value = 3`
|
||||||
|
|
||||||
|
Then attach a follow-up effect, such as `SetMinigamePhase` or `EndMinigame`.
|
||||||
@@ -0,0 +1,147 @@
|
|||||||
|
# Minigame Definition Reference
|
||||||
|
|
||||||
|
Minigame definitions live in `src/main/resources/Server/Minigames/<Id>.json`. JSON keys use PascalCase. The filename should match the `Id` field.
|
||||||
|
|
||||||
|
## Identity
|
||||||
|
|
||||||
|
| Field | Type | Required | Default | Description |
|
||||||
|
|-------|------|----------|---------|-------------|
|
||||||
|
| `Id` | String | Yes | none | Unique asset ID. Use underscores instead of spaces. |
|
||||||
|
| `Name` | String | Yes | none | Display name shown in commands and UI. |
|
||||||
|
| `Description` | String | No | `""` | Short display description. |
|
||||||
|
| `Enabled` | Boolean | No | `true` | Whether the minigame can be started. |
|
||||||
|
| `Debug` | Boolean | No | `false` | Enables verbose minigame debug messages when supported by runtime logic. |
|
||||||
|
| `GameType` | Enum | No | `GENERIC` | `GENERIC`, `BATTLE`, `PUZZLE`, `RACE`, `CAPTURE`, or `SURVIVAL`. |
|
||||||
|
|
||||||
|
## World Binding
|
||||||
|
|
||||||
|
| Field | Type | Required | Default | Description |
|
||||||
|
|-------|------|----------|---------|-------------|
|
||||||
|
| `WorldId` | String | No | `""` | Optional world identifier. |
|
||||||
|
| `RequiredGameMode` | String | No | `"adventure"` | Game mode expected for players. |
|
||||||
|
|
||||||
|
## Players
|
||||||
|
|
||||||
|
| Field | Type | Required | Default | Description |
|
||||||
|
|-------|------|----------|---------|-------------|
|
||||||
|
| `MinPlayers` | Integer | No | `1` | Minimum player count. |
|
||||||
|
| `MaxPlayers` | Integer | No | `16` | Maximum player count. |
|
||||||
|
| `AllowJoinMidgame` | Boolean | No | `false` | Whether players can join after the game starts. |
|
||||||
|
| `AllowSpectators` | Boolean | No | `true` | Whether spectators are allowed. |
|
||||||
|
|
||||||
|
## Teams
|
||||||
|
|
||||||
|
| Field | Type | Required | Default | Description |
|
||||||
|
|-------|------|----------|---------|-------------|
|
||||||
|
| `TeamMode` | Enum | No | `SOLO` | `SOLO`, `TEAMS`, or `FREE_FOR_ALL`. |
|
||||||
|
| `TeamCount` | Integer | No | `0` | Number of teams when team mode is enabled. `0` means automatic. |
|
||||||
|
| `PlayersPerTeam` | Integer | No | `0` | Maximum players per team. `0` means no explicit limit. |
|
||||||
|
|
||||||
|
## Timing
|
||||||
|
|
||||||
|
| Field | Type | Required | Default | Description |
|
||||||
|
|-------|------|----------|---------|-------------|
|
||||||
|
| `RoundLengthSeconds` | Integer | No | `300` | Active round duration. |
|
||||||
|
| `TotalRounds` | Integer | No | `1` | Number of rounds to run. Values less than or equal to `0` allow open-ended round progression. |
|
||||||
|
| `CountdownSeconds` | Integer | No | `10` | Pre-game countdown duration. |
|
||||||
|
| `OvertimeEnabled` | Boolean | No | `false` | Enables overtime handling. |
|
||||||
|
| `SuddenDeathEnabled` | Boolean | No | `false` | Enables sudden-death handling. |
|
||||||
|
|
||||||
|
## Win Condition
|
||||||
|
|
||||||
|
| Field | Type | Required | Default | Description |
|
||||||
|
|-------|------|----------|---------|-------------|
|
||||||
|
| `WinCondition` | Enum | No | `HIGHEST_SCORE` | How the winner is determined. |
|
||||||
|
| `MapSelectionMode` | Enum | No | `VOTE` | How a waiting session chooses a discovered map: `VOTE` or `RANDOM`. |
|
||||||
|
| `FriendlyFireKillScoreMode` | Enum | No | `NO_POINTS` | How same-team player kills affect kill scoring: `OFF`, `LOSES_POINTS`, or `NO_POINTS`. |
|
||||||
|
|
||||||
|
Supported values:
|
||||||
|
|
||||||
|
| Value | Description |
|
||||||
|
|-------|-------------|
|
||||||
|
| `HIGHEST_SCORE` | Highest score at the end wins. |
|
||||||
|
| `FIRST_TO_SCORE` | First player or team to a target score wins. |
|
||||||
|
| `LAST_ALIVE` | Last active player or team wins. |
|
||||||
|
| `OBJECTIVE_COMPLETE` | Objective completion determines the winner. |
|
||||||
|
| `CUSTOM` | Trigger logic determines the winner. |
|
||||||
|
|
||||||
|
## World Rules
|
||||||
|
|
||||||
|
| Field | Type | Required | Default | Description |
|
||||||
|
|-------|------|----------|---------|-------------|
|
||||||
|
| `AllowPvp` | Boolean | No | `false` | Whether players can damage each other. |
|
||||||
|
| `AllowBlockBreaking` | Boolean | No | `false` | Whether blocks can be broken. |
|
||||||
|
| `AllowBlockPlacing` | Boolean | No | `false` | Whether blocks can be placed. |
|
||||||
|
|
||||||
|
## Inventory
|
||||||
|
|
||||||
|
| Field | Type | Required | Default | Description |
|
||||||
|
|-------|------|----------|---------|-------------|
|
||||||
|
| `SavePlayerInventory` | Boolean | No | `true` | Save player inventory on join. |
|
||||||
|
| `RestorePlayerInventory` | Boolean | No | `true` | Restore player inventory when the game ends. |
|
||||||
|
| `ResetOnEnd` | Boolean | No | `true` | Reset runtime state when the game ends. |
|
||||||
|
|
||||||
|
## Items
|
||||||
|
|
||||||
|
| Field | Type | Required | Default | Description |
|
||||||
|
|-------|------|----------|---------|-------------|
|
||||||
|
| `StartItems` | `ItemStackConfig[]` | No | `[]` | Items given to players at game start. |
|
||||||
|
| `Rewards` | `RewardConfig[]` | No | `[]` | Rewards distributed at game end. |
|
||||||
|
|
||||||
|
## ItemStackConfig
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"ItemId": "event_fishing_rod",
|
||||||
|
"Amount": 1
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
| Field | Type | Required | Description |
|
||||||
|
|-------|------|----------|-------------|
|
||||||
|
| `ItemId` | String | Yes | Hytale item ID. |
|
||||||
|
| `Amount` | Integer | Yes | Stack amount. |
|
||||||
|
|
||||||
|
## RewardConfig
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"Target": "winner",
|
||||||
|
"Items": [
|
||||||
|
{ "ItemId": "gold_coin", "Amount": 25 }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
| Field | Type | Required | Description |
|
||||||
|
|-------|------|----------|-------------|
|
||||||
|
| `Target` | String | Yes | `winner`, `participant`, `team_winner`, or a team ID. |
|
||||||
|
| `Items` | `ItemStackConfig[]` | Yes | Items to grant. |
|
||||||
|
|
||||||
|
## Minimal Example
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"Id": "Quick_Brawl",
|
||||||
|
"Name": "Quick Brawl",
|
||||||
|
"Enabled": true,
|
||||||
|
"MinPlayers": 2,
|
||||||
|
"MaxPlayers": 8,
|
||||||
|
"TeamMode": "SOLO",
|
||||||
|
"WinCondition": "LAST_ALIVE",
|
||||||
|
"RoundLengthSeconds": 120,
|
||||||
|
"AllowPvp": true
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Validation
|
||||||
|
|
||||||
|
Current validation is definition-focused:
|
||||||
|
|
||||||
|
- `Name` must not be blank.
|
||||||
|
- `MinPlayers` must be greater than or equal to 1.
|
||||||
|
- `MaxPlayers` must be greater than or equal to `MinPlayers`.
|
||||||
|
- `RoundLengthSeconds` must be greater than 0.
|
||||||
|
- `CountdownSeconds` must be greater than or equal to 0.
|
||||||
|
|
||||||
|
The old custom `MinigameAreaVolume` validation requirement no longer applies. Minigame areas are normal Hytale trigger volumes configured in the editor.
|
||||||
+103
@@ -0,0 +1,103 @@
|
|||||||
|
# Trigger Volume Tags
|
||||||
|
|
||||||
|
core-minigames uses normal Hytale trigger-volume tags for map discovery, runtime routing, team spawns, kill scoring, and runtime signals. Tag keys and values are case-sensitive.
|
||||||
|
|
||||||
|
## Map And Runtime Tags
|
||||||
|
|
||||||
|
These tags identify which minigame map or variant a volume belongs to.
|
||||||
|
|
||||||
|
| Tag | Required | Values | Used By |
|
||||||
|
|-----|----------|--------|---------|
|
||||||
|
| `MinigameId` | Yes for discovered map volumes | A loaded `MinigameDefinition` ID, such as `KOTH` | Map discovery, runtime routing, queue start, kill scoring, respawns, signals |
|
||||||
|
| `MapId` | Yes for discovered map volumes | Map or arena ID, such as `Castle` | Map discovery, voting, runtime routing |
|
||||||
|
| `VariantId` | No | Variant ID, such as `Night` or `AltA` | Runtime routing and random variant selection |
|
||||||
|
| `MapRole` | No | `MainArena` | Marks the primary arena volume for a discovered map candidate |
|
||||||
|
|
||||||
|
Minimum map candidate:
|
||||||
|
|
||||||
|
```text
|
||||||
|
MinigameId=KOTH
|
||||||
|
MapId=Castle
|
||||||
|
MapRole=MainArena
|
||||||
|
```
|
||||||
|
|
||||||
|
Variant candidate:
|
||||||
|
|
||||||
|
```text
|
||||||
|
MinigameId=KOTH
|
||||||
|
MapId=Castle
|
||||||
|
VariantId=Night
|
||||||
|
MapRole=MainArena
|
||||||
|
```
|
||||||
|
|
||||||
|
Overlapping volumes are allowed. Volumes with the same `MinigameId`, `MapId`, and `VariantId` are grouped into one discovered map candidate.
|
||||||
|
|
||||||
|
## Spawn Tags
|
||||||
|
|
||||||
|
Team spawn volumes are normal trigger volumes used as teleport destinations by `RespawnPlayer`.
|
||||||
|
|
||||||
|
| Tag | Required | Values | Used By |
|
||||||
|
|-----|----------|--------|---------|
|
||||||
|
| `MinigameId` | Recommended | Minigame ID | Filters spawns to the current runtime's minigame |
|
||||||
|
| `MapId` | Recommended | Map ID | Filters spawns to the current runtime's map |
|
||||||
|
| `VariantId` | No | Variant ID | Filters spawns to the current runtime's variant when present |
|
||||||
|
| `SpawnRole` | Yes | `TeamSpawn` | Marks the volume as a team spawn candidate |
|
||||||
|
| `TeamId` | Yes | Team ID, such as `red` or `blue` | Selects spawns for the player's assigned team |
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```text
|
||||||
|
MinigameId=KOTH
|
||||||
|
MapId=Castle
|
||||||
|
SpawnRole=TeamSpawn
|
||||||
|
TeamId=red
|
||||||
|
```
|
||||||
|
|
||||||
|
`RespawnPlayer` chooses a random matching team spawn when no explicit `DestinationId` and no player checkpoint are available.
|
||||||
|
|
||||||
|
## Signal Tags
|
||||||
|
|
||||||
|
Signal tags let runtime state changes fire normal Hytale trigger-volume logic by toggling a tag on matching volumes.
|
||||||
|
|
||||||
|
| Tag | Required | Values | Behavior |
|
||||||
|
|-----|----------|--------|----------|
|
||||||
|
| `SignalRoundStart` | No | Round number, such as `1` | Fires once when that round starts. |
|
||||||
|
| `SignalRoundTick` | No | Round number, such as `1` | Fires repeatedly while that round is active. |
|
||||||
|
| `SignalPhaseTick` | No | `MinigamePhase` enum name, such as `ACTIVE` | Fires repeatedly while the runtime is in that phase. |
|
||||||
|
| `SignalTickDelay` | No | Seconds, such as `1` or `0.5` | Delay between repeated signal fires. Minimum effective delay is 50ms. |
|
||||||
|
| `SignalOnTimerExpire` | No | Timer name from `StartTimer` | Fires once when the named timer expires. |
|
||||||
|
| `MinigameSignal` | Internal | `0` or `1` | Written by the runtime signal service to trigger Hytale `TAG_ADDED` behavior. Do not configure manually. |
|
||||||
|
|
||||||
|
Signal volumes must also have `MinigameId=<id>`. Timer names starting with `$` are internal runtime timers and do not fire trigger-volume signal tags.
|
||||||
|
|
||||||
|
Round-start example:
|
||||||
|
|
||||||
|
```text
|
||||||
|
MinigameId=KOTH
|
||||||
|
MapId=Castle
|
||||||
|
SignalRoundStart=1
|
||||||
|
```
|
||||||
|
|
||||||
|
Active-phase tick example:
|
||||||
|
|
||||||
|
```text
|
||||||
|
MinigameId=KOTH
|
||||||
|
MapId=Castle
|
||||||
|
SignalPhaseTick=ACTIVE
|
||||||
|
SignalTickDelay=1
|
||||||
|
```
|
||||||
|
|
||||||
|
Timer-expire example:
|
||||||
|
|
||||||
|
```text
|
||||||
|
MinigameId=KOTH
|
||||||
|
MapId=Castle
|
||||||
|
SignalOnTimerExpire=overtime_warning
|
||||||
|
```
|
||||||
|
|
||||||
|
## Validation Notes
|
||||||
|
|
||||||
|
- A volume with `MapId` but no `MinigameId` produces a warning and is ignored for map discovery.
|
||||||
|
- A discovered map candidate without `MapRole=MainArena` produces a warning but does not stop the minigame from loading.
|
||||||
|
- Missing map discovery is a warning, not a startup failure.
|
||||||
|
- Duplicate or overlapping variants are allowed when they intentionally represent alternate zones for the same map.
|
||||||
+122
@@ -0,0 +1,122 @@
|
|||||||
|
# Volumes
|
||||||
|
|
||||||
|
Minigame areas now use Hytale's built-in trigger volumes. The mod no longer defines custom volume types such as `MinigameAreaVolume`, `JoinQueueVolume`, or `ScoreVolume`.
|
||||||
|
|
||||||
|
This is intentional: the area, shape, enter/exit behavior, tags, and trigger-effect asset wiring belong to Hytale's trigger-volume system. core-minigames adds minigame-specific conditions and effects that can be attached to those trigger volumes.
|
||||||
|
|
||||||
|
## Defining Minigame Maps
|
||||||
|
|
||||||
|
1. Create a normal Hytale trigger volume in the editor.
|
||||||
|
2. Shape it around the arena or interaction area.
|
||||||
|
3. Tag it with `MinigameId=<MinigameId>` and `MapId=<MapId>`.
|
||||||
|
4. Add `VariantId=<VariantId>` when the same map can randomly use overlapping alternate zones.
|
||||||
|
5. Add `MapRole=MainArena` to the primary arena volume for each map or variant.
|
||||||
|
6. Attach trigger effects and conditions for the relevant minigame.
|
||||||
|
7. Use the `MinigameId` dropdown to select the `MinigameDefinition` asset.
|
||||||
|
|
||||||
|
There is no required root `MinigameAreaVolume` type. If validation needs an arena boundary later, it should inspect Hytale trigger-volume metadata instead of requiring a custom minigame volume class.
|
||||||
|
|
||||||
|
Map candidates are discovered from trigger-volume tags. See [Trigger Volume Tags](tags.md) for the full tag reference.
|
||||||
|
|
||||||
|
Overlapping volumes are valid. Runtime routing uses the selected `MinigameId`, `MapId`, and `VariantId`, so the same Hytale build can host multiple arena definitions.
|
||||||
|
|
||||||
|
## Common Patterns
|
||||||
|
|
||||||
|
### Start Zone
|
||||||
|
|
||||||
|
Use a normal trigger volume at an entrance, sign, NPC, or lobby pad.
|
||||||
|
|
||||||
|
Attach:
|
||||||
|
|
||||||
|
- Effect: `JoinMinigameQueue`
|
||||||
|
- Field: `MinigameId = <your minigame>`
|
||||||
|
|
||||||
|
For exits or cancel pads:
|
||||||
|
|
||||||
|
- Effect: `LeaveMinigameQueue`
|
||||||
|
- Field: `MinigameId = <your minigame>`
|
||||||
|
|
||||||
|
For voting buttons or pads:
|
||||||
|
|
||||||
|
- Effect: `VoteForMap`
|
||||||
|
- Field: `MapId = <map id>`, or leave blank to use the volume's `MapId` tag.
|
||||||
|
|
||||||
|
To start the queued match:
|
||||||
|
|
||||||
|
- Effect: `StartQueuedMinigame`
|
||||||
|
- Field: `MinigameId = <your minigame>`
|
||||||
|
|
||||||
|
Optionally guard it with:
|
||||||
|
|
||||||
|
- Condition: `MinigamePhase`
|
||||||
|
- Field: `Phase = WAITING_FOR_PLAYERS`
|
||||||
|
|
||||||
|
### Score Zone
|
||||||
|
|
||||||
|
Use a normal trigger volume around the scoring area.
|
||||||
|
|
||||||
|
Attach:
|
||||||
|
|
||||||
|
- Condition: `PlayerInMinigame`
|
||||||
|
- Effect: `ModifyPlayerScore`
|
||||||
|
- Fields: `Operation = ADD`, `Amount = <points>`
|
||||||
|
|
||||||
|
If the score is team-based, use `ModifyTeamScore` instead.
|
||||||
|
|
||||||
|
### Phase Gate
|
||||||
|
|
||||||
|
Use Hytale's normal volume behavior for the area and guard attached effects with:
|
||||||
|
|
||||||
|
- Condition: `MinigamePhase`
|
||||||
|
- Field: `Phase = ACTIVE`
|
||||||
|
|
||||||
|
### Team Spawns
|
||||||
|
|
||||||
|
Use normal trigger volumes as spawn markers. The volume position is the teleport destination.
|
||||||
|
|
||||||
|
Tag each team spawn volume with:
|
||||||
|
|
||||||
|
- `MinigameId=<your minigame>`
|
||||||
|
- `MapId=<map id>`
|
||||||
|
- optional `VariantId=<variant id>`
|
||||||
|
- `SpawnRole=TeamSpawn`
|
||||||
|
- `TeamId=<team id>`
|
||||||
|
|
||||||
|
`RespawnPlayer` resolves destinations in this order:
|
||||||
|
|
||||||
|
1. The effect's explicit `DestinationId`.
|
||||||
|
2. The player's stored checkpoint from `SetSpawnPoint`.
|
||||||
|
3. A random matching team spawn volume for the player's `TeamId`.
|
||||||
|
|
||||||
|
This means checkpoint-based games can still override spawns per player, while team arena games can rely on tagged team spawn volumes.
|
||||||
|
|
||||||
|
### Counters
|
||||||
|
|
||||||
|
Use a trigger volume to increment a named runtime counter:
|
||||||
|
|
||||||
|
- Effect: `ModifyCounter`
|
||||||
|
- Fields: `CounterId = <name>`, `Operation = ADD`, `Amount = 1`
|
||||||
|
|
||||||
|
Then use the `Counter` condition to branch once a threshold is reached.
|
||||||
|
|
||||||
|
### Kill Scoring
|
||||||
|
|
||||||
|
Use a trigger volume covering the arena or scoring zone and attach:
|
||||||
|
|
||||||
|
- Effect: `AwardKillScore`
|
||||||
|
- Fields: `TargetIds = ["Player"]` or NPC role ids such as `["fox"]`, `Points = <points>`, `AwardTarget = PLAYER`
|
||||||
|
|
||||||
|
The death system checks this volume when an entity dies inside it. It routes by the volume's `MinigameId`, `MapId`, and optional `VariantId` tags, so overlapping arenas can have different kill scoring rules.
|
||||||
|
|
||||||
|
### Pickup Spawners
|
||||||
|
|
||||||
|
Use a small trigger volume at the pickup location and attach:
|
||||||
|
|
||||||
|
- Effect: `SpawnItem`
|
||||||
|
- Fields: `ItemId = <item id>`, `Amount = <stack size>`, `RespawnDelaySeconds = <seconds>`
|
||||||
|
|
||||||
|
The item spawns at the volume position plus optional `OffsetX`, `OffsetY`, and `OffsetZ`. Attach the effect to a `TICK` trigger when the pickup should automatically respawn after being collected or despawning. If the same volume has multiple item spawners, set a different `SpawnKey` for each one.
|
||||||
|
|
||||||
|
## Reusable Effect Assets
|
||||||
|
|
||||||
|
For repeated logic, define a Hytale trigger effect asset and attach it to multiple trigger volumes. The minigame trigger condition/effect entries are normal codec types, so they can be reused the same way Hytale's built-in trigger-volume effects are reused.
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
# The current version of your project. Please use semantic versioning!
|
||||||
|
version=0.5.3
|
||||||
|
|
||||||
|
# The group ID used for maven publishing. Usually the same as your package name
|
||||||
|
# but not the same as your plugin group!
|
||||||
|
maven_group=net.kewwbec
|
||||||
|
|
||||||
|
# The version of Java used by your plugin. The game is built on Java 21 but
|
||||||
|
# actually runs on Java 25.
|
||||||
|
java_version=25
|
||||||
|
|
||||||
|
# Determines if your plugin should also be loaded as an asset pack. If your
|
||||||
|
# pack contains assets, or you intend to use the in-game asset editor, you
|
||||||
|
# want this to be true.
|
||||||
|
includes_pack=true
|
||||||
|
|
||||||
|
# The release channel your plugin should be built and ran against. This is
|
||||||
|
# usually release or pre-release. You can verify your settings in the
|
||||||
|
# official launcher.
|
||||||
|
patchline=release
|
||||||
|
|
||||||
|
# The exact Hytale build to compile against. Use the build string from the
|
||||||
|
# launcher (format YYYY.MM.DD-<hash>) so Gradle pulls the matching server jar
|
||||||
|
# for your selected patchline.
|
||||||
|
hytale_build=0.5.3
|
||||||
|
|
||||||
|
# Determines if the development server should also load mods from the user's
|
||||||
|
# standard mods folder. This lets you test mods by installing them where a
|
||||||
|
# normal player would, instead of adding them as dependencies or adding them
|
||||||
|
# to the development server manually.
|
||||||
|
load_user_mods=false
|
||||||
|
|
||||||
|
# If Hytale was installed to a custom location, you must set the home path
|
||||||
|
# manually. You may also want to use a custom path if you are building in
|
||||||
|
# a non-standard environment like a build server. The home path should
|
||||||
|
# the folder that contains the install and UserData folder.
|
||||||
|
# hytale_home=./test-file
|
||||||
Vendored
BIN
Binary file not shown.
+6
@@ -0,0 +1,6 @@
|
|||||||
|
#Tue Nov 25 02:23:10 MST 2025
|
||||||
|
distributionBase=GRADLE_USER_HOME
|
||||||
|
distributionPath=wrapper/dists
|
||||||
|
distributionUrl=https\://services.gradle.org/distributions/gradle-9.2.0-bin.zip
|
||||||
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
|
zipStorePath=wrapper/dists
|
||||||
@@ -0,0 +1,234 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
#
|
||||||
|
# Copyright © 2015-2021 the original authors.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
#
|
||||||
|
|
||||||
|
##############################################################################
|
||||||
|
#
|
||||||
|
# Gradle start up script for POSIX generated by Gradle.
|
||||||
|
#
|
||||||
|
# Important for running:
|
||||||
|
#
|
||||||
|
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
|
||||||
|
# noncompliant, but you have some other compliant shell such as ksh or
|
||||||
|
# bash, then to run this script, type that shell name before the whole
|
||||||
|
# command line, like:
|
||||||
|
#
|
||||||
|
# ksh Gradle
|
||||||
|
#
|
||||||
|
# Busybox and similar reduced shells will NOT work, because this script
|
||||||
|
# requires all of these POSIX shell features:
|
||||||
|
# * functions;
|
||||||
|
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
|
||||||
|
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
|
||||||
|
# * compound commands having a testable exit status, especially «case»;
|
||||||
|
# * various built-in commands including «command», «set», and «ulimit».
|
||||||
|
#
|
||||||
|
# Important for patching:
|
||||||
|
#
|
||||||
|
# (2) This script targets any POSIX shell, so it avoids extensions provided
|
||||||
|
# by Bash, Ksh, etc; in particular arrays are avoided.
|
||||||
|
#
|
||||||
|
# The "traditional" practice of packing multiple parameters into a
|
||||||
|
# space-separated string is a well documented source of bugs and security
|
||||||
|
# problems, so this is (mostly) avoided, by progressively accumulating
|
||||||
|
# options in "$@", and eventually passing that to Java.
|
||||||
|
#
|
||||||
|
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
|
||||||
|
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
|
||||||
|
# see the in-line comments for details.
|
||||||
|
#
|
||||||
|
# There are tweaks for specific operating systems such as AIX, CygWin,
|
||||||
|
# Darwin, MinGW, and NonStop.
|
||||||
|
#
|
||||||
|
# (3) This script is generated from the Groovy template
|
||||||
|
# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
||||||
|
# within the Gradle project.
|
||||||
|
#
|
||||||
|
# You can find Gradle at https://github.com/gradle/gradle/.
|
||||||
|
#
|
||||||
|
##############################################################################
|
||||||
|
|
||||||
|
# Attempt to set APP_HOME
|
||||||
|
|
||||||
|
# Resolve links: $0 may be a link
|
||||||
|
app_path=$0
|
||||||
|
|
||||||
|
# Need this for daisy-chained symlinks.
|
||||||
|
while
|
||||||
|
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
|
||||||
|
[ -h "$app_path" ]
|
||||||
|
do
|
||||||
|
ls=$( ls -ld "$app_path" )
|
||||||
|
link=${ls#*' -> '}
|
||||||
|
case $link in #(
|
||||||
|
/*) app_path=$link ;; #(
|
||||||
|
*) app_path=$APP_HOME$link ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
|
||||||
|
|
||||||
|
APP_NAME="Gradle"
|
||||||
|
APP_BASE_NAME=${0##*/}
|
||||||
|
|
||||||
|
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||||
|
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||||
|
|
||||||
|
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||||
|
MAX_FD=maximum
|
||||||
|
|
||||||
|
warn () {
|
||||||
|
echo "$*"
|
||||||
|
} >&2
|
||||||
|
|
||||||
|
die () {
|
||||||
|
echo
|
||||||
|
echo "$*"
|
||||||
|
echo
|
||||||
|
exit 1
|
||||||
|
} >&2
|
||||||
|
|
||||||
|
# OS specific support (must be 'true' or 'false').
|
||||||
|
cygwin=false
|
||||||
|
msys=false
|
||||||
|
darwin=false
|
||||||
|
nonstop=false
|
||||||
|
case "$( uname )" in #(
|
||||||
|
CYGWIN* ) cygwin=true ;; #(
|
||||||
|
Darwin* ) darwin=true ;; #(
|
||||||
|
MSYS* | MINGW* ) msys=true ;; #(
|
||||||
|
NONSTOP* ) nonstop=true ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
||||||
|
|
||||||
|
|
||||||
|
# Determine the Java command to use to start the JVM.
|
||||||
|
if [ -n "$JAVA_HOME" ] ; then
|
||||||
|
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
||||||
|
# IBM's JDK on AIX uses strange locations for the executables
|
||||||
|
JAVACMD=$JAVA_HOME/jre/sh/java
|
||||||
|
else
|
||||||
|
JAVACMD=$JAVA_HOME/bin/java
|
||||||
|
fi
|
||||||
|
if [ ! -x "$JAVACMD" ] ; then
|
||||||
|
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
|
||||||
|
|
||||||
|
Please set the JAVA_HOME variable in your environment to match the
|
||||||
|
location of your Java installation."
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
JAVACMD=java
|
||||||
|
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||||
|
|
||||||
|
Please set the JAVA_HOME variable in your environment to match the
|
||||||
|
location of your Java installation."
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Increase the maximum file descriptors if we can.
|
||||||
|
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
|
||||||
|
case $MAX_FD in #(
|
||||||
|
max*)
|
||||||
|
MAX_FD=$( ulimit -H -n ) ||
|
||||||
|
warn "Could not query maximum file descriptor limit"
|
||||||
|
esac
|
||||||
|
case $MAX_FD in #(
|
||||||
|
'' | soft) :;; #(
|
||||||
|
*)
|
||||||
|
ulimit -n "$MAX_FD" ||
|
||||||
|
warn "Could not set maximum file descriptor limit to $MAX_FD"
|
||||||
|
esac
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Collect all arguments for the java command, stacking in reverse order:
|
||||||
|
# * args from the command line
|
||||||
|
# * the main class name
|
||||||
|
# * -classpath
|
||||||
|
# * -D...appname settings
|
||||||
|
# * --module-path (only if needed)
|
||||||
|
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
|
||||||
|
|
||||||
|
# For Cygwin or MSYS, switch paths to Windows format before running java
|
||||||
|
if "$cygwin" || "$msys" ; then
|
||||||
|
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
|
||||||
|
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
|
||||||
|
|
||||||
|
JAVACMD=$( cygpath --unix "$JAVACMD" )
|
||||||
|
|
||||||
|
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
||||||
|
for arg do
|
||||||
|
if
|
||||||
|
case $arg in #(
|
||||||
|
-*) false ;; # don't mess with options #(
|
||||||
|
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
|
||||||
|
[ -e "$t" ] ;; #(
|
||||||
|
*) false ;;
|
||||||
|
esac
|
||||||
|
then
|
||||||
|
arg=$( cygpath --path --ignore --mixed "$arg" )
|
||||||
|
fi
|
||||||
|
# Roll the args list around exactly as many times as the number of
|
||||||
|
# args, so each arg winds up back in the position where it started, but
|
||||||
|
# possibly modified.
|
||||||
|
#
|
||||||
|
# NB: a `for` loop captures its iteration list before it begins, so
|
||||||
|
# changing the positional parameters here affects neither the number of
|
||||||
|
# iterations, nor the values presented in `arg`.
|
||||||
|
shift # remove old arg
|
||||||
|
set -- "$@" "$arg" # push replacement arg
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Collect all arguments for the java command;
|
||||||
|
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
|
||||||
|
# shell script including quotes and variable substitutions, so put them in
|
||||||
|
# double quotes to make sure that they get re-expanded; and
|
||||||
|
# * put everything else in single quotes, so that it's not re-expanded.
|
||||||
|
|
||||||
|
set -- \
|
||||||
|
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
||||||
|
-classpath "$CLASSPATH" \
|
||||||
|
org.gradle.wrapper.GradleWrapperMain \
|
||||||
|
"$@"
|
||||||
|
|
||||||
|
# Use "xargs" to parse quoted args.
|
||||||
|
#
|
||||||
|
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
|
||||||
|
#
|
||||||
|
# In Bash we could simply go:
|
||||||
|
#
|
||||||
|
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
|
||||||
|
# set -- "${ARGS[@]}" "$@"
|
||||||
|
#
|
||||||
|
# but POSIX shell has neither arrays nor command substitution, so instead we
|
||||||
|
# post-process each arg (as a line of input to sed) to backslash-escape any
|
||||||
|
# character that might be a shell metacharacter, then use eval to reverse
|
||||||
|
# that process (while maintaining the separation between arguments), and wrap
|
||||||
|
# the whole thing up as a single "set" statement.
|
||||||
|
#
|
||||||
|
# This will of course break if any of these variables contains a newline or
|
||||||
|
# an unmatched quote.
|
||||||
|
#
|
||||||
|
|
||||||
|
eval "set -- $(
|
||||||
|
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
|
||||||
|
xargs -n1 |
|
||||||
|
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
|
||||||
|
tr '\n' ' '
|
||||||
|
)" '"$@"'
|
||||||
|
|
||||||
|
exec "$JAVACMD" "$@"
|
||||||
Vendored
+89
@@ -0,0 +1,89 @@
|
|||||||
|
@rem
|
||||||
|
@rem Copyright 2015 the original author or authors.
|
||||||
|
@rem
|
||||||
|
@rem Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
@rem you may not use this file except in compliance with the License.
|
||||||
|
@rem You may obtain a copy of the License at
|
||||||
|
@rem
|
||||||
|
@rem https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
@rem
|
||||||
|
@rem Unless required by applicable law or agreed to in writing, software
|
||||||
|
@rem distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
@rem See the License for the specific language governing permissions and
|
||||||
|
@rem limitations under the License.
|
||||||
|
@rem
|
||||||
|
|
||||||
|
@if "%DEBUG%" == "" @echo off
|
||||||
|
@rem ##########################################################################
|
||||||
|
@rem
|
||||||
|
@rem Gradle startup script for Windows
|
||||||
|
@rem
|
||||||
|
@rem ##########################################################################
|
||||||
|
|
||||||
|
@rem Set local scope for the variables with windows NT shell
|
||||||
|
if "%OS%"=="Windows_NT" setlocal
|
||||||
|
|
||||||
|
set DIRNAME=%~dp0
|
||||||
|
if "%DIRNAME%" == "" set DIRNAME=.
|
||||||
|
set APP_BASE_NAME=%~n0
|
||||||
|
set APP_HOME=%DIRNAME%
|
||||||
|
|
||||||
|
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
|
||||||
|
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
|
||||||
|
|
||||||
|
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||||
|
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
|
||||||
|
|
||||||
|
@rem Find java.exe
|
||||||
|
if defined JAVA_HOME goto findJavaFromJavaHome
|
||||||
|
|
||||||
|
set JAVA_EXE=java.exe
|
||||||
|
%JAVA_EXE% -version >NUL 2>&1
|
||||||
|
if "%ERRORLEVEL%" == "0" goto execute
|
||||||
|
|
||||||
|
echo.
|
||||||
|
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||||
|
echo.
|
||||||
|
echo Please set the JAVA_HOME variable in your environment to match the
|
||||||
|
echo location of your Java installation.
|
||||||
|
|
||||||
|
goto fail
|
||||||
|
|
||||||
|
:findJavaFromJavaHome
|
||||||
|
set JAVA_HOME=%JAVA_HOME:"=%
|
||||||
|
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||||
|
|
||||||
|
if exist "%JAVA_EXE%" goto execute
|
||||||
|
|
||||||
|
echo.
|
||||||
|
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
|
||||||
|
echo.
|
||||||
|
echo Please set the JAVA_HOME variable in your environment to match the
|
||||||
|
echo location of your Java installation.
|
||||||
|
|
||||||
|
goto fail
|
||||||
|
|
||||||
|
:execute
|
||||||
|
@rem Setup the command line
|
||||||
|
|
||||||
|
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
||||||
|
|
||||||
|
|
||||||
|
@rem Execute Gradle
|
||||||
|
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
|
||||||
|
|
||||||
|
:end
|
||||||
|
@rem End local scope for the variables with windows NT shell
|
||||||
|
if "%ERRORLEVEL%"=="0" goto mainEnd
|
||||||
|
|
||||||
|
:fail
|
||||||
|
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
||||||
|
rem the _cmd.exe /c_ return code!
|
||||||
|
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
|
||||||
|
exit /b 1
|
||||||
|
|
||||||
|
:mainEnd
|
||||||
|
if "%OS%"=="Windows_NT" endlocal
|
||||||
|
|
||||||
|
:omega
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
rootProject.name = 'core-minigames'
|
||||||
@@ -0,0 +1,287 @@
|
|||||||
|
package net.kewwbec.minigames;
|
||||||
|
|
||||||
|
import com.hypixel.hytale.assetstore.AssetRegistry;
|
||||||
|
import com.hypixel.hytale.assetstore.map.DefaultAssetMap;
|
||||||
|
import com.hypixel.hytale.builtin.triggervolumes.TriggerVolumesPlugin;
|
||||||
|
import com.hypixel.hytale.builtin.triggervolumes.asset.TriggerEffectAsset;
|
||||||
|
import com.hypixel.hytale.builtin.triggervolumes.effect.TriggerCondition;
|
||||||
|
import com.hypixel.hytale.builtin.triggervolumes.effect.TriggerEffect;
|
||||||
|
import com.hypixel.hytale.codec.builder.BuilderCodec;
|
||||||
|
import com.hypixel.hytale.logger.HytaleLogger;
|
||||||
|
import com.hypixel.hytale.server.core.asset.HytaleAssetStore;
|
||||||
|
import com.hypixel.hytale.server.core.plugin.JavaPlugin;
|
||||||
|
import com.hypixel.hytale.server.core.plugin.JavaPluginInit;
|
||||||
|
import net.kewwbec.minigames.adapter.HytaleServerAdapters;
|
||||||
|
import net.kewwbec.minigames.command.MinigameCommand;
|
||||||
|
import net.kewwbec.minigames.model.MinigameDefinition;
|
||||||
|
import net.kewwbec.minigames.runtime.DefaultMinigameRuntimeService;
|
||||||
|
import net.kewwbec.minigames.service.DefaultMinigameService;
|
||||||
|
import net.kewwbec.minigames.service.MinigameMapDiscoveryService;
|
||||||
|
import net.kewwbec.minigames.service.MinigameQueueService;
|
||||||
|
import net.kewwbec.minigames.service.MinigameServices;
|
||||||
|
import net.kewwbec.minigames.service.MinigameVolumeSignalService;
|
||||||
|
import net.kewwbec.minigames.system.MinigameKillScoreSystem;
|
||||||
|
import net.kewwbec.minigames.volume.condition.CounterCondition;
|
||||||
|
import net.kewwbec.minigames.volume.condition.CurrentRoundCondition;
|
||||||
|
import net.kewwbec.minigames.volume.condition.MinigamePhaseCondition;
|
||||||
|
import net.kewwbec.minigames.volume.condition.ObjectiveStatusCondition;
|
||||||
|
import net.kewwbec.minigames.volume.condition.PlayerInMinigameCondition;
|
||||||
|
import net.kewwbec.minigames.volume.condition.PlayerLivesCondition;
|
||||||
|
import net.kewwbec.minigames.volume.condition.PlayerScoreCondition;
|
||||||
|
import net.kewwbec.minigames.volume.condition.PlayerStatusCondition;
|
||||||
|
import net.kewwbec.minigames.volume.condition.TeamCondition;
|
||||||
|
import net.kewwbec.minigames.volume.condition.TeamScoreCondition;
|
||||||
|
import net.kewwbec.minigames.volume.condition.TimerElapsedCondition;
|
||||||
|
import net.kewwbec.minigames.volume.condition.WinConditionMetCondition;
|
||||||
|
import net.kewwbec.minigames.volume.effect.AssignTeamEffect;
|
||||||
|
import net.kewwbec.minigames.volume.effect.AwardKillScoreEffect;
|
||||||
|
import net.kewwbec.minigames.volume.effect.ClearInventoryEffect;
|
||||||
|
import net.kewwbec.minigames.volume.effect.EndMinigameEffect;
|
||||||
|
import net.kewwbec.minigames.volume.effect.LeaveMinigameQueueEffect;
|
||||||
|
import net.kewwbec.minigames.volume.effect.ModifyCounterEffect;
|
||||||
|
import net.kewwbec.minigames.volume.effect.ModifyObjectiveProgressEffect;
|
||||||
|
import net.kewwbec.minigames.volume.effect.ModifyPlayerLivesEffect;
|
||||||
|
import net.kewwbec.minigames.volume.effect.ModifyPlayerScoreEffect;
|
||||||
|
import net.kewwbec.minigames.volume.effect.ModifyTeamScoreEffect;
|
||||||
|
import net.kewwbec.minigames.volume.effect.RespawnPlayerEffect;
|
||||||
|
import net.kewwbec.minigames.volume.effect.ResetMinigameEffect;
|
||||||
|
import net.kewwbec.minigames.volume.effect.ResetScoresEffect;
|
||||||
|
import net.kewwbec.minigames.volume.effect.RestoreInventoryEffect;
|
||||||
|
import net.kewwbec.minigames.volume.effect.SaveInventoryEffect;
|
||||||
|
import net.kewwbec.minigames.volume.effect.SendMinigameMessageEffect;
|
||||||
|
import net.kewwbec.minigames.volume.effect.SetMinigamePhaseEffect;
|
||||||
|
import net.kewwbec.minigames.volume.effect.SetObjectiveStatusEffect;
|
||||||
|
import net.kewwbec.minigames.volume.effect.SetPlayerStatusEffect;
|
||||||
|
import net.kewwbec.minigames.volume.effect.SetRoundEffect;
|
||||||
|
import net.kewwbec.minigames.volume.effect.SetSpawnPointEffect;
|
||||||
|
import net.kewwbec.minigames.volume.effect.JoinMinigameQueueEffect;
|
||||||
|
import net.kewwbec.minigames.volume.effect.SpawnItemEffect;
|
||||||
|
import net.kewwbec.minigames.volume.effect.StartMinigameEffect;
|
||||||
|
import net.kewwbec.minigames.volume.effect.StartQueuedMinigameEffect;
|
||||||
|
import net.kewwbec.minigames.volume.effect.StartTimerEffect;
|
||||||
|
import net.kewwbec.minigames.volume.effect.StopTimerEffect;
|
||||||
|
import net.kewwbec.minigames.volume.effect.VoteForMapEffect;
|
||||||
|
|
||||||
|
import javax.annotation.Nonnull;
|
||||||
|
import javax.annotation.Nullable;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
|
||||||
|
public final class MinigameCorePlugin extends JavaPlugin {
|
||||||
|
private static final HytaleLogger LOGGER = HytaleLogger.forEnclosingClass();
|
||||||
|
private static MinigameCorePlugin instance;
|
||||||
|
private MinigameServices services;
|
||||||
|
|
||||||
|
public MinigameCorePlugin(@Nonnull JavaPluginInit init) {
|
||||||
|
super(init);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void setup() {
|
||||||
|
instance = this;
|
||||||
|
getEntityStoreRegistry().registerSystem(new MinigameKillScoreSystem());
|
||||||
|
registerTriggerVolumeEffectTypes();
|
||||||
|
registerTriggerVolumeConditionTypes();
|
||||||
|
|
||||||
|
AssetRegistry.register(
|
||||||
|
HytaleAssetStore.builder(MinigameDefinition.class, new DefaultAssetMap<String, MinigameDefinition>())
|
||||||
|
.setPath("Minigames")
|
||||||
|
.setCodec(MinigameDefinition.CODEC)
|
||||||
|
.setKeyFunction(MinigameDefinition::getId)
|
||||||
|
.loadsAfter(TriggerEffectAsset.class)
|
||||||
|
.build()
|
||||||
|
);
|
||||||
|
|
||||||
|
var dataRoot = Path.of("minigame-core");
|
||||||
|
var runtimeService = new DefaultMinigameRuntimeService();
|
||||||
|
var minigameService = new DefaultMinigameService(dataRoot, runtimeService);
|
||||||
|
var adapters = new HytaleServerAdapters();
|
||||||
|
var signals = new MinigameVolumeSignalService();
|
||||||
|
runtimeService.setSignalService(signals);
|
||||||
|
runtimeService.setPlayerAdapter(adapters);
|
||||||
|
this.services = new MinigameServices(minigameService, runtimeService, adapters, new MinigameMapDiscoveryService(), new MinigameQueueService(), signals);
|
||||||
|
registerTriggerVolumeAssetSources();
|
||||||
|
|
||||||
|
this.getCommandRegistry().registerCommand(new MinigameCommand(services));
|
||||||
|
LOGGER.atInfo().log("core-minigames registered " + registeredTriggerConditionCount()
|
||||||
|
+ " trigger conditions and " + registeredTriggerEffectCount() + " trigger effects.");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void shutdown() {
|
||||||
|
if (services != null) {
|
||||||
|
services.runtime().shutdownAll("minigames.runtime.reason.plugin_shutdown");
|
||||||
|
services.signals().shutdown();
|
||||||
|
}
|
||||||
|
services = null;
|
||||||
|
instance = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
public static MinigameServices getServices() {
|
||||||
|
return instance != null ? instance.services : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void registerTriggerVolumeEffectTypes() {
|
||||||
|
var triggerVolumes = TriggerVolumesPlugin.get();
|
||||||
|
registerTriggerVolumeEffectType(triggerVolumes, StartMinigameEffect.TYPE_ID, StartMinigameEffect.class, StartMinigameEffect.CODEC);
|
||||||
|
registerTriggerVolumeEffectType(triggerVolumes, EndMinigameEffect.TYPE_ID, EndMinigameEffect.class, EndMinigameEffect.CODEC);
|
||||||
|
registerTriggerVolumeEffectType(triggerVolumes, ResetMinigameEffect.TYPE_ID, ResetMinigameEffect.class, ResetMinigameEffect.CODEC);
|
||||||
|
registerTriggerVolumeEffectType(triggerVolumes, SetMinigamePhaseEffect.TYPE_ID, SetMinigamePhaseEffect.class, SetMinigamePhaseEffect.CODEC);
|
||||||
|
registerTriggerVolumeEffectType(triggerVolumes, ResetScoresEffect.TYPE_ID, ResetScoresEffect.class, ResetScoresEffect.CODEC);
|
||||||
|
registerTriggerVolumeEffectType(triggerVolumes, ModifyPlayerScoreEffect.TYPE_ID, ModifyPlayerScoreEffect.class, ModifyPlayerScoreEffect.CODEC);
|
||||||
|
registerTriggerVolumeEffectType(triggerVolumes, ModifyTeamScoreEffect.TYPE_ID, ModifyTeamScoreEffect.class, ModifyTeamScoreEffect.CODEC);
|
||||||
|
registerTriggerVolumeEffectType(triggerVolumes, ModifyCounterEffect.TYPE_ID, ModifyCounterEffect.class, ModifyCounterEffect.CODEC);
|
||||||
|
registerTriggerVolumeEffectType(triggerVolumes, ModifyPlayerLivesEffect.TYPE_ID, ModifyPlayerLivesEffect.class, ModifyPlayerLivesEffect.CODEC);
|
||||||
|
registerTriggerVolumeEffectType(triggerVolumes, SetPlayerStatusEffect.TYPE_ID, SetPlayerStatusEffect.class, SetPlayerStatusEffect.CODEC);
|
||||||
|
registerTriggerVolumeEffectType(triggerVolumes, JoinMinigameQueueEffect.TYPE_ID, JoinMinigameQueueEffect.class, JoinMinigameQueueEffect.CODEC);
|
||||||
|
registerTriggerVolumeEffectType(triggerVolumes, LeaveMinigameQueueEffect.TYPE_ID, LeaveMinigameQueueEffect.class, LeaveMinigameQueueEffect.CODEC);
|
||||||
|
registerTriggerVolumeEffectType(triggerVolumes, VoteForMapEffect.TYPE_ID, VoteForMapEffect.class, VoteForMapEffect.CODEC);
|
||||||
|
registerTriggerVolumeEffectType(triggerVolumes, StartQueuedMinigameEffect.TYPE_ID, StartQueuedMinigameEffect.class, StartQueuedMinigameEffect.CODEC);
|
||||||
|
registerTriggerVolumeEffectType(triggerVolumes, SpawnItemEffect.TYPE_ID, SpawnItemEffect.class, SpawnItemEffect.CODEC);
|
||||||
|
registerTriggerVolumeEffectType(triggerVolumes, SetRoundEffect.TYPE_ID, SetRoundEffect.class, SetRoundEffect.CODEC);
|
||||||
|
registerTriggerVolumeEffectType(triggerVolumes, StartTimerEffect.TYPE_ID, StartTimerEffect.class, StartTimerEffect.CODEC);
|
||||||
|
registerTriggerVolumeEffectType(triggerVolumes, StopTimerEffect.TYPE_ID, StopTimerEffect.class, StopTimerEffect.CODEC);
|
||||||
|
registerTriggerVolumeEffectType(triggerVolumes, SetSpawnPointEffect.TYPE_ID, SetSpawnPointEffect.class, SetSpawnPointEffect.CODEC);
|
||||||
|
registerTriggerVolumeEffectType(triggerVolumes, RespawnPlayerEffect.TYPE_ID, RespawnPlayerEffect.class, RespawnPlayerEffect.CODEC);
|
||||||
|
registerTriggerVolumeEffectType(triggerVolumes, SendMinigameMessageEffect.TYPE_ID, SendMinigameMessageEffect.class, SendMinigameMessageEffect.CODEC);
|
||||||
|
registerTriggerVolumeEffectType(triggerVolumes, AssignTeamEffect.TYPE_ID, AssignTeamEffect.class, AssignTeamEffect.CODEC);
|
||||||
|
registerTriggerVolumeEffectType(triggerVolumes, SaveInventoryEffect.TYPE_ID, SaveInventoryEffect.class, SaveInventoryEffect.CODEC);
|
||||||
|
registerTriggerVolumeEffectType(triggerVolumes, RestoreInventoryEffect.TYPE_ID, RestoreInventoryEffect.class, RestoreInventoryEffect.CODEC);
|
||||||
|
registerTriggerVolumeEffectType(triggerVolumes, ClearInventoryEffect.TYPE_ID, ClearInventoryEffect.class, ClearInventoryEffect.CODEC);
|
||||||
|
registerTriggerVolumeEffectType(triggerVolumes, SetObjectiveStatusEffect.TYPE_ID, SetObjectiveStatusEffect.class, SetObjectiveStatusEffect.CODEC);
|
||||||
|
registerTriggerVolumeEffectType(triggerVolumes, ModifyObjectiveProgressEffect.TYPE_ID, ModifyObjectiveProgressEffect.class, ModifyObjectiveProgressEffect.CODEC);
|
||||||
|
registerTriggerVolumeEffectType(triggerVolumes, AwardKillScoreEffect.TYPE_ID, AwardKillScoreEffect.class, AwardKillScoreEffect.CODEC);
|
||||||
|
}
|
||||||
|
|
||||||
|
private <T extends TriggerEffect> void registerTriggerVolumeEffectType(
|
||||||
|
@Nullable TriggerVolumesPlugin triggerVolumes,
|
||||||
|
@Nonnull String typeId,
|
||||||
|
@Nonnull Class<T> clazz,
|
||||||
|
@Nonnull BuilderCodec<T> codec
|
||||||
|
) {
|
||||||
|
if (TriggerEffect.CODEC.getCodecFor(typeId) != null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (triggerVolumes != null) {
|
||||||
|
triggerVolumes.registerEffectType(typeId, clazz, codec);
|
||||||
|
} else {
|
||||||
|
TriggerEffect.CODEC.register(typeId, clazz, codec);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void registerTriggerVolumeConditionTypes() {
|
||||||
|
registerTriggerVolumeConditionType(MinigamePhaseCondition.TYPE_ID, MinigamePhaseCondition.class, MinigamePhaseCondition.CODEC);
|
||||||
|
registerTriggerVolumeConditionType(PlayerInMinigameCondition.TYPE_ID, PlayerInMinigameCondition.class, PlayerInMinigameCondition.CODEC);
|
||||||
|
registerTriggerVolumeConditionType(PlayerStatusCondition.TYPE_ID, PlayerStatusCondition.class, PlayerStatusCondition.CODEC);
|
||||||
|
registerTriggerVolumeConditionType(PlayerScoreCondition.TYPE_ID, PlayerScoreCondition.class, PlayerScoreCondition.CODEC);
|
||||||
|
registerTriggerVolumeConditionType(TeamScoreCondition.TYPE_ID, TeamScoreCondition.class, TeamScoreCondition.CODEC);
|
||||||
|
registerTriggerVolumeConditionType(CounterCondition.TYPE_ID, CounterCondition.class, CounterCondition.CODEC);
|
||||||
|
registerTriggerVolumeConditionType(CurrentRoundCondition.TYPE_ID, CurrentRoundCondition.class, CurrentRoundCondition.CODEC);
|
||||||
|
registerTriggerVolumeConditionType(PlayerLivesCondition.TYPE_ID, PlayerLivesCondition.class, PlayerLivesCondition.CODEC);
|
||||||
|
registerTriggerVolumeConditionType(TeamCondition.TYPE_ID, TeamCondition.class, TeamCondition.CODEC);
|
||||||
|
registerTriggerVolumeConditionType(TimerElapsedCondition.TYPE_ID, TimerElapsedCondition.class, TimerElapsedCondition.CODEC);
|
||||||
|
registerTriggerVolumeConditionType(ObjectiveStatusCondition.TYPE_ID, ObjectiveStatusCondition.class, ObjectiveStatusCondition.CODEC);
|
||||||
|
registerTriggerVolumeConditionType(WinConditionMetCondition.TYPE_ID, WinConditionMetCondition.class, WinConditionMetCondition.CODEC);
|
||||||
|
}
|
||||||
|
|
||||||
|
private <T extends TriggerCondition> void registerTriggerVolumeConditionType(
|
||||||
|
@Nonnull String typeId,
|
||||||
|
@Nonnull Class<T> clazz,
|
||||||
|
@Nonnull BuilderCodec<T> codec
|
||||||
|
) {
|
||||||
|
if (TriggerCondition.CODEC.getCodecFor(typeId) == null) {
|
||||||
|
TriggerCondition.CODEC.register(typeId, clazz, codec);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void registerTriggerVolumeAssetSources() {
|
||||||
|
var triggerVolumes = TriggerVolumesPlugin.get();
|
||||||
|
if (triggerVolumes == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
triggerVolumes.registerAssetSource("MinigameDefinition", () -> MinigameDefinition.getAssetMap().getAssetMap().keySet());
|
||||||
|
registerMinigameIdAssetField(triggerVolumes, StartMinigameEffect.TYPE_ID);
|
||||||
|
registerMinigameIdAssetField(triggerVolumes, EndMinigameEffect.TYPE_ID);
|
||||||
|
registerMinigameIdAssetField(triggerVolumes, ResetMinigameEffect.TYPE_ID);
|
||||||
|
registerMinigameIdAssetField(triggerVolumes, SetMinigamePhaseEffect.TYPE_ID);
|
||||||
|
registerMinigameIdAssetField(triggerVolumes, ResetScoresEffect.TYPE_ID);
|
||||||
|
registerMinigameIdAssetField(triggerVolumes, ModifyPlayerScoreEffect.TYPE_ID);
|
||||||
|
registerMinigameIdAssetField(triggerVolumes, ModifyTeamScoreEffect.TYPE_ID);
|
||||||
|
registerMinigameIdAssetField(triggerVolumes, ModifyCounterEffect.TYPE_ID);
|
||||||
|
registerMinigameIdAssetField(triggerVolumes, ModifyPlayerLivesEffect.TYPE_ID);
|
||||||
|
registerMinigameIdAssetField(triggerVolumes, SetPlayerStatusEffect.TYPE_ID);
|
||||||
|
registerMinigameIdAssetField(triggerVolumes, JoinMinigameQueueEffect.TYPE_ID);
|
||||||
|
registerMinigameIdAssetField(triggerVolumes, LeaveMinigameQueueEffect.TYPE_ID);
|
||||||
|
registerMinigameIdAssetField(triggerVolumes, VoteForMapEffect.TYPE_ID);
|
||||||
|
registerMinigameIdAssetField(triggerVolumes, StartQueuedMinigameEffect.TYPE_ID);
|
||||||
|
registerMinigameIdAssetField(triggerVolumes, SpawnItemEffect.TYPE_ID);
|
||||||
|
registerMinigameIdAssetField(triggerVolumes, SetRoundEffect.TYPE_ID);
|
||||||
|
registerMinigameIdAssetField(triggerVolumes, StartTimerEffect.TYPE_ID);
|
||||||
|
registerMinigameIdAssetField(triggerVolumes, StopTimerEffect.TYPE_ID);
|
||||||
|
registerMinigameIdAssetField(triggerVolumes, SetSpawnPointEffect.TYPE_ID);
|
||||||
|
registerMinigameIdAssetField(triggerVolumes, RespawnPlayerEffect.TYPE_ID);
|
||||||
|
registerMinigameIdAssetField(triggerVolumes, SendMinigameMessageEffect.TYPE_ID);
|
||||||
|
registerMinigameIdAssetField(triggerVolumes, AssignTeamEffect.TYPE_ID);
|
||||||
|
registerMinigameIdAssetField(triggerVolumes, SaveInventoryEffect.TYPE_ID);
|
||||||
|
registerMinigameIdAssetField(triggerVolumes, RestoreInventoryEffect.TYPE_ID);
|
||||||
|
registerMinigameIdAssetField(triggerVolumes, ClearInventoryEffect.TYPE_ID);
|
||||||
|
registerMinigameIdAssetField(triggerVolumes, SetObjectiveStatusEffect.TYPE_ID);
|
||||||
|
registerMinigameIdAssetField(triggerVolumes, ModifyObjectiveProgressEffect.TYPE_ID);
|
||||||
|
registerMinigameIdAssetField(triggerVolumes, AwardKillScoreEffect.TYPE_ID);
|
||||||
|
registerMinigameIdAssetField(triggerVolumes, MinigamePhaseCondition.TYPE_ID);
|
||||||
|
registerMinigameIdAssetField(triggerVolumes, PlayerInMinigameCondition.TYPE_ID);
|
||||||
|
registerMinigameIdAssetField(triggerVolumes, PlayerStatusCondition.TYPE_ID);
|
||||||
|
registerMinigameIdAssetField(triggerVolumes, PlayerScoreCondition.TYPE_ID);
|
||||||
|
registerMinigameIdAssetField(triggerVolumes, TeamScoreCondition.TYPE_ID);
|
||||||
|
registerMinigameIdAssetField(triggerVolumes, CounterCondition.TYPE_ID);
|
||||||
|
registerMinigameIdAssetField(triggerVolumes, CurrentRoundCondition.TYPE_ID);
|
||||||
|
registerMinigameIdAssetField(triggerVolumes, PlayerLivesCondition.TYPE_ID);
|
||||||
|
registerMinigameIdAssetField(triggerVolumes, TeamCondition.TYPE_ID);
|
||||||
|
registerMinigameIdAssetField(triggerVolumes, TimerElapsedCondition.TYPE_ID);
|
||||||
|
registerMinigameIdAssetField(triggerVolumes, ObjectiveStatusCondition.TYPE_ID);
|
||||||
|
registerMinigameIdAssetField(triggerVolumes, WinConditionMetCondition.TYPE_ID);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void registerMinigameIdAssetField(@Nonnull TriggerVolumesPlugin triggerVolumes, @Nonnull String typeId) {
|
||||||
|
triggerVolumes.registerAssetField(typeId, "MinigameId", "MinigameDefinition");
|
||||||
|
}
|
||||||
|
|
||||||
|
private int registeredTriggerEffectCount() {
|
||||||
|
int count = 0;
|
||||||
|
for (String typeId : TriggerEffect.CODEC.getRegisteredIds()) {
|
||||||
|
if (typeId.startsWith("StartMinigame") || typeId.startsWith("EndMinigame")
|
||||||
|
|| typeId.startsWith("ResetMinigame") || typeId.startsWith("SetMinigame")
|
||||||
|
|| typeId.startsWith("ResetScores") || typeId.startsWith("ModifyPlayer")
|
||||||
|
|| typeId.startsWith("ModifyTeam") || typeId.startsWith("ModifyCounter")
|
||||||
|
|| typeId.startsWith("ModifyObjective") || typeId.startsWith("SetSpawn")
|
||||||
|
|| typeId.startsWith("SetObjective") || typeId.startsWith("SetPlayerStatus")
|
||||||
|
|| typeId.startsWith("SetRound") || typeId.startsWith("Start") || typeId.startsWith("Stop")
|
||||||
|
|| typeId.equals(JoinMinigameQueueEffect.TYPE_ID) || typeId.equals(LeaveMinigameQueueEffect.TYPE_ID)
|
||||||
|
|| typeId.equals(VoteForMapEffect.TYPE_ID) || typeId.equals(StartQueuedMinigameEffect.TYPE_ID)
|
||||||
|
|| typeId.equals(SpawnItemEffect.TYPE_ID) || typeId.equals(RespawnPlayerEffect.TYPE_ID)
|
||||||
|
|| typeId.equals(SendMinigameMessageEffect.TYPE_ID) || typeId.equals(AssignTeamEffect.TYPE_ID)
|
||||||
|
|| typeId.equals(SaveInventoryEffect.TYPE_ID) || typeId.equals(RestoreInventoryEffect.TYPE_ID)
|
||||||
|
|| typeId.equals(ClearInventoryEffect.TYPE_ID) || typeId.equals(AwardKillScoreEffect.TYPE_ID)) {
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
private int registeredTriggerConditionCount() {
|
||||||
|
int count = 0;
|
||||||
|
for (String typeId : TriggerCondition.CODEC.getRegisteredIds()) {
|
||||||
|
if (typeId.equals(MinigamePhaseCondition.TYPE_ID) || typeId.equals(PlayerInMinigameCondition.TYPE_ID)
|
||||||
|
|| typeId.equals(PlayerStatusCondition.TYPE_ID) || typeId.equals(PlayerScoreCondition.TYPE_ID)
|
||||||
|
|| typeId.equals(TeamScoreCondition.TYPE_ID) || typeId.equals(CounterCondition.TYPE_ID)
|
||||||
|
|| typeId.equals(CurrentRoundCondition.TYPE_ID) || typeId.equals(PlayerLivesCondition.TYPE_ID)
|
||||||
|
|| typeId.equals(TeamCondition.TYPE_ID) || typeId.equals(TimerElapsedCondition.TYPE_ID)
|
||||||
|
|| typeId.equals(ObjectiveStatusCondition.TYPE_ID) || typeId.equals(WinConditionMetCondition.TYPE_ID)) {
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
package net.kewwbec.minigames.adapter;
|
||||||
|
|
||||||
|
import com.hypixel.hytale.server.core.Message;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
public final class HytaleAdapters {
|
||||||
|
private HytaleAdapters() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface TriggerVolumeAdapter {
|
||||||
|
void registerVolumeHandler(String volumeTypeId, VolumeHandler handler);
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface VolumeHandler {
|
||||||
|
void handle(String volumeId, String eventId, Map<String, Object> context);
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface PlayerAdapter {
|
||||||
|
boolean isOnline(String playerId);
|
||||||
|
void sendMessage(String playerId, Message message);
|
||||||
|
String getDisplayName(String playerId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface WorldAdapter {
|
||||||
|
boolean worldExists(String worldId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface EntityAdapter {
|
||||||
|
void despawn(String entityId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface InventoryAdapter {
|
||||||
|
Map<String, Object> saveInventory(String playerId);
|
||||||
|
void restoreInventory(String playerId, Map<String, Object> inventory);
|
||||||
|
void clearInventory(String playerId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface ScoreboardAdapter {
|
||||||
|
void show(String playerId, String title, Map<String, Integer> lines);
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface TeleportAdapter {
|
||||||
|
void teleport(String playerId, String destinationId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface SoundAdapter {
|
||||||
|
void play(String playerId, String soundId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface VfxAdapter {
|
||||||
|
void playAt(String effectId, Map<String, Object> location);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,283 @@
|
|||||||
|
package net.kewwbec.minigames.adapter;
|
||||||
|
|
||||||
|
import com.hypixel.hytale.component.Component;
|
||||||
|
import com.hypixel.hytale.component.ComponentType;
|
||||||
|
import com.hypixel.hytale.component.Ref;
|
||||||
|
import com.hypixel.hytale.component.RemoveReason;
|
||||||
|
import com.hypixel.hytale.logger.HytaleLogger;
|
||||||
|
import com.hypixel.hytale.math.vector.Rotation3f;
|
||||||
|
import com.hypixel.hytale.math.vector.Transform;
|
||||||
|
import com.hypixel.hytale.protocol.SoundCategory;
|
||||||
|
import com.hypixel.hytale.server.core.Message;
|
||||||
|
import com.hypixel.hytale.server.core.asset.type.soundevent.config.SoundEvent;
|
||||||
|
import com.hypixel.hytale.server.core.inventory.InventoryComponent;
|
||||||
|
import com.hypixel.hytale.server.core.inventory.InventoryUtils;
|
||||||
|
import com.hypixel.hytale.server.core.modules.entity.teleport.Teleport;
|
||||||
|
import com.hypixel.hytale.server.core.universe.PlayerRef;
|
||||||
|
import com.hypixel.hytale.server.core.universe.Universe;
|
||||||
|
import com.hypixel.hytale.server.core.universe.world.SoundUtil;
|
||||||
|
import com.hypixel.hytale.server.core.universe.world.World;
|
||||||
|
import com.hypixel.hytale.server.core.universe.world.storage.EntityStore;
|
||||||
|
|
||||||
|
import org.joml.Vector3d;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.UUID;
|
||||||
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
|
||||||
|
public final class HytaleServerAdapters implements
|
||||||
|
HytaleAdapters.TriggerVolumeAdapter,
|
||||||
|
HytaleAdapters.PlayerAdapter,
|
||||||
|
HytaleAdapters.WorldAdapter,
|
||||||
|
HytaleAdapters.EntityAdapter,
|
||||||
|
HytaleAdapters.InventoryAdapter,
|
||||||
|
HytaleAdapters.ScoreboardAdapter,
|
||||||
|
HytaleAdapters.TeleportAdapter,
|
||||||
|
HytaleAdapters.SoundAdapter,
|
||||||
|
HytaleAdapters.VfxAdapter {
|
||||||
|
private static final HytaleLogger LOGGER = HytaleLogger.forEnclosingClass();
|
||||||
|
|
||||||
|
private final Map<String, HytaleAdapters.VolumeHandler> volumeHandlers = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void registerVolumeHandler(String volumeTypeId, HytaleAdapters.VolumeHandler handler) {
|
||||||
|
volumeHandlers.put(volumeTypeId, handler);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void dispatchVolumeEvent(String volumeTypeId, String volumeId, String eventId, Map<String, Object> context) {
|
||||||
|
var handler = volumeHandlers.get(volumeTypeId);
|
||||||
|
if (handler != null) {
|
||||||
|
handler.handle(volumeId, eventId, context);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isOnline(String playerId) {
|
||||||
|
return findPlayer(playerId).isPresent();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void sendMessage(String playerId, Message message) {
|
||||||
|
findPlayer(playerId).ifPresent(player -> player.sendMessage(message));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getDisplayName(String playerId) {
|
||||||
|
return findPlayer(playerId).map(PlayerRef::getUsername).orElse(playerId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean worldExists(String worldId) {
|
||||||
|
var universe = Universe.get();
|
||||||
|
return universe != null && universe.getWorld(worldId) != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void despawn(String entityId) {
|
||||||
|
parseUuid(entityId).ifPresent(uuid -> {
|
||||||
|
var universe = Universe.get();
|
||||||
|
if (universe == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (World world : universe.getWorlds().values()) {
|
||||||
|
Ref<EntityStore> ref = world.getEntityRef(uuid);
|
||||||
|
if (ref != null && ref.isValid()) {
|
||||||
|
runInWorld(world, () -> {
|
||||||
|
if (ref.isValid()) {
|
||||||
|
ref.getStore().removeEntity(ref, RemoveReason.REMOVE);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Map<String, Object> saveInventory(String playerId) {
|
||||||
|
return withPlayerRef(playerId, ref -> {
|
||||||
|
var store = ref.getStore();
|
||||||
|
var snapshot = new HashMap<String, Object>();
|
||||||
|
copyInventory(snapshot, "armor", store.getComponent(ref, InventoryComponent.Armor.getComponentType()));
|
||||||
|
copyInventory(snapshot, "hotbar", store.getComponent(ref, InventoryComponent.Hotbar.getComponentType()));
|
||||||
|
copyInventory(snapshot, "utility", store.getComponent(ref, InventoryComponent.Utility.getComponentType()));
|
||||||
|
copyInventory(snapshot, "storage", store.getComponent(ref, InventoryComponent.Storage.getComponentType()));
|
||||||
|
copyInventory(snapshot, "backpack", store.getComponent(ref, InventoryComponent.Backpack.getComponentType()));
|
||||||
|
return Map.copyOf(snapshot);
|
||||||
|
}).orElseGet(Map::of);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void restoreInventory(String playerId, Map<String, Object> inventory) {
|
||||||
|
withPlayerRef(playerId, ref -> {
|
||||||
|
var store = ref.getStore();
|
||||||
|
restoreInventory(store, ref, "armor", inventory, InventoryComponent.Armor.getComponentType());
|
||||||
|
restoreInventory(store, ref, "hotbar", inventory, InventoryComponent.Hotbar.getComponentType());
|
||||||
|
restoreInventory(store, ref, "utility", inventory, InventoryComponent.Utility.getComponentType());
|
||||||
|
restoreInventory(store, ref, "storage", inventory, InventoryComponent.Storage.getComponentType());
|
||||||
|
restoreInventory(store, ref, "backpack", inventory, InventoryComponent.Backpack.getComponentType());
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void clearInventory(String playerId) {
|
||||||
|
withPlayerRef(playerId, ref -> {
|
||||||
|
InventoryUtils.clear(ref, ref.getStore());
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void show(String playerId, String title, Map<String, Integer> lines) {
|
||||||
|
LOGGER.atInfo().log("Scoreboard adapter is not wired to a Hytale UI API yet: player=%s title=%s lines=%s", playerId, title, lines);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void teleport(String playerId, String destinationId) {
|
||||||
|
findPlayer(playerId).ifPresent(player -> {
|
||||||
|
var ref = player.getReference();
|
||||||
|
if (ref == null || !ref.isValid()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var currentWorld = ref.getStore().getExternalData().getWorld();
|
||||||
|
var destination = parseDestination(destinationId, currentWorld);
|
||||||
|
if (destination == null) {
|
||||||
|
LOGGER.atWarning().log("Invalid teleport destination '%s' for player %s", destinationId, playerId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
runInWorld(currentWorld, () -> {
|
||||||
|
if (ref.isValid()) {
|
||||||
|
ref.getStore().putComponent(ref, Teleport.getComponentType(), Teleport.createForPlayer(destination.world(), destination.transform()));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void play(String playerId, String soundId) {
|
||||||
|
findPlayer(playerId).ifPresent(player -> {
|
||||||
|
int soundIndex = SoundEvent.getAssetMap().getIndex(soundId);
|
||||||
|
SoundUtil.playSoundEvent2dToPlayer(player, soundIndex, SoundCategory.UI);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void playAt(String effectId, Map<String, Object> location) {
|
||||||
|
LOGGER.atInfo().log("VFX adapter is not wired to a Hytale particle/effect API yet: effect=%s location=%s", effectId, location);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Optional<PlayerRef> findPlayer(String playerId) {
|
||||||
|
var universe = Universe.get();
|
||||||
|
if (universe == null || playerId == null || playerId.isBlank()) {
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
var uuid = parseUuid(playerId);
|
||||||
|
if (uuid.isPresent()) {
|
||||||
|
return Optional.ofNullable(universe.getPlayer(uuid.get()));
|
||||||
|
}
|
||||||
|
return universe.getPlayers().stream()
|
||||||
|
.filter(player -> player.getUsername().equalsIgnoreCase(playerId))
|
||||||
|
.findFirst();
|
||||||
|
}
|
||||||
|
|
||||||
|
private Optional<UUID> parseUuid(String value) {
|
||||||
|
try {
|
||||||
|
return Optional.of(UUID.fromString(value));
|
||||||
|
} catch (IllegalArgumentException ignored) {
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private <T> Optional<T> withPlayerRef(String playerId, WorldOperation<T> operation) {
|
||||||
|
return findPlayer(playerId).flatMap(player -> {
|
||||||
|
var ref = player.getReference();
|
||||||
|
if (ref == null || !ref.isValid()) {
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
var world = ref.getStore().getExternalData().getWorld();
|
||||||
|
if (world.isInThread()) {
|
||||||
|
return Optional.ofNullable(operation.run(ref));
|
||||||
|
}
|
||||||
|
return Optional.ofNullable(CompletableFuture.supplyAsync(() -> operation.run(ref), world).join());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void runInWorld(World world, Runnable action) {
|
||||||
|
if (world.isInThread()) {
|
||||||
|
action.run();
|
||||||
|
} else {
|
||||||
|
CompletableFuture.runAsync(action, world).join();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void copyInventory(Map<String, Object> snapshot, String key, InventoryComponent component) {
|
||||||
|
if (component != null) {
|
||||||
|
snapshot.put(key, component.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings({"unchecked", "rawtypes"})
|
||||||
|
private <T extends InventoryComponent> void restoreInventory(
|
||||||
|
com.hypixel.hytale.component.Store<EntityStore> store,
|
||||||
|
Ref<EntityStore> ref,
|
||||||
|
String key,
|
||||||
|
Map<String, Object> inventory,
|
||||||
|
ComponentType<EntityStore, T> componentType
|
||||||
|
) {
|
||||||
|
Object value = inventory.get(key);
|
||||||
|
if (value instanceof Component<?> component) {
|
||||||
|
store.putComponent(ref, (ComponentType) componentType, (Component<EntityStore>) component);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Destination parseDestination(String destinationId, World currentWorld) {
|
||||||
|
if (destinationId == null || destinationId.isBlank()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var parts = destinationId.split(",");
|
||||||
|
if (parts.length == 3 || parts.length == 6) {
|
||||||
|
return coordinates(currentWorld, parts, 0);
|
||||||
|
}
|
||||||
|
if (parts.length == 4 || parts.length == 7) {
|
||||||
|
var world = Universe.get().getWorld(parts[0]);
|
||||||
|
return world == null ? null : coordinates(world, parts, 1);
|
||||||
|
}
|
||||||
|
if (destinationId.toLowerCase(Locale.ROOT).startsWith("world:")) {
|
||||||
|
var world = Universe.get().getWorld(destinationId.substring("world:".length()));
|
||||||
|
return world == null ? null : new Destination(world, new Transform());
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Destination coordinates(World world, String[] parts, int offset) {
|
||||||
|
try {
|
||||||
|
double x = Double.parseDouble(parts[offset]);
|
||||||
|
double y = Double.parseDouble(parts[offset + 1]);
|
||||||
|
double z = Double.parseDouble(parts[offset + 2]);
|
||||||
|
var rotation = new Rotation3f();
|
||||||
|
if (parts.length - offset >= 6) {
|
||||||
|
rotation.set(
|
||||||
|
Float.parseFloat(parts[offset + 3]),
|
||||||
|
Float.parseFloat(parts[offset + 4]),
|
||||||
|
Float.parseFloat(parts[offset + 5]));
|
||||||
|
}
|
||||||
|
return new Destination(world, new Transform(new Vector3d(x, y, z), rotation));
|
||||||
|
} catch (NumberFormatException ignored) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private record Destination(World world, Transform transform) {
|
||||||
|
}
|
||||||
|
|
||||||
|
@FunctionalInterface
|
||||||
|
private interface WorldOperation<T> {
|
||||||
|
T run(Ref<EntityStore> ref);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
package net.kewwbec.minigames.command;
|
||||||
|
|
||||||
|
import com.hypixel.hytale.server.core.command.system.basecommands.AbstractCommandCollection;
|
||||||
|
import net.kewwbec.minigames.command.sub.*;
|
||||||
|
import net.kewwbec.minigames.command.sub.volume.MinigameVolumeCommand;
|
||||||
|
import net.kewwbec.minigames.service.MinigameServices;
|
||||||
|
|
||||||
|
public final class MinigameCommand extends AbstractCommandCollection {
|
||||||
|
public MinigameCommand(MinigameServices services) {
|
||||||
|
super("minigame", "minigames.command.root.description");
|
||||||
|
this.setPermissionGroups("hytale:WorldEditor");
|
||||||
|
|
||||||
|
addSubCommand(new MinigameCreateCommand(services));
|
||||||
|
addSubCommand(new MinigameEditCommand(services));
|
||||||
|
addSubCommand(new MinigameDeleteCommand(services));
|
||||||
|
addSubCommand(new MinigameEnableCommand(services));
|
||||||
|
addSubCommand(new MinigameDisableCommand(services));
|
||||||
|
addSubCommand(new MinigameStartCommand(services));
|
||||||
|
addSubCommand(new MinigameStopCommand(services));
|
||||||
|
addSubCommand(new MinigameResetCommand(services));
|
||||||
|
addSubCommand(new MinigameValidateCommand(services));
|
||||||
|
addSubCommand(new MinigameExportCommand(services));
|
||||||
|
addSubCommand(new MinigameImportCommand(services));
|
||||||
|
addSubCommand(new MinigameListCommand(services));
|
||||||
|
addSubCommand(new MinigameInfoCommand(services));
|
||||||
|
addSubCommand(new MinigameMapsCommand(services));
|
||||||
|
addSubCommand(new MinigameVoteCommand(services));
|
||||||
|
addSubCommand(new MinigameJoinCommand(services));
|
||||||
|
addSubCommand(new MinigameLeaveCommand(services));
|
||||||
|
addSubCommand(new MinigameSpectateCommand(services));
|
||||||
|
addSubCommand(new MinigameVolumeCommand(services));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
package net.kewwbec.minigames.command.sub;
|
||||||
|
|
||||||
|
import com.hypixel.hytale.server.core.Message;
|
||||||
|
import com.hypixel.hytale.server.core.command.system.CommandContext;
|
||||||
|
import com.hypixel.hytale.server.core.command.system.arguments.system.RequiredArg;
|
||||||
|
import com.hypixel.hytale.server.core.command.system.arguments.types.ArgTypes;
|
||||||
|
import com.hypixel.hytale.server.core.command.system.basecommands.CommandBase;
|
||||||
|
import net.kewwbec.minigames.model.MinigameDefinition;
|
||||||
|
import net.kewwbec.minigames.service.MinigameServices;
|
||||||
|
|
||||||
|
import javax.annotation.Nonnull;
|
||||||
|
|
||||||
|
public final class MinigameCreateCommand extends CommandBase {
|
||||||
|
private final MinigameServices services;
|
||||||
|
private final RequiredArg<String> idArg;
|
||||||
|
private final RequiredArg<String> nameArg;
|
||||||
|
|
||||||
|
public MinigameCreateCommand(MinigameServices services) {
|
||||||
|
super("create", "minigames.command.spec.create.description");
|
||||||
|
this.services = services;
|
||||||
|
this.idArg = withRequiredArg("id", "minigames.command.spec.create.arg.id", ArgTypes.STRING);
|
||||||
|
this.nameArg = withRequiredArg("name", "minigames.command.spec.create.arg.name", ArgTypes.GREEDY_STRING);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void executeSync(@Nonnull CommandContext ctx) {
|
||||||
|
String id = ctx.get(idArg);
|
||||||
|
String name = ctx.get(nameArg);
|
||||||
|
|
||||||
|
if (services.minigames().definition(id).isPresent()) {
|
||||||
|
ctx.sendMessage(Message.translation("minigames.command.error.already_exists").param("id", id));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
MinigameDefinition def = services.minigames().create(id, name);
|
||||||
|
ctx.sendMessage(Message.translation("minigames.command.create.success")
|
||||||
|
.param("id", def.getId())
|
||||||
|
.param("name", def.getName()));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
package net.kewwbec.minigames.command.sub;
|
||||||
|
|
||||||
|
import com.hypixel.hytale.server.core.Message;
|
||||||
|
import com.hypixel.hytale.server.core.command.system.CommandContext;
|
||||||
|
import com.hypixel.hytale.server.core.command.system.arguments.system.RequiredArg;
|
||||||
|
import com.hypixel.hytale.server.core.command.system.arguments.types.ArgTypes;
|
||||||
|
import com.hypixel.hytale.server.core.command.system.basecommands.CommandBase;
|
||||||
|
import net.kewwbec.minigames.service.MinigameServices;
|
||||||
|
|
||||||
|
import javax.annotation.Nonnull;
|
||||||
|
|
||||||
|
public final class MinigameDeleteCommand extends CommandBase {
|
||||||
|
private final MinigameServices services;
|
||||||
|
private final RequiredArg<String> idArg;
|
||||||
|
|
||||||
|
public MinigameDeleteCommand(MinigameServices services) {
|
||||||
|
super("delete", "minigames.command.spec.delete.description", true);
|
||||||
|
this.services = services;
|
||||||
|
this.idArg = withRequiredArg("id", "minigames.command.spec.delete.arg.id", ArgTypes.STRING);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void executeSync(@Nonnull CommandContext ctx) {
|
||||||
|
String id = ctx.get(idArg);
|
||||||
|
|
||||||
|
if (services.minigames().definition(id).isEmpty()) {
|
||||||
|
ctx.sendMessage(Message.translation("minigames.command.error.not_found").param("id", id));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!services.runtime().runtimes(id).isEmpty()) {
|
||||||
|
services.runtime().end(id, "minigames.runtime.reason.deleted");
|
||||||
|
}
|
||||||
|
|
||||||
|
services.minigames().delete(id);
|
||||||
|
ctx.sendMessage(Message.translation("minigames.command.delete.success").param("id", id));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
package net.kewwbec.minigames.command.sub;
|
||||||
|
|
||||||
|
import com.hypixel.hytale.server.core.Message;
|
||||||
|
import com.hypixel.hytale.server.core.command.system.CommandContext;
|
||||||
|
import com.hypixel.hytale.server.core.command.system.arguments.system.RequiredArg;
|
||||||
|
import com.hypixel.hytale.server.core.command.system.arguments.types.ArgTypes;
|
||||||
|
import com.hypixel.hytale.server.core.command.system.basecommands.CommandBase;
|
||||||
|
import net.kewwbec.minigames.service.MinigameServices;
|
||||||
|
|
||||||
|
import javax.annotation.Nonnull;
|
||||||
|
|
||||||
|
public final class MinigameDisableCommand extends CommandBase {
|
||||||
|
private final MinigameServices services;
|
||||||
|
private final RequiredArg<String> idArg;
|
||||||
|
|
||||||
|
public MinigameDisableCommand(MinigameServices services) {
|
||||||
|
super("disable", "minigames.command.spec.disable.description");
|
||||||
|
this.services = services;
|
||||||
|
this.idArg = withRequiredArg("id", "minigames.command.spec.disable.arg.id", ArgTypes.STRING);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void executeSync(@Nonnull CommandContext ctx) {
|
||||||
|
String id = ctx.get(idArg);
|
||||||
|
|
||||||
|
if (services.minigames().definition(id).isEmpty()) {
|
||||||
|
ctx.sendMessage(Message.translation("minigames.command.error.not_found").param("id", id));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
services.minigames().setEnabled(id, false);
|
||||||
|
ctx.sendMessage(Message.translation("minigames.command.disable.success").param("id", id));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
package net.kewwbec.minigames.command.sub;
|
||||||
|
|
||||||
|
import com.hypixel.hytale.server.core.Message;
|
||||||
|
import com.hypixel.hytale.server.core.command.system.CommandContext;
|
||||||
|
import com.hypixel.hytale.server.core.command.system.arguments.system.RequiredArg;
|
||||||
|
import com.hypixel.hytale.server.core.command.system.arguments.types.ArgTypes;
|
||||||
|
import com.hypixel.hytale.server.core.command.system.basecommands.CommandBase;
|
||||||
|
import net.kewwbec.minigames.service.MinigameServices;
|
||||||
|
|
||||||
|
import javax.annotation.Nonnull;
|
||||||
|
|
||||||
|
public final class MinigameEditCommand extends CommandBase {
|
||||||
|
private final MinigameServices services;
|
||||||
|
private final RequiredArg<String> idArg;
|
||||||
|
|
||||||
|
public MinigameEditCommand(MinigameServices services) {
|
||||||
|
super("edit", "minigames.command.spec.edit.description");
|
||||||
|
this.services = services;
|
||||||
|
this.idArg = withRequiredArg("id", "minigames.command.spec.edit.arg.id", ArgTypes.STRING);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void executeSync(@Nonnull CommandContext ctx) {
|
||||||
|
String id = ctx.get(idArg);
|
||||||
|
|
||||||
|
if (services.minigames().definition(id).isEmpty()) {
|
||||||
|
ctx.sendMessage(Message.translation("minigames.command.error.not_found").param("id", id));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.sendMessage(Message.translation("minigames.command.error.not_implemented").param("command", "edit"));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
package net.kewwbec.minigames.command.sub;
|
||||||
|
|
||||||
|
import com.hypixel.hytale.server.core.Message;
|
||||||
|
import com.hypixel.hytale.server.core.command.system.CommandContext;
|
||||||
|
import com.hypixel.hytale.server.core.command.system.arguments.system.RequiredArg;
|
||||||
|
import com.hypixel.hytale.server.core.command.system.arguments.types.ArgTypes;
|
||||||
|
import com.hypixel.hytale.server.core.command.system.basecommands.CommandBase;
|
||||||
|
import net.kewwbec.minigames.service.MinigameServices;
|
||||||
|
|
||||||
|
import javax.annotation.Nonnull;
|
||||||
|
|
||||||
|
public final class MinigameEnableCommand extends CommandBase {
|
||||||
|
private final MinigameServices services;
|
||||||
|
private final RequiredArg<String> idArg;
|
||||||
|
|
||||||
|
public MinigameEnableCommand(MinigameServices services) {
|
||||||
|
super("enable", "minigames.command.spec.enable.description");
|
||||||
|
this.services = services;
|
||||||
|
this.idArg = withRequiredArg("id", "minigames.command.spec.enable.arg.id", ArgTypes.STRING);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void executeSync(@Nonnull CommandContext ctx) {
|
||||||
|
String id = ctx.get(idArg);
|
||||||
|
|
||||||
|
if (services.minigames().definition(id).isEmpty()) {
|
||||||
|
ctx.sendMessage(Message.translation("minigames.command.error.not_found").param("id", id));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
services.minigames().setEnabled(id, true);
|
||||||
|
ctx.sendMessage(Message.translation("minigames.command.enable.success").param("id", id));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
package net.kewwbec.minigames.command.sub;
|
||||||
|
|
||||||
|
import com.hypixel.hytale.server.core.Message;
|
||||||
|
import com.hypixel.hytale.server.core.command.system.CommandContext;
|
||||||
|
import com.hypixel.hytale.server.core.command.system.arguments.system.RequiredArg;
|
||||||
|
import com.hypixel.hytale.server.core.command.system.arguments.types.ArgTypes;
|
||||||
|
import com.hypixel.hytale.server.core.command.system.basecommands.CommandBase;
|
||||||
|
import net.kewwbec.minigames.service.MinigameServices;
|
||||||
|
|
||||||
|
import javax.annotation.Nonnull;
|
||||||
|
|
||||||
|
public final class MinigameExportCommand extends CommandBase {
|
||||||
|
private final MinigameServices services;
|
||||||
|
private final RequiredArg<String> idArg;
|
||||||
|
|
||||||
|
public MinigameExportCommand(MinigameServices services) {
|
||||||
|
super("export", "minigames.command.spec.export.description");
|
||||||
|
this.services = services;
|
||||||
|
this.idArg = withRequiredArg("id", "minigames.command.spec.export.arg.id", ArgTypes.STRING);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void executeSync(@Nonnull CommandContext ctx) {
|
||||||
|
String id = ctx.get(idArg);
|
||||||
|
|
||||||
|
if (services.minigames().definition(id).isEmpty()) {
|
||||||
|
ctx.sendMessage(Message.translation("minigames.command.error.not_found").param("id", id));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.sendMessage(Message.translation("minigames.command.error.not_implemented").param("command", "export"));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
package net.kewwbec.minigames.command.sub;
|
||||||
|
|
||||||
|
import com.hypixel.hytale.server.core.Message;
|
||||||
|
import com.hypixel.hytale.server.core.command.system.CommandContext;
|
||||||
|
import com.hypixel.hytale.server.core.command.system.arguments.system.RequiredArg;
|
||||||
|
import com.hypixel.hytale.server.core.command.system.arguments.types.ArgTypes;
|
||||||
|
import com.hypixel.hytale.server.core.command.system.basecommands.CommandBase;
|
||||||
|
import net.kewwbec.minigames.service.MinigameServices;
|
||||||
|
|
||||||
|
import javax.annotation.Nonnull;
|
||||||
|
|
||||||
|
public final class MinigameImportCommand extends CommandBase {
|
||||||
|
private final MinigameServices services;
|
||||||
|
private final RequiredArg<String> sourceArg;
|
||||||
|
|
||||||
|
public MinigameImportCommand(MinigameServices services) {
|
||||||
|
super("import", "minigames.command.spec.import.description");
|
||||||
|
this.services = services;
|
||||||
|
this.sourceArg = withRequiredArg("source", "minigames.command.spec.import.arg.source", ArgTypes.STRING);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void executeSync(@Nonnull CommandContext ctx) {
|
||||||
|
ctx.sendMessage(Message.translation("minigames.command.error.not_implemented").param("command", "import"));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
package net.kewwbec.minigames.command.sub;
|
||||||
|
|
||||||
|
import com.hypixel.hytale.server.core.Message;
|
||||||
|
import com.hypixel.hytale.server.core.command.system.CommandContext;
|
||||||
|
import com.hypixel.hytale.server.core.command.system.arguments.system.RequiredArg;
|
||||||
|
import com.hypixel.hytale.server.core.command.system.arguments.types.ArgTypes;
|
||||||
|
import com.hypixel.hytale.server.core.command.system.basecommands.CommandBase;
|
||||||
|
import net.kewwbec.minigames.model.MinigameDefinition;
|
||||||
|
import net.kewwbec.minigames.service.MinigameServices;
|
||||||
|
|
||||||
|
import javax.annotation.Nonnull;
|
||||||
|
|
||||||
|
public final class MinigameInfoCommand extends CommandBase {
|
||||||
|
private final MinigameServices services;
|
||||||
|
private final RequiredArg<String> idArg;
|
||||||
|
|
||||||
|
public MinigameInfoCommand(MinigameServices services) {
|
||||||
|
super("info", "minigames.command.spec.info.description");
|
||||||
|
this.services = services;
|
||||||
|
this.idArg = withRequiredArg("id", "minigames.command.spec.info.arg.id", ArgTypes.STRING);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void executeSync(@Nonnull CommandContext ctx) {
|
||||||
|
String id = ctx.get(idArg);
|
||||||
|
var def = services.minigames().definition(id);
|
||||||
|
|
||||||
|
if (def.isEmpty()) {
|
||||||
|
ctx.sendMessage(Message.translation("minigames.command.error.not_found").param("id", id));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
MinigameDefinition d = def.get();
|
||||||
|
boolean running = !services.runtime().runtimes(id).isEmpty();
|
||||||
|
|
||||||
|
ctx.sendMessage(Message.translation("minigames.command.info.header")
|
||||||
|
.param("id", d.getId())
|
||||||
|
.param("name", d.getName())
|
||||||
|
.param("description", d.getDescription().isBlank() ? "-" : d.getDescription())
|
||||||
|
.param("enabled", d.isEnabled())
|
||||||
|
.param("running", running)
|
||||||
|
.param("gameType", d.getGameType().name())
|
||||||
|
.param("worldId", d.getWorldId().isBlank() ? "-" : d.getWorldId())
|
||||||
|
.param("minPlayers", d.getMinPlayers())
|
||||||
|
.param("maxPlayers", d.getMaxPlayers())
|
||||||
|
.param("teamMode", d.getTeamMode().name())
|
||||||
|
.param("winCondition", d.getWinCondition().name())
|
||||||
|
.param("roundLength", d.getRoundLengthSeconds())
|
||||||
|
.param("allowJoinMidgame", d.isAllowJoinMidgame())
|
||||||
|
.param("allowSpectators", d.isAllowSpectators())
|
||||||
|
.param("allowPvp", d.isAllowPvp()));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
package net.kewwbec.minigames.command.sub;
|
||||||
|
|
||||||
|
import com.hypixel.hytale.server.core.Message;
|
||||||
|
import com.hypixel.hytale.server.core.command.system.CommandContext;
|
||||||
|
import com.hypixel.hytale.server.core.command.system.arguments.system.OptionalArg;
|
||||||
|
import com.hypixel.hytale.server.core.command.system.arguments.system.RequiredArg;
|
||||||
|
import com.hypixel.hytale.server.core.command.system.arguments.types.ArgTypes;
|
||||||
|
import com.hypixel.hytale.server.core.command.system.basecommands.CommandBase;
|
||||||
|
import com.hypixel.hytale.server.core.universe.PlayerRef;
|
||||||
|
import net.kewwbec.minigames.service.MinigameServices;
|
||||||
|
|
||||||
|
import javax.annotation.Nonnull;
|
||||||
|
|
||||||
|
public final class MinigameJoinCommand extends CommandBase {
|
||||||
|
private final MinigameServices services;
|
||||||
|
private final RequiredArg<String> idArg;
|
||||||
|
private final OptionalArg<PlayerRef> playerArg;
|
||||||
|
|
||||||
|
public MinigameJoinCommand(MinigameServices services) {
|
||||||
|
super("join", "minigames.command.spec.join.description");
|
||||||
|
this.services = services;
|
||||||
|
this.idArg = withRequiredArg("id", "minigames.command.spec.join.arg.id", ArgTypes.STRING);
|
||||||
|
this.playerArg = withOptionalArg("player", "minigames.command.spec.join.arg.player", ArgTypes.PLAYER_REF);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void executeSync(@Nonnull CommandContext ctx) {
|
||||||
|
String id = ctx.get(idArg);
|
||||||
|
|
||||||
|
if (services.minigames().definition(id).isEmpty()) {
|
||||||
|
ctx.sendMessage(Message.translation("minigames.command.error.not_found").param("id", id));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
PlayerRef target = ctx.provided(playerArg) ? ctx.get(playerArg) : ArgTypes.PLAYER_REF.processedGet(ctx.sender(), ctx, playerArg);
|
||||||
|
if (target == null) {
|
||||||
|
ctx.sendMessage(Message.translation("minigames.command.error.player_required"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
services.queue().join(id, target.getUuid().toString());
|
||||||
|
ctx.sendMessage(Message.raw("[Minigames] Joined queue for " + id + "."));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
package net.kewwbec.minigames.command.sub;
|
||||||
|
|
||||||
|
import com.hypixel.hytale.server.core.Message;
|
||||||
|
import com.hypixel.hytale.server.core.command.system.CommandContext;
|
||||||
|
import com.hypixel.hytale.server.core.command.system.arguments.system.OptionalArg;
|
||||||
|
import com.hypixel.hytale.server.core.command.system.arguments.types.ArgTypes;
|
||||||
|
import com.hypixel.hytale.server.core.command.system.basecommands.CommandBase;
|
||||||
|
import com.hypixel.hytale.server.core.universe.PlayerRef;
|
||||||
|
import net.kewwbec.minigames.service.MinigameServices;
|
||||||
|
|
||||||
|
import javax.annotation.Nonnull;
|
||||||
|
|
||||||
|
public final class MinigameLeaveCommand extends CommandBase {
|
||||||
|
private final MinigameServices services;
|
||||||
|
private final OptionalArg<PlayerRef> playerArg;
|
||||||
|
|
||||||
|
public MinigameLeaveCommand(MinigameServices services) {
|
||||||
|
super("leave", "minigames.command.spec.leave.description");
|
||||||
|
this.services = services;
|
||||||
|
this.playerArg = withOptionalArg("player", "minigames.command.spec.leave.arg.player", ArgTypes.PLAYER_REF);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void executeSync(@Nonnull CommandContext ctx) {
|
||||||
|
PlayerRef target = ctx.provided(playerArg) ? ctx.get(playerArg) : ArgTypes.PLAYER_REF.processedGet(ctx.sender(), ctx, playerArg);
|
||||||
|
if (target == null) {
|
||||||
|
ctx.sendMessage(Message.translation("minigames.command.error.player_required"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.sendMessage(Message.translation("minigames.command.error.not_implemented").param("command", "leave"));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
package net.kewwbec.minigames.command.sub;
|
||||||
|
|
||||||
|
import com.hypixel.hytale.server.core.Message;
|
||||||
|
import com.hypixel.hytale.server.core.command.system.CommandContext;
|
||||||
|
import com.hypixel.hytale.server.core.command.system.basecommands.CommandBase;
|
||||||
|
import net.kewwbec.minigames.model.MinigameDefinition;
|
||||||
|
import net.kewwbec.minigames.service.MinigameServices;
|
||||||
|
|
||||||
|
import javax.annotation.Nonnull;
|
||||||
|
import java.util.Collection;
|
||||||
|
|
||||||
|
public final class MinigameListCommand extends CommandBase {
|
||||||
|
private final MinigameServices services;
|
||||||
|
|
||||||
|
public MinigameListCommand(MinigameServices services) {
|
||||||
|
super("list", "minigames.command.spec.list.description");
|
||||||
|
this.services = services;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void executeSync(@Nonnull CommandContext ctx) {
|
||||||
|
Collection<MinigameDefinition> defs = services.minigames().definitions();
|
||||||
|
|
||||||
|
if (defs.isEmpty()) {
|
||||||
|
ctx.sendMessage(Message.translation("minigames.command.list.empty"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Message list = Message.translation("minigames.command.list.header")
|
||||||
|
.param("count", defs.size());
|
||||||
|
|
||||||
|
for (MinigameDefinition def : defs.stream().sorted(java.util.Comparator.comparing(MinigameDefinition::getId)).toList()) {
|
||||||
|
boolean running = !services.runtime().runtimes(def.getId()).isEmpty();
|
||||||
|
list.insert("\n").insert(
|
||||||
|
Message.translation("minigames.command.list.entry")
|
||||||
|
.param("id", def.getId())
|
||||||
|
.param("name", def.getName())
|
||||||
|
.param("enabled", def.isEnabled())
|
||||||
|
.param("running", running)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.sendMessage(list);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
package net.kewwbec.minigames.command.sub;
|
||||||
|
|
||||||
|
import com.hypixel.hytale.server.core.Message;
|
||||||
|
import com.hypixel.hytale.server.core.command.system.CommandContext;
|
||||||
|
import com.hypixel.hytale.server.core.command.system.arguments.system.RequiredArg;
|
||||||
|
import com.hypixel.hytale.server.core.command.system.arguments.types.ArgTypes;
|
||||||
|
import com.hypixel.hytale.server.core.command.system.basecommands.CommandBase;
|
||||||
|
import com.hypixel.hytale.server.core.universe.PlayerRef;
|
||||||
|
import com.hypixel.hytale.server.core.universe.world.storage.EntityStore;
|
||||||
|
import com.hypixel.hytale.component.Store;
|
||||||
|
import net.kewwbec.minigames.service.MinigameServices;
|
||||||
|
|
||||||
|
import javax.annotation.Nonnull;
|
||||||
|
|
||||||
|
public final class MinigameMapsCommand extends CommandBase {
|
||||||
|
private final MinigameServices services;
|
||||||
|
private final RequiredArg<String> idArg;
|
||||||
|
|
||||||
|
public MinigameMapsCommand(MinigameServices services) {
|
||||||
|
super("maps", "minigames.command.spec.maps.description");
|
||||||
|
this.services = services;
|
||||||
|
this.idArg = withRequiredArg("id", "minigames.command.spec.maps.arg.id", ArgTypes.STRING);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void executeSync(@Nonnull CommandContext ctx) {
|
||||||
|
String id = ctx.get(idArg);
|
||||||
|
if (services.minigames().definition(id).isEmpty()) {
|
||||||
|
ctx.sendMessage(Message.translation("minigames.command.error.not_found").param("id", id));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
PlayerRef player = ctx.sender() instanceof PlayerRef playerRef ? playerRef : null;
|
||||||
|
Store<EntityStore> store = player != null && player.getReference() != null ? player.getReference().getStore() : null;
|
||||||
|
var result = services.maps().discover(store, id);
|
||||||
|
if (result.candidates().isEmpty()) {
|
||||||
|
ctx.sendMessage(Message.raw("[Minigames] No maps discovered for " + id + ". Run this as a player in the world that owns the trigger volumes."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Message message = Message.raw("[Minigames] Maps for " + id + ":");
|
||||||
|
for (var candidate : result.candidates()) {
|
||||||
|
message.insert("\n - " + candidate.displayId() + " volumes=" + candidate.volumeCount() + " mainArena=" + candidate.hasMainArena());
|
||||||
|
}
|
||||||
|
for (var warning : result.warnings()) {
|
||||||
|
message.insert("\n ! " + warning.translationKey() + " " + warning.params());
|
||||||
|
}
|
||||||
|
ctx.sendMessage(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
package net.kewwbec.minigames.command.sub;
|
||||||
|
|
||||||
|
import com.hypixel.hytale.server.core.Message;
|
||||||
|
import com.hypixel.hytale.server.core.command.system.CommandContext;
|
||||||
|
import com.hypixel.hytale.server.core.command.system.arguments.system.RequiredArg;
|
||||||
|
import com.hypixel.hytale.server.core.command.system.arguments.types.ArgTypes;
|
||||||
|
import com.hypixel.hytale.server.core.command.system.basecommands.CommandBase;
|
||||||
|
import net.kewwbec.minigames.service.MinigameServices;
|
||||||
|
|
||||||
|
import javax.annotation.Nonnull;
|
||||||
|
|
||||||
|
public final class MinigameResetCommand extends CommandBase {
|
||||||
|
private final MinigameServices services;
|
||||||
|
private final RequiredArg<String> idArg;
|
||||||
|
|
||||||
|
public MinigameResetCommand(MinigameServices services) {
|
||||||
|
super("reset", "minigames.command.spec.reset.description");
|
||||||
|
this.services = services;
|
||||||
|
this.idArg = withRequiredArg("id", "minigames.command.spec.reset.arg.id", ArgTypes.STRING);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void executeSync(@Nonnull CommandContext ctx) {
|
||||||
|
String id = ctx.get(idArg);
|
||||||
|
|
||||||
|
if (services.minigames().definition(id).isEmpty()) {
|
||||||
|
ctx.sendMessage(Message.translation("minigames.command.error.not_found").param("id", id));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
services.minigames().reset(id);
|
||||||
|
ctx.sendMessage(Message.translation("minigames.command.reset.success").param("id", id));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
package net.kewwbec.minigames.command.sub;
|
||||||
|
|
||||||
|
import com.hypixel.hytale.server.core.Message;
|
||||||
|
import com.hypixel.hytale.server.core.command.system.CommandContext;
|
||||||
|
import com.hypixel.hytale.server.core.command.system.arguments.system.OptionalArg;
|
||||||
|
import com.hypixel.hytale.server.core.command.system.arguments.system.RequiredArg;
|
||||||
|
import com.hypixel.hytale.server.core.command.system.arguments.types.ArgTypes;
|
||||||
|
import com.hypixel.hytale.server.core.command.system.basecommands.CommandBase;
|
||||||
|
import com.hypixel.hytale.server.core.universe.PlayerRef;
|
||||||
|
import net.kewwbec.minigames.service.MinigameServices;
|
||||||
|
|
||||||
|
import javax.annotation.Nonnull;
|
||||||
|
|
||||||
|
public final class MinigameSpectateCommand extends CommandBase {
|
||||||
|
private final MinigameServices services;
|
||||||
|
private final RequiredArg<String> idArg;
|
||||||
|
private final OptionalArg<PlayerRef> playerArg;
|
||||||
|
|
||||||
|
public MinigameSpectateCommand(MinigameServices services) {
|
||||||
|
super("spectate", "minigames.command.spec.spectate.description");
|
||||||
|
this.services = services;
|
||||||
|
this.idArg = withRequiredArg("id", "minigames.command.spec.spectate.arg.id", ArgTypes.STRING);
|
||||||
|
this.playerArg = withOptionalArg("player", "minigames.command.spec.spectate.arg.player", ArgTypes.PLAYER_REF);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void executeSync(@Nonnull CommandContext ctx) {
|
||||||
|
String id = ctx.get(idArg);
|
||||||
|
|
||||||
|
if (services.minigames().definition(id).isEmpty()) {
|
||||||
|
ctx.sendMessage(Message.translation("minigames.command.error.not_found").param("id", id));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (services.runtime().runtimes(id).isEmpty()) {
|
||||||
|
ctx.sendMessage(Message.translation("minigames.command.spectate.error.not_running").param("id", id));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
PlayerRef target = ctx.provided(playerArg) ? ctx.get(playerArg) : ArgTypes.PLAYER_REF.processedGet(ctx.sender(), ctx, playerArg);
|
||||||
|
if (target == null) {
|
||||||
|
ctx.sendMessage(Message.translation("minigames.command.error.player_required"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.sendMessage(Message.translation("minigames.command.error.not_implemented").param("command", "spectate"));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
package net.kewwbec.minigames.command.sub;
|
||||||
|
|
||||||
|
import com.hypixel.hytale.server.core.Message;
|
||||||
|
import com.hypixel.hytale.server.core.command.system.CommandContext;
|
||||||
|
import com.hypixel.hytale.server.core.command.system.arguments.system.RequiredArg;
|
||||||
|
import com.hypixel.hytale.server.core.command.system.arguments.types.ArgTypes;
|
||||||
|
import com.hypixel.hytale.server.core.command.system.basecommands.CommandBase;
|
||||||
|
import com.hypixel.hytale.server.core.universe.PlayerRef;
|
||||||
|
import com.hypixel.hytale.server.core.universe.world.storage.EntityStore;
|
||||||
|
import com.hypixel.hytale.component.Store;
|
||||||
|
import net.kewwbec.minigames.model.MinigameDefinition;
|
||||||
|
import net.kewwbec.minigames.service.MinigameServices;
|
||||||
|
|
||||||
|
import javax.annotation.Nonnull;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
public final class MinigameStartCommand extends CommandBase {
|
||||||
|
private final MinigameServices services;
|
||||||
|
private final RequiredArg<String> idArg;
|
||||||
|
|
||||||
|
public MinigameStartCommand(MinigameServices services) {
|
||||||
|
super("start", "minigames.command.spec.start.description");
|
||||||
|
this.services = services;
|
||||||
|
this.idArg = withRequiredArg("id", "minigames.command.spec.start.arg.id", ArgTypes.STRING);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void executeSync(@Nonnull CommandContext ctx) {
|
||||||
|
String id = ctx.get(idArg);
|
||||||
|
Optional<MinigameDefinition> def = services.minigames().definition(id);
|
||||||
|
|
||||||
|
if (def.isEmpty()) {
|
||||||
|
ctx.sendMessage(Message.translation("minigames.command.error.not_found").param("id", id));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!def.get().isEnabled()) {
|
||||||
|
ctx.sendMessage(Message.translation("minigames.command.start.error.disabled").param("id", id));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
PlayerRef player = ctx.sender() instanceof PlayerRef playerRef ? playerRef : null;
|
||||||
|
Store<EntityStore> store = player != null && player.getReference() != null ? player.getReference().getStore() : null;
|
||||||
|
var discovered = services.maps().discover(store, id);
|
||||||
|
services.queue().startQueued(def.get(), discovered.candidates(), services.runtime());
|
||||||
|
ctx.sendMessage(Message.translation("minigames.command.start.success").param("id", id));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
package net.kewwbec.minigames.command.sub;
|
||||||
|
|
||||||
|
import com.hypixel.hytale.server.core.Message;
|
||||||
|
import com.hypixel.hytale.server.core.command.system.CommandContext;
|
||||||
|
import com.hypixel.hytale.server.core.command.system.arguments.system.DefaultArg;
|
||||||
|
import com.hypixel.hytale.server.core.command.system.arguments.system.RequiredArg;
|
||||||
|
import com.hypixel.hytale.server.core.command.system.arguments.types.ArgTypes;
|
||||||
|
import com.hypixel.hytale.server.core.command.system.basecommands.CommandBase;
|
||||||
|
import net.kewwbec.minigames.service.MinigameServices;
|
||||||
|
|
||||||
|
import javax.annotation.Nonnull;
|
||||||
|
|
||||||
|
public final class MinigameStopCommand extends CommandBase {
|
||||||
|
private static final String DEFAULT_STOP_REASON = "minigames.runtime.reason.admin_stop";
|
||||||
|
|
||||||
|
private final MinigameServices services;
|
||||||
|
private final RequiredArg<String> idArg;
|
||||||
|
private final DefaultArg<String> reasonArg;
|
||||||
|
|
||||||
|
public MinigameStopCommand(MinigameServices services) {
|
||||||
|
super("stop", "minigames.command.spec.stop.description", true);
|
||||||
|
this.services = services;
|
||||||
|
this.idArg = withRequiredArg("id", "minigames.command.spec.stop.arg.id", ArgTypes.STRING);
|
||||||
|
this.reasonArg = withDefaultArg("reason", "minigames.command.spec.stop.arg.reason",
|
||||||
|
ArgTypes.GREEDY_STRING, DEFAULT_STOP_REASON, "minigames.command.spec.stop.arg.reason.default");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void executeSync(@Nonnull CommandContext ctx) {
|
||||||
|
String id = ctx.get(idArg);
|
||||||
|
|
||||||
|
if (services.minigames().definition(id).isEmpty()) {
|
||||||
|
ctx.sendMessage(Message.translation("minigames.command.error.not_found").param("id", id));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (services.runtime().runtimes(id).isEmpty()) {
|
||||||
|
ctx.sendMessage(Message.translation("minigames.command.stop.error.not_running").param("id", id));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
String reason = ctx.get(reasonArg);
|
||||||
|
services.minigames().end(id, reason);
|
||||||
|
ctx.sendMessage(Message.translation("minigames.command.stop.success").param("id", id));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
package net.kewwbec.minigames.command.sub;
|
||||||
|
|
||||||
|
import com.hypixel.hytale.server.core.Message;
|
||||||
|
import com.hypixel.hytale.server.core.command.system.CommandContext;
|
||||||
|
import com.hypixel.hytale.server.core.command.system.arguments.system.RequiredArg;
|
||||||
|
import com.hypixel.hytale.server.core.command.system.arguments.types.ArgTypes;
|
||||||
|
import com.hypixel.hytale.server.core.command.system.basecommands.CommandBase;
|
||||||
|
import net.kewwbec.minigames.model.ValidationIssue;
|
||||||
|
import net.kewwbec.minigames.service.MinigameServices;
|
||||||
|
|
||||||
|
import javax.annotation.Nonnull;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public final class MinigameValidateCommand extends CommandBase {
|
||||||
|
private final MinigameServices services;
|
||||||
|
private final RequiredArg<String> idArg;
|
||||||
|
|
||||||
|
public MinigameValidateCommand(MinigameServices services) {
|
||||||
|
super("validate", "minigames.command.spec.validate.description");
|
||||||
|
this.services = services;
|
||||||
|
this.idArg = withRequiredArg("id", "minigames.command.spec.validate.arg.id", ArgTypes.STRING);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void executeSync(@Nonnull CommandContext ctx) {
|
||||||
|
String id = ctx.get(idArg);
|
||||||
|
|
||||||
|
if (services.minigames().definition(id).isEmpty()) {
|
||||||
|
ctx.sendMessage(Message.translation("minigames.command.error.not_found").param("id", id));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<ValidationIssue> issues = services.minigames().validate(id);
|
||||||
|
|
||||||
|
if (issues.isEmpty()) {
|
||||||
|
ctx.sendMessage(Message.translation("minigames.command.validate.success").param("id", id));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Message report = Message.translation("minigames.command.validate.issues")
|
||||||
|
.param("id", id)
|
||||||
|
.param("count", issues.size());
|
||||||
|
|
||||||
|
for (ValidationIssue issue : issues) {
|
||||||
|
Message issueMsg = Message.translation(issue.translationKey());
|
||||||
|
issue.params().forEach((k, v) -> issueMsg.param(k, String.valueOf(v)));
|
||||||
|
report.insert("\n - ").insert(issueMsg);
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.sendMessage(report);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
package net.kewwbec.minigames.command.sub;
|
||||||
|
|
||||||
|
import com.hypixel.hytale.server.core.Message;
|
||||||
|
import com.hypixel.hytale.server.core.command.system.CommandContext;
|
||||||
|
import com.hypixel.hytale.server.core.command.system.arguments.system.RequiredArg;
|
||||||
|
import com.hypixel.hytale.server.core.command.system.arguments.types.ArgTypes;
|
||||||
|
import com.hypixel.hytale.server.core.command.system.basecommands.CommandBase;
|
||||||
|
import com.hypixel.hytale.server.core.universe.PlayerRef;
|
||||||
|
import net.kewwbec.minigames.service.MinigameServices;
|
||||||
|
|
||||||
|
import javax.annotation.Nonnull;
|
||||||
|
|
||||||
|
public final class MinigameVoteCommand extends CommandBase {
|
||||||
|
private final MinigameServices services;
|
||||||
|
private final RequiredArg<String> idArg;
|
||||||
|
private final RequiredArg<String> mapArg;
|
||||||
|
|
||||||
|
public MinigameVoteCommand(MinigameServices services) {
|
||||||
|
super("vote", "minigames.command.spec.vote.description");
|
||||||
|
this.services = services;
|
||||||
|
this.idArg = withRequiredArg("id", "minigames.command.spec.vote.arg.id", ArgTypes.STRING);
|
||||||
|
this.mapArg = withRequiredArg("mapId", "minigames.command.spec.vote.arg.mapId", ArgTypes.STRING);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void executeSync(@Nonnull CommandContext ctx) {
|
||||||
|
String id = ctx.get(idArg);
|
||||||
|
String mapId = ctx.get(mapArg);
|
||||||
|
if (services.minigames().definition(id).isEmpty()) {
|
||||||
|
ctx.sendMessage(Message.translation("minigames.command.error.not_found").param("id", id));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!(ctx.sender() instanceof PlayerRef player)) {
|
||||||
|
ctx.sendMessage(Message.translation("minigames.command.error.player_required"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
services.queue().vote(id, player.getUuid().toString(), mapId);
|
||||||
|
ctx.sendMessage(Message.raw("[Minigames] Voted for " + mapId + " in " + id + "."));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
package net.kewwbec.minigames.command.sub.volume;
|
||||||
|
|
||||||
|
import com.hypixel.hytale.server.core.command.system.basecommands.AbstractCommandCollection;
|
||||||
|
import net.kewwbec.minigames.service.MinigameServices;
|
||||||
|
|
||||||
|
public final class MinigameVolumeCommand extends AbstractCommandCollection {
|
||||||
|
public MinigameVolumeCommand(MinigameServices services) {
|
||||||
|
super("volume", "minigames.command.spec.volume.description");
|
||||||
|
addSubCommand(new MinigameVolumeCreateCommand(services));
|
||||||
|
addSubCommand(new MinigameVolumeLinkCommand(services));
|
||||||
|
addSubCommand(new MinigameVolumeEditCommand(services));
|
||||||
|
addSubCommand(new MinigameVolumeTestCommand(services));
|
||||||
|
}
|
||||||
|
}
|
||||||
+27
@@ -0,0 +1,27 @@
|
|||||||
|
package net.kewwbec.minigames.command.sub.volume;
|
||||||
|
|
||||||
|
import com.hypixel.hytale.server.core.Message;
|
||||||
|
import com.hypixel.hytale.server.core.command.system.CommandContext;
|
||||||
|
import com.hypixel.hytale.server.core.command.system.arguments.system.RequiredArg;
|
||||||
|
import com.hypixel.hytale.server.core.command.system.arguments.types.ArgTypes;
|
||||||
|
import com.hypixel.hytale.server.core.command.system.basecommands.CommandBase;
|
||||||
|
import net.kewwbec.minigames.service.MinigameServices;
|
||||||
|
|
||||||
|
import javax.annotation.Nonnull;
|
||||||
|
|
||||||
|
public final class MinigameVolumeCreateCommand extends CommandBase {
|
||||||
|
private final MinigameServices services;
|
||||||
|
private final RequiredArg<String> typeArg;
|
||||||
|
|
||||||
|
public MinigameVolumeCreateCommand(MinigameServices services) {
|
||||||
|
super("create", "minigames.command.spec.volume.create.description");
|
||||||
|
this.services = services;
|
||||||
|
this.typeArg = withRequiredArg("type", "minigames.command.spec.volume.create.arg.type", ArgTypes.STRING);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void executeSync(@Nonnull CommandContext ctx) {
|
||||||
|
ctx.get(typeArg);
|
||||||
|
ctx.sendMessage(Message.translation("minigames.command.error.not_implemented").param("command", "volume create"));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
package net.kewwbec.minigames.command.sub.volume;
|
||||||
|
|
||||||
|
import com.hypixel.hytale.server.core.Message;
|
||||||
|
import com.hypixel.hytale.server.core.command.system.CommandContext;
|
||||||
|
import com.hypixel.hytale.server.core.command.system.arguments.system.RequiredArg;
|
||||||
|
import com.hypixel.hytale.server.core.command.system.arguments.types.ArgTypes;
|
||||||
|
import com.hypixel.hytale.server.core.command.system.basecommands.CommandBase;
|
||||||
|
import net.kewwbec.minigames.service.MinigameServices;
|
||||||
|
|
||||||
|
import javax.annotation.Nonnull;
|
||||||
|
|
||||||
|
public final class MinigameVolumeEditCommand extends CommandBase {
|
||||||
|
private final MinigameServices services;
|
||||||
|
private final RequiredArg<String> volumeIdArg;
|
||||||
|
|
||||||
|
public MinigameVolumeEditCommand(MinigameServices services) {
|
||||||
|
super("edit", "minigames.command.spec.volume.edit.description");
|
||||||
|
this.services = services;
|
||||||
|
this.volumeIdArg = withRequiredArg("volumeId", "minigames.command.spec.volume.edit.arg.volumeId", ArgTypes.STRING);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void executeSync(@Nonnull CommandContext ctx) {
|
||||||
|
ctx.sendMessage(Message.translation("minigames.command.error.not_implemented").param("command", "volume edit"));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
package net.kewwbec.minigames.command.sub.volume;
|
||||||
|
|
||||||
|
import com.hypixel.hytale.server.core.Message;
|
||||||
|
import com.hypixel.hytale.server.core.command.system.CommandContext;
|
||||||
|
import com.hypixel.hytale.server.core.command.system.arguments.system.RequiredArg;
|
||||||
|
import com.hypixel.hytale.server.core.command.system.arguments.types.ArgTypes;
|
||||||
|
import com.hypixel.hytale.server.core.command.system.basecommands.CommandBase;
|
||||||
|
import net.kewwbec.minigames.service.MinigameServices;
|
||||||
|
|
||||||
|
import javax.annotation.Nonnull;
|
||||||
|
|
||||||
|
public final class MinigameVolumeLinkCommand extends CommandBase {
|
||||||
|
private final MinigameServices services;
|
||||||
|
private final RequiredArg<String> volumeIdArg;
|
||||||
|
private final RequiredArg<String> minigameIdArg;
|
||||||
|
|
||||||
|
public MinigameVolumeLinkCommand(MinigameServices services) {
|
||||||
|
super("link", "minigames.command.spec.volume.link.description");
|
||||||
|
this.services = services;
|
||||||
|
this.volumeIdArg = withRequiredArg("volumeId", "minigames.command.spec.volume.link.arg.volumeId", ArgTypes.STRING);
|
||||||
|
this.minigameIdArg = withRequiredArg("minigameId", "minigames.command.spec.volume.link.arg.minigameId", ArgTypes.STRING);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void executeSync(@Nonnull CommandContext ctx) {
|
||||||
|
String minigameId = ctx.get(minigameIdArg);
|
||||||
|
|
||||||
|
if (services.minigames().definition(minigameId).isEmpty()) {
|
||||||
|
ctx.sendMessage(Message.translation("minigames.command.error.not_found").param("id", minigameId));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.sendMessage(Message.translation("minigames.command.error.not_implemented").param("command", "volume link"));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
package net.kewwbec.minigames.command.sub.volume;
|
||||||
|
|
||||||
|
import com.hypixel.hytale.server.core.Message;
|
||||||
|
import com.hypixel.hytale.server.core.command.system.CommandContext;
|
||||||
|
import com.hypixel.hytale.server.core.command.system.arguments.system.RequiredArg;
|
||||||
|
import com.hypixel.hytale.server.core.command.system.arguments.types.ArgTypes;
|
||||||
|
import com.hypixel.hytale.server.core.command.system.basecommands.CommandBase;
|
||||||
|
import net.kewwbec.minigames.service.MinigameServices;
|
||||||
|
|
||||||
|
import javax.annotation.Nonnull;
|
||||||
|
|
||||||
|
public final class MinigameVolumeTestCommand extends CommandBase {
|
||||||
|
private final MinigameServices services;
|
||||||
|
private final RequiredArg<String> volumeIdArg;
|
||||||
|
|
||||||
|
public MinigameVolumeTestCommand(MinigameServices services) {
|
||||||
|
super("test", "minigames.command.spec.volume.test.description");
|
||||||
|
this.services = services;
|
||||||
|
this.volumeIdArg = withRequiredArg("volumeId", "minigames.command.spec.volume.test.arg.volumeId", ArgTypes.STRING);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void executeSync(@Nonnull CommandContext ctx) {
|
||||||
|
ctx.sendMessage(Message.translation("minigames.command.error.not_implemented").param("command", "volume test"));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
package net.kewwbec.minigames.controlplane;
|
||||||
|
|
||||||
|
import net.kewwbec.minigames.model.MinigameDefinition;
|
||||||
|
import net.kewwbec.minigames.runtime.MinigameRuntime;
|
||||||
|
|
||||||
|
public final class ControlPlaneAdapters {
|
||||||
|
private ControlPlaneAdapters() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface ControlPlaneCatalogAdapter {
|
||||||
|
void syncActivityDefinition(MinigameDefinition definition);
|
||||||
|
void syncUniverseMap(MinigameDefinition definition);
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface ControlPlaneResultAdapter {
|
||||||
|
void reportCompletedAttempt(MinigameRuntime runtime);
|
||||||
|
void reportMatchResult(MinigameRuntime runtime);
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface ControlPlaneRewardAdapter {
|
||||||
|
boolean rewardKeyExists(String rewardKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface ControlPlanePluginConfigAdapter {
|
||||||
|
void referenceConfigVersion(String pluginId, String versionId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface ControlPlaneServerRuntimeAdapter {
|
||||||
|
String serverProfileId();
|
||||||
|
void heartbeat();
|
||||||
|
void reportPlayerPresence(String playerId, String state);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
package net.kewwbec.minigames.model;
|
||||||
|
|
||||||
|
public final class Enums {
|
||||||
|
private Enums() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum GameType {
|
||||||
|
GENERIC, BATTLE, PUZZLE, RACE, CAPTURE, SURVIVAL
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum MinigamePhase {
|
||||||
|
CREATED, WAITING_FOR_PLAYERS, COUNTDOWN, STARTING, ACTIVE, OVERTIME, SUDDEN_DEATH, ENDING, RESETTING, DISABLED
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum TeamMode {
|
||||||
|
SOLO, TEAMS, FREE_FOR_ALL
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum WinCondition {
|
||||||
|
HIGHEST_SCORE, FIRST_TO_SCORE, LAST_ALIVE, OBJECTIVE_COMPLETE, CUSTOM
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum MapSelectionMode {
|
||||||
|
VOTE, RANDOM
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum FriendlyFireKillScoreMode {
|
||||||
|
OFF, LOSES_POINTS, NO_POINTS
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum PlayerStatus {
|
||||||
|
QUEUED, WAITING, ACTIVE, SPECTATOR, ELIMINATED, LEFT
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum ObjectiveStatus {
|
||||||
|
INACTIVE, ACTIVE, COMPLETE, FAILED
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
package net.kewwbec.minigames.model;
|
||||||
|
|
||||||
|
import com.hypixel.hytale.codec.Codec;
|
||||||
|
import com.hypixel.hytale.codec.KeyedCodec;
|
||||||
|
import com.hypixel.hytale.codec.builder.BuilderCodec;
|
||||||
|
import com.hypixel.hytale.codec.validation.Validators;
|
||||||
|
|
||||||
|
public class ItemStackConfig {
|
||||||
|
public static final BuilderCodec<ItemStackConfig> CODEC = BuilderCodec.builder(ItemStackConfig.class, ItemStackConfig::new)
|
||||||
|
.<String>append(
|
||||||
|
new KeyedCodec<>("ItemId", Codec.STRING),
|
||||||
|
(cfg, v) -> cfg.itemId = v,
|
||||||
|
cfg -> cfg.itemId
|
||||||
|
)
|
||||||
|
.addValidator(Validators.nonNull())
|
||||||
|
.add()
|
||||||
|
.<Integer>append(
|
||||||
|
new KeyedCodec<>("Amount", Codec.INTEGER),
|
||||||
|
(cfg, v) -> cfg.amount = v,
|
||||||
|
cfg -> cfg.amount
|
||||||
|
)
|
||||||
|
.addValidator(Validators.greaterThan(0))
|
||||||
|
.add()
|
||||||
|
.build();
|
||||||
|
|
||||||
|
protected String itemId;
|
||||||
|
protected int amount = 1;
|
||||||
|
|
||||||
|
public ItemStackConfig() {}
|
||||||
|
|
||||||
|
public ItemStackConfig(String itemId, int amount) {
|
||||||
|
this.itemId = itemId;
|
||||||
|
this.amount = amount;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getItemId() { return itemId; }
|
||||||
|
public int getAmount() { return amount; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,363 @@
|
|||||||
|
package net.kewwbec.minigames.model;
|
||||||
|
|
||||||
|
import com.hypixel.hytale.assetstore.AssetExtraInfo;
|
||||||
|
import com.hypixel.hytale.assetstore.AssetKeyValidator;
|
||||||
|
import com.hypixel.hytale.assetstore.AssetRegistry;
|
||||||
|
import com.hypixel.hytale.assetstore.AssetStore;
|
||||||
|
import com.hypixel.hytale.assetstore.codec.AssetBuilderCodec;
|
||||||
|
import com.hypixel.hytale.assetstore.map.DefaultAssetMap;
|
||||||
|
import com.hypixel.hytale.assetstore.map.JsonAssetWithMap;
|
||||||
|
import com.hypixel.hytale.codec.Codec;
|
||||||
|
import com.hypixel.hytale.codec.KeyedCodec;
|
||||||
|
import com.hypixel.hytale.codec.codecs.EnumCodec;
|
||||||
|
import com.hypixel.hytale.codec.codecs.array.ArrayCodec;
|
||||||
|
import com.hypixel.hytale.codec.validation.ValidatorCache;
|
||||||
|
import com.hypixel.hytale.codec.validation.Validators;
|
||||||
|
import com.hypixel.hytale.protocol.GameMode;
|
||||||
|
import net.kewwbec.minigames.model.Enums.GameType;
|
||||||
|
import net.kewwbec.minigames.model.Enums.FriendlyFireKillScoreMode;
|
||||||
|
import net.kewwbec.minigames.model.Enums.MapSelectionMode;
|
||||||
|
import net.kewwbec.minigames.model.Enums.TeamMode;
|
||||||
|
import net.kewwbec.minigames.model.Enums.WinCondition;
|
||||||
|
|
||||||
|
import javax.annotation.Nonnull;
|
||||||
|
|
||||||
|
public class MinigameDefinition implements JsonAssetWithMap<String, DefaultAssetMap<String, MinigameDefinition>> {
|
||||||
|
|
||||||
|
public static final AssetBuilderCodec<String, MinigameDefinition> CODEC = AssetBuilderCodec.builder(
|
||||||
|
MinigameDefinition.class,
|
||||||
|
MinigameDefinition::new,
|
||||||
|
Codec.STRING,
|
||||||
|
(def, id) -> def.id = id,
|
||||||
|
def -> def.id,
|
||||||
|
(asset, data) -> asset.data = data,
|
||||||
|
asset -> asset.data
|
||||||
|
)
|
||||||
|
.<String>appendInherited(
|
||||||
|
new KeyedCodec<>("Name", Codec.STRING),
|
||||||
|
(def, v) -> def.name = v,
|
||||||
|
def -> def.name,
|
||||||
|
(def, parent) -> def.name = parent.name
|
||||||
|
)
|
||||||
|
.addValidator(Validators.nonNull())
|
||||||
|
.add()
|
||||||
|
.<String>appendInherited(
|
||||||
|
new KeyedCodec<>("Description", Codec.STRING),
|
||||||
|
(def, v) -> def.description = v,
|
||||||
|
def -> def.description,
|
||||||
|
(def, parent) -> def.description = parent.description
|
||||||
|
)
|
||||||
|
.add()
|
||||||
|
.<Boolean>appendInherited(
|
||||||
|
new KeyedCodec<>("Enabled", Codec.BOOLEAN),
|
||||||
|
(def, v) -> def.enabled = v,
|
||||||
|
def -> def.enabled,
|
||||||
|
(def, parent) -> def.enabled = parent.enabled
|
||||||
|
)
|
||||||
|
.add()
|
||||||
|
.<Boolean>appendInherited(
|
||||||
|
new KeyedCodec<>("Debug", Codec.BOOLEAN, false),
|
||||||
|
(def, v) -> def.debug = v,
|
||||||
|
def -> def.debug,
|
||||||
|
(def, parent) -> def.debug = parent.debug
|
||||||
|
)
|
||||||
|
.add()
|
||||||
|
.<GameType>appendInherited(
|
||||||
|
new KeyedCodec<>("GameType", new EnumCodec<>(GameType.class)),
|
||||||
|
(def, v) -> def.gameType = v,
|
||||||
|
def -> def.gameType,
|
||||||
|
(def, parent) -> def.gameType = parent.gameType
|
||||||
|
)
|
||||||
|
.add()
|
||||||
|
.<String>appendInherited(
|
||||||
|
new KeyedCodec<>("WorldId", Codec.STRING),
|
||||||
|
(def, v) -> def.worldId = v,
|
||||||
|
def -> def.worldId,
|
||||||
|
(def, parent) -> def.worldId = parent.worldId
|
||||||
|
)
|
||||||
|
.add()
|
||||||
|
.<GameMode>appendInherited(
|
||||||
|
new KeyedCodec<>("RequiredGameMode", new EnumCodec<>(GameMode.class)),
|
||||||
|
(def, v) -> def.requiredGameMode = v,
|
||||||
|
def -> def.requiredGameMode,
|
||||||
|
(def, parent) -> def.requiredGameMode = parent.requiredGameMode
|
||||||
|
)
|
||||||
|
.add()
|
||||||
|
.<Integer>appendInherited(
|
||||||
|
new KeyedCodec<>("MinPlayers", Codec.INTEGER),
|
||||||
|
(def, v) -> def.minPlayers = v,
|
||||||
|
def -> def.minPlayers,
|
||||||
|
(def, parent) -> def.minPlayers = parent.minPlayers
|
||||||
|
)
|
||||||
|
.addValidator(Validators.greaterThan(0))
|
||||||
|
.add()
|
||||||
|
.<Integer>appendInherited(
|
||||||
|
new KeyedCodec<>("MaxPlayers", Codec.INTEGER),
|
||||||
|
(def, v) -> def.maxPlayers = v,
|
||||||
|
def -> def.maxPlayers,
|
||||||
|
(def, parent) -> def.maxPlayers = parent.maxPlayers
|
||||||
|
)
|
||||||
|
.addValidator(Validators.greaterThan(0))
|
||||||
|
.add()
|
||||||
|
.<TeamMode>appendInherited(
|
||||||
|
new KeyedCodec<>("TeamMode", new EnumCodec<>(TeamMode.class)),
|
||||||
|
(def, v) -> def.teamMode = v,
|
||||||
|
def -> def.teamMode,
|
||||||
|
(def, parent) -> def.teamMode = parent.teamMode
|
||||||
|
)
|
||||||
|
.addValidator(Validators.nonNull())
|
||||||
|
.add()
|
||||||
|
.<Integer>appendInherited(
|
||||||
|
new KeyedCodec<>("TeamCount", Codec.INTEGER),
|
||||||
|
(def, v) -> def.teamCount = v,
|
||||||
|
def -> def.teamCount,
|
||||||
|
(def, parent) -> def.teamCount = parent.teamCount
|
||||||
|
)
|
||||||
|
.add()
|
||||||
|
.<Integer>appendInherited(
|
||||||
|
new KeyedCodec<>("PlayersPerTeam", Codec.INTEGER),
|
||||||
|
(def, v) -> def.playersPerTeam = v,
|
||||||
|
def -> def.playersPerTeam,
|
||||||
|
(def, parent) -> def.playersPerTeam = parent.playersPerTeam
|
||||||
|
)
|
||||||
|
.add()
|
||||||
|
.<Integer>appendInherited(
|
||||||
|
new KeyedCodec<>("RoundLengthSeconds", Codec.INTEGER),
|
||||||
|
(def, v) -> def.roundLengthSeconds = v,
|
||||||
|
def -> def.roundLengthSeconds,
|
||||||
|
(def, parent) -> def.roundLengthSeconds = parent.roundLengthSeconds
|
||||||
|
)
|
||||||
|
.addValidator(Validators.greaterThan(0))
|
||||||
|
.add()
|
||||||
|
.<Integer>appendInherited(
|
||||||
|
new KeyedCodec<>("TotalRounds", Codec.INTEGER),
|
||||||
|
(def, v) -> def.totalRounds = v,
|
||||||
|
def -> def.totalRounds,
|
||||||
|
(def, parent) -> def.totalRounds = parent.totalRounds
|
||||||
|
)
|
||||||
|
.addValidator(Validators.greaterThan(0))
|
||||||
|
.add()
|
||||||
|
.<Integer>appendInherited(
|
||||||
|
new KeyedCodec<>("CountdownSeconds", Codec.INTEGER),
|
||||||
|
(def, v) -> def.countdownSeconds = v,
|
||||||
|
def -> def.countdownSeconds,
|
||||||
|
(def, parent) -> def.countdownSeconds = parent.countdownSeconds
|
||||||
|
)
|
||||||
|
.add()
|
||||||
|
.<Boolean>appendInherited(
|
||||||
|
new KeyedCodec<>("OvertimeEnabled", Codec.BOOLEAN),
|
||||||
|
(def, v) -> def.overtimeEnabled = v,
|
||||||
|
def -> def.overtimeEnabled,
|
||||||
|
(def, parent) -> def.overtimeEnabled = parent.overtimeEnabled
|
||||||
|
)
|
||||||
|
.add()
|
||||||
|
.<Boolean>appendInherited(
|
||||||
|
new KeyedCodec<>("SuddenDeathEnabled", Codec.BOOLEAN),
|
||||||
|
(def, v) -> def.suddenDeathEnabled = v,
|
||||||
|
def -> def.suddenDeathEnabled,
|
||||||
|
(def, parent) -> def.suddenDeathEnabled = parent.suddenDeathEnabled
|
||||||
|
)
|
||||||
|
.add()
|
||||||
|
.<WinCondition>appendInherited(
|
||||||
|
new KeyedCodec<>("WinCondition", new EnumCodec<>(WinCondition.class)),
|
||||||
|
(def, v) -> def.winCondition = v,
|
||||||
|
def -> def.winCondition,
|
||||||
|
(def, parent) -> def.winCondition = parent.winCondition
|
||||||
|
)
|
||||||
|
.addValidator(Validators.nonNull())
|
||||||
|
.add()
|
||||||
|
.<MapSelectionMode>appendInherited(
|
||||||
|
new KeyedCodec<>("MapSelectionMode", new EnumCodec<>(MapSelectionMode.class), false),
|
||||||
|
(def, v) -> def.mapSelectionMode = v,
|
||||||
|
def -> def.mapSelectionMode,
|
||||||
|
(def, parent) -> def.mapSelectionMode = parent.mapSelectionMode
|
||||||
|
)
|
||||||
|
.add()
|
||||||
|
.<FriendlyFireKillScoreMode>appendInherited(
|
||||||
|
new KeyedCodec<>("FriendlyFireKillScoreMode", new EnumCodec<>(FriendlyFireKillScoreMode.class), false),
|
||||||
|
(def, v) -> def.friendlyFireKillScoreMode = v,
|
||||||
|
def -> def.friendlyFireKillScoreMode,
|
||||||
|
(def, parent) -> def.friendlyFireKillScoreMode = parent.friendlyFireKillScoreMode
|
||||||
|
)
|
||||||
|
.add()
|
||||||
|
.<Boolean>appendInherited(
|
||||||
|
new KeyedCodec<>("ResetOnEnd", Codec.BOOLEAN),
|
||||||
|
(def, v) -> def.resetOnEnd = v,
|
||||||
|
def -> def.resetOnEnd,
|
||||||
|
(def, parent) -> def.resetOnEnd = parent.resetOnEnd
|
||||||
|
)
|
||||||
|
.add()
|
||||||
|
.<Boolean>appendInherited(
|
||||||
|
new KeyedCodec<>("SavePlayerInventory", Codec.BOOLEAN),
|
||||||
|
(def, v) -> def.savePlayerInventory = v,
|
||||||
|
def -> def.savePlayerInventory,
|
||||||
|
(def, parent) -> def.savePlayerInventory = parent.savePlayerInventory
|
||||||
|
)
|
||||||
|
.add()
|
||||||
|
.<Boolean>appendInherited(
|
||||||
|
new KeyedCodec<>("RestorePlayerInventory", Codec.BOOLEAN),
|
||||||
|
(def, v) -> def.restorePlayerInventory = v,
|
||||||
|
def -> def.restorePlayerInventory,
|
||||||
|
(def, parent) -> def.restorePlayerInventory = parent.restorePlayerInventory
|
||||||
|
)
|
||||||
|
.add()
|
||||||
|
.<Boolean>appendInherited(
|
||||||
|
new KeyedCodec<>("AllowJoinMidgame", Codec.BOOLEAN),
|
||||||
|
(def, v) -> def.allowJoinMidgame = v,
|
||||||
|
def -> def.allowJoinMidgame,
|
||||||
|
(def, parent) -> def.allowJoinMidgame = parent.allowJoinMidgame
|
||||||
|
)
|
||||||
|
.add()
|
||||||
|
.<Boolean>appendInherited(
|
||||||
|
new KeyedCodec<>("AllowSpectators", Codec.BOOLEAN),
|
||||||
|
(def, v) -> def.allowSpectators = v,
|
||||||
|
def -> def.allowSpectators,
|
||||||
|
(def, parent) -> def.allowSpectators = parent.allowSpectators
|
||||||
|
)
|
||||||
|
.add()
|
||||||
|
.<Boolean>appendInherited(
|
||||||
|
new KeyedCodec<>("AllowPvp", Codec.BOOLEAN),
|
||||||
|
(def, v) -> def.allowPvp = v,
|
||||||
|
def -> def.allowPvp,
|
||||||
|
(def, parent) -> def.allowPvp = parent.allowPvp
|
||||||
|
)
|
||||||
|
.add()
|
||||||
|
.<Boolean>appendInherited(
|
||||||
|
new KeyedCodec<>("AllowBlockBreaking", Codec.BOOLEAN),
|
||||||
|
(def, v) -> def.allowBlockBreaking = v,
|
||||||
|
def -> def.allowBlockBreaking,
|
||||||
|
(def, parent) -> def.allowBlockBreaking = parent.allowBlockBreaking
|
||||||
|
)
|
||||||
|
.add()
|
||||||
|
.<Boolean>appendInherited(
|
||||||
|
new KeyedCodec<>("AllowBlockPlacing", Codec.BOOLEAN),
|
||||||
|
(def, v) -> def.allowBlockPlacing = v,
|
||||||
|
def -> def.allowBlockPlacing,
|
||||||
|
(def, parent) -> def.allowBlockPlacing = parent.allowBlockPlacing
|
||||||
|
)
|
||||||
|
.add()
|
||||||
|
.<ItemStackConfig[]>appendInherited(
|
||||||
|
new KeyedCodec<>("StartItems", new ArrayCodec<>(ItemStackConfig.CODEC, ItemStackConfig[]::new)),
|
||||||
|
(def, v) -> def.startItems = v,
|
||||||
|
def -> def.startItems,
|
||||||
|
(def, parent) -> def.startItems = parent.startItems
|
||||||
|
)
|
||||||
|
.add()
|
||||||
|
.<RewardConfig[]>appendInherited(
|
||||||
|
new KeyedCodec<>("Rewards", new ArrayCodec<>(RewardConfig.CODEC, RewardConfig[]::new)),
|
||||||
|
(def, v) -> def.rewards = v,
|
||||||
|
def -> def.rewards,
|
||||||
|
(def, parent) -> def.rewards = parent.rewards
|
||||||
|
)
|
||||||
|
.add()
|
||||||
|
.build();
|
||||||
|
|
||||||
|
public static final ValidatorCache<String> VALIDATOR_CACHE = new ValidatorCache<>(new AssetKeyValidator<>(MinigameDefinition::getAssetStore));
|
||||||
|
|
||||||
|
private static AssetStore<String, MinigameDefinition, DefaultAssetMap<String, MinigameDefinition>> ASSET_STORE;
|
||||||
|
|
||||||
|
public static final MinigameDefinition UNKNOWN = new MinigameDefinition("Unknown") {{
|
||||||
|
this.name = "Unknown Minigame";
|
||||||
|
this.description = "";
|
||||||
|
this.enabled = false;
|
||||||
|
this.gameType = GameType.GENERIC;
|
||||||
|
this.teamMode = TeamMode.SOLO;
|
||||||
|
this.winCondition = WinCondition.HIGHEST_SCORE;
|
||||||
|
}};
|
||||||
|
|
||||||
|
protected AssetExtraInfo.Data data;
|
||||||
|
protected String id;
|
||||||
|
protected String name;
|
||||||
|
protected String description = "";
|
||||||
|
protected boolean enabled = true;
|
||||||
|
protected boolean debug = false;
|
||||||
|
protected GameType gameType = GameType.GENERIC;
|
||||||
|
protected String worldId = "";
|
||||||
|
protected GameMode requiredGameMode = GameMode.Adventure;
|
||||||
|
protected int minPlayers = 1;
|
||||||
|
protected int maxPlayers = 16;
|
||||||
|
protected TeamMode teamMode = TeamMode.SOLO;
|
||||||
|
protected int teamCount = 0;
|
||||||
|
protected int playersPerTeam = 0;
|
||||||
|
protected int roundLengthSeconds = 300;
|
||||||
|
protected int totalRounds = 1;
|
||||||
|
protected int countdownSeconds = 10;
|
||||||
|
protected boolean overtimeEnabled = false;
|
||||||
|
protected boolean suddenDeathEnabled = false;
|
||||||
|
protected WinCondition winCondition = WinCondition.HIGHEST_SCORE;
|
||||||
|
protected MapSelectionMode mapSelectionMode = MapSelectionMode.VOTE;
|
||||||
|
protected FriendlyFireKillScoreMode friendlyFireKillScoreMode = FriendlyFireKillScoreMode.NO_POINTS;
|
||||||
|
protected boolean resetOnEnd = true;
|
||||||
|
protected boolean savePlayerInventory = true;
|
||||||
|
protected boolean restorePlayerInventory = true;
|
||||||
|
protected boolean allowJoinMidgame = false;
|
||||||
|
protected boolean allowSpectators = true;
|
||||||
|
protected boolean allowPvp = false;
|
||||||
|
protected boolean allowBlockBreaking = false;
|
||||||
|
protected boolean allowBlockPlacing = false;
|
||||||
|
protected ItemStackConfig[] startItems = new ItemStackConfig[0];
|
||||||
|
protected RewardConfig[] rewards = new RewardConfig[0];
|
||||||
|
|
||||||
|
protected MinigameDefinition() {}
|
||||||
|
|
||||||
|
public MinigameDefinition(@Nonnull String id) {
|
||||||
|
this.id = id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static AssetStore<String, MinigameDefinition, DefaultAssetMap<String, MinigameDefinition>> getAssetStore() {
|
||||||
|
if (ASSET_STORE == null) {
|
||||||
|
ASSET_STORE = AssetRegistry.getAssetStore(MinigameDefinition.class);
|
||||||
|
}
|
||||||
|
return ASSET_STORE;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static DefaultAssetMap<String, MinigameDefinition> getAssetMap() {
|
||||||
|
return getAssetStore().getAssetMap();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static MinigameDefinition create(@Nonnull String id, @Nonnull String name) {
|
||||||
|
MinigameDefinition def = new MinigameDefinition(id);
|
||||||
|
def.name = name;
|
||||||
|
return def;
|
||||||
|
}
|
||||||
|
|
||||||
|
public AssetExtraInfo.Data getData() { return data; }
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getId() { return id; }
|
||||||
|
|
||||||
|
public String getName() { return name; }
|
||||||
|
public String getDescription() { return description != null ? description : ""; }
|
||||||
|
public boolean isEnabled() { return enabled; }
|
||||||
|
public boolean isDebug() { return debug; }
|
||||||
|
public GameType getGameType() { return gameType != null ? gameType : GameType.GENERIC; }
|
||||||
|
public String getWorldId() { return worldId != null ? worldId : ""; }
|
||||||
|
public GameMode getRequiredGameMode() { return requiredGameMode != null ? requiredGameMode : GameMode.Adventure; }
|
||||||
|
public int getMinPlayers() { return minPlayers; }
|
||||||
|
public int getMaxPlayers() { return maxPlayers; }
|
||||||
|
public TeamMode getTeamMode() { return teamMode != null ? teamMode : TeamMode.SOLO; }
|
||||||
|
public int getTeamCount() { return teamCount; }
|
||||||
|
public int getPlayersPerTeam() { return playersPerTeam; }
|
||||||
|
public int getRoundLengthSeconds() { return roundLengthSeconds; }
|
||||||
|
public int getTotalRounds() { return totalRounds; }
|
||||||
|
public int getCountdownSeconds() { return countdownSeconds; }
|
||||||
|
public boolean isOvertimeEnabled() { return overtimeEnabled; }
|
||||||
|
public boolean isSuddenDeathEnabled() { return suddenDeathEnabled; }
|
||||||
|
public WinCondition getWinCondition() { return winCondition != null ? winCondition : WinCondition.HIGHEST_SCORE; }
|
||||||
|
public MapSelectionMode getMapSelectionMode() { return mapSelectionMode != null ? mapSelectionMode : MapSelectionMode.VOTE; }
|
||||||
|
public FriendlyFireKillScoreMode getFriendlyFireKillScoreMode() { return friendlyFireKillScoreMode != null ? friendlyFireKillScoreMode : FriendlyFireKillScoreMode.NO_POINTS; }
|
||||||
|
public boolean isResetOnEnd() { return resetOnEnd; }
|
||||||
|
public boolean isSavePlayerInventory() { return savePlayerInventory; }
|
||||||
|
public boolean isRestorePlayerInventory() { return restorePlayerInventory; }
|
||||||
|
public boolean isAllowJoinMidgame() { return allowJoinMidgame; }
|
||||||
|
public boolean isAllowSpectators() { return allowSpectators; }
|
||||||
|
public boolean isAllowPvp() { return allowPvp; }
|
||||||
|
public boolean isAllowBlockBreaking() { return allowBlockBreaking; }
|
||||||
|
public boolean isAllowBlockPlacing() { return allowBlockPlacing; }
|
||||||
|
public ItemStackConfig[] getStartItems() { return startItems != null ? startItems : new ItemStackConfig[0]; }
|
||||||
|
public RewardConfig[] getRewards() { return rewards != null ? rewards : new RewardConfig[0]; }
|
||||||
|
|
||||||
|
public void setEnabled(boolean enabled) { this.enabled = enabled; }
|
||||||
|
public void setName(String name) { this.name = name; }
|
||||||
|
public void setDescription(String description) { this.description = description; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package net.kewwbec.minigames.model;
|
||||||
|
|
||||||
|
import javax.annotation.Nonnull;
|
||||||
|
|
||||||
|
public record MinigameMapCandidate(
|
||||||
|
@Nonnull String minigameId,
|
||||||
|
@Nonnull String mapId,
|
||||||
|
@Nonnull String variantId,
|
||||||
|
int volumeCount,
|
||||||
|
boolean hasMainArena
|
||||||
|
) {
|
||||||
|
public static MinigameMapCandidate none(@Nonnull String minigameId) {
|
||||||
|
return new MinigameMapCandidate(minigameId, "", "", 0, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
|
public String displayId() {
|
||||||
|
return variantId.isBlank() ? mapId : mapId + "/" + variantId;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
package net.kewwbec.minigames.model;
|
||||||
|
|
||||||
|
import javax.annotation.Nonnull;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public record MinigameMapDiscoveryResult(
|
||||||
|
@Nonnull List<MinigameMapCandidate> candidates,
|
||||||
|
@Nonnull List<ValidationIssue> warnings
|
||||||
|
) {
|
||||||
|
public static MinigameMapDiscoveryResult empty() {
|
||||||
|
return new MinigameMapDiscoveryResult(List.of(), List.of());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
package net.kewwbec.minigames.model;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import static net.kewwbec.minigames.model.Enums.ObjectiveStatus;
|
||||||
|
|
||||||
|
public final class ObjectiveState {
|
||||||
|
private final String objectiveId;
|
||||||
|
private final String name;
|
||||||
|
private final String type;
|
||||||
|
private ObjectiveStatus status = ObjectiveStatus.INACTIVE;
|
||||||
|
private String ownerTeamId;
|
||||||
|
private String ownerPlayerId;
|
||||||
|
private int progress;
|
||||||
|
private int requiredProgress = 1;
|
||||||
|
private final Map<String, Object> metadata = new HashMap<>();
|
||||||
|
|
||||||
|
public ObjectiveState(String objectiveId, String name, String type) {
|
||||||
|
this.objectiveId = objectiveId;
|
||||||
|
this.name = name;
|
||||||
|
this.type = type;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String objectiveId() { return objectiveId; }
|
||||||
|
public String name() { return name; }
|
||||||
|
public String type() { return type; }
|
||||||
|
public ObjectiveStatus status() { return status; }
|
||||||
|
public void status(ObjectiveStatus status) { this.status = status; }
|
||||||
|
public String ownerTeamId() { return ownerTeamId; }
|
||||||
|
public void ownerTeamId(String ownerTeamId) { this.ownerTeamId = ownerTeamId; }
|
||||||
|
public String ownerPlayerId() { return ownerPlayerId; }
|
||||||
|
public void ownerPlayerId(String ownerPlayerId) { this.ownerPlayerId = ownerPlayerId; }
|
||||||
|
public int progress() { return progress; }
|
||||||
|
public void progress(int progress) { this.progress = progress; }
|
||||||
|
public int requiredProgress() { return requiredProgress; }
|
||||||
|
public void requiredProgress(int requiredProgress) { this.requiredProgress = requiredProgress; }
|
||||||
|
public Map<String, Object> metadata() { return metadata; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
package net.kewwbec.minigames.model;
|
||||||
|
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
import static net.kewwbec.minigames.model.Enums.PlayerStatus;
|
||||||
|
|
||||||
|
public final class PlayerMinigameState {
|
||||||
|
private final String playerId;
|
||||||
|
private final String minigameId;
|
||||||
|
private String teamId;
|
||||||
|
private PlayerStatus status = PlayerStatus.QUEUED;
|
||||||
|
private int score;
|
||||||
|
private int lives;
|
||||||
|
private String checkpointId;
|
||||||
|
private final Set<String> tags = new HashSet<>();
|
||||||
|
private final Map<String, Object> variables = new HashMap<>();
|
||||||
|
private final Map<String, Object> savedInventory = new HashMap<>();
|
||||||
|
private final Instant joinedAt = Instant.now();
|
||||||
|
private Instant lastActiveAt = joinedAt;
|
||||||
|
|
||||||
|
public PlayerMinigameState(String playerId, String minigameId) {
|
||||||
|
this.playerId = playerId;
|
||||||
|
this.minigameId = minigameId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String playerId() { return playerId; }
|
||||||
|
public String minigameId() { return minigameId; }
|
||||||
|
public String teamId() { return teamId; }
|
||||||
|
public void teamId(String teamId) { this.teamId = teamId; }
|
||||||
|
public PlayerStatus status() { return status; }
|
||||||
|
public void status(PlayerStatus status) { this.status = status; touch(); }
|
||||||
|
public int score() { return score; }
|
||||||
|
public void score(int score) { this.score = score; touch(); }
|
||||||
|
public int lives() { return lives; }
|
||||||
|
public void lives(int lives) { this.lives = lives; touch(); }
|
||||||
|
public String checkpointId() { return checkpointId; }
|
||||||
|
public void checkpointId(String checkpointId) { this.checkpointId = checkpointId; touch(); }
|
||||||
|
public Set<String> tags() { return tags; }
|
||||||
|
public Map<String, Object> variables() { return variables; }
|
||||||
|
public Map<String, Object> savedInventory() { return savedInventory; }
|
||||||
|
public Instant joinedAt() { return joinedAt; }
|
||||||
|
public Instant lastActiveAt() { return lastActiveAt; }
|
||||||
|
public void touch() { lastActiveAt = Instant.now(); }
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
package net.kewwbec.minigames.model;
|
||||||
|
|
||||||
|
import com.hypixel.hytale.codec.Codec;
|
||||||
|
import com.hypixel.hytale.codec.KeyedCodec;
|
||||||
|
import com.hypixel.hytale.codec.builder.BuilderCodec;
|
||||||
|
import com.hypixel.hytale.codec.codecs.array.ArrayCodec;
|
||||||
|
import com.hypixel.hytale.codec.validation.Validators;
|
||||||
|
|
||||||
|
public class RewardConfig {
|
||||||
|
public static final BuilderCodec<RewardConfig> CODEC = BuilderCodec.builder(RewardConfig.class, RewardConfig::new)
|
||||||
|
.<String>append(
|
||||||
|
new KeyedCodec<>("Target", Codec.STRING),
|
||||||
|
(cfg, v) -> cfg.target = v,
|
||||||
|
cfg -> cfg.target
|
||||||
|
)
|
||||||
|
.addValidator(Validators.nonNull())
|
||||||
|
.add()
|
||||||
|
.<ItemStackConfig[]>append(
|
||||||
|
new KeyedCodec<>("Items", new ArrayCodec<>(ItemStackConfig.CODEC, ItemStackConfig[]::new)),
|
||||||
|
(cfg, v) -> cfg.items = v,
|
||||||
|
cfg -> cfg.items
|
||||||
|
)
|
||||||
|
.add()
|
||||||
|
.build();
|
||||||
|
|
||||||
|
protected String target;
|
||||||
|
protected ItemStackConfig[] items = new ItemStackConfig[0];
|
||||||
|
|
||||||
|
public RewardConfig() {}
|
||||||
|
|
||||||
|
public RewardConfig(String target, ItemStackConfig[] items) {
|
||||||
|
this.target = target;
|
||||||
|
this.items = items;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getTarget() { return target; }
|
||||||
|
public ItemStackConfig[] getItems() { return items != null ? items : new ItemStackConfig[0]; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
package net.kewwbec.minigames.model;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public record SpawnItemConfig(List<SpawnItemEntry> items, boolean respawn, int respawnCooldownSeconds) {
|
||||||
|
public SpawnItemConfig {
|
||||||
|
if (items == null || items.isEmpty()) throw new IllegalArgumentException("items must not be empty");
|
||||||
|
if (respawnCooldownSeconds < 0) throw new IllegalArgumentException("respawnCooldownSeconds must be non-negative");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
package net.kewwbec.minigames.model;
|
||||||
|
|
||||||
|
public record SpawnItemEntry(ItemStackConfig item, float dropRate) {
|
||||||
|
public SpawnItemEntry {
|
||||||
|
if (item == null) throw new IllegalArgumentException("item must not be null");
|
||||||
|
if (dropRate < 0f || dropRate > 1f) throw new IllegalArgumentException("dropRate must be between 0.0 and 1.0");
|
||||||
|
}
|
||||||
|
|
||||||
|
public SpawnItemEntry(ItemStackConfig item) {
|
||||||
|
this(item, 1f);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
package net.kewwbec.minigames.model;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.LinkedHashSet;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
public final class TeamState {
|
||||||
|
private final String teamId;
|
||||||
|
private final String name;
|
||||||
|
private final String color;
|
||||||
|
private final Set<String> players = new LinkedHashSet<>();
|
||||||
|
private int score;
|
||||||
|
private final Set<String> tags = new HashSet<>();
|
||||||
|
private final Map<String, Object> variables = new HashMap<>();
|
||||||
|
|
||||||
|
public TeamState(String teamId, String name, String color) {
|
||||||
|
this.teamId = teamId;
|
||||||
|
this.name = name;
|
||||||
|
this.color = color;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String teamId() { return teamId; }
|
||||||
|
public String name() { return name; }
|
||||||
|
public String color() { return color; }
|
||||||
|
public Set<String> players() { return players; }
|
||||||
|
public int score() { return score; }
|
||||||
|
public void score(int score) { this.score = score; }
|
||||||
|
public Set<String> tags() { return tags; }
|
||||||
|
public Map<String, Object> variables() { return variables; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
package net.kewwbec.minigames.model;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
public record ValidationIssue(String translationKey, Map<String, Object> params) {
|
||||||
|
public static ValidationIssue of(String translationKey) {
|
||||||
|
return new ValidationIssue(translationKey, Map.of());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ValidationIssue of(String translationKey, String paramName, Object paramValue) {
|
||||||
|
return new ValidationIssue(translationKey, Map.of(paramName, paramValue));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
package net.kewwbec.minigames.persistence;
|
||||||
|
|
||||||
|
import com.hypixel.hytale.assetstore.AssetExtraInfo;
|
||||||
|
import net.kewwbec.minigames.model.MinigameDefinition;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
|
||||||
|
public final class LocalJsonMinigameDefinitionStore implements MinigameDefinitionStore {
|
||||||
|
private static final String PACK_KEY = "net.kewwbec:core-minigames";
|
||||||
|
|
||||||
|
private final Path minigameDirectory;
|
||||||
|
|
||||||
|
public LocalJsonMinigameDefinitionStore(Path dataRoot) {
|
||||||
|
this.minigameDirectory = dataRoot.resolve("minigames");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void save(MinigameDefinition definition) throws IOException {
|
||||||
|
Files.createDirectories(minigameDirectory);
|
||||||
|
Path filePath = minigameDirectory.resolve(definition.getId() + ".json");
|
||||||
|
var extraInfo = new AssetExtraInfo<>(
|
||||||
|
filePath,
|
||||||
|
new AssetExtraInfo.Data(MinigameDefinition.class, definition.getId(), null)
|
||||||
|
);
|
||||||
|
String json = MinigameDefinition.CODEC.encode(definition, extraInfo).toString();
|
||||||
|
Files.writeString(filePath, json, StandardCharsets.UTF_8);
|
||||||
|
MinigameDefinition.getAssetStore().loadAssetsFromPaths(PACK_KEY, java.util.List.of(filePath));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void delete(String id) throws IOException {
|
||||||
|
Path filePath = minigameDirectory.resolve(id + ".json");
|
||||||
|
Files.deleteIfExists(filePath);
|
||||||
|
MinigameDefinition.getAssetStore().removeAssets(java.util.Set.of(id));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
package net.kewwbec.minigames.persistence;
|
||||||
|
|
||||||
|
import net.kewwbec.minigames.model.MinigameDefinition;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
public interface MinigameDefinitionStore {
|
||||||
|
void save(MinigameDefinition definition) throws IOException;
|
||||||
|
void delete(String id) throws IOException;
|
||||||
|
}
|
||||||
@@ -0,0 +1,228 @@
|
|||||||
|
package net.kewwbec.minigames.runtime;
|
||||||
|
|
||||||
|
import com.hypixel.hytale.server.core.HytaleServer;
|
||||||
|
import com.hypixel.hytale.server.core.Message;
|
||||||
|
import net.kewwbec.minigames.adapter.HytaleAdapters;
|
||||||
|
import net.kewwbec.minigames.model.MinigameDefinition;
|
||||||
|
import net.kewwbec.minigames.model.MinigameMapCandidate;
|
||||||
|
import net.kewwbec.minigames.model.PlayerMinigameState;
|
||||||
|
import net.kewwbec.minigames.service.MinigameVolumeSignalService;
|
||||||
|
|
||||||
|
import javax.annotation.Nullable;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.Comparator;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
import static net.kewwbec.minigames.model.Enums.MinigamePhase;
|
||||||
|
|
||||||
|
public final class DefaultMinigameRuntimeService implements MinigameRuntimeService {
|
||||||
|
private final Map<String, MinigameRuntime> runtimes = new HashMap<>();
|
||||||
|
@Nullable
|
||||||
|
private MinigameVolumeSignalService signalService;
|
||||||
|
@Nullable
|
||||||
|
private HytaleAdapters.PlayerAdapter playerAdapter;
|
||||||
|
|
||||||
|
public void setSignalService(@Nullable MinigameVolumeSignalService signalService) {
|
||||||
|
this.signalService = signalService;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setPlayerAdapter(@Nullable HytaleAdapters.PlayerAdapter playerAdapter) {
|
||||||
|
this.playerAdapter = playerAdapter;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public MinigameRuntime start(MinigameDefinition definition) {
|
||||||
|
return start(definition, MinigameMapCandidate.none(definition.getId()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public MinigameRuntime start(MinigameDefinition definition, MinigameMapCandidate map) {
|
||||||
|
var selected = map != null ? map : MinigameMapCandidate.none(definition.getId());
|
||||||
|
var runtime = new MinigameRuntime(UUID.randomUUID().toString(), definition, selected.mapId(), selected.variantId());
|
||||||
|
runtime.phase(MinigamePhase.WAITING_FOR_PLAYERS);
|
||||||
|
runtimes.put(runtime.runtimeId(), runtime);
|
||||||
|
dispatch(new MinigameEvent("on_game_start", definition.getId(), eventPayload(runtime)));
|
||||||
|
return runtime;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void end(String minigameId, String reason) {
|
||||||
|
for (MinigameRuntime runtime : matching(minigameId)) {
|
||||||
|
if (signalService != null) signalService.cancelAll(runtime.runtimeId());
|
||||||
|
runtime.phase(MinigamePhase.ENDING);
|
||||||
|
var payload = new HashMap<String, Object>(eventPayload(runtime));
|
||||||
|
payload.put("reason", reason);
|
||||||
|
dispatch(new MinigameEvent("on_game_end", runtime.minigameId(), payload));
|
||||||
|
runtimes.remove(runtime.runtimeId());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void reset(String minigameId) {
|
||||||
|
for (MinigameRuntime runtime : matching(minigameId)) {
|
||||||
|
if (signalService != null) signalService.cancelAll(runtime.runtimeId());
|
||||||
|
runtime.phase(MinigamePhase.RESETTING);
|
||||||
|
runtime.players().clear();
|
||||||
|
runtime.teams().clear();
|
||||||
|
runtime.objectives().clear();
|
||||||
|
runtime.variables().clear();
|
||||||
|
runtime.timers().clear();
|
||||||
|
runtime.activeVolumes().clear();
|
||||||
|
runtime.spectators().clear();
|
||||||
|
runtime.eliminatedPlayers().clear();
|
||||||
|
runtime.temporaryBlocks().clear();
|
||||||
|
runtime.temporaryEntities().clear();
|
||||||
|
dispatch(new MinigameEvent("on_arena_reset", runtime.minigameId(), eventPayload(runtime)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Optional<MinigameRuntime> runtime(String id) {
|
||||||
|
MinigameRuntime byRuntimeId = runtimes.get(id);
|
||||||
|
if (byRuntimeId != null) {
|
||||||
|
return Optional.of(byRuntimeId);
|
||||||
|
}
|
||||||
|
return runtimes.values().stream().filter(runtime -> runtime.minigameId().equals(id)).findFirst();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Collection<MinigameRuntime> runtimes(String minigameId) {
|
||||||
|
return runtimes.values().stream().filter(runtime -> runtime.minigameId().equals(minigameId)).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Optional<MinigameRuntime> runtimeForArena(String minigameId, String mapId, String variantId) {
|
||||||
|
String normalizedMap = mapId != null ? mapId : "";
|
||||||
|
String normalizedVariant = variantId != null ? variantId : "";
|
||||||
|
return runtimes.values().stream()
|
||||||
|
.filter(runtime -> runtime.minigameId().equals(minigameId))
|
||||||
|
.filter(runtime -> normalizedMap.isBlank() || runtime.mapId().equals(normalizedMap))
|
||||||
|
.filter(runtime -> normalizedVariant.isBlank() || runtime.variantId().equals(normalizedVariant))
|
||||||
|
.findFirst();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Optional<MinigameRuntime> runtimeForPlayer(String playerId) {
|
||||||
|
if (playerId == null || playerId.isBlank()) {
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
return runtimes.values().stream().filter(runtime -> runtime.players().containsKey(playerId)).findFirst();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Collection<MinigameRuntime> runtimes() {
|
||||||
|
return runtimes.values();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void shutdownAll(String reason) {
|
||||||
|
for (String runtimeId : runtimes.keySet().toArray(String[]::new)) {
|
||||||
|
end(runtimeId, reason);
|
||||||
|
}
|
||||||
|
runtimes.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void dispatch(MinigameRuntime runtime, String eventId, Map<String, Object> payload) {
|
||||||
|
// Internal round timer — handle auto-progression, do not propagate as a public event
|
||||||
|
if ("on_timer_expired".equals(eventId)) {
|
||||||
|
Object timerName = payload != null ? payload.get("timer_name") : null;
|
||||||
|
if ("$round".equals(timerName)) {
|
||||||
|
handleRoundTimerExpiry(runtime);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var full = new HashMap<String, Object>(eventPayload(runtime));
|
||||||
|
if (payload != null) full.putAll(payload);
|
||||||
|
dispatch(new MinigameEvent(eventId, runtime.minigameId(), Map.copyOf(full)));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void handleRoundTimerExpiry(MinigameRuntime runtime) {
|
||||||
|
int current = runtime.roundNumber();
|
||||||
|
|
||||||
|
// Guard against stale callbacks (e.g. round was manually advanced before timer fired)
|
||||||
|
Object target = runtime.variables().get("$roundTimerTarget");
|
||||||
|
if (!Integer.valueOf(current).equals(target)) return;
|
||||||
|
|
||||||
|
int total = runtime.definition().getTotalRounds();
|
||||||
|
|
||||||
|
var endPayload = new HashMap<>(eventPayload(runtime));
|
||||||
|
endPayload.put("round", current);
|
||||||
|
dispatch(new MinigameEvent("on_round_end", runtime.minigameId(), Map.copyOf(endPayload)));
|
||||||
|
|
||||||
|
if (total > 0 && current >= total) {
|
||||||
|
sendEndScores(runtime);
|
||||||
|
|
||||||
|
var donePayload = new HashMap<>(eventPayload(runtime));
|
||||||
|
donePayload.put("final_round", current);
|
||||||
|
dispatch(new MinigameEvent("on_game_rounds_complete", runtime.minigameId(), Map.copyOf(donePayload)));
|
||||||
|
} else {
|
||||||
|
int next = current + 1;
|
||||||
|
runtime.roundNumber(next);
|
||||||
|
runtime.variables().put("$roundTimerTarget", next);
|
||||||
|
|
||||||
|
if (signalService != null) {
|
||||||
|
signalService.onRoundStart(runtime, next);
|
||||||
|
int roundLen = runtime.definition().getRoundLengthSeconds();
|
||||||
|
if (roundLen > 0) {
|
||||||
|
signalService.startTimer(runtime, "$round", roundLen * 1000L);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var startPayload = new HashMap<>(eventPayload(runtime));
|
||||||
|
startPayload.put("round", next);
|
||||||
|
dispatch(new MinigameEvent("on_round_start", runtime.minigameId(), Map.copyOf(startPayload)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Collection<MinigameRuntime> matching(String id) {
|
||||||
|
MinigameRuntime runtime = runtimes.get(id);
|
||||||
|
if (runtime != null) {
|
||||||
|
return java.util.List.of(runtime);
|
||||||
|
}
|
||||||
|
return runtimes(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Map<String, Object> eventPayload(MinigameRuntime runtime) {
|
||||||
|
return Map.of(
|
||||||
|
"runtime_id", runtime.runtimeId(),
|
||||||
|
"minigame_id", runtime.minigameId(),
|
||||||
|
"map_id", runtime.mapId(),
|
||||||
|
"variant_id", runtime.variantId()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void sendEndScores(MinigameRuntime runtime) {
|
||||||
|
if (playerAdapter == null || runtime.players().isEmpty()) return;
|
||||||
|
|
||||||
|
var ranked = runtime.players().entrySet().stream()
|
||||||
|
.sorted(Map.Entry.<String, PlayerMinigameState>comparingByValue(
|
||||||
|
Comparator.comparingInt((PlayerMinigameState player) -> player.score()).reversed()))
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
var recipients = runtime.players().keySet();
|
||||||
|
for (String recipient : recipients) {
|
||||||
|
playerAdapter.sendMessage(recipient, Message.translation("minigames.endscores.header"));
|
||||||
|
int rank = 1;
|
||||||
|
for (var entry : ranked) {
|
||||||
|
String name = playerAdapter.getDisplayName(entry.getKey());
|
||||||
|
playerAdapter.sendMessage(recipient,
|
||||||
|
Message.translation("minigames.endscores.entry")
|
||||||
|
.param("rank", rank)
|
||||||
|
.param("player", name)
|
||||||
|
.param("score", entry.getValue().score()));
|
||||||
|
rank++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void dispatch(MinigameEvent event) {
|
||||||
|
HytaleServer server = HytaleServer.get();
|
||||||
|
if (server != null) {
|
||||||
|
server.getEventBus().dispatchFor(MinigameEvent.class, event.minigameId()).dispatch(event);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
package net.kewwbec.minigames.runtime;
|
||||||
|
|
||||||
|
import com.hypixel.hytale.event.IEvent;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
public record MinigameEvent(String eventId, String minigameId, Map<String, Object> payload) implements IEvent<String> {
|
||||||
|
}
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
package net.kewwbec.minigames.runtime;
|
||||||
|
|
||||||
|
import net.kewwbec.minigames.model.MinigameDefinition;
|
||||||
|
import net.kewwbec.minigames.model.ObjectiveState;
|
||||||
|
import net.kewwbec.minigames.model.PlayerMinigameState;
|
||||||
|
import net.kewwbec.minigames.model.TeamState;
|
||||||
|
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
import static net.kewwbec.minigames.model.Enums.MinigamePhase;
|
||||||
|
|
||||||
|
public final class MinigameRuntime {
|
||||||
|
private final String runtimeId;
|
||||||
|
private final MinigameDefinition definition;
|
||||||
|
private final String minigameId;
|
||||||
|
private final String mapId;
|
||||||
|
private final String variantId;
|
||||||
|
private MinigamePhase phase = MinigamePhase.CREATED;
|
||||||
|
private int roundNumber = 1;
|
||||||
|
private final Instant createdAt = Instant.now();
|
||||||
|
private final Map<String, PlayerMinigameState> players = new HashMap<>();
|
||||||
|
private final Map<String, TeamState> teams = new HashMap<>();
|
||||||
|
private final Map<String, ObjectiveState> objectives = new HashMap<>();
|
||||||
|
private final Map<String, Object> variables = new HashMap<>();
|
||||||
|
private final Set<String> activeVolumes = new HashSet<>();
|
||||||
|
private final Set<String> spectators = new HashSet<>();
|
||||||
|
private final Set<String> eliminatedPlayers = new HashSet<>();
|
||||||
|
private final Set<String> temporaryEntities = new HashSet<>();
|
||||||
|
private final Set<String> temporaryBlocks = new HashSet<>();
|
||||||
|
private final Map<String, Long> timers = new HashMap<>();
|
||||||
|
|
||||||
|
public MinigameRuntime(String runtimeId, MinigameDefinition definition) {
|
||||||
|
this(runtimeId, definition, "", "");
|
||||||
|
}
|
||||||
|
|
||||||
|
public MinigameRuntime(String runtimeId, MinigameDefinition definition, String mapId, String variantId) {
|
||||||
|
this.runtimeId = runtimeId;
|
||||||
|
this.definition = definition;
|
||||||
|
this.minigameId = definition.getId();
|
||||||
|
this.mapId = mapId != null ? mapId : "";
|
||||||
|
this.variantId = variantId != null ? variantId : "";
|
||||||
|
}
|
||||||
|
|
||||||
|
public String runtimeId() { return runtimeId; }
|
||||||
|
public MinigameDefinition definition() { return definition; }
|
||||||
|
public String minigameId() { return minigameId; }
|
||||||
|
public String mapId() { return mapId; }
|
||||||
|
public String variantId() { return variantId; }
|
||||||
|
public MinigamePhase phase() { return phase; }
|
||||||
|
public void phase(MinigamePhase phase) { this.phase = phase; }
|
||||||
|
public int roundNumber() { return roundNumber; }
|
||||||
|
public void roundNumber(int roundNumber) { this.roundNumber = roundNumber; }
|
||||||
|
public Instant createdAt() { return createdAt; }
|
||||||
|
public Map<String, PlayerMinigameState> players() { return players; }
|
||||||
|
public Map<String, TeamState> teams() { return teams; }
|
||||||
|
public Map<String, ObjectiveState> objectives() { return objectives; }
|
||||||
|
public Map<String, Object> variables() { return variables; }
|
||||||
|
public Set<String> activeVolumes() { return activeVolumes; }
|
||||||
|
public Set<String> spectators() { return spectators; }
|
||||||
|
public Set<String> eliminatedPlayers() { return eliminatedPlayers; }
|
||||||
|
public Set<String> temporaryEntities() { return temporaryEntities; }
|
||||||
|
public Set<String> temporaryBlocks() { return temporaryBlocks; }
|
||||||
|
public Map<String, Long> timers() { return timers; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
package net.kewwbec.minigames.runtime;
|
||||||
|
|
||||||
|
import net.kewwbec.minigames.model.MinigameDefinition;
|
||||||
|
import net.kewwbec.minigames.model.MinigameMapCandidate;
|
||||||
|
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
public interface MinigameRuntimeService {
|
||||||
|
MinigameRuntime start(MinigameDefinition definition);
|
||||||
|
MinigameRuntime start(MinigameDefinition definition, MinigameMapCandidate map);
|
||||||
|
void end(String id, String reason);
|
||||||
|
void reset(String id);
|
||||||
|
Optional<MinigameRuntime> runtime(String id);
|
||||||
|
Collection<MinigameRuntime> runtimes(String minigameId);
|
||||||
|
Optional<MinigameRuntime> runtimeForArena(String minigameId, String mapId, String variantId);
|
||||||
|
Optional<MinigameRuntime> runtimeForPlayer(String playerId);
|
||||||
|
Collection<MinigameRuntime> runtimes();
|
||||||
|
void shutdownAll(String reason);
|
||||||
|
void dispatch(MinigameRuntime runtime, String eventId, Map<String, Object> payload);
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
package net.kewwbec.minigames.service;
|
||||||
|
|
||||||
|
import net.kewwbec.minigames.runtime.MinigameRuntime;
|
||||||
|
|
||||||
|
public interface ArenaResetService {
|
||||||
|
void snapshot(MinigameRuntime runtime);
|
||||||
|
void restore(MinigameRuntime runtime);
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
package net.kewwbec.minigames.service;
|
||||||
|
|
||||||
|
import net.kewwbec.minigames.runtime.MinigameRuntime;
|
||||||
|
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
public final class CheckpointService {
|
||||||
|
public void saveCheckpoint(MinigameRuntime runtime, String playerId, String checkpointId) {
|
||||||
|
var player = runtime.players().get(playerId);
|
||||||
|
if (player != null) {
|
||||||
|
player.checkpointId(checkpointId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Optional<String> getCheckpoint(MinigameRuntime runtime, String playerId) {
|
||||||
|
var player = runtime.players().get(playerId);
|
||||||
|
return player == null ? Optional.empty() : Optional.ofNullable(player.checkpointId());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,113 @@
|
|||||||
|
package net.kewwbec.minigames.service;
|
||||||
|
|
||||||
|
import net.kewwbec.minigames.model.MinigameDefinition;
|
||||||
|
import net.kewwbec.minigames.model.ValidationIssue;
|
||||||
|
import net.kewwbec.minigames.persistence.LocalJsonMinigameDefinitionStore;
|
||||||
|
import net.kewwbec.minigames.runtime.MinigameRuntime;
|
||||||
|
import net.kewwbec.minigames.runtime.MinigameRuntimeService;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
public final class DefaultMinigameService implements MinigameService {
|
||||||
|
private static final String PACK_KEY = "net.kewwbec:core-minigames";
|
||||||
|
|
||||||
|
private final MinigameRuntimeService runtimeService;
|
||||||
|
private final LocalJsonMinigameDefinitionStore store;
|
||||||
|
|
||||||
|
public DefaultMinigameService(Path dataRoot, MinigameRuntimeService runtimeService) {
|
||||||
|
this.runtimeService = runtimeService;
|
||||||
|
this.store = new LocalJsonMinigameDefinitionStore(dataRoot);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public MinigameDefinition create(String id, String name) {
|
||||||
|
var definition = MinigameDefinition.create(id, name);
|
||||||
|
MinigameDefinition.getAssetStore().loadAssets(PACK_KEY, List.of(definition));
|
||||||
|
try {
|
||||||
|
store.save(definition);
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new RuntimeException("Failed to save minigame definition: " + id, e);
|
||||||
|
}
|
||||||
|
return definition;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void update(MinigameDefinition definition) {
|
||||||
|
MinigameDefinition.getAssetStore().loadAssets(PACK_KEY, List.of(definition));
|
||||||
|
try {
|
||||||
|
store.save(definition);
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new RuntimeException("Failed to save minigame definition: " + definition.getId(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void delete(String id) {
|
||||||
|
runtimeService.end(id, "minigames.runtime.reason.definition_deleted");
|
||||||
|
try {
|
||||||
|
store.delete(id);
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new RuntimeException("Failed to delete minigame definition: " + id, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setEnabled(String id, boolean enabled) {
|
||||||
|
definition(id).ifPresent(def -> {
|
||||||
|
def.setEnabled(enabled);
|
||||||
|
MinigameDefinition.getAssetStore().loadAssets(PACK_KEY, List.of(def));
|
||||||
|
try {
|
||||||
|
store.save(def);
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new RuntimeException("Failed to save minigame definition: " + id, e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public MinigameRuntime start(String id) {
|
||||||
|
var definition = definition(id).orElseThrow(() -> new IllegalArgumentException("minigames.error.unknown_minigame:" + id));
|
||||||
|
return runtimeService.start(definition);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void end(String id, String reason) {
|
||||||
|
runtimeService.end(id, reason);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void reset(String id) {
|
||||||
|
runtimeService.reset(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Optional<MinigameDefinition> definition(String id) {
|
||||||
|
return Optional.ofNullable(MinigameDefinition.getAssetMap().getAsset(id));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Collection<MinigameDefinition> definitions() {
|
||||||
|
return MinigameDefinition.getAssetMap().getAssetMap().values();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<ValidationIssue> validate(String id) {
|
||||||
|
var errors = new ArrayList<ValidationIssue>();
|
||||||
|
var definition = definition(id);
|
||||||
|
if (definition.isEmpty()) {
|
||||||
|
return List.of(ValidationIssue.of("minigames.validation.minigame_missing", "id", id));
|
||||||
|
}
|
||||||
|
var value = definition.get();
|
||||||
|
if (value.getName() == null || value.getName().isBlank()) errors.add(ValidationIssue.of("minigames.validation.display_name_missing"));
|
||||||
|
if (value.getMinPlayers() < 1) errors.add(ValidationIssue.of("minigames.validation.min_players_below_one"));
|
||||||
|
if (value.getMaxPlayers() < value.getMinPlayers()) errors.add(ValidationIssue.of("minigames.validation.max_players_below_min"));
|
||||||
|
if (value.getRoundLengthSeconds() <= 0) errors.add(ValidationIssue.of("minigames.validation.round_length_not_positive"));
|
||||||
|
if (value.getCountdownSeconds() < 0) errors.add(ValidationIssue.of("minigames.validation.countdown_negative"));
|
||||||
|
return errors;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
package net.kewwbec.minigames.service;
|
||||||
|
|
||||||
|
import net.kewwbec.minigames.runtime.MinigameEvent;
|
||||||
|
import net.kewwbec.minigames.runtime.MinigameRuntime;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
public final class InteractionService {
|
||||||
|
public MinigameEvent toEvent(MinigameRuntime runtime, String eventId, Map<String, Object> facts) {
|
||||||
|
var payload = Map.<String, Object>copyOf(facts);
|
||||||
|
runtime.variables().put("last_interaction", payload);
|
||||||
|
return new MinigameEvent(eventId, runtime.definition().getId(), payload);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,178 @@
|
|||||||
|
package net.kewwbec.minigames.service;
|
||||||
|
|
||||||
|
import com.hypixel.hytale.assetstore.AssetRegistry;
|
||||||
|
import com.hypixel.hytale.builtin.triggervolumes.TriggerVolumesPlugin;
|
||||||
|
import com.hypixel.hytale.builtin.triggervolumes.effect.TriggerContext;
|
||||||
|
import com.hypixel.hytale.builtin.triggervolumes.manager.TriggerVolumeManager;
|
||||||
|
import com.hypixel.hytale.builtin.triggervolumes.manager.VolumeEntry;
|
||||||
|
import com.hypixel.hytale.component.Store;
|
||||||
|
import com.hypixel.hytale.server.core.universe.world.storage.EntityStore;
|
||||||
|
import net.kewwbec.minigames.model.MinigameMapCandidate;
|
||||||
|
import net.kewwbec.minigames.model.MinigameMapDiscoveryResult;
|
||||||
|
import net.kewwbec.minigames.model.ValidationIssue;
|
||||||
|
import net.kewwbec.minigames.runtime.MinigameRuntime;
|
||||||
|
|
||||||
|
import javax.annotation.Nonnull;
|
||||||
|
import javax.annotation.Nullable;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Comparator;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.ThreadLocalRandom;
|
||||||
|
|
||||||
|
public final class MinigameMapDiscoveryService {
|
||||||
|
public static final String TAG_MINIGAME_ID = "MinigameId";
|
||||||
|
public static final String TAG_MAP_ID = "MapId";
|
||||||
|
public static final String TAG_VARIANT_ID = "VariantId";
|
||||||
|
public static final String TAG_MAP_ROLE = "MapRole";
|
||||||
|
public static final String TAG_SPAWN_ROLE = "SpawnRole";
|
||||||
|
public static final String TAG_TEAM_ID = "TeamId";
|
||||||
|
public static final String ROLE_MAIN_ARENA = "MainArena";
|
||||||
|
public static final String ROLE_TEAM_SPAWN = "TeamSpawn";
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
|
public MinigameMapDiscoveryResult discover(@Nullable TriggerContext context, @Nonnull String minigameId) {
|
||||||
|
if (context == null || context.getStore() == null) {
|
||||||
|
return MinigameMapDiscoveryResult.empty();
|
||||||
|
}
|
||||||
|
return discover(context.getStore(), minigameId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
|
public MinigameMapDiscoveryResult discover(@Nullable Store<EntityStore> store, @Nonnull String minigameId) {
|
||||||
|
TriggerVolumeManager manager = manager(store);
|
||||||
|
if (manager == null) {
|
||||||
|
return MinigameMapDiscoveryResult.empty();
|
||||||
|
}
|
||||||
|
return discover(manager, minigameId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
|
public MinigameMapDiscoveryResult discover(@Nonnull TriggerVolumeManager manager, @Nonnull String minigameId) {
|
||||||
|
var warnings = new ArrayList<ValidationIssue>();
|
||||||
|
var groups = new HashMap<Key, MutableCandidate>();
|
||||||
|
|
||||||
|
for (VolumeEntry volume : manager.getVolumes()) {
|
||||||
|
Map<String, String> tags = volume.getRawTags();
|
||||||
|
String volumeMinigameId = clean(tags.get(TAG_MINIGAME_ID));
|
||||||
|
String mapId = clean(tags.get(TAG_MAP_ID));
|
||||||
|
String variantId = clean(tags.get(TAG_VARIANT_ID));
|
||||||
|
|
||||||
|
if (!mapId.isBlank() && volumeMinigameId.isBlank()) {
|
||||||
|
warnings.add(ValidationIssue.of("minigames.validation.warning.map_volume_missing_minigame", "volumeId", volume.getId()));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!minigameId.equals(volumeMinigameId) || mapId.isBlank()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var key = new Key(mapId, variantId);
|
||||||
|
MutableCandidate candidate = groups.computeIfAbsent(key, ignored -> new MutableCandidate());
|
||||||
|
candidate.volumeCount++;
|
||||||
|
candidate.hasMainArena |= ROLE_MAIN_ARENA.equals(clean(tags.get(TAG_MAP_ROLE)));
|
||||||
|
}
|
||||||
|
|
||||||
|
var candidates = groups.entrySet().stream()
|
||||||
|
.map(entry -> new MinigameMapCandidate(minigameId, entry.getKey().mapId(), entry.getKey().variantId(), entry.getValue().volumeCount, entry.getValue().hasMainArena))
|
||||||
|
.sorted(Comparator.comparing(MinigameMapCandidate::mapId).thenComparing(MinigameMapCandidate::variantId))
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
if (candidates.isEmpty()) {
|
||||||
|
warnings.add(ValidationIssue.of("minigames.validation.warning.no_maps_discovered", "id", minigameId));
|
||||||
|
}
|
||||||
|
for (MinigameMapCandidate candidate : candidates) {
|
||||||
|
if (!candidate.hasMainArena()) {
|
||||||
|
warnings.add(ValidationIssue.of("minigames.validation.warning.map_missing_main_arena", "mapId", candidate.displayId()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new MinigameMapDiscoveryResult(candidates, warnings);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
|
public MinigameMapCandidate candidateFromVolume(@Nonnull TriggerContext context, @Nonnull String fallbackMinigameId) {
|
||||||
|
VolumeEntry volume = context.getVolume();
|
||||||
|
if (volume == null) {
|
||||||
|
return MinigameMapCandidate.none(fallbackMinigameId);
|
||||||
|
}
|
||||||
|
Map<String, String> tags = volume.getRawTags();
|
||||||
|
String minigameId = clean(tags.get(TAG_MINIGAME_ID));
|
||||||
|
if (minigameId.isBlank()) {
|
||||||
|
minigameId = fallbackMinigameId;
|
||||||
|
}
|
||||||
|
return new MinigameMapCandidate(minigameId, clean(tags.get(TAG_MAP_ID)), clean(tags.get(TAG_VARIANT_ID)), 1, ROLE_MAIN_ARENA.equals(clean(tags.get(TAG_MAP_ROLE))));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
|
public String teamSpawnDestination(@Nullable Store<EntityStore> store, @Nonnull MinigameRuntime runtime, @Nonnull String teamId) {
|
||||||
|
TriggerVolumeManager manager = manager(store);
|
||||||
|
if (manager == null || teamId.isBlank()) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
List<VolumeEntry> candidates = manager.getVolumes().stream()
|
||||||
|
.filter(volume -> isTeamSpawn(volume, runtime, teamId))
|
||||||
|
.toList();
|
||||||
|
if (candidates.isEmpty()) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
VolumeEntry selected = candidates.get(ThreadLocalRandom.current().nextInt(candidates.size()));
|
||||||
|
var pos = selected.getPosition();
|
||||||
|
if (selected.getWorldName() != null && !selected.getWorldName().isBlank()) {
|
||||||
|
return selected.getWorldName() + "," + pos.x() + "," + pos.y() + "," + pos.z();
|
||||||
|
}
|
||||||
|
return pos.x() + "," + pos.y() + "," + pos.z();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean isTeamSpawn(@Nonnull VolumeEntry volume, @Nonnull MinigameRuntime runtime, @Nonnull String teamId) {
|
||||||
|
Map<String, String> tags = volume.getRawTags();
|
||||||
|
String minigameId = clean(tags.get(TAG_MINIGAME_ID));
|
||||||
|
String mapId = clean(tags.get(TAG_MAP_ID));
|
||||||
|
String variantId = clean(tags.get(TAG_VARIANT_ID));
|
||||||
|
String spawnRole = clean(tags.get(TAG_SPAWN_ROLE));
|
||||||
|
String spawnTeamId = clean(tags.get(TAG_TEAM_ID));
|
||||||
|
if (!ROLE_TEAM_SPAWN.equals(spawnRole) || !teamId.equals(spawnTeamId)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!minigameId.isBlank() && !minigameId.equals(runtime.minigameId())) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!mapId.isBlank() && !mapId.equals(runtime.mapId())) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return variantId.isBlank() || variantId.equals(runtime.variantId());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
private static TriggerVolumeManager manager(@Nullable Store<EntityStore> store) {
|
||||||
|
TriggerVolumesPlugin plugin = TriggerVolumesPlugin.get();
|
||||||
|
if (plugin == null || store == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return store.getResource(plugin.getManagerResourceType());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
|
private static String clean(@Nullable String value) {
|
||||||
|
return value != null && !value.isBlank() ? value.trim() : "";
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean hasTag(@Nonnull VolumeEntry volume, @Nonnull String key, @Nonnull String value) {
|
||||||
|
String raw = volume.getRawTags().get(key);
|
||||||
|
if (raw == null || raw.isBlank()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return raw.equals(value) || volume.hasTag(AssetRegistry.getOrCreateTagIndex(key + "=" + value));
|
||||||
|
}
|
||||||
|
|
||||||
|
private record Key(String mapId, String variantId) {
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class MutableCandidate {
|
||||||
|
private int volumeCount;
|
||||||
|
private boolean hasMainArena;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,112 @@
|
|||||||
|
package net.kewwbec.minigames.service;
|
||||||
|
|
||||||
|
import net.kewwbec.minigames.model.MinigameDefinition;
|
||||||
|
import net.kewwbec.minigames.model.MinigameMapCandidate;
|
||||||
|
import net.kewwbec.minigames.runtime.MinigameRuntime;
|
||||||
|
import net.kewwbec.minigames.runtime.MinigameRuntimeService;
|
||||||
|
|
||||||
|
import javax.annotation.Nonnull;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Comparator;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.LinkedHashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.Random;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
import static net.kewwbec.minigames.model.Enums.MapSelectionMode;
|
||||||
|
|
||||||
|
public final class MinigameQueueService {
|
||||||
|
private final Map<String, Session> sessions = new HashMap<>();
|
||||||
|
private final Random random = new Random();
|
||||||
|
|
||||||
|
public void join(@Nonnull String minigameId, @Nonnull String playerId) {
|
||||||
|
session(minigameId).players.add(playerId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void leave(@Nonnull String minigameId, @Nonnull String playerId) {
|
||||||
|
Session session = sessions.get(minigameId);
|
||||||
|
if (session == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
session.players.remove(playerId);
|
||||||
|
session.votes.remove(playerId);
|
||||||
|
if (session.players.isEmpty() && session.votes.isEmpty()) {
|
||||||
|
sessions.remove(minigameId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void vote(@Nonnull String minigameId, @Nonnull String playerId, @Nonnull String mapId) {
|
||||||
|
Session session = session(minigameId);
|
||||||
|
session.players.add(playerId);
|
||||||
|
session.votes.put(playerId, mapId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
|
public MinigameRuntime startQueued(
|
||||||
|
@Nonnull MinigameDefinition definition,
|
||||||
|
@Nonnull List<MinigameMapCandidate> candidates,
|
||||||
|
@Nonnull MinigameRuntimeService runtimeService
|
||||||
|
) {
|
||||||
|
MinigameMapCandidate selected = select(definition, candidates);
|
||||||
|
MinigameRuntime runtime = runtimeService.start(definition, selected);
|
||||||
|
Session session = session(definition.getId());
|
||||||
|
for (String playerId : session.players) {
|
||||||
|
runtime.players().putIfAbsent(playerId, new net.kewwbec.minigames.model.PlayerMinigameState(playerId, definition.getId()));
|
||||||
|
}
|
||||||
|
sessions.remove(definition.getId());
|
||||||
|
return runtime;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
|
public MinigameMapCandidate select(@Nonnull MinigameDefinition definition, @Nonnull List<MinigameMapCandidate> candidates) {
|
||||||
|
return select(definition.getId(), definition.getMapSelectionMode(), candidates);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
|
MinigameMapCandidate select(@Nonnull String minigameId, @Nonnull MapSelectionMode selectionMode, @Nonnull List<MinigameMapCandidate> candidates) {
|
||||||
|
List<MinigameMapCandidate> available = candidates.isEmpty() ? List.of(MinigameMapCandidate.none(minigameId)) : candidates;
|
||||||
|
if (selectionMode == MapSelectionMode.RANDOM) {
|
||||||
|
return available.get(random.nextInt(available.size()));
|
||||||
|
}
|
||||||
|
|
||||||
|
Session session = session(minigameId);
|
||||||
|
Optional<String> winningMapId = session.votes.values().stream()
|
||||||
|
.collect(java.util.stream.Collectors.groupingBy(value -> value, java.util.stream.Collectors.counting()))
|
||||||
|
.entrySet()
|
||||||
|
.stream()
|
||||||
|
.sorted(Map.Entry.<String, Long>comparingByValue(Comparator.reverseOrder()).thenComparing(Map.Entry::getKey))
|
||||||
|
.map(Map.Entry::getKey)
|
||||||
|
.findFirst();
|
||||||
|
|
||||||
|
if (winningMapId.isPresent()) {
|
||||||
|
List<MinigameMapCandidate> voted = available.stream().filter(candidate -> candidate.mapId().equals(winningMapId.get())).toList();
|
||||||
|
if (!voted.isEmpty()) {
|
||||||
|
return voted.get(random.nextInt(voted.size()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return available.get(random.nextInt(available.size()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
|
public Set<String> players(@Nonnull String minigameId) {
|
||||||
|
return Set.copyOf(session(minigameId).players);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
|
public Map<String, String> votes(@Nonnull String minigameId) {
|
||||||
|
return Map.copyOf(session(minigameId).votes);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Session session(@Nonnull String minigameId) {
|
||||||
|
return sessions.computeIfAbsent(minigameId, ignored -> new Session());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class Session {
|
||||||
|
private final Set<String> players = new LinkedHashSet<>();
|
||||||
|
private final Map<String, String> votes = new HashMap<>();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
package net.kewwbec.minigames.service;
|
||||||
|
|
||||||
|
import net.kewwbec.minigames.model.MinigameDefinition;
|
||||||
|
import net.kewwbec.minigames.model.ValidationIssue;
|
||||||
|
import net.kewwbec.minigames.runtime.MinigameRuntime;
|
||||||
|
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
public interface MinigameService {
|
||||||
|
MinigameDefinition create(String id, String name);
|
||||||
|
void update(MinigameDefinition definition);
|
||||||
|
void delete(String id);
|
||||||
|
void setEnabled(String id, boolean enabled);
|
||||||
|
MinigameRuntime start(String id);
|
||||||
|
void end(String id, String reason);
|
||||||
|
void reset(String id);
|
||||||
|
Optional<MinigameDefinition> definition(String id);
|
||||||
|
Collection<MinigameDefinition> definitions();
|
||||||
|
List<ValidationIssue> validate(String id);
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
package net.kewwbec.minigames.service;
|
||||||
|
|
||||||
|
import net.kewwbec.minigames.adapter.HytaleServerAdapters;
|
||||||
|
import net.kewwbec.minigames.runtime.MinigameRuntimeService;
|
||||||
|
|
||||||
|
public record MinigameServices(
|
||||||
|
MinigameService minigames,
|
||||||
|
MinigameRuntimeService runtime,
|
||||||
|
HytaleServerAdapters adapters,
|
||||||
|
MinigameMapDiscoveryService maps,
|
||||||
|
MinigameQueueService queue,
|
||||||
|
MinigameVolumeSignalService signals
|
||||||
|
) {
|
||||||
|
}
|
||||||
@@ -0,0 +1,315 @@
|
|||||||
|
package net.kewwbec.minigames.service;
|
||||||
|
|
||||||
|
import com.hypixel.hytale.builtin.triggervolumes.TriggerVolumesPlugin;
|
||||||
|
import com.hypixel.hytale.builtin.triggervolumes.manager.TriggerVolumeManager;
|
||||||
|
import com.hypixel.hytale.builtin.triggervolumes.manager.VolumeEntry;
|
||||||
|
import com.hypixel.hytale.component.Ref;
|
||||||
|
import com.hypixel.hytale.logger.HytaleLogger;
|
||||||
|
import com.hypixel.hytale.server.core.universe.PlayerRef;
|
||||||
|
import com.hypixel.hytale.server.core.universe.Universe;
|
||||||
|
import com.hypixel.hytale.server.core.universe.world.World;
|
||||||
|
import com.hypixel.hytale.server.core.universe.world.storage.EntityStore;
|
||||||
|
import net.kewwbec.minigames.MinigameCorePlugin;
|
||||||
|
import net.kewwbec.minigames.model.Enums.MinigamePhase;
|
||||||
|
import net.kewwbec.minigames.runtime.MinigameRuntime;
|
||||||
|
|
||||||
|
import javax.annotation.Nullable;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.UUID;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
import java.util.concurrent.Executors;
|
||||||
|
import java.util.concurrent.Future;
|
||||||
|
import java.util.concurrent.ScheduledExecutorService;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
import java.util.logging.Level;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fires TAG_ADDED signals on trigger volumes based on game state (round changes, phase changes, timer expiry).
|
||||||
|
*
|
||||||
|
* Volume designers set config tags to describe WHEN a volume should fire:
|
||||||
|
* SignalRoundStart = <n> fire once when round n starts
|
||||||
|
* SignalRoundTick = <n> fire repeatedly while round n is active
|
||||||
|
* SignalPhaseTick = <phase> fire repeatedly while the given phase is active
|
||||||
|
* SignalTickDelay = <seconds> repeat interval (0 = 50ms minimum)
|
||||||
|
* SignalOnTimerExpire = <timerName> fire once when the named timer expires
|
||||||
|
*/
|
||||||
|
public final class MinigameVolumeSignalService {
|
||||||
|
|
||||||
|
// Config tags read from volumes — set by map designers, never written by this service
|
||||||
|
public static final String TAG_SIGNAL_ROUND_START = "SignalRoundStart";
|
||||||
|
public static final String TAG_SIGNAL_ROUND_TICK = "SignalRoundTick";
|
||||||
|
public static final String TAG_SIGNAL_PHASE_TICK = "SignalPhaseTick";
|
||||||
|
public static final String TAG_SIGNAL_TICK_DELAY = "SignalTickDelay";
|
||||||
|
public static final String TAG_SIGNAL_TIMER_EXPIRE = "SignalOnTimerExpire";
|
||||||
|
|
||||||
|
// Signal tag written/toggled by this service to fire TAG_ADDED
|
||||||
|
public static final String TAG_SIGNAL_KEY = "MinigameSignal";
|
||||||
|
|
||||||
|
private static final HytaleLogger LOGGER = HytaleLogger.forEnclosingClass();
|
||||||
|
private static final long MIN_DELAY_MS = 50L;
|
||||||
|
|
||||||
|
private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(r -> {
|
||||||
|
Thread t = new Thread(r, "minigame-signal-scheduler");
|
||||||
|
t.setDaemon(true);
|
||||||
|
return t;
|
||||||
|
});
|
||||||
|
|
||||||
|
private final Map<String, List<Future<?>>> roundLoops = new ConcurrentHashMap<>();
|
||||||
|
private final Map<String, List<Future<?>>> phaseLoops = new ConcurrentHashMap<>();
|
||||||
|
private final Map<String, Map<String, Future<?>>> timerFutures = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when a round advances. Fires one-shot RoundStart signals and starts RoundTick loops.
|
||||||
|
* Must be called from the world thread.
|
||||||
|
*/
|
||||||
|
public void onRoundStart(MinigameRuntime runtime, int newRound) {
|
||||||
|
cancelLoops(roundLoops, runtime.runtimeId());
|
||||||
|
|
||||||
|
String roundKey = String.valueOf(newRound);
|
||||||
|
|
||||||
|
for (World world : worlds()) {
|
||||||
|
TriggerVolumeManager manager = manager(world);
|
||||||
|
if (manager == null) continue;
|
||||||
|
|
||||||
|
ActorPair actor = findActor(runtime, world);
|
||||||
|
if (actor == null) continue;
|
||||||
|
|
||||||
|
for (VolumeEntry volume : manager.getVolumes()) {
|
||||||
|
if (!runtime.minigameId().equals(volume.getRawTags().get(MinigameMapDiscoveryService.TAG_MINIGAME_ID))) continue;
|
||||||
|
|
||||||
|
// One-shot: fire immediately
|
||||||
|
if (roundKey.equals(volume.getRawTags().get(TAG_SIGNAL_ROUND_START))) {
|
||||||
|
toggle(manager, volume.getId(), actor);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Repeating: schedule loop
|
||||||
|
if (roundKey.equals(volume.getRawTags().get(TAG_SIGNAL_ROUND_TICK))) {
|
||||||
|
long delay = parseDelayMs(volume.getRawTags().get(TAG_SIGNAL_TICK_DELAY));
|
||||||
|
Future<?> future = scheduleLoop(runtime.runtimeId(), runtime.minigameId(),
|
||||||
|
TAG_SIGNAL_ROUND_TICK, roundKey, volume.getId(), world.getName(), delay);
|
||||||
|
roundLoops.computeIfAbsent(runtime.runtimeId(), k -> new ArrayList<>()).add(future);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when the game phase changes. Cancels old phase loops and starts new ones.
|
||||||
|
* Must be called from the world thread.
|
||||||
|
*/
|
||||||
|
public void onPhaseChange(MinigameRuntime runtime, MinigamePhase newPhase) {
|
||||||
|
cancelLoops(phaseLoops, runtime.runtimeId());
|
||||||
|
|
||||||
|
String phaseKey = newPhase.name();
|
||||||
|
|
||||||
|
for (World world : worlds()) {
|
||||||
|
TriggerVolumeManager manager = manager(world);
|
||||||
|
if (manager == null) continue;
|
||||||
|
|
||||||
|
ActorPair actor = findActor(runtime, world);
|
||||||
|
if (actor == null) continue;
|
||||||
|
|
||||||
|
for (VolumeEntry volume : manager.getVolumes()) {
|
||||||
|
if (!runtime.minigameId().equals(volume.getRawTags().get(MinigameMapDiscoveryService.TAG_MINIGAME_ID))) continue;
|
||||||
|
|
||||||
|
if (phaseKey.equals(volume.getRawTags().get(TAG_SIGNAL_PHASE_TICK))) {
|
||||||
|
long delay = parseDelayMs(volume.getRawTags().get(TAG_SIGNAL_TICK_DELAY));
|
||||||
|
Future<?> future = scheduleLoop(runtime.runtimeId(), runtime.minigameId(),
|
||||||
|
TAG_SIGNAL_PHASE_TICK, phaseKey, volume.getId(), world.getName(), delay);
|
||||||
|
phaseLoops.computeIfAbsent(runtime.runtimeId(), k -> new ArrayList<>()).add(future);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Schedules a named timer. When it expires, fires TAG_ADDED on volumes tagged with
|
||||||
|
* SignalOnTimerExpire=timerName and dispatches an on_timer_expired event.
|
||||||
|
* Replaces any existing timer with the same name.
|
||||||
|
*/
|
||||||
|
public void startTimer(MinigameRuntime runtime, String timerName, long durationMs) {
|
||||||
|
cancelTimer(runtime.runtimeId(), timerName);
|
||||||
|
runtime.timers().put(timerName, System.currentTimeMillis() + durationMs);
|
||||||
|
|
||||||
|
Future<?> future = scheduler.schedule(
|
||||||
|
() -> fireTimerExpiry(runtime.runtimeId(), runtime.minigameId(), timerName),
|
||||||
|
durationMs,
|
||||||
|
TimeUnit.MILLISECONDS
|
||||||
|
);
|
||||||
|
timerFutures.computeIfAbsent(runtime.runtimeId(), k -> new ConcurrentHashMap<>())
|
||||||
|
.put(timerName, future);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Cancels a named timer before it fires. */
|
||||||
|
public void cancelTimer(String runtimeId, String timerName) {
|
||||||
|
var futures = timerFutures.get(runtimeId);
|
||||||
|
if (futures != null) {
|
||||||
|
Future<?> f = futures.remove(timerName);
|
||||||
|
if (f != null) f.cancel(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Cancels all signal loops and timers for the given runtime. Call when the runtime ends. */
|
||||||
|
public void cancelAll(String runtimeId) {
|
||||||
|
cancelLoops(roundLoops, runtimeId);
|
||||||
|
cancelLoops(phaseLoops, runtimeId);
|
||||||
|
var futures = timerFutures.remove(runtimeId);
|
||||||
|
if (futures != null) futures.values().forEach(f -> f.cancel(false));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Shuts down the background scheduler. Call on plugin shutdown. */
|
||||||
|
public void shutdown() {
|
||||||
|
scheduler.shutdownNow();
|
||||||
|
}
|
||||||
|
|
||||||
|
private Future<?> scheduleLoop(String runtimeId, String minigameId, String tagKey, String tagValue,
|
||||||
|
String volumeId, String worldName, long delayMs) {
|
||||||
|
return scheduler.scheduleAtFixedRate(
|
||||||
|
() -> tickSignal(runtimeId, minigameId, tagKey, tagValue, volumeId, worldName),
|
||||||
|
delayMs, delayMs, TimeUnit.MILLISECONDS
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void tickSignal(String runtimeId, String minigameId, String expectedTagKey, String expectedTagValue,
|
||||||
|
String volumeId, String worldName) {
|
||||||
|
try {
|
||||||
|
Universe universe = Universe.get();
|
||||||
|
TriggerVolumesPlugin plugin = TriggerVolumesPlugin.get();
|
||||||
|
if (universe == null || plugin == null) return;
|
||||||
|
|
||||||
|
World world = universe.getWorld(worldName);
|
||||||
|
if (world == null) return;
|
||||||
|
|
||||||
|
world.execute(() -> {
|
||||||
|
var services = MinigameCorePlugin.getServices();
|
||||||
|
if (services == null) return;
|
||||||
|
|
||||||
|
var rtOpt = services.runtime().runtime(runtimeId);
|
||||||
|
if (rtOpt.isEmpty()) return;
|
||||||
|
MinigameRuntime rt = rtOpt.get();
|
||||||
|
|
||||||
|
if (TAG_SIGNAL_ROUND_TICK.equals(expectedTagKey)) {
|
||||||
|
if (!expectedTagValue.equals(String.valueOf(rt.roundNumber()))) return;
|
||||||
|
} else if (TAG_SIGNAL_PHASE_TICK.equals(expectedTagKey)) {
|
||||||
|
if (!expectedTagValue.equals(rt.phase().name())) return;
|
||||||
|
}
|
||||||
|
|
||||||
|
TriggerVolumeManager manager = world.getEntityStore().getStore().getResource(plugin.getManagerResourceType());
|
||||||
|
if (manager == null) return;
|
||||||
|
|
||||||
|
VolumeEntry volume = manager.getVolume(volumeId);
|
||||||
|
if (volume == null) return;
|
||||||
|
|
||||||
|
ActorPair actor = findActor(rt, world);
|
||||||
|
if (actor == null) return;
|
||||||
|
|
||||||
|
toggle(manager, volumeId, actor);
|
||||||
|
});
|
||||||
|
} catch (Exception e) {
|
||||||
|
LOGGER.at(Level.WARNING).withCause(e).log("Error in minigame signal tick for runtime=%s volume=%s", runtimeId, volumeId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void fireTimerExpiry(String runtimeId, String minigameId, String timerName) {
|
||||||
|
try {
|
||||||
|
Universe universe = Universe.get();
|
||||||
|
TriggerVolumesPlugin plugin = TriggerVolumesPlugin.get();
|
||||||
|
if (universe == null || plugin == null) return;
|
||||||
|
|
||||||
|
var dispatched = new AtomicBoolean(false);
|
||||||
|
|
||||||
|
for (World world : worlds()) {
|
||||||
|
world.execute(() -> {
|
||||||
|
var services = MinigameCorePlugin.getServices();
|
||||||
|
if (services == null) return;
|
||||||
|
|
||||||
|
var rtOpt = services.runtime().runtime(runtimeId);
|
||||||
|
if (rtOpt.isEmpty()) return;
|
||||||
|
MinigameRuntime rt = rtOpt.get();
|
||||||
|
|
||||||
|
if (dispatched.compareAndSet(false, true)) {
|
||||||
|
services.runtime().dispatch(rt, "on_timer_expired", Map.of("timer_name", timerName));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Internal timers (name starts with '$') are handled by the runtime service,
|
||||||
|
// not by volume signals.
|
||||||
|
if (timerName.startsWith("$")) return;
|
||||||
|
|
||||||
|
TriggerVolumeManager manager = world.getEntityStore().getStore().getResource(plugin.getManagerResourceType());
|
||||||
|
if (manager == null) return;
|
||||||
|
|
||||||
|
ActorPair actor = findActor(rt, world);
|
||||||
|
if (actor == null) return;
|
||||||
|
|
||||||
|
for (VolumeEntry volume : manager.getVolumes()) {
|
||||||
|
if (!minigameId.equals(volume.getRawTags().get(MinigameMapDiscoveryService.TAG_MINIGAME_ID))) continue;
|
||||||
|
if (timerName.equals(volume.getRawTags().get(TAG_SIGNAL_TIMER_EXPIRE))) {
|
||||||
|
toggle(manager, volume.getId(), actor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
LOGGER.at(Level.WARNING).withCause(e).log("Error in minigame timer expiry for runtime=%s timer=%s", runtimeId, timerName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Toggles MinigameSignal between "0" and "1" to fire TAG_ADDED. */
|
||||||
|
private static void toggle(TriggerVolumeManager manager, String volumeId, ActorPair actor) {
|
||||||
|
VolumeEntry volume = manager.getVolume(volumeId);
|
||||||
|
if (volume == null) return;
|
||||||
|
String current = volume.getRawTags().get(TAG_SIGNAL_KEY);
|
||||||
|
String next = "1".equals(current) ? "0" : "1";
|
||||||
|
manager.setTag(volumeId, TAG_SIGNAL_KEY, next, actor.ref(), actor.uuid());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
private static ActorPair findActor(MinigameRuntime runtime, World world) {
|
||||||
|
Universe universe = Universe.get();
|
||||||
|
if (universe == null) return null;
|
||||||
|
for (String playerId : runtime.players().keySet()) {
|
||||||
|
try {
|
||||||
|
UUID uuid = UUID.fromString(playerId);
|
||||||
|
PlayerRef playerRef = universe.getPlayer(uuid);
|
||||||
|
if (playerRef == null) continue;
|
||||||
|
Ref<EntityStore> ref = playerRef.getReference();
|
||||||
|
if (ref == null || !ref.isValid()) continue;
|
||||||
|
if (ref.getStore().getExternalData().getWorld() != world) continue;
|
||||||
|
return new ActorPair(ref, uuid);
|
||||||
|
} catch (IllegalArgumentException ignored) {}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
private static TriggerVolumeManager manager(World world) {
|
||||||
|
TriggerVolumesPlugin plugin = TriggerVolumesPlugin.get();
|
||||||
|
if (plugin == null) return null;
|
||||||
|
return world.getEntityStore().getStore().getResource(plugin.getManagerResourceType());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Iterable<World> worlds() {
|
||||||
|
Universe universe = Universe.get();
|
||||||
|
return universe != null ? universe.getWorlds().values() : List.of();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void cancelLoops(Map<String, List<Future<?>>> loopMap, String runtimeId) {
|
||||||
|
List<Future<?>> futures = loopMap.remove(runtimeId);
|
||||||
|
if (futures != null) futures.forEach(f -> f.cancel(false));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static long parseDelayMs(@Nullable String value) {
|
||||||
|
if (value == null || value.isBlank()) return MIN_DELAY_MS;
|
||||||
|
try {
|
||||||
|
double seconds = Double.parseDouble(value);
|
||||||
|
return Math.max(MIN_DELAY_MS, (long) (seconds * 1000));
|
||||||
|
} catch (NumberFormatException ignored) {
|
||||||
|
return MIN_DELAY_MS;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private record ActorPair(Ref<EntityStore> ref, UUID uuid) {}
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
package net.kewwbec.minigames.service;
|
||||||
|
|
||||||
|
import net.kewwbec.minigames.model.RewardConfig;
|
||||||
|
import net.kewwbec.minigames.runtime.MinigameRuntime;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public interface RewardService {
|
||||||
|
void giveRewards(MinigameRuntime runtime, String targetPlayerId, List<RewardConfig> rewards);
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
package net.kewwbec.minigames.service;
|
||||||
|
|
||||||
|
import net.kewwbec.minigames.runtime.MinigameRuntime;
|
||||||
|
|
||||||
|
import java.util.Comparator;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
public final class ScoreService {
|
||||||
|
public void addPlayerScore(MinigameRuntime runtime, String playerId, int amount) {
|
||||||
|
var player = runtime.players().get(playerId);
|
||||||
|
if (player != null) {
|
||||||
|
player.score(player.score() + amount);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addTeamScore(MinigameRuntime runtime, String teamId, int amount) {
|
||||||
|
var team = runtime.teams().get(teamId);
|
||||||
|
if (team != null) {
|
||||||
|
team.score(team.score() + amount);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Optional<String> resolveWinningPlayer(MinigameRuntime runtime) {
|
||||||
|
return runtime.players().values().stream()
|
||||||
|
.max(Comparator.comparingInt(player -> player.score()))
|
||||||
|
.map(player -> player.playerId());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
package net.kewwbec.minigames.service;
|
||||||
|
|
||||||
|
import net.kewwbec.minigames.runtime.MinigameRuntime;
|
||||||
|
|
||||||
|
public final class StreakService {
|
||||||
|
private static final String PREFIX = "streak:";
|
||||||
|
|
||||||
|
public int increment(MinigameRuntime runtime, String ownerId, String streakId) {
|
||||||
|
var key = key(ownerId, streakId);
|
||||||
|
var next = current(runtime, ownerId, streakId) + 1;
|
||||||
|
runtime.variables().put(key, next);
|
||||||
|
return next;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void reset(MinigameRuntime runtime, String ownerId, String streakId) {
|
||||||
|
runtime.variables().remove(key(ownerId, streakId));
|
||||||
|
}
|
||||||
|
|
||||||
|
public int current(MinigameRuntime runtime, String ownerId, String streakId) {
|
||||||
|
var value = runtime.variables().get(key(ownerId, streakId));
|
||||||
|
return value instanceof Number number ? number.intValue() : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String key(String ownerId, String streakId) {
|
||||||
|
return PREFIX + ownerId + ":" + streakId;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
package net.kewwbec.minigames.service;
|
||||||
|
|
||||||
|
import net.kewwbec.minigames.model.TeamState;
|
||||||
|
import net.kewwbec.minigames.runtime.MinigameRuntime;
|
||||||
|
|
||||||
|
public final class TeamService {
|
||||||
|
public TeamState createTeam(MinigameRuntime runtime, String teamId, String name, String color) {
|
||||||
|
var team = new TeamState(teamId, name, color);
|
||||||
|
runtime.teams().put(teamId, team);
|
||||||
|
return team;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void assignPlayer(MinigameRuntime runtime, String playerId, String teamId) {
|
||||||
|
var player = runtime.players().get(playerId);
|
||||||
|
var team = runtime.teams().get(teamId);
|
||||||
|
if (player == null || team == null) {
|
||||||
|
throw new IllegalArgumentException("minigames.error.invalid_team_assignment");
|
||||||
|
}
|
||||||
|
player.teamId(teamId);
|
||||||
|
team.players().add(playerId);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,229 @@
|
|||||||
|
package net.kewwbec.minigames.system;
|
||||||
|
|
||||||
|
import com.hypixel.hytale.builtin.triggervolumes.TriggerVolumesPlugin;
|
||||||
|
import com.hypixel.hytale.builtin.triggervolumes.asset.TriggerEffectAsset;
|
||||||
|
import com.hypixel.hytale.builtin.triggervolumes.effect.TriggerEffect;
|
||||||
|
import com.hypixel.hytale.builtin.triggervolumes.manager.TriggerVolumeManager;
|
||||||
|
import com.hypixel.hytale.builtin.triggervolumes.manager.VolumeEntry;
|
||||||
|
import com.hypixel.hytale.component.Archetype;
|
||||||
|
import com.hypixel.hytale.component.CommandBuffer;
|
||||||
|
import com.hypixel.hytale.component.Ref;
|
||||||
|
import com.hypixel.hytale.component.Store;
|
||||||
|
import com.hypixel.hytale.component.query.Query;
|
||||||
|
import com.hypixel.hytale.server.core.entity.UUIDComponent;
|
||||||
|
import com.hypixel.hytale.server.core.modules.entity.component.TransformComponent;
|
||||||
|
import com.hypixel.hytale.server.core.modules.entity.damage.Damage;
|
||||||
|
import com.hypixel.hytale.server.core.modules.entity.damage.DeathComponent;
|
||||||
|
import com.hypixel.hytale.server.core.modules.entity.damage.DeathSystems;
|
||||||
|
import com.hypixel.hytale.server.core.universe.PlayerRef;
|
||||||
|
import com.hypixel.hytale.server.core.universe.world.storage.EntityStore;
|
||||||
|
import com.hypixel.hytale.server.npc.entities.NPCEntity;
|
||||||
|
import net.kewwbec.minigames.MinigameCorePlugin;
|
||||||
|
import net.kewwbec.minigames.model.PlayerMinigameState;
|
||||||
|
import net.kewwbec.minigames.runtime.MinigameRuntime;
|
||||||
|
import net.kewwbec.minigames.service.MinigameMapDiscoveryService;
|
||||||
|
import net.kewwbec.minigames.volume.effect.AwardKillScoreEffect;
|
||||||
|
|
||||||
|
import javax.annotation.Nonnull;
|
||||||
|
import javax.annotation.Nullable;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import static net.kewwbec.minigames.model.Enums.FriendlyFireKillScoreMode.LOSES_POINTS;
|
||||||
|
|
||||||
|
public final class MinigameKillScoreSystem extends DeathSystems.OnDeathSystem {
|
||||||
|
@Nonnull
|
||||||
|
private static final Query<EntityStore> QUERY = Archetype.of(DeathComponent.getComponentType(), TransformComponent.getComponentType());
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
|
@Override
|
||||||
|
public Query<EntityStore> getQuery() {
|
||||||
|
return QUERY;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onComponentAdded(
|
||||||
|
@Nonnull Ref<EntityStore> victimRef,
|
||||||
|
@Nonnull DeathComponent component,
|
||||||
|
@Nonnull Store<EntityStore> store,
|
||||||
|
@Nonnull CommandBuffer<EntityStore> commandBuffer
|
||||||
|
) {
|
||||||
|
Damage deathInfo = component.getDeathInfo();
|
||||||
|
if (!(deathInfo != null && deathInfo.getSource() instanceof Damage.EntitySource source)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ref<EntityStore> attackerRef = source.getRef();
|
||||||
|
if (!attackerRef.isValid()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
String attackerId = playerId(store, attackerRef);
|
||||||
|
if (attackerId == null || attackerId.isBlank()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var services = MinigameCorePlugin.getServices();
|
||||||
|
if (services == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
MinigameRuntime runtime = services.runtime().runtimeForPlayer(attackerId).orElse(null);
|
||||||
|
if (runtime == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
String victimKind = victimKind(store, victimRef);
|
||||||
|
if (victimKind.isBlank()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
TransformComponent transform = store.getComponent(victimRef, TransformComponent.getComponentType());
|
||||||
|
TriggerVolumeManager manager = manager(store);
|
||||||
|
if (transform == null || manager == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
String victimPlayerId = playerId(store, victimRef);
|
||||||
|
boolean friendlyKill = victimPlayerId != null && sameTeam(runtime.players().get(attackerId), runtime.players().get(victimPlayerId));
|
||||||
|
for (VolumeEntry volume : manager.getVolumes()) {
|
||||||
|
if (!matchesRuntime(volume, runtime) || !volume.getShape().contains(volume.getPosition(), transform.getPosition())) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for (var effect : effects(volume)) {
|
||||||
|
if (effect instanceof AwardKillScoreEffect killScore && matchesRule(killScore, runtime, victimKind)) {
|
||||||
|
apply(runtime, attackerId, killScore, friendlyKill);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
|
private static Iterable<TriggerEffect> effects(@Nonnull VolumeEntry volume) {
|
||||||
|
String assetRef = volume.getEffectAssetRef();
|
||||||
|
if (assetRef != null && !assetRef.isBlank()) {
|
||||||
|
TriggerEffectAsset asset = com.hypixel.hytale.assetstore.AssetRegistry.getAssetStore(TriggerEffectAsset.class).getAssetMap().getAsset(assetRef);
|
||||||
|
if (asset != null) {
|
||||||
|
return java.util.List.of(asset.getEffects());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return volume.getEffects();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void apply(
|
||||||
|
@Nonnull MinigameRuntime runtime,
|
||||||
|
@Nonnull String attackerId,
|
||||||
|
@Nonnull AwardKillScoreEffect rule,
|
||||||
|
boolean friendlyKill
|
||||||
|
) {
|
||||||
|
int points = Math.max(0, rule.points());
|
||||||
|
if (friendlyKill) {
|
||||||
|
if (runtime.definition().getFriendlyFireKillScoreMode() != LOSES_POINTS) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
points = -points;
|
||||||
|
}
|
||||||
|
|
||||||
|
PlayerMinigameState player = runtime.players().get(attackerId);
|
||||||
|
if (player == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rule.awardTarget() == AwardKillScoreEffect.AwardTarget.PLAYER || rule.awardTarget() == AwardKillScoreEffect.AwardTarget.BOTH) {
|
||||||
|
player.score(player.score() + points);
|
||||||
|
}
|
||||||
|
if ((rule.awardTarget() == AwardKillScoreEffect.AwardTarget.TEAM || rule.awardTarget() == AwardKillScoreEffect.AwardTarget.BOTH)
|
||||||
|
&& player.teamId() != null && !player.teamId().isBlank()) {
|
||||||
|
var team = runtime.teams().get(player.teamId());
|
||||||
|
if (team != null) {
|
||||||
|
team.score(team.score() + points);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, Object> payload = new HashMap<>();
|
||||||
|
payload.put("runtime_id", runtime.runtimeId());
|
||||||
|
payload.put("minigame_id", runtime.minigameId());
|
||||||
|
payload.put("map_id", runtime.mapId());
|
||||||
|
payload.put("variant_id", runtime.variantId());
|
||||||
|
payload.put("player_id", attackerId);
|
||||||
|
payload.put("points", points);
|
||||||
|
payload.put("friendly_kill", friendlyKill);
|
||||||
|
MinigameCorePlugin.getServices().runtime().dispatch(runtime, "on_kill_score", payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean matchesRule(@Nonnull AwardKillScoreEffect rule, @Nonnull MinigameRuntime runtime, @Nonnull String victimKind) {
|
||||||
|
String ruleMinigame = rule.configuredMinigameId();
|
||||||
|
if (!ruleMinigame.isBlank() && !ruleMinigame.equals(runtime.minigameId())) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
String normalizedVictim = normalize(victimKind);
|
||||||
|
for (String target : rule.targetIds()) {
|
||||||
|
String normalized = normalize(target);
|
||||||
|
if (normalized.equals("*") || normalized.equals(normalizedVictim)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean matchesRuntime(@Nonnull VolumeEntry volume, @Nonnull MinigameRuntime runtime) {
|
||||||
|
Map<String, String> tags = volume.getRawTags();
|
||||||
|
String minigameId = clean(tags.get(MinigameMapDiscoveryService.TAG_MINIGAME_ID));
|
||||||
|
String mapId = clean(tags.get(MinigameMapDiscoveryService.TAG_MAP_ID));
|
||||||
|
String variantId = clean(tags.get(MinigameMapDiscoveryService.TAG_VARIANT_ID));
|
||||||
|
if (!minigameId.isBlank() && !minigameId.equals(runtime.minigameId())) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!mapId.isBlank() && !mapId.equals(runtime.mapId())) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return variantId.isBlank() || variantId.equals(runtime.variantId());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean sameTeam(@Nullable PlayerMinigameState attacker, @Nullable PlayerMinigameState victim) {
|
||||||
|
return attacker != null
|
||||||
|
&& victim != null
|
||||||
|
&& attacker.teamId() != null
|
||||||
|
&& !attacker.teamId().isBlank()
|
||||||
|
&& attacker.teamId().equals(victim.teamId());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
|
private static String victimKind(@Nonnull Store<EntityStore> store, @Nonnull Ref<EntityStore> victimRef) {
|
||||||
|
if (store.getComponent(victimRef, PlayerRef.getComponentType()) != null) {
|
||||||
|
return "Player";
|
||||||
|
}
|
||||||
|
NPCEntity npc = store.getComponent(victimRef, NPCEntity.getComponentType());
|
||||||
|
if (npc != null && npc.getRoleName() != null) {
|
||||||
|
return npc.getRoleName();
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
private static String playerId(@Nonnull Store<EntityStore> store, @Nonnull Ref<EntityStore> ref) {
|
||||||
|
PlayerRef player = store.getComponent(ref, PlayerRef.getComponentType());
|
||||||
|
if (player != null) {
|
||||||
|
return player.getUuid().toString();
|
||||||
|
}
|
||||||
|
UUIDComponent uuid = store.getComponent(ref, UUIDComponent.getComponentType());
|
||||||
|
return uuid != null ? uuid.getUuid().toString() : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
private static TriggerVolumeManager manager(@Nonnull Store<EntityStore> store) {
|
||||||
|
TriggerVolumesPlugin plugin = TriggerVolumesPlugin.get();
|
||||||
|
return plugin != null ? store.getResource(plugin.getManagerResourceType()) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
|
private static String clean(@Nullable String value) {
|
||||||
|
return value != null ? value.trim() : "";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
|
private static String normalize(@Nullable String value) {
|
||||||
|
return clean(value).toLowerCase(Locale.ROOT);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
package net.kewwbec.minigames.volume.condition;
|
||||||
|
|
||||||
|
import com.hypixel.hytale.builtin.triggervolumes.effect.TriggerContext;
|
||||||
|
import com.hypixel.hytale.codec.Codec;
|
||||||
|
import com.hypixel.hytale.codec.KeyedCodec;
|
||||||
|
import com.hypixel.hytale.codec.builder.BuilderCodec;
|
||||||
|
import com.hypixel.hytale.codec.codecs.EnumCodec;
|
||||||
|
|
||||||
|
import javax.annotation.Nonnull;
|
||||||
|
|
||||||
|
public final class CounterCondition extends MinigameRuntimeCondition {
|
||||||
|
public static final String TYPE_ID = "Counter";
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
|
public static final BuilderCodec<CounterCondition> CODEC =
|
||||||
|
BuilderCodec.builder(CounterCondition.class, CounterCondition::new, MINIGAME_BASE_CODEC)
|
||||||
|
.append(new KeyedCodec<>("CounterId", Codec.STRING), (condition, value) -> condition.counterId = value, condition -> condition.counterId)
|
||||||
|
.add()
|
||||||
|
.append(new KeyedCodec<>("Compare", new EnumCodec<>(NumberCompare.class), false), (condition, value) -> condition.compare = value, condition -> condition.compare)
|
||||||
|
.add()
|
||||||
|
.append(new KeyedCodec<>("Value", Codec.INTEGER), (condition, value) -> condition.value = value, condition -> condition.value)
|
||||||
|
.add()
|
||||||
|
.build();
|
||||||
|
|
||||||
|
private String counterId;
|
||||||
|
private NumberCompare compare = NumberCompare.GREATER_THAN_OR_EQUAL;
|
||||||
|
private int value;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean test(@Nonnull TriggerContext context) {
|
||||||
|
String game = resolvedMinigameId(context);
|
||||||
|
var rt = runtime(context);
|
||||||
|
if (rt.isEmpty()) {
|
||||||
|
debugMessage(context, TYPE_ID, false, "minigame=" + game + " no runtime found");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
int actual = number(rt.get().variables().get("counter:" + counterId));
|
||||||
|
NumberCompare op = compare != null ? compare : NumberCompare.GREATER_THAN_OR_EQUAL;
|
||||||
|
boolean result = op.test(actual, value);
|
||||||
|
debugMessage(context, TYPE_ID, result, "minigame=" + game + " counter=" + counterId + " actual=" + actual + " " + op + " " + value);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int number(Object value) {
|
||||||
|
if (value instanceof Number number) {
|
||||||
|
return number.intValue();
|
||||||
|
}
|
||||||
|
if (value == null) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return Integer.parseInt(String.valueOf(value));
|
||||||
|
} catch (NumberFormatException ignored) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
package net.kewwbec.minigames.volume.condition;
|
||||||
|
|
||||||
|
import com.hypixel.hytale.builtin.triggervolumes.effect.TriggerContext;
|
||||||
|
import com.hypixel.hytale.codec.Codec;
|
||||||
|
import com.hypixel.hytale.codec.KeyedCodec;
|
||||||
|
import com.hypixel.hytale.codec.builder.BuilderCodec;
|
||||||
|
import com.hypixel.hytale.codec.codecs.EnumCodec;
|
||||||
|
|
||||||
|
import javax.annotation.Nonnull;
|
||||||
|
|
||||||
|
public final class CurrentRoundCondition extends MinigameRuntimeCondition {
|
||||||
|
public static final String TYPE_ID = "CurrentRound";
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
|
public static final BuilderCodec<CurrentRoundCondition> CODEC =
|
||||||
|
BuilderCodec.builder(CurrentRoundCondition.class, CurrentRoundCondition::new, MINIGAME_BASE_CODEC)
|
||||||
|
.append(new KeyedCodec<>("Compare", new EnumCodec<>(NumberCompare.class), false), (condition, value) -> condition.compare = value, condition -> condition.compare)
|
||||||
|
.add()
|
||||||
|
.append(new KeyedCodec<>("Round", Codec.INTEGER), (condition, value) -> condition.round = value, condition -> condition.round)
|
||||||
|
.add()
|
||||||
|
.build();
|
||||||
|
|
||||||
|
private NumberCompare compare = NumberCompare.EQUAL;
|
||||||
|
private int round = 1;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean test(@Nonnull TriggerContext context) {
|
||||||
|
String game = resolvedMinigameId(context);
|
||||||
|
var rt = runtime(context);
|
||||||
|
if (rt.isEmpty()) {
|
||||||
|
debugMessage(context, TYPE_ID, false, "minigame=" + game + " no runtime found");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
int actual = rt.get().roundNumber();
|
||||||
|
NumberCompare op = compare != null ? compare : NumberCompare.EQUAL;
|
||||||
|
boolean result = op.test(actual, round);
|
||||||
|
debugMessage(context, TYPE_ID, result, "minigame=" + game + " actualRound=" + actual + " " + op + " " + round);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
package net.kewwbec.minigames.volume.condition;
|
||||||
|
|
||||||
|
import com.hypixel.hytale.builtin.triggervolumes.effect.TriggerContext;
|
||||||
|
import com.hypixel.hytale.codec.Codec;
|
||||||
|
import com.hypixel.hytale.codec.KeyedCodec;
|
||||||
|
import com.hypixel.hytale.codec.builder.BuilderCodec;
|
||||||
|
import com.hypixel.hytale.codec.codecs.EnumCodec;
|
||||||
|
import net.kewwbec.minigames.model.Enums.MinigamePhase;
|
||||||
|
|
||||||
|
import javax.annotation.Nonnull;
|
||||||
|
|
||||||
|
public final class MinigamePhaseCondition extends MinigameRuntimeCondition {
|
||||||
|
public static final String TYPE_ID = "MinigamePhase";
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
|
public static final BuilderCodec<MinigamePhaseCondition> CODEC =
|
||||||
|
BuilderCodec.builder(MinigamePhaseCondition.class, MinigamePhaseCondition::new, MINIGAME_BASE_CODEC)
|
||||||
|
.append(new KeyedCodec<>("Phase", new EnumCodec<>(MinigamePhase.class)), (condition, value) -> condition.phase = value, condition -> condition.phase)
|
||||||
|
.add()
|
||||||
|
.build();
|
||||||
|
|
||||||
|
private MinigamePhase phase = MinigamePhase.ACTIVE;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean test(@Nonnull TriggerContext context) {
|
||||||
|
String id = resolvedMinigameId(context);
|
||||||
|
var rt = runtime(context);
|
||||||
|
if (rt.isEmpty()) {
|
||||||
|
debugMessage(context, TYPE_ID, false, "minigame=" + id + " no runtime found, expected phase=" + phase);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
MinigamePhase actual = rt.get().phase();
|
||||||
|
boolean result = actual == phase;
|
||||||
|
debugMessage(context, TYPE_ID, result, "minigame=" + id + " expected=" + phase + " actual=" + actual);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,116 @@
|
|||||||
|
package net.kewwbec.minigames.volume.condition;
|
||||||
|
|
||||||
|
import com.hypixel.hytale.builtin.triggervolumes.effect.TriggerCondition;
|
||||||
|
import com.hypixel.hytale.builtin.triggervolumes.effect.TriggerContext;
|
||||||
|
import com.hypixel.hytale.codec.Codec;
|
||||||
|
import com.hypixel.hytale.codec.KeyedCodec;
|
||||||
|
import com.hypixel.hytale.codec.builder.BuilderCodec;
|
||||||
|
import com.hypixel.hytale.server.core.Message;
|
||||||
|
import com.hypixel.hytale.server.core.entity.UUIDComponent;
|
||||||
|
import com.hypixel.hytale.server.core.universe.PlayerRef;
|
||||||
|
import net.kewwbec.minigames.MinigameCorePlugin;
|
||||||
|
import net.kewwbec.minigames.model.MinigameDefinition;
|
||||||
|
import net.kewwbec.minigames.runtime.MinigameRuntime;
|
||||||
|
|
||||||
|
import javax.annotation.Nonnull;
|
||||||
|
import javax.annotation.Nullable;
|
||||||
|
import java.awt.Color;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
public abstract class MinigameRuntimeCondition extends TriggerCondition {
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
|
public static final BuilderCodec<MinigameRuntimeCondition> MINIGAME_BASE_CODEC =
|
||||||
|
BuilderCodec.abstractBuilder(MinigameRuntimeCondition.class, BASE_CODEC)
|
||||||
|
.<String>append(
|
||||||
|
new KeyedCodec<>("MinigameId", Codec.STRING, false),
|
||||||
|
(cond, value) -> cond.minigameId = value,
|
||||||
|
cond -> cond.minigameId
|
||||||
|
)
|
||||||
|
.add()
|
||||||
|
.build();
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
protected String minigameId;
|
||||||
|
|
||||||
|
private volatile long lastDebugMs = 0L;
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
|
protected Optional<MinigameRuntime> runtime(@Nonnull TriggerContext context) {
|
||||||
|
var services = MinigameCorePlugin.getServices();
|
||||||
|
if (services == null) {
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
String playerId = resolvePlayerId(context, null);
|
||||||
|
if (playerId != null) {
|
||||||
|
Optional<MinigameRuntime> playerRuntime = services.runtime().runtimeForPlayer(playerId);
|
||||||
|
if (playerRuntime.isPresent()) {
|
||||||
|
return playerRuntime;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var candidate = services.maps().candidateFromVolume(context, minigameId != null ? minigameId : "");
|
||||||
|
String resolvedMinigameId = !candidate.minigameId().isBlank() ? candidate.minigameId() : minigameId;
|
||||||
|
if (resolvedMinigameId == null || resolvedMinigameId.isBlank()) {
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
return services.runtime().runtimeForArena(resolvedMinigameId, candidate.mapId(), candidate.variantId())
|
||||||
|
.or(() -> services.runtime().runtime(resolvedMinigameId));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
|
protected String resolvedMinigameId(@Nonnull TriggerContext context) {
|
||||||
|
var services = MinigameCorePlugin.getServices();
|
||||||
|
if (services != null) {
|
||||||
|
var candidate = services.maps().candidateFromVolume(context, minigameId != null ? minigameId : "");
|
||||||
|
if (!candidate.minigameId().isBlank()) {
|
||||||
|
return candidate.minigameId();
|
||||||
|
}
|
||||||
|
if (minigameId == null || minigameId.isBlank()) {
|
||||||
|
String playerId = resolvePlayerId(context, null);
|
||||||
|
if (playerId != null) {
|
||||||
|
var playerRuntime = services.runtime().runtimeForPlayer(playerId);
|
||||||
|
if (playerRuntime.isPresent()) {
|
||||||
|
return playerRuntime.get().minigameId();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return minigameId != null ? minigameId : "";
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void debugMessage(@Nonnull TriggerContext context, @Nonnull String conditionType, boolean result, @Nonnull String details) {
|
||||||
|
String id = resolvedMinigameId(context);
|
||||||
|
if (id.isBlank()) return;
|
||||||
|
var def = MinigameDefinition.getAssetMap().getAsset(id);
|
||||||
|
if (def == null || !def.isDebug()) return;
|
||||||
|
long now = System.currentTimeMillis();
|
||||||
|
if (now - lastDebugMs < 5000L) return;
|
||||||
|
lastDebugMs = now;
|
||||||
|
PlayerRef playerRef = context.getStore().getComponent(context.getEntityRef(), PlayerRef.getComponentType());
|
||||||
|
if (playerRef != null) {
|
||||||
|
playerRef.sendMessage(
|
||||||
|
Message.translation("minigames.debug.condition")
|
||||||
|
.param("type", conditionType)
|
||||||
|
.param("details", details)
|
||||||
|
.param("result", result ? "PASS" : "FAIL")
|
||||||
|
.color(result ? Color.GREEN : Color.RED)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
protected static String resolvePlayerId(@Nonnull TriggerContext context, @Nullable String configuredPlayerId) {
|
||||||
|
if (configuredPlayerId != null && !configuredPlayerId.isBlank()) {
|
||||||
|
return configuredPlayerId;
|
||||||
|
}
|
||||||
|
|
||||||
|
PlayerRef playerRef = context.getStore().getComponent(context.getEntityRef(), PlayerRef.getComponentType());
|
||||||
|
if (playerRef != null) {
|
||||||
|
return playerRef.getUuid().toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
UUIDComponent uuidComponent = context.getStore().getComponent(context.getEntityRef(), UUIDComponent.getComponentType());
|
||||||
|
return uuidComponent != null ? uuidComponent.getUuid().toString() : null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
package net.kewwbec.minigames.volume.condition;
|
||||||
|
|
||||||
|
public enum NumberCompare {
|
||||||
|
EQUAL,
|
||||||
|
NOT_EQUAL,
|
||||||
|
GREATER_THAN,
|
||||||
|
GREATER_THAN_OR_EQUAL,
|
||||||
|
LESS_THAN,
|
||||||
|
LESS_THAN_OR_EQUAL;
|
||||||
|
|
||||||
|
public boolean test(int actual, int expected) {
|
||||||
|
return switch (this) {
|
||||||
|
case EQUAL -> actual == expected;
|
||||||
|
case NOT_EQUAL -> actual != expected;
|
||||||
|
case GREATER_THAN -> actual > expected;
|
||||||
|
case GREATER_THAN_OR_EQUAL -> actual >= expected;
|
||||||
|
case LESS_THAN -> actual < expected;
|
||||||
|
case LESS_THAN_OR_EQUAL -> actual <= expected;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
package net.kewwbec.minigames.volume.condition;
|
||||||
|
|
||||||
|
import com.hypixel.hytale.builtin.triggervolumes.effect.TriggerContext;
|
||||||
|
import com.hypixel.hytale.codec.Codec;
|
||||||
|
import com.hypixel.hytale.codec.KeyedCodec;
|
||||||
|
import com.hypixel.hytale.codec.builder.BuilderCodec;
|
||||||
|
import com.hypixel.hytale.codec.codecs.EnumCodec;
|
||||||
|
import net.kewwbec.minigames.model.Enums.ObjectiveStatus;
|
||||||
|
|
||||||
|
import javax.annotation.Nonnull;
|
||||||
|
|
||||||
|
public final class ObjectiveStatusCondition extends MinigameRuntimeCondition {
|
||||||
|
public static final String TYPE_ID = "ObjectiveStatus";
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
|
public static final BuilderCodec<ObjectiveStatusCondition> CODEC =
|
||||||
|
BuilderCodec.builder(ObjectiveStatusCondition.class, ObjectiveStatusCondition::new, MINIGAME_BASE_CODEC)
|
||||||
|
.append(new KeyedCodec<>("ObjectiveId", Codec.STRING), (condition, value) -> condition.objectiveId = value, condition -> condition.objectiveId)
|
||||||
|
.add()
|
||||||
|
.append(new KeyedCodec<>("Status", new EnumCodec<>(ObjectiveStatus.class)), (condition, value) -> condition.status = value, condition -> condition.status)
|
||||||
|
.add()
|
||||||
|
.build();
|
||||||
|
|
||||||
|
private String objectiveId;
|
||||||
|
private ObjectiveStatus status = ObjectiveStatus.COMPLETE;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean test(@Nonnull TriggerContext context) {
|
||||||
|
String game = resolvedMinigameId(context);
|
||||||
|
var rt = runtime(context);
|
||||||
|
if (rt.isEmpty()) {
|
||||||
|
debugMessage(context, TYPE_ID, false, "minigame=" + game + " no runtime found");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (objectiveId == null || objectiveId.isBlank()) {
|
||||||
|
debugMessage(context, TYPE_ID, false, "minigame=" + game + " no objective id configured");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
var objective = rt.get().objectives().get(objectiveId);
|
||||||
|
if (objective == null) {
|
||||||
|
debugMessage(context, TYPE_ID, false, "minigame=" + game + " objective='" + objectiveId + "' not found");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
ObjectiveStatus expected = status != null ? status : ObjectiveStatus.COMPLETE;
|
||||||
|
boolean result = objective.status() == expected;
|
||||||
|
debugMessage(context, TYPE_ID, result,
|
||||||
|
"minigame=" + game + " objective='" + objectiveId + "' status=" + objective.status() + " expected=" + expected);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
package net.kewwbec.minigames.volume.condition;
|
||||||
|
|
||||||
|
import com.hypixel.hytale.builtin.triggervolumes.effect.TriggerContext;
|
||||||
|
import com.hypixel.hytale.codec.Codec;
|
||||||
|
import com.hypixel.hytale.codec.KeyedCodec;
|
||||||
|
import com.hypixel.hytale.codec.builder.BuilderCodec;
|
||||||
|
|
||||||
|
import javax.annotation.Nonnull;
|
||||||
|
|
||||||
|
public final class PlayerInMinigameCondition extends MinigameRuntimeCondition {
|
||||||
|
public static final String TYPE_ID = "PlayerInMinigame";
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
|
public static final BuilderCodec<PlayerInMinigameCondition> CODEC =
|
||||||
|
BuilderCodec.builder(PlayerInMinigameCondition.class, PlayerInMinigameCondition::new, MINIGAME_BASE_CODEC)
|
||||||
|
.append(new KeyedCodec<>("PlayerId", Codec.STRING, false), (condition, value) -> condition.playerId = value, condition -> condition.playerId)
|
||||||
|
.add()
|
||||||
|
.build();
|
||||||
|
|
||||||
|
private String playerId;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean test(@Nonnull TriggerContext context) {
|
||||||
|
String id = resolvePlayerId(context, playerId);
|
||||||
|
String game = resolvedMinigameId(context);
|
||||||
|
if (id == null) {
|
||||||
|
debugMessage(context, TYPE_ID, false, "minigame=" + game + " no player resolved");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
var rt = runtime(context);
|
||||||
|
if (rt.isEmpty()) {
|
||||||
|
debugMessage(context, TYPE_ID, false, "minigame=" + game + " player=" + id + " no runtime found");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
boolean result = rt.get().players().containsKey(id);
|
||||||
|
debugMessage(context, TYPE_ID, result, "minigame=" + game + " player=" + id + (result ? " is in game" : " is NOT in game (playerCount=" + rt.get().players().size() + ")"));
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
package net.kewwbec.minigames.volume.condition;
|
||||||
|
|
||||||
|
import com.hypixel.hytale.builtin.triggervolumes.effect.TriggerContext;
|
||||||
|
import com.hypixel.hytale.codec.Codec;
|
||||||
|
import com.hypixel.hytale.codec.KeyedCodec;
|
||||||
|
import com.hypixel.hytale.codec.builder.BuilderCodec;
|
||||||
|
import com.hypixel.hytale.codec.codecs.EnumCodec;
|
||||||
|
|
||||||
|
import javax.annotation.Nonnull;
|
||||||
|
|
||||||
|
public final class PlayerLivesCondition extends MinigameRuntimeCondition {
|
||||||
|
public static final String TYPE_ID = "PlayerLives";
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
|
public static final BuilderCodec<PlayerLivesCondition> CODEC =
|
||||||
|
BuilderCodec.builder(PlayerLivesCondition.class, PlayerLivesCondition::new, MINIGAME_BASE_CODEC)
|
||||||
|
.append(new KeyedCodec<>("PlayerId", Codec.STRING, false), (condition, value) -> condition.playerId = value, condition -> condition.playerId)
|
||||||
|
.add()
|
||||||
|
.append(new KeyedCodec<>("Compare", new EnumCodec<>(NumberCompare.class), false), (condition, value) -> condition.compare = value, condition -> condition.compare)
|
||||||
|
.add()
|
||||||
|
.append(new KeyedCodec<>("Value", Codec.INTEGER), (condition, value) -> condition.value = value, condition -> condition.value)
|
||||||
|
.add()
|
||||||
|
.build();
|
||||||
|
|
||||||
|
private String playerId;
|
||||||
|
private NumberCompare compare = NumberCompare.GREATER_THAN_OR_EQUAL;
|
||||||
|
private int value;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean test(@Nonnull TriggerContext context) {
|
||||||
|
String id = resolvePlayerId(context, playerId);
|
||||||
|
String game = resolvedMinigameId(context);
|
||||||
|
if (id == null) {
|
||||||
|
debugMessage(context, TYPE_ID, false, "minigame=" + game + " no player resolved");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
var rt = runtime(context);
|
||||||
|
if (rt.isEmpty()) {
|
||||||
|
debugMessage(context, TYPE_ID, false, "minigame=" + game + " player=" + id + " no runtime found");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
var player = rt.get().players().get(id);
|
||||||
|
if (player == null) {
|
||||||
|
debugMessage(context, TYPE_ID, false, "minigame=" + game + " player=" + id + " not in runtime");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
NumberCompare op = compare != null ? compare : NumberCompare.GREATER_THAN_OR_EQUAL;
|
||||||
|
boolean result = op.test(player.lives(), value);
|
||||||
|
debugMessage(context, TYPE_ID, result, "minigame=" + game + " player=" + id + " lives=" + player.lives() + " " + op + " " + value);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
package net.kewwbec.minigames.volume.condition;
|
||||||
|
|
||||||
|
import com.hypixel.hytale.builtin.triggervolumes.effect.TriggerContext;
|
||||||
|
import com.hypixel.hytale.codec.Codec;
|
||||||
|
import com.hypixel.hytale.codec.KeyedCodec;
|
||||||
|
import com.hypixel.hytale.codec.builder.BuilderCodec;
|
||||||
|
import com.hypixel.hytale.codec.codecs.EnumCodec;
|
||||||
|
|
||||||
|
import javax.annotation.Nonnull;
|
||||||
|
|
||||||
|
public final class PlayerScoreCondition extends MinigameRuntimeCondition {
|
||||||
|
public static final String TYPE_ID = "PlayerScore";
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
|
public static final BuilderCodec<PlayerScoreCondition> CODEC =
|
||||||
|
BuilderCodec.builder(PlayerScoreCondition.class, PlayerScoreCondition::new, MINIGAME_BASE_CODEC)
|
||||||
|
.append(new KeyedCodec<>("PlayerId", Codec.STRING, false), (condition, value) -> condition.playerId = value, condition -> condition.playerId)
|
||||||
|
.add()
|
||||||
|
.append(new KeyedCodec<>("Compare", new EnumCodec<>(NumberCompare.class), false), (condition, value) -> condition.compare = value, condition -> condition.compare)
|
||||||
|
.add()
|
||||||
|
.append(new KeyedCodec<>("Value", Codec.INTEGER), (condition, value) -> condition.value = value, condition -> condition.value)
|
||||||
|
.add()
|
||||||
|
.build();
|
||||||
|
|
||||||
|
private String playerId;
|
||||||
|
private NumberCompare compare = NumberCompare.GREATER_THAN_OR_EQUAL;
|
||||||
|
private int value;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean test(@Nonnull TriggerContext context) {
|
||||||
|
String id = resolvePlayerId(context, playerId);
|
||||||
|
String game = resolvedMinigameId(context);
|
||||||
|
if (id == null) {
|
||||||
|
debugMessage(context, TYPE_ID, false, "minigame=" + game + " no player resolved");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
var rt = runtime(context);
|
||||||
|
if (rt.isEmpty()) {
|
||||||
|
debugMessage(context, TYPE_ID, false, "minigame=" + game + " player=" + id + " no runtime found");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
var player = rt.get().players().get(id);
|
||||||
|
if (player == null) {
|
||||||
|
debugMessage(context, TYPE_ID, false, "minigame=" + game + " player=" + id + " not in runtime");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
NumberCompare op = compare != null ? compare : NumberCompare.GREATER_THAN_OR_EQUAL;
|
||||||
|
boolean result = op.test(player.score(), value);
|
||||||
|
debugMessage(context, TYPE_ID, result, "minigame=" + game + " player=" + id + " score=" + player.score() + " " + op + " " + value);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
package net.kewwbec.minigames.volume.condition;
|
||||||
|
|
||||||
|
import com.hypixel.hytale.builtin.triggervolumes.effect.TriggerContext;
|
||||||
|
import com.hypixel.hytale.codec.Codec;
|
||||||
|
import com.hypixel.hytale.codec.KeyedCodec;
|
||||||
|
import com.hypixel.hytale.codec.builder.BuilderCodec;
|
||||||
|
import com.hypixel.hytale.codec.codecs.EnumCodec;
|
||||||
|
import net.kewwbec.minigames.model.Enums.PlayerStatus;
|
||||||
|
|
||||||
|
import javax.annotation.Nonnull;
|
||||||
|
|
||||||
|
public final class PlayerStatusCondition extends MinigameRuntimeCondition {
|
||||||
|
public static final String TYPE_ID = "PlayerStatus";
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
|
public static final BuilderCodec<PlayerStatusCondition> CODEC =
|
||||||
|
BuilderCodec.builder(PlayerStatusCondition.class, PlayerStatusCondition::new, MINIGAME_BASE_CODEC)
|
||||||
|
.append(new KeyedCodec<>("PlayerId", Codec.STRING, false), (condition, value) -> condition.playerId = value, condition -> condition.playerId)
|
||||||
|
.add()
|
||||||
|
.append(new KeyedCodec<>("Status", new EnumCodec<>(PlayerStatus.class)), (condition, value) -> condition.status = value, condition -> condition.status)
|
||||||
|
.add()
|
||||||
|
.build();
|
||||||
|
|
||||||
|
private String playerId;
|
||||||
|
private PlayerStatus status = PlayerStatus.ACTIVE;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean test(@Nonnull TriggerContext context) {
|
||||||
|
String id = resolvePlayerId(context, playerId);
|
||||||
|
String game = resolvedMinigameId(context);
|
||||||
|
if (id == null) {
|
||||||
|
debugMessage(context, TYPE_ID, false, "minigame=" + game + " no player resolved");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
var rt = runtime(context);
|
||||||
|
if (rt.isEmpty()) {
|
||||||
|
debugMessage(context, TYPE_ID, false, "minigame=" + game + " player=" + id + " no runtime found");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
var player = rt.get().players().get(id);
|
||||||
|
if (player == null) {
|
||||||
|
debugMessage(context, TYPE_ID, false, "minigame=" + game + " player=" + id + " not in runtime");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
boolean result = player.status() == status;
|
||||||
|
debugMessage(context, TYPE_ID, result, "minigame=" + game + " player=" + id + " expected=" + status + " actual=" + player.status());
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
package net.kewwbec.minigames.volume.condition;
|
||||||
|
|
||||||
|
import com.hypixel.hytale.builtin.triggervolumes.effect.TriggerContext;
|
||||||
|
import com.hypixel.hytale.codec.Codec;
|
||||||
|
import com.hypixel.hytale.codec.KeyedCodec;
|
||||||
|
import com.hypixel.hytale.codec.builder.BuilderCodec;
|
||||||
|
|
||||||
|
import javax.annotation.Nonnull;
|
||||||
|
|
||||||
|
public final class TeamCondition extends MinigameRuntimeCondition {
|
||||||
|
public static final String TYPE_ID = "TeamCondition";
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
|
public static final BuilderCodec<TeamCondition> CODEC =
|
||||||
|
BuilderCodec.builder(TeamCondition.class, TeamCondition::new, MINIGAME_BASE_CODEC)
|
||||||
|
.append(new KeyedCodec<>("PlayerId", Codec.STRING, false), (condition, value) -> condition.playerId = value, condition -> condition.playerId)
|
||||||
|
.add()
|
||||||
|
.append(new KeyedCodec<>("TeamId", Codec.STRING), (condition, value) -> condition.teamId = value, condition -> condition.teamId)
|
||||||
|
.add()
|
||||||
|
.build();
|
||||||
|
|
||||||
|
private String playerId;
|
||||||
|
private String teamId;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean test(@Nonnull TriggerContext context) {
|
||||||
|
String id = resolvePlayerId(context, playerId);
|
||||||
|
String game = resolvedMinigameId(context);
|
||||||
|
if (id == null) {
|
||||||
|
debugMessage(context, TYPE_ID, false, "minigame=" + game + " no player resolved");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
var rt = runtime(context);
|
||||||
|
if (rt.isEmpty()) {
|
||||||
|
debugMessage(context, TYPE_ID, false, "minigame=" + game + " player=" + id + " no runtime found");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
var player = rt.get().players().get(id);
|
||||||
|
if (player == null) {
|
||||||
|
debugMessage(context, TYPE_ID, false, "minigame=" + game + " player=" + id + " not in runtime");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
boolean result = teamId != null && teamId.equals(player.teamId());
|
||||||
|
debugMessage(context, TYPE_ID, result,
|
||||||
|
"minigame=" + game + " player=" + id + " team=" + player.teamId() + " expected=" + teamId);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
package net.kewwbec.minigames.volume.condition;
|
||||||
|
|
||||||
|
import com.hypixel.hytale.builtin.triggervolumes.effect.TriggerContext;
|
||||||
|
import com.hypixel.hytale.codec.Codec;
|
||||||
|
import com.hypixel.hytale.codec.KeyedCodec;
|
||||||
|
import com.hypixel.hytale.codec.builder.BuilderCodec;
|
||||||
|
import com.hypixel.hytale.codec.codecs.EnumCodec;
|
||||||
|
|
||||||
|
import javax.annotation.Nonnull;
|
||||||
|
|
||||||
|
public final class TeamScoreCondition extends MinigameRuntimeCondition {
|
||||||
|
public static final String TYPE_ID = "TeamScore";
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
|
public static final BuilderCodec<TeamScoreCondition> CODEC =
|
||||||
|
BuilderCodec.builder(TeamScoreCondition.class, TeamScoreCondition::new, MINIGAME_BASE_CODEC)
|
||||||
|
.append(new KeyedCodec<>("TeamId", Codec.STRING), (condition, value) -> condition.teamId = value, condition -> condition.teamId)
|
||||||
|
.add()
|
||||||
|
.append(new KeyedCodec<>("Compare", new EnumCodec<>(NumberCompare.class), false), (condition, value) -> condition.compare = value, condition -> condition.compare)
|
||||||
|
.add()
|
||||||
|
.append(new KeyedCodec<>("Value", Codec.INTEGER), (condition, value) -> condition.value = value, condition -> condition.value)
|
||||||
|
.add()
|
||||||
|
.build();
|
||||||
|
|
||||||
|
private String teamId;
|
||||||
|
private NumberCompare compare = NumberCompare.GREATER_THAN_OR_EQUAL;
|
||||||
|
private int value;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean test(@Nonnull TriggerContext context) {
|
||||||
|
String game = resolvedMinigameId(context);
|
||||||
|
var rt = runtime(context);
|
||||||
|
if (rt.isEmpty()) {
|
||||||
|
debugMessage(context, TYPE_ID, false, "minigame=" + game + " no runtime found");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
var team = rt.get().teams().get(teamId);
|
||||||
|
if (team == null) {
|
||||||
|
debugMessage(context, TYPE_ID, false, "minigame=" + game + " team=" + teamId + " not found");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
NumberCompare op = compare != null ? compare : NumberCompare.GREATER_THAN_OR_EQUAL;
|
||||||
|
boolean result = op.test(team.score(), value);
|
||||||
|
debugMessage(context, TYPE_ID, result, "minigame=" + game + " team=" + teamId + " score=" + team.score() + " " + op + " " + value);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
package net.kewwbec.minigames.volume.condition;
|
||||||
|
|
||||||
|
import com.hypixel.hytale.builtin.triggervolumes.effect.TriggerContext;
|
||||||
|
import com.hypixel.hytale.codec.Codec;
|
||||||
|
import com.hypixel.hytale.codec.KeyedCodec;
|
||||||
|
import com.hypixel.hytale.codec.builder.BuilderCodec;
|
||||||
|
|
||||||
|
import javax.annotation.Nonnull;
|
||||||
|
|
||||||
|
public final class TimerElapsedCondition extends MinigameRuntimeCondition {
|
||||||
|
public static final String TYPE_ID = "TimerElapsed";
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
|
public static final BuilderCodec<TimerElapsedCondition> CODEC =
|
||||||
|
BuilderCodec.builder(TimerElapsedCondition.class, TimerElapsedCondition::new, MINIGAME_BASE_CODEC)
|
||||||
|
.append(new KeyedCodec<>("TimerName", Codec.STRING), (condition, value) -> condition.timerName = value, condition -> condition.timerName)
|
||||||
|
.add()
|
||||||
|
.build();
|
||||||
|
|
||||||
|
private String timerName;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean test(@Nonnull TriggerContext context) {
|
||||||
|
String game = resolvedMinigameId(context);
|
||||||
|
var rt = runtime(context);
|
||||||
|
if (rt.isEmpty()) {
|
||||||
|
debugMessage(context, TYPE_ID, false, "minigame=" + game + " no runtime found");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (timerName == null || timerName.isBlank()) {
|
||||||
|
debugMessage(context, TYPE_ID, false, "minigame=" + game + " no timer name configured");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
Long expiry = rt.get().timers().get(timerName);
|
||||||
|
if (expiry == null) {
|
||||||
|
debugMessage(context, TYPE_ID, false, "minigame=" + game + " timer='" + timerName + "' not started");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
boolean result = System.currentTimeMillis() >= expiry;
|
||||||
|
debugMessage(context, TYPE_ID, result, "minigame=" + game + " timer='" + timerName + "' elapsed=" + result);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
package net.kewwbec.minigames.volume.condition;
|
||||||
|
|
||||||
|
import com.hypixel.hytale.builtin.triggervolumes.effect.TriggerContext;
|
||||||
|
import com.hypixel.hytale.codec.Codec;
|
||||||
|
import com.hypixel.hytale.codec.KeyedCodec;
|
||||||
|
import com.hypixel.hytale.codec.builder.BuilderCodec;
|
||||||
|
import net.kewwbec.minigames.model.Enums.ObjectiveStatus;
|
||||||
|
import net.kewwbec.minigames.model.Enums.PlayerStatus;
|
||||||
|
import net.kewwbec.minigames.model.Enums.WinCondition;
|
||||||
|
|
||||||
|
import javax.annotation.Nonnull;
|
||||||
|
|
||||||
|
public final class WinConditionMetCondition extends MinigameRuntimeCondition {
|
||||||
|
public static final String TYPE_ID = "WinConditionMet";
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
|
public static final BuilderCodec<WinConditionMetCondition> CODEC =
|
||||||
|
BuilderCodec.builder(WinConditionMetCondition.class, WinConditionMetCondition::new, MINIGAME_BASE_CODEC)
|
||||||
|
.append(new KeyedCodec<>("TargetScore", Codec.INTEGER, false), (condition, value) -> condition.targetScore = value, condition -> condition.targetScore)
|
||||||
|
.add()
|
||||||
|
.build();
|
||||||
|
|
||||||
|
private int targetScore = 0;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean test(@Nonnull TriggerContext context) {
|
||||||
|
String game = resolvedMinigameId(context);
|
||||||
|
var rt = runtime(context);
|
||||||
|
if (rt.isEmpty()) {
|
||||||
|
debugMessage(context, TYPE_ID, false, "minigame=" + game + " no runtime found");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
WinCondition winCondition = rt.get().definition().getWinCondition();
|
||||||
|
boolean result = switch (winCondition) {
|
||||||
|
case LAST_ALIVE -> {
|
||||||
|
long alive = rt.get().players().values().stream()
|
||||||
|
.filter(p -> p.status() == PlayerStatus.ACTIVE)
|
||||||
|
.count();
|
||||||
|
yield alive <= 1;
|
||||||
|
}
|
||||||
|
case FIRST_TO_SCORE -> rt.get().players().values().stream()
|
||||||
|
.anyMatch(p -> p.score() >= targetScore);
|
||||||
|
case OBJECTIVE_COMPLETE -> !rt.get().objectives().isEmpty()
|
||||||
|
&& rt.get().objectives().values().stream()
|
||||||
|
.filter(o -> o.status() != ObjectiveStatus.INACTIVE)
|
||||||
|
.allMatch(o -> o.status() == ObjectiveStatus.COMPLETE);
|
||||||
|
default -> false;
|
||||||
|
};
|
||||||
|
|
||||||
|
debugMessage(context, TYPE_ID, result,
|
||||||
|
"minigame=" + game + " winCondition=" + winCondition + " result=" + result);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
package net.kewwbec.minigames.volume.effect;
|
||||||
|
|
||||||
|
import com.hypixel.hytale.builtin.triggervolumes.effect.TriggerContext;
|
||||||
|
import com.hypixel.hytale.codec.Codec;
|
||||||
|
import com.hypixel.hytale.codec.KeyedCodec;
|
||||||
|
import com.hypixel.hytale.codec.builder.BuilderCodec;
|
||||||
|
import net.kewwbec.minigames.model.TeamState;
|
||||||
|
|
||||||
|
import javax.annotation.Nonnull;
|
||||||
|
|
||||||
|
public final class AssignTeamEffect extends MinigameRuntimeEffect {
|
||||||
|
public static final String TYPE_ID = "AssignTeam";
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
|
public static final BuilderCodec<AssignTeamEffect> CODEC =
|
||||||
|
BuilderCodec.builder(AssignTeamEffect.class, AssignTeamEffect::new, MINIGAME_BASE_CODEC)
|
||||||
|
.append(new KeyedCodec<>("PlayerId", Codec.STRING, false), (effect, value) -> effect.playerId = value, effect -> effect.playerId)
|
||||||
|
.add()
|
||||||
|
.append(new KeyedCodec<>("TeamId", Codec.STRING), (effect, value) -> effect.teamId = value, effect -> effect.teamId)
|
||||||
|
.add()
|
||||||
|
.build();
|
||||||
|
|
||||||
|
private String playerId;
|
||||||
|
private String teamId;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void execute(@Nonnull TriggerContext context) {
|
||||||
|
String id = resolvePlayerId(context, playerId);
|
||||||
|
if (id == null) {
|
||||||
|
debugMessage(context, TYPE_ID, "SKIPPED (no player resolved)");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (teamId == null || teamId.isBlank()) {
|
||||||
|
debugMessage(context, TYPE_ID, "SKIPPED (no team id configured)");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var rt = runtime(context);
|
||||||
|
if (rt.isEmpty()) {
|
||||||
|
debugMessage(context, TYPE_ID, "SKIPPED (no runtime for minigame='" + resolvedMinigameId(context) + "')");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var player = rt.get().players().get(id);
|
||||||
|
if (player == null) {
|
||||||
|
debugMessage(context, TYPE_ID, "SKIPPED (player='" + id + "' not in runtime)");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove from old team
|
||||||
|
String prevTeam = player.teamId();
|
||||||
|
if (prevTeam != null && !prevTeam.isBlank()) {
|
||||||
|
var old = rt.get().teams().get(prevTeam);
|
||||||
|
if (old != null) old.players().remove(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add to new team, auto-creating if needed
|
||||||
|
rt.get().teams().computeIfAbsent(teamId, k -> new TeamState(k, k, null)).players().add(id);
|
||||||
|
player.teamId(teamId);
|
||||||
|
|
||||||
|
debugMessage(context, TYPE_ID, "FIRED player='" + id + "' team=" + prevTeam + "->" + teamId);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
package net.kewwbec.minigames.volume.effect;
|
||||||
|
|
||||||
|
import com.hypixel.hytale.builtin.triggervolumes.effect.TriggerContext;
|
||||||
|
import com.hypixel.hytale.codec.Codec;
|
||||||
|
import com.hypixel.hytale.codec.KeyedCodec;
|
||||||
|
import com.hypixel.hytale.codec.builder.BuilderCodec;
|
||||||
|
import com.hypixel.hytale.codec.codecs.EnumCodec;
|
||||||
|
|
||||||
|
import javax.annotation.Nonnull;
|
||||||
|
|
||||||
|
public final class AwardKillScoreEffect extends MinigameRuntimeEffect {
|
||||||
|
public static final String TYPE_ID = "AwardKillScore";
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
|
public static final BuilderCodec<AwardKillScoreEffect> CODEC =
|
||||||
|
BuilderCodec.builder(AwardKillScoreEffect.class, AwardKillScoreEffect::new, MINIGAME_BASE_CODEC)
|
||||||
|
.append(new KeyedCodec<>("TargetIds", Codec.STRING_ARRAY, false), (effect, value) -> effect.targetIds = value, effect -> effect.targetIds)
|
||||||
|
.add()
|
||||||
|
.append(new KeyedCodec<>("Points", Codec.INTEGER, false), (effect, value) -> effect.points = value, effect -> effect.points)
|
||||||
|
.add()
|
||||||
|
.append(new KeyedCodec<>("AwardTarget", new EnumCodec<>(AwardTarget.class), false), (effect, value) -> effect.awardTarget = value, effect -> effect.awardTarget)
|
||||||
|
.add()
|
||||||
|
.build();
|
||||||
|
|
||||||
|
private String[] targetIds = new String[] {"Player"};
|
||||||
|
private int points = 1;
|
||||||
|
private AwardTarget awardTarget = AwardTarget.PLAYER;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void execute(@Nonnull TriggerContext context) {
|
||||||
|
debugMessage(context, TYPE_ID, "CONFIG_ONLY kill scoring is applied by the minigame death system");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
|
public String configuredMinigameId() {
|
||||||
|
return minigameId != null ? minigameId : "";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
|
public String[] targetIds() {
|
||||||
|
return targetIds != null ? targetIds : new String[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
public int points() {
|
||||||
|
return points;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
|
public AwardTarget awardTarget() {
|
||||||
|
return awardTarget != null ? awardTarget : AwardTarget.PLAYER;
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum AwardTarget {
|
||||||
|
PLAYER,
|
||||||
|
TEAM,
|
||||||
|
BOTH
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
package net.kewwbec.minigames.volume.effect;
|
||||||
|
|
||||||
|
import com.hypixel.hytale.builtin.triggervolumes.effect.TriggerContext;
|
||||||
|
import com.hypixel.hytale.codec.Codec;
|
||||||
|
import com.hypixel.hytale.codec.KeyedCodec;
|
||||||
|
import com.hypixel.hytale.codec.builder.BuilderCodec;
|
||||||
|
|
||||||
|
import javax.annotation.Nonnull;
|
||||||
|
|
||||||
|
public final class ClearInventoryEffect extends MinigameRuntimeEffect {
|
||||||
|
public static final String TYPE_ID = "ClearInventory";
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
|
public static final BuilderCodec<ClearInventoryEffect> CODEC =
|
||||||
|
BuilderCodec.builder(ClearInventoryEffect.class, ClearInventoryEffect::new, MINIGAME_BASE_CODEC)
|
||||||
|
.append(new KeyedCodec<>("PlayerId", Codec.STRING, false), (effect, value) -> effect.playerId = value, effect -> effect.playerId)
|
||||||
|
.add()
|
||||||
|
.build();
|
||||||
|
|
||||||
|
private String playerId;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void execute(@Nonnull TriggerContext context) {
|
||||||
|
String id = resolvePlayerId(context, playerId);
|
||||||
|
if (id == null) {
|
||||||
|
debugMessage(context, TYPE_ID, "SKIPPED (no player resolved)");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var services = services();
|
||||||
|
if (services == null) {
|
||||||
|
debugMessage(context, TYPE_ID, "SKIPPED (services unavailable)");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
services.adapters().clearInventory(id);
|
||||||
|
debugMessage(context, TYPE_ID, "FIRED player='" + id + "'");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
package net.kewwbec.minigames.volume.effect;
|
||||||
|
|
||||||
|
import com.hypixel.hytale.builtin.triggervolumes.effect.TriggerContext;
|
||||||
|
import com.hypixel.hytale.codec.Codec;
|
||||||
|
import com.hypixel.hytale.codec.KeyedCodec;
|
||||||
|
import com.hypixel.hytale.codec.builder.BuilderCodec;
|
||||||
|
|
||||||
|
import javax.annotation.Nonnull;
|
||||||
|
|
||||||
|
public final class EndMinigameEffect extends MinigameRuntimeEffect {
|
||||||
|
public static final String TYPE_ID = "EndMinigame";
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
|
public static final BuilderCodec<EndMinigameEffect> CODEC =
|
||||||
|
BuilderCodec.builder(EndMinigameEffect.class, EndMinigameEffect::new, MINIGAME_BASE_CODEC)
|
||||||
|
.append(new KeyedCodec<>("Reason", Codec.STRING, false), (effect, value) -> effect.reason = value, effect -> effect.reason)
|
||||||
|
.add()
|
||||||
|
.build();
|
||||||
|
|
||||||
|
private String reason = "minigames.runtime.reason.trigger_volume";
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void execute(@Nonnull TriggerContext context) {
|
||||||
|
var services = services();
|
||||||
|
if (services == null) {
|
||||||
|
debugMessage(context, TYPE_ID, "SKIPPED (services null)");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var rt = runtime(context);
|
||||||
|
if (rt.isPresent()) {
|
||||||
|
String stopReason = reason != null ? reason : "minigames.runtime.reason.trigger_volume";
|
||||||
|
services.runtime().end(rt.get().runtimeId(), stopReason);
|
||||||
|
debugMessage(context, TYPE_ID, "FIRED reason='" + stopReason + "'");
|
||||||
|
} else {
|
||||||
|
debugMessage(context, TYPE_ID, "SKIPPED (no runtime for minigame='" + resolvedMinigameId(context) + "')");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
package net.kewwbec.minigames.volume.effect;
|
||||||
|
|
||||||
|
import com.hypixel.hytale.builtin.triggervolumes.effect.TriggerContext;
|
||||||
|
import com.hypixel.hytale.codec.Codec;
|
||||||
|
import com.hypixel.hytale.codec.KeyedCodec;
|
||||||
|
import com.hypixel.hytale.codec.builder.BuilderCodec;
|
||||||
|
|
||||||
|
import javax.annotation.Nonnull;
|
||||||
|
|
||||||
|
public final class JoinMinigameQueueEffect extends MinigameRuntimeEffect {
|
||||||
|
public static final String TYPE_ID = "JoinMinigameQueue";
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
|
public static final BuilderCodec<JoinMinigameQueueEffect> CODEC =
|
||||||
|
BuilderCodec.builder(JoinMinigameQueueEffect.class, JoinMinigameQueueEffect::new, MINIGAME_BASE_CODEC)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void execute(@Nonnull TriggerContext context) {
|
||||||
|
var services = services();
|
||||||
|
String playerId = resolvePlayerId(context, null);
|
||||||
|
String id = resolvedMinigameId(context);
|
||||||
|
if (services != null && playerId != null && !id.isBlank()) {
|
||||||
|
services.queue().join(id, playerId);
|
||||||
|
debugMessage(context, TYPE_ID, "FIRED player='" + playerId + "' joined queue for minigame='" + id + "'");
|
||||||
|
} else {
|
||||||
|
debugMessage(context, TYPE_ID, "SKIPPED (services=" + services + " player='" + playerId + "' id='" + id + "')");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
package net.kewwbec.minigames.volume.effect;
|
||||||
|
|
||||||
|
import com.hypixel.hytale.builtin.triggervolumes.effect.TriggerContext;
|
||||||
|
import com.hypixel.hytale.codec.builder.BuilderCodec;
|
||||||
|
|
||||||
|
import javax.annotation.Nonnull;
|
||||||
|
|
||||||
|
public final class LeaveMinigameQueueEffect extends MinigameRuntimeEffect {
|
||||||
|
public static final String TYPE_ID = "LeaveMinigameQueue";
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
|
public static final BuilderCodec<LeaveMinigameQueueEffect> CODEC =
|
||||||
|
BuilderCodec.builder(LeaveMinigameQueueEffect.class, LeaveMinigameQueueEffect::new, MINIGAME_BASE_CODEC)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void execute(@Nonnull TriggerContext context) {
|
||||||
|
var services = services();
|
||||||
|
String playerId = resolvePlayerId(context, null);
|
||||||
|
String id = resolvedMinigameId(context);
|
||||||
|
if (services != null && playerId != null && !id.isBlank()) {
|
||||||
|
services.queue().leave(id, playerId);
|
||||||
|
debugMessage(context, TYPE_ID, "FIRED player='" + playerId + "' left queue for minigame='" + id + "'");
|
||||||
|
} else {
|
||||||
|
debugMessage(context, TYPE_ID, "SKIPPED (services=" + services + " player='" + playerId + "' id='" + id + "')");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,121 @@
|
|||||||
|
package net.kewwbec.minigames.volume.effect;
|
||||||
|
|
||||||
|
import com.hypixel.hytale.builtin.triggervolumes.effect.TriggerContext;
|
||||||
|
import com.hypixel.hytale.builtin.triggervolumes.effect.TriggerEffect;
|
||||||
|
import com.hypixel.hytale.codec.Codec;
|
||||||
|
import com.hypixel.hytale.codec.KeyedCodec;
|
||||||
|
import com.hypixel.hytale.codec.builder.BuilderCodec;
|
||||||
|
import com.hypixel.hytale.server.core.Message;
|
||||||
|
import com.hypixel.hytale.server.core.entity.UUIDComponent;
|
||||||
|
import com.hypixel.hytale.server.core.universe.PlayerRef;
|
||||||
|
import net.kewwbec.minigames.MinigameCorePlugin;
|
||||||
|
import net.kewwbec.minigames.model.MinigameDefinition;
|
||||||
|
import net.kewwbec.minigames.runtime.MinigameRuntime;
|
||||||
|
import net.kewwbec.minigames.service.MinigameServices;
|
||||||
|
|
||||||
|
import javax.annotation.Nonnull;
|
||||||
|
import javax.annotation.Nullable;
|
||||||
|
import java.awt.Color;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
public abstract class MinigameRuntimeEffect extends TriggerEffect {
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
|
public static final BuilderCodec<MinigameRuntimeEffect> MINIGAME_BASE_CODEC =
|
||||||
|
BuilderCodec.abstractBuilder(MinigameRuntimeEffect.class, BASE_CODEC)
|
||||||
|
.<String>append(
|
||||||
|
new KeyedCodec<>("MinigameId", Codec.STRING, false),
|
||||||
|
(effect, value) -> effect.minigameId = value,
|
||||||
|
effect -> effect.minigameId
|
||||||
|
)
|
||||||
|
.add()
|
||||||
|
.build();
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
protected String minigameId;
|
||||||
|
|
||||||
|
private volatile long lastDebugMs = 0L;
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
|
protected Optional<MinigameRuntime> runtime(@Nonnull TriggerContext context) {
|
||||||
|
MinigameServices services = MinigameCorePlugin.getServices();
|
||||||
|
if (services == null) {
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
String playerId = resolvePlayerId(context, null);
|
||||||
|
if (playerId != null) {
|
||||||
|
Optional<MinigameRuntime> playerRuntime = services.runtime().runtimeForPlayer(playerId);
|
||||||
|
if (playerRuntime.isPresent()) {
|
||||||
|
return playerRuntime;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var candidate = services.maps().candidateFromVolume(context, minigameId != null ? minigameId : "");
|
||||||
|
String resolvedMinigameId = !candidate.minigameId().isBlank() ? candidate.minigameId() : minigameId;
|
||||||
|
if (resolvedMinigameId == null || resolvedMinigameId.isBlank()) {
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
return services.runtime().runtimeForArena(resolvedMinigameId, candidate.mapId(), candidate.variantId())
|
||||||
|
.or(() -> services.runtime().runtime(resolvedMinigameId));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
protected static MinigameServices services() {
|
||||||
|
return MinigameCorePlugin.getServices();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
|
protected String resolvedMinigameId(@Nonnull TriggerContext context) {
|
||||||
|
var services = services();
|
||||||
|
if (services != null) {
|
||||||
|
var candidate = services.maps().candidateFromVolume(context, minigameId != null ? minigameId : "");
|
||||||
|
if (!candidate.minigameId().isBlank()) {
|
||||||
|
return candidate.minigameId();
|
||||||
|
}
|
||||||
|
if (minigameId == null || minigameId.isBlank()) {
|
||||||
|
String playerId = resolvePlayerId(context, null);
|
||||||
|
if (playerId != null) {
|
||||||
|
var playerRuntime = services.runtime().runtimeForPlayer(playerId);
|
||||||
|
if (playerRuntime.isPresent()) {
|
||||||
|
return playerRuntime.get().minigameId();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return minigameId != null ? minigameId : "";
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void debugMessage(@Nonnull TriggerContext context, @Nonnull String effectType, @Nonnull String details) {
|
||||||
|
String id = resolvedMinigameId(context);
|
||||||
|
if (id.isBlank()) return;
|
||||||
|
var def = MinigameDefinition.getAssetMap().getAsset(id);
|
||||||
|
if (def == null || !def.isDebug()) return;
|
||||||
|
long now = System.currentTimeMillis();
|
||||||
|
if (now - lastDebugMs < 5000L) return;
|
||||||
|
lastDebugMs = now;
|
||||||
|
PlayerRef playerRef = context.getStore().getComponent(context.getEntityRef(), PlayerRef.getComponentType());
|
||||||
|
if (playerRef != null) {
|
||||||
|
playerRef.sendMessage(
|
||||||
|
Message.translation("minigames.debug.effect")
|
||||||
|
.param("type", effectType)
|
||||||
|
.param("details", details)
|
||||||
|
.color(Color.CYAN)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
protected static String resolvePlayerId(@Nonnull TriggerContext context, @Nullable String configuredPlayerId) {
|
||||||
|
if (configuredPlayerId != null && !configuredPlayerId.isBlank()) {
|
||||||
|
return configuredPlayerId;
|
||||||
|
}
|
||||||
|
|
||||||
|
PlayerRef playerRef = context.getStore().getComponent(context.getEntityRef(), PlayerRef.getComponentType());
|
||||||
|
if (playerRef != null) {
|
||||||
|
return playerRef.getUuid().toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
UUIDComponent uuidComponent = context.getStore().getComponent(context.getEntityRef(), UUIDComponent.getComponentType());
|
||||||
|
return uuidComponent != null ? uuidComponent.getUuid().toString() : null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
package net.kewwbec.minigames.volume.effect;
|
||||||
|
|
||||||
|
import com.hypixel.hytale.builtin.triggervolumes.effect.TriggerContext;
|
||||||
|
import com.hypixel.hytale.codec.Codec;
|
||||||
|
import com.hypixel.hytale.codec.KeyedCodec;
|
||||||
|
import com.hypixel.hytale.codec.builder.BuilderCodec;
|
||||||
|
import com.hypixel.hytale.codec.codecs.EnumCodec;
|
||||||
|
|
||||||
|
import javax.annotation.Nonnull;
|
||||||
|
|
||||||
|
public final class ModifyCounterEffect extends MinigameRuntimeEffect {
|
||||||
|
public static final String TYPE_ID = "ModifyCounter";
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
|
public static final BuilderCodec<ModifyCounterEffect> CODEC =
|
||||||
|
BuilderCodec.builder(ModifyCounterEffect.class, ModifyCounterEffect::new, MINIGAME_BASE_CODEC)
|
||||||
|
.append(new KeyedCodec<>("CounterId", Codec.STRING), (effect, value) -> effect.counterId = value, effect -> effect.counterId)
|
||||||
|
.add()
|
||||||
|
.append(new KeyedCodec<>("Operation", new EnumCodec<>(ModifyPlayerScoreEffect.Operation.class), false), (effect, value) -> effect.operation = value, effect -> effect.operation)
|
||||||
|
.add()
|
||||||
|
.append(new KeyedCodec<>("Amount", Codec.INTEGER, false), (effect, value) -> effect.amount = value, effect -> effect.amount)
|
||||||
|
.add()
|
||||||
|
.build();
|
||||||
|
|
||||||
|
private String counterId;
|
||||||
|
private ModifyPlayerScoreEffect.Operation operation = ModifyPlayerScoreEffect.Operation.ADD;
|
||||||
|
private int amount = 1;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void execute(@Nonnull TriggerContext context) {
|
||||||
|
if (counterId == null || counterId.isBlank()) {
|
||||||
|
debugMessage(context, TYPE_ID, "SKIPPED (counterId blank)");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var rt = runtime(context);
|
||||||
|
if (rt.isPresent()) {
|
||||||
|
String key = "counter:" + counterId;
|
||||||
|
int current = number(rt.get().variables().get(key));
|
||||||
|
int next = ModifyPlayerScoreEffect.apply(current, operation, amount);
|
||||||
|
rt.get().variables().put(key, next);
|
||||||
|
debugMessage(context, TYPE_ID, "FIRED counter='" + counterId + "' " + current + "->" + next);
|
||||||
|
} else {
|
||||||
|
debugMessage(context, TYPE_ID, "SKIPPED (no runtime for minigame='" + resolvedMinigameId(context) + "')");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int number(Object value) {
|
||||||
|
if (value instanceof Number number) {
|
||||||
|
return number.intValue();
|
||||||
|
}
|
||||||
|
if (value == null) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return Integer.parseInt(String.valueOf(value));
|
||||||
|
} catch (NumberFormatException ignored) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
package net.kewwbec.minigames.volume.effect;
|
||||||
|
|
||||||
|
import com.hypixel.hytale.builtin.triggervolumes.effect.TriggerContext;
|
||||||
|
import com.hypixel.hytale.codec.Codec;
|
||||||
|
import com.hypixel.hytale.codec.KeyedCodec;
|
||||||
|
import com.hypixel.hytale.codec.builder.BuilderCodec;
|
||||||
|
import com.hypixel.hytale.codec.codecs.EnumCodec;
|
||||||
|
import net.kewwbec.minigames.model.Enums.ObjectiveStatus;
|
||||||
|
import net.kewwbec.minigames.model.ObjectiveState;
|
||||||
|
|
||||||
|
import javax.annotation.Nonnull;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
public final class ModifyObjectiveProgressEffect extends MinigameRuntimeEffect {
|
||||||
|
public static final String TYPE_ID = "ModifyObjectiveProgress";
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
|
public static final BuilderCodec<ModifyObjectiveProgressEffect> CODEC =
|
||||||
|
BuilderCodec.builder(ModifyObjectiveProgressEffect.class, ModifyObjectiveProgressEffect::new, MINIGAME_BASE_CODEC)
|
||||||
|
.append(new KeyedCodec<>("ObjectiveId", Codec.STRING), (effect, value) -> effect.objectiveId = value, effect -> effect.objectiveId)
|
||||||
|
.add()
|
||||||
|
.append(new KeyedCodec<>("Operation", new EnumCodec<>(ModifyPlayerScoreEffect.Operation.class), false), (effect, value) -> effect.operation = value, effect -> effect.operation)
|
||||||
|
.add()
|
||||||
|
.append(new KeyedCodec<>("Amount", Codec.INTEGER), (effect, value) -> effect.amount = value, effect -> effect.amount)
|
||||||
|
.add()
|
||||||
|
.build();
|
||||||
|
|
||||||
|
private String objectiveId;
|
||||||
|
private ModifyPlayerScoreEffect.Operation operation = ModifyPlayerScoreEffect.Operation.ADD;
|
||||||
|
private int amount = 1;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void execute(@Nonnull TriggerContext context) {
|
||||||
|
if (objectiveId == null || objectiveId.isBlank()) {
|
||||||
|
debugMessage(context, TYPE_ID, "SKIPPED (no objective id configured)");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var rt = runtime(context);
|
||||||
|
if (rt.isEmpty()) {
|
||||||
|
debugMessage(context, TYPE_ID, "SKIPPED (no runtime for minigame='" + resolvedMinigameId(context) + "')");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var objective = rt.get().objectives().computeIfAbsent(objectiveId,
|
||||||
|
k -> new ObjectiveState(k, k, "generic"));
|
||||||
|
|
||||||
|
int prev = objective.progress();
|
||||||
|
int next = Math.max(0, ModifyPlayerScoreEffect.apply(prev, operation, amount));
|
||||||
|
objective.progress(next);
|
||||||
|
|
||||||
|
if (next >= objective.requiredProgress() && objective.status() == ObjectiveStatus.ACTIVE) {
|
||||||
|
objective.status(ObjectiveStatus.COMPLETE);
|
||||||
|
var services = services();
|
||||||
|
if (services != null) {
|
||||||
|
services.runtime().dispatch(rt.get(), "on_objective_complete",
|
||||||
|
Map.of("objective_id", objectiveId, "progress", next));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
debugMessage(context, TYPE_ID, "FIRED objective='" + objectiveId + "' progress=" + prev + "->" + next
|
||||||
|
+ "/" + objective.requiredProgress());
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user