Skip to content

Commit 0b5fa41

Browse files
authored
Merge pull request #38 from ez-plugins/fix/spigot-compat
Spigot compatibility - safe YAML item loading and Base64 serialzation
2 parents b0325ef + edb62b5 commit 0b5fa41

10 files changed

Lines changed: 519 additions & 20 deletions

File tree

.github/workflows/release.yml

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -146,8 +146,7 @@ jobs:
146146
${{ github.event.inputs.prerelease == 'true' && 'beta' || 'release' }}
147147
changelog: ${{ steps.changelog.outputs.notes }}
148148
dependencies: |
149-
placeholder-api(optional)
150-
vault(optional)
149+
placeholderapi(optional)
151150
teams-api(optional)
152151
discordsrv(optional)
153152
ezshops(optional)

.github/workflows/spigot-smoke.yml

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
name: Spigot Smoke
2+
3+
on:
4+
push:
5+
pull_request:
6+
7+
# Confirm that the YAML storage layer is fully functional on Spigot servers.
8+
# The smoke tests exercise:
9+
# - ItemStackSerializer Base64 round-trips
10+
# - YamlAuctionHistoryStorage: fresh install + item round-trip
11+
# - YamlAuctionStorage: fresh install + listing / order / returns round-trips
12+
#
13+
# These tests specifically guard against the "Material cannot be null" ERROR
14+
# that occurred when item data written by a Paper server was loaded on Spigot
15+
# (the Bukkit YAML constructor auto-deserialized items before our code could
16+
# handle them). The new item-data (Base64) format avoids the auto-deserialization
17+
# entirely.
18+
19+
jobs:
20+
spigot-smoke:
21+
name: YAML item storage round-trip (Spigot compat)
22+
runs-on: ubuntu-latest
23+
24+
steps:
25+
- name: Checkout
26+
uses: actions/checkout@v4
27+
28+
- name: Set up JDK 21
29+
uses: actions/setup-java@v4
30+
with:
31+
distribution: temurin
32+
java-version: '21'
33+
cache: maven
34+
35+
- name: Run smoke tests
36+
run: |
37+
mvn -B test \
38+
-Dtest="com.skyblockexp.ezauction.storage.yaml.YamlAuctionStorageSmokeTest" \
39+
-DfailIfNoTests=true
40+
41+
- name: Upload test reports on failure
42+
if: failure()
43+
uses: actions/upload-artifact@v4
44+
with:
45+
name: spigot-smoke-reports
46+
path: target/surefire-reports/

CHANGELOG.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,15 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). Version
88

99
---
1010

11+
## [2.2.1] - 2026-05-17
12+
13+
### Fixed
14+
15+
- **Spigot compatibility**`ItemStack` data is now stored as a Base64-encoded byte stream (`item-data` key) via `BukkitObjectOutputStream` instead of YAML-native serialization. This eliminates the `[ERROR] Material cannot be null` log entries that appeared during startup when loading YAML files written by a Paper 1.21+ server on a Spigot server. Existing files are migrated to the new format automatically on the next save cycle; the old `item` key is still read as a fallback during the transition.
16+
- **Smoke test coverage** – Added `YamlAuctionStorageSmokeTest` (8 tests) and a dedicated `spigot-smoke.yml` GitHub Actions workflow to guard this code path in CI.
17+
18+
---
19+
1120
## [2.2.0] - 2026-05-11
1221

1322
### Added

