diff --git a/buildSrc/src/main/kotlin/multification-repositories.gradle.kts b/buildSrc/src/main/kotlin/multification-repositories.gradle.kts index aedf61f..751fbc1 100644 --- a/buildSrc/src/main/kotlin/multification-repositories.gradle.kts +++ b/buildSrc/src/main/kotlin/multification-repositories.gradle.kts @@ -9,4 +9,5 @@ repositories { maven("https://hub.spigotmc.org/nexus/content/repositories/snapshots/") // spigot maven("https://repo.panda-lang.org/releases/") // expressible maven("https://repo.stellardrift.ca/repository/snapshots/") + maven("https://storehouse.okaeri.eu/repository/maven-public/") // okaeri configs } \ No newline at end of file diff --git a/multification-cdn/src/com/eternalcode/multification/cdn/MultificationNoticeCdnComposer.java b/multification-cdn/src/com/eternalcode/multification/cdn/MultificationNoticeCdnComposer.java index 2b91859..6f76523 100644 --- a/multification-cdn/src/com/eternalcode/multification/cdn/MultificationNoticeCdnComposer.java +++ b/multification-cdn/src/com/eternalcode/multification/cdn/MultificationNoticeCdnComposer.java @@ -7,6 +7,7 @@ import com.eternalcode.multification.notice.NoticeKey; import com.eternalcode.multification.notice.resolver.NoticeResolverRegistry; import com.eternalcode.multification.notice.resolver.NoticeSerdesResult; +import com.eternalcode.multification.notice.resolver.NoticeSerdesResult.Multiple; import com.eternalcode.multification.notice.resolver.chat.ChatContent; import com.eternalcode.multification.notice.NoticePart; @@ -97,8 +98,8 @@ private Result, Exception> serializeAll(SerializeContext context) { continue; } - if (result instanceof NoticeSerdesResult.Multi multi) { - section.append(toSection(key, context.description, multi.elements())); + if (result instanceof Multiple multiple) { + section.append(toSection(key, context.description, multiple.elements())); continue; } diff --git a/multification-core/src/com/eternalcode/multification/notice/resolver/NoticeSerdesResult.java b/multification-core/src/com/eternalcode/multification/notice/resolver/NoticeSerdesResult.java index 9115dd6..f58a8f9 100644 --- a/multification-core/src/com/eternalcode/multification/notice/resolver/NoticeSerdesResult.java +++ b/multification-core/src/com/eternalcode/multification/notice/resolver/NoticeSerdesResult.java @@ -25,7 +25,7 @@ public List anyElements() { } } - record Multi(List elements) implements NoticeSerdesResult { + record Multiple(List elements) implements NoticeSerdesResult { @Override public List anyElements() { return elements; diff --git a/multification-core/src/com/eternalcode/multification/notice/resolver/chat/ChatResolver.java b/multification-core/src/com/eternalcode/multification/notice/resolver/chat/ChatResolver.java index e3eae3c..2e3c4bd 100644 --- a/multification-core/src/com/eternalcode/multification/notice/resolver/chat/ChatResolver.java +++ b/multification-core/src/com/eternalcode/multification/notice/resolver/chat/ChatResolver.java @@ -2,6 +2,7 @@ import com.eternalcode.multification.notice.NoticeKey; import com.eternalcode.multification.notice.resolver.NoticeSerdesResult; +import com.eternalcode.multification.notice.resolver.NoticeSerdesResult.Multiple; import com.eternalcode.multification.notice.resolver.text.TextContentResolver; import java.util.List; import java.util.Optional; @@ -50,7 +51,7 @@ public NoticeSerdesResult serialize(ChatContent content) { return new NoticeSerdesResult.Single(messages.get(0)); } - return new NoticeSerdesResult.Multi(messages); + return new Multiple(messages); } @Override diff --git a/multification-okaeri/build.gradle.kts b/multification-okaeri/build.gradle.kts new file mode 100644 index 0000000..d79913d --- /dev/null +++ b/multification-okaeri/build.gradle.kts @@ -0,0 +1,20 @@ +plugins { + `multification-java` + `multification-java-17` + `multification-java-unit-test` + `multification-repositories` + `multification-publish` +} + +dependencies { + api(project(":multification-core")) + + // okaeri configs + val okaeriConfigsVersion = "5.0.0-beta.5" + api("eu.okaeri:okaeri-configs-yaml-snakeyaml:${okaeriConfigsVersion}") + api("eu.okaeri:okaeri-configs-serdes-commons:${okaeriConfigsVersion}") + + testImplementation(project(":multification-bukkit")) + testImplementation("org.spigotmc:spigot-api:${Versions.SPIGOT_API}") + testImplementation("net.kyori:adventure-api:${Versions.ADVENTURE_API}") +} diff --git a/multification-okaeri/src/com/eternalcode/multification/okaeri/MultificationNoticeSerializer.java b/multification-okaeri/src/com/eternalcode/multification/okaeri/MultificationNoticeSerializer.java new file mode 100644 index 0000000..34afeb1 --- /dev/null +++ b/multification-okaeri/src/com/eternalcode/multification/okaeri/MultificationNoticeSerializer.java @@ -0,0 +1,142 @@ +package com.eternalcode.multification.okaeri; + +import com.eternalcode.multification.notice.Notice; +import com.eternalcode.multification.notice.Notice.Builder; +import com.eternalcode.multification.notice.NoticeKey; +import com.eternalcode.multification.notice.NoticePart; +import com.eternalcode.multification.notice.resolver.NoticeContent; +import com.eternalcode.multification.notice.resolver.NoticeDeserializeResult; +import com.eternalcode.multification.notice.resolver.NoticeResolverRegistry; +import com.eternalcode.multification.notice.resolver.NoticeSerdesResult; +import com.eternalcode.multification.notice.resolver.NoticeSerdesResult.Multiple; +import com.eternalcode.multification.notice.resolver.NoticeSerdesResult.Single; +import com.eternalcode.multification.notice.resolver.chat.ChatContent; +import eu.okaeri.configs.schema.GenericsDeclaration; +import eu.okaeri.configs.serdes.DeserializationData; +import eu.okaeri.configs.serdes.ObjectSerializer; +import eu.okaeri.configs.serdes.SerializationData; +import java.util.Collections; +import java.util.List; +import java.util.Set; +import org.jetbrains.annotations.NotNull; + +public class MultificationNoticeSerializer implements ObjectSerializer { + + private static final int SINGLE_SERIALIZE_DESERIALIZE_PART = 1; + + private final NoticeResolverRegistry noticeRegistry; + + public MultificationNoticeSerializer(NoticeResolverRegistry noticeRegistry) { + this.noticeRegistry = noticeRegistry; + } + + @Override + public boolean supports(@NotNull Class type) { + return Notice.class.isAssignableFrom(type); + } + + @Override + public void serialize(Notice notice, @NotNull SerializationData data, @NotNull GenericsDeclaration generics) { + List> parts = notice.parts(); + + boolean isChatBeautifulSerialized = trySerializeChatBeautiful(data, notice); + + if (isChatBeautifulSerialized) { + return; + } + + for (NoticePart part : parts) { + NoticeSerdesResult result = this.noticeRegistry.serialize(part); + + if (result instanceof NoticeSerdesResult.Single single) { + data.add(part.noticeKey().key(), single.element()); + continue; + } + + if (result instanceof Multiple multiple) { + data.add(part.noticeKey().key(), multiple.elements()); + } + } + } + + @Override + public Notice deserialize(DeserializationData data, @NotNull GenericsDeclaration generics) { + Builder builder = Notice.builder(); + + if (data.isValue()) { + Object value = data.getValueRaw(); + + if (value instanceof String stringValue) { + List messages = Collections.singletonList(stringValue); + builder.withPart(NoticeKey.CHAT, new ChatContent(messages)); + } + + if (value instanceof List) { + List messages = data.getValueAsList(String.class); + builder.withPart(NoticeKey.CHAT, new ChatContent(messages)); + } + + return builder.build(); + } + + Set keys = data.asMap().keySet(); + + for (String key : keys) { + Object value = data.getRaw(key); + + if (value instanceof String stringValue) { + NoticeDeserializeResult noticeResult = this.noticeRegistry.deserialize(key, new Single(stringValue)) + .orElseThrow(() -> new UnsupportedOperationException( + "Unsupported notice key: " + key + " with value: " + stringValue)); + + this.withPart(builder, noticeResult); + continue; + } + + if (value instanceof List) { + List messages = data.getAsList(key, String.class); + + NoticeDeserializeResult noticeResult = this.noticeRegistry.deserialize(key, new Multiple(messages)) + .orElseThrow(() -> new UnsupportedOperationException( + "Unsupported notice key: " + key + " with values: " + messages)); + + this.withPart(builder, noticeResult); + continue; + } + + throw new UnsupportedOperationException( + "Unsupported notice type: " + value.getClass() + " for key: " + key); + } + + return builder.build(); + } + + private void withPart(Builder builder, NoticeDeserializeResult noticeResult) { + builder.withPart(noticeResult.noticeKey(), noticeResult.content()); + } + + private static boolean trySerializeChatBeautiful(SerializationData data, Notice notice) { + List> parts = notice.parts(); + + if (parts.size() != 1) { + return false; + } + + NoticePart part = parts.get(0); + + if (part.noticeKey() != NoticeKey.CHAT) { + return false; + } + + ChatContent chat = (ChatContent) part.content(); + List messages = chat.contents(); + + if (messages.size() == SINGLE_SERIALIZE_DESERIALIZE_PART) { + data.setValue(messages.get(0)); + return true; + } + + data.setValue(messages); + return true; + } +} \ No newline at end of file diff --git a/multification-okaeri/src/com/eternalcode/multification/okaeri/MultificationSerdesPack.java b/multification-okaeri/src/com/eternalcode/multification/okaeri/MultificationSerdesPack.java new file mode 100644 index 0000000..c780a51 --- /dev/null +++ b/multification-okaeri/src/com/eternalcode/multification/okaeri/MultificationSerdesPack.java @@ -0,0 +1,25 @@ +package com.eternalcode.multification.okaeri; +import com.eternalcode.multification.Multification; +import com.eternalcode.multification.notice.resolver.NoticeResolverRegistry; +import eu.okaeri.configs.serdes.OkaeriSerdesPack; +import eu.okaeri.configs.serdes.SerdesRegistry; +import org.jetbrains.annotations.ApiStatus.Experimental; +import org.jetbrains.annotations.NotNull; + +public class MultificationSerdesPack implements OkaeriSerdesPack { + + private final NoticeResolverRegistry noticeRegistry; + + public MultificationSerdesPack(NoticeResolverRegistry noticeRegistry) { + this.noticeRegistry = noticeRegistry; + } + + public MultificationSerdesPack(Multification multification) { + this.noticeRegistry = multification.getNoticeRegistry(); + } + + @Override + public void register(@NotNull SerdesRegistry registry) { + registry.register(new MultificationNoticeSerializer(this.noticeRegistry)); + } +} \ No newline at end of file diff --git a/multification-okaeri/test/com/eternalcode/multification/okaeri/NoticeSerializerTest.java b/multification-okaeri/test/com/eternalcode/multification/okaeri/NoticeSerializerTest.java new file mode 100644 index 0000000..d047195 --- /dev/null +++ b/multification-okaeri/test/com/eternalcode/multification/okaeri/NoticeSerializerTest.java @@ -0,0 +1,333 @@ +package com.eternalcode.multification.okaeri; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertNull; + +import com.eternalcode.example.bukkit.notice.BukkitNotice; +import com.eternalcode.example.bukkit.notice.BukkitNoticeKey; +import com.eternalcode.example.bukkit.notice.resolver.sound.SoundBukkit; +import com.eternalcode.example.bukkit.notice.resolver.sound.SoundBukkitResolver; +import com.eternalcode.multification.notice.Notice; +import com.eternalcode.multification.notice.NoticeKey; +import com.eternalcode.multification.notice.NoticePart; +import com.eternalcode.multification.notice.resolver.NoticeResolverDefaults; +import com.eternalcode.multification.notice.resolver.NoticeResolverRegistry; +import com.eternalcode.multification.notice.resolver.actionbar.ActionbarContent; +import com.eternalcode.multification.notice.resolver.chat.ChatContent; +import com.eternalcode.multification.notice.resolver.sound.SoundAdventure; +import com.eternalcode.multification.notice.resolver.title.TitleContent; +import com.eternalcode.multification.notice.resolver.title.TitleHide; +import com.eternalcode.multification.notice.resolver.title.TitleTimes; +import eu.okaeri.configs.ConfigManager; +import eu.okaeri.configs.OkaeriConfig; +import eu.okaeri.configs.yaml.snakeyaml.YamlSnakeYamlConfigurer; +import java.time.Duration; +import net.kyori.adventure.key.Key; +import org.bukkit.Sound; +import org.bukkit.SoundCategory; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +@SuppressWarnings("FieldMayBeFinal") +class NoticeSerializerTest { + + private static final NoticeResolverRegistry registry = NoticeResolverDefaults.createRegistry() + .registerResolver(new SoundBukkitResolver()); + + public static class ConfigEmpty extends OkaeriConfig { + Notice notice = Notice.empty(); + } + + @Test + @DisplayName("Should serialize and deserialize empty notice to empty entry") + void serializeEmptyNoticeToEmptyEntry() { + ConfigEmpty configEmpty = assertRender(new ConfigEmpty(), + """ + notice: {} + """ + ); + + assertEquals(0, configEmpty.notice.parts().size()); + } + + public static class ConfigOneLineChat extends OkaeriConfig { + Notice notice = Notice.chat("Hello world"); + } + + @Test + @DisplayName("Should serialize and deserialize simple chat notice to one line entry") + void serializeSimpleChatNoticeToOneLineEntry() { + ConfigOneLineChat oneLineChat = assertRender(new ConfigOneLineChat(), + """ + notice: "Hello world" + """ + ); + + assertEquals(1, oneLineChat.notice.parts().size()); + + NoticePart part = oneLineChat.notice.parts().get(0); + ChatContent chat = assertInstanceOf(ChatContent.class, part.content()); + assertEquals(NoticeKey.CHAT, part.noticeKey()); + assertEquals("Hello world", chat.messages().get(0)); + } + + + public static class ConfigMultiLineChat extends OkaeriConfig { + Notice notice = Notice.chat("First line", "Second line"); + } + @Test + @DisplayName("Should serialize simple chat notice to multiline entry") + void serializeSimpleChatNoticeToMultilineEntry() { + ConfigMultiLineChat configMultiLineChat = assertRender(new ConfigMultiLineChat(), + """ + notice: + - "First line" + - "Second line" + """); + + assertEquals(1, configMultiLineChat.notice.parts().size()); + + NoticePart part = configMultiLineChat.notice.parts().get(0); + ChatContent chat = assertInstanceOf(ChatContent.class, part.content()); + assertEquals(NoticeKey.CHAT, part.noticeKey()); + assertEquals("First line", chat.messages().get(0)); + assertEquals("Second line", chat.messages().get(1)); + } + + public static class ConfigSimpleTitle extends OkaeriConfig { + Notice notice = Notice.title("Hello world"); + } + @Test + @DisplayName("Should serialize simple title notice to title section") + void serializeSimpleTitleNoticeToOneLineEntry() { + ConfigSimpleTitle configSimpleTitle = assertRender(new ConfigSimpleTitle(), + """ + notice: + title: "Hello world" + """); + + assertEquals(1, configSimpleTitle.notice.parts().size()); + + NoticePart part = configSimpleTitle.notice.parts().get(0); + TitleContent title = assertInstanceOf(TitleContent.class, part.content()); + assertEquals(NoticeKey.TITLE, part.noticeKey()); + assertEquals("Hello world", title.content()); + } + + public static class ConfigFullTitle extends OkaeriConfig { + Notice notice = Notice.title("Title", "Subtitle", Duration.ofSeconds(1), Duration.ofSeconds(2), Duration.ofSeconds(1)); + } + @Test + @DisplayName("Should serialize title subtitle with delay notice to title section") + void serializeTitleSubtitleWithDelayNoticeToOneLineEntry() { + ConfigFullTitle configFullTitle = assertRender(new ConfigFullTitle(), + """ + notice: + title: "Title" + subtitle: "Subtitle" + times: "1s 2s 1s" + """); + + assertEquals(3, configFullTitle.notice.parts().size()); + + NoticePart titlePart = configFullTitle.notice.parts().get(0); + TitleContent title = assertInstanceOf(TitleContent.class, titlePart.content()); + assertEquals(NoticeKey.TITLE, titlePart.noticeKey()); + assertEquals("Title", title.content()); + + NoticePart subtitlePart = configFullTitle.notice.parts().get(1); + TitleContent subtitle = assertInstanceOf(TitleContent.class, subtitlePart.content()); + assertEquals(NoticeKey.SUBTITLE, subtitlePart.noticeKey()); + assertEquals("Subtitle", subtitle.content()); + + NoticePart timesPart = configFullTitle.notice.parts().get(2); + TitleTimes times = assertInstanceOf(TitleTimes.class, timesPart.content()); + assertEquals(NoticeKey.TITLE_TIMES, timesPart.noticeKey()); + assertEquals(1, times.fadeIn().getSeconds()); + assertEquals(2, times.stay().getSeconds()); + assertEquals(1, times.fadeOut().getSeconds()); + } + + public static class ConfigSimpleActionBar extends OkaeriConfig { + Notice notice = Notice.actionbar("Hello world"); + } + @Test + @DisplayName("Should serialize simple actionbar notice to actionbar section") + void serializeSimpleActionBarNoticeToOneLineEntry() { + ConfigSimpleActionBar configSimpleActionBar = assertRender(new ConfigSimpleActionBar(), + """ + notice: + actionbar: "Hello world" + """); + + assertEquals(1, configSimpleActionBar.notice.parts().size()); + + NoticePart part = configSimpleActionBar.notice.parts().get(0); + ActionbarContent actionbarContent = assertInstanceOf(ActionbarContent.class, part.content()); + assertEquals(NoticeKey.ACTION_BAR, part.noticeKey()); + assertEquals("Hello world", actionbarContent.content()); + } + + public static class ConfigHideTitle extends OkaeriConfig { + Notice notice = Notice.hideTitle(); + } + @Test + @DisplayName("Should serialize hide title notice with hide title property") + void serializeHideTitleNoticeWithHideTitleProperty() { + ConfigHideTitle configHideTitle = assertRender(new ConfigHideTitle(), + """ + notice: + hideTitle: 'true' + """); + + assertEquals(1, configHideTitle.notice.parts().size()); + + NoticePart part = configHideTitle.notice.parts().get(0); + assertInstanceOf(TitleHide.class, part.content()); + assertEquals(NoticeKey.TITLE_HIDE, part.noticeKey()); + } + + public static class ConfigSound extends OkaeriConfig { + Notice notice = BukkitNotice.sound(Sound.BLOCK_ANVIL_LAND, SoundCategory.MASTER, 1.0f, 1.0f); + } + @Test + @DisplayName("Should serialize sound notice with sound property") + void serializeSoundNoticeWithSoundProperty() { + ConfigSound configSound = assertRender(new ConfigSound(), + """ + notice: + sound: "BLOCK_ANVIL_LAND MASTER 1.0 1.0" + """); + + assertEquals(1, configSound.notice.parts().size()); + + NoticePart part = configSound.notice.parts().get(0); + SoundBukkit sound = assertInstanceOf(SoundBukkit.class, part.content()); + assertEquals(BukkitNoticeKey.SOUND, part.noticeKey()); + assertEquals(Sound.BLOCK_ANVIL_LAND, sound.sound()); + assertEquals(SoundCategory.MASTER, sound.category()); + assertEquals(1.0f, sound.volume()); + assertEquals(1.0f, sound.pitch()); + } + + public static class ConfigSoundWithoutCategory extends OkaeriConfig { + Notice notice = BukkitNotice.sound(Sound.BLOCK_ANVIL_LAND, 1.0f, 1.0f); + } + @Test + @DisplayName("Should serialize sound notice without category property") + void serializeSoundNoticeWithoutCategoryProperty() { + ConfigSoundWithoutCategory configSoundWithoutCategory = assertRender(new ConfigSoundWithoutCategory(), + """ + notice: + sound: "BLOCK_ANVIL_LAND 1.0 1.0" + """); + + assertEquals(1, configSoundWithoutCategory.notice.parts().size()); + + NoticePart part = configSoundWithoutCategory.notice.parts().get(0); + SoundBukkit sound = assertInstanceOf(SoundBukkit.class, part.content()); + assertEquals(BukkitNoticeKey.SOUND, part.noticeKey()); + assertEquals(Sound.BLOCK_ANVIL_LAND, sound.sound()); + assertNull(sound.category()); + assertEquals(1.0f, sound.volume()); + assertEquals(1.0f, sound.pitch()); + } + + + public static class ConfigSoundShort extends OkaeriConfig { + Notice notice = BukkitNotice.sound(Sound.BLOCK_ANVIL_LAND); + } + @Test + @DisplayName("Should serialize sound notice without volume and pitch") + void serializeSoundNoticeWithoutVolumeAndPitch() { + ConfigSoundShort configSoundShort = assertRender(new ConfigSoundShort(), + """ + notice: + sound: "BLOCK_ANVIL_LAND" + """); + + assertEquals(1, configSoundShort.notice.parts().size()); + + NoticePart part = configSoundShort.notice.parts().get(0); + SoundBukkit sound = assertInstanceOf(SoundBukkit.class, part.content()); + assertEquals(BukkitNoticeKey.SOUND, part.noticeKey()); + assertEquals(Sound.BLOCK_ANVIL_LAND, sound.sound()); + assertNull(sound.category()); + assertEquals(1.0f, sound.volumeOrDefault()); + assertEquals(1.0f, sound.pitchOrDefault()); + assertEquals(-1.0f, sound.volume()); + assertEquals(-1.0f, sound.pitch()); + } + + + public static class ConfigSoundAdventure extends OkaeriConfig { + Notice notice = Notice.sound(Key.key(Key.MINECRAFT_NAMESPACE, "entity.experience_orb.pickup"), net.kyori.adventure.sound.Sound.Source.MASTER, 1.0f, 1.0f); + } + @Test + @DisplayName("Should serialize adventure sound notice with sound property") + void serializeSoundNoticeWithSoundAdventureProperty() { + ConfigSoundAdventure configSound = assertRender(new ConfigSoundAdventure(), + """ + notice: + sound: "entity.experience_orb.pickup MASTER 1.0 1.0" + """); + + assertEquals(1, configSound.notice.parts().size()); + + NoticePart part = configSound.notice.parts().get(0); + SoundAdventure sound = assertInstanceOf(SoundAdventure.class, part.content()); + assertEquals(NoticeKey.SOUND, part.noticeKey()); + assertEquals("entity.experience_orb.pickup", sound.sound().value()); + assertEquals(net.kyori.adventure.sound.Sound.Source.MASTER, sound.category()); + assertEquals(1.0f, sound.volume()); + assertEquals(1.0f, sound.pitch()); + } + + ///TODO zmien wszystkie klasy na public + public static class ConfigSoundAdventureWithoutCategory extends OkaeriConfig { + Notice notice = Notice.sound(Key.key(Key.MINECRAFT_NAMESPACE, "entity.experience_orb.pickup"), 1.0f, 1.0f); + } + @Test + @DisplayName("Should serialize adventure sound notice without category property") + void serializeSoundNoticeWithoutCategoryAdventureProperty() { + ConfigSoundAdventureWithoutCategory configSound = assertRender(new ConfigSoundAdventureWithoutCategory(), + """ + notice: + sound: "entity.experience_orb.pickup 1.0 1.0" + """); + + assertEquals(1, configSound.notice.parts().size()); + + NoticePart part = configSound.notice.parts().get(0); + SoundAdventure sound = assertInstanceOf(SoundAdventure.class, part.content()); + assertEquals(NoticeKey.SOUND, part.noticeKey()); + assertEquals("entity.experience_orb.pickup", sound.sound().value()); + assertNull(sound.category()); + assertEquals(1.0f, sound.volume()); + assertEquals(1.0f, sound.pitch()); + } + + @SuppressWarnings("unchecked") + private T assertRender(T entity, String expected) { + entity.withConfigurer(new YamlSnakeYamlConfigurer(), new MultificationSerdesPack(registry)); + + String actual = entity.saveToString(); + + actual = removeBlankNewLines(actual); + expected = removeBlankNewLines(expected); + + assertEquals(expected, actual); + + return (T) entity.load(expected); + } + + private String removeBlankNewLines(String string) { + return string + .replaceAll("\"", "") + .replaceAll("\n+", "\n") + .replaceAll("\n+$", "") + .replaceAll("^\n+", ""); + } + +} diff --git a/settings.gradle.kts b/settings.gradle.kts index d4a3c0c..f85e277 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -2,6 +2,7 @@ rootProject.name = "multification" include("multification-core") include("multification-cdn") +include("multification-okaeri") include("multification-bukkit") include("examples:bukkit")