Bunch of config stuff + logging basics

master
kalle 2021-11-07 16:31:24 +01:00
parent f05208f1a0
commit cbf1e64db9
22 changed files with 372 additions and 110 deletions

View File

@ -11,22 +11,6 @@ class DArena : JavaPlugin() {
private lateinit var pointsManager: PointsManager
private lateinit var teamManager: TeamManager
// TODO:
// Thinking:
// - Datastructures for arenas
// - Datastructures for teams (Everyone is always in a team even when solo)
// - How will points work for teams/individuals
// - Teams are always the same during one event
// - Teams on the leaderboard
// - Rotate teams each new game as an option
// Programing:
// - Loading arenas from config files
// - Commands to interact with the plugin
// - Custom world creation for arenas
// - Basic arena mechanics
// - Points system
// - Advanced arena mechanics (Special items like fireballs)
override fun onEnable() {
configManager = ConfigManager(File(dataFolder, "config.yml"))
configManager.load()

View File

@ -11,7 +11,7 @@ class Arena(
private val arenaUtil: ArenaUtil,
private val plugin: JavaPlugin,
private val world: ArenaWorld = ArenaWorld(config.name, plugin),
) {
) {
private lateinit var session: ArenaSession
// Simple stuff done: 0.001474s
// createArena start: 0.0001026s
@ -33,19 +33,25 @@ class Arena(
session.spectators.addAll(arenaUtil.getPossibleSpectators());
// Reset the world
world.reset()
// Place all spectators in the arena
session.spectators.forEach {
// config.spectatorSpawn.spawn(world, it)
}
// Randomize spawns
session.spawns.addAll(config.spawns)
session.spawns.shuffle()
session.initialSpawns.addAll(config.spawns.filter {
//TODO: make this filter only spawns that meet the intitial spawn condidtions. Or just replace this whole method.
return@filter true
}.keys)
session.initialSpawns.shuffle()
// Spawn all the players
session.participants.forEachIndexed { index, player ->
// TODO: Freeze players in place (for in arena countdown) (if countdown is 0 dont freeze them)
session.spawns[index % session.spawns.size].spawn(world, player)
config.spawns[session.initialSpawns[index % session.initialSpawns.size]]!!.spawn(world, player)
}
// TODO:
}

View File

@ -1,44 +1,73 @@
package nl.kallestruik.darena.arenas
import nl.kallestruik.darena.types.arena.ArenaLoadout
import nl.kallestruik.darena.types.arena.ArenaSpawn
import nl.kallestruik.darena.types.arena.ArenaCheckpoint
import nl.kallestruik.darena.types.arena.ArenaPoints
import nl.kallestruik.darena.types.arena.ArenaSpawn
import nl.kallestruik.darena.types.arena.ArenaSpawnRule
import nl.kallestruik.darena.util.ConfigHelper
import java.io.File
import org.bukkit.configuration.InvalidConfigurationException
import org.bukkit.configuration.file.YamlConfiguration
import kotlin.collections.mutableMapOf
data class ArenaConfig(
var name: String = "[Missing name]",
var spawns: List<ArenaSpawn> = emptyList(),
var loadouts: List<ArenaLoadout> = emptyList(),
var checkpoints: List<ArenaCheckpoint> = emptyList(),
var name: String,
var file: File,
var spawns: MutableMap<String, ArenaSpawn> = mutableMapOf(),
var loadouts: MutableMap<String, ArenaLoadout> = mutableMapOf(),
var checkpoints: MutableMap<String, ArenaCheckpoint> = mutableMapOf(),
var points: ArenaPoints = ArenaPoints(),
var spawnRules: MutableMap<String, ArenaSpawnRule> = mutableMapOf()
) {
fun save(toString: Boolean = false): String? {
val config = YamlConfiguration()
config.set("name", name)
ConfigHelper.saveMap(ConfigHelper.getOrCreateSection(config, "spawns"), spawns)
ConfigHelper.saveMap(ConfigHelper.getOrCreateSection(config, "loadouts"), loadouts)
ConfigHelper.saveMap(ConfigHelper.getOrCreateSection(config, "checkpoints"), checkpoints)
points.save(ConfigHelper.getOrCreateSection(config, "points"))
ConfigHelper.saveMap(ConfigHelper.getOrCreateSection(config, "spawnRules"), spawnRules)
if (toString) {
return config.saveToString()
}
config.save(file)
return null
}
companion object {
@Throws(InvalidConfigurationException::class)
fun load(file: File): ArenaConfig {
val config = ConfigHelper.getOrCreateConfig(file, "template/arena.yml")
val arenaConfig = ArenaConfig()
if (config.contains("name")) {
arenaConfig.name = config.getString("name")!!
if (!config.contains("name")) {
throw InvalidConfigurationException("The arena configuration file '${file.name}' does not contain the required attribute 'name'")
}
val arenaConfig = ArenaConfig(config.getString("name")!!, file)
if (config.contains("spawns")) {
arenaConfig.spawns = ArenaSpawn.loadList(config.getConfigurationSection("spawns")!!)
arenaConfig.spawns = ConfigHelper.loadMap(config.getConfigurationSection("spawns")!!, ArenaSpawn)
}
if (config.contains("loadouts")) {
arenaConfig.loadouts = ArenaLoadout.loadList(config.getConfigurationSection("loadouts")!!)
arenaConfig.loadouts = ConfigHelper.loadMap(config.getConfigurationSection("loadouts")!!, ArenaLoadout)
}
if (config.contains("checkpoints")) {
arenaConfig.checkpoints = ArenaCheckpoint.loadList(config.getConfigurationSection("checkpoints")!!)
arenaConfig.checkpoints = ConfigHelper.loadMap(config.getConfigurationSection("checkpoints")!!, ArenaCheckpoint)
}
if (config.contains("points")) {
arenaConfig.points = ArenaPoints.load(config.getConfigurationSection("points")!!)
}
if (config.contains("spawnRules")) {
arenaConfig.spawnRules = ConfigHelper.loadMap(config.getConfigurationSection("spawnRules")!!, ArenaSpawnRule)
}
return arenaConfig
}
}

View File

@ -2,11 +2,12 @@ package nl.kallestruik.darena.arenas
import nl.kallestruik.darena.types.arena.ArenaSpawn
import org.bukkit.entity.Player
import java.util.UUID
data class ArenaSession(
val participants: MutableList<Player> = ArrayList(),
val spectators: MutableList<Player> = ArrayList(),
val completedObjective: MutableList<Player> = ArrayList(),
val deaths: MutableList<Player> = ArrayList(),
val spawns: MutableList<ArenaSpawn> = ArrayList(),
)
val completedObjective: MutableList<UUID> = ArrayList(),
val deaths: MutableList<UUID> = ArrayList(),
val initialSpawns: MutableList<String> = ArrayList(),
)

View File

@ -19,9 +19,9 @@ class ArenaManager(
arenas.add(Arena(
ArenaConfig.load(path.toFile()),
arenaUtil,
plugin
))
plugin)
)
}
}
}
}
}

View File

@ -0,0 +1,4 @@
package nl.kallestruik.darena.types
interface ConfigListLoadable<T> {
fun loadFromList(key: String, value: Any): T

View File

@ -0,0 +1,7 @@
package nl.kallestruik.darena.types
interface ConfigListSaveable {
fun getKey(): String
fun getValue(): Any
}

View File

@ -0,0 +1,6 @@
package nl.kallestruik.darena.types
import org.bukkit.configuration.ConfigurationSection
interface ConfigLoadable<T> {
fun load(section: ConfigurationSection): T

View File

@ -0,0 +1,7 @@
package nl.kallestruik.darena.types
import org.bukkit.configuration.ConfigurationSection
interface ConfigSaveable {
fun save(section: ConfigurationSection)
}

View File

@ -2,24 +2,28 @@ package nl.kallestruik.darena.types.arena
import org.bukkit.configuration.ConfigurationSection
import javax.naming.ConfigurationException
import nl.kallestruik.darena.types.ConfigSaveable
import nl.kallestruik.darena.types.ConfigLoadable
import nl.kallestruik.darena.util.ConfigHelper
import nl.kallestruik.darena.util.Logger
data class ArenaCheckpoint(
val label: String,
val lower: ArenaLocation,
val upper: ArenaLocation,
val spawn: String,
) {
companion object {
fun loadList(section: ConfigurationSection): List<ArenaCheckpoint> {
val list = mutableListOf<ArenaCheckpoint>()
for (key in section.getKeys(false)) {
list.add(load(section.getConfigurationSection(key)!!))
}
): ConfigSaveable {
override fun save(section: ConfigurationSection) {
Logger.trace(ArenaCheckpoint::class, "save(section: ${section.currentPath})")
section.set("label", label)
lower.save(ConfigHelper.getOrCreateSection(section, "lower"))
upper.save(ConfigHelper.getOrCreateSection(section, "upper"))
section.set("spawn", spawn)
}
return list
}
fun load(section: ConfigurationSection): ArenaCheckpoint {
companion object: ConfigLoadable<ArenaCheckpoint> {
override fun load(section: ConfigurationSection): ArenaCheckpoint {
Logger.trace(ArenaCheckpoint::class, "load(section: ${section.currentPath})")
return ArenaCheckpoint(
section.name,
ArenaLocation.load(section.getConfigurationSection("lower")!!),

View File

@ -2,25 +2,30 @@ package nl.kallestruik.darena.types.arena
import org.bukkit.configuration.ConfigurationSection
import org.bukkit.configuration.InvalidConfigurationException
import nl.kallestruik.darena.types.ConfigListSaveable
import nl.kallestruik.darena.types.ConfigListLoadable
import nl.kallestruik.darena.util.Logger
data class ArenaEnchantment(
val name: String,
val level: Int,
) {
companion object {
fun loadList(section: ConfigurationSection): List<ArenaEnchantment> {
val list = mutableListOf<ArenaEnchantment>()
for (key in section.getKeys(false)) {
list.add(load(section.getConfigurationSection(key)!!))
}
): ConfigListSaveable {
override fun getKey(): String {
Logger.trace(ArenaEnchantment::class, "getKey()")
return name
}
return list
}
override fun getValue(): Int {
Logger.trace(ArenaEnchantment::class, "getValue()")
return level
}
fun load(section: ConfigurationSection): ArenaEnchantment {
companion object: ConfigListLoadable<ArenaEnchantment> {
override fun loadFromList(key: String, value: Any): ArenaEnchantment {
Logger.trace(ArenaEnchantment::class, "loadFromList(key: $key, value: $value)")
return ArenaEnchantment(
section.getString("name") ?: throw InvalidConfigurationException("Could not find required field name in section '${section.currentPath}'"),
section.getInt("level", 1)
key,
value as Int
)
}
}

View File

@ -1,6 +1,8 @@
package nl.kallestruik.darena.types.arena
import nl.kallestruik.darena.util.ConfigHelper
import nl.kallestruik.darena.types.ConfigSaveable
import nl.kallestruik.darena.types.ConfigLoadable
import org.bukkit.Material
import org.bukkit.configuration.ConfigurationSection
@ -9,18 +11,16 @@ data class ArenaItem(
val amount: Int = 1,
val enchantments: List<ArenaEnchantment> = listOf(),
val unbreakable: Boolean = false,
) {
companion object {
fun loadList(section: ConfigurationSection): List<ArenaItem> {
val list = mutableListOf<ArenaItem>()
for (key in section.getKeys(false)) {
list.add(load(section.getConfigurationSection(key)!!))
}
): ConfigSaveable {
override fun save(section: ConfigurationSection) {
section.set("material", material.toString())
section.set("amount", amount)
ConfigHelper.saveList(ConfigHelper.getOrCreateSection(section, "enchantments"), enchantments, ArenaEnchantment)
section.set("unbreakable", unbreakable)
}
return list
}
fun load(section: ConfigurationSection): ArenaItem {
companion object: ConfigLoadable<ArenaItem> {
override fun load(section: ConfigurationSection): ArenaItem {
val enchantments = section.getConfigurationSection("enchantments")?.let {
ArenaEnchantment.loadList(it)
} ?: listOf()

View File

@ -1,24 +1,25 @@
package nl.kallestruik.darena.types.arena
import org.bukkit.configuration.ConfigurationSection
import nl.kallestruik.darena.util.ConfigHelper
import nl.kallestruik.darena.types.ConfigSaveable
import nl.kallestruik.darena.types.ConfigLoadable
data class ArenaLoadout(
val name: String,
val hotbar: List<ArenaItem>,
val armor: List<ArenaItem>,
val offhand: ArenaItem,
) {
companion object {
fun loadList(section: ConfigurationSection): List<ArenaLoadout> {
val list = mutableListOf<ArenaLoadout>()
for (key in section.getKeys(false)) {
list.add(load(section.getConfigurationSection(key)!!))
}
): ConfigSaveable {
override fun save(section: ConfigurationSection) {
ConfigHelper.saveList(ConfigHelper.getOrCreateSection(section, "hotbar"), hotbar)
ConfigHelper.saveList(ConfigHelper.getOrCreateSection(section, "armor"), armor)
offhand.save(ConfigHelper.getOrCreateSection(section, "offhand"))
}
return list
}
companion object : ConfigLoadable<ArenaLoadout> {
override fun load(section: ConfigurationSection): ArenaLoadout {
fun load(section: ConfigurationSection): ArenaLoadout {
val hotbar = section.getConfigurationSection("hotbar")?.let {
ArenaItem.loadList(it)
}?: listOf()
@ -39,4 +40,4 @@ data class ArenaLoadout(
)
}
}
}
}

View File

@ -1,15 +1,22 @@
package nl.kallestruik.darena.types.arena
import org.bukkit.configuration.ConfigurationSection
import nl.kallestruik.darena.types.ConfigSaveable
import nl.kallestruik.darena.types.ConfigLoadable
data class ArenaLocation(
val x: Int,
val y: Int,
val z: Int
) {
companion object {
): ConfigSaveable {
override fun save(section: ConfigurationSection) {
section.set("x", x)
section.set("y", y)
section.set("z", z)
}
fun load(section: ConfigurationSection): ArenaLocation {
companion object: ConfigLoadable<ArenaLocation> {
override fun load(section: ConfigurationSection): ArenaLocation {
return ArenaLocation(
section.getInt("x", 0),
section.getInt("y", 0),

View File

@ -8,6 +8,17 @@ data class ArenaPoints(
val lastManStanding: List<Int> = listOf(0),
val checkpoints: Map<String, List<Int>> = mapOf()
) {
fun save(section: ConfigurationSection) {
section.set("kill", ConfigHelper.intListToString(kill))
section.set("lastManStanding", ConfigHelper.intListToString(lastManStanding))
val checkpointSection = ConfigHelper.getOrCreateSection(section, "checkpoints")
for (entry in checkpoints) {
checkpointSection.set(entry.key, ConfigHelper.intListToString(entry.value))
}
}
companion object {
fun load(section: ConfigurationSection): ArenaPoints {
return ArenaPoints(

View File

@ -0,0 +1,28 @@
package nl.kallestruik.darena.types.arena
import nl.kallestruik.darena.types.ConfigSaveable
import nl.kallestruik.darena.types.ConfigLoadable
import org.bukkit.configuration.ConfigurationSection
class ArenaPotionEffect(
val name: String,
val strength: Int,
val duration: Int,
val hidden: Boolean
): ConfigSaveable {
override fun save(section: ConfigurationSection) {
section.set("strength", strength)
section.set("duration", duration)
section.set("hidden", hidden)
}
companion object: ConfigLoadable<ArenaPotionEffect> {
override fun load(section: ConfigurationSection): ArenaPotionEffect {
return ArenaPotionEffect(
section.name,
section.getInt("strength"),
section.getInt("duration"),
section.getBoolean("hidden"))
}
}
}

View File

@ -1,6 +1,9 @@
package nl.kallestruik.darena.types.arena
import nl.kallestruik.darena.arenas.world.ArenaWorld
import nl.kallestruik.darena.util.ConfigHelper
import nl.kallestruik.darena.types.ConfigSaveable
import nl.kallestruik.darena.types.ConfigLoadable
import org.bukkit.Location
import org.bukkit.configuration.ConfigurationSection
import org.bukkit.entity.Player
@ -13,23 +16,22 @@ data class ArenaSpawn(
val yaw: Float,
val pitch: Float,
val loadout: String
) {
): ConfigSaveable {
fun spawn(world: ArenaWorld, player: Player) {
player.teleport(Location(world.world, x, y, z, yaw, pitch))
}
companion object {
fun loadList(section: ConfigurationSection): List<ArenaSpawn> {
val list = mutableListOf<ArenaSpawn>()
override fun save(section: ConfigurationSection) {
section.set("x", x)
section.set("y", y)
section.set("z", z)
section.set("yaw", yaw.toDouble())
section.set("pitch", pitch.toDouble())
section.set("loadout", loadout)
}
for (key in section.getKeys(false)) {
list.add(load(section.getConfigurationSection(key)!!))
}
return list
}
fun load(section: ConfigurationSection): ArenaSpawn {
companion object: ConfigLoadable<ArenaSpawn> {
override fun load(section: ConfigurationSection): ArenaSpawn {
return ArenaSpawn(
section.name,
section.getDouble("x"),
@ -41,4 +43,4 @@ data class ArenaSpawn(
)
}
}
}
}

View File

@ -0,0 +1,27 @@
package nl.kallestruik.darena.types.arena
import nl.kallestruik.darena.types.ConfigLoadable
import nl.kallestruik.darena.types.ConfigSaveable
import nl.kallestruik.darena.util.ConfigHelper
import org.bukkit.configuration.ConfigurationSection
class ArenaSpawnRule(
val reuseSpawns: Boolean,
val effects: Map<String, ArenaPotionEffect>,
val spawns: List<String>
): ConfigSaveable {
override fun save(section: ConfigurationSection) {
section.set("reuseSpawns", reuseSpawns)
ConfigHelper.saveMap(ConfigHelper.getOrCreateSection(section, "effects"), effects)
section.set("spawns", spawns)
}
companion object: ConfigLoadable<ArenaSpawnRule> {
override fun load(section: ConfigurationSection): ArenaSpawnRule {
return ArenaSpawnRule(
section.getBoolean("reuseSpawn", true),
ConfigHelper.loadMap(section.getConfigurationSection("effects")!!, ArenaPotionEffect),
section.getStringList("spawns"))
}
}
}

View File

@ -7,10 +7,12 @@ import org.bukkit.entity.Player
class ArenaUtil {
fun getPossibleParticipants(): List<Player> {
Logger.trace(ArenaUtil::class, "getPossibleParticipants()")
return Bukkit.getOnlinePlayers().filter { it.gameMode == GameMode.SURVIVAL }
}
fun getPossibleSpectators(): List<Player> {
Logger.trace(ArenaUtil::class, "getPossibleSpectators()")
return Bukkit.getOnlinePlayers().filter { it.gameMode == GameMode.SPECTATOR }
}
}
}

View File

@ -1,6 +1,10 @@
package nl.kallestruik.darena.util
import nl.kallestruik.darena.exceptions.MaterialNotFoundException
import nl.kallestruik.darena.types.ConfigSaveable
import nl.kallestruik.darena.types.ConfigLoadable
import nl.kallestruik.darena.types.ConfigListSaveable
import nl.kallestruik.darena.types.ConfigListLoadable
import org.bukkit.Material
import org.bukkit.configuration.file.YamlConfiguration
import org.bukkit.configuration.ConfigurationSection
@ -9,6 +13,7 @@ import java.io.*
object ConfigHelper {
fun getOrCreateConfig(file: File, template: String): YamlConfiguration {
Logger.trace(ConfigHelper::class, "getOrCreateConfig(file: ${file.toString()}, template: $template)")
if (!file.parentFile.exists())
file.parentFile.mkdirs()
if (!file.exists())
@ -20,10 +25,12 @@ object ConfigHelper {
}
private fun createConfig(file: File, template: String) {
Logger.trace(ConfigHelper::class, "createConfig(file: ${file.toString()}, template: $template)")
try {
ConfigHelper.javaClass.getResourceAsStream("/$template").use { stream ->
FileOutputStream(file).use { resStreamOut ->
if (stream == null) {
Logger.error(ConfigHelper::class, "Cannot get resource \"$template\" from jar file.")
throw IOException("Cannot get resource \"$template\" from Jar file.")
}
var readBytes: Int
@ -38,20 +45,77 @@ object ConfigHelper {
}
}
fun getOrCreateSection(section: ConfigurationSection, key: String): ConfigurationSection {
Logger.trace(ConfigHelper::class, "getOrCreateSection(section: ${section.currentPath}, key: $key)")
if (section.contains(key) && section.isConfigurationSection(key)) {
return section.getConfigurationSection(key)!!
}
return section.createSection(key)
}
fun <T: ConfigSaveable> saveMap(section: ConfigurationSection, map: Map<String, T>) {
Logger.trace(ConfigHelper::class, "saveMap(section: ${section.currentPath}, map: ${map.toString()})")
for (entry in map.entries) {
entry.value.save(ConfigHelper.getOrCreateSection(section, entry.key))
}
}
fun <T: ConfigListSaveable> saveList(section: ConfigurationSection, list: List<T>) {
Logger.trace(ConfigHelper::class, "saveList(section: ${section.currentPath}, list: ${list.toString()})")
for (item in list) {
section.set(item.getKey(), item.getValue())
}
}
fun <R, T: ConfigLoadable<R>> loadMap(section: ConfigurationSection, loader: T): MutableMap<String, R> {
Logger.trace(ConfigHelper::class, "loadMap(section: ${section.currentPath}, loader: ${loader::class.qualifiedName})")
val map = mutableMapOf<String, R>()
for (key in section.getKeys(false)) {
map.put(key, loader.load(section.getConfigurationSection(key)!!))
}
return map
}
fun <R, T: ConfigListLoadable<R>> loadList(section: ConfigurationSection, loader: T): MutableList<R> {
Logger.trace(ConfigHelper::class, "loadList(section: ${section.currentPath}, loader: ${loader::class.qualifiedName})")
val list = mutableListOf<R>()
for (key in section.getKeys(false)) {
loader.loadFromList(key, section.get(key)!!)
}
return list
}
@Throws(MaterialNotFoundException::class)
fun matchMaterial(material: String): Material {
return Material.matchMaterial(material) ?: throw MaterialNotFoundException("There is not material with the name '$material'")
Logger.trace(ConfigHelper::class, "matchMaterial(material: $material)")
return Material.matchMaterial(material) ?: throw MaterialNotFoundException("There is no material with the name '$material'")
}
fun parseIntList(string: String): List<Int> {
Logger.trace(ConfigHelper::class, "parseIntList(string: $string)")
val list = mutableListOf<Int>()
for (key in string.split(" ")) {
list.add(key.toInt())
}
return list
}
fun intListToString(list: List<Int>): String {
Logger.trace(ConfigHelper::class, "list: ${list.toString()}")
val sb = StringBuilder()
for (i in list) {
sb.append(i)
}
return sb.toString().trim()
}
fun parseStringIntListMap(section: ConfigurationSection): Map<String, List<Int>> {
Logger.trace(ConfigHelper::class, "parseStringIntListMap(section: ${section.currentPath})")
val map = mutableMapOf<String, List<Int>>()
for (key in section.getKeys(true)) {
map[key] = parseIntList(section.getString(key)!!)

View File

@ -0,0 +1,53 @@
package nl.kallestruik.darena.util
import kotlin.reflect.KClass
object Logger {
var level: LogLevel = LogLevel.INFO
fun trace(source: KClass<*>, message: String) {
if (level.shouldLog(LogLevel.TRACE))
println("[TRACE] [${source.qualifiedName}] $message")
}
fun debug(source: KClass<*>, message: String) {
if (level.shouldLog(LogLevel.DEBUG))
println("[DEBUG] [${source.qualifiedName}] $message")
}
fun info(source: KClass<*>, message: String) {
if (level.shouldLog(LogLevel.INFO))
println("[INFO] [${source.qualifiedName}] $message")
}
fun warn(source: KClass<*>, message: String) {
if (level.shouldLog(LogLevel.WARN))
println("[WARN] [${source.qualifiedName}] $message")
}
fun error(source: KClass<*>, message: String) {
if (level.shouldLog(LogLevel.ERROR))
println("[ERROR] [${source.qualifiedName}] $message")
}
fun critical(source: KClass<*>, message: String) {
if (level.shouldLog(LogLevel.CRITICAL))
println("[CRITICAL] [${source.qualifiedName}] $message")
}
enum LogLevel(
val weight: Int
) {
TRACE(100),
DEBUG(80),
INFO(60),
WARN(40),
ERROR(20),
CRITICAL(0)
fun shouldLog(level: LogLevel): Boolean {
return level.weight <= weight
}
}
}

View File

@ -8,35 +8,30 @@
# z: 0
# yaw: 0
# pitch: 0
# loadout: "default"
# label2:
# x: 10
# y: 100
# z: 0
# yaw: 0
# pitch: 0
# loadout: "default"
# label3:
# x: 10
# y: 100
# z: 10
# yaw: 0
# pitch: 0
# loadout: "default"
# label4:
# x: 0
# y: 100
# z: 10
# yaw: 0
# pitch: 0
# loadout: "default"
# specatorSpawn:
# x: 0
# y: 150
# z: 0
# yaw: 0
# pitch: 0
# loadout: "none"
#loadouts:
# default:
# hotbar:
@ -76,7 +71,7 @@
# x: 10
# y: 10
# z: 10
# spawn: "label1"
# spawnRule: "label1"
# checkpoint2:
# lower:
# x: 20
@ -86,7 +81,7 @@
# x: 30
# y: 10
# z: 30
# spawn: "label2"
# spawnRule: "label2"
# finish:
# lower:
# x: 40
@ -96,7 +91,7 @@
# x: 50
# y: 10
# z: 50
# spawn: "spectatorSpawn"
# spawnRule: "spectator"
#points:
# kill: 10 5
# last-man-standing: 10 5 3 0
@ -104,3 +99,22 @@
# checkpoint1: 10
# checkpoint2: 5
# finish: 20 10 5
#spawnRules:
# initial:
# reuse-spawns: true
# effects:
# swiftness:
# strength: 1
# duration: 10
# hidden: false
# invisability:
# strength: 1
# duration: 100000000
# hidden: true
# spawns:
# - "label1"
# loadout: "default"
# spectator:
# reuse-spawns: true
# spawns:
# - "spectatorSpawn"