listings/bbcode.txt

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,11 @@
44
[SIZE=2]Paper / Spigot / Purpur / Bukkit 1.7 - 1.21.* • Vault economy • YAML fallback + MySQL • English/Dutch/Spanish/Chinese menus included • PlaceholderAPI stats
55
[COLOR=#ff6600]Requires Vault and a compatible economy plugin to process currency.[/COLOR][/SIZE][/CENTER]
66

7+
[SIZE=4][B]What's New — v2.2.1[/B][/SIZE]
8+
[LIST]
9+
[*][B][COLOR=#55ff55]Fixed[/COLOR] Spigot compatibility[/B] – Item data is now stored as a Base64 byte stream ([icode]item-data[/icode] key via [icode]BukkitObjectOutputStream[/icode]) instead of YAML-native serialization. This removes the [icode][ERROR] Material cannot be null[/icode] startup errors that appeared when loading YAML data written by a Paper 1.21+ server on a Spigot server. Existing saves migrate automatically on the next save cycle.
10+
[/LIST]
11+
712
[SIZE=4][B]Why EzAuction?[/B][/SIZE]
813
[LIST]
914
[*][B]Menu-first trading[/B] – Paginated browsers, sell/order editors, and confirmation popups keep every transaction guided and click-friendly.
@@ -17,6 +22,8 @@
1722
[*][B]Offline-safe returns[/B] – Expired or undeliverable items go to a claimable stash with login reminders and `/auction claim` recovery.
1823
[*][B]Rank-aware limits[/B] – Plug in a custom `AuctionListingLimitResolver` service to scale slot caps with ranks, islands, or progression data.
1924
[*][B]Smart value overlays[/B] – Surface configured price hints or pipe in [URL='https://www.spigotmc.org/resources/1-21-%E2%9A%A0%EF%B8%8F-ezshops-%E2%9A%A0%EF%B8%8F-dynamic-pricing-gui-player-shops-sell-hand-inv.129780/']EzShops [/URL]buy/sell estimates directly into menu lore.[IMG]https://i.ibb.co/fzcXWVjq/ez-auction-shop-pricing.png[/IMG]
25+
[*][B]Team auctions[/B] – [URL='https://modrinth.com/plugin/teams-api']TeamsAPI[/URL] integration lets team members co-list and co-claim auctions, perfect for faction and skyblock economies.
26+
[IMG]https://i.ibb.co/vCYzR3R6/image.png[/IMG]
2027
[/LIST]
2128
[SIZE=4][B]Feature Highlights[/B][/SIZE]
2229
[LIST]
@@ -109,6 +116,13 @@ holograms:
109116
[*][B]Curate from the live menu[/B] – The Live Auctions GUI shows queue position, seller names, price summaries, and quick actions to refresh or jump back into the main browser.
110117
[/LIST]
111118

119+
[SIZE=4][B]Team Auctions[/B][/SIZE]
120+
[LIST]
121+
[*][B]Scope listings to your team[/B] – Enable [icode]team-auctions.enabled[/icode] in [icode]auction.yml[/icode] so sellers can post under [icode]/auction team sell[/icode] and only members of the seller's team can see and purchase those listings.
122+
[*][B]Private team browser[/B] – [icode]/auction team[/icode] opens a dedicated tab showing only your team's active listings; the tab is hidden entirely when team auctions are disabled or TeamsAPI is absent.
123+
[*][B]Optional soft-dependency[/B] – Powered by the optional [URL='https://modrinth.com/plugin/teams-api']TeamsAPI[/URL] plugin. Standard auctions continue to work normally without it.
124+
[/LIST]
125+
112126
[B]Commands & Permissions[/B]
113127
[table]
114128
[tr][th]Command[/th][th]Description[/th][th]Permission[/th][/tr]
@@ -118,6 +132,8 @@ holograms:
118132
[tr][td]/auction cancel [id][/td][td]Review and cancel active listings or buy orders.[/td][td]ezauction.auction[/td][/tr]
119133
[tr][td]/auction history [buy|sell][/td][td]Show the latest transactions for each category.[/td][td]ezauction.auction.history[/td][/tr]
120134
[tr][td]/auction live[/td][td]Preview upcoming live auction broadcasts and refresh the queue.[/td][td]ezauction.auction.live[/td][/tr]
135+
[tr][td]/auction team[/td][td]Browse team-scoped listings (hidden when disabled or TeamsAPI absent).[/td][td]ezauction.auction.team[/td][/tr]
136+
[tr][td]/auction team sell[/td][td]List the held item as a team auction visible only to your team members.[/td][td]ezauction.auction.team.sell[/td][/tr]
121137
[tr][td]/auction claim[/td][td]Withdraw stored return items.[/td][td]ezauction.auction[/td][/tr]
122138
[tr][td]/auction search <query>[/td][td]Open the auction browser filtered by item name, enchantment, or keyword.[/td][td]ezauction.auction.search[/td][/tr]
123139
[tr][td]/auctionhologram <create|remove|list> [args][/td][td]Manage auction holograms: create, remove, or list in-world auction displays.[/td][td]ezauction.hologram.manage[/td][/tr]

listings/markdown.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,16 @@ Whether you're running a survival, skyblock, or factions server, EzAuction gives
1212

1313
> **Requires Vault and a compatible economy plugin such as [EzEconomy](https://modrinth.com/plugin/ezeconomy) to process currency.**
1414
15+
## What's New — v2.2.1
16+
17+
- **Fixed: Spigot compatibility** – Item data is now stored as a Base64 byte stream (`item-data` key via `BukkitObjectOutputStream`) instead of YAML-native serialization. This removes the `[ERROR] Material cannot be null` startup errors that occurred when loading YAML data written by a Paper 1.21+ server on a Spigot server. Existing saves migrate to the new format automatically on the next save cycle.
18+
1519
## Why Use EzAuction?
1620
- **Menu-first trading** – Paginated browsers, sell/order editors, and confirmation popups keep every transaction guided and click-friendly.
1721
- **Listings + buy orders** – Run timed sales, reserve balance for buy requests, and fulfill either side straight from the GUI or command flow.
1822
- **Team auctions**[TeamsAPI](https://modrinth.com/plugin/teams-api) integration lets team members co-list and co-claim auctions, perfect for faction and skyblock economies.
1923

20-
![Team Auction GUI](TEAM_AUCTION_SCREENSHOT_URL)
24+
![Team Auction GUI](https://i.ibb.co/vCYzR3R6/image.png)
2125

2226
- **Async persistence & fallbacks** – Listings and history write through dedicated executors, automatically falling back to YAML storage if MySQL is unreachable.
2327
- **Offline-safe returns** – Expired or undeliverable items go to a claimable stash with login reminders and `/auction claim` recovery.

pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
<groupId>com.skyblockexp</groupId>
88
<artifactId>ezauction</artifactId>
9-
<version>2.2.0</version>
9+
<version>2.2.1</version>
1010
<name>EzAuction Plugin</name>
1111
<description>Standalone auction house plugin for many of your advanced auction features.</description>
1212
<packaging>jar</packaging>
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
package com.skyblockexp.ezauction.storage.yaml;
2+
3+
import org.bukkit.configuration.InvalidConfigurationException;
4+
import org.bukkit.configuration.file.YamlConfiguration;
5+
import org.bukkit.inventory.ItemStack;
6+
import org.bukkit.util.io.BukkitObjectInputStream;
7+
import org.bukkit.util.io.BukkitObjectOutputStream;
8+
9+
import java.io.ByteArrayInputStream;
10+
import java.io.ByteArrayOutputStream;
11+
import java.io.File;
12+
import java.io.IOException;
13+
import java.nio.file.Files;
14+
import java.util.Base64;
15+
16+
/**
17+
* Encodes and decodes {@link ItemStack} instances to and from a Base64 byte string
18+
* using {@link BukkitObjectOutputStream}.
19+
*
20+
* <p>Unlike storing items directly in a {@link org.bukkit.configuration.file.YamlConfiguration}
21+
* via {@code section.set("item", itemStack)}, this format is stored as a plain YAML string
22+
* scalar. Bukkit's {@code YamlConstructor} therefore does <em>not</em> attempt to
23+
* auto-deserialize it during file loading, which eliminates the
24+
* {@code Material cannot be null} {@code ERROR} log entries that occur when item data was
25+
* written on a Paper server and later loaded on a Spigot server (or vice-versa).
26+
*
27+
* <p>Requires a running Bukkit server (i.e. must not be called before {@code onEnable}).
28+
*/
29+
final class ItemStackSerializer {
30+
31+
private ItemStackSerializer() {}
32+
33+
/**
34+
* Serialises {@code item} to a Base64 string.
35+
*
36+
* @param item the item to serialise; must not be {@code null}
37+
* @return a non-null, non-empty Base64 string
38+
* @throws IOException if the item cannot be serialised
39+
*/
40+
static String serialize(ItemStack item) throws IOException {
41+
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
42+
try (BukkitObjectOutputStream out = new BukkitObjectOutputStream(bytes)) {
43+
out.writeObject(item);
44+
}
45+
return Base64.getEncoder().encodeToString(bytes.toByteArray());
46+
}
47+
48+
/**
49+
* Deserialises an {@link ItemStack} from a Base64 string produced by
50+
* {@link #serialize(ItemStack)}.
51+
*
52+
* @param base64 the Base64 string to deserialise
53+
* @return the restored {@link ItemStack}
54+
* @throws IOException if the byte data cannot be read
55+
* @throws ClassNotFoundException if the item class cannot be resolved
56+
*/
57+
static ItemStack deserialize(String base64) throws IOException, ClassNotFoundException {
58+
byte[] data = Base64.getDecoder().decode(base64);
59+
try (BukkitObjectInputStream in = new BukkitObjectInputStream(new ByteArrayInputStream(data))) {
60+
return (ItemStack) in.readObject();
61+
}
62+
}
63+
64+
/**
65+
* Loads a YAML {@link File} safely by stripping
66+
* {@code ==: org.bukkit.inventory.ItemStack} type-tag lines from the raw text
67+
* before handing it to {@link YamlConfiguration#loadFromString}.
68+
*
69+
* <p>Bukkit's {@code YamlConstructor} auto-deserializes any map node tagged
70+
* {@code ==: org.bukkit.inventory.ItemStack} during file loading. On Spigot this
71+
* fails with {@code Material cannot be null} whenever the data was originally
72+
* written by a Paper 1.21+ server. Removing those lines turns the node into a
73+
* plain map; {@code getItemStack()} then returns {@code null} (handled gracefully
74+
* by the callers), and no ERROR is logged.
75+
*
76+
* @param file the YAML file to load; need not exist
77+
* @return a populated {@link YamlConfiguration}, or an empty one if the file does
78+
* not exist or cannot be parsed
79+
*/
80+
static YamlConfiguration loadSafe(File file) {
81+
YamlConfiguration config = new YamlConfiguration();
82+
if (file == null || !file.exists()) {
83+
return config;
84+
}
85+
try {
86+
String raw = Files.readString(file.toPath());
87+
// Replace Bukkit YAML type-tag lines with a YAML comment so the
88+
// YamlConstructor never attempts platform-specific deserialization.
89+
raw = raw.replace("==: org.bukkit.inventory.ItemStack", "# legacy-item-tag stripped");
90+
config.loadFromString(raw);
91+
} catch (IOException | InvalidConfigurationException ignored) {
92+
// Return the empty config; callers treat missing sections as empty data.
93+
}
94+
return config;
95+
}
96+
}

src/main/java/com/skyblockexp/ezauction/storage/yaml/YamlAuctionHistoryStorage.java

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@ private Map<UUID, Deque<AuctionTransactionHistoryEntry>> loadAllUnlocked() {
123123
if (historyFile == null || !historyFile.exists()) {
124124
return entries;
125125
}
126-
YamlConfiguration configuration = YamlConfiguration.loadConfiguration(historyFile);
126+
YamlConfiguration configuration = ItemStackSerializer.loadSafe(historyFile);
127127
ConfigurationSection historySection = configuration.getConfigurationSection("history");
128128
if (historySection == null) {
129129
return entries;
@@ -199,7 +199,19 @@ private AuctionTransactionHistoryEntry loadEntry(ConfigurationSection section) {
199199
}
200200
}
201201
String counterpartName = section.getString("counterpart-name");
202-
ItemStack item = section.getItemStack("item");
202+
ItemStack item = null;
203+
String itemData = section.getString("item-data");
204+
if (itemData != null && !itemData.isEmpty()) {
205+
try {
206+
item = ItemStackSerializer.deserialize(itemData);
207+
} catch (Exception e) {
208+
plugin.getLogger().log(Level.WARNING, "Failed to deserialize item from auction history entry.", e);
209+
}
210+
} else {
211+
// Legacy: YAML-serialised ItemStack written before v2.2.1 or by a Paper server.
212+
// Paper-written data may not be deserializable on Spigot; the item will be null.
213+
item = section.getItemStack("item");
214+
}
203215
if (item != null) {
204216
item = item.clone();
205217
}
@@ -223,7 +235,12 @@ private void writeEntry(AuctionTransactionHistoryEntry entry, ConfigurationSecti
223235
section.set("counterpart-name", entry.counterpartName());
224236
}
225237
if (entry.item() != null) {
226-
section.set("item", entry.item());
238+
try {
239+
section.set("item-data", ItemStackSerializer.serialize(entry.item()));
240+
} catch (IOException e) {
241+
plugin.getLogger().log(Level.WARNING,
242+
"Failed to serialize item for auction history entry; it will not be persisted.", e);
243+
}
227244
}
228245
}
229246

0 commit comments

Comments
 (0)