From d2e4ecbc3e446076c0af9e9ebf113535e6ecd729 Mon Sep 17 00:00:00 2001 From: theEvilReaper Date: Sun, 13 Sep 2026 17:20:32 +0200 Subject: [PATCH 1/7] feat(tag): add rank tag mapping --- .../cygnus/common/rank/RankTag.java | 97 +++++++++++++++++++ .../cygnus/common/rank/RankTagTest.java | 89 +++++++++++++++++ 2 files changed, 186 insertions(+) create mode 100644 common/src/main/java/net/onelitefeather/cygnus/common/rank/RankTag.java create mode 100644 common/src/test/java/net/onelitefeather/cygnus/common/rank/RankTagTest.java diff --git a/common/src/main/java/net/onelitefeather/cygnus/common/rank/RankTag.java b/common/src/main/java/net/onelitefeather/cygnus/common/rank/RankTag.java new file mode 100644 index 00000000..d7a6a19d --- /dev/null +++ b/common/src/main/java/net/onelitefeather/cygnus/common/rank/RankTag.java @@ -0,0 +1,97 @@ +package net.onelitefeather.cygnus.common.rank; + +import net.kyori.adventure.key.Key; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; + +import java.util.Locale; +import java.util.Optional; + +/** + * Maps each LuckPerms rank to its name tag icon in the {@code olf:rank_tags} resource pack font. + *

+ * The glyph is rendered {@link NamedTextColor#WHITE} because the pack's icons are full-color bitmaps + * rather than the grayscale masks vanilla glyphs use - any other color would tint the artwork instead + * of leaving it as designed. + *

+ * + * @author theEvilReaper + * @version 1.0.0 + * @since 1.0.0 + */ +public enum RankTag { + + ADMINISTRATOR(0xF0190), + ASSISTENT(0xF219B), + MOD(0xF119B), + CONTENT(0xF2190), + MEDIA(0xF2195), + LITE(0xF2191), + PLAYER(0xF1196); + + private final Component glyph; + + RankTag(int codepoint) { + this.glyph = Component.text(new String(Character.toChars(codepoint)), NamedTextColor.WHITE).font(rankFont()); + } + + /** + * Returns the {@code olf:rank_tags} resource pack font every {@link #glyph()} is drawn from. + *

+ * A method rather than a static field: enum constants are initialized before the class's other + * static fields, so a static field here would not yet be set while the constants above are built. + *

+ * + * @return the font key + */ + private static Key rankFont() { + return Key.key("olf", "rank_tags"); + } + + /** + * Returns the styled glyph component for this rank. + * + * @return the icon component + */ + public Component glyph() { + return glyph; + } + + /** + * Prepends this rank's icon and a space in front of the given name. + *

+ * The icon has to be appended as a child of a plain, font-less root rather than used as the root + * itself: Adventure components inherit style from their parent, so a root carrying + * {@code font(olf:rank_tags)} would leak that font onto the space and name appended after it, + * which has no letter glyphs and renders them as missing-character boxes in the client. + *

+ * + * @param name the name component to prefix + * @return the icon followed by a space and the given name + */ + public Component prefix(Component name) { + return Component.text() + .append(glyph) + .appendSpace() + .append(name) + .build(); + } + + /** + * Looks up the rank tag whose name matches a LuckPerms group id (e.g. {@code "administrator"}), + * case-insensitively. + * + * @param group the LuckPerms group id, or {@code null} + * @return the matching tag, or empty when the group is {@code null} or names no known rank + */ + public static Optional fromGroup(String group) { + if (group == null) { + return Optional.empty(); + } + try { + return Optional.of(RankTag.valueOf(group.toUpperCase(Locale.ROOT))); + } catch (IllegalArgumentException exception) { + return Optional.empty(); + } + } +} diff --git a/common/src/test/java/net/onelitefeather/cygnus/common/rank/RankTagTest.java b/common/src/test/java/net/onelitefeather/cygnus/common/rank/RankTagTest.java new file mode 100644 index 00000000..40b72a61 --- /dev/null +++ b/common/src/test/java/net/onelitefeather/cygnus/common/rank/RankTagTest.java @@ -0,0 +1,89 @@ +package net.onelitefeather.cygnus.common.rank; + +import net.kyori.adventure.key.Key; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; +import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer; +import org.junit.jupiter.api.Test; + +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class RankTagTest { + + private static final Key RANK_FONT = Key.key("olf", "rank_tags"); + + @Test + void testAdministratorGlyph() { + assertGlyph(RankTag.ADMINISTRATOR, 0xF0190); + } + + @Test + void testAssistentGlyph() { + assertGlyph(RankTag.ASSISTENT, 0xF219B); + } + + @Test + void testModGlyph() { + assertGlyph(RankTag.MOD, 0xF119B); + } + + @Test + void testContentGlyph() { + assertGlyph(RankTag.CONTENT, 0xF2190); + } + + @Test + void testMediaGlyph() { + assertGlyph(RankTag.MEDIA, 0xF2195); + } + + @Test + void testLiteGlyph() { + assertGlyph(RankTag.LITE, 0xF2191); + } + + @Test + void testPlayerGlyph() { + assertGlyph(RankTag.PLAYER, 0xF1196); + } + + @Test + void testPrefixDoesNotLeakTheRankFontOntoTheName() { + Component name = Component.text("theEvilReaper", NamedTextColor.GREEN); + Component prefixed = RankTag.ADMINISTRATOR.prefix(name); + + assertNull(prefixed.style().font(), "the rank font must not leak onto the space and name, or the client shows missing-glyph boxes"); + assertEquals(3, prefixed.children().size()); + assertEquals(RankTag.ADMINISTRATOR.glyph(), prefixed.children().get(0)); + assertEquals(Component.space(), prefixed.children().get(1)); + assertEquals(name, prefixed.children().get(2)); + + String plainText = PlainTextComponentSerializer.plainText().serialize(prefixed); + assertTrue(plainText.endsWith(" theEvilReaper")); + } + + @Test + void testFromGroupMatchesCaseInsensitively() { + assertEquals(Optional.of(RankTag.ADMINISTRATOR), RankTag.fromGroup("administrator")); + assertEquals(Optional.of(RankTag.ADMINISTRATOR), RankTag.fromGroup("Administrator")); + assertEquals(Optional.of(RankTag.PLAYER), RankTag.fromGroup("PLAYER")); + } + + @Test + void testFromGroupIsEmptyForUnknownOrNullGroup() { + assertEquals(Optional.empty(), RankTag.fromGroup("default")); + assertEquals(Optional.empty(), RankTag.fromGroup(null)); + } + + private void assertGlyph(RankTag tag, int expectedCodepoint) { + assertEquals(RANK_FONT, tag.glyph().style().font()); + assertEquals(NamedTextColor.WHITE, tag.glyph().style().color()); + + String expectedGlyph = new String(Character.toChars(expectedCodepoint)); + assertEquals(expectedGlyph, PlainTextComponentSerializer.plainText().serialize(tag.glyph())); + } +} From e6edeffa146f647dc8c07bb0fbcef1c6ed7f4316 Mon Sep 17 00:00:00 2001 From: theEvilReaper Date: Sun, 13 Sep 2026 17:25:28 +0200 Subject: [PATCH 2/7] chore(player): add rankTag method --- .../common/player/PermissionAwarePlayer.java | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/common/src/main/java/net/onelitefeather/cygnus/common/player/PermissionAwarePlayer.java b/common/src/main/java/net/onelitefeather/cygnus/common/player/PermissionAwarePlayer.java index f2790c4e..9d2c700e 100644 --- a/common/src/main/java/net/onelitefeather/cygnus/common/player/PermissionAwarePlayer.java +++ b/common/src/main/java/net/onelitefeather/cygnus/common/player/PermissionAwarePlayer.java @@ -2,6 +2,7 @@ import net.kyori.adventure.permission.PermissionChecker; import net.kyori.adventure.pointer.Pointers; +import net.kyori.adventure.text.Component; import net.kyori.adventure.util.TriState; import net.luckperms.api.LuckPermsProvider; import net.luckperms.api.model.user.User; @@ -11,6 +12,7 @@ import net.minestom.server.network.player.PlayerConnection; import net.onelitefeather.cygnus.common.permission.LuckPermsSupport; import net.onelitefeather.cygnus.common.permission.TriStates; +import net.onelitefeather.cygnus.common.rank.RankTag; import org.jetbrains.annotations.NotNull; /** @@ -75,4 +77,28 @@ public Pointers pointers() { QueryOptions queryOptions = LuckPermsProvider.get().getContextManager().getQueryOptions(this); return TriStates.fromLuckPerms(user.getCachedData().getPermissionData(queryOptions).checkPermission(permission)); } + + /** + * Resolves this player's name tag icon from their LuckPerms primary group. + * + * @return the matching {@link RankTag}, or {@link RankTag#PLAYER} when LuckPerms is absent, this + * player has no LuckPerms user data, or their primary group names no known rank + */ + public RankTag rankTag() { + if (!LuckPermsSupport.isPresent()) { + return RankTag.PLAYER; + } + User user = LuckPermsProvider.get().getUserManager().getUser(getUuid()); + if (user == null) { + return RankTag.PLAYER; + } + return RankTag.fromGroup(user.getPrimaryGroup()).orElse(RankTag.PLAYER); + } + + /** + * Sets this player's display name to their username prefixed with {@link #rankTag()}. + */ + public void applyRankTagDisplayName() { + setDisplayName(rankTag().prefix(Component.text(getUsername()))); + } } From 561f066dd78e8b68a143680e49cb76e858dc5a14 Mon Sep 17 00:00:00 2001 From: theEvilReaper Date: Sun, 13 Sep 2026 17:25:38 +0200 Subject: [PATCH 3/7] chore(game): wire rank tag usage --- .../cygnus/listener/PlayerSpawnListener.java | 4 +++ .../cygnus/phase/RestartPhase.java | 28 +++++++++++++++++++ .../listener/PlayerSpawnListenerTest.java | 4 ++- 3 files changed, 35 insertions(+), 1 deletion(-) diff --git a/game/src/main/java/net/onelitefeather/cygnus/listener/PlayerSpawnListener.java b/game/src/main/java/net/onelitefeather/cygnus/listener/PlayerSpawnListener.java index a7a34443..d3aba255 100644 --- a/game/src/main/java/net/onelitefeather/cygnus/listener/PlayerSpawnListener.java +++ b/game/src/main/java/net/onelitefeather/cygnus/listener/PlayerSpawnListener.java @@ -8,6 +8,7 @@ import net.minestom.server.event.player.PlayerSpawnEvent; import net.onelitefeather.cygnus.common.Messages; import net.onelitefeather.cygnus.common.Tags; +import net.onelitefeather.cygnus.common.player.PermissionAwarePlayer; import net.onelitefeather.cygnus.phase.LobbyPhase; import java.util.function.Consumer; @@ -36,6 +37,9 @@ public void accept(PlayerSpawnEvent event) { player.setDisplayName(Component.text(player.getUsername())); if (phaseSupplier.get() instanceof LobbyPhase lobbyPhase) { + if (player instanceof PermissionAwarePlayer permissionAwarePlayer) { + permissionAwarePlayer.applyRankTagDisplayName(); + } Broadcaster.broadcast(Messages.getJoinMessage(player)); this.spawnSupplier.accept(player); lobbyPhase.setLevel(player); diff --git a/game/src/main/java/net/onelitefeather/cygnus/phase/RestartPhase.java b/game/src/main/java/net/onelitefeather/cygnus/phase/RestartPhase.java index 6ac0132e..895c1cef 100644 --- a/game/src/main/java/net/onelitefeather/cygnus/phase/RestartPhase.java +++ b/game/src/main/java/net/onelitefeather/cygnus/phase/RestartPhase.java @@ -8,6 +8,7 @@ import net.minestom.server.entity.Player; import net.onelitefeather.cygnus.common.Messages; import net.onelitefeather.cygnus.common.bootstrap.ServiceShutdown; +import net.onelitefeather.cygnus.common.player.PermissionAwarePlayer; import java.time.temporal.ChronoUnit; @@ -32,6 +33,33 @@ public RestartPhase() { this.setEndTicks(-1); } + /** + * Re-applies every online player's rank tag to their tab list name. + *

+ * The round overwrote it with a {@link net.onelitefeather.cygnus.team.RoleIcon} (Slender/Survivor) + * or a struck-through spectator name; the restart lobby shows rank instead of round role again. + *

+ */ + @Override + public void onStart() { + super.onStart(); + resetDisplayNames(); + } + + /** + * Sets every online player's display name back to their rank tag. + *

+ * Package-private so it can be exercised directly in tests without going through {@link #onStart()}, + * which also schedules the phase's repeating update task. + *

+ */ + void resetDisplayNames() { + for (Player player : MinecraftServer.getConnectionManager().getOnlinePlayers()) { + if (player instanceof PermissionAwarePlayer permissionAwarePlayer) { + permissionAwarePlayer.applyRankTagDisplayName(); + } + } + } /** * Ends the process once the countdown has run out. diff --git a/game/src/test/java/net/onelitefeather/cygnus/listener/PlayerSpawnListenerTest.java b/game/src/test/java/net/onelitefeather/cygnus/listener/PlayerSpawnListenerTest.java index e7b6b842..5c187f08 100644 --- a/game/src/test/java/net/onelitefeather/cygnus/listener/PlayerSpawnListenerTest.java +++ b/game/src/test/java/net/onelitefeather/cygnus/listener/PlayerSpawnListenerTest.java @@ -8,6 +8,7 @@ import net.onelitefeather.cygnus.CygnusPlayerTestBase; import net.onelitefeather.cygnus.common.Tags; import net.onelitefeather.cygnus.common.config.GameConfig; +import net.onelitefeather.cygnus.common.rank.RankTag; import net.onelitefeather.cygnus.jumpscare.JumpScareManager; import net.onelitefeather.cygnus.phase.GamePhase; import net.onelitefeather.cygnus.phase.LobbyPhase; @@ -41,7 +42,8 @@ void testFirstSpawnInLobbyPhaseSetsDisplayNameAndTeleports(@NotNull Env env) { listener.accept(firstSpawn); assertTrue(spawned.get(), "Spawn supplier must be called on first spawn in lobby"); - assertEquals(Component.text(player.getUsername()), player.getDisplayName()); + assertEquals(RankTag.PLAYER.prefix(Component.text(player.getUsername())), player.getDisplayName(), + "the lobby must show the player's rank tag (LuckPerms is absent in tests, so it falls back to RankTag.PLAYER)"); env.destroyInstance(instance, true); } From a5d6326e054d80c198a972e22e8cd7ec9769a96a Mon Sep 17 00:00:00 2001 From: theEvilReaper Date: Sun, 13 Sep 2026 17:25:48 +0200 Subject: [PATCH 4/7] test(player): add new cases --- .../PermissionAwarePlayerRankTagTest.java | 68 +++++++++++++++++++ 1 file changed, 68 insertions(+) create mode 100644 common/src/test/java/net/onelitefeather/cygnus/common/player/PermissionAwarePlayerRankTagTest.java diff --git a/common/src/test/java/net/onelitefeather/cygnus/common/player/PermissionAwarePlayerRankTagTest.java b/common/src/test/java/net/onelitefeather/cygnus/common/player/PermissionAwarePlayerRankTagTest.java new file mode 100644 index 00000000..31b3cfbd --- /dev/null +++ b/common/src/test/java/net/onelitefeather/cygnus/common/player/PermissionAwarePlayerRankTagTest.java @@ -0,0 +1,68 @@ +package net.onelitefeather.cygnus.common.player; + +import net.kyori.adventure.text.Component; +import net.minestom.server.entity.Player; +import net.minestom.server.instance.Instance; +import net.minestom.server.network.player.GameProfile; +import net.minestom.server.network.player.PlayerConnection; +import net.minestom.testing.Env; +import net.minestom.testing.extension.MicrotusExtension; +import net.onelitefeather.cygnus.common.rank.RankTag; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; + +/** + * Verifies that a player falls back to {@link RankTag#PLAYER} without LuckPerms present, which is + * the state every test run is in. + * + * @author theEvilReaper + * @version 1.0.0 + * @since 1.0.0 + */ +@ExtendWith(MicrotusExtension.class) +class PermissionAwarePlayerRankTagTest { + + @BeforeAll + static void setUp(Env env) { + env.process().connection().setPlayerProvider(TestPlayer::new); + } + + @Test + void testRankTagFallsBackToPlayerWithoutLuckPerms(Env env) { + Instance instance = env.createFlatInstance(); + Player player = env.createPlayer(instance); + + PermissionAwarePlayer permissionAware = assertInstanceOf(PermissionAwarePlayer.class, player); + assertEquals(RankTag.PLAYER, permissionAware.rankTag()); + + env.destroyInstance(instance, true); + } + + @Test + void testApplyRankTagDisplayNameSetsThePrefixedName(Env env) { + Instance instance = env.createFlatInstance(); + Player player = env.createPlayer(instance); + + PermissionAwarePlayer permissionAware = assertInstanceOf(PermissionAwarePlayer.class, player); + permissionAware.applyRankTagDisplayName(); + + assertEquals(RankTag.PLAYER.prefix(Component.text(player.getUsername())), player.getDisplayName()); + + env.destroyInstance(instance, true); + } + + /** + * A player which adds nothing to {@link PermissionAwarePlayer}, so the test observes the + * rank tag handling of the base class and nothing else. + */ + private static final class TestPlayer extends PermissionAwarePlayer { + + private TestPlayer(PlayerConnection playerConnection, GameProfile gameProfile) { + super(playerConnection, gameProfile); + } + } +} From 25ecfe6217ced62a2cc510ff5f57f392ee1b7b71 Mon Sep 17 00:00:00 2001 From: theEvilReaper Date: Sun, 13 Sep 2026 17:26:08 +0200 Subject: [PATCH 5/7] test(phase): add new cases --- .../cygnus/phase/RestartPhaseTest.java | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 game/src/test/java/net/onelitefeather/cygnus/phase/RestartPhaseTest.java diff --git a/game/src/test/java/net/onelitefeather/cygnus/phase/RestartPhaseTest.java b/game/src/test/java/net/onelitefeather/cygnus/phase/RestartPhaseTest.java new file mode 100644 index 00000000..b4496e55 --- /dev/null +++ b/game/src/test/java/net/onelitefeather/cygnus/phase/RestartPhaseTest.java @@ -0,0 +1,31 @@ +package net.onelitefeather.cygnus.phase; + +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; +import net.minestom.server.entity.Player; +import net.minestom.server.instance.Instance; +import net.minestom.testing.Env; +import net.onelitefeather.cygnus.CygnusPlayerTestBase; +import net.onelitefeather.cygnus.common.rank.RankTag; +import net.onelitefeather.cygnus.team.RoleIcon; +import org.jetbrains.annotations.NotNull; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class RestartPhaseTest extends CygnusPlayerTestBase { + + @Test + void testResetDisplayNamesReplacesTheRoundRoleIconWithTheRankTag(@NotNull Env env) { + Instance instance = env.createFlatInstance(); + Player player = env.createPlayer(instance); + player.setDisplayName(RoleIcon.SLENDER.prefix(Component.text(player.getUsername(), NamedTextColor.GRAY))); + + new RestartPhase().resetDisplayNames(); + + assertEquals(RankTag.PLAYER.prefix(Component.text(player.getUsername())), player.getDisplayName(), + "the restart lobby must show the rank tag again, not the round's role icon (LuckPerms is absent in tests, so it falls back to RankTag.PLAYER)"); + + env.destroyInstance(instance, true); + } +} From 6161b2d80a18c2ac332bd06aa8deddece5e59e65 Mon Sep 17 00:00:00 2001 From: Joltras Date: Sun, 13 Sep 2026 17:31:12 +0200 Subject: [PATCH 6/7] chore(tag): remove black shadow --- .../net/onelitefeather/cygnus/common/rank/RankTag.java | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/common/src/main/java/net/onelitefeather/cygnus/common/rank/RankTag.java b/common/src/main/java/net/onelitefeather/cygnus/common/rank/RankTag.java index d7a6a19d..ecac669f 100644 --- a/common/src/main/java/net/onelitefeather/cygnus/common/rank/RankTag.java +++ b/common/src/main/java/net/onelitefeather/cygnus/common/rank/RankTag.java @@ -3,6 +3,7 @@ import net.kyori.adventure.key.Key; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.format.NamedTextColor; +import net.kyori.adventure.text.format.ShadowColor; import java.util.Locale; import java.util.Optional; @@ -12,7 +13,8 @@ *

* The glyph is rendered {@link NamedTextColor#WHITE} because the pack's icons are full-color bitmaps * rather than the grayscale masks vanilla glyphs use - any other color would tint the artwork instead - * of leaving it as designed. + * of leaving it as designed. The client's default drop shadow is disabled for the same reason: it is + * meant for flat glyph masks and just muddies a full-color bitmap. *

* * @author theEvilReaper @@ -32,7 +34,9 @@ public enum RankTag { private final Component glyph; RankTag(int codepoint) { - this.glyph = Component.text(new String(Character.toChars(codepoint)), NamedTextColor.WHITE).font(rankFont()); + this.glyph = Component.text(new String(Character.toChars(codepoint)), NamedTextColor.WHITE) + .font(rankFont()) + .shadowColor(ShadowColor.none()); } /** From d13f6311efe9fd5791713b00bb162a0e5366bfd8 Mon Sep 17 00:00:00 2001 From: Joltras Date: Sun, 13 Sep 2026 17:36:46 +0200 Subject: [PATCH 7/7] chore(tag): improve annotation handling --- .../net/onelitefeather/cygnus/common/rank/RankTag.java | 9 ++++----- .../onelitefeather/cygnus/common/rank/package-info.java | 4 ++++ 2 files changed, 8 insertions(+), 5 deletions(-) create mode 100644 common/src/main/java/net/onelitefeather/cygnus/common/rank/package-info.java diff --git a/common/src/main/java/net/onelitefeather/cygnus/common/rank/RankTag.java b/common/src/main/java/net/onelitefeather/cygnus/common/rank/RankTag.java index ecac669f..a018b639 100644 --- a/common/src/main/java/net/onelitefeather/cygnus/common/rank/RankTag.java +++ b/common/src/main/java/net/onelitefeather/cygnus/common/rank/RankTag.java @@ -4,6 +4,7 @@ import net.kyori.adventure.text.Component; import net.kyori.adventure.text.format.NamedTextColor; import net.kyori.adventure.text.format.ShadowColor; +import org.jetbrains.annotations.Nullable; import java.util.Locale; import java.util.Optional; @@ -19,7 +20,7 @@ * * @author theEvilReaper * @version 1.0.0 - * @since 1.0.0 + * @since 2.15.0 */ public enum RankTag { @@ -88,10 +89,8 @@ public Component prefix(Component name) { * @param group the LuckPerms group id, or {@code null} * @return the matching tag, or empty when the group is {@code null} or names no known rank */ - public static Optional fromGroup(String group) { - if (group == null) { - return Optional.empty(); - } + public static Optional fromGroup(@Nullable String group) { + if (group == null) return Optional.empty(); try { return Optional.of(RankTag.valueOf(group.toUpperCase(Locale.ROOT))); } catch (IllegalArgumentException exception) { diff --git a/common/src/main/java/net/onelitefeather/cygnus/common/rank/package-info.java b/common/src/main/java/net/onelitefeather/cygnus/common/rank/package-info.java new file mode 100644 index 00000000..47c2df3b --- /dev/null +++ b/common/src/main/java/net/onelitefeather/cygnus/common/rank/package-info.java @@ -0,0 +1,4 @@ +@NotNullByDefault +package net.onelitefeather.cygnus.common.rank; + +import org.jetbrains.annotations.NotNullByDefault; \ No newline at end of file