Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 12 additions & 4 deletions src/main/java/net/tfminecraft/magic/gear/GearStationStore.java
Original file line number Diff line number Diff line change
Expand Up @@ -156,10 +156,16 @@ public static Occupancy deposit(org.bukkit.entity.Player player, net.tfminecraft
return net.tfminecraft.magic.gear.ownership.GearEffects.deposit(player, token, location, item, charged, rested, afterInventory, beforeInventory);
}

public static Occupancy install(Location location, ItemStack item, Map<Object, Object> row) throws IOException {
/**
* Caches the item read back from the row, as a restart would. Paper can write a freshly
* built item's components in another order than its re-read copy, so caching the original
* would make {@link #expected} refuse the station until the next restart.
*/
public static Occupancy install(Location location, Map<Object, Object> row) throws IOException {
UUID owner = row.get("owner") instanceof String text ? uuidOf(text) : null;
ItemStack item = decodeItem(row);
UUID displayId = spawnDisplay(location, item);
Occupancy occupancy = new Occupancy(item.clone(), displayId, owner, readChargedRaw(row), Boolean.TRUE.equals(row.get("rested")));
Occupancy occupancy = new Occupancy(item, displayId, owner, readChargedRaw(row), Boolean.TRUE.equals(row.get("rested")));
occupancy.generation = net.tfminecraft.magic.gear.ownership.GearPropertyStore.id(row);
if (row.get("ownership") instanceof Map<?, ?> authority && "CHARACTER".equals(authority.get("kind")))
occupancy.characterOwner = net.tfminecraft.magic.gear.ownership.GearPropertyStore.owner(row);
Expand Down Expand Up @@ -225,8 +231,10 @@ public static boolean update(Location location, Occupancy captured, ItemStack it
var station = expected(location, captured);
var after = net.tfminecraft.magic.gear.ownership.GearYaml.copy(station.raw());
after.put("item-bytes", net.tfminecraft.magic.gear.ownership.GearPropertyStore.b64(net.tfminecraft.magic.gear.ownership.GearItems.bytes(item)));
after.put("effect-state", "active"); property().patch(station, after);
captured.setItem(item.clone()); captured.setOrbSessionActive(false); return true;
after.put("effect-state", "active");
ItemStack cached = decodeItem(after); // the re-read copy, as in install
property().patch(station, after);
captured.setItem(cached); captured.setOrbSessionActive(false); return true;
} catch (IOException ex) { warning(ex); return false; }
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ public static GearStationStore.Occupancy deposit(Player player, GearIdentity.Tok
}
player.getInventory().setContents(cloneContents(inventoryAfter)); player.updateInventory(); InventoryCheckpoint.save(player);
var expected = store.station(operation); var after = GearYaml.copy(expected.raw()); after.put("effect-state", "active"); store.patch(expected, after);
return GearStationStore.install(location, item, after);
return GearStationStore.install(location, after);
} catch (IOException | RuntimeException ex) { hold(operation); GearStationStore.warning(ex); return null; }
}
public static boolean charge(Player player, GearIdentity.Token token, Location station, ItemStack hand,
Expand Down
3 changes: 2 additions & 1 deletion src/test/java/net/tfminecraft/magic/GearCoverageSupport.java
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ abstract class GearCoverageSupport {
org.mockito.MockedStatic<net.tfminecraft.magic.gear.ownership.InventoryCheckpoint> checkpoint;
org.mockito.MockedStatic<net.tfminecraft.magic.gear.ownership.GearItems> codec;
Map<java.lang.reflect.Field, Object> orbConfig = new HashMap<>();
/** Fixture bytes: equal items always serialize to the same id. */
final Map<String, ItemStack> snapshots = new LinkedHashMap<>();

@BeforeEach
void setupGear() throws Exception {
Expand All @@ -34,7 +36,6 @@ void setupGear() throws Exception {
when(Magic.plugin.getDataFolder()).thenReturn(temp.toFile());
characters = new GearCharacterFixture();
checkpoint = mockStatic(net.tfminecraft.magic.gear.ownership.InventoryCheckpoint.class);
var snapshots = new LinkedHashMap<String, ItemStack>();
codec = mockStatic(net.tfminecraft.magic.gear.ownership.GearItems.class);
codec.when(() -> net.tfminecraft.magic.gear.ownership.GearItems.bytes(any())).thenAnswer(call -> {
ItemStack item = call.getArgument(0);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -353,15 +353,15 @@ void installPreservesExplicitOwnerlessAndCannotPublishAnUnknownAuthority() throw
var occupancy = seed(location, item(), null, Map.of());
patch(occupancy, row -> row.put("owner", 7));
var ownerless = GearStationStore.property().station(occupancy.generation()).raw();
var installed = GearStationStore.install(location, item(), ownerless);
var installed = GearStationStore.install(location, ownerless);
assertNull(installed.getOwner());
assertNull(installed.characterOwner());
assertEquals(occupancy.generation(), installed.generation());
assertDoesNotThrow(() -> GearStationStore.expected(location, installed));
patch(installed, row -> row.put("ownership", "unrecognized-envelope"));
String held = Files.readString(source());
var raw = GearStationStore.property().station(installed.generation()).raw();
var unknown = GearStationStore.install(location, item(), raw);
var unknown = GearStationStore.install(location, raw);
assertNull(unknown.characterOwner());
assertThrows(IOException.class, () -> GearStationStore.expected(location, unknown));
assertFalse(GearStationStore.update(location, unknown, attuned()));
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package net.tfminecraft.magic;

import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.any;

import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.Map;
import net.tfminecraft.magic.gear.GearStationStore;
import net.tfminecraft.magic.gear.ownership.GearItems;
import net.tfminecraft.tlibs.character.CharacterSession;
import org.bukkit.inventory.ItemStack;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

/**
* Paper can serialize a freshly built item differently from the copy it reads back (the same
* components in another order). The station cache must hold the re-read copy, or every charge
* and abort is refused as "mid-attunement" until the next restart.
*/
class GearStationRereadBytesTest extends GearStationCoverageSupport {

@BeforeEach
void rereadCopiesSerializeDifferently() {
codec
.when(() -> GearItems.item(any()))
.thenAnswer(
call -> {
var stored = snapshots.get(new String((byte[]) call.getArgument(0), StandardCharsets.UTF_8));
if (stored == null) throw new IllegalArgumentException("Unknown fixture bytes");
var copy = stored.clone();
var meta = copy.getItemMeta();
if (meta.getLore() == null) meta.setLore(List.of("re-read"));
copy.setItemMeta(meta);
return copy;
});
}

@Test
void freshDepositAndChargeResultStayUsableBeforeARestart() throws Exception {
var location = station();
var player = server.addPlayer();
var account = player.getUniqueId();
characters.sessions.put(account, new CharacterSession(characters.ref(account), 1));
var fresh = item();
assertFalse(java.util.Arrays.equals(GearItems.bytes(fresh), GearItems.bytes(GearItems.item(GearItems.bytes(fresh)))));

var placed = GearStationStore.occupy(location, fresh, account, Map.of());
assertNotNull(placed);
assertDoesNotThrow(() -> GearStationStore.expected(location, placed));
assertEquals(List.of("re-read"), placed.getItem().getItemMeta().getLore());

assertTrue(GearStationStore.update(location, placed, attuned()));
assertTrue(GearStationStore.isAttuned(placed.getItem()));
assertDoesNotThrow(() -> GearStationStore.expected(location, placed));

ItemStack released = GearStationStore.takeForAbort(location);
assertNotNull(released);
assertTrue(GearStationStore.acknowledgeRelease(location, placed.generation()));
}
}
Loading