package com.mythlane.gravityflip.physics;
import com.hypixel.hytale.component.ComponentType;
import com.hypixel.hytale.component.Ref;
import com.hypixel.hytale.component.Store;
import com.hypixel.hytale.server.core.entity.UUIDComponent;
import com.hypixel.hytale.server.core.modules.entity.component.TransformComponent;
import com.hypixel.hytale.server.core.modules.physics.component.PhysicsValues;
import com.hypixel.hytale.server.core.universe.world.World;
import com.hypixel.hytale.server.core.universe.world.storage.EntityStore;
import com.mythlane.gravityflip.region.GravityFlipRegion;
import com.mythlane.gravityflip.region.RegionSnapshot;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Consumer;
/**
* Tick-driven service that toggles the native {@code PhysicsValues.invertedGravity} flag on every
* entity present in an enabled {@link GravityFlipRegion}. Mutations are queued via
* {@code CommandBuffer.replaceComponent(...)} inside a {@code Store.forEachEntityParallel(...)}
* lambda — the ECS engine commits them on the main thread after the parallel pass.
*
*
Dedup: a thread-safe {@link Set}{@code } tracks entities flipped at the previous tick.
* Entities that left an active region are restored to {@code invertedGravity=false} in a second
* pass (trade-off documented in the plan — second O(N) scan accepted v1).
*/
public final class GravityApplier {
// Lazy ComponentType holders — pattern identique à RegionRegistry.transform()
// (évite static-init Hytale PluginBase pendant les tests).
private static volatile ComponentType physicsType;
private static volatile ComponentType uuidType;
private static volatile ComponentType transformType;
private static ComponentType physicsType() {
ComponentType t = physicsType;
if (t == null) {
synchronized (GravityApplier.class) {
t = physicsType;
if (t == null) { t = PhysicsValues.getComponentType(); physicsType = t; }
}
}
return t;
}
private static ComponentType uuidType() {
ComponentType t = uuidType;
if (t == null) {
synchronized (GravityApplier.class) {
t = uuidType;
if (t == null) { t = UUIDComponent.getComponentType(); uuidType = t; }
}
}
return t;
}
private static ComponentType transformType() {
ComponentType t = transformType;
if (t == null) {
synchronized (GravityApplier.class) {
t = transformType;
if (t == null) { t = TransformComponent.getComponentType(); transformType = t; }
}
}
return t;
}
// THREADING: écrit/lu depuis les workers ECS via forEachEntityParallel → ConcurrentHashMap.newKeySet obligatoire.
private final Set previouslyInverted = ConcurrentHashMap.newKeySet();
private final Consumer errorHandler;
public GravityApplier(Consumer errorHandler) {
this.errorHandler = errorHandler == null ? t -> {} : errorHandler;
}
/** Tick entry point. NO-OP si world ou snapshot est null. */
public void apply(World world, RegionSnapshot snapshot) {
if (world == null || snapshot == null) return;
Collection enabledRegions = snapshot.byRegion().keySet();
// Stratégie figée (cf. WARNING 1 résolu) : on accepte la duplication du containsPosition
// (cf. must_haves trade-off) plutôt que de coupler à un nouveau contrat RegionSnapshot
// qui exposerait des UUIDs préformés. Coût : 2 pass O(N) sur le store par tick.
try {
Store store = world.getEntityStore().getStore();
ComponentType PHYST = physicsType();
ComponentType UUIDT = uuidType();
ComponentType TT = transformType();
// PASS 1 — pour chaque entité avec PhysicsValues : si dans une région activée, queue le flip ON
// via cmdBuf.replaceComponent ET enregistre l'UUID dans currentlyInRegion (thread-safe).
// THREADING: lambda sur worker ECS → currentlyInRegion = ConcurrentHashMap.newKeySet ; toutes les
// mutations PhysicsValues passent par cmdBuf (jamais replaceValues direct).
Set currentlyInRegion = ConcurrentHashMap.newKeySet();
store.forEachEntityParallel(PHYST, (index, chunk, cmdBuf) -> {
TransformComponent t;
UUIDComponent uc;
PhysicsValues v;
try {
t = chunk.getComponent(index, TT);
uc = chunk.getComponent(index, UUIDT);
v = chunk.getComponent(index, PHYST);
} catch (Throwable ignored) { return; }
if (t == null || uc == null || v == null) return;
com.hypixel.hytale.math.vector.Vector3d pos = t.getPosition();
double x = pos.x, y = pos.y, z = pos.z;
boolean inAnyRegion = false;
for (GravityFlipRegion r : enabledRegions) {
if (r.asBox().containsPosition(x, y, z)) { inAnyRegion = true; break; }
}
if (!inAnyRegion) return;
UUID u = uc.getUuid();
currentlyInRegion.add(u);
if (!v.isInvertedGravity()) {
Ref ref = chunk.getReferenceTo(index);
cmdBuf.replaceComponent(ref, PHYST,
new PhysicsValues(v.getMass(), v.getDragCoefficient(), true));
}
});
// PASS 2 — restore : pour chaque UUID dans previouslyInverted \ currentlyInRegion,
// re-localiser l'entité (second scan) et queue le flip OFF via cmdBuf.
// Trade-off perf accepté v1 — voir SUMMARY follow-up.
Set toRestore = ConcurrentHashMap.newKeySet();
toRestore.addAll(previouslyInverted);
toRestore.removeAll(currentlyInRegion);
if (!toRestore.isEmpty()) {
store.forEachEntityParallel(PHYST, (index, chunk, cmdBuf) -> {
UUIDComponent uc;
PhysicsValues v;
try {
uc = chunk.getComponent(index, UUIDT);
v = chunk.getComponent(index, PHYST);
} catch (Throwable ignored) { return; }
if (uc == null || v == null) return;
if (toRestore.contains(uc.getUuid()) && v.isInvertedGravity()) {
Ref ref = chunk.getReferenceTo(index);
cmdBuf.replaceComponent(ref, PHYST,
new PhysicsValues(v.getMass(), v.getDragCoefficient(), false));
}
});
}
// Update tracker — ces ops sont sur le tick thread après la fin du pass parallel.
// ConcurrentHashMap.newKeySet supporte clear()/addAll() concurrents safely.
previouslyInverted.clear();
previouslyInverted.addAll(currentlyInRegion);
} catch (Throwable th) {
errorHandler.accept(th);
}
}
/** Pure data-diff utilitaire pour tests unitaires (pas de runtime Hytale). */
public static DiffResult diff(Set previous, Set current) {
Set toFlip = new HashSet<>(current);
toFlip.removeAll(previous);
Set toRestore = new HashSet<>(previous);
toRestore.removeAll(current);
return new DiffResult(toFlip, toRestore);
}
public static final class DiffResult {
public final Set toFlip;
public final Set toRestore;
DiffResult(Set f, Set r) { this.toFlip = f; this.toRestore = r; }
}
// ---- Test hooks (package-private) ----
/** Vue immuable du tracker — pour tests unitaires sémantiques (résolution WARNING 2). */
Set previouslyInvertedView() {
return Collections.unmodifiableSet(previouslyInverted);
}
/**
* Force une valeur du tracker hors runtime ECS — pour tester la sémantique sans dépendre de World/Store.
* Package-private : NE PAS appeler depuis le code de production.
*/
void __updateTrackerForTest(Set newState) {
previouslyInverted.clear();
previouslyInverted.addAll(newState);
}
}