🦋 Debugging for the 1.2.
All checks were successful
Auto Build / Build (push) Successful in 2m40s

This commit is contained in:
2025-12-06 16:40:12 +01:00
parent d6103b078b
commit 97b73b0c55
17 changed files with 182 additions and 101 deletions

View File

@@ -2,6 +2,7 @@ package fr.azur.tcoww
import de.maxhenkel.voicechat.api.BukkitVoicechatService
import fr.azur.tcoww.events.GameEvents
import fr.azur.tcoww.events.PhaseEvents
import fr.azur.tcoww.events.PowerEvents
import fr.azur.tcoww.events.ToolsEvents
import fr.azur.tcoww.events.UIEvents
@@ -36,6 +37,7 @@ class Tcoww : JavaPlugin() {
registerEvents(GameEvents, this@Tcoww)
registerEvents(PowerEvents, this@Tcoww)
registerEvents(ToolsEvents, this@Tcoww)
registerEvents(PhaseEvents, this@Tcoww)
}
val service = server.servicesManager.load(BukkitVoicechatService::class.java)
@@ -44,7 +46,7 @@ class Tcoww : JavaPlugin() {
registerRoles()
val mainScoreboard = server.scoreboardManager.mainScoreboard
if (mainScoreboard.getTeam("NoCollide") == null) {
if (mainScoreboard.getTeam("NoCollision") == null) {
val team = server.scoreboardManager.mainScoreboard.registerNewTeam("NoCollision")
team.setOption(Team.Option.COLLISION_RULE, Team.OptionStatus.NEVER)
}
@@ -55,6 +57,7 @@ class Tcoww : JavaPlugin() {
fun reload() {
this.reloadConfig()
Manager.reloadSkin()
}
private fun registerRoles() {

View File

@@ -7,6 +7,7 @@ import fr.azur.tcoww.game.NightLightCycle
import fr.azur.tcoww.roles.Child
import fr.azur.tcoww.roles.Role
import fr.azur.tcoww.roles.Role.Companion.werewolfRole
import fr.azur.tcoww.roles.Werewolf
import fr.azur.tcoww.roles.Witch
import fr.azur.tcoww.utils.DataKeys.Insomnia
import fr.azur.tcoww.utils.DataKeys.PlayerDead
@@ -28,6 +29,7 @@ import org.bukkit.event.EventHandler
import org.bukkit.event.EventPriority
import org.bukkit.event.Listener
import org.bukkit.event.entity.EntityDamageByEntityEvent
import org.bukkit.event.entity.EntityRegainHealthEvent
import org.bukkit.event.entity.PlayerDeathEvent
import org.bukkit.event.player.PlayerBedEnterEvent
import org.bukkit.event.player.PlayerBedLeaveEvent
@@ -101,18 +103,17 @@ object GameEvents : Listener {
PersistentDataType.INTEGER,
) == 2 &&
(it.itemMeta as Damageable).damage != 1
}
} ?: return@callback
if (player.persistentDataContainer.get(
PlayerDead,
PersistentDataType.BOOLEAN,
) == true
) == false
) {
return@callback
}
if (item == null) return@callback
(item.itemMeta as Damageable).damage = 1
player.inventory.remove(item)
player.persistentDataContainer.set(
NamespacedKey("tcoww", "dead"),
@@ -144,7 +145,7 @@ object GameEvents : Listener {
@EventHandler
fun onHit(event: EntityDamageByEntityEvent) {
val current = Game.current ?: return
if (current.timeGestion.phase != NightLightCycle.Phase.NIGHT) return
if (current.timeGestion.phase != NightLightCycle.Phase.NIGHT || Werewolf.remainingKill < 1) return
val damager = event.damager
val target = event.entity
@@ -159,6 +160,7 @@ object GameEvents : Listener {
target.health = 0.0
target.persistentDataContainer.set(Insomnia, PersistentDataType.BOOLEAN, true)
if (target.isSleeping) target.wakeup(false)
Werewolf.remainingKill--
}
}
@@ -244,4 +246,13 @@ object GameEvents : Listener {
}
}
}
@EventHandler
fun disablePotionRegen(event: EntityRegainHealthEvent) {
val player = event.entity as? Player ?: return
if (player.hasPotionEffect(PotionEffectType.WITHER)) {
event.isCancelled = true
}
}
}

View File

@@ -7,10 +7,12 @@ import fr.azur.tcoww.game.NightLightCycle
import fr.azur.tcoww.roles.Child
import fr.azur.tcoww.roles.Role
import fr.azur.tcoww.roles.Role.Companion.werewolfRole
import fr.azur.tcoww.roles.Werewolf
import fr.azur.tcoww.utils.BedGestion.isOccupied
import fr.azur.tcoww.utils.DataKeys.Insomnia
import fr.azur.tcoww.utils.DataKeys.PlayerDead
import fr.azur.tcoww.utils.DataKeys.PowerItems
import fr.azur.tcoww.utils.Players.corpseKey
import fr.azur.tcoww.utils.Players.isTransformed
import fr.azur.tcoww.utils.Players.kill
import fr.azur.tcoww.utils.Players.tfHuman
@@ -59,23 +61,39 @@ object PhaseEvents : Listener {
}
NightLightCycle.Phase.VOTE -> {
event.world.entities
.filter {
it.persistentDataContainer.getOrDefault(
corpseKey,
PersistentDataType.BOOLEAN,
false,
)
}.forEach { it.remove() }
event.world.players.forEach { player ->
player.sendMessage(
Component
.text("Le jour se couche, ce soir un vote est organisé.")
.color(NamedTextColor.DARK_AQUA)
.appendNewline()
.append(
Component
.text("Faite /vote <pseudo> pour voter.")
.color(NamedTextColor.AQUA),
),
)
if (event.dayCount >= 1) {
player.sendMessage(
Component
.text("Le jour se couche, ce soir un vote est organisé.")
.color(NamedTextColor.DARK_AQUA)
.appendNewline()
.append(
Component
.text("Faite /vote pour voter.")
.color(NamedTextColor.AQUA),
),
)
} else {
player.sendMessage(
Component
.text("Le jour se couche, ce soir aucun vote n'est organisé. Faute de preuve.", NamedTextColor.DARK_AQUA),
)
}
}
}
NightLightCycle.Phase.NIGHT -> {
if (Vote.vote.isNotEmpty()) {
if (Vote.vote.isNotEmpty() && current.timeGestion.dayCount >= 1) {
val voteMap =
Vote.vote.values
.groupingBy { it }
@@ -103,6 +121,8 @@ object PhaseEvents : Listener {
Vote.vote.clear()
}
Werewolf.remainingKill = 1
event.world.players.forEach { player ->
player.sendMessage(
Component
@@ -148,7 +168,7 @@ object PhaseEvents : Listener {
}
}
},
Tcoww.instance.config.getLong("player-sleep"),
Tcoww.instance.config.getLong("player-sleep") * 20,
)
}

View File

@@ -18,7 +18,6 @@ import org.bukkit.event.Listener
import org.bukkit.event.inventory.InventoryMoveItemEvent
import org.bukkit.event.player.PlayerDropItemEvent
import org.bukkit.event.player.PlayerInteractEvent
import org.bukkit.inventory.meta.Damageable
import org.bukkit.persistence.PersistentDataType
import org.bukkit.potion.PotionEffect
import org.bukkit.potion.PotionEffectType
@@ -51,10 +50,11 @@ object PowerEvents : Listener {
fun clickEvent(event: PlayerInteractEvent) {
val item = event.item ?: return
val player = event.player
if (!item.persistentDataContainer.has(NamespacedKey("tcoww", "power"))) return
event.isCancelled = true
if (player.hasCooldown(item)) return
when (item.persistentDataContainer.get(NamespacedKey("tcoww", "power"), PersistentDataType.INTEGER)) {
1 -> {
if ((item.itemMeta as Damageable).damage == 1) return
val current = Game.current ?: return
val menu = PlayerSelectMenu(current.remainingPlayer)
@@ -62,15 +62,13 @@ object PowerEvents : Listener {
menu.future.whenComplete { selectedPlayer, _ ->
val target = selectedPlayer ?: return@whenComplete
(item.itemMeta as Damageable).damage = 1
player.inventory.remove(item)
target.addPotionEffect(PotionEffect(PotionEffectType.WITHER, 600, 4))
}
}
2 -> {
if ((item.itemMeta as Damageable).damage == 1) return
val current = Game.current ?: return
val menu =
PlayerSelectMenu(
current.remainingPlayer.filter {
@@ -85,7 +83,7 @@ object PowerEvents : Listener {
menu.future.whenComplete { selectedPlayer, _ ->
val target = selectedPlayer ?: return@whenComplete
(item.itemMeta as Damageable).damage = 1
player.inventory.remove(item)
target.persistentDataContainer.set(
NamespacedKey("tcoww", "dead"),
PersistentDataType.BOOLEAN,
@@ -104,13 +102,12 @@ object PowerEvents : Listener {
}
3 -> {
player.setCooldown(item, 600)
if (player.isTransformed()) {
player.tfHuman()
} else {
player.tfWerewolf()
}
player.setCooldown(item, 200)
}
4 -> {

View File

@@ -3,6 +3,8 @@ package fr.azur.tcoww.game
import fr.azur.tcoww.Tcoww
import fr.azur.tcoww.roles.Role
import fr.azur.tcoww.roles.Role.Companion.werewolfRole
import fr.azur.tcoww.utils.DataKeys.Insomnia
import fr.azur.tcoww.utils.DataKeys.PlayerDead
import fr.azur.tcoww.utils.Players.tfHuman
import net.kyori.adventure.text.Component
import net.kyori.adventure.text.format.NamedTextColor
@@ -73,7 +75,7 @@ class Game(
}
fun checkGameEnd(): Boolean {
val team = players.map { player -> player.werewolfRole.team }.toSet()
val team = remainingPlayer.map { player -> player.werewolfRole.team }.toSet()
return team.count() <= 1
}
@@ -82,33 +84,26 @@ class Game(
current = null
timeGestion.stop()
world.time = 18000
val endComponent =
Component
.text("La partie est terminée !", NamedTextColor.DARK_GREEN)
val winComponent =
Component
.text("Victoire des ", NamedTextColor.DARK_GREEN)
.append(remainingPlayer[0].werewolfRole.team.displayName)
.append(Component.text(".", NamedTextColor.DARK_GREEN))
players.forEach { player ->
player.gameMode = GameMode.SPECTATOR
player.sendMessage(
Component
.text("La partie est terminée !", NamedTextColor.DARK_GREEN)
.appendNewline()
.apply {
if (!withoutResult) return@apply
remainingPlayer.forEachIndexed { index, player ->
append(player.displayName())
if (index != remainingPlayer.lastIndex) {
append(
Component.text(
when (index) {
remainingPlayer.lastIndex - 1 -> " et "
else -> ", "
},
NamedTextColor.DARK_GREEN,
),
)
}
append(Component.text(" ont gagné !", NamedTextColor.DARK_GREEN))
}
},
)
player.sendMessage(endComponent)
if (remainingPlayer[0].werewolfRole.team == Role.Team.Solo) {
TODO()
} else {
if (!withoutResult) player.sendMessage(winComponent)
}
}
val lobbyLoc = plugin.config.getLocation("lobby-location")
Bukkit.getScheduler().runTaskLater(
@@ -117,6 +112,10 @@ class Game(
players.forEach { player ->
if (lobbyLoc != null) player.teleport(lobbyLoc)
player.gameMode = GameMode.ADVENTURE
player.persistentDataContainer.remove(PlayerDead)
player.persistentDataContainer.remove(Insomnia)
player.inventory.clear()
player.resetCooldown()
}
},
200,

View File

@@ -23,6 +23,7 @@ class NightLightCycle(
val world: World,
val newPhase: Phase,
val duration: Duration,
val dayCount: Int,
) : Event() {
override fun getHandlers(): HandlerList = handlerList
@@ -55,19 +56,19 @@ class NightLightCycle(
val duration = Duration.fromPlugin(plugin)
var targetInstant: Long
var targetInstant: Long = 0
var tick: Float = 0F
var tickOrigin: Int = 0
var realDifference: Int
var tickDifference: Int
var realDifference: Long = 0
var tickDifference: Int = 0
var realOrigin: Long = 0
var dayCount: Int = 0
init {
world.setGameRule(GameRule.DO_DAYLIGHT_CYCLE, false)
realDifference = duration.day * 1000
targetInstant = System.currentTimeMillis() + realDifference.toLong()
tickDifference = 12_000
Bukkit.getPluginManager().callEvent(NightLightCyclePhaseChangeEvent(world, phase, duration))
changePhase(Phase.DAY)
process =
Bukkit.getScheduler().runTaskTimer(
@@ -81,44 +82,49 @@ class NightLightCycle(
}
fun changePhase(newPhase: Phase) {
realOrigin = System.currentTimeMillis()
when (newPhase) {
Phase.DAY -> {
tickDifference = 12_000
tickOrigin = 0
realDifference = duration.day * 1000
realDifference = duration.day * 1000L
}
Phase.VOTE -> {
tickDifference = 1_000
tickOrigin = 12_000
realDifference = duration.vote * 1000
realDifference = duration.vote * 1000L
}
Phase.NIGHT -> {
tickDifference = 11_000
tickDifference = 10_000
tickOrigin = 13_000
realDifference = duration.night * 1000
realDifference = duration.night * 1000L
}
Phase.CREPUSCULAR -> {
tickDifference = 1_000
tickOrigin = 23_000
realDifference = duration.crepuscular * 1000
realDifference = duration.crepuscular * 1000L
}
}
targetInstant = System.currentTimeMillis() + realDifference
tick = tickDifference / realDifference.toFloat()
phase = newPhase
Bukkit.getPluginManager().callEvent(NightLightCyclePhaseChangeEvent(world, newPhase, duration))
Bukkit.getPluginManager().callEvent(NightLightCyclePhaseChangeEvent(world, newPhase, duration, dayCount))
}
fun tick() {
if (System.currentTimeMillis() >= targetInstant) {
val realtime = System.currentTimeMillis()
if (realtime >= targetInstant) {
val phase = phases[(phases.indexOf(phase) + 1) % phases.count()]
if (phase == Phase.DAY) {
dayCount += 1
}
changePhase(phase)
}
val result = tickOrigin + (realtime - realOrigin) * tick
world.time = result.toLong().coerceIn(0, 23999)
}
fun stop() {

View File

@@ -59,10 +59,13 @@ enum class CustomItems(
BlasTechDL44(true, {
ItemStack.of(Material.CROSSBOW).apply {
itemMeta =
(itemMeta as DamageableCrossbow).apply {
(itemMeta as CrossbowMeta).apply {
addChargedProjectile(GunBullet.item)
displayName(Component.text("BlasTech DL-44", NamedTextColor.GOLD))
addEnchant(Enchantment.INFINITY, 1, true)
}
itemMeta =
(itemMeta as Damageable).apply {
setMaxDamage(1)
}
}
@@ -87,9 +90,9 @@ enum class CustomItems(
DeathPotion(true, {
ItemStack.of(Material.POTION).apply {
itemMeta =
(itemMeta as DamageablePotion).apply {
(itemMeta as PotionMeta).apply {
setMaxStackSize(1)
setMaxDamage(1)
customName(
Component.text("Potion de poison", NamedTextColor.DARK_PURPLE),
)
@@ -105,14 +108,18 @@ enum class CustomItems(
),
)
}
itemMeta =
(itemMeta as Damageable).apply {
setMaxDamage(1)
}
}
}),
HealPotion(true, {
ItemStack.of(Material.POTION).apply {
itemMeta =
(itemMeta as DamageablePotion).apply {
(itemMeta as PotionMeta).apply {
setMaxStackSize(1)
setMaxDamage(1)
customName(
Component.text("Potion de vie", NamedTextColor.DARK_PURPLE),
)
@@ -124,6 +131,10 @@ enum class CustomItems(
color = Color.RED
lore(listOf(Component.text("Click droit pour utiliser.", NamedTextColor.GOLD)))
}
itemMeta =
(itemMeta as Damageable).apply {
setMaxDamage(1)
}
}
}),
SpawnLocTool(true, {
@@ -230,10 +241,6 @@ enum class CustomItems(
}
}
interface DamageableCrossbow :
Damageable,
CrossbowMeta
interface DamageablePotion :
Damageable,
PotionMeta {

View File

@@ -9,7 +9,7 @@ import org.bukkit.entity.Player
object FortuneTeller : Role {
override fun handle(player: Player) {
player.inventory.addItem(CustomItems.CrystalBall.item)
player.inventory.setItem(9, CustomItems.CrystalBall.item)
}
override val team = Role.Team.Villager

View File

@@ -9,7 +9,7 @@ import org.bukkit.entity.Player
object Hunter : Role {
override fun handle(player: Player) {
player.inventory.addItem(CustomItems.BlasTechDL44.item)
player.inventory.setItem(9, CustomItems.BlasTechDL44.item)
}
override val team = Role.Team.Villager

View File

@@ -1,6 +1,7 @@
package fr.azur.tcoww.roles
import net.kyori.adventure.text.Component
import net.kyori.adventure.text.JoinConfiguration
import net.kyori.adventure.text.format.NamedTextColor
import org.bukkit.Material
import org.bukkit.NamespacedKey
@@ -37,12 +38,13 @@ interface Role {
val description: List<Component>
val lineDescription: Component
get() {
return Component.empty().apply {
description.forEach {
append(it)
appendNewline()
}
}
val joinConfig =
JoinConfiguration
.builder()
.separator(Component.newline())
.build()
return Component.join(joinConfig, description)
}
companion object {
@@ -56,19 +58,20 @@ interface Role {
player.sendMessage(
Component.text("Votre role est ", NamedTextColor.DARK_AQUA).append(role.displayName),
)
player.sendMessage(role.lineDescription)
player.sendMessage(
role.lineDescription.append {
if (role.team == Team.Solo) {
Component.text("Vous gagnez Seul.", NamedTextColor.AQUA)
} else {
Component
.text("Vous gagnez avec les ", NamedTextColor.AQUA)
.append(role.team.displayName)
.append(Component.text(".", NamedTextColor.AQUA))
}
if (role.team == Team.Solo) {
Component.text("Vous gagnez Seul.", NamedTextColor.AQUA)
} else {
Component
.text("Vous gagnez avec les ", NamedTextColor.AQUA)
.append(role.team.displayName)
.append(Component.text(".", NamedTextColor.AQUA))
},
)
role.handle(player)
player.persistentDataContainer.set(roleKey, PersistentDataType.STRING, role.id.toString())
}

View File

@@ -9,6 +9,7 @@ import org.bukkit.NamespacedKey
import org.bukkit.entity.Player
object Werewolf : Role {
var remainingKill = 0
override val team = Role.Team.LG
override var id = NamespacedKey("tcoww", "werewolf")
override val displayName = Component.text("Loup-Garou", NamedTextColor.DARK_GRAY)
@@ -19,14 +20,21 @@ object Werewolf : Role {
listOf(
Component.text("Pour la faire courte, vous avez faim, très faim.", NamedTextColor.BLUE),
Component.text(
"Votre but est de tuer les villageois. Sans vous faire atraper et voter. Toute les nuits, vous pouvez vous tranformer en loup pour manger un villageois.",
"Votre but est de tuer les villageois. Sans vous faire atraper et voter.",
NamedTextColor.BLUE,
),
Component.text(
"Toute les nuits, vous pouvez vous tranformer en loup pour manger un villageois.",
NamedTextColor.BLUE,
),
)
override fun handle(player: Player) {
val tfItem = CustomItems.WereWolfTransformItem.item
player.inventory.addItem(tfItem)
player.setCooldown(tfItem, ((Tcoww.instance.config.getInt("duration.day") + Tcoww.instance.config.getInt("duration.day")) * 20))
player.inventory.setItem(9, tfItem)
player.setCooldown(
tfItem,
((Tcoww.instance.config.getInt("duration.day") + Tcoww.instance.config.getInt("duration.day")) * 20),
)
}
}

View File

@@ -9,8 +9,8 @@ import org.bukkit.entity.Player
object Witch : Role {
override fun handle(player: Player) {
player.inventory.addItem(CustomItems.DeathPotion.item)
player.inventory.addItem(CustomItems.HealPotion.item)
player.inventory.setItem(9, CustomItems.DeathPotion.item)
player.inventory.setItem(10, CustomItems.HealPotion.item)
}
override val team = Role.Team.Villager

View File

@@ -1,6 +1,8 @@
package fr.azur.tcoww.ui
import fr.azur.tcoww.Tcoww
import fr.azur.tcoww.utils.Players.isTransformed
import fr.azur.tcoww.utils.skins.Manager
import net.kyori.adventure.text.Component
import net.kyori.adventure.text.format.NamedTextColor
import org.bukkit.Material
@@ -25,7 +27,12 @@ class PlayerSelectMenu(
ItemStack.of(Material.PLAYER_HEAD).apply {
itemMeta =
(itemMeta as SkullMeta).apply {
playerProfile = player.playerProfile
playerProfile =
if (player.isTransformed()) {
Manager.oldPlayerProfile[player.uniqueId]
} else {
player.playerProfile
}
}
}
setItem(index, item)
@@ -36,8 +43,10 @@ class PlayerSelectMenu(
override fun onClick(event: InventoryClickEvent) {
val index = event.slot
event.isCancelled = true
if (index !in 0..<players.count() || event.clickedInventory != inventory) return
future.complete(players.elementAt(index))
event.inventory.close()
}
override fun onClose(event: InventoryCloseEvent) {

View File

@@ -26,7 +26,7 @@ object Players {
Bukkit
.getServer()
.scoreboardManager.mainScoreboard
.getTeam("NoCollide")
.getTeam("NoCollision")
fun Player.tfWerewolf() {
if (isTransformed()) return

View File

@@ -4,7 +4,10 @@ import de.maxhenkel.voicechat.api.Group
import de.maxhenkel.voicechat.api.VoicechatPlugin
import de.maxhenkel.voicechat.api.VoicechatServerApi
import de.maxhenkel.voicechat.api.events.EventRegistration
import de.maxhenkel.voicechat.api.events.LeaveGroupEvent
import de.maxhenkel.voicechat.api.events.VoicechatServerStartedEvent
import fr.azur.tcoww.game.Game
import fr.azur.tcoww.game.NightLightCycle
import org.bukkit.entity.Player
import java.util.UUID
import kotlin.random.Random
@@ -17,6 +20,7 @@ object VoiceChatPlugin : VoicechatPlugin {
override fun registerEvents(registration: EventRegistration) {
registration.registerEvent(VoicechatServerStartedEvent::class.java, this::onServerStarted)
registration.registerEvent(LeaveGroupEvent::class.java, this::onGroupRemove)
}
private fun onServerStarted(event: VoicechatServerStartedEvent) {
@@ -32,6 +36,17 @@ object VoiceChatPlugin : VoicechatPlugin {
.build()
}
private fun onGroupRemove(event: LeaveGroupEvent) {
val current = Game.current ?: return
if ((
current.timeGestion.phase == NightLightCycle.Phase.NIGHT ||
current.timeGestion.phase == NightLightCycle.Phase.CREPUSCULAR
)
) {
event.cancel()
}
}
fun addWerewolf(player: Player) {
val connection = api.getConnectionOf(player.uniqueId) ?: return
connection.group = werewolfGroup

View File

@@ -1,5 +1,6 @@
package fr.azur.tcoww.utils.skins
import com.destroystokyo.paper.profile.PlayerProfile
import fr.azur.tcoww.Tcoww
import net.skinsrestorer.api.SkinsRestorer
import net.skinsrestorer.api.SkinsRestorerProvider
@@ -14,6 +15,7 @@ object Manager {
var maxSkinId: Int = 0
private lateinit var skinsRestorer: SkinsRestorer
var oldPlayerSkin = mutableMapOf<UUID, SkinIdentifier?>()
var oldPlayerProfile = mutableMapOf<UUID, PlayerProfile>()
var wwPlayerFur = mutableMapOf<UUID, Int>()
fun handle() {
@@ -32,13 +34,14 @@ object Manager {
maxSkinId = skinsValue.size
skinsValue.forEachIndexed { index, skin ->
skinsRestorer.skinStorage.setCustomSkinData("tcoww$index", SkinProperty.of(skin.value, skin.signature))
skinsRestorer.skinStorage.setCustomSkinData("tcoww_$index", SkinProperty.of(skin.value, skin.signature))
}
}
fun applyWerewolfFur(player: Player) {
val skin = skinsRestorer.playerStorage.getSkinIdOfPlayer(player.uniqueId)
oldPlayerSkin[player.uniqueId] = skin.getOrNull()
oldPlayerProfile[player.uniqueId] = player.playerProfile
val result = skinsRestorer.skinStorage.findOrCreateSkinData("tcoww_${getWerewolfFur(player.uniqueId)}")

View File

@@ -1,5 +1,5 @@
name: tcoww
version: '1.0-SNAPSHOT'
version: '1.2.0'
main: fr.azur.tcoww.Tcoww
api-version: '1.21'
load: POSTWORLD