First commit

This commit is contained in:
2026-06-07 14:34:30 -07:00
commit 8eb6e563c5
130 changed files with 8412 additions and 0 deletions
+63
View File
@@ -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/
+155
View File
@@ -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
View File
@@ -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.")
}
}
}
+139
View File
@@ -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.
+100
View File
@@ -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.
+138
View File
@@ -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.
+101
View File
@@ -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`.
+147
View File
@@ -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
View File
@@ -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
View File
@@ -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.
+37
View File
@@ -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
Binary file not shown.
+6
View File
@@ -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
Vendored
+234
View File
@@ -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
View File
@@ -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
View File
+1
View File
@@ -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));
}
}
@@ -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