Skip to content

Conversation

@P1otrulla
Copy link
Member

@P1otrulla P1otrulla commented Oct 12, 2025

* Register new placeholders for `msg_toggle` and `socialspy_status` in `MsgPlaceholderSetup`.
* Extend `MsgMessages` with a `Placeholders` interface and implement language-specific placeholder formats in `ENMsgMessages` and `PLMsgMessages`.
* Add caching logic for `msg_toggle` state in `MsgPlaceholderSetup`.
* Introduce new formatted placeholders for `msg_toggle` and `socialspy_status`.
* Extend `MsgMessages.Placeholders` to support a loading state with translations.
…erAPI`

* Adjust cache duration in `MsgPlaceholderSetup` from 200ms to 5s.
* Rename `msg_toggle` placeholders to `msg_status` for consistency.
* Extend `ENPlaceholders` and `PLPlaceholders` with `OkaeriConfig`.
* Add `PlaceholderAPI` dependency in `runServer` task configuration.
* Replace existing cache in `MsgPlaceholderSetup` with `AsyncPlaceholderCached`.
* Introduce `AsyncPlaceholderCacheRegistry` for managing placeholder caches.
* Add `AsyncPlaceholderCacheController` to handle cache invalidation on player quit.
* Simplify placeholder logic and remove redundant caching methods.
…derCacheRegistry`

* Replace individual `stateCache` with dynamically registered cache in the `AsyncPlaceholderCacheRegistry`.
* Simplify placeholder setup logic and adjust `MsgToggleService` to utilize the registry for cache management.
…lidation

* Add `Subscribe` annotations to invalidate cache on `EternalReloadEvent` and `EternalShutdownEvent`.
* Rename `onEternalDisable` to `onDisable` and `onReload`.
Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a new asynchronous caching mechanism for placeholders and integrates it with the messaging feature. The changes include a new AsyncPlaceholderCacheRegistry and AsyncPlaceholderCached for managing async data loading, new placeholders for message and social spy status, and refactoring MsgToggleServiceImpl to use the new caching system. The overall implementation is good, but I've found a few areas for improvement. There's a race condition in AsyncPlaceholderCached that could lead to multiple concurrent loads for the same data, some code duplication in MsgPlaceholderSetup when defining similar placeholders, and a minor inconsistency in a comment in one of the message files. I've left specific comments with suggestions on how to address these points.

@P1otrulla P1otrulla changed the title Msg placeholders & Async placeholder for future GH-930 Msg placeholders & Async placeholder for future Oct 12, 2025
@P1otrulla P1otrulla requested a review from noyzys October 12, 2025 12:12
}

@Subscribe(EternalInitializeEvent.class)
void setUpPlaceholders(PlaceholderRegistry placeholderRegistry) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Proponowalbym poprawic nazewnictwo metody obecnie jest to ogolnikowe a nie jasne ze chodzi o rejestracje placeholderow, czyli jezeli jest kompozycja na registry dajemy setUpPlaceholders -> registerMessagePlaceholders, lub samo registryPlaceholders ale jezeli wciagamy "msg" to Message

player -> String.valueOf(this.msgService.isSpy(player.getUniqueId()))
));

placeholderRegistry.registerPlaceholder(PlaceholderReplacer.of(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Takie wyrazenia w scope mozna byloby wyizolowac do prywatnej metody przykladowo

private String socialSpyStatusFormatted(UUID uuid, Translation translation) {
    return msgService.isSpy(uuid)
        ? translation.msg().placeholders().socialSpyEnabled()
        : translation.msg().placeholders().socialSpyDisabled();
}

void setUpPlaceholders(PlaceholderRegistry placeholderRegistry) {
Translation translation = this.translationManager.getMessages();

AsyncPlaceholderCached<MsgState> stateCache = this.cacheRegistry.register(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Narazie wystarczy ale w przyszlosci jezeli bedziesz planowac w roadmapie dalej zasilac w pelni asynchroniczne placeholdery to wypadaloby zwracac Cfa i opakowywac go w calosci


placeholderRegistry.registerPlaceholder(PlaceholderReplacer.of(
"socialspy_status",
player -> String.valueOf(this.msgService.isSpy(player.getUniqueId()))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Obecny auto boxing zamienic na Boolean#toString intuicyjniej bardziej i wyizolowac do prywatnej metody cos w stylu:

private String getSocialSpyStatus(UUID uuid) {
    return Boolean.toString(msgService.isSpy(uuid));
}

@Controller
public class MsgPlaceholderSetup {

public static final String MSG_STATE_CACHE_KEY = "msg_state";
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tutaj mozna byloby rozwazyc wrzucenie dodatkowych stalych globalnych na sama mozliwosc kontrolowania centralnia czas cache zamiast overidowac w metodzie rejeestracji

.build();
}

public T getCached(UUID uuid) {
Copy link
Member

@noyzys noyzys Oct 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

return null - jak value nie jest jeszcze dostepna to wymaga obslugi null majac ze soba ryzuko rzucenia NPE.
Obecnie nie zwraca pelnej informacji o tym ze loading odbywa sie cyklu async.

  • Proponowalbym zwracac CFA dlaczego lepiej? - bo zwracamy CFA zamiast null od razu wiemy ze score jest cyklem async,
  • -Mozna dorzucic null check na UUID uzyskujemy safe API
  • Izolacja logiki ladowania do loadAsync - czystsza seperacja wlasciwej odpowiedzialnosci.
  • Obslugujemy async ladowanie danych z pola loader i aktualizujemy cache,
  • Zapobiegamy wielokrotnemu ruszaniu loadera dla tego samego UUID
  • Wazne zeby logika bla non-blocking przy Paper API nie blokujac tickow serwera
  • Ogolnie to najlepiej opakowac takie logiki w jakis error handling dla spokoju i wytrwalosci

Finalnie by to wjechalo tak:

    public CompletableFuture<T> getCached(UUID uuid) {
        // ewentualny null check dla UUID
        T cached = cache.getIfPresent(uuid);
        if (cached != null) {
            return CompletableFuture.completedFuture(cached);
        }

        return loading.computeIfAbsent(uuid, this::loadAsync);
    }

    private CompletableFuture<T> loadAsync(UUID uuid) {
        return loader.apply(uuid).whenComplete((value, throwable) -> {
            if (value != null) {
                cache.put(uuid, value);
            }

            loading.remove(uuid); 
        });
    }

public AsyncPlaceholderCacheRegistry() {
}

public <T> AsyncPlaceholderCached<T> register(String key, Function<UUID, CompletableFuture<T>> loader) {
Copy link
Member

@noyzys noyzys Oct 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moze byc taka sytuacja ze key juz istnieje w mapie ale zostanie nadpisany bez ostrzezenia to mamy niestety utrate cache,
Minusem troche moze byc to ze nie ma konfiguracji loadera z timeoutem,
Null-check dla key i loader i ewentualne rzucenie wyjatku kiedy klucz juz istnieje na runtime, przekazywanie timeoutu loadera do PlaceholderCached to samo w metodach nizej.
Wlasciwa walidacja do logiki tutaj bylaby bardzo potrzebna jedna metoda zalatwiasz cala walidacje przykladowo:

private <T> void validateRegistrationParameters(String key, Function<UUID, CompletableFuture<T>> loader, Duration expireAfterWrite) {
    if (key == null || key.trim().isEmpty()) {
        throw new IllegalArgumentException("Cache key cannot be null or empty");
    }

    if (loader == null) {
        throw new IllegalArgumentException("Loader function cannot be null");
    }

    if (expireAfterWrite == null) {
        throw new IllegalArgumentException("Expire duration cannot be null");
    }

    if (expireAfterWrite.isNegative() || expireAfterWrite.isZero()) {
        throw new IllegalArgumentException("Expire duration must be positive");
    }
}

Wszystko tutaj mozna rozbudowac i rozszerzyc na jakas konfiguracje jezeli potrzeba w obecnym stanie jezeli nie - zostawiamy i tyle

import java.util.function.Consumer;

@Service
class MsgToggleServiceImpl implements MsgToggleService {
Copy link
Member

@noyzys noyzys Oct 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rozbilbym to na kilka innych komponentow z ich odpowiedzialnoscia z kosztem korzysci,

  • MessageStateCache -> operacja na cache, izolacja logiki cache,
  • MessageStateToggleCalculator -> logika biznesowa od samego toggle w zamian za kalkulacje i invertowanie,
  • MessageStateUpdateCoordinator -> koordynacja samym update cos ala transaction-like,
  • MessageToggleServiceImpl -> Orchestracja uzyskujemy cienka warstwe ktora koordynuje tylko komponenty

Wszystko wchodzi do glownego MsgToggleService. Wyzej zaznaczylem Message ale to wiadomo o co raczej chodzi

    @Override
    public CompletableFuture<Void> setState(UUID playerId, MsgState state) {
        return updateCoordinator.updateState(playerId, state);
    }

// i tak dalej:
MsgState newState = toggleCalculator.calculateToggledState(currentState);

Copy link
Member

@noyzys noyzys left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Jezeli cos niezrozumialego smialo pytac lub dawac wlasne propozycje zmian ;).
Wszystko mozna podkrecic i jezeli jest jakis problem nie sugerowac sie moimi zmianami bo zawsze jest jakies ala w trakcie review


placeholderRegistry.registerPlaceholder(PlaceholderReplacer.of(
"socialspy_status",
player -> String.valueOf(this.msgService.isSpy(player.getUniqueId()))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

microoptimizations: use Boolean#toString instead of String.valueOf

Suggested change
player -> String.valueOf(this.msgService.isSpy(player.getUniqueId()))
player -> Boolean.toString(this.msgService.isSpy(player.getUniqueId()))

@Getter
@Accessors(fluent = true)
public static class PLPlaceholders extends OkaeriConfig implements MsgMessages.Placeholders {
private String loading = "<yellow>Ładowanie...";
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
private String loading = "<yellow>Ładowanie...";
private String loading = "<yellow>Wczytywanie...";

Wczytywanie (w kontekście danych) tutaj brzmi lepiej, ładowanie bardziej się kojarzy z jakimiś intensywnymi zadaniami

@P1otrulla P1otrulla requested a review from Qbiterv October 12, 2025 22:37

@Getter
@Accessors(fluent = true)
public static class ENPlaceholders extends OkaeriConfig implements MsgMessages.Placeholders {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This section is too general; you should move it to the feature-related section. Creating such a section makes it similar to PlayerSection/ItemSection in the past. Currently, it only has a few messages, but over time, it will become very messy. Alternatively, I suggest you move it to the placeholders.yml file, but remember to also divide it into feature-related sections.

AsyncPlaceholderCached<MsgState> stateCache = this.cacheRegistry.register(
MSG_STATE_CACHE_KEY,
this.msgToggleRepository::getPrivateChatState,
Duration.ofMinutes(10)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

move to private static variable, maybe?

public Notice socialSpyEnable = Notice.chat("<green>► <white>SocialSpy has been {STATE}<white>!");
public Notice socialSpyDisable = Notice.chat("<red>► <white>SocialSpy has been {STATE}<white>!");

@Comment("# Formatowanie placeholderów")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Gemini is right 👍

@vLuckyyy vLuckyyy requested review from Rollczi and removed request for Qbiterv October 19, 2025 12:38
Copy link
Member

@Rollczi Rollczi left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 komentarz na razie bo może być dużo do przerobienia


private final MsgToggleRepository msgToggleRepository;
private final ConcurrentHashMap<UUID, MsgState> cachedToggleStates;
private final AsyncPlaceholderCacheRegistry cacheRegistry;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

słabe rozwiązanie, najlepiej jakby tego cache nie było w repo + nie było tutaj nic placeholderach, bo to tworzy dziwne zależności. Raczej placeholdery widzą API serisów i tyle. Serwisy nie powinny widzieć placeholders

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add a placeholder for the /msgtoggle command. i.e: %eternalcore_msgtoggle%

8 participants