From a5f43096a576d8153437dd2fd413342ffa9e4b94 Mon Sep 17 00:00:00 2001 From: Gabber235 Date: Thu, 10 Aug 2023 12:20:10 +0200 Subject: [PATCH 1/4] Version bump --- adapters/BasicAdapter/build.gradle.kts | 2 +- adapters/CitizensAdapter/build.gradle.kts | 2 +- adapters/CombatLogXAdapter/build.gradle.kts | 2 +- app/pubspec.lock | 4 ++-- version.txt | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/adapters/BasicAdapter/build.gradle.kts b/adapters/BasicAdapter/build.gradle.kts index 90f3dcdabc..0b5cc5ee79 100644 --- a/adapters/BasicAdapter/build.gradle.kts +++ b/adapters/BasicAdapter/build.gradle.kts @@ -22,7 +22,7 @@ repositories { dependencies { compileOnly(kotlin("stdlib")) - compileOnly("io.papermc.paper:paper-api:1.19.4-R0.1-SNAPSHOT") + compileOnly("io.papermc.paper:paper-api:1.20.1-R0.1-SNAPSHOT") compileOnly("me.gabber235:typewriter:$version") diff --git a/adapters/CitizensAdapter/build.gradle.kts b/adapters/CitizensAdapter/build.gradle.kts index 5d8c71bc49..1dcc458055 100644 --- a/adapters/CitizensAdapter/build.gradle.kts +++ b/adapters/CitizensAdapter/build.gradle.kts @@ -25,7 +25,7 @@ repositories { dependencies { compileOnly(kotlin("stdlib")) - compileOnly("io.papermc.paper:paper-api:1.19.4-R0.1-SNAPSHOT") + compileOnly("io.papermc.paper:paper-api:1.20.1-R0.1-SNAPSHOT") compileOnly("me.gabber235:typewriter:$version") diff --git a/adapters/CombatLogXAdapter/build.gradle.kts b/adapters/CombatLogXAdapter/build.gradle.kts index b96667480d..8460b75f77 100644 --- a/adapters/CombatLogXAdapter/build.gradle.kts +++ b/adapters/CombatLogXAdapter/build.gradle.kts @@ -25,7 +25,7 @@ repositories { dependencies { compileOnly(kotlin("stdlib")) - compileOnly("io.papermc.paper:paper-api:1.19.4-R0.1-SNAPSHOT") + compileOnly("io.papermc.paper:paper-api:1.20.1-R0.1-SNAPSHOT") compileOnly("me.gabber235:typewriter:$version") diff --git a/app/pubspec.lock b/app/pubspec.lock index beaf1fe08b..dffdefb97c 100644 --- a/app/pubspec.lock +++ b/app/pubspec.lock @@ -269,10 +269,10 @@ packages: dependency: "direct main" description: name: collection - sha256: f092b211a4319e98e5ff58223576de6c2803db36221657b46c82574721240687 + sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a url: "https://pub.dev" source: hosted - version: "1.17.2" + version: "1.18.0" collection_ext: dependency: "direct main" description: diff --git a/version.txt b/version.txt index 9325c3ccda..a2268e2de4 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -0.3.0 \ No newline at end of file +0.3.1 \ No newline at end of file From 4995074553ddce0edc7791934b052f044af2c6cb Mon Sep 17 00:00:00 2001 From: Gabber235 Date: Thu, 10 Aug 2023 12:20:49 +0200 Subject: [PATCH 2/4] Fix player state reste --- .../cinematic/BlindingCinematicEntry.kt | 9 ++++---- .../entries/cinematic/CameraCinematicEntry.kt | 13 ++++++++++- .../DisplayDialogueCinematicAction.kt | 12 +++++++--- .../gabber235/typewriter/utils/PlayerState.kt | 22 +++++++++++++++++-- 4 files changed, 45 insertions(+), 11 deletions(-) diff --git a/adapters/BasicAdapter/src/main/kotlin/me/gabber235/typewriter/entries/cinematic/BlindingCinematicEntry.kt b/adapters/BasicAdapter/src/main/kotlin/me/gabber235/typewriter/entries/cinematic/BlindingCinematicEntry.kt index 6a2dddc834..f8a95c7bec 100644 --- a/adapters/BasicAdapter/src/main/kotlin/me/gabber235/typewriter/entries/cinematic/BlindingCinematicEntry.kt +++ b/adapters/BasicAdapter/src/main/kotlin/me/gabber235/typewriter/entries/cinematic/BlindingCinematicEntry.kt @@ -18,6 +18,7 @@ import me.gabber235.typewriter.utils.GenericPlayerStateProvider.LOCATION import org.bukkit.GameMode import org.bukkit.entity.Player import org.bukkit.potion.PotionEffect +import org.bukkit.potion.PotionEffect.INFINITE_DURATION import org.bukkit.potion.PotionEffectType.BLINDNESS @Entry("blinding_cinematic", "Blind the player so the screen looks black", Colors.CYAN, Icons.SOLID_EYE_SLASH) @@ -64,10 +65,10 @@ class BlindingCinematicAction( override suspend fun startSegment(segment: BlindingSegment) { super.startSegment(segment) - state = player.state(LOCATION, GAME_MODE) + state = player.state(LOCATION, GAME_MODE, EffectStateProvider(BLINDNESS)) withContext(plugin.minecraftDispatcher) { - player.addPotionEffect(PotionEffect(BLINDNESS, Int.MAX_VALUE, 0, false, false, false)) + player.addPotionEffect(PotionEffect(BLINDNESS, INFINITE_DURATION, 0, false, false, false)) if (segment.teleport) { /// Teleport the player high up to prevent them from seeing the world @@ -85,9 +86,7 @@ class BlindingCinematicAction( val state = state ?: return this.state = null withContext(plugin.minecraftDispatcher) { - player.removePotionEffect(BLINDNESS) player.restore(state) } } - -} \ No newline at end of file +} diff --git a/adapters/BasicAdapter/src/main/kotlin/me/gabber235/typewriter/entries/cinematic/CameraCinematicEntry.kt b/adapters/BasicAdapter/src/main/kotlin/me/gabber235/typewriter/entries/cinematic/CameraCinematicEntry.kt index d81f48ff84..1a579ff42f 100644 --- a/adapters/BasicAdapter/src/main/kotlin/me/gabber235/typewriter/entries/cinematic/CameraCinematicEntry.kt +++ b/adapters/BasicAdapter/src/main/kotlin/me/gabber235/typewriter/entries/cinematic/CameraCinematicEntry.kt @@ -21,6 +21,9 @@ import me.gabber235.typewriter.utils.GenericPlayerStateProvider.* import org.bukkit.Location import org.bukkit.entity.EntityType import org.bukkit.entity.Player +import org.bukkit.potion.PotionEffect +import org.bukkit.potion.PotionEffect.INFINITE_DURATION +import org.bukkit.potion.PotionEffectType.INVISIBILITY @Entry("camera_cinematic", "Create a cinematic camera path", Colors.CYAN, Icons.VIDEO) /** @@ -86,11 +89,19 @@ class CameraCinematicAction( } segments.forEach { it.setup() } - originalState = player.state(LOCATION, ALLOW_FLIGHT, FLYING, VISIBLE_PLAYERS, SHOWING_PLAYER) + originalState = player.state( + LOCATION, + ALLOW_FLIGHT, + FLYING, + VISIBLE_PLAYERS, + SHOWING_PLAYER, + EffectStateProvider(INVISIBILITY) + ) withContext(plugin.minecraftDispatcher) { player.allowFlight = true player.isFlying = true + player.addPotionEffect(PotionEffect(INVISIBILITY, INFINITE_DURATION, 0, false, false)) server.onlinePlayers.forEach { it.hidePlayer(plugin, player) player.hidePlayer(plugin, it) diff --git a/adapters/BasicAdapter/src/main/kotlin/me/gabber235/typewriter/entries/cinematic/DisplayDialogueCinematicAction.kt b/adapters/BasicAdapter/src/main/kotlin/me/gabber235/typewriter/entries/cinematic/DisplayDialogueCinematicAction.kt index 5c6da8b4fe..b06e81b9e7 100644 --- a/adapters/BasicAdapter/src/main/kotlin/me/gabber235/typewriter/entries/cinematic/DisplayDialogueCinematicAction.kt +++ b/adapters/BasicAdapter/src/main/kotlin/me/gabber235/typewriter/entries/cinematic/DisplayDialogueCinematicAction.kt @@ -1,12 +1,16 @@ package me.gabber235.typewriter.entries.cinematic +import com.github.shynixn.mccoroutine.bukkit.minecraftDispatcher +import kotlinx.coroutines.withContext import me.gabber235.typewriter.adapters.modifiers.Colored import me.gabber235.typewriter.adapters.modifiers.Help import me.gabber235.typewriter.adapters.modifiers.Placeholder import me.gabber235.typewriter.entry.dialogue.playSpeakerSound import me.gabber235.typewriter.entry.entries.* import me.gabber235.typewriter.extensions.placeholderapi.parsePlaceholders -import me.gabber235.typewriter.utils.GenericPlayerStateProvider +import me.gabber235.typewriter.plugin +import me.gabber235.typewriter.utils.GenericPlayerStateProvider.EXP +import me.gabber235.typewriter.utils.GenericPlayerStateProvider.LEVEL import me.gabber235.typewriter.utils.PlayerState import me.gabber235.typewriter.utils.restore import me.gabber235.typewriter.utils.state @@ -52,7 +56,7 @@ class DisplayDialogueCinematicAction( override suspend fun setup() { super.setup() - state = player.state(GenericPlayerStateProvider.EXP, GenericPlayerStateProvider.LEVEL) + state = player.state(EXP, LEVEL) player.exp = 0f player.level = 0 setup?.invoke(player) @@ -91,9 +95,11 @@ class DisplayDialogueCinematicAction( override suspend fun teardown() { super.teardown() - player.restore(state) teardown?.invoke(player) reset?.invoke(player) + withContext(plugin.minecraftDispatcher) { + player.restore(state) + } } override fun canFinish(frame: Int): Boolean = segments canFinishAt frame diff --git a/plugin/src/main/kotlin/me/gabber235/typewriter/utils/PlayerState.kt b/plugin/src/main/kotlin/me/gabber235/typewriter/utils/PlayerState.kt index 1f08e488e7..f2a66bf16a 100644 --- a/plugin/src/main/kotlin/me/gabber235/typewriter/utils/PlayerState.kt +++ b/plugin/src/main/kotlin/me/gabber235/typewriter/utils/PlayerState.kt @@ -4,6 +4,8 @@ import me.gabber235.typewriter.plugin import org.bukkit.GameMode import org.bukkit.Location import org.bukkit.entity.Player +import org.bukkit.potion.PotionEffect +import org.bukkit.potion.PotionEffectType interface PlayerStateProvider { fun store(player: Player): Any @@ -27,7 +29,7 @@ enum class GenericPlayerStateProvider(private val store: Player.() -> Any, priva VISIBLE_PLAYERS({ server.onlinePlayers.filter { it != this && canSee(it) }.map { it.uniqueId.toString() }.toList() }, { data -> - val visible = data as List + val visible = data as List<*> server.onlinePlayers.filter { it != this && it.uniqueId.toString() in visible } .forEach { showPlayer(plugin, it) } }), @@ -36,7 +38,7 @@ enum class GenericPlayerStateProvider(private val store: Player.() -> Any, priva SHOWING_PLAYER({ server.onlinePlayers.filter { it != this && it.canSee(this) }.map { it.uniqueId.toString() }.toList() }, { data -> - val showing = data as List + val showing = data as List<*> server.onlinePlayers.filter { it != this && it.uniqueId.toString() in showing } .forEach { it.showPlayer(plugin, this) } }) @@ -46,6 +48,22 @@ enum class GenericPlayerStateProvider(private val store: Player.() -> Any, priva override fun restore(player: Player, value: Any) = player.restore(value) } +class EffectStateProvider( + private val effect: PotionEffectType, +) : PlayerStateProvider { + override fun store(player: Player): Any { + return player.getPotionEffect(effect) ?: return false + } + + override fun restore(player: Player, value: Any) { + if (value !is PotionEffect) { + player.removePotionEffect(effect) + return + } + player.addPotionEffect(value) + } +} + fun Player.state(vararg keys: PlayerStateProvider): PlayerState = state(keys) @JvmName("stateArray") From 209e15c25a0b85e48477dd21237467434cbb281d Mon Sep 17 00:00:00 2001 From: Gabber235 Date: Thu, 10 Aug 2023 12:50:13 +0200 Subject: [PATCH 3/4] Add capture button for locations --- app/lib/models/adapter.dart | 6 + app/lib/utils/color_filter.dart | 25 ++ .../widgets/inspector/editors/optional.dart | 120 +++++-- .../typewriter/adapters/AdapterLoader.kt | 9 +- .../typewriter/adapters/CustomEditors.kt | 28 ++ .../typewriter/adapters/EntryBlueprint.kt | 307 +++++++++--------- .../adapters/editors/LocationEditor.kt | 18 + .../modifiers/CaptureModifierComputer.kt | 4 +- .../modifiers/HelpModifierComputer.kt | 11 +- .../typewriter/capture/AssetCapturer.kt | 2 +- .../capture/ImmediateFieldCapturer.kt | 31 ++ .../capturers/LocationSnapshotCapturer.kt | 11 +- .../typewriter/entry/AssetManager.kt | 17 +- .../typewriter/entry/StagingManager.kt | 10 + .../typewriter/ui/ClientSynchronizer.kt | 8 + .../gabber235/typewriter/utils/Extensions.kt | 15 + 16 files changed, 429 insertions(+), 193 deletions(-) create mode 100644 app/lib/utils/color_filter.dart create mode 100644 plugin/src/main/kotlin/me/gabber235/typewriter/capture/ImmediateFieldCapturer.kt diff --git a/app/lib/models/adapter.dart b/app/lib/models/adapter.dart index 0d3a8cbfa0..701100f732 100644 --- a/app/lib/models/adapter.dart +++ b/app/lib/models/adapter.dart @@ -241,6 +241,12 @@ extension FieldTypeExtension on FieldInfo { /// If the [ObjectEditor] needs to show a default layout or if a field declares a custom layout. bool get hasCustomLayout { + if (this is CustomField) { + final editor = (this as CustomField).editor; + if (editor == "optional") { + return true; + } + } if (this is ObjectField) { return true; } diff --git a/app/lib/utils/color_filter.dart b/app/lib/utils/color_filter.dart new file mode 100644 index 0000000000..2441285316 --- /dev/null +++ b/app/lib/utils/color_filter.dart @@ -0,0 +1,25 @@ +import "package:flutter/material.dart"; + +// https://api.flutter.dev/flutter/dart-ui/ColorFilter/ColorFilter.matrix.html +const ColorFilter greyscale = ColorFilter.matrix([ + 0.2126, + 0.7152, + 0.0722, + 0, + 0, + 0.2126, + 0.7152, + 0.0722, + 0, + 0, + 0.2126, + 0.7152, + 0.0722, + 0, + 0, + 0, + 0, + 0, + 1, + 0, +]); diff --git a/app/lib/widgets/inspector/editors/optional.dart b/app/lib/widgets/inspector/editors/optional.dart index c6c2823995..8e4700b343 100644 --- a/app/lib/widgets/inspector/editors/optional.dart +++ b/app/lib/widgets/inspector/editors/optional.dart @@ -1,18 +1,22 @@ import "package:flutter/material.dart"; +import "package:typewriter/utils/color_filter.dart"; import "package:flutter_animate/flutter_animate.dart"; import "package:hooks_riverpod/hooks_riverpod.dart"; import "package:typewriter/models/adapter.dart"; import "package:typewriter/utils/passing_reference.dart"; import "package:typewriter/widgets/inspector/editors.dart"; import "package:typewriter/widgets/inspector/editors/field.dart"; +import "package:typewriter/widgets/inspector/header.dart"; import "package:typewriter/widgets/inspector/inspector.dart"; class OptionalEditorFilter extends EditorFilter { @override - bool canEdit(FieldInfo info) => info is CustomField && info.editor == "optional"; + bool canEdit(FieldInfo info) => + info is CustomField && info.editor == "optional"; @override - Widget build(String path, FieldInfo info) => OptionalEditor(path: path, field: info as CustomField); + Widget build(String path, FieldInfo info) => + OptionalEditor(path: path, field: info as CustomField); } class OptionalEditor extends HookConsumerWidget { @@ -44,32 +48,100 @@ class OptionalEditor extends HookConsumerWidget { ); } - return Row( - children: [ - Checkbox( - value: enabled, - onChanged: (value) { - ref.read(inspectingEntryDefinitionProvider)?.updateField(ref.passing, "$path.enabled", value ?? false); - }, - ), - Expanded( - child: AnimatedOpacity( - opacity: enabled ? 1 : 0.6, - duration: 200.ms, - curve: Curves.easeOut, - child: MouseRegion( - cursor: enabled ? MouseCursor.defer : SystemMouseCursors.forbidden, - child: AbsorbPointer( - absorbing: !enabled, - child: FieldEditor( - path: "$path.value", - type: subField, + final subPath = "$path.value"; + final headerActionFilters = ref.watch(headerActionFiltersProvider); + final subFieldActions = headerActionFilters + .where((filter) => filter.shouldShow(subPath, subField)) + .toList(); + + return FieldHeader( + field: field, + path: path, + leading: _buildActions( + HeaderActionLocation.leading, + subFieldActions, + subPath, + subField, + enabled: enabled, + ), + trailing: _buildActions( + HeaderActionLocation.trailing, + subFieldActions, + subPath, + subField, + enabled: enabled, + ), + actions: _buildActions( + HeaderActionLocation.actions, + subFieldActions, + subPath, + subField, + enabled: enabled, + ), + child: Row( + children: [ + Checkbox( + value: enabled, + onChanged: (value) { + ref + .read(inspectingEntryDefinitionProvider) + ?.updateField(ref.passing, "$path.enabled", value ?? false); + }, + ), + Expanded( + child: AnimatedOpacity( + opacity: enabled ? 1 : 0.6, + duration: 200.ms, + curve: Curves.easeOut, + child: MouseRegion( + cursor: + enabled ? MouseCursor.defer : SystemMouseCursors.forbidden, + child: AbsorbPointer( + absorbing: !enabled, + child: FieldEditor( + path: "$path.value", + type: subField, + ), ), ), ), ), - ), - ], + ], + ), ); } + + List _buildActions( + HeaderActionLocation location, + List actions, + String path, + FieldInfo field, { + bool enabled = true, + }) { + final children = actions + .where((action) => action.location(path, field) == location) + .map((action) => action.build(path, field)) + .toList(); + if (enabled) { + return children; + } + + return [ + for (final child in children) + MouseRegion( + cursor: SystemMouseCursors.forbidden, + child: AbsorbPointer( + child: AnimatedOpacity( + opacity: 0.6, + duration: 200.ms, + curve: Curves.easeOut, + child: ColorFiltered( + colorFilter: greyscale, + child: child, + ), + ), + ), + ), + ]; + } } diff --git a/plugin/src/main/kotlin/me/gabber235/typewriter/adapters/AdapterLoader.kt b/plugin/src/main/kotlin/me/gabber235/typewriter/adapters/AdapterLoader.kt index 500e86a193..dd0b1a7b25 100644 --- a/plugin/src/main/kotlin/me/gabber235/typewriter/adapters/AdapterLoader.kt +++ b/plugin/src/main/kotlin/me/gabber235/typewriter/adapters/AdapterLoader.kt @@ -3,6 +3,7 @@ package me.gabber235.typewriter.adapters import com.google.gson.GsonBuilder import com.google.gson.JsonArray import com.google.gson.reflect.TypeToken +import me.gabber235.typewriter.adapters.editors.LocationFieldCapturer import me.gabber235.typewriter.capture.Capturer import me.gabber235.typewriter.capture.CapturerCreator import me.gabber235.typewriter.entry.dialogue.DialogueMessenger @@ -35,6 +36,12 @@ private val gson = ).enableComplexMapKeySerialization() .create() +val staticCaptureClasses by lazy { + listOf( + LocationFieldCapturer::class, + ) +} + interface AdapterLoader { val adapters: List val adaptersJson: JsonArray @@ -173,7 +180,7 @@ class AdapterLoaderImpl : AdapterLoader, KoinComponent { private fun constructCapturers(captureClasses: List>) = captureClasses.map { captureClass -> captureClass.kotlin as KClass> - } + } + staticCaptureClasses //TODO: Make compatible with java. private fun findFilterForMessenger(messengerClass: Class<*>) = diff --git a/plugin/src/main/kotlin/me/gabber235/typewriter/adapters/CustomEditors.kt b/plugin/src/main/kotlin/me/gabber235/typewriter/adapters/CustomEditors.kt index e7d827134c..dbb1d041de 100644 --- a/plugin/src/main/kotlin/me/gabber235/typewriter/adapters/CustomEditors.kt +++ b/plugin/src/main/kotlin/me/gabber235/typewriter/adapters/CustomEditors.kt @@ -3,6 +3,7 @@ package me.gabber235.typewriter.adapters import com.google.gson.* import com.google.gson.reflect.TypeToken import me.gabber235.typewriter.adapters.editors.* +import me.gabber235.typewriter.adapters.modifiers.StaticModifierComputer import me.gabber235.typewriter.utils.CronExpression import org.bukkit.Location import org.bukkit.Material @@ -21,6 +22,7 @@ typealias GsonSerializer = (T, Type, JsonSerializationContext) -> JsonElement typealias FieInfoGenerator = (TypeToken<*>) -> FieldInfo typealias DefaultGenerator = (TypeToken<*>) -> JsonElement +typealias FieldModifierGenerator = (FieldInfo) -> FieldModifier? @Target(AnnotationTarget.FUNCTION) annotation class CustomEditor(val klass: KClass<*>) @@ -166,6 +168,32 @@ class ObjectEditor(val klass: KClass, val name: String) { internal fun generateDefault(token: TypeToken<*>): JsonElement { return defaultGenerator?.invoke(token) ?: JsonNull.INSTANCE } + + /** + * Save the modifiers for this editor. + * NOTE: This field is used internally and should not be accessed directly. + */ + private val modifiers = mutableListOf() + + /** + * Adds a modifier to this editor. + */ + operator fun FieldModifier?.unaryPlus() { + if (this == null) return + modifiers.add { this } + } + + infix fun StaticModifierComputer.with(annotation: A) { + modifiers.add { this.computeModifier(annotation, it) } + } + + /** + * Generates the modifiers for this editor. + * NOTE: This method is used internally and should not be accessed directly. + */ + internal fun generateModifiers(info: FieldInfo): List { + return modifiers.mapNotNull { it(info) } + } } internal val customEditors by lazy { diff --git a/plugin/src/main/kotlin/me/gabber235/typewriter/adapters/EntryBlueprint.kt b/plugin/src/main/kotlin/me/gabber235/typewriter/adapters/EntryBlueprint.kt index 836b5f0947..1cd520fa24 100644 --- a/plugin/src/main/kotlin/me/gabber235/typewriter/adapters/EntryBlueprint.kt +++ b/plugin/src/main/kotlin/me/gabber235/typewriter/adapters/EntryBlueprint.kt @@ -5,194 +5,195 @@ import com.google.gson.annotations.SerializedName import com.google.gson.reflect.TypeToken import lirand.api.utilities.allFields import me.gabber235.typewriter.entry.Entry -import me.gabber235.typewriter.entry.entries.* import java.lang.reflect.ParameterizedType data class EntryBlueprint( - val name: String, - val description: String, - val adapter: String, - val fields: FieldInfo, - val color: String, // Hex color - val icon: String, // Font Awesome icon from [Icons] - val tags: List, - @Transient - val clazz: Class, + val name: String, + val description: String, + val adapter: String, + val fields: FieldInfo, + val color: String, // Hex color + val icon: String, // Font Awesome icon from [Icons] + val tags: List, + @Transient + val clazz: Class, ) sealed class FieldInfo { - val modifiers: MutableList = mutableListOf() - - companion object { - - fun fromTypeToken(token: TypeToken<*>): FieldInfo { - val customEditor = computeCustomEditor(token) - if (customEditor != null) { - return CustomField( - editor = customEditor.name, - fieldInfo = customEditor.generateFieldInfo(token), - default = customEditor.generateDefault(token), - ) - } - - val primitive = PrimitiveFieldType.fromTokenType(token) - if (primitive != null) { - return PrimitiveField(primitive) - } - - - return when { - token.rawType.isEnum -> EnumField.fromTypeToken(token) - List::class.java.isAssignableFrom(token.rawType) -> { - val type = token.type - if (type is ParameterizedType) { - val typeArg = type.actualTypeArguments[0] - ListField(fromTypeToken(TypeToken.get(typeArg))) - } else { - throw IllegalArgumentException("Unknown type for list field: $type") - } - } - - Map::class.java.isAssignableFrom(token.rawType) -> { - val type = token.type - if (type is ParameterizedType) { - val typeArgs = type.actualTypeArguments - MapField(fromTypeToken(TypeToken.get(typeArgs[0])), fromTypeToken(TypeToken.get(typeArgs[1]))) - } else { - throw IllegalArgumentException("Unknown type for map field: $type") - } - } - - else -> ObjectField.fromTypeToken(token) - } - } - } - - abstract fun default(): JsonElement + val modifiers: MutableList = mutableListOf() + + companion object { + + fun fromTypeToken(token: TypeToken<*>): FieldInfo { + val customEditor = computeCustomEditor(token) + if (customEditor != null) { + return CustomField( + editor = customEditor.name, + fieldInfo = customEditor.generateFieldInfo(token), + default = customEditor.generateDefault(token), + ).apply { + customEditor.generateModifiers(this).forEach { it.appendModifier(this) } + } + } + + val primitive = PrimitiveFieldType.fromTokenType(token) + if (primitive != null) { + return PrimitiveField(primitive) + } + + + return when { + token.rawType.isEnum -> EnumField.fromTypeToken(token) + List::class.java.isAssignableFrom(token.rawType) -> { + val type = token.type + if (type is ParameterizedType) { + val typeArg = type.actualTypeArguments[0] + ListField(fromTypeToken(TypeToken.get(typeArg))) + } else { + throw IllegalArgumentException("Unknown type for list field: $type") + } + } + + Map::class.java.isAssignableFrom(token.rawType) -> { + val type = token.type + if (type is ParameterizedType) { + val typeArgs = type.actualTypeArguments + MapField(fromTypeToken(TypeToken.get(typeArgs[0])), fromTypeToken(TypeToken.get(typeArgs[1]))) + } else { + throw IllegalArgumentException("Unknown type for map field: $type") + } + } + + else -> ObjectField.fromTypeToken(token) + } + } + } + + abstract fun default(): JsonElement } class PrimitiveField(val type: PrimitiveFieldType) : FieldInfo() { - override fun default(): JsonElement = type.default + override fun default(): JsonElement = type.default } enum class PrimitiveFieldType { - @SerializedName("boolean") - BOOLEAN, - - @SerializedName("double") - DOUBLE, - - @SerializedName("integer") - INTEGER, - - @SerializedName("string") - STRING, - ; - - val default: JsonElement - get() = when (this) { - BOOLEAN -> JsonPrimitive(false) - DOUBLE -> JsonPrimitive(0.0) - INTEGER -> JsonPrimitive(0) - STRING -> JsonPrimitive("") - } - - companion object { - fun fromTokenType(token: TypeToken<*>): PrimitiveFieldType? { - return when (token.rawType) { - Boolean::class.java -> BOOLEAN - Double::class.java -> DOUBLE - Double::class.javaObjectType -> DOUBLE - Float::class.java -> DOUBLE - Float::class.javaObjectType -> DOUBLE - Int::class.java -> INTEGER - Int::class.javaObjectType -> INTEGER - Long::class.java -> INTEGER - Long::class.javaObjectType -> INTEGER - String::class.java -> STRING - else -> null - } - } - } + @SerializedName("boolean") + BOOLEAN, + + @SerializedName("double") + DOUBLE, + + @SerializedName("integer") + INTEGER, + + @SerializedName("string") + STRING, + ; + + val default: JsonElement + get() = when (this) { + BOOLEAN -> JsonPrimitive(false) + DOUBLE -> JsonPrimitive(0.0) + INTEGER -> JsonPrimitive(0) + STRING -> JsonPrimitive("") + } + + companion object { + fun fromTokenType(token: TypeToken<*>): PrimitiveFieldType? { + return when (token.rawType) { + Boolean::class.java -> BOOLEAN + Double::class.java -> DOUBLE + Double::class.javaObjectType -> DOUBLE + Float::class.java -> DOUBLE + Float::class.javaObjectType -> DOUBLE + Int::class.java -> INTEGER + Int::class.javaObjectType -> INTEGER + Long::class.java -> INTEGER + Long::class.javaObjectType -> INTEGER + String::class.java -> STRING + else -> null + } + } + } } // If the field is an enum, the values will be the enum constants class EnumField(private val values: List) : FieldInfo() { - companion object { - fun fromTypeToken(token: TypeToken<*>): EnumField { - /// If the enum fields have an @SerializedName annotation, use that as the value - /// Otherwise, use the enum constant name - - val values = token.rawType.enumConstants.filterIsInstance>().map { enumConstant -> - val field = enumConstant.javaClass.getField(enumConstant.name) - val annotation = field.getAnnotation(SerializedName::class.java) - annotation?.value ?: enumConstant.name - } - - return EnumField(values) - } - } - - override fun default(): JsonElement = JsonPrimitive(values.first()) + companion object { + fun fromTypeToken(token: TypeToken<*>): EnumField { + /// If the enum fields have an @SerializedName annotation, use that as the value + /// Otherwise, use the enum constant name + + val values = token.rawType.enumConstants.filterIsInstance>().map { enumConstant -> + val field = enumConstant.javaClass.getField(enumConstant.name) + val annotation = field.getAnnotation(SerializedName::class.java) + annotation?.value ?: enumConstant.name + } + + return EnumField(values) + } + } + + override fun default(): JsonElement = JsonPrimitive(values.first()) } // If the field is a list, this is the type of the list class ListField(val type: FieldInfo) : FieldInfo() { - override fun default(): JsonElement = JsonArray() + override fun default(): JsonElement = JsonArray() } // If the field is a map, this is the type of the map class MapField(val key: FieldInfo, val value: FieldInfo) : FieldInfo() { - override fun default(): JsonElement = JsonObject() + override fun default(): JsonElement = JsonObject() } // If the field is an object, this is the type of the object class ObjectField(val fields: Map) : FieldInfo() { - override fun default(): JsonElement { - val obj = JsonObject() - fields.forEach { (name, field) -> - obj.add(name, field.default()) - } - return obj - } - - companion object { - fun fromTypeToken(token: TypeToken<*>): ObjectField { - val fields = mutableMapOf() - - token.rawType.allFields.forEach { field -> - val name = - if (field.isAnnotationPresent(SerializedName::class.java)) { - field.getAnnotation(SerializedName::class.java).value - } else { - field.name - } - - val info = FieldInfo.fromTypeToken(TypeToken.get(field.genericType)) - - // If a field has a modifier for the ui. E.g. it is a trigger or fact or something else. - // We add a bit of extra information to the field. This is used by the UI to - // display the field differently. - computeFieldModifiers(field, info) - - fields[name] = info - } - - return ObjectField(fields) - } - - } + override fun default(): JsonElement { + val obj = JsonObject() + fields.forEach { (name, field) -> + obj.add(name, field.default()) + } + return obj + } + + companion object { + fun fromTypeToken(token: TypeToken<*>): ObjectField { + val fields = mutableMapOf() + + token.rawType.allFields.forEach { field -> + val name = + if (field.isAnnotationPresent(SerializedName::class.java)) { + field.getAnnotation(SerializedName::class.java).value + } else { + field.name + } + + val info = FieldInfo.fromTypeToken(TypeToken.get(field.genericType)) + + // If a field has a modifier for the ui. E.g. it is a trigger or fact or something else. + // We add a bit of extra information to the field. This is used by the UI to + // display the field differently. + computeFieldModifiers(field, info) + + fields[name] = info + } + + return ObjectField(fields) + } + + } } // If a custom editor takes over a field, this is the type of the object. // The custom editor will be responsible for displaying the field. class CustomField( - val editor: String, - val fieldInfo: FieldInfo? = null, - val default: JsonElement = JsonNull.INSTANCE, + val editor: String, + val fieldInfo: FieldInfo? = null, + val default: JsonElement = JsonNull.INSTANCE, ) : FieldInfo() { - override fun default(): JsonElement = default + override fun default(): JsonElement = default } diff --git a/plugin/src/main/kotlin/me/gabber235/typewriter/adapters/editors/LocationEditor.kt b/plugin/src/main/kotlin/me/gabber235/typewriter/adapters/editors/LocationEditor.kt index d301f250a3..802c4fb848 100644 --- a/plugin/src/main/kotlin/me/gabber235/typewriter/adapters/editors/LocationEditor.kt +++ b/plugin/src/main/kotlin/me/gabber235/typewriter/adapters/editors/LocationEditor.kt @@ -4,7 +4,14 @@ import com.google.gson.JsonObject import lirand.api.extensions.server.server import me.gabber235.typewriter.adapters.CustomEditor import me.gabber235.typewriter.adapters.ObjectEditor +import me.gabber235.typewriter.adapters.modifiers.Capture +import me.gabber235.typewriter.adapters.modifiers.CaptureModifierComputer +import me.gabber235.typewriter.capture.CapturerCreator +import me.gabber235.typewriter.capture.ImmediateFieldCapturer +import me.gabber235.typewriter.capture.RecorderRequestContext +import me.gabber235.typewriter.capture.capturers.LocationSnapshotCapturer import me.gabber235.typewriter.utils.logErrorIfNull +import me.gabber235.typewriter.utils.ok import org.bukkit.Location @CustomEditor(Location::class) @@ -54,4 +61,15 @@ fun ObjectEditor.location() = reference { obj } + + CaptureModifierComputer with Capture(LocationFieldCapturer::class) +} + +class LocationFieldCapturer(title: String, entryId: String, fieldPath: String) : + ImmediateFieldCapturer(title, entryId, fieldPath, LocationSnapshotCapturer(title)) { + companion object : CapturerCreator { + override fun create(context: RecorderRequestContext): Result { + return ok(LocationFieldCapturer(context.title, context.entryId, context.fieldPath)) + } + } } \ No newline at end of file diff --git a/plugin/src/main/kotlin/me/gabber235/typewriter/adapters/modifiers/CaptureModifierComputer.kt b/plugin/src/main/kotlin/me/gabber235/typewriter/adapters/modifiers/CaptureModifierComputer.kt index 736504edba..d50a795508 100644 --- a/plugin/src/main/kotlin/me/gabber235/typewriter/adapters/modifiers/CaptureModifierComputer.kt +++ b/plugin/src/main/kotlin/me/gabber235/typewriter/adapters/modifiers/CaptureModifierComputer.kt @@ -2,8 +2,8 @@ package me.gabber235.typewriter.adapters.modifiers import me.gabber235.typewriter.adapters.FieldInfo import me.gabber235.typewriter.adapters.FieldModifier +import me.gabber235.typewriter.capture.Capturer import me.gabber235.typewriter.capture.CapturerCreator -import me.gabber235.typewriter.capture.RecordedCapturer import me.gabber235.typewriter.logger import kotlin.reflect.KClass import kotlin.reflect.full.companionObject @@ -11,7 +11,7 @@ import kotlin.reflect.full.isSubclassOf import kotlin.reflect.jvm.jvmName @Target(AnnotationTarget.FIELD, AnnotationTarget.VALUE_PARAMETER, AnnotationTarget.PROPERTY) -annotation class Capture(val capturer: KClass>) +annotation class Capture(val capturer: KClass>) object CaptureModifierComputer : StaticModifierComputer { override val annotationClass: Class = Capture::class.java diff --git a/plugin/src/main/kotlin/me/gabber235/typewriter/adapters/modifiers/HelpModifierComputer.kt b/plugin/src/main/kotlin/me/gabber235/typewriter/adapters/modifiers/HelpModifierComputer.kt index 52716851fc..30ee53bbcc 100644 --- a/plugin/src/main/kotlin/me/gabber235/typewriter/adapters/modifiers/HelpModifierComputer.kt +++ b/plugin/src/main/kotlin/me/gabber235/typewriter/adapters/modifiers/HelpModifierComputer.kt @@ -8,12 +8,9 @@ import me.gabber235.typewriter.adapters.FieldModifier.DynamicModifier annotation class Help(val text: String) object HelpModifierComputer : StaticModifierComputer { - override val annotationClass: Class = Help::class.java + override val annotationClass: Class = Help::class.java - override fun computeModifier(annotation: Help, info: FieldInfo): FieldModifier { - // If the field is wrapped in a list or other container we try if the inner type can be modified - innerCompute(annotation, info)?.let { return it } - - return DynamicModifier("help", annotation.text) - } + override fun computeModifier(annotation: Help, info: FieldInfo): FieldModifier { + return DynamicModifier("help", annotation.text) + } } diff --git a/plugin/src/main/kotlin/me/gabber235/typewriter/capture/AssetCapturer.kt b/plugin/src/main/kotlin/me/gabber235/typewriter/capture/AssetCapturer.kt index ebc81da0d3..68eeb4c6e0 100644 --- a/plugin/src/main/kotlin/me/gabber235/typewriter/capture/AssetCapturer.kt +++ b/plugin/src/main/kotlin/me/gabber235/typewriter/capture/AssetCapturer.kt @@ -10,7 +10,7 @@ import org.koin.java.KoinJavaComponent.inject open class AssetCapturer( override val title: String, private val asset: AssetEntry, - private val capturer: RecordedCapturer + private val capturer: RecordedCapturer, ) : RecordedCapturer { private val gson: Gson by inject(Gson::class.java, named("bukkitDataParser")) private val assetManager: AssetManager by inject(AssetManager::class.java) diff --git a/plugin/src/main/kotlin/me/gabber235/typewriter/capture/ImmediateFieldCapturer.kt b/plugin/src/main/kotlin/me/gabber235/typewriter/capture/ImmediateFieldCapturer.kt new file mode 100644 index 0000000000..9aac4993a7 --- /dev/null +++ b/plugin/src/main/kotlin/me/gabber235/typewriter/capture/ImmediateFieldCapturer.kt @@ -0,0 +1,31 @@ +package me.gabber235.typewriter.capture + +import com.google.gson.Gson +import me.gabber235.typewriter.entry.StagingManager +import me.gabber235.typewriter.ui.ClientSynchronizer +import org.bukkit.entity.Player +import org.koin.core.qualifier.named +import org.koin.java.KoinJavaComponent.inject + +open class ImmediateFieldCapturer( + override val title: String, + private val entryId: String, + private val fieldPath: String, + private val capturer: ImmediateCapturer, +) : ImmediateCapturer { + private val gson: Gson by inject(Gson::class.java, named("entryParser")) + private val stagingManager: StagingManager by inject(StagingManager::class.java) + private val clientSynchronizer: ClientSynchronizer by inject(ClientSynchronizer::class.java) + override fun capture(player: Player): T { + val result = capturer.capture(player) + val data = gson.toJsonTree(result) + + val pageId = stagingManager.findEntryPage(entryId).getOrThrow() + + stagingManager.updateEntryField(pageId, entryId, fieldPath, data) + // TODO: This should automatically be done. + clientSynchronizer.sendEntryFieldUpdate(pageId, entryId, fieldPath, data) + + return result + } +} \ No newline at end of file diff --git a/plugin/src/main/kotlin/me/gabber235/typewriter/capture/capturers/LocationSnapshotCapturer.kt b/plugin/src/main/kotlin/me/gabber235/typewriter/capture/capturers/LocationSnapshotCapturer.kt index fc982e67da..51c7059a7a 100644 --- a/plugin/src/main/kotlin/me/gabber235/typewriter/capture/capturers/LocationSnapshotCapturer.kt +++ b/plugin/src/main/kotlin/me/gabber235/typewriter/capture/capturers/LocationSnapshotCapturer.kt @@ -1,11 +1,20 @@ package me.gabber235.typewriter.capture.capturers import me.gabber235.typewriter.capture.ImmediateCapturer +import me.gabber235.typewriter.utils.round import org.bukkit.Location import org.bukkit.entity.Player class LocationSnapshotCapturer(override val title: String) : ImmediateCapturer { override fun capture(player: Player): Location { - return player.location + val location = player.location + return Location( + location.world, + location.x.round(2), + location.y.round(2), + location.z.round(2), + location.yaw.round(2), + location.pitch.round(2) + ) } } \ No newline at end of file diff --git a/plugin/src/main/kotlin/me/gabber235/typewriter/entry/AssetManager.kt b/plugin/src/main/kotlin/me/gabber235/typewriter/entry/AssetManager.kt index c109df12be..8047020c89 100644 --- a/plugin/src/main/kotlin/me/gabber235/typewriter/entry/AssetManager.kt +++ b/plugin/src/main/kotlin/me/gabber235/typewriter/entry/AssetManager.kt @@ -11,7 +11,7 @@ import java.io.StringReader interface AssetStorage { fun storeAsset(path: String, content: String) - fun fetchAsset(path: String): String? + fun fetchAsset(path: String): Result fun deleteAsset(path: String) fun fetchAllAssetPaths(): Set @@ -66,7 +66,12 @@ class AssetManager : KoinComponent { } fun fetchAsset(entry: AssetEntry): String? { - return storage.fetchAsset(entry.path) + val result = storage.fetchAsset(entry.path) + if (result.isFailure) { + plugin.logger.severe("Failed to fetch asset ${entry.path}") + return null + } + return result.getOrNull() } fun shutdown() { @@ -81,8 +86,12 @@ class LocalAssetStorage : AssetStorage { file.writeText(content) } - override fun fetchAsset(path: String): String { - return plugin.dataFolder.resolve("assets/$path").readText() + override fun fetchAsset(path: String): Result { + val file = plugin.dataFolder.resolve("assets/$path") + if (!file.exists()) { + return Result.failure(IllegalArgumentException("Asset $path not found.")) + } + return Result.success(file.readText()) } override fun deleteAsset(path: String) { diff --git a/plugin/src/main/kotlin/me/gabber235/typewriter/entry/StagingManager.kt b/plugin/src/main/kotlin/me/gabber235/typewriter/entry/StagingManager.kt index 8ebc01024a..dc4a3a5512 100644 --- a/plugin/src/main/kotlin/me/gabber235/typewriter/entry/StagingManager.kt +++ b/plugin/src/main/kotlin/me/gabber235/typewriter/entry/StagingManager.kt @@ -34,6 +34,8 @@ interface StagingManager { fun updateEntry(pageId: String, data: JsonObject): Result fun reorderEntry(pageId: String, entryId: String, newIndex: Int): Result fun deleteEntry(pageId: String, entryId: String): Result + + fun findEntryPage(entryId: String): Result suspend fun publish(): Result fun shutdown() } @@ -225,6 +227,14 @@ class StagingManagerImpl : StagingManager, KoinComponent { return ok("Successfully deleted entry with id $entryId") } + override fun findEntryPage(entryId: String): Result { + val page = pages.values.find { page -> + page["entries"].asJsonArray.any { entry -> entry.asJsonObject["id"].asString == entryId } + } ?: return failure("Entry does not exist") + + return ok(page["name"].asString) + } + private fun getPage(id: String): Result { val page = pages[id] ?: return failure("Page does not exist") return ok(page) diff --git a/plugin/src/main/kotlin/me/gabber235/typewriter/ui/ClientSynchronizer.kt b/plugin/src/main/kotlin/me/gabber235/typewriter/ui/ClientSynchronizer.kt index 0398ee55f1..a21e95de4c 100644 --- a/plugin/src/main/kotlin/me/gabber235/typewriter/ui/ClientSynchronizer.kt +++ b/plugin/src/main/kotlin/me/gabber235/typewriter/ui/ClientSynchronizer.kt @@ -38,6 +38,8 @@ interface ClientSynchronizer { fun handleUpdateWriter(client: SocketIOClient, data: String, ack: AckRequest) fun handleCaptureRequest(client: SocketIOClient, data: String, ack: AckRequest) + + fun sendEntryFieldUpdate(pageId: String, entryId: String, fieldPath: String, data: JsonElement) } class ClientSynchronizerImpl : ClientSynchronizer, KoinComponent { @@ -175,6 +177,12 @@ class ClientSynchronizerImpl : ClientSynchronizer, KoinComponent { val result = recorders.requestRecording(player!!, context) ack.sendResult(result.toResult()) } + + override fun sendEntryFieldUpdate(pageId: String, entryId: String, fieldPath: String, data: JsonElement) { + val update = EntryUpdate(pageId, entryId, fieldPath, data) + val updateData = gson.toJson(update) + communicationHandler.server?.broadcastOperations?.sendEvent("updateEntry", updateData) + } } fun AckRequest.sendResult(result: Result) { diff --git a/plugin/src/main/kotlin/me/gabber235/typewriter/utils/Extensions.kt b/plugin/src/main/kotlin/me/gabber235/typewriter/utils/Extensions.kt index a96f582ef4..6aa69385c3 100644 --- a/plugin/src/main/kotlin/me/gabber235/typewriter/utils/Extensions.kt +++ b/plugin/src/main/kotlin/me/gabber235/typewriter/utils/Extensions.kt @@ -8,6 +8,7 @@ import org.geysermc.floodgate.api.FloodgateApi import org.koin.java.KoinJavaComponent.get import java.io.File import java.time.Duration +import kotlin.math.round operator fun File.get(name: String): File = File(this, name) @@ -39,3 +40,17 @@ val Location.highUpLocation: Location location.y += 200 return location } + +fun Double.round(decimals: Int): Double { + var multiplier = 1.0 + repeat(decimals) { multiplier *= 10 } + + return round(this * multiplier) / multiplier +} + +fun Float.round(decimals: Int): Float { + var multiplier = 1.0 + repeat(decimals) { multiplier *= 10 } + + return (round(this * multiplier) / multiplier).toFloat() +} \ No newline at end of file From 39ffdc6bd808bc26a0d793621f80272396547fe5 Mon Sep 17 00:00:00 2001 From: Gabber235 Date: Thu, 10 Aug 2023 12:53:54 +0200 Subject: [PATCH 4/4] Fix border radius --- app/lib/widgets/components/general/toasts.dart | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/app/lib/widgets/components/general/toasts.dart b/app/lib/widgets/components/general/toasts.dart index e9618817a8..c15de724c4 100644 --- a/app/lib/widgets/components/general/toasts.dart +++ b/app/lib/widgets/components/general/toasts.dart @@ -182,6 +182,7 @@ class ToastDisplay extends HookConsumerWidget { bottom: 0, child: SingleChildScrollView( child: Column( + mainAxisSize: MainAxisSize.min, children: [ for (final toast in toasts) if (toast is TemporaryToast) @@ -259,6 +260,10 @@ class _TemporaryToast extends HookConsumerWidget { return Card( color: toast.color, elevation: 4.0, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8.0), + ), + clipBehavior: Clip.antiAlias, child: SizedBox( width: width, child: Column